From 0b8a3d2d11f4039d08588147dd4a15fc17617450 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 10 Oct 2021 13:53:09 +0000 Subject: [PATCH 0001/2451] [CI] Updating repo.json for refs/tags/0.4.4.6 --- repo.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 49d39441..6d282045 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.4.5", - "TestingAssemblyVersion": "0.4.4.5", + "AssemblyVersion": "0.4.4.6", + "TestingAssemblyVersion": "0.4.4.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -13,9 +13,10 @@ "IsTestingExclusive": "False", "DownloadCount": 0, "LastUpdate": 0, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.5/Penumbra.zip", + "LoadPriority": 69420, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b6304d43db7040ab13b8de7f3fee625963ce1f3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Oct 2021 11:50:59 +0200 Subject: [PATCH 0002/2451] Update PlayerWatcher to deal with multiple actors with the same name. --- Penumbra.PlayerWatch/IPlayerWatcher.cs | 2 +- Penumbra.PlayerWatch/PlayerWatchBase.cs | 93 +++++++++++++++++-------- Penumbra.PlayerWatch/PlayerWatcher.cs | 6 +- Penumbra/UI/MenuTabs/TabDebug.cs | 11 ++- 4 files changed, 75 insertions(+), 37 deletions(-) diff --git a/Penumbra.PlayerWatch/IPlayerWatcher.cs b/Penumbra.PlayerWatch/IPlayerWatcher.cs index d6e077c5..8a884bc1 100644 --- a/Penumbra.PlayerWatch/IPlayerWatcher.cs +++ b/Penumbra.PlayerWatch/IPlayerWatcher.cs @@ -26,6 +26,6 @@ namespace Penumbra.PlayerWatch public void RemovePlayerFromWatch( string playerName ); public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); - public IEnumerable< (string, CharacterEquipment) > WatchedPlayers(); + public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers(); } } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index 6c203151..c9c51727 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -11,21 +11,33 @@ using Penumbra.GameData.Structs; namespace Penumbra.PlayerWatch { + internal readonly struct WatchedPlayer + { + public readonly Dictionary< uint, CharacterEquipment > FoundActors; + public readonly HashSet< PlayerWatcher > RegisteredWatchers; + + public WatchedPlayer( PlayerWatcher watcher ) + { + FoundActors = new Dictionary< uint, CharacterEquipment >(4); + RegisteredWatchers = new HashSet< PlayerWatcher >{ watcher }; + } + } + internal class PlayerWatchBase : IDisposable { public const int GPosePlayerIdx = 201; public const int GPoseTableEnd = GPosePlayerIdx + 48; private const int ObjectsPerFrame = 32; - private readonly Framework _framework; - private readonly ClientState _clientState; - private readonly ObjectTable _objects; - internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); - internal readonly Dictionary< string, (CharacterEquipment, HashSet< PlayerWatcher >) > Equip = new(); - private int _frameTicker; - private bool _inGPose; - private bool _enabled; - private bool _cancel; + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objects; + internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); + internal readonly Dictionary< string, WatchedPlayer > Equip = new(); + private int _frameTicker; + private bool _inGPose; + private bool _enabled; + private bool _cancel; internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) { @@ -47,9 +59,12 @@ namespace Penumbra.PlayerWatch { if( RegisteredWatchers.Remove( watcher ) ) { - foreach( var items in Equip.Values ) + foreach( var (key, value) in Equip.ToArray() ) { - items.Item2.Remove( watcher ); + if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 ) + { + Equip.Remove( key ); + } } } @@ -68,12 +83,16 @@ namespace Penumbra.PlayerWatch } } + private static uint GetId( GameObject actor ) + => actor.ObjectId ^ actor.OwnerId; + internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) { + var name = actor.Name.ToString(); var equipment = new CharacterEquipment( actor ); - if( Equip.ContainsKey( actor.Name.ToString() ) ) + if (Equip.TryGetValue( name, out var watched )) { - Equip[ actor.Name.ToString() ] = ( equipment, Equip[ actor.Name.ToString() ].Item2 ); + watched.FoundActors[ GetId( actor ) ] = equipment; } return equipment; @@ -83,11 +102,11 @@ namespace Penumbra.PlayerWatch { if( Equip.TryGetValue( playerName, out var items ) ) { - items.Item2.Add( watcher ); + items.RegisteredWatchers.Add( watcher ); } else { - Equip[ playerName ] = ( new CharacterEquipment(), new HashSet< PlayerWatcher > { watcher } ); + Equip[ playerName ] = new WatchedPlayer( watcher ); } } @@ -95,8 +114,7 @@ namespace Penumbra.PlayerWatch { if( Equip.TryGetValue( playerName, out var items ) ) { - items.Item2.Remove( watcher ); - if( items.Item2.Count == 0 ) + if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 ) { Equip.Remove( playerName ); } @@ -140,7 +158,7 @@ namespace Penumbra.PlayerWatch _cancel = true; foreach( var kvp in Equip ) { - kvp.Value.Item1.Clear(); + kvp.Value.FoundActors.Clear(); } _frameTicker = 0; @@ -167,7 +185,7 @@ namespace Penumbra.PlayerWatch if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) { - TriggerEvents( watcher.Item2, ( Character )player ); + TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); } } } @@ -184,7 +202,7 @@ namespace Penumbra.PlayerWatch var a = _objects[ i ]; if( a == null ) { - return CharacterFactory.Convert( player); + return CharacterFactory.Convert( player ); } if( a.Name == player.Name ) @@ -193,14 +211,14 @@ namespace Penumbra.PlayerWatch } } - return CharacterFactory.Convert(player)!; + return CharacterFactory.Convert( player )!; } - private bool TryGetPlayer( GameObject gameObject, out (CharacterEquipment, HashSet< PlayerWatcher >) equip ) + private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) { - equip = default; + watch = default; var name = gameObject.Name.ToString(); - return name.Length != 0 && Equip.TryGetValue( name, out equip ); + return name.Length != 0 && Equip.TryGetValue( name, out watch ); } private static bool InvalidObjectKind( ObjectKind kind ) @@ -217,11 +235,17 @@ namespace Penumbra.PlayerWatch private GameObject? GetNextObject() { if( _frameTicker == GPosePlayerIdx - 1 ) + { _frameTicker = GPoseTableEnd; + } else if( _frameTicker == _objects.Length - 1 ) + { _frameTicker = 0; + } else + { ++_frameTicker; + } return _objects[ _frameTicker ]; } @@ -247,9 +271,9 @@ namespace Penumbra.PlayerWatch for( var i = 0; i < ObjectsPerFrame; ++i ) { var actor = GetNextObject(); - if( actor == null - || InvalidObjectKind(actor.ObjectKind) - || !TryGetPlayer( actor, out var equip ) ) + if( actor == null + || InvalidObjectKind( actor.ObjectKind ) + || !TryGetPlayer( actor, out var watch ) ) { continue; } @@ -266,11 +290,20 @@ namespace Penumbra.PlayerWatch continue; } - PluginLog.Verbose( "Comparing Gear for {PlayerName} at {Address}...", character.Name, character.Address ); - if( !equip.Item1.CompareAndUpdate( character ) ) + var id = GetId( character ); + PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address); + if( !watch.FoundActors.TryGetValue( id, out var equip ) ) { - TriggerEvents( equip.Item2, character ); + equip = new CharacterEquipment( character ); + watch.FoundActors[ id ] = equip; + TriggerEvents( watch.RegisteredWatchers, character ); } + else if (!equip.CompareAndUpdate( character )) + { + TriggerEvents( watch.RegisteredWatchers, character ); + } + + break; // Only one comparison per frame. } } } diff --git a/Penumbra.PlayerWatch/PlayerWatcher.cs b/Penumbra.PlayerWatch/PlayerWatcher.cs index 817d368a..9eab7134 100644 --- a/Penumbra.PlayerWatch/PlayerWatcher.cs +++ b/Penumbra.PlayerWatch/PlayerWatcher.cs @@ -86,12 +86,12 @@ namespace Penumbra.PlayerWatch return _playerWatch!.UpdatePlayerWithoutEvent( actor ); } - public IEnumerable< (string, CharacterEquipment) > WatchedPlayers() + public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers() { CheckValidity(); return _playerWatch!.Equip - .Where( kvp => kvp.Value.Item2.Contains( this ) ) - .Select( kvp => ( kvp.Key, kvp.Value.Item1 ) ); + .Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) ) + .Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) ); } } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index baf36b5b..98a920de 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -9,6 +9,7 @@ using Dalamud.Game.ClientState.Objects.Types; using ImGuiNET; using Penumbra.Api; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop; using Penumbra.Meta; @@ -28,13 +29,14 @@ namespace Penumbra.UI } var players = Penumbra.PlayerWatcher.WatchedPlayers().ToArray(); - if( !players.Any() ) + var count = players.Sum( s => Math.Max(1, s.Item2.Length) ); + if( count == 0 ) { return; } if( !ImGui.BeginTable( "##ObjectTable", 13, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 4 * players.Length ) ) ) + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 4 * count ) ) ) { return; } @@ -43,7 +45,10 @@ namespace Penumbra.UI var identifier = GameData.GameData.GetIdentifier(); - foreach( var (actor, equip) in players ) + foreach( var (actor, equip) in players.SelectMany( kvp => kvp.Item2.Any() + ? kvp.Item2 + .Select( x => ( $"{kvp.Item1} ({x.Item1})", x.Item2 ) ) + : new[] { ( kvp.Item1, new CharacterEquipment() ) } ) ) { // @formatter:off ImGui.TableNextRow(); From 0c135b574b2045f279865d1852f6474e41aab769 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 11 Oct 2021 09:52:42 +0000 Subject: [PATCH 0003/2451] [CI] Updating repo.json for refs/tags/0.4.4.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6d282045..84ba52cc 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.4.6", - "TestingAssemblyVersion": "0.4.4.6", + "AssemblyVersion": "0.4.4.7", + "TestingAssemblyVersion": "0.4.4.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From be404c79b39a29b66f7f086e763e9259a310a58e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Oct 2021 13:05:47 +0200 Subject: [PATCH 0004/2451] Allow Retainers to be watched with PlayerWatcher. --- Penumbra.PlayerWatch/CharacterFactory.cs | 1 + Penumbra.PlayerWatch/PlayerWatchBase.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra.PlayerWatch/CharacterFactory.cs b/Penumbra.PlayerWatch/CharacterFactory.cs index 806efafe..9c18c29e 100644 --- a/Penumbra.PlayerWatch/CharacterFactory.cs +++ b/Penumbra.PlayerWatch/CharacterFactory.cs @@ -42,6 +42,7 @@ namespace Penumbra.PlayerWatch { ObjectKind.BattleNpc => Character( actor.Address ), ObjectKind.Companion => Character( actor.Address ), + ObjectKind.Retainer => Character( actor.Address ), ObjectKind.EventNpc => Character( actor.Address ), _ => null, }, diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index c9c51727..3045af88 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -228,6 +228,7 @@ namespace Penumbra.PlayerWatch ObjectKind.BattleNpc => false, ObjectKind.EventNpc => false, ObjectKind.Player => false, + ObjectKind.Retainer => false, _ => true, }; } From 8ee15e4ea255d5e2037a24a1f6db05d98b6e836f Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 12 Oct 2021 11:07:30 +0000 Subject: [PATCH 0005/2451] [CI] Updating repo.json for refs/tags/0.4.4.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 84ba52cc..05de9355 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.4.7", - "TestingAssemblyVersion": "0.4.4.7", + "AssemblyVersion": "0.4.4.8", + "TestingAssemblyVersion": "0.4.4.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 39fbf128c2fcf710bac0ab734652ca79eb3d660c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Oct 2021 16:08:43 +0200 Subject: [PATCH 0006/2451] Change handling of setting root or temp path to accept button clicks. --- Penumbra/UI/MenuTabs/TabSettings.cs | 44 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index b52eeb1e..336d4443 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Numerics; using System.Text.RegularExpressions; using Dalamud.Interface; @@ -38,47 +39,49 @@ namespace Penumbra.UI private readonly SettingsInterface _base; private readonly Configuration _config; private bool _configChanged; + private string _newModDirectory; + private string _newTempDirectory; + public TabSettings( SettingsInterface ui ) { - _base = ui; - _config = Penumbra.Config; - _configChanged = false; + _base = ui; + _config = Penumbra.Config; + _configChanged = false; + _newModDirectory = _config.ModDirectory; + _newTempDirectory = _config.TempDirectory; } - private static bool DrawPressEnterWarning( float? width = null ) + private static bool DrawPressEnterWarning( string old, float? width = null ) { - const uint red = 0xFF202080; - using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ) - .Push( ImGuiCol.ButtonActive, red ) - .Push( ImGuiCol.ButtonHovered, red ); - var w = Vector2.UnitX * ( width ?? ImGui.CalcItemWidth() ); - return ImGui.Button( "Press Enter to Save", w ); + const uint red = 0xFF202080; + using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ); + var w = Vector2.UnitX * ( width ?? ImGui.CalcItemWidth() ); + return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); } private void DrawRootFolder() { - var basePath = _config.ModDirectory; - var save = ImGui.InputText( LabelRootFolder, ref basePath, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - if( _config.ModDirectory == basePath ) + var save = ImGui.InputText( LabelRootFolder, ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) { return; } - if( save || DrawPressEnterWarning() ) + if( save || DrawPressEnterWarning( _config.ModDirectory ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods( basePath ); + _base._modManager.DiscoverMods( _newModDirectory ); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); + _newModDirectory = _config.ModDirectory; } } private void DrawTempFolder() { - var tempPath = _config.TempDirectory; ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); ImGui.BeginGroup(); - var save = ImGui.InputText( LabelTempFolder, ref tempPath, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + var save = ImGui.InputText( LabelTempFolder, ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); ImGuiCustom.HoverTooltip( "The folder used to store temporary meta manipulation files.\n" + "Leave this blank if you have no reason not to.\n" @@ -100,14 +103,15 @@ namespace Penumbra.UI } ImGui.EndGroup(); - if( tempPath == _config.TempDirectory ) + if( _newTempDirectory == _config.TempDirectory ) { return; } - if( save || DrawPressEnterWarning( 400 ) ) + if( save || DrawPressEnterWarning( _config.TempDirectory, 400 ) ) { - _base._modManager.SetTempDirectory( tempPath ); + _base._modManager.SetTempDirectory( _newTempDirectory ); + _newTempDirectory = _config.TempDirectory; } } From 795d605d3fd2cb8c89370c7c7628e2993f2e298c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Oct 2021 16:09:13 +0200 Subject: [PATCH 0007/2451] Rename eqp entry 14 to DisableBreastPhysics. --- Penumbra.GameData/Structs/EqpEntry.cs | 98 +++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 2e4c0d86..5b54baa8 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -9,23 +9,23 @@ namespace Penumbra.GameData.Structs [Flags] public enum EqpEntry : ulong { - BodyEnabled = 0x00_01ul, - BodyHideWaist = 0x00_02ul, - _2 = 0x00_04ul, - BodyHideGlovesS = 0x00_08ul, - _4 = 0x00_10ul, - BodyHideGlovesM = 0x00_20ul, - BodyHideGlovesL = 0x00_40ul, - BodyHideGorget = 0x00_80ul, - BodyShowLeg = 0x01_00ul, - BodyShowHand = 0x02_00ul, - BodyShowHead = 0x04_00ul, - BodyShowNecklace = 0x08_00ul, - BodyShowBracelet = 0x10_00ul, - BodyShowTail = 0x20_00ul, - _14 = 0x40_00ul, - _15 = 0x80_00ul, - BodyMask = 0xFF_FFul, + BodyEnabled = 0x00_01ul, + BodyHideWaist = 0x00_02ul, + _2 = 0x00_04ul, + BodyHideGlovesS = 0x00_08ul, + _4 = 0x00_10ul, + BodyHideGlovesM = 0x00_20ul, + BodyHideGlovesL = 0x00_40ul, + BodyHideGorget = 0x00_80ul, + BodyShowLeg = 0x01_00ul, + BodyShowHand = 0x02_00ul, + BodyShowHead = 0x04_00ul, + BodyShowNecklace = 0x08_00ul, + BodyShowBracelet = 0x10_00ul, + BodyShowTail = 0x20_00ul, + DisableBreastPhysics = 0x40_00ul, + _15 = 0x80_00ul, + BodyMask = 0xFF_FFul, LegsEnabled = 0x01ul << 16, LegsHideKneePads = 0x02ul << 16, @@ -133,22 +133,22 @@ namespace Penumbra.GameData.Structs { return entry switch { - EqpEntry.BodyEnabled => EquipSlot.Body, - EqpEntry.BodyHideWaist => EquipSlot.Body, - EqpEntry._2 => EquipSlot.Body, - EqpEntry.BodyHideGlovesS => EquipSlot.Body, - EqpEntry._4 => EquipSlot.Body, - EqpEntry.BodyHideGlovesM => EquipSlot.Body, - EqpEntry.BodyHideGlovesL => EquipSlot.Body, - EqpEntry.BodyHideGorget => EquipSlot.Body, - EqpEntry.BodyShowLeg => EquipSlot.Body, - EqpEntry.BodyShowHand => EquipSlot.Body, - EqpEntry.BodyShowHead => EquipSlot.Body, - EqpEntry.BodyShowNecklace => EquipSlot.Body, - EqpEntry.BodyShowBracelet => EquipSlot.Body, - EqpEntry.BodyShowTail => EquipSlot.Body, - EqpEntry._14 => EquipSlot.Body, - EqpEntry._15 => EquipSlot.Body, + EqpEntry.BodyEnabled => EquipSlot.Body, + EqpEntry.BodyHideWaist => EquipSlot.Body, + EqpEntry._2 => EquipSlot.Body, + EqpEntry.BodyHideGlovesS => EquipSlot.Body, + EqpEntry._4 => EquipSlot.Body, + EqpEntry.BodyHideGlovesM => EquipSlot.Body, + EqpEntry.BodyHideGlovesL => EquipSlot.Body, + EqpEntry.BodyHideGorget => EquipSlot.Body, + EqpEntry.BodyShowLeg => EquipSlot.Body, + EqpEntry.BodyShowHand => EquipSlot.Body, + EqpEntry.BodyShowHead => EquipSlot.Body, + EqpEntry.BodyShowNecklace => EquipSlot.Body, + EqpEntry.BodyShowBracelet => EquipSlot.Body, + EqpEntry.BodyShowTail => EquipSlot.Body, + EqpEntry.DisableBreastPhysics => EquipSlot.Body, + EqpEntry._15 => EquipSlot.Body, EqpEntry.LegsEnabled => EquipSlot.Legs, EqpEntry.LegsHideKneePads => EquipSlot.Legs, @@ -210,22 +210,22 @@ namespace Penumbra.GameData.Structs { return entry switch { - EqpEntry.BodyEnabled => "Enabled", - EqpEntry.BodyHideWaist => "Hide Waist", - EqpEntry._2 => "Unknown 2", - EqpEntry.BodyHideGlovesS => "Hide Small Gloves", - EqpEntry._4 => "Unknown 4", - EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", - EqpEntry.BodyHideGlovesL => "Hide Large Gloves", - EqpEntry.BodyHideGorget => "Hide Gorget", - EqpEntry.BodyShowLeg => "Show Legs", - EqpEntry.BodyShowHand => "Show Hands", - EqpEntry.BodyShowHead => "Show Head", - EqpEntry.BodyShowNecklace => "Show Necklace", - EqpEntry.BodyShowBracelet => "Show Bracelet", - EqpEntry.BodyShowTail => "Show Tail", - EqpEntry._14 => "Unknown 14", - EqpEntry._15 => "Unknown 15", + EqpEntry.BodyEnabled => "Enabled", + EqpEntry.BodyHideWaist => "Hide Waist", + EqpEntry._2 => "Unknown 2", + EqpEntry.BodyHideGlovesS => "Hide Small Gloves", + EqpEntry._4 => "Unknown 4", + EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", + EqpEntry.BodyHideGlovesL => "Hide Large Gloves", + EqpEntry.BodyHideGorget => "Hide Gorget", + EqpEntry.BodyShowLeg => "Show Legs", + EqpEntry.BodyShowHand => "Show Hands", + EqpEntry.BodyShowHead => "Show Head", + EqpEntry.BodyShowNecklace => "Show Necklace", + EqpEntry.BodyShowBracelet => "Show Bracelet", + EqpEntry.BodyShowTail => "Show Tail", + EqpEntry.DisableBreastPhysics => "Disable Breast Physics", + EqpEntry._15 => "Unknown 15", EqpEntry.LegsEnabled => "Enabled", EqpEntry.LegsHideKneePads => "Hide Knee Pads", From 4a82e6faf1419ac7cd1bae8455c39347a44c0f26 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Oct 2021 16:12:32 +0200 Subject: [PATCH 0008/2451] Add unsolved conflicts to both mods, add button to auto-generate groups based on folders, misc. fixes. --- Penumbra/Mod/ModCleanup.cs | 42 +++++++++++++++++++ Penumbra/Mods/ModCollectionCache.cs | 18 ++++++-- Penumbra/Mods/ModManager.cs | 4 +- .../TabInstalled/TabInstalledDetails.cs | 13 ++++++ .../TabInstalledDetailsManipulations.cs | 2 +- .../TabInstalled/TabInstalledModPanel.cs | 31 ++++++++++++-- .../TabInstalled/TabInstalledSelector.cs | 4 +- 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index fca6acfd..0026fd99 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -478,5 +479,46 @@ namespace Penumbra.Mod RemoveUselessGroups( meta ); ClearEmptySubDirectories( baseDir ); } + + public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) + { + meta.Groups.Clear(); + ClearEmptySubDirectories( baseDir ); + foreach( var groupDir in baseDir.EnumerateDirectories() ) + { + var group = new OptionGroup + { + GroupName = groupDir.Name, + SelectionType = SelectType.Single, + Options = new List< Option >(), + }; + + foreach( var optionDir in groupDir.EnumerateDirectories() ) + { + var option = new Option + { + OptionDesc = string.Empty, + OptionName = optionDir.Name, + OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), + }; + foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + { + var relPath = new RelPath( file, baseDir ); + var gamePath = new GamePath( file, optionDir ); + option.OptionFiles[ relPath ] = new HashSet< GamePath > { gamePath }; + } + + if( option.OptionFiles.Any() ) + { + group.Options.Add( option ); + } + } + + if( group.Options.Any() ) + { + meta.Groups.Add( groupDir.Name, @group ); + } + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 9914553a..1e4582c6 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -97,6 +97,10 @@ namespace Penumbra.Mods else { mod.Cache.AddConflict( oldMod, gamePath ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority) + { + oldMod.Cache.AddConflict( mod, gamePath ); + } } } @@ -223,6 +227,10 @@ namespace Penumbra.Mods else { mod.Cache.AddConflict( oldMod, swap.Key ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) + { + oldMod.Cache.AddConflict( mod, swap.Key ); + } } } } @@ -231,13 +239,17 @@ namespace Penumbra.Mods { foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) { - if( MetaManipulations.TryGetValue( manip, out var precedingMod ) ) + if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) { - mod.Cache.AddConflict( precedingMod, manip ); + MetaManipulations.ApplyMod( manip, mod ); } else { - MetaManipulations.ApplyMod( manip, mod ); + mod.Cache.AddConflict( oldMod, manip ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) + { + oldMod.Cache.AddConflict( mod, manip ); + } } } } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 4a42d7b6..846d06fb 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -298,10 +298,10 @@ namespace Penumbra.Mods return true; } - public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false ) + public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) { var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ); + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index fd386d50..22aea5c7 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -1,7 +1,10 @@ using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Interface; using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; @@ -198,6 +201,14 @@ namespace Penumbra.UI _base._penumbra.Api.InvokeTooltip( data ); raii.Pop(); } + + if( data is Item it ) + { + var modelId = $"({( ( Quad )it.ModelMain ).A})"; + var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; + ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - offset); + ImGui.TextColored( new Vector4(0.5f, 0.5f, 0.5f, 1 ), modelId ); + } } } @@ -401,6 +412,7 @@ namespace Penumbra.UI continue; } + _fullFilenameList![ i ].selected = false; var relName = _fullFilenameList[ i ].relName; if( defaultIndex >= 0 ) { @@ -428,6 +440,7 @@ namespace Penumbra.UI if( changed ) { + _fullFilenameList = null; _selector.SaveCurrentMod(); // Since files may have changed, we need to recompute effective files. foreach( var collection in _modManager.Collections.Collections.Values diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 1766286f..5239b00f 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -701,7 +701,7 @@ namespace Penumbra.UI { MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, ( ( ImcFile.ImageChangeData )def ).ToInteger() ), diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 2250b80e..bbc2c42a 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -216,6 +216,11 @@ namespace Penumbra.UI if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { Mod.Settings.Enabled = enabled; + if( !enabled ) + { + Mod.Cache.ClearConflicts(); + } + _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); _selector.Cache.TriggerFilterReset(); } @@ -425,7 +430,7 @@ namespace Penumbra.UI { if( ImGui.Button( "Recompute Metadata" ) ) { - _selector.ReloadCurrentMod( true, true ); + _selector.ReloadCurrentMod( true, true, true ); } ImGuiCustom.HoverTooltip( @@ -440,7 +445,7 @@ namespace Penumbra.UI { ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); + _selector.ReloadCurrentMod( true, true, true ); } ImGuiCustom.HoverTooltip( TooltipDeduplicate ); @@ -452,12 +457,28 @@ namespace Penumbra.UI { ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); + _selector.ReloadCurrentMod( true, true, true ); } ImGuiCustom.HoverTooltip( TooltipNormalize ); } + private void DrawAutoGenerateGroupsButton() + { + if( ImGui.Button( "Auto-Generate Groups" ) ) + { + ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod( true, true ); + } + + ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" + + "First subdirectory: Option Group\n" + + "Second subdirectory: Option Name\n" + + "Afterwards: Relative file paths.\n" + + "Experimental - Use at own risk!" ); + } + private void DrawSplitButton() { if( ImGui.Button( "Split Mod" ) ) @@ -487,6 +508,8 @@ namespace Penumbra.UI ImGui.SameLine(); DrawNormalizeButton(); ImGui.SameLine(); + DrawAutoGenerateGroupsButton(); + ImGui.SameLine(); DrawSplitButton(); DrawSortOrder( Mod!.Data, _modManager, _selector ); @@ -498,7 +521,7 @@ namespace Penumbra.UI { using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); - + if( !ret || Mod == null ) { return; diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 5f73a2e9..4e430298 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -452,14 +452,14 @@ namespace Penumbra.UI SetSelection( idx, mod ); } - public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false ) + public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) { if( Mod == null ) { return; } - if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta ) ) + if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) { SelectModOnUpdate( Mod.Data.BasePath.Name ); _base._menu.InstalledTab.ModPanel.Details.ResetState(); From 78bef3dec067fd229f58fcf3e6f8f76579683823 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 18 Oct 2021 14:14:23 +0000 Subject: [PATCH 0009/2451] [CI] Updating repo.json for refs/tags/0.4.4.9 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 05de9355..cd344865 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.4.8", - "TestingAssemblyVersion": "0.4.4.8", + "AssemblyVersion": "0.4.4.9", + "TestingAssemblyVersion": "0.4.4.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b901c9e7444af71664b5c9ce7ee8fbb08fde5402 Mon Sep 17 00:00:00 2001 From: Brenden Reeves Date: Mon, 18 Oct 2021 21:44:55 -0500 Subject: [PATCH 0010/2451] Added Enable and Disable Commands --- .idea/.idea.Penumbra/.idea/indexLayout.xml | 2 +- Penumbra/Penumbra.cs | 38 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.idea/.idea.Penumbra/.idea/indexLayout.xml b/.idea/.idea.Penumbra/.idea/indexLayout.xml index 27ba142e..7b08163c 100644 --- a/.idea/.idea.Penumbra/.idea/indexLayout.xml +++ b/.idea/.idea.Penumbra/.idea/indexLayout.xml @@ -1,6 +1,6 @@ - + diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6ac949d6..6cf1e395 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -180,6 +180,44 @@ namespace Penumbra SettingsInterface.MakeDebugTabVisible(); break; } + case "enable": + { + if( Config.IsEnabled ) + { + Dalamud.Chat.Print("Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable"); + } + else + { + Config.IsEnabled = true; + ObjectReloader.RedrawAll( RedrawType.WithSettings ); + if( Config.EnablePlayerWatch ) + { + Penumbra.PlayerWatcher.SetStatus( true ); + } + Config.Save(); + Dalamud.Chat.Print("Your mods have now been enabled."); + } + break; + } + case "disable": + { + if( !Config.IsEnabled ) + { + Dalamud.Chat.Print("Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"); + } + else + { + Config.IsEnabled = false; + ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); + if( Config.EnablePlayerWatch ) + { + Penumbra.PlayerWatcher.SetStatus( false ); + } + Config.Save(); + Dalamud.Chat.Print("Your mods have now been disabled."); + } + break; + } } return; From 982385ccbb64d7bcb925c1109bb4fd8cdc3190dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Oct 2021 11:30:08 +0200 Subject: [PATCH 0011/2451] Create enable/disable functions and clean up PR. --- .idea/.idea.Penumbra/.idea/indexLayout.xml | 2 +- Penumbra/Penumbra.cs | 90 ++++++++++++++-------- Penumbra/UI/MenuTabs/TabSettings.cs | 9 +-- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/.idea/.idea.Penumbra/.idea/indexLayout.xml b/.idea/.idea.Penumbra/.idea/indexLayout.xml index 7b08163c..27ba142e 100644 --- a/.idea/.idea.Penumbra/.idea/indexLayout.xml +++ b/.idea/.idea.Penumbra/.idea/indexLayout.xml @@ -1,6 +1,6 @@ - + diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6cf1e395..093eaf06 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -90,6 +90,47 @@ namespace Penumbra Ipc = new PenumbraIpc( pluginInterface, Api ); } + public bool Enable() + { + if( Config.IsEnabled ) + { + return false; + } + + Config.IsEnabled = true; + Service< ResidentResources >.Get().ReloadPlayerResources(); + if( Config.EnablePlayerWatch ) + { + PlayerWatcher.SetStatus( true ); + } + + Config.Save(); + ObjectReloader.RedrawAll( RedrawType.WithSettings ); + return true; + } + + public bool Disable() + { + if( !Config.IsEnabled ) + { + return false; + } + + Config.IsEnabled = false; + Service< ResidentResources >.Get().ReloadPlayerResources(); + if( Config.EnablePlayerWatch ) + { + PlayerWatcher.SetStatus( false ); + } + + Config.Save(); + ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); + return true; + } + + public bool SetEnabled( bool enabled ) + => enabled ? Enable() : Disable(); + private void SubscribeItemLinks() { Api.ChangedItemTooltip += it => @@ -149,6 +190,9 @@ namespace Penumbra private void OnCommand( string command, string rawArgs ) { + const string modsEnabled = "Your mods have now been enabled."; + const string modsDisabled = "Your mods have now been disabled."; + var args = rawArgs.Split( new[] { ' ' }, 2 ); if( args.Length > 0 && args[ 0 ].Length > 0 ) { @@ -182,40 +226,24 @@ namespace Penumbra } case "enable": { - if( Config.IsEnabled ) - { - Dalamud.Chat.Print("Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable"); - } - else - { - Config.IsEnabled = true; - ObjectReloader.RedrawAll( RedrawType.WithSettings ); - if( Config.EnablePlayerWatch ) - { - Penumbra.PlayerWatcher.SetStatus( true ); - } - Config.Save(); - Dalamud.Chat.Print("Your mods have now been enabled."); - } + Dalamud.Chat.Print( Enable() + ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" + : modsEnabled ); break; } case "disable": { - if( !Config.IsEnabled ) - { - Dalamud.Chat.Print("Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"); - } - else - { - Config.IsEnabled = false; - ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); - if( Config.EnablePlayerWatch ) - { - Penumbra.PlayerWatcher.SetStatus( false ); - } - Config.Save(); - Dalamud.Chat.Print("Your mods have now been disabled."); - } + Dalamud.Chat.Print( Disable() + ? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" + : modsDisabled ); + break; + } + case "toggle": + { + SetEnabled( !Config.IsEnabled ); + Dalamud.Chat.Print( Config.IsEnabled + ? modsEnabled + : modsDisabled ); break; } } @@ -226,4 +254,4 @@ namespace Penumbra SettingsInterface.FlipVisibility(); } } -} +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 336d4443..f5a34cd1 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -146,14 +146,7 @@ namespace Penumbra.UI var enabled = _config.IsEnabled; if( ImGui.Checkbox( LabelEnabled, ref enabled ) ) { - _config.IsEnabled = enabled; - _configChanged = true; - Service< ResidentResources >.Get().ReloadPlayerResources(); - _base._penumbra.ObjectReloader.RedrawAll( enabled ? RedrawType.WithSettings : RedrawType.WithoutSettings ); - if( _config.EnablePlayerWatch ) - { - Penumbra.PlayerWatcher.SetStatus( enabled ); - } + _base._penumbra.SetEnabled( enabled ); } } From 0c0eeec1580053ed20f9e0203a4ec64deccc24a6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Oct 2021 18:45:22 +0200 Subject: [PATCH 0012/2451] Add some methods and interface to view currently Changed Items. --- Penumbra/Api/IPenumbraApi.cs | 4 ++ Penumbra/Api/PenumbraApi.cs | 31 ++++++++ Penumbra/Api/PenumbraIpc.cs | 30 +++++--- Penumbra/Mods/ModCollectionCache.cs | 51 +++++++++++-- Penumbra/UI/MenuTabs/TabChangedItems.cs | 72 +++++++++++++++++++ .../TabInstalled/TabInstalledDetails.cs | 25 +------ Penumbra/UI/MenuTabs/TabSettings.cs | 1 - Penumbra/UI/SettingsMenu.cs | 6 +- Penumbra/UI/UiHelpers.cs | 39 ++++++++++ 9 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 Penumbra/UI/MenuTabs/TabChangedItems.cs create mode 100644 Penumbra/UI/UiHelpers.cs diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 92d9ef3d..487cd568 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.GameData.Enums; @@ -43,5 +44,8 @@ namespace Penumbra.Api // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile( string gamePath, string characterName ) where T : FileResource; + + // Gets a dictionary of effected items from a collection + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection(string collectionName); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 51360940..5190bc07 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Mods; @@ -131,5 +135,32 @@ namespace Penumbra.Api public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + { + CheckInitialized(); + try + { + var modManager = Service< ModManager >.Get(); + if( !modManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) + { + collection = ModCollection.Empty; + } + + if( collection.Cache != null ) + { + return collection.Cache.ChangedItems; + } + + PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); + return new Dictionary< string, object? >(); + + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + throw; + } + } } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index fba72015..2f77cc61 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin; @@ -18,15 +19,17 @@ namespace Penumbra.Api public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; internal readonly IPenumbraApi Api; @@ -137,6 +140,16 @@ namespace Penumbra.Api { PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } + + try + { + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems.RegisterFunc( api.GetChangedItemsForCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } } public void Dispose() @@ -147,6 +160,7 @@ namespace Penumbra.Api ProviderRedrawAll?.UnregisterAction(); ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); + ProviderGetChangedItems?.UnregisterFunc(); Api.ChangedItemClicked -= OnClick; Api.ChangedItemTooltip -= OnTooltip; } diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 1e4582c6..17fe47b3 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -24,10 +25,20 @@ namespace Penumbra.Mods public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); - public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FileInfo > MissingFiles = new(); - public readonly MetaManager MetaManipulations; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); + public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public readonly HashSet< FileInfo > MissingFiles = new(); + public readonly MetaManager MetaManipulations; + + public IReadOnlyDictionary< string, object? > ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); @@ -52,6 +63,7 @@ namespace Penumbra.Mods SwappedFiles.Clear(); MissingFiles.Clear(); RegisteredFiles.Clear(); + _changedItems.Clear(); foreach( var mod in AvailableMods.Values .Where( m => m.Settings.Enabled ) @@ -65,6 +77,35 @@ namespace Penumbra.Mods AddMetaFiles(); } + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip meta files because IMCs would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet(); + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) + { + identifier.Identify( _changedItems, resolved ); + } + + foreach( var swapped in SwappedFiles.Keys ) + { + identifier.Identify( _changedItems, swapped ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + private void AddFiles( Mod.Mod mod ) { @@ -97,7 +138,7 @@ namespace Penumbra.Mods else { mod.Cache.AddConflict( oldMod, gamePath ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority) + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) { oldMod.Cache.AddConflict( mod, gamePath ); } diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs new file mode 100644 index 00000000..c161b6d9 --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using Penumbra.Mods; +using Penumbra.UI.Custom; +using Penumbra.Util; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabChangedItems + { + private const string LabelTab = "Changed Items"; + private readonly ModManager _modManager; + private readonly SettingsInterface _base; + + private string _filter = string.Empty; + private string _filterLower = string.Empty; + + public TabChangedItems( SettingsInterface ui ) + { + _base = ui; + _modManager = Service< ModManager >.Get(); + } + + public void Draw() + { + var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var count = items.Count + forced.Count; + if( count > 0 && !ImGui.BeginTabItem( LabelTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) + { + _filterLower = _filter.ToLowerInvariant(); + } + + if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndTable ); + + var list = items.AsEnumerable(); + if( forced.Count > 0 ) + { + list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); + } + + if( _filter.Any() ) + { + list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); + } + + foreach( var (name, data) in list ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _base.DrawChangedItem( name, data ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 22aea5c7..b95e0f6f 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -185,30 +185,7 @@ namespace Penumbra.UI raii.Push( ImGui.EndListBox ); foreach( var (name, data) in Mod.Data.ChangedItems ) { - var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; - - if( ret != MouseButton.None ) - { - _base._penumbra.Api.InvokeClick( ret, data ); - } - - if( _base._penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) - { - ImGui.BeginTooltip(); - raii.Push( ImGui.EndTooltip ); - _base._penumbra.Api.InvokeTooltip( data ); - raii.Pop(); - } - - if( data is Item it ) - { - var modelId = $"({( ( Quad )it.ModelMain ).A})"; - var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; - ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - offset); - ImGui.TextColored( new Vector4(0.5f, 0.5f, 0.5f, 1 ), modelId ); - } + _base.DrawChangedItem( name, data ); } } diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index f5a34cd1..8db86756 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -7,7 +7,6 @@ using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; -using Penumbra.GameData.Enums; using Penumbra.Interop; using Penumbra.Mods; using Penumbra.UI.Custom; diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index e46034e8..7a3ec8ab 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -19,9 +19,10 @@ namespace Penumbra.UI private readonly TabSettings _settingsTab; private readonly TabImport _importTab; private readonly TabBrowser _browserTab; + private readonly TabEffective _effectiveTab; + private readonly TabChangedItems _changedItems; internal readonly TabCollections CollectionsTab; internal readonly TabInstalled InstalledTab; - private readonly TabEffective _effectiveTab; public SettingsMenu( SettingsInterface ui ) { @@ -32,6 +33,7 @@ namespace Penumbra.UI InstalledTab = new TabInstalled( _base ); CollectionsTab = new TabCollections( InstalledTab.Selector ); _effectiveTab = new TabEffective(); + _changedItems = new TabChangedItems( _base ); } #if DEBUG @@ -72,7 +74,7 @@ namespace Penumbra.UI { _browserTab.Draw(); InstalledTab.Draw(); - + _changedItems.Draw(); if( Penumbra.Config.ShowAdvanced ) { _effectiveTab.Draw(); diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs new file mode 100644 index 00000000..47c0e59b --- /dev/null +++ b/Penumbra/UI/UiHelpers.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; +using Penumbra.UI.Custom; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + internal void DrawChangedItem( string name, object? data ) + { + var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; + + if( ret != MouseButton.None ) + { + _penumbra.Api.InvokeClick( ret, data ); + } + + if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) + { + ImGui.BeginTooltip(); + using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); + _penumbra.Api.InvokeTooltip( data ); + } + + if( data is Item it ) + { + var modelId = $"({( ( Quad )it.ModelMain ).A})"; + var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; + ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); + ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); + } + } + } +} \ No newline at end of file From 572f4f5e6b4a0fd922f173a37d0f7aa7cd6dd10f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Oct 2021 18:47:17 +0200 Subject: [PATCH 0013/2451] Add filters to the effective files tab. --- Penumbra/UI/MenuTabs/TabEffective.cs | 205 ++++++++++++++++++++------- 1 file changed, 156 insertions(+), 49 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 8d46d86a..51143c4d 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -1,9 +1,9 @@ +using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; using Penumbra.GameData.Util; -using Penumbra.Meta; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -17,14 +17,21 @@ namespace Penumbra.UI private const string LabelTab = "Effective Changes"; private readonly ModManager _modManager; + private string _gamePathFilter = string.Empty; + private string _gamePathFilterLower = string.Empty; + private string _filePathFilter = string.Empty; + private string _filePathFilterLower = string.Empty; + private readonly float _leftTextLength = - ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X + 40; + ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; + + private float _arrowLength = 0; public TabEffective() => _modManager = Service< ModManager >.Get(); - private static void DrawFileLine( FileInfo file, GamePath path ) + private static void DrawLine( string path, string name ) { ImGui.TableNextColumn(); ImGuiCustom.CopyOnClickSelectable( path ); @@ -32,18 +39,92 @@ namespace Penumbra.UI ImGui.TableNextColumn(); ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.SameLine(); - ImGuiCustom.CopyOnClickSelectable( file.FullName ); + ImGuiCustom.CopyOnClickSelectable( name ); } - private static void DrawManipulationLine( MetaManipulation manip, Mod.Mod mod ) + private void DrawFilters() { - ImGui.TableNextColumn(); - ImGui.Selectable( manip.IdentifierString() ); + if( _arrowLength == 0 ) + { + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; + } - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.SameLine(); - ImGui.Selectable( mod.Data.Meta.Name ); + ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); + if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) + { + _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + } + + ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + { + _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + } + } + + private bool CheckFilters( KeyValuePair< GamePath, FileInfo > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( (string, string, string) kvp ) + { + if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + } + + private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + { + void DrawFileLines( ModCollectionCache cache ) + { + foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) + { + DrawLine( gp, fp.FullName ); + } + + foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) + { + DrawLine( gp, fp ); + } + + foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations + .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + } + + if( active != null ) + { + DrawFileLines( active ); + } + + if( forced != null ) + { + DrawFileLines( forced ); + } } public void Draw() @@ -55,57 +136,82 @@ namespace Penumbra.UI using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + DrawFilters(); + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; var activeCollection = _modManager.Collections.ActiveCollection.Cache; var forcedCollection = _modManager.Collections.ForcedCollection.Cache; - var (activeResolved, activeMeta) = activeCollection != null - ? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count ) - : ( 0, 0 ); - var (forcedResolved, forcedMeta) = forcedCollection != null - ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count ) - : ( 0, 0 ); - - var lines = activeResolved + forcedResolved + activeMeta + forcedMeta; - ImGuiListClipperPtr clipper; - unsafe + var (activeResolved, activeSwap, activeMeta) = activeCollection != null + ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null + ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; + if( totalLines == 0 ) { - clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); + return; } - clipper.Begin( lines ); - if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) { raii.Push( ImGui.EndTable ); - ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength ); - while( clipper.Step() ) + ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); + + if( _filePathFilter.Any() || _gamePathFilter.Any() ) { - for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) + DrawFilteredRows( activeCollection, forcedCollection ); + } + else + { + ImGuiListClipperPtr clipper; + unsafe { - var row = actualRow; - ImGui.TableNextRow(); - if( row < activeResolved ) + clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); + } + + clipper.Begin( totalLines ); + + + while( clipper.Step() ) + { + for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) { - var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); - DrawFileLine( file, gamePath ); - } - else if( ( row -= activeResolved ) < forcedResolved ) - { - var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); - DrawFileLine( file, gamePath ); - } - else if( ( row -= forcedResolved ) < activeMeta ) - { - var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawManipulationLine( manip, mod ); - } - else - { - row -= activeMeta; - var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawManipulationLine( manip, mod ); + var row = actualRow; + ImGui.TableNextRow(); + if( row < activeResolved ) + { + var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= activeResolved ) < activeSwap ) + { + var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else if( ( row -= activeSwap ) < activeMeta ) + { + var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + } + else if( ( row -= activeMeta ) < forcedResolved ) + { + var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= forcedResolved ) < forcedSwap ) + { + var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else + { + row -= forcedSwap; + var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + } } } } @@ -113,4 +219,5 @@ namespace Penumbra.UI } } } -} \ No newline at end of file +} + From 2b0d55bc69d9d009649ecaa4d84ff83fb8749354 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Oct 2021 18:53:20 +0200 Subject: [PATCH 0014/2451] Fix scrolling in Changed Items tab. --- Penumbra/UI/MenuTabs/TabChangedItems.cs | 4 ++-- Penumbra/UI/UiHelpers.cs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index c161b6d9..a22a5ff5 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -42,7 +42,7 @@ namespace Penumbra.UI _filterLower = _filter.ToLowerInvariant(); } - if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg, AutoFillSize ) ) + if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) { return; } @@ -64,7 +64,7 @@ namespace Penumbra.UI { ImGui.TableNextRow(); ImGui.TableNextColumn(); - _base.DrawChangedItem( name, data ); + _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); } } } diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 47c0e59b..a69a8296 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -9,7 +9,7 @@ namespace Penumbra.UI { public partial class SettingsInterface { - internal void DrawChangedItem( string name, object? data ) + internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; @@ -30,7 +30,8 @@ namespace Penumbra.UI if( data is Item it ) { var modelId = $"({( ( Quad )it.ModelMain ).A})"; - var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; + var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset; + ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); } From 702476f43ca25b34070a749b64a01f59db984aac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Oct 2021 18:58:05 +0200 Subject: [PATCH 0015/2451] Fix folder name --- Penumbra/{API => Api}/ModsController.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Penumbra/{API => Api}/ModsController.cs (100%) diff --git a/Penumbra/API/ModsController.cs b/Penumbra/Api/ModsController.cs similarity index 100% rename from Penumbra/API/ModsController.cs rename to Penumbra/Api/ModsController.cs From e488506cde962b18fe0fba83dcfb7509e7c287bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Oct 2021 15:09:26 +0200 Subject: [PATCH 0016/2451] Only compute changed items when necessary. --- Penumbra/UI/MenuTabs/TabChangedItems.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index a22a5ff5..45c85437 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -26,13 +26,12 @@ namespace Penumbra.UI public void Draw() { - var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); - var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); - var count = items.Count + forced.Count; - if( count > 0 && !ImGui.BeginTabItem( LabelTab ) ) + if( !ImGui.BeginTabItem( LabelTab ) ) { return; } + var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary(); + var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary(); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); From 906e0579437a138830e6d68103b0f4b4952add70 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Oct 2021 15:12:47 +0200 Subject: [PATCH 0017/2451] Signify freshly added mods in the mod-selector. --- Penumbra/Importer/TexToolsImport.cs | 34 +++++++++++-------- Penumbra/UI/MenuTabs/TabImport.cs | 9 ++++- .../UI/MenuTabs/TabInstalled/ModFilter.cs | 6 +++- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 17 +++++++++- .../UI/MenuTabs/TabInstalled/TabInstalled.cs | 7 ++-- .../TabInstalled/TabInstalledModPanel.cs | 11 ++++-- .../TabInstalled/TabInstalledSelector.cs | 11 ++++-- Penumbra/UI/SettingsMenu.cs | 2 +- 8 files changed, 71 insertions(+), 26 deletions(-) diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 0a724f6f..fbbd4fc4 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -55,13 +55,14 @@ namespace Penumbra.Importer private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) => new( Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() ) ); - public void ImportModPack( FileInfo modPackFile ) + public DirectoryInfo ImportModPack( FileInfo modPackFile ) { CurrentModPack = modPackFile.Name; - VerifyVersionAndImport( modPackFile ); + var dir = VerifyVersionAndImport( modPackFile ); State = ImporterState.Done; + return dir; } private void WriteZipEntryToTempFile( Stream s ) @@ -106,7 +107,7 @@ namespace Penumbra.Importer return new MagicTempFileStreamManagerAndDeleter( fs ); } - private void VerifyVersionAndImport( FileInfo modPackFile ) + private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) { using var zfs = modPackFile.OpenRead(); using var extractedModPack = new ZipFile( zfs ); @@ -127,7 +128,7 @@ namespace Penumbra.Importer PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); } - ImportV2ModPack( modPackFile, extractedModPack, modRaw ); + return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); } else { @@ -136,11 +137,11 @@ namespace Penumbra.Importer PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); } - ImportV1ModPack( modPackFile, extractedModPack, modRaw ); + return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); } } - private void ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) { PluginLog.Log( " -> Importing V1 ModPack" ); @@ -170,28 +171,31 @@ namespace Penumbra.Importer ); ExtractSimpleModList( ExtractedDirectory, modList, modData ); + + return ExtractedDirectory; } - private void ImportV2ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV2ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) { var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw ); if( modList?.TTMPVersion == null ) { PluginLog.Error( "Could not extract V2 Modpack. No version given." ); - return; + return new DirectoryInfo( "" ); } if( modList.TTMPVersion.EndsWith( "s" ) ) { - ImportSimpleV2ModPack( extractedModPack, modList ); - return; + return ImportSimpleV2ModPack( extractedModPack, modList ); } if( modList.TTMPVersion.EndsWith( "w" ) ) { - ImportExtendedV2ModPack( extractedModPack, modRaw ); + return ImportExtendedV2ModPack( extractedModPack, modRaw ); } + + return new DirectoryInfo( "" ); } public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) @@ -219,7 +223,7 @@ namespace Penumbra.Importer return newModFolder; } - private void ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) + private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) { PluginLog.Log( " -> Importing Simple V2 ModPack" ); @@ -242,9 +246,10 @@ namespace Penumbra.Importer JsonConvert.SerializeObject( modMeta ) ); ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); + return ExtractedDirectory; } - private void ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) { PluginLog.Log( " -> Importing Extended V2 ModPack" ); @@ -273,7 +278,7 @@ namespace Penumbra.Importer if( modList.ModPackPages == null ) { - return; + return ExtractedDirectory; } // Iterate through all pages @@ -307,6 +312,7 @@ namespace Penumbra.Importer Path.Combine( ExtractedDirectory.FullName, "meta.json" ), JsonConvert.SerializeObject( modMeta, Formatting.Indented ) ); + return ExtractedDirectory; } private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta ) diff --git a/Penumbra/UI/MenuTabs/TabImport.cs b/Penumbra/UI/MenuTabs/TabImport.cs index e0166852..6680947b 100644 --- a/Penumbra/UI/MenuTabs/TabImport.cs +++ b/Penumbra/UI/MenuTabs/TabImport.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -35,6 +36,8 @@ namespace Penumbra.UI private readonly SettingsInterface _base; private readonly ModManager _manager; + public readonly HashSet< string > NewMods = new(); + public TabImport( SettingsInterface ui ) { _base = ui; @@ -72,7 +75,11 @@ namespace Penumbra.UI try { _texToolsImport = new TexToolsImport( _manager.BasePath ); - _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + if( dir.Name.Any() ) + { + NewMods.Add( dir.Name ); + } PluginLog.Information( $"-> {fileName} OK!" ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs index 590ffff0..a90f620c 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs @@ -18,11 +18,13 @@ namespace Penumbra.UI HasNoConfig = 1 << 10, HasNoFiles = 1 << 11, HasFiles = 1 << 12, + IsNew = 1 << 13, + NotNew = 1 << 14, }; public static class ModFilterExtensions { - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 ); + public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 15 ) - 1 ); public static string ToName( this ModFilter filter ) => filter switch @@ -40,6 +42,8 @@ namespace Penumbra.UI ModFilter.HasConfig => "Configuration", ModFilter.HasNoFiles => "No Files", ModFilter.HasFiles => "Files", + ModFilter.IsNew => "Newly Imported", + ModFilter.NotNew => "Not Newly Imported", _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), }; } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index ba1e3127..6d4adbd5 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -8,6 +8,7 @@ namespace Penumbra.UI { public class ModListCache : IDisposable { + public const uint NewModColor = 0xFF66DD66u; public const uint DisabledModColor = 0xFF666666u; public const uint ConflictingModColor = 0xFFAAAAFFu; public const uint HandledConflictModColor = 0xFF88DDDDu; @@ -17,6 +18,7 @@ namespace Penumbra.UI private readonly List< Mod.Mod > _modsInOrder = new(); private readonly List< (bool visible, uint color) > _visibleMods = new(); private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); + private readonly IReadOnlySet< string > _newMods; private string _modFilter = string.Empty; private string _modFilterChanges = string.Empty; @@ -40,9 +42,10 @@ namespace Penumbra.UI } } - public ModListCache( ModManager manager ) + public ModListCache( ModManager manager, IReadOnlySet< string > newMods ) { _manager = manager; + _newMods = newMods; ResetModList(); ModFileSystem.ModFileSystemChanged += TriggerListReset; } @@ -225,6 +228,7 @@ namespace Penumbra.UI private (bool, uint) CheckFilters( Mod.Mod mod ) { var ret = ( false, 0u ); + if( _modFilter.Any() && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) { return ret; @@ -261,6 +265,12 @@ namespace Penumbra.UI return ret; } + var isNew = _newMods.Contains( mod.Data.BasePath.Name ); + if( CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) + { + return ret; + } + if( !mod.Settings.Enabled ) { if( !StateFilter.HasFlag( ModFilter.Disabled ) || !StateFilter.HasFlag( ModFilter.NoConflict ) ) @@ -303,6 +313,11 @@ namespace Penumbra.UI } ret.Item1 = true; + if( isNew ) + { + ret.Item2 = NewModColor; + } + SetFolderAndParentsVisible( mod.Data.SortOrder.ParentFolder ); return ret; } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs index ae1764b5..f3769ae6 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using ImGuiNET; using Penumbra.Mods; using Penumbra.UI.Custom; @@ -15,10 +16,10 @@ namespace Penumbra.UI public readonly Selector Selector; public readonly ModPanel ModPanel; - public TabInstalled( SettingsInterface ui ) + public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) { - Selector = new Selector( ui ); - ModPanel = new ModPanel( ui, Selector ); + Selector = new Selector( ui, newMods ); + ModPanel = new ModPanel( ui, Selector, newMods ); _modManager = Service< ModManager >.Get(); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index bbc2c42a..e2e819bf 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Numerics; @@ -50,16 +51,18 @@ namespace Penumbra.UI private readonly SettingsInterface _base; private readonly Selector _selector; private readonly ModManager _modManager; + private readonly HashSet< string > _newMods; public readonly PluginDetails Details; private bool _editMode; private string _currentWebsite; private bool _validWebsite; - public ModPanel( SettingsInterface ui, Selector s ) + public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) { _base = ui; _selector = s; + _newMods = newMods; Details = new PluginDetails( _base, _selector ); _currentWebsite = Meta?.Website ?? ""; _modManager = Service< ModManager >.Get(); @@ -216,7 +219,11 @@ namespace Penumbra.UI if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { Mod.Settings.Enabled = enabled; - if( !enabled ) + if( enabled ) + { + _newMods.Remove( Mod.Data.BasePath.Name ); + } + else { Mod.Cache.ClearConflicts(); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 4e430298..e8e0e283 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -196,7 +197,7 @@ namespace Penumbra.UI private static void DrawModHelpPopup() { ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 33 * ImGui.GetTextLineHeightWithSpacing() ), + ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), ImGuiCond.Appearing ); var _ = true; if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) @@ -219,6 +220,10 @@ namespace Penumbra.UI ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.NewModColor ), + "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); + ImGui.Bullet(); + ImGui.SameLine(); ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); ImGui.Bullet(); @@ -582,11 +587,11 @@ namespace Penumbra.UI private float _selectorScalingFactor = 1; - public Selector( SettingsInterface ui ) + public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) { _base = ui; _modManager = Service< ModManager >.Get(); - Cache = new ModListCache( _modManager ); + Cache = new ModListCache( _modManager, newMods ); } private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index 7a3ec8ab..43e7bd49 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -30,7 +30,7 @@ namespace Penumbra.UI _settingsTab = new TabSettings( _base ); _importTab = new TabImport( _base ); _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base ); + InstalledTab = new TabInstalled( _base, _importTab.NewMods ); CollectionsTab = new TabCollections( InstalledTab.Selector ); _effectiveTab = new TabEffective(); _changedItems = new TabChangedItems( _base ); From 743f83d12e6b46478c3273d51d4af24e8998fb6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:03:49 +0100 Subject: [PATCH 0018/2451] Add keeping track of seen players to player watcher. --- Penumbra.PlayerWatch/IPlayerWatcher.cs | 39 +- Penumbra.PlayerWatch/PlayerWatchBase.cs | 480 ++++++++++++------------ Penumbra.PlayerWatch/PlayerWatcher.cs | 154 ++++---- 3 files changed, 340 insertions(+), 333 deletions(-) diff --git a/Penumbra.PlayerWatch/IPlayerWatcher.cs b/Penumbra.PlayerWatch/IPlayerWatcher.cs index 8a884bc1..bfdff17e 100644 --- a/Penumbra.PlayerWatch/IPlayerWatcher.cs +++ b/Penumbra.PlayerWatch/IPlayerWatcher.cs @@ -3,29 +3,28 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch +namespace Penumbra.PlayerWatch; + +public delegate void PlayerChange( Character actor ); + +public interface IPlayerWatcherBase : IDisposable { - public delegate void PlayerChange( Character actor ); + public int Version { get; } + public bool Valid { get; } +} - public interface IPlayerWatcherBase : IDisposable - { - public int Version { get; } - public bool Valid { get; } - } +public interface IPlayerWatcher : IPlayerWatcherBase +{ + public event PlayerChange? PlayerChanged; + public bool Active { get; } - public interface IPlayerWatcher : IPlayerWatcherBase - { - public event PlayerChange? PlayerChanged; - public bool Active { get; } + public void Enable(); + public void Disable(); + public void SetStatus( bool enabled ); - public void Enable(); - public void Disable(); - public void SetStatus( bool enabled ); + public void AddPlayerToWatch( string playerName ); + public void RemovePlayerFromWatch( string playerName ); + public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); - public void AddPlayerToWatch( string playerName ); - public void RemovePlayerFromWatch( string playerName ); - public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); - - public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers(); - } + public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers(); } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index 3045af88..41cb9fdb 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -9,303 +9,311 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch -{ - internal readonly struct WatchedPlayer - { - public readonly Dictionary< uint, CharacterEquipment > FoundActors; - public readonly HashSet< PlayerWatcher > RegisteredWatchers; +namespace Penumbra.PlayerWatch; - public WatchedPlayer( PlayerWatcher watcher ) +internal readonly struct WatchedPlayer +{ + public readonly Dictionary< ulong, CharacterEquipment > FoundActors; + public readonly HashSet< PlayerWatcher > RegisteredWatchers; + + public WatchedPlayer( PlayerWatcher watcher ) + { + FoundActors = new Dictionary< ulong, CharacterEquipment >( 4 ); + RegisteredWatchers = new HashSet< PlayerWatcher > { watcher }; + } +} + +internal class PlayerWatchBase : IDisposable +{ + public const int GPosePlayerIdx = 201; + public const int GPoseTableEnd = GPosePlayerIdx + 48; + private const int ObjectsPerFrame = 32; + + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objects; + internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); + internal readonly Dictionary< string, WatchedPlayer > Equip = new(); + internal HashSet< ulong > SeenActors; + private int _frameTicker; + private bool _inGPose; + private bool _enabled; + private bool _cancel; + + internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) + { + _framework = framework; + _clientState = clientState; + _objects = objects; + SeenActors = new HashSet< ulong >( _objects.Length ); + } + + internal void RegisterWatcher( PlayerWatcher watcher ) + { + RegisteredWatchers.Add( watcher ); + if( watcher.Active ) { - FoundActors = new Dictionary< uint, CharacterEquipment >(4); - RegisteredWatchers = new HashSet< PlayerWatcher >{ watcher }; + EnablePlayerWatch(); } } - internal class PlayerWatchBase : IDisposable + internal void UnregisterWatcher( PlayerWatcher watcher ) { - public const int GPosePlayerIdx = 201; - public const int GPoseTableEnd = GPosePlayerIdx + 48; - private const int ObjectsPerFrame = 32; - - private readonly Framework _framework; - private readonly ClientState _clientState; - private readonly ObjectTable _objects; - internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); - internal readonly Dictionary< string, WatchedPlayer > Equip = new(); - private int _frameTicker; - private bool _inGPose; - private bool _enabled; - private bool _cancel; - - internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) + if( RegisteredWatchers.Remove( watcher ) ) { - _framework = framework; - _clientState = clientState; - _objects = objects; - } - - internal void RegisterWatcher( PlayerWatcher watcher ) - { - RegisteredWatchers.Add( watcher ); - if( watcher.Active ) + foreach( var (key, value) in Equip.ToArray() ) { - EnablePlayerWatch(); - } - } - - internal void UnregisterWatcher( PlayerWatcher watcher ) - { - if( RegisteredWatchers.Remove( watcher ) ) - { - foreach( var (key, value) in Equip.ToArray() ) + if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 ) { - if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 ) - { - Equip.Remove( key ); - } - } - } - - CheckActiveStatus(); - } - - internal void CheckActiveStatus() - { - if( RegisteredWatchers.Any( w => w.Active ) ) - { - EnablePlayerWatch(); - } - else - { - DisablePlayerWatch(); - } - } - - private static uint GetId( GameObject actor ) - => actor.ObjectId ^ actor.OwnerId; - - internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) - { - var name = actor.Name.ToString(); - var equipment = new CharacterEquipment( actor ); - if (Equip.TryGetValue( name, out var watched )) - { - watched.FoundActors[ GetId( actor ) ] = equipment; - } - - return equipment; - } - - internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher ) - { - if( Equip.TryGetValue( playerName, out var items ) ) - { - items.RegisteredWatchers.Add( watcher ); - } - else - { - Equip[ playerName ] = new WatchedPlayer( watcher ); - } - } - - public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher ) - { - if( Equip.TryGetValue( playerName, out var items ) ) - { - if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 ) - { - Equip.Remove( playerName ); + Equip.Remove( key ); } } } - internal void EnablePlayerWatch() + CheckActiveStatus(); + } + + internal void CheckActiveStatus() + { + if( RegisteredWatchers.Any( w => w.Active ) ) { - if( !_enabled ) - { - _enabled = true; - _framework.Update += OnFrameworkUpdate; - _clientState.TerritoryChanged += OnTerritoryChange; - _clientState.Logout += OnLogout; - } + EnablePlayerWatch(); + } + else + { + DisablePlayerWatch(); + } + } + + private static ulong GetId( GameObject actor ) + => actor.ObjectId | ( ( ulong )actor.OwnerId << 32 ); + + internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) + { + var name = actor.Name.ToString(); + var equipment = new CharacterEquipment( actor ); + if( Equip.TryGetValue( name, out var watched ) ) + { + watched.FoundActors[ GetId( actor ) ] = equipment; } - internal void DisablePlayerWatch() + return equipment; + } + + internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher ) + { + if( Equip.TryGetValue( playerName, out var items ) ) { - if( _enabled ) + items.RegisteredWatchers.Add( watcher ); + } + else + { + Equip[ playerName ] = new WatchedPlayer( watcher ); + } + } + + public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher ) + { + if( Equip.TryGetValue( playerName, out var items ) ) + { + if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 ) { - _enabled = false; - _framework.Update -= OnFrameworkUpdate; - _clientState.TerritoryChanged -= OnTerritoryChange; - _clientState.Logout -= OnLogout; + Equip.Remove( playerName ); } } + } - public void Dispose() - => DisablePlayerWatch(); - - private void OnTerritoryChange( object? _1, ushort _2 ) - => Clear(); - - private void OnLogout( object? _1, object? _2 ) - => Clear(); - - internal void Clear() + internal void EnablePlayerWatch() + { + if( !_enabled ) { - PluginLog.Debug( "Clearing PlayerWatcher Store." ); - _cancel = true; - foreach( var kvp in Equip ) - { - kvp.Value.FoundActors.Clear(); - } + _enabled = true; + _framework.Update += OnFrameworkUpdate; + _clientState.TerritoryChanged += OnTerritoryChange; + _clientState.Logout += OnLogout; + } + } - _frameTicker = 0; + internal void DisablePlayerWatch() + { + if( _enabled ) + { + _enabled = false; + _framework.Update -= OnFrameworkUpdate; + _clientState.TerritoryChanged -= OnTerritoryChange; + _clientState.Logout -= OnLogout; + } + } + + public void Dispose() + => DisablePlayerWatch(); + + private void OnTerritoryChange( object? _1, ushort _2 ) + => Clear(); + + private void OnLogout( object? _1, object? _2 ) + => Clear(); + + internal void Clear() + { + PluginLog.Debug( "Clearing PlayerWatcher Store." ); + _cancel = true; + foreach( var kvp in Equip ) + { + kvp.Value.FoundActors.Clear(); } - private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player ) + _frameTicker = 0; + } + + private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player ) + { + PluginLog.Debug( "Triggering events for {PlayerName} at {Address}.", player.Name, player.Address ); + foreach( var watcher in watchers.Where( w => w.Active ) ) { - PluginLog.Debug( "Triggering events for {PlayerName} at {Address}.", player.Name, player.Address ); - foreach( var watcher in watchers.Where( w => w.Active ) ) + watcher.Trigger( player ); + } + } + + internal void TriggerGPose() + { + for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) + { + var player = _objects[ i ]; + if( player == null ) { - watcher.Trigger( player ); + return; + } + + if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) + { + TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); } } + } - internal void TriggerGPose() + private Character? CheckGPoseObject( GameObject player ) + { + if( !_inGPose ) { - for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) - { - var player = _objects[ i ]; - if( player == null ) - { - return; - } - - if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) - { - TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); - } - } + return CharacterFactory.Convert( player ); } - private Character? CheckGPoseObject( GameObject player ) + for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) { - if( !_inGPose ) + var a = _objects[ i ]; + if( a == null ) { return CharacterFactory.Convert( player ); } - for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) + if( a.Name == player.Name ) { - var a = _objects[ i ]; - if( a == null ) - { - return CharacterFactory.Convert( player ); - } + return CharacterFactory.Convert( a ); + } + } - if( a.Name == player.Name ) - { - return CharacterFactory.Convert( a ); - } + return CharacterFactory.Convert( player )!; + } + + private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) + { + watch = default; + var name = gameObject.Name.ToString(); + return name.Length != 0 && Equip.TryGetValue( name, out watch ); + } + + private static bool InvalidObjectKind( ObjectKind kind ) + { + return kind switch + { + ObjectKind.BattleNpc => false, + ObjectKind.EventNpc => false, + ObjectKind.Player => false, + ObjectKind.Retainer => false, + _ => true, + }; + } + + private GameObject? GetNextObject() + { + if( _frameTicker == GPosePlayerIdx - 1 ) + { + _frameTicker = GPoseTableEnd; + } + else if( _frameTicker == _objects.Length - 1 ) + { + _frameTicker = 0; + foreach( var (_, equip) in Equip.Values.SelectMany( d => d.FoundActors.Where( p => !SeenActors.Contains( p.Key ) ) ) ) + { + equip.Clear(); } - return CharacterFactory.Convert( player )!; + SeenActors.Clear(); + } + else + { + ++_frameTicker; } - private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) - { - watch = default; - var name = gameObject.Name.ToString(); - return name.Length != 0 && Equip.TryGetValue( name, out watch ); - } + return _objects[ _frameTicker ]; + } - private static bool InvalidObjectKind( ObjectKind kind ) - { - return kind switch - { - ObjectKind.BattleNpc => false, - ObjectKind.EventNpc => false, - ObjectKind.Player => false, - ObjectKind.Retainer => false, - _ => true, - }; - } + private void OnFrameworkUpdate( object framework ) + { + var newInGPose = _objects[ GPosePlayerIdx ] != null; - private GameObject? GetNextObject() + if( newInGPose != _inGPose ) { - if( _frameTicker == GPosePlayerIdx - 1 ) + if( newInGPose ) { - _frameTicker = GPoseTableEnd; - } - else if( _frameTicker == _objects.Length - 1 ) - { - _frameTicker = 0; + TriggerGPose(); } else { - ++_frameTicker; + Clear(); } - return _objects[ _frameTicker ]; + _inGPose = newInGPose; } - private void OnFrameworkUpdate( object framework ) + for( var i = 0; i < ObjectsPerFrame; ++i ) { - var newInGPose = _objects[ GPosePlayerIdx ] != null; - - if( newInGPose != _inGPose ) + var actor = GetNextObject(); + if( actor == null + || InvalidObjectKind( actor.ObjectKind ) + || !TryGetPlayer( actor, out var watch ) ) { - if( newInGPose ) - { - TriggerGPose(); - } - else - { - Clear(); - } - - _inGPose = newInGPose; + continue; } - for( var i = 0; i < ObjectsPerFrame; ++i ) + var character = CheckGPoseObject( actor ); + if( _cancel ) { - var actor = GetNextObject(); - if( actor == null - || InvalidObjectKind( actor.ObjectKind ) - || !TryGetPlayer( actor, out var watch ) ) - { - continue; - } - - var character = CheckGPoseObject( actor ); - if( _cancel ) - { - _cancel = false; - return; - } - - if( character == null || character.ModelType() != 0 ) - { - continue; - } - - var id = GetId( character ); - PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address); - if( !watch.FoundActors.TryGetValue( id, out var equip ) ) - { - equip = new CharacterEquipment( character ); - watch.FoundActors[ id ] = equip; - TriggerEvents( watch.RegisteredWatchers, character ); - } - else if (!equip.CompareAndUpdate( character )) - { - TriggerEvents( watch.RegisteredWatchers, character ); - } - - break; // Only one comparison per frame. + _cancel = false; + return; } + + if( character == null || character.ModelType() != 0 ) + { + continue; + } + + var id = GetId( character ); + SeenActors.Add( id ); + PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address ); + if( !watch.FoundActors.TryGetValue( id, out var equip ) ) + { + equip = new CharacterEquipment( character ); + watch.FoundActors[ id ] = equip; + TriggerEvents( watch.RegisteredWatchers, character ); + } + else if( !equip.CompareAndUpdate( character ) ) + { + TriggerEvents( watch.RegisteredWatchers, character ); + } + + break; // Only one comparison per frame. } } } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatcher.cs b/Penumbra.PlayerWatch/PlayerWatcher.cs index 9eab7134..7ae94f79 100644 --- a/Penumbra.PlayerWatch/PlayerWatcher.cs +++ b/Penumbra.PlayerWatch/PlayerWatcher.cs @@ -7,97 +7,97 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch +namespace Penumbra.PlayerWatch; + +public class PlayerWatcher : IPlayerWatcher { - public class PlayerWatcher : IPlayerWatcher + public int Version + => 3; + + private static PlayerWatchBase? _playerWatch; + + public event PlayerChange? PlayerChanged; + + public bool Active { get; set; } = true; + + public bool Valid + => _playerWatch != null; + + internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) { - public int Version { get; } = 2; + _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); + _playerWatch.RegisterWatcher( this ); + } - private static PlayerWatchBase? _playerWatch; + public void Enable() + => SetStatus( true ); - public event PlayerChange? PlayerChanged; + public void Disable() + => SetStatus( false ); - public bool Active { get; set; } = true; + public void SetStatus( bool enabled ) + { + Active = enabled && Valid; + _playerWatch?.CheckActiveStatus(); + } - public bool Valid - => _playerWatch != null; + internal void Trigger( Character actor ) + => PlayerChanged?.Invoke( actor ); - internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) + public void Dispose() + { + if( _playerWatch == null ) { - _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); - _playerWatch.RegisterWatcher( this ); + return; } - public void Enable() - => SetStatus( true ); - - public void Disable() - => SetStatus( false ); - - public void SetStatus( bool enabled ) + Active = false; + PlayerChanged = null; + _playerWatch.UnregisterWatcher( this ); + if( _playerWatch.RegisteredWatchers.Count == 0 ) { - Active = enabled && Valid; - _playerWatch?.CheckActiveStatus(); - } - - internal void Trigger( Character actor ) - => PlayerChanged?.Invoke( actor ); - - public void Dispose() - { - if( _playerWatch == null ) - { - return; - } - - Active = false; - PlayerChanged = null; - _playerWatch.UnregisterWatcher( this ); - if( _playerWatch.RegisteredWatchers.Count == 0 ) - { - _playerWatch.Dispose(); - _playerWatch = null; - } - } - - private void CheckValidity() - { - if( !Valid ) - { - throw new Exception( $"PlayerWatch was already disposed." ); - } - } - - public void AddPlayerToWatch( string name ) - { - CheckValidity(); - _playerWatch!.AddPlayerToWatch( name, this ); - } - - public void RemovePlayerFromWatch( string playerName ) - { - CheckValidity(); - _playerWatch!.RemovePlayerFromWatch( playerName, this ); - } - - public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) - { - CheckValidity(); - return _playerWatch!.UpdatePlayerWithoutEvent( actor ); - } - - public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers() - { - CheckValidity(); - return _playerWatch!.Equip - .Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) ) - .Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) ); + _playerWatch.Dispose(); + _playerWatch = null; } } - public static class PlayerWatchFactory + private void CheckValidity() { - public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) - => new PlayerWatcher( framework, clientState, objects ); + if( !Valid ) + { + throw new Exception( $"PlayerWatch was already disposed." ); + } } + + public void AddPlayerToWatch( string name ) + { + CheckValidity(); + _playerWatch!.AddPlayerToWatch( name, this ); + } + + public void RemovePlayerFromWatch( string playerName ) + { + CheckValidity(); + _playerWatch!.RemovePlayerFromWatch( playerName, this ); + } + + public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) + { + CheckValidity(); + return _playerWatch!.UpdatePlayerWithoutEvent( actor ); + } + + public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers() + { + CheckValidity(); + return _playerWatch!.Equip + .Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) ) + .Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) ); + } +} + +public static class PlayerWatchFactory +{ + public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) + => new PlayerWatcher( framework, clientState, objects ); } \ No newline at end of file From ba2ffcc79099f7bc7f48d52b508b161dbf4e8e80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:04:11 +0100 Subject: [PATCH 0019/2451] Some more information in ResourceManager --- Penumbra/UI/MenuTabs/TabResourceManager.cs | 206 ++++++++++----------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 107bce9f..344c4d70 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -6,121 +6,121 @@ using FFXIVClientStructs.STD; using ImGuiNET; using Penumbra.UI.Custom; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private static string GetNodeLabel( uint label, uint type, ulong count ) { - private static string GetNodeLabel( string label, uint type, ulong count ) + var byte1 = type >> 24; + var byte2 = ( type >> 16 ) & 0xFF; + var byte3 = ( type >> 8 ) & 0xFF; + var byte4 = type & 0xFF; + return byte1 == 0 + ? $"({type:X8}) {( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug" + : $"({type:X8}) {( char )byte1}{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug"; + } + + private unsafe void DrawResourceMap( string label, StdMap< uint, Pointer< ResourceHandle > >* typeMap ) + { + if( typeMap == null || !ImGui.TreeNodeEx( label ) ) { - var byte1 = type >> 24; - var byte2 = ( type >> 16 ) & 0xFF; - var byte3 = ( type >> 8 ) & 0xFF; - var byte4 = type & 0xFF; - return byte1 == 0 - ? $"{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug" - : $"{( char )byte1}{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug"; + return; } - private unsafe void DrawResourceMap( string label, StdMap< uint, Pointer< ResourceHandle > >* typeMap ) + using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + + if( typeMap->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) { - if( typeMap == null || !ImGui.TreeNodeEx( label ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - - if( typeMap->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale ); - ImGui.TableHeadersRow(); - - var node = typeMap->SmallestValue; - while( !node->IsNil ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item1.ToString() ); - ImGui.TableNextColumn(); - var address = $"0x{( ulong )node->KeyValuePair.Item2.Value:X}"; - ImGui.Text( address ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( address ); - } - - ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); - node = node->Next(); - } + return; } - private unsafe void DrawCategoryContainer( string label, ResourceGraph.CategoryContainer container ) + raii.Push( ImGui.EndTable ); + + ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, + ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale ); + ImGui.TableHeadersRow(); + + var node = typeMap->SmallestValue; + while( !node->IsNil ) { - var map = container.MainMap; - if( map == null || !ImGui.TreeNodeEx( $"{label} - {map->Count}###{label}Debug" ) ) + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( $"0x{node->KeyValuePair.Item1:X8}" ); + ImGui.TableNextColumn(); + var address = $"0x{( ulong )node->KeyValuePair.Item2.Value:X}"; + ImGui.Text( address ); + if( ImGui.IsItemClicked() ) { - return; + ImGui.SetClipboardText( address ); } - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - - var node = map->SmallestValue; - while( !node->IsNil ) - { - DrawResourceMap( GetNodeLabel( label, node->KeyValuePair.Item1, node->KeyValuePair.Item2.Value->Count ), - node->KeyValuePair.Item2.Value ); - node = node->Next(); - } - } - - private unsafe void DrawResourceManagerTab() - { - if( !ImGui.BeginTabItem( "Resource Manager Tab" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1D93AC0 ); - - if( resourceHandler == null ) - { - return; - } - - raii.Push( ImGui.EndChild ); - if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) ) - { - return; - } - - DrawCategoryContainer( "Common", resourceHandler->ResourceGraph->CommonContainer ); - DrawCategoryContainer( "BgCommon", resourceHandler->ResourceGraph->BgCommonContainer ); - DrawCategoryContainer( "Bg", resourceHandler->ResourceGraph->BgContainer ); - DrawCategoryContainer( "Cut", resourceHandler->ResourceGraph->CutContainer ); - DrawCategoryContainer( "Chara", resourceHandler->ResourceGraph->CharaContainer ); - DrawCategoryContainer( "Shader", resourceHandler->ResourceGraph->ShaderContainer ); - DrawCategoryContainer( "Ui", resourceHandler->ResourceGraph->UiContainer ); - DrawCategoryContainer( "Sound", resourceHandler->ResourceGraph->SoundContainer ); - DrawCategoryContainer( "Vfx", resourceHandler->ResourceGraph->VfxContainer ); - DrawCategoryContainer( "UiScript", resourceHandler->ResourceGraph->UiScriptContainer ); - DrawCategoryContainer( "Exd", resourceHandler->ResourceGraph->ExdContainer ); - DrawCategoryContainer( "GameScript", resourceHandler->ResourceGraph->GameScriptContainer ); - DrawCategoryContainer( "Music", resourceHandler->ResourceGraph->MusicContainer ); - DrawCategoryContainer( "SqpackTest", resourceHandler->ResourceGraph->SqpackTestContainer ); - DrawCategoryContainer( "Debug", resourceHandler->ResourceGraph->DebugContainer ); + ImGui.TableNextColumn(); + ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); + node = node->Next(); } } + + private unsafe void DrawCategoryContainer( ResourceCategory category, ResourceGraph.CategoryContainer container ) + { + var map = container.MainMap; + if( map == null || !ImGui.TreeNodeEx( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}Debug" ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + + var node = map->SmallestValue; + while( !node->IsNil ) + { + DrawResourceMap( GetNodeLabel( ( uint )category, node->KeyValuePair.Item1, node->KeyValuePair.Item2.Value->Count ), + node->KeyValuePair.Item2.Value ); + node = node->Next(); + } + } + + private unsafe void DrawResourceManagerTab() + { + if( !ImGui.BeginTabItem( "Resource Manager Tab" ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1D93AC0 ); + + if( resourceHandler == null ) + { + return; + } + + raii.Push( ImGui.EndChild ); + if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) ) + { + return; + } + + DrawCategoryContainer( ResourceCategory.Common, resourceHandler->ResourceGraph->CommonContainer ); + DrawCategoryContainer( ResourceCategory.BgCommon, resourceHandler->ResourceGraph->BgCommonContainer ); + DrawCategoryContainer( ResourceCategory.Bg, resourceHandler->ResourceGraph->BgContainer ); + DrawCategoryContainer( ResourceCategory.Cut, resourceHandler->ResourceGraph->CutContainer ); + DrawCategoryContainer( ResourceCategory.Chara, resourceHandler->ResourceGraph->CharaContainer ); + DrawCategoryContainer( ResourceCategory.Shader, resourceHandler->ResourceGraph->ShaderContainer ); + DrawCategoryContainer( ResourceCategory.Ui, resourceHandler->ResourceGraph->UiContainer ); + DrawCategoryContainer( ResourceCategory.Sound, resourceHandler->ResourceGraph->SoundContainer ); + DrawCategoryContainer( ResourceCategory.Vfx, resourceHandler->ResourceGraph->VfxContainer ); + DrawCategoryContainer( ResourceCategory.UiScript, resourceHandler->ResourceGraph->UiScriptContainer ); + DrawCategoryContainer( ResourceCategory.Exd, resourceHandler->ResourceGraph->ExdContainer ); + DrawCategoryContainer( ResourceCategory.GameScript, resourceHandler->ResourceGraph->GameScriptContainer ); + DrawCategoryContainer( ResourceCategory.Music, resourceHandler->ResourceGraph->MusicContainer ); + DrawCategoryContainer( ResourceCategory.SqpackTest, resourceHandler->ResourceGraph->SqpackTestContainer ); + DrawCategoryContainer( ResourceCategory.Debug, resourceHandler->ResourceGraph->DebugContainer ); + } } \ No newline at end of file From cd74a414532aeb51046019448db46bf72c83f456 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:05:03 +0100 Subject: [PATCH 0020/2451] Remove checking for available mods in the mod panel so you can create empty mods from the start. --- .../UI/MenuTabs/TabInstalled/TabInstalled.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs index f3769ae6..c4ed131c 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs @@ -23,11 +23,6 @@ namespace Penumbra.UI _modManager = Service< ModManager >.Get(); } - private static void DrawNoModsAvailable() - { - ImGui.Text( "You don't have any mods :(" ); - } - public void Draw() { var ret = ImGui.BeginTabItem( LabelTab ); @@ -38,16 +33,9 @@ namespace Penumbra.UI using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - if( _modManager.Mods.Count > 0 ) - { - Selector.Draw(); - ImGui.SameLine(); - ModPanel.Draw(); - } - else - { - DrawNoModsAvailable(); - } + Selector.Draw(); + ImGui.SameLine(); + ModPanel.Draw(); } } } From 88a1e9f2ae9c5046f13122831c8cb2ff56572863 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:05:58 +0100 Subject: [PATCH 0021/2451] Add settings cleanup to Auto-Generate Groups. --- Penumbra/Mod/ModCleanup.cs | 4 ++++ Penumbra/Mods/ModCollection.cs | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 0026fd99..dc3e1510 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -519,6 +519,10 @@ namespace Penumbra.Mod meta.Groups.Add( groupDir.Name, @group ); } } + + foreach(var collection in Service.Get().Collections.Collections.Values) + collection.UpdateSetting(baseDir, meta, true); + } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 8b23612d..feae0857 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -96,19 +96,24 @@ namespace Penumbra.Mods public void ClearCache() => Cache = null; - public void UpdateSetting( ModData mod ) + public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear ) { - if( !Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + if( !Settings.TryGetValue( modPath.Name, out var settings ) ) { return; } - if( settings.FixInvalidSettings( mod.Meta ) ) + if (clear) + settings.Settings.Clear(); + if( settings.FixInvalidSettings( meta ) ) { Save(); } } + public void UpdateSetting( ModData mod ) + => UpdateSetting( mod.BasePath, mod.Meta, false ); + public void UpdateSettings( bool forceSave ) { if( Cache == null ) From 87e8f2599c0798342df77e3ca8790a88affd5230 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:06:16 +0100 Subject: [PATCH 0022/2451] Remove currently unused window even from debug... --- Penumbra/UI/MenuTabs/TabBrowser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/MenuTabs/TabBrowser.cs b/Penumbra/UI/MenuTabs/TabBrowser.cs index 8c80cb19..e88f88d6 100644 --- a/Penumbra/UI/MenuTabs/TabBrowser.cs +++ b/Penumbra/UI/MenuTabs/TabBrowser.cs @@ -7,7 +7,7 @@ namespace Penumbra.UI { private class TabBrowser { - [Conditional( "DEBUG" )] + [Conditional( "LEAVEMEALONE" )] public void Draw() { var ret = ImGui.BeginTabItem( "Available Mods" ); From 3128c2017d31e56338e6524df1c279a8d6f816cf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Nov 2021 16:06:38 +0100 Subject: [PATCH 0023/2451] Prevent some crashes when default values can't be obtained. --- Penumbra/Meta/Files/MetaDefaults.cs | 84 ++++++++++++++++++----------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Files/MetaDefaults.cs b/Penumbra/Meta/Files/MetaDefaults.cs index 5aa828ee..34600aa8 100644 --- a/Penumbra/Meta/Files/MetaDefaults.cs +++ b/Penumbra/Meta/Files/MetaDefaults.cs @@ -118,44 +118,64 @@ namespace Penumbra.Meta.Files // Check that a given meta manipulation is an actual change to the default value. We don't need to keep changes to default. public bool CheckAgainstDefault( MetaManipulation m ) { - return m.Type switch + try { - MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) - ?.GetValue( m ).Equal( m.ImcValue ) - ?? true, - MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) - == m.GmpValue, - MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) - .Reduce( m.EqpIdentifier.Slot ) - == m.EqpValue, - MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )?.GetEntry( m.EqdpIdentifier.SetId ) - .Reduce( m.EqdpIdentifier.Slot ) - == m.EqdpValue, - MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) - ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) - == m.EstValue, - MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ] - == m.RspValue, - _ => throw new NotImplementedException(), - }; + return m.Type switch + { + MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) + ?.GetValue( m ).Equal( m.ImcValue ) + ?? true, + MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) + == m.GmpValue, + MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) + .Reduce( m.EqpIdentifier.Slot ) + == m.EqpValue, + MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ) + ?.GetEntry( m.EqdpIdentifier.SetId ) + .Reduce( m.EqdpIdentifier.Slot ) + == m.EqdpValue, + MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) + ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) + == m.EstValue, + MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ] + == m.RspValue, + _ => false, + }; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" ); + } + + return false; } public object? GetDefaultValue( MetaManipulation m ) { - return m.Type switch + try { - MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) - ?.GetValue( m ), - MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ), - MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) - .Reduce( m.EqpIdentifier.Slot ), - MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )?.GetEntry( m.EqdpIdentifier.SetId ) - .Reduce( m.EqdpIdentifier.Slot ), - MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) - ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ), - MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ], - _ => throw new NotImplementedException(), - }; + return m.Type switch + { + MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) + ?.GetValue( m ), + MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ), + MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) + .Reduce( m.EqpIdentifier.Slot ), + MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ) + ?.GetEntry( m.EqdpIdentifier.SetId ) + .Reduce( m.EqdpIdentifier.Slot ), + MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) + ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ), + MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ], + _ => false, + }; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" ); + } + + return false; } // Create a deep copy of a default file as a new file. From f79405e65679ed60fb0a0f8969931af941c457d4 Mon Sep 17 00:00:00 2001 From: Eternita-S Date: Mon, 29 Nov 2021 18:17:02 +0300 Subject: [PATCH 0024/2451] Title screen menu button offset --- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/LaunchButton.cs | 2 +- Penumbra/UI/MenuTabs/TabSettings.cs | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 1a9fb2b4..716d5ede 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Dalamud.Configuration; using Dalamud.Logging; @@ -36,6 +37,7 @@ namespace Penumbra public Dictionary< string, string > ModSortOrder { get; set; } = new(); public bool InvertModListOrder { internal get; set; } + public Vector2 ManageModsButtonOffset { get; set; } = Vector2.Zero; public static Configuration Load() { diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index f146b23f..0c8c05d9 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -41,7 +41,7 @@ namespace Penumbra.UI var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); - ImGui.SetNextWindowPos( ss - WindowPosOffset, ImGuiCond.Always ); + ImGui.SetNextWindowPos( ss - WindowPosOffset + Penumbra.Config.ManageModsButtonOffset, ImGuiCond.Always ); if( ImGui.Begin( MenuButtonsName, ButtonFlags ) && ImGui.Button( MenuButtonLabel, WindowSize ) ) diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 8db86756..f5b6ffe6 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -34,6 +34,7 @@ namespace Penumbra.UI private const string LabelDisableNotifications = "Disable filesystem change notifications"; private const string LabelEnableHttpApi = "Enable HTTP API"; private const string LabelReloadResource = "Reload Player Resource"; + private const string LabelManageModsOffset = "\"Manage mods\" title screen button offset"; private readonly SettingsInterface _base; private readonly Configuration _config; @@ -155,7 +156,18 @@ namespace Penumbra.UI if( ImGui.Checkbox( LabelShowAdvanced, ref showAdvanced ) ) { _config.ShowAdvanced = showAdvanced; - _configChanged = true; + _configChanged = true; + } + } + + private void DrawManageModsButtonOffset() + { + var manageModsButtonOffset = _config.ManageModsButtonOffset; + ImGui.SetNextItemWidth( 150f ); + if( ImGui.DragFloat2( LabelManageModsOffset, ref manageModsButtonOffset, 1f ) ) + { + _config.ManageModsButtonOffset = manageModsButtonOffset; + _configChanged = true; } } @@ -305,6 +317,7 @@ namespace Penumbra.UI ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawScaleModSelectorBox(); DrawSortFoldersFirstBox(); + DrawManageModsButtonOffset(); DrawShowAdvancedBox(); if( _config.ShowAdvanced ) From 0e552d24a6306dde7688d7542677f0c1ddf51872 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Nov 2021 16:41:31 +0100 Subject: [PATCH 0025/2451] Slight modifications to button offset PR. --- Penumbra/Configuration.cs | 2 +- Penumbra/UI/LaunchButton.cs | 95 ++--- Penumbra/UI/MenuTabs/TabSettings.cs | 543 ++++++++++++++-------------- 3 files changed, 322 insertions(+), 318 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 716d5ede..2125315e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -37,7 +37,7 @@ namespace Penumbra public Dictionary< string, string > ModSortOrder { get; set; } = new(); public bool InvertModListOrder { internal get; set; } - public Vector2 ManageModsButtonOffset { get; set; } = Vector2.Zero; + public Vector2 ManageModsButtonOffset { get; set; } = 50 * Vector2.One; public static Configuration Load() { diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 0c8c05d9..18ca07bb 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,56 +1,59 @@ -using System.Numerics; +using Dalamud.Interface; using ImGuiNET; +using Penumbra.UI.Custom; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class ManageModsButton { - private class ManageModsButton + // magic numbers + private const int Width = 200; + private const int Height = 45; + private const string MenuButtonsName = "Penumbra Menu Buttons"; + private const string MenuButtonLabel = "Manage Mods"; + + private const ImGuiWindowFlags ButtonFlags = + ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoSavedSettings; + + private readonly SettingsInterface _base; + + public ManageModsButton( SettingsInterface ui ) + => _base = ui; + + internal bool ForceDraw = false; + + public void Draw() { - // magic numbers - private const int Padding = 50; - private const int Width = 200; - private const int Height = 45; - private const string MenuButtonsName = "Penumbra Menu Buttons"; - private const string MenuButtonLabel = "Manage Mods"; - - private static readonly Vector2 WindowSize = new( Width, Height ); - private static readonly Vector2 WindowPosOffset = new( Padding + Width, Padding + Height ); - - private const ImGuiWindowFlags ButtonFlags = - ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoBackground - | ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoScrollbar - | ImGuiWindowFlags.NoResize - | ImGuiWindowFlags.NoSavedSettings; - - private readonly SettingsInterface _base; - - public ManageModsButton( SettingsInterface ui ) - => _base = ui; - - public void Draw() + if( !ForceDraw && ( Dalamud.Conditions.Any() || _base._menu.Visible ) ) { - if( Dalamud.Conditions.Any() || _base._menu.Visible ) - { - return; - } - - var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; - ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); - - ImGui.SetNextWindowPos( ss - WindowPosOffset + Penumbra.Config.ManageModsButtonOffset, ImGuiCond.Always ); - - if( ImGui.Begin( MenuButtonsName, ButtonFlags ) - && ImGui.Button( MenuButtonLabel, WindowSize ) ) - { - _base.FlipVisibility(); - } - - ImGui.End(); + return; } + + using var color = ImGuiRaii.PushColor( ImGuiCol.Button, 0xFF0000C8, ForceDraw ); + + var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; + ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); + + var windowSize = ImGuiHelpers.ScaledVector2( Width, Height ); + + ImGui.SetNextWindowPos( ss - windowSize - Penumbra.Config.ManageModsButtonOffset * ImGuiHelpers.GlobalScale, ImGuiCond.Always ); + + if( ImGui.Begin( MenuButtonsName, ButtonFlags ) + && ImGui.Button( MenuButtonLabel, windowSize ) ) + { + _base.FlipVisibility(); + } + + ImGui.End(); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index f5b6ffe6..f61783dd 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -12,324 +12,325 @@ using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabSettings { - private class TabSettings + private const string LabelTab = "Settings"; + private const string LabelRootFolder = "Root Folder"; + private const string LabelTempFolder = "Temporary Folder"; + private const string LabelRediscoverButton = "Rediscover Mods"; + private const string LabelOpenFolder = "Open Mods Folder"; + private const string LabelOpenTempFolder = "Open Temporary Folder"; + private const string LabelEnabled = "Enable Mods"; + private const string LabelEnabledPlayerWatch = "Enable automatic Character Redraws"; + private const string LabelWaitFrames = "Wait Frames"; + private const string LabelSortFoldersFirst = "Sort Mod Folders Before Mods"; + private const string LabelScaleModSelector = "Scale Mod Selector With Window Size"; + private const string LabelShowAdvanced = "Show Advanced Settings"; + private const string LabelLogLoadedFiles = "Log all loaded files"; + private const string LabelDisableNotifications = "Disable filesystem change notifications"; + private const string LabelEnableHttpApi = "Enable HTTP API"; + private const string LabelReloadResource = "Reload Player Resource"; + private const string LabelManageModsOffset = "\"Manage mods\"-Button Offset"; + + private readonly SettingsInterface _base; + private readonly Configuration _config; + private bool _configChanged; + private string _newModDirectory; + private string _newTempDirectory; + + + public TabSettings( SettingsInterface ui ) { - private const string LabelTab = "Settings"; - private const string LabelRootFolder = "Root Folder"; - private const string LabelTempFolder = "Temporary Folder"; - private const string LabelRediscoverButton = "Rediscover Mods"; - private const string LabelOpenFolder = "Open Mods Folder"; - private const string LabelOpenTempFolder = "Open Temporary Folder"; - private const string LabelEnabled = "Enable Mods"; - private const string LabelEnabledPlayerWatch = "Enable automatic Character Redraws"; - private const string LabelWaitFrames = "Wait Frames"; - private const string LabelSortFoldersFirst = "Sort Mod Folders Before Mods"; - private const string LabelScaleModSelector = "Scale Mod Selector With Window Size"; - private const string LabelShowAdvanced = "Show Advanced Settings"; - private const string LabelLogLoadedFiles = "Log all loaded files"; - private const string LabelDisableNotifications = "Disable filesystem change notifications"; - private const string LabelEnableHttpApi = "Enable HTTP API"; - private const string LabelReloadResource = "Reload Player Resource"; - private const string LabelManageModsOffset = "\"Manage mods\" title screen button offset"; + _base = ui; + _config = Penumbra.Config; + _configChanged = false; + _newModDirectory = _config.ModDirectory; + _newTempDirectory = _config.TempDirectory; + } - private readonly SettingsInterface _base; - private readonly Configuration _config; - private bool _configChanged; - private string _newModDirectory; - private string _newTempDirectory; + private static bool DrawPressEnterWarning( string old, float? width = null ) + { + const uint red = 0xFF202080; + using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ); + var w = Vector2.UnitX * ( width ?? ImGui.CalcItemWidth() ); + return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); + } - - public TabSettings( SettingsInterface ui ) + private void DrawRootFolder() + { + var save = ImGui.InputText( LabelRootFolder, ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) { - _base = ui; - _config = Penumbra.Config; - _configChanged = false; - _newModDirectory = _config.ModDirectory; + return; + } + + if( save || DrawPressEnterWarning( _config.ModDirectory ) ) + { + _base._menu.InstalledTab.Selector.ClearSelection(); + _base._modManager.DiscoverMods( _newModDirectory ); + _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); + _newModDirectory = _config.ModDirectory; + } + } + + private void DrawTempFolder() + { + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + ImGui.BeginGroup(); + var save = ImGui.InputText( LabelTempFolder, ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + + ImGuiCustom.HoverTooltip( "The folder used to store temporary meta manipulation files.\n" + + "Leave this blank if you have no reason not to.\n" + + "A folder 'penumbrametatmp' will be created as a subdirectory to the specified directory.\n" + + "If none is specified (i.e. this is blank) this folder will be created in the root folder instead." ); + + ImGui.SameLine(); + if( ImGui.Button( LabelOpenTempFolder ) ) + { + if( !Directory.Exists( _base._modManager.TempPath.FullName ) || !_base._modManager.TempWritable ) + { + return; + } + + Process.Start( new ProcessStartInfo( _base._modManager.TempPath.FullName ) + { + UseShellExecute = true, + } ); + } + + ImGui.EndGroup(); + if( _newTempDirectory == _config.TempDirectory ) + { + return; + } + + if( save || DrawPressEnterWarning( _config.TempDirectory, 400 ) ) + { + _base._modManager.SetTempDirectory( _newTempDirectory ); _newTempDirectory = _config.TempDirectory; } + } - private static bool DrawPressEnterWarning( string old, float? width = null ) + private void DrawRediscoverButton() + { + if( ImGui.Button( LabelRediscoverButton ) ) { - const uint red = 0xFF202080; - using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ); - var w = Vector2.UnitX * ( width ?? ImGui.CalcItemWidth() ); - return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); + _base._menu.InstalledTab.Selector.ClearSelection(); + _base._modManager.DiscoverMods(); + _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); } + } - private void DrawRootFolder() + private void DrawOpenModsButton() + { + if( ImGui.Button( LabelOpenFolder ) ) { - var save = ImGui.InputText( LabelRootFolder, ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) + if( !Directory.Exists( _config.ModDirectory ) || !Service< ModManager >.Get().Valid ) { return; } - if( save || DrawPressEnterWarning( _config.ModDirectory ) ) + Process.Start( new ProcessStartInfo( _config.ModDirectory ) { - _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods( _newModDirectory ); - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); - _newModDirectory = _config.ModDirectory; - } + UseShellExecute = true, + } ); + } + } + + private void DrawEnabledBox() + { + var enabled = _config.IsEnabled; + if( ImGui.Checkbox( LabelEnabled, ref enabled ) ) + { + _base._penumbra.SetEnabled( enabled ); + } + } + + private void DrawShowAdvancedBox() + { + var showAdvanced = _config.ShowAdvanced; + if( ImGui.Checkbox( LabelShowAdvanced, ref showAdvanced ) ) + { + _config.ShowAdvanced = showAdvanced; + _configChanged = true; + } + } + + private void DrawManageModsButtonOffsetButton() + { + var manageModsButtonOffset = _config.ManageModsButtonOffset; + ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + if( ImGui.DragFloat2( LabelManageModsOffset, ref manageModsButtonOffset, 1f ) ) + { + _config.ManageModsButtonOffset = manageModsButtonOffset; + _configChanged = true; } - private void DrawTempFolder() + _base._manageModsButton.ForceDraw = ImGui.IsItemActive(); + } + + private void DrawSortFoldersFirstBox() + { + var foldersFirst = _config.SortFoldersFirst; + if( ImGui.Checkbox( LabelSortFoldersFirst, ref foldersFirst ) ) { - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - ImGui.BeginGroup(); - var save = ImGui.InputText( LabelTempFolder, ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + _config.SortFoldersFirst = foldersFirst; + _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); + _configChanged = true; + } + } - ImGuiCustom.HoverTooltip( "The folder used to store temporary meta manipulation files.\n" - + "Leave this blank if you have no reason not to.\n" - + "A folder 'penumbrametatmp' will be created as a subdirectory to the specified directory.\n" - + "If none is specified (i.e. this is blank) this folder will be created in the root folder instead." ); + private void DrawScaleModSelectorBox() + { + var scaleModSelector = _config.ScaleModSelector; + if( ImGui.Checkbox( LabelScaleModSelector, ref scaleModSelector ) ) + { + _config.ScaleModSelector = scaleModSelector; + _configChanged = true; + } + } - ImGui.SameLine(); - if( ImGui.Button( LabelOpenTempFolder ) ) + private void DrawLogLoadedFilesBox() + { + ImGui.Checkbox( LabelLogLoadedFiles, ref _base._penumbra.ResourceLoader.LogAllFiles ); + ImGui.SameLine(); + var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; + var tmp = regex; + if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) + { + try { - if( !Directory.Exists( _base._modManager.TempPath.FullName ) || !_base._modManager.TempWritable ) - { - return; - } - - Process.Start( new ProcessStartInfo( _base._modManager.TempPath.FullName ) - { - UseShellExecute = true, - } ); + var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; + _base._penumbra.ResourceLoader.LogFileFilter = newRegex; } - - ImGui.EndGroup(); - if( _newTempDirectory == _config.TempDirectory ) + catch( Exception e ) { - return; - } - - if( save || DrawPressEnterWarning( _config.TempDirectory, 400 ) ) - { - _base._modManager.SetTempDirectory( _newTempDirectory ); - _newTempDirectory = _config.TempDirectory; + PluginLog.Debug( "Could not create regex:\n{Exception}", e ); } } + } - private void DrawRediscoverButton() + private void DrawDisableNotificationsBox() + { + var fsWatch = _config.DisableFileSystemNotifications; + if( ImGui.Checkbox( LabelDisableNotifications, ref fsWatch ) ) { - if( ImGui.Button( LabelRediscoverButton ) ) + _config.DisableFileSystemNotifications = fsWatch; + _configChanged = true; + } + } + + private void DrawEnableHttpApiBox() + { + var http = _config.EnableHttpApi; + if( ImGui.Checkbox( LabelEnableHttpApi, ref http ) ) + { + if( http ) { - _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods(); - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); + _base._penumbra.CreateWebServer(); } + else + { + _base._penumbra.ShutdownWebServer(); + } + + _config.EnableHttpApi = http; + _configChanged = true; + } + } + + private void DrawEnabledPlayerWatcher() + { + var enabled = _config.EnablePlayerWatch; + if( ImGui.Checkbox( LabelEnabledPlayerWatch, ref enabled ) ) + { + _config.EnablePlayerWatch = enabled; + _configChanged = true; + Penumbra.PlayerWatcher.SetStatus( enabled ); } - private void DrawOpenModsButton() - { - if( ImGui.Button( LabelOpenFolder ) ) - { - if( !Directory.Exists( _config.ModDirectory ) || !Service< ModManager >.Get().Valid ) - { - return; - } + ImGuiCustom.HoverTooltip( + "If this setting is enabled, penumbra will keep tabs on characters that have a corresponding collection setup in the Collections tab.\n" + + "Penumbra will try to automatically redraw those characters using their collection when they first appear in an instance, or when they change their current equip." ); - Process.Start( new ProcessStartInfo( _config.ModDirectory ) - { - UseShellExecute = true, - } ); - } + if( !_config.EnablePlayerWatch || !_config.ShowAdvanced ) + { + return; } - private void DrawEnabledBox() + var waitFrames = _config.WaitFrames; + ImGui.SameLine(); + ImGui.SetNextItemWidth( 50 ); + if( ImGui.InputInt( LabelWaitFrames, ref waitFrames, 0, 0 ) + && waitFrames != _config.WaitFrames + && waitFrames > 0 + && waitFrames < 3000 ) { - var enabled = _config.IsEnabled; - if( ImGui.Checkbox( LabelEnabled, ref enabled ) ) - { - _base._penumbra.SetEnabled( enabled ); - } + _base._penumbra.ObjectReloader.DefaultWaitFrames = waitFrames; + _config.WaitFrames = waitFrames; + _configChanged = true; } - private void DrawShowAdvancedBox() + ImGuiCustom.HoverTooltip( + "The number of frames penumbra waits after some events (like zone changes) until it starts trying to redraw actors again, in a range of [1, 3001].\n" + + "Keep this as low as possible while producing stable results." ); + } + + private static void DrawReloadResourceButton() + { + if( ImGui.Button( LabelReloadResource ) ) { - var showAdvanced = _config.ShowAdvanced; - if( ImGui.Checkbox( LabelShowAdvanced, ref showAdvanced ) ) - { - _config.ShowAdvanced = showAdvanced; - _configChanged = true; - } + Service< ResidentResources >.Get().ReloadPlayerResources(); + } + } + + private void DrawAdvancedSettings() + { + DrawTempFolder(); + DrawLogLoadedFilesBox(); + DrawDisableNotificationsBox(); + DrawEnableHttpApiBox(); + DrawReloadResourceButton(); + } + + public void Draw() + { + if( !ImGui.BeginTabItem( LabelTab ) ) + { + return; } - private void DrawManageModsButtonOffset() + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + DrawRootFolder(); + + DrawRediscoverButton(); + ImGui.SameLine(); + DrawOpenModsButton(); + + ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); + DrawEnabledBox(); + DrawEnabledPlayerWatcher(); + + ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); + DrawScaleModSelectorBox(); + DrawSortFoldersFirstBox(); + DrawManageModsButtonOffsetButton(); + DrawShowAdvancedBox(); + + if( _config.ShowAdvanced ) { - var manageModsButtonOffset = _config.ManageModsButtonOffset; - ImGui.SetNextItemWidth( 150f ); - if( ImGui.DragFloat2( LabelManageModsOffset, ref manageModsButtonOffset, 1f ) ) - { - _config.ManageModsButtonOffset = manageModsButtonOffset; - _configChanged = true; - } + DrawAdvancedSettings(); } - private void DrawSortFoldersFirstBox() + if( _configChanged ) { - var foldersFirst = _config.SortFoldersFirst; - if( ImGui.Checkbox( LabelSortFoldersFirst, ref foldersFirst ) ) - { - _config.SortFoldersFirst = foldersFirst; - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); - _configChanged = true; - } - } - - private void DrawScaleModSelectorBox() - { - var scaleModSelector = _config.ScaleModSelector; - if( ImGui.Checkbox( LabelScaleModSelector, ref scaleModSelector ) ) - { - _config.ScaleModSelector = scaleModSelector; - _configChanged = true; - } - } - - private void DrawLogLoadedFilesBox() - { - ImGui.Checkbox( LabelLogLoadedFiles, ref _base._penumbra.ResourceLoader.LogAllFiles ); - ImGui.SameLine(); - var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; - var tmp = regex; - if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) - { - try - { - var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; - _base._penumbra.ResourceLoader.LogFileFilter = newRegex; - } - catch( Exception e ) - { - PluginLog.Debug( "Could not create regex:\n{Exception}", e ); - } - } - } - - private void DrawDisableNotificationsBox() - { - var fsWatch = _config.DisableFileSystemNotifications; - if( ImGui.Checkbox( LabelDisableNotifications, ref fsWatch ) ) - { - _config.DisableFileSystemNotifications = fsWatch; - _configChanged = true; - } - } - - private void DrawEnableHttpApiBox() - { - var http = _config.EnableHttpApi; - if( ImGui.Checkbox( LabelEnableHttpApi, ref http ) ) - { - if( http ) - { - _base._penumbra.CreateWebServer(); - } - else - { - _base._penumbra.ShutdownWebServer(); - } - - _config.EnableHttpApi = http; - _configChanged = true; - } - } - - private void DrawEnabledPlayerWatcher() - { - var enabled = _config.EnablePlayerWatch; - if( ImGui.Checkbox( LabelEnabledPlayerWatch, ref enabled ) ) - { - _config.EnablePlayerWatch = enabled; - _configChanged = true; - Penumbra.PlayerWatcher.SetStatus( enabled ); - } - - ImGuiCustom.HoverTooltip( - "If this setting is enabled, penumbra will keep tabs on characters that have a corresponding collection setup in the Collections tab.\n" - + "Penumbra will try to automatically redraw those characters using their collection when they first appear in an instance, or when they change their current equip." ); - - if( !_config.EnablePlayerWatch || !_config.ShowAdvanced ) - { - return; - } - - var waitFrames = _config.WaitFrames; - ImGui.SameLine(); - ImGui.SetNextItemWidth( 50 ); - if( ImGui.InputInt( LabelWaitFrames, ref waitFrames, 0, 0 ) - && waitFrames != _config.WaitFrames - && waitFrames > 0 - && waitFrames < 3000 ) - { - _base._penumbra.ObjectReloader.DefaultWaitFrames = waitFrames; - _config.WaitFrames = waitFrames; - _configChanged = true; - } - - ImGuiCustom.HoverTooltip( - "The number of frames penumbra waits after some events (like zone changes) until it starts trying to redraw actors again, in a range of [1, 3001].\n" - + "Keep this as low as possible while producing stable results." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( LabelReloadResource ) ) - { - Service< ResidentResources >.Get().ReloadPlayerResources(); - } - } - - private void DrawAdvancedSettings() - { - DrawTempFolder(); - DrawLogLoadedFilesBox(); - DrawDisableNotificationsBox(); - DrawEnableHttpApiBox(); - DrawReloadResourceButton(); - } - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - DrawRootFolder(); - - DrawRediscoverButton(); - ImGui.SameLine(); - DrawOpenModsButton(); - - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); - DrawEnabledBox(); - DrawEnabledPlayerWatcher(); - - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); - DrawScaleModSelectorBox(); - DrawSortFoldersFirstBox(); - DrawManageModsButtonOffset(); - DrawShowAdvancedBox(); - - if( _config.ShowAdvanced ) - { - DrawAdvancedSettings(); - } - - if( _configChanged ) - { - _config.Save(); - _configChanged = false; - } + _config.Save(); + _configChanged = false; } } } From fa12682da6d1acc20b4764dc6830a9f094a2e1cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Nov 2021 17:04:10 +0100 Subject: [PATCH 0026/2451] Increase used dotnet-version. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54e31343..544b918e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.100 + dotnet-version: 6.0.100 - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 763348f6..cda3844a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.100 + dotnet-version: 6.0.100 - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 3bcd7a44da33fa40c95f071fba35d63d43ab0ee7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 29 Nov 2021 16:07:41 +0000 Subject: [PATCH 0027/2451] [CI] Updating repo.json for refs/tags/0.4.5.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cd344865..92371a2c 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.4.9", - "TestingAssemblyVersion": "0.4.4.9", + "AssemblyVersion": "0.4.5.0", + "TestingAssemblyVersion": "0.4.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.4.9/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 1d254e58562bc1cf5df496ddb8e3028464261f0f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Dec 2021 10:23:51 +0100 Subject: [PATCH 0028/2451] Update for 6.0 --- .../Structs/CharacterEquipment.cs | 6 +- Penumbra.PlayerWatch/CharacterFactory.cs | 2 +- Penumbra/Extensions/FuckedExtensions.cs | 81 --- Penumbra/Interop/ResidentResources.cs | 2 +- Penumbra/Interop/ResourceLoader.cs | 487 ++++++++++-------- Penumbra/Penumbra.json | 2 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- 7 files changed, 280 insertions(+), 302 deletions(-) delete mode 100644 Penumbra/Extensions/FuckedExtensions.cs diff --git a/Penumbra.GameData/Structs/CharacterEquipment.cs b/Penumbra.GameData/Structs/CharacterEquipment.cs index 84d244ae..11be31ed 100644 --- a/Penumbra.GameData/Structs/CharacterEquipment.cs +++ b/Penumbra.GameData/Structs/CharacterEquipment.cs @@ -9,9 +9,9 @@ namespace Penumbra.GameData.Structs [StructLayout( LayoutKind.Sequential, Pack = 1 )] public class CharacterEquipment { - public const int MainWeaponOffset = 0x0F08; - public const int OffWeaponOffset = 0x0F70; - public const int EquipmentOffset = 0x1040; + public const int MainWeaponOffset = 0x0C78; + public const int OffWeaponOffset = 0x0CE0; + public const int EquipmentOffset = 0xDB0; public const int EquipmentSlots = 10; public const int WeaponSlots = 2; diff --git a/Penumbra.PlayerWatch/CharacterFactory.cs b/Penumbra.PlayerWatch/CharacterFactory.cs index 9c18c29e..b88cb28f 100644 --- a/Penumbra.PlayerWatch/CharacterFactory.cs +++ b/Penumbra.PlayerWatch/CharacterFactory.cs @@ -8,7 +8,7 @@ namespace Penumbra.PlayerWatch { public static class CharacterFactory { - private static ConstructorInfo? _characterConstructor = null; + private static ConstructorInfo? _characterConstructor; private static void Initialize() { diff --git a/Penumbra/Extensions/FuckedExtensions.cs b/Penumbra/Extensions/FuckedExtensions.cs deleted file mode 100644 index f9f41413..00000000 --- a/Penumbra/Extensions/FuckedExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Reflection; -using System.Reflection.Emit; - -namespace Penumbra.Extensions -{ - public static class FuckedExtensions - { - private delegate ref TFieldType RefGet< in TObject, TFieldType >( TObject obj ); - - /// - /// Create a delegate which will return a zero-copy reference to a given field in a manner that's fucked tiers of quick and - /// fucked tiers of stupid, but hey, why not? - /// - /// - /// The only thing that this can't do is inline, this always ends up as a call instruction because we're generating code at - /// runtime and need to jump to it. That said, this is still super quick and provides a convenient and type safe shim around - /// a primitive type - /// - /// You can use the resultant to access a ref to a field on an object without invoking any - /// unsafe code too. - /// - /// The name of the field to grab a reference to - /// The object that holds the field - /// The type of the underlying field - /// A delegate that will return a reference to a particular field - zero copy - /// - private static RefGet< TObject, TField > CreateRefGetter< TObject, TField >( string fieldName ) - where TField : unmanaged - { - const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; - - var fieldInfo = typeof( TObject ).GetField( fieldName, flags ); - if( fieldInfo == null ) - { - throw new MissingFieldException( typeof( TObject ).Name, fieldName ); - } - - var dm = new DynamicMethod( - $"__refget_{typeof( TObject ).Name}_{fieldInfo.Name}", - typeof( TField ).MakeByRefType(), - new[] { typeof( TObject ) }, - typeof( TObject ), - true - ); - - var il = dm.GetILGenerator(); - - il.Emit( OpCodes.Ldarg_0 ); - il.Emit( OpCodes.Ldflda, fieldInfo ); - il.Emit( OpCodes.Ret ); - - return ( RefGet< TObject, TField > )dm.CreateDelegate( typeof( RefGet< TObject, TField > ) ); - } - - private static readonly RefGet< string, byte > StringRefGet = CreateRefGetter< string, byte >( "_firstChar" ); - - public static unsafe IntPtr UnsafePtr( this string str ) - { - // nb: you can do it without __makeref but the code becomes way shittier because the way of getting the ptr - // is more fucked up so it's easier to just abuse __makeref - // but you can just use the StringRefGet func to get a `ref byte` too, though you'll probs want a better delegate so it's - // actually usable, lol - var fieldRef = __makeref( StringRefGet( str ) ); - - return *( IntPtr* )&fieldRef; - } - - public static unsafe int UnsafeLength( this string str ) - { - var fieldRef = __makeref( StringRefGet( str ) ); - - // c# strings are utf16 so we just multiply len by 2 to get the total byte count + 2 for null terminator (:D) - // very simple and intuitive - - // this also maps to a defined structure, so you can just move the pointer backwards to read from the native string struct - // see: https://github.com/dotnet/coreclr/blob/master/src/vm/object.h#L897-L909 - return *( int* )( *( IntPtr* )&fieldRef - 4 ) * 2 + 2; - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResources.cs b/Penumbra/Interop/ResidentResources.cs index 4a2cc2b9..9bf6d93a 100644 --- a/Penumbra/Interop/ResidentResources.cs +++ b/Penumbra/Interop/ResidentResources.cs @@ -45,7 +45,7 @@ namespace Penumbra.Interop var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64(); var loadPlayerResourcesAddress = Dalamud.SigScanner.ScanText( - "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A " ); + "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" ); GeneralUtil.PrintDebugAddress( "LoadPlayerResources", loadPlayerResourcesAddress ); var unloadPlayerResourcesAddress = diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 8d4c5937..0ce2be54 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -11,262 +11,321 @@ using Penumbra.Structs; using Penumbra.Util; using FileMode = Penumbra.Structs.FileMode; -namespace Penumbra.Interop +namespace Penumbra.Interop; + +public class ResourceLoader : IDisposable { - public class ResourceLoader : IDisposable + public Penumbra Penumbra { get; set; } + + public bool IsEnabled { get; set; } + + public Crc32 Crc32 { get; } + + + // Delegate prototypes + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public unsafe delegate byte ReadFilePrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType + , uint* pResourceHash, char* pPath, void* pUnknown ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType + , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public delegate bool CheckFileStatePrototype( IntPtr unk1, ulong unk2 ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public delegate byte LoadTexFileExternPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public delegate byte LoadTexFileLocalPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3 ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public delegate byte LoadMdlFileExternPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr unk3 ); + + [UnmanagedFunctionPointer( CallingConvention.ThisCall )] + public delegate byte LoadMdlFileLocalPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2 ); + + // Hooks + public Hook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } + public Hook< GetResourceAsyncPrototype >? GetResourceAsyncHook { get; private set; } + public Hook< ReadSqpackPrototype >? ReadSqpackHook { get; private set; } + public Hook< CheckFileStatePrototype >? CheckFileStateHook { get; private set; } + public Hook< LoadTexFileExternPrototype >? LoadTexFileExternHook { get; private set; } + public Hook< LoadMdlFileExternPrototype >? LoadMdlFileExternHook { get; private set; } + + // Unmanaged functions + public ReadFilePrototype? ReadFile { get; private set; } + public CheckFileStatePrototype? CheckFileState { get; private set; } + public LoadTexFileLocalPrototype? LoadTexFileLocal { get; private set; } + public LoadMdlFileLocalPrototype? LoadMdlFileLocal { get; private set; } + + public bool LogAllFiles = false; + public Regex? LogFileFilter = null; + + + public ResourceLoader( Penumbra penumbra ) { - public Penumbra Penumbra { get; set; } + Penumbra = penumbra; + Crc32 = new Crc32(); + } - public bool IsEnabled { get; set; } + public unsafe void Init() + { + var readFileAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" ); + GeneralUtil.PrintDebugAddress( "ReadFile", readFileAddress ); - public Crc32 Crc32 { get; } + var readSqpackAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3" ); + GeneralUtil.PrintDebugAddress( "ReadSqPack", readSqpackAddress ); + var getResourceSyncAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00" ); + GeneralUtil.PrintDebugAddress( "GetResourceSync", getResourceSyncAddress ); - // Delegate prototypes - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadFilePrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + var getResourceAsyncAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00" ); + GeneralUtil.PrintDebugAddress( "GetResourceAsync", getResourceAsyncAddress ); - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + var checkFileStateAddress = Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24" ); + GeneralUtil.PrintDebugAddress( "CheckFileState", checkFileStateAddress ); - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown ); + var loadTexFileLocalAddress = + Dalamud.SigScanner.ScanText( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" ); + GeneralUtil.PrintDebugAddress( "LoadTexFileLocal", loadTexFileLocalAddress ); - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); + var loadTexFileExternAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8" ); + GeneralUtil.PrintDebugAddress( "LoadTexFileExtern", loadTexFileExternAddress ); - // Hooks - public Hook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } - public Hook< GetResourceAsyncPrototype >? GetResourceAsyncHook { get; private set; } - public Hook< ReadSqpackPrototype >? ReadSqpackHook { get; private set; } + var loadMdlFileLocalAddress = + Dalamud.SigScanner.ScanText( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" ); + GeneralUtil.PrintDebugAddress( "LoadMdlFileLocal", loadMdlFileLocalAddress ); - // Unmanaged functions - public ReadFilePrototype? ReadFile { get; private set; } + var loadMdlFileExternAddress = + Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 02 B0 F1" ); + GeneralUtil.PrintDebugAddress( "LoadMdlFileExtern", loadMdlFileExternAddress ); + ReadSqpackHook = new Hook< ReadSqpackPrototype >( readSqpackAddress, ReadSqpackHandler ); + GetResourceSyncHook = new Hook< GetResourceSyncPrototype >( getResourceSyncAddress, GetResourceSyncHandler ); + GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( getResourceAsyncAddress, GetResourceAsyncHandler ); - public bool LogAllFiles = false; - public Regex? LogFileFilter = null; + ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress ); + LoadTexFileLocal = Marshal.GetDelegateForFunctionPointer< LoadTexFileLocalPrototype >( loadTexFileLocalAddress ); + LoadMdlFileLocal = Marshal.GetDelegateForFunctionPointer< LoadMdlFileLocalPrototype >( loadMdlFileLocalAddress ); + CheckFileStateHook = new Hook< CheckFileStatePrototype >( checkFileStateAddress, CheckFileStateDetour ); + LoadTexFileExternHook = new Hook< LoadTexFileExternPrototype >( loadTexFileExternAddress, LoadTexFileExternDetour ); + LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); + } - public ResourceLoader( Penumbra penumbra ) + private static bool CheckFileStateDetour( IntPtr _, ulong _2 ) + => true; + + private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr _ ) + => LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ); + + private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr _ ) + => LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ); + + private unsafe void* GetResourceSyncHandler( + IntPtr pFileManager, + uint* pCategoryId, + char* pResourceType, + uint* pResourceHash, + char* pPath, + void* pUnknown + ) + => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); + + private unsafe void* GetResourceAsyncHandler( + IntPtr pFileManager, + uint* pCategoryId, + char* pResourceType, + uint* pResourceHash, + char* pPath, + void* pUnknown, + bool isUnknown + ) + => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + + private unsafe void* CallOriginalHandler( + bool isSync, + IntPtr pFileManager, + uint* pCategoryId, + char* pResourceType, + uint* pResourceHash, + char* pPath, + void* pUnknown, + bool isUnknown + ) + { + if( isSync ) { - Penumbra = penumbra; - Crc32 = new Crc32(); - } - - public unsafe void Init() - { - var readFileAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" ); - GeneralUtil.PrintDebugAddress( "ReadFile", readFileAddress ); - - var readSqpackAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3" ); - GeneralUtil.PrintDebugAddress( "ReadSqPack", readSqpackAddress ); - - var getResourceSyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceSync", getResourceSyncAddress ); - - var getResourceAsyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceAsync", getResourceAsyncAddress ); - - - ReadSqpackHook = new Hook< ReadSqpackPrototype >( readSqpackAddress, ReadSqpackHandler ); - GetResourceSyncHook = new Hook< GetResourceSyncPrototype >( getResourceSyncAddress, GetResourceSyncHandler ); - GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( getResourceAsyncAddress, GetResourceAsyncHandler ); - - ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress ); - } - - - private unsafe void* GetResourceSyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown - ) - => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); - - private unsafe void* GetResourceAsyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - - private unsafe void* CallOriginalHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - { - if( isSync ) + if( GetResourceSyncHook == null ) { - if( GetResourceSyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceSync is null." ); - return null; - } - - return GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown ); - } - - if( GetResourceAsyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceAsync is null." ); + PluginLog.Error( "[GetResourceHandler] GetResourceSync is null." ); return null; } - return GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + return GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown ); } - private unsafe void* GetResourceHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) + if( GetResourceAsyncHook == null ) { - string file; - var modManager = Service< ModManager >.Get(); + PluginLog.Error( "[GetResourceHandler] GetResourceAsync is null." ); + return null; + } - if( !Penumbra.Config.IsEnabled || modManager == null ) + return GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + } + + private unsafe void* GetResourceHandler( + bool isSync, + IntPtr pFileManager, + uint* pCategoryId, + char* pResourceType, + uint* pResourceHash, + char* pPath, + void* pUnknown, + bool isUnknown + ) + { + string file; + var modManager = Service< ModManager >.Get(); + + if( !Penumbra.Config.IsEnabled || modManager == null ) + { + if( LogAllFiles ) { - if( LogAllFiles ) + file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; + if( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) { - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - if( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) - { - PluginLog.Information( "[GetResourceHandler] {0}", file ); - } + PluginLog.Information( "[GetResourceHandler] {0}", file ); } - - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - var gameFsPath = GamePath.GenerateUncheckedLower( file ); - var replacementPath = modManager.ResolveSwappedOrReplacementPath( gameFsPath ); - if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) - { - PluginLog.Information( "[GetResourceHandler] {0}", file ); - } - - // path must be < 260 because statically defined array length :( - if( replacementPath == null ) - { - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - var path = Encoding.ASCII.GetBytes( replacementPath ); - - var bPath = stackalloc byte[path.Length + 1]; - Marshal.Copy( path, 0, new IntPtr( bPath ), path.Length ); - pPath = ( char* )bPath; - - Crc32.Init(); - Crc32.Update( path ); - *pResourceHash = Crc32.Checksum; - - PluginLog.Verbose( "[GetResourceHandler] resolved {GamePath} to {NewPath}", gameFsPath, replacementPath ); - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } - - private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) + file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; + var gameFsPath = GamePath.GenerateUncheckedLower( file ); + var replacementPath = modManager.ResolveSwappedOrReplacementPath( gameFsPath ); + if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) { - if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) - { - PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); - - var isRooted = Path.IsPathRooted( gameFsPath ); - - if( gameFsPath == null || gameFsPath.Length >= 260 || !isRooted ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - PluginLog.Debug( "loading modded file: {GameFsPath}", gameFsPath ); - - pFileDesc->FileMode = FileMode.LoadUnpackedResource; - - // note: must be utf16 - var utfPath = Encoding.Unicode.GetBytes( gameFsPath ); - - Marshal.Copy( utfPath, 0, new IntPtr( &pFileDesc->UtfFileName ), utfPath.Length ); - - var fd = stackalloc byte[0x20 + utfPath.Length + 0x16]; - Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); - - pFileDesc->FileDescriptor = fd; - - return ReadFile( pFileHandler, pFileDesc, priority, isSync ); + PluginLog.Information( "[GetResourceHandler] {0}", file ); } - public void Enable() + // path must be < 260 because statically defined array length :( + if( replacementPath == null ) { - if( IsEnabled ) - { - return; - } - - if( ReadSqpackHook == null || GetResourceSyncHook == null || GetResourceAsyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] Could not activate hooks because at least one was not set." ); - return; - } - - ReadSqpackHook.Enable(); - GetResourceSyncHook.Enable(); - GetResourceAsyncHook.Enable(); - - IsEnabled = true; + return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } - public void Disable() + var path = Encoding.ASCII.GetBytes( replacementPath ); + + var bPath = stackalloc byte[path.Length + 1]; + Marshal.Copy( path, 0, new IntPtr( bPath ), path.Length ); + pPath = ( char* )bPath; + + Crc32.Init(); + Crc32.Update( path ); + *pResourceHash = Crc32.Checksum; + + PluginLog.Verbose( "[GetResourceHandler] resolved {GamePath} to {NewPath}", gameFsPath, replacementPath ); + + return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + } + + + private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) + { + if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) { - if( !IsEnabled ) - { - return; - } - - ReadSqpackHook?.Disable(); - GetResourceSyncHook?.Disable(); - GetResourceAsyncHook?.Disable(); - - IsEnabled = false; + PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); + return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; } - public void Dispose() + var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); + + var isRooted = Path.IsPathRooted( gameFsPath ); + + if( gameFsPath == null || gameFsPath.Length >= 260 || !isRooted ) { - Disable(); - ReadSqpackHook?.Dispose(); - GetResourceSyncHook?.Dispose(); - GetResourceAsyncHook?.Dispose(); + return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; } + + PluginLog.Debug( "loading modded file: {GameFsPath}", gameFsPath ); + + pFileDesc->FileMode = FileMode.LoadUnpackedResource; + + // note: must be utf16 + var utfPath = Encoding.Unicode.GetBytes( gameFsPath ); + + Marshal.Copy( utfPath, 0, new IntPtr( &pFileDesc->UtfFileName ), utfPath.Length ); + + var fd = stackalloc byte[0x20 + utfPath.Length + 0x16]; + Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); + + pFileDesc->FileDescriptor = fd; + + return ReadFile( pFileHandler, pFileDesc, priority, isSync ); + } + + public void Enable() + { + if( IsEnabled ) + { + return; + } + + if( ReadSqpackHook == null || GetResourceSyncHook == null || GetResourceAsyncHook == null || CheckFileStateHook == null || LoadTexFileExternHook == null || LoadMdlFileExternHook == null) + { + PluginLog.Error( "[GetResourceHandler] Could not activate hooks because at least one was not set." ); + return; + } + + ReadSqpackHook.Enable(); + GetResourceSyncHook.Enable(); + GetResourceAsyncHook.Enable(); + CheckFileStateHook.Enable(); + LoadTexFileExternHook.Enable(); + LoadMdlFileExternHook.Enable(); + + IsEnabled = true; + } + + public void Disable() + { + if( !IsEnabled ) + { + return; + } + + ReadSqpackHook?.Disable(); + GetResourceSyncHook?.Disable(); + GetResourceAsyncHook?.Disable(); + CheckFileStateHook?.Disable(); + LoadTexFileExternHook?.Disable(); + LoadMdlFileExternHook?.Disable(); + IsEnabled = false; + } + + public void Dispose() + { + Disable(); + ReadSqpackHook?.Dispose(); + GetResourceSyncHook?.Dispose(); + GetResourceAsyncHook?.Dispose(); + CheckFileStateHook?.Dispose(); + LoadTexFileExternHook?.Dispose(); + LoadMdlFileExternHook?.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index ec60e72c..85b613eb 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -7,7 +7,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 69420, + "DalamudApiLevel": 5, "LoadPriority": 69420, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 344c4d70..ecdaa87e 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -94,7 +94,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1D93AC0 ); + var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E5B440 ); if( resourceHandler == null ) { From 63268512dbf804a0aad4c979f00669fe820fa754 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 10 Dec 2021 09:26:14 +0000 Subject: [PATCH 0029/2451] [CI] Updating repo.json for refs/tags/0.4.6.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 92371a2c..78a60dc1 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.5.0", - "TestingAssemblyVersion": "0.4.5.0", + "AssemblyVersion": "0.4.6.0", + "TestingAssemblyVersion": "0.4.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 4, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.5.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f66e08431c3d2bef3fbbb52e43d78d421231ecf6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Dec 2021 10:49:26 +0100 Subject: [PATCH 0030/2451] Increase Base Repo API --- base_repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_repo.json b/base_repo.json index 2e3a2a06..2a3a2145 100644 --- a/base_repo.json +++ b/base_repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "1.0.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 4, + "DalamudApiLevel": 5, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 393859232d3d21b5d87799d93745b1eaec454061 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 10 Dec 2021 09:51:32 +0000 Subject: [PATCH 0031/2451] [CI] Updating repo.json for refs/tags/0.4.6.1 --- repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/repo.json b/repo.json index 78a60dc1..14146e36 100644 --- a/repo.json +++ b/repo.json @@ -4,19 +4,19 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.0", - "TestingAssemblyVersion": "0.4.6.0", + "AssemblyVersion": "0.4.6.1", + "TestingAssemblyVersion": "0.4.6.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 4, + "DalamudApiLevel": 5, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From dbcd847736bf9ac643ce1e86e4dcc7a4a5c98705 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Dec 2021 16:54:36 +0100 Subject: [PATCH 0032/2451] Fix GPose Redrawing --- Penumbra/Interop/ObjectReloader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index b8000f9e..03af4650 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -85,7 +85,7 @@ namespace Penumbra.Interop if( _inGPose ) { var ptr = ( void*** )actor.Address; - var disableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 17 ] ) ); + var disableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 18 ] ) ); disableDraw( actor.Address ); } } @@ -119,7 +119,7 @@ namespace Penumbra.Interop if( _inGPose ) { var ptr = ( void*** )actor.Address; - var enableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 16 ] ) ); + var enableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 17 ] ) ); enableDraw( actor.Address ); } } From 3458a2920be164b414515727ab4adf783ecf5b58 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Dec 2021 16:54:59 +0100 Subject: [PATCH 0033/2451] Add Miera (and Hrothgals) to enums. --- Penumbra.GameData/Enums/Race.cs | 861 ++++++++++++++++---------------- 1 file changed, 440 insertions(+), 421 deletions(-) diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index 8221a625..a83990ee 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -2,452 +2,471 @@ using System; using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum Race : byte { - public enum Race : byte + Unknown, + Hyur, + Elezen, + Lalafell, + Miqote, + Roegadyn, + AuRa, + Hrothgar, + Viera, +} + +public enum Gender : byte +{ + Unknown, + Male, + Female, + MaleNpc, + FemaleNpc, +} + +public enum ModelRace : byte +{ + Unknown, + Midlander, + Highlander, + Elezen, + Lalafell, + Miqote, + Roegadyn, + AuRa, + Hrothgar, + Viera, +} + +public enum SubRace : byte +{ + Unknown, + Midlander, + Highlander, + Wildwood, + Duskwight, + Plainsfolk, + Dunesfolk, + SeekerOfTheSun, + KeeperOfTheMoon, + Seawolf, + Hellsguard, + Raen, + Xaela, + Helion, + Lost, + Rava, + Veena, +} + +// The combined gender-race-npc numerical code as used by the game. +public enum GenderRace : ushort +{ + Unknown = 0, + MidlanderMale = 0101, + MidlanderMaleNpc = 0104, + MidlanderFemale = 0201, + MidlanderFemaleNpc = 0204, + HighlanderMale = 0301, + HighlanderMaleNpc = 0304, + HighlanderFemale = 0401, + HighlanderFemaleNpc = 0404, + ElezenMale = 0501, + ElezenMaleNpc = 0504, + ElezenFemale = 0601, + ElezenFemaleNpc = 0604, + MiqoteMale = 0701, + MiqoteMaleNpc = 0704, + MiqoteFemale = 0801, + MiqoteFemaleNpc = 0804, + RoegadynMale = 0901, + RoegadynMaleNpc = 0904, + RoegadynFemale = 1001, + RoegadynFemaleNpc = 1004, + LalafellMale = 1101, + LalafellMaleNpc = 1104, + LalafellFemale = 1201, + LalafellFemaleNpc = 1204, + AuRaMale = 1301, + AuRaMaleNpc = 1304, + AuRaFemale = 1401, + AuRaFemaleNpc = 1404, + HrothgarMale = 1501, + HrothgarMaleNpc = 1504, + HrothgarFemale = 1601, + HrothgarFemaleNpc = 1604, + VieraMale = 1701, + VieraMaleNpc = 1704, + VieraFemale = 1801, + VieraFemaleNpc = 1804, + UnknownMaleNpc = 9104, + UnknownFemaleNpc = 9204, +} + +public static class RaceEnumExtensions +{ + public static int ToRspIndex( this SubRace subRace ) { - Unknown, - Hyur, - Elezen, - Lalafell, - Miqote, - Roegadyn, - AuRa, - Hrothgar, - Viera, + return subRace switch + { + SubRace.Midlander => 0, + SubRace.Highlander => 1, + SubRace.Wildwood => 10, + SubRace.Duskwight => 11, + SubRace.Plainsfolk => 20, + SubRace.Dunesfolk => 21, + SubRace.SeekerOfTheSun => 30, + SubRace.KeeperOfTheMoon => 31, + SubRace.Seawolf => 40, + SubRace.Hellsguard => 41, + SubRace.Raen => 50, + SubRace.Xaela => 51, + SubRace.Helion => 60, + SubRace.Lost => 61, + SubRace.Rava => 70, + SubRace.Veena => 71, + _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), + }; } - public enum Gender : byte + public static Race ToRace( this ModelRace race ) { - Unknown, - Male, - Female, - MaleNpc, - FemaleNpc, + return race switch + { + ModelRace.Unknown => Race.Unknown, + ModelRace.Midlander => Race.Hyur, + ModelRace.Highlander => Race.Hyur, + ModelRace.Elezen => Race.Elezen, + ModelRace.Lalafell => Race.Lalafell, + ModelRace.Miqote => Race.Miqote, + ModelRace.Roegadyn => Race.Roegadyn, + ModelRace.AuRa => Race.AuRa, + ModelRace.Hrothgar => Race.Hrothgar, + ModelRace.Viera => Race.Viera, + _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), + }; } - public enum ModelRace : byte + public static Race ToRace( this SubRace subRace ) { - Unknown, - Midlander, - Highlander, - Elezen, - Lalafell, - Miqote, - Roegadyn, - AuRa, - Hrothgar, - Viera, + return subRace switch + { + SubRace.Unknown => Race.Unknown, + SubRace.Midlander => Race.Hyur, + SubRace.Highlander => Race.Hyur, + SubRace.Wildwood => Race.Elezen, + SubRace.Duskwight => Race.Elezen, + SubRace.Plainsfolk => Race.Lalafell, + SubRace.Dunesfolk => Race.Lalafell, + SubRace.SeekerOfTheSun => Race.Miqote, + SubRace.KeeperOfTheMoon => Race.Miqote, + SubRace.Seawolf => Race.Roegadyn, + SubRace.Hellsguard => Race.Roegadyn, + SubRace.Raen => Race.AuRa, + SubRace.Xaela => Race.AuRa, + SubRace.Helion => Race.Hrothgar, + SubRace.Lost => Race.Hrothgar, + SubRace.Rava => Race.Viera, + SubRace.Veena => Race.Viera, + _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), + }; } - public enum SubRace : byte + public static string ToName( this ModelRace modelRace ) { - Unknown, - Midlander, - Highlander, - Wildwood, - Duskwight, - Plainsfolk, - Dunesfolk, - SeekerOfTheSun, - KeeperOfTheMoon, - Seawolf, - Hellsguard, - Raen, - Xaela, - Helion, - Lost, - Rava, - Veena, + return modelRace switch + { + ModelRace.Midlander => SubRace.Midlander.ToName(), + ModelRace.Highlander => SubRace.Highlander.ToName(), + ModelRace.Elezen => Race.Elezen.ToName(), + ModelRace.Lalafell => Race.Lalafell.ToName(), + ModelRace.Miqote => Race.Miqote.ToName(), + ModelRace.Roegadyn => Race.Roegadyn.ToName(), + ModelRace.AuRa => Race.AuRa.ToName(), + ModelRace.Hrothgar => Race.Hrothgar.ToName(), + ModelRace.Viera => Race.Viera.ToName(), + _ => throw new ArgumentOutOfRangeException( nameof( modelRace ), modelRace, null ), + }; } - // The combined gender-race-npc numerical code as used by the game. - public enum GenderRace : ushort + public static string ToName( this Race race ) { - Unknown = 0, - MidlanderMale = 0101, - MidlanderMaleNpc = 0104, - MidlanderFemale = 0201, - MidlanderFemaleNpc = 0204, - HighlanderMale = 0301, - HighlanderMaleNpc = 0304, - HighlanderFemale = 0401, - HighlanderFemaleNpc = 0404, - ElezenMale = 0501, - ElezenMaleNpc = 0504, - ElezenFemale = 0601, - ElezenFemaleNpc = 0604, - MiqoteMale = 0701, - MiqoteMaleNpc = 0704, - MiqoteFemale = 0801, - MiqoteFemaleNpc = 0804, - RoegadynMale = 0901, - RoegadynMaleNpc = 0904, - RoegadynFemale = 1001, - RoegadynFemaleNpc = 1004, - LalafellMale = 1101, - LalafellMaleNpc = 1104, - LalafellFemale = 1201, - LalafellFemaleNpc = 1204, - AuRaMale = 1301, - AuRaMaleNpc = 1304, - AuRaFemale = 1401, - AuRaFemaleNpc = 1404, - HrothgarMale = 1501, - HrothgarMaleNpc = 1504, - VieraFemale = 1801, - VieraFemaleNpc = 1804, - UnknownMaleNpc = 9104, - UnknownFemaleNpc = 9204, + return race switch + { + Race.Hyur => "Hyur", + Race.Elezen => "Elezen", + Race.Lalafell => "Lalafell", + Race.Miqote => "Miqo'te", + Race.Roegadyn => "Roegadyn", + Race.AuRa => "Au Ra", + Race.Hrothgar => "Hrothgar", + Race.Viera => "Viera", + _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), + }; } - public static class RaceEnumExtensions + public static string ToName( this Gender gender ) { - public static int ToRspIndex( this SubRace subRace ) + return gender switch { - return subRace switch - { - SubRace.Midlander => 0, - SubRace.Highlander => 1, - SubRace.Wildwood => 10, - SubRace.Duskwight => 11, - SubRace.Plainsfolk => 20, - SubRace.Dunesfolk => 21, - SubRace.SeekerOfTheSun => 30, - SubRace.KeeperOfTheMoon => 31, - SubRace.Seawolf => 40, - SubRace.Hellsguard => 41, - SubRace.Raen => 50, - SubRace.Xaela => 51, - SubRace.Helion => 60, - SubRace.Lost => 61, - SubRace.Rava => 70, - SubRace.Veena => 71, - _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), - }; - } - - public static Race ToRace( this ModelRace race ) - { - return race switch - { - ModelRace.Unknown => Race.Unknown, - ModelRace.Midlander => Race.Hyur, - ModelRace.Highlander => Race.Hyur, - ModelRace.Elezen => Race.Elezen, - ModelRace.Lalafell => Race.Lalafell, - ModelRace.Miqote => Race.Miqote, - ModelRace.Roegadyn => Race.Roegadyn, - ModelRace.AuRa => Race.AuRa, - ModelRace.Hrothgar => Race.Hrothgar, - ModelRace.Viera => Race.Viera, - _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), - }; - } - - public static Race ToRace( this SubRace subRace ) - { - return subRace switch - { - SubRace.Unknown => Race.Unknown, - SubRace.Midlander => Race.Hyur, - SubRace.Highlander => Race.Hyur, - SubRace.Wildwood => Race.Elezen, - SubRace.Duskwight => Race.Elezen, - SubRace.Plainsfolk => Race.Lalafell, - SubRace.Dunesfolk => Race.Lalafell, - SubRace.SeekerOfTheSun => Race.Miqote, - SubRace.KeeperOfTheMoon => Race.Miqote, - SubRace.Seawolf => Race.Roegadyn, - SubRace.Hellsguard => Race.Roegadyn, - SubRace.Raen => Race.AuRa, - SubRace.Xaela => Race.AuRa, - SubRace.Helion => Race.Hrothgar, - SubRace.Lost => Race.Hrothgar, - SubRace.Rava => Race.Viera, - SubRace.Veena => Race.Viera, - _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), - }; - } - - public static string ToName( this ModelRace modelRace ) - { - return modelRace switch - { - ModelRace.Midlander => SubRace.Midlander.ToName(), - ModelRace.Highlander => SubRace.Highlander.ToName(), - ModelRace.Elezen => Race.Elezen.ToName(), - ModelRace.Lalafell => Race.Lalafell.ToName(), - ModelRace.Miqote => Race.Miqote.ToName(), - ModelRace.Roegadyn => Race.Roegadyn.ToName(), - ModelRace.AuRa => Race.AuRa.ToName(), - ModelRace.Hrothgar => Race.Hrothgar.ToName(), - ModelRace.Viera => Race.Viera.ToName(), - _ => throw new ArgumentOutOfRangeException( nameof( modelRace ), modelRace, null ), - }; - } - - public static string ToName( this Race race ) - { - return race switch - { - Race.Hyur => "Hyur", - Race.Elezen => "Elezen", - Race.Lalafell => "Lalafell", - Race.Miqote => "Miqo'te", - Race.Roegadyn => "Roegadyn", - Race.AuRa => "Au Ra", - Race.Hrothgar => "Hrothgar", - Race.Viera => "Viera", - _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), - }; - } - - public static string ToName( this Gender gender ) - { - return gender switch - { - Gender.Male => "Male", - Gender.Female => "Female", - Gender.MaleNpc => "Male (NPC)", - Gender.FemaleNpc => "Female (NPC)", - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static string ToName( this SubRace subRace ) - { - return subRace switch - { - SubRace.Midlander => "Midlander", - SubRace.Highlander => "Highlander", - SubRace.Wildwood => "Wildwood", - SubRace.Duskwight => "Duskwright", - SubRace.Plainsfolk => "Plainsfolk", - SubRace.Dunesfolk => "Dunesfolk", - SubRace.SeekerOfTheSun => "Seeker Of The Sun", - SubRace.KeeperOfTheMoon => "Keeper Of The Moon", - SubRace.Seawolf => "Seawolf", - SubRace.Hellsguard => "Hellsguard", - SubRace.Raen => "Raen", - SubRace.Xaela => "Xaela", - SubRace.Helion => "Hellion", - SubRace.Lost => "Lost", - SubRace.Rava => "Rava", - SubRace.Veena => "Veena", - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static bool FitsRace( this SubRace subRace, Race race ) - => subRace.ToRace() == race; - - public static byte ToByte( this Gender gender, ModelRace modelRace ) - => ( byte )( ( int )gender | ( ( int )modelRace << 3 ) ); - - public static byte ToByte( this ModelRace modelRace, Gender gender ) - => gender.ToByte( modelRace ); - - public static byte ToByte( this GenderRace value ) - { - var (gender, race) = value.Split(); - return gender.ToByte( race ); - } - - public static (Gender, ModelRace) Split( this GenderRace value ) - { - return value switch - { - GenderRace.Unknown => ( Gender.Unknown, ModelRace.Unknown ), - GenderRace.MidlanderMale => ( Gender.Male, ModelRace.Midlander ), - GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Midlander ), - GenderRace.MidlanderFemale => ( Gender.Female, ModelRace.Midlander ), - GenderRace.MidlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Midlander ), - GenderRace.HighlanderMale => ( Gender.Male, ModelRace.Highlander ), - GenderRace.HighlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Highlander ), - GenderRace.HighlanderFemale => ( Gender.Female, ModelRace.Highlander ), - GenderRace.HighlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Highlander ), - GenderRace.ElezenMale => ( Gender.Male, ModelRace.Elezen ), - GenderRace.ElezenMaleNpc => ( Gender.MaleNpc, ModelRace.Elezen ), - GenderRace.ElezenFemale => ( Gender.Female, ModelRace.Elezen ), - GenderRace.ElezenFemaleNpc => ( Gender.FemaleNpc, ModelRace.Elezen ), - GenderRace.LalafellMale => ( Gender.Male, ModelRace.Lalafell ), - GenderRace.LalafellMaleNpc => ( Gender.MaleNpc, ModelRace.Lalafell ), - GenderRace.LalafellFemale => ( Gender.Female, ModelRace.Lalafell ), - GenderRace.LalafellFemaleNpc => ( Gender.FemaleNpc, ModelRace.Lalafell ), - GenderRace.MiqoteMale => ( Gender.Male, ModelRace.Miqote ), - GenderRace.MiqoteMaleNpc => ( Gender.MaleNpc, ModelRace.Miqote ), - GenderRace.MiqoteFemale => ( Gender.Female, ModelRace.Miqote ), - GenderRace.MiqoteFemaleNpc => ( Gender.FemaleNpc, ModelRace.Miqote ), - GenderRace.RoegadynMale => ( Gender.Male, ModelRace.Roegadyn ), - GenderRace.RoegadynMaleNpc => ( Gender.MaleNpc, ModelRace.Roegadyn ), - GenderRace.RoegadynFemale => ( Gender.Female, ModelRace.Roegadyn ), - GenderRace.RoegadynFemaleNpc => ( Gender.FemaleNpc, ModelRace.Roegadyn ), - GenderRace.AuRaMale => ( Gender.Male, ModelRace.AuRa ), - GenderRace.AuRaMaleNpc => ( Gender.MaleNpc, ModelRace.AuRa ), - GenderRace.AuRaFemale => ( Gender.Female, ModelRace.AuRa ), - GenderRace.AuRaFemaleNpc => ( Gender.FemaleNpc, ModelRace.AuRa ), - GenderRace.HrothgarMale => ( Gender.Male, ModelRace.Hrothgar ), - GenderRace.HrothgarMaleNpc => ( Gender.MaleNpc, ModelRace.Hrothgar ), - GenderRace.VieraFemale => ( Gender.Female, ModelRace.Viera ), - GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, ModelRace.Viera ), - GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, ModelRace.Unknown ), - GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, ModelRace.Unknown ), - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static bool IsValid( this GenderRace value ) - => value != GenderRace.Unknown && Enum.IsDefined( typeof( GenderRace ), value ); - - public static string ToRaceCode( this GenderRace value ) - { - return value switch - { - GenderRace.MidlanderMale => "0101", - GenderRace.MidlanderMaleNpc => "0104", - GenderRace.MidlanderFemale => "0201", - GenderRace.MidlanderFemaleNpc => "0204", - GenderRace.HighlanderMale => "0301", - GenderRace.HighlanderMaleNpc => "0304", - GenderRace.HighlanderFemale => "0401", - GenderRace.HighlanderFemaleNpc => "0404", - GenderRace.ElezenMale => "0501", - GenderRace.ElezenMaleNpc => "0504", - GenderRace.ElezenFemale => "0601", - GenderRace.ElezenFemaleNpc => "0604", - GenderRace.MiqoteMale => "0701", - GenderRace.MiqoteMaleNpc => "0704", - GenderRace.MiqoteFemale => "0801", - GenderRace.MiqoteFemaleNpc => "0804", - GenderRace.RoegadynMale => "0901", - GenderRace.RoegadynMaleNpc => "0904", - GenderRace.RoegadynFemale => "1001", - GenderRace.RoegadynFemaleNpc => "1004", - GenderRace.LalafellMale => "1101", - GenderRace.LalafellMaleNpc => "1104", - GenderRace.LalafellFemale => "1201", - GenderRace.LalafellFemaleNpc => "1204", - GenderRace.AuRaMale => "1301", - GenderRace.AuRaMaleNpc => "1304", - GenderRace.AuRaFemale => "1401", - GenderRace.AuRaFemaleNpc => "1404", - GenderRace.HrothgarMale => "1501", - GenderRace.HrothgarMaleNpc => "1504", - GenderRace.VieraFemale => "1801", - GenderRace.VieraFemaleNpc => "1804", - GenderRace.UnknownMaleNpc => "9104", - GenderRace.UnknownFemaleNpc => "9204", - _ => throw new InvalidEnumArgumentException(), - }; - } + Gender.Male => "Male", + Gender.Female => "Female", + Gender.MaleNpc => "Male (NPC)", + Gender.FemaleNpc => "Female (NPC)", + _ => throw new InvalidEnumArgumentException(), + }; } - public static partial class Names + public static string ToName( this SubRace subRace ) { - public static GenderRace GenderRaceFromCode( string code ) + return subRace switch { - return code switch - { - "0101" => GenderRace.MidlanderMale, - "0104" => GenderRace.MidlanderMaleNpc, - "0201" => GenderRace.MidlanderFemale, - "0204" => GenderRace.MidlanderFemaleNpc, - "0301" => GenderRace.HighlanderMale, - "0304" => GenderRace.HighlanderMaleNpc, - "0401" => GenderRace.HighlanderFemale, - "0404" => GenderRace.HighlanderFemaleNpc, - "0501" => GenderRace.ElezenMale, - "0504" => GenderRace.ElezenMaleNpc, - "0601" => GenderRace.ElezenFemale, - "0604" => GenderRace.ElezenFemaleNpc, - "0701" => GenderRace.MiqoteMale, - "0704" => GenderRace.MiqoteMaleNpc, - "0801" => GenderRace.MiqoteFemale, - "0804" => GenderRace.MiqoteFemaleNpc, - "0901" => GenderRace.RoegadynMale, - "0904" => GenderRace.RoegadynMaleNpc, - "1001" => GenderRace.RoegadynFemale, - "1004" => GenderRace.RoegadynFemaleNpc, - "1101" => GenderRace.LalafellMale, - "1104" => GenderRace.LalafellMaleNpc, - "1201" => GenderRace.LalafellFemale, - "1204" => GenderRace.LalafellFemaleNpc, - "1301" => GenderRace.AuRaMale, - "1304" => GenderRace.AuRaMaleNpc, - "1401" => GenderRace.AuRaFemale, - "1404" => GenderRace.AuRaFemaleNpc, - "1501" => GenderRace.HrothgarMale, - "1504" => GenderRace.HrothgarMaleNpc, - "1801" => GenderRace.VieraFemale, - "1804" => GenderRace.VieraFemaleNpc, - "9104" => GenderRace.UnknownMaleNpc, - "9204" => GenderRace.UnknownFemaleNpc, - _ => throw new KeyNotFoundException(), - }; - } + SubRace.Midlander => "Midlander", + SubRace.Highlander => "Highlander", + SubRace.Wildwood => "Wildwood", + SubRace.Duskwight => "Duskwright", + SubRace.Plainsfolk => "Plainsfolk", + SubRace.Dunesfolk => "Dunesfolk", + SubRace.SeekerOfTheSun => "Seeker Of The Sun", + SubRace.KeeperOfTheMoon => "Keeper Of The Moon", + SubRace.Seawolf => "Seawolf", + SubRace.Hellsguard => "Hellsguard", + SubRace.Raen => "Raen", + SubRace.Xaela => "Xaela", + SubRace.Helion => "Hellion", + SubRace.Lost => "Lost", + SubRace.Rava => "Rava", + SubRace.Veena => "Veena", + _ => throw new InvalidEnumArgumentException(), + }; + } - public static GenderRace GenderRaceFromByte( byte value ) - { - var gender = ( Gender )( value & 0b111 ); - var race = ( ModelRace )( value >> 3 ); - return CombinedRace( gender, race ); - } + public static bool FitsRace( this SubRace subRace, Race race ) + => subRace.ToRace() == race; - public static GenderRace CombinedRace( Gender gender, ModelRace modelRace ) + public static byte ToByte( this Gender gender, ModelRace modelRace ) + => ( byte )( ( int )gender | ( ( int )modelRace << 3 ) ); + + public static byte ToByte( this ModelRace modelRace, Gender gender ) + => gender.ToByte( modelRace ); + + public static byte ToByte( this GenderRace value ) + { + var (gender, race) = value.Split(); + return gender.ToByte( race ); + } + + public static (Gender, ModelRace) Split( this GenderRace value ) + { + return value switch { - return gender switch + GenderRace.Unknown => ( Gender.Unknown, ModelRace.Unknown ), + GenderRace.MidlanderMale => ( Gender.Male, ModelRace.Midlander ), + GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Midlander ), + GenderRace.MidlanderFemale => ( Gender.Female, ModelRace.Midlander ), + GenderRace.MidlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Midlander ), + GenderRace.HighlanderMale => ( Gender.Male, ModelRace.Highlander ), + GenderRace.HighlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Highlander ), + GenderRace.HighlanderFemale => ( Gender.Female, ModelRace.Highlander ), + GenderRace.HighlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Highlander ), + GenderRace.ElezenMale => ( Gender.Male, ModelRace.Elezen ), + GenderRace.ElezenMaleNpc => ( Gender.MaleNpc, ModelRace.Elezen ), + GenderRace.ElezenFemale => ( Gender.Female, ModelRace.Elezen ), + GenderRace.ElezenFemaleNpc => ( Gender.FemaleNpc, ModelRace.Elezen ), + GenderRace.LalafellMale => ( Gender.Male, ModelRace.Lalafell ), + GenderRace.LalafellMaleNpc => ( Gender.MaleNpc, ModelRace.Lalafell ), + GenderRace.LalafellFemale => ( Gender.Female, ModelRace.Lalafell ), + GenderRace.LalafellFemaleNpc => ( Gender.FemaleNpc, ModelRace.Lalafell ), + GenderRace.MiqoteMale => ( Gender.Male, ModelRace.Miqote ), + GenderRace.MiqoteMaleNpc => ( Gender.MaleNpc, ModelRace.Miqote ), + GenderRace.MiqoteFemale => ( Gender.Female, ModelRace.Miqote ), + GenderRace.MiqoteFemaleNpc => ( Gender.FemaleNpc, ModelRace.Miqote ), + GenderRace.RoegadynMale => ( Gender.Male, ModelRace.Roegadyn ), + GenderRace.RoegadynMaleNpc => ( Gender.MaleNpc, ModelRace.Roegadyn ), + GenderRace.RoegadynFemale => ( Gender.Female, ModelRace.Roegadyn ), + GenderRace.RoegadynFemaleNpc => ( Gender.FemaleNpc, ModelRace.Roegadyn ), + GenderRace.AuRaMale => ( Gender.Male, ModelRace.AuRa ), + GenderRace.AuRaMaleNpc => ( Gender.MaleNpc, ModelRace.AuRa ), + GenderRace.AuRaFemale => ( Gender.Female, ModelRace.AuRa ), + GenderRace.AuRaFemaleNpc => ( Gender.FemaleNpc, ModelRace.AuRa ), + GenderRace.HrothgarMale => ( Gender.Male, ModelRace.Hrothgar ), + GenderRace.HrothgarMaleNpc => ( Gender.MaleNpc, ModelRace.Hrothgar ), + GenderRace.HrothgarFemale => ( Gender.Female, ModelRace.Hrothgar ), + GenderRace.HrothgarFemaleNpc => ( Gender.FemaleNpc, ModelRace.Hrothgar ), + GenderRace.VieraMale => ( Gender.Male, ModelRace.Viera ), + GenderRace.VieraMaleNpc => ( Gender.Male, ModelRace.Viera ), + GenderRace.VieraFemale => ( Gender.Female, ModelRace.Viera ), + GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, ModelRace.Viera ), + GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, ModelRace.Unknown ), + GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, ModelRace.Unknown ), + _ => throw new InvalidEnumArgumentException(), + }; + } + + public static bool IsValid( this GenderRace value ) + => value != GenderRace.Unknown && Enum.IsDefined( typeof( GenderRace ), value ); + + public static string ToRaceCode( this GenderRace value ) + { + return value switch + { + GenderRace.MidlanderMale => "0101", + GenderRace.MidlanderMaleNpc => "0104", + GenderRace.MidlanderFemale => "0201", + GenderRace.MidlanderFemaleNpc => "0204", + GenderRace.HighlanderMale => "0301", + GenderRace.HighlanderMaleNpc => "0304", + GenderRace.HighlanderFemale => "0401", + GenderRace.HighlanderFemaleNpc => "0404", + GenderRace.ElezenMale => "0501", + GenderRace.ElezenMaleNpc => "0504", + GenderRace.ElezenFemale => "0601", + GenderRace.ElezenFemaleNpc => "0604", + GenderRace.MiqoteMale => "0701", + GenderRace.MiqoteMaleNpc => "0704", + GenderRace.MiqoteFemale => "0801", + GenderRace.MiqoteFemaleNpc => "0804", + GenderRace.RoegadynMale => "0901", + GenderRace.RoegadynMaleNpc => "0904", + GenderRace.RoegadynFemale => "1001", + GenderRace.RoegadynFemaleNpc => "1004", + GenderRace.LalafellMale => "1101", + GenderRace.LalafellMaleNpc => "1104", + GenderRace.LalafellFemale => "1201", + GenderRace.LalafellFemaleNpc => "1204", + GenderRace.AuRaMale => "1301", + GenderRace.AuRaMaleNpc => "1304", + GenderRace.AuRaFemale => "1401", + GenderRace.AuRaFemaleNpc => "1404", + GenderRace.HrothgarMale => "1501", + GenderRace.HrothgarMaleNpc => "1504", + GenderRace.HrothgarFemale => "1601", + GenderRace.HrothgarFemaleNpc => "1604", + GenderRace.VieraMale => "1701", + GenderRace.VieraMaleNpc => "1704", + GenderRace.VieraFemale => "1801", + GenderRace.VieraFemaleNpc => "1804", + GenderRace.UnknownMaleNpc => "9104", + GenderRace.UnknownFemaleNpc => "9204", + _ => throw new InvalidEnumArgumentException(), + }; + } +} + +public static partial class Names +{ + public static GenderRace GenderRaceFromCode( string code ) + { + return code switch + { + "0101" => GenderRace.MidlanderMale, + "0104" => GenderRace.MidlanderMaleNpc, + "0201" => GenderRace.MidlanderFemale, + "0204" => GenderRace.MidlanderFemaleNpc, + "0301" => GenderRace.HighlanderMale, + "0304" => GenderRace.HighlanderMaleNpc, + "0401" => GenderRace.HighlanderFemale, + "0404" => GenderRace.HighlanderFemaleNpc, + "0501" => GenderRace.ElezenMale, + "0504" => GenderRace.ElezenMaleNpc, + "0601" => GenderRace.ElezenFemale, + "0604" => GenderRace.ElezenFemaleNpc, + "0701" => GenderRace.MiqoteMale, + "0704" => GenderRace.MiqoteMaleNpc, + "0801" => GenderRace.MiqoteFemale, + "0804" => GenderRace.MiqoteFemaleNpc, + "0901" => GenderRace.RoegadynMale, + "0904" => GenderRace.RoegadynMaleNpc, + "1001" => GenderRace.RoegadynFemale, + "1004" => GenderRace.RoegadynFemaleNpc, + "1101" => GenderRace.LalafellMale, + "1104" => GenderRace.LalafellMaleNpc, + "1201" => GenderRace.LalafellFemale, + "1204" => GenderRace.LalafellFemaleNpc, + "1301" => GenderRace.AuRaMale, + "1304" => GenderRace.AuRaMaleNpc, + "1401" => GenderRace.AuRaFemale, + "1404" => GenderRace.AuRaFemaleNpc, + "1501" => GenderRace.HrothgarMale, + "1504" => GenderRace.HrothgarMaleNpc, + "1601" => GenderRace.HrothgarFemale, + "1604" => GenderRace.HrothgarFemaleNpc, + "1701" => GenderRace.VieraMale, + "1704" => GenderRace.VieraMaleNpc, + "1801" => GenderRace.VieraFemale, + "1804" => GenderRace.VieraFemaleNpc, + "9104" => GenderRace.UnknownMaleNpc, + "9204" => GenderRace.UnknownFemaleNpc, + _ => throw new KeyNotFoundException(), + }; + } + + public static GenderRace GenderRaceFromByte( byte value ) + { + var gender = ( Gender )( value & 0b111 ); + var race = ( ModelRace )( value >> 3 ); + return CombinedRace( gender, race ); + } + + public static GenderRace CombinedRace( Gender gender, ModelRace modelRace ) + { + return gender switch + { + Gender.Male => modelRace switch { - Gender.Male => modelRace switch - { - ModelRace.Midlander => GenderRace.MidlanderMale, - ModelRace.Highlander => GenderRace.HighlanderMale, - ModelRace.Elezen => GenderRace.ElezenMale, - ModelRace.Lalafell => GenderRace.LalafellMale, - ModelRace.Miqote => GenderRace.MiqoteMale, - ModelRace.Roegadyn => GenderRace.RoegadynMale, - ModelRace.AuRa => GenderRace.AuRaMale, - ModelRace.Hrothgar => GenderRace.HrothgarMale, - _ => GenderRace.Unknown, - }, - Gender.MaleNpc => modelRace switch - { - ModelRace.Midlander => GenderRace.MidlanderMaleNpc, - ModelRace.Highlander => GenderRace.HighlanderMaleNpc, - ModelRace.Elezen => GenderRace.ElezenMaleNpc, - ModelRace.Lalafell => GenderRace.LalafellMaleNpc, - ModelRace.Miqote => GenderRace.MiqoteMaleNpc, - ModelRace.Roegadyn => GenderRace.RoegadynMaleNpc, - ModelRace.AuRa => GenderRace.AuRaMaleNpc, - ModelRace.Hrothgar => GenderRace.HrothgarMaleNpc, - _ => GenderRace.Unknown, - }, - Gender.Female => modelRace switch - { - ModelRace.Midlander => GenderRace.MidlanderFemale, - ModelRace.Highlander => GenderRace.HighlanderFemale, - ModelRace.Elezen => GenderRace.ElezenFemale, - ModelRace.Lalafell => GenderRace.LalafellFemale, - ModelRace.Miqote => GenderRace.MiqoteFemale, - ModelRace.Roegadyn => GenderRace.RoegadynFemale, - ModelRace.AuRa => GenderRace.AuRaFemale, - ModelRace.Viera => GenderRace.VieraFemale, - _ => GenderRace.Unknown, - }, - Gender.FemaleNpc => modelRace switch - { - ModelRace.Midlander => GenderRace.MidlanderFemaleNpc, - ModelRace.Highlander => GenderRace.HighlanderFemaleNpc, - ModelRace.Elezen => GenderRace.ElezenFemaleNpc, - ModelRace.Lalafell => GenderRace.LalafellFemaleNpc, - ModelRace.Miqote => GenderRace.MiqoteFemaleNpc, - ModelRace.Roegadyn => GenderRace.RoegadynFemaleNpc, - ModelRace.AuRa => GenderRace.AuRaFemaleNpc, - ModelRace.Viera => GenderRace.VieraFemaleNpc, - _ => GenderRace.Unknown, - }, - _ => GenderRace.Unknown, - }; - } + ModelRace.Midlander => GenderRace.MidlanderMale, + ModelRace.Highlander => GenderRace.HighlanderMale, + ModelRace.Elezen => GenderRace.ElezenMale, + ModelRace.Lalafell => GenderRace.LalafellMale, + ModelRace.Miqote => GenderRace.MiqoteMale, + ModelRace.Roegadyn => GenderRace.RoegadynMale, + ModelRace.AuRa => GenderRace.AuRaMale, + ModelRace.Hrothgar => GenderRace.HrothgarMale, + ModelRace.Viera => GenderRace.VieraMale, + _ => GenderRace.Unknown, + }, + Gender.MaleNpc => modelRace switch + { + ModelRace.Midlander => GenderRace.MidlanderMaleNpc, + ModelRace.Highlander => GenderRace.HighlanderMaleNpc, + ModelRace.Elezen => GenderRace.ElezenMaleNpc, + ModelRace.Lalafell => GenderRace.LalafellMaleNpc, + ModelRace.Miqote => GenderRace.MiqoteMaleNpc, + ModelRace.Roegadyn => GenderRace.RoegadynMaleNpc, + ModelRace.AuRa => GenderRace.AuRaMaleNpc, + ModelRace.Hrothgar => GenderRace.HrothgarMaleNpc, + ModelRace.Viera => GenderRace.VieraMaleNpc, + _ => GenderRace.Unknown, + }, + Gender.Female => modelRace switch + { + ModelRace.Midlander => GenderRace.MidlanderFemale, + ModelRace.Highlander => GenderRace.HighlanderFemale, + ModelRace.Elezen => GenderRace.ElezenFemale, + ModelRace.Lalafell => GenderRace.LalafellFemale, + ModelRace.Miqote => GenderRace.MiqoteFemale, + ModelRace.Roegadyn => GenderRace.RoegadynFemale, + ModelRace.AuRa => GenderRace.AuRaFemale, + ModelRace.Hrothgar => GenderRace.HrothgarFemale, + ModelRace.Viera => GenderRace.VieraFemale, + _ => GenderRace.Unknown, + }, + Gender.FemaleNpc => modelRace switch + { + ModelRace.Midlander => GenderRace.MidlanderFemaleNpc, + ModelRace.Highlander => GenderRace.HighlanderFemaleNpc, + ModelRace.Elezen => GenderRace.ElezenFemaleNpc, + ModelRace.Lalafell => GenderRace.LalafellFemaleNpc, + ModelRace.Miqote => GenderRace.MiqoteFemaleNpc, + ModelRace.Roegadyn => GenderRace.RoegadynFemaleNpc, + ModelRace.AuRa => GenderRace.AuRaFemaleNpc, + ModelRace.Hrothgar => GenderRace.HrothgarFemaleNpc, + ModelRace.Viera => GenderRace.VieraFemaleNpc, + _ => GenderRace.Unknown, + }, + _ => GenderRace.Unknown, + }; } } \ No newline at end of file From 2c22e37666d57453723bedf400f393b687159408 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 10 Dec 2021 15:57:20 +0000 Subject: [PATCH 0034/2451] [CI] Updating repo.json for refs/tags/0.4.6.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 14146e36..05300a9d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.1", - "TestingAssemblyVersion": "0.4.6.1", + "AssemblyVersion": "0.4.6.2", + "TestingAssemblyVersion": "0.4.6.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 12d483e010a19d04c1d68ddb63821bbeaade81c8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 13 Dec 2021 13:15:53 +0100 Subject: [PATCH 0035/2451] Fix collections not correctly building on first launch. --- Penumbra/Mods/CollectionManager.cs | 20 ++++++++++++++++++++ Penumbra/Mods/ModManager.cs | 2 ++ Penumbra/UI/MenuTabs/TabDebug.cs | 17 ++++++++++++----- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 9e617746..f3ce8cd5 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -54,6 +54,24 @@ namespace Penumbra.Mods public bool ResetActiveCollection() => SetActiveCollection( DefaultCollection, string.Empty ); + public void CreateNecessaryCaches() + { + if( !_manager.TempWritable ) + { + PluginLog.Error( "No temporary directory available." ); + return; + } + + if( DefaultCollection.Cache == null ) + DefaultCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst )); + + if( ForcedCollection.Cache == null ) + ForcedCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + + foreach (var (_, collection) in CharacterCollection.Where( kvp => kvp.Value.Cache == null )) + collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + } + public void RecreateCaches() { if( !_manager.TempWritable ) @@ -66,6 +84,8 @@ namespace Penumbra.Mods { collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } + + CreateNecessaryCaches(); } public void RemoveModFromCaches( DirectoryInfo modDir ) diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 846d06fb..03846e9c 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -129,6 +129,8 @@ namespace Penumbra.Mods TempWritable = false; } } + + Collections?.RecreateCaches(); } } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 98a920de..a57697d0 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -145,15 +145,22 @@ namespace Penumbra.UI var manager = Service< ModManager >.Get(); PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); - PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); + PrintValue( " has Cache", (manager.Collections.ActiveCollection.Cache != null).ToString() ); + PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.CurrentCollection.Cache != null ).ToString() ); + PrintValue( "Default Collection", manager.Collections.DefaultCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.DefaultCollection.Cache != null ).ToString() ); + PrintValue( "Forced Collection", manager.Collections.ForcedCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.ForcedCollection.Cache != null ).ToString() ); + PrintValue( "Mod Manager BasePath", manager.BasePath?.Name ?? "NULL" ); + PrintValue( "Mod Manager BasePath-Full", manager.BasePath?.FullName ?? "NULL" ); PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); + PrintValue( "Mod Manager BasePath Exists", manager.BasePath != null ? Directory.Exists( manager.BasePath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Mod Manager Temp Path", manager.TempPath.FullName ); + PrintValue( "Mod Manager Temp Path", manager.TempPath?.FullName ?? "NULL" ); PrintValue( "Mod Manager Temp Path IsRooted", ( !Penumbra.Config.TempDirectory.Any() || Path.IsPathRooted( Penumbra.Config.TempDirectory ) ).ToString() ); - PrintValue( "Mod Manager Temp Path Exists", Directory.Exists( manager.TempPath.FullName ).ToString() ); + PrintValue( "Mod Manager Temp Path Exists", manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); } From ece034cafceec78382c32cb2d021034befc9870f Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 13 Dec 2021 12:20:41 +0000 Subject: [PATCH 0036/2451] [CI] Updating repo.json for refs/tags/0.4.6.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 05300a9d..6176b296 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.2", - "TestingAssemblyVersion": "0.4.6.2", + "AssemblyVersion": "0.4.6.3", + "TestingAssemblyVersion": "0.4.6.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From cd1faf586019b045b66fbf1915a67a0a08db8220 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 13 Dec 2021 17:30:58 +0100 Subject: [PATCH 0037/2451] Fix empty collection building cache after last change. --- Penumbra/Mods/CollectionManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index f3ce8cd5..f9cc99e5 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -62,13 +62,13 @@ namespace Penumbra.Mods return; } - if( DefaultCollection.Cache == null ) + if( DefaultCollection.Name != ModCollection.Empty.Name && DefaultCollection.Cache == null ) DefaultCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst )); - if( ForcedCollection.Cache == null ) + if( ForcedCollection.Name != ModCollection.Empty.Name && ForcedCollection.Cache == null ) ForcedCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - foreach (var (_, collection) in CharacterCollection.Where( kvp => kvp.Value.Cache == null )) + foreach (var (_, collection) in CharacterCollection.Where( kvp => kvp.Value.Name != ModCollection.Empty.Name && kvp.Value.Cache == null )) collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } From 7b5e5f68159c4e322ef7905c00ae5bd34e30b71d Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 13 Dec 2021 16:34:35 +0000 Subject: [PATCH 0038/2451] [CI] Updating repo.json for refs/tags/0.4.6.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6176b296..c0a1fd62 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.3", - "TestingAssemblyVersion": "0.4.6.3", + "AssemblyVersion": "0.4.6.4", + "TestingAssemblyVersion": "0.4.6.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8064376d3d6bfd308dcf1d0dc17f834bad901b38 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 13 Dec 2021 21:26:59 +0100 Subject: [PATCH 0039/2451] Further tries to fix. --- Penumbra/Mods/CollectionManager.cs | 708 ++++++++++++++--------------- Penumbra/Mods/ModManager.cs | 5 +- 2 files changed, 357 insertions(+), 356 deletions(-) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index f9cc99e5..22bd00ce 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -7,414 +7,412 @@ using Penumbra.Interop; using Penumbra.Mod; using Penumbra.Util; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +// Contains all collections and respective functions, as well as the collection settings. +public class CollectionManager { - // Contains all collections and respective functions, as well as the collection settings. - public class CollectionManager + private readonly ModManager _manager; + + public string CollectionChangedTo { get; private set; } = string.Empty; + public Dictionary< string, ModCollection > Collections { get; } = new(); + public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); + + public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; + public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty; + public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; + public ModCollection ActiveCollection { get; private set; } + + public CollectionManager( ModManager manager ) { - private readonly ModManager _manager; + _manager = manager; - public string CollectionChangedTo { get; private set; } = string.Empty; - public Dictionary< string, ModCollection > Collections { get; } = new(); + ReadCollections(); + LoadConfigCollections( Penumbra.Config ); + ActiveCollection = DefaultCollection; + } - public ModCollection CurrentCollection { get; private set; } = null!; - public ModCollection DefaultCollection { get; private set; } = null!; - public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; - public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); - - public ModCollection ActiveCollection { get; private set; } - - public CollectionManager( ModManager manager ) + public bool SetActiveCollection( ModCollection newActive, string name ) + { + CollectionChangedTo = name; + if( newActive == ActiveCollection ) { - _manager = manager; - - ReadCollections(); - LoadConfigCollections( Penumbra.Config ); - ActiveCollection = DefaultCollection; + return false; } - public bool SetActiveCollection( ModCollection newActive, string name ) + if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) { - CollectionChangedTo = name; - if( newActive == ActiveCollection ) + var resourceManager = Service< ResidentResources >.Get(); + resourceManager.ReloadPlayerResources(); + } + + ActiveCollection = newActive; + return true; + } + + public bool ResetActiveCollection() + => SetActiveCollection( DefaultCollection, string.Empty ); + + public void CreateNecessaryCaches() + { + AddCache( DefaultCollection ); + AddCache( ForcedCollection ); + foreach( var (_, collection) in CharacterCollection ) + { + AddCache( collection ); + } + } + + public void RecreateCaches() + { + if( !_manager.TempWritable ) + { + PluginLog.Error( "No temporary directory available." ); + return; + } + + foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) + { + collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + } + + CreateNecessaryCaches(); + } + + public void RemoveModFromCaches( DirectoryInfo modDir ) + { + foreach( var collection in Collections.Values ) + { + collection.Cache?.RemoveMod( modDir ); + } + } + + internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta ) + { + foreach( var collection in Collections.Values ) + { + if( metaChanges ) { - return false; + collection.UpdateSetting( mod ); } - if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) + if( fileChanges.HasFlag( ResourceChange.Files ) + && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) + && settings.Enabled ) { + collection.Cache?.CalculateEffectiveFileList(); + } + + if( reloadMeta ) + { + collection.Cache?.UpdateMetaManipulations(); + } + } + + if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) + { + Service< ResidentResources >.Get().ReloadPlayerResources(); + } + } + + public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed == string.Empty || Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = new ModCollection( name, settings ); + Collections.Add( name, newCollection ); + newCollection.Save(); + SetCurrentCollection( newCollection ); + return true; + } + + public bool RemoveCollection( string name ) + { + if( name == ModCollection.DefaultCollection ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( !Collections.TryGetValue( name, out var collection ) ) + { + return false; + } + + if( CurrentCollection == collection ) + { + SetCurrentCollection( Collections[ ModCollection.DefaultCollection ] ); + } + + if( ForcedCollection == collection ) + { + SetForcedCollection( ModCollection.Empty ); + } + + if( DefaultCollection == collection ) + { + SetDefaultCollection( ModCollection.Empty ); + } + + foreach( var (characterName, characterCollection) in CharacterCollection.ToArray() ) + { + if( characterCollection == collection ) + { + SetCharacterCollection( characterName, ModCollection.Empty ); + } + } + + collection.Delete(); + Collections.Remove( name ); + return true; + + } + + private void AddCache( ModCollection collection ) + { + if( !_manager.TempWritable ) + { + PluginLog.Error( "No tmp directory available." ); + return; + } + + if( collection.Cache == null && collection.Name != string.Empty ) + { + collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + } + } + + private void RemoveCache( ModCollection collection ) + { + if( collection.Name != ForcedCollection.Name + && collection.Name != CurrentCollection.Name + && collection.Name != DefaultCollection.Name + && CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) ) + { + collection.ClearCache(); + } + } + + private void SetCollection( ModCollection newCollection, ModCollection oldCollection, Action< ModCollection > setter, + Action< string > configSetter ) + { + if( newCollection.Name == oldCollection.Name ) + { + return; + } + + AddCache( newCollection ); + + setter( newCollection ); + RemoveCache( oldCollection ); + configSetter( newCollection.Name ); + Penumbra.Config.Save(); + } + + public void SetDefaultCollection( ModCollection newCollection ) + => SetCollection( newCollection, DefaultCollection, c => + { + if( !CollectionChangedTo.Any() ) + { + ActiveCollection = c; var resourceManager = Service< ResidentResources >.Get(); resourceManager.ReloadPlayerResources(); } - ActiveCollection = newActive; - return true; - } + DefaultCollection = c; + }, s => Penumbra.Config.DefaultCollection = s ); - public bool ResetActiveCollection() - => SetActiveCollection( DefaultCollection, string.Empty ); + public void SetForcedCollection( ModCollection newCollection ) + => SetCollection( newCollection, ForcedCollection, c => ForcedCollection = c, s => Penumbra.Config.ForcedCollection = s ); - public void CreateNecessaryCaches() - { - if( !_manager.TempWritable ) + public void SetCurrentCollection( ModCollection newCollection ) + => SetCollection( newCollection, CurrentCollection, c => CurrentCollection = c, s => Penumbra.Config.CurrentCollection = s ); + + public void SetCharacterCollection( string characterName, ModCollection newCollection ) + => SetCollection( newCollection, + CharacterCollection.TryGetValue( characterName, out var oldCollection ) ? oldCollection : ModCollection.Empty, + c => { - PluginLog.Error( "No temporary directory available." ); - return; - } - - if( DefaultCollection.Name != ModCollection.Empty.Name && DefaultCollection.Cache == null ) - DefaultCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst )); - - if( ForcedCollection.Name != ModCollection.Empty.Name && ForcedCollection.Cache == null ) - ForcedCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - - foreach (var (_, collection) in CharacterCollection.Where( kvp => kvp.Value.Name != ModCollection.Empty.Name && kvp.Value.Cache == null )) - collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - public void RecreateCaches() - { - if( !_manager.TempWritable ) - { - PluginLog.Error( "No temporary directory available." ); - return; - } - - foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) - { - collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - CreateNecessaryCaches(); - } - - public void RemoveModFromCaches( DirectoryInfo modDir ) - { - foreach( var collection in Collections.Values ) - { - collection.Cache?.RemoveMod( modDir ); - } - } - - internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta ) - { - foreach( var collection in Collections.Values ) - { - if( metaChanges ) - { - collection.UpdateSetting( mod ); - } - - if( fileChanges.HasFlag( ResourceChange.Files ) - && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) - && settings.Enabled ) - { - collection.Cache?.CalculateEffectiveFileList(); - } - - if( reloadMeta ) - { - collection.Cache?.UpdateMetaManipulations(); - } - } - - if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) - { - Service< ResidentResources >.Get().ReloadPlayerResources(); - } - } - - public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed == string.Empty || Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) - { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = new ModCollection( name, settings ); - Collections.Add( name, newCollection ); - newCollection.Save(); - SetCurrentCollection( newCollection ); - return true; - } - - public bool RemoveCollection( string name ) - { - if( name == ModCollection.DefaultCollection ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - if( Collections.TryGetValue( name, out var collection ) ) - { - if( CurrentCollection == collection ) - { - SetCurrentCollection( Collections[ ModCollection.DefaultCollection ] ); - } - - if( ForcedCollection == collection ) - { - SetForcedCollection( ModCollection.Empty ); - } - - if( DefaultCollection == collection ) - { - SetDefaultCollection( ModCollection.Empty ); - } - - foreach( var kvp in CharacterCollection.ToArray() ) - { - if( kvp.Value == collection ) - { - SetCharacterCollection( kvp.Key, ModCollection.Empty ); - } - } - - collection.Delete(); - Collections.Remove( name ); - return true; - } - - return false; - } - - private void AddCache( ModCollection collection ) - { - if( !_manager.TempWritable ) - { - PluginLog.Error( "No tmp directory available." ); - return; - } - - if( collection.Cache == null && collection.Name != string.Empty ) - { - collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - } - - private void RemoveCache( ModCollection collection ) - { - if( collection.Name != ForcedCollection.Name - && collection.Name != CurrentCollection.Name - && collection.Name != DefaultCollection.Name - && CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) ) - { - collection.ClearCache(); - } - } - - private void SetCollection( ModCollection newCollection, ModCollection oldCollection, Action< ModCollection > setter, - Action< string > configSetter ) - { - if( newCollection.Name == oldCollection.Name ) - { - return; - } - - AddCache( newCollection ); - - setter( newCollection ); - RemoveCache( oldCollection ); - configSetter( newCollection.Name ); - Penumbra.Config.Save(); - } - - public void SetDefaultCollection( ModCollection newCollection ) - => SetCollection( newCollection, DefaultCollection, c => - { - if( !CollectionChangedTo.Any() ) + if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) { ActiveCollection = c; var resourceManager = Service< ResidentResources >.Get(); resourceManager.ReloadPlayerResources(); } - DefaultCollection = c; - }, s => Penumbra.Config.DefaultCollection = s ); + CharacterCollection[ characterName ] = c; + }, s => Penumbra.Config.CharacterCollections[ characterName ] = s ); - public void SetForcedCollection( ModCollection newCollection ) - => SetCollection( newCollection, ForcedCollection, c => ForcedCollection = c, s => Penumbra.Config.ForcedCollection = s ); - - public void SetCurrentCollection( ModCollection newCollection ) - => SetCollection( newCollection, CurrentCollection, c => CurrentCollection = c, s => Penumbra.Config.CurrentCollection = s ); - - public void SetCharacterCollection( string characterName, ModCollection newCollection ) - => SetCollection( newCollection, - CharacterCollection.TryGetValue( characterName, out var oldCollection ) ? oldCollection : ModCollection.Empty, - c => - { - if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) - { - ActiveCollection = c; - var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadPlayerResources(); - } - - CharacterCollection[ characterName ] = c; - }, s => Penumbra.Config.CharacterCollections[ characterName ] = s ); - - public bool CreateCharacterCollection( string characterName ) + public bool CreateCharacterCollection( string characterName ) + { + if( !CharacterCollection.ContainsKey( characterName ) ) { - if( !CharacterCollection.ContainsKey( characterName ) ) - { - CharacterCollection[ characterName ] = ModCollection.Empty; - Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; - Penumbra.Config.Save(); - Penumbra.PlayerWatcher.AddPlayerToWatch( characterName ); - return true; - } + CharacterCollection[ characterName ] = ModCollection.Empty; + Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; + Penumbra.Config.Save(); + Penumbra.PlayerWatcher.AddPlayerToWatch( characterName ); + return true; + } + return false; + } + + public void RemoveCharacterCollection( string characterName ) + { + if( CharacterCollection.TryGetValue( characterName, out var collection ) ) + { + RemoveCache( collection ); + CharacterCollection.Remove( characterName ); + Penumbra.PlayerWatcher.RemovePlayerFromWatch( characterName ); + } + + if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) + { + Penumbra.Config.Save(); + } + } + + private bool LoadCurrentCollection( Configuration config ) + { + if( Collections.TryGetValue( config.CurrentCollection, out var currentCollection ) ) + { + CurrentCollection = currentCollection; + AddCache( CurrentCollection ); return false; } - public void RemoveCharacterCollection( string characterName ) + PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); + CurrentCollection = Collections[ ModCollection.DefaultCollection ]; + if( CurrentCollection.Cache == null ) { - if( CharacterCollection.TryGetValue( characterName, out var collection ) ) - { - RemoveCache( collection ); - CharacterCollection.Remove( characterName ); - Penumbra.PlayerWatcher.RemovePlayerFromWatch( characterName ); - } + CurrentCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + } - if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) + config.CurrentCollection = ModCollection.DefaultCollection; + return true; + } + + private bool LoadForcedCollection( Configuration config ) + { + if( config.ForcedCollection == string.Empty ) + { + ForcedCollection = ModCollection.Empty; + return false; + } + + if( Collections.TryGetValue( config.ForcedCollection, out var forcedCollection ) ) + { + ForcedCollection = forcedCollection; + AddCache( ForcedCollection ); + return false; + } + + PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." ); + ForcedCollection = ModCollection.Empty; + config.ForcedCollection = string.Empty; + return true; + } + + private bool LoadDefaultCollection( Configuration config ) + { + if( config.DefaultCollection == string.Empty ) + { + DefaultCollection = ModCollection.Empty; + return false; + } + + if( Collections.TryGetValue( config.DefaultCollection, out var defaultCollection ) ) + { + DefaultCollection = defaultCollection; + AddCache( DefaultCollection ); + return false; + } + + PluginLog.Error( $"Last choice of DefaultCollection {config.DefaultCollection} is not available, reset to None." ); + DefaultCollection = ModCollection.Empty; + config.DefaultCollection = string.Empty; + return true; + } + + private bool LoadCharacterCollections( Configuration config ) + { + var configChanged = false; + foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) + { + Penumbra.PlayerWatcher.AddPlayerToWatch( player ); + if( collectionName == string.Empty ) { - Penumbra.Config.Save(); + CharacterCollection.Add( player, ModCollection.Empty ); + } + else if( Collections.TryGetValue( collectionName, out var charCollection ) ) + { + AddCache( charCollection ); + CharacterCollection.Add( player, charCollection ); + } + else + { + PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); + CharacterCollection.Add( player, ModCollection.Empty ); + config.CharacterCollections[ player ] = string.Empty; + configChanged = true; } } - private bool LoadCurrentCollection( Configuration config ) - { - if( Collections.TryGetValue( config.CurrentCollection, out var currentCollection ) ) - { - CurrentCollection = currentCollection; - AddCache( CurrentCollection ); - return false; - } + return configChanged; + } - PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); - CurrentCollection = Collections[ ModCollection.DefaultCollection ]; - config.CurrentCollection = ModCollection.DefaultCollection; - return true; + private void LoadConfigCollections( Configuration config ) + { + var configChanged = LoadCurrentCollection( config ); + configChanged |= LoadDefaultCollection( config ); + configChanged |= LoadForcedCollection( config ); + configChanged |= LoadCharacterCollections( config ); + + if( configChanged ) + { + config.Save(); } + } - private bool LoadForcedCollection( Configuration config ) + private void ReadCollections() + { + var collectionDir = ModCollection.CollectionDir(); + if( collectionDir.Exists ) { - if( config.ForcedCollection == string.Empty ) + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) { - ForcedCollection = ModCollection.Empty; - return false; - } - - if( Collections.TryGetValue( config.ForcedCollection, out var forcedCollection ) ) - { - ForcedCollection = forcedCollection; - AddCache( ForcedCollection ); - return false; - } - - PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." ); - ForcedCollection = ModCollection.Empty; - config.ForcedCollection = string.Empty; - return true; - } - - private bool LoadDefaultCollection( Configuration config ) - { - if( config.DefaultCollection == string.Empty ) - { - DefaultCollection = ModCollection.Empty; - return false; - } - - if( Collections.TryGetValue( config.DefaultCollection, out var defaultCollection ) ) - { - DefaultCollection = defaultCollection; - AddCache( DefaultCollection ); - return false; - } - - PluginLog.Error( $"Last choice of DefaultCollection {config.DefaultCollection} is not available, reset to None." ); - DefaultCollection = ModCollection.Empty; - config.DefaultCollection = string.Empty; - return true; - } - - private bool LoadCharacterCollections( Configuration config ) - { - var configChanged = false; - foreach( var kvp in config.CharacterCollections.ToArray() ) - { - Penumbra.PlayerWatcher.AddPlayerToWatch( kvp.Key ); - if( kvp.Value == string.Empty ) + var collection = ModCollection.LoadFromFile( file ); + if( collection == null || collection.Name == string.Empty ) { - CharacterCollection.Add( kvp.Key, ModCollection.Empty ); + continue; } - else if( Collections.TryGetValue( kvp.Value, out var charCollection ) ) + + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) { - AddCache( charCollection ); - CharacterCollection.Add( kvp.Key, charCollection ); + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( Collections.ContainsKey( collection.Name ) ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); } else { - PluginLog.Error( $"Last choice of <{kvp.Key}>'s Collection {kvp.Value} is not available, reset to None." ); - CharacterCollection.Add( kvp.Key, ModCollection.Empty ); - config.CharacterCollections[ kvp.Key ] = string.Empty; - configChanged = true; + Collections.Add( collection.Name, collection ); } } - - return configChanged; - } - - private void LoadConfigCollections( Configuration config ) - { - var configChanged = LoadCurrentCollection( config ); - configChanged |= LoadDefaultCollection( config ); - configChanged |= LoadForcedCollection( config ); - configChanged |= LoadCharacterCollections( config ); - - if( configChanged ) - { - config.Save(); - } } - private void ReadCollections() + if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) { - var collectionDir = ModCollection.CollectionDir(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection.LoadFromFile( file ); - if( collection != null ) - { - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( Collections.ContainsKey( collection.Name ) ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - Collections.Add( collection.Name, collection ); - } - } - } - } - - if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) - { - var defaultCollection = new ModCollection(); - defaultCollection.Save(); - Collections.Add( defaultCollection.Name, defaultCollection ); - } + var defaultCollection = new ModCollection(); + defaultCollection.Save(); + Collections.Add( defaultCollection.Name, defaultCollection ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 03846e9c..87b78d4b 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -130,7 +130,10 @@ namespace Penumbra.Mods } } - Collections?.RecreateCaches(); + if( !firstTime ) + { + Collections.RecreateCaches(); + } } } From 0823423eda53c3e543c191e3f4ee900fb63be2e3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 13 Dec 2021 22:46:42 +0000 Subject: [PATCH 0040/2451] [CI] Updating repo.json for refs/tags/0.4.6.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c0a1fd62..5ad53c7e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.4", - "TestingAssemblyVersion": "0.4.6.4", + "AssemblyVersion": "0.4.6.5", + "TestingAssemblyVersion": "0.4.6.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 59fa4c4fe4179ca593021f549ceb223d0bb935c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jan 2022 11:14:41 +0100 Subject: [PATCH 0041/2451] Temporary fix for E4S crashes. --- Penumbra/Interop/ResourceLoader.cs | 48 ++- Penumbra/UI/MenuTabs/TabDebug.cs | 593 +++++++++++++++-------------- 2 files changed, 343 insertions(+), 298 deletions(-) diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 0ce2be54..5cfc23f3 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Hooking; using Dalamud.Logging; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Structs; @@ -18,6 +19,7 @@ public class ResourceLoader : IDisposable public Penumbra Penumbra { get; set; } public bool IsEnabled { get; set; } + public bool HacksEnabled { get; set; } public Crc32 Crc32 { get; } @@ -126,6 +128,29 @@ public class ResourceLoader : IDisposable LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); } + private bool CheckForTerritory() + { + var territory = Dalamud.GameData.GetExcelSheet< TerritoryType >()?.GetRow( Dalamud.ClientState.TerritoryType ); + var bad = territory?.Unknown40 ?? false; + switch( bad ) + { + case true when HacksEnabled: + CheckFileStateHook?.Disable(); + LoadTexFileExternHook?.Disable(); + LoadMdlFileExternHook?.Disable(); + HacksEnabled = false; + return bad; + case false when Penumbra.Config.IsEnabled && !HacksEnabled: + CheckFileStateHook?.Enable(); + LoadTexFileExternHook?.Enable(); + LoadMdlFileExternHook?.Enable(); + HacksEnabled = true; + break; + } + + return bad; + } + private static bool CheckFileStateDetour( IntPtr _, ulong _2 ) => true; @@ -198,6 +223,11 @@ public class ResourceLoader : IDisposable bool isUnknown ) { + if( CheckForTerritory() ) + { + return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + } + string file; var modManager = Service< ModManager >.Get(); @@ -247,6 +277,11 @@ public class ResourceLoader : IDisposable private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) { + if( CheckForTerritory() ) + { + return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + } + if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) { PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); @@ -286,7 +321,12 @@ public class ResourceLoader : IDisposable return; } - if( ReadSqpackHook == null || GetResourceSyncHook == null || GetResourceAsyncHook == null || CheckFileStateHook == null || LoadTexFileExternHook == null || LoadMdlFileExternHook == null) + if( ReadSqpackHook == null + || GetResourceSyncHook == null + || GetResourceAsyncHook == null + || CheckFileStateHook == null + || LoadTexFileExternHook == null + || LoadMdlFileExternHook == null ) { PluginLog.Error( "[GetResourceHandler] Could not activate hooks because at least one was not set." ); return; @@ -299,7 +339,8 @@ public class ResourceLoader : IDisposable LoadTexFileExternHook.Enable(); LoadMdlFileExternHook.Enable(); - IsEnabled = true; + IsEnabled = true; + HacksEnabled = true; } public void Disable() @@ -315,7 +356,8 @@ public class ResourceLoader : IDisposable CheckFileStateHook?.Disable(); LoadTexFileExternHook?.Disable(); LoadMdlFileExternHook?.Disable(); - IsEnabled = false; + IsEnabled = false; + HacksEnabled = false; } public void Dispose() diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index a57697d0..ea46be36 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -17,39 +17,39 @@ using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private static void DrawDebugTabPlayers() { - private static void DrawDebugTabPlayers() + if( !ImGui.CollapsingHeader( "Players##Debug" ) ) { - if( !ImGui.CollapsingHeader( "Players##Debug" ) ) - { - return; - } + return; + } - var players = Penumbra.PlayerWatcher.WatchedPlayers().ToArray(); - var count = players.Sum( s => Math.Max(1, s.Item2.Length) ); - if( count == 0 ) - { - return; - } + var players = Penumbra.PlayerWatcher.WatchedPlayers().ToArray(); + var count = players.Sum( s => Math.Max( 1, s.Item2.Length ) ); + if( count == 0 ) + { + return; + } - if( !ImGui.BeginTable( "##ObjectTable", 13, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 4 * count ) ) ) - { - return; - } + if( !ImGui.BeginTable( "##ObjectTable", 13, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 4 * count ) ) ) + { + return; + } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - var identifier = GameData.GameData.GetIdentifier(); + var identifier = GameData.GameData.GetIdentifier(); - foreach( var (actor, equip) in players.SelectMany( kvp => kvp.Item2.Any() - ? kvp.Item2 - .Select( x => ( $"{kvp.Item1} ({x.Item1})", x.Item2 ) ) - : new[] { ( kvp.Item1, new CharacterEquipment() ) } ) ) - { + foreach( var (actor, equip) in players.SelectMany( kvp => kvp.Item2.Any() + ? kvp.Item2 + .Select( x => ( $"{kvp.Item1} ({x.Item1})", x.Item2 ) ) + : new[] { ( kvp.Item1, new CharacterEquipment() ) } ) ) + { // @formatter:off ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -115,295 +115,298 @@ namespace Penumbra.UI ImGui.Text( identifier.Identify( equip.LFinger.Set, 0, equip.LFinger.Variant, EquipSlot.LFinger )?.Name.ToString() ?? "Unknown" ); ImGui.TableNextColumn(); ImGui.Text( identifier.Identify( equip.RFinger.Set, 0, equip.RFinger.Variant, EquipSlot.LFinger )?.Name.ToString() ?? "Unknown" ); - // @formatter:on - } + // @formatter:on + } + } + + private static void PrintValue( string name, string value ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( name ); + ImGui.TableNextColumn(); + ImGui.Text( value ); + } + + private void DrawDebugTabGeneral() + { + if( !ImGui.CollapsingHeader( "General##Debug" ) ) + { + return; } - private static void PrintValue( string name, string value ) + if( !ImGui.BeginTable( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ) ) { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( name ); - ImGui.TableNextColumn(); - ImGui.Text( value ); + return; } - private static void DrawDebugTabGeneral() + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + var manager = Service< ModManager >.Get(); + PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.ActiveCollection.Cache != null ).ToString() ); + PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.CurrentCollection.Cache != null ).ToString() ); + PrintValue( "Default Collection", manager.Collections.DefaultCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.DefaultCollection.Cache != null ).ToString() ); + PrintValue( "Forced Collection", manager.Collections.ForcedCollection.Name ); + PrintValue( " has Cache", ( manager.Collections.ForcedCollection.Cache != null ).ToString() ); + PrintValue( "Mod Manager BasePath", manager.BasePath?.Name ?? "NULL" ); + PrintValue( "Mod Manager BasePath-Full", manager.BasePath?.FullName ?? "NULL" ); + PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); + PrintValue( "Mod Manager BasePath Exists", + manager.BasePath != null ? Directory.Exists( manager.BasePath.FullName ).ToString() : false.ToString() ); + PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); + PrintValue( "Mod Manager Temp Path", manager.TempPath?.FullName ?? "NULL" ); + PrintValue( "Mod Manager Temp Path IsRooted", + ( !Penumbra.Config.TempDirectory.Any() || Path.IsPathRooted( Penumbra.Config.TempDirectory ) ).ToString() ); + PrintValue( "Mod Manager Temp Path Exists", + manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); + PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); + PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); + PrintValue( "Resource Loader Hacks Enabled", _penumbra.ResourceLoader.HacksEnabled.ToString() ); + } + + private void DrawDebugTabRedraw() + { + if( !ImGui.CollapsingHeader( "Redrawing##Debug" ) ) { - if( !ImGui.CollapsingHeader( "General##Debug" ) ) - { - return; - } - - if( !ImGui.BeginTable( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - var manager = Service< ModManager >.Get(); - PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); - PrintValue( " has Cache", (manager.Collections.ActiveCollection.Cache != null).ToString() ); - PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.CurrentCollection.Cache != null ).ToString() ); - PrintValue( "Default Collection", manager.Collections.DefaultCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.DefaultCollection.Cache != null ).ToString() ); - PrintValue( "Forced Collection", manager.Collections.ForcedCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.ForcedCollection.Cache != null ).ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath?.Name ?? "NULL" ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath?.FullName ?? "NULL" ); - PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", manager.BasePath != null ? Directory.Exists( manager.BasePath.FullName ).ToString() : false.ToString() ); - PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Mod Manager Temp Path", manager.TempPath?.FullName ?? "NULL" ); - PrintValue( "Mod Manager Temp Path IsRooted", - ( !Penumbra.Config.TempDirectory.Any() || Path.IsPathRooted( Penumbra.Config.TempDirectory ) ).ToString() ); - PrintValue( "Mod Manager Temp Path Exists", manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); - PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); + return; } - private void DrawDebugTabRedraw() + var queue = ( Queue< (int, string, RedrawType) >? )_penumbra.ObjectReloader.GetType() + .GetField( "_objectIds", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ) + ?? new Queue< (int, string, RedrawType) >(); + + var currentFrame = ( int? )_penumbra.ObjectReloader.GetType() + .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var changedSettings = ( bool? )_penumbra.ObjectReloader.GetType() + .GetField( "_changedSettings", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var currentObjectId = ( uint? )_penumbra.ObjectReloader.GetType() + .GetField( "_currentObjectId", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var currentObjectName = ( string? )_penumbra.ObjectReloader.GetType() + .GetField( "_currentObjectName", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var currentObjectStartState = ( ObjectReloader.LoadingFlags? )_penumbra.ObjectReloader.GetType() + .GetField( "_currentObjectStartState", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var currentRedrawType = ( RedrawType? )_penumbra.ObjectReloader.GetType() + .GetField( "_currentRedrawType", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var (currentObject, currentObjectIdx) = ( (GameObject?, int) )_penumbra.ObjectReloader.GetType() + .GetMethod( "FindCurrentObject", BindingFlags.NonPublic | BindingFlags.Instance )? + .Invoke( _penumbra.ObjectReloader, Array.Empty< object >() )!; + + var currentRender = currentObject != null + ? ( ObjectReloader.LoadingFlags? )Marshal.ReadInt32( ObjectReloader.RenderPtr( currentObject ) ) + : null; + + var waitFrames = ( int? )_penumbra.ObjectReloader.GetType() + .GetField( "_waitFrames", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var wasTarget = ( bool? )_penumbra.ObjectReloader.GetType() + .GetField( "_wasTarget", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + var gPose = ( bool? )_penumbra.ObjectReloader.GetType() + .GetField( "_inGPose", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( _penumbra.ObjectReloader ); + + using var raii = new ImGuiRaii.EndStack(); + if( ImGui.BeginTable( "##RedrawData", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 7 ) ) ) { - if( !ImGui.CollapsingHeader( "Redrawing##Debug" ) ) - { - return; - } - - var queue = ( Queue< (int, string, RedrawType) >? )_penumbra.ObjectReloader.GetType() - .GetField( "_objectIds", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ) - ?? new Queue< (int, string, RedrawType) >(); - - var currentFrame = ( int? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var changedSettings = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_changedSettings", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectId = ( uint? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectId", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectName = ( string? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectName", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectStartState = ( ObjectReloader.LoadingFlags? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectStartState", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentRedrawType = ( RedrawType? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentRedrawType", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var (currentObject, currentObjectIdx) = ( (GameObject?, int) )_penumbra.ObjectReloader.GetType() - .GetMethod( "FindCurrentObject", BindingFlags.NonPublic | BindingFlags.Instance )? - .Invoke( _penumbra.ObjectReloader, Array.Empty< object >() )!; - - var currentRender = currentObject != null - ? ( ObjectReloader.LoadingFlags? )Marshal.ReadInt32( ObjectReloader.RenderPtr( currentObject ) ) - : null; - - var waitFrames = ( int? )_penumbra.ObjectReloader.GetType() - .GetField( "_waitFrames", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var wasTarget = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_wasTarget", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var gPose = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_inGPose", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - using var raii = new ImGuiRaii.EndStack(); - if( ImGui.BeginTable( "##RedrawData", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 7 ) ) ) - { - raii.Push( ImGui.EndTable ); - PrintValue( "Current Wait Frame", waitFrames?.ToString() ?? "null" ); - PrintValue( "Current Frame", currentFrame?.ToString() ?? "null" ); - PrintValue( "Currently in GPose", gPose?.ToString() ?? "null" ); - PrintValue( "Current Changed Settings", changedSettings?.ToString() ?? "null" ); - PrintValue( "Current Object Id", currentObjectId?.ToString( "X8" ) ?? "null" ); - PrintValue( "Current Object Name", currentObjectName ?? "null" ); - PrintValue( "Current Object Start State", ( ( int? )currentObjectStartState )?.ToString( "X8" ) ?? "null" ); - PrintValue( "Current Object Was Target", wasTarget?.ToString() ?? "null" ); - PrintValue( "Current Object Redraw", currentRedrawType?.ToString() ?? "null" ); - PrintValue( "Current Object Address", currentObject?.Address.ToString( "X16" ) ?? "null" ); - PrintValue( "Current Object Index", currentObjectIdx >= 0 ? currentObjectIdx.ToString() : "null" ); - PrintValue( "Current Object Render Flags", ( ( int? )currentRender )?.ToString( "X8" ) ?? "null" ); - } - - if( queue.Any() - && ImGui.BeginTable( "##RedrawTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * queue.Count ) ) ) - { - raii.Push( ImGui.EndTable ); - foreach( var (objectId, objectName, redraw) in queue ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( objectName ); - ImGui.TableNextColumn(); - ImGui.Text( $"0x{objectId:X8}" ); - ImGui.TableNextColumn(); - ImGui.Text( redraw.ToString() ); - } - } - - if( queue.Any() && ImGui.Button( "Clear" ) ) - { - queue.Clear(); - _penumbra.ObjectReloader.GetType() - .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic )?.SetValue( _penumbra.ObjectReloader, 0 ); - } + raii.Push( ImGui.EndTable ); + PrintValue( "Current Wait Frame", waitFrames?.ToString() ?? "null" ); + PrintValue( "Current Frame", currentFrame?.ToString() ?? "null" ); + PrintValue( "Currently in GPose", gPose?.ToString() ?? "null" ); + PrintValue( "Current Changed Settings", changedSettings?.ToString() ?? "null" ); + PrintValue( "Current Object Id", currentObjectId?.ToString( "X8" ) ?? "null" ); + PrintValue( "Current Object Name", currentObjectName ?? "null" ); + PrintValue( "Current Object Start State", ( ( int? )currentObjectStartState )?.ToString( "X8" ) ?? "null" ); + PrintValue( "Current Object Was Target", wasTarget?.ToString() ?? "null" ); + PrintValue( "Current Object Redraw", currentRedrawType?.ToString() ?? "null" ); + PrintValue( "Current Object Address", currentObject?.Address.ToString( "X16" ) ?? "null" ); + PrintValue( "Current Object Index", currentObjectIdx >= 0 ? currentObjectIdx.ToString() : "null" ); + PrintValue( "Current Object Render Flags", ( ( int? )currentRender )?.ToString( "X8" ) ?? "null" ); } - private static void DrawDebugTabTempFiles() + if( queue.Any() + && ImGui.BeginTable( "##RedrawTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * queue.Count ) ) ) { - if( !ImGui.CollapsingHeader( "Temporary Files##Debug" ) ) - { - return; - } - - if( !ImGui.BeginTable( "##tempFileTable", 4, ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - foreach( var collection in Service< ModManager >.Get().Collections.Collections.Values.Where( c => c.Cache != null ) ) - { - var manip = collection.Cache!.MetaManipulations; - var files = ( Dictionary< GamePath, MetaManager.FileInformation >? )manip.GetType() - .GetField( "_currentFiles", BindingFlags.NonPublic | BindingFlags.Instance )?.GetValue( manip ) - ?? new Dictionary< GamePath, MetaManager.FileInformation >(); - - - foreach( var (file, info) in files ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( info.CurrentFile?.FullName ?? "None" ); - ImGui.TableNextColumn(); - ImGui.Text( file ); - ImGui.TableNextColumn(); - info.CurrentFile?.Refresh(); - ImGui.Text( info.CurrentFile?.Exists ?? false ? "Exists" : "Missing" ); - ImGui.TableNextColumn(); - ImGui.Text( info.Changed ? "Data Changed" : "Unchanged" ); - } - } - } - - private void DrawDebugTabIpc() - { - if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) - { - return; - } - - var ipc = _penumbra.Ipc; - ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); - ImGui.Text( "Available subscriptions:" ); - using var indent = ImGuiRaii.PushIndent(); - if( ipc.ProviderApiVersion != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); - } - - if( ipc.ProviderRedrawName != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); - } - - if( ipc.ProviderRedrawObject != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); - } - - if( ipc.ProviderRedrawAll != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); - } - - if( ipc.ProviderResolveDefault != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); - } - - if( ipc.ProviderResolveCharacter != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); - } - - if( ipc.ProviderChangedItemTooltip != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); - } - - if( ipc.ProviderChangedItemClick != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); - } - } - - private void DrawDebugTabMissingFiles() - { - if( !ImGui.CollapsingHeader( "Missing Files##Debug" ) ) - { - return; - } - - var manager = Service< ModManager >.Get(); - var cache = manager.Collections.CurrentCollection.Cache; - if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - foreach( var file in cache.MissingFiles ) + raii.Push( ImGui.EndTable ); + foreach( var (objectId, objectName, redraw) in queue ) { ImGui.TableNextRow(); ImGui.TableNextColumn(); - if( ImGui.Selectable( file.FullName ) ) - { - ImGui.SetClipboardText( file.FullName ); - } - - ImGuiCustom.HoverTooltip( "Click to copy to clipboard." ); + ImGui.Text( objectName ); + ImGui.TableNextColumn(); + ImGui.Text( $"0x{objectId:X8}" ); + ImGui.TableNextColumn(); + ImGui.Text( redraw.ToString() ); } } - private void DrawDebugTab() + if( queue.Any() && ImGui.Button( "Clear" ) ) { - if( !ImGui.BeginTabItem( "Debug Tab" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - DrawDebugTabGeneral(); - ImGui.NewLine(); - DrawDebugTabMissingFiles(); - ImGui.NewLine(); - DrawDebugTabRedraw(); - ImGui.NewLine(); - DrawDebugTabPlayers(); - ImGui.NewLine(); - DrawDebugTabTempFiles(); - ImGui.NewLine(); - DrawDebugTabIpc(); - ImGui.NewLine(); + queue.Clear(); + _penumbra.ObjectReloader.GetType() + .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic )?.SetValue( _penumbra.ObjectReloader, 0 ); } } + + private static void DrawDebugTabTempFiles() + { + if( !ImGui.CollapsingHeader( "Temporary Files##Debug" ) ) + { + return; + } + + if( !ImGui.BeginTable( "##tempFileTable", 4, ImGuiTableFlags.SizingFixedFit ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + foreach( var collection in Service< ModManager >.Get().Collections.Collections.Values.Where( c => c.Cache != null ) ) + { + var manip = collection.Cache!.MetaManipulations; + var files = ( Dictionary< GamePath, MetaManager.FileInformation >? )manip.GetType() + .GetField( "_currentFiles", BindingFlags.NonPublic | BindingFlags.Instance )?.GetValue( manip ) + ?? new Dictionary< GamePath, MetaManager.FileInformation >(); + + + foreach( var (file, info) in files ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( info.CurrentFile?.FullName ?? "None" ); + ImGui.TableNextColumn(); + ImGui.Text( file ); + ImGui.TableNextColumn(); + info.CurrentFile?.Refresh(); + ImGui.Text( info.CurrentFile?.Exists ?? false ? "Exists" : "Missing" ); + ImGui.TableNextColumn(); + ImGui.Text( info.Changed ? "Data Changed" : "Unchanged" ); + } + } + } + + private void DrawDebugTabIpc() + { + if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) + { + return; + } + + var ipc = _penumbra.Ipc; + ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); + ImGui.Text( "Available subscriptions:" ); + using var indent = ImGuiRaii.PushIndent(); + if( ipc.ProviderApiVersion != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); + } + + if( ipc.ProviderRedrawName != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); + } + + if( ipc.ProviderRedrawObject != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); + } + + if( ipc.ProviderRedrawAll != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); + } + + if( ipc.ProviderResolveDefault != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); + } + + if( ipc.ProviderResolveCharacter != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); + } + + if( ipc.ProviderChangedItemTooltip != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); + } + + if( ipc.ProviderChangedItemClick != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); + } + } + + private void DrawDebugTabMissingFiles() + { + if( !ImGui.CollapsingHeader( "Missing Files##Debug" ) ) + { + return; + } + + var manager = Service< ModManager >.Get(); + var cache = manager.Collections.CurrentCollection.Cache; + if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + foreach( var file in cache.MissingFiles ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + if( ImGui.Selectable( file.FullName ) ) + { + ImGui.SetClipboardText( file.FullName ); + } + + ImGuiCustom.HoverTooltip( "Click to copy to clipboard." ); + } + } + + private void DrawDebugTab() + { + if( !ImGui.BeginTabItem( "Debug Tab" ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + DrawDebugTabGeneral(); + ImGui.NewLine(); + DrawDebugTabMissingFiles(); + ImGui.NewLine(); + DrawDebugTabRedraw(); + ImGui.NewLine(); + DrawDebugTabPlayers(); + ImGui.NewLine(); + DrawDebugTabTempFiles(); + ImGui.NewLine(); + DrawDebugTabIpc(); + ImGui.NewLine(); + } } \ No newline at end of file From a1f02975cb53f39cf869238e41837177f46ff5d6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 3 Jan 2022 10:21:30 +0000 Subject: [PATCH 0042/2451] [CI] Updating repo.json for refs/tags/0.4.6.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5ad53c7e..e57f21f6 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.5", - "TestingAssemblyVersion": "0.4.6.5", + "AssemblyVersion": "0.4.6.6", + "TestingAssemblyVersion": "0.4.6.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f6018126666d38f0a02be7c8fa206d15d8e78271 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 4 Jan 2022 00:30:18 +0100 Subject: [PATCH 0043/2451] More sophisticated fix against E4S crashes with working mods in E4S. --- Penumbra.GameData/Util/GamePath.cs | 2 +- Penumbra/Interop/ResourceLoader.cs | 56 ++----- Penumbra/Meta/MetaCollection.cs | 2 +- Penumbra/Meta/MetaManager.cs | 21 ++- Penumbra/Mod/ModResources.cs | 137 ++++++++--------- Penumbra/Mods/ModCollectionCache.cs | 26 ++-- Penumbra/Mods/ModManager.cs | 8 + Penumbra/UI/MenuTabs/TabDebug.cs | 2 - Penumbra/UI/MenuTabs/TabEffective.cs | 2 +- .../TabInstalled/TabInstalledDetails.cs | 2 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- Penumbra/Util/FullPath.cs | 85 +++++++++++ Penumbra/Util/RelPath.cs | 138 +++++++++--------- 13 files changed, 273 insertions(+), 210 deletions(-) create mode 100644 Penumbra/Util/FullPath.cs diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index b602d8d6..002740f1 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -22,7 +22,7 @@ namespace Penumbra.GameData.Util } else { - _path = ""; + _path = string.Empty; } } diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 5cfc23f3..fbb05077 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Hooking; using Dalamud.Logging; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Structs; @@ -19,10 +18,10 @@ public class ResourceLoader : IDisposable public Penumbra Penumbra { get; set; } public bool IsEnabled { get; set; } - public bool HacksEnabled { get; set; } public Crc32 Crc32 { get; } + public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); // Delegate prototypes [UnmanagedFunctionPointer( CallingConvention.ThisCall )] @@ -40,7 +39,7 @@ public class ResourceLoader : IDisposable , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate bool CheckFileStatePrototype( IntPtr unk1, ulong unk2 ); + public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc ); [UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate byte LoadTexFileExternPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); @@ -64,7 +63,6 @@ public class ResourceLoader : IDisposable // Unmanaged functions public ReadFilePrototype? ReadFile { get; private set; } - public CheckFileStatePrototype? CheckFileState { get; private set; } public LoadTexFileLocalPrototype? LoadTexFileLocal { get; private set; } public LoadMdlFileLocalPrototype? LoadMdlFileLocal { get; private set; } @@ -128,37 +126,21 @@ public class ResourceLoader : IDisposable LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); } - private bool CheckForTerritory() + private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) { - var territory = Dalamud.GameData.GetExcelSheet< TerritoryType >()?.GetRow( Dalamud.ClientState.TerritoryType ); - var bad = territory?.Unknown40 ?? false; - switch( bad ) - { - case true when HacksEnabled: - CheckFileStateHook?.Disable(); - LoadTexFileExternHook?.Disable(); - LoadMdlFileExternHook?.Disable(); - HacksEnabled = false; - return bad; - case false when Penumbra.Config.IsEnabled && !HacksEnabled: - CheckFileStateHook?.Enable(); - LoadTexFileExternHook?.Enable(); - LoadMdlFileExternHook?.Enable(); - HacksEnabled = true; - break; - } - - return bad; + var modManager = Service< ModManager >.Get(); + return modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); } - private static bool CheckFileStateDetour( IntPtr _, ulong _2 ) - => true; + private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ) + : LoadTexFileExternHook!.Original( resourceHandle, unk1, unk2, unk3, ptr ); - private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr _ ) - => LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ); - - private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr _ ) - => LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ); + private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ) + : LoadMdlFileExternHook!.Original( resourceHandle, unk1, unk2, ptr ); private unsafe void* GetResourceSyncHandler( IntPtr pFileManager, @@ -223,11 +205,6 @@ public class ResourceLoader : IDisposable bool isUnknown ) { - if( CheckForTerritory() ) - { - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - string file; var modManager = Service< ModManager >.Get(); @@ -277,11 +254,6 @@ public class ResourceLoader : IDisposable private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) { - if( CheckForTerritory() ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) { PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); @@ -340,7 +312,6 @@ public class ResourceLoader : IDisposable LoadMdlFileExternHook.Enable(); IsEnabled = true; - HacksEnabled = true; } public void Disable() @@ -357,7 +328,6 @@ public class ResourceLoader : IDisposable LoadTexFileExternHook?.Disable(); LoadMdlFileExternHook?.Disable(); IsEnabled = false; - HacksEnabled = false; } public void Dispose() diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 3e47ff47..e0722424 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -147,7 +147,7 @@ namespace Penumbra.Meta // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, // combining them with the given ModMeta. - public void Update( IEnumerable< FileInfo > files, DirectoryInfo basePath, ModMeta modMeta ) + public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) { DefaultData.Clear(); GroupData.Clear(); diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index f0d2cad9..57bb0602 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -17,7 +17,7 @@ namespace Penumbra.Meta { public readonly object Data; public bool Changed; - public FileInfo? CurrentFile; + public FullPath? CurrentFile; public FileInformation( object data ) => Data = data; @@ -35,7 +35,7 @@ namespace Penumbra.Meta _ => throw new NotImplementedException(), }; DisposeFile( CurrentFile ); - CurrentFile = TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" ); + CurrentFile = new FullPath(TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" )); Changed = false; } } @@ -45,7 +45,7 @@ namespace Penumbra.Meta private readonly MetaDefaults _default; private readonly DirectoryInfo _dir; private readonly ResidentResources _resourceManagement; - private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; + private readonly Dictionary< GamePath, FullPath > _resolvedFiles; private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); @@ -53,9 +53,9 @@ namespace Penumbra.Meta public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - public IEnumerable< (GamePath, FileInfo) > Files + public IEnumerable< (GamePath, FullPath) > Files => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) - .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile! ) ); + .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); public int Count => _currentManipulations.Count; @@ -63,9 +63,8 @@ namespace Penumbra.Meta public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) => _currentManipulations.TryGetValue( manip, out mod! ); - private static void DisposeFile( FileInfo? file ) + private static void DisposeFile( FullPath? file ) { - file?.Refresh(); if( !( file?.Exists ?? false ) ) { return; @@ -73,11 +72,11 @@ namespace Penumbra.Meta try { - file.Delete(); + File.Delete( file.Value.FullName ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete temporary file \"{file.FullName}\":\n{e}" ); + PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); } } @@ -120,7 +119,7 @@ namespace Penumbra.Meta private void ClearDirectory() => ClearDirectory( _dir ); - public MetaManager( string name, Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo tempDir ) + public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) { _resolvedFiles = resolvedFiles; _default = Service< MetaDefaults >.Get(); @@ -139,7 +138,7 @@ namespace Penumbra.Meta foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) { kvp.Value.Write( _dir, kvp.Key ); - _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!; + _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; } } diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index 859a92a5..d47851a5 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -3,86 +3,87 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Penumbra.Meta; +using Penumbra.Util; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +[Flags] +public enum ResourceChange { - [Flags] - public enum ResourceChange + None = 0, + Files = 1, + Meta = 2, +} + +// Contains static mod data that should only change on filesystem changes. +public class ModResources +{ + public List< FullPath > ModFiles { get; private set; } = new(); + public List< FullPath > MetaFiles { get; private set; } = new(); + + public MetaCollection MetaManipulations { get; private set; } = new(); + + + private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) { - None = 0, - Files = 1, - Meta = 2, + MetaManipulations.Update( MetaFiles, basePath, meta ); + MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); } - // Contains static mod data that should only change on filesystem changes. - public class ModResources + public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) { - public List< FileInfo > ModFiles { get; private set; } = new(); - public List< FileInfo > MetaFiles { get; private set; } = new(); - - public MetaCollection MetaManipulations { get; private set; } = new(); - - - private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) + var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); + if( newManipulations == null ) { - MetaManipulations.Update( MetaFiles, basePath, meta ); - MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); + ForceManipulationsUpdate( meta, basePath ); } - - public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) + else { - var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); - if( newManipulations == null ) + MetaManipulations = newManipulations; + if( validate && !MetaManipulations.Validate( meta ) ) { ForceManipulationsUpdate( meta, basePath ); } - else - { - MetaManipulations = newManipulations; - if( validate && !MetaManipulations.Validate( meta ) ) - { - ForceManipulationsUpdate( meta, basePath ); - } - } - } - - // Update the current set of files used by the mod, - // returns true if anything changed. - public ResourceChange RefreshModFiles( DirectoryInfo basePath ) - { - List< FileInfo > tmpFiles = new( ModFiles.Count ); - List< FileInfo > tmpMetas = new( MetaFiles.Count ); - // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo - foreach( var file in basePath.EnumerateDirectories() - .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .OrderBy( f => f.FullName ) ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - tmpMetas.Add( file ); - break; - default: - tmpFiles.Add( file ); - break; - } - } - - ResourceChange changes = 0; - if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) - { - ModFiles = tmpFiles; - changes |= ResourceChange.Files; - } - - if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) - { - MetaFiles = tmpMetas; - changes |= ResourceChange.Meta; - } - - return changes; } } + + // Update the current set of files used by the mod, + // returns true if anything changed. + public ResourceChange RefreshModFiles( DirectoryInfo basePath ) + { + List< FullPath > tmpFiles = new(ModFiles.Count); + List< FullPath > tmpMetas = new(MetaFiles.Count); + // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo + foreach( var file in basePath.EnumerateDirectories() + .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + .Select( f => new FullPath( f ) ) + .OrderBy( f => f.FullName ) ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + tmpMetas.Add( file ); + break; + default: + tmpFiles.Add( file ); + break; + } + } + + ResourceChange changes = 0; + if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) + { + ModFiles = tmpFiles; + changes |= ResourceChange.Files; + } + + if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) + { + MetaFiles = tmpMetas; + changes |= ResourceChange.Meta; + } + + return changes; + } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 17fe47b3..1b3e6c1c 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Logging; using Penumbra.GameData.Util; using Penumbra.Meta; @@ -26,9 +25,10 @@ namespace Penumbra.Mods public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); + public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FileInfo > MissingFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly HashSet< ulong > Checksums = new(); public readonly MetaManager MetaManipulations; public IReadOnlyDictionary< string, object? > ChangedItems @@ -75,6 +75,9 @@ namespace Penumbra.Mods } AddMetaFiles(); + Checksums.Clear(); + foreach( var file in ResolvedFiles ) + Checksums.Add( file.Value.Crc64 ); } private void SetChangedItems() @@ -128,7 +131,7 @@ namespace Penumbra.Mods AddRemainingFiles( mod ); } - private void AddFile( Mod.Mod mod, GamePath gamePath, FileInfo file ) + private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) { if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) { @@ -145,7 +148,7 @@ namespace Penumbra.Mods } } - private void AddMissingFile( FileInfo file ) + private void AddMissingFile( FullPath file ) { switch( file.Extension.ToLowerInvariant() ) { @@ -162,16 +165,15 @@ namespace Penumbra.Mods { foreach( var (file, paths) in option.OptionFiles ) { - var fullPath = Path.Combine( mod.Data.BasePath.FullName, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.FullName == fullPath ); + var fullPath = new FullPath(mod.Data.BasePath, file); + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals(fullPath) ); if( idx < 0 ) { - AddMissingFile( new FileInfo( fullPath ) ); + AddMissingFile( fullPath ); continue; } var registeredFile = mod.Data.Resources.ModFiles[ idx ]; - registeredFile.Refresh(); if( !registeredFile.Exists ) { AddMissingFile( registeredFile ); @@ -230,10 +232,9 @@ namespace Penumbra.Mods } var file = mod.Data.Resources.ModFiles[ i ]; - file.Refresh(); if( file.Exists ) { - AddFile( mod, new GamePath( file, mod.Data.BasePath ), file ); + AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); } else { @@ -350,14 +351,13 @@ namespace Penumbra.Mods } } - public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath ) + public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) { if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) { return null; } - candidate.Refresh(); if( candidate.FullName.Length >= 260 || !candidate.Exists ) { return null; diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 87b78d4b..b7ab6544 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -347,6 +347,14 @@ namespace Penumbra.Mods return true; } + public bool CheckCrc64( ulong crc ) + { + if( Collections.ActiveCollection.Cache?.Checksums.Contains( crc ) ?? false ) + return true; + + return Collections.ForcedCollection.Cache?.Checksums.Contains( crc ) ?? false; + } + public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) { var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index ea46be36..49961160 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -165,7 +165,6 @@ public partial class SettingsInterface manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); - PrintValue( "Resource Loader Hacks Enabled", _penumbra.ResourceLoader.HacksEnabled.ToString() ); } private void DrawDebugTabRedraw() @@ -298,7 +297,6 @@ public partial class SettingsInterface ImGui.TableNextColumn(); ImGui.Text( file ); ImGui.TableNextColumn(); - info.CurrentFile?.Refresh(); ImGui.Text( info.CurrentFile?.Exists ?? false ? "Exists" : "Missing" ); ImGui.TableNextColumn(); ImGui.Text( info.Changed ? "Data Changed" : "Unchanged" ); diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 51143c4d..63edcebf 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -64,7 +64,7 @@ namespace Penumbra.UI } } - private bool CheckFilters( KeyValuePair< GamePath, FileInfo > kvp ) + private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) { if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) { diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index b95e0f6f..cf079a24 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -55,7 +55,7 @@ namespace Penumbra.UI private Option? _selectedOption; private string _currentGamePaths = ""; - private (FileInfo name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; + private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; private readonly Selector _selector; private readonly SettingsInterface _base; diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index ecdaa87e..9cd6812b 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -94,7 +94,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E5B440 ); + var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E603C0 ); if( resourceHandler == null ) { diff --git a/Penumbra/Util/FullPath.cs b/Penumbra/Util/FullPath.cs new file mode 100644 index 00000000..829bc5bb --- /dev/null +++ b/Penumbra/Util/FullPath.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using Penumbra.GameData.Util; + +namespace Penumbra.Util; + +public readonly struct FullPath : IComparable, IEquatable< FullPath > +{ + public readonly string FullName; + public readonly string InternalName; + public readonly ulong Crc64; + + public FullPath( DirectoryInfo baseDir, RelPath relPath ) + { + FullName = Path.Combine( baseDir.FullName, relPath ); + InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); + Crc64 = ComputeCrc64( InternalName ); + } + + public FullPath( FileInfo file ) + { + FullName = file.FullName; + InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); + Crc64 = ComputeCrc64( InternalName ); + } + + public bool Exists + => File.Exists( FullName ); + + public string Extension + => Path.GetExtension( FullName ); + + public string Name + => Path.GetFileName( FullName ); + + public GamePath ToGamePath( DirectoryInfo dir ) + => FullName.StartsWith(dir.FullName) ? GamePath.GenerateUnchecked( InternalName[(dir.FullName.Length+1)..]) : GamePath.GenerateUnchecked( string.Empty ); + + private static ulong ComputeCrc64( string name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( '/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + public int CompareTo( object? obj ) + => obj switch + { + FullPath p => string.Compare( InternalName, p.InternalName, StringComparison.InvariantCulture ), + FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), + _ => -1, + }; + + public bool Equals( FullPath other ) + { + if( Crc64 != other.Crc64 ) + { + return false; + } + + if( FullName.Length == 0 || other.FullName.Length == 0 ) + { + return true; + } + + return InternalName.Equals( other.InternalName ); + } + + public override int GetHashCode() + => Crc64.GetHashCode(); + + public override string ToString() + => FullName; +} \ No newline at end of file diff --git a/Penumbra/Util/RelPath.cs b/Penumbra/Util/RelPath.cs index 2c08cd9a..6d6ea369 100644 --- a/Penumbra/Util/RelPath.cs +++ b/Penumbra/Util/RelPath.cs @@ -3,79 +3,81 @@ using System.IO; using System.Linq; using Penumbra.GameData.Util; -namespace Penumbra.Util +namespace Penumbra.Util; + +public readonly struct RelPath : IComparable { - public readonly struct RelPath : IComparable + public const int MaxRelPathLength = 256; + + private readonly string _path; + + private RelPath( string path, bool _ ) + => _path = path; + + private RelPath( string? path ) { - public const int MaxRelPathLength = 256; - - private readonly string _path; - - private RelPath( string path, bool _ ) - => _path = path; - - private RelPath( string? path ) + if( path != null && path.Length < MaxRelPathLength ) { - if( path != null && path.Length < MaxRelPathLength ) - { - _path = Trim( ReplaceSlash( path ) ); - } - else - { - _path = ""; - } + _path = Trim( ReplaceSlash( path ) ); } - - public RelPath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file, baseDir ) ? Trim( Substring( file, baseDir ) ) : ""; - - public RelPath( GamePath gamePath ) - => _path = ReplaceSlash( gamePath ); - - public GamePath ToGamePath( int skipFolders = 0 ) + else { - string p = this; - if( skipFolders > 0 ) - { - p = string.Join( "/", p.Split( '\\' ).Skip( skipFolders ) ); - return GamePath.GenerateUncheckedLower( p ); - } - - return GamePath.GenerateUncheckedLower( p.Replace( '\\', '/' ) ); + _path = ""; } - - private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxRelPathLength; - - private static string Substring( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '/', '\\' ); - - private static string Trim( string path ) - => path.TrimStart( '\\' ); - - public static implicit operator string( RelPath relPath ) - => relPath._path; - - public static explicit operator RelPath( string relPath ) - => new( relPath ); - - public bool Empty - => _path.Length == 0; - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), - _ => -1, - }; - } - - public override string ToString() - => _path; } + + public RelPath( FullPath file, DirectoryInfo baseDir ) + => _path = CheckPre( file.FullName, baseDir ) ? ReplaceSlash( Trim( Substring( file.FullName, baseDir ) ) ) : string.Empty; + + public RelPath( FileInfo file, DirectoryInfo baseDir ) + => _path = CheckPre( file.FullName, baseDir ) ? Trim( Substring( file.FullName, baseDir ) ) : string.Empty; + + public RelPath( GamePath gamePath ) + => _path = ReplaceSlash( gamePath ); + + public GamePath ToGamePath( int skipFolders = 0 ) + { + string p = this; + if( skipFolders > 0 ) + { + p = string.Join( "/", p.Split( '\\' ).Skip( skipFolders ) ); + return GamePath.GenerateUncheckedLower( p ); + } + + return GamePath.GenerateUncheckedLower( p.Replace( '\\', '/' ) ); + } + + private static bool CheckPre( string file, DirectoryInfo baseDir ) + => file.StartsWith( baseDir.FullName ) && file.Length < MaxRelPathLength; + + private static string Substring( string file, DirectoryInfo baseDir ) + => file.Substring( baseDir.FullName.Length ); + + private static string ReplaceSlash( string path ) + => path.Replace( '/', '\\' ); + + private static string Trim( string path ) + => path.TrimStart( '\\' ); + + public static implicit operator string( RelPath relPath ) + => relPath._path; + + public static explicit operator RelPath( string relPath ) + => new(relPath); + + public bool Empty + => _path.Length == 0; + + public int CompareTo( object? rhs ) + { + return rhs switch + { + string path => string.Compare( _path, path, StringComparison.InvariantCulture ), + RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), + _ => -1, + }; + } + + public override string ToString() + => _path; } \ No newline at end of file From 19b295bbc32576b8b68346176b402a580024efb6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 4 Jan 2022 11:36:41 +0000 Subject: [PATCH 0044/2451] [CI] Updating repo.json for refs/tags/0.4.6.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e57f21f6..bc6c1428 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.6", - "TestingAssemblyVersion": "0.4.6.6", + "AssemblyVersion": "0.4.6.7", + "TestingAssemblyVersion": "0.4.6.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From aa7d71530dc6d356df3b40a32b00e1bd76639902 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jan 2022 11:46:53 +0100 Subject: [PATCH 0045/2451] Change EST files to be sorted and thus work. --- Penumbra/Meta/Files/EstFile.cs | 239 +++++++++++++++++---------------- 1 file changed, 122 insertions(+), 117 deletions(-) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index fc00fed8..18a53062 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -4,154 +4,159 @@ using System.Linq; using Lumina.Data; using Penumbra.GameData.Enums; -namespace Penumbra.Meta.Files +namespace Penumbra.Meta.Files; + +// EST Structure: +// 1x [NumEntries : UInt32] +// Apparently entries need to be sorted. +// #NumEntries x [SetId : UInt16] [RaceId : UInt16] +// #NumEntries x [SkeletonId : UInt16] +public class EstFile { - // EST Structure: - // 1x [NumEntries : UInt32] - // #NumEntries x [SetId : UInt16] [RaceId : UInt16] - // #NumEntries x [SkeletonId : UInt16] - public class EstFile + private const ushort EntryDescSize = 4; + private const ushort EntrySize = 2; + + private readonly SortedList< GenderRace, SortedList< ushort, ushort > > _entries = new(); + private uint NumEntries { get; set; } + + private EstFile( EstFile clone ) { - private const ushort EntryDescSize = 4; - private const ushort EntrySize = 2; - - private readonly Dictionary< GenderRace, Dictionary< ushort, ushort > > _entries = new(); - private uint NumEntries { get; set; } - - private EstFile( EstFile clone ) + NumEntries = clone.NumEntries; + _entries = new SortedList< GenderRace, SortedList< ushort, ushort > >( clone._entries.Count ); + foreach( var (genderRace, data) in clone._entries ) { - NumEntries = clone.NumEntries; - _entries = new Dictionary< GenderRace, Dictionary< ushort, ushort > >( clone._entries.Count ); - foreach( var kvp in clone._entries ) + var dict = new SortedList< ushort, ushort >( data.Count ); + foreach( var (setId, value) in data ) { - var dict = kvp.Value.ToDictionary( k => k.Key, k => k.Value ); - _entries.Add( kvp.Key, dict ); + dict.Add( setId, value ); } + + _entries.Add( genderRace, dict ); + } + } + + public EstFile Clone() + => new(this); + + private bool DeleteEntry( GenderRace gr, ushort setId ) + { + if( !_entries.TryGetValue( gr, out var setDict ) ) + { + return false; } - public EstFile Clone() - => new( this ); - - private bool DeleteEntry( GenderRace gr, ushort setId ) + if( !setDict.ContainsKey( setId ) ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) - { - return false; - } - - if( !setDict.ContainsKey( setId ) ) - { - return false; - } - - setDict.Remove( setId ); - if( setDict.Count == 0 ) - { - _entries.Remove( gr ); - } - - --NumEntries; - return true; + return false; } - private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry ) + setDict.Remove( setId ); + if( setDict.Count == 0 ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) - { - _entries[ gr ] = new Dictionary< ushort, ushort >(); - setDict = _entries[ gr ]; - } + _entries.Remove( gr ); + } - if( setDict.TryGetValue( setId, out var oldEntry ) ) - { - if( oldEntry == entry ) - { - return ( false, false ); - } + --NumEntries; + return true; + } - setDict[ setId ] = entry; - return ( false, true ); + private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry ) + { + if( !_entries.TryGetValue( gr, out var setDict ) ) + { + _entries[ gr ] = new SortedList< ushort, ushort >(); + setDict = _entries[ gr ]; + } + + if( setDict.TryGetValue( setId, out var oldEntry ) ) + { + if( oldEntry == entry ) + { + return ( false, false ); } setDict[ setId ] = entry; - return ( true, true ); + return ( false, true ); } - public bool SetEntry( GenderRace gr, ushort setId, ushort entry ) + setDict[ setId ] = entry; + return ( true, true ); + } + + public bool SetEntry( GenderRace gr, ushort setId, ushort entry ) + { + if( entry == 0 ) { - if( entry == 0 ) - { - return DeleteEntry( gr, setId ); - } - - var (addedNew, changed) = AddEntry( gr, setId, entry ); - if( !addedNew ) - { - return changed; - } - - ++NumEntries; - return true; + return DeleteEntry( gr, setId ); } - public ushort GetEntry( GenderRace gr, ushort setId ) + var (addedNew, changed) = AddEntry( gr, setId, entry ); + if( !addedNew ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) - { - return 0; - } - - return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry; + return changed; } - public byte[] WriteBytes() + ++NumEntries; + return true; + } + + public ushort GetEntry( GenderRace gr, ushort setId ) + { + if( !_entries.TryGetValue( gr, out var setDict ) ) { - using MemoryStream mem = new( ( int )( 4 + ( EntryDescSize + EntrySize ) * NumEntries ) ); - using BinaryWriter bw = new( mem ); - - bw.Write( NumEntries ); - foreach( var kvp1 in _entries ) - { - foreach( var kvp2 in kvp1.Value ) - { - bw.Write( kvp2.Key ); - bw.Write( ( ushort )kvp1.Key ); - } - } - - foreach( var kvp2 in _entries.SelectMany( kvp1 => kvp1.Value ) ) - { - bw.Write( kvp2.Value ); - } - - return mem.ToArray(); + return 0; } + return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry; + } - public EstFile( FileResource file ) + public byte[] WriteBytes() + { + using MemoryStream mem = new(( int )( 4 + ( EntryDescSize + EntrySize ) * NumEntries )); + using BinaryWriter bw = new(mem); + + bw.Write( NumEntries ); + foreach( var kvp1 in _entries ) { - file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); - NumEntries = file.Reader.ReadUInt32(); - - var currentEntryDescOffset = 4; - var currentEntryOffset = 4 + EntryDescSize * NumEntries; - for( var i = 0; i < NumEntries; ++i ) + foreach( var kvp2 in kvp1.Value ) { - file.Reader.BaseStream.Seek( currentEntryDescOffset, SeekOrigin.Begin ); - currentEntryDescOffset += EntryDescSize; - var setId = file.Reader.ReadUInt16(); - var raceId = ( GenderRace )file.Reader.ReadUInt16(); - if( !raceId.IsValid() ) - { - continue; - } - - file.Reader.BaseStream.Seek( currentEntryOffset, SeekOrigin.Begin ); - currentEntryOffset += EntrySize; - var entry = file.Reader.ReadUInt16(); - - AddEntry( raceId, setId, entry ); + bw.Write( kvp2.Key ); + bw.Write( ( ushort )kvp1.Key ); } } + + foreach( var kvp2 in _entries.SelectMany( kvp1 => kvp1.Value ) ) + { + bw.Write( kvp2.Value ); + } + + return mem.ToArray(); + } + + + public EstFile( FileResource file ) + { + file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); + NumEntries = file.Reader.ReadUInt32(); + + var currentEntryDescOffset = 4; + var currentEntryOffset = 4 + EntryDescSize * NumEntries; + for( var i = 0; i < NumEntries; ++i ) + { + file.Reader.BaseStream.Seek( currentEntryDescOffset, SeekOrigin.Begin ); + currentEntryDescOffset += EntryDescSize; + var setId = file.Reader.ReadUInt16(); + var raceId = ( GenderRace )file.Reader.ReadUInt16(); + if( !raceId.IsValid() ) + { + continue; + } + + file.Reader.BaseStream.Seek( currentEntryOffset, SeekOrigin.Begin ); + currentEntryOffset += EntrySize; + var entry = file.Reader.ReadUInt16(); + + AddEntry( raceId, setId, entry ); + } } } \ No newline at end of file From 3e5ea0d89c46b701e4a0686e5c29b5e9c8a00395 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 6 Jan 2022 10:50:49 +0000 Subject: [PATCH 0046/2451] [CI] Updating repo.json for refs/tags/0.4.6.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index bc6c1428..890f3b8f 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.7", - "TestingAssemblyVersion": "0.4.6.7", + "AssemblyVersion": "0.4.6.8", + "TestingAssemblyVersion": "0.4.6.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 7f9ca5db768464e137f08226e4eb3aca192a21ba Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jan 2022 13:36:40 +0100 Subject: [PATCH 0047/2451] Add a bunch of help texts and expand on information. --- Penumbra/Configuration.cs | 1 + Penumbra/Interop/ResidentResources.cs | 2 +- Penumbra/Meta/MetaManager.cs | 2 +- Penumbra/Mods/CollectionManager.cs | 8 +- Penumbra/Mods/ModCollection.cs | 2 +- Penumbra/Penumbra.cs | 6 +- Penumbra/UI/MenuTabs/TabCollections.cs | 564 ++++++++++-------- .../TabInstalled/TabInstalledSelector.cs | 2 +- Penumbra/UI/MenuTabs/TabSettings.cs | 177 +++--- Penumbra/UI/SettingsMenu.cs | 131 ++-- 10 files changed, 505 insertions(+), 390 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 2125315e..274a14fc 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -32,6 +32,7 @@ namespace Penumbra public string ForcedCollection { get; set; } = ""; public bool SortFoldersFirst { get; set; } = false; + public bool HasReadCharacterCollectionDesc { get; set; } = false; public Dictionary< string, string > CharacterCollections { get; set; } = new(); public Dictionary< string, string > ModSortOrder { get; set; } = new(); diff --git a/Penumbra/Interop/ResidentResources.cs b/Penumbra/Interop/ResidentResources.cs index 9bf6d93a..93e1e867 100644 --- a/Penumbra/Interop/ResidentResources.cs +++ b/Penumbra/Interop/ResidentResources.cs @@ -75,7 +75,7 @@ namespace Penumbra.Interop } // Forces the reload of a specific set of 85 files, notably containing the eqp, eqdp, gmp and est tables, by filename. - public unsafe void ReloadPlayerResources() + public unsafe void ReloadResidentResources() { ReloadCharacterResources(); diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 57bb0602..cfb88ce2 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -93,7 +93,7 @@ namespace Penumbra.Meta ClearDirectory(); if( reload ) { - _resourceManagement.ReloadPlayerResources(); + _resourceManagement.ReloadResidentResources(); } } diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 22bd00ce..c6d220a7 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -43,7 +43,7 @@ public class CollectionManager if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) { var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadPlayerResources(); + resourceManager.ReloadResidentResources(); } ActiveCollection = newActive; @@ -111,7 +111,7 @@ public class CollectionManager if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) { - Service< ResidentResources >.Get().ReloadPlayerResources(); + Service< ResidentResources >.Get().ReloadResidentResources(); } } @@ -221,7 +221,7 @@ public class CollectionManager { ActiveCollection = c; var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadPlayerResources(); + resourceManager.ReloadResidentResources(); } DefaultCollection = c; @@ -242,7 +242,7 @@ public class CollectionManager { ActiveCollection = c; var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadPlayerResources(); + resourceManager.ReloadResidentResources(); } CharacterCollection[ characterName ] = c; diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index feae0857..0a6153d9 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -145,7 +145,7 @@ namespace Penumbra.Mods Cache.UpdateMetaManipulations(); if( activeCollection ) { - Service< ResidentResources >.Get().ReloadPlayerResources(); + Service< ResidentResources >.Get().ReloadResidentResources(); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 093eaf06..baa5f368 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -65,7 +65,7 @@ namespace Penumbra ResourceLoader.Init(); ResourceLoader.Enable(); - gameUtils.ReloadPlayerResources(); + gameUtils.ReloadResidentResources(); SettingsInterface = new SettingsInterface( this ); @@ -98,7 +98,7 @@ namespace Penumbra } Config.IsEnabled = true; - Service< ResidentResources >.Get().ReloadPlayerResources(); + Service< ResidentResources >.Get().ReloadResidentResources(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( true ); @@ -117,7 +117,7 @@ namespace Penumbra } Config.IsEnabled = false; - Service< ResidentResources >.Get().ReloadPlayerResources(); + Service< ResidentResources >.Get().ReloadResidentResources(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( false ); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 898f24e7..cf6cbb92 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; using Penumbra.Mod; @@ -10,293 +11,390 @@ using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabCollections { - private class TabCollections + private const string CharacterCollectionHelpPopup = "Character Collection Information"; + private readonly Selector _selector; + private readonly ModManager _manager; + private string _collectionNames = null!; + private string _collectionNamesWithNone = null!; + private ModCollection[] _collections = null!; + private int _currentCollectionIndex; + private int _currentForcedIndex; + private int _currentDefaultIndex; + private readonly Dictionary< string, int > _currentCharacterIndices = new(); + private string _newCollectionName = string.Empty; + private string _newCharacterName = string.Empty; + + private void UpdateNames() { - public const string LabelCurrentCollection = "Current Collection"; - private readonly Selector _selector; - private readonly ModManager _manager; - private string _collectionNames = null!; - private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; - private int _currentCollectionIndex; - private int _currentForcedIndex; - private int _currentDefaultIndex; - private readonly Dictionary< string, int > _currentCharacterIndices = new(); - private string _newCollectionName = string.Empty; - private string _newCharacterName = string.Empty; + _collections = _manager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; + _collectionNamesWithNone = "None\0" + _collectionNames; + UpdateIndices(); + } - private void UpdateNames() + + private int GetIndex( ModCollection collection ) + { + var ret = _collections.IndexOf( c => c.Name == collection.Name ); + if( ret < 0 ) { - _collections = _manager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); - _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; - _collectionNamesWithNone = "None\0" + _collectionNames; - UpdateIndices(); + PluginLog.Error( $"Collection {collection.Name} is not found in collections." ); + return 0; } + return ret; + } - private int GetIndex( ModCollection collection ) + private void UpdateIndex() + => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; + + private void UpdateForcedIndex() + => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); + + private void UpdateDefaultIndex() + => _currentDefaultIndex = GetIndex( _manager.Collections.DefaultCollection ); + + private void UpdateCharacterIndices() + { + _currentCharacterIndices.Clear(); + foreach( var kvp in _manager.Collections.CharacterCollection ) { - var ret = _collections.IndexOf( c => c.Name == collection.Name ); - if( ret < 0 ) - { - PluginLog.Error( $"Collection {collection.Name} is not found in collections." ); - return 0; - } + _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); + } + } - return ret; + private void UpdateIndices() + { + UpdateIndex(); + UpdateDefaultIndex(); + UpdateForcedIndex(); + UpdateCharacterIndices(); + } + + public TabCollections( Selector selector ) + { + _selector = selector; + _manager = Service< ModManager >.Get(); + UpdateNames(); + } + + private void CreateNewCollection( Dictionary< string, ModSettings > settings ) + { + if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) + { + UpdateNames(); + SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ], true ); } - private void UpdateIndex() - => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; + _newCollectionName = string.Empty; + } - private void UpdateForcedIndex() - => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); - - private void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( _manager.Collections.DefaultCollection ); - - private void UpdateCharacterIndices() + private void DrawCleanCollectionButton() + { + if( ImGui.Button( "Clean Settings" ) ) { - _currentCharacterIndices.Clear(); - foreach( var kvp in _manager.Collections.CharacterCollection ) - { - _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); - } + var changes = ModFunctions.CleanUpCollection( _manager.Collections.CurrentCollection.Settings, + _manager.BasePath.EnumerateDirectories() ); + _manager.Collections.CurrentCollection.UpdateSettings( changes ); } - private void UpdateIndices() + ImGuiCustom.HoverTooltip( + "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); + } + + private void DrawNewCollectionInput() + { + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of mods." ); + + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _newCollectionName.Length == 0 ); + + if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) { - UpdateIndex(); - UpdateDefaultIndex(); - UpdateForcedIndex(); - UpdateCharacterIndices(); + CreateNewCollection( new Dictionary< string, ModSettings >() ); } - public TabCollections( Selector selector ) + var hover = ImGui.IsItemHovered(); + ImGui.SameLine(); + if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - _selector = selector; - _manager = Service< ModManager >.Get(); + CreateNewCollection( _manager.Collections.CurrentCollection.Settings ); + } + + hover |= ImGui.IsItemHovered(); + + style.Pop(); + if( _newCollectionName.Length == 0 && hover ) + { + ImGui.SetTooltip( "Please enter a name before creating a collection." ); + } + + var deleteCondition = _manager.Collections.Collections.Count > 1 + && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; + ImGui.SameLine(); + if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) + { + _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); + SetCurrentCollection( _manager.Collections.CurrentCollection, true ); UpdateNames(); } - private void CreateNewCollection( Dictionary< string, ModSettings > settings ) + if( !deleteCondition ) { - if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) - { - UpdateNames(); - SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ], true ); - } - - _newCollectionName = string.Empty; + ImGuiCustom.HoverTooltip( "You can not delete the default collection." ); } - private void DrawCleanCollectionButton() + if( Penumbra.Config.ShowAdvanced ) { - if( ImGui.Button( "Clean Settings" ) ) - { - var changes = ModFunctions.CleanUpCollection( _manager.Collections.CurrentCollection.Settings, - _manager.BasePath.EnumerateDirectories() ); - _manager.Collections.CurrentCollection.UpdateSettings( changes ); - } + ImGui.SameLine(); + DrawCleanCollectionButton(); + } + } - ImGuiCustom.HoverTooltip( - "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); + private void SetCurrentCollection( int idx, bool force ) + { + if( !force && idx == _currentCollectionIndex ) + { + return; } - private void DrawNewCollectionInput() + _manager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); + _currentCollectionIndex = idx; + _selector.Cache.TriggerListReset(); + if( _selector.Mod != null ) { - ImGui.InputTextWithHint( "##New Collection", "New Collection", ref _newCollectionName, 64 ); + _selector.SelectModOnUpdate( _selector.Mod.Data.BasePath.Name ); + } + } - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _newCollectionName.Length == 0 ); + public void SetCurrentCollection( ModCollection collection, bool force = false ) + { + var idx = Array.IndexOf( _collections, collection ) - 1; + if( idx >= 0 ) + { + SetCurrentCollection( idx, force ); + } + } - if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) + public void DrawCurrentCollectionSelector( bool tooltip ) + { + var index = _currentCollectionIndex; + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + var combo = ImGui.Combo( "Current Collection", ref index, _collectionNames ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); + + if( combo ) + { + SetCurrentCollection( index, false ); + } + } + + private void DrawDefaultCollectionSelector() + { + var index = _currentDefaultIndex; + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) + { + _manager.Collections.SetDefaultCollection( _collections[ index ] ); + _currentDefaultIndex = index; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" + + "They also take precedence before the forced collection." ); + + ImGui.SameLine(); + ImGui.Text( "Default Collection" ); + } + + private void DrawForcedCollectionSelector() + { + var index = _currentForcedIndex; + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _manager.Collections.CharacterCollection.Count == 0 ); + if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) + && index != _currentForcedIndex + && _manager.Collections.CharacterCollection.Count > 0 ) + { + _manager.Collections.SetForcedCollection( _collections[ index ] ); + _currentForcedIndex = index; + } + + style.Pop(); + if( _manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( + "Forced Collections only provide value if you have at least one Character Collection. There is no need to set one until then." ); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Mods in the forced collection are always loaded if not overwritten by anything in the current or character-based collection.\n" + + "Please avoid mixing meta-manipulating mods in Forced and other collections, as this will probably not work correctly." ); + ImGui.SameLine(); + ImGui.Text( "Forced Collection" ); + } + + private void DrawNewCharacterCollection() + { + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + ImGui.InputTextWithHint( "##New Character", "New Character Name", ref _newCharacterName, 32 ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Click Me for Information!" ); + ImGui.OpenPopupOnItemClick( CharacterCollectionHelpPopup, ImGuiPopupFlags.MouseButtonLeft ); + + ImGui.SameLine(); + if( ImGuiCustom.DisableButton( "Create New Character Collection", + _newCharacterName.Length > 0 && Penumbra.Config.HasReadCharacterCollectionDesc ) ) + { + _manager.Collections.CreateCharacterCollection( _newCharacterName ); + _currentCharacterIndices[ _newCharacterName ] = 0; + _newCharacterName = string.Empty; + } + + ImGuiCustom.HoverTooltip( "Please enter a Character name before creating the collection.\n" + + "You also need to have read the help text for character collections." ); + + DrawCharacterCollectionHelp(); + } + + private static void DrawCharacterCollectionHelp() + { + var size = new Vector2( 700 * ImGuiHelpers.GlobalScale, 34 * ImGui.GetTextLineHeightWithSpacing() ); + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + ImGui.SetNextWindowSize( size, ImGuiCond.Appearing ); + var _ = true; + if( ImGui.BeginPopupModal( CharacterCollectionHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) + { + const string header = "Character Collections are a Hack! Use them at your own risk."; + using var end = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var textWidth = ImGui.CalcTextSize( header ).X; + ImGui.NewLine(); + ImGui.SetCursorPosX( ( size.X - textWidth ) / 2 ); + using var color = ImGuiRaii.PushColor( ImGuiCol.Text, 0xFF0000B8 ); + ImGui.Text( header ); + color.Pop(); + ImGui.NewLine(); + ImGui.TextWrapped( + "Character Collections are collections that get applied whenever the named character gets redrawn by Penumbra," + + " whether by a manual '/penumbra redraw' command, or by the automatic redrawing feature.\n" + + "This means that they specifically require redrawing of a character to even apply, and thus can not work with mods that modify something that does not depend on characters being drawn, such as:\n" + + " - animations\n" + + " - sounds\n" + + " - most effects\n" + + " - most ui elements.\n" + + "They can also not work with actors that are not named, like the Character Preview or TryOn Actors, and they can not work in cutscenes, since redrawing in cutscenes would cancel all animations.\n" + + "They also do not work with every character customization (like skin, tattoo, hair, etc. changes) since those are not always re-requested by the game on redrawing a player. They may work, they may not, you need to test it.\n" + + "\n" + + "Due to the nature of meta manipulating mods, you can not mix meta manipulations inside a Character (or the Default) collection with meta manipulations inside the Forced collection.\n" + + "\n" + + "To verify that you have actually read this, you need to hold control and shift while clicking the Understood button for it to take effect.\n" + + "Due to the nature of redrawing being a hack, weird things (or maybe even crashes) may happen when using Character Collections. The way this works is:\n" + + " - Penumbra queues a redraw of an actor.\n" + + " - When the redraw queue reaches that actor, the actor gets undrawn (turned invisible).\n" + + " - Penumbra checks the actors name and if it matches a Character Collection, it replaces the Default collection with that one.\n" + + " - Penumbra triggers the redraw of that actor. The game requests files.\n" + + " - Penumbra potentially redirects those file requests to the modded files in the active collection, which is either Default or Character. (Or, afterwards, Forced).\n" + + " - The actor is drawn.\n" + + " - Penumbra returns the active collection to the Default Collection.\n" + + "If any of those steps fails, or if the file requests take too long, it may happen that a character is drawn with half of its models from the Default and the other half from the Character Collection, or a modded Model is loaded, but not its corresponding modded textures, which lets it stay invisible, or similar problems." ); + + var buttonSize = ImGuiHelpers.ScaledVector2( 150, 0 ); + var offset = ( size.X - buttonSize.X ) / 2; + ImGui.SetCursorPos( new Vector2( offset, size.Y - 3 * ImGui.GetTextLineHeightWithSpacing() ) ); + var state = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; + color.Push( ImGuiCol.ButtonHovered, 0xFF00A000, state ); + if( ImGui.Button( "Understood!", buttonSize ) ) { - CreateNewCollection( new Dictionary< string, ModSettings >() ); + if( state && !Penumbra.Config.HasReadCharacterCollectionDesc ) + { + Penumbra.Config.HasReadCharacterCollectionDesc = true; + Penumbra.Config.Save(); + } + + ImGui.CloseCurrentPopup(); + } + } + } + + + private void DrawCharacterCollectionSelectors() + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); + if( !ImGui.BeginChild( "##CollectionChild", AutoFillSize, true ) ) + { + return; + } + + DrawDefaultCollectionSelector(); + DrawForcedCollectionSelector(); + + foreach( var name in _manager.Collections.CharacterCollection.Keys.ToArray() ) + { + var idx = _currentCharacterIndices[ name ]; + var tmp = idx; + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) + { + _manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); + _currentCharacterIndices[ name ] = tmp; } ImGui.SameLine(); - if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) + + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FramePadding, Vector2.One * ImGuiHelpers.GlobalScale * 1.5f ); + if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) { - CreateNewCollection( _manager.Collections.CurrentCollection.Settings ); + _manager.Collections.RemoveCharacterCollection( name ); } style.Pop(); - var deleteCondition = _manager.Collections.Collections.Count > 1 - && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; - ImGui.SameLine(); - if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) - { - _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); - SetCurrentCollection( _manager.Collections.CurrentCollection, true ); - UpdateNames(); - } - - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawCleanCollectionButton(); - } - } - - private void SetCurrentCollection( int idx, bool force ) - { - if( !force && idx == _currentCollectionIndex ) - { - return; - } - - _manager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); - _currentCollectionIndex = idx; - _selector.Cache.TriggerListReset(); - if( _selector.Mod != null ) - { - _selector.SelectModOnUpdate( _selector.Mod.Data.BasePath.Name ); - } - } - - public void SetCurrentCollection( ModCollection collection, bool force = false ) - { - var idx = Array.IndexOf( _collections, collection ) - 1; - if( idx >= 0 ) - { - SetCurrentCollection( idx, force ); - } - } - - public void DrawCurrentCollectionSelector( bool tooltip ) - { - var index = _currentCollectionIndex; - var combo = ImGui.Combo( LabelCurrentCollection, ref index, _collectionNames ); - ImGuiCustom.HoverTooltip( - "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); - - if( combo ) - { - SetCurrentCollection( index, false ); - } - } - - private void DrawDefaultCollectionSelector() - { - var index = _currentDefaultIndex; - if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) - { - _manager.Collections.SetDefaultCollection( _collections[ index ] ); - _currentDefaultIndex = index; - } - - ImGuiCustom.HoverTooltip( - "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" - + "They also take precedence before the forced collection." ); + font.Pop(); ImGui.SameLine(); - ImGuiHelpers.ScaledDummy( 24, 0 ); - ImGui.SameLine(); - ImGui.Text( "Default Collection" ); + ImGui.Text( name ); } - private void DrawForcedCollectionSelector() + DrawNewCharacterCollection(); + } + + public void Draw() + { + if( !ImGui.BeginTabItem( "Collections" ) ) { - var index = _currentForcedIndex; - if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) && index != _currentForcedIndex ) - { - _manager.Collections.SetForcedCollection( _collections[ index ] ); - _currentForcedIndex = index; - } - - ImGuiCustom.HoverTooltip( - "Mods in the forced collection are always loaded if not overwritten by anything in the current or character-based collection.\n" - + "Please avoid mixing meta-manipulating mods in Forced and other collections, as this will probably not work correctly." ); - - ImGui.SameLine(); - ImGuiHelpers.ScaledDummy( 24, 0 ); - ImGui.SameLine(); - ImGui.Text( "Forced Collection" ); + return; } - private void DrawNewCharacterCollection() + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) + .Push( ImGui.EndChild ); + + if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) ) { - ImGui.InputTextWithHint( "##New Character", "New Character Name", ref _newCharacterName, 32 ); + DrawCurrentCollectionSelector( true ); - ImGui.SameLine(); - if( ImGuiCustom.DisableButton( "Create New Character Collection", _newCharacterName.Length > 0 ) ) - { - _manager.Collections.CreateCharacterCollection( _newCharacterName ); - _currentCharacterIndices[ _newCharacterName ] = 0; - _newCharacterName = string.Empty; - } - - ImGuiCustom.HoverTooltip( - "A character collection will be used whenever you manually redraw a character with the Name you have set up.\n" - + "If you enable automatic character redraws in the Settings tab, penumbra will try to use Character collections for corresponding characters automatically.\n" ); + ImGuiHelpers.ScaledDummy( 0, 10 ); + DrawNewCollectionInput(); } + raii.Pop(); - private void DrawCharacterCollectionSelectors() - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); - if( !ImGui.BeginChild( "##CollectionChild", AutoFillSize, true ) ) - { - return; - } - - DrawDefaultCollectionSelector(); - DrawForcedCollectionSelector(); - - foreach( var name in _manager.Collections.CharacterCollection.Keys.ToArray() ) - { - var idx = _currentCharacterIndices[ name ]; - var tmp = idx; - if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) - { - _manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); - _currentCharacterIndices[ name ] = tmp; - } - - ImGui.SameLine(); - - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) - { - _manager.Collections.RemoveCharacterCollection( name ); - } - - font.Pop(); - - ImGui.SameLine(); - ImGui.Text( name ); - } - - DrawNewCharacterCollection(); - } - - public void Draw() - { - if( !ImGui.BeginTabItem( "Collections" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) - .Push( ImGui.EndChild ); - - if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) ) - { - DrawCurrentCollectionSelector( true ); - - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawNewCollectionInput(); - } - - raii.Pop(); - - DrawCharacterCollectionSelectors(); - } + DrawCharacterCollectionSelectors(); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index e8e0e283..ed219462 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -616,7 +616,7 @@ namespace Penumbra.UI const float size = 200; DrawModsSelectorFilter(); - var textSize = ImGui.CalcTextSize( TabCollections.LabelCurrentCollection ).X + ImGui.GetStyle().ItemInnerSpacing.X; + var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; var comboSize = size * ImGui.GetIO().FontGlobalScale; var offset = comboSize + textSize; diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index f61783dd..64e3f126 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Numerics; using System.Text.RegularExpressions; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; using Penumbra.Interop; -using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -18,24 +18,6 @@ public partial class SettingsInterface { private class TabSettings { - private const string LabelTab = "Settings"; - private const string LabelRootFolder = "Root Folder"; - private const string LabelTempFolder = "Temporary Folder"; - private const string LabelRediscoverButton = "Rediscover Mods"; - private const string LabelOpenFolder = "Open Mods Folder"; - private const string LabelOpenTempFolder = "Open Temporary Folder"; - private const string LabelEnabled = "Enable Mods"; - private const string LabelEnabledPlayerWatch = "Enable automatic Character Redraws"; - private const string LabelWaitFrames = "Wait Frames"; - private const string LabelSortFoldersFirst = "Sort Mod Folders Before Mods"; - private const string LabelScaleModSelector = "Scale Mod Selector With Window Size"; - private const string LabelShowAdvanced = "Show Advanced Settings"; - private const string LabelLogLoadedFiles = "Log all loaded files"; - private const string LabelDisableNotifications = "Disable filesystem change notifications"; - private const string LabelEnableHttpApi = "Enable HTTP API"; - private const string LabelReloadResource = "Reload Player Resource"; - private const string LabelManageModsOffset = "\"Manage mods\"-Button Offset"; - private readonly SettingsInterface _base; private readonly Configuration _config; private bool _configChanged; @@ -52,17 +34,46 @@ public partial class SettingsInterface _newTempDirectory = _config.TempDirectory; } - private static bool DrawPressEnterWarning( string old, float? width = null ) + private static bool DrawPressEnterWarning( string old ) { const uint red = 0xFF202080; using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ); - var w = Vector2.UnitX * ( width ?? ImGui.CalcItemWidth() ); + var w = Vector2.UnitX * ImGui.CalcItemWidth(); return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); } + private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) + { + ImGui.PushID( id ); + var ret = ImGui.Button( "Open Directory" ); + ImGuiCustom.HoverTooltip( "Open this directory in your configured file explorer." ); + if( ret && condition && Directory.Exists( directory.FullName ) ) + { + Process.Start( new ProcessStartInfo( directory.FullName ) + { + UseShellExecute = true, + } ); + } + + ImGui.PopID(); + } + private void DrawRootFolder() { - var save = ImGui.InputText( LabelRootFolder, ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + ImGui.BeginGroup(); + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + var save = ImGui.InputText( "Root Directory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); + ImGui.SameLine(); + DrawOpenDirectoryButton( 0, _base._modManager.BasePath, _base._modManager.Valid ); + ImGui.EndGroup(); + if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) { return; @@ -79,36 +90,24 @@ public partial class SettingsInterface private void DrawTempFolder() { - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); ImGui.BeginGroup(); - var save = ImGui.InputText( LabelTempFolder, ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - - ImGuiCustom.HoverTooltip( "The folder used to store temporary meta manipulation files.\n" - + "Leave this blank if you have no reason not to.\n" - + "A folder 'penumbrametatmp' will be created as a subdirectory to the specified directory.\n" - + "If none is specified (i.e. this is blank) this folder will be created in the root folder instead." ); - + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + var save = ImGui.InputText( "Temp Directory", ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); ImGui.SameLine(); - if( ImGui.Button( LabelOpenTempFolder ) ) - { - if( !Directory.Exists( _base._modManager.TempPath.FullName ) || !_base._modManager.TempWritable ) - { - return; - } - - Process.Start( new ProcessStartInfo( _base._modManager.TempPath.FullName ) - { - UseShellExecute = true, - } ); - } - + ImGuiComponents.HelpMarker( "This is where Penumbra will store temporary meta manipulation files.\n" + + "Leave this blank if you have no reason not to.\n" + + "A directory 'penumbrametatmp' will be created as a sub-directory to the specified directory.\n" + + "If none is specified (i.e. this is blank) this directory will be created in the root directory instead.\n" ); + ImGui.SameLine(); + DrawOpenDirectoryButton( 1, _base._modManager.TempPath, _base._modManager.TempWritable ); ImGui.EndGroup(); + if( _newTempDirectory == _config.TempDirectory ) { return; } - if( save || DrawPressEnterWarning( _config.TempDirectory, 400 ) ) + if( save || DrawPressEnterWarning( _config.TempDirectory ) ) { _base._modManager.SetTempDirectory( _newTempDirectory ); _newTempDirectory = _config.TempDirectory; @@ -117,34 +116,21 @@ public partial class SettingsInterface private void DrawRediscoverButton() { - if( ImGui.Button( LabelRediscoverButton ) ) + if( ImGui.Button( "Rediscover Mods" ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); _base._modManager.DiscoverMods(); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); } - } - private void DrawOpenModsButton() - { - if( ImGui.Button( LabelOpenFolder ) ) - { - if( !Directory.Exists( _config.ModDirectory ) || !Service< ModManager >.Get().Valid ) - { - return; - } - - Process.Start( new ProcessStartInfo( _config.ModDirectory ) - { - UseShellExecute = true, - } ); - } + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); } private void DrawEnabledBox() { var enabled = _config.IsEnabled; - if( ImGui.Checkbox( LabelEnabled, ref enabled ) ) + if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) { _base._penumbra.SetEnabled( enabled ); } @@ -153,53 +139,68 @@ public partial class SettingsInterface private void DrawShowAdvancedBox() { var showAdvanced = _config.ShowAdvanced; - if( ImGui.Checkbox( LabelShowAdvanced, ref showAdvanced ) ) + if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) { _config.ShowAdvanced = showAdvanced; _configChanged = true; } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Enable some advanced options in this window and in the mod selector.\n" + + "This is required to enable manually editing any mod information." ); } private void DrawManageModsButtonOffsetButton() { var manageModsButtonOffset = _config.ManageModsButtonOffset; ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragFloat2( LabelManageModsOffset, ref manageModsButtonOffset, 1f ) ) + if( ImGui.DragFloat2( "\"Manage Mods\"-Button Offset", ref manageModsButtonOffset, 1f ) ) { _config.ManageModsButtonOffset = manageModsButtonOffset; _configChanged = true; } _base._manageModsButton.ForceDraw = ImGui.IsItemActive(); + ImGuiComponents.HelpMarker( + "Shift the \"Manage Mods\"-Button displayed in the login-lobby by the given amount of pixels in X/Y-direction." ); } private void DrawSortFoldersFirstBox() { var foldersFirst = _config.SortFoldersFirst; - if( ImGui.Checkbox( LabelSortFoldersFirst, ref foldersFirst ) ) + if( ImGui.Checkbox( "Sort Mod-Folders Before Mods", ref foldersFirst ) ) { _config.SortFoldersFirst = foldersFirst; _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); _configChanged = true; } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Prioritizes all mod-folders in the mod-selector in the Installed Mods tab so that folders come before single mods, instead of being sorted completely alphabetically" ); } private void DrawScaleModSelectorBox() { var scaleModSelector = _config.ScaleModSelector; - if( ImGui.Checkbox( LabelScaleModSelector, ref scaleModSelector ) ) + if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) { _config.ScaleModSelector = scaleModSelector; _configChanged = true; } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); } private void DrawLogLoadedFilesBox() { - ImGui.Checkbox( LabelLogLoadedFiles, ref _base._penumbra.ResourceLoader.LogAllFiles ); + ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); ImGui.SameLine(); var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; var tmp = regex; + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) { try @@ -212,22 +213,28 @@ public partial class SettingsInterface PluginLog.Debug( "Could not create regex:\n{Exception}", e ); } } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); } private void DrawDisableNotificationsBox() { var fsWatch = _config.DisableFileSystemNotifications; - if( ImGui.Checkbox( LabelDisableNotifications, ref fsWatch ) ) + if( ImGui.Checkbox( "Disable Filesystem Change Notifications", ref fsWatch ) ) { _config.DisableFileSystemNotifications = fsWatch; _configChanged = true; } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Currently does nothing." ); } private void DrawEnableHttpApiBox() { var http = _config.EnableHttpApi; - if( ImGui.Checkbox( LabelEnableHttpApi, ref http ) ) + if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) { if( http ) { @@ -241,21 +248,25 @@ public partial class SettingsInterface _config.EnableHttpApi = http; _configChanged = true; } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Currently does nothing." ); } private void DrawEnabledPlayerWatcher() { var enabled = _config.EnablePlayerWatch; - if( ImGui.Checkbox( LabelEnabledPlayerWatch, ref enabled ) ) + if( ImGui.Checkbox( "Enable Automatic Character Redraws", ref enabled ) ) { _config.EnablePlayerWatch = enabled; _configChanged = true; Penumbra.PlayerWatcher.SetStatus( enabled ); } - ImGuiCustom.HoverTooltip( - "If this setting is enabled, penumbra will keep tabs on characters that have a corresponding collection setup in the Collections tab.\n" - + "Penumbra will try to automatically redraw those characters using their collection when they first appear in an instance, or when they change their current equip." ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "If this setting is enabled, Penumbra will keep tabs on characters that have a corresponding character collection setup in the Collections tab.\n" + + "Penumbra will try to automatically redraw those characters using their collection when they first appear in an instance, or when they change their current equip.\n" ); if( !_config.EnablePlayerWatch || !_config.ShowAdvanced ) { @@ -264,28 +275,32 @@ public partial class SettingsInterface var waitFrames = _config.WaitFrames; ImGui.SameLine(); - ImGui.SetNextItemWidth( 50 ); - if( ImGui.InputInt( LabelWaitFrames, ref waitFrames, 0, 0 ) + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "Wait Frames", ref waitFrames, 0, 0 ) && waitFrames != _config.WaitFrames - && waitFrames > 0 - && waitFrames < 3000 ) + && waitFrames is > 0 and < 3000 ) { _base._penumbra.ObjectReloader.DefaultWaitFrames = waitFrames; _config.WaitFrames = waitFrames; _configChanged = true; } - ImGuiCustom.HoverTooltip( + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "The number of frames penumbra waits after some events (like zone changes) until it starts trying to redraw actors again, in a range of [1, 3001].\n" + "Keep this as low as possible while producing stable results." ); } private static void DrawReloadResourceButton() { - if( ImGui.Button( LabelReloadResource ) ) + if( ImGui.Button( "Reload Resident Resources" ) ) { - Service< ResidentResources >.Get().ReloadPlayerResources(); + Service< ResidentResources >.Get().ReloadResidentResources(); } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Reload some specific files that the game keeps in memory at all times.\n" + + "You usually should not need to do this." ); } private void DrawAdvancedSettings() @@ -299,7 +314,7 @@ public partial class SettingsInterface public void Draw() { - if( !ImGui.BeginTabItem( LabelTab ) ) + if( !ImGui.BeginTabItem( "Settings" ) ) { return; } @@ -309,8 +324,6 @@ public partial class SettingsInterface DrawRootFolder(); DrawRediscoverButton(); - ImGui.SameLine(); - DrawOpenModsButton(); ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawEnabledBox(); diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index 43e7bd49..12c07617 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -1,91 +1,94 @@ using System.Numerics; +using Dalamud.Interface; using ImGuiNET; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class SettingsMenu { - private class SettingsMenu + public static float InputTextWidth + => 450 * ImGuiHelpers.GlobalScale; + + private const string PenumbraSettingsLabel = "PenumbraSettings"; + + public static readonly Vector2 MinSettingsSize = new(800, 450); + public static readonly Vector2 MaxSettingsSize = new(69420, 42069); + + private readonly SettingsInterface _base; + private readonly TabSettings _settingsTab; + private readonly TabImport _importTab; + private readonly TabBrowser _browserTab; + private readonly TabEffective _effectiveTab; + private readonly TabChangedItems _changedItems; + internal readonly TabCollections CollectionsTab; + internal readonly TabInstalled InstalledTab; + + public SettingsMenu( SettingsInterface ui ) { - private const string PenumbraSettingsLabel = "PenumbraSettings"; - - public static readonly Vector2 MinSettingsSize = new( 800, 450 ); - public static readonly Vector2 MaxSettingsSize = new( 69420, 42069 ); - - private readonly SettingsInterface _base; - private readonly TabSettings _settingsTab; - private readonly TabImport _importTab; - private readonly TabBrowser _browserTab; - private readonly TabEffective _effectiveTab; - private readonly TabChangedItems _changedItems; - internal readonly TabCollections CollectionsTab; - internal readonly TabInstalled InstalledTab; - - public SettingsMenu( SettingsInterface ui ) - { - _base = ui; - _settingsTab = new TabSettings( _base ); - _importTab = new TabImport( _base ); - _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base, _importTab.NewMods ); - CollectionsTab = new TabCollections( InstalledTab.Selector ); - _effectiveTab = new TabEffective(); - _changedItems = new TabChangedItems( _base ); - } + _base = ui; + _settingsTab = new TabSettings( _base ); + _importTab = new TabImport( _base ); + _browserTab = new TabBrowser(); + InstalledTab = new TabInstalled( _base, _importTab.NewMods ); + CollectionsTab = new TabCollections( InstalledTab.Selector ); + _effectiveTab = new TabEffective(); + _changedItems = new TabChangedItems( _base ); + } #if DEBUG - private const bool DefaultVisibility = true; + private const bool DefaultVisibility = true; #else private const bool DefaultVisibility = false; #endif - public bool Visible = DefaultVisibility; - public bool DebugTabVisible = DefaultVisibility; + public bool Visible = DefaultVisibility; + public bool DebugTabVisible = DefaultVisibility; - public void Draw() + public void Draw() + { + if( !Visible ) { - if( !Visible ) - { - return; - } + return; + } - ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); + ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); #if DEBUG - var ret = ImGui.Begin( _base._penumbra.PluginDebugTitleStr, ref Visible ); + var ret = ImGui.Begin( _base._penumbra.PluginDebugTitleStr, ref Visible ); #else var ret = ImGui.Begin( _base._penumbra.Name, ref Visible ); #endif - using var raii = ImGuiRaii.DeferredEnd( ImGui.End ); - if( !ret ) + using var raii = ImGuiRaii.DeferredEnd( ImGui.End ); + if( !ret ) + { + return; + } + + ImGui.BeginTabBar( PenumbraSettingsLabel ); + raii.Push( ImGui.EndTabBar ); + + _settingsTab.Draw(); + CollectionsTab.Draw(); + _importTab.Draw(); + + if( Service< ModManager >.Get().Valid && !_importTab.IsImporting() ) + { + _browserTab.Draw(); + InstalledTab.Draw(); + _changedItems.Draw(); + if( Penumbra.Config.ShowAdvanced ) { - return; + _effectiveTab.Draw(); } + } - ImGui.BeginTabBar( PenumbraSettingsLabel ); - raii.Push( ImGui.EndTabBar ); - - _settingsTab.Draw(); - CollectionsTab.Draw(); - _importTab.Draw(); - - if( Service< ModManager >.Get().Valid && !_importTab.IsImporting() ) - { - _browserTab.Draw(); - InstalledTab.Draw(); - _changedItems.Draw(); - if( Penumbra.Config.ShowAdvanced ) - { - _effectiveTab.Draw(); - } - } - - if( DebugTabVisible ) - { - _base.DrawDebugTab(); - _base.DrawResourceManagerTab(); - } + if( DebugTabVisible ) + { + _base.DrawDebugTab(); + _base.DrawResourceManagerTab(); } } } From c542c7992320fa78b84c92c965a9cb1b2be0a15a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 8 Jan 2022 12:41:06 +0000 Subject: [PATCH 0048/2451] [CI] Updating repo.json for refs/tags/0.4.6.9 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 890f3b8f..32ec5c54 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.8", - "TestingAssemblyVersion": "0.4.6.8", + "AssemblyVersion": "0.4.6.9", + "TestingAssemblyVersion": "0.4.6.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 84fd0262c2c6c568d85826f99641bac7ccf243bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jan 2022 15:15:17 +0100 Subject: [PATCH 0049/2451] Fix metadata bug. --- Penumbra/Interop/ResidentResources.cs | 2 +- Penumbra/Mods/CollectionManager.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/ResidentResources.cs b/Penumbra/Interop/ResidentResources.cs index 93e1e867..e1d43b62 100644 --- a/Penumbra/Interop/ResidentResources.cs +++ b/Penumbra/Interop/ResidentResources.cs @@ -101,7 +101,7 @@ namespace Penumbra.Interop var handle = ( ResourceHandle* )oldResources[ i ]; if( oldResources[ i ].ToPointer() == pResources[ i ] ) { - PluginLog.Debug( $"Unchanged resource: {ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}" ); + PluginLog.Verbose( $"Unchanged resource: {ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}" ); ( ( ResourceHandle* )oldResources[ i ] )->DecRef(); continue; } diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index c6d220a7..4a71de10 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -43,10 +43,14 @@ public class CollectionManager if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) { var resourceManager = Service< ResidentResources >.Get(); + ActiveCollection = newActive; resourceManager.ReloadResidentResources(); } + else + { + ActiveCollection = newActive; + } - ActiveCollection = newActive; return true; } @@ -170,7 +174,6 @@ public class CollectionManager collection.Delete(); Collections.Remove( name ); return true; - } private void AddCache( ModCollection collection ) From 62459c058deca7010eb06a05d0a4ebcc23ec85bb Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 8 Jan 2022 14:17:08 +0000 Subject: [PATCH 0050/2451] [CI] Updating repo.json for refs/tags/0.4.7.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 32ec5c54..8db80dbe 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.6.9", - "TestingAssemblyVersion": "0.4.6.9", + "AssemblyVersion": "0.4.7.0", + "TestingAssemblyVersion": "0.4.7.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.6.9/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6b509d66cb2fdf7c3aac99a3e53f5a99f0e96441 Mon Sep 17 00:00:00 2001 From: Muhammad Asavir Date: Sun, 9 Jan 2022 23:35:03 -0500 Subject: [PATCH 0051/2451] Update README.md Add the custom plugin repo URL to the readme to make it more obvious and deter people from installing manually. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 401d4be6..e9a35c14 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,7 @@ Contributions are welcome, but please make an issue first before writing any cod ## TexTools Mods Penumbra has support for most TexTools modpacks however this is provided on a best-effort basis and support is not guaranteed. Built in tooling will be added to Penumbra over time to avoid many common TexTools use cases. + +## Installing +While this project is still a work in progress, you can use it by including the below custom repository URL in the custom plugin repository list in XIVLauncher. Please don't install it manually in cause there is a bug to be fixed in future versions and you won't get updates automatically. +- https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json From e435ec5893f3e8107d69835143e6b7c49bd7d496 Mon Sep 17 00:00:00 2001 From: Muhammad Asavir Date: Mon, 10 Jan 2022 18:12:26 -0500 Subject: [PATCH 0052/2451] Update README.md Typo fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9a35c14..164a58e4 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,5 @@ Contributions are welcome, but please make an issue first before writing any cod Penumbra has support for most TexTools modpacks however this is provided on a best-effort basis and support is not guaranteed. Built in tooling will be added to Penumbra over time to avoid many common TexTools use cases. ## Installing -While this project is still a work in progress, you can use it by including the below custom repository URL in the custom plugin repository list in XIVLauncher. Please don't install it manually in cause there is a bug to be fixed in future versions and you won't get updates automatically. +While this project is still a work in progress, you can use it by including the below custom repository URL in the custom plugin repository list in XIVLauncher. Please don't install it manually in case there is a bug to be fixed in future versions and you won't get updates automatically. - https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json From 7f60d47c0e8281e815fe8be18cb8452161bf09b8 Mon Sep 17 00:00:00 2001 From: Franz Renatus Date: Tue, 18 Jan 2022 08:57:30 -0800 Subject: [PATCH 0053/2451] Adjust the Installing section of README.md While a bit more hand-holdy, this should cut down on the number of people asking how to add the custom repo URL and dissuade people from thing to manually install the plugin as there are multiple users who were getting confused by the prior terminology used. (Like making assumptions that adding the URL was all they needed to do) --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 164a58e4..d914afaf 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,12 @@ Contributions are welcome, but please make an issue first before writing any cod Penumbra has support for most TexTools modpacks however this is provided on a best-effort basis and support is not guaranteed. Built in tooling will be added to Penumbra over time to avoid many common TexTools use cases. ## Installing -While this project is still a work in progress, you can use it by including the below custom repository URL in the custom plugin repository list in XIVLauncher. Please don't install it manually in case there is a bug to be fixed in future versions and you won't get updates automatically. +While this project is still a work in progress, you can use it by addin the following URL to the custom plugin repositories list in your Dalamud settings +1. `/xlsettings` -> Experimental tab +2. Copy and paste the repo.json link below +3. Click on the + button +4. Click on the "Save and Close" button +5. You will now see Penumbra listed in the Dalamud Plugin Installer + +Please do not install Penumbra manually by downloading a release zip and unpacking it into your devPlugins folder. That will require manually updating Penumbra and you will miss out on features and bug fixes as you won't get update notifications automatically. Any manually installed copies of Penumbra should be removed before switching to the custom plugin respository method, as they will conflict. - https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json From e18fcafc51918d2f17fbe040df01240c70e44c6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Jan 2022 12:36:19 +0100 Subject: [PATCH 0054/2451] Add option to disable disabling sound streaming. --- Penumbra/Configuration.cs | 1 + Penumbra/Mods/ModCollectionCache.cs | 660 ++++++++++++++-------------- Penumbra/Penumbra.cs | 428 +++++++++--------- Penumbra/UI/MenuTabs/TabSettings.cs | 28 ++ 4 files changed, 584 insertions(+), 533 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 274a14fc..559ff2df 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -20,6 +20,7 @@ namespace Penumbra public bool DisableFileSystemNotifications { get; set; } + public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } public bool EnablePlayerWatch { get; set; } = false; public int WaitFrames { get; set; } = 30; diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 1b3e6c1c..4322b73d 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -12,364 +12,384 @@ using Penumbra.Mod; using Penumbra.Structs; using Penumbra.Util; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +// The ModCollectionCache contains all required temporary data to use a collection. +// It will only be setup if a collection gets activated in any way. +public class ModCollectionCache { - // The ModCollectionCache contains all required temporary data to use a collection. - // It will only be setup if a collection gets activated in any way. - public class ModCollectionCache + // Shared caches to avoid allocations. + private static readonly BitArray FileSeen = new(256); + private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256); + + public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); + + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); + public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly HashSet< ulong > Checksums = new(); + public readonly MetaManager MetaManipulations; + + public IReadOnlyDictionary< string, object? > ChangedItems { - // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new( 256 ); - private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new( 256 ); - - public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); - public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly HashSet< ulong > Checksums = new(); - public readonly MetaManager MetaManipulations; - - public IReadOnlyDictionary< string, object? > ChangedItems + get { - get + SetChangedItems(); + return _changedItems; + } + } + + public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) + => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); + + private static void ResetFileSeen( int size ) + { + if( size < FileSeen.Length ) + { + FileSeen.Length = size; + FileSeen.SetAll( false ); + } + else + { + FileSeen.SetAll( false ); + FileSeen.Length = size; + } + } + + public void CalculateEffectiveFileList() + { + ResolvedFiles.Clear(); + SwappedFiles.Clear(); + MissingFiles.Clear(); + RegisteredFiles.Clear(); + _changedItems.Clear(); + + foreach( var mod in AvailableMods.Values + .Where( m => m.Settings.Enabled ) + .OrderByDescending( m => m.Settings.Priority ) ) + { + mod.Cache.ClearFileConflicts(); + AddFiles( mod ); + AddSwaps( mod ); + } + + AddMetaFiles(); + Checksums.Clear(); + foreach( var file in ResolvedFiles ) + { + Checksums.Add( file.Value.Crc64 ); + } + } + + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip meta files because IMCs would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet(); + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) { - SetChangedItems(); - return _changedItems; + identifier.Identify( _changedItems, resolved ); + } + + foreach( var swapped in SwappedFiles.Keys ) + { + identifier.Identify( _changedItems, swapped ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + + + private void AddFiles( Mod.Mod mod ) + { + ResetFileSeen( mod.Data.Resources.ModFiles.Count ); + // Iterate in reverse so that later groups take precedence before earlier ones. + foreach( var group in mod.Data.Meta.Groups.Values.Reverse() ) + { + switch( group.SelectionType ) + { + case SelectType.Single: + AddFilesForSingle( group, mod ); + break; + case SelectType.Multi: + AddFilesForMulti( group, mod ); + break; + default: throw new InvalidEnumArgumentException(); } } - public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) - => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); + AddRemainingFiles( mod ); + } - private static void ResetFileSeen( int size ) + private bool FilterFile( GamePath gamePath ) + { + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + if( !Penumbra.Config.DisableSoundStreaming + && gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) ) { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } + return true; } - public void CalculateEffectiveFileList() + return false; + } + + + private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) + { + if( FilterFile( gamePath ) ) { - ResolvedFiles.Clear(); - SwappedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); - _changedItems.Clear(); - - foreach( var mod in AvailableMods.Values - .Where( m => m.Settings.Enabled ) - .OrderByDescending( m => m.Settings.Priority ) ) - { - mod.Cache.ClearFileConflicts(); - AddFiles( mod ); - AddSwaps( mod ); - } - - AddMetaFiles(); - Checksums.Clear(); - foreach( var file in ResolvedFiles ) - Checksums.Add( file.Value.Crc64 ); + return; } - private void SetChangedItems() + if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) { - if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) + RegisteredFiles.Add( gamePath, mod ); + ResolvedFiles[ gamePath ] = file; + } + else + { + mod.Cache.AddConflict( oldMod, gamePath ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) { + oldMod.Cache.AddConflict( mod, gamePath ); + } + } + } + + private void AddMissingFile( FullPath file ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": return; + default: + MissingFiles.Add( file ); + return; + } + } + + private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled ) + { + foreach( var (file, paths) in option.OptionFiles ) + { + var fullPath = new FullPath( mod.Data.BasePath, file ); + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + if( idx < 0 ) + { + AddMissingFile( fullPath ); + continue; } - try + var registeredFile = mod.Data.Resources.ModFiles[ idx ]; + if( !registeredFile.Exists ) { - // Skip meta files because IMCs would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet(); - var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) - { - identifier.Identify( _changedItems, resolved ); - } - - foreach( var swapped in SwappedFiles.Keys ) - { - identifier.Identify( _changedItems, swapped ); - } + AddMissingFile( registeredFile ); + continue; } - catch( Exception e ) + + FileSeen.Set( idx, true ); + if( enabled ) { - PluginLog.Error( $"Unknown Error:\n{e}" ); + foreach( var path in paths ) + { + AddFile( mod, path, registeredFile ); + } } } + } + private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod ) + { + Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - private void AddFiles( Mod.Mod mod ) + if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) { - ResetFileSeen( mod.Data.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - foreach( var group in mod.Data.Meta.Groups.Values.Reverse() ) - { - switch( group.SelectionType ) - { - case SelectType.Single: - AddFilesForSingle( group, mod ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod ); + setting = 0; } - private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) + for( var i = 0; i < singleGroup.Options.Count; ++i ) { - if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) + AddPathsForOption( singleGroup.Options[ i ], mod, setting == i ); + } + } + + private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod ) + { + Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); + + if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) + { + return; + } + + // Also iterate options in reverse so that later options take precedence before earlier ones. + for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) + { + AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 ); + } + } + + private void AddRemainingFiles( Mod.Mod mod ) + { + for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i ) + { + if( FileSeen.Get( i ) ) { - RegisteredFiles.Add( gamePath, mod ); - ResolvedFiles[ gamePath ] = file; + continue; + } + + var file = mod.Data.Resources.ModFiles[ i ]; + if( file.Exists ) + { + AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); } else { - mod.Cache.AddConflict( oldMod, gamePath ); + MissingFiles.Add( file ); + } + } + } + + private void AddMetaFiles() + { + foreach( var (gamePath, file) in MetaManipulations.Files ) + { + if( RegisteredFiles.TryGetValue( gamePath, out var mod ) ) + { + PluginLog.Warning( + $"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." ); + } + + ResolvedFiles[ gamePath ] = file; + } + } + + private void AddSwaps( Mod.Mod mod ) + { + foreach( var (key, value) in mod.Data.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + { + if( !RegisteredFiles.TryGetValue( key, out var oldMod ) ) + { + RegisteredFiles.Add( key, mod ); + SwappedFiles.Add( key, value ); + } + else + { + mod.Cache.AddConflict( oldMod, key ); if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) { - oldMod.Cache.AddConflict( mod, gamePath ); + oldMod.Cache.AddConflict( mod, key ); } } } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - var fullPath = new FullPath(mod.Data.BasePath, file); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals(fullPath) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Data.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) - { - AddFile( mod, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - - if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - - if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( Mod.Mod mod ) - { - for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Data.Resources.ModFiles[ i ]; - if( file.Exists ) - { - AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); - } - else - { - MissingFiles.Add( file ); - } - } - } - - private void AddMetaFiles() - { - foreach( var (gamePath, file) in MetaManipulations.Files ) - { - if( RegisteredFiles.TryGetValue( gamePath, out var mod ) ) - { - PluginLog.Warning( - $"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." ); - } - - ResolvedFiles[ gamePath ] = file; - } - } - - private void AddSwaps( Mod.Mod mod ) - { - foreach( var swap in mod.Data.Meta.FileSwaps ) - { - if( !RegisteredFiles.TryGetValue( swap.Key, out var oldMod ) ) - { - RegisteredFiles.Add( swap.Key, mod ); - SwappedFiles.Add( swap.Key, swap.Value ); - } - else - { - mod.Cache.AddConflict( oldMod, swap.Key ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, swap.Key ); - } - } - } - } - - private void AddManipulations( Mod.Mod mod ) - { - foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) - { - if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - else - { - mod.Cache.AddConflict( oldMod, manip ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, manip ); - } - } - } - } - - public void UpdateMetaManipulations() - { - MetaManipulations.Reset( false ); - - foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) - { - mod.Cache.ClearMetaConflicts(); - AddManipulations( mod ); - } - - MetaManipulations.WriteNewFiles(); - } - - public void RemoveMod( DirectoryInfo basePath ) - { - if( AvailableMods.TryGetValue( basePath.Name, out var mod ) ) - { - AvailableMods.Remove( basePath.Name ); - if( mod.Settings.Enabled ) - { - CalculateEffectiveFileList(); - if( mod.Data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - } - } - - private class PriorityComparer : IComparer< Mod.Mod > - { - public int Compare( Mod.Mod? x, Mod.Mod? y ) - => ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 ); - } - - private static readonly PriorityComparer Comparer = new(); - - public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) - { - if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) ) - { - var newMod = new Mod.Mod( settings, data ); - AvailableMods[ data.BasePath.Name ] = newMod; - - if( updateFileList && settings.Enabled ) - { - CalculateEffectiveFileList(); - if( data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - } - } - - public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.FullName.Length >= 260 || !candidate.Exists ) - { - return null; - } - - return candidate; - } - - public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) - => SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; - - public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null; } + + private void AddManipulations( Mod.Mod mod ) + { + foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) + { + if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + } + else + { + mod.Cache.AddConflict( oldMod, manip ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) + { + oldMod.Cache.AddConflict( mod, manip ); + } + } + } + } + + public void UpdateMetaManipulations() + { + MetaManipulations.Reset( false ); + + foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) + { + mod.Cache.ClearMetaConflicts(); + AddManipulations( mod ); + } + + MetaManipulations.WriteNewFiles(); + } + + public void RemoveMod( DirectoryInfo basePath ) + { + if( AvailableMods.TryGetValue( basePath.Name, out var mod ) ) + { + AvailableMods.Remove( basePath.Name ); + if( mod.Settings.Enabled ) + { + CalculateEffectiveFileList(); + if( mod.Data.Resources.MetaManipulations.Count > 0 ) + { + UpdateMetaManipulations(); + } + } + } + } + + private class PriorityComparer : IComparer< Mod.Mod > + { + public int Compare( Mod.Mod? x, Mod.Mod? y ) + => ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 ); + } + + private static readonly PriorityComparer Comparer = new(); + + public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) + { + if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) ) + { + var newMod = new Mod.Mod( settings, data ); + AvailableMods[ data.BasePath.Name ] = newMod; + + if( updateFileList && settings.Enabled ) + { + CalculateEffectiveFileList(); + if( data.Resources.MetaManipulations.Count > 0 ) + { + UpdateMetaManipulations(); + } + } + } + } + + public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.FullName.Length >= 260 || !candidate.Exists ) + { + return null; + } + + return candidate; + } + + public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) + => SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; + + public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) + => GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null; } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index baa5f368..4f9e7715 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -14,244 +14,246 @@ using Penumbra.PlayerWatch; using Penumbra.UI; using Penumbra.Util; -namespace Penumbra +namespace Penumbra; + +public class Penumbra : IDalamudPlugin { - public class Penumbra : IDalamudPlugin + public string Name { get; } = "Penumbra"; + public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build"; + + private const string CommandName = "/penumbra"; + + public static Configuration Config { get; private set; } = null!; + public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; + + public ResourceLoader ResourceLoader { get; } + public SettingsInterface SettingsInterface { get; } + public MusicManager MusicManager { get; } + public ObjectReloader ObjectReloader { get; } + + public PenumbraApi Api { get; } + public PenumbraIpc Ipc { get; } + + private WebServer? _webServer; + + public Penumbra( DalamudPluginInterface pluginInterface ) { - public string Name { get; } = "Penumbra"; - public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build"; + FFXIVClientStructs.Resolver.Initialize(); + Dalamud.Initialize( pluginInterface ); + GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + Config = Configuration.Load(); - private const string CommandName = "/penumbra"; - - public static Configuration Config { get; private set; } = null!; - public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; - - public ResourceLoader ResourceLoader { get; } - public SettingsInterface SettingsInterface { get; } - public MusicManager MusicManager { get; } - public ObjectReloader ObjectReloader { get; } - - public PenumbraApi Api { get; } - public PenumbraIpc Ipc { get; } - - private WebServer? _webServer; - - public Penumbra( DalamudPluginInterface pluginInterface ) + MusicManager = new MusicManager(); + if( Config.DisableSoundStreaming ) { - FFXIVClientStructs.Resolver.Initialize(); - Dalamud.Initialize( pluginInterface ); - GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); - Config = Configuration.Load(); - - MusicManager = new MusicManager(); MusicManager.DisableStreaming(); - - var gameUtils = Service< ResidentResources >.Set(); - PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); - Service< MetaDefaults >.Set(); - var modManager = Service< ModManager >.Set(); - - modManager.DiscoverMods(); - - ObjectReloader = new ObjectReloader( modManager, Config.WaitFrames ); - - ResourceLoader = new ResourceLoader( this ); - - Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) - { - HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", - } ); - - ResourceLoader.Init(); - ResourceLoader.Enable(); - - gameUtils.ReloadResidentResources(); - - SettingsInterface = new SettingsInterface( this ); - - if( Config.EnableHttpApi ) - { - CreateWebServer(); - } - - if( !Config.EnablePlayerWatch || !Config.IsEnabled ) - { - PlayerWatcher.Disable(); - } - - PlayerWatcher.PlayerChanged += p => - { - PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); - ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); - }; - - Api = new PenumbraApi( this ); - SubscribeItemLinks(); - Ipc = new PenumbraIpc( pluginInterface, Api ); } - public bool Enable() + var gameUtils = Service< ResidentResources >.Set(); + PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); + Service< MetaDefaults >.Set(); + var modManager = Service< ModManager >.Set(); + + modManager.DiscoverMods(); + + ObjectReloader = new ObjectReloader( modManager, Config.WaitFrames ); + + ResourceLoader = new ResourceLoader( this ); + + Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { - if( Config.IsEnabled ) - { - return false; - } + HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", + } ); - Config.IsEnabled = true; - Service< ResidentResources >.Get().ReloadResidentResources(); - if( Config.EnablePlayerWatch ) - { - PlayerWatcher.SetStatus( true ); - } + ResourceLoader.Init(); + ResourceLoader.Enable(); - Config.Save(); - ObjectReloader.RedrawAll( RedrawType.WithSettings ); - return true; + gameUtils.ReloadResidentResources(); + + SettingsInterface = new SettingsInterface( this ); + + if( Config.EnableHttpApi ) + { + CreateWebServer(); } - public bool Disable() + if( !Config.EnablePlayerWatch || !Config.IsEnabled ) { - if( !Config.IsEnabled ) - { - return false; - } - - Config.IsEnabled = false; - Service< ResidentResources >.Get().ReloadResidentResources(); - if( Config.EnablePlayerWatch ) - { - PlayerWatcher.SetStatus( false ); - } - - Config.Save(); - ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); - return true; + PlayerWatcher.Disable(); } - public bool SetEnabled( bool enabled ) - => enabled ? Enable() : Disable(); - - private void SubscribeItemLinks() + PlayerWatcher.PlayerChanged += p => { - Api.ChangedItemTooltip += it => + PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); + ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); + }; + + Api = new PenumbraApi( this ); + SubscribeItemLinks(); + Ipc = new PenumbraIpc( pluginInterface, Api ); + } + + public bool Enable() + { + if( Config.IsEnabled ) + { + return false; + } + + Config.IsEnabled = true; + Service< ResidentResources >.Get().ReloadResidentResources(); + if( Config.EnablePlayerWatch ) + { + PlayerWatcher.SetStatus( true ); + } + + Config.Save(); + ObjectReloader.RedrawAll( RedrawType.WithSettings ); + return true; + } + + public bool Disable() + { + if( !Config.IsEnabled ) + { + return false; + } + + Config.IsEnabled = false; + Service< ResidentResources >.Get().ReloadResidentResources(); + if( Config.EnablePlayerWatch ) + { + PlayerWatcher.SetStatus( false ); + } + + Config.Save(); + ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); + return true; + } + + public bool SetEnabled( bool enabled ) + => enabled ? Enable() : Disable(); + + private void SubscribeItemLinks() + { + Api.ChangedItemTooltip += it => + { + if( it is Item ) { - if( it is Item ) + ImGui.Text( "Left Click to create an item link in chat." ); + } + }; + Api.ChangedItemClicked += ( button, it ) => + { + if( button == MouseButton.Left && it is Item item ) + { + ChatUtil.LinkItem( item ); + } + }; + } + + public void CreateWebServer() + { + const string prefix = "http://localhost:42069/"; + + ShutdownWebServer(); + + _webServer = new WebServer( o => o + .WithUrlPrefix( prefix ) + .WithMode( HttpListenerMode.EmbedIO ) ) + .WithCors( prefix ) + .WithWebApi( "/api", m => m + .WithController( () => new ModsController( this ) ) ); + + _webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); + + _webServer.RunAsync(); + } + + public void ShutdownWebServer() + { + _webServer?.Dispose(); + _webServer = null; + } + + public void Dispose() + { + Ipc.Dispose(); + Api.Dispose(); + SettingsInterface.Dispose(); + ObjectReloader.Dispose(); + PlayerWatcher.Dispose(); + + Dalamud.Commands.RemoveHandler( CommandName ); + + ResourceLoader.Dispose(); + + ShutdownWebServer(); + } + + private void OnCommand( string command, string rawArgs ) + { + const string modsEnabled = "Your mods have now been enabled."; + const string modsDisabled = "Your mods have now been disabled."; + + var args = rawArgs.Split( new[] { ' ' }, 2 ); + if( args.Length > 0 && args[ 0 ].Length > 0 ) + { + switch( args[ 0 ] ) + { + case "reload": { - ImGui.Text( "Left Click to create an item link in chat." ); + Service< ModManager >.Get().DiscoverMods(); + Dalamud.Chat.Print( + $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods." + ); + break; } - }; - Api.ChangedItemClicked += ( button, it ) => - { - if( button == MouseButton.Left && it is Item item ) + case "redraw": { - ChatUtil.LinkItem( item ); + if( args.Length > 1 ) + { + ObjectReloader.RedrawObject( args[ 1 ] ); + } + else + { + ObjectReloader.RedrawAll(); + } + + break; } - }; - } - - public void CreateWebServer() - { - const string prefix = "http://localhost:42069/"; - - ShutdownWebServer(); - - _webServer = new WebServer( o => o - .WithUrlPrefix( prefix ) - .WithMode( HttpListenerMode.EmbedIO ) ) - .WithCors( prefix ) - .WithWebApi( "/api", m => m - .WithController( () => new ModsController( this ) ) ); - - _webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); - - _webServer.RunAsync(); - } - - public void ShutdownWebServer() - { - _webServer?.Dispose(); - _webServer = null; - } - - public void Dispose() - { - Ipc.Dispose(); - Api.Dispose(); - SettingsInterface.Dispose(); - ObjectReloader.Dispose(); - PlayerWatcher.Dispose(); - - Dalamud.Commands.RemoveHandler( CommandName ); - - ResourceLoader.Dispose(); - - ShutdownWebServer(); - } - - private void OnCommand( string command, string rawArgs ) - { - const string modsEnabled = "Your mods have now been enabled."; - const string modsDisabled = "Your mods have now been disabled."; - - var args = rawArgs.Split( new[] { ' ' }, 2 ); - if( args.Length > 0 && args[ 0 ].Length > 0 ) - { - switch( args[ 0 ] ) + case "debug": { - case "reload": - { - Service< ModManager >.Get().DiscoverMods(); - Dalamud.Chat.Print( - $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods." - ); - break; - } - case "redraw": - { - if( args.Length > 1 ) - { - ObjectReloader.RedrawObject( args[ 1 ] ); - } - else - { - ObjectReloader.RedrawAll(); - } - - break; - } - case "debug": - { - SettingsInterface.MakeDebugTabVisible(); - break; - } - case "enable": - { - Dalamud.Chat.Print( Enable() - ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" - : modsEnabled ); - break; - } - case "disable": - { - Dalamud.Chat.Print( Disable() - ? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" - : modsDisabled ); - break; - } - case "toggle": - { - SetEnabled( !Config.IsEnabled ); - Dalamud.Chat.Print( Config.IsEnabled - ? modsEnabled - : modsDisabled ); - break; - } + SettingsInterface.MakeDebugTabVisible(); + break; + } + case "enable": + { + Dalamud.Chat.Print( Enable() + ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" + : modsEnabled ); + break; + } + case "disable": + { + Dalamud.Chat.Print( Disable() + ? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" + : modsDisabled ); + break; + } + case "toggle": + { + SetEnabled( !Config.IsEnabled ); + Dalamud.Chat.Print( Config.IsEnabled + ? modsEnabled + : modsDisabled ); + break; } - - return; } - SettingsInterface.FlipVisibility(); + return; } + + SettingsInterface.FlipVisibility(); } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 64e3f126..b82c14c6 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -194,6 +194,33 @@ public partial class SettingsInterface "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); } + private void DrawDisableSoundStreamingBox() + { + var tmp = Penumbra.Config.DisableSoundStreaming; + if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) + { + Penumbra.Config.DisableSoundStreaming = tmp; + _configChanged = true; + if( tmp ) + { + _base._penumbra.MusicManager.DisableStreaming(); + } + else + { + _base._penumbra.MusicManager.EnableStreaming(); + } + + _base.ReloadMods(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Disable streaming in the games audio engine.\n" + + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems.\n" + + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." ); + } + private void DrawLogLoadedFilesBox() { ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); @@ -306,6 +333,7 @@ public partial class SettingsInterface private void DrawAdvancedSettings() { DrawTempFolder(); + DrawDisableSoundStreamingBox(); DrawLogLoadedFilesBox(); DrawDisableNotificationsBox(); DrawEnableHttpApiBox(); From 076be3925cb62e7792c461546d1bb4470d113058 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Jan 2022 11:38:53 +0000 Subject: [PATCH 0055/2451] [CI] Updating repo.json for refs/tags/0.4.7.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8db80dbe..c962327e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.0", - "TestingAssemblyVersion": "0.4.7.0", + "AssemblyVersion": "0.4.7.1", + "TestingAssemblyVersion": "0.4.7.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 75823d413e66870cb76ac901384e10480766d014 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Jan 2022 19:13:05 +0100 Subject: [PATCH 0056/2451] Allow changing skin materials in .mdls in edit mode. --- Penumbra/Importer/TexToolsMeta.cs | 12 +-- .../TabInstalled/TabInstalledModPanel.cs | 38 ++++++++++ Penumbra/Util/ModelChanger.cs | 74 +++++++++++++++++++ 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Util/ModelChanger.cs diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 18b5e5a8..e801a477 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -39,8 +39,8 @@ namespace Penumbra.Importer private const string Ext = @"\.meta"; // These are the valid regexes for .meta files that we are able to support at the moment. - private static readonly Regex HousingMeta = new( $"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}" ); - private static readonly Regex CharaMeta = new( $"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}" ); + private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}"); + private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}"); public readonly ObjectType PrimaryType; public readonly BodySlot SecondaryType; @@ -129,7 +129,7 @@ namespace Penumbra.Importer } if( match.Groups[ "SecondaryType" ].Success - && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) + && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) { SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); } @@ -234,8 +234,8 @@ namespace Penumbra.Importer var id = reader.ReadUInt16(); var value = reader.ReadUInt16(); if( !gr.IsValid() - || info.PrimaryType == ObjectType.Character && info.SecondaryType != BodySlot.Face && info.SecondaryType != BodySlot.Hair - || info.PrimaryType == ObjectType.Equipment && info.EquipSlot != EquipSlot.Head && info.EquipSlot != EquipSlot.Body ) + || info.PrimaryType == ObjectType.Character && info.SecondaryType != BodySlot.Face && info.SecondaryType != BodySlot.Hair + || info.PrimaryType == ObjectType.Equipment && info.EquipSlot != EquipSlot.Head && info.EquipSlot != EquipSlot.Body ) { continue; } @@ -323,7 +323,7 @@ namespace Penumbra.Importer Version = version; } - public static TexToolsMeta Invalid = new( string.Empty, 0 ); + public static TexToolsMeta Invalid = new(string.Empty, 0); public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) { diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index e2e819bf..f0b2dbe3 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -499,6 +499,42 @@ namespace Penumbra.UI + "Experimental - Use at own risk!" ); } + private void DrawMaterialChangeButtons() + { + if( ImGui.Button( "Skin Material B to D" ) ) + { + ModelChanger.ChangeMtrlBToD( Mod!.Data ); + } + ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from B to D.\n" + + "This is usually to convert Bibo+ models to T&F3 skins.\n" + + "This overwrites .mdl files, use at your own risk!" ); + + ImGui.SameLine(); + if( ImGui.Button( "Skin Material D to B" ) ) + { + ModelChanger.ChangeMtrlDToB( Mod!.Data ); + } + ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from D to B.\n" + + "This is usually to convert T&F3 models to Bibo+ skins.\n" + + "This overwrites .mdl files, use at your own risk!" ); + + ImGui.SameLine(); + if( ImGui.Button( "Skin Material A to E" ) ) + { + ModelChanger.ChangeMtrlAToE( Mod!.Data ); + } + ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from A to E.\n" + + "This overwrites .mdl files, use at your own risk!" ); + + ImGui.SameLine(); + if( ImGui.Button( "Skin Material E to A" ) ) + { + ModelChanger.ChangeMtrlEToA( Mod!.Data ); + } + ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from E to A.\n" + + "This overwrites .mdl files, use at your own risk!" ); + } + private void DrawEditLine() { DrawOpenModFolderButton(); @@ -519,6 +555,8 @@ namespace Penumbra.UI ImGui.SameLine(); DrawSplitButton(); + DrawMaterialChangeButtons(); + DrawSortOrder( Mod!.Data, _modManager, _selector ); } diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs new file mode 100644 index 00000000..70c88354 --- /dev/null +++ b/Penumbra/Util/ModelChanger.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Logging; +using Penumbra.Mod; + +namespace Penumbra.Util; + +public static class ModelChanger +{ + private const string SkinMaterialString = "/mt_c0201b0001_d.mtrl"; + private static readonly byte[] SkinMaterial = Encoding.UTF8.GetBytes( SkinMaterialString ); + + public static int ChangeMtrl( FullPath file, byte from, byte to ) + { + if( !file.Exists ) + { + return 0; + } + + try + { + var text = File.ReadAllBytes( file.FullName ); + var replaced = 0; + + var length = text.Length - SkinMaterial.Length; + SkinMaterial[ 15 ] = from; + for( var i = 0; i < length; ++i ) + { + if( SkinMaterial.Where( ( t, j ) => text[ i + j ] != t ).Any() ) + { + continue; + } + + text[ i + 15 ] = to; + i += SkinMaterial.Length; + ++replaced; + } + + if( replaced == 0 ) + { + return 0; + } + + File.WriteAllBytes( file.FullName, text ); + return replaced; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write .mdl data for file {file.FullName}, replacing {( char )from} with {( char )to}:\n{e}" ); + return -1; + } + } + + public static bool ChangeModMaterials( ModData mod, byte from, byte to ) + { + return mod.Resources.ModFiles + .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) + .All( file => ChangeMtrl( file, from, to ) >= 0 ); + } + + public static bool ChangeMtrlBToD( ModData mod ) + => ChangeModMaterials( mod, ( byte )'b', ( byte )'d' ); + + public static bool ChangeMtrlDToB( ModData mod ) + => ChangeModMaterials( mod, ( byte )'d', ( byte )'b' ); + + public static bool ChangeMtrlEToA( ModData mod ) + => ChangeModMaterials( mod, ( byte )'e', ( byte )'a' ); + + public static bool ChangeMtrlAToE( ModData mod ) + => ChangeModMaterials( mod, ( byte )'a', ( byte )'e' ); +} \ No newline at end of file From 40bb7567dd0530875f49ced0faa7a0532282e604 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Jan 2022 18:15:24 +0000 Subject: [PATCH 0057/2451] [CI] Updating repo.json for refs/tags/0.4.7.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c962327e..aaeef754 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.1", - "TestingAssemblyVersion": "0.4.7.1", + "AssemblyVersion": "0.4.7.2", + "TestingAssemblyVersion": "0.4.7.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b6817c47edf95e5f1df5f173f808ef373f71382a Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 31 Jan 2022 22:08:33 +0100 Subject: [PATCH 0058/2451] Replace manage mods button with title screen menu --- Penumbra/Dalamud.cs | 66 ++++++++++++++-------------- Penumbra/Penumbra.csproj | 10 ++++- Penumbra/UI/LaunchButton.cs | 59 ++++++++++--------------- Penumbra/UI/MenuTabs/TabSettings.cs | 16 ------- Penumbra/UI/SettingsInterface.cs | 2 +- Penumbra/tsmLogo.png | Bin 0 -> 7563 bytes 6 files changed, 65 insertions(+), 88 deletions(-) create mode 100644 Penumbra/tsmLogo.png diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index eea4d05a..d914765e 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -1,32 +1,34 @@ -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.IoC; -using Dalamud.Plugin; -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - -namespace Penumbra -{ - public class Dalamud - { - public static void Initialize(DalamudPluginInterface pluginInterface) - => pluginInterface.Create(); - - // @formatter:off - [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; - // @formatter:on - } -} +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Interface; +using Dalamud.IoC; +using Dalamud.Plugin; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Penumbra +{ + public class Dalamud + { + public static void Initialize(DalamudPluginInterface pluginInterface) + => pluginInterface.Create(); + + // @formatter:off + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + // @formatter:on + } +} \ No newline at end of file diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 7b6e54b8..347b9ddc 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -29,6 +29,12 @@ $(MSBuildWarningsAsMessages);MSB3277 + + + PreserveNewest + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll @@ -62,7 +68,7 @@ - + @@ -72,4 +78,4 @@ Always - + \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 18ca07bb..1540fd55 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,59 +1,44 @@ +using System; +using System.IO; using Dalamud.Interface; using ImGuiNET; +using ImGuiScene; using Penumbra.UI.Custom; namespace Penumbra.UI; public partial class SettingsInterface { - private class ManageModsButton + private class ManageModsButton : IDisposable { // magic numbers - private const int Width = 200; - private const int Height = 45; - private const string MenuButtonsName = "Penumbra Menu Buttons"; private const string MenuButtonLabel = "Manage Mods"; - private const ImGuiWindowFlags ButtonFlags = - ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoBackground - | ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoScrollbar - | ImGuiWindowFlags.NoResize - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoSavedSettings; - - private readonly SettingsInterface _base; + private readonly SettingsInterface _base; + private readonly TextureWrap _icon; + private readonly TitleScreenMenu.TitleScreenMenuEntry _entry; public ManageModsButton( SettingsInterface ui ) - => _base = ui; - - internal bool ForceDraw = false; - - public void Draw() { - if( !ForceDraw && ( Dalamud.Conditions.Any() || _base._menu.Visible ) ) - { - return; - } + _base = ui; - using var color = ImGuiRaii.PushColor( ImGuiCol.Button, 0xFF0000C8, ForceDraw ); + _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + "tsmLogo.png" ) ); + if( _icon == null ) + throw new Exception( "Could not load title screen icon." ); - var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; - ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); + _entry = Dalamud.TitleScreenMenu.AddEntry( MenuButtonLabel, _icon, OnTriggered ); + } - var windowSize = ImGuiHelpers.ScaledVector2( Width, Height ); + private void OnTriggered() + { + _base.FlipVisibility(); + } - ImGui.SetNextWindowPos( ss - windowSize - Penumbra.Config.ManageModsButtonOffset * ImGuiHelpers.GlobalScale, ImGuiCond.Always ); - - if( ImGui.Begin( MenuButtonsName, ButtonFlags ) - && ImGui.Button( MenuButtonLabel, windowSize ) ) - { - _base.FlipVisibility(); - } - - ImGui.End(); + public void Dispose() + { + _icon.Dispose(); + Dalamud.TitleScreenMenu.RemoveEntry( _entry ); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index b82c14c6..50da80e2 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -150,21 +150,6 @@ public partial class SettingsInterface + "This is required to enable manually editing any mod information." ); } - private void DrawManageModsButtonOffsetButton() - { - var manageModsButtonOffset = _config.ManageModsButtonOffset; - ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragFloat2( "\"Manage Mods\"-Button Offset", ref manageModsButtonOffset, 1f ) ) - { - _config.ManageModsButtonOffset = manageModsButtonOffset; - _configChanged = true; - } - - _base._manageModsButton.ForceDraw = ImGui.IsItemActive(); - ImGuiComponents.HelpMarker( - "Shift the \"Manage Mods\"-Button displayed in the login-lobby by the given amount of pixels in X/Y-direction." ); - } - private void DrawSortFoldersFirstBox() { var foldersFirst = _config.SortFoldersFirst; @@ -360,7 +345,6 @@ public partial class SettingsInterface ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawScaleModSelectorBox(); DrawSortFoldersFirstBox(); - DrawManageModsButtonOffsetButton(); DrawShowAdvancedBox(); if( _config.ShowAdvanced ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 46ace4b4..3a21ee22 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -34,6 +34,7 @@ namespace Penumbra.UI public void Dispose() { + _manageModsButton.Dispose(); _menu.InstalledTab.Selector.Cache.Dispose(); Dalamud.PluginInterface.UiBuilder.Draw -= Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfig; @@ -51,7 +52,6 @@ namespace Penumbra.UI public void Draw() { _menuBar.Draw(); - _manageModsButton.Draw(); _menu.Draw(); } diff --git a/Penumbra/tsmLogo.png b/Penumbra/tsmLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..caae3825ef505c8760025d2cbb1c172c3176039b GIT binary patch literal 7563 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 literal 0 HcmV?d00001 From 685772e6acd85fb908f8a025550482c25ab895f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 31 Jan 2022 22:38:28 +0100 Subject: [PATCH 0059/2451] Slight modifications. --- Penumbra/Configuration.cs | 1 - Penumbra/Penumbra.cs | 8 ++++---- Penumbra/UI/LaunchButton.cs | 26 ++++++++++++-------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 559ff2df..9c6142a8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -39,7 +39,6 @@ namespace Penumbra public Dictionary< string, string > ModSortOrder { get; set; } = new(); public bool InvertModListOrder { internal get; set; } - public Vector2 ManageModsButtonOffset { get; set; } = 50 * Vector2.One; public static Configuration Load() { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4f9e7715..7b9c9bb5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -70,6 +70,10 @@ public class Penumbra : IDalamudPlugin gameUtils.ReloadResidentResources(); + Api = new PenumbraApi( this ); + Ipc = new PenumbraIpc( pluginInterface, Api ); + SubscribeItemLinks(); + SettingsInterface = new SettingsInterface( this ); if( Config.EnableHttpApi ) @@ -87,10 +91,6 @@ public class Penumbra : IDalamudPlugin PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); }; - - Api = new PenumbraApi( this ); - SubscribeItemLinks(); - Ipc = new PenumbraIpc( pluginInterface, Api ); } public bool Enable() diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 1540fd55..f78c9e89 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,9 +1,7 @@ using System; using System.IO; using Dalamud.Interface; -using ImGuiNET; using ImGuiScene; -using Penumbra.UI.Custom; namespace Penumbra.UI; @@ -11,12 +9,9 @@ public partial class SettingsInterface { private class ManageModsButton : IDisposable { - // magic numbers - private const string MenuButtonLabel = "Manage Mods"; - - private readonly SettingsInterface _base; - private readonly TextureWrap _icon; - private readonly TitleScreenMenu.TitleScreenMenuEntry _entry; + private readonly SettingsInterface _base; + private readonly TextureWrap? _icon; + private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; public ManageModsButton( SettingsInterface ui ) { @@ -24,10 +19,10 @@ public partial class SettingsInterface _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, "tsmLogo.png" ) ); - if( _icon == null ) - throw new Exception( "Could not load title screen icon." ); - - _entry = Dalamud.TitleScreenMenu.AddEntry( MenuButtonLabel, _icon, OnTriggered ); + if( _icon != null ) + { + _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); + } } private void OnTriggered() @@ -37,8 +32,11 @@ public partial class SettingsInterface public void Dispose() { - _icon.Dispose(); - Dalamud.TitleScreenMenu.RemoveEntry( _entry ); + _icon?.Dispose(); + if( _entry != null ) + { + Dalamud.TitleScreenMenu.RemoveEntry( _entry ); + } } } } \ No newline at end of file From 1d5ddb0590989636b561b663aad46df4b3e1047a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 31 Jan 2022 21:40:38 +0000 Subject: [PATCH 0060/2451] [CI] Updating repo.json for refs/tags/0.4.7.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index aaeef754..2fb78234 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.2", - "TestingAssemblyVersion": "0.4.7.2", + "AssemblyVersion": "0.4.7.3", + "TestingAssemblyVersion": "0.4.7.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a62fa06b03041703ba6727b9b4ced01f935af983 Mon Sep 17 00:00:00 2001 From: Lucy Awrey <35198881+lucyawrey@users.noreply.github.com> Date: Thu, 3 Feb 2022 17:02:03 -0500 Subject: [PATCH 0061/2451] Implemented command for changing the current default and forced collections --- Penumbra/Penumbra.cs | 66 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 7b9c9bb5..563bbee7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -13,6 +13,7 @@ using Penumbra.Mods; using Penumbra.PlayerWatch; using Penumbra.UI; using Penumbra.Util; +using System.Linq; namespace Penumbra; @@ -36,6 +37,9 @@ public class Penumbra : IDalamudPlugin private WebServer? _webServer; + private readonly ModManager _modManager; + private readonly ModCollection[] _collections; + public Penumbra( DalamudPluginInterface pluginInterface ) { FFXIVClientStructs.Resolver.Initialize(); @@ -52,11 +56,11 @@ public class Penumbra : IDalamudPlugin var gameUtils = Service< ResidentResources >.Set(); PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); Service< MetaDefaults >.Set(); - var modManager = Service< ModManager >.Set(); + _modManager = Service< ModManager >.Set(); - modManager.DiscoverMods(); + _modManager.DiscoverMods(); - ObjectReloader = new ObjectReloader( modManager, Config.WaitFrames ); + ObjectReloader = new ObjectReloader( _modManager, Config.WaitFrames ); ResourceLoader = new ResourceLoader( this ); @@ -91,6 +95,8 @@ public class Penumbra : IDalamudPlugin PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); }; + + _collections = _modManager.Collections.Collections.Values.ToArray(); } public bool Enable() @@ -191,12 +197,53 @@ public class Penumbra : IDalamudPlugin ShutdownWebServer(); } + public bool SetCollection(string type, string collection) + { + type = type.ToLower(); + collection = collection.ToLower(); + + if( type != "default" && type != "forced" ) + { + Dalamud.Chat.Print( "Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} " ); + return false; + } + + var currentCollection = ( type == "default" ) ? _modManager.Collections.DefaultCollection : _modManager.Collections.ForcedCollection; + + if( collection == currentCollection.Name.ToLower() ) + { + Dalamud.Chat.Print( $"{currentCollection.Name} is already the active collection." ); + return false; + } + + var newCollection = ( collection == "none" ) ? ModCollection.Empty : _collections.FirstOrDefault( c => c.Name.ToLower() == collection ); + + if ( newCollection == null ) + { + Dalamud.Chat.Print( $"The collection {collection} does not exist." ); + return false; + } + + if( type == "default" ) + { + _modManager.Collections.SetDefaultCollection( newCollection ); + Dalamud.Chat.Print( $"Set { newCollection.Name } as default collection." ); + } + else if ( type == "forced" ) + { + _modManager.Collections.SetForcedCollection( newCollection ); + Dalamud.Chat.Print( $"Set { newCollection.Name } as forced collection." ); + } + + return true; + } + private void OnCommand( string command, string rawArgs ) { const string modsEnabled = "Your mods have now been enabled."; const string modsDisabled = "Your mods have now been disabled."; - var args = rawArgs.Split( new[] { ' ' }, 2 ); + var args = rawArgs.Split( new[] { ' ' }, 3 ); if( args.Length > 0 && args[ 0 ].Length > 0 ) { switch( args[ 0 ] ) @@ -249,6 +296,17 @@ public class Penumbra : IDalamudPlugin : modsDisabled ); break; } + case "collection": + { + if( args.Length == 3 ) + { + SetCollection(args[ 1 ], args[ 2 ]); + } else + { + Dalamud.Chat.Print( "Missing arguments, the correct command format is: /penumbra collection {default|forced} " ); + } + break; + } } return; From 09c92ef0b177d516157df4a868d83bd5332d1786 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 3 Feb 2022 23:53:27 +0100 Subject: [PATCH 0062/2451] Small fixes. --- Penumbra/Penumbra.cs | 84 ++++++++------- Penumbra/UI/MenuTabs/TabCollections.cs | 4 +- Penumbra/UI/SettingsInterface.cs | 141 +++++++++++++------------ 3 files changed, 120 insertions(+), 109 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 563bbee7..3a9ffab1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -38,7 +39,6 @@ public class Penumbra : IDalamudPlugin private WebServer? _webServer; private readonly ModManager _modManager; - private readonly ModCollection[] _collections; public Penumbra( DalamudPluginInterface pluginInterface ) { @@ -95,8 +95,6 @@ public class Penumbra : IDalamudPlugin PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); }; - - _collections = _modManager.Collections.Collections.Values.ToArray(); } public bool Enable() @@ -171,7 +169,7 @@ public class Penumbra : IDalamudPlugin .WithWebApi( "/api", m => m .WithController( () => new ModsController( this ) ) ); - _webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); + _webServer.StateChanged += ( _, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); _webServer.RunAsync(); } @@ -197,45 +195,50 @@ public class Penumbra : IDalamudPlugin ShutdownWebServer(); } - public bool SetCollection(string type, string collection) + public bool SetCollection( string type, string collectionName ) { - type = type.ToLower(); - collection = collection.ToLower(); + type = type.ToLowerInvariant(); + collectionName = collectionName.ToLowerInvariant(); - if( type != "default" && type != "forced" ) - { - Dalamud.Chat.Print( "Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} " ); - return false; - } - - var currentCollection = ( type == "default" ) ? _modManager.Collections.DefaultCollection : _modManager.Collections.ForcedCollection; - - if( collection == currentCollection.Name.ToLower() ) - { - Dalamud.Chat.Print( $"{currentCollection.Name} is already the active collection." ); - return false; - } - - var newCollection = ( collection == "none" ) ? ModCollection.Empty : _collections.FirstOrDefault( c => c.Name.ToLower() == collection ); - - if ( newCollection == null ) + var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) + ? ModCollection.Empty + : _modManager.Collections.Collections.Values.FirstOrDefault( c + => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); + if( collection == null ) { Dalamud.Chat.Print( $"The collection {collection} does not exist." ); return false; } - if( type == "default" ) + switch( type ) { - _modManager.Collections.SetDefaultCollection( newCollection ); - Dalamud.Chat.Print( $"Set { newCollection.Name } as default collection." ); - } - else if ( type == "forced" ) - { - _modManager.Collections.SetForcedCollection( newCollection ); - Dalamud.Chat.Print( $"Set { newCollection.Name } as forced collection." ); - } + case "default": + if( collection == _modManager.Collections.DefaultCollection ) + { + Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); + return false; + } - return true; + _modManager.Collections.SetDefaultCollection( collection ); + Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); + SettingsInterface.ResetDefaultCollection(); + return true; + case "forced": + if( collection == _modManager.Collections.ForcedCollection ) + { + Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); + return false; + } + + _modManager.Collections.SetForcedCollection( collection ); + Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); + SettingsInterface.ResetForcedCollection(); + return true; + default: + Dalamud.Chat.Print( + "Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} " ); + return false; + } } private void OnCommand( string command, string rawArgs ) @@ -252,7 +255,7 @@ public class Penumbra : IDalamudPlugin { Service< ModManager >.Get().DiscoverMods(); Dalamud.Chat.Print( - $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods." + $"Reloaded Penumbra mods. You have {_modManager.Mods.Count} mods." ); break; } @@ -300,11 +303,14 @@ public class Penumbra : IDalamudPlugin { if( args.Length == 3 ) { - SetCollection(args[ 1 ], args[ 2 ]); - } else - { - Dalamud.Chat.Print( "Missing arguments, the correct command format is: /penumbra collection {default|forced} " ); + SetCollection( args[ 1 ], args[ 2 ] ); } + else + { + Dalamud.Chat.Print( "Missing arguments, the correct command format is:" + + " /penumbra collection {default|forced} " ); + } + break; } } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index cf6cbb92..cee1bd71 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -54,10 +54,10 @@ public partial class SettingsInterface private void UpdateIndex() => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; - private void UpdateForcedIndex() + public void UpdateForcedIndex() => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); - private void UpdateDefaultIndex() + public void UpdateDefaultIndex() => _currentDefaultIndex = GetIndex( _manager.Collections.DefaultCollection ); private void UpdateCharacterIndices() diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 3a21ee22..20eaff02 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -3,81 +3,86 @@ using System.Numerics; using Penumbra.Mods; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface : IDisposable { - public partial class SettingsInterface : IDisposable + private const float DefaultVerticalSpace = 20f; + + private static readonly Vector2 AutoFillSize = new(-1, -1); + private static readonly Vector2 ZeroVector = new(0, 0); + + private readonly Penumbra _penumbra; + + private readonly ManageModsButton _manageModsButton; + private readonly MenuBar _menuBar; + private readonly SettingsMenu _menu; + private readonly ModManager _modManager; + + public SettingsInterface( Penumbra penumbra ) { - private const float DefaultVerticalSpace = 20f; + _penumbra = penumbra; + _manageModsButton = new ManageModsButton( this ); + _menuBar = new MenuBar( this ); + _menu = new SettingsMenu( this ); + _modManager = Service< ModManager >.Get(); - private static readonly Vector2 AutoFillSize = new( -1, -1 ); - private static readonly Vector2 ZeroVector = new( 0, 0 ); + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; + Dalamud.PluginInterface.UiBuilder.Draw += Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi += OpenConfig; + } - private readonly Penumbra _penumbra; + public void Dispose() + { + _manageModsButton.Dispose(); + _menu.InstalledTab.Selector.Cache.Dispose(); + Dalamud.PluginInterface.UiBuilder.Draw -= Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfig; + } - private readonly ManageModsButton _manageModsButton; - private readonly MenuBar _menuBar; - private readonly SettingsMenu _menu; - private readonly ModManager _modManager; + private void OpenConfig() + => _menu.Visible = true; - public SettingsInterface( Penumbra penumbra ) + public void FlipVisibility() + => _menu.Visible = !_menu.Visible; + + public void MakeDebugTabVisible() + => _menu.DebugTabVisible = true; + + public void Draw() + { + _menuBar.Draw(); + _menu.Draw(); + } + + private void ReloadMods() + { + _menu.InstalledTab.Selector.ClearSelection(); + _modManager.DiscoverMods( Penumbra.Config.ModDirectory ); + _menu.InstalledTab.Selector.Cache.TriggerListReset(); + } + + private void SaveCurrentCollection( bool recalculateMeta ) + { + var current = _modManager.Collections.CurrentCollection; + current.Save(); + RecalculateCurrent( recalculateMeta ); + } + + private void RecalculateCurrent( bool recalculateMeta ) + { + var current = _modManager.Collections.CurrentCollection; + if( current.Cache != null ) { - _penumbra = penumbra; - _manageModsButton = new ManageModsButton( this ); - _menuBar = new MenuBar( this ); - _menu = new SettingsMenu( this ); - _modManager = Service< ModManager >.Get(); - - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; - Dalamud.PluginInterface.UiBuilder.Draw += Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi += OpenConfig; - } - - public void Dispose() - { - _manageModsButton.Dispose(); - _menu.InstalledTab.Selector.Cache.Dispose(); - Dalamud.PluginInterface.UiBuilder.Draw -= Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfig; - } - - private void OpenConfig() - => _menu.Visible = true; - - public void FlipVisibility() - => _menu.Visible = !_menu.Visible; - - public void MakeDebugTabVisible() - => _menu.DebugTabVisible = true; - - public void Draw() - { - _menuBar.Draw(); - _menu.Draw(); - } - - private void ReloadMods() - { - _menu.InstalledTab.Selector.ClearSelection(); - _modManager.DiscoverMods( Penumbra.Config.ModDirectory ); - _menu.InstalledTab.Selector.Cache.TriggerListReset(); - } - - private void SaveCurrentCollection( bool recalculateMeta ) - { - var current = _modManager.Collections.CurrentCollection; - current.Save(); - RecalculateCurrent( recalculateMeta ); - } - - private void RecalculateCurrent( bool recalculateMeta ) - { - var current = _modManager.Collections.CurrentCollection; - if( current.Cache != null ) - { - current.CalculateEffectiveFileList( _modManager.TempPath, recalculateMeta, - current == _modManager.Collections.ActiveCollection ); - _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); - } + current.CalculateEffectiveFileList( _modManager.TempPath, recalculateMeta, + current == _modManager.Collections.ActiveCollection ); + _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } } + + public void ResetDefaultCollection() + => _menu.CollectionsTab.UpdateDefaultIndex(); + + public void ResetForcedCollection() + => _menu.CollectionsTab.UpdateForcedIndex(); } \ No newline at end of file From 947e40b1eb3b6e826bd587fb07e47f169e2fb8e0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Feb 2022 11:50:43 +0100 Subject: [PATCH 0063/2451] Remove ClientStructs Initialize --- Penumbra/Penumbra.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3a9ffab1..2a6a04ff 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -42,7 +42,6 @@ public class Penumbra : IDalamudPlugin public Penumbra( DalamudPluginInterface pluginInterface ) { - FFXIVClientStructs.Resolver.Initialize(); Dalamud.Initialize( pluginInterface ); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); Config = Configuration.Load(); From aa180dcdf606e3719ef79e699bcff46bdd9e5cb3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Feb 2022 15:00:50 +0100 Subject: [PATCH 0064/2451] Change Skin Material Replacement to accept arbitrary suffix-strings for From and To. --- .../TabInstalled/TabInstalledModPanel.cs | 1055 ++++++++--------- Penumbra/Util/ModelChanger.cs | 152 ++- 2 files changed, 638 insertions(+), 569 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index f0b2dbe3..f9aaa269 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -11,593 +11,588 @@ using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class ModPanel { - private class ModPanel + private const string LabelModPanel = "selectedModInfo"; + private const string LabelEditName = "##editName"; + private const string LabelEditVersion = "##editVersion"; + private const string LabelEditAuthor = "##editAuthor"; + private const string LabelEditWebsite = "##editWebsite"; + private const string LabelModEnabled = "Enabled"; + private const string LabelEditingEnabled = "Enable Editing"; + private const string LabelOverWriteDir = "OverwriteDir"; + private const string ButtonOpenWebsite = "Open Website"; + private const string ButtonOpenModFolder = "Open Mod Folder"; + private const string ButtonRenameModFolder = "Rename Mod Folder"; + private const string ButtonEditJson = "Edit JSON"; + private const string ButtonReloadJson = "Reload JSON"; + private const string ButtonDeduplicate = "Deduplicate"; + private const string ButtonNormalize = "Normalize"; + private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; + private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application."; + private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; + private const string TooltipReloadJson = "Reload the configuration of all mods."; + private const string PopupRenameFolder = "Rename Folder"; + + private const string TooltipDeduplicate = + "Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n" + + "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!"; + + private const string TooltipNormalize = + "Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!"; + + private const float HeaderLineDistance = 10f; + private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f); + + private readonly SettingsInterface _base; + private readonly Selector _selector; + private readonly ModManager _modManager; + private readonly HashSet< string > _newMods; + public readonly PluginDetails Details; + + private bool _editMode; + private string _currentWebsite; + private bool _validWebsite; + + private string _fromMaterial = string.Empty; + private string _toMaterial = string.Empty; + + public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) { - private const string LabelModPanel = "selectedModInfo"; - private const string LabelEditName = "##editName"; - private const string LabelEditVersion = "##editVersion"; - private const string LabelEditAuthor = "##editAuthor"; - private const string LabelEditWebsite = "##editWebsite"; - private const string LabelModEnabled = "Enabled"; - private const string LabelEditingEnabled = "Enable Editing"; - private const string LabelOverWriteDir = "OverwriteDir"; - private const string ButtonOpenWebsite = "Open Website"; - private const string ButtonOpenModFolder = "Open Mod Folder"; - private const string ButtonRenameModFolder = "Rename Mod Folder"; - private const string ButtonEditJson = "Edit JSON"; - private const string ButtonReloadJson = "Reload JSON"; - private const string ButtonDeduplicate = "Deduplicate"; - private const string ButtonNormalize = "Normalize"; - private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; - private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application."; - private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; - private const string TooltipReloadJson = "Reload the configuration of all mods."; - private const string PopupRenameFolder = "Rename Folder"; + _base = ui; + _selector = s; + _newMods = newMods; + Details = new PluginDetails( _base, _selector ); + _currentWebsite = Meta?.Website ?? ""; + _modManager = Service< ModManager >.Get(); + } - private const string TooltipDeduplicate = - "Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n" - + "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!"; + private Mod.Mod? Mod + => _selector.Mod; - private const string TooltipNormalize = - "Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!"; + private ModMeta? Meta + => Mod?.Data.Meta; - private const float HeaderLineDistance = 10f; - private static readonly Vector4 GreyColor = new( 1f, 1f, 1f, 0.66f ); - - private readonly SettingsInterface _base; - private readonly Selector _selector; - private readonly ModManager _modManager; - private readonly HashSet< string > _newMods; - public readonly PluginDetails Details; - - private bool _editMode; - private string _currentWebsite; - private bool _validWebsite; - - public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) + private void DrawName() + { + var name = Meta!.Name; + if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) { - _base = ui; - _selector = s; - _newMods = newMods; - Details = new PluginDetails( _base, _selector ); - _currentWebsite = Meta?.Website ?? ""; - _modManager = Service< ModManager >.Get(); - } - - private Mod.Mod? Mod - => _selector.Mod; - - private ModMeta? Meta - => Mod?.Data.Meta; - - private void DrawName() - { - var name = Meta!.Name; - if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) + _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); + if( !_modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) { - _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - if( !_modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) - { - Mod.Data.Rename( name ); - } + Mod.Data.Rename( name ); } } + } - private void DrawVersion() - { - if( _editMode ) - { - ImGui.BeginGroup(); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - ImGui.Text( "(Version " ); - - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); - ImGui.SameLine(); - var version = Meta!.Version; - if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) - && version != Meta.Version ) - { - Meta.Version = version; - _selector.SaveCurrentMod(); - } - - ImGui.SameLine(); - ImGui.Text( ")" ); - } - else if( Meta!.Version.Length > 0 ) - { - ImGui.Text( $"(Version {Meta.Version})" ); - } - } - - private void DrawAuthor() - { - ImGui.BeginGroup(); - ImGui.TextColored( GreyColor, "by" ); - - ImGui.SameLine(); - var author = Meta!.Author; - if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) - && author != Meta.Author ) - { - Meta.Author = author; - _selector.SaveCurrentMod(); - _selector.Cache.TriggerFilterReset(); - } - - ImGui.EndGroup(); - } - - private void DrawWebsite() + private void DrawVersion() + { + if( _editMode ) { ImGui.BeginGroup(); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - if( _editMode ) + ImGui.Text( "(Version " ); + + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); + ImGui.SameLine(); + var version = Meta!.Version; + if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) + && version != Meta.Version ) + { + Meta.Version = version; + _selector.SaveCurrentMod(); + } + + ImGui.SameLine(); + ImGui.Text( ")" ); + } + else if( Meta!.Version.Length > 0 ) + { + ImGui.Text( $"(Version {Meta.Version})" ); + } + } + + private void DrawAuthor() + { + ImGui.BeginGroup(); + ImGui.TextColored( GreyColor, "by" ); + + ImGui.SameLine(); + var author = Meta!.Author; + if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) + && author != Meta.Author ) + { + Meta.Author = author; + _selector.SaveCurrentMod(); + _selector.Cache.TriggerFilterReset(); + } + + ImGui.EndGroup(); + } + + private void DrawWebsite() + { + ImGui.BeginGroup(); + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); + if( _editMode ) + { + ImGui.TextColored( GreyColor, "from" ); + ImGui.SameLine(); + var website = Meta!.Website; + if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) + && website != Meta.Website ) + { + Meta.Website = website; + _selector.SaveCurrentMod(); + } + } + else if( Meta!.Website.Length > 0 ) + { + if( _currentWebsite != Meta.Website ) + { + _currentWebsite = Meta.Website; + _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) + && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); + } + + if( _validWebsite ) + { + if( ImGui.SmallButton( ButtonOpenWebsite ) ) + { + try + { + var process = new ProcessStartInfo( Meta.Website ) + { + UseShellExecute = true, + }; + Process.Start( process ); + } + catch( System.ComponentModel.Win32Exception ) + { + // Do nothing. + } + } + + ImGuiCustom.HoverTooltip( Meta.Website ); + } + else { ImGui.TextColored( GreyColor, "from" ); ImGui.SameLine(); - var website = Meta!.Website; - if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) - && website != Meta.Website ) - { - Meta.Website = website; - _selector.SaveCurrentMod(); - } - } - else if( Meta!.Website.Length > 0 ) - { - if( _currentWebsite != Meta.Website ) - { - _currentWebsite = Meta.Website; - _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - } - - if( _validWebsite ) - { - if( ImGui.SmallButton( ButtonOpenWebsite ) ) - { - try - { - var process = new ProcessStartInfo( Meta.Website ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch( System.ComponentModel.Win32Exception ) - { - // Do nothing. - } - } - - ImGuiCustom.HoverTooltip( Meta.Website ); - } - else - { - ImGui.TextColored( GreyColor, "from" ); - ImGui.SameLine(); - ImGui.Text( Meta.Website ); - } + ImGui.Text( Meta.Website ); } } + } - private void DrawHeaderLine() + private void DrawHeaderLine() + { + DrawName(); + ImGui.SameLine(); + DrawVersion(); + ImGui.SameLine(); + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + + private void DrawPriority() + { + var priority = Mod!.Settings.Priority; + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) { - DrawName(); - ImGui.SameLine(); - DrawVersion(); - ImGui.SameLine(); - DrawAuthor(); - ImGui.SameLine(); - DrawWebsite(); + Mod.Settings.Priority = priority; + _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); + _selector.Cache.TriggerFilterReset(); } - private void DrawPriority() + ImGuiCustom.HoverTooltip( + "Higher priority mods take precedence over other mods in the case of file conflicts.\n" + + "In case of identical priority, the alphabetically first mod takes precedence." ); + } + + private void DrawEnabledMark() + { + var enabled = Mod!.Settings.Enabled; + if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { - var priority = Mod!.Settings.Priority; - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) + Mod.Settings.Enabled = enabled; + if( enabled ) { - Mod.Settings.Priority = priority; - _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); - _selector.Cache.TriggerFilterReset(); + _newMods.Remove( Mod.Data.BasePath.Name ); + } + else + { + Mod.Cache.ClearConflicts(); } - ImGuiCustom.HoverTooltip( - "Higher priority mods take precedence over other mods in the case of file conflicts.\n" - + "In case of identical priority, the alphabetically first mod takes precedence." ); + _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); + _selector.Cache.TriggerFilterReset(); + } + } + + public static bool DrawSortOrder( ModData mod, ModManager manager, Selector selector ) + { + var currentSortOrder = mod.SortOrder.FullPath; + ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + manager.ChangeSortOrder( mod, currentSortOrder ); + selector.SelectModOnUpdate( mod.BasePath.Name ); + return true; } - private void DrawEnabledMark() - { - var enabled = Mod!.Settings.Enabled; - if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) - { - Mod.Settings.Enabled = enabled; - if( enabled ) - { - _newMods.Remove( Mod.Data.BasePath.Name ); - } - else - { - Mod.Cache.ClearConflicts(); - } + return false; + } - _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); - _selector.Cache.TriggerFilterReset(); + private void DrawEditableMark() + { + ImGui.Checkbox( LabelEditingEnabled, ref _editMode ); + } + + private void DrawOpenModFolderButton() + { + Mod!.Data.BasePath.Refresh(); + if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists ) + { + Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } ); + } + + ImGuiCustom.HoverTooltip( TooltipOpenModFolder ); + } + + private string _newName = ""; + private bool _keyboardFocus = true; + + private void RenameModFolder( string newName ) + { + _newName = newName.ReplaceBadXivSymbols(); + if( _newName.Length == 0 ) + { + PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); + ImGui.CloseCurrentPopup(); + } + else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) + { + var dir = Mod!.Data.BasePath; + DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + + if( newDir.Exists ) + { + ImGui.OpenPopup( LabelOverWriteDir ); } - } - - public static bool DrawSortOrder( ModData mod, ModManager manager, Selector selector ) - { - var currentSortOrder = mod.SortOrder.FullPath; - ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) + else if( _modManager.RenameModFolder( Mod.Data, newDir ) ) { - manager.ChangeSortOrder( mod, currentSortOrder ); - selector.SelectModOnUpdate( mod.BasePath.Name ); - return true; - } - - return false; - } - - private void DrawEditableMark() - { - ImGui.Checkbox( LabelEditingEnabled, ref _editMode ); - } - - private void DrawOpenModFolderButton() - { - Mod!.Data.BasePath.Refresh(); - if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists ) - { - Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } ); - } - - ImGuiCustom.HoverTooltip( TooltipOpenModFolder ); - } - - private string _newName = ""; - private bool _keyboardFocus = true; - - private void RenameModFolder( string newName ) - { - _newName = newName.ReplaceBadXivSymbols(); - if( _newName.Length == 0 ) - { - PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); + _selector.ReloadCurrentMod(); ImGui.CloseCurrentPopup(); } - else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - DirectoryInfo dir = Mod!.Data.BasePath; - DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); + } + } - if( newDir.Exists ) + private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) + { + try + { + foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + { + var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); + if( targetFile.Exists ) { - ImGui.OpenPopup( LabelOverWriteDir ); - } - else if( _modManager.RenameModFolder( Mod.Data, newDir ) ) - { - _selector.ReloadCurrentMod(); - ImGui.CloseCurrentPopup(); + targetFile.Delete(); } + + targetFile.Directory?.Create(); + file.MoveTo( targetFile.FullName ); } + + source.Delete( true ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); } - private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) + return false; + } + + private bool OverwriteDirPopup() + { + var closeParent = false; + var _ = true; + if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) { - try - { - foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - { - var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); - if( targetFile.Exists ) - { - targetFile.Delete(); - } - - targetFile.Directory?.Create(); - file.MoveTo( targetFile.FullName ); - } - - source.Delete( true ); - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); - } - - return false; - } - - private bool OverwriteDirPopup() - { - var closeParent = false; - var _ = true; - if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - { - return closeParent; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - DirectoryInfo dir = Mod!.Data.BasePath; - DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); - ImGui.Text( - $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); - var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( "Yes", buttonSize ) ) - { - if( MergeFolderInto( dir, newDir ) ) - { - Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); - - _selector.SelectModOnUpdate( _newName ); - - closeParent = true; - ImGui.CloseCurrentPopup(); - } - } - - ImGui.SameLine(); - - if( ImGui.Button( "Cancel", buttonSize ) ) - { - _keyboardFocus = true; - ImGui.CloseCurrentPopup(); - } - return closeParent; } - private void DrawRenameModFolderPopup() - { - var _ = true; - _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) ); - if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) + var dir = Mod!.Data.BasePath; + DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + ImGui.Text( + $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); + var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); + if( ImGui.Button( "Yes", buttonSize ) ) + { + if( MergeFolderInto( dir, newDir ) ) + { + Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); + + _selector.SelectModOnUpdate( _newName ); + + closeParent = true; + ImGui.CloseCurrentPopup(); + } + } + + ImGui.SameLine(); + + if( ImGui.Button( "Cancel", buttonSize ) ) + { + _keyboardFocus = true; + ImGui.CloseCurrentPopup(); + } + + return closeParent; + } + + private void DrawRenameModFolderPopup() + { + var _ = true; + _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); + + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) ); + if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + + if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + { + ImGui.CloseCurrentPopup(); + } + + var newName = Mod!.Data.BasePath.Name; + + if( _keyboardFocus ) + { + ImGui.SetKeyboardFocusHere(); + _keyboardFocus = false; + } + + if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + RenameModFolder( newName ); + } + + ImGui.TextColored( GreyColor, + "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); + + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + + + if( OverwriteDirPopup() ) + { + ImGui.CloseCurrentPopup(); + } + } + + private void DrawRenameModFolderButton() + { + DrawRenameModFolderPopup(); + if( ImGui.Button( ButtonRenameModFolder ) ) + { + ImGui.OpenPopup( PopupRenameFolder ); + } + + ImGuiCustom.HoverTooltip( TooltipRenameModFolder ); + } + + private void DrawEditJsonButton() + { + if( ImGui.Button( ButtonEditJson ) ) + { + _selector.SaveCurrentMod(); + Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } ); + } + + ImGuiCustom.HoverTooltip( TooltipEditJson ); + } + + private void DrawReloadJsonButton() + { + if( ImGui.Button( ButtonReloadJson ) ) + { + _selector.ReloadCurrentMod( true, false ); + } + + ImGuiCustom.HoverTooltip( TooltipReloadJson ); + } + + private void DrawResetMetaButton() + { + if( ImGui.Button( "Recompute Metadata" ) ) + { + _selector.ReloadCurrentMod( true, true, true ); + } + + ImGuiCustom.HoverTooltip( + "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" + + "Also reloads the mod.\n" + + "Be aware that this removes all manually added metadata changes." ); + } + + private void DrawDeduplicateButton() + { + if( ImGui.Button( ButtonDeduplicate ) ) + { + ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod( true, true, true ); + } + + ImGuiCustom.HoverTooltip( TooltipDeduplicate ); + } + + private void DrawNormalizeButton() + { + if( ImGui.Button( ButtonNormalize ) ) + { + ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod( true, true, true ); + } + + ImGuiCustom.HoverTooltip( TooltipNormalize ); + } + + private void DrawAutoGenerateGroupsButton() + { + if( ImGui.Button( "Auto-Generate Groups" ) ) + { + ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod( true, true ); + } + + ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" + + "First subdirectory: Option Group\n" + + "Second subdirectory: Option Name\n" + + "Afterwards: Relative file paths.\n" + + "Experimental - Use at own risk!" ); + } + + private void DrawSplitButton() + { + if( ImGui.Button( "Split Mod" ) ) + { + ModCleanup.SplitMod( Mod!.Data ); + } + + ImGuiCustom.HoverTooltip( + "Split off all options of a mod into single mods that are placed in a collective folder.\n" + + "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n" + + "Experimental - Use at own risk!" ); + } + + private void DrawMaterialChangeRow() + { + ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 ); + ImGui.SameLine(); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() ); + font.Pop(); + ImGui.SameLine(); + ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 ); + ImGui.SameLine(); + var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial ); + using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings ); + if( ImGui.Button( "Convert" ) && validStrings ) + { + ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial ); + } + + alpha.Pop(); + + ImGuiCustom.HoverTooltip( + "Change the skin material of all models in this mod reference " + + "from the suffix given in the first text input to " + + "the suffix given in the second input.\n" + + "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n" + + "This overwrites .mdl files, use at your own risk!" ); + } + + private void DrawEditLine() + { + DrawOpenModFolderButton(); + ImGui.SameLine(); + DrawRenameModFolderButton(); + ImGui.SameLine(); + DrawEditJsonButton(); + ImGui.SameLine(); + DrawReloadJsonButton(); + + DrawResetMetaButton(); + ImGui.SameLine(); + DrawDeduplicateButton(); + ImGui.SameLine(); + DrawNormalizeButton(); + ImGui.SameLine(); + DrawAutoGenerateGroupsButton(); + ImGui.SameLine(); + DrawSplitButton(); + + DrawMaterialChangeRow(); + + DrawSortOrder( Mod!.Data, _modManager, _selector ); + } + + public void Draw() + { + try + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); + var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); + + if( !ret || Mod == null ) { return; } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + DrawHeaderLine(); - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } - - var newName = Mod!.Data.BasePath.Name; - - if( _keyboardFocus ) - { - ImGui.SetKeyboardFocusHere(); - _keyboardFocus = false; - } - - if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - RenameModFolder( newName ); - } - - ImGui.TextColored( GreyColor, - "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); - - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - - - if( OverwriteDirPopup() ) - { - ImGui.CloseCurrentPopup(); - } - } - - private void DrawRenameModFolderButton() - { - DrawRenameModFolderPopup(); - if( ImGui.Button( ButtonRenameModFolder ) ) - { - ImGui.OpenPopup( PopupRenameFolder ); - } - - ImGuiCustom.HoverTooltip( TooltipRenameModFolder ); - } - - private void DrawEditJsonButton() - { - if( ImGui.Button( ButtonEditJson ) ) - { - _selector.SaveCurrentMod(); - Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } ); - } - - ImGuiCustom.HoverTooltip( TooltipEditJson ); - } - - private void DrawReloadJsonButton() - { - if( ImGui.Button( ButtonReloadJson ) ) - { - _selector.ReloadCurrentMod( true, false ); - } - - ImGuiCustom.HoverTooltip( TooltipReloadJson ); - } - - private void DrawResetMetaButton() - { - if( ImGui.Button( "Recompute Metadata" ) ) - { - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( - "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" - + "Also reloads the mod.\n" - + "Be aware that this removes all manually added metadata changes." ); - } - - private void DrawDeduplicateButton() - { - if( ImGui.Button( ButtonDeduplicate ) ) - { - ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( TooltipDeduplicate ); - } - - private void DrawNormalizeButton() - { - if( ImGui.Button( ButtonNormalize ) ) - { - ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( TooltipNormalize ); - } - - private void DrawAutoGenerateGroupsButton() - { - if( ImGui.Button( "Auto-Generate Groups" ) ) - { - ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true ); - } - - ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" - + "First subdirectory: Option Group\n" - + "Second subdirectory: Option Name\n" - + "Afterwards: Relative file paths.\n" - + "Experimental - Use at own risk!" ); - } - - private void DrawSplitButton() - { - if( ImGui.Button( "Split Mod" ) ) - { - ModCleanup.SplitMod( Mod!.Data ); - } - - ImGuiCustom.HoverTooltip( - "Split off all options of a mod into single mods that are placed in a collective folder.\n" - + "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n" - + "Experimental - Use at own risk!" ); - } - - private void DrawMaterialChangeButtons() - { - if( ImGui.Button( "Skin Material B to D" ) ) - { - ModelChanger.ChangeMtrlBToD( Mod!.Data ); - } - ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from B to D.\n" - + "This is usually to convert Bibo+ models to T&F3 skins.\n" - + "This overwrites .mdl files, use at your own risk!" ); + // Next line with fixed distance. + ImGuiCustom.VerticalDistance( HeaderLineDistance ); + DrawEnabledMark(); ImGui.SameLine(); - if( ImGui.Button( "Skin Material D to B" ) ) + DrawPriority(); + if( Penumbra.Config.ShowAdvanced ) { - ModelChanger.ChangeMtrlDToB( Mod!.Data ); - } - ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from D to B.\n" - + "This is usually to convert T&F3 models to Bibo+ skins.\n" - + "This overwrites .mdl files, use at your own risk!" ); - - ImGui.SameLine(); - if( ImGui.Button( "Skin Material A to E" ) ) - { - ModelChanger.ChangeMtrlAToE( Mod!.Data ); - } - ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from A to E.\n" - + "This overwrites .mdl files, use at your own risk!" ); - - ImGui.SameLine(); - if( ImGui.Button( "Skin Material E to A" ) ) - { - ModelChanger.ChangeMtrlEToA( Mod!.Data ); - } - ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from E to A.\n" - + "This overwrites .mdl files, use at your own risk!" ); - } - - private void DrawEditLine() - { - DrawOpenModFolderButton(); - ImGui.SameLine(); - DrawRenameModFolderButton(); - ImGui.SameLine(); - DrawEditJsonButton(); - ImGui.SameLine(); - DrawReloadJsonButton(); - - DrawResetMetaButton(); - ImGui.SameLine(); - DrawDeduplicateButton(); - ImGui.SameLine(); - DrawNormalizeButton(); - ImGui.SameLine(); - DrawAutoGenerateGroupsButton(); - ImGui.SameLine(); - DrawSplitButton(); - - DrawMaterialChangeButtons(); - - DrawSortOrder( Mod!.Data, _modManager, _selector ); - } - - public void Draw() - { - try - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); - var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); - - if( !ret || Mod == null ) - { - return; - } - - DrawHeaderLine(); - - // Next line with fixed distance. - ImGuiCustom.VerticalDistance( HeaderLineDistance ); - - DrawEnabledMark(); ImGui.SameLine(); - DrawPriority(); - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawEditableMark(); - } - - // Next line, if editable. - if( _editMode ) - { - DrawEditLine(); - } - - Details.Draw( _editMode ); + DrawEditableMark(); } - catch( Exception ex ) + + // Next line, if editable. + if( _editMode ) { - PluginLog.LogError( ex, "Oh no" ); + DrawEditLine(); } + + Details.Draw( _editMode ); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "Oh no" ); } } } diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 70c88354..eebfe839 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -9,10 +10,105 @@ namespace Penumbra.Util; public static class ModelChanger { - private const string SkinMaterialString = "/mt_c0201b0001_d.mtrl"; - private static readonly byte[] SkinMaterial = Encoding.UTF8.GetBytes( SkinMaterialString ); + private static int FindSubSequence( byte[] main, byte[] sub, int from = 0 ) + { + if( sub.Length + from > main.Length ) + { + return -1; + } - public static int ChangeMtrl( FullPath file, byte from, byte to ) + var length = main.Length - sub.Length; + for( var i = from; i < length; ++i ) + { + var span = main.AsSpan( i, sub.Length ); + if( span.SequenceEqual( sub ) ) + { + return i; + } + } + + return -1; + } + + private static bool ConvertString( string text, out byte[] data ) + { + data = Encoding.UTF8.GetBytes( text ); + return data.Length == text.Length && !data.Any( b => b > 0b10000000 ); + } + + public static bool ValidStrings( string from, string to ) + => from.Length != 0 + && to.Length != 0 + && from.Length < 16 + && to.Length < 16 + && from != to + && Encoding.UTF8.GetByteCount( from ) == from.Length + && Encoding.UTF8.GetByteCount( to ) == to.Length; + + private static bool ConvertName( string name, out byte[] data ) + { + if( name.Length != 0 ) + { + return ConvertString( $"/mt_c0201b0001_{name}.mtrl", out data ); + } + + data = Array.Empty< byte >(); + return false; + } + + private static int ReplaceEqualSequences( byte[] main, byte[] subLhs, byte[] subRhs ) + { + if( subLhs.SequenceEqual( subRhs ) ) + { + return 0; + } + + var i = 0; + var replacements = 0; + while( ( i = FindSubSequence( main, subLhs, i ) ) > 0 ) + { + subRhs.CopyTo( main.AsSpan( i ) ); + i += subLhs.Length; + ++replacements; + } + + return replacements; + } + + private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs ) + { + if( subLhs.Length == subRhs.Length ) + { + return ReplaceEqualSequences( main, subLhs, subRhs ); + } + + var replacements = new List< int >( 4 ); + for( var i = FindSubSequence( main, subLhs ); i >= 0; i = FindSubSequence( main, subLhs, i + subLhs.Length ) ) + { + replacements.Add( i ); + } + + var ret = new byte[main.Length + ( subRhs.Length - subLhs.Length ) * replacements.Count]; + + var last = 0; + var totalLength = 0; + foreach( var i in replacements ) + { + var length = i - last; + main.AsSpan( last, length ).CopyTo( ret.AsSpan( totalLength ) ); + totalLength += length; + subRhs.CopyTo( ret.AsSpan( totalLength ) ); + totalLength += subRhs.Length; + last = i + subLhs.Length; + } + + main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) ); + + main = ret; + return replacements.Count; + } + + public static int ChangeMtrl( FullPath file, byte[] from, byte[] to ) { if( !file.Exists ) { @@ -22,53 +118,31 @@ public static class ModelChanger try { var text = File.ReadAllBytes( file.FullName ); - var replaced = 0; - - var length = text.Length - SkinMaterial.Length; - SkinMaterial[ 15 ] = from; - for( var i = 0; i < length; ++i ) + var replaced = ReplaceSubSequences( ref text, from, to ); + if( replaced > 0 ) { - if( SkinMaterial.Where( ( t, j ) => text[ i + j ] != t ).Any() ) - { - continue; - } - - text[ i + 15 ] = to; - i += SkinMaterial.Length; - ++replaced; + File.WriteAllBytes( file.FullName, text ); } - if( replaced == 0 ) - { - return 0; - } - - File.WriteAllBytes( file.FullName, text ); return replaced; } catch( Exception e ) { - PluginLog.Error( $"Could not write .mdl data for file {file.FullName}, replacing {( char )from} with {( char )to}:\n{e}" ); + PluginLog.Error( $"Could not write .mdl data for file {file.FullName}:\n{e}" ); return -1; } } - public static bool ChangeModMaterials( ModData mod, byte from, byte to ) + public static bool ChangeModMaterials( ModData mod, string from, string to ) { - return mod.Resources.ModFiles - .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) - .All( file => ChangeMtrl( file, from, to ) >= 0 ); + if( ValidStrings( from, to ) && ConvertName( from, out var lhs ) && ConvertName( to, out var rhs ) ) + { + return mod.Resources.ModFiles + .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) + .All( file => ChangeMtrl( file, lhs, rhs ) >= 0 ); + } + + PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." ); + return false; } - - public static bool ChangeMtrlBToD( ModData mod ) - => ChangeModMaterials( mod, ( byte )'b', ( byte )'d' ); - - public static bool ChangeMtrlDToB( ModData mod ) - => ChangeModMaterials( mod, ( byte )'d', ( byte )'b' ); - - public static bool ChangeMtrlEToA( ModData mod ) - => ChangeModMaterials( mod, ( byte )'e', ( byte )'a' ); - - public static bool ChangeMtrlAToE( ModData mod ) - => ChangeModMaterials( mod, ( byte )'a', ( byte )'e' ); } \ No newline at end of file From a9065d97d3f7e6d8dff961fc650245ec2e500b37 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 16 Feb 2022 20:40:42 +0000 Subject: [PATCH 0065/2451] [CI] Updating repo.json for refs/tags/0.4.7.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2fb78234..cb6dd61b 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.3", - "TestingAssemblyVersion": "0.4.7.3", + "AssemblyVersion": "0.4.7.4", + "TestingAssemblyVersion": "0.4.7.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 25b65ce628ca759e0e5031659d132a7c4cd8c90f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Feb 2022 18:30:36 +0100 Subject: [PATCH 0066/2451] Fix redraw not working with names anymore. --- Penumbra/Penumbra.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2a6a04ff..6a7157b8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,7 +245,7 @@ public class Penumbra : IDalamudPlugin const string modsEnabled = "Your mods have now been enabled."; const string modsDisabled = "Your mods have now been disabled."; - var args = rawArgs.Split( new[] { ' ' }, 3 ); + var args = rawArgs.Split( new[] { ' ' }, 2 ); if( args.Length > 0 && args[ 0 ].Length > 0 ) { switch( args[ 0 ] ) @@ -300,9 +300,13 @@ public class Penumbra : IDalamudPlugin } case "collection": { - if( args.Length == 3 ) + if( args.Length == 2 ) { - SetCollection( args[ 1 ], args[ 2 ] ); + args = args[ 1 ].Split( new[] { ' ' }, 2 ); + if( args.Length == 2 ) + { + SetCollection( args[ 0 ], args[ 1 ] ); + } } else { From 3d6d3ed2d6739bec5d15e1a30e784f919e4aaa6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Feb 2022 18:31:46 +0100 Subject: [PATCH 0067/2451] Fix material change in mdls for different suffix sizes. --- .../TabInstalledDetailsManipulations.cs | 2 +- Penumbra/Util/ModelChanger.cs | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 5239b00f..4c9a7ad2 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -576,7 +576,7 @@ namespace Penumbra.UI } ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Value}##{manipIdx}" ) ) + if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) ) { ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index eebfe839..b4d20bf6 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -37,11 +38,11 @@ public static class ModelChanger } public static bool ValidStrings( string from, string to ) - => from.Length != 0 - && to.Length != 0 - && from.Length < 16 - && to.Length < 16 - && from != to + => from.Length != 0 + && to.Length != 0 + && from.Length < 16 + && to.Length < 16 + && from != to && Encoding.UTF8.GetByteCount( from ) == from.Length && Encoding.UTF8.GetByteCount( to ) == to.Length; @@ -75,6 +76,17 @@ public static class ModelChanger return replacements; } + private static void ReplaceStringSize( byte[] main, int sizeDiff ) + { + var stackSize = BitConverter.ToUInt32( main, 4 ); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32( main, ( int )stringsLengthOffset ); + var newLength = stringsLength + sizeDiff; + Debug.Assert( newLength > 0 ); + BitConverter.TryWriteBytes( main.AsSpan( ( int )stringsLengthOffset ), ( uint )newLength ); + } + private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs ) { if( subLhs.Length == subRhs.Length ) @@ -88,7 +100,8 @@ public static class ModelChanger replacements.Add( i ); } - var ret = new byte[main.Length + ( subRhs.Length - subLhs.Length ) * replacements.Count]; + var sizeDiff = ( subRhs.Length - subLhs.Length ) * replacements.Count; + var ret = new byte[main.Length + sizeDiff]; var last = 0; var totalLength = 0; @@ -103,7 +116,7 @@ public static class ModelChanger } main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) ); - + ReplaceStringSize( ret, sizeDiff ); main = ret; return replacements.Count; } From 38ad1eef96e34a160417d0cee29bea25196f0ce1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 19 Feb 2022 17:34:08 +0000 Subject: [PATCH 0068/2451] [CI] Updating repo.json for refs/tags/0.4.7.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cb6dd61b..a3749225 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.4", - "TestingAssemblyVersion": "0.4.7.4", + "AssemblyVersion": "0.4.7.5", + "TestingAssemblyVersion": "0.4.7.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f4f3a4dfc189c2a5857082e4327d03d3745888f5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Feb 2022 01:09:28 +0100 Subject: [PATCH 0069/2451] Fix material change in mdls for different suffix sizes., next try. --- Penumbra/Util/ModelChanger.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index b4d20bf6..9938007f 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -76,15 +76,21 @@ public static class ModelChanger return replacements; } - private static void ReplaceStringSize( byte[] main, int sizeDiff ) + private static void ReplaceOffsetsAndSizes( byte[] main, int sizeDiff ) { var stackSize = BitConverter.ToUInt32( main, 4 ); var runtimeBegin = stackSize + 0x44; var stringsLengthOffset = runtimeBegin + 4; var stringsLength = BitConverter.ToUInt32( main, ( int )stringsLengthOffset ); - var newLength = stringsLength + sizeDiff; - Debug.Assert( newLength > 0 ); - BitConverter.TryWriteBytes( main.AsSpan( ( int )stringsLengthOffset ), ( uint )newLength ); + + BitConverter.TryWriteBytes( main.AsSpan( 8 ), ( uint )( BitConverter.ToUInt32( main, 8 ) + sizeDiff ) ); // RuntimeSize + BitConverter.TryWriteBytes( main.AsSpan( 16 ), ( uint )( BitConverter.ToUInt32( main, 16 ) + sizeDiff ) ); // VertexOffset 1 + BitConverter.TryWriteBytes( main.AsSpan( 20 ), ( uint )( BitConverter.ToUInt32( main, 20 ) + sizeDiff ) ); // VertexOffset 2 + BitConverter.TryWriteBytes( main.AsSpan( 24 ), ( uint )( BitConverter.ToUInt32( main, 24 ) + sizeDiff ) ); // VertexOffset 3 + BitConverter.TryWriteBytes( main.AsSpan( 28 ), ( uint )( BitConverter.ToUInt32( main, 28 ) + sizeDiff ) ); // IndexOffset 1 + BitConverter.TryWriteBytes( main.AsSpan( 32 ), ( uint )( BitConverter.ToUInt32( main, 32 ) + sizeDiff ) ); // IndexOffset 2 + BitConverter.TryWriteBytes( main.AsSpan( 36 ), ( uint )( BitConverter.ToUInt32( main, 36 ) + sizeDiff ) ); // IndexOffset 3 + BitConverter.TryWriteBytes( main.AsSpan( ( int )stringsLengthOffset ), ( uint )( stringsLength + sizeDiff ) ); } private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs ) @@ -116,7 +122,7 @@ public static class ModelChanger } main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) ); - ReplaceStringSize( ret, sizeDiff ); + ReplaceOffsetsAndSizes( ret, sizeDiff ); main = ret; return replacements.Count; } From 7beee50fdf77879082037354424935b05f5311ea Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 20 Feb 2022 00:11:42 +0000 Subject: [PATCH 0070/2451] [CI] Updating repo.json for refs/tags/0.4.7.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a3749225..6a27b27a 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.5", - "TestingAssemblyVersion": "0.4.7.5", + "AssemblyVersion": "0.4.7.6", + "TestingAssemblyVersion": "0.4.7.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 733b60faaef2cee5553050c52ec30807cbf064ff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Feb 2022 19:47:44 +0100 Subject: [PATCH 0071/2451] Fix material change in mdls another time, this time with actual .mdl parsing and writing. --- Penumbra.GameData/Files/MdlFile.Write.cs | 317 +++++++++++++++++++++++ Penumbra.GameData/Files/MdlFile.cs | 259 ++++++++++++++++++ Penumbra/Util/ModelChanger.cs | 137 ++-------- 3 files changed, 602 insertions(+), 111 deletions(-) create mode 100644 Penumbra.GameData/Files/MdlFile.Write.cs create mode 100644 Penumbra.GameData/Files/MdlFile.cs diff --git a/Penumbra.GameData/Files/MdlFile.Write.cs b/Penumbra.GameData/Files/MdlFile.Write.cs new file mode 100644 index 00000000..b4017c52 --- /dev/null +++ b/Penumbra.GameData/Files/MdlFile.Write.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Lumina.Data.Parsing; + +namespace Penumbra.GameData.Files; + +public partial class MdlFile +{ + private static uint Write( BinaryWriter w, string s, long basePos ) + { + var currentPos = w.BaseStream.Position; + w.Write( Encoding.UTF8.GetBytes( s ) ); + w.Write( ( byte )0 ); + return ( uint )( currentPos - basePos ); + } + + private List< uint > WriteStrings( BinaryWriter w ) + { + var startPos = ( int )w.BaseStream.Position; + var basePos = startPos + 8; + var count = ( ushort )( Attributes.Length + Bones.Length + Materials.Length + Shapes.Length ); + + w.Write( count ); + w.Seek( basePos, SeekOrigin.Begin ); + var ret = Attributes.Concat( Bones ) + .Concat( Materials ) + .Concat( Shapes.Select( s => s.ShapeName ) ) + .Select( attribute => Write( w, attribute, basePos ) ).ToList(); + + w.Write( ( ushort )0 ); // Seems to always have two additional null-bytes, not padding. + var size = ( int )w.BaseStream.Position - basePos; + w.Seek( startPos + 4, SeekOrigin.Begin ); + w.Write( ( uint )size ); + w.Seek( basePos + size, SeekOrigin.Begin ); + return ret; + } + + private void WriteModelFileHeader( BinaryWriter w, uint runtimeSize ) + { + w.Write( Version ); + w.Write( StackSize ); + w.Write( runtimeSize ); + w.Write( ( ushort )VertexDeclarations.Length ); + w.Write( ( ushort )Materials.Length ); + w.Write( VertexOffset[ 0 ] > 0 ? VertexOffset[ 0 ] + runtimeSize : 0u ); + w.Write( VertexOffset[ 1 ] > 0 ? VertexOffset[ 1 ] + runtimeSize : 0u ); + w.Write( VertexOffset[ 2 ] > 0 ? VertexOffset[ 2 ] + runtimeSize : 0u ); + w.Write( IndexOffset[ 0 ] > 0 ? IndexOffset[ 0 ] + runtimeSize : 0u ); + w.Write( IndexOffset[ 1 ] > 0 ? IndexOffset[ 1 ] + runtimeSize : 0u ); + w.Write( IndexOffset[ 2 ] > 0 ? IndexOffset[ 2 ] + runtimeSize : 0u ); + w.Write( VertexBufferSize[ 0 ] ); + w.Write( VertexBufferSize[ 1 ] ); + w.Write( VertexBufferSize[ 2 ] ); + w.Write( IndexBufferSize[ 0 ] ); + w.Write( IndexBufferSize[ 1 ] ); + w.Write( IndexBufferSize[ 2 ] ); + w.Write( LodCount ); + w.Write( EnableIndexBufferStreaming ); + w.Write( EnableEdgeGeometry ); + w.Write( ( byte )0 ); // Padding + } + + private void WriteModelHeader( BinaryWriter w ) + { + w.Write( Radius ); + w.Write( ( ushort )Meshes.Length ); + w.Write( ( ushort )Attributes.Length ); + w.Write( ( ushort )SubMeshes.Length ); + w.Write( ( ushort )Materials.Length ); + w.Write( ( ushort )Bones.Length ); + w.Write( ( ushort )BoneTables.Length ); + w.Write( ( ushort )Shapes.Length ); + w.Write( ( ushort )ShapeMeshes.Length ); + w.Write( ( ushort )ShapeValues.Length ); + w.Write( LodCount ); + w.Write( ( byte )Flags1 ); + w.Write( ( ushort )ElementIds.Length ); + w.Write( ( byte )TerrainShadowMeshes.Length ); + w.Write( ( byte )Flags2 ); + w.Write( ModelClipOutDistance ); + w.Write( ShadowClipOutDistance ); + w.Write( Unknown4 ); + w.Write( ( ushort )TerrainShadowSubMeshes.Length ); + w.Write( Unknown5 ); + w.Write( BgChangeMaterialIndex ); + w.Write( BgCrestChangeMaterialIndex ); + w.Write( Unknown6 ); + w.Write( Unknown7 ); + w.Write( Unknown8 ); + w.Write( Unknown9 ); + w.Write( ( uint )0 ); // 6 byte padding + w.Write( ( ushort )0 ); + } + + + private static void Write( BinaryWriter w, in MdlStructs.VertexElement vertex ) + { + w.Write( vertex.Stream ); + w.Write( vertex.Offset ); + w.Write( vertex.Type ); + w.Write( vertex.Usage ); + w.Write( vertex.UsageIndex ); + w.Write( ( ushort )0 ); // 3 byte padding + w.Write( ( byte )0 ); + } + + private static void Write( BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl ) + { + foreach( var vertex in vertexDecl.VertexElements ) + { + Write( w, vertex ); + } + + Write( w, new MdlStructs.VertexElement() { Stream = 255 } ); + w.Seek( ( int )( NumVertices - 1 - vertexDecl.VertexElements.Length ) * 8, SeekOrigin.Current ); + } + + private static void Write( BinaryWriter w, in MdlStructs.ElementIdStruct elementId ) + { + w.Write( elementId.ElementId ); + w.Write( elementId.ParentBoneName ); + w.Write( elementId.Translate[ 0 ] ); + w.Write( elementId.Translate[ 1 ] ); + w.Write( elementId.Translate[ 2 ] ); + w.Write( elementId.Rotate[ 0 ] ); + w.Write( elementId.Rotate[ 1 ] ); + w.Write( elementId.Rotate[ 2 ] ); + } + + private static unsafe void Write< T >( BinaryWriter w, in T data ) where T : unmanaged + { + fixed( T* ptr = &data ) + { + var bytePtr = ( byte* )ptr; + var size = sizeof( T ); + var span = new ReadOnlySpan< byte >( bytePtr, size ); + w.Write( span ); + } + } + + private static void Write( BinaryWriter w, MdlStructs.MeshStruct mesh ) + { + w.Write( mesh.VertexCount ); + w.Write( ( ushort )0 ); // padding + w.Write( mesh.IndexCount ); + w.Write( mesh.MaterialIndex ); + w.Write( mesh.SubMeshIndex ); + w.Write( mesh.SubMeshCount ); + w.Write( mesh.BoneTableIndex ); + w.Write( mesh.StartIndex ); + w.Write( mesh.VertexBufferOffset[ 0 ] ); + w.Write( mesh.VertexBufferOffset[ 1 ] ); + w.Write( mesh.VertexBufferOffset[ 2 ] ); + w.Write( mesh.VertexBufferStride[ 0 ] ); + w.Write( mesh.VertexBufferStride[ 1 ] ); + w.Write( mesh.VertexBufferStride[ 2 ] ); + w.Write( mesh.VertexStreamCount ); + } + + private static void Write( BinaryWriter w, MdlStructs.BoneTableStruct bone ) + { + foreach( var index in bone.BoneIndex ) + { + w.Write( index ); + } + + w.Write( bone.BoneCount ); + w.Write( ( ushort )0 ); // 3 bytes padding + w.Write( ( byte )0 ); + } + + private void Write( BinaryWriter w, int shapeIdx, IReadOnlyList< uint > offsets ) + { + var shape = Shapes[ shapeIdx ]; + var offset = offsets[ Attributes.Length + Bones.Length + Materials.Length + shapeIdx ]; + w.Write( offset ); + w.Write( shape.ShapeMeshStartIndex[ 0 ] ); + w.Write( shape.ShapeMeshStartIndex[ 1 ] ); + w.Write( shape.ShapeMeshStartIndex[ 2 ] ); + w.Write( shape.ShapeMeshCount[ 0 ] ); + w.Write( shape.ShapeMeshCount[ 1 ] ); + w.Write( shape.ShapeMeshCount[ 2 ] ); + } + + private static void Write( BinaryWriter w, MdlStructs.BoundingBoxStruct box ) + { + w.Write( box.Min[ 0 ] ); + w.Write( box.Min[ 1 ] ); + w.Write( box.Min[ 2 ] ); + w.Write( box.Min[ 3 ] ); + w.Write( box.Max[ 0 ] ); + w.Write( box.Max[ 1 ] ); + w.Write( box.Max[ 2 ] ); + w.Write( box.Max[ 3 ] ); + } + + public byte[] Write() + { + using var stream = new MemoryStream(); + using( var w = new BinaryWriter( stream ) ) + { + // Skip and write this later when we actually know it. + w.Seek( ( int )FileHeaderSize, SeekOrigin.Begin ); + + foreach( var vertexDecl in VertexDeclarations ) + { + Write( w, vertexDecl ); + } + + var offsets = WriteStrings( w ); + WriteModelHeader( w ); + + foreach( var elementId in ElementIds ) + { + Write( w, elementId ); + } + + foreach( var lod in Lods ) + { + Write( w, lod ); + } + + if( Flags2.HasFlag( MdlStructs.ModelFlags2.ExtraLodEnabled ) ) + { + foreach( var extraLod in ExtraLods ) + { + Write( w, extraLod ); + } + } + + foreach( var mesh in Meshes ) + { + Write( w, mesh ); + } + + for( var i = 0; i < Attributes.Length; ++i ) + { + w.Write( offsets[ i ] ); + } + + foreach( var terrainShadowMesh in TerrainShadowMeshes ) + { + Write( w, terrainShadowMesh ); + } + + foreach( var subMesh in SubMeshes ) + { + Write( w, subMesh ); + } + + foreach( var terrainShadowSubMesh in TerrainShadowSubMeshes ) + { + Write( w, terrainShadowSubMesh ); + } + + for( var i = 0; i < Materials.Length; ++i ) + { + w.Write( offsets[ Attributes.Length + Bones.Length + i ] ); + } + + for( var i = 0; i < Bones.Length; ++i ) + { + w.Write( offsets[ Attributes.Length + i ] ); + } + + foreach( var boneTable in BoneTables ) + { + Write( w, boneTable ); + } + + for( var i = 0; i < Shapes.Length; ++i ) + { + Write( w, i, offsets ); + } + + foreach( var shapeMesh in ShapeMeshes ) + { + Write( w, shapeMesh ); + } + + foreach( var shapeValue in ShapeValues ) + { + Write( w, shapeValue ); + } + + w.Write( SubMeshBoneMap.Length * 2 ); + foreach( var bone in SubMeshBoneMap ) + { + w.Write( bone ); + } + + w.Write( ( byte )0 ); // number of padding bytes, which is 0 for us. + + Write( w, BoundingBoxes ); + Write( w, ModelBoundingBoxes ); + Write( w, WaterBoundingBoxes ); + Write( w, VerticalFogBoundingBoxes ); + foreach( var box in BoneBoundingBoxes ) + { + Write( w, box ); + } + + var totalSize = w.BaseStream.Position; + var runtimeSize = ( uint )( totalSize - StackSize - FileHeaderSize ); + w.Write( RemainingData ); + + // Write header data. + w.Seek( 0, SeekOrigin.Begin ); + WriteModelFileHeader( w, runtimeSize ); + } + + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs new file mode 100644 index 00000000..cf829a65 --- /dev/null +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -0,0 +1,259 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Lumina.Data.Parsing; +using Lumina.Extensions; + +namespace Penumbra.GameData.Files; + +public partial class MdlFile +{ + public const uint NumVertices = 17; + public const uint FileHeaderSize = 0x44; + + // Refers to string, thus not Lumina struct. + public struct Shape + { + public string ShapeName = string.Empty; + public ushort[] ShapeMeshStartIndex; + public ushort[] ShapeMeshCount; + + public Shape( MdlStructs.ShapeStruct data, uint[] offsets, string[] strings ) + { + var idx = offsets.AsSpan().IndexOf( data.StringOffset ); + ShapeName = idx >= 0 ? strings[ idx ] : string.Empty; + ShapeMeshStartIndex = data.ShapeMeshStartIndex; + ShapeMeshCount = data.ShapeMeshCount; + } + } + + // Raw data to write back. + public uint Version; + public float Radius; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public byte BgChangeMaterialIndex; + public byte BgCrestChangeMaterialIndex; + public ushort Unknown4; + public byte Unknown5; + public byte Unknown6; + public ushort Unknown7; + public ushort Unknown8; + public ushort Unknown9; + + // Offsets are stored relative to RuntimeSize instead of file start. + public uint[] VertexOffset; + public uint[] IndexOffset; + + public uint[] VertexBufferSize; + public uint[] IndexBufferSize; + public byte LodCount; + public bool EnableIndexBufferStreaming; + public bool EnableEdgeGeometry; + + + public MdlStructs.ModelFlags1 Flags1; + public MdlStructs.ModelFlags2 Flags2; + + public MdlStructs.BoundingBoxStruct BoundingBoxes; + public MdlStructs.BoundingBoxStruct ModelBoundingBoxes; + public MdlStructs.BoundingBoxStruct WaterBoundingBoxes; + public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes; + + public MdlStructs.VertexDeclarationStruct[] VertexDeclarations; + public MdlStructs.ElementIdStruct[] ElementIds; + public MdlStructs.MeshStruct[] Meshes; + public MdlStructs.BoneTableStruct[] BoneTables; + public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes; + public MdlStructs.SubmeshStruct[] SubMeshes; + public MdlStructs.ShapeMeshStruct[] ShapeMeshes; + public MdlStructs.ShapeValueStruct[] ShapeValues; + public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes; + public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubMeshes; + public MdlStructs.LodStruct[] Lods; + public MdlStructs.ExtraLodStruct[] ExtraLods; + public ushort[] SubMeshBoneMap; + + // Strings are written in order + public string[] Attributes; + public string[] Bones; + public string[] Materials; + public Shape[] Shapes; + + // Raw, unparsed data. + public byte[] RemainingData; + + public MdlFile( byte[] data ) + { + using var stream = new MemoryStream( data ); + using var r = new BinaryReader( stream ); + + var header = LoadModelFileHeader( r ); + LodCount = header.LodCount; + VertexBufferSize = header.VertexBufferSize; + IndexBufferSize = header.IndexBufferSize; + VertexOffset = header.VertexOffset; + IndexOffset = header.IndexOffset; + for( var i = 0; i < 3; ++i ) + { + if( VertexOffset[ i ] > 0 ) + { + VertexOffset[ i ] -= header.RuntimeSize; + } + + if( IndexOffset[ i ] > 0 ) + { + IndexOffset[ i ] -= header.RuntimeSize; + } + } + + VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount]; + for( var i = 0; i < header.VertexDeclarationCount; ++i ) + { + VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( r ); + } + + var (offsets, strings) = LoadStrings( r ); + + var modelHeader = LoadModelHeader( r ); + ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount]; + for( var i = 0; i < modelHeader.ElementIdCount; i++ ) + { + ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r ); + } + + Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 ); + ExtraLods = modelHeader.ExtraLodEnabled + ? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 ) + : Array.Empty< MdlStructs.ExtraLodStruct >(); + + Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount]; + for( var i = 0; i < modelHeader.MeshCount; i++ ) + { + Meshes[ i ] = MdlStructs.MeshStruct.Read( r ); + } + + Attributes = new string[modelHeader.AttributeCount]; + for( var i = 0; i < modelHeader.AttributeCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount ); + SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount ); + TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount ); + + Materials = new string[modelHeader.MaterialCount]; + for( var i = 0; i < modelHeader.MaterialCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + Bones = new string[modelHeader.BoneCount]; + for( var i = 0; i < modelHeader.BoneCount; ++i ) + { + var offset = r.ReadUInt32(); + var stringIdx = offsets.AsSpan().IndexOf( offset ); + Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + } + + BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount]; + for( var i = 0; i < modelHeader.BoneTableCount; i++ ) + { + BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r ); + } + + Shapes = new Shape[modelHeader.ShapeCount]; + for( var i = 0; i < modelHeader.ShapeCount; i++ ) + { + Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings ); + } + + ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount ); + ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount ); + + var submeshBoneMapSize = r.ReadUInt32(); + SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray(); + + var paddingAmount = r.ReadByte(); + r.Seek( r.BaseStream.Position + paddingAmount ); + + // Dunno what this first one is for? + BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount]; + for( var i = 0; i < modelHeader.BoneCount; i++ ) + { + BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( r ); + } + + RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) ); + } + + private MdlStructs.ModelFileHeader LoadModelFileHeader( BinaryReader r ) + { + var header = MdlStructs.ModelFileHeader.Read( r ); + Version = header.Version; + EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; + EnableEdgeGeometry = header.EnableEdgeGeometry; + return header; + } + + private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r ) + { + var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >(); + Radius = modelHeader.Radius; + Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType() + .GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType() + .GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + ModelClipOutDistance = modelHeader.ModelClipOutDistance; + ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; + Unknown4 = modelHeader.Unknown4; + Unknown5 = ( byte )( modelHeader.GetType() + .GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) + ?? 0 ); + Unknown6 = modelHeader.Unknown6; + Unknown7 = modelHeader.Unknown7; + Unknown8 = modelHeader.Unknown8; + Unknown9 = modelHeader.Unknown9; + BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; + BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; + + return modelHeader; + } + + private static (uint[], string[]) LoadStrings( BinaryReader r ) + { + var stringCount = r.ReadUInt16(); + r.ReadUInt16(); + var stringSize = ( int )r.ReadUInt32(); + var stringData = r.ReadBytes( stringSize ); + var start = 0; + var strings = new string[stringCount]; + var offsets = new uint[stringCount]; + for( var i = 0; i < stringCount; ++i ) + { + var span = stringData.AsSpan( start ); + var idx = span.IndexOf( ( byte )'\0' ); + strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] ); + offsets[ i ] = ( uint )start; + start = start + idx + 1; + } + + return ( offsets, strings ); + } + + public unsafe uint StackSize + => ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) ); + +} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 9938007f..7cb1f068 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -1,41 +1,18 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Windows.Forms; using Dalamud.Logging; +using Penumbra.GameData.Files; using Penumbra.Mod; namespace Penumbra.Util; public static class ModelChanger { - private static int FindSubSequence( byte[] main, byte[] sub, int from = 0 ) - { - if( sub.Length + from > main.Length ) - { - return -1; - } - - var length = main.Length - sub.Length; - for( var i = from; i < length; ++i ) - { - var span = main.AsSpan( i, sub.Length ); - if( span.SequenceEqual( sub ) ) - { - return i; - } - } - - return -1; - } - - private static bool ConvertString( string text, out byte[] data ) - { - data = Encoding.UTF8.GetBytes( text ); - return data.Length == text.Length && !data.Any( b => b > 0b10000000 ); - } + public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; public static bool ValidStrings( string from, string to ) => from.Length != 0 @@ -46,88 +23,12 @@ public static class ModelChanger && Encoding.UTF8.GetByteCount( from ) == from.Length && Encoding.UTF8.GetByteCount( to ) == to.Length; - private static bool ConvertName( string name, out byte[] data ) - { - if( name.Length != 0 ) - { - return ConvertString( $"/mt_c0201b0001_{name}.mtrl", out data ); - } - data = Array.Empty< byte >(); - return false; - } + [Conditional( "Debug" )] + private static void WriteBackup( string name, byte[] text ) + => File.WriteAllBytes( name + ".bak", text ); - private static int ReplaceEqualSequences( byte[] main, byte[] subLhs, byte[] subRhs ) - { - if( subLhs.SequenceEqual( subRhs ) ) - { - return 0; - } - - var i = 0; - var replacements = 0; - while( ( i = FindSubSequence( main, subLhs, i ) ) > 0 ) - { - subRhs.CopyTo( main.AsSpan( i ) ); - i += subLhs.Length; - ++replacements; - } - - return replacements; - } - - private static void ReplaceOffsetsAndSizes( byte[] main, int sizeDiff ) - { - var stackSize = BitConverter.ToUInt32( main, 4 ); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( main, ( int )stringsLengthOffset ); - - BitConverter.TryWriteBytes( main.AsSpan( 8 ), ( uint )( BitConverter.ToUInt32( main, 8 ) + sizeDiff ) ); // RuntimeSize - BitConverter.TryWriteBytes( main.AsSpan( 16 ), ( uint )( BitConverter.ToUInt32( main, 16 ) + sizeDiff ) ); // VertexOffset 1 - BitConverter.TryWriteBytes( main.AsSpan( 20 ), ( uint )( BitConverter.ToUInt32( main, 20 ) + sizeDiff ) ); // VertexOffset 2 - BitConverter.TryWriteBytes( main.AsSpan( 24 ), ( uint )( BitConverter.ToUInt32( main, 24 ) + sizeDiff ) ); // VertexOffset 3 - BitConverter.TryWriteBytes( main.AsSpan( 28 ), ( uint )( BitConverter.ToUInt32( main, 28 ) + sizeDiff ) ); // IndexOffset 1 - BitConverter.TryWriteBytes( main.AsSpan( 32 ), ( uint )( BitConverter.ToUInt32( main, 32 ) + sizeDiff ) ); // IndexOffset 2 - BitConverter.TryWriteBytes( main.AsSpan( 36 ), ( uint )( BitConverter.ToUInt32( main, 36 ) + sizeDiff ) ); // IndexOffset 3 - BitConverter.TryWriteBytes( main.AsSpan( ( int )stringsLengthOffset ), ( uint )( stringsLength + sizeDiff ) ); - } - - private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs ) - { - if( subLhs.Length == subRhs.Length ) - { - return ReplaceEqualSequences( main, subLhs, subRhs ); - } - - var replacements = new List< int >( 4 ); - for( var i = FindSubSequence( main, subLhs ); i >= 0; i = FindSubSequence( main, subLhs, i + subLhs.Length ) ) - { - replacements.Add( i ); - } - - var sizeDiff = ( subRhs.Length - subLhs.Length ) * replacements.Count; - var ret = new byte[main.Length + sizeDiff]; - - var last = 0; - var totalLength = 0; - foreach( var i in replacements ) - { - var length = i - last; - main.AsSpan( last, length ).CopyTo( ret.AsSpan( totalLength ) ); - totalLength += length; - subRhs.CopyTo( ret.AsSpan( totalLength ) ); - totalLength += subRhs.Length; - last = i + subLhs.Length; - } - - main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) ); - ReplaceOffsetsAndSizes( ret, sizeDiff ); - main = ret; - return replacements.Count; - } - - public static int ChangeMtrl( FullPath file, byte[] from, byte[] to ) + public static int ChangeMtrl( FullPath file, string from, string to ) { if( !file.Exists ) { @@ -136,11 +37,25 @@ public static class ModelChanger try { - var text = File.ReadAllBytes( file.FullName ); - var replaced = ReplaceSubSequences( ref text, from, to ); + var data = File.ReadAllBytes( file.FullName ); + var mdlFile = new MdlFile( data ); + from = string.Format( MaterialFormat, from ); + to = string.Format( MaterialFormat, to ); + + var replaced = 0; + for( var i = 0; i < mdlFile.Materials.Length; ++i ) + { + if( mdlFile.Materials[ i ] == @from ) + { + mdlFile.Materials[ i ] = to; + ++replaced; + } + } + if( replaced > 0 ) { - File.WriteAllBytes( file.FullName, text ); + WriteBackup( file.FullName, data ); + File.WriteAllBytes( file.FullName, mdlFile.Write() ); } return replaced; @@ -154,11 +69,11 @@ public static class ModelChanger public static bool ChangeModMaterials( ModData mod, string from, string to ) { - if( ValidStrings( from, to ) && ConvertName( from, out var lhs ) && ConvertName( to, out var rhs ) ) + if( ValidStrings( from, to ) ) { return mod.Resources.ModFiles .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) - .All( file => ChangeMtrl( file, lhs, rhs ) >= 0 ); + .All( file => ChangeMtrl( file, from, to ) >= 0 ); } PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." ); From 564a4195cbd0c75d9f985bfacfd3d2fe6cd2d52d Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 21 Feb 2022 18:50:13 +0000 Subject: [PATCH 0072/2451] [CI] Updating repo.json for refs/tags/0.4.7.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6a27b27a..a1b977c4 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.6", - "TestingAssemblyVersion": "0.4.7.6", + "AssemblyVersion": "0.4.7.7", + "TestingAssemblyVersion": "0.4.7.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 689a4c73d9a3bae4d7d56191edd158d96a61e5da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Feb 2022 22:04:56 +0100 Subject: [PATCH 0073/2451] Small improvements. --- Penumbra/Interop/MusicManager.cs | 2 +- Penumbra/Mods/ModManagerEditExtensions.cs | 351 ++++++++++----------- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- Penumbra/Util/ModelChanger.cs | 1 - 4 files changed, 176 insertions(+), 180 deletions(-) diff --git a/Penumbra/Interop/MusicManager.cs b/Penumbra/Interop/MusicManager.cs index 45d0c8c4..7c46427c 100644 --- a/Penumbra/Interop/MusicManager.cs +++ b/Penumbra/Interop/MusicManager.cs @@ -24,7 +24,7 @@ namespace Penumbra.Interop PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", musicInitCallLocation.ToInt64(), musicManagerOffset ); _musicManager = *( IntPtr* )( framework + musicManagerOffset ); - PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager ); + PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); } public bool StreamingEnabled diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 6f510f8a..bca0e200 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -2,218 +2,215 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; -using System.Linq; using Dalamud.Logging; -using Dalamud.Plugin; using Penumbra.Mod; using Penumbra.Structs; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +// Extracted to keep the main file a bit more clean. +// Contains all change functions on a specific mod that also require corresponding changes to collections. +public static class ModManagerEditExtensions { - // Extracted to keep the main file a bit more clean. - // Contains all change functions on a specific mod that also require corresponding changes to collections. - public static class ModManagerEditExtensions + public static bool RenameMod( this ModManager manager, string newName, ModData mod ) { - public static bool RenameMod( this ModManager manager, string newName, ModData mod ) + if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) { - if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) - { - return false; - } - - mod.Meta.Name = newName; - mod.SaveMeta(); - - return true; + return false; } - public static bool ChangeSortOrder( this ModManager manager, ModData mod, string newSortOrder ) + mod.Meta.Name = newName; + mod.SaveMeta(); + + return true; + } + + public static bool ChangeSortOrder( this ModManager manager, ModData mod, string newSortOrder ) + { + if( string.Equals( mod.SortOrder.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) { - if( string.Equals(mod.SortOrder.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) + return false; + } + + var inRoot = new SortOrder( manager.StructuredMods, mod.Meta.Name ); + if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) + { + mod.SortOrder = inRoot; + manager.Config.ModSortOrder.Remove( mod.BasePath.Name ); + } + else + { + mod.Move( newSortOrder ); + manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullPath; + } + + manager.Config.Save(); + + return true; + } + + public static bool RenameModFolder( this ModManager manager, ModData mod, DirectoryInfo newDir, bool move = true ) + { + if( move ) + { + newDir.Refresh(); + if( newDir.Exists ) { return false; } - var inRoot = new SortOrder( manager.StructuredMods, mod.Meta.Name ); - if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) + var oldDir = new DirectoryInfo( mod.BasePath.FullName ); + try { - mod.SortOrder = inRoot; - manager.Config.ModSortOrder.Remove( mod.BasePath.Name ); + oldDir.MoveTo( newDir.FullName ); } - else + catch( Exception e ) { - mod.Move( newSortOrder ); - manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullPath; + PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" ); + return false; } + } + manager.Mods.Remove( mod.BasePath.Name ); + manager.Mods[ newDir.Name ] = mod; + + var oldBasePath = mod.BasePath; + mod.BasePath = newDir; + mod.MetaFile = ModData.MetaFileInfo( newDir ); + manager.UpdateMod( mod ); + + if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) + { + manager.Config.ModSortOrder[ newDir.Name ] = manager.Config.ModSortOrder[ oldBasePath.Name ]; + manager.Config.ModSortOrder.Remove( oldBasePath.Name ); manager.Config.Save(); - - return true; } - public static bool RenameModFolder( this ModManager manager, ModData mod, DirectoryInfo newDir, bool move = true ) + foreach( var collection in manager.Collections.Collections.Values ) { - if( move ) + if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) { - newDir.Refresh(); - if( newDir.Exists ) - { - return false; - } - - var oldDir = new DirectoryInfo( mod.BasePath.FullName ); - try - { - oldDir.MoveTo( newDir.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" ); - return false; - } - } - - manager.Mods.Remove( mod.BasePath.Name ); - manager.Mods[ newDir.Name ] = mod; - - var oldBasePath = mod.BasePath; - mod.BasePath = newDir; - mod.MetaFile = ModData.MetaFileInfo( newDir ); - manager.UpdateMod( mod ); - - if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) - { - manager.Config.ModSortOrder[ newDir.Name ] = manager.Config.ModSortOrder[ oldBasePath.Name ]; - manager.Config.ModSortOrder.Remove( oldBasePath.Name ); - manager.Config.Save(); - } - - foreach( var collection in manager.Collections.Collections.Values ) - { - if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) - { - collection.Settings[ newDir.Name ] = settings; - collection.Settings.Remove( oldBasePath.Name ); - collection.Save(); - } - - if( collection.Cache != null ) - { - collection.Cache.RemoveMod( newDir ); - collection.AddMod( mod ); - } - } - - return true; - } - - public static bool ChangeModGroup( this ModManager manager, string oldGroupName, string newGroupName, ModData mod, - SelectType type = SelectType.Single ) - { - if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) - { - return false; - } - - if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) ) - { - if( newGroupName.Length > 0 ) - { - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = oldGroup.SelectionType, - Options = oldGroup.Options, - }; - } - - mod.Meta.Groups.Remove( oldGroupName ); - } - else - { - if( newGroupName.Length == 0 ) - { - return false; - } - - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = type, - Options = new List< Option >(), - }; - } - - mod.SaveMeta(); - - foreach( var collection in manager.Collections.Collections.Values ) - { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - continue; - } - - if( newGroupName.Length > 0 ) - { - settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0; - } - - settings.Settings.Remove( oldGroupName ); + collection.Settings[ newDir.Name ] = settings; + collection.Settings.Remove( oldBasePath.Name ); collection.Save(); } - return true; + if( collection.Cache != null ) + { + collection.Cache.RemoveMod( newDir ); + collection.AddMod( mod ); + } } - public static bool RemoveModOption( this ModManager manager, int optionIdx, OptionGroup group, ModData mod ) + return true; + } + + public static bool ChangeModGroup( this ModManager manager, string oldGroupName, string newGroupName, ModData mod, + SelectType type = SelectType.Single ) + { + if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) { - if( optionIdx < 0 || optionIdx >= group.Options.Count ) + return false; + } + + if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) ) + { + if( newGroupName.Length > 0 ) + { + mod.Meta.Groups[ newGroupName ] = new OptionGroup() + { + GroupName = newGroupName, + SelectionType = oldGroup.SelectionType, + Options = oldGroup.Options, + }; + } + + mod.Meta.Groups.Remove( oldGroupName ); + } + else + { + if( newGroupName.Length == 0 ) { return false; } - group.Options.RemoveAt( optionIdx ); - mod.SaveMeta(); - - static int MoveMultiSetting( int oldSetting, int idx ) + mod.Meta.Groups[ newGroupName ] = new OptionGroup() { - var bitmaskFront = ( 1 << idx ) - 1; - var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) ); - return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); - } - - foreach( var collection in manager.Collections.Collections.Values ) - { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - continue; - } - - if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) ) - { - setting = 0; - } - - var newSetting = group.SelectionType switch - { - SelectType.Single => setting >= optionIdx ? setting - 1 : setting, - SelectType.Multi => MoveMultiSetting( setting, optionIdx ), - _ => throw new InvalidEnumArgumentException(), - }; - - if( newSetting != setting ) - { - settings.Settings[ group.GroupName ] = newSetting; - collection.Save(); - if( collection.Cache != null && settings.Enabled ) - { - collection.CalculateEffectiveFileList( manager.TempPath, mod.Resources.MetaManipulations.Count > 0, - collection == manager.Collections.ActiveCollection ); - } - } - } - - return true; + GroupName = newGroupName, + SelectionType = type, + Options = new List< Option >(), + }; } + + mod.SaveMeta(); + + foreach( var collection in manager.Collections.Collections.Values ) + { + if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + continue; + } + + if( newGroupName.Length > 0 ) + { + settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0; + } + + settings.Settings.Remove( oldGroupName ); + collection.Save(); + } + + return true; + } + + public static bool RemoveModOption( this ModManager manager, int optionIdx, OptionGroup group, ModData mod ) + { + if( optionIdx < 0 || optionIdx >= group.Options.Count ) + { + return false; + } + + group.Options.RemoveAt( optionIdx ); + mod.SaveMeta(); + + static int MoveMultiSetting( int oldSetting, int idx ) + { + var bitmaskFront = ( 1 << idx ) - 1; + var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) ); + return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); + } + + foreach( var collection in manager.Collections.Collections.Values ) + { + if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + continue; + } + + if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) ) + { + setting = 0; + } + + var newSetting = group.SelectionType switch + { + SelectType.Single => setting >= optionIdx ? setting - 1 : setting, + SelectType.Multi => MoveMultiSetting( setting, optionIdx ), + _ => throw new InvalidEnumArgumentException(), + }; + + if( newSetting != setting ) + { + settings.Settings[ group.GroupName ] = newSetting; + collection.Save(); + if( collection.Cache != null && settings.Enabled ) + { + collection.CalculateEffectiveFileList( manager.TempPath, mod.Resources.MetaManipulations.Count > 0, + collection == manager.Collections.ActiveCollection ); + } + } + } + + return true; } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 9cd6812b..1f3ad8de 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -94,7 +94,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E603C0 ); + var resourceHandler = *( ResourceManager** )Dalamud.SigScanner.GetStaticAddressFromSig( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0" ); if( resourceHandler == null ) { diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 7cb1f068..8cb65a70 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text; -using System.Windows.Forms; using Dalamud.Logging; using Penumbra.GameData.Files; using Penumbra.Mod; From 7e7e74a5346857328ee161d571c1f1ead6524e9a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Feb 2022 22:05:30 +0100 Subject: [PATCH 0074/2451] Make renaming folders case-sensitive? --- .../TabInstalled/TabInstalledModPanel.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index f9aaa269..5fcbf4e0 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -292,6 +292,31 @@ public partial class SettingsInterface ImGui.CloseCurrentPopup(); } } + else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCulture ) ) + { + var dir = Mod!.Data.BasePath; + DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + var sourceUri = new Uri( dir.FullName ); + var targetUri = new Uri( newDir.FullName ); + if( sourceUri.Equals( targetUri ) ) + { + var tmpFolder = new DirectoryInfo(TempFile.TempFileName( dir.Parent! ).FullName); + if( _modManager.RenameModFolder( Mod.Data, tmpFolder ) ) + { + if( !_modManager.RenameModFolder( Mod.Data, newDir ) ) + { + PluginLog.Error("Could not recapitalize folder after renaming, reverting rename." ); + _modManager.RenameModFolder( Mod.Data, dir ); + } + _selector.ReloadCurrentMod(); + } + ImGui.CloseCurrentPopup(); + } + else + { + ImGui.OpenPopup( LabelOverWriteDir ); + } + } } private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) From 6df82fdf18cd3e36a6f732adb4a70f6e7c2e46c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Feb 2022 14:02:45 +0100 Subject: [PATCH 0075/2451] Make recapitalizing internal mod folders possible without changing behaviour otherwise. --- Penumbra/Mods/ModFileSystem.cs | 429 +++++++++--------- Penumbra/Mods/ModFolder.cs | 12 +- .../TabInstalled/TabInstalledModPanel.cs | 6 +- 3 files changed, 227 insertions(+), 220 deletions(-) diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 0a0dc54f..b0d01bd7 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,260 +1,261 @@ using System; using System.Linq; using Penumbra.Mod; -using Penumbra.Util; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +public delegate void OnModFileSystemChange(); + +public static partial class ModFileSystem { - public delegate void OnModFileSystemChange(); + // The root folder that should be used as the base for all structured mods. + public static ModFolder Root = ModFolder.CreateRoot(); - public static partial class ModFileSystem + // Gets invoked every time the file system changes. + public static event OnModFileSystemChange? ModFileSystemChanged; + + internal static void InvokeChange() + => ModFileSystemChanged?.Invoke(); + + // Find a specific mod folder by its path from Root. + // Returns true if the folder was found, and false if not. + // The out parameter will contain the furthest existing folder. + public static bool Find( string path, out ModFolder folder ) { - // The root folder that should be used as the base for all structured mods. - public static ModFolder Root = ModFolder.CreateRoot(); - - // Gets invoked every time the file system changes. - public static event OnModFileSystemChange? ModFileSystemChanged; - - internal static void InvokeChange() - => ModFileSystemChanged?.Invoke(); - - // Find a specific mod folder by its path from Root. - // Returns true if the folder was found, and false if not. - // The out parameter will contain the furthest existing folder. - public static bool Find( string path, out ModFolder folder ) + var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); + folder = Root; + foreach( var part in split ) { - var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - folder = Root; - foreach( var part in split ) + if( !folder.FindSubFolder( part, out folder ) ) { - if( !folder.FindSubFolder( part, out folder ) ) - { - return false; - } + return false; } + } + return true; + } + + // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. + // Saves and returns true if anything changed. + public static bool Rename( this ModData mod, string newName ) + { + if( RenameNoSave( mod, newName ) ) + { + SaveMod( mod ); return true; } - // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. - // Saves and returns true if anything changed. - public static bool Rename( this ModData mod, string newName ) - { - if( RenameNoSave( mod, newName ) ) - { - SaveMod( mod ); - return true; - } + return false; + } - return false; + // Rename the target folder, merging it and its subfolders if the new name already exists. + // Saves all mods manipulated thus, and returns true if anything changed. + public static bool Rename( this ModFolder target, string newName ) + { + if( RenameNoSave( target, newName ) ) + { + SaveModChildren( target ); + return true; } - // Rename the target folder, merging it and its subfolders if the new name already exists. - // Saves all mods manipulated thus, and returns true if anything changed. - public static bool Rename( this ModFolder target, string newName ) - { - if( RenameNoSave( target, newName ) ) - { - SaveModChildren( target ); - return true; - } + return false; + } - return false; + // Move a single mod to the target folder. + // Returns true and saves if anything changed. + public static bool Move( this ModData mod, ModFolder target ) + { + if( MoveNoSave( mod, target ) ) + { + SaveMod( mod ); + return true; } - // Move a single mod to the target folder. - // Returns true and saves if anything changed. - public static bool Move( this ModData mod, ModFolder target ) - { - if( MoveNoSave( mod, target ) ) - { - SaveMod( mod ); - return true; - } + return false; + } - return false; + // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. + // Creates all necessary Subfolders. + public static void Move( this ModData mod, string sortOrder ) + { + var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); + var folder = Root; + for( var i = 0; i < split.Length - 1; ++i ) + { + folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1; } - // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. - // Creates all necessary Subfolders. - public static void Move( this ModData mod, string sortOrder ) + if( MoveNoSave( mod, folder ) | RenameNoSave( mod, split.Last() ) ) { - var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - var folder = Root; - for( var i = 0; i < split.Length - 1; ++i ) - { - folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1; - } - - if( MoveNoSave( mod, folder ) | RenameNoSave( mod, split.Last() ) ) - { - SaveMod( mod ); - } - } - - // Moves folder to target. - // If an identically named subfolder of target already exists, merges instead. - // Root is not movable. - public static bool Move( this ModFolder folder, ModFolder target ) - { - if( MoveNoSave( folder, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } - - // Merge source with target, moving all direct mod children of source to target, - // and moving all subfolders of source to target, or merging them with targets subfolders if they exist. - // Returns true and saves if anything changed. - public static bool Merge( this ModFolder source, ModFolder target ) - { - if( MergeNoSave( source, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; + SaveMod( mod ); } } - // Internal stuff. - public static partial class ModFileSystem + // Moves folder to target. + // If an identically named subfolder of target already exists, merges instead. + // Root is not movable. + public static bool Move( this ModFolder folder, ModFolder target ) { - // Reset all sort orders for all descendants of the given folder. - // Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries. - private static void SaveModChildren( ModFolder target ) + if( MoveNoSave( folder, target ) ) { - foreach( var mod in target.AllMods( true ) ) - { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( ModData mod ) - { - if( ReferenceEquals( mod.SortOrder.ParentFolder, Root ) - && string.Equals( mod.SortOrder.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) - { - Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); - } - else - { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - private static bool RenameNoSave( this ModFolder target, string newName ) - { - if( ReferenceEquals( target, Root ) ) - { - throw new InvalidOperationException( "Can not rename root." ); - } - - newName = newName.Replace( '/', '\\' ); - if( target.Name == newName ) - { - return false; - } - - if( target.Parent!.FindSubFolder( newName, out var preExisting ) ) - { - MergeNoSave( target, preExisting ); - } - else - { - var parent = target.Parent; - parent.RemoveFolderIgnoreEmpty( target ); - target.Name = newName; - parent.FindOrAddSubFolder( target ); - } - + SaveModChildren( target ); return true; } - private static bool RenameNoSave( ModData mod, string newName ) - { - newName = newName.Replace( '/', '\\' ); - if( mod.SortOrder.SortOrderName == newName ) - { - return false; - } + return false; + } - mod.SortOrder.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.SortOrder = new SortOrder( mod.SortOrder.ParentFolder, newName ); - mod.SortOrder.ParentFolder.AddMod( mod ); + // Merge source with target, moving all direct mod children of source to target, + // and moving all subfolders of source to target, or merging them with targets subfolders if they exist. + // Returns true and saves if anything changed. + public static bool Merge( this ModFolder source, ModFolder target ) + { + if( MergeNoSave( source, target ) ) + { + SaveModChildren( target ); return true; } - private static bool MoveNoSave( ModData mod, ModFolder target ) - { - var oldParent = mod.SortOrder.ParentFolder; - if( ReferenceEquals( target, oldParent ) ) - { - return false; - } + return false; + } +} - oldParent.RemoveMod( mod ); - mod.SortOrder = new SortOrder( target, mod.SortOrder.SortOrderName ); - target.AddMod( mod ); - return true; +// Internal stuff. +public static partial class ModFileSystem +{ + // Reset all sort orders for all descendants of the given folder. + // Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries. + private static void SaveModChildren( ModFolder target ) + { + foreach( var mod in target.AllMods( true ) ) + { + Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; } - private static bool MergeNoSave( ModFolder source, ModFolder target ) + Penumbra.Config.Save(); + InvokeChange(); + } + + // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. + private static void SaveMod( ModData mod ) + { + if( ReferenceEquals( mod.SortOrder.ParentFolder, Root ) + && string.Equals( mod.SortOrder.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) { - if( ReferenceEquals( source, target ) ) - { - return false; - } - - var any = false; - while( source.SubFolders.Count > 0 ) - { - any |= MoveNoSave( source.SubFolders.First(), target ); - } - - while( source.Mods.Count > 0 ) - { - any |= MoveNoSave( source.Mods.First(), target ); - } - - source.Parent?.RemoveSubFolder( source ); - - return any || source.Parent != null; + Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); + } + else + { + Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; } - private static bool MoveNoSave( ModFolder folder, ModFolder target ) + Penumbra.Config.Save(); + InvokeChange(); + } + + private static bool RenameNoSave( this ModFolder target, string newName ) + { + if( ReferenceEquals( target, Root ) ) { - // Moving a folder into itself is not permitted. - if( ReferenceEquals( folder, target ) ) - { - return false; - } - - if( ReferenceEquals( target, folder.Parent! ) ) - { - return false; - } - - folder.Parent!.RemoveSubFolder( folder ); - var subFolderIdx = target.FindOrAddSubFolder( folder ); - if( subFolderIdx > 0 ) - { - var main = target.SubFolders[ subFolderIdx ]; - MergeNoSave( folder, main ); - } - - return true; + throw new InvalidOperationException( "Can not rename root." ); } + + newName = newName.Replace( '/', '\\' ); + if( target.Name == newName ) + { + return false; + } + + ModFolder.FolderComparer.CompareType = StringComparison.InvariantCulture; + if( target.Parent!.FindSubFolder( newName, out var preExisting ) ) + { + MergeNoSave( target, preExisting ); + ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; + } + else + { + ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; + var parent = target.Parent; + parent.RemoveFolderIgnoreEmpty( target ); + target.Name = newName; + parent.FindOrAddSubFolder( target ); + } + + return true; + } + + private static bool RenameNoSave( ModData mod, string newName ) + { + newName = newName.Replace( '/', '\\' ); + if( mod.SortOrder.SortOrderName == newName ) + { + return false; + } + + mod.SortOrder.ParentFolder.RemoveModIgnoreEmpty( mod ); + mod.SortOrder = new SortOrder( mod.SortOrder.ParentFolder, newName ); + mod.SortOrder.ParentFolder.AddMod( mod ); + return true; + } + + private static bool MoveNoSave( ModData mod, ModFolder target ) + { + var oldParent = mod.SortOrder.ParentFolder; + if( ReferenceEquals( target, oldParent ) ) + { + return false; + } + + oldParent.RemoveMod( mod ); + mod.SortOrder = new SortOrder( target, mod.SortOrder.SortOrderName ); + target.AddMod( mod ); + return true; + } + + private static bool MergeNoSave( ModFolder source, ModFolder target ) + { + if( ReferenceEquals( source, target ) ) + { + return false; + } + + var any = false; + while( source.SubFolders.Count > 0 ) + { + any |= MoveNoSave( source.SubFolders.First(), target ); + } + + while( source.Mods.Count > 0 ) + { + any |= MoveNoSave( source.Mods.First(), target ); + } + + source.Parent?.RemoveSubFolder( source ); + + return any || source.Parent != null; + } + + private static bool MoveNoSave( ModFolder folder, ModFolder target ) + { + // Moving a folder into itself is not permitted. + if( ReferenceEquals( folder, target ) ) + { + return false; + } + + if( ReferenceEquals( target, folder.Parent! ) ) + { + return false; + } + + folder.Parent!.RemoveSubFolder( folder ); + var subFolderIdx = target.FindOrAddSubFolder( folder ); + if( subFolderIdx > 0 ) + { + var main = target.SubFolders[ subFolderIdx ]; + MergeNoSave( folder, main ); + } + + return true; } } \ No newline at end of file diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs index 4b4b6422..76768be0 100644 --- a/Penumbra/Mods/ModFolder.cs +++ b/Penumbra/Mods/ModFolder.cs @@ -148,15 +148,19 @@ namespace Penumbra.Mods internal class ModFolderComparer : IComparer< ModFolder > { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. public int Compare( ModFolder? x, ModFolder? y ) => ReferenceEquals( x, y ) ? 0 - : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, StringComparison.InvariantCultureIgnoreCase ); + : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); } internal class ModDataComparer : IComparer< ModData > { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. public int Compare( ModData? x, ModData? y ) @@ -166,7 +170,7 @@ namespace Penumbra.Mods return 0; } - var cmp = string.Compare( x?.SortOrder.SortOrderName, y?.SortOrder.SortOrderName, StringComparison.InvariantCultureIgnoreCase ); + var cmp = string.Compare( x?.SortOrder.SortOrderName, y?.SortOrder.SortOrderName, CompareType ); if( cmp != 0 ) { return cmp; @@ -176,8 +180,8 @@ namespace Penumbra.Mods } } - private static readonly ModFolderComparer FolderComparer = new(); - private static readonly ModDataComparer ModComparer = new(); + internal static readonly ModFolderComparer FolderComparer = new(); + internal static readonly ModDataComparer ModComparer = new(); // Get an enumerator for actually sorted objects instead of folder-first objects. private IEnumerable< object > GetSortedEnumerator() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 5fcbf4e0..8be04f51 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -300,16 +300,18 @@ public partial class SettingsInterface var targetUri = new Uri( newDir.FullName ); if( sourceUri.Equals( targetUri ) ) { - var tmpFolder = new DirectoryInfo(TempFile.TempFileName( dir.Parent! ).FullName); + var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); if( _modManager.RenameModFolder( Mod.Data, tmpFolder ) ) { if( !_modManager.RenameModFolder( Mod.Data, newDir ) ) { - PluginLog.Error("Could not recapitalize folder after renaming, reverting rename." ); + PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); _modManager.RenameModFolder( Mod.Data, dir ); } + _selector.ReloadCurrentMod(); } + ImGui.CloseCurrentPopup(); } else From bf40c2a3cb55d3d7fb4b850f3e75c27068b341af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 8 Mar 2022 10:09:34 +0100 Subject: [PATCH 0076/2451] Add option to expand or collapse all sub-folders in the right-click context for mod folders. --- .../TabInstalled/TabInstalledSelector.cs | 1379 +++++++++-------- 1 file changed, 705 insertions(+), 674 deletions(-) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index ed219462..dbecb883 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -14,758 +14,789 @@ using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + // Constants + private partial class Selector { - // Constants - private partial class Selector + private const string LabelSelectorList = "##availableModList"; + private const string LabelModFilter = "##ModFilter"; + private const string LabelAddModPopup = "AddModPopup"; + private const string LabelModHelpPopup = "Help##Selector"; + + private const string TooltipModFilter = + "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; + + private const string TooltipDelete = "Delete the selected mod"; + private const string TooltipAdd = "Add an empty mod"; + private const string DialogDeleteMod = "PenumbraDeleteMod"; + private const string ButtonYesDelete = "Yes, delete it"; + private const string ButtonNoDelete = "No, keep it"; + + private const float SelectorPanelWidth = 240f; + + private static readonly Vector2 SelectorButtonSizes = new(100, 0); + private static readonly Vector2 HelpButtonSizes = new(40, 0); + + private static readonly Vector4 DeleteModNameColor = new(0.7f, 0.1f, 0.1f, 1); + } + + // Buttons + private partial class Selector + { + // === Delete === + private int? _deleteIndex; + + private void DrawModTrashButton() { - private const string LabelSelectorList = "##availableModList"; - private const string LabelModFilter = "##ModFilter"; - private const string LabelAddModPopup = "AddModPopup"; - private const string LabelModHelpPopup = "Help##Selector"; + using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - private const string TooltipModFilter = - "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; + if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) + { + _deleteIndex = _index; + } - private const string TooltipDelete = "Delete the selected mod"; - private const string TooltipAdd = "Add an empty mod"; - private const string DialogDeleteMod = "PenumbraDeleteMod"; - private const string ButtonYesDelete = "Yes, delete it"; - private const string ButtonNoDelete = "No, keep it"; + raii.Pop(); - private const float SelectorPanelWidth = 240f; - - private static readonly Vector2 SelectorButtonSizes = new( 100, 0 ); - private static readonly Vector2 HelpButtonSizes = new( 40, 0 ); - - private static readonly Vector4 DeleteModNameColor = new( 0.7f, 0.1f, 0.1f, 1 ); + ImGuiCustom.HoverTooltip( TooltipDelete ); } - // Buttons - private partial class Selector + private void DrawDeleteModal() { - // === Delete === - private int? _deleteIndex; - - private void DrawModTrashButton() + if( _deleteIndex == null ) { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) - { - _deleteIndex = _index; - } - - raii.Pop(); - - ImGuiCustom.HoverTooltip( TooltipDelete ); + return; } - private void DrawDeleteModal() + ImGui.OpenPopup( DialogDeleteMod ); + + var _ = true; + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); + if( !ret ) { - if( _deleteIndex == null ) - { - return; - } - - ImGui.OpenPopup( DialogDeleteMod ); - - var _ = true; - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); - if( !ret ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( Mod == null ) - { - _deleteIndex = null; - ImGui.CloseCurrentPopup(); - return; - } - - ImGui.Text( "Are you sure you want to delete the following mod:" ); - var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); - ImGui.Dummy( halfLine ); - ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); - ImGui.Dummy( halfLine ); - - var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( ButtonYesDelete, buttonSize ) ) - { - ImGui.CloseCurrentPopup(); - var mod = Mod; - Cache.RemoveMod( mod ); - _modManager.DeleteMod( mod.Data.BasePath ); - ModFileSystem.InvokeChange(); - ClearSelection(); - } - - ImGui.SameLine(); - - if( ImGui.Button( ButtonNoDelete, buttonSize ) ) - { - ImGui.CloseCurrentPopup(); - _deleteIndex = null; - } + return; } - // === Add === - private bool _modAddKeyboardFocus = true; + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - private void DrawModAddButton() + if( Mod == null ) { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) - { - _modAddKeyboardFocus = true; - ImGui.OpenPopup( LabelAddModPopup ); - } - - raii.Pop(); - - ImGuiCustom.HoverTooltip( TooltipAdd ); - - DrawModAddPopup(); + _deleteIndex = null; + ImGui.CloseCurrentPopup(); + return; } - private void DrawModAddPopup() + ImGui.Text( "Are you sure you want to delete the following mod:" ); + var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); + ImGui.Dummy( halfLine ); + ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); + ImGui.Dummy( halfLine ); + + var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); + if( ImGui.Button( ButtonYesDelete, buttonSize ) ) { - if( !ImGui.BeginPopup( LabelAddModPopup ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( _modAddKeyboardFocus ) - { - ImGui.SetKeyboardFocusHere(); - _modAddKeyboardFocus = false; - } - - var newName = ""; - if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - try - { - var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), - newName ); - var modMeta = new ModMeta - { - Author = "Unknown", - Name = newName.Replace( '/', '\\' ), - Description = string.Empty, - }; - - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - modMeta.SaveToFile( metaFile ); - _modManager.AddMod( newDir ); - ModFileSystem.InvokeChange(); - SelectModOnUpdate( newDir.Name ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); - } - - ImGui.CloseCurrentPopup(); - } - - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } + ImGui.CloseCurrentPopup(); + var mod = Mod; + Cache.RemoveMod( mod ); + _modManager.DeleteMod( mod.Data.BasePath ); + ModFileSystem.InvokeChange(); + ClearSelection(); } - // === Help === - private void DrawModHelpButton() + ImGui.SameLine(); + + if( ImGui.Button( ButtonNoDelete, buttonSize ) ) { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) - { - ImGui.OpenPopup( LabelModHelpPopup ); - } - } - - private static void DrawModHelpPopup() - { - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), - ImGuiCond.Appearing ); - var _ = true; - if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information." ); - ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); - ImGui.Indent(); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.Text( "Enabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.NewModColor ), - "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), - "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.ConflictingModColor ), - "Enabled and conflicting with another enabled Mod on the same priority." ); - ImGui.Unindent(); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); - ImGui.Indent(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); - ImGui.BulletText( - "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" - + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" - + "where ModName will be sorted as if it was the string '1'." ); - ImGui.Unindent(); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); - ImGui.BulletText( "Right-clicking a folder opens a context menu." ); - ImGui.Indent(); - ImGui.BulletText( - "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); - ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); - ImGui.Indent(); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Management" ); - ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); - ImGui.BulletText( "You can add a completely empty mod with the plus button." ); - ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); - ImGui.BulletText( - "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); - ImGui.BulletText( - "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); - ImGui.SameLine(); - if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) - { - ImGui.CloseCurrentPopup(); - } - } - - // === Main === - private void DrawModsSelectorButtons() - { - // Selector controls - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) - .Push( ImGuiStyleVar.FrameRounding, 0 ); - - DrawModAddButton(); - ImGui.SameLine(); - DrawModHelpButton(); - ImGui.SameLine(); - DrawModTrashButton(); - } - } - - // Filters - private partial class Selector - { - private string _modFilterInput = ""; - - private void DrawTextFilter() - { - ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); - var tmp = _modFilterInput; - if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) - { - Cache.SetTextFilter( tmp ); - _modFilterInput = tmp; - } - - ImGuiCustom.HoverTooltip( TooltipModFilter ); - } - - private void DrawToggleFilter() - { - if( ImGui.BeginCombo( "##ModStateFilter", "", - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); - var flags = ( int )Cache.StateFilter; - foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) - { - ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); - } - - Cache.StateFilter = ( ModFilter )flags; - } - - ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); - } - - private void DrawModsSelectorFilter() - { - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); - DrawTextFilter(); - ImGui.SameLine(); - DrawToggleFilter(); - } - } - - // Drag'n Drop - private partial class Selector - { - private const string DraggedModLabel = "ModIndex"; - private const string DraggedFolderLabel = "FolderName"; - - private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); - - private static unsafe bool IsDropping( string name ) - => ImGui.AcceptDragDropPayload( name ).NativePtr != null; - - private void DragDropTarget( ModFolder folder ) - { - if( !ImGui.BeginDragDropTarget() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); - - if( IsDropping( DraggedModLabel ) ) - { - var payload = ImGui.GetDragDropPayload(); - var modIndex = Marshal.ReadInt32( payload.Data ); - var mod = Cache.GetMod( modIndex ).Item1; - mod?.Data.Move( folder ); - } - else if( IsDropping( DraggedFolderLabel ) ) - { - var payload = ImGui.GetDragDropPayload(); - var folderName = Marshal.PtrToStringUni( payload.Data ); - if( ModFileSystem.Find( folderName!, out var droppedFolder ) - && !ReferenceEquals( droppedFolder, folder ) - && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) - { - droppedFolder.Move( folder ); - } - } - } - - private void DragDropSourceFolder( ModFolder folder ) - { - if( !ImGui.BeginDragDropSource() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); - - var folderName = folder.FullName; - var ptr = Marshal.StringToHGlobalUni( folderName ); - ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); - ImGui.Text( $"Moving {folderName}..." ); - } - - private void DragDropSourceMod( int modIndex, string modName ) - { - if( !ImGui.BeginDragDropSource() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); - - Marshal.WriteInt32( _dragDropPayload, modIndex ); - ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); - ImGui.Text( $"Moving {modName}..." ); - } - - ~Selector() - => Marshal.FreeHGlobal( _dragDropPayload ); - } - - // Selection - private partial class Selector - { - public Mod.Mod? Mod { get; private set; } - private int _index; - private string _nextDir = string.Empty; - - private void SetSelection( int idx, Mod.Mod? info ) - { - Mod = info; - if( idx != _index ) - { - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - } - - _index = idx; + ImGui.CloseCurrentPopup(); _deleteIndex = null; } - - private void SetSelection( int idx ) - { - if( idx >= Cache.Count ) - { - idx = -1; - } - - if( idx < 0 ) - { - SetSelection( 0, null ); - } - else - { - SetSelection( idx, Cache.GetMod( idx ).Item1 ); - } - } - - public void ReloadSelection() - => SetSelection( _index, Cache.GetMod( _index ).Item1 ); - - public void ClearSelection() - => SetSelection( -1 ); - - public void SelectModOnUpdate( string directory ) - => _nextDir = directory; - - public void SelectModByDir( string name ) - { - var (mod, idx) = Cache.GetModByBasePath( name ); - SetSelection( idx, mod ); - } - - public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - { - if( Mod == null ) - { - return; - } - - if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) - { - SelectModOnUpdate( Mod.Data.BasePath.Name ); - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - } - } - - public void SaveCurrentMod() - => Mod?.Data.SaveMeta(); } - // Right-Clicks - private partial class Selector + // === Add === + private bool _modAddKeyboardFocus = true; + + private void DrawModAddButton() { - // === Mod === - private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) + using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); + + if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) { - if( !ImGui.BeginPopup( popupName ) ) + _modAddKeyboardFocus = true; + ImGui.OpenPopup( LabelAddModPopup ); + } + + raii.Pop(); + + ImGuiCustom.HoverTooltip( TooltipAdd ); + + DrawModAddPopup(); + } + + private void DrawModAddPopup() + { + if( !ImGui.BeginPopup( LabelAddModPopup ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + + if( _modAddKeyboardFocus ) + { + ImGui.SetKeyboardFocusHere(); + _modAddKeyboardFocus = false; + } + + var newName = ""; + if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + try { - return; + var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), + newName ); + var modMeta = new ModMeta + { + Author = "Unknown", + Name = newName.Replace( '/', '\\' ), + Description = string.Empty, + }; + + var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); + modMeta.SaveToFile( metaFile ); + _modManager.AddMod( newDir ); + ModFileSystem.InvokeChange(); + SelectModOnUpdate( newDir.Name ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + ImGui.CloseCurrentPopup(); + } - if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) + if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + { + ImGui.CloseCurrentPopup(); + } + } + + // === Help === + private void DrawModHelpButton() + { + using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); + if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) + { + ImGui.OpenPopup( LabelModHelpPopup ); + } + } + + private static void DrawModHelpPopup() + { + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), + ImGuiCond.Appearing ); + var _ = true; + if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Selector" ); + ImGui.BulletText( "Select a mod to obtain more information." ); + ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); + ImGui.Indent(); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.Text( "Enabled in the current collection." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.NewModColor ), + "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), + "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.ConflictingModColor ), + "Enabled and conflicting with another enabled Mod on the same priority." ); + ImGui.Unindent(); + ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); + ImGui.Indent(); + ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); + ImGui.BulletText( + "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" + + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" + + "where ModName will be sorted as if it was the string '1'." ); + ImGui.Unindent(); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); + ImGui.BulletText( "Right-clicking a folder opens a context menu." ); + ImGui.Indent(); + ImGui.BulletText( + "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); + ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); + ImGui.Unindent(); + ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); + ImGui.Indent(); + ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); + ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); + ImGui.Unindent(); + ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Management" ); + ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); + ImGui.BulletText( "You can add a completely empty mod with the plus button." ); + ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); + ImGui.BulletText( + "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); + ImGui.BulletText( + "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); + ImGui.SameLine(); + if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) + { + ImGui.CloseCurrentPopup(); + } + } + + // === Main === + private void DrawModsSelectorButtons() + { + // Selector controls + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) + .Push( ImGuiStyleVar.FrameRounding, 0 ); + + DrawModAddButton(); + ImGui.SameLine(); + DrawModHelpButton(); + ImGui.SameLine(); + DrawModTrashButton(); + } + } + + // Filters + private partial class Selector + { + private string _modFilterInput = ""; + + private void DrawTextFilter() + { + ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); + var tmp = _modFilterInput; + if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) + { + Cache.SetTextFilter( tmp ); + _modFilterInput = tmp; + } + + ImGuiCustom.HoverTooltip( TooltipModFilter ); + } + + private void DrawToggleFilter() + { + if( ImGui.BeginCombo( "##ModStateFilter", "", + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + var flags = ( int )Cache.StateFilter; + foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) { - ImGui.CloseCurrentPopup(); + ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); } - if( firstOpen ) + Cache.StateFilter = ( ModFilter )flags; + } + + ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); + } + + private void DrawModsSelectorFilter() + { + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); + DrawTextFilter(); + ImGui.SameLine(); + DrawToggleFilter(); + } + } + + // Drag'n Drop + private partial class Selector + { + private const string DraggedModLabel = "ModIndex"; + private const string DraggedFolderLabel = "FolderName"; + + private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); + + private static unsafe bool IsDropping( string name ) + => ImGui.AcceptDragDropPayload( name ).NativePtr != null; + + private void DragDropTarget( ModFolder folder ) + { + if( !ImGui.BeginDragDropTarget() ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); + + if( IsDropping( DraggedModLabel ) ) + { + var payload = ImGui.GetDragDropPayload(); + var modIndex = Marshal.ReadInt32( payload.Data ); + var mod = Cache.GetMod( modIndex ).Item1; + mod?.Data.Move( folder ); + } + else if( IsDropping( DraggedFolderLabel ) ) + { + var payload = ImGui.GetDragDropPayload(); + var folderName = Marshal.PtrToStringUni( payload.Data ); + if( ModFileSystem.Find( folderName!, out var droppedFolder ) + && !ReferenceEquals( droppedFolder, folder ) + && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) { - ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.FullPath.Length - 1 ); + droppedFolder.Move( folder ); + } + } + } + + private void DragDropSourceFolder( ModFolder folder ) + { + if( !ImGui.BeginDragDropSource() ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); + + var folderName = folder.FullName; + var ptr = Marshal.StringToHGlobalUni( folderName ); + ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); + ImGui.Text( $"Moving {folderName}..." ); + } + + private void DragDropSourceMod( int modIndex, string modName ) + { + if( !ImGui.BeginDragDropSource() ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); + + Marshal.WriteInt32( _dragDropPayload, modIndex ); + ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); + ImGui.Text( $"Moving {modName}..." ); + } + + ~Selector() + => Marshal.FreeHGlobal( _dragDropPayload ); + } + + // Selection + private partial class Selector + { + public Mod.Mod? Mod { get; private set; } + private int _index; + private string _nextDir = string.Empty; + + private void SetSelection( int idx, Mod.Mod? info ) + { + Mod = info; + if( idx != _index ) + { + _base._menu.InstalledTab.ModPanel.Details.ResetState(); + } + + _index = idx; + _deleteIndex = null; + } + + private void SetSelection( int idx ) + { + if( idx >= Cache.Count ) + { + idx = -1; + } + + if( idx < 0 ) + { + SetSelection( 0, null ); + } + else + { + SetSelection( idx, Cache.GetMod( idx ).Item1 ); + } + } + + public void ReloadSelection() + => SetSelection( _index, Cache.GetMod( _index ).Item1 ); + + public void ClearSelection() + => SetSelection( -1 ); + + public void SelectModOnUpdate( string directory ) + => _nextDir = directory; + + public void SelectModByDir( string name ) + { + var (mod, idx) = Cache.GetModByBasePath( name ); + SetSelection( idx, mod ); + } + + public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + { + if( Mod == null ) + { + return; + } + + if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) + { + SelectModOnUpdate( Mod.Data.BasePath.Name ); + _base._menu.InstalledTab.ModPanel.Details.ResetState(); + } + } + + public void SaveCurrentMod() + => Mod?.Data.SaveMeta(); + } + + // Right-Clicks + private partial class Selector + { + // === Mod === + private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) + { + if( !ImGui.BeginPopup( popupName ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + + if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) + { + ImGui.CloseCurrentPopup(); + } + + if( firstOpen ) + { + ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.FullPath.Length - 1 ); + } + } + + // === Folder === + private string _newFolderName = string.Empty; + private int _expandIndex = -1; + private bool _expandCollapse; + private bool _currentlyExpanding; + + private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) + { + var change = false; + var metaManips = false; + foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) + { + var (mod, _, _) = Cache.GetMod( currentIdx++ ); + if( mod != null ) + { + change |= mod.Settings.Enabled != toWhat; + mod!.Settings.Enabled = toWhat; + metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; } } - // === Folder === - private string _newFolderName = string.Empty; - - private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) + if( !change ) { - var change = false; - var metaManips = false; - foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) + return; + } + + Cache.TriggerFilterReset(); + var collection = _modManager.Collections.CurrentCollection; + if( collection.Cache != null ) + { + collection.CalculateEffectiveFileList( _modManager.TempPath, metaManips, + collection == _modManager.Collections.ActiveCollection ); + } + + collection.Save(); + } + + private void DrawRenameFolderInput( ModFolder folder ) + { + ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + return; + } + + if( _newFolderName.Any() ) + { + folder.Rename( _newFolderName ); + } + else + { + folder.Merge( folder.Parent! ); + } + + _newFolderName = string.Empty; + } + + private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) + { + if( !ImGui.BeginPopup( treeName ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + + if( ImGui.MenuItem( "Expand All Descendants" ) ) + { + _expandIndex = currentIdx; + _expandCollapse = false; + } + + if( ImGui.MenuItem( "Collapse All Descendants" ) ) + { + _expandIndex = currentIdx; + _expandCollapse = true; + } + + if( ImGui.MenuItem( "Enable All Descendants" ) ) + { + ChangeStatusOfChildren( folder, currentIdx, true ); + } + + if( ImGui.MenuItem( "Disable All Descendants" ) ) + { + ChangeStatusOfChildren( folder, currentIdx, false ); + } + + ImGuiHelpers.ScaledDummy( 0, 10 ); + DrawRenameFolderInput( folder ); + } + } + + // Main-Interface + private partial class Selector + { + private readonly SettingsInterface _base; + private readonly ModManager _modManager; + public readonly ModListCache Cache; + + private float _selectorScalingFactor = 1; + + public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) + { + _base = ui; + _modManager = Service< ModManager >.Get(); + Cache = new ModListCache( _modManager, newMods ); + } + + private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) + { + if( collection == ModCollection.Empty + || collection == _modManager.Collections.CurrentCollection ) + { + using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); + ImGui.Button( label, Vector2.UnitX * size ); + } + else if( ImGui.Button( label, Vector2.UnitX * size ) ) + { + _base._menu.CollectionsTab.SetCurrentCollection( collection ); + } + + ImGuiCustom.HoverTooltip( + $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); + } + + private void DrawHeaderBar() + { + const float size = 200; + + DrawModsSelectorFilter(); + var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; + var comboSize = size * ImGui.GetIO().FontGlobalScale; + var offset = comboSize + textSize; + + var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth() + - offset + - SelectorPanelWidth * _selectorScalingFactor + - 4 * ImGui.GetStyle().ItemSpacing.X ) + / 2, 5f ); + ImGui.SameLine(); + DrawCollectionButton( "Default", "default", buttonSize, _modManager.Collections.DefaultCollection ); + + ImGui.SameLine(); + DrawCollectionButton( "Forced", "forced", buttonSize, _modManager.Collections.ForcedCollection ); + + ImGui.SameLine(); + ImGui.SetNextItemWidth( comboSize ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); + } + + private void DrawFolderContent( ModFolder folder, ref int idx ) + { + // Collection may be manipulated. + foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) + { + if( item is ModFolder sub ) { - var (mod, _, _) = Cache.GetMod( currentIdx++ ); - if( mod != null ) + var (visible, _) = Cache.GetFolder( sub ); + if( visible ) { - change |= mod.Settings.Enabled != toWhat; - mod!.Settings.Enabled = toWhat; - metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; + DrawModFolder( sub, ref idx ); + } + else + { + idx += sub.TotalDescendantMods(); } } - - if( !change ) + else if( item is ModData _ ) { - return; + var (mod, visible, color) = Cache.GetMod( idx ); + if( mod != null && visible ) + { + DrawMod( mod, idx++, color ); + } + else + { + ++idx; + } } + } + } - Cache.TriggerFilterReset(); - var collection = _modManager.Collections.CurrentCollection; - if( collection.Cache != null ) - { - collection.CalculateEffectiveFileList( _modManager.TempPath, metaManips, - collection == _modManager.Collections.ActiveCollection ); - } + private void DrawModFolder( ModFolder folder, ref int idx ) + { + var treeName = $"{folder.Name}##{folder.FullName}"; + var open = ImGui.TreeNodeEx( treeName ); + using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); - collection.Save(); + if( idx == _expandIndex ) + { + _currentlyExpanding = true; } - private void DrawRenameFolderInput( ModFolder folder ) + if( _currentlyExpanding ) { - ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - return; - } - - if( _newFolderName.Any() ) - { - folder.Rename( _newFolderName ); - } - else - { - folder.Merge( folder.Parent! ); - } + ImGui.SetNextItemOpen( !_expandCollapse ); + } + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { _newFolderName = string.Empty; + ImGui.OpenPopup( treeName ); } - private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) + DrawFolderContextMenu( folder, idx, treeName ); + DragDropTarget( folder ); + DragDropSourceFolder( folder ); + + if( open ) { - if( !ImGui.BeginPopup( treeName ) ) - { - return; - } + DrawFolderContent( folder, ref idx ); + } + else + { + idx += folder.TotalDescendantMods(); + } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( ImGui.MenuItem( "Enable All Descendants" ) ) - { - ChangeStatusOfChildren( folder, currentIdx, true ); - } - - if( ImGui.MenuItem( "Disable All Descendants" ) ) - { - ChangeStatusOfChildren( folder, currentIdx, false ); - } - - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawRenameFolderInput( folder ); + if( idx == _expandIndex ) + { + _currentlyExpanding = false; + _expandIndex = -1; } } - // Main-Interface - private partial class Selector + private void DrawMod( Mod.Mod mod, int modIndex, uint color ) { - private readonly SettingsInterface _base; - private readonly ModManager _modManager; - public readonly ModListCache Cache; + using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); - private float _selectorScalingFactor = 1; + var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); + colorRaii.Pop(); - public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) + var popupName = $"##SortOrderPopup{modIndex}"; + var firstOpen = false; + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) { - _base = ui; - _modManager = Service< ModManager >.Get(); - Cache = new ModListCache( _modManager, newMods ); + ImGui.OpenPopup( popupName ); + firstOpen = true; } - private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) + DragDropTarget( mod.Data.SortOrder.ParentFolder ); + DragDropSourceMod( modIndex, mod.Data.Meta.Name ); + + DrawModOrderPopup( popupName, mod, firstOpen ); + + if( selected ) { - if( collection == ModCollection.Empty - || collection == _modManager.Collections.CurrentCollection ) + SetSelection( modIndex, mod ); + } + } + + public void Draw() + { + if( Cache.Update() ) + { + if( _nextDir.Any() ) { - using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( label, Vector2.UnitX * size ); + SelectModByDir( _nextDir ); + _nextDir = string.Empty; } - else if( ImGui.Button( label, Vector2.UnitX * size ) ) + else if( Mod != null ) { - _base._menu.CollectionsTab.SetCurrentCollection( collection ); - } - - ImGuiCustom.HoverTooltip( - $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); - } - - private void DrawHeaderBar() - { - const float size = 200; - - DrawModsSelectorFilter(); - var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; - var comboSize = size * ImGui.GetIO().FontGlobalScale; - var offset = comboSize + textSize; - - var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth() - - offset - - SelectorPanelWidth * _selectorScalingFactor - - 4 * ImGui.GetStyle().ItemSpacing.X ) - / 2, 5f ); - ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, _modManager.Collections.DefaultCollection ); - - ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, _modManager.Collections.ForcedCollection ); - - ImGui.SameLine(); - ImGui.SetNextItemWidth( comboSize ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); - } - - private void DrawFolderContent( ModFolder folder, ref int idx ) - { - // Collection may be manipulated. - foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) - { - if( item is ModFolder sub ) - { - var (visible, _) = Cache.GetFolder( sub ); - if( visible ) - { - DrawModFolder( sub, ref idx ); - } - else - { - idx += sub.TotalDescendantMods(); - } - } - else if( item is ModData _ ) - { - var (mod, visible, color) = Cache.GetMod( idx ); - if( mod != null && visible ) - { - DrawMod( mod, idx++, color ); - } - else - { - ++idx; - } - } + SelectModByDir( Mod.Data.BasePath.Name ); } } - private void DrawModFolder( ModFolder folder, ref int idx ) + _selectorScalingFactor = ImGuiHelpers.GlobalScale + * ( Penumbra.Config.ScaleModSelector + ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X + : 1f ); + // Selector pane + DrawHeaderBar(); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + ImGui.BeginGroup(); + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) + .Push( ImGui.EndChild ); + // Inlay selector list + if( ImGui.BeginChild( LabelSelectorList, + new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), + true, ImGuiWindowFlags.HorizontalScrollbar ) ) { - var treeName = $"{folder.Name}##{folder.FullName}"; - var open = ImGui.TreeNodeEx( treeName ); - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - _newFolderName = string.Empty; - ImGui.OpenPopup( treeName ); - } - - DrawFolderContextMenu( folder, idx, treeName ); - DragDropTarget( folder ); - DragDropSourceFolder( folder ); - - if( open ) - { - DrawFolderContent( folder, ref idx ); - } - else - { - idx += folder.TotalDescendantMods(); - } - } - - private void DrawMod( Mod.Mod mod, int modIndex, uint color ) - { - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); - - var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); - colorRaii.Pop(); - - var popupName = $"##SortOrderPopup{modIndex}"; - var firstOpen = false; - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - ImGui.OpenPopup( popupName ); - firstOpen = true; - } - - DragDropTarget( mod.Data.SortOrder.ParentFolder ); - DragDropSourceMod( modIndex, mod.Data.Meta.Name ); - - DrawModOrderPopup( popupName, mod, firstOpen ); - - if( selected ) - { - SetSelection( modIndex, mod ); - } - } - - public void Draw() - { - if( Cache.Update() ) - { - if( _nextDir.Any() ) - { - SelectModByDir( _nextDir ); - _nextDir = string.Empty; - } - else if( Mod != null ) - { - SelectModByDir( Mod.Data.BasePath.Name ); - } - } - - _selectorScalingFactor = ImGuiHelpers.GlobalScale - * ( Penumbra.Config.ScaleModSelector - ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X - : 1f ); - // Selector pane - DrawHeaderBar(); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGui.BeginGroup(); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) - .Push( ImGui.EndChild ); - // Inlay selector list - if( ImGui.BeginChild( LabelSelectorList, - new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), - true, ImGuiWindowFlags.HorizontalScrollbar ) ) - { - style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); - - var modIndex = 0; - DrawFolderContent( _modManager.StructuredMods, ref modIndex ); - style.Pop(); - } - - raii.Pop(); - - DrawModsSelectorButtons(); + style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); + var modIndex = 0; + DrawFolderContent( _modManager.StructuredMods, ref modIndex ); style.Pop(); - DrawModHelpPopup(); - - DrawDeleteModal(); } + + raii.Pop(); + + DrawModsSelectorButtons(); + + style.Pop(); + DrawModHelpPopup(); + + DrawDeleteModal(); } } } \ No newline at end of file From 6fa79c62c45d37ebcc41aac8c9d639296be7beaf Mon Sep 17 00:00:00 2001 From: Yuki Date: Sun, 20 Mar 2022 16:16:27 +1100 Subject: [PATCH 0077/2451] Add redraw route to http api --- Penumbra/Api/RedrawController.cs | 35 ++++++++++++++++++++++++++++++++ Penumbra/Penumbra.cs | 3 ++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Api/RedrawController.cs diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs new file mode 100644 index 00000000..4f55c69c --- /dev/null +++ b/Penumbra/Api/RedrawController.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Routing; +using EmbedIO.WebApi; +using Penumbra.GameData.Enums; + +namespace Penumbra.Api +{ + public class RedrawController : WebApiController + { + private readonly Penumbra _penumbra; + + public RedrawController( Penumbra penumbra ) + => _penumbra = penumbra; + + [Route( HttpVerbs.Post, "/redraw" )] + public async Task Redraw() + { + RedrawData data = await HttpContext.GetRequestDataAsync(); + _penumbra.Api.RedrawObject( data.Name, data.Type ); + } + + [Route( HttpVerbs.Post, "/redrawAll" )] + public void RedrawAll() + { + _penumbra.Api.RedrawAll(RedrawType.WithoutSettings); + } + + public class RedrawData + { + public string Name { get; set; } = string.Empty; + public RedrawType Type { get; set; } = RedrawType.WithSettings; + } + } +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6a7157b8..683ad296 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -166,7 +166,8 @@ public class Penumbra : IDalamudPlugin .WithMode( HttpListenerMode.EmbedIO ) ) .WithCors( prefix ) .WithWebApi( "/api", m => m - .WithController( () => new ModsController( this ) ) ); + .WithController( () => new ModsController( this ) ) + .WithController( () => new RedrawController( this ) ) ); _webServer.StateChanged += ( _, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); From c8293c9a6bd498ebd473d9276e8de32be2d36a6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 13:00:49 +0100 Subject: [PATCH 0078/2451] Fix handling of weird TTMP files. --- Penumbra/Importer/TexToolsImport.cs | 732 ++++++++++++++-------------- 1 file changed, 369 insertions(+), 363 deletions(-) diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index fbbd4fc4..44329dc6 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using Dalamud.Logging; -using Dalamud.Plugin; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.GameData.Util; @@ -14,408 +13,415 @@ using Penumbra.Structs; using Penumbra.Util; using FileMode = System.IO.FileMode; -namespace Penumbra.Importer +namespace Penumbra.Importer; + +internal class TexToolsImport { - internal class TexToolsImport + private readonly DirectoryInfo _outDirectory; + + private const string TempFileName = "textools-import"; + private readonly string _resolvedTempFilePath; + + public DirectoryInfo? ExtractedDirectory { get; private set; } + + public ImporterState State { get; private set; } + + public long TotalProgress { get; private set; } + public long CurrentProgress { get; private set; } + + public float Progress { - private readonly DirectoryInfo _outDirectory; - - private const string TempFileName = "textools-import"; - private readonly string _resolvedTempFilePath; - - public DirectoryInfo? ExtractedDirectory { get; private set; } - - public ImporterState State { get; private set; } - - public long TotalProgress { get; private set; } - public long CurrentProgress { get; private set; } - - public float Progress + get { - get + if( CurrentProgress != 0 ) { - if( CurrentProgress != 0 ) - { - // ReSharper disable twice RedundantCast - return ( float )CurrentProgress / ( float )TotalProgress; - } + // ReSharper disable twice RedundantCast + return ( float )CurrentProgress / ( float )TotalProgress; + } - return 0; + return 0; + } + } + + public string? CurrentModPack { get; private set; } + + public TexToolsImport( DirectoryInfo outDirectory ) + { + _outDirectory = outDirectory; + _resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName ); + } + + private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new(Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() )); + + public DirectoryInfo ImportModPack( FileInfo modPackFile ) + { + CurrentModPack = modPackFile.Name; + + var dir = VerifyVersionAndImport( modPackFile ); + + State = ImporterState.Done; + return dir; + } + + private void WriteZipEntryToTempFile( Stream s ) + { + var fs = new FileStream( _resolvedTempFilePath, FileMode.Create ); + s.CopyTo( fs ); + fs.Close(); + } + + // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry + private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) + { + for( var i = 0; i < file.Count; i++ ) + { + var entry = file[ i ]; + + if( entry.Name.Contains( fileName ) ) + { + return entry; } } - public string? CurrentModPack { get; private set; } + return null; + } - public TexToolsImport( DirectoryInfo outDirectory ) + private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName ) + { + State = ImporterState.WritingPackToDisk; + + // write shitty zip garbage to disk + var entry = FindZipEntry( file, entryName ); + if( entry == null ) { - _outDirectory = outDirectory; - _resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName ); + throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); } - private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new( Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() ) ); + using var s = file.GetInputStream( entry ); - public DirectoryInfo ImportModPack( FileInfo modPackFile ) + WriteZipEntryToTempFile( s ); + + var fs = new FileStream( _resolvedTempFilePath, FileMode.Open ); + return new MagicTempFileStreamManagerAndDeleter( fs ); + } + + private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) + { + using var zfs = modPackFile.OpenRead(); + using var extractedModPack = new ZipFile( zfs ); + + var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); + if( mpl == null ) { - CurrentModPack = modPackFile.Name; - - var dir = VerifyVersionAndImport( modPackFile ); - - State = ImporterState.Done; - return dir; + throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); } - private void WriteZipEntryToTempFile( Stream s ) + var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); + + // At least a better validation than going by the extension. + if( modRaw.Contains( "\"TTMPVersion\":" ) ) { - var fs = new FileStream( _resolvedTempFilePath, FileMode.Create ); - s.CopyTo( fs ); - fs.Close(); + if( modPackFile.Extension != ".ttmp2" ) + { + PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); + } + + return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); } - // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry - private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) + if( modPackFile.Extension != ".ttmp" ) { - for( var i = 0; i < file.Count; i++ ) - { - var entry = file[ i ]; - - if( entry.Name.Contains( fileName ) ) - { - return entry; - } - } - - return null; + PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); } - private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName ) + return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); + } + + private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + { + PluginLog.Log( " -> Importing V1 ModPack" ); + + var modListRaw = modRaw.Split( + new[] { "\r\n", "\r", "\n" }, + StringSplitOptions.None + ); + + var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > ); + + // Create a new ModMeta from the TTMP modlist info + var modMeta = new ModMeta { - State = ImporterState.WritingPackToDisk; + Author = "Unknown", + Name = modPackFile.Name, + Description = "Mod imported from TexTools mod pack", + }; - // write shitty zip garbage to disk - var entry = FindZipEntry( file, entryName ); - if( entry == null ) - { - throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); - } + // Open the mod data file from the modpack as a SqPackStream + using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - using var s = file.GetInputStream( entry ); + ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); - WriteZipEntryToTempFile( s ); + File.WriteAllText( + Path.Combine( ExtractedDirectory.FullName, "meta.json" ), + JsonConvert.SerializeObject( modMeta ) + ); - var fs = new FileStream( _resolvedTempFilePath, FileMode.Open ); - return new MagicTempFileStreamManagerAndDeleter( fs ); + ExtractSimpleModList( ExtractedDirectory, modList, modData ); + + return ExtractedDirectory; + } + + private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) + { + var modList = JsonConvert.DeserializeObject( modRaw ); + + if( modList.TTMPVersion?.EndsWith( "s" ) ?? false ) + { + return ImportSimpleV2ModPack( extractedModPack, modList ); } - private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) + if( modList.TTMPVersion?.EndsWith( "w" ) ?? false ) { - using var zfs = modPackFile.OpenRead(); - using var extractedModPack = new ZipFile( zfs ); - - var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); - if( mpl == null ) - { - throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); - } - - var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); - - // At least a better validation than going by the extension. - if( modRaw.Contains( "\"TTMPVersion\":" ) ) - { - if( modPackFile.Extension != ".ttmp2" ) - { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); - } - - return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); - } - else - { - if( modPackFile.Extension != ".ttmp" ) - { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); - } - - return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); - } + return ImportExtendedV2ModPack( extractedModPack, modRaw ); } - private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + try { - PluginLog.Log( " -> Importing V1 ModPack" ); - - var modListRaw = modRaw.Split( - new[] { "\r\n", "\r", "\n" }, - StringSplitOptions.None - ); - - var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > ); - - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = "Unknown", - Name = modPackFile.Name, - Description = "Mod imported from TexTools mod pack", - }; - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) - ); - - ExtractSimpleModList( ExtractedDirectory, modList, modData ); - - return ExtractedDirectory; + PluginLog.Warning( $"Unknown TTMPVersion {modList.TTMPVersion ?? "NULL"} given, trying to export as simple Modpack." ); + return ImportSimpleV2ModPack( extractedModPack, modList ); } - - private DirectoryInfo ImportV2ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + catch( Exception e1 ) { - var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw ); - - if( modList?.TTMPVersion == null ) - { - PluginLog.Error( "Could not extract V2 Modpack. No version given." ); - return new DirectoryInfo( "" ); - } - - if( modList.TTMPVersion.EndsWith( "s" ) ) - { - return ImportSimpleV2ModPack( extractedModPack, modList ); - } - - if( modList.TTMPVersion.EndsWith( "w" ) ) + PluginLog.Warning( $"Exporting as simple Modpack failed with following error, retrying as extended Modpack:\n{e1}" ); + try { return ImportExtendedV2ModPack( extractedModPack, modRaw ); } - - return new DirectoryInfo( "" ); - } - - public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) - { - var name = Path.GetFileName( modListName ); - if( !name.Any() ) + catch( Exception e2 ) { - name = "_"; + throw new IOException( "Exporting as extended Modpack failed, too. Version unsupported or file defect.", e2 ); } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase; - var i = 2; - while( newModFolder.Exists && i < 12 ) - { - newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" ); - } - - if( newModFolder.Exists ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - newModFolder.Create(); - return newModFolder; - } - - private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) - { - PluginLog.Log( " -> Importing Simple V2 ModPack" ); - - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description!, - }; - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - - File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) ); - - ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); - return ExtractedDirectory; - } - - private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) - { - PluginLog.Log( " -> Importing Extended V2 ModPack" ); - - var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw ); - - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description ?? "", - Version = modList.Version ?? "", - }; - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - - if( modList.SimpleModsList != null ) - { - ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData ); - } - - if( modList.ModPackPages == null ) - { - return ExtractedDirectory; - } - - // Iterate through all pages - foreach( var page in modList.ModPackPages ) - { - if( page.ModGroups == null ) - { - continue; - } - - foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) - { - var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! ); - if( groupFolder.Exists ) - { - groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" ); - group.GroupName += $" ({page.PageIndex})"; - } - - foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) ) - { - var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); - ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); - } - - AddMeta( ExtractedDirectory, groupFolder, group, modMeta ); - } - } - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta, Formatting.Indented ) - ); - return ExtractedDirectory; - } - - private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta ) - { - var inf = new OptionGroup - { - SelectionType = group.SelectionType, - GroupName = group.GroupName!, - Options = new List< Option >(), - }; - foreach( var opt in group.OptionList! ) - { - var option = new Option - { - OptionName = opt.Name!, - OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, - OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), - }; - var optDir = NewOptionDirectory( groupFolder, opt.Name! ); - if( optDir.Exists ) - { - foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) ); - } - } - - inf.Options.Add( option ); - } - - meta.Groups.Add( inf.GroupName, inf ); - } - - private void ImportMetaModPack( FileInfo file ) - { - throw new NotImplementedException(); - } - - private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream ) - { - State = ImporterState.ExtractingModFiles; - - // haha allocation go brr - var wtf = mods.ToList(); - - TotalProgress += wtf.LongCount(); - - // Extract each SimpleMod into the new mod folder - foreach( var simpleMod in wtf.Where( m => m != null ) ) - { - ExtractMod( outDirectory, simpleMod, dataStream ); - CurrentProgress++; - } - } - - private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream ) - { - PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) ); - - try - { - var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); - - var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) ); - extractedFile.Directory?.Create(); - - if( extractedFile.FullName.EndsWith( "mdl" ) ) - { - ProcessMdl( data.Data ); - } - - File.WriteAllBytes( extractedFile.FullName, data.Data ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Could not extract mod." ); - } - } - - private void ProcessMdl( byte[] mdl ) - { - // Model file header LOD num - mdl[ 64 ] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32( mdl, 4 ); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - var modelHeaderLodOffset = 22; - mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; - } - - private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) - => file.GetInputStream( entry ); - - private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) - { - using var ms = new MemoryStream(); - using var s = GetStreamFromZipEntry( file, entry ); - s.CopyTo( ms ); - return encoding.GetString( ms.ToArray() ); } } + + public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) + { + var name = Path.GetFileName( modListName ); + if( !name.Any() ) + { + name = "_"; + } + + var newModFolderBase = NewOptionDirectory( outDirectory, name ); + var newModFolder = newModFolderBase; + var i = 2; + while( newModFolder.Exists && i < 12 ) + { + newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" ); + } + + if( newModFolder.Exists ) + { + throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); + } + + newModFolder.Create(); + return newModFolder; + } + + private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) + { + PluginLog.Log( " -> Importing Simple V2 ModPack" ); + + // Create a new ModMeta from the TTMP modlist info + var modMeta = new ModMeta + { + Author = modList.Author ?? "Unknown", + Name = modList.Name ?? "New Mod", + Description = string.IsNullOrEmpty( modList.Description ) + ? "Mod imported from TexTools mod pack" + : modList.Description!, + }; + + // Open the mod data file from the modpack as a SqPackStream + using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); + + ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); + + File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ), + JsonConvert.SerializeObject( modMeta ) ); + + ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); + return ExtractedDirectory; + } + + private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) + { + PluginLog.Log( " -> Importing Extended V2 ModPack" ); + + var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw ); + + // Create a new ModMeta from the TTMP modlist info + var modMeta = new ModMeta + { + Author = modList.Author ?? "Unknown", + Name = modList.Name ?? "New Mod", + Description = string.IsNullOrEmpty( modList.Description ) + ? "Mod imported from TexTools mod pack" + : modList.Description ?? "", + Version = modList.Version ?? "", + }; + + // Open the mod data file from the modpack as a SqPackStream + using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); + + ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); + + if( modList.SimpleModsList != null ) + { + ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData ); + } + + if( modList.ModPackPages == null ) + { + return ExtractedDirectory; + } + + // Iterate through all pages + foreach( var page in modList.ModPackPages ) + { + if( page.ModGroups == null ) + { + continue; + } + + foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) + { + var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! ); + if( groupFolder.Exists ) + { + groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" ); + group.GroupName += $" ({page.PageIndex})"; + } + + foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) ) + { + var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); + ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); + } + + AddMeta( ExtractedDirectory, groupFolder, group, modMeta ); + } + } + + File.WriteAllText( + Path.Combine( ExtractedDirectory.FullName, "meta.json" ), + JsonConvert.SerializeObject( modMeta, Formatting.Indented ) + ); + return ExtractedDirectory; + } + + private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta ) + { + var inf = new OptionGroup + { + SelectionType = group.SelectionType, + GroupName = group.GroupName!, + Options = new List< Option >(), + }; + foreach( var opt in group.OptionList! ) + { + var option = new Option + { + OptionName = opt.Name!, + OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, + OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), + }; + var optDir = NewOptionDirectory( groupFolder, opt.Name! ); + if( optDir.Exists ) + { + foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + { + option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) ); + } + } + + inf.Options.Add( option ); + } + + meta.Groups.Add( inf.GroupName, inf ); + } + + private void ImportMetaModPack( FileInfo file ) + { + throw new NotImplementedException(); + } + + private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream ) + { + State = ImporterState.ExtractingModFiles; + + // haha allocation go brr + var wtf = mods.ToList(); + + TotalProgress += wtf.LongCount(); + + // Extract each SimpleMod into the new mod folder + foreach( var simpleMod in wtf.Where( m => m != null ) ) + { + ExtractMod( outDirectory, simpleMod, dataStream ); + CurrentProgress++; + } + } + + private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream ) + { + PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) ); + + try + { + var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); + + var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) ); + extractedFile.Directory?.Create(); + + if( extractedFile.FullName.EndsWith( "mdl" ) ) + { + ProcessMdl( data.Data ); + } + + File.WriteAllBytes( extractedFile.FullName, data.Data ); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "Could not extract mod." ); + } + } + + private void ProcessMdl( byte[] mdl ) + { + // Model file header LOD num + mdl[ 64 ] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32( mdl, 4 ); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + var modelHeaderLodOffset = 22; + mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; + } + + private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) + => file.GetInputStream( entry ); + + private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) + { + using var ms = new MemoryStream(); + using var s = GetStreamFromZipEntry( file, entry ); + s.CopyTo( ms ); + return encoding.GetString( ms.ToArray() ); + } } \ No newline at end of file From 6d3a4f08c58ff97472fda1555365b716659f7475 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 13:04:37 +0100 Subject: [PATCH 0079/2451] Change HTTP API help text. --- Penumbra/UI/MenuTabs/TabSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 50da80e2..112c8136 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -262,7 +262,7 @@ public partial class SettingsInterface } ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Currently does nothing." ); + ImGuiComponents.HelpMarker( "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); } private void DrawEnabledPlayerWatcher() From 18384a93863ac788c2f16c842e6d9e7bd4fee383 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 20 Mar 2022 12:07:45 +0000 Subject: [PATCH 0080/2451] [CI] Updating repo.json for refs/tags/0.4.8.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a1b977c4..1f55826d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.7.7", - "TestingAssemblyVersion": "0.4.7.7", + "AssemblyVersion": "0.4.8.0", + "TestingAssemblyVersion": "0.4.8.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 5, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.7.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8865ff5e7957cb0651fde34294bd1609ae20f366 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Apr 2022 16:01:06 +0200 Subject: [PATCH 0081/2451] Fix 6.1 offsets. --- .../Structs/CharacterEquipment.cs | 255 +++++++++--------- Penumbra.PlayerWatch/PlayerWatchBase.cs | 4 +- Penumbra/Penumbra.json | 2 +- base_repo.json | 2 +- 4 files changed, 131 insertions(+), 132 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterEquipment.cs b/Penumbra.GameData/Structs/CharacterEquipment.cs index 11be31ed..357ce36b 100644 --- a/Penumbra.GameData/Structs/CharacterEquipment.cs +++ b/Penumbra.GameData/Structs/CharacterEquipment.cs @@ -4,146 +4,145 @@ using Dalamud.Game.ClientState.Objects.Types; // Read the customization data regarding weapons and displayable equipment from an actor struct. // Stores the data in a 56 bytes, i.e. 7 longs for easier comparison. -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public class CharacterEquipment { - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public class CharacterEquipment + public const int MainWeaponOffset = 0x6D0; + public const int OffWeaponOffset = 0x738; + public const int EquipmentOffset = 0x808; + public const int EquipmentSlots = 10; + public const int WeaponSlots = 2; + + public CharacterWeapon MainHand; + public CharacterWeapon OffHand; + public CharacterArmor Head; + public CharacterArmor Body; + public CharacterArmor Hands; + public CharacterArmor Legs; + public CharacterArmor Feet; + public CharacterArmor Ears; + public CharacterArmor Neck; + public CharacterArmor Wrists; + public CharacterArmor RFinger; + public CharacterArmor LFinger; + public ushort IsSet; // Also fills struct size to 56, a multiple of 8. + + public CharacterEquipment() + => Clear(); + + public CharacterEquipment( Character actor ) + : this( actor.Address ) + { } + + public override string ToString() + => IsSet == 0 + ? "(Not Set)" + : $"({MainHand}) | ({OffHand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | " + + $"({Feet}) | ({Ears}) | ({Neck}) | ({Wrists}) | ({LFinger}) | ({RFinger})"; + + public bool Equal( Character rhs ) + => CompareData( new CharacterEquipment( rhs ) ); + + public bool Equal( CharacterEquipment rhs ) + => CompareData( rhs ); + + public bool CompareAndUpdate( Character rhs ) + => CompareAndOverwrite( new CharacterEquipment( rhs ) ); + + public bool CompareAndUpdate( CharacterEquipment rhs ) + => CompareAndOverwrite( rhs ); + + private unsafe CharacterEquipment( IntPtr actorAddress ) { - public const int MainWeaponOffset = 0x0C78; - public const int OffWeaponOffset = 0x0CE0; - public const int EquipmentOffset = 0xDB0; - public const int EquipmentSlots = 10; - public const int WeaponSlots = 2; - - public CharacterWeapon MainHand; - public CharacterWeapon OffHand; - public CharacterArmor Head; - public CharacterArmor Body; - public CharacterArmor Hands; - public CharacterArmor Legs; - public CharacterArmor Feet; - public CharacterArmor Ears; - public CharacterArmor Neck; - public CharacterArmor Wrists; - public CharacterArmor RFinger; - public CharacterArmor LFinger; - public ushort IsSet; // Also fills struct size to 56, a multiple of 8. - - public CharacterEquipment() - => Clear(); - - public CharacterEquipment( Character actor ) - : this( actor.Address ) - { } - - public override string ToString() - => IsSet == 0 - ? "(Not Set)" - : $"({MainHand}) | ({OffHand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | " - + $"({Feet}) | ({Ears}) | ({Neck}) | ({Wrists}) | ({LFinger}) | ({RFinger})"; - - public bool Equal( Character rhs ) - => CompareData( new CharacterEquipment( rhs ) ); - - public bool Equal( CharacterEquipment rhs ) - => CompareData( rhs ); - - public bool CompareAndUpdate( Character rhs ) - => CompareAndOverwrite( new CharacterEquipment( rhs ) ); - - public bool CompareAndUpdate( CharacterEquipment rhs ) - => CompareAndOverwrite( rhs ); - - private unsafe CharacterEquipment( IntPtr actorAddress ) + IsSet = 1; + var actorPtr = ( byte* )actorAddress.ToPointer(); + fixed( CharacterWeapon* main = &MainHand, off = &OffHand ) { - IsSet = 1; - var actorPtr = ( byte* )actorAddress.ToPointer(); - fixed( CharacterWeapon* main = &MainHand, off = &OffHand ) - { - Buffer.MemoryCopy( actorPtr + MainWeaponOffset, main, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); - Buffer.MemoryCopy( actorPtr + OffWeaponOffset, off, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); - } - - fixed( CharacterArmor* equipment = &Head ) - { - Buffer.MemoryCopy( actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof( CharacterArmor ), - EquipmentSlots * sizeof( CharacterArmor ) ); - } + Buffer.MemoryCopy( actorPtr + MainWeaponOffset, main, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); + Buffer.MemoryCopy( actorPtr + OffWeaponOffset, off, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); } - public unsafe void Clear() + fixed( CharacterArmor* equipment = &Head ) { - fixed( CharacterWeapon* main = &MainHand ) - { - var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - for( ulong* ptr = ( ulong* )main, end = ptr + structSizeEights; ptr != end; ++ptr ) - { - *ptr = 0; - } - } + Buffer.MemoryCopy( actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof( CharacterArmor ), + EquipmentSlots * sizeof( CharacterArmor ) ); } + } - private unsafe bool CompareAndOverwrite( CharacterEquipment rhs ) + public unsafe void Clear() + { + fixed( CharacterWeapon* main = &MainHand ) { var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - var ret = true; - fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) + for( ulong* ptr = ( ulong* )main, end = ptr + structSizeEights; ptr != end; ++ptr ) { - var ptr1 = ( ulong* )data1; - var ptr2 = ( ulong* )data2; - for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) - { - if( *ptr1 != *ptr2 ) - { - *ptr1 = *ptr2; - ret = false; - } - } - } - - return ret; - } - - private unsafe bool CompareData( CharacterEquipment rhs ) - { - var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) - { - var ptr1 = ( ulong* )data1; - var ptr2 = ( ulong* )data2; - for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) - { - if( *ptr1 != *ptr2 ) - { - return false; - } - } - } - - return true; - } - - public unsafe void WriteBytes( byte[] array, int offset = 0 ) - { - fixed( CharacterWeapon* data = &MainHand ) - { - Marshal.Copy( new IntPtr( data ), array, offset, 56 ); - } - } - - public byte[] ToBytes() - { - var ret = new byte[56]; - WriteBytes( ret ); - return ret; - } - - public unsafe void FromBytes( byte[] array, int offset = 0 ) - { - fixed( CharacterWeapon* data = &MainHand ) - { - Marshal.Copy( array, offset, new IntPtr( data ), 56 ); + *ptr = 0; } } } + + private unsafe bool CompareAndOverwrite( CharacterEquipment rhs ) + { + var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; + var ret = true; + fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) + { + var ptr1 = ( ulong* )data1; + var ptr2 = ( ulong* )data2; + for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) + { + if( *ptr1 != *ptr2 ) + { + *ptr1 = *ptr2; + ret = false; + } + } + } + + return ret; + } + + private unsafe bool CompareData( CharacterEquipment rhs ) + { + var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; + fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) + { + var ptr1 = ( ulong* )data1; + var ptr2 = ( ulong* )data2; + for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) + { + if( *ptr1 != *ptr2 ) + { + return false; + } + } + } + + return true; + } + + public unsafe void WriteBytes( byte[] array, int offset = 0 ) + { + fixed( CharacterWeapon* data = &MainHand ) + { + Marshal.Copy( new IntPtr( data ), array, offset, 56 ); + } + } + + public byte[] ToBytes() + { + var ret = new byte[56]; + WriteBytes( ret ); + return ret; + } + + public unsafe void FromBytes( byte[] array, int offset = 0 ) + { + fixed( CharacterWeapon* data = &MainHand ) + { + Marshal.Copy( array, offset, new IntPtr( data ), 56 ); + } + } } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index 41cb9fdb..dd777043 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -26,7 +26,7 @@ internal readonly struct WatchedPlayer internal class PlayerWatchBase : IDisposable { public const int GPosePlayerIdx = 201; - public const int GPoseTableEnd = GPosePlayerIdx + 48; + public const int GPoseTableEnd = GPosePlayerIdx + 40; private const int ObjectsPerFrame = 32; private readonly Framework _framework; @@ -301,7 +301,7 @@ internal class PlayerWatchBase : IDisposable var id = GetId( character ); SeenActors.Add( id ); - PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address ); + PluginLog.Verbose( "Comparing Gear for {PlayerName:l} ({Id}) at 0x{Address:X}...", character.Name, id, character.Address.ToInt64() ); if( !watch.FoundActors.TryGetValue( id, out var equip ) ) { equip = new CharacterEquipment( character ); diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 85b613eb..0e05326e 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -7,7 +7,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 5, + "DalamudApiLevel": 6, "LoadPriority": 69420, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } \ No newline at end of file diff --git a/base_repo.json b/base_repo.json index 2a3a2145..71b15948 100644 --- a/base_repo.json +++ b/base_repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "1.0.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 5, + "DalamudApiLevel": 6, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 1ee7a7cbf2b1c9fd02eff0798bec61ac7ec321b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Apr 2022 16:38:39 +0200 Subject: [PATCH 0082/2451] Use net5 build --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 544b918e..b2d2f887 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + 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: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cda3844a..cbb70556 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + 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: | From 53d19ff473aaae1c0e863887d814caed71513ad4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 14 Apr 2022 14:44:24 +0000 Subject: [PATCH 0083/2451] [CI] Updating repo.json for refs/tags/0.4.8.1 --- repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/repo.json b/repo.json index 1f55826d..5396ef8a 100644 --- a/repo.json +++ b/repo.json @@ -4,19 +4,19 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.8.0", - "TestingAssemblyVersion": "0.4.8.0", + "AssemblyVersion": "0.4.8.1", + "TestingAssemblyVersion": "0.4.8.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 5, + "DalamudApiLevel": 6, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9130932a7f0057fe23ebb4f5e307c1e3beb2ccfc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Apr 2022 13:17:19 +0200 Subject: [PATCH 0084/2451] Fix VFunc index for GPose Redraw. --- Penumbra/Interop/ObjectReloader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 03af4650..b8000f9e 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -85,7 +85,7 @@ namespace Penumbra.Interop if( _inGPose ) { var ptr = ( void*** )actor.Address; - var disableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 18 ] ) ); + var disableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 17 ] ) ); disableDraw( actor.Address ); } } @@ -119,7 +119,7 @@ namespace Penumbra.Interop if( _inGPose ) { var ptr = ( void*** )actor.Address; - var enableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 17 ] ) ); + var enableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 16 ] ) ); enableDraw( actor.Address ); } } From 1658102c344932b4664ea9fcc3fe28d40385a89f Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 15 Apr 2022 11:19:04 +0000 Subject: [PATCH 0085/2451] [CI] Updating repo.json for refs/tags/0.4.8.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5396ef8a..2847dcff 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.8.1", - "TestingAssemblyVersion": "0.4.8.1", + "AssemblyVersion": "0.4.8.2", + "TestingAssemblyVersion": "0.4.8.2", "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.4.8.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e15d844d4b0bda0ba62f864809d0181fc52b717b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Feb 2022 11:51:04 +0100 Subject: [PATCH 0086/2451] Start --- Penumbra/Interop/PathResolver.cs | 108 +++++++++++++++++++++++++++++ Penumbra/Interop/ResourceLoader.cs | 10 +-- Penumbra/Penumbra.cs | 3 + 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 Penumbra/Interop/PathResolver.cs diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs new file mode 100644 index 00000000..6c683863 --- /dev/null +++ b/Penumbra/Interop/PathResolver.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Util; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Interop; + +public unsafe class PathResolver : IDisposable +{ + public delegate IntPtr ResolveMdlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + public delegate IntPtr ResolveMtrlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); + + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41" )] + public Hook< ResolveMdlPath >? ResolveMdlPathHook; + + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F" )] + public Hook? ResolveMtrlPathHook; + + private global::Dalamud.Game.ClientState.Objects.Types.GameObject? FindParent( IntPtr drawObject ) + => Dalamud.Objects.FirstOrDefault( a => ( ( GameObject* )a.Address )->DrawObject == ( DrawObject* )drawObject ); + + private readonly byte[] _data = new byte[512]; + + private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + var ret = ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ); + var n = Marshal.PtrToStringAnsi( ret )!; + var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; + PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4}\n{n}\n{name}" ); + if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) + { + var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); + if( replacement != null ) + { + for( var i = 0; i < replacement.Length; ++i ) + { + _data[ i ] = ( byte )replacement[ i ]; + } + + _data[ replacement.Length ] = 0; + fixed( byte* data = _data ) + { + return ( IntPtr )data; + } + } + } + + return ret; + } + + private unsafe IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) + { + var ret = ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ); + var n = Marshal.PtrToStringAnsi( ret )!; + var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; + PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4} {unk5:X}\n{n}\n{name}" ); + if( Service.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) + { + var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); + if( replacement != null ) + { + for( var i = 0; i < replacement.Length; ++i ) + { + _data[i] = ( byte )replacement[i]; + } + + _data[replacement.Length] = 0; + fixed( byte* data = _data ) + { + return ( IntPtr )data; + } + } + } + + return ret; + } + + public PathResolver() + { + SignatureHelper.Initialise( this ); + Enable(); + } + + public void Enable() + { + ResolveMdlPathHook?.Enable(); + ResolveMtrlPathHook?.Enable(); + } + + public void Disable() + { + ResolveMdlPathHook?.Disable(); + ResolveMtrlPathHook?.Disable(); + } + + public void Dispose() + { + ResolveMdlPathHook?.Dispose(); + ResolveMtrlPathHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index fbb05077..6f8fbb49 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -129,7 +129,7 @@ public class ResourceLoader : IDisposable private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) { var modManager = Service< ModManager >.Get(); - return modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); + return true || modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); } private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) @@ -282,8 +282,8 @@ public class ResourceLoader : IDisposable Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); pFileDesc->FileDescriptor = fd; - - return ReadFile( pFileHandler, pFileDesc, priority, isSync ); + var ret = ReadFile( pFileHandler, pFileDesc, priority, isSync ); + return ret; } public void Enable() @@ -311,7 +311,7 @@ public class ResourceLoader : IDisposable LoadTexFileExternHook.Enable(); LoadMdlFileExternHook.Enable(); - IsEnabled = true; + IsEnabled = true; } public void Disable() @@ -327,7 +327,7 @@ public class ResourceLoader : IDisposable CheckFileStateHook?.Disable(); LoadTexFileExternHook?.Disable(); LoadMdlFileExternHook?.Disable(); - IsEnabled = false; + IsEnabled = false; } public void Dispose() diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 683ad296..14329de8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,6 +29,7 @@ public class Penumbra : IDalamudPlugin public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; public ResourceLoader ResourceLoader { get; } + public PathResolver PathResolver { get; } public SettingsInterface SettingsInterface { get; } public MusicManager MusicManager { get; } public ObjectReloader ObjectReloader { get; } @@ -53,6 +54,7 @@ public class Penumbra : IDalamudPlugin } var gameUtils = Service< ResidentResources >.Set(); + PathResolver = new PathResolver(); PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); Service< MetaDefaults >.Set(); _modManager = Service< ModManager >.Set(); @@ -190,6 +192,7 @@ public class Penumbra : IDalamudPlugin Dalamud.Commands.RemoveHandler( CommandName ); + PathResolver.Dispose(); ResourceLoader.Dispose(); ShutdownWebServer(); From 0e8f83947123144763de92a37f4a4af4da10aefd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 27 Feb 2022 00:04:01 +0100 Subject: [PATCH 0087/2451] tmp --- Penumbra.GameData/Util/Functions.cs | 63 ++ Penumbra.GameData/Util/GamePath.cs | 677 ++++++++++++++++++--- Penumbra/Interop/PathResolver.cs | 148 +++-- Penumbra/Interop/ResourceLoader.cs | 49 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 15 +- 5 files changed, 812 insertions(+), 140 deletions(-) create mode 100644 Penumbra.GameData/Util/Functions.cs diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs new file mode 100644 index 00000000..91dc7799 --- /dev/null +++ b/Penumbra.GameData/Util/Functions.cs @@ -0,0 +1,63 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.GameData.Util; + +public static class Functions +{ + public static ulong ComputeCrc64( string name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( '/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + public static ulong ComputeCrc64( ReadOnlySpan< byte > name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( ( byte )'/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe IntPtr memcpy( byte* dest, byte* src, int count ); + + public static unsafe void MemCpyUnchecked( byte* dest, byte* src, int count ) + => memcpy( dest, src, count ); + + + [DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe int memcmp( byte* b1, byte* b2, int count ); + + public static unsafe int MemCmpUnchecked( byte* ptr1, byte* ptr2, int count ) + => memcmp( ptr1, ptr2, count ); + + + [DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe int memicmp( byte* b1, byte* b2, int count ); + + public static unsafe int MemCmpCaseInsensitiveUnchecked( byte* ptr1, byte* ptr2, int count ) + => memicmp( ptr1, ptr2, count ); +} \ No newline at end of file diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index 002740f1..4c670605 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -1,105 +1,620 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Penumbra.GameData.Util +namespace Penumbra.GameData.Util; + +public static unsafe class ByteStringFunctions { - public readonly struct GamePath : IComparable + public class NullTerminator { - public const int MaxGamePathLength = 256; + public readonly byte* NullBytePtr; - private readonly string _path; - - private GamePath( string path, bool _ ) - => _path = path; - - public GamePath( string? path ) + public NullTerminator() { - if( path != null && path.Length < MaxGamePathLength ) - { - _path = Lower( Trim( ReplaceSlash( path ) ) ); - } - else - { - _path = string.Empty; - } + NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); + *NullBytePtr = 0; } - public GamePath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : ""; - - private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength; - - private static string Substring( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '\\', '/' ); - - private static string Trim( string path ) - => path.TrimStart( '/' ); - - private static string Lower( string path ) - => path.ToLowerInvariant(); - - public static GamePath GenerateUnchecked( string path ) - => new( path, true ); - - public static GamePath GenerateUncheckedLower( string path ) - => new( Lower( path ), true ); - - public static implicit operator string( GamePath gamePath ) - => gamePath._path; - - public static explicit operator GamePath( string gamePath ) - => new( gamePath ); - - public bool Empty - => _path.Length == 0; - - public string Filename() - { - var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); - return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path.Substring( idx + 1 ); - } - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), - _ => -1, - }; - } - - public override string ToString() - => _path; + ~NullTerminator() + => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); } - public class GamePathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( GamePath ); + private static readonly byte[] LowerCaseBytes = Enumerable.Range( 0, 256 ) + .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) + .ToArray(); - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + public static byte* FromString( string s, out int length ) + { + length = Encoding.UTF8.GetByteCount( s ); + var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); + fixed( char* ptr = s ) { - var token = JToken.Load( reader ); - return token.ToObject< GamePath >(); + Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); } - public override bool CanWrite - => true; + path[ length ] = 0; + return path; + } - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + public static byte* CopyPath( byte* path, int length ) + { + var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); + Functions.MemCpyUnchecked( ret, path, length ); + ret[ length ] = 0; + return ret; + } + + public static int CheckLength( byte* path ) + { + var end = path + int.MaxValue; + for( var it = path; it < end; ++it ) { - if( value != null ) + if( *it == 0 ) { - var v = ( GamePath )value; - serializer.Serialize( writer, v.ToString() ); + return ( int )( it - path ); } } + + throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); + } + + public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + public static int Compare( byte* lhs, int lhsLength, byte* rhs ) + { + var end = lhs + lhsLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + { + var end = lhs + maxLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *lhs == 0 ) + { + return *rhs == 0 ? 0 : -1; + } + + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + + public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; + } + + private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) + => Compare( lhs, lhsLength, rhs ) == 0; + + private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + => Compare( lhs, rhs, maxLength ) == 0; + + + public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; + } + + public static void AsciiToLowerInPlace( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + path[ i ] = LowerCaseBytes[ path[ i ] ]; + } + } + + public static byte* AsciiToLower( byte* path, int length ) + { + var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); + ptr[ length ] = 0; + for( var i = 0; i < length; ++i ) + { + ptr[ i ] = LowerCaseBytes[ path[ i ] ]; + } + + return ptr; + } + + public static bool IsLowerCase( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + if( path[ i ] != LowerCaseBytes[ path[ i ] ] ) + { + return false; + } + } + + return true; + } +} + +public unsafe class AsciiString : IEnumerable< byte >, IEquatable< AsciiString >, IComparable< AsciiString > +{ + private static readonly ByteStringFunctions.NullTerminator Null = new(); + + [Flags] + private enum Flags : byte + { + IsOwned = 0x01, + IsNullTerminated = 0x02, + LowerCaseChecked = 0x04, + IsLowerCase = 0x08, + } + + public readonly IntPtr Path; + public readonly ulong Crc64; + public readonly int Length; + private Flags _flags; + + public bool IsNullTerminated + { + get => _flags.HasFlag( Flags.IsNullTerminated ); + init => _flags = value ? _flags | Flags.IsNullTerminated : _flags & ~ Flags.IsNullTerminated; + } + + public bool IsOwned + { + get => _flags.HasFlag( Flags.IsOwned ); + init => _flags = value ? _flags | Flags.IsOwned : _flags & ~Flags.IsOwned; + } + + public bool IsLowerCase + { + get + { + if( _flags.HasFlag( Flags.LowerCaseChecked ) ) + { + return _flags.HasFlag( Flags.IsLowerCase ); + } + + _flags |= Flags.LowerCaseChecked; + var ret = ByteStringFunctions.IsLowerCase( Ptr, Length ); + if( ret ) + { + _flags |= Flags.IsLowerCase; + } + + return ret; + } + } + + public bool IsEmpty + => Length == 0; + + public AsciiString() + { + Path = ( IntPtr )Null.NullBytePtr; + Length = 0; + IsNullTerminated = true; + IsOwned = false; + _flags |= Flags.LowerCaseChecked | Flags.IsLowerCase; + Crc64 = 0; + } + + public static bool FromString( string? path, out AsciiString ret, bool toLower = false ) + { + if( string.IsNullOrEmpty( path ) ) + { + ret = Empty; + return true; + } + + var p = ByteStringFunctions.FromString( path, out var l ); + if( l != path.Length ) + { + ret = Empty; + return false; + } + + if( toLower ) + { + ByteStringFunctions.AsciiToLowerInPlace( p, l ); + } + + ret = new AsciiString( p, l, true, true, toLower ? true : null ); + return true; + } + + public static AsciiString FromStringUnchecked( string? path, bool? isLower ) + { + if( string.IsNullOrEmpty( path ) ) + { + return Empty; + } + + var p = ByteStringFunctions.FromString( path, out var l ); + return new AsciiString( p, l, true, true, isLower ); + } + + public AsciiString( byte* path ) + : this( path, ByteStringFunctions.CheckLength( path ), true, false ) + { } + + protected AsciiString( byte* path, int length, bool isNullTerminated, bool isOwned, bool? isLower = null ) + { + Length = length; + Path = ( IntPtr )path; + IsNullTerminated = isNullTerminated; + IsOwned = isOwned; + Crc64 = Functions.ComputeCrc64( Span ); + if( isLower != null ) + { + _flags |= Flags.LowerCaseChecked; + if( isLower.Value ) + { + _flags |= Flags.IsLowerCase; + } + } + } + + public ReadOnlySpan< byte > Span + => new(( void* )Path, Length); + + private byte* Ptr + => ( byte* )Path; + + public override string ToString() + => Encoding.ASCII.GetString( Ptr, Length ); + + public IEnumerator< byte > GetEnumerator() + { + for( var i = 0; i < Length; ++i ) + { + yield return Span[ i ]; + } + } + + ~AsciiString() + { + if( IsOwned ) + { + Marshal.FreeHGlobal( Path ); + } + } + + public bool Equals( AsciiString? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + return Crc64 == other.Crc64 && ByteStringFunctions.Equals( Ptr, Length, other.Ptr, other.Length ); + } + + public override int GetHashCode() + => Crc64.GetHashCode(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int CompareTo( AsciiString? other ) + { + if( ReferenceEquals( this, other ) ) + { + return 0; + } + + if( ReferenceEquals( null, other ) ) + { + return 1; + } + + return ByteStringFunctions.Compare( Ptr, Length, other.Ptr, other.Length ); + } + + private bool? IsLowerInternal + => _flags.HasFlag( Flags.LowerCaseChecked ) ? _flags.HasFlag( Flags.IsLowerCase ) : null; + + public AsciiString Clone() + => new(ByteStringFunctions.CopyPath( Ptr, Length ), Length, true, true, IsLowerInternal); + + public AsciiString Substring( int from ) + => from < Length + ? new AsciiString( Ptr + from, Length - from, IsNullTerminated, false, IsLowerInternal ) + : Empty; + + public AsciiString Substring( int from, int length ) + { + Debug.Assert( from >= 0 ); + if( from >= Length ) + { + return Empty; + } + + var maxLength = Length - from; + return length < maxLength + ? new AsciiString( Ptr + from, length, false, false, IsLowerInternal ) + : new AsciiString( Ptr + from, maxLength, true, false, IsLowerInternal ); + } + + public int IndexOf( byte b, int from = 0 ) + { + var end = Ptr + Length; + for( var tmp = Ptr + from; tmp < end; ++tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - Ptr ); + } + } + + return -1; + } + + public int LastIndexOf( byte b, int to = 0 ) + { + var end = Ptr + to; + for( var tmp = Ptr + Length - 1; tmp >= end; --tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - Ptr ); + } + } + + return -1; + } + + + public static readonly AsciiString Empty = new(); +} + +public readonly struct NewGamePath +{ + public const int MaxGamePathLength = 256; + + private readonly AsciiString _string; + + private NewGamePath( AsciiString s ) + => _string = s; + + + public static readonly NewGamePath Empty = new(AsciiString.Empty); + + public static NewGamePath FromStringUnchecked( string? s, bool? isLower ) + => new(AsciiString.FromStringUnchecked( s, isLower )); + + public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( s.IsNullOrEmpty() ) + { + return true; + } + + var substring = s.Replace( '\\', '/' ); + substring.TrimStart( '/' ); + if( substring.Length > MaxGamePathLength ) + { + return false; + } + + if( substring.Length == 0 ) + { + return true; + } + + if( !AsciiString.FromString( substring, out var ascii, toLower ) ) + { + return false; + } + + path = new NewGamePath( ascii ); + return true; + } + + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path, toLower ); + } + + public AsciiString Filename() + { + var idx = _string.LastIndexOf( ( byte )'/' ); + return idx == -1 ? _string : _string.Substring( idx + 1 ); + } + + public override string ToString() + => _string.ToString(); +} + +public readonly struct GamePath : IComparable +{ + public const int MaxGamePathLength = 256; + + private readonly string _path; + + private GamePath( string path, bool _ ) + => _path = path; + + public GamePath( string? path ) + { + if( path != null && path.Length < MaxGamePathLength ) + { + _path = Lower( Trim( ReplaceSlash( path ) ) ); + } + else + { + _path = string.Empty; + } + } + + public GamePath( FileInfo file, DirectoryInfo baseDir ) + => _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : ""; + + private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) + => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength; + + private static string Substring( FileInfo file, DirectoryInfo baseDir ) + => file.FullName.Substring( baseDir.FullName.Length ); + + private static string ReplaceSlash( string path ) + => path.Replace( '\\', '/' ); + + private static string Trim( string path ) + => path.TrimStart( '/' ); + + private static string Lower( string path ) + => path.ToLowerInvariant(); + + public static GamePath GenerateUnchecked( string path ) + => new(path, true); + + public static GamePath GenerateUncheckedLower( string path ) + => new(Lower( path ), true); + + public static implicit operator string( GamePath gamePath ) + => gamePath._path; + + public static explicit operator GamePath( string gamePath ) + => new(gamePath); + + public bool Empty + => _path.Length == 0; + + public string Filename() + { + var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); + return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path[ ( idx + 1 ).. ]; + } + + public int CompareTo( object? rhs ) + { + return rhs switch + { + string path => string.Compare( _path, path, StringComparison.InvariantCulture ), + GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), + _ => -1, + }; + } + + public override string ToString() + => _path; +} + +public class GamePathConverter : JsonConverter +{ + public override bool CanConvert( Type objectType ) + => objectType == typeof( GamePath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ); + return token.ToObject< GamePath >(); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value != null ) + { + var v = ( GamePath )value; + serializer.Serialize( writer, v.ToString() ); + } } } \ No newline at end of file diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs index 6c683863..1fce700d 100644 --- a/Penumbra/Interop/PathResolver.cs +++ b/Penumbra/Interop/PathResolver.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Dalamud.Hooking; @@ -6,81 +8,123 @@ using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Util; +using String = FFXIVClientStructs.STD.String; namespace Penumbra.Interop; public unsafe class PathResolver : IDisposable { - public delegate IntPtr ResolveMdlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + public delegate IntPtr ResolveMdlImcPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); public delegate IntPtr ResolveMtrlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); + public delegate void LoadMtrlTex( IntPtr mtrlResourceHandle ); - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41" )] - public Hook< ResolveMdlPath >? ResolveMdlPathHook; + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", + DetourName = "ResolveMdlPathDetour" )] + public Hook< ResolveMdlImcPath >? ResolveMdlPathHook; - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F" )] - public Hook? ResolveMtrlPathHook; + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", + DetourName = "ResolveMtrlPathDetour" )] + public Hook< ResolveMtrlPath >? ResolveMtrlPathHook; + + [Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] + public Hook< ResolveMdlImcPath >? ResolveImcPathHook; + + [Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41" )] + public Hook< LoadMtrlTex >? LoadMtrlTexHook; private global::Dalamud.Game.ClientState.Objects.Types.GameObject? FindParent( IntPtr drawObject ) => Dalamud.Objects.FirstOrDefault( a => ( ( GameObject* )a.Address )->DrawObject == ( DrawObject* )drawObject ); private readonly byte[] _data = new byte[512]; - private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - var ret = ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ); - var n = Marshal.PtrToStringAnsi( ret )!; - var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; - PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4}\n{n}\n{name}" ); - if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) - { - var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); - if( replacement != null ) - { - for( var i = 0; i < replacement.Length; ++i ) - { - _data[ i ] = ( byte )replacement[ i ]; - } + public static Dictionary< string, ModCollection > Dict = new(); - _data[ replacement.Length ] = 0; - fixed( byte* data = _data ) + private IntPtr WriteData( string characterName, string path ) + { + _data[ 0 ] = ( byte )'|'; + var i = 1; + foreach( var c in characterName ) + { + _data[ i++ ] = ( byte )c; + } + + _data[ i++ ] = ( byte )'|'; + + foreach( var c in path ) + { + _data[ i++ ] = ( byte )c; + } + + _data[ i ] = 0; + fixed( byte* data = _data ) + { + return ( IntPtr )data; + } + } + + private void LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + { + var handle = ( ResourceHandle* )mtrlResourceHandle; + var mtrlName = handle->FileName.ToString(); + if( Dict.TryGetValue( mtrlName, out var collection ) ) + { + var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); + if( numTex != 0 ) + { + PluginLog.Information( $"{mtrlResourceHandle:X} -> {mtrlName} ({collection.Name}), {numTex} Texes" ); + var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); + for( var i = 0; i < numTex; ++i ) { - return ( IntPtr )data; + var texStringPtr = ( IntPtr )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); + var texString = Marshal.PtrToStringAnsi( texStringPtr ) ?? string.Empty; + PluginLog.Information( $"{texStringPtr:X}: {texString}" ); + Dict[ texString ] = collection; } } } - return ret; + LoadMtrlTexHook!.Original( mtrlResourceHandle ); } + private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) + { + if( path == IntPtr.Zero ) + { + return path; + } + + var n = Marshal.PtrToStringAnsi( path ); + if( n == null ) + { + return path; + } + + var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; + PluginLog.Information( $"{drawObject:X} {path:X}\n{n}\n{name}" ); + if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var value ) ) + { + Dict[ n ] = value; + } + else + { + Dict.Remove( n ); + } + + return path; + } + + private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private unsafe IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + private unsafe IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) - { - var ret = ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ); - var n = Marshal.PtrToStringAnsi( ret )!; - var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; - PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4} {unk5:X}\n{n}\n{name}" ); - if( Service.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) - { - var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); - if( replacement != null ) - { - for( var i = 0; i < replacement.Length; ++i ) - { - _data[i] = ( byte )replacement[i]; - } - - _data[replacement.Length] = 0; - fixed( byte* data = _data ) - { - return ( IntPtr )data; - } - } - } - - return ret; - } + => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); public PathResolver() { @@ -92,17 +136,23 @@ public unsafe class PathResolver : IDisposable { ResolveMdlPathHook?.Enable(); ResolveMtrlPathHook?.Enable(); + ResolveImcPathHook?.Enable(); + LoadMtrlTexHook?.Enable(); } public void Disable() { ResolveMdlPathHook?.Disable(); ResolveMtrlPathHook?.Disable(); + ResolveImcPathHook?.Disable(); + LoadMtrlTexHook?.Disable(); } public void Dispose() { ResolveMdlPathHook?.Dispose(); ResolveMtrlPathHook?.Dispose(); + ResolveImcPathHook?.Dispose(); + LoadMtrlTexHook?.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 6f8fbb49..09e9e160 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Hooking; using Dalamud.Logging; +using ImGuiNET; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Structs; @@ -223,8 +224,10 @@ public class ResourceLoader : IDisposable } file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - var gameFsPath = GamePath.GenerateUncheckedLower( file ); - var replacementPath = modManager.ResolveSwappedOrReplacementPath( gameFsPath ); + var gameFsPath = GamePath.GenerateUncheckedLower( file ); + var replacementPath = PathResolver.Dict.TryGetValue( file, out var collection ) + ? collection.ResolveSwappedOrReplacementPath( gameFsPath ) + : modManager.ResolveSwappedOrReplacementPath( gameFsPath ); if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) { PluginLog.Information( "[GetResourceHandler] {0}", file ); @@ -236,6 +239,8 @@ public class ResourceLoader : IDisposable return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } + if (collection != null) + PathResolver.Dict[ replacementPath ] = collection; var path = Encoding.ASCII.GetBytes( replacementPath ); var bPath = stackalloc byte[path.Length + 1]; @@ -261,15 +266,42 @@ public class ResourceLoader : IDisposable } var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); - - var isRooted = Path.IsPathRooted( gameFsPath ); - - if( gameFsPath == null || gameFsPath.Length >= 260 || !isRooted ) + if( gameFsPath is not { Length: < 260 } ) { return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; } - PluginLog.Debug( "loading modded file: {GameFsPath}", gameFsPath ); + + //var collection = gameFsPath.StartsWith( '|' ); + //if( collection ) + //{ + // var end = gameFsPath.IndexOf( '|', 1 ); + // if( end < 0 ) + // { + // PluginLog.Error( $"Unterminated Collection Name {gameFsPath}" ); + // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + // } + // + // var name = gameFsPath[ 1..end ]; + // gameFsPath = gameFsPath[ ( end + 1 ).. ]; + // PluginLog.Debug( "Loading file for {Name}: {GameFsPath}", name, gameFsPath ); + // + // if( !Path.IsPathRooted( gameFsPath ) ) + // { + // var encoding = Encoding.UTF8.GetBytes( gameFsPath ); + // Marshal.Copy( encoding, 0, new IntPtr( pFileDesc->ResourceHandle->FileName() ), encoding.Length ); + // pFileDesc->ResourceHandle->FileName()[ encoding.Length ] = 0; + // pFileDesc->ResourceHandle->FileNameLength -= name.Length + 2; + // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + // } + //} + //else + if( !Path.IsPathRooted( gameFsPath ) ) + { + return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + } + + PluginLog.Debug( "Loading modded file: {GameFsPath}", gameFsPath ); pFileDesc->FileMode = FileMode.LoadUnpackedResource; @@ -282,8 +314,7 @@ public class ResourceLoader : IDisposable Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); pFileDesc->FileDescriptor = fd; - var ret = ReadFile( pFileHandler, pFileDesc, priority, isSync ); - return ret; + return ReadFile( pFileHandler, pFileDesc, priority, isSync ); } public void Enable() diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 1f3ad8de..39f89602 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -58,8 +58,21 @@ public partial class SettingsInterface ImGui.SetClipboardText( address ); } + ref var name = ref node->KeyValuePair.Item2.Value->FileName; ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); + if( name.Capacity > 15 ) + { + ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + } + else + { + fixed( byte* ptr = name.Buffer ) + { + ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + } + } + + //ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); ImGui.TableNextColumn(); ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); node = node->Next(); From c3454f1d16b115c034df24ae29c6c81de1e111a4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Mar 2022 00:40:42 +0100 Subject: [PATCH 0088/2451] Add Byte String stuff, remove Services, cleanup and refactor interop stuff, disable path resolver for the moment --- .../ByteString/ByteStringFunctions.Case.cs | 94 ++ .../ByteStringFunctions.Comparison.cs | 95 ++ .../ByteStringFunctions.Construction.cs | 68 + .../ByteStringFunctions.Manipulation.cs | 45 + Penumbra.GameData/ByteString/FullPath.cs | 96 ++ Penumbra.GameData/ByteString/NewGamePath.cs | 159 +++ Penumbra.GameData/ByteString/NewRelPath.cs | 133 ++ .../ByteString/Utf8String.Access.cs | 75 + .../ByteString/Utf8String.Comparison.cs | 127 ++ .../ByteString/Utf8String.Construction.cs | 214 +++ .../ByteString/Utf8String.Manipulation.cs | 142 ++ Penumbra.GameData/Util/Functions.cs | 94 +- Penumbra.GameData/Util/GamePath.cs | 522 +------ Penumbra/Api/ModsController.cs | 69 +- Penumbra/Api/PenumbraApi.cs | 259 ++-- Penumbra/Configuration.cs | 110 +- Penumbra/Importer/TexToolsMeta.cs | 2 +- Penumbra/Interop/CharacterUtility.cs | 75 + Penumbra/Interop/MusicManager.cs | 71 +- Penumbra/Interop/ObjectReloader.cs | 2 +- Penumbra/Interop/PathResolver.cs | 560 ++++++-- Penumbra/Interop/ResidentResourceManager.cs | 36 + Penumbra/Interop/ResidentResources.cs | 125 -- Penumbra/Interop/ResourceLoader.Debug.cs | 205 +++ .../Interop/ResourceLoader.Replacement.cs | 168 +++ Penumbra/Interop/ResourceLoader.TexMdl.cs | 104 ++ Penumbra/Interop/ResourceLoader.cs | 438 ++---- Penumbra/Interop/Structs/CharacterUtility.cs | 87 ++ Penumbra/Interop/Structs/FileMode.cs | 11 + Penumbra/Interop/Structs/ResourceHandle.cs | 67 + Penumbra/Interop/Structs/SeFileDescriptor.cs | 21 + Penumbra/Meta/MetaCollection.cs | 342 ++--- Penumbra/Meta/MetaManager.cs | 317 ++--- Penumbra/Mod/ModCleanup.cs | 9 +- Penumbra/Mod/ModResources.cs | 2 +- Penumbra/Mods/CollectionManager.cs | 11 +- Penumbra/Mods/ModCollection.cs | 2 +- Penumbra/Mods/ModCollectionCache.cs | 15 +- Penumbra/Penumbra.cs | 84 +- Penumbra/Structs/CharacterUtility.cs | 13 - Penumbra/Structs/FileMode.cs | 12 - Penumbra/Structs/ResourceHandle.cs | 29 - Penumbra/Structs/SeFileDescriptor.cs | 21 - Penumbra/UI/MenuBar.cs | 61 - Penumbra/UI/MenuTabs/TabChangedItems.cs | 95 +- Penumbra/UI/MenuTabs/TabCollections.cs | 59 +- Penumbra/UI/MenuTabs/TabDebug.Model.cs | 131 ++ Penumbra/UI/MenuTabs/TabDebug.cs | 103 +- Penumbra/UI/MenuTabs/TabDebugModels.cs | 148 -- Penumbra/UI/MenuTabs/TabEffective.cs | 334 +++-- Penumbra/UI/MenuTabs/TabImport.cs | 296 ++-- .../UI/MenuTabs/TabInstalled/TabInstalled.cs | 49 +- .../TabInstalled/TabInstalledDetails.cs | 1253 ++++++++--------- .../TabInstalled/TabInstalledDetailsEdit.cs | 12 +- .../TabInstalledDetailsManipulations.cs | 14 +- .../TabInstalled/TabInstalledModPanel.cs | 32 +- .../TabInstalled/TabInstalledSelector.cs | 26 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 108 +- Penumbra/UI/MenuTabs/TabSettings.cs | 63 +- Penumbra/UI/SettingsInterface.cs | 18 +- Penumbra/UI/SettingsMenu.cs | 2 +- Penumbra/Util/FullPath.cs | 85 -- Penumbra/Util/ModelChanger.cs | 1 + Penumbra/Util/RelPath.cs | 1 + Penumbra/Util/Service.cs | 56 - 65 files changed, 4707 insertions(+), 3371 deletions(-) create mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs create mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs create mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs create mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs create mode 100644 Penumbra.GameData/ByteString/FullPath.cs create mode 100644 Penumbra.GameData/ByteString/NewGamePath.cs create mode 100644 Penumbra.GameData/ByteString/NewRelPath.cs create mode 100644 Penumbra.GameData/ByteString/Utf8String.Access.cs create mode 100644 Penumbra.GameData/ByteString/Utf8String.Comparison.cs create mode 100644 Penumbra.GameData/ByteString/Utf8String.Construction.cs create mode 100644 Penumbra.GameData/ByteString/Utf8String.Manipulation.cs create mode 100644 Penumbra/Interop/CharacterUtility.cs create mode 100644 Penumbra/Interop/ResidentResourceManager.cs delete mode 100644 Penumbra/Interop/ResidentResources.cs create mode 100644 Penumbra/Interop/ResourceLoader.Debug.cs create mode 100644 Penumbra/Interop/ResourceLoader.Replacement.cs create mode 100644 Penumbra/Interop/ResourceLoader.TexMdl.cs create mode 100644 Penumbra/Interop/Structs/CharacterUtility.cs create mode 100644 Penumbra/Interop/Structs/FileMode.cs create mode 100644 Penumbra/Interop/Structs/ResourceHandle.cs create mode 100644 Penumbra/Interop/Structs/SeFileDescriptor.cs delete mode 100644 Penumbra/Structs/CharacterUtility.cs delete mode 100644 Penumbra/Structs/FileMode.cs delete mode 100644 Penumbra/Structs/ResourceHandle.cs delete mode 100644 Penumbra/Structs/SeFileDescriptor.cs delete mode 100644 Penumbra/UI/MenuBar.cs create mode 100644 Penumbra/UI/MenuTabs/TabDebug.Model.cs delete mode 100644 Penumbra/UI/MenuTabs/TabDebugModels.cs delete mode 100644 Penumbra/Util/FullPath.cs delete mode 100644 Penumbra/Util/Service.cs diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs new file mode 100644 index 00000000..e1e0970c --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System.Runtime.InteropServices; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + private static readonly byte[] AsciiLowerCaseBytes = Enumerable.Range( 0, 256 ) + .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) + .ToArray(); + + // Convert a byte to its ASCII-lowercase version. + public static byte AsciiToLower( byte b ) + => AsciiLowerCaseBytes[ b ]; + + // Check if a byte is ASCII-lowercase. + public static bool AsciiIsLower( byte b ) + => AsciiToLower( b ) == b; + + // Check if a byte array of given length is ASCII-lowercase. + public static bool IsAsciiLowerCase( byte* path, int length ) + { + var end = path + length; + for( ; path < end; ++path ) + { + if( *path != AsciiLowerCaseBytes[*path] ) + { + return false; + } + } + + return true; + } + + // Compare two byte arrays of given lengths ASCII-case-insensitive. + public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + // Check two byte arrays of given lengths for ASCII-case-insensitive equality. + public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; + } + + // Check if a byte array of given length consists purely of ASCII characters. + public static bool IsAscii( byte* path, int length ) + { + var length8 = length / 8; + var end8 = ( ulong* )path + length8; + for( var ptr8 = ( ulong* )path; ptr8 < end8; ++ptr8 ) + { + if( ( *ptr8 & 0x8080808080808080ul ) != 0 ) + { + return false; + } + } + + var end = path + length; + for( path += length8 * 8; path < end; ++path ) + { + if( *path > 127 ) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs new file mode 100644 index 00000000..3e7382c9 --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs @@ -0,0 +1,95 @@ +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Lexicographically compare two byte arrays of given length. + public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + // Lexicographically compare one byte array of given length with a null-terminated byte array of unknown length. + public static int Compare( byte* lhs, int lhsLength, byte* rhs ) + { + var end = lhs + lhsLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + // Lexicographically compare two null-terminated byte arrays of unknown length not larger than maxLength. + public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + { + var end = lhs + maxLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *lhs == 0 ) + { + return *rhs == 0 ? 0 : -1; + } + + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + // Check two byte arrays of given length for equality. + public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; + } + + // Check one byte array of given length for equality against a null-terminated byte array of unknown length. + private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) + => Compare( lhs, lhsLength, rhs ) == 0; + + // Check two null-terminated byte arrays of unknown length not larger than maxLength for equality. + private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + => Compare( lhs, rhs, maxLength ) == 0; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs new file mode 100644 index 00000000..ca4cadd0 --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Used for static null-terminators. + public class NullTerminator + { + public readonly byte* NullBytePtr; + + public NullTerminator() + { + NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); + *NullBytePtr = 0; + } + + ~NullTerminator() + => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); + } + + // Convert a C# unicode-string to an unmanaged UTF8-byte array and return the pointer. + // If the length would exceed the given maxLength, return a nullpointer instead. + public static byte* Utf8FromString( string s, out int length, int maxLength = int.MaxValue ) + { + length = Encoding.UTF8.GetByteCount( s ); + if( length >= maxLength ) + { + return null; + } + + var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); + fixed( char* ptr = s ) + { + Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); + } + + path[ length ] = 0; + return path; + } + + // Create a copy of a given string and return the pointer. + public static byte* CopyString( byte* path, int length ) + { + var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); + Functions.MemCpyUnchecked( ret, path, length ); + ret[ length ] = 0; + return ret; + } + + // Check the length of a null-terminated byte array no longer than the given maxLength. + public static int CheckLength( byte* path, int maxLength = int.MaxValue ) + { + var end = path + maxLength; + for( var it = path; it < end; ++it ) + { + if( *it == 0 ) + { + return ( int )( it - path ); + } + } + + throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs new file mode 100644 index 00000000..7d21593a --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Replace all occurrences of from in a byte array of known length with to. + public static int Replace( byte* ptr, int length, byte from, byte to ) + { + var end = ptr + length; + var numReplaced = 0; + for( ; ptr < end; ++ptr ) + { + if( *ptr == from ) + { + *ptr = to; + ++numReplaced; + } + } + + return numReplaced; + } + + // Convert a byte array of given length to ASCII-lowercase. + public static void AsciiToLowerInPlace( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + path[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; + } + } + + // Copy a byte array and convert the copy to ASCII-lowercase. + public static byte* AsciiToLower( byte* path, int length ) + { + var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); + ptr[ length ] = 0; + for( var i = 0; i < length; ++i ) + { + ptr[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; + } + + return ptr; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs new file mode 100644 index 00000000..f9c679dc --- /dev/null +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public readonly struct FullPath : IComparable, IEquatable< FullPath > +{ + public readonly string FullName; + public readonly Utf8String InternalName; + public readonly ulong Crc64; + + + public FullPath( DirectoryInfo baseDir, NewRelPath relPath ) + : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) + { } + + public FullPath( FileInfo file ) + : this( file.FullName ) + { } + + public FullPath( string s ) + { + FullName = s; + InternalName = Utf8String.FromString( FullName, out var name, true ) ? name : Utf8String.Empty; + Crc64 = Functions.ComputeCrc64( InternalName.Span ); + } + + public bool Exists + => File.Exists( FullName ); + + public string Extension + => Path.GetExtension( FullName ); + + public string Name + => Path.GetFileName( FullName ); + + public bool ToGamePath( DirectoryInfo dir, out NewGamePath path ) + { + path = NewGamePath.Empty; + if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) + { + return false; + } + + var substring = InternalName.Substring( dir.FullName.Length + 1 ); + + path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) ); + return true; + } + + public bool ToRelPath( DirectoryInfo dir, out NewRelPath path ) + { + path = NewRelPath.Empty; + if( !FullName.StartsWith( dir.FullName ) ) + { + return false; + } + + var substring = InternalName.Substring( dir.FullName.Length + 1 ); + + path = new NewRelPath( substring ); + return true; + } + + public int CompareTo( object? obj ) + => obj switch + { + FullPath p => InternalName.CompareTo( p.InternalName ), + FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), + Utf8String u => InternalName.CompareTo( u ), + string s => string.Compare( FullName, s, StringComparison.InvariantCultureIgnoreCase ), + _ => -1, + }; + + public bool Equals( FullPath other ) + { + if( Crc64 != other.Crc64 ) + { + return false; + } + + if( FullName.Length == 0 || other.FullName.Length == 0 ) + { + return true; + } + + return InternalName.Equals( other.InternalName ); + } + + public override int GetHashCode() + => InternalName.Crc32; + + public override string ToString() + => FullName; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewGamePath.cs b/Penumbra.GameData/ByteString/NewGamePath.cs new file mode 100644 index 00000000..1685f15a --- /dev/null +++ b/Penumbra.GameData/ByteString/NewGamePath.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.GameData.ByteString; + +// NewGamePath wrap some additional validity checking around Utf8String, +// provide some filesystem helpers, and conversion to Json. +[JsonConverter( typeof( NewGamePathConverter ) )] +public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable +{ + public const int MaxGamePathLength = 256; + + public readonly Utf8String Path; + public static readonly NewGamePath Empty = new(Utf8String.Empty); + + internal NewGamePath( Utf8String s ) + => Path = s; + + public int Length + => Path.Length; + + public bool IsEmpty + => Path.IsEmpty; + + public NewGamePath ToLower() + => new(Path.AsciiToLower()); + + public static unsafe bool FromPointer( byte* ptr, out NewGamePath path, bool lower = false ) + { + var utf = new Utf8String( ptr ); + return ReturnChecked( utf, out path, lower ); + } + + public static bool FromSpan( ReadOnlySpan< byte > data, out NewGamePath path, bool lower = false ) + { + var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); + return ReturnChecked( utf, out path, lower ); + } + + // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. + // Does not check for initial slashes either, since they are assumed to be by choice. + // Checks for maxlength, ASCII and lowercase. + private static bool ReturnChecked( Utf8String utf, out NewGamePath path, bool lower = false ) + { + path = Empty; + if( !utf.IsAscii || utf.Length > MaxGamePathLength ) + { + return false; + } + + path = new NewGamePath( lower ? utf.AsciiToLower() : utf ); + return true; + } + + public NewGamePath Clone() + => new(Path.Clone()); + + public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( s.IsNullOrEmpty() ) + { + return true; + } + + var substring = s!.Replace( '\\', '/' ); + substring.TrimStart( '/' ); + if( substring.Length > MaxGamePathLength ) + { + return false; + } + + if( substring.Length == 0 ) + { + return true; + } + + if( !Utf8String.FromString( substring, out var ascii, toLower ) || !ascii.IsAscii ) + { + return false; + } + + path = new NewGamePath( ascii ); + return true; + } + + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path, toLower ); + } + + public Utf8String Filename() + { + var idx = Path.LastIndexOf( ( byte )'/' ); + return idx == -1 ? Path : Path.Substring( idx + 1 ); + } + + public Utf8String Extension() + { + var idx = Path.LastIndexOf( ( byte )'.' ); + return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); + } + + public bool Equals( NewGamePath other ) + => Path.Equals( other.Path ); + + public override int GetHashCode() + => Path.GetHashCode(); + + public int CompareTo( NewGamePath other ) + => Path.CompareTo( other.Path ); + + public override string ToString() + => Path.ToString(); + + public void Dispose() + => Path.Dispose(); + + public bool IsRooted() + => Path.Length >= 1 && ( Path[ 0 ] == '/' || Path[ 0 ] == '\\' ) + || Path.Length >= 2 + && ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' ) + && Path[ 1 ] == ':'; + + private class NewGamePathConverter : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( NewGamePath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ).ToString(); + return FromString( token, out var p, true ) + ? p + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewGamePath )}." ); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value is NewGamePath p ) + { + serializer.Serialize( writer, p.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewRelPath.cs b/Penumbra.GameData/ByteString/NewRelPath.cs new file mode 100644 index 00000000..25b6f9e0 --- /dev/null +++ b/Penumbra.GameData/ByteString/NewRelPath.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.GameData.ByteString; + +[JsonConverter( typeof( NewRelPathConverter ) )] +public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable +{ + public const int MaxRelPathLength = 250; + + public readonly Utf8String Path; + public static readonly NewRelPath Empty = new(Utf8String.Empty); + + internal NewRelPath( Utf8String path ) + => Path = path; + + public static bool FromString( string? s, out NewRelPath path ) + { + path = Empty; + if( s.IsNullOrEmpty() ) + { + return true; + } + + var substring = s!.Replace( '/', '\\' ); + substring.TrimStart( '\\' ); + if( substring.Length > MaxRelPathLength ) + { + return false; + } + + if( substring.Length == 0 ) + { + return true; + } + + if( !Utf8String.FromString( substring, out var ascii ) || !ascii.IsAscii ) + { + return false; + } + + path = new NewRelPath( ascii ); + return true; + } + + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewRelPath path ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path ); + } + + public static bool FromFile( FullPath file, DirectoryInfo baseDir, out NewRelPath path ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path ); + } + + public NewRelPath( NewGamePath gamePath ) + => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); + + public unsafe NewGamePath ToGamePath( int skipFolders = 0 ) + { + var idx = 0; + while( skipFolders > 0 ) + { + idx = Path.IndexOf( ( byte )'\\', idx ) + 1; + --skipFolders; + if( idx <= 0 ) + { + return NewGamePath.Empty; + } + } + + var length = Path.Length - idx; + var ptr = ByteStringFunctions.CopyString( Path.Path + idx, length ); + ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); + ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); + var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); + return new NewGamePath( utf ); + } + + public int CompareTo( NewRelPath rhs ) + => Path.CompareTo( rhs.Path ); + + public bool Equals( NewRelPath other ) + => Path.Equals( other.Path ); + + public override string ToString() + => Path.ToString(); + + public void Dispose() + => Path.Dispose(); + + private class NewRelPathConverter : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( NewRelPath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ).ToString(); + return FromString( token, out var p ) + ? p + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewRelPath )}." ); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value is NewRelPath p ) + { + serializer.Serialize( writer, p.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Access.cs b/Penumbra.GameData/ByteString/Utf8String.Access.cs new file mode 100644 index 00000000..a74dd672 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Access.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.GameData.ByteString; + +// Utf8String is a wrapper around unsafe byte strings. +// It may be used to store owned strings in unmanaged space, +// as well as refer to unowned strings. +// Unowned strings may change their value and thus become corrupt, +// so they should never be stored, just used locally or with great care. +// The string keeps track of whether it is owned or not, it also can keep track +// of some other information, like the string being pure ASCII, ASCII-lowercase or null-terminated. +// Owned strings are always null-terminated. +// Any constructed string will compute its own CRC32-value (as long as the string itself is not changed). +public sealed unsafe partial class Utf8String : IEnumerable< byte > +{ + // We keep information on some of the state of the Utf8String in specific bits. + // This costs some potential max size, but that is not relevant for our case. + // Except for destruction/dispose, or if the non-owned pointer changes values, + // the CheckedFlag, AsciiLowerCaseFlag and AsciiFlag are the only things that are mutable. + private const uint NullTerminatedFlag = 0x80000000; + private const uint OwnedFlag = 0x40000000; + private const uint AsciiCheckedFlag = 0x04000000; + private const uint AsciiFlag = 0x08000000; + private const uint AsciiLowerCheckedFlag = 0x10000000; + private const uint AsciiLowerFlag = 0x20000000; + private const uint FlagMask = 0x03FFFFFF; + + public bool IsNullTerminated + => ( _length & NullTerminatedFlag ) != 0; + + public bool IsOwned + => ( _length & OwnedFlag ) != 0; + + public bool IsAscii + => CheckAscii(); + + public bool IsAsciiLowerCase + => CheckAsciiLower(); + + public byte* Path + => _path; + + public int Crc32 + => _crc32; + + public int Length + => ( int )( _length & FlagMask ); + + public bool IsEmpty + => Length == 0; + + public ReadOnlySpan< byte > Span + => new(_path, Length); + + public byte this[ int idx ] + => ( uint )idx < Length ? _path[ idx ] : throw new IndexOutOfRangeException(); + + public IEnumerator< byte > GetEnumerator() + { + for( var i = 0; i < Length; ++i ) + { + yield return Span[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + // Only not readonly due to dispose. + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() + => _crc32; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs new file mode 100644 index 00000000..c2ca9488 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, IComparable< Utf8String > +{ + public bool Equals( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); + } + + public bool EqualsCi( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) + { + return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); + } + + return ByteStringFunctions.AsciiCaselessEquals( _path, Length, other._path, other.Length ); + } + + public int CompareTo( Utf8String? other ) + { + if( ReferenceEquals( this, other ) ) + { + return 0; + } + + if( ReferenceEquals( null, other ) ) + { + return 1; + } + + return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); + } + + public int CompareToCi( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return 0; + } + + if( ReferenceEquals( this, other ) ) + { + return 1; + } + + if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) + { + return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); + } + + return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); + } + + public bool StartsWith( params char[] chars ) + { + if( chars.Length > Length ) + { + return false; + } + + var ptr = _path; + return chars.All( t => *ptr++ == ( byte )t ); + } + + public bool EndsWith( params char[] chars ) + { + if( chars.Length > Length ) + { + return false; + } + + var ptr = _path + Length - chars.Length; + return chars.All( c => *ptr++ == ( byte )c ); + } + + public int IndexOf( byte b, int from = 0 ) + { + var end = _path + Length; + for( var tmp = _path + from; tmp < end; ++tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - _path ); + } + } + + return -1; + } + + public int LastIndexOf( byte b, int to = 0 ) + { + var end = _path + to; + for( var tmp = _path + Length - 1; tmp >= end; --tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - _path ); + } + } + + return -1; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs new file mode 100644 index 00000000..96ec7313 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -0,0 +1,214 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String : IDisposable +{ + // statically allocated null-terminator for empty strings to point to. + private static readonly ByteStringFunctions.NullTerminator Null = new(); + + public static readonly Utf8String Empty = new(); + + // actual data members. + private byte* _path; + private uint _length; + private int _crc32; + + // Create an empty string. + public Utf8String() + { + _path = Null.NullBytePtr; + _length |= AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag | AsciiFlag; + _crc32 = 0; + } + + // Create a temporary Utf8String from a byte pointer. + // This computes CRC, checks for ASCII and AsciiLower and assumes Null-Termination. + public Utf8String( byte* path ) + { + var length = Functions.ComputeCrc32AsciiLowerAndSize( path, out var crc32, out var lower, out var ascii ); + Setup( path, length, crc32, true, false, lower, ascii ); + } + + // Construct a temporary Utf8String from a given byte string of known size. + // Other known attributes can also be provided and are not computed. + // Can throw ArgumentOutOfRange if length is higher than max length. + // The Crc32 will be computed. + public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) + => FromSpanUnsafe( new ReadOnlySpan< byte >( path, length ), isNullTerminated, isLower, isAscii ); + + // Same as above, just with a span. + public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) + { + fixed( byte* ptr = path ) + { + return new Utf8String().Setup( ptr, path.Length, null, isNullTerminated, false, isLower, isAscii ); + } + } + + // Construct a Utf8String from a given unicode string, possibly converted to ascii lowercase. + // Only returns false if the length exceeds the max length. + public static bool FromString( string? path, out Utf8String ret, bool toAsciiLower = false ) + { + if( string.IsNullOrEmpty( path ) ) + { + ret = Empty; + return true; + } + + var p = ByteStringFunctions.Utf8FromString( path, out var l, ( int )FlagMask ); + if( p == null ) + { + ret = Empty; + return false; + } + + if( toAsciiLower ) + { + ByteStringFunctions.AsciiToLowerInPlace( p, l ); + } + + ret = new Utf8String().Setup( p, l, null, true, true, toAsciiLower ? true : null, l == path.Length ); + return true; + } + + // Does not check for length and just assumes the isLower state from the second argument. + public static Utf8String FromStringUnsafe( string? path, bool? isLower ) + { + if( string.IsNullOrEmpty( path ) ) + { + return Empty; + } + + var p = ByteStringFunctions.Utf8FromString( path, out var l ); + var ret = new Utf8String().Setup( p, l, null, true, true, isLower, l == path.Length ); + return ret; + } + + // Free memory if the string is owned. + private void ReleaseUnmanagedResources() + { + if( !IsOwned ) + { + return; + } + + Marshal.FreeHGlobal( ( IntPtr )_path ); + GC.RemoveMemoryPressure( Length ); + _length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag; + _path = Null.NullBytePtr; + _crc32 = 0; + } + + // Manually free memory. Sets the string to an empty string. + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize( this ); + } + + ~Utf8String() + { + ReleaseUnmanagedResources(); + } + + // Setup from all given values. + // Only called from constructors or factory functions in this library. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + internal Utf8String Setup( byte* path, int length, int? crc32, bool isNullTerminated, bool isOwned, + bool? isLower = null, bool? isAscii = null ) + { + if( length > FlagMask ) + { + throw new ArgumentOutOfRangeException( nameof( length ) ); + } + + _path = path; + _length = ( uint )length; + _crc32 = crc32 ?? ( int )~Lumina.Misc.Crc32.Get( new ReadOnlySpan< byte >( path, length ) ); + if( isNullTerminated ) + { + _length |= NullTerminatedFlag; + } + + if( isOwned ) + { + _length |= OwnedFlag; + } + + if( isLower != null ) + { + _length |= AsciiLowerCheckedFlag; + if( isLower.Value ) + { + _length |= AsciiLowerFlag; + } + } + + if( isAscii != null ) + { + _length |= AsciiCheckedFlag; + if( isAscii.Value ) + { + _length |= AsciiFlag; + } + } + + return this; + } + + private bool CheckAscii() + { + switch( _length & ( AsciiCheckedFlag | AsciiFlag ) ) + { + case AsciiCheckedFlag: return false; + case AsciiCheckedFlag | AsciiFlag: return true; + default: + _length |= AsciiCheckedFlag; + var isAscii = ByteStringFunctions.IsAscii( _path, Length ); + if( isAscii ) + { + _length |= AsciiFlag; + } + + return isAscii; + } + } + + private bool CheckAsciiLower() + { + switch( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) + { + case AsciiLowerCheckedFlag: return false; + case AsciiLowerCheckedFlag | AsciiLowerFlag: return true; + default: + _length |= AsciiLowerCheckedFlag; + var isAsciiLower = ByteStringFunctions.IsAsciiLowerCase( _path, Length ); + if( isAsciiLower ) + { + _length |= AsciiLowerFlag; + } + + return isAsciiLower; + } + } + + private bool? IsAsciiInternal + => ( _length & ( AsciiCheckedFlag | AsciiFlag ) ) switch + { + AsciiCheckedFlag => false, + AsciiFlag => true, + _ => null, + }; + + private bool? IsAsciiLowerInternal + => ( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) switch + { + AsciiLowerCheckedFlag => false, + AsciiLowerFlag => true, + _ => null, + }; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs new file mode 100644 index 00000000..08a38e91 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String +{ + // Create a C# Unicode string from this string. + // If the string is known to be pure ASCII, use that encoding, otherwise UTF8. + public override string ToString() + => Length == 0 + ? string.Empty + : ( _length & AsciiFlag ) != 0 + ? Encoding.ASCII.GetString( _path, Length ) + : Encoding.UTF8.GetString( _path, Length ); + + + // Convert the ascii portion of the string to lowercase. + // Only creates a new string and copy if the string is not already known to be lowercase. + public Utf8String AsciiToLower() + => ( _length & AsciiLowerFlag ) == 0 + ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) + : this; + + // Convert the ascii portion of the string to lowercase. + // Guaranteed to create an owned copy. + public Utf8String AsciiToLowerClone() + => ( _length & AsciiLowerFlag ) == 0 + ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) + : Clone(); + + // Create an owned copy of the given string. + public Utf8String Clone() + { + var ret = new Utf8String(); + ret._length = _length | OwnedFlag | NullTerminatedFlag; + ret._path = ByteStringFunctions.CopyString(Path, Length); + ret._crc32 = Crc32; + return ret; + } + + // Create a non-owning substring from the given position. + // If from is negative or too large, the returned string will be the empty string. + public Utf8String Substring( int from ) + => ( uint )from < Length + ? FromByteStringUnsafe( _path + from, Length - from, IsNullTerminated, IsAsciiLowerInternal, IsAsciiInternal ) + : Empty; + + // Create a non-owning substring from the given position of the given length. + // If from is negative or too large, the returned string will be the empty string. + // If from + length is too large, it will be the same as if length was not specified. + public Utf8String Substring( int from, int length ) + { + var maxLength = Length - ( uint )from; + if( maxLength <= 0 ) + { + return Empty; + } + + return length < maxLength + ? FromByteStringUnsafe( _path + from, length, false, IsAsciiLowerInternal, IsAsciiInternal ) + : Substring( from ); + } + + // Create a owned copy of the string and replace all occurences of from with to in it. + public Utf8String Replace( byte from, byte to ) + { + var length = Length; + var newPtr = ByteStringFunctions.CopyString( _path, length ); + var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to ); + return new Utf8String().Setup( newPtr, length, numReplaced > 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); + } + + // Join a number of strings with a given byte between them. + public static Utf8String Join( byte splitter, params Utf8String[] strings ) + { + var length = strings.Sum( s => s.Length ) + strings.Length; + var data = ( byte* )Marshal.AllocHGlobal( length ); + + var ptr = data; + bool? isLower = ByteStringFunctions.AsciiIsLower( splitter ); + bool? isAscii = splitter < 128; + foreach( var s in strings ) + { + Functions.MemCpyUnchecked( ptr, s.Path, s.Length ); + ptr += s.Length; + *ptr++ = splitter; + isLower = Combine( isLower, s.IsAsciiLowerInternal ); + isAscii &= s.IsAscii; + } + + --length; + data[ length ] = 0; + var ret = FromByteStringUnsafe( data, length, true, isLower, isAscii ); + ret._length |= OwnedFlag; + return ret; + } + + // Split a string and return a list of the substrings delimited by b. + // You can specify the maximum number of splits (if the maximum is reached, the last substring may contain delimiters). + // You can also specify to ignore empty substrings inside delimiters. Those are also not counted for max splits. + public List< Utf8String > Split( byte b, int maxSplits = int.MaxValue, bool removeEmpty = true ) + { + var ret = new List< Utf8String >(); + var start = 0; + for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) + { + if( start + 1 != idx || !removeEmpty ) + { + ret.Add( Substring( start, idx - start ) ); + } + + start = idx + 1; + if( ret.Count == maxSplits - 1 ) + { + break; + } + } + + ret.Add( Substring( start ) ); + return ret; + } + + private static bool? Combine( bool? val1, bool? val2 ) + { + return ( val1, val2 ) switch + { + (null, null) => null, + (null, true) => null, + (null, false) => false, + (true, null) => null, + (true, true) => true, + (true, false) => false, + (false, null) => false, + (false, true) => false, + (false, false) => false, + }; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs index 91dc7799..5d9ab10e 100644 --- a/Penumbra.GameData/Util/Functions.cs +++ b/Penumbra.GameData/Util/Functions.cs @@ -1,5 +1,7 @@ using System; +using System.Reflection; using System.Runtime.InteropServices; +using ByteStringFunctions = Penumbra.GameData.ByteString.ByteStringFunctions; namespace Penumbra.GameData.Util; @@ -41,10 +43,96 @@ public static class Functions return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); } - [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe IntPtr memcpy( byte* dest, byte* src, int count ); + private static readonly uint[] CrcTable = + typeof( Lumina.Misc.Crc32 ).GetField( "CrcTable", BindingFlags.Static | BindingFlags.NonPublic )?.GetValue( null ) as uint[] + ?? throw new Exception( "Could not fetch CrcTable from Lumina." ); - public static unsafe void MemCpyUnchecked( byte* dest, byte* src, int count ) + + public static unsafe int ComputeCrc64LowerAndSize( byte* ptr, out ulong crc64, out int crc32Ret, out bool isLower, out bool isAscii ) + { + var tmp = ptr; + uint crcFolder = 0; + uint crcFile = 0; + var crc32 = uint.MaxValue; + crc64 = 0; + isLower = true; + isAscii = true; + while( true ) + { + var value = *tmp; + if( value == 0 ) + { + break; + } + + if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) + { + isLower = false; + } + + if( value > 0x80 ) + { + isAscii = false; + } + + if( value == ( byte )'/' ) + { + crcFolder = crc32; + crcFile = uint.MaxValue; + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + } + else + { + crcFile = CrcTable[ ( byte )( crcFolder ^ value ) ] ^ ( crcFolder >> 8 ); + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + } + + ++tmp; + } + + var size = ( int )( tmp - ptr ); + crc64 = ~( ( ulong )crcFolder << 32 ) | crcFile; + crc32Ret = ( int )~crc32; + return size; + } + + public static unsafe int ComputeCrc32AsciiLowerAndSize( byte* ptr, out int crc32Ret, out bool isLower, out bool isAscii ) + { + var tmp = ptr; + var crc32 = uint.MaxValue; + isLower = true; + isAscii = true; + while( true ) + { + var value = *tmp; + if( value == 0 ) + { + break; + } + + if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) + { + isLower = false; + } + + if( value > 0x80 ) + { + isAscii = false; + } + + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + ++tmp; + } + + var size = ( int )( tmp - ptr ); + crc32Ret = ( int )~crc32; + return size; + } + + [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe IntPtr memcpy( void* dest, void* src, int count ); + + public static unsafe void MemCpyUnchecked( void* dest, void* src, int count ) => memcpy( dest, src, count ); diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index 4c670605..ded35038 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -1,526 +1,11 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using JsonSerializer = Newtonsoft.Json.JsonSerializer; namespace Penumbra.GameData.Util; -public static unsafe class ByteStringFunctions -{ - public class NullTerminator - { - public readonly byte* NullBytePtr; - - public NullTerminator() - { - NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); - *NullBytePtr = 0; - } - - ~NullTerminator() - => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); - } - - private static readonly byte[] LowerCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) - .ToArray(); - - public static byte* FromString( string s, out int length ) - { - length = Encoding.UTF8.GetByteCount( s ); - var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); - fixed( char* ptr = s ) - { - Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); - } - - path[ length ] = 0; - return path; - } - - public static byte* CopyPath( byte* path, int length ) - { - var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); - Functions.MemCpyUnchecked( ret, path, length ); - ret[ length ] = 0; - return ret; - } - - public static int CheckLength( byte* path ) - { - var end = path + int.MaxValue; - for( var it = path; it < end; ++it ) - { - if( *it == 0 ) - { - return ( int )( it - path ); - } - } - - throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); - } - - public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - public static int Compare( byte* lhs, int lhsLength, byte* rhs ) - { - var end = lhs + lhsLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - { - var end = lhs + maxLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *lhs == 0 ) - { - return *rhs == 0 ? 0 : -1; - } - - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - - public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; - } - - private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) - => Compare( lhs, lhsLength, rhs ) == 0; - - private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - => Compare( lhs, rhs, maxLength ) == 0; - - - public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; - } - - public static void AsciiToLowerInPlace( byte* path, int length ) - { - for( var i = 0; i < length; ++i ) - { - path[ i ] = LowerCaseBytes[ path[ i ] ]; - } - } - - public static byte* AsciiToLower( byte* path, int length ) - { - var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); - ptr[ length ] = 0; - for( var i = 0; i < length; ++i ) - { - ptr[ i ] = LowerCaseBytes[ path[ i ] ]; - } - - return ptr; - } - - public static bool IsLowerCase( byte* path, int length ) - { - for( var i = 0; i < length; ++i ) - { - if( path[ i ] != LowerCaseBytes[ path[ i ] ] ) - { - return false; - } - } - - return true; - } -} - -public unsafe class AsciiString : IEnumerable< byte >, IEquatable< AsciiString >, IComparable< AsciiString > -{ - private static readonly ByteStringFunctions.NullTerminator Null = new(); - - [Flags] - private enum Flags : byte - { - IsOwned = 0x01, - IsNullTerminated = 0x02, - LowerCaseChecked = 0x04, - IsLowerCase = 0x08, - } - - public readonly IntPtr Path; - public readonly ulong Crc64; - public readonly int Length; - private Flags _flags; - - public bool IsNullTerminated - { - get => _flags.HasFlag( Flags.IsNullTerminated ); - init => _flags = value ? _flags | Flags.IsNullTerminated : _flags & ~ Flags.IsNullTerminated; - } - - public bool IsOwned - { - get => _flags.HasFlag( Flags.IsOwned ); - init => _flags = value ? _flags | Flags.IsOwned : _flags & ~Flags.IsOwned; - } - - public bool IsLowerCase - { - get - { - if( _flags.HasFlag( Flags.LowerCaseChecked ) ) - { - return _flags.HasFlag( Flags.IsLowerCase ); - } - - _flags |= Flags.LowerCaseChecked; - var ret = ByteStringFunctions.IsLowerCase( Ptr, Length ); - if( ret ) - { - _flags |= Flags.IsLowerCase; - } - - return ret; - } - } - - public bool IsEmpty - => Length == 0; - - public AsciiString() - { - Path = ( IntPtr )Null.NullBytePtr; - Length = 0; - IsNullTerminated = true; - IsOwned = false; - _flags |= Flags.LowerCaseChecked | Flags.IsLowerCase; - Crc64 = 0; - } - - public static bool FromString( string? path, out AsciiString ret, bool toLower = false ) - { - if( string.IsNullOrEmpty( path ) ) - { - ret = Empty; - return true; - } - - var p = ByteStringFunctions.FromString( path, out var l ); - if( l != path.Length ) - { - ret = Empty; - return false; - } - - if( toLower ) - { - ByteStringFunctions.AsciiToLowerInPlace( p, l ); - } - - ret = new AsciiString( p, l, true, true, toLower ? true : null ); - return true; - } - - public static AsciiString FromStringUnchecked( string? path, bool? isLower ) - { - if( string.IsNullOrEmpty( path ) ) - { - return Empty; - } - - var p = ByteStringFunctions.FromString( path, out var l ); - return new AsciiString( p, l, true, true, isLower ); - } - - public AsciiString( byte* path ) - : this( path, ByteStringFunctions.CheckLength( path ), true, false ) - { } - - protected AsciiString( byte* path, int length, bool isNullTerminated, bool isOwned, bool? isLower = null ) - { - Length = length; - Path = ( IntPtr )path; - IsNullTerminated = isNullTerminated; - IsOwned = isOwned; - Crc64 = Functions.ComputeCrc64( Span ); - if( isLower != null ) - { - _flags |= Flags.LowerCaseChecked; - if( isLower.Value ) - { - _flags |= Flags.IsLowerCase; - } - } - } - - public ReadOnlySpan< byte > Span - => new(( void* )Path, Length); - - private byte* Ptr - => ( byte* )Path; - - public override string ToString() - => Encoding.ASCII.GetString( Ptr, Length ); - - public IEnumerator< byte > GetEnumerator() - { - for( var i = 0; i < Length; ++i ) - { - yield return Span[ i ]; - } - } - - ~AsciiString() - { - if( IsOwned ) - { - Marshal.FreeHGlobal( Path ); - } - } - - public bool Equals( AsciiString? other ) - { - if( ReferenceEquals( null, other ) ) - { - return false; - } - - if( ReferenceEquals( this, other ) ) - { - return true; - } - - return Crc64 == other.Crc64 && ByteStringFunctions.Equals( Ptr, Length, other.Ptr, other.Length ); - } - - public override int GetHashCode() - => Crc64.GetHashCode(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int CompareTo( AsciiString? other ) - { - if( ReferenceEquals( this, other ) ) - { - return 0; - } - - if( ReferenceEquals( null, other ) ) - { - return 1; - } - - return ByteStringFunctions.Compare( Ptr, Length, other.Ptr, other.Length ); - } - - private bool? IsLowerInternal - => _flags.HasFlag( Flags.LowerCaseChecked ) ? _flags.HasFlag( Flags.IsLowerCase ) : null; - - public AsciiString Clone() - => new(ByteStringFunctions.CopyPath( Ptr, Length ), Length, true, true, IsLowerInternal); - - public AsciiString Substring( int from ) - => from < Length - ? new AsciiString( Ptr + from, Length - from, IsNullTerminated, false, IsLowerInternal ) - : Empty; - - public AsciiString Substring( int from, int length ) - { - Debug.Assert( from >= 0 ); - if( from >= Length ) - { - return Empty; - } - - var maxLength = Length - from; - return length < maxLength - ? new AsciiString( Ptr + from, length, false, false, IsLowerInternal ) - : new AsciiString( Ptr + from, maxLength, true, false, IsLowerInternal ); - } - - public int IndexOf( byte b, int from = 0 ) - { - var end = Ptr + Length; - for( var tmp = Ptr + from; tmp < end; ++tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - Ptr ); - } - } - - return -1; - } - - public int LastIndexOf( byte b, int to = 0 ) - { - var end = Ptr + to; - for( var tmp = Ptr + Length - 1; tmp >= end; --tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - Ptr ); - } - } - - return -1; - } - - - public static readonly AsciiString Empty = new(); -} - -public readonly struct NewGamePath -{ - public const int MaxGamePathLength = 256; - - private readonly AsciiString _string; - - private NewGamePath( AsciiString s ) - => _string = s; - - - public static readonly NewGamePath Empty = new(AsciiString.Empty); - - public static NewGamePath FromStringUnchecked( string? s, bool? isLower ) - => new(AsciiString.FromStringUnchecked( s, isLower )); - - public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) - { - path = Empty; - if( s.IsNullOrEmpty() ) - { - return true; - } - - var substring = s.Replace( '\\', '/' ); - substring.TrimStart( '/' ); - if( substring.Length > MaxGamePathLength ) - { - return false; - } - - if( substring.Length == 0 ) - { - return true; - } - - if( !AsciiString.FromString( substring, out var ascii, toLower ) ) - { - return false; - } - - path = new NewGamePath( ascii ); - return true; - } - - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ baseDir.FullName.Length.. ]; - return FromString( substring, out path, toLower ); - } - - public AsciiString Filename() - { - var idx = _string.LastIndexOf( ( byte )'/' ); - return idx == -1 ? _string : _string.Substring( idx + 1 ); - } - - public override string ToString() - => _string.ToString(); -} - public readonly struct GamePath : IComparable { public const int MaxGamePathLength = 256; @@ -532,7 +17,7 @@ public readonly struct GamePath : IComparable public GamePath( string? path ) { - if( path != null && path.Length < MaxGamePathLength ) + if( path is { Length: < MaxGamePathLength } ) { _path = Lower( Trim( ReplaceSlash( path ) ) ); } @@ -617,4 +102,5 @@ public class GamePathConverter : JsonConverter serializer.Serialize( writer, v.ToString() ); } } -} \ No newline at end of file +} + diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 277f4a3a..1f03dfc5 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -3,47 +3,42 @@ using System.Linq; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; -using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class ModsController : WebApiController { - public class ModsController : WebApiController + private readonly Penumbra _penumbra; + + public ModsController( Penumbra penumbra ) + => _penumbra = penumbra; + + [Route( HttpVerbs.Get, "/mods" )] + public object? GetMods() { - private readonly Penumbra _penumbra; + return Penumbra.ModManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new + { + x.Settings.Enabled, + x.Settings.Priority, + x.Data.BasePath.Name, + x.Data.Meta, + BasePath = x.Data.BasePath.FullName, + Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), + } ) + ?? null; + } - public ModsController( Penumbra penumbra ) - => _penumbra = penumbra; + [Route( HttpVerbs.Post, "/mods" )] + public object CreateMod() + => new { }; - [Route( HttpVerbs.Get, "/mods" )] - public object? GetMods() - { - var modManager = Service< ModManager >.Get(); - return modManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new - { - x.Settings.Enabled, - x.Settings.Priority, - x.Data.BasePath.Name, - x.Data.Meta, - BasePath = x.Data.BasePath.FullName, - Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), - } ) - ?? null; - } - - [Route( HttpVerbs.Post, "/mods" )] - public object CreateMod() - => new { }; - - [Route( HttpVerbs.Get, "/files" )] - public object GetFiles() - { - var modManager = Service< ModManager >.Get(); - return modManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( - o => ( string )o.Key, - o => o.Value.FullName - ) - ?? new Dictionary< string, string >(); - } + [Route( HttpVerbs.Get, "/files" )] + public object GetFiles() + { + return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + o => ( string )o.Key, + o => o.Value.FullName + ) + ?? new Dictionary< string, string >(); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5190bc07..39bcbd35 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,166 +1,157 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class PenumbraApi : IDisposable, IPenumbraApi { - public class PenumbraApi : IDisposable, IPenumbraApi + public int ApiVersion { get; } = 3; + private Penumbra? _penumbra; + private Lumina.GameData? _lumina; + + public bool Valid + => _penumbra != null; + + public PenumbraApi( Penumbra penumbra ) { - public int ApiVersion { get; } = 3; - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; + _penumbra = penumbra; + _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); + } - public bool Valid - => _penumbra != null; + public void Dispose() + { + _penumbra = null; + _lumina = null; + } - public PenumbraApi( Penumbra penumbra ) + public event ChangedItemClick? ChangedItemClicked; + public event ChangedItemHover? ChangedItemTooltip; + + internal bool HasTooltip + => ChangedItemTooltip != null; + + internal void InvokeTooltip( object? it ) + => ChangedItemTooltip?.Invoke( it ); + + internal void InvokeClick( MouseButton button, object? it ) + => ChangedItemClicked?.Invoke( button, it ); + + + private void CheckInitialized() + { + if( !Valid ) { - _penumbra = penumbra; - _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + throw new Exception( "PluginShare is not initialized." ); + } + } + + public void RedrawObject( string name, RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawObject( name, setting ); + } + + public void RedrawObject( GameObject? gameObject, RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); + } + + public void RedrawAll( RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawAll( setting ); + } + + private static string ResolvePath( string path, ModManager manager, ModCollection collection ) + { + if( !Penumbra.Config.IsEnabled ) + { + return path; } - public void Dispose() + var gamePath = new GamePath( path ); + var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + ret ??= path; + return ret; + } + + public string ResolvePath( string path ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, Penumbra.ModManager.Collections.DefaultCollection ); + } + + public string ResolvePath( string path, string characterName ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, + Penumbra.ModManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection ) + ? collection + : ModCollection.Empty ); + } + + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + { + CheckInitialized(); + try { - _penumbra = null; - _lumina = null; - } - - public event ChangedItemClick? ChangedItemClicked; - public event ChangedItemHover? ChangedItemTooltip; - - internal bool HasTooltip - => ChangedItemTooltip != null; - - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); - - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); - - - private void CheckInitialized() - { - if( !Valid ) + if( Path.IsPathRooted( resolvedPath ) ) { - throw new Exception( "PluginShare is not initialized." ); - } - } - - public void RedrawObject( string name, RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawObject( name, setting ); - } - - public void RedrawObject( GameObject? gameObject, RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); - } - - public void RedrawAll( RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawAll( setting ); - } - - private static string ResolvePath( string path, ModManager manager, ModCollection collection ) - { - if( !Penumbra.Config.IsEnabled ) - { - return path; + return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - var gamePath = new GamePath( path ); - var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= path; - return ret; + return Dalamud.GameData.GetFile< T >( resolvedPath ); } - - public string ResolvePath( string path ) + catch( Exception e ) { - CheckInitialized(); - var modManager = Service< ModManager >.Get(); - return ResolvePath( path, modManager, modManager.Collections.DefaultCollection ); + PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + return null; } + } - public string ResolvePath( string path, string characterName ) + public T? GetFile< T >( string gamePath ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath ) ); + + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + { + CheckInitialized(); + try { - CheckInitialized(); - var modManager = Service< ModManager >.Get(); - return ResolvePath( path, modManager, - modManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection ) - ? collection - : ModCollection.Empty ); + if( !Penumbra.ModManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) + { + collection = ModCollection.Empty; + } + + if( collection.Cache != null ) + { + return collection.Cache.ChangedItems; + } + + PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); + return new Dictionary< string, object? >(); } - - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + catch( Exception e ) { - CheckInitialized(); - try - { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } - - return Dalamud.GameData.GetFile< T >( resolvedPath ); - } - catch( Exception e ) - { - PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); - return null; - } - } - - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); - - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); - - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) - { - CheckInitialized(); - try - { - var modManager = Service< ModManager >.Get(); - if( !modManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) - { - collection = ModCollection.Empty; - } - - if( collection.Cache != null ) - { - return collection.Cache.ChangedItems; - } - - PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); - - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); - throw; - } + PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + throw; } } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9c6142a8..8d360422 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,69 +1,73 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Numerics; using Dalamud.Configuration; using Dalamud.Logging; -namespace Penumbra +namespace Penumbra; + +[Serializable] +public class Configuration : IPluginConfiguration { - [Serializable] - public class Configuration : IPluginConfiguration + private const int CurrentVersion = 1; + + public int Version { get; set; } = CurrentVersion; + + public bool IsEnabled { get; set; } = true; +#if DEBUG + public bool DebugMode { get; set; } = true; +#else + public bool DebugMode { get; set; } = false; +#endif + public bool ScaleModSelector { get; set; } = false; + public bool ShowAdvanced { get; set; } + + public bool DisableFileSystemNotifications { get; set; } + + public bool DisableSoundStreaming { get; set; } = true; + public bool EnableHttpApi { get; set; } + public bool EnablePlayerWatch { get; set; } = false; + public int WaitFrames { get; set; } = 30; + + public string ModDirectory { get; set; } = string.Empty; + public string TempDirectory { get; set; } = string.Empty; + + public string CurrentCollection { get; set; } = "Default"; + public string DefaultCollection { get; set; } = "Default"; + public string ForcedCollection { get; set; } = ""; + + public bool SortFoldersFirst { get; set; } = false; + public bool HasReadCharacterCollectionDesc { get; set; } = false; + + public Dictionary< string, string > CharacterCollections { get; set; } = new(); + public Dictionary< string, string > ModSortOrder { get; set; } = new(); + + public bool InvertModListOrder { internal get; set; } + + public static Configuration Load() { - private const int CurrentVersion = 1; - - public int Version { get; set; } = CurrentVersion; - - public bool IsEnabled { get; set; } = true; - - public bool ScaleModSelector { get; set; } = false; - public bool ShowAdvanced { get; set; } - - public bool DisableFileSystemNotifications { get; set; } - - public bool DisableSoundStreaming { get; set; } = true; - public bool EnableHttpApi { get; set; } - public bool EnablePlayerWatch { get; set; } = false; - public int WaitFrames { get; set; } = 30; - - public string ModDirectory { get; set; } = string.Empty; - public string TempDirectory { get; set; } = string.Empty; - - public string CurrentCollection { get; set; } = "Default"; - public string DefaultCollection { get; set; } = "Default"; - public string ForcedCollection { get; set; } = ""; - - public bool SortFoldersFirst { get; set; } = false; - public bool HasReadCharacterCollectionDesc { get; set; } = false; - - public Dictionary< string, string > CharacterCollections { get; set; } = new(); - public Dictionary< string, string > ModSortOrder { get; set; } = new(); - - public bool InvertModListOrder { internal get; set; } - - public static Configuration Load() + var configuration = Dalamud.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + if( configuration.Version == CurrentVersion ) { - var configuration = Dalamud.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - if( configuration.Version == CurrentVersion ) - { - return configuration; - } - - MigrateConfiguration.Version0To1( configuration ); - configuration.Save(); - return configuration; } - public void Save() + MigrateConfiguration.Version0To1( configuration ); + configuration.Save(); + + return configuration; + } + + public void Save() + { + try { - try - { - Dalamud.PluginInterface.SavePluginConfig( this ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); - } + Dalamud.PluginInterface.SavePluginConfig( this ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index e801a477..343c128c 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -155,7 +155,7 @@ namespace Penumbra.Importer { try { - if( !Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation ) ) + if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) ) { Manipulations.Add( manipulation ); } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs new file mode 100644 index 00000000..f73c654f --- /dev/null +++ b/Penumbra/Interop/CharacterUtility.cs @@ -0,0 +1,75 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop; + +public unsafe class CharacterUtility : IDisposable +{ + // A static pointer to the CharacterUtility address. + [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] + private readonly Structs.CharacterUtility** _characterUtilityAddress = null; + + // The initial function in which all the character resources get loaded. + public delegate void LoadDataFilesDelegate( Structs.CharacterUtility* characterUtility ); + + [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] + public Hook< LoadDataFilesDelegate > LoadDataFilesHook = null!; + + public Structs.CharacterUtility* Address + => *_characterUtilityAddress; + + public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumResources]; + + public CharacterUtility() + { + SignatureHelper.Initialise( this ); + LoadDataFilesHook.Enable(); + } + + // Self-disabling hook to set default resources after loading them. + private void LoadDataFilesDetour( Structs.CharacterUtility* characterUtility ) + { + LoadDataFilesHook.Original( characterUtility ); + LoadDefaultResources(); + PluginLog.Debug( "Character Utility resources loaded and defaults stored, disabling hook." ); + LoadDataFilesHook.Disable(); + } + + // We store the default data of the resources so we can always restore them. + private void LoadDefaultResources() + { + for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ i ]; + DefaultResources[ i ] = resource->GetData(); + } + } + + // Set the data of one of the stored resources to a given pointer and length. + public bool SetResource( int idx, IntPtr data, int length ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; + var ret = resource->SetData( data, length ); + PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", idx, ( ulong )data, length ); + return ret; + } + + // Reset the data of one of the stored resources to its default values. + public void ResetResource( int idx ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; + resource->SetData( DefaultResources[ idx ].Address, DefaultResources[ idx ].Size ); + } + + public void Dispose() + { + for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + { + ResetResource( i ); + } + + LoadDataFilesHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/MusicManager.cs b/Penumbra/Interop/MusicManager.cs index 7c46427c..39885eef 100644 --- a/Penumbra/Interop/MusicManager.cs +++ b/Penumbra/Interop/MusicManager.cs @@ -1,46 +1,39 @@ using System; using Dalamud.Logging; -namespace Penumbra.Interop +namespace Penumbra.Interop; + +// Use this to disable streaming of specific soundfiles, +// which will allow replacement of .scd files. +public unsafe class MusicManager { - // Use this to disable streaming of specific soundfiles, - // which will allow replacement of .scd files. - public unsafe class MusicManager + private readonly IntPtr _musicManager; + + public MusicManager() { - private readonly IntPtr _musicManager; - - public MusicManager( ) - { - var framework = Dalamud.Framework.Address.BaseAddress; - - // the wildcard is basically the framework offset we want (lol) - // .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h] - // .text:0000000000090521 39 78 20 cmp [rax+20h], edi - // .text:0000000000090524 0F 94 C2 setz dl - // .text:0000000000090527 45 33 C0 xor r8d, r8d - // .text:000000000009052A E8 41 1C 15 00 call musicInit - var musicInitCallLocation = Dalamud.SigScanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); - var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); - PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", - musicInitCallLocation.ToInt64(), musicManagerOffset ); - _musicManager = *( IntPtr* )( framework + musicManagerOffset ); - PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); - } - - public bool StreamingEnabled - { - get => *( bool* )( _musicManager + 50 ); - private set - { - PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); - *( bool* )( _musicManager + 50 ) = value; - } - } - - public void EnableStreaming() - => StreamingEnabled = true; - - public void DisableStreaming() - => StreamingEnabled = false; + var framework = Dalamud.Framework.Address.BaseAddress; + // The wildcard is the offset in framework to the MusicManager in Framework. + var musicInitCallLocation = Dalamud.SigScanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); + var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); + PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", + musicInitCallLocation.ToInt64(), musicManagerOffset ); + _musicManager = *( IntPtr* )( framework + musicManagerOffset ); + PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); } + + public bool StreamingEnabled + { + get => *( bool* )( _musicManager + 50 ); + private set + { + PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); + *( bool* )( _musicManager + 50 ) = value; + } + } + + public void EnableStreaming() + => StreamingEnabled = true; + + public void DisableStreaming() + => StreamingEnabled = false; } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index b8000f9e..2b497874 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -354,7 +354,7 @@ namespace Penumbra.Interop { if( actor != null ) { - RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), settings ); + RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), RedrawType.WithoutSettings ); // TODO settings ); } } diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs index 1fce700d..b94c2e86 100644 --- a/Penumbra/Interop/PathResolver.cs +++ b/Penumbra/Interop/PathResolver.cs @@ -1,158 +1,440 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Util; -using String = FFXIVClientStructs.STD.String; namespace Penumbra.Interop; public unsafe class PathResolver : IDisposable { - public delegate IntPtr ResolveMdlImcPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); - public delegate IntPtr ResolveMtrlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); - public delegate void LoadMtrlTex( IntPtr mtrlResourceHandle ); - - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", - DetourName = "ResolveMdlPathDetour" )] - public Hook< ResolveMdlImcPath >? ResolveMdlPathHook; - - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", - DetourName = "ResolveMtrlPathDetour" )] - public Hook< ResolveMtrlPath >? ResolveMtrlPathHook; - - [Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] - public Hook< ResolveMdlImcPath >? ResolveImcPathHook; - - [Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41" )] - public Hook< LoadMtrlTex >? LoadMtrlTexHook; - - private global::Dalamud.Game.ClientState.Objects.Types.GameObject? FindParent( IntPtr drawObject ) - => Dalamud.Objects.FirstOrDefault( a => ( ( GameObject* )a.Address )->DrawObject == ( DrawObject* )drawObject ); - - private readonly byte[] _data = new byte[512]; - - public static Dictionary< string, ModCollection > Dict = new(); - - private IntPtr WriteData( string characterName, string path ) - { - _data[ 0 ] = ( byte )'|'; - var i = 1; - foreach( var c in characterName ) - { - _data[ i++ ] = ( byte )c; - } - - _data[ i++ ] = ( byte )'|'; - - foreach( var c in path ) - { - _data[ i++ ] = ( byte )c; - } - - _data[ i ] = 0; - fixed( byte* data = _data ) - { - return ( IntPtr )data; - } - } - - private void LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) - { - var handle = ( ResourceHandle* )mtrlResourceHandle; - var mtrlName = handle->FileName.ToString(); - if( Dict.TryGetValue( mtrlName, out var collection ) ) - { - var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); - if( numTex != 0 ) - { - PluginLog.Information( $"{mtrlResourceHandle:X} -> {mtrlName} ({collection.Name}), {numTex} Texes" ); - var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); - for( var i = 0; i < numTex; ++i ) - { - var texStringPtr = ( IntPtr )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); - var texString = Marshal.PtrToStringAnsi( texStringPtr ) ?? string.Empty; - PluginLog.Information( $"{texStringPtr:X}: {texString}" ); - Dict[ texString ] = collection; - } - } - } - - LoadMtrlTexHook!.Original( mtrlResourceHandle ); - } - - private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) - { - if( path == IntPtr.Zero ) - { - return path; - } - - var n = Marshal.PtrToStringAnsi( path ); - if( n == null ) - { - return path; - } - - var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; - PluginLog.Information( $"{drawObject:X} {path:X}\n{n}\n{name}" ); - if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var value ) ) - { - Dict[ n ] = value; - } - else - { - Dict.Remove( n ); - } - - return path; - } - - private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private unsafe IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private unsafe IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) - => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - public PathResolver() - { - SignatureHelper.Initialise( this ); - Enable(); - } - - public void Enable() - { - ResolveMdlPathHook?.Enable(); - ResolveMtrlPathHook?.Enable(); - ResolveImcPathHook?.Enable(); - LoadMtrlTexHook?.Enable(); - } - - public void Disable() - { - ResolveMdlPathHook?.Disable(); - ResolveMtrlPathHook?.Disable(); - ResolveImcPathHook?.Disable(); - LoadMtrlTexHook?.Disable(); - } - + //public delegate IntPtr ResolveMdlImcPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + //public delegate IntPtr ResolveMtrlPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); + //public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); + //public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + //public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + //public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", + // DetourName = "ResolveMdlPathDetour" )] + //public Hook< ResolveMdlImcPathDelegate >? ResolveMdlPathHook; + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", + // DetourName = "ResolveMtrlPathDetour" )] + //public Hook< ResolveMtrlPathDelegate >? ResolveMtrlPathHook; + // + //[Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] + //public Hook< ResolveMdlImcPathDelegate >? ResolveImcPathHook; + // + //[Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41", DetourName = "LoadMtrlTexDetour" )] + //public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; + // + //[Signature( "?? 89 ?? ?? ?? 57 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 44 ?? ?? ?? ?? ?? ?? ?? 4C", + // DetourName = "LoadMtrlShpkDetour" )] + //public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook; + // + //[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] + //public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; + // + //[Signature( + // "40 ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 48 8B ?? 48 8B ?? ?? ?? ?? ?? E8 ?? ?? ?? ?? ?? BB" )] + //public Hook< EnableDrawDelegate >? EnableDrawHook; + // + //[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", + // DetourName = "CharacterBaseDestructorDetour" )] + //public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook; + // + //public delegate void UpdateModelDelegate( IntPtr drawObject ); + // + //[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )] + //public Hook< UpdateModelDelegate >? UpdateModelsHook; + // + //public delegate void SetupConnectorModelAttributesDelegate( IntPtr drawObject, IntPtr unk ); + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 41 ?? 41 ?? 41 ?? 41 ?? 48 83 ?? ?? 8B ?? ?? ?? 4C", + // DetourName = "SetupConnectorModelAttributesDetour" )] + //public Hook< SetupConnectorModelAttributesDelegate >? SetupConnectorModelAttributesHook; + // + //public delegate void SetupModelAttributesDelegate( IntPtr drawObject ); + // + //[Signature( "48 89 6C 24 ?? 56 57 41 54 41 55 41 56 48 83 EC 20", DetourName = "SetupModelAttributesDetour" )] + //public Hook< SetupModelAttributesDelegate >? SetupModelAttributesHook; + // + //[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", + // DetourName = "GetSlotEqpFlagIndirectDetour" )] + //public Hook< SetupModelAttributesDelegate >? GetSlotEqpFlagIndirectHook; + // + //public delegate void ApplyVisorStuffDelegate( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ); + // + //[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "ApplyVisorStuffDetour" )] + //public Hook< ApplyVisorStuffDelegate >? ApplyVisorStuffHook; + // + //private readonly ResourceLoader _loader; + //private readonly ResidentResourceManager _resident; + //internal readonly Dictionary< IntPtr, int > _drawObjectToObject = new(); + //internal readonly Dictionary< Utf8String, ModCollection > _pathCollections = new(); + // + //internal GameObject* _lastGameObject = null; + //internal DrawObject* _lastDrawObject = null; + // + //private bool EqpDataChanged = false; + //private IntPtr DefaultEqpData; + //private int DefaultEqpLength; + // + //private void ApplyVisorStuffDetour( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ) + //{ + // PluginLog.Information( $"{drawObject:X} {unk1:X} {unk2} {unk3:X} {unk4} {unk5} {( ulong )FindParent( drawObject ):X}" ); + // ApplyVisorStuffHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); + //} + // + //private void GetSlotEqpFlagIndirectDetour( IntPtr drawObject ) + //{ + // if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) + // { + // return; + // } + // + // ChangeEqp( drawObject ); + // GetSlotEqpFlagIndirectHook!.Original( drawObject ); + // RestoreEqp(); + //} + // + //private void ChangeEqp( IntPtr drawObject ) + //{ + // var parent = FindParent( drawObject ); + // if( parent == null ) + // { + // return; + // } + // + // var name = new Utf8String( parent->Name ); + // if( name.Length == 0 ) + // { + // return; + // } + // + // var charName = name.ToString(); + // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) + // { + // collection = Service< ModManager >.Get().Collections.DefaultCollection; + // } + // + // if( collection.Cache == null ) + // { + // collection = Service< ModManager >.Get().Collections.ForcedCollection; + // } + // + // var data = collection.Cache?.MetaManipulations.EqpData; + // if( data == null || data.Length == 0 ) + // { + // return; + // } + // + // _resident.CharacterUtility->EqpResource->SetData( data ); + // PluginLog.Information( $"Changed eqp data to {collection.Name}." ); + // EqpDataChanged = true; + //} + // + //private void RestoreEqp() + //{ + // if( !EqpDataChanged ) + // { + // return; + // } + // + // _resident.CharacterUtility->EqpResource->SetData( new ReadOnlySpan< byte >( ( void* )DefaultEqpData, DefaultEqpLength ) ); + // PluginLog.Information( $"Changed eqp data back." ); + // EqpDataChanged = false; + //} + // + //private void SetupModelAttributesDetour( IntPtr drawObject ) + //{ + // ChangeEqp( drawObject ); + // SetupModelAttributesHook!.Original( drawObject ); + // RestoreEqp(); + //} + // + //private void UpdateModelsDetour( IntPtr drawObject ) + //{ + // if( *( int* )( drawObject + 0x90c ) == 0 ) + // { + // return; + // } + // + // ChangeEqp( drawObject ); + // UpdateModelsHook!.Original.Invoke( drawObject ); + // RestoreEqp(); + //} + // + //private void SetupConnectorModelAttributesDetour( IntPtr drawObject, IntPtr unk ) + //{ + // ChangeEqp( drawObject ); + // SetupConnectorModelAttributesHook!.Original.Invoke( drawObject, unk ); + // RestoreEqp(); + //} + // + //private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + //{ + // _lastGameObject = ( GameObject* )gameObject; + // EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); + // _lastGameObject = null; + //} + // + //private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + //{ + // var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); + // if( _lastGameObject != null ) + // { + // _drawObjectToObject[ ret ] = _lastGameObject->ObjectIndex; + // } + // + // return ret; + //} + // + //private void CharacterBaseDestructorDetour( IntPtr drawBase ) + //{ + // _drawObjectToObject.Remove( drawBase ); + // CharacterBaseDestructorHook!.Original.Invoke( drawBase ); + //} + // + //private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + //{ + // gameObject = ( GameObject* )( Dalamud.Objects[ gameObjectIdx ]?.Address ?? IntPtr.Zero ); + // if( gameObject != null && gameObject->DrawObject == ( DrawObject* )drawObject ) + // { + // return true; + // } + // + // _drawObjectToObject.Remove( drawObject ); + // return false; + //} + // + //private GameObject* FindParent( IntPtr drawObject ) + //{ + // if( _drawObjectToObject.TryGetValue( drawObject, out var gameObjectIdx ) ) + // { + // if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) ) + // { + // return gameObject; + // } + // + // _drawObjectToObject.Remove( drawObject ); + // } + // + // if( _lastGameObject != null && ( _lastGameObject->DrawObject == null || _lastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + // { + // return _lastGameObject; + // } + // + // return null; + //} + // + //private void SetCollection( Utf8String path, ModCollection? collection ) + //{ + // if( collection == null ) + // { + // _pathCollections.Remove( path ); + // } + // else if( _pathCollections.ContainsKey( path ) ) + // { + // _pathCollections[ path ] = collection; + // } + // else + // { + // _pathCollections[ path.Clone() ] = collection; + // } + //} + // + //private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) + //{ + // if( mtrlResourceHandle == IntPtr.Zero ) + // { + // return; + // } + // + // var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); + // if( numTex == 0 ) + // { + // return; + // } + // + // var handle = ( Structs.ResourceHandle* )mtrlResourceHandle; + // var mtrlName = Utf8String.FromSpanUnsafe( handle->FileNameSpan(), true, null, true ); + // var collection = _pathCollections.TryGetValue( mtrlName, out var c ) ? c : null; + // var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); + // for( var i = 0; i < numTex; ++i ) + // { + // var texStringPtr = ( byte* )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); + // var texString = new Utf8String( texStringPtr ); + // SetCollection( texString, collection ); + // } + //} + // + //private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + //{ + // LoadMtrlTexHelper( mtrlResourceHandle ); + // return LoadMtrlTexHook!.Original( mtrlResourceHandle ); + //} + // + //private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) + // => LoadMtrlShpkHook!.Original( mtrlResourceHandle ); + // + //private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) + //{ + // if( path == IntPtr.Zero ) + // { + // return path; + // } + // + // var p = new Utf8String( ( byte* )path ); + // + // var parent = FindParent( drawObject ); + // if( parent == null ) + // { + // return path; + // } + // + // var name = new Utf8String( parent->Name ); + // if( name.Length == 0 ) + // { + // return path; + // } + // + // var charName = name.ToString(); + // var gamePath = new Utf8String( ( byte* )path ); + // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) + // { + // SetCollection( gamePath, null ); + // return path; + // } + // + // SetCollection( gamePath, collection ); + // return path; + //} + // + //private IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + //{ + // ChangeEqp( drawObject ); + // var ret = ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + // RestoreEqp(); + // return ret; + //} + // + //private IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + // => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + // + //private IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) + // => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // + //public PathResolver( ResourceLoader loader, ResidentResourceManager resident ) + //{ + // _loader = loader; + // _resident = resident; + // SignatureHelper.Initialise( this ); + // var data = _resident.CharacterUtility->EqpResource->GetData(); + // fixed( byte* ptr = data ) + // { + // DefaultEqpData = ( IntPtr )ptr; + // } + // + // DefaultEqpLength = data.Length; + // Enable(); + // foreach( var gameObject in Dalamud.Objects ) + // { + // var ptr = ( GameObject* )gameObject.Address; + // if( ptr->IsCharacter() && ptr->DrawObject != null ) + // { + // _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ptr->ObjectIndex; + // } + // } + //} + // + // + //private (FullPath?, object?) CharacterReplacer( NewGamePath path ) + //{ + // var modManager = Service< ModManager >.Get(); + // var gamePath = new GamePath( path.ToString() ); + // var nonDefault = _pathCollections.TryGetValue( path.Path, out var collection ); + // if( !nonDefault ) + // { + // collection = modManager.Collections.DefaultCollection; + // } + // + // var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); + // if( resolved == null ) + // { + // resolved = modManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); + // if( resolved == null ) + // { + // return ( null, collection ); + // } + // + // collection = modManager.Collections.ForcedCollection; + // } + // + // var fullPath = new FullPath( resolved ); + // if( nonDefault ) + // { + // SetCollection( fullPath.InternalName, nonDefault ? collection : null ); + // } + // + // return ( fullPath, collection ); + //} + // + //public void Enable() + //{ + // ResolveMdlPathHook?.Enable(); + // ResolveMtrlPathHook?.Enable(); + // ResolveImcPathHook?.Enable(); + // LoadMtrlTexHook?.Enable(); + // LoadMtrlShpkHook?.Enable(); + // EnableDrawHook?.Enable(); + // CharacterBaseCreateHook?.Enable(); + // _loader.ResolvePath = CharacterReplacer; + // CharacterBaseDestructorHook?.Enable(); + // SetupConnectorModelAttributesHook?.Enable(); + // UpdateModelsHook?.Enable(); + // SetupModelAttributesHook?.Enable(); + // ApplyVisorStuffHook?.Enable(); + //} + // + //public void Disable() + //{ + // _loader.ResolvePath = ResourceLoader.DefaultReplacer; + // ResolveMdlPathHook?.Disable(); + // ResolveMtrlPathHook?.Disable(); + // ResolveImcPathHook?.Disable(); + // LoadMtrlTexHook?.Disable(); + // LoadMtrlShpkHook?.Disable(); + // EnableDrawHook?.Disable(); + // CharacterBaseCreateHook?.Disable(); + // CharacterBaseDestructorHook?.Disable(); + // SetupConnectorModelAttributesHook?.Disable(); + // UpdateModelsHook?.Disable(); + // SetupModelAttributesHook?.Disable(); + // ApplyVisorStuffHook?.Disable(); + //} + // public void Dispose() { - ResolveMdlPathHook?.Dispose(); - ResolveMtrlPathHook?.Dispose(); - ResolveImcPathHook?.Dispose(); - LoadMtrlTexHook?.Dispose(); + // Disable(); + // ResolveMdlPathHook?.Dispose(); + // ResolveMtrlPathHook?.Dispose(); + // ResolveImcPathHook?.Dispose(); + // LoadMtrlTexHook?.Dispose(); + // LoadMtrlShpkHook?.Dispose(); + // EnableDrawHook?.Dispose(); + // CharacterBaseCreateHook?.Dispose(); + // CharacterBaseDestructorHook?.Dispose(); + // SetupConnectorModelAttributesHook?.Dispose(); + // UpdateModelsHook?.Dispose(); + // SetupModelAttributesHook?.Dispose(); + // ApplyVisorStuffHook?.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs new file mode 100644 index 00000000..dbe49df1 --- /dev/null +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -0,0 +1,36 @@ +using Dalamud.Logging; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop; + +public unsafe class ResidentResourceManager +{ + // Some attach and physics files are stored in the resident resource manager, and we need to manually trigger a reload of them to get them to apply. + public delegate void* ResidentResourceDelegate( void* residentResourceManager ); + + [Signature( "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" )] + public ResidentResourceDelegate LoadPlayerResources = null!; + + [Signature( "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" )] + public ResidentResourceDelegate UnloadPlayerResources = null!; + + // A static pointer to the resident resource manager address. + [Signature( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05", ScanType = ScanType.StaticAddress )] + private readonly void** _residentResourceManagerAddress = null; + + public void* Address + => *_residentResourceManagerAddress; + + public ResidentResourceManager() + { + SignatureHelper.Initialise( this ); + } + + // Reload certain player resources by force. + public void Reload() + { + PluginLog.Debug( "Reload of resident resources triggered." ); + UnloadPlayerResources.Invoke( Address ); + LoadPlayerResources.Invoke( Address ); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResources.cs b/Penumbra/Interop/ResidentResources.cs deleted file mode 100644 index e1d43b62..00000000 --- a/Penumbra/Interop/ResidentResources.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud.Logging; -using Penumbra.Structs; -using Penumbra.Util; -using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; - -namespace Penumbra.Interop -{ - public class ResidentResources - { - private const int NumResources = 85; - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* LoadPlayerResourcesPrototype( IntPtr pResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* UnloadPlayerResourcesPrototype( IntPtr pResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* LoadCharacterResourcesPrototype( CharacterUtility* pCharacterResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* UnloadCharacterResourcePrototype( IntPtr resource ); - - - public LoadPlayerResourcesPrototype LoadPlayerResources { get; } - public UnloadPlayerResourcesPrototype UnloadPlayerResources { get; } - public LoadCharacterResourcesPrototype LoadDataFiles { get; } - public UnloadCharacterResourcePrototype UnloadCharacterResource { get; } - - // Object addresses - private readonly IntPtr _residentResourceManagerAddress; - - public IntPtr ResidentResourceManager - => Marshal.ReadIntPtr( _residentResourceManagerAddress ); - - private readonly IntPtr _characterUtilityAddress; - - public unsafe CharacterUtility* CharacterUtility - => ( CharacterUtility* )Marshal.ReadIntPtr( _characterUtilityAddress ).ToPointer(); - - public ResidentResources() - { - var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64(); - var loadPlayerResourcesAddress = - Dalamud.SigScanner.ScanText( - "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" ); - GeneralUtil.PrintDebugAddress( "LoadPlayerResources", loadPlayerResourcesAddress ); - - var unloadPlayerResourcesAddress = - Dalamud.SigScanner.ScanText( - "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" ); - GeneralUtil.PrintDebugAddress( "UnloadPlayerResources", unloadPlayerResourcesAddress ); - - var loadDataFilesAddress = Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" ); - GeneralUtil.PrintDebugAddress( "LoadDataFiles", loadDataFilesAddress ); - - var unloadCharacterResourceAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? FF 4C 89 37 48 83 C7 08 48 83 ED 01 75 ?? 48 8B CB" ); - GeneralUtil.PrintDebugAddress( "UnloadCharacterResource", unloadCharacterResourceAddress ); - - _residentResourceManagerAddress = Dalamud.SigScanner.GetStaticAddressFromSig( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05" ); - GeneralUtil.PrintDebugAddress( "ResidentResourceManager", _residentResourceManagerAddress ); - - _characterUtilityAddress = - Dalamud.SigScanner.GetStaticAddressFromSig( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" ); - GeneralUtil.PrintDebugAddress( "CharacterUtility", _characterUtilityAddress ); - - LoadPlayerResources = Marshal.GetDelegateForFunctionPointer< LoadPlayerResourcesPrototype >( loadPlayerResourcesAddress ); - UnloadPlayerResources = Marshal.GetDelegateForFunctionPointer< UnloadPlayerResourcesPrototype >( unloadPlayerResourcesAddress ); - LoadDataFiles = Marshal.GetDelegateForFunctionPointer< LoadCharacterResourcesPrototype >( loadDataFilesAddress ); - UnloadCharacterResource = - Marshal.GetDelegateForFunctionPointer< UnloadCharacterResourcePrototype >( unloadCharacterResourceAddress ); - } - - // Forces the reload of a specific set of 85 files, notably containing the eqp, eqdp, gmp and est tables, by filename. - public unsafe void ReloadResidentResources() - { - ReloadCharacterResources(); - - UnloadPlayerResources( ResidentResourceManager ); - LoadPlayerResources( ResidentResourceManager ); - } - - public unsafe string ResourceToPath( byte* resource ) - => Marshal.PtrToStringAnsi( new IntPtr( *( char** )( resource + 9 * 8 ) ) )!; - - private unsafe void ReloadCharacterResources() - { - var oldResources = new IntPtr[NumResources]; - var resources = new IntPtr( &CharacterUtility->Resources ); - var pResources = ( void** )resources.ToPointer(); - - Marshal.Copy( resources, oldResources, 0, NumResources ); - - LoadDataFiles( CharacterUtility ); - - for( var i = 0; i < NumResources; i++ ) - { - var handle = ( ResourceHandle* )oldResources[ i ]; - if( oldResources[ i ].ToPointer() == pResources[ i ] ) - { - PluginLog.Verbose( $"Unchanged resource: {ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}" ); - ( ( ResourceHandle* )oldResources[ i ] )->DecRef(); - continue; - } - - PluginLog.Debug( "Freeing " - + $"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with " - + $"{ResourceToPath( ( byte* )pResources[ i ] )}" ); - - UnloadCharacterResource( oldResources[ i ] ); - - // Temporary fix against crashes? - if( handle->RefCount <= 0 ) - { - handle->RefCount = 1; - handle->IncRef(); - handle->RefCount = 1; - } - } - } - } -} diff --git a/Penumbra/Interop/ResourceLoader.Debug.cs b/Penumbra/Interop/ResourceLoader.Debug.cs new file mode 100644 index 00000000..dcfd7f97 --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.Debug.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.STD; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Interop; + +public unsafe partial class ResourceLoader +{ + // A static pointer to the SE Resource Manager + [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0", ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] + public static ResourceManager** ResourceManager; + + // Gather some debugging data about penumbra-loaded objects. + public struct DebugData + { + public ResourceHandle* OriginalResource; + public ResourceHandle* ManipulatedResource; + public NewGamePath OriginalPath; + public FullPath ManipulatedPath; + public ResourceCategory Category; + public object? ResolverInfo; + public uint Extension; + } + + private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); + private readonly List< (FullPath, DebugData?) > _deleteList = new(); + + public IReadOnlyDictionary< FullPath, DebugData > DebugList + => _debugList; + + public void EnableDebug() + { + ResourceLoaded += AddModifiedDebugInfo; + } + + public void DisableDebug() + { + ResourceLoaded -= AddModifiedDebugInfo; + } + + private void AddModifiedDebugInfo( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) + { + if( manipulatedPath == null ) + { + return; + } + + var crc = ( uint )originalPath.Path.Crc32; + var originalResource = ( *ResourceManager )->FindResourceHandle( &handle->Category, &handle->FileType, &crc ); + _debugList[ manipulatedPath.Value ] = new DebugData() + { + OriginalResource = originalResource, + ManipulatedResource = handle, + Category = handle->Category, + Extension = handle->FileType, + OriginalPath = originalPath.Clone(), + ManipulatedPath = manipulatedPath.Value, + ResolverInfo = resolverInfo, + }; + } + + // Find a key in a StdMap. + private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key ) + where TKey : unmanaged, IComparable< TKey > + where TValue : unmanaged + { + if( map == null || map->Count == 0 ) + { + return null; + } + + var node = map->Head->Parent; + while( !node->IsNil ) + { + switch( key.CompareTo( node->KeyValuePair.Item1 ) ) + { + case 0: return &node->KeyValuePair.Item2; + case < 0: + node = node->Left; + break; + default: + node = node->Right; + break; + } + } + + return null; + } + + // Iterate in tree-order through a map, applying action to each KeyValuePair. + private static void IterateMap< TKey, TValue >( StdMap< TKey, TValue >* map, Action< TKey, TValue > action ) + where TKey : unmanaged + where TValue : unmanaged + { + if( map == null || map->Count == 0 ) + { + return; + } + + for( var node = map->SmallestValue; !node->IsNil; node = node->Next() ) + { + action( node->KeyValuePair.Item1, node->KeyValuePair.Item2 ); + } + } + + + // Find a resource in the resource manager by its category, extension and crc-hash + public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 ) + { + var manager = *ResourceManager; + var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; + var extMap = FindInMap( category->MainMap, ext ); + if( extMap == null ) + { + return null; + } + + var ret = FindInMap( extMap->Value, crc32 ); + return ret == null ? null : ret->Value; + } + + public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph ); + public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph ); + public delegate void ResourceAction( uint crc32, ResourceHandle* graph ); + + // Iteration functions through the resource manager. + public static void IterateGraphs( ExtMapAction action ) + { + var manager = *ResourceManager; + foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) ) + { + var graph = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )resourceType; + action( resourceType, graph->MainMap ); + } + } + + public static void IterateExtMap( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, ResourceMapAction action ) + => IterateMap( map, ( ext, m ) => action( ext, m.Value ) ); + + public static void IterateResourceMap( StdMap< uint, Pointer< ResourceHandle > >* map, ResourceAction action ) + => IterateMap( map, ( crc, r ) => action( crc, r.Value ) ); + + public static void IterateResources( ResourceAction action ) + { + IterateGraphs( ( _, extMap ) + => IterateExtMap( extMap, ( _, resourceMap ) + => IterateResourceMap( resourceMap, action ) ) ); + } + + public void UpdateDebugInfo() + { + var manager = *ResourceManager; + _deleteList.Clear(); + foreach( var data in _debugList.Values ) + { + var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); + var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); + if( modifiedResource == null ) + { + _deleteList.Add( ( data.ManipulatedPath, null ) ); + } + else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource ) + { + _deleteList.Add( ( data.ManipulatedPath, data with + { + OriginalResource = regularResource, + ManipulatedResource = modifiedResource, + } ) ); + } + } + + foreach( var (path, data) in _deleteList ) + { + if( data == null ) + { + _debugList.Remove( path ); + } + else + { + _debugList[ path ] = data.Value; + } + } + } + + // Logging functions for EnableLogging. + private static void LogPath( NewGamePath path, bool synchronous ) + => PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); + + private static void LogResource( ResourceHandle* handle, NewGamePath path, FullPath? manipulatedPath, object? _ ) + { + var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); + PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); + } + + private static void LogLoadedFile( Utf8String path, bool success, bool custom ) + => PluginLog.Information( success + ? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}" + : $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/ResourceLoader.Replacement.cs new file mode 100644 index 00000000..4792ec3f --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.Replacement.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Util; +using FileMode = Penumbra.Interop.Structs.FileMode; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; + +namespace Penumbra.Interop; + +public unsafe partial class ResourceLoader +{ + // Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. + // Both work basically the same, so we can reduce the main work to one function used by both hooks. + public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, + uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); + + [Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )] + public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; + + public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, + uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); + + [Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )] + public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; + + private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + int* resourceHash, byte* path, void* unk ) + => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false ); + + private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + int* resourceHash, byte* path, void* unk, bool isUnk ) + => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + + private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + uint* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) + => isSync + ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk ) + : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + + + [Conditional( "DEBUG" )] + private static void CompareHash( int local, int game, NewGamePath path ) + { + if( local != game ) + { + PluginLog.Warning( "Hash function appears to have changed. {Hash1:X8} vs {Hash2:X8} for {Path}.", game, local, path ); + } + } + + private event Action< NewGamePath, FullPath?, object? >? PathResolved; + + private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + int* resourceHash, byte* path, void* unk, bool isUnk ) + { + if( !NewGamePath.FromPointer( path, out var gamePath ) ) + { + PluginLog.Error( "Could not create GamePath from resource path." ); + return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + } + + CompareHash( gamePath.Path.Crc32, *resourceHash, gamePath ); + + ResourceRequested?.Invoke( gamePath, isSync ); + + // If no replacements are being made, we still want to be able to trigger the event. + var (resolvedPath, data) = DoReplacements ? ResolvePath( gamePath.ToLower() ) : ( null, null ); + PathResolved?.Invoke( gamePath, resolvedPath, data ); + if( resolvedPath == null ) + { + var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + ResourceLoaded?.Invoke( retUnmodified, gamePath, null, data ); + return retUnmodified; + } + + // Replace the hash and path with the correct one for the replacement. + *resourceHash = resolvedPath.Value.InternalName.Crc32; + path = resolvedPath.Value.InternalName.Path; + var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + ResourceLoaded?.Invoke( retModified, gamePath, resolvedPath.Value, data ); + return retModified; + } + + + // We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. + public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync ); + + [Signature( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" )] + public ReadFileDelegate ReadFile = null!; + + // We hook ReadSqPack to redirect rooted files to ReadFile. + public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + + [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = "ReadSqPackDetour" )] + public Hook< ReadSqPackPrototype > ReadSqPackHook = null!; + + private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + if( !DoReplacements ) + { + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + } + + if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) + { + PluginLog.Error( "Failure to load file from SqPack: invalid File Descriptor." ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + } + + var valid = NewGamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); + byte ret; + // The internal buffer size does not allow for more than 260 characters. + // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. + if( !valid || !gamePath.IsRooted() ) + { + ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); + } + else + { + // Specify that we are loading unpacked files from the drive. + // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, + // but since we only allow ASCII in the game paths, this is just a matter of upcasting. + fileDescriptor->FileMode = FileMode.LoadUnpackedResource; + + var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; + fileDescriptor->FileDescriptor = fd; + var fdPtr = ( char* )( fd + 0x21 ); + for( var i = 0; i < gamePath.Length; ++i ) + { + ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; + fdPtr[ i ] = ( char )gamePath.Path[ i ]; + } + + ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; + fdPtr[ gamePath.Length ] = '\0'; + + // Use the SE ReadFile function. + ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath.Path, ret != 0, true ); + } + + return ret; + } + + // Use the default method of path replacement. + public static (FullPath?, object?) DefaultReplacer( NewGamePath path ) + { + var gamePath = new GamePath( path.ToString() ); + var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath ); + return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null ); + } + + private void DisposeHooks() + { + DisableHooks(); + ReadSqPackHook.Dispose(); + GetResourceSyncHook.Dispose(); + GetResourceAsyncHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.TexMdl.cs b/Penumbra/Interop/ResourceLoader.TexMdl.cs new file mode 100644 index 00000000..d5616055 --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.TexMdl.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Util; +using Penumbra.Util; + +namespace Penumbra.Interop; + +// Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection. +public unsafe partial class ResourceLoader +{ + // Custom ulong flag to signal our files as opposed to SE files. + public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); + + // We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, + // i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. + private readonly HashSet< ulong > _customFileCrc = new(); + + public IReadOnlySet< ulong > CustomFileCrc + => _customFileCrc; + + + // The function that checks a files CRC64 to determine whether it is 'protected'. + // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. + public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); + + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = "CheckFileStateDetour" )] + public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; + + private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) + => _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 ); + + + // We use the local functions for our own files in the extern hook. + public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 ); + + [Signature( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" )] + public LoadTexFileLocalDelegate LoadTexFileLocal = null!; + + public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 ); + + [Signature( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" )] + public LoadMdlFileLocalPrototype LoadMdlFileLocal = null!; + + + // We hook the extern functions to just return the local one if given the custom flag as last argument. + public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); + + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = "LoadTexFileExternDetour" )] + public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; + + private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadTexFileLocal.Invoke( resourceHandle, unk1, unk2, unk3 ) + : LoadTexFileExternHook.Original( resourceHandle, unk1, unk2, unk3, ptr ); + + public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); + + + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = "LoadMdlFileExternDetour" )] + public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; + + private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadMdlFileLocal.Invoke( resourceHandle, unk1, unk2 ) + : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); + + + private void AddCrc( NewGamePath _, FullPath? path, object? _2 ) + { + if( path is { Extension: ".mdl" or ".tex" } p ) + { + _customFileCrc.Add( p.Crc64 ); + } + } + + private void EnableTexMdlTreatment() + { + PathResolved += AddCrc; + CheckFileStateHook.Enable(); + LoadTexFileExternHook.Enable(); + LoadMdlFileExternHook.Enable(); + } + + private void DisableTexMdlTreatment() + { + PathResolved -= AddCrc; + _customFileCrc.Clear(); + _customFileCrc.TrimExcess(); + CheckFileStateHook.Disable(); + LoadTexFileExternHook.Disable(); + LoadMdlFileExternHook.Disable(); + } + + private void DisposeTexMdlTreatment() + { + CheckFileStateHook.Dispose(); + LoadTexFileExternHook.Dispose(); + LoadMdlFileExternHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 09e9e160..562d2cfb 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -1,374 +1,128 @@ using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using Dalamud.Hooking; -using Dalamud.Logging; -using ImGuiNET; -using Penumbra.GameData.Util; -using Penumbra.Mods; -using Penumbra.Structs; -using Penumbra.Util; -using FileMode = Penumbra.Structs.FileMode; +using Dalamud.Utility.Signatures; +using Penumbra.GameData.ByteString; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop; -public class ResourceLoader : IDisposable +public unsafe partial class ResourceLoader : IDisposable { - public Penumbra Penumbra { get; set; } + // Toggle whether replacing paths is active, independently of hook and event state. + public bool DoReplacements { get; private set; } - public bool IsEnabled { get; set; } + // Hooks are required for everything, even events firing. + public bool HooksEnabled { get; private set; } - public Crc32 Crc32 { get; } + // This Logging just logs all file requests, returns and loads to the Dalamud log. + // Events can be used to make smarter logging. + public bool IsLoggingEnabled { get; private set; } - public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); - - // Delegate prototypes - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadFilePrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadTexFileExternPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadTexFileLocalPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadMdlFileExternPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr unk3 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadMdlFileLocalPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2 ); - - // Hooks - public Hook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } - public Hook< GetResourceAsyncPrototype >? GetResourceAsyncHook { get; private set; } - public Hook< ReadSqpackPrototype >? ReadSqpackHook { get; private set; } - public Hook< CheckFileStatePrototype >? CheckFileStateHook { get; private set; } - public Hook< LoadTexFileExternPrototype >? LoadTexFileExternHook { get; private set; } - public Hook< LoadMdlFileExternPrototype >? LoadMdlFileExternHook { get; private set; } - - // Unmanaged functions - public ReadFilePrototype? ReadFile { get; private set; } - public LoadTexFileLocalPrototype? LoadTexFileLocal { get; private set; } - public LoadMdlFileLocalPrototype? LoadMdlFileLocal { get; private set; } - - public bool LogAllFiles = false; - public Regex? LogFileFilter = null; - - - public ResourceLoader( Penumbra penumbra ) + public void EnableLogging() { - Penumbra = penumbra; - Crc32 = new Crc32(); - } - - public unsafe void Init() - { - var readFileAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" ); - GeneralUtil.PrintDebugAddress( "ReadFile", readFileAddress ); - - var readSqpackAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3" ); - GeneralUtil.PrintDebugAddress( "ReadSqPack", readSqpackAddress ); - - var getResourceSyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceSync", getResourceSyncAddress ); - - var getResourceAsyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceAsync", getResourceAsyncAddress ); - - var checkFileStateAddress = Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24" ); - GeneralUtil.PrintDebugAddress( "CheckFileState", checkFileStateAddress ); - - var loadTexFileLocalAddress = - Dalamud.SigScanner.ScanText( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" ); - GeneralUtil.PrintDebugAddress( "LoadTexFileLocal", loadTexFileLocalAddress ); - - var loadTexFileExternAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8" ); - GeneralUtil.PrintDebugAddress( "LoadTexFileExtern", loadTexFileExternAddress ); - - var loadMdlFileLocalAddress = - Dalamud.SigScanner.ScanText( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" ); - GeneralUtil.PrintDebugAddress( "LoadMdlFileLocal", loadMdlFileLocalAddress ); - - var loadMdlFileExternAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 02 B0 F1" ); - GeneralUtil.PrintDebugAddress( "LoadMdlFileExtern", loadMdlFileExternAddress ); - - ReadSqpackHook = new Hook< ReadSqpackPrototype >( readSqpackAddress, ReadSqpackHandler ); - GetResourceSyncHook = new Hook< GetResourceSyncPrototype >( getResourceSyncAddress, GetResourceSyncHandler ); - GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( getResourceAsyncAddress, GetResourceAsyncHandler ); - - ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress ); - LoadTexFileLocal = Marshal.GetDelegateForFunctionPointer< LoadTexFileLocalPrototype >( loadTexFileLocalAddress ); - LoadMdlFileLocal = Marshal.GetDelegateForFunctionPointer< LoadMdlFileLocalPrototype >( loadMdlFileLocalAddress ); - - CheckFileStateHook = new Hook< CheckFileStatePrototype >( checkFileStateAddress, CheckFileStateDetour ); - LoadTexFileExternHook = new Hook< LoadTexFileExternPrototype >( loadTexFileExternAddress, LoadTexFileExternDetour ); - LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); - } - - private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) - { - var modManager = Service< ModManager >.Get(); - return true || modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); - } - - private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ) - : LoadTexFileExternHook!.Original( resourceHandle, unk1, unk2, unk3, ptr ); - - private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ) - : LoadMdlFileExternHook!.Original( resourceHandle, unk1, unk2, ptr ); - - private unsafe void* GetResourceSyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown - ) - => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); - - private unsafe void* GetResourceAsyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - - private unsafe void* CallOriginalHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - { - if( isSync ) - { - if( GetResourceSyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceSync is null." ); - return null; - } - - return GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown ); - } - - if( GetResourceAsyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceAsync is null." ); - return null; - } - - return GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - private unsafe void* GetResourceHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - { - string file; - var modManager = Service< ModManager >.Get(); - - if( !Penumbra.Config.IsEnabled || modManager == null ) - { - if( LogAllFiles ) - { - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - if( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) - { - PluginLog.Information( "[GetResourceHandler] {0}", file ); - } - } - - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - var gameFsPath = GamePath.GenerateUncheckedLower( file ); - var replacementPath = PathResolver.Dict.TryGetValue( file, out var collection ) - ? collection.ResolveSwappedOrReplacementPath( gameFsPath ) - : modManager.ResolveSwappedOrReplacementPath( gameFsPath ); - if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) - { - PluginLog.Information( "[GetResourceHandler] {0}", file ); - } - - // path must be < 260 because statically defined array length :( - if( replacementPath == null ) - { - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - if (collection != null) - PathResolver.Dict[ replacementPath ] = collection; - var path = Encoding.ASCII.GetBytes( replacementPath ); - - var bPath = stackalloc byte[path.Length + 1]; - Marshal.Copy( path, 0, new IntPtr( bPath ), path.Length ); - pPath = ( char* )bPath; - - Crc32.Init(); - Crc32.Update( path ); - *pResourceHash = Crc32.Checksum; - - PluginLog.Verbose( "[GetResourceHandler] resolved {GamePath} to {NewPath}", gameFsPath, replacementPath ); - - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - - private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) - { - if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) - { - PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); - if( gameFsPath is not { Length: < 260 } ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - - //var collection = gameFsPath.StartsWith( '|' ); - //if( collection ) - //{ - // var end = gameFsPath.IndexOf( '|', 1 ); - // if( end < 0 ) - // { - // PluginLog.Error( $"Unterminated Collection Name {gameFsPath}" ); - // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - // } - // - // var name = gameFsPath[ 1..end ]; - // gameFsPath = gameFsPath[ ( end + 1 ).. ]; - // PluginLog.Debug( "Loading file for {Name}: {GameFsPath}", name, gameFsPath ); - // - // if( !Path.IsPathRooted( gameFsPath ) ) - // { - // var encoding = Encoding.UTF8.GetBytes( gameFsPath ); - // Marshal.Copy( encoding, 0, new IntPtr( pFileDesc->ResourceHandle->FileName() ), encoding.Length ); - // pFileDesc->ResourceHandle->FileName()[ encoding.Length ] = 0; - // pFileDesc->ResourceHandle->FileNameLength -= name.Length + 2; - // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - // } - //} - //else - if( !Path.IsPathRooted( gameFsPath ) ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - PluginLog.Debug( "Loading modded file: {GameFsPath}", gameFsPath ); - - pFileDesc->FileMode = FileMode.LoadUnpackedResource; - - // note: must be utf16 - var utfPath = Encoding.Unicode.GetBytes( gameFsPath ); - - Marshal.Copy( utfPath, 0, new IntPtr( &pFileDesc->UtfFileName ), utfPath.Length ); - - var fd = stackalloc byte[0x20 + utfPath.Length + 0x16]; - Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); - - pFileDesc->FileDescriptor = fd; - return ReadFile( pFileHandler, pFileDesc, priority, isSync ); - } - - public void Enable() - { - if( IsEnabled ) + if( IsLoggingEnabled ) { return; } - if( ReadSqpackHook == null - || GetResourceSyncHook == null - || GetResourceAsyncHook == null - || CheckFileStateHook == null - || LoadTexFileExternHook == null - || LoadMdlFileExternHook == null ) + IsLoggingEnabled = true; + ResourceRequested += LogPath; + ResourceLoaded += LogResource; + FileLoaded += LogLoadedFile; + EnableHooks(); + } + + public void DisableLogging() + { + if( !IsLoggingEnabled ) { - PluginLog.Error( "[GetResourceHandler] Could not activate hooks because at least one was not set." ); return; } - ReadSqpackHook.Enable(); + IsLoggingEnabled = false; + ResourceRequested -= LogPath; + ResourceLoaded -= LogResource; + FileLoaded -= LogLoadedFile; + } + + public void EnableReplacements() + { + if( DoReplacements ) + { + return; + } + + DoReplacements = true; + EnableTexMdlTreatment(); + EnableHooks(); + } + + public void DisableReplacements() + { + if( !DoReplacements ) + { + return; + } + + DoReplacements = false; + DisableTexMdlTreatment(); + } + + public void EnableHooks() + { + if( HooksEnabled ) + { + return; + } + + HooksEnabled = true; + ReadSqPackHook.Enable(); GetResourceSyncHook.Enable(); GetResourceAsyncHook.Enable(); - CheckFileStateHook.Enable(); - LoadTexFileExternHook.Enable(); - LoadMdlFileExternHook.Enable(); - - IsEnabled = true; } - public void Disable() + public void DisableHooks() { - if( !IsEnabled ) + if( !HooksEnabled ) { return; } - ReadSqpackHook?.Disable(); - GetResourceSyncHook?.Disable(); - GetResourceAsyncHook?.Disable(); - CheckFileStateHook?.Disable(); - LoadTexFileExternHook?.Disable(); - LoadMdlFileExternHook?.Disable(); - IsEnabled = false; + HooksEnabled = false; + ReadSqPackHook.Disable(); + GetResourceSyncHook.Disable(); + GetResourceAsyncHook.Disable(); } + public ResourceLoader( Penumbra _ ) + { + SignatureHelper.Initialise( this ); + } + + // Event fired whenever a resource is requested. + public delegate void ResourceRequestedDelegate( NewGamePath path, bool synchronous ); + public event ResourceRequestedDelegate? ResourceRequested; + + // Event fired whenever a resource is returned. + // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. + // resolveData is additional data returned by the current ResolvePath function and is user-defined. + public delegate void ResourceLoadedDelegate( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, + object? resolveData ); + + public event ResourceLoadedDelegate? ResourceLoaded; + + + // Event fired whenever a resource is newly loaded. + // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) + // custom is true if the file was loaded from local files instead of the default SqPacks. + public delegate void FileLoadedDelegate( Utf8String path, bool success, bool custom ); + public event FileLoadedDelegate? FileLoaded; + + // Customization point to control how path resolving is handled. + public Func< NewGamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; + public void Dispose() { - Disable(); - ReadSqpackHook?.Dispose(); - GetResourceSyncHook?.Dispose(); - GetResourceAsyncHook?.Dispose(); - CheckFileStateHook?.Dispose(); - LoadTexFileExternHook?.Dispose(); - LoadMdlFileExternHook?.Dispose(); + DisposeHooks(); + DisposeTexMdlTreatment(); } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs new file mode 100644 index 00000000..35f274c4 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterUtility +{ + public const int NumResources = 85; + public const int EqpIdx = 0; + public const int GmpIdx = 1; + public const int HumanCmpIdx = 63; + public const int FaceEstIdx = 64; + public const int HairEstIdx = 65; + public const int BodyEstIdx = 66; + public const int HeadEstIdx = 67; + + public static int EqdpIdx( ushort raceCode, bool accessory ) + => ( accessory ? 28 : 0 ) + + raceCode switch + { + 0101 => 2, + 0201 => 3, + 0301 => 4, + 0401 => 5, + 0501 => 6, + 0601 => 7, + 0701 => 8, + 0801 => 9, + 0901 => 10, + 1001 => 11, + 1101 => 12, + 1201 => 13, + 1301 => 14, + 1401 => 15, + 1501 => 16, + 1601 => 17, // Does not exist yet + 1701 => 18, + 1801 => 19, + 0104 => 20, + 0204 => 21, + 0504 => 22, + 0604 => 23, + 0704 => 24, + 0804 => 25, + 1304 => 26, + 1404 => 27, + 9104 => 28, + 9204 => 29, + _ => throw new ArgumentException(), + }; + + [FieldOffset( 0 )] + public void* VTable; + + [FieldOffset( 8 )] + public fixed ulong Resources[NumResources]; + + [FieldOffset( 8 + EqpIdx * 8 )] + public ResourceHandle* EqpResource; + + [FieldOffset( 8 + GmpIdx * 8 )] + public ResourceHandle* GmpResource; + + public ResourceHandle* Resource( int idx ) + => ( ResourceHandle* )Resources[ idx ]; + + public ResourceHandle* EqdpResource( ushort raceCode, bool accessory ) + => Resource( EqdpIdx( raceCode, accessory ) ); + + [FieldOffset( 8 + HumanCmpIdx * 8 )] + public ResourceHandle* HumanCmpResource; + + [FieldOffset( 8 + FaceEstIdx * 8 )] + public ResourceHandle* FaceEstResource; + + [FieldOffset( 8 + HairEstIdx * 8 )] + public ResourceHandle* HairEstResource; + + [FieldOffset( 8 + BodyEstIdx * 8 )] + public ResourceHandle* BodyEstResource; + + [FieldOffset( 8 + HeadEstIdx * 8 )] + public ResourceHandle* HeadEstResource; + + // not included resources have no known use case. +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/FileMode.cs b/Penumbra/Interop/Structs/FileMode.cs new file mode 100644 index 00000000..21270176 --- /dev/null +++ b/Penumbra/Interop/Structs/FileMode.cs @@ -0,0 +1,11 @@ +namespace Penumbra.Interop.Structs; + +public enum FileMode : uint +{ + LoadUnpackedResource = 0, + LoadFileResource = 1, // Shit in My Games uses this + + // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? + LoadIndexResource = 0xA, // load index/index2 + LoadSqPackResource = 0xB, +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs new file mode 100644 index 00000000..d0f05f07 --- /dev/null +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct ResourceHandle +{ + [StructLayout( LayoutKind.Explicit )] + public struct DataIndirection + { + [FieldOffset( 0x10 )] + public byte* DataPtr; + + [FieldOffset( 0x28 )] + public ulong DataLength; + } + + public const int SsoSize = 15; + + public byte* FileName() + { + if( FileNameLength > SsoSize ) + { + return FileNameData; + } + + fixed( byte** name = &FileNameData ) + { + return ( byte* )name; + } + } + + public ReadOnlySpan< byte > FileNameSpan() + => new(FileName(), FileNameLength); + + [FieldOffset( 0x48 )] + public byte* FileNameData; + + [FieldOffset( 0x58 )] + public int FileNameLength; + + [FieldOffset( 0xB0 )] + public DataIndirection* Data; + + [FieldOffset( 0xB8 )] + public uint DataLength; + + + public (IntPtr Data, int Length) GetData() + => Data != null + ? ( ( IntPtr )Data->DataPtr, ( int )Data->DataLength ) + : ( IntPtr.Zero, 0 ); + + public bool SetData( IntPtr data, int length ) + { + if( Data == null ) + { + return false; + } + + Data->DataPtr = length != 0 ? ( byte* )data : null; + Data->DataLength = ( ulong )length; + DataLength = ( uint )length; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs new file mode 100644 index 00000000..c83f3796 --- /dev/null +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; +using Penumbra.Structs; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct SeFileDescriptor +{ + [FieldOffset( 0x00 )] + public FileMode FileMode; + + [FieldOffset( 0x30 )] + public void* FileDescriptor; // + + [FieldOffset( 0x50 )] + public ResourceHandle* ResourceHandle; // + + + [FieldOffset( 0x70 )] + public char Utf16FileName; // +} \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index e0722424..f6c84358 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -4,231 +4,231 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; +using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Meta.Files; using Penumbra.Mod; using Penumbra.Structs; using Penumbra.Util; -namespace Penumbra.Meta +namespace Penumbra.Meta; + +// Corresponds meta manipulations of any kind with the settings for a mod. +// DefaultData contains all manipulations that are active regardless of option groups. +// GroupData contains a mapping of Group -> { Options -> {Manipulations} }. +public class MetaCollection { - // Corresponds meta manipulations of any kind with the settings for a mod. - // DefaultData contains all manipulations that are active regardless of option groups. - // GroupData contains a mapping of Group -> { Options -> {Manipulations} }. - public class MetaCollection + public List< MetaManipulation > DefaultData = new(); + public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); + + + // Store total number of manipulations for some ease of access. + [JsonIgnore] + internal int Count; + + + // Return an enumeration of all active meta manipulations for a given mod with given settings. + public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) { - public List< MetaManipulation > DefaultData = new(); - public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); - - - // Store total number of manipulations for some ease of access. - [JsonIgnore] - internal int Count; - - - // Return an enumeration of all active meta manipulations for a given mod with given settings. - public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) + if( Count == DefaultData.Count ) { - if( Count == DefaultData.Count ) + return DefaultData; + } + + IEnumerable< MetaManipulation > ret = DefaultData; + + foreach( var group in modMeta.Groups ) + { + if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) { - return DefaultData; + continue; } - IEnumerable< MetaManipulation > ret = DefaultData; - - foreach( var group in modMeta.Groups ) + if( group.Value.SelectionType == SelectType.Single ) { - if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) + var settingName = group.Value.Options[ setting ].OptionName; + if( metas.TryGetValue( settingName, out var meta ) ) { - continue; + ret = ret.Concat( meta ); } - - if( group.Value.SelectionType == SelectType.Single ) + } + else + { + for( var i = 0; i < group.Value.Options.Count; ++i ) { - var settingName = group.Value.Options[ setting ].OptionName; + var flag = 1 << i; + if( ( setting & flag ) == 0 ) + { + continue; + } + + var settingName = group.Value.Options[ i ].OptionName; if( metas.TryGetValue( settingName, out var meta ) ) { ret = ret.Concat( meta ); } } - else - { - for( var i = 0; i < group.Value.Options.Count; ++i ) - { - var flag = 1 << i; - if( ( setting & flag ) == 0 ) - { - continue; - } - - var settingName = group.Value.Options[ i ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - } } - - return ret; } - // Check that the collection is still basically valid, - // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, - // and that the contained manipulations are still valid and non-default manipulations. - public bool Validate( ModMeta modMeta ) + return ret; + } + + // Check that the collection is still basically valid, + // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, + // and that the contained manipulations are still valid and non-default manipulations. + public bool Validate( ModMeta modMeta ) + { + var defaultFiles = Penumbra.MetaDefaults; + SortLists(); + foreach( var group in GroupData ) { - var defaultFiles = Service< MetaDefaults >.Get(); - SortLists(); - foreach( var group in GroupData ) + if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) { - if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) + return false; + } + + foreach( var option in group.Value ) + { + if( options.Options.All( o => o.OptionName != option.Key ) ) { return false; } - foreach( var option in group.Value ) + if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) { - if( options.Options.All( o => o.OptionName != option.Key ) ) - { - return false; - } - - if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) - { - return false; - } + return false; } } - - return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); - } - - // Re-sort all manipulations. - private void SortLists() - { - DefaultData.Sort(); - foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) - { - list.Sort(); - } } - // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. - // Creates the option group and the option if necessary. - private void AddMeta( string group, string option, TexToolsMeta meta ) + return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); + } + + // Re-sort all manipulations. + private void SortLists() + { + DefaultData.Sort(); + foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) { - if( meta.Manipulations.Count == 0 ) - { - return; - } + list.Sort(); + } + } - if( group.Length == 0 ) - { - DefaultData.AddRange( meta.Manipulations ); - } - else if( option.Length == 0 ) - { } - else if( !GroupData.TryGetValue( group, out var options ) ) - { - GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); - } - else if( !options.TryGetValue( option, out var list ) ) - { - options.Add( option, meta.Manipulations.ToList() ); - } - else - { - list.AddRange( meta.Manipulations ); - } - - Count += meta.Manipulations.Count; + // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. + // Creates the option group and the option if necessary. + private void AddMeta( string group, string option, TexToolsMeta meta ) + { + if( meta.Manipulations.Count == 0 ) + { + return; } - // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, - // combining them with the given ModMeta. - public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) + if( group.Length == 0 ) { - DefaultData.Clear(); - GroupData.Clear(); - Count = 0; - foreach( var file in files ) - { - TexToolsMeta metaData = file.Extension.ToLowerInvariant() switch - { - ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), - ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), - _ => TexToolsMeta.Invalid, - }; - - if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) - { - continue; - } - - var path = new RelPath( file, basePath ); - var foundAny = false; - foreach( var group in modMeta.Groups ) - { - foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) - { - foundAny = true; - AddMeta( group.Key, option.OptionName, metaData ); - } - } - - if( !foundAny ) - { - AddMeta( string.Empty, string.Empty, metaData ); - } - } - - SortLists(); + DefaultData.AddRange( meta.Manipulations ); + } + else if( option.Length == 0 ) + { } + else if( !GroupData.TryGetValue( group, out var options ) ) + { + GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); + } + else if( !options.TryGetValue( option, out var list ) ) + { + options.Add( option, meta.Manipulations.ToList() ); + } + else + { + list.AddRange( meta.Manipulations ); } - public static FileInfo FileName( DirectoryInfo basePath ) - => new( Path.Combine( basePath.FullName, "metadata_manipulations.json" ) ); + Count += meta.Manipulations.Count; + } - public void SaveToFile( FileInfo file ) + // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, + // combining them with the given ModMeta. + public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) + { + DefaultData.Clear(); + GroupData.Clear(); + Count = 0; + foreach( var file in files ) { - try + var metaData = file.Extension.ToLowerInvariant() switch { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( file.FullName, text ); + ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), + ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), + _ => TexToolsMeta.Invalid, + }; + + if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) + { + continue; } - catch( Exception e ) + + var path = new RelPath( file, basePath ); + var foundAny = false; + foreach( var group in modMeta.Groups ) { - PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); + foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) + { + foundAny = true; + AddMeta( group.Key, option.OptionName, metaData ); + } + } + + if( !foundAny ) + { + AddMeta( string.Empty, string.Empty, metaData ); } } - public static MetaCollection? LoadFromFile( FileInfo file ) + SortLists(); + } + + public static FileInfo FileName( DirectoryInfo basePath ) + => new(Path.Combine( basePath.FullName, "metadata_manipulations.json" )); + + public void SaveToFile( FileInfo file ) + { + try { - if( !file.Exists ) + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( file.FullName, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); + } + } + + public static MetaCollection? LoadFromFile( FileInfo file ) + { + if( !file.Exists ) + { + return null; + } + + try + { + var text = File.ReadAllText( file.FullName ); + + var collection = JsonConvert.DeserializeObject< MetaCollection >( text, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + + if( collection != null ) { - return null; + collection.Count = collection.DefaultData.Count + + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); } - try - { - var text = File.ReadAllText( file.FullName ); - - var collection = JsonConvert.DeserializeObject< MetaCollection >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - - if( collection != null ) - { - collection.Count = collection.DefaultData.Count - + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); - } - - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); - return null; - } + return collection; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); + return null; } } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index cfb88ce2..6ff76118 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -4,182 +4,185 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Lumina.Data.Files; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Interop; using Penumbra.Meta.Files; using Penumbra.Util; -namespace Penumbra.Meta +namespace Penumbra.Meta; + +public class MetaManager : IDisposable { - public class MetaManager : IDisposable + internal class FileInformation { - internal class FileInformation + public readonly object Data; + public bool Changed; + public FullPath? CurrentFile; + public byte[] ByteData = Array.Empty< byte >(); + + public FileInformation( object data ) + => Data = data; + + public void Write( DirectoryInfo dir, GamePath originalPath ) { - public readonly object Data; - public bool Changed; - public FullPath? CurrentFile; - - public FileInformation( object data ) - => Data = data; - - public void Write( DirectoryInfo dir, GamePath originalPath ) + ByteData = Data switch { - var data = Data switch - { - EqdpFile eqdp => eqdp.WriteBytes(), - EqpFile eqp => eqp.WriteBytes(), - GmpFile gmp => gmp.WriteBytes(), - EstFile est => est.WriteBytes(), - ImcFile imc => imc.WriteBytes(), - CmpFile cmp => cmp.WriteBytes(), - _ => throw new NotImplementedException(), - }; - DisposeFile( CurrentFile ); - CurrentFile = new FullPath(TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" )); - Changed = false; - } + EqdpFile eqdp => eqdp.WriteBytes(), + EqpFile eqp => eqp.WriteBytes(), + GmpFile gmp => gmp.WriteBytes(), + EstFile est => est.WriteBytes(), + ImcFile imc => imc.WriteBytes(), + CmpFile cmp => cmp.WriteBytes(), + _ => throw new NotImplementedException(), + }; + DisposeFile( CurrentFile ); + CurrentFile = new FullPath( TempFile.WriteNew( dir, ByteData, $"_{originalPath.Filename()}" ) ); + Changed = false; + } + } + + public const string TmpDirectory = "penumbrametatmp"; + + private readonly DirectoryInfo _dir; + private readonly Dictionary< GamePath, FullPath > _resolvedFiles; + + private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); + private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); + + public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations + => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); + + public IEnumerable< (GamePath, FullPath) > Files + => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) + .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); + + public int Count + => _currentManipulations.Count; + + public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) + => _currentManipulations.TryGetValue( manip, out mod! ); + + public byte[] EqpData = Array.Empty< byte >(); + + private static void DisposeFile( FullPath? file ) + { + if( !( file?.Exists ?? false ) ) + { + return; } - public const string TmpDirectory = "penumbrametatmp"; - - private readonly MetaDefaults _default; - private readonly DirectoryInfo _dir; - private readonly ResidentResources _resourceManagement; - private readonly Dictionary< GamePath, FullPath > _resolvedFiles; - - private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); - private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); - - public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations - => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - - public IEnumerable< (GamePath, FullPath) > Files - => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) - .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); - - public int Count - => _currentManipulations.Count; - - public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) - => _currentManipulations.TryGetValue( manip, out mod! ); - - private static void DisposeFile( FullPath? file ) + try { - if( !( file?.Exists ?? false ) ) - { - return; - } + File.Delete( file.Value.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); + } + } + public void Reset( bool reload = true ) + { + foreach( var file in _currentFiles ) + { + _resolvedFiles.Remove( file.Key ); + DisposeFile( file.Value.CurrentFile ); + } + + _currentManipulations.Clear(); + _currentFiles.Clear(); + ClearDirectory(); + if( reload ) + { + Penumbra.ResidentResources.Reload(); + } + } + + public void Dispose() + => Reset(); + + private static void ClearDirectory( DirectoryInfo modDir ) + { + modDir.Refresh(); + if( modDir.Exists ) + { try { - File.Delete( file.Value.FullName ); + Directory.Delete( modDir.FullName, true ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); - } - } - - public void Reset( bool reload = true ) - { - foreach( var file in _currentFiles ) - { - _resolvedFiles.Remove( file.Key ); - DisposeFile( file.Value.CurrentFile ); - } - - _currentManipulations.Clear(); - _currentFiles.Clear(); - ClearDirectory(); - if( reload ) - { - _resourceManagement.ReloadResidentResources(); - } - } - - public void Dispose() - => Reset(); - - private static void ClearDirectory( DirectoryInfo modDir ) - { - modDir.Refresh(); - if( modDir.Exists ) - { - try - { - Directory.Delete( modDir.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not clear temporary metafile directory \"{modDir.FullName}\":\n{e}" ); - } - } - } - - private void ClearDirectory() - => ClearDirectory( _dir ); - - public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) - { - _resolvedFiles = resolvedFiles; - _default = Service< MetaDefaults >.Get(); - _resourceManagement = Service< ResidentResources >.Get(); - _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); - ClearDirectory(); - } - - public void WriteNewFiles() - { - if( _currentFiles.Any() ) - { - Directory.CreateDirectory( _dir.FullName ); - } - - foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) - { - kvp.Value.Write( _dir, kvp.Key ); - _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; - } - } - - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) - { - if( _currentManipulations.ContainsKey( m ) ) - { - return false; - } - - _currentManipulations.Add( m, mod ); - var gamePath = m.CorrespondingFilename(); - try - { - if( !_currentFiles.TryGetValue( gamePath, out var file ) ) - { - file = new FileInformation( _default.CreateNewFile( m ) ?? throw new IOException() ) - { - Changed = true, - CurrentFile = null, - }; - _currentFiles[ gamePath ] = file; - } - - file.Changed |= m.Type switch - { - MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), - MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), - MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), - MetaType.Est => m.Apply( ( EstFile )file.Data ), - MetaType.Imc => m.Apply( ( ImcFile )file.Data ), - MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), - _ => throw new NotImplementedException(), - }; - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" ); - return false; + PluginLog.Error( $"Could not clear temporary metafile directory \"{modDir.FullName}\":\n{e}" ); } } } + + private void ClearDirectory() + => ClearDirectory( _dir ); + + public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) + { + _resolvedFiles = resolvedFiles; + _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); + ClearDirectory(); + } + + public void WriteNewFiles() + { + if( _currentFiles.Any() ) + { + Directory.CreateDirectory( _dir.FullName ); + } + + foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) + { + kvp.Value.Write( _dir, kvp.Key ); + _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; + if( kvp.Value.Data is EqpFile ) + { + EqpData = kvp.Value.ByteData; + } + } + } + + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + { + if( _currentManipulations.ContainsKey( m ) ) + { + return false; + } + + _currentManipulations.Add( m, mod ); + var gamePath = m.CorrespondingFilename(); + try + { + if( !_currentFiles.TryGetValue( gamePath, out var file ) ) + { + file = new FileInformation( Penumbra.MetaDefaults.CreateNewFile( m ) ?? throw new IOException() ) + { + Changed = true, + CurrentFile = null, + }; + _currentFiles[ gamePath ] = file; + } + + file.Changed |= m.Type switch + { + MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), + MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), + MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), + MetaType.Est => m.Apply( ( EstFile )file.Data ), + MetaType.Imc => m.Apply( ( ImcFile )file.Data ), + MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), + _ => throw new NotImplementedException(), + }; + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" ); + return false; + } + } } \ No newline at end of file diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index dc3e1510..a8f33f8c 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -65,9 +65,8 @@ namespace Penumbra.Mod private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { - var manager = Service< ModManager >.Get(); - manager.AddMod( newDir ); - var newMod = manager.Mods[ newDir.Name ]; + Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; newMod.Move( newSortOrder ); newMod.ComputeChangedItems(); ModFileSystem.InvokeChange(); @@ -516,11 +515,11 @@ namespace Penumbra.Mod if( group.Options.Any() ) { - meta.Groups.Add( groupDir.Name, @group ); + meta.Groups.Add( groupDir.Name, group ); } } - foreach(var collection in Service.Get().Collections.Collections.Values) + foreach(var collection in Penumbra.ModManager.Collections.Collections.Values) collection.UpdateSetting(baseDir, meta, true); } diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index d47851a5..590cda79 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.GameData.ByteString; using Penumbra.Meta; -using Penumbra.Util; namespace Penumbra.Mod; diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 4a71de10..70145b03 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -42,9 +42,8 @@ public class CollectionManager if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) { - var resourceManager = Service< ResidentResources >.Get(); ActiveCollection = newActive; - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } else { @@ -115,7 +114,7 @@ public class CollectionManager if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } } @@ -223,8 +222,7 @@ public class CollectionManager if( !CollectionChangedTo.Any() ) { ActiveCollection = c; - var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } DefaultCollection = c; @@ -244,8 +242,7 @@ public class CollectionManager if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) { ActiveCollection = c; - var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } CharacterCollection[ characterName ] = c; diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 0a6153d9..92f8275a 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -145,7 +145,7 @@ namespace Penumbra.Mods Cache.UpdateMetaManipulations(); if( activeCollection ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } } } diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 4322b73d..c7d7cc1c 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; @@ -186,8 +187,9 @@ public class ModCollectionCache { foreach( var (file, paths) in option.OptionFiles ) { - var fullPath = new FullPath( mod.Data.BasePath, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + var fullPath = new FullPath( mod.Data.BasePath, + NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); if( idx < 0 ) { AddMissingFile( fullPath ); @@ -255,7 +257,14 @@ public class ModCollectionCache var file = mod.Data.Resources.ModFiles[ i ]; if( file.Exists ) { - AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); + if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) + { + AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO + } + else + { + PluginLog.Warning( $"Could not convert {file} in {mod.Data.BasePath.FullName} to GamePath." ); + } } else { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 14329de8..cabae47d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -18,18 +18,44 @@ using System.Linq; namespace Penumbra; +public class Penumbra2 // : IDalamudPlugin +{ + public string Name + => "Penumbra"; + + private const string CommandName = "/penumbra"; + + public static Configuration Config { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; + + public void Dispose() + { + ResourceLoader.Dispose(); + } +} + public class Penumbra : IDalamudPlugin { - public string Name { get; } = "Penumbra"; - public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build"; + public string Name + => "Penumbra"; + + public string PluginDebugTitleStr + => "Penumbra - Debug Build"; private const string CommandName = "/penumbra"; public static Configuration Config { get; private set; } = null!; public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; + public static ResidentResourceManager ResidentResources { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaDefaults MetaDefaults { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + + public ResourceLoader ResourceLoader { get; } - public PathResolver PathResolver { get; } + + //public PathResolver PathResolver { get; } public SettingsInterface SettingsInterface { get; } public MusicManager MusicManager { get; } public ObjectReloader ObjectReloader { get; } @@ -39,8 +65,6 @@ public class Penumbra : IDalamudPlugin private WebServer? _webServer; - private readonly ModManager _modManager; - public Penumbra( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); @@ -53,27 +77,29 @@ public class Penumbra : IDalamudPlugin MusicManager.DisableStreaming(); } - var gameUtils = Service< ResidentResources >.Set(); - PathResolver = new PathResolver(); - PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); - Service< MetaDefaults >.Set(); - _modManager = Service< ModManager >.Set(); - - _modManager.DiscoverMods(); - - ObjectReloader = new ObjectReloader( _modManager, Config.WaitFrames ); - - ResourceLoader = new ResourceLoader( this ); + ResidentResources = new ResidentResourceManager(); + CharacterUtility = new CharacterUtility(); + MetaDefaults = new MetaDefaults(); + ResourceLoader = new ResourceLoader( this ); + ModManager = new ModManager(); + ModManager.DiscoverMods(); + //PathResolver = new PathResolver( ResourceLoader, gameUtils ); + PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); + ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - ResourceLoader.Init(); - ResourceLoader.Enable(); + ResourceLoader.EnableReplacements(); + ResourceLoader.EnableLogging(); + if( Config.DebugMode ) + { + ResourceLoader.EnableDebug(); + } - gameUtils.ReloadResidentResources(); + ResidentResources.Reload(); Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( pluginInterface, Api ); @@ -106,7 +132,7 @@ public class Penumbra : IDalamudPlugin } Config.IsEnabled = true; - Service< ResidentResources >.Get().ReloadResidentResources(); + ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( true ); @@ -125,7 +151,7 @@ public class Penumbra : IDalamudPlugin } Config.IsEnabled = false; - Service< ResidentResources >.Get().ReloadResidentResources(); + ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( false ); @@ -192,7 +218,7 @@ public class Penumbra : IDalamudPlugin Dalamud.Commands.RemoveHandler( CommandName ); - PathResolver.Dispose(); + //PathResolver.Dispose(); ResourceLoader.Dispose(); ShutdownWebServer(); @@ -205,7 +231,7 @@ public class Penumbra : IDalamudPlugin var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) ? ModCollection.Empty - : _modManager.Collections.Collections.Values.FirstOrDefault( c + : ModManager.Collections.Collections.Values.FirstOrDefault( c => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); if( collection == null ) { @@ -216,24 +242,24 @@ public class Penumbra : IDalamudPlugin switch( type ) { case "default": - if( collection == _modManager.Collections.DefaultCollection ) + if( collection == ModManager.Collections.DefaultCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); return false; } - _modManager.Collections.SetDefaultCollection( collection ); + ModManager.Collections.SetDefaultCollection( collection ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; case "forced": - if( collection == _modManager.Collections.ForcedCollection ) + if( collection == ModManager.Collections.ForcedCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); return false; } - _modManager.Collections.SetForcedCollection( collection ); + ModManager.Collections.SetForcedCollection( collection ); Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); SettingsInterface.ResetForcedCollection(); return true; @@ -256,9 +282,9 @@ public class Penumbra : IDalamudPlugin { case "reload": { - Service< ModManager >.Get().DiscoverMods(); + ModManager.DiscoverMods(); Dalamud.Chat.Print( - $"Reloaded Penumbra mods. You have {_modManager.Mods.Count} mods." + $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods." ); break; } diff --git a/Penumbra/Structs/CharacterUtility.cs b/Penumbra/Structs/CharacterUtility.cs deleted file mode 100644 index 2459e2d6..00000000 --- a/Penumbra/Structs/CharacterUtility.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Sequential )] - public unsafe struct CharacterUtility - { - public void* VTable; - - public IntPtr Resources; // Size: 85, I hate C# - } -} \ No newline at end of file diff --git a/Penumbra/Structs/FileMode.cs b/Penumbra/Structs/FileMode.cs deleted file mode 100644 index 13235521..00000000 --- a/Penumbra/Structs/FileMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Penumbra.Structs -{ - public enum FileMode : uint - { - LoadUnpackedResource = 0, - LoadFileResource = 1, // Shit in My Games uses this - - // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? - LoadIndexResource = 0xA, // load index/index2 - LoadSqPackResource = 0xB, - } -} \ No newline at end of file diff --git a/Penumbra/Structs/ResourceHandle.cs b/Penumbra/Structs/ResourceHandle.cs deleted file mode 100644 index 3318bb99..00000000 --- a/Penumbra/Structs/ResourceHandle.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Explicit )] - public unsafe struct ResourceHandle - { - public const int SsoSize = 15; - - public byte* FileName() - { - if( FileNameLength > SsoSize ) - { - return _fileName; - } - - fixed( byte** name = &_fileName ) - { - return ( byte* )name; - } - } - - [FieldOffset( 0x48 )] - private byte* _fileName; - - [FieldOffset( 0x58 )] - public int FileNameLength; - } -} \ No newline at end of file diff --git a/Penumbra/Structs/SeFileDescriptor.cs b/Penumbra/Structs/SeFileDescriptor.cs deleted file mode 100644 index dc22b81b..00000000 --- a/Penumbra/Structs/SeFileDescriptor.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Explicit )] - public unsafe struct SeFileDescriptor - { - [FieldOffset( 0x00 )] - public FileMode FileMode; - - [FieldOffset( 0x30 )] - public void* FileDescriptor; // - - [FieldOffset( 0x50 )] - public ResourceHandle* ResourceHandle; // - - - [FieldOffset( 0x70 )] - public byte UtfFileName; // - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuBar.cs b/Penumbra/UI/MenuBar.cs deleted file mode 100644 index 5c3c6e41..00000000 --- a/Penumbra/UI/MenuBar.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI -{ - public partial class SettingsInterface - { - private class MenuBar - { - private const string MenuLabel = "Penumbra"; - private const string MenuItemToggle = "Toggle UI"; - private const string SlashCommand = "/penumbra"; - private const string MenuItemRediscover = "Rediscover Mods"; - private const string MenuItemHide = "Hide Menu Bar"; - -#if DEBUG - private bool _showDebugBar = true; -#else - private const bool _showDebugBar = false; -#endif - - private readonly SettingsInterface _base; - - public MenuBar( SettingsInterface ui ) - => _base = ui; - - public void Draw() - { - if( !_showDebugBar || !ImGui.BeginMainMenuBar() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndMainMenuBar ); - - if( !ImGui.BeginMenu( MenuLabel ) ) - { - return; - } - - raii.Push( ImGui.EndMenu ); - - if( ImGui.MenuItem( MenuItemToggle, SlashCommand, _base._menu.Visible ) ) - { - _base.FlipVisibility(); - } - - if( ImGui.MenuItem( MenuItemRediscover ) ) - { - _base.ReloadMods(); - } -#if DEBUG - if( ImGui.MenuItem( MenuItemHide ) ) - { - _showDebugBar = false; - } -#endif - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index 45c85437..9e45038d 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -1,70 +1,65 @@ using System.Collections.Generic; using System.Linq; using ImGuiNET; -using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabChangedItems { - private class TabChangedItems + private const string LabelTab = "Changed Items"; + private readonly SettingsInterface _base; + + private string _filter = string.Empty; + private string _filterLower = string.Empty; + + public TabChangedItems( SettingsInterface ui ) + => _base = ui; + + public void Draw() { - private const string LabelTab = "Changed Items"; - private readonly ModManager _modManager; - private readonly SettingsInterface _base; - - private string _filter = string.Empty; - private string _filterLower = string.Empty; - - public TabChangedItems( SettingsInterface ui ) + if( !ImGui.BeginTabItem( LabelTab ) ) { - _base = ui; - _modManager = Service< ModManager >.Get(); + return; } - public void Draw() + var modManager = Penumbra.ModManager; + var items = modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary(); - var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary(); + _filterLower = _filter.ToLowerInvariant(); + } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) + { + return; + } - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) - { - _filterLower = _filter.ToLowerInvariant(); - } + raii.Push( ImGui.EndTable ); - if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) - { - return; - } + var list = items.AsEnumerable(); + if( forced.Count > 0 ) + { + list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); + } - raii.Push( ImGui.EndTable ); + if( _filter.Any() ) + { + list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); + } - var list = items.AsEnumerable(); - if( forced.Count > 0 ) - { - list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); - } - - if( _filter.Any() ) - { - list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); - } - - foreach( var (name, data) in list ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); - } + foreach( var (name, data) in list ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); } } } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index cee1bd71..e2e6bdac 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -19,7 +19,6 @@ public partial class SettingsInterface { private const string CharacterCollectionHelpPopup = "Character Collection Information"; private readonly Selector _selector; - private readonly ModManager _manager; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; private ModCollection[] _collections = null!; @@ -32,7 +31,7 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = _manager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.ModManager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); @@ -52,18 +51,18 @@ public partial class SettingsInterface } private void UpdateIndex() - => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; + => _currentCollectionIndex = GetIndex( Penumbra.ModManager.Collections.CurrentCollection ) - 1; public void UpdateForcedIndex() - => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); + => _currentForcedIndex = GetIndex( Penumbra.ModManager.Collections.ForcedCollection ); public void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( _manager.Collections.DefaultCollection ); + => _currentDefaultIndex = GetIndex( Penumbra.ModManager.Collections.DefaultCollection ); private void UpdateCharacterIndices() { _currentCharacterIndices.Clear(); - foreach( var kvp in _manager.Collections.CharacterCollection ) + foreach( var kvp in Penumbra.ModManager.Collections.CharacterCollection ) { _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); } @@ -80,16 +79,16 @@ public partial class SettingsInterface public TabCollections( Selector selector ) { _selector = selector; - _manager = Service< ModManager >.Get(); UpdateNames(); } private void CreateNewCollection( Dictionary< string, ModSettings > settings ) { - if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) + var manager = Penumbra.ModManager; + if( manager.Collections.AddCollection( _newCollectionName, settings ) ) { UpdateNames(); - SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ], true ); + SetCurrentCollection( manager.Collections.Collections[ _newCollectionName ], true ); } _newCollectionName = string.Empty; @@ -99,9 +98,10 @@ public partial class SettingsInterface { if( ImGui.Button( "Clean Settings" ) ) { - var changes = ModFunctions.CleanUpCollection( _manager.Collections.CurrentCollection.Settings, - _manager.BasePath.EnumerateDirectories() ); - _manager.Collections.CurrentCollection.UpdateSettings( changes ); + var manager = Penumbra.ModManager; + var changes = ModFunctions.CleanUpCollection( manager.Collections.CurrentCollection.Settings, + manager.BasePath.EnumerateDirectories() ); + manager.Collections.CurrentCollection.UpdateSettings( changes ); } ImGuiCustom.HoverTooltip( @@ -126,9 +126,10 @@ public partial class SettingsInterface var hover = ImGui.IsItemHovered(); ImGui.SameLine(); + var manager = Penumbra.ModManager; if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( _manager.Collections.CurrentCollection.Settings ); + CreateNewCollection( manager.Collections.CurrentCollection.Settings ); } hover |= ImGui.IsItemHovered(); @@ -139,13 +140,13 @@ public partial class SettingsInterface ImGui.SetTooltip( "Please enter a name before creating a collection." ); } - var deleteCondition = _manager.Collections.Collections.Count > 1 - && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; + var deleteCondition = manager.Collections.Collections.Count > 1 + && manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; ImGui.SameLine(); if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) { - _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); - SetCurrentCollection( _manager.Collections.CurrentCollection, true ); + manager.Collections.RemoveCollection( manager.Collections.CurrentCollection.Name ); + SetCurrentCollection( manager.Collections.CurrentCollection, true ); UpdateNames(); } @@ -168,7 +169,7 @@ public partial class SettingsInterface return; } - _manager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); + Penumbra.ModManager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -207,7 +208,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) { - _manager.Collections.SetDefaultCollection( _collections[ index ] ); + Penumbra.ModManager.Collections.SetDefaultCollection( _collections[ index ] ); _currentDefaultIndex = index; } @@ -224,17 +225,18 @@ public partial class SettingsInterface { var index = _currentForcedIndex; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _manager.Collections.CharacterCollection.Count == 0 ); + var manager = Penumbra.ModManager; + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, manager.Collections.CharacterCollection.Count == 0 ); if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) - && index != _currentForcedIndex - && _manager.Collections.CharacterCollection.Count > 0 ) + && index != _currentForcedIndex + && manager.Collections.CharacterCollection.Count > 0 ) { - _manager.Collections.SetForcedCollection( _collections[ index ] ); + manager.Collections.SetForcedCollection( _collections[ index ] ); _currentForcedIndex = index; } style.Pop(); - if( _manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) + if( manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) { ImGui.SetTooltip( "Forced Collections only provide value if you have at least one Character Collection. There is no need to set one until then." ); @@ -260,7 +262,7 @@ public partial class SettingsInterface if( ImGuiCustom.DisableButton( "Create New Character Collection", _newCharacterName.Length > 0 && Penumbra.Config.HasReadCharacterCollectionDesc ) ) { - _manager.Collections.CreateCharacterCollection( _newCharacterName ); + Penumbra.ModManager.Collections.CreateCharacterCollection( _newCharacterName ); _currentCharacterIndices[ _newCharacterName ] = 0; _newCharacterName = string.Empty; } @@ -342,14 +344,15 @@ public partial class SettingsInterface DrawDefaultCollectionSelector(); DrawForcedCollectionSelector(); - foreach( var name in _manager.Collections.CharacterCollection.Keys.ToArray() ) + var manager = Penumbra.ModManager; + foreach( var name in manager.Collections.CharacterCollection.Keys.ToArray() ) { var idx = _currentCharacterIndices[ name ]; var tmp = idx; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) { - _manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); + manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); _currentCharacterIndices[ name ] = tmp; } @@ -360,7 +363,7 @@ public partial class SettingsInterface using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FramePadding, Vector2.One * ImGuiHelpers.GlobalScale * 1.5f ); if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) { - _manager.Collections.RemoveCharacterCollection( name ); + manager.Collections.RemoveCharacterCollection( name ); } style.Pop(); diff --git a/Penumbra/UI/MenuTabs/TabDebug.Model.cs b/Penumbra/UI/MenuTabs/TabDebug.Model.cs new file mode 100644 index 00000000..527a265d --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabDebug.Model.cs @@ -0,0 +1,131 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using ImGuiNET; +using Penumbra.UI.Custom; + +namespace Penumbra.UI; + +public partial class SettingsInterface +{ + [StructLayout( LayoutKind.Explicit )] + private unsafe struct RenderModel + { + [FieldOffset( 0x18 )] + public RenderModel* PreviousModel; + + [FieldOffset( 0x20 )] + public RenderModel* NextModel; + + [FieldOffset( 0x30 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x40 )] + public Skeleton* Skeleton; + + [FieldOffset( 0x58 )] + public void** BoneList; + + [FieldOffset( 0x60 )] + public int BoneListCount; + + [FieldOffset( 0x68 )] + private void* UnkDXBuffer1; + + [FieldOffset( 0x70 )] + private void* UnkDXBuffer2; + + [FieldOffset( 0x78 )] + private void* UnkDXBuffer3; + + [FieldOffset( 0x90 )] + public void** Materials; + + [FieldOffset( 0x98 )] + public int MaterialCount; + } + + [StructLayout( LayoutKind.Explicit )] + private unsafe struct Material + { + [FieldOffset( 0x10 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x28 )] + public void* MaterialData; + + [FieldOffset( 0x48 )] + public Texture* Tex1; + + [FieldOffset( 0x60 )] + public Texture* Tex2; + + [FieldOffset( 0x78 )] + public Texture* Tex3; + } + + private static unsafe void DrawPlayerModelInfo() + { + var player = Dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) + { + return; + } + + var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); + if( model == null ) + { + return; + } + + if( !ImGui.BeginTable( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Slot" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc File" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model File" ); + + for( var i = 0; i < model->SlotCount; ++i ) + { + var imc = ( ResourceHandle* )model->IMCArray[ i ]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( $"Slot {i}" ); + ImGui.TableNextColumn(); + ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); + ImGui.TableNextColumn(); + if( imc != null ) + { + ImGui.Text( imc->FileName.ToString() ); + } + + var mdl = ( RenderModel* )model->ModelArray[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); + if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + { + continue; + } + + ImGui.TableNextColumn(); + if( mdl != null ) + { + ImGui.Text( mdl->ResourceHandle->FileName.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 49961160..758a46ea 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Drawing.Text; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.FFXIV.Client.System.String; using ImGuiNET; using Penumbra.Api; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; @@ -16,6 +20,8 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; +using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; +using Utf8String = Penumbra.GameData.ByteString.Utf8String; namespace Penumbra.UI; @@ -143,7 +149,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - var manager = Service< ModManager >.Get(); + var manager = Penumbra.ModManager; PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); PrintValue( " has Cache", ( manager.Collections.ActiveCollection.Cache != null ).ToString() ); PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); @@ -164,7 +170,7 @@ public partial class SettingsInterface PrintValue( "Mod Manager Temp Path Exists", manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); - PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); + //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } private void DrawDebugTabRedraw() @@ -281,7 +287,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - foreach( var collection in Service< ModManager >.Get().Collections.Collections.Values.Where( c => c.Cache != null ) ) + foreach( var collection in Penumbra.ModManager.Collections.Collections.Values.Where( c => c.Cache != null ) ) { var manip = collection.Cache!.MetaManipulations; var files = ( Dictionary< GamePath, MetaManager.FileInformation >? )manip.GetType() @@ -363,8 +369,7 @@ public partial class SettingsInterface return; } - var manager = Service< ModManager >.Get(); - var cache = manager.Collections.CurrentCollection.Cache; + var cache = Penumbra.ModManager.Collections.CurrentCollection.Cache; if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) { return; @@ -385,6 +390,86 @@ public partial class SettingsInterface } } + private unsafe void DrawDebugTabReplacedResources() + { + if( !ImGui.CollapsingHeader( "Replaced Resources##Debug" ) ) + { + return; + } + + _penumbra.ResourceLoader.UpdateDebugInfo(); + + if( _penumbra.ResourceLoader.DebugList.Count == 0 + || !ImGui.BeginTable( "##ReplacedResourcesDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + foreach( var data in _penumbra.ResourceLoader.DebugList.Values.ToArray() ) + { + var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; + var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; + ImGui.TableNextColumn(); + ImGui.Text( data.ManipulatedPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountManip.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( data.OriginalPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountOrig.ToString() ); + } + } + + private unsafe void DrawPathResolverDebug() + { + if( !ImGui.CollapsingHeader( "Path Resolver##Debug" ) ) + { + return; + } + + //if( ImGui.TreeNodeEx( "Draw Object to Object" ) ) + //{ + // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + // if( ImGui.BeginTable( "###DrawObjectResolverTable", 4, ImGuiTableFlags.SizingFixedFit ) ) + // { + // end.Push( ImGui.EndTable ); + // foreach( var (ptr, idx) in _penumbra.PathResolver._drawObjectToObject ) + // { + // ImGui.TableNextColumn(); + // ImGui.Text( ptr.ToString( "X" ) ); + // ImGui.TableNextColumn(); + // ImGui.Text( idx.ToString() ); + // ImGui.TableNextColumn(); + // ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + // ImGui.TableNextColumn(); + // ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + // } + // } + //} + // + //if( ImGui.TreeNodeEx( "Path Collections" ) ) + //{ + // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + // if( ImGui.BeginTable( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ) ) + // { + // end.Push( ImGui.EndTable ); + // foreach( var (path, collection) in _penumbra.PathResolver._pathCollections ) + // { + // ImGui.TableNextColumn(); + // ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + // ImGui.TableNextColumn(); + // ImGui.Text( collection.Name ); + // } + // } + //} + } + private void DrawDebugTab() { if( !ImGui.BeginTabItem( "Debug Tab" ) ) @@ -396,8 +481,16 @@ public partial class SettingsInterface DrawDebugTabGeneral(); ImGui.NewLine(); + DrawDebugTabReplacedResources(); + ImGui.NewLine(); + DrawResourceProblems(); + ImGui.NewLine(); DrawDebugTabMissingFiles(); ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); DrawDebugTabPlayers(); diff --git a/Penumbra/UI/MenuTabs/TabDebugModels.cs b/Penumbra/UI/MenuTabs/TabDebugModels.cs deleted file mode 100644 index 67765caf..00000000 --- a/Penumbra/UI/MenuTabs/TabDebugModels.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using ImGuiNET; -using Lumina.Models.Models; -using Penumbra.UI.Custom; -using DalamudCharacter = Dalamud.Game.ClientState.Objects.Types.Character; - -namespace Penumbra.UI -{ - public partial class SettingsInterface - { - [StructLayout( LayoutKind.Explicit )] - private unsafe struct RenderModel - { - [FieldOffset(0x18)] - public RenderModel* PreviousModel; - [FieldOffset( 0x20 )] - public RenderModel* NextModel; - - [FieldOffset( 0x30 )] - public ResourceHandle* ResourceHandle; - - [FieldOffset( 0x40 )] - public Skeleton* Skeleton; - - [FieldOffset( 0x58 )] - public void** BoneList; - [FieldOffset( 0x60 )] - public int BoneListCount; - - [FieldOffset( 0x68 )] - private void* UnkDXBuffer1; - - [FieldOffset( 0x70 )] - private void* UnkDXBuffer2; - - [FieldOffset( 0x78 )] - private void* UnkDXBuffer3; - - [FieldOffset( 0x90 )] - public void** Materials; - - [FieldOffset( 0x98 )] - public int MaterialCount; - } - - [StructLayout( LayoutKind.Explicit )] - private unsafe struct Material - { - [FieldOffset(0x10)] - public ResourceHandle* ResourceHandle; - [FieldOffset(0x28)] - public void* MaterialData; - - [FieldOffset( 0x48 )] - public Texture* Tex1; - [FieldOffset( 0x60 )] - public Texture* Tex2; - [FieldOffset( 0x78 )] - public Texture* Tex3; - } - - private static unsafe void DrawPlayerModelInfo( DalamudCharacter character ) - { - var name = character.Name.ToString(); - if( !ImGui.CollapsingHeader( $"{name}##Draw" ) ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )character.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - if( !ImGui.BeginTable( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) - { - ImGui.Text( imc->FileName.ToString() ); - } - - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - continue; - } - - - ImGui.TableNextColumn(); - if( mdl != null ) - { - ImGui.Text( mdl->ResourceHandle->FileName.ToString() ); - } - } - } - - private void DrawPlayerModelTab() - { - if( !ImGui.BeginTabItem( "Model Debug" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var player = Dalamud.ClientState.LocalPlayer; - if( player == null ) - { - return; - } - - DrawPlayerModelInfo( player ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 63edcebf..ff2606f8 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -3,215 +3,210 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabEffective { - private class TabEffective + private const string LabelTab = "Effective Changes"; + + private string _gamePathFilter = string.Empty; + private string _gamePathFilterLower = string.Empty; + private string _filePathFilter = string.Empty; + private string _filePathFilterLower = string.Empty; + + private readonly float _leftTextLength = + ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; + + private float _arrowLength = 0; + + private static void DrawLine( string path, string name ) { - private const string LabelTab = "Effective Changes"; - private readonly ModManager _modManager; + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( path ); - private string _gamePathFilter = string.Empty; - private string _gamePathFilterLower = string.Empty; - private string _filePathFilter = string.Empty; - private string _filePathFilterLower = string.Empty; + ImGui.TableNextColumn(); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.SameLine(); + ImGuiCustom.CopyOnClickSelectable( name ); + } - private readonly float _leftTextLength = - ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; - - private float _arrowLength = 0; - - public TabEffective() - => _modManager = Service< ModManager >.Get(); - - - private static void DrawLine( string path, string name ) + private void DrawFilters() + { + if( _arrowLength == 0 ) { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( path ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.SameLine(); - ImGuiCustom.CopyOnClickSelectable( name ); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; } - private void DrawFilters() + ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); + if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) { - if( _arrowLength == 0 ) + _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + } + + ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + { + _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + } + } + + private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( (string, string, string) kvp ) + { + if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + } + + private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + { + void DrawFileLines( ModCollectionCache cache ) + { + foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) { - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; + DrawLine( gp, fp.FullName ); } - ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) + foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) { - _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + DrawLine( gp, fp ); } - ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations + .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) + .Where( CheckFilters ) ) { - _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + DrawLine( mp, mod ); } } - private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) + if( active != null ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) - { - return false; - } - - return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + DrawFileLines( active ); } - private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + if( forced != null ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) - { - return false; - } + DrawFileLines( forced ); + } + } - return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + public void Draw() + { + if( !ImGui.BeginTabItem( LabelTab ) ) + { + return; } - private bool CheckFilters( (string, string, string) kvp ) - { - if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) - { - return false; - } + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + DrawFilters(); + + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; + + var modManager = Penumbra.ModManager; + var activeCollection = modManager.Collections.ActiveCollection.Cache; + var forcedCollection = modManager.Collections.ForcedCollection.Cache; + + var (activeResolved, activeSwap, activeMeta) = activeCollection != null + ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null + ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; + if( totalLines == 0 ) + { + return; } - private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) { - void DrawFileLines( ModCollectionCache cache ) + raii.Push( ImGui.EndTable ); + ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); + + if( _filePathFilter.Any() || _gamePathFilter.Any() ) { - foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) + DrawFilteredRows( activeCollection, forcedCollection ); + } + else + { + ImGuiListClipperPtr clipper; + unsafe { - DrawLine( gp, fp.FullName ); + clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); } - foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) + clipper.Begin( totalLines ); + + + while( clipper.Step() ) { - DrawLine( gp, fp ); - } - - foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations - .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - } - - if( active != null ) - { - DrawFileLines( active ); - } - - if( forced != null ) - { - DrawFileLines( forced ); - } - } - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - DrawFilters(); - - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - var activeCollection = _modManager.Collections.ActiveCollection.Cache; - var forcedCollection = _modManager.Collections.ForcedCollection.Cache; - - var (activeResolved, activeSwap, activeMeta) = activeCollection != null - ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null - ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; - if( totalLines == 0 ) - { - return; - } - - if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) - { - raii.Push( ImGui.EndTable ); - ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); - - if( _filePathFilter.Any() || _gamePathFilter.Any() ) - { - DrawFilteredRows( activeCollection, forcedCollection ); - } - else - { - ImGuiListClipperPtr clipper; - unsafe + for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) { - clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); - } - - clipper.Begin( totalLines ); - - - while( clipper.Step() ) - { - for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) + var row = actualRow; + ImGui.TableNextRow(); + if( row < activeResolved ) { - var row = actualRow; - ImGui.TableNextRow(); - if( row < activeResolved ) - { - var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= activeResolved ) < activeSwap ) - { - var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else if( ( row -= activeSwap ) < activeMeta ) - { - var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } - else if( ( row -= activeMeta ) < forcedResolved ) - { - var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= forcedResolved ) < forcedSwap ) - { - var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else - { - row -= forcedSwap; - var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } + var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= activeResolved ) < activeSwap ) + { + var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else if( ( row -= activeSwap ) < activeMeta ) + { + var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + } + else if( ( row -= activeMeta ) < forcedResolved ) + { + var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= forcedResolved ) < forcedSwap ) + { + var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else + { + row -= forcedSwap; + var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); } } } @@ -219,5 +214,4 @@ namespace Penumbra.UI } } } -} - +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabImport.cs b/Penumbra/UI/MenuTabs/TabImport.cs index 6680947b..4b49f88a 100644 --- a/Penumbra/UI/MenuTabs/TabImport.cs +++ b/Penumbra/UI/MenuTabs/TabImport.cs @@ -8,188 +8,182 @@ using System.Windows.Forms; using Dalamud.Logging; using ImGuiNET; using Penumbra.Importer; -using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabImport { - private class TabImport + private const string LabelTab = "Import Mods"; + private const string LabelImportButton = "Import TexTools Modpacks"; + private const string LabelFileDialog = "Pick one or more modpacks."; + private const string LabelFileImportRunning = "Import in progress..."; + private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; + private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; + + private const uint ColorRed = 0xFF0000C8; + private const uint ColorYellow = 0xFF00C8C8; + + private static readonly Vector2 ImportBarSize = new(-1, 0); + + private bool _isImportRunning; + private string _errorMessage = string.Empty; + private TexToolsImport? _texToolsImport; + private readonly SettingsInterface _base; + + public readonly HashSet< string > NewMods = new(); + + public TabImport( SettingsInterface ui ) + => _base = ui; + + public bool IsImporting() + => _isImportRunning; + + private void RunImportTask() { - private const string LabelTab = "Import Mods"; - private const string LabelImportButton = "Import TexTools Modpacks"; - private const string LabelFileDialog = "Pick one or more modpacks."; - private const string LabelFileImportRunning = "Import in progress..."; - private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; - private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; - - private const uint ColorRed = 0xFF0000C8; - private const uint ColorYellow = 0xFF00C8C8; - - private static readonly Vector2 ImportBarSize = new( -1, 0 ); - - private bool _isImportRunning; - private string _errorMessage = string.Empty; - private TexToolsImport? _texToolsImport; - private readonly SettingsInterface _base; - private readonly ModManager _manager; - - public readonly HashSet< string > NewMods = new(); - - public TabImport( SettingsInterface ui ) + _isImportRunning = true; + Task.Run( async () => { - _base = ui; - _manager = Service< ModManager >.Get(); - } - - public bool IsImporting() - => _isImportRunning; - - private void RunImportTask() - { - _isImportRunning = true; - Task.Run( async () => + try { - try + var picker = new OpenFileDialog { - var picker = new OpenFileDialog + Multiselect = true, + Filter = FileTypeFilter, + CheckFileExists = true, + Title = LabelFileDialog, + }; + + var result = await picker.ShowDialogAsync(); + + if( result == DialogResult.OK ) + { + _errorMessage = string.Empty; + + foreach( var fileName in picker.FileNames ) { - Multiselect = true, - Filter = FileTypeFilter, - CheckFileExists = true, - Title = LabelFileDialog, - }; + PluginLog.Information( $"-> {fileName} START" ); - var result = await picker.ShowDialogAsync(); - - if( result == DialogResult.OK ) - { - _errorMessage = string.Empty; - - foreach( var fileName in picker.FileNames ) + try { - PluginLog.Information( $"-> {fileName} START" ); - - try + _texToolsImport = new TexToolsImport( Penumbra.ModManager.BasePath ); + var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + if( dir.Name.Any() ) { - _texToolsImport = new TexToolsImport( _manager.BasePath ); - var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); - if( dir.Name.Any() ) - { - NewMods.Add( dir.Name ); - } + NewMods.Add( dir.Name ); + } - PluginLog.Information( $"-> {fileName} OK!" ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); - _errorMessage = ex.Message; - } + PluginLog.Information( $"-> {fileName} OK!" ); } - - var directory = _texToolsImport?.ExtractedDirectory; - _texToolsImport = null; - _base.ReloadMods(); - if( directory != null ) + catch( Exception ex ) { - _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); + PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); + _errorMessage = ex.Message; } } + + var directory = _texToolsImport?.ExtractedDirectory; + _texToolsImport = null; + _base.ReloadMods(); + if( directory != null ) + { + _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); + } } - catch( Exception e ) - { - PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); - } + } + catch( Exception e ) + { + PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); + } - _isImportRunning = false; - } ); - } + _isImportRunning = false; + } ); + } - private void DrawImportButton() + private void DrawImportButton() + { + if( !Penumbra.ModManager.Valid ) { - if( !_manager.Valid ) - { - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( LabelImportButton ); - style.Pop(); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); + ImGui.Button( LabelImportButton ); + style.Pop(); - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( "Can not import since the mod directory path is not valid." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); - color.Pop(); - - ImGui.Text( "Please set the mod directory in the settings tab." ); - ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); - color.Push( ImGuiCol.Text, ColorYellow ); - ImGui.Text( " D:\\ffxivmods" ); - color.Pop(); - ImGui.Text( "You can return to this tab once you've done that." ); - } - else if( ImGui.Button( LabelImportButton ) ) - { - RunImportTask(); - } - } - - private void DrawImportProgress() - { - ImGui.Button( LabelFileImportRunning ); - - if( _texToolsImport == null ) - { - return; - } - - switch( _texToolsImport.State ) - { - case ImporterState.None: break; - case ImporterState.WritingPackToDisk: - ImGui.Text( TooltipModpack1 ); - break; - case ImporterState.ExtractingModFiles: - { - var str = - $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - - ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); - break; - } - case ImporterState.Done: break; - default: throw new ArgumentOutOfRangeException(); - } - } - - private void DrawFailedImportMessage() - { using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); + ImGui.Text( "Can not import since the mod directory path is not valid." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); + color.Pop(); + + ImGui.Text( "Please set the mod directory in the settings tab." ); + ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); + color.Push( ImGuiCol.Text, ColorYellow ); + ImGui.Text( " D:\\ffxivmods" ); + color.Pop(); + ImGui.Text( "You can return to this tab once you've done that." ); + } + else if( ImGui.Button( LabelImportButton ) ) + { + RunImportTask(); + } + } + + private void DrawImportProgress() + { + ImGui.Button( LabelFileImportRunning ); + + if( _texToolsImport == null ) + { + return; } - public void Draw() + switch( _texToolsImport.State ) { - if( !ImGui.BeginTabItem( LabelTab ) ) + case ImporterState.None: break; + case ImporterState.WritingPackToDisk: + ImGui.Text( TooltipModpack1 ); + break; + case ImporterState.ExtractingModFiles: { - return; - } + var str = + $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); + break; + } + case ImporterState.Done: break; + default: throw new ArgumentOutOfRangeException(); + } + } - if( !_isImportRunning ) - { - DrawImportButton(); - } - else - { - DrawImportProgress(); - } + private void DrawFailedImportMessage() + { + using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); + ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); + } - if( _errorMessage.Any() ) - { - DrawFailedImportMessage(); - } + public void Draw() + { + if( !ImGui.BeginTabItem( LabelTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !_isImportRunning ) + { + DrawImportButton(); + } + else + { + DrawImportProgress(); + } + + if( _errorMessage.Any() ) + { + DrawFailedImportMessage(); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs index c4ed131c..beecf9b6 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs @@ -1,42 +1,37 @@ using System.Collections.Generic; using ImGuiNET; -using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabInstalled { - private class TabInstalled + private const string LabelTab = "Installed Mods"; + + public readonly Selector Selector; + public readonly ModPanel ModPanel; + + public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) { - private const string LabelTab = "Installed Mods"; + Selector = new Selector( ui, newMods ); + ModPanel = new ModPanel( ui, Selector, newMods ); + } - private readonly ModManager _modManager; - public readonly Selector Selector; - public readonly ModPanel ModPanel; - - public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) { - Selector = new Selector( ui, newMods ); - ModPanel = new ModPanel( ui, Selector, newMods ); - _modManager = Service< ModManager >.Get(); + return; } - public void Draw() - { - var ret = ImGui.BeginTabItem( LabelTab ); - if( !ret ) - { - return; - } + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - Selector.Draw(); - ImGui.SameLine(); - ModPanel.Draw(); - } + Selector.Draw(); + ImGui.SameLine(); + ModPanel.Draw(); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index cf079a24..cabba206 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -5,6 +5,7 @@ using Dalamud.Interface; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; @@ -15,693 +16,691 @@ using Penumbra.UI.Custom; using Penumbra.Util; using ImGui = ImGuiNET.ImGui; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private partial class PluginDetails { - private partial class PluginDetails + private const string LabelPluginDetails = "PenumbraPluginDetails"; + private const string LabelAboutTab = "About"; + private const string LabelChangedItemsTab = "Changed Items"; + private const string LabelChangedItemsHeader = "##changedItems"; + private const string LabelConflictsTab = "Mod Conflicts"; + private const string LabelConflictsHeader = "##conflicts"; + private const string LabelFileSwapTab = "File Swaps"; + private const string LabelFileSwapHeader = "##fileSwaps"; + private const string LabelFileListTab = "Files"; + private const string LabelFileListHeader = "##fileList"; + private const string LabelGroupSelect = "##groupSelect"; + private const string LabelOptionSelect = "##optionSelect"; + private const string LabelConfigurationTab = "Configuration"; + + private const string TooltipFilesTab = + "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" + + "Yellow files are restricted to some options."; + + private const float OptionSelectionWidth = 140f; + private const float CheckMarkSize = 50f; + private const uint ColorDarkGreen = 0xFF00A000; + private const uint ColorGreen = 0xFF00C800; + private const uint ColorYellow = 0xFF00C8C8; + private const uint ColorDarkRed = 0xFF0000A0; + private const uint ColorRed = 0xFF0000C8; + + + private bool _editMode; + private int _selectedGroupIndex; + private OptionGroup? _selectedGroup; + private int _selectedOptionIndex; + private Option? _selectedOption; + private string _currentGamePaths = ""; + + private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; + + private readonly Selector _selector; + private readonly SettingsInterface _base; + + private void SelectGroup( int idx ) { - private const string LabelPluginDetails = "PenumbraPluginDetails"; - private const string LabelAboutTab = "About"; - private const string LabelChangedItemsTab = "Changed Items"; - private const string LabelChangedItemsHeader = "##changedItems"; - private const string LabelConflictsTab = "Mod Conflicts"; - private const string LabelConflictsHeader = "##conflicts"; - private const string LabelFileSwapTab = "File Swaps"; - private const string LabelFileSwapHeader = "##fileSwaps"; - private const string LabelFileListTab = "Files"; - private const string LabelFileListHeader = "##fileList"; - private const string LabelGroupSelect = "##groupSelect"; - private const string LabelOptionSelect = "##optionSelect"; - private const string LabelConfigurationTab = "Configuration"; - - private const string TooltipFilesTab = - "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" - + "Yellow files are restricted to some options."; - - private const float OptionSelectionWidth = 140f; - private const float CheckMarkSize = 50f; - private const uint ColorDarkGreen = 0xFF00A000; - private const uint ColorGreen = 0xFF00C800; - private const uint ColorYellow = 0xFF00C8C8; - private const uint ColorDarkRed = 0xFF0000A0; - private const uint ColorRed = 0xFF0000C8; - - - private bool _editMode; - private int _selectedGroupIndex; - private OptionGroup? _selectedGroup; - private int _selectedOptionIndex; - private Option? _selectedOption; - private string _currentGamePaths = ""; - - private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; - - private readonly Selector _selector; - private readonly SettingsInterface _base; - private readonly ModManager _modManager; - - private void SelectGroup( int idx ) + // Not using the properties here because we need it to be not null forgiving in this case. + var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; + _selectedGroupIndex = idx; + if( _selectedGroupIndex >= numGroups ) { - // Not using the properties here because we need it to be not null forgiving in this case. - var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; - _selectedGroupIndex = idx; - if( _selectedGroupIndex >= numGroups ) - { - _selectedGroupIndex = 0; - } - - if( numGroups > 0 ) - { - _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; - } - else - { - _selectedGroup = null; - } + _selectedGroupIndex = 0; } - private void SelectGroup() - => SelectGroup( _selectedGroupIndex ); - - private void SelectOption( int idx ) + if( numGroups > 0 ) { - _selectedOptionIndex = idx; - if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) - { - _selectedOptionIndex = 0; - } + _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; + } + else + { + _selectedGroup = null; + } + } - if( _selectedGroup?.Options.Count > 0 ) - { - _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; - } - else - { - _selectedOption = null; - } + private void SelectGroup() + => SelectGroup( _selectedGroupIndex ); + + private void SelectOption( int idx ) + { + _selectedOptionIndex = idx; + if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) + { + _selectedOptionIndex = 0; } - private void SelectOption() - => SelectOption( _selectedOptionIndex ); + if( _selectedGroup?.Options.Count > 0 ) + { + _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; + } + else + { + _selectedOption = null; + } + } - public void ResetState() + private void SelectOption() + => SelectOption( _selectedOptionIndex ); + + public void ResetState() + { + _fullFilenameList = null; + SelectGroup(); + SelectOption(); + } + + public PluginDetails( SettingsInterface ui, Selector s ) + { + _base = ui; + _selector = s; + ResetState(); + } + + // This is only drawn when we have a mod selected, so we can forgive nulls. + private Mod.Mod Mod + => _selector.Mod!; + + private ModMeta Meta + => Mod.Data.Meta; + + private void Save() + { + Penumbra.ModManager.Collections.CurrentCollection.Save(); + } + + private void DrawAboutTab() + { + if( !_editMode && Meta.Description.Length == 0 ) + { + return; + } + + if( !ImGui.BeginTabItem( LabelAboutTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + var desc = Meta.Description; + var flags = _editMode + ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine + : ImGuiInputTextFlags.ReadOnly; + + if( _editMode ) + { + if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, + AutoFillSize, flags ) ) + { + Meta.Description = desc; + _selector.SaveCurrentMod(); + } + + ImGuiCustom.HoverTooltip( TooltipAboutEdit ); + } + else + { + ImGui.TextWrapped( desc ); + } + } + + private void DrawChangedItemsTab() + { + if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + foreach( var (name, data) in Mod.Data.ChangedItems ) + { + _base.DrawChangedItem( name, data ); + } + } + + private void DrawConflictTab() + { + if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + using var indent = ImGuiRaii.PushIndent( 0 ); + foreach( var (mod, (files, manipulations)) in Mod.Cache.Conflicts ) + { + if( ImGui.Selectable( mod.Data.Meta.Name ) ) + { + _selector.SelectModByDir( mod.Data.BasePath.Name ); + } + + ImGui.SameLine(); + ImGui.Text( $"(Priority {mod.Settings.Priority})" ); + + indent.Push( 15f ); + foreach( var file in files ) + { + ImGui.Selectable( file ); + } + + foreach( var manip in manipulations ) + { + ImGui.Text( manip.IdentifierString() ); + } + + indent.Pop( 15f ); + } + } + + private void DrawFileSwapTab() + { + if( _editMode ) + { + DrawFileSwapTabEdit(); + return; + } + + if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; + + ImGui.SetNextItemWidth( -1 ); + if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndTable ); + + foreach( var (source, target) in Meta.FileSwaps ) + { + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( source ); + + ImGui.TableNextColumn(); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); + + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( target ); + + ImGui.TableNextRow(); + } + } + + private void UpdateFilenameList() + { + if( _fullFilenameList != null ) + { + return; + } + + _fullFilenameList = Mod.Data.Resources.ModFiles + .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); + + if( Meta.Groups.Count == 0 ) + { + return; + } + + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) + { + foreach( var group in Meta.Groups.Values ) + { + var inAll = true; + foreach( var option in group.Options ) + { + if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) + { + _fullFilenameList[ i ].color = ColorYellow; + } + else + { + inAll = false; + } + } + + if( inAll && group.SelectionType == SelectType.Single ) + { + _fullFilenameList[ i ].color = ColorGreen; + } + } + } + } + + private void DrawFileListTab() + { + if( !ImGui.BeginTabItem( LabelFileListTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + ImGuiCustom.HoverTooltip( TooltipFilesTab ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) + { + raii.Push( ImGui.EndListBox ); + UpdateFilenameList(); + using var colorRaii = new ImGuiRaii.Color(); + foreach( var (name, _, color, _) in _fullFilenameList! ) + { + colorRaii.Push( ImGuiCol.Text, color ); + ImGui.Selectable( name.FullName ); + colorRaii.Pop(); + } + } + else { _fullFilenameList = null; - SelectGroup(); - SelectOption(); } + } - public PluginDetails( SettingsInterface ui, Selector s ) + private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) + { + removeFolders = 0; + var defaultIndex = + gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); + if( defaultIndex < 0 ) { - _base = ui; - _selector = s; - ResetState(); - _modManager = Service< ModManager >.Get(); - } - - // This is only drawn when we have a mod selected, so we can forgive nulls. - private Mod.Mod Mod - => _selector.Mod!; - - private ModMeta Meta - => Mod.Data.Meta; - - private void Save() - { - _modManager.Collections.CurrentCollection.Save(); - } - - private void DrawAboutTab() - { - if( !_editMode && Meta.Description.Length == 0 ) - { - return; - } - - if( !ImGui.BeginTabItem( LabelAboutTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var desc = Meta.Description; - var flags = _editMode - ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine - : ImGuiInputTextFlags.ReadOnly; - - if( _editMode ) - { - if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, - AutoFillSize, flags ) ) - { - Meta.Description = desc; - _selector.SaveCurrentMod(); - } - - ImGuiCustom.HoverTooltip( TooltipAboutEdit ); - } - else - { - ImGui.TextWrapped( desc ); - } - } - - private void DrawChangedItemsTab() - { - if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - foreach( var (name, data) in Mod.Data.ChangedItems ) - { - _base.DrawChangedItem( name, data ); - } - } - - private void DrawConflictTab() - { - if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - using var indent = ImGuiRaii.PushIndent( 0 ); - foreach( var (mod, (files, manipulations)) in Mod.Cache.Conflicts ) - { - if( ImGui.Selectable( mod.Data.Meta.Name ) ) - { - _selector.SelectModByDir( mod.Data.BasePath.Name ); - } - - ImGui.SameLine(); - ImGui.Text( $"(Priority {mod.Settings.Priority})" ); - - indent.Push( 15f ); - foreach( var file in files ) - { - ImGui.Selectable( file ); - } - - foreach( var manip in manipulations ) - { - ImGui.Text( manip.IdentifierString() ); - } - - indent.Pop( 15f ); - } - } - - private void DrawFileSwapTab() - { - if( _editMode ) - { - DrawFileSwapTabEdit(); - return; - } - - if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - foreach( var (source, target) in Meta.FileSwaps ) - { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( source ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); - - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( target ); - - ImGui.TableNextRow(); - } - } - - private void UpdateFilenameList() - { - if( _fullFilenameList != null ) - { - return; - } - - _fullFilenameList = Mod.Data.Resources.ModFiles - .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); - - if( Meta.Groups.Count == 0 ) - { - return; - } - - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - foreach( var group in Meta.Groups.Values ) - { - var inAll = true; - foreach( var option in group.Options ) - { - if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) - { - _fullFilenameList[ i ].color = ColorYellow; - } - else - { - inAll = false; - } - } - - if( inAll && group.SelectionType == SelectType.Single ) - { - _fullFilenameList[ i ].color = ColorGreen; - } - } - } - } - - private void DrawFileListTab() - { - if( !ImGui.BeginTabItem( LabelFileListTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - ImGuiCustom.HoverTooltip( TooltipFilesTab ); - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) - { - raii.Push( ImGui.EndListBox ); - UpdateFilenameList(); - using var colorRaii = new ImGuiRaii.Color(); - foreach( var (name, _, color, _) in _fullFilenameList! ) - { - colorRaii.Push( ImGuiCol.Text, color ); - ImGui.Selectable( name.FullName ); - colorRaii.Pop(); - } - } - else - { - _fullFilenameList = null; - } - } - - private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) - { - removeFolders = 0; - var defaultIndex = - gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); - if( defaultIndex < 0 ) - { - return defaultIndex; - } - - string path = gamePaths[ defaultIndex ]; - if( path.Length == TextDefaultGamePath.Length ) - { - return defaultIndex; - } - - if( path[ TextDefaultGamePath.Length ] != '-' - || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) - { - return -1; - } - return defaultIndex; } - private void HandleSelectedFilesButton( bool remove ) + string path = gamePaths[ defaultIndex ]; + if( path.Length == TextDefaultGamePath.Length ) { - if( _selectedOption == null ) - { - return; - } - - var option = ( Option )_selectedOption; - - var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); - if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) - { - return; - } - - var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); - var changed = false; - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - if( !_fullFilenameList![ i ].selected ) - { - continue; - } - - _fullFilenameList![ i ].selected = false; - var relName = _fullFilenameList[ i ].relName; - if( defaultIndex >= 0 ) - { - gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); - } - - if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) - { - if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) - { - changed = true; - } - - if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) - { - changed = true; - } - } - else - { - changed = gamePaths - .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); - } - } - - if( changed ) - { - _fullFilenameList = null; - _selector.SaveCurrentMod(); - // Since files may have changed, we need to recompute effective files. - foreach( var collection in _modManager.Collections.Collections.Values - .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) - { - collection.CalculateEffectiveFileList( _modManager.TempPath, false, - collection == _modManager.Collections.ActiveCollection ); - } - - // If the mod is enabled in the current collection, its conflicts may have changed. - if( Mod!.Settings.Enabled ) - { - _selector.Cache.TriggerFilterReset(); - } - } + return defaultIndex; } - private void DrawAddToGroupButton() + if( path[ TextDefaultGamePath.Length ] != '-' + || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) { - if( ImGui.Button( ButtonAddToGroup ) ) - { - HandleSelectedFilesButton( false ); - } + return -1; } - private void DrawRemoveFromGroupButton() + return defaultIndex; + } + + private void HandleSelectedFilesButton( bool remove ) + { + if( _selectedOption == null ) { - if( ImGui.Button( ButtonRemoveFromGroup ) ) - { - HandleSelectedFilesButton( true ); - } + return; } - private void DrawGamePathInput() + var option = ( Option )_selectedOption; + + var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); + if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) { - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, - 128 ); - ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); + return; } - private void DrawGroupRow() + var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); + var changed = false; + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) { - if( _selectedGroup == null ) + if( !_fullFilenameList![ i ].selected ) { - SelectGroup(); + continue; } - if( _selectedOption == null ) + _fullFilenameList![ i ].selected = false; + var relName = _fullFilenameList[ i ].relName; + if( defaultIndex >= 0 ) { - SelectOption(); + gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); } - if( !DrawEditGroupSelector() ) + if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) { - return; - } - - ImGui.SameLine(); - if( !DrawEditOptionSelector() ) - { - return; - } - - ImGui.SameLine(); - DrawAddToGroupButton(); - ImGui.SameLine(); - DrawRemoveFromGroupButton(); - ImGui.SameLine(); - DrawGamePathInput(); - } - - private void DrawFileAndGamePaths( int idx ) - { - void Selectable( uint colorNormal, uint colorReplace ) - { - var loc = _fullFilenameList![ idx ].color; - if( loc == colorNormal ) + if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) { - loc = colorReplace; + changed = true; } - using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); - ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); - } - - const float indentWidth = 30f; - if( _selectedOption == null ) - { - Selectable( 0, ColorGreen ); - return; - } - - var fileName = _fullFilenameList![ idx ].relName; - var optionFiles = ( ( Option )_selectedOption ).OptionFiles; - if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) - { - Selectable( 0, ColorGreen ); - - using var indent = ImGuiRaii.PushIndent( indentWidth ); - var tmpPaths = gamePaths.ToArray(); - foreach( var gamePath in tmpPaths ) + if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) { - string tmp = gamePath; - if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) - && tmp != gamePath ) - { - gamePaths.Remove( gamePath ); - if( tmp.Length > 0 ) - { - gamePaths.Add( new GamePath( tmp ) ); - } - else if( gamePaths.Count == 0 ) - { - optionFiles.Remove( fileName ); - } - - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); - } + changed = true; } } else { - Selectable( ColorYellow, ColorRed ); + changed = gamePaths + .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); } } - private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) + if( changed ) { - var enabled = ( flag & ( 1 << idx ) ) != 0; - var oldEnabled = enabled; - if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) + _fullFilenameList = null; + _selector.SaveCurrentMod(); + // Since files may have changed, we need to recompute effective files. + var modManager = Penumbra.ModManager; + foreach( var collection in modManager.Collections.Collections.Values + .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) { - Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; - Save(); - // If the mod is enabled, recalculate files and filters. - if( Mod.Settings.Enabled ) + collection.CalculateEffectiveFileList( modManager.TempPath, false, + collection == modManager.Collections.ActiveCollection ); + } + + // If the mod is enabled in the current collection, its conflicts may have changed. + if( Mod!.Settings.Enabled ) + { + _selector.Cache.TriggerFilterReset(); + } + } + } + + private void DrawAddToGroupButton() + { + if( ImGui.Button( ButtonAddToGroup ) ) + { + HandleSelectedFilesButton( false ); + } + } + + private void DrawRemoveFromGroupButton() + { + if( ImGui.Button( ButtonRemoveFromGroup ) ) + { + HandleSelectedFilesButton( true ); + } + } + + private void DrawGamePathInput() + { + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, + 128 ); + ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); + } + + private void DrawGroupRow() + { + if( _selectedGroup == null ) + { + SelectGroup(); + } + + if( _selectedOption == null ) + { + SelectOption(); + } + + if( !DrawEditGroupSelector() ) + { + return; + } + + ImGui.SameLine(); + if( !DrawEditOptionSelector() ) + { + return; + } + + ImGui.SameLine(); + DrawAddToGroupButton(); + ImGui.SameLine(); + DrawRemoveFromGroupButton(); + ImGui.SameLine(); + DrawGamePathInput(); + } + + private void DrawFileAndGamePaths( int idx ) + { + void Selectable( uint colorNormal, uint colorReplace ) + { + var loc = _fullFilenameList![ idx ].color; + if( loc == colorNormal ) + { + loc = colorReplace; + } + + using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); + ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); + } + + const float indentWidth = 30f; + if( _selectedOption == null ) + { + Selectable( 0, ColorGreen ); + return; + } + + var fileName = _fullFilenameList![ idx ].relName; + var optionFiles = ( ( Option )_selectedOption ).OptionFiles; + if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) + { + Selectable( 0, ColorGreen ); + + using var indent = ImGuiRaii.PushIndent( indentWidth ); + var tmpPaths = gamePaths.ToArray(); + foreach( var gamePath in tmpPaths ) + { + string tmp = gamePath; + if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) + && tmp != gamePath ) { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); - } - } - } - - private void DrawMultiSelector( OptionGroup group ) - { - if( group.Options.Count == 0 ) - { - return; - } - - ImGuiCustom.BeginFramedGroup( group.GroupName ); - using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); - for( var i = 0; i < group.Options.Count; ++i ) - { - DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], - $"{group.Options[ i ].OptionName}##{group.GroupName}" ); - } - } - - private void DrawSingleSelector( OptionGroup group ) - { - if( group.Options.Count < 2 ) - { - return; - } - - var code = Mod.Settings.Settings[ group.GroupName ]; - if( ImGui.Combo( group.GroupName, ref code - , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) - && code != Mod.Settings.Settings[ group.GroupName ] ) - { - Mod.Settings.Settings[ group.GroupName ] = code; - Save(); - // If the mod is enabled, recalculate files and filters. - if( Mod.Settings.Enabled ) - { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); - } - } - } - - private void DrawGroupSelectors() - { - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) - { - DrawSingleSelector( g ); - } - - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) - { - DrawMultiSelector( g ); - } - } - - private void DrawConfigurationTab() - { - if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - if( _editMode ) - { - DrawGroupSelectorsEdit(); - } - else - { - DrawGroupSelectors(); - } - } - - private void DrawMetaManipulationsTab() - { - if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - - var manips = Mod.Data.Resources.MetaManipulations; - var changes = false; - if( _editMode || manips.DefaultData.Count > 0 ) - { - if( ImGui.CollapsingHeader( "Default" ) ) - { - changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); - } - } - - foreach( var (groupName, group) in manips.GroupData ) - { - foreach( var (optionName, option) in group ) - { - if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) + gamePaths.Remove( gamePath ); + if( tmp.Length > 0 ) { - changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); + gamePaths.Add( new GamePath( tmp ) ); } + else if( gamePaths.Count == 0 ) + { + optionFiles.Remove( fileName ); + } + + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod(); } } - - if( changes ) - { - Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); - Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); - _selector.ReloadCurrentMod( true, false ); - } } - - public void Draw( bool editMode ) + else { - _editMode = editMode; - if( !ImGui.BeginTabBar( LabelPluginDetails ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); - DrawAboutTab(); - DrawChangedItemsTab(); - - DrawConfigurationTab(); - if( _editMode ) - { - DrawFileListTabEdit(); - } - else - { - DrawFileListTab(); - } - - DrawFileSwapTab(); - DrawMetaManipulationsTab(); - DrawConflictTab(); + Selectable( ColorYellow, ColorRed ); } } + + private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) + { + var enabled = ( flag & ( 1 << idx ) ) != 0; + var oldEnabled = enabled; + if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) + { + Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; + Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } + } + } + + private void DrawMultiSelector( OptionGroup group ) + { + if( group.Options.Count == 0 ) + { + return; + } + + ImGuiCustom.BeginFramedGroup( group.GroupName ); + using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); + for( var i = 0; i < group.Options.Count; ++i ) + { + DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], + $"{group.Options[ i ].OptionName}##{group.GroupName}" ); + } + } + + private void DrawSingleSelector( OptionGroup group ) + { + if( group.Options.Count < 2 ) + { + return; + } + + var code = Mod.Settings.Settings[ group.GroupName ]; + if( ImGui.Combo( group.GroupName, ref code + , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) + && code != Mod.Settings.Settings[ group.GroupName ] ) + { + Mod.Settings.Settings[ group.GroupName ] = code; + Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } + } + } + + private void DrawGroupSelectors() + { + foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) + { + DrawSingleSelector( g ); + } + + foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) + { + DrawMultiSelector( g ); + } + } + + private void DrawConfigurationTab() + { + if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + if( _editMode ) + { + DrawGroupSelectorsEdit(); + } + else + { + DrawGroupSelectors(); + } + } + + private void DrawMetaManipulationsTab() + { + if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + + var manips = Mod.Data.Resources.MetaManipulations; + var changes = false; + if( _editMode || manips.DefaultData.Count > 0 ) + { + if( ImGui.CollapsingHeader( "Default" ) ) + { + changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); + } + } + + foreach( var (groupName, group) in manips.GroupData ) + { + foreach( var (optionName, option) in group ) + { + if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) + { + changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); + } + } + } + + if( changes ) + { + Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); + Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); + _selector.ReloadCurrentMod( true, false ); + } + } + + public void Draw( bool editMode ) + { + _editMode = editMode; + if( !ImGui.BeginTabBar( LabelPluginDetails ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); + DrawAboutTab(); + DrawChangedItemsTab(); + + DrawConfigurationTab(); + if( _editMode ) + { + DrawFileListTabEdit(); + } + else + { + DrawFileListTab(); + } + + DrawFileSwapTab(); + DrawMetaManipulationsTab(); + DrawConflictTab(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 8248e91b..3c8a5feb 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -112,7 +112,7 @@ namespace Penumbra.UI var groupName = group.GroupName; if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) { - if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { _selector.Cache.TriggerFilterReset(); } @@ -165,7 +165,7 @@ namespace Penumbra.UI { if( newName.Length == 0 ) { - _modManager.RemoveModOption( i, group, Mod.Data ); + Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); } else if( newName != opt.OptionName ) { @@ -189,7 +189,7 @@ namespace Penumbra.UI var groupName = group.GroupName; if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { _selector.Cache.TriggerFilterReset(); } @@ -221,7 +221,7 @@ namespace Penumbra.UI { if( newName.Length == 0 ) { - _modManager.RemoveModOption( code, group, Mod.Data ); + Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); } else { @@ -267,7 +267,7 @@ namespace Penumbra.UI if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); // Adds empty group, so can not change filters. } } @@ -280,7 +280,7 @@ namespace Penumbra.UI if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); // Adds empty group, so can not change filters. } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 4c9a7ad2..4d3148ae 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -229,7 +229,7 @@ namespace Penumbra.UI if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) { using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ( EqpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var attributes = Eqp.EqpAttributes[ id.Slot ]; foreach( var flag in attributes ) @@ -254,7 +254,7 @@ namespace Penumbra.UI private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( GmpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( GmpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].GmpIdentifier; var val = list[ manipIdx ].GmpValue; @@ -364,7 +364,7 @@ namespace Penumbra.UI private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( EqdpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( EqdpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].EqdpIdentifier; var val = list[ manipIdx ].EqdpValue; @@ -401,7 +401,7 @@ namespace Penumbra.UI private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ushort )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( ushort )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].EstIdentifier; var val = list[ manipIdx ].EstValue; @@ -433,7 +433,7 @@ namespace Penumbra.UI private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ImcFile.ImageChangeData )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( ImcFile.ImageChangeData )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].ImcIdentifier; var val = list[ manipIdx ].ImcValue; @@ -492,7 +492,7 @@ namespace Penumbra.UI private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( float )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( float )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].RspIdentifier; var val = list[ manipIdx ].RspValue; @@ -694,7 +694,7 @@ namespace Penumbra.UI && newManip != null && list.All( m => m.Identifier != newManip.Value.Identifier ) ) { - var def = Service< MetaDefaults >.Get().GetDefaultValue( newManip.Value ); + var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); if( def != null ) { var manip = newManip.Value.Type switch diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 8be04f51..10b85e3d 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -50,7 +50,6 @@ public partial class SettingsInterface private readonly SettingsInterface _base; private readonly Selector _selector; - private readonly ModManager _modManager; private readonly HashSet< string > _newMods; public readonly PluginDetails Details; @@ -68,7 +67,6 @@ public partial class SettingsInterface _newMods = newMods; Details = new PluginDetails( _base, _selector ); _currentWebsite = Meta?.Website ?? ""; - _modManager = Service< ModManager >.Get(); } private Mod.Mod? Mod @@ -79,11 +77,12 @@ public partial class SettingsInterface private void DrawName() { - var name = Meta!.Name; - if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) + var name = Meta!.Name; + var modManager = Penumbra.ModManager; + if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) { _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - if( !_modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) + if( !modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) { Mod.Data.Rename( name ); } @@ -286,7 +285,7 @@ public partial class SettingsInterface { ImGui.OpenPopup( LabelOverWriteDir ); } - else if( _modManager.RenameModFolder( Mod.Data, newDir ) ) + else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) { _selector.ReloadCurrentMod(); ImGui.CloseCurrentPopup(); @@ -301,12 +300,12 @@ public partial class SettingsInterface if( sourceUri.Equals( targetUri ) ) { var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); - if( _modManager.RenameModFolder( Mod.Data, tmpFolder ) ) + if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) ) { - if( !_modManager.RenameModFolder( Mod.Data, newDir ) ) + if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) { PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); - _modManager.RenameModFolder( Mod.Data, dir ); + Penumbra.ModManager.RenameModFolder( Mod.Data, dir ); } _selector.ReloadCurrentMod(); @@ -364,17 +363,14 @@ public partial class SettingsInterface ImGui.Text( $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( "Yes", buttonSize ) ) + if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) ) { - if( MergeFolderInto( dir, newDir ) ) - { - Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); + Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false ); - _selector.SelectModOnUpdate( _newName ); + _selector.SelectModOnUpdate( _newName ); - closeParent = true; - ImGui.CloseCurrentPopup(); - } + closeParent = true; + ImGui.CloseCurrentPopup(); } ImGui.SameLine(); @@ -580,7 +576,7 @@ public partial class SettingsInterface DrawMaterialChangeRow(); - DrawSortOrder( Mod!.Data, _modManager, _selector ); + DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index dbecb883..a435a6f2 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -101,7 +101,7 @@ public partial class SettingsInterface ImGui.CloseCurrentPopup(); var mod = Mod; Cache.RemoveMod( mod ); - _modManager.DeleteMod( mod.Data.BasePath ); + Penumbra.ModManager.DeleteMod( mod.Data.BasePath ); ModFileSystem.InvokeChange(); ClearSelection(); } @@ -166,7 +166,7 @@ public partial class SettingsInterface var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); modMeta.SaveToFile( metaFile ); - _modManager.AddMod( newDir ); + Penumbra.ModManager.AddMod( newDir ); ModFileSystem.InvokeChange(); SelectModOnUpdate( newDir.Name ); } @@ -464,7 +464,7 @@ public partial class SettingsInterface return; } - if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) + if( _index >= 0 && Penumbra.ModManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) { SelectModOnUpdate( Mod.Data.BasePath.Name ); _base._menu.InstalledTab.ModPanel.Details.ResetState(); @@ -526,11 +526,11 @@ public partial class SettingsInterface } Cache.TriggerFilterReset(); - var collection = _modManager.Collections.CurrentCollection; + var collection = Penumbra.ModManager.Collections.CurrentCollection; if( collection.Cache != null ) { - collection.CalculateEffectiveFileList( _modManager.TempPath, metaManips, - collection == _modManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( Penumbra.ModManager.TempPath, metaManips, + collection == Penumbra.ModManager.Collections.ActiveCollection ); } collection.Save(); @@ -597,7 +597,6 @@ public partial class SettingsInterface private partial class Selector { private readonly SettingsInterface _base; - private readonly ModManager _modManager; public readonly ModListCache Cache; private float _selectorScalingFactor = 1; @@ -605,14 +604,13 @@ public partial class SettingsInterface public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) { _base = ui; - _modManager = Service< ModManager >.Get(); - Cache = new ModListCache( _modManager, newMods ); + Cache = new ModListCache( Penumbra.ModManager, newMods ); } private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) { if( collection == ModCollection.Empty - || collection == _modManager.Collections.CurrentCollection ) + || collection == Penumbra.ModManager.Collections.CurrentCollection ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); @@ -641,10 +639,10 @@ public partial class SettingsInterface - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2, 5f ); ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, _modManager.Collections.DefaultCollection ); + DrawCollectionButton( "Default", "default", buttonSize, Penumbra.ModManager.Collections.DefaultCollection ); ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, _modManager.Collections.ForcedCollection ); + DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.ModManager.Collections.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize ); @@ -655,7 +653,7 @@ public partial class SettingsInterface private void DrawFolderContent( ModFolder folder, ref int idx ) { // Collection may be manipulated. - foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) + foreach( var item in folder.GetItems( Penumbra.ModManager.Config.SortFoldersFirst ).ToArray() ) { if( item is ModFolder sub ) { @@ -785,7 +783,7 @@ public partial class SettingsInterface style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); var modIndex = 0; - DrawFolderContent( _modManager.StructuredMods, ref modIndex ); + DrawFolderContent( Penumbra.ModManager.StructuredMods, ref modIndex ); style.Pop(); } diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 39f89602..f46ad018 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -1,9 +1,12 @@ +using System; +using System.Linq; using System.Numerics; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImGuiNET; +using Penumbra.Interop; using Penumbra.UI.Custom; namespace Penumbra.UI; @@ -21,16 +24,22 @@ public partial class SettingsInterface : $"({type:X8}) {( char )byte1}{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug"; } - private unsafe void DrawResourceMap( string label, StdMap< uint, Pointer< ResourceHandle > >* typeMap ) + private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) { - if( typeMap == null || !ImGui.TreeNodeEx( label ) ) + if( map == null ) + { + return; + } + + var label = GetNodeLabel( ( uint )category, ext, map->Count ); + if( !ImGui.TreeNodeEx( label ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - if( typeMap->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + if( map->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) { return; } @@ -44,21 +53,19 @@ public partial class SettingsInterface ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale ); ImGui.TableHeadersRow(); - var node = typeMap->SmallestValue; - while( !node->IsNil ) + ResourceLoader.IterateResourceMap( map, ( hash, r ) => { - ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.Text( $"0x{node->KeyValuePair.Item1:X8}" ); + ImGui.Text( $"0x{hash:X8}" ); ImGui.TableNextColumn(); - var address = $"0x{( ulong )node->KeyValuePair.Item2.Value:X}"; + var address = $"0x{( ulong )r:X}"; ImGui.Text( address ); if( ImGui.IsItemClicked() ) { ImGui.SetClipboardText( address ); } - ref var name = ref node->KeyValuePair.Item2.Value->FileName; + ref var name = ref r->FileName; ImGui.TableNextColumn(); if( name.Capacity > 15 ) { @@ -72,30 +79,73 @@ public partial class SettingsInterface } } - //ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); + if( ImGui.IsItemClicked() ) + { + var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData(); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); + } + ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); - node = node->Next(); - } + ImGui.Text( r->RefCount.ToString() ); + } ); } - private unsafe void DrawCategoryContainer( ResourceCategory category, ResourceGraph.CategoryContainer container ) + private unsafe void DrawCategoryContainer( ResourceCategory category, + StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) { - var map = container.MainMap; if( map == null || !ImGui.TreeNodeEx( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}Debug" ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + ResourceLoader.IterateExtMap( map, ( ext, map ) => DrawResourceMap( category, ext, map ) ); + } - var node = map->SmallestValue; - while( !node->IsNil ) + + private static unsafe void DrawResourceProblems() + { + if( !ImGui.CollapsingHeader( "Resource Problems##ResourceManager" ) + || !ImGui.BeginTable( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) { - DrawResourceMap( GetNodeLabel( ( uint )category, node->KeyValuePair.Item1, node->KeyValuePair.Item2.Value->Count ), - node->KeyValuePair.Item2.Value ); - node = node->Next(); + return; } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + ResourceLoader.IterateResources( ( _, r ) => + { + if( r->RefCount < 10000 ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.Text( r->Category.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( r->FileType.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->Id.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->RefCount.ToString() ); + ImGui.TableNextColumn(); + ref var name = ref r->FileName; + if( name.Capacity > 15 ) + { + ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + } + else + { + fixed( byte* ptr = name.Buffer ) + { + ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + } + } + } ); } private unsafe void DrawResourceManagerTab() @@ -107,7 +157,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )Dalamud.SigScanner.GetStaticAddressFromSig( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0" ); + var resourceHandler = *ResourceLoader.ResourceManager; if( resourceHandler == null ) { @@ -120,20 +170,6 @@ public partial class SettingsInterface return; } - DrawCategoryContainer( ResourceCategory.Common, resourceHandler->ResourceGraph->CommonContainer ); - DrawCategoryContainer( ResourceCategory.BgCommon, resourceHandler->ResourceGraph->BgCommonContainer ); - DrawCategoryContainer( ResourceCategory.Bg, resourceHandler->ResourceGraph->BgContainer ); - DrawCategoryContainer( ResourceCategory.Cut, resourceHandler->ResourceGraph->CutContainer ); - DrawCategoryContainer( ResourceCategory.Chara, resourceHandler->ResourceGraph->CharaContainer ); - DrawCategoryContainer( ResourceCategory.Shader, resourceHandler->ResourceGraph->ShaderContainer ); - DrawCategoryContainer( ResourceCategory.Ui, resourceHandler->ResourceGraph->UiContainer ); - DrawCategoryContainer( ResourceCategory.Sound, resourceHandler->ResourceGraph->SoundContainer ); - DrawCategoryContainer( ResourceCategory.Vfx, resourceHandler->ResourceGraph->VfxContainer ); - DrawCategoryContainer( ResourceCategory.UiScript, resourceHandler->ResourceGraph->UiScriptContainer ); - DrawCategoryContainer( ResourceCategory.Exd, resourceHandler->ResourceGraph->ExdContainer ); - DrawCategoryContainer( ResourceCategory.GameScript, resourceHandler->ResourceGraph->GameScriptContainer ); - DrawCategoryContainer( ResourceCategory.Music, resourceHandler->ResourceGraph->MusicContainer ); - DrawCategoryContainer( ResourceCategory.SqpackTest, resourceHandler->ResourceGraph->SqpackTestContainer ); - DrawCategoryContainer( ResourceCategory.Debug, resourceHandler->ResourceGraph->DebugContainer ); + ResourceLoader.IterateGraphs( DrawCategoryContainer ); } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 112c8136..884a99fa 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -3,12 +3,12 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; -using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Logging; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.Interop; +using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -71,7 +71,8 @@ public partial class SettingsInterface + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); ImGui.SameLine(); - DrawOpenDirectoryButton( 0, _base._modManager.BasePath, _base._modManager.Valid ); + var modManager = Penumbra.ModManager; + DrawOpenDirectoryButton( 0, modManager.BasePath, modManager.Valid ); ImGui.EndGroup(); if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) @@ -82,7 +83,7 @@ public partial class SettingsInterface if( save || DrawPressEnterWarning( _config.ModDirectory ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods( _newModDirectory ); + modManager.DiscoverMods( _newModDirectory ); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); _newModDirectory = _config.ModDirectory; } @@ -99,7 +100,8 @@ public partial class SettingsInterface + "A directory 'penumbrametatmp' will be created as a sub-directory to the specified directory.\n" + "If none is specified (i.e. this is blank) this directory will be created in the root directory instead.\n" ); ImGui.SameLine(); - DrawOpenDirectoryButton( 1, _base._modManager.TempPath, _base._modManager.TempWritable ); + var modManager = Penumbra.ModManager; + DrawOpenDirectoryButton( 1, modManager.TempPath, modManager.TempWritable ); ImGui.EndGroup(); if( _newTempDirectory == _config.TempDirectory ) @@ -109,7 +111,7 @@ public partial class SettingsInterface if( save || DrawPressEnterWarning( _config.TempDirectory ) ) { - _base._modManager.SetTempDirectory( _newTempDirectory ); + modManager.SetTempDirectory( _newTempDirectory ); _newTempDirectory = _config.TempDirectory; } } @@ -119,7 +121,7 @@ public partial class SettingsInterface if( ImGui.Button( "Rediscover Mods" ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods(); + Penumbra.ModManager.DiscoverMods(); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); } @@ -208,26 +210,26 @@ public partial class SettingsInterface private void DrawLogLoadedFilesBox() { - ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); - ImGui.SameLine(); - var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; - var tmp = regex; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) - { - try - { - var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; - _base._penumbra.ResourceLoader.LogFileFilter = newRegex; - } - catch( Exception e ) - { - PluginLog.Debug( "Could not create regex:\n{Exception}", e ); - } - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); + //ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); + //ImGui.SameLine(); + //var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; + //var tmp = regex; + //ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + //if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) + //{ + // try + // { + // var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; + // _base._penumbra.ResourceLoader.LogFileFilter = newRegex; + // } + // catch( Exception e ) + // { + // PluginLog.Debug( "Could not create regex:\n{Exception}", e ); + // } + //} + // + //ImGui.SameLine(); + //ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); } private void DrawDisableNotificationsBox() @@ -307,7 +309,7 @@ public partial class SettingsInterface { if( ImGui.Button( "Reload Resident Resources" ) ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } ImGui.SameLine(); @@ -325,6 +327,11 @@ public partial class SettingsInterface DrawReloadResourceButton(); } + public static unsafe void Text( Utf8String s ) + { + ImGuiNative.igTextUnformatted( ( byte* )s.Path, ( byte* )s.Path + s.Length ); + } + public void Draw() { if( !ImGui.BeginTabItem( "Settings" ) ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 20eaff02..7f0f74d6 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -1,7 +1,5 @@ using System; using System.Numerics; -using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI; @@ -15,17 +13,13 @@ public partial class SettingsInterface : IDisposable private readonly Penumbra _penumbra; private readonly ManageModsButton _manageModsButton; - private readonly MenuBar _menuBar; private readonly SettingsMenu _menu; - private readonly ModManager _modManager; public SettingsInterface( Penumbra penumbra ) { _penumbra = penumbra; _manageModsButton = new ManageModsButton( this ); - _menuBar = new MenuBar( this ); _menu = new SettingsMenu( this ); - _modManager = Service< ModManager >.Get(); Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.Draw += Draw; @@ -51,31 +45,31 @@ public partial class SettingsInterface : IDisposable public void Draw() { - _menuBar.Draw(); _menu.Draw(); } private void ReloadMods() { _menu.InstalledTab.Selector.ClearSelection(); - _modManager.DiscoverMods( Penumbra.Config.ModDirectory ); + Penumbra.ModManager.DiscoverMods( Penumbra.Config.ModDirectory ); _menu.InstalledTab.Selector.Cache.TriggerListReset(); } private void SaveCurrentCollection( bool recalculateMeta ) { - var current = _modManager.Collections.CurrentCollection; + var current = Penumbra.ModManager.Collections.CurrentCollection; current.Save(); RecalculateCurrent( recalculateMeta ); } private void RecalculateCurrent( bool recalculateMeta ) { - var current = _modManager.Collections.CurrentCollection; + var modManager = Penumbra.ModManager; + var current = modManager.Collections.CurrentCollection; if( current.Cache != null ) { - current.CalculateEffectiveFileList( _modManager.TempPath, recalculateMeta, - current == _modManager.Collections.ActiveCollection ); + current.CalculateEffectiveFileList( modManager.TempPath, recalculateMeta, + current == modManager.Collections.ActiveCollection ); _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } } diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index 12c07617..1bb8628e 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -74,7 +74,7 @@ public partial class SettingsInterface CollectionsTab.Draw(); _importTab.Draw(); - if( Service< ModManager >.Get().Valid && !_importTab.IsImporting() ) + if( Penumbra.ModManager.Valid && !_importTab.IsImporting() ) { _browserTab.Draw(); InstalledTab.Draw(); diff --git a/Penumbra/Util/FullPath.cs b/Penumbra/Util/FullPath.cs deleted file mode 100644 index 829bc5bb..00000000 --- a/Penumbra/Util/FullPath.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.IO; -using Penumbra.GameData.Util; - -namespace Penumbra.Util; - -public readonly struct FullPath : IComparable, IEquatable< FullPath > -{ - public readonly string FullName; - public readonly string InternalName; - public readonly ulong Crc64; - - public FullPath( DirectoryInfo baseDir, RelPath relPath ) - { - FullName = Path.Combine( baseDir.FullName, relPath ); - InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); - Crc64 = ComputeCrc64( InternalName ); - } - - public FullPath( FileInfo file ) - { - FullName = file.FullName; - InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); - Crc64 = ComputeCrc64( InternalName ); - } - - public bool Exists - => File.Exists( FullName ); - - public string Extension - => Path.GetExtension( FullName ); - - public string Name - => Path.GetFileName( FullName ); - - public GamePath ToGamePath( DirectoryInfo dir ) - => FullName.StartsWith(dir.FullName) ? GamePath.GenerateUnchecked( InternalName[(dir.FullName.Length+1)..]) : GamePath.GenerateUnchecked( string.Empty ); - - private static ulong ComputeCrc64( string name ) - { - if( name.Length == 0 ) - { - return 0; - } - - var lastSlash = name.LastIndexOf( '/' ); - if( lastSlash == -1 ) - { - return Lumina.Misc.Crc32.Get( name ); - } - - var folder = name[ ..lastSlash ]; - var file = name[ ( lastSlash + 1 ).. ]; - return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); - } - - public int CompareTo( object? obj ) - => obj switch - { - FullPath p => string.Compare( InternalName, p.InternalName, StringComparison.InvariantCulture ), - FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), - _ => -1, - }; - - public bool Equals( FullPath other ) - { - if( Crc64 != other.Crc64 ) - { - return false; - } - - if( FullName.Length == 0 || other.FullName.Length == 0 ) - { - return true; - } - - return InternalName.Equals( other.InternalName ); - } - - public override int GetHashCode() - => Crc64.GetHashCode(); - - public override string ToString() - => FullName; -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 8cb65a70..53429d70 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; using Penumbra.Mod; diff --git a/Penumbra/Util/RelPath.cs b/Penumbra/Util/RelPath.cs index 6d6ea369..f4ce0021 100644 --- a/Penumbra/Util/RelPath.cs +++ b/Penumbra/Util/RelPath.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; namespace Penumbra.Util; diff --git a/Penumbra/Util/Service.cs b/Penumbra/Util/Service.cs deleted file mode 100644 index 5391db22..00000000 --- a/Penumbra/Util/Service.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace Penumbra.Util -{ - /// - /// Basic service locator - /// - /// The class you want to store in the service locator - public static class Service< T > where T : class - { - private static T? _object; - - public static void Set( T obj ) - { - // ReSharper disable once JoinNullCheckWithUsage - if( obj == null ) - { - throw new ArgumentNullException( $"{nameof( obj )} is null!" ); - } - - _object = obj; - } - - public static T Set() - { - _object = Activator.CreateInstance< T >(); - - return _object; - } - - public static T Set( params object[] args ) - { - var obj = ( T? )Activator.CreateInstance( typeof( T ), args ); - - // ReSharper disable once JoinNullCheckWithUsage - if( obj == null ) - { - throw new Exception( "what he fuc" ); - } - - _object = obj; - - return obj; - } - - public static T Get() - { - if( _object == null ) - { - throw new InvalidOperationException( $"{nameof( T )} hasn't been registered!" ); - } - - return _object; - } - } -} \ No newline at end of file From 5d77cd55149dd61c00a631c7499fea95815a5e1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Mar 2022 14:14:42 +0100 Subject: [PATCH 0089/2451] Add function to replace all skin materials. --- Penumbra/Util/ModelChanger.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 53429d70..de6d9ec8 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; @@ -12,7 +13,9 @@ namespace Penumbra.Util; public static class ModelChanger { - public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; + public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; + public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); + public static bool ValidStrings( string from, string to ) => from.Length != 0 @@ -39,15 +42,20 @@ public static class ModelChanger { var data = File.ReadAllBytes( file.FullName ); var mdlFile = new MdlFile( data ); - from = string.Format( MaterialFormat, from ); - to = string.Format( MaterialFormat, to ); + Func< string, bool > compare = MaterialRegex.IsMatch; + if( from.Length > 0 ) + { + from = string.Format( MaterialFormat, from ); + compare = s => s == from; + } + to = string.Format( MaterialFormat, to ); var replaced = 0; for( var i = 0; i < mdlFile.Materials.Length; ++i ) { - if( mdlFile.Materials[ i ] == @from ) + if( compare(mdlFile.Materials[i]) ) { - mdlFile.Materials[ i ] = to; + mdlFile.Materials[i] = to; ++replaced; } } From f5fccb0235a333f882b7f8fbf64654e0582252e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Mar 2022 16:45:16 +0100 Subject: [PATCH 0090/2451] Change most things to new byte strings, introduce new ResourceLoader and Logger fully. --- Penumbra.GameData/ByteString/FullPath.cs | 47 +- .../{NewGamePath.cs => Utf8GamePath.cs} | 47 +- .../{NewRelPath.cs => Utf8RelPath.cs} | 43 +- .../ByteString/Utf8String.Comparison.cs | 13 + Penumbra/Api/ModsController.cs | 2 +- Penumbra/Api/PenumbraApi.cs | 8 +- Penumbra/Configuration.cs | 10 +- Penumbra/Dalamud.cs | 14 +- Penumbra/Importer/Models/ExtendedModPack.cs | 2 +- Penumbra/Importer/TexToolsImport.cs | 10 +- Penumbra/Interop/ResourceLoader.Debug.cs | 16 +- .../Interop/ResourceLoader.Replacement.cs | 15 +- Penumbra/Interop/ResourceLoader.TexMdl.cs | 4 +- Penumbra/Interop/ResourceLoader.cs | 11 +- Penumbra/Interop/ResourceLogger.cs | 98 ++ Penumbra/Interop/Structs/SeFileDescriptor.cs | 1 - Penumbra/Meta/MetaCollection.cs | 9 +- Penumbra/Meta/MetaManager.cs | 27 +- Penumbra/MigrateConfiguration.cs | 119 ++- Penumbra/Mod/GroupInformation.cs | 102 +++ Penumbra/Mod/Mod.cs | 54 +- Penumbra/Mod/ModCache.cs | 91 +- Penumbra/Mod/ModCleanup.cs | 856 +++++++++--------- Penumbra/Mod/ModData.cs | 239 +++-- Penumbra/Mod/ModFunctions.cs | 145 ++- Penumbra/Mod/ModMeta.cs | 193 ++-- Penumbra/Mod/ModSettings.cs | 108 ++- Penumbra/Mod/NamedModSettings.cs | 60 +- Penumbra/Mods/CollectionManager.cs | 21 +- Penumbra/Mods/ModCollection.cs | 397 ++++---- Penumbra/Mods/ModCollectionCache.cs | 110 +-- Penumbra/Mods/ModManager.cs | 11 +- Penumbra/Mods/ModManagerEditExtensions.cs | 1 - Penumbra/Penumbra.cs | 50 +- Penumbra/Structs/GroupInformation.cs | 103 --- Penumbra/UI/Custom/ImGuiUtil.cs | 15 +- Penumbra/UI/MenuTabs/TabEffective.cs | 133 ++- .../TabInstalled/TabInstalledDetails.cs | 45 +- .../TabInstalled/TabInstalledDetailsEdit.cs | 650 ++++++------- .../TabInstalled/TabInstalledSelector.cs | 4 +- Penumbra/UI/MenuTabs/TabSettings.cs | 74 +- Penumbra/UI/SettingsMenu.cs | 2 - Penumbra/UI/UiHelpers.cs | 47 +- Penumbra/Util/ArrayExtensions.cs | 86 +- Penumbra/Util/BinaryReaderExtensions.cs | 165 +--- Penumbra/Util/ChatUtil.cs | 46 +- Penumbra/Util/Crc32.cs | 57 -- Penumbra/Util/DialogExtensions.cs | 125 ++- Penumbra/Util/GeneralUtil.cs | 14 - Penumbra/Util/MemoryStreamExtensions.cs | 12 - Penumbra/Util/ModelChanger.cs | 9 +- Penumbra/Util/PenumbraSqPackStream.cs | 699 +++++++------- Penumbra/Util/SingleOrArrayConverter.cs | 55 +- Penumbra/Util/StringPathExtensions.cs | 93 +- Penumbra/Util/TempFile.cs | 43 +- 55 files changed, 2681 insertions(+), 2730 deletions(-) rename Penumbra.GameData/ByteString/{NewGamePath.cs => Utf8GamePath.cs} (72%) rename Penumbra.GameData/ByteString/{NewRelPath.cs => Utf8RelPath.cs} (72%) create mode 100644 Penumbra/Interop/ResourceLogger.cs create mode 100644 Penumbra/Mod/GroupInformation.cs delete mode 100644 Penumbra/Structs/GroupInformation.cs delete mode 100644 Penumbra/Util/Crc32.cs delete mode 100644 Penumbra/Util/GeneralUtil.cs delete mode 100644 Penumbra/Util/MemoryStreamExtensions.cs diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index f9c679dc..2b3ffe23 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -1,17 +1,21 @@ using System; using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Penumbra.GameData.Util; namespace Penumbra.GameData.ByteString; +[JsonConverter( typeof( FullPathConverter ) )] public readonly struct FullPath : IComparable, IEquatable< FullPath > { public readonly string FullName; public readonly Utf8String InternalName; public readonly ulong Crc64; + public static readonly FullPath Empty = new(string.Empty); - public FullPath( DirectoryInfo baseDir, NewRelPath relPath ) + public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath ) : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) { } @@ -19,10 +23,11 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > : this( file.FullName ) { } + public FullPath( string s ) { FullName = s; - InternalName = Utf8String.FromString( FullName, out var name, true ) ? name : Utf8String.Empty; + InternalName = Utf8String.FromString( FullName, out var name, true ) ? name.Replace( ( byte )'\\', ( byte )'/' ) : Utf8String.Empty; Crc64 = Functions.ComputeCrc64( InternalName.Span ); } @@ -35,9 +40,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > public string Name => Path.GetFileName( FullName ); - public bool ToGamePath( DirectoryInfo dir, out NewGamePath path ) + public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path ) { - path = NewGamePath.Empty; + path = Utf8GamePath.Empty; if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) { return false; @@ -45,13 +50,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > var substring = InternalName.Substring( dir.FullName.Length + 1 ); - path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) ); + path = new Utf8GamePath( substring ); return true; } - public bool ToRelPath( DirectoryInfo dir, out NewRelPath path ) + public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path ) { - path = NewRelPath.Empty; + path = Utf8RelPath.Empty; if( !FullName.StartsWith( dir.FullName ) ) { return false; @@ -59,7 +64,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > var substring = InternalName.Substring( dir.FullName.Length + 1 ); - path = new NewRelPath( substring ); + path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) ); return true; } @@ -88,9 +93,35 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > return InternalName.Equals( other.InternalName ); } + public bool IsRooted + => new Utf8GamePath( InternalName ).IsRooted(); + public override int GetHashCode() => InternalName.Crc32; public override string ToString() => FullName; + + public class FullPathConverter : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( FullPath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ).ToString(); + return new FullPath( token ); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value is FullPath p ) + { + serializer.Serialize( writer, p.ToString() ); + } + } + } } \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewGamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs similarity index 72% rename from Penumbra.GameData/ByteString/NewGamePath.cs rename to Penumbra.GameData/ByteString/Utf8GamePath.cs index 1685f15a..b0d1778f 100644 --- a/Penumbra.GameData/ByteString/NewGamePath.cs +++ b/Penumbra.GameData/ByteString/Utf8GamePath.cs @@ -3,20 +3,21 @@ using System.IO; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Util; namespace Penumbra.GameData.ByteString; // NewGamePath wrap some additional validity checking around Utf8String, // provide some filesystem helpers, and conversion to Json. -[JsonConverter( typeof( NewGamePathConverter ) )] -public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable +[JsonConverter( typeof( Utf8GamePathConverter ) )] +public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable { public const int MaxGamePathLength = 256; - public readonly Utf8String Path; - public static readonly NewGamePath Empty = new(Utf8String.Empty); + public readonly Utf8String Path; + public static readonly Utf8GamePath Empty = new(Utf8String.Empty); - internal NewGamePath( Utf8String s ) + internal Utf8GamePath( Utf8String s ) => Path = s; public int Length @@ -25,16 +26,16 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New public bool IsEmpty => Path.IsEmpty; - public NewGamePath ToLower() + public Utf8GamePath ToLower() => new(Path.AsciiToLower()); - public static unsafe bool FromPointer( byte* ptr, out NewGamePath path, bool lower = false ) + public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false ) { var utf = new Utf8String( ptr ); return ReturnChecked( utf, out path, lower ); } - public static bool FromSpan( ReadOnlySpan< byte > data, out NewGamePath path, bool lower = false ) + public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false ) { var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); return ReturnChecked( utf, out path, lower ); @@ -43,7 +44,7 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. // Does not check for initial slashes either, since they are assumed to be by choice. // Checks for maxlength, ASCII and lowercase. - private static bool ReturnChecked( Utf8String utf, out NewGamePath path, bool lower = false ) + private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false ) { path = Empty; if( !utf.IsAscii || utf.Length > MaxGamePathLength ) @@ -51,14 +52,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New return false; } - path = new NewGamePath( lower ? utf.AsciiToLower() : utf ); + path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf ); return true; } - public NewGamePath Clone() + public Utf8GamePath Clone() => new(Path.Clone()); - public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) + public static explicit operator Utf8GamePath( string s ) + => FromString( s, out var p, true ) ? p : Empty; + + public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false ) { path = Empty; if( s.IsNullOrEmpty() ) @@ -83,11 +87,11 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New return false; } - path = new NewGamePath( ascii ); + path = new Utf8GamePath( ascii ); return true; } - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false ) { path = Empty; if( !file.FullName.StartsWith( baseDir.FullName ) ) @@ -111,13 +115,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); } - public bool Equals( NewGamePath other ) + public bool Equals( Utf8GamePath other ) => Path.Equals( other.Path ); public override int GetHashCode() => Path.GetHashCode(); - public int CompareTo( NewGamePath other ) + public int CompareTo( Utf8GamePath other ) => Path.CompareTo( other.Path ); public override string ToString() @@ -132,17 +136,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New && ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' ) && Path[ 1 ] == ':'; - private class NewGamePathConverter : JsonConverter + public class Utf8GamePathConverter : JsonConverter { public override bool CanConvert( Type objectType ) - => objectType == typeof( NewGamePath ); + => objectType == typeof( Utf8GamePath ); public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) { var token = JToken.Load( reader ).ToString(); return FromString( token, out var p, true ) ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewGamePath )}." ); + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." ); } public override bool CanWrite @@ -150,10 +154,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) { - if( value is NewGamePath p ) + if( value is Utf8GamePath p ) { serializer.Serialize( writer, p.ToString() ); } } } + + public GamePath ToGamePath() + => GamePath.GenerateUnchecked( ToString() ); } \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewRelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs similarity index 72% rename from Penumbra.GameData/ByteString/NewRelPath.cs rename to Penumbra.GameData/ByteString/Utf8RelPath.cs index 25b6f9e0..5fd79ef7 100644 --- a/Penumbra.GameData/ByteString/NewRelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -1,23 +1,28 @@ using System; using System.IO; using Dalamud.Utility; +using Microsoft.VisualBasic.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Penumbra.GameData.ByteString; -[JsonConverter( typeof( NewRelPathConverter ) )] -public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable +[JsonConverter( typeof( Utf8RelPathConverter ) )] +public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable { public const int MaxRelPathLength = 250; - public readonly Utf8String Path; - public static readonly NewRelPath Empty = new(Utf8String.Empty); + public readonly Utf8String Path; + public static readonly Utf8RelPath Empty = new(Utf8String.Empty); - internal NewRelPath( Utf8String path ) + internal Utf8RelPath( Utf8String path ) => Path = path; - public static bool FromString( string? s, out NewRelPath path ) + + public static explicit operator Utf8RelPath( string s ) + => FromString( s, out var p ) ? p : Empty; + + public static bool FromString( string? s, out Utf8RelPath path ) { path = Empty; if( s.IsNullOrEmpty() ) @@ -42,11 +47,11 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe return false; } - path = new NewRelPath( ascii ); + path = new Utf8RelPath( ascii ); return true; } - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewRelPath path ) + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path ) { path = Empty; if( !file.FullName.StartsWith( baseDir.FullName ) ) @@ -58,7 +63,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe return FromString( substring, out path ); } - public static bool FromFile( FullPath file, DirectoryInfo baseDir, out NewRelPath path ) + public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path ) { path = Empty; if( !file.FullName.StartsWith( baseDir.FullName ) ) @@ -70,10 +75,10 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe return FromString( substring, out path ); } - public NewRelPath( NewGamePath gamePath ) + public Utf8RelPath( Utf8GamePath gamePath ) => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); - public unsafe NewGamePath ToGamePath( int skipFolders = 0 ) + public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 ) { var idx = 0; while( skipFolders > 0 ) @@ -82,7 +87,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe --skipFolders; if( idx <= 0 ) { - return NewGamePath.Empty; + return Utf8GamePath.Empty; } } @@ -91,13 +96,13 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); - return new NewGamePath( utf ); + return new Utf8GamePath( utf ); } - public int CompareTo( NewRelPath rhs ) + public int CompareTo( Utf8RelPath rhs ) => Path.CompareTo( rhs.Path ); - public bool Equals( NewRelPath other ) + public bool Equals( Utf8RelPath other ) => Path.Equals( other.Path ); public override string ToString() @@ -106,17 +111,17 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe public void Dispose() => Path.Dispose(); - private class NewRelPathConverter : JsonConverter + public class Utf8RelPathConverter : JsonConverter { public override bool CanConvert( Type objectType ) - => objectType == typeof( NewRelPath ); + => objectType == typeof( Utf8RelPath ); public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) { var token = JToken.Load( reader ).ToString(); return FromString( token, out var p ) ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewRelPath )}." ); + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." ); } public override bool CanWrite @@ -124,7 +129,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) { - if( value is NewRelPath p ) + if( value is Utf8RelPath p ) { serializer.Serialize( writer, p.ToString() ); } diff --git a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs index c2ca9488..6d96dce5 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs @@ -75,6 +75,19 @@ public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, ICompa return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); } + public bool StartsWith( Utf8String other ) + { + var otherLength = other.Length; + return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength ); + } + + public bool EndsWith( Utf8String other ) + { + var otherLength = other.Length; + var offset = Length - otherLength; + return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength ); + } + public bool StartsWith( params char[] chars ) { if( chars.Length > Length ) diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 1f03dfc5..d4fc8b70 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -36,7 +36,7 @@ public class ModsController : WebApiController public object GetFiles() { return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( - o => ( string )o.Key, + o => o.Key.ToString(), o => o.Value.FullName ) ?? new Dictionary< string, string >(); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 39bcbd35..ef9e8e26 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -5,6 +5,7 @@ using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Mods; @@ -78,16 +79,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi private static string ResolvePath( string path, ModManager manager, ModCollection collection ) { - if( !Penumbra.Config.IsEnabled ) + if( !Penumbra.Config.EnableMods ) { return path; } - var gamePath = new GamePath( path ); + var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= path; - return ret; + return ret?.ToString() ?? path; } public string ResolvePath( string path ) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8d360422..a59ab245 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Numerics; using Dalamud.Configuration; using Dalamud.Logging; @@ -14,13 +12,19 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = CurrentVersion; - public bool IsEnabled { get; set; } = true; + public bool EnableMods { get; set; } = true; #if DEBUG public bool DebugMode { get; set; } = true; #else public bool DebugMode { get; set; } = false; #endif + + public bool EnableFullResourceLogging { get; set; } = false; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool ScaleModSelector { get; set; } = false; + public bool ShowAdvanced { get; set; } public bool DisableFileSystemNotifications { get; set; } diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index d914765e..aee12c85 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -8,14 +8,15 @@ using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.Plugin; + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local -namespace Penumbra +namespace Penumbra; + +public class Dalamud { - public class Dalamud - { - public static void Initialize(DalamudPluginInterface pluginInterface) - => pluginInterface.Create(); + public static void Initialize( DalamudPluginInterface pluginInterface ) + => pluginInterface.Create< Dalamud >(); // @formatter:off [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; @@ -29,6 +30,5 @@ namespace Penumbra [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - // @formatter:on - } + // @formatter:on } \ No newline at end of file diff --git a/Penumbra/Importer/Models/ExtendedModPack.cs b/Penumbra/Importer/Models/ExtendedModPack.cs index 91bb5d01..c499ece3 100644 --- a/Penumbra/Importer/Models/ExtendedModPack.cs +++ b/Penumbra/Importer/Models/ExtendedModPack.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Penumbra.Structs; +using Penumbra.Mod; namespace Penumbra.Importer.Models { diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 44329dc6..aac92447 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -6,10 +6,10 @@ using System.Text; using Dalamud.Logging; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Importer.Models; using Penumbra.Mod; -using Penumbra.Structs; using Penumbra.Util; using FileMode = System.IO.FileMode; @@ -336,14 +336,18 @@ internal class TexToolsImport { OptionName = opt.Name!, OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, - OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), + OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), }; var optDir = NewOptionDirectory( groupFolder, opt.Name! ); if( optDir.Exists ) { foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) { - option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) ); + if( Utf8RelPath.FromFile( file, baseFolder, out var rel ) + && Utf8GamePath.FromFile( file, optDir, out var game, true ) ) + { + option.AddFile( rel, game ); + } } } diff --git a/Penumbra/Interop/ResourceLoader.Debug.cs b/Penumbra/Interop/ResourceLoader.Debug.cs index dcfd7f97..06f819f6 100644 --- a/Penumbra/Interop/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/ResourceLoader.Debug.cs @@ -21,7 +21,7 @@ public unsafe partial class ResourceLoader { public ResourceHandle* OriginalResource; public ResourceHandle* ManipulatedResource; - public NewGamePath OriginalPath; + public Utf8GamePath OriginalPath; public FullPath ManipulatedPath; public ResourceCategory Category; public object? ResolverInfo; @@ -44,7 +44,7 @@ public unsafe partial class ResourceLoader ResourceLoaded -= AddModifiedDebugInfo; } - private void AddModifiedDebugInfo( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) + private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) { if( manipulatedPath == null ) { @@ -188,11 +188,11 @@ public unsafe partial class ResourceLoader } } - // Logging functions for EnableLogging. - private static void LogPath( NewGamePath path, bool synchronous ) - => PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); + // Logging functions for EnableFullLogging. + private static void LogPath( Utf8GamePath path, bool synchronous ) + => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( ResourceHandle* handle, NewGamePath path, FullPath? manipulatedPath, object? _ ) + private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); @@ -200,6 +200,6 @@ public unsafe partial class ResourceLoader private static void LogLoadedFile( Utf8String path, bool success, bool custom ) => PluginLog.Information( success - ? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}" - : $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); + ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}" + : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); } \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/ResourceLoader.Replacement.cs index 4792ec3f..07113232 100644 --- a/Penumbra/Interop/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/ResourceLoader.Replacement.cs @@ -46,7 +46,7 @@ public unsafe partial class ResourceLoader [Conditional( "DEBUG" )] - private static void CompareHash( int local, int game, NewGamePath path ) + private static void CompareHash( int local, int game, Utf8GamePath path ) { if( local != game ) { @@ -54,12 +54,12 @@ public unsafe partial class ResourceLoader } } - private event Action< NewGamePath, FullPath?, object? >? PathResolved; + private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) { - if( !NewGamePath.FromPointer( path, out var gamePath ) ) + if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { PluginLog.Error( "Could not create GamePath from resource path." ); return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); @@ -114,7 +114,7 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - var valid = NewGamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); + var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); byte ret; // The internal buffer size does not allow for more than 260 characters. // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. @@ -151,11 +151,10 @@ public unsafe partial class ResourceLoader } // Use the default method of path replacement. - public static (FullPath?, object?) DefaultReplacer( NewGamePath path ) + public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path ) { - var gamePath = new GamePath( path.ToString() ); - var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath ); - return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null ); + var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); + return( resolved, null ); } private void DisposeHooks() diff --git a/Penumbra/Interop/ResourceLoader.TexMdl.cs b/Penumbra/Interop/ResourceLoader.TexMdl.cs index d5616055..544e6ba4 100644 --- a/Penumbra/Interop/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/ResourceLoader.TexMdl.cs @@ -4,8 +4,6 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.Util; namespace Penumbra.Interop; @@ -69,7 +67,7 @@ public unsafe partial class ResourceLoader : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); - private void AddCrc( NewGamePath _, FullPath? path, object? _2 ) + private void AddCrc( Utf8GamePath _, FullPath? path, object? _2 ) { if( path is { Extension: ".mdl" or ".tex" } p ) { diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 562d2cfb..8e86c215 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -17,7 +17,7 @@ public unsafe partial class ResourceLoader : IDisposable // Events can be used to make smarter logging. public bool IsLoggingEnabled { get; private set; } - public void EnableLogging() + public void EnableFullLogging() { if( IsLoggingEnabled ) { @@ -31,7 +31,7 @@ public unsafe partial class ResourceLoader : IDisposable EnableHooks(); } - public void DisableLogging() + public void DisableFullLogging() { if( !IsLoggingEnabled ) { @@ -99,13 +99,13 @@ public unsafe partial class ResourceLoader : IDisposable } // Event fired whenever a resource is requested. - public delegate void ResourceRequestedDelegate( NewGamePath path, bool synchronous ); + public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous ); public event ResourceRequestedDelegate? ResourceRequested; // Event fired whenever a resource is returned. // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. // resolveData is additional data returned by the current ResolvePath function and is user-defined. - public delegate void ResourceLoadedDelegate( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, + public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; @@ -118,10 +118,11 @@ public unsafe partial class ResourceLoader : IDisposable public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. - public Func< NewGamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; + public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; public void Dispose() { + DisableFullLogging(); DisposeHooks(); DisposeTexMdlTreatment(); } diff --git a/Penumbra/Interop/ResourceLogger.cs b/Penumbra/Interop/ResourceLogger.cs new file mode 100644 index 00000000..025b7409 --- /dev/null +++ b/Penumbra/Interop/ResourceLogger.cs @@ -0,0 +1,98 @@ +using System; +using System.Text.RegularExpressions; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Interop; + +// A logger class that contains the relevant data to log requested files via regex. +// Filters are case-insensitive. +public class ResourceLogger : IDisposable +{ + // Enable or disable the logging of resources subject to the current filter. + public void SetState( bool value ) + { + if( value == Penumbra.Config.EnableResourceLogging ) + { + return; + } + + Penumbra.Config.EnableResourceLogging = value; + Penumbra.Config.Save(); + if( value ) + { + _resourceLoader.ResourceRequested += OnResourceRequested; + } + else + { + _resourceLoader.ResourceRequested -= OnResourceRequested; + } + } + + // Set the current filter to a new string, doing all other necessary work. + public void SetFilter( string newFilter ) + { + if( newFilter == Filter ) + { + return; + } + + Penumbra.Config.ResourceLoggingFilter = newFilter; + Penumbra.Config.Save(); + SetupRegex(); + } + + // Returns whether the current filter is a valid regular expression. + public bool ValidRegex + => _filterRegex != null; + + private readonly ResourceLoader _resourceLoader; + private Regex? _filterRegex; + + private static string Filter + => Penumbra.Config.ResourceLoggingFilter; + + private void SetupRegex() + { + try + { + _filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant ); + } + catch + { + _filterRegex = null; + } + } + + public ResourceLogger( ResourceLoader loader ) + { + _resourceLoader = loader; + SetupRegex(); + if( Penumbra.Config.EnableResourceLogging ) + { + _resourceLoader.ResourceRequested += OnResourceRequested; + } + } + + private void OnResourceRequested( Utf8GamePath data, bool synchronous ) + { + var path = Match( data.Path ); + if( path != null ) + { + PluginLog.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); + } + } + + // Returns the converted string if the filter matches, and null otherwise. + // The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it. + private string? Match( Utf8String data ) + { + var s = data.ToString(); + return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.InvariantCultureIgnoreCase ) ) + ? s + : null; + } + + public void Dispose() + => _resourceLoader.ResourceRequested -= OnResourceRequested; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs index c83f3796..4e1a1f57 100644 --- a/Penumbra/Interop/Structs/SeFileDescriptor.cs +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using Penumbra.Structs; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index f6c84358..059999af 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -8,7 +8,6 @@ using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Meta.Files; using Penumbra.Mod; -using Penumbra.Structs; using Penumbra.Util; namespace Penumbra.Meta; @@ -167,14 +166,14 @@ public class MetaCollection continue; } - var path = new RelPath( file, basePath ); + Utf8RelPath.FromFile( file, basePath, out var path ); var foundAny = false; - foreach( var group in modMeta.Groups ) + foreach( var (name, group) in modMeta.Groups ) { - foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) + foreach( var option in group.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) { foundAny = true; - AddMeta( group.Key, option.OptionName, metaData ); + AddMeta( name, option.OptionName, metaData ); } } diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 6ff76118..0fac0dc3 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -6,7 +6,6 @@ using Dalamud.Logging; using Lumina.Data.Files; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Interop; using Penumbra.Meta.Files; using Penumbra.Util; @@ -24,7 +23,7 @@ public class MetaManager : IDisposable public FileInformation( object data ) => Data = data; - public void Write( DirectoryInfo dir, GamePath originalPath ) + public void Write( DirectoryInfo dir, Utf8GamePath originalPath ) { ByteData = Data switch { @@ -44,16 +43,16 @@ public class MetaManager : IDisposable public const string TmpDirectory = "penumbrametatmp"; - private readonly DirectoryInfo _dir; - private readonly Dictionary< GamePath, FullPath > _resolvedFiles; + private readonly DirectoryInfo _dir; + private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles; - private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); - private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); + private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); + private readonly Dictionary< Utf8GamePath, FileInformation > _currentFiles = new(); public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - public IEnumerable< (GamePath, FullPath) > Files + public IEnumerable< (Utf8GamePath, FullPath) > Files => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); @@ -121,7 +120,7 @@ public class MetaManager : IDisposable private void ClearDirectory() => ClearDirectory( _dir ); - public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) + public MetaManager( string name, Dictionary< Utf8GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) { _resolvedFiles = resolvedFiles; _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); @@ -135,13 +134,13 @@ public class MetaManager : IDisposable Directory.CreateDirectory( _dir.FullName ); } - foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) + foreach( var (key, value) in _currentFiles.Where( kvp => kvp.Value.Changed ) ) { - kvp.Value.Write( _dir, kvp.Key ); - _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; - if( kvp.Value.Data is EqpFile ) + value.Write( _dir, key ); + _resolvedFiles[ key ] = value.CurrentFile!.Value; + if( value.Data is EqpFile ) { - EqpData = kvp.Value.ByteData; + EqpData = value.ByteData; } } } @@ -154,7 +153,7 @@ public class MetaManager : IDisposable } _currentManipulations.Add( m, mod ); - var gamePath = m.CorrespondingFilename(); + var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO try { if( !_currentFiles.TryGetValue( gamePath, out var file ) ) diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 71b3646c..2f4fd5a7 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -3,84 +3,81 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; -using Dalamud.Plugin; using Newtonsoft.Json.Linq; using Penumbra.Mod; using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra +namespace Penumbra; + +public static class MigrateConfiguration { - public static class MigrateConfiguration + public static void Version0To1( Configuration config ) { - public static void Version0To1( Configuration config ) + if( config.Version != 0 ) { - if( config.Version != 0 ) - { - return; - } - - config.ModDirectory = config.CurrentCollection; - config.CurrentCollection = "Default"; - config.DefaultCollection = "Default"; - config.Version = 1; - ResettleCollectionJson( config ); + return; } - private static void ResettleCollectionJson( Configuration config ) + config.ModDirectory = config.CurrentCollection; + config.CurrentCollection = "Default"; + config.DefaultCollection = "Default"; + config.Version = 1; + ResettleCollectionJson( config ); + } + + private static void ResettleCollectionJson( Configuration config ) + { + var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) ); + if( !collectionJson.Exists ) { - var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) ); - if( !collectionJson.Exists ) - { - return; - } + return; + } - var defaultCollection = new ModCollection(); - var defaultCollectionFile = defaultCollection.FileName(); - if( defaultCollectionFile.Exists ) - { - return; - } + var defaultCollection = new ModCollection(); + var defaultCollectionFile = defaultCollection.FileName(); + if( defaultCollectionFile.Exists ) + { + return; + } - try - { - var text = File.ReadAllText( collectionJson.FullName ); - var data = JArray.Parse( text ); + try + { + var text = File.ReadAllText( collectionJson.FullName ); + var data = JArray.Parse( text ); - var maxPriority = 0; - foreach( var setting in data.Cast< JObject >() ) + var maxPriority = 0; + foreach( var setting in data.Cast< JObject >() ) + { + var modName = ( string )setting[ "FolderName" ]!; + var enabled = ( bool )setting[ "Enabled" ]!; + var priority = ( int )setting[ "Priority" ]!; + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); + + var save = new ModSettings() { - var modName = ( string )setting[ "FolderName" ]!; - var enabled = ( bool )setting[ "Enabled" ]!; - var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); - - var save = new ModSettings() - { - Enabled = enabled, - Priority = priority, - Settings = settings!, - }; - defaultCollection.Settings.Add( modName, save ); - maxPriority = Math.Max( maxPriority, priority ); - } - - if( !config.InvertModListOrder ) - { - foreach( var setting in defaultCollection.Settings.Values ) - { - setting.Priority = maxPriority - setting.Priority; - } - } - - defaultCollection.Save(); + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + defaultCollection.Settings.Add( modName, save ); + maxPriority = Math.Max( maxPriority, priority ); } - catch( Exception e ) + + if( !config.InvertModListOrder ) { - PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); - throw; + foreach( var setting in defaultCollection.Settings.Values ) + { + setting.Priority = maxPriority - setting.Priority; + } } + + defaultCollection.Save(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); + throw; } } } \ No newline at end of file diff --git a/Penumbra/Mod/GroupInformation.cs b/Penumbra/Mod/GroupInformation.cs new file mode 100644 index 00000000..7c86b5f3 --- /dev/null +++ b/Penumbra/Mod/GroupInformation.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.ComponentModel; +using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Util; + +namespace Penumbra.Mod; + +public enum SelectType +{ + Single, + Multi, +} + +public struct Option +{ + public string OptionName; + public string OptionDesc; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] + public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles; + + public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath ) + { + if( OptionFiles.TryGetValue( filePath, out var set ) ) + { + return set.Add( gamePath ); + } + + OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath }; + return true; + } +} + +public struct OptionGroup +{ + public string GroupName; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType; + + public List< Option > Options; + + private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) + { + // Selection contains the path, merge all GamePaths for this config. + if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) + { + paths.UnionWith( groupPaths ); + return true; + } + + // If the group contains the file in another selection, return true to skip it for default files. + for( var i = 0; i < Options.Count; ++i ) + { + if( i == selection ) + { + continue; + } + + if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) + { + return true; + } + } + + return false; + } + + private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) + { + var doNotAdd = false; + for( var i = 0; i < Options.Count; ++i ) + { + if( ( selection & ( 1 << i ) ) != 0 ) + { + if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) + { + paths.UnionWith( groupPaths ); + doNotAdd = true; + } + } + else if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) + { + doNotAdd = true; + } + } + + return doNotAdd; + } + + // Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist. + internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) + { + return SelectionType switch + { + SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ), + SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ), + _ => throw new InvalidEnumArgumentException( "Invalid option group type." ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Mod/Mod.cs b/Penumbra/Mod/Mod.cs index 820c79c5..cda33a3f 100644 --- a/Penumbra/Mod/Mod.cs +++ b/Penumbra/Mod/Mod.cs @@ -1,35 +1,33 @@ using System.Collections.Generic; using System.IO; -using Penumbra.GameData.Util; -using Penumbra.Util; +using Penumbra.GameData.ByteString; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// A complete Mod containing settings (i.e. dependent on a collection) +// and the resulting cache. +public class Mod { - // A complete Mod containing settings (i.e. dependent on a collection) - // and the resulting cache. - public class Mod + public ModSettings Settings { get; } + public ModData Data { get; } + public ModCache Cache { get; } + + public Mod( ModSettings settings, ModData data ) { - public ModSettings Settings { get; } - public ModData Data { get; } - public ModCache Cache { get; } - - public Mod( ModSettings settings, ModData data ) - { - Settings = settings; - Data = data; - Cache = new ModCache(); - } - - public bool FixSettings() - => Settings.FixInvalidSettings( Data.Meta ); - - public HashSet< GamePath > GetFiles( FileInfo file ) - { - var relPath = new RelPath( file, Data.BasePath ); - return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); - } - - public override string ToString() - => Data.Meta.Name; + Settings = settings; + Data = data; + Cache = new ModCache(); } + + public bool FixSettings() + => Settings.FixInvalidSettings( Data.Meta ); + + public HashSet< Utf8GamePath > GetFiles( FileInfo file ) + { + var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty; + return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); + } + + public override string ToString() + => Data.Meta.Name; } \ No newline at end of file diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index accb1272..7764828f 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -1,58 +1,57 @@ using System.Collections.Generic; using System.Linq; -using Penumbra.GameData.Util; +using Penumbra.GameData.ByteString; using Penumbra.Meta; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// The ModCache contains volatile information dependent on all current settings in a collection. +public class ModCache { - // The ModCache contains volatile information dependent on all current settings in a collection. - public class ModCache + public Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new(); + + public void AddConflict( Mod precedingMod, Utf8GamePath gamePath ) { - public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new(); - - public void AddConflict( Mod precedingMod, GamePath gamePath ) + if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) ) { - if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) ) - { - conflicts.Files.Add( gamePath ); - } - else - { - Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() ); - } + conflicts.Files.Add( gamePath ); } - - public void AddConflict( Mod precedingMod, MetaManipulation manipulation ) + else { - if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) ) - { - conflicts.Manipulations.Add( manipulation ); - } - else - { - Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } ); - } - } - - public void ClearConflicts() - => Conflicts.Clear(); - - public void ClearFileConflicts() - { - Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => - { - kvp.Value.Files.Clear(); - return kvp.Value; - } ); - } - - public void ClearMetaConflicts() - { - Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => - { - kvp.Value.Manipulations.Clear(); - return kvp.Value; - } ); + Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() ); } } + + public void AddConflict( Mod precedingMod, MetaManipulation manipulation ) + { + if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) ) + { + conflicts.Manipulations.Add( manipulation ); + } + else + { + Conflicts[ precedingMod ] = ( new List< Utf8GamePath >(), new List< MetaManipulation > { manipulation } ); + } + } + + public void ClearConflicts() + => Conflicts.Clear(); + + public void ClearFileConflicts() + { + Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => + { + kvp.Value.Files.Clear(); + return kvp.Value; + } ); + } + + public void ClearMetaConflicts() + { + Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => + { + kvp.Value.Manipulations.Clear(); + return kvp.Value; + } ); + } } \ No newline at end of file diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index a8f33f8c..0851ef77 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -2,526 +2,530 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Security.Cryptography; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Importer; using Penumbra.Mods; -using Penumbra.Structs; using Penumbra.Util; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +public class ModCleanup { - public class ModCleanup + private const string Duplicates = "Duplicates"; + private const string Required = "Required"; + + private readonly DirectoryInfo _baseDir; + private readonly ModMeta _mod; + private SHA256? _hasher; + + private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); + + private SHA256 Sha() { - private const string Duplicates = "Duplicates"; - private const string Required = "Required"; + _hasher ??= SHA256.Create(); + return _hasher; + } + private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) + { + _baseDir = baseDir; + _mod = mod; + BuildDict(); + } - private readonly DirectoryInfo _baseDir; - private readonly ModMeta _mod; - private SHA256? _hasher; - - private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); - - private SHA256 Sha() + private void BuildDict() + { + foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) { - _hasher ??= SHA256.Create(); - return _hasher; - } - - private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) - { - _baseDir = baseDir; - _mod = mod; - BuildDict(); - } - - private void BuildDict() - { - foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + var fileLength = file.Length; + if( _filesBySize.TryGetValue( fileLength, out var files ) ) { - var fileLength = file.Length; - if( _filesBySize.TryGetValue( fileLength, out var files ) ) - { - files.Add( file ); - } - else - { - _filesBySize[ fileLength ] = new List< FileInfo >() { file }; - } + files.Add( file ); + } + else + { + _filesBySize[ fileLength ] = new List< FileInfo > { file }; } } + } - private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option ) - { - var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; - var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName ); - return newDir; - } + private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option ) + { + var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; + return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); + } - private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) - { - Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; - newMod.Move( newSortOrder ); - newMod.ComputeChangedItems(); - ModFileSystem.InvokeChange(); - return newMod; - } + private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) + { + Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; + newMod.Move( newSortOrder ); + newMod.ComputeChangedItems(); + ModFileSystem.InvokeChange(); + return newMod; + } - private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option ) + private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option ) + { + var newMeta = new ModMeta { - var newMeta = new ModMeta + Author = mod.Meta.Author, + Name = name, + Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", + }; + var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); + newMeta.SaveToFile( metaFile ); + return newMeta; + } + + private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option ) + { + try + { + var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); + var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; + var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); + foreach( var (fileName, paths) in option.OptionFiles ) { - Author = mod.Meta.Author, - Name = name, - Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", - }; - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - newMeta.SaveToFile( metaFile ); - return newMeta; - } - - private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option ) - { - try - { - var newDir = CreateNewModDir( mod, group.GroupName!, option.OptionName ); - var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; - var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName!, option.OptionName ); - foreach( var (fileName, paths) in option.OptionFiles ) + var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); + unseenPaths.Remove( oldPath ); + if( File.Exists( oldPath ) ) { - var oldPath = Path.Combine( mod.BasePath.FullName, fileName ); - unseenPaths.Remove( oldPath ); - if( File.Exists( oldPath ) ) + foreach( var path in paths ) { - foreach( var path in paths ) - { - var newPath = Path.Combine( newDir.FullName, path ); - Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); - File.Copy( oldPath, newPath, true ); - } - } - } - - var newSortOrder = group.SelectionType == SelectType.Single - ? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" - : $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; - CreateNewMod( newDir, newSortOrder ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not split Mod:\n{e}" ); - } - } - - public static void SplitMod( ModData mod ) - { - if( !mod.Meta.Groups.Any() ) - { - return; - } - - var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); - foreach( var group in mod.Meta.Groups.Values ) - { - foreach( var option in group.Options ) - { - CreateModSplit( unseenPaths, mod, group, option ); - } - } - - if( !unseenPaths.Any() ) - { - return; - } - - var defaultGroup = new OptionGroup() - { - GroupName = "Default", - SelectionType = SelectType.Multi, - }; - var defaultOption = new Option() - { - OptionName = "Files", - OptionFiles = unseenPaths.ToDictionary( p => new RelPath( new FileInfo( p ), mod.BasePath ), - p => new HashSet< GamePath >() { new( new FileInfo( p ), mod.BasePath ) } ), - }; - CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); - } - - private static Option FindOrCreateDuplicates( ModMeta meta ) - { - static Option RequiredOption() - => new() - { - OptionName = Required, - OptionDesc = "", - OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), - }; - - if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) - { - var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); - if( idx >= 0 ) - { - return duplicates.Options[ idx ]; - } - - duplicates.Options.Add( RequiredOption() ); - return duplicates.Options.Last(); - } - - meta.Groups.Add( Duplicates, new OptionGroup - { - GroupName = Duplicates, - SelectionType = SelectType.Single, - Options = new List< Option > { RequiredOption() }, - } ); - - return meta.Groups[ Duplicates ].Options.First(); - } - - public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) - { - var dedup = new ModCleanup( baseDir, mod ); - foreach( var pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) - { - if( pair.Value.Count == 2 ) - { - if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) ) - { - dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] ); - } - } - else - { - var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray(); - var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray(); - - for( var i = 0; i < pair.Value.Count; ++i ) - { - if( deleted[ i ] ) - { - continue; - } - - for( var j = i + 1; j < pair.Value.Count; ++j ) - { - if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) - { - continue; - } - - dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] ); - deleted[ j ] = true; - } + var newPath = Path.Combine( newDir.FullName, path.ToString() ); + Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); + File.Copy( oldPath, newPath, true ); } } } - CleanUpDuplicates( mod ); - ClearEmptySubDirectories( dedup._baseDir ); + var newSortOrder = group.SelectionType == SelectType.Single + ? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" + : $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; + CreateNewMod( newDir, newSortOrder ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not split Mod:\n{e}" ); + } + } + + public static void SplitMod( ModData mod ) + { + if( mod.Meta.Groups.Count == 0 ) + { + return; } - private void ReplaceFile( FileInfo f1, FileInfo f2 ) + var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); + foreach( var group in mod.Meta.Groups.Values ) { - RelPath relName1 = new( f1, _baseDir ); - RelPath relName2 = new( f2, _baseDir ); - - var inOption1 = false; - var inOption2 = false; - foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) + foreach( var option in group.Options ) { - if( option.OptionFiles.ContainsKey( relName1 ) ) - { - inOption1 = true; - } - - if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) - { - continue; - } - - inOption2 = true; - - foreach( var value in values ) - { - option.AddFile( relName1, value ); - } - - option.OptionFiles.Remove( relName2 ); - } - - if( !inOption1 || !inOption2 ) - { - var duplicates = FindOrCreateDuplicates( _mod ); - if( !inOption1 ) - { - duplicates.AddFile( relName1, relName2.ToGamePath() ); - } - - if( !inOption2 ) - { - duplicates.AddFile( relName1, relName1.ToGamePath() ); - } - } - - PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); - f2.Delete(); - } - - public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) - => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); - - public static bool CompareHashes( byte[] f1, byte[] f2 ) - => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); - - public byte[] ComputeHash( FileInfo f ) - { - var stream = File.OpenRead( f.FullName ); - var ret = Sha().ComputeHash( stream ); - stream.Dispose(); - return ret; - } - - // Does not delete the base directory itself even if it is completely empty at the end. - public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) - { - foreach( var subDir in baseDir.GetDirectories() ) - { - ClearEmptySubDirectories( subDir ); - if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) - { - subDir.Delete(); - } + CreateModSplit( unseenPaths, mod, group, option ); } } - private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false ) + if( unseenPaths.Count == 0 ) { - var groupEnumerator = exceptDuplicates - ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) - : meta.Groups.Values; - return groupEnumerator.SelectMany( group => group.Options ) - .Any( option => option.OptionFiles.ContainsKey( relPath ) ); + return; } - private static void CleanUpDuplicates( ModMeta meta ) + var defaultGroup = new OptionGroup() { - if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) + GroupName = "Default", + SelectionType = SelectType.Multi, + }; + var defaultOption = new Option() + { + OptionName = "Files", + OptionFiles = unseenPaths.ToDictionary( + p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, + p => new HashSet< Utf8GamePath >() + { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), + }; + CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); + } + + private static Option FindOrCreateDuplicates( ModMeta meta ) + { + static Option RequiredOption() + => new() { - return; + OptionName = Required, + OptionDesc = "", + OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), + }; + + if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) + { + var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); + if( idx >= 0 ) + { + return duplicates.Options[ idx ]; } - var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); - if( requiredIdx >= 0 ) + duplicates.Options.Add( RequiredOption() ); + return duplicates.Options.Last(); + } + + meta.Groups.Add( Duplicates, new OptionGroup + { + GroupName = Duplicates, + SelectionType = SelectType.Single, + Options = new List< Option > { RequiredOption() }, + } ); + + return meta.Groups[ Duplicates ].Options.First(); + } + + public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) + { + var dedup = new ModCleanup( baseDir, mod ); + foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) + { + if( value.Count == 2 ) { - var required = info.Options[ requiredIdx ]; - foreach( var kvp in required.OptionFiles.ToArray() ) + if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) { - if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) ) + dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); + } + } + else + { + var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); + var hashes = value.Select( dedup.ComputeHash ).ToArray(); + + for( var i = 0; i < value.Count; ++i ) + { + if( deleted[ i ] ) { continue; } - if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( kvp.Key.ToGamePath() ) == 0 ) + for( var j = i + 1; j < value.Count; ++j ) { - required.OptionFiles.Remove( kvp.Key ); + if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) + { + continue; + } + + dedup.ReplaceFile( value[ i ], value[ j ] ); + deleted[ j ] = true; } } - - if( required.OptionFiles.Count == 0 ) - { - info.Options.RemoveAt( requiredIdx ); - } - } - - if( info.Options.Count == 0 ) - { - meta.Groups.Remove( Duplicates ); } } - public enum GroupType + CleanUpDuplicates( mod ); + ClearEmptySubDirectories( dedup._baseDir ); + } + + private void ReplaceFile( FileInfo f1, FileInfo f2 ) + { + if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) + || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) { - Both = 0, - Single = 1, - Multi = 2, + return; + } + + var inOption1 = false; + var inOption2 = false; + foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) + { + if( option.OptionFiles.ContainsKey( relName1 ) ) + { + inOption1 = true; + } + + if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) + { + continue; + } + + inOption2 = true; + + foreach( var value in values ) + { + option.AddFile( relName1, value ); + } + + option.OptionFiles.Remove( relName2 ); + } + + if( !inOption1 || !inOption2 ) + { + var duplicates = FindOrCreateDuplicates( _mod ); + if( !inOption1 ) + { + duplicates.AddFile( relName1, relName2.ToGamePath() ); + } + + if( !inOption2 ) + { + duplicates.AddFile( relName1, relName1.ToGamePath() ); + } + } + + PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); + f2.Delete(); + } + + public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) + => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); + + public static bool CompareHashes( byte[] f1, byte[] f2 ) + => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); + + public byte[] ComputeHash( FileInfo f ) + { + var stream = File.OpenRead( f.FullName ); + var ret = Sha().ComputeHash( stream ); + stream.Dispose(); + return ret; + } + + // Does not delete the base directory itself even if it is completely empty at the end. + public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) + { + foreach( var subDir in baseDir.GetDirectories() ) + { + ClearEmptySubDirectories( subDir ); + if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) + { + subDir.Delete(); + } + } + } + + private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) + { + var groupEnumerator = exceptDuplicates + ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) + : meta.Groups.Values; + return groupEnumerator.SelectMany( group => group.Options ) + .Any( option => option.OptionFiles.ContainsKey( relPath ) ); + } + + private static void CleanUpDuplicates( ModMeta meta ) + { + if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) + { + return; + } + + var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); + if( requiredIdx >= 0 ) + { + var required = info.Options[ requiredIdx ]; + foreach( var (key, value) in required.OptionFiles.ToArray() ) + { + if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) + { + continue; + } + + if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) + { + required.OptionFiles.Remove( key ); + } + } + + if( required.OptionFiles.Count == 0 ) + { + info.Options.RemoveAt( requiredIdx ); + } + } + + if( info.Options.Count == 0 ) + { + meta.Groups.Remove( Duplicates ); + } + } + + public enum GroupType + { + Both = 0, + Single = 1, + Multi = 2, + }; + + private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, + bool skipDuplicates = true ) + { + if( meta.Groups.Count == 0 ) + { + return; + } + + var enumerator = type switch + { + GroupType.Both => meta.Groups.Values, + GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), + GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), + _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), }; - - private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both, - bool skipDuplicates = true ) + foreach( var group in enumerator ) { - if( meta.Groups.Count == 0 ) + var optionEnum = skipDuplicates + ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) + : group.Options; + foreach( var option in optionEnum ) { - return; - } - - var enumerator = type switch - { - GroupType.Both => meta.Groups.Values, - GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), - GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), - _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), - }; - foreach( var group in enumerator ) - { - var optionEnum = skipDuplicates - ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) - : group.Options; - foreach( var option in optionEnum ) + if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) { - if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) - { - option.OptionFiles.Remove( relPath ); - } + option.OptionFiles.Remove( relPath ); } } } + } - public static bool MoveFile( ModMeta meta, string basePath, RelPath oldRelPath, RelPath newRelPath ) + public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) + { + if( oldRelPath.Equals( newRelPath ) ) { - if( oldRelPath == newRelPath ) - { - return true; - } - - try - { - var newFullPath = Path.Combine( basePath, newRelPath ); - new FileInfo( newFullPath ).Directory!.Create(); - File.Move( Path.Combine( basePath, oldRelPath ), newFullPath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); - return false; - } - - foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) - { - if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) - { - option.OptionFiles.Add( newRelPath, gamePaths ); - option.OptionFiles.Remove( oldRelPath ); - } - } - return true; } - - private static void RemoveUselessGroups( ModMeta meta ) + try { - meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) - .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); + var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); + new FileInfo( newFullPath ).Directory!.Create(); + File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); + return false; } - // Goes through all Single-Select options and checks if file links are in each of them. - // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). - public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) + foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) { - foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) + if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) { - var firstOption = true; - HashSet< (RelPath, GamePath) > groupList = new(); - foreach( var option in group.Options ) + option.OptionFiles.Add( newRelPath, gamePaths ); + option.OptionFiles.Remove( oldRelPath ); + } + } + + return true; + } + + + private static void RemoveUselessGroups( ModMeta meta ) + { + meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) + .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); + } + + // Goes through all Single-Select options and checks if file links are in each of them. + // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). + public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) + { + foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) + { + var firstOption = true; + HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); + foreach( var option in group.Options ) + { + HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); + foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) { - HashSet< (RelPath, GamePath) > optionList = new(); - foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) - { - optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); - } - - if( firstOption ) - { - groupList = optionList; - } - else - { - groupList.IntersectWith( optionList ); - } - - firstOption = false; + optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); } - var newPath = new Dictionary< RelPath, GamePath >(); - foreach( var (path, gamePath) in groupList ) + if( firstOption ) { - var relPath = new RelPath( gamePath ); - if( newPath.TryGetValue( path, out var usedGamePath ) ) - { - var required = FindOrCreateDuplicates( meta ); - var usedRelPath = new RelPath( usedGamePath ); - required.AddFile( usedRelPath, gamePath ); - required.AddFile( usedRelPath, usedGamePath ); - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } - else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) - { - newPath[ path ] = gamePath; - if( FileIsInAnyGroup( meta, relPath ) ) - { - FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); - } - - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } + groupList = optionList; } + else + { + groupList.IntersectWith( optionList ); + } + + firstOption = false; } - RemoveUselessGroups( meta ); - ClearEmptySubDirectories( baseDir ); + var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); + foreach( var (path, gamePath) in groupList ) + { + var relPath = new Utf8RelPath( gamePath ); + if( newPath.TryGetValue( path, out var usedGamePath ) ) + { + var required = FindOrCreateDuplicates( meta ); + var usedRelPath = new Utf8RelPath( usedGamePath ); + required.AddFile( usedRelPath, gamePath ); + required.AddFile( usedRelPath, usedGamePath ); + RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); + } + else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) + { + newPath[ path ] = gamePath; + if( FileIsInAnyGroup( meta, relPath ) ) + { + FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); + } + + RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); + } + } } - public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) + RemoveUselessGroups( meta ); + ClearEmptySubDirectories( baseDir ); + } + + public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) + { + meta.Groups.Clear(); + ClearEmptySubDirectories( baseDir ); + foreach( var groupDir in baseDir.EnumerateDirectories() ) { - meta.Groups.Clear(); - ClearEmptySubDirectories( baseDir ); - foreach( var groupDir in baseDir.EnumerateDirectories() ) + var group = new OptionGroup { - var group = new OptionGroup + GroupName = groupDir.Name, + SelectionType = SelectType.Single, + Options = new List< Option >(), + }; + + foreach( var optionDir in groupDir.EnumerateDirectories() ) + { + var option = new Option { - GroupName = groupDir.Name, - SelectionType = SelectType.Single, - Options = new List< Option >(), + OptionDesc = string.Empty, + OptionName = optionDir.Name, + OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), }; - - foreach( var optionDir in groupDir.EnumerateDirectories() ) + foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) { - var option = new Option + if( Utf8RelPath.FromFile( file, baseDir, out var rel ) + && Utf8GamePath.FromFile( file, optionDir, out var game ) ) { - OptionDesc = string.Empty, - OptionName = optionDir.Name, - OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), - }; - foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - var relPath = new RelPath( file, baseDir ); - var gamePath = new GamePath( file, optionDir ); - option.OptionFiles[ relPath ] = new HashSet< GamePath > { gamePath }; - } - - if( option.OptionFiles.Any() ) - { - group.Options.Add( option ); + option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; } } - if( group.Options.Any() ) + if( option.OptionFiles.Any() ) { - meta.Groups.Add( groupDir.Name, group ); + group.Options.Add( option ); } } - foreach(var collection in Penumbra.ModManager.Collections.Collections.Values) - collection.UpdateSetting(baseDir, meta, true); - + if( group.Options.Any() ) + { + meta.Groups.Add( groupDir.Name, group ); + } + } + + foreach( var collection in Penumbra.ModManager.Collections.Collections.Values ) + { + collection.UpdateSetting( baseDir, meta, true ); } } } \ No newline at end of file diff --git a/Penumbra/Mod/ModData.cs b/Penumbra/Mod/ModData.cs index 603f254e..6400a88d 100644 --- a/Penumbra/Mod/ModData.cs +++ b/Penumbra/Mod/ModData.cs @@ -3,134 +3,133 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +public struct SortOrder : IComparable< SortOrder > { - public struct SortOrder : IComparable< SortOrder > + public ModFolder ParentFolder { get; set; } + + private string _sortOrderName; + + public string SortOrderName { - public ModFolder ParentFolder { get; set; } - - private string _sortOrderName; - - public string SortOrderName - { - get => _sortOrderName; - set => _sortOrderName = value.Replace( '/', '\\' ); - } - - public string SortOrderPath - => ParentFolder.FullName; - - public string FullName - { - get - { - var path = SortOrderPath; - return path.Any() ? $"{path}/{SortOrderName}" : SortOrderName; - } - } - - - public SortOrder( ModFolder parentFolder, string name ) - { - ParentFolder = parentFolder; - _sortOrderName = name.Replace( '/', '\\' ); - } - - public string FullPath - => SortOrderPath.Any() ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; - - public int CompareTo( SortOrder other ) - => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); + get => _sortOrderName; + set => _sortOrderName = value.Replace( '/', '\\' ); } - // ModData contains all permanent information about a mod, - // and is independent of collections or settings. - // It only changes when the user actively changes the mod or their filesystem. - public class ModData + public string SortOrderPath + => ParentFolder.FullName; + + public string FullName { - public DirectoryInfo BasePath; - public ModMeta Meta; - public ModResources Resources; - - public SortOrder SortOrder; - - public SortedList< string, object? > ChangedItems { get; } = new(); - public string LowerChangedItemsString { get; private set; } = string.Empty; - public FileInfo MetaFile { get; set; } - - private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) + get { - BasePath = basePath; - Meta = meta; - Resources = resources; - MetaFile = MetaFileInfo( basePath ); - SortOrder = new SortOrder( parentFolder, Meta.Name ); - SortOrder.ParentFolder.AddMod( this ); - - ComputeChangedItems(); + var path = SortOrderPath; + return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName; } - - public void ComputeChangedItems() - { - var identifier = GameData.GameData.GetIdentifier(); - ChangedItems.Clear(); - foreach( var file in Resources.ModFiles.Select( f => new RelPath( f, BasePath ) ) ) - { - foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) - { - identifier.Identify( ChangedItems, path ); - } - } - - foreach( var path in Meta.FileSwaps.Keys ) - { - identifier.Identify( ChangedItems, path ); - } - - LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); - } - - public static FileInfo MetaFileInfo( DirectoryInfo basePath ) - => new( Path.Combine( basePath.FullName, "meta.json" ) ); - - public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) - { - basePath.Refresh(); - if( !basePath.Exists ) - { - PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); - return null; - } - - var metaFile = MetaFileInfo( basePath ); - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - var meta = ModMeta.LoadFromFile( metaFile ); - if( meta == null ) - { - return null; - } - - var data = new ModResources(); - if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) - { - data.SetManipulations( meta, basePath ); - } - - return new ModData( parentFolder, basePath, meta, data ); - } - - public void SaveMeta() - => Meta.SaveToFile( MetaFile ); - - public override string ToString() - => SortOrder.FullPath; } + + + public SortOrder( ModFolder parentFolder, string name ) + { + ParentFolder = parentFolder; + _sortOrderName = name.Replace( '/', '\\' ); + } + + public string FullPath + => SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; + + public int CompareTo( SortOrder other ) + => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); +} + +// ModData contains all permanent information about a mod, +// and is independent of collections or settings. +// It only changes when the user actively changes the mod or their filesystem. +public class ModData +{ + public DirectoryInfo BasePath; + public ModMeta Meta; + public ModResources Resources; + + public SortOrder SortOrder; + + public SortedList< string, object? > ChangedItems { get; } = new(); + public string LowerChangedItemsString { get; private set; } = string.Empty; + public FileInfo MetaFile { get; set; } + + private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) + { + BasePath = basePath; + Meta = meta; + Resources = resources; + MetaFile = MetaFileInfo( basePath ); + SortOrder = new SortOrder( parentFolder, Meta.Name ); + SortOrder.ParentFolder.AddMod( this ); + + ComputeChangedItems(); + } + + public void ComputeChangedItems() + { + var identifier = GameData.GameData.GetIdentifier(); + ChangedItems.Clear(); + foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) + { + foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) + { + identifier.Identify( ChangedItems, path.ToGamePath() ); + } + } + + foreach( var path in Meta.FileSwaps.Keys ) + { + identifier.Identify( ChangedItems, path.ToGamePath() ); + } + + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); + } + + public static FileInfo MetaFileInfo( DirectoryInfo basePath ) + => new(Path.Combine( basePath.FullName, "meta.json" )); + + public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + { + basePath.Refresh(); + if( !basePath.Exists ) + { + PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + return null; + } + + var metaFile = MetaFileInfo( basePath ); + if( !metaFile.Exists ) + { + PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); + return null; + } + + var meta = ModMeta.LoadFromFile( metaFile ); + if( meta == null ) + { + return null; + } + + var data = new ModResources(); + if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) + { + data.SetManipulations( meta, basePath ); + } + + return new ModData( parentFolder, basePath, meta, data ); + } + + public void SaveMeta() + => Meta.SaveToFile( MetaFile ); + + public override string ToString() + => SortOrder.FullPath; } \ No newline at end of file diff --git a/Penumbra/Mod/ModFunctions.cs b/Penumbra/Mod/ModFunctions.cs index 8f7f6e1c..7372cfa0 100644 --- a/Penumbra/Mod/ModFunctions.cs +++ b/Penumbra/Mod/ModFunctions.cs @@ -1,103 +1,100 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Penumbra.GameData.Util; -using Penumbra.Structs; -using Penumbra.Util; +using Penumbra.GameData.ByteString; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// Functions that do not really depend on only one component of a mod. +public static class ModFunctions { - // Functions that do not really depend on only one component of a mod. - public static class ModFunctions + public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) { - public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) + var hashes = modPaths.Select( p => p.Name ).ToHashSet(); + var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray(); + var anyChanges = false; + foreach( var toRemove in missingMods ) { - var hashes = modPaths.Select( p => p.Name ).ToHashSet(); - var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray(); - var anyChanges = false; - foreach( var toRemove in missingMods ) - { - anyChanges |= settings.Remove( toRemove ); - } - - return anyChanges; + anyChanges |= settings.Remove( toRemove ); } - public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta ) + return anyChanges; + } + + public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta ) + { + var doNotAdd = false; + var files = new HashSet< Utf8GamePath >(); + foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) ) { - var doNotAdd = false; - var files = new HashSet< GamePath >(); - foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) ) - { - doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files ); - } - - if( !doNotAdd ) - { - files.Add( new GamePath( relPath ) ); - } - - return files; + doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files ); } - public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta ) + if( !doNotAdd ) { - var ret = new HashSet< GamePath >(); - foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) ) - { - if( option.OptionFiles.TryGetValue( relPath, out var files ) ) - { - ret.UnionWith( files ); - } - } - - if( ret.Count == 0 ) - { - ret.Add( relPath.ToGamePath() ); - } - - return ret; + files.Add( relPath.ToGamePath() ); } - public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) + return files; + } + + public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta ) + { + var ret = new HashSet< Utf8GamePath >(); + foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) ) { - ModSettings ret = new() + if( option.OptionFiles.TryGetValue( relPath, out var files ) ) { - Priority = namedSettings.Priority, - Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), - }; + ret.UnionWith( files ); + } + } - foreach( var kvp in namedSettings.Settings ) + if( ret.Count == 0 ) + { + ret.Add( relPath.ToGamePath() ); + } + + return ret; + } + + public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) + { + ModSettings ret = new() + { + Priority = namedSettings.Priority, + Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), + }; + + foreach( var setting in namedSettings.Settings.Keys ) + { + if( !meta.Groups.TryGetValue( setting, out var info ) ) { - if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) - { - continue; - } + continue; + } - if( info.SelectionType == SelectType.Single ) + if( info.SelectionType == SelectType.Single ) + { + if( namedSettings.Settings[ setting ].Count == 0 ) { - if( namedSettings.Settings[ kvp.Key ].Count == 0 ) - { - ret.Settings[ kvp.Key ] = 0; - } - else - { - var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() ); - ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx; - } + ret.Settings[ setting ] = 0; } else { - foreach( var idx in namedSettings.Settings[ kvp.Key ] - .Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) - .Where( idx => idx >= 0 ) ) - { - ret.Settings[ kvp.Key ] |= 1 << idx; - } + var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() ); + ret.Settings[ setting ] = idx < 0 ? 0 : idx; + } + } + else + { + foreach( var idx in namedSettings.Settings[ setting ] + .Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) + .Where( idx => idx >= 0 ) ) + { + ret.Settings[ setting ] |= 1 << idx; } } - - return ret; } + + return ret; } } \ No newline at end of file diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mod/ModMeta.cs index 946d11f8..5a4776eb 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mod/ModMeta.cs @@ -4,134 +4,133 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Structs; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// Contains descriptive data about the mod as well as possible settings and fileswaps. +public class ModMeta { - // Contains descriptive data about the mod as well as possible settings and fileswaps. - public class ModMeta + public uint FileVersion { get; set; } + + public string Name { - public uint FileVersion { get; set; } - - public string Name + get => _name; + set { - get => _name; - set - { - _name = value; - LowerName = value.ToLowerInvariant(); - } + _name = value; + LowerName = value.ToLowerInvariant(); } + } - private string _name = "Mod"; + private string _name = "Mod"; - [JsonIgnore] - public string LowerName { get; private set; } = "mod"; + [JsonIgnore] + public string LowerName { get; private set; } = "mod"; - private string _author = ""; + private string _author = ""; - public string Author + public string Author + { + get => _author; + set { - get => _author; - set - { - _author = value; - LowerAuthor = value.ToLowerInvariant(); - } + _author = value; + LowerAuthor = value.ToLowerInvariant(); } + } - [JsonIgnore] - public string LowerAuthor { get; private set; } = ""; + [JsonIgnore] + public string LowerAuthor { get; private set; } = ""; - public string Description { get; set; } = ""; - public string Version { get; set; } = ""; - public string Website { get; set; } = ""; + public string Description { get; set; } = ""; + public string Version { get; set; } = ""; + public string Website { get; set; } = ""; - [JsonProperty( ItemConverterType = typeof( GamePathConverter ) )] - public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new(); + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + public Dictionary< string, OptionGroup > Groups { get; set; } = new(); - [JsonIgnore] - private int FileHash { get; set; } + [JsonIgnore] + private int FileHash { get; set; } - [JsonIgnore] - public bool HasGroupsWithConfig { get; private set; } + [JsonIgnore] + public bool HasGroupsWithConfig { get; private set; } - public bool RefreshFromFile( FileInfo filePath ) + public bool RefreshFromFile( FileInfo filePath ) + { + var newMeta = LoadFromFile( filePath ); + if( newMeta == null ) { - var newMeta = LoadFromFile( filePath ); - if( newMeta == null ) - { - return true; - } - - if( newMeta.FileHash == FileHash ) - { - return false; - } - - FileVersion = newMeta.FileVersion; - Name = newMeta.Name; - Author = newMeta.Author; - Description = newMeta.Description; - Version = newMeta.Version; - Website = newMeta.Website; - FileSwaps = newMeta.FileSwaps; - Groups = newMeta.Groups; - FileHash = newMeta.FileHash; - HasGroupsWithConfig = newMeta.HasGroupsWithConfig; return true; } - public static ModMeta? LoadFromFile( FileInfo filePath ) + if( newMeta.FileHash == FileHash ) { - try - { - var text = File.ReadAllText( filePath.FullName ); - - var meta = JsonConvert.DeserializeObject< ModMeta >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - if( meta != null ) - { - meta.FileHash = text.GetHashCode(); - meta.RefreshHasGroupsWithConfig(); - } - - return meta; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod meta:\n{e}" ); - return null; - } + return false; } - public bool RefreshHasGroupsWithConfig() + FileVersion = newMeta.FileVersion; + Name = newMeta.Name; + Author = newMeta.Author; + Description = newMeta.Description; + Version = newMeta.Version; + Website = newMeta.Website; + FileSwaps = newMeta.FileSwaps; + Groups = newMeta.Groups; + FileHash = newMeta.FileHash; + HasGroupsWithConfig = newMeta.HasGroupsWithConfig; + return true; + } + + public static ModMeta? LoadFromFile( FileInfo filePath ) + { + try { - var oldValue = HasGroupsWithConfig; - HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); - return oldValue != HasGroupsWithConfig; + var text = File.ReadAllText( filePath.FullName ); + + var meta = JsonConvert.DeserializeObject< ModMeta >( text, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + if( meta != null ) + { + meta.FileHash = text.GetHashCode(); + meta.RefreshHasGroupsWithConfig(); + } + + return meta; } - - - public void SaveToFile( FileInfo filePath ) + catch( Exception e ) { - try + PluginLog.Error( $"Could not load mod meta:\n{e}" ); + return null; + } + } + + public bool RefreshHasGroupsWithConfig() + { + var oldValue = HasGroupsWithConfig; + HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); + return oldValue != HasGroupsWithConfig; + } + + + public void SaveToFile( FileInfo filePath ) + { + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + var newHash = text.GetHashCode(); + if( newHash != FileHash ) { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - var newHash = text.GetHashCode(); - if( newHash != FileHash ) - { - File.WriteAllText( filePath.FullName, text ); - FileHash = newHash; - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + File.WriteAllText( filePath.FullName, text ); + FileHash = newHash; } } + catch( Exception e ) + { + PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + } } } \ No newline at end of file diff --git a/Penumbra/Mod/ModSettings.cs b/Penumbra/Mod/ModSettings.cs index d24b6c0b..4f82df49 100644 --- a/Penumbra/Mod/ModSettings.cs +++ b/Penumbra/Mod/ModSettings.cs @@ -1,75 +1,73 @@ using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Structs; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// Contains the settings for a given mod. +public class ModSettings { - // Contains the settings for a given mod. - public class ModSettings + public bool Enabled { get; set; } + public int Priority { get; set; } + public Dictionary< string, int > Settings { get; set; } = new(); + + // For backwards compatibility + private Dictionary< string, int > Conf { - public bool Enabled { get; set; } - public int Priority { get; set; } - public Dictionary< string, int > Settings { get; set; } = new(); + set => Settings = value; + } - // For backwards compatibility - private Dictionary< string, int > Conf + public ModSettings DeepCopy() + { + var settings = new ModSettings { - set => Settings = value; + Enabled = Enabled, + Priority = Priority, + Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + }; + return settings; + } + + public static ModSettings DefaultSettings( ModMeta meta ) + { + return new ModSettings + { + Enabled = false, + Priority = 0, + Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ), + }; + } + + public bool FixSpecificSetting( string name, ModMeta meta ) + { + if( !meta.Groups.TryGetValue( name, out var group ) ) + { + return Settings.Remove( name ); } - public ModSettings DeepCopy() + if( Settings.TryGetValue( name, out var oldSetting ) ) { - var settings = new ModSettings + Settings[ name ] = group.SelectionType switch { - Enabled = Enabled, - Priority = Priority, - Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), + SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), + _ => Settings[ group.GroupName ], }; - return settings; + return oldSetting != Settings[ group.GroupName ]; } - public static ModSettings DefaultSettings( ModMeta meta ) + Settings[ name ] = 0; + return true; + } + + public bool FixInvalidSettings( ModMeta meta ) + { + if( meta.Groups.Count == 0 ) { - return new() - { - Enabled = false, - Priority = 0, - Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ), - }; + return false; } - public bool FixSpecificSetting( string name, ModMeta meta ) - { - if( !meta.Groups.TryGetValue( name, out var group ) ) - { - return Settings.Remove( name ); - } - - if( Settings.TryGetValue( name, out var oldSetting ) ) - { - Settings[ name ] = group.SelectionType switch - { - SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), - SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), - _ => Settings[ group.GroupName ], - }; - return oldSetting != Settings[ group.GroupName ]; - } - - Settings[ name ] = 0; - return true; - } - - public bool FixInvalidSettings( ModMeta meta ) - { - if( meta.Groups.Count == 0 ) - { - return false; - } - - return Settings.Keys.ToArray().Union( meta.Groups.Keys ) - .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) ); - } + return Settings.Keys.ToArray().Union( meta.Groups.Keys ) + .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) ); } } \ No newline at end of file diff --git a/Penumbra/Mod/NamedModSettings.cs b/Penumbra/Mod/NamedModSettings.cs index 9e812ef5..45770e7e 100644 --- a/Penumbra/Mod/NamedModSettings.cs +++ b/Penumbra/Mod/NamedModSettings.cs @@ -1,45 +1,43 @@ using System.Collections.Generic; using System.Linq; -using Penumbra.Structs; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +// Contains settings with the option selections stored by names instead of index. +// This is meant to make them possibly more portable when we support importing collections from other users. +// Enabled does not exist, because disabled mods would not be exported in this way. +public class NamedModSettings { - // Contains settings with the option selections stored by names instead of index. - // This is meant to make them possibly more portable when we support importing collections from other users. - // Enabled does not exist, because disabled mods would not be exported in this way. - public class NamedModSettings + public int Priority { get; set; } + public Dictionary< string, HashSet< string > > Settings { get; set; } = new(); + + public void AddFromModSetting( ModSettings s, ModMeta meta ) { - public int Priority { get; set; } - public Dictionary< string, HashSet< string > > Settings { get; set; } = new(); + Priority = s.Priority; + Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() ); - public void AddFromModSetting( ModSettings s, ModMeta meta ) + foreach( var kvp in Settings ) { - Priority = s.Priority; - Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() ); - - foreach( var kvp in Settings ) + if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) { - if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) - { - continue; - } + continue; + } - var setting = s.Settings[ kvp.Key ]; - if( info.SelectionType == SelectType.Single ) + var setting = s.Settings[ kvp.Key ]; + if( info.SelectionType == SelectType.Single ) + { + var name = setting < info.Options.Count + ? info.Options[ setting ].OptionName + : info.Options[ 0 ].OptionName; + kvp.Value.Add( name ); + } + else + { + for( var i = 0; i < info.Options.Count; ++i ) { - var name = setting < info.Options.Count - ? info.Options[ setting ].OptionName - : info.Options[ 0 ].OptionName; - kvp.Value.Add( name ); - } - else - { - for( var i = 0; i < info.Options.Count; ++i ) + if( ( ( setting >> i ) & 1 ) != 0 ) { - if( ( ( setting >> i ) & 1 ) != 0 ) - { - kvp.Value.Add( info.Options[ i ].OptionName ); - } + kvp.Value.Add( info.Options[ i ].OptionName ); } } } diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 70145b03..f614fef3 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; -using Penumbra.Interop; using Penumbra.Mod; using Penumbra.Util; @@ -250,16 +249,16 @@ public class CollectionManager public bool CreateCharacterCollection( string characterName ) { - if( !CharacterCollection.ContainsKey( characterName ) ) + if( CharacterCollection.ContainsKey( characterName ) ) { - CharacterCollection[ characterName ] = ModCollection.Empty; - Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; - Penumbra.Config.Save(); - Penumbra.PlayerWatcher.AddPlayerToWatch( characterName ); - return true; + return false; } - return false; + CharacterCollection[ characterName ] = ModCollection.Empty; + Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; + Penumbra.Config.Save(); + Penumbra.PlayerWatcher.AddPlayerToWatch( characterName ); + return true; } public void RemoveCharacterCollection( string characterName ) @@ -299,7 +298,7 @@ public class CollectionManager private bool LoadForcedCollection( Configuration config ) { - if( config.ForcedCollection == string.Empty ) + if( config.ForcedCollection.Length == 0 ) { ForcedCollection = ModCollection.Empty; return false; @@ -320,7 +319,7 @@ public class CollectionManager private bool LoadDefaultCollection( Configuration config ) { - if( config.DefaultCollection == string.Empty ) + if( config.DefaultCollection.Length == 0 ) { DefaultCollection = ModCollection.Empty; return false; @@ -345,7 +344,7 @@ public class CollectionManager foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) { Penumbra.PlayerWatcher.AddPlayerToWatch( player ); - if( collectionName == string.Empty ) + if( collectionName.Length == 0 ) { CharacterCollection.Add( player, ModCollection.Empty ); } diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 92f8275a..cfaba390 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -1,255 +1,256 @@ -using Dalamud.Plugin; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Interop; using Penumbra.Mod; using Penumbra.Util; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +// A ModCollection is a named set of ModSettings to all of the users' installed mods. +// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. +// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. +// Active ModCollections build a cache of currently relevant data. +public class ModCollection { - // A ModCollection is a named set of ModSettings to all of the users' installed mods. - // It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. - // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. - // Active ModCollections build a cache of currently relevant data. - public class ModCollection + public const string DefaultCollection = "Default"; + + public string Name { get; set; } + + public Dictionary< string, ModSettings > Settings { get; } + + public ModCollection() { - public const string DefaultCollection = "Default"; + Name = DefaultCollection; + Settings = new Dictionary< string, ModSettings >(); + } - public string Name { get; set; } + public ModCollection( string name, Dictionary< string, ModSettings > settings ) + { + Name = name; + Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + } - public Dictionary< string, ModSettings > Settings { get; } - - public ModCollection() + public Mod.Mod GetMod( ModData mod ) + { + if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) ) { - Name = DefaultCollection; - Settings = new Dictionary< string, ModSettings >(); + return ret; } - public ModCollection( string name, Dictionary< string, ModSettings > settings ) + if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { - Name = name; - Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + return new Mod.Mod( settings, mod ); } - public Mod.Mod GetMod( ModData mod ) - { - if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) ) - { - return ret; - } + var newSettings = ModSettings.DefaultSettings( mod.Meta ); + Settings.Add( mod.BasePath.Name, newSettings ); + Save(); + return new Mod.Mod( newSettings, mod ); + } + private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) + { + var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); + + foreach( var s in removeList ) + { + Settings.Remove( s.Key ); + } + + return removeList.Length > 0; + } + + public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data ) + { + Cache = new ModCollectionCache( Name, modDirectory ); + var changedSettings = false; + foreach( var mod in data ) + { if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { - return new Mod.Mod( settings, mod ); + Cache.AddMod( settings, mod, false ); } + else + { + changedSettings = true; + var newSettings = ModSettings.DefaultSettings( mod.Meta ); + Settings.Add( mod.BasePath.Name, newSettings ); + Cache.AddMod( newSettings, mod, false ); + } + } - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); + if( changedSettings ) + { Save(); - return new Mod.Mod( newSettings, mod ); } - private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) + CalculateEffectiveFileList( modDirectory, true, false ); + } + + public void ClearCache() + => Cache = null; + + public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear ) + { + if( !Settings.TryGetValue( modPath.Name, out var settings ) ) { - var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); - - foreach( var s in removeList ) - { - Settings.Remove( s.Key ); - } - - return removeList.Length > 0; + return; } - public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data ) + if( clear ) { - Cache = new ModCollectionCache( Name, modDirectory ); - var changedSettings = false; - foreach( var mod in data ) - { - if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - Cache.AddMod( settings, mod, false ); - } - else - { - changedSettings = true; - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); - Cache.AddMod( newSettings, mod, false ); - } - } - - if( changedSettings ) - { - Save(); - } - - CalculateEffectiveFileList( modDirectory, true, false ); + settings.Settings.Clear(); } - public void ClearCache() - => Cache = null; - - public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear ) + if( settings.FixInvalidSettings( meta ) ) { - if( !Settings.TryGetValue( modPath.Name, out var settings ) ) - { - return; - } + Save(); + } + } - if (clear) - settings.Settings.Clear(); - if( settings.FixInvalidSettings( meta ) ) - { - Save(); - } + public void UpdateSetting( ModData mod ) + => UpdateSetting( mod.BasePath, mod.Meta, false ); + + public void UpdateSettings( bool forceSave ) + { + if( Cache == null ) + { + return; } - public void UpdateSetting( ModData mod ) - => UpdateSetting( mod.BasePath, mod.Meta, false ); - - public void UpdateSettings( bool forceSave ) + var changes = false; + foreach( var mod in Cache.AvailableMods.Values ) { - if( Cache == null ) - { - return; - } - - var changes = false; - foreach( var mod in Cache.AvailableMods.Values ) - { - changes |= mod.FixSettings(); - } - - if( forceSave || changes ) - { - Save(); - } + changes |= mod.FixSettings(); } - public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection ) + if( forceSave || changes ) { - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, - withMetaManipulations, activeCollection ); - Cache ??= new ModCollectionCache( Name, modDir ); - UpdateSettings( false ); - Cache.CalculateEffectiveFileList(); - if( withMetaManipulations ) + Save(); + } + } + + public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection ) + { + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, + withMetaManipulations, activeCollection ); + Cache ??= new ModCollectionCache( Name, modDir ); + UpdateSettings( false ); + Cache.CalculateEffectiveFileList(); + if( withMetaManipulations ) + { + Cache.UpdateMetaManipulations(); + if( activeCollection ) { - Cache.UpdateMetaManipulations(); - if( activeCollection ) - { - Penumbra.ResidentResources.Reload(); - } + Penumbra.ResidentResources.Reload(); } } + } - [JsonIgnore] - public ModCollectionCache? Cache { get; private set; } + [JsonIgnore] + public ModCollectionCache? Cache { get; private set; } - public static ModCollection? LoadFromFile( FileInfo file ) + public static ModCollection? LoadFromFile( FileInfo file ) + { + if( !file.Exists ) { - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) ); - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); return null; } - private void SaveToFile( FileInfo file ) + try { - try - { - File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" ); - } + var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) ); + return collection; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); } - public static DirectoryInfo CollectionDir() - => new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ) ); - - private static FileInfo FileName( DirectoryInfo collectionDir, string name ) - => new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) ); - - public FileInfo FileName() - => new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - $"{Name.RemoveInvalidPathSymbols()}.json" ) ); - - public void Save() - { - try - { - var dir = CollectionDir(); - dir.Create(); - var file = FileName( dir, Name ); - SaveToFile( file ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public static ModCollection? Load( string name ) - { - var file = FileName( CollectionDir(), name ); - return file.Exists ? LoadFromFile( file ) : null; - } - - public void Delete() - { - var file = FileName( CollectionDir(), Name ); - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" ); - } - } - } - - public void AddMod( ModData data ) - { - if( Cache == null ) - { - return; - } - - Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings ) - ? settings - : ModSettings.DefaultSettings( data.Meta ), - data ); - } - - public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) - => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); - - public static readonly ModCollection Empty = new() { Name = "" }; + return null; } + + private void SaveToFile( FileInfo file ) + { + try + { + File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" ); + } + } + + public static DirectoryInfo CollectionDir() + => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" )); + + private static FileInfo FileName( DirectoryInfo collectionDir, string name ) + => new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" )); + + public FileInfo FileName() + => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), + $"{Name.RemoveInvalidPathSymbols()}.json" )); + + public void Save() + { + try + { + var dir = CollectionDir(); + dir.Create(); + var file = FileName( dir, Name ); + SaveToFile( file ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + } + } + + public static ModCollection? Load( string name ) + { + var file = FileName( CollectionDir(), name ); + return file.Exists ? LoadFromFile( file ) : null; + } + + public void Delete() + { + var file = FileName( CollectionDir(), Name ); + if( file.Exists ) + { + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" ); + } + } + } + + public void AddMod( ModData data ) + { + if( Cache == null ) + { + return; + } + + Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings ) + ? settings + : ModSettings.DefaultSettings( data.Meta ), + data ); + } + + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) + => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); + + public static readonly ModCollection Empty = new() { Name = "" }; } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index c7d7cc1c..2dee1a90 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; -using Penumbra.Structs; using Penumbra.Util; namespace Penumbra.Mods; @@ -20,17 +19,16 @@ namespace Penumbra.Mods; public class ModCollectionCache { // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256); + private static readonly BitArray FileSeen = new(256); + private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256); public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); - public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly HashSet< ulong > Checksums = new(); - public readonly MetaManager MetaManipulations; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly HashSet< ulong > Checksums = new(); + public readonly MetaManager MetaManipulations; public IReadOnlyDictionary< string, object? > ChangedItems { @@ -61,7 +59,6 @@ public class ModCollectionCache public void CalculateEffectiveFileList() { ResolvedFiles.Clear(); - SwappedFiles.Clear(); MissingFiles.Clear(); RegisteredFiles.Clear(); _changedItems.Clear(); @@ -85,7 +82,7 @@ public class ModCollectionCache private void SetChangedItems() { - if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) + if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) { return; } @@ -98,12 +95,7 @@ public class ModCollectionCache var identifier = GameData.GameData.GetIdentifier(); foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) { - identifier.Identify( _changedItems, resolved ); - } - - foreach( var swapped in SwappedFiles.Keys ) - { - identifier.Identify( _changedItems, swapped ); + identifier.Identify( _changedItems, resolved.ToGamePath() ); } } catch( Exception e ) @@ -134,12 +126,12 @@ public class ModCollectionCache AddRemainingFiles( mod ); } - private bool FilterFile( GamePath gamePath ) + private static bool FilterFile( Utf8GamePath gamePath ) { // If audio streaming is not disabled, replacing .scd files crashes the game, // so only add those files if it is disabled. if( !Penumbra.Config.DisableSoundStreaming - && gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) ) + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) { return true; } @@ -148,7 +140,7 @@ public class ModCollectionCache } - private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) + private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file ) { if( FilterFile( gamePath ) ) { @@ -187,9 +179,8 @@ public class ModCollectionCache { foreach( var (file, paths) in option.OptionFiles ) { - var fullPath = new FullPath( mod.Data.BasePath, - NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + var fullPath = new FullPath( mod.Data.BasePath, file ); + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); if( idx < 0 ) { AddMissingFile( fullPath ); @@ -259,7 +250,7 @@ public class ModCollectionCache { if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) { - AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO + AddFile( mod, gamePath, file ); } else { @@ -294,7 +285,7 @@ public class ModCollectionCache if( !RegisteredFiles.TryGetValue( key, out var oldMod ) ) { RegisteredFiles.Add( key, mod ); - SwappedFiles.Add( key, value ); + ResolvedFiles.Add( key, value ); } else { @@ -341,54 +332,54 @@ public class ModCollectionCache public void RemoveMod( DirectoryInfo basePath ) { - if( AvailableMods.TryGetValue( basePath.Name, out var mod ) ) + if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) ) { - AvailableMods.Remove( basePath.Name ); - if( mod.Settings.Enabled ) - { - CalculateEffectiveFileList(); - if( mod.Data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } + return; + } + + AvailableMods.Remove( basePath.Name ); + if( !mod.Settings.Enabled ) + { + return; + } + + CalculateEffectiveFileList(); + if( mod.Data.Resources.MetaManipulations.Count > 0 ) + { + UpdateMetaManipulations(); } } - private class PriorityComparer : IComparer< Mod.Mod > - { - public int Compare( Mod.Mod? x, Mod.Mod? y ) - => ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 ); - } - - private static readonly PriorityComparer Comparer = new(); - public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) { - if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) ) + if( AvailableMods.ContainsKey( data.BasePath.Name ) ) { - var newMod = new Mod.Mod( settings, data ); - AvailableMods[ data.BasePath.Name ] = newMod; + return; + } - if( updateFileList && settings.Enabled ) - { - CalculateEffectiveFileList(); - if( data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } + AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data ); + + if( !updateFileList || !settings.Enabled ) + { + return; + } + + CalculateEffectiveFileList(); + if( data.Resources.MetaManipulations.Count > 0 ) + { + UpdateMetaManipulations(); } } - public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) + public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) { if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) { return null; } - if( candidate.FullName.Length >= 260 || !candidate.Exists ) + if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) { return null; } @@ -396,9 +387,6 @@ public class ModCollectionCache return candidate; } - public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) - => SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; - - public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null; + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) + => GetCandidateForGameFile( gameResourcePath ); } \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index b7ab6544..d0b7bf2d 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; @@ -347,15 +348,7 @@ namespace Penumbra.Mods return true; } - public bool CheckCrc64( ulong crc ) - { - if( Collections.ActiveCollection.Cache?.Checksums.Contains( crc ) ?? false ) - return true; - - return Collections.ForcedCollection.Cache?.Checksums.Contains( crc ) ?? false; - } - - public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) { var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index bca0e200..f6804d4f 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.IO; using Dalamud.Logging; using Penumbra.Mod; -using Penumbra.Structs; namespace Penumbra.Mods; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cabae47d..cfbb5f02 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -18,22 +18,6 @@ using System.Linq; namespace Penumbra; -public class Penumbra2 // : IDalamudPlugin -{ - public string Name - => "Penumbra"; - - private const string CommandName = "/penumbra"; - - public static Configuration Config { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; private set; } = null!; - - public void Dispose() - { - ResourceLoader.Dispose(); - } -} - public class Penumbra : IDalamudPlugin { public string Name @@ -54,6 +38,7 @@ public class Penumbra : IDalamudPlugin public ResourceLoader ResourceLoader { get; } + public ResourceLogger ResourceLogger { get; } //public PathResolver PathResolver { get; } public SettingsInterface SettingsInterface { get; } @@ -81,19 +66,18 @@ public class Penumbra : IDalamudPlugin CharacterUtility = new CharacterUtility(); MetaDefaults = new MetaDefaults(); ResourceLoader = new ResourceLoader( this ); + ResourceLogger = new ResourceLogger( ResourceLoader ); + PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); ModManager = new ModManager(); ModManager.DiscoverMods(); - //PathResolver = new PathResolver( ResourceLoader, gameUtils ); - PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); + //PathResolver = new PathResolver( ResourceLoader, gameUtils ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - ResourceLoader.EnableReplacements(); - ResourceLoader.EnableLogging(); if( Config.DebugMode ) { ResourceLoader.EnableDebug(); @@ -112,7 +96,7 @@ public class Penumbra : IDalamudPlugin CreateWebServer(); } - if( !Config.EnablePlayerWatch || !Config.IsEnabled ) + if( !Config.EnablePlayerWatch || !Config.EnableMods ) { PlayerWatcher.Disable(); } @@ -122,16 +106,25 @@ public class Penumbra : IDalamudPlugin PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); }; + + ResourceLoader.EnableHooks(); + if (Config.EnableMods) + ResourceLoader.EnableReplacements(); + if (Config.DebugMode) + ResourceLoader.EnableDebug(); + if (Config.EnableFullResourceLogging) + ResourceLoader.EnableFullLogging(); } public bool Enable() { - if( Config.IsEnabled ) + if( Config.EnableMods ) { return false; } - Config.IsEnabled = true; + Config.EnableMods = true; + ResourceLoader.EnableReplacements(); ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { @@ -145,12 +138,13 @@ public class Penumbra : IDalamudPlugin public bool Disable() { - if( !Config.IsEnabled ) + if( !Config.EnableMods ) { return false; } - Config.IsEnabled = false; + Config.EnableMods = false; + ResourceLoader.DisableReplacements(); ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { @@ -219,7 +213,9 @@ public class Penumbra : IDalamudPlugin Dalamud.Commands.RemoveHandler( CommandName ); //PathResolver.Dispose(); + ResourceLogger.Dispose(); ResourceLoader.Dispose(); + ShutdownWebServer(); } @@ -322,8 +318,8 @@ public class Penumbra : IDalamudPlugin } case "toggle": { - SetEnabled( !Config.IsEnabled ); - Dalamud.Chat.Print( Config.IsEnabled + SetEnabled( !Config.EnableMods ); + Dalamud.Chat.Print( Config.EnableMods ? modsEnabled : modsDisabled ); break; diff --git a/Penumbra/Structs/GroupInformation.cs b/Penumbra/Structs/GroupInformation.cs deleted file mode 100644 index f9681f11..00000000 --- a/Penumbra/Structs/GroupInformation.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using Newtonsoft.Json; -using Penumbra.GameData.Util; -using Penumbra.Util; - -namespace Penumbra.Structs -{ - public enum SelectType - { - Single, - Multi, - } - - public struct Option - { - public string OptionName; - public string OptionDesc; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )] - public Dictionary< RelPath, HashSet< GamePath > > OptionFiles; - - public bool AddFile( RelPath filePath, GamePath gamePath ) - { - if( OptionFiles.TryGetValue( filePath, out var set ) ) - { - return set.Add( gamePath ); - } - - OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath }; - return true; - } - } - - public struct OptionGroup - { - public string GroupName; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType; - - public List< Option > Options; - - private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) - { - // Selection contains the path, merge all GamePaths for this config. - if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - return true; - } - - // If the group contains the file in another selection, return true to skip it for default files. - for( var i = 0; i < Options.Count; ++i ) - { - if( i == selection ) - { - continue; - } - - if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - return true; - } - } - - return false; - } - - private bool ApplyMultiGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) - { - var doNotAdd = false; - for( var i = 0; i < Options.Count; ++i ) - { - if( ( selection & ( 1 << i ) ) != 0 ) - { - if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - doNotAdd = true; - } - } - else if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - doNotAdd = true; - } - } - - return doNotAdd; - } - - // Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist. - internal bool ApplyGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) - { - return SelectionType switch - { - SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ), - SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ), - _ => throw new InvalidEnumArgumentException( "Invalid option group type." ), - }; - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiUtil.cs b/Penumbra/UI/Custom/ImGuiUtil.cs index 42e9d8f2..f2082e8d 100644 --- a/Penumbra/UI/Custom/ImGuiUtil.cs +++ b/Penumbra/UI/Custom/ImGuiUtil.cs @@ -1,8 +1,8 @@ using System.Numerics; -using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; using Dalamud.Interface; using ImGuiNET; +using Penumbra.GameData.ByteString; namespace Penumbra.UI.Custom { @@ -20,6 +20,19 @@ namespace Penumbra.UI.Custom ImGui.SetTooltip( "Click to copy to clipboard." ); } } + + public static unsafe void CopyOnClickSelectable( Utf8String text ) + { + if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) + { + ImGuiNative.igSetClipboardText( text.Path ); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( "Click to copy to clipboard." ); + } + } } public static partial class ImGuiCustom diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index ff2606f8..83c157d3 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -21,11 +21,21 @@ public partial class SettingsInterface private string _filePathFilter = string.Empty; private string _filePathFilterLower = string.Empty; - private readonly float _leftTextLength = - ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; + private const float LeftTextLength = 600; private float _arrowLength = 0; + private static void DrawLine( Utf8GamePath path, FullPath name ) + { + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( path.Path ); + + ImGui.TableNextColumn(); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.SameLine(); + ImGuiCustom.CopyOnClickSelectable( name.InternalName ); + } + private static void DrawLine( string path, string name ) { ImGui.TableNextColumn(); @@ -45,13 +55,13 @@ public partial class SettingsInterface _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; } - ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale ); if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) { _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); } - ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); ImGui.SetNextItemWidth( -1 ); if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) { @@ -59,7 +69,7 @@ public partial class SettingsInterface } } - private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) + private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) { if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) { @@ -69,7 +79,7 @@ public partial class SettingsInterface return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); } - private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp ) { if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) { @@ -94,11 +104,6 @@ public partial class SettingsInterface void DrawFileLines( ModCollectionCache cache ) { foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) - { - DrawLine( gp, fp.FullName ); - } - - foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) { DrawLine( gp, fp ); } @@ -139,75 +144,67 @@ public partial class SettingsInterface var activeCollection = modManager.Collections.ActiveCollection.Cache; var forcedCollection = modManager.Collections.ForcedCollection.Cache; - var (activeResolved, activeSwap, activeMeta) = activeCollection != null - ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null - ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; + var (activeResolved, activeMeta) = activeCollection != null + ? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count ) + : ( 0, 0 ); + var (forcedResolved, forcedMeta) = forcedCollection != null + ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count ) + : ( 0, 0 ); + var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta; if( totalLines == 0 ) { return; } - if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) + if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) { - raii.Push( ImGui.EndTable ); - ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); + return; + } - if( _filePathFilter.Any() || _gamePathFilter.Any() ) + raii.Push( ImGui.EndTable ); + ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, LeftTextLength * ImGuiHelpers.GlobalScale ); + + if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 ) + { + DrawFilteredRows( activeCollection, forcedCollection ); + } + else + { + ImGuiListClipperPtr clipper; + unsafe { - DrawFilteredRows( activeCollection, forcedCollection ); + clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); } - else + + clipper.Begin( totalLines ); + + + while( clipper.Step() ) { - ImGuiListClipperPtr clipper; - unsafe + for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) { - clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); - } - - clipper.Begin( totalLines ); - - - while( clipper.Step() ) - { - for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) + var row = actualRow; + ImGui.TableNextRow(); + if( row < activeResolved ) { - var row = actualRow; - ImGui.TableNextRow(); - if( row < activeResolved ) - { - var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= activeResolved ) < activeSwap ) - { - var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else if( ( row -= activeSwap ) < activeMeta ) - { - var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } - else if( ( row -= activeMeta ) < forcedResolved ) - { - var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= forcedResolved ) < forcedSwap ) - { - var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else - { - row -= forcedSwap; - var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } + var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file ); + } + else if( ( row -= activeResolved ) < activeMeta ) + { + var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + } + else if( ( row -= activeMeta ) < forcedResolved ) + { + var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file ); + } + else + { + row -= forcedResolved; + var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index cabba206..e2d0a710 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -11,7 +11,6 @@ using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; using Penumbra.Mods; -using Penumbra.Structs; using Penumbra.UI.Custom; using Penumbra.Util; using ImGui = ImGuiNET.ImGui; @@ -56,7 +55,7 @@ public partial class SettingsInterface private Option? _selectedOption; private string _currentGamePaths = ""; - private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; + private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList; private readonly Selector _selector; private readonly SettingsInterface _base; @@ -218,7 +217,10 @@ public partial class SettingsInterface indent.Push( 15f ); foreach( var file in files ) { - ImGui.Selectable( file ); + unsafe + { + ImGuiNative.igSelectable_Bool( file.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); + } } foreach( var manip in manipulations ) @@ -258,13 +260,13 @@ public partial class SettingsInterface foreach( var (source, target) in Meta.FileSwaps ) { ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( source ); + ImGuiCustom.CopyOnClickSelectable( source.Path ); ImGui.TableNextColumn(); ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( target ); + ImGuiCustom.CopyOnClickSelectable( target.InternalName ); ImGui.TableNextRow(); } @@ -278,7 +280,8 @@ public partial class SettingsInterface } _fullFilenameList = Mod.Data.Resources.ModFiles - .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); + .Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) + .ToArray(); if( Meta.Groups.Count == 0 ) { @@ -339,24 +342,23 @@ public partial class SettingsInterface } } - private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) + private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders ) { removeFolders = 0; - var defaultIndex = - gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); + var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) ); if( defaultIndex < 0 ) { return defaultIndex; } - string path = gamePaths[ defaultIndex ]; + var path = gamePaths[ defaultIndex ].Path; if( path.Length == TextDefaultGamePath.Length ) { return defaultIndex; } - if( path[ TextDefaultGamePath.Length ] != '-' - || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) + if( path[ TextDefaultGamePath.Length ] != ( byte )'-' + || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) ) { return -1; } @@ -373,8 +375,9 @@ public partial class SettingsInterface var option = ( Option )_selectedOption; - var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); - if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) + var gamePaths = _currentGamePaths.Split( ';' ) + .Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray(); + if( gamePaths.Length == 0 ) { return; } @@ -517,18 +520,18 @@ public partial class SettingsInterface { Selectable( 0, ColorGreen ); - using var indent = ImGuiRaii.PushIndent( indentWidth ); - var tmpPaths = gamePaths.ToArray(); - foreach( var gamePath in tmpPaths ) + using var indent = ImGuiRaii.PushIndent( indentWidth ); + foreach( var gamePath in gamePaths.ToArray() ) { - string tmp = gamePath; + var tmp = gamePath.ToString(); + var old = tmp; if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) - && tmp != gamePath ) + && tmp != old ) { gamePaths.Remove( gamePath ); - if( tmp.Length > 0 ) + if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) ) { - gamePaths.Add( new GamePath( tmp ) ); + gamePaths.Add( p ); } else if( gamePaths.Count == 0 ) { diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 3c8a5feb..3329a554 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -3,136 +3,179 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; +using Penumbra.Mod; using Penumbra.Mods; -using Penumbra.Structs; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private partial class PluginDetails { - private partial class PluginDetails + private const string LabelDescEdit = "##descedit"; + private const string LabelNewSingleGroupEdit = "##newSingleGroup"; + private const string LabelNewMultiGroup = "##newMultiGroup"; + private const string LabelGamePathsEditBox = "##gamePathsEdit"; + private const string ButtonAddToGroup = "Add to Group"; + private const string ButtonRemoveFromGroup = "Remove from Group"; + private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; + private const string TextNoOptionAvailable = "[Not Available]"; + private const string TextDefaultGamePath = "default"; + private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true ); + private const char GamePathsSeparator = ';'; + + private static readonly string TooltipFilesTabEdit = + $"{TooltipFilesTab}\n" + + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; + + private static readonly string TooltipGamePathsEdit = + $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" + + $"Use '{TextDefaultGamePath}' to add the original file path." + + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; + + private const float MultiEditBoxWidth = 300f; + + private bool DrawEditGroupSelector() { - private const string LabelDescEdit = "##descedit"; - private const string LabelNewSingleGroupEdit = "##newSingleGroup"; - private const string LabelNewMultiGroup = "##newMultiGroup"; - private const string LabelGamePathsEditBox = "##gamePathsEdit"; - private const string ButtonAddToGroup = "Add to Group"; - private const string ButtonRemoveFromGroup = "Remove from Group"; - private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; - private const string TextNoOptionAvailable = "[Not Available]"; - private const string TextDefaultGamePath = "default"; - private const char GamePathsSeparator = ';'; - - private static readonly string TooltipFilesTabEdit = - $"{TooltipFilesTab}\n" - + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; - - private static readonly string TooltipGamePathsEdit = - $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" - + $"Use '{TextDefaultGamePath}' to add the original file path." - + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; - - private const float MultiEditBoxWidth = 300f; - - private bool DrawEditGroupSelector() + ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); + if( Meta!.Groups.Count == 0 ) { - ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); - if( Meta!.Groups.Count == 0 ) - { - ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 ); - return false; - } - - if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex - , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() - , Meta.Groups.Count ) ) - { - SelectGroup(); - SelectOption( 0 ); - } - - return true; + ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 ); + return false; } - private bool DrawEditOptionSelector() + if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex + , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() + , Meta.Groups.Count ) ) { + SelectGroup(); + SelectOption( 0 ); + } + + return true; + } + + private bool DrawEditOptionSelector() + { + ImGui.SameLine(); + ImGui.SetNextItemWidth( OptionSelectionWidth ); + if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 ) + { + ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); + return false; + } + + var group = ( OptionGroup )_selectedGroup!; + if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), + group.Options.Count ) ) + { + SelectOption(); + } + + return true; + } + + private void DrawFileListTabEdit() + { + if( ImGui.BeginTabItem( LabelFileListTab ) ) + { + UpdateFilenameList(); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); + } + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) ) + { + for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) + { + DrawFileAndGamePaths( i ); + } + } + + ImGui.EndListBox(); + + DrawGroupRow(); + ImGui.EndTabItem(); + } + else + { + _fullFilenameList = null; + } + } + + private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group ) + { + var groupName = group.GroupName; + if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) + { + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.TriggerFilterReset(); + } + } + + return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); + } + + private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) + { + var newOption = ""; + ImGui.SetCursorPosX( nameBoxStart ); + ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); + if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) + && newOption.Length != 0 ) + { + group.Options.Add( new Option() + { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } ); + _selector.SaveCurrentMod(); + if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.TriggerFilterReset(); + } + } + } + + private void DrawMultiSelectorEdit( OptionGroup group ) + { + var nameBoxStart = CheckMarkSize; + var flag = Mod!.Settings.Settings[ group.GroupName ]; + + using var raii = DrawMultiSelectorEditBegin( group ); + for( var i = 0; i < group.Options.Count; ++i ) + { + var opt = group.Options[ i ]; + var label = $"##{group.GroupName}_{i}"; + DrawMultiSelectorCheckBox( group, i, flag, label ); + ImGui.SameLine(); - ImGui.SetNextItemWidth( OptionSelectionWidth ); - if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 ) + var newName = opt.OptionName; + + if( nameBoxStart == CheckMarkSize ) { - ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); - return false; + nameBoxStart = ImGui.GetCursorPosX(); } - var group = ( OptionGroup )_selectedGroup!; - if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), - group.Options.Count ) ) - { - SelectOption(); - } - - return true; - } - - private void DrawFileListTabEdit() - { - if( ImGui.BeginTabItem( LabelFileListTab ) ) - { - UpdateFilenameList(); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); - } - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) ) - { - for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) - { - DrawFileAndGamePaths( i ); - } - } - - ImGui.EndListBox(); - - DrawGroupRow(); - ImGui.EndTabItem(); - } - else - { - _fullFilenameList = null; - } - } - - private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group ) - { - var groupName = group.GroupName; - if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) - { - if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - - return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); - } - - private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) - { - var newOption = ""; - ImGui.SetCursorPosX( nameBoxStart ); ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) - && newOption.Length != 0 ) + if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - group.Options.Add( new Option() - { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } ); - _selector.SaveCurrentMod(); + if( newName.Length == 0 ) + { + Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); + } + else if( newName != opt.OptionName ) + { + group.Options[ i ] = new Option() + { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; + _selector.SaveCurrentMod(); + } + if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) { _selector.Cache.TriggerFilterReset(); @@ -140,244 +183,201 @@ namespace Penumbra.UI } } - private void DrawMultiSelectorEdit( OptionGroup group ) + DrawMultiSelectorEditAdd( group, nameBoxStart ); + } + + private void DrawSingleSelectorEditGroup( OptionGroup group ) + { + var groupName = group.GroupName; + if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - var nameBoxStart = CheckMarkSize; - var flag = Mod!.Settings.Settings[ group.GroupName ]; - - using var raii = DrawMultiSelectorEditBegin( group ); - for( var i = 0; i < group.Options.Count; ++i ) + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { - var opt = group.Options[ i ]; - var label = $"##{group.GroupName}_{i}"; - DrawMultiSelectorCheckBox( group, i, flag, label ); - - ImGui.SameLine(); - var newName = opt.OptionName; - - if( nameBoxStart == CheckMarkSize ) - { - nameBoxStart = ImGui.GetCursorPosX(); - } - - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - if( newName.Length == 0 ) - { - Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); - } - else if( newName != opt.OptionName ) - { - group.Options[ i ] = new Option() - { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; - _selector.SaveCurrentMod(); - } - - if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - DrawMultiSelectorEditAdd( group, nameBoxStart ); - } - - private void DrawSingleSelectorEditGroup( OptionGroup group ) - { - var groupName = group.GroupName; - if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } + _selector.Cache.TriggerFilterReset(); } } + } - private float DrawSingleSelectorEdit( OptionGroup group ) + private float DrawSingleSelectorEdit( OptionGroup group ) + { + var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; + var code = oldSetting; + if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, + group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) { - var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; - var code = oldSetting; - if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, - group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) + if( code == group.Options.Count ) { - if( code == group.Options.Count ) + if( newName.Length > 0 ) { - if( newName.Length > 0 ) + Mod.Settings.Settings[ group.GroupName ] = code; + group.Options.Add( new Option() { - Mod.Settings.Settings[ group.GroupName ] = code; - group.Options.Add( new Option() - { - OptionName = newName, - OptionDesc = "", - OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), - } ); - _selector.SaveCurrentMod(); - } + OptionName = newName, + OptionDesc = "", + OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), + } ); + _selector.SaveCurrentMod(); + } + } + else + { + if( newName.Length == 0 ) + { + Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); } else { - if( newName.Length == 0 ) + if( newName != group.Options[ code ].OptionName ) { - Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); - } - else - { - if( newName != group.Options[ code ].OptionName ) + group.Options[ code ] = new Option() { - group.Options[ code ] = new Option() - { - OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, - OptionFiles = group.Options[ code ].OptionFiles, - }; - _selector.SaveCurrentMod(); - } + OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, + OptionFiles = group.Options[ code ].OptionFiles, + }; + _selector.SaveCurrentMod(); } } - - if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } } - if( code != oldSetting ) + if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { - Save(); + _selector.Cache.TriggerFilterReset(); + } + } + + if( code != oldSetting ) + { + Save(); + } + + ImGui.SameLine(); + var labelEditPos = ImGui.GetCursorPosX(); + DrawSingleSelectorEditGroup( group ); + + return labelEditPos; + } + + private void DrawAddSingleGroupField( float labelEditPos ) + { + var newGroup = ""; + ImGui.SetCursorPosX( labelEditPos ); + if( labelEditPos == CheckMarkSize ) + { + ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); + } + + if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); + // Adds empty group, so can not change filters. + } + } + + private void DrawAddMultiGroupField() + { + var newGroup = ""; + ImGui.SetCursorPosX( CheckMarkSize ); + ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); + if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); + // Adds empty group, so can not change filters. + } + } + + private void DrawGroupSelectorsEdit() + { + var labelEditPos = CheckMarkSize; + var groups = Meta.Groups.Values.ToArray(); + foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) + { + labelEditPos = DrawSingleSelectorEdit( g ); + } + + DrawAddSingleGroupField( labelEditPos ); + + foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) + { + DrawMultiSelectorEdit( g ); + } + + DrawAddMultiGroupField(); + } + + private void DrawFileSwapTabEdit() + { + if( !ImGui.BeginTabItem( LabelFileSwapTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + + var swaps = Meta.FileSwaps.Keys.ToArray(); + + ImGui.PushFont( UiBuilder.IconFont ); + var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X; + ImGui.PopFont(); + + var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; + for( var idx = 0; idx < swaps.Length + 1; ++idx ) + { + var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ]; + var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ]; + var keyString = key.ToString(); + var valueString = value.ToString(); + + ImGui.SetNextItemWidth( width ); + if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString, + GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 ) + { + if( idx < swaps.Length ) + { + Meta.FileSwaps.Remove( key ); + } + + if( !newKey.IsEmpty ) + { + Meta.FileSwaps[ newKey ] = value; + } + + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod(); + } + } + + if( idx >= swaps.Length ) + { + continue; } ImGui.SameLine(); - var labelEditPos = ImGui.GetCursorPosX(); - DrawSingleSelectorEditGroup( group ); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); + ImGui.SameLine(); - return labelEditPos; - } - - private void DrawAddSingleGroupField( float labelEditPos ) - { - var newGroup = ""; - ImGui.SetCursorPosX( labelEditPos ); - if( labelEditPos == CheckMarkSize ) + ImGui.SetNextItemWidth( width ); + if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString, + GamePath.MaxGamePathLength, + ImGuiInputTextFlags.EnterReturnsTrue ) ) { - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - } - - if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); - // Adds empty group, so can not change filters. - } - } - - private void DrawAddMultiGroupField() - { - var newGroup = ""; - ImGui.SetCursorPosX( CheckMarkSize ); - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); - // Adds empty group, so can not change filters. - } - } - - private void DrawGroupSelectorsEdit() - { - var labelEditPos = CheckMarkSize; - var groups = Meta.Groups.Values.ToArray(); - foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) - { - labelEditPos = DrawSingleSelectorEdit( g ); - } - - DrawAddSingleGroupField( labelEditPos ); - - foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) - { - DrawMultiSelectorEdit( g ); - } - - DrawAddMultiGroupField(); - } - - private void DrawFileSwapTabEdit() - { - if( !ImGui.BeginTabItem( LabelFileSwapTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - - var swaps = Meta.FileSwaps.Keys.ToArray(); - - ImGui.PushFont( UiBuilder.IconFont ); - var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X; - ImGui.PopFont(); - - var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; - for( var idx = 0; idx < swaps.Length + 1; ++idx ) - { - var key = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : swaps[ idx ]; - var value = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : Meta.FileSwaps[ key ]; - string keyString = key; - string valueString = value; - - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString, - GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) ) + var newValue = new FullPath( valueString.ToLowerInvariant() ); + if( newValue.CompareTo( value ) != 0 ) { - var newKey = new GamePath( keyString ); - if( newKey.CompareTo( key ) != 0 ) - { - if( idx < swaps.Length ) - { - Meta.FileSwaps.Remove( key ); - } - - if( newKey != string.Empty ) - { - Meta.FileSwaps[ newKey ] = value; - } - - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); - } - } - - if( idx >= swaps.Length ) - { - continue; - } - - ImGui.SameLine(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); - ImGui.SameLine(); - - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString, - GamePath.MaxGamePathLength, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - var newValue = new GamePath( valueString ); - if( newValue.CompareTo( value ) != 0 ) - { - Meta.FileSwaps[ key ] = newValue; - _selector.SaveCurrentMod(); - _selector.Cache.TriggerListReset(); - } + Meta.FileSwaps[ key ] = newValue; + _selector.SaveCurrentMod(); + _selector.Cache.TriggerListReset(); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index a435a6f2..e6bfdea5 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -488,7 +488,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) + if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) ) { ImGui.CloseCurrentPopup(); } @@ -509,7 +509,7 @@ public partial class SettingsInterface { var change = false; var metaManips = false; - foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) + foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) ) { var (mod, _, _) = Cache.GetMod( currentIdx++ ); if( mod != null ) diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 884a99fa..8e1ba624 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -3,8 +3,10 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; +using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Logging; using ImGuiNET; using Penumbra.GameData.ByteString; using Penumbra.Interop; @@ -131,7 +133,7 @@ public partial class SettingsInterface private void DrawEnabledBox() { - var enabled = _config.IsEnabled; + var enabled = _config.EnableMods; if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) { _base._penumbra.SetEnabled( enabled ); @@ -317,14 +319,84 @@ public partial class SettingsInterface + "You usually should not need to do this." ); } + private void DrawEnableFullResourceLoggingBox() + { + var tmp = _config.EnableFullResourceLogging; + if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != _config.EnableFullResourceLogging ) + { + if( tmp ) + { + _base._penumbra.ResourceLoader.EnableFullLogging(); + } + else + { + _base._penumbra.ResourceLoader.DisableFullLogging(); + } + + _config.EnableFullResourceLogging = tmp; + _configChanged = true; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); + } + + private void DrawEnableDebugModeBox() + { + var tmp = _config.DebugMode; + if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != _config.DebugMode ) + { + if( tmp ) + { + _base._penumbra.ResourceLoader.EnableDebug(); + } + else + { + _base._penumbra.ResourceLoader.DisableDebug(); + } + + _config.DebugMode = tmp; + _configChanged = true; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." ); + } + + private void DrawRequestedResourceLogging() + { + var tmp = _config.EnableResourceLogging; + if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) ) + { + _base._penumbra.ResourceLogger.SetState( tmp ); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n" + + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" + + "Red boundary indicates invalid regex." ); + ImGui.SameLine(); + var tmpString = Penumbra.Config.ResourceLoggingFilter; + using var color = ImGuiRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_base._penumbra.ResourceLogger.ValidRegex ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, + !_base._penumbra.ResourceLogger.ValidRegex ); + if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) + { + _base._penumbra.ResourceLogger.SetFilter( tmpString ); + } + } + private void DrawAdvancedSettings() { DrawTempFolder(); + DrawRequestedResourceLogging(); DrawDisableSoundStreamingBox(); DrawLogLoadedFilesBox(); DrawDisableNotificationsBox(); DrawEnableHttpApiBox(); DrawReloadResourceButton(); + DrawEnableDebugModeBox(); + DrawEnableFullResourceLoggingBox(); } public static unsafe void Text( Utf8String s ) diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index 1bb8628e..cee41088 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -1,9 +1,7 @@ using System.Numerics; using Dalamud.Interface; using ImGuiNET; -using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; namespace Penumbra.UI; diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index a69a8296..3d730f57 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -5,36 +5,35 @@ using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.UI.Custom; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) { - internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0) + var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; + + if( ret != MouseButton.None ) { - var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; + _penumbra.Api.InvokeClick( ret, data ); + } - if( ret != MouseButton.None ) - { - _penumbra.Api.InvokeClick( ret, data ); - } + if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) + { + ImGui.BeginTooltip(); + using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); + _penumbra.Api.InvokeTooltip( data ); + } - if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) - { - ImGui.BeginTooltip(); - using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); - _penumbra.Api.InvokeTooltip( data ); - } + if( data is Item it ) + { + var modelId = $"({( ( Quad )it.ModelMain ).A})"; + var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset; - if( data is Item it ) - { - var modelId = $"({( ( Quad )it.ModelMain ).A})"; - var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset; - - ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); - ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); - } + ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); + ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); } } } \ No newline at end of file diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index 8308890e..14b07748 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -1,87 +1,33 @@ using System; using System.Collections.Generic; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class ArrayExtensions { - public static class ArrayExtensions + public static int IndexOf< T >( this T[] array, Predicate< T > match ) { - public static T[] Slice< T >( this T[] source, int index, int length ) + for( var i = 0; i < array.Length; ++i ) { - var slice = new T[length]; - Array.Copy( source, index * length, slice, 0, length ); - return slice; - } - - public static void Swap< T >( this T[] array, int idx1, int idx2 ) - { - var tmp = array[ idx1 ]; - array[ idx1 ] = array[ idx2 ]; - array[ idx2 ] = tmp; - } - - public static void Swap< T >( this List< T > array, int idx1, int idx2 ) - { - var tmp = array[ idx1 ]; - array[ idx1 ] = array[ idx2 ]; - array[ idx2 ] = tmp; - } - - public static int IndexOf< T >( this T[] array, Predicate< T > match ) - { - for( var i = 0; i < array.Length; ++i ) + if( match( array[ i ] ) ) { - if( match( array[ i ] ) ) - { - return i; - } + return i; } - - return -1; } - public static void Swap< T >( this T[] array, T lhs, T rhs ) + return -1; + } + + public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate ) + { + for( var i = 0; i < array.Count; ++i ) { - var idx1 = Array.IndexOf( array, lhs ); - if( idx1 < 0 ) + if( predicate.Invoke( array[ i ] ) ) { - return; + return i; } - - var idx2 = Array.IndexOf( array, rhs ); - if( idx2 < 0 ) - { - return; - } - - array.Swap( idx1, idx2 ); } - public static void Swap< T >( this List< T > array, T lhs, T rhs ) - { - var idx1 = array.IndexOf( lhs ); - if( idx1 < 0 ) - { - return; - } - - var idx2 = array.IndexOf( rhs ); - if( idx2 < 0 ) - { - return; - } - - array.Swap( idx1, idx2 ); - } - - public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate ) - { - for( var i = 0; i < array.Count; ++i ) - { - if( predicate.Invoke( array[ i ] ) ) - return i; - } - - return -1; - } + return -1; } } \ No newline at end of file diff --git a/Penumbra/Util/BinaryReaderExtensions.cs b/Penumbra/Util/BinaryReaderExtensions.cs index 19be89ca..dec2d44d 100644 --- a/Penumbra/Util/BinaryReaderExtensions.cs +++ b/Penumbra/Util/BinaryReaderExtensions.cs @@ -3,135 +3,54 @@ using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class BinaryReaderExtensions { - public static class BinaryReaderExtensions + /// + /// Reads a structure from the current stream position. + /// + /// + /// The structure to read in to + /// The file data as a structure + public static T ReadStructure< T >( this BinaryReader br ) where T : struct { - /// - /// Reads a structure from the current stream position. - /// - /// - /// The structure to read in to - /// The file data as a structure - public static T ReadStructure< T >( this BinaryReader br ) where T : struct - { - ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() ); + ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() ); - return MemoryMarshal.Read< T >( data ); + return MemoryMarshal.Read< T >( data ); + } + + /// + /// Reads many structures from the current stream position. + /// + /// + /// The number of T to read from the stream + /// The structure to read in to + /// A list containing the structures read from the stream + public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct + { + var size = Marshal.SizeOf< T >(); + var data = br.ReadBytes( size * count ); + + var list = new List< T >( count ); + + for( var i = 0; i < count; i++ ) + { + var offset = size * i; + var span = new ReadOnlySpan< byte >( data, offset, size ); + + list.Add( MemoryMarshal.Read< T >( span ) ); } - /// - /// Reads many structures from the current stream position. - /// - /// - /// The number of T to read from the stream - /// The structure to read in to - /// A list containing the structures read from the stream - public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct - { - var size = Marshal.SizeOf< T >(); - var data = br.ReadBytes( size * count ); + return list; + } - var list = new List< T >( count ); - - for( var i = 0; i < count; i++ ) - { - var offset = size * i; - var span = new ReadOnlySpan< byte >( data, offset, size ); - - list.Add( MemoryMarshal.Read< T >( span ) ); - } - - return list; - } - - public static T[] ReadStructuresAsArray< T >( this BinaryReader br, int count ) where T : struct - { - var size = Marshal.SizeOf< T >(); - var data = br.ReadBytes( size * count ); - - // im a pirate arr - var arr = new T[count]; - - for( var i = 0; i < count; i++ ) - { - var offset = size * i; - var span = new ReadOnlySpan< byte >( data, offset, size ); - - arr[ i ] = MemoryMarshal.Read< T >( span ); - } - - return arr; - } - - /// - /// Moves the BinaryReader position to offset, reads a string, then - /// sets the reader position back to where it was when it started - /// - /// - /// The offset to read a string starting from. - /// - public static string ReadStringOffsetData( this BinaryReader br, long offset ) - => Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) ); - - /// - /// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then - /// sets the reader position back to where it was when it started - /// - /// - /// The offset to read data starting from. - /// - public static byte[] ReadRawOffsetData( this BinaryReader br, long offset ) - { - var originalPosition = br.BaseStream.Position; - br.BaseStream.Position = offset; - - var chars = new List< byte >(); - - byte current; - while( ( current = br.ReadByte() ) != 0 ) - { - chars.Add( current ); - } - - br.BaseStream.Position = originalPosition; - - return chars.ToArray(); - } - - /// - /// Seeks this BinaryReader's position to the given offset. Syntactic sugar. - /// - public static void Seek( this BinaryReader br, long offset ) - { - br.BaseStream.Position = offset; - } - - /// - /// Reads a byte and moves the stream position back to where it started before the operation - /// - /// The reader to use to read the byte - /// The byte that was read - public static byte PeekByte( this BinaryReader br ) - { - var data = br.ReadByte(); - br.BaseStream.Position--; - return data; - } - - /// - /// Reads bytes and moves the stream position back to where it started before the operation - /// - /// The reader to use to read the bytes - /// The number of bytes to read - /// The read bytes - public static byte[] PeekBytes( this BinaryReader br, int count ) - { - var data = br.ReadBytes( count ); - br.BaseStream.Position -= count; - return data; - } + /// + /// Seeks this BinaryReader's position to the given offset. Syntactic sugar. + /// + public static void Seek( this BinaryReader br, long offset ) + { + br.BaseStream.Position = offset; } } \ No newline at end of file diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs index 9bd2fc52..0b500f17 100644 --- a/Penumbra/Util/ChatUtil.cs +++ b/Penumbra/Util/ChatUtil.cs @@ -2,36 +2,34 @@ using System.Collections.Generic; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class ChatUtil { - public static class ChatUtil + public static void LinkItem( Item item ) { - public static void LinkItem( Item item ) + var payloadList = new List< Payload > { - var payloadList = new List< Payload > - { - new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ), - new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ), - new ItemPayload( item.RowId, false ), - new UIForegroundPayload( 500 ), - new UIGlowPayload( 501 ), - new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), - new UIForegroundPayload( 0 ), - new UIGlowPayload( 0 ), - new TextPayload( item.Name ), - new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), - new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ), - }; + new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ), + new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ), + new ItemPayload( item.RowId, false ), + new UIForegroundPayload( 500 ), + new UIGlowPayload( 501 ), + new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), + new UIForegroundPayload( 0 ), + new UIGlowPayload( 0 ), + new TextPayload( item.Name ), + new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), + new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ), + }; - var payload = new SeString( payloadList ); + var payload = new SeString( payloadList ); - Dalamud.Chat.PrintChat( new XivChatEntry - { - Message = payload, - } ); - } + Dalamud.Chat.PrintChat( new XivChatEntry + { + Message = payload, + } ); } } \ No newline at end of file diff --git a/Penumbra/Util/Crc32.cs b/Penumbra/Util/Crc32.cs deleted file mode 100644 index 655d18e2..00000000 --- a/Penumbra/Util/Crc32.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Penumbra.Util -{ - /// - /// Performs the 32-bit reversed variant of the cyclic redundancy check algorithm - /// - public class Crc32 - { - private const uint Poly = 0xedb88320; - - private static readonly uint[] CrcArray = - Enumerable.Range( 0, 256 ).Select( i => - { - var k = ( uint )i; - for( var j = 0; j < 8; j++ ) - { - k = ( k & 1 ) != 0 ? ( k >> 1 ) ^ Poly : k >> 1; - } - - return k; - } ).ToArray(); - - public uint Checksum - => ~_crc32; - - private uint _crc32 = 0xFFFFFFFF; - - /// - /// Initializes Crc32's state - /// - public void Init() - { - _crc32 = 0xFFFFFFFF; - } - - /// - /// Updates Crc32's state with new data - /// - /// Data to calculate the new CRC from - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public void Update( byte[] data ) - { - foreach( var b in data ) - { - Update( b ); - } - } - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public void Update( byte b ) - { - _crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ ( ( _crc32 >> 8 ) & 0x00FFFFFF ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/DialogExtensions.cs b/Penumbra/Util/DialogExtensions.cs index eb9c166c..33df7bfc 100644 --- a/Penumbra/Util/DialogExtensions.cs +++ b/Penumbra/Util/DialogExtensions.cs @@ -5,78 +5,77 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class DialogExtensions { - public static class DialogExtensions + public static Task< DialogResult > ShowDialogAsync( this CommonDialog form ) { - public static Task< DialogResult > ShowDialogAsync( this CommonDialog form ) + using var process = Process.GetCurrentProcess(); + return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) ); + } + + public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner ) + { + var taskSource = new TaskCompletionSource< DialogResult >(); + var th = new Thread( () => DialogThread( form, owner, taskSource ) ); + th.Start(); + return taskSource.Task; + } + + [STAThread] + private static void DialogThread( CommonDialog form, IWin32Window owner, + TaskCompletionSource< DialogResult > taskSource ) + { + Application.SetCompatibleTextRenderingDefault( false ); + Application.EnableVisualStyles(); + using var hiddenForm = new HiddenForm( form, owner, taskSource ); + Application.Run( hiddenForm ); + Application.ExitThread(); + } + + public class DialogHandle : IWin32Window + { + public IntPtr Handle { get; set; } + + public DialogHandle( IntPtr handle ) + => Handle = handle; + } + + public class HiddenForm : Form + { + private readonly CommonDialog _form; + private readonly IWin32Window _owner; + private readonly TaskCompletionSource< DialogResult > _taskSource; + + public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource ) { - using var process = Process.GetCurrentProcess(); - return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) ); + _form = form; + _owner = owner; + _taskSource = taskSource; + + Opacity = 0; + FormBorderStyle = FormBorderStyle.None; + ShowInTaskbar = false; + Size = new Size( 0, 0 ); + + Shown += HiddenForm_Shown; } - public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner ) + private void HiddenForm_Shown( object? sender, EventArgs _ ) { - var taskSource = new TaskCompletionSource< DialogResult >(); - var th = new Thread( () => DialogThread( form, owner, taskSource ) ); - th.Start(); - return taskSource.Task; - } - - [STAThread] - private static void DialogThread( CommonDialog form, IWin32Window owner, - TaskCompletionSource< DialogResult > taskSource ) - { - Application.SetCompatibleTextRenderingDefault( false ); - Application.EnableVisualStyles(); - using var hiddenForm = new HiddenForm( form, owner, taskSource ); - Application.Run( hiddenForm ); - Application.ExitThread(); - } - - public class DialogHandle : IWin32Window - { - public IntPtr Handle { get; set; } - - public DialogHandle( IntPtr handle ) - => Handle = handle; - } - - public class HiddenForm : Form - { - private readonly CommonDialog _form; - private readonly IWin32Window _owner; - private readonly TaskCompletionSource< DialogResult > _taskSource; - - public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource ) + Hide(); + try { - _form = form; - _owner = owner; - _taskSource = taskSource; - - Opacity = 0; - FormBorderStyle = FormBorderStyle.None; - ShowInTaskbar = false; - Size = new Size( 0, 0 ); - - Shown += HiddenForm_Shown; + var result = _form.ShowDialog( _owner ); + _taskSource.SetResult( result ); + } + catch( Exception e ) + { + _taskSource.SetException( e ); } - private void HiddenForm_Shown( object? sender, EventArgs _ ) - { - Hide(); - try - { - var result = _form.ShowDialog( _owner ); - _taskSource.SetResult( result ); - } - catch( Exception e ) - { - _taskSource.SetException( e ); - } - - Close(); - } + Close(); } } } \ No newline at end of file diff --git a/Penumbra/Util/GeneralUtil.cs b/Penumbra/Util/GeneralUtil.cs deleted file mode 100644 index 698609db..00000000 --- a/Penumbra/Util/GeneralUtil.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Dalamud.Logging; - -namespace Penumbra.Util -{ - public static class GeneralUtil - { - public static void PrintDebugAddress( string name, IntPtr address ) - { - var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64(); - PluginLog.Debug( "{Name} found at 0x{Address:X16}, +0x{Offset:X}", name, address.ToInt64(), address.ToInt64() - module ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/MemoryStreamExtensions.cs b/Penumbra/Util/MemoryStreamExtensions.cs deleted file mode 100644 index 5d4c6235..00000000 --- a/Penumbra/Util/MemoryStreamExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.IO; - -namespace Penumbra.Util -{ - public static class MemoryStreamExtensions - { - public static void Write( this MemoryStream stream, byte[] data ) - { - stream.Write( data, 0, data.Length ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index de6d9ec8..6b2378f0 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -16,7 +16,6 @@ public static class ModelChanger public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); - public static bool ValidStrings( string from, string to ) => from.Length != 0 && to.Length != 0 @@ -40,8 +39,8 @@ public static class ModelChanger try { - var data = File.ReadAllBytes( file.FullName ); - var mdlFile = new MdlFile( data ); + var data = File.ReadAllBytes( file.FullName ); + var mdlFile = new MdlFile( data ); Func< string, bool > compare = MaterialRegex.IsMatch; if( from.Length > 0 ) { @@ -53,9 +52,9 @@ public static class ModelChanger var replaced = 0; for( var i = 0; i < mdlFile.Materials.Length; ++i ) { - if( compare(mdlFile.Materials[i]) ) + if( compare( mdlFile.Materials[ i ] ) ) { - mdlFile.Materials[i] = to; + mdlFile.Materials[ i ] = to; ++replaced; } } diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index a85e6574..8e26d45b 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -3,432 +3,405 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; -using System.Text; -using Lumina; using Lumina.Data.Structs; -namespace Penumbra.Util +namespace Penumbra.Util; + +public class PenumbraSqPackStream : IDisposable { - public class PenumbraSqPackStream : IDisposable + public Stream BaseStream { get; protected set; } + + protected BinaryReader Reader { get; set; } + + public PenumbraSqPackStream( FileInfo file ) + : this( file.OpenRead() ) + { } + + public PenumbraSqPackStream( Stream stream ) { - public Stream BaseStream { get; protected set; } + BaseStream = stream; + Reader = new BinaryReader( BaseStream ); + } - protected BinaryReader Reader { get; set; } + public SqPackHeader GetSqPackHeader() + { + BaseStream.Position = 0; - public PenumbraSqPackStream( FileInfo file ) - : this( file.OpenRead() ) - { } + return Reader.ReadStructure< SqPackHeader >(); + } - public PenumbraSqPackStream( Stream stream ) - { - BaseStream = stream; - Reader = new BinaryReader( BaseStream ); - } + public SqPackFileInfo GetFileMetadata( long offset ) + { + BaseStream.Position = offset; - public SqPackHeader GetSqPackHeader() - { - BaseStream.Position = 0; + return Reader.ReadStructure< SqPackFileInfo >(); + } - return Reader.ReadStructure< SqPackHeader >(); - } + public T ReadFile< T >( long offset ) where T : PenumbraFileResource + { + using var ms = new MemoryStream(); - public SqPackFileInfo GetFileMetadata( long offset ) + BaseStream.Position = offset; + + var fileInfo = Reader.ReadStructure< SqPackFileInfo >(); + var file = Activator.CreateInstance< T >(); + + // check if we need to read the extended model header or just default to the standard file header + if( fileInfo.Type == FileType.Model ) { BaseStream.Position = offset; - return Reader.ReadStructure< SqPackFileInfo >(); + var modelFileInfo = Reader.ReadStructure< ModelBlock >(); + + file.FileInfo = new PenumbraFileInfo + { + HeaderSize = modelFileInfo.Size, + Type = modelFileInfo.Type, + BlockCount = modelFileInfo.UsedNumberOfBlocks, + RawFileSize = modelFileInfo.RawFileSize, + Offset = offset, + + // todo: is this useful? + ModelBlock = modelFileInfo, + }; + } + else + { + file.FileInfo = new PenumbraFileInfo + { + HeaderSize = fileInfo.Size, + Type = fileInfo.Type, + BlockCount = fileInfo.NumberOfBlocks, + RawFileSize = fileInfo.RawFileSize, + Offset = offset, + }; } - public T ReadFile< T >( long offset ) where T : PenumbraFileResource + switch( fileInfo.Type ) { - using var ms = new MemoryStream(); + case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." ); - BaseStream.Position = offset; + case FileType.Standard: + ReadStandardFile( file, ms ); + break; - var fileInfo = Reader.ReadStructure< SqPackFileInfo >(); - var file = Activator.CreateInstance< T >(); + case FileType.Model: + ReadModelFile( file, ms ); + break; - // check if we need to read the extended model header or just default to the standard file header - if( fileInfo.Type == FileType.Model ) - { - BaseStream.Position = offset; + case FileType.Texture: + ReadTextureFile( file, ms ); + break; - var modelFileInfo = Reader.ReadStructure< ModelBlock >(); - - file.FileInfo = new PenumbraFileInfo - { - HeaderSize = modelFileInfo.Size, - Type = modelFileInfo.Type, - BlockCount = modelFileInfo.UsedNumberOfBlocks, - RawFileSize = modelFileInfo.RawFileSize, - Offset = offset, - - // todo: is this useful? - ModelBlock = modelFileInfo, - }; - } - else - { - file.FileInfo = new PenumbraFileInfo - { - HeaderSize = fileInfo.Size, - Type = fileInfo.Type, - BlockCount = fileInfo.NumberOfBlocks, - RawFileSize = fileInfo.RawFileSize, - Offset = offset, - }; - } - - switch( fileInfo.Type ) - { - case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." ); - - case FileType.Standard: - ReadStandardFile( file, ms ); - break; - - case FileType.Model: - ReadModelFile( file, ms ); - break; - - case FileType.Texture: - ReadTextureFile( file, ms ); - break; - - default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." ); - } - - file.Data = ms.ToArray(); - if( file.Data.Length != file.FileInfo.RawFileSize ) - { - Debug.WriteLine( "Read data size does not match file size." ); - } - - file.FileStream = new MemoryStream( file.Data, false ); - file.Reader = new BinaryReader( file.FileStream ); - file.FileStream.Position = 0; - - file.LoadFile(); - - return file; + default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." ); } - private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms ) + file.Data = ms.ToArray(); + if( file.Data.Length != file.FileInfo.RawFileSize ) { - var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount ); - - foreach( var block in blocks ) - { - ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms ); - } - - // reset position ready for reading - ms.Position = 0; + Debug.WriteLine( "Read data size does not match file size." ); } - private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms ) + file.FileStream = new MemoryStream( file.Data, false ); + file.Reader = new BinaryReader( file.FileStream ); + file.FileStream.Position = 0; + + file.LoadFile(); + + return file; + } + + private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms ) + { + var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount ); + + foreach( var block in blocks ) { - var mdlBlock = resource.FileInfo!.ModelBlock; - var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms ); + } - // 1/1/3/3/3 stack/runtime/vertex/egeo/index - // TODO: consider testing if this is more reliable than the Explorer method - // of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2] - // i don't want to move this to that method right now, because i know sometimes the index is 0 - // but it seems to work fine in explorer... - int totalBlocks = mdlBlock.StackBlockNum; - totalBlocks += mdlBlock.RuntimeBlockNum; - for( var i = 0; i < 3; i++ ) + // reset position ready for reading + ms.Position = 0; + } + + private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms ) + { + var mdlBlock = resource.FileInfo!.ModelBlock; + var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + + // 1/1/3/3/3 stack/runtime/vertex/egeo/index + // TODO: consider testing if this is more reliable than the Explorer method + // of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2] + // i don't want to move this to that method right now, because i know sometimes the index is 0 + // but it seems to work fine in explorer... + int totalBlocks = mdlBlock.StackBlockNum; + totalBlocks += mdlBlock.RuntimeBlockNum; + for( var i = 0; i < 3; i++ ) + { + totalBlocks += mdlBlock.VertexBufferBlockNum[ i ]; + } + + for( var i = 0; i < 3; i++ ) + { + totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; + } + + for( var i = 0; i < 3; i++ ) + { + totalBlocks += mdlBlock.IndexBufferBlockNum[ i ]; + } + + var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks ); + var currentBlock = 0; + var vertexDataOffsets = new int[3]; + var indexDataOffsets = new int[3]; + var vertexBufferSizes = new int[3]; + var indexBufferSizes = new int[3]; + + ms.Seek( 0x44, SeekOrigin.Begin ); + + Reader.Seek( baseOffset + mdlBlock.StackOffset ); + var stackStart = ms.Position; + for( var i = 0; i < mdlBlock.StackBlockNum; i++ ) + { + var lastPos = Reader.BaseStream.Position; + ReadFileBlock( ms ); + Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + currentBlock++; + } + + var stackEnd = ms.Position; + var stackSize = ( int )( stackEnd - stackStart ); + + Reader.Seek( baseOffset + mdlBlock.RuntimeOffset ); + var runtimeStart = ms.Position; + for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ ) + { + var lastPos = Reader.BaseStream.Position; + ReadFileBlock( ms ); + Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + currentBlock++; + } + + var runtimeEnd = ms.Position; + var runtimeSize = ( int )( runtimeEnd - runtimeStart ); + + for( var i = 0; i < 3; i++ ) + { + if( mdlBlock.VertexBufferBlockNum[ i ] != 0 ) { - totalBlocks += mdlBlock.VertexBufferBlockNum[ i ]; - } - - for( var i = 0; i < 3; i++ ) - { - totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; - } - - for( var i = 0; i < 3; i++ ) - { - totalBlocks += mdlBlock.IndexBufferBlockNum[ i ]; - } - - var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks ); - var currentBlock = 0; - var vertexDataOffsets = new int[3]; - var indexDataOffsets = new int[3]; - var vertexBufferSizes = new int[3]; - var indexBufferSizes = new int[3]; - - ms.Seek( 0x44, SeekOrigin.Begin ); - - Reader.Seek( baseOffset + mdlBlock.StackOffset ); - var stackStart = ms.Position; - for( var i = 0; i < mdlBlock.StackBlockNum; i++ ) - { - var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); - currentBlock++; - } - - var stackEnd = ms.Position; - var stackSize = ( int )( stackEnd - stackStart ); - - Reader.Seek( baseOffset + mdlBlock.RuntimeOffset ); - var runtimeStart = ms.Position; - for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ ) - { - var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); - currentBlock++; - } - - var runtimeEnd = ms.Position; - var runtimeSize = ( int )( runtimeEnd - runtimeStart ); - - for( var i = 0; i < 3; i++ ) - { - if( mdlBlock.VertexBufferBlockNum[ i ] != 0 ) + var currentVertexOffset = ( int )ms.Position; + if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] ) { - var currentVertexOffset = ( int )ms.Position; - if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] ) - { - vertexDataOffsets[ i ] = currentVertexOffset; - } - else - { - vertexDataOffsets[ i ] = 0; - } - - Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] ); - - for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ ) - { - var lastPos = Reader.BaseStream.Position; - vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); - currentBlock++; - } + vertexDataOffsets[ i ] = currentVertexOffset; + } + else + { + vertexDataOffsets[ i ] = 0; } - if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 ) + Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] ); + + for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ ) { - for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ ) - { - var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); - currentBlock++; - } - } - - if( mdlBlock.IndexBufferBlockNum[ i ] != 0 ) - { - var currentIndexOffset = ( int )ms.Position; - if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] ) - { - indexDataOffsets[ i ] = currentIndexOffset; - } - else - { - indexDataOffsets[ i ] = 0; - } - - // i guess this is only needed in the vertex area, for i = 0 - // Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] ); - - for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ ) - { - var lastPos = Reader.BaseStream.Position; - indexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); - currentBlock++; - } + var lastPos = Reader.BaseStream.Position; + vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); + Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + currentBlock++; } } - ms.Seek( 0, SeekOrigin.Begin ); - ms.Write( BitConverter.GetBytes( mdlBlock.Version ) ); - ms.Write( BitConverter.GetBytes( stackSize ) ); - ms.Write( BitConverter.GetBytes( runtimeSize ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) ); - for( var i = 0; i < 3; i++ ) + if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 ) { - ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) ); + for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ ) + { + var lastPos = Reader.BaseStream.Position; + ReadFileBlock( ms ); + Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + currentBlock++; + } } - for( var i = 0; i < 3; i++ ) + if( mdlBlock.IndexBufferBlockNum[ i ] != 0 ) { - ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) ); - } + var currentIndexOffset = ( int )ms.Position; + if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] ) + { + indexDataOffsets[ i ] = currentIndexOffset; + } + else + { + indexDataOffsets[ i ] = 0; + } - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) ); - } + // i guess this is only needed in the vertex area, for i = 0 + // Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] ); - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) ); + for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ ) + { + var lastPos = Reader.BaseStream.Position; + indexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); + Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + currentBlock++; + } } - - ms.Write( new[] { mdlBlock.NumLods } ); - ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) ); - ms.Write( new byte[] { 0 } ); } - private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) + ms.Seek( 0, SeekOrigin.Begin ); + ms.Write( BitConverter.GetBytes( mdlBlock.Version ) ); + ms.Write( BitConverter.GetBytes( stackSize ) ); + ms.Write( BitConverter.GetBytes( runtimeSize ) ); + ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) ); + ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) ); + for( var i = 0; i < 3; i++ ) { - var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount ); + ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) ); + } - // if there is a mipmap header, the comp_offset - // will not be 0 - var mipMapSize = blocks[ 0 ].CompressedOffset; - if( mipMapSize != 0 ) + for( var i = 0; i < 3; i++ ) + { + ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) ); + } + + for( var i = 0; i < 3; i++ ) + { + ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) ); + } + + for( var i = 0; i < 3; i++ ) + { + ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) ); + } + + ms.Write( new[] { mdlBlock.NumLods } ); + ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) ); + ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) ); + ms.Write( new byte[] { 0 } ); + } + + private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) + { + var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount ); + + // if there is a mipmap header, the comp_offset + // will not be 0 + var mipMapSize = blocks[ 0 ].CompressedOffset; + if( mipMapSize != 0 ) + { + var originalPos = BaseStream.Position; + + BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + ms.Write( Reader.ReadBytes( ( int )mipMapSize ) ); + + BaseStream.Position = originalPos; + } + + // i is for texture blocks, j is 'data blocks'... + for( byte i = 0; i < blocks.Count; i++ ) + { + // start from comp_offset + var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + ReadFileBlock( runningBlockTotal, ms, true ); + + for( var j = 1; j < blocks[ i ].BlockCount; j++ ) { - var originalPos = BaseStream.Position; - - BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; - ms.Write( Reader.ReadBytes( ( int )mipMapSize ) ); - - BaseStream.Position = originalPos; - } - - // i is for texture blocks, j is 'data blocks'... - for( byte i = 0; i < blocks.Count; i++ ) - { - // start from comp_offset - var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + runningBlockTotal += ( uint )Reader.ReadInt16(); ReadFileBlock( runningBlockTotal, ms, true ); - - for( var j = 1; j < blocks[ i ].BlockCount; j++ ) - { - runningBlockTotal += ( uint )Reader.ReadInt16(); - ReadFileBlock( runningBlockTotal, ms, true ); - } - - // unknown - Reader.ReadInt16(); } + + // unknown + Reader.ReadInt16(); } + } - protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false ) - => ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition ); + protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false ) + => ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition ); - protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false ) + protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false ) + { + var originalPosition = BaseStream.Position; + BaseStream.Position = offset; + + var blockHeader = Reader.ReadStructure< DatBlockHeader >(); + + // uncompressed block + if( blockHeader.CompressedSize == 32000 ) { - var originalPosition = BaseStream.Position; - BaseStream.Position = offset; - - var blockHeader = Reader.ReadStructure< DatBlockHeader >(); - - // uncompressed block - if( blockHeader.CompressedSize == 32000 ) - { - dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) ); - return blockHeader.UncompressedSize; - } - - var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize ); - - using( var compressedStream = new MemoryStream( data ) ) - { - using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress ); - zlibStream.CopyTo( dest ); - } - - if( resetPosition ) - { - BaseStream.Position = originalPosition; - } - + dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) ); return blockHeader.UncompressedSize; } - public void Dispose() + var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize ); + + using( var compressedStream = new MemoryStream( data ) ) { - Reader?.Dispose(); + using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress ); + zlibStream.CopyTo( dest ); } - public class PenumbraFileInfo + if( resetPosition ) { - public uint HeaderSize; - public FileType Type; - public uint RawFileSize; - public uint BlockCount; - - public long Offset { get; internal set; } - - public ModelBlock ModelBlock { get; internal set; } + BaseStream.Position = originalPosition; } - public class PenumbraFileResource + return blockHeader.UncompressedSize; + } + + public void Dispose() + { + Reader.Dispose(); + } + + public class PenumbraFileInfo + { + public uint HeaderSize; + public FileType Type; + public uint RawFileSize; + public uint BlockCount; + + public long Offset { get; internal set; } + + public ModelBlock ModelBlock { get; internal set; } + } + + public class PenumbraFileResource + { + public PenumbraFileResource() + { } + + public PenumbraFileInfo? FileInfo { get; internal set; } + + public byte[] Data { get; internal set; } = new byte[0]; + + public MemoryStream? FileStream { get; internal set; } + + public BinaryReader? Reader { get; internal set; } + + /// + /// Called once the files are read out from the dats. Used to further parse the file into usable data structures. + /// + public virtual void LoadFile() { - public PenumbraFileResource() - { } - - public PenumbraFileInfo? FileInfo { get; internal set; } - - public byte[] Data { get; internal set; } = new byte[0]; - - public Span< byte > DataSpan - => Data.AsSpan(); - - public MemoryStream? FileStream { get; internal set; } - - public BinaryReader? Reader { get; internal set; } - - public ParsedFilePath? FilePath { get; internal set; } - - /// - /// Called once the files are read out from the dats. Used to further parse the file into usable data structures. - /// - public virtual void LoadFile() - { - // this function is intentionally left blank - } - - public virtual void SaveFile( string path ) - { - File.WriteAllBytes( path, Data ); - } - - public string GetFileHash() - { - using var sha256 = System.Security.Cryptography.SHA256.Create(); - var hash = sha256.ComputeHash( Data ); - - var sb = new StringBuilder(); - foreach( var b in hash ) - { - sb.Append( $"{b:x2}" ); - } - - return sb.ToString(); - } - } - - [StructLayout( LayoutKind.Sequential )] - private struct DatBlockHeader - { - public uint Size; - public uint unknown1; - public uint CompressedSize; - public uint UncompressedSize; - }; - - [StructLayout( LayoutKind.Sequential )] - private struct LodBlock - { - public uint CompressedOffset; - public uint CompressedSize; - public uint DecompressedSize; - public uint BlockOffset; - public uint BlockCount; + // this function is intentionally left blank } } + + [StructLayout( LayoutKind.Sequential )] + private struct DatBlockHeader + { + public uint Size; + public uint unknown1; + public uint CompressedSize; + public uint UncompressedSize; + }; + + [StructLayout( LayoutKind.Sequential )] + private struct LodBlock + { + public uint CompressedOffset; + public uint CompressedSize; + public uint DecompressedSize; + public uint BlockOffset; + public uint BlockCount; + } } \ No newline at end of file diff --git a/Penumbra/Util/SingleOrArrayConverter.cs b/Penumbra/Util/SingleOrArrayConverter.cs index 62840df0..29da6249 100644 --- a/Penumbra/Util/SingleOrArrayConverter.cs +++ b/Penumbra/Util/SingleOrArrayConverter.cs @@ -3,44 +3,43 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Penumbra.Util +namespace Penumbra.Util; + +public class SingleOrArrayConverter< T > : JsonConverter { - public class SingleOrArrayConverter< T > : JsonConverter + public override bool CanConvert( Type objectType ) + => objectType == typeof( HashSet< T > ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) { - public override bool CanConvert( Type objectType ) - => objectType == typeof( HashSet< T > ); + var token = JToken.Load( reader ); - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + if( token.Type == JTokenType.Array ) { - var token = JToken.Load( reader ); - - if( token.Type == JTokenType.Array ) - { - return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); - } - - var tmp = token.ToObject< T >(); - return tmp != null - ? new HashSet< T > { tmp } - : new HashSet< T >(); + return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); } - public override bool CanWrite - => true; + var tmp = token.ToObject< T >(); + return tmp != null + ? new HashSet< T > { tmp } + : new HashSet< T >(); + } - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + writer.WriteStartArray(); + if( value != null ) { - writer.WriteStartArray(); - if( value != null ) + var v = ( HashSet< T > )value; + foreach( var val in v ) { - var v = ( HashSet< T > )value; - foreach( var val in v ) - { - serializer.Serialize( writer, val?.ToString() ); - } + serializer.Serialize( writer, val?.ToString() ); } - - writer.WriteEndArray(); } + + writer.WriteEndArray(); } } \ No newline at end of file diff --git a/Penumbra/Util/StringPathExtensions.cs b/Penumbra/Util/StringPathExtensions.cs index 1e309cac..bcce2a88 100644 --- a/Penumbra/Util/StringPathExtensions.cs +++ b/Penumbra/Util/StringPathExtensions.cs @@ -2,67 +2,66 @@ using System.Collections.Generic; using System.IO; using System.Text; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class StringPathExtensions { - public static class StringPathExtensions + private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars()); + + public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" ) { - private static readonly HashSet< char > Invalid = new( Path.GetInvalidFileNameChars() ); - - public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" ) + StringBuilder sb = new(s.Length); + foreach( var c in s ) { - StringBuilder sb = new( s.Length ); - foreach( var c in s ) + if( Invalid.Contains( c ) ) { - if( Invalid.Contains( c ) ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } + sb.Append( replacement ); + } + else + { + sb.Append( c ); } - - return sb.ToString(); } - public static string RemoveInvalidPathSymbols( this string s ) - => string.Concat( s.Split( Path.GetInvalidFileNameChars() ) ); + return sb.ToString(); + } - public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" ) + public static string RemoveInvalidPathSymbols( this string s ) + => string.Concat( s.Split( Path.GetInvalidFileNameChars() ) ); + + public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" ) + { + StringBuilder sb = new(s.Length); + foreach( var c in s ) { - StringBuilder sb = new( s.Length ); - foreach( var c in s ) + if( c >= 128 ) { - if( c >= 128 ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } + sb.Append( replacement ); + } + else + { + sb.Append( c ); } - - return sb.ToString(); } - public static string ReplaceBadXivSymbols( this string s, string replacement = "_" ) - { - StringBuilder sb = new( s.Length ); - foreach( var c in s ) - { - if( c >= 128 || Invalid.Contains( c ) ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } + return sb.ToString(); + } - return sb.ToString(); + public static string ReplaceBadXivSymbols( this string s, string replacement = "_" ) + { + StringBuilder sb = new(s.Length); + foreach( var c in s ) + { + if( c >= 128 || Invalid.Contains( c ) ) + { + sb.Append( replacement ); + } + else + { + sb.Append( c ); + } } + + return sb.ToString(); } } \ No newline at end of file diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs index fba296f4..4e2e22ca 100644 --- a/Penumbra/Util/TempFile.cs +++ b/Penumbra/Util/TempFile.cs @@ -1,34 +1,33 @@ using System.IO; using System.Linq; -namespace Penumbra.Util +namespace Penumbra.Util; + +public static class TempFile { - public static class TempFile + public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" ) { - public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" ) + const uint maxTries = 15; + for( var i = 0; i < maxTries; ++i ) { - const uint maxTries = 15; - for( var i = 0; i < maxTries; ++i ) + var name = Path.GetRandomFileName(); + var path = new FileInfo( Path.Combine( baseDir.FullName, + suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) ); + if( !path.Exists ) { - var name = Path.GetRandomFileName(); - var path = new FileInfo( Path.Combine( baseDir.FullName, - suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) ); - if( !path.Exists ) - { - return path; - } + return path; } - - throw new IOException(); } - public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" ) - { - var fileName = TempFileName( baseDir, suffix ); - using var stream = fileName.OpenWrite(); - stream.Write( data, 0, data.Length ); - fileName.Refresh(); - return fileName; - } + throw new IOException(); + } + + public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" ) + { + var fileName = TempFileName( baseDir, suffix ); + using var stream = fileName.OpenWrite(); + stream.Write( data, 0, data.Length ); + fileName.Refresh(); + return fileName; } } \ No newline at end of file From 46581780e0b4b0b0922b8fa7f739fdd8f1148670 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Mar 2022 14:09:45 +0100 Subject: [PATCH 0091/2451] tmp --- .../ByteString/Utf8String.Construction.cs | 1 + Penumbra.GameData/Structs/EqpEntry.cs | 594 +++++++-------- Penumbra.GameData/Structs/GmpEntry.cs | 2 + Penumbra.GameData/Util/Functions.cs | 14 +- Penumbra/Importer/TexToolsImport.cs | 1 - Penumbra/Importer/TexToolsMeta.cs | 685 +++++++++--------- Penumbra/Interop/Structs/CharacterUtility.cs | 8 +- Penumbra/Interop/Structs/ResourceHandle.cs | 3 + Penumbra/Meta/EntryExtensions.cs | 60 -- Penumbra/Meta/Files/CmpFile.cs | 89 +-- Penumbra/Meta/Files/EqdpFile.cs | 301 +++----- Penumbra/Meta/Files/EqpFile.cs | 216 ------ Penumbra/Meta/Files/EqpGmpFile.cs | 164 +++++ Penumbra/Meta/Files/EstFile.cs | 274 ++++--- Penumbra/Meta/Files/GmpFile.cs | 42 -- Penumbra/Meta/Files/ImcExtensions.cs | 151 ---- Penumbra/Meta/Files/ImcFile.cs | 166 +++++ Penumbra/Meta/Files/MetaBaseFile.cs | 50 ++ Penumbra/Meta/Files/MetaDefaults.cs | 196 ----- Penumbra/Meta/Identifier.cs | 172 ----- .../Meta/Manipulations/EqdpManipulation.cs | 56 ++ .../Meta/Manipulations/EqpManipulation.cs | 50 ++ .../Meta/Manipulations/EstManipulation.cs | 63 ++ .../Meta/Manipulations/GmpManipulation.cs | 45 ++ .../Meta/Manipulations/ImcManipulation.cs | 61 ++ .../Meta/Manipulations/MetaManipulation.cs | 109 +++ .../Meta/Manipulations/RspManipulation.cs | 48 ++ Penumbra/Meta/MetaCollection.cs | 3 +- Penumbra/Meta/MetaManager.cs | 312 ++++++-- Penumbra/Meta/MetaManipulation.cs | 259 ------- Penumbra/Mod/ModCache.cs | 1 + Penumbra/Mods/ModCollectionCache.cs | 2 +- Penumbra/Penumbra.cs | 16 + Penumbra/UI/MenuTabs/TabEffective.cs | 16 +- .../TabInstalled/TabInstalledDetails.cs | 2 +- .../TabInstalledDetailsManipulations.cs | 553 +++++++------- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- 37 files changed, 2343 insertions(+), 2444 deletions(-) delete mode 100644 Penumbra/Meta/EntryExtensions.cs delete mode 100644 Penumbra/Meta/Files/EqpFile.cs create mode 100644 Penumbra/Meta/Files/EqpGmpFile.cs delete mode 100644 Penumbra/Meta/Files/GmpFile.cs delete mode 100644 Penumbra/Meta/Files/ImcExtensions.cs create mode 100644 Penumbra/Meta/Files/ImcFile.cs create mode 100644 Penumbra/Meta/Files/MetaBaseFile.cs delete mode 100644 Penumbra/Meta/Files/MetaDefaults.cs delete mode 100644 Penumbra/Meta/Identifier.cs create mode 100644 Penumbra/Meta/Manipulations/EqdpManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/EqpManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/EstManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/GmpManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/ImcManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/MetaManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/RspManipulation.cs delete mode 100644 Penumbra/Meta/MetaManipulation.cs diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs index 96ec7313..487b1a16 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -136,6 +136,7 @@ public sealed unsafe partial class Utf8String : IDisposable if( isOwned ) { + GC.AddMemoryPressure( length + 1 ); _length |= OwnedFlag; } diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 5b54baa8..18b3075a 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -4,305 +4,307 @@ using System.Linq; using System.ComponentModel; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[Flags] +public enum EqpEntry : ulong { - [Flags] - public enum EqpEntry : ulong + BodyEnabled = 0x00_01ul, + BodyHideWaist = 0x00_02ul, + _2 = 0x00_04ul, + BodyHideGlovesS = 0x00_08ul, + _4 = 0x00_10ul, + BodyHideGlovesM = 0x00_20ul, + BodyHideGlovesL = 0x00_40ul, + BodyHideGorget = 0x00_80ul, + BodyShowLeg = 0x01_00ul, + BodyShowHand = 0x02_00ul, + BodyShowHead = 0x04_00ul, + BodyShowNecklace = 0x08_00ul, + BodyShowBracelet = 0x10_00ul, + BodyShowTail = 0x20_00ul, + DisableBreastPhysics = 0x40_00ul, + _15 = 0x80_00ul, + BodyMask = 0xFF_FFul, + + LegsEnabled = 0x01ul << 16, + LegsHideKneePads = 0x02ul << 16, + LegsHideBootsS = 0x04ul << 16, + LegsHideBootsM = 0x08ul << 16, + _20 = 0x10ul << 16, + LegsShowFoot = 0x20ul << 16, + LegsShowTail = 0x40ul << 16, + _23 = 0x80ul << 16, + LegsMask = 0xFFul << 16, + + HandsEnabled = 0x01ul << 24, + HandsHideElbow = 0x02ul << 24, + HandsHideForearm = 0x04ul << 24, + _27 = 0x08ul << 24, + HandShowBracelet = 0x10ul << 24, + HandShowRingL = 0x20ul << 24, + HandShowRingR = 0x40ul << 24, + _31 = 0x80ul << 24, + HandsMask = 0xFFul << 24, + + FeetEnabled = 0x01ul << 32, + FeetHideKnee = 0x02ul << 32, + FeetHideCalf = 0x04ul << 32, + FeetHideAnkle = 0x08ul << 32, + _36 = 0x10ul << 32, + _37 = 0x20ul << 32, + _38 = 0x40ul << 32, + _39 = 0x80ul << 32, + FeetMask = 0xFFul << 32, + + HeadEnabled = 0x00_00_01ul << 40, + HeadHideScalp = 0x00_00_02ul << 40, + HeadHideHair = 0x00_00_04ul << 40, + HeadShowHairOverride = 0x00_00_08ul << 40, + HeadHideNeck = 0x00_00_10ul << 40, + HeadShowNecklace = 0x00_00_20ul << 40, + _46 = 0x00_00_40ul << 40, + HeadShowEarrings = 0x00_00_80ul << 40, + HeadShowEarringsHuman = 0x00_01_00ul << 40, + HeadShowEarringsAura = 0x00_02_00ul << 40, + HeadShowEarHuman = 0x00_04_00ul << 40, + HeadShowEarMiqote = 0x00_08_00ul << 40, + HeadShowEarAuRa = 0x00_10_00ul << 40, + HeadShowEarViera = 0x00_20_00ul << 40, + _54 = 0x00_40_00ul << 40, + _55 = 0x00_80_00ul << 40, + HeadShowHrothgarHat = 0x01_00_00ul << 40, + HeadShowVieraHat = 0x02_00_00ul << 40, + _58 = 0x04_00_00ul << 40, + _59 = 0x08_00_00ul << 40, + _60 = 0x10_00_00ul << 40, + _61 = 0x20_00_00ul << 40, + _62 = 0x40_00_00ul << 40, + _63 = 0x80_00_00ul << 40, + HeadMask = 0xFF_FF_FFul << 40, +} + +public static class Eqp +{ + // cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags + public const EqpEntry DefaultEntry = ( EqpEntry )0x3fe00070603f00; + + public static (int, int) BytesAndOffset( EquipSlot slot ) { - BodyEnabled = 0x00_01ul, - BodyHideWaist = 0x00_02ul, - _2 = 0x00_04ul, - BodyHideGlovesS = 0x00_08ul, - _4 = 0x00_10ul, - BodyHideGlovesM = 0x00_20ul, - BodyHideGlovesL = 0x00_40ul, - BodyHideGorget = 0x00_80ul, - BodyShowLeg = 0x01_00ul, - BodyShowHand = 0x02_00ul, - BodyShowHead = 0x04_00ul, - BodyShowNecklace = 0x08_00ul, - BodyShowBracelet = 0x10_00ul, - BodyShowTail = 0x20_00ul, - DisableBreastPhysics = 0x40_00ul, - _15 = 0x80_00ul, - BodyMask = 0xFF_FFul, - - LegsEnabled = 0x01ul << 16, - LegsHideKneePads = 0x02ul << 16, - LegsHideBootsS = 0x04ul << 16, - LegsHideBootsM = 0x08ul << 16, - _20 = 0x10ul << 16, - LegsShowFoot = 0x20ul << 16, - LegsShowTail = 0x40ul << 16, - _23 = 0x80ul << 16, - LegsMask = 0xFFul << 16, - - HandsEnabled = 0x01ul << 24, - HandsHideElbow = 0x02ul << 24, - HandsHideForearm = 0x04ul << 24, - _27 = 0x08ul << 24, - HandShowBracelet = 0x10ul << 24, - HandShowRingL = 0x20ul << 24, - HandShowRingR = 0x40ul << 24, - _31 = 0x80ul << 24, - HandsMask = 0xFFul << 24, - - FeetEnabled = 0x01ul << 32, - FeetHideKnee = 0x02ul << 32, - FeetHideCalf = 0x04ul << 32, - FeetHideAnkle = 0x08ul << 32, - _36 = 0x10ul << 32, - _37 = 0x20ul << 32, - _38 = 0x40ul << 32, - _39 = 0x80ul << 32, - FeetMask = 0xFFul << 32, - - HeadEnabled = 0x00_00_01ul << 40, - HeadHideScalp = 0x00_00_02ul << 40, - HeadHideHair = 0x00_00_04ul << 40, - HeadShowHairOverride = 0x00_00_08ul << 40, - HeadHideNeck = 0x00_00_10ul << 40, - HeadShowNecklace = 0x00_00_20ul << 40, - _46 = 0x00_00_40ul << 40, - HeadShowEarrings = 0x00_00_80ul << 40, - HeadShowEarringsHuman = 0x00_01_00ul << 40, - HeadShowEarringsAura = 0x00_02_00ul << 40, - HeadShowEarHuman = 0x00_04_00ul << 40, - HeadShowEarMiqote = 0x00_08_00ul << 40, - HeadShowEarAuRa = 0x00_10_00ul << 40, - HeadShowEarViera = 0x00_20_00ul << 40, - _54 = 0x00_40_00ul << 40, - _55 = 0x00_80_00ul << 40, - HeadShowHrothgarHat = 0x01_00_00ul << 40, - HeadShowVieraHat = 0x02_00_00ul << 40, - _58 = 0x04_00_00ul << 40, - _59 = 0x08_00_00ul << 40, - _60 = 0x10_00_00ul << 40, - _61 = 0x20_00_00ul << 40, - _62 = 0x40_00_00ul << 40, - _63 = 0x80_00_00ul << 40, - HeadMask = 0xFF_FF_FFul << 40, - } - - public static class Eqp - { - public static (int, int) BytesAndOffset( EquipSlot slot ) + return slot switch { - return slot switch - { - EquipSlot.Body => ( 2, 0 ), - EquipSlot.Legs => ( 1, 2 ), - EquipSlot.Hands => ( 1, 3 ), - EquipSlot.Feet => ( 1, 4 ), - EquipSlot.Head => ( 3, 5 ), - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value ) - { - EqpEntry ret = 0; - var (bytes, offset) = BytesAndOffset( slot ); - if( bytes != value.Length ) - { - throw new ArgumentException(); - } - - for( var i = 0; i < bytes; ++i ) - { - ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) ); - } - - return ret; - } - - public static EqpEntry Mask( EquipSlot slot ) - { - return slot switch - { - EquipSlot.Body => EqpEntry.BodyMask, - EquipSlot.Head => EqpEntry.HeadMask, - EquipSlot.Legs => EqpEntry.LegsMask, - EquipSlot.Feet => EqpEntry.FeetMask, - EquipSlot.Hands => EqpEntry.HandsMask, - _ => 0, - }; - } - - public static EquipSlot ToEquipSlot( this EqpEntry entry ) - { - return entry switch - { - EqpEntry.BodyEnabled => EquipSlot.Body, - EqpEntry.BodyHideWaist => EquipSlot.Body, - EqpEntry._2 => EquipSlot.Body, - EqpEntry.BodyHideGlovesS => EquipSlot.Body, - EqpEntry._4 => EquipSlot.Body, - EqpEntry.BodyHideGlovesM => EquipSlot.Body, - EqpEntry.BodyHideGlovesL => EquipSlot.Body, - EqpEntry.BodyHideGorget => EquipSlot.Body, - EqpEntry.BodyShowLeg => EquipSlot.Body, - EqpEntry.BodyShowHand => EquipSlot.Body, - EqpEntry.BodyShowHead => EquipSlot.Body, - EqpEntry.BodyShowNecklace => EquipSlot.Body, - EqpEntry.BodyShowBracelet => EquipSlot.Body, - EqpEntry.BodyShowTail => EquipSlot.Body, - EqpEntry.DisableBreastPhysics => EquipSlot.Body, - EqpEntry._15 => EquipSlot.Body, - - EqpEntry.LegsEnabled => EquipSlot.Legs, - EqpEntry.LegsHideKneePads => EquipSlot.Legs, - EqpEntry.LegsHideBootsS => EquipSlot.Legs, - EqpEntry.LegsHideBootsM => EquipSlot.Legs, - EqpEntry._20 => EquipSlot.Legs, - EqpEntry.LegsShowFoot => EquipSlot.Legs, - EqpEntry.LegsShowTail => EquipSlot.Legs, - EqpEntry._23 => EquipSlot.Legs, - - EqpEntry.HandsEnabled => EquipSlot.Hands, - EqpEntry.HandsHideElbow => EquipSlot.Hands, - EqpEntry.HandsHideForearm => EquipSlot.Hands, - EqpEntry._27 => EquipSlot.Hands, - EqpEntry.HandShowBracelet => EquipSlot.Hands, - EqpEntry.HandShowRingL => EquipSlot.Hands, - EqpEntry.HandShowRingR => EquipSlot.Hands, - EqpEntry._31 => EquipSlot.Hands, - - EqpEntry.FeetEnabled => EquipSlot.Feet, - EqpEntry.FeetHideKnee => EquipSlot.Feet, - EqpEntry.FeetHideCalf => EquipSlot.Feet, - EqpEntry.FeetHideAnkle => EquipSlot.Feet, - EqpEntry._36 => EquipSlot.Feet, - EqpEntry._37 => EquipSlot.Feet, - EqpEntry._38 => EquipSlot.Feet, - EqpEntry._39 => EquipSlot.Feet, - - EqpEntry.HeadEnabled => EquipSlot.Head, - EqpEntry.HeadHideScalp => EquipSlot.Head, - EqpEntry.HeadHideHair => EquipSlot.Head, - EqpEntry.HeadShowHairOverride => EquipSlot.Head, - EqpEntry.HeadHideNeck => EquipSlot.Head, - EqpEntry.HeadShowNecklace => EquipSlot.Head, - EqpEntry._46 => EquipSlot.Head, - EqpEntry.HeadShowEarrings => EquipSlot.Head, - EqpEntry.HeadShowEarringsHuman => EquipSlot.Head, - EqpEntry.HeadShowEarringsAura => EquipSlot.Head, - EqpEntry.HeadShowEarHuman => EquipSlot.Head, - EqpEntry.HeadShowEarMiqote => EquipSlot.Head, - EqpEntry.HeadShowEarAuRa => EquipSlot.Head, - EqpEntry.HeadShowEarViera => EquipSlot.Head, - EqpEntry._54 => EquipSlot.Head, - EqpEntry._55 => EquipSlot.Head, - EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, - EqpEntry.HeadShowVieraHat => EquipSlot.Head, - EqpEntry._58 => EquipSlot.Head, - EqpEntry._59 => EquipSlot.Head, - EqpEntry._60 => EquipSlot.Head, - EqpEntry._61 => EquipSlot.Head, - EqpEntry._62 => EquipSlot.Head, - EqpEntry._63 => EquipSlot.Head, - - _ => EquipSlot.Unknown, - }; - } - - public static string ToLocalName( this EqpEntry entry ) - { - return entry switch - { - EqpEntry.BodyEnabled => "Enabled", - EqpEntry.BodyHideWaist => "Hide Waist", - EqpEntry._2 => "Unknown 2", - EqpEntry.BodyHideGlovesS => "Hide Small Gloves", - EqpEntry._4 => "Unknown 4", - EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", - EqpEntry.BodyHideGlovesL => "Hide Large Gloves", - EqpEntry.BodyHideGorget => "Hide Gorget", - EqpEntry.BodyShowLeg => "Show Legs", - EqpEntry.BodyShowHand => "Show Hands", - EqpEntry.BodyShowHead => "Show Head", - EqpEntry.BodyShowNecklace => "Show Necklace", - EqpEntry.BodyShowBracelet => "Show Bracelet", - EqpEntry.BodyShowTail => "Show Tail", - EqpEntry.DisableBreastPhysics => "Disable Breast Physics", - EqpEntry._15 => "Unknown 15", - - EqpEntry.LegsEnabled => "Enabled", - EqpEntry.LegsHideKneePads => "Hide Knee Pads", - EqpEntry.LegsHideBootsS => "Hide Small Boots", - EqpEntry.LegsHideBootsM => "Hide Medium Boots", - EqpEntry._20 => "Unknown 20", - EqpEntry.LegsShowFoot => "Show Foot", - EqpEntry.LegsShowTail => "Show Tail", - EqpEntry._23 => "Unknown 23", - - EqpEntry.HandsEnabled => "Enabled", - EqpEntry.HandsHideElbow => "Hide Elbow", - EqpEntry.HandsHideForearm => "Hide Forearm", - EqpEntry._27 => "Unknown 27", - EqpEntry.HandShowBracelet => "Show Bracelet", - EqpEntry.HandShowRingL => "Show Left Ring", - EqpEntry.HandShowRingR => "Show Right Ring", - EqpEntry._31 => "Unknown 31", - - EqpEntry.FeetEnabled => "Enabled", - EqpEntry.FeetHideKnee => "Hide Knees", - EqpEntry.FeetHideCalf => "Hide Calves", - EqpEntry.FeetHideAnkle => "Hide Ankles", - EqpEntry._36 => "Unknown 36", - EqpEntry._37 => "Unknown 37", - EqpEntry._38 => "Unknown 38", - EqpEntry._39 => "Unknown 39", - - EqpEntry.HeadEnabled => "Enabled", - EqpEntry.HeadHideScalp => "Hide Scalp", - EqpEntry.HeadHideHair => "Hide Hair", - EqpEntry.HeadShowHairOverride => "Show Hair Override", - EqpEntry.HeadHideNeck => "Hide Neck", - EqpEntry.HeadShowNecklace => "Show Necklace", - EqpEntry._46 => "Unknown 46", - EqpEntry.HeadShowEarrings => "Show Earrings", - EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)", - EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)", - EqpEntry.HeadShowEarHuman => "Show Ears (Human)", - EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)", - EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)", - EqpEntry.HeadShowEarViera => "Show Ears (Viera)", - EqpEntry._54 => "Unknown 54", - EqpEntry._55 => "Unknown 55", - EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar", - EqpEntry.HeadShowVieraHat => "Show on Viera", - EqpEntry._58 => "Unknown 58", - EqpEntry._59 => "Unknown 59", - EqpEntry._60 => "Unknown 60", - EqpEntry._61 => "Unknown 61", - EqpEntry._62 => "Unknown 62", - EqpEntry._63 => "Unknown 63", - - _ => throw new InvalidEnumArgumentException(), - }; - } - - private static EqpEntry[] GetEntriesForSlot( EquipSlot slot ) - { - return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) ) - .Where( e => e.ToEquipSlot() == slot ) - .ToArray(); - } - - public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body ); - public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs ); - public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands ); - public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet ); - public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head ); - - public static IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() - { - [ EquipSlot.Body ] = EqpAttributesBody, - [ EquipSlot.Legs ] = EqpAttributesLegs, - [ EquipSlot.Hands ] = EqpAttributesHands, - [ EquipSlot.Feet ] = EqpAttributesFeet, - [ EquipSlot.Head ] = EqpAttributesHead, + EquipSlot.Body => ( 2, 0 ), + EquipSlot.Legs => ( 1, 2 ), + EquipSlot.Hands => ( 1, 3 ), + EquipSlot.Feet => ( 1, 4 ), + EquipSlot.Head => ( 3, 5 ), + _ => throw new InvalidEnumArgumentException(), }; } + + public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value ) + { + EqpEntry ret = 0; + var (bytes, offset) = BytesAndOffset( slot ); + if( bytes != value.Length ) + { + throw new ArgumentException(); + } + + for( var i = 0; i < bytes; ++i ) + { + ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) ); + } + + return ret; + } + + public static EqpEntry Mask( EquipSlot slot ) + { + return slot switch + { + EquipSlot.Body => EqpEntry.BodyMask, + EquipSlot.Head => EqpEntry.HeadMask, + EquipSlot.Legs => EqpEntry.LegsMask, + EquipSlot.Feet => EqpEntry.FeetMask, + EquipSlot.Hands => EqpEntry.HandsMask, + _ => 0, + }; + } + + public static EquipSlot ToEquipSlot( this EqpEntry entry ) + { + return entry switch + { + EqpEntry.BodyEnabled => EquipSlot.Body, + EqpEntry.BodyHideWaist => EquipSlot.Body, + EqpEntry._2 => EquipSlot.Body, + EqpEntry.BodyHideGlovesS => EquipSlot.Body, + EqpEntry._4 => EquipSlot.Body, + EqpEntry.BodyHideGlovesM => EquipSlot.Body, + EqpEntry.BodyHideGlovesL => EquipSlot.Body, + EqpEntry.BodyHideGorget => EquipSlot.Body, + EqpEntry.BodyShowLeg => EquipSlot.Body, + EqpEntry.BodyShowHand => EquipSlot.Body, + EqpEntry.BodyShowHead => EquipSlot.Body, + EqpEntry.BodyShowNecklace => EquipSlot.Body, + EqpEntry.BodyShowBracelet => EquipSlot.Body, + EqpEntry.BodyShowTail => EquipSlot.Body, + EqpEntry.DisableBreastPhysics => EquipSlot.Body, + EqpEntry._15 => EquipSlot.Body, + + EqpEntry.LegsEnabled => EquipSlot.Legs, + EqpEntry.LegsHideKneePads => EquipSlot.Legs, + EqpEntry.LegsHideBootsS => EquipSlot.Legs, + EqpEntry.LegsHideBootsM => EquipSlot.Legs, + EqpEntry._20 => EquipSlot.Legs, + EqpEntry.LegsShowFoot => EquipSlot.Legs, + EqpEntry.LegsShowTail => EquipSlot.Legs, + EqpEntry._23 => EquipSlot.Legs, + + EqpEntry.HandsEnabled => EquipSlot.Hands, + EqpEntry.HandsHideElbow => EquipSlot.Hands, + EqpEntry.HandsHideForearm => EquipSlot.Hands, + EqpEntry._27 => EquipSlot.Hands, + EqpEntry.HandShowBracelet => EquipSlot.Hands, + EqpEntry.HandShowRingL => EquipSlot.Hands, + EqpEntry.HandShowRingR => EquipSlot.Hands, + EqpEntry._31 => EquipSlot.Hands, + + EqpEntry.FeetEnabled => EquipSlot.Feet, + EqpEntry.FeetHideKnee => EquipSlot.Feet, + EqpEntry.FeetHideCalf => EquipSlot.Feet, + EqpEntry.FeetHideAnkle => EquipSlot.Feet, + EqpEntry._36 => EquipSlot.Feet, + EqpEntry._37 => EquipSlot.Feet, + EqpEntry._38 => EquipSlot.Feet, + EqpEntry._39 => EquipSlot.Feet, + + EqpEntry.HeadEnabled => EquipSlot.Head, + EqpEntry.HeadHideScalp => EquipSlot.Head, + EqpEntry.HeadHideHair => EquipSlot.Head, + EqpEntry.HeadShowHairOverride => EquipSlot.Head, + EqpEntry.HeadHideNeck => EquipSlot.Head, + EqpEntry.HeadShowNecklace => EquipSlot.Head, + EqpEntry._46 => EquipSlot.Head, + EqpEntry.HeadShowEarrings => EquipSlot.Head, + EqpEntry.HeadShowEarringsHuman => EquipSlot.Head, + EqpEntry.HeadShowEarringsAura => EquipSlot.Head, + EqpEntry.HeadShowEarHuman => EquipSlot.Head, + EqpEntry.HeadShowEarMiqote => EquipSlot.Head, + EqpEntry.HeadShowEarAuRa => EquipSlot.Head, + EqpEntry.HeadShowEarViera => EquipSlot.Head, + EqpEntry._54 => EquipSlot.Head, + EqpEntry._55 => EquipSlot.Head, + EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, + EqpEntry.HeadShowVieraHat => EquipSlot.Head, + EqpEntry._58 => EquipSlot.Head, + EqpEntry._59 => EquipSlot.Head, + EqpEntry._60 => EquipSlot.Head, + EqpEntry._61 => EquipSlot.Head, + EqpEntry._62 => EquipSlot.Head, + EqpEntry._63 => EquipSlot.Head, + + _ => EquipSlot.Unknown, + }; + } + + public static string ToLocalName( this EqpEntry entry ) + { + return entry switch + { + EqpEntry.BodyEnabled => "Enabled", + EqpEntry.BodyHideWaist => "Hide Waist", + EqpEntry._2 => "Unknown 2", + EqpEntry.BodyHideGlovesS => "Hide Small Gloves", + EqpEntry._4 => "Unknown 4", + EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", + EqpEntry.BodyHideGlovesL => "Hide Large Gloves", + EqpEntry.BodyHideGorget => "Hide Gorget", + EqpEntry.BodyShowLeg => "Show Legs", + EqpEntry.BodyShowHand => "Show Hands", + EqpEntry.BodyShowHead => "Show Head", + EqpEntry.BodyShowNecklace => "Show Necklace", + EqpEntry.BodyShowBracelet => "Show Bracelet", + EqpEntry.BodyShowTail => "Show Tail", + EqpEntry.DisableBreastPhysics => "Disable Breast Physics", + EqpEntry._15 => "Unknown 15", + + EqpEntry.LegsEnabled => "Enabled", + EqpEntry.LegsHideKneePads => "Hide Knee Pads", + EqpEntry.LegsHideBootsS => "Hide Small Boots", + EqpEntry.LegsHideBootsM => "Hide Medium Boots", + EqpEntry._20 => "Unknown 20", + EqpEntry.LegsShowFoot => "Show Foot", + EqpEntry.LegsShowTail => "Show Tail", + EqpEntry._23 => "Unknown 23", + + EqpEntry.HandsEnabled => "Enabled", + EqpEntry.HandsHideElbow => "Hide Elbow", + EqpEntry.HandsHideForearm => "Hide Forearm", + EqpEntry._27 => "Unknown 27", + EqpEntry.HandShowBracelet => "Show Bracelet", + EqpEntry.HandShowRingL => "Show Left Ring", + EqpEntry.HandShowRingR => "Show Right Ring", + EqpEntry._31 => "Unknown 31", + + EqpEntry.FeetEnabled => "Enabled", + EqpEntry.FeetHideKnee => "Hide Knees", + EqpEntry.FeetHideCalf => "Hide Calves", + EqpEntry.FeetHideAnkle => "Hide Ankles", + EqpEntry._36 => "Unknown 36", + EqpEntry._37 => "Unknown 37", + EqpEntry._38 => "Unknown 38", + EqpEntry._39 => "Unknown 39", + + EqpEntry.HeadEnabled => "Enabled", + EqpEntry.HeadHideScalp => "Hide Scalp", + EqpEntry.HeadHideHair => "Hide Hair", + EqpEntry.HeadShowHairOverride => "Show Hair Override", + EqpEntry.HeadHideNeck => "Hide Neck", + EqpEntry.HeadShowNecklace => "Show Necklace", + EqpEntry._46 => "Unknown 46", + EqpEntry.HeadShowEarrings => "Show Earrings", + EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)", + EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)", + EqpEntry.HeadShowEarHuman => "Show Ears (Human)", + EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)", + EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)", + EqpEntry.HeadShowEarViera => "Show Ears (Viera)", + EqpEntry._54 => "Unknown 54", + EqpEntry._55 => "Unknown 55", + EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar", + EqpEntry.HeadShowVieraHat => "Show on Viera", + EqpEntry._58 => "Unknown 58", + EqpEntry._59 => "Unknown 59", + EqpEntry._60 => "Unknown 60", + EqpEntry._61 => "Unknown 61", + EqpEntry._62 => "Unknown 62", + EqpEntry._63 => "Unknown 63", + + _ => throw new InvalidEnumArgumentException(), + }; + } + + private static EqpEntry[] GetEntriesForSlot( EquipSlot slot ) + { + return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) ) + .Where( e => e.ToEquipSlot() == slot ) + .ToArray(); + } + + public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body ); + public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs ); + public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands ); + public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet ); + public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head ); + + public static IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() + { + [ EquipSlot.Body ] = EqpAttributesBody, + [ EquipSlot.Legs ] = EqpAttributesLegs, + [ EquipSlot.Hands ] = EqpAttributesHands, + [ EquipSlot.Feet ] = EqpAttributesFeet, + [ EquipSlot.Head ] = EqpAttributesHead, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/GmpEntry.cs b/Penumbra.GameData/Structs/GmpEntry.cs index be9a7b58..c6af3fba 100644 --- a/Penumbra.GameData/Structs/GmpEntry.cs +++ b/Penumbra.GameData/Structs/GmpEntry.cs @@ -4,6 +4,8 @@ namespace Penumbra.GameData.Structs { public struct GmpEntry { + public static readonly GmpEntry Default = new (); + public bool Enabled { get => ( Value & 1 ) == 1; diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs index 5d9ab10e..81e3dc28 100644 --- a/Penumbra.GameData/Util/Functions.cs +++ b/Penumbra.GameData/Util/Functions.cs @@ -137,15 +137,21 @@ public static class Functions [DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memcmp( byte* b1, byte* b2, int count ); + private static extern unsafe int memcmp( void* b1, void* b2, int count ); - public static unsafe int MemCmpUnchecked( byte* ptr1, byte* ptr2, int count ) + public static unsafe int MemCmpUnchecked( void* ptr1, void* ptr2, int count ) => memcmp( ptr1, ptr2, count ); [DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memicmp( byte* b1, byte* b2, int count ); + private static extern unsafe int memicmp( void* b1, void* b2, int count ); - public static unsafe int MemCmpCaseInsensitiveUnchecked( byte* ptr1, byte* ptr2, int count ) + public static unsafe int MemCmpCaseInsensitiveUnchecked( void* ptr1, void* ptr2, int count ) => memicmp( ptr1, ptr2, count ); + + [DllImport( "msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe void* memset( void* dest, int c, int count ); + + public static unsafe void* MemSet( void* dest, byte value, int count ) + => memset( dest, value, count ); } \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index aac92447..46e3bcd5 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -7,7 +7,6 @@ using Dalamud.Logging; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Importer.Models; using Penumbra.Mod; using Penumbra.Util; diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 343c128c..4dc10b26 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -7,376 +7,383 @@ using Lumina.Data.Files; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; -using Penumbra.Meta; using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.Util; -namespace Penumbra.Importer +namespace Penumbra.Importer; + +// TexTools provices custom generated *.meta files for its modpacks, that contain changes to +// - imc files +// - eqp files +// - gmp files +// - est files +// - eqdp files +// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. +// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. +// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. +// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. +public class TexToolsMeta { - // TexTools provices custom generated *.meta files for its modpacks, that contain changes to - // - imc files - // - eqp files - // - gmp files - // - est files - // - eqdp files - // made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. - // We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. - // TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. - public class TexToolsMeta + // The info class determines the files or table locations the changes need to apply to from the filename. + public class Info { - // The info class determines the files or table locations the changes need to apply to from the filename. - public class Info + private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex + private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex + private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex + private const string Pir = @"\k'PrimaryId'"; // language=regex + private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex + private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex + private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex + private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex + private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex + private const string Ext = @"\.meta"; + + // These are the valid regexes for .meta files that we are able to support at the moment. + private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled); + private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled); + + public readonly ObjectType PrimaryType; + public readonly BodySlot SecondaryType; + public readonly ushort PrimaryId; + public readonly ushort SecondaryId; + public readonly EquipSlot EquipSlot = EquipSlot.Unknown; + public readonly CustomizationType CustomizationType = CustomizationType.Unknown; + + private static bool ValidType( ObjectType type ) { - private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex - private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex - private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex - private const string Pir = @"\k'PrimaryId'"; // language=regex - private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex - private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex - private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex - private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex - private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex - private const string Ext = @"\.meta"; - - // These are the valid regexes for .meta files that we are able to support at the moment. - private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}"); - private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}"); - - public readonly ObjectType PrimaryType; - public readonly BodySlot SecondaryType; - public readonly ushort PrimaryId; - public readonly ushort SecondaryId; - public readonly EquipSlot EquipSlot = EquipSlot.Unknown; - public readonly CustomizationType CustomizationType = CustomizationType.Unknown; - - private static bool ValidType( ObjectType type ) + return type switch { - return type switch - { - ObjectType.Accessory => true, - ObjectType.Character => true, - ObjectType.Equipment => true, - ObjectType.DemiHuman => true, - ObjectType.Housing => true, - ObjectType.Monster => true, - ObjectType.Weapon => true, - ObjectType.Icon => false, - ObjectType.Font => false, - ObjectType.Interface => false, - ObjectType.LoadingScreen => false, - ObjectType.Map => false, - ObjectType.Vfx => false, - ObjectType.Unknown => false, - ObjectType.World => false, - _ => false, - }; - } - - public Info( string fileName ) - : this( new GamePath( fileName ) ) - { } - - public Info( GamePath fileName ) - { - PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); - PrimaryId = 0; - SecondaryType = BodySlot.Unknown; - SecondaryId = 0; - if( !ValidType( PrimaryType ) ) - { - PrimaryType = ObjectType.Unknown; - return; - } - - if( PrimaryType == ObjectType.Housing ) - { - var housingMatch = HousingMeta.Match( fileName ); - if( housingMatch.Success ) - { - PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value ); - } - - return; - } - - var match = CharaMeta.Match( fileName ); - if( !match.Success ) - { - return; - } - - PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value ); - if( match.Groups[ "Slot" ].Success ) - { - switch( PrimaryType ) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) ) - { - EquipSlot = tmpSlot; - } - - break; - case ObjectType.Character: - if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) ) - { - CustomizationType = tmpCustom; - } - - break; - } - } - - if( match.Groups[ "SecondaryType" ].Success - && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) - { - SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); - } - } + ObjectType.Accessory => true, + ObjectType.Character => true, + ObjectType.Equipment => true, + ObjectType.DemiHuman => true, + ObjectType.Housing => true, + ObjectType.Monster => true, + ObjectType.Weapon => true, + ObjectType.Icon => false, + ObjectType.Font => false, + ObjectType.Interface => false, + ObjectType.LoadingScreen => false, + ObjectType.Map => false, + ObjectType.Vfx => false, + ObjectType.Unknown => false, + ObjectType.World => false, + _ => false, + }; } - public readonly uint Version; - public readonly string FilePath; - public readonly List< MetaManipulation > Manipulations = new(); + public Info( string fileName ) + : this( new GamePath( fileName ) ) + { } - private static string ReadNullTerminated( BinaryReader reader ) + public Info( GamePath fileName ) { - var builder = new System.Text.StringBuilder(); - for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) + PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); + PrimaryId = 0; + SecondaryType = BodySlot.Unknown; + SecondaryId = 0; + if( !ValidType( PrimaryType ) ) { - builder.Append( c ); + PrimaryType = ObjectType.Unknown; + return; } - return builder.ToString(); - } - - private void AddIfNotDefault( MetaManipulation manipulation ) - { - try + if( PrimaryType == ObjectType.Housing ) { - if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) ) + var housingMatch = HousingMeta.Match( fileName ); + if( housingMatch.Success ) { - Manipulations.Add( manipulation ); + PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value ); } - } - catch( Exception e ) - { - PluginLog.Debug( "Skipped {Type}-manipulation:\n{e:l}", manipulation.Type, e ); - } - } - private void DeserializeEqpEntry( Info info, byte[]? data ) - { - if( data == null || !info.EquipSlot.IsEquipment() ) + return; + } + + var match = CharaMeta.Match( fileName ); + if( !match.Success ) { return; } - try + PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value ); + if( match.Groups[ "Slot" ].Success ) { - var value = Eqp.FromSlotAndBytes( info.EquipSlot, data ); - - AddIfNotDefault( MetaManipulation.Eqp( info.EquipSlot, info.PrimaryId, value ) ); - } - catch( ArgumentException ) - { } - } - - private void DeserializeEqdpEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 5; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) - { - var gr = ( GenderRace )reader.ReadUInt32(); - var byteValue = reader.ReadByte(); - if( !gr.IsValid() || !info.EquipSlot.IsEquipment() && !info.EquipSlot.IsAccessory() ) + switch( PrimaryType ) { - continue; - } + case ObjectType.Equipment: + case ObjectType.Accessory: + if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) ) + { + EquipSlot = tmpSlot; + } - var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); - AddIfNotDefault( MetaManipulation.Eqdp( info.EquipSlot, gr, info.PrimaryId, value ) ); - } - } + break; + case ObjectType.Character: + if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) ) + { + CustomizationType = tmpCustom; + } - private void DeserializeGmpEntry( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - using var reader = new BinaryReader( new MemoryStream( data ) ); - var value = ( GmpEntry )reader.ReadUInt32(); - value.UnknownTotal = reader.ReadByte(); - AddIfNotDefault( MetaManipulation.Gmp( info.PrimaryId, value ) ); - } - - private void DeserializeEstEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) - { - var gr = ( GenderRace )reader.ReadUInt16(); - var id = reader.ReadUInt16(); - var value = reader.ReadUInt16(); - if( !gr.IsValid() - || info.PrimaryType == ObjectType.Character && info.SecondaryType != BodySlot.Face && info.SecondaryType != BodySlot.Hair - || info.PrimaryType == ObjectType.Equipment && info.EquipSlot != EquipSlot.Head && info.EquipSlot != EquipSlot.Body ) - { - continue; - } - - AddIfNotDefault( MetaManipulation.Est( info.PrimaryType, info.EquipSlot, gr, info.SecondaryType, id, value ) ); - } - } - - private void DeserializeImcEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) - { - var value = ImcFile.ImageChangeData.Read( reader ); - if( info.PrimaryType == ObjectType.Equipment || info.PrimaryType == ObjectType.Accessory ) - { - AddIfNotDefault( MetaManipulation.Imc( info.EquipSlot, info.PrimaryId, ( ushort )i, value ) ); - } - else - { - AddIfNotDefault( MetaManipulation.Imc( info.PrimaryType, info.SecondaryType, info.PrimaryId - , info.SecondaryId, ( ushort )i, value ) ); + break; } } - } - public TexToolsMeta( byte[] data ) - { - try + if( match.Groups[ "SecondaryType" ].Success + && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) { - using var reader = new BinaryReader( new MemoryStream( data ) ); - Version = reader.ReadUInt32(); - FilePath = ReadNullTerminated( reader ); - var metaInfo = new Info( FilePath ); - var numHeaders = reader.ReadUInt32(); - var headerSize = reader.ReadUInt32(); - var headerStart = reader.ReadUInt32(); - reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); - - List< (MetaType type, uint offset, int size) > entries = new(); - for( var i = 0; i < numHeaders; ++i ) - { - var currentOffset = reader.BaseStream.Position; - var type = ( MetaType )reader.ReadUInt32(); - var offset = reader.ReadUInt32(); - var size = reader.ReadInt32(); - entries.Add( ( type, offset, size ) ); - reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); - } - - byte[]? ReadEntry( MetaType type ) - { - var idx = entries.FindIndex( t => t.type == type ); - if( idx < 0 ) - { - return null; - } - - reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); - return reader.ReadBytes( entries[ idx ].size ); - } - - DeserializeEqpEntry( metaInfo, ReadEntry( MetaType.Eqp ) ); - DeserializeGmpEntry( metaInfo, ReadEntry( MetaType.Gmp ) ); - DeserializeEqdpEntries( metaInfo, ReadEntry( MetaType.Eqdp ) ); - DeserializeEstEntries( metaInfo, ReadEntry( MetaType.Est ) ); - DeserializeImcEntries( metaInfo, ReadEntry( MetaType.Imc ) ); + SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); } - catch( Exception e ) - { - FilePath = ""; - PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); - } - } - - private TexToolsMeta( string filePath, uint version ) - { - FilePath = filePath; - Version = version; - } - - public static TexToolsMeta Invalid = new(string.Empty, 0); - - public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) - { - if( data.Length != 45 && data.Length != 42 ) - { - PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); - return Invalid; - } - - using var s = new MemoryStream( data ); - using var br = new BinaryReader( s ); - var flag = br.ReadByte(); - var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); - - var ret = new TexToolsMeta( filePath, version ); - - var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); - if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) - { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); - return Invalid; - } - - var gender = br.ReadByte(); - if( gender != 1 && gender != 0 ) - { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); - return Invalid; - } - - if( gender == 1 ) - { - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) ); - - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) ); - } - else - { - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) ); - ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) ); - } - - return ret; } } + + public readonly uint Version; + public readonly string FilePath; + public readonly List< EqpManipulation > EqpManipulations = new(); + public readonly List< GmpManipulation > GmpManipulations = new(); + public readonly List< EqdpManipulation > EqdpManipulations = new(); + public readonly List< EstManipulation > EstManipulations = new(); + public readonly List< RspManipulation > RspManipulations = new(); + public readonly List< ImcManipulation > ImcManipulations = new(); + + private void DeserializeEqpEntry( Info info, byte[]? data ) + { + if( data == null || !info.EquipSlot.IsEquipment() ) + { + return; + } + + var value = Eqp.FromSlotAndBytes( info.EquipSlot, data ); + var def = new EqpManipulation( ExpandedEqpFile.GetDefault( info.PrimaryId ), info.EquipSlot, info.PrimaryId ); + var manip = new EqpManipulation( value, info.EquipSlot, info.PrimaryId ); + if( def.Entry != manip.Entry ) + { + EqpManipulations.Add( manip ); + } + } + + private void DeserializeEqdpEntries( Info info, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 5; + using var reader = new BinaryReader( new MemoryStream( data ) ); + for( var i = 0; i < num; ++i ) + { + var gr = ( GenderRace )reader.ReadUInt32(); + var byteValue = reader.ReadByte(); + if( !gr.IsValid() || !info.EquipSlot.IsEquipment() && !info.EquipSlot.IsAccessory() ) + { + continue; + } + + var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); + var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, info.EquipSlot.IsAccessory(), info.PrimaryId ), info.EquipSlot, + gr.Split().Item1, gr.Split().Item2, info.PrimaryId ); + var manip = new EqdpManipulation( value, info.EquipSlot, gr.Split().Item1, gr.Split().Item2, info.PrimaryId ); + if( def.Entry != manip.Entry ) + { + EqdpManipulations.Add( manip ); + } + } + } + + private void DeserializeGmpEntry( Info info, byte[]? data ) + { + if( data == null ) + { + return; + } + + using var reader = new BinaryReader( new MemoryStream( data ) ); + var value = ( GmpEntry )reader.ReadUInt32(); + value.UnknownTotal = reader.ReadByte(); + var def = ExpandedGmpFile.GetDefault( info.PrimaryId ); + if( value != def ) + { + GmpManipulations.Add( new GmpManipulation( value, info.PrimaryId ) ); + } + } + + private void DeserializeEstEntries( Info info, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 6; + using var reader = new BinaryReader( new MemoryStream( data ) ); + for( var i = 0; i < num; ++i ) + { + var gr = ( GenderRace )reader.ReadUInt16(); + var id = reader.ReadUInt16(); + var value = reader.ReadUInt16(); + var type = ( info.SecondaryType, info.EquipSlot ) switch + { + (BodySlot.Face, _) => EstManipulation.EstType.Face, + (BodySlot.Hair, _) => EstManipulation.EstType.Hair, + (_, EquipSlot.Head) => EstManipulation.EstType.Head, + (_, EquipSlot.Body) => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; + if( !gr.IsValid() || type == 0 ) + { + continue; + } + + var def = EstFile.GetDefault( type, gr, id ); + if( def != value ) + { + EstManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) ); + } + } + } + + private void DeserializeImcEntries( Info info, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 6; + using var reader = new BinaryReader( new MemoryStream( data ) ); + var values = reader.ReadStructures< ImcEntry >( num ); + ushort i = 0; + if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) + { + foreach( var value in values ) + { + ImcEntry def; + if( !value.Equals( def ) ) + ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) ); + ++i; + } + } + else + { + foreach( var value in values ) + { + ImcEntry def; + if( !value.Equals( def ) ) + ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) ); + ++i; + } + } + } + + public TexToolsMeta( byte[] data ) + { + try + { + //using var reader = new BinaryReader( new MemoryStream( data ) ); + //Version = reader.ReadUInt32(); + //FilePath = ReadNullTerminated( reader ); + //var metaInfo = new Info( FilePath ); + //var numHeaders = reader.ReadUInt32(); + //var headerSize = reader.ReadUInt32(); + //var headerStart = reader.ReadUInt32(); + //reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); + // + //List< (MetaType type, uint offset, int size) > entries = new(); + //for( var i = 0; i < numHeaders; ++i ) + //{ + // var currentOffset = reader.BaseStream.Position; + // var type = ( MetaType )reader.ReadUInt32(); + // var offset = reader.ReadUInt32(); + // var size = reader.ReadInt32(); + // entries.Add( ( type, offset, size ) ); + // reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); + //} + // + //byte[]? ReadEntry( MetaType type ) + //{ + // var idx = entries.FindIndex( t => t.type == type ); + // if( idx < 0 ) + // { + // return null; + // } + // + // reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); + // return reader.ReadBytes( entries[ idx ].size ); + //} + // + //DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); + //DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); + //DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); + //DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); + //DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); + } + catch( Exception e ) + { + FilePath = ""; + PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); + } + } + + private TexToolsMeta( string filePath, uint version ) + { + FilePath = filePath; + Version = version; + } + + public static TexToolsMeta Invalid = new(string.Empty, 0); + + public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) + { + if( data.Length != 45 && data.Length != 42 ) + { + PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); + return Invalid; + } + + using var s = new MemoryStream( data ); + using var br = new BinaryReader( s ); + var flag = br.ReadByte(); + var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); + + var ret = new TexToolsMeta( filePath, version ); + + var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); + if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); + return Invalid; + } + + var gender = br.ReadByte(); + if( gender != 1 && gender != 0 ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); + return Invalid; + } + + //if( gender == 1 ) + //{ + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) ); + // + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) ); + //} + //else + //{ + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) ); + // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) ); + //} + // + return ret; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 35f274c4..9164f6e8 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; @@ -14,10 +15,11 @@ public unsafe struct CharacterUtility public const int HairEstIdx = 65; public const int BodyEstIdx = 66; public const int HeadEstIdx = 67; + public const int NumEqdpFiles = 2 * 28; - public static int EqdpIdx( ushort raceCode, bool accessory ) + public static int EqdpIdx( GenderRace raceCode, bool accessory ) => ( accessory ? 28 : 0 ) - + raceCode switch + + ( int )raceCode switch { 0101 => 2, 0201 => 3, @@ -65,7 +67,7 @@ public unsafe struct CharacterUtility public ResourceHandle* Resource( int idx ) => ( ResourceHandle* )Resources[ idx ]; - public ResourceHandle* EqdpResource( ushort raceCode, bool accessory ) + public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) => Resource( EqdpIdx( raceCode, accessory ) ); [FieldOffset( 8 + HumanCmpIdx * 8 )] diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index d0f05f07..6ed2cc8e 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -9,6 +9,9 @@ public unsafe struct ResourceHandle [StructLayout( LayoutKind.Explicit )] public struct DataIndirection { + [FieldOffset( 0x00 )] + public void** VTable; + [FieldOffset( 0x10 )] public byte* DataPtr; diff --git a/Penumbra/Meta/EntryExtensions.cs b/Penumbra/Meta/EntryExtensions.cs deleted file mode 100644 index 293d135e..00000000 --- a/Penumbra/Meta/EntryExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Penumbra.Meta -{ - public static class EqdpEntryExtensions - { - public static bool Apply( this ref EqdpEntry entry, MetaManipulation manipulation ) - { - if( manipulation.Type != MetaType.Eqdp ) - { - return false; - } - - var mask = Eqdp.Mask( manipulation.EqdpIdentifier.Slot ); - var result = ( entry & ~mask ) | manipulation.EqdpValue; - var ret = result == entry; - entry = result; - return ret; - } - - public static EqdpEntry Reduce( this EqdpEntry entry, EquipSlot slot ) - => entry & Eqdp.Mask( slot ); - } - - - public static class EqpEntryExtensions - { - public static bool Apply( this ref EqpEntry entry, MetaManipulation manipulation ) - { - if( manipulation.Type != MetaType.Eqp ) - { - return false; - } - - var mask = Eqp.Mask( manipulation.EqpIdentifier.Slot ); - var result = ( entry & ~mask ) | manipulation.EqpValue; - var ret = result != entry; - entry = result; - return ret; - } - - public static EqpEntry Reduce( this EqpEntry entry, EquipSlot slot ) - => entry & Eqp.Mask( slot ); - } - - public static class GmpEntryExtension - { - public static GmpEntry Apply( this ref GmpEntry entry, MetaManipulation manipulation ) - { - if( manipulation.Type != MetaType.Gmp ) - { - return entry; - } - - entry.Value = manipulation.GmpValue.Value; - return entry; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index d02a3486..e54cc3e4 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -1,73 +1,42 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; +using System.Collections.Generic; -namespace Penumbra.Meta.Files +namespace Penumbra.Meta.Files; + +public sealed unsafe class CmpFile : MetaBaseFile { - public class CmpFile + private const int RacialScalingStart = 0x2A800; + + public float this[ SubRace subRace, RspAttribute attribute ] { - private const int RacialScalingStart = 0x2A800; + get => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ); + set => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ) = value; + } - private readonly byte[] _byteData = new byte[RacialScalingStart]; - private readonly RspEntry[] _rspEntries; + public override void Reset() + => Functions.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); - public CmpFile( byte[] bytes ) + public void Reset( IEnumerable< (SubRace, RspAttribute) > entries ) + { + foreach( var (r, a) in entries ) { - if( bytes.Length < RacialScalingStart ) - { - throw new ArgumentOutOfRangeException(); - } - - Array.Copy( bytes, _byteData, RacialScalingStart ); - var rspEntryNum = ( bytes.Length - RacialScalingStart ) / RspEntry.ByteSize; - var tmp = new List< RspEntry >( rspEntryNum ); - for( var i = 0; i < rspEntryNum; ++i ) - { - tmp.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) ); - } - - _rspEntries = tmp.ToArray(); + this[ r, a ] = GetDefault( r, a ); } + } - public RspEntry this[ SubRace subRace ] - => _rspEntries[ subRace.ToRspIndex() ]; + public CmpFile() + : base( CharacterUtility.HumanCmpIdx ) + { + AllocateData( DefaultData.Length ); + Reset(); + } - public bool Set( SubRace subRace, RspAttribute attribute, float value ) - { - var entry = _rspEntries[ subRace.ToRspIndex() ]; - var oldValue = entry[ attribute ]; - if( oldValue == value ) - { - return false; - } - - entry[ attribute ] = value; - return true; - } - - public byte[] WriteBytes() - { - using var s = new MemoryStream( RacialScalingStart + _rspEntries.Length * RspEntry.ByteSize ); - s.Write( _byteData, 0, _byteData.Length ); - foreach( var entry in _rspEntries ) - { - var bytes = entry.ToBytes(); - s.Write( bytes, 0, bytes.Length ); - } - - return s.ToArray(); - } - - private CmpFile( byte[] data, RspEntry[] entries ) - { - _byteData = data.ToArray(); - _rspEntries = entries.Select( e => new RspEntry( e ) ).ToArray(); - } - - public CmpFile Clone() - => new( _byteData, _rspEntries ); + public static float GetDefault( SubRace subRace, RspAttribute attribute ) + { + var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ CharacterUtility.HumanCmpIdx ].Address; + return *( float* )( data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index fa373a4f..ae64ad1c 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -1,214 +1,137 @@ using System; -using System.IO; -using System.Linq; -using Lumina.Data; +using System.Collections.Generic; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; -namespace Penumbra.Meta.Files +namespace Penumbra.Meta.Files; + +// EQDP file structure: +// [Identifier][BlockSize:ushort][BlockCount:ushort] +// BlockCount x [BlockHeader:ushort] +// Containing offsets for blocks, ushort.Max means collapsed. +// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2. +// ExpandedBlockCount x [Entry] + +// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it. +public sealed unsafe class ExpandedEqdpFile : MetaBaseFile { - // EQDP file structure: - // [Identifier][BlockSize:ushort][BlockCount:ushort] - // BlockCount x [BlockHeader:ushort] - // Containing offsets for blocks, ushort.Max means collapsed. - // Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2. - // ExpandedBlockCount x [Entry] - public class EqdpFile + private const ushort BlockHeaderSize = 2; + private const ushort PreambleSize = 4; + private const ushort CollapsedBlock = ushort.MaxValue; + private const ushort IdentifierSize = 2; + private const ushort EqdpEntrySize = 2; + private const int FileAlignment = 1 << 9; + + public readonly int DataOffset; + + public ushort Identifier + => *( ushort* )Data; + + public ushort BlockSize + => *( ushort* )( Data + 2 ); + + public ushort BlockCount + => *( ushort* )( Data + 4 ); + + public int Count + => ( Length - DataOffset ) / EqdpEntrySize; + + public EqdpEntry this[ int idx ] { - private const ushort BlockHeaderSize = 2; - private const ushort PreambleSize = 4; - private const ushort CollapsedBlock = ushort.MaxValue; - private const ushort IdentifierSize = 2; - private const ushort EqdpEntrySize = 2; - private const int FileAlignment = 1 << 9; - - private EqdpFile( EqdpFile clone ) + get { - Identifier = clone.Identifier; - BlockSize = clone.BlockSize; - TotalBlockCount = clone.TotalBlockCount; - ExpandedBlockCount = clone.ExpandedBlockCount; - Blocks = new EqdpEntry[clone.TotalBlockCount][]; - for( var i = 0; i < TotalBlockCount; ++i ) + if( idx >= Count || idx < 0 ) { - if( clone.Blocks[ i ] != null ) - { - Blocks[ i ] = ( EqdpEntry[] )clone.Blocks[ i ]!.Clone(); - } + throw new IndexOutOfRangeException(); } + + return *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ); } - - public ref EqdpEntry this[ ushort setId ] - => ref GetTrueEntry( setId ); - - - public EqdpFile Clone() - => new( this ); - - private ushort Identifier { get; } - private ushort BlockSize { get; } - private ushort TotalBlockCount { get; } - private ushort ExpandedBlockCount { get; set; } - private EqdpEntry[]?[] Blocks { get; } - - private int BlockIdx( ushort id ) - => ( ushort )( id / BlockSize ); - - private int SubIdx( ushort id ) - => ( ushort )( id % BlockSize ); - - private bool ExpandBlock( int idx ) + set { - if( idx < TotalBlockCount && Blocks[ idx ] == null ) + if( idx >= Count || idx < 0 ) { - Blocks[ idx ] = new EqdpEntry[BlockSize]; - ++ExpandedBlockCount; - return true; + throw new IndexOutOfRangeException(); } - return false; + *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ) = value; } + } - private bool CollapseBlock( int idx ) + public override void Reset() + { + var def = ( byte* )DefaultData.Data; + Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); + + var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); + var dataBasePtr = ( byte* )( controlPtr + BlockCount ); + var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); + for( var i = 0; i < BlockCount; ++i ) { - if( idx >= TotalBlockCount || Blocks[ idx ] == null ) + if( controlPtr[ i ] == CollapsedBlock ) { - return false; - } - - Blocks[ idx ] = null; - --ExpandedBlockCount; - return true; - } - - public bool SetEntry( ushort idx, EqdpEntry entry ) - { - var block = BlockIdx( idx ); - if( block >= TotalBlockCount ) - { - return false; - } - - if( entry != 0 ) - { - ExpandBlock( block ); - if( Blocks[ block ]![ SubIdx( idx ) ] != entry ) - { - Blocks[ block ]![ SubIdx( idx ) ] = entry; - return true; - } + Functions.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); } else { - var array = Blocks[ block ]; - if( array != null ) - { - array[ SubIdx( idx ) ] = entry; - if( array.All( e => e == 0 ) ) - { - CollapseBlock( block ); - } - - return true; - } + Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); } - return false; - } - - public EqdpEntry GetEntry( ushort idx ) - { - var block = BlockIdx( idx ); - var array = block < Blocks.Length ? Blocks[ block ] : null; - return array?[ SubIdx( idx ) ] ?? 0; - } - - private ref EqdpEntry GetTrueEntry( ushort idx ) - { - var block = BlockIdx( idx ); - if( block >= TotalBlockCount ) - { - throw new ArgumentOutOfRangeException(); - } - - ExpandBlock( block ); - var array = Blocks[ block ]!; - return ref array[ SubIdx( idx ) ]; - } - - private void WriteHeaders( BinaryWriter bw ) - { - ushort offset = 0; - foreach( var block in Blocks ) - { - if( block == null ) - { - bw.Write( CollapsedBlock ); - continue; - } - - bw.Write( offset ); - offset += BlockSize; - } - } - - private static void WritePadding( BinaryWriter bw, int paddingSize ) - { - var buffer = new byte[paddingSize]; - bw.Write( buffer, 0, paddingSize ); - } - - private void WriteBlocks( BinaryWriter bw ) - { - foreach( var entry in Blocks.Where( block => block != null ) - .SelectMany( block => block! ) ) - { - bw.Write( ( ushort )entry ); - } - } - - public byte[] WriteBytes() - { - var dataSize = PreambleSize + IdentifierSize + BlockHeaderSize * TotalBlockCount + ExpandedBlockCount * BlockSize * EqdpEntrySize; - var paddingSize = FileAlignment - ( dataSize & ( FileAlignment - 1 ) ); - using var mem = - new MemoryStream( dataSize + paddingSize ); - using var bw = new BinaryWriter( mem ); - bw.Write( Identifier ); - bw.Write( BlockSize ); - bw.Write( TotalBlockCount ); - - WriteHeaders( bw ); - WriteBlocks( bw ); - WritePadding( bw, paddingSize ); - - return mem.ToArray(); - } - - public EqdpFile( FileResource file ) - { - file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); - - Identifier = file.Reader.ReadUInt16(); - BlockSize = file.Reader.ReadUInt16(); - TotalBlockCount = file.Reader.ReadUInt16(); - Blocks = new EqdpEntry[TotalBlockCount][]; - ExpandedBlockCount = 0; - for( var i = 0; i < TotalBlockCount; ++i ) - { - var offset = file.Reader.ReadUInt16(); - if( offset != CollapsedBlock ) - { - ExpandBlock( ( ushort )i ); - } - } - - foreach( var array in Blocks.Where( array => array != null ) ) - { - for( var i = 0; i < BlockSize; ++i ) - { - array![ i ] = ( EqdpEntry )file.Reader.ReadUInt16(); - } - } + myDataPtr += BlockSize; } } + + public void Reset( IEnumerable< int > entries ) + { + foreach( var entry in entries ) + { + this[ entry ] = GetDefault( entry ); + } + } + + public ExpandedEqdpFile( GenderRace raceCode, bool accessory ) + : base( CharacterUtility.EqdpIdx( raceCode, accessory ) ) + { + var def = ( byte* )DefaultData.Data; + var blockSize = *( ushort* )( def + IdentifierSize ); + var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 ); + var totalBlockSize = blockSize * EqdpEntrySize; + + DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize; + + var fullLength = DataOffset + totalBlockCount * totalBlockSize; + fullLength += ( FileAlignment - ( Length & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); + AllocateData( fullLength ); + Reset(); + } + + public EqdpEntry GetDefault( int setIdx ) + => GetDefault( Index, setIdx ); + + public static EqdpEntry GetDefault( int fileIdx, int setIdx ) + { + var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; + var blockSize = *( ushort* )( data + IdentifierSize ); + var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 ); + + var blockIdx = setIdx / blockSize; + if( blockIdx >= totalBlockCount ) + { + return 0; + } + + var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ]; + if( block == CollapsedBlock ) + { + return 0; + } + + var blockData = ( EqdpEntry* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block ); + return *( blockData + blockIdx % blockSize ); + } + + public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) + => GetDefault( CharacterUtility.EqdpIdx( raceCode, accessory ), setIdx ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpFile.cs b/Penumbra/Meta/Files/EqpFile.cs deleted file mode 100644 index 7de89fdb..00000000 --- a/Penumbra/Meta/Files/EqpFile.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Lumina.Data; -using Penumbra.GameData.Structs; - -namespace Penumbra.Meta.Files -{ - // EQP Structure: - // 64 x [Block collapsed or not bit] - // 159 x [EquipmentParameter:ulong] - // (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong] - // Item 0 does not exist and is sent to Item 1 instead. - public sealed class EqpFile : EqpGmpBase - { - private readonly EqpEntry[]?[] _entries = new EqpEntry[TotalBlockCount][]; - - protected override ulong ControlBlock - { - get => ( ulong )_entries[ 0 ]![ 0 ]; - set => _entries[ 0 ]![ 0 ] = ( EqpEntry )value; - } - - private EqpFile( EqpFile clone ) - { - ExpandedBlockCount = clone.ExpandedBlockCount; - _entries = clone.Clone( clone._entries ); - } - - public byte[] WriteBytes() - => WriteBytes( _entries, e => ( ulong )e ); - - public EqpFile Clone() - => new( this ); - - public EqpFile( FileResource file ) - => ReadFile( _entries, file, I => ( EqpEntry )I ); - - public EqpEntry GetEntry( ushort setId ) - => GetEntry( _entries, setId, ( EqpEntry )0 ); - - public bool SetEntry( ushort setId, EqpEntry entry ) - => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 ); - - public ref EqpEntry this[ ushort setId ] - => ref GetTrueEntry( _entries, setId ); - } - - public class EqpGmpBase - { - protected const ushort ParameterSize = 8; - protected const ushort BlockSize = 160; - protected const ushort TotalBlockCount = 64; - - protected int ExpandedBlockCount { get; set; } - - private static int BlockIdx( ushort idx ) - => idx / BlockSize; - - private static int SubIdx( ushort idx ) - => idx % BlockSize; - - protected virtual ulong ControlBlock { get; set; } - - protected T[]?[] Clone< T >( T[]?[] clone ) - { - var ret = new T[TotalBlockCount][]; - for( var i = 0; i < TotalBlockCount; ++i ) - { - if( clone[ i ] != null ) - { - ret[ i ] = ( T[] )clone[ i ]!.Clone(); - } - } - - return ret; - } - - protected EqpGmpBase() - { } - - protected bool ExpandBlock< T >( T[]?[] blocks, int idx ) - { - if( idx >= TotalBlockCount || blocks[ idx ] != null ) - { - return false; - } - - blocks[ idx ] = new T[BlockSize]; - ++ExpandedBlockCount; - ControlBlock |= 1ul << idx; - return true; - } - - protected bool CollapseBlock< T >( T[]?[] blocks, int idx ) - { - if( idx >= TotalBlockCount || blocks[ idx ] == null ) - { - return false; - } - - blocks[ idx ] = null; - --ExpandedBlockCount; - ControlBlock &= ~( 1ul << idx ); - return true; - } - - protected T GetEntry< T >( T[]?[] blocks, ushort idx, T defaultEntry ) - { - // Skip the zeroth item. - idx = idx == 0 ? ( ushort )1 : idx; - var block = BlockIdx( idx ); - var array = block < blocks.Length ? blocks[ block ] : null; - if( array == null ) - { - return defaultEntry; - } - - return array[ SubIdx( idx ) ]; - } - - protected ref T GetTrueEntry< T >( T[]?[] blocks, ushort idx ) - { - // Skip the zeroth item. - idx = idx == 0 ? ( ushort )1 : idx; - var block = BlockIdx( idx ); - if( block >= TotalBlockCount ) - { - throw new ArgumentOutOfRangeException(); - } - - ExpandBlock( blocks, block ); - var array = blocks[ block ]!; - return ref array[ SubIdx( idx ) ]; - } - - protected byte[] WriteBytes< T >( T[]?[] blocks, Func< T, ulong > transform ) - { - var dataSize = ExpandedBlockCount * BlockSize * ParameterSize; - using var mem = new MemoryStream( dataSize ); - using var bw = new BinaryWriter( mem ); - - foreach( var parameter in blocks.Where( array => array != null ) - .SelectMany( array => array! ) ) - { - bw.Write( transform( parameter ) ); - } - - return mem.ToArray(); - } - - protected void ReadFile< T >( T[]?[] blocks, FileResource file, Func< ulong, T > convert ) - { - file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); - var blockBits = file.Reader.ReadUInt64(); - // reset to 0 and just put the bitmask in the first block - // item 0 is not accessible and it simplifies printing. - file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); - - ExpandedBlockCount = 0; - for( var i = 0; i < TotalBlockCount; ++i ) - { - var flag = 1ul << i; - if( ( blockBits & flag ) != flag ) - { - continue; - } - - ++ExpandedBlockCount; - - var tmp = new T[BlockSize]; - for( var j = 0; j < BlockSize; ++j ) - { - tmp[ j ] = convert( file.Reader.ReadUInt64() ); - } - - blocks[ i ] = tmp; - } - } - - protected bool SetEntry< T >( T[]?[] blocks, ushort idx, T entry, Func< T, bool > isDefault, Func< T, T, bool > isEqual ) - { - var block = BlockIdx( idx ); - if( block >= TotalBlockCount ) - { - return false; - } - - if( !isDefault( entry ) ) - { - ExpandBlock( blocks, block ); - if( !isEqual( entry, blocks[ block ]![ SubIdx( idx ) ] ) ) - { - blocks[ block ]![ SubIdx( idx ) ] = entry; - return true; - } - } - else - { - var array = blocks[ block ]; - if( array != null ) - { - array[ SubIdx( idx ) ] = entry; - if( array.All( e => e!.Equals( 0ul ) ) ) - { - CollapseBlock( blocks, block ); - } - - return true; - } - } - - return false; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs new file mode 100644 index 00000000..62aff162 --- /dev/null +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Penumbra.GameData.Structs; +using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Files; + +// EQP/GMP Structure: +// 64 x [Block collapsed or not bit] +// 159 x [EquipmentParameter:ulong] +// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong] +// Item 0 does not exist and is sent to Item 1 instead. +public unsafe class ExpandedEqpGmpBase : MetaBaseFile +{ + protected const int BlockSize = 160; + protected const int NumBlocks = 64; + protected const int EntrySize = 8; + protected const int MaxSize = BlockSize * NumBlocks * EntrySize; + + public const int Count = BlockSize * NumBlocks; + + public ulong ControlBlock + => *( ulong* )Data; + + protected T Get< T >( int idx ) where T : unmanaged + { + return idx switch + { + >= Count => throw new IndexOutOfRangeException(), + <= 1 => *( ( T* )Data + 1 ), + _ => *( ( T* )Data + idx ), + }; + } + + protected void Set< T >( int idx, T value ) where T : unmanaged + { + idx = idx switch + { + >= Count => throw new IndexOutOfRangeException(), + <= 0 => 1, + _ => idx, + }; + + *( ( T* )Data + idx ) = value; + } + + protected virtual void SetEmptyBlock( int idx ) + { + Functions.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); + } + + public sealed override void Reset() + { + var ptr = ( byte* )DefaultData.Data; + var controlBlock = *( ulong* )ptr; + *( ulong* )ptr = ulong.MaxValue; + for( var i = 0; i < 64; ++i ) + { + var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; + if( !collapsed ) + { + Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + i * BlockSize * EntrySize, BlockSize * EntrySize ); + } + else + { + SetEmptyBlock( i ); + } + } + } + + public ExpandedEqpGmpBase( bool gmp ) + : base( gmp ? CharacterUtility.GmpIdx : CharacterUtility.EqpIdx ) + { + AllocateData( MaxSize ); + Reset(); + } + + protected static T GetDefault< T >( int fileIdx, int setIdx, T def ) where T : unmanaged + { + var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; + if( setIdx == 0 ) + { + setIdx = 1; + } + + var blockIdx = setIdx / BlockSize; + if( blockIdx >= NumBlocks ) + { + return def; + } + + var control = *( ulong* )data; + var blockBit = 1ul << blockIdx; + if( ( control & blockBit ) == 0 ) + { + return def; + } + + var count = BitOperations.PopCount( control & ( blockBit - 1 ) ); + var idx = setIdx % BlockSize; + var ptr = ( T* )data + BlockSize * count + idx; + return *ptr; + } +} + +public sealed class ExpandedEqpFile : ExpandedEqpGmpBase +{ + public ExpandedEqpFile() + : base( false ) + { } + + public EqpEntry this[ int idx ] + { + get => Get< EqpEntry >( idx ); + set => Set( idx, value ); + } + + public static EqpEntry GetDefault( int setIdx ) + => GetDefault( CharacterUtility.EqpIdx, setIdx, Eqp.DefaultEntry ); + + protected override unsafe void SetEmptyBlock( int idx ) + { + var blockPtr = ( ulong* )( Data + idx * BlockSize * EntrySize ); + var endPtr = blockPtr + BlockSize; + for( var ptr = blockPtr; ptr < endPtr; ++ptr ) + { + *ptr = ( ulong )Eqp.DefaultEntry; + } + } + + public void Reset( IEnumerable< int > entries ) + { + foreach( var entry in entries ) + { + this[ entry ] = GetDefault( entry ); + } + } +} + +public sealed class ExpandedGmpFile : ExpandedEqpGmpBase +{ + public ExpandedGmpFile() + : base( true ) + { } + + public GmpEntry this[ int idx ] + { + get => Get< GmpEntry >( idx ); + set => Set( idx, value ); + } + + public static GmpEntry GetDefault( int setIdx ) + => GetDefault( CharacterUtility.GmpIdx, setIdx, GmpEntry.Default ); + + public void Reset( IEnumerable< int > entries ) + { + foreach( var entry in entries ) + { + this[ entry ] = GetDefault( entry ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 18a53062..4eb53be3 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Lumina.Data; +using System; +using System.Runtime.InteropServices; using Penumbra.GameData.Enums; +using Penumbra.GameData.Util; +using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -11,152 +11,192 @@ namespace Penumbra.Meta.Files; // Apparently entries need to be sorted. // #NumEntries x [SetId : UInt16] [RaceId : UInt16] // #NumEntries x [SkeletonId : UInt16] -public class EstFile +public sealed unsafe class EstFile : MetaBaseFile { private const ushort EntryDescSize = 4; private const ushort EntrySize = 2; + private const int IncreaseSize = 100; - private readonly SortedList< GenderRace, SortedList< ushort, ushort > > _entries = new(); - private uint NumEntries { get; set; } + public int Count + => *( int* )Data; - private EstFile( EstFile clone ) + private int Size + => 4 + Count * ( EntryDescSize + EntrySize ); + + public enum EstEntryChange { - NumEntries = clone.NumEntries; - _entries = new SortedList< GenderRace, SortedList< ushort, ushort > >( clone._entries.Count ); - foreach( var (genderRace, data) in clone._entries ) + Unchanged, + Changed, + Added, + Removed, + } + + public ushort this[ GenderRace genderRace, ushort setId ] + { + get { - var dict = new SortedList< ushort, ushort >( data.Count ); - foreach( var (setId, value) in data ) + var (idx, exists) = FindEntry( genderRace, setId ); + if( !exists ) { - dict.Add( setId, value ); + return 0; } - _entries.Add( genderRace, dict ); + return *( ushort* )( Data + EntryDescSize * ( Count + 1 ) + EntrySize * idx ); + } + set => SetEntry( genderRace, setId, value ); + } + + private void InsertEntry( int idx, GenderRace genderRace, ushort setId, ushort skeletonId ) + { + if( Length < Size + EntryDescSize + EntrySize ) + { + var data = Data; + var length = Length; + AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) ); + Functions.MemCpyUnchecked( Data, data, length ); + Functions.MemSet( Data + length, 0, IncreaseSize * ( EntryDescSize + EntrySize ) ); + GC.RemoveMemoryPressure( length ); + Marshal.FreeHGlobal( ( IntPtr )data ); + } + + var control = ( uint* )( Data + 4 ); + var entries = ( ushort* )( Data + 4 * ( Count + 1 ) ); + + for( var i = Count; i > idx; --i ) + { + *( entries + i + 2 ) = entries[ i - 1 ]; + } + + for( var i = idx - 1; i >= 0; --i ) + { + *( entries + i + 2 ) = entries[ i ]; + } + + for( var i = Count; i > idx; --i ) + { + *( control + i ) = control[ i - 1 ]; + } + + *( int* )Data = Count + 1; + + *( ushort* )control = setId; + *( ( ushort* )control + 1 ) = ( ushort )genderRace; + control[ idx ] = skeletonId; + } + + private void RemoveEntry( int idx ) + { + var entries = ( ushort* )( Data + 4 * Count ); + var control = ( uint* )( Data + 4 ); + *( int* )Data = Count - 1; + var count = Count; + + for( var i = idx; i < count; ++i ) + { + control[ i ] = control[ i + 1 ]; + } + + for( var i = 0; i < count; ++i ) + { + entries[ i ] = entries[ i + 1 ]; + } + + entries[ count ] = 0; + entries[ count + 1 ] = 0; + entries[ count + 2 ] = 0; + } + + [StructLayout( LayoutKind.Sequential, Size = 4 )] + private struct Info : IComparable< Info > + { + public readonly ushort SetId; + public readonly GenderRace GenderRace; + + public Info( GenderRace gr, ushort setId ) + { + GenderRace = gr; + SetId = setId; + } + + public int CompareTo( Info other ) + { + var genderRaceComparison = GenderRace.CompareTo( other.GenderRace ); + return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo( other.SetId ); } } - public EstFile Clone() - => new(this); - - private bool DeleteEntry( GenderRace gr, ushort setId ) + private static (int, bool) FindEntry( ReadOnlySpan< Info > data, GenderRace genderRace, ushort setId ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) - { - return false; - } - - if( !setDict.ContainsKey( setId ) ) - { - return false; - } - - setDict.Remove( setId ); - if( setDict.Count == 0 ) - { - _entries.Remove( gr ); - } - - --NumEntries; - return true; + var idx = data.BinarySearch( new Info( genderRace, setId ) ); + return idx < 0 ? ( ~idx, false ) : ( idx, true ); } - private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry ) + private (int, bool) FindEntry( GenderRace genderRace, ushort setId ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) - { - _entries[ gr ] = new SortedList< ushort, ushort >(); - setDict = _entries[ gr ]; - } + var span = new ReadOnlySpan< Info >( Data + 4, Count ); + return FindEntry( span, genderRace, setId ); + } - if( setDict.TryGetValue( setId, out var oldEntry ) ) + public EstEntryChange SetEntry( GenderRace genderRace, ushort setId, ushort skeletonId ) + { + var (idx, exists) = FindEntry( genderRace, setId ); + if( exists ) { - if( oldEntry == entry ) + var value = *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ); + if( value == skeletonId ) { - return ( false, false ); + return EstEntryChange.Unchanged; } - setDict[ setId ] = entry; - return ( false, true ); + if( skeletonId == 0 ) + { + RemoveEntry( idx ); + return EstEntryChange.Removed; + } + + *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ) = skeletonId; + return EstEntryChange.Changed; } - setDict[ setId ] = entry; - return ( true, true ); + if( skeletonId == 0 ) + { + return EstEntryChange.Unchanged; + } + + InsertEntry( idx, genderRace, setId, skeletonId ); + return EstEntryChange.Added; } - public bool SetEntry( GenderRace gr, ushort setId, ushort entry ) + public override void Reset() { - if( entry == 0 ) - { - return DeleteEntry( gr, setId ); - } - - var (addedNew, changed) = AddEntry( gr, setId, entry ); - if( !addedNew ) - { - return changed; - } - - ++NumEntries; - return true; + var (d, length) = DefaultData; + var data = ( byte* )d; + Functions.MemCpyUnchecked( Data, data, length ); + Functions.MemSet( Data + length, 0, Length - length ); } - public ushort GetEntry( GenderRace gr, ushort setId ) + public EstFile( EstManipulation.EstType estType ) + : base( ( int )estType ) { - if( !_entries.TryGetValue( gr, out var setDict ) ) + var length = DefaultData.Length; + AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) ); + Reset(); + } + + public ushort GetDefault( GenderRace genderRace, ushort setId ) + => GetDefault( ( EstManipulation.EstType )Index, genderRace, setId ); + + public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) + { + var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ ( int )estType ].Address; + var count = *( int* )data; + var span = new ReadOnlySpan< Info >( data + 4, count ); + var (idx, found) = FindEntry( span, genderRace, setId ); + if( !found ) { return 0; } - return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry; - } - - public byte[] WriteBytes() - { - using MemoryStream mem = new(( int )( 4 + ( EntryDescSize + EntrySize ) * NumEntries )); - using BinaryWriter bw = new(mem); - - bw.Write( NumEntries ); - foreach( var kvp1 in _entries ) - { - foreach( var kvp2 in kvp1.Value ) - { - bw.Write( kvp2.Key ); - bw.Write( ( ushort )kvp1.Key ); - } - } - - foreach( var kvp2 in _entries.SelectMany( kvp1 => kvp1.Value ) ) - { - bw.Write( kvp2.Value ); - } - - return mem.ToArray(); - } - - - public EstFile( FileResource file ) - { - file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin ); - NumEntries = file.Reader.ReadUInt32(); - - var currentEntryDescOffset = 4; - var currentEntryOffset = 4 + EntryDescSize * NumEntries; - for( var i = 0; i < NumEntries; ++i ) - { - file.Reader.BaseStream.Seek( currentEntryDescOffset, SeekOrigin.Begin ); - currentEntryDescOffset += EntryDescSize; - var setId = file.Reader.ReadUInt16(); - var raceId = ( GenderRace )file.Reader.ReadUInt16(); - if( !raceId.IsValid() ) - { - continue; - } - - file.Reader.BaseStream.Seek( currentEntryOffset, SeekOrigin.Begin ); - currentEntryOffset += EntrySize; - var entry = file.Reader.ReadUInt16(); - - AddEntry( raceId, setId, entry ); - } + return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/GmpFile.cs b/Penumbra/Meta/Files/GmpFile.cs deleted file mode 100644 index 35603500..00000000 --- a/Penumbra/Meta/Files/GmpFile.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Lumina.Data; -using Penumbra.GameData.Structs; - -namespace Penumbra.Meta.Files -{ - // GmpFiles use the same structure as Eqp Files. - // Entries are also one ulong. - public sealed class GmpFile : EqpGmpBase - { - private readonly GmpEntry[]?[] _entries = new GmpEntry[TotalBlockCount][]; - - protected override ulong ControlBlock - { - get => _entries[ 0 ]![ 0 ]; - set => _entries[ 0 ]![ 0 ] = ( GmpEntry )value; - } - - private GmpFile( GmpFile clone ) - { - ExpandedBlockCount = clone.ExpandedBlockCount; - _entries = clone.Clone( clone._entries ); - } - - public byte[] WriteBytes() - => WriteBytes( _entries, e => ( ulong )e ); - - public GmpFile Clone() - => new( this ); - - public GmpFile( FileResource file ) - => ReadFile( _entries, file, i => ( GmpEntry )i ); - - public GmpEntry GetEntry( ushort setId ) - => GetEntry( _entries, setId, ( GmpEntry )0 ); - - public bool SetEntry( ushort setId, GmpEntry entry ) - => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 ); - - public ref GmpEntry this[ ushort setId ] - => ref GetTrueEntry( _entries, setId ); - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcExtensions.cs b/Penumbra/Meta/Files/ImcExtensions.cs deleted file mode 100644 index 3b97f751..00000000 --- a/Penumbra/Meta/Files/ImcExtensions.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Linq; -using Lumina.Data.Files; -using Penumbra.GameData.Enums; - -namespace Penumbra.Meta.Files -{ - public class InvalidImcVariantException : ArgumentOutOfRangeException - { - public InvalidImcVariantException() - : base( "Trying to manipulate invalid variant." ) - { } - } - - // Imc files are already supported in Lumina, but changing the provided data is not supported. - // We use reflection and extension methods to support changing the data of a given Imc file. - public static class ImcExtensions - { - public static ulong ToInteger( this ImcFile.ImageChangeData imc ) - { - ulong ret = imc.MaterialId; - ret |= ( ulong )imc.DecalId << 8; - ret |= ( ulong )imc.AttributeMask << 16; - ret |= ( ulong )imc.SoundId << 16; - ret |= ( ulong )imc.VfxId << 32; - ret |= ( ulong )imc.ActualMaterialAnimationId() << 40; - return ret; - } - - public static byte ActualMaterialAnimationId( this ImcFile.ImageChangeData imc ) - { - var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance ); - return ( byte )( tmp?.GetValue( imc ) ?? 0 ); - } - - public static ImcFile.ImageChangeData FromValues( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, - byte materialAnimationId ) - { - var ret = new ImcFile.ImageChangeData() - { - DecalId = decalId, - MaterialId = materialId, - VfxId = vfxId, - }; - ret.GetType().GetField( "_AttributeAndSound", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )! - .SetValue( ret, ( ushort )( ( attributeMask & 0x3FF ) | ( soundId << 10 ) ) ); - ret.GetType().GetField( "_AttributeAndSound", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )!.SetValue( ret, materialAnimationId ); - return ret; - } - - public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs ) - => lhs.MaterialId == rhs.MaterialId - && lhs.DecalId == rhs.DecalId - && lhs.AttributeMask == rhs.AttributeMask - && lhs.SoundId == rhs.SoundId - && lhs.VfxId == rhs.VfxId - && lhs.MaterialAnimationId == rhs.MaterialAnimationId; - - private static void WriteBytes( this ImcFile.ImageChangeData variant, BinaryWriter bw ) - { - bw.Write( variant.MaterialId ); - bw.Write( variant.DecalId ); - bw.Write( ( ushort )( variant.AttributeMask | variant.SoundId ) ); - bw.Write( variant.VfxId ); - bw.Write( variant.ActualMaterialAnimationId() ); - } - - public static byte[] WriteBytes( this ImcFile file ) - { - var parts = file.PartMask == 31 ? 5 : 1; - var dataSize = 4 + 6 * parts * ( 1 + file.Count ); - using var mem = new MemoryStream( dataSize ); - using var bw = new BinaryWriter( mem ); - - bw.Write( file.Count ); - bw.Write( file.PartMask ); - for( var i = 0; i < parts; ++i ) - { - file.GetDefaultVariant( i ).WriteBytes( bw ); - } - - for( var i = 0; i < file.Count; ++i ) - { - for( var j = 0; j < parts; ++j ) - { - file.GetVariant( j, i ).WriteBytes( bw ); - } - } - - return mem.ToArray(); - } - - public static ref ImcFile.ImageChangeData GetValue( this ImcFile file, MetaManipulation manipulation ) - { - var parts = file.GetParts(); - var imc = manipulation.ImcIdentifier; - var idx = 0; - if( imc.ObjectType == ObjectType.Equipment || imc.ObjectType == ObjectType.Accessory ) - { - idx = imc.EquipSlot switch - { - EquipSlot.Head => 0, - EquipSlot.Ears => 0, - EquipSlot.Body => 1, - EquipSlot.Neck => 1, - EquipSlot.Hands => 2, - EquipSlot.Wrists => 2, - EquipSlot.Legs => 3, - EquipSlot.RFinger => 3, - EquipSlot.Feet => 4, - EquipSlot.LFinger => 4, - _ => throw new InvalidEnumArgumentException(), - }; - } - - if( imc.Variant == 0 ) - { - return ref parts[ idx ].DefaultVariant; - } - - if( imc.Variant > parts[ idx ].Variants.Length ) - { - throw new InvalidImcVariantException(); - } - - return ref parts[ idx ].Variants[ imc.Variant - 1 ]; - } - - public static ImcFile Clone( this ImcFile file ) - { - var ret = new ImcFile - { - Count = file.Count, - PartMask = file.PartMask, - }; - var parts = file.GetParts().Select( p => new ImcFile.ImageChangeParts() - { - DefaultVariant = p.DefaultVariant, - Variants = ( ImcFile.ImageChangeData[] )p.Variants.Clone(), - } ).ToArray(); - var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance ); - prop!.SetValue( ret, parts ); - return ret; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs new file mode 100644 index 00000000..36576d00 --- /dev/null +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -0,0 +1,166 @@ +using System; +using System.Numerics; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Util; + +namespace Penumbra.Meta.Files; + +public struct ImcEntry : IEquatable< ImcEntry > +{ + public byte MaterialId; + public byte DecalId; + private ushort _attributeAndSound; + public byte VfxId; + public byte MaterialAnimationId; + + public ushort AttributeMask + => ( ushort )( _attributeAndSound & 0x3FF ); + + public byte SoundId + => ( byte )( _attributeAndSound >> 10 ); + + public bool Equals( ImcEntry other ) + => MaterialId == other.MaterialId + && DecalId == other.DecalId + && _attributeAndSound == other._attributeAndSound + && VfxId == other.VfxId + && MaterialAnimationId == other.MaterialAnimationId; + + public override bool Equals( object? obj ) + => obj is ImcEntry other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId ); +} + +public unsafe class ImcFile : MetaBaseFile +{ + private const int PreambleSize = 4; + + public int ActualLength + => NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize; + + public int Count + => *( ushort* )Data; + + public ushort PartMask + => *( ushort* )( Data + 2 ); + + public readonly int NumParts; + public readonly Utf8GamePath Path; + + public ImcEntry* DefaultPartPtr( int partIdx ) + { + var flag = 1 << partIdx; + if( ( PartMask & flag ) == 0 ) + { + return null; + } + + return ( ImcEntry* )( Data + PreambleSize ) + partIdx; + } + + public ImcEntry* VariantPtr( int partIdx, int variantIdx ) + { + var flag = 1 << partIdx; + if( ( PartMask & flag ) == 0 || variantIdx >= Count ) + { + return null; + } + + var numParts = NumParts; + var ptr = ( ImcEntry* )( Data + PreambleSize ); + ptr += numParts; + ptr += variantIdx * numParts; + ptr += partIdx; + return ptr; + } + + public static int PartIndex( EquipSlot slot ) + => slot switch + { + EquipSlot.Head => 0, + EquipSlot.Ears => 0, + EquipSlot.Body => 1, + EquipSlot.Neck => 1, + EquipSlot.Hands => 2, + EquipSlot.Wrists => 2, + EquipSlot.Legs => 3, + EquipSlot.RFinger => 3, + EquipSlot.Feet => 4, + EquipSlot.LFinger => 4, + _ => 0, + }; + + public bool EnsureVariantCount( int numVariants ) + { + if( numVariants <= Count ) + { + return true; + } + + var numParts = NumParts; + if( ActualLength > Length ) + { + PluginLog.Warning( "Adding too many variants to IMC, size exceeded." ); + return false; + } + + var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); + var endPtr = defaultPtr + ( numVariants + 1 ) * numParts; + for( var ptr = defaultPtr + numParts; ptr < endPtr; ptr += numParts ) + { + Functions.MemCpyUnchecked( ptr, defaultPtr, numParts * sizeof( ImcEntry ) ); + } + + PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants ); + *( ushort* )Count = ( ushort )numVariants; + return true; + } + + public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry ) + { + var numParts = NumParts; + if( partIdx >= numParts ) + { + return false; + } + + EnsureVariantCount( variantIdx + 1 ); + + var variantPtr = VariantPtr( partIdx, variantIdx ); + if( variantPtr == null ) + { + PluginLog.Error( "Error during expansion of imc file." ); + return false; + } + + if( variantPtr->Equals( entry ) ) + { + return false; + } + + *variantPtr = entry; + return true; + } + + + public ImcFile( Utf8GamePath path ) + : base( 0 ) + { + var file = Dalamud.GameData.GetFile( path.ToString() ); + if( file == null ) + { + throw new Exception(); + } + + fixed( byte* ptr = file.Data ) + { + NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); + AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts ); + Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs new file mode 100644 index 00000000..1d82e224 --- /dev/null +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Meta.Files; + +public unsafe class MetaBaseFile : IDisposable +{ + public byte* Data { get; private set; } + public int Length { get; private set; } + public int Index { get; } + + public MetaBaseFile( int idx ) + => Index = idx; + + protected (IntPtr Data, int Length) DefaultData + => Penumbra.CharacterUtility.DefaultResources[ Index ]; + + // Reset to default values. + public virtual void Reset() + {} + + // Obtain memory. + protected void AllocateData( int length ) + { + Length = length; + Data = ( byte* )Marshal.AllocHGlobal( length ); + GC.AddMemoryPressure( length ); + } + + // Free memory. + protected void ReleaseUnmanagedResources() + { + Marshal.FreeHGlobal( ( IntPtr )Data ); + GC.RemoveMemoryPressure( Length ); + Length = 0; + Data = null; + } + + // Manually free memory. + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize( this ); + } + + ~MetaBaseFile() + { + ReleaseUnmanagedResources(); + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaDefaults.cs b/Penumbra/Meta/Files/MetaDefaults.cs deleted file mode 100644 index 34600aa8..00000000 --- a/Penumbra/Meta/Files/MetaDefaults.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Logging; -using Dalamud.Plugin; -using Lumina.Data; -using Lumina.Data.Files; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; - -namespace Penumbra.Meta.Files -{ - // This class manages the default meta files obtained via lumina from the game files themselves. - // On first call, the default version of any supported file will be cached and can be returned without reparsing. - public class MetaDefaults - { - private readonly Dictionary< GamePath, object > _defaultFiles = new(); - - private object CreateNewFile( string path ) - { - if( path.EndsWith( ".imc" ) ) - { - return GetImcFile( path ); - } - - var rawFile = FetchFile( path ); - if( path.EndsWith( ".eqp" ) ) - { - return new EqpFile( rawFile ); - } - - if( path.EndsWith( ".gmp" ) ) - { - return new GmpFile( rawFile ); - } - - if( path.EndsWith( ".eqdp" ) ) - { - return new EqdpFile( rawFile ); - } - - if( path.EndsWith( ".est" ) ) - { - return new EstFile( rawFile ); - } - - if( path.EndsWith( ".cmp" ) ) - { - return new CmpFile( rawFile.Data ); - } - - throw new NotImplementedException(); - } - - private T? GetDefaultFile< T >( GamePath path, string error = "" ) where T : class - { - try - { - if( _defaultFiles.TryGetValue( path, out var file ) ) - { - return ( T )file; - } - - var newFile = CreateNewFile( path ); - _defaultFiles.Add( path, newFile ); - return ( T )_defaultFiles[ path ]; - } - catch( Exception e ) - { - PluginLog.Error( $"{error}{e}" ); - return null; - } - } - - private EqdpFile? GetDefaultEqdpFile( EquipSlot slot, GenderRace gr ) - => GetDefaultFile< EqdpFile >( MetaFileNames.Eqdp( slot, gr ), - $"Could not obtain Eqdp file for {slot} {gr}:\n" ); - - private GmpFile? GetDefaultGmpFile() - => GetDefaultFile< GmpFile >( MetaFileNames.Gmp(), "Could not obtain Gmp file:\n" ); - - private EqpFile? GetDefaultEqpFile() - => GetDefaultFile< EqpFile >( MetaFileNames.Eqp(), "Could not obtain Eqp file:\n" ); - - private EstFile? GetDefaultEstFile( ObjectType type, EquipSlot equip, BodySlot body ) - => GetDefaultFile< EstFile >( MetaFileNames.Est( type, equip, body ), $"Could not obtain Est file for {type} {equip} {body}:\n" ); - - private ImcFile? GetDefaultImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 ) - => GetDefaultFile< ImcFile >( MetaFileNames.Imc( type, primarySetId, secondarySetId ), - $"Could not obtain Imc file for {type}, {primarySetId} {secondarySetId}:\n" ); - - private CmpFile? GetDefaultCmpFile() - => GetDefaultFile< CmpFile >( MetaFileNames.Cmp(), "Could not obtain Cmp file:\n" ); - - public EqdpFile? GetNewEqdpFile( EquipSlot slot, GenderRace gr ) - => GetDefaultEqdpFile( slot, gr )?.Clone(); - - public GmpFile? GetNewGmpFile() - => GetDefaultGmpFile()?.Clone(); - - public EqpFile? GetNewEqpFile() - => GetDefaultEqpFile()?.Clone(); - - public EstFile? GetNewEstFile( ObjectType type, EquipSlot equip, BodySlot body ) - => GetDefaultEstFile( type, equip, body )?.Clone(); - - public ImcFile? GetNewImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 ) - => GetDefaultImcFile( type, primarySetId, secondarySetId )?.Clone(); - - public CmpFile? GetNewCmpFile() - => GetDefaultCmpFile()?.Clone(); - - private static ImcFile GetImcFile( string path ) - => Dalamud.GameData.GetFile< ImcFile >( path )!; - - private static FileResource FetchFile( string name ) - => Dalamud.GameData.GetFile( name )!; - - // Check that a given meta manipulation is an actual change to the default value. We don't need to keep changes to default. - public bool CheckAgainstDefault( MetaManipulation m ) - { - try - { - return m.Type switch - { - MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) - ?.GetValue( m ).Equal( m.ImcValue ) - ?? true, - MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) - == m.GmpValue, - MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) - .Reduce( m.EqpIdentifier.Slot ) - == m.EqpValue, - MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ) - ?.GetEntry( m.EqdpIdentifier.SetId ) - .Reduce( m.EqdpIdentifier.Slot ) - == m.EqdpValue, - MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) - ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) - == m.EstValue, - MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ] - == m.RspValue, - _ => false, - }; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" ); - } - - return false; - } - - public object? GetDefaultValue( MetaManipulation m ) - { - try - { - return m.Type switch - { - MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) - ?.GetValue( m ), - MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ), - MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) - .Reduce( m.EqpIdentifier.Slot ), - MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ) - ?.GetEntry( m.EqdpIdentifier.SetId ) - .Reduce( m.EqdpIdentifier.Slot ), - MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) - ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ), - MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ], - _ => false, - }; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" ); - } - - return false; - } - - // Create a deep copy of a default file as a new file. - public object? CreateNewFile( MetaManipulation m ) - { - return m.Type switch - { - MetaType.Imc => GetNewImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ), - MetaType.Gmp => GetNewGmpFile(), - MetaType.Eqp => GetNewEqpFile(), - MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ), - MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ), - MetaType.Rsp => GetNewCmpFile(), - _ => throw new NotImplementedException(), - }; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Identifier.cs b/Penumbra/Meta/Identifier.cs deleted file mode 100644 index 896a5a8b..00000000 --- a/Penumbra/Meta/Identifier.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Runtime.InteropServices; -using Penumbra.GameData.Enums; - -// A struct for each type of meta change that contains all relevant information, -// to uniquely identify the corresponding file and location for the change. -// The first byte is guaranteed to be the MetaType enum for each case. -namespace Penumbra.Meta -{ - public enum MetaType : byte - { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, - }; - - [StructLayout( LayoutKind.Explicit )] - public struct EqpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public EquipSlot Slot; - - [FieldOffset( 2 )] - public ushort SetId; - - public override string ToString() - => $"Eqp - {SetId} - {Slot}"; - } - - [StructLayout( LayoutKind.Explicit )] - public struct EqdpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public EquipSlot Slot; - - [FieldOffset( 2 )] - public GenderRace GenderRace; - - [FieldOffset( 4 )] - public ushort SetId; - - public override string ToString() - => $"Eqdp - {SetId} - {Slot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}"; - } - - [StructLayout( LayoutKind.Explicit )] - public struct GmpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public ushort SetId; - - public override string ToString() - => $"Gmp - {SetId}"; - } - - [StructLayout( LayoutKind.Explicit )] - public struct EstIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public ObjectType ObjectType; - - [FieldOffset( 2 )] - public EquipSlot EquipSlot; - - [FieldOffset( 3 )] - public BodySlot BodySlot; - - [FieldOffset( 4 )] - public GenderRace GenderRace; - - [FieldOffset( 6 )] - public ushort PrimaryId; - - public override string ToString() - => ObjectType == ObjectType.Equipment - ? $"Est - {PrimaryId} - {EquipSlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}" - : $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}"; - } - - [StructLayout( LayoutKind.Explicit )] - public struct ImcIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public byte _objectAndBody; - - public ObjectType ObjectType - { - get => ( ObjectType )( _objectAndBody & 0b00011111 ); - set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value ); - } - - public BodySlot BodySlot - { - get => ( BodySlot )( _objectAndBody >> 5 ); - set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( ( byte )value << 5 ) ); - } - - [FieldOffset( 2 )] - public ushort PrimaryId; - - [FieldOffset( 4 )] - public ushort Variant; - - [FieldOffset( 6 )] - public ushort SecondaryId; - - [FieldOffset( 6 )] - public EquipSlot EquipSlot; - - public override string ToString() - { - return ObjectType switch - { - ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}", - ObjectType.Equipment => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}", - _ => $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}", - }; - } - } - - [StructLayout( LayoutKind.Explicit )] - public struct RspIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public SubRace SubRace; - - [FieldOffset( 2 )] - public RspAttribute Attribute; - - public override string ToString() - => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs new file mode 100644 index 00000000..c7fdd553 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -0,0 +1,56 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct EqdpManipulation : IEquatable< EqdpManipulation > +{ + public readonly EqdpEntry Entry; + public readonly Gender Gender; + public readonly ModelRace Race; + public readonly ushort SetId; + public readonly EquipSlot Slot; + + public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) + { + Entry = Eqdp.Mask( slot ) & entry; + Gender = gender; + Race = race; + SetId = setId; + Slot = slot; + } + + public override string ToString() + => $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}"; + + public bool Equals( EqdpManipulation other ) + => Gender == other.Gender + && Race == other.Race + && SetId == other.SetId + && Slot == other.Slot; + + public override bool Equals( object? obj ) + => obj is EqdpManipulation other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot ); + + public int FileIndex() + => CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); + + public bool Apply( ExpandedEqdpFile file ) + { + var entry = file[ SetId ]; + var mask = Eqdp.Mask( Slot ); + if( ( entry & mask ) == Entry ) + { + return false; + } + + file[ SetId ] = ( entry & ~mask ) | Entry; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs new file mode 100644 index 00000000..eab5b864 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -0,0 +1,50 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct EqpManipulation : IEquatable< EqpManipulation > +{ + public readonly EqpEntry Entry; + public readonly ushort SetId; + public readonly EquipSlot Slot; + + public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) + { + Slot = slot; + SetId = setId; + Entry = Eqp.Mask( slot ) & entry; + } + + public override string ToString() + => $"Eqp - {SetId} - {Slot}"; + + public bool Equals( EqpManipulation other ) + => Slot == other.Slot + && SetId == other.SetId; + + public override bool Equals( object? obj ) + => obj is EqpManipulation other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( ( int )Slot, SetId ); + + public int FileIndex() + => CharacterUtility.EqpIdx; + + public bool Apply( ExpandedEqpFile file ) + { + var entry = file[ SetId ]; + var mask = Eqp.Mask( Slot ); + if( ( entry & mask ) == Entry ) + { + return false; + } + + file[ SetId ] = ( entry & ~mask ) | Entry; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs new file mode 100644 index 00000000..985f9a65 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -0,0 +1,63 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct EstManipulation : IEquatable< EstManipulation > +{ + public enum EstType : byte + { + Hair = CharacterUtility.HairEstIdx, + Face = CharacterUtility.FaceEstIdx, + Body = CharacterUtility.BodyEstIdx, + Head = CharacterUtility.HeadEstIdx, + } + + public readonly ushort SkeletonIdx; + public readonly Gender Gender; + public readonly ModelRace Race; + public readonly ushort SetId; + public readonly EstType Type; + + public EstManipulation( Gender gender, ModelRace race, EstType estType, ushort setId, ushort skeletonIdx ) + { + SkeletonIdx = skeletonIdx; + Gender = gender; + Race = race; + SetId = setId; + Type = estType; + } + + + public override string ToString() + => $"Est - {SetId} - {Type} - {Race.ToName()} {Gender.ToName()}"; + + public bool Equals( EstManipulation other ) + => Gender == other.Gender + && Race == other.Race + && SetId == other.SetId + && Type == other.Type; + + public override bool Equals( object? obj ) + => obj is EstManipulation other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Type ); + + public int FileIndex() + => ( int )Type; + + public bool Apply( EstFile file ) + { + return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, SkeletonIdx ) switch + { + EstFile.EstEntryChange.Unchanged => false, + EstFile.EstEntryChange.Changed => true, + EstFile.EstEntryChange.Added => true, + EstFile.EstEntryChange.Removed => true, + _ => throw new ArgumentOutOfRangeException(), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs new file mode 100644 index 00000000..2af52dfb --- /dev/null +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -0,0 +1,45 @@ +using System; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct GmpManipulation : IEquatable< GmpManipulation > +{ + public readonly GmpEntry Entry; + public readonly ushort SetId; + + public GmpManipulation( GmpEntry entry, ushort setId ) + { + Entry = entry; + SetId = setId; + } + + public override string ToString() + => $"Gmp - {SetId}"; + + public bool Equals( GmpManipulation other ) + => SetId == other.SetId; + + public override bool Equals( object? obj ) + => obj is GmpManipulation other && Equals( other ); + + public override int GetHashCode() + => SetId.GetHashCode(); + + public int FileIndex() + => CharacterUtility.GmpIdx; + + public bool Apply( ExpandedGmpFile file ) + { + var entry = file[ SetId ]; + if( entry == Entry ) + { + return false; + } + + file[ SetId ] = Entry; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs new file mode 100644 index 00000000..287708a0 --- /dev/null +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Files; +using ImcFile = Lumina.Data.Files.ImcFile; + +namespace Penumbra.Meta.Manipulations; + +[StructLayout( LayoutKind.Sequential )] +public readonly struct ImcManipulation : IEquatable< ImcManipulation > +{ + public readonly ImcEntry Entry; + public readonly ushort PrimaryId; + public readonly ushort Variant; + public readonly ushort SecondaryId; + public readonly ObjectType ObjectType; + public readonly EquipSlot EquipSlot; + public readonly BodySlot BodySlot; + + public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) + { + Entry = entry; + PrimaryId = primaryId; + Variant = variant; + SecondaryId = 0; + ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; + EquipSlot = equipSlot; + BodySlot = BodySlot.Unknown; + } + + public ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, + ImcEntry entry ) + { + Entry = entry; + ObjectType = objectType; + BodySlot = bodySlot; + SecondaryId = secondaryId; + PrimaryId = primaryId; + Variant = variant; + EquipSlot = EquipSlot.Unknown; + } + + public override string ToString() + => ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" + : $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}"; + + public bool Equals( ImcManipulation other ) + => PrimaryId == other.PrimaryId + && Variant == other.Variant + && SecondaryId == other.SecondaryId + && ObjectType == other.ObjectType + && EquipSlot == other.EquipSlot + && BodySlot == other.BodySlot; + + public override bool Equals( object? obj ) + => obj is ImcManipulation other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot ); +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs new file mode 100644 index 00000000..f33e7a97 --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -0,0 +1,109 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Meta.Manipulations; + +[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] +public readonly struct MetaManipulation : IEquatable< MetaManipulation > +{ + public enum Type : byte + { + Eqp, + Gmp, + Eqdp, + Est, + Rsp, + Imc, + } + + [FieldOffset( 0 )] + public readonly EqpManipulation Eqp = default; + + [FieldOffset( 0 )] + public readonly GmpManipulation Gmp = default; + + [FieldOffset( 0 )] + public readonly EqdpManipulation Eqdp = default; + + [FieldOffset( 0 )] + public readonly EstManipulation Est = default; + + [FieldOffset( 0 )] + public readonly RspManipulation Rsp = default; + + [FieldOffset( 0 )] + public readonly ImcManipulation Imc = default; + + [FieldOffset( 15 )] + public readonly Type ManipulationType; + + public MetaManipulation( EqpManipulation eqp ) + => ( ManipulationType, Eqp ) = ( Type.Eqp, eqp ); + + public MetaManipulation( GmpManipulation gmp ) + => ( ManipulationType, Gmp ) = ( Type.Gmp, gmp ); + + public MetaManipulation( EqdpManipulation eqdp ) + => ( ManipulationType, Eqdp ) = ( Type.Eqdp, eqdp ); + + public MetaManipulation( EstManipulation est ) + => ( ManipulationType, Est ) = ( Type.Est, est ); + + public MetaManipulation( RspManipulation rsp ) + => ( ManipulationType, Rsp ) = ( Type.Rsp, rsp ); + + public MetaManipulation( ImcManipulation imc ) + => ( ManipulationType, Imc ) = ( Type.Imc, imc ); + + public static implicit operator MetaManipulation( EqpManipulation eqp ) + => new(eqp); + + public static implicit operator MetaManipulation( GmpManipulation gmp ) + => new(gmp); + + public static implicit operator MetaManipulation( EqdpManipulation eqdp ) + => new(eqdp); + + public static implicit operator MetaManipulation( EstManipulation est ) + => new(est); + + public static implicit operator MetaManipulation( RspManipulation rsp ) + => new(rsp); + + public static implicit operator MetaManipulation( ImcManipulation imc ) + => new(imc); + + public bool Equals( MetaManipulation other ) + { + if( ManipulationType != other.ManipulationType ) + { + return false; + } + + return ManipulationType switch + { + Type.Eqp => Eqp.Equals( other.Eqp ), + Type.Gmp => Gmp.Equals( other.Gmp ), + Type.Eqdp => Eqdp.Equals( other.Eqdp ), + Type.Est => Est.Equals( other.Est ), + Type.Rsp => Rsp.Equals( other.Rsp ), + Type.Imc => Imc.Equals( other.Imc ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + public override bool Equals( object? obj ) + => obj is MetaManipulation other && Equals( other ); + + public override int GetHashCode() + => ManipulationType switch + { + Type.Eqp => Eqp.GetHashCode(), + Type.Gmp => Gmp.GetHashCode(), + Type.Eqdp => Eqdp.GetHashCode(), + Type.Est => Est.GetHashCode(), + Type.Rsp => Rsp.GetHashCode(), + Type.Imc => Imc.GetHashCode(), + _ => throw new ArgumentOutOfRangeException(), + }; +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs new file mode 100644 index 00000000..0ad017cd --- /dev/null +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -0,0 +1,48 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct RspManipulation : IEquatable< RspManipulation > +{ + public readonly float Entry; + public readonly SubRace SubRace; + public readonly RspAttribute Attribute; + + public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) + { + Entry = entry; + SubRace = subRace; + Attribute = attribute; + } + + public override string ToString() + => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; + + public bool Equals( RspManipulation other ) + => SubRace == other.SubRace + && Attribute == other.Attribute; + + public override bool Equals( object? obj ) + => obj is RspManipulation other && Equals( other ); + + public override int GetHashCode() + => HashCode.Combine( ( int )SubRace, ( int )Attribute ); + + public int FileIndex() + => CharacterUtility.HumanCmpIdx; + + public bool Apply( CmpFile file ) + { + var value = file[ SubRace, Attribute ]; + if( value == Entry ) + { + return false; + } + + file[ SubRace, Attribute ] = Entry; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 059999af..a0ec0027 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -6,9 +6,8 @@ using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Importer; -using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.Mod; -using Penumbra.Util; namespace Penumbra.Meta; diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 0fac0dc3..1c56b35b 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -5,12 +5,273 @@ using System.Linq; using Dalamud.Logging; using Lumina.Data.Files; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.Util; namespace Penumbra.Meta; +public struct TemporaryImcFile : IDisposable +{ + + public void Dispose() + { + + } +} + +public class MetaManager2 : IDisposable +{ + public readonly List< MetaBaseFile > ChangedData = new(7 + CharacterUtility.NumEqdpFiles); + + public ExpandedEqpFile? EqpFile; + public ExpandedGmpFile? GmpFile; + public ExpandedEqdpFile?[] EqdpFile = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles]; + public EstFile? FaceEstFile; + public EstFile? HairEstFile; + public EstFile? BodyEstFile; + public EstFile? HeadEstFile; + public CmpFile? CmpFile; + + public readonly Dictionary< EqpManipulation, Mod.Mod > EqpManipulations = new(); + public readonly Dictionary< EstManipulation, Mod.Mod > EstManipulations = new(); + public readonly Dictionary< GmpManipulation, Mod.Mod > GmpManipulations = new(); + public readonly Dictionary< RspManipulation, Mod.Mod > RspManipulations = new(); + public readonly Dictionary< EqdpManipulation, Mod.Mod > EqdpManipulations = new(); + + public readonly Dictionary< ImcManipulation, Mod.Mod > ImcManipulations = new(); + public readonly List< TemporaryImcFile > ImcFiles = new(); + + public void ResetEqp() + { + if( EqpFile != null ) + { + EqpFile.Reset( EqpManipulations.Keys.Select( m => ( int )m.SetId ) ); + EqpManipulations.Clear(); + ChangedData.Remove( EqpFile ); + } + } + + public void ResetGmp() + { + if( GmpFile != null ) + { + GmpFile.Reset( GmpManipulations.Keys.Select( m => ( int )m.SetId ) ); + GmpManipulations.Clear(); + ChangedData.Remove( GmpFile ); + } + } + + public void ResetCmp() + { + if( CmpFile != null ) + { + CmpFile.Reset( RspManipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) ); + RspManipulations.Clear(); + ChangedData.Remove( CmpFile ); + } + } + + public void ResetEst() + { + FaceEstFile?.Reset(); + HairEstFile?.Reset(); + BodyEstFile?.Reset(); + HeadEstFile?.Reset(); + RspManipulations.Clear(); + ChangedData.RemoveAll( f => f is EstFile ); + } + + public void ResetEqdp() + { + foreach( var file in EqdpFile ) + { + file?.Reset( EqdpManipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); + } + + ChangedData.RemoveAll( f => f is ExpandedEqdpFile ); + EqdpManipulations.Clear(); + } + + public void ResetImc() + { + foreach( var file in ImcFiles ) + file.Dispose(); + ImcFiles.Clear(); + ImcManipulations.Clear(); + } + + public void Reset() + { + ChangedData.Clear(); + ResetEqp(); + ResetGmp(); + ResetCmp(); + ResetEst(); + ResetEqdp(); + ResetImc(); + } + + private static void Dispose< T >( ref T? file ) where T : class, IDisposable + { + if( file != null ) + { + file.Dispose(); + file = null; + } + } + + public void Dispose() + { + ChangedData.Clear(); + EqpManipulations.Clear(); + EstManipulations.Clear(); + GmpManipulations.Clear(); + RspManipulations.Clear(); + EqdpManipulations.Clear(); + Dispose( ref EqpFile ); + Dispose( ref GmpFile ); + Dispose( ref FaceEstFile ); + Dispose( ref HairEstFile ); + Dispose( ref BodyEstFile ); + Dispose( ref HeadEstFile ); + Dispose( ref CmpFile ); + for( var i = 0; i < CharacterUtility.NumEqdpFiles; ++i ) + { + Dispose( ref EqdpFile[ i ] ); + } + + ResetImc(); + } + + private void AddFile( MetaBaseFile file ) + { + if( !ChangedData.Contains( file ) ) + { + ChangedData.Add( file ); + } + } + + + public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) + { + if( !EqpManipulations.TryAdd( m, mod ) ) + { + return false; + } + + EqpFile ??= new ExpandedEqpFile(); + if( !m.Apply( EqpFile ) ) + { + return false; + } + + AddFile( EqpFile ); + return true; + } + + public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) + { + if( !GmpManipulations.TryAdd( m, mod ) ) + { + return false; + } + + GmpFile ??= new ExpandedGmpFile(); + if( !m.Apply( GmpFile ) ) + { + return false; + } + + AddFile( GmpFile ); + return true; + } + + public bool ApplyMod( EstManipulation m, Mod.Mod mod ) + { + if( !EstManipulations.TryAdd( m, mod ) ) + { + return false; + } + + var file = m.Type switch + { + EstManipulation.EstType.Hair => HairEstFile ??= new EstFile( EstManipulation.EstType.Hair ), + EstManipulation.EstType.Face => FaceEstFile ??= new EstFile( EstManipulation.EstType.Face ), + EstManipulation.EstType.Body => BodyEstFile ??= new EstFile( EstManipulation.EstType.Body ), + EstManipulation.EstType.Head => HeadEstFile ??= new EstFile( EstManipulation.EstType.Head ), + _ => throw new ArgumentOutOfRangeException(), + }; + if( !m.Apply( file ) ) + { + return false; + } + + AddFile( file ); + return true; + } + + public bool ApplyMod( RspManipulation m, Mod.Mod mod ) + { + if( !RspManipulations.TryAdd( m, mod ) ) + { + return false; + } + + CmpFile ??= new CmpFile(); + if( !m.Apply( CmpFile ) ) + { + return false; + } + + AddFile( CmpFile ); + return true; + } + + public bool ApplyMod( EqdpManipulation m, Mod.Mod mod ) + { + if( !EqdpManipulations.TryAdd( m, mod ) ) + { + return false; + } + + var file = EqdpFile[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); + if( !m.Apply( file ) ) + { + return false; + } + + AddFile( file ); + return true; + } + + public bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + { + if( !ImcManipulations.TryAdd( m, mod ) ) + { + return false; + } + + return true; + } + + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Eqp => ApplyMod( m.Eqp, mod ), + MetaManipulation.Type.Gmp => ApplyMod( m.Gmp, mod ), + MetaManipulation.Type.Eqdp => ApplyMod( m.Eqdp, mod ), + MetaManipulation.Type.Est => ApplyMod( m.Est, mod ), + MetaManipulation.Type.Rsp => ApplyMod( m.Rsp, mod ), + MetaManipulation.Type.Imc => ApplyMod( m.Imc, mod ), + _ => throw new ArgumentOutOfRangeException() + }; + } +} + public class MetaManager : IDisposable { internal class FileInformation @@ -27,12 +288,7 @@ public class MetaManager : IDisposable { ByteData = Data switch { - EqdpFile eqdp => eqdp.WriteBytes(), - EqpFile eqp => eqp.WriteBytes(), - GmpFile gmp => gmp.WriteBytes(), - EstFile est => est.WriteBytes(), ImcFile imc => imc.WriteBytes(), - CmpFile cmp => cmp.WriteBytes(), _ => throw new NotImplementedException(), }; DisposeFile( CurrentFile ); @@ -138,50 +394,6 @@ public class MetaManager : IDisposable { value.Write( _dir, key ); _resolvedFiles[ key ] = value.CurrentFile!.Value; - if( value.Data is EqpFile ) - { - EqpData = value.ByteData; - } - } - } - - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) - { - if( _currentManipulations.ContainsKey( m ) ) - { - return false; - } - - _currentManipulations.Add( m, mod ); - var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO - try - { - if( !_currentFiles.TryGetValue( gamePath, out var file ) ) - { - file = new FileInformation( Penumbra.MetaDefaults.CreateNewFile( m ) ?? throw new IOException() ) - { - Changed = true, - CurrentFile = null, - }; - _currentFiles[ gamePath ] = file; - } - - file.Changed |= m.Type switch - { - MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), - MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), - MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), - MetaType.Est => m.Apply( ( EstFile )file.Data ), - MetaType.Imc => m.Apply( ( ImcFile )file.Data ), - MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), - _ => throw new NotImplementedException(), - }; - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" ); - return false; } } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaManipulation.cs b/Penumbra/Meta/MetaManipulation.cs deleted file mode 100644 index 326b8519..00000000 --- a/Penumbra/Meta/MetaManipulation.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; -using Newtonsoft.Json; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; -using Penumbra.Meta.Files; -using Swan; -using ImcFile = Lumina.Data.Files.ImcFile; - -namespace Penumbra.Meta -{ - // Write a single meta manipulation as a Base64string of the 16 bytes defining it. - public class MetaManipulationConverter : JsonConverter< MetaManipulation > - { - public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer ) - { - var s = Convert.ToBase64String( manip.ToBytes() ); - writer.WriteValue( s ); - } - - public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue, - JsonSerializer serializer ) - - { - if( reader.TokenType != JsonToken.String ) - { - throw new JsonReaderException(); - } - - var bytes = Convert.FromBase64String( ( string )reader.Value! ); - using MemoryStream m = new( bytes ); - using BinaryReader br = new( m ); - var i = br.ReadUInt64(); - var v = br.ReadUInt64(); - return new MetaManipulation( i, v ); - } - } - - // A MetaManipulation is a union of a type of Identifier (first 8 bytes, cf. Identifier.cs) - // and the appropriate Value to change the meta entry to (the other 8 bytes). - // Its comparison for sorting and hashes depends only on the identifier. - // The first byte is guaranteed to be a MetaType enum value in any case, so Type can always be read. - [StructLayout( LayoutKind.Explicit )] - [JsonConverter( typeof( MetaManipulationConverter ) )] - public struct MetaManipulation : IComparable - { - public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value ) - => new() - { - EqpIdentifier = new EqpIdentifier() - { - Type = MetaType.Eqp, - Slot = equipSlot, - SetId = setId, - }, - EqpValue = value, - }; - - public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value ) - => new() - { - EqdpIdentifier = new EqdpIdentifier() - { - Type = MetaType.Eqdp, - Slot = equipSlot, - GenderRace = gr, - SetId = setId, - }, - EqdpValue = value, - }; - - public static MetaManipulation Gmp( ushort setId, GmpEntry value ) - => new() - { - GmpIdentifier = new GmpIdentifier() - { - Type = MetaType.Gmp, - SetId = setId, - }, - GmpValue = value, - }; - - public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId, - ushort value ) - => new() - { - EstIdentifier = new EstIdentifier() - { - Type = MetaType.Est, - ObjectType = type, - GenderRace = gr, - EquipSlot = equipSlot, - BodySlot = bodySlot, - PrimaryId = setId, - }, - EstValue = value, - }; - - public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId - , ushort idx, ImcFile.ImageChangeData value ) - => new() - { - ImcIdentifier = new ImcIdentifier() - { - Type = MetaType.Imc, - ObjectType = type, - BodySlot = secondaryType, - PrimaryId = primaryId, - SecondaryId = secondaryId, - Variant = idx, - }, - ImcValue = value, - }; - - public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value ) - => new() - { - ImcIdentifier = new ImcIdentifier() - { - Type = MetaType.Imc, - ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, - EquipSlot = slot, - PrimaryId = primaryId, - Variant = idx, - }, - ImcValue = value, - }; - - public static MetaManipulation Rsp( SubRace subRace, RspAttribute attribute, float value ) - => new() - { - RspIdentifier = new RspIdentifier() - { - Type = MetaType.Rsp, - SubRace = subRace, - Attribute = attribute, - }, - RspValue = value, - }; - - internal MetaManipulation( ulong identifier, ulong value ) - : this() - { - Identifier = identifier; - Value = value; - } - - [FieldOffset( 0 )] - public readonly ulong Identifier; - - [FieldOffset( 8 )] - public readonly ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 0 )] - public EqpIdentifier EqpIdentifier; - - [FieldOffset( 0 )] - public GmpIdentifier GmpIdentifier; - - [FieldOffset( 0 )] - public EqdpIdentifier EqdpIdentifier; - - [FieldOffset( 0 )] - public EstIdentifier EstIdentifier; - - [FieldOffset( 0 )] - public ImcIdentifier ImcIdentifier; - - [FieldOffset( 0 )] - public RspIdentifier RspIdentifier; - - - [FieldOffset( 8 )] - public EqpEntry EqpValue; - - [FieldOffset( 8 )] - public GmpEntry GmpValue; - - [FieldOffset( 8 )] - public EqdpEntry EqdpValue; - - [FieldOffset( 8 )] - public ushort EstValue; - - [FieldOffset( 8 )] - public ImcFile.ImageChangeData ImcValue; // 6 bytes. - - [FieldOffset( 8 )] - public float RspValue; - - public override int GetHashCode() - => Identifier.GetHashCode(); - - public int CompareTo( object? rhs ) - => Identifier.CompareTo( rhs is MetaManipulation m ? m.Identifier : null ); - - public GamePath CorrespondingFilename() - { - return Type switch - { - MetaType.Eqp => MetaFileNames.Eqp(), - MetaType.Eqdp => MetaFileNames.Eqdp( EqdpIdentifier.Slot, EqdpIdentifier.GenderRace ), - MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ), - MetaType.Gmp => MetaFileNames.Gmp(), - MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ), - MetaType.Rsp => MetaFileNames.Cmp(), - _ => throw new InvalidEnumArgumentException(), - }; - } - - // No error checking. - public bool Apply( EqpFile file ) - => file[ EqpIdentifier.SetId ].Apply( this ); - - public bool Apply( EqdpFile file ) - => file[ EqdpIdentifier.SetId ].Apply( this ); - - public bool Apply( GmpFile file ) - => file.SetEntry( GmpIdentifier.SetId, GmpValue ); - - public bool Apply( EstFile file ) - => file.SetEntry( EstIdentifier.GenderRace, EstIdentifier.PrimaryId, EstValue ); - - public bool Apply( ImcFile file ) - { - ref var value = ref file.GetValue( this ); - if( ImcValue.Equal( value ) ) - { - return false; - } - - value = ImcValue; - return true; - } - - public bool Apply( CmpFile file ) - => file.Set( RspIdentifier.SubRace, RspIdentifier.Attribute, RspValue ); - - public string IdentifierString() - { - return Type switch - { - MetaType.Eqp => EqpIdentifier.ToString(), - MetaType.Eqdp => EqdpIdentifier.ToString(), - MetaType.Est => EstIdentifier.ToString(), - MetaType.Gmp => GmpIdentifier.ToString(), - MetaType.Imc => ImcIdentifier.ToString(), - MetaType.Rsp => RspIdentifier.ToString(), - _ => throw new InvalidEnumArgumentException(), - }; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 7764828f..3c413428 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta; +using Penumbra.Meta.Manipulations; namespace Penumbra.Mod; diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 2dee1a90..d9f4b144 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -304,7 +304,7 @@ public class ModCollectionCache { if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) { - MetaManipulations.ApplyMod( manip, mod ); + //MetaManipulations.ApplyMod( manip, mod ); } else { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cfbb5f02..d37a1531 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -15,9 +15,13 @@ using Penumbra.PlayerWatch; using Penumbra.UI; using Penumbra.Util; using System.Linq; +using Penumbra.Meta.Manipulations; namespace Penumbra; +public class MetaDefaults +{ +} public class Penumbra : IDalamudPlugin { public string Name @@ -33,6 +37,7 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaDefaults MetaDefaults { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; @@ -114,6 +119,17 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableDebug(); if (Config.EnableFullResourceLogging) ResourceLoader.EnableFullLogging(); + + unsafe + { + PluginLog.Information( $"MetaManipulation: {sizeof( MetaManipulation )}" ); + PluginLog.Information( $"EqpManipulation: {sizeof( EqpManipulation )}" ); + PluginLog.Information( $"GmpManipulation: {sizeof( GmpManipulation )}" ); + PluginLog.Information( $"EqdpManipulation: {sizeof( EqdpManipulation )}" ); + PluginLog.Information( $"EstManipulation: {sizeof( EstManipulation )}" ); + PluginLog.Information( $"RspManipulation: {sizeof( RspManipulation )}" ); + PluginLog.Information( $"ImcManipulation: {sizeof( ImcManipulation )}" ); + } } public bool Enable() diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 83c157d3..0ff8a78c 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -108,12 +108,12 @@ public partial class SettingsInterface DrawLine( gp, fp ); } - foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations - .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } + //foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations + // .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) + // .Where( CheckFilters ) ) + //{ + // DrawLine( mp, mod ); + //} } if( active != null ) @@ -193,7 +193,7 @@ public partial class SettingsInterface else if( ( row -= activeResolved ) < activeMeta ) { var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + DrawLine( manip.ToString(), mod.Data.Meta.Name ); } else if( ( row -= activeMeta ) < forcedResolved ) { @@ -204,7 +204,7 @@ public partial class SettingsInterface { row -= forcedResolved; var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + DrawLine( manip.ToString(), mod.Data.Meta.Name ); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index e2d0a710..7da647b1 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -225,7 +225,7 @@ public partial class SettingsInterface foreach( var manip in manipulations ) { - ImGui.Text( manip.IdentifierString() ); + //ImGui.Text( manip.IdentifierString() ); } indent.Pop( 15f ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 4d3148ae..a48a1732 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.UI.Custom; using Penumbra.Util; using ObjectType = Penumbra.GameData.Enums.ObjectType; @@ -223,32 +224,32 @@ namespace Penumbra.UI private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) { var ret = false; - var id = list[ manipIdx ].EqpIdentifier; - var val = list[ manipIdx ].EqpValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; - var attributes = Eqp.EqpAttributes[ id.Slot ]; - - foreach( var flag in attributes ) - { - var name = flag.ToLocalName(); - var tmp = val.HasFlag( flag ); - if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) - { - list[ manipIdx ] = MetaManipulation.Eqp( id.Slot, id.SetId, tmp ? val | flag : val & ~flag ); - ret = true; - } - } - } - - ImGui.Text( ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Slot.ToString() ); + //var id = list[ manipIdx ].EqpIdentifier; + //var val = list[ manipIdx ].EqpValue; + // + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; + // var attributes = Eqp.EqpAttributes[ id.Slot ]; + // + // foreach( var flag in attributes ) + // { + // var name = flag.ToLocalName(); + // var tmp = val.HasFlag( flag ); + // if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) + // { + // list[ manipIdx ] = MetaManipulation.Eqp( id.Slot, id.SetId, tmp ? val | flag : val & ~flag ); + // ret = true; + // } + // } + //} + // + //ImGui.Text( ObjectType.Equipment.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.SetId.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.Slot.ToString() ); return ret; } @@ -256,42 +257,42 @@ namespace Penumbra.UI { var defaults = ( GmpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; - var id = list[ manipIdx ].GmpIdentifier; - var val = list[ manipIdx ].GmpValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var enabled = val.Enabled; - var animated = val.Animated; - var rotationA = val.RotationA; - var rotationB = val.RotationB; - var rotationC = val.RotationC; - ushort unk = val.UnknownTotal; - - ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; - ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); - ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); - ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); - ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); - ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); - - if( ret && _editMode ) - { - list[ manipIdx ] = MetaManipulation.Gmp( id.SetId, - new GmpEntry - { - Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, - RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, - } ); - } - } - - ImGui.Text( ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( EquipSlot.Head.ToString() ); + //var id = list[ manipIdx ].GmpIdentifier; + //var val = list[ manipIdx ].GmpValue; + // + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // var enabled = val.Enabled; + // var animated = val.Animated; + // var rotationA = val.RotationA; + // var rotationB = val.RotationB; + // var rotationC = val.RotationC; + // ushort unk = val.UnknownTotal; + // + // ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; + // ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); + // ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); + // ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); + // ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); + // ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); + // + // if( ret && _editMode ) + // { + // list[ manipIdx ] = MetaManipulation.Gmp( id.SetId, + // new GmpEntry + // { + // Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, + // RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, + // } ); + // } + //} + // + //ImGui.Text( ObjectType.Equipment.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.SetId.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( EquipSlot.Head.ToString() ); return ret; } @@ -366,36 +367,36 @@ namespace Penumbra.UI { var defaults = ( EqdpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; - var id = list[ manipIdx ].EqdpIdentifier; - var val = list[ manipIdx ].EqdpValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var (bit1, bit2) = GetEqdpBits( id.Slot, val ); - var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); - - ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); - ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); - - if( ret && _editMode ) - { - list[ manipIdx ] = MetaManipulation.Eqdp( id.Slot, id.GenderRace, id.SetId, SetEqdpBits( id.Slot, val, bit1, bit2 ) ); - } - } - - ImGui.Text( id.Slot.IsAccessory() - ? ObjectType.Accessory.ToString() - : ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Slot.ToString() ); - ImGui.TableNextColumn(); - var (gender, race) = id.GenderRace.Split(); - ImGui.Text( race.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( gender.ToString() ); + //var id = list[ manipIdx ].EqdpIdentifier; + //var val = list[ manipIdx ].EqdpValue; + // + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // var (bit1, bit2) = GetEqdpBits( id.Slot, val ); + // var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); + // + // ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); + // ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); + // + // if( ret && _editMode ) + // { + // list[ manipIdx ] = MetaManipulation.Eqdp( id.Slot, id.GenderRace, id.SetId, SetEqdpBits( id.Slot, val, bit1, bit2 ) ); + // } + //} + // + //ImGui.Text( id.Slot.IsAccessory() + // ? ObjectType.Accessory.ToString() + // : ObjectType.Equipment.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.SetId.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.Slot.ToString() ); + //ImGui.TableNextColumn(); + //var (gender, race) = id.GenderRace.Split(); + //ImGui.Text( race.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( gender.ToString() ); return ret; } @@ -403,30 +404,30 @@ namespace Penumbra.UI { var defaults = ( ushort )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; - var id = list[ manipIdx ].EstIdentifier; - var val = list[ manipIdx ].EstValue; - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( id.Value, val ); - ret = true; - } - } - - ImGui.Text( id.ObjectType.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.PrimaryId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.ObjectType == ObjectType.Equipment - ? id.EquipSlot.ToString() - : id.BodySlot.ToString() ); - ImGui.TableNextColumn(); - var (gender, race) = id.GenderRace.Split(); - ImGui.Text( race.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( gender.ToString() ); + //var id = list[ manipIdx ].EstIdentifier; + //var val = list[ manipIdx ].EstValue; + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) + // { + // list[ manipIdx ] = new MetaManipulation( id.Value, val ); + // ret = true; + // } + //} + // + //ImGui.Text( id.ObjectType.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.PrimaryId.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.ObjectType == ObjectType.Equipment + // ? id.EquipSlot.ToString() + // : id.BodySlot.ToString() ); + //ImGui.TableNextColumn(); + //var (gender, race) = id.GenderRace.Split(); + //ImGui.Text( race.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( gender.ToString() ); return ret; } @@ -435,58 +436,58 @@ namespace Penumbra.UI { var defaults = ( ImcFile.ImageChangeData )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; - var id = list[ manipIdx ].ImcIdentifier; - var val = list[ manipIdx ].ImcValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - ushort materialId = val.MaterialId; - ushort vfxId = val.VfxId; - ushort decalId = val.DecalId; - var soundId = ( ushort )( val.SoundId >> 10 ); - var attributeMask = val.AttributeMask; - var materialAnimationId = ( ushort )( val.MaterialAnimationId >> 12 ); - ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); - ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); - ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, - byte.MaxValue ); - - if( ret && _editMode ) - { - var value = ImcExtensions.FromValues( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, - ( byte )vfxId, ( byte )materialAnimationId ); - list[ manipIdx ] = new MetaManipulation( id.Value, value.ToInteger() ); - } - } - - ImGui.Text( id.ObjectType.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.PrimaryId.ToString() ); - ImGui.TableNextColumn(); - if( id.ObjectType == ObjectType.Accessory - || id.ObjectType == ObjectType.Equipment ) - { - ImGui.Text( id.ObjectType == ObjectType.Equipment - || id.ObjectType == ObjectType.Accessory - ? id.EquipSlot.ToString() - : id.BodySlot.ToString() ); - } - - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - if( id.ObjectType != ObjectType.Equipment - && id.ObjectType != ObjectType.Accessory ) - { - ImGui.Text( id.SecondaryId.ToString() ); - } - - ImGui.TableNextColumn(); - ImGui.Text( id.Variant.ToString() ); + //var id = list[ manipIdx ].ImcIdentifier; + //var val = list[ manipIdx ].ImcValue; + // + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // ushort materialId = val.MaterialId; + // ushort vfxId = val.VfxId; + // ushort decalId = val.DecalId; + // var soundId = ( ushort )( val.SoundId >> 10 ); + // var attributeMask = val.AttributeMask; + // var materialAnimationId = ( ushort )( val.MaterialAnimationId >> 12 ); + // ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); + // ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); + // ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); + // ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); + // ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); + // ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, + // byte.MaxValue ); + // + // if( ret && _editMode ) + // { + // var value = ImcExtensions.FromValues( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, + // ( byte )vfxId, ( byte )materialAnimationId ); + // list[ manipIdx ] = new MetaManipulation( id.Value, value.ToInteger() ); + // } + //} + // + //ImGui.Text( id.ObjectType.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.PrimaryId.ToString() ); + //ImGui.TableNextColumn(); + //if( id.ObjectType == ObjectType.Accessory + // || id.ObjectType == ObjectType.Equipment ) + //{ + // ImGui.Text( id.ObjectType == ObjectType.Equipment + // || id.ObjectType == ObjectType.Accessory + // ? id.EquipSlot.ToString() + // : id.BodySlot.ToString() ); + //} + // + //ImGui.TableNextColumn(); + //ImGui.TableNextColumn(); + //ImGui.TableNextColumn(); + //if( id.ObjectType != ObjectType.Equipment + // && id.ObjectType != ObjectType.Accessory ) + //{ + // ImGui.Text( id.SecondaryId.ToString() ); + //} + // + //ImGui.TableNextColumn(); + //ImGui.Text( id.Variant.ToString() ); return ret; } @@ -494,45 +495,45 @@ namespace Penumbra.UI { var defaults = ( float )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; - var id = list[ manipIdx ].RspIdentifier; - var val = list[ manipIdx ].RspValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - if( DefaultButton( - $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) - && _editMode ) - { - list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults ); - ret = true; - } - - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", - _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - && val >= 0 - && val <= 5 - && _editMode ) - { - list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val ); - ret = true; - } - } - - ImGui.Text( id.Attribute.ToUngenderedString() ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( id.SubRace.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Attribute.ToGender().ToString() ); + //var id = list[ manipIdx ].RspIdentifier; + //var val = list[ manipIdx ].RspValue; + // + //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + //{ + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // if( DefaultButton( + // $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) + // && _editMode ) + // { + // list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults ); + // ret = true; + // } + // + // ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + // if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", + // _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) + // && val >= 0 + // && val <= 5 + // && _editMode ) + // { + // list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val ); + // ret = true; + // } + //} + // + //ImGui.Text( id.Attribute.ToUngenderedString() ); + //ImGui.TableNextColumn(); + //ImGui.TableNextColumn(); + //ImGui.TableNextColumn(); + //ImGui.Text( id.SubRace.ToString() ); + //ImGui.TableNextColumn(); + //ImGui.Text( id.Attribute.ToGender().ToString() ); return ret; } private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) { - var type = list[ manipIdx ].Type; + var type = list[ manipIdx ].ManipulationType; if( _editMode ) { @@ -553,40 +554,40 @@ namespace Penumbra.UI ImGui.TableNextColumn(); var changes = false; - switch( type ) - { - case MetaType.Eqp: - changes = DrawEqpRow( manipIdx, list ); - break; - case MetaType.Gmp: - changes = DrawGmpRow( manipIdx, list ); - break; - case MetaType.Eqdp: - changes = DrawEqdpRow( manipIdx, list ); - break; - case MetaType.Est: - changes = DrawEstRow( manipIdx, list ); - break; - case MetaType.Imc: - changes = DrawImcRow( manipIdx, list ); - break; - case MetaType.Rsp: - changes = DrawRspRow( manipIdx, list ); - break; - } - - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - ImGui.TableNextRow(); + //switch( type ) + //{ + // case MetaType.Eqp: + // changes = DrawEqpRow( manipIdx, list ); + // break; + // case MetaType.Gmp: + // changes = DrawGmpRow( manipIdx, list ); + // break; + // case MetaType.Eqdp: + // changes = DrawEqdpRow( manipIdx, list ); + // break; + // case MetaType.Est: + // changes = DrawEstRow( manipIdx, list ); + // break; + // case MetaType.Imc: + // changes = DrawImcRow( manipIdx, list ); + // break; + // case MetaType.Rsp: + // changes = DrawRspRow( manipIdx, list ); + // break; + //} + // + //ImGui.TableSetColumnIndex( 9 ); + //if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) ) + //{ + // ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); + //} + // + //ImGui.TableNextRow(); return changes; } - private MetaType DrawNewTypeSelection() + private MetaManipulation.Type DrawNewTypeSelection() { ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); ImGui.SameLine(); @@ -599,7 +600,7 @@ namespace Penumbra.UI ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); ImGui.SameLine(); ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); - return ( MetaType )_newManipTypeIdx; + return ( MetaManipulation.Type )_newManipTypeIdx; } private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) @@ -615,7 +616,7 @@ namespace Penumbra.UI MetaManipulation? newManip = null; switch( manipType ) { - case MetaType.Imc: + case MetaManipulation.Type.Imc: { RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); @@ -625,39 +626,39 @@ namespace Penumbra.UI { case ObjectType.Equipment: CustomCombo( "Equipment Slot", EqdpEquipSlots, out equipSlot, ref _newManipEquipSlot ); - newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant, - new ImcFile.ImageChangeData() ); + //newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant, + // new ImcFile.ImageChangeData() ); break; case ObjectType.DemiHuman: case ObjectType.Weapon: case ObjectType.Monster: RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); - newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, - _newManipVariant, new ImcFile.ImageChangeData() ); + //newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, + // _newManipVariant, new ImcFile.ImageChangeData() ); break; } break; } - case MetaType.Eqdp: + case MetaManipulation.Type.Eqdp: { RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); CustomCombo( "Race", Races, out var race, ref _newManipRace ); CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId, - new EqdpEntry() ); + //newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId, + // new EqdpEntry() ); break; } - case MetaType.Eqp: + case MetaManipulation.Type.Eqp: { RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 ); + //newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 ); break; } - case MetaType.Est: + case MetaManipulation.Type.Est: { RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); CustomCombo( "Object Type", ObjectTypes, out var objectType, ref _newManipObjectType ); @@ -675,47 +676,47 @@ namespace Penumbra.UI CustomCombo( "Race", Races, out var race, ref _newManipRace ); CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot, - ( ushort )_newManipSetId, 0 ); + //newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot, + // ( ushort )_newManipSetId, 0 ); break; } - case MetaType.Gmp: + case MetaManipulation.Type.Gmp: RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); - newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() ); + //newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() ); break; - case MetaType.Rsp: + case MetaManipulation.Type.Rsp: CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); - newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f ); + //newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f ); break; } - if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) - && newManip != null - && list.All( m => m.Identifier != newManip.Value.Identifier ) ) - { - var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); - if( def != null ) - { - var manip = newManip.Value.Type switch - { - MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), - MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, - ( ( ImcFile.ImageChangeData )def ).ToInteger() ), - MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace, - newManip.Value.RspIdentifier.Attribute, ( float )def ), - _ => throw new InvalidEnumArgumentException(), - }; - list.Add( manip ); - change = true; - ++count; - } - - ImGui.CloseCurrentPopup(); - } + //if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) + // && newManip != null + // && list.All( m => m.Identifier != newManip.Value.Identifier ) ) + //{ + // var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); + // if( def != null ) + // { + // var manip = newManip.Value.Type switch + // { + // MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), + // MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, + // ( ( ImcFile.ImageChangeData )def ).ToInteger() ), + // MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace, + // newManip.Value.RspIdentifier.Attribute, ( float )def ), + // _ => throw new InvalidEnumArgumentException(), + // }; + // list.Add( manip ); + // change = true; + // ++count; + // } + // + // ImGui.CloseCurrentPopup(); + //} return change; } diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index f46ad018..501653d4 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -82,7 +82,7 @@ public partial class SettingsInterface if( ImGui.IsItemClicked() ) { var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData(); - ImGui.SetClipboardText( string.Join( " ", + ImGui.SetClipboardText( ((IntPtr)( ( Interop.Structs.ResourceHandle* )r )->Data->VTable).ToString("X") + string.Join( " ", new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); } From de082439a4a235885371049ac5431246bec7daae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Mar 2022 11:13:50 +0100 Subject: [PATCH 0092/2451] tmp --- Penumbra/Configuration.cs | 1 - Penumbra/Importer/TexToolsMeta.cs | 145 +++-- Penumbra/Interop/CharacterUtility.cs | 16 +- .../Interop/ResourceLoader.Replacement.cs | 16 +- Penumbra/Interop/Structs/CharacterUtility.cs | 19 +- Penumbra/Meta/Files/EqpGmpFile.cs | 2 +- Penumbra/Meta/Files/ImcFile.cs | 36 +- Penumbra/Meta/Files/MetaBaseFile.cs | 6 +- Penumbra/Meta/Files/MetaFilenames.cs | 83 --- .../Meta/Manipulations/ImcManipulation.cs | 31 +- .../Meta/Manipulations/MetaManipulation.cs | 9 +- Penumbra/Meta/MetaCollection.cs | 34 +- Penumbra/Meta/MetaManager.cs | 254 ++++---- Penumbra/Mods/CollectionManager.cs | 18 +- Penumbra/Mods/ModCollection.cs | 10 +- Penumbra/Mods/ModCollectionCache.cs | 40 +- Penumbra/Mods/ModManager.Directory.cs | 110 ++++ Penumbra/Mods/ModManager.cs | 564 +++++++----------- Penumbra/Mods/ModManagerEditExtensions.cs | 2 +- Penumbra/Penumbra.cs | 25 +- Penumbra/UI/MenuTabs/TabDebug.cs | 110 ++-- Penumbra/UI/MenuTabs/TabEffective.cs | 10 +- .../TabInstalled/TabInstalledDetails.cs | 3 +- .../TabInstalledDetailsManipulations.cs | 11 - .../TabInstalled/TabInstalledSelector.cs | 3 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- Penumbra/UI/MenuTabs/TabSettings.cs | 39 +- Penumbra/UI/SettingsInterface.cs | 2 +- 28 files changed, 766 insertions(+), 835 deletions(-) delete mode 100644 Penumbra/Meta/Files/MetaFilenames.cs create mode 100644 Penumbra/Mods/ModManager.Directory.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a59ab245..fbfd7ddc 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -35,7 +35,6 @@ public class Configuration : IPluginConfiguration public int WaitFrames { get; set; } = 30; public string ModDirectory { get; set; } = string.Empty; - public string TempDirectory { get; set; } = string.Empty; public string CurrentCollection { get; set; } = "Default"; public string DefaultCollection { get; set; } = "Default"; diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 4dc10b26..8c9e418f 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -256,11 +256,10 @@ public class TexToolsMeta ushort i = 0; if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) { + // TODO check against default. foreach( var value in values ) { - ImcEntry def; - if( !value.Equals( def ) ) - ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) ); + ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) ); ++i; } } @@ -268,55 +267,64 @@ public class TexToolsMeta { foreach( var value in values ) { - ImcEntry def; - if( !value.Equals( def ) ) - ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) ); + ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) ); ++i; } } } + private static string ReadNullTerminated( BinaryReader reader ) + { + var builder = new System.Text.StringBuilder(); + for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) + { + builder.Append( c ); + } + + return builder.ToString(); + } + public TexToolsMeta( byte[] data ) { try { - //using var reader = new BinaryReader( new MemoryStream( data ) ); - //Version = reader.ReadUInt32(); - //FilePath = ReadNullTerminated( reader ); - //var metaInfo = new Info( FilePath ); - //var numHeaders = reader.ReadUInt32(); - //var headerSize = reader.ReadUInt32(); - //var headerStart = reader.ReadUInt32(); - //reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); - // - //List< (MetaType type, uint offset, int size) > entries = new(); - //for( var i = 0; i < numHeaders; ++i ) - //{ - // var currentOffset = reader.BaseStream.Position; - // var type = ( MetaType )reader.ReadUInt32(); - // var offset = reader.ReadUInt32(); - // var size = reader.ReadInt32(); - // entries.Add( ( type, offset, size ) ); - // reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); - //} - // - //byte[]? ReadEntry( MetaType type ) - //{ - // var idx = entries.FindIndex( t => t.type == type ); - // if( idx < 0 ) - // { - // return null; - // } - // - // reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); - // return reader.ReadBytes( entries[ idx ].size ); - //} - // - //DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); - //DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); - //DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); - //DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); - //DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); + using var reader = new BinaryReader( new MemoryStream( data ) ); + Version = reader.ReadUInt32(); + FilePath = ReadNullTerminated( reader ); + var metaInfo = new Info( FilePath ); + var numHeaders = reader.ReadUInt32(); + var headerSize = reader.ReadUInt32(); + var headerStart = reader.ReadUInt32(); + reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); + + List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); + for( var i = 0; i < numHeaders; ++i ) + { + var currentOffset = reader.BaseStream.Position; + var type = ( MetaManipulation.Type )reader.ReadUInt32(); + var offset = reader.ReadUInt32(); + var size = reader.ReadInt32(); + entries.Add( ( type, offset, size ) ); + reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); + } + + byte[]? ReadEntry( MetaManipulation.Type type ) + { + var idx = entries.FindIndex( t => t.type == type ); + if( idx < 0 ) + { + return null; + } + + reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); + return reader.ReadBytes( entries[ idx ].size ); + } + + DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); + DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); + DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); + DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); + DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); } catch( Exception e ) { @@ -362,28 +370,35 @@ public class TexToolsMeta return Invalid; } - //if( gender == 1 ) - //{ - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) ); - // - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) ); - //} - //else - //{ - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) ); - // ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) ); - //} - // + void Add( RspAttribute attribute, float value ) + { + var def = CmpFile.GetDefault( subRace, attribute ); + if (value != def) + ret!.RspManipulations.Add(new RspManipulation(subRace, attribute, value)); + } + + if( gender == 1 ) + { + Add(RspAttribute.FemaleMinSize, br.ReadSingle() ); + Add(RspAttribute.FemaleMaxSize, br.ReadSingle() ); + Add(RspAttribute.FemaleMinTail, br.ReadSingle() ); + Add(RspAttribute.FemaleMaxTail, br.ReadSingle() ); + + Add(RspAttribute.BustMinX, br.ReadSingle() ); + Add(RspAttribute.BustMinY, br.ReadSingle() ); + Add(RspAttribute.BustMinZ, br.ReadSingle() ); + Add(RspAttribute.BustMaxX, br.ReadSingle() ); + Add(RspAttribute.BustMaxY, br.ReadSingle() ); + Add(RspAttribute.BustMaxZ, br.ReadSingle() ); + } + else + { + Add(RspAttribute.MaleMinSize, br.ReadSingle() ); + Add(RspAttribute.MaleMaxSize, br.ReadSingle() ); + Add(RspAttribute.MaleMinTail, br.ReadSingle() ); + Add(RspAttribute.MaleMaxTail, br.ReadSingle() ); + } + return ret; } } \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index f73c654f..9d938f34 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -20,12 +20,20 @@ public unsafe class CharacterUtility : IDisposable public Structs.CharacterUtility* Address => *_characterUtilityAddress; - public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumResources]; + public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumRelevantResources]; public CharacterUtility() { SignatureHelper.Initialise( this ); - LoadDataFilesHook.Enable(); + + if( Address->EqpResource != null ) + { + LoadDefaultResources(); + } + else + { + LoadDataFilesHook.Enable(); + } } // Self-disabling hook to set default resources after loading them. @@ -40,7 +48,7 @@ public unsafe class CharacterUtility : IDisposable // We store the default data of the resources so we can always restore them. private void LoadDefaultResources() { - for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) { var resource = ( Structs.ResourceHandle* )Address->Resources[ i ]; DefaultResources[ i ] = resource->GetData(); @@ -65,7 +73,7 @@ public unsafe class CharacterUtility : IDisposable public void Dispose() { - for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) { ResetResource( i ); } diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/ResourceLoader.Replacement.cs index 07113232..9ecb85f7 100644 --- a/Penumbra/Interop/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/ResourceLoader.Replacement.cs @@ -5,10 +5,7 @@ using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; -using Penumbra.Mods; -using Penumbra.Util; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -123,6 +120,10 @@ public unsafe partial class ResourceLoader ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); } + else if( ResourceLoadCustomization != null && gamePath.Path[0] == (byte) '|' ) + { + ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); + } else { // Specify that we are loading unpacked files from the drive. @@ -150,11 +151,18 @@ public unsafe partial class ResourceLoader return ret; } + // Customize file loading for any GamePaths that start with "|". + public delegate byte ResourceLoadCustomizationDelegate( Utf8GamePath gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ); + + public ResourceLoadCustomizationDelegate? ResourceLoadCustomization; + + // Use the default method of path replacement. public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path ) { var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); - return( resolved, null ); + return ( resolved, null ); } private void DisposeHooks() diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 9164f6e8..62762243 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -7,15 +7,16 @@ namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] public unsafe struct CharacterUtility { - public const int NumResources = 85; - public const int EqpIdx = 0; - public const int GmpIdx = 1; - public const int HumanCmpIdx = 63; - public const int FaceEstIdx = 64; - public const int HairEstIdx = 65; - public const int BodyEstIdx = 66; - public const int HeadEstIdx = 67; - public const int NumEqdpFiles = 2 * 28; + public const int NumResources = 85; + public const int NumRelevantResources = 68; + public const int EqpIdx = 0; + public const int GmpIdx = 1; + public const int HumanCmpIdx = 63; + public const int FaceEstIdx = 64; + public const int HairEstIdx = 65; + public const int BodyEstIdx = 66; + public const int HeadEstIdx = 67; + public const int NumEqdpFiles = 2 * 28; public static int EqdpIdx( GenderRace raceCode, bool accessory ) => ( accessory ? 28 : 0 ) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 62aff162..5b6d6479 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -55,7 +55,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile { var ptr = ( byte* )DefaultData.Data; var controlBlock = *( ulong* )ptr; - *( ulong* )ptr = ulong.MaxValue; + *( ulong* )Data = ulong.MaxValue; for( var i = 0; i < 64; ++i ) { var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 36576d00..1c23dd32 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,19 +1,21 @@ using System; using System.Numerics; using Dalamud.Logging; +using Dalamud.Memory; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; -public struct ImcEntry : IEquatable< ImcEntry > +public readonly struct ImcEntry : IEquatable< ImcEntry > { - public byte MaterialId; - public byte DecalId; - private ushort _attributeAndSound; - public byte VfxId; - public byte MaterialAnimationId; + public readonly byte MaterialId; + public readonly byte DecalId; + private readonly ushort _attributeAndSound; + public readonly byte VfxId; + public readonly byte MaterialAnimationId; public ushort AttributeMask => ( ushort )( _attributeAndSound & 0x3FF ); @@ -163,4 +165,26 @@ public unsafe class ImcFile : MetaBaseFile Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); } } + + public void Replace( ResourceHandle* resource ) + { + var (data, length) = resource->GetData(); + if( data == IntPtr.Zero ) + { + return; + } + + var requiredLength = ActualLength; + if( length >= requiredLength ) + { + Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); + Functions.MemSet( ( byte* )data + requiredLength, 0, length - requiredLength ); + return; + } + + MemoryHelper.GameFree( ref data, ( ulong )length ); + var file = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )requiredLength ); + Functions.MemCpyUnchecked( file, Data, requiredLength ); + resource->SetData( ( IntPtr )file, requiredLength ); + } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 1d82e224..304f0f09 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Dalamud.Memory; namespace Penumbra.Meta.Files; @@ -23,14 +24,15 @@ public unsafe class MetaBaseFile : IDisposable protected void AllocateData( int length ) { Length = length; - Data = ( byte* )Marshal.AllocHGlobal( length ); + Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); ; GC.AddMemoryPressure( length ); } // Free memory. protected void ReleaseUnmanagedResources() { - Marshal.FreeHGlobal( ( IntPtr )Data ); + var ptr = ( IntPtr )Data; + MemoryHelper.GameFree( ref ptr, (ulong) Length ); GC.RemoveMemoryPressure( Length ); Length = 0; Data = null; diff --git a/Penumbra/Meta/Files/MetaFilenames.cs b/Penumbra/Meta/Files/MetaFilenames.cs deleted file mode 100644 index 06360afe..00000000 --- a/Penumbra/Meta/Files/MetaFilenames.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; - -namespace Penumbra.Meta.Files -{ - // Contains all filenames for meta changes depending on their parameters. - public static class MetaFileNames - { - public static GamePath Eqp() - => GamePath.GenerateUnchecked( "chara/xls/equipmentparameter/equipmentparameter.eqp" ); - - public static GamePath Gmp() - => GamePath.GenerateUnchecked( "chara/xls/equipmentparameter/gimmickparameter.gmp" ); - - public static GamePath Est( ObjectType type, EquipSlot equip, BodySlot slot ) - { - return type switch - { - ObjectType.Equipment => equip switch - { - EquipSlot.Body => GamePath.GenerateUnchecked( "chara/xls/charadb/extra_top.est" ), - EquipSlot.Head => GamePath.GenerateUnchecked( "chara/xls/charadb/extra_met.est" ), - _ => throw new NotImplementedException(), - }, - ObjectType.Character => slot switch - { - BodySlot.Hair => GamePath.GenerateUnchecked( "chara/xls/charadb/hairskeletontemplate.est" ), - BodySlot.Face => GamePath.GenerateUnchecked( "chara/xls/charadb/faceskeletontemplate.est" ), - _ => throw new NotImplementedException(), - }, - _ => throw new NotImplementedException(), - }; - } - - public static GamePath Imc( ObjectType type, ushort primaryId, ushort secondaryId ) - { - return type switch - { - ObjectType.Accessory => GamePath.GenerateUnchecked( $"chara/accessory/a{primaryId:D4}/a{primaryId:D4}.imc" ), - ObjectType.Equipment => GamePath.GenerateUnchecked( $"chara/equipment/e{primaryId:D4}/e{primaryId:D4}.imc" ), - ObjectType.DemiHuman => GamePath.GenerateUnchecked( - $"chara/demihuman/d{primaryId:D4}/obj/equipment/e{secondaryId:D4}/e{secondaryId:D4}.imc" ), - ObjectType.Monster => GamePath.GenerateUnchecked( - $"chara/monster/m{primaryId:D4}/obj/body/b{secondaryId:D4}/b{secondaryId:D4}.imc" ), - ObjectType.Weapon => GamePath.GenerateUnchecked( - $"chara/weapon/w{primaryId:D4}/obj/body/b{secondaryId:D4}/b{secondaryId:D4}.imc" ), - _ => throw new NotImplementedException(), - }; - } - - public static GamePath Eqdp( ObjectType type, GenderRace gr ) - { - return type switch - { - ObjectType.Accessory => GamePath.GenerateUnchecked( $"chara/xls/charadb/accessorydeformerparameter/c{gr.ToRaceCode()}.eqdp" ), - ObjectType.Equipment => GamePath.GenerateUnchecked( $"chara/xls/charadb/equipmentdeformerparameter/c{gr.ToRaceCode()}.eqdp" ), - _ => throw new NotImplementedException(), - }; - } - - public static GamePath Eqdp( EquipSlot slot, GenderRace gr ) - { - return slot switch - { - EquipSlot.Head => Eqdp( ObjectType.Equipment, gr ), - EquipSlot.Body => Eqdp( ObjectType.Equipment, gr ), - EquipSlot.Feet => Eqdp( ObjectType.Equipment, gr ), - EquipSlot.Hands => Eqdp( ObjectType.Equipment, gr ), - EquipSlot.Legs => Eqdp( ObjectType.Equipment, gr ), - EquipSlot.Neck => Eqdp( ObjectType.Accessory, gr ), - EquipSlot.Ears => Eqdp( ObjectType.Accessory, gr ), - EquipSlot.Wrists => Eqdp( ObjectType.Accessory, gr ), - EquipSlot.LFinger => Eqdp( ObjectType.Accessory, gr ), - EquipSlot.RFinger => Eqdp( ObjectType.Accessory, gr ), - _ => throw new NotImplementedException(), - }; - } - - public static GamePath Cmp() - => GamePath.GenerateUnchecked( "chara/xls/charamake/human.cmp" ); - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 287708a0..42a19a22 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,8 +1,8 @@ using System; using System.Runtime.InteropServices; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Meta.Files; -using ImcFile = Lumina.Data.Files.ImcFile; namespace Penumbra.Meta.Manipulations; @@ -58,4 +58,33 @@ public readonly struct ImcManipulation : IEquatable< ImcManipulation > public override int GetHashCode() => HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot ); + + public Utf8GamePath GamePath() + { + return ObjectType switch + { + ObjectType.Accessory => Utf8GamePath.FromString( $"chara/accessory/a{PrimaryId:D4}/a{PrimaryId:D4}.imc", out var p ) + ? p + : Utf8GamePath.Empty, + ObjectType.Equipment => Utf8GamePath.FromString( $"chara/equipment/e{PrimaryId:D4}/e{PrimaryId:D4}.imc", out var p ) + ? p + : Utf8GamePath.Empty, + ObjectType.DemiHuman => Utf8GamePath.FromString( + $"chara/demihuman/d{PrimaryId:D4}/obj/equipment/e{SecondaryId:D4}/e{SecondaryId:D4}.imc", out var p ) + ? p + : Utf8GamePath.Empty, + ObjectType.Monster => Utf8GamePath.FromString( $"chara/monster/m{PrimaryId:D4}/obj/body/b{SecondaryId:D4}/b{SecondaryId:D4}.imc", + out var p ) + ? p + : Utf8GamePath.Empty, + ObjectType.Weapon => Utf8GamePath.FromString( $"chara/weapon/w{PrimaryId:D4}/obj/body/b{SecondaryId:D4}/b{SecondaryId:D4}.imc", + out var p ) + ? p + : Utf8GamePath.Empty, + _ => throw new NotImplementedException(), + }; + } + + public bool Apply( ImcFile file ) + => file.SetEntry( ImcFile.PartIndex( EquipSlot ), Variant, Entry ); } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f33e7a97..af07d17b 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,10 +1,11 @@ using System; using System.Runtime.InteropServices; +using Penumbra.GameData.Util; namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] -public readonly struct MetaManipulation : IEquatable< MetaManipulation > +public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable { public enum Type : byte { @@ -106,4 +107,10 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation > Type.Imc => Imc.GetHashCode(), _ => throw new ArgumentOutOfRangeException(), }; + + public unsafe int CompareTo( MetaManipulation other ) + { + fixed(MetaManipulation* lhs = &this) + return Functions.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); + } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index a0ec0027..2c09aba3 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -93,14 +93,14 @@ public class MetaCollection return false; } - if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) - { - return false; - } + //if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) + //{ + // return false; + //} } - } + } // TODO - return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); + return true; //DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); } // Re-sort all manipulations. @@ -117,31 +117,33 @@ public class MetaCollection // Creates the option group and the option if necessary. private void AddMeta( string group, string option, TexToolsMeta meta ) { - if( meta.Manipulations.Count == 0 ) - { - return; - } + var manipulations = meta.EqpManipulations.Select( m => new MetaManipulation( m ) ) + .Concat( meta.EqdpManipulations.Select( m => new MetaManipulation( m ) ) ) + .Concat( meta.EstManipulations.Select( m => new MetaManipulation( m ) ) ) + .Concat( meta.GmpManipulations.Select( m => new MetaManipulation( m ) ) ) + .Concat( meta.RspManipulations.Select( m => new MetaManipulation( m ) ) ) + .Concat( meta.ImcManipulations.Select( m => new MetaManipulation( m ) ) ).ToList(); if( group.Length == 0 ) { - DefaultData.AddRange( meta.Manipulations ); + DefaultData.AddRange( manipulations ); } else if( option.Length == 0 ) { } else if( !GroupData.TryGetValue( group, out var options ) ) { - GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); + GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, manipulations } } ); } else if( !options.TryGetValue( option, out var list ) ) { - options.Add( option, meta.Manipulations.ToList() ); + options.Add( option, manipulations ); } else { - list.AddRange( meta.Manipulations ); + list.AddRange( manipulations ); } - Count += meta.Manipulations.Count; + Count += manipulations.Count; } // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, @@ -160,7 +162,7 @@ public class MetaCollection _ => TexToolsMeta.Invalid, }; - if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) + if( metaData.FilePath == string.Empty ) { continue; } diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 1c56b35b..d9519863 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -2,26 +2,19 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; -using Lumina.Data.Files; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Util; +using Penumbra.Mods; +using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility; +using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.Meta; -public struct TemporaryImcFile : IDisposable -{ - - public void Dispose() - { - - } -} - public class MetaManager2 : IDisposable { public readonly List< MetaBaseFile > ChangedData = new(7 + CharacterUtility.NumEqdpFiles); @@ -42,7 +35,51 @@ public class MetaManager2 : IDisposable public readonly Dictionary< EqdpManipulation, Mod.Mod > EqdpManipulations = new(); public readonly Dictionary< ImcManipulation, Mod.Mod > ImcManipulations = new(); - public readonly List< TemporaryImcFile > ImcFiles = new(); + public readonly Dictionary< Utf8GamePath, ImcFile > ImcFiles = new(); + + private readonly ModCollection _collection; + + public unsafe void SetFiles() + { + foreach( var file in ChangedData ) + { + Penumbra.CharacterUtility.SetResource( file.Index, ( IntPtr )file.Data, file.Length ); + } + } + + public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod ) + { + mod = manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => EqpManipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, + MetaManipulation.Type.Gmp => GmpManipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, + MetaManipulation.Type.Eqdp => EqdpManipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, + MetaManipulation.Type.Est => EstManipulations.TryGetValue( manip.Est, out var m ) ? m : null, + MetaManipulation.Type.Rsp => RspManipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, + MetaManipulation.Type.Imc => ImcManipulations.TryGetValue( manip.Imc, out var m ) ? m : null, + _ => throw new ArgumentOutOfRangeException(), + }; + return mod != null; + } + + public int Count + => ImcManipulations.Count + + EqdpManipulations.Count + + RspManipulations.Count + + GmpManipulations.Count + + EstManipulations.Count + + EqpManipulations.Count; + + public MetaManager2( ModCollection collection ) + => _collection = collection; + + public void ApplyImcFiles( Dictionary< Utf8GamePath, FullPath > resolvedFiles ) + { + foreach( var path in ImcFiles.Keys ) + { + resolvedFiles[ path ] = CreateImcPath( path ); + } + } public void ResetEqp() { @@ -95,10 +132,21 @@ public class MetaManager2 : IDisposable EqdpManipulations.Clear(); } + private FullPath CreateImcPath( Utf8GamePath path ) + { + var d = new DirectoryInfo( $":{_collection.Name}/" ); + return new FullPath( d, new Utf8RelPath( path ) ); + } + public void ResetImc() { - foreach( var file in ImcFiles ) + foreach( var (path, file) in ImcFiles ) + { + _collection.Cache?.ResolvedFiles.Remove( path ); + path.Dispose(); file.Dispose(); + } + ImcFiles.Clear(); ImcManipulations.Clear(); } @@ -154,7 +202,6 @@ public class MetaManager2 : IDisposable } } - public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) { if( !EqpManipulations.TryAdd( m, mod ) ) @@ -247,16 +294,63 @@ public class MetaManager2 : IDisposable return true; } - public bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) { + const uint imcExt = 0x00696D63; + if( !ImcManipulations.TryAdd( m, mod ) ) { return false; } + var path = m.GamePath(); + if( !ImcFiles.TryGetValue( path, out var file ) ) + { + file = new ImcFile( path ); + } + + if( !m.Apply( file ) ) + { + return false; + } + + ImcFiles[ path ] = file; + var fullPath = CreateImcPath( path ); + if( _collection.Cache != null ) + { + _collection.Cache.ResolvedFiles[ path ] = fullPath; + } + + var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 ); + if( resource != null ) + { + file.Replace( ( ResourceHandle* )resource ); + } + return true; } + public static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + var split = gamePath.Path.Split( ( byte )'|', 2, true ); + fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; + + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) + && collection.Cache != null + && collection.Cache.MetaManipulations.ImcFiles.TryGetValue( + Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + { + file.Replace( fileDescriptor->ResourceHandle ); + } + + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + return ret; + } + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) { return m.ManipulationType switch @@ -267,133 +361,7 @@ public class MetaManager2 : IDisposable MetaManipulation.Type.Est => ApplyMod( m.Est, mod ), MetaManipulation.Type.Rsp => ApplyMod( m.Rsp, mod ), MetaManipulation.Type.Imc => ApplyMod( m.Imc, mod ), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(), }; } -} - -public class MetaManager : IDisposable -{ - internal class FileInformation - { - public readonly object Data; - public bool Changed; - public FullPath? CurrentFile; - public byte[] ByteData = Array.Empty< byte >(); - - public FileInformation( object data ) - => Data = data; - - public void Write( DirectoryInfo dir, Utf8GamePath originalPath ) - { - ByteData = Data switch - { - ImcFile imc => imc.WriteBytes(), - _ => throw new NotImplementedException(), - }; - DisposeFile( CurrentFile ); - CurrentFile = new FullPath( TempFile.WriteNew( dir, ByteData, $"_{originalPath.Filename()}" ) ); - Changed = false; - } - } - - public const string TmpDirectory = "penumbrametatmp"; - - private readonly DirectoryInfo _dir; - private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles; - - private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); - private readonly Dictionary< Utf8GamePath, FileInformation > _currentFiles = new(); - - public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations - => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - - public IEnumerable< (Utf8GamePath, FullPath) > Files - => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) - .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); - - public int Count - => _currentManipulations.Count; - - public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) - => _currentManipulations.TryGetValue( manip, out mod! ); - - public byte[] EqpData = Array.Empty< byte >(); - - private static void DisposeFile( FullPath? file ) - { - if( !( file?.Exists ?? false ) ) - { - return; - } - - try - { - File.Delete( file.Value.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); - } - } - - public void Reset( bool reload = true ) - { - foreach( var file in _currentFiles ) - { - _resolvedFiles.Remove( file.Key ); - DisposeFile( file.Value.CurrentFile ); - } - - _currentManipulations.Clear(); - _currentFiles.Clear(); - ClearDirectory(); - if( reload ) - { - Penumbra.ResidentResources.Reload(); - } - } - - public void Dispose() - => Reset(); - - private static void ClearDirectory( DirectoryInfo modDir ) - { - modDir.Refresh(); - if( modDir.Exists ) - { - try - { - Directory.Delete( modDir.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not clear temporary metafile directory \"{modDir.FullName}\":\n{e}" ); - } - } - } - - private void ClearDirectory() - => ClearDirectory( _dir ); - - public MetaManager( string name, Dictionary< Utf8GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) - { - _resolvedFiles = resolvedFiles; - _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); - ClearDirectory(); - } - - public void WriteNewFiles() - { - if( _currentFiles.Any() ) - { - Directory.CreateDirectory( _dir.FullName ); - } - - foreach( var (key, value) in _currentFiles.Where( kvp => kvp.Value.Changed ) ) - { - value.Write( _dir, key ); - _resolvedFiles[ key ] = value.CurrentFile!.Value; - } - } } \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index f614fef3..82874d87 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -67,15 +67,9 @@ public class CollectionManager public void RecreateCaches() { - if( !_manager.TempWritable ) - { - PluginLog.Error( "No temporary directory available." ); - return; - } - foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) { - collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } CreateNecessaryCaches(); @@ -176,15 +170,9 @@ public class CollectionManager private void AddCache( ModCollection collection ) { - if( !_manager.TempWritable ) - { - PluginLog.Error( "No tmp directory available." ); - return; - } - if( collection.Cache == null && collection.Name != string.Empty ) { - collection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } } @@ -289,7 +277,7 @@ public class CollectionManager CurrentCollection = Collections[ ModCollection.DefaultCollection ]; if( CurrentCollection.Cache == null ) { - CurrentCollection.CreateCache( _manager.TempPath, _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); + CurrentCollection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } config.CurrentCollection = ModCollection.DefaultCollection; diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index cfaba390..90a96dd1 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -65,9 +65,9 @@ public class ModCollection return removeList.Length > 0; } - public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data ) + public void CreateCache( IEnumerable< ModData > data ) { - Cache = new ModCollectionCache( Name, modDirectory ); + Cache = new ModCollectionCache( this ); var changedSettings = false; foreach( var mod in data ) { @@ -89,7 +89,7 @@ public class ModCollection Save(); } - CalculateEffectiveFileList( modDirectory, true, false ); + CalculateEffectiveFileList( true, false ); } public void ClearCache() @@ -135,11 +135,11 @@ public class ModCollection } } - public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection ) + public void CalculateEffectiveFileList( bool withMetaManipulations, bool activeCollection ) { PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, withMetaManipulations, activeCollection ); - Cache ??= new ModCollectionCache( Name, modDir ); + Cache ??= new ModCollectionCache( this ); UpdateSettings( false ); Cache.CalculateEffectiveFileList(); if( withMetaManipulations ) diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index d9f4b144..20a7b583 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -27,8 +27,7 @@ public class ModCollectionCache private readonly SortedList< string, object? > _changedItems = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new(); - public readonly HashSet< ulong > Checksums = new(); - public readonly MetaManager MetaManipulations; + public readonly MetaManager2 MetaManipulations; public IReadOnlyDictionary< string, object? > ChangedItems { @@ -39,8 +38,8 @@ public class ModCollectionCache } } - public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) - => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); + public ModCollectionCache( ModCollection collection ) + => MetaManipulations = new MetaManager2( collection ); private static void ResetFileSeen( int size ) { @@ -73,11 +72,6 @@ public class ModCollectionCache } AddMetaFiles(); - Checksums.Clear(); - foreach( var file in ResolvedFiles ) - { - Checksums.Add( file.Value.Crc64 ); - } } private void SetChangedItems() @@ -89,11 +83,10 @@ public class ModCollectionCache try { - // Skip meta files because IMCs would result in far too many false-positive items, + // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. - var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet(); var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) + foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) { identifier.Identify( _changedItems, resolved.ToGamePath() ); } @@ -265,18 +258,7 @@ public class ModCollectionCache } private void AddMetaFiles() - { - foreach( var (gamePath, file) in MetaManipulations.Files ) - { - if( RegisteredFiles.TryGetValue( gamePath, out var mod ) ) - { - PluginLog.Warning( - $"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." ); - } - - ResolvedFiles[ gamePath ] = file; - } - } + => MetaManipulations.ApplyImcFiles( ResolvedFiles ); private void AddSwaps( Mod.Mod mod ) { @@ -304,12 +286,12 @@ public class ModCollectionCache { if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) { - //MetaManipulations.ApplyMod( manip, mod ); + MetaManipulations.ApplyMod( manip, mod ); } else { - mod.Cache.AddConflict( oldMod, manip ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) + mod.Cache.AddConflict( oldMod!, manip ); + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod!.Settings.Priority ) { oldMod.Cache.AddConflict( mod, manip ); } @@ -319,15 +301,13 @@ public class ModCollectionCache public void UpdateMetaManipulations() { - MetaManipulations.Reset( false ); + MetaManipulations.Reset(); foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) { mod.Cache.ClearMetaConflicts(); AddManipulations( mod ); } - - MetaManipulations.WriteNewFiles(); } public void RemoveMod( DirectoryInfo basePath ) diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs new file mode 100644 index 00000000..b14f4929 --- /dev/null +++ b/Penumbra/Mods/ModManager.Directory.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Penumbra.Mod; + +namespace Penumbra.Mods; + +public partial class ModManagerNew +{ + private readonly List _mods = new(); + public IReadOnlyList Mods + => _mods; + + public void DiscoverMods() + { + //_mods.Clear(); + // + //if( CheckValidity() ) + //{ + // foreach( var modFolder in BasePath.EnumerateDirectories() ) + // { + // var mod = ModData.LoadMod( StructuredMods, modFolder ); + // if( mod == null ) + // { + // continue; + // } + // + // Mods.Add( modFolder.Name, mod ); + // } + // + // SetModStructure(); + //} + // + //Collections.RecreateCaches(); + } +} + +public partial class ModManagerNew +{ + public DirectoryInfo BasePath { get; private set; } = null!; + public bool Valid { get; private set; } + + public event Action< DirectoryInfo >? BasePathChanged; + + public ModManagerNew() + { + InitBaseDirectory( Penumbra.Config.ModDirectory ); + } + + public bool CheckValidity() + { + if( Valid ) + { + Valid = Directory.Exists(BasePath.FullName); + } + + return Valid; + } + + private static (DirectoryInfo, bool) CreateDirectory( string path ) + { + var newDir = new DirectoryInfo( path ); + if( !newDir.Exists ) + { + try + { + Directory.CreateDirectory( newDir.FullName ); + newDir.Refresh(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); + return ( newDir, false ); + } + } + + return ( newDir, true ); + } + + private void InitBaseDirectory( string path ) + { + if( path.Length == 0 ) + { + Valid = false; + BasePath = new DirectoryInfo( "." ); + return; + } + + + ( BasePath, Valid ) = CreateDirectory( path ); + + if( Penumbra.Config.ModDirectory != BasePath.FullName ) + { + Penumbra.Config.ModDirectory = BasePath.FullName; + Penumbra.Config.Save(); + } + } + + private void ChangeBaseDirectory( string path ) + { + if( string.Equals( path, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + InitBaseDirectory( path ); + BasePathChanged?.Invoke( BasePath ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index d0b7bf2d..4538fa85 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -4,401 +4,297 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +// The ModManager handles the basic mods installed to the mod directory. +// It also contains the CollectionManager that handles all collections. +public class ModManager { - // The ModManager handles the basic mods installed to the mod directory. - // It also contains the CollectionManager that handles all collections. - public class ModManager + public DirectoryInfo BasePath { get; private set; } = null!; + + public Dictionary< string, ModData > Mods { get; } = new(); + public ModFolder StructuredMods { get; } = ModFileSystem.Root; + + public CollectionManager Collections { get; } + + public bool Valid { get; private set; } + + public Configuration Config + => Penumbra.Config; + + public void DiscoverMods( string newDir ) { - public DirectoryInfo BasePath { get; private set; } = null!; - public DirectoryInfo TempPath { get; private set; } = null!; + SetBaseDirectory( newDir, false ); + DiscoverMods(); + } - public Dictionary< string, ModData > Mods { get; } = new(); - public ModFolder StructuredMods { get; } = ModFileSystem.Root; - - public CollectionManager Collections { get; } - - public bool Valid { get; private set; } - public bool TempWritable { get; private set; } - - public Configuration Config - => Penumbra.Config; - - public void DiscoverMods( string newDir ) + private void SetBaseDirectory( string newPath, bool firstTime ) + { + if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) { - SetBaseDirectory( newDir, false ); - DiscoverMods(); + return; } - private void ClearOldTmpDir() + if( !newPath.Any() ) { - if( !TempWritable ) - { - return; - } - - TempPath.Refresh(); - if( TempPath.Exists ) + Valid = false; + BasePath = new DirectoryInfo( "." ); + } + else + { + var newDir = new DirectoryInfo( newPath ); + if( !newDir.Exists ) { try { - TempPath.Delete( true ); + Directory.CreateDirectory( newDir.FullName ); + newDir.Refresh(); } catch( Exception e ) { - PluginLog.Error( $"Could not delete temporary directory {TempPath.FullName}:\n{e}" ); + PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); } } + + BasePath = newDir; + Valid = true; + if( Config.ModDirectory != BasePath.FullName ) + { + Config.ModDirectory = BasePath.FullName; + Config.Save(); + } + + if( !firstTime ) + { + Collections.RecreateCaches(); + } + } + } + + public ModManager() + { + SetBaseDirectory( Config.ModDirectory, true ); + Collections = new CollectionManager( this ); + } + + private bool SetSortOrderPath( ModData mod, string path ) + { + mod.Move( path ); + var fixedPath = mod.SortOrder.FullPath; + if( !fixedPath.Any() || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) + { + Config.ModSortOrder.Remove( mod.BasePath.Name ); + return true; + } + + if( path != fixedPath ) + { + Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; + return true; + } + + return false; + } + + private void SetModStructure( bool removeOldPaths = false ) + { + var changes = false; + + foreach( var kvp in Config.ModSortOrder.ToArray() ) + { + if( kvp.Value.Any() && Mods.TryGetValue( kvp.Key, out var mod ) ) + { + changes |= SetSortOrderPath( mod, kvp.Value ); + } + else if( removeOldPaths ) + { + changes = true; + Config.ModSortOrder.Remove( kvp.Key ); + } } - private static bool CheckTmpDir( string newPath, out DirectoryInfo tmpDir ) + if( changes ) { - tmpDir = new DirectoryInfo( Path.Combine( newPath, MetaManager.TmpDirectory ) ); - try + Config.Save(); + } + } + + public void DiscoverMods() + { + Mods.Clear(); + BasePath.Refresh(); + + StructuredMods.SubFolders.Clear(); + StructuredMods.Mods.Clear(); + if( Valid && BasePath.Exists ) + { + foreach( var modFolder in BasePath.EnumerateDirectories() ) { - if( tmpDir.Exists ) + var mod = ModData.LoadMod( StructuredMods, modFolder ); + if( mod == null ) { - tmpDir.Delete( true ); - tmpDir.Refresh(); + continue; } - Directory.CreateDirectory( tmpDir.FullName ); - tmpDir.Refresh(); - return true; + Mods.Add( modFolder.Name, mod ); + } + + SetModStructure(); + } + + Collections.RecreateCaches(); + } + + public void DeleteMod( DirectoryInfo modFolder ) + { + modFolder.Refresh(); + if( modFolder.Exists ) + { + try + { + Directory.Delete( modFolder.FullName, true ); } catch( Exception e ) { - PluginLog.Error( $"Could not create temporary directory {tmpDir.FullName}:\n{e}" ); - return false; + PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); + } + + if( Mods.TryGetValue( modFolder.Name, out var mod ) ) + { + mod.SortOrder.ParentFolder.RemoveMod( mod ); + Mods.Remove( modFolder.Name ); + Collections.RemoveModFromCaches( modFolder ); } } + } - private void SetBaseDirectory( string newPath, bool firstTime ) + public bool AddMod( DirectoryInfo modFolder ) + { + var mod = ModData.LoadMod( StructuredMods, modFolder ); + if( mod == null ) { - if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - if( !newPath.Any() ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - BasePath = newDir; - Valid = true; - if( Config.ModDirectory != BasePath.FullName ) - { - Config.ModDirectory = BasePath.FullName; - Config.Save(); - } - - if( !Config.TempDirectory.Any() ) - { - if( CheckTmpDir( BasePath.FullName, out var newTmpDir ) ) - { - if( !firstTime ) - { - ClearOldTmpDir(); - } - - TempPath = newTmpDir; - TempWritable = true; - } - else - { - TempWritable = false; - } - } - - if( !firstTime ) - { - Collections.RecreateCaches(); - } - } - } - - private void SetTempDirectory( string newPath, bool firstTime ) - { - if( !Valid || !firstTime && string.Equals( newPath, Config.TempDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - if( !newPath.Any() && CheckTmpDir( BasePath.FullName, out var newTmpDir ) - || newPath.Any() && CheckTmpDir( newPath, out newTmpDir ) ) - { - if( !firstTime ) - { - ClearOldTmpDir(); - } - - TempPath = newTmpDir; - TempWritable = true; - var newName = newPath.Any() ? TempPath.Parent!.FullName : string.Empty; - if( Config.TempDirectory != newName ) - { - Config.TempDirectory = newName; - Config.Save(); - } - - if( !firstTime ) - { - Collections.RecreateCaches(); - } - } - else - { - TempWritable = false; - } - } - - public void SetTempDirectory( string newPath ) - => SetTempDirectory( newPath, false ); - - public ModManager() - { - SetBaseDirectory( Config.ModDirectory, true ); - SetTempDirectory( Config.TempDirectory, true ); - Collections = new CollectionManager( this ); - } - - private bool SetSortOrderPath( ModData mod, string path ) - { - mod.Move( path ); - var fixedPath = mod.SortOrder.FullPath; - if( !fixedPath.Any() || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - Config.ModSortOrder.Remove( mod.BasePath.Name ); - return true; - } - - if( path != fixedPath ) - { - Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; - return true; - } - return false; } - private void SetModStructure( bool removeOldPaths = false ) + if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { - var changes = false; - - foreach( var kvp in Config.ModSortOrder.ToArray() ) - { - if( kvp.Value.Any() && Mods.TryGetValue( kvp.Key, out var mod ) ) - { - changes |= SetSortOrderPath( mod, kvp.Value ); - } - else if( removeOldPaths ) - { - changes = true; - Config.ModSortOrder.Remove( kvp.Key ); - } - } - - if( changes ) + if( SetSortOrderPath( mod, sortOrder ) ) { Config.Save(); } } - public void DiscoverMods() + if( Mods.ContainsKey( modFolder.Name ) ) { - Mods.Clear(); - BasePath.Refresh(); - - StructuredMods.SubFolders.Clear(); - StructuredMods.Mods.Clear(); - if( Valid && BasePath.Exists ) - { - foreach( var modFolder in BasePath.EnumerateDirectories() ) - { - var mod = ModData.LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - continue; - } - - Mods.Add( modFolder.Name, mod ); - } - - SetModStructure(); - } - - Collections.RecreateCaches(); + return false; } - public void DeleteMod( DirectoryInfo modFolder ) + Mods.Add( modFolder.Name, mod ); + foreach( var collection in Collections.Collections.Values ) { - modFolder.Refresh(); - if( modFolder.Exists ) - { - try - { - Directory.Delete( modFolder.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); - } - - if( Mods.TryGetValue( modFolder.Name, out var mod ) ) - { - mod.SortOrder.ParentFolder.RemoveMod( mod ); - Mods.Remove( modFolder.Name ); - Collections.RemoveModFromCaches( modFolder ); - } - } + collection.AddMod( mod ); } - public bool AddMod( DirectoryInfo modFolder ) - { - var mod = ModData.LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - return false; - } + return true; + } + public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + { + var oldName = mod.Meta.Name; + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; + var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); + + if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) + { + return false; + } + + if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) + { + mod.ComputeChangedItems(); if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { - if( SetSortOrderPath( mod, sortOrder ) ) + mod.Move( sortOrder ); + var path = mod.SortOrder.FullPath; + if( path != sortOrder ) { + Config.ModSortOrder[ mod.BasePath.Name ] = path; Config.Save(); } } - - if( Mods.ContainsKey( modFolder.Name ) ) + else { - return false; + mod.SortOrder = new SortOrder( StructuredMods, mod.Meta.Name ); } - - Mods.Add( modFolder.Name, mod ); - foreach( var collection in Collections.Collections.Values ) - { - collection.AddMod( mod ); - } - - return true; } - public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); + + recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); + if( recomputeMeta ) { - var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; - var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); - - if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) - { - return false; - } - - if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) - { - mod.ComputeChangedItems(); - if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - mod.Move( sortOrder ); - var path = mod.SortOrder.FullPath; - if( path != sortOrder ) - { - Config.ModSortOrder[ mod.BasePath.Name ] = path; - Config.Save(); - } - } - else - { - mod.SortOrder = new SortOrder( StructuredMods, mod.Meta.Name ); - } - } - - var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); - - recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); - if( recomputeMeta ) - { - mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); - mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); - } - - Collections.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); - - return true; + mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); + mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); } - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - { - var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - return ret; - } + Collections.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); - // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) - // { - // #if DEBUG - // PluginLog.Verbose( "file changed: {FullPath}", e.FullPath ); - // #endif - // - // if( _plugin.ImportInProgress ) - // { - // return; - // } - // - // if( _plugin.Configuration.DisableFileSystemNotifications ) - // { - // return; - // } - // - // var file = e.FullPath; - // - // if( !ResolvedFiles.Any( x => x.Value.FullName == file ) ) - // { - // return; - // } - // - // PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file ); - // _plugin.GameUtils.ReloadPlayerResources(); - // } - // - // private void FileSystemPasta() - // { - // haha spaghet - // _fileSystemWatcher?.Dispose(); - // _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName ) - // { - // NotifyFilter = NotifyFilters.LastWrite | - // NotifyFilters.FileName | - // NotifyFilters.DirectoryName, - // IncludeSubdirectories = true, - // EnableRaisingEvents = true - // }; - // - // _fileSystemWatcher.Changed += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Created += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged; - // } + return true; } + + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) + { + var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + return ret; + } + + // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) + // { + // #if DEBUG + // PluginLog.Verbose( "file changed: {FullPath}", e.FullPath ); + // #endif + // + // if( _plugin.ImportInProgress ) + // { + // return; + // } + // + // if( _plugin.Configuration.DisableFileSystemNotifications ) + // { + // return; + // } + // + // var file = e.FullPath; + // + // if( !ResolvedFiles.Any( x => x.Value.FullName == file ) ) + // { + // return; + // } + // + // PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file ); + // _plugin.GameUtils.ReloadPlayerResources(); + // } + // + // private void FileSystemPasta() + // { + // haha spaghet + // _fileSystemWatcher?.Dispose(); + // _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName ) + // { + // NotifyFilter = NotifyFilters.LastWrite | + // NotifyFilters.FileName | + // NotifyFilters.DirectoryName, + // IncludeSubdirectories = true, + // EnableRaisingEvents = true + // }; + // + // _fileSystemWatcher.Changed += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Created += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged; + // _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged; + // } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index f6804d4f..6b8dde28 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -204,7 +204,7 @@ public static class ModManagerEditExtensions collection.Save(); if( collection.Cache != null && settings.Enabled ) { - collection.CalculateEffectiveFileList( manager.TempPath, mod.Resources.MetaManipulations.Count > 0, + collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, collection == manager.Collections.ActiveCollection ); } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d37a1531..666a349f 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -18,10 +18,10 @@ using System.Linq; using Penumbra.Meta.Manipulations; namespace Penumbra; -public class MetaDefaults -{ -} +public class MetaDefaults +{ } + public class Penumbra : IDalamudPlugin { public string Name @@ -41,8 +41,7 @@ public class Penumbra : IDalamudPlugin public static MetaDefaults MetaDefaults { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; - - public ResourceLoader ResourceLoader { get; } + public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } //public PathResolver PathResolver { get; } @@ -113,12 +112,20 @@ public class Penumbra : IDalamudPlugin }; ResourceLoader.EnableHooks(); - if (Config.EnableMods) + if( Config.EnableMods ) + { ResourceLoader.EnableReplacements(); - if (Config.DebugMode) + } + + if( Config.DebugMode ) + { ResourceLoader.EnableDebug(); - if (Config.EnableFullResourceLogging) + } + + if( Config.EnableFullResourceLogging ) + { ResourceLoader.EnableFullLogging(); + } unsafe { @@ -231,7 +238,7 @@ public class Penumbra : IDalamudPlugin //PathResolver.Dispose(); ResourceLogger.Dispose(); ResourceLoader.Dispose(); - + ShutdownWebServer(); } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 758a46ea..7d2e8ca9 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -20,6 +20,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; +using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; using Utf8String = Penumbra.GameData.ByteString.Utf8String; @@ -164,12 +165,6 @@ public partial class SettingsInterface PrintValue( "Mod Manager BasePath Exists", manager.BasePath != null ? Directory.Exists( manager.BasePath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Mod Manager Temp Path", manager.TempPath?.FullName ?? "NULL" ); - PrintValue( "Mod Manager Temp Path IsRooted", - ( !Penumbra.Config.TempDirectory.Any() || Path.IsPathRooted( Penumbra.Config.TempDirectory ) ).ToString() ); - PrintValue( "Mod Manager Temp Path Exists", - manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); - PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } @@ -273,43 +268,6 @@ public partial class SettingsInterface } } - private static void DrawDebugTabTempFiles() - { - if( !ImGui.CollapsingHeader( "Temporary Files##Debug" ) ) - { - return; - } - - if( !ImGui.BeginTable( "##tempFileTable", 4, ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - foreach( var collection in Penumbra.ModManager.Collections.Collections.Values.Where( c => c.Cache != null ) ) - { - var manip = collection.Cache!.MetaManipulations; - var files = ( Dictionary< GamePath, MetaManager.FileInformation >? )manip.GetType() - .GetField( "_currentFiles", BindingFlags.NonPublic | BindingFlags.Instance )?.GetValue( manip ) - ?? new Dictionary< GamePath, MetaManager.FileInformation >(); - - - foreach( var (file, info) in files ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( info.CurrentFile?.FullName ?? "None" ); - ImGui.TableNextColumn(); - ImGui.Text( file ); - ImGui.TableNextColumn(); - ImGui.Text( info.CurrentFile?.Exists ?? false ? "Exists" : "Missing" ); - ImGui.TableNextColumn(); - ImGui.Text( info.Changed ? "Data Changed" : "Unchanged" ); - } - } - } - private void DrawDebugTabIpc() { if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) @@ -369,7 +327,7 @@ public partial class SettingsInterface return; } - var cache = Penumbra.ModManager.Collections.CurrentCollection.Cache; + var cache = Penumbra.ModManager.Collections.CurrentCollection.Cache; if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) { return; @@ -397,9 +355,9 @@ public partial class SettingsInterface return; } - _penumbra.ResourceLoader.UpdateDebugInfo(); + Penumbra.ResourceLoader.UpdateDebugInfo(); - if( _penumbra.ResourceLoader.DebugList.Count == 0 + if( Penumbra.ResourceLoader.DebugList.Count == 0 || !ImGui.BeginTable( "##ReplacedResourcesDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) { return; @@ -407,7 +365,7 @@ public partial class SettingsInterface using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - foreach( var data in _penumbra.ResourceLoader.DebugList.Values.ToArray() ) + foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) { var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; @@ -426,6 +384,52 @@ public partial class SettingsInterface } } + public unsafe void DrawDebugCharacterUtility() + { + if( !ImGui.CollapsingHeader( "Character Utility##Debug" ) ) + { + return; + } + + if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + for( var i = 0; i < CharacterUtility.NumRelevantResources; ++i ) + { + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( resource->FileName(), resource->FileName() + resource->FileNameLength ); + ImGui.TableNextColumn(); + ImGui.Text( $"0x{resource->GetData().Data:X}" ); + if( ImGui.IsItemClicked() ) + { + var (data, length) = resource->GetData(); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + + ImGui.TableNextColumn(); + ImGui.Text( $"{resource->GetData().Length}" ); + ImGui.TableNextColumn(); + ImGui.Text( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, + Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + + ImGui.TableNextColumn(); + ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); + } + } + private unsafe void DrawPathResolverDebug() { if( !ImGui.CollapsingHeader( "Path Resolver##Debug" ) ) @@ -479,6 +483,14 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + if( !ImGui.BeginChild( "##DebugChild", -Vector2.One ) ) + { + ImGui.EndChild(); + return; + } + + raii.Push( ImGui.EndChild ); + DrawDebugTabGeneral(); ImGui.NewLine(); DrawDebugTabReplacedResources(); @@ -491,12 +503,12 @@ public partial class SettingsInterface ImGui.NewLine(); DrawPathResolverDebug(); ImGui.NewLine(); + DrawDebugCharacterUtility(); + ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); DrawDebugTabPlayers(); ImGui.NewLine(); - DrawDebugTabTempFiles(); - ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 0ff8a78c..ae9f35b9 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -192,8 +192,9 @@ public partial class SettingsInterface } else if( ( row -= activeResolved ) < activeMeta ) { - var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.ToString(), mod.Data.Meta.Name ); + // TODO + //var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( 0.ToString(), 0.ToString() ); } else if( ( row -= activeMeta ) < forcedResolved ) { @@ -202,9 +203,10 @@ public partial class SettingsInterface } else { + // TODO row -= forcedResolved; - var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.ToString(), mod.Data.Meta.Name ); + //var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( 0.ToString(), 0.ToString() ); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 7da647b1..d2000ba6 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -426,8 +426,7 @@ public partial class SettingsInterface foreach( var collection in modManager.Collections.Collections.Values .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) { - collection.CalculateEffectiveFileList( modManager.TempPath, false, - collection == modManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( false, collection == modManager.Collections.ActiveCollection ); } // If the mod is enabled in the current collection, its conflicts may have changed. diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index a48a1732..ffe45792 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -1,18 +1,12 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; -using Lumina.Data.Files; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.UI.Custom; -using Penumbra.Util; using ObjectType = Penumbra.GameData.Enums.ObjectType; namespace Penumbra.UI @@ -255,7 +249,6 @@ namespace Penumbra.UI private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( GmpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; //var id = list[ manipIdx ].GmpIdentifier; //var val = list[ manipIdx ].GmpValue; @@ -365,7 +358,6 @@ namespace Penumbra.UI private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( EqdpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; //var id = list[ manipIdx ].EqdpIdentifier; //var val = list[ manipIdx ].EqdpValue; @@ -402,7 +394,6 @@ namespace Penumbra.UI private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ushort )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; //var id = list[ manipIdx ].EstIdentifier; //var val = list[ manipIdx ].EstValue; @@ -434,7 +425,6 @@ namespace Penumbra.UI private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ImcFile.ImageChangeData )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; //var id = list[ manipIdx ].ImcIdentifier; //var val = list[ manipIdx ].ImcValue; @@ -493,7 +483,6 @@ namespace Penumbra.UI private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( float )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; //var id = list[ manipIdx ].RspIdentifier; //var val = list[ manipIdx ].RspValue; diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index e6bfdea5..d805e41b 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -529,8 +529,7 @@ public partial class SettingsInterface var collection = Penumbra.ModManager.Collections.CurrentCollection; if( collection.Cache != null ) { - collection.CalculateEffectiveFileList( Penumbra.ModManager.TempPath, metaManips, - collection == Penumbra.ModManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.ModManager.Collections.ActiveCollection ); } collection.Save(); diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 501653d4..f46ad018 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -82,7 +82,7 @@ public partial class SettingsInterface if( ImGui.IsItemClicked() ) { var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData(); - ImGui.SetClipboardText( ((IntPtr)( ( Interop.Structs.ResourceHandle* )r )->Data->VTable).ToString("X") + string.Join( " ", + ImGui.SetClipboardText( string.Join( " ", new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); } diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 8e1ba624..9d099eb0 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -24,8 +24,6 @@ public partial class SettingsInterface private readonly Configuration _config; private bool _configChanged; private string _newModDirectory; - private string _newTempDirectory; - public TabSettings( SettingsInterface ui ) { @@ -33,7 +31,6 @@ public partial class SettingsInterface _config = Penumbra.Config; _configChanged = false; _newModDirectory = _config.ModDirectory; - _newTempDirectory = _config.TempDirectory; } private static bool DrawPressEnterWarning( string old ) @@ -91,33 +88,6 @@ public partial class SettingsInterface } } - private void DrawTempFolder() - { - ImGui.BeginGroup(); - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - var save = ImGui.InputText( "Temp Directory", ref _newTempDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "This is where Penumbra will store temporary meta manipulation files.\n" - + "Leave this blank if you have no reason not to.\n" - + "A directory 'penumbrametatmp' will be created as a sub-directory to the specified directory.\n" - + "If none is specified (i.e. this is blank) this directory will be created in the root directory instead.\n" ); - ImGui.SameLine(); - var modManager = Penumbra.ModManager; - DrawOpenDirectoryButton( 1, modManager.TempPath, modManager.TempWritable ); - ImGui.EndGroup(); - - if( _newTempDirectory == _config.TempDirectory ) - { - return; - } - - if( save || DrawPressEnterWarning( _config.TempDirectory ) ) - { - modManager.SetTempDirectory( _newTempDirectory ); - _newTempDirectory = _config.TempDirectory; - } - } - private void DrawRediscoverButton() { if( ImGui.Button( "Rediscover Mods" ) ) @@ -326,11 +296,11 @@ public partial class SettingsInterface { if( tmp ) { - _base._penumbra.ResourceLoader.EnableFullLogging(); + Penumbra.ResourceLoader.EnableFullLogging(); } else { - _base._penumbra.ResourceLoader.DisableFullLogging(); + Penumbra.ResourceLoader.DisableFullLogging(); } _config.EnableFullResourceLogging = tmp; @@ -348,11 +318,11 @@ public partial class SettingsInterface { if( tmp ) { - _base._penumbra.ResourceLoader.EnableDebug(); + Penumbra.ResourceLoader.EnableDebug(); } else { - _base._penumbra.ResourceLoader.DisableDebug(); + Penumbra.ResourceLoader.DisableDebug(); } _config.DebugMode = tmp; @@ -388,7 +358,6 @@ public partial class SettingsInterface private void DrawAdvancedSettings() { - DrawTempFolder(); DrawRequestedResourceLogging(); DrawDisableSoundStreamingBox(); DrawLogLoadedFilesBox(); diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 7f0f74d6..897419f3 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -68,7 +68,7 @@ public partial class SettingsInterface : IDisposable var current = modManager.Collections.CurrentCollection; if( current.Cache != null ) { - current.CalculateEffectiveFileList( modManager.TempPath, recalculateMeta, + current.CalculateEffectiveFileList( recalculateMeta, current == modManager.Collections.ActiveCollection ); _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } From 707570615cbdc3e2ca203ea40c8aba62693e0377 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Mar 2022 16:10:54 +0100 Subject: [PATCH 0093/2451] tmp2 --- Penumbra/Meta/Files/ImcFile.cs | 25 ++- .../Meta/Manipulations/EqdpManipulation.cs | 27 ++- .../Meta/Manipulations/EqpManipulation.cs | 13 +- .../Meta/Manipulations/EstManipulation.cs | 39 +++- .../Meta/Manipulations/GmpManipulation.cs | 7 +- .../Meta/Manipulations/ImcManipulation.cs | 70 ++++++- .../Meta/Manipulations/MetaManipulation.cs | 197 ++++++++++++++++-- .../Meta/Manipulations/RspManipulation.cs | 20 +- Penumbra/Meta/MetaManager.cs | 2 +- .../TabInstalledDetailsManipulations.cs | 128 ++++++------ 10 files changed, 426 insertions(+), 102 deletions(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 1c23dd32..d7eaaa80 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -2,6 +2,7 @@ using System; using System.Numerics; using Dalamud.Logging; using Dalamud.Memory; +using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; @@ -18,10 +19,16 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > public readonly byte MaterialAnimationId; public ushort AttributeMask - => ( ushort )( _attributeAndSound & 0x3FF ); + { + get => ( ushort )( _attributeAndSound & 0x3FF ); + init => _attributeAndSound = ( ushort )( ( _attributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); + } public byte SoundId - => ( byte )( _attributeAndSound >> 10 ); + { + get => ( byte )( _attributeAndSound >> 10 ); + init => _attributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); + } public bool Equals( ImcEntry other ) => MaterialId == other.MaterialId @@ -35,6 +42,18 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > public override int GetHashCode() => HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId ); + + [JsonConstructor] + public ImcEntry( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId ) + { + MaterialId = materialId; + DecalId = decalId; + _attributeAndSound = 0; + VfxId = vfxId; + MaterialAnimationId = materialAnimationId; + AttributeMask = attributeMask; + SoundId = soundId; + } } public unsafe class ImcFile : MetaBaseFile @@ -118,7 +137,7 @@ public unsafe class ImcFile : MetaBaseFile } PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants ); - *( ushort* )Count = ( ushort )numVariants; + *( ushort* )Data = ( ushort )numVariants; return true; } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index c7fdd553..9abcd47b 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -1,4 +1,7 @@ using System; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -6,12 +9,16 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -public readonly struct EqdpManipulation : IEquatable< EqdpManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > { public readonly EqdpEntry Entry; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly Gender Gender; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly ModelRace Race; public readonly ushort SetId; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly EquipSlot Slot; public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) @@ -38,6 +45,24 @@ public readonly struct EqdpManipulation : IEquatable< EqdpManipulation > public override int GetHashCode() => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot ); + public int CompareTo( EqdpManipulation other ) + { + var r = Race.CompareTo( other.Race ); + if( r != 0 ) + { + return r; + } + + var g = Gender.CompareTo( other.Gender ); + if( g != 0 ) + { + return g; + } + + var set = SetId.CompareTo( other.SetId ); + return set != 0 ? set : Slot.CompareTo( other.Slot ); + } + public int FileIndex() => CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index eab5b864..430dbb9f 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -1,4 +1,7 @@ using System; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -6,10 +9,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -public readonly struct EqpManipulation : IEquatable< EqpManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > { public readonly EqpEntry Entry; public readonly ushort SetId; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly EquipSlot Slot; public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) @@ -32,6 +37,12 @@ public readonly struct EqpManipulation : IEquatable< EqpManipulation > public override int GetHashCode() => HashCode.Combine( ( int )Slot, SetId ); + public int CompareTo( EqpManipulation other ) + { + var set = SetId.CompareTo( other.SetId ); + return set != 0 ? set : Slot.CompareTo( other.Slot ); + } + public int FileIndex() => CharacterUtility.EqpIdx; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 985f9a65..613ef965 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -1,11 +1,15 @@ using System; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -public readonly struct EstManipulation : IEquatable< EstManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct EstManipulation : IMetaManipulation< EstManipulation > { public enum EstType : byte { @@ -16,10 +20,13 @@ public readonly struct EstManipulation : IEquatable< EstManipulation > } public readonly ushort SkeletonIdx; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly Gender Gender; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly ModelRace Race; public readonly ushort SetId; - public readonly EstType Type; + [JsonConverter( typeof( StringEnumConverter ) )] + public readonly EstType Slot; public EstManipulation( Gender gender, ModelRace race, EstType estType, ushort setId, ushort skeletonIdx ) { @@ -27,27 +34,45 @@ public readonly struct EstManipulation : IEquatable< EstManipulation > Gender = gender; Race = race; SetId = setId; - Type = estType; + Slot = estType; } public override string ToString() - => $"Est - {SetId} - {Type} - {Race.ToName()} {Gender.ToName()}"; + => $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}"; public bool Equals( EstManipulation other ) => Gender == other.Gender && Race == other.Race && SetId == other.SetId - && Type == other.Type; + && Slot == other.Slot; public override bool Equals( object? obj ) => obj is EstManipulation other && Equals( other ); public override int GetHashCode() - => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Type ); + => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot ); + + public int CompareTo( EstManipulation other ) + { + var r = Race.CompareTo( other.Race ); + if( r != 0 ) + { + return r; + } + + var g = Gender.CompareTo( other.Gender ); + if( g != 0 ) + { + return g; + } + + var s = Slot.CompareTo( other.Slot ); + return s != 0 ? s : SetId.CompareTo( other.SetId ); + } public int FileIndex() - => ( int )Type; + => ( int )Slot; public bool Apply( EstFile file ) { diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 2af52dfb..edae0b72 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -1,11 +1,13 @@ using System; +using System.Runtime.InteropServices; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -public readonly struct GmpManipulation : IEquatable< GmpManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > { public readonly GmpEntry Entry; public readonly ushort SetId; @@ -28,6 +30,9 @@ public readonly struct GmpManipulation : IEquatable< GmpManipulation > public override int GetHashCode() => SetId.GetHashCode(); + public int CompareTo( GmpManipulation other ) + => SetId.CompareTo( other.SetId ); + public int FileIndex() => CharacterUtility.GmpIdx; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 42a19a22..f00aaffc 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,21 +1,29 @@ using System; using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential )] -public readonly struct ImcManipulation : IEquatable< ImcManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { - public readonly ImcEntry Entry; - public readonly ushort PrimaryId; - public readonly ushort Variant; - public readonly ushort SecondaryId; + public readonly ImcEntry Entry; + public readonly ushort PrimaryId; + public readonly ushort Variant; + public readonly ushort SecondaryId; + + [JsonConverter( typeof( StringEnumConverter ) )] public readonly ObjectType ObjectType; - public readonly EquipSlot EquipSlot; - public readonly BodySlot BodySlot; + + [JsonConverter( typeof( StringEnumConverter ) )] + public readonly EquipSlot EquipSlot; + + [JsonConverter( typeof( StringEnumConverter ) )] + public readonly BodySlot BodySlot; public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) { @@ -40,6 +48,19 @@ public readonly struct ImcManipulation : IEquatable< ImcManipulation > EquipSlot = EquipSlot.Unknown; } + [JsonConstructor] + internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, + EquipSlot equipSlot, ImcEntry entry ) + { + Entry = entry; + ObjectType = objectType; + BodySlot = bodySlot; + PrimaryId = primaryId; + SecondaryId = secondaryId; + Variant = variant; + EquipSlot = equipSlot; + } + public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" @@ -59,6 +80,39 @@ public readonly struct ImcManipulation : IEquatable< ImcManipulation > public override int GetHashCode() => HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot ); + public int CompareTo( ImcManipulation other ) + { + var o = ObjectType.CompareTo( other.ObjectType ); + if( o != 0 ) + { + return o; + } + + var i = PrimaryId.CompareTo( other.PrimaryId ); + if( i != 0 ) + { + return i; + } + + if( ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + { + var e = EquipSlot.CompareTo( other.EquipSlot ); + return e != 0 ? e : Variant.CompareTo( other.Variant ); + } + + var s = SecondaryId.CompareTo( other.SecondaryId ); + if( s != 0 ) + { + return s; + } + + var b = BodySlot.CompareTo( other.BodySlot ); + return b != 0 ? b : Variant.CompareTo( other.Variant ); + } + + public int FileIndex() + => -1; + public Utf8GamePath GamePath() { return ObjectType switch diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index af07d17b..92caaa23 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,60 +1,225 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.Util; namespace Penumbra.Meta.Manipulations; +public interface IMetaManipulation +{ + public int FileIndex(); +} + +public interface IMetaManipulation< T > : IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct +{ } + +public struct ManipulationSet< T > where T : struct, IMetaManipulation< T > +{ + private List< T >? _data = null; + + public IReadOnlyList< T > Data + => ( IReadOnlyList< T >? )_data ?? Array.Empty< T >(); + + public int Count + => _data?.Count ?? 0; + + public ManipulationSet( int count = 0 ) + { + if( count > 0 ) + { + _data = new List< T >( count ); + } + } + + public bool TryAdd( T manip ) + { + if( _data == null ) + { + _data = new List< T > { manip }; + return true; + } + + var idx = _data.BinarySearch( manip ); + if( idx >= 0 ) + { + return false; + } + + _data.Insert( ~idx, manip ); + return true; + } + + public int Set( T manip ) + { + if( _data == null ) + { + _data = new List< T > { manip }; + return 0; + } + + var idx = _data.BinarySearch( manip ); + if( idx >= 0 ) + { + _data[ idx ] = manip; + return idx; + } + + idx = ~idx; + _data.Insert( idx, manip ); + return idx; + } + + public bool TryGet( T manip, out T value ) + { + var idx = _data?.BinarySearch( manip ) ?? -1; + if( idx < 0 ) + { + value = default; + return false; + } + + value = _data![ idx ]; + return true; + } + + public bool Remove( T manip ) + { + var idx = _data?.BinarySearch( manip ) ?? -1; + if( idx < 0 ) + { + return false; + } + + _data!.RemoveAt( idx ); + return true; + } +} + [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] -public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable +public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > { public enum Type : byte { - Eqp, - Gmp, - Eqdp, - Est, - Rsp, - Imc, + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, } [FieldOffset( 0 )] + [JsonIgnore] public readonly EqpManipulation Eqp = default; [FieldOffset( 0 )] + [JsonIgnore] public readonly GmpManipulation Gmp = default; [FieldOffset( 0 )] + [JsonIgnore] public readonly EqdpManipulation Eqdp = default; [FieldOffset( 0 )] + [JsonIgnore] public readonly EstManipulation Est = default; [FieldOffset( 0 )] + [JsonIgnore] public readonly RspManipulation Rsp = default; [FieldOffset( 0 )] + [JsonIgnore] public readonly ImcManipulation Imc = default; [FieldOffset( 15 )] + [JsonConverter( typeof( StringEnumConverter ) )] + [JsonProperty("Type")] public readonly Type ManipulationType; + public object? Manipulation + { + get => ManipulationType switch + { + Type.Unknown => null, + Type.Imc => Imc, + Type.Eqdp => Eqdp, + Type.Eqp => Eqp, + Type.Est => Est, + Type.Gmp => Gmp, + Type.Rsp => Rsp, + _ => null, + }; + init + { + switch( value ) + { + case EqpManipulation m: + Eqp = m; + ManipulationType = Type.Eqp; + return; + case EqdpManipulation m: + Eqdp = m; + ManipulationType = Type.Eqdp; + return; + case GmpManipulation m: + Gmp = m; + ManipulationType = Type.Gmp; + return; + case EstManipulation m: + Est = m; + ManipulationType = Type.Est; + return; + case RspManipulation m: + Rsp = m; + ManipulationType = Type.Rsp; + return; + case ImcManipulation m: + Imc = m; + ManipulationType = Type.Imc; + return; + } + } + } + public MetaManipulation( EqpManipulation eqp ) - => ( ManipulationType, Eqp ) = ( Type.Eqp, eqp ); + { + Eqp = eqp; + ManipulationType = Type.Eqp; + } public MetaManipulation( GmpManipulation gmp ) - => ( ManipulationType, Gmp ) = ( Type.Gmp, gmp ); + { + Gmp = gmp; + ManipulationType = Type.Gmp; + } public MetaManipulation( EqdpManipulation eqdp ) - => ( ManipulationType, Eqdp ) = ( Type.Eqdp, eqdp ); + { + Eqdp = eqdp; + ManipulationType = Type.Eqdp; + } public MetaManipulation( EstManipulation est ) - => ( ManipulationType, Est ) = ( Type.Est, est ); + { + Est = est; + ManipulationType = Type.Est; + } public MetaManipulation( RspManipulation rsp ) - => ( ManipulationType, Rsp ) = ( Type.Rsp, rsp ); + { + Rsp = rsp; + ManipulationType = Type.Rsp; + } public MetaManipulation( ImcManipulation imc ) - => ( ManipulationType, Imc ) = ( Type.Imc, imc ); + { + Imc = imc; + ManipulationType = Type.Imc; + } public static implicit operator MetaManipulation( EqpManipulation eqp ) => new(eqp); @@ -110,7 +275,9 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa public unsafe int CompareTo( MetaManipulation other ) { - fixed(MetaManipulation* lhs = &this) - return Functions.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); + fixed( MetaManipulation* lhs = &this ) + { + return Functions.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); + } } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 0ad017cd..d25b695f 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -1,14 +1,22 @@ using System; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -public readonly struct RspManipulation : IEquatable< RspManipulation > +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct RspManipulation : IMetaManipulation< RspManipulation > { - public readonly float Entry; - public readonly SubRace SubRace; + public readonly float Entry; + + [JsonConverter( typeof( StringEnumConverter ) )] + public readonly SubRace SubRace; + + [JsonConverter( typeof( StringEnumConverter ) )] public readonly RspAttribute Attribute; public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) @@ -31,6 +39,12 @@ public readonly struct RspManipulation : IEquatable< RspManipulation > public override int GetHashCode() => HashCode.Combine( ( int )SubRace, ( int )Attribute ); + public int CompareTo( RspManipulation other ) + { + var s = SubRace.CompareTo( other.SubRace ); + return s != 0 ? s : Attribute.CompareTo( other.Attribute ); + } + public int FileIndex() => CharacterUtility.HumanCmpIdx; diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index d9519863..f733b6ee 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -243,7 +243,7 @@ public class MetaManager2 : IDisposable return false; } - var file = m.Type switch + var file = m.Slot switch { EstManipulation.EstType.Hair => HairEstFile ??= new EstFile( EstManipulation.EstType.Hair ), EstManipulation.EstType.Face => FaceEstFile ??= new EstFile( EstManipulation.EstType.Face ), diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index ffe45792..cca21491 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Drawing.Text; +using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -484,39 +486,39 @@ namespace Penumbra.UI private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) { var ret = false; - //var id = list[ manipIdx ].RspIdentifier; - //var val = list[ manipIdx ].RspValue; - // - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // if( DefaultButton( - // $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) - // && _editMode ) - // { - // list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults ); - // ret = true; - // } - // - // ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - // if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", - // _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - // && val >= 0 - // && val <= 5 - // && _editMode ) - // { - // list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val ); - // ret = true; - // } - //} - // - //ImGui.Text( id.Attribute.ToUngenderedString() ); - //ImGui.TableNextColumn(); - //ImGui.TableNextColumn(); - //ImGui.TableNextColumn(); - //ImGui.Text( id.SubRace.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.Attribute.ToGender().ToString() ); + var id = list[ manipIdx ].RspIdentifier; + var val = list[ manipIdx ].RspValue; + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + if( DefaultButton( + $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) + && _editMode ) + { + list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults ); + ret = true; + } + + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", + _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) + && val >= 0 + && val <= 5 + && _editMode ) + { + list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val ); + ret = true; + } + } + + ImGui.Text( id.Attribute.ToUngenderedString() ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.Text( id.SubRace.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Attribute.ToGender().ToString() ); return ret; } @@ -543,35 +545,35 @@ namespace Penumbra.UI ImGui.TableNextColumn(); var changes = false; - //switch( type ) - //{ - // case MetaType.Eqp: - // changes = DrawEqpRow( manipIdx, list ); - // break; - // case MetaType.Gmp: - // changes = DrawGmpRow( manipIdx, list ); - // break; - // case MetaType.Eqdp: - // changes = DrawEqdpRow( manipIdx, list ); - // break; - // case MetaType.Est: - // changes = DrawEstRow( manipIdx, list ); - // break; - // case MetaType.Imc: - // changes = DrawImcRow( manipIdx, list ); - // break; - // case MetaType.Rsp: - // changes = DrawRspRow( manipIdx, list ); - // break; - //} - // - //ImGui.TableSetColumnIndex( 9 ); - //if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) ) - //{ - // ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - //} - // - //ImGui.TableNextRow(); + switch( type ) + { + case MetaManipulation.Type.Eqp: + changes = DrawEqpRow( manipIdx, list ); + break; + case MetaManipulation.Type.Gmp: + changes = DrawGmpRow( manipIdx, list ); + break; + case MetaManipulation.Type.Eqdp: + changes = DrawEqdpRow( manipIdx, list ); + break; + case MetaManipulation.Type.Est: + changes = DrawEstRow( manipIdx, list ); + break; + case MetaManipulation.Type.Imc: + changes = DrawImcRow( manipIdx, list ); + break; + case MetaManipulation.Type.Rsp: + changes = DrawRspRow( manipIdx, list ); + break; + } + + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{manipIdx}##{manipIdx}" ) ) + { + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); + } + + ImGui.TableNextRow(); return changes; } @@ -714,6 +716,8 @@ namespace Penumbra.UI { var numRows = _editMode ? 11 : 10; var changes = false; + + if( list.Count > 0 && ImGui.BeginTable( label, numRows, ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) From 6f527a1dbc28fdf04b73c43fd4e3f33a37586e85 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Mar 2022 15:23:00 +0100 Subject: [PATCH 0094/2451] Metamanipulations seemingly working. --- .../ByteString/Utf8String.Manipulation.cs | 2 +- Penumbra/Importer/TexToolsMeta.cs | 86 +- Penumbra/Interop/CharacterUtility.cs | 55 +- .../Interop/ResourceLoader.Replacement.cs | 15 +- Penumbra/Interop/Structs/CharacterUtility.cs | 82 +- Penumbra/Meta/Files/EqdpFile.cs | 28 +- Penumbra/Meta/Files/EqpGmpFile.cs | 40 +- Penumbra/Meta/Files/ImcFile.cs | 81 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 59 + Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 72 + Penumbra/Meta/Manager/MetaManager.Eqp.cs | 59 + Penumbra/Meta/Manager/MetaManager.Est.cs | 78 + Penumbra/Meta/Manager/MetaManager.Gmp.cs | 57 + Penumbra/Meta/Manager/MetaManager.Imc.cs | 148 ++ Penumbra/Meta/Manager/MetaManager.cs | 99 ++ .../Meta/Manipulations/ImcManipulation.cs | 4 + Penumbra/Meta/MetaCollection.cs | 6 + Penumbra/Meta/MetaManager.cs | 367 ----- Penumbra/Mods/CollectionManager.cs | 17 +- Penumbra/Mods/ModCollectionCache.cs | 9 +- Penumbra/Mods/ModManager.Directory.cs | 12 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Penumbra.csproj | 3 +- Penumbra/UI/MenuTabs/TabDebug.cs | 41 +- .../TabInstalledDetailsManipulations.cs | 1441 +++++++++-------- Penumbra/UI/MenuTabs/TabResourceManager.cs | 11 + 26 files changed, 1637 insertions(+), 1237 deletions(-) create mode 100644 Penumbra/Meta/Manager/MetaManager.Cmp.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.Eqdp.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.Eqp.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.Est.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.Gmp.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.Imc.cs create mode 100644 Penumbra/Meta/Manager/MetaManager.cs delete mode 100644 Penumbra/Meta/MetaManager.cs diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs index 08a38e91..0d895fa6 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs @@ -108,7 +108,7 @@ public sealed unsafe partial class Utf8String var start = 0; for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) { - if( start + 1 != idx || !removeEmpty ) + if( start != idx || !removeEmpty ) { ret.Add( Substring( start, idx - start ) ); } diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 8c9e418f..4b18fd07 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using Dalamud.Logging; using Lumina.Data.Files; @@ -10,6 +11,7 @@ using Penumbra.GameData.Util; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Util; +using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.Importer; @@ -254,22 +256,38 @@ public class TexToolsMeta using var reader = new BinaryReader( new MemoryStream( data ) ); var values = reader.ReadStructures< ImcEntry >( num ); ushort i = 0; - if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) + try { - // TODO check against default. - foreach( var value in values ) + if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) { - ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) ); - ++i; + var def = new ImcFile( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, new ImcEntry() ).GamePath() ); + var partIdx = ImcFile.PartIndex( info.EquipSlot ); + foreach( var value in values ) + { + if( !value.Equals( def.GetEntry( partIdx, i ) ) ) + { + ImcManipulations.Add( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, value ) ); + } + + ++i; + } + } + else + { + var def = new ImcFile( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, + new ImcEntry() ).GamePath() ); + foreach( var value in values.Where( v => true || !v.Equals( def.GetEntry( 0, i ) ) ) ) + { + ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, + value ) ); + ++i; + } } } - else + catch( Exception e ) { - foreach( var value in values ) - { - ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) ); - ++i; - } + PluginLog.Error( "Could not compute IMC manipulation. This is in all likelihood due to TexTools corrupting your index files.\n" + + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); } } @@ -296,7 +314,7 @@ public class TexToolsMeta var headerSize = reader.ReadUInt32(); var headerStart = reader.ReadUInt32(); reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); - + List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); for( var i = 0; i < numHeaders; ++i ) { @@ -307,7 +325,7 @@ public class TexToolsMeta entries.Add( ( type, offset, size ) ); reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); } - + byte[]? ReadEntry( MetaManipulation.Type type ) { var idx = entries.FindIndex( t => t.type == type ); @@ -315,11 +333,11 @@ public class TexToolsMeta { return null; } - + reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); return reader.ReadBytes( entries[ idx ].size ); } - + DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); @@ -373,32 +391,34 @@ public class TexToolsMeta void Add( RspAttribute attribute, float value ) { var def = CmpFile.GetDefault( subRace, attribute ); - if (value != def) - ret!.RspManipulations.Add(new RspManipulation(subRace, attribute, value)); + if( value != def ) + { + ret!.RspManipulations.Add( new RspManipulation( subRace, attribute, value ) ); + } } if( gender == 1 ) { - Add(RspAttribute.FemaleMinSize, br.ReadSingle() ); - Add(RspAttribute.FemaleMaxSize, br.ReadSingle() ); - Add(RspAttribute.FemaleMinTail, br.ReadSingle() ); - Add(RspAttribute.FemaleMaxTail, br.ReadSingle() ); - - Add(RspAttribute.BustMinX, br.ReadSingle() ); - Add(RspAttribute.BustMinY, br.ReadSingle() ); - Add(RspAttribute.BustMinZ, br.ReadSingle() ); - Add(RspAttribute.BustMaxX, br.ReadSingle() ); - Add(RspAttribute.BustMaxY, br.ReadSingle() ); - Add(RspAttribute.BustMaxZ, br.ReadSingle() ); + Add( RspAttribute.FemaleMinSize, br.ReadSingle() ); + Add( RspAttribute.FemaleMaxSize, br.ReadSingle() ); + Add( RspAttribute.FemaleMinTail, br.ReadSingle() ); + Add( RspAttribute.FemaleMaxTail, br.ReadSingle() ); + + Add( RspAttribute.BustMinX, br.ReadSingle() ); + Add( RspAttribute.BustMinY, br.ReadSingle() ); + Add( RspAttribute.BustMinZ, br.ReadSingle() ); + Add( RspAttribute.BustMaxX, br.ReadSingle() ); + Add( RspAttribute.BustMaxY, br.ReadSingle() ); + Add( RspAttribute.BustMaxZ, br.ReadSingle() ); } else { - Add(RspAttribute.MaleMinSize, br.ReadSingle() ); - Add(RspAttribute.MaleMaxSize, br.ReadSingle() ); - Add(RspAttribute.MaleMinTail, br.ReadSingle() ); - Add(RspAttribute.MaleMaxTail, br.ReadSingle() ); + Add( RspAttribute.MaleMinSize, br.ReadSingle() ); + Add( RspAttribute.MaleMaxSize, br.ReadSingle() ); + Add( RspAttribute.MaleMinTail, br.ReadSingle() ); + Add( RspAttribute.MaleMaxTail, br.ReadSingle() ); } - + return ret; } } \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 9d938f34..03f4bba0 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; @@ -20,7 +21,33 @@ public unsafe class CharacterUtility : IDisposable public Structs.CharacterUtility* Address => *_characterUtilityAddress; - public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumRelevantResources]; + // The relevant indices depend on which meta manipulations we allow for. + // The defines are set in the project configuration. + public static readonly int[] RelevantIndices + = Array.Empty< int >() +#if USE_EQP + .Append( Structs.CharacterUtility.EqpIdx ) +#endif +#if USE_GMP + .Append( Structs.CharacterUtility.GmpIdx ) +#endif +#if USE_EQDP + .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) ) +#endif +#if USE_CMP + .Append( Structs.CharacterUtility.HumanCmpIdx ) +#endif +#if USE_EST + .Concat( Enumerable.Range( Structs.CharacterUtility.FaceEstIdx, 4 ) ) +#endif + .ToArray(); + + private static readonly int[] ReverseIndices + = Enumerable.Range( 0, Structs.CharacterUtility.NumResources ) + .Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray(); + + + public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length]; public CharacterUtility() { @@ -48,9 +75,9 @@ public unsafe class CharacterUtility : IDisposable // We store the default data of the resources so we can always restore them. private void LoadDefaultResources() { - for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) + for( var i = 0; i < RelevantIndices.Length; ++i ) { - var resource = ( Structs.ResourceHandle* )Address->Resources[ i ]; + var resource = ( Structs.ResourceHandle* )Address->Resources[ RelevantIndices[ i ] ]; DefaultResources[ i ] = resource->GetData(); } } @@ -65,19 +92,25 @@ public unsafe class CharacterUtility : IDisposable } // Reset the data of one of the stored resources to its default values. - public void ResetResource( int idx ) + public void ResetResource( int fileIdx ) { - var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; - resource->SetData( DefaultResources[ idx ].Address, DefaultResources[ idx ].Size ); + var (data, size) = DefaultResources[ ReverseIndices[ fileIdx ] ]; + var resource = ( Structs.ResourceHandle* )Address->Resources[ fileIdx ]; + resource->SetData( data, size ); + } + + // Return all relevant resources to the default resource. + public void ResetAll() + { + foreach( var idx in RelevantIndices ) + { + ResetResource( idx ); + } } public void Dispose() { - for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) - { - ResetResource( i ); - } - + ResetAll(); LoadDataFilesHook.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/ResourceLoader.Replacement.cs index 9ecb85f7..837d2c19 100644 --- a/Penumbra/Interop/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/ResourceLoader.Replacement.cs @@ -117,12 +117,15 @@ public unsafe partial class ResourceLoader // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. if( !valid || !gamePath.IsRooted() ) { - ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); - } - else if( ResourceLoadCustomization != null && gamePath.Path[0] == (byte) '|' ) - { - ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); + if( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' ) + { + ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); + } + else + { + ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); + } } else { diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 62762243..cdd0c6cb 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; @@ -7,49 +8,52 @@ namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] public unsafe struct CharacterUtility { - public const int NumResources = 85; - public const int NumRelevantResources = 68; - public const int EqpIdx = 0; - public const int GmpIdx = 1; - public const int HumanCmpIdx = 63; - public const int FaceEstIdx = 64; - public const int HairEstIdx = 65; - public const int BodyEstIdx = 66; - public const int HeadEstIdx = 67; - public const int NumEqdpFiles = 2 * 28; + public static readonly int[] EqdpIndices + = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).ToArray(); + + public const int NumResources = 85; + public const int EqpIdx = 0; + public const int GmpIdx = 1; + public const int HumanCmpIdx = 63; + public const int FaceEstIdx = 64; + public const int HairEstIdx = 65; + public const int BodyEstIdx = 66; + public const int HeadEstIdx = 67; + public const int EqdpStartIdx = 2; + public const int NumEqdpFiles = 2 * 28; public static int EqdpIdx( GenderRace raceCode, bool accessory ) - => ( accessory ? 28 : 0 ) + => ( accessory ? NumEqdpFiles / 2 : 0 ) + ( int )raceCode switch { - 0101 => 2, - 0201 => 3, - 0301 => 4, - 0401 => 5, - 0501 => 6, - 0601 => 7, - 0701 => 8, - 0801 => 9, - 0901 => 10, - 1001 => 11, - 1101 => 12, - 1201 => 13, - 1301 => 14, - 1401 => 15, - 1501 => 16, - 1601 => 17, // Does not exist yet - 1701 => 18, - 1801 => 19, - 0104 => 20, - 0204 => 21, - 0504 => 22, - 0604 => 23, - 0704 => 24, - 0804 => 25, - 1304 => 26, - 1404 => 27, - 9104 => 28, - 9204 => 29, + 0101 => EqdpStartIdx, + 0201 => EqdpStartIdx + 1, + 0301 => EqdpStartIdx + 2, + 0401 => EqdpStartIdx + 3, + 0501 => EqdpStartIdx + 4, + 0601 => EqdpStartIdx + 5, + 0701 => EqdpStartIdx + 6, + 0801 => EqdpStartIdx + 7, + 0901 => EqdpStartIdx + 8, + 1001 => EqdpStartIdx + 9, + 1101 => EqdpStartIdx + 10, + 1201 => EqdpStartIdx + 11, + 1301 => EqdpStartIdx + 12, + 1401 => EqdpStartIdx + 13, + 1501 => EqdpStartIdx + 14, + 1601 => EqdpStartIdx + 15, // Does not exist yet + 1701 => EqdpStartIdx + 16, + 1801 => EqdpStartIdx + 17, + 0104 => EqdpStartIdx + 18, + 0204 => EqdpStartIdx + 19, + 0504 => EqdpStartIdx + 20, + 0604 => EqdpStartIdx + 21, + 0704 => EqdpStartIdx + 22, + 0804 => EqdpStartIdx + 23, + 1304 => EqdpStartIdx + 24, + 1404 => EqdpStartIdx + 25, + 9104 => EqdpStartIdx + 26, + 9204 => EqdpStartIdx + 27, _ => throw new ArgumentException(), }; diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index ae64ad1c..053d83f0 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -47,7 +47,8 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile throw new IndexOutOfRangeException(); } - return *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ); + var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 ); + return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) ); } set { @@ -56,7 +57,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile throw new IndexOutOfRangeException(); } - *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ) = value; + *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) = ( ushort )value; } } @@ -65,9 +66,12 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile var def = ( byte* )DefaultData.Data; Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); - var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); - var dataBasePtr = ( byte* )( controlPtr + BlockCount ); - var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); + var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); + var dataBasePtr = controlPtr + BlockCount; + var myDataPtrStart = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); + var myDataPtr = myDataPtrStart; + var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize ); + var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 ); for( var i = 0; i < BlockCount; ++i ) { if( controlPtr[ i ] == CollapsedBlock ) @@ -76,11 +80,16 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } else { + var y = new ReadOnlySpan< ushort >( dataBasePtr + controlPtr[ i ], BlockSize ); + var z = new ReadOnlySpan< ushort >( myDataPtr, BlockSize ); Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); } - myDataPtr += BlockSize; + myControlPtr[ i ] = ( ushort )( i * BlockSize ); + myDataPtr += BlockSize; } + + Functions.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); } public void Reset( IEnumerable< int > entries ) @@ -102,7 +111,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize; var fullLength = DataOffset + totalBlockCount * totalBlockSize; - fullLength += ( FileAlignment - ( Length & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); + fullLength += ( FileAlignment - ( fullLength & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); AllocateData( fullLength ); Reset(); } @@ -128,8 +137,9 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile return 0; } - var blockData = ( EqdpEntry* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block ); - return *( blockData + blockIdx % blockSize ); + var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 ); + var x = new ReadOnlySpan< ushort >( blockData, blockSize ); + return (EqdpEntry) (*( blockData + setIdx % blockSize )); } public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 5b6d6479..c85799a6 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.CompilerServices; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; @@ -24,17 +25,17 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public ulong ControlBlock => *( ulong* )Data; - protected T Get< T >( int idx ) where T : unmanaged + protected ulong GetInternal( int idx ) { return idx switch { >= Count => throw new IndexOutOfRangeException(), - <= 1 => *( ( T* )Data + 1 ), - _ => *( ( T* )Data + idx ), + <= 1 => *( ( ulong* )Data + 1 ), + _ => *( ( ulong* )Data + idx ), }; } - protected void Set< T >( int idx, T value ) where T : unmanaged + protected void SetInternal( int idx, ulong value ) { idx = idx switch { @@ -43,7 +44,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile _ => idx, }; - *( ( T* )Data + idx ) = value; + *( ( ulong* )Data + idx ) = value; } protected virtual void SetEmptyBlock( int idx ) @@ -53,21 +54,24 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public sealed override void Reset() { - var ptr = ( byte* )DefaultData.Data; - var controlBlock = *( ulong* )ptr; - *( ulong* )Data = ulong.MaxValue; - for( var i = 0; i < 64; ++i ) + var ptr = ( byte* )DefaultData.Data; + var controlBlock = *( ulong* )ptr; + var expandedBlocks = 0; + for( var i = 0; i < NumBlocks; ++i ) { var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; if( !collapsed ) { - Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + i * BlockSize * EntrySize, BlockSize * EntrySize ); + Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); + expandedBlocks++; } else { SetEmptyBlock( i ); } } + + *( ulong* )Data = ulong.MaxValue; } public ExpandedEqpGmpBase( bool gmp ) @@ -77,7 +81,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile Reset(); } - protected static T GetDefault< T >( int fileIdx, int setIdx, T def ) where T : unmanaged + protected static ulong GetDefaultInternal( int fileIdx, int setIdx, ulong def ) { var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; if( setIdx == 0 ) @@ -100,7 +104,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile var count = BitOperations.PopCount( control & ( blockBit - 1 ) ); var idx = setIdx % BlockSize; - var ptr = ( T* )data + BlockSize * count + idx; + var ptr = ( ulong* )data + BlockSize * count + idx; return *ptr; } } @@ -113,12 +117,12 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase public EqpEntry this[ int idx ] { - get => Get< EqpEntry >( idx ); - set => Set( idx, value ); + get => ( EqpEntry )GetInternal( idx ); + set => SetInternal( idx, ( ulong )value ); } public static EqpEntry GetDefault( int setIdx ) - => GetDefault( CharacterUtility.EqpIdx, setIdx, Eqp.DefaultEntry ); + => ( EqpEntry )GetDefaultInternal( CharacterUtility.EqpIdx, setIdx, ( ulong )Eqp.DefaultEntry ); protected override unsafe void SetEmptyBlock( int idx ) { @@ -147,12 +151,12 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase public GmpEntry this[ int idx ] { - get => Get< GmpEntry >( idx ); - set => Set( idx, value ); + get => ( GmpEntry )GetInternal( idx ); + set => SetInternal( idx, ( ulong )value ); } public static GmpEntry GetDefault( int setIdx ) - => GetDefault( CharacterUtility.GmpIdx, setIdx, GmpEntry.Default ); + => ( GmpEntry )GetDefaultInternal( CharacterUtility.GmpIdx, setIdx, ( ulong )GmpEntry.Default ); public void Reset( IEnumerable< int > entries ) { diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index d7eaaa80..35e53d66 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Numerics; using Dalamud.Logging; using Dalamud.Memory; @@ -7,6 +8,7 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -64,41 +66,57 @@ public unsafe class ImcFile : MetaBaseFile => NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize; public int Count - => *( ushort* )Data; - - public ushort PartMask - => *( ushort* )( Data + 2 ); + => CountInternal( Data ); public readonly int NumParts; public readonly Utf8GamePath Path; - public ImcEntry* DefaultPartPtr( int partIdx ) + private static int CountInternal( byte* data ) + => *( ushort* )data; + + private static ushort PartMask( byte* data ) + => *( ushort* )( data + 2 ); + + private static ImcEntry* DefaultPartPtr( byte* data, int partIdx ) { var flag = 1 << partIdx; - if( ( PartMask & flag ) == 0 ) + if( ( PartMask( data ) & flag ) == 0 ) { return null; } - return ( ImcEntry* )( Data + PreambleSize ) + partIdx; + return ( ImcEntry* )( data + PreambleSize ) + partIdx; } - public ImcEntry* VariantPtr( int partIdx, int variantIdx ) + private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) { + if( variantIdx == 0 ) + { + return DefaultPartPtr( data, partIdx ); + } + + --variantIdx; var flag = 1 << partIdx; - if( ( PartMask & flag ) == 0 || variantIdx >= Count ) + + if( ( PartMask( data ) & flag ) == 0 || variantIdx >= CountInternal( data ) ) { return null; } - var numParts = NumParts; - var ptr = ( ImcEntry* )( Data + PreambleSize ); + var numParts = BitOperations.PopCount( PartMask( data ) ); + var ptr = ( ImcEntry* )( data + PreambleSize ); ptr += numParts; ptr += variantIdx * numParts; ptr += partIdx; return ptr; } + public ImcEntry GetEntry( int partIdx, int variantIdx ) + { + var ptr = VariantPtr( Data, partIdx, variantIdx ); + return ptr == null ? new ImcEntry() : *ptr; + } + public static int PartIndex( EquipSlot slot ) => slot switch { @@ -122,7 +140,6 @@ public unsafe class ImcFile : MetaBaseFile return true; } - var numParts = NumParts; if( ActualLength > Length ) { PluginLog.Warning( "Adding too many variants to IMC, size exceeded." ); @@ -130,10 +147,10 @@ public unsafe class ImcFile : MetaBaseFile } var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); - var endPtr = defaultPtr + ( numVariants + 1 ) * numParts; - for( var ptr = defaultPtr + numParts; ptr < endPtr; ptr += numParts ) + var endPtr = defaultPtr + ( numVariants + 1 ) * NumParts; + for( var ptr = defaultPtr + NumParts; ptr < endPtr; ptr += NumParts ) { - Functions.MemCpyUnchecked( ptr, defaultPtr, numParts * sizeof( ImcEntry ) ); + Functions.MemCpyUnchecked( ptr, defaultPtr, NumParts * sizeof( ImcEntry ) ); } PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants ); @@ -143,15 +160,14 @@ public unsafe class ImcFile : MetaBaseFile public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry ) { - var numParts = NumParts; - if( partIdx >= numParts ) + if( partIdx >= NumParts ) { return false; } - EnsureVariantCount( variantIdx + 1 ); + EnsureVariantCount( variantIdx ); - var variantPtr = VariantPtr( partIdx, variantIdx ); + var variantPtr = VariantPtr( Data, partIdx, variantIdx ); if( variantPtr == null ) { PluginLog.Error( "Error during expansion of imc file." ); @@ -168,9 +184,20 @@ public unsafe class ImcFile : MetaBaseFile } + public override void Reset() + { + var file = Dalamud.GameData.GetFile( Path.ToString() ); + fixed( byte* ptr = file!.Data ) + { + Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); + Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); + } + } + public ImcFile( Utf8GamePath path ) : base( 0 ) { + Path = path; var file = Dalamud.GameData.GetFile( path.ToString() ); if( file == null ) { @@ -182,6 +209,22 @@ public unsafe class ImcFile : MetaBaseFile NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts ); Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); + Functions.MemSet( Data + file.Data.Length, 0, sizeof( ImcEntry ) * 100 * NumParts ); + } + } + + public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx ) + { + var file = Dalamud.GameData.GetFile( path.ToString() ); + if( file == null ) + { + throw new Exception(); + } + + fixed( byte* ptr = file.Data ) + { + var entry = VariantPtr( ptr, PartIndex( slot ), variantIdx ); + return entry == null ? new ImcEntry() : *entry; } } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs new file mode 100644 index 00000000..d073631a --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public struct MetaManagerCmp : IDisposable + { + public CmpFile? File = null; + public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new(); + + public MetaManagerCmp() + { } + + [Conditional( "USE_CMP" )] + public void Reset() + { + if( File == null ) + { + return; + } + + File.Reset( Manipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) ); + Manipulations.Clear(); + } + + [Conditional( "USE_CMP" )] + public void SetFiles() + => SetFile( File, CharacterUtility.HumanCmpIdx ); + + public bool ApplyMod( RspManipulation m, Mod.Mod mod ) + { +#if USE_CMP + if( !Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + File ??= new CmpFile(); + return m.Apply( File ); +#else + return false; +#endif + } + + public void Dispose() + { + File?.Dispose(); + File = null; + Manipulations.Clear(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs new file mode 100644 index 00000000..608c77d4 --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public struct MetaManagerEqdp : IDisposable + { + public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles]; + + public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new(); + + public MetaManagerEqdp() + { } + + [Conditional( "USE_EQDP" )] + public void SetFiles() + { + foreach( var idx in CharacterUtility.EqdpIndices ) + { + SetFile( Files[ idx - CharacterUtility.EqdpStartIdx ], idx ); + } + } + + [Conditional( "USE_EQDP" )] + public void Reset() + { + foreach( var file in Files ) + { + file?.Reset( Manipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); + } + + Manipulations.Clear(); + } + + public bool ApplyMod( EqdpManipulation m, Mod.Mod mod ) + { +#if USE_EQDP + if( !Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + var file = Files[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); + return m.Apply( file ); +#else + return false; +#endif + } + + public ExpandedEqdpFile? File( GenderRace race, bool accessory ) + => Files[ CharacterUtility.EqdpIdx( race, accessory ) - CharacterUtility.EqdpStartIdx ]; + + public void Dispose() + { + for( var i = 0; i < Files.Length; ++i ) + { + Files[ i ]?.Dispose(); + Files[ i ] = null; + } + + Manipulations.Clear(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs new file mode 100644 index 00000000..65c38ef5 --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public struct MetaManagerEqp : IDisposable + { + public ExpandedEqpFile? File = null; + public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new(); + + public MetaManagerEqp() + { } + + [Conditional( "USE_EQP" )] + public void SetFiles() + => SetFile( File, CharacterUtility.EqpIdx ); + + [Conditional( "USE_EQP" )] + public void Reset() + { + if( File == null ) + { + return; + } + + File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) ); + Manipulations.Clear(); + } + + public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) + { +#if USE_EQP + if( !Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + File ??= new ExpandedEqpFile(); + return m.Apply( File ); +#else + return false; +#endif + } + + public void Dispose() + { + File?.Dispose(); + File = null; + Manipulations.Clear(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs new file mode 100644 index 00000000..558873cc --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public struct MetaManagerEst : IDisposable + { + public EstFile? FaceFile = null; + public EstFile? HairFile = null; + public EstFile? BodyFile = null; + public EstFile? HeadFile = null; + + public readonly Dictionary< EstManipulation, Mod.Mod > Manipulations = new(); + + public MetaManagerEst() + { } + + [Conditional( "USE_EST" )] + public void SetFiles() + { + SetFile( FaceFile, CharacterUtility.FaceEstIdx ); + SetFile( HairFile, CharacterUtility.HairEstIdx ); + SetFile( BodyFile, CharacterUtility.BodyEstIdx ); + SetFile( HeadFile, CharacterUtility.HeadEstIdx ); + } + + [Conditional( "USE_EST" )] + public void Reset() + { + FaceFile?.Reset(); + HairFile?.Reset(); + BodyFile?.Reset(); + HeadFile?.Reset(); + Manipulations.Clear(); + } + + public bool ApplyMod( EstManipulation m, Mod.Mod mod ) + { +#if USE_EST + if( !Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + var file = m.Slot switch + { + EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), + EstManipulation.EstType.Face => FaceFile ??= new EstFile( EstManipulation.EstType.Face ), + EstManipulation.EstType.Body => BodyFile ??= new EstFile( EstManipulation.EstType.Body ), + EstManipulation.EstType.Head => HeadFile ??= new EstFile( EstManipulation.EstType.Head ), + _ => throw new ArgumentOutOfRangeException(), + }; + return m.Apply( file ); +#else + return false; +#endif + } + + public void Dispose() + { + FaceFile?.Dispose(); + HairFile?.Dispose(); + BodyFile?.Dispose(); + HeadFile?.Dispose(); + FaceFile = null; + HairFile = null; + BodyFile = null; + HeadFile = null; + Manipulations.Clear(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs new file mode 100644 index 00000000..8daf8dd8 --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public struct MetaManagerGmp : IDisposable + { + public ExpandedGmpFile? File = null; + public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new(); + + public MetaManagerGmp() + { } + + [Conditional( "USE_GMP" )] + public void Reset() + { + if( File != null ) + { + File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) ); + Manipulations.Clear(); + } + } + + [Conditional( "USE_GMP" )] + public void SetFiles() + => SetFile( File, CharacterUtility.GmpIdx ); + + public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) + { +#if USE_GMP + if( Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + File ??= new ExpandedGmpFile(); + return m.Apply( File ); +#else + return false; +#endif + } + + public void Dispose() + { + File?.Dispose(); + File = null; + Manipulations.Clear(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs new file mode 100644 index 00000000..e2457668 --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.ByteString; +using Penumbra.Interop; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager +{ + public readonly struct MetaManagerImc : IDisposable + { + public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); + public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); + + private readonly ModCollection _collection; + private readonly ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate; + + + public MetaManagerImc( ModCollection collection ) + { + _collection = collection; + _previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization; + } + + [Conditional( "USE_IMC" )] + public void SetFiles() + { + if( _collection.Cache == null ) + { + return; + } + + foreach( var path in Files.Keys ) + { + _collection.Cache.ResolvedFiles[ path ] = CreateImcPath( path ); + } + } + + [Conditional( "USE_IMC" )] + public void Reset() + { + foreach( var (path, file) in Files ) + { + _collection.Cache?.ResolvedFiles.Remove( path ); + file.Reset(); + } + + Manipulations.Clear(); + } + + public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + { + const uint imcExt = 0x00696D63; +#if USE_IMC + if( !Manipulations.TryAdd( m, mod ) ) + { + return false; + } + + var path = m.GamePath(); + if( !Files.TryGetValue( path, out var file ) ) + { + file = new ImcFile( path ); + } + + if( !m.Apply( file ) ) + { + return false; + } + + Files[ path ] = file; + var fullPath = CreateImcPath( path ); + if( _collection.Cache != null ) + { + _collection.Cache.ResolvedFiles[ path ] = fullPath; + } + + var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 ); + if( resource != null ) + { + file.Replace( ( ResourceHandle* )resource ); + } + + return true; +#else + return false; +#endif + } + + + public void Dispose() + { + foreach( var file in Files.Values ) + { + file.Dispose(); + } + + Files.Clear(); + Manipulations.Clear(); + RestoreDelegate(); + } + + [Conditional( "USE_IMC" )] + private unsafe void SetupDelegate() + { + Penumbra.ResourceLoader.ResourceLoadCustomization = ImcHandler; + } + + [Conditional( "USE_IMC" )] + private unsafe void RestoreDelegate() + { + if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcHandler ) + { + Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; + } + } + + private FullPath CreateImcPath( Utf8GamePath path ) + => new($"|{_collection.Name}|{path}"); + + private static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + var split = gamePath.Path.Split( ( byte )'|', 2, true ); + fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; + + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) + && collection.Cache != null + && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( + Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + { + file.Replace( fileDescriptor->ResourceHandle ); + } + + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + return ret; + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs new file mode 100644 index 00000000..3eef46fd --- /dev/null +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -0,0 +1,99 @@ +using System; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.Meta.Manager; + +public partial class MetaManager : IDisposable +{ + public MetaManagerEqp Eqp = new(); + public MetaManagerEqdp Eqdp = new(); + public MetaManagerGmp Gmp = new(); + public MetaManagerEst Est = new(); + public MetaManagerCmp Cmp = new(); + public MetaManagerImc Imc; + + private static unsafe void SetFile( MetaBaseFile? file, int index ) + { + if( file == null ) + { + Penumbra.CharacterUtility.ResetResource( index ); + } + else + { + Penumbra.CharacterUtility.SetResource( index, ( IntPtr )file.Data, file.Length ); + } + } + + public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod ) + { + mod = manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, + MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, + MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, + MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, + MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, + MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, + _ => throw new ArgumentOutOfRangeException(), + }; + return mod != null; + } + + public int Count + => Imc.Manipulations.Count + + Eqdp.Manipulations.Count + + Cmp.Manipulations.Count + + Gmp.Manipulations.Count + + Est.Manipulations.Count + + Eqp.Manipulations.Count; + + public MetaManager( ModCollection collection ) + => Imc = new MetaManagerImc( collection ); + + public void SetFiles() + { + Eqp.SetFiles(); + Eqdp.SetFiles(); + Gmp.SetFiles(); + Est.SetFiles(); + Cmp.SetFiles(); + Imc.SetFiles(); + } + + public void Reset() + { + Eqp.Reset(); + Eqdp.Reset(); + Gmp.Reset(); + Est.Reset(); + Cmp.Reset(); + Imc.Reset(); + } + + public void Dispose() + { + Eqp.Dispose(); + Eqdp.Dispose(); + Gmp.Dispose(); + Est.Dispose(); + Cmp.Dispose(); + Imc.Dispose(); + } + + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), + MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), + MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), + MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), + MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), + MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index f00aaffc..5ac63e99 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -61,6 +61,10 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > EquipSlot = equipSlot; } + public ImcManipulation( ImcManipulation copy, ImcEntry entry ) + : this( copy.ObjectType, copy.BodySlot, copy.PrimaryId, copy.SecondaryId, copy.Variant, copy.EquipSlot, entry ) + {} + public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 2c09aba3..8e0337e6 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -219,6 +219,12 @@ public class MetaCollection if( collection != null ) { + if( collection.DefaultData.Concat( collection.GroupData.Values.SelectMany( kvp => kvp.Values.SelectMany( l => l ) ) ) + .Any( m => m.ManipulationType == MetaManipulation.Type.Unknown || !Enum.IsDefined( m.ManipulationType ) ) ) + { + throw new Exception( "Invalid collection" ); + } + collection.Count = collection.DefaultData.Count + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); } diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs deleted file mode 100644 index f733b6ee..00000000 --- a/Penumbra/Meta/MetaManager.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; -using Penumbra.Interop; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility; -using ImcFile = Penumbra.Meta.Files.ImcFile; - -namespace Penumbra.Meta; - -public class MetaManager2 : IDisposable -{ - public readonly List< MetaBaseFile > ChangedData = new(7 + CharacterUtility.NumEqdpFiles); - - public ExpandedEqpFile? EqpFile; - public ExpandedGmpFile? GmpFile; - public ExpandedEqdpFile?[] EqdpFile = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles]; - public EstFile? FaceEstFile; - public EstFile? HairEstFile; - public EstFile? BodyEstFile; - public EstFile? HeadEstFile; - public CmpFile? CmpFile; - - public readonly Dictionary< EqpManipulation, Mod.Mod > EqpManipulations = new(); - public readonly Dictionary< EstManipulation, Mod.Mod > EstManipulations = new(); - public readonly Dictionary< GmpManipulation, Mod.Mod > GmpManipulations = new(); - public readonly Dictionary< RspManipulation, Mod.Mod > RspManipulations = new(); - public readonly Dictionary< EqdpManipulation, Mod.Mod > EqdpManipulations = new(); - - public readonly Dictionary< ImcManipulation, Mod.Mod > ImcManipulations = new(); - public readonly Dictionary< Utf8GamePath, ImcFile > ImcFiles = new(); - - private readonly ModCollection _collection; - - public unsafe void SetFiles() - { - foreach( var file in ChangedData ) - { - Penumbra.CharacterUtility.SetResource( file.Index, ( IntPtr )file.Data, file.Length ); - } - } - - public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod ) - { - mod = manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => EqpManipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, - MetaManipulation.Type.Gmp => GmpManipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, - MetaManipulation.Type.Eqdp => EqdpManipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, - MetaManipulation.Type.Est => EstManipulations.TryGetValue( manip.Est, out var m ) ? m : null, - MetaManipulation.Type.Rsp => RspManipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, - MetaManipulation.Type.Imc => ImcManipulations.TryGetValue( manip.Imc, out var m ) ? m : null, - _ => throw new ArgumentOutOfRangeException(), - }; - return mod != null; - } - - public int Count - => ImcManipulations.Count - + EqdpManipulations.Count - + RspManipulations.Count - + GmpManipulations.Count - + EstManipulations.Count - + EqpManipulations.Count; - - public MetaManager2( ModCollection collection ) - => _collection = collection; - - public void ApplyImcFiles( Dictionary< Utf8GamePath, FullPath > resolvedFiles ) - { - foreach( var path in ImcFiles.Keys ) - { - resolvedFiles[ path ] = CreateImcPath( path ); - } - } - - public void ResetEqp() - { - if( EqpFile != null ) - { - EqpFile.Reset( EqpManipulations.Keys.Select( m => ( int )m.SetId ) ); - EqpManipulations.Clear(); - ChangedData.Remove( EqpFile ); - } - } - - public void ResetGmp() - { - if( GmpFile != null ) - { - GmpFile.Reset( GmpManipulations.Keys.Select( m => ( int )m.SetId ) ); - GmpManipulations.Clear(); - ChangedData.Remove( GmpFile ); - } - } - - public void ResetCmp() - { - if( CmpFile != null ) - { - CmpFile.Reset( RspManipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) ); - RspManipulations.Clear(); - ChangedData.Remove( CmpFile ); - } - } - - public void ResetEst() - { - FaceEstFile?.Reset(); - HairEstFile?.Reset(); - BodyEstFile?.Reset(); - HeadEstFile?.Reset(); - RspManipulations.Clear(); - ChangedData.RemoveAll( f => f is EstFile ); - } - - public void ResetEqdp() - { - foreach( var file in EqdpFile ) - { - file?.Reset( EqdpManipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); - } - - ChangedData.RemoveAll( f => f is ExpandedEqdpFile ); - EqdpManipulations.Clear(); - } - - private FullPath CreateImcPath( Utf8GamePath path ) - { - var d = new DirectoryInfo( $":{_collection.Name}/" ); - return new FullPath( d, new Utf8RelPath( path ) ); - } - - public void ResetImc() - { - foreach( var (path, file) in ImcFiles ) - { - _collection.Cache?.ResolvedFiles.Remove( path ); - path.Dispose(); - file.Dispose(); - } - - ImcFiles.Clear(); - ImcManipulations.Clear(); - } - - public void Reset() - { - ChangedData.Clear(); - ResetEqp(); - ResetGmp(); - ResetCmp(); - ResetEst(); - ResetEqdp(); - ResetImc(); - } - - private static void Dispose< T >( ref T? file ) where T : class, IDisposable - { - if( file != null ) - { - file.Dispose(); - file = null; - } - } - - public void Dispose() - { - ChangedData.Clear(); - EqpManipulations.Clear(); - EstManipulations.Clear(); - GmpManipulations.Clear(); - RspManipulations.Clear(); - EqdpManipulations.Clear(); - Dispose( ref EqpFile ); - Dispose( ref GmpFile ); - Dispose( ref FaceEstFile ); - Dispose( ref HairEstFile ); - Dispose( ref BodyEstFile ); - Dispose( ref HeadEstFile ); - Dispose( ref CmpFile ); - for( var i = 0; i < CharacterUtility.NumEqdpFiles; ++i ) - { - Dispose( ref EqdpFile[ i ] ); - } - - ResetImc(); - } - - private void AddFile( MetaBaseFile file ) - { - if( !ChangedData.Contains( file ) ) - { - ChangedData.Add( file ); - } - } - - public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) - { - if( !EqpManipulations.TryAdd( m, mod ) ) - { - return false; - } - - EqpFile ??= new ExpandedEqpFile(); - if( !m.Apply( EqpFile ) ) - { - return false; - } - - AddFile( EqpFile ); - return true; - } - - public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) - { - if( !GmpManipulations.TryAdd( m, mod ) ) - { - return false; - } - - GmpFile ??= new ExpandedGmpFile(); - if( !m.Apply( GmpFile ) ) - { - return false; - } - - AddFile( GmpFile ); - return true; - } - - public bool ApplyMod( EstManipulation m, Mod.Mod mod ) - { - if( !EstManipulations.TryAdd( m, mod ) ) - { - return false; - } - - var file = m.Slot switch - { - EstManipulation.EstType.Hair => HairEstFile ??= new EstFile( EstManipulation.EstType.Hair ), - EstManipulation.EstType.Face => FaceEstFile ??= new EstFile( EstManipulation.EstType.Face ), - EstManipulation.EstType.Body => BodyEstFile ??= new EstFile( EstManipulation.EstType.Body ), - EstManipulation.EstType.Head => HeadEstFile ??= new EstFile( EstManipulation.EstType.Head ), - _ => throw new ArgumentOutOfRangeException(), - }; - if( !m.Apply( file ) ) - { - return false; - } - - AddFile( file ); - return true; - } - - public bool ApplyMod( RspManipulation m, Mod.Mod mod ) - { - if( !RspManipulations.TryAdd( m, mod ) ) - { - return false; - } - - CmpFile ??= new CmpFile(); - if( !m.Apply( CmpFile ) ) - { - return false; - } - - AddFile( CmpFile ); - return true; - } - - public bool ApplyMod( EqdpManipulation m, Mod.Mod mod ) - { - if( !EqdpManipulations.TryAdd( m, mod ) ) - { - return false; - } - - var file = EqdpFile[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); - if( !m.Apply( file ) ) - { - return false; - } - - AddFile( file ); - return true; - } - - public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) - { - const uint imcExt = 0x00696D63; - - if( !ImcManipulations.TryAdd( m, mod ) ) - { - return false; - } - - var path = m.GamePath(); - if( !ImcFiles.TryGetValue( path, out var file ) ) - { - file = new ImcFile( path ); - } - - if( !m.Apply( file ) ) - { - return false; - } - - ImcFiles[ path ] = file; - var fullPath = CreateImcPath( path ); - if( _collection.Cache != null ) - { - _collection.Cache.ResolvedFiles[ path ] = fullPath; - } - - var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 ); - if( resource != null ) - { - file.Replace( ( ResourceHandle* )resource ); - } - - return true; - } - - public static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) - { - var split = gamePath.Path.Split( ( byte )'|', 2, true ); - fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; - - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) - && collection.Cache != null - && collection.Cache.MetaManipulations.ImcFiles.TryGetValue( - Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) - { - file.Replace( fileDescriptor->ResourceHandle ); - } - - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; - return ret; - } - - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) - { - return m.ManipulationType switch - { - MetaManipulation.Type.Eqp => ApplyMod( m.Eqp, mod ), - MetaManipulation.Type.Gmp => ApplyMod( m.Gmp, mod ), - MetaManipulation.Type.Eqdp => ApplyMod( m.Eqdp, mod ), - MetaManipulation.Type.Est => ApplyMod( m.Est, mod ), - MetaManipulation.Type.Rsp => ApplyMod( m.Rsp, mod ), - MetaManipulation.Type.Imc => ApplyMod( m.Imc, mod ), - _ => throw new ArgumentOutOfRangeException(), - }; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 82874d87..bf52a347 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.Interop.Structs; using Penumbra.Mod; using Penumbra.Util; @@ -43,6 +44,14 @@ public class CollectionManager { ActiveCollection = newActive; Penumbra.ResidentResources.Reload(); + if( ActiveCollection.Cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + ActiveCollection.Cache.MetaManipulations.SetFiles(); + } } else { @@ -206,10 +215,9 @@ public class CollectionManager public void SetDefaultCollection( ModCollection newCollection ) => SetCollection( newCollection, DefaultCollection, c => { - if( !CollectionChangedTo.Any() ) + if( CollectionChangedTo.Length == 0 ) { - ActiveCollection = c; - Penumbra.ResidentResources.Reload(); + SetActiveCollection( c, string.Empty ); } DefaultCollection = c; @@ -228,8 +236,7 @@ public class CollectionManager { if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) { - ActiveCollection = c; - Penumbra.ResidentResources.Reload(); + SetActiveCollection( c, CollectionChangedTo ); } CharacterCollection[ characterName ] = c; diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 20a7b583..6af67ad3 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -7,8 +7,7 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.Meta; +using Penumbra.Meta.Manager; using Penumbra.Mod; using Penumbra.Util; @@ -27,7 +26,7 @@ public class ModCollectionCache private readonly SortedList< string, object? > _changedItems = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new(); - public readonly MetaManager2 MetaManipulations; + public readonly MetaManager MetaManipulations; public IReadOnlyDictionary< string, object? > ChangedItems { @@ -39,7 +38,7 @@ public class ModCollectionCache } public ModCollectionCache( ModCollection collection ) - => MetaManipulations = new MetaManager2( collection ); + => MetaManipulations = new MetaManager( collection ); private static void ResetFileSeen( int size ) { @@ -258,7 +257,7 @@ public class ModCollectionCache } private void AddMetaFiles() - => MetaManipulations.ApplyImcFiles( ResolvedFiles ); + => MetaManipulations.Imc.SetFiles(); private void AddSwaps( Mod.Mod mod ) { diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs index b14f4929..59f52ace 100644 --- a/Penumbra/Mods/ModManager.Directory.cs +++ b/Penumbra/Mods/ModManager.Directory.cs @@ -2,14 +2,16 @@ using System; using System.Collections.Generic; using System.IO; using Dalamud.Logging; +using Penumbra.Meta.Manipulations; using Penumbra.Mod; namespace Penumbra.Mods; public partial class ModManagerNew { - private readonly List _mods = new(); - public IReadOnlyList Mods + private readonly List< ModData > _mods = new(); + + public IReadOnlyList< ModData > Mods => _mods; public void DiscoverMods() @@ -35,7 +37,6 @@ public partial class ModManagerNew //Collections.RecreateCaches(); } } - public partial class ModManagerNew { public DirectoryInfo BasePath { get; private set; } = null!; @@ -52,7 +53,7 @@ public partial class ModManagerNew { if( Valid ) { - Valid = Directory.Exists(BasePath.FullName); + Valid = Directory.Exists( BasePath.FullName ); } return Valid; @@ -87,7 +88,6 @@ public partial class ModManagerNew return; } - ( BasePath, Valid ) = CreateDirectory( path ); if( Penumbra.Config.ModDirectory != BasePath.FullName ) @@ -105,6 +105,6 @@ public partial class ModManagerNew } InitBaseDirectory( path ); - BasePathChanged?.Invoke( BasePath ); + BasePathChanged?.Invoke( BasePath ); } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 666a349f..930c72c7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -238,7 +238,7 @@ public class Penumbra : IDalamudPlugin //PathResolver.Dispose(); ResourceLogger.Dispose(); ResourceLoader.Dispose(); - + CharacterUtility.Dispose(); ShutdownWebServer(); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 347b9ddc..13a25cc0 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -18,11 +18,12 @@ full - DEBUG;TRACE + DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC pdbonly + $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 7d2e8ca9..b353192f 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -1,28 +1,19 @@ using System; using System.Collections.Generic; -using System.Drawing.Text; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using FFXIVClientStructs.FFXIV.Client.System.String; using ImGuiNET; using Penumbra.Api; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; using Penumbra.Interop; -using Penumbra.Meta; -using Penumbra.Mods; +using Penumbra.Meta.Files; using Penumbra.UI.Custom; -using Penumbra.Util; -using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; -using Utf8String = Penumbra.GameData.ByteString.Utf8String; namespace Penumbra.UI; @@ -391,6 +382,31 @@ public partial class SettingsInterface return; } + var eqp = 0; + ImGui.InputInt( "##EqpInput", ref eqp ); + try + { + var def = ExpandedEqpFile.GetDefault( eqp ); + var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqp.File?[ eqp ] ?? def; + ImGui.Text( Convert.ToString( ( long )def, 2 ).PadLeft( 64, '0' ) ); + ImGui.Text( Convert.ToString( ( long )val, 2 ).PadLeft( 64, '0' ) ); + } + catch + { } + + var eqdp = 0; + ImGui.InputInt( "##EqdpInput", ref eqdp ); + try + { + var def = ExpandedEqdpFile.GetDefault(GenderRace.MidlanderMale, false, eqdp ); + var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqdp.File(GenderRace.MidlanderMale, false)?[eqdp] ?? def; + ImGui.Text( Convert.ToString( ( ushort )def, 2 ).PadLeft( 16, '0' ) ); + ImGui.Text( Convert.ToString( ( ushort )val, 2 ).PadLeft( 16, '0' ) ); + } + catch + { } + + if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) { return; @@ -398,9 +414,10 @@ public partial class SettingsInterface using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - for( var i = 0; i < CharacterUtility.NumRelevantResources; ++i ) + for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) { - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ i ]; + var idx = CharacterUtility.RelevantIndices[ i ]; + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; ImGui.TableNextColumn(); ImGui.Text( $"0x{( ulong )resource:X}" ); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index cca21491..3e545e34 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -3,772 +3,805 @@ using System.Collections.Generic; using System.Drawing.Text; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using Dalamud.Interface; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.UI.Custom; using ObjectType = Penumbra.GameData.Enums.ObjectType; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private partial class PluginDetails { - private partial class PluginDetails + private int _newManipTypeIdx = 0; + private ushort _newManipSetId = 0; + private ushort _newManipSecondaryId = 0; + private int _newManipSubrace = 0; + private int _newManipRace = 0; + private int _newManipAttribute = 0; + private int _newManipEquipSlot = 0; + private int _newManipObjectType = 0; + private int _newManipGender = 0; + private int _newManipBodySlot = 0; + private ushort _newManipVariant = 0; + + + private static readonly (string, EquipSlot)[] EqpEquipSlots = { - private int _newManipTypeIdx = 0; - private ushort _newManipSetId = 0; - private ushort _newManipSecondaryId = 0; - private int _newManipSubrace = 0; - private int _newManipRace = 0; - private int _newManipAttribute = 0; - private int _newManipEquipSlot = 0; - private int _newManipObjectType = 0; - private int _newManipGender = 0; - private int _newManipBodySlot = 0; - private ushort _newManipVariant = 0; + ( "Head", EquipSlot.Head ), + ( "Body", EquipSlot.Body ), + ( "Hands", EquipSlot.Hands ), + ( "Legs", EquipSlot.Legs ), + ( "Feet", EquipSlot.Feet ), + }; + private static readonly (string, EquipSlot)[] EqdpEquipSlots = + { + EqpEquipSlots[ 0 ], + EqpEquipSlots[ 1 ], + EqpEquipSlots[ 2 ], + EqpEquipSlots[ 3 ], + EqpEquipSlots[ 4 ], + ( "Ears", EquipSlot.Ears ), + ( "Neck", EquipSlot.Neck ), + ( "Wrist", EquipSlot.Wrists ), + ( "Left Finger", EquipSlot.LFinger ), + ( "Right Finger", EquipSlot.RFinger ), + }; - private static readonly (string, EquipSlot)[] EqpEquipSlots = + private static readonly (string, ModelRace)[] Races = + { + ( ModelRace.Midlander.ToName(), ModelRace.Midlander ), + ( ModelRace.Highlander.ToName(), ModelRace.Highlander ), + ( ModelRace.Elezen.ToName(), ModelRace.Elezen ), + ( ModelRace.Miqote.ToName(), ModelRace.Miqote ), + ( ModelRace.Roegadyn.ToName(), ModelRace.Roegadyn ), + ( ModelRace.Lalafell.ToName(), ModelRace.Lalafell ), + ( ModelRace.AuRa.ToName(), ModelRace.AuRa ), + ( ModelRace.Viera.ToName(), ModelRace.Viera ), + ( ModelRace.Hrothgar.ToName(), ModelRace.Hrothgar ), + }; + + private static readonly (string, Gender)[] Genders = + { + ( Gender.Male.ToName(), Gender.Male ), + ( Gender.Female.ToName(), Gender.Female ), + ( Gender.MaleNpc.ToName(), Gender.MaleNpc ), + ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), + }; + + private static readonly (string, ObjectType)[] ObjectTypes = + { + ( "Equipment", ObjectType.Equipment ), + ( "Customization", ObjectType.Character ), + }; + + private static readonly (string, EquipSlot)[] EstEquipSlots = + { + EqpEquipSlots[ 0 ], + EqpEquipSlots[ 1 ], + }; + + private static readonly (string, BodySlot)[] EstBodySlots = + { + ( "Hair", BodySlot.Hair ), + ( "Face", BodySlot.Face ), + }; + + private static readonly (string, SubRace)[] Subraces = + { + ( SubRace.Midlander.ToName(), SubRace.Midlander ), + ( SubRace.Highlander.ToName(), SubRace.Highlander ), + ( SubRace.Wildwood.ToName(), SubRace.Wildwood ), + ( SubRace.Duskwight.ToName(), SubRace.Duskwight ), + ( SubRace.SeekerOfTheSun.ToName(), SubRace.SeekerOfTheSun ), + ( SubRace.KeeperOfTheMoon.ToName(), SubRace.KeeperOfTheMoon ), + ( SubRace.Seawolf.ToName(), SubRace.Seawolf ), + ( SubRace.Hellsguard.ToName(), SubRace.Hellsguard ), + ( SubRace.Plainsfolk.ToName(), SubRace.Plainsfolk ), + ( SubRace.Dunesfolk.ToName(), SubRace.Dunesfolk ), + ( SubRace.Raen.ToName(), SubRace.Raen ), + ( SubRace.Xaela.ToName(), SubRace.Xaela ), + ( SubRace.Rava.ToName(), SubRace.Rava ), + ( SubRace.Veena.ToName(), SubRace.Veena ), + ( SubRace.Helion.ToName(), SubRace.Helion ), + ( SubRace.Lost.ToName(), SubRace.Lost ), + }; + + private static readonly (string, RspAttribute)[] RspAttributes = + { + ( RspAttribute.MaleMinSize.ToFullString(), RspAttribute.MaleMinSize ), + ( RspAttribute.MaleMaxSize.ToFullString(), RspAttribute.MaleMaxSize ), + ( RspAttribute.FemaleMinSize.ToFullString(), RspAttribute.FemaleMinSize ), + ( RspAttribute.FemaleMaxSize.ToFullString(), RspAttribute.FemaleMaxSize ), + ( RspAttribute.BustMinX.ToFullString(), RspAttribute.BustMinX ), + ( RspAttribute.BustMaxX.ToFullString(), RspAttribute.BustMaxX ), + ( RspAttribute.BustMinY.ToFullString(), RspAttribute.BustMinY ), + ( RspAttribute.BustMaxY.ToFullString(), RspAttribute.BustMaxY ), + ( RspAttribute.BustMinZ.ToFullString(), RspAttribute.BustMinZ ), + ( RspAttribute.BustMaxZ.ToFullString(), RspAttribute.BustMaxZ ), + ( RspAttribute.MaleMinTail.ToFullString(), RspAttribute.MaleMinTail ), + ( RspAttribute.MaleMaxTail.ToFullString(), RspAttribute.MaleMaxTail ), + ( RspAttribute.FemaleMinTail.ToFullString(), RspAttribute.FemaleMinTail ), + ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), + }; + + private static readonly (string, ObjectType)[] ImcObjectType = + { + ObjectTypes[ 0 ], + ( "Weapon", ObjectType.Weapon ), + ( "Demihuman", ObjectType.DemiHuman ), + ( "Monster", ObjectType.Monster ), + }; + + private static readonly (string, BodySlot)[] ImcBodySlots = + { + EstBodySlots[ 0 ], + EstBodySlots[ 1 ], + ( "Body", BodySlot.Body ), + ( "Tail", BodySlot.Tail ), + ( "Ears", BodySlot.Zear ), + }; + + private static bool PrintCheckBox( string name, ref bool value, bool def ) + { + var color = value == def ? 0 : value ? ColorDarkGreen : ColorDarkRed; + if( color == 0 ) { - ( "Head", EquipSlot.Head ), - ( "Body", EquipSlot.Body ), - ( "Hands", EquipSlot.Hands ), - ( "Legs", EquipSlot.Legs ), - ( "Feet", EquipSlot.Feet ), - }; - - private static readonly (string, EquipSlot)[] EqdpEquipSlots = - { - EqpEquipSlots[ 0 ], - EqpEquipSlots[ 1 ], - EqpEquipSlots[ 2 ], - EqpEquipSlots[ 3 ], - EqpEquipSlots[ 4 ], - ( "Ears", EquipSlot.Ears ), - ( "Neck", EquipSlot.Neck ), - ( "Wrist", EquipSlot.Wrists ), - ( "Left Finger", EquipSlot.LFinger ), - ( "Right Finger", EquipSlot.RFinger ), - }; - - private static readonly (string, ModelRace)[] Races = - { - ( ModelRace.Midlander.ToName(), ModelRace.Midlander ), - ( ModelRace.Highlander.ToName(), ModelRace.Highlander ), - ( ModelRace.Elezen.ToName(), ModelRace.Elezen ), - ( ModelRace.Miqote.ToName(), ModelRace.Miqote ), - ( ModelRace.Roegadyn.ToName(), ModelRace.Roegadyn ), - ( ModelRace.Lalafell.ToName(), ModelRace.Lalafell ), - ( ModelRace.AuRa.ToName(), ModelRace.AuRa ), - ( ModelRace.Viera.ToName(), ModelRace.Viera ), - ( ModelRace.Hrothgar.ToName(), ModelRace.Hrothgar ), - }; - - private static readonly (string, Gender)[] Genders = - { - ( Gender.Male.ToName(), Gender.Male ), - ( Gender.Female.ToName(), Gender.Female ), - ( Gender.MaleNpc.ToName(), Gender.MaleNpc ), - ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), - }; - - private static readonly (string, ObjectType)[] ObjectTypes = - { - ( "Equipment", ObjectType.Equipment ), - ( "Customization", ObjectType.Character ), - }; - - private static readonly (string, EquipSlot)[] EstEquipSlots = - { - EqpEquipSlots[ 0 ], - EqpEquipSlots[ 1 ], - }; - - private static readonly (string, BodySlot)[] EstBodySlots = - { - ( "Hair", BodySlot.Hair ), - ( "Face", BodySlot.Face ), - }; - - private static readonly (string, SubRace)[] Subraces = - { - ( SubRace.Midlander.ToName(), SubRace.Midlander ), - ( SubRace.Highlander.ToName(), SubRace.Highlander ), - ( SubRace.Wildwood.ToName(), SubRace.Wildwood ), - ( SubRace.Duskwight.ToName(), SubRace.Duskwight ), - ( SubRace.SeekerOfTheSun.ToName(), SubRace.SeekerOfTheSun ), - ( SubRace.KeeperOfTheMoon.ToName(), SubRace.KeeperOfTheMoon ), - ( SubRace.Seawolf.ToName(), SubRace.Seawolf ), - ( SubRace.Hellsguard.ToName(), SubRace.Hellsguard ), - ( SubRace.Plainsfolk.ToName(), SubRace.Plainsfolk ), - ( SubRace.Dunesfolk.ToName(), SubRace.Dunesfolk ), - ( SubRace.Raen.ToName(), SubRace.Raen ), - ( SubRace.Xaela.ToName(), SubRace.Xaela ), - ( SubRace.Rava.ToName(), SubRace.Rava ), - ( SubRace.Veena.ToName(), SubRace.Veena ), - ( SubRace.Helion.ToName(), SubRace.Helion ), - ( SubRace.Lost.ToName(), SubRace.Lost ), - }; - - private static readonly (string, RspAttribute)[] RspAttributes = - { - ( RspAttribute.MaleMinSize.ToFullString(), RspAttribute.MaleMinSize ), - ( RspAttribute.MaleMaxSize.ToFullString(), RspAttribute.MaleMaxSize ), - ( RspAttribute.FemaleMinSize.ToFullString(), RspAttribute.FemaleMinSize ), - ( RspAttribute.FemaleMaxSize.ToFullString(), RspAttribute.FemaleMaxSize ), - ( RspAttribute.BustMinX.ToFullString(), RspAttribute.BustMinX ), - ( RspAttribute.BustMaxX.ToFullString(), RspAttribute.BustMaxX ), - ( RspAttribute.BustMinY.ToFullString(), RspAttribute.BustMinY ), - ( RspAttribute.BustMaxY.ToFullString(), RspAttribute.BustMaxY ), - ( RspAttribute.BustMinZ.ToFullString(), RspAttribute.BustMinZ ), - ( RspAttribute.BustMaxZ.ToFullString(), RspAttribute.BustMaxZ ), - ( RspAttribute.MaleMinTail.ToFullString(), RspAttribute.MaleMinTail ), - ( RspAttribute.MaleMaxTail.ToFullString(), RspAttribute.MaleMaxTail ), - ( RspAttribute.FemaleMinTail.ToFullString(), RspAttribute.FemaleMinTail ), - ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), - }; - - private static readonly (string, ObjectType)[] ImcObjectType = - { - ObjectTypes[ 0 ], - ( "Weapon", ObjectType.Weapon ), - ( "Demihuman", ObjectType.DemiHuman ), - ( "Monster", ObjectType.Monster ), - }; - - private static readonly (string, BodySlot)[] ImcBodySlots = - { - EstBodySlots[ 0 ], - EstBodySlots[ 1 ], - ( "Body", BodySlot.Body ), - ( "Tail", BodySlot.Tail ), - ( "Ears", BodySlot.Zear ), - }; - - private static bool PrintCheckBox( string name, ref bool value, bool def ) - { - var color = value == def ? 0 : value ? ColorDarkGreen : ColorDarkRed; - if( color == 0 ) - { - return ImGui.Checkbox( name, ref value ); - } - - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color ); - var ret = ImGui.Checkbox( name, ref value ); - return ret; + return ImGui.Checkbox( name, ref value ); } - private bool RestrictedInputInt( string name, ref ushort value, ushort min, ushort max ) - { - int tmp = value; - if( ImGui.InputInt( name, ref tmp, 0, 0, _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - && tmp != value - && tmp >= min - && tmp <= max ) - { - value = ( ushort )tmp; - return true; - } + using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color ); + var ret = ImGui.Checkbox( name, ref value ); + return ret; + } + private bool RestrictedInputInt( string name, ref ushort value, ushort min, ushort max ) + { + int tmp = value; + if( ImGui.InputInt( name, ref tmp, 0, 0, _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) + && tmp != value + && tmp >= min + && tmp <= max ) + { + value = ( ushort )tmp; + return true; + } + + return false; + } + + private static bool DefaultButton< T >( string name, ref T value, T defaultValue ) where T : IComparable< T > + { + var compare = defaultValue.CompareTo( value ); + var color = compare < 0 ? ColorDarkGreen : + compare > 0 ? ColorDarkRed : ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Button ] ); + + using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Button, color ); + var ret = ImGui.Button( name, Vector2.UnitX * 120 ) && compare != 0; + ImGui.SameLine(); + return ret; + } + + private bool DrawInputWithDefault( string name, ref ushort value, ushort defaultValue, ushort max ) + => DefaultButton( $"{( _editMode ? "Set to " : "" )}Default: {defaultValue}##imc{name}", ref value, defaultValue ) + || RestrictedInputInt( name, ref value, 0, max ); + + private static bool CustomCombo< T >( string label, IList< (string, T) > namesAndValues, out T value, ref int idx ) + { + value = idx < namesAndValues.Count ? namesAndValues[ idx ].Item2 : default!; + + if( !ImGui.BeginCombo( label, idx < namesAndValues.Count ? namesAndValues[ idx ].Item1 : string.Empty ) ) + { return false; } - private static bool DefaultButton< T >( string name, ref T value, T defaultValue ) where T : IComparable< T > + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + + for( var i = 0; i < namesAndValues.Count; ++i ) { - var compare = defaultValue.CompareTo( value ); - var color = compare < 0 ? ColorDarkGreen : - compare > 0 ? ColorDarkRed : ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Button ] ); - - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Button, color ); - var ret = ImGui.Button( name, Vector2.UnitX * 120 ) && compare != 0; - ImGui.SameLine(); - return ret; - } - - private bool DrawInputWithDefault( string name, ref ushort value, ushort defaultValue, ushort max ) - => DefaultButton( $"{( _editMode ? "Set to " : "" )}Default: {defaultValue}##imc{name}", ref value, defaultValue ) - || RestrictedInputInt( name, ref value, 0, max ); - - private static bool CustomCombo< T >( string label, IList< (string, T) > namesAndValues, out T value, ref int idx ) - { - value = idx < namesAndValues.Count ? namesAndValues[ idx ].Item2 : default!; - - if( !ImGui.BeginCombo( label, idx < namesAndValues.Count ? namesAndValues[ idx ].Item1 : string.Empty ) ) + if( !ImGui.Selectable( $"{namesAndValues[ i ].Item1}##{label}{i}", idx == i ) || idx == i ) { - return false; + continue; } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + idx = i; + value = namesAndValues[ i ].Item2; + return true; + } - for( var i = 0; i < namesAndValues.Count; ++i ) + return false; + } + + private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Eqp; + var val = id.Entry; + + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var defaults = ExpandedEqpFile.GetDefault( id.SetId ); + var attributes = Eqp.EqpAttributes[ id.Slot ]; + + foreach( var flag in attributes ) { - if( !ImGui.Selectable( $"{namesAndValues[ i ].Item1}##{label}{i}", idx == i ) || idx == i ) + var name = flag.ToLocalName(); + var tmp = val.HasFlag( flag ); + if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) { - continue; + list[ manipIdx ] = new MetaManipulation( new EqpManipulation( tmp ? val | flag : val & ~flag, id.Slot, id.SetId ) ); + ret = true; } + } + } - idx = i; - value = namesAndValues[ i ].Item2; + ImGui.Text( ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Slot.ToString() ); + return ret; + } + + private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Gmp; + var val = id.Entry; + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var defaults = ExpandedGmpFile.GetDefault( id.SetId ); + var enabled = val.Enabled; + var animated = val.Animated; + var rotationA = val.RotationA; + var rotationB = val.RotationB; + var rotationC = val.RotationC; + ushort unk = val.UnknownTotal; + + ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; + ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); + ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); + ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); + ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); + ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); + + if( ret && _editMode ) + { + list[ manipIdx ] = new MetaManipulation( new GmpManipulation( new GmpEntry + { + Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, + RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, + }, id.SetId ) ); + } + } + + ImGui.Text( ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( EquipSlot.Head.ToString() ); + return ret; + } + + private static (bool, bool) GetEqdpBits( EquipSlot slot, EqdpEntry entry ) + { + return slot switch + { + EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), + EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), + EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), + EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), + EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), + EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), + EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), + EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), + EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), + EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), + _ => ( false, false ), + }; + } + + private static EqdpEntry SetEqdpBits( EquipSlot slot, EqdpEntry value, bool bit1, bool bit2 ) + { + switch( slot ) + { + case EquipSlot.Head: + value = bit1 ? value | EqdpEntry.Head1 : value & ~EqdpEntry.Head1; + value = bit2 ? value | EqdpEntry.Head2 : value & ~EqdpEntry.Head2; + return value; + case EquipSlot.Body: + value = bit1 ? value | EqdpEntry.Body1 : value & ~EqdpEntry.Body1; + value = bit2 ? value | EqdpEntry.Body2 : value & ~EqdpEntry.Body2; + return value; + case EquipSlot.Hands: + value = bit1 ? value | EqdpEntry.Hands1 : value & ~EqdpEntry.Hands1; + value = bit2 ? value | EqdpEntry.Hands2 : value & ~EqdpEntry.Hands2; + return value; + case EquipSlot.Legs: + value = bit1 ? value | EqdpEntry.Legs1 : value & ~EqdpEntry.Legs1; + value = bit2 ? value | EqdpEntry.Legs2 : value & ~EqdpEntry.Legs2; + return value; + case EquipSlot.Feet: + value = bit1 ? value | EqdpEntry.Feet1 : value & ~EqdpEntry.Feet1; + value = bit2 ? value | EqdpEntry.Feet2 : value & ~EqdpEntry.Feet2; + return value; + case EquipSlot.Neck: + value = bit1 ? value | EqdpEntry.Neck1 : value & ~EqdpEntry.Neck1; + value = bit2 ? value | EqdpEntry.Neck2 : value & ~EqdpEntry.Neck2; + return value; + case EquipSlot.Ears: + value = bit1 ? value | EqdpEntry.Ears1 : value & ~EqdpEntry.Ears1; + value = bit2 ? value | EqdpEntry.Ears2 : value & ~EqdpEntry.Ears2; + return value; + case EquipSlot.Wrists: + value = bit1 ? value | EqdpEntry.Wrists1 : value & ~EqdpEntry.Wrists1; + value = bit2 ? value | EqdpEntry.Wrists2 : value & ~EqdpEntry.Wrists2; + return value; + case EquipSlot.RFinger: + value = bit1 ? value | EqdpEntry.RingR1 : value & ~EqdpEntry.RingR1; + value = bit2 ? value | EqdpEntry.RingR2 : value & ~EqdpEntry.RingR2; + return value; + case EquipSlot.LFinger: + value = bit1 ? value | EqdpEntry.RingL1 : value & ~EqdpEntry.RingL1; + value = bit2 ? value | EqdpEntry.RingL2 : value & ~EqdpEntry.RingL2; + return value; + } + + return value; + } + + private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Eqdp; + var val = id.Entry; + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var defaults = ExpandedEqdpFile.GetDefault( id.FileIndex(), id.SetId ); + var (bit1, bit2) = GetEqdpBits( id.Slot, val ); + var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); + + ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); + ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); + + if( ret && _editMode ) + { + list[ manipIdx ] = new MetaManipulation( new EqdpManipulation( SetEqdpBits( id.Slot, val, bit1, bit2 ), id.Slot, id.Gender, + id.Race, id.SetId ) ); + } + } + + ImGui.Text( id.Slot.IsAccessory() + ? ObjectType.Accessory.ToString() + : ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Slot.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Race.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Gender.ToString() ); + return ret; + } + + private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Est; + var val = id.SkeletonIdx; + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var defaults = EstFile.GetDefault( id.Slot, Names.CombinedRace( id.Gender, id.Race ), id.SetId ); + if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) + { + list[ manipIdx ] = new MetaManipulation( new EstManipulation( id.Gender, id.Race, id.Slot, id.SetId, val ) ); + ret = true; + } + } + + ImGui.Text( id.Slot.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.Text( id.Race.ToName() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Gender.ToName() ); + + return ret; + } + + private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Imc; + var val = id.Entry; + + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var defaults = new ImcFile( id.GamePath() ).GetEntry( ImcFile.PartIndex( id.EquipSlot ), id.Variant ); + ushort materialId = val.MaterialId; + ushort vfxId = val.VfxId; + ushort decalId = val.DecalId; + var soundId = ( ushort )val.SoundId; + var attributeMask = val.AttributeMask; + var materialAnimationId = ( ushort )val.MaterialAnimationId; + ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); + ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); + ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); + ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); + ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); + ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, + byte.MaxValue ); + + if( ret && _editMode ) + { + var value = new ImcEntry( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, ( byte )vfxId, + ( byte )materialAnimationId ); + list[ manipIdx ] = new MetaManipulation( new ImcManipulation( id, value ) ); + } + } + + ImGui.Text( id.ObjectType.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.PrimaryId.ToString() ); + ImGui.TableNextColumn(); + if( id.ObjectType is ObjectType.Accessory or ObjectType.Equipment ) + { + ImGui.Text( id.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? id.EquipSlot.ToString() + : id.BodySlot.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + if( id.ObjectType != ObjectType.Equipment + && id.ObjectType != ObjectType.Accessory ) + { + ImGui.Text( id.SecondaryId.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.Text( id.Variant.ToString() ); + return ret; + } + + private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) + { + var ret = false; + var id = list[ manipIdx ].Rsp; + var defaults = CmpFile.GetDefault( id.SubRace, id.Attribute ); + var val = id.Entry; + if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + if( DefaultButton( + $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) + && _editMode ) + { + list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); + ret = true; + } + + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", + _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) + && val >= 0 + && val <= 5 + && _editMode ) + { + list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); + ret = true; + } + } + + ImGui.Text( id.Attribute.ToUngenderedString() ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.Text( id.SubRace.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( id.Attribute.ToGender().ToString() ); + return ret; + } + + private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) + { + var type = list[ manipIdx ].ManipulationType; + + if( _editMode ) + { + ImGui.TableNextColumn(); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##manipDelete{manipIdx}" ) ) + { + list.RemoveAt( manipIdx ); + ImGui.TableNextRow(); + --manipIdx; + --count; return true; } - - return false; } - private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) + ImGui.TableNextColumn(); + ImGui.Text( type.ToString() ); + ImGui.TableNextColumn(); + + var changes = false; + switch( type ) { - var ret = false; - //var id = list[ manipIdx ].EqpIdentifier; - //var val = list[ manipIdx ].EqpValue; - // - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; - // var attributes = Eqp.EqpAttributes[ id.Slot ]; - // - // foreach( var flag in attributes ) - // { - // var name = flag.ToLocalName(); - // var tmp = val.HasFlag( flag ); - // if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) - // { - // list[ manipIdx ] = MetaManipulation.Eqp( id.Slot, id.SetId, tmp ? val | flag : val & ~flag ); - // ret = true; - // } - // } - //} - // - //ImGui.Text( ObjectType.Equipment.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.SetId.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.Slot.ToString() ); - return ret; - } - - private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - //var id = list[ manipIdx ].GmpIdentifier; - //var val = list[ manipIdx ].GmpValue; - // - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // var enabled = val.Enabled; - // var animated = val.Animated; - // var rotationA = val.RotationA; - // var rotationB = val.RotationB; - // var rotationC = val.RotationC; - // ushort unk = val.UnknownTotal; - // - // ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; - // ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); - // ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); - // ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); - // ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); - // ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); - // - // if( ret && _editMode ) - // { - // list[ manipIdx ] = MetaManipulation.Gmp( id.SetId, - // new GmpEntry - // { - // Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, - // RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, - // } ); - // } - //} - // - //ImGui.Text( ObjectType.Equipment.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.SetId.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( EquipSlot.Head.ToString() ); - return ret; - } - - private static (bool, bool) GetEqdpBits( EquipSlot slot, EqdpEntry entry ) - { - return slot switch - { - EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), - EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), - EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), - EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), - EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), - EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), - EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), - EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), - EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), - EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), - _ => ( false, false ), - }; - } - - private static EqdpEntry SetEqdpBits( EquipSlot slot, EqdpEntry value, bool bit1, bool bit2 ) - { - switch( slot ) - { - case EquipSlot.Head: - value = bit1 ? value | EqdpEntry.Head1 : value & ~EqdpEntry.Head1; - value = bit2 ? value | EqdpEntry.Head2 : value & ~EqdpEntry.Head2; - return value; - case EquipSlot.Body: - value = bit1 ? value | EqdpEntry.Body1 : value & ~EqdpEntry.Body1; - value = bit2 ? value | EqdpEntry.Body2 : value & ~EqdpEntry.Body2; - return value; - case EquipSlot.Hands: - value = bit1 ? value | EqdpEntry.Hands1 : value & ~EqdpEntry.Hands1; - value = bit2 ? value | EqdpEntry.Hands2 : value & ~EqdpEntry.Hands2; - return value; - case EquipSlot.Legs: - value = bit1 ? value | EqdpEntry.Legs1 : value & ~EqdpEntry.Legs1; - value = bit2 ? value | EqdpEntry.Legs2 : value & ~EqdpEntry.Legs2; - return value; - case EquipSlot.Feet: - value = bit1 ? value | EqdpEntry.Feet1 : value & ~EqdpEntry.Feet1; - value = bit2 ? value | EqdpEntry.Feet2 : value & ~EqdpEntry.Feet2; - return value; - case EquipSlot.Neck: - value = bit1 ? value | EqdpEntry.Neck1 : value & ~EqdpEntry.Neck1; - value = bit2 ? value | EqdpEntry.Neck2 : value & ~EqdpEntry.Neck2; - return value; - case EquipSlot.Ears: - value = bit1 ? value | EqdpEntry.Ears1 : value & ~EqdpEntry.Ears1; - value = bit2 ? value | EqdpEntry.Ears2 : value & ~EqdpEntry.Ears2; - return value; - case EquipSlot.Wrists: - value = bit1 ? value | EqdpEntry.Wrists1 : value & ~EqdpEntry.Wrists1; - value = bit2 ? value | EqdpEntry.Wrists2 : value & ~EqdpEntry.Wrists2; - return value; - case EquipSlot.RFinger: - value = bit1 ? value | EqdpEntry.RingR1 : value & ~EqdpEntry.RingR1; - value = bit2 ? value | EqdpEntry.RingR2 : value & ~EqdpEntry.RingR2; - return value; - case EquipSlot.LFinger: - value = bit1 ? value | EqdpEntry.RingL1 : value & ~EqdpEntry.RingL1; - value = bit2 ? value | EqdpEntry.RingL2 : value & ~EqdpEntry.RingL2; - return value; - } - - return value; - } - - private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - //var id = list[ manipIdx ].EqdpIdentifier; - //var val = list[ manipIdx ].EqdpValue; - // - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // var (bit1, bit2) = GetEqdpBits( id.Slot, val ); - // var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); - // - // ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); - // ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); - // - // if( ret && _editMode ) - // { - // list[ manipIdx ] = MetaManipulation.Eqdp( id.Slot, id.GenderRace, id.SetId, SetEqdpBits( id.Slot, val, bit1, bit2 ) ); - // } - //} - // - //ImGui.Text( id.Slot.IsAccessory() - // ? ObjectType.Accessory.ToString() - // : ObjectType.Equipment.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.SetId.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.Slot.ToString() ); - //ImGui.TableNextColumn(); - //var (gender, race) = id.GenderRace.Split(); - //ImGui.Text( race.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( gender.ToString() ); - return ret; - } - - private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - //var id = list[ manipIdx ].EstIdentifier; - //var val = list[ manipIdx ].EstValue; - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) - // { - // list[ manipIdx ] = new MetaManipulation( id.Value, val ); - // ret = true; - // } - //} - // - //ImGui.Text( id.ObjectType.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.PrimaryId.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.ObjectType == ObjectType.Equipment - // ? id.EquipSlot.ToString() - // : id.BodySlot.ToString() ); - //ImGui.TableNextColumn(); - //var (gender, race) = id.GenderRace.Split(); - //ImGui.Text( race.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( gender.ToString() ); - - return ret; - } - - private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - //var id = list[ manipIdx ].ImcIdentifier; - //var val = list[ manipIdx ].ImcValue; - // - //if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - //{ - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // ushort materialId = val.MaterialId; - // ushort vfxId = val.VfxId; - // ushort decalId = val.DecalId; - // var soundId = ( ushort )( val.SoundId >> 10 ); - // var attributeMask = val.AttributeMask; - // var materialAnimationId = ( ushort )( val.MaterialAnimationId >> 12 ); - // ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); - // ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); - // ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); - // ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); - // ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); - // ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, - // byte.MaxValue ); - // - // if( ret && _editMode ) - // { - // var value = ImcExtensions.FromValues( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, - // ( byte )vfxId, ( byte )materialAnimationId ); - // list[ manipIdx ] = new MetaManipulation( id.Value, value.ToInteger() ); - // } - //} - // - //ImGui.Text( id.ObjectType.ToString() ); - //ImGui.TableNextColumn(); - //ImGui.Text( id.PrimaryId.ToString() ); - //ImGui.TableNextColumn(); - //if( id.ObjectType == ObjectType.Accessory - // || id.ObjectType == ObjectType.Equipment ) - //{ - // ImGui.Text( id.ObjectType == ObjectType.Equipment - // || id.ObjectType == ObjectType.Accessory - // ? id.EquipSlot.ToString() - // : id.BodySlot.ToString() ); - //} - // - //ImGui.TableNextColumn(); - //ImGui.TableNextColumn(); - //ImGui.TableNextColumn(); - //if( id.ObjectType != ObjectType.Equipment - // && id.ObjectType != ObjectType.Accessory ) - //{ - // ImGui.Text( id.SecondaryId.ToString() ); - //} - // - //ImGui.TableNextColumn(); - //ImGui.Text( id.Variant.ToString() ); - return ret; - } - - private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].RspIdentifier; - var val = list[ manipIdx ].RspValue; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - if( DefaultButton( - $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) - && _editMode ) + case MetaManipulation.Type.Eqp: + changes = DrawEqpRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{list[ manipIdx ].Eqp.Entry}##{manipIdx}" ) ) { - list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults ); - ret = true; + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } - - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", - _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - && val >= 0 - && val <= 5 - && _editMode ) + + break; + case MetaManipulation.Type.Gmp: + changes = DrawGmpRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{list[ manipIdx ].Gmp.Entry.Value}##{manipIdx}" ) ) { - list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val ); - ret = true; + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } - } - - ImGui.Text( id.Attribute.ToUngenderedString() ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( id.SubRace.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Attribute.ToGender().ToString() ); - return ret; - } - private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) - { - var type = list[ manipIdx ].ManipulationType; - - if( _editMode ) - { - ImGui.TableNextColumn(); - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##manipDelete{manipIdx}" ) ) + break; + case MetaManipulation.Type.Eqdp: + changes = DrawEqdpRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + var (bit1, bit2) = GetEqdpBits( list[ manipIdx ].Eqdp.Slot, list[ manipIdx ].Eqdp.Entry ); + if( ImGui.Selectable( $"{bit1} {bit2}##{manipIdx}" ) ) { - list.RemoveAt( manipIdx ); - ImGui.TableNextRow(); - --manipIdx; - --count; - return true; + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } - } - ImGui.TableNextColumn(); - ImGui.Text( type.ToString() ); - ImGui.TableNextColumn(); + break; + case MetaManipulation.Type.Est: + changes = DrawEstRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{list[ manipIdx ].Est.SkeletonIdx}##{manipIdx}" ) ) + { + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); + } - var changes = false; - switch( type ) - { - case MetaManipulation.Type.Eqp: - changes = DrawEqpRow( manipIdx, list ); - break; - case MetaManipulation.Type.Gmp: - changes = DrawGmpRow( manipIdx, list ); - break; - case MetaManipulation.Type.Eqdp: - changes = DrawEqdpRow( manipIdx, list ); - break; - case MetaManipulation.Type.Est: - changes = DrawEstRow( manipIdx, list ); - break; - case MetaManipulation.Type.Imc: - changes = DrawImcRow( manipIdx, list ); - break; - case MetaManipulation.Type.Rsp: - changes = DrawRspRow( manipIdx, list ); - break; - } - - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{manipIdx}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - ImGui.TableNextRow(); - return changes; + break; + case MetaManipulation.Type.Imc: + changes = DrawImcRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{list[ manipIdx ].Imc.Entry.MaterialId}##{manipIdx}" ) ) + { + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); + } + + break; + case MetaManipulation.Type.Rsp: + changes = DrawRspRow( manipIdx, list ); + ImGui.TableSetColumnIndex( 9 ); + if( ImGui.Selectable( $"{list[ manipIdx ].Rsp.Entry}##{manipIdx}" ) ) + { + ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); + } + + break; } - private MetaManipulation.Type DrawNewTypeSelection() + ImGui.TableNextRow(); + return changes; + } + + + private MetaManipulation.Type DrawNewTypeSelection() + { + ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); + ImGui.SameLine(); + ImGui.RadioButton( "EQDP##newManipType", ref _newManipTypeIdx, 2 ); + ImGui.SameLine(); + ImGui.RadioButton( "EQP##newManipType", ref _newManipTypeIdx, 3 ); + ImGui.SameLine(); + ImGui.RadioButton( "EST##newManipType", ref _newManipTypeIdx, 4 ); + ImGui.SameLine(); + ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); + ImGui.SameLine(); + ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); + return ( MetaManipulation.Type )_newManipTypeIdx; + } + + private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) + { + var change = false; + if( !ImGui.BeginPopup( popupName ) ) { - ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); - ImGui.SameLine(); - ImGui.RadioButton( "EQDP##newManipType", ref _newManipTypeIdx, 2 ); - ImGui.SameLine(); - ImGui.RadioButton( "EQP##newManipType", ref _newManipTypeIdx, 3 ); - ImGui.SameLine(); - ImGui.RadioButton( "EST##newManipType", ref _newManipTypeIdx, 4 ); - ImGui.SameLine(); - ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); - ImGui.SameLine(); - ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); - return ( MetaManipulation.Type )_newManipTypeIdx; - } - - private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) - { - var change = false; - if( !ImGui.BeginPopup( popupName ) ) - { - return change; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var manipType = DrawNewTypeSelection(); - MetaManipulation? newManip = null; - switch( manipType ) - { - case MetaManipulation.Type.Imc: - { - RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); - RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); - CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); - EquipSlot equipSlot = default; - switch( objectType ) - { - case ObjectType.Equipment: - CustomCombo( "Equipment Slot", EqdpEquipSlots, out equipSlot, ref _newManipEquipSlot ); - //newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant, - // new ImcFile.ImageChangeData() ); - break; - case ObjectType.DemiHuman: - case ObjectType.Weapon: - case ObjectType.Monster: - RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); - CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); - //newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, - // _newManipVariant, new ImcFile.ImageChangeData() ); - break; - } - - break; - } - case MetaManipulation.Type.Eqdp: - { - RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - CustomCombo( "Race", Races, out var race, ref _newManipRace ); - CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - //newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId, - // new EqdpEntry() ); - break; - } - case MetaManipulation.Type.Eqp: - { - RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - //newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 ); - break; - } - case MetaManipulation.Type.Est: - { - RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Object Type", ObjectTypes, out var objectType, ref _newManipObjectType ); - EquipSlot equipSlot = default; - BodySlot bodySlot = default; - switch( ( ObjectType )_newManipObjectType ) - { - case ObjectType.Equipment: - CustomCombo( "Equipment Slot", EstEquipSlots, out equipSlot, ref _newManipEquipSlot ); - break; - case ObjectType.Character: - CustomCombo( "Body Slot", EstBodySlots, out bodySlot, ref _newManipBodySlot ); - break; - } - - CustomCombo( "Race", Races, out var race, ref _newManipRace ); - CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - //newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot, - // ( ushort )_newManipSetId, 0 ); - break; - } - case MetaManipulation.Type.Gmp: - RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); - //newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() ); - break; - case MetaManipulation.Type.Rsp: - CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); - CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); - //newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f ); - break; - } - - //if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) - // && newManip != null - // && list.All( m => m.Identifier != newManip.Value.Identifier ) ) - //{ - // var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); - // if( def != null ) - // { - // var manip = newManip.Value.Type switch - // { - // MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), - // MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, - // ( ( ImcFile.ImageChangeData )def ).ToInteger() ), - // MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace, - // newManip.Value.RspIdentifier.Attribute, ( float )def ), - // _ => throw new InvalidEnumArgumentException(), - // }; - // list.Add( manip ); - // change = true; - // ++count; - // } - // - // ImGui.CloseCurrentPopup(); - //} - return change; } - private bool DrawMetaManipulationsTable( string label, IList< MetaManipulation > list, ref int count ) + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + var manipType = DrawNewTypeSelection(); + MetaManipulation? newManip = null; + switch( manipType ) { - var numRows = _editMode ? 11 : 10; - var changes = false; - - - if( list.Count > 0 - && ImGui.BeginTable( label, numRows, - ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + case MetaManipulation.Type.Imc: { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - if( _editMode ) + RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); + RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); + CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); + EquipSlot equipSlot = default; + switch( objectType ) { - ImGui.TableNextColumn(); + case ObjectType.Equipment: + CustomCombo( "Equipment Slot", EqdpEquipSlots, out equipSlot, ref _newManipEquipSlot ); + //newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant, + // new ImcFile.ImageChangeData() ); + break; + case ObjectType.DemiHuman: + case ObjectType.Weapon: + case ObjectType.Monster: + RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); + CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); + //newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, + // _newManipVariant, new ImcFile.ImageChangeData() ); + break; } - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Type##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Object Type##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Set##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Slot##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Race##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Gender##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Secondary ID##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Variant##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Value##{label}" ); - ImGui.TableNextRow(); - - for( var i = 0; i < list.Count; ++i ) - { - changes |= DrawManipulationRow( ref i, list, ref count ); - } + break; } + case MetaManipulation.Type.Eqdp: + { + RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); + CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); + CustomCombo( "Race", Races, out var race, ref _newManipRace ); + CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); + //newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId, + // new EqdpEntry() ); + break; + } + case MetaManipulation.Type.Eqp: + { + RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); + CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); + //newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 ); + break; + } + case MetaManipulation.Type.Est: + { + RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); + CustomCombo( "Object Type", ObjectTypes, out var objectType, ref _newManipObjectType ); + EquipSlot equipSlot = default; + BodySlot bodySlot = default; + switch( ( ObjectType )_newManipObjectType ) + { + case ObjectType.Equipment: + CustomCombo( "Equipment Slot", EstEquipSlots, out equipSlot, ref _newManipEquipSlot ); + break; + case ObjectType.Character: + CustomCombo( "Body Slot", EstBodySlots, out bodySlot, ref _newManipBodySlot ); + break; + } - var popupName = $"##newManip{label}"; + CustomCombo( "Race", Races, out var race, ref _newManipRace ); + CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); + //newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot, + // ( ushort )_newManipSetId, 0 ); + break; + } + case MetaManipulation.Type.Gmp: + RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); + //newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() ); + break; + case MetaManipulation.Type.Rsp: + CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); + CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); + //newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f ); + break; + } + + //if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) + // && newManip != null + // && list.All( m => m.Identifier != newManip.Value.Identifier ) ) + //{ + // var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); + // if( def != null ) + // { + // var manip = newManip.Value.Type switch + // { + // MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), + // MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), + // MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, + // ( ( ImcFile.ImageChangeData )def ).ToInteger() ), + // MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace, + // newManip.Value.RspIdentifier.Attribute, ( float )def ), + // _ => throw new InvalidEnumArgumentException(), + // }; + // list.Add( manip ); + // change = true; + // ++count; + // } + // + // ImGui.CloseCurrentPopup(); + //} + + return change; + } + + private bool DrawMetaManipulationsTable( string label, IList< MetaManipulation > list, ref int count ) + { + var numRows = _editMode ? 11 : 10; + var changes = false; + + + if( list.Count > 0 + && ImGui.BeginTable( label, numRows, + ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + { + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); if( _editMode ) { - changes |= DrawNewManipulationPopup( $"##newManip{label}", list, ref count ); - if( ImGui.Button( $"Add New Manipulation##{label}", Vector2.UnitX * -1 ) ) - { - ImGui.OpenPopup( popupName ); - } - - return changes; + ImGui.TableNextColumn(); } - return false; + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Type##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Object Type##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Set##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Slot##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Race##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Gender##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Secondary ID##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Variant##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Value##{label}" ); + ImGui.TableNextRow(); + + for( var i = 0; i < list.Count; ++i ) + { + changes |= DrawManipulationRow( ref i, list, ref count ); + } } + + var popupName = $"##newManip{label}"; + if( _editMode ) + { + changes |= DrawNewManipulationPopup( $"##newManip{label}", list, ref count ); + if( ImGui.Button( $"Add New Manipulation##{label}", Vector2.UnitX * -1 ) ) + { + ImGui.OpenPopup( popupName ); + } + + return changes; + } + + return false; } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index f46ad018..fa187a94 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -1,11 +1,13 @@ using System; using System.Linq; using System.Numerics; +using System.Reflection.Metadata.Ecma335; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.Interop; using Penumbra.UI.Custom; @@ -55,6 +57,11 @@ public partial class SettingsInterface ResourceLoader.IterateResourceMap( map, ( hash, r ) => { + if( _filter.Length != 0 && !r->FileName.ToString().Contains( _filter, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + ImGui.TableNextColumn(); ImGui.Text( $"0x{hash:X8}" ); ImGui.TableNextColumn(); @@ -148,6 +155,8 @@ public partial class SettingsInterface } ); } + private string _filter = string.Empty; + private unsafe void DrawResourceManagerTab() { if( !ImGui.BeginTabItem( "Resource Manager Tab" ) ) @@ -164,6 +173,8 @@ public partial class SettingsInterface return; } + ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _filter, Utf8GamePath.MaxGamePathLength ); + raii.Push( ImGui.EndChild ); if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) ) { From e7282384f59ef651774e89dd9177a9b90892299f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Mar 2022 14:33:25 +0100 Subject: [PATCH 0095/2451] Working on PathResolver --- Penumbra.GameData/ByteString/FullPath.cs | 2 +- Penumbra.GameData/Enums/Race.cs | 24 - Penumbra/Dalamud.cs | 1 + .../{ => Loader}/ResourceLoader.Debug.cs | 2 +- .../ResourceLoader.Replacement.cs | 2 +- .../{ => Loader}/ResourceLoader.TexMdl.cs | 2 +- .../Interop/{ => Loader}/ResourceLoader.cs | 2 +- .../Interop/{ => Loader}/ResourceLogger.cs | 2 +- Penumbra/Interop/PathResolver.cs | 440 ------------------ .../Interop/Resolver/PathResolver.Data.cs | 249 ++++++++++ .../Interop/Resolver/PathResolver.Human.cs | 128 +++++ .../Interop/Resolver/PathResolver.Material.cs | 91 ++++ .../Interop/Resolver/PathResolver.Meta.cs | 274 +++++++++++ .../Interop/Resolver/PathResolver.Resolve.cs | 89 ++++ Penumbra/Interop/Resolver/PathResolver.cs | 99 ++++ Penumbra/Interop/Structs/MtrlResource.cs | 28 ++ Penumbra/Meta/Files/CmpFile.cs | 32 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 12 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 9 + Penumbra/Meta/Manager/MetaManager.Eqp.cs | 4 + Penumbra/Meta/Manager/MetaManager.Est.cs | 9 + Penumbra/Meta/Manager/MetaManager.Gmp.cs | 13 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 4 +- Penumbra/Meta/Manager/MetaManager.cs | 4 +- Penumbra/Mods/ModCollection.cs | 81 ++++ Penumbra/Penumbra.cs | 9 +- Penumbra/Penumbra.csproj | 4 + Penumbra/UI/MenuTabs/TabDebug.cs | 78 ++-- Penumbra/UI/MenuTabs/TabResourceManager.cs | 3 +- 29 files changed, 1170 insertions(+), 527 deletions(-) rename Penumbra/Interop/{ => Loader}/ResourceLoader.Debug.cs (99%) rename Penumbra/Interop/{ => Loader}/ResourceLoader.Replacement.cs (99%) rename Penumbra/Interop/{ => Loader}/ResourceLoader.TexMdl.cs (99%) rename Penumbra/Interop/{ => Loader}/ResourceLoader.cs (99%) rename Penumbra/Interop/{ => Loader}/ResourceLogger.cs (98%) delete mode 100644 Penumbra/Interop/PathResolver.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.Data.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.Human.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.Material.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.Meta.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.Resolve.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.cs create mode 100644 Penumbra/Interop/Structs/MtrlResource.cs diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index 2b3ffe23..5ccf71c2 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -27,7 +27,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > public FullPath( string s ) { FullName = s; - InternalName = Utf8String.FromString( FullName, out var name, true ) ? name.Replace( ( byte )'\\', ( byte )'/' ) : Utf8String.Empty; + InternalName = Utf8String.FromString( FullName.Replace( '\\', '/' ), out var name, true ) ? name : Utf8String.Empty; Crc64 = Functions.ComputeCrc64( InternalName.Span ); } diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index a83990ee..47575a31 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -107,30 +107,6 @@ public enum GenderRace : ushort public static class RaceEnumExtensions { - public static int ToRspIndex( this SubRace subRace ) - { - return subRace switch - { - SubRace.Midlander => 0, - SubRace.Highlander => 1, - SubRace.Wildwood => 10, - SubRace.Duskwight => 11, - SubRace.Plainsfolk => 20, - SubRace.Dunesfolk => 21, - SubRace.SeekerOfTheSun => 30, - SubRace.KeeperOfTheMoon => 31, - SubRace.Seawolf => 40, - SubRace.Hellsguard => 41, - SubRace.Raen => 50, - SubRace.Xaela => 51, - SubRace.Helion => 60, - SubRace.Lost => 61, - SubRace.Rava => 70, - SubRace.Veena => 71, - _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), - }; - } - public static Race ToRace( this ModelRace race ) { return race switch diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index aee12c85..b8f0c425 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -30,5 +30,6 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; // @formatter:on } \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs similarity index 99% rename from Penumbra/Interop/ResourceLoader.Debug.cs rename to Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 06f819f6..07b225c1 100644 --- a/Penumbra/Interop/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -8,7 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.GameData.ByteString; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs similarity index 99% rename from Penumbra/Interop/ResourceLoader.Replacement.cs rename to Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 837d2c19..b9473345 100644 --- a/Penumbra/Interop/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -9,7 +9,7 @@ using Penumbra.Interop.Structs; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { diff --git a/Penumbra/Interop/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs similarity index 99% rename from Penumbra/Interop/ResourceLoader.TexMdl.cs rename to Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 544e6ba4..42332f16 100644 --- a/Penumbra/Interop/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -5,7 +5,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.ByteString; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Loader; // Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection. public unsafe partial class ResourceLoader diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs similarity index 99% rename from Penumbra/Interop/ResourceLoader.cs rename to Penumbra/Interop/Loader/ResourceLoader.cs index 8e86c215..161bbd07 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -3,7 +3,7 @@ using Dalamud.Utility.Signatures; using Penumbra.GameData.ByteString; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader : IDisposable { diff --git a/Penumbra/Interop/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs similarity index 98% rename from Penumbra/Interop/ResourceLogger.cs rename to Penumbra/Interop/Loader/ResourceLogger.cs index 025b7409..47be8a68 100644 --- a/Penumbra/Interop/ResourceLogger.cs +++ b/Penumbra/Interop/Loader/ResourceLogger.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.ByteString; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Loader; // A logger class that contains the relevant data to log requested files via regex. // Filters are case-insensitive. diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs deleted file mode 100644 index b94c2e86..00000000 --- a/Penumbra/Interop/PathResolver.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Hooking; -using Dalamud.Logging; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.Mods; -using Penumbra.Util; - -namespace Penumbra.Interop; - -public unsafe class PathResolver : IDisposable -{ - //public delegate IntPtr ResolveMdlImcPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); - //public delegate IntPtr ResolveMtrlPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); - //public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); - //public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); - //public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); - //public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); - // - //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", - // DetourName = "ResolveMdlPathDetour" )] - //public Hook< ResolveMdlImcPathDelegate >? ResolveMdlPathHook; - // - //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", - // DetourName = "ResolveMtrlPathDetour" )] - //public Hook< ResolveMtrlPathDelegate >? ResolveMtrlPathHook; - // - //[Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] - //public Hook< ResolveMdlImcPathDelegate >? ResolveImcPathHook; - // - //[Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41", DetourName = "LoadMtrlTexDetour" )] - //public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; - // - //[Signature( "?? 89 ?? ?? ?? 57 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 44 ?? ?? ?? ?? ?? ?? ?? 4C", - // DetourName = "LoadMtrlShpkDetour" )] - //public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook; - // - //[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] - //public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; - // - //[Signature( - // "40 ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 48 8B ?? 48 8B ?? ?? ?? ?? ?? E8 ?? ?? ?? ?? ?? BB" )] - //public Hook< EnableDrawDelegate >? EnableDrawHook; - // - //[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", - // DetourName = "CharacterBaseDestructorDetour" )] - //public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook; - // - //public delegate void UpdateModelDelegate( IntPtr drawObject ); - // - //[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )] - //public Hook< UpdateModelDelegate >? UpdateModelsHook; - // - //public delegate void SetupConnectorModelAttributesDelegate( IntPtr drawObject, IntPtr unk ); - // - //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 41 ?? 41 ?? 41 ?? 41 ?? 48 83 ?? ?? 8B ?? ?? ?? 4C", - // DetourName = "SetupConnectorModelAttributesDetour" )] - //public Hook< SetupConnectorModelAttributesDelegate >? SetupConnectorModelAttributesHook; - // - //public delegate void SetupModelAttributesDelegate( IntPtr drawObject ); - // - //[Signature( "48 89 6C 24 ?? 56 57 41 54 41 55 41 56 48 83 EC 20", DetourName = "SetupModelAttributesDetour" )] - //public Hook< SetupModelAttributesDelegate >? SetupModelAttributesHook; - // - //[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", - // DetourName = "GetSlotEqpFlagIndirectDetour" )] - //public Hook< SetupModelAttributesDelegate >? GetSlotEqpFlagIndirectHook; - // - //public delegate void ApplyVisorStuffDelegate( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ); - // - //[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "ApplyVisorStuffDetour" )] - //public Hook< ApplyVisorStuffDelegate >? ApplyVisorStuffHook; - // - //private readonly ResourceLoader _loader; - //private readonly ResidentResourceManager _resident; - //internal readonly Dictionary< IntPtr, int > _drawObjectToObject = new(); - //internal readonly Dictionary< Utf8String, ModCollection > _pathCollections = new(); - // - //internal GameObject* _lastGameObject = null; - //internal DrawObject* _lastDrawObject = null; - // - //private bool EqpDataChanged = false; - //private IntPtr DefaultEqpData; - //private int DefaultEqpLength; - // - //private void ApplyVisorStuffDetour( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ) - //{ - // PluginLog.Information( $"{drawObject:X} {unk1:X} {unk2} {unk3:X} {unk4} {unk5} {( ulong )FindParent( drawObject ):X}" ); - // ApplyVisorStuffHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); - //} - // - //private void GetSlotEqpFlagIndirectDetour( IntPtr drawObject ) - //{ - // if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) - // { - // return; - // } - // - // ChangeEqp( drawObject ); - // GetSlotEqpFlagIndirectHook!.Original( drawObject ); - // RestoreEqp(); - //} - // - //private void ChangeEqp( IntPtr drawObject ) - //{ - // var parent = FindParent( drawObject ); - // if( parent == null ) - // { - // return; - // } - // - // var name = new Utf8String( parent->Name ); - // if( name.Length == 0 ) - // { - // return; - // } - // - // var charName = name.ToString(); - // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) - // { - // collection = Service< ModManager >.Get().Collections.DefaultCollection; - // } - // - // if( collection.Cache == null ) - // { - // collection = Service< ModManager >.Get().Collections.ForcedCollection; - // } - // - // var data = collection.Cache?.MetaManipulations.EqpData; - // if( data == null || data.Length == 0 ) - // { - // return; - // } - // - // _resident.CharacterUtility->EqpResource->SetData( data ); - // PluginLog.Information( $"Changed eqp data to {collection.Name}." ); - // EqpDataChanged = true; - //} - // - //private void RestoreEqp() - //{ - // if( !EqpDataChanged ) - // { - // return; - // } - // - // _resident.CharacterUtility->EqpResource->SetData( new ReadOnlySpan< byte >( ( void* )DefaultEqpData, DefaultEqpLength ) ); - // PluginLog.Information( $"Changed eqp data back." ); - // EqpDataChanged = false; - //} - // - //private void SetupModelAttributesDetour( IntPtr drawObject ) - //{ - // ChangeEqp( drawObject ); - // SetupModelAttributesHook!.Original( drawObject ); - // RestoreEqp(); - //} - // - //private void UpdateModelsDetour( IntPtr drawObject ) - //{ - // if( *( int* )( drawObject + 0x90c ) == 0 ) - // { - // return; - // } - // - // ChangeEqp( drawObject ); - // UpdateModelsHook!.Original.Invoke( drawObject ); - // RestoreEqp(); - //} - // - //private void SetupConnectorModelAttributesDetour( IntPtr drawObject, IntPtr unk ) - //{ - // ChangeEqp( drawObject ); - // SetupConnectorModelAttributesHook!.Original.Invoke( drawObject, unk ); - // RestoreEqp(); - //} - // - //private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) - //{ - // _lastGameObject = ( GameObject* )gameObject; - // EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); - // _lastGameObject = null; - //} - // - //private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) - //{ - // var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); - // if( _lastGameObject != null ) - // { - // _drawObjectToObject[ ret ] = _lastGameObject->ObjectIndex; - // } - // - // return ret; - //} - // - //private void CharacterBaseDestructorDetour( IntPtr drawBase ) - //{ - // _drawObjectToObject.Remove( drawBase ); - // CharacterBaseDestructorHook!.Original.Invoke( drawBase ); - //} - // - //private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) - //{ - // gameObject = ( GameObject* )( Dalamud.Objects[ gameObjectIdx ]?.Address ?? IntPtr.Zero ); - // if( gameObject != null && gameObject->DrawObject == ( DrawObject* )drawObject ) - // { - // return true; - // } - // - // _drawObjectToObject.Remove( drawObject ); - // return false; - //} - // - //private GameObject* FindParent( IntPtr drawObject ) - //{ - // if( _drawObjectToObject.TryGetValue( drawObject, out var gameObjectIdx ) ) - // { - // if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) ) - // { - // return gameObject; - // } - // - // _drawObjectToObject.Remove( drawObject ); - // } - // - // if( _lastGameObject != null && ( _lastGameObject->DrawObject == null || _lastGameObject->DrawObject == ( DrawObject* )drawObject ) ) - // { - // return _lastGameObject; - // } - // - // return null; - //} - // - //private void SetCollection( Utf8String path, ModCollection? collection ) - //{ - // if( collection == null ) - // { - // _pathCollections.Remove( path ); - // } - // else if( _pathCollections.ContainsKey( path ) ) - // { - // _pathCollections[ path ] = collection; - // } - // else - // { - // _pathCollections[ path.Clone() ] = collection; - // } - //} - // - //private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) - //{ - // if( mtrlResourceHandle == IntPtr.Zero ) - // { - // return; - // } - // - // var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); - // if( numTex == 0 ) - // { - // return; - // } - // - // var handle = ( Structs.ResourceHandle* )mtrlResourceHandle; - // var mtrlName = Utf8String.FromSpanUnsafe( handle->FileNameSpan(), true, null, true ); - // var collection = _pathCollections.TryGetValue( mtrlName, out var c ) ? c : null; - // var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); - // for( var i = 0; i < numTex; ++i ) - // { - // var texStringPtr = ( byte* )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); - // var texString = new Utf8String( texStringPtr ); - // SetCollection( texString, collection ); - // } - //} - // - //private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) - //{ - // LoadMtrlTexHelper( mtrlResourceHandle ); - // return LoadMtrlTexHook!.Original( mtrlResourceHandle ); - //} - // - //private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) - // => LoadMtrlShpkHook!.Original( mtrlResourceHandle ); - // - //private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) - //{ - // if( path == IntPtr.Zero ) - // { - // return path; - // } - // - // var p = new Utf8String( ( byte* )path ); - // - // var parent = FindParent( drawObject ); - // if( parent == null ) - // { - // return path; - // } - // - // var name = new Utf8String( parent->Name ); - // if( name.Length == 0 ) - // { - // return path; - // } - // - // var charName = name.ToString(); - // var gamePath = new Utf8String( ( byte* )path ); - // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) - // { - // SetCollection( gamePath, null ); - // return path; - // } - // - // SetCollection( gamePath, collection ); - // return path; - //} - // - //private IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - //{ - // ChangeEqp( drawObject ); - // var ret = ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); - // RestoreEqp(); - // return ret; - //} - // - //private IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - // => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - // - //private IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) - // => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - // - //public PathResolver( ResourceLoader loader, ResidentResourceManager resident ) - //{ - // _loader = loader; - // _resident = resident; - // SignatureHelper.Initialise( this ); - // var data = _resident.CharacterUtility->EqpResource->GetData(); - // fixed( byte* ptr = data ) - // { - // DefaultEqpData = ( IntPtr )ptr; - // } - // - // DefaultEqpLength = data.Length; - // Enable(); - // foreach( var gameObject in Dalamud.Objects ) - // { - // var ptr = ( GameObject* )gameObject.Address; - // if( ptr->IsCharacter() && ptr->DrawObject != null ) - // { - // _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ptr->ObjectIndex; - // } - // } - //} - // - // - //private (FullPath?, object?) CharacterReplacer( NewGamePath path ) - //{ - // var modManager = Service< ModManager >.Get(); - // var gamePath = new GamePath( path.ToString() ); - // var nonDefault = _pathCollections.TryGetValue( path.Path, out var collection ); - // if( !nonDefault ) - // { - // collection = modManager.Collections.DefaultCollection; - // } - // - // var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); - // if( resolved == null ) - // { - // resolved = modManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); - // if( resolved == null ) - // { - // return ( null, collection ); - // } - // - // collection = modManager.Collections.ForcedCollection; - // } - // - // var fullPath = new FullPath( resolved ); - // if( nonDefault ) - // { - // SetCollection( fullPath.InternalName, nonDefault ? collection : null ); - // } - // - // return ( fullPath, collection ); - //} - // - //public void Enable() - //{ - // ResolveMdlPathHook?.Enable(); - // ResolveMtrlPathHook?.Enable(); - // ResolveImcPathHook?.Enable(); - // LoadMtrlTexHook?.Enable(); - // LoadMtrlShpkHook?.Enable(); - // EnableDrawHook?.Enable(); - // CharacterBaseCreateHook?.Enable(); - // _loader.ResolvePath = CharacterReplacer; - // CharacterBaseDestructorHook?.Enable(); - // SetupConnectorModelAttributesHook?.Enable(); - // UpdateModelsHook?.Enable(); - // SetupModelAttributesHook?.Enable(); - // ApplyVisorStuffHook?.Enable(); - //} - // - //public void Disable() - //{ - // _loader.ResolvePath = ResourceLoader.DefaultReplacer; - // ResolveMdlPathHook?.Disable(); - // ResolveMtrlPathHook?.Disable(); - // ResolveImcPathHook?.Disable(); - // LoadMtrlTexHook?.Disable(); - // LoadMtrlShpkHook?.Disable(); - // EnableDrawHook?.Disable(); - // CharacterBaseCreateHook?.Disable(); - // CharacterBaseDestructorHook?.Disable(); - // SetupConnectorModelAttributesHook?.Disable(); - // UpdateModelsHook?.Disable(); - // SetupModelAttributesHook?.Disable(); - // ApplyVisorStuffHook?.Disable(); - //} - // - public void Dispose() - { - // Disable(); - // ResolveMdlPathHook?.Dispose(); - // ResolveMtrlPathHook?.Dispose(); - // ResolveImcPathHook?.Dispose(); - // LoadMtrlTexHook?.Dispose(); - // LoadMtrlShpkHook?.Dispose(); - // EnableDrawHook?.Dispose(); - // CharacterBaseCreateHook?.Dispose(); - // CharacterBaseDestructorHook?.Dispose(); - // SetupConnectorModelAttributesHook?.Dispose(); - // UpdateModelsHook?.Dispose(); - // SetupModelAttributesHook?.Dispose(); - // ApplyVisorStuffHook?.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs new file mode 100644 index 00000000..3df5bd1c --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Penumbra.GameData.ByteString; +using Penumbra.Mods; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + // Keep track of created DrawObjects that are CharacterBase, + // and use the last game object that called EnableDraw to link them. + public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + + [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] + public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; + + private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + { + using var cmp = MetaChanger.ChangeCmp( this, out var collection ); + var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); + if( LastGameObject != null ) + { + DrawObjectToObject[ ret ] = ( collection!, LastGameObject->ObjectIndex ); + } + + return ret; + } + + + // Remove DrawObjects from the list when they are destroyed. + public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + + [Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", + DetourName = "CharacterBaseDestructorDetour" )] + public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook; + + private void CharacterBaseDestructorDetour( IntPtr drawBase ) + { + DrawObjectToObject.Remove( drawBase ); + CharacterBaseDestructorHook!.Original.Invoke( drawBase ); + } + + + // EnableDraw is what creates DrawObjects for gameObjects, + // so we always keep track of the current GameObject to be able to link it to the DrawObject. + public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + + [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0" )] + public Hook< EnableDrawDelegate >? EnableDrawHook; + + private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + { + LastGameObject = ( GameObject* )gameObject; + EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); + LastGameObject = null; + } + + private void EnableDataHooks() + { + CharacterBaseCreateHook?.Enable(); + EnableDrawHook?.Enable(); + CharacterBaseDestructorHook?.Enable(); + } + + private void DisableDataHooks() + { + CharacterBaseCreateHook?.Disable(); + EnableDrawHook?.Disable(); + CharacterBaseDestructorHook?.Disable(); + } + + private void DisposeDataHooks() + { + CharacterBaseCreateHook?.Dispose(); + EnableDrawHook?.Dispose(); + CharacterBaseDestructorHook?.Dispose(); + } + + + // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. + // It contains any DrawObjects that correspond to a human actor, even those without specific collections. + internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); + + // This map links files to their corresponding collection, if it is non-default. + internal readonly Dictionary< Utf8String, ModCollection > PathCollections = new(); + + internal GameObject* LastGameObject = null; + + // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. + private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + { + var tmp = Dalamud.Objects[ gameObjectIdx ]; + if( tmp != null ) + { + gameObject = ( GameObject* )tmp.Address; + if( gameObject->DrawObject == ( DrawObject* )drawObject ) + { + return true; + } + } + + gameObject = null; + DrawObjectToObject.Remove( drawObject ); + return false; + } + + // Obtain the name of the current player, if one exists. + private static string? GetPlayerName() + => Dalamud.Objects[ 0 ]?.Name.ToString(); + + // Obtain the name of the inspect target from its window, if it exists. + private static string? GetInspectName() + { + var addon = Dalamud.GameGui.GetAddonByName( "CharacterInspect", 1 ); + if( addon == IntPtr.Zero ) + { + return null; + } + + var ui = ( AtkUnitBase* )addon; + if( ui->UldManager.NodeListCount < 60 ) + { + return null; + } + + var text = ( AtkTextNode* )ui->UldManager.NodeList[ 60 ]; + return text != null ? text->NodeText.ToString() : null; + } + + // Guesstimate whether an unnamed cutscene actor corresponds to the player or not, + // and if so, return the player name. + private static string? GetCutsceneName( GameObject* gameObject ) + { + if( gameObject->Name[ 0 ] != 0 || gameObject->ObjectKind != ( byte )ObjectKind.Player ) + { + return null; + } + + var player = Dalamud.Objects[ 0 ]; + if( player == null ) + { + return null; + } + + var pc = ( Character* )player.Address; + return pc->ClassJob == ( ( Character* )gameObject )->ClassJob ? player.Name.ToString() : null; + } + + // Identify the correct collection for a GameObject by index and name. + private static ModCollection IdentifyCollection( GameObject* gameObject ) + { + if( gameObject == null ) + { + return Penumbra.ModManager.Collections.DefaultCollection; + } + + var name = gameObject->ObjectIndex switch + { + 240 => GetPlayerName(), // character window + 241 => GetInspectName(), // inspect + 242 => GetPlayerName(), // try-on + >= 200 => GetCutsceneName( gameObject ), + _ => null, + } + ?? new Utf8String( gameObject->Name ).ToString(); + + return Penumbra.ModManager.Collections.CharacterCollection.TryGetValue( name, out var col ) + ? col + : Penumbra.ModManager.Collections.DefaultCollection; + } + + // Update collections linked to Game/DrawObjects due to a change in collection configuration. + private void CheckCollections() + { + foreach( var (key, (_, idx)) in DrawObjectToObject.ToArray() ) + { + if( !VerifyEntry( key, idx, out var obj ) ) + { + DrawObjectToObject.Remove( key ); + } + + var collection = IdentifyCollection( obj ); + DrawObjectToObject[ key ] = ( collection, idx ); + } + } + + // Use the stored information to find the GameObject and Collection linked to a DrawObject. + private GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) + { + if( DrawObjectToObject.TryGetValue( drawObject, out var data ) ) + { + var gameObjectIdx = data.Item2; + if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) ) + { + collection = data.Item1; + return gameObject; + } + } + + if( LastGameObject != null && ( LastGameObject->DrawObject == null || LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + { + collection = IdentifyCollection( LastGameObject ); + return LastGameObject; + } + + + collection = IdentifyCollection( null ); + return null; + } + + + // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. + private void SetCollection( Utf8String path, ModCollection? collection ) + { + if( collection == null ) + { + PathCollections.Remove( path ); + } + else if( PathCollections.ContainsKey( path ) || path.IsOwned ) + { + PathCollections[ path ] = collection; + } + else + { + PathCollections[ path.Clone() ] = collection; + } + } + + // Find all current DrawObjects used in the GameObject table. + private void InitializeDrawObjects() + { + foreach( var gameObject in Dalamud.Objects ) + { + var ptr = ( GameObject* )gameObject.Address; + if( ptr->IsCharacter() && ptr->DrawObject != null ) + { + DrawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Human.cs b/Penumbra/Interop/Resolver/PathResolver.Human.cs new file mode 100644 index 00000000..2acba0f6 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Human.cs @@ -0,0 +1,128 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop.Resolver; + +// We can hook the different Resolve-Functions using just the VTable of Human. +// The other DrawObject VTables and the ResolveRoot function are currently unused. +public unsafe partial class PathResolver +{ + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )] + public IntPtr* DrawObjectHumanVTable; + + // [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )] + // public IntPtr* DrawObjectVTable; + // + // [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] + // public IntPtr* DrawObjectDemihumanVTable; + // + // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] + // public IntPtr* DrawObjectMonsterVTable; + // + // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", + // ScanType = ScanType.StaticAddress )] + // public IntPtr* DrawObjectWeaponVTable; + // + // public const int ResolveRootIdx = 71; + + public const int ResolveSklbIdx = 72; + public const int ResolveMdlIdx = 73; + public const int ResolveSkpIdx = 74; + public const int ResolvePhybIdx = 75; + public const int ResolvePapIdx = 76; + public const int ResolveTmbIdx = 77; + public const int ResolveMPapIdx = 79; + public const int ResolveImcIdx = 81; + public const int ResolveMtrlIdx = 82; + public const int ResolveDecalIdx = 83; + public const int ResolveVfxIdx = 84; + public const int ResolveEidIdx = 85; + + public const int OnModelLoadCompleteIdx = 58; + + public delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + public delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ); + public delegate IntPtr MaterialResolveDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ); + public delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 ); + + public delegate void OnModelLoadCompleteDelegate( IntPtr drawObject ); + + public Hook< GeneralResolveDelegate >? ResolveDecalPathHook; + public Hook< EidResolveDelegate >? ResolveEidPathHook; + public Hook< GeneralResolveDelegate >? ResolveImcPathHook; + public Hook< MPapResolveDelegate >? ResolveMPapPathHook; + public Hook< GeneralResolveDelegate >? ResolveMdlPathHook; + public Hook< MaterialResolveDetour >? ResolveMtrlPathHook; + public Hook< MaterialResolveDetour >? ResolvePapPathHook; + public Hook< GeneralResolveDelegate >? ResolvePhybPathHook; + public Hook< GeneralResolveDelegate >? ResolveSklbPathHook; + public Hook< GeneralResolveDelegate >? ResolveSkpPathHook; + public Hook< EidResolveDelegate >? ResolveTmbPathHook; + public Hook< MaterialResolveDetour >? ResolveVfxPathHook; + + + private void SetupHumanHooks() + { + ResolveDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour ); + ResolveEidPathHook = new Hook< EidResolveDelegate >( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour ); + ResolveImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour ); + ResolveMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour ); + ResolveMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour ); + ResolveMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour ); + ResolvePapPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour ); + ResolvePhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour ); + ResolveSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour ); + ResolveSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour ); + ResolveTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour ); + ResolveVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour ); + } + + private void EnableHumanHooks() + { + ResolveDecalPathHook?.Enable(); + ResolveEidPathHook?.Enable(); + ResolveImcPathHook?.Enable(); + ResolveMPapPathHook?.Enable(); + ResolveMdlPathHook?.Enable(); + ResolveMtrlPathHook?.Enable(); + ResolvePapPathHook?.Enable(); + ResolvePhybPathHook?.Enable(); + ResolveSklbPathHook?.Enable(); + ResolveSkpPathHook?.Enable(); + ResolveTmbPathHook?.Enable(); + ResolveVfxPathHook?.Enable(); + } + + private void DisableHumanHooks() + { + ResolveDecalPathHook?.Disable(); + ResolveEidPathHook?.Disable(); + ResolveImcPathHook?.Disable(); + ResolveMPapPathHook?.Disable(); + ResolveMdlPathHook?.Disable(); + ResolveMtrlPathHook?.Disable(); + ResolvePapPathHook?.Disable(); + ResolvePhybPathHook?.Disable(); + ResolveSklbPathHook?.Disable(); + ResolveSkpPathHook?.Disable(); + ResolveTmbPathHook?.Disable(); + ResolveVfxPathHook?.Disable(); + } + + private void DisposeHumanHooks() + { + ResolveDecalPathHook?.Dispose(); + ResolveEidPathHook?.Dispose(); + ResolveImcPathHook?.Dispose(); + ResolveMPapPathHook?.Dispose(); + ResolveMdlPathHook?.Dispose(); + ResolveMtrlPathHook?.Dispose(); + ResolvePapPathHook?.Dispose(); + ResolvePhybPathHook?.Dispose(); + ResolveSklbPathHook?.Dispose(); + ResolveSkpPathHook?.Dispose(); + ResolveTmbPathHook?.Dispose(); + ResolveVfxPathHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs new file mode 100644 index 00000000..8afffdba --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -0,0 +1,91 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Resolver; + +// Materials do contain their own paths to textures and shader packages. +// Those are loaded synchronously. +// Thus, we need to ensure the correct files are loaded when a material is loaded. +public unsafe partial class PathResolver +{ + public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); + + [Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour" )] + public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; + + private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + { + LoadMtrlTexHelper( mtrlResourceHandle ); + return LoadMtrlTexHook!.Original( mtrlResourceHandle ); + } + + [Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", + DetourName = "LoadMtrlShpkDetour" )] + public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook; + + private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) + { + LoadMtrlShpkHelper( mtrlResourceHandle ); + return LoadMtrlShpkHook!.Original( mtrlResourceHandle ); + } + + // Taken from the actual hooked function. The Shader string is just concatenated to this base directory. + private static readonly Utf8String ShaderBase = Utf8String.FromStringUnsafe( "shader/sm5/shpk", true ); + + private void LoadMtrlShpkHelper( IntPtr mtrlResourceHandle ) + { + if( mtrlResourceHandle == IntPtr.Zero ) + { + return; + } + + var mtrl = ( MtrlResource* )mtrlResourceHandle; + var shpkPath = Utf8String.Join( ( byte )'/', ShaderBase, new Utf8String( mtrl->ShpkString ).AsciiToLower() ); + var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + var collection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; + SetCollection( shpkPath, collection ); + } + + private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) + { + if( mtrlResourceHandle == IntPtr.Zero ) + { + return; + } + + var mtrl = ( MtrlResource* )mtrlResourceHandle; + if( mtrl->NumTex == 0 ) + { + return; + } + + var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + var collection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; + for( var i = 0; i < mtrl->NumTex; ++i ) + { + var texString = new Utf8String( mtrl->TexString( i ) ); + SetCollection( texString, collection ); + } + } + + private void EnableMtrlHooks() + { + LoadMtrlShpkHook?.Enable(); + LoadMtrlTexHook?.Enable(); + } + + private void DisableMtrlHooks() + { + LoadMtrlShpkHook?.Disable(); + LoadMtrlTexHook?.Disable(); + } + + private void DisposeMtrlHooks() + { + LoadMtrlShpkHook?.Dispose(); + LoadMtrlTexHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs new file mode 100644 index 00000000..2ae74559 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -0,0 +1,274 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.Interop.Resolver; + +// State: 6.08 Hotfix. +// GetSlotEqpData seems to be the only function using the EQP table. +// It is only called by CheckSlotsForUnload (called by UpdateModels), +// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete) +// and a unnamed function called by UpdateRender. +// It seems to be enough to change the EQP entries for UpdateModels. + +// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables. +// They are called by ResolveMdlPath, UpdateModels and SetupConnectorModelAttributes, +// which is called by SetupModelAttributes, which is called by OnModelLoadComplete and UpdateModels. +// It seems to be enough to change EQDP on UpdateModels and ResolveMDLPath. + +// EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by +// ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath. + +// RSP entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", or maybe "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", +// possibly also "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" +// which is called by a lot of functions, but the mostly relevant is probably Human.SetupFromCharacterData, which is only called by CharacterBase.Create. + +// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. + +public unsafe partial class PathResolver +{ + public delegate void UpdateModelDelegate( IntPtr drawObject ); + + [Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )] + public Hook< UpdateModelDelegate >? UpdateModelsHook; + + private void UpdateModelsDetour( IntPtr drawObject ) + { + // Shortcut because this is called all the time. + // Same thing is checked at the beginning of the original function. + if( *( int* )( drawObject + 0x90c ) == 0 ) + { + return; + } + + var collection = GetCollection( drawObject ); + if( collection != null ) + { + using var eqp = MetaChanger.ChangeEqp( collection ); + using var eqdp = MetaChanger.ChangeEqdp( collection ); + UpdateModelsHook!.Original.Invoke( drawObject ); + } + else + { + UpdateModelsHook!.Original.Invoke( drawObject ); + } + } + + [Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", + DetourName = "GetEqpIndirectDetour" )] + public Hook< OnModelLoadCompleteDelegate >? GetEqpIndirectHook; + + private void GetEqpIndirectDetour( IntPtr drawObject ) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) + { + return; + } + + using var eqp = MetaChanger.ChangeEqp( this, drawObject ); + GetEqpIndirectHook!.Original( drawObject ); + } + + public Hook< OnModelLoadCompleteDelegate >? OnModelLoadCompleteHook; + + private void OnModelLoadCompleteDetour( IntPtr drawObject ) + { + var collection = GetCollection( drawObject ); + if( collection != null ) + { + using var eqp = MetaChanger.ChangeEqp( collection ); + using var eqdp = MetaChanger.ChangeEqdp( collection ); + OnModelLoadCompleteHook!.Original.Invoke( drawObject ); + } + else + { + OnModelLoadCompleteHook!.Original.Invoke( drawObject ); + } + } + + // GMP + public delegate void ApplyVisorDelegate( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ); + + [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "ApplyVisorDetour" )] + public Hook< ApplyVisorDelegate >? ApplyVisorHook; + + private void ApplyVisorDetour( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ) + { + using var gmp = MetaChanger.ChangeGmp( this, drawObject ); + ApplyVisorHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); + } + + private void SetupMetaHooks() + { + OnModelLoadCompleteHook = + new Hook< OnModelLoadCompleteDelegate >( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour ); + } + + private void EnableMetaHooks() + { +#if USE_EQP + GetEqpIndirectHook?.Enable(); +#endif +#if USE_EQP || USE_EQDP + UpdateModelsHook?.Enable(); + OnModelLoadCompleteHook?.Enable(); +#endif +#if USE_GMP + ApplyVisorHook?.Enable(); +#endif + } + + private void DisableMetaHooks() + { + GetEqpIndirectHook?.Disable(); + UpdateModelsHook?.Disable(); + OnModelLoadCompleteHook?.Disable(); + ApplyVisorHook?.Disable(); + } + + private void DisposeMetaHooks() + { + GetEqpIndirectHook?.Dispose(); + UpdateModelsHook?.Dispose(); + OnModelLoadCompleteHook?.Dispose(); + ApplyVisorHook?.Dispose(); + } + + private ModCollection? GetCollection( IntPtr drawObject ) + { + var parent = FindParent( drawObject, out var collection ); + if( parent == null || collection == Penumbra.ModManager.Collections.DefaultCollection ) + { + return null; + } + + return collection.Cache == null ? Penumbra.ModManager.Collections.ForcedCollection : collection; + } + + + // Small helper to handle setting metadata and reverting it at the end of the function. + private readonly struct MetaChanger : IDisposable + { + private readonly MetaManipulation.Type _type; + + private MetaChanger( MetaManipulation.Type type ) + => _type = type; + + public static MetaChanger ChangeEqp( ModCollection collection ) + { +#if USE_EQP + collection.SetEqpFiles(); + return new MetaChanger( MetaManipulation.Type.Eqp ); +#else + return new MetaChanger( MetaManipulation.Type.Unknown ); +#endif + } + + public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject ) + { +#if USE_EQP + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + return ChangeEqp( collection ); + } +#endif + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + + public static MetaChanger ChangeEqdp( PathResolver resolver, IntPtr drawObject ) + { +#if USE_EQDP + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + return ChangeEqdp( collection ); + } +#endif + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + + public static MetaChanger ChangeEqdp( ModCollection collection ) + { +#if USE_EQDP + collection.SetEqdpFiles(); + return new MetaChanger( MetaManipulation.Type.Eqdp ); +#else + return new MetaChanger( MetaManipulation.Type.Unknown ); +#endif + } + + public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) + { +#if USE_GMP + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + collection.SetGmpFiles(); + return new MetaChanger( MetaManipulation.Type.Gmp ); + } +#endif + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + + public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) + { +#if USE_EST + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + collection.SetEstFiles(); + return new MetaChanger( MetaManipulation.Type.Est ); + } +#endif + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + + public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) + { + if( resolver.LastGameObject != null ) + { + collection = IdentifyCollection( resolver.LastGameObject ); +#if USE_CMP + if( collection != Penumbra.ModManager.Collections.DefaultCollection && collection.Cache != null ) + { + collection.SetCmpFiles(); + return new MetaChanger( MetaManipulation.Type.Rsp ); + } +#endif + } + else + { + collection = null; + } + + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + + public void Dispose() + { + switch( _type ) + { + case MetaManipulation.Type.Eqdp: + Penumbra.ModManager.Collections.DefaultCollection.SetEqdpFiles(); + break; + case MetaManipulation.Type.Eqp: + Penumbra.ModManager.Collections.DefaultCollection.SetEqpFiles(); + break; + case MetaManipulation.Type.Est: + Penumbra.ModManager.Collections.DefaultCollection.SetEstFiles(); + break; + case MetaManipulation.Type.Gmp: + Penumbra.ModManager.Collections.DefaultCollection.SetGmpFiles(); + break; + case MetaManipulation.Type.Rsp: + Penumbra.ModManager.Collections.DefaultCollection.SetCmpFiles(); + break; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs new file mode 100644 index 00000000..afc6b2c0 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.CompilerServices; +using Penumbra.GameData.ByteString; +using Penumbra.Mods; + +namespace Penumbra.Interop.Resolver; + +// The actual resolve detours are basically all the same. +public unsafe partial class PathResolver +{ + private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolvePathDetour( drawObject, ResolveMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var eqdp = MetaChanger.ChangeEqdp( this, drawObject ); + return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolvePapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + { + using var est = MetaChanger.ChangeEst( this, drawObject ); + return ResolvePathDetour( drawObject, ResolvePapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + } + + private IntPtr ResolvePhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( this, drawObject ); + return ResolvePathDetour( drawObject, ResolvePhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( this, drawObject ); + return ResolvePathDetour( drawObject, ResolveSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( this, drawObject ); + return ResolvePathDetour( drawObject, ResolveSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveTmbPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) + => ResolvePathDetour( FindParent( drawObject, out var collection ) == null + ? Penumbra.ModManager.Collections.DefaultCollection + : collection, path ); + + + // Just add or remove the resolved path. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path ) + { + if( path == IntPtr.Zero ) + { + return path; + } + + var gamePath = new Utf8String( ( byte* )path ); + if( collection == Penumbra.ModManager.Collections.DefaultCollection ) + { + SetCollection( gamePath, null ); + return path; + } + + SetCollection( gamePath, collection ); + return path; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs new file mode 100644 index 00000000..da0ae9b1 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -0,0 +1,99 @@ +using System; +using Dalamud.Utility.Signatures; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Loader; + +namespace Penumbra.Interop.Resolver; + +// The Path Resolver handles character collections. +// It will hook any path resolving functions for humans, +// as well as DrawObject creation. +// It links draw objects to actors, and actors to character collections, +// to resolve paths for character collections. +public partial class PathResolver : IDisposable +{ + private readonly ResourceLoader _loader; + + // Keep track of the last path resolver to be able to restore it. + private Func< Utf8GamePath, (FullPath?, object?) > _oldResolver = null!; + + public PathResolver( ResourceLoader loader ) + { + _loader = loader; + SignatureHelper.Initialise( this ); + SetupHumanHooks(); + SetupMetaHooks(); + Enable(); + } + + // The modified resolver that handles game path resolving. + private (FullPath?, object?) CharacterResolver( Utf8GamePath gamePath ) + { + // Check if the path was marked for a specific collection, if not use the default collection. + var nonDefault = PathCollections.TryGetValue( gamePath.Path, out var collection ); + if( !nonDefault ) + { + collection = Penumbra.ModManager.Collections.DefaultCollection; + } + else + { + // We can remove paths after they have actually been loaded. + // A potential next request will add the path anew. + PathCollections.Remove( gamePath.Path ); + } + + // Resolve using character/default collection first, otherwise forced, as usual. + var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); + if( resolved == null ) + { + resolved = Penumbra.ModManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); + if( resolved == null ) + { + return ( null, collection ); + } + + collection = Penumbra.ModManager.Collections.ForcedCollection; + } + + // 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. + if( nonDefault && resolved.Value.Extension == ".mtrl" ) + { + SetCollection( resolved.Value.InternalName, nonDefault ? collection : null ); + } + + return ( resolved, collection ); + } + + public void Enable() + { + InitializeDrawObjects(); + + EnableHumanHooks(); + EnableMtrlHooks(); + EnableDataHooks(); + EnableMetaHooks(); + + _oldResolver = _loader.ResolvePath; + _loader.ResolvePath = CharacterResolver; + } + + public void Disable() + { + DisableHumanHooks(); + DisableMtrlHooks(); + DisableDataHooks(); + DisableMetaHooks(); + + _loader.ResolvePath = _oldResolver; + } + + public void Dispose() + { + Disable(); + DisposeHumanHooks(); + DisposeMtrlHooks(); + DisposeDataHooks(); + DisposeMetaHooks(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs new file mode 100644 index 00000000..ff5b6abf --- /dev/null +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct MtrlResource +{ + [FieldOffset( 0x00 )] + public ResourceHandle Handle; + + [FieldOffset( 0xD0 )] + public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + + [FieldOffset( 0xE0 )] + public byte* StringList; + + [FieldOffset( 0xF8 )] + public ushort ShpkOffset; + + [FieldOffset( 0xFA )] + public byte NumTex; + + public byte* ShpkString + => StringList + ShpkOffset; + + public byte* TexString( int idx ) + => StringList + *( TexSpace + 4 + idx * 8 ); +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index e54cc3e4..bdb5c10b 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -1,3 +1,4 @@ +using System; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; @@ -6,14 +7,16 @@ using System.Collections.Generic; namespace Penumbra.Meta.Files; +// The human.cmp file contains many character-relevant parameters like color sets. +// We only support manipulating the racial scaling parameters at the moment. public sealed unsafe class CmpFile : MetaBaseFile { private const int RacialScalingStart = 0x2A800; public float this[ SubRace subRace, RspAttribute attribute ] { - get => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ); - set => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ) = value; + get => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); + set => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ) = value; } public override void Reset() @@ -37,6 +40,29 @@ public sealed unsafe class CmpFile : MetaBaseFile public static float GetDefault( SubRace subRace, RspAttribute attribute ) { var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ CharacterUtility.HumanCmpIdx ].Address; - return *( float* )( data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ); + return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); } + + private static int ToRspIndex( SubRace subRace ) + => subRace switch + { + SubRace.Midlander => 0, + SubRace.Highlander => 1, + SubRace.Wildwood => 10, + SubRace.Duskwight => 11, + SubRace.Plainsfolk => 20, + SubRace.Dunesfolk => 21, + SubRace.SeekerOfTheSun => 30, + SubRace.KeeperOfTheMoon => 31, + SubRace.Seawolf => 40, + SubRace.Hellsguard => 41, + SubRace.Raen => 50, + SubRace.Xaela => 51, + SubRace.Helion => 60, + SubRace.Lost => 61, + SubRace.Rava => 70, + SubRace.Veena => 71, + SubRace.Unknown => 0, + _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), + }; } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index d073631a..6cfa47b8 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -18,6 +18,14 @@ public partial class MetaManager public MetaManagerCmp() { } + [Conditional( "USE_CMP" )] + public void SetFiles() + => SetFile( File, CharacterUtility.HumanCmpIdx ); + + [Conditional( "USE_CMP" )] + public static void ResetFiles() + => SetFile( null, CharacterUtility.HumanCmpIdx ); + [Conditional( "USE_CMP" )] public void Reset() { @@ -30,10 +38,6 @@ public partial class MetaManager Manipulations.Clear(); } - [Conditional( "USE_CMP" )] - public void SetFiles() - => SetFile( File, CharacterUtility.HumanCmpIdx ); - public bool ApplyMod( RspManipulation m, Mod.Mod mod ) { #if USE_CMP diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 608c77d4..8edf1150 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -29,6 +29,15 @@ public partial class MetaManager } } + [Conditional( "USE_EQDP" )] + public static void ResetFiles() + { + foreach( var idx in CharacterUtility.EqdpIndices ) + { + SetFile( null, idx ); + } + } + [Conditional( "USE_EQDP" )] public void Reset() { diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 65c38ef5..dd7c63ff 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -22,6 +22,10 @@ public partial class MetaManager public void SetFiles() => SetFile( File, CharacterUtility.EqpIdx ); + [Conditional( "USE_EQP" )] + public static void ResetFiles() + => SetFile( null, CharacterUtility.EqpIdx ); + [Conditional( "USE_EQP" )] public void Reset() { diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index 558873cc..f53d145d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -30,6 +30,15 @@ public partial class MetaManager SetFile( HeadFile, CharacterUtility.HeadEstIdx ); } + [Conditional( "USE_EST" )] + public static void ResetFiles() + { + SetFile( null, CharacterUtility.FaceEstIdx ); + SetFile( null, CharacterUtility.HairEstIdx ); + SetFile( null, CharacterUtility.BodyEstIdx ); + SetFile( null, CharacterUtility.HeadEstIdx ); + } + [Conditional( "USE_EST" )] public void Reset() { diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 8daf8dd8..d281bb37 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -18,6 +18,15 @@ public partial class MetaManager public MetaManagerGmp() { } + + [Conditional( "USE_GMP" )] + public void SetFiles() + => SetFile( File, CharacterUtility.GmpIdx ); + + [Conditional( "USE_GMP" )] + public static void ResetFiles() + => SetFile( null, CharacterUtility.GmpIdx ); + [Conditional( "USE_GMP" )] public void Reset() { @@ -28,10 +37,6 @@ public partial class MetaManager } } - [Conditional( "USE_GMP" )] - public void SetFiles() - => SetFile( File, CharacterUtility.GmpIdx ); - public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) { #if USE_GMP diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index e2457668..8c4fb7c2 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; -using Penumbra.Interop; +using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -26,6 +26,7 @@ public partial class MetaManager { _collection = collection; _previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization; + SetupDelegate(); } [Conditional( "USE_IMC" )] @@ -93,7 +94,6 @@ public partial class MetaManager #endif } - public void Dispose() { foreach( var file in Files.Values ) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 3eef46fd..d0932ae4 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -14,7 +15,8 @@ public partial class MetaManager : IDisposable public MetaManagerCmp Cmp = new(); public MetaManagerImc Imc; - private static unsafe void SetFile( MetaBaseFile? file, int index ) + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + public static unsafe void SetFile( MetaBaseFile? file, int index ) { if( file == null ) { diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 90a96dd1..0408367a 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -1,11 +1,14 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Manager; using Penumbra.Mod; using Penumbra.Util; @@ -252,5 +255,83 @@ public class ModCollection public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); + + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( Cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + Cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( Cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + Cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( Cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + Cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( Cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + Cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( Cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + Cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( Cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + Cache.MetaManipulations.SetFiles(); + } + } + public static readonly ModCollection Empty = new() { Name = "" }; } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 930c72c7..cc9cc5e2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -9,12 +9,13 @@ using Lumina.Excel.GeneratedSheets; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; -using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.PlayerWatch; using Penumbra.UI; using Penumbra.Util; using System.Linq; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Meta.Manipulations; namespace Penumbra; @@ -44,7 +45,7 @@ public class Penumbra : IDalamudPlugin public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } - //public PathResolver PathResolver { get; } + public PathResolver PathResolver { get; } public SettingsInterface SettingsInterface { get; } public MusicManager MusicManager { get; } public ObjectReloader ObjectReloader { get; } @@ -75,7 +76,7 @@ public class Penumbra : IDalamudPlugin ModManager = new ModManager(); ModManager.DiscoverMods(); ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); - //PathResolver = new PathResolver( ResourceLoader, gameUtils ); + PathResolver = new PathResolver( ResourceLoader ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -235,7 +236,7 @@ public class Penumbra : IDalamudPlugin Dalamud.Commands.RemoveHandler( CommandName ); - //PathResolver.Dispose(); + PathResolver.Dispose(); ResourceLogger.Dispose(); ResourceLoader.Dispose(); CharacterUtility.Dispose(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 13a25cc0..84c1b98d 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -79,4 +79,8 @@ Always + + + + \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index b353192f..8f6fd22a 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -398,8 +398,10 @@ public partial class SettingsInterface ImGui.InputInt( "##EqdpInput", ref eqdp ); try { - var def = ExpandedEqdpFile.GetDefault(GenderRace.MidlanderMale, false, eqdp ); - var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqdp.File(GenderRace.MidlanderMale, false)?[eqdp] ?? def; + var def = ExpandedEqdpFile.GetDefault( GenderRace.MidlanderMale, false, eqdp ); + var val = + Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqdp.File( GenderRace.MidlanderMale, false )?[ eqdp ] + ?? def; ImGui.Text( Convert.ToString( ( ushort )def, 2 ).PadLeft( 16, '0' ) ); ImGui.Text( Convert.ToString( ( ushort )val, 2 ).PadLeft( 16, '0' ) ); } @@ -454,41 +456,43 @@ public partial class SettingsInterface return; } - //if( ImGui.TreeNodeEx( "Draw Object to Object" ) ) - //{ - // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - // if( ImGui.BeginTable( "###DrawObjectResolverTable", 4, ImGuiTableFlags.SizingFixedFit ) ) - // { - // end.Push( ImGui.EndTable ); - // foreach( var (ptr, idx) in _penumbra.PathResolver._drawObjectToObject ) - // { - // ImGui.TableNextColumn(); - // ImGui.Text( ptr.ToString( "X" ) ); - // ImGui.TableNextColumn(); - // ImGui.Text( idx.ToString() ); - // ImGui.TableNextColumn(); - // ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); - // ImGui.TableNextColumn(); - // ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); - // } - // } - //} - // - //if( ImGui.TreeNodeEx( "Path Collections" ) ) - //{ - // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - // if( ImGui.BeginTable( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ) ) - // { - // end.Push( ImGui.EndTable ); - // foreach( var (path, collection) in _penumbra.PathResolver._pathCollections ) - // { - // ImGui.TableNextColumn(); - // ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); - // ImGui.TableNextColumn(); - // ImGui.Text( collection.Name ); - // } - // } - //} + if( ImGui.TreeNodeEx( "Draw Object to Object" ) ) + { + using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + if( ImGui.BeginTable( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ) ) + { + end.Push( ImGui.EndTable ); + foreach( var (ptr, (c, idx)) in _penumbra.PathResolver.DrawObjectToObject ) + { + ImGui.TableNextColumn(); + ImGui.Text( ptr.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( idx.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( c.Name ); + } + } + } + + if( ImGui.TreeNodeEx( "Path Collections" ) ) + { + using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + if( ImGui.BeginTable( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ) ) + { + end.Push( ImGui.EndTable ); + foreach( var (path, collection) in _penumbra.PathResolver.PathCollections ) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + ImGui.TableNextColumn(); + ImGui.Text( collection.Name ); + } + } + } } private void DrawDebugTab() diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index fa187a94..507d3fe7 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -1,14 +1,13 @@ using System; using System.Linq; using System.Numerics; -using System.Reflection.Metadata.Ecma335; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImGuiNET; using Penumbra.GameData.ByteString; -using Penumbra.Interop; +using Penumbra.Interop.Loader; using Penumbra.UI.Custom; namespace Penumbra.UI; From 8d2e84eecf5392b0c7db0decddf1ad49ed19d0d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Mar 2022 15:17:55 +0100 Subject: [PATCH 0096/2451] Some object reloading changes. --- Penumbra/Interop/ObjectReloader.cs | 718 ++++++++++++-------------- Penumbra/Interop/Structs/DrawState.cs | 14 + Penumbra/UI/MenuTabs/TabDebug.cs | 8 +- 3 files changed, 353 insertions(+), 387 deletions(-) create mode 100644 Penumbra/Interop/Structs/DrawState.cs diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 2b497874..5865602d 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -7,439 +7,389 @@ using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; using Penumbra.Mods; -namespace Penumbra.Interop +namespace Penumbra.Interop; + +public unsafe class ObjectReloader : IDisposable { - public class ObjectReloader : IDisposable + private delegate void ManipulateDraw( IntPtr actor ); + + private const int RenderModeOffset = 0x0104; + private const int UnloadAllRedrawDelay = 250; + private const uint NpcObjectId = unchecked( ( uint )-536870912 ); + public const int GPosePlayerIdx = 201; + public const int GPoseEndIdx = GPosePlayerIdx + 48; + + private readonly ModManager _mods; + private readonly Queue< (uint actorId, string name, RedrawType s) > _actorIds = new(); + + internal int DefaultWaitFrames; + + private int _waitFrames; + private int _currentFrame; + private bool _changedSettings; + private uint _currentObjectId = uint.MaxValue; + private DrawState _currentObjectStartState = 0; + private RedrawType _currentRedrawType = RedrawType.Unload; + private string? _currentObjectName; + private bool _wasTarget; + private bool _inGPose; + + public static DrawState* ActorDrawState( GameObject actor ) + => ( DrawState* )( actor.Address + 0x0104 ); + + private static delegate*< IntPtr, void > GetDisableDraw( GameObject actor ) + => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]; + + private static delegate*< IntPtr, void > GetEnableDraw( GameObject actor ) + => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]; + + public ObjectReloader( ModManager mods, int defaultWaitFrames ) { - private delegate void ManipulateDraw( IntPtr actor ); + _mods = mods; + DefaultWaitFrames = defaultWaitFrames; + } - [Flags] - public enum LoadingFlags : int + private void ChangeSettings() + { + if( _currentObjectName != null && _mods.Collections.CharacterCollection.TryGetValue( _currentObjectName, out var collection ) ) { - Invisibility = 0x00_00_00_02, - IsLoading = 0x00_00_08_00, - SomeNpcFlag = 0x00_00_01_00, - MaybeCulled = 0x00_00_04_00, - MaybeHiddenMinion = 0x00_00_80_00, - MaybeHiddenSummon = 0x00_80_00_00, + _changedSettings = true; + _mods.Collections.SetActiveCollection( collection, _currentObjectName ); } + } - private const int RenderModeOffset = 0x0104; - private const int UnloadAllRedrawDelay = 250; - private const uint NpcObjectId = unchecked( ( uint )-536870912 ); - public const int GPosePlayerIdx = 201; - public const int GPoseEndIdx = GPosePlayerIdx + 48; + private void RestoreSettings() + { + _mods.Collections.ResetActiveCollection(); + _changedSettings = false; + } - private readonly ModManager _mods; - private readonly Queue< (uint actorId, string name, RedrawType s) > _actorIds = new(); + private unsafe void WriteInvisible( GameObject actor, int actorIdx ) + { + _currentObjectStartState = *ActorDrawState( actor ); + *ActorDrawState( actor ) |= DrawState.Invisibility; - internal int DefaultWaitFrames; - - private int _waitFrames; - private int _currentFrame; - private bool _changedSettings; - private uint _currentObjectId = uint.MaxValue; - private LoadingFlags _currentObjectStartState = 0; - private RedrawType _currentRedrawType = RedrawType.Unload; - private string? _currentObjectName; - private bool _wasTarget; - private bool _inGPose; - - public static IntPtr RenderPtr( GameObject actor ) - => actor.Address + RenderModeOffset; - - public ObjectReloader( ModManager mods, int defaultWaitFrames ) + if( _inGPose ) { - _mods = mods; - DefaultWaitFrames = defaultWaitFrames; + GetDisableDraw( actor )( actor.Address ); } + } - private void ChangeSettings() + private bool StillLoading( DrawState* renderPtr ) + { + const DrawState stillLoadingFlags = DrawState.SomeNpcFlag + | DrawState.MaybeCulled + | DrawState.MaybeHiddenMinion + | DrawState.MaybeHiddenSummon; + + if( renderPtr != null ) { - if( _currentObjectName != null && _mods.Collections.CharacterCollection.TryGetValue( _currentObjectName, out var collection ) ) - { - _changedSettings = true; - _mods.Collections.SetActiveCollection( collection, _currentObjectName ); - } - } - - private void RestoreSettings() - { - _mods.Collections.ResetActiveCollection(); - _changedSettings = false; - } - - private unsafe void WriteInvisible( GameObject actor, int actorIdx ) - { - var renderPtr = RenderPtr( actor ); - if( renderPtr == IntPtr.Zero ) - { - return; - } - - _currentObjectStartState = *( LoadingFlags* )renderPtr; - *( LoadingFlags* )renderPtr |= LoadingFlags.Invisibility; - - if( _inGPose ) - { - var ptr = ( void*** )actor.Address; - var disableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 17 ] ) ); - disableDraw( actor.Address ); - } - } - - private unsafe bool StillLoading( IntPtr renderPtr ) - { - const LoadingFlags stillLoadingFlags = LoadingFlags.SomeNpcFlag - | LoadingFlags.MaybeCulled - | LoadingFlags.MaybeHiddenMinion - | LoadingFlags.MaybeHiddenSummon; - - if( renderPtr != IntPtr.Zero ) - { - var loadingFlags = *( LoadingFlags* )renderPtr; - if( loadingFlags == _currentObjectStartState ) - { - return false; - } - - return !( loadingFlags == 0 || ( loadingFlags & stillLoadingFlags ) != 0 ); - } - - return false; - } - - private unsafe void WriteVisible( GameObject actor, int actorIdx ) - { - var renderPtr = RenderPtr( actor ); - *( LoadingFlags* )renderPtr &= ~LoadingFlags.Invisibility; - - if( _inGPose ) - { - var ptr = ( void*** )actor.Address; - var enableDraw = Marshal.GetDelegateForFunctionPointer< ManipulateDraw >( new IntPtr( ptr[ 0 ][ 16 ] ) ); - enableDraw( actor.Address ); - } - } - - private bool CheckObject( GameObject actor ) - { - if( _currentObjectId != actor.ObjectId ) + var loadingFlags = *( DrawState* )renderPtr; + if( loadingFlags == _currentObjectStartState ) { return false; } - if( _currentObjectId != NpcObjectId ) - { - return true; - } - - return _currentObjectName == actor.Name.ToString(); + return !( loadingFlags == 0 || ( loadingFlags & stillLoadingFlags ) != 0 ); } - private bool CheckObjectGPose( GameObject actor ) - => actor.ObjectId == NpcObjectId && _currentObjectName == actor.Name.ToString(); + return false; + } - private (GameObject?, int) FindCurrentObject() + private void WriteVisible( GameObject actor, int actorIdx ) + { + *ActorDrawState( actor ) &= ~DrawState.Invisibility; + + if( _inGPose ) { - if( _inGPose ) + GetEnableDraw( actor )( actor.Address ); + } + } + + private bool CheckObject( GameObject actor ) + { + if( _currentObjectId != actor.ObjectId ) + { + return false; + } + + if( _currentObjectId != NpcObjectId ) + { + return true; + } + + return _currentObjectName == actor.Name.ToString(); + } + + private bool CheckObjectGPose( GameObject actor ) + => actor.ObjectId == NpcObjectId && _currentObjectName == actor.Name.ToString(); + + private (GameObject?, int) FindCurrentObject() + { + if( _inGPose ) + { + for( var i = GPosePlayerIdx; i < GPoseEndIdx; ++i ) { - for( var i = GPosePlayerIdx; i < GPoseEndIdx; ++i ) - { - var actor = Dalamud.Objects[ i ]; - if( actor == null ) - { - break; - } - - if( CheckObjectGPose( actor ) ) - { - return ( actor, i ); - } - } - } - - for( var i = 0; i < Dalamud.Objects.Length; ++i ) - { - if( i == GPosePlayerIdx ) - { - i = GPoseEndIdx; - } - var actor = Dalamud.Objects[ i ]; - if( actor != null && CheckObject( actor ) ) + if( actor == null ) + { + break; + } + + if( CheckObjectGPose( actor ) ) { return ( actor, i ); } } - - return ( null, -1 ); } - private void PopObject() + for( var i = 0; i < Dalamud.Objects.Length; ++i ) { - if( _actorIds.Count > 0 ) + if( i == GPosePlayerIdx ) { - var (id, name, s) = _actorIds.Dequeue(); - _currentObjectName = name; - _currentObjectId = id; - _currentRedrawType = s; - var (actor, _) = FindCurrentObject(); - if( actor == null ) + i = GPoseEndIdx; + } + + var actor = Dalamud.Objects[ i ]; + if( actor != null && CheckObject( actor ) ) + { + return ( actor, i ); + } + } + + return ( null, -1 ); + } + + private void PopObject() + { + if( _actorIds.Count > 0 ) + { + var (id, name, s) = _actorIds.Dequeue(); + _currentObjectName = name; + _currentObjectId = id; + _currentRedrawType = s; + var (actor, _) = FindCurrentObject(); + if( actor == null ) + { + return; + } + + _wasTarget = actor.Address == Dalamud.Targets.Target?.Address; + + ++_currentFrame; + } + else + { + Dalamud.Framework.Update -= OnUpdateEvent; + } + } + + private void ApplySettingsOrRedraw() + { + var (actor, idx) = FindCurrentObject(); + if( actor == null ) + { + _currentFrame = 0; + return; + } + + switch( _currentRedrawType ) + { + case RedrawType.Unload: + WriteInvisible( actor, idx ); + _currentFrame = 0; + break; + case RedrawType.RedrawWithSettings: + ChangeSettings(); + ++_currentFrame; + break; + case RedrawType.RedrawWithoutSettings: + WriteVisible( actor, idx ); + _currentFrame = 0; + break; + case RedrawType.WithoutSettings: + WriteInvisible( actor, idx ); + ++_currentFrame; + break; + case RedrawType.WithSettings: + ChangeSettings(); + WriteInvisible( actor, idx ); + ++_currentFrame; + break; + case RedrawType.OnlyWithSettings: + ChangeSettings(); + if( !_changedSettings ) { return; } - _wasTarget = actor.Address == Dalamud.Targets.Target?.Address; - + WriteInvisible( actor, idx ); ++_currentFrame; - } - else - { - Dalamud.Framework.Update -= OnUpdateEvent; - } - } - - private void ApplySettingsOrRedraw() - { - var (actor, idx) = FindCurrentObject(); - if( actor == null ) - { - _currentFrame = 0; - return; - } - - switch( _currentRedrawType ) - { - case RedrawType.Unload: - WriteInvisible( actor, idx ); - _currentFrame = 0; - break; - case RedrawType.RedrawWithSettings: - ChangeSettings(); - ++_currentFrame; - break; - case RedrawType.RedrawWithoutSettings: - WriteVisible( actor, idx ); - _currentFrame = 0; - break; - case RedrawType.WithoutSettings: - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.WithSettings: - ChangeSettings(); - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.OnlyWithSettings: - ChangeSettings(); - if( !_changedSettings ) - { - return; - } - - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.AfterGPoseWithSettings: - case RedrawType.AfterGPoseWithoutSettings: - if( _inGPose ) - { - _actorIds.Enqueue( ( _currentObjectId, _currentObjectName!, _currentRedrawType ) ); - _currentFrame = 0; - } - else - { - _currentRedrawType = _currentRedrawType == RedrawType.AfterGPoseWithSettings - ? RedrawType.WithSettings - : RedrawType.WithoutSettings; - } - - break; - default: throw new InvalidEnumArgumentException(); - } - } - - private void StartRedrawAndWait() - { - var (actor, idx) = FindCurrentObject(); - if( actor == null ) - { - RevertSettings(); - return; - } - - WriteVisible( actor, idx ); - _currentFrame = _changedSettings || _wasTarget ? _currentFrame + 1 : 0; - } - - private void RevertSettings() - { - var (actor, _) = FindCurrentObject(); - if( actor != null ) - { - if( !StillLoading( RenderPtr( actor ) ) ) + break; + case RedrawType.AfterGPoseWithSettings: + case RedrawType.AfterGPoseWithoutSettings: + if( _inGPose ) { - RestoreSettings(); - if( _wasTarget && Dalamud.Targets.Target == null ) - { - Dalamud.Targets.SetTarget( actor ); - } - + _actorIds.Enqueue( ( _currentObjectId, _currentObjectName!, _currentRedrawType ) ); _currentFrame = 0; } - } - else + else + { + _currentRedrawType = _currentRedrawType == RedrawType.AfterGPoseWithSettings + ? RedrawType.WithSettings + : RedrawType.WithoutSettings; + } + + break; + default: throw new InvalidEnumArgumentException(); + } + } + + private void StartRedrawAndWait() + { + var (actor, idx) = FindCurrentObject(); + if( actor == null ) + { + RevertSettings(); + return; + } + + WriteVisible( actor, idx ); + _currentFrame = _changedSettings || _wasTarget ? _currentFrame + 1 : 0; + } + + private void RevertSettings() + { + var (actor, _) = FindCurrentObject(); + if( actor != null ) + { + if( !StillLoading( ActorDrawState( actor ) ) ) { + RestoreSettings(); + if( _wasTarget && Dalamud.Targets.Target == null ) + { + Dalamud.Targets.SetTarget( actor ); + } + _currentFrame = 0; } } - - private void OnUpdateEvent( object framework ) + else { - if( Dalamud.Conditions[ ConditionFlag.BetweenAreas51 ] - || Dalamud.Conditions[ ConditionFlag.BetweenAreas ] - || Dalamud.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) - { - _waitFrames = DefaultWaitFrames; - return; - } - - if( _waitFrames > 0 ) - { - --_waitFrames; - return; - } - - _inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null; - - switch( _currentFrame ) - { - case 0: - PopObject(); - break; - case 1: - ApplySettingsOrRedraw(); - break; - case 2: - StartRedrawAndWait(); - break; - case 3: - RevertSettings(); - break; - default: - _currentFrame = 0; - break; - } - } - - private void RedrawObjectIntern( uint objectId, string actorName, RedrawType settings ) - { - if( _actorIds.Contains( ( objectId, actorName, settings ) ) ) - { - return; - } - - _actorIds.Enqueue( ( objectId, actorName, settings ) ); - if( _actorIds.Count == 1 ) - { - Dalamud.Framework.Update += OnUpdateEvent; - } - } - - public void RedrawObject( GameObject? actor, RedrawType settings = RedrawType.WithSettings ) - { - if( actor != null ) - { - RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), RedrawType.WithoutSettings ); // TODO settings ); - } - } - - private GameObject? GetLocalPlayer() - { - var gPosePlayer = Dalamud.Objects[ GPosePlayerIdx ]; - return gPosePlayer ?? Dalamud.Objects[ 0 ]; - } - - private GameObject? GetName( string name ) - { - var lowerName = name.ToLowerInvariant(); - return lowerName switch - { - "" => null, - "" => GetLocalPlayer(), - "self" => GetLocalPlayer(), - "" => Dalamud.Targets.Target, - "target" => Dalamud.Targets.Target, - "" => Dalamud.Targets.FocusTarget, - "focus" => Dalamud.Targets.FocusTarget, - "" => Dalamud.Targets.MouseOverTarget, - "mouseover" => Dalamud.Targets.MouseOverTarget, - _ => Dalamud.Objects.FirstOrDefault( - a => string.Equals( a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase ) ), - }; - } - - public void RedrawObject( string name, RedrawType settings = RedrawType.WithSettings ) - => RedrawObject( GetName( name ), settings ); - - public void RedrawAll( RedrawType settings = RedrawType.WithSettings ) - { - Clear(); - foreach( var actor in Dalamud.Objects ) - { - RedrawObject( actor, settings ); - } - } - - private void UnloadAll() - { - Clear(); - foreach( var (actor, index) in Dalamud.Objects.Select( ( a, i ) => ( a, i ) ) ) - { - WriteInvisible( actor, index ); - } - } - - private void RedrawAllWithoutSettings() - { - Clear(); - foreach( var (actor, index) in Dalamud.Objects.Select( ( a, i ) => ( a, i ) ) ) - { - WriteVisible( actor, index ); - } - } - - public async void UnloadAtOnceRedrawWithSettings() - { - Clear(); - UnloadAll(); - await Task.Delay( UnloadAllRedrawDelay ); - RedrawAll( RedrawType.RedrawWithSettings ); - } - - public async void UnloadAtOnceRedrawWithoutSettings() - { - Clear(); - UnloadAll(); - await Task.Delay( UnloadAllRedrawDelay ); - RedrawAllWithoutSettings(); - } - - public void Clear() - { - RestoreSettings(); _currentFrame = 0; } + } - public void Dispose() + private void OnUpdateEvent( object framework ) + { + if( Dalamud.Conditions[ ConditionFlag.BetweenAreas51 ] + || Dalamud.Conditions[ ConditionFlag.BetweenAreas ] + || Dalamud.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) { - RevertSettings(); - _actorIds.Clear(); - Dalamud.Framework.Update -= OnUpdateEvent; + _waitFrames = DefaultWaitFrames; + return; + } + + if( _waitFrames > 0 ) + { + --_waitFrames; + return; + } + + _inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null; + + switch( _currentFrame ) + { + case 0: + PopObject(); + break; + case 1: + ApplySettingsOrRedraw(); + break; + case 2: + StartRedrawAndWait(); + break; + case 3: + RevertSettings(); + break; + default: + _currentFrame = 0; + break; } } + + private void RedrawObjectIntern( uint objectId, string actorName, RedrawType settings ) + { + if( _actorIds.Contains( ( objectId, actorName, settings ) ) ) + { + return; + } + + _actorIds.Enqueue( ( objectId, actorName, settings ) ); + if( _actorIds.Count == 1 ) + { + Dalamud.Framework.Update += OnUpdateEvent; + } + } + + public void RedrawObject( GameObject? actor, RedrawType settings = RedrawType.WithSettings ) + { + if( actor != null ) + { + RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), RedrawType.WithoutSettings ); // TODO settings ); + } + } + + private GameObject? GetLocalPlayer() + { + var gPosePlayer = Dalamud.Objects[ GPosePlayerIdx ]; + return gPosePlayer ?? Dalamud.Objects[ 0 ]; + } + + private GameObject? GetName( string name ) + { + var lowerName = name.ToLowerInvariant(); + return lowerName switch + { + "" => null, + "" => GetLocalPlayer(), + "self" => GetLocalPlayer(), + "" => Dalamud.Targets.Target, + "target" => Dalamud.Targets.Target, + "" => Dalamud.Targets.FocusTarget, + "focus" => Dalamud.Targets.FocusTarget, + "" => Dalamud.Targets.MouseOverTarget, + "mouseover" => Dalamud.Targets.MouseOverTarget, + _ => Dalamud.Objects.FirstOrDefault( + a => string.Equals( a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase ) ), + }; + } + + public void RedrawObject( string name, RedrawType settings = RedrawType.WithSettings ) + => RedrawObject( GetName( name ), settings ); + + public void RedrawAll( RedrawType settings = RedrawType.WithSettings ) + { + Clear(); + foreach( var actor in Dalamud.Objects ) + { + RedrawObject( actor, settings ); + } + } + + public void Clear() + { + RestoreSettings(); + _currentFrame = 0; + } + + public void Dispose() + { + RevertSettings(); + _actorIds.Clear(); + Dalamud.Framework.Update -= OnUpdateEvent; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/DrawState.cs b/Penumbra/Interop/Structs/DrawState.cs new file mode 100644 index 00000000..b30d1a76 --- /dev/null +++ b/Penumbra/Interop/Structs/DrawState.cs @@ -0,0 +1,14 @@ +using System; + +namespace Penumbra.Interop.Structs; + +[Flags] +public enum DrawState : uint +{ + Invisibility = 0x00_00_00_02, + IsLoading = 0x00_00_08_00, + SomeNpcFlag = 0x00_00_01_00, + MaybeCulled = 0x00_00_04_00, + MaybeHiddenMinion = 0x00_00_80_00, + MaybeHiddenSummon = 0x00_80_00_00, +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 8f6fd22a..81b86c32 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -11,8 +11,10 @@ using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop; +using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.UI.Custom; +using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; namespace Penumbra.UI; @@ -159,7 +161,7 @@ public partial class SettingsInterface //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } - private void DrawDebugTabRedraw() + private unsafe void DrawDebugTabRedraw() { if( !ImGui.CollapsingHeader( "Redrawing##Debug" ) ) { @@ -187,7 +189,7 @@ public partial class SettingsInterface .GetField( "_currentObjectName", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( _penumbra.ObjectReloader ); - var currentObjectStartState = ( ObjectReloader.LoadingFlags? )_penumbra.ObjectReloader.GetType() + var currentObjectStartState = ( DrawState? )_penumbra.ObjectReloader.GetType() .GetField( "_currentObjectStartState", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( _penumbra.ObjectReloader ); @@ -200,7 +202,7 @@ public partial class SettingsInterface .Invoke( _penumbra.ObjectReloader, Array.Empty< object >() )!; var currentRender = currentObject != null - ? ( ObjectReloader.LoadingFlags? )Marshal.ReadInt32( ObjectReloader.RenderPtr( currentObject ) ) + ? ObjectReloader.ActorDrawState( currentObject ) : null; var waitFrames = ( int? )_penumbra.ObjectReloader.GetType() From 581b91b337f86b7e4075b635a4d700c656c13530 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Mar 2022 17:12:31 +0100 Subject: [PATCH 0097/2451] Fixes. --- Penumbra.GameData/ByteString/Utf8RelPath.cs | 9 +++- .../ByteString/Utf8String.Manipulation.cs | 2 +- Penumbra/Importer/TexToolsMeta.cs | 1 - Penumbra/Interop/CharacterUtility.cs | 3 ++ .../Interop/Resolver/PathResolver.Meta.cs | 38 ++++++++++++-- Penumbra/Interop/Structs/CharacterUtility.cs | 4 +- Penumbra/Meta/Files/CmpFile.cs | 2 +- Penumbra/Meta/Files/EstFile.cs | 52 +++++++++---------- Penumbra/Meta/Files/MetaBaseFile.cs | 10 ++-- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 2 +- .../Meta/Manipulations/EstManipulation.cs | 19 ++++--- Penumbra/UI/MenuTabs/TabCollections.cs | 6 +-- Penumbra/UI/MenuTabs/TabDebug.cs | 27 ++++++++++ 13 files changed, 123 insertions(+), 52 deletions(-) diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs index 5fd79ef7..d03ac745 100644 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -20,7 +20,14 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf public static explicit operator Utf8RelPath( string s ) - => FromString( s, out var p ) ? p : Empty; + { + if( !FromString( s, out var p ) ) + { + return Empty; + } + + return new Utf8RelPath( p.Path.AsciiToLower() ); + } public static bool FromString( string? s, out Utf8RelPath path ) { diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs index 0d895fa6..2a941020 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs @@ -71,7 +71,7 @@ public sealed unsafe partial class Utf8String var length = Length; var newPtr = ByteStringFunctions.CopyString( _path, length ); var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to ); - return new Utf8String().Setup( newPtr, length, numReplaced > 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); + return new Utf8String().Setup( newPtr, length, numReplaced == 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); } // Join a number of strings with a given byte between them. diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 4b18fd07..e39b06ac 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Dalamud.Logging; -using Lumina.Data.Files; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 03f4bba0..11d54eb4 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -49,6 +49,9 @@ public unsafe class CharacterUtility : IDisposable public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length]; + public (IntPtr Address, int Size) DefaultResource( int fullIdx ) + => DefaultResources[ ReverseIndices[ fullIdx ] ]; + public CharacterUtility() { SignatureHelper.Initialise( this ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 2ae74559..bd843982 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -21,9 +21,11 @@ namespace Penumbra.Interop.Resolver; // EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by // ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath. -// RSP entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", or maybe "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", -// possibly also "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" -// which is called by a lot of functions, but the mostly relevant is probably Human.SetupFromCharacterData, which is only called by CharacterBase.Create. +// RSP height entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF" +// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05" +// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" +// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, +// and RspSetupCharacter, which is hooked here. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. @@ -102,6 +104,18 @@ public unsafe partial class PathResolver ApplyVisorHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); } + // RSP + public delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); + + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56 ", DetourName = "RspSetupCharacterDetour" )] + public Hook< RspSetupCharacterDelegate >? RspSetupCharacterHook; + + private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) + { + using var rsp = MetaChanger.ChangeCmp( this, drawObject ); + RspSetupCharacterHook!.Original( drawObject, unk2, unk3, unk4, unk5 ); + } + private void SetupMetaHooks() { OnModelLoadCompleteHook = @@ -119,6 +133,9 @@ public unsafe partial class PathResolver #endif #if USE_GMP ApplyVisorHook?.Enable(); +#endif +#if USE_CMP + RspSetupCharacterHook?.Enable(); #endif } @@ -128,6 +145,7 @@ public unsafe partial class PathResolver UpdateModelsHook?.Disable(); OnModelLoadCompleteHook?.Disable(); ApplyVisorHook?.Disable(); + RspSetupCharacterHook?.Disable(); } private void DisposeMetaHooks() @@ -136,6 +154,7 @@ public unsafe partial class PathResolver UpdateModelsHook?.Dispose(); OnModelLoadCompleteHook?.Dispose(); ApplyVisorHook?.Dispose(); + RspSetupCharacterHook?.Dispose(); } private ModCollection? GetCollection( IntPtr drawObject ) @@ -249,6 +268,19 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } + public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) + { +#if USE_CMP + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + collection.SetCmpFiles(); + return new MetaChanger( MetaManipulation.Type.Rsp ); + } +#endif + return new MetaChanger( MetaManipulation.Type.Unknown ); + } + public void Dispose() { switch( _type ) diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index cdd0c6cb..aaceee6b 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -17,8 +17,8 @@ public unsafe struct CharacterUtility public const int HumanCmpIdx = 63; public const int FaceEstIdx = 64; public const int HairEstIdx = 65; - public const int BodyEstIdx = 66; - public const int HeadEstIdx = 67; + public const int HeadEstIdx = 66; + public const int BodyEstIdx = 67; public const int EqdpStartIdx = 2; public const int NumEqdpFiles = 2 * 28; diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index bdb5c10b..356d826d 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -39,7 +39,7 @@ public sealed unsafe class CmpFile : MetaBaseFile public static float GetDefault( SubRace subRace, RspAttribute attribute ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ CharacterUtility.HumanCmpIdx ].Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( CharacterUtility.HumanCmpIdx ).Address; return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); } diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 4eb53be3..8880b9e0 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Windows.Forms; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta.Manipulations; @@ -15,7 +16,7 @@ public sealed unsafe class EstFile : MetaBaseFile { private const ushort EntryDescSize = 4; private const ushort EntrySize = 2; - private const int IncreaseSize = 100; + private const int IncreaseSize = 512; public int Count => *( int* )Data; @@ -52,58 +53,57 @@ public sealed unsafe class EstFile : MetaBaseFile { var data = Data; var length = Length; - AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) ); + AllocateData( length + IncreaseSize ); Functions.MemCpyUnchecked( Data, data, length ); - Functions.MemSet( Data + length, 0, IncreaseSize * ( EntryDescSize + EntrySize ) ); + Functions.MemSet( Data + length, 0, IncreaseSize ); GC.RemoveMemoryPressure( length ); Marshal.FreeHGlobal( ( IntPtr )data ); } - var control = ( uint* )( Data + 4 ); - var entries = ( ushort* )( Data + 4 * ( Count + 1 ) ); + var control = ( Info* )( Data + 4 ); + var entries = ( ushort* )( control + Count ); - for( var i = Count; i > idx; --i ) + for( var i = Count - 1; i >= idx; --i ) { - *( entries + i + 2 ) = entries[ i - 1 ]; + entries[ i + 3 ] = entries[ i ]; } + entries[ idx + 2 ] = skeletonId; + for( var i = idx - 1; i >= 0; --i ) { - *( entries + i + 2 ) = entries[ i ]; + entries[ i + 2 ] = entries[ i ]; } - for( var i = Count; i > idx; --i ) + for( var i = Count - 1; i >= idx; --i ) { - *( control + i ) = control[ i - 1 ]; + control[ i + 1 ] = control[ i ]; } + control[ idx ] = new Info( genderRace, setId ); + *( int* )Data = Count + 1; - - *( ushort* )control = setId; - *( ( ushort* )control + 1 ) = ( ushort )genderRace; - control[ idx ] = skeletonId; } private void RemoveEntry( int idx ) { - var entries = ( ushort* )( Data + 4 * Count ); - var control = ( uint* )( Data + 4 ); - *( int* )Data = Count - 1; - var count = Count; + var control = ( Info* )( Data + 4 ); + var entries = ( ushort* )( control + Count ); - for( var i = idx; i < count; ++i ) + for( var i = idx; i < Count; ++i ) { control[ i ] = control[ i + 1 ]; } - for( var i = 0; i < count; ++i ) + for( var i = 0; i < Count; ++i ) { - entries[ i ] = entries[ i + 1 ]; + entries[ i - 2 ] = entries[ i + 1 ]; } - entries[ count ] = 0; - entries[ count + 1 ] = 0; - entries[ count + 2 ] = 0; + entries[ Count - 3 ] = 0; + entries[ Count - 2 ] = 0; + entries[ Count - 1 ] = 0; + *( int* )Data = Count - 1; } [StructLayout( LayoutKind.Sequential, Size = 4 )] @@ -179,7 +179,7 @@ public sealed unsafe class EstFile : MetaBaseFile : base( ( int )estType ) { var length = DefaultData.Length; - AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) ); + AllocateData( length + IncreaseSize ); Reset(); } @@ -188,7 +188,7 @@ public sealed unsafe class EstFile : MetaBaseFile public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ ( int )estType ].Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( ( int )estType ).Address; var count = *( int* )data; var span = new ReadOnlySpan< Info >( data + 4, count ); var (idx, found) = FindEntry( span, genderRace, setId ); diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 304f0f09..c566cd16 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; using Dalamud.Memory; namespace Penumbra.Meta.Files; @@ -14,17 +13,18 @@ public unsafe class MetaBaseFile : IDisposable => Index = idx; protected (IntPtr Data, int Length) DefaultData - => Penumbra.CharacterUtility.DefaultResources[ Index ]; + => Penumbra.CharacterUtility.DefaultResource( Index ); // Reset to default values. public virtual void Reset() - {} + { } // Obtain memory. protected void AllocateData( int length ) { Length = length; - Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); ; + Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); + ; GC.AddMemoryPressure( length ); } @@ -32,7 +32,7 @@ public unsafe class MetaBaseFile : IDisposable protected void ReleaseUnmanagedResources() { var ptr = ( IntPtr )Data; - MemoryHelper.GameFree( ref ptr, (ulong) Length ); + MemoryHelper.GameFree( ref ptr, ( ulong )Length ); GC.RemoveMemoryPressure( Length ); Length = 0; Data = null; diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index d281bb37..8f43ac00 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -40,7 +40,7 @@ public partial class MetaManager public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) { #if USE_GMP - if( Manipulations.TryAdd( m, mod ) ) + if( !Manipulations.TryAdd( m, mod ) ) { return false; } diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 613ef965..d25893f1 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,22 +19,27 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.HeadEstIdx, } - public readonly ushort SkeletonIdx; + public readonly ushort SkeletonIdx; + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly Gender Gender; + public readonly Gender Gender; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly ModelRace Race; - public readonly ushort SetId; - [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EstType Slot; - public EstManipulation( Gender gender, ModelRace race, EstType estType, ushort setId, ushort skeletonIdx ) + public readonly ushort SetId; + + [JsonConverter( typeof( StringEnumConverter ) )] + public readonly EstType Slot; + + [JsonConstructor] + public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort skeletonIdx ) { SkeletonIdx = skeletonIdx; Gender = gender; Race = race; SetId = setId; - Slot = estType; + Slot = slot; } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index e2e6bdac..c5ce81a3 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -360,17 +360,15 @@ public partial class SettingsInterface using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FramePadding, Vector2.One * ImGuiHelpers.GlobalScale * 1.5f ); - if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) + if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}", Vector2.One * ImGui.GetFrameHeight() ) ) { manager.Collections.RemoveCharacterCollection( name ); } - style.Pop(); - font.Pop(); ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); ImGui.Text( name ); } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 81b86c32..72c3e86e 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Structs; using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.UI.Custom; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; @@ -410,6 +411,32 @@ public partial class SettingsInterface catch { } + var est = 0; + ImGui.InputInt( "##EstInput", ref est ); + try + { + var def = EstFile.GetDefault( EstManipulation.EstType.Body, GenderRace.MidlanderFemale, ( ushort )est ); + var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Est.BodyFile?[ GenderRace.MidlanderFemale, + ( ushort )est ] + ?? def; + ImGui.Text( def.ToString() ); + ImGui.Text( val.ToString() ); + } + catch + { } + + var gmp = 0; + ImGui.InputInt( "##GmpInput", ref gmp ); + try + { + var def = ExpandedGmpFile.GetDefault( gmp ); + var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Gmp.File?[ gmp ] ?? def; + ImGui.Text( def.Value.ToString("X") ); + ImGui.Text( val.Value.ToString("X") ); + } + catch + { } + if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) { From e6752ade04ef16fbff648763fc875af2c57efdf0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Mar 2022 14:03:57 +0100 Subject: [PATCH 0098/2451] More fixes, some cleanup. --- Penumbra.GameData/ByteString/Utf8RelPath.cs | 6 +- .../ByteString/Utf8String.Construction.cs | 2 +- Penumbra/Configuration.cs | 2 - Penumbra/Interop/ObjectReloader.cs | 27 +--- .../Interop/Resolver/PathResolver.Data.cs | 13 +- .../Interop/Resolver/PathResolver.Material.cs | 2 + .../Interop/Resolver/PathResolver.Meta.cs | 19 +-- Penumbra/Interop/Resolver/PathResolver.cs | 6 + Penumbra/Interop/Structs/ResourceHandle.cs | 14 +- Penumbra/Meta/Files/EqdpFile.cs | 4 +- Penumbra/Meta/Files/ImcFile.cs | 4 +- Penumbra/Meta/Files/MetaBaseFile.cs | 12 +- Penumbra/Mods/CollectionManager.cs | 127 ++++++++++-------- Penumbra/Mods/ModManager.cs | 1 + Penumbra/Penumbra.cs | 39 +----- Penumbra/UI/MenuTabs/TabCollections.cs | 8 +- Penumbra/UI/MenuTabs/TabDebug.cs | 104 +------------- .../TabInstalledDetailsManipulations.cs | 112 ++++++--------- Penumbra/UI/MenuTabs/TabResourceManager.cs | 5 +- Penumbra/UI/MenuTabs/TabSettings.cs | 47 +------ 20 files changed, 193 insertions(+), 361 deletions(-) diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs index d03ac745..d0259cc4 100644 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -49,7 +49,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf return true; } - if( !Utf8String.FromString( substring, out var ascii ) || !ascii.IsAscii ) + if( !Utf8String.FromString( substring, out var ascii, true ) || !ascii.IsAscii ) { return false; } @@ -66,7 +66,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf return false; } - var substring = file.FullName[ baseDir.FullName.Length.. ]; + var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; return FromString( substring, out path ); } @@ -78,7 +78,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf return false; } - var substring = file.FullName[ baseDir.FullName.Length.. ]; + var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; return FromString( substring, out path ); } diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs index 487b1a16..f6c47a8b 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -97,7 +97,7 @@ public sealed unsafe partial class Utf8String : IDisposable } Marshal.FreeHGlobal( ( IntPtr )_path ); - GC.RemoveMemoryPressure( Length ); + GC.RemoveMemoryPressure( Length + 1 ); _length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag; _path = Null.NullBytePtr; _crc32 = 0; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index fbfd7ddc..56be9eb2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -31,8 +31,6 @@ public class Configuration : IPluginConfiguration public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } - public bool EnablePlayerWatch { get; set; } = false; - public int WaitFrames { get; set; } = 30; public string ModDirectory { get; set; } = string.Empty; diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 5865602d..56a40584 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; @@ -16,18 +14,13 @@ public unsafe class ObjectReloader : IDisposable { private delegate void ManipulateDraw( IntPtr actor ); - private const int RenderModeOffset = 0x0104; - private const int UnloadAllRedrawDelay = 250; - private const uint NpcObjectId = unchecked( ( uint )-536870912 ); - public const int GPosePlayerIdx = 201; - public const int GPoseEndIdx = GPosePlayerIdx + 48; + private const uint NpcObjectId = unchecked( ( uint )-536870912 ); + public const int GPosePlayerIdx = 201; + public const int GPoseEndIdx = GPosePlayerIdx + 48; private readonly ModManager _mods; private readonly Queue< (uint actorId, string name, RedrawType s) > _actorIds = new(); - internal int DefaultWaitFrames; - - private int _waitFrames; private int _currentFrame; private bool _changedSettings; private uint _currentObjectId = uint.MaxValue; @@ -46,11 +39,8 @@ public unsafe class ObjectReloader : IDisposable private static delegate*< IntPtr, void > GetEnableDraw( GameObject actor ) => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]; - public ObjectReloader( ModManager mods, int defaultWaitFrames ) - { - _mods = mods; - DefaultWaitFrames = defaultWaitFrames; - } + public ObjectReloader( ModManager mods ) + => _mods = mods; private void ChangeSettings() { @@ -289,13 +279,6 @@ public unsafe class ObjectReloader : IDisposable || Dalamud.Conditions[ ConditionFlag.BetweenAreas ] || Dalamud.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) { - _waitFrames = DefaultWaitFrames; - return; - } - - if( _waitFrames > 0 ) - { - --_waitFrames; return; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 3df5bd1c..229d6380 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -68,10 +68,12 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Enable(); EnableDrawHook?.Enable(); CharacterBaseDestructorHook?.Enable(); + Penumbra.ModManager.Collections.CollectionChanged += CheckCollections; } private void DisableDataHooks() { + Penumbra.ModManager.Collections.CollectionChanged -= CheckCollections; CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); @@ -178,8 +180,13 @@ public unsafe partial class PathResolver } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections() + private void CheckCollections( ModCollection? _1, ModCollection? _2, CollectionType type, string? name ) { + if( type is not (CollectionType.Character or CollectionType.Default) ) + { + return; + } + foreach( var (key, (_, idx)) in DrawObjectToObject.ToArray() ) { if( !VerifyEntry( key, idx, out var obj ) ) @@ -187,8 +194,8 @@ public unsafe partial class PathResolver DrawObjectToObject.Remove( key ); } - var collection = IdentifyCollection( obj ); - DrawObjectToObject[ key ] = ( collection, idx ); + var newCollection = IdentifyCollection( obj ); + DrawObjectToObject[ key ] = ( newCollection, idx ); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 8afffdba..8fde48cc 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData.ByteString; @@ -64,6 +65,7 @@ public unsafe partial class PathResolver var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); var collection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; + var x = PathCollections.ToList(); for( var i = 0; i < mtrl->NumTex; ++i ) { var texString = new Utf8String( mtrl->TexString( i ) ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index bd843982..f92750e9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -92,16 +92,17 @@ public unsafe partial class PathResolver } } - // GMP - public delegate void ApplyVisorDelegate( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ); + // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, + // but it only applies a changed gmp file after a redraw for some reason. + public delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState ); - [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "ApplyVisorDetour" )] - public Hook< ApplyVisorDelegate >? ApplyVisorHook; + [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "SetupVisorDetour" )] + public Hook< SetupVisorDelegate >? SetupVisorHook; - private void ApplyVisorDetour( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ) + private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) { using var gmp = MetaChanger.ChangeGmp( this, drawObject ); - ApplyVisorHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); + return SetupVisorHook!.Original( drawObject, modelId, visorState ); } // RSP @@ -132,7 +133,7 @@ public unsafe partial class PathResolver OnModelLoadCompleteHook?.Enable(); #endif #if USE_GMP - ApplyVisorHook?.Enable(); + SetupVisorHook?.Enable(); #endif #if USE_CMP RspSetupCharacterHook?.Enable(); @@ -144,7 +145,7 @@ public unsafe partial class PathResolver GetEqpIndirectHook?.Disable(); UpdateModelsHook?.Disable(); OnModelLoadCompleteHook?.Disable(); - ApplyVisorHook?.Disable(); + SetupVisorHook?.Disable(); RspSetupCharacterHook?.Disable(); } @@ -153,7 +154,7 @@ public unsafe partial class PathResolver GetEqpIndirectHook?.Dispose(); UpdateModelsHook?.Dispose(); OnModelLoadCompleteHook?.Dispose(); - ApplyVisorHook?.Dispose(); + SetupVisorHook?.Dispose(); RspSetupCharacterHook?.Dispose(); } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index da0ae9b1..894727d4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -49,6 +49,12 @@ public partial class PathResolver : IDisposable resolved = Penumbra.ModManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); if( resolved == null ) { + // We also need to handle defaulted materials against a non-default collection. + if( nonDefault && gamePath.Path.EndsWith( 'm', 't', 'r', 'l' ) ) + { + SetCollection( gamePath.Path, collection ); + } + return ( null, collection ); } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 6ed2cc8e..9493a2eb 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -37,19 +37,31 @@ public unsafe struct ResourceHandle public ReadOnlySpan< byte > FileNameSpan() => new(FileName(), FileNameLength); + [FieldOffset( 0x00 )] + public void** VTable; + [FieldOffset( 0x48 )] public byte* FileNameData; [FieldOffset( 0x58 )] public int FileNameLength; + // May return null. + public static byte* GetData( ResourceHandle* handle ) + => ( ( delegate*< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); + + public static ulong GetLength( ResourceHandle* handle ) + => ( ( delegate*< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); + + + // Only use these if you know what you are doing. + // Those are actually only sure to be accessible for DefaultResourceHandles. [FieldOffset( 0xB0 )] public DataIndirection* Data; [FieldOffset( 0xB8 )] public uint DataLength; - public (IntPtr Data, int Length) GetData() => Data != null ? ( ( IntPtr )Data->DataPtr, ( int )Data->DataLength ) diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 053d83f0..6d4ebe8d 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -121,7 +121,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public static EqdpEntry GetDefault( int fileIdx, int setIdx ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( fileIdx ).Address; var blockSize = *( ushort* )( data + IdentifierSize ); var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 ); @@ -139,7 +139,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 ); var x = new ReadOnlySpan< ushort >( blockData, blockSize ); - return (EqdpEntry) (*( blockData + setIdx % blockSize )); + return ( EqdpEntry )( *( blockData + setIdx % blockSize ) ); } public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 35e53d66..f3ce0a4a 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -201,7 +201,9 @@ public unsafe class ImcFile : MetaBaseFile var file = Dalamud.GameData.GetFile( path.ToString() ); if( file == null ) { - throw new Exception(); + throw new Exception( + "Could not obtain default Imc File.\n" + + "Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted." ); } fixed( byte* ptr = file.Data ) diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index c566cd16..1d24cfb4 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -24,8 +24,10 @@ public unsafe class MetaBaseFile : IDisposable { Length = length; Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); - ; - GC.AddMemoryPressure( length ); + if( length > 0 ) + { + GC.AddMemoryPressure( length ); + } } // Free memory. @@ -33,7 +35,11 @@ public unsafe class MetaBaseFile : IDisposable { var ptr = ( IntPtr )Data; MemoryHelper.GameFree( ref ptr, ( ulong )Length ); - GC.RemoveMemoryPressure( Length ); + if( Length > 0 ) + { + GC.RemoveMemoryPressure( Length ); + } + Length = 0; Data = null; } diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index bf52a347..fa3835da 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,14 +1,24 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; -using Penumbra.Interop.Structs; using Penumbra.Mod; using Penumbra.Util; namespace Penumbra.Mods; +public enum CollectionType : byte +{ + Inactive, + Default, + Forced, + Character, + Current, +} + +public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, CollectionType type, + string? characterName = null ); + // Contains all collections and respective functions, as well as the collection settings. public class CollectionManager { @@ -21,7 +31,10 @@ public class CollectionManager public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty; public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; - public ModCollection ActiveCollection { get; private set; } + public ModCollection ActiveCollection { get; private set; } = ModCollection.Empty; + + // Is invoked after the collections actually changed. + public event CollectionChangeDelegate? CollectionChanged; public CollectionManager( ModManager manager ) { @@ -29,7 +42,7 @@ public class CollectionManager ReadCollections(); LoadConfigCollections( Penumbra.Config ); - ActiveCollection = DefaultCollection; + SetActiveCollection( DefaultCollection, string.Empty ); } public bool SetActiveCollection( ModCollection newActive, string name ) @@ -44,14 +57,7 @@ public class CollectionManager { ActiveCollection = newActive; Penumbra.ResidentResources.Reload(); - if( ActiveCollection.Cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - ActiveCollection.Cache.MetaManipulations.SetFiles(); - } + ActiveCollection.SetFiles(); } else { @@ -131,8 +137,9 @@ public class CollectionManager var newCollection = new ModCollection( name, settings ); Collections.Add( name, newCollection ); + CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); newCollection.Save(); - SetCurrentCollection( newCollection ); + SetCollection( newCollection, CollectionType.Current ); return true; } @@ -149,26 +156,27 @@ public class CollectionManager return false; } + CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); if( CurrentCollection == collection ) { - SetCurrentCollection( Collections[ ModCollection.DefaultCollection ] ); + SetCollection( Collections[ ModCollection.DefaultCollection ], CollectionType.Current ); } if( ForcedCollection == collection ) { - SetForcedCollection( ModCollection.Empty ); + SetCollection( ModCollection.Empty, CollectionType.Forced ); } if( DefaultCollection == collection ) { - SetDefaultCollection( ModCollection.Empty ); + SetCollection( ModCollection.Empty, CollectionType.Default ); } foreach( var (characterName, characterCollection) in CharacterCollection.ToArray() ) { if( characterCollection == collection ) { - SetCharacterCollection( characterName, ModCollection.Empty ); + SetCollection( ModCollection.Empty, CollectionType.Character, characterName ); } } @@ -196,51 +204,63 @@ public class CollectionManager } } - private void SetCollection( ModCollection newCollection, ModCollection oldCollection, Action< ModCollection > setter, - Action< string > configSetter ) + public void SetCollection( ModCollection newCollection, CollectionType type, string? characterName = null ) { - if( newCollection.Name == oldCollection.Name ) + var oldCollection = type switch + { + CollectionType.Default => DefaultCollection, + CollectionType.Forced => ForcedCollection, + CollectionType.Current => CurrentCollection, + CollectionType.Character => characterName?.Length > 0 + ? CharacterCollection.TryGetValue( characterName, out var c ) + ? c + : ModCollection.Empty + : null, + _ => null, + }; + + if( oldCollection == null || newCollection.Name == oldCollection.Name ) { return; } AddCache( newCollection ); - - setter( newCollection ); RemoveCache( oldCollection ); - configSetter( newCollection.Name ); - Penumbra.Config.Save(); - } - - public void SetDefaultCollection( ModCollection newCollection ) - => SetCollection( newCollection, DefaultCollection, c => + switch( type ) { - if( CollectionChangedTo.Length == 0 ) - { - SetActiveCollection( c, string.Empty ); - } - - DefaultCollection = c; - }, s => Penumbra.Config.DefaultCollection = s ); - - public void SetForcedCollection( ModCollection newCollection ) - => SetCollection( newCollection, ForcedCollection, c => ForcedCollection = c, s => Penumbra.Config.ForcedCollection = s ); - - public void SetCurrentCollection( ModCollection newCollection ) - => SetCollection( newCollection, CurrentCollection, c => CurrentCollection = c, s => Penumbra.Config.CurrentCollection = s ); - - public void SetCharacterCollection( string characterName, ModCollection newCollection ) - => SetCollection( newCollection, - CharacterCollection.TryGetValue( characterName, out var oldCollection ) ? oldCollection : ModCollection.Empty, - c => - { - if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) + case CollectionType.Default: + DefaultCollection = newCollection; + Penumbra.Config.DefaultCollection = newCollection.Name; + if( CollectionChangedTo.Length == 0 ) { - SetActiveCollection( c, CollectionChangedTo ); + SetActiveCollection( newCollection, string.Empty ); } - CharacterCollection[ characterName ] = c; - }, s => Penumbra.Config.CharacterCollections[ characterName ] = s ); + break; + case CollectionType.Forced: + ForcedCollection = newCollection; + Penumbra.Config.ForcedCollection = newCollection.Name; + break; + case CollectionType.Current: + CurrentCollection = newCollection; + Penumbra.Config.CurrentCollection = newCollection.Name; + break; + case CollectionType.Character: + if( CollectionChangedTo == characterName && CharacterCollection.ContainsKey( characterName ) ) + { + SetActiveCollection( newCollection, CollectionChangedTo ); + } + + CharacterCollection[ characterName! ] = newCollection; + Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; + + break; + } + + CollectionChanged?.Invoke( oldCollection, newCollection, type, characterName ); + + Penumbra.Config.Save(); + } public bool CreateCharacterCollection( string characterName ) { @@ -252,7 +272,7 @@ public class CollectionManager CharacterCollection[ characterName ] = ModCollection.Empty; Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; Penumbra.Config.Save(); - Penumbra.PlayerWatcher.AddPlayerToWatch( characterName ); + CollectionChanged?.Invoke( null, ModCollection.Empty, CollectionType.Character, characterName ); return true; } @@ -262,7 +282,7 @@ public class CollectionManager { RemoveCache( collection ); CharacterCollection.Remove( characterName ); - Penumbra.PlayerWatcher.RemovePlayerFromWatch( characterName ); + CollectionChanged?.Invoke( collection, null, CollectionType.Character, characterName ); } if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) @@ -338,7 +358,6 @@ public class CollectionManager var configChanged = false; foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) { - Penumbra.PlayerWatcher.AddPlayerToWatch( player ); if( collectionName.Length == 0 ) { CharacterCollection.Add( player, ModCollection.Empty ); diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 4538fa85..4a7ba3bd 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -146,6 +146,7 @@ public class ModManager } Collections.RecreateCaches(); + Collections.DefaultCollection.SetFiles(); } public void DeleteMod( DirectoryInfo modFolder ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cc9cc5e2..a487b7f9 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,7 +34,6 @@ public class Penumbra : IDalamudPlugin private const string CommandName = "/penumbra"; public static Configuration Config { get; private set; } = null!; - public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; @@ -72,10 +71,9 @@ public class Penumbra : IDalamudPlugin MetaDefaults = new MetaDefaults(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); ModManager = new ModManager(); ModManager.DiscoverMods(); - ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); + ObjectReloader = new ObjectReloader( ModManager ); PathResolver = new PathResolver( ResourceLoader ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) @@ -101,17 +99,6 @@ public class Penumbra : IDalamudPlugin CreateWebServer(); } - if( !Config.EnablePlayerWatch || !Config.EnableMods ) - { - PlayerWatcher.Disable(); - } - - PlayerWatcher.PlayerChanged += p => - { - PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); - ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); - }; - ResourceLoader.EnableHooks(); if( Config.EnableMods ) { @@ -127,17 +114,6 @@ public class Penumbra : IDalamudPlugin { ResourceLoader.EnableFullLogging(); } - - unsafe - { - PluginLog.Information( $"MetaManipulation: {sizeof( MetaManipulation )}" ); - PluginLog.Information( $"EqpManipulation: {sizeof( EqpManipulation )}" ); - PluginLog.Information( $"GmpManipulation: {sizeof( GmpManipulation )}" ); - PluginLog.Information( $"EqdpManipulation: {sizeof( EqdpManipulation )}" ); - PluginLog.Information( $"EstManipulation: {sizeof( EstManipulation )}" ); - PluginLog.Information( $"RspManipulation: {sizeof( RspManipulation )}" ); - PluginLog.Information( $"ImcManipulation: {sizeof( ImcManipulation )}" ); - } } public bool Enable() @@ -150,10 +126,6 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = true; ResourceLoader.EnableReplacements(); ResidentResources.Reload(); - if( Config.EnablePlayerWatch ) - { - PlayerWatcher.SetStatus( true ); - } Config.Save(); ObjectReloader.RedrawAll( RedrawType.WithSettings ); @@ -170,10 +142,6 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = false; ResourceLoader.DisableReplacements(); ResidentResources.Reload(); - if( Config.EnablePlayerWatch ) - { - PlayerWatcher.SetStatus( false ); - } Config.Save(); ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); @@ -232,7 +200,6 @@ public class Penumbra : IDalamudPlugin Api.Dispose(); SettingsInterface.Dispose(); ObjectReloader.Dispose(); - PlayerWatcher.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); @@ -268,7 +235,7 @@ public class Penumbra : IDalamudPlugin return false; } - ModManager.Collections.SetDefaultCollection( collection ); + ModManager.Collections.SetCollection( collection, CollectionType.Default ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; @@ -279,7 +246,7 @@ public class Penumbra : IDalamudPlugin return false; } - ModManager.Collections.SetForcedCollection( collection ); + ModManager.Collections.SetCollection( collection, CollectionType.Forced ); Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); SettingsInterface.ResetForcedCollection(); return true; diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index c5ce81a3..2b4fa4d8 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -169,7 +169,7 @@ public partial class SettingsInterface return; } - Penumbra.ModManager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); + Penumbra.ModManager.Collections.SetCollection( _collections[ idx + 1 ], CollectionType.Current ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -208,7 +208,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) { - Penumbra.ModManager.Collections.SetDefaultCollection( _collections[ index ] ); + Penumbra.ModManager.Collections.SetCollection( _collections[ index ], CollectionType.Default ); _currentDefaultIndex = index; } @@ -231,7 +231,7 @@ public partial class SettingsInterface && index != _currentForcedIndex && manager.Collections.CharacterCollection.Count > 0 ) { - manager.Collections.SetForcedCollection( _collections[ index ] ); + manager.Collections.SetCollection( _collections[ index ], CollectionType.Forced ); _currentForcedIndex = index; } @@ -352,7 +352,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) { - manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); + manager.Collections.SetCollection( _collections[ tmp ], CollectionType.Character, name ); _currentCharacterIndices[ name ] = tmp; } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 72c3e86e..b0281631 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -22,104 +22,6 @@ namespace Penumbra.UI; public partial class SettingsInterface { - private static void DrawDebugTabPlayers() - { - if( !ImGui.CollapsingHeader( "Players##Debug" ) ) - { - return; - } - - var players = Penumbra.PlayerWatcher.WatchedPlayers().ToArray(); - var count = players.Sum( s => Math.Max( 1, s.Item2.Length ) ); - if( count == 0 ) - { - return; - } - - if( !ImGui.BeginTable( "##ObjectTable", 13, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 4 * count ) ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - var identifier = GameData.GameData.GetIdentifier(); - - foreach( var (actor, equip) in players.SelectMany( kvp => kvp.Item2.Any() - ? kvp.Item2 - .Select( x => ( $"{kvp.Item1} ({x.Item1})", x.Item2 ) ) - : new[] { ( kvp.Item1, new CharacterEquipment() ) } ) ) - { - // @formatter:off - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( actor ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.MainHand}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Head}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Body}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Hands}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Legs}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Feet}" ); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - if (equip.IsSet == 0) - { - ImGui.Text( "(not set)" ); - } - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, EquipSlot.MainHand )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Head.Set, 0, equip.Head.Variant, EquipSlot.Head )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Body.Set, 0, equip.Body.Variant, EquipSlot.Body )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Hands.Set, 0, equip.Hands.Variant, EquipSlot.Hands )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Legs.Set, 0, equip.Legs.Variant, EquipSlot.Legs )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Feet.Set, 0, equip.Feet.Variant, EquipSlot.Feet )?.Name.ToString() ?? "Unknown" ); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.OffHand}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Ears}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Neck}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.Wrists}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.LFinger}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"{equip.RFinger}" ); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, EquipSlot.OffHand )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Ears.Set, 0, equip.Ears.Variant, EquipSlot.Ears )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Neck.Set, 0, equip.Neck.Variant, EquipSlot.Neck )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.Wrists.Set, 0, equip.Wrists.Variant, EquipSlot.Wrists )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.LFinger.Set, 0, equip.LFinger.Variant, EquipSlot.LFinger )?.Name.ToString() ?? "Unknown" ); - ImGui.TableNextColumn(); - ImGui.Text( identifier.Identify( equip.RFinger.Set, 0, equip.RFinger.Variant, EquipSlot.LFinger )?.Name.ToString() ?? "Unknown" ); - // @formatter:on - } - } - private static void PrintValue( string name, string value ) { ImGui.TableNextRow(); @@ -431,8 +333,8 @@ public partial class SettingsInterface { var def = ExpandedGmpFile.GetDefault( gmp ); var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Gmp.File?[ gmp ] ?? def; - ImGui.Text( def.Value.ToString("X") ); - ImGui.Text( val.Value.ToString("X") ); + ImGui.Text( def.Value.ToString( "X" ) ); + ImGui.Text( val.Value.ToString( "X" ) ); } catch { } @@ -557,8 +459,6 @@ public partial class SettingsInterface ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); - DrawDebugTabPlayers(); - ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 3e545e34..a61f6e90 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Drawing.Text; +using System.ComponentModel; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Interface; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Files; @@ -77,22 +75,12 @@ public partial class SettingsInterface ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), }; - private static readonly (string, ObjectType)[] ObjectTypes = + private static readonly (string, EstManipulation.EstType)[] EstTypes = { - ( "Equipment", ObjectType.Equipment ), - ( "Customization", ObjectType.Character ), - }; - - private static readonly (string, EquipSlot)[] EstEquipSlots = - { - EqpEquipSlots[ 0 ], - EqpEquipSlots[ 1 ], - }; - - private static readonly (string, BodySlot)[] EstBodySlots = - { - ( "Hair", BodySlot.Hair ), - ( "Face", BodySlot.Face ), + ( "Hair", EstManipulation.EstType.Hair ), + ( "Face", EstManipulation.EstType.Face ), + ( "Body", EstManipulation.EstType.Body ), + ( "Head", EstManipulation.EstType.Head ), }; private static readonly (string, SubRace)[] Subraces = @@ -133,9 +121,11 @@ public partial class SettingsInterface ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), }; + private static readonly (string, ObjectType)[] ImcObjectType = { - ObjectTypes[ 0 ], + ( "Equipment", ObjectType.Equipment ), + ( "Customization", ObjectType.Character ), ( "Weapon", ObjectType.Weapon ), ( "Demihuman", ObjectType.DemiHuman ), ( "Monster", ObjectType.Monster ), @@ -143,8 +133,8 @@ public partial class SettingsInterface private static readonly (string, BodySlot)[] ImcBodySlots = { - EstBodySlots[ 0 ], - EstBodySlots[ 1 ], + ( "Hair", BodySlot.Hair ), + ( "Face", BodySlot.Face ), ( "Body", BodySlot.Body ), ( "Tail", BodySlot.Tail ), ( "Ears", BodySlot.Zear ), @@ -646,24 +636,26 @@ public partial class SettingsInterface RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); - EquipSlot equipSlot = default; + ImcManipulation imc = new(); switch( objectType ) { case ObjectType.Equipment: - CustomCombo( "Equipment Slot", EqdpEquipSlots, out equipSlot, ref _newManipEquipSlot ); - //newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant, - // new ImcFile.ImageChangeData() ); + CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); + imc = new ImcManipulation( equipSlot, _newManipVariant, _newManipSetId, new ImcEntry() ); break; case ObjectType.DemiHuman: case ObjectType.Weapon: case ObjectType.Monster: RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); - //newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, - // _newManipVariant, new ImcFile.ImageChangeData() ); + imc = new ImcManipulation( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, + _newManipVariant, new ImcEntry() ); break; } + newManip = new MetaManipulation( new ImcManipulation( imc.ObjectType, imc.BodySlot, imc.PrimaryId, imc.SecondaryId, + imc.Variant, imc.EquipSlot, ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ) ) ); + break; } case MetaManipulation.Type.Eqdp: @@ -672,76 +664,50 @@ public partial class SettingsInterface CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); CustomCombo( "Race", Races, out var race, ref _newManipRace ); CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - //newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId, - // new EqdpEntry() ); + var eqdp = new EqdpManipulation( new EqdpEntry(), equipSlot, gender, race, _newManipSetId ); + newManip = new MetaManipulation( new EqdpManipulation( ExpandedEqdpFile.GetDefault( eqdp.FileIndex(), eqdp.SetId ), + equipSlot, gender, race, _newManipSetId ) ); break; } case MetaManipulation.Type.Eqp: { RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - //newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 ); + newManip = new MetaManipulation( new EqpManipulation( ExpandedEqpFile.GetDefault( _newManipSetId ) & Eqp.Mask( equipSlot ), + equipSlot, _newManipSetId ) ); break; } case MetaManipulation.Type.Est: { RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Object Type", ObjectTypes, out var objectType, ref _newManipObjectType ); - EquipSlot equipSlot = default; - BodySlot bodySlot = default; - switch( ( ObjectType )_newManipObjectType ) - { - case ObjectType.Equipment: - CustomCombo( "Equipment Slot", EstEquipSlots, out equipSlot, ref _newManipEquipSlot ); - break; - case ObjectType.Character: - CustomCombo( "Body Slot", EstBodySlots, out bodySlot, ref _newManipBodySlot ); - break; - } - + CustomCombo( "Est Type", EstTypes, out var estType, ref _newManipObjectType ); CustomCombo( "Race", Races, out var race, ref _newManipRace ); CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - //newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot, - // ( ushort )_newManipSetId, 0 ); + newManip = new MetaManipulation( new EstManipulation( gender, race, estType, _newManipSetId, + EstFile.GetDefault( estType, Names.CombinedRace( gender, race ), _newManipSetId ) ) ); break; } case MetaManipulation.Type.Gmp: RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); - //newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() ); + newManip = new MetaManipulation( new GmpManipulation( ExpandedGmpFile.GetDefault( _newManipSetId ), _newManipSetId ) ); break; case MetaManipulation.Type.Rsp: CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); - //newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f ); + newManip = new MetaManipulation( new RspManipulation( subRace, rspAttribute, + CmpFile.GetDefault( subRace, rspAttribute ) ) ); break; } - //if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) - // && newManip != null - // && list.All( m => m.Identifier != newManip.Value.Identifier ) ) - //{ - // var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); - // if( def != null ) - // { - // var manip = newManip.Value.Type switch - // { - // MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ), - // MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ), - // MetaType.Imc => new MetaManipulation( newManip.Value.Identifier, - // ( ( ImcFile.ImageChangeData )def ).ToInteger() ), - // MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace, - // newManip.Value.RspIdentifier.Attribute, ( float )def ), - // _ => throw new InvalidEnumArgumentException(), - // }; - // list.Add( manip ); - // change = true; - // ++count; - // } - // - // ImGui.CloseCurrentPopup(); - //} + if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) + && newManip != null + && list.All( m => !m.Equals( newManip ) ) ) + { + list.Add( newManip.Value ); + change = true; + ++count; + ImGui.CloseCurrentPopup(); + } return change; } diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 507d3fe7..af8f613f 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -87,9 +87,10 @@ public partial class SettingsInterface if( ImGui.IsItemClicked() ) { - var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData(); + var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); + var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); } diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 9d099eb0..d258231a 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -27,10 +27,10 @@ public partial class SettingsInterface public TabSettings( SettingsInterface ui ) { - _base = ui; - _config = Penumbra.Config; - _configChanged = false; - _newModDirectory = _config.ModDirectory; + _base = ui; + _config = Penumbra.Config; + _configChanged = false; + _newModDirectory = _config.ModDirectory; } private static bool DrawPressEnterWarning( string old ) @@ -239,44 +239,6 @@ public partial class SettingsInterface ImGuiComponents.HelpMarker( "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); } - private void DrawEnabledPlayerWatcher() - { - var enabled = _config.EnablePlayerWatch; - if( ImGui.Checkbox( "Enable Automatic Character Redraws", ref enabled ) ) - { - _config.EnablePlayerWatch = enabled; - _configChanged = true; - Penumbra.PlayerWatcher.SetStatus( enabled ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "If this setting is enabled, Penumbra will keep tabs on characters that have a corresponding character collection setup in the Collections tab.\n" - + "Penumbra will try to automatically redraw those characters using their collection when they first appear in an instance, or when they change their current equip.\n" ); - - if( !_config.EnablePlayerWatch || !_config.ShowAdvanced ) - { - return; - } - - var waitFrames = _config.WaitFrames; - ImGui.SameLine(); - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "Wait Frames", ref waitFrames, 0, 0 ) - && waitFrames != _config.WaitFrames - && waitFrames is > 0 and < 3000 ) - { - _base._penumbra.ObjectReloader.DefaultWaitFrames = waitFrames; - _config.WaitFrames = waitFrames; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "The number of frames penumbra waits after some events (like zone changes) until it starts trying to redraw actors again, in a range of [1, 3001].\n" - + "Keep this as low as possible while producing stable results." ); - } - private static void DrawReloadResourceButton() { if( ImGui.Button( "Reload Resident Resources" ) ) @@ -388,7 +350,6 @@ public partial class SettingsInterface ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawEnabledBox(); - DrawEnabledPlayerWatcher(); ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawScaleModSelectorBox(); From 0ba0c6d057512b18678558ea8ee4c0977c033ca9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Mar 2022 16:43:31 +0100 Subject: [PATCH 0099/2451] Change handling of tex and shpk files to be simpler. --- .../Interop/Resolver/PathResolver.Material.cs | 39 ++++++++++++------- Penumbra/Interop/Resolver/PathResolver.cs | 6 ++- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 8fde48cc..a17efab2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,6 +4,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData.ByteString; using Penumbra.Interop.Structs; +using Penumbra.Mods; namespace Penumbra.Interop.Resolver; @@ -20,7 +21,9 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { LoadMtrlTexHelper( mtrlResourceHandle ); - return LoadMtrlTexHook!.Original( mtrlResourceHandle ); + var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle ); + _mtrlCollection = null; + return ret; } [Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", @@ -30,11 +33,12 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { LoadMtrlShpkHelper( mtrlResourceHandle ); - return LoadMtrlShpkHook!.Original( mtrlResourceHandle ); + var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle ); + _mtrlCollection = null; + return ret; } - // Taken from the actual hooked function. The Shader string is just concatenated to this base directory. - private static readonly Utf8String ShaderBase = Utf8String.FromStringUnsafe( "shader/sm5/shpk", true ); + private ModCollection? _mtrlCollection; private void LoadMtrlShpkHelper( IntPtr mtrlResourceHandle ) { @@ -43,11 +47,9 @@ public unsafe partial class PathResolver return; } - var mtrl = ( MtrlResource* )mtrlResourceHandle; - var shpkPath = Utf8String.Join( ( byte )'/', ShaderBase, new Utf8String( mtrl->ShpkString ).AsciiToLower() ); - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - var collection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; - SetCollection( shpkPath, collection ); + var mtrl = ( MtrlResource* )mtrlResourceHandle; + var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; } private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) @@ -63,14 +65,21 @@ public unsafe partial class PathResolver return; } - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - var collection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; - var x = PathCollections.ToList(); - for( var i = 0; i < mtrl->NumTex; ++i ) + var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; + } + + // Check specifically for shpk and tex files whether we are currently in a material load. + private bool HandleMaterialSubFiles( Utf8GamePath gamePath, out ModCollection? collection ) + { + if( _mtrlCollection != null && ( gamePath.Path.EndsWith( 't', 'e', 'x' ) || gamePath.Path.EndsWith( 's', 'h', 'p', 'k' ) ) ) { - var texString = new Utf8String( mtrl->TexString( i ) ); - SetCollection( texString, collection ); + collection = _mtrlCollection; + return true; } + + collection = null; + return false; } private void EnableMtrlHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 894727d4..56eda2c0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -29,8 +29,10 @@ public partial class PathResolver : IDisposable // The modified resolver that handles game path resolving. private (FullPath?, object?) CharacterResolver( Utf8GamePath gamePath ) { - // Check if the path was marked for a specific collection, if not use the default collection. - var nonDefault = PathCollections.TryGetValue( gamePath.Path, out var collection ); + // Check if the path was marked for a specific collection, + // or if it is a file loaded by a material, and if we are currently in a material load. + // If not use the default collection. + var nonDefault = HandleMaterialSubFiles( gamePath, out var collection ) || PathCollections.TryGetValue( gamePath.Path, out collection ); if( !nonDefault ) { collection = Penumbra.ModManager.Collections.DefaultCollection; From 5ed80c753f1022575c7a31a0bedebaeb5889ce86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Mar 2022 10:07:59 +0100 Subject: [PATCH 0100/2451] File fixes. --- Penumbra.GameData/ByteString/Utf8GamePath.cs | 5 ++--- Penumbra.GameData/ByteString/Utf8RelPath.cs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData/ByteString/Utf8GamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs index b0d1778f..afd5d12b 100644 --- a/Penumbra.GameData/ByteString/Utf8GamePath.cs +++ b/Penumbra.GameData/ByteString/Utf8GamePath.cs @@ -70,8 +70,7 @@ public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< U return true; } - var substring = s!.Replace( '\\', '/' ); - substring.TrimStart( '/' ); + var substring = s!.Replace( '\\', '/' ).TrimStart( '/' ); if( substring.Length > MaxGamePathLength ) { return false; @@ -99,7 +98,7 @@ public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< U return false; } - var substring = file.FullName[ baseDir.FullName.Length.. ]; + var substring = file.FullName[ ( baseDir.FullName.Length + 1 ).. ]; return FromString( substring, out path, toLower ); } diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs index d0259cc4..f9321944 100644 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -37,8 +37,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf return true; } - var substring = s!.Replace( '/', '\\' ); - substring.TrimStart( '\\' ); + var substring = s!.Replace( '/', '\\' ).TrimStart('\\'); if( substring.Length > MaxRelPathLength ) { return false; From c7344efdc2364b656b485baa8cccb883294b52c2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Mar 2022 10:09:17 +0100 Subject: [PATCH 0101/2451] Female Hrothgar resource handle related fixes... --- Penumbra/Interop/CharacterUtility.cs | 21 +++++++++++--------- Penumbra/Interop/Structs/CharacterUtility.cs | 5 +++-- Penumbra/Meta/Files/EqdpFile.cs | 5 +++-- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 12 ++++++----- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 11d54eb4..7923254b 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -3,6 +3,7 @@ using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; +using ImGuiScene; namespace Penumbra.Interop; @@ -32,7 +33,7 @@ public unsafe class CharacterUtility : IDisposable .Append( Structs.CharacterUtility.GmpIdx ) #endif #if USE_EQDP - .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) ) + .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ).Where( i => i != 17 ) ) // TODO: Female Hrothgar #endif #if USE_CMP .Append( Structs.CharacterUtility.HumanCmpIdx ) @@ -42,7 +43,7 @@ public unsafe class CharacterUtility : IDisposable #endif .ToArray(); - private static readonly int[] ReverseIndices + public static readonly int[] ReverseIndices = Enumerable.Range( 0, Structs.CharacterUtility.NumResources ) .Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray(); @@ -86,20 +87,22 @@ public unsafe class CharacterUtility : IDisposable } // Set the data of one of the stored resources to a given pointer and length. - public bool SetResource( int idx, IntPtr data, int length ) + public bool SetResource( int resourceIdx, IntPtr data, int length ) { - var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; + var resource = Address->Resource( resourceIdx ); var ret = resource->SetData( data, length ); - PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", idx, ( ulong )data, length ); + PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); return ret; } // Reset the data of one of the stored resources to its default values. - public void ResetResource( int fileIdx ) + public void ResetResource( int resourceIdx ) { - var (data, size) = DefaultResources[ ReverseIndices[ fileIdx ] ]; - var resource = ( Structs.ResourceHandle* )Address->Resources[ fileIdx ]; - resource->SetData( data, size ); + var relevantIdx = ReverseIndices[ resourceIdx ]; + var (data, length) = DefaultResources[ relevantIdx ]; + var resource = Address->Resource( resourceIdx ); + PluginLog.Verbose( "Reset resource {Idx} to default at 0x{DefaultData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); + resource->SetData( data, length ); } // Return all relevant resources to the default resource. diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index aaceee6b..fc4e4bee 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -8,8 +8,9 @@ namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] public unsafe struct CharacterUtility { + // TODO: female Hrothgar public static readonly int[] EqdpIndices - = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).ToArray(); + = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).Where( i => i != EqdpStartIdx + 15 ).ToArray(); public const int NumResources = 85; public const int EqpIdx = 0; @@ -41,7 +42,7 @@ public unsafe struct CharacterUtility 1301 => EqdpStartIdx + 12, 1401 => EqdpStartIdx + 13, 1501 => EqdpStartIdx + 14, - 1601 => EqdpStartIdx + 15, // Does not exist yet + 1601 => EqdpStartIdx + 15, // TODO: female Hrothgar 1701 => EqdpStartIdx + 16, 1801 => EqdpStartIdx + 17, 0104 => EqdpStartIdx + 18, diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 6d4ebe8d..c0f2add2 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -120,8 +120,10 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile => GetDefault( Index, setIdx ); public static EqdpEntry GetDefault( int fileIdx, int setIdx ) + => GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( fileIdx ).Address, setIdx ); + + public static EqdpEntry GetDefault( byte* data, int setIdx ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( fileIdx ).Address; var blockSize = *( ushort* )( data + IdentifierSize ); var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 ); @@ -138,7 +140,6 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 ); - var x = new ReadOnlySpan< ushort >( blockData, blockSize ); return ( EqdpEntry )( *( blockData + setIdx % blockSize ) ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 8edf1150..1dba4520 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -6,6 +6,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Util; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager { public struct MetaManagerEqdp : IDisposable { - public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles]; + public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 1]; // TODO: female Hrothgar public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new(); @@ -23,9 +24,9 @@ public partial class MetaManager [Conditional( "USE_EQDP" )] public void SetFiles() { - foreach( var idx in CharacterUtility.EqdpIndices ) + for( var i = 0; i < CharacterUtility.EqdpIndices.Length; ++i ) { - SetFile( Files[ idx - CharacterUtility.EqdpStartIdx ], idx ); + SetFile( Files[ i ], CharacterUtility.EqdpIndices[ i ] ); } } @@ -57,7 +58,8 @@ public partial class MetaManager return false; } - var file = Files[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); + var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= + new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar return m.Apply( file ); #else return false; @@ -65,7 +67,7 @@ public partial class MetaManager } public ExpandedEqdpFile? File( GenderRace race, bool accessory ) - => Files[ CharacterUtility.EqdpIdx( race, accessory ) - CharacterUtility.EqdpStartIdx ]; + => Files[ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar public void Dispose() { From 6949011acf9daea64b1fce7ac7a1f11b8d49e52b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Mar 2022 10:11:26 +0100 Subject: [PATCH 0102/2451] Fix for EQDP not working on redraws due to resetting itself early. --- .../Interop/Resolver/PathResolver.Meta.cs | 42 +++++++++++++++---- .../Interop/Resolver/PathResolver.Resolve.cs | 6 +-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index f92750e9..243ee3fd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -1,6 +1,8 @@ using System; using Dalamud.Hooking; +using Dalamud.Logging; using Dalamud.Utility.Signatures; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -171,12 +173,26 @@ public unsafe partial class PathResolver // Small helper to handle setting metadata and reverting it at the end of the function. + // Since eqp and eqdp may be called multiple times in a row, we need to count them, + // so that we do not reset the files too early. private readonly struct MetaChanger : IDisposable { + private static int _eqpCounter; + private static int _eqdpCounter; private readonly MetaManipulation.Type _type; private MetaChanger( MetaManipulation.Type type ) - => _type = type; + { + _type = type; + if( type == MetaManipulation.Type.Eqp ) + { + ++_eqpCounter; + } + else if( type == MetaManipulation.Type.Eqdp ) + { + ++_eqdpCounter; + } + } public static MetaChanger ChangeEqp( ModCollection collection ) { @@ -200,13 +216,17 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeEqdp( PathResolver resolver, IntPtr drawObject ) + // We only need to change anything if it is actually equipment here. + public static MetaChanger ChangeEqdp( PathResolver resolver, IntPtr drawObject, uint modelType ) { #if USE_EQDP - var collection = resolver.GetCollection( drawObject ); - if( collection != null ) + if( modelType < 10 ) { - return ChangeEqdp( collection ); + var collection = resolver.GetCollection( drawObject ); + if( collection != null ) + { + return ChangeEqdp( collection ); + } } #endif return new MetaChanger( MetaManipulation.Type.Unknown ); @@ -287,10 +307,18 @@ public unsafe partial class PathResolver switch( _type ) { case MetaManipulation.Type.Eqdp: - Penumbra.ModManager.Collections.DefaultCollection.SetEqdpFiles(); + if( --_eqdpCounter == 0 ) + { + Penumbra.ModManager.Collections.DefaultCollection.SetEqdpFiles(); + } + break; case MetaManipulation.Type.Eqp: - Penumbra.ModManager.Collections.DefaultCollection.SetEqpFiles(); + if( --_eqpCounter == 0 ) + { + Penumbra.ModManager.Collections.DefaultCollection.SetEqpFiles(); + } + break; case MetaManipulation.Type.Est: Penumbra.ModManager.Collections.DefaultCollection.SetEstFiles(); diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index afc6b2c0..70fba613 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -20,10 +20,10 @@ public unsafe partial class PathResolver private IntPtr ResolveMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) => ResolvePathDetour( drawObject, ResolveMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) { - using var eqdp = MetaChanger.ChangeEqdp( this, drawObject ); - return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + using var eqdp = MetaChanger.ChangeEqdp( this, drawObject, modelType ); + return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); } private IntPtr ResolveMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) From d07355c0f87241fa4bdc941cf7f08a15c25faf52 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Mar 2022 11:07:36 +0100 Subject: [PATCH 0103/2451] Fix behaviour for non-main-map resources in debug. --- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 11 +++++++---- Penumbra/Meta/MetaCollection.cs | 1 - Penumbra/Penumbra.cs | 7 ------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 07b225c1..6e876ab4 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -21,7 +21,7 @@ public unsafe partial class ResourceLoader { public ResourceHandle* OriginalResource; public ResourceHandle* ManipulatedResource; - public Utf8GamePath OriginalPath; + public Utf8GamePath OriginalPath; public FullPath ManipulatedPath; public ResourceCategory Category; public object? ResolverInfo; @@ -52,7 +52,7 @@ public unsafe partial class ResourceLoader } var crc = ( uint )originalPath.Path.Crc32; - var originalResource = ( *ResourceManager )->FindResourceHandle( &handle->Category, &handle->FileType, &crc ); + var originalResource = FindResource( handle->Category, handle->FileType, crc ); _debugList[ manipulatedPath.Value ] = new DebugData() { OriginalResource = originalResource, @@ -113,9 +113,12 @@ public unsafe partial class ResourceLoader // Find a resource in the resource manager by its category, extension and crc-hash public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 ) { - var manager = *ResourceManager; + var manager = *ResourceManager; + var catIdx = ( uint )cat >> 0x18; + cat = ( ResourceCategory )( ushort )cat; var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; - var extMap = FindInMap( category->MainMap, ext ); + var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ], + ext ); if( extMap == null ) { return null; diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 8e0337e6..7dc29720 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -77,7 +77,6 @@ public class MetaCollection // and that the contained manipulations are still valid and non-default manipulations. public bool Validate( ModMeta modMeta ) { - var defaultFiles = Penumbra.MetaDefaults; SortLists(); foreach( var group in GroupData ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a487b7f9..862a9a79 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -10,19 +10,14 @@ using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; using Penumbra.Mods; -using Penumbra.PlayerWatch; using Penumbra.UI; using Penumbra.Util; using System.Linq; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Meta.Manipulations; namespace Penumbra; -public class MetaDefaults -{ } - public class Penumbra : IDalamudPlugin { public string Name @@ -38,7 +33,6 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static MetaDefaults MetaDefaults { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; @@ -68,7 +62,6 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); - MetaDefaults = new MetaDefaults(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new ModManager(); From d03a3168b0590b3d31f68f56b0b813b21df0a00f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Mar 2022 21:48:51 +0100 Subject: [PATCH 0104/2451] Fix IMC handling. --- .../Interop/Loader/ResourceLoader.Debug.cs | 25 ++++++------ .../Loader/ResourceLoader.Replacement.cs | 4 +- Penumbra/Interop/Loader/ResourceLoader.cs | 2 +- Penumbra/Interop/ObjectReloader.cs | 15 ++++---- Penumbra/Interop/Structs/ResourceHandle.cs | 17 ++++++++- Penumbra/Meta/Files/ImcFile.cs | 14 ++++--- Penumbra/Meta/Manager/MetaManager.Imc.cs | 38 ++++++++++++++----- Penumbra/Mods/CollectionManager.cs | 3 +- 8 files changed, 77 insertions(+), 41 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 6e876ab4..b39a3c57 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -19,13 +19,13 @@ public unsafe partial class ResourceLoader // Gather some debugging data about penumbra-loaded objects. public struct DebugData { - public ResourceHandle* OriginalResource; - public ResourceHandle* ManipulatedResource; - public Utf8GamePath OriginalPath; - public FullPath ManipulatedPath; - public ResourceCategory Category; - public object? ResolverInfo; - public uint Extension; + public Structs.ResourceHandle* OriginalResource; + public Structs.ResourceHandle* ManipulatedResource; + public Utf8GamePath OriginalPath; + public FullPath ManipulatedPath; + public ResourceCategory Category; + public object? ResolverInfo; + public uint Extension; } private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); @@ -44,7 +44,8 @@ public unsafe partial class ResourceLoader ResourceLoaded -= AddModifiedDebugInfo; } - private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) + private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + object? resolverInfo ) { if( manipulatedPath == null ) { @@ -55,7 +56,7 @@ public unsafe partial class ResourceLoader var originalResource = FindResource( handle->Category, handle->FileType, crc ); _debugList[ manipulatedPath.Value ] = new DebugData() { - OriginalResource = originalResource, + OriginalResource = ( Structs.ResourceHandle* )originalResource, ManipulatedResource = handle, Category = handle->Category, Extension = handle->FileType, @@ -172,8 +173,8 @@ public unsafe partial class ResourceLoader { _deleteList.Add( ( data.ManipulatedPath, data with { - OriginalResource = regularResource, - ManipulatedResource = modifiedResource, + OriginalResource = ( Structs.ResourceHandle* )regularResource, + ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource, } ) ); } } @@ -195,7 +196,7 @@ public unsafe partial class ResourceLoader private static void LogPath( Utf8GamePath path, bool synchronous ) => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) + private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index b9473345..a7b5ddfb 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -72,7 +72,7 @@ public unsafe partial class ResourceLoader if( resolvedPath == null ) { var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); - ResourceLoaded?.Invoke( retUnmodified, gamePath, null, data ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); return retUnmodified; } @@ -80,7 +80,7 @@ public unsafe partial class ResourceLoader *resourceHash = resolvedPath.Value.InternalName.Crc32; path = resolvedPath.Value.InternalName.Path; var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); - ResourceLoaded?.Invoke( retModified, gamePath, resolvedPath.Value, data ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retModified, gamePath, resolvedPath.Value, data ); return retModified; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 161bbd07..98457acc 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -105,7 +105,7 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is returned. // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. // resolveData is additional data returned by the current ResolvePath function and is user-defined. - public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + public delegate void ResourceLoadedDelegate( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 56a40584..6d345858 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; @@ -33,11 +34,11 @@ public unsafe class ObjectReloader : IDisposable public static DrawState* ActorDrawState( GameObject actor ) => ( DrawState* )( actor.Address + 0x0104 ); - private static delegate*< IntPtr, void > GetDisableDraw( GameObject actor ) - => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]; + private static void DisableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address ); - private static delegate*< IntPtr, void > GetEnableDraw( GameObject actor ) - => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]; + private static void EnableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); public ObjectReloader( ModManager mods ) => _mods = mods; @@ -57,14 +58,14 @@ public unsafe class ObjectReloader : IDisposable _changedSettings = false; } - private unsafe void WriteInvisible( GameObject actor, int actorIdx ) + private void WriteInvisible( GameObject actor, int actorIdx ) { _currentObjectStartState = *ActorDrawState( actor ); *ActorDrawState( actor ) |= DrawState.Invisibility; if( _inGPose ) { - GetDisableDraw( actor )( actor.Address ); + DisableDraw( actor ); } } @@ -95,7 +96,7 @@ public unsafe class ObjectReloader : IDisposable if( _inGPose ) { - GetEnableDraw( actor )( actor.Address ); + EnableDraw( actor ); } } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 9493a2eb..447fa361 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.Resource; namespace Penumbra.Interop.Structs; @@ -40,18 +41,30 @@ public unsafe struct ResourceHandle [FieldOffset( 0x00 )] public void** VTable; + [FieldOffset( 0x08 )] + public ResourceCategory Category; + + [FieldOffset( 0x0C )] + public uint FileType; + + [FieldOffset( 0x10 )] + public uint Id; + [FieldOffset( 0x48 )] public byte* FileNameData; [FieldOffset( 0x58 )] public int FileNameLength; + [FieldOffset( 0xAC )] + public uint RefCount; + // May return null. public static byte* GetData( ResourceHandle* handle ) - => ( ( delegate*< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); public static ulong GetLength( ResourceHandle* handle ) - => ( ( delegate*< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); // Only use these if you know what you are doing. diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index f3ce0a4a..7f2e27fc 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Numerics; using Dalamud.Logging; using Dalamud.Memory; @@ -8,7 +7,6 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; -using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -68,8 +66,9 @@ public unsafe class ImcFile : MetaBaseFile public int Count => CountInternal( Data ); - public readonly int NumParts; public readonly Utf8GamePath Path; + public readonly int NumParts; + public bool ChangesSinceLoad = true; private static int CountInternal( byte* data ) => *( ushort* )data; @@ -179,7 +178,8 @@ public unsafe class ImcFile : MetaBaseFile return false; } - *variantPtr = entry; + *variantPtr = entry; + ChangesSinceLoad = true; return true; } @@ -192,6 +192,8 @@ public unsafe class ImcFile : MetaBaseFile Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); } + + ChangesSinceLoad = true; } public ImcFile( Utf8GamePath path ) @@ -209,9 +211,8 @@ public unsafe class ImcFile : MetaBaseFile fixed( byte* ptr = file.Data ) { NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); - AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts ); + AllocateData( file.Data.Length ); Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); - Functions.MemSet( Data + file.Data.Length, 0, sizeof( ImcEntry ) * 100 * NumParts ); } } @@ -239,6 +240,7 @@ public unsafe class ImcFile : MetaBaseFile } var requiredLength = ActualLength; + resource->SetData( (IntPtr) Data, Length ); if( length >= requiredLength ) { Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 8c4fb7c2..8dee907f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; @@ -57,7 +58,6 @@ public partial class MetaManager public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) { - const uint imcExt = 0x00696D63; #if USE_IMC if( !Manipulations.TryAdd( m, mod ) ) { @@ -82,12 +82,6 @@ public partial class MetaManager _collection.Cache.ResolvedFiles[ path ] = fullPath; } - var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 ); - if( resource != null ) - { - file.Replace( ( ResourceHandle* )resource ); - } - return true; #else return false; @@ -109,22 +103,25 @@ public partial class MetaManager [Conditional( "USE_IMC" )] private unsafe void SetupDelegate() { - Penumbra.ResourceLoader.ResourceLoadCustomization = ImcHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } [Conditional( "USE_IMC" )] private unsafe void RestoreDelegate() { - if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcHandler ) + if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) { Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; } + + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; } private FullPath CreateImcPath( Utf8GamePath path ) => new($"|{_collection.Name}|{path}"); - private static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, + private static unsafe byte ImcLoadHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { var split = gamePath.Path.Split( ( byte )'|', 2, true ); @@ -137,12 +134,33 @@ public partial class MetaManager && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) { + PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", gamePath, + collection.Name ); file.Replace( fileDescriptor->ResourceHandle ); + file.ChangesSinceLoad = false; } fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; return ret; } + + private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) + { + // Only check imcs. + if( resource->FileType != 0x00696D63 + || resolveData is not ModCollection collection + || collection.Cache == null + || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) + || !file.ChangesSinceLoad ) + { + return; + } + + PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, + collection.Name ); + file.Replace( resource ); + file.ChangesSinceLoad = false; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index fa3835da..cbada433 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -25,7 +26,7 @@ public class CollectionManager private readonly ModManager _manager; public string CollectionChangedTo { get; private set; } = string.Empty; - public Dictionary< string, ModCollection > Collections { get; } = new(); + public Dictionary< string, ModCollection > Collections { get; } = new(StringComparer.InvariantCultureIgnoreCase); public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; From 4888bc243f30ef7fb9fdd0d83d171bb762509113 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 11:20:50 +0100 Subject: [PATCH 0105/2451] Misc fixes --- Penumbra/Meta/Files/EqdpFile.cs | 12 ++++-------- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index c0f2add2..6de0007d 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -66,12 +66,10 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile var def = ( byte* )DefaultData.Data; Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); - var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); - var dataBasePtr = controlPtr + BlockCount; - var myDataPtrStart = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); - var myDataPtr = myDataPtrStart; - var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize ); - var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 ); + var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); + var dataBasePtr = controlPtr + BlockCount; + var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); + var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize ); for( var i = 0; i < BlockCount; ++i ) { if( controlPtr[ i ] == CollapsedBlock ) @@ -80,8 +78,6 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } else { - var y = new ReadOnlySpan< ushort >( dataBasePtr + controlPtr[ i ], BlockSize ); - var z = new ReadOnlySpan< ushort >( myDataPtr, BlockSize ); Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 8dee907f..a67f79c6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -56,7 +56,7 @@ public partial class MetaManager Manipulations.Clear(); } - public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + public bool ApplyMod( ImcManipulation m, Mod.Mod mod ) { #if USE_IMC if( !Manipulations.TryAdd( m, mod ) ) From 3ef3e75c6a0c3a682e53161d416f4c9e8df709a3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 12:23:05 +0100 Subject: [PATCH 0106/2451] Removed ActiveCollection since it is no longer needed. --- Penumbra/Interop/MusicManager.cs | 14 +++-- Penumbra/Interop/ObjectReloader.cs | 2 - Penumbra/Mods/CollectionManager.cs | 50 +++------------- Penumbra/Mods/ModCollection.cs | 16 +++--- Penumbra/Mods/ModManager.cs | 2 +- Penumbra/Mods/ModManagerEditExtensions.cs | 2 +- Penumbra/UI/MenuTabs/TabChangedItems.cs | 4 +- Penumbra/UI/MenuTabs/TabDebug.cs | 57 +------------------ Penumbra/UI/MenuTabs/TabEffective.cs | 26 ++++----- .../TabInstalled/TabInstalledDetails.cs | 2 +- .../TabInstalled/TabInstalledSelector.cs | 6 +- Penumbra/UI/SettingsInterface.cs | 3 +- 12 files changed, 48 insertions(+), 136 deletions(-) diff --git a/Penumbra/Interop/MusicManager.cs b/Penumbra/Interop/MusicManager.cs index 39885eef..7e7568d8 100644 --- a/Penumbra/Interop/MusicManager.cs +++ b/Penumbra/Interop/MusicManager.cs @@ -1,5 +1,6 @@ using System; using Dalamud.Logging; +using Dalamud.Utility.Signatures; namespace Penumbra.Interop; @@ -7,16 +8,17 @@ namespace Penumbra.Interop; // which will allow replacement of .scd files. public unsafe class MusicManager { + // The wildcard is the offset in framework to the MusicManager in Framework. + [Signature( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0", ScanType = ScanType.Text )] + private readonly IntPtr _musicInitCallLocation = IntPtr.Zero; + private readonly IntPtr _musicManager; public MusicManager() { - var framework = Dalamud.Framework.Address.BaseAddress; - // The wildcard is the offset in framework to the MusicManager in Framework. - var musicInitCallLocation = Dalamud.SigScanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); - var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); - PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", - musicInitCallLocation.ToInt64(), musicManagerOffset ); + SignatureHelper.Initialise( this ); + var framework = Dalamud.Framework.Address.BaseAddress; + var musicManagerOffset = *( int* )( _musicInitCallLocation + 3 ); _musicManager = *( IntPtr* )( framework + musicManagerOffset ); PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 6d345858..b28e54ab 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -48,13 +48,11 @@ public unsafe class ObjectReloader : IDisposable if( _currentObjectName != null && _mods.Collections.CharacterCollection.TryGetValue( _currentObjectName, out var collection ) ) { _changedSettings = true; - _mods.Collections.SetActiveCollection( collection, _currentObjectName ); } } private void RestoreSettings() { - _mods.Collections.ResetActiveCollection(); _changedSettings = false; } diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index cbada433..8018d942 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Lumina.Excel.GeneratedSheets; using Penumbra.Mod; using Penumbra.Util; @@ -25,14 +26,15 @@ public class CollectionManager { private readonly ModManager _manager; - public string CollectionChangedTo { get; private set; } = string.Empty; public Dictionary< string, ModCollection > Collections { get; } = new(StringComparer.InvariantCultureIgnoreCase); public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty; public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; - public ModCollection ActiveCollection { get; private set; } = ModCollection.Empty; + + public bool IsActive( ModCollection collection ) + => ReferenceEquals( collection, DefaultCollection ) || ReferenceEquals( collection, ForcedCollection ); // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; @@ -43,34 +45,8 @@ public class CollectionManager ReadCollections(); LoadConfigCollections( Penumbra.Config ); - SetActiveCollection( DefaultCollection, string.Empty ); } - public bool SetActiveCollection( ModCollection newActive, string name ) - { - CollectionChangedTo = name; - if( newActive == ActiveCollection ) - { - return false; - } - - if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) - { - ActiveCollection = newActive; - Penumbra.ResidentResources.Reload(); - ActiveCollection.SetFiles(); - } - else - { - ActiveCollection = newActive; - } - - return true; - } - - public bool ResetActiveCollection() - => SetActiveCollection( DefaultCollection, string.Empty ); - public void CreateNecessaryCaches() { AddCache( DefaultCollection ); @@ -121,7 +97,7 @@ public class CollectionManager } } - if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) + if( reloadMeta && DefaultCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) { Penumbra.ResidentResources.Reload(); } @@ -130,7 +106,7 @@ public class CollectionManager public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) { var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed == string.Empty || Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + if( nameFixed.Length == 0 || Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) { PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); return false; @@ -232,29 +208,21 @@ public class CollectionManager case CollectionType.Default: DefaultCollection = newCollection; Penumbra.Config.DefaultCollection = newCollection.Name; - if( CollectionChangedTo.Length == 0 ) - { - SetActiveCollection( newCollection, string.Empty ); - } - + Penumbra.ResidentResources.Reload(); + DefaultCollection.SetFiles(); break; case CollectionType.Forced: ForcedCollection = newCollection; Penumbra.Config.ForcedCollection = newCollection.Name; + Penumbra.ResidentResources.Reload(); break; case CollectionType.Current: CurrentCollection = newCollection; Penumbra.Config.CurrentCollection = newCollection.Name; break; case CollectionType.Character: - if( CollectionChangedTo == characterName && CharacterCollection.ContainsKey( characterName ) ) - { - SetActiveCollection( newCollection, CollectionChangedTo ); - } - CharacterCollection[ characterName! ] = newCollection; Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; - break; } diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 0408367a..8b285c36 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -6,8 +6,6 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.Interop.Structs; using Penumbra.Meta.Manager; using Penumbra.Mod; using Penumbra.Util; @@ -138,20 +136,20 @@ public class ModCollection } } - public void CalculateEffectiveFileList( bool withMetaManipulations, bool activeCollection ) + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) { - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, - withMetaManipulations, activeCollection ); + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); Cache ??= new ModCollectionCache( this ); UpdateSettings( false ); Cache.CalculateEffectiveFileList(); if( withMetaManipulations ) { Cache.UpdateMetaManipulations(); - if( activeCollection ) - { - Penumbra.ResidentResources.Reload(); - } + } + + if( reloadResident ) + { + Penumbra.ResidentResources.Reload(); } } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 4a7ba3bd..e391fa3e 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -248,7 +248,7 @@ public class ModManager public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) { - var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + var ret = Collections.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); return ret; } diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 6b8dde28..17e600c4 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -205,7 +205,7 @@ public static class ModManagerEditExtensions if( collection.Cache != null && settings.Enabled ) { collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - collection == manager.Collections.ActiveCollection ); + manager.Collections.IsActive( collection ) ); } } } diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index 9e45038d..87d0b58c 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -26,8 +26,8 @@ public partial class SettingsInterface } var modManager = Penumbra.ModManager; - var items = modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); - var forced = modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var items = modManager.Collections.DefaultCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index b0281631..ab109e49 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -6,7 +6,9 @@ using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -47,8 +49,6 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); var manager = Penumbra.ModManager; - PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.ActiveCollection.Cache != null ).ToString() ); PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); PrintValue( " has Cache", ( manager.Collections.CurrentCollection.Cache != null ).ToString() ); PrintValue( "Default Collection", manager.Collections.DefaultCollection.Name ); @@ -287,59 +287,6 @@ public partial class SettingsInterface return; } - var eqp = 0; - ImGui.InputInt( "##EqpInput", ref eqp ); - try - { - var def = ExpandedEqpFile.GetDefault( eqp ); - var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqp.File?[ eqp ] ?? def; - ImGui.Text( Convert.ToString( ( long )def, 2 ).PadLeft( 64, '0' ) ); - ImGui.Text( Convert.ToString( ( long )val, 2 ).PadLeft( 64, '0' ) ); - } - catch - { } - - var eqdp = 0; - ImGui.InputInt( "##EqdpInput", ref eqdp ); - try - { - var def = ExpandedEqdpFile.GetDefault( GenderRace.MidlanderMale, false, eqdp ); - var val = - Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqdp.File( GenderRace.MidlanderMale, false )?[ eqdp ] - ?? def; - ImGui.Text( Convert.ToString( ( ushort )def, 2 ).PadLeft( 16, '0' ) ); - ImGui.Text( Convert.ToString( ( ushort )val, 2 ).PadLeft( 16, '0' ) ); - } - catch - { } - - var est = 0; - ImGui.InputInt( "##EstInput", ref est ); - try - { - var def = EstFile.GetDefault( EstManipulation.EstType.Body, GenderRace.MidlanderFemale, ( ushort )est ); - var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Est.BodyFile?[ GenderRace.MidlanderFemale, - ( ushort )est ] - ?? def; - ImGui.Text( def.ToString() ); - ImGui.Text( val.ToString() ); - } - catch - { } - - var gmp = 0; - ImGui.InputInt( "##GmpInput", ref gmp ); - try - { - var def = ExpandedGmpFile.GetDefault( gmp ); - var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Gmp.File?[ gmp ] ?? def; - ImGui.Text( def.Value.ToString( "X" ) ); - ImGui.Text( val.Value.ToString( "X" ) ); - } - catch - { } - - if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) { return; diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index ae9f35b9..101ac00e 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -14,7 +14,7 @@ public partial class SettingsInterface { private class TabEffective { - private const string LabelTab = "Effective Changes"; + private const string LabelTab = "Effective Changes"; private string _gamePathFilter = string.Empty; private string _gamePathFilterLower = string.Empty; @@ -140,17 +140,17 @@ public partial class SettingsInterface const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - var modManager = Penumbra.ModManager; - var activeCollection = modManager.Collections.ActiveCollection.Cache; - var forcedCollection = modManager.Collections.ForcedCollection.Cache; + var modManager = Penumbra.ModManager; + var defaultCollection = modManager.Collections.DefaultCollection.Cache; + var forcedCollection = modManager.Collections.ForcedCollection.Cache; - var (activeResolved, activeMeta) = activeCollection != null - ? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count ) + var (defaultResolved, defaultMeta) = defaultCollection != null + ? ( defaultCollection.ResolvedFiles.Count, defaultCollection.MetaManipulations.Count ) : ( 0, 0 ); var (forcedResolved, forcedMeta) = forcedCollection != null ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count ) : ( 0, 0 ); - var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta; + var totalLines = defaultResolved + forcedResolved + defaultMeta + forcedMeta; if( totalLines == 0 ) { return; @@ -166,7 +166,7 @@ public partial class SettingsInterface if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 ) { - DrawFilteredRows( activeCollection, forcedCollection ); + DrawFilteredRows( defaultCollection, forcedCollection ); } else { @@ -185,18 +185,18 @@ public partial class SettingsInterface { var row = actualRow; ImGui.TableNextRow(); - if( row < activeResolved ) + if( row < defaultResolved ) { - var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); + var (gamePath, file) = defaultCollection!.ResolvedFiles.ElementAt( row ); DrawLine( gamePath, file ); } - else if( ( row -= activeResolved ) < activeMeta ) + else if( ( row -= defaultResolved ) < defaultMeta ) { // TODO //var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); DrawLine( 0.ToString(), 0.ToString() ); } - else if( ( row -= activeMeta ) < forcedResolved ) + else if( ( row -= defaultMeta ) < forcedResolved ) { var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); DrawLine( gamePath, file ); @@ -204,7 +204,7 @@ public partial class SettingsInterface else { // TODO - row -= forcedResolved; + row -= forcedResolved; //var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); DrawLine( 0.ToString(), 0.ToString() ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index d2000ba6..192126b1 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -426,7 +426,7 @@ public partial class SettingsInterface foreach( var collection in modManager.Collections.Collections.Values .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) { - collection.CalculateEffectiveFileList( false, collection == modManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( false, modManager.Collections.IsActive( collection ) ); } // If the mod is enabled in the current collection, its conflicts may have changed. diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index d805e41b..943bc84a 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -529,7 +529,7 @@ public partial class SettingsInterface var collection = Penumbra.ModManager.Collections.CurrentCollection; if( collection.Cache != null ) { - collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.ModManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( metaManips, Penumbra.ModManager.Collections.IsActive( collection ) ); } collection.Save(); @@ -602,8 +602,8 @@ public partial class SettingsInterface public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) { - _base = ui; - Cache = new ModListCache( Penumbra.ModManager, newMods ); + _base = ui; + Cache = new ModListCache( Penumbra.ModManager, newMods ); } private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 897419f3..915bf84d 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -68,8 +68,7 @@ public partial class SettingsInterface : IDisposable var current = modManager.Collections.CurrentCollection; if( current.Cache != null ) { - current.CalculateEffectiveFileList( recalculateMeta, - current == modManager.Collections.ActiveCollection ); + current.CalculateEffectiveFileList( recalculateMeta, modManager.Collections.IsActive( current ) ); _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } } From ad55d178d49b1324f99a0e97a3df6f5eecb46549 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 14:37:35 +0100 Subject: [PATCH 0107/2451] Support Weapons in character collections. --- Penumbra/Interop/Loader/ResourceLoader.cs | 1 - .../Interop/Resolver/PathResolver.Human.cs | 4 - .../Interop/Resolver/PathResolver.Material.cs | 1 - .../Interop/Resolver/PathResolver.Resolve.cs | 58 ++++++++++++ .../Interop/Resolver/PathResolver.Weapon.cs | 90 +++++++++++++++++++ Penumbra/Interop/Resolver/PathResolver.cs | 4 + Penumbra/Mods/CollectionManager.cs | 1 - 7 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 Penumbra/Interop/Resolver/PathResolver.Weapon.cs diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 98457acc..f02f0895 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -1,7 +1,6 @@ using System; using Dalamud.Utility.Signatures; using Penumbra.GameData.ByteString; -using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.Loader; diff --git a/Penumbra/Interop/Resolver/PathResolver.Human.cs b/Penumbra/Interop/Resolver/PathResolver.Human.cs index 2acba0f6..1ce81342 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Human.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Human.cs @@ -20,10 +20,6 @@ public unsafe partial class PathResolver // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] // public IntPtr* DrawObjectMonsterVTable; // - // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", - // ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectWeaponVTable; - // // public const int ResolveRootIdx = 71; public const int ResolveSklbIdx = 72; diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index a17efab2..01f1a2d1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData.ByteString; diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index 70fba613..77fba089 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.ByteString; using Penumbra.Mods; @@ -8,6 +9,7 @@ namespace Penumbra.Interop.Resolver; // The actual resolve detours are basically all the same. public unsafe partial class PathResolver { + // Humans private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) => ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); @@ -60,12 +62,68 @@ public unsafe partial class PathResolver => ResolvePathDetour( drawObject, ResolveVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // Weapons + private IntPtr ResolveWeaponDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveWeaponEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponEidPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveWeaponImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveWeaponMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveWeaponMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); + + private IntPtr ResolveWeaponMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveWeaponPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveWeaponPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveWeaponSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveWeaponSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveWeaponTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponTmbPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveWeaponVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPathDetour( drawObject, ResolveWeaponVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + + // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) => ResolvePathDetour( FindParent( drawObject, out var collection ) == null ? Penumbra.ModManager.Collections.DefaultCollection : collection, path ); + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private IntPtr ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path ) + { + var parentObject = ( ( DrawObject* )drawObject )->Object.ParentObject; + if( parentObject == null && LastGameObject != null ) + { + var collection = IdentifyCollection( LastGameObject ); + return ResolvePathDetour( collection, path ); + } + else + { + var parent = FindParent( ( IntPtr )parentObject, out var collection ); + return ResolvePathDetour( parent == null + ? Penumbra.ModManager.Collections.DefaultCollection + : collection, path ); + } + } // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] diff --git a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs new file mode 100644 index 00000000..7fc1f766 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs @@ -0,0 +1,90 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", + ScanType = ScanType.StaticAddress )] + public IntPtr* DrawObjectWeaponVTable; + + public Hook< GeneralResolveDelegate >? ResolveWeaponDecalPathHook; + public Hook< EidResolveDelegate >? ResolveWeaponEidPathHook; + public Hook< GeneralResolveDelegate >? ResolveWeaponImcPathHook; + public Hook< MPapResolveDelegate >? ResolveWeaponMPapPathHook; + public Hook< GeneralResolveDelegate >? ResolveWeaponMdlPathHook; + public Hook< MaterialResolveDetour >? ResolveWeaponMtrlPathHook; + public Hook< MaterialResolveDetour >? ResolveWeaponPapPathHook; + public Hook< GeneralResolveDelegate >? ResolveWeaponPhybPathHook; + public Hook< GeneralResolveDelegate >? ResolveWeaponSklbPathHook; + public Hook< GeneralResolveDelegate >? ResolveWeaponSkpPathHook; + public Hook< EidResolveDelegate >? ResolveWeaponTmbPathHook; + public Hook< MaterialResolveDetour >? ResolveWeaponVfxPathHook; + + + private void SetupWeaponHooks() + { + ResolveWeaponDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour ); + ResolveWeaponEidPathHook = new Hook< EidResolveDelegate >( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour ); + ResolveWeaponImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour ); + ResolveWeaponMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour ); + ResolveWeaponMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour ); + ResolveWeaponMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour ); + ResolveWeaponPapPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour ); + ResolveWeaponPhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour ); + ResolveWeaponSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour ); + ResolveWeaponSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour ); + ResolveWeaponTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour ); + ResolveWeaponVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour ); + } + + private void EnableWeaponHooks() + { + ResolveWeaponDecalPathHook?.Enable(); + ResolveWeaponEidPathHook?.Enable(); + ResolveWeaponImcPathHook?.Enable(); + ResolveWeaponMPapPathHook?.Enable(); + ResolveWeaponMdlPathHook?.Enable(); + ResolveWeaponMtrlPathHook?.Enable(); + ResolveWeaponPapPathHook?.Enable(); + ResolveWeaponPhybPathHook?.Enable(); + ResolveWeaponSklbPathHook?.Enable(); + ResolveWeaponSkpPathHook?.Enable(); + ResolveWeaponTmbPathHook?.Enable(); + ResolveWeaponVfxPathHook?.Enable(); + } + + private void DisableWeaponHooks() + { + ResolveWeaponDecalPathHook?.Disable(); + ResolveWeaponEidPathHook?.Disable(); + ResolveWeaponImcPathHook?.Disable(); + ResolveWeaponMPapPathHook?.Disable(); + ResolveWeaponMdlPathHook?.Disable(); + ResolveWeaponMtrlPathHook?.Disable(); + ResolveWeaponPapPathHook?.Disable(); + ResolveWeaponPhybPathHook?.Disable(); + ResolveWeaponSklbPathHook?.Disable(); + ResolveWeaponSkpPathHook?.Disable(); + ResolveWeaponTmbPathHook?.Disable(); + ResolveWeaponVfxPathHook?.Disable(); + } + + private void DisposeWeaponHooks() + { + ResolveWeaponDecalPathHook?.Dispose(); + ResolveWeaponEidPathHook?.Dispose(); + ResolveWeaponImcPathHook?.Dispose(); + ResolveWeaponMPapPathHook?.Dispose(); + ResolveWeaponMdlPathHook?.Dispose(); + ResolveWeaponMtrlPathHook?.Dispose(); + ResolveWeaponPapPathHook?.Dispose(); + ResolveWeaponPhybPathHook?.Dispose(); + ResolveWeaponSklbPathHook?.Dispose(); + ResolveWeaponSkpPathHook?.Dispose(); + ResolveWeaponTmbPathHook?.Dispose(); + ResolveWeaponVfxPathHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 56eda2c0..cef6c550 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -22,6 +22,7 @@ public partial class PathResolver : IDisposable _loader = loader; SignatureHelper.Initialise( this ); SetupHumanHooks(); + SetupWeaponHooks(); SetupMetaHooks(); Enable(); } @@ -78,6 +79,7 @@ public partial class PathResolver : IDisposable InitializeDrawObjects(); EnableHumanHooks(); + EnableWeaponHooks(); EnableMtrlHooks(); EnableDataHooks(); EnableMetaHooks(); @@ -89,6 +91,7 @@ public partial class PathResolver : IDisposable public void Disable() { DisableHumanHooks(); + DisableWeaponHooks(); DisableMtrlHooks(); DisableDataHooks(); DisableMetaHooks(); @@ -100,6 +103,7 @@ public partial class PathResolver : IDisposable { Disable(); DisposeHumanHooks(); + DisposeWeaponHooks(); DisposeMtrlHooks(); DisposeDataHooks(); DisposeMetaHooks(); diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 8018d942..0bb7182a 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; -using Lumina.Excel.GeneratedSheets; using Penumbra.Mod; using Penumbra.Util; From 98b4b29ff5ced970e8bde36c077e531e13c647f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 16:21:14 +0100 Subject: [PATCH 0108/2451] Add debug display for ResidentResources. --- Penumbra/Interop/ResidentResourceManager.cs | 4 +- .../Interop/Resolver/PathResolver.Resolve.cs | 3 + Penumbra/Interop/Structs/FileMode.cs | 4 +- .../Structs/ResidentResourceManager.cs | 19 +++++ Penumbra/Penumbra.csproj | 4 - Penumbra/UI/MenuTabs/TabDebug.cs | 32 +++++++ Penumbra/Util/RelPath.cs | 84 ------------------- Penumbra/Util/TempFile.cs | 11 +-- 8 files changed, 59 insertions(+), 102 deletions(-) create mode 100644 Penumbra/Interop/Structs/ResidentResourceManager.cs delete mode 100644 Penumbra/Util/RelPath.cs diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs index dbe49df1..8e75dcde 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -16,9 +16,9 @@ public unsafe class ResidentResourceManager // A static pointer to the resident resource manager address. [Signature( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05", ScanType = ScanType.StaticAddress )] - private readonly void** _residentResourceManagerAddress = null; + private readonly Structs.ResidentResourceManager** _residentResourceManagerAddress = null; - public void* Address + public Structs.ResidentResourceManager* Address => *_residentResourceManagerAddress; public ResidentResourceManager() diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index 77fba089..ce3ec0dd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -107,6 +107,9 @@ public unsafe partial class PathResolver ? Penumbra.ModManager.Collections.DefaultCollection : collection, path ); + // Weapons have the characters DrawObject as a parent, + // but that may not be set yet when creating a new object, so we have to do the same detour + // as for Human DrawObjects that are just being created. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path ) { diff --git a/Penumbra/Interop/Structs/FileMode.cs b/Penumbra/Interop/Structs/FileMode.cs index 21270176..13966e65 100644 --- a/Penumbra/Interop/Structs/FileMode.cs +++ b/Penumbra/Interop/Structs/FileMode.cs @@ -3,9 +3,9 @@ namespace Penumbra.Interop.Structs; public enum FileMode : uint { LoadUnpackedResource = 0, - LoadFileResource = 1, // Shit in My Games uses this + LoadFileResource = 1, // The config files in MyGames use this. - // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? + // Probably debug options only. LoadIndexResource = 0xA, // load index/index2 LoadSqPackResource = 0xB, } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResidentResourceManager.cs b/Penumbra/Interop/Structs/ResidentResourceManager.cs new file mode 100644 index 00000000..d5dd1715 --- /dev/null +++ b/Penumbra/Interop/Structs/ResidentResourceManager.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct ResidentResourceManager +{ + [FieldOffset( 0x00 )] + public void** VTable; + + [FieldOffset( 0x08 )] + public void** ResourceListVTable; + + [FieldOffset( 0x14 )] + public uint NumResources; + + [FieldOffset( 0x18 )] + public ResourceHandle** ResourceList; +} \ No newline at end of file diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 84c1b98d..13a25cc0 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -79,8 +79,4 @@ Always - - - - \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index ab109e49..4aba0ee8 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -327,6 +327,36 @@ public partial class SettingsInterface } } + + public unsafe void DrawDebugResidentResources() + { + if( !ImGui.CollapsingHeader( "Resident Resources##Debug" ) ) + { + return; + } + + if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) + { + return; + } + + if( !ImGui.BeginTable( "##Resident ResourcesDebugList", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) + { + var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( resource->FileName(), resource->FileName() + resource->FileNameLength ); + } + } + private unsafe void DrawPathResolverDebug() { if( !ImGui.CollapsingHeader( "Path Resolver##Debug" ) ) @@ -404,6 +434,8 @@ public partial class SettingsInterface ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawDebugResidentResources(); + ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); DrawDebugTabIpc(); diff --git a/Penumbra/Util/RelPath.cs b/Penumbra/Util/RelPath.cs deleted file mode 100644 index f4ce0021..00000000 --- a/Penumbra/Util/RelPath.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; - -namespace Penumbra.Util; - -public readonly struct RelPath : IComparable -{ - public const int MaxRelPathLength = 256; - - private readonly string _path; - - private RelPath( string path, bool _ ) - => _path = path; - - private RelPath( string? path ) - { - if( path != null && path.Length < MaxRelPathLength ) - { - _path = Trim( ReplaceSlash( path ) ); - } - else - { - _path = ""; - } - } - - public RelPath( FullPath file, DirectoryInfo baseDir ) - => _path = CheckPre( file.FullName, baseDir ) ? ReplaceSlash( Trim( Substring( file.FullName, baseDir ) ) ) : string.Empty; - - public RelPath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file.FullName, baseDir ) ? Trim( Substring( file.FullName, baseDir ) ) : string.Empty; - - public RelPath( GamePath gamePath ) - => _path = ReplaceSlash( gamePath ); - - public GamePath ToGamePath( int skipFolders = 0 ) - { - string p = this; - if( skipFolders > 0 ) - { - p = string.Join( "/", p.Split( '\\' ).Skip( skipFolders ) ); - return GamePath.GenerateUncheckedLower( p ); - } - - return GamePath.GenerateUncheckedLower( p.Replace( '\\', '/' ) ); - } - - private static bool CheckPre( string file, DirectoryInfo baseDir ) - => file.StartsWith( baseDir.FullName ) && file.Length < MaxRelPathLength; - - private static string Substring( string file, DirectoryInfo baseDir ) - => file.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '/', '\\' ); - - private static string Trim( string path ) - => path.TrimStart( '\\' ); - - public static implicit operator string( RelPath relPath ) - => relPath._path; - - public static explicit operator RelPath( string relPath ) - => new(relPath); - - public bool Empty - => _path.Length == 0; - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), - _ => -1, - }; - } - - public override string ToString() - => _path; -} \ No newline at end of file diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs index 4e2e22ca..19e8b47e 100644 --- a/Penumbra/Util/TempFile.cs +++ b/Penumbra/Util/TempFile.cs @@ -12,7 +12,7 @@ public static class TempFile { var name = Path.GetRandomFileName(); var path = new FileInfo( Path.Combine( baseDir.FullName, - suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) ); + suffix.Length > 0 ? name[ ..name.LastIndexOf( '.' ) ] + suffix : name ) ); if( !path.Exists ) { return path; @@ -21,13 +21,4 @@ public static class TempFile throw new IOException(); } - - public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" ) - { - var fileName = TempFileName( baseDir, suffix ); - using var stream = fileName.OpenWrite(); - stream.Write( data, 0, data.Length ); - fileName.Refresh(); - return fileName; - } } \ No newline at end of file From 7540694050991ac6f98121e2aa8bdf0a2936f9e4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Mar 2022 23:45:16 +0100 Subject: [PATCH 0109/2451] Fix IMC adding new variants. --- Penumbra/Meta/Files/ImcFile.cs | 33 ++++++++++-------------- Penumbra/Meta/Files/MetaBaseFile.cs | 26 +++++++++++++++++++ Penumbra/Meta/Manager/MetaManager.Imc.cs | 32 +++++++++++++---------- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 7f2e27fc..1a992a89 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -70,6 +70,9 @@ public unsafe class ImcFile : MetaBaseFile public readonly int NumParts; public bool ChangesSinceLoad = true; + public ReadOnlySpan< ImcEntry > Span + => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); + private static int CountInternal( byte* data ) => *( ushort* )data; @@ -89,24 +92,15 @@ public unsafe class ImcFile : MetaBaseFile private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) { - if( variantIdx == 0 ) - { - return DefaultPartPtr( data, partIdx ); - } - - --variantIdx; var flag = 1 << partIdx; - - if( ( PartMask( data ) & flag ) == 0 || variantIdx >= CountInternal( data ) ) + if( ( PartMask( data ) & flag ) == 0 || variantIdx > CountInternal( data ) ) { return null; } var numParts = BitOperations.PopCount( PartMask( data ) ); var ptr = ( ImcEntry* )( data + PreambleSize ); - ptr += numParts; - ptr += variantIdx * numParts; - ptr += partIdx; + ptr += variantIdx * numParts + partIdx; return ptr; } @@ -139,21 +133,22 @@ public unsafe class ImcFile : MetaBaseFile return true; } + var oldCount = Count; + *( ushort* )Data = ( ushort )numVariants; if( ActualLength > Length ) { - PluginLog.Warning( "Adding too many variants to IMC, size exceeded." ); - return false; + var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; + PluginLog.Verbose( "Resized IMC {Path} from {Length} to {NewLength}.", Path, Length, newLength ); + ResizeResources( newLength ); } var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); - var endPtr = defaultPtr + ( numVariants + 1 ) * NumParts; - for( var ptr = defaultPtr + NumParts; ptr < endPtr; ptr += NumParts ) + for( var i = oldCount + 1; i < numVariants + 1; ++i ) { - Functions.MemCpyUnchecked( ptr, defaultPtr, NumParts * sizeof( ImcEntry ) ); + Functions.MemCpyUnchecked( defaultPtr + i, defaultPtr, NumParts * sizeof( ImcEntry ) ); } - PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants ); - *( ushort* )Data = ( ushort )numVariants; + PluginLog.Verbose( "Expanded IMC {Path} from {Count} to {NewCount} variants.", Path, oldCount, numVariants ); return true; } @@ -240,7 +235,7 @@ public unsafe class ImcFile : MetaBaseFile } var requiredLength = ActualLength; - resource->SetData( (IntPtr) Data, Length ); + resource->SetData( ( IntPtr )Data, Length ); if( length >= requiredLength ) { Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 1d24cfb4..e6e79740 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,5 +1,6 @@ using System; using Dalamud.Memory; +using Penumbra.GameData.Util; namespace Penumbra.Meta.Files; @@ -44,6 +45,31 @@ public unsafe class MetaBaseFile : IDisposable Data = null; } + // Resize memory while retaining data. + protected void ResizeResources( int newLength ) + { + if( newLength == Length ) + { + return; + } + + var data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )newLength ); + if( newLength > Length ) + { + Functions.MemCpyUnchecked( data, Data, Length ); + Functions.MemSet( data + Length, 0, newLength - Length ); + } + else + { + Functions.MemCpyUnchecked( data, Data, newLength ); + } + + ReleaseUnmanagedResources(); + GC.AddMemoryPressure( newLength ); + Data = data; + Length = newLength; + } + // Manually free memory. public void Dispose() { diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index a67f79c6..c8c54a0e 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -20,7 +20,8 @@ public partial class MetaManager public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); private readonly ModCollection _collection; - private readonly ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate; + private static int _imcManagerCount; + private static ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate; public MetaManagerImc( ModCollection collection ) @@ -97,25 +98,30 @@ public partial class MetaManager Files.Clear(); Manipulations.Clear(); - RestoreDelegate(); } [Conditional( "USE_IMC" )] - private unsafe void SetupDelegate() + private static unsafe void SetupDelegate() { - Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; - Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; - } - - [Conditional( "USE_IMC" )] - private unsafe void RestoreDelegate() - { - if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) + if( _imcManagerCount++ == 0 ) { - Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; + Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } + } - Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; + [Conditional( "USE_IMC" )] + private static unsafe void RestoreDelegate() + { + if( --_imcManagerCount == 0 ) + { + if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) + { + Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; + } + + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; + } } private FullPath CreateImcPath( Utf8GamePath path ) From b08bf388cc6c06c7a8ffeca22b13fb2625198481 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Mar 2022 14:09:40 +0100 Subject: [PATCH 0110/2451] Change Redrawing to be simpler and not use a queue or settings. --- Penumbra.GameData/Enums/RedrawType.cs | 19 +- Penumbra/Api/ModsController.cs | 3 +- Penumbra/Api/PenumbraApi.cs | 3 +- Penumbra/Api/RedrawController.cs | 45 ++- Penumbra/Interop/ObjectReloader.cs | 427 +++++++++----------------- Penumbra/Penumbra.cs | 10 +- Penumbra/UI/MenuTabs/TabDebug.cs | 103 ------- 7 files changed, 182 insertions(+), 428 deletions(-) diff --git a/Penumbra.GameData/Enums/RedrawType.cs b/Penumbra.GameData/Enums/RedrawType.cs index c3668504..b4d11d64 100644 --- a/Penumbra.GameData/Enums/RedrawType.cs +++ b/Penumbra.GameData/Enums/RedrawType.cs @@ -1,14 +1,9 @@ -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum RedrawType { - public enum RedrawType - { - WithoutSettings, - WithSettings, - OnlyWithSettings, - Unload, - RedrawWithoutSettings, - RedrawWithSettings, - AfterGPoseWithSettings, - AfterGPoseWithoutSettings, - } + Redraw, + Unload, + Load, + AfterGPose, } \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index d4fc8b70..511ff0ea 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -24,8 +24,7 @@ public class ModsController : WebApiController x.Data.Meta, BasePath = x.Data.BasePath.FullName, Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), - } ) - ?? null; + } ); } [Route( HttpVerbs.Post, "/mods" )] diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index ef9e8e26..ec27c168 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -7,14 +7,13 @@ using Dalamud.Logging; using Lumina.Data; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.Mods; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { - public int ApiVersion { get; } = 3; + public int ApiVersion { get; } = 4; private Penumbra? _penumbra; private Lumina.GameData? _lumina; diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs index 4f55c69c..f00f1871 100644 --- a/Penumbra/Api/RedrawController.cs +++ b/Penumbra/Api/RedrawController.cs @@ -4,32 +4,31 @@ using EmbedIO.Routing; using EmbedIO.WebApi; using Penumbra.GameData.Enums; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class RedrawController : WebApiController { - public class RedrawController : WebApiController + private readonly Penumbra _penumbra; + + public RedrawController( Penumbra penumbra ) + => _penumbra = penumbra; + + [Route( HttpVerbs.Post, "/redraw" )] + public async Task Redraw() { - private readonly Penumbra _penumbra; + var data = await HttpContext.GetRequestDataAsync< RedrawData >(); + _penumbra.Api.RedrawObject( data.Name, data.Type ); + } - public RedrawController( Penumbra penumbra ) - => _penumbra = penumbra; + [Route( HttpVerbs.Post, "/redrawAll" )] + public void RedrawAll() + { + _penumbra.Api.RedrawAll( RedrawType.Redraw ); + } - [Route( HttpVerbs.Post, "/redraw" )] - public async Task Redraw() - { - RedrawData data = await HttpContext.GetRequestDataAsync(); - _penumbra.Api.RedrawObject( data.Name, data.Type ); - } - - [Route( HttpVerbs.Post, "/redrawAll" )] - public void RedrawAll() - { - _penumbra.Api.RedrawAll(RedrawType.WithoutSettings); - } - - public class RedrawData - { - public string Name { get; set; } = string.Empty; - public RedrawType Type { get; set; } = RedrawType.WithSettings; - } + public class RedrawData + { + public string Name { get; set; } = string.Empty; + public RedrawType Type { get; set; } = RedrawType.Redraw; } } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index b28e54ab..ab91e8dc 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -1,35 +1,29 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mods; namespace Penumbra.Interop; public unsafe class ObjectReloader : IDisposable { - private delegate void ManipulateDraw( IntPtr actor ); - - private const uint NpcObjectId = unchecked( ( uint )-536870912 ); public const int GPosePlayerIdx = 201; - public const int GPoseEndIdx = GPosePlayerIdx + 48; + public const int GPoseSlots = 42; + public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; - private readonly ModManager _mods; - private readonly Queue< (uint actorId, string name, RedrawType s) > _actorIds = new(); + private readonly List< int > _unloadedObjects = new(Dalamud.Objects.Length); + private readonly List< int > _afterGPoseObjects = new(GPoseSlots); + private int _target = -1; + private int _waitFrame = 0; - private int _currentFrame; - private bool _changedSettings; - private uint _currentObjectId = uint.MaxValue; - private DrawState _currentObjectStartState = 0; - private RedrawType _currentRedrawType = RedrawType.Unload; - private string? _currentObjectName; - private bool _wasTarget; - private bool _inGPose; + public ObjectReloader() + => Dalamud.Framework.Update += OnUpdateEvent; + + public void Dispose() + => Dalamud.Framework.Update -= OnUpdateEvent; public static DrawState* ActorDrawState( GameObject actor ) => ( DrawState* )( actor.Address + 0x0104 ); @@ -40,236 +34,122 @@ public unsafe class ObjectReloader : IDisposable private static void EnableDraw( GameObject actor ) => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); - public ObjectReloader( ModManager mods ) - => _mods = mods; + private static int ObjectTableIndex( GameObject actor ) + => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; - private void ChangeSettings() + private static void WriteInvisible( GameObject? actor ) { - if( _currentObjectName != null && _mods.Collections.CharacterCollection.TryGetValue( _currentObjectName, out var collection ) ) + if( actor == null ) { - _changedSettings = true; + return; } - } - private void RestoreSettings() - { - _changedSettings = false; - } - - private void WriteInvisible( GameObject actor, int actorIdx ) - { - _currentObjectStartState = *ActorDrawState( actor ); *ActorDrawState( actor ) |= DrawState.Invisibility; - if( _inGPose ) + if( ObjectTableIndex( actor ) is >= GPosePlayerIdx and < GPoseEndIdx ) { DisableDraw( actor ); } } - private bool StillLoading( DrawState* renderPtr ) + private static void WriteVisible( GameObject? actor ) { - const DrawState stillLoadingFlags = DrawState.SomeNpcFlag - | DrawState.MaybeCulled - | DrawState.MaybeHiddenMinion - | DrawState.MaybeHiddenSummon; - - if( renderPtr != null ) + if( actor == null ) { - var loadingFlags = *( DrawState* )renderPtr; - if( loadingFlags == _currentObjectStartState ) - { - return false; - } - - return !( loadingFlags == 0 || ( loadingFlags & stillLoadingFlags ) != 0 ); + return; } - return false; - } - - private void WriteVisible( GameObject actor, int actorIdx ) - { *ActorDrawState( actor ) &= ~DrawState.Invisibility; - if( _inGPose ) + if( ObjectTableIndex( actor ) is >= GPosePlayerIdx and < GPoseEndIdx ) { EnableDraw( actor ); } } - private bool CheckObject( GameObject actor ) + private void ReloadActor( GameObject? actor ) { - if( _currentObjectId != actor.ObjectId ) - { - return false; - } - - if( _currentObjectId != NpcObjectId ) - { - return true; - } - - return _currentObjectName == actor.Name.ToString(); - } - - private bool CheckObjectGPose( GameObject actor ) - => actor.ObjectId == NpcObjectId && _currentObjectName == actor.Name.ToString(); - - private (GameObject?, int) FindCurrentObject() - { - if( _inGPose ) - { - for( var i = GPosePlayerIdx; i < GPoseEndIdx; ++i ) - { - var actor = Dalamud.Objects[ i ]; - if( actor == null ) - { - break; - } - - if( CheckObjectGPose( actor ) ) - { - return ( actor, i ); - } - } - } - - for( var i = 0; i < Dalamud.Objects.Length; ++i ) - { - if( i == GPosePlayerIdx ) - { - i = GPoseEndIdx; - } - - var actor = Dalamud.Objects[ i ]; - if( actor != null && CheckObject( actor ) ) - { - return ( actor, i ); - } - } - - return ( null, -1 ); - } - - private void PopObject() - { - if( _actorIds.Count > 0 ) - { - var (id, name, s) = _actorIds.Dequeue(); - _currentObjectName = name; - _currentObjectId = id; - _currentRedrawType = s; - var (actor, _) = FindCurrentObject(); - if( actor == null ) - { - return; - } - - _wasTarget = actor.Address == Dalamud.Targets.Target?.Address; - - ++_currentFrame; - } - else - { - Dalamud.Framework.Update -= OnUpdateEvent; - } - } - - private void ApplySettingsOrRedraw() - { - var (actor, idx) = FindCurrentObject(); - if( actor == null ) - { - _currentFrame = 0; - return; - } - - switch( _currentRedrawType ) - { - case RedrawType.Unload: - WriteInvisible( actor, idx ); - _currentFrame = 0; - break; - case RedrawType.RedrawWithSettings: - ChangeSettings(); - ++_currentFrame; - break; - case RedrawType.RedrawWithoutSettings: - WriteVisible( actor, idx ); - _currentFrame = 0; - break; - case RedrawType.WithoutSettings: - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.WithSettings: - ChangeSettings(); - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.OnlyWithSettings: - ChangeSettings(); - if( !_changedSettings ) - { - return; - } - - WriteInvisible( actor, idx ); - ++_currentFrame; - break; - case RedrawType.AfterGPoseWithSettings: - case RedrawType.AfterGPoseWithoutSettings: - if( _inGPose ) - { - _actorIds.Enqueue( ( _currentObjectId, _currentObjectName!, _currentRedrawType ) ); - _currentFrame = 0; - } - else - { - _currentRedrawType = _currentRedrawType == RedrawType.AfterGPoseWithSettings - ? RedrawType.WithSettings - : RedrawType.WithoutSettings; - } - - break; - default: throw new InvalidEnumArgumentException(); - } - } - - private void StartRedrawAndWait() - { - var (actor, idx) = FindCurrentObject(); - if( actor == null ) - { - RevertSettings(); - return; - } - - WriteVisible( actor, idx ); - _currentFrame = _changedSettings || _wasTarget ? _currentFrame + 1 : 0; - } - - private void RevertSettings() - { - var (actor, _) = FindCurrentObject(); if( actor != null ) { - if( !StillLoading( ActorDrawState( actor ) ) ) + WriteInvisible( actor ); + var idx = ObjectTableIndex( actor ); + if( actor.Address == Dalamud.Targets.Target?.Address ) { - RestoreSettings(); - if( _wasTarget && Dalamud.Targets.Target == null ) - { - Dalamud.Targets.SetTarget( actor ); - } - - _currentFrame = 0; + _target = idx; } + + _unloadedObjects.Add( idx ); + _waitFrame = 1; } - else + } + + private void ReloadActorAfterGPose( GameObject? actor ) + { + if( Dalamud.Objects[ GPosePlayerIdx ] != null ) { - _currentFrame = 0; + ReloadActor( actor ); + return; } + + if( actor != null ) + { + WriteInvisible( actor ); + _afterGPoseObjects.Add( ObjectTableIndex( actor ) ); + _waitFrame = 1; + } + } + + private void HandleTarget() + { + if( _target < 0 ) + { + return; + } + + var actor = Dalamud.Objects[ _target ]; + if( actor == null || Dalamud.Targets.Target != null ) + { + _target = -1; + return; + } + + if( ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->DrawObject == null ) + { + return; + } + + Dalamud.Targets.SetTarget( actor ); + _target = -1; + } + + private void HandleRedraw() + { + if( _unloadedObjects.Count == 0 ) + { + return; + } + + foreach( var idx in _unloadedObjects ) + { + WriteVisible( Dalamud.Objects[ idx ] ); + } + + _unloadedObjects.Clear(); + } + + private void HandleAfterGPose() + { + if( _afterGPoseObjects.Count == 0 || Dalamud.Objects[ GPosePlayerIdx ] != null ) + { + return; + } + + foreach( var idx in _afterGPoseObjects ) + { + WriteVisible( Dalamud.Objects[ idx ] ); + } + + _afterGPoseObjects.Clear(); } private void OnUpdateEvent( object framework ) @@ -281,47 +161,34 @@ public unsafe class ObjectReloader : IDisposable return; } - _inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null; - - switch( _currentFrame ) - { - case 0: - PopObject(); - break; - case 1: - ApplySettingsOrRedraw(); - break; - case 2: - StartRedrawAndWait(); - break; - case 3: - RevertSettings(); - break; - default: - _currentFrame = 0; - break; - } - } - - private void RedrawObjectIntern( uint objectId, string actorName, RedrawType settings ) - { - if( _actorIds.Contains( ( objectId, actorName, settings ) ) ) + if( _waitFrame > 0 ) { + --_waitFrame; return; } - _actorIds.Enqueue( ( objectId, actorName, settings ) ); - if( _actorIds.Count == 1 ) - { - Dalamud.Framework.Update += OnUpdateEvent; - } + HandleRedraw(); + HandleAfterGPose(); + HandleTarget(); } - public void RedrawObject( GameObject? actor, RedrawType settings = RedrawType.WithSettings ) + public void RedrawObject( GameObject? actor, RedrawType settings ) { - if( actor != null ) + switch( settings ) { - RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), RedrawType.WithoutSettings ); // TODO settings ); + case RedrawType.Redraw: + ReloadActor( actor ); + break; + case RedrawType.Unload: + WriteInvisible( actor ); + break; + case RedrawType.Load: + WriteVisible( actor ); + break; + case RedrawType.AfterGPose: + ReloadActorAfterGPose( actor ); + break; + default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); } } @@ -331,47 +198,45 @@ public unsafe class ObjectReloader : IDisposable return gPosePlayer ?? Dalamud.Objects[ 0 ]; } - private GameObject? GetName( string name ) + private bool GetName( string lowerName, out GameObject? actor ) { - var lowerName = name.ToLowerInvariant(); - return lowerName switch + ( actor, var ret ) = lowerName switch { - "" => null, - "" => GetLocalPlayer(), - "self" => GetLocalPlayer(), - "" => Dalamud.Targets.Target, - "target" => Dalamud.Targets.Target, - "" => Dalamud.Targets.FocusTarget, - "focus" => Dalamud.Targets.FocusTarget, - "" => Dalamud.Targets.MouseOverTarget, - "mouseover" => Dalamud.Targets.MouseOverTarget, - _ => Dalamud.Objects.FirstOrDefault( - a => string.Equals( a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase ) ), + "" => ( null, true ), + "" => ( GetLocalPlayer(), true ), + "self" => ( GetLocalPlayer(), true ), + "" => ( Dalamud.Targets.Target, true ), + "target" => ( Dalamud.Targets.Target, true ), + "" => ( Dalamud.Targets.FocusTarget, true ), + "focus" => ( Dalamud.Targets.FocusTarget, true ), + "" => ( Dalamud.Targets.MouseOverTarget, true ), + "mouseover" => ( Dalamud.Targets.MouseOverTarget, true ), + _ => ( null, false ), }; + return ret; } - public void RedrawObject( string name, RedrawType settings = RedrawType.WithSettings ) - => RedrawObject( GetName( name ), settings ); - - public void RedrawAll( RedrawType settings = RedrawType.WithSettings ) + public void RedrawObject( string name, RedrawType settings ) + { + var lowerName = name.ToLowerInvariant(); + if( GetName( lowerName, out var target ) ) + { + RedrawObject( target, settings ); + } + else + { + foreach( var actor in Dalamud.Objects.Where( a => a.Name.ToString().ToLowerInvariant() == lowerName ) ) + { + RedrawObject( actor, settings ); + } + } + } + + public void RedrawAll( RedrawType settings ) { - Clear(); foreach( var actor in Dalamud.Objects ) { RedrawObject( actor, settings ); } } - - public void Clear() - { - RestoreSettings(); - _currentFrame = 0; - } - - public void Dispose() - { - RevertSettings(); - _actorIds.Clear(); - Dalamud.Framework.Update -= OnUpdateEvent; - } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 862a9a79..d82f556c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -66,7 +66,7 @@ public class Penumbra : IDalamudPlugin ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new ModManager(); ModManager.DiscoverMods(); - ObjectReloader = new ObjectReloader( ModManager ); + ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) @@ -121,7 +121,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); Config.Save(); - ObjectReloader.RedrawAll( RedrawType.WithSettings ); + ObjectReloader.RedrawAll( RedrawType.Redraw ); return true; } @@ -137,7 +137,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); Config.Save(); - ObjectReloader.RedrawAll( RedrawType.WithoutSettings ); + ObjectReloader.RedrawAll( RedrawType.Redraw ); return true; } @@ -272,11 +272,11 @@ public class Penumbra : IDalamudPlugin { if( args.Length > 1 ) { - ObjectReloader.RedrawObject( args[ 1 ] ); + ObjectReloader.RedrawObject( args[ 1 ], RedrawType.Redraw ); } else { - ObjectReloader.RedrawAll(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); } break; diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 4aba0ee8..2fe9b996 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -63,107 +63,6 @@ public partial class SettingsInterface PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } - - private unsafe void DrawDebugTabRedraw() - { - if( !ImGui.CollapsingHeader( "Redrawing##Debug" ) ) - { - return; - } - - var queue = ( Queue< (int, string, RedrawType) >? )_penumbra.ObjectReloader.GetType() - .GetField( "_objectIds", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ) - ?? new Queue< (int, string, RedrawType) >(); - - var currentFrame = ( int? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var changedSettings = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_changedSettings", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectId = ( uint? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectId", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectName = ( string? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectName", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentObjectStartState = ( DrawState? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentObjectStartState", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var currentRedrawType = ( RedrawType? )_penumbra.ObjectReloader.GetType() - .GetField( "_currentRedrawType", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var (currentObject, currentObjectIdx) = ( (GameObject?, int) )_penumbra.ObjectReloader.GetType() - .GetMethod( "FindCurrentObject", BindingFlags.NonPublic | BindingFlags.Instance )? - .Invoke( _penumbra.ObjectReloader, Array.Empty< object >() )!; - - var currentRender = currentObject != null - ? ObjectReloader.ActorDrawState( currentObject ) - : null; - - var waitFrames = ( int? )_penumbra.ObjectReloader.GetType() - .GetField( "_waitFrames", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var wasTarget = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_wasTarget", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - var gPose = ( bool? )_penumbra.ObjectReloader.GetType() - .GetField( "_inGPose", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( _penumbra.ObjectReloader ); - - using var raii = new ImGuiRaii.EndStack(); - if( ImGui.BeginTable( "##RedrawData", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 7 ) ) ) - { - raii.Push( ImGui.EndTable ); - PrintValue( "Current Wait Frame", waitFrames?.ToString() ?? "null" ); - PrintValue( "Current Frame", currentFrame?.ToString() ?? "null" ); - PrintValue( "Currently in GPose", gPose?.ToString() ?? "null" ); - PrintValue( "Current Changed Settings", changedSettings?.ToString() ?? "null" ); - PrintValue( "Current Object Id", currentObjectId?.ToString( "X8" ) ?? "null" ); - PrintValue( "Current Object Name", currentObjectName ?? "null" ); - PrintValue( "Current Object Start State", ( ( int? )currentObjectStartState )?.ToString( "X8" ) ?? "null" ); - PrintValue( "Current Object Was Target", wasTarget?.ToString() ?? "null" ); - PrintValue( "Current Object Redraw", currentRedrawType?.ToString() ?? "null" ); - PrintValue( "Current Object Address", currentObject?.Address.ToString( "X16" ) ?? "null" ); - PrintValue( "Current Object Index", currentObjectIdx >= 0 ? currentObjectIdx.ToString() : "null" ); - PrintValue( "Current Object Render Flags", ( ( int? )currentRender )?.ToString( "X8" ) ?? "null" ); - } - - if( queue.Any() - && ImGui.BeginTable( "##RedrawTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollX, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * queue.Count ) ) ) - { - raii.Push( ImGui.EndTable ); - foreach( var (objectId, objectName, redraw) in queue ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( objectName ); - ImGui.TableNextColumn(); - ImGui.Text( $"0x{objectId:X8}" ); - ImGui.TableNextColumn(); - ImGui.Text( redraw.ToString() ); - } - } - - if( queue.Any() && ImGui.Button( "Clear" ) ) - { - queue.Clear(); - _penumbra.ObjectReloader.GetType() - .GetField( "_currentFrame", BindingFlags.Instance | BindingFlags.NonPublic )?.SetValue( _penumbra.ObjectReloader, 0 ); - } - } - private void DrawDebugTabIpc() { if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) @@ -436,8 +335,6 @@ public partial class SettingsInterface ImGui.NewLine(); DrawDebugResidentResources(); ImGui.NewLine(); - DrawDebugTabRedraw(); - ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } From efc21c7882ccdafa0414b64f1e6602b0ce7ad623 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Mar 2022 16:11:35 +0100 Subject: [PATCH 0111/2451] Some more Redraw changes. --- Penumbra.GameData/Enums/RedrawType.cs | 2 - Penumbra/Interop/ObjectReloader.cs | 74 ++++++++++++++------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Penumbra.GameData/Enums/RedrawType.cs b/Penumbra.GameData/Enums/RedrawType.cs index b4d11d64..4b698377 100644 --- a/Penumbra.GameData/Enums/RedrawType.cs +++ b/Penumbra.GameData/Enums/RedrawType.cs @@ -3,7 +3,5 @@ namespace Penumbra.GameData.Enums; public enum RedrawType { Redraw, - Unload, - Load, AfterGPose, } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index ab91e8dc..81599fc6 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -8,16 +8,15 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop; -public unsafe class ObjectReloader : IDisposable +public sealed unsafe class ObjectReloader : IDisposable { - public const int GPosePlayerIdx = 201; - public const int GPoseSlots = 42; - public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; + public const int GPosePlayerIdx = 201; + public const int GPoseSlots = 42; + public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; - private readonly List< int > _unloadedObjects = new(Dalamud.Objects.Length); - private readonly List< int > _afterGPoseObjects = new(GPoseSlots); - private int _target = -1; - private int _waitFrame = 0; + private readonly List< int > _queue = new(100); + private readonly List< int > _afterGPoseQueue = new(GPoseSlots); + private int _target = -1; public ObjectReloader() => Dalamud.Framework.Update += OnUpdateEvent; @@ -71,15 +70,13 @@ public unsafe class ObjectReloader : IDisposable { if( actor != null ) { - WriteInvisible( actor ); var idx = ObjectTableIndex( actor ); if( actor.Address == Dalamud.Targets.Target?.Address ) { _target = idx; } - _unloadedObjects.Add( idx ); - _waitFrame = 1; + _queue.Add( ~idx ); } } @@ -94,8 +91,7 @@ public unsafe class ObjectReloader : IDisposable if( actor != null ) { WriteInvisible( actor ); - _afterGPoseObjects.Add( ObjectTableIndex( actor ) ); - _waitFrame = 1; + _afterGPoseQueue.Add( ~ObjectTableIndex( actor ) ); } } @@ -124,32 +120,52 @@ public unsafe class ObjectReloader : IDisposable private void HandleRedraw() { - if( _unloadedObjects.Count == 0 ) + if( _queue.Count == 0 ) { return; } - foreach( var idx in _unloadedObjects ) + var numKept = 0; + foreach( var idx in _queue ) { - WriteVisible( Dalamud.Objects[ idx ] ); + if( idx < 0 ) + { + var newIdx = ~idx; + WriteInvisible( Dalamud.Objects[ newIdx ] ); + _queue[ numKept++ ] = newIdx; + } + else + { + WriteVisible( Dalamud.Objects[ idx ] ); + } } - _unloadedObjects.Clear(); + _queue.RemoveRange( numKept, _queue.Count - numKept ); } private void HandleAfterGPose() { - if( _afterGPoseObjects.Count == 0 || Dalamud.Objects[ GPosePlayerIdx ] != null ) + if( _afterGPoseQueue.Count == 0 || Dalamud.Objects[ GPosePlayerIdx ] != null ) { return; } - foreach( var idx in _afterGPoseObjects ) + var numKept = 0; + foreach( var idx in _afterGPoseQueue ) { - WriteVisible( Dalamud.Objects[ idx ] ); + if( idx < 0 ) + { + var newIdx = ~idx; + WriteInvisible( Dalamud.Objects[ newIdx ] ); + _afterGPoseQueue[ numKept++ ] = newIdx; + } + else + { + WriteVisible( Dalamud.Objects[ idx ] ); + } } - _afterGPoseObjects.Clear(); + _afterGPoseQueue.RemoveRange( numKept, _queue.Count - numKept ); } private void OnUpdateEvent( object framework ) @@ -161,12 +177,6 @@ public unsafe class ObjectReloader : IDisposable return; } - if( _waitFrame > 0 ) - { - --_waitFrame; - return; - } - HandleRedraw(); HandleAfterGPose(); HandleTarget(); @@ -179,12 +189,6 @@ public unsafe class ObjectReloader : IDisposable case RedrawType.Redraw: ReloadActor( actor ); break; - case RedrawType.Unload: - WriteInvisible( actor ); - break; - case RedrawType.Load: - WriteVisible( actor ); - break; case RedrawType.AfterGPose: ReloadActorAfterGPose( actor ); break; @@ -192,13 +196,13 @@ public unsafe class ObjectReloader : IDisposable } } - private GameObject? GetLocalPlayer() + private static GameObject? GetLocalPlayer() { var gPosePlayer = Dalamud.Objects[ GPosePlayerIdx ]; return gPosePlayer ?? Dalamud.Objects[ 0 ]; } - private bool GetName( string lowerName, out GameObject? actor ) + private static bool GetName( string lowerName, out GameObject? actor ) { ( actor, var ret ) = lowerName switch { From 2b0844a21e5c85a985dd93e676d88d831715762f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Mar 2022 23:28:07 +0100 Subject: [PATCH 0112/2451] Fix EST file resource reallocation. --- Penumbra/Meta/Files/EstFile.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 8880b9e0..aacfbf00 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -51,13 +51,7 @@ public sealed unsafe class EstFile : MetaBaseFile { if( Length < Size + EntryDescSize + EntrySize ) { - var data = Data; - var length = Length; - AllocateData( length + IncreaseSize ); - Functions.MemCpyUnchecked( Data, data, length ); - Functions.MemSet( Data + length, 0, IncreaseSize ); - GC.RemoveMemoryPressure( length ); - Marshal.FreeHGlobal( ( IntPtr )data ); + ResizeResources( Length + IncreaseSize ); } var control = ( Info* )( Data + 4 ); From 2cece9c422fe2711d7186df9fdce167ea889037c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Mar 2022 23:28:32 +0100 Subject: [PATCH 0113/2451] Fix EQP Entries not deserializing correctly due to C# json bug. --- .../Meta/Manipulations/EqpManipulation.cs | 8 +- .../Util/FixedUlongStringEnumConverter.cs | 92 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Util/FixedUlongStringEnumConverter.cs diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 430dbb9f..b87d0d7a 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -6,14 +6,18 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.Util; namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > { - public readonly EqpEntry Entry; - public readonly ushort SetId; + [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] + public readonly EqpEntry Entry; + + public readonly ushort SetId; + [JsonConverter( typeof( StringEnumConverter ) )] public readonly EquipSlot Slot; diff --git a/Penumbra/Util/FixedUlongStringEnumConverter.cs b/Penumbra/Util/FixedUlongStringEnumConverter.cs new file mode 100644 index 00000000..1ab68e4e --- /dev/null +++ b/Penumbra/Util/FixedUlongStringEnumConverter.cs @@ -0,0 +1,92 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Penumbra.Util; + +// Json.Net has a bug +// ulong enums can not be correctly deserialized if they exceed long.MaxValue. +// These converters fix this, taken from https://stackoverflow.com/questions/61740964/json-net-unable-to-deserialize-ulong-flag-type-enum/ +public class ForceNumericFlagEnumConverter : FixedUlongStringEnumConverter +{ + private static bool HasFlagsAttribute( Type? objectType ) + => objectType != null && Attribute.IsDefined( Nullable.GetUnderlyingType( objectType ) ?? objectType, typeof( System.FlagsAttribute ) ); + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + var enumType = value?.GetType(); + if( HasFlagsAttribute( enumType ) ) + { + var underlyingType = Enum.GetUnderlyingType( enumType! ); + var underlyingValue = Convert.ChangeType( value, underlyingType ); + writer.WriteValue( underlyingValue ); + } + else + { + base.WriteJson( writer, value, serializer ); + } + } +} + +public class FixedUlongStringEnumConverter : StringEnumConverter +{ + public override object? ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + if( reader.MoveToContentAndAssert().TokenType != JsonToken.Integer || reader.ValueType != typeof( System.Numerics.BigInteger ) ) + { + return base.ReadJson( reader, objectType, existingValue, serializer ); + } + + // Todo: throw an exception if !this.AllowIntegerValues + // https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Converters_StringEnumConverter_AllowIntegerValues.htm + var enumType = Nullable.GetUnderlyingType( objectType ) ?? objectType; + if( Enum.GetUnderlyingType( enumType ) == typeof( ulong ) ) + { + var bigInteger = ( System.Numerics.BigInteger )reader.Value!; + if( bigInteger >= ulong.MinValue && bigInteger <= ulong.MaxValue ) + { + return Enum.ToObject( enumType, checked( ( ulong )bigInteger ) ); + } + } + + return base.ReadJson( reader, objectType, existingValue, serializer ); + } +} + +public static partial class JsonExtensions +{ + public static JsonReader MoveToContentAndAssert( this JsonReader reader ) + { + if( reader == null ) + { + throw new ArgumentNullException(); + } + + if( reader.TokenType == JsonToken.None ) // Skip past beginning of stream. + { + reader.ReadAndAssert(); + } + + while( reader.TokenType == JsonToken.Comment ) // Skip past comments. + { + reader.ReadAndAssert(); + } + + return reader; + } + + public static JsonReader ReadAndAssert( this JsonReader reader ) + { + if( reader == null ) + { + throw new ArgumentNullException(); + } + + if( !reader.Read() ) + { + throw new JsonReaderException( "Unexpected end of JSON stream." ); + } + + return reader; + } +} \ No newline at end of file From 4a4d93baf374251abb5e0ba0047bd25874ed5502 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Mar 2022 16:11:59 +0100 Subject: [PATCH 0114/2451] Change moddata to list. --- Penumbra/Mod/ModCleanup.cs | 6 +- Penumbra/Mods/ModManager.cs | 119 +++++++++------------- Penumbra/Mods/ModManagerEditExtensions.cs | 3 - Penumbra/Util/ArrayExtensions.cs | 61 ++++++++--- 4 files changed, 96 insertions(+), 93 deletions(-) diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 0851ef77..03633c33 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -7,10 +7,8 @@ using System.Linq; using System.Security.Cryptography; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Importer; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Mod; @@ -62,8 +60,8 @@ public class ModCleanup private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { - Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; + var idx = Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[idx]; newMod.Move( newSortOrder ); newMod.ComputeChangedItems(); ModFileSystem.InvokeChange(); diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index e391fa3e..c414260c 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -6,20 +6,36 @@ using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta; using Penumbra.Mod; +using Penumbra.Util; namespace Penumbra.Mods; +public enum ModChangeType +{ + Added, + Removed, + Changed, +} + +public delegate void ModChangeDelegate( ModChangeType type, int modIndex, ModData modData ); + // The ModManager handles the basic mods installed to the mod directory. // It also contains the CollectionManager that handles all collections. public class ModManager { public DirectoryInfo BasePath { get; private set; } = null!; - public Dictionary< string, ModData > Mods { get; } = new(); + private List< ModData > ModsInternal { get; init; } = new(); + + public IReadOnlyList< ModData > Mods + => ModsInternal; + public ModFolder StructuredMods { get; } = ModFileSystem.Root; public CollectionManager Collections { get; } + public event ModChangeDelegate? ModChange; + public bool Valid { get; private set; } public Configuration Config @@ -38,7 +54,7 @@ public class ModManager return; } - if( !newPath.Any() ) + if( newPath.Length == 0 ) { Valid = false; BasePath = new DirectoryInfo( "." ); @@ -84,7 +100,7 @@ public class ModManager { mod.Move( path ); var fixedPath = mod.SortOrder.FullPath; - if( !fixedPath.Any() || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) + if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) { Config.ModSortOrder.Remove( mod.BasePath.Name ); return true; @@ -103,16 +119,16 @@ public class ModManager { var changes = false; - foreach( var kvp in Config.ModSortOrder.ToArray() ) + foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) { - if( kvp.Value.Any() && Mods.TryGetValue( kvp.Key, out var mod ) ) + if( path.Length > 0 && ModsInternal.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) { - changes |= SetSortOrderPath( mod, kvp.Value ); + changes |= SetSortOrderPath( mod, path ); } else if( removeOldPaths ) { changes = true; - Config.ModSortOrder.Remove( kvp.Key ); + Config.ModSortOrder.Remove( folder ); } } @@ -124,7 +140,7 @@ public class ModManager public void DiscoverMods() { - Mods.Clear(); + ModsInternal.Clear(); BasePath.Refresh(); StructuredMods.SubFolders.Clear(); @@ -139,7 +155,7 @@ public class ModManager continue; } - Mods.Add( modFolder.Name, mod ); + ModsInternal.Add( mod ); } SetModStructure(); @@ -151,8 +167,7 @@ public class ModManager public void DeleteMod( DirectoryInfo modFolder ) { - modFolder.Refresh(); - if( modFolder.Exists ) + if( Directory.Exists( modFolder.FullName ) ) { try { @@ -162,22 +177,25 @@ public class ModManager { PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); } + } - if( Mods.TryGetValue( modFolder.Name, out var mod ) ) - { - mod.SortOrder.ParentFolder.RemoveMod( mod ); - Mods.Remove( modFolder.Name ); - Collections.RemoveModFromCaches( modFolder ); - } + var idx = ModsInternal.FindIndex( m => m.BasePath.Name == modFolder.Name ); + if( idx >= 0 ) + { + var mod = ModsInternal[ idx ]; + mod.SortOrder.ParentFolder.RemoveMod( mod ); + ModsInternal.RemoveAt( idx ); + Collections.RemoveModFromCaches( modFolder ); + ModChange?.Invoke( ModChangeType.Removed, idx, mod ); } } - public bool AddMod( DirectoryInfo modFolder ) + public int AddMod( DirectoryInfo modFolder ) { var mod = ModData.LoadMod( StructuredMods, modFolder ); if( mod == null ) { - return false; + return -1; } if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) @@ -188,22 +206,24 @@ public class ModManager } } - if( Mods.ContainsKey( modFolder.Name ) ) + if( ModsInternal.Any( m => m.BasePath.Name == modFolder.Name ) ) { - return false; + return -1; } - Mods.Add( modFolder.Name, mod ); + ModsInternal.Add( mod ); + ModChange?.Invoke( ModChangeType.Added, ModsInternal.Count - 1, mod ); foreach( var collection in Collections.Collections.Values ) { collection.AddMod( mod ); } - return true; + return ModsInternal.Count - 1; } - public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) { + var mod = Mods[ idx ]; var oldName = mod.Meta.Name; var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); @@ -242,60 +262,17 @@ public class ModManager } Collections.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); - + ModChange?.Invoke( ModChangeType.Changed, idx, mod ); return true; } + public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) { var ret = Collections.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); return ret; } - - // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) - // { - // #if DEBUG - // PluginLog.Verbose( "file changed: {FullPath}", e.FullPath ); - // #endif - // - // if( _plugin.ImportInProgress ) - // { - // return; - // } - // - // if( _plugin.Configuration.DisableFileSystemNotifications ) - // { - // return; - // } - // - // var file = e.FullPath; - // - // if( !ResolvedFiles.Any( x => x.Value.FullName == file ) ) - // { - // return; - // } - // - // PluginLog.Log( "a loaded file has been modified - file: {FullPath}", file ); - // _plugin.GameUtils.ReloadPlayerResources(); - // } - // - // private void FileSystemPasta() - // { - // haha spaghet - // _fileSystemWatcher?.Dispose(); - // _fileSystemWatcher = new FileSystemWatcher( _basePath.FullName ) - // { - // NotifyFilter = NotifyFilters.LastWrite | - // NotifyFilters.FileName | - // NotifyFilters.DirectoryName, - // IncludeSubdirectories = true, - // EnableRaisingEvents = true - // }; - // - // _fileSystemWatcher.Changed += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Created += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Deleted += FileSystemWatcherOnChanged; - // _fileSystemWatcher.Renamed += FileSystemWatcherOnChanged; - // } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 17e600c4..e143b10a 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -70,9 +70,6 @@ public static class ModManagerEditExtensions } } - manager.Mods.Remove( mod.BasePath.Name ); - manager.Mods[ newDir.Name ] = mod; - var oldBasePath = mod.BasePath; mod.BasePath = newDir; mod.MetaFile = ModData.MetaFileInfo( newDir ); diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index 14b07748..acae0203 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -1,28 +1,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Penumbra.Util; public static class ArrayExtensions { - public static int IndexOf< T >( this T[] array, Predicate< T > match ) - { - for( var i = 0; i < array.Length; ++i ) - { - if( match( array[ i ] ) ) - { - return i; - } - } - - return -1; - } - - public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate ) + public static int IndexOf< T >( this IReadOnlyList< T > array, Predicate< T > predicate ) { for( var i = 0; i < array.Count; ++i ) { - if( predicate.Invoke( array[ i ] ) ) + if( predicate( array[ i ] ) ) { return i; } @@ -30,4 +18,47 @@ public static class ArrayExtensions return -1; } + + public static int IndexOf< T >( this IReadOnlyList< T > array, T needle ) + { + for( var i = 0; i < array.Count; ++i ) + { + if( needle!.Equals( array[i] ) ) + { + return i; + } + } + + return -1; + } + + public static bool FindFirst< T >( this IReadOnlyList< T > array, Predicate< T > predicate, [NotNullWhen( true )] out T? result ) + { + foreach( var obj in array ) + { + if( predicate( obj ) ) + { + result = obj!; + return true; + } + } + + result = default; + return false; + } + + public static bool FindFirst< T >( this IReadOnlyList< T > array, T needle, [NotNullWhen( true )] out T? result ) where T : IEquatable< T > + { + foreach( var obj in array ) + { + if( obj.Equals( needle ) ) + { + result = obj!; + return true; + } + } + + result = default; + return false; + } } \ No newline at end of file From 0eff4e2e6724ebe17c0eff65f21161c1ffa512e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Mar 2022 22:58:00 +0100 Subject: [PATCH 0115/2451] tmp --- Penumbra/Mods/CollectionManager.cs | 35 +++++++++++++++++++++++++++++- Penumbra/Mods/ModManager.cs | 21 +++++------------- Penumbra/Penumbra.cs | 7 ++++-- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 0bb7182a..9763bfa4 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -21,7 +21,7 @@ public delegate void CollectionChangeDelegate( ModCollection? oldCollection, Mod string? characterName = null ); // Contains all collections and respective functions, as well as the collection settings. -public class CollectionManager +public class CollectionManager : IDisposable { private readonly ModManager _manager; @@ -42,10 +42,43 @@ public class CollectionManager { _manager = manager; + _manager.ModsRediscovered += OnModsRediscovered; ReadCollections(); LoadConfigCollections( Penumbra.Config ); } + public void Dispose() + { + _manager.ModsRediscovered -= OnModsRediscovered; + } + + private void OnModsRediscovered() + { + RecreateCaches(); + DefaultCollection.SetFiles(); + } + + private void OnModChanged( ModChangeType type, int idx, ModData mod ) + { + switch( type ) + { + case ModChangeType.Added: + foreach( var collection in Collections.Values ) + { + collection.AddMod( mod ); + } + + break; + case ModChangeType.Removed: + RemoveModFromCaches( mod.BasePath ); + break; + case ModChangeType.Changed: + // TODO + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + public void CreateNecessaryCaches() { AddCache( DefaultCollection ); diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index c414260c..d3eadd77 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -32,9 +32,8 @@ public class ModManager public ModFolder StructuredMods { get; } = ModFileSystem.Root; - public CollectionManager Collections { get; } - public event ModChangeDelegate? ModChange; + public event Action? ModsRediscovered; public bool Valid { get; private set; } @@ -82,18 +81,14 @@ public class ModManager Config.ModDirectory = BasePath.FullName; Config.Save(); } - - if( !firstTime ) - { - Collections.RecreateCaches(); - } } + + ModsRediscovered?.Invoke(); } public ModManager() { SetBaseDirectory( Config.ModDirectory, true ); - Collections = new CollectionManager( this ); } private bool SetSortOrderPath( ModData mod, string path ) @@ -161,8 +156,7 @@ public class ModManager SetModStructure(); } - Collections.RecreateCaches(); - Collections.DefaultCollection.SetFiles(); + ModsRediscovered?.Invoke(); } public void DeleteMod( DirectoryInfo modFolder ) @@ -185,7 +179,6 @@ public class ModManager var mod = ModsInternal[ idx ]; mod.SortOrder.ParentFolder.RemoveMod( mod ); ModsInternal.RemoveAt( idx ); - Collections.RemoveModFromCaches( modFolder ); ModChange?.Invoke( ModChangeType.Removed, idx, mod ); } } @@ -213,10 +206,6 @@ public class ModManager ModsInternal.Add( mod ); ModChange?.Invoke( ModChangeType.Added, ModsInternal.Count - 1, mod ); - foreach( var collection in Collections.Collections.Values ) - { - collection.AddMod( mod ); - } return ModsInternal.Count - 1; } @@ -261,7 +250,7 @@ public class ModManager mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); } - Collections.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); + Penumbra.CollectionManager.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); // TODO ModChange?.Invoke( ModChangeType.Changed, idx, mod ); return true; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d82f556c..40c12864 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,6 +34,7 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } @@ -66,8 +67,9 @@ public class Penumbra : IDalamudPlugin ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new ModManager(); ModManager.DiscoverMods(); - ObjectReloader = new ObjectReloader(); - PathResolver = new PathResolver( ResourceLoader ); + CollectionManager = new CollectionManager( ModManager ); + ObjectReloader = new ObjectReloader(); + PathResolver = new PathResolver( ResourceLoader ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -193,6 +195,7 @@ public class Penumbra : IDalamudPlugin Api.Dispose(); SettingsInterface.Dispose(); ObjectReloader.Dispose(); + CollectionManager.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); From 9c0fc8a8c7577e29dc3bd9be7a28ef7bcd5a1b61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 23 Mar 2022 11:45:38 +0100 Subject: [PATCH 0116/2451] Move CollectionManager out of ModManager --- Penumbra/Api/ModsController.cs | 4 +- Penumbra/Api/PenumbraApi.cs | 10 ++-- .../Interop/Resolver/PathResolver.Data.cs | 10 ++-- .../Interop/Resolver/PathResolver.Meta.cs | 16 +++--- .../Interop/Resolver/PathResolver.Resolve.cs | 6 +- Penumbra/Interop/Resolver/PathResolver.cs | 6 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- Penumbra/Mod/ModCleanup.cs | 2 +- Penumbra/Mods/ModManager.cs | 4 +- Penumbra/Mods/ModManagerEditExtensions.cs | 8 +-- Penumbra/Penumbra.cs | 10 ++-- Penumbra/UI/MenuTabs/TabChangedItems.cs | 4 +- Penumbra/UI/MenuTabs/TabCollections.cs | 57 +++++++++---------- Penumbra/UI/MenuTabs/TabDebug.cs | 21 ++++--- Penumbra/UI/MenuTabs/TabEffective.cs | 4 +- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 8 +-- .../TabInstalled/TabInstalledDetails.cs | 7 +-- .../TabInstalled/TabInstalledSelector.cs | 10 ++-- Penumbra/UI/SettingsInterface.cs | 7 +-- 19 files changed, 94 insertions(+), 102 deletions(-) diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 511ff0ea..3232931a 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,7 +16,7 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - return Penumbra.ModManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new + return Penumbra.CollectionManager.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new { x.Settings.Enabled, x.Settings.Priority, @@ -34,7 +34,7 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/files" )] public object GetFiles() { - return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + return Penumbra.CollectionManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( o => o.Key.ToString(), o => o.Value.FullName ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index ec27c168..ef649144 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -76,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, ModManager manager, ModCollection collection ) + private static string ResolvePath( string path, ModManager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { @@ -85,21 +85,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + ret ??= Penumbra.CollectionManager.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); return ret?.ToString() ?? path; } public string ResolvePath( string path ) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.ModManager.Collections.DefaultCollection ); + return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.DefaultCollection ); } public string ResolvePath( string path, string characterName ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, - Penumbra.ModManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection ) + Penumbra.CollectionManager.CharacterCollection.TryGetValue( characterName, out var collection ) ? collection : ModCollection.Empty ); } @@ -134,7 +134,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if( !Penumbra.ModManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) + if( !Penumbra.CollectionManager.Collections.TryGetValue( collectionName, out var collection ) ) { collection = ModCollection.Empty; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 229d6380..d5804d23 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -68,12 +68,12 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Enable(); EnableDrawHook?.Enable(); CharacterBaseDestructorHook?.Enable(); - Penumbra.ModManager.Collections.CollectionChanged += CheckCollections; + Penumbra.CollectionManager.CollectionChanged += CheckCollections; } private void DisableDataHooks() { - Penumbra.ModManager.Collections.CollectionChanged -= CheckCollections; + Penumbra.CollectionManager.CollectionChanged -= CheckCollections; CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); @@ -161,7 +161,7 @@ public unsafe partial class PathResolver { if( gameObject == null ) { - return Penumbra.ModManager.Collections.DefaultCollection; + return Penumbra.CollectionManager.DefaultCollection; } var name = gameObject->ObjectIndex switch @@ -174,9 +174,9 @@ public unsafe partial class PathResolver } ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.ModManager.Collections.CharacterCollection.TryGetValue( name, out var col ) + return Penumbra.CollectionManager.CharacterCollection.TryGetValue( name, out var col ) ? col - : Penumbra.ModManager.Collections.DefaultCollection; + : Penumbra.CollectionManager.DefaultCollection; } // Update collections linked to Game/DrawObjects due to a change in collection configuration. diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 243ee3fd..ee556c25 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -163,12 +163,12 @@ public unsafe partial class PathResolver private ModCollection? GetCollection( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection == Penumbra.ModManager.Collections.DefaultCollection ) + if( parent == null || collection == Penumbra.CollectionManager.DefaultCollection ) { return null; } - return collection.Cache == null ? Penumbra.ModManager.Collections.ForcedCollection : collection; + return collection.Cache == null ? Penumbra.CollectionManager.ForcedCollection : collection; } @@ -274,7 +274,7 @@ public unsafe partial class PathResolver { collection = IdentifyCollection( resolver.LastGameObject ); #if USE_CMP - if( collection != Penumbra.ModManager.Collections.DefaultCollection && collection.Cache != null ) + if( collection != Penumbra.CollectionManager.DefaultCollection && collection.Cache != null ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); @@ -309,25 +309,25 @@ public unsafe partial class PathResolver case MetaManipulation.Type.Eqdp: if( --_eqdpCounter == 0 ) { - Penumbra.ModManager.Collections.DefaultCollection.SetEqdpFiles(); + Penumbra.CollectionManager.DefaultCollection.SetEqdpFiles(); } break; case MetaManipulation.Type.Eqp: if( --_eqpCounter == 0 ) { - Penumbra.ModManager.Collections.DefaultCollection.SetEqpFiles(); + Penumbra.CollectionManager.DefaultCollection.SetEqpFiles(); } break; case MetaManipulation.Type.Est: - Penumbra.ModManager.Collections.DefaultCollection.SetEstFiles(); + Penumbra.CollectionManager.DefaultCollection.SetEstFiles(); break; case MetaManipulation.Type.Gmp: - Penumbra.ModManager.Collections.DefaultCollection.SetGmpFiles(); + Penumbra.CollectionManager.DefaultCollection.SetGmpFiles(); break; case MetaManipulation.Type.Rsp: - Penumbra.ModManager.Collections.DefaultCollection.SetCmpFiles(); + Penumbra.CollectionManager.DefaultCollection.SetCmpFiles(); break; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index ce3ec0dd..f79f25b6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -104,7 +104,7 @@ public unsafe partial class PathResolver [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) => ResolvePathDetour( FindParent( drawObject, out var collection ) == null - ? Penumbra.ModManager.Collections.DefaultCollection + ? Penumbra.CollectionManager.DefaultCollection : collection, path ); // Weapons have the characters DrawObject as a parent, @@ -123,7 +123,7 @@ public unsafe partial class PathResolver { var parent = FindParent( ( IntPtr )parentObject, out var collection ); return ResolvePathDetour( parent == null - ? Penumbra.ModManager.Collections.DefaultCollection + ? Penumbra.CollectionManager.DefaultCollection : collection, path ); } } @@ -138,7 +138,7 @@ public unsafe partial class PathResolver } var gamePath = new Utf8String( ( byte* )path ); - if( collection == Penumbra.ModManager.Collections.DefaultCollection ) + if( collection == Penumbra.CollectionManager.DefaultCollection ) { SetCollection( gamePath, null ); return path; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index cef6c550..ae58a64e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -36,7 +36,7 @@ public partial class PathResolver : IDisposable var nonDefault = HandleMaterialSubFiles( gamePath, out var collection ) || PathCollections.TryGetValue( gamePath.Path, out collection ); if( !nonDefault ) { - collection = Penumbra.ModManager.Collections.DefaultCollection; + collection = Penumbra.CollectionManager.DefaultCollection; } else { @@ -49,7 +49,7 @@ public partial class PathResolver : IDisposable var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); if( resolved == null ) { - resolved = Penumbra.ModManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); + resolved = Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); if( resolved == null ) { // We also need to handle defaulted materials against a non-default collection. @@ -61,7 +61,7 @@ public partial class PathResolver : IDisposable return ( null, collection ); } - collection = Penumbra.ModManager.Collections.ForcedCollection; + collection = Penumbra.CollectionManager.ForcedCollection; } // Since mtrl files load their files separately, we need to add the new, resolved path diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index c8c54a0e..991094f4 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -135,7 +135,7 @@ public partial class MetaManager fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) + if( Penumbra.CollectionManager.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) && collection.Cache != null && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 03633c33..8e202351 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -521,7 +521,7 @@ public class ModCleanup } } - foreach( var collection in Penumbra.ModManager.Collections.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections.Values ) { collection.UpdateSetting( baseDir, meta, true ); } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index d3eadd77..ba58e522 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -260,8 +260,8 @@ public class ModManager public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) { - var ret = Collections.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + var ret = Penumbra.CollectionManager.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); + ret ??= Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); return ret; } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index e143b10a..59714c16 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -82,7 +82,7 @@ public static class ModManagerEditExtensions manager.Config.Save(); } - foreach( var collection in manager.Collections.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections.Values ) { if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) { @@ -140,7 +140,7 @@ public static class ModManagerEditExtensions mod.SaveMeta(); - foreach( var collection in manager.Collections.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections.Values ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -176,7 +176,7 @@ public static class ModManagerEditExtensions return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); } - foreach( var collection in manager.Collections.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections.Values ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -202,7 +202,7 @@ public static class ModManagerEditExtensions if( collection.Cache != null && settings.Enabled ) { collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - manager.Collections.IsActive( collection ) ); + Penumbra.CollectionManager.IsActive( collection ) ); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 40c12864..8dc512f8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -214,7 +214,7 @@ public class Penumbra : IDalamudPlugin var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) ? ModCollection.Empty - : ModManager.Collections.Collections.Values.FirstOrDefault( c + : CollectionManager.Collections.Values.FirstOrDefault( c => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); if( collection == null ) { @@ -225,24 +225,24 @@ public class Penumbra : IDalamudPlugin switch( type ) { case "default": - if( collection == ModManager.Collections.DefaultCollection ) + if( collection == CollectionManager.DefaultCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); return false; } - ModManager.Collections.SetCollection( collection, CollectionType.Default ); + CollectionManager.SetCollection( collection, CollectionType.Default ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; case "forced": - if( collection == ModManager.Collections.ForcedCollection ) + if( collection == CollectionManager.ForcedCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); return false; } - ModManager.Collections.SetCollection( collection, CollectionType.Forced ); + CollectionManager.SetCollection( collection, CollectionType.Forced ); Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); SettingsInterface.ResetForcedCollection(); return true; diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index 87d0b58c..bcd6ff0a 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -26,8 +26,8 @@ public partial class SettingsInterface } var modManager = Penumbra.ModManager; - var items = modManager.Collections.DefaultCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); - var forced = modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var items = Penumbra.CollectionManager.DefaultCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = Penumbra.CollectionManager.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 2b4fa4d8..18bf6b82 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -31,7 +31,7 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = Penumbra.ModManager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.CollectionManager.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); @@ -51,18 +51,18 @@ public partial class SettingsInterface } private void UpdateIndex() - => _currentCollectionIndex = GetIndex( Penumbra.ModManager.Collections.CurrentCollection ) - 1; + => _currentCollectionIndex = GetIndex( Penumbra.CollectionManager.CurrentCollection ) - 1; public void UpdateForcedIndex() - => _currentForcedIndex = GetIndex( Penumbra.ModManager.Collections.ForcedCollection ); + => _currentForcedIndex = GetIndex( Penumbra.CollectionManager.ForcedCollection ); public void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( Penumbra.ModManager.Collections.DefaultCollection ); + => _currentDefaultIndex = GetIndex( Penumbra.CollectionManager.DefaultCollection ); private void UpdateCharacterIndices() { _currentCharacterIndices.Clear(); - foreach( var kvp in Penumbra.ModManager.Collections.CharacterCollection ) + foreach( var kvp in Penumbra.CollectionManager.CharacterCollection ) { _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); } @@ -84,11 +84,10 @@ public partial class SettingsInterface private void CreateNewCollection( Dictionary< string, ModSettings > settings ) { - var manager = Penumbra.ModManager; - if( manager.Collections.AddCollection( _newCollectionName, settings ) ) + if( Penumbra.CollectionManager.AddCollection( _newCollectionName, settings ) ) { UpdateNames(); - SetCurrentCollection( manager.Collections.Collections[ _newCollectionName ], true ); + SetCurrentCollection( Penumbra.CollectionManager.Collections[ _newCollectionName ], true ); } _newCollectionName = string.Empty; @@ -98,10 +97,9 @@ public partial class SettingsInterface { if( ImGui.Button( "Clean Settings" ) ) { - var manager = Penumbra.ModManager; - var changes = ModFunctions.CleanUpCollection( manager.Collections.CurrentCollection.Settings, - manager.BasePath.EnumerateDirectories() ); - manager.Collections.CurrentCollection.UpdateSettings( changes ); + var changes = ModFunctions.CleanUpCollection( Penumbra.CollectionManager.CurrentCollection.Settings, + Penumbra.ModManager.BasePath.EnumerateDirectories() ); + Penumbra.CollectionManager.CurrentCollection.UpdateSettings( changes ); } ImGuiCustom.HoverTooltip( @@ -126,10 +124,9 @@ public partial class SettingsInterface var hover = ImGui.IsItemHovered(); ImGui.SameLine(); - var manager = Penumbra.ModManager; if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( manager.Collections.CurrentCollection.Settings ); + CreateNewCollection( Penumbra.CollectionManager.CurrentCollection.Settings ); } hover |= ImGui.IsItemHovered(); @@ -140,13 +137,13 @@ public partial class SettingsInterface ImGui.SetTooltip( "Please enter a name before creating a collection." ); } - var deleteCondition = manager.Collections.Collections.Count > 1 - && manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; + var deleteCondition = Penumbra.CollectionManager.Collections.Count > 1 + && Penumbra.CollectionManager.CurrentCollection.Name != ModCollection.DefaultCollection; ImGui.SameLine(); if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) { - manager.Collections.RemoveCollection( manager.Collections.CurrentCollection.Name ); - SetCurrentCollection( manager.Collections.CurrentCollection, true ); + Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.CurrentCollection.Name ); + SetCurrentCollection( Penumbra.CollectionManager.CurrentCollection, true ); UpdateNames(); } @@ -169,7 +166,7 @@ public partial class SettingsInterface return; } - Penumbra.ModManager.Collections.SetCollection( _collections[ idx + 1 ], CollectionType.Current ); + Penumbra.CollectionManager.SetCollection( _collections[ idx + 1 ], CollectionType.Current ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -208,7 +205,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) { - Penumbra.ModManager.Collections.SetCollection( _collections[ index ], CollectionType.Default ); + Penumbra.CollectionManager.SetCollection( _collections[ index ], CollectionType.Default ); _currentDefaultIndex = index; } @@ -225,18 +222,17 @@ public partial class SettingsInterface { var index = _currentForcedIndex; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - var manager = Penumbra.ModManager; - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, manager.Collections.CharacterCollection.Count == 0 ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, Penumbra.CollectionManager.CharacterCollection.Count == 0 ); if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) - && index != _currentForcedIndex - && manager.Collections.CharacterCollection.Count > 0 ) + && index != _currentForcedIndex + && Penumbra.CollectionManager.CharacterCollection.Count > 0 ) { - manager.Collections.SetCollection( _collections[ index ], CollectionType.Forced ); + Penumbra.CollectionManager.SetCollection( _collections[ index ], CollectionType.Forced ); _currentForcedIndex = index; } style.Pop(); - if( manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) + if( Penumbra.CollectionManager.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) { ImGui.SetTooltip( "Forced Collections only provide value if you have at least one Character Collection. There is no need to set one until then." ); @@ -262,7 +258,7 @@ public partial class SettingsInterface if( ImGuiCustom.DisableButton( "Create New Character Collection", _newCharacterName.Length > 0 && Penumbra.Config.HasReadCharacterCollectionDesc ) ) { - Penumbra.ModManager.Collections.CreateCharacterCollection( _newCharacterName ); + Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); _currentCharacterIndices[ _newCharacterName ] = 0; _newCharacterName = string.Empty; } @@ -344,15 +340,14 @@ public partial class SettingsInterface DrawDefaultCollectionSelector(); DrawForcedCollectionSelector(); - var manager = Penumbra.ModManager; - foreach( var name in manager.Collections.CharacterCollection.Keys.ToArray() ) + foreach( var name in Penumbra.CollectionManager.CharacterCollection.Keys.ToArray() ) { var idx = _currentCharacterIndices[ name ]; var tmp = idx; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) { - manager.Collections.SetCollection( _collections[ tmp ], CollectionType.Character, name ); + Penumbra.CollectionManager.SetCollection( _collections[ tmp ], CollectionType.Character, name ); _currentCharacterIndices[ name ] = tmp; } @@ -362,7 +357,7 @@ public partial class SettingsInterface if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}", Vector2.One * ImGui.GetFrameHeight() ) ) { - manager.Collections.RemoveCharacterCollection( name ); + Penumbra.CollectionManager.RemoveCharacterCollection( name ); } font.Pop(); diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 2fe9b996..83ed25ee 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -49,17 +49,16 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); var manager = Penumbra.ModManager; - PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.CurrentCollection.Cache != null ).ToString() ); - PrintValue( "Default Collection", manager.Collections.DefaultCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.DefaultCollection.Cache != null ).ToString() ); - PrintValue( "Forced Collection", manager.Collections.ForcedCollection.Name ); - PrintValue( " has Cache", ( manager.Collections.ForcedCollection.Cache != null ).ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath?.Name ?? "NULL" ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath?.FullName ?? "NULL" ); + PrintValue( "Current Collection", Penumbra.CollectionManager.CurrentCollection.Name ); + PrintValue( " has Cache", ( Penumbra.CollectionManager.CurrentCollection.Cache != null ).ToString() ); + PrintValue( "Default Collection", Penumbra.CollectionManager.DefaultCollection.Name ); + PrintValue( " has Cache", ( Penumbra.CollectionManager.DefaultCollection.Cache != null ).ToString() ); + PrintValue( "Forced Collection", Penumbra.CollectionManager.ForcedCollection.Name ); + PrintValue( " has Cache", ( Penumbra.CollectionManager.ForcedCollection.Cache != null ).ToString() ); + PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); + PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", - manager.BasePath != null ? Directory.Exists( manager.BasePath.FullName ).ToString() : false.ToString() ); + PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } @@ -122,7 +121,7 @@ public partial class SettingsInterface return; } - var cache = Penumbra.ModManager.Collections.CurrentCollection.Cache; + var cache = Penumbra.CollectionManager.CurrentCollection.Cache; if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) { return; diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 101ac00e..672e5a8c 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -141,8 +141,8 @@ public partial class SettingsInterface const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; var modManager = Penumbra.ModManager; - var defaultCollection = modManager.Collections.DefaultCollection.Cache; - var forcedCollection = modManager.Collections.ForcedCollection.Cache; + var defaultCollection = Penumbra.CollectionManager.DefaultCollection.Cache; + var forcedCollection = Penumbra.CollectionManager.ForcedCollection.Cache; var (defaultResolved, defaultMeta) = defaultCollection != null ? ( defaultCollection.ResolvedFiles.Count, defaultCollection.MetaManipulations.Count ) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index 6d4adbd5..a87a8b22 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -120,13 +120,13 @@ namespace Penumbra.UI var lower = filter.ToLowerInvariant(); if( lower.StartsWith( "c:" ) ) { - _modFilterChanges = lower.Substring( 2 ); + _modFilterChanges = lower[ 2.. ]; _modFilter = string.Empty; _modFilterAuthor = string.Empty; } else if( lower.StartsWith( "a:" ) ) { - _modFilterAuthor = lower.Substring( 2 ); + _modFilterAuthor = lower[ 2.. ]; _modFilter = string.Empty; _modFilterChanges = string.Empty; } @@ -147,11 +147,11 @@ namespace Penumbra.UI _visibleFolders.Clear(); PluginLog.Debug( "Resetting mod selector list..." ); - if( !_modsInOrder.Any() ) + if( _modsInOrder.Count == 0 ) { foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) { - var mod = _manager.Collections.CurrentCollection.GetMod( modData ); + var mod = Penumbra.CollectionManager.CurrentCollection.GetMod( modData ); _modsInOrder.Add( mod ); _visibleMods.Add( CheckFilters( mod ) ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 192126b1..1e0a92d1 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -127,7 +127,7 @@ public partial class SettingsInterface private void Save() { - Penumbra.ModManager.Collections.CurrentCollection.Save(); + Penumbra.CollectionManager.CurrentCollection.Save(); } private void DrawAboutTab() @@ -422,11 +422,10 @@ public partial class SettingsInterface _fullFilenameList = null; _selector.SaveCurrentMod(); // Since files may have changed, we need to recompute effective files. - var modManager = Penumbra.ModManager; - foreach( var collection in modManager.Collections.Collections.Values + foreach( var collection in Penumbra.CollectionManager.Collections.Values .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) { - collection.CalculateEffectiveFileList( false, modManager.Collections.IsActive( collection ) ); + collection.CalculateEffectiveFileList( false, Penumbra.CollectionManager.IsActive( collection ) ); } // If the mod is enabled in the current collection, its conflicts may have changed. diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 943bc84a..3374a522 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -526,10 +526,10 @@ public partial class SettingsInterface } Cache.TriggerFilterReset(); - var collection = Penumbra.ModManager.Collections.CurrentCollection; + var collection = Penumbra.CollectionManager.CurrentCollection; if( collection.Cache != null ) { - collection.CalculateEffectiveFileList( metaManips, Penumbra.ModManager.Collections.IsActive( collection ) ); + collection.CalculateEffectiveFileList( metaManips, Penumbra.CollectionManager.IsActive( collection ) ); } collection.Save(); @@ -609,7 +609,7 @@ public partial class SettingsInterface private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) { if( collection == ModCollection.Empty - || collection == Penumbra.ModManager.Collections.CurrentCollection ) + || collection == Penumbra.CollectionManager.CurrentCollection ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); @@ -638,10 +638,10 @@ public partial class SettingsInterface - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2, 5f ); ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, Penumbra.ModManager.Collections.DefaultCollection ); + DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.DefaultCollection ); ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.ModManager.Collections.ForcedCollection ); + DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.CollectionManager.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize ); diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 915bf84d..8939be48 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -57,18 +57,17 @@ public partial class SettingsInterface : IDisposable private void SaveCurrentCollection( bool recalculateMeta ) { - var current = Penumbra.ModManager.Collections.CurrentCollection; + var current = Penumbra.CollectionManager.CurrentCollection; current.Save(); RecalculateCurrent( recalculateMeta ); } private void RecalculateCurrent( bool recalculateMeta ) { - var modManager = Penumbra.ModManager; - var current = modManager.Collections.CurrentCollection; + var current = Penumbra.CollectionManager.CurrentCollection; if( current.Cache != null ) { - current.CalculateEffectiveFileList( recalculateMeta, modManager.Collections.IsActive( current ) ); + current.CalculateEffectiveFileList( recalculateMeta, Penumbra.CollectionManager.IsActive( current ) ); _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } } From 519543772c3bbcb380b9850d61ca0c20ff4f56db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 23 Mar 2022 13:03:55 +0100 Subject: [PATCH 0117/2451] Turn Collections to List instead of Dict. --- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- Penumbra/Mod/ModCleanup.cs | 2 +- Penumbra/Mod/ModMeta.cs | 1 - Penumbra/Mods/CollectionManager.cs | 59 ++++++++++++------- Penumbra/Mods/ModManagerEditExtensions.cs | 6 +- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/MenuTabs/TabCollections.cs | 6 +- .../TabInstalled/TabInstalledDetails.cs | 6 +- 9 files changed, 50 insertions(+), 36 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index ef649144..540e1432 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -134,7 +134,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if( !Penumbra.CollectionManager.Collections.TryGetValue( collectionName, out var collection ) ) + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { collection = ModCollection.Empty; } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 991094f4..abe86fe5 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -135,7 +135,7 @@ public partial class MetaManager fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.CollectionManager.Collections.TryGetValue( split[ 0 ].ToString(), out var collection ) + if( Penumbra.CollectionManager.ByName( split[ 0 ].ToString(), out var collection ) && collection.Cache != null && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 8e202351..38c316e2 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -521,7 +521,7 @@ public class ModCleanup } } - foreach( var collection in Penumbra.CollectionManager.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections ) { collection.UpdateSetting( baseDir, meta, true ); } diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mod/ModMeta.cs index 5a4776eb..d4c18b3b 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mod/ModMeta.cs @@ -5,7 +5,6 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; namespace Penumbra.Mod; diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 9763bfa4..7f6e5468 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; @@ -25,7 +26,7 @@ public class CollectionManager : IDisposable { private readonly ModManager _manager; - public Dictionary< string, ModCollection > Collections { get; } = new(StringComparer.InvariantCultureIgnoreCase); + public List< ModCollection > Collections { get; } = new(); public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; @@ -35,6 +36,15 @@ public class CollectionManager : IDisposable public bool IsActive( ModCollection collection ) => ReferenceEquals( collection, DefaultCollection ) || ReferenceEquals( collection, ForcedCollection ); + public ModCollection Default + => ByName( ModCollection.DefaultCollection )!; + + public ModCollection? ByName( string name ) + => Collections.Find( c => c.Name == name ); + + public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) + => Collections.FindFirst( c => c.Name == name, out collection ); + // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; @@ -43,6 +53,7 @@ public class CollectionManager : IDisposable _manager = manager; _manager.ModsRediscovered += OnModsRediscovered; + _manager.ModChange += OnModChanged; ReadCollections(); LoadConfigCollections( Penumbra.Config ); } @@ -50,6 +61,7 @@ public class CollectionManager : IDisposable public void Dispose() { _manager.ModsRediscovered -= OnModsRediscovered; + _manager.ModChange -= OnModChanged; } private void OnModsRediscovered() @@ -63,7 +75,7 @@ public class CollectionManager : IDisposable switch( type ) { case ModChangeType.Added: - foreach( var collection in Collections.Values ) + foreach( var collection in Collections ) { collection.AddMod( mod ); } @@ -75,7 +87,7 @@ public class CollectionManager : IDisposable case ModChangeType.Changed: // TODO break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } } @@ -91,7 +103,7 @@ public class CollectionManager : IDisposable public void RecreateCaches() { - foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) + foreach( var collection in Collections.Where( c => c.Cache != null ) ) { collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); } @@ -101,7 +113,7 @@ public class CollectionManager : IDisposable public void RemoveModFromCaches( DirectoryInfo modDir ) { - foreach( var collection in Collections.Values ) + foreach( var collection in Collections ) { collection.Cache?.RemoveMod( modDir ); } @@ -109,7 +121,7 @@ public class CollectionManager : IDisposable internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta ) { - foreach( var collection in Collections.Values ) + foreach( var collection in Collections ) { if( metaChanges ) { @@ -138,16 +150,16 @@ public class CollectionManager : IDisposable public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) { var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 || Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + if( nameFixed.Length == 0 || Collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) { PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); return false; } var newCollection = new ModCollection( name, settings ); - Collections.Add( name, newCollection ); - CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); + Collections.Add( newCollection ); newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); SetCollection( newCollection, CollectionType.Current ); return true; } @@ -160,15 +172,17 @@ public class CollectionManager : IDisposable return false; } - if( !Collections.TryGetValue( name, out var collection ) ) + var idx = Collections.IndexOf( c => c.Name == name ); + if( idx < 0 ) { return false; } - CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); + var collection = Collections[ idx ]; + if( CurrentCollection == collection ) { - SetCollection( Collections[ ModCollection.DefaultCollection ], CollectionType.Current ); + SetCollection( Default, CollectionType.Current ); } if( ForcedCollection == collection ) @@ -190,7 +204,8 @@ public class CollectionManager : IDisposable } collection.Delete(); - Collections.Remove( name ); + Collections.RemoveAt( idx ); + CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); return true; } @@ -294,7 +309,7 @@ public class CollectionManager : IDisposable private bool LoadCurrentCollection( Configuration config ) { - if( Collections.TryGetValue( config.CurrentCollection, out var currentCollection ) ) + if( ByName( config.CurrentCollection, out var currentCollection ) ) { CurrentCollection = currentCollection; AddCache( CurrentCollection ); @@ -302,7 +317,7 @@ public class CollectionManager : IDisposable } PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); - CurrentCollection = Collections[ ModCollection.DefaultCollection ]; + CurrentCollection = Default; if( CurrentCollection.Cache == null ) { CurrentCollection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); @@ -320,7 +335,7 @@ public class CollectionManager : IDisposable return false; } - if( Collections.TryGetValue( config.ForcedCollection, out var forcedCollection ) ) + if( ByName( config.ForcedCollection, out var forcedCollection ) ) { ForcedCollection = forcedCollection; AddCache( ForcedCollection ); @@ -341,7 +356,7 @@ public class CollectionManager : IDisposable return false; } - if( Collections.TryGetValue( config.DefaultCollection, out var defaultCollection ) ) + if( ByName( config.DefaultCollection, out var defaultCollection ) ) { DefaultCollection = defaultCollection; AddCache( DefaultCollection ); @@ -363,7 +378,7 @@ public class CollectionManager : IDisposable { CharacterCollection.Add( player, ModCollection.Empty ); } - else if( Collections.TryGetValue( collectionName, out var charCollection ) ) + else if( ByName( collectionName, out var charCollection ) ) { AddCache( charCollection ); CharacterCollection.Add( player, charCollection ); @@ -411,22 +426,22 @@ public class CollectionManager : IDisposable PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); } - if( Collections.ContainsKey( collection.Name ) ) + if( ByName( collection.Name ) != null ) { PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); } else { - Collections.Add( collection.Name, collection ); + Collections.Add( collection ); } } } - if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) + if( ByName( ModCollection.DefaultCollection ) == null ) { var defaultCollection = new ModCollection(); defaultCollection.Save(); - Collections.Add( defaultCollection.Name, defaultCollection ); + Collections.Add( defaultCollection ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 59714c16..93c96c86 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -82,7 +82,7 @@ public static class ModManagerEditExtensions manager.Config.Save(); } - foreach( var collection in Penumbra.CollectionManager.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections ) { if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) { @@ -140,7 +140,7 @@ public static class ModManagerEditExtensions mod.SaveMeta(); - foreach( var collection in Penumbra.CollectionManager.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -176,7 +176,7 @@ public static class ModManagerEditExtensions return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); } - foreach( var collection in Penumbra.CollectionManager.Collections.Values ) + foreach( var collection in Penumbra.CollectionManager.Collections ) { if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8dc512f8..bdea9ecd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -214,7 +214,7 @@ public class Penumbra : IDalamudPlugin var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) ? ModCollection.Empty - : CollectionManager.Collections.Values.FirstOrDefault( c + : CollectionManager.Collections.FirstOrDefault( c => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); if( collection == null ) { diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 18bf6b82..4bcb4224 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -31,7 +31,7 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = Penumbra.CollectionManager.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.CollectionManager.Collections.Prepend( ModCollection.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); @@ -87,7 +87,7 @@ public partial class SettingsInterface if( Penumbra.CollectionManager.AddCollection( _newCollectionName, settings ) ) { UpdateNames(); - SetCurrentCollection( Penumbra.CollectionManager.Collections[ _newCollectionName ], true ); + SetCurrentCollection( Penumbra.CollectionManager.ByName( _newCollectionName )!, true ); } _newCollectionName = string.Empty; @@ -222,7 +222,7 @@ public partial class SettingsInterface { var index = _currentForcedIndex; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, Penumbra.CollectionManager.CharacterCollection.Count == 0 ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, Penumbra.CollectionManager.CharacterCollection.Count == 0 ); if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) && index != _currentForcedIndex && Penumbra.CollectionManager.CharacterCollection.Count > 0 ) diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 1e0a92d1..3843fdb6 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -422,14 +422,14 @@ public partial class SettingsInterface _fullFilenameList = null; _selector.SaveCurrentMod(); // Since files may have changed, we need to recompute effective files. - foreach( var collection in Penumbra.CollectionManager.Collections.Values - .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) + foreach( var collection in Penumbra.CollectionManager.Collections + .Where( c => c.Cache != null && c.Settings[ Mod.Data.BasePath.Name ].Enabled ) ) { collection.CalculateEffectiveFileList( false, Penumbra.CollectionManager.IsActive( collection ) ); } // If the mod is enabled in the current collection, its conflicts may have changed. - if( Mod!.Settings.Enabled ) + if( Mod.Settings.Enabled ) { _selector.Cache.TriggerFilterReset(); } From b6ed27e235d5f71c0944cf619ef318481837f3e5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Mar 2022 18:42:51 +0100 Subject: [PATCH 0118/2451] Another ObjectReloader fix. --- Penumbra/Interop/ObjectReloader.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 81599fc6..50556dda 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -126,8 +126,9 @@ public sealed unsafe class ObjectReloader : IDisposable } var numKept = 0; - foreach( var idx in _queue ) + for( var i = 0; i < _queue.Count; ++i ) { + var idx = _queue[ i ]; if( idx < 0 ) { var newIdx = ~idx; @@ -151,8 +152,9 @@ public sealed unsafe class ObjectReloader : IDisposable } var numKept = 0; - foreach( var idx in _afterGPoseQueue ) + for( var i = 0; i < _afterGPoseQueue.Count; ++i ) { + var idx = _afterGPoseQueue[ i ]; if( idx < 0 ) { var newIdx = ~idx; From 1e5776a481d38bce445fd43aedf332036afdc238 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Mar 2022 22:01:39 +0100 Subject: [PATCH 0119/2451] Change resolving to possibly work correctly for all materials and load specific materials for each collection. --- Penumbra.GameData/ByteString/Utf8GamePath.cs | 11 +- Penumbra.GameData/Enums/ResourceType.cs | 110 ++++++++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 8 +- .../Loader/ResourceLoader.Replacement.cs | 163 ++++++++++++------ Penumbra/Interop/Loader/ResourceLoader.cs | 19 +- .../Interop/Resolver/PathResolver.Data.cs | 11 +- .../Interop/Resolver/PathResolver.Material.cs | 64 ++++--- .../Interop/Resolver/PathResolver.Resolve.cs | 6 - Penumbra/Interop/Resolver/PathResolver.cs | 38 ++-- Penumbra/Interop/Structs/ResourceHandle.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 46 +++-- Penumbra/Mods/CollectionManager.cs | 16 +- Penumbra/Mods/ModManager.cs | 39 +++-- Penumbra/Penumbra.cs | 1 + Penumbra/UI/MenuTabs/TabResourceManager.cs | 11 +- Penumbra/Util/ArrayExtensions.cs | 33 +++- 16 files changed, 408 insertions(+), 172 deletions(-) create mode 100644 Penumbra.GameData/Enums/ResourceType.cs diff --git a/Penumbra.GameData/ByteString/Utf8GamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs index afd5d12b..79386002 100644 --- a/Penumbra.GameData/ByteString/Utf8GamePath.cs +++ b/Penumbra.GameData/ByteString/Utf8GamePath.cs @@ -130,10 +130,13 @@ public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< U => Path.Dispose(); public bool IsRooted() - => Path.Length >= 1 && ( Path[ 0 ] == '/' || Path[ 0 ] == '\\' ) - || Path.Length >= 2 - && ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' ) - && Path[ 1 ] == ':'; + => IsRooted( Path ); + + public static bool IsRooted( Utf8String path ) + => path.Length >= 1 && ( path[ 0 ] == '/' || path[ 0 ] == '\\' ) + || path.Length >= 2 + && ( path[ 0 ] >= 'A' && path[ 0 ] <= 'Z' || path[ 0 ] >= 'a' && path[ 0 ] <= 'z' ) + && path[ 1 ] == ':'; public class Utf8GamePathConverter : JsonConverter { diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs new file mode 100644 index 00000000..dedaf3c2 --- /dev/null +++ b/Penumbra.GameData/Enums/ResourceType.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using Penumbra.GameData.ByteString; + +namespace Penumbra.GameData.Enums; + +public enum ResourceType : uint +{ + Aet = 0x00616574, + Amb = 0x00616D62, + Atch = 0x61746368, + Atex = 0x61746578, + Avfx = 0x61766678, + Awt = 0x00617774, + Cmp = 0x00636D70, + Dic = 0x00646963, + Eid = 0x00656964, + Envb = 0x656E7662, + Eqdp = 0x65716470, + Eqp = 0x00657170, + Essb = 0x65737362, + Est = 0x00657374, + Exd = 0x00657864, + Exh = 0x00657868, + Exl = 0x0065786C, + Fdt = 0x00666474, + Gfd = 0x00676664, + Ggd = 0x00676764, + Gmp = 0x00676D70, + Gzd = 0x00677A64, + Imc = 0x00696D63, + Lcb = 0x006C6362, + Lgb = 0x006C6762, + Luab = 0x6C756162, + Lvb = 0x006C7662, + Mdl = 0x006D646C, + Mlt = 0x006D6C74, + Mtrl = 0x6D74726C, + Obsb = 0x6F627362, + Pap = 0x00706170, + Pbd = 0x00706264, + Pcb = 0x00706362, + Phyb = 0x70687962, + Plt = 0x00706C74, + Scd = 0x00736364, + Sgb = 0x00736762, + Shcd = 0x73686364, + Shpk = 0x7368706B, + Sklb = 0x736B6C62, + Skp = 0x00736B70, + Stm = 0x0073746D, + Svb = 0x00737662, + Tera = 0x74657261, + Tex = 0x00746578, + Tmb = 0x00746D62, + Ugd = 0x00756764, + Uld = 0x00756C64, + Waoe = 0x77616F65, + Wtd = 0x00777464, +} + +public static class ResourceTypeExtensions +{ + public static ResourceType FromBytes( byte a1, byte a2, byte a3 ) + => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 16 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 8 ) + | ByteStringFunctions.AsciiToLower( a3 ) ); + + public static ResourceType FromBytes( byte a1, byte a2, byte a3, byte a4 ) + => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 24 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 16 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a3 ) << 8 ) + | ByteStringFunctions.AsciiToLower( a4 ) ); + + public static ResourceType FromBytes( char a1, char a2, char a3 ) + => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3 ); + + public static ResourceType FromBytes( char a1, char a2, char a3, char a4 ) + => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3, ( byte )a4 ); + + public static ResourceType FromString( string path ) + { + var ext = Path.GetExtension( path.AsSpan() ); + ext = ext.Length == 0 ? path.AsSpan() : ext[ 1.. ]; + + return ext.Length switch + { + 0 => 0, + 1 => ( ResourceType )ext[ ^1 ], + 2 => FromBytes( '\0', ext[ ^2 ], ext[ ^1 ] ), + 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + }; + } + + public static ResourceType FromString( Utf8String path ) + { + var extIdx = path.LastIndexOf( ( byte )'.' ); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? Utf8String.Empty : path.Substring( extIdx + 1 ); + + return ext.Length switch + { + 0 => 0, + 1 => ( ResourceType )ext[ ^1 ], + 2 => FromBytes( 0, ext[ ^2 ], ext[ ^1 ] ), + 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index b39a3c57..f43067d0 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,6 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Loader; @@ -25,7 +27,7 @@ public unsafe partial class ResourceLoader public FullPath ManipulatedPath; public ResourceCategory Category; public object? ResolverInfo; - public uint Extension; + public ResourceType Extension; } private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); @@ -112,14 +114,14 @@ public unsafe partial class ResourceLoader // Find a resource in the resource manager by its category, extension and crc-hash - public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 ) + public static ResourceHandle* FindResource( ResourceCategory cat, ResourceType ext, uint crc32 ) { var manager = *ResourceManager; var catIdx = ( uint )cat >> 0x18; cat = ( ResourceCategory )( ushort )cat; var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ], - ext ); + ( uint )ext ); if( extMap == null ) { return null; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index a7b5ddfb..7e9089c7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,10 +1,12 @@ using System; using System.Diagnostics; +using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -16,27 +18,27 @@ public unsafe partial class ResourceLoader // Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. // Both work basically the same, so we can reduce the main work to one function used by both hooks. public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); [Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )] public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); [Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )] public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; - private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, void* unk ) => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false ); - private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - uint* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) + ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) => isSync ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk ) : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); @@ -53,7 +55,8 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) @@ -67,7 +70,7 @@ public unsafe partial class ResourceLoader ResourceRequested?.Invoke( gamePath, isSync ); // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = DoReplacements ? ResolvePath( gamePath.ToLower() ) : ( null, null ); + var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); PathResolved?.Invoke( gamePath, resolvedPath, data ); if( resolvedPath == null ) { @@ -85,6 +88,37 @@ public unsafe partial class ResourceLoader } + // Use the default method of path replacement. + public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) + { + var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); + return ( resolved, null ); + } + + // Try all resolve path subscribers or use the default replacer. + private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + { + if( !DoReplacements ) + { + return ( null, null ); + } + + path = path.ToLower(); + if( ResolvePathCustomization != null ) + { + foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + { + if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) + { + return ret; + } + } + } + + return DefaultResolver( path ); + } + + // We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ); @@ -111,63 +145,82 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); - byte ret; - // The internal buffer size does not allow for more than 260 characters. - // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. - if( !valid || !gamePath.IsRooted() ) + if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) ) { - if( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' ) - { - ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); - } - else - { - ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); - } - } - else - { - // Specify that we are loading unpacked files from the drive. - // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, - // but since we only allow ASCII in the game paths, this is just a matter of upcasting. - fileDescriptor->FileMode = FileMode.LoadUnpackedResource; - - var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; - fileDescriptor->FileDescriptor = fd; - var fdPtr = ( char* )( fd + 0x21 ); - for( var i = 0; i < gamePath.Length; ++i ) - { - ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; - fdPtr[ i ] = ( char )gamePath.Path[ i ]; - } - - ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; - fdPtr[ gamePath.Length ] = '\0'; - - // Use the SE ReadFile function. - ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath.Path, ret != 0, true ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } + byte ret = 0; + // Paths starting with a '|' are handled separately to allow for special treatment. + // They are expected to also have a closing '|'. + if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) + { + return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); + } + + // Split the path into the special-treatment part (between the first and second '|') + // and the actual path. + var split = gamePath.Path.Split( ( byte )'|', 3, false ); + fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; + var funcFound = ResourceLoadCustomization.GetInvocationList() + .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + + if( !funcFound ) + { + ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); + } + + // Return original resource handle path so that they can be loaded separately. + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; return ret; } - // Customize file loading for any GamePaths that start with "|". - public delegate byte ResourceLoadCustomizationDelegate( Utf8GamePath gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ); - - public ResourceLoadCustomizationDelegate? ResourceLoadCustomization; - - - // Use the default method of path replacement. - public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path ) + // Load the resource from an SqPack and trigger the FileLoaded event. + private byte DefaultResourceLoad( Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { - var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); - return ( resolved, null ); + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( path, ret != 0, false ); + return ret; } + // Load the resource from a path on the users hard drives. + private byte DefaultRootedResourceLoad( Utf8String gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + // Specify that we are loading unpacked files from the drive. + // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, + // but since we only allow ASCII in the game paths, this is just a matter of upcasting. + fileDescriptor->FileMode = FileMode.LoadUnpackedResource; + + var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; + fileDescriptor->FileDescriptor = fd; + var fdPtr = ( char* )( fd + 0x21 ); + for( var i = 0; i < gamePath.Length; ++i ) + { + ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; + fdPtr[ i ] = ( char )gamePath.Path[ i ]; + } + + ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; + fdPtr[ gamePath.Length ] = '\0'; + + // Use the SE ReadFile function. + var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath, ret != 0, true ); + return ret; + } + + // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. + internal byte DefaultLoadResource( Utf8String gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync ) + => Utf8GamePath.IsRooted( gamePath ) + ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) + : DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ); + private void DisposeHooks() { DisableHooks(); diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index f02f0895..ad721b5e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -1,6 +1,9 @@ using System; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; namespace Penumbra.Interop.Loader; @@ -104,7 +107,7 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is returned. // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. // resolveData is additional data returned by the current ResolvePath function and is user-defined. - public delegate void ResourceLoadedDelegate( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; @@ -117,7 +120,19 @@ public unsafe partial class ResourceLoader : IDisposable public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. - public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; + // Resolving goes through all subscribed functions in arbitrary order until one returns true, + // or uses default resolving if none return true. + public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash, + out (FullPath?, object?) ret ); + + public event ResolvePathDelegate? ResolvePathCustomization; + + // Customize file loading for any GamePaths that start with "|". + // Same procedure as above. + public delegate bool ResourceLoadCustomizationDelegate( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue ); + + public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization; public void Dispose() { diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index d5804d23..7907b172 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; @@ -92,7 +93,7 @@ public unsafe partial class PathResolver internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); // This map links files to their corresponding collection, if it is non-default. - internal readonly Dictionary< Utf8String, ModCollection > PathCollections = new(); + internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); internal GameObject* LastGameObject = null; @@ -225,13 +226,9 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection? collection ) + private void SetCollection( Utf8String path, ModCollection collection ) { - if( collection == null ) - { - PathCollections.Remove( path ); - } - else if( PathCollections.ContainsKey( path ) || path.IsOwned ) + if( PathCollections.ContainsKey( path ) || path.IsOwned ) { PathCollections[ path ] = collection; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 01f1a2d1..ed2ce84c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,7 +1,9 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Mods; @@ -19,7 +21,7 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlTexHelper( mtrlResourceHandle ); + LoadMtrlHelper( mtrlResourceHandle ); var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; @@ -31,7 +33,7 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlShpkHelper( mtrlResourceHandle ); + LoadMtrlHelper( mtrlResourceHandle ); var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; @@ -39,7 +41,7 @@ public unsafe partial class PathResolver private ModCollection? _mtrlCollection; - private void LoadMtrlShpkHelper( IntPtr mtrlResourceHandle ) + private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) { if( mtrlResourceHandle == IntPtr.Zero ) { @@ -51,27 +53,10 @@ public unsafe partial class PathResolver _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; } - private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) - { - if( mtrlResourceHandle == IntPtr.Zero ) - { - return; - } - - var mtrl = ( MtrlResource* )mtrlResourceHandle; - if( mtrl->NumTex == 0 ) - { - return; - } - - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; - } - // Check specifically for shpk and tex files whether we are currently in a material load. - private bool HandleMaterialSubFiles( Utf8GamePath gamePath, out ModCollection? collection ) + private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection ) { - if( _mtrlCollection != null && ( gamePath.Path.EndsWith( 't', 'e', 'x' ) || gamePath.Path.EndsWith( 's', 'h', 'p', 'k' ) ) ) + if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { collection = _mtrlCollection; return true; @@ -81,16 +66,51 @@ public unsafe partial class PathResolver return false; } + // We need to set the correct collection for the actual material path that is loaded + // before actually loading the file. + private bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) + { + ret = 0; + if( fileDescriptor->ResourceHandle->FileType == ResourceType.Mtrl + && Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) + { + SetCollection( path, collection ); + } + + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); + PathCollections.TryRemove( path, out _ ); + return true; + } + + // Materials need to be set per collection so they can load their textures independently from each other. + private void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, object?) data ) + { + if( nonDefault && type == ResourceType.Mtrl ) + { + var fullPath = new FullPath( $"|{collection.Name}|{path}" ); + SetCollection( fullPath.InternalName, collection ); + data = ( fullPath, collection ); + } + else + { + data = ( resolved, collection ); + } + } + private void EnableMtrlHooks() { LoadMtrlShpkHook?.Enable(); LoadMtrlTexHook?.Enable(); + Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; } private void DisableMtrlHooks() { LoadMtrlShpkHook?.Disable(); LoadMtrlTexHook?.Disable(); + Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler; } private void DisposeMtrlHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index f79f25b6..e6d443ed 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -138,12 +138,6 @@ public unsafe partial class PathResolver } var gamePath = new Utf8String( ( byte* )path ); - if( collection == Penumbra.CollectionManager.DefaultCollection ) - { - SetCollection( gamePath, null ); - return path; - } - SetCollection( gamePath, collection ); return path; } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index ae58a64e..c9543189 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,6 +1,8 @@ using System; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; namespace Penumbra.Interop.Resolver; @@ -14,9 +16,6 @@ public partial class PathResolver : IDisposable { private readonly ResourceLoader _loader; - // Keep track of the last path resolver to be able to restore it. - private Func< Utf8GamePath, (FullPath?, object?) > _oldResolver = null!; - public PathResolver( ResourceLoader loader ) { _loader = loader; @@ -28,22 +27,18 @@ public partial class PathResolver : IDisposable } // The modified resolver that handles game path resolving. - private (FullPath?, object?) CharacterResolver( Utf8GamePath gamePath ) + private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, object?) data ) { // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load. // If not use the default collection. - var nonDefault = HandleMaterialSubFiles( gamePath, out var collection ) || PathCollections.TryGetValue( gamePath.Path, out collection ); + // We can remove paths after they have actually been loaded. + // A potential next request will add the path anew. + var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ); if( !nonDefault ) { collection = Penumbra.CollectionManager.DefaultCollection; } - else - { - // We can remove paths after they have actually been loaded. - // A potential next request will add the path anew. - PathCollections.Remove( gamePath.Path ); - } // Resolve using character/default collection first, otherwise forced, as usual. var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); @@ -53,12 +48,8 @@ public partial class PathResolver : IDisposable if( resolved == null ) { // We also need to handle defaulted materials against a non-default collection. - if( nonDefault && gamePath.Path.EndsWith( 'm', 't', 'r', 'l' ) ) - { - SetCollection( gamePath.Path, collection ); - } - - return ( null, collection ); + HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data ); + return true; } collection = Penumbra.CollectionManager.ForcedCollection; @@ -66,12 +57,8 @@ public partial class PathResolver : IDisposable // 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. - if( nonDefault && resolved.Value.Extension == ".mtrl" ) - { - SetCollection( resolved.Value.InternalName, nonDefault ? collection : null ); - } - - return ( resolved, collection ); + HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data ); + return true; } public void Enable() @@ -84,8 +71,7 @@ public partial class PathResolver : IDisposable EnableDataHooks(); EnableMetaHooks(); - _oldResolver = _loader.ResolvePath; - _loader.ResolvePath = CharacterResolver; + _loader.ResolvePathCustomization += CharacterResolver; } public void Disable() @@ -96,7 +82,7 @@ public partial class PathResolver : IDisposable DisableDataHooks(); DisableMetaHooks(); - _loader.ResolvePath = _oldResolver; + _loader.ResolvePathCustomization -= CharacterResolver; } public void Dispose() diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 447fa361..88f38dca 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Structs; @@ -45,7 +47,7 @@ public unsafe struct ResourceHandle public ResourceCategory Category; [FieldOffset( 0x0C )] - public uint FileType; + public ResourceType FileType; [FieldOffset( 0x10 )] public uint Id; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index abe86fe5..fadd9545 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -4,7 +4,9 @@ using System.Diagnostics; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -19,15 +21,13 @@ public partial class MetaManager public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); - private readonly ModCollection _collection; - private static int _imcManagerCount; - private static ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate; + private readonly ModCollection _collection; + private static int _imcManagerCount; public MetaManagerImc( ModCollection collection ) { - _collection = collection; - _previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization; + _collection = collection; SetupDelegate(); } @@ -105,7 +105,7 @@ public partial class MetaManager { if( _imcManagerCount++ == 0 ) { - Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } } @@ -115,46 +115,42 @@ public partial class MetaManager { if( --_imcManagerCount == 0 ) { - if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; - } - - Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; } } private FullPath CreateImcPath( Utf8GamePath path ) => new($"|{_collection.Name}|{path}"); - private static unsafe byte ImcLoadHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { - var split = gamePath.Path.Split( ( byte )'|', 2, true ); - fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; + ret = 0; + if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc ) + { + return false; + } - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.CollectionManager.ByName( split[ 0 ].ToString(), out var collection ) + ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) && collection.Cache != null && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( - Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) { - PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", gamePath, + PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); file.Replace( fileDescriptor->ResourceHandle ); file.ChangesSinceLoad = false; } - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; - return ret; + return true; } private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) { // Only check imcs. - if( resource->FileType != 0x00696D63 + if( resource->FileType != ResourceType.Imc || resolveData is not ModCollection collection || collection.Cache == null || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 7f6e5468..b99d9657 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -22,7 +22,7 @@ public delegate void CollectionChangeDelegate( ModCollection? oldCollection, Mod string? characterName = null ); // Contains all collections and respective functions, as well as the collection settings. -public class CollectionManager : IDisposable +public sealed class CollectionManager : IDisposable { private readonly ModManager _manager; @@ -40,10 +40,20 @@ public class CollectionManager : IDisposable => ByName( ModCollection.DefaultCollection )!; public ModCollection? ByName( string name ) - => Collections.Find( c => c.Name == name ); + => name.Length > 0 + ? Collections.Find( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ) ) + : ModCollection.Empty; public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => Collections.FindFirst( c => c.Name == name, out collection ); + { + if( name.Length > 0 ) + { + return Collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + } + + collection = ModCollection.Empty; + return true; + } // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index ba58e522..a23a9a32 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -21,14 +22,23 @@ public delegate void ModChangeDelegate( ModChangeType type, int modIndex, ModDat // The ModManager handles the basic mods installed to the mod directory. // It also contains the CollectionManager that handles all collections. -public class ModManager +public class ModManager : IEnumerable< ModData > { public DirectoryInfo BasePath { get; private set; } = null!; - private List< ModData > ModsInternal { get; init; } = new(); + private readonly List< ModData > _mods = new(); + + public ModData this[ int idx ] + => _mods[ idx ]; public IReadOnlyList< ModData > Mods - => ModsInternal; + => _mods; + + public IEnumerator< ModData > GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); public ModFolder StructuredMods { get; } = ModFileSystem.Root; @@ -37,6 +47,9 @@ public class ModManager public bool Valid { get; private set; } + public int Count + => _mods.Count; + public Configuration Config => Penumbra.Config; @@ -116,7 +129,7 @@ public class ModManager foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) { - if( path.Length > 0 && ModsInternal.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) + if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) { changes |= SetSortOrderPath( mod, path ); } @@ -135,7 +148,7 @@ public class ModManager public void DiscoverMods() { - ModsInternal.Clear(); + _mods.Clear(); BasePath.Refresh(); StructuredMods.SubFolders.Clear(); @@ -150,7 +163,7 @@ public class ModManager continue; } - ModsInternal.Add( mod ); + _mods.Add( mod ); } SetModStructure(); @@ -173,12 +186,12 @@ public class ModManager } } - var idx = ModsInternal.FindIndex( m => m.BasePath.Name == modFolder.Name ); + var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); if( idx >= 0 ) { - var mod = ModsInternal[ idx ]; + var mod = _mods[ idx ]; mod.SortOrder.ParentFolder.RemoveMod( mod ); - ModsInternal.RemoveAt( idx ); + _mods.RemoveAt( idx ); ModChange?.Invoke( ModChangeType.Removed, idx, mod ); } } @@ -199,15 +212,15 @@ public class ModManager } } - if( ModsInternal.Any( m => m.BasePath.Name == modFolder.Name ) ) + if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) { return -1; } - ModsInternal.Add( mod ); - ModChange?.Invoke( ModChangeType.Added, ModsInternal.Count - 1, mod ); + _mods.Add( mod ); + ModChange?.Invoke( ModChangeType.Added, _mods.Count - 1, mod ); - return ModsInternal.Count - 1; + return _mods.Count - 1; } public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index bdea9ecd..8517d5c5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -109,6 +109,7 @@ public class Penumbra : IDalamudPlugin { ResourceLoader.EnableFullLogging(); } + ResidentResources.Reload(); } public bool Enable() diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index af8f613f..f8f8d9a4 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -87,10 +87,13 @@ public partial class SettingsInterface if( ImGui.IsItemClicked() ) { - var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); - var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); + if( data != null ) + { + var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); } diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index acae0203..a5a8bb82 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -23,7 +23,7 @@ public static class ArrayExtensions { for( var i = 0; i < array.Count; ++i ) { - if( needle!.Equals( array[i] ) ) + if( needle!.Equals( array[ i ] ) ) { return i; } @@ -61,4 +61,35 @@ public static class ArrayExtensions result = default; return false; } + + public static bool Move< T >( this IList< T > list, int idx1, int idx2 ) + { + idx1 = Math.Clamp( idx1, 0, list.Count - 1 ); + idx2 = Math.Clamp( idx2, 0, list.Count - 1 ); + if( idx1 == idx2 ) + { + return false; + } + + var tmp = list[ idx1 ]; + // move element down and shift other elements up + if( idx1 < idx2 ) + { + for( var i = idx1; i < idx2; i++ ) + { + list[ i ] = list[ i + 1 ]; + } + } + // move element up and shift other elements down + else + { + for( var i = idx1; i > idx2; i-- ) + { + list[ i ] = list[ i - 1 ]; + } + } + + list[ idx2 ] = tmp; + return true; + } } \ No newline at end of file From 9f6729dd0b65c0c5d74be054d63f31d36dc07544 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Mar 2022 22:32:40 +0100 Subject: [PATCH 0120/2451] Only enable PathResolver if any character collections are set, fix mtrl staying in PathCollections. --- .../Interop/Resolver/PathResolver.Material.cs | 9 +++-- Penumbra/Interop/Resolver/PathResolver.cs | 37 ++++++++++++++++++- Penumbra/Penumbra.cs | 4 ++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index ed2ce84c..8569f547 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -72,8 +72,12 @@ public unsafe partial class PathResolver SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; - if( fileDescriptor->ResourceHandle->FileType == ResourceType.Mtrl - && Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) + if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl ) + { + return false; + } + + if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) { SetCollection( path, collection ); } @@ -90,7 +94,6 @@ public unsafe partial class PathResolver if( nonDefault && type == ResourceType.Mtrl ) { var fullPath = new FullPath( $"|{collection.Name}|{path}" ); - SetCollection( fullPath.InternalName, collection ); data = ( fullPath, collection ); } else diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index c9543189..9dc67a17 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Mods; namespace Penumbra.Interop.Resolver; @@ -15,6 +16,7 @@ namespace Penumbra.Interop.Resolver; public partial class PathResolver : IDisposable { private readonly ResourceLoader _loader; + public bool Enabled { get; private set; } public PathResolver( ResourceLoader loader ) { @@ -23,7 +25,7 @@ public partial class PathResolver : IDisposable SetupHumanHooks(); SetupWeaponHooks(); SetupMetaHooks(); - Enable(); + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; } // The modified resolver that handles game path resolving. @@ -63,6 +65,12 @@ public partial class PathResolver : IDisposable public void Enable() { + if( Enabled ) + { + return; + } + + Enabled = true; InitializeDrawObjects(); EnableHumanHooks(); @@ -76,12 +84,21 @@ public partial class PathResolver : IDisposable public void Disable() { + if( !Enabled ) + { + return; + } + + Enabled = false; DisableHumanHooks(); DisableWeaponHooks(); DisableMtrlHooks(); DisableDataHooks(); DisableMetaHooks(); + DrawObjectToObject.Clear(); + PathCollections.Clear(); + _loader.ResolvePathCustomization -= CharacterResolver; } @@ -93,5 +110,23 @@ public partial class PathResolver : IDisposable DisposeMtrlHooks(); DisposeDataHooks(); DisposeMetaHooks(); + Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; + } + + private void OnCollectionChange( ModCollection? _1, ModCollection? _2, CollectionType type, string? characterName ) + { + if( type != CollectionType.Character ) + { + return; + } + + if( Penumbra.CollectionManager.CharacterCollection.Count > 0 ) + { + Enable(); + } + else + { + Disable(); + } } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8517d5c5..e2116f65 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -109,6 +109,10 @@ public class Penumbra : IDalamudPlugin { ResourceLoader.EnableFullLogging(); } + + if (CollectionManager.CharacterCollection.Count > 0) + PathResolver.Enable(); + ResidentResources.Reload(); } From 2877e9f22ff846483cb6fe10da184247906ecd8a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Mar 2022 14:59:21 +0100 Subject: [PATCH 0121/2451] Small debugging changes. --- Penumbra.GameData/ByteString/Utf8RelPath.cs | 1 - .../Interop/Loader/ResourceLoader.Replacement.cs | 4 ++-- Penumbra/Interop/Resolver/PathResolver.Material.cs | 7 +++++++ Penumbra/Meta/Manager/MetaManager.Imc.cs | 1 + Penumbra/UI/MenuTabs/TabDebug.cs | 14 ++------------ 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs index f9321944..da7e9332 100644 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -1,7 +1,6 @@ using System; using System.IO; using Dalamud.Utility; -using Microsoft.VisualBasic.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 7e9089c7..d89a18b7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -150,7 +150,6 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - byte ret = 0; // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) @@ -160,7 +159,8 @@ public unsafe partial class ResourceLoader // Split the path into the special-treatment part (between the first and second '|') // and the actual path. - var split = gamePath.Path.Split( ( byte )'|', 3, false ); + byte ret = 0; + var split = gamePath.Path.Split( ( byte )'|', 3, false ); fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; var funcFound = ResourceLoadCustomization.GetInvocationList() diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 8569f547..5dcd2612 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,5 +1,6 @@ using System; using Dalamud.Hooking; +using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; @@ -79,8 +80,14 @@ public unsafe partial class PathResolver if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) { + PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", split, path ); SetCollection( path, collection ); } + else + { + PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); + } + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); PathCollections.TryRemove( path, out _ ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index fadd9545..4e3d302a 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -132,6 +132,7 @@ public partial class MetaManager return false; } + PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) && collection.Cache != null diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 83ed25ee..756d69d2 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -1,21 +1,9 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using System.Reflection; -using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using Penumbra.Api; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; using Penumbra.UI.Custom; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; @@ -60,8 +48,10 @@ public partial class SettingsInterface PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); + PrintValue( "Path Resolver Enabled", _penumbra.PathResolver.Enabled.ToString() ); //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } + private void DrawDebugTabIpc() { if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) From bc47e08e08d355434b195b5224e7577fad69c7f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Mar 2022 15:01:24 +0100 Subject: [PATCH 0122/2451] Collection inheritance start. --- Penumbra/Mod/ModCache.cs | 87 +++++- Penumbra/Mods/CollectionManager.cs | 117 +++++++++ Penumbra/Mods/ModCollection.Changes.cs | 113 ++++++++ Penumbra/Mods/ModCollection.Inheritance.cs | 72 +++++ Penumbra/Mods/ModCollection.Migration.cs | 47 ++++ Penumbra/Mods/ModCollection.cs | 188 +++++++++++++ Penumbra/Mods/ModCollectionCache.cs | 291 +++++++++++++++++++++ 7 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Mods/ModCollection.Changes.cs create mode 100644 Penumbra/Mods/ModCollection.Inheritance.cs create mode 100644 Penumbra/Mods/ModCollection.Migration.cs diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 3c413428..6fc6486c 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -1,11 +1,96 @@ +using System; using System.Collections.Generic; using System.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Meta; using Penumbra.Meta.Manipulations; namespace Penumbra.Mod; +public struct ModCache2 +{ + public readonly struct ModCacheStruct : IComparable< ModCacheStruct > + { + public readonly object Conflict; + public readonly int Mod1; + public readonly int Mod2; + public readonly bool Mod1Priority; + public readonly bool Solved; + + public ModCacheStruct( int modIdx1, int modIdx2, int priority1, int priority2, object conflict ) + { + Mod1 = modIdx1; + Mod2 = modIdx2; + Conflict = conflict; + Mod1Priority = priority1 >= priority2; + Solved = priority1 != priority2; + } + + public int CompareTo( ModCacheStruct other ) + { + var idxComp = Mod1.CompareTo( other.Mod1 ); + if( idxComp != 0 ) + { + return idxComp; + } + + if( Mod1Priority != other.Mod1Priority ) + { + return Mod1Priority ? 1 : -1; + } + + idxComp = Mod2.CompareTo( other.Mod2 ); + if( idxComp != 0 ) + { + return idxComp; + } + + return Conflict switch + { + Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ), + Utf8GamePath => -1, + MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ), + MetaManipulation => 1, + _ => 0, + }; + } + } + + private List< ModCacheStruct >? _conflicts; + + public IReadOnlyList< ModCacheStruct > Conflicts + => _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); + + public void Sort() + => _conflicts?.Sort(); + + public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath ) + { + _conflicts ??= new List< ModCacheStruct >( 2 ); + + _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) ); + _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) ); + } + + public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) + { + _conflicts ??= new List< ModCacheStruct >( 2 ); + _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) ); + _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) ); + } + + public void ClearConflicts() + => _conflicts?.Clear(); + + public void ClearFileConflicts() + => _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath ); + + public void ClearMetaConflicts() + => _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation ); + + public void ClearConflictsWithMod( int modIdx ) + => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx ); +} + // The ModCache contains volatile information dependent on all current settings in a collection. public class ModCache { diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index b99d9657..a00b01be 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -9,6 +10,122 @@ using Penumbra.Util; namespace Penumbra.Mods; +public sealed class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > +{ + private readonly ModManager _modManager; + + private readonly List< ModCollection2 > _collections = new(); + + public ModCollection2 this[ int idx ] + => _collections[ idx ]; + + public ModCollection2? this[ string name ] + => ByName( name, out var c ) ? c : null; + + public ModCollection2 Default + => this[ ModCollection2.DefaultCollection ]!; + + public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) + => _collections.FindFirst( c => c.Name == name, out collection ); + + public IEnumerator< ModCollection2 > GetEnumerator() + => _collections.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public CollectionManager2( ModManager manager ) + { + _modManager = manager; + + //_modManager.ModsRediscovered += OnModsRediscovered; + //_modManager.ModChange += OnModChanged; + ReadCollections(); + //LoadConfigCollections( Penumbra.Config ); + } + + public void Dispose() + { } + + private void AddDefaultCollection() + { + if( this[ ModCollection.DefaultCollection ] != null ) + { + return; + } + + var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + defaultCollection.Save(); + _collections.Add( defaultCollection ); + } + + private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + { + foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + { + var changes = false; + foreach( var subCollectionName in inheritance ) + { + if( !ByName( subCollectionName, out var subCollection ) ) + { + changes = true; + PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + } + else if( !collection.AddInheritance( subCollection ) ) + { + changes = true; + PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + } + } + + foreach( var (setting, mod) in collection.Settings.Zip( Penumbra.ModManager.Mods ).Where( s => s.First != null ) ) + { + changes |= setting!.FixInvalidSettings( mod.Meta ); + } + + if( changes ) + { + collection.Save(); + } + } + } + + private void ReadCollections() + { + var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); + var inheritances = new List< IReadOnlyList< string > >(); + if( collectionDir.Exists ) + { + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + { + var collection = ModCollection2.LoadFromFile( file, out var inheritance ); + if( collection == null || collection.Name.Length == 0 ) + { + continue; + } + + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( this[ collection.Name ] != null ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + inheritances.Add( inheritance ); + _collections.Add( collection ); + } + } + } + + AddDefaultCollection(); + ApplyInheritancesAndFixSettings( inheritances ); + } +} + public enum CollectionType : byte { Inactive, diff --git a/Penumbra/Mods/ModCollection.Changes.cs b/Penumbra/Mods/ModCollection.Changes.cs new file mode 100644 index 00000000..0423ffe8 --- /dev/null +++ b/Penumbra/Mods/ModCollection.Changes.cs @@ -0,0 +1,113 @@ +using System; +using Penumbra.Mod; + +namespace Penumbra.Mods; + +public enum ModSettingChange +{ + Inheritance, + EnableState, + Priority, + Setting, +} + +public partial class ModCollection2 +{ + public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName ); + public event ModSettingChangeDelegate ModSettingChanged; + + // Enable or disable the mod inheritance of mod idx. + public void SetModInheritance( int idx, bool inherit ) + { + if( FixInheritance( idx, inherit ) ) + { + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null ); + } + } + + // Set the enabled state mod idx to newValue if it differs from the current priority. + // If mod idx is currently inherited, stop the inheritance. + public void SetModState( int idx, bool newValue ) + { + var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; + if( newValue != oldValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Enabled = newValue; + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null ); + } + } + + // Set the priority of mod idx to newValue if it differs from the current priority. + // If mod idx is currently inherited, stop the inheritance. + public void SetModPriority( int idx, int newValue ) + { + var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; + if( newValue != oldValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Priority = newValue; + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null ); + } + } + + // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. + // If mod idx is currently inherited, stop the inheritance. + public void SetModSetting( int idx, string settingName, int newValue ) + { + var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; + var oldValue = settings != null + ? settings.TryGetValue( settingName, out var v ) ? v : newValue + : Penumbra.ModManager.Mods[ idx ].Meta.Groups.ContainsKey( settingName ) + ? 0 + : newValue; + if( oldValue != newValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Settings[ settingName ] = newValue; + _settings[ idx ]!.FixSpecificSetting( settingName, Penumbra.ModManager.Mods[ idx ].Meta ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName ); + } + } + + // Change one of the available mod settings for mod idx discerned by type. + // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. + // The setting will also be automatically fixed if it is invalid for that setting group. + // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. + public void ChangeModSetting( ModSettingChange type, int idx, int newValue, string? settingName = null ) + { + switch( type ) + { + case ModSettingChange.Inheritance: + SetModInheritance( idx, newValue != 0 ); + break; + case ModSettingChange.EnableState: + SetModState( idx, newValue != 0 ); + break; + case ModSettingChange.Priority: + SetModPriority( idx, newValue ); + break; + case ModSettingChange.Setting: + SetModSetting( idx, settingName ?? string.Empty, newValue ); + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + // Set inheritance of a mod without saving, + // to be used as an intermediary. + private bool FixInheritance( int idx, bool inherit ) + { + var settings = _settings[ idx ]; + if( inherit != ( settings == null ) ) + { + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + return true; + } + + return false; + } + + private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4 ) + => Save(); +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.Inheritance.cs b/Penumbra/Mods/ModCollection.Inheritance.cs new file mode 100644 index 00000000..13ba09ca --- /dev/null +++ b/Penumbra/Mods/ModCollection.Inheritance.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class ModCollection2 +{ + private readonly List< ModCollection2 > _inheritance = new(); + + public event Action InheritanceChanged; + + public IReadOnlyList< ModCollection2 > Inheritance + => _inheritance; + + public IEnumerable< ModCollection2 > GetFlattenedInheritance() + { + yield return this; + + foreach( var collection in _inheritance.SelectMany( c => c._inheritance ) + .Where( c => !ReferenceEquals( this, c ) ) + .Distinct() ) + { + yield return collection; + } + } + + public bool AddInheritance( ModCollection2 collection ) + { + if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) + { + return false; + } + + _inheritance.Add( collection ); + InheritanceChanged.Invoke(); + return true; + } + + public void RemoveInheritance( int idx ) + { + _inheritance.RemoveAt( idx ); + InheritanceChanged.Invoke(); + } + + public void MoveInheritance( int from, int to ) + { + if( _inheritance.Move( from, to ) ) + { + InheritanceChanged.Invoke(); + } + } + + public (ModSettings? Settings, ModCollection2 Collection) this[ int idx ] + { + get + { + foreach( var collection in GetFlattenedInheritance() ) + { + var settings = _settings[ idx ]; + if( settings != null ) + { + return ( settings, collection ); + } + } + + return ( null, this ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.Migration.cs b/Penumbra/Mods/ModCollection.Migration.cs new file mode 100644 index 00000000..76b40ae7 --- /dev/null +++ b/Penumbra/Mods/ModCollection.Migration.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Penumbra.Mod; + +namespace Penumbra.Mods; + +public partial class ModCollection2 +{ + private static class Migration + { + public static void Migrate( ModCollection2 collection ) + { + var changes = MigrateV0ToV1( collection ); + if( changes ) + { + collection.Save(); + } + } + + private static bool MigrateV0ToV1( ModCollection2 collection ) + { + if( collection.Version > 0 ) + { + return false; + } + + collection.Version = 1; + for( var i = 0; i < collection._settings.Count; ++i ) + { + var setting = collection._settings[ i ]; + if( SettingIsDefaultV0( collection._settings[ i ] ) ) + { + collection._settings[ i ] = null; + } + } + + foreach( var (key, _) in collection._unusedSettings.Where( kvp => SettingIsDefaultV0( kvp.Value ) ).ToList() ) + { + collection._unusedSettings.Remove( key ); + } + + return true; + } + + private static bool SettingIsDefaultV0( ModSettings? setting ) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 8b285c36..9a00250b 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using Dalamud.Logging; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Mod; @@ -12,6 +14,192 @@ using Penumbra.Util; namespace Penumbra.Mods; +public partial class ModCollection2 +{ + public const int CurrentVersion = 1; + public const string DefaultCollection = "Default"; + + public string Name { get; private init; } + public int Version { get; private set; } + + private readonly List< ModSettings? > _settings; + + public IReadOnlyList< ModSettings? > Settings + => _settings; + + public IEnumerable< ModSettings? > ActualSettings + => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + + private readonly Dictionary< string, ModSettings > _unusedSettings; + + private ModCollection2( string name, ModCollection2 duplicate ) + { + Name = name; + Version = duplicate.Version; + _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); + _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + _inheritance = duplicate._inheritance.ToList(); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) + { + Name = name; + Version = version; + _unusedSettings = allSettings; + _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); + for( var i = 0; i < Penumbra.ModManager.Count; ++i ) + { + var modName = Penumbra.ModManager[ i ].BasePath.Name; + if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) + { + _unusedSettings.Remove( modName ); + _settings[ i ] = settings; + } + } + + Migration.Migrate( this ); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + public static ModCollection2 CreateNewEmpty( string name ) + => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + + public ModCollection2 Duplicate( string name ) + => new(name, this); + + private void CleanUnavailableSettings() + { + var any = _unusedSettings.Count > 0; + _unusedSettings.Clear(); + if( any ) + { + Save(); + } + } + + public void AddMod( ModData mod ) + { + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + _settings.Add( settings ); + _unusedSettings.Remove( mod.BasePath.Name ); + } + else + { + _settings.Add( null ); + } + } + + public void RemoveMod( ModData mod, int idx ) + { + var settings = _settings[ idx ]; + if( settings != null ) + { + _unusedSettings.Add( mod.BasePath.Name, settings ); + } + + _settings.RemoveAt( idx ); + } + + public static string CollectionDirectory + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); + + private FileInfo FileName + => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + + public void Save() + { + try + { + var file = FileName; + file.Directory?.Create(); + using var s = file.Open( FileMode.Truncate ); + using var w = new StreamWriter( s, Encoding.UTF8 ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); + j.WriteStartObject(); + j.WritePropertyName( nameof( Version ) ); + j.WriteValue( Version ); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( Name ); + j.WritePropertyName( nameof( Settings ) ); + j.WriteStartObject(); + for( var i = 0; i < _settings.Count; ++i ) + { + var settings = _settings[ i ]; + if( settings != null ) + { + j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); + x.Serialize( j, settings ); + } + } + + foreach( var settings in _unusedSettings ) + { + j.WritePropertyName( settings.Key ); + x.Serialize( j, settings.Value ); + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( Inheritance ) ); + x.Serialize( j, Inheritance ); + j.WriteEndObject(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + } + } + + public void Delete() + { + var file = FileName; + if( file.Exists ) + { + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); + } + } + } + + public static ModCollection2? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + { + inheritance = Array.Empty< string >(); + if( !file.Exists ) + { + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + return null; + } + + try + { + var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); + var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; + var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() + ?? new Dictionary< string, ModSettings >(); + inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + + return new ModCollection2( name, version, settings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + } + + return null; + } +} + // A ModCollection is a named set of ModSettings to all of the users' installed mods. // It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 6af67ad3..d7d56224 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -13,6 +13,297 @@ using Penumbra.Util; namespace Penumbra.Mods; +// The ModCollectionCache contains all required temporary data to use a collection. +// It will only be setup if a collection gets activated in any way. +public class ModCollectionCache2 +{ + // Shared caches to avoid allocations. + private static readonly BitArray FileSeen = new(256); + private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); + private static readonly List< ModSettings? > ResolvedSettings = new(128); + + private readonly ModCollection2 _collection; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly MetaManager MetaManipulations; + private ModCache2 _cache; + + public IReadOnlyDictionary< string, object? > ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } + + public ModCollectionCache2( ModCollection2 collection ) + => _collection = collection; + + //MetaManipulations = new MetaManager( collection ); + private static void ResetFileSeen( int size ) + { + if( size < FileSeen.Length ) + { + FileSeen.Length = size; + FileSeen.SetAll( false ); + } + else + { + FileSeen.SetAll( false ); + FileSeen.Length = size; + } + } + + private void ClearStorageAndPrepare() + { + ResolvedFiles.Clear(); + MissingFiles.Clear(); + RegisteredFiles.Clear(); + _changedItems.Clear(); + _cache.ClearFileConflicts(); + + ResolvedSettings.Clear(); + ResolvedSettings.AddRange( _collection.ActualSettings ); + } + + public void CalculateEffectiveFileList() + { + ClearStorageAndPrepare(); + + for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) + { + if( ResolvedSettings[ i ]?.Enabled == true ) + { + AddFiles( i ); + AddSwaps( i ); + } + } + + AddMetaFiles(); + } + + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) + { + identifier.Identify( _changedItems, resolved.ToGamePath() ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + + + private void AddFiles( int idx ) + { + var mod = Penumbra.ModManager.Mods[ idx ]; + ResetFileSeen( mod.Resources.ModFiles.Count ); + // Iterate in reverse so that later groups take precedence before earlier ones. + foreach( var group in mod.Meta.Groups.Values.Reverse() ) + { + switch( group.SelectionType ) + { + case SelectType.Single: + AddFilesForSingle( group, mod, idx ); + break; + case SelectType.Multi: + AddFilesForMulti( group, mod, idx ); + break; + default: throw new InvalidEnumArgumentException(); + } + } + + AddRemainingFiles( mod, idx ); + } + + private static bool FilterFile( Utf8GamePath gamePath ) + { + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + if( !Penumbra.Config.DisableSoundStreaming + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) + { + return true; + } + + return false; + } + + + private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) + { + if( FilterFile( gamePath ) ) + { + return; + } + + if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) + { + RegisteredFiles.Add( gamePath, modIdx ); + ResolvedFiles[ gamePath ] = file; + } + else + { + var priority = ResolvedSettings[ modIdx ]!.Priority; + var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; + _cache.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); + if( priority > oldPriority ) + { + ResolvedFiles[ gamePath ] = file; + RegisteredFiles[ gamePath ] = modIdx; + } + } + } + + private void AddMissingFile( FullPath file ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + return; + default: + MissingFiles.Add( file ); + return; + } + } + + private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) + { + foreach( var (file, paths) in option.OptionFiles ) + { + var fullPath = new FullPath( mod.BasePath, file ); + var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + if( idx < 0 ) + { + AddMissingFile( fullPath ); + continue; + } + + var registeredFile = mod.Resources.ModFiles[ idx ]; + if( !registeredFile.Exists ) + { + AddMissingFile( registeredFile ); + continue; + } + + FileSeen.Set( idx, true ); + if( enabled ) + { + foreach( var path in paths ) + { + AddFile( modIdx, path, registeredFile ); + } + } + } + } + + private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) + { + Debug.Assert( singleGroup.SelectionType == SelectType.Single ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) + { + setting = 0; + } + + for( var i = 0; i < singleGroup.Options.Count; ++i ) + { + AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); + } + } + + private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) + { + Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) + { + return; + } + + // Also iterate options in reverse so that later options take precedence before earlier ones. + for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) + { + AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); + } + } + + private void AddRemainingFiles( ModData mod, int modIdx ) + { + for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) + { + if( FileSeen.Get( i ) ) + { + continue; + } + + var file = mod.Resources.ModFiles[ i ]; + if( file.Exists ) + { + if( file.ToGamePath( mod.BasePath, out var gamePath ) ) + { + AddFile( modIdx, gamePath, file ); + } + else + { + PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); + } + } + else + { + MissingFiles.Add( file ); + } + } + } + + private void AddMetaFiles() + => MetaManipulations.Imc.SetFiles(); + + private void AddSwaps( int modIdx ) + { + var mod = Penumbra.ModManager.Mods[ modIdx ]; + foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + { + AddFile( modIdx, gamePath, swapPath ); + } + } + + // TODO Manipulations + public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) + { + return null; + } + + return candidate; + } + + public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) + => GetCandidateForGameFile( gameResourcePath ); +} + // The ModCollectionCache contains all required temporary data to use a collection. // It will only be setup if a collection gets activated in any way. public class ModCollectionCache From 9a0b0bfa0fc6aacc5e5467ad75ed4062e71f845e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Mar 2022 18:34:32 +0100 Subject: [PATCH 0123/2451] tmp --- Penumbra/Api/ModsController.cs | 20 +- Penumbra/Api/PenumbraApi.cs | 18 +- .../Collections/CollectionManager.Active.cs | 261 +++++++ Penumbra/Collections/CollectionManager.cs | 223 ++++++ Penumbra/Collections/ModCollection.Cache.cs | 472 +++++++++++++ .../ModCollection.Changes.cs | 2 +- .../ModCollection.Inheritance.cs | 4 +- .../ModCollection.Migration.cs | 4 +- Penumbra/Collections/ModCollection.cs | 209 ++++++ .../Loader/ResourceLoader.Replacement.cs | 3 +- .../Interop/Resolver/PathResolver.Data.cs | 19 +- .../Interop/Resolver/PathResolver.Material.cs | 7 +- .../Interop/Resolver/PathResolver.Meta.cs | 25 +- .../Interop/Resolver/PathResolver.Resolve.cs | 8 +- Penumbra/Interop/Resolver/PathResolver.cs | 25 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 12 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 10 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 12 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 10 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 14 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 55 +- Penumbra/Meta/Manager/MetaManager.cs | 36 +- Penumbra/MigrateConfiguration.cs | 13 +- Penumbra/Mod/ModCache.cs | 2 +- Penumbra/Mod/ModCleanup.cs | 15 +- Penumbra/Mods/CollectionManager.cs | 574 --------------- Penumbra/Mods/ModCollection.cs | 523 -------------- Penumbra/Mods/ModCollectionCache.cs | 662 ------------------ Penumbra/Mods/ModManager.cs | 10 +- Penumbra/Mods/ModManagerEditExtensions.cs | 32 +- Penumbra/Penumbra.cs | 36 +- Penumbra/UI/MenuTabs/TabCollections.cs | 9 +- Penumbra/UI/MenuTabs/TabEffective.cs | 15 +- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 2 +- .../TabInstalled/TabInstalledSelector.cs | 20 +- 35 files changed, 1365 insertions(+), 1997 deletions(-) create mode 100644 Penumbra/Collections/CollectionManager.Active.cs create mode 100644 Penumbra/Collections/CollectionManager.cs create mode 100644 Penumbra/Collections/ModCollection.Cache.cs rename Penumbra/{Mods => Collections}/ModCollection.Changes.cs (99%) rename Penumbra/{Mods => Collections}/ModCollection.Inheritance.cs (97%) rename Penumbra/{Mods => Collections}/ModCollection.Migration.cs (94%) create mode 100644 Penumbra/Collections/ModCollection.cs delete mode 100644 Penumbra/Mods/CollectionManager.cs delete mode 100644 Penumbra/Mods/ModCollection.cs delete mode 100644 Penumbra/Mods/ModCollectionCache.cs diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 3232931a..03cf6885 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,15 +16,15 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - return Penumbra.CollectionManager.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new - { - x.Settings.Enabled, - x.Settings.Priority, - x.Data.BasePath.Name, - x.Data.Meta, - BasePath = x.Data.BasePath.FullName, - Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), - } ); + return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new + { + x.Second?.Enabled, + x.Second?.Priority, + x.First.BasePath.Name, + x.First.Meta, + BasePath = x.First.BasePath.FullName, + Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), + } ); } [Route( HttpVerbs.Post, "/mods" )] @@ -34,7 +34,7 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/files" )] public object GetFiles() { - return Penumbra.CollectionManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), o => o.Value.FullName ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 540e1432..6a373d14 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -5,6 +5,7 @@ using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -76,7 +77,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, ModManager _, ModCollection collection ) + private static string ResolvePath( string path, ModManager _, ModCollection2 collection ) { if( !Penumbra.Config.EnableMods ) { @@ -84,24 +85,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= Penumbra.CollectionManager.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } public string ResolvePath( string path ) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.DefaultCollection ); + return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); } public string ResolvePath( string path, string characterName ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.CharacterCollection.TryGetValue( characterName, out var collection ) - ? collection - : ModCollection.Empty ); + Penumbra.CollectionManager.Character( characterName ) ); } private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource @@ -136,12 +134,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { - collection = ModCollection.Empty; + collection = ModCollection2.Empty; } - if( collection.Cache != null ) + if( collection.HasCache ) { - return collection.Cache.ChangedItems; + return collection.ChangedItems; } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs new file mode 100644 index 00000000..3250e10d --- /dev/null +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Dalamud.Logging; +using Penumbra.Meta.Manager; +using Penumbra.Mod; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public sealed partial class CollectionManager2 +{ + // Is invoked after the collections actually changed. + public event CollectionChangeDelegate? CollectionChanged; + + private int _currentIdx = -1; + private int _defaultIdx = -1; + private int _defaultNameIdx = 0; + + public ModCollection2 Current + => this[ _currentIdx ]; + + public ModCollection2 Default + => this[ _defaultIdx ]; + + private readonly Dictionary< string, int > _character = new(); + + public ModCollection2 Character( string name ) + => _character.TryGetValue( name, out var idx ) ? _collections[ idx ] : Default; + + public bool HasCharacterCollections + => _character.Count > 0; + + private void OnModChanged( ModChangeType type, int idx, ModData mod ) + { + switch( type ) + { + case ModChangeType.Added: + foreach( var collection in _collections ) + { + collection.AddMod( mod ); + } + + foreach( var collection in _collections.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + case ModChangeType.Removed: + var list = new List< ModSettings? >( _collections.Count ); + foreach( var collection in _collections ) + { + list.Add( collection[ idx ].Settings ); + collection.RemoveMod( mod, idx ); + } + + foreach( var (collection, _) in _collections.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + case ModChangeType.Changed: + foreach( var collection in _collections.Where( + collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + { + collection.Save(); + } + + foreach( var collection in _collections.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + private void CreateNecessaryCaches() + { + if( _defaultIdx >= 0 ) + { + Default.CreateCache(); + } + + if( _currentIdx >= 0 ) + { + Current.CreateCache(); + } + + foreach( var idx in _character.Values.Where( i => i >= 0 ) ) + { + _collections[ idx ].CreateCache(); + } + } + + public void UpdateCaches() + { + foreach( var collection in _collections ) + { + collection.UpdateCache(); + } + } + + private void RemoveCache( int idx ) + { + if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) + { + _collections[ idx ].ClearCache(); + } + } + + public void SetCollection( string name, CollectionType type, string? characterName = null ) + => SetCollection( GetIndexForCollectionName( name ), type, characterName ); + + public void SetCollection( ModCollection2 collection, CollectionType type, string? characterName = null ) + => SetCollection( GetIndexForCollectionName( collection.Name ), type, characterName ); + + public void SetCollection( int newIdx, CollectionType type, string? characterName = null ) + { + var oldCollectionIdx = type switch + { + CollectionType.Default => _defaultIdx, + CollectionType.Current => _currentIdx, + CollectionType.Character => characterName?.Length > 0 + ? _character.TryGetValue( characterName, out var c ) + ? c + : _defaultIdx + : -2, + _ => -2, + }; + + if( oldCollectionIdx == -2 || newIdx == oldCollectionIdx ) + { + return; + } + + var newCollection = this[ newIdx ]; + if( newIdx >= 0 ) + { + newCollection.CreateCache(); + } + + RemoveCache( oldCollectionIdx ); + switch( type ) + { + case CollectionType.Default: + _defaultIdx = newIdx; + Penumbra.Config.DefaultCollection = newCollection.Name; + Penumbra.ResidentResources.Reload(); + Default.SetFiles(); + break; + case CollectionType.Current: + _currentIdx = newIdx; + Penumbra.Config.CurrentCollection = newCollection.Name; + break; + case CollectionType.Character: + _character[ characterName! ] = newIdx; + Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; + break; + } + + CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName ); + Penumbra.Config.Save(); + } + + public bool CreateCharacterCollection( string characterName ) + { + if( _character.ContainsKey( characterName ) ) + { + return false; + } + + _character[ characterName ] = -1; + Penumbra.Config.CharacterCollections[ characterName ] = ModCollection2.Empty.Name; + Penumbra.Config.Save(); + CollectionChanged?.Invoke( null, ModCollection2.Empty, CollectionType.Character, characterName ); + return true; + } + + public void RemoveCharacterCollection( string characterName ) + { + if( _character.TryGetValue( characterName, out var collection ) ) + { + RemoveCache( collection ); + _character.Remove( characterName ); + CollectionChanged?.Invoke( this[ collection ], null, CollectionType.Character, characterName ); + } + + if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) + { + Penumbra.Config.Save(); + } + } + + private int GetIndexForCollectionName( string name ) + { + if( name.Length == 0 || name == ModCollection2.DefaultCollection ) + { + return -1; + } + + var idx = _collections.IndexOf( c => c.Name == Penumbra.Config.DefaultCollection ); + return idx < 0 ? -2 : idx; + } + + public void LoadCollections() + { + var configChanged = false; + _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + if( _defaultIdx == -2 ) + { + PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); + _defaultIdx = -1; + Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; + configChanged = true; + } + + _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + if( _currentIdx == -2 ) + { + PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); + _currentIdx = _defaultNameIdx; + Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; + configChanged = true; + } + + if( LoadCharacterCollections() || configChanged ) + { + Penumbra.Config.Save(); + } + + CreateNecessaryCaches(); + } + + private bool LoadCharacterCollections() + { + var configChanged = false; + foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() ) + { + var idx = GetIndexForCollectionName( collectionName ); + if( idx == -2 ) + { + PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); + _character.Add( player, -1 ); + Penumbra.Config.CharacterCollections[ player ] = ModCollection2.Empty.Name; + configChanged = true; + } + else + { + _character.Add( player, idx ); + } + } + + return configChanged; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs new file mode 100644 index 00000000..7fd81136 --- /dev/null +++ b/Penumbra/Collections/CollectionManager.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public enum CollectionType : byte +{ + Inactive, + Default, + Character, + Current, +} + +public sealed partial class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > +{ + public delegate void CollectionChangeDelegate( ModCollection2? oldCollection, ModCollection2? newCollection, CollectionType type, + string? characterName = null ); + + private readonly ModManager _modManager; + + private readonly List< ModCollection2 > _collections = new(); + + public ModCollection2 this[ Index idx ] + => idx.Value == -1 ? ModCollection2.Empty : _collections[ idx ]; + + public ModCollection2? this[ string name ] + => ByName( name, out var c ) ? c : null; + + public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) + => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + + public IEnumerator< ModCollection2 > GetEnumerator() + => _collections.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public CollectionManager2( ModManager manager ) + { + _modManager = manager; + + _modManager.ModsRediscovered += OnModsRediscovered; + _modManager.ModChange += OnModChanged; + ReadCollections(); + LoadCollections(); + } + + public void Dispose() + { + _modManager.ModsRediscovered -= OnModsRediscovered; + _modManager.ModChange -= OnModChanged; + } + + private void OnModsRediscovered() + { + UpdateCaches(); + Default.SetFiles(); + } + + private void AddDefaultCollection() + { + var idx = _collections.IndexOf( c => c.Name == ModCollection2.DefaultCollection ); + if( idx >= 0 ) + { + _defaultNameIdx = idx; + return; + } + + var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + defaultCollection.Save(); + _defaultNameIdx = _collections.Count; + _collections.Add( defaultCollection ); + } + + private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + { + foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + { + var changes = false; + foreach( var subCollectionName in inheritance ) + { + if( !ByName( subCollectionName, out var subCollection ) ) + { + changes = true; + PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + } + else if( !collection.AddInheritance( subCollection ) ) + { + changes = true; + PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + } + } + + foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) + { + changes |= setting!.FixInvalidSettings( mod.Meta ); + } + + if( changes ) + { + collection.Save(); + } + } + } + + private void ReadCollections() + { + var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); + var inheritances = new List< IReadOnlyList< string > >(); + if( collectionDir.Exists ) + { + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + { + var collection = ModCollection2.LoadFromFile( file, out var inheritance ); + if( collection == null || collection.Name.Length == 0 ) + { + continue; + } + + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( this[ collection.Name ] != null ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + inheritances.Add( inheritance ); + _collections.Add( collection ); + } + } + } + + AddDefaultCollection(); + ApplyInheritancesAndFixSettings( inheritances ); + } + + public bool AddCollection( string name, ModCollection2? duplicate ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed.Length == 0 + || nameFixed == ModCollection2.Empty.Name.ToLowerInvariant() + || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = duplicate?.Duplicate( name ) ?? ModCollection2.CreateNewEmpty( name ); + _collections.Add( newCollection ); + newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); + SetCollection( _collections.Count - 1, CollectionType.Current ); + return true; + } + + public bool RemoveCollection( int idx ) + { + if( idx < 0 || idx >= _collections.Count ) + { + PluginLog.Error( "Can not remove the empty collection." ); + return false; + } + + if( idx == _defaultNameIdx ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( idx == _currentIdx ) + { + SetCollection( _defaultNameIdx, CollectionType.Current ); + } + else if( _currentIdx > idx ) + { + --_currentIdx; + } + + if( idx == _defaultIdx ) + { + SetCollection( -1, CollectionType.Default ); + } + else if( _defaultIdx > idx ) + { + --_defaultIdx; + } + + if( _defaultNameIdx > idx ) + { + --_defaultNameIdx; + } + + foreach( var (characterName, characterIdx) in _character.ToList() ) + { + if( idx == characterIdx ) + { + SetCollection( -1, CollectionType.Character, characterName ); + } + else if( characterIdx > idx ) + { + _character[ characterName ] = characterIdx - 1; + } + } + + var collection = _collections[ idx ]; + collection.Delete(); + _collections.RemoveAt( idx ); + CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs new file mode 100644 index 00000000..697dde2f --- /dev/null +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manager; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public partial class ModCollection2 +{ + private Cache? _cache; + + public bool HasCache + => _cache != null; + + public void CreateCache() + { + if( _cache == null ) + { + _cache = new Cache( this ); + _cache.CalculateEffectiveFileList(); + } + } + + public void UpdateCache() + => _cache?.CalculateEffectiveFileList(); + + public void ClearCache() + => _cache = null; + + public FullPath? ResolvePath( Utf8GamePath path ) + => _cache?.ResolvePath( path ); + + internal void ForceFile( Utf8GamePath path, FullPath fullPath ) + => _cache!.ResolvedFiles[ path ] = fullPath; + + internal void RemoveFile( Utf8GamePath path ) + => _cache!.ResolvedFiles.Remove( path ); + + internal MetaManager? MetaCache + => _cache?.MetaManipulations; + + internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + + internal IReadOnlySet< FullPath > MissingFiles + => _cache?.MissingFiles ?? new HashSet< FullPath >(); + + internal IReadOnlyDictionary< string, object? > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + + internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); + + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) + { + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); + _cache ??= new Cache( this ); + _cache.CalculateEffectiveFileList(); + if( withMetaManipulations ) + { + _cache.UpdateMetaManipulations(); + } + + if( reloadResident ) + { + Penumbra.ResidentResources.Reload(); + } + } + + + // The ModCollectionCache contains all required temporary data to use a collection. + // It will only be setup if a collection gets activated in any way. + private class Cache + { + // Shared caches to avoid allocations. + private static readonly BitArray FileSeen = new(256); + private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); + private static readonly List< ModSettings? > ResolvedSettings = new(128); + + private readonly ModCollection2 _collection; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly MetaManager MetaManipulations; + public ConflictCache Conflicts; + + public IReadOnlyDictionary< string, object? > ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } + + public Cache( ModCollection2 collection ) + { + _collection = collection; + MetaManipulations = new MetaManager( collection ); + } + + private static void ResetFileSeen( int size ) + { + if( size < FileSeen.Length ) + { + FileSeen.Length = size; + FileSeen.SetAll( false ); + } + else + { + FileSeen.SetAll( false ); + FileSeen.Length = size; + } + } + + private void ClearStorageAndPrepare() + { + ResolvedFiles.Clear(); + MissingFiles.Clear(); + RegisteredFiles.Clear(); + _changedItems.Clear(); + ResolvedSettings.Clear(); + ResolvedSettings.AddRange( _collection.ActualSettings ); + } + + public void CalculateEffectiveFileList() + { + ClearStorageAndPrepare(); + + for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) + { + if( ResolvedSettings[ i ]?.Enabled == true ) + { + AddFiles( i ); + AddSwaps( i ); + } + } + + AddMetaFiles(); + Conflicts.Sort(); + } + + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) + { + identifier.Identify( _changedItems, resolved.ToGamePath() ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + + + private void AddFiles( int idx ) + { + var mod = Penumbra.ModManager.Mods[ idx ]; + ResetFileSeen( mod.Resources.ModFiles.Count ); + // Iterate in reverse so that later groups take precedence before earlier ones. + foreach( var group in mod.Meta.Groups.Values.Reverse() ) + { + switch( group.SelectionType ) + { + case SelectType.Single: + AddFilesForSingle( group, mod, idx ); + break; + case SelectType.Multi: + AddFilesForMulti( group, mod, idx ); + break; + default: throw new InvalidEnumArgumentException(); + } + } + + AddRemainingFiles( mod, idx ); + } + + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + private static bool FilterFile( Utf8GamePath gamePath ) + => !Penumbra.Config.DisableSoundStreaming + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); + + + private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) + { + if( FilterFile( gamePath ) ) + { + return; + } + + if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) + { + RegisteredFiles.Add( gamePath, modIdx ); + ResolvedFiles[ gamePath ] = file; + } + else + { + var priority = ResolvedSettings[ modIdx ]!.Priority; + var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; + Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); + if( priority > oldPriority ) + { + ResolvedFiles[ gamePath ] = file; + RegisteredFiles[ gamePath ] = modIdx; + } + } + } + + private void AddMissingFile( FullPath file ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + return; + default: + MissingFiles.Add( file ); + return; + } + } + + private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) + { + foreach( var (file, paths) in option.OptionFiles ) + { + var fullPath = new FullPath( mod.BasePath, file ); + var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + if( idx < 0 ) + { + AddMissingFile( fullPath ); + continue; + } + + var registeredFile = mod.Resources.ModFiles[ idx ]; + if( !registeredFile.Exists ) + { + AddMissingFile( registeredFile ); + continue; + } + + FileSeen.Set( idx, true ); + if( enabled ) + { + foreach( var path in paths ) + { + AddFile( modIdx, path, registeredFile ); + } + } + } + } + + private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) + { + Debug.Assert( singleGroup.SelectionType == SelectType.Single ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) + { + setting = 0; + } + + for( var i = 0; i < singleGroup.Options.Count; ++i ) + { + AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); + } + } + + private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) + { + Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) + { + return; + } + + // Also iterate options in reverse so that later options take precedence before earlier ones. + for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) + { + AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); + } + } + + private void AddRemainingFiles( ModData mod, int modIdx ) + { + for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) + { + if( FileSeen.Get( i ) ) + { + continue; + } + + var file = mod.Resources.ModFiles[ i ]; + if( file.Exists ) + { + if( file.ToGamePath( mod.BasePath, out var gamePath ) ) + { + AddFile( modIdx, gamePath, file ); + } + else + { + PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); + } + } + else + { + MissingFiles.Add( file ); + } + } + } + + private void AddMetaFiles() + => MetaManipulations.Imc.SetFiles(); + + private void AddSwaps( int modIdx ) + { + var mod = Penumbra.ModManager.Mods[ modIdx ]; + foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + { + AddFile( modIdx, gamePath, swapPath ); + } + } + + private void AddManipulations( int modIdx ) + { + var mod = Penumbra.ModManager.Mods[ modIdx ]; + foreach( var manip in mod.Resources.MetaManipulations.GetManipulationsForConfig( ResolvedSettings[ modIdx ]!, mod.Meta ) ) + { + if( !MetaManipulations.TryGetValue( manip, out var oldModIdx ) ) + { + MetaManipulations.ApplyMod( manip, modIdx ); + } + else + { + var priority = ResolvedSettings[ modIdx ]!.Priority; + var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; + Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, manip ); + if( priority > oldPriority ) + { + MetaManipulations.ApplyMod( manip, modIdx ); + } + } + } + } + + public void UpdateMetaManipulations() + { + MetaManipulations.Reset(); + Conflicts.ClearMetaConflicts(); + + foreach( var mod in Penumbra.ModManager.Mods.Zip( ResolvedSettings ) + .Select( ( m, i ) => ( m.First, m.Second, i ) ) + .Where( m => m.Second?.Enabled == true && m.First.Resources.MetaManipulations.Count > 0 ) ) + { + AddManipulations( mod.i ); + } + } + + public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) + { + return null; + } + + return candidate; + } + } + + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + _cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + _cache.MetaManipulations.SetFiles(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs similarity index 99% rename from Penumbra/Mods/ModCollection.Changes.cs rename to Penumbra/Collections/ModCollection.Changes.cs index 0423ffe8..02f6f240 100644 --- a/Penumbra/Mods/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,7 +1,7 @@ using System; using Penumbra.Mod; -namespace Penumbra.Mods; +namespace Penumbra.Collections; public enum ModSettingChange { diff --git a/Penumbra/Mods/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs similarity index 97% rename from Penumbra/Mods/ModCollection.Inheritance.cs rename to Penumbra/Collections/ModCollection.Inheritance.cs index 13ba09ca..dee20aef 100644 --- a/Penumbra/Mods/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -4,7 +4,7 @@ using System.Linq; using Penumbra.Mod; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Collections; public partial class ModCollection2 { @@ -53,7 +53,7 @@ public partial class ModCollection2 } } - public (ModSettings? Settings, ModCollection2 Collection) this[ int idx ] + public (ModSettings? Settings, ModCollection2 Collection) this[ Index idx ] { get { diff --git a/Penumbra/Mods/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs similarity index 94% rename from Penumbra/Mods/ModCollection.Migration.cs rename to Penumbra/Collections/ModCollection.Migration.cs index 76b40ae7..abd935ab 100644 --- a/Penumbra/Mods/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,9 +1,9 @@ using System.Linq; using Penumbra.Mod; -namespace Penumbra.Mods; +namespace Penumbra.Collections; -public partial class ModCollection2 +public sealed partial class ModCollection2 { private static class Migration { diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs new file mode 100644 index 00000000..5f6c46ea --- /dev/null +++ b/Penumbra/Collections/ModCollection.cs @@ -0,0 +1,209 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Logging; +using Newtonsoft.Json.Linq; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Collections; + +// A ModCollection is a named set of ModSettings to all of the users' installed mods. +// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. +// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. +// Active ModCollections build a cache of currently relevant data. +public partial class ModCollection2 +{ + public const int CurrentVersion = 1; + public const string DefaultCollection = "Default"; + + public static readonly ModCollection2 Empty = CreateNewEmpty( "None" ); + + public string Name { get; private init; } + public int Version { get; private set; } + + private readonly List< ModSettings? > _settings; + + public IReadOnlyList< ModSettings? > Settings + => _settings; + + public IEnumerable< ModSettings? > ActualSettings + => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + + private readonly Dictionary< string, ModSettings > _unusedSettings; + + + private ModCollection2( string name, ModCollection2 duplicate ) + { + Name = name; + Version = duplicate.Version; + _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); + _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + _inheritance = duplicate._inheritance.ToList(); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) + { + Name = name; + Version = version; + _unusedSettings = allSettings; + _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); + for( var i = 0; i < Penumbra.ModManager.Count; ++i ) + { + var modName = Penumbra.ModManager[ i ].BasePath.Name; + if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) + { + _unusedSettings.Remove( modName ); + _settings[ i ] = settings; + } + } + + Migration.Migrate( this ); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + public static ModCollection2 CreateNewEmpty( string name ) + => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + + public ModCollection2 Duplicate( string name ) + => new(name, this); + + internal static ModCollection2 MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + => new(name, 0, allSettings); + + private void CleanUnavailableSettings() + { + var any = _unusedSettings.Count > 0; + _unusedSettings.Clear(); + if( any ) + { + Save(); + } + } + + public void AddMod( ModData mod ) + { + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + _settings.Add( settings ); + _unusedSettings.Remove( mod.BasePath.Name ); + } + else + { + _settings.Add( null ); + } + } + + public void RemoveMod( ModData mod, int idx ) + { + var settings = _settings[ idx ]; + if( settings != null ) + { + _unusedSettings.Add( mod.BasePath.Name, settings ); + } + + _settings.RemoveAt( idx ); + } + + public static string CollectionDirectory + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); + + public FileInfo FileName + => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + + public void Save() + { + try + { + var file = FileName; + file.Directory?.Create(); + using var s = file.Open( FileMode.Truncate ); + using var w = new StreamWriter( s, Encoding.UTF8 ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); + j.WriteStartObject(); + j.WritePropertyName( nameof( Version ) ); + j.WriteValue( Version ); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( Name ); + j.WritePropertyName( nameof( Settings ) ); + j.WriteStartObject(); + for( var i = 0; i < _settings.Count; ++i ) + { + var settings = _settings[ i ]; + if( settings != null ) + { + j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); + x.Serialize( j, settings ); + } + } + + foreach( var settings in _unusedSettings ) + { + j.WritePropertyName( settings.Key ); + x.Serialize( j, settings.Value ); + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( Inheritance ) ); + x.Serialize( j, Inheritance ); + j.WriteEndObject(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + } + } + + public void Delete() + { + var file = FileName; + if( file.Exists ) + { + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); + } + } + } + + public static ModCollection2? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + { + inheritance = Array.Empty< string >(); + if( !file.Exists ) + { + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + return null; + } + + try + { + var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); + var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; + var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() + ?? new Dictionary< string, ModSettings >(); + inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + + return new ModCollection2( name, version, settings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + } + + return null; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index d89a18b7..3d869944 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -91,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); + var resolved = ModManager.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7907b172..ba8d6b05 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Component.GUI; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Mods; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -90,10 +91,10 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); + internal readonly Dictionary< IntPtr, (ModCollection2, int) > DrawObjectToObject = new(); // This map links files to their corresponding collection, if it is non-default. - internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); + internal readonly ConcurrentDictionary< Utf8String, ModCollection2 > PathCollections = new(); internal GameObject* LastGameObject = null; @@ -158,11 +159,11 @@ public unsafe partial class PathResolver } // Identify the correct collection for a GameObject by index and name. - private static ModCollection IdentifyCollection( GameObject* gameObject ) + private static ModCollection2 IdentifyCollection( GameObject* gameObject ) { if( gameObject == null ) { - return Penumbra.CollectionManager.DefaultCollection; + return Penumbra.CollectionManager.Default; } var name = gameObject->ObjectIndex switch @@ -175,13 +176,11 @@ public unsafe partial class PathResolver } ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.CollectionManager.CharacterCollection.TryGetValue( name, out var col ) - ? col - : Penumbra.CollectionManager.DefaultCollection; + return Penumbra.CollectionManager.Character( name ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection? _1, ModCollection? _2, CollectionType type, string? name ) + private void CheckCollections( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? name ) { if( type is not (CollectionType.Character or CollectionType.Default) ) { @@ -201,7 +200,7 @@ public unsafe partial class PathResolver } // Use the stored information to find the GameObject and Collection linked to a DrawObject. - private GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) + private GameObject* FindParent( IntPtr drawObject, out ModCollection2 collection ) { if( DrawObjectToObject.TryGetValue( drawObject, out var data ) ) { @@ -226,7 +225,7 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection collection ) + private void SetCollection( Utf8String path, ModCollection2 collection ) { if( PathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 5dcd2612..51aeeae8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -3,6 +3,7 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -40,7 +41,7 @@ public unsafe partial class PathResolver return ret; } - private ModCollection? _mtrlCollection; + private ModCollection2? _mtrlCollection; private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) { @@ -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, out ModCollection2? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { @@ -95,7 +96,7 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - private void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + private static void HandleMtrlCollection( ModCollection2 collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, object?) data ) { if( nonDefault && type == ResourceType.Mtrl ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index ee556c25..d32eee9f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -2,6 +2,7 @@ using System; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; +using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -160,15 +161,15 @@ public unsafe partial class PathResolver RspSetupCharacterHook?.Dispose(); } - private ModCollection? GetCollection( IntPtr drawObject ) + private ModCollection2? GetCollection( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection == Penumbra.CollectionManager.DefaultCollection ) + if( parent == null || collection == Penumbra.CollectionManager.Default ) { return null; } - return collection.Cache == null ? Penumbra.CollectionManager.ForcedCollection : collection; + return collection.HasCache ? collection : null; } @@ -194,7 +195,7 @@ public unsafe partial class PathResolver } } - public static MetaChanger ChangeEqp( ModCollection collection ) + public static MetaChanger ChangeEqp( ModCollection2 collection ) { #if USE_EQP collection.SetEqpFiles(); @@ -232,7 +233,7 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeEqdp( ModCollection collection ) + public static MetaChanger ChangeEqdp( ModCollection2 collection ) { #if USE_EQDP collection.SetEqdpFiles(); @@ -268,13 +269,13 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) + public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection2? collection ) { if( resolver.LastGameObject != null ) { collection = IdentifyCollection( resolver.LastGameObject ); #if USE_CMP - if( collection != Penumbra.CollectionManager.DefaultCollection && collection.Cache != null ) + if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); @@ -309,25 +310,25 @@ public unsafe partial class PathResolver case MetaManipulation.Type.Eqdp: if( --_eqdpCounter == 0 ) { - Penumbra.CollectionManager.DefaultCollection.SetEqdpFiles(); + Penumbra.CollectionManager.Default.SetEqdpFiles(); } break; case MetaManipulation.Type.Eqp: if( --_eqpCounter == 0 ) { - Penumbra.CollectionManager.DefaultCollection.SetEqpFiles(); + Penumbra.CollectionManager.Default.SetEqpFiles(); } break; case MetaManipulation.Type.Est: - Penumbra.CollectionManager.DefaultCollection.SetEstFiles(); + Penumbra.CollectionManager.Default.SetEstFiles(); break; case MetaManipulation.Type.Gmp: - Penumbra.CollectionManager.DefaultCollection.SetGmpFiles(); + Penumbra.CollectionManager.Default.SetGmpFiles(); break; case MetaManipulation.Type.Rsp: - Penumbra.CollectionManager.DefaultCollection.SetCmpFiles(); + Penumbra.CollectionManager.Default.SetCmpFiles(); break; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index e6d443ed..eea23033 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -1,8 +1,8 @@ using System; using System.Runtime.CompilerServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; @@ -104,7 +104,7 @@ public unsafe partial class PathResolver [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) => ResolvePathDetour( FindParent( drawObject, out var collection ) == null - ? Penumbra.CollectionManager.DefaultCollection + ? Penumbra.CollectionManager.Default : collection, path ); // Weapons have the characters DrawObject as a parent, @@ -123,14 +123,14 @@ public unsafe partial class PathResolver { var parent = FindParent( ( IntPtr )parentObject, out var collection ); return ResolvePathDetour( parent == null - ? Penumbra.CollectionManager.DefaultCollection + ? Penumbra.CollectionManager.Default : collection, path ); } } // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path ) + private IntPtr ResolvePathDetour( ModCollection2 collection, IntPtr path ) { if( path == IntPtr.Zero ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 9dc67a17..4c85380c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; @@ -39,27 +40,17 @@ public partial class PathResolver : IDisposable var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ); if( !nonDefault ) { - collection = Penumbra.CollectionManager.DefaultCollection; + collection = Penumbra.CollectionManager.Default; } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); - if( resolved == null ) - { - resolved = Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); - if( resolved == null ) - { - // We also need to handle defaulted materials against a non-default collection. - HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data ); - return true; - } - - collection = Penumbra.CollectionManager.ForcedCollection; - } + 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. - HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data ); + // We also need to handle defaulted materials against a non-default collection. + var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; + HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data ); return true; } @@ -113,14 +104,14 @@ public partial class PathResolver : IDisposable Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; } - private void OnCollectionChange( ModCollection? _1, ModCollection? _2, CollectionType type, string? characterName ) + private void OnCollectionChange( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? characterName ) { if( type != CollectionType.Character ) { return; } - if( Penumbra.CollectionManager.CharacterCollection.Count > 0 ) + if( Penumbra.CollectionManager.HasCharacterCollections ) { Enable(); } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index 6cfa47b8..ce838fe9 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerCmp : IDisposable { - public CmpFile? File = null; - public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new(); + public CmpFile? File = null; + public readonly Dictionary< RspManipulation, int > Manipulations = new(); public MetaManagerCmp() { } @@ -38,14 +38,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, Mod.Mod mod ) + public bool ApplyMod( RspManipulation m, int modIdx ) { #if USE_CMP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; File ??= new CmpFile(); return m.Apply( File ); #else diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 1dba4520..785d3d2e 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -16,7 +16,7 @@ public partial class MetaManager { public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 1]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, int > Manipulations = new(); public MetaManagerEqdp() { } @@ -50,14 +50,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, Mod.Mod mod ) + public bool ApplyMod( EqdpManipulation m, int modIdx ) { #if USE_EQDP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar return m.Apply( file ); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index dd7c63ff..92d8f3d8 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerEqp : IDisposable { - public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new(); + public ExpandedEqpFile? File = null; + public readonly Dictionary< EqpManipulation, int > Manipulations = new(); public MetaManagerEqp() { } @@ -38,14 +38,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) + public bool ApplyMod( EqpManipulation m, int modIdx ) { #if USE_EQP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; File ??= new ExpandedEqpFile(); return m.Apply( File ); #else diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index f53d145d..c6901f2f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -16,7 +16,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< EstManipulation, int > Manipulations = new(); public MetaManagerEst() { } @@ -49,14 +49,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, Mod.Mod mod ) + public bool ApplyMod( EstManipulation m, int modIdx ) { #if USE_EST - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var file = m.Slot switch { EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 8f43ac00..4bdee5c3 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerGmp : IDisposable { - public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new(); + public ExpandedGmpFile? File = null; + public readonly Dictionary< GmpManipulation, int > Manipulations = new(); public MetaManagerGmp() { } @@ -37,15 +37,11 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) + public bool ApplyMod( GmpManipulation m, int modIdx ) { #if USE_GMP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - - File ??= new ExpandedGmpFile(); + Manipulations[ m ] = modIdx; + File ??= new ExpandedGmpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 4e3d302a..2a6e3a37 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using System.Diagnostics; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -18,14 +16,14 @@ public partial class MetaManager { public readonly struct MetaManagerImc : IDisposable { - public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); + public readonly Dictionary< ImcManipulation, int > Manipulations = new(); - private readonly ModCollection _collection; - private static int _imcManagerCount; + private readonly ModCollection2 _collection; + private static int _imcManagerCount; - public MetaManagerImc( ModCollection collection ) + public MetaManagerImc( ModCollection2 collection ) { _collection = collection; SetupDelegate(); @@ -34,37 +32,43 @@ public partial class MetaManager [Conditional( "USE_IMC" )] public void SetFiles() { - if( _collection.Cache == null ) + if( !_collection.HasCache ) { return; } foreach( var path in Files.Keys ) { - _collection.Cache.ResolvedFiles[ path ] = CreateImcPath( path ); + _collection.ForceFile( path, CreateImcPath( path ) ); } } [Conditional( "USE_IMC" )] public void Reset() { - foreach( var (path, file) in Files ) + if( _collection.HasCache ) { - _collection.Cache?.ResolvedFiles.Remove( path ); - file.Reset(); + foreach( var (path, file) in Files ) + { + _collection.RemoveFile( path ); + file.Reset(); + } + } + else + { + foreach( var (_, file) in Files ) + { + file.Reset(); + } } Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + public bool ApplyMod( ImcManipulation m, int modIdx ) { #if USE_IMC - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var path = m.GamePath(); if( !Files.TryGetValue( path, out var file ) ) { @@ -78,9 +82,9 @@ public partial class MetaManager Files[ path ] = file; var fullPath = CreateImcPath( path ); - if( _collection.Cache != null ) + if( _collection.HasCache ) { - _collection.Cache.ResolvedFiles[ path ] = fullPath; + _collection.ForceFile( path, fullPath ); } return true; @@ -135,8 +139,8 @@ public partial class MetaManager PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) - && collection.Cache != null - && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( + && collection.HasCache + && collection.MetaCache!.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, @@ -152,9 +156,8 @@ public partial class MetaManager { // Only check imcs. if( resource->FileType != ResourceType.Imc - || resolveData is not ModCollection collection - || collection.Cache == null - || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) + || resolveData is not ModCollection2 { HasCache: true } collection + || !collection.MetaCache!.Imc.Files.TryGetValue( gamePath, out var file ) || !file.ChangesSinceLoad ) { return; diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index d0932ae4..0672d953 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,8 +1,8 @@ using System; using System.Runtime.CompilerServices; +using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -28,19 +28,19 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod ) + public bool TryGetValue( MetaManipulation manip, out int modIdx ) { - mod = manip.ManipulationType switch + modIdx = manip.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, - MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, - MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, - MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, - MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, - MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, + MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : -1, + MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : -1, + MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : -1, + MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : -1, + MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : -1, + MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : -1, _ => throw new ArgumentOutOfRangeException(), }; - return mod != null; + return modIdx != -1; } public int Count @@ -51,7 +51,7 @@ public partial class MetaManager : IDisposable + Est.Manipulations.Count + Eqp.Manipulations.Count; - public MetaManager( ModCollection collection ) + public MetaManager( ModCollection2 collection ) => Imc = new MetaManagerImc( collection ); public void SetFiles() @@ -84,16 +84,16 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + public bool ApplyMod( MetaManipulation m, int modIdx ) { return m.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), - MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), - MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), - MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), - MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), - MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), + MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, modIdx ), + MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, modIdx ), + MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, modIdx ), + MetaManipulation.Type.Est => Est.ApplyMod( m.Est, modIdx ), + MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, modIdx ), + MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, modIdx ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 2f4fd5a7..742b1d5d 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json.Linq; +using Penumbra.Collections; using Penumbra.Mod; using Penumbra.Mods; @@ -33,8 +34,8 @@ public static class MigrateConfiguration return; } - var defaultCollection = new ModCollection(); - var defaultCollectionFile = defaultCollection.FileName(); + var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + var defaultCollectionFile = defaultCollection.FileName; if( defaultCollectionFile.Exists ) { return; @@ -46,6 +47,7 @@ public static class MigrateConfiguration var data = JArray.Parse( text ); var maxPriority = 0; + var dict = new Dictionary< string, ModSettings >(); foreach( var setting in data.Cast< JObject >() ) { var modName = ( string )setting[ "FolderName" ]!; @@ -54,24 +56,25 @@ public static class MigrateConfiguration var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); - var save = new ModSettings() + dict[ modName ] = new ModSettings() { Enabled = enabled, Priority = priority, Settings = settings!, }; - defaultCollection.Settings.Add( modName, save ); + ; maxPriority = Math.Max( maxPriority, priority ); } if( !config.InvertModListOrder ) { - foreach( var setting in defaultCollection.Settings.Values ) + foreach( var setting in dict.Values ) { setting.Priority = maxPriority - setting.Priority; } } + defaultCollection = ModCollection2.MigrateFromV0( ModCollection2.DefaultCollection, dict ); defaultCollection.Save(); } catch( Exception e ) diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 6fc6486c..80e7451c 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Mod; -public struct ModCache2 +public struct ConflictCache { public readonly struct ModCacheStruct : IComparable< ModCacheStruct > { diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 38c316e2..654c2174 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -9,6 +9,7 @@ using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Mod; @@ -60,8 +61,8 @@ public class ModCleanup private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { - var idx = Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[idx]; + var idx = Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[ idx ]; newMod.Move( newSortOrder ); newMod.ComputeChangedItems(); ModFileSystem.InvokeChange(); @@ -509,21 +510,23 @@ public class ModCleanup } } - if( option.OptionFiles.Any() ) + if( option.OptionFiles.Count > 0 ) { group.Options.Add( option ); } } - if( group.Options.Any() ) + if( group.Options.Count > 0 ) { meta.Groups.Add( groupDir.Name, group ); } } - foreach( var collection in Penumbra.CollectionManager.Collections ) + // TODO + var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); + foreach( var collection in Penumbra.CollectionManager ) { - collection.UpdateSetting( baseDir, meta, true ); + collection.Settings[ idx ]?.FixInvalidSettings( meta ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs deleted file mode 100644 index a00b01be..00000000 --- a/Penumbra/Mods/CollectionManager.cs +++ /dev/null @@ -1,574 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > -{ - private readonly ModManager _modManager; - - private readonly List< ModCollection2 > _collections = new(); - - public ModCollection2 this[ int idx ] - => _collections[ idx ]; - - public ModCollection2? this[ string name ] - => ByName( name, out var c ) ? c : null; - - public ModCollection2 Default - => this[ ModCollection2.DefaultCollection ]!; - - public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) - => _collections.FindFirst( c => c.Name == name, out collection ); - - public IEnumerator< ModCollection2 > GetEnumerator() - => _collections.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public CollectionManager2( ModManager manager ) - { - _modManager = manager; - - //_modManager.ModsRediscovered += OnModsRediscovered; - //_modManager.ModChange += OnModChanged; - ReadCollections(); - //LoadConfigCollections( Penumbra.Config ); - } - - public void Dispose() - { } - - private void AddDefaultCollection() - { - if( this[ ModCollection.DefaultCollection ] != null ) - { - return; - } - - var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); - defaultCollection.Save(); - _collections.Add( defaultCollection ); - } - - private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) - { - foreach( var (collection, inheritance) in this.Zip( inheritances ) ) - { - var changes = false; - foreach( var subCollectionName in inheritance ) - { - if( !ByName( subCollectionName, out var subCollection ) ) - { - changes = true; - PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); - } - else if( !collection.AddInheritance( subCollection ) ) - { - changes = true; - PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); - } - } - - foreach( var (setting, mod) in collection.Settings.Zip( Penumbra.ModManager.Mods ).Where( s => s.First != null ) ) - { - changes |= setting!.FixInvalidSettings( mod.Meta ); - } - - if( changes ) - { - collection.Save(); - } - } - } - - private void ReadCollections() - { - var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); - var inheritances = new List< IReadOnlyList< string > >(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection2.LoadFromFile( file, out var inheritance ); - if( collection == null || collection.Name.Length == 0 ) - { - continue; - } - - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( this[ collection.Name ] != null ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - inheritances.Add( inheritance ); - _collections.Add( collection ); - } - } - } - - AddDefaultCollection(); - ApplyInheritancesAndFixSettings( inheritances ); - } -} - -public enum CollectionType : byte -{ - Inactive, - Default, - Forced, - Character, - Current, -} - -public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, CollectionType type, - string? characterName = null ); - -// Contains all collections and respective functions, as well as the collection settings. -public sealed class CollectionManager : IDisposable -{ - private readonly ModManager _manager; - - public List< ModCollection > Collections { get; } = new(); - public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); - - public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; - public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty; - public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; - - public bool IsActive( ModCollection collection ) - => ReferenceEquals( collection, DefaultCollection ) || ReferenceEquals( collection, ForcedCollection ); - - public ModCollection Default - => ByName( ModCollection.DefaultCollection )!; - - public ModCollection? ByName( string name ) - => name.Length > 0 - ? Collections.Find( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ) ) - : ModCollection.Empty; - - public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - { - if( name.Length > 0 ) - { - return Collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); - } - - collection = ModCollection.Empty; - return true; - } - - // Is invoked after the collections actually changed. - public event CollectionChangeDelegate? CollectionChanged; - - public CollectionManager( ModManager manager ) - { - _manager = manager; - - _manager.ModsRediscovered += OnModsRediscovered; - _manager.ModChange += OnModChanged; - ReadCollections(); - LoadConfigCollections( Penumbra.Config ); - } - - public void Dispose() - { - _manager.ModsRediscovered -= OnModsRediscovered; - _manager.ModChange -= OnModChanged; - } - - private void OnModsRediscovered() - { - RecreateCaches(); - DefaultCollection.SetFiles(); - } - - private void OnModChanged( ModChangeType type, int idx, ModData mod ) - { - switch( type ) - { - case ModChangeType.Added: - foreach( var collection in Collections ) - { - collection.AddMod( mod ); - } - - break; - case ModChangeType.Removed: - RemoveModFromCaches( mod.BasePath ); - break; - case ModChangeType.Changed: - // TODO - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } - } - - public void CreateNecessaryCaches() - { - AddCache( DefaultCollection ); - AddCache( ForcedCollection ); - foreach( var (_, collection) in CharacterCollection ) - { - AddCache( collection ); - } - } - - public void RecreateCaches() - { - foreach( var collection in Collections.Where( c => c.Cache != null ) ) - { - collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - CreateNecessaryCaches(); - } - - public void RemoveModFromCaches( DirectoryInfo modDir ) - { - foreach( var collection in Collections ) - { - collection.Cache?.RemoveMod( modDir ); - } - } - - internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta ) - { - foreach( var collection in Collections ) - { - if( metaChanges ) - { - collection.UpdateSetting( mod ); - } - - if( fileChanges.HasFlag( ResourceChange.Files ) - && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) - && settings.Enabled ) - { - collection.Cache?.CalculateEffectiveFileList(); - } - - if( reloadMeta ) - { - collection.Cache?.UpdateMetaManipulations(); - } - } - - if( reloadMeta && DefaultCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) - { - Penumbra.ResidentResources.Reload(); - } - } - - public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 || Collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) - { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = new ModCollection( name, settings ); - Collections.Add( newCollection ); - newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); - SetCollection( newCollection, CollectionType.Current ); - return true; - } - - public bool RemoveCollection( string name ) - { - if( name == ModCollection.DefaultCollection ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - var idx = Collections.IndexOf( c => c.Name == name ); - if( idx < 0 ) - { - return false; - } - - var collection = Collections[ idx ]; - - if( CurrentCollection == collection ) - { - SetCollection( Default, CollectionType.Current ); - } - - if( ForcedCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Forced ); - } - - if( DefaultCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Default ); - } - - foreach( var (characterName, characterCollection) in CharacterCollection.ToArray() ) - { - if( characterCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Character, characterName ); - } - } - - collection.Delete(); - Collections.RemoveAt( idx ); - CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); - return true; - } - - private void AddCache( ModCollection collection ) - { - if( collection.Cache == null && collection.Name != string.Empty ) - { - collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - } - - private void RemoveCache( ModCollection collection ) - { - if( collection.Name != ForcedCollection.Name - && collection.Name != CurrentCollection.Name - && collection.Name != DefaultCollection.Name - && CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) ) - { - collection.ClearCache(); - } - } - - public void SetCollection( ModCollection newCollection, CollectionType type, string? characterName = null ) - { - var oldCollection = type switch - { - CollectionType.Default => DefaultCollection, - CollectionType.Forced => ForcedCollection, - CollectionType.Current => CurrentCollection, - CollectionType.Character => characterName?.Length > 0 - ? CharacterCollection.TryGetValue( characterName, out var c ) - ? c - : ModCollection.Empty - : null, - _ => null, - }; - - if( oldCollection == null || newCollection.Name == oldCollection.Name ) - { - return; - } - - AddCache( newCollection ); - RemoveCache( oldCollection ); - switch( type ) - { - case CollectionType.Default: - DefaultCollection = newCollection; - Penumbra.Config.DefaultCollection = newCollection.Name; - Penumbra.ResidentResources.Reload(); - DefaultCollection.SetFiles(); - break; - case CollectionType.Forced: - ForcedCollection = newCollection; - Penumbra.Config.ForcedCollection = newCollection.Name; - Penumbra.ResidentResources.Reload(); - break; - case CollectionType.Current: - CurrentCollection = newCollection; - Penumbra.Config.CurrentCollection = newCollection.Name; - break; - case CollectionType.Character: - CharacterCollection[ characterName! ] = newCollection; - Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; - break; - } - - CollectionChanged?.Invoke( oldCollection, newCollection, type, characterName ); - - Penumbra.Config.Save(); - } - - public bool CreateCharacterCollection( string characterName ) - { - if( CharacterCollection.ContainsKey( characterName ) ) - { - return false; - } - - CharacterCollection[ characterName ] = ModCollection.Empty; - Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; - Penumbra.Config.Save(); - CollectionChanged?.Invoke( null, ModCollection.Empty, CollectionType.Character, characterName ); - return true; - } - - public void RemoveCharacterCollection( string characterName ) - { - if( CharacterCollection.TryGetValue( characterName, out var collection ) ) - { - RemoveCache( collection ); - CharacterCollection.Remove( characterName ); - CollectionChanged?.Invoke( collection, null, CollectionType.Character, characterName ); - } - - if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) - { - Penumbra.Config.Save(); - } - } - - private bool LoadCurrentCollection( Configuration config ) - { - if( ByName( config.CurrentCollection, out var currentCollection ) ) - { - CurrentCollection = currentCollection; - AddCache( CurrentCollection ); - return false; - } - - PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); - CurrentCollection = Default; - if( CurrentCollection.Cache == null ) - { - CurrentCollection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - config.CurrentCollection = ModCollection.DefaultCollection; - return true; - } - - private bool LoadForcedCollection( Configuration config ) - { - if( config.ForcedCollection.Length == 0 ) - { - ForcedCollection = ModCollection.Empty; - return false; - } - - if( ByName( config.ForcedCollection, out var forcedCollection ) ) - { - ForcedCollection = forcedCollection; - AddCache( ForcedCollection ); - return false; - } - - PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." ); - ForcedCollection = ModCollection.Empty; - config.ForcedCollection = string.Empty; - return true; - } - - private bool LoadDefaultCollection( Configuration config ) - { - if( config.DefaultCollection.Length == 0 ) - { - DefaultCollection = ModCollection.Empty; - return false; - } - - if( ByName( config.DefaultCollection, out var defaultCollection ) ) - { - DefaultCollection = defaultCollection; - AddCache( DefaultCollection ); - return false; - } - - PluginLog.Error( $"Last choice of DefaultCollection {config.DefaultCollection} is not available, reset to None." ); - DefaultCollection = ModCollection.Empty; - config.DefaultCollection = string.Empty; - return true; - } - - private bool LoadCharacterCollections( Configuration config ) - { - var configChanged = false; - foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) - { - if( collectionName.Length == 0 ) - { - CharacterCollection.Add( player, ModCollection.Empty ); - } - else if( ByName( collectionName, out var charCollection ) ) - { - AddCache( charCollection ); - CharacterCollection.Add( player, charCollection ); - } - else - { - PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); - CharacterCollection.Add( player, ModCollection.Empty ); - config.CharacterCollections[ player ] = string.Empty; - configChanged = true; - } - } - - return configChanged; - } - - private void LoadConfigCollections( Configuration config ) - { - var configChanged = LoadCurrentCollection( config ); - configChanged |= LoadDefaultCollection( config ); - configChanged |= LoadForcedCollection( config ); - configChanged |= LoadCharacterCollections( config ); - - if( configChanged ) - { - config.Save(); - } - } - - private void ReadCollections() - { - var collectionDir = ModCollection.CollectionDir(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection.LoadFromFile( file ); - if( collection == null || collection.Name == string.Empty ) - { - continue; - } - - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( ByName( collection.Name ) != null ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - Collections.Add( collection ); - } - } - } - - if( ByName( ModCollection.DefaultCollection ) == null ) - { - var defaultCollection = new ModCollection(); - defaultCollection.Save(); - Collections.Add( defaultCollection ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs deleted file mode 100644 index 9a00250b..00000000 --- a/Penumbra/Mods/ModCollection.cs +++ /dev/null @@ -1,523 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using Dalamud.Logging; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manager; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class ModCollection2 -{ - public const int CurrentVersion = 1; - public const string DefaultCollection = "Default"; - - public string Name { get; private init; } - public int Version { get; private set; } - - private readonly List< ModSettings? > _settings; - - public IReadOnlyList< ModSettings? > Settings - => _settings; - - public IEnumerable< ModSettings? > ActualSettings - => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); - - private readonly Dictionary< string, ModSettings > _unusedSettings; - - private ModCollection2( string name, ModCollection2 duplicate ) - { - Name = name; - Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); - _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); - _inheritance = duplicate._inheritance.ToList(); - ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; - } - - private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) - { - Name = name; - Version = version; - _unusedSettings = allSettings; - _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) - { - var modName = Penumbra.ModManager[ i ].BasePath.Name; - if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) - { - _unusedSettings.Remove( modName ); - _settings[ i ] = settings; - } - } - - Migration.Migrate( this ); - ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; - } - - public static ModCollection2 CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); - - public ModCollection2 Duplicate( string name ) - => new(name, this); - - private void CleanUnavailableSettings() - { - var any = _unusedSettings.Count > 0; - _unusedSettings.Clear(); - if( any ) - { - Save(); - } - } - - public void AddMod( ModData mod ) - { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - _settings.Add( settings ); - _unusedSettings.Remove( mod.BasePath.Name ); - } - else - { - _settings.Add( null ); - } - } - - public void RemoveMod( ModData mod, int idx ) - { - var settings = _settings[ idx ]; - if( settings != null ) - { - _unusedSettings.Add( mod.BasePath.Name, settings ); - } - - _settings.RemoveAt( idx ); - } - - public static string CollectionDirectory - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); - - private FileInfo FileName - => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); - - public void Save() - { - try - { - var file = FileName; - file.Directory?.Create(); - using var s = file.Open( FileMode.Truncate ); - using var w = new StreamWriter( s, Encoding.UTF8 ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); - j.WriteStartObject(); - j.WritePropertyName( nameof( Version ) ); - j.WriteValue( Version ); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( Name ); - j.WritePropertyName( nameof( Settings ) ); - j.WriteStartObject(); - for( var i = 0; i < _settings.Count; ++i ) - { - var settings = _settings[ i ]; - if( settings != null ) - { - j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, settings ); - } - } - - foreach( var settings in _unusedSettings ) - { - j.WritePropertyName( settings.Key ); - x.Serialize( j, settings.Value ); - } - - j.WriteEndObject(); - j.WritePropertyName( nameof( Inheritance ) ); - x.Serialize( j, Inheritance ); - j.WriteEndObject(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public void Delete() - { - var file = FileName; - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); - } - } - } - - public static ModCollection2? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) - { - inheritance = Array.Empty< string >(); - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); - var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; - var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() - ?? new Dictionary< string, ModSettings >(); - inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); - - return new ModCollection2( name, version, settings ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } -} - -// A ModCollection is a named set of ModSettings to all of the users' installed mods. -// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. -// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. -// Active ModCollections build a cache of currently relevant data. -public class ModCollection -{ - public const string DefaultCollection = "Default"; - - public string Name { get; set; } - - public Dictionary< string, ModSettings > Settings { get; } - - public ModCollection() - { - Name = DefaultCollection; - Settings = new Dictionary< string, ModSettings >(); - } - - public ModCollection( string name, Dictionary< string, ModSettings > settings ) - { - Name = name; - Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); - } - - public Mod.Mod GetMod( ModData mod ) - { - if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) ) - { - return ret; - } - - if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - return new Mod.Mod( settings, mod ); - } - - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); - Save(); - return new Mod.Mod( newSettings, mod ); - } - - private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) - { - var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); - - foreach( var s in removeList ) - { - Settings.Remove( s.Key ); - } - - return removeList.Length > 0; - } - - public void CreateCache( IEnumerable< ModData > data ) - { - Cache = new ModCollectionCache( this ); - var changedSettings = false; - foreach( var mod in data ) - { - if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - Cache.AddMod( settings, mod, false ); - } - else - { - changedSettings = true; - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); - Cache.AddMod( newSettings, mod, false ); - } - } - - if( changedSettings ) - { - Save(); - } - - CalculateEffectiveFileList( true, false ); - } - - public void ClearCache() - => Cache = null; - - public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear ) - { - if( !Settings.TryGetValue( modPath.Name, out var settings ) ) - { - return; - } - - if( clear ) - { - settings.Settings.Clear(); - } - - if( settings.FixInvalidSettings( meta ) ) - { - Save(); - } - } - - public void UpdateSetting( ModData mod ) - => UpdateSetting( mod.BasePath, mod.Meta, false ); - - public void UpdateSettings( bool forceSave ) - { - if( Cache == null ) - { - return; - } - - var changes = false; - foreach( var mod in Cache.AvailableMods.Values ) - { - changes |= mod.FixSettings(); - } - - if( forceSave || changes ) - { - Save(); - } - } - - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) - { - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); - Cache ??= new ModCollectionCache( this ); - UpdateSettings( false ); - Cache.CalculateEffectiveFileList(); - if( withMetaManipulations ) - { - Cache.UpdateMetaManipulations(); - } - - if( reloadResident ) - { - Penumbra.ResidentResources.Reload(); - } - } - - - [JsonIgnore] - public ModCollectionCache? Cache { get; private set; } - - public static ModCollection? LoadFromFile( FileInfo file ) - { - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) ); - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } - - private void SaveToFile( FileInfo file ) - { - try - { - File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" ); - } - } - - public static DirectoryInfo CollectionDir() - => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" )); - - private static FileInfo FileName( DirectoryInfo collectionDir, string name ) - => new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" )); - - public FileInfo FileName() - => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - $"{Name.RemoveInvalidPathSymbols()}.json" )); - - public void Save() - { - try - { - var dir = CollectionDir(); - dir.Create(); - var file = FileName( dir, Name ); - SaveToFile( file ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public static ModCollection? Load( string name ) - { - var file = FileName( CollectionDir(), name ); - return file.Exists ? LoadFromFile( file ) : null; - } - - public void Delete() - { - var file = FileName( CollectionDir(), Name ); - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" ); - } - } - } - - public void AddMod( ModData data ) - { - if( Cache == null ) - { - return; - } - - Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings ) - ? settings - : ModSettings.DefaultSettings( data.Meta ), - data ); - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); - - - [Conditional( "USE_EQP" )] - public void SetEqpFiles() - { - if( Cache == null ) - { - MetaManager.MetaManagerEqp.ResetFiles(); - } - else - { - Cache.MetaManipulations.Eqp.SetFiles(); - } - } - - [Conditional( "USE_EQDP" )] - public void SetEqdpFiles() - { - if( Cache == null ) - { - MetaManager.MetaManagerEqdp.ResetFiles(); - } - else - { - Cache.MetaManipulations.Eqdp.SetFiles(); - } - } - - [Conditional( "USE_GMP" )] - public void SetGmpFiles() - { - if( Cache == null ) - { - MetaManager.MetaManagerGmp.ResetFiles(); - } - else - { - Cache.MetaManipulations.Gmp.SetFiles(); - } - } - - [Conditional( "USE_EST" )] - public void SetEstFiles() - { - if( Cache == null ) - { - MetaManager.MetaManagerEst.ResetFiles(); - } - else - { - Cache.MetaManipulations.Est.SetFiles(); - } - } - - [Conditional( "USE_CMP" )] - public void SetCmpFiles() - { - if( Cache == null ) - { - MetaManager.MetaManagerCmp.ResetFiles(); - } - else - { - Cache.MetaManipulations.Cmp.SetFiles(); - } - } - - public void SetFiles() - { - if( Cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - Cache.MetaManipulations.SetFiles(); - } - } - - public static readonly ModCollection Empty = new() { Name = "" }; -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs deleted file mode 100644 index d7d56224..00000000 --- a/Penumbra/Mods/ModCollectionCache.cs +++ /dev/null @@ -1,662 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manager; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// The ModCollectionCache contains all required temporary data to use a collection. -// It will only be setup if a collection gets activated in any way. -public class ModCollectionCache2 -{ - // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); - private static readonly List< ModSettings? > ResolvedSettings = new(128); - - private readonly ModCollection2 _collection; - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly MetaManager MetaManipulations; - private ModCache2 _cache; - - public IReadOnlyDictionary< string, object? > ChangedItems - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - public ModCollectionCache2( ModCollection2 collection ) - => _collection = collection; - - //MetaManipulations = new MetaManager( collection ); - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - private void ClearStorageAndPrepare() - { - ResolvedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); - _changedItems.Clear(); - _cache.ClearFileConflicts(); - - ResolvedSettings.Clear(); - ResolvedSettings.AddRange( _collection.ActualSettings ); - } - - public void CalculateEffectiveFileList() - { - ClearStorageAndPrepare(); - - for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) - { - if( ResolvedSettings[ i ]?.Enabled == true ) - { - AddFiles( i ); - AddSwaps( i ); - } - } - - AddMetaFiles(); - } - - private void SetChangedItems() - { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) - { - return; - } - - try - { - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) - { - identifier.Identify( _changedItems, resolved.ToGamePath() ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Unknown Error:\n{e}" ); - } - } - - - private void AddFiles( int idx ) - { - var mod = Penumbra.ModManager.Mods[ idx ]; - ResetFileSeen( mod.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - foreach( var group in mod.Meta.Groups.Values.Reverse() ) - { - switch( group.SelectionType ) - { - case SelectType.Single: - AddFilesForSingle( group, mod, idx ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod, idx ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod, idx ); - } - - private static bool FilterFile( Utf8GamePath gamePath ) - { - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - if( !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) - { - return true; - } - - return false; - } - - - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) - { - RegisteredFiles.Add( gamePath, modIdx ); - ResolvedFiles[ gamePath ] = file; - } - else - { - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - _cache.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); - if( priority > oldPriority ) - { - ResolvedFiles[ gamePath ] = file; - RegisteredFiles[ gamePath ] = modIdx; - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - var fullPath = new FullPath( mod.BasePath, file ); - var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) - { - AddFile( modIdx, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( ModData mod, int modIdx ) - { - for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.BasePath, out var gamePath ) ) - { - AddFile( modIdx, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); - } - } - else - { - MissingFiles.Add( file ); - } - } - } - - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - private void AddSwaps( int modIdx ) - { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) - { - AddFile( modIdx, gamePath, swapPath ); - } - } - - // TODO Manipulations - public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath ); -} - -// The ModCollectionCache contains all required temporary data to use a collection. -// It will only be setup if a collection gets activated in any way. -public class ModCollectionCache -{ - // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256); - - public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly MetaManager MetaManipulations; - - public IReadOnlyDictionary< string, object? > ChangedItems - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - public ModCollectionCache( ModCollection collection ) - => MetaManipulations = new MetaManager( collection ); - - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - public void CalculateEffectiveFileList() - { - ResolvedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); - _changedItems.Clear(); - - foreach( var mod in AvailableMods.Values - .Where( m => m.Settings.Enabled ) - .OrderByDescending( m => m.Settings.Priority ) ) - { - mod.Cache.ClearFileConflicts(); - AddFiles( mod ); - AddSwaps( mod ); - } - - AddMetaFiles(); - } - - private void SetChangedItems() - { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) - { - return; - } - - try - { - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) - { - identifier.Identify( _changedItems, resolved.ToGamePath() ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Unknown Error:\n{e}" ); - } - } - - - private void AddFiles( Mod.Mod mod ) - { - ResetFileSeen( mod.Data.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - foreach( var group in mod.Data.Meta.Groups.Values.Reverse() ) - { - switch( group.SelectionType ) - { - case SelectType.Single: - AddFilesForSingle( group, mod ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod ); - } - - private static bool FilterFile( Utf8GamePath gamePath ) - { - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - if( !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) - { - return true; - } - - return false; - } - - - private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) - { - RegisteredFiles.Add( gamePath, mod ); - ResolvedFiles[ gamePath ] = file; - } - else - { - mod.Cache.AddConflict( oldMod, gamePath ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, gamePath ); - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - var fullPath = new FullPath( mod.Data.BasePath, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Data.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) - { - AddFile( mod, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - - if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - - if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( Mod.Mod mod ) - { - for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Data.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) - { - AddFile( mod, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.Data.BasePath.FullName} to GamePath." ); - } - } - else - { - MissingFiles.Add( file ); - } - } - } - - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - private void AddSwaps( Mod.Mod mod ) - { - foreach( var (key, value) in mod.Data.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) - { - if( !RegisteredFiles.TryGetValue( key, out var oldMod ) ) - { - RegisteredFiles.Add( key, mod ); - ResolvedFiles.Add( key, value ); - } - else - { - mod.Cache.AddConflict( oldMod, key ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, key ); - } - } - } - } - - private void AddManipulations( Mod.Mod mod ) - { - foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) - { - if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - else - { - mod.Cache.AddConflict( oldMod!, manip ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod!.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, manip ); - } - } - } - } - - public void UpdateMetaManipulations() - { - MetaManipulations.Reset(); - - foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) - { - mod.Cache.ClearMetaConflicts(); - AddManipulations( mod ); - } - } - - public void RemoveMod( DirectoryInfo basePath ) - { - if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) ) - { - return; - } - - AvailableMods.Remove( basePath.Name ); - if( !mod.Settings.Enabled ) - { - return; - } - - CalculateEffectiveFileList(); - if( mod.Data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - - public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) - { - if( AvailableMods.ContainsKey( data.BasePath.Name ) ) - { - return; - } - - AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data ); - - if( !updateFileList || !settings.Enabled ) - { - return; - } - - CalculateEffectiveFileList(); - if( data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - - public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath ); -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index a23a9a32..b88a9bca 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -263,7 +263,7 @@ public class ModManager : IEnumerable< ModData > mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); } - Penumbra.CollectionManager.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); // TODO + // TODO: more specific mod changes? ModChange?.Invoke( ModChangeType.Changed, idx, mod ); return true; } @@ -271,10 +271,6 @@ public class ModManager : IEnumerable< ModData > public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - { - var ret = Penumbra.CollectionManager.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - ret ??= Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - return ret; - } + public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 93c96c86..5083075a 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.IO; using Dalamud.Logging; using Penumbra.Mod; +using Penumbra.Util; namespace Penumbra.Mods; @@ -82,20 +83,13 @@ public static class ModManagerEditExtensions manager.Config.Save(); } - foreach( var collection in Penumbra.CollectionManager.Collections ) + var idx = manager.Mods.IndexOf( mod ); + foreach( var collection in Penumbra.CollectionManager ) { - if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) + if( collection.Settings[ idx ] != null ) { - collection.Settings[ newDir.Name ] = settings; - collection.Settings.Remove( oldBasePath.Name ); collection.Save(); } - - if( collection.Cache != null ) - { - collection.Cache.RemoveMod( newDir ); - collection.AddMod( mod ); - } } return true; @@ -140,9 +134,13 @@ public static class ModManagerEditExtensions mod.SaveMeta(); - foreach( var collection in Penumbra.CollectionManager.Collections ) + // TODO to indices + var idx = Penumbra.ModManager.Mods.IndexOf( mod ); + + foreach( var collection in Penumbra.CollectionManager ) { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + var settings = collection.Settings[ idx ]; + if( settings == null ) { continue; } @@ -176,9 +174,11 @@ public static class ModManagerEditExtensions return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); } - foreach( var collection in Penumbra.CollectionManager.Collections ) + var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO + foreach( var collection in Penumbra.CollectionManager ) { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + var settings = collection.Settings[ idx ]; + if( settings == null ) { continue; } @@ -199,10 +199,10 @@ public static class ModManagerEditExtensions { settings.Settings[ group.GroupName ] = newSetting; collection.Save(); - if( collection.Cache != null && settings.Enabled ) + if( collection.HasCache && settings.Enabled ) { collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - Penumbra.CollectionManager.IsActive( collection ) ); + Penumbra.CollectionManager.Default == collection ); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e2116f65..70535479 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -12,7 +13,7 @@ using Penumbra.Interop; using Penumbra.Mods; using Penumbra.UI; using Penumbra.Util; -using System.Linq; +using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; @@ -34,7 +35,7 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; + public static CollectionManager2 CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } @@ -67,7 +68,7 @@ public class Penumbra : IDalamudPlugin ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new ModManager(); ModManager.DiscoverMods(); - CollectionManager = new CollectionManager( ModManager ); + CollectionManager = new CollectionManager2( ModManager ); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -110,10 +111,15 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - if (CollectionManager.CharacterCollection.Count > 0) + if( CollectionManager.HasCharacterCollections ) + { PathResolver.Enable(); + } ResidentResources.Reload(); + //var c = ModCollection2.LoadFromFile( new FileInfo(@"C:\Users\Ozy\AppData\Roaming\XIVLauncher\pluginConfigs\Penumbra\collections\Rayla.json"), + // out var inheritance ); + //c?.Save(); } public bool Enable() @@ -217,10 +223,9 @@ public class Penumbra : IDalamudPlugin type = type.ToLowerInvariant(); collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) - ? ModCollection.Empty - : CollectionManager.Collections.FirstOrDefault( c - => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); + var collection = string.Equals( collectionName, ModCollection2.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) + ? ModCollection2.Empty + : CollectionManager[collectionName]; if( collection == null ) { Dalamud.Chat.Print( $"The collection {collection} does not exist." ); @@ -230,7 +235,7 @@ public class Penumbra : IDalamudPlugin switch( type ) { case "default": - if( collection == CollectionManager.DefaultCollection ) + if( collection == CollectionManager.Default ) { Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); return false; @@ -240,20 +245,9 @@ public class Penumbra : IDalamudPlugin Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; - case "forced": - if( collection == CollectionManager.ForcedCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); - return false; - } - - CollectionManager.SetCollection( collection, CollectionType.Forced ); - Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); - SettingsInterface.ResetForcedCollection(); - return true; default: Dalamud.Chat.Print( - "Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} " ); + "Second command argument is not default, the correct command format is: /penumbra collection default " ); return false; } } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 4bcb4224..2d94eaef 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -6,6 +6,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; +using Penumbra.Collections; using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; @@ -21,7 +22,7 @@ public partial class SettingsInterface private readonly Selector _selector; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; + private ModCollection2[] _collections = null!; private int _currentCollectionIndex; private int _currentForcedIndex; private int _currentDefaultIndex; @@ -31,14 +32,14 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = Penumbra.CollectionManager.Collections.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.CollectionManager.Prepend( ModCollection2.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); } - private int GetIndex( ModCollection collection ) + private int GetIndex( ModCollection2 collection ) { var ret = _collections.IndexOf( c => c.Name == collection.Name ); if( ret < 0 ) @@ -175,7 +176,7 @@ public partial class SettingsInterface } } - public void SetCurrentCollection( ModCollection collection, bool force = false ) + public void SetCurrentCollection( ModCollection2 collection, bool force = false ) { var idx = Array.IndexOf( _collections, collection ) - 1; if( idx >= 0 ) diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 672e5a8c..cbf17b9b 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Mods; @@ -99,9 +100,9 @@ public partial class SettingsInterface return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); } - private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + private void DrawFilteredRows( ModCollection2 active ) { - void DrawFileLines( ModCollectionCache cache ) + void DrawFileLines( ModCollection2.Cache cache ) { foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) { @@ -116,15 +117,7 @@ public partial class SettingsInterface //} } - if( active != null ) - { - DrawFileLines( active ); - } - - if( forced != null ) - { - DrawFileLines( forced ); - } + DrawFileLines( active ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index a87a8b22..fd0a0951 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -151,7 +151,7 @@ namespace Penumbra.UI { foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) { - var mod = Penumbra.CollectionManager.CurrentCollection.GetMod( modData ); + var mod = Penumbra.CollectionManager.Current.GetMod( modData ); _modsInOrder.Add( mod ); _visibleMods.Add( CheckFilters( mod ) ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 3374a522..1ae71c8b 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -8,6 +8,7 @@ using System.Windows.Forms.VisualStyles; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; +using Penumbra.Collections; using Penumbra.Importer; using Penumbra.Mod; using Penumbra.Mods; @@ -606,10 +607,10 @@ public partial class SettingsInterface Cache = new ModListCache( Penumbra.ModManager, newMods ); } - private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) + private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection2 collection ) { - if( collection == ModCollection.Empty - || collection == Penumbra.CollectionManager.CurrentCollection ) + if( collection == ModCollection2.Empty + || collection == Penumbra.CollectionManager.Current ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); @@ -632,16 +633,13 @@ public partial class SettingsInterface var comboSize = size * ImGui.GetIO().FontGlobalScale; var offset = comboSize + textSize; - var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth() - - offset - - SelectorPanelWidth * _selectorScalingFactor - - 4 * ImGui.GetStyle().ItemSpacing.X ) - / 2, 5f ); + var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth() + - offset + - SelectorPanelWidth * _selectorScalingFactor + - 3 * ImGui.GetStyle().ItemSpacing.X, 5f ); ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.DefaultCollection ); + DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default ); - ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.CollectionManager.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize ); From ac70f8db89637fdaf21c975d05dcca25e72734e0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Mar 2022 23:28:52 +0100 Subject: [PATCH 0124/2451] tmp2 --- Penumbra/Api/PenumbraApi.cs | 5 +- .../Collections/CollectionManager.Active.cs | 486 +++++++-------- Penumbra/Collections/CollectionManager.cs | 375 ++++++------ .../ConflictCache.cs} | 59 +- Penumbra/Collections/ModCollection.Cache.cs | 75 ++- Penumbra/Collections/ModCollection.Changes.cs | 30 +- .../Collections/ModCollection.Inheritance.cs | 36 +- .../Collections/ModCollection.Migration.cs | 6 +- Penumbra/Collections/ModCollection.cs | 43 +- .../Loader/ResourceLoader.Replacement.cs | 3 +- .../Interop/Resolver/PathResolver.Data.cs | 14 +- .../Interop/Resolver/PathResolver.Material.cs | 6 +- .../Interop/Resolver/PathResolver.Meta.cs | 8 +- .../Interop/Resolver/PathResolver.Resolve.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 6 +- Penumbra/Meta/Manager/MetaManager.cs | 2 +- Penumbra/MigrateConfiguration.cs | 4 +- Penumbra/Mod/FullMod.cs | 31 + Penumbra/Mod/Mod.SortOrder.cs | 45 ++ Penumbra/Mod/Mod.cs | 97 ++- Penumbra/Mod/ModCleanup.cs | 14 +- Penumbra/Mod/ModData.cs | 135 ----- Penumbra/Mod/ModManager.cs | 286 +++++++++ Penumbra/Mods/ModFileSystem.cs | 32 +- Penumbra/Mods/ModFolder.cs | 20 +- Penumbra/Mods/ModManager.Directory.cs | 4 +- Penumbra/Mods/ModManager.cs | 276 --------- Penumbra/Mods/ModManagerEditExtensions.cs | 22 +- Penumbra/Penumbra.cs | 17 +- Penumbra/UI/MenuTabs/TabChangedItems.cs | 10 +- Penumbra/UI/MenuTabs/TabCollections.cs | 81 +-- Penumbra/UI/MenuTabs/TabDebug.cs | 16 +- Penumbra/UI/MenuTabs/TabEffective.cs | 105 ++-- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 572 +++++++++--------- .../TabInstalled/TabInstalledDetails.cs | 69 ++- .../TabInstalled/TabInstalledDetailsEdit.cs | 7 +- .../TabInstalled/TabInstalledModPanel.cs | 17 +- .../TabInstalled/TabInstalledSelector.cs | 24 +- Penumbra/UI/SettingsInterface.cs | 20 - Penumbra/Util/ModelChanger.cs | 2 +- 41 files changed, 1546 insertions(+), 1520 deletions(-) rename Penumbra/{Mod/ModCache.cs => Collections/ConflictCache.cs} (65%) create mode 100644 Penumbra/Mod/FullMod.cs create mode 100644 Penumbra/Mod/Mod.SortOrder.cs delete mode 100644 Penumbra/Mod/ModData.cs create mode 100644 Penumbra/Mod/ModManager.cs delete mode 100644 Penumbra/Mods/ModManager.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 6a373d14..7712c56c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,6 +8,7 @@ using Lumina.Data; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Mod; using Penumbra.Mods; namespace Penumbra.Api; @@ -77,7 +78,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, ModManager _, ModCollection2 collection ) + private static string ResolvePath( string path, Mod.Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { @@ -134,7 +135,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { - collection = ModCollection2.Empty; + collection = ModCollection.Empty; } if( collection.HasCache ) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 3250e10d..17f11eae 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,261 +1,261 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Dalamud.Logging; -using Penumbra.Meta.Manager; using Penumbra.Mod; -using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; -public sealed partial class CollectionManager2 +public partial class ModCollection { - // Is invoked after the collections actually changed. - public event CollectionChangeDelegate? CollectionChanged; - - private int _currentIdx = -1; - private int _defaultIdx = -1; - private int _defaultNameIdx = 0; - - public ModCollection2 Current - => this[ _currentIdx ]; - - public ModCollection2 Default - => this[ _defaultIdx ]; - - private readonly Dictionary< string, int > _character = new(); - - public ModCollection2 Character( string name ) - => _character.TryGetValue( name, out var idx ) ? _collections[ idx ] : Default; - - public bool HasCharacterCollections - => _character.Count > 0; - - private void OnModChanged( ModChangeType type, int idx, ModData mod ) + public sealed partial class Manager { - switch( type ) + // Is invoked after the collections actually changed. + public event CollectionChangeDelegate? CollectionChanged; + + private int _currentIdx = 1; + private int _defaultIdx = 0; + private int _defaultNameIdx = 0; + + public ModCollection Current + => this[ _currentIdx ]; + + public ModCollection Default + => this[ _defaultIdx ]; + + private readonly Dictionary< string, int > _character = new(); + + public ModCollection Character( string name ) + => _character.TryGetValue( name, out var idx ) ? this[ idx ] : Default; + + public IEnumerable< (string, ModCollection) > Characters + => _character.Select( kvp => ( kvp.Key, this[ kvp.Value ] ) ); + + public bool HasCharacterCollections + => _character.Count > 0; + + private void OnModChanged( Mod.Mod.ChangeType type, int idx, Mod.Mod mod ) { - case ModChangeType.Added: - foreach( var collection in _collections ) - { - collection.AddMod( mod ); - } - - foreach( var collection in _collections.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) - { - collection.UpdateCache(); - } - - break; - case ModChangeType.Removed: - var list = new List< ModSettings? >( _collections.Count ); - foreach( var collection in _collections ) - { - list.Add( collection[ idx ].Settings ); - collection.RemoveMod( mod, idx ); - } - - foreach( var (collection, _) in _collections.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) - { - collection.UpdateCache(); - } - - break; - case ModChangeType.Changed: - foreach( var collection in _collections.Where( - collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) - { - collection.Save(); - } - - foreach( var collection in _collections.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) - { - collection.UpdateCache(); - } - - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } - } - - private void CreateNecessaryCaches() - { - if( _defaultIdx >= 0 ) - { - Default.CreateCache(); - } - - if( _currentIdx >= 0 ) - { - Current.CreateCache(); - } - - foreach( var idx in _character.Values.Where( i => i >= 0 ) ) - { - _collections[ idx ].CreateCache(); - } - } - - public void UpdateCaches() - { - foreach( var collection in _collections ) - { - collection.UpdateCache(); - } - } - - private void RemoveCache( int idx ) - { - if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) - { - _collections[ idx ].ClearCache(); - } - } - - public void SetCollection( string name, CollectionType type, string? characterName = null ) - => SetCollection( GetIndexForCollectionName( name ), type, characterName ); - - public void SetCollection( ModCollection2 collection, CollectionType type, string? characterName = null ) - => SetCollection( GetIndexForCollectionName( collection.Name ), type, characterName ); - - public void SetCollection( int newIdx, CollectionType type, string? characterName = null ) - { - var oldCollectionIdx = type switch - { - CollectionType.Default => _defaultIdx, - CollectionType.Current => _currentIdx, - CollectionType.Character => characterName?.Length > 0 - ? _character.TryGetValue( characterName, out var c ) - ? c - : _defaultIdx - : -2, - _ => -2, - }; - - if( oldCollectionIdx == -2 || newIdx == oldCollectionIdx ) - { - return; - } - - var newCollection = this[ newIdx ]; - if( newIdx >= 0 ) - { - newCollection.CreateCache(); - } - - RemoveCache( oldCollectionIdx ); - switch( type ) - { - case CollectionType.Default: - _defaultIdx = newIdx; - Penumbra.Config.DefaultCollection = newCollection.Name; - Penumbra.ResidentResources.Reload(); - Default.SetFiles(); - break; - case CollectionType.Current: - _currentIdx = newIdx; - Penumbra.Config.CurrentCollection = newCollection.Name; - break; - case CollectionType.Character: - _character[ characterName! ] = newIdx; - Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; - break; - } - - CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName ); - Penumbra.Config.Save(); - } - - public bool CreateCharacterCollection( string characterName ) - { - if( _character.ContainsKey( characterName ) ) - { - return false; - } - - _character[ characterName ] = -1; - Penumbra.Config.CharacterCollections[ characterName ] = ModCollection2.Empty.Name; - Penumbra.Config.Save(); - CollectionChanged?.Invoke( null, ModCollection2.Empty, CollectionType.Character, characterName ); - return true; - } - - public void RemoveCharacterCollection( string characterName ) - { - if( _character.TryGetValue( characterName, out var collection ) ) - { - RemoveCache( collection ); - _character.Remove( characterName ); - CollectionChanged?.Invoke( this[ collection ], null, CollectionType.Character, characterName ); - } - - if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) - { - Penumbra.Config.Save(); - } - } - - private int GetIndexForCollectionName( string name ) - { - if( name.Length == 0 || name == ModCollection2.DefaultCollection ) - { - return -1; - } - - var idx = _collections.IndexOf( c => c.Name == Penumbra.Config.DefaultCollection ); - return idx < 0 ? -2 : idx; - } - - public void LoadCollections() - { - var configChanged = false; - _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); - if( _defaultIdx == -2 ) - { - PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); - _defaultIdx = -1; - Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; - configChanged = true; - } - - _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); - if( _currentIdx == -2 ) - { - PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); - _currentIdx = _defaultNameIdx; - Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; - configChanged = true; - } - - if( LoadCharacterCollections() || configChanged ) - { - Penumbra.Config.Save(); - } - - CreateNecessaryCaches(); - } - - private bool LoadCharacterCollections() - { - var configChanged = false; - foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() ) - { - var idx = GetIndexForCollectionName( collectionName ); - if( idx == -2 ) + var meta = mod.Resources.MetaManipulations.Count > 0; + switch( type ) { - PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); - _character.Add( player, -1 ); - Penumbra.Config.CharacterCollections[ player ] = ModCollection2.Empty.Name; - configChanged = true; - } - else - { - _character.Add( player, idx ); + case Mod.Mod.ChangeType.Added: + foreach( var collection in this ) + { + collection.AddMod( mod ); + } + + foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + + break; + case Mod.Mod.ChangeType.Removed: + var list = new List< ModSettings? >( _collections.Count ); + foreach( var collection in this ) + { + list.Add( collection[ idx ].Settings ); + collection.RemoveMod( mod, idx ); + } + + foreach( var (collection, _) in this.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + + break; + case Mod.Mod.ChangeType.Changed: + foreach( var collection in this.Where( + collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + { + collection.Save(); + } + + foreach( var collection in this.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } } - return configChanged; + private void CreateNecessaryCaches() + { + if( _defaultIdx > Empty.Index ) + { + Default.CreateCache(true); + } + + if( _currentIdx > Empty.Index ) + { + Current.CreateCache(false); + } + + foreach( var idx in _character.Values.Where( i => i > Empty.Index ) ) + { + _collections[ idx ].CreateCache(false); + } + } + + public void ForceCacheUpdates() + { + foreach( var collection in this ) + { + collection.ForceCacheUpdate(collection == Default); + } + } + + private void RemoveCache( int idx ) + { + if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) + { + _collections[ idx ].ClearCache(); + } + } + + public void SetCollection( ModCollection collection, Type type, string? characterName = null ) + => SetCollection( collection.Index, type, characterName ); + + public void SetCollection( int newIdx, Type type, string? characterName = null ) + { + var oldCollectionIdx = type switch + { + Type.Default => _defaultIdx, + Type.Current => _currentIdx, + Type.Character => characterName?.Length > 0 + ? _character.TryGetValue( characterName, out var c ) + ? c + : _defaultIdx + : -1, + _ => -1, + }; + + if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) + { + return; + } + + var newCollection = this[ newIdx ]; + if( newIdx > Empty.Index ) + { + newCollection.CreateCache(false); + } + + RemoveCache( oldCollectionIdx ); + switch( type ) + { + case Type.Default: + _defaultIdx = newIdx; + Penumbra.Config.DefaultCollection = newCollection.Name; + Penumbra.ResidentResources.Reload(); + Default.SetFiles(); + break; + case Type.Current: + _currentIdx = newIdx; + Penumbra.Config.CurrentCollection = newCollection.Name; + break; + case Type.Character: + _character[ characterName! ] = newIdx; + Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; + break; + } + + CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName ); + Penumbra.Config.Save(); + } + + public bool CreateCharacterCollection( string characterName ) + { + if( _character.ContainsKey( characterName ) ) + { + return false; + } + + _character[ characterName ] = Empty.Index; + Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name; + Penumbra.Config.Save(); + CollectionChanged?.Invoke( null, Empty, Type.Character, characterName ); + return true; + } + + public void RemoveCharacterCollection( string characterName ) + { + if( _character.TryGetValue( characterName, out var collection ) ) + { + RemoveCache( collection ); + _character.Remove( characterName ); + CollectionChanged?.Invoke( this[ collection ], null, Type.Character, characterName ); + } + + if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) + { + Penumbra.Config.Save(); + } + } + + private int GetIndexForCollectionName( string name ) + { + if( name.Length == 0 ) + { + return Empty.Index; + } + + return _collections.IndexOf( c => c.Name == name ); + } + + public void LoadCollections() + { + var configChanged = false; + _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + if( _defaultIdx < 0 ) + { + PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); + _defaultIdx = Empty.Index; + Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; + configChanged = true; + } + + _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + if( _currentIdx < 0 ) + { + PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); + _currentIdx = _defaultNameIdx; + Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; + configChanged = true; + } + + if( LoadCharacterCollections() || configChanged ) + { + Penumbra.Config.Save(); + } + + CreateNecessaryCaches(); + } + + private bool LoadCharacterCollections() + { + var configChanged = false; + foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() ) + { + var idx = GetIndexForCollectionName( collectionName ); + if( idx < 0 ) + { + PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); + _character.Add( player, Empty.Index ); + Penumbra.Config.CharacterCollections[ player ] = Empty.Name; + configChanged = true; + } + else + { + _character.Add( player, idx ); + } + } + + return configChanged; + } } } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 7fd81136..440f88ac 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -2,222 +2,239 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using System.IO; using System.Linq; using Dalamud.Logging; -using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; -public enum CollectionType : byte +public partial class ModCollection { - Inactive, - Default, - Character, - Current, -} - -public sealed partial class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > -{ - public delegate void CollectionChangeDelegate( ModCollection2? oldCollection, ModCollection2? newCollection, CollectionType type, - string? characterName = null ); - - private readonly ModManager _modManager; - - private readonly List< ModCollection2 > _collections = new(); - - public ModCollection2 this[ Index idx ] - => idx.Value == -1 ? ModCollection2.Empty : _collections[ idx ]; - - public ModCollection2? this[ string name ] - => ByName( name, out var c ) ? c : null; - - public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) - => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); - - public IEnumerator< ModCollection2 > GetEnumerator() - => _collections.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public CollectionManager2( ModManager manager ) + public enum Type : byte { - _modManager = manager; - - _modManager.ModsRediscovered += OnModsRediscovered; - _modManager.ModChange += OnModChanged; - ReadCollections(); - LoadCollections(); + Inactive, + Default, + Character, + Current, } - public void Dispose() + public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { - _modManager.ModsRediscovered -= OnModsRediscovered; - _modManager.ModChange -= OnModChanged; - } + public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, Type type, + string? characterName = null ); - private void OnModsRediscovered() - { - UpdateCaches(); - Default.SetFiles(); - } + private readonly Mod.Mod.Manager _modManager; - private void AddDefaultCollection() - { - var idx = _collections.IndexOf( c => c.Name == ModCollection2.DefaultCollection ); - if( idx >= 0 ) + private readonly List< ModCollection > _collections = new() { - _defaultNameIdx = idx; - return; + Empty, + }; + + public ModCollection this[ Index idx ] + => _collections[ idx ]; + + public ModCollection this[ int idx ] + => _collections[ idx ]; + + public ModCollection? this[ string name ] + => ByName( name, out var c ) ? c : null; + + public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) + => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + + public IEnumerator< ModCollection > GetEnumerator() + => _collections.Skip( 1 ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public Manager( Mod.Mod.Manager manager ) + { + _modManager = manager; + + _modManager.ModsRediscovered += OnModsRediscovered; + _modManager.ModChange += OnModChanged; + ReadCollections(); + LoadCollections(); } - var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); - defaultCollection.Save(); - _defaultNameIdx = _collections.Count; - _collections.Add( defaultCollection ); - } - - private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) - { - foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + public void Dispose() { - var changes = false; - foreach( var subCollectionName in inheritance ) - { - if( !ByName( subCollectionName, out var subCollection ) ) - { - changes = true; - PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); - } - else if( !collection.AddInheritance( subCollection ) ) - { - changes = true; - PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); - } - } - - foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) - { - changes |= setting!.FixInvalidSettings( mod.Meta ); - } - - if( changes ) - { - collection.Save(); - } + _modManager.ModsRediscovered -= OnModsRediscovered; + _modManager.ModChange -= OnModChanged; } - } - private void ReadCollections() - { - var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); - var inheritances = new List< IReadOnlyList< string > >(); - if( collectionDir.Exists ) + private void OnModsRediscovered() { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + ForceCacheUpdates(); + Default.SetFiles(); + } + + private void AddDefaultCollection() + { + var idx = _collections.IndexOf( c => c.Name == DefaultCollection ); + if( idx >= 0 ) { - var collection = ModCollection2.LoadFromFile( file, out var inheritance ); - if( collection == null || collection.Name.Length == 0 ) + _defaultNameIdx = idx; + return; + } + + var defaultCollection = CreateNewEmpty( DefaultCollection ); + defaultCollection.Save(); + _defaultNameIdx = _collections.Count; + _collections.Add( defaultCollection ); + } + + private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + { + foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + { + var changes = false; + foreach( var subCollectionName in inheritance ) { - continue; + if( !ByName( subCollectionName, out var subCollection ) ) + { + changes = true; + PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + } + else if( !collection.AddInheritance( subCollection ) ) + { + changes = true; + PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + } } - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + changes |= setting!.FixInvalidSettings( mod.Meta ); } - if( this[ collection.Name ] != null ) + if( changes ) { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - inheritances.Add( inheritance ); - _collections.Add( collection ); + collection.Save(); } } } - AddDefaultCollection(); - ApplyInheritancesAndFixSettings( inheritances ); - } - - public bool AddCollection( string name, ModCollection2? duplicate ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 - || nameFixed == ModCollection2.Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + private void ReadCollections() { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = duplicate?.Duplicate( name ) ?? ModCollection2.CreateNewEmpty( name ); - _collections.Add( newCollection ); - newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); - SetCollection( _collections.Count - 1, CollectionType.Current ); - return true; - } - - public bool RemoveCollection( int idx ) - { - if( idx < 0 || idx >= _collections.Count ) - { - PluginLog.Error( "Can not remove the empty collection." ); - return false; - } - - if( idx == _defaultNameIdx ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - if( idx == _currentIdx ) - { - SetCollection( _defaultNameIdx, CollectionType.Current ); - } - else if( _currentIdx > idx ) - { - --_currentIdx; - } - - if( idx == _defaultIdx ) - { - SetCollection( -1, CollectionType.Default ); - } - else if( _defaultIdx > idx ) - { - --_defaultIdx; - } - - if( _defaultNameIdx > idx ) - { - --_defaultNameIdx; - } - - foreach( var (characterName, characterIdx) in _character.ToList() ) - { - if( idx == characterIdx ) + var collectionDir = new DirectoryInfo( CollectionDirectory ); + var inheritances = new List< IReadOnlyList< string > >(); + if( collectionDir.Exists ) { - SetCollection( -1, CollectionType.Character, characterName ); - } - else if( characterIdx > idx ) - { - _character[ characterName ] = characterIdx - 1; + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + { + var collection = LoadFromFile( file, out var inheritance ); + if( collection == null || collection.Name.Length == 0 ) + { + continue; + } + + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( this[ collection.Name ] != null ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + inheritances.Add( inheritance ); + collection.Index = _collections.Count; + _collections.Add( collection ); + } + } } + + AddDefaultCollection(); + ApplyInheritancesAndFixSettings( inheritances ); } - var collection = _collections[ idx ]; - collection.Delete(); - _collections.RemoveAt( idx ); - CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); - return true; + public bool AddCollection( string name, ModCollection? duplicate ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed.Length == 0 + || nameFixed == Empty.Name.ToLowerInvariant() + || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); + newCollection.Index = _collections.Count; + _collections.Add( newCollection ); + newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); + SetCollection( newCollection.Index, Type.Current ); + return true; + } + + public bool RemoveCollection( ModCollection collection ) + => RemoveCollection( collection.Index ); + + public bool RemoveCollection( int idx ) + { + if( idx <= Empty.Index || idx >= _collections.Count ) + { + PluginLog.Error( "Can not remove the empty collection." ); + return false; + } + + if( idx == _defaultNameIdx ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( idx == _currentIdx ) + { + SetCollection( _defaultNameIdx, Type.Current ); + } + else if( _currentIdx > idx ) + { + --_currentIdx; + } + + if( idx == _defaultIdx ) + { + SetCollection( -1, Type.Default ); + } + else if( _defaultIdx > idx ) + { + --_defaultIdx; + } + + if( _defaultNameIdx > idx ) + { + --_defaultNameIdx; + } + + foreach( var (characterName, characterIdx) in _character.ToList() ) + { + if( idx == characterIdx ) + { + SetCollection( -1, Type.Character, characterName ); + } + else if( characterIdx > idx ) + { + _character[ characterName ] = characterIdx - 1; + } + } + + var collection = _collections[ idx ]; + collection.Delete(); + _collections.RemoveAt( idx ); + for( var i = idx; i < _collections.Count; ++i ) + { + --_collections[ i ].Index; + } + + CollectionChanged?.Invoke( collection, null, Type.Inactive ); + return true; + } } } \ No newline at end of file diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Collections/ConflictCache.cs similarity index 65% rename from Penumbra/Mod/ModCache.cs rename to Penumbra/Collections/ConflictCache.cs index 80e7451c..85db330d 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Collections/ConflictCache.cs @@ -4,7 +4,7 @@ using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; -namespace Penumbra.Mod; +namespace Penumbra.Collections; public struct ConflictCache { @@ -60,6 +60,12 @@ public struct ConflictCache public IReadOnlyList< ModCacheStruct > Conflicts => _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); + public IEnumerable< ModCacheStruct > ModConflicts( int modIdx ) + { + return _conflicts?.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ) + ?? Array.Empty< ModCacheStruct >(); + } + public void Sort() => _conflicts?.Sort(); @@ -89,55 +95,4 @@ public struct ConflictCache public void ClearConflictsWithMod( int modIdx ) => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx ); -} - -// The ModCache contains volatile information dependent on all current settings in a collection. -public class ModCache -{ - public Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new(); - - public void AddConflict( Mod precedingMod, Utf8GamePath gamePath ) - { - if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) ) - { - conflicts.Files.Add( gamePath ); - } - else - { - Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() ); - } - } - - public void AddConflict( Mod precedingMod, MetaManipulation manipulation ) - { - if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) ) - { - conflicts.Manipulations.Add( manipulation ); - } - else - { - Conflicts[ precedingMod ] = ( new List< Utf8GamePath >(), new List< MetaManipulation > { manipulation } ); - } - } - - public void ClearConflicts() - => Conflicts.Clear(); - - public void ClearFileConflicts() - { - Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => - { - kvp.Value.Files.Clear(); - return kvp.Value; - } ); - } - - public void ClearMetaConflicts() - { - Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => - { - kvp.Value.Manipulations.Clear(); - return kvp.Value; - } ); - } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 697dde2f..01a53f87 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,10 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; -using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; @@ -14,27 +12,34 @@ using Penumbra.Util; namespace Penumbra.Collections; -public partial class ModCollection2 +public partial class ModCollection { private Cache? _cache; public bool HasCache => _cache != null; - public void CreateCache() + public void CreateCache( bool isDefault ) { + if( Index == 0 ) + { + return; + } + if( _cache == null ) { - _cache = new Cache( this ); - _cache.CalculateEffectiveFileList(); + CalculateEffectiveFileList( true, isDefault ); } } - public void UpdateCache() - => _cache?.CalculateEffectiveFileList(); + public void ForceCacheUpdate( bool isDefault ) + => CalculateEffectiveFileList( true, isDefault ); public void ClearCache() - => _cache = null; + { + _cache?.Dispose(); + _cache = null; + } public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); @@ -60,8 +65,16 @@ public partial class ModCollection2 internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); + internal IEnumerable< ConflictCache.ModCacheStruct > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.ModCacheStruct >(); + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) { + if( Index == 0 ) + { + return; + } + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); _cache ??= new Cache( this ); _cache.CalculateEffectiveFileList(); @@ -74,19 +87,21 @@ public partial class ModCollection2 { Penumbra.ResidentResources.Reload(); } + + _cache.Conflicts.Sort(); } // The ModCollectionCache contains all required temporary data to use a collection. // It will only be setup if a collection gets activated in any way. - private class Cache + private class Cache : IDisposable { // Shared caches to avoid allocations. private static readonly BitArray FileSeen = new(256); private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); private static readonly List< ModSettings? > ResolvedSettings = new(128); - private readonly ModCollection2 _collection; + private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new(); @@ -102,12 +117,35 @@ public partial class ModCollection2 } } - public Cache( ModCollection2 collection ) + public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; + _collection.InheritanceChanged += OnInheritanceChange; } + public void Dispose() + { + _collection.ModSettingChanged -= OnModSettingChange; + _collection.InheritanceChanged -= OnInheritanceChange; + } + + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + { + if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any() + || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) + { + return; + } + + var hasMeta = Penumbra.ModManager[ modIdx ].Resources.MetaManipulations.Count > 0; + _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); + } + + private void OnInheritanceChange( bool _ ) + => _collection.CalculateEffectiveFileList( true, true ); + private static void ResetFileSeen( int size ) { if( size < FileSeen.Length ) @@ -136,6 +174,7 @@ public partial class ModCollection2 { ClearStorageAndPrepare(); + Conflicts.ClearFileConflicts(); for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) { if( ResolvedSettings[ i ]?.Enabled == true ) @@ -240,7 +279,7 @@ public partial class ModCollection2 } } - private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) + private void AddPathsForOption( Option option, Mod.Mod mod, int modIdx, bool enabled ) { foreach( var (file, paths) in option.OptionFiles ) { @@ -270,7 +309,7 @@ public partial class ModCollection2 } } - private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) + private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod, int modIdx ) { Debug.Assert( singleGroup.SelectionType == SelectType.Single ); var settings = ResolvedSettings[ modIdx ]!; @@ -285,7 +324,7 @@ public partial class ModCollection2 } } - private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) + private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod, int modIdx ) { Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); var settings = ResolvedSettings[ modIdx ]!; @@ -301,7 +340,7 @@ public partial class ModCollection2 } } - private void AddRemainingFiles( ModData mod, int modIdx ) + private void AddRemainingFiles( Mod.Mod mod, int modIdx ) { for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) { diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 02f6f240..2cfd00ef 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -11,9 +11,9 @@ public enum ModSettingChange Setting, } -public partial class ModCollection2 +public partial class ModCollection { - public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName ); + public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ); public event ModSettingChangeDelegate ModSettingChanged; // Enable or disable the mod inheritance of mod idx. @@ -21,7 +21,7 @@ public partial class ModCollection2 { if( FixInheritance( idx, inherit ) ) { - ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null ); + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null, false ); } } @@ -32,9 +32,9 @@ public partial class ModCollection2 var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; if( newValue != oldValue ) { - var inheritance = FixInheritance( idx, true ); + var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Enabled = newValue; - ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null ); + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null, false ); } } @@ -45,9 +45,9 @@ public partial class ModCollection2 var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; if( newValue != oldValue ) { - var inheritance = FixInheritance( idx, true ); + var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Priority = newValue; - ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null ); + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null, false ); } } @@ -63,10 +63,10 @@ public partial class ModCollection2 : newValue; if( oldValue != newValue ) { - var inheritance = FixInheritance( idx, true ); + var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Settings[ settingName ] = newValue; _settings[ idx ]!.FixSpecificSetting( settingName, Penumbra.ModManager.Mods[ idx ].Meta ); - ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName, false ); } } @@ -108,6 +108,14 @@ public partial class ModCollection2 return false; } - private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4 ) - => Save(); + private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited ) + => SaveOnChange( inherited ); + + private void SaveOnChange( bool inherited ) + { + if( !inherited ) + { + Save(); + } + } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index dee20aef..158e0159 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -6,16 +6,16 @@ using Penumbra.Util; namespace Penumbra.Collections; -public partial class ModCollection2 +public partial class ModCollection { - private readonly List< ModCollection2 > _inheritance = new(); + private readonly List< ModCollection > _inheritance = new(); - public event Action InheritanceChanged; + public event Action< bool > InheritanceChanged; - public IReadOnlyList< ModCollection2 > Inheritance + public IReadOnlyList< ModCollection > Inheritance => _inheritance; - public IEnumerable< ModCollection2 > GetFlattenedInheritance() + public IEnumerable< ModCollection > GetFlattenedInheritance() { yield return this; @@ -27,7 +27,7 @@ public partial class ModCollection2 } } - public bool AddInheritance( ModCollection2 collection ) + public bool AddInheritance( ModCollection collection ) { if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) { @@ -35,25 +35,41 @@ public partial class ModCollection2 } _inheritance.Add( collection ); - InheritanceChanged.Invoke(); + collection.ModSettingChanged += OnInheritedModSettingChange; + collection.InheritanceChanged += OnInheritedInheritanceChange; + InheritanceChanged.Invoke( false ); return true; } public void RemoveInheritance( int idx ) { + var inheritance = _inheritance[ idx ]; + inheritance.ModSettingChanged -= OnInheritedModSettingChange; + inheritance.InheritanceChanged -= OnInheritedInheritanceChange; _inheritance.RemoveAt( idx ); - InheritanceChanged.Invoke(); + InheritanceChanged.Invoke( false ); } public void MoveInheritance( int from, int to ) { if( _inheritance.Move( from, to ) ) { - InheritanceChanged.Invoke(); + InheritanceChanged.Invoke( false ); } } - public (ModSettings? Settings, ModCollection2 Collection) this[ Index idx ] + private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + { + if( _settings[ modIdx ] == null ) + { + ModSettingChanged.Invoke( type, modIdx, oldValue, optionName, true ); + } + } + + private void OnInheritedInheritanceChange( bool _ ) + => InheritanceChanged.Invoke( true ); + + public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] { get { diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index abd935ab..19a89036 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -3,11 +3,11 @@ using Penumbra.Mod; namespace Penumbra.Collections; -public sealed partial class ModCollection2 +public sealed partial class ModCollection { private static class Migration { - public static void Migrate( ModCollection2 collection ) + public static void Migrate( ModCollection collection ) { var changes = MigrateV0ToV1( collection ); if( changes ) @@ -16,7 +16,7 @@ public sealed partial class ModCollection2 } } - private static bool MigrateV0ToV1( ModCollection2 collection ) + private static bool MigrateV0ToV1( ModCollection collection ) { if( collection.Version > 0 ) { diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 5f6c46ea..ce90144d 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -16,15 +16,25 @@ namespace Penumbra.Collections; // It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. // Active ModCollections build a cache of currently relevant data. -public partial class ModCollection2 +public partial class ModCollection { public const int CurrentVersion = 1; public const string DefaultCollection = "Default"; + public const string EmptyCollection = "None"; - public static readonly ModCollection2 Empty = CreateNewEmpty( "None" ); + public static readonly ModCollection Empty = CreateEmpty(); + + private static ModCollection CreateEmpty() + { + var collection = CreateNewEmpty( EmptyCollection ); + collection.Index = 0; + collection._settings.Clear(); + return collection; + } public string Name { get; private init; } public int Version { get; private set; } + public int Index { get; private set; } = -1; private readonly List< ModSettings? > _settings; @@ -37,7 +47,7 @@ public partial class ModCollection2 private readonly Dictionary< string, ModSettings > _unusedSettings; - private ModCollection2( string name, ModCollection2 duplicate ) + private ModCollection( string name, ModCollection duplicate ) { Name = name; Version = duplicate.Version; @@ -45,10 +55,10 @@ public partial class ModCollection2 _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); _inheritance = duplicate._inheritance.ToList(); ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; + InheritanceChanged += SaveOnChange; } - private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) + private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) { Name = name; Version = version; @@ -66,19 +76,19 @@ public partial class ModCollection2 Migration.Migrate( this ); ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; + InheritanceChanged += SaveOnChange; } - public static ModCollection2 CreateNewEmpty( string name ) + public static ModCollection CreateNewEmpty( string name ) => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); - public ModCollection2 Duplicate( string name ) + public ModCollection Duplicate( string name ) => new(name, this); - internal static ModCollection2 MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) => new(name, 0, allSettings); - private void CleanUnavailableSettings() + public void CleanUnavailableSettings() { var any = _unusedSettings.Count > 0; _unusedSettings.Clear(); @@ -88,7 +98,7 @@ public partial class ModCollection2 } } - public void AddMod( ModData mod ) + public void AddMod( Mod.Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -101,7 +111,7 @@ public partial class ModCollection2 } } - public void RemoveMod( ModData mod, int idx ) + public void RemoveMod( Mod.Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) @@ -165,6 +175,11 @@ public partial class ModCollection2 public void Delete() { + if( Index == 0 ) + { + return; + } + var file = FileName; if( file.Exists ) { @@ -179,7 +194,7 @@ public partial class ModCollection2 } } - public static ModCollection2? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) { inheritance = Array.Empty< string >(); if( !file.Exists ) @@ -197,7 +212,7 @@ public partial class ModCollection2 ?? new Dictionary< string, ModSettings >(); inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); - return new ModCollection2( name, version, settings ); + return new ModCollection( name, version, settings ); } catch( Exception e ) { diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 3d869944..de7308a5 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mod; using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -92,7 +93,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = ModManager.ResolvePath( path ); + var resolved = Mod.Mod.Manager.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index ba8d6b05..7a7b5e84 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -91,10 +91,10 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - internal readonly Dictionary< IntPtr, (ModCollection2, int) > DrawObjectToObject = new(); + internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); // This map links files to their corresponding collection, if it is non-default. - internal readonly ConcurrentDictionary< Utf8String, ModCollection2 > PathCollections = new(); + internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); internal GameObject* LastGameObject = null; @@ -159,7 +159,7 @@ public unsafe partial class PathResolver } // Identify the correct collection for a GameObject by index and name. - private static ModCollection2 IdentifyCollection( GameObject* gameObject ) + private static ModCollection IdentifyCollection( GameObject* gameObject ) { if( gameObject == null ) { @@ -180,9 +180,9 @@ public unsafe partial class PathResolver } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? name ) + private void CheckCollections( ModCollection? _1, ModCollection? _2, ModCollection.Type type, string? name ) { - if( type is not (CollectionType.Character or CollectionType.Default) ) + if( type is not (ModCollection.Type.Character or ModCollection.Type.Default) ) { return; } @@ -200,7 +200,7 @@ public unsafe partial class PathResolver } // Use the stored information to find the GameObject and Collection linked to a DrawObject. - private GameObject* FindParent( IntPtr drawObject, out ModCollection2 collection ) + private GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) { if( DrawObjectToObject.TryGetValue( drawObject, out var data ) ) { @@ -225,7 +225,7 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection2 collection ) + private void SetCollection( Utf8String path, ModCollection collection ) { if( PathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 51aeeae8..27d3ebcd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -41,7 +41,7 @@ public unsafe partial class PathResolver return ret; } - private ModCollection2? _mtrlCollection; + private ModCollection? _mtrlCollection; private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) { @@ -56,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 ModCollection2? collection ) + private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { @@ -96,7 +96,7 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - private static void HandleMtrlCollection( ModCollection2 collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + private static void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, object?) data ) { if( nonDefault && type == ResourceType.Mtrl ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index d32eee9f..b678028d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -161,7 +161,7 @@ public unsafe partial class PathResolver RspSetupCharacterHook?.Dispose(); } - private ModCollection2? GetCollection( IntPtr drawObject ) + private ModCollection? GetCollection( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); if( parent == null || collection == Penumbra.CollectionManager.Default ) @@ -195,7 +195,7 @@ public unsafe partial class PathResolver } } - public static MetaChanger ChangeEqp( ModCollection2 collection ) + public static MetaChanger ChangeEqp( ModCollection collection ) { #if USE_EQP collection.SetEqpFiles(); @@ -233,7 +233,7 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeEqdp( ModCollection2 collection ) + public static MetaChanger ChangeEqdp( ModCollection collection ) { #if USE_EQDP collection.SetEqdpFiles(); @@ -269,7 +269,7 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection2? collection ) + public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) { if( resolver.LastGameObject != null ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index eea23033..ebdb9ff0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -130,7 +130,7 @@ public unsafe partial class PathResolver // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePathDetour( ModCollection2 collection, IntPtr path ) + private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path ) { if( path == IntPtr.Zero ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4c85380c..7f2c89e7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -104,9 +104,9 @@ public partial class PathResolver : IDisposable Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; } - private void OnCollectionChange( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? characterName ) + private void OnCollectionChange( ModCollection? _1, ModCollection? _2, ModCollection.Type type, string? characterName ) { - if( type != CollectionType.Character ) + if( type != ModCollection.Type.Character ) { return; } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 2a6e3a37..a4f32b76 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -19,11 +19,11 @@ public partial class MetaManager public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); public readonly Dictionary< ImcManipulation, int > Manipulations = new(); - private readonly ModCollection2 _collection; + private readonly ModCollection _collection; private static int _imcManagerCount; - public MetaManagerImc( ModCollection2 collection ) + public MetaManagerImc( ModCollection collection ) { _collection = collection; SetupDelegate(); @@ -156,7 +156,7 @@ public partial class MetaManager { // Only check imcs. if( resource->FileType != ResourceType.Imc - || resolveData is not ModCollection2 { HasCache: true } collection + || resolveData is not ModCollection { HasCache: true } collection || !collection.MetaCache!.Imc.Files.TryGetValue( gamePath, out var file ) || !file.ChangesSinceLoad ) { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 0672d953..06c97baf 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -51,7 +51,7 @@ public partial class MetaManager : IDisposable + Est.Manipulations.Count + Eqp.Manipulations.Count; - public MetaManager( ModCollection2 collection ) + public MetaManager( ModCollection collection ) => Imc = new MetaManagerImc( collection ); public void SetFiles() diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 742b1d5d..be9df0d4 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -34,7 +34,7 @@ public static class MigrateConfiguration return; } - var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection ); var defaultCollectionFile = defaultCollection.FileName; if( defaultCollectionFile.Exists ) { @@ -74,7 +74,7 @@ public static class MigrateConfiguration } } - defaultCollection = ModCollection2.MigrateFromV0( ModCollection2.DefaultCollection, dict ); + defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); defaultCollection.Save(); } catch( Exception e ) diff --git a/Penumbra/Mod/FullMod.cs b/Penumbra/Mod/FullMod.cs new file mode 100644 index 00000000..6ffe9152 --- /dev/null +++ b/Penumbra/Mod/FullMod.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.IO; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mod; + +// A complete Mod containing settings (i.e. dependent on a collection) +// and the resulting cache. +public class FullMod +{ + public ModSettings Settings { get; } + public Mod Data { get; } + + public FullMod( ModSettings settings, Mod data ) + { + Settings = settings; + Data = data; + } + + public bool FixSettings() + => Settings.FixInvalidSettings( Data.Meta ); + + public HashSet< Utf8GamePath > GetFiles( FileInfo file ) + { + var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty; + return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); + } + + public override string ToString() + => Data.Meta.Name; +} \ No newline at end of file diff --git a/Penumbra/Mod/Mod.SortOrder.cs b/Penumbra/Mod/Mod.SortOrder.cs new file mode 100644 index 00000000..0e9c39eb --- /dev/null +++ b/Penumbra/Mod/Mod.SortOrder.cs @@ -0,0 +1,45 @@ +using System; +using Penumbra.Mods; + +namespace Penumbra.Mod; + +public partial class Mod +{ + public struct SortOrder : IComparable< SortOrder > + { + public ModFolder ParentFolder { get; set; } + + private string _sortOrderName; + + public string SortOrderName + { + get => _sortOrderName; + set => _sortOrderName = value.Replace( '/', '\\' ); + } + + public string SortOrderPath + => ParentFolder.FullName; + + public string FullName + { + get + { + var path = SortOrderPath; + return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName; + } + } + + + public SortOrder( ModFolder parentFolder, string name ) + { + ParentFolder = parentFolder; + _sortOrderName = name.Replace( '/', '\\' ); + } + + public string FullPath + => SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; + + public int CompareTo( SortOrder other ) + => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); + } +} \ No newline at end of file diff --git a/Penumbra/Mod/Mod.cs b/Penumbra/Mod/Mod.cs index cda33a3f..d5f7cc2f 100644 --- a/Penumbra/Mod/Mod.cs +++ b/Penumbra/Mod/Mod.cs @@ -1,33 +1,96 @@ using System.Collections.Generic; using System.IO; +using System.Linq; +using Dalamud.Logging; using Penumbra.GameData.ByteString; +using Penumbra.Mods; namespace Penumbra.Mod; -// A complete Mod containing settings (i.e. dependent on a collection) -// and the resulting cache. -public class Mod +// Mod contains all permanent information about a mod, +// and is independent of collections or settings. +// It only changes when the user actively changes the mod or their filesystem. +public partial class Mod { - public ModSettings Settings { get; } - public ModData Data { get; } - public ModCache Cache { get; } + public DirectoryInfo BasePath; + public ModMeta Meta; + public ModResources Resources; - public Mod( ModSettings settings, ModData data ) + public SortOrder Order; + + public SortedList< string, object? > ChangedItems { get; } = new(); + public string LowerChangedItemsString { get; private set; } = string.Empty; + public FileInfo MetaFile { get; set; } + public int Index { get; private set; } = -1; + + private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) { - Settings = settings; - Data = data; - Cache = new ModCache(); + BasePath = basePath; + Meta = meta; + Resources = resources; + MetaFile = MetaFileInfo( basePath ); + Order = new SortOrder( parentFolder, Meta.Name ); + Order.ParentFolder.AddMod( this ); + ComputeChangedItems(); } - public bool FixSettings() - => Settings.FixInvalidSettings( Data.Meta ); - - public HashSet< Utf8GamePath > GetFiles( FileInfo file ) + public void ComputeChangedItems() { - var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty; - return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); + var identifier = GameData.GameData.GetIdentifier(); + ChangedItems.Clear(); + foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) + { + foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) + { + identifier.Identify( ChangedItems, path.ToGamePath() ); + } + } + + foreach( var path in Meta.FileSwaps.Keys ) + { + identifier.Identify( ChangedItems, path.ToGamePath() ); + } + + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); } + public static FileInfo MetaFileInfo( DirectoryInfo basePath ) + => new(Path.Combine( basePath.FullName, "meta.json" )); + + public static Mod? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + { + basePath.Refresh(); + if( !basePath.Exists ) + { + PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + return null; + } + + var metaFile = MetaFileInfo( basePath ); + if( !metaFile.Exists ) + { + PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); + return null; + } + + var meta = ModMeta.LoadFromFile( metaFile ); + if( meta == null ) + { + return null; + } + + var data = new ModResources(); + if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) + { + data.SetManipulations( meta, basePath ); + } + + return new Mod( parentFolder, basePath, meta, data ); + } + + public void SaveMeta() + => Meta.SaveToFile( MetaFile ); + public override string ToString() - => Data.Meta.Name; + => Order.FullPath; } \ No newline at end of file diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 654c2174..0a2b3e4a 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -53,13 +53,13 @@ public class ModCleanup } } - private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option ) + private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) { var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); } - private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) + private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { var idx = Penumbra.ModManager.AddMod( newDir ); var newMod = Penumbra.ModManager.Mods[ idx ]; @@ -69,7 +69,7 @@ public class ModCleanup return newMod; } - private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option ) + private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) { var newMeta = new ModMeta { @@ -82,7 +82,7 @@ public class ModCleanup return newMeta; } - private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option ) + private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) { try { @@ -105,8 +105,8 @@ public class ModCleanup } var newSortOrder = group.SelectionType == SelectType.Single - ? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" - : $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; + ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" + : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; CreateNewMod( newDir, newSortOrder ); } catch( Exception e ) @@ -115,7 +115,7 @@ public class ModCleanup } } - public static void SplitMod( ModData mod ) + public static void SplitMod( Mod mod ) { if( mod.Meta.Groups.Count == 0 ) { diff --git a/Penumbra/Mod/ModData.cs b/Penumbra/Mod/ModData.cs deleted file mode 100644 index 6400a88d..00000000 --- a/Penumbra/Mod/ModData.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Mods; - -namespace Penumbra.Mod; - -public struct SortOrder : IComparable< SortOrder > -{ - public ModFolder ParentFolder { get; set; } - - private string _sortOrderName; - - public string SortOrderName - { - get => _sortOrderName; - set => _sortOrderName = value.Replace( '/', '\\' ); - } - - public string SortOrderPath - => ParentFolder.FullName; - - public string FullName - { - get - { - var path = SortOrderPath; - return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName; - } - } - - - public SortOrder( ModFolder parentFolder, string name ) - { - ParentFolder = parentFolder; - _sortOrderName = name.Replace( '/', '\\' ); - } - - public string FullPath - => SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; - - public int CompareTo( SortOrder other ) - => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); -} - -// ModData contains all permanent information about a mod, -// and is independent of collections or settings. -// It only changes when the user actively changes the mod or their filesystem. -public class ModData -{ - public DirectoryInfo BasePath; - public ModMeta Meta; - public ModResources Resources; - - public SortOrder SortOrder; - - public SortedList< string, object? > ChangedItems { get; } = new(); - public string LowerChangedItemsString { get; private set; } = string.Empty; - public FileInfo MetaFile { get; set; } - - private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) - { - BasePath = basePath; - Meta = meta; - Resources = resources; - MetaFile = MetaFileInfo( basePath ); - SortOrder = new SortOrder( parentFolder, Meta.Name ); - SortOrder.ParentFolder.AddMod( this ); - - ComputeChangedItems(); - } - - public void ComputeChangedItems() - { - var identifier = GameData.GameData.GetIdentifier(); - ChangedItems.Clear(); - foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) - { - foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - } - - foreach( var path in Meta.FileSwaps.Keys ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - - LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); - } - - public static FileInfo MetaFileInfo( DirectoryInfo basePath ) - => new(Path.Combine( basePath.FullName, "meta.json" )); - - public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) - { - basePath.Refresh(); - if( !basePath.Exists ) - { - PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); - return null; - } - - var metaFile = MetaFileInfo( basePath ); - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - var meta = ModMeta.LoadFromFile( metaFile ); - if( meta == null ) - { - return null; - } - - var data = new ModResources(); - if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) - { - data.SetManipulations( meta, basePath ); - } - - return new ModData( parentFolder, basePath, meta, data ); - } - - public void SaveMeta() - => Meta.SaveToFile( MetaFile ); - - public override string ToString() - => SortOrder.FullPath; -} \ No newline at end of file diff --git a/Penumbra/Mod/ModManager.cs b/Penumbra/Mod/ModManager.cs new file mode 100644 index 00000000..5f30c6bd --- /dev/null +++ b/Penumbra/Mod/ModManager.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta; +using Penumbra.Mod; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Mod; + +public partial class Mod +{ + public enum ChangeType + { + Added, + Removed, + Changed, + } + + // The ModManager handles the basic mods installed to the mod directory. + // It also contains the CollectionManager that handles all collections. + public class Manager : IEnumerable< Mod > + { + public DirectoryInfo BasePath { get; private set; } = null!; + + private readonly List< Mod > _mods = new(); + + public Mod this[ int idx ] + => _mods[ idx ]; + + public IReadOnlyList< Mod > Mods + => _mods; + + public IEnumerator< Mod > GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ModFolder StructuredMods { get; } = ModFileSystem.Root; + + public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod ); + + public event ModChangeDelegate? ModChange; + public event Action? ModsRediscovered; + + public bool Valid { get; private set; } + + public int Count + => _mods.Count; + + public Configuration Config + => Penumbra.Config; + + public void DiscoverMods( string newDir ) + { + SetBaseDirectory( newDir, false ); + DiscoverMods(); + } + + private void SetBaseDirectory( string newPath, bool firstTime ) + { + if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + if( newPath.Length == 0 ) + { + Valid = false; + BasePath = new DirectoryInfo( "." ); + } + else + { + var newDir = new DirectoryInfo( newPath ); + if( !newDir.Exists ) + { + try + { + Directory.CreateDirectory( newDir.FullName ); + newDir.Refresh(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); + } + } + + BasePath = newDir; + Valid = true; + if( Config.ModDirectory != BasePath.FullName ) + { + Config.ModDirectory = BasePath.FullName; + Config.Save(); + } + } + + ModsRediscovered?.Invoke(); + } + + public Manager() + { + SetBaseDirectory( Config.ModDirectory, true ); + } + + private bool SetSortOrderPath( Mod mod, string path ) + { + mod.Move( path ); + var fixedPath = mod.Order.FullPath; + if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) + { + Config.ModSortOrder.Remove( mod.BasePath.Name ); + return true; + } + + if( path != fixedPath ) + { + Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; + return true; + } + + return false; + } + + private void SetModStructure( bool removeOldPaths = false ) + { + var changes = false; + + foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) + { + if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) + { + changes |= SetSortOrderPath( mod, path ); + } + else if( removeOldPaths ) + { + changes = true; + Config.ModSortOrder.Remove( folder ); + } + } + + if( changes ) + { + Config.Save(); + } + } + + public void DiscoverMods() + { + _mods.Clear(); + BasePath.Refresh(); + + StructuredMods.SubFolders.Clear(); + StructuredMods.Mods.Clear(); + if( Valid && BasePath.Exists ) + { + foreach( var modFolder in BasePath.EnumerateDirectories() ) + { + var mod = LoadMod( StructuredMods, modFolder ); + if( mod == null ) + { + continue; + } + + mod.Index = _mods.Count; + _mods.Add( mod ); + } + + SetModStructure(); + } + + ModsRediscovered?.Invoke(); + } + + public void DeleteMod( DirectoryInfo modFolder ) + { + if( Directory.Exists( modFolder.FullName ) ) + { + try + { + Directory.Delete( modFolder.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); + } + } + + var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); + if( idx >= 0 ) + { + var mod = _mods[ idx ]; + mod.Order.ParentFolder.RemoveMod( mod ); + _mods.RemoveAt( idx ); + for( var i = idx; i < _mods.Count; ++i ) + { + --_mods[ i ].Index; + } + + ModChange?.Invoke( ChangeType.Removed, idx, mod ); + } + } + + public int AddMod( DirectoryInfo modFolder ) + { + var mod = LoadMod( StructuredMods, modFolder ); + if( mod == null ) + { + return -1; + } + + if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + { + if( SetSortOrderPath( mod, sortOrder ) ) + { + Config.Save(); + } + } + + if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) + { + return -1; + } + + _mods.Add( mod ); + ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); + + return _mods.Count - 1; + } + + public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + { + var mod = Mods[ idx ]; + var oldName = mod.Meta.Name; + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; + var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); + + if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) + { + return false; + } + + if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) + { + mod.ComputeChangedItems(); + if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + { + mod.Move( sortOrder ); + var path = mod.Order.FullPath; + if( path != sortOrder ) + { + Config.ModSortOrder[ mod.BasePath.Name ] = path; + Config.Save(); + } + } + else + { + mod.Order = new SortOrder( StructuredMods, mod.Meta.Name ); + } + } + + var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); + + recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); + if( recomputeMeta ) + { + mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); + mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); + } + + // TODO: more specific mod changes? + ModChange?.Invoke( ChangeType.Changed, idx, mod ); + return true; + } + + public bool UpdateMod( Mod mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) + => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); + + public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index b0d01bd7..ba946687 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -37,7 +37,7 @@ public static partial class ModFileSystem // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. // Saves and returns true if anything changed. - public static bool Rename( this ModData mod, string newName ) + public static bool Rename( this Mod.Mod mod, string newName ) { if( RenameNoSave( mod, newName ) ) { @@ -63,7 +63,7 @@ public static partial class ModFileSystem // Move a single mod to the target folder. // Returns true and saves if anything changed. - public static bool Move( this ModData mod, ModFolder target ) + public static bool Move( this Mod.Mod mod, ModFolder target ) { if( MoveNoSave( mod, target ) ) { @@ -76,7 +76,7 @@ public static partial class ModFileSystem // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. // Creates all necessary Subfolders. - public static void Move( this ModData mod, string sortOrder ) + public static void Move( this Mod.Mod mod, string sortOrder ) { var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); var folder = Root; @@ -129,7 +129,7 @@ public static partial class ModFileSystem { foreach( var mod in target.AllMods( true ) ) { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; + Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; } Penumbra.Config.Save(); @@ -137,16 +137,16 @@ public static partial class ModFileSystem } // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( ModData mod ) + private static void SaveMod( Mod.Mod mod ) { - if( ReferenceEquals( mod.SortOrder.ParentFolder, Root ) - && string.Equals( mod.SortOrder.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) + if( ReferenceEquals( mod.Order.ParentFolder, Root ) + && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) { Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); } else { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; + Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; } Penumbra.Config.Save(); @@ -184,30 +184,30 @@ public static partial class ModFileSystem return true; } - private static bool RenameNoSave( ModData mod, string newName ) + private static bool RenameNoSave( Mod.Mod mod, string newName ) { newName = newName.Replace( '/', '\\' ); - if( mod.SortOrder.SortOrderName == newName ) + if( mod.Order.SortOrderName == newName ) { return false; } - mod.SortOrder.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.SortOrder = new SortOrder( mod.SortOrder.ParentFolder, newName ); - mod.SortOrder.ParentFolder.AddMod( mod ); + mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); + mod.Order = new Mod.Mod.SortOrder( mod.Order.ParentFolder, newName ); + mod.Order.ParentFolder.AddMod( mod ); return true; } - private static bool MoveNoSave( ModData mod, ModFolder target ) + private static bool MoveNoSave( Mod.Mod mod, ModFolder target ) { - var oldParent = mod.SortOrder.ParentFolder; + var oldParent = mod.Order.ParentFolder; if( ReferenceEquals( target, oldParent ) ) { return false; } oldParent.RemoveMod( mod ); - mod.SortOrder = new SortOrder( target, mod.SortOrder.SortOrderName ); + mod.Order = new Mod.Mod.SortOrder( target, mod.Order.SortOrderName ); target.AddMod( mod ); return true; } diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs index 76768be0..e2369819 100644 --- a/Penumbra/Mods/ModFolder.cs +++ b/Penumbra/Mods/ModFolder.cs @@ -27,7 +27,7 @@ namespace Penumbra.Mods } public List< ModFolder > SubFolders { get; } = new(); - public List< ModData > Mods { get; } = new(); + public List< Mod.Mod > Mods { get; } = new(); public ModFolder( ModFolder parent, string name ) { @@ -45,7 +45,7 @@ namespace Penumbra.Mods => SubFolders.Sum( f => f.TotalDescendantFolders() ); // Return all descendant mods in the specified order. - public IEnumerable< ModData > AllMods( bool foldersFirst ) + public IEnumerable< Mod.Mod > AllMods( bool foldersFirst ) { if( foldersFirst ) { @@ -59,7 +59,7 @@ namespace Penumbra.Mods return folder.AllMods( false ); } - return new[] { ( ModData )f }; + return new[] { ( Mod.Mod )f }; } ); } @@ -116,7 +116,7 @@ namespace Penumbra.Mods // Add the given mod as a child, if it is not already a child. // Returns the index of the found or inserted mod. - public int AddMod( ModData mod ) + public int AddMod( Mod.Mod mod ) { var idx = Mods.BinarySearch( mod, ModComparer ); if( idx >= 0 ) @@ -132,7 +132,7 @@ namespace Penumbra.Mods // Remove mod as a child if it exists. // If this folder is empty afterwards, remove it from its parent. - public void RemoveMod( ModData mod ) + public void RemoveMod( Mod.Mod mod ) { RemoveModIgnoreEmpty( mod ); CheckEmpty(); @@ -157,20 +157,20 @@ namespace Penumbra.Mods : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); } - internal class ModDataComparer : IComparer< ModData > + internal class ModDataComparer : IComparer< Mod.Mod > { public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. - public int Compare( ModData? x, ModData? y ) + public int Compare( Mod.Mod? x, Mod.Mod? y ) { if( ReferenceEquals( x, y ) ) { return 0; } - var cmp = string.Compare( x?.SortOrder.SortOrderName, y?.SortOrder.SortOrderName, CompareType ); + var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); if( cmp != 0 ) { return cmp; @@ -193,7 +193,7 @@ namespace Penumbra.Mods for( ; modIdx < Mods.Count; ++modIdx ) { var mod = Mods[ modIdx ]; - var modString = mod.SortOrder.SortOrderName; + var modString = mod.Order.SortOrderName; if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) { yield return mod; @@ -235,7 +235,7 @@ namespace Penumbra.Mods } // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveModIgnoreEmpty( ModData mod ) + internal void RemoveModIgnoreEmpty( Mod.Mod mod ) { var idx = Mods.BinarySearch( mod, ModComparer ); if( idx >= 0 ) diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs index 59f52ace..c8149a65 100644 --- a/Penumbra/Mods/ModManager.Directory.cs +++ b/Penumbra/Mods/ModManager.Directory.cs @@ -9,9 +9,9 @@ namespace Penumbra.Mods; public partial class ModManagerNew { - private readonly List< ModData > _mods = new(); + private readonly List< Mod.Mod > _mods = new(); - public IReadOnlyList< ModData > Mods + public IReadOnlyList< Mod.Mod > Mods => _mods; public void DiscoverMods() diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs deleted file mode 100644 index b88a9bca..00000000 --- a/Penumbra/Mods/ModManager.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Meta; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public enum ModChangeType -{ - Added, - Removed, - Changed, -} - -public delegate void ModChangeDelegate( ModChangeType type, int modIndex, ModData modData ); - -// The ModManager handles the basic mods installed to the mod directory. -// It also contains the CollectionManager that handles all collections. -public class ModManager : IEnumerable< ModData > -{ - public DirectoryInfo BasePath { get; private set; } = null!; - - private readonly List< ModData > _mods = new(); - - public ModData this[ int idx ] - => _mods[ idx ]; - - public IReadOnlyList< ModData > Mods - => _mods; - - public IEnumerator< ModData > GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ModFolder StructuredMods { get; } = ModFileSystem.Root; - - public event ModChangeDelegate? ModChange; - public event Action? ModsRediscovered; - - public bool Valid { get; private set; } - - public int Count - => _mods.Count; - - public Configuration Config - => Penumbra.Config; - - public void DiscoverMods( string newDir ) - { - SetBaseDirectory( newDir, false ); - DiscoverMods(); - } - - private void SetBaseDirectory( string newPath, bool firstTime ) - { - if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - if( newPath.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - BasePath = newDir; - Valid = true; - if( Config.ModDirectory != BasePath.FullName ) - { - Config.ModDirectory = BasePath.FullName; - Config.Save(); - } - } - - ModsRediscovered?.Invoke(); - } - - public ModManager() - { - SetBaseDirectory( Config.ModDirectory, true ); - } - - private bool SetSortOrderPath( ModData mod, string path ) - { - mod.Move( path ); - var fixedPath = mod.SortOrder.FullPath; - if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - Config.ModSortOrder.Remove( mod.BasePath.Name ); - return true; - } - - if( path != fixedPath ) - { - Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; - return true; - } - - return false; - } - - private void SetModStructure( bool removeOldPaths = false ) - { - var changes = false; - - foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) - { - if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) - { - changes |= SetSortOrderPath( mod, path ); - } - else if( removeOldPaths ) - { - changes = true; - Config.ModSortOrder.Remove( folder ); - } - } - - if( changes ) - { - Config.Save(); - } - } - - public void DiscoverMods() - { - _mods.Clear(); - BasePath.Refresh(); - - StructuredMods.SubFolders.Clear(); - StructuredMods.Mods.Clear(); - if( Valid && BasePath.Exists ) - { - foreach( var modFolder in BasePath.EnumerateDirectories() ) - { - var mod = ModData.LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - continue; - } - - _mods.Add( mod ); - } - - SetModStructure(); - } - - ModsRediscovered?.Invoke(); - } - - public void DeleteMod( DirectoryInfo modFolder ) - { - if( Directory.Exists( modFolder.FullName ) ) - { - try - { - Directory.Delete( modFolder.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); - } - } - - var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); - if( idx >= 0 ) - { - var mod = _mods[ idx ]; - mod.SortOrder.ParentFolder.RemoveMod( mod ); - _mods.RemoveAt( idx ); - ModChange?.Invoke( ModChangeType.Removed, idx, mod ); - } - } - - public int AddMod( DirectoryInfo modFolder ) - { - var mod = ModData.LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - return -1; - } - - if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - if( SetSortOrderPath( mod, sortOrder ) ) - { - Config.Save(); - } - } - - if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) - { - return -1; - } - - _mods.Add( mod ); - ModChange?.Invoke( ModChangeType.Added, _mods.Count - 1, mod ); - - return _mods.Count - 1; - } - - public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - { - var mod = Mods[ idx ]; - var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; - var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); - - if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) - { - return false; - } - - if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) - { - mod.ComputeChangedItems(); - if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - mod.Move( sortOrder ); - var path = mod.SortOrder.FullPath; - if( path != sortOrder ) - { - Config.ModSortOrder[ mod.BasePath.Name ] = path; - Config.Save(); - } - } - else - { - mod.SortOrder = new SortOrder( StructuredMods, mod.Meta.Name ); - } - } - - var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); - - recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); - if( recomputeMeta ) - { - mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); - mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); - } - - // TODO: more specific mod changes? - ModChange?.Invoke( ModChangeType.Changed, idx, mod ); - return true; - } - - public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); - - public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 5083075a..7160d874 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -12,7 +12,7 @@ namespace Penumbra.Mods; // Contains all change functions on a specific mod that also require corresponding changes to collections. public static class ModManagerEditExtensions { - public static bool RenameMod( this ModManager manager, string newName, ModData mod ) + public static bool RenameMod( this Mod.Mod.Manager manager, string newName, Mod.Mod mod ) { if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) { @@ -25,23 +25,23 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeSortOrder( this ModManager manager, ModData mod, string newSortOrder ) + public static bool ChangeSortOrder( this Mod.Mod.Manager manager, Mod.Mod mod, string newSortOrder ) { - if( string.Equals( mod.SortOrder.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) + if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) { return false; } - var inRoot = new SortOrder( manager.StructuredMods, mod.Meta.Name ); + var inRoot = new Mod.Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) { - mod.SortOrder = inRoot; + mod.Order = inRoot; manager.Config.ModSortOrder.Remove( mod.BasePath.Name ); } else { mod.Move( newSortOrder ); - manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullPath; + manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; } manager.Config.Save(); @@ -49,7 +49,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RenameModFolder( this ModManager manager, ModData mod, DirectoryInfo newDir, bool move = true ) + public static bool RenameModFolder( this Mod.Mod.Manager manager, Mod.Mod mod, DirectoryInfo newDir, bool move = true ) { if( move ) { @@ -73,7 +73,7 @@ public static class ModManagerEditExtensions var oldBasePath = mod.BasePath; mod.BasePath = newDir; - mod.MetaFile = ModData.MetaFileInfo( newDir ); + mod.MetaFile = Mod.Mod.MetaFileInfo( newDir ); manager.UpdateMod( mod ); if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) @@ -95,7 +95,7 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeModGroup( this ModManager manager, string oldGroupName, string newGroupName, ModData mod, + public static bool ChangeModGroup( this Mod.Mod.Manager manager, string oldGroupName, string newGroupName, Mod.Mod mod, SelectType type = SelectType.Single ) { if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) @@ -157,7 +157,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RemoveModOption( this ModManager manager, int optionIdx, OptionGroup group, ModData mod ) + public static bool RemoveModOption( this Mod.Mod.Manager manager, int optionIdx, OptionGroup group, Mod.Mod mod ) { if( optionIdx < 0 || optionIdx >= group.Options.Count ) { @@ -202,7 +202,7 @@ public static class ModManagerEditExtensions if( collection.HasCache && settings.Enabled ) { collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - Penumbra.CollectionManager.Default == collection ); + Penumbra.CollectionManager.Default == collection ); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 70535479..61a803ea 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -16,6 +16,7 @@ using Penumbra.Util; using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; +using Penumbra.Mod; namespace Penumbra; @@ -34,8 +35,8 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static ModManager ModManager { get; private set; } = null!; - public static CollectionManager2 CollectionManager { get; private set; } = null!; + public static Mod.Mod.Manager ModManager { get; private set; } = null!; + public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } @@ -66,9 +67,9 @@ public class Penumbra : IDalamudPlugin CharacterUtility = new CharacterUtility(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new ModManager(); + ModManager = new Mod.Mod.Manager(); ModManager.DiscoverMods(); - CollectionManager = new CollectionManager2( ModManager ); + CollectionManager = new ModCollection.Manager( ModManager ); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -223,9 +224,9 @@ public class Penumbra : IDalamudPlugin type = type.ToLowerInvariant(); collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection2.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) - ? ModCollection2.Empty - : CollectionManager[collectionName]; + var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) + ? ModCollection.Empty + : CollectionManager[ collectionName ]; if( collection == null ) { Dalamud.Chat.Print( $"The collection {collection} does not exist." ); @@ -241,7 +242,7 @@ public class Penumbra : IDalamudPlugin return false; } - CollectionManager.SetCollection( collection, CollectionType.Default ); + CollectionManager.SetCollection( collection, ModCollection.Type.Default ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index bcd6ff0a..56bddabd 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -25,9 +25,7 @@ public partial class SettingsInterface return; } - var modManager = Penumbra.ModManager; - var items = Penumbra.CollectionManager.DefaultCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); - var forced = Penumbra.CollectionManager.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var items = Penumbra.CollectionManager.Default.ChangedItems; using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); @@ -45,12 +43,8 @@ public partial class SettingsInterface raii.Push( ImGui.EndTable ); var list = items.AsEnumerable(); - if( forced.Count > 0 ) - { - list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); - } - if( _filter.Any() ) + if( _filter.Length > 0 ) { list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 2d94eaef..4775526e 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -22,9 +22,8 @@ public partial class SettingsInterface private readonly Selector _selector; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; - private ModCollection2[] _collections = null!; + private ModCollection[] _collections = null!; private int _currentCollectionIndex; - private int _currentForcedIndex; private int _currentDefaultIndex; private readonly Dictionary< string, int > _currentCharacterIndices = new(); private string _newCollectionName = string.Empty; @@ -32,14 +31,14 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = Penumbra.CollectionManager.Prepend( ModCollection2.Empty ).ToArray(); + _collections = Penumbra.CollectionManager.Prepend( ModCollection.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); } - private int GetIndex( ModCollection2 collection ) + private int GetIndex( ModCollection collection ) { var ret = _collections.IndexOf( c => c.Name == collection.Name ); if( ret < 0 ) @@ -52,20 +51,17 @@ public partial class SettingsInterface } private void UpdateIndex() - => _currentCollectionIndex = GetIndex( Penumbra.CollectionManager.CurrentCollection ) - 1; - - public void UpdateForcedIndex() - => _currentForcedIndex = GetIndex( Penumbra.CollectionManager.ForcedCollection ); + => _currentCollectionIndex = GetIndex( Penumbra.CollectionManager.Current ) - 1; public void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( Penumbra.CollectionManager.DefaultCollection ); + => _currentDefaultIndex = GetIndex( Penumbra.CollectionManager.Default ); private void UpdateCharacterIndices() { _currentCharacterIndices.Clear(); - foreach( var kvp in Penumbra.CollectionManager.CharacterCollection ) + foreach( var (character, collection) in Penumbra.CollectionManager.Characters ) { - _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); + _currentCharacterIndices[ character ] = GetIndex( collection ); } } @@ -73,7 +69,6 @@ public partial class SettingsInterface { UpdateIndex(); UpdateDefaultIndex(); - UpdateForcedIndex(); UpdateCharacterIndices(); } @@ -83,24 +78,22 @@ public partial class SettingsInterface UpdateNames(); } - private void CreateNewCollection( Dictionary< string, ModSettings > settings ) + private void CreateNewCollection( bool duplicate ) { - if( Penumbra.CollectionManager.AddCollection( _newCollectionName, settings ) ) + if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) { UpdateNames(); - SetCurrentCollection( Penumbra.CollectionManager.ByName( _newCollectionName )!, true ); + SetCurrentCollection( Penumbra.CollectionManager[ _newCollectionName ]!, true ); } _newCollectionName = string.Empty; } - private void DrawCleanCollectionButton() + private static void DrawCleanCollectionButton() { if( ImGui.Button( "Clean Settings" ) ) { - var changes = ModFunctions.CleanUpCollection( Penumbra.CollectionManager.CurrentCollection.Settings, - Penumbra.ModManager.BasePath.EnumerateDirectories() ); - Penumbra.CollectionManager.CurrentCollection.UpdateSettings( changes ); + Penumbra.CollectionManager.Current.CleanUnavailableSettings(); } ImGuiCustom.HoverTooltip( @@ -120,14 +113,14 @@ public partial class SettingsInterface if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( new Dictionary< string, ModSettings >() ); + CreateNewCollection( false ); } var hover = ImGui.IsItemHovered(); ImGui.SameLine(); if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( Penumbra.CollectionManager.CurrentCollection.Settings ); + CreateNewCollection( true ); } hover |= ImGui.IsItemHovered(); @@ -138,13 +131,12 @@ public partial class SettingsInterface ImGui.SetTooltip( "Please enter a name before creating a collection." ); } - var deleteCondition = Penumbra.CollectionManager.Collections.Count > 1 - && Penumbra.CollectionManager.CurrentCollection.Name != ModCollection.DefaultCollection; + var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; ImGui.SameLine(); if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.CurrentCollection.Name ); - SetCurrentCollection( Penumbra.CollectionManager.CurrentCollection, true ); + Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); + SetCurrentCollection( Penumbra.CollectionManager.Current, true ); UpdateNames(); } @@ -167,7 +159,7 @@ public partial class SettingsInterface return; } - Penumbra.CollectionManager.SetCollection( _collections[ idx + 1 ], CollectionType.Current ); + Penumbra.CollectionManager.SetCollection( _collections[ idx + 1 ], ModCollection.Type.Current ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -176,7 +168,7 @@ public partial class SettingsInterface } } - public void SetCurrentCollection( ModCollection2 collection, bool force = false ) + public void SetCurrentCollection( ModCollection collection, bool force = false ) { var idx = Array.IndexOf( _collections, collection ) - 1; if( idx >= 0 ) @@ -206,7 +198,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) { - Penumbra.CollectionManager.SetCollection( _collections[ index ], CollectionType.Default ); + Penumbra.CollectionManager.SetCollection( _collections[ index ], ModCollection.Type.Default ); _currentDefaultIndex = index; } @@ -219,34 +211,6 @@ public partial class SettingsInterface ImGui.Text( "Default Collection" ); } - private void DrawForcedCollectionSelector() - { - var index = _currentForcedIndex; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, Penumbra.CollectionManager.CharacterCollection.Count == 0 ); - if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) - && index != _currentForcedIndex - && Penumbra.CollectionManager.CharacterCollection.Count > 0 ) - { - Penumbra.CollectionManager.SetCollection( _collections[ index ], CollectionType.Forced ); - _currentForcedIndex = index; - } - - style.Pop(); - if( Penumbra.CollectionManager.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( - "Forced Collections only provide value if you have at least one Character Collection. There is no need to set one until then." ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Mods in the forced collection are always loaded if not overwritten by anything in the current or character-based collection.\n" - + "Please avoid mixing meta-manipulating mods in Forced and other collections, as this will probably not work correctly." ); - ImGui.SameLine(); - ImGui.Text( "Forced Collection" ); - } - private void DrawNewCharacterCollection() { ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); @@ -339,16 +303,15 @@ public partial class SettingsInterface } DrawDefaultCollectionSelector(); - DrawForcedCollectionSelector(); - foreach( var name in Penumbra.CollectionManager.CharacterCollection.Keys.ToArray() ) + foreach( var (name, collection) in Penumbra.CollectionManager.Characters.ToArray() ) { var idx = _currentCharacterIndices[ name ]; var tmp = idx; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) { - Penumbra.CollectionManager.SetCollection( _collections[ tmp ], CollectionType.Character, name ); + Penumbra.CollectionManager.SetCollection( _collections[ tmp ], ModCollection.Type.Character, name ); _currentCharacterIndices[ name ] = tmp; } diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 756d69d2..bfa7d1b8 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -37,12 +37,10 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); var manager = Penumbra.ModManager; - PrintValue( "Current Collection", Penumbra.CollectionManager.CurrentCollection.Name ); - PrintValue( " has Cache", ( Penumbra.CollectionManager.CurrentCollection.Cache != null ).ToString() ); - PrintValue( "Default Collection", Penumbra.CollectionManager.DefaultCollection.Name ); - PrintValue( " has Cache", ( Penumbra.CollectionManager.DefaultCollection.Cache != null ).ToString() ); - PrintValue( "Forced Collection", Penumbra.CollectionManager.ForcedCollection.Name ); - PrintValue( " has Cache", ( Penumbra.CollectionManager.ForcedCollection.Cache != null ).ToString() ); + PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); + PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); @@ -111,15 +109,15 @@ public partial class SettingsInterface return; } - var cache = Penumbra.CollectionManager.CurrentCollection.Cache; - if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) + if( !Penumbra.CollectionManager.Current.HasCache + || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - foreach( var file in cache.MissingFiles ) + foreach( var file in Penumbra.CollectionManager.Current.MissingFiles ) { ImGui.TableNextRow(); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index cbf17b9b..04c0dbd1 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -100,24 +100,66 @@ public partial class SettingsInterface return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); } - private void DrawFilteredRows( ModCollection2 active ) + private void DrawFilteredRows( ModCollection active ) { - void DrawFileLines( ModCollection2.Cache cache ) + foreach( var (gp, fp) in active.ResolvedFiles.Where( CheckFilters ) ) { - foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) - { - DrawLine( gp, fp ); - } - - //foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations - // .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) - // .Where( CheckFilters ) ) - //{ - // DrawLine( mp, mod ); - //} + DrawLine( gp, fp ); } - DrawFileLines( active ); + var cache = active.MetaCache; + if( cache == null ) + { + return; + } + + foreach( var (mp, mod, _) in cache.Cmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + + foreach( var (mp, mod, _) in cache.Eqp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + + foreach( var (mp, mod, _) in cache.Eqdp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + + foreach( var (mp, mod, _) in cache.Gmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + + foreach( var (mp, mod, _) in cache.Est.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } + + foreach( var (mp, mod, _) in cache.Imc.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, + Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + .Where( CheckFilters ) ) + { + DrawLine( mp, mod ); + } } public void Draw() @@ -133,17 +175,12 @@ public partial class SettingsInterface const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - var modManager = Penumbra.ModManager; - var defaultCollection = Penumbra.CollectionManager.DefaultCollection.Cache; - var forcedCollection = Penumbra.CollectionManager.ForcedCollection.Cache; + var resolved = Penumbra.CollectionManager.Default.ResolvedFiles; + var meta = Penumbra.CollectionManager.Default.MetaCache; + var metaCount = meta?.Count ?? 0; + var resolvedCount = resolved.Count; - var (defaultResolved, defaultMeta) = defaultCollection != null - ? ( defaultCollection.ResolvedFiles.Count, defaultCollection.MetaManipulations.Count ) - : ( 0, 0 ); - var (forcedResolved, forcedMeta) = forcedCollection != null - ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count ) - : ( 0, 0 ); - var totalLines = defaultResolved + forcedResolved + defaultMeta + forcedMeta; + var totalLines = resolvedCount + metaCount; if( totalLines == 0 ) { return; @@ -159,7 +196,7 @@ public partial class SettingsInterface if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 ) { - DrawFilteredRows( defaultCollection, forcedCollection ); + DrawFilteredRows( Penumbra.CollectionManager.Default ); } else { @@ -178,29 +215,17 @@ public partial class SettingsInterface { var row = actualRow; ImGui.TableNextRow(); - if( row < defaultResolved ) + if( row < resolvedCount ) { - var (gamePath, file) = defaultCollection!.ResolvedFiles.ElementAt( row ); + var (gamePath, file) = resolved.ElementAt( row ); DrawLine( gamePath, file ); } - else if( ( row -= defaultResolved ) < defaultMeta ) + else if( ( row -= resolved.Count ) < metaCount ) { // TODO //var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); DrawLine( 0.ToString(), 0.ToString() ); } - else if( ( row -= defaultMeta ) < forcedResolved ) - { - var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file ); - } - else - { - // TODO - row -= forcedResolved; - //var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( 0.ToString(), 0.ToString() ); - } } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index fd0a0951..b797902e 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -2,324 +2,328 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; +using Penumbra.Mod; using Penumbra.Mods; +using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public class ModListCache : IDisposable { - public class ModListCache : IDisposable + public const uint NewModColor = 0xFF66DD66u; + public const uint DisabledModColor = 0xFF666666u; + public const uint ConflictingModColor = 0xFFAAAAFFu; + public const uint HandledConflictModColor = 0xFF88DDDDu; + + private readonly Mod.Mod.Manager _manager; + + private readonly List< FullMod > _modsInOrder = new(); + private readonly List< (bool visible, uint color) > _visibleMods = new(); + private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); + private readonly IReadOnlySet< string > _newMods; + + private string _modFilter = string.Empty; + private string _modFilterChanges = string.Empty; + private string _modFilterAuthor = string.Empty; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private bool _listResetNecessary; + private bool _filterResetNecessary; + + + public ModFilter StateFilter { - public const uint NewModColor = 0xFF66DD66u; - public const uint DisabledModColor = 0xFF666666u; - public const uint ConflictingModColor = 0xFFAAAAFFu; - public const uint HandledConflictModColor = 0xFF88DDDDu; - - private readonly ModManager _manager; - - private readonly List< Mod.Mod > _modsInOrder = new(); - private readonly List< (bool visible, uint color) > _visibleMods = new(); - private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); - private readonly IReadOnlySet< string > _newMods; - - private string _modFilter = string.Empty; - private string _modFilterChanges = string.Empty; - private string _modFilterAuthor = string.Empty; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - private bool _listResetNecessary; - private bool _filterResetNecessary; - - - public ModFilter StateFilter + get => _stateFilter; + set { - get => _stateFilter; - set + var diff = _stateFilter != value; + _stateFilter = value; + if( diff ) { - var diff = _stateFilter != value; - _stateFilter = value; - if( diff ) - { - TriggerFilterReset(); - } + TriggerFilterReset(); } } + } - public ModListCache( ModManager manager, IReadOnlySet< string > newMods ) + public ModListCache( Mod.Mod.Manager manager, IReadOnlySet< string > newMods ) + { + _manager = manager; + _newMods = newMods; + ResetModList(); + ModFileSystem.ModFileSystemChanged += TriggerListReset; + } + + public void Dispose() + { + ModFileSystem.ModFileSystemChanged -= TriggerListReset; + } + + public int Count + => _modsInOrder.Count; + + + public bool Update() + { + if( _listResetNecessary ) { - _manager = manager; - _newMods = newMods; ResetModList(); - ModFileSystem.ModFileSystemChanged += TriggerListReset; - } - - public void Dispose() - { - ModFileSystem.ModFileSystemChanged -= TriggerListReset; - } - - public int Count - => _modsInOrder.Count; - - - public bool Update() - { - if( _listResetNecessary ) - { - ResetModList(); - return true; - } - - if( _filterResetNecessary ) - { - ResetFilters(); - return true; - } - - return false; - } - - public void TriggerListReset() - => _listResetNecessary = true; - - public void TriggerFilterReset() - => _filterResetNecessary = true; - - public void RemoveMod( Mod.Mod mod ) - { - var idx = _modsInOrder.IndexOf( mod ); - if( idx >= 0 ) - { - _modsInOrder.RemoveAt( idx ); - _visibleMods.RemoveAt( idx ); - UpdateFolders(); - } - } - - private void SetFolderAndParentsVisible( ModFolder? folder ) - { - while( folder != null && ( !_visibleFolders.TryGetValue( folder, out var state ) || !state.visible ) ) - { - _visibleFolders[ folder ] = ( true, true ); - folder = folder.Parent; - } - } - - private void UpdateFolders() - { - _visibleFolders.Clear(); - - for( var i = 0; i < _modsInOrder.Count; ++i ) - { - if( _visibleMods[ i ].visible ) - { - SetFolderAndParentsVisible( _modsInOrder[ i ].Data.SortOrder.ParentFolder ); - } - } - } - - public void SetTextFilter( string filter ) - { - var lower = filter.ToLowerInvariant(); - if( lower.StartsWith( "c:" ) ) - { - _modFilterChanges = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterAuthor = string.Empty; - } - else if( lower.StartsWith( "a:" ) ) - { - _modFilterAuthor = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterChanges = string.Empty; - } - else - { - _modFilter = lower; - _modFilterAuthor = string.Empty; - _modFilterChanges = string.Empty; - } - - ResetFilters(); - } - - private void ResetModList() - { - _modsInOrder.Clear(); - _visibleMods.Clear(); - _visibleFolders.Clear(); - - PluginLog.Debug( "Resetting mod selector list..." ); - if( _modsInOrder.Count == 0 ) - { - foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) - { - var mod = Penumbra.CollectionManager.Current.GetMod( modData ); - _modsInOrder.Add( mod ); - _visibleMods.Add( CheckFilters( mod ) ); - } - } - - _listResetNecessary = false; - _filterResetNecessary = false; - } - - private void ResetFilters() - { - _visibleMods.Clear(); - _visibleFolders.Clear(); - PluginLog.Debug( "Resetting mod selector filters..." ); - foreach( var mod in _modsInOrder ) - { - _visibleMods.Add( CheckFilters( mod ) ); - } - - _filterResetNecessary = false; - } - - public (Mod.Mod? mod, int idx) GetModByName( string name ) - { - for( var i = 0; i < Count; ++i ) - { - if( _modsInOrder[ i ].Data.Meta.Name == name ) - { - return ( _modsInOrder[ i ], i ); - } - } - - return ( null, 0 ); - } - - public (Mod.Mod? mod, int idx) GetModByBasePath( string basePath ) - { - for( var i = 0; i < Count; ++i ) - { - if( _modsInOrder[ i ].Data.BasePath.Name == basePath ) - { - return ( _modsInOrder[ i ], i ); - } - } - - return ( null, 0 ); - } - - public (bool visible, bool enabled) GetFolder( ModFolder folder ) - => _visibleFolders.TryGetValue( folder, out var ret ) ? ret : ( false, false ); - - public (Mod.Mod?, bool visible, uint color) GetMod( int idx ) - => idx >= 0 && idx < _modsInOrder.Count - ? ( _modsInOrder[ idx ], _visibleMods[ idx ].visible, _visibleMods[ idx ].color ) - : ( null, false, 0 ); - - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) - { - if( count == 0 ) - { - if( StateFilter.HasFlag( hasNoFlag ) ) - { - return false; - } - } - else if( StateFilter.HasFlag( hasFlag ) ) - { - return false; - } - return true; } - private (bool, uint) CheckFilters( Mod.Mod mod ) + if( _filterResetNecessary ) { - var ret = ( false, 0u ); + ResetFilters(); + return true; + } - if( _modFilter.Any() && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) + return false; + } + + public void TriggerListReset() + => _listResetNecessary = true; + + public void TriggerFilterReset() + => _filterResetNecessary = true; + + public void RemoveMod( FullMod mod ) + { + var idx = _modsInOrder.IndexOf( mod ); + if( idx >= 0 ) + { + _modsInOrder.RemoveAt( idx ); + _visibleMods.RemoveAt( idx ); + UpdateFolders(); + } + } + + private void SetFolderAndParentsVisible( ModFolder? folder ) + { + while( folder != null && ( !_visibleFolders.TryGetValue( folder, out var state ) || !state.visible ) ) + { + _visibleFolders[ folder ] = ( true, true ); + folder = folder.Parent; + } + } + + private void UpdateFolders() + { + _visibleFolders.Clear(); + + for( var i = 0; i < _modsInOrder.Count; ++i ) + { + if( _visibleMods[ i ].visible ) + { + SetFolderAndParentsVisible( _modsInOrder[ i ].Data.Order.ParentFolder ); + } + } + } + + public void SetTextFilter( string filter ) + { + var lower = filter.ToLowerInvariant(); + if( lower.StartsWith( "c:" ) ) + { + _modFilterChanges = lower[ 2.. ]; + _modFilter = string.Empty; + _modFilterAuthor = string.Empty; + } + else if( lower.StartsWith( "a:" ) ) + { + _modFilterAuthor = lower[ 2.. ]; + _modFilter = string.Empty; + _modFilterChanges = string.Empty; + } + else + { + _modFilter = lower; + _modFilterAuthor = string.Empty; + _modFilterChanges = string.Empty; + } + + ResetFilters(); + } + + private void ResetModList() + { + _modsInOrder.Clear(); + _visibleMods.Clear(); + _visibleFolders.Clear(); + + PluginLog.Debug( "Resetting mod selector list..." ); + if( _modsInOrder.Count == 0 ) + { + foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) + { + var idx = Penumbra.ModManager.Mods.IndexOf( modData ); + var mod = new FullMod( Penumbra.CollectionManager.Current[ idx ].Settings ?? ModSettings.DefaultSettings( modData.Meta ), + modData ); + _modsInOrder.Add( mod ); + _visibleMods.Add( CheckFilters( mod ) ); + } + } + + _listResetNecessary = false; + _filterResetNecessary = false; + } + + private void ResetFilters() + { + _visibleMods.Clear(); + _visibleFolders.Clear(); + PluginLog.Debug( "Resetting mod selector filters..." ); + foreach( var mod in _modsInOrder ) + { + _visibleMods.Add( CheckFilters( mod ) ); + } + + _filterResetNecessary = false; + } + + public (FullMod? mod, int idx) GetModByName( string name ) + { + for( var i = 0; i < Count; ++i ) + { + if( _modsInOrder[ i ].Data.Meta.Name == name ) + { + return ( _modsInOrder[ i ], i ); + } + } + + return ( null, 0 ); + } + + public (FullMod? mod, int idx) GetModByBasePath( string basePath ) + { + for( var i = 0; i < Count; ++i ) + { + if( _modsInOrder[ i ].Data.BasePath.Name == basePath ) + { + return ( _modsInOrder[ i ], i ); + } + } + + return ( null, 0 ); + } + + public (bool visible, bool enabled) GetFolder( ModFolder folder ) + => _visibleFolders.TryGetValue( folder, out var ret ) ? ret : ( false, false ); + + public (FullMod?, bool visible, uint color) GetMod( int idx ) + => idx >= 0 && idx < _modsInOrder.Count + ? ( _modsInOrder[ idx ], _visibleMods[ idx ].visible, _visibleMods[ idx ].color ) + : ( null, false, 0 ); + + private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + { + if( count == 0 ) + { + if( StateFilter.HasFlag( hasNoFlag ) ) + { + return false; + } + } + else if( StateFilter.HasFlag( hasFlag ) ) + { + return false; + } + + return true; + } + + private (bool, uint) CheckFilters( FullMod mod ) + { + var ret = ( false, 0u ); + + if( _modFilter.Length > 0 && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) + { + return ret; + } + + if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) ) + { + return ret; + } + + if( _modFilterChanges.Length > 0 && !mod.Data.LowerChangedItemsString.Contains( _modFilterChanges ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, + ModFilter.HasMetaManipulations ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) ) + { + return ret; + } + + var isNew = _newMods.Contains( mod.Data.BasePath.Name ); + if( CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) + { + return ret; + } + + if( !mod.Settings.Enabled ) + { + if( !StateFilter.HasFlag( ModFilter.Disabled ) || !StateFilter.HasFlag( ModFilter.NoConflict ) ) { return ret; } - if( _modFilterAuthor.Any() && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) ) - { - return ret; - } + ret.Item2 = ret.Item2 == 0 ? DisabledModColor : ret.Item2; + } - if( _modFilterChanges.Any() && !mod.Data.LowerChangedItemsString.Contains( _modFilterChanges ) ) - { - return ret; - } + if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) + { + return ret; + } - if( CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) ) + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Data.Index ).ToList(); + if( conflicts.Count > 0 ) + { + if( conflicts.Any( c => !c.Solved ) ) { - return ret; - } - - if( CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, - ModFilter.HasMetaManipulations ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) ) - { - return ret; - } - - var isNew = _newMods.Contains( mod.Data.BasePath.Name ); - if( CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) - { - return ret; - } - - if( !mod.Settings.Enabled ) - { - if( !StateFilter.HasFlag( ModFilter.Disabled ) || !StateFilter.HasFlag( ModFilter.NoConflict ) ) + if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) { return ret; } - ret.Item2 = ret.Item2 == 0 ? DisabledModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? ConflictingModColor : ret.Item2; } - - if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) + else { - return ret; - } - - if( mod.Cache.Conflicts.Any() ) - { - if( mod.Cache.Conflicts.Keys.Any( m => m.Settings.Priority == mod.Settings.Priority ) ) + if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) { - if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return ret; - } - - ret.Item2 = ret.Item2 == 0 ? ConflictingModColor : ret.Item2; + return ret; } - else - { - if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return ret; - } - ret.Item2 = ret.Item2 == 0 ? HandledConflictModColor : ret.Item2; - } + ret.Item2 = ret.Item2 == 0 ? HandledConflictModColor : ret.Item2; } - else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return ret; - } - - ret.Item1 = true; - if( isNew ) - { - ret.Item2 = NewModColor; - } - - SetFolderAndParentsVisible( mod.Data.SortOrder.ParentFolder ); + } + else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) + { return ret; } + + ret.Item1 = true; + if( isNew ) + { + ret.Item2 = NewModColor; + } + + SetFolderAndParentsVisible( mod.Data.Order.ParentFolder ); + return ret; } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 3843fdb6..d3444394 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; +using Penumbra.Meta.Manipulations; using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; @@ -119,17 +120,12 @@ public partial class SettingsInterface } // This is only drawn when we have a mod selected, so we can forgive nulls. - private Mod.Mod Mod + private FullMod Mod => _selector.Mod!; private ModMeta Meta => Mod.Data.Meta; - private void Save() - { - Penumbra.CollectionManager.CurrentCollection.Save(); - } - private void DrawAboutTab() { if( !_editMode && Meta.Description.Length == 0 ) @@ -189,7 +185,8 @@ public partial class SettingsInterface private void DrawConflictTab() { - if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( Mod.Data.Index ).ToList(); + if( conflicts.Count == 0 || !ImGui.BeginTabItem( LabelConflictsTab ) ) { return; } @@ -203,32 +200,43 @@ public partial class SettingsInterface } raii.Push( ImGui.EndListBox ); - using var indent = ImGuiRaii.PushIndent( 0 ); - foreach( var (mod, (files, manipulations)) in Mod.Cache.Conflicts ) + using var indent = ImGuiRaii.PushIndent( 0 ); + Mod.Mod? oldBadMod = null; + foreach( var conflict in conflicts ) { - if( ImGui.Selectable( mod.Data.Meta.Name ) ) + var badMod = Penumbra.ModManager[ conflict.Mod2 ]; + if( badMod != oldBadMod ) { - _selector.SelectModByDir( mod.Data.BasePath.Name ); + if( oldBadMod != null ) + { + indent.Pop( 30f ); + } + + if( ImGui.Selectable( badMod.Meta.Name ) ) + { + _selector.SelectModByDir( badMod.BasePath.Name ); + } + + ImGui.SameLine(); + using var color = ImGuiRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorGreen : ColorRed ); + ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); + + indent.Push( 30f ); } - ImGui.SameLine(); - ImGui.Text( $"(Priority {mod.Settings.Priority})" ); - - indent.Push( 15f ); - foreach( var file in files ) + if( conflict.Conflict is Utf8GamePath p ) { unsafe { - ImGuiNative.igSelectable_Bool( file.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); + ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); } } - - foreach( var manip in manipulations ) + else if( conflict.Conflict is MetaManipulation m ) { - //ImGui.Text( manip.IdentifierString() ); + ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); } - indent.Pop( 15f ); + oldBadMod = badMod; } } @@ -421,11 +429,12 @@ public partial class SettingsInterface { _fullFilenameList = null; _selector.SaveCurrentMod(); + var idx = Penumbra.ModManager.Mods.IndexOf( Mod.Data ); // Since files may have changed, we need to recompute effective files. - foreach( var collection in Penumbra.CollectionManager.Collections - .Where( c => c.Cache != null && c.Settings[ Mod.Data.BasePath.Name ].Enabled ) ) + foreach( var collection in Penumbra.CollectionManager + .Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( false, Penumbra.CollectionManager.IsActive( collection ) ); + collection.CalculateEffectiveFileList( false, collection == Penumbra.CollectionManager.Default ); } // If the mod is enabled in the current collection, its conflicts may have changed. @@ -553,12 +562,12 @@ public partial class SettingsInterface var oldEnabled = enabled; if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) { - Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; - Save(); + Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, + Mod.Settings.Settings[ group.GroupName ] ^ ( 1 << idx ) ); // If the mod is enabled, recalculate files and filters. if( Mod.Settings.Enabled ) { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + _selector.Cache.TriggerFilterReset(); } } } @@ -591,12 +600,10 @@ public partial class SettingsInterface , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) && code != Mod.Settings.Settings[ group.GroupName ] ) { - Mod.Settings.Settings[ group.GroupName ] = code; - Save(); - // If the mod is enabled, recalculate files and filters. + Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, code ); if( Mod.Settings.Enabled ) { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + _selector.Cache.TriggerFilterReset(); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 3329a554..5bb4acaa 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -209,7 +209,7 @@ public partial class SettingsInterface { if( newName.Length > 0 ) { - Mod.Settings.Settings[ group.GroupName ] = code; + Penumbra.CollectionManager.Current.SetModSetting(Mod.Data.Index, group.GroupName, code); group.Options.Add( new Option() { OptionName = newName, @@ -245,11 +245,6 @@ public partial class SettingsInterface } } - if( code != oldSetting ) - { - Save(); - } - ImGui.SameLine(); var labelEditPos = ImGui.GetCursorPosX(); DrawSingleSelectorEditGroup( group ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 10b85e3d..e6f3b170 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -69,7 +69,7 @@ public partial class SettingsInterface _currentWebsite = Meta?.Website ?? ""; } - private Mod.Mod? Mod + private Mod.FullMod? Mod => _selector.Mod; private ModMeta? Meta @@ -205,8 +205,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) { - Mod.Settings.Priority = priority; - _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); + Penumbra.CollectionManager.Current.SetModPriority( Mod.Data.Index, priority ); _selector.Cache.TriggerFilterReset(); } @@ -220,24 +219,18 @@ public partial class SettingsInterface var enabled = Mod!.Settings.Enabled; if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { - Mod.Settings.Enabled = enabled; + Penumbra.CollectionManager.Current.SetModState( Mod.Data.Index, enabled ); if( enabled ) { _newMods.Remove( Mod.Data.BasePath.Name ); } - else - { - Mod.Cache.ClearConflicts(); - } - - _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); _selector.Cache.TriggerFilterReset(); } } - public static bool DrawSortOrder( ModData mod, ModManager manager, Selector selector ) + public static bool DrawSortOrder( Mod.Mod mod, Mod.Mod.Manager manager, Selector selector ) { - var currentSortOrder = mod.SortOrder.FullPath; + var currentSortOrder = mod.Order.FullPath; ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) { diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 1ae71c8b..5984ac48 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -410,11 +410,11 @@ public partial class SettingsInterface // Selection private partial class Selector { - public Mod.Mod? Mod { get; private set; } + public Mod.FullMod? Mod { get; private set; } private int _index; private string _nextDir = string.Empty; - private void SetSelection( int idx, Mod.Mod? info ) + private void SetSelection( int idx, Mod.FullMod? info ) { Mod = info; if( idx != _index ) @@ -480,7 +480,7 @@ public partial class SettingsInterface private partial class Selector { // === Mod === - private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) + private void DrawModOrderPopup( string popupName, Mod.FullMod mod, bool firstOpen ) { if( !ImGui.BeginPopup( popupName ) ) { @@ -496,7 +496,7 @@ public partial class SettingsInterface if( firstOpen ) { - ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.FullPath.Length - 1 ); + ImGui.SetKeyboardFocusHere( mod.Data.Order.FullPath.Length - 1 ); } } @@ -527,10 +527,10 @@ public partial class SettingsInterface } Cache.TriggerFilterReset(); - var collection = Penumbra.CollectionManager.CurrentCollection; - if( collection.Cache != null ) + var collection = Penumbra.CollectionManager.Current; + if( collection.HasCache ) { - collection.CalculateEffectiveFileList( metaManips, Penumbra.CollectionManager.IsActive( collection ) ); + collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.CollectionManager.Default ); } collection.Save(); @@ -607,9 +607,9 @@ public partial class SettingsInterface Cache = new ModListCache( Penumbra.ModManager, newMods ); } - private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection2 collection ) + private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) { - if( collection == ModCollection2.Empty + if( collection == ModCollection.Empty || collection == Penumbra.CollectionManager.Current ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); @@ -664,7 +664,7 @@ public partial class SettingsInterface idx += sub.TotalDescendantMods(); } } - else if( item is ModData _ ) + else if( item is Mod.Mod _ ) { var (mod, visible, color) = Cache.GetMod( idx ); if( mod != null && visible ) @@ -721,7 +721,7 @@ public partial class SettingsInterface } } - private void DrawMod( Mod.Mod mod, int modIndex, uint color ) + private void DrawMod( Mod.FullMod mod, int modIndex, uint color ) { using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); @@ -736,7 +736,7 @@ public partial class SettingsInterface firstOpen = true; } - DragDropTarget( mod.Data.SortOrder.ParentFolder ); + DragDropTarget( mod.Data.Order.ParentFolder ); DragDropSourceMod( modIndex, mod.Data.Meta.Name ); DrawModOrderPopup( popupName, mod, firstOpen ); diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 8939be48..b406da71 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -55,26 +55,6 @@ public partial class SettingsInterface : IDisposable _menu.InstalledTab.Selector.Cache.TriggerListReset(); } - private void SaveCurrentCollection( bool recalculateMeta ) - { - var current = Penumbra.CollectionManager.CurrentCollection; - current.Save(); - RecalculateCurrent( recalculateMeta ); - } - - private void RecalculateCurrent( bool recalculateMeta ) - { - var current = Penumbra.CollectionManager.CurrentCollection; - if( current.Cache != null ) - { - current.CalculateEffectiveFileList( recalculateMeta, Penumbra.CollectionManager.IsActive( current ) ); - _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); - } - } - public void ResetDefaultCollection() => _menu.CollectionsTab.UpdateDefaultIndex(); - - public void ResetForcedCollection() - => _menu.CollectionsTab.UpdateForcedIndex(); } \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 6b2378f0..47d26e1b 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -74,7 +74,7 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( ModData mod, string from, string to ) + public static bool ChangeModMaterials( Mod.Mod mod, string from, string to ) { if( ValidStrings( from, to ) ) { From 7915d516e245af159b2f40514f40206f85a1ebb7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 28 Mar 2022 15:54:18 +0200 Subject: [PATCH 0125/2451] Imc Fixes. --- Penumbra/Interop/MetaFileManager.cs | 101 +++++++++++++++++++++++ Penumbra/Meta/Files/ImcFile.cs | 34 ++------ Penumbra/Meta/Files/MetaBaseFile.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 4 +- 4 files changed, 112 insertions(+), 31 deletions(-) create mode 100644 Penumbra/Interop/MetaFileManager.cs diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs new file mode 100644 index 00000000..c6b5241a --- /dev/null +++ b/Penumbra/Interop/MetaFileManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop; + +public unsafe class MetaFileManager : IDisposable +{ + public MetaFileManager() + { + SignatureHelper.Initialise( this ); + InitImc(); + } + + public void Dispose() + { + DisposeImc(); + } + + + // Allocate in the games space for file storage. + // We only need this if using any meta file. +#if USE_IMC || USE_CMP || USE_EQDP || USE_EQP || USE_EST || USE_GMP + [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] + public IntPtr GetFileSpaceAddress; +#endif + public IMemorySpace* GetFileSpace() + => ( ( delegate* unmanaged< IMemorySpace* > )GetFileSpaceAddress )(); + + public void* AllocateFileMemory( ulong length, ulong alignment = 0 ) + => GetFileSpace()->Malloc( length, alignment ); + + public void* AllocateFileMemory( int length, int alignment = 0 ) + => AllocateFileMemory( ( ulong )length, ( ulong )alignment ); + + + // We only need this for IMC files, since we need to hook their cleanup function. +#if USE_IMC + [Signature( "48 8D 05 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 89 03", ScanType = ScanType.StaticAddress )] + public IntPtr* DefaultResourceHandleVTable; +#endif + + public delegate void ClearResource( ResourceHandle* resource ); + public Hook< ClearResource > ClearDefaultResourceHook = null!; + + private readonly Dictionary< IntPtr, (IntPtr, int) > _originalImcData = new(); + + // We store the original data of loaded IMCs so that we can restore it before they get destroyed, + // similar to the other meta files, just with arbitrary destruction. + private void ClearDefaultResourceDetour( ResourceHandle* resource ) + { + if( _originalImcData.TryGetValue( ( IntPtr )resource, out var data ) ) + { + PluginLog.Debug( "Restoring data of {$Name:l} (0x{Resource}) to 0x{Data:X} and Length {Length} before deletion.", + Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource, ( ulong )data.Item1, data.Item2 ); + resource->SetData( data.Item1, data.Item2 ); + _originalImcData.Remove( ( IntPtr )resource ); + } + + ClearDefaultResourceHook.Original( resource ); + } + + // Called when a new IMC is manipulated to store its data. + [Conditional( "USE_IMC" )] + public void AddImcFile( ResourceHandle* resource, IntPtr data, int length ) + { + PluginLog.Debug( "Storing data 0x{Data:X} of Length {Length} for {$Name:l} (0x{Resource:X}).", ( ulong )data, length, + Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource ); + _originalImcData[ ( IntPtr )resource ] = ( data, length ); + } + + // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. + [Conditional( "USE_IMC" )] + private void InitImc() + { + ClearDefaultResourceHook = new Hook< ClearResource >( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); + ClearDefaultResourceHook.Enable(); + } + + [Conditional( "USE_IMC" )] + private void DisposeImc() + { + ClearDefaultResourceHook.Disable(); + ClearDefaultResourceHook.Dispose(); + // Restore all IMCs to their default values on dispose. + // This should only be relevant when testing/disabling/reenabling penumbra. + foreach( var (resourcePtr, (data, length)) in _originalImcData ) + { + var resource = ( ResourceHandle* )resourcePtr; + resource->SetData( data, length ); + } + + _originalImcData.Clear(); + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 1a992a89..67be61e4 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.Interop; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; @@ -68,7 +69,7 @@ public unsafe class ImcFile : MetaBaseFile public readonly Utf8GamePath Path; public readonly int NumParts; - public bool ChangesSinceLoad = true; + public bool ChangesSinceLoad = false; public ReadOnlySpan< ImcEntry > Span => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); @@ -79,17 +80,6 @@ public unsafe class ImcFile : MetaBaseFile private static ushort PartMask( byte* data ) => *( ushort* )( data + 2 ); - private static ImcEntry* DefaultPartPtr( byte* data, int partIdx ) - { - var flag = 1 << partIdx; - if( ( PartMask( data ) & flag ) == 0 ) - { - return null; - } - - return ( ImcEntry* )( data + PreambleSize ) + partIdx; - } - private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) { var flag = 1 << partIdx; @@ -140,6 +130,7 @@ public unsafe class ImcFile : MetaBaseFile var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; PluginLog.Verbose( "Resized IMC {Path} from {Length} to {NewLength}.", Path, Length, newLength ); ResizeResources( newLength ); + ChangesSinceLoad = true; } var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); @@ -173,8 +164,7 @@ public unsafe class ImcFile : MetaBaseFile return false; } - *variantPtr = entry; - ChangesSinceLoad = true; + *variantPtr = entry; return true; } @@ -187,8 +177,6 @@ public unsafe class ImcFile : MetaBaseFile Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); } - - ChangesSinceLoad = true; } public ImcFile( Utf8GamePath path ) @@ -226,7 +214,7 @@ public unsafe class ImcFile : MetaBaseFile } } - public void Replace( ResourceHandle* resource ) + public void Replace( ResourceHandle* resource, bool firstTime ) { var (data, length) = resource->GetData(); if( data == IntPtr.Zero ) @@ -234,18 +222,10 @@ public unsafe class ImcFile : MetaBaseFile return; } - var requiredLength = ActualLength; resource->SetData( ( IntPtr )Data, Length ); - if( length >= requiredLength ) + if( firstTime ) { - Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); - Functions.MemSet( ( byte* )data + requiredLength, 0, length - requiredLength ); - return; + Penumbra.MetaFileManager.AddImcFile( resource, data, length ); } - - MemoryHelper.GameFree( ref data, ( ulong )length ); - var file = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )requiredLength ); - Functions.MemCpyUnchecked( file, Data, requiredLength ); - resource->SetData( ( IntPtr )file, requiredLength ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index e6e79740..303499a5 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -24,7 +24,7 @@ public unsafe class MetaBaseFile : IDisposable protected void AllocateData( int length ) { Length = length; - Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); + Data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( length ); if( length > 0 ) { GC.AddMemoryPressure( length ); @@ -53,7 +53,7 @@ public unsafe class MetaBaseFile : IDisposable return; } - var data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )newLength ); + var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); if( newLength > Length ) { Functions.MemCpyUnchecked( data, Data, Length ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index a4f32b76..c921049f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -145,7 +145,7 @@ public partial class MetaManager { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); - file.Replace( fileDescriptor->ResourceHandle ); + file.Replace( fileDescriptor->ResourceHandle, true); file.ChangesSinceLoad = false; } @@ -165,7 +165,7 @@ public partial class MetaManager PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, collection.Name ); - file.Replace( resource ); + file.Replace( resource, false ); file.ChangesSinceLoad = false; } } From 1861c40a4ff928428ed36d22d44f340322f739f3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 28 Mar 2022 17:25:59 +0200 Subject: [PATCH 0126/2451] Complete mod collection cleanup, initial stuff for inheritance. Some further cleanup. --- Penumbra/Api/PenumbraApi.cs | 4 +- .../Collections/CollectionManager.Active.cs | 245 +++++------ Penumbra/Collections/CollectionManager.cs | 235 ++++++---- Penumbra/Collections/ConflictCache.cs | 102 +++-- Penumbra/Collections/ModCollection.Cache.cs | 217 ++++----- Penumbra/Collections/ModCollection.Changes.cs | 21 +- Penumbra/Collections/ModCollection.File.cs | 127 ++++++ .../Collections/ModCollection.Inheritance.cs | 23 +- .../Collections/ModCollection.Migration.cs | 11 +- Penumbra/Collections/ModCollection.cs | 157 ++----- Penumbra/Configuration.cs | 6 +- Penumbra/Importer/Models/ExtendedModPack.cs | 2 +- Penumbra/Importer/TexToolsImport.cs | 2 +- .../Interop/Loader/ResourceLoader.Debug.cs | 27 +- .../Loader/ResourceLoader.Replacement.cs | 3 +- .../Interop/Resolver/PathResolver.Data.cs | 1 - .../Interop/Resolver/PathResolver.Material.cs | 1 - .../Interop/Resolver/PathResolver.Meta.cs | 1 - Penumbra/Interop/Resolver/PathResolver.cs | 1 - Penumbra/Meta/MetaCollection.cs | 2 +- Penumbra/MigrateConfiguration.cs | 7 +- Penumbra/{Mod => Mods}/FullMod.cs | 2 +- Penumbra/{Mod => Mods}/GroupInformation.cs | 2 +- Penumbra/{Mod => Mods}/Mod.SortOrder.cs | 3 +- Penumbra/{Mod => Mods}/Mod.cs | 5 +- Penumbra/{Mod => Mods}/ModCleanup.cs | 3 +- Penumbra/Mods/ModFileSystem.cs | 19 +- Penumbra/Mods/ModFolder.cs | 416 +++++++++--------- Penumbra/{Mod => Mods}/ModFunctions.cs | 2 +- Penumbra/Mods/ModManager.Directory.cs | 7 +- Penumbra/{Mod => Mods}/ModManager.cs | 3 +- Penumbra/Mods/ModManagerEditExtensions.cs | 15 +- Penumbra/{Mod => Mods}/ModMeta.cs | 41 +- Penumbra/{Mod => Mods}/ModResources.cs | 2 +- Penumbra/{Mod => Mods}/ModSettings.cs | 2 +- Penumbra/{Mod => Mods}/NamedModSettings.cs | 2 +- Penumbra/Penumbra.cs | 11 +- Penumbra/UI/MenuTabs/TabCollections.cs | 68 ++- Penumbra/UI/MenuTabs/TabEffective.cs | 62 ++- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 33 +- .../TabInstalled/TabInstalledDetails.cs | 7 +- .../TabInstalled/TabInstalledDetailsEdit.cs | 1 - .../TabInstalled/TabInstalledModPanel.cs | 9 +- .../TabInstalled/TabInstalledSelector.cs | 11 +- Penumbra/UI/MenuTabs/TabSettings.cs | 1 - Penumbra/Util/LowerString.cs | 122 +++++ Penumbra/Util/ModelChanger.cs | 4 +- Penumbra/Util/TempFile.cs | 1 - 48 files changed, 1151 insertions(+), 898 deletions(-) create mode 100644 Penumbra/Collections/ModCollection.File.cs rename Penumbra/{Mod => Mods}/FullMod.cs (96%) rename Penumbra/{Mod => Mods}/GroupInformation.cs (99%) rename Penumbra/{Mod => Mods}/Mod.SortOrder.cs (96%) rename Penumbra/{Mod => Mods}/Mod.cs (97%) rename Penumbra/{Mod => Mods}/ModCleanup.cs (99%) rename Penumbra/{Mod => Mods}/ModFunctions.cs (99%) rename Penumbra/{Mod => Mods}/ModManager.cs (99%) rename Penumbra/{Mod => Mods}/ModMeta.cs (80%) rename Penumbra/{Mod => Mods}/ModResources.cs (99%) rename Penumbra/{Mod => Mods}/ModSettings.cs (98%) rename Penumbra/{Mod => Mods}/NamedModSettings.cs (98%) create mode 100644 Penumbra/Util/LowerString.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7712c56c..cac23ecb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,8 +8,6 @@ using Lumina.Data; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Mod; -using Penumbra.Mods; namespace Penumbra.Api; @@ -78,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mod.Mod.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 17f11eae..30b9298d 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,8 +1,7 @@ -using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; @@ -14,122 +13,39 @@ public partial class ModCollection // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; - private int _currentIdx = 1; - private int _defaultIdx = 0; - private int _defaultNameIdx = 0; + // The collection currently selected for changing settings. + public ModCollection Current { get; private set; } = Empty; - public ModCollection Current - => this[ _currentIdx ]; + // The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } = Empty; - public ModCollection Default - => this[ _defaultIdx ]; + // A single collection that can not be deleted as a fallback for the current collection. + public ModCollection DefaultName { get; private set; } = Empty; - private readonly Dictionary< string, int > _character = new(); + // The list of character collections. + private readonly Dictionary< string, ModCollection > _characters = new(); + public IReadOnlyDictionary< string, ModCollection > Characters + => _characters; + + // If a name does not correspond to a character, return the default collection instead. public ModCollection Character( string name ) - => _character.TryGetValue( name, out var idx ) ? this[ idx ] : Default; - - public IEnumerable< (string, ModCollection) > Characters - => _character.Select( kvp => ( kvp.Key, this[ kvp.Value ] ) ); + => _characters.TryGetValue( name, out var c ) ? c : Default; public bool HasCharacterCollections - => _character.Count > 0; - - private void OnModChanged( Mod.Mod.ChangeType type, int idx, Mod.Mod mod ) - { - var meta = mod.Resources.MetaManipulations.Count > 0; - switch( type ) - { - case Mod.Mod.ChangeType.Added: - foreach( var collection in this ) - { - collection.AddMod( mod ); - } - - foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - case Mod.Mod.ChangeType.Removed: - var list = new List< ModSettings? >( _collections.Count ); - foreach( var collection in this ) - { - list.Add( collection[ idx ].Settings ); - collection.RemoveMod( mod, idx ); - } - - foreach( var (collection, _) in this.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - case Mod.Mod.ChangeType.Changed: - foreach( var collection in this.Where( - collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) - { - collection.Save(); - } - - foreach( var collection in this.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } - } - - private void CreateNecessaryCaches() - { - if( _defaultIdx > Empty.Index ) - { - Default.CreateCache(true); - } - - if( _currentIdx > Empty.Index ) - { - Current.CreateCache(false); - } - - foreach( var idx in _character.Values.Where( i => i > Empty.Index ) ) - { - _collections[ idx ].CreateCache(false); - } - } - - public void ForceCacheUpdates() - { - foreach( var collection in this ) - { - collection.ForceCacheUpdate(collection == Default); - } - } - - private void RemoveCache( int idx ) - { - if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) - { - _collections[ idx ].ClearCache(); - } - } - - public void SetCollection( ModCollection collection, Type type, string? characterName = null ) - => SetCollection( collection.Index, type, characterName ); + => _characters.Count > 0; + // Set a active collection, can be used to set Default, Current or Character collections. public void SetCollection( int newIdx, Type type, string? characterName = null ) { var oldCollectionIdx = type switch { - Type.Default => _defaultIdx, - Type.Current => _currentIdx, + Type.Default => Default.Index, + Type.Current => Current.Index, Type.Character => characterName?.Length > 0 - ? _character.TryGetValue( characterName, out var c ) - ? c - : _defaultIdx + ? _characters.TryGetValue( characterName, out var c ) + ? c.Index + : Default.Index : -1, _ => -1, }; @@ -142,24 +58,24 @@ public partial class ModCollection var newCollection = this[ newIdx ]; if( newIdx > Empty.Index ) { - newCollection.CreateCache(false); + newCollection.CreateCache( false ); } RemoveCache( oldCollectionIdx ); switch( type ) { case Type.Default: - _defaultIdx = newIdx; + Default = newCollection; Penumbra.Config.DefaultCollection = newCollection.Name; Penumbra.ResidentResources.Reload(); Default.SetFiles(); break; case Type.Current: - _currentIdx = newIdx; + Current = newCollection; Penumbra.Config.CurrentCollection = newCollection.Name; break; case Type.Character: - _character[ characterName! ] = newIdx; + _characters[ characterName! ] = newCollection; Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; break; } @@ -168,27 +84,32 @@ public partial class ModCollection Penumbra.Config.Save(); } + public void SetCollection( ModCollection collection, Type type, string? characterName = null ) + => SetCollection( collection.Index, type, characterName ); + + // Create a new character collection. Returns false if the character name already has a collection. public bool CreateCharacterCollection( string characterName ) { - if( _character.ContainsKey( characterName ) ) + if( _characters.ContainsKey( characterName ) ) { return false; } - _character[ characterName ] = Empty.Index; + _characters[ characterName ] = Empty; Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name; Penumbra.Config.Save(); CollectionChanged?.Invoke( null, Empty, Type.Character, characterName ); return true; } + // Remove a character collection if it exists. public void RemoveCharacterCollection( string characterName ) { - if( _character.TryGetValue( characterName, out var collection ) ) + if( _characters.TryGetValue( characterName, out var collection ) ) { - RemoveCache( collection ); - _character.Remove( characterName ); - CollectionChanged?.Invoke( this[ collection ], null, Type.Character, characterName ); + RemoveCache( collection.Index ); + _characters.Remove( characterName ); + CollectionChanged?.Invoke( collection, null, Type.Character, characterName ); } if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) @@ -197,36 +118,41 @@ public partial class ModCollection } } + // Obtain the index of a collection by name. private int GetIndexForCollectionName( string name ) - { - if( name.Length == 0 ) - { - return Empty.Index; - } + => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); - return _collections.IndexOf( c => c.Name == name ); - } + // Load default, current and character collections from config. + // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. public void LoadCollections() { var configChanged = false; - _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); - if( _defaultIdx < 0 ) + var defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + if( defaultIdx < 0 ) { PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); - _defaultIdx = Empty.Index; - Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; + Default = Empty; + Penumbra.Config.DefaultCollection = Default.Name; configChanged = true; } + else + { + Default = this[ defaultIdx ]; + } - _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); - if( _currentIdx < 0 ) + var currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + if( currentIdx < 0 ) { PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); - _currentIdx = _defaultNameIdx; - Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; + Current = DefaultName; + Penumbra.Config.DefaultCollection = Current.Name; configChanged = true; } + else + { + Current = this[ currentIdx ]; + } if( LoadCharacterCollections() || configChanged ) { @@ -236,6 +162,7 @@ public partial class ModCollection CreateNecessaryCaches(); } + // Load character collections. If a player name comes up multiple times, the last one is applied. private bool LoadCharacterCollections() { var configChanged = false; @@ -245,17 +172,71 @@ public partial class ModCollection if( idx < 0 ) { PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); - _character.Add( player, Empty.Index ); + _characters.Add( player, Empty ); Penumbra.Config.CharacterCollections[ player ] = Empty.Name; configChanged = true; } else { - _character.Add( player, idx ); + _characters.Add( player, this[ idx ] ); } } return configChanged; } + + + // Cache handling. + private void CreateNecessaryCaches() + { + Default.CreateCache( true ); + Current.CreateCache( false ); + + foreach( var collection in _characters.Values ) + { + collection.CreateCache( false ); + } + } + + private void RemoveCache( int idx ) + { + if( idx != Default.Index && idx != Current.Index && _characters.Values.All( c => c.Index != idx ) ) + { + _collections[ idx ].ClearCache(); + } + } + + private void ForceCacheUpdates() + { + foreach( var collection in this ) + { + collection.ForceCacheUpdate( collection == Default ); + } + } + + // Recalculate effective files for active collections on events. + private void OnModAddedActive( bool meta ) + { + foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } + + private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + { + foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } + + private void OnModChangedActive( bool meta, int modIdx ) + { + foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 440f88ac..76961f81 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; @@ -13,19 +14,23 @@ public partial class ModCollection { public enum Type : byte { - Inactive, - Default, - Character, - Current, + Inactive, // A collection was added or removed + Default, // The default collection was changed + Character, // A character collection was changed + Current, // The current collection was changed. } public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { + // On addition, oldCollection is null. On deletion, newCollection is null. + // CharacterName is onls set for type == Character. public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, Type type, string? characterName = null ); - private readonly Mod.Mod.Manager _modManager; + private readonly Mods.Mod.Manager _modManager; + // The empty collection is always available and always has index 0. + // It can not be deleted or moved. private readonly List< ModCollection > _collections = new() { Empty, @@ -34,25 +39,28 @@ public partial class ModCollection public ModCollection this[ Index idx ] => _collections[ idx ]; - public ModCollection this[ int idx ] - => _collections[ idx ]; - public ModCollection? this[ string name ] => ByName( name, out var c ) ? c : null; + public int Count + => _collections.Count; + + // Obtain a collection case-independently by name. public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + // Default enumeration skips the empty collection. public IEnumerator< ModCollection > GetEnumerator() => _collections.Skip( 1 ).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mod.Mod.Manager manager ) + public Manager( Mods.Mod.Manager manager ) { _modManager = manager; + // The collection manager reacts to changes in mods by itself. _modManager.ModsRediscovered += OnModsRediscovered; _modManager.ModChange += OnModChanged; ReadCollections(); @@ -65,27 +73,143 @@ public partial class ModCollection _modManager.ModChange -= OnModChanged; } + // Add a new collection of the given name. + // If duplicate is not-null, the new collection will be a duplicate of it. + // If the name of the collection would result in an already existing filename, skip it. + // Returns true if the collection was successfully created and fires a Inactive event. + // Also sets the current collection to the new collection afterwards. + public bool AddCollection( string name, ModCollection? duplicate ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed.Length == 0 + || nameFixed == Empty.Name.ToLowerInvariant() + || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); + newCollection.Index = _collections.Count; + _collections.Add( newCollection ); + newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); + SetCollection( newCollection.Index, Type.Current ); + return true; + } + + // Remove the given collection if it exists and is neither the empty nor the default-named collection. + // If the removed collection was active, it also sets the corresponding collection to the appropriate default. + public bool RemoveCollection( int idx ) + { + if( idx <= Empty.Index || idx >= _collections.Count ) + { + PluginLog.Error( "Can not remove the empty collection." ); + return false; + } + + if( idx == DefaultName.Index ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( idx == Current.Index ) + { + SetCollection( DefaultName, Type.Current ); + } + + if( idx == Default.Index ) + { + SetCollection( Empty, Type.Default ); + } + + foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) + { + SetCollection( Empty, Type.Character, characterName ); + } + + var collection = _collections[ idx ]; + collection.Delete(); + _collections.RemoveAt( idx ); + for( var i = idx; i < _collections.Count; ++i ) + { + --_collections[ i ].Index; + } + + CollectionChanged?.Invoke( collection, null, Type.Inactive ); + return true; + } + + public bool RemoveCollection( ModCollection collection ) + => RemoveCollection( collection.Index ); + + private void OnModsRediscovered() { + // When mods are rediscovered, force all cache updates and set the files of the default collection. ForceCacheUpdates(); Default.SetFiles(); } + + // A changed mod forces changes for all collections, active and inactive. + private void OnModChanged( Mod.ChangeType type, int idx, Mod mod ) + { + switch( type ) + { + case Mod.ChangeType.Added: + foreach( var collection in this ) + { + collection.AddMod( mod ); + } + + OnModAddedActive( mod.Resources.MetaManipulations.Count > 0 ); + break; + case Mod.ChangeType.Removed: + var settings = new List< ModSettings? >( _collections.Count ); + foreach( var collection in this ) + { + settings.Add( collection[ idx ].Settings ); + collection.RemoveMod( mod, idx ); + } + + OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); + break; + case Mod.ChangeType.Changed: + foreach( var collection in this.Where( + collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + { + collection.Save(); + } + + OnModChangedActive( mod.Resources.MetaManipulations.Count > 0, mod.Index ); + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + // Add the collection with the default name if it does not exist. + // It should always be ensured that it exists, otherwise it will be created. + // This can also not be deleted, so there are always at least the empty and a collection with default name. private void AddDefaultCollection() { - var idx = _collections.IndexOf( c => c.Name == DefaultCollection ); + var idx = GetIndexForCollectionName( DefaultCollection ); if( idx >= 0 ) { - _defaultNameIdx = idx; + DefaultName = this[ idx ]; return; } var defaultCollection = CreateNewEmpty( DefaultCollection ); defaultCollection.Save(); - _defaultNameIdx = _collections.Count; + defaultCollection.Index = _collections.Count; _collections.Add( defaultCollection ); } + // Inheritances can not be setup before all collections are read, + // so this happens after reading the collections. + // During this iteration, we can also fix all settings that are not valid for the given mod anymore. private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) { foreach( var (collection, inheritance) in this.Zip( inheritances ) ) @@ -117,6 +241,9 @@ public partial class ModCollection } } + // Read all collection files in the Collection Directory. + // Ensure that the default named collection exists, and apply inheritances afterwards. + // Duplicate collection files are not deleted, just not added here. private void ReadCollections() { var collectionDir = new DirectoryInfo( CollectionDirectory ); @@ -152,89 +279,5 @@ public partial class ModCollection AddDefaultCollection(); ApplyInheritancesAndFixSettings( inheritances ); } - - public bool AddCollection( string name, ModCollection? duplicate ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 - || nameFixed == Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) - { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); - newCollection.Index = _collections.Count; - _collections.Add( newCollection ); - newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); - SetCollection( newCollection.Index, Type.Current ); - return true; - } - - public bool RemoveCollection( ModCollection collection ) - => RemoveCollection( collection.Index ); - - public bool RemoveCollection( int idx ) - { - if( idx <= Empty.Index || idx >= _collections.Count ) - { - PluginLog.Error( "Can not remove the empty collection." ); - return false; - } - - if( idx == _defaultNameIdx ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - if( idx == _currentIdx ) - { - SetCollection( _defaultNameIdx, Type.Current ); - } - else if( _currentIdx > idx ) - { - --_currentIdx; - } - - if( idx == _defaultIdx ) - { - SetCollection( -1, Type.Default ); - } - else if( _defaultIdx > idx ) - { - --_defaultIdx; - } - - if( _defaultNameIdx > idx ) - { - --_defaultNameIdx; - } - - foreach( var (characterName, characterIdx) in _character.ToList() ) - { - if( idx == characterIdx ) - { - SetCollection( -1, Type.Character, characterName ); - } - else if( characterIdx > idx ) - { - _character[ characterName ] = characterIdx - 1; - } - } - - var collection = _collections[ idx ]; - collection.Delete(); - _collections.RemoveAt( idx ); - for( var i = idx; i < _collections.Count; ++i ) - { - --_collections[ i ].Index; - } - - CollectionChanged?.Invoke( collection, null, Type.Inactive ); - return true; - } } } \ No newline at end of file diff --git a/Penumbra/Collections/ConflictCache.cs b/Penumbra/Collections/ConflictCache.cs index 85db330d..1891a8bb 100644 --- a/Penumbra/Collections/ConflictCache.cs +++ b/Penumbra/Collections/ConflictCache.cs @@ -8,24 +8,26 @@ namespace Penumbra.Collections; public struct ConflictCache { - public readonly struct ModCacheStruct : IComparable< ModCacheStruct > + // A conflict stores all data about a mod conflict. + public readonly struct Conflict : IComparable< Conflict > { - public readonly object Conflict; + public readonly object Data; public readonly int Mod1; public readonly int Mod2; public readonly bool Mod1Priority; public readonly bool Solved; - public ModCacheStruct( int modIdx1, int modIdx2, int priority1, int priority2, object conflict ) + public Conflict( int modIdx1, int modIdx2, bool priority, bool solved, object data ) { Mod1 = modIdx1; Mod2 = modIdx2; - Conflict = conflict; - Mod1Priority = priority1 >= priority2; - Solved = priority1 != priority2; + Data = data; + Mod1Priority = priority; + Solved = solved; } - public int CompareTo( ModCacheStruct other ) + // Order: Mod1 -> Mod1 overwritten -> Mod2 -> File > MetaManipulation + public int CompareTo( Conflict other ) { var idxComp = Mod1.CompareTo( other.Mod1 ); if( idxComp != 0 ) @@ -44,55 +46,85 @@ public struct ConflictCache return idxComp; } - return Conflict switch + return Data switch { - Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ), - Utf8GamePath => -1, - MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ), - MetaManipulation => 1, - _ => 0, + Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ), + Utf8GamePath => -1, + MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ), + MetaManipulation => 1, + _ => 0, }; } } - private List< ModCacheStruct >? _conflicts; + private readonly List< Conflict > _conflicts = new(); + private bool _isSorted = true; - public IReadOnlyList< ModCacheStruct > Conflicts - => _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); + public ConflictCache() + { } - public IEnumerable< ModCacheStruct > ModConflicts( int modIdx ) + public IReadOnlyList< Conflict > Conflicts { - return _conflicts?.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ) - ?? Array.Empty< ModCacheStruct >(); + get + { + Sort(); + return _conflicts; + } } - public void Sort() - => _conflicts?.Sort(); + // Find all mod conflicts concerning the specified mod (in both directions). + public IEnumerable< Conflict > ModConflicts( int modIdx ) + { + return _conflicts.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ); + } + + private void Sort() + { + if( !_isSorted ) + { + _conflicts?.Sort(); + } + } + + // Add both directions for the mod. + // On same priority, it is assumed that mod1 is the earlier one. + // Also update older conflicts to refer to the highest-prioritized conflict. + private void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, object data ) + { + var solved = priority1 != priority2; + var priority = priority1 >= priority2; + var prioritizedMod = priority ? modIdx1 : modIdx2; + _conflicts.Add( new Conflict( modIdx1, modIdx2, priority, solved, data ) ); + _conflicts.Add( new Conflict( modIdx2, modIdx1, !priority, solved, data ) ); + for( var i = 0; i < _conflicts.Count; ++i ) + { + var c = _conflicts[ i ]; + if( data.Equals( c.Data ) ) + { + _conflicts[ i ] = c.Mod1Priority + ? new Conflict( prioritizedMod, c.Mod2, true, c.Solved || solved, data ) + : new Conflict( c.Mod1, prioritizedMod, false, c.Solved || solved, data ); + } + } + + _isSorted = false; + } public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath ) - { - _conflicts ??= new List< ModCacheStruct >( 2 ); - - _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) ); - _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) ); - } + => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath ); public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) - { - _conflicts ??= new List< ModCacheStruct >( 2 ); - _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) ); - _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) ); - } + => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation ); public void ClearConflicts() => _conflicts?.Clear(); public void ClearFileConflicts() - => _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath ); + => _conflicts?.RemoveAll( m => m.Data is Utf8GamePath ); public void ClearMetaConflicts() - => _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation ); + => _conflicts?.RemoveAll( m => m.Data is MetaManipulation ); public void ClearConflictsWithMod( int modIdx ) - => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx ); + => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == modIdx ); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 01a53f87..33726689 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -7,49 +7,53 @@ using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { + // Only active collections need to have a cache. private Cache? _cache; public bool HasCache => _cache != null; + // Only create, do not update. public void CreateCache( bool isDefault ) { - if( Index == 0 ) - { - return; - } - if( _cache == null ) { CalculateEffectiveFileList( true, isDefault ); } } + // Force an update with metadata for this cache. public void ForceCacheUpdate( bool isDefault ) => CalculateEffectiveFileList( true, isDefault ); + + // Clear the current cache. public void ClearCache() { _cache?.Dispose(); _cache = null; } + public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); + // Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile( Utf8GamePath path, FullPath fullPath ) => _cache!.ResolvedFiles[ path ] = fullPath; + // Force a file resolve to be removed. internal void RemoveFile( Utf8GamePath path ) => _cache!.ResolvedFiles.Remove( path ); + // Obtain data from the cache. internal MetaManager? MetaCache => _cache?.MetaManipulations; @@ -62,14 +66,17 @@ public partial class ModCollection internal IReadOnlyDictionary< string, object? > ChangedItems => _cache?.ChangedItems ?? new Dictionary< string, object? >(); - internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); + internal IReadOnlyList< ConflictCache.Conflict > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); - internal IEnumerable< ConflictCache.ModCacheStruct > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.ModCacheStruct >(); + internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); + // Update the effective file list for the given cache. + // Creates a cache if necessary. public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) { + // Skip the empty collection. if( Index == 0 ) { return; @@ -87,8 +94,84 @@ public partial class ModCollection { Penumbra.ResidentResources.Reload(); } + } - _cache.Conflicts.Sort(); + // Set Metadata files. + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + _cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + _cache.MetaManipulations.SetFiles(); + } } @@ -106,8 +189,9 @@ public partial class ModCollection public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new(); public readonly MetaManager MetaManipulations; - public ConflictCache Conflicts; + public ConflictCache Conflicts = new(); + // Obtain currently changed items. Computes them if they haven't been computed before. public IReadOnlyDictionary< string, object? > ChangedItems { get @@ -117,6 +201,7 @@ public partial class ModCollection } } + // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { _collection = collection; @@ -133,6 +218,8 @@ public partial class ModCollection private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) { + // Recompute the file list if it was not just a non-conflicting priority change + // or a setting change for a disabled mod. if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any() || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) { @@ -143,9 +230,12 @@ public partial class ModCollection _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); } + // Inheritance changes are too big to check for relevance, + // just recompute everything. private void OnInheritanceChange( bool _ ) => _collection.CalculateEffectiveFileList( true, true ); + // Reset the shared file-seen cache. private static void ResetFileSeen( int size ) { if( size < FileSeen.Length ) @@ -160,6 +250,8 @@ public partial class ModCollection } } + + // Clear all local and global caches to prepare for recomputation. private void ClearStorageAndPrepare() { ResolvedFiles.Clear(); @@ -167,15 +259,15 @@ public partial class ModCollection RegisteredFiles.Clear(); _changedItems.Clear(); ResolvedSettings.Clear(); + Conflicts.ClearFileConflicts(); + // Obtains actual settings for this collection with all inheritances. ResolvedSettings.AddRange( _collection.ActualSettings ); } public void CalculateEffectiveFileList() { ClearStorageAndPrepare(); - - Conflicts.ClearFileConflicts(); - for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) + for( var i = 0; i < Penumbra.ModManager.Count; ++i ) { if( ResolvedSettings[ i ]?.Enabled == true ) { @@ -185,7 +277,6 @@ public partial class ModCollection } AddMetaFiles(); - Conflicts.Sort(); } private void SetChangedItems() @@ -204,6 +295,7 @@ public partial class ModCollection { identifier.Identify( _changedItems, resolved.ToGamePath() ); } + // TODO: Meta Manipulations } catch( Exception e ) { @@ -211,12 +303,12 @@ public partial class ModCollection } } - private void AddFiles( int idx ) { var mod = Penumbra.ModManager.Mods[ idx ]; ResetFileSeen( mod.Resources.ModFiles.Count ); // Iterate in reverse so that later groups take precedence before earlier ones. + // TODO: add group priorities. foreach( var group in mod.Meta.Groups.Values.Reverse() ) { switch( group.SelectionType ) @@ -240,7 +332,6 @@ public partial class ModCollection => !Penumbra.Config.DisableSoundStreaming && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) { if( FilterFile( gamePath ) ) @@ -250,11 +341,13 @@ public partial class ModCollection if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) { + // No current conflict, just add. RegisteredFiles.Add( gamePath, modIdx ); ResolvedFiles[ gamePath ] = file; } else { + // Conflict, check which mod has higher priority, replace if necessary, add conflict. var priority = ResolvedSettings[ modIdx ]!.Priority; var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); @@ -270,6 +363,8 @@ public partial class ModCollection { switch( file.Extension.ToLowerInvariant() ) { + // We do not care for those file types + case ".scp" when !Penumbra.Config.DisableSoundStreaming: case ".meta": case ".rgsp": return; @@ -279,10 +374,11 @@ public partial class ModCollection } } - private void AddPathsForOption( Option option, Mod.Mod mod, int modIdx, bool enabled ) + private void AddPathsForOption( Option option, Mod mod, int modIdx, bool enabled ) { foreach( var (file, paths) in option.OptionFiles ) { + // TODO: complete rework of options. var fullPath = new FullPath( mod.BasePath, file ); var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); if( idx < 0 ) @@ -309,7 +405,7 @@ public partial class ModCollection } } - private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod, int modIdx ) + private void AddFilesForSingle( OptionGroup singleGroup, Mod mod, int modIdx ) { Debug.Assert( singleGroup.SelectionType == SelectType.Single ); var settings = ResolvedSettings[ modIdx ]!; @@ -324,7 +420,7 @@ public partial class ModCollection } } - private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod, int modIdx ) + private void AddFilesForMulti( OptionGroup multiGroup, Mod mod, int modIdx ) { Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); var settings = ResolvedSettings[ modIdx ]!; @@ -340,7 +436,7 @@ public partial class ModCollection } } - private void AddRemainingFiles( Mod.Mod mod, int modIdx ) + private void AddRemainingFiles( Mod mod, int modIdx ) { for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) { @@ -431,81 +527,4 @@ public partial class ModCollection return candidate; } } - - [Conditional( "USE_EQP" )] - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqp.SetFiles(); - } - } - - [Conditional( "USE_EQDP" )] - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqdp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqdp.SetFiles(); - } - } - - [Conditional( "USE_GMP" )] - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerGmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Gmp.SetFiles(); - } - } - - [Conditional( "USE_EST" )] - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEst.ResetFiles(); - } - else - { - _cache.MetaManipulations.Est.SetFiles(); - } - } - - [Conditional( "USE_CMP" )] - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerCmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Cmp.SetFiles(); - } - } - - public void SetFiles() - { - if( _cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - _cache.MetaManipulations.SetFiles(); - } - } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 2cfd00ef..f4c1b32e 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,18 +1,21 @@ using System; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Collections; +// Different types a mod setting can change: public enum ModSettingChange { - Inheritance, - EnableState, - Priority, - Setting, + Inheritance, // it was set to inherit from other collections or not inherit anymore + EnableState, // it was enabled or disabled + Priority, // its priority was changed + Setting, // a specific setting was changed } public partial class ModCollection { + // If the change type is a bool, oldValue will be 1 for true and 0 for false. + // optionName will only be set for type == Setting. public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ); public event ModSettingChangeDelegate ModSettingChanged; @@ -99,13 +102,13 @@ public partial class ModCollection private bool FixInheritance( int idx, bool inherit ) { var settings = _settings[ idx ]; - if( inherit != ( settings == null ) ) + if( inherit == ( settings == null ) ) { - _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); - return true; + return false; } - return false; + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + return true; } private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited ) diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs new file mode 100644 index 00000000..cad2c209 --- /dev/null +++ b/Penumbra/Collections/ModCollection.File.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +// File operations like saving, loading and deleting for a collection. +public partial class ModCollection +{ + public static string CollectionDirectory + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); + + // We need to remove all invalid path symbols from the collection name to be able to save it to file. + public FileInfo FileName + => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + + // Custom serialization due to shared mod information across managers. + public void Save() + { + try + { + var file = FileName; + file.Directory?.Create(); + using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew ); + using var w = new StreamWriter( s, Encoding.UTF8 ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); + j.WriteStartObject(); + j.WritePropertyName( nameof( Version ) ); + j.WriteValue( Version ); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( Name ); + j.WritePropertyName( nameof( Settings ) ); + + // Write all used and unused settings by mod directory name. + j.WriteStartObject(); + for( var i = 0; i < _settings.Count; ++i ) + { + var settings = _settings[ i ]; + if( settings != null ) + { + j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); + x.Serialize( j, settings ); + } + } + + foreach( var (modDir, settings) in _unusedSettings ) + { + j.WritePropertyName( modDir ); + x.Serialize( j, settings ); + } + + j.WriteEndObject(); + + // Inherit by collection name. + j.WritePropertyName( nameof( Inheritance ) ); + x.Serialize( j, Inheritance.Select( c => c.Name ) ); + j.WriteEndObject(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + } + } + + public void Delete() + { + if( Index == 0 ) + { + return; + } + + var file = FileName; + if( !file.Exists ) + { + return; + } + + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); + } + } + + // Since inheritances depend on other collections existing, + // we return them as a list to be applied after reading all collections. + public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + { + inheritance = Array.Empty< string >(); + if( !file.Exists ) + { + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + return null; + } + + try + { + var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); + var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; + var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + // Custom deserialization that is converted with the constructor. + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() + ?? new Dictionary< string, ModSettings >(); + inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + + return new ModCollection( name, version, settings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + } + + return null; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 158e0159..390c9c87 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,20 +1,26 @@ using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; +// ModCollections can inherit from an arbitrary number of other collections. +// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. +// Circular dependencies are resolved by distinctness. public partial class ModCollection { - private readonly List< ModCollection > _inheritance = new(); - + // A change in inheritance usually requires complete recomputation. public event Action< bool > InheritanceChanged; + private readonly List< ModCollection > _inheritance = new(); + public IReadOnlyList< ModCollection > Inheritance => _inheritance; + // Iterate over all collections inherited from in depth-first order. + // Skip already visited collections to avoid circular dependencies. public IEnumerable< ModCollection > GetFlattenedInheritance() { yield return this; @@ -27,6 +33,9 @@ public partial class ModCollection } } + // Add a new collection to the inheritance list. + // We do not check if this collection would be visited before, + // only that it is unique in the list itself. public bool AddInheritance( ModCollection collection ) { if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) @@ -35,6 +44,7 @@ public partial class ModCollection } _inheritance.Add( collection ); + // Changes in inherited collections may need to trigger further changes here. collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; InheritanceChanged.Invoke( false ); @@ -50,6 +60,7 @@ public partial class ModCollection InheritanceChanged.Invoke( false ); } + // Order in the inheritance list is relevant. public void MoveInheritance( int from, int to ) { if( _inheritance.Move( from, to ) ) @@ -58,6 +69,7 @@ public partial class ModCollection } } + // Carry changes in collections inherited from forward if they are relevant for this collection. private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) { if( _settings[ modIdx ] == null ) @@ -69,13 +81,16 @@ public partial class ModCollection private void OnInheritedInheritanceChange( bool _ ) => InheritanceChanged.Invoke( true ); + // Obtain the actual settings for a given mod via index. + // Also returns the collection the settings are taken from. + // If no collection provides settings for this mod, this collection is returned together with null. public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] { get { foreach( var collection in GetFlattenedInheritance() ) { - var settings = _settings[ idx ]; + var settings = collection._settings[ idx ]; if( settings != null ) { return ( settings, collection ); diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 19a89036..42903ea8 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,10 +1,12 @@ +using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Collections; public sealed partial class ModCollection { + // Migration to convert ModCollections from older versions to newer. private static class Migration { public static void Migrate( ModCollection collection ) @@ -24,9 +26,10 @@ public sealed partial class ModCollection } collection.Version = 1; + + // Remove all completely defaulted settings from active and inactive mods. for( var i = 0; i < collection._settings.Count; ++i ) { - var setting = collection._settings[ i ]; if( SettingIsDefaultV0( collection._settings[ i ] ) ) { collection._settings[ i ] = null; @@ -41,7 +44,11 @@ public sealed partial class ModCollection return true; } + // We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0( ModSettings? setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); } + + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + => new(name, 0, allSettings); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index ce90144d..9e4f7c60 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,21 +1,28 @@ -using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Text; -using Dalamud.Logging; -using Newtonsoft.Json.Linq; -using Penumbra.Mod; -using Penumbra.Util; +using Penumbra.Mods; namespace Penumbra.Collections; +public partial class ModCollection +{ + // Create the always available Empty Collection that will always sit at index 0, + // can not be deleted and does never create a cache. + private static ModCollection CreateEmpty() + { + var collection = CreateNewEmpty( EmptyCollection ); + collection.Index = 0; + collection._settings.Clear(); + return collection; + } +} + // A ModCollection is a named set of ModSettings to all of the users' installed mods. -// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. -// Active ModCollections build a cache of currently relevant data. +// Invariants: +// - Index is the collections index in the ModCollection.Manager +// - Settings has the same size as ModManager.Mods. +// - any change in settings or inheritance of the collection causes a Save. public partial class ModCollection { public const int CurrentVersion = 1; @@ -24,29 +31,28 @@ public partial class ModCollection public static readonly ModCollection Empty = CreateEmpty(); - private static ModCollection CreateEmpty() - { - var collection = CreateNewEmpty( EmptyCollection ); - collection.Index = 0; - collection._settings.Clear(); - return collection; - } - + // The collection name can contain invalid path characters, + // but after removing those and going to lower case it has to be unique. public string Name { get; private init; } public int Version { get; private set; } public int Index { get; private set; } = -1; + // If a ModSetting is null, it can be inherited from other collections. + // If no collection provides a setting for the mod, it is just disabled. private readonly List< ModSettings? > _settings; public IReadOnlyList< ModSettings? > Settings => _settings; + // Evaluates the settings along the whole inheritance tree. public IEnumerable< ModSettings? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + // Settings for deleted mods will be kept via directory name. private readonly Dictionary< string, ModSettings > _unusedSettings; + // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) { Name = name; @@ -58,6 +64,7 @@ public partial class ModCollection InheritanceChanged += SaveOnChange; } + // Constructor for reading from files. private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) { Name = name; @@ -79,15 +86,15 @@ public partial class ModCollection InheritanceChanged += SaveOnChange; } + // Create a new, unique empty collection of a given name. public static ModCollection CreateNewEmpty( string name ) => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) => new(name, this); - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) - => new(name, 0, allSettings); - + // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { var any = _unusedSettings.Count > 0; @@ -98,7 +105,8 @@ public partial class ModCollection } } - public void AddMod( Mod.Mod mod ) + // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + private void AddMod( Mods.Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -111,7 +119,8 @@ public partial class ModCollection } } - public void RemoveMod( Mod.Mod mod, int idx ) + // Move settings from the current mod list to the unused mod settings. + private void RemoveMod( Mods.Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) @@ -121,104 +130,4 @@ public partial class ModCollection _settings.RemoveAt( idx ); } - - public static string CollectionDirectory - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); - - public FileInfo FileName - => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); - - public void Save() - { - try - { - var file = FileName; - file.Directory?.Create(); - using var s = file.Open( FileMode.Truncate ); - using var w = new StreamWriter( s, Encoding.UTF8 ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); - j.WriteStartObject(); - j.WritePropertyName( nameof( Version ) ); - j.WriteValue( Version ); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( Name ); - j.WritePropertyName( nameof( Settings ) ); - j.WriteStartObject(); - for( var i = 0; i < _settings.Count; ++i ) - { - var settings = _settings[ i ]; - if( settings != null ) - { - j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, settings ); - } - } - - foreach( var settings in _unusedSettings ) - { - j.WritePropertyName( settings.Key ); - x.Serialize( j, settings.Value ); - } - - j.WriteEndObject(); - j.WritePropertyName( nameof( Inheritance ) ); - x.Serialize( j, Inheritance ); - j.WriteEndObject(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public void Delete() - { - if( Index == 0 ) - { - return; - } - - var file = FileName; - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); - } - } - } - - public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) - { - inheritance = Array.Empty< string >(); - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); - var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; - var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() - ?? new Dictionary< string, ModSettings >(); - inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); - - return new ModCollection( name, version, settings ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 56be9eb2..da2df11e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -5,8 +5,9 @@ using Dalamud.Logging; namespace Penumbra; + [Serializable] -public class Configuration : IPluginConfiguration +public partial class Configuration : IPluginConfiguration { private const int CurrentVersion = 1; @@ -36,7 +37,7 @@ public class Configuration : IPluginConfiguration public string CurrentCollection { get; set; } = "Default"; public string DefaultCollection { get; set; } = "Default"; - public string ForcedCollection { get; set; } = ""; + public bool SortFoldersFirst { get; set; } = false; public bool HasReadCharacterCollectionDesc { get; set; } = false; @@ -44,7 +45,6 @@ public class Configuration : IPluginConfiguration public Dictionary< string, string > CharacterCollections { get; set; } = new(); public Dictionary< string, string > ModSortOrder { get; set; } = new(); - public bool InvertModListOrder { internal get; set; } public static Configuration Load() { diff --git a/Penumbra/Importer/Models/ExtendedModPack.cs b/Penumbra/Importer/Models/ExtendedModPack.cs index c499ece3..45593faa 100644 --- a/Penumbra/Importer/Models/ExtendedModPack.cs +++ b/Penumbra/Importer/Models/ExtendedModPack.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Importer.Models { diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 46e3bcd5..a816d6a5 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -8,7 +8,7 @@ using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Importer.Models; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; using FileMode = System.IO.FileMode; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index f43067d0..ada899fd 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Loader; @@ -30,8 +29,7 @@ public unsafe partial class ResourceLoader public ResourceType Extension; } - private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); - private readonly List< (FullPath, DebugData?) > _deleteList = new(); + private readonly SortedList< FullPath, DebugData > _debugList = new(); public IReadOnlyDictionary< FullPath, DebugData > DebugList => _debugList; @@ -161,35 +159,22 @@ public unsafe partial class ResourceLoader public void UpdateDebugInfo() { - var manager = *ResourceManager; - _deleteList.Clear(); - foreach( var data in _debugList.Values ) + for( var i = 0; i < _debugList.Count; ++i ) { + var data = _debugList.Values[ i ]; var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); if( modifiedResource == null ) { - _deleteList.Add( ( data.ManipulatedPath, null ) ); + _debugList.RemoveAt( i-- ); } else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource ) { - _deleteList.Add( ( data.ManipulatedPath, data with + _debugList[ _debugList.Keys[ i ] ] = data with { OriginalResource = ( Structs.ResourceHandle* )regularResource, ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource, - } ) ); - } - } - - foreach( var (path, data) in _deleteList ) - { - if( data == null ) - { - _debugList.Remove( path ); - } - else - { - _debugList[ path ] = data.Value; + }; } } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index de7308a5..f7e78b12 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mod; using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -93,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Mod.Mod.Manager.ResolvePath( path ); + var resolved = Mods.Mod.Manager.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7a7b5e84..b0104fec 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -10,7 +10,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.Mods; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 27d3ebcd..e2d88fee 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -7,7 +7,6 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index b678028d..5ac6dd0e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -5,7 +5,6 @@ using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 7f2c89e7..844f5db9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,7 +5,6 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 7dc29720..d489702e 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Meta.Manipulations; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Meta; diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index be9df0d4..57060fc9 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -5,11 +5,16 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.Collections; -using Penumbra.Mod; using Penumbra.Mods; namespace Penumbra; +public partial class Configuration +{ + public string ForcedCollection { internal get; set; } = ""; + public bool InvertModListOrder { internal get; set; } +} + public static class MigrateConfiguration { public static void Version0To1( Configuration config ) diff --git a/Penumbra/Mod/FullMod.cs b/Penumbra/Mods/FullMod.cs similarity index 96% rename from Penumbra/Mod/FullMod.cs rename to Penumbra/Mods/FullMod.cs index 6ffe9152..50ee92b5 100644 --- a/Penumbra/Mod/FullMod.cs +++ b/Penumbra/Mods/FullMod.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using Penumbra.GameData.ByteString; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // A complete Mod containing settings (i.e. dependent on a collection) // and the resulting cache. diff --git a/Penumbra/Mod/GroupInformation.cs b/Penumbra/Mods/GroupInformation.cs similarity index 99% rename from Penumbra/Mod/GroupInformation.cs rename to Penumbra/Mods/GroupInformation.cs index 7c86b5f3..f71e0d3c 100644 --- a/Penumbra/Mod/GroupInformation.cs +++ b/Penumbra/Mods/GroupInformation.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public enum SelectType { diff --git a/Penumbra/Mod/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs similarity index 96% rename from Penumbra/Mod/Mod.SortOrder.cs rename to Penumbra/Mods/Mod.SortOrder.cs index 0e9c39eb..caaac4f9 100644 --- a/Penumbra/Mod/Mod.SortOrder.cs +++ b/Penumbra/Mods/Mod.SortOrder.cs @@ -1,7 +1,6 @@ using System; -using Penumbra.Mods; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public partial class Mod { diff --git a/Penumbra/Mod/Mod.cs b/Penumbra/Mods/Mod.cs similarity index 97% rename from Penumbra/Mod/Mod.cs rename to Penumbra/Mods/Mod.cs index d5f7cc2f..427d13ba 100644 --- a/Penumbra/Mod/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -3,9 +3,8 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.Mods; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Mod contains all permanent information about a mod, // and is independent of collections or settings. @@ -23,7 +22,7 @@ public partial class Mod public FileInfo MetaFile { get; set; } public int Index { get; private set; } = -1; - private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) + private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources) { BasePath = basePath; Meta = meta; diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs similarity index 99% rename from Penumbra/Mod/ModCleanup.cs rename to Penumbra/Mods/ModCleanup.cs index 0a2b3e4a..d09a26b8 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -8,10 +8,9 @@ using System.Security.Cryptography; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Importer; -using Penumbra.Mods; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public class ModCleanup { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index ba946687..4cb331aa 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Penumbra.Mod; namespace Penumbra.Mods; @@ -37,7 +36,7 @@ public static partial class ModFileSystem // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. // Saves and returns true if anything changed. - public static bool Rename( this Mod.Mod mod, string newName ) + public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName ) { if( RenameNoSave( mod, newName ) ) { @@ -63,7 +62,7 @@ public static partial class ModFileSystem // Move a single mod to the target folder. // Returns true and saves if anything changed. - public static bool Move( this Mod.Mod mod, ModFolder target ) + public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target ) { if( MoveNoSave( mod, target ) ) { @@ -76,7 +75,7 @@ public static partial class ModFileSystem // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. // Creates all necessary Subfolders. - public static void Move( this Mod.Mod mod, string sortOrder ) + public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder ) { var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); var folder = Root; @@ -137,10 +136,10 @@ public static partial class ModFileSystem } // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( Mod.Mod mod ) + private static void SaveMod( global::Penumbra.Mods.Mod mod ) { if( ReferenceEquals( mod.Order.ParentFolder, Root ) - && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) + && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) { Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); } @@ -184,7 +183,7 @@ public static partial class ModFileSystem return true; } - private static bool RenameNoSave( Mod.Mod mod, string newName ) + private static bool RenameNoSave( global::Penumbra.Mods.Mod mod, string newName ) { newName = newName.Replace( '/', '\\' ); if( mod.Order.SortOrderName == newName ) @@ -193,12 +192,12 @@ public static partial class ModFileSystem } mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.Order = new Mod.Mod.SortOrder( mod.Order.ParentFolder, newName ); + mod.Order = new global::Penumbra.Mods.Mod.SortOrder( mod.Order.ParentFolder, newName ); mod.Order.ParentFolder.AddMod( mod ); return true; } - private static bool MoveNoSave( Mod.Mod mod, ModFolder target ) + private static bool MoveNoSave( global::Penumbra.Mods.Mod mod, ModFolder target ) { var oldParent = mod.Order.ParentFolder; if( ReferenceEquals( target, oldParent ) ) @@ -207,7 +206,7 @@ public static partial class ModFileSystem } oldParent.RemoveMod( mod ); - mod.Order = new Mod.Mod.SortOrder( target, mod.Order.SortOrderName ); + mod.Order = new global::Penumbra.Mods.Mod.SortOrder( target, mod.Order.SortOrderName ); target.AddMod( mod ); return true; } diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs index e2369819..80a1df51 100644 --- a/Penumbra/Mods/ModFolder.cs +++ b/Penumbra/Mods/ModFolder.cs @@ -1,247 +1,245 @@ using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +public partial class ModFolder { - public partial class ModFolder + public ModFolder? Parent; + + public string FullName { - public ModFolder? Parent; - - public string FullName + get { - get - { - var parentPath = Parent?.FullName ?? string.Empty; - return parentPath.Any() ? $"{parentPath}/{Name}" : Name; - } - } - - private string _name = string.Empty; - - public string Name - { - get => _name; - set => _name = value.Replace( '/', '\\' ); - } - - public List< ModFolder > SubFolders { get; } = new(); - public List< Mod.Mod > Mods { get; } = new(); - - public ModFolder( ModFolder parent, string name ) - { - Parent = parent; - Name = name; - } - - public override string ToString() - => FullName; - - public int TotalDescendantMods() - => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); - - public int TotalDescendantFolders() - => SubFolders.Sum( f => f.TotalDescendantFolders() ); - - // Return all descendant mods in the specified order. - public IEnumerable< Mod.Mod > AllMods( bool foldersFirst ) - { - if( foldersFirst ) - { - return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); - } - - return GetSortedEnumerator().SelectMany( f => - { - if( f is ModFolder folder ) - { - return folder.AllMods( false ); - } - - return new[] { ( Mod.Mod )f }; - } ); - } - - // Return all descendant subfolders. - public IEnumerable< ModFolder > AllFolders() - => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); - - // Iterate through all descendants in the specified order, returning subfolders as well as mods. - public IEnumerable< object > GetItems( bool foldersFirst ) - => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); - - // Find a subfolder by name. Returns true and sets folder to it if it exists. - public bool FindSubFolder( string name, out ModFolder folder ) - { - var subFolder = new ModFolder( this, name ); - var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); - folder = idx >= 0 ? SubFolders[ idx ] : this; - return idx >= 0; - } - - // Checks if an equivalent subfolder as folder already exists and returns its index. - // If it does not exist, inserts folder as a subfolder and returns the new index. - // Also sets this as folders parent. - public int FindOrAddSubFolder( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - SubFolders.Insert( idx, folder ); - folder.Parent = this; - return idx; - } - - // Checks if a subfolder with the given name already exists and returns it and its index. - // If it does not exists, creates and inserts it and returns the new subfolder and its index. - public (ModFolder, int) FindOrCreateSubFolder( string name ) - { - var subFolder = new ModFolder( this, name ); - var idx = FindOrAddSubFolder( subFolder ); - return ( SubFolders[ idx ], idx ); - } - - // Remove folder as a subfolder if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveSubFolder( ModFolder folder ) - { - RemoveFolderIgnoreEmpty( folder ); - CheckEmpty(); - } - - // Add the given mod as a child, if it is not already a child. - // Returns the index of the found or inserted mod. - public int AddMod( Mod.Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - Mods.Insert( idx, mod ); - - return idx; - } - - // Remove mod as a child if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveMod( Mod.Mod mod ) - { - RemoveModIgnoreEmpty( mod ); - CheckEmpty(); + var parentPath = Parent?.FullName ?? string.Empty; + return parentPath.Any() ? $"{parentPath}/{Name}" : Name; } } - // Internals - public partial class ModFolder + private string _name = string.Empty; + + public string Name { - // Create a Root folder without parent. - internal static ModFolder CreateRoot() - => new( null!, string.Empty ); + get => _name; + set => _name = value.Replace( '/', '\\' ); + } - internal class ModFolderComparer : IComparer< ModFolder > + public List< ModFolder > SubFolders { get; } = new(); + public List< Mod > Mods { get; } = new(); + + public ModFolder( ModFolder parent, string name ) + { + Parent = parent; + Name = name; + } + + public override string ToString() + => FullName; + + public int TotalDescendantMods() + => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); + + public int TotalDescendantFolders() + => SubFolders.Sum( f => f.TotalDescendantFolders() ); + + // Return all descendant mods in the specified order. + public IEnumerable< Mod > AllMods( bool foldersFirst ) + { + if( foldersFirst ) { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. - public int Compare( ModFolder? x, ModFolder? y ) - => ReferenceEquals( x, y ) - ? 0 - : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); + return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); } - internal class ModDataComparer : IComparer< Mod.Mod > + return GetSortedEnumerator().SelectMany( f => { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. - // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. - public int Compare( Mod.Mod? x, Mod.Mod? y ) + if( f is ModFolder folder ) { - if( ReferenceEquals( x, y ) ) - { - return 0; - } - - var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); - if( cmp != 0 ) - { - return cmp; - } - - return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); + return folder.AllMods( false ); } + + return new[] { ( Mod )f }; + } ); + } + + // Return all descendant subfolders. + public IEnumerable< ModFolder > AllFolders() + => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); + + // Iterate through all descendants in the specified order, returning subfolders as well as mods. + public IEnumerable< object > GetItems( bool foldersFirst ) + => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); + + // Find a subfolder by name. Returns true and sets folder to it if it exists. + public bool FindSubFolder( string name, out ModFolder folder ) + { + var subFolder = new ModFolder( this, name ); + var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); + folder = idx >= 0 ? SubFolders[ idx ] : this; + return idx >= 0; + } + + // Checks if an equivalent subfolder as folder already exists and returns its index. + // If it does not exist, inserts folder as a subfolder and returns the new index. + // Also sets this as folders parent. + public int FindOrAddSubFolder( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx >= 0 ) + { + return idx; } - internal static readonly ModFolderComparer FolderComparer = new(); - internal static readonly ModDataComparer ModComparer = new(); + idx = ~idx; + SubFolders.Insert( idx, folder ); + folder.Parent = this; + return idx; + } - // Get an enumerator for actually sorted objects instead of folder-first objects. - private IEnumerable< object > GetSortedEnumerator() + // Checks if a subfolder with the given name already exists and returns it and its index. + // If it does not exists, creates and inserts it and returns the new subfolder and its index. + public (ModFolder, int) FindOrCreateSubFolder( string name ) + { + var subFolder = new ModFolder( this, name ); + var idx = FindOrAddSubFolder( subFolder ); + return ( SubFolders[ idx ], idx ); + } + + // Remove folder as a subfolder if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveSubFolder( ModFolder folder ) + { + RemoveFolderIgnoreEmpty( folder ); + CheckEmpty(); + } + + // Add the given mod as a child, if it is not already a child. + // Returns the index of the found or inserted mod. + public int AddMod( Mod mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) { - var modIdx = 0; - foreach( var folder in SubFolders ) - { - var folderString = folder.Name; - for( ; modIdx < Mods.Count; ++modIdx ) - { - var mod = Mods[ modIdx ]; - var modString = mod.Order.SortOrderName; - if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) - { - yield return mod; - } - else - { - break; - } - } + return idx; + } - yield return folder; + idx = ~idx; + Mods.Insert( idx, mod ); + + return idx; + } + + // Remove mod as a child if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveMod( Mod mod ) + { + RemoveModIgnoreEmpty( mod ); + CheckEmpty(); + } +} + +// Internals +public partial class ModFolder +{ + // Create a Root folder without parent. + internal static ModFolder CreateRoot() + => new(null!, string.Empty); + + internal class ModFolderComparer : IComparer< ModFolder > + { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + + // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. + public int Compare( ModFolder? x, ModFolder? y ) + => ReferenceEquals( x, y ) + ? 0 + : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); + } + + internal class ModDataComparer : IComparer< Mod > + { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + + // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. + // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. + public int Compare( Mod? x, Mod? y ) + { + if( ReferenceEquals( x, y ) ) + { + return 0; } + var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); + if( cmp != 0 ) + { + return cmp; + } + + return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); + } + } + + internal static readonly ModFolderComparer FolderComparer = new(); + internal static readonly ModDataComparer ModComparer = new(); + + // Get an enumerator for actually sorted objects instead of folder-first objects. + private IEnumerable< object > GetSortedEnumerator() + { + var modIdx = 0; + foreach( var folder in SubFolders ) + { + var folderString = folder.Name; for( ; modIdx < Mods.Count; ++modIdx ) { - yield return Mods[ modIdx ]; + var mod = Mods[ modIdx ]; + var modString = mod.Order.SortOrderName; + if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) + { + yield return mod; + } + else + { + break; + } } + + yield return folder; } - private void CheckEmpty() + for( ; modIdx < Mods.Count; ++modIdx ) { - if( Mods.Count == 0 && SubFolders.Count == 0 ) - { - Parent?.RemoveSubFolder( this ); - } + yield return Mods[ modIdx ]; + } + } + + private void CheckEmpty() + { + if( Mods.Count == 0 && SubFolders.Count == 0 ) + { + Parent?.RemoveSubFolder( this ); + } + } + + // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveFolderIgnoreEmpty( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx < 0 ) + { + return; } - // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveFolderIgnoreEmpty( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx < 0 ) - { - return; - } + SubFolders[ idx ].Parent = null; + SubFolders.RemoveAt( idx ); + } - SubFolders[ idx ].Parent = null; - SubFolders.RemoveAt( idx ); - } - - // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveModIgnoreEmpty( Mod.Mod mod ) + // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveModIgnoreEmpty( Mod mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - Mods.RemoveAt( idx ); - } + Mods.RemoveAt( idx ); } } } \ No newline at end of file diff --git a/Penumbra/Mod/ModFunctions.cs b/Penumbra/Mods/ModFunctions.cs similarity index 99% rename from Penumbra/Mod/ModFunctions.cs rename to Penumbra/Mods/ModFunctions.cs index 7372cfa0..72787222 100644 --- a/Penumbra/Mod/ModFunctions.cs +++ b/Penumbra/Mods/ModFunctions.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using Penumbra.GameData.ByteString; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Functions that do not really depend on only one component of a mod. public static class ModFunctions diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs index c8149a65..56673d20 100644 --- a/Penumbra/Mods/ModManager.Directory.cs +++ b/Penumbra/Mods/ModManager.Directory.cs @@ -2,16 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using Dalamud.Logging; -using Penumbra.Meta.Manipulations; -using Penumbra.Mod; namespace Penumbra.Mods; public partial class ModManagerNew { - private readonly List< Mod.Mod > _mods = new(); + private readonly List< Mod > _mods = new(); - public IReadOnlyList< Mod.Mod > Mods + public IReadOnlyList< Mod > Mods => _mods; public void DiscoverMods() @@ -37,6 +35,7 @@ public partial class ModManagerNew //Collections.RecreateCaches(); } } + public partial class ModManagerNew { public DirectoryInfo BasePath { get; private set; } = null!; diff --git a/Penumbra/Mod/ModManager.cs b/Penumbra/Mods/ModManager.cs similarity index 99% rename from Penumbra/Mod/ModManager.cs rename to Penumbra/Mods/ModManager.cs index 5f30c6bd..8c1f90df 100644 --- a/Penumbra/Mod/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -6,11 +6,10 @@ using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public partial class Mod { diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 7160d874..4ecf5fb3 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using Dalamud.Logging; -using Penumbra.Mod; using Penumbra.Util; namespace Penumbra.Mods; @@ -12,7 +11,7 @@ namespace Penumbra.Mods; // Contains all change functions on a specific mod that also require corresponding changes to collections. public static class ModManagerEditExtensions { - public static bool RenameMod( this Mod.Mod.Manager manager, string newName, Mod.Mod mod ) + public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod ) { if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) { @@ -25,14 +24,14 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeSortOrder( this Mod.Mod.Manager manager, Mod.Mod mod, string newSortOrder ) + public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder ) { if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) { return false; } - var inRoot = new Mod.Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); + var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) { mod.Order = inRoot; @@ -49,7 +48,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RenameModFolder( this Mod.Mod.Manager manager, Mod.Mod mod, DirectoryInfo newDir, bool move = true ) + public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true ) { if( move ) { @@ -73,7 +72,7 @@ public static class ModManagerEditExtensions var oldBasePath = mod.BasePath; mod.BasePath = newDir; - mod.MetaFile = Mod.Mod.MetaFileInfo( newDir ); + mod.MetaFile = Mod.MetaFileInfo( newDir ); manager.UpdateMod( mod ); if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) @@ -95,7 +94,7 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeModGroup( this Mod.Mod.Manager manager, string oldGroupName, string newGroupName, Mod.Mod mod, + public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod, SelectType type = SelectType.Single ) { if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) @@ -157,7 +156,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RemoveModOption( this Mod.Mod.Manager manager, int optionIdx, OptionGroup group, Mod.Mod mod ) + public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod ) { if( optionIdx < 0 || optionIdx >= group.Options.Count ) { diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mods/ModMeta.cs similarity index 80% rename from Penumbra/Mod/ModMeta.cs rename to Penumbra/Mods/ModMeta.cs index d4c18b3b..228f3bb2 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -5,47 +5,20 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.GameData.ByteString; +using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains descriptive data about the mod as well as possible settings and fileswaps. public class ModMeta { public uint FileVersion { get; set; } - public string Name - { - get => _name; - set - { - _name = value; - LowerName = value.ToLowerInvariant(); - } - } - - private string _name = "Mod"; - - [JsonIgnore] - public string LowerName { get; private set; } = "mod"; - - private string _author = ""; - - public string Author - { - get => _author; - set - { - _author = value; - LowerAuthor = value.ToLowerInvariant(); - } - } - - [JsonIgnore] - public string LowerAuthor { get; private set; } = ""; - - public string Description { get; set; } = ""; - public string Version { get; set; } = ""; - public string Website { get; set; } = ""; + public LowerString Name { get; set; } = "Mod"; + public LowerString Author { get; set; } = LowerString.Empty; + public string Description { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Website { get; set; } = string.Empty; [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mods/ModResources.cs similarity index 99% rename from Penumbra/Mod/ModResources.cs rename to Penumbra/Mods/ModResources.cs index 590cda79..cd05fc7e 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mods/ModResources.cs @@ -5,7 +5,7 @@ using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta; -namespace Penumbra.Mod; +namespace Penumbra.Mods; [Flags] public enum ResourceChange diff --git a/Penumbra/Mod/ModSettings.cs b/Penumbra/Mods/ModSettings.cs similarity index 98% rename from Penumbra/Mod/ModSettings.cs rename to Penumbra/Mods/ModSettings.cs index 4f82df49..aadc0242 100644 --- a/Penumbra/Mod/ModSettings.cs +++ b/Penumbra/Mods/ModSettings.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains the settings for a given mod. public class ModSettings diff --git a/Penumbra/Mod/NamedModSettings.cs b/Penumbra/Mods/NamedModSettings.cs similarity index 98% rename from Penumbra/Mod/NamedModSettings.cs rename to Penumbra/Mods/NamedModSettings.cs index 45770e7e..5a0ded71 100644 --- a/Penumbra/Mod/NamedModSettings.cs +++ b/Penumbra/Mods/NamedModSettings.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains settings with the option selections stored by names instead of index. // This is meant to make them possibly more portable when we support importing collections from other users. diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 61a803ea..322c9f7e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -10,13 +9,12 @@ using Lumina.Excel.GeneratedSheets; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; -using Penumbra.Mods; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra; @@ -34,8 +32,9 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Mod.Manager ModManager { get; private set; } = null!; + public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; @@ -65,9 +64,10 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); + MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod.Mod.Manager(); + ModManager = new Mod.Manager(); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); ObjectReloader = new ObjectReloader(); @@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin PathResolver.Dispose(); ResourceLogger.Dispose(); + MetaFileManager.Dispose(); ResourceLoader.Dispose(); CharacterUtility.Dispose(); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 4775526e..760da897 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -7,7 +7,6 @@ using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; using Penumbra.Collections; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -22,7 +21,7 @@ public partial class SettingsInterface private readonly Selector _selector; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; + private ModCollection[] _collections = null!; private int _currentCollectionIndex; private int _currentDefaultIndex; private readonly Dictionary< string, int > _currentCharacterIndices = new(); @@ -192,6 +191,65 @@ public partial class SettingsInterface } } + private static void DrawInheritance( ModCollection collection ) + { + ImGui.PushID( collection.Index ); + if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) ) + { + foreach( var inheritance in collection.Inheritance ) + { + DrawInheritance( inheritance ); + } + } + + ImGui.PopID(); + } + + private void DrawCurrentCollectionInheritance() + { + if( !ImGui.BeginListBox( "##inheritanceList", + new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox ); + DrawInheritance( _collections[ _currentCollectionIndex + 1 ] ); + } + + private static int _newInheritanceIdx = 0; + + private void DrawNewInheritanceSelection() + { + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) ) + { + using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + foreach( var collection in Penumbra.CollectionManager ) + { + if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) ) + { + _newInheritanceIdx = collection.Index; + } + } + } + + ImGui.SameLine(); + var valid = _newInheritanceIdx > ModCollection.Empty.Index + && _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx + && _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid ); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid ) + { + _collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] ); + } + + style.Pop(); + font.Pop(); + ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." ); + } + private void DrawDefaultCollectionSelector() { var index = _currentDefaultIndex; @@ -344,12 +402,14 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) .Push( ImGui.EndChild ); - if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) ) + if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ) ) { DrawCurrentCollectionSelector( true ); - ImGuiHelpers.ScaledDummy( 0, 10 ); DrawNewCollectionInput(); + ImGuiHelpers.ScaledDummy( 0, 10 ); + DrawCurrentCollectionInheritance(); + DrawNewInheritanceSelection(); } raii.Pop(); diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 04c0dbd1..8420cb5c 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -6,8 +6,8 @@ using ImGuiNET; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Mods; using Penumbra.UI.Custom; +using Penumbra.Util; namespace Penumbra.UI; @@ -17,10 +17,8 @@ public partial class SettingsInterface { private const string LabelTab = "Effective Changes"; - private string _gamePathFilter = string.Empty; - private string _gamePathFilterLower = string.Empty; - private string _filePathFilter = string.Empty; - private string _filePathFilterLower = string.Empty; + private LowerString _gamePathFilter = LowerString.Empty; + private LowerString _filePathFilter = LowerString.Empty; private const float LeftTextLength = 600; @@ -57,47 +55,49 @@ public partial class SettingsInterface } ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) + var tmp = _gamePathFilter.Text; + if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref tmp, 256 ) ) { - _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + _gamePathFilter = tmp; } ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + tmp = _filePathFilter.Text; + if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref tmp, 256 ) ) { - _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + _filePathFilter = tmp; } } private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilter.Lower ); } private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Value.ToString().Contains( _filePathFilter.Lower ); } - private bool CheckFilters( (string, string, string) kvp ) + private bool CheckFilters( (string, LowerString) kvp ) { - if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Item2.Contains( _filePathFilter.Lower ); } private void DrawFilteredRows( ModCollection active ) @@ -113,49 +113,43 @@ public partial class SettingsInterface return; } - foreach( var (mp, mod, _) in cache.Cmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Cmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Eqp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Eqp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Eqdp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Eqdp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Gmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Gmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Est.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Est.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Imc.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Imc.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index b797902e..7c35efea 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.Util; @@ -15,19 +14,19 @@ public class ModListCache : IDisposable public const uint ConflictingModColor = 0xFFAAAAFFu; public const uint HandledConflictModColor = 0xFF88DDDDu; - private readonly Mod.Mod.Manager _manager; + private readonly Mods.Mod.Manager _manager; private readonly List< FullMod > _modsInOrder = new(); private readonly List< (bool visible, uint color) > _visibleMods = new(); private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); private readonly IReadOnlySet< string > _newMods; - private string _modFilter = string.Empty; - private string _modFilterChanges = string.Empty; - private string _modFilterAuthor = string.Empty; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - private bool _listResetNecessary; - private bool _filterResetNecessary; + private LowerString _modFilter = LowerString.Empty; + private LowerString _modFilterAuthor = LowerString.Empty; + private LowerString _modFilterChanges = LowerString.Empty; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private bool _listResetNecessary; + private bool _filterResetNecessary; public ModFilter StateFilter @@ -44,7 +43,7 @@ public class ModListCache : IDisposable } } - public ModListCache( Mod.Mod.Manager manager, IReadOnlySet< string > newMods ) + public ModListCache( Mods.Mod.Manager manager, IReadOnlySet< string > newMods ) { _manager = manager; _newMods = newMods; @@ -123,20 +122,20 @@ public class ModListCache : IDisposable if( lower.StartsWith( "c:" ) ) { _modFilterChanges = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterAuthor = string.Empty; + _modFilter = LowerString.Empty; + _modFilterAuthor = LowerString.Empty; } else if( lower.StartsWith( "a:" ) ) { _modFilterAuthor = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterChanges = string.Empty; + _modFilter = LowerString.Empty; + _modFilterChanges = LowerString.Empty; } else { _modFilter = lower; - _modFilterAuthor = string.Empty; - _modFilterChanges = string.Empty; + _modFilterAuthor = LowerString.Empty; + _modFilterChanges = LowerString.Empty; } ResetFilters(); @@ -233,12 +232,12 @@ public class ModListCache : IDisposable { var ret = ( false, 0u ); - if( _modFilter.Length > 0 && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) + if( _modFilter.Length > 0 && !mod.Data.Meta.Name.Contains( _modFilter ) ) { return ret; } - if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) ) + if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.Author.Contains( _modFilterAuthor ) ) { return ret; } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index d3444394..66d40c65 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Meta.Manipulations; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -201,7 +200,7 @@ public partial class SettingsInterface raii.Push( ImGui.EndListBox ); using var indent = ImGuiRaii.PushIndent( 0 ); - Mod.Mod? oldBadMod = null; + Mods.Mod? oldBadMod = null; foreach( var conflict in conflicts ) { var badMod = Penumbra.ModManager[ conflict.Mod2 ]; @@ -224,14 +223,14 @@ public partial class SettingsInterface indent.Push( 30f ); } - if( conflict.Conflict is Utf8GamePath p ) + if( conflict.Data is Utf8GamePath p ) { unsafe { ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); } } - else if( conflict.Conflict is MetaManipulation m ) + else if( conflict.Data is MetaManipulation m ) { ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 5bb4acaa..7a80f150 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -5,7 +5,6 @@ using Dalamud.Interface; using ImGuiNET; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index e6f3b170..25fc4a4c 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -6,7 +6,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -69,7 +68,7 @@ public partial class SettingsInterface _currentWebsite = Meta?.Website ?? ""; } - private Mod.FullMod? Mod + private Mods.FullMod? Mod => _selector.Mod; private ModMeta? Meta @@ -77,7 +76,7 @@ public partial class SettingsInterface private void DrawName() { - var name = Meta!.Name; + var name = Meta!.Name.Text; var modManager = Penumbra.ModManager; if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) { @@ -122,7 +121,7 @@ public partial class SettingsInterface ImGui.TextColored( GreyColor, "by" ); ImGui.SameLine(); - var author = Meta!.Author; + var author = Meta!.Author.Text; if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) && author != Meta.Author ) { @@ -228,7 +227,7 @@ public partial class SettingsInterface } } - public static bool DrawSortOrder( Mod.Mod mod, Mod.Mod.Manager manager, Selector selector ) + public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector ) { var currentSortOrder = mod.Order.FullPath; ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 5984ac48..6ccf9d3f 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -10,7 +10,6 @@ using Dalamud.Logging; using ImGuiNET; using Penumbra.Collections; using Penumbra.Importer; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -410,11 +409,11 @@ public partial class SettingsInterface // Selection private partial class Selector { - public Mod.FullMod? Mod { get; private set; } + public Mods.FullMod? Mod { get; private set; } private int _index; private string _nextDir = string.Empty; - private void SetSelection( int idx, Mod.FullMod? info ) + private void SetSelection( int idx, Mods.FullMod? info ) { Mod = info; if( idx != _index ) @@ -480,7 +479,7 @@ public partial class SettingsInterface private partial class Selector { // === Mod === - private void DrawModOrderPopup( string popupName, Mod.FullMod mod, bool firstOpen ) + private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen ) { if( !ImGui.BeginPopup( popupName ) ) { @@ -664,7 +663,7 @@ public partial class SettingsInterface idx += sub.TotalDescendantMods(); } } - else if( item is Mod.Mod _ ) + else if( item is Mods.Mod _ ) { var (mod, visible, color) = Cache.GetMod( idx ); if( mod != null && visible ) @@ -721,7 +720,7 @@ public partial class SettingsInterface } } - private void DrawMod( Mod.FullMod mod, int modIndex, uint color ) + private void DrawMod( Mods.FullMod mod, int modIndex, uint color ) { using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index d258231a..ee11e961 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -10,7 +10,6 @@ using Dalamud.Logging; using ImGuiNET; using Penumbra.GameData.ByteString; using Penumbra.Interop; -using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; diff --git a/Penumbra/Util/LowerString.cs b/Penumbra/Util/LowerString.cs new file mode 100644 index 00000000..4942a5bc --- /dev/null +++ b/Penumbra/Util/LowerString.cs @@ -0,0 +1,122 @@ +using System; +using ImGuiNET; +using Newtonsoft.Json; + +namespace Penumbra.Util; + +[JsonConverter( typeof( Converter ) )] +public readonly struct LowerString : IEquatable< LowerString >, IComparable< LowerString > +{ + public static readonly LowerString Empty = new(string.Empty); + + public readonly string Text = string.Empty; + public readonly string Lower = string.Empty; + + public LowerString( string text ) + { + Text = string.Intern( text ); + Lower = string.Intern( text.ToLowerInvariant() ); + } + + + public int Length + => Text.Length; + + public int Count + => Length; + + public bool Equals( LowerString other ) + => string.Equals( Lower, other.Lower, StringComparison.InvariantCulture ); + + public bool Equals( string other ) + => string.Equals( Lower, other, StringComparison.InvariantCultureIgnoreCase ); + + public int CompareTo( LowerString other ) + => string.Compare( Lower, other.Lower, StringComparison.InvariantCulture ); + + public int CompareTo( string other ) + => string.Compare( Lower, other, StringComparison.InvariantCultureIgnoreCase ); + + public bool Contains( LowerString other ) + => Lower.Contains( other.Lower, StringComparison.InvariantCulture ); + + public bool Contains( string other ) + => Lower.Contains( other, StringComparison.InvariantCultureIgnoreCase ); + + public bool StartsWith( LowerString other ) + => Lower.StartsWith( other.Lower, StringComparison.InvariantCulture ); + + public bool StartsWith( string other ) + => Lower.StartsWith( other, StringComparison.InvariantCultureIgnoreCase ); + + public bool EndsWith( LowerString other ) + => Lower.EndsWith( other.Lower, StringComparison.InvariantCulture ); + + public bool EndsWith( string other ) + => Lower.EndsWith( other, StringComparison.InvariantCultureIgnoreCase ); + + public override string ToString() + => Text; + + public static implicit operator string( LowerString s ) + => s.Text; + + public static implicit operator LowerString( string s ) + => new(s); + + private class Converter : JsonConverter< LowerString > + { + public override void WriteJson( JsonWriter writer, LowerString value, JsonSerializer serializer ) + { + writer.WriteValue( value.Text ); + } + + public override LowerString ReadJson( JsonReader reader, Type objectType, LowerString existingValue, bool hasExistingValue, + JsonSerializer serializer ) + { + if( reader.Value is string text ) + { + return new LowerString( text ); + } + + return existingValue; + } + } + + public static bool InputWithHint( string label, string hint, ref LowerString s, uint maxLength = 128, + ImGuiInputTextFlags flags = ImGuiInputTextFlags.None ) + { + var tmp = s.Text; + if( !ImGui.InputTextWithHint( label, hint, ref tmp, maxLength, flags ) || tmp == s.Text ) + { + return false; + } + + s = new LowerString( tmp ); + return true; + } + + public override bool Equals( object? obj ) + => obj is LowerString lowerString && Equals( lowerString ); + + public override int GetHashCode() + => Text.GetHashCode(); + + public static bool operator ==( LowerString lhs, LowerString rhs ) + => lhs.Equals( rhs ); + + public static bool operator !=( LowerString lhs, LowerString rhs ) + => lhs.Equals( rhs ); + + public static bool operator ==( LowerString lhs, string rhs ) + => lhs.Equals( rhs ); + + public static bool operator !=( LowerString lhs, string rhs ) + => lhs.Equals( rhs ); + + public static bool operator ==( string lhs, LowerString rhs ) + => rhs.Equals( lhs ); + + public static bool operator !=( string lhs, LowerString rhs ) + => rhs.Equals( lhs ); +} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 47d26e1b..75c03c3e 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Util; @@ -74,7 +74,7 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( Mod.Mod mod, string from, string to ) + public static bool ChangeModMaterials( Mods.Mod mod, string from, string to ) { if( ValidStrings( from, to ) ) { diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs index 19e8b47e..76fad9e2 100644 --- a/Penumbra/Util/TempFile.cs +++ b/Penumbra/Util/TempFile.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Linq; namespace Penumbra.Util; From 5bfcb71f5288633b88fc5539dff84982c6f9f729 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Mar 2022 13:33:03 +0200 Subject: [PATCH 0127/2451] Start for Mod rework, currently not applied. --- Penumbra.GameData/Structs/GmpEntry.cs | 177 +++++----- .../Collections/CollectionManager.Active.cs | 8 - Penumbra/Collections/CollectionManager.cs | 47 +-- Penumbra/Collections/ModCollection.Cache.cs | 11 +- Penumbra/Collections/ModCollection.cs | 79 +++-- .../Meta/Manipulations/EstManipulation.cs | 16 +- .../Meta/Manipulations/MetaManipulation.cs | 21 +- Penumbra/Mods/IModGroup.cs | 53 +++ Penumbra/Mods/ISubMod.cs | 14 + Penumbra/Mods/Mod.SortOrder.cs | 1 - Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/Mod2.BasePath.cs | 56 ++++ Penumbra/Mods/Mod2.ChangedItems.cs | 23 ++ Penumbra/Mods/Mod2.Files.MultiModGroup.cs | 53 +++ Penumbra/Mods/Mod2.Files.SingleModGroup.cs | 52 +++ Penumbra/Mods/Mod2.Files.SubMod.cs | 31 ++ Penumbra/Mods/Mod2.Files.cs | 43 +++ Penumbra/Mods/Mod2.Manager.BasePath.cs | 77 +++++ Penumbra/Mods/Mod2.Manager.Meta.cs | 67 ++++ Penumbra/Mods/Mod2.Manager.Options.cs | 301 ++++++++++++++++++ Penumbra/Mods/Mod2.Manager.Root.cs | 91 ++++++ Penumbra/Mods/Mod2.Manager.cs | 35 ++ Penumbra/Mods/Mod2.Meta.Migration.cs | 62 ++++ Penumbra/Mods/Mod2.Meta.cs | 121 +++++++ Penumbra/Mods/Mod2.SortOrder.cs | 8 + Penumbra/Mods/ModManager.Directory.cs | 109 ------- Penumbra/Mods/ModManager.cs | 10 +- Penumbra/Mods/ModMeta.cs | 155 ++++++--- .../TabInstalledDetailsManipulations.cs | 4 +- Penumbra/Util/Functions.cs | 19 ++ 30 files changed, 1440 insertions(+), 306 deletions(-) create mode 100644 Penumbra/Mods/IModGroup.cs create mode 100644 Penumbra/Mods/ISubMod.cs create mode 100644 Penumbra/Mods/Mod2.BasePath.cs create mode 100644 Penumbra/Mods/Mod2.ChangedItems.cs create mode 100644 Penumbra/Mods/Mod2.Files.MultiModGroup.cs create mode 100644 Penumbra/Mods/Mod2.Files.SingleModGroup.cs create mode 100644 Penumbra/Mods/Mod2.Files.SubMod.cs create mode 100644 Penumbra/Mods/Mod2.Files.cs create mode 100644 Penumbra/Mods/Mod2.Manager.BasePath.cs create mode 100644 Penumbra/Mods/Mod2.Manager.Meta.cs create mode 100644 Penumbra/Mods/Mod2.Manager.Options.cs create mode 100644 Penumbra/Mods/Mod2.Manager.Root.cs create mode 100644 Penumbra/Mods/Mod2.Manager.cs create mode 100644 Penumbra/Mods/Mod2.Meta.Migration.cs create mode 100644 Penumbra/Mods/Mod2.Meta.cs create mode 100644 Penumbra/Mods/Mod2.SortOrder.cs delete mode 100644 Penumbra/Mods/ModManager.Directory.cs create mode 100644 Penumbra/Util/Functions.cs diff --git a/Penumbra.GameData/Structs/GmpEntry.cs b/Penumbra.GameData/Structs/GmpEntry.cs index c6af3fba..8ad571ed 100644 --- a/Penumbra.GameData/Structs/GmpEntry.cs +++ b/Penumbra.GameData/Structs/GmpEntry.cs @@ -1,94 +1,103 @@ +using System; using System.IO; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +public struct GmpEntry : IEquatable< GmpEntry > { - public struct GmpEntry + public static readonly GmpEntry Default = new(); + + public bool Enabled { - public static readonly GmpEntry Default = new (); - - public bool Enabled + get => ( Value & 1 ) == 1; + set { - get => ( Value & 1 ) == 1; - set + if( value ) { - if( value ) - { - Value |= 1ul; - } - else - { - Value &= ~1ul; - } + Value |= 1ul; + } + else + { + Value &= ~1ul; } } - - public bool Animated - { - get => ( Value & 2 ) == 2; - set - { - if( value ) - { - Value |= 2ul; - } - else - { - Value &= ~2ul; - } - } - } - - public ushort RotationA - { - get => ( ushort )( ( Value >> 2 ) & 0x3FF ); - set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 ); - } - - public ushort RotationB - { - get => ( ushort )( ( Value >> 12 ) & 0x3FF ); - set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 ); - } - - public ushort RotationC - { - get => ( ushort )( ( Value >> 22 ) & 0x3FF ); - set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 ); - } - - public byte UnknownA - { - get => ( byte )( ( Value >> 32 ) & 0x0F ); - set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 ); - } - - public byte UnknownB - { - get => ( byte )( ( Value >> 36 ) & 0x0F ); - set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 ); - } - - public byte UnknownTotal - { - get => ( byte )( ( Value >> 32 ) & 0xFF ); - set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 ); - } - - public ulong Value { get; set; } - - public static GmpEntry FromTexToolsMeta( byte[] data ) - { - GmpEntry ret = new(); - using var reader = new BinaryReader( new MemoryStream( data ) ); - ret.Value = reader.ReadUInt32(); - ret.UnknownTotal = data[ 4 ]; - return ret; - } - - public static implicit operator ulong( GmpEntry entry ) - => entry.Value; - - public static explicit operator GmpEntry( ulong entry ) - => new() { Value = entry }; } + + public bool Animated + { + get => ( Value & 2 ) == 2; + set + { + if( value ) + { + Value |= 2ul; + } + else + { + Value &= ~2ul; + } + } + } + + public ushort RotationA + { + get => ( ushort )( ( Value >> 2 ) & 0x3FF ); + set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 ); + } + + public ushort RotationB + { + get => ( ushort )( ( Value >> 12 ) & 0x3FF ); + set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 ); + } + + public ushort RotationC + { + get => ( ushort )( ( Value >> 22 ) & 0x3FF ); + set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 ); + } + + public byte UnknownA + { + get => ( byte )( ( Value >> 32 ) & 0x0F ); + set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 ); + } + + public byte UnknownB + { + get => ( byte )( ( Value >> 36 ) & 0x0F ); + set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 ); + } + + public byte UnknownTotal + { + get => ( byte )( ( Value >> 32 ) & 0xFF ); + set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 ); + } + + public ulong Value { get; set; } + + public static GmpEntry FromTexToolsMeta( byte[] data ) + { + GmpEntry ret = new(); + using var reader = new BinaryReader( new MemoryStream( data ) ); + ret.Value = reader.ReadUInt32(); + ret.UnknownTotal = data[ 4 ]; + return ret; + } + + public static implicit operator ulong( GmpEntry entry ) + => entry.Value; + + public static explicit operator GmpEntry( ulong entry ) + => new() { Value = entry }; + + public bool Equals( GmpEntry other ) + => Value == other.Value; + + public override bool Equals( object? obj ) + => obj is GmpEntry other && Equals( other ); + + public override int GetHashCode() + => Value.GetHashCode(); } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 30b9298d..5e264981 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -206,14 +206,6 @@ public partial class ModCollection } } - private void ForceCacheUpdates() - { - foreach( var collection in this ) - { - collection.ForceCacheUpdate( collection == Default ); - } - } - // Recalculate effective files for active collections on events. private void OnModAddedActive( bool meta ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 76961f81..d54ffa05 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -56,21 +56,23 @@ public partial class ModCollection IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mods.Mod.Manager manager ) + public Manager( Mod.Manager manager ) { _modManager = manager; // The collection manager reacts to changes in mods by itself. - _modManager.ModsRediscovered += OnModsRediscovered; - _modManager.ModChange += OnModChanged; + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModChange += OnModChanged; ReadCollections(); LoadCollections(); } public void Dispose() { - _modManager.ModsRediscovered -= OnModsRediscovered; - _modManager.ModChange -= OnModChanged; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModChange -= OnModChanged; } // Add a new collection of the given name. @@ -144,12 +146,27 @@ public partial class ModCollection public bool RemoveCollection( ModCollection collection ) => RemoveCollection( collection.Index ); - - private void OnModsRediscovered() + private void OnModDiscoveryStarted() { - // When mods are rediscovered, force all cache updates and set the files of the default collection. - ForceCacheUpdates(); - Default.SetFiles(); + foreach( var collection in this ) + { + collection.PrepareModDiscovery(); + } + } + + private void OnModDiscoveryFinished() + { + // First, re-apply all mod settings. + foreach( var collection in this ) + { + collection.ApplyModSettings(); + } + + // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. + foreach( var collection in this ) + { + collection.ForceCacheUpdate( collection == Default ); + } } @@ -209,8 +226,7 @@ public partial class ModCollection // Inheritances can not be setup before all collections are read, // so this happens after reading the collections. - // During this iteration, we can also fix all settings that are not valid for the given mod anymore. - private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + private void ApplyInheritances( IEnumerable< IReadOnlyList< string > > inheritances ) { foreach( var (collection, inheritance) in this.Zip( inheritances ) ) { @@ -229,11 +245,6 @@ public partial class ModCollection } } - foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) - { - changes |= setting!.FixInvalidSettings( mod.Meta ); - } - if( changes ) { collection.Save(); @@ -277,7 +288,7 @@ public partial class ModCollection } AddDefaultCollection(); - ApplyInheritancesAndFixSettings( inheritances ); + ApplyInheritances( inheritances ); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 33726689..226f7b05 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -74,7 +74,7 @@ public partial class ModCollection // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) { // Skip the empty collection. if( Index == 0 ) @@ -82,15 +82,20 @@ public partial class ModCollection return; } - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + withMetaManipulations, reloadDefault ); _cache ??= new Cache( this ); _cache.CalculateEffectiveFileList(); if( withMetaManipulations ) { _cache.UpdateMetaManipulations(); + if( reloadDefault ) + { + SetFiles(); + } } - if( reloadResident ) + if( reloadDefault ) { Penumbra.ResidentResources.Reload(); } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9e4f7c60..6e82617f 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,22 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using Penumbra.Mods; namespace Penumbra.Collections; -public partial class ModCollection -{ - // Create the always available Empty Collection that will always sit at index 0, - // can not be deleted and does never create a cache. - private static ModCollection CreateEmpty() - { - var collection = CreateNewEmpty( EmptyCollection ); - collection.Index = 0; - collection._settings.Clear(); - return collection; - } -} - // A ModCollection is a named set of ModSettings to all of the users' installed mods. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. // Invariants: @@ -51,7 +39,6 @@ public partial class ModCollection // Settings for deleted mods will be kept via directory name. private readonly Dictionary< string, ModSettings > _unusedSettings; - // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) { @@ -70,16 +57,9 @@ public partial class ModCollection Name = name; Version = version; _unusedSettings = allSettings; - _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) - { - var modName = Penumbra.ModManager[ i ].BasePath.Name; - if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) - { - _unusedSettings.Remove( modName ); - _settings[ i ] = settings; - } - } + + _settings = new List< ModSettings? >(); + ApplyModSettings(); Migration.Migrate( this ); ModSettingChanged += SaveOnChange; @@ -106,7 +86,7 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private void AddMod( Mods.Mod mod ) + private void AddMod( Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -120,7 +100,7 @@ public partial class ModCollection } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mods.Mod mod, int idx ) + private void RemoveMod( Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) @@ -130,4 +110,51 @@ public partial class ModCollection _settings.RemoveAt( idx ); } + + // Create the always available Empty Collection that will always sit at index 0, + // can not be deleted and does never create a cache. + private static ModCollection CreateEmpty() + { + var collection = CreateNewEmpty( EmptyCollection ); + collection.Index = 0; + collection._settings.Clear(); + return collection; + } + + // Move all settings to unused settings for rediscovery. + private void PrepareModDiscovery() + { + foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) + { + _unusedSettings[ mod.BasePath.Name ] = setting!; + } + + _settings.Clear(); + } + + // Apply all mod settings from unused settings to the current set of mods. + // Also fixes invalid settings. + private void ApplyModSettings() + { + _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); + var changes = false; + foreach( var mod in Penumbra.ModManager ) + { + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var s ) ) + { + changes |= s.FixInvalidSettings( mod.Meta ); + _settings.Add( s ); + _unusedSettings.Remove( mod.BasePath.Name ); + } + else + { + _settings.Add( null ); + } + } + + if( changes ) + { + Save(); + } + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index d25893f1..b607e5e5 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,7 +19,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.HeadEstIdx, } - public readonly ushort SkeletonIdx; + public readonly ushort Entry; // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] public readonly Gender Gender; @@ -33,13 +33,13 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public readonly EstType Slot; [JsonConstructor] - public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort skeletonIdx ) + public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) { - SkeletonIdx = skeletonIdx; - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; + Entry = entry; + Gender = gender; + Race = race; + SetId = setId; + Slot = slot; } @@ -81,7 +81,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public bool Apply( EstFile file ) { - return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, SkeletonIdx ) switch + return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, Entry ) switch { EstFile.EstEntryChange.Unchanged => false, EstFile.EstEntryChange.Changed => true, diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 92caaa23..421642a0 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -137,7 +137,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa [FieldOffset( 15 )] [JsonConverter( typeof( StringEnumConverter ) )] - [JsonProperty("Type")] + [JsonProperty( "Type" )] public readonly Type ManipulationType; public object? Manipulation @@ -239,6 +239,25 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa public static implicit operator MetaManipulation( ImcManipulation imc ) => new(imc); + public bool EntryEquals( MetaManipulation other ) + { + if( ManipulationType != other.ManipulationType ) + { + return false; + } + + return ManipulationType switch + { + Type.Eqp => Eqp.Entry.Equals( other.Eqp.Entry ), + Type.Gmp => Gmp.Entry.Equals( other.Gmp.Entry ), + Type.Eqdp => Eqdp.Entry.Equals( other.Eqdp.Entry ), + Type.Est => Est.Entry.Equals( other.Est.Entry ), + Type.Rsp => Rsp.Entry.Equals( other.Rsp.Entry ), + Type.Imc => Imc.Entry.Equals( other.Imc.Entry ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + public bool Equals( MetaManipulation other ) { if( ManipulationType != other.ManipulationType ) diff --git a/Penumbra/Mods/IModGroup.cs b/Penumbra/Mods/IModGroup.cs new file mode 100644 index 00000000..498d17d2 --- /dev/null +++ b/Penumbra/Mods/IModGroup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public interface IModGroup : IEnumerable< ISubMod > +{ + public string Name { get; } + public string Description { get; } + public SelectType Type { get; } + public int Priority { get; } + + public int OptionPriority( Index optionIdx ); + + public ISubMod this[ Index idx ] { get; } + + public int Count { get; } + + public bool IsOption + => Type switch + { + SelectType.Single => Count > 1, + SelectType.Multi => Count > 0, + _ => false, + }; + + public void Save( DirectoryInfo basePath ); + + public string FileName( DirectoryInfo basePath ) + => Path.Combine( basePath.FullName, Name.RemoveInvalidPathSymbols() + ".json" ); + + public void DeleteFile( DirectoryInfo basePath ) + { + var file = FileName( basePath ); + if( !File.Exists( file ) ) + { + return; + } + + try + { + File.Delete( file ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete file {file}:\n{e}" ); + throw; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ISubMod.cs b/Penumbra/Mods/ISubMod.cs new file mode 100644 index 00000000..ee05e876 --- /dev/null +++ b/Penumbra/Mods/ISubMod.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public interface ISubMod +{ + public string Name { get; } + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } + public IReadOnlyList< MetaManipulation > Manipulations { get; } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs index caaac4f9..5cf55c5e 100644 --- a/Penumbra/Mods/Mod.SortOrder.cs +++ b/Penumbra/Mods/Mod.SortOrder.cs @@ -28,7 +28,6 @@ public partial class Mod } } - public SortOrder( ModFolder parentFolder, string name ) { ParentFolder = parentFolder; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 427d13ba..9ca93f85 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -9,7 +9,7 @@ namespace Penumbra.Mods; // Mod contains all permanent information about a mod, // and is independent of collections or settings. // It only changes when the user actively changes the mod or their filesystem. -public partial class Mod +public sealed partial class Mod { public DirectoryInfo BasePath; public ModMeta Meta; diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs new file mode 100644 index 00000000..2fe9dfe3 --- /dev/null +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -0,0 +1,56 @@ +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public enum ModPathChangeType +{ + Added, + Deleted, + Moved, +} + +public partial class Mod2 +{ + public DirectoryInfo BasePath { get; private set; } + public int Index { get; private set; } = -1; + + private FileInfo MetaFile + => new(Path.Combine( BasePath.FullName, "meta.json" )); + + private Mod2( ModFolder parentFolder, DirectoryInfo basePath ) + { + BasePath = basePath; + Order = new Mod.SortOrder( parentFolder, Name ); + //Order.ParentFolder.AddMod( this ); // TODO + ComputeChangedItems(); + } + + public static Mod2? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + { + basePath.Refresh(); + if( !basePath.Exists ) + { + PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + return null; + } + + var mod = new Mod2( parentFolder, basePath ); + + var metaFile = mod.MetaFile; + if( !metaFile.Exists ) + { + PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); + return null; + } + + mod.LoadMetaFromFile( metaFile ); + if( mod.Name.Length == 0 ) + { + PluginLog.Error( $"Mod at {basePath} without name is not supported." ); + } + + mod.ReloadFiles(); + return mod; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.ChangedItems.cs b/Penumbra/Mods/Mod2.ChangedItems.cs new file mode 100644 index 00000000..c6007321 --- /dev/null +++ b/Penumbra/Mods/Mod2.ChangedItems.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public SortedList ChangedItems { get; } = new(); + public string LowerChangedItemsString { get; private set; } = string.Empty; + + public void ComputeChangedItems() + { + var identifier = GameData.GameData.GetIdentifier(); + ChangedItems.Clear(); + foreach( var (file, _) in AllFiles ) + { + identifier.Identify( ChangedItems, file.ToGamePath() ); + } + + // TODO: manipulations + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs b/Penumbra/Mods/Mod2.Files.MultiModGroup.cs new file mode 100644 index 00000000..4ce3fd48 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.MultiModGroup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class MultiModGroup : IModGroup + { + public SelectType Type + => SelectType.Multi; + + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } = 0; + + public int OptionPriority( Index idx ) + => PrioritizedOptions[ idx ].Priority; + + public ISubMod this[ Index idx ] + => PrioritizedOptions[ idx ].Mod; + + public int Count + => PrioritizedOptions.Count; + + public readonly List< (SubMod Mod, int Priority) > PrioritizedOptions = new(); + + public IEnumerator< ISubMod > GetEnumerator() + => PrioritizedOptions.Select( o => o.Mod ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void Save( DirectoryInfo basePath ) + { + var path = ( ( IModGroup )this ).FileName( basePath ); + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( path, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs b/Penumbra/Mods/Mod2.Files.SingleModGroup.cs new file mode 100644 index 00000000..92212ad7 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.SingleModGroup.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class SingleModGroup : IModGroup + { + public SelectType Type + => SelectType.Single; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } = 0; + + public readonly List< SubMod > OptionData = new(); + + public int OptionPriority( Index _ ) + => Priority; + + public ISubMod this[ Index idx ] + => OptionData[ idx ]; + + public int Count + => OptionData.Count; + + public IEnumerator< ISubMod > GetEnumerator() + => OptionData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void Save( DirectoryInfo basePath ) + { + var path = ( ( IModGroup )this ).FileName( basePath ); + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( path, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SubMod.cs b/Penumbra/Mods/Mod2.Files.SubMod.cs new file mode 100644 index 00000000..245de4f8 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.SubMod.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class SubMod : ISubMod + { + public string Name { get; set; } = "Default"; + + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); + + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + + public readonly List< MetaManipulation > ManipulationData = new(); + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlyList< MetaManipulation > Manipulations + => ManipulationData; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs new file mode 100644 index 00000000..67cd3ad4 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + public IReadOnlyDictionary< Utf8GamePath, FullPath > RemainingFiles + => _remainingFiles; + + public IReadOnlyList< IModGroup > Options + => _options; + + public bool HasOptions { get; private set; } = false; + + private void SetHasOptions() + { + HasOptions = _options.Any( o + => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); + } + + private readonly Dictionary< Utf8GamePath, FullPath > _remainingFiles = new(); + private readonly List< IModGroup > _options = new(); + + public IEnumerable< (Utf8GamePath, FullPath) > AllFiles + => _remainingFiles.Concat( _options.SelectMany( o => o ).SelectMany( o => o.Files.Concat( o.FileSwaps ) ) ) + .Select( kvp => ( kvp.Key, kvp.Value ) ); + + public IEnumerable< MetaManipulation > AllManipulations + => _options.SelectMany( o => o ).SelectMany( o => o.Manipulations ); + + private void ReloadFiles() + { + // _remainingFiles.Clear(); + // _options.Clear(); + // HasOptions = false; + // if( !Directory.Exists( BasePath.FullName ) ) + // return; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.BasePath.cs b/Penumbra/Mods/Mod2.Manager.BasePath.cs new file mode 100644 index 00000000..a0c9b8ca --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.BasePath.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + public partial class Manager + { + public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ); + + public event ModPathChangeDelegate? ModPathChanged; + + public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) + { + var mod = this[ idx ]; + // TODO + } + + public void DeleteMod( Index idx ) + { + var mod = this[ idx ]; + if( Directory.Exists( mod.BasePath.FullName ) ) + { + try + { + Directory.Delete( mod.BasePath.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" ); + } + } + + // TODO + // mod.Order.ParentFolder.RemoveMod( mod ); + // _mods.RemoveAt( idx ); + //for( var i = idx; i < _mods.Count; ++i ) + //{ + // --_mods[i].Index; + //} + + ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + } + + public Mod2 AddMod( DirectoryInfo modFolder ) + { + // TODO + + //var mod = LoadMod( StructuredMods, modFolder ); + //if( mod == null ) + //{ + // return -1; + //} + // + //if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + //{ + // if( SetSortOrderPath( mod, sortOrder ) ) + // { + // Config.Save(); + // } + //} + // + //if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) + //{ + // return -1; + //} + // + //_mods.Add( mod ); + //ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); + // + return this[^1]; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Meta.cs b/Penumbra/Mods/Mod2.Manager.Meta.cs new file mode 100644 index 00000000..5fa8c6f8 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Meta.cs @@ -0,0 +1,67 @@ +using System; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public partial class Manager + { + public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod2 mod ); + public event ModMetaChangeDelegate? ModMetaChanged; + + public void ChangeModName( Index idx, string newName ) + { + var mod = this[ idx ]; + if( mod.Name != newName ) + { + mod.Name = newName; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Name, mod ); + } + } + + public void ChangeModAuthor( Index idx, string newAuthor ) + { + var mod = this[ idx ]; + if( mod.Author != newAuthor ) + { + mod.Author = newAuthor; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Author, mod ); + } + } + + public void ChangeModDescription( Index idx, string newDescription ) + { + var mod = this[ idx ]; + if( mod.Description != newDescription ) + { + mod.Description = newDescription; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Description, mod ); + } + } + + public void ChangeModVersion( Index idx, string newVersion ) + { + var mod = this[ idx ]; + if( mod.Version != newVersion ) + { + mod.Version = newVersion; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Version, mod ); + } + } + + public void ChangeModWebsite( Index idx, string newWebsite ) + { + var mod = this[ idx ]; + if( mod.Website != newWebsite ) + { + mod.Website = newWebsite; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Website, mod ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Options.cs b/Penumbra/Mods/Mod2.Manager.Options.cs new file mode 100644 index 00000000..3c26d6be --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Options.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionChanged, + DisplayChange, +} + +public sealed partial class Mod2 +{ + public sealed partial class Manager + { + public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ); + public event ModOptionChangeDelegate ModOptionChanged; + + public void RenameModGroup( Mod2 mod, int groupIdx, string newName ) + { + var group = mod._options[ groupIdx ]; + var oldName = group.Name; + if( oldName == newName || !VerifyFileName( mod, group, newName ) ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; + + ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, 0 ); + } + + public void AddModGroup( Mod2 mod, SelectType type, string newName ) + { + if( !VerifyFileName( mod, null, newName ) ) + { + return; + } + + var maxPriority = mod._options.Max( o => o.Priority ) + 1; + + mod._options.Add( type == SelectType.Multi + ? new MultiModGroup { Name = newName, Priority = maxPriority } + : new SingleModGroup { Name = newName, Priority = maxPriority } ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._options.Count - 1, 0 ); + } + + public void DeleteModGroup( Mod2 mod, int groupIdx ) + { + var group = mod._options[ groupIdx ]; + mod._options.RemoveAt( groupIdx ); + group.DeleteFile( BasePath ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 ); + } + + public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription ) + { + var group = mod._options[ groupIdx ]; + if( group.Description == newDescription ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, 0 ); + } + + public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority ) + { + var group = mod._options[ groupIdx ]; + if( group.Priority == newPriority ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1 ); + } + + public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + ChangeGroupPriority( mod, groupIdx, newPriority ); + break; + case MultiModGroup m: + if( m.PrioritizedOptions[ optionIdx ].Priority == newPriority ) + { + return; + } + + m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority ); + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx ); + return; + } + } + + public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + if( s.OptionData[ optionIdx ].Name == newName ) + { + return; + } + + s.OptionData[ optionIdx ].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[ optionIdx ].Mod; + if( option.Name == newName ) + { + return; + } + + option.Name = newName; + return; + } + + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx ); + } + + public void AddOption( Mod2 mod, int groupIdx, string newName ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + s.OptionData.Add( new SubMod { Name = newName } ); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add( ( new SubMod { Name = newName }, 0 ) ); + break; + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._options[ groupIdx ].Count - 1 ); + } + + public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + s.OptionData.RemoveAt( optionIdx ); + break; + case MultiModGroup m: + m.PrioritizedOptions.RemoveAt( optionIdx ); + break; + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx ); + } + + public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var idx = subMod.ManipulationData.FindIndex( m => m.Equals( manip ) ); + if( delete ) + { + if( idx < 0 ) + { + return; + } + + subMod.ManipulationData.RemoveAt( idx ); + } + else + { + if( idx >= 0 ) + { + if( manip.EntryEquals( subMod.ManipulationData[ idx ] ) ) + { + return; + } + + subMod.ManipulationData[ idx ] = manip; + } + else + { + subMod.ManipulationData.Add( manip ); + } + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + + public void OptionSetFile( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( OptionSetFile( subMod.FileData, gamePath, newPath ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + } + + public void OptionSetFileSwap( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + } + + private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName ) + { + var path = newName.RemoveInvalidPathSymbols(); + if( mod.Options.Any( o => !ReferenceEquals( o, group ) + && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) + { + PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); + return false; + } + + return true; + } + + private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx ) + { + return mod._options[ groupIdx ] switch + { + SingleModGroup s => s.OptionData[ optionIdx ], + MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, + _ => throw new InvalidOperationException(), + }; + } + + private static bool OptionSetFile( IDictionary< Utf8GamePath, FullPath > dict, Utf8GamePath gamePath, FullPath? newPath ) + { + if( dict.TryGetValue( gamePath, out var oldPath ) ) + { + if( newPath == null ) + { + dict.Remove( gamePath ); + return true; + } + + if( newPath.Value.Equals( oldPath ) ) + { + return false; + } + + dict[ gamePath ] = newPath.Value; + return true; + } + + if( newPath == null ) + { + return false; + } + + dict.Add( gamePath, newPath.Value ); + return true; + } + + private static void OnModOptionChange( ModOptionChangeType type, Mod2 mod, int groupIdx, int _ ) + { + // File deletion is handled in the actual function. + if( type != ModOptionChangeType.GroupDeleted ) + { + mod._options[groupIdx].Save( mod.BasePath ); + } + + // State can not change on adding groups, as they have no immediate options. + mod.HasOptions = type switch + { + ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._options[groupIdx].IsOption, + ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + _ => mod.HasOptions, + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Root.cs b/Penumbra/Mods/Mod2.Manager.Root.cs new file mode 100644 index 00000000..85216b66 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Root.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public sealed partial class Manager + { + public DirectoryInfo BasePath { get; private set; } = null!; + public bool Valid { get; private set; } + + + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; + + public void DiscoverMods( string newDir ) + { + SetBaseDirectory( newDir, false ); + DiscoverMods(); + } + + private void SetBaseDirectory( string newPath, bool firstTime ) + { + if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + if( newPath.Length == 0 ) + { + Valid = false; + BasePath = new DirectoryInfo( "." ); + } + else + { + var newDir = new DirectoryInfo( newPath ); + if( !newDir.Exists ) + { + try + { + Directory.CreateDirectory( newDir.FullName ); + newDir.Refresh(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); + } + } + + BasePath = newDir; + Valid = true; + if( Penumbra.Config.ModDirectory != BasePath.FullName ) + { + Penumbra.Config.ModDirectory = BasePath.FullName; + Penumbra.Config.Save(); + } + } + } + + public void DiscoverMods() + { + ModDiscoveryStarted?.Invoke(); + _mods.Clear(); + BasePath.Refresh(); + + // TODO + //StructuredMods.SubFolders.Clear(); + //StructuredMods.Mods.Clear(); + if( Valid && BasePath.Exists ) + { + foreach( var modFolder in BasePath.EnumerateDirectories() ) + { + //var mod = LoadMod( StructuredMods, modFolder ); + //if( mod == null ) + //{ + // continue; + //} + // + //mod.Index = _mods.Count; + //_mods.Add( mod ); + } + + //SetModStructure(); + } + + ModDiscoveryFinished?.Invoke(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.cs b/Penumbra/Mods/Mod2.Manager.cs new file mode 100644 index 00000000..2ba5fe27 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public sealed partial class Manager : IEnumerable< Mod2 > + { + private readonly List< Mod2 > _mods = new(); + + public Mod2 this[ Index idx ] + => _mods[ idx ]; + + public IReadOnlyList< Mod2 > Mods + => _mods; + + public int Count + => _mods.Count; + + public IEnumerator< Mod2 > GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + + public Manager( string modDirectory ) + { + SetBaseDirectory( modDirectory, true ); + ModOptionChanged += OnModOptionChange; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs new file mode 100644 index 00000000..a0975396 --- /dev/null +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + private static class Migration + { + public static void Migrate( Mod2 mod, string text ) + { + MigrateV0ToV1( mod, text ); + } + + private static void MigrateV0ToV1( Mod2 mod, string text ) + { + if( mod.FileVersion > 0 ) + { + return; + } + + var data = JObject.Parse( text ); + var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + foreach( var group in groups.Values ) + { } + + foreach( var swap in swaps ) + { } + } + + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] + public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType = SelectType.Single; + + public List< OptionV0 > Options = new(); + + public OptionGroupV0() + { } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod2.Meta.cs new file mode 100644 index 00000000..0bc6f72d --- /dev/null +++ b/Penumbra/Mods/Mod2.Meta.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Util; + +namespace Penumbra.Mods; + +[Flags] +public enum MetaChangeType : byte +{ + None = 0x00, + Name = 0x01, + Author = 0x02, + Description = 0x04, + Version = 0x08, + Website = 0x10, + Deletion = 0x20, +} + +public sealed partial class Mod2 +{ + public const uint CurrentFileVersion = 1; + public uint FileVersion { get; private set; } = CurrentFileVersion; + public LowerString Name { get; private set; } = "Mod"; + public LowerString Author { get; private set; } = LowerString.Empty; + public string Description { get; private set; } = string.Empty; + public string Version { get; private set; } = string.Empty; + public string Website { get; private set; } = string.Empty; + + private void SaveMeta() + => SaveToFile( MetaFile ); + + private MetaChangeType LoadMetaFromFile( FileInfo filePath ) + { + if( !File.Exists( filePath.FullName ) ) + { + return MetaChangeType.Deletion; + } + + try + { + var text = File.ReadAllText( filePath.FullName ); + var json = JObject.Parse( text ); + + var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty; + var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty; + var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty; + var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; + var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; + var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; + + MetaChangeType changes = 0; + if( newFileVersion < CurrentFileVersion ) + { + Migration.Migrate( this, text ); + FileVersion = newFileVersion; + } + + if( Name != newName ) + { + changes |= MetaChangeType.Name; + Name = newName; + } + + if( Author != newAuthor ) + { + changes |= MetaChangeType.Author; + Author = newAuthor; + } + + if( Description != newDescription ) + { + changes |= MetaChangeType.Description; + Description = newDescription; + } + + if( Version != newVersion ) + { + changes |= MetaChangeType.Version; + Version = newVersion; + } + + if( Website != newWebsite ) + { + changes |= MetaChangeType.Website; + Website = newWebsite; + } + + + return changes; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod meta:\n{e}" ); + return MetaChangeType.Deletion; + } + } + + private void SaveToFile( FileInfo filePath ) + { + try + { + var jObject = new JObject + { + { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, + { nameof( Name ), JToken.FromObject( Name ) }, + { nameof( Author ), JToken.FromObject( Author ) }, + { nameof( Description ), JToken.FromObject( Description ) }, + { nameof( Version ), JToken.FromObject( Version ) }, + { nameof( Website ), JToken.FromObject( Website ) }, + }; + File.WriteAllText( filePath.FullName, jObject.ToString( Formatting.Indented ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.SortOrder.cs b/Penumbra/Mods/Mod2.SortOrder.cs new file mode 100644 index 00000000..e3f1cc37 --- /dev/null +++ b/Penumbra/Mods/Mod2.SortOrder.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public Mod.SortOrder Order; + public override string ToString() + => Order.FullPath; +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs deleted file mode 100644 index 56673d20..00000000 --- a/Penumbra/Mods/ModManager.Directory.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Dalamud.Logging; - -namespace Penumbra.Mods; - -public partial class ModManagerNew -{ - private readonly List< Mod > _mods = new(); - - public IReadOnlyList< Mod > Mods - => _mods; - - public void DiscoverMods() - { - //_mods.Clear(); - // - //if( CheckValidity() ) - //{ - // foreach( var modFolder in BasePath.EnumerateDirectories() ) - // { - // var mod = ModData.LoadMod( StructuredMods, modFolder ); - // if( mod == null ) - // { - // continue; - // } - // - // Mods.Add( modFolder.Name, mod ); - // } - // - // SetModStructure(); - //} - // - //Collections.RecreateCaches(); - } -} - -public partial class ModManagerNew -{ - public DirectoryInfo BasePath { get; private set; } = null!; - public bool Valid { get; private set; } - - public event Action< DirectoryInfo >? BasePathChanged; - - public ModManagerNew() - { - InitBaseDirectory( Penumbra.Config.ModDirectory ); - } - - public bool CheckValidity() - { - if( Valid ) - { - Valid = Directory.Exists( BasePath.FullName ); - } - - return Valid; - } - - private static (DirectoryInfo, bool) CreateDirectory( string path ) - { - var newDir = new DirectoryInfo( path ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - return ( newDir, false ); - } - } - - return ( newDir, true ); - } - - private void InitBaseDirectory( string path ) - { - if( path.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - return; - } - - ( BasePath, Valid ) = CreateDirectory( path ); - - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - Penumbra.Config.ModDirectory = BasePath.FullName; - Penumbra.Config.Save(); - } - } - - private void ChangeBaseDirectory( string path ) - { - if( string.Equals( path, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - InitBaseDirectory( path ); - BasePathChanged?.Invoke( BasePath ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 8c1f90df..aeae873b 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -45,7 +45,8 @@ public partial class Mod public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod ); public event ModChangeDelegate? ModChange; - public event Action? ModsRediscovered; + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; public bool Valid { get; private set; } @@ -97,8 +98,6 @@ public partial class Mod Config.Save(); } } - - ModsRediscovered?.Invoke(); } public Manager() @@ -150,6 +149,7 @@ public partial class Mod public void DiscoverMods() { + ModDiscoveryStarted?.Invoke(); _mods.Clear(); BasePath.Refresh(); @@ -172,7 +172,7 @@ public partial class Mod SetModStructure(); } - ModsRediscovered?.Invoke(); + ModDiscoveryFinished?.Invoke(); } public void DeleteMod( DirectoryInfo modFolder ) @@ -235,7 +235,7 @@ public partial class Mod { var mod = Mods[ idx ]; var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force; var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 228f3bb2..68469515 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Util; @@ -12,51 +13,83 @@ namespace Penumbra.Mods; // Contains descriptive data about the mod as well as possible settings and fileswaps. public class ModMeta { - public uint FileVersion { get; set; } + public const uint CurrentFileVersion = 1; + [Flags] + public enum ChangeType : byte + { + Name = 0x01, + Author = 0x02, + Description = 0x04, + Version = 0x08, + Website = 0x10, + Deletion = 0x20, + } + + public uint FileVersion { get; set; } = CurrentFileVersion; public LowerString Name { get; set; } = "Mod"; public LowerString Author { get; set; } = LowerString.Empty; public string Description { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; public string Website { get; set; } = string.Empty; - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); + public bool HasGroupsWithConfig = false; - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + public bool RefreshHasGroupsWithConfig() + { + var oldValue = HasGroupsWithConfig; + HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); + return oldValue != HasGroupsWithConfig; + } - [JsonIgnore] - private int FileHash { get; set; } - - [JsonIgnore] - public bool HasGroupsWithConfig { get; private set; } - - public bool RefreshFromFile( FileInfo filePath ) + public ChangeType RefreshFromFile( FileInfo filePath ) { var newMeta = LoadFromFile( filePath ); if( newMeta == null ) { - return true; + return ChangeType.Deletion; } - if( newMeta.FileHash == FileHash ) + ChangeType changes = 0; + + if( Name != newMeta.Name ) { - return false; + changes |= ChangeType.Name; + Name = newMeta.Name; } - FileVersion = newMeta.FileVersion; - Name = newMeta.Name; - Author = newMeta.Author; - Description = newMeta.Description; - Version = newMeta.Version; - Website = newMeta.Website; - FileSwaps = newMeta.FileSwaps; - Groups = newMeta.Groups; - FileHash = newMeta.FileHash; - HasGroupsWithConfig = newMeta.HasGroupsWithConfig; - return true; + if( Author != newMeta.Author ) + { + changes |= ChangeType.Author; + Author = newMeta.Author; + } + + if( Description != newMeta.Description ) + { + changes |= ChangeType.Description; + Description = newMeta.Description; + } + + if( Version != newMeta.Version ) + { + changes |= ChangeType.Version; + Version = newMeta.Version; + } + + if( Website != newMeta.Website ) + { + changes |= ChangeType.Website; + Website = newMeta.Website; + } + + return changes; } + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); + + public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + public static ModMeta? LoadFromFile( FileInfo filePath ) { try @@ -67,8 +100,8 @@ public class ModMeta new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); if( meta != null ) { - meta.FileHash = text.GetHashCode(); meta.RefreshHasGroupsWithConfig(); + Migration.Migrate( meta, text ); } return meta; @@ -80,29 +113,71 @@ public class ModMeta } } - public bool RefreshHasGroupsWithConfig() - { - var oldValue = HasGroupsWithConfig; - HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); - return oldValue != HasGroupsWithConfig; - } - public void SaveToFile( FileInfo filePath ) { try { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - var newHash = text.GetHashCode(); - if( newHash != FileHash ) - { - File.WriteAllText( filePath.FullName, text ); - FileHash = newHash; - } + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( filePath.FullName, text ); } catch( Exception e ) { PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); } } + + private static class Migration + { + public static void Migrate( ModMeta meta, string text ) + { + MigrateV0ToV1( meta, text ); + } + + private static void MigrateV0ToV1( ModMeta meta, string text ) + { + if( meta.FileVersion > 0 ) + { + return; + } + + var data = JObject.Parse( text ); + var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + foreach( var group in groups.Values ) + { } + + foreach( var swap in swaps ) + { } + + //var meta = + } + + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] + public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType = SelectType.Single; + + public List< OptionV0 > Options = new(); + + public OptionGroupV0() + { } + } + } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index a61f6e90..8331ceb8 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -393,7 +393,7 @@ public partial class SettingsInterface { var ret = false; var id = list[ manipIdx ].Est; - var val = id.SkeletonIdx; + var val = id.Entry; if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) { @@ -570,7 +570,7 @@ public partial class SettingsInterface case MetaManipulation.Type.Est: changes = DrawEstRow( manipIdx, list ); ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Est.SkeletonIdx}##{manipIdx}" ) ) + if( ImGui.Selectable( $"{list[ manipIdx ].Est.Entry}##{manipIdx}" ) ) { ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } diff --git a/Penumbra/Util/Functions.cs b/Penumbra/Util/Functions.cs new file mode 100644 index 00000000..a3c50e4a --- /dev/null +++ b/Penumbra/Util/Functions.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Penumbra.Util; + +public static class Functions +{ + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + public static bool SetDifferent< T >( T oldValue, T newValue, Action< T > set ) where T : IEquatable< T > + { + if( oldValue.Equals( newValue ) ) + { + return false; + } + + set( newValue ); + return true; + } +} \ No newline at end of file From a806dd28c380d04795c5008130560038e8fded27 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Mar 2022 13:34:07 +0200 Subject: [PATCH 0128/2451] Add OtterGui reference for shared interface code. --- .gitmodules | 4 ++++ OtterGui | 1 + Penumbra.sln | 10 ++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 OtterGui diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..685aaa2e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "OtterGui"] + path = OtterGui + url = git@github.com:Ottermandias/OtterGui.git + branch = main diff --git a/OtterGui b/OtterGui new file mode 160000 index 00000000..0d5859c5 --- /dev/null +++ b/OtterGui @@ -0,0 +1 @@ +Subproject commit 0d5859c54e91b62065a4b9e81b7533ba35674fc5 diff --git a/Penumbra.sln b/Penumbra.sln index 58df4bf5..57a5cff2 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29709.97 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32210.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra", "Penumbra\Penumbra.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" EndProject @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{01685BD8-8847-4B49-BF90-1683B4C76B0E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{87750518-1A20-40B4-9FC1-22F906EFB290}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.Build.0 = Release|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d906e5aedfab68e159889c3ebe533f64d6a439e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Mar 2022 17:11:48 +0200 Subject: [PATCH 0129/2451] tmp --- .../Collections/CollectionManager.Active.cs | 140 ++++++++++++------ Penumbra/Collections/CollectionManager.cs | 9 +- Penumbra/Configuration.cs | 18 +-- .../Interop/Resolver/PathResolver.Data.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 2 +- Penumbra/MigrateConfiguration.cs | 138 +++++++++++++++-- Penumbra/Mods/Mod2.Manager.FileSystem.cs | 12 ++ Penumbra/Mods/Mod2.Manager.cs | 2 + Penumbra/Mods/ModFileSystem.cs | 16 +- Penumbra/Mods/ModManager.cs | 29 +++- Penumbra/Mods/ModManagerEditExtensions.cs | 10 +- Penumbra/Penumbra.csproj | 1 + .../TabInstalled/TabInstalledModPanel.cs | 2 +- 13 files changed, 279 insertions(+), 102 deletions(-) create mode 100644 Penumbra/Mods/Mod2.Manager.FileSystem.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 5e264981..e1c6f830 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,6 +1,10 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Penumbra.Mods; using Penumbra.Util; @@ -65,23 +69,19 @@ public partial class ModCollection switch( type ) { case Type.Default: - Default = newCollection; - Penumbra.Config.DefaultCollection = newCollection.Name; + Default = newCollection; Penumbra.ResidentResources.Reload(); Default.SetFiles(); break; case Type.Current: - Current = newCollection; - Penumbra.Config.CurrentCollection = newCollection.Name; + Current = newCollection; break; case Type.Character: - _characters[ characterName! ] = newCollection; - Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; + _characters[ characterName! ] = newCollection; break; } - CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName ); - Penumbra.Config.Save(); + CollectionChanged?.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } public void SetCollection( ModCollection collection, Type type, string? characterName = null ) @@ -95,10 +95,8 @@ public partial class ModCollection return false; } - _characters[ characterName ] = Empty; - Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name; - Penumbra.Config.Save(); - CollectionChanged?.Invoke( null, Empty, Type.Character, characterName ); + _characters[ characterName ] = Empty; + CollectionChanged?.Invoke( Type.Character, null, Empty, characterName ); return true; } @@ -109,12 +107,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged?.Invoke( collection, null, Type.Character, characterName ); - } - - if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) - { - Penumbra.Config.Save(); + CollectionChanged?.Invoke( Type.Character, collection, null, characterName ); } } @@ -122,59 +115,66 @@ public partial class ModCollection private int GetIndexForCollectionName( string name ) => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); + public static string ActiveCollectionFile + => Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); + // Load default, current and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. public void LoadCollections() { - var configChanged = false; - var defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + var file = ActiveCollectionFile; + var configChanged = true; + var jObject = new JObject(); + if( File.Exists( file ) ) + { + try + { + jObject = JObject.Parse( File.ReadAllText( file ) ); + configChanged = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); + } + } + + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name; + var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { - PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); - Default = Empty; - Penumbra.Config.DefaultCollection = Default.Name; - configChanged = true; + PluginLog.Error( $"Last choice of Default Collection {defaultName} is not available, reset to {Empty.Name}." ); + Default = Empty; + configChanged = true; } else { Default = this[ defaultIdx ]; } - var currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; + var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); - Current = DefaultName; - Penumbra.Config.DefaultCollection = Current.Name; - configChanged = true; + PluginLog.Error( $"Last choice of Current Collection {currentName} is not available, reset to {DefaultCollection}." ); + Current = DefaultName; + configChanged = true; } else { Current = this[ currentIdx ]; } - if( LoadCharacterCollections() || configChanged ) - { - Penumbra.Config.Save(); - } - - CreateNecessaryCaches(); - } - - // Load character collections. If a player name comes up multiple times, the last one is applied. - private bool LoadCharacterCollections() - { - var configChanged = false; - foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() ) + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject[ nameof( Characters ) ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); + foreach( var (player, collectionName) in characters ) { var idx = GetIndexForCollectionName( collectionName ); if( idx < 0 ) { - PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); + PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); _characters.Add( player, Empty ); - Penumbra.Config.CharacterCollections[ player ] = Empty.Name; - configChanged = true; + configChanged = true; } else { @@ -182,7 +182,55 @@ public partial class ModCollection } } - return configChanged; + if( configChanged ) + { + SaveActiveCollections(); + } + + CreateNecessaryCaches(); + } + + + public void SaveActiveCollections() + => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ) ); + + internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters ) + { + var file = ActiveCollectionFile; + try + { + using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); + using var writer = new StreamWriter( stream ); + using var j = new JsonTextWriter( writer ); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName( nameof( Default ) ); + j.WriteValue( def ); + j.WritePropertyName( nameof( Current ) ); + j.WriteValue( current ); + j.WritePropertyName( nameof( Characters ) ); + j.WriteStartObject(); + foreach( var (character, collection) in characters ) + { + j.WritePropertyName( character, true ); + j.WriteValue( collection ); + } + + j.WriteEndObject(); + j.WriteEndObject(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save active collections to file {file}:\n{e}" ); + } + } + + private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 ) + { + if( type != Type.Inactive ) + { + SaveActiveCollections(); + } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index d54ffa05..a04a36e6 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -24,10 +24,10 @@ public partial class ModCollection { // On addition, oldCollection is null. On deletion, newCollection is null. // CharacterName is onls set for type == Character. - public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, Type type, + public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, string? characterName = null ); - private readonly Mods.Mod.Manager _modManager; + private readonly Mod.Manager _modManager; // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -64,6 +64,7 @@ public partial class ModCollection _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; _modManager.ModChange += OnModChanged; + CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); } @@ -95,7 +96,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); + CollectionChanged?.Invoke( Type.Inactive, null, newCollection ); SetCollection( newCollection.Index, Type.Current ); return true; } @@ -139,7 +140,7 @@ public partial class ModCollection --_collections[ i ].Index; } - CollectionChanged?.Invoke( collection, null, Type.Inactive ); + CollectionChanged?.Invoke( Type.Inactive, collection, null ); return true; } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index da2df11e..6221d992 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -5,11 +5,10 @@ using Dalamud.Logging; namespace Penumbra; - [Serializable] public partial class Configuration : IPluginConfiguration { - private const int CurrentVersion = 1; + private const int CurrentVersion = 2; public int Version { get; set; } = CurrentVersion; @@ -35,26 +34,19 @@ public partial class Configuration : IPluginConfiguration public string ModDirectory { get; set; } = string.Empty; - public string CurrentCollection { get; set; } = "Default"; - public string DefaultCollection { get; set; } = "Default"; - - public bool SortFoldersFirst { get; set; } = false; public bool HasReadCharacterCollectionDesc { get; set; } = false; - public Dictionary< string, string > CharacterCollections { get; set; } = new(); - public Dictionary< string, string > ModSortOrder { get; set; } = new(); - - public static Configuration Load() { - var configuration = Dalamud.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - if( configuration.Version == CurrentVersion ) + var iConfiguration = Dalamud.PluginInterface.GetPluginConfig(); + var configuration = iConfiguration as Configuration ?? new Configuration(); + if( iConfiguration is { Version: CurrentVersion } ) { return configuration; } - MigrateConfiguration.Version0To1( configuration ); + MigrateConfiguration.Migrate( configuration ); configuration.Save(); return configuration; diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index b0104fec..abcccc9a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -179,7 +179,7 @@ public unsafe partial class PathResolver } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection? _1, ModCollection? _2, ModCollection.Type type, string? name ) + private void CheckCollections( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? name ) { if( type is not (ModCollection.Type.Character or ModCollection.Type.Default) ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 844f5db9..4cd10e14 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -103,7 +103,7 @@ public partial class PathResolver : IDisposable Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; } - private void OnCollectionChange( ModCollection? _1, ModCollection? _2, ModCollection.Type type, string? characterName ) + private void OnCollectionChange( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? characterName ) { if( type != ModCollection.Type.Character ) { diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 57060fc9..cc2bce8a 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -3,37 +3,128 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Mods; namespace Penumbra; -public partial class Configuration +public class MigrateConfiguration { - public string ForcedCollection { internal get; set; } = ""; - public bool InvertModListOrder { internal get; set; } -} + private Configuration _config = null!; + private JObject _data = null!; -public static class MigrateConfiguration -{ - public static void Version0To1( Configuration config ) + public string CurrentCollection = ModCollection.DefaultCollection; + public string DefaultCollection = ModCollection.DefaultCollection; + public string ForcedCollection = string.Empty; + public Dictionary< string, string > CharacterCollections = new(); + public Dictionary< string, string > ModSortOrder = new(); + public bool InvertModListOrder = false; + + + public static void Migrate( Configuration config ) { - if( config.Version != 0 ) + var m = new MigrateConfiguration + { + _config = config, + _data = JObject.Parse( File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ) ), + }; + + m.CreateBackup(); + m.Version0To1(); + m.Version1To2(); + } + + private void Version1To2() + { + if( _config.Version != 1 ) { return; } - config.ModDirectory = config.CurrentCollection; - config.CurrentCollection = "Default"; - config.DefaultCollection = "Default"; - config.Version = 1; - ResettleCollectionJson( config ); + ResettleSortOrder(); + ResettleCollectionSettings(); + ResettleForcedCollection(); + _config.Version = 2; } - private static void ResettleCollectionJson( Configuration config ) + private void ResettleForcedCollection() { - var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) ); + ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; + if( ForcedCollection.Length <= 0 ) + { + return; + } + + foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) ) + { + try + { + var jObject = JObject.Parse( File.ReadAllText( collection ) ); + if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection ) + { + jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } ); + File.WriteAllText( collection, jObject.ToString() ); + } + } + catch( Exception e ) + { + PluginLog.Error( + $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); + } + } + } + + private void ResettleSortOrder() + { + ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; + var file = Mod2.Manager.ModFileSystemFile; + using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); + using var writer = new StreamWriter( stream ); + using var j = new JsonTextWriter( writer ); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName( "Data" ); + j.WriteStartObject(); + foreach( var (mod, path) in ModSortOrder ) + { + j.WritePropertyName( mod, true ); + j.WriteValue( path ); + } + + j.WriteEndObject(); + j.WritePropertyName( "EmptyFolders" ); + j.WriteStartArray(); + j.WriteEndArray(); + j.WriteEndObject(); + } + + private void ResettleCollectionSettings() + { + CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; + DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; + CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; + ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, + CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ) ); + } + + private void Version0To1() + { + if( _config.Version != 0 ) + { + return; + } + + _config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty; + _config.Version = 1; + ResettleCollectionJson(); + } + + private void ResettleCollectionJson() + { + var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) ); if( !collectionJson.Exists ) { return; @@ -71,7 +162,8 @@ public static class MigrateConfiguration maxPriority = Math.Max( maxPriority, priority ); } - if( !config.InvertModListOrder ) + InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; + if( !InvertModListOrder ) { foreach( var setting in dict.Values ) { @@ -88,4 +180,18 @@ public static class MigrateConfiguration throw; } } + + private void CreateBackup() + { + var name = Dalamud.PluginInterface.ConfigFile.FullName; + var bakName = name + ".bak"; + try + { + File.Copy( name, bakName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.FileSystem.cs b/Penumbra/Mods/Mod2.Manager.FileSystem.cs new file mode 100644 index 00000000..b8466c50 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.FileSystem.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public sealed partial class Manager + { + public static string ModFileSystemFile + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.cs b/Penumbra/Mods/Mod2.Manager.cs index 2ba5fe27..f385a7a0 100644 --- a/Penumbra/Mods/Mod2.Manager.cs +++ b/Penumbra/Mods/Mod2.Manager.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json.Linq; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 4cb331aa..c4fbcc89 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -128,7 +128,7 @@ public static partial class ModFileSystem { foreach( var mod in target.AllMods( true ) ) { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; + Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; } Penumbra.Config.Save(); @@ -136,16 +136,16 @@ public static partial class ModFileSystem } // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( global::Penumbra.Mods.Mod mod ) + private static void SaveMod( Mod mod ) { if( ReferenceEquals( mod.Order.ParentFolder, Root ) && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) { - Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); + Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); } else { - Penumbra.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; + Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; } Penumbra.Config.Save(); @@ -183,7 +183,7 @@ public static partial class ModFileSystem return true; } - private static bool RenameNoSave( global::Penumbra.Mods.Mod mod, string newName ) + private static bool RenameNoSave( Mod mod, string newName ) { newName = newName.Replace( '/', '\\' ); if( mod.Order.SortOrderName == newName ) @@ -192,12 +192,12 @@ public static partial class ModFileSystem } mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.Order = new global::Penumbra.Mods.Mod.SortOrder( mod.Order.ParentFolder, newName ); + mod.Order = new Mod.SortOrder( mod.Order.ParentFolder, newName ); mod.Order.ParentFolder.AddMod( mod ); return true; } - private static bool MoveNoSave( global::Penumbra.Mods.Mod mod, ModFolder target ) + private static bool MoveNoSave( Mod mod, ModFolder target ) { var oldParent = mod.Order.ParentFolder; if( ReferenceEquals( target, oldParent ) ) @@ -206,7 +206,7 @@ public static partial class ModFileSystem } oldParent.RemoveMod( mod ); - mod.Order = new global::Penumbra.Mods.Mod.SortOrder( target, mod.Order.SortOrderName ); + mod.Order = new Mod.SortOrder( target, mod.Order.SortOrderName ); target.AddMod( mod ); return true; } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index aeae873b..1f39e7c9 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Logging; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta; using Penumbra.Mods; @@ -103,21 +104,35 @@ public partial class Mod public Manager() { SetBaseDirectory( Config.ModDirectory, true ); + // TODO + try + { + var data = JObject.Parse( File.ReadAllText( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), + "sort_order.json" ) ) ); + TemporaryModSortOrder = data["Data"]?.ToObject>() ?? new Dictionary(); + + } + catch + { + TemporaryModSortOrder = new Dictionary(); + } } + public Dictionary TemporaryModSortOrder; + private bool SetSortOrderPath( Mod mod, string path ) { mod.Move( path ); var fixedPath = mod.Order.FullPath; if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) { - Config.ModSortOrder.Remove( mod.BasePath.Name ); + Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); return true; } if( path != fixedPath ) { - Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; + TemporaryModSortOrder[ mod.BasePath.Name ] = fixedPath; return true; } @@ -128,7 +143,7 @@ public partial class Mod { var changes = false; - foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) + foreach( var (folder, path) in TemporaryModSortOrder.ToArray() ) { if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) { @@ -137,7 +152,7 @@ public partial class Mod else if( removeOldPaths ) { changes = true; - Config.ModSortOrder.Remove( folder ); + TemporaryModSortOrder.Remove( folder ); } } @@ -212,7 +227,7 @@ public partial class Mod return -1; } - if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { if( SetSortOrderPath( mod, sortOrder ) ) { @@ -246,13 +261,13 @@ public partial class Mod if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) { mod.ComputeChangedItems(); - if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { mod.Move( sortOrder ); var path = mod.Order.FullPath; if( path != sortOrder ) { - Config.ModSortOrder[ mod.BasePath.Name ] = path; + TemporaryModSortOrder[ mod.BasePath.Name ] = path; Config.Save(); } } diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 4ecf5fb3..baf8ffe6 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -35,12 +35,12 @@ public static class ModManagerEditExtensions if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) { mod.Order = inRoot; - manager.Config.ModSortOrder.Remove( mod.BasePath.Name ); + manager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); } else { mod.Move( newSortOrder ); - manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; + manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; } manager.Config.Save(); @@ -75,10 +75,10 @@ public static class ModManagerEditExtensions mod.MetaFile = Mod.MetaFileInfo( newDir ); manager.UpdateMod( mod ); - if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) + if( manager.TemporaryModSortOrder.ContainsKey( oldBasePath.Name ) ) { - manager.Config.ModSortOrder[ newDir.Name ] = manager.Config.ModSortOrder[ oldBasePath.Name ]; - manager.Config.ModSortOrder.Remove( oldBasePath.Name ); + manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ]; + manager.TemporaryModSortOrder.Remove( oldBasePath.Name ); manager.Config.Save(); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 13a25cc0..a8a7591b 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -70,6 +70,7 @@ + diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 25fc4a4c..435dffd4 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -81,7 +81,7 @@ public partial class SettingsInterface if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) { _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - if( !modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) + if( !modManager.TemporaryModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) { Mod.Data.Rename( name ); } From c210a4f10a8525fc101c7a53d903bbc8df6d675d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Mar 2022 23:22:11 +0200 Subject: [PATCH 0130/2451] Add backup mechanism and some collection cleanup. --- .../Collections/CollectionManager.Active.cs | 52 ++++--- Penumbra/Collections/CollectionManager.cs | 4 +- Penumbra/Mods/ModManager.cs | 13 +- Penumbra/Penumbra.cs | 16 +- Penumbra/Util/Backup.cs | 146 ++++++++++++++++++ 5 files changed, 202 insertions(+), 29 deletions(-) create mode 100644 Penumbra/Util/Backup.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index e1c6f830..fe396ccd 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -15,7 +15,7 @@ public partial class ModCollection public sealed partial class Manager { // Is invoked after the collections actually changed. - public event CollectionChangeDelegate? CollectionChanged; + public event CollectionChangeDelegate CollectionChanged; // The collection currently selected for changing settings. public ModCollection Current { get; private set; } = Empty; @@ -81,7 +81,7 @@ public partial class ModCollection break; } - CollectionChanged?.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); + CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } public void SetCollection( ModCollection collection, Type type, string? characterName = null ) @@ -96,7 +96,7 @@ public partial class ModCollection } _characters[ characterName ] = Empty; - CollectionChanged?.Invoke( Type.Character, null, Empty, characterName ); + CollectionChanged.Invoke( Type.Character, null, Empty, characterName ); return true; } @@ -107,7 +107,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged?.Invoke( Type.Character, collection, null, characterName ); + CollectionChanged.Invoke( Type.Character, collection, null, characterName ); } } @@ -118,27 +118,13 @@ public partial class ModCollection public static string ActiveCollectionFile => Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); - // Load default, current and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. public void LoadCollections() { - var file = ActiveCollectionFile; - var configChanged = true; - var jObject = new JObject(); - if( File.Exists( file ) ) - { - try - { - jObject = JObject.Parse( File.ReadAllText( file ) ); - configChanged = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); - } - } + var configChanged = !ReadActiveCollections( out var jObject ); + // Load the default collection. var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name; var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) @@ -152,6 +138,7 @@ public partial class ModCollection Default = this[ defaultIdx ]; } + // Load the current collection. var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) @@ -182,6 +169,7 @@ public partial class ModCollection } } + // Save any changes and create all required caches. if( configChanged ) { SaveActiveCollections(); @@ -225,6 +213,30 @@ public partial class ModCollection } } + // Read the active collection file into a jObject. + // Returns true if this is successful, false if the file does not exist or it is unsuccessful. + private static bool ReadActiveCollections( out JObject ret ) + { + var file = ActiveCollectionFile; + if( File.Exists( file ) ) + { + try + { + ret = JObject.Parse( File.ReadAllText( file ) ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); + } + } + + ret = new JObject(); + return false; + } + + + // Save if any of the active collections is changed. private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 ) { if( type != Type.Inactive ) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index a04a36e6..79d7d33c 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -96,7 +96,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); - CollectionChanged?.Invoke( Type.Inactive, null, newCollection ); + CollectionChanged.Invoke( Type.Inactive, null, newCollection ); SetCollection( newCollection.Index, Type.Current ); return true; } @@ -140,7 +140,7 @@ public partial class ModCollection --_collections[ i ].Index; } - CollectionChanged?.Invoke( Type.Inactive, collection, null ); + CollectionChanged.Invoke( Type.Inactive, collection, null ); return true; } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 1f39e7c9..19458764 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -101,24 +101,25 @@ public partial class Mod } } + public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), + "sort_order.json" ); + public Manager() { SetBaseDirectory( Config.ModDirectory, true ); // TODO try { - var data = JObject.Parse( File.ReadAllText( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - "sort_order.json" ) ) ); - TemporaryModSortOrder = data["Data"]?.ToObject>() ?? new Dictionary(); - + var data = JObject.Parse( File.ReadAllText( SortOrderFile ) ); + TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); } catch { - TemporaryModSortOrder = new Dictionary(); + TemporaryModSortOrder = new Dictionary< string, string >(); } } - public Dictionary TemporaryModSortOrder; + public Dictionary< string, string > TemporaryModSortOrder; private bool SetSortOrderPath( Mod mod, string path ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 322c9f7e..ac6dcfa7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -54,6 +57,7 @@ public class Penumbra : IDalamudPlugin { Dalamud.Initialize( pluginInterface ); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); MusicManager = new MusicManager(); @@ -64,7 +68,7 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); - MetaFileManager = new MetaFileManager(); + MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new Mod.Manager(); @@ -337,4 +341,14 @@ public class Penumbra : IDalamudPlugin SettingsInterface.FlipVisibility(); } + + // Collect all relevant files for penumbra configuration. + private static IReadOnlyList< FileInfo > PenumbraBackupFiles() + { + var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); + list.Add( Dalamud.PluginInterface.ConfigFile ); + list.Add( new FileInfo( Mod.Manager.SortOrderFile ) ); + list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); + return list; + } } \ No newline at end of file diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs new file mode 100644 index 00000000..878731d7 --- /dev/null +++ b/Penumbra/Util/Backup.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using Dalamud.Logging; + +namespace Penumbra.Util; + +public static class Backup +{ + public const int MaxNumBackups = 10; + + // Create a backup named by ISO 8601 of the current time. + // If the newest previously existing backup equals the current state of files, + // do not create a new backup. + // If the maximum number of backups is exceeded afterwards, delete the oldest backup. + public static void CreateBackup( IReadOnlyCollection< FileInfo > files ) + { + try + { + var configDirectory = Dalamud.PluginInterface.ConfigDirectory.Parent!.FullName; + var directory = CreateBackupDirectory(); + var (newestFile, oldestFile, numFiles) = CheckExistingBackups( directory ); + var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmss}.zip" ); + if( newestFile == null || CheckNewestBackup( newestFile, configDirectory, files.Count ) ) + { + CreateBackup( files, newBackupName, configDirectory ); + if( numFiles > MaxNumBackups ) + { + oldestFile!.Delete(); + } + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create backups:\n{e}" ); + } + } + + + // Obtain the backup directory. Create it if it does not exist. + private static DirectoryInfo CreateBackupDirectory() + { + var path = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!.FullName, "backups", + Dalamud.PluginInterface.ConfigDirectory.Name ); + var dir = new DirectoryInfo( path ); + if( !dir.Exists ) + { + dir = Directory.CreateDirectory( dir.FullName ); + } + + return dir; + } + + // Check the already existing backups. + // Only keep MaxNumBackups at once, and delete the oldest if the number would be exceeded. + // Return the newest backup. + private static (FileInfo? Newest, FileInfo? Oldest, int Count) CheckExistingBackups( DirectoryInfo backupDirectory ) + { + var count = 0; + FileInfo? newest = null; + FileInfo? oldest = null; + + foreach( var file in backupDirectory.EnumerateFiles( "*.zip" ) ) + { + ++count; + var time = file.CreationTimeUtc; + if( ( oldest?.CreationTimeUtc ?? DateTime.MinValue ) < time ) + { + oldest = file; + } + + if( ( newest?.CreationTimeUtc ?? DateTime.MaxValue ) > time ) + { + newest = file; + } + } + + return ( newest, oldest, count ); + } + + // Compare the newest backup against the currently existing files. + // If there are any differences, return false, and if they are completely identical, return true. + private static bool CheckNewestBackup( FileInfo newestFile, string configDirectory, int fileCount ) + { + using var oldFileStream = File.Open( newestFile.FullName, FileMode.Open ); + using var oldZip = new ZipArchive( oldFileStream, ZipArchiveMode.Read ); + // Number of stored files is different. + if( fileCount != oldZip.Entries.Count ) + { + return true; + } + + // Since number of files is identical, + // the backups are identical if every file in the old backup + // still exists and is identical. + foreach( var entry in oldZip.Entries ) + { + var file = Path.Combine( configDirectory, entry.FullName ); + if( !File.Exists( file ) ) + { + return true; + } + + using var currentData = File.OpenRead( file ); + using var oldData = entry.Open(); + + if( !Equals( currentData, oldData ) ) + { + return true; + } + } + + return false; + } + + // Create the actual backup, storing all the files relative to the given configDirectory in the zip. + private static void CreateBackup( IEnumerable< FileInfo > files, string fileName, string configDirectory ) + { + using var fileStream = File.Open( fileName, FileMode.Create ); + using var zip = new ZipArchive( fileStream, ZipArchiveMode.Create ); + foreach( var file in files ) + { + zip.CreateEntryFromFile( file.FullName, Path.GetRelativePath( configDirectory, file.FullName ), CompressionLevel.Optimal ); + } + } + + // Compare two streams per byte and return if they are equal. + private static bool Equals( Stream lhs, Stream rhs ) + { + while( true ) + { + var current = lhs.ReadByte(); + var old = rhs.ReadByte(); + if( current != old ) + { + return false; + } + + if( current == -1 ) + { + return true; + } + } + } +} \ No newline at end of file From 33db1565444934dc9b0c6495d20a81fd94a24846 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 4 Apr 2022 20:57:53 +0200 Subject: [PATCH 0131/2451] test --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 0d5859c5..a78f17dd 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0d5859c54e91b62065a4b9e81b7533ba35674fc5 +Subproject commit a78f17dd1bc4cbe7e9d6c04af828ff1adac4bd6f From 8db54ef4f4444b8ebd38427d43798be332b1c273 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 6 Apr 2022 11:34:12 +0200 Subject: [PATCH 0132/2451] temp --- OtterGui | 2 +- Penumbra/Collections/ModCollection.Changes.cs | 45 ++- Penumbra/Mods/Mod2.Meta.cs | 2 +- Penumbra/Mods/ModFileSystemA.cs | 39 +++ Penumbra/Mods/ModMeta.cs | 1 + Penumbra/UI/Colors.cs | 10 + Penumbra/UI/MenuTabs/ModFileSystemSelector.cs | 330 ++++++++++++++++++ Penumbra/UI/MenuTabs/TabBrowser.cs | 47 ++- Penumbra/UI/MenuTabs/TabDebug.cs | 24 ++ Penumbra/UI/MenuTabs/TabEffective.cs | 1 + .../UI/MenuTabs/TabInstalled/ModFilter.cs | 93 ++--- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 25 +- .../TabInstalled/TabInstalledSelector.cs | 8 +- Penumbra/UI/MenuTabs/TabSettings.cs | 4 +- Penumbra/Util/Backup.cs | 3 +- Penumbra/Util/LowerString.cs | 122 ------- Penumbra/Util/ModelChanger.cs | 2 +- 17 files changed, 546 insertions(+), 212 deletions(-) create mode 100644 Penumbra/Mods/ModFileSystemA.cs create mode 100644 Penumbra/UI/Colors.cs create mode 100644 Penumbra/UI/MenuTabs/ModFileSystemSelector.cs delete mode 100644 Penumbra/Util/LowerString.cs diff --git a/OtterGui b/OtterGui index a78f17dd..baee5028 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a78f17dd1bc4cbe7e9d6c04af828ff1adac4bd6f +Subproject commit baee502862a5e8cdfa407f703ce98abad5cc623b diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index f4c1b32e..114efe63 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Penumbra.Mods; namespace Penumbra.Collections; @@ -6,10 +8,12 @@ namespace Penumbra.Collections; // Different types a mod setting can change: public enum ModSettingChange { - Inheritance, // it was set to inherit from other collections or not inherit anymore - EnableState, // it was enabled or disabled - Priority, // its priority was changed - Setting, // a specific setting was changed + Inheritance, // it was set to inherit from other collections or not inherit anymore + EnableState, // it was enabled or disabled + Priority, // its priority was changed + Setting, // a specific setting was changed + MultiInheritance, // multiple mods were set to inherit from other collections or not inherit anymore. + MultiEnableState, // multiple mods were enabled or disabled at once. } public partial class ModCollection @@ -28,7 +32,7 @@ public partial class ModCollection } } - // Set the enabled state mod idx to newValue if it differs from the current priority. + // Set the enabled state mod idx to newValue if it differs from the current enabled state. // If mod idx is currently inherited, stop the inheritance. public void SetModState( int idx, bool newValue ) { @@ -41,6 +45,37 @@ public partial class ModCollection } } + // Enable or disable the mod inheritance of every mod in mods. + public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) + { + if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) + { + ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, null, false ); + } + } + + // Set the enabled state of every mod in mods to the new value. + // If the mod is currently inherited, stop the inheritance. + public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue ) + { + var changes = false; + foreach( var mod in mods ) + { + var oldValue = _settings[ mod.Index ]?.Enabled ?? this[ mod.Index ].Settings?.Enabled ?? false; + if( newValue != oldValue ) + { + FixInheritance( mod.Index, false ); + _settings[ mod.Index ]!.Enabled = newValue; + changes = true; + } + } + + if( changes ) + { + ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, null, false ); + } + } + // Set the priority of mod idx to newValue if it differs from the current priority. // If mod idx is currently inherited, stop the inheritance. public void SetModPriority( int idx, int newValue ) diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod2.Meta.cs index 0bc6f72d..8a440829 100644 --- a/Penumbra/Mods/Mod2.Meta.cs +++ b/Penumbra/Mods/Mod2.Meta.cs @@ -3,7 +3,7 @@ using System.IO; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Penumbra.Util; +using OtterGui; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystemA.cs new file mode 100644 index 00000000..1bece099 --- /dev/null +++ b/Penumbra/Mods/ModFileSystemA.cs @@ -0,0 +1,39 @@ +using System.IO; +using OtterGui.Filesystem; + +namespace Penumbra.Mods; + +public sealed class ModFileSystemA : FileSystem< Mod > +{ + public void Save() + => SaveToFile( new FileInfo( Mod.Manager.SortOrderFile ), SaveMod, true ); + + public static ModFileSystemA Load() + { + var x = new ModFileSystemA(); + if( x.Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + { + x.Save(); + } + + x.Changed += ( _1, _2, _3, _4 ) => x.Save(); + + return x; + } + + private static string ModToIdentifier( Mod mod ) + => mod.BasePath.Name; + + private static string ModToName( Mod mod ) + => mod.Meta.Name.Text; + + private static (string, bool) SaveMod( Mod mod, string fullPath ) + { + if( fullPath == ModToName( mod ) ) + { + return ( string.Empty, false ); + } + + return ( ModToIdentifier( mod ), true ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 68469515..253839ce 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -5,6 +5,7 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.GameData.ByteString; using Penumbra.Util; diff --git a/Penumbra/UI/Colors.cs b/Penumbra/UI/Colors.cs new file mode 100644 index 00000000..013af594 --- /dev/null +++ b/Penumbra/UI/Colors.cs @@ -0,0 +1,10 @@ +namespace Penumbra.UI; + +public static class Colors +{ + public const uint DefaultTextColor = 0xFFFFFFFFu; + public const uint NewModColor = 0xFF66DD66u; + public const uint DisabledModColor = 0xFF666666u; + public const uint ConflictingModColor = 0xFFAAAAFFu; + public const uint HandledConflictModColor = 0xFF88DDDDu; +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs b/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs new file mode 100644 index 00000000..c80a0c47 --- /dev/null +++ b/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Mods; + +namespace Penumbra.UI; + +public sealed class ModFileSystemSelector : FileSystemSelector< Mod, ModState > +{ + private readonly IReadOnlySet< Mod > _newMods = new HashSet(); + private LowerString _modFilter = LowerString.Empty; + private LowerString _modFilterAuthor = LowerString.Empty; + private LowerString _modFilterChanges = LowerString.Empty; + private LowerString _modFilterName = LowerString.Empty; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + + public ModFilter StateFilter + { + get => _stateFilter; + set + { + var diff = _stateFilter != value; + _stateFilter = value; + if( diff ) + { + SetFilterDirty(); + } + } + } + + protected override bool ChangeFilter( string filterValue ) + { + if( filterValue.StartsWith( "c:", StringComparison.InvariantCultureIgnoreCase ) ) + { + _modFilterChanges = new LowerString( filterValue[ 2.. ] ); + _modFilter = LowerString.Empty; + _modFilterAuthor = LowerString.Empty; + _modFilterName = LowerString.Empty; + } + else if( filterValue.StartsWith( "a:", StringComparison.InvariantCultureIgnoreCase ) ) + { + _modFilterAuthor = new LowerString( filterValue[ 2.. ] ); + _modFilter = LowerString.Empty; + _modFilterChanges = LowerString.Empty; + _modFilterName = LowerString.Empty; + } + else if( filterValue.StartsWith( "n:", StringComparison.InvariantCultureIgnoreCase ) ) + { + _modFilterName = new LowerString( filterValue[ 2.. ] ); + _modFilter = LowerString.Empty; + _modFilterChanges = LowerString.Empty; + _modFilterAuthor = LowerString.Empty; + } + else + { + _modFilter = new LowerString( filterValue ); + _modFilterAuthor = LowerString.Empty; + _modFilterChanges = LowerString.Empty; + _modFilterName = LowerString.Empty; + } + + return true; + } + + private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + { + if( count == 0 ) + { + if( StateFilter.HasFlag( hasNoFlag ) ) + { + return false; + } + } + else if( StateFilter.HasFlag( hasFlag ) ) + { + return false; + } + + return true; + } + + private ModState GetModState( Mod mod, ModSettings? settings ) + { + if( settings?.Enabled != true ) + { + return new ModState { Color = ImGui.GetColorU32( ImGuiCol.TextDisabled ) }; + } + + return new ModState { Color = ImGui.GetColorU32( ImGuiCol.Text ) }; + } + + protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) + { + if( path is ModFileSystemA.Folder f ) + { + return base.ApplyFiltersAndState( f, out state ); + } + + return ApplyFiltersAndState( ( ModFileSystemA.Leaf )path, out state ); + } + + private bool CheckPath( string path, Mod mod ) + => _modFilter.IsEmpty + || path.Contains( _modFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) + || mod.Meta.Name.Contains( _modFilter ); + + private bool CheckName( Mod mod ) + => _modFilterName.IsEmpty || mod.Meta.Name.Contains( _modFilterName ); + + private bool CheckAuthor( Mod mod ) + => _modFilterAuthor.IsEmpty || mod.Meta.Author.Contains( _modFilterAuthor ); + + private bool CheckItems( Mod mod ) + => _modFilterChanges.IsEmpty || mod.LowerChangedItemsString.Contains( _modFilterChanges.Lower ); + + private bool ApplyFiltersAndState( ModFileSystemA.Leaf leaf, out ModState state ) + { + state = new ModState { Color = Colors.DefaultTextColor }; + var mod = leaf.Value; + var (settings, collection) = Current[ mod.Index ]; + // Check string filters. + if( !( CheckPath( leaf.FullName(), mod ) + && CheckName( mod ) + && CheckAuthor( mod ) + && CheckItems( mod ) ) ) + { + return true; + } + + var isNew = _newMods.Contains( mod ); + if( CheckFlags( mod.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) + || CheckFlags( mod.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) + || CheckFlags( mod.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) + || CheckFlags( mod.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) + || CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) + { + return true; + } + + if( settings == null ) + { + state.Color = Colors.DisabledModColor; + if( !StateFilter.HasFlag( ModFilter.Undefined ) ) + { + return true; + } + + settings = new ModSettings(); + } + + + if( !settings.Enabled ) + { + state.Color = Colors.DisabledModColor; + if( !StateFilter.HasFlag( ModFilter.Disabled ) ) + { + return true; + } + } + else + { + if( !StateFilter.HasFlag( ModFilter.Enabled ) ) + { + return true; + } + + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); + if( conflicts.Count > 0 ) + { + if( conflicts.Any( c => !c.Solved ) ) + { + if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) + { + return true; + } + + state.Color = Colors.ConflictingModColor; + } + else + { + if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) + { + return true; + } + + state.Color = Colors.HandledConflictModColor; + } + } + else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return true; + } + } + + if( collection == Current ) + { + if( !StateFilter.HasFlag( ModFilter.Uninherited ) ) + { + return true; + } + } + else + { + if( !StateFilter.HasFlag( ModFilter.Inherited ) ) + { + return true; + } + } + + if( isNew ) + { + state.Color = Colors.NewModColor; + } + + return false; + } + + + protected override float CustomFilters( float width ) + { + var pos = ImGui.GetCursorPos(); + var remainingWidth = width - ImGui.GetFrameHeight(); + var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); + ImGui.SetCursorPos( comboPos ); + using var combo = ImRaii.Combo( "##filterCombo", string.Empty, + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); + + if( combo ) + { + ImGui.Text( "A" ); + ImGui.Text( "B" ); + ImGui.Text( "C" ); + } + + combo.Dispose(); + ImGui.SetCursorPos( pos ); + return remainingWidth; + } + + + public ModFileSystemSelector( ModFileSystemA fileSystem ) + : base( fileSystem ) + { + SubscribeRightClickFolder( EnableDescendants, 10 ); + SubscribeRightClickFolder( DisableDescendants, 10 ); + SubscribeRightClickFolder( InheritDescendants, 15 ); + SubscribeRightClickFolder( OwnDescendants, 15 ); + AddButton( AddNewModButton, 0 ); + AddButton( DeleteModButton, 1000 ); + } + + private static ModCollection Current + => Penumbra.CollectionManager.Current; + + private static void EnableDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Enable Descendants" ) ) + { + SetDescendants( folder, true, false ); + } + } + + private static void DisableDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Disable Descendants" ) ) + { + SetDescendants( folder, false, false ); + } + } + + private static void InheritDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Inherit Descendants" ) ) + { + SetDescendants( folder, true, true ); + } + } + + private static void OwnDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) + { + SetDescendants( folder, false, true ); + } + } + + private static void AddNewModButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) ) + { } + } + + private void DeleteModButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, + "Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) ) + { } + } + + private static void SetDescendants( ModFileSystemA.Folder folder, bool enabled, bool inherit = false ) + { + var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystemA.Leaf >().Select( l => l.Value ); + if( inherit ) + { + Current.SetMultipleModInheritances( mods, enabled ); + } + else + { + Current.SetMultipleModStates( mods, enabled ); + } + } + + public override SortMode SortMode + => Penumbra.Config.SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; + + protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + { + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); + using var _ = ImRaii.TreeNode( leaf.Value.Meta.Name, flags ); + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabBrowser.cs b/Penumbra/UI/MenuTabs/TabBrowser.cs index e88f88d6..6e00a9af 100644 --- a/Penumbra/UI/MenuTabs/TabBrowser.cs +++ b/Penumbra/UI/MenuTabs/TabBrowser.cs @@ -1,24 +1,37 @@ -using System.Diagnostics; -using ImGuiNET; +using System.Runtime.InteropServices; +using OtterGui.Raii; +using Penumbra.Mods; -namespace Penumbra.UI +namespace Penumbra.UI; + +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public struct ModState { - public partial class SettingsInterface - { - private class TabBrowser - { - [Conditional( "LEAVEMEALONE" )] - public void Draw() - { - var ret = ImGui.BeginTabItem( "Available Mods" ); - if( !ret ) - { - return; - } + public uint Color; +} - ImGui.Text( "woah" ); - ImGui.EndTabItem(); +public partial class SettingsInterface +{ + private class TabBrowser + { + private readonly ModFileSystemA _fileSystem; + private readonly ModFileSystemSelector _selector; + + public TabBrowser() + { + _fileSystem = ModFileSystemA.Load(); + _selector = new ModFileSystemSelector( _fileSystem ); + } + + public void Draw() + { + using var ret = ImRaii.TabItem( "Available Mods" ); + if( !ret ) + { + return; } + + _selector.Draw( 400 ); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index bfa7d1b8..18353834 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Numerics; using ImGuiNET; +using OtterGui.Raii; using Penumbra.Api; using Penumbra.UI.Custom; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -12,6 +13,17 @@ namespace Penumbra.UI; public partial class SettingsInterface { + private string ImGuiIdTester = string.Empty; + + private void DrawImGuiIdTester() + { + ImGui.SetNextItemWidth( 200 ); + ImGui.InputText( "##abc1", ref ImGuiIdTester, 32 ); + ImGui.SameLine(); + ImGui.Text( ImGui.GetID( ImGuiIdTester ).ToString( "X" ) ); + } + + private static void PrintValue( string name, string value ) { ImGui.TableNextRow(); @@ -289,6 +301,16 @@ public partial class SettingsInterface } } + private void DrawDebugTabUtility() + { + if( !ImGui.CollapsingHeader( "Utilities##Debug" ) ) + { + return; + } + + DrawImGuiIdTester(); + } + private void DrawDebugTab() { if( !ImGui.BeginTabItem( "Debug Tab" ) ) @@ -324,5 +346,7 @@ public partial class SettingsInterface ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); + DrawDebugTabUtility(); + ImGui.NewLine(); } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 8420cb5c..e9a962e7 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs index a90f620c..a5a0b6d8 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs @@ -1,50 +1,55 @@ using System; -namespace Penumbra.UI +namespace Penumbra.UI; + +[Flags] +public enum ModFilter { - [Flags] - public enum ModFilter - { - Enabled = 1 << 0, - Disabled = 1 << 1, - NoConflict = 1 << 2, - SolvedConflict = 1 << 3, - UnsolvedConflict = 1 << 4, - HasNoMetaManipulations = 1 << 5, - HasMetaManipulations = 1 << 6, - HasNoFileSwaps = 1 << 7, - HasFileSwaps = 1 << 8, - HasConfig = 1 << 9, - HasNoConfig = 1 << 10, - HasNoFiles = 1 << 11, - HasFiles = 1 << 12, - IsNew = 1 << 13, - NotNew = 1 << 14, - }; + Enabled = 1 << 0, + Disabled = 1 << 1, + NoConflict = 1 << 2, + SolvedConflict = 1 << 3, + UnsolvedConflict = 1 << 4, + HasNoMetaManipulations = 1 << 5, + HasMetaManipulations = 1 << 6, + HasNoFileSwaps = 1 << 7, + HasFileSwaps = 1 << 8, + HasConfig = 1 << 9, + HasNoConfig = 1 << 10, + HasNoFiles = 1 << 11, + HasFiles = 1 << 12, + IsNew = 1 << 13, + NotNew = 1 << 14, + Inherited = 1 << 15, + Uninherited = 1 << 16, + Undefined = 1 << 17, +}; - public static class ModFilterExtensions - { - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 15 ) - 1 ); +public static class ModFilterExtensions +{ + public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 18 ) - 1 ); - public static string ToName( this ModFilter filter ) - => filter switch - { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", - ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), - }; - } + public static string ToName( this ModFilter filter ) + => filter switch + { + ModFilter.Enabled => "Enabled", + ModFilter.Disabled => "Disabled", + ModFilter.NoConflict => "No Conflicts", + ModFilter.SolvedConflict => "Solved Conflicts", + ModFilter.UnsolvedConflict => "Unsolved Conflicts", + ModFilter.HasNoMetaManipulations => "No Meta Manipulations", + ModFilter.HasMetaManipulations => "Meta Manipulations", + ModFilter.HasNoFileSwaps => "No File Swaps", + ModFilter.HasFileSwaps => "File Swaps", + ModFilter.HasNoConfig => "No Configuration", + ModFilter.HasConfig => "Configuration", + ModFilter.HasNoFiles => "No Files", + ModFilter.HasFiles => "Files", + ModFilter.IsNew => "Newly Imported", + ModFilter.NotNew => "Not Newly Imported", + ModFilter.Inherited => "Inherited Configuration", + ModFilter.Uninherited => "Own Configuration", + ModFilter.Undefined => "Not Configured", + _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), + }; } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index 7c35efea..1b9b48f9 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; +using OtterGui; using Penumbra.Mods; using Penumbra.Util; @@ -9,12 +10,7 @@ namespace Penumbra.UI; public class ModListCache : IDisposable { - public const uint NewModColor = 0xFF66DD66u; - public const uint DisabledModColor = 0xFF666666u; - public const uint ConflictingModColor = 0xFFAAAAFFu; - public const uint HandledConflictModColor = 0xFF88DDDDu; - - private readonly Mods.Mod.Manager _manager; + private readonly Mod.Manager _manager; private readonly List< FullMod > _modsInOrder = new(); private readonly List< (bool visible, uint color) > _visibleMods = new(); @@ -24,10 +20,11 @@ public class ModListCache : IDisposable private LowerString _modFilter = LowerString.Empty; private LowerString _modFilterAuthor = LowerString.Empty; private LowerString _modFilterChanges = LowerString.Empty; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - private bool _listResetNecessary; - private bool _filterResetNecessary; + private bool _listResetNecessary; + private bool _filterResetNecessary; + + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; public ModFilter StateFilter { @@ -43,7 +40,7 @@ public class ModListCache : IDisposable } } - public ModListCache( Mods.Mod.Manager manager, IReadOnlySet< string > newMods ) + public ModListCache( Mod.Manager manager, IReadOnlySet< string > newMods ) { _manager = manager; _newMods = newMods; @@ -281,7 +278,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? DisabledModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? Colors.DisabledModColor : ret.Item2; } if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) @@ -299,7 +296,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? ConflictingModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? Colors.ConflictingModColor : ret.Item2; } else { @@ -308,7 +305,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? HandledConflictModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? Colors.HandledConflictModColor : ret.Item2; } } else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) @@ -319,7 +316,7 @@ public class ModListCache : IDisposable ret.Item1 = true; if( isNew ) { - ret.Item2 = NewModColor; + ret.Item2 = Colors.NewModColor; } SetFolderAndParentsVisible( mod.Data.Order.ParentFolder ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 6ccf9d3f..cc773a28 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -217,18 +217,18 @@ public partial class SettingsInterface ImGui.Text( "Enabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.DisabledModColor ), "Disabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.NewModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.NewModColor ), "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.HandledConflictModColor ), "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.ConflictingModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.ConflictingModColor ), "Enabled and conflicting with another enabled Mod on the same priority." ); ImGui.Unindent(); ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index ee11e961..fe36cebb 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -235,7 +235,8 @@ public partial class SettingsInterface } ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); + ImGuiComponents.HelpMarker( + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); } private static void DrawReloadResourceButton() @@ -342,7 +343,6 @@ public partial class SettingsInterface } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - DrawRootFolder(); DrawRediscoverButton(); diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs index 878731d7..a096e238 100644 --- a/Penumbra/Util/Backup.cs +++ b/Penumbra/Util/Backup.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using Dalamud.Logging; namespace Penumbra.Util; @@ -119,7 +120,7 @@ public static class Backup { using var fileStream = File.Open( fileName, FileMode.Create ); using var zip = new ZipArchive( fileStream, ZipArchiveMode.Create ); - foreach( var file in files ) + foreach( var file in files.Where( f => File.Exists( f.FullName ) ) ) { zip.CreateEntryFromFile( file.FullName, Path.GetRelativePath( configDirectory, file.FullName ), CompressionLevel.Optimal ); } diff --git a/Penumbra/Util/LowerString.cs b/Penumbra/Util/LowerString.cs deleted file mode 100644 index 4942a5bc..00000000 --- a/Penumbra/Util/LowerString.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using ImGuiNET; -using Newtonsoft.Json; - -namespace Penumbra.Util; - -[JsonConverter( typeof( Converter ) )] -public readonly struct LowerString : IEquatable< LowerString >, IComparable< LowerString > -{ - public static readonly LowerString Empty = new(string.Empty); - - public readonly string Text = string.Empty; - public readonly string Lower = string.Empty; - - public LowerString( string text ) - { - Text = string.Intern( text ); - Lower = string.Intern( text.ToLowerInvariant() ); - } - - - public int Length - => Text.Length; - - public int Count - => Length; - - public bool Equals( LowerString other ) - => string.Equals( Lower, other.Lower, StringComparison.InvariantCulture ); - - public bool Equals( string other ) - => string.Equals( Lower, other, StringComparison.InvariantCultureIgnoreCase ); - - public int CompareTo( LowerString other ) - => string.Compare( Lower, other.Lower, StringComparison.InvariantCulture ); - - public int CompareTo( string other ) - => string.Compare( Lower, other, StringComparison.InvariantCultureIgnoreCase ); - - public bool Contains( LowerString other ) - => Lower.Contains( other.Lower, StringComparison.InvariantCulture ); - - public bool Contains( string other ) - => Lower.Contains( other, StringComparison.InvariantCultureIgnoreCase ); - - public bool StartsWith( LowerString other ) - => Lower.StartsWith( other.Lower, StringComparison.InvariantCulture ); - - public bool StartsWith( string other ) - => Lower.StartsWith( other, StringComparison.InvariantCultureIgnoreCase ); - - public bool EndsWith( LowerString other ) - => Lower.EndsWith( other.Lower, StringComparison.InvariantCulture ); - - public bool EndsWith( string other ) - => Lower.EndsWith( other, StringComparison.InvariantCultureIgnoreCase ); - - public override string ToString() - => Text; - - public static implicit operator string( LowerString s ) - => s.Text; - - public static implicit operator LowerString( string s ) - => new(s); - - private class Converter : JsonConverter< LowerString > - { - public override void WriteJson( JsonWriter writer, LowerString value, JsonSerializer serializer ) - { - writer.WriteValue( value.Text ); - } - - public override LowerString ReadJson( JsonReader reader, Type objectType, LowerString existingValue, bool hasExistingValue, - JsonSerializer serializer ) - { - if( reader.Value is string text ) - { - return new LowerString( text ); - } - - return existingValue; - } - } - - public static bool InputWithHint( string label, string hint, ref LowerString s, uint maxLength = 128, - ImGuiInputTextFlags flags = ImGuiInputTextFlags.None ) - { - var tmp = s.Text; - if( !ImGui.InputTextWithHint( label, hint, ref tmp, maxLength, flags ) || tmp == s.Text ) - { - return false; - } - - s = new LowerString( tmp ); - return true; - } - - public override bool Equals( object? obj ) - => obj is LowerString lowerString && Equals( lowerString ); - - public override int GetHashCode() - => Text.GetHashCode(); - - public static bool operator ==( LowerString lhs, LowerString rhs ) - => lhs.Equals( rhs ); - - public static bool operator !=( LowerString lhs, LowerString rhs ) - => lhs.Equals( rhs ); - - public static bool operator ==( LowerString lhs, string rhs ) - => lhs.Equals( rhs ); - - public static bool operator !=( LowerString lhs, string rhs ) - => lhs.Equals( rhs ); - - public static bool operator ==( string lhs, LowerString rhs ) - => rhs.Equals( lhs ); - - public static bool operator !=( string lhs, LowerString rhs ) - => rhs.Equals( lhs ); -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 75c03c3e..fd9c4eb3 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -26,7 +26,7 @@ public static class ModelChanger && Encoding.UTF8.GetByteCount( to ) == to.Length; - [Conditional( "Debug" )] + [Conditional( "DEBUG" )] private static void WriteBackup( string name, byte[] text ) => File.WriteAllBytes( name + ".bak", text ); From 069ae772a5ae99efbf30c4ea17ac2e582cff59e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 7 Apr 2022 21:16:04 +0200 Subject: [PATCH 0133/2451] Working on the selector. --- OtterGui | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 3 +- .../Collections/ModCollection.Inheritance.cs | 3 +- Penumbra/Configuration.cs | 25 +- Penumbra/Mods/ModManager.cs | 1 - Penumbra/Mods/ModSettings.cs | 2 + Penumbra/UI/Classes/Colors.cs | 43 +++ .../Classes/ModFileSystemSelector.Filters.cs | 277 +++++++++++++++ Penumbra/UI/Classes/ModFileSystemSelector.cs | 181 ++++++++++ .../TabInstalled => Classes}/ModFilter.cs | 2 +- Penumbra/UI/Colors.cs | 10 - Penumbra/UI/MenuTabs/ModFileSystemSelector.cs | 330 ------------------ .../UI/MenuTabs/TabInstalled/ModListCache.cs | 9 +- .../TabInstalled/TabInstalledSelector.cs | 9 +- Penumbra/UI/MenuTabs/TabSettings.cs | 19 +- 15 files changed, 558 insertions(+), 358 deletions(-) create mode 100644 Penumbra/UI/Classes/Colors.cs create mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs create mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.cs rename Penumbra/UI/{MenuTabs/TabInstalled => Classes}/ModFilter.cs (98%) delete mode 100644 Penumbra/UI/Colors.cs delete mode 100644 Penumbra/UI/MenuTabs/ModFileSystemSelector.cs diff --git a/OtterGui b/OtterGui index baee5028..0c32ec43 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit baee502862a5e8cdfa407f703ce98abad5cc623b +Subproject commit 0c32ec432d38093a402e0dae6dc5c62a883b163b diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 226f7b05..3e700cb5 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -231,7 +231,8 @@ public partial class ModCollection return; } - var hasMeta = Penumbra.ModManager[ modIdx ].Resources.MetaManipulations.Count > 0; + var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance + || Penumbra.ModManager[ modIdx ].Resources.MetaManipulations.Count > 0; _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); } diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 390c9c87..e333e6e0 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -12,6 +12,7 @@ namespace Penumbra.Collections; public partial class ModCollection { // A change in inheritance usually requires complete recomputation. + // The bool signifies whether the change was in an already inherited collection. public event Action< bool > InheritanceChanged; private readonly List< ModCollection > _inheritance = new(); @@ -25,7 +26,7 @@ public partial class ModCollection { yield return this; - foreach( var collection in _inheritance.SelectMany( c => c._inheritance ) + foreach( var collection in _inheritance.SelectMany( c => c.GetFlattenedInheritance() ) .Where( c => !ReferenceEquals( this, c ) ) .Distinct() ) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 6221d992..63c4b303 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; using Dalamud.Configuration; using Dalamud.Logging; +using Penumbra.UI; +using Penumbra.UI.Classes; namespace Penumbra; @@ -37,17 +41,21 @@ public partial class Configuration : IPluginConfiguration public bool SortFoldersFirst { get; set; } = false; public bool HasReadCharacterCollectionDesc { get; set; } = false; + public Dictionary< ColorId, uint > Colors { get; set; } + = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); + public static Configuration Load() { var iConfiguration = Dalamud.PluginInterface.GetPluginConfig(); var configuration = iConfiguration as Configuration ?? new Configuration(); if( iConfiguration is { Version: CurrentVersion } ) { + configuration.AddColors( false ); return configuration; } MigrateConfiguration.Migrate( configuration ); - configuration.Save(); + configuration.AddColors( true ); return configuration; } @@ -63,4 +71,19 @@ public partial class Configuration : IPluginConfiguration PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); } } + + // Add missing colors to the dictionary if necessary. + private void AddColors( bool forceSave ) + { + var save = false; + foreach( var color in Enum.GetValues< ColorId >() ) + { + save |= Colors.TryAdd( color, color.Data().DefaultColor ); + } + + if( save || forceSave ) + { + Save(); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 19458764..fc126dd4 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -7,7 +7,6 @@ using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta; -using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModSettings.cs b/Penumbra/Mods/ModSettings.cs index aadc0242..d32c6d70 100644 --- a/Penumbra/Mods/ModSettings.cs +++ b/Penumbra/Mods/ModSettings.cs @@ -7,6 +7,8 @@ namespace Penumbra.Mods; // Contains the settings for a given mod. public class ModSettings { + public static readonly ModSettings Empty = new(); + public bool Enabled { get; set; } public int Priority { get; set; } public Dictionary< string, int > Settings { get; set; } = new(); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs new file mode 100644 index 00000000..9c95a9bc --- /dev/null +++ b/Penumbra/UI/Classes/Colors.cs @@ -0,0 +1,43 @@ +using System; + +namespace Penumbra.UI.Classes; + +public enum ColorId +{ + EnabledMod, + DisabledMod, + UndefinedMod, + InheritedMod, + InheritedDisabledMod, + NewMod, + ConflictingMod, + HandledConflictMod, + FolderExpanded, + FolderCollapsed, + FolderLine, +} + +public static class Colors +{ + public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) + => color switch + { + // @formatter:off + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), + // @formatter:on + }; + + public static uint Value( this ColorId color ) + => Penumbra.Config.Colors.TryGetValue( color, out var value ) ? value : color.Data().DefaultColor; +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs new file mode 100644 index 00000000..13c797d2 --- /dev/null +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using ImGuiNET; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Mods; + +namespace Penumbra.UI.Classes; + +public partial class ModFileSystemSelector +{ + [StructLayout( LayoutKind.Sequential, Pack = 1 )] + public struct ModState + { + public uint Color; + } + + private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >(); + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + + private void SetFilterTooltip() + { + FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" + + "Enter n:[string] to filter only for mod names and no paths.\n" + + "Enter c:[string] to filter for mods changing specific items.\n" + + "Enter a:[string] to filter for mods by specific authors."; + } + + // Appropriately identify and set the string filter and its type. + protected override bool ChangeFilter( string filterValue ) + { + ( _modFilter, _filterType ) = filterValue.Length switch + { + 0 => ( LowerString.Empty, -1 ), + > 1 when filterValue[ 1 ] == ':' => + filterValue[ 0 ] switch + { + 'n' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), + 'N' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), + 'a' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), + 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), + 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), + 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), + _ => ( new LowerString( filterValue ), 0 ), + }, + _ => ( new LowerString( filterValue ), 0 ), + }; + + return true; + } + + // Check the state filter for a specific pair of has/has-not flags. + // Uses count == 0 to check for has-not and count != 0 for has. + // Returns true if it should be filtered and false if not. + private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + { + return count switch + { + 0 when _stateFilter.HasFlag( hasNoFlag ) => false, + 0 => true, + _ when _stateFilter.HasFlag( hasFlag ) => false, + _ => true, + }; + } + + // The overwritten filter method also computes the state. + // Folders have default state and are filtered out on the direct string instead of the other options. + // If any filter is set, they should be hidden by default unless their children are visible, + // or they contain the path search string. + protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) + { + if( path is ModFileSystemA.Folder f ) + { + state = default; + return ModFilterExtensions.UnfilteredStateMods != _stateFilter + || FilterValue.Length > 0 && !f.FullName().Contains( FilterValue, IgnoreCase ); + } + + return ApplyFiltersAndState( ( ModFileSystemA.Leaf )path, out state ); + } + + // Apply the string filters. + private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod mod ) + { + return _filterType switch + { + -1 => false, + 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Meta.Name.Contains( _modFilter ) ), + 1 => !mod.Meta.Name.Contains( _modFilter ), + 2 => !mod.Meta.Author.Contains( _modFilter ), + 3 => !mod.LowerChangedItemsString.Contains( _modFilter ), + _ => false, // Should never happen + }; + } + + // Only get the text color for a mod if no filters are set. + private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) + { + if( _newMods.Contains( mod ) ) + { + return ColorId.NewMod.Value(); + } + + if( settings == null ) + { + return ColorId.UndefinedMod.Value(); + } + + if( !settings.Enabled ) + { + return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value(); + } + + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); + if( conflicts.Count == 0 ) + { + return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value(); + } + + return conflicts.Any( c => !c.Solved ) + ? ColorId.ConflictingMod.Value() + : ColorId.HandledConflictMod.Value(); + } + + private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) + { + var isNew = _newMods.Contains( mod ); + // Handle mod details. + if( CheckFlags( mod.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) + || CheckFlags( mod.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) + || CheckFlags( mod.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) + || CheckFlags( mod.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) + || CheckFlags( isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew ) ) + { + return true; + } + + // Handle Inheritance + if( collection == Penumbra.CollectionManager.Current ) + { + if( !_stateFilter.HasFlag( ModFilter.Uninherited ) ) + { + return true; + } + } + else + { + state.Color = ColorId.InheritedMod.Value(); + if( !_stateFilter.HasFlag( ModFilter.Inherited ) ) + { + return true; + } + } + + // Handle settings. + if( settings == null ) + { + state.Color = ColorId.UndefinedMod.Value(); + if( !_stateFilter.HasFlag( ModFilter.Undefined ) + || !_stateFilter.HasFlag( ModFilter.Disabled ) + || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return true; + } + } + else if( !settings.Enabled ) + { + state.Color = collection == Penumbra.CollectionManager.Current ? ColorId.DisabledMod.Value() : ColorId.InheritedDisabledMod.Value(); + if( !_stateFilter.HasFlag( ModFilter.Disabled ) + || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return true; + } + } + else + { + if( !_stateFilter.HasFlag( ModFilter.Enabled ) ) + { + return true; + } + + // Conflicts can only be relevant if the mod is enabled. + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); + if( conflicts.Count > 0 ) + { + if( conflicts.Any( c => !c.Solved ) ) + { + if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) + { + return true; + } + + state.Color = ColorId.ConflictingMod.Value(); + } + else + { + if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) ) + { + return true; + } + + state.Color = ColorId.HandledConflictMod.Value(); + } + } + else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return true; + } + } + + // isNew color takes precedence before other colors. + if( isNew ) + { + state.Color = ColorId.NewMod.Value(); + } + + return false; + } + + // Combined wrapper for handling all filters and setting state. + private bool ApplyFiltersAndState( ModFileSystemA.Leaf leaf, out ModState state ) + { + state = new ModState { Color = ColorId.EnabledMod.Value() }; + var mod = leaf.Value; + var (settings, collection) = Penumbra.CollectionManager.Current[ mod.Index ]; + + if( ApplyStringFilters( leaf, mod ) ) + { + return true; + } + + if( _stateFilter != ModFilterExtensions.UnfilteredStateMods ) + { + return CheckStateFilters( mod, settings, collection, ref state ); + } + + state.Color = GetTextColor( mod, settings, collection ); + return false; + } + + // Add the state filter combo-button to the right of the filter box. + protected override float CustomFilters( float width ) + { + var pos = ImGui.GetCursorPos(); + var remainingWidth = width - ImGui.GetFrameHeight(); + var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); + ImGui.SetCursorPos( comboPos ); + using var combo = ImRaii.Combo( "##filterCombo", string.Empty, + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); + + if( combo ) + { + var flags = ( int )_stateFilter; + foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) + { + if( ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ) ) + { + _stateFilter = ( ModFilter )flags; + SetFilterDirty(); + } + } + } + + combo.Dispose(); + ImGuiUtil.HoverTooltip( "Filter mods for their activation status." ); + ImGui.SetCursorPos( pos ); + return remainingWidth; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs new file mode 100644 index 00000000..a34b3bd6 --- /dev/null +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Mods; + +namespace Penumbra.UI.Classes; + +public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > +{ + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + + public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet newMods ) + : base( fileSystem ) + { + _newMods = newMods; + SubscribeRightClickFolder( EnableDescendants, 10 ); + SubscribeRightClickFolder( DisableDescendants, 10 ); + SubscribeRightClickFolder( InheritDescendants, 15 ); + SubscribeRightClickFolder( OwnDescendants, 15 ); + AddButton( AddNewModButton, 0 ); + AddButton( DeleteModButton, 1000 ); + SetFilterTooltip(); + + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); + } + + public override void Dispose() + { + base.Dispose(); + Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; + Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; + Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; + } + + // Customization points. + public override SortMode SortMode + => Penumbra.Config.SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; + + protected override uint ExpandedFolderColor + => ColorId.FolderExpanded.Value(); + + protected override uint CollapsedFolderColor + => ColorId.FolderCollapsed.Value(); + + protected override uint FolderLineColor + => ColorId.FolderLine.Value(); + + protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + { + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); + using var _ = ImRaii.TreeNode( leaf.Value.Meta.Name, flags ); + } + + + // Add custom context menu items. + private static void EnableDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Enable Descendants" ) ) + { + SetDescendants( folder, true ); + } + } + + private static void DisableDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Disable Descendants" ) ) + { + SetDescendants( folder, false ); + } + } + + private static void InheritDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Inherit Descendants" ) ) + { + SetDescendants( folder, true, true ); + } + } + + private static void OwnDescendants( ModFileSystemA.Folder folder ) + { + if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) + { + SetDescendants( folder, false, true ); + } + } + + + // Add custom buttons. + private static void AddNewModButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) ) + { } + } + + private void DeleteModButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, + "Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) ) + { } + } + + + // Helpers. + private static void SetDescendants( ModFileSystemA.Folder folder, bool enabled, bool inherit = false ) + { + var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystemA.Leaf >().Select( l => l.Value ); + if( inherit ) + { + Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); + } + else + { + Penumbra.CollectionManager.Current.SetMultipleModStates( mods, enabled ); + } + } + + // Automatic cache update functions. + private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ) + { + // TODO: maybe make more efficient + SetFilterDirty(); + if( modIdx == Selected?.Index ) + { + OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + } + } + + private void OnInheritanceChange( bool _ ) + { + SetFilterDirty(); + OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + } + + private void OnCollectionChange( ModCollection.Type type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + { + if( type != ModCollection.Type.Current || oldCollection == newCollection ) + { + return; + } + + if( oldCollection != null ) + { + oldCollection.ModSettingChanged -= OnSettingChange; + oldCollection.InheritanceChanged -= OnInheritanceChange; + } + + if( newCollection != null ) + { + newCollection.ModSettingChanged += OnSettingChange; + newCollection.InheritanceChanged += OnInheritanceChange; + } + + SetFilterDirty(); + OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + } + + private void OnSelectionChange( ModFileSystemA.Leaf? _1, ModFileSystemA.Leaf? newSelection, in ModState _2 ) + { + if( newSelection == null ) + { + SelectedSettings = ModSettings.Empty; + SelectedSettingCollection = ModCollection.Empty; + } + else + { + ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Value.Index ]; + SelectedSettings = settings ?? ModSettings.Empty; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs b/Penumbra/UI/Classes/ModFilter.cs similarity index 98% rename from Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs rename to Penumbra/UI/Classes/ModFilter.cs index a5a0b6d8..8812a203 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs +++ b/Penumbra/UI/Classes/ModFilter.cs @@ -1,6 +1,6 @@ using System; -namespace Penumbra.UI; +namespace Penumbra.UI.Classes; [Flags] public enum ModFilter diff --git a/Penumbra/UI/Colors.cs b/Penumbra/UI/Colors.cs deleted file mode 100644 index 013af594..00000000 --- a/Penumbra/UI/Colors.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Penumbra.UI; - -public static class Colors -{ - public const uint DefaultTextColor = 0xFFFFFFFFu; - public const uint NewModColor = 0xFF66DD66u; - public const uint DisabledModColor = 0xFF666666u; - public const uint ConflictingModColor = 0xFFAAAAFFu; - public const uint HandledConflictModColor = 0xFF88DDDDu; -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs b/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs deleted file mode 100644 index c80a0c47..00000000 --- a/Penumbra/UI/MenuTabs/ModFileSystemSelector.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Filesystem; -using OtterGui.FileSystem.Selector; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Mods; - -namespace Penumbra.UI; - -public sealed class ModFileSystemSelector : FileSystemSelector< Mod, ModState > -{ - private readonly IReadOnlySet< Mod > _newMods = new HashSet(); - private LowerString _modFilter = LowerString.Empty; - private LowerString _modFilterAuthor = LowerString.Empty; - private LowerString _modFilterChanges = LowerString.Empty; - private LowerString _modFilterName = LowerString.Empty; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - - public ModFilter StateFilter - { - get => _stateFilter; - set - { - var diff = _stateFilter != value; - _stateFilter = value; - if( diff ) - { - SetFilterDirty(); - } - } - } - - protected override bool ChangeFilter( string filterValue ) - { - if( filterValue.StartsWith( "c:", StringComparison.InvariantCultureIgnoreCase ) ) - { - _modFilterChanges = new LowerString( filterValue[ 2.. ] ); - _modFilter = LowerString.Empty; - _modFilterAuthor = LowerString.Empty; - _modFilterName = LowerString.Empty; - } - else if( filterValue.StartsWith( "a:", StringComparison.InvariantCultureIgnoreCase ) ) - { - _modFilterAuthor = new LowerString( filterValue[ 2.. ] ); - _modFilter = LowerString.Empty; - _modFilterChanges = LowerString.Empty; - _modFilterName = LowerString.Empty; - } - else if( filterValue.StartsWith( "n:", StringComparison.InvariantCultureIgnoreCase ) ) - { - _modFilterName = new LowerString( filterValue[ 2.. ] ); - _modFilter = LowerString.Empty; - _modFilterChanges = LowerString.Empty; - _modFilterAuthor = LowerString.Empty; - } - else - { - _modFilter = new LowerString( filterValue ); - _modFilterAuthor = LowerString.Empty; - _modFilterChanges = LowerString.Empty; - _modFilterName = LowerString.Empty; - } - - return true; - } - - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) - { - if( count == 0 ) - { - if( StateFilter.HasFlag( hasNoFlag ) ) - { - return false; - } - } - else if( StateFilter.HasFlag( hasFlag ) ) - { - return false; - } - - return true; - } - - private ModState GetModState( Mod mod, ModSettings? settings ) - { - if( settings?.Enabled != true ) - { - return new ModState { Color = ImGui.GetColorU32( ImGuiCol.TextDisabled ) }; - } - - return new ModState { Color = ImGui.GetColorU32( ImGuiCol.Text ) }; - } - - protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) - { - if( path is ModFileSystemA.Folder f ) - { - return base.ApplyFiltersAndState( f, out state ); - } - - return ApplyFiltersAndState( ( ModFileSystemA.Leaf )path, out state ); - } - - private bool CheckPath( string path, Mod mod ) - => _modFilter.IsEmpty - || path.Contains( _modFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) - || mod.Meta.Name.Contains( _modFilter ); - - private bool CheckName( Mod mod ) - => _modFilterName.IsEmpty || mod.Meta.Name.Contains( _modFilterName ); - - private bool CheckAuthor( Mod mod ) - => _modFilterAuthor.IsEmpty || mod.Meta.Author.Contains( _modFilterAuthor ); - - private bool CheckItems( Mod mod ) - => _modFilterChanges.IsEmpty || mod.LowerChangedItemsString.Contains( _modFilterChanges.Lower ); - - private bool ApplyFiltersAndState( ModFileSystemA.Leaf leaf, out ModState state ) - { - state = new ModState { Color = Colors.DefaultTextColor }; - var mod = leaf.Value; - var (settings, collection) = Current[ mod.Index ]; - // Check string filters. - if( !( CheckPath( leaf.FullName(), mod ) - && CheckName( mod ) - && CheckAuthor( mod ) - && CheckItems( mod ) ) ) - { - return true; - } - - var isNew = _newMods.Contains( mod ); - if( CheckFlags( mod.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) - || CheckFlags( mod.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - || CheckFlags( mod.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - || CheckFlags( mod.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) - || CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) - { - return true; - } - - if( settings == null ) - { - state.Color = Colors.DisabledModColor; - if( !StateFilter.HasFlag( ModFilter.Undefined ) ) - { - return true; - } - - settings = new ModSettings(); - } - - - if( !settings.Enabled ) - { - state.Color = Colors.DisabledModColor; - if( !StateFilter.HasFlag( ModFilter.Disabled ) ) - { - return true; - } - } - else - { - if( !StateFilter.HasFlag( ModFilter.Enabled ) ) - { - return true; - } - - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); - if( conflicts.Count > 0 ) - { - if( conflicts.Any( c => !c.Solved ) ) - { - if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return true; - } - - state.Color = Colors.ConflictingModColor; - } - else - { - if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return true; - } - - state.Color = Colors.HandledConflictModColor; - } - } - else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - - if( collection == Current ) - { - if( !StateFilter.HasFlag( ModFilter.Uninherited ) ) - { - return true; - } - } - else - { - if( !StateFilter.HasFlag( ModFilter.Inherited ) ) - { - return true; - } - } - - if( isNew ) - { - state.Color = Colors.NewModColor; - } - - return false; - } - - - protected override float CustomFilters( float width ) - { - var pos = ImGui.GetCursorPos(); - var remainingWidth = width - ImGui.GetFrameHeight(); - var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); - ImGui.SetCursorPos( comboPos ); - using var combo = ImRaii.Combo( "##filterCombo", string.Empty, - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); - - if( combo ) - { - ImGui.Text( "A" ); - ImGui.Text( "B" ); - ImGui.Text( "C" ); - } - - combo.Dispose(); - ImGui.SetCursorPos( pos ); - return remainingWidth; - } - - - public ModFileSystemSelector( ModFileSystemA fileSystem ) - : base( fileSystem ) - { - SubscribeRightClickFolder( EnableDescendants, 10 ); - SubscribeRightClickFolder( DisableDescendants, 10 ); - SubscribeRightClickFolder( InheritDescendants, 15 ); - SubscribeRightClickFolder( OwnDescendants, 15 ); - AddButton( AddNewModButton, 0 ); - AddButton( DeleteModButton, 1000 ); - } - - private static ModCollection Current - => Penumbra.CollectionManager.Current; - - private static void EnableDescendants( ModFileSystemA.Folder folder ) - { - if( ImGui.MenuItem( "Enable Descendants" ) ) - { - SetDescendants( folder, true, false ); - } - } - - private static void DisableDescendants( ModFileSystemA.Folder folder ) - { - if( ImGui.MenuItem( "Disable Descendants" ) ) - { - SetDescendants( folder, false, false ); - } - } - - private static void InheritDescendants( ModFileSystemA.Folder folder ) - { - if( ImGui.MenuItem( "Inherit Descendants" ) ) - { - SetDescendants( folder, true, true ); - } - } - - private static void OwnDescendants( ModFileSystemA.Folder folder ) - { - if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) - { - SetDescendants( folder, false, true ); - } - } - - private static void AddNewModButton( Vector2 size ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) ) - { } - } - - private void DeleteModButton( Vector2 size ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, - "Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) ) - { } - } - - private static void SetDescendants( ModFileSystemA.Folder folder, bool enabled, bool inherit = false ) - { - var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystemA.Leaf >().Select( l => l.Value ); - if( inherit ) - { - Current.SetMultipleModInheritances( mods, enabled ); - } - else - { - Current.SetMultipleModStates( mods, enabled ); - } - } - - public override SortMode SortMode - => Penumbra.Config.SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; - - protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) - { - var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); - using var _ = ImRaii.TreeNode( leaf.Value.Meta.Name, flags ); - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index 1b9b48f9..69956e52 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Logging; using OtterGui; using Penumbra.Mods; +using Penumbra.UI.Classes; using Penumbra.Util; namespace Penumbra.UI; @@ -278,7 +279,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? Colors.DisabledModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? ColorId.DisabledMod.Value() : ret.Item2; } if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) @@ -296,7 +297,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? Colors.ConflictingModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? ColorId.ConflictingMod.Value() : ret.Item2; } else { @@ -305,7 +306,7 @@ public class ModListCache : IDisposable return ret; } - ret.Item2 = ret.Item2 == 0 ? Colors.HandledConflictModColor : ret.Item2; + ret.Item2 = ret.Item2 == 0 ? ColorId.HandledConflictMod.Value() : ret.Item2; } } else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) @@ -316,7 +317,7 @@ public class ModListCache : IDisposable ret.Item1 = true; if( isNew ) { - ret.Item2 = Colors.NewModColor; + ret.Item2 = ColorId.NewMod.Value(); } SetFolderAndParentsVisible( mod.Data.Order.ParentFolder ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index cc773a28..9b6f7279 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -11,6 +11,7 @@ using ImGuiNET; using Penumbra.Collections; using Penumbra.Importer; using Penumbra.Mods; +using Penumbra.UI.Classes; using Penumbra.UI.Custom; using Penumbra.Util; @@ -217,18 +218,18 @@ public partial class SettingsInterface ImGui.Text( "Enabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.DisabledModColor ), "Disabled in the current collection." ); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), "Disabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.NewModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.NewMod.Value() ), "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.HandledConflictModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.HandledConflictMod.Value() ), "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); ImGui.Bullet(); ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( Colors.ConflictingModColor ), + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), "Enabled and conflicting with another enabled Mod on the same priority." ); ImGui.Unindent(); ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index fe36cebb..0589faf6 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -3,15 +3,13 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; -using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Logging; using ImGuiNET; +using OtterGui; using Penumbra.GameData.ByteString; -using Penumbra.Interop; +using Penumbra.UI.Classes; using Penumbra.UI.Custom; -using Penumbra.Util; namespace Penumbra.UI; @@ -360,6 +358,19 @@ public partial class SettingsInterface DrawAdvancedSettings(); } + if( ImGui.CollapsingHeader( "Colors" ) ) + { + foreach( var color in Enum.GetValues< ColorId >() ) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; + if( ImGuiUtil.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) + { + _configChanged = true; + } + } + } + if( _configChanged ) { _config.Save(); From 48e442a9fd289fae80a440032fd85d2b8c72ccfb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Apr 2022 16:29:20 +0200 Subject: [PATCH 0134/2451] Tmp for Mod2 --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 8 +- Penumbra/Mods/IModGroup.cs | 53 ----- Penumbra/Mods/ISubMod.cs | 14 -- .../{ => Manager}/Mod2.Manager.BasePath.cs | 0 .../{ => Manager}/Mod2.Manager.FileSystem.cs | 0 .../Mods/{ => Manager}/Mod2.Manager.Meta.cs | 0 .../{ => Manager}/Mod2.Manager.Options.cs | 50 +++-- .../Mods/{ => Manager}/Mod2.Manager.Root.cs | 0 Penumbra/Mods/{ => Manager}/Mod2.Manager.cs | 2 - Penumbra/Mods/Mod2.BasePath.cs | 32 +-- Penumbra/Mods/Mod2.ChangedItems.cs | 6 +- Penumbra/Mods/Mod2.Files.SubMod.cs | 31 --- Penumbra/Mods/Mod2.Files.cs | 93 +++++++-- Penumbra/Mods/Mod2.Meta.Migration.cs | 186 ++++++++++++++++-- Penumbra/Mods/Mod2.Meta.cs | 34 ++-- Penumbra/Mods/Mod2.SortOrder.cs | 8 - Penumbra/Mods/ModFileSystemA.cs | 43 +++- Penumbra/Mods/ModManager.cs | 49 ++++- Penumbra/Mods/ModManagerEditExtensions.cs | 4 +- Penumbra/Mods/Subclasses/IModGroup.cs | 79 ++++++++ Penumbra/Mods/Subclasses/ISubMod.cs | 53 +++++ .../Mod2.Files.MultiModGroup.cs | 34 +++- .../Mod2.Files.SingleModGroup.cs | 34 +++- Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs | 117 +++++++++++ Penumbra/Mods/{ => Subclasses}/ModSettings.cs | 0 Penumbra/Penumbra.cs | 8 +- Penumbra/Penumbra.csproj.DotSettings | 3 + Penumbra/UI/MenuTabs/TabBrowser.cs | 4 +- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 2 +- 30 files changed, 697 insertions(+), 252 deletions(-) delete mode 100644 Penumbra/Mods/IModGroup.cs delete mode 100644 Penumbra/Mods/ISubMod.cs rename Penumbra/Mods/{ => Manager}/Mod2.Manager.BasePath.cs (100%) rename Penumbra/Mods/{ => Manager}/Mod2.Manager.FileSystem.cs (100%) rename Penumbra/Mods/{ => Manager}/Mod2.Manager.Meta.cs (100%) rename Penumbra/Mods/{ => Manager}/Mod2.Manager.Options.cs (87%) rename Penumbra/Mods/{ => Manager}/Mod2.Manager.Root.cs (100%) rename Penumbra/Mods/{ => Manager}/Mod2.Manager.cs (94%) delete mode 100644 Penumbra/Mods/Mod2.Files.SubMod.cs delete mode 100644 Penumbra/Mods/Mod2.SortOrder.cs create mode 100644 Penumbra/Mods/Subclasses/IModGroup.cs create mode 100644 Penumbra/Mods/Subclasses/ISubMod.cs rename Penumbra/Mods/{ => Subclasses}/Mod2.Files.MultiModGroup.cs (52%) rename Penumbra/Mods/{ => Subclasses}/Mod2.Files.SingleModGroup.cs (50%) create mode 100644 Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs rename Penumbra/Mods/{ => Subclasses}/ModSettings.cs (100%) create mode 100644 Penumbra/Penumbra.csproj.DotSettings diff --git a/OtterGui b/OtterGui index 0c32ec43..5968fc8d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0c32ec432d38093a402e0dae6dc5c62a883b163b +Subproject commit 5968fc8dde7867ec9b7216deeed93d7b59a41ab8 diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 79d7d33c..752a5349 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -172,7 +172,7 @@ public partial class ModCollection // A changed mod forces changes for all collections, active and inactive. - private void OnModChanged( Mod.ChangeType type, int idx, Mod mod ) + private void OnModChanged( Mod.ChangeType type, Mod mod ) { switch( type ) { @@ -188,15 +188,15 @@ public partial class ModCollection var settings = new List< ModSettings? >( _collections.Count ); foreach( var collection in this ) { - settings.Add( collection[ idx ].Settings ); - collection.RemoveMod( mod, idx ); + settings.Add( collection[ mod.Index ].Settings ); + collection.RemoveMod( mod, mod.Index ); } OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); break; case Mod.ChangeType.Changed: foreach( var collection in this.Where( - collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + collection => collection.Settings[ mod.Index ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) { collection.Save(); } diff --git a/Penumbra/Mods/IModGroup.cs b/Penumbra/Mods/IModGroup.cs deleted file mode 100644 index 498d17d2..00000000 --- a/Penumbra/Mods/IModGroup.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Dalamud.Logging; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public interface IModGroup : IEnumerable< ISubMod > -{ - public string Name { get; } - public string Description { get; } - public SelectType Type { get; } - public int Priority { get; } - - public int OptionPriority( Index optionIdx ); - - public ISubMod this[ Index idx ] { get; } - - public int Count { get; } - - public bool IsOption - => Type switch - { - SelectType.Single => Count > 1, - SelectType.Multi => Count > 0, - _ => false, - }; - - public void Save( DirectoryInfo basePath ); - - public string FileName( DirectoryInfo basePath ) - => Path.Combine( basePath.FullName, Name.RemoveInvalidPathSymbols() + ".json" ); - - public void DeleteFile( DirectoryInfo basePath ) - { - var file = FileName( basePath ); - if( !File.Exists( file ) ) - { - return; - } - - try - { - File.Delete( file ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete file {file}:\n{e}" ); - throw; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ISubMod.cs b/Penumbra/Mods/ISubMod.cs deleted file mode 100644 index ee05e876..00000000 --- a/Penumbra/Mods/ISubMod.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public interface ISubMod -{ - public string Name { get; } - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } - public IReadOnlyList< MetaManipulation > Manipulations { get; } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.BasePath.cs rename to Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs diff --git a/Penumbra/Mods/Mod2.Manager.FileSystem.cs b/Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.FileSystem.cs rename to Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs diff --git a/Penumbra/Mods/Mod2.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod2.Manager.Meta.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.Meta.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Meta.cs diff --git a/Penumbra/Mods/Mod2.Manager.Options.cs b/Penumbra/Mods/Manager/Mod2.Manager.Options.cs similarity index 87% rename from Penumbra/Mods/Mod2.Manager.Options.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Options.cs index 3c26d6be..248745ee 100644 --- a/Penumbra/Mods/Mod2.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.Options.cs @@ -29,7 +29,7 @@ public sealed partial class Mod2 public void RenameModGroup( Mod2 mod, int groupIdx, string newName ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; var oldName = group.Name; if( oldName == newName || !VerifyFileName( mod, group, newName ) ) { @@ -53,25 +53,25 @@ public sealed partial class Mod2 return; } - var maxPriority = mod._options.Max( o => o.Priority ) + 1; + var maxPriority = mod._groups.Max( o => o.Priority ) + 1; - mod._options.Add( type == SelectType.Multi + mod._groups.Add( type == SelectType.Multi ? new MultiModGroup { Name = newName, Priority = maxPriority } : new SingleModGroup { Name = newName, Priority = maxPriority } ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._options.Count - 1, 0 ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, 0 ); } public void DeleteModGroup( Mod2 mod, int groupIdx ) { - var group = mod._options[ groupIdx ]; - mod._options.RemoveAt( groupIdx ); + var group = mod._groups[ groupIdx ]; + mod._groups.RemoveAt( groupIdx ); group.DeleteFile( BasePath ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 ); } public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; if( group.Description == newDescription ) { return; @@ -88,7 +88,7 @@ public sealed partial class Mod2 public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; if( group.Priority == newPriority ) { return; @@ -105,7 +105,7 @@ public sealed partial class Mod2 public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: ChangeGroupPriority( mod, groupIdx, newPriority ); @@ -124,7 +124,7 @@ public sealed partial class Mod2 public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: if( s.OptionData[ optionIdx ].Name == newName ) @@ -150,7 +150,7 @@ public sealed partial class Mod2 public void AddOption( Mod2 mod, int groupIdx, string newName ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: s.OptionData.Add( new SubMod { Name = newName } ); @@ -160,12 +160,12 @@ public sealed partial class Mod2 break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._options[ groupIdx ].Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1 ); } public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: s.OptionData.RemoveAt( optionIdx ); @@ -181,26 +181,24 @@ public sealed partial class Mod2 public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - var idx = subMod.ManipulationData.FindIndex( m => m.Equals( manip ) ); if( delete ) { - if( idx < 0 ) + if( !subMod.ManipulationData.Remove( manip ) ) { return; } - - subMod.ManipulationData.RemoveAt( idx ); } else { - if( idx >= 0 ) + if( subMod.ManipulationData.TryGetValue( manip, out var oldManip ) ) { - if( manip.EntryEquals( subMod.ManipulationData[ idx ] ) ) + if( manip.EntryEquals( oldManip ) ) { return; } - subMod.ManipulationData[ idx ] = manip; + subMod.ManipulationData.Remove( oldManip ); + subMod.ManipulationData.Add( manip ); } else { @@ -232,7 +230,7 @@ public sealed partial class Mod2 private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName ) { var path = newName.RemoveInvalidPathSymbols(); - if( mod.Options.Any( o => !ReferenceEquals( o, group ) + if( mod.Groups.Any( o => !ReferenceEquals( o, group ) && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) { PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); @@ -244,7 +242,7 @@ public sealed partial class Mod2 private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx ) { - return mod._options[ groupIdx ] switch + return mod._groups[ groupIdx ] switch { SingleModGroup s => s.OptionData[ optionIdx ], MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, @@ -285,15 +283,15 @@ public sealed partial class Mod2 // File deletion is handled in the actual function. if( type != ModOptionChangeType.GroupDeleted ) { - mod._options[groupIdx].Save( mod.BasePath ); + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath ); } // State can not change on adding groups, as they have no immediate options. mod.HasOptions = type switch { - ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), - ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._options[groupIdx].IsOption, - ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption, + ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), _ => mod.HasOptions, }; } diff --git a/Penumbra/Mods/Mod2.Manager.Root.cs b/Penumbra/Mods/Manager/Mod2.Manager.Root.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.Root.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Root.cs diff --git a/Penumbra/Mods/Mod2.Manager.cs b/Penumbra/Mods/Manager/Mod2.Manager.cs similarity index 94% rename from Penumbra/Mods/Mod2.Manager.cs rename to Penumbra/Mods/Manager/Mod2.Manager.cs index f385a7a0..2ba5fe27 100644 --- a/Penumbra/Mods/Mod2.Manager.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json.Linq; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs index 2fe9dfe3..5b086b31 100644 --- a/Penumbra/Mods/Mod2.BasePath.cs +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -15,18 +15,10 @@ public partial class Mod2 public DirectoryInfo BasePath { get; private set; } public int Index { get; private set; } = -1; - private FileInfo MetaFile - => new(Path.Combine( BasePath.FullName, "meta.json" )); + private Mod2( DirectoryInfo basePath ) + => BasePath = basePath; - private Mod2( ModFolder parentFolder, DirectoryInfo basePath ) - { - BasePath = basePath; - Order = new Mod.SortOrder( parentFolder, Name ); - //Order.ParentFolder.AddMod( this ); // TODO - ComputeChangedItems(); - } - - public static Mod2? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + public static Mod2? LoadMod( DirectoryInfo basePath ) { basePath.Refresh(); if( !basePath.Exists ) @@ -35,22 +27,18 @@ public partial class Mod2 return null; } - var mod = new Mod2( parentFolder, basePath ); - - var metaFile = mod.MetaFile; - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - mod.LoadMetaFromFile( metaFile ); + var mod = new Mod2( basePath ); + mod.LoadMeta(); if( mod.Name.Length == 0 ) { PluginLog.Error( $"Mod at {basePath} without name is not supported." ); } - mod.ReloadFiles(); + mod.LoadDefaultOption(); + mod.LoadAllGroups(); + mod.ComputeChangedItems(); + mod.SetHasOptions(); + return mod; } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.ChangedItems.cs b/Penumbra/Mods/Mod2.ChangedItems.cs index c6007321..bcd7fc4b 100644 --- a/Penumbra/Mods/Mod2.ChangedItems.cs +++ b/Penumbra/Mods/Mod2.ChangedItems.cs @@ -5,16 +5,16 @@ namespace Penumbra.Mods; public sealed partial class Mod2 { - public SortedList ChangedItems { get; } = new(); + public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; public void ComputeChangedItems() { var identifier = GameData.GameData.GetIdentifier(); ChangedItems.Clear(); - foreach( var (file, _) in AllFiles ) + foreach( var gamePath in AllRedirects ) { - identifier.Identify( ChangedItems, file.ToGamePath() ); + identifier.Identify( ChangedItems, gamePath.ToGamePath() ); } // TODO: manipulations diff --git a/Penumbra/Mods/Mod2.Files.SubMod.cs b/Penumbra/Mods/Mod2.Files.SubMod.cs deleted file mode 100644 index 245de4f8..00000000 --- a/Penumbra/Mods/Mod2.Files.SubMod.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public partial class Mod2 -{ - private sealed class SubMod : ISubMod - { - public string Name { get; set; } = "Default"; - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - - public readonly List< MetaManipulation > ManipulationData = new(); - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files - => FileData; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps - => FileSwapData; - - public IReadOnlyList< MetaManipulation > Manipulations - => ManipulationData; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs index 67cd3ad4..7ba7c63f 100644 --- a/Penumbra/Mods/Mod2.Files.cs +++ b/Penumbra/Mods/Mod2.Files.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -8,36 +11,88 @@ namespace Penumbra.Mods; public partial class Mod2 { - public IReadOnlyDictionary< Utf8GamePath, FullPath > RemainingFiles - => _remainingFiles; + public ISubMod Default + => _default; - public IReadOnlyList< IModGroup > Options - => _options; + public IReadOnlyList< IModGroup > Groups + => _groups; - public bool HasOptions { get; private set; } = false; + public bool HasOptions { get; private set; } private void SetHasOptions() { - HasOptions = _options.Any( o - => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); + HasOptions = _groups.Any( o + => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 + || o is SingleModGroup s && s.OptionData.Count > 1 ); } - private readonly Dictionary< Utf8GamePath, FullPath > _remainingFiles = new(); - private readonly List< IModGroup > _options = new(); - public IEnumerable< (Utf8GamePath, FullPath) > AllFiles - => _remainingFiles.Concat( _options.SelectMany( o => o ).SelectMany( o => o.Files.Concat( o.FileSwaps ) ) ) - .Select( kvp => ( kvp.Key, kvp.Value ) ); + private readonly SubMod _default = new(); + private readonly List< IModGroup > _groups = new(); + + public IEnumerable< ISubMod > AllSubMods + => _groups.SelectMany( o => o ).Prepend( _default ); public IEnumerable< MetaManipulation > AllManipulations - => _options.SelectMany( o => o ).SelectMany( o => o.Manipulations ); + => AllSubMods.SelectMany( s => s.Manipulations ); - private void ReloadFiles() + public IEnumerable< Utf8GamePath > AllRedirects + => AllSubMods.SelectMany( s => s.Files.Keys.Concat( s.FileSwaps.Keys ) ); + + public IEnumerable< FullPath > AllFiles + => AllSubMods.SelectMany( o => o.Files ) + .Select( p => p.Value ); + + public IEnumerable< FileInfo > GroupFiles + => BasePath.EnumerateFiles( "group_*.json" ); + + public List< FullPath > FindUnusedFiles() { - // _remainingFiles.Clear(); - // _options.Clear(); - // HasOptions = false; - // if( !Directory.Exists( BasePath.FullName ) ) - // return; + var modFiles = AllFiles.ToHashSet(); + return BasePath.EnumerateDirectories() + .SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + .Select( f => new FullPath( f ) ) + .Where( f => !modFiles.Contains( f ) ) + .ToList(); + } + + public List< FullPath > FindMissingFiles() + => AllFiles.Where( f => !f.Exists ).ToList(); + + public static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) + { + if( !File.Exists( file.FullName ) ) + { + return null; + } + + try + { + var json = JObject.Parse( File.ReadAllText( file.FullName ) ); + switch( json[ nameof( Type ) ]?.ToObject< SelectType >() ?? SelectType.Single ) + { + case SelectType.Multi: return MultiModGroup.Load( json, basePath ); + case SelectType.Single: return SingleModGroup.Load( json, basePath ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read mod group from {file.FullName}:\n{e}" ); + } + + return null; + } + + private void LoadAllGroups() + { + _groups.Clear(); + foreach( var file in GroupFiles ) + { + var group = LoadModGroup( file, BasePath ); + if( group != null ) + { + _groups.Add( group ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index a0975396..f7a5e63f 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -1,7 +1,12 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; +using Penumbra.Importer; using Penumbra.Util; namespace Penumbra.Mods; @@ -10,29 +15,184 @@ public sealed partial class Mod2 { private static class Migration { - public static void Migrate( Mod2 mod, string text ) - { - MigrateV0ToV1( mod, text ); - } + public static bool Migrate( Mod2 mod, JObject json ) + => MigrateV0ToV1( mod, json ); - private static void MigrateV0ToV1( Mod2 mod, string text ) + private static bool MigrateV0ToV1( Mod2 mod, JObject json ) { if( mod.FileVersion > 0 ) + { + return false; + } + + var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var priority = 1; + foreach( var group in groups.Values ) + { + ConvertGroup( mod, group, ref priority ); + } + + foreach( var unusedFile in mod.FindUnusedFiles() ) + { + if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) ) + { + mod._default.FileData.Add( gamePath, unusedFile ); + } + } + + mod._default.FileSwapData.Clear(); + mod._default.FileSwapData.EnsureCapacity( swaps.Count ); + foreach( var (gamePath, swapPath) in swaps ) + { + mod._default.FileSwapData.Add( gamePath, swapPath ); + } + + HandleMetaChanges( mod._default, mod.BasePath ); + foreach( var group in mod.Groups ) + { + IModGroup.SaveModGroup( group, mod.BasePath ); + } + + mod.SaveDefaultMod(); + + return true; + } + + private static void ConvertGroup( Mod2 mod, OptionGroupV0 group, ref int priority ) + { + if( group.Options.Count == 0 ) { return; } - var data = JObject.Parse( text ); - var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() - ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - foreach( var group in groups.Values ) - { } + switch( group.SelectionType ) + { + case SelectType.Multi: - foreach( var swap in swaps ) - { } + var optionPriority = 0; + var newMultiGroup = new MultiModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add( newMultiGroup ); + foreach( var option in group.Options ) + { + newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option ), optionPriority++ ) ); + } + + break; + case SelectType.Single: + if( group.Options.Count == 1 ) + { + AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ] ); + return; + } + + var newSingleGroup = new SingleModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add( newSingleGroup ); + foreach( var option in group.Options ) + { + newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option ) ); + } + + break; + } } + private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option ) + { + foreach( var (relPath, gamePaths) in option.OptionFiles ) + { + foreach( var gamePath in gamePaths ) + { + mod.FileData.TryAdd( gamePath, new FullPath( basePath, relPath ) ); + } + } + } + + private static void HandleMetaChanges( SubMod subMod, DirectoryInfo basePath ) + { + foreach( var (key, file) in subMod.Files.ToList() ) + { + try + { + switch( file.Extension ) + { + case ".meta": + subMod.FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + foreach( var manip in meta.EqpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EqdpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EstManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.GmpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.ImcManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + break; + case ".rgsp": + subMod.FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + foreach( var manip in rgsp.RspManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + break; + default: continue; + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not migrate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + continue; + } + } + } + + private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option ) + { + var subMod = new SubMod() { Name = option.OptionName }; + AddFilesToSubMod( subMod, basePath, option ); + HandleMetaChanges( subMod, basePath ); + return subMod; + } private struct OptionV0 { diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod2.Meta.cs index 8a440829..bcc05cf8 100644 --- a/Penumbra/Mods/Mod2.Meta.cs +++ b/Penumbra/Mods/Mod2.Meta.cs @@ -17,6 +17,7 @@ public enum MetaChangeType : byte Version = 0x08, Website = 0x10, Deletion = 0x20, + Migration = 0x40, } public sealed partial class Mod2 @@ -29,19 +30,21 @@ public sealed partial class Mod2 public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; - private void SaveMeta() - => SaveToFile( MetaFile ); + private FileInfo MetaFile + => new(Path.Combine( BasePath.FullName, "meta.json" )); - private MetaChangeType LoadMetaFromFile( FileInfo filePath ) + private MetaChangeType LoadMeta() { - if( !File.Exists( filePath.FullName ) ) + var metaFile = MetaFile; + if( !File.Exists( metaFile.FullName ) ) { + PluginLog.Debug( "No mod meta found for {ModLocation}.", BasePath.Name ); return MetaChangeType.Deletion; } try { - var text = File.ReadAllText( filePath.FullName ); + var text = File.ReadAllText( metaFile.FullName ); var json = JObject.Parse( text ); var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty; @@ -52,12 +55,6 @@ public sealed partial class Mod2 var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; MetaChangeType changes = 0; - if( newFileVersion < CurrentFileVersion ) - { - Migration.Migrate( this, text ); - FileVersion = newFileVersion; - } - if( Name != newName ) { changes |= MetaChangeType.Name; @@ -88,6 +85,14 @@ public sealed partial class Mod2 Website = newWebsite; } + if( FileVersion != newFileVersion ) + { + FileVersion = newFileVersion; + if( Migration.Migrate( this, json ) ) + { + changes |= MetaChangeType.Migration; + } + } return changes; } @@ -98,8 +103,9 @@ public sealed partial class Mod2 } } - private void SaveToFile( FileInfo filePath ) + private void SaveMeta() { + var metaFile = MetaFile; try { var jObject = new JObject @@ -111,11 +117,11 @@ public sealed partial class Mod2 { nameof( Version ), JToken.FromObject( Version ) }, { nameof( Website ), JToken.FromObject( Website ) }, }; - File.WriteAllText( filePath.FullName, jObject.ToString( Formatting.Indented ) ); + File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); } catch( Exception e ) { - PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.SortOrder.cs b/Penumbra/Mods/Mod2.SortOrder.cs deleted file mode 100644 index e3f1cc37..00000000 --- a/Penumbra/Mods/Mod2.SortOrder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Penumbra.Mods; - -public sealed partial class Mod2 -{ - public Mod.SortOrder Order; - public override string ToString() - => Order.FullPath; -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystemA.cs index 1bece099..c684d371 100644 --- a/Penumbra/Mods/ModFileSystemA.cs +++ b/Penumbra/Mods/ModFileSystemA.cs @@ -1,26 +1,52 @@ +using System; using System.IO; using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystemA : FileSystem< Mod > +public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable { + // Save the current sort order. + // Does not save or copy the backup in the current mod directory, + // as this is done on mod directory changes only. public void Save() => SaveToFile( new FileInfo( Mod.Manager.SortOrderFile ), SaveMod, true ); + // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystemA Load() { - var x = new ModFileSystemA(); - if( x.Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) - { - x.Save(); - } + var ret = new ModFileSystemA(); + ret.Reload(); - x.Changed += ( _1, _2, _3, _4 ) => x.Save(); + ret.Changed += ret.OnChange; + Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - return x; + return ret; } + public void Dispose() + => Penumbra.ModManager.ModDiscoveryFinished -= Reload; + + // Reload the whole filesystem from currently loaded mods and the current sort order file. + // Used on construction and on mod rediscoveries. + private void Reload() + { + if( Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + { + Save(); + } + } + + // Save the filesystem on every filesystem change except full reloading. + private void OnChange( FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3 ) + { + if( type != FileSystemChangeType.Reload ) + { + Save(); + } + } + + // Used for saving and loading. private static string ModToIdentifier( Mod mod ) => mod.BasePath.Name; @@ -29,6 +55,7 @@ public sealed class ModFileSystemA : FileSystem< Mod > private static (string, bool) SaveMod( Mod mod, string fullPath ) { + // Only save pairs with non-default paths. if( fullPath == ModToName( mod ) ) { return ( string.Empty, false ); diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index fc126dd4..1f2f5cb7 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -42,7 +42,7 @@ public partial class Mod public ModFolder StructuredMods { get; } = ModFileSystem.Root; - public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod ); + public delegate void ModChangeDelegate( ChangeType type, Mod mod ); public event ModChangeDelegate? ModChange; public event Action? ModDiscoveryStarted; @@ -90,8 +90,14 @@ public partial class Mod } } + if( !firstTime ) + { + HandleSortOrderFiles( newDir ); + } + BasePath = newDir; - Valid = true; + + Valid = true; if( Config.ModDirectory != BasePath.FullName ) { Config.ModDirectory = BasePath.FullName; @@ -100,8 +106,37 @@ public partial class Mod } } - public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - "sort_order.json" ); + private const string SortOrderFileName = "sort_order.json"; + public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), SortOrderFileName ); + + private void HandleSortOrderFiles( DirectoryInfo newDir ) + { + try + { + var mainFile = SortOrderFile; + // Copy old sort order to backup. + var oldSortOrderFile = Path.Combine( BasePath.FullName, SortOrderFileName ); + PluginLog.Debug( "Copying current sort older file to {BackupFile}...", oldSortOrderFile ); + File.Copy( mainFile, oldSortOrderFile, true ); + BasePath = newDir; + var newSortOrderFile = Path.Combine( newDir.FullName, SortOrderFileName ); + // Copy new sort order to main, if it exists. + if( File.Exists( newSortOrderFile ) ) + { + File.Copy( newSortOrderFile, mainFile, true ); + PluginLog.Debug( "Copying stored sort order file from {BackupFile}...", newSortOrderFile ); + } + else + { + File.Delete( mainFile ); + PluginLog.Debug( "Deleting current sort order file...", newSortOrderFile ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not swap Sort Order files:\n{e}" ); + } + } public Manager() { @@ -215,7 +250,7 @@ public partial class Mod --_mods[ i ].Index; } - ModChange?.Invoke( ChangeType.Removed, idx, mod ); + ModChange?.Invoke( ChangeType.Removed, mod ); } } @@ -241,7 +276,7 @@ public partial class Mod } _mods.Add( mod ); - ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); + ModChange?.Invoke( ChangeType.Added, mod ); return _mods.Count - 1; } @@ -287,7 +322,7 @@ public partial class Mod } // TODO: more specific mod changes? - ModChange?.Invoke( ChangeType.Changed, idx, mod ); + ModChange?.Invoke( ChangeType.Changed, mod ); return true; } diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index baf8ffe6..82869ad9 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -43,7 +43,7 @@ public static class ModManagerEditExtensions manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; } - manager.Config.Save(); + Penumbra.Config.Save(); return true; } @@ -79,7 +79,7 @@ public static class ModManagerEditExtensions { manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ]; manager.TemporaryModSortOrder.Remove( oldBasePath.Name ); - manager.Config.Save(); + Penumbra.Config.Save(); } var idx = manager.Mods.IndexOf( mod ); diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs new file mode 100644 index 00000000..ac643ecb --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public interface IModGroup : IEnumerable< ISubMod > +{ + public string Name { get; } + public string Description { get; } + public SelectType Type { get; } + public int Priority { get; } + + public int OptionPriority( Index optionIdx ); + + public ISubMod this[ Index idx ] { get; } + + public int Count { get; } + + public bool IsOption + => Type switch + { + SelectType.Single => Count > 1, + SelectType.Multi => Count > 0, + _ => false, + }; + + public string FileName( DirectoryInfo basePath ) + => Path.Combine( basePath.FullName, $"group_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); + + public void DeleteFile( DirectoryInfo basePath ) + { + var file = FileName( basePath ); + if( !File.Exists( file ) ) + { + return; + } + + try + { + File.Delete( file ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete file {file}:\n{e}" ); + throw; + } + } + + public static void SaveModGroup( IModGroup group, DirectoryInfo basePath ) + { + var file = group.FileName( basePath ); + using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); + using var writer = new StreamWriter( s ); + using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); + j.WritePropertyName( nameof( group.Name ) ); + j.WriteValue( group.Name ); + j.WritePropertyName( nameof( group.Description ) ); + j.WriteValue( group.Description ); + j.WritePropertyName( nameof( group.Priority ) ); + j.WriteValue( group.Priority ); + j.WritePropertyName( nameof( Type ) ); + j.WriteValue( group.Type.ToString() ); + j.WritePropertyName( "Options" ); + j.WriteStartArray(); + for( var idx = 0; idx < group.Count; ++idx ) + { + ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == SelectType.Multi ? group.OptionPriority( idx ) : null ); + } + + j.WriteEndArray(); + j.WriteEndObject(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs new file mode 100644 index 00000000..f6781566 --- /dev/null +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public interface ISubMod +{ + public string Name { get; } + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } + public IReadOnlySet< MetaManipulation > Manipulations { get; } + + public static void WriteSubMod( JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority ) + { + j.WriteStartObject(); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( mod.Name ); + if( priority != null ) + { + j.WritePropertyName( nameof( IModGroup.Priority ) ); + j.WriteValue( priority.Value ); + } + + j.WritePropertyName( nameof( mod.Files ) ); + j.WriteStartObject(); + foreach( var (gamePath, file) in mod.Files ) + { + if( file.ToRelPath( basePath, out var relPath ) ) + { + j.WritePropertyName( gamePath.ToString() ); + j.WriteValue( relPath.ToString() ); + } + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( mod.FileSwaps ) ); + j.WriteStartObject(); + foreach( var (gamePath, file) in mod.FileSwaps ) + { + j.WritePropertyName( gamePath.ToString() ); + j.WriteValue( file.ToString() ); + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( mod.Manipulations ) ); + serializer.Serialize( j, mod.Manipulations ); + j.WriteEndObject(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs similarity index 52% rename from Penumbra/Mods/Mod2.Files.MultiModGroup.cs rename to Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs index 4ce3fd48..cd05a844 100644 --- a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs @@ -3,8 +3,8 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Penumbra.Mods; @@ -17,7 +17,7 @@ public partial class Mod2 public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } = 0; + public int Priority { get; set; } public int OptionPriority( Index idx ) => PrioritizedOptions[ idx ].Priority; @@ -25,6 +25,7 @@ public partial class Mod2 public ISubMod this[ Index idx ] => PrioritizedOptions[ idx ].Mod; + [JsonIgnore] public int Count => PrioritizedOptions.Count; @@ -36,18 +37,31 @@ public partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public void Save( DirectoryInfo basePath ) + public static MultiModGroup? Load( JObject json, DirectoryInfo basePath ) { - var path = ( ( IModGroup )this ).FileName( basePath ); - try + var options = json[ "Options" ]; + var ret = new MultiModGroup() { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( path, text ); - } - catch( Exception e ) + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + }; + if( ret.Name.Length == 0 ) { - PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + return null; } + + if( options != null ) + { + foreach( var child in options.Children() ) + { + var subMod = new SubMod(); + subMod.Load( basePath, child, out var priority ); + ret.PrioritizedOptions.Add( ( subMod, priority ) ); + } + } + + return ret; } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs similarity index 50% rename from Penumbra/Mods/Mod2.Files.SingleModGroup.cs rename to Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs index 92212ad7..40ebbcce 100644 --- a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs @@ -2,8 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; -using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Penumbra.Mods; @@ -16,7 +16,7 @@ public partial class Mod2 public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } = 0; + public int Priority { get; set; } public readonly List< SubMod > OptionData = new(); @@ -26,6 +26,7 @@ public partial class Mod2 public ISubMod this[ Index idx ] => OptionData[ idx ]; + [JsonIgnore] public int Count => OptionData.Count; @@ -35,18 +36,31 @@ public partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public void Save( DirectoryInfo basePath ) + public static SingleModGroup? Load( JObject json, DirectoryInfo basePath ) { - var path = ( ( IModGroup )this ).FileName( basePath ); - try + var options = json[ "Options" ]; + var ret = new SingleModGroup { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( path, text ); - } - catch( Exception e ) + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + }; + if( ret.Name.Length == 0 ) { - PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + return null; } + + if( options != null ) + { + foreach( var child in options.Children() ) + { + var subMod = new SubMod(); + subMod.Load( basePath, child, out _ ); + ret.OptionData.Add( subMod ); + } + } + + return ret; } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs new file mode 100644 index 00000000..766c59da --- /dev/null +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private string DefaultFile + => Path.Combine( BasePath.FullName, "default_mod.json" ); + + private void SaveDefaultMod() + { + var defaultFile = DefaultFile; + + using var stream = File.Exists( defaultFile ) + ? File.Open( defaultFile, FileMode.Truncate ) + : File.Open( defaultFile, FileMode.CreateNew ); + + using var w = new StreamWriter( stream ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer + { + Formatting = Formatting.Indented, + }; + ISubMod.WriteSubMod( j, serializer, _default, BasePath, 0 ); + } + + private void LoadDefaultOption() + { + var defaultFile = DefaultFile; + try + { + if( !File.Exists( defaultFile ) ) + { + _default.Load( BasePath, new JObject(), out _ ); + } + else + { + _default.Load( BasePath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse default file for {Name}:\n{e}" ); + } + } + + + private sealed class SubMod : ISubMod + { + public string Name { get; set; } = "Default"; + + public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); + public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public readonly HashSet< MetaManipulation > ManipulationData = new(); + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlySet< MetaManipulation > Manipulations + => ManipulationData; + + public void Load( DirectoryInfo basePath, JToken json, out int priority ) + { + FileData.Clear(); + FileSwapData.Clear(); + ManipulationData.Clear(); + + Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; + priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; + + var files = ( JObject? )json[ nameof( Files ) ]; + if( files != null ) + { + foreach( var property in files.Properties() ) + { + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) + { + FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); + } + } + } + + var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; + if( swaps != null ) + { + foreach( var property in swaps.Properties() ) + { + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) + { + FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); + } + } + } + + var manips = json[ nameof( Manipulations ) ]; + if( manips != null ) + { + foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ) ) + { + ManipulationData.Add( s ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs similarity index 100% rename from Penumbra/Mods/ModSettings.cs rename to Penumbra/Mods/Subclasses/ModSettings.cs diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ac6dcfa7..37ae5260 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -122,9 +122,11 @@ public class Penumbra : IDalamudPlugin } ResidentResources.Reload(); - //var c = ModCollection2.LoadFromFile( new FileInfo(@"C:\Users\Ozy\AppData\Roaming\XIVLauncher\pluginConfigs\Penumbra\collections\Rayla.json"), - // out var inheritance ); - //c?.Save(); + + foreach( var folder in ModManager.BasePath.EnumerateDirectories() ) + { + var m = Mod2.LoadMod( folder ); + } } public bool Enable() diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings new file mode 100644 index 00000000..b43e7ec2 --- /dev/null +++ b/Penumbra/Penumbra.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabBrowser.cs b/Penumbra/UI/MenuTabs/TabBrowser.cs index 6e00a9af..1dc57a16 100644 --- a/Penumbra/UI/MenuTabs/TabBrowser.cs +++ b/Penumbra/UI/MenuTabs/TabBrowser.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Runtime.InteropServices; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -20,7 +22,7 @@ public partial class SettingsInterface public TabBrowser() { _fileSystem = ModFileSystemA.Load(); - _selector = new ModFileSystemSelector( _fileSystem ); + _selector = new ModFileSystemSelector( _fileSystem, new HashSet() ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index 69956e52..be4bf261 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -148,7 +148,7 @@ public class ModListCache : IDisposable PluginLog.Debug( "Resetting mod selector list..." ); if( _modsInOrder.Count == 0 ) { - foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) + foreach( var modData in _manager.StructuredMods.AllMods( Penumbra.Config.SortFoldersFirst ) ) { var idx = Penumbra.ModManager.Mods.IndexOf( modData ); var mod = new FullMod( Penumbra.CollectionManager.Current[ idx ].Settings ?? ModSettings.DefaultSettings( modData.Meta ), From da73feacf450d003503ce2be397ae82d345dac7c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Apr 2022 15:23:58 +0200 Subject: [PATCH 0135/2451] tmp --- OtterGui | 2 +- Penumbra/Api/ModsController.cs | 20 +- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Api/SimpleRedirectManager.cs | 124 ++ .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionManager.cs | 54 +- .../Collections/ModCollection.Cache.Access.cs | 168 +++ Penumbra/Collections/ModCollection.Cache.cs | 581 +++------ Penumbra/Collections/ModCollection.Changes.cs | 37 +- Penumbra/Collections/ModCollection.File.cs | 6 +- .../Collections/ModCollection.Inheritance.cs | 6 +- .../Collections/ModCollection.Migration.cs | 7 +- Penumbra/Collections/ModCollection.cs | 50 +- Penumbra/Importer/TexToolsImport.cs | 101 +- .../Loader/ResourceLoader.Replacement.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 7 +- Penumbra/Meta/MetaCollection.cs | 239 ---- Penumbra/MigrateConfiguration.cs | 17 +- Penumbra/Mods/FullMod.cs | 31 - Penumbra/Mods/GroupInformation.cs | 102 -- Penumbra/Mods/Manager/Mod2.Manager.cs | 1 - Penumbra/Mods/Mod.SortOrder.cs | 43 - Penumbra/Mods/Mod.cs | 95 -- Penumbra/Mods/Mod2.BasePath.cs | 2 +- Penumbra/Mods/Mod2.Creation.cs | 108 ++ Penumbra/Mods/Mod2.Files.cs | 29 +- Penumbra/Mods/Mod2.Meta.Migration.cs | 71 +- Penumbra/Mods/ModCleanup.cs | 1034 ++++++++--------- Penumbra/Mods/ModFileSystem.cs | 260 ----- Penumbra/Mods/ModFileSystemA.cs | 14 +- Penumbra/Mods/ModFolder.cs | 245 ---- Penumbra/Mods/ModFunctions.cs | 100 -- Penumbra/Mods/ModManager.cs | 335 ------ Penumbra/Mods/ModManagerEditExtensions.cs | 211 ---- Penumbra/Mods/ModMeta.cs | 184 --- Penumbra/Mods/ModResources.cs | 89 -- Penumbra/Mods/NamedModSettings.cs | 46 - Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs | 78 ++ Penumbra/Mods/Subclasses/ModSettings.cs | 156 ++- Penumbra/Mods/Subclasses/SelectType.cs | 7 + Penumbra/Penumbra.cs | 60 +- Penumbra/Penumbra.csproj | 6 + Penumbra/UI/Classes/Colors.cs | 2 + .../Classes/ModFileSystemSelector.Filters.cs | 32 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 43 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 22 + Penumbra/UI/ConfigWindow.DebugTab.cs | 35 + Penumbra/UI/ConfigWindow.EffectiveTab.cs | 27 + .../UI/{UiHelpers.cs => ConfigWindow.Misc.cs} | 8 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 22 + Penumbra/UI/ConfigWindow.ResourceTab.cs | 27 + Penumbra/UI/ConfigWindow.SettingsTab.cs | 317 +++++ Penumbra/UI/ConfigWindow.cs | 68 ++ Penumbra/UI/LaunchButton.cs | 43 +- Penumbra/UI/SettingsInterface.cs | 60 - Penumbra/UI/SettingsMenu.cs | 93 -- Penumbra/Util/ModelChanger.cs | 4 +- 59 files changed, 2115 insertions(+), 3428 deletions(-) create mode 100644 Penumbra/Api/SimpleRedirectManager.cs create mode 100644 Penumbra/Collections/ModCollection.Cache.Access.cs delete mode 100644 Penumbra/Meta/MetaCollection.cs delete mode 100644 Penumbra/Mods/FullMod.cs delete mode 100644 Penumbra/Mods/GroupInformation.cs delete mode 100644 Penumbra/Mods/Mod.SortOrder.cs delete mode 100644 Penumbra/Mods/Mod.cs create mode 100644 Penumbra/Mods/Mod2.Creation.cs delete mode 100644 Penumbra/Mods/ModFileSystem.cs delete mode 100644 Penumbra/Mods/ModFolder.cs delete mode 100644 Penumbra/Mods/ModFunctions.cs delete mode 100644 Penumbra/Mods/ModManager.cs delete mode 100644 Penumbra/Mods/ModManagerEditExtensions.cs delete mode 100644 Penumbra/Mods/ModMeta.cs delete mode 100644 Penumbra/Mods/ModResources.cs delete mode 100644 Penumbra/Mods/NamedModSettings.cs create mode 100644 Penumbra/Mods/Subclasses/SelectType.cs create mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.cs create mode 100644 Penumbra/UI/ConfigWindow.DebugTab.cs create mode 100644 Penumbra/UI/ConfigWindow.EffectiveTab.cs rename Penumbra/UI/{UiHelpers.cs => ConfigWindow.Misc.cs} (86%) create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.cs create mode 100644 Penumbra/UI/ConfigWindow.ResourceTab.cs create mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.cs create mode 100644 Penumbra/UI/ConfigWindow.cs delete mode 100644 Penumbra/UI/SettingsInterface.cs delete mode 100644 Penumbra/UI/SettingsMenu.cs diff --git a/OtterGui b/OtterGui index 5968fc8d..05619f96 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5968fc8dde7867ec9b7216deeed93d7b59a41ab8 +Subproject commit 05619f966e6acfe8b0b6e947243c5d930c7525a4 diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 03cf6885..bde6d2db 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,15 +16,17 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new - { - x.Second?.Enabled, - x.Second?.Priority, - x.First.BasePath.Name, - x.First.Meta, - BasePath = x.First.BasePath.FullName, - Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), - } ); + // TODO + return null; + //return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new + //{ + // x.Second?.Enabled, + // x.Second?.Priority, + // x.First.BasePath.Name, + // x.First.Name, + // BasePath = x.First.BasePath.FullName, + // Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), + //} ); } [Route( HttpVerbs.Post, "/mods" )] diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index cac23ecb..8610ac17 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -76,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mods.Mod2.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs new file mode 100644 index 00000000..1c633a65 --- /dev/null +++ b/Penumbra/Api/SimpleRedirectManager.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Mods; + +namespace Penumbra.Api; + +public enum RedirectResult +{ + Registered = 0, + Success = 0, + IdenticalFileRegistered = 1, + InvalidGamePath = 2, + OtherOwner = 3, + NotRegistered = 4, + NoPermission = 5, + FilteredGamePath = 6, + UnknownError = 7, +} + +public class SimpleRedirectManager +{ + internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); + public readonly HashSet< string > AllowedTags = new(); + + public void Apply( IDictionary< Utf8GamePath, FullPath > dict ) + { + foreach( var (gamePath, (file, _)) in Replacements ) + { + dict.TryAdd( gamePath, file ); + } + } + + private RedirectResult? CheckPermission( string tag ) + => AllowedTags.Contains( tag ) ? null : RedirectResult.NoPermission; + + public RedirectResult IsRegistered( Utf8GamePath path, string tag ) + => CheckPermission( tag ) + ?? ( Replacements.TryGetValue( path, out var pair ) + ? pair.Tag == tag ? RedirectResult.Registered : RedirectResult.OtherOwner + : RedirectResult.NotRegistered ); + + public RedirectResult Register( Utf8GamePath path, FullPath file, string tag ) + { + if( CheckPermission( tag ) != null ) + { + return RedirectResult.NoPermission; + } + + if( Mod2.FilterFile( path ) ) + { + return RedirectResult.FilteredGamePath; + } + + try + { + if( Replacements.TryGetValue( path, out var pair ) ) + { + if( file.Equals( pair.File ) ) + { + return RedirectResult.IdenticalFileRegistered; + } + + if( tag != pair.Tag ) + { + return RedirectResult.OtherOwner; + } + } + + Replacements[ path ] = ( file, tag ); + return RedirectResult.Success; + } + catch( Exception e ) + { + PluginLog.Error( $"[{tag}] Unknown Error registering simple redirect {path} -> {file}:\n{e}" ); + return RedirectResult.UnknownError; + } + } + + public RedirectResult Unregister( Utf8GamePath path, string tag ) + { + if( CheckPermission( tag ) != null ) + { + return RedirectResult.NoPermission; + } + + try + { + if( !Replacements.TryGetValue( path, out var pair ) ) + { + return RedirectResult.NotRegistered; + } + + if( tag != pair.Tag ) + { + return RedirectResult.OtherOwner; + } + + Replacements.Remove( path ); + return RedirectResult.Success; + } + catch( Exception e ) + { + PluginLog.Error( $"[{tag}] Unknown Error unregistering simple redirect {path}:\n{e}" ); + return RedirectResult.UnknownError; + } + } + + public RedirectResult Register( string path, string file, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? Register( gamePath, new FullPath( file ), tag ) + : RedirectResult.InvalidGamePath; + + public RedirectResult Unregister( string path, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? Unregister( gamePath, tag ) + : RedirectResult.InvalidGamePath; + + public RedirectResult IsRegistered( string path, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? IsRegistered( gamePath, tag ) + : RedirectResult.InvalidGamePath; +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index fe396ccd..8674d5a3 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -275,7 +275,7 @@ public partial class ModCollection } } - private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + private void OnModRemovedActive( bool meta, IEnumerable< ModSettings2? > settings ) { foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 752a5349..3a7335cc 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -27,7 +27,7 @@ public partial class ModCollection public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, string? characterName = null ); - private readonly Mod.Manager _modManager; + private readonly Mod2.Manager _modManager; // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -56,14 +56,15 @@ public partial class ModCollection IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mod.Manager manager ) + public Manager( Mod2.Manager manager ) { _modManager = manager; // The collection manager reacts to changes in mods by itself. _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModChange += OnModChanged; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChanged; CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); @@ -73,7 +74,8 @@ public partial class ModCollection { _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModChange -= OnModChanged; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChanged; } // Add a new collection of the given name. @@ -171,42 +173,64 @@ public partial class ModCollection } - // A changed mod forces changes for all collections, active and inactive. - private void OnModChanged( Mod.ChangeType type, Mod mod ) + // A changed mod path forces changes for all collections, active and inactive. + private void OnModPathChanged( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ) { switch( type ) { - case Mod.ChangeType.Added: + case ModPathChangeType.Added: foreach( var collection in this ) { collection.AddMod( mod ); } - OnModAddedActive( mod.Resources.MetaManipulations.Count > 0 ); + OnModAddedActive( mod.TotalManipulations > 0 ); break; - case Mod.ChangeType.Removed: - var settings = new List< ModSettings? >( _collections.Count ); + case ModPathChangeType.Deleted: + var settings = new List< ModSettings2? >( _collections.Count ); foreach( var collection in this ) { settings.Add( collection[ mod.Index ].Settings ); collection.RemoveMod( mod, mod.Index ); } - OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); + OnModRemovedActive( mod.TotalManipulations > 0, settings ); break; - case Mod.ChangeType.Changed: - foreach( var collection in this.Where( - collection => collection.Settings[ mod.Index ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + case ModPathChangeType.Moved: + foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) { collection.Save(); } - OnModChangedActive( mod.Resources.MetaManipulations.Count > 0, mod.Index ); + OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } } + + private void OnModOptionsChanged( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + { + if( type == ModOptionChangeType.DisplayChange ) + { + return; + } + + // TODO + switch( type ) + { + case ModOptionChangeType.GroupRenamed: + case ModOptionChangeType.GroupAdded: + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.PriorityChanged: + case ModOptionChangeType.OptionAdded: + case ModOptionChangeType.OptionDeleted: + case ModOptionChangeType.OptionChanged: + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + // Add the collection with the default name if it does not exist. // It should always be ensured that it exists, otherwise it will be created. // This can also not be deleted, so there are always at least the empty and a collection with default name. diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs new file mode 100644 index 00000000..8e58762a --- /dev/null +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manager; + +namespace Penumbra.Collections; + +public partial class ModCollection +{ + // Only active collections need to have a cache. + private Cache? _cache; + + public bool HasCache + => _cache != null; + + // Only create, do not update. + public void CreateCache( bool isDefault ) + { + if( _cache == null ) + { + CalculateEffectiveFileList( true, isDefault ); + } + } + + // Force an update with metadata for this cache. + public void ForceCacheUpdate( bool isDefault ) + => CalculateEffectiveFileList( true, isDefault ); + + + // Clear the current cache. + public void ClearCache() + { + _cache?.Dispose(); + _cache = null; + } + + + public FullPath? ResolvePath( Utf8GamePath path ) + => _cache?.ResolvePath( path ); + + // Force a file to be resolved to a specific path regardless of conflicts. + internal void ForceFile( Utf8GamePath path, FullPath fullPath ) + => _cache!.ResolvedFiles[ path ] = fullPath; + + // Force a file resolve to be removed. + internal void RemoveFile( Utf8GamePath path ) + => _cache!.ResolvedFiles.Remove( path ); + + // Obtain data from the cache. + internal MetaManager? MetaCache + => _cache?.MetaManipulations; + + internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + + internal IReadOnlySet< FullPath > MissingFiles + => _cache?.MissingFiles ?? new HashSet< FullPath >(); + + internal IReadOnlyDictionary< string, object? > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + + internal IReadOnlyList< ConflictCache.Conflict > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); + + internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); + + // Update the effective file list for the given cache. + // Creates a cache if necessary. + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) + { + // Skip the empty collection. + if( Index == 0 ) + { + return; + } + + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + withMetaManipulations, reloadDefault ); + _cache ??= new Cache( this ); + _cache.CalculateEffectiveFileList( withMetaManipulations ); + if( reloadDefault ) + { + SetFiles(); + Penumbra.ResidentResources.Reload(); + } + } + + // Set Metadata files. + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + _cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + _cache.MetaManipulations.SetFiles(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 3e700cb5..6de9072c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,193 +1,24 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { - // Only active collections need to have a cache. - private Cache? _cache; - - public bool HasCache - => _cache != null; - - // Only create, do not update. - public void CreateCache( bool isDefault ) - { - if( _cache == null ) - { - CalculateEffectiveFileList( true, isDefault ); - } - } - - // Force an update with metadata for this cache. - public void ForceCacheUpdate( bool isDefault ) - => CalculateEffectiveFileList( true, isDefault ); - - - // Clear the current cache. - public void ClearCache() - { - _cache?.Dispose(); - _cache = null; - } - - - public FullPath? ResolvePath( Utf8GamePath path ) - => _cache?.ResolvePath( path ); - - // Force a file to be resolved to a specific path regardless of conflicts. - internal void ForceFile( Utf8GamePath path, FullPath fullPath ) - => _cache!.ResolvedFiles[ path ] = fullPath; - - // Force a file resolve to be removed. - internal void RemoveFile( Utf8GamePath path ) - => _cache!.ResolvedFiles.Remove( path ); - - // Obtain data from the cache. - internal MetaManager? MetaCache - => _cache?.MetaManipulations; - - internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); - - internal IReadOnlySet< FullPath > MissingFiles - => _cache?.MissingFiles ?? new HashSet< FullPath >(); - - internal IReadOnlyDictionary< string, object? > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, object? >(); - - internal IReadOnlyList< ConflictCache.Conflict > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); - - internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); - - // Update the effective file list for the given cache. - // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) - { - // Skip the empty collection. - if( Index == 0 ) - { - return; - } - - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, - withMetaManipulations, reloadDefault ); - _cache ??= new Cache( this ); - _cache.CalculateEffectiveFileList(); - if( withMetaManipulations ) - { - _cache.UpdateMetaManipulations(); - if( reloadDefault ) - { - SetFiles(); - } - } - - if( reloadDefault ) - { - Penumbra.ResidentResources.Reload(); - } - } - - // Set Metadata files. - [Conditional( "USE_EQP" )] - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqp.SetFiles(); - } - } - - [Conditional( "USE_EQDP" )] - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqdp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqdp.SetFiles(); - } - } - - [Conditional( "USE_GMP" )] - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerGmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Gmp.SetFiles(); - } - } - - [Conditional( "USE_EST" )] - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEst.ResetFiles(); - } - else - { - _cache.MetaManipulations.Est.SetFiles(); - } - } - - [Conditional( "USE_CMP" )] - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerCmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Cmp.SetFiles(); - } - } - - public void SetFiles() - { - if( _cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - _cache.MetaManipulations.SetFiles(); - } - } - - - // The ModCollectionCache contains all required temporary data to use a collection. + // The Cache contains all required temporary data to use a collection. // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); - private static readonly List< ModSettings? > ResolvedSettings = new(128); + private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024); + private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024); + private static readonly List< ModSettings2? > ResolvedSettings = new(128); private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); @@ -221,7 +52,24 @@ public partial class ModCollection _collection.InheritanceChanged -= OnInheritanceChange; } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + // Resolve a given game path according to this collection. + public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) + { + return null; + } + + return candidate; + } + + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { // Recompute the file list if it was not just a non-conflicting priority change // or a setting change for a disabled mod. @@ -232,7 +80,7 @@ public partial class ModCollection } var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance - || Penumbra.ModManager[ modIdx ].Resources.MetaManipulations.Count > 0; + || Penumbra.ModManager[ modIdx ].AllManipulations.Any(); _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); } @@ -241,22 +89,6 @@ public partial class ModCollection private void OnInheritanceChange( bool _ ) => _collection.CalculateEffectiveFileList( true, true ); - // Reset the shared file-seen cache. - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - // Clear all local and global caches to prepare for recomputation. private void ClearStorageAndPrepare() { @@ -270,21 +102,27 @@ public partial class ModCollection ResolvedSettings.AddRange( _collection.ActualSettings ); } - public void CalculateEffectiveFileList() + // Recalculate all file changes from current settings. Include all fixed custom redirects. + // Recalculate meta manipulations only if withManipulations is true. + public void CalculateEffectiveFileList( bool withManipulations ) { ClearStorageAndPrepare(); + if( withManipulations ) + { + RegisteredManipulations.Clear(); + MetaManipulations.Reset(); + } + + AddCustomRedirects(); for( var i = 0; i < Penumbra.ModManager.Count; ++i ) { - if( ResolvedSettings[ i ]?.Enabled == true ) - { - AddFiles( i ); - AddSwaps( i ); - } + AddMod( i, withManipulations ); } AddMetaFiles(); } + // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) @@ -309,228 +147,185 @@ public partial class ModCollection } } - private void AddFiles( int idx ) + // Add a specific file redirection, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddFile( Utf8GamePath path, FullPath file, FileRegister priority ) { - var mod = Penumbra.ModManager.Mods[ idx ]; - ResetFileSeen( mod.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - // TODO: add group priorities. - foreach( var group in mod.Meta.Groups.Values.Reverse() ) + if( RegisteredFiles.TryGetValue( path, out var register ) ) { - switch( group.SelectionType ) + if( register.SameMod( priority, out var less ) ) { - case SelectType.Single: - AddFilesForSingle( group, mod, idx ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod, idx ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod, idx ); - } - - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - private static bool FilterFile( Utf8GamePath gamePath ) - => !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) - { - // No current conflict, just add. - RegisteredFiles.Add( gamePath, modIdx ); - ResolvedFiles[ gamePath ] = file; - } - else - { - // Conflict, check which mod has higher priority, replace if necessary, add conflict. - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); - if( priority > oldPriority ) - { - ResolvedFiles[ gamePath ] = file; - RegisteredFiles[ gamePath ] = modIdx; - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - // We do not care for those file types - case ".scp" when !Penumbra.Config.DisableSoundStreaming: - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, Mod mod, int modIdx, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - // TODO: complete rework of options. - var fullPath = new FullPath( mod.BasePath, file ); - var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) + Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path ); + if( less ) { - AddFile( modIdx, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, Mod mod, int modIdx ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, Mod mod, int modIdx ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( Mod mod, int modIdx ) - { - for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.BasePath, out var gamePath ) ) - { - AddFile( modIdx, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); + RegisteredFiles[ path ] = priority; + ResolvedFiles[ path ] = file; } } else { - MissingFiles.Add( file ); + // File seen before in the same mod: + // use higher priority or earlier recurrences in case of same priority. + // Do not add conflicts. + if( less ) + { + RegisteredFiles[ path ] = priority; + ResolvedFiles[ path ] = file; + } + } + } + else // File not seen before, just add it. + { + RegisteredFiles.Add( path, priority ); + ResolvedFiles.Add( path, file ); + } + } + + // Add a specific manipulation, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddManipulation( MetaManipulation manip, FileRegister priority ) + { + if( RegisteredManipulations.TryGetValue( manip, out var register ) ) + { + if( register.SameMod( priority, out var less ) ) + { + Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, manip ); + if( less ) + { + RegisteredManipulations[ manip ] = priority; + MetaManipulations.ApplyMod( manip, priority.ModIdx ); + } + } + else + { + // Manipulation seen before in the same mod: + // use higher priority or earlier occurrences in case of same priority. + // Do not add conflicts. + if( less ) + { + RegisteredManipulations[ manip ] = priority; + MetaManipulations.ApplyMod( manip, priority.ModIdx ); + } + } + } + else // Manipulation not seen before, just add it. + { + RegisteredManipulations[ manip ] = priority; + MetaManipulations.ApplyMod( manip, priority.ModIdx ); + } + } + + // Add all files and possibly manipulations of a specific submod with the given priorities. + private void AddSubMod( ISubMod mod, FileRegister priority, bool withManipulations ) + { + foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) ) + { + // Skip all filtered files + if( Mod2.FilterFile( path ) ) + { + continue; + } + + AddFile( path, file, priority ); + } + + if( withManipulations ) + { + foreach( var manip in mod.Manipulations ) + { + AddManipulation( manip, priority ); } } } + // Add all files and possibly manipulations of a given mod according to its settings in this collection. + private void AddMod( int modIdx, bool withManipulations ) + { + var settings = ResolvedSettings[ modIdx ]; + if( settings is not { Enabled: true } ) + { + return; + } + + var mod = Penumbra.ModManager.Mods[ modIdx ]; + AddSubMod( mod.Default, new FileRegister( modIdx, settings.Priority, 0, 0 ), withManipulations ); + for( var idx = 0; idx < mod.Groups.Count; ++idx ) + { + var config = settings.Settings[ idx ]; + var group = mod.Groups[ idx ]; + switch( group.Type ) + { + case SelectType.Single: + var singlePriority = new FileRegister( modIdx, settings.Priority, group.Priority, group.Priority ); + AddSubMod( group[ ( int )config ], singlePriority, withManipulations ); + break; + case SelectType.Multi: + { + for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + { + if( ( ( 1 << optionIdx ) & config ) != 0 ) + { + var priority = new FileRegister( modIdx, settings.Priority, group.Priority, group.OptionPriority( optionIdx ) ); + AddSubMod( group[ optionIdx ], priority, withManipulations ); + } + } + + break; + } + } + } + } + + // Add all necessary meta file redirects. private void AddMetaFiles() => MetaManipulations.Imc.SetFiles(); - private void AddSwaps( int modIdx ) + // Add all API redirects. + private void AddCustomRedirects() { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + Penumbra.Redirects.Apply( ResolvedFiles ); + foreach( var gamePath in ResolvedFiles.Keys ) { - AddFile( modIdx, gamePath, swapPath ); + RegisteredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); } } - private void AddManipulations( int modIdx ) + + // Struct to keep track of all priorities involved in a mod and register and compare accordingly. + private readonly record struct FileRegister( int ModIdx, int ModPriority, int GroupPriority, int OptionPriority ) { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var manip in mod.Resources.MetaManipulations.GetManipulationsForConfig( ResolvedSettings[ modIdx ]!, mod.Meta ) ) + public readonly int ModIdx = ModIdx; + public readonly int ModPriority = ModPriority; + public readonly int GroupPriority = GroupPriority; + public readonly int OptionPriority = OptionPriority; + + public bool SameMod( FileRegister other, out bool less ) { - if( !MetaManipulations.TryGetValue( manip, out var oldModIdx ) ) + if( ModIdx != other.ModIdx ) { - MetaManipulations.ApplyMod( manip, modIdx ); + less = ModPriority < other.ModPriority; + return true; + } + + if( GroupPriority < other.GroupPriority ) + { + less = true; + } + else if( GroupPriority == other.GroupPriority ) + { + less = OptionPriority < other.OptionPriority; } else { - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, manip ); - if( priority > oldPriority ) - { - MetaManipulations.ApplyMod( manip, modIdx ); - } + less = false; } + + return false; } - } - - public void UpdateMetaManipulations() - { - MetaManipulations.Reset(); - Conflicts.ClearMetaConflicts(); - - foreach( var mod in Penumbra.ModManager.Mods.Zip( ResolvedSettings ) - .Select( ( m, i ) => ( m.First, m.Second, i ) ) - .Where( m => m.Second?.Enabled == true && m.First.Resources.MetaManipulations.Count > 0 ) ) - { - AddManipulations( mod.i ); - } - } - - public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } + }; } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 114efe63..78e6ee07 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -20,7 +20,7 @@ public partial class ModCollection { // If the change type is a bool, oldValue will be 1 for true and 0 for false. // optionName will only be set for type == Setting. - public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ); + public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ); public event ModSettingChangeDelegate ModSettingChanged; // Enable or disable the mod inheritance of mod idx. @@ -28,7 +28,7 @@ public partial class ModCollection { if( FixInheritance( idx, inherit ) ) { - ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); } } @@ -41,22 +41,22 @@ public partial class ModCollection { var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Enabled = newValue; - ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); } } // Enable or disable the mod inheritance of every mod in mods. - public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) + public void SetMultipleModInheritances( IEnumerable< Mod2 > mods, bool inherit ) { if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) { - ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, 0, false ); } } // Set the enabled state of every mod in mods to the new value. // If the mod is currently inherited, stop the inheritance. - public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue ) + public void SetMultipleModStates( IEnumerable< Mod2 > mods, bool newValue ) { var changes = false; foreach( var mod in mods ) @@ -72,7 +72,7 @@ public partial class ModCollection if( changes ) { - ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, 0, false ); } } @@ -85,26 +85,21 @@ public partial class ModCollection { var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Priority = newValue; - ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null, false ); + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); } } // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. // If mod idx is currently inherited, stop the inheritance. - public void SetModSetting( int idx, string settingName, int newValue ) + public void SetModSetting( int idx, int groupIdx, uint newValue ) { var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; - var oldValue = settings != null - ? settings.TryGetValue( settingName, out var v ) ? v : newValue - : Penumbra.ModManager.Mods[ idx ].Meta.Groups.ContainsKey( settingName ) - ? 0 - : newValue; + var oldValue = settings?[ groupIdx ] ?? 0; if( oldValue != newValue ) { var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Settings[ settingName ] = newValue; - _settings[ idx ]!.FixSpecificSetting( settingName, Penumbra.ModManager.Mods[ idx ].Meta ); - ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName, false ); + _settings[ idx ]!.SetValue( Penumbra.ModManager.Mods[ idx ], groupIdx, newValue ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); } } @@ -112,7 +107,7 @@ public partial class ModCollection // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. // The setting will also be automatically fixed if it is invalid for that setting group. // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - public void ChangeModSetting( ModSettingChange type, int idx, int newValue, string? settingName = null ) + public void ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) { switch( type ) { @@ -126,7 +121,7 @@ public partial class ModCollection SetModPriority( idx, newValue ); break; case ModSettingChange.Setting: - SetModSetting( idx, settingName ?? string.Empty, newValue ); + SetModSetting( idx, groupIdx, ( uint )newValue ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } @@ -142,11 +137,11 @@ public partial class ModCollection return false; } - _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); return true; } - private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited ) + private void SaveOnChange( ModSettingChange _1, int _2, int _3, int _4, bool inherited ) => SaveOnChange( inherited ); private void SaveOnChange( bool inherited ) diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index cad2c209..0311e0b0 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -48,7 +48,7 @@ public partial class ModCollection if( settings != null ) { j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, settings ); + x.Serialize( j, new ModSettings2.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); } } @@ -111,8 +111,8 @@ public partial class ModCollection var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; // Custom deserialization that is converted with the constructor. - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() - ?? new Dictionary< string, ModSettings >(); + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings2.SavedSettings > >() + ?? new Dictionary< string, ModSettings2.SavedSettings >(); inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); return new ModCollection( name, version, settings ); diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index e333e6e0..34156396 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -71,11 +71,11 @@ public partial class ModCollection } // Carry changes in collections inherited from forward if they are relevant for this collection. - private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { if( _settings[ modIdx ] == null ) { - ModSettingChanged.Invoke( type, modIdx, oldValue, optionName, true ); + ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); } } @@ -85,7 +85,7 @@ public partial class ModCollection // Obtain the actual settings for a given mod via index. // Also returns the collection the settings are taken from. // If no collection provides settings for this mod, this collection is returned together with null. - public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] + public (ModSettings2? Settings, ModCollection Collection) this[ Index idx ] { get { diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 42903ea8..4bfcccda 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -45,10 +45,13 @@ public sealed partial class ModCollection } // We treat every completely defaulted setting as inheritance-ready. - private static bool SettingIsDefaultV0( ModSettings? setting ) + private static bool SettingIsDefaultV0( ModSettings2.SavedSettings setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); + + private static bool SettingIsDefaultV0( ModSettings2? setting ) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 ); } - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings2.SavedSettings > allSettings ) => new(name, 0, allSettings); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 6e82617f..ce3df492 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -27,17 +27,17 @@ public partial class ModCollection // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List< ModSettings? > _settings; + private readonly List< ModSettings2? > _settings; - public IReadOnlyList< ModSettings? > Settings + public IReadOnlyList< ModSettings2? > Settings => _settings; // Evaluates the settings along the whole inheritance tree. - public IEnumerable< ModSettings? > ActualSettings + public IEnumerable< ModSettings2? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary< string, ModSettings > _unusedSettings; + private readonly Dictionary< string, ModSettings2.SavedSettings > _unusedSettings; // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) @@ -52,13 +52,13 @@ public partial class ModCollection } // Constructor for reading from files. - private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) + private ModCollection( string name, int version, Dictionary< string, ModSettings2.SavedSettings > allSettings ) { Name = name; Version = version; _unusedSettings = allSettings; - _settings = new List< ModSettings? >(); + _settings = new List< ModSettings2? >(); ApplyModSettings(); Migration.Migrate( this ); @@ -68,7 +68,7 @@ public partial class ModCollection // Create a new, unique empty collection of a given name. public static ModCollection CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + => new(name, CurrentVersion, new Dictionary< string, ModSettings2.SavedSettings >()); // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) @@ -86,26 +86,27 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private void AddMod( Mod mod ) + private bool AddMod( Mod2 mod ) { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) ) { + var ret = save.ToSettings( mod, out var settings ); _settings.Add( settings ); _unusedSettings.Remove( mod.BasePath.Name ); + return ret; } - else - { - _settings.Add( null ); - } + + _settings.Add( null ); + return false; } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mod mod, int idx ) + private void RemoveMod( Mod2 mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings.Add( mod.BasePath.Name, settings ); + _unusedSettings.Add( mod.BasePath.Name, new ModSettings2.SavedSettings( settings, mod ) ); } _settings.RemoveAt( idx ); @@ -126,7 +127,7 @@ public partial class ModCollection { foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) { - _unusedSettings[ mod.BasePath.Name ] = setting!; + _unusedSettings[ mod.BasePath.Name ] = new ModSettings2.SavedSettings( setting!, mod ); } _settings.Clear(); @@ -137,22 +138,7 @@ public partial class ModCollection private void ApplyModSettings() { _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); - var changes = false; - foreach( var mod in Penumbra.ModManager ) - { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var s ) ) - { - changes |= s.FixInvalidSettings( mod.Meta ); - _settings.Add( s ); - _unusedSettings.Remove( mod.BasePath.Name ); - } - else - { - _settings.Add( null ); - } - } - - if( changes ) + if( Penumbra.ModManager.Aggregate( false, ( current, mod ) => current | AddMod( mod ) ) ) { Save(); } diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index a816d6a5..0caa5a05 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -6,7 +6,6 @@ using System.Text; using Dalamud.Logging; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; using Penumbra.Importer.Models; using Penumbra.Mods; using Penumbra.Util; @@ -148,23 +147,13 @@ internal class TexToolsImport var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = "Unknown", - Name = modPackFile.Name, - Description = "Mod imported from TexTools mod pack", - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) - ); + // Create a new ModMeta from the TTMP modlist info + Mod2.CreateMeta( ExtractedDirectory, string.IsNullOrEmpty( modPackFile.Name ) ? "New Mod" : modPackFile.Name, "Unknown", + "Mod imported from TexTools mod pack.", null, null ); ExtractSimpleModList( ExtractedDirectory, modList, modData ); @@ -173,7 +162,7 @@ internal class TexToolsImport private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) { - var modList = JsonConvert.DeserializeObject( modRaw ); + var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw ); if( modList.TTMPVersion?.EndsWith( "s" ) ?? false ) { @@ -233,23 +222,13 @@ internal class TexToolsImport { PluginLog.Log( " -> Importing Simple V2 ModPack" ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description!, - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - - File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) ); + Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", string.IsNullOrEmpty( modList.Description ) + ? "Mod imported from TexTools mod pack" + : modList.Description, null, null ); ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); return ExtractedDirectory; @@ -261,21 +240,12 @@ internal class TexToolsImport var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description ?? "", - Version = modList.Version ?? "", - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); + Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", + string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, null ); if( modList.SimpleModsList != null ) { @@ -288,6 +258,8 @@ internal class TexToolsImport } // Iterate through all pages + var options = new List< ISubMod >(); + var groupPriority = 0; foreach( var page in modList.ModPackPages ) { if( page.ModGroups == null ) @@ -297,6 +269,8 @@ internal class TexToolsImport foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) { + options.Clear(); + var description = new StringBuilder(); var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! ); if( groupFolder.Exists ) { @@ -308,52 +282,19 @@ internal class TexToolsImport { var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); - } - - AddMeta( ExtractedDirectory, groupFolder, group, modMeta ); - } - } - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta, Formatting.Indented ) - ); - return ExtractedDirectory; - } - - private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta ) - { - var inf = new OptionGroup - { - SelectionType = group.SelectionType, - GroupName = group.GroupName!, - Options = new List< Option >(), - }; - foreach( var opt in group.OptionList! ) - { - var option = new Option - { - OptionName = opt.Name!, - OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - var optDir = NewOptionDirectory( groupFolder, opt.Name! ); - if( optDir.Exists ) - { - foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - if( Utf8RelPath.FromFile( file, baseFolder, out var rel ) - && Utf8GamePath.FromFile( file, optDir, out var game, true ) ) + options.Add( Mod2.CreateSubMod( ExtractedDirectory, optionFolder, option ) ); + description.Append( option.Description ); + if( !string.IsNullOrEmpty( option.Description ) ) { - option.AddFile( rel, game ); + description.Append( '\n' ); } } + + Mod2.CreateOptionGroup( ExtractedDirectory, group, groupPriority++, description.ToString(), options ); } - - inf.Options.Add( option ); } - - meta.Groups.Add( inf.GroupName, inf ); + Mod2.CreateDefaultFiles( ExtractedDirectory ); + return ExtractedDirectory; } private void ImportMetaModPack( FileInfo file ) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f7e78b12..dd7eb89e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -92,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Mods.Mod.Manager.ResolvePath( path ); + var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index ce838fe9..e591a0b6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -41,8 +41,8 @@ public partial class MetaManager public bool ApplyMod( RspManipulation m, int modIdx ) { #if USE_CMP - Manipulations[ m ] = modIdx; - File ??= new CmpFile(); + Manipulations[ m ] = modIdx; + File ??= new CmpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 92d8f3d8..053e8228 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -41,8 +41,8 @@ public partial class MetaManager public bool ApplyMod( EqpManipulation m, int modIdx ) { #if USE_EQP - Manipulations[ m ] = modIdx; - File ??= new ExpandedEqpFile(); + Manipulations[ m ] = modIdx; + File ??= new ExpandedEqpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index c921049f..23c83487 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -20,7 +20,7 @@ public partial class MetaManager public readonly Dictionary< ImcManipulation, int > Manipulations = new(); private readonly ModCollection _collection; - private static int _imcManagerCount; + private static int _imcManagerCount; public MetaManagerImc( ModCollection collection ) @@ -102,6 +102,7 @@ public partial class MetaManager Files.Clear(); Manipulations.Clear(); + RestoreDelegate(); } [Conditional( "USE_IMC" )] @@ -141,11 +142,11 @@ public partial class MetaManager if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) && collection.HasCache && collection.MetaCache!.Imc.Files.TryGetValue( - Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); - file.Replace( fileDescriptor->ResourceHandle, true); + file.Replace( fileDescriptor->ResourceHandle, true ); file.ChangesSinceLoad = false; } diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs deleted file mode 100644 index d489702e..00000000 --- a/Penumbra/Meta/MetaCollection.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Importer; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; - -namespace Penumbra.Meta; - -// Corresponds meta manipulations of any kind with the settings for a mod. -// DefaultData contains all manipulations that are active regardless of option groups. -// GroupData contains a mapping of Group -> { Options -> {Manipulations} }. -public class MetaCollection -{ - public List< MetaManipulation > DefaultData = new(); - public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); - - - // Store total number of manipulations for some ease of access. - [JsonIgnore] - internal int Count; - - - // Return an enumeration of all active meta manipulations for a given mod with given settings. - public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) - { - if( Count == DefaultData.Count ) - { - return DefaultData; - } - - IEnumerable< MetaManipulation > ret = DefaultData; - - foreach( var group in modMeta.Groups ) - { - if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) - { - continue; - } - - if( group.Value.SelectionType == SelectType.Single ) - { - var settingName = group.Value.Options[ setting ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - else - { - for( var i = 0; i < group.Value.Options.Count; ++i ) - { - var flag = 1 << i; - if( ( setting & flag ) == 0 ) - { - continue; - } - - var settingName = group.Value.Options[ i ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - } - } - - return ret; - } - - // Check that the collection is still basically valid, - // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, - // and that the contained manipulations are still valid and non-default manipulations. - public bool Validate( ModMeta modMeta ) - { - SortLists(); - foreach( var group in GroupData ) - { - if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) - { - return false; - } - - foreach( var option in group.Value ) - { - if( options.Options.All( o => o.OptionName != option.Key ) ) - { - return false; - } - - //if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) - //{ - // return false; - //} - } - } // TODO - - return true; //DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); - } - - // Re-sort all manipulations. - private void SortLists() - { - DefaultData.Sort(); - foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) - { - list.Sort(); - } - } - - // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. - // Creates the option group and the option if necessary. - private void AddMeta( string group, string option, TexToolsMeta meta ) - { - var manipulations = meta.EqpManipulations.Select( m => new MetaManipulation( m ) ) - .Concat( meta.EqdpManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.EstManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.GmpManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.RspManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.ImcManipulations.Select( m => new MetaManipulation( m ) ) ).ToList(); - - if( group.Length == 0 ) - { - DefaultData.AddRange( manipulations ); - } - else if( option.Length == 0 ) - { } - else if( !GroupData.TryGetValue( group, out var options ) ) - { - GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, manipulations } } ); - } - else if( !options.TryGetValue( option, out var list ) ) - { - options.Add( option, manipulations ); - } - else - { - list.AddRange( manipulations ); - } - - Count += manipulations.Count; - } - - // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, - // combining them with the given ModMeta. - public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) - { - DefaultData.Clear(); - GroupData.Clear(); - Count = 0; - foreach( var file in files ) - { - var metaData = file.Extension.ToLowerInvariant() switch - { - ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), - ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), - _ => TexToolsMeta.Invalid, - }; - - if( metaData.FilePath == string.Empty ) - { - continue; - } - - Utf8RelPath.FromFile( file, basePath, out var path ); - var foundAny = false; - foreach( var (name, group) in modMeta.Groups ) - { - foreach( var option in group.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) - { - foundAny = true; - AddMeta( name, option.OptionName, metaData ); - } - } - - if( !foundAny ) - { - AddMeta( string.Empty, string.Empty, metaData ); - } - } - - SortLists(); - } - - public static FileInfo FileName( DirectoryInfo basePath ) - => new(Path.Combine( basePath.FullName, "metadata_manipulations.json" )); - - public void SaveToFile( FileInfo file ) - { - try - { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( file.FullName, text ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); - } - } - - public static MetaCollection? LoadFromFile( FileInfo file ) - { - if( !file.Exists ) - { - return null; - } - - try - { - var text = File.ReadAllText( file.FullName ); - - var collection = JsonConvert.DeserializeObject< MetaCollection >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - - if( collection != null ) - { - if( collection.DefaultData.Concat( collection.GroupData.Values.SelectMany( kvp => kvp.Values.SelectMany( l => l ) ) ) - .Any( m => m.ManipulationType == MetaManipulation.Type.Unknown || !Enum.IsDefined( m.ManipulationType ) ) ) - { - throw new Exception( "Invalid collection" ); - } - - collection.Count = collection.DefaultData.Count - + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); - } - - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); - return null; - } - } -} \ No newline at end of file diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index cc2bce8a..b4c05850 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -5,7 +5,6 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Mods; @@ -21,7 +20,7 @@ public class MigrateConfiguration public string ForcedCollection = string.Empty; public Dictionary< string, string > CharacterCollections = new(); public Dictionary< string, string > ModSortOrder = new(); - public bool InvertModListOrder = false; + public bool InvertModListOrder; public static void Migrate( Configuration config ) @@ -143,32 +142,28 @@ public class MigrateConfiguration var data = JArray.Parse( text ); var maxPriority = 0; - var dict = new Dictionary< string, ModSettings >(); + var dict = new Dictionary< string, ModSettings2.SavedSettings >(); foreach( var setting in data.Cast< JObject >() ) { var modName = ( string )setting[ "FolderName" ]!; var enabled = ( bool )setting[ "Enabled" ]!; var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); - dict[ modName ] = new ModSettings() + dict[ modName ] = new ModSettings2.SavedSettings() { Enabled = enabled, Priority = priority, Settings = settings!, }; - ; maxPriority = Math.Max( maxPriority, priority ); } InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; if( !InvertModListOrder ) { - foreach( var setting in dict.Values ) - { - setting.Priority = maxPriority - setting.Priority; - } + dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); } defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); diff --git a/Penumbra/Mods/FullMod.cs b/Penumbra/Mods/FullMod.cs deleted file mode 100644 index 50ee92b5..00000000 --- a/Penumbra/Mods/FullMod.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// A complete Mod containing settings (i.e. dependent on a collection) -// and the resulting cache. -public class FullMod -{ - public ModSettings Settings { get; } - public Mod Data { get; } - - public FullMod( ModSettings settings, Mod data ) - { - Settings = settings; - Data = data; - } - - public bool FixSettings() - => Settings.FixInvalidSettings( Data.Meta ); - - public HashSet< Utf8GamePath > GetFiles( FileInfo file ) - { - var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty; - return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); - } - - public override string ToString() - => Data.Meta.Name; -} \ No newline at end of file diff --git a/Penumbra/Mods/GroupInformation.cs b/Penumbra/Mods/GroupInformation.cs deleted file mode 100644 index f71e0d3c..00000000 --- a/Penumbra/Mods/GroupInformation.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public enum SelectType -{ - Single, - Multi, -} - -public struct Option -{ - public string OptionName; - public string OptionDesc; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] - public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles; - - public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath ) - { - if( OptionFiles.TryGetValue( filePath, out var set ) ) - { - return set.Add( gamePath ); - } - - OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath }; - return true; - } -} - -public struct OptionGroup -{ - public string GroupName; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType; - - public List< Option > Options; - - private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) - { - // Selection contains the path, merge all GamePaths for this config. - if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - return true; - } - - // If the group contains the file in another selection, return true to skip it for default files. - for( var i = 0; i < Options.Count; ++i ) - { - if( i == selection ) - { - continue; - } - - if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - return true; - } - } - - return false; - } - - private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) - { - var doNotAdd = false; - for( var i = 0; i < Options.Count; ++i ) - { - if( ( selection & ( 1 << i ) ) != 0 ) - { - if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - doNotAdd = true; - } - } - else if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - doNotAdd = true; - } - } - - return doNotAdd; - } - - // Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist. - internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) - { - return SelectionType switch - { - SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ), - SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ), - _ => throw new InvalidEnumArgumentException( "Invalid option group type." ), - }; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod2.Manager.cs b/Penumbra/Mods/Manager/Mod2.Manager.cs index 2ba5fe27..c2250d3a 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.cs @@ -25,7 +25,6 @@ public sealed partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( string modDirectory ) { SetBaseDirectory( modDirectory, true ); diff --git a/Penumbra/Mods/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs deleted file mode 100644 index 5cf55c5e..00000000 --- a/Penumbra/Mods/Mod.SortOrder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public struct SortOrder : IComparable< SortOrder > - { - public ModFolder ParentFolder { get; set; } - - private string _sortOrderName; - - public string SortOrderName - { - get => _sortOrderName; - set => _sortOrderName = value.Replace( '/', '\\' ); - } - - public string SortOrderPath - => ParentFolder.FullName; - - public string FullName - { - get - { - var path = SortOrderPath; - return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName; - } - } - - public SortOrder( ModFolder parentFolder, string name ) - { - ParentFolder = parentFolder; - _sortOrderName = name.Replace( '/', '\\' ); - } - - public string FullPath - => SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; - - public int CompareTo( SortOrder other ) - => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs deleted file mode 100644 index 9ca93f85..00000000 --- a/Penumbra/Mods/Mod.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// Mod contains all permanent information about a mod, -// and is independent of collections or settings. -// It only changes when the user actively changes the mod or their filesystem. -public sealed partial class Mod -{ - public DirectoryInfo BasePath; - public ModMeta Meta; - public ModResources Resources; - - public SortOrder Order; - - public SortedList< string, object? > ChangedItems { get; } = new(); - public string LowerChangedItemsString { get; private set; } = string.Empty; - public FileInfo MetaFile { get; set; } - public int Index { get; private set; } = -1; - - private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources) - { - BasePath = basePath; - Meta = meta; - Resources = resources; - MetaFile = MetaFileInfo( basePath ); - Order = new SortOrder( parentFolder, Meta.Name ); - Order.ParentFolder.AddMod( this ); - ComputeChangedItems(); - } - - public void ComputeChangedItems() - { - var identifier = GameData.GameData.GetIdentifier(); - ChangedItems.Clear(); - foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) - { - foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - } - - foreach( var path in Meta.FileSwaps.Keys ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - - LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); - } - - public static FileInfo MetaFileInfo( DirectoryInfo basePath ) - => new(Path.Combine( basePath.FullName, "meta.json" )); - - public static Mod? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) - { - basePath.Refresh(); - if( !basePath.Exists ) - { - PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); - return null; - } - - var metaFile = MetaFileInfo( basePath ); - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - var meta = ModMeta.LoadFromFile( metaFile ); - if( meta == null ) - { - return null; - } - - var data = new ModResources(); - if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) - { - data.SetManipulations( meta, basePath ); - } - - return new Mod( parentFolder, basePath, meta, data ); - } - - public void SaveMeta() - => Meta.SaveToFile( MetaFile ); - - public override string ToString() - => Order.FullPath; -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs index 5b086b31..765b6106 100644 --- a/Penumbra/Mods/Mod2.BasePath.cs +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -37,7 +37,7 @@ public partial class Mod2 mod.LoadDefaultOption(); mod.LoadAllGroups(); mod.ComputeChangedItems(); - mod.SetHasOptions(); + mod.SetCounts(); return mod; } diff --git a/Penumbra/Mods/Mod2.Creation.cs b/Penumbra/Mods/Mod2.Creation.cs new file mode 100644 index 00000000..915ace3f --- /dev/null +++ b/Penumbra/Mods/Mod2.Creation.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Importer.Models; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, + string? website ) + { + var mod = new Mod2( directory ); + if( name is { Length: 0 } ) + { + mod.Name = name; + } + + if( author != null ) + { + mod.Author = author; + } + + if( description != null ) + { + mod.Description = description; + } + + if( version != null ) + { + mod.Version = version; + } + + if( website != null ) + { + mod.Website = website; + } + + mod.SaveMeta(); + } + + internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, + int priority, string desc, List< ISubMod > subMods ) + { + switch( groupData.SelectionType ) + { + case SelectType.Multi: + { + var group = new MultiModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + case SelectType.Single: + { + var group = new SingleModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + } + } + + internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod() + { + Name = option.Name!, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod2( directory ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + { + mod._default.FileData.TryAdd( gamePath, file ); + } + } + + mod._default.IncorporateMetaChanges( directory, true ); + mod.SaveDefaultMod(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs index 7ba7c63f..a9353bc1 100644 --- a/Penumbra/Mods/Mod2.Files.cs +++ b/Penumbra/Mods/Mod2.Files.cs @@ -17,19 +17,31 @@ public partial class Mod2 public IReadOnlyList< IModGroup > Groups => _groups; + private readonly SubMod _default = new(); + private readonly List< IModGroup > _groups = new(); + + public int TotalFileCount { get; private set; } + public int TotalSwapCount { get; private set; } + public int TotalManipulations { get; private set; } public bool HasOptions { get; private set; } - private void SetHasOptions() + private void SetCounts() { + TotalFileCount = 0; + TotalSwapCount = 0; + TotalManipulations = 0; + foreach( var s in AllSubMods ) + { + TotalFileCount += s.Files.Count; + TotalSwapCount += s.FileSwaps.Count; + TotalManipulations += s.Manipulations.Count; + } + HasOptions = _groups.Any( o => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); } - - private readonly SubMod _default = new(); - private readonly List< IModGroup > _groups = new(); - public IEnumerable< ISubMod > AllSubMods => _groups.SelectMany( o => o ).Prepend( _default ); @@ -56,6 +68,13 @@ public partial class Mod2 .ToList(); } + // Filter invalid files. + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + public static bool FilterFile( Utf8GamePath gamePath ) + => !Penumbra.Config.DisableSoundStreaming + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); + public List< FullPath > FindMissingFiles() => AllFiles.Where( f => !f.Exists ).ToList(); diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index f7a5e63f..9623487e 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -49,7 +49,7 @@ public sealed partial class Mod2 mod._default.FileSwapData.Add( gamePath, swapPath ); } - HandleMetaChanges( mod._default, mod.BasePath ); + mod._default.IncorporateMetaChanges( mod.BasePath, false ); foreach( var group in mod.Groups ) { IModGroup.SaveModGroup( group, mod.BasePath ); @@ -119,78 +119,11 @@ public sealed partial class Mod2 } } - private static void HandleMetaChanges( SubMod subMod, DirectoryInfo basePath ) - { - foreach( var (key, file) in subMod.Files.ToList() ) - { - try - { - switch( file.Extension ) - { - case ".meta": - subMod.FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); - foreach( var manip in meta.EqpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EqdpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EstManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.GmpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.ImcManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - break; - case ".rgsp": - subMod.FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); - foreach( var manip in rgsp.RspManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - break; - default: continue; - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not migrate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); - continue; - } - } - } - private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option ) { var subMod = new SubMod() { Name = option.OptionName }; AddFilesToSubMod( subMod, basePath, option ); - HandleMetaChanges( subMod, basePath ); + subMod.IncorporateMetaChanges( basePath, false ); return subMod; } diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index d09a26b8..80e7632b 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -12,520 +12,520 @@ using Penumbra.Util; namespace Penumbra.Mods; -public class ModCleanup -{ - private const string Duplicates = "Duplicates"; - private const string Required = "Required"; - - private readonly DirectoryInfo _baseDir; - private readonly ModMeta _mod; - private SHA256? _hasher; - - private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); - - private SHA256 Sha() - { - _hasher ??= SHA256.Create(); - return _hasher; - } - - private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) - { - _baseDir = baseDir; - _mod = mod; - BuildDict(); - } - - private void BuildDict() - { - foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - var fileLength = file.Length; - if( _filesBySize.TryGetValue( fileLength, out var files ) ) - { - files.Add( file ); - } - else - { - _filesBySize[ fileLength ] = new List< FileInfo > { file }; - } - } - } - - private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) - { - var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; - return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); - } - - private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) - { - var idx = Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[ idx ]; - newMod.Move( newSortOrder ); - newMod.ComputeChangedItems(); - ModFileSystem.InvokeChange(); - return newMod; - } - - private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) - { - var newMeta = new ModMeta - { - Author = mod.Meta.Author, - Name = name, - Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", - }; - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - newMeta.SaveToFile( metaFile ); - return newMeta; - } - - private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) - { - try - { - var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); - var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; - var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); - foreach( var (fileName, paths) in option.OptionFiles ) - { - var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); - unseenPaths.Remove( oldPath ); - if( File.Exists( oldPath ) ) - { - foreach( var path in paths ) - { - var newPath = Path.Combine( newDir.FullName, path.ToString() ); - Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); - File.Copy( oldPath, newPath, true ); - } - } - } - - var newSortOrder = group.SelectionType == SelectType.Single - ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" - : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; - CreateNewMod( newDir, newSortOrder ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not split Mod:\n{e}" ); - } - } - - public static void SplitMod( Mod mod ) - { - if( mod.Meta.Groups.Count == 0 ) - { - return; - } - - var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); - foreach( var group in mod.Meta.Groups.Values ) - { - foreach( var option in group.Options ) - { - CreateModSplit( unseenPaths, mod, group, option ); - } - } - - if( unseenPaths.Count == 0 ) - { - return; - } - - var defaultGroup = new OptionGroup() - { - GroupName = "Default", - SelectionType = SelectType.Multi, - }; - var defaultOption = new Option() - { - OptionName = "Files", - OptionFiles = unseenPaths.ToDictionary( - p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, - p => new HashSet< Utf8GamePath >() - { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), - }; - CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); - } - - private static Option FindOrCreateDuplicates( ModMeta meta ) - { - static Option RequiredOption() - => new() - { - OptionName = Required, - OptionDesc = "", - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - - if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) - { - var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); - if( idx >= 0 ) - { - return duplicates.Options[ idx ]; - } - - duplicates.Options.Add( RequiredOption() ); - return duplicates.Options.Last(); - } - - meta.Groups.Add( Duplicates, new OptionGroup - { - GroupName = Duplicates, - SelectionType = SelectType.Single, - Options = new List< Option > { RequiredOption() }, - } ); - - return meta.Groups[ Duplicates ].Options.First(); - } - - public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) - { - var dedup = new ModCleanup( baseDir, mod ); - foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) - { - if( value.Count == 2 ) - { - if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) - { - dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); - } - } - else - { - var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); - var hashes = value.Select( dedup.ComputeHash ).ToArray(); - - for( var i = 0; i < value.Count; ++i ) - { - if( deleted[ i ] ) - { - continue; - } - - for( var j = i + 1; j < value.Count; ++j ) - { - if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) - { - continue; - } - - dedup.ReplaceFile( value[ i ], value[ j ] ); - deleted[ j ] = true; - } - } - } - } - - CleanUpDuplicates( mod ); - ClearEmptySubDirectories( dedup._baseDir ); - } - - private void ReplaceFile( FileInfo f1, FileInfo f2 ) - { - if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) - || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) - { - return; - } - - var inOption1 = false; - var inOption2 = false; - foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) - { - if( option.OptionFiles.ContainsKey( relName1 ) ) - { - inOption1 = true; - } - - if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) - { - continue; - } - - inOption2 = true; - - foreach( var value in values ) - { - option.AddFile( relName1, value ); - } - - option.OptionFiles.Remove( relName2 ); - } - - if( !inOption1 || !inOption2 ) - { - var duplicates = FindOrCreateDuplicates( _mod ); - if( !inOption1 ) - { - duplicates.AddFile( relName1, relName2.ToGamePath() ); - } - - if( !inOption2 ) - { - duplicates.AddFile( relName1, relName1.ToGamePath() ); - } - } - - PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); - f2.Delete(); - } - - public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) - => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); - - public static bool CompareHashes( byte[] f1, byte[] f2 ) - => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); - - public byte[] ComputeHash( FileInfo f ) - { - var stream = File.OpenRead( f.FullName ); - var ret = Sha().ComputeHash( stream ); - stream.Dispose(); - return ret; - } - - // Does not delete the base directory itself even if it is completely empty at the end. - public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) - { - foreach( var subDir in baseDir.GetDirectories() ) - { - ClearEmptySubDirectories( subDir ); - if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) - { - subDir.Delete(); - } - } - } - - private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) - { - var groupEnumerator = exceptDuplicates - ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) - : meta.Groups.Values; - return groupEnumerator.SelectMany( group => group.Options ) - .Any( option => option.OptionFiles.ContainsKey( relPath ) ); - } - - private static void CleanUpDuplicates( ModMeta meta ) - { - if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) - { - return; - } - - var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); - if( requiredIdx >= 0 ) - { - var required = info.Options[ requiredIdx ]; - foreach( var (key, value) in required.OptionFiles.ToArray() ) - { - if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) - { - continue; - } - - if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) - { - required.OptionFiles.Remove( key ); - } - } - - if( required.OptionFiles.Count == 0 ) - { - info.Options.RemoveAt( requiredIdx ); - } - } - - if( info.Options.Count == 0 ) - { - meta.Groups.Remove( Duplicates ); - } - } - - public enum GroupType - { - Both = 0, - Single = 1, - Multi = 2, - }; - - private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, - bool skipDuplicates = true ) - { - if( meta.Groups.Count == 0 ) - { - return; - } - - var enumerator = type switch - { - GroupType.Both => meta.Groups.Values, - GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), - GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), - _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), - }; - foreach( var group in enumerator ) - { - var optionEnum = skipDuplicates - ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) - : group.Options; - foreach( var option in optionEnum ) - { - if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) - { - option.OptionFiles.Remove( relPath ); - } - } - } - } - - public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) - { - if( oldRelPath.Equals( newRelPath ) ) - { - return true; - } - - try - { - var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); - new FileInfo( newFullPath ).Directory!.Create(); - File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); - return false; - } - - foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) - { - if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) - { - option.OptionFiles.Add( newRelPath, gamePaths ); - option.OptionFiles.Remove( oldRelPath ); - } - } - - return true; - } - - - private static void RemoveUselessGroups( ModMeta meta ) - { - meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) - .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); - } - - // Goes through all Single-Select options and checks if file links are in each of them. - // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). - public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) - { - foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) - { - var firstOption = true; - HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); - foreach( var option in group.Options ) - { - HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); - foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) - { - optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); - } - - if( firstOption ) - { - groupList = optionList; - } - else - { - groupList.IntersectWith( optionList ); - } - - firstOption = false; - } - - var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); - foreach( var (path, gamePath) in groupList ) - { - var relPath = new Utf8RelPath( gamePath ); - if( newPath.TryGetValue( path, out var usedGamePath ) ) - { - var required = FindOrCreateDuplicates( meta ); - var usedRelPath = new Utf8RelPath( usedGamePath ); - required.AddFile( usedRelPath, gamePath ); - required.AddFile( usedRelPath, usedGamePath ); - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } - else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) - { - newPath[ path ] = gamePath; - if( FileIsInAnyGroup( meta, relPath ) ) - { - FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); - } - - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } - } - } - - RemoveUselessGroups( meta ); - ClearEmptySubDirectories( baseDir ); - } - - public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) - { - meta.Groups.Clear(); - ClearEmptySubDirectories( baseDir ); - foreach( var groupDir in baseDir.EnumerateDirectories() ) - { - var group = new OptionGroup - { - GroupName = groupDir.Name, - SelectionType = SelectType.Single, - Options = new List< Option >(), - }; - - foreach( var optionDir in groupDir.EnumerateDirectories() ) - { - var option = new Option - { - OptionDesc = string.Empty, - OptionName = optionDir.Name, - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - if( Utf8RelPath.FromFile( file, baseDir, out var rel ) - && Utf8GamePath.FromFile( file, optionDir, out var game ) ) - { - option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; - } - } - - if( option.OptionFiles.Count > 0 ) - { - group.Options.Add( option ); - } - } - - if( group.Options.Count > 0 ) - { - meta.Groups.Add( groupDir.Name, group ); - } - } - - // TODO - var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); - foreach( var collection in Penumbra.CollectionManager ) - { - collection.Settings[ idx ]?.FixInvalidSettings( meta ); - } - } -} \ No newline at end of file +//ublic class ModCleanup +// +// private const string Duplicates = "Duplicates"; +// private const string Required = "Required"; +// +// private readonly DirectoryInfo _baseDir; +// private readonly ModMeta _mod; +// private SHA256? _hasher; +// +// private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); +// +// private SHA256 Sha() +// { +// _hasher ??= SHA256.Create(); +// return _hasher; +// } +// +// private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) +// { +// _baseDir = baseDir; +// _mod = mod; +// BuildDict(); +// } +// +// private void BuildDict() +// { +// foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) +// { +// var fileLength = file.Length; +// if( _filesBySize.TryGetValue( fileLength, out var files ) ) +// { +// files.Add( file ); +// } +// else +// { +// _filesBySize[ fileLength ] = new List< FileInfo > { file }; +// } +// } +// } +// +// private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) +// { +// var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; +// return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); +// } +// +// private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) +// { +// var idx = Penumbra.ModManager.AddMod( newDir ); +// var newMod = Penumbra.ModManager.Mods[ idx ]; +// newMod.Move( newSortOrder ); +// newMod.ComputeChangedItems(); +// ModFileSystem.InvokeChange(); +// return newMod; +// } +// +// private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) +// { +// var newMeta = new ModMeta +// { +// Author = mod.Meta.Author, +// Name = name, +// Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", +// }; +// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); +// newMeta.SaveToFile( metaFile ); +// return newMeta; +// } +// +// private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) +// { +// try +// { +// var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); +// var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; +// var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); +// foreach( var (fileName, paths) in option.OptionFiles ) +// { +// var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); +// unseenPaths.Remove( oldPath ); +// if( File.Exists( oldPath ) ) +// { +// foreach( var path in paths ) +// { +// var newPath = Path.Combine( newDir.FullName, path.ToString() ); +// Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); +// File.Copy( oldPath, newPath, true ); +// } +// } +// } +// +// var newSortOrder = group.SelectionType == SelectType.Single +// ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" +// : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; +// CreateNewMod( newDir, newSortOrder ); +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Could not split Mod:\n{e}" ); +// } +// } +// +// public static void SplitMod( Mod mod ) +// { +// if( mod.Meta.Groups.Count == 0 ) +// { +// return; +// } +// +// var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); +// foreach( var group in mod.Meta.Groups.Values ) +// { +// foreach( var option in group.Options ) +// { +// CreateModSplit( unseenPaths, mod, group, option ); +// } +// } +// +// if( unseenPaths.Count == 0 ) +// { +// return; +// } +// +// var defaultGroup = new OptionGroup() +// { +// GroupName = "Default", +// SelectionType = SelectType.Multi, +// }; +// var defaultOption = new Option() +// { +// OptionName = "Files", +// OptionFiles = unseenPaths.ToDictionary( +// p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, +// p => new HashSet< Utf8GamePath >() +// { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), +// }; +// CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); +// } +// +// private static Option FindOrCreateDuplicates( ModMeta meta ) +// { +// static Option RequiredOption() +// => new() +// { +// OptionName = Required, +// OptionDesc = "", +// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), +// }; +// +// if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) +// { +// var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); +// if( idx >= 0 ) +// { +// return duplicates.Options[ idx ]; +// } +// +// duplicates.Options.Add( RequiredOption() ); +// return duplicates.Options.Last(); +// } +// +// meta.Groups.Add( Duplicates, new OptionGroup +// { +// GroupName = Duplicates, +// SelectionType = SelectType.Single, +// Options = new List< Option > { RequiredOption() }, +// } ); +// +// return meta.Groups[ Duplicates ].Options.First(); +// } +// +// public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) +// { +// var dedup = new ModCleanup( baseDir, mod ); +// foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) +// { +// if( value.Count == 2 ) +// { +// if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) +// { +// dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); +// } +// } +// else +// { +// var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); +// var hashes = value.Select( dedup.ComputeHash ).ToArray(); +// +// for( var i = 0; i < value.Count; ++i ) +// { +// if( deleted[ i ] ) +// { +// continue; +// } +// +// for( var j = i + 1; j < value.Count; ++j ) +// { +// if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) +// { +// continue; +// } +// +// dedup.ReplaceFile( value[ i ], value[ j ] ); +// deleted[ j ] = true; +// } +// } +// } +// } +// +// CleanUpDuplicates( mod ); +// ClearEmptySubDirectories( dedup._baseDir ); +// } +// +// private void ReplaceFile( FileInfo f1, FileInfo f2 ) +// { +// if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) +// || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) +// { +// return; +// } +// +// var inOption1 = false; +// var inOption2 = false; +// foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) +// { +// if( option.OptionFiles.ContainsKey( relName1 ) ) +// { +// inOption1 = true; +// } +// +// if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) +// { +// continue; +// } +// +// inOption2 = true; +// +// foreach( var value in values ) +// { +// option.AddFile( relName1, value ); +// } +// +// option.OptionFiles.Remove( relName2 ); +// } +// +// if( !inOption1 || !inOption2 ) +// { +// var duplicates = FindOrCreateDuplicates( _mod ); +// if( !inOption1 ) +// { +// duplicates.AddFile( relName1, relName2.ToGamePath() ); +// } +// +// if( !inOption2 ) +// { +// duplicates.AddFile( relName1, relName1.ToGamePath() ); +// } +// } +// +// PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); +// f2.Delete(); +// } +// +// public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) +// => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); +// +// public static bool CompareHashes( byte[] f1, byte[] f2 ) +// => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); +// +// public byte[] ComputeHash( FileInfo f ) +// { +// var stream = File.OpenRead( f.FullName ); +// var ret = Sha().ComputeHash( stream ); +// stream.Dispose(); +// return ret; +// } +// +// // Does not delete the base directory itself even if it is completely empty at the end. +// public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) +// { +// foreach( var subDir in baseDir.GetDirectories() ) +// { +// ClearEmptySubDirectories( subDir ); +// if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) +// { +// subDir.Delete(); +// } +// } +// } +// +// private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) +// { +// var groupEnumerator = exceptDuplicates +// ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) +// : meta.Groups.Values; +// return groupEnumerator.SelectMany( group => group.Options ) +// .Any( option => option.OptionFiles.ContainsKey( relPath ) ); +// } +// +// private static void CleanUpDuplicates( ModMeta meta ) +// { +// if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) +// { +// return; +// } +// +// var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); +// if( requiredIdx >= 0 ) +// { +// var required = info.Options[ requiredIdx ]; +// foreach( var (key, value) in required.OptionFiles.ToArray() ) +// { +// if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) +// { +// continue; +// } +// +// if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) +// { +// required.OptionFiles.Remove( key ); +// } +// } +// +// if( required.OptionFiles.Count == 0 ) +// { +// info.Options.RemoveAt( requiredIdx ); +// } +// } +// +// if( info.Options.Count == 0 ) +// { +// meta.Groups.Remove( Duplicates ); +// } +// } +// +// public enum GroupType +// { +// Both = 0, +// Single = 1, +// Multi = 2, +// }; +// +// private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, +// bool skipDuplicates = true ) +// { +// if( meta.Groups.Count == 0 ) +// { +// return; +// } +// +// var enumerator = type switch +// { +// GroupType.Both => meta.Groups.Values, +// GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), +// GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), +// _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), +// }; +// foreach( var group in enumerator ) +// { +// var optionEnum = skipDuplicates +// ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) +// : group.Options; +// foreach( var option in optionEnum ) +// { +// if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) +// { +// option.OptionFiles.Remove( relPath ); +// } +// } +// } +// } +// +// public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) +// { +// if( oldRelPath.Equals( newRelPath ) ) +// { +// return true; +// } +// +// try +// { +// var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); +// new FileInfo( newFullPath ).Directory!.Create(); +// File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); +// return false; +// } +// +// foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) +// { +// if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) +// { +// option.OptionFiles.Add( newRelPath, gamePaths ); +// option.OptionFiles.Remove( oldRelPath ); +// } +// } +// +// return true; +// } +// +// +// private static void RemoveUselessGroups( ModMeta meta ) +// { +// meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) +// .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); +// } +// +// // Goes through all Single-Select options and checks if file links are in each of them. +// // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). +// public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) +// { +// foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) +// { +// var firstOption = true; +// HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); +// foreach( var option in group.Options ) +// { +// HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); +// foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) +// { +// optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); +// } +// +// if( firstOption ) +// { +// groupList = optionList; +// } +// else +// { +// groupList.IntersectWith( optionList ); +// } +// +// firstOption = false; +// } +// +// var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); +// foreach( var (path, gamePath) in groupList ) +// { +// var relPath = new Utf8RelPath( gamePath ); +// if( newPath.TryGetValue( path, out var usedGamePath ) ) +// { +// var required = FindOrCreateDuplicates( meta ); +// var usedRelPath = new Utf8RelPath( usedGamePath ); +// required.AddFile( usedRelPath, gamePath ); +// required.AddFile( usedRelPath, usedGamePath ); +// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); +// } +// else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) +// { +// newPath[ path ] = gamePath; +// if( FileIsInAnyGroup( meta, relPath ) ) +// { +// FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); +// } +// +// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); +// } +// } +// } +// +// RemoveUselessGroups( meta ); +// ClearEmptySubDirectories( baseDir ); +// } +// +// public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) +// { +// meta.Groups.Clear(); +// ClearEmptySubDirectories( baseDir ); +// foreach( var groupDir in baseDir.EnumerateDirectories() ) +// { +// var group = new OptionGroup +// { +// GroupName = groupDir.Name, +// SelectionType = SelectType.Single, +// Options = new List< Option >(), +// }; +// +// foreach( var optionDir in groupDir.EnumerateDirectories() ) +// { +// var option = new Option +// { +// OptionDesc = string.Empty, +// OptionName = optionDir.Name, +// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), +// }; +// foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) +// { +// if( Utf8RelPath.FromFile( file, baseDir, out var rel ) +// && Utf8GamePath.FromFile( file, optionDir, out var game ) ) +// { +// option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; +// } +// } +// +// if( option.OptionFiles.Count > 0 ) +// { +// group.Options.Add( option ); +// } +// } +// +// if( group.Options.Count > 0 ) +// { +// meta.Groups.Add( groupDir.Name, group ); +// } +// } +// +// // TODO +// var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); +// foreach( var collection in Penumbra.CollectionManager ) +// { +// collection.Settings[ idx ]?.FixInvalidSettings( meta ); +// } +// } +// \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs deleted file mode 100644 index c4fbcc89..00000000 --- a/Penumbra/Mods/ModFileSystem.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.Mods; - -public delegate void OnModFileSystemChange(); - -public static partial class ModFileSystem -{ - // The root folder that should be used as the base for all structured mods. - public static ModFolder Root = ModFolder.CreateRoot(); - - // Gets invoked every time the file system changes. - public static event OnModFileSystemChange? ModFileSystemChanged; - - internal static void InvokeChange() - => ModFileSystemChanged?.Invoke(); - - // Find a specific mod folder by its path from Root. - // Returns true if the folder was found, and false if not. - // The out parameter will contain the furthest existing folder. - public static bool Find( string path, out ModFolder folder ) - { - var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - folder = Root; - foreach( var part in split ) - { - if( !folder.FindSubFolder( part, out folder ) ) - { - return false; - } - } - - return true; - } - - // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. - // Saves and returns true if anything changed. - public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName ) - { - if( RenameNoSave( mod, newName ) ) - { - SaveMod( mod ); - return true; - } - - return false; - } - - // Rename the target folder, merging it and its subfolders if the new name already exists. - // Saves all mods manipulated thus, and returns true if anything changed. - public static bool Rename( this ModFolder target, string newName ) - { - if( RenameNoSave( target, newName ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } - - // Move a single mod to the target folder. - // Returns true and saves if anything changed. - public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target ) - { - if( MoveNoSave( mod, target ) ) - { - SaveMod( mod ); - return true; - } - - return false; - } - - // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. - // Creates all necessary Subfolders. - public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder ) - { - var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - var folder = Root; - for( var i = 0; i < split.Length - 1; ++i ) - { - folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1; - } - - if( MoveNoSave( mod, folder ) | RenameNoSave( mod, split.Last() ) ) - { - SaveMod( mod ); - } - } - - // Moves folder to target. - // If an identically named subfolder of target already exists, merges instead. - // Root is not movable. - public static bool Move( this ModFolder folder, ModFolder target ) - { - if( MoveNoSave( folder, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } - - // Merge source with target, moving all direct mod children of source to target, - // and moving all subfolders of source to target, or merging them with targets subfolders if they exist. - // Returns true and saves if anything changed. - public static bool Merge( this ModFolder source, ModFolder target ) - { - if( MergeNoSave( source, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } -} - -// Internal stuff. -public static partial class ModFileSystem -{ - // Reset all sort orders for all descendants of the given folder. - // Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries. - private static void SaveModChildren( ModFolder target ) - { - foreach( var mod in target.AllMods( true ) ) - { - Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( Mod mod ) - { - if( ReferenceEquals( mod.Order.ParentFolder, Root ) - && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) - { - Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - } - else - { - Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - private static bool RenameNoSave( this ModFolder target, string newName ) - { - if( ReferenceEquals( target, Root ) ) - { - throw new InvalidOperationException( "Can not rename root." ); - } - - newName = newName.Replace( '/', '\\' ); - if( target.Name == newName ) - { - return false; - } - - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCulture; - if( target.Parent!.FindSubFolder( newName, out var preExisting ) ) - { - MergeNoSave( target, preExisting ); - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; - } - else - { - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; - var parent = target.Parent; - parent.RemoveFolderIgnoreEmpty( target ); - target.Name = newName; - parent.FindOrAddSubFolder( target ); - } - - return true; - } - - private static bool RenameNoSave( Mod mod, string newName ) - { - newName = newName.Replace( '/', '\\' ); - if( mod.Order.SortOrderName == newName ) - { - return false; - } - - mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.Order = new Mod.SortOrder( mod.Order.ParentFolder, newName ); - mod.Order.ParentFolder.AddMod( mod ); - return true; - } - - private static bool MoveNoSave( Mod mod, ModFolder target ) - { - var oldParent = mod.Order.ParentFolder; - if( ReferenceEquals( target, oldParent ) ) - { - return false; - } - - oldParent.RemoveMod( mod ); - mod.Order = new Mod.SortOrder( target, mod.Order.SortOrderName ); - target.AddMod( mod ); - return true; - } - - private static bool MergeNoSave( ModFolder source, ModFolder target ) - { - if( ReferenceEquals( source, target ) ) - { - return false; - } - - var any = false; - while( source.SubFolders.Count > 0 ) - { - any |= MoveNoSave( source.SubFolders.First(), target ); - } - - while( source.Mods.Count > 0 ) - { - any |= MoveNoSave( source.Mods.First(), target ); - } - - source.Parent?.RemoveSubFolder( source ); - - return any || source.Parent != null; - } - - private static bool MoveNoSave( ModFolder folder, ModFolder target ) - { - // Moving a folder into itself is not permitted. - if( ReferenceEquals( folder, target ) ) - { - return false; - } - - if( ReferenceEquals( target, folder.Parent! ) ) - { - return false; - } - - folder.Parent!.RemoveSubFolder( folder ); - var subFolderIdx = target.FindOrAddSubFolder( folder ); - if( subFolderIdx > 0 ) - { - var main = target.SubFolders[ subFolderIdx ]; - MergeNoSave( folder, main ); - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystemA.cs index c684d371..c2b62921 100644 --- a/Penumbra/Mods/ModFileSystemA.cs +++ b/Penumbra/Mods/ModFileSystemA.cs @@ -4,13 +4,13 @@ using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable +public sealed class ModFileSystemA : FileSystem< Mod2 >, IDisposable { // Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. public void Save() - => SaveToFile( new FileInfo( Mod.Manager.SortOrderFile ), SaveMod, true ); + => SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true ); // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystemA Load() @@ -31,7 +31,7 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable // Used on construction and on mod rediscoveries. private void Reload() { - if( Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + if( Load( new FileInfo( Mod2.Manager.ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) { Save(); } @@ -47,13 +47,13 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable } // Used for saving and loading. - private static string ModToIdentifier( Mod mod ) + private static string ModToIdentifier( Mod2 mod ) => mod.BasePath.Name; - private static string ModToName( Mod mod ) - => mod.Meta.Name.Text; + private static string ModToName( Mod2 mod ) + => mod.Name.Text; - private static (string, bool) SaveMod( Mod mod, string fullPath ) + private static (string, bool) SaveMod( Mod2 mod, string fullPath ) { // Only save pairs with non-default paths. if( fullPath == ModToName( mod ) ) diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs deleted file mode 100644 index 80a1df51..00000000 --- a/Penumbra/Mods/ModFolder.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.Mods; - -public partial class ModFolder -{ - public ModFolder? Parent; - - public string FullName - { - get - { - var parentPath = Parent?.FullName ?? string.Empty; - return parentPath.Any() ? $"{parentPath}/{Name}" : Name; - } - } - - private string _name = string.Empty; - - public string Name - { - get => _name; - set => _name = value.Replace( '/', '\\' ); - } - - public List< ModFolder > SubFolders { get; } = new(); - public List< Mod > Mods { get; } = new(); - - public ModFolder( ModFolder parent, string name ) - { - Parent = parent; - Name = name; - } - - public override string ToString() - => FullName; - - public int TotalDescendantMods() - => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); - - public int TotalDescendantFolders() - => SubFolders.Sum( f => f.TotalDescendantFolders() ); - - // Return all descendant mods in the specified order. - public IEnumerable< Mod > AllMods( bool foldersFirst ) - { - if( foldersFirst ) - { - return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); - } - - return GetSortedEnumerator().SelectMany( f => - { - if( f is ModFolder folder ) - { - return folder.AllMods( false ); - } - - return new[] { ( Mod )f }; - } ); - } - - // Return all descendant subfolders. - public IEnumerable< ModFolder > AllFolders() - => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); - - // Iterate through all descendants in the specified order, returning subfolders as well as mods. - public IEnumerable< object > GetItems( bool foldersFirst ) - => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); - - // Find a subfolder by name. Returns true and sets folder to it if it exists. - public bool FindSubFolder( string name, out ModFolder folder ) - { - var subFolder = new ModFolder( this, name ); - var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); - folder = idx >= 0 ? SubFolders[ idx ] : this; - return idx >= 0; - } - - // Checks if an equivalent subfolder as folder already exists and returns its index. - // If it does not exist, inserts folder as a subfolder and returns the new index. - // Also sets this as folders parent. - public int FindOrAddSubFolder( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - SubFolders.Insert( idx, folder ); - folder.Parent = this; - return idx; - } - - // Checks if a subfolder with the given name already exists and returns it and its index. - // If it does not exists, creates and inserts it and returns the new subfolder and its index. - public (ModFolder, int) FindOrCreateSubFolder( string name ) - { - var subFolder = new ModFolder( this, name ); - var idx = FindOrAddSubFolder( subFolder ); - return ( SubFolders[ idx ], idx ); - } - - // Remove folder as a subfolder if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveSubFolder( ModFolder folder ) - { - RemoveFolderIgnoreEmpty( folder ); - CheckEmpty(); - } - - // Add the given mod as a child, if it is not already a child. - // Returns the index of the found or inserted mod. - public int AddMod( Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - Mods.Insert( idx, mod ); - - return idx; - } - - // Remove mod as a child if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveMod( Mod mod ) - { - RemoveModIgnoreEmpty( mod ); - CheckEmpty(); - } -} - -// Internals -public partial class ModFolder -{ - // Create a Root folder without parent. - internal static ModFolder CreateRoot() - => new(null!, string.Empty); - - internal class ModFolderComparer : IComparer< ModFolder > - { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. - public int Compare( ModFolder? x, ModFolder? y ) - => ReferenceEquals( x, y ) - ? 0 - : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); - } - - internal class ModDataComparer : IComparer< Mod > - { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. - // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. - public int Compare( Mod? x, Mod? y ) - { - if( ReferenceEquals( x, y ) ) - { - return 0; - } - - var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); - if( cmp != 0 ) - { - return cmp; - } - - return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); - } - } - - internal static readonly ModFolderComparer FolderComparer = new(); - internal static readonly ModDataComparer ModComparer = new(); - - // Get an enumerator for actually sorted objects instead of folder-first objects. - private IEnumerable< object > GetSortedEnumerator() - { - var modIdx = 0; - foreach( var folder in SubFolders ) - { - var folderString = folder.Name; - for( ; modIdx < Mods.Count; ++modIdx ) - { - var mod = Mods[ modIdx ]; - var modString = mod.Order.SortOrderName; - if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) - { - yield return mod; - } - else - { - break; - } - } - - yield return folder; - } - - for( ; modIdx < Mods.Count; ++modIdx ) - { - yield return Mods[ modIdx ]; - } - } - - private void CheckEmpty() - { - if( Mods.Count == 0 && SubFolders.Count == 0 ) - { - Parent?.RemoveSubFolder( this ); - } - } - - // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveFolderIgnoreEmpty( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx < 0 ) - { - return; - } - - SubFolders[ idx ].Parent = null; - SubFolders.RemoveAt( idx ); - } - - // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveModIgnoreEmpty( Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - Mods.RemoveAt( idx ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFunctions.cs b/Penumbra/Mods/ModFunctions.cs deleted file mode 100644 index 72787222..00000000 --- a/Penumbra/Mods/ModFunctions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// Functions that do not really depend on only one component of a mod. -public static class ModFunctions -{ - public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) - { - var hashes = modPaths.Select( p => p.Name ).ToHashSet(); - var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray(); - var anyChanges = false; - foreach( var toRemove in missingMods ) - { - anyChanges |= settings.Remove( toRemove ); - } - - return anyChanges; - } - - public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta ) - { - var doNotAdd = false; - var files = new HashSet< Utf8GamePath >(); - foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) ) - { - doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files ); - } - - if( !doNotAdd ) - { - files.Add( relPath.ToGamePath() ); - } - - return files; - } - - public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta ) - { - var ret = new HashSet< Utf8GamePath >(); - foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) ) - { - if( option.OptionFiles.TryGetValue( relPath, out var files ) ) - { - ret.UnionWith( files ); - } - } - - if( ret.Count == 0 ) - { - ret.Add( relPath.ToGamePath() ); - } - - return ret; - } - - public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) - { - ModSettings ret = new() - { - Priority = namedSettings.Priority, - Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), - }; - - foreach( var setting in namedSettings.Settings.Keys ) - { - if( !meta.Groups.TryGetValue( setting, out var info ) ) - { - continue; - } - - if( info.SelectionType == SelectType.Single ) - { - if( namedSettings.Settings[ setting ].Count == 0 ) - { - ret.Settings[ setting ] = 0; - } - else - { - var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() ); - ret.Settings[ setting ] = idx < 0 ? 0 : idx; - } - } - else - { - foreach( var idx in namedSettings.Settings[ setting ] - .Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) - .Where( idx => idx >= 0 ) ) - { - ret.Settings[ setting ] |= 1 << idx; - } - } - } - - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs deleted file mode 100644 index 1f2f5cb7..00000000 --- a/Penumbra/Mods/ModManager.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Meta; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public enum ChangeType - { - Added, - Removed, - Changed, - } - - // The ModManager handles the basic mods installed to the mod directory. - // It also contains the CollectionManager that handles all collections. - public class Manager : IEnumerable< Mod > - { - public DirectoryInfo BasePath { get; private set; } = null!; - - private readonly List< Mod > _mods = new(); - - public Mod this[ int idx ] - => _mods[ idx ]; - - public IReadOnlyList< Mod > Mods - => _mods; - - public IEnumerator< Mod > GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ModFolder StructuredMods { get; } = ModFileSystem.Root; - - public delegate void ModChangeDelegate( ChangeType type, Mod mod ); - - public event ModChangeDelegate? ModChange; - public event Action? ModDiscoveryStarted; - public event Action? ModDiscoveryFinished; - - public bool Valid { get; private set; } - - public int Count - => _mods.Count; - - public Configuration Config - => Penumbra.Config; - - public void DiscoverMods( string newDir ) - { - SetBaseDirectory( newDir, false ); - DiscoverMods(); - } - - private void SetBaseDirectory( string newPath, bool firstTime ) - { - if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - if( newPath.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - if( !firstTime ) - { - HandleSortOrderFiles( newDir ); - } - - BasePath = newDir; - - Valid = true; - if( Config.ModDirectory != BasePath.FullName ) - { - Config.ModDirectory = BasePath.FullName; - Config.Save(); - } - } - } - - private const string SortOrderFileName = "sort_order.json"; - public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), SortOrderFileName ); - - private void HandleSortOrderFiles( DirectoryInfo newDir ) - { - try - { - var mainFile = SortOrderFile; - // Copy old sort order to backup. - var oldSortOrderFile = Path.Combine( BasePath.FullName, SortOrderFileName ); - PluginLog.Debug( "Copying current sort older file to {BackupFile}...", oldSortOrderFile ); - File.Copy( mainFile, oldSortOrderFile, true ); - BasePath = newDir; - var newSortOrderFile = Path.Combine( newDir.FullName, SortOrderFileName ); - // Copy new sort order to main, if it exists. - if( File.Exists( newSortOrderFile ) ) - { - File.Copy( newSortOrderFile, mainFile, true ); - PluginLog.Debug( "Copying stored sort order file from {BackupFile}...", newSortOrderFile ); - } - else - { - File.Delete( mainFile ); - PluginLog.Debug( "Deleting current sort order file...", newSortOrderFile ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not swap Sort Order files:\n{e}" ); - } - } - - public Manager() - { - SetBaseDirectory( Config.ModDirectory, true ); - // TODO - try - { - var data = JObject.Parse( File.ReadAllText( SortOrderFile ) ); - TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); - } - catch - { - TemporaryModSortOrder = new Dictionary< string, string >(); - } - } - - public Dictionary< string, string > TemporaryModSortOrder; - - private bool SetSortOrderPath( Mod mod, string path ) - { - mod.Move( path ); - var fixedPath = mod.Order.FullPath; - if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - return true; - } - - if( path != fixedPath ) - { - TemporaryModSortOrder[ mod.BasePath.Name ] = fixedPath; - return true; - } - - return false; - } - - private void SetModStructure( bool removeOldPaths = false ) - { - var changes = false; - - foreach( var (folder, path) in TemporaryModSortOrder.ToArray() ) - { - if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) - { - changes |= SetSortOrderPath( mod, path ); - } - else if( removeOldPaths ) - { - changes = true; - TemporaryModSortOrder.Remove( folder ); - } - } - - if( changes ) - { - Config.Save(); - } - } - - public void DiscoverMods() - { - ModDiscoveryStarted?.Invoke(); - _mods.Clear(); - BasePath.Refresh(); - - StructuredMods.SubFolders.Clear(); - StructuredMods.Mods.Clear(); - if( Valid && BasePath.Exists ) - { - foreach( var modFolder in BasePath.EnumerateDirectories() ) - { - var mod = LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - continue; - } - - mod.Index = _mods.Count; - _mods.Add( mod ); - } - - SetModStructure(); - } - - ModDiscoveryFinished?.Invoke(); - } - - public void DeleteMod( DirectoryInfo modFolder ) - { - if( Directory.Exists( modFolder.FullName ) ) - { - try - { - Directory.Delete( modFolder.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); - } - } - - var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); - if( idx >= 0 ) - { - var mod = _mods[ idx ]; - mod.Order.ParentFolder.RemoveMod( mod ); - _mods.RemoveAt( idx ); - for( var i = idx; i < _mods.Count; ++i ) - { - --_mods[ i ].Index; - } - - ModChange?.Invoke( ChangeType.Removed, mod ); - } - } - - public int AddMod( DirectoryInfo modFolder ) - { - var mod = LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - return -1; - } - - if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - if( SetSortOrderPath( mod, sortOrder ) ) - { - Config.Save(); - } - } - - if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) - { - return -1; - } - - _mods.Add( mod ); - ModChange?.Invoke( ChangeType.Added, mod ); - - return _mods.Count - 1; - } - - public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - { - var mod = Mods[ idx ]; - var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force; - var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); - - if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) - { - return false; - } - - if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) - { - mod.ComputeChangedItems(); - if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - mod.Move( sortOrder ); - var path = mod.Order.FullPath; - if( path != sortOrder ) - { - TemporaryModSortOrder[ mod.BasePath.Name ] = path; - Config.Save(); - } - } - else - { - mod.Order = new SortOrder( StructuredMods, mod.Meta.Name ); - } - } - - var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); - - recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); - if( recomputeMeta ) - { - mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); - mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); - } - - // TODO: more specific mod changes? - ModChange?.Invoke( ChangeType.Changed, mod ); - return true; - } - - public bool UpdateMod( Mod mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); - - public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs deleted file mode 100644 index 82869ad9..00000000 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using Dalamud.Logging; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// Extracted to keep the main file a bit more clean. -// Contains all change functions on a specific mod that also require corresponding changes to collections. -public static class ModManagerEditExtensions -{ - public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod ) - { - if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) - { - return false; - } - - mod.Meta.Name = newName; - mod.SaveMeta(); - - return true; - } - - public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder ) - { - if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) - { - return false; - } - - var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); - if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) - { - mod.Order = inRoot; - manager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - } - else - { - mod.Move( newSortOrder ); - manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; - } - - Penumbra.Config.Save(); - - return true; - } - - public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true ) - { - if( move ) - { - newDir.Refresh(); - if( newDir.Exists ) - { - return false; - } - - var oldDir = new DirectoryInfo( mod.BasePath.FullName ); - try - { - oldDir.MoveTo( newDir.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" ); - return false; - } - } - - var oldBasePath = mod.BasePath; - mod.BasePath = newDir; - mod.MetaFile = Mod.MetaFileInfo( newDir ); - manager.UpdateMod( mod ); - - if( manager.TemporaryModSortOrder.ContainsKey( oldBasePath.Name ) ) - { - manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ]; - manager.TemporaryModSortOrder.Remove( oldBasePath.Name ); - Penumbra.Config.Save(); - } - - var idx = manager.Mods.IndexOf( mod ); - foreach( var collection in Penumbra.CollectionManager ) - { - if( collection.Settings[ idx ] != null ) - { - collection.Save(); - } - } - - return true; - } - - public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod, - SelectType type = SelectType.Single ) - { - if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) - { - return false; - } - - if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) ) - { - if( newGroupName.Length > 0 ) - { - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = oldGroup.SelectionType, - Options = oldGroup.Options, - }; - } - - mod.Meta.Groups.Remove( oldGroupName ); - } - else - { - if( newGroupName.Length == 0 ) - { - return false; - } - - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = type, - Options = new List< Option >(), - }; - } - - mod.SaveMeta(); - - // TODO to indices - var idx = Penumbra.ModManager.Mods.IndexOf( mod ); - - foreach( var collection in Penumbra.CollectionManager ) - { - var settings = collection.Settings[ idx ]; - if( settings == null ) - { - continue; - } - - if( newGroupName.Length > 0 ) - { - settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0; - } - - settings.Settings.Remove( oldGroupName ); - collection.Save(); - } - - return true; - } - - public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod ) - { - if( optionIdx < 0 || optionIdx >= group.Options.Count ) - { - return false; - } - - group.Options.RemoveAt( optionIdx ); - mod.SaveMeta(); - - static int MoveMultiSetting( int oldSetting, int idx ) - { - var bitmaskFront = ( 1 << idx ) - 1; - var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) ); - return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); - } - - var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO - foreach( var collection in Penumbra.CollectionManager ) - { - var settings = collection.Settings[ idx ]; - if( settings == null ) - { - continue; - } - - if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) ) - { - setting = 0; - } - - var newSetting = group.SelectionType switch - { - SelectType.Single => setting >= optionIdx ? setting - 1 : setting, - SelectType.Multi => MoveMultiSetting( setting, optionIdx ), - _ => throw new InvalidEnumArgumentException(), - }; - - if( newSetting != setting ) - { - settings.Settings[ group.GroupName ] = newSetting; - collection.Save(); - if( collection.HasCache && settings.Enabled ) - { - collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - Penumbra.CollectionManager.Default == collection ); - } - } - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs deleted file mode 100644 index 253839ce..00000000 --- a/Penumbra/Mods/ModMeta.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.GameData.ByteString; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// Contains descriptive data about the mod as well as possible settings and fileswaps. -public class ModMeta -{ - public const uint CurrentFileVersion = 1; - - [Flags] - public enum ChangeType : byte - { - Name = 0x01, - Author = 0x02, - Description = 0x04, - Version = 0x08, - Website = 0x10, - Deletion = 0x20, - } - - public uint FileVersion { get; set; } = CurrentFileVersion; - public LowerString Name { get; set; } = "Mod"; - public LowerString Author { get; set; } = LowerString.Empty; - public string Description { get; set; } = string.Empty; - public string Version { get; set; } = string.Empty; - public string Website { get; set; } = string.Empty; - - public bool HasGroupsWithConfig = false; - - public bool RefreshHasGroupsWithConfig() - { - var oldValue = HasGroupsWithConfig; - HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); - return oldValue != HasGroupsWithConfig; - } - - public ChangeType RefreshFromFile( FileInfo filePath ) - { - var newMeta = LoadFromFile( filePath ); - if( newMeta == null ) - { - return ChangeType.Deletion; - } - - ChangeType changes = 0; - - if( Name != newMeta.Name ) - { - changes |= ChangeType.Name; - Name = newMeta.Name; - } - - if( Author != newMeta.Author ) - { - changes |= ChangeType.Author; - Author = newMeta.Author; - } - - if( Description != newMeta.Description ) - { - changes |= ChangeType.Description; - Description = newMeta.Description; - } - - if( Version != newMeta.Version ) - { - changes |= ChangeType.Version; - Version = newMeta.Version; - } - - if( Website != newMeta.Website ) - { - changes |= ChangeType.Website; - Website = newMeta.Website; - } - - return changes; - } - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); - - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); - - public static ModMeta? LoadFromFile( FileInfo filePath ) - { - try - { - var text = File.ReadAllText( filePath.FullName ); - - var meta = JsonConvert.DeserializeObject< ModMeta >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - if( meta != null ) - { - meta.RefreshHasGroupsWithConfig(); - Migration.Migrate( meta, text ); - } - - return meta; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod meta:\n{e}" ); - return null; - } - } - - - public void SaveToFile( FileInfo filePath ) - { - try - { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( filePath.FullName, text ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); - } - } - - private static class Migration - { - public static void Migrate( ModMeta meta, string text ) - { - MigrateV0ToV1( meta, text ); - } - - private static void MigrateV0ToV1( ModMeta meta, string text ) - { - if( meta.FileVersion > 0 ) - { - return; - } - - var data = JObject.Parse( text ); - var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() - ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - foreach( var group in groups.Values ) - { } - - foreach( var swap in swaps ) - { } - - //var meta = - } - - - private struct OptionV0 - { - public string OptionName = string.Empty; - public string OptionDesc = string.Empty; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] - public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); - - public OptionV0() - { } - } - - private struct OptionGroupV0 - { - public string GroupName = string.Empty; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType = SelectType.Single; - - public List< OptionV0 > Options = new(); - - public OptionGroupV0() - { } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModResources.cs b/Penumbra/Mods/ModResources.cs deleted file mode 100644 index cd05fc7e..00000000 --- a/Penumbra/Mods/ModResources.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Meta; - -namespace Penumbra.Mods; - -[Flags] -public enum ResourceChange -{ - None = 0, - Files = 1, - Meta = 2, -} - -// Contains static mod data that should only change on filesystem changes. -public class ModResources -{ - public List< FullPath > ModFiles { get; private set; } = new(); - public List< FullPath > MetaFiles { get; private set; } = new(); - - public MetaCollection MetaManipulations { get; private set; } = new(); - - - private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) - { - MetaManipulations.Update( MetaFiles, basePath, meta ); - MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); - } - - public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) - { - var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); - if( newManipulations == null ) - { - ForceManipulationsUpdate( meta, basePath ); - } - else - { - MetaManipulations = newManipulations; - if( validate && !MetaManipulations.Validate( meta ) ) - { - ForceManipulationsUpdate( meta, basePath ); - } - } - } - - // Update the current set of files used by the mod, - // returns true if anything changed. - public ResourceChange RefreshModFiles( DirectoryInfo basePath ) - { - List< FullPath > tmpFiles = new(ModFiles.Count); - List< FullPath > tmpMetas = new(MetaFiles.Count); - // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo - foreach( var file in basePath.EnumerateDirectories() - .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .Select( f => new FullPath( f ) ) - .OrderBy( f => f.FullName ) ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - tmpMetas.Add( file ); - break; - default: - tmpFiles.Add( file ); - break; - } - } - - ResourceChange changes = 0; - if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) - { - ModFiles = tmpFiles; - changes |= ResourceChange.Files; - } - - if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) - { - MetaFiles = tmpMetas; - changes |= ResourceChange.Meta; - } - - return changes; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/NamedModSettings.cs b/Penumbra/Mods/NamedModSettings.cs deleted file mode 100644 index 5a0ded71..00000000 --- a/Penumbra/Mods/NamedModSettings.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.Mods; - -// Contains settings with the option selections stored by names instead of index. -// This is meant to make them possibly more portable when we support importing collections from other users. -// Enabled does not exist, because disabled mods would not be exported in this way. -public class NamedModSettings -{ - public int Priority { get; set; } - public Dictionary< string, HashSet< string > > Settings { get; set; } = new(); - - public void AddFromModSetting( ModSettings s, ModMeta meta ) - { - Priority = s.Priority; - Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() ); - - foreach( var kvp in Settings ) - { - if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) - { - continue; - } - - var setting = s.Settings[ kvp.Key ]; - if( info.SelectionType == SelectType.Single ) - { - var name = setting < info.Options.Count - ? info.Options[ setting ].OptionName - : info.Options[ 0 ].OptionName; - kvp.Value.Add( name ); - } - else - { - for( var i = 0; i < info.Options.Count; ++i ) - { - if( ( ( setting >> i ) & 1 ) != 0 ) - { - kvp.Value.Add( info.Options[ i ].OptionName ); - } - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs index 766c59da..ccbf2df8 100644 --- a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs @@ -6,6 +6,7 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; +using Penumbra.Importer; using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; @@ -113,5 +114,82 @@ public partial class Mod2 } } } + + public void IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + { + foreach( var (key, file) in Files.ToList() ) + { + try + { + switch( file.Extension ) + { + case ".meta": + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } + + foreach( var manip in meta.EqpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EqdpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EstManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.GmpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.ImcManipulations ) + { + ManipulationData.Add( manip ); + } + + break; + case ".rgsp": + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } + + foreach( var manip in rgsp.RspManipulations ) + { + ManipulationData.Add( manip ); + } + + break; + default: continue; + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + continue; + } + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index d32c6d70..d40f578b 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -4,72 +4,140 @@ using System.Linq; namespace Penumbra.Mods; + // Contains the settings for a given mod. -public class ModSettings +public class ModSettings2 { - public static readonly ModSettings Empty = new(); - - public bool Enabled { get; set; } + public static readonly ModSettings2 Empty = new(); + public List< uint > Settings { get; init; } = new(); public int Priority { get; set; } - public Dictionary< string, int > Settings { get; set; } = new(); + public bool Enabled { get; set; } - // For backwards compatibility - private Dictionary< string, int > Conf - { - set => Settings = value; - } - - public ModSettings DeepCopy() - { - var settings = new ModSettings + public ModSettings2 DeepCopy() + => new() { Enabled = Enabled, Priority = Priority, - Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + Settings = Settings.ToList(), }; - return settings; - } - public static ModSettings DefaultSettings( ModMeta meta ) - { - return new ModSettings + public static ModSettings2 DefaultSettings( Mod2 mod ) + => new() { Enabled = false, Priority = 0, - Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ), + Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(), + }; + + + + public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + { + switch( type ) + { + case ModOptionChangeType.GroupAdded: + Settings.Insert( groupIdx, 0 ); + break; + case ModOptionChangeType.GroupDeleted: + Settings.RemoveAt( groupIdx ); + break; + case ModOptionChangeType.OptionDeleted: + var group = mod.Groups[ groupIdx ]; + var config = Settings[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => config >= optionIdx ? Math.Max( 0, config - 1 ) : config, + SelectType.Multi => RemoveBit( config, optionIdx ), + _ => config, + }; + break; + } + } + + public void SetValue( Mod2 mod, int groupIdx, uint newValue ) + { + AddMissingSettings( groupIdx + 1 ); + var group = mod.Groups[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => ( uint )Math.Max( newValue, group.Count ), + SelectType.Multi => ( ( 1u << group.Count ) - 1 ) & newValue, + _ => newValue, }; } - public bool FixSpecificSetting( string name, ModMeta meta ) + private static uint RemoveBit( uint config, int bit ) { - if( !meta.Groups.TryGetValue( name, out var group ) ) - { - return Settings.Remove( name ); - } - - if( Settings.TryGetValue( name, out var oldSetting ) ) - { - Settings[ name ] = group.SelectionType switch - { - SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), - SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), - _ => Settings[ group.GroupName ], - }; - return oldSetting != Settings[ group.GroupName ]; - } - - Settings[ name ] = 0; - return true; + var lowMask = ( 1u << bit ) - 1u; + var highMask = ~( ( 1u << ( bit + 1 ) ) - 1u ); + var low = config & lowMask; + var high = ( config & highMask ) >> 1; + return low | high; } - public bool FixInvalidSettings( ModMeta meta ) + internal bool AddMissingSettings( int totalCount ) { - if( meta.Groups.Count == 0 ) + if( totalCount <= Settings.Count ) { return false; } - return Settings.Keys.ToArray().Union( meta.Groups.Keys ) - .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) ); + Settings.AddRange( Enumerable.Repeat( 0u, totalCount - Settings.Count ) ); + return true; + } + + public struct SavedSettings + { + public Dictionary< string, uint > Settings; + public int Priority; + public bool Enabled; + + public SavedSettings DeepCopy() + => new() + { + Enabled = Enabled, + Priority = Priority, + Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + }; + + public SavedSettings( ModSettings2 settings, Mod2 mod ) + { + Priority = settings.Priority; + Enabled = settings.Enabled; + Settings = new Dictionary< string, uint >( mod.Groups.Count ); + settings.AddMissingSettings( mod.Groups.Count ); + + foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) + { + Settings.Add( group.Name, setting ); + } + } + + public bool ToSettings( Mod2 mod, out ModSettings2 settings ) + { + var list = new List< uint >( mod.Groups.Count ); + var changes = Settings.Count != mod.Groups.Count; + foreach( var group in mod.Groups ) + { + if( Settings.TryGetValue( group.Name, out var config ) ) + { + list.Add( config ); + } + else + { + list.Add( 0 ); + changes = true; + } + } + + settings = new ModSettings2 + { + Enabled = Enabled, + Priority = Priority, + Settings = list, + }; + + return changes; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/SelectType.cs b/Penumbra/Mods/Subclasses/SelectType.cs new file mode 100644 index 00000000..0843729c --- /dev/null +++ b/Penumbra/Mods/Subclasses/SelectType.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Mods; + +public enum SelectType +{ + Single, + Multi, +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 37ae5260..9bdb5bd4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using EmbedIO; @@ -36,20 +37,22 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - - public static Mod.Manager ModManager { get; private set; } = null!; + public static Mod2.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static SimpleRedirectManager Redirects { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; set; } = null!; - public ResourceLogger ResourceLogger { get; } - public PathResolver PathResolver { get; } - public SettingsInterface SettingsInterface { get; } - public MusicManager MusicManager { get; } - public ObjectReloader ObjectReloader { get; } - - public PenumbraApi Api { get; } - public PenumbraIpc Ipc { get; } + public readonly ResourceLogger ResourceLogger; + public readonly PathResolver PathResolver; + public readonly MusicManager MusicManager; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystemA ModFileSystem; + public readonly PenumbraApi Api; + public readonly PenumbraIpc Ipc; + private readonly ConfigWindow _configWindow; + private readonly LaunchButton _launchButton; + private readonly WindowSystem _windowSystem; private WebServer? _webServer; @@ -68,12 +71,14 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); + Redirects = new SimpleRedirectManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod.Manager(); + ModManager = new Mod2.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); + ModFileSystem = ModFileSystemA.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -92,8 +97,7 @@ public class Penumbra : IDalamudPlugin Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( pluginInterface, Api ); SubscribeItemLinks(); - - SettingsInterface = new SettingsInterface( this ); + SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); if( Config.EnableHttpApi ) { @@ -129,6 +133,24 @@ public class Penumbra : IDalamudPlugin } } + private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) + { + cfg = new ConfigWindow( this ); + btn = new LaunchButton( _configWindow ); + system = new WindowSystem( Name ); + system.AddWindow( _configWindow ); + Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; + } + + private void DisposeInterface() + { + Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; + _launchButton.Dispose(); + _configWindow.Dispose(); + } + public bool Enable() { if( Config.EnableMods ) @@ -209,10 +231,11 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + DisposeInterface(); Ipc.Dispose(); Api.Dispose(); - SettingsInterface.Dispose(); ObjectReloader.Dispose(); + ModFileSystem.Dispose(); CollectionManager.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); @@ -251,7 +274,6 @@ public class Penumbra : IDalamudPlugin CollectionManager.SetCollection( collection, ModCollection.Type.Default ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); - SettingsInterface.ResetDefaultCollection(); return true; default: Dalamud.Chat.Print( @@ -293,7 +315,7 @@ public class Penumbra : IDalamudPlugin } case "debug": { - SettingsInterface.MakeDebugTabVisible(); + // TODO break; } case "enable": @@ -341,7 +363,7 @@ public class Penumbra : IDalamudPlugin return; } - SettingsInterface.FlipVisibility(); + _configWindow.Toggle(); } // Collect all relevant files for penumbra configuration. @@ -349,7 +371,7 @@ public class Penumbra : IDalamudPlugin { var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); list.Add( Dalamud.PluginInterface.ConfigFile ); - list.Add( new FileInfo( Mod.Manager.SortOrderFile ) ); + list.Add( new FileInfo( Mod2.Manager.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); return list; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index a8a7591b..707b9f9e 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -30,6 +30,12 @@ $(MSBuildWarningsAsMessages);MSB3277 + + + + + + PreserveNewest diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 9c95a9bc..f51c99fb 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -19,6 +19,8 @@ public enum ColorId public static class Colors { + public const uint PressEnterWarningBg = 0xFF202080; + public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch { diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 13c797d2..6caf5bd8 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -20,11 +20,11 @@ public partial class ModFileSystemSelector public uint Color; } - private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; - private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >(); - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private readonly IReadOnlySet< Mod2 > _newMods = new HashSet< Mod2 >(); + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; private void SetFilterTooltip() { @@ -75,7 +75,7 @@ public partial class ModFileSystemSelector // Folders have default state and are filtered out on the direct string instead of the other options. // If any filter is set, they should be hidden by default unless their children are visible, // or they contain the path search string. - protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) + protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state ) { if( path is ModFileSystemA.Folder f ) { @@ -88,21 +88,21 @@ public partial class ModFileSystemSelector } // Apply the string filters. - private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod mod ) + private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod2 mod ) { return _filterType switch { -1 => false, - 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Meta.Name.Contains( _modFilter ) ), - 1 => !mod.Meta.Name.Contains( _modFilter ), - 2 => !mod.Meta.Author.Contains( _modFilter ), + 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), + 1 => !mod.Name.Contains( _modFilter ), + 2 => !mod.Author.Contains( _modFilter ), 3 => !mod.LowerChangedItemsString.Contains( _modFilter ), _ => false, // Should never happen }; } // Only get the text color for a mod if no filters are set. - private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) + private uint GetTextColor( Mod2 mod, ModSettings2? settings, ModCollection collection ) { if( _newMods.Contains( mod ) ) { @@ -130,14 +130,14 @@ public partial class ModFileSystemSelector : ColorId.HandledConflictMod.Value(); } - private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) + private bool CheckStateFilters( Mod2 mod, ModSettings2? settings, ModCollection collection, ref ModState state ) { var isNew = _newMods.Contains( mod ); // Handle mod details. - if( CheckFlags( mod.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) - || CheckFlags( mod.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - || CheckFlags( mod.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - || CheckFlags( mod.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) + if( CheckFlags( mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles ) + || CheckFlags( mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) + || CheckFlags( mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) + || CheckFlags( mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) || CheckFlags( isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew ) ) { return true; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index a34b3bd6..57ec835f 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -12,12 +12,12 @@ using Penumbra.Mods; namespace Penumbra.UI.Classes; -public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > +public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, ModFileSystemSelector.ModState > { - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet newMods ) + public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet< Mod2 > newMods ) : base( fileSystem ) { _newMods = newMods; @@ -29,13 +29,19 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; + Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; + Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; + Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); } public override void Dispose() { base.Dispose(); + Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; + Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; @@ -54,11 +60,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override uint FolderLineColor => ColorId.FolderLine.Value(); - protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + protected override void DrawLeafName( FileSystem< Mod2 >.Leaf leaf, in ModState state, bool selected ) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); - using var _ = ImRaii.TreeNode( leaf.Value.Meta.Name, flags ); + using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); } @@ -126,7 +132,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } // Automatic cache update functions. - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ) + private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) { // TODO: maybe make more efficient SetFilterDirty(); @@ -169,13 +175,32 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { if( newSelection == null ) { - SelectedSettings = ModSettings.Empty; + SelectedSettings = ModSettings2.Empty; SelectedSettingCollection = ModCollection.Empty; } else { ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Value.Index ]; - SelectedSettings = settings ?? ModSettings.Empty; + SelectedSettings = settings ?? ModSettings2.Empty; + } + } + + // Keep selections across rediscoveries if possible. + private string _lastSelectedDirectory = string.Empty; + + private void StoreCurrentSelection() + { + _lastSelectedDirectory = Selected?.BasePath.FullName ?? string.Empty; + ClearSelection(); + } + + private void RestoreLastSelection() + { + if( _lastSelectedDirectory.Length > 0 ) + { + SelectedLeaf = ( ModFileSystemA.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) + .FirstOrDefault( l => l is ModFileSystemA.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); + _lastSelectedDirectory = string.Empty; } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs new file mode 100644 index 00000000..eeacb28e --- /dev/null +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -0,0 +1,22 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawCollectionsTab() + { + using var tab = ImRaii.TabItem( "Collections" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##CollectionsTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs new file mode 100644 index 00000000..a76969b0 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -0,0 +1,35 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ +#if DEBUG + private const bool DefaultVisibility = true; +#else + private const bool DefaultVisibility = false; +#endif + + public bool DebugTabVisible = DefaultVisibility; + + public void DrawDebugTab() + { + if( !DebugTabVisible ) + { + return; + } + + using var tab = ImRaii.TabItem( "Debug" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##DebugTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs new file mode 100644 index 00000000..db44fc62 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawEffectiveChangesTab() + { + if( !Penumbra.Config.ShowAdvanced ) + { + return; + } + + using var tab = ImRaii.TabItem( "Effective Changes" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/ConfigWindow.Misc.cs similarity index 86% rename from Penumbra/UI/UiHelpers.cs rename to Penumbra/UI/ConfigWindow.Misc.cs index 3d730f57..d4608546 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -2,13 +2,19 @@ using System.Numerics; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.UI.Custom; namespace Penumbra.UI; -public partial class SettingsInterface +public partial class ConfigWindow { + internal static unsafe void Text( Utf8String s ) + { + ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); + } + internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs new file mode 100644 index 00000000..8b5bc12e --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -0,0 +1,22 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawModsTab() + { + using var tab = ImRaii.TabItem( "Mods" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##ModsTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs new file mode 100644 index 00000000..db1e1d67 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawResourceManagerTab() + { + if( !DebugTabVisible ) + { + return; + } + + using var tab = ImRaii.TabItem( "Resource Manager" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs new file mode 100644 index 00000000..224adccf --- /dev/null +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -0,0 +1,317 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private string _newModDirectory = string.Empty; + + private static bool DrawPressEnterWarning( string old ) + { + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var w = new Vector2( ImGui.CalcItemWidth(), 0 ); + return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); + } + + private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) + { + using var _ = ImRaii.PushId( id ); + var ret = ImGui.Button( "Open Directory" ); + ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); + if( ret && condition && Directory.Exists( directory.FullName ) ) + { + Process.Start( new ProcessStartInfo( directory.FullName ) + { + UseShellExecute = true, + } ); + } + } + + private void DrawRootFolder() + { + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth( _inputTextWidth.X ); + var save = ImGui.InputText( "Root Directory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); + ImGui.SameLine(); + DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); + group.Dispose(); + + if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 ) + { + return; + } + + if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory ) ) + { + Penumbra.ModManager.DiscoverMods( _newModDirectory ); + } + } + + + private void DrawRediscoverButton() + { + if( ImGui.Button( "Rediscover Mods" ) ) + { + Penumbra.ModManager.DiscoverMods(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); + } + + private void DrawEnabledBox() + { + var enabled = Penumbra.Config.EnableMods; + if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) + { + _penumbra.SetEnabled( enabled ); + } + } + + private void DrawShowAdvancedBox() + { + var showAdvanced = Penumbra.Config.ShowAdvanced; + if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) + { + Penumbra.Config.ShowAdvanced = showAdvanced; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Enable some advanced options in this window and in the mod selector.\n" + + "This is required to enable manually editing any mod information." ); + } + + private void DrawFolderSortType() + { + // TODO provide all options + var foldersFirst = Penumbra.Config.SortFoldersFirst; + if( ImGui.Checkbox( "Sort Mod-Folders Before Mods", ref foldersFirst ) ) + { + Penumbra.Config.SortFoldersFirst = foldersFirst; + Selector.SetFilterDirty(); + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Prioritizes all mod-folders in the mod-selector in the Installed Mods tab so that folders come before single mods, instead of being sorted completely alphabetically" ); + } + + private void DrawScaleModSelectorBox() + { + // TODO set scale + var scaleModSelector = Penumbra.Config.ScaleModSelector; + if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + { + Penumbra.Config.ScaleModSelector = scaleModSelector; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); + } + + private void DrawDisableSoundStreamingBox() + { + var tmp = Penumbra.Config.DisableSoundStreaming; + if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) + { + Penumbra.Config.DisableSoundStreaming = tmp; + Penumbra.Config.Save(); + if( tmp ) + { + _penumbra.MusicManager.DisableStreaming(); + } + else + { + _penumbra.MusicManager.EnableStreaming(); + } + + Penumbra.ModManager.DiscoverMods(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Disable streaming in the games audio engine.\n" + + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems.\n" + + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." ); + } + + + private void DrawEnableHttpApiBox() + { + var http = Penumbra.Config.EnableHttpApi; + if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) + { + if( http ) + { + _penumbra.CreateWebServer(); + } + else + { + _penumbra.ShutdownWebServer(); + } + + Penumbra.Config.EnableHttpApi = http; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); + } + + private static void DrawReloadResourceButton() + { + if( ImGui.Button( "Reload Resident Resources" ) ) + { + Penumbra.ResidentResources.Reload(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Reload some specific files that the game keeps in memory at all times.\n" + + "You usually should not need to do this." ); + } + + private void DrawEnableFullResourceLoggingBox() + { + var tmp = Penumbra.Config.EnableFullResourceLogging; + if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableFullLogging(); + } + else + { + Penumbra.ResourceLoader.DisableFullLogging(); + } + + Penumbra.Config.EnableFullResourceLogging = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); + } + + private void DrawEnableDebugModeBox() + { + var tmp = Penumbra.Config.DebugMode; + if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableDebug(); + } + else + { + Penumbra.ResourceLoader.DisableDebug(); + } + + Penumbra.Config.DebugMode = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." ); + } + + private void DrawRequestedResourceLogging() + { + var tmp = Penumbra.Config.EnableResourceLogging; + if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) ) + { + _penumbra.ResourceLogger.SetState( tmp ); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n" + + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" + + "Red boundary indicates invalid regex." ); + ImGui.SameLine(); + var tmpString = Penumbra.Config.ResourceLoggingFilter; + using var color = ImRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_penumbra.ResourceLogger.ValidRegex ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_penumbra.ResourceLogger.ValidRegex ); + if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) + { + _penumbra.ResourceLogger.SetFilter( tmpString ); + } + } + + private void DrawAdvancedSettings() + { + DrawRequestedResourceLogging(); + DrawDisableSoundStreamingBox(); + DrawEnableHttpApiBox(); + DrawReloadResourceButton(); + DrawEnableDebugModeBox(); + DrawEnableFullResourceLoggingBox(); + } + + public void DrawSettingsTab() + { + using var tab = ImRaii.TabItem( "Settings" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); + if( !child ) + { + return; + } + + DrawRootFolder(); + + DrawRediscoverButton(); + + ImGui.Dummy( _verticalSpace ); + DrawEnabledBox(); + + ImGui.Dummy( _verticalSpace ); + DrawFolderSortType(); + DrawScaleModSelectorBox(); + DrawShowAdvancedBox(); + + if( Penumbra.Config.ShowAdvanced ) + { + DrawAdvancedSettings(); + } + + if( ImGui.CollapsingHeader( "Colors" ) ) + { + foreach( var color in Enum.GetValues< ColorId >() ) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; + if( ImGuiUtil.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) + { + Penumbra.Config.Save(); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs new file mode 100644 index 00000000..58250e6b --- /dev/null +++ b/Penumbra/UI/ConfigWindow.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public sealed partial class ConfigWindow : Window, IDisposable +{ + private readonly Penumbra _penumbra; + public readonly ModFileSystemSelector Selector; + + public ConfigWindow( Penumbra penumbra ) + : base( GetLabel() ) + { + _penumbra = penumbra; + Selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = true; + RespectCloseHotkey = true; + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2( 1024, 768 ), + MaximumSize = new Vector2( 4096, 2160 ), + }; + } + + public override void Draw() + { + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); + SetupSizes(); + DrawSettingsTab(); + DrawModsTab(); + DrawCollectionsTab(); + DrawEffectiveChangesTab(); + DrawDebugTab(); + DrawResourceManagerTab(); + } + + public void Dispose() + { + Selector.Dispose(); + } + + private static string GetLabel() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; + return version.Length == 0 + ? "Penumbra###PenumbraConfigWindow" + : $"Penumbra v{version}###PenumbraConfigWindow"; + } + + private Vector2 _verticalSpace; + private Vector2 _inputTextWidth; + + private void SetupSizes() + { + _verticalSpace = new Vector2( 0, 20f * ImGuiHelpers.GlobalScale ); + _inputTextWidth = new Vector2( 450f * ImGuiHelpers.GlobalScale, 0 ); + } +} \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index f78c9e89..0033916f 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -5,38 +5,33 @@ using ImGuiScene; namespace Penumbra.UI; -public partial class SettingsInterface +public class LaunchButton : IDisposable { - private class ManageModsButton : IDisposable + private readonly ConfigWindow _configWindow; + private readonly TextureWrap? _icon; + private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; + + public LaunchButton( ConfigWindow ui ) { - private readonly SettingsInterface _base; - private readonly TextureWrap? _icon; - private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; + _configWindow = ui; - public ManageModsButton( SettingsInterface ui ) + _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + "tsmLogo.png" ) ); + if( _icon != null ) { - _base = ui; - - _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, - "tsmLogo.png" ) ); - if( _icon != null ) - { - _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - } + _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); } + } - private void OnTriggered() - { - _base.FlipVisibility(); - } + private void OnTriggered() + => _configWindow.Toggle(); - public void Dispose() + public void Dispose() + { + _icon?.Dispose(); + if( _entry != null ) { - _icon?.Dispose(); - if( _entry != null ) - { - Dalamud.TitleScreenMenu.RemoveEntry( _entry ); - } + Dalamud.TitleScreenMenu.RemoveEntry( _entry ); } } } \ No newline at end of file diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs deleted file mode 100644 index b406da71..00000000 --- a/Penumbra/UI/SettingsInterface.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Numerics; - -namespace Penumbra.UI; - -public partial class SettingsInterface : IDisposable -{ - private const float DefaultVerticalSpace = 20f; - - private static readonly Vector2 AutoFillSize = new(-1, -1); - private static readonly Vector2 ZeroVector = new(0, 0); - - private readonly Penumbra _penumbra; - - private readonly ManageModsButton _manageModsButton; - private readonly SettingsMenu _menu; - - public SettingsInterface( Penumbra penumbra ) - { - _penumbra = penumbra; - _manageModsButton = new ManageModsButton( this ); - _menu = new SettingsMenu( this ); - - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; - Dalamud.PluginInterface.UiBuilder.Draw += Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi += OpenConfig; - } - - public void Dispose() - { - _manageModsButton.Dispose(); - _menu.InstalledTab.Selector.Cache.Dispose(); - Dalamud.PluginInterface.UiBuilder.Draw -= Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfig; - } - - private void OpenConfig() - => _menu.Visible = true; - - public void FlipVisibility() - => _menu.Visible = !_menu.Visible; - - public void MakeDebugTabVisible() - => _menu.DebugTabVisible = true; - - public void Draw() - { - _menu.Draw(); - } - - private void ReloadMods() - { - _menu.InstalledTab.Selector.ClearSelection(); - Penumbra.ModManager.DiscoverMods( Penumbra.Config.ModDirectory ); - _menu.InstalledTab.Selector.Cache.TriggerListReset(); - } - - public void ResetDefaultCollection() - => _menu.CollectionsTab.UpdateDefaultIndex(); -} \ No newline at end of file diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs deleted file mode 100644 index cee41088..00000000 --- a/Penumbra/UI/SettingsMenu.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class SettingsMenu - { - public static float InputTextWidth - => 450 * ImGuiHelpers.GlobalScale; - - private const string PenumbraSettingsLabel = "PenumbraSettings"; - - public static readonly Vector2 MinSettingsSize = new(800, 450); - public static readonly Vector2 MaxSettingsSize = new(69420, 42069); - - private readonly SettingsInterface _base; - private readonly TabSettings _settingsTab; - private readonly TabImport _importTab; - private readonly TabBrowser _browserTab; - private readonly TabEffective _effectiveTab; - private readonly TabChangedItems _changedItems; - internal readonly TabCollections CollectionsTab; - internal readonly TabInstalled InstalledTab; - - public SettingsMenu( SettingsInterface ui ) - { - _base = ui; - _settingsTab = new TabSettings( _base ); - _importTab = new TabImport( _base ); - _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base, _importTab.NewMods ); - CollectionsTab = new TabCollections( InstalledTab.Selector ); - _effectiveTab = new TabEffective(); - _changedItems = new TabChangedItems( _base ); - } - -#if DEBUG - private const bool DefaultVisibility = true; -#else - private const bool DefaultVisibility = false; -#endif - public bool Visible = DefaultVisibility; - public bool DebugTabVisible = DefaultVisibility; - - public void Draw() - { - if( !Visible ) - { - return; - } - - ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); -#if DEBUG - var ret = ImGui.Begin( _base._penumbra.PluginDebugTitleStr, ref Visible ); -#else - var ret = ImGui.Begin( _base._penumbra.Name, ref Visible ); -#endif - using var raii = ImGuiRaii.DeferredEnd( ImGui.End ); - if( !ret ) - { - return; - } - - ImGui.BeginTabBar( PenumbraSettingsLabel ); - raii.Push( ImGui.EndTabBar ); - - _settingsTab.Draw(); - CollectionsTab.Draw(); - _importTab.Draw(); - - if( Penumbra.ModManager.Valid && !_importTab.IsImporting() ) - { - _browserTab.Draw(); - InstalledTab.Draw(); - _changedItems.Draw(); - if( Penumbra.Config.ShowAdvanced ) - { - _effectiveTab.Draw(); - } - } - - if( DebugTabVisible ) - { - _base.DrawDebugTab(); - _base.DrawResourceManagerTab(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index fd9c4eb3..2ff2b37d 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -74,11 +74,11 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( Mods.Mod mod, string from, string to ) + public static bool ChangeModMaterials( Mod2 mod, string from, string to ) { if( ValidStrings( from, to ) ) { - return mod.Resources.ModFiles + return mod.AllFiles .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) .All( file => ChangeMtrl( file, from, to ) >= 0 ); } From f3b906007dd90069e2e8ba4a335f9072b389509d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Apr 2022 22:02:05 +0200 Subject: [PATCH 0136/2451] Blep --- Penumbra/Importer/TexToolsMeta.cs | 12 +- Penumbra/Mods/Mod2.Meta.Migration.cs | 5 +- Penumbra/Penumbra.csproj | 168 +++++++++++++-------------- 3 files changed, 96 insertions(+), 89 deletions(-) diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index e39b06ac..1afd8f0f 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -275,17 +275,21 @@ public class TexToolsMeta { var def = new ImcFile( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, new ImcEntry() ).GamePath() ); - foreach( var value in values.Where( v => true || !v.Equals( def.GetEntry( 0, i ) ) ) ) + foreach( var value in values ) { - ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, - value ) ); + if( !value.Equals( def.GetEntry( 0, i ) ) ) + { + ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, + value ) ); + } + ++i; } } } catch( Exception e ) { - PluginLog.Error( "Could not compute IMC manipulation. This is in all likelihood due to TexTools corrupting your index files.\n" + PluginLog.Warning( $"Could not compute IMC manipulation for {info.PrimaryType} {info.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); } } diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index 9623487e..a675c660 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -38,7 +38,10 @@ public sealed partial class Mod2 { if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) ) { - mod._default.FileData.Add( gamePath, unusedFile ); + if( !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) + { + PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); + } } } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 707b9f9e..0ce77a6e 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,89 +1,89 @@ - - - net5.0-windows - preview - x64 - Penumbra - absolute gangstas - Penumbra - Copyright © 2020 - 1.0.0.0 - 1.0.0.0 - bin\$(Configuration)\ - true - enable - true - true - - - - full - DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - - - pdbonly - $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - - - $(MSBuildWarningsAsMessages);MSB3277 - - + + + net5.0-windows + preview + x64 + Penumbra + absolute gangstas + Penumbra + Copyright © 2020 + 1.0.0.0 + 1.0.0.0 + bin\$(Configuration)\ + true + enable + true + true + + + + full + DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC + + + + pdbonly + $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC + + + + $(MSBuildWarningsAsMessages);MSB3277 + + - - - - - PreserveNewest - - - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll - False - - - - - - - - - - - - - - - - - - Always - - + + + + + PreserveNewest + + + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll + False + + + + + + + + + + + + + + + + + + Always + + \ No newline at end of file From 65bd1d1b529e21093d81925304f2bddb5c9e0870 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Apr 2022 16:14:13 +0200 Subject: [PATCH 0137/2451] Almost there... --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 6 +- Penumbra/Configuration.Constants.cs | 16 + Penumbra/Configuration.cs | 30 +- Penumbra/Dalamud.cs | 2 +- .../Interop/Loader/ResourceLoader.Debug.cs | 2 + Penumbra/Interop/Structs/Material.cs | 23 + Penumbra/Interop/Structs/RenderModel.cs | 41 + Penumbra/MigrateConfiguration.cs | 331 ++++---- Penumbra/Mods/Manager/Mod2.Manager.Root.cs | 21 +- Penumbra/Mods/Mod2.Meta.Migration.cs | 8 +- .../{ModFileSystemA.cs => ModFileSystem.cs} | 6 +- Penumbra/Penumbra.cs | 31 +- Penumbra/Penumbra.csproj | 180 ++-- Penumbra/UI/Classes/Colors.cs | 3 + .../Classes/ModFileSystemSelector.Filters.cs | 8 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 31 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 54 ++ Penumbra/UI/ConfigWindow.CollectionsTab.cs | 204 ++++- Penumbra/UI/ConfigWindow.DebugTab.cs | 369 +++++++- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 190 ++++- Penumbra/UI/ConfigWindow.Misc.cs | 74 +- Penumbra/UI/ConfigWindow.ModsTab.Details.cs | 714 ++++++++++++++++ .../UI/ConfigWindow.ModsTab.DetailsEdit.cs | 381 +++++++++ ...onfigWindow.ModsTab.DetailsManipulation.cs | 773 +++++++++++++++++ Penumbra/UI/ConfigWindow.ModsTab.Import.cs | 177 ++++ ...Panel.cs => ConfigWindow.ModsTab.Panel.cs} | 0 Penumbra/UI/ConfigWindow.ModsTab.Selector.cs | 797 ++++++++++++++++++ Penumbra/UI/ConfigWindow.ModsTab.cs | 84 +- Penumbra/UI/ConfigWindow.ResourceTab.cs | 163 ++++ .../UI/ConfigWindow.SettingsTab.Advanced.cs | 169 ++++ .../ConfigWindow.SettingsTab.ModSelector.cs | 91 ++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 281 ++---- Penumbra/UI/ConfigWindow.cs | 14 +- Penumbra/UI/Custom/ImGuiFramedGroup.cs | 146 ---- Penumbra/UI/Custom/ImGuiRenameableCombo.cs | 54 -- Penumbra/UI/Custom/ImGuiResizingTextInput.cs | 49 -- Penumbra/UI/Custom/ImGuiUtil.cs | 88 -- Penumbra/UI/Custom/Raii/Color.cs | 52 -- Penumbra/UI/Custom/Raii/EndStack.cs | 40 - Penumbra/UI/Custom/Raii/Font.cs | 39 - Penumbra/UI/Custom/Raii/Indent.cs | 72 -- Penumbra/UI/Custom/Raii/Style.cs | 92 -- Penumbra/UI/LaunchButton.cs | 2 + Penumbra/UI/MenuTabs/TabBrowser.cs | 39 - Penumbra/UI/MenuTabs/TabChangedItems.cs | 60 -- Penumbra/UI/MenuTabs/TabCollections.cs | 420 --------- Penumbra/UI/MenuTabs/TabDebug.Model.cs | 131 --- Penumbra/UI/MenuTabs/TabDebug.cs | 352 -------- Penumbra/UI/MenuTabs/TabEffective.cs | 229 ----- Penumbra/UI/MenuTabs/TabImport.cs | 190 ----- .../UI/MenuTabs/TabInstalled/ModListCache.cs | 326 ------- .../UI/MenuTabs/TabInstalled/TabInstalled.cs | 37 - .../TabInstalled/TabInstalledDetails.cs | 713 ---------------- .../TabInstalled/TabInstalledDetailsEdit.cs | 380 --------- .../TabInstalledDetailsManipulations.cs | 773 ----------------- .../TabInstalled/TabInstalledSelector.cs | 797 ------------------ Penumbra/UI/MenuTabs/TabResourceManager.cs | 189 ----- Penumbra/UI/MenuTabs/TabSettings.cs | 381 --------- 59 files changed, 4733 insertions(+), 6194 deletions(-) create mode 100644 Penumbra/Configuration.Constants.cs create mode 100644 Penumbra/Interop/Structs/Material.cs create mode 100644 Penumbra/Interop/Structs/RenderModel.cs rename Penumbra/Mods/{ModFileSystemA.cs => ModFileSystem.cs} (92%) create mode 100644 Penumbra/UI/ConfigWindow.ChangedItemsTab.cs create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Details.cs create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Import.cs rename Penumbra/UI/{MenuTabs/TabInstalled/TabInstalledModPanel.cs => ConfigWindow.ModsTab.Panel.cs} (100%) create mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Selector.cs create mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs create mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs delete mode 100644 Penumbra/UI/Custom/ImGuiFramedGroup.cs delete mode 100644 Penumbra/UI/Custom/ImGuiRenameableCombo.cs delete mode 100644 Penumbra/UI/Custom/ImGuiResizingTextInput.cs delete mode 100644 Penumbra/UI/Custom/ImGuiUtil.cs delete mode 100644 Penumbra/UI/Custom/Raii/Color.cs delete mode 100644 Penumbra/UI/Custom/Raii/EndStack.cs delete mode 100644 Penumbra/UI/Custom/Raii/Font.cs delete mode 100644 Penumbra/UI/Custom/Raii/Indent.cs delete mode 100644 Penumbra/UI/Custom/Raii/Style.cs delete mode 100644 Penumbra/UI/MenuTabs/TabBrowser.cs delete mode 100644 Penumbra/UI/MenuTabs/TabChangedItems.cs delete mode 100644 Penumbra/UI/MenuTabs/TabCollections.cs delete mode 100644 Penumbra/UI/MenuTabs/TabDebug.Model.cs delete mode 100644 Penumbra/UI/MenuTabs/TabDebug.cs delete mode 100644 Penumbra/UI/MenuTabs/TabEffective.cs delete mode 100644 Penumbra/UI/MenuTabs/TabImport.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs delete mode 100644 Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs delete mode 100644 Penumbra/UI/MenuTabs/TabResourceManager.cs delete mode 100644 Penumbra/UI/MenuTabs/TabSettings.cs diff --git a/OtterGui b/OtterGui index 05619f96..4cc10240 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 05619f966e6acfe8b0b6e947243c5d930c7525a4 +Subproject commit 4cc1024096905b0b20c1559c534b1dd3fe7b25ad diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 3a7335cc..30fd44fb 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -56,6 +56,9 @@ public partial class ModCollection IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerable< ModCollection > GetEnumeratorWithEmpty() + => _collections; + public Manager( Mod2.Manager manager ) { _modManager = manager; @@ -227,7 +230,8 @@ public partial class ModCollection case ModOptionChangeType.OptionAdded: case ModOptionChangeType.OptionDeleted: case ModOptionChangeType.OptionChanged: - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: + throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } } diff --git a/Penumbra/Configuration.Constants.cs b/Penumbra/Configuration.Constants.cs new file mode 100644 index 00000000..0cdcd9ec --- /dev/null +++ b/Penumbra/Configuration.Constants.cs @@ -0,0 +1,16 @@ +namespace Penumbra; + +public partial class Configuration +{ + // Contains some default values or boundaries for config values. + public static class Constants + { + public const int CurrentVersion = 3; + public const float MaxAbsoluteSize = 600; + public const int DefaultAbsoluteSize = 250; + public const float MinAbsoluteSize = 50; + public const int MaxScaledSize = 80; + public const int DefaultScaledSize = 20; + public const int MinScaledSize = 5; + } +} \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63c4b303..05d9cc81 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Forms; using Dalamud.Configuration; using Dalamud.Logging; -using Penumbra.UI; +using OtterGui.Filesystem; using Penumbra.UI.Classes; namespace Penumbra; @@ -12,11 +11,12 @@ namespace Penumbra; [Serializable] public partial class Configuration : IPluginConfiguration { - private const int CurrentVersion = 2; - - public int Version { get; set; } = CurrentVersion; + public int Version { get; set; } = Constants.CurrentVersion; public bool EnableMods { get; set; } = true; + public string ModDirectory { get; set; } = string.Empty; + + #if DEBUG public bool DebugMode { get; set; } = true; #else @@ -27,39 +27,39 @@ public partial class Configuration : IPluginConfiguration public bool EnableResourceLogging { get; set; } = false; public string ResourceLoggingFilter { get; set; } = string.Empty; + + public SortMode SortMode { get; set; } = SortMode.FoldersFirst; public bool ScaleModSelector { get; set; } = false; + public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; + public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool ShowAdvanced { get; set; } - - public bool DisableFileSystemNotifications { get; set; } - public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } - public string ModDirectory { get; set; } = string.Empty; - - public bool SortFoldersFirst { get; set; } = false; - public bool HasReadCharacterCollectionDesc { get; set; } = false; - public Dictionary< ColorId, uint > Colors { get; set; } = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); + // Load the current configuration. + // Includes adding new colors and migrating from old versions. public static Configuration Load() { var iConfiguration = Dalamud.PluginInterface.GetPluginConfig(); var configuration = iConfiguration as Configuration ?? new Configuration(); - if( iConfiguration is { Version: CurrentVersion } ) + if( iConfiguration is { Version: Constants.CurrentVersion } ) { configuration.AddColors( false ); return configuration; } - MigrateConfiguration.Migrate( configuration ); + Migration.Migrate( configuration ); configuration.AddColors( true ); return configuration; } + // Save the current configuration. public void Save() { try diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index b8f0c425..443c0ec0 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -30,6 +30,6 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; // @formatter:on } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index ada899fd..e0dad891 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -157,6 +157,8 @@ public unsafe partial class ResourceLoader => IterateResourceMap( resourceMap, action ) ) ); } + // Update the list of currently replaced resources. + // Only used when the Replaced Resources Tab in the Debug tab is open. public void UpdateDebugInfo() { for( var i = 0; i < _debugList.Count; ++i ) diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs new file mode 100644 index 00000000..dbd7c2b0 --- /dev/null +++ b/Penumbra/Interop/Structs/Material.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct Material +{ + [FieldOffset( 0x10 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x28 )] + public void* MaterialData; + + [FieldOffset( 0x48 )] + public Texture* Tex1; + + [FieldOffset( 0x60 )] + public Texture* Tex2; + + [FieldOffset( 0x78 )] + public Texture* Tex3; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs new file mode 100644 index 00000000..b9e04908 --- /dev/null +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -0,0 +1,41 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct RenderModel +{ + [FieldOffset( 0x18 )] + public RenderModel* PreviousModel; + + [FieldOffset( 0x20 )] + public RenderModel* NextModel; + + [FieldOffset( 0x30 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x40 )] + public Skeleton* Skeleton; + + [FieldOffset( 0x58 )] + public void** BoneList; + + [FieldOffset( 0x60 )] + public int BoneListCount; + + [FieldOffset( 0x68 )] + private void* UnkDXBuffer1; + + [FieldOffset( 0x70 )] + private void* UnkDXBuffer2; + + [FieldOffset( 0x78 )] + private void* UnkDXBuffer3; + + [FieldOffset( 0x90 )] + public void** Materials; + + [FieldOffset( 0x98 )] + public int MaterialCount; +} \ No newline at end of file diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index b4c05850..1344bf27 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -5,188 +5,217 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Mods; namespace Penumbra; -public class MigrateConfiguration +public partial class Configuration { - private Configuration _config = null!; - private JObject _data = null!; - - public string CurrentCollection = ModCollection.DefaultCollection; - public string DefaultCollection = ModCollection.DefaultCollection; - public string ForcedCollection = string.Empty; - public Dictionary< string, string > CharacterCollections = new(); - public Dictionary< string, string > ModSortOrder = new(); - public bool InvertModListOrder; - - - public static void Migrate( Configuration config ) + // Contains everything to migrate from older versions of the config to the current, + // including deprecated fields. + private class Migration { - var m = new MigrateConfiguration - { - _config = config, - _data = JObject.Parse( File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ) ), - }; + private Configuration _config = null!; + private JObject _data = null!; - m.CreateBackup(); - m.Version0To1(); - m.Version1To2(); - } + public string CurrentCollection = ModCollection.DefaultCollection; + public string DefaultCollection = ModCollection.DefaultCollection; + public string ForcedCollection = string.Empty; + public Dictionary< string, string > CharacterCollections = new(); + public Dictionary< string, string > ModSortOrder = new(); + public bool InvertModListOrder; + public bool SortFoldersFirst; - private void Version1To2() - { - if( _config.Version != 1 ) + public static void Migrate( Configuration config ) { - return; + var m = new Migration + { + _config = config, + _data = JObject.Parse( File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ) ), + }; + + CreateBackup(); + m.Version0To1(); + m.Version1To2(); + m.Version2To3(); } - ResettleSortOrder(); - ResettleCollectionSettings(); - ResettleForcedCollection(); - _config.Version = 2; - } - - private void ResettleForcedCollection() - { - ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; - if( ForcedCollection.Length <= 0 ) + // SortFoldersFirst was changed from a bool to the enum SortMode. + private void Version2To3() { - return; + if( _config.Version != 2 ) + { + return; + } + + SortFoldersFirst = _data[ nameof( SortFoldersFirst ) ]?.ToObject< bool >() ?? false; + _config.SortMode = SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; + _config.Version = 3; } - foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) ) + // The forced collection was removed due to general inheritance. + // Sort Order was moved to a separate file and may contain empty folders. + // Active collections in general were moved to their own file. + private void Version1To2() { + if( _config.Version != 1 ) + { + return; + } + + ResettleSortOrder(); + ResettleCollectionSettings(); + ResettleForcedCollection(); + _config.Version = 2; + } + + private void ResettleForcedCollection() + { + ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; + if( ForcedCollection.Length <= 0 ) + { + return; + } + + // Add the previous forced collection to all current collections except itself as an inheritance. + foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) ) + { + try + { + var jObject = JObject.Parse( File.ReadAllText( collection ) ); + if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection ) + { + jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } ); + File.WriteAllText( collection, jObject.ToString() ); + } + } + catch( Exception e ) + { + PluginLog.Error( + $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); + } + } + } + + // Move the current sort order to its own file. + private void ResettleSortOrder() + { + ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; + var file = Mod2.Manager.ModFileSystemFile; + using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); + using var writer = new StreamWriter( stream ); + using var j = new JsonTextWriter( writer ); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName( "Data" ); + j.WriteStartObject(); + foreach( var (mod, path) in ModSortOrder ) + { + j.WritePropertyName( mod, true ); + j.WriteValue( path ); + } + + j.WriteEndObject(); + j.WritePropertyName( "EmptyFolders" ); + j.WriteStartArray(); + j.WriteEndArray(); + j.WriteEndObject(); + } + + // Move the active collections to their own file. + private void ResettleCollectionSettings() + { + CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; + DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; + CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; + ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, + CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ) ); + } + + // Collections were introduced and the previous CurrentCollection got put into ModDirectory. + private void Version0To1() + { + if( _config.Version != 0 ) + { + return; + } + + _config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty; + _config.Version = 1; + ResettleCollectionJson(); + } + + // Move the previous mod configurations to a new default collection file. + private void ResettleCollectionJson() + { + var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) ); + if( !collectionJson.Exists ) + { + return; + } + + var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection ); + var defaultCollectionFile = defaultCollection.FileName; + if( defaultCollectionFile.Exists ) + { + return; + } + try { - var jObject = JObject.Parse( File.ReadAllText( collection ) ); - if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection ) + var text = File.ReadAllText( collectionJson.FullName ); + var data = JArray.Parse( text ); + + var maxPriority = 0; + var dict = new Dictionary< string, ModSettings2.SavedSettings >(); + foreach( var setting in data.Cast< JObject >() ) { - jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } ); - File.WriteAllText( collection, jObject.ToString() ); + var modName = ( string )setting[ "FolderName" ]!; + var enabled = ( bool )setting[ "Enabled" ]!; + var priority = ( int )setting[ "Priority" ]!; + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); + + dict[ modName ] = new ModSettings2.SavedSettings() + { + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + maxPriority = Math.Max( maxPriority, priority ); } + + InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; + if( !InvertModListOrder ) + { + dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); + } + + defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); + defaultCollection.Save(); } catch( Exception e ) { - PluginLog.Error( - $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); + PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); + throw; } } - } - private void ResettleSortOrder() - { - ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; - var file = Mod2.Manager.ModFileSystemFile; - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( "Data" ); - j.WriteStartObject(); - foreach( var (mod, path) in ModSortOrder ) + // Create a backup of the configuration file specifically. + private static void CreateBackup() { - j.WritePropertyName( mod, true ); - j.WriteValue( path ); - } - - j.WriteEndObject(); - j.WritePropertyName( "EmptyFolders" ); - j.WriteStartArray(); - j.WriteEndArray(); - j.WriteEndObject(); - } - - private void ResettleCollectionSettings() - { - CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; - DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; - CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ) ); - } - - private void Version0To1() - { - if( _config.Version != 0 ) - { - return; - } - - _config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty; - _config.Version = 1; - ResettleCollectionJson(); - } - - private void ResettleCollectionJson() - { - var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) ); - if( !collectionJson.Exists ) - { - return; - } - - var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection ); - var defaultCollectionFile = defaultCollection.FileName; - if( defaultCollectionFile.Exists ) - { - return; - } - - try - { - var text = File.ReadAllText( collectionJson.FullName ); - var data = JArray.Parse( text ); - - var maxPriority = 0; - var dict = new Dictionary< string, ModSettings2.SavedSettings >(); - foreach( var setting in data.Cast< JObject >() ) + var name = Dalamud.PluginInterface.ConfigFile.FullName; + var bakName = name + ".bak"; + try { - var modName = ( string )setting[ "FolderName" ]!; - var enabled = ( bool )setting[ "Enabled" ]!; - var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); - - dict[ modName ] = new ModSettings2.SavedSettings() - { - Enabled = enabled, - Priority = priority, - Settings = settings!, - }; - maxPriority = Math.Max( maxPriority, priority ); + File.Copy( name, bakName, true ); } - - InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; - if( !InvertModListOrder ) + catch( Exception e ) { - dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); + PluginLog.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); } - - defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); - defaultCollection.Save(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); - throw; - } - } - - private void CreateBackup() - { - var name = Dalamud.PluginInterface.ConfigFile.FullName; - var bakName = name + ".bak"; - try - { - File.Copy( name, bakName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod2.Manager.Root.cs b/Penumbra/Mods/Manager/Mod2.Manager.Root.cs index 85216b66..59f61e6c 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.Root.cs @@ -65,24 +65,19 @@ public sealed partial class Mod2 _mods.Clear(); BasePath.Refresh(); - // TODO - //StructuredMods.SubFolders.Clear(); - //StructuredMods.Mods.Clear(); if( Valid && BasePath.Exists ) { foreach( var modFolder in BasePath.EnumerateDirectories() ) { - //var mod = LoadMod( StructuredMods, modFolder ); - //if( mod == null ) - //{ - // continue; - //} - // - //mod.Index = _mods.Count; - //_mods.Add( mod ); + var mod = LoadMod( modFolder ); + if( mod == null ) + { + continue; + } + + mod.Index = _mods.Count; + _mods.Add( mod ); } - - //SetModStructure(); } ModDiscoveryFinished?.Invoke(); diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index a675c660..5b13121f 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -36,12 +36,10 @@ public sealed partial class Mod2 foreach( var unusedFile in mod.FindUnusedFiles() ) { - if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) ) + if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) + && !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) { - if( !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) - { - PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); - } + PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); } } diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystem.cs similarity index 92% rename from Penumbra/Mods/ModFileSystemA.cs rename to Penumbra/Mods/ModFileSystem.cs index c2b62921..3a65011b 100644 --- a/Penumbra/Mods/ModFileSystemA.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -4,7 +4,7 @@ using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystemA : FileSystem< Mod2 >, IDisposable +public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable { // Save the current sort order. // Does not save or copy the backup in the current mod directory, @@ -13,9 +13,9 @@ public sealed class ModFileSystemA : FileSystem< Mod2 >, IDisposable => SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true ); // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public static ModFileSystemA Load() + public static ModFileSystem Load() { - var ret = new ModFileSystemA(); + var ret = new ModFileSystem(); ret.Reload(); ret.Changed += ret.OnChange; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9bdb5bd4..f4a931fa 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; @@ -27,11 +28,13 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public string PluginDebugTitleStr - => "Penumbra - Debug Build"; - private const string CommandName = "/penumbra"; + public static string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; + + public static string CommitHash = + Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; @@ -47,14 +50,14 @@ public class Penumbra : IDalamudPlugin public readonly PathResolver PathResolver; public readonly MusicManager MusicManager; public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystemA ModFileSystem; + public readonly ModFileSystem ModFileSystem; public readonly PenumbraApi Api; public readonly PenumbraIpc Ipc; private readonly ConfigWindow _configWindow; private readonly LaunchButton _launchButton; private readonly WindowSystem _windowSystem; - private WebServer? _webServer; + internal WebServer? WebServer; public Penumbra( DalamudPluginInterface pluginInterface ) { @@ -78,7 +81,7 @@ public class Penumbra : IDalamudPlugin ModManager = new Mod2.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); - ModFileSystem = ModFileSystemA.Load(); + ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -113,6 +116,7 @@ public class Penumbra : IDalamudPlugin if( Config.DebugMode ) { ResourceLoader.EnableDebug(); + _configWindow.IsOpen = true; } if( Config.EnableFullResourceLogging ) @@ -126,11 +130,6 @@ public class Penumbra : IDalamudPlugin } ResidentResources.Reload(); - - foreach( var folder in ModManager.BasePath.EnumerateDirectories() ) - { - var m = Mod2.LoadMod( folder ); - } } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) @@ -210,7 +209,7 @@ public class Penumbra : IDalamudPlugin ShutdownWebServer(); - _webServer = new WebServer( o => o + WebServer = new WebServer( o => o .WithUrlPrefix( prefix ) .WithMode( HttpListenerMode.EmbedIO ) ) .WithCors( prefix ) @@ -218,15 +217,15 @@ public class Penumbra : IDalamudPlugin .WithController( () => new ModsController( this ) ) .WithController( () => new RedrawController( this ) ) ); - _webServer.StateChanged += ( _, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); + WebServer.StateChanged += ( _, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); - _webServer.RunAsync(); + WebServer.RunAsync(); } public void ShutdownWebServer() { - _webServer?.Dispose(); - _webServer = null; + WebServer?.Dispose(); + WebServer = null; } public void Dispose() diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0ce77a6e..2a14edb7 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,89 +1,93 @@ - - - net5.0-windows - preview - x64 - Penumbra - absolute gangstas - Penumbra - Copyright © 2020 - 1.0.0.0 - 1.0.0.0 - bin\$(Configuration)\ - true - enable - true - true - - - - full - DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - - - pdbonly - $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - - - - - - - - PreserveNewest - - - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll - False - - - - - - - - - - - - - - - - - - Always - - + + + net5.0-windows + preview + x64 + Penumbra + absolute gangstas + Penumbra + Copyright © 2020 + 1.0.0.0 + 1.0.0.0 + bin\$(Configuration)\ + true + enable + true + true + + + + full + DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC + + + + pdbonly + $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC + + + + $(MSBuildWarningsAsMessages);MSB3277 + + + + + PreserveNewest + + + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll + False + + + + + + + + + + + + + + + + + + Always + + + + + + + + + + $(GitCommitHash) + + \ No newline at end of file diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index f51c99fb..7ed19b08 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -15,11 +15,13 @@ public enum ColorId FolderExpanded, FolderCollapsed, FolderLine, + ItemId, } public static class Colors { public const uint PressEnterWarningBg = 0xFF202080; + public const uint RegexWarningBorder = 0xFF0000B0; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch @@ -36,6 +38,7 @@ public static class Colors ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 6caf5bd8..66473453 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -77,18 +77,18 @@ public partial class ModFileSystemSelector // or they contain the path search string. protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state ) { - if( path is ModFileSystemA.Folder f ) + if( path is ModFileSystem.Folder f ) { state = default; return ModFilterExtensions.UnfilteredStateMods != _stateFilter || FilterValue.Length > 0 && !f.FullName().Contains( FilterValue, IgnoreCase ); } - return ApplyFiltersAndState( ( ModFileSystemA.Leaf )path, out state ); + return ApplyFiltersAndState( ( ModFileSystem.Leaf )path, out state ); } // Apply the string filters. - private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod2 mod ) + private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod2 mod ) { return _filterType switch { @@ -226,7 +226,7 @@ public partial class ModFileSystemSelector } // Combined wrapper for handling all filters and setting state. - private bool ApplyFiltersAndState( ModFileSystemA.Leaf leaf, out ModState state ) + private bool ApplyFiltersAndState( ModFileSystem.Leaf leaf, out ModState state ) { state = new ModState { Color = ColorId.EnabledMod.Value() }; var mod = leaf.Value; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 57ec835f..3ee304cb 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -17,7 +17,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet< Mod2 > newMods ) + public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod2 > newMods ) : base( fileSystem ) { _newMods = newMods; @@ -29,6 +29,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); + SelectionChanged += OnSelectionChange; Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; @@ -49,7 +50,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo // Customization points. public override SortMode SortMode - => Penumbra.Config.SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; + => Penumbra.Config.SortMode; protected override uint ExpandedFolderColor => ColorId.FolderExpanded.Value(); @@ -69,7 +70,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo // Add custom context menu items. - private static void EnableDescendants( ModFileSystemA.Folder folder ) + private static void EnableDescendants( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Enable Descendants" ) ) { @@ -77,7 +78,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } } - private static void DisableDescendants( ModFileSystemA.Folder folder ) + private static void DisableDescendants( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Disable Descendants" ) ) { @@ -85,7 +86,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } } - private static void InheritDescendants( ModFileSystemA.Folder folder ) + private static void InheritDescendants( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Inherit Descendants" ) ) { @@ -93,7 +94,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } } - private static void OwnDescendants( ModFileSystemA.Folder folder ) + private static void OwnDescendants( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) { @@ -118,9 +119,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo // Helpers. - private static void SetDescendants( ModFileSystemA.Folder folder, bool enabled, bool inherit = false ) + private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) { - var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystemA.Leaf >().Select( l => l.Value ); + var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => l.Value ); if( inherit ) { Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); @@ -138,14 +139,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo SetFilterDirty(); if( modIdx == Selected?.Index ) { - OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + OnSelectionChange( Selected, Selected, default ); } } private void OnInheritanceChange( bool _ ) { SetFilterDirty(); - OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + OnSelectionChange( Selected, Selected, default ); } private void OnCollectionChange( ModCollection.Type type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) @@ -168,10 +169,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } SetFilterDirty(); - OnSelectionChange( SelectedLeaf, SelectedLeaf, default ); + OnSelectionChange( Selected, Selected, default ); } - private void OnSelectionChange( ModFileSystemA.Leaf? _1, ModFileSystemA.Leaf? newSelection, in ModState _2 ) + private void OnSelectionChange( Mod2? _1, Mod2? newSelection, in ModState _2 ) { if( newSelection == null ) { @@ -180,7 +181,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } else { - ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Value.Index ]; + ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ]; SelectedSettings = settings ?? ModSettings2.Empty; } } @@ -198,8 +199,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo { if( _lastSelectedDirectory.Length > 0 ) { - SelectedLeaf = ( ModFileSystemA.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) - .FirstOrDefault( l => l is ModFileSystemA.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); + SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) + .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); _lastSelectedDirectory = string.Empty; } } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs new file mode 100644 index 00000000..109bc32b --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private LowerString _changedItemFilter = LowerString.Empty; + + public void DrawChangedItemTab() + { + using var tab = ImRaii.TabItem( "Changed Items" ); + if( !tab ) + { + return; + } + + ImGui.SetNextItemWidth( -1 ); + LowerString.InputWithHint( "##changedItemsFilter", "Filter...", ref _changedItemFilter, 64 ); + + using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); + if( !child ) + { + return; + } + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips( height ); + using var list = ImRaii.Table( "##changedItems", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !list ) + { + return; + } + + var items = Penumbra.CollectionManager.Default.ChangedItems; + var rest = _changedItemFilter.IsEmpty + ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItem, items.Count ) + : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItem ); + ImGuiClip.DrawEndDummy( rest, height ); + } + + private bool FilterChangedItem( KeyValuePair< string, object? > item ) + => item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + + private void DrawChangedItem( KeyValuePair< string, object? > item ) + { + ImGui.TableNextColumn(); + DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize ); + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index eeacb28e..7f5f8ee7 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -1,10 +1,205 @@ +using System.Linq; using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using Penumbra.Collections; namespace Penumbra.UI; public partial class ConfigWindow { + private string _newCollectionName = string.Empty; + private string _newCharacterName = string.Empty; + + private void CreateNewCollection( bool duplicate ) + { + if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) + { + _newCollectionName = string.Empty; + } + } + + private static void DrawCleanCollectionButton() + { + if( ImGui.Button( "Clean Settings" ) ) + { + Penumbra.CollectionManager.Current.CleanUnavailableSettings(); + } + + ImGuiUtil.HoverTooltip( "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); + } + + private void DrawNewCollectionInput() + { + ImGui.SetNextItemWidth( _inputTextWidth.X ); + ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of mods." ); + + var createCondition = _newCollectionName.Length > 0; + var tt = createCondition ? string.Empty : "Please enter a name before creating a collection."; + if( ImGuiUtil.DrawDisabledButton( "Create New Empty Collection", Vector2.Zero, tt, !createCondition ) ) + { + CreateNewCollection( false ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Duplicate Current Collection", Vector2.Zero, tt, !createCondition ) ) + { + CreateNewCollection( true ); + } + + var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; + tt = deleteCondition ? string.Empty : "You can not delete the default collection."; + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Delete Current Collection", Vector2.Zero, tt, !deleteCondition ) ) + { + Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); + } + + if( Penumbra.Config.ShowAdvanced ) + { + ImGui.SameLine(); + DrawCleanCollectionButton(); + } + } + + public void DrawCurrentCollectionSelector() + { + DrawCollectionSelector( "##current", _inputTextWidth.X, ModCollection.Type.Current, false, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Current Collection", + "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); + } + + private void DrawDefaultCollectionSelector() + { + DrawCollectionSelector( "##default", _inputTextWidth.X, ModCollection.Type.Default, true, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Default Collection", + "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" + + "They also take precedence before the forced collection." ); + } + + private void DrawNewCharacterCollection() + { + const string description = "Character Collections apply specifically to game objects of the given name.\n" + + "The default collection does not apply to any character that has a character collection specified.\n" + + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; + + ImGui.SetNextItemWidth(_inputTextWidth.X ); + ImGui.InputTextWithHint( "##NewCharacter", "New Character Name", ref _newCharacterName, 32 ); + ImGui.SameLine(); + var disabled = _newCharacterName.Length == 0; + var tt = disabled ? "Please enter a Character name before creating the collection.\n\n" + description : description; + if( ImGuiUtil.DrawDisabledButton( "Create New Character Collection", Vector2.Zero, tt, disabled) ) + { + Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); + _newCharacterName = string.Empty; + } + } + + private void DrawCharacterCollectionSelectors() + { + using var child = ImRaii.Child( "##Collections", -Vector2.One, true ); + if( !child ) + return; + + DrawDefaultCollectionSelector(); + + foreach( var name in Penumbra.CollectionManager.Characters.Keys.ToArray() ) + { + using var id = ImRaii.PushId( name ); + DrawCollectionSelector( string.Empty, _inputTextWidth.X, ModCollection.Type.Character, true, name ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), Vector2.One * ImGui.GetFrameHeight(), string.Empty, false, true) ) + { + Penumbra.CollectionManager.RemoveCharacterCollection( name ); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( name ); + } + DrawNewCharacterCollection(); + } + + //private static void DrawInheritance( ModCollection collection ) + // { + // ImGui.PushID( collection.Index ); + // if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) ) + // { + // foreach( var inheritance in collection.Inheritance ) + // { + // DrawInheritance( inheritance ); + // } + // } + // + // ImGui.PopID(); + // } + // + // private void DrawCurrentCollectionInheritance() + // { + // if( !ImGui.BeginListBox( "##inheritanceList", + // new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) ) + // { + // return; + // } + // + // using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox ); + // DrawInheritance( _collections[ _currentCollectionIndex + 1 ] ); + // } + // + //private static int _newInheritanceIdx = 0; + // + //private void DrawNewInheritanceSelection() + //{ + // ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + // if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) ) + // { + // using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + // foreach( var collection in Penumbra.CollectionManager ) + // { + // if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) ) + // { + // _newInheritanceIdx = collection.Index; + // } + // } + // } + // + // ImGui.SameLine(); + // var valid = _newInheritanceIdx > ModCollection.Empty.Index + // && _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx + // && _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx ); + // using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid ); + // using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + // if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid ) + // { + // _collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] ); + // } + // + // style.Pop(); + // font.Pop(); + // ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." ); + //} + + private void DrawMainSelectors() + { + using var main = ImRaii.Child( "##CollectionsMain", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ); + if( !main ) + { + return; + } + + DrawCurrentCollectionSelector(); + ImGuiHelpers.ScaledDummy( 0, 10 ); + DrawNewCollectionInput(); + } + public void DrawCollectionsTab() { using var tab = ImRaii.TabItem( "Collections" ); @@ -13,10 +208,9 @@ public partial class ConfigWindow return; } - using var child = ImRaii.Child( "##CollectionsTab", -Vector2.One ); - if( !child ) - { - return; - } + + DrawMainSelectors(); + DrawCharacterCollectionSelectors(); } + } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index a76969b0..1bd4fefc 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,13 +1,26 @@ +using System; +using System.IO; +using System.Linq; using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using Penumbra.Api; +using Penumbra.Interop.Structs; +using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; public partial class ConfigWindow { #if DEBUG - private const bool DefaultVisibility = true; + private const string DebugVersionString = "(Debug)"; + private const bool DefaultVisibility = true; #else + private const string DebugVersionString = "(Release)"; private const bool DefaultVisibility = false; #endif @@ -31,5 +44,359 @@ public partial class ConfigWindow { return; } + + DrawDebugTabGeneral(); + ImGui.NewLine(); + DrawDebugTabReplacedResources(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); + DrawDebugCharacterUtility(); + ImGui.NewLine(); + DrawDebugResidentResources(); + ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawDebugTabIpc(); + ImGui.NewLine(); + } + + // Draw general information about mod and collection state. + private void DrawDebugTabGeneral() + { + if( !ImGui.CollapsingHeader( "General" ) ) + { + return; + } + + using var table = ImRaii.Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); + if( !table ) + { + return; + } + + var manager = Penumbra.ModManager; + PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); + PrintValue( "Git Commit Hash", Penumbra.CommitHash ); + PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); + PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); + PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); + PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); + PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); + PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); + PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); + PrintValue( "Path Resolver Enabled", _penumbra.PathResolver.Enabled.ToString() ); + PrintValue( "Music Manager Streaming Disabled", ( !_penumbra.MusicManager.StreamingEnabled ).ToString() ); + PrintValue( "Web Server Enabled", ( _penumbra.WebServer != null ).ToString() ); + } + + // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. + // Resources are collected by iterating through the + private static unsafe void DrawDebugTabReplacedResources() + { + if( !ImGui.CollapsingHeader( "Replaced Resources" ) ) + { + return; + } + + Penumbra.ResourceLoader.UpdateDebugInfo(); + + if( Penumbra.ResourceLoader.DebugList.Count == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) + { + var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; + var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; + ImGui.TableNextColumn(); + ImGui.Text( data.ManipulatedPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountManip.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( data.OriginalPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountOrig.ToString() ); + } + } + + // Draw information about which draw objects correspond to which game objects + // and which paths are due to be loaded by which collection. + private unsafe void DrawPathResolverDebug() + { + if( !ImGui.CollapsingHeader( "Path Resolver" ) ) + { + return; + } + + using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); + if( drawTree ) + { + using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (ptr, (c, idx)) in _penumbra.PathResolver.DrawObjectToObject ) + { + ImGui.TableNextColumn(); + ImGui.Text( ptr.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( idx.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( c.Name ); + } + } + } + + drawTree.Dispose(); + + using var pathTree = ImRaii.TreeNode( "Path Collections" ); + if( pathTree ) + { + using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (path, collection) in _penumbra.PathResolver.PathCollections ) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + ImGui.TableNextColumn(); + ImGui.Text( collection.Name ); + } + } + } + } + + // Draw information about the character utility class from SE, + // displaying all files, their sizes, the default files and the default sizes. + public unsafe void DrawDebugCharacterUtility() + { + if( !ImGui.CollapsingHeader( "Character Utility" ) ) + { + return; + } + + using var table = ImRaii.Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) + { + var idx = CharacterUtility.RelevantIndices[ i ]; + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + Text( resource ); + ImGui.TableNextColumn(); + ImGui.Selectable( $"0x{resource->GetData().Data:X}" ); + if( ImGui.IsItemClicked() ) + { + var (data, length) = resource->GetData(); + if( data != IntPtr.Zero && length > 0 ) + { + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + } + + ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); + + ImGui.TableNextColumn(); + ImGui.Text( $"{resource->GetData().Length}" ); + ImGui.TableNextColumn(); + ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, + Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + + ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); + + ImGui.TableNextColumn(); + ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); + } + } + + // Draw information about the resident resource files. + public unsafe void DrawDebugResidentResources() + { + if( !ImGui.CollapsingHeader( "Resident Resources" ) ) + { + return; + } + + if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) + { + var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + Text( resource ); + } + } + + // Draw information about the models, materials and resources currently loaded by the local player. + private static unsafe void DrawPlayerModelInfo() + { + var player = Dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) + { + return; + } + + var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); + if( model == null ) + { + return; + } + + using var table = ImRaii.Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( "Slot" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc File" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model File" ); + + for( var i = 0; i < model->SlotCount; ++i ) + { + var imc = ( ResourceHandle* )model->IMCArray[ i ]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( $"Slot {i}" ); + ImGui.TableNextColumn(); + ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); + ImGui.TableNextColumn(); + if( imc != null ) + { + Text( imc ); + } + + var mdl = ( RenderModel* )model->ModelArray[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); + if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + { + continue; + } + + ImGui.TableNextColumn(); + { + Text( mdl->ResourceHandle ); + } + } + } + + // Draw information about IPC options and availability. + private void DrawDebugTabIpc() + { + if( !ImGui.CollapsingHeader( "IPC" ) ) + { + return; + } + + var ipc = _penumbra.Ipc; + ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); + ImGui.Text( "Available subscriptions:" ); + using var indent = ImRaii.PushIndent(); + if( ipc.ProviderApiVersion != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); + } + + if( ipc.ProviderRedrawName != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); + } + + if( ipc.ProviderRedrawObject != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); + } + + if( ipc.ProviderRedrawAll != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); + } + + if( ipc.ProviderResolveDefault != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); + } + + if( ipc.ProviderResolveCharacter != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); + } + + if( ipc.ProviderChangedItemTooltip != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); + } + + if( ipc.ProviderChangedItemClick != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); + } + + if( ipc.ProviderGetChangedItems != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderGetChangedItems ); + } + } + + // Helper to print a property and its value in a 2-column table. + private static void PrintValue( string name, string value ) + { + ImGui.TableNextColumn(); + ImGui.Text( name ); + ImGui.TableNextColumn(); + ImGui.Text( value ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index db44fc62..b097bcb5 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -1,10 +1,18 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; namespace Penumbra.UI; public partial class ConfigWindow { + // Draw the effective tab if ShowAdvanced is on. public void DrawEffectiveChangesTab() { if( !Penumbra.Config.ShowAdvanced ) @@ -18,10 +26,190 @@ public partial class ConfigWindow return; } - using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One ); + SetupEffectiveSizes(); + DrawFilters(); + using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); if( !child ) { return; } + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips( height ); + using var table = ImRaii.Table( "##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg ); + if( !table ) + { + return; + } + + ImGui.TableSetupColumn( "##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength ); + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); + ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); + + DrawEffectiveRows( Penumbra.CollectionManager.Default, skips, height, + _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); + } + + // Sizes + private float _effectiveLeftTextLength; + private float _effectiveRightTextLength; + private float _effectiveUnscaledArrowLength; + private float _effectiveArrowLength; + + // Setup table sizes. + private void SetupEffectiveSizes() + { + if( _effectiveUnscaledArrowLength == 0 ) + { + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + _effectiveUnscaledArrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; + } + + _effectiveArrowLength = _effectiveUnscaledArrowLength * ImGuiHelpers.GlobalScale; + _effectiveLeftTextLength = 450 * ImGuiHelpers.GlobalScale; + _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; + } + + // Filters + private LowerString _effectiveGamePathFilter = LowerString.Empty; + private LowerString _effectiveFilePathFilter = LowerString.Empty; + + // Draw the header line for filters + private void DrawFilters() + { + var tmp = _effectiveGamePathFilter.Text; + ImGui.SetNextItemWidth( _effectiveLeftTextLength ); + if( ImGui.InputTextWithHint( "##gamePathFilter", "Filter game path...", ref tmp, 256 ) ) + { + _effectiveGamePathFilter = tmp; + } + + ImGui.SameLine( _effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( -1 ); + tmp = _effectiveFilePathFilter.Text; + if( ImGui.InputTextWithHint( "##fileFilter", "Filter file path...", ref tmp, 256 ) ) + { + _effectiveFilePathFilter = tmp; + } + } + + // Draw all rows respecting filters and using clipping. + private void DrawEffectiveRows( ModCollection active, int skips, float height, bool hasFilters ) + { + // We can use the known counts if no filters are active. + var stop = hasFilters + ? ImGuiClip.FilteredClippedDraw( active.ResolvedFiles, skips, CheckFilters, DrawLine ) + : ImGuiClip.ClippedDraw( active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count ); + + var m = active.MetaCache; + // If no meta manipulations are active, we can just draw the end dummy. + if( m is { Count: > 0 } ) + { + // We can treat all meta manipulations the same, + // we are only really interested in their ToString function here. + static (object, int) Convert< T >( KeyValuePair< T, int > kvp ) + => ( kvp.Key!, kvp.Value ); + + var it = m.Cmp.Manipulations.Select( Convert ) + .Concat( m.Eqp.Manipulations.Select( Convert ) ) + .Concat( m.Eqdp.Manipulations.Select( Convert ) ) + .Concat( m.Gmp.Manipulations.Select( Convert ) ) + .Concat( m.Est.Manipulations.Select( Convert ) ) + .Concat( m.Imc.Manipulations.Select( Convert ) ); + + // Filters mean we can not use the known counts. + if( hasFilters ) + { + var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager.Mods[ p.Item2 ].Name ) ); + if( stop >= 0 ) + { + ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); + } + else + { + stop = ImGuiClip.FilteredClippedDraw( it2, skips, CheckFilters, DrawLine, ~stop ); + ImGuiClip.DrawEndDummy( stop, height ); + } + } + else + { + if( stop >= 0 ) + { + ImGuiClip.DrawEndDummy( stop + m.Count, height ); + } + else + { + stop = ImGuiClip.ClippedDraw( it, skips, DrawLine, m.Count, ~stop ); + ImGuiClip.DrawEndDummy( stop, height ); + } + } + } + else + { + ImGuiClip.DrawEndDummy( stop, height ); + } + } + + // Draw a line for a game path and its redirected file. + private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair ) + { + var (path, name) = pair; + ImGui.TableNextColumn(); + CopyOnClickSelectable( path.Path ); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + CopyOnClickSelectable( name.InternalName ); + } + + // Draw a line for a path and its name. + private static void DrawLine( (string, LowerString) pair ) + { + var (path, name) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( path ); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( name ); + } + + // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. + private static void DrawLine( (object, int) pair ) + { + var (manipulation, modIdx) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager.Mods[ modIdx ].Name ); + } + + // Check filters for file replacements. + private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) + { + var (gamePath, fullPath) = kvp; + if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) + { + return false; + } + + return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); + } + + // Check filters for meta manipulations. + private bool CheckFilters( (string, LowerString) kvp ) + { + var (name, path) = kvp; + if( _effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains( _effectiveGamePathFilter.Lower ) ) + { + return false; + } + + return _effectiveFilePathFilter.Length == 0 || path.Contains( _effectiveFilePathFilter.Lower ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index d4608546..8258fe33 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -1,20 +1,35 @@ +using System; +using System.Linq; using System.Numerics; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.UI.Custom; +using Penumbra.Interop.Structs; +using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { + // Draw text given by a Utf8String. internal static unsafe void Text( Utf8String s ) - { - ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); - } + => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); + // Draw text given by a byte pointer. + internal static unsafe void Text( byte* s, int length ) + => ImGuiNative.igTextUnformatted( s, s + length ); + + // Draw the name of a resource file. + internal static unsafe void Text( ResourceHandle* resource ) + => Text( resource->FileName(), resource->FileNameLength ); + + // Draw a changed item, invoking the Api-Events for clicks and tooltips. + // Also draw the item Id in grey internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; @@ -28,18 +43,57 @@ public partial class ConfigWindow if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) { - ImGui.BeginTooltip(); - using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); + using var tt = ImRaii.Tooltip(); _penumbra.Api.InvokeTooltip( data ); } if( data is Item it ) { - var modelId = $"({( ( Quad )it.ModelMain ).A})"; - var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset; + ImGui.SameLine( ImGui.GetContentRegionAvail().X ); + ImGuiUtil.RightJustify( $"({( ( Quad )it.ModelMain ).A})", ColorId.ItemId.Value() ); + } + } - ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); - ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); + // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, + // using an Utf8String. + internal static unsafe void CopyOnClickSelectable( Utf8String text ) + { + if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) + { + ImGuiNative.igSetClipboardText( text.Path ); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( "Click to copy to clipboard." ); + } + } + + // Draw a collection selector of a certain width for a certain type. + private static void DrawCollectionSelector( string label, float width, ModCollection.Type type, bool withEmpty, string? characterName ) + { + ImGui.SetNextItemWidth( width ); + var current = type switch + { + ModCollection.Type.Default => Penumbra.CollectionManager.Default, + ModCollection.Type.Character => Penumbra.CollectionManager.Character( characterName ?? string.Empty ), + ModCollection.Type.Current => Penumbra.CollectionManager.Current, + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; + + using var combo = ImRaii.Combo( label, current.Name ); + if( !combo ) + { + return; + } + + foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ) ) + { + using var id = ImRaii.PushId( collection.Index ); + if( ImGui.Selectable( collection.Name, collection == current ) ) + { + Penumbra.CollectionManager.SetCollection( collection, type, characterName ); + } } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Details.cs b/Penumbra/UI/ConfigWindow.ModsTab.Details.cs new file mode 100644 index 00000000..6dfb603f --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.Details.cs @@ -0,0 +1,714 @@ +//using System.IO; +//using System.Linq; +//using System.Numerics; +//using Dalamud.Interface; +//using FFXIVClientStructs.FFXIV.Client.UI.Misc; +//using ImGuiNET; +//using Lumina.Data.Parsing; +//using Lumina.Excel.GeneratedSheets; +//using Penumbra.GameData.ByteString; +//using Penumbra.GameData.Enums; +//using Penumbra.GameData.Util; +//using Penumbra.Meta; +//using Penumbra.Meta.Manipulations; +//using Penumbra.Mods; +//using Penumbra.UI.Custom; +//using Penumbra.Util; +//using ImGui = ImGuiNET.ImGui; +// +//namespace Penumbra.UI; +// +//public partial class SettingsInterface +//{ +// private partial class PluginDetails +// { +// private const string LabelPluginDetails = "PenumbraPluginDetails"; +// private const string LabelAboutTab = "About"; +// private const string LabelChangedItemsTab = "Changed Items"; +// private const string LabelChangedItemsHeader = "##changedItems"; +// private const string LabelConflictsTab = "Mod Conflicts"; +// private const string LabelConflictsHeader = "##conflicts"; +// private const string LabelFileSwapTab = "File Swaps"; +// private const string LabelFileSwapHeader = "##fileSwaps"; +// private const string LabelFileListTab = "Files"; +// private const string LabelFileListHeader = "##fileList"; +// private const string LabelGroupSelect = "##groupSelect"; +// private const string LabelOptionSelect = "##optionSelect"; +// private const string LabelConfigurationTab = "Configuration"; +// +// private const string TooltipFilesTab = +// "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" +// + "Yellow files are restricted to some options."; +// +// private const float OptionSelectionWidth = 140f; +// private const float CheckMarkSize = 50f; +// private const uint ColorDarkGreen = 0xFF00A000; +// private const uint ColorGreen = 0xFF00C800; +// private const uint ColorYellow = 0xFF00C8C8; +// private const uint ColorDarkRed = 0xFF0000A0; +// private const uint ColorRed = 0xFF0000C8; +// +// +// private bool _editMode; +// private int _selectedGroupIndex; +// private OptionGroup? _selectedGroup; +// private int _selectedOptionIndex; +// private ConfigModule.Option? _selectedOption; +// private string _currentGamePaths = ""; +// +// private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList; +// +// private readonly Selector _selector; +// private readonly SettingsInterface _base; +// +// private void SelectGroup( int idx ) +// { +// // Not using the properties here because we need it to be not null forgiving in this case. +// var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; +// _selectedGroupIndex = idx; +// if( _selectedGroupIndex >= numGroups ) +// { +// _selectedGroupIndex = 0; +// } +// +// if( numGroups > 0 ) +// { +// _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; +// } +// else +// { +// _selectedGroup = null; +// } +// } +// +// private void SelectGroup() +// => SelectGroup( _selectedGroupIndex ); +// +// private void SelectOption( int idx ) +// { +// _selectedOptionIndex = idx; +// if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) +// { +// _selectedOptionIndex = 0; +// } +// +// if( _selectedGroup?.Options.Count > 0 ) +// { +// _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; +// } +// else +// { +// _selectedOption = null; +// } +// } +// +// private void SelectOption() +// => SelectOption( _selectedOptionIndex ); +// +// public void ResetState() +// { +// _fullFilenameList = null; +// SelectGroup(); +// SelectOption(); +// } +// +// public PluginDetails( SettingsInterface ui, Selector s ) +// { +// _base = ui; +// _selector = s; +// ResetState(); +// } +// +// // This is only drawn when we have a mod selected, so we can forgive nulls. +// private FullMod Mod +// => _selector.Mod!; +// +// private ModMeta Meta +// => Mod.Data.Meta; +// +// private void DrawAboutTab() +// { +// if( !_editMode && Meta.Description.Length == 0 ) +// { +// return; +// } +// +// if( !ImGui.BeginTabItem( LabelAboutTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// var desc = Meta.Description; +// var flags = _editMode +// ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine +// : ImGuiInputTextFlags.ReadOnly; +// +// if( _editMode ) +// { +// if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, +// AutoFillSize, flags ) ) +// { +// Meta.Description = desc; +// _selector.SaveCurrentMod(); +// } +// +// ImGuiCustom.HoverTooltip( TooltipAboutEdit ); +// } +// else +// { +// ImGui.TextWrapped( desc ); +// } +// } +// +// private void DrawChangedItemsTab() +// { +// if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) +// { +// return; +// } +// +// raii.Push( ImGui.EndListBox ); +// foreach( var (name, data) in Mod.Data.ChangedItems ) +// { +// _base.DrawChangedItem( name, data ); +// } +// } +// +// private void DrawConflictTab() +// { +// var conflicts = Penumbra.CollectionManager.Current.ModConflicts( Mod.Data.Index ).ToList(); +// if( conflicts.Count == 0 || !ImGui.BeginTabItem( LabelConflictsTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// ImGui.SetNextItemWidth( -1 ); +// if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) +// { +// return; +// } +// +// raii.Push( ImGui.EndListBox ); +// using var indent = ImGuiRaii.PushIndent( 0 ); +// Mods.Mod? oldBadMod = null; +// foreach( var conflict in conflicts ) +// { +// var badMod = Penumbra.ModManager[ conflict.Mod2 ]; +// if( badMod != oldBadMod ) +// { +// if( oldBadMod != null ) +// { +// indent.Pop( 30f ); +// } +// +// if( ImGui.Selectable( badMod.Meta.Name ) ) +// { +// _selector.SelectModByDir( badMod.BasePath.Name ); +// } +// +// ImGui.SameLine(); +// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorGreen : ColorRed ); +// ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); +// +// indent.Push( 30f ); +// } +// +// if( conflict.Data is Utf8GamePath p ) +// { +// unsafe +// { +// ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); +// } +// } +// else if( conflict.Data is MetaManipulation m ) +// { +// ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); +// } +// +// oldBadMod = badMod; +// } +// } +// +// private void DrawFileSwapTab() +// { +// if( _editMode ) +// { +// DrawFileSwapTabEdit(); +// return; +// } +// +// if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; +// +// ImGui.SetNextItemWidth( -1 ); +// if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) +// { +// return; +// } +// +// raii.Push( ImGui.EndTable ); +// +// foreach( var (source, target) in Meta.FileSwaps ) +// { +// ImGui.TableNextColumn(); +// ImGuiCustom.CopyOnClickSelectable( source.Path ); +// +// ImGui.TableNextColumn(); +// ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); +// +// ImGui.TableNextColumn(); +// ImGuiCustom.CopyOnClickSelectable( target.InternalName ); +// +// ImGui.TableNextRow(); +// } +// } +// +// private void UpdateFilenameList() +// { +// if( _fullFilenameList != null ) +// { +// return; +// } +// +// _fullFilenameList = Mod.Data.Resources.ModFiles +// .Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) +// .ToArray(); +// +// if( Meta.Groups.Count == 0 ) +// { +// return; +// } +// +// for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) +// { +// foreach( var group in Meta.Groups.Values ) +// { +// var inAll = true; +// foreach( var option in group.Options ) +// { +// if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) +// { +// _fullFilenameList[ i ].color = ColorYellow; +// } +// else +// { +// inAll = false; +// } +// } +// +// if( inAll && group.SelectionType == SelectType.Single ) +// { +// _fullFilenameList[ i ].color = ColorGreen; +// } +// } +// } +// } +// +// private void DrawFileListTab() +// { +// if( !ImGui.BeginTabItem( LabelFileListTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// ImGuiCustom.HoverTooltip( TooltipFilesTab ); +// +// ImGui.SetNextItemWidth( -1 ); +// if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) +// { +// raii.Push( ImGui.EndListBox ); +// UpdateFilenameList(); +// using var colorRaii = new ImGuiRaii.Color(); +// foreach( var (name, _, color, _) in _fullFilenameList! ) +// { +// colorRaii.Push( ImGuiCol.Text, color ); +// ImGui.Selectable( name.FullName ); +// colorRaii.Pop(); +// } +// } +// else +// { +// _fullFilenameList = null; +// } +// } +// +// private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders ) +// { +// removeFolders = 0; +// var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) ); +// if( defaultIndex < 0 ) +// { +// return defaultIndex; +// } +// +// var path = gamePaths[ defaultIndex ].Path; +// if( path.Length == TextDefaultGamePath.Length ) +// { +// return defaultIndex; +// } +// +// if( path[ TextDefaultGamePath.Length ] != ( byte )'-' +// || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) ) +// { +// return -1; +// } +// +// return defaultIndex; +// } +// +// private void HandleSelectedFilesButton( bool remove ) +// { +// if( _selectedOption == null ) +// { +// return; +// } +// +// var option = ( ConfigModule.Option )_selectedOption; +// +// var gamePaths = _currentGamePaths.Split( ';' ) +// .Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray(); +// if( gamePaths.Length == 0 ) +// { +// return; +// } +// +// var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); +// var changed = false; +// for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) +// { +// if( !_fullFilenameList![ i ].selected ) +// { +// continue; +// } +// +// _fullFilenameList![ i ].selected = false; +// var relName = _fullFilenameList[ i ].relName; +// if( defaultIndex >= 0 ) +// { +// gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); +// } +// +// if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) +// { +// if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) +// { +// changed = true; +// } +// +// if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) +// { +// changed = true; +// } +// } +// else +// { +// changed = gamePaths +// .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); +// } +// } +// +// if( changed ) +// { +// _fullFilenameList = null; +// _selector.SaveCurrentMod(); +// var idx = Penumbra.ModManager.Mods.IndexOf( Mod.Data ); +// // Since files may have changed, we need to recompute effective files. +// foreach( var collection in Penumbra.CollectionManager +// .Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) +// { +// collection.CalculateEffectiveFileList( false, collection == Penumbra.CollectionManager.Default ); +// } +// +// // If the mod is enabled in the current collection, its conflicts may have changed. +// if( Mod.Settings.Enabled ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// private void DrawAddToGroupButton() +// { +// if( ImGui.Button( ButtonAddToGroup ) ) +// { +// HandleSelectedFilesButton( false ); +// } +// } +// +// private void DrawRemoveFromGroupButton() +// { +// if( ImGui.Button( ButtonRemoveFromGroup ) ) +// { +// HandleSelectedFilesButton( true ); +// } +// } +// +// private void DrawGamePathInput() +// { +// ImGui.SetNextItemWidth( -1 ); +// ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, +// 128 ); +// ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); +// } +// +// private void DrawGroupRow() +// { +// if( _selectedGroup == null ) +// { +// SelectGroup(); +// } +// +// if( _selectedOption == null ) +// { +// SelectOption(); +// } +// +// if( !DrawEditGroupSelector() ) +// { +// return; +// } +// +// ImGui.SameLine(); +// if( !DrawEditOptionSelector() ) +// { +// return; +// } +// +// ImGui.SameLine(); +// DrawAddToGroupButton(); +// ImGui.SameLine(); +// DrawRemoveFromGroupButton(); +// ImGui.SameLine(); +// DrawGamePathInput(); +// } +// +// private void DrawFileAndGamePaths( int idx ) +// { +// void Selectable( uint colorNormal, uint colorReplace ) +// { +// var loc = _fullFilenameList![ idx ].color; +// if( loc == colorNormal ) +// { +// loc = colorReplace; +// } +// +// using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); +// ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); +// } +// +// const float indentWidth = 30f; +// if( _selectedOption == null ) +// { +// Selectable( 0, ColorGreen ); +// return; +// } +// +// var fileName = _fullFilenameList![ idx ].relName; +// var optionFiles = ( ( ConfigModule.Option )_selectedOption ).OptionFiles; +// if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) +// { +// Selectable( 0, ColorGreen ); +// +// using var indent = ImGuiRaii.PushIndent( indentWidth ); +// foreach( var gamePath in gamePaths.ToArray() ) +// { +// var tmp = gamePath.ToString(); +// var old = tmp; +// if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) +// && tmp != old ) +// { +// gamePaths.Remove( gamePath ); +// if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) ) +// { +// gamePaths.Add( p ); +// } +// else if( gamePaths.Count == 0 ) +// { +// optionFiles.Remove( fileName ); +// } +// +// _selector.SaveCurrentMod(); +// _selector.ReloadCurrentMod(); +// } +// } +// } +// else +// { +// Selectable( ColorYellow, ColorRed ); +// } +// } +// +// private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) +// { +// var enabled = ( flag & ( 1 << idx ) ) != 0; +// var oldEnabled = enabled; +// if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) +// { +// Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, +// Mod.Settings.Settings[ group.GroupName ] ^ ( 1 << idx ) ); +// // If the mod is enabled, recalculate files and filters. +// if( Mod.Settings.Enabled ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// private void DrawMultiSelector( OptionGroup group ) +// { +// if( group.Options.Count == 0 ) +// { +// return; +// } +// +// ImGuiCustom.BeginFramedGroup( group.GroupName ); +// using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); +// for( var i = 0; i < group.Options.Count; ++i ) +// { +// DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], +// $"{group.Options[ i ].OptionName}##{group.GroupName}" ); +// } +// } +// +// private void DrawSingleSelector( OptionGroup group ) +// { +// if( group.Options.Count < 2 ) +// { +// return; +// } +// +// var code = Mod.Settings.Settings[ group.GroupName ]; +// if( ImGui.Combo( group.GroupName, ref code +// , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) +// && code != Mod.Settings.Settings[ group.GroupName ] ) +// { +// Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, code ); +// if( Mod.Settings.Enabled ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// private void DrawGroupSelectors() +// { +// foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) +// { +// DrawSingleSelector( g ); +// } +// +// foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) +// { +// DrawMultiSelector( g ); +// } +// } +// +// private void DrawConfigurationTab() +// { +// if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// if( _editMode ) +// { +// DrawGroupSelectorsEdit(); +// } +// else +// { +// DrawGroupSelectors(); +// } +// } +// +// private void DrawMetaManipulationsTab() +// { +// if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) +// { +// return; +// } +// +// raii.Push( ImGui.EndListBox ); +// +// var manips = Mod.Data.Resources.MetaManipulations; +// var changes = false; +// if( _editMode || manips.DefaultData.Count > 0 ) +// { +// if( ImGui.CollapsingHeader( "Default" ) ) +// { +// changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); +// } +// } +// +// foreach( var (groupName, group) in manips.GroupData ) +// { +// foreach( var (optionName, option) in group ) +// { +// if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) +// { +// changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); +// } +// } +// } +// +// if( changes ) +// { +// Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); +// Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); +// _selector.ReloadCurrentMod( true, false ); +// } +// } +// +// public void Draw( bool editMode ) +// { +// _editMode = editMode; +// if( !ImGui.BeginTabBar( LabelPluginDetails ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); +// DrawAboutTab(); +// DrawChangedItemsTab(); +// +// DrawConfigurationTab(); +// if( _editMode ) +// { +// DrawFileListTabEdit(); +// } +// else +// { +// DrawFileListTab(); +// } +// +// DrawFileSwapTab(); +// DrawMetaManipulationsTab(); +// DrawConflictTab(); +// } +// } +//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs b/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs new file mode 100644 index 00000000..a6024185 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs @@ -0,0 +1,381 @@ +//using System.Collections.Generic; +//using System.Linq; +//using System.Numerics; +//using Dalamud.Interface; +//using FFXIVClientStructs.FFXIV.Client.UI.Misc; +//using ImGuiNET; +//using Penumbra.GameData.ByteString; +//using Penumbra.GameData.Util; +//using Penumbra.Mods; +//using Penumbra.UI.Custom; +//using Penumbra.Util; +// +//namespace Penumbra.UI; +// +//public partial class SettingsInterface +//{ +// private partial class PluginDetails +// { +// private const string LabelDescEdit = "##descedit"; +// private const string LabelNewSingleGroupEdit = "##newSingleGroup"; +// private const string LabelNewMultiGroup = "##newMultiGroup"; +// private const string LabelGamePathsEditBox = "##gamePathsEdit"; +// private const string ButtonAddToGroup = "Add to Group"; +// private const string ButtonRemoveFromGroup = "Remove from Group"; +// private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; +// private const string TextNoOptionAvailable = "[Not Available]"; +// private const string TextDefaultGamePath = "default"; +// private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true ); +// private const char GamePathsSeparator = ';'; +// +// private static readonly string TooltipFilesTabEdit = +// $"{TooltipFilesTab}\n" +// + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; +// +// private static readonly string TooltipGamePathsEdit = +// $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" +// + $"Use '{TextDefaultGamePath}' to add the original file path." +// + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; +// +// private const float MultiEditBoxWidth = 300f; +// +// private bool DrawEditGroupSelector() +// { +// ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); +// if( Meta!.Groups.Count == 0 ) +// { +// ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 ); +// return false; +// } +// +// if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex +// , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() +// , Meta.Groups.Count ) ) +// { +// SelectGroup(); +// SelectOption( 0 ); +// } +// +// return true; +// } +// +// private bool DrawEditOptionSelector() +// { +// ImGui.SameLine(); +// ImGui.SetNextItemWidth( OptionSelectionWidth ); +// if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 ) +// { +// ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); +// return false; +// } +// +// var group = ( OptionGroup )_selectedGroup!; +// if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), +// group.Options.Count ) ) +// { +// SelectOption(); +// } +// +// return true; +// } +// +// private void DrawFileListTabEdit() +// { +// if( ImGui.BeginTabItem( LabelFileListTab ) ) +// { +// UpdateFilenameList(); +// if( ImGui.IsItemHovered() ) +// { +// ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); +// } +// +// ImGui.SetNextItemWidth( -1 ); +// if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) ) +// { +// for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) +// { +// DrawFileAndGamePaths( i ); +// } +// } +// +// ImGui.EndListBox(); +// +// DrawGroupRow(); +// ImGui.EndTabItem(); +// } +// else +// { +// _fullFilenameList = null; +// } +// } +// +// private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group ) +// { +// var groupName = group.GroupName; +// if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) +// { +// if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// +// return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); +// } +// +// private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) +// { +// var newOption = ""; +// ImGui.SetCursorPosX( nameBoxStart ); +// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); +// if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, +// ImGuiInputTextFlags.EnterReturnsTrue ) +// && newOption.Length != 0 ) +// { +// group.Options.Add( new ConfigModule.Option() +// { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } ); +// _selector.SaveCurrentMod(); +// if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// private void DrawMultiSelectorEdit( OptionGroup group ) +// { +// var nameBoxStart = CheckMarkSize; +// var flag = Mod!.Settings.Settings[ group.GroupName ]; +// +// using var raii = DrawMultiSelectorEditBegin( group ); +// for( var i = 0; i < group.Options.Count; ++i ) +// { +// var opt = group.Options[ i ]; +// var label = $"##{group.GroupName}_{i}"; +// DrawMultiSelectorCheckBox( group, i, flag, label ); +// +// ImGui.SameLine(); +// var newName = opt.OptionName; +// +// if( nameBoxStart == CheckMarkSize ) +// { +// nameBoxStart = ImGui.GetCursorPosX(); +// } +// +// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); +// if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// if( newName.Length == 0 ) +// { +// Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); +// } +// else if( newName != opt.OptionName ) +// { +// group.Options[ i ] = new ConfigModule.Option() +// { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; +// _selector.SaveCurrentMod(); +// } +// +// if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// DrawMultiSelectorEditAdd( group, nameBoxStart ); +// } +// +// private void DrawSingleSelectorEditGroup( OptionGroup group ) +// { +// var groupName = group.GroupName; +// if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// } +// +// private float DrawSingleSelectorEdit( OptionGroup group ) +// { +// var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; +// var code = oldSetting; +// if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, +// group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) +// { +// if( code == group.Options.Count ) +// { +// if( newName.Length > 0 ) +// { +// Penumbra.CollectionManager.Current.SetModSetting(Mod.Data.Index, group.GroupName, code); +// group.Options.Add( new ConfigModule.Option() +// { +// OptionName = newName, +// OptionDesc = "", +// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), +// } ); +// _selector.SaveCurrentMod(); +// } +// } +// else +// { +// if( newName.Length == 0 ) +// { +// Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); +// } +// else +// { +// if( newName != group.Options[ code ].OptionName ) +// { +// group.Options[ code ] = new ConfigModule.Option() +// { +// OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, +// OptionFiles = group.Options[ code ].OptionFiles, +// }; +// _selector.SaveCurrentMod(); +// } +// } +// } +// +// if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) +// { +// _selector.Cache.TriggerFilterReset(); +// } +// } +// +// ImGui.SameLine(); +// var labelEditPos = ImGui.GetCursorPosX(); +// DrawSingleSelectorEditGroup( group ); +// +// return labelEditPos; +// } +// +// private void DrawAddSingleGroupField( float labelEditPos ) +// { +// var newGroup = ""; +// ImGui.SetCursorPosX( labelEditPos ); +// if( labelEditPos == CheckMarkSize ) +// { +// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); +// } +// +// if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, +// ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); +// // Adds empty group, so can not change filters. +// } +// } +// +// private void DrawAddMultiGroupField() +// { +// var newGroup = ""; +// ImGui.SetCursorPosX( CheckMarkSize ); +// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); +// if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, +// ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); +// // Adds empty group, so can not change filters. +// } +// } +// +// private void DrawGroupSelectorsEdit() +// { +// var labelEditPos = CheckMarkSize; +// var groups = Meta.Groups.Values.ToArray(); +// foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) +// { +// labelEditPos = DrawSingleSelectorEdit( g ); +// } +// +// DrawAddSingleGroupField( labelEditPos ); +// +// foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) +// { +// DrawMultiSelectorEdit( g ); +// } +// +// DrawAddMultiGroupField(); +// } +// +// private void DrawFileSwapTabEdit() +// { +// if( !ImGui.BeginTabItem( LabelFileSwapTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// ImGui.SetNextItemWidth( -1 ); +// if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) ) +// { +// return; +// } +// +// raii.Push( ImGui.EndListBox ); +// +// var swaps = Meta.FileSwaps.Keys.ToArray(); +// +// ImGui.PushFont( UiBuilder.IconFont ); +// var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X; +// ImGui.PopFont(); +// +// var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; +// for( var idx = 0; idx < swaps.Length + 1; ++idx ) +// { +// var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ]; +// var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ]; +// var keyString = key.ToString(); +// var valueString = value.ToString(); +// +// ImGui.SetNextItemWidth( width ); +// if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString, +// GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 ) +// { +// if( idx < swaps.Length ) +// { +// Meta.FileSwaps.Remove( key ); +// } +// +// if( !newKey.IsEmpty ) +// { +// Meta.FileSwaps[ newKey ] = value; +// } +// +// _selector.SaveCurrentMod(); +// _selector.ReloadCurrentMod(); +// } +// } +// +// if( idx >= swaps.Length ) +// { +// continue; +// } +// +// ImGui.SameLine(); +// ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); +// ImGui.SameLine(); +// +// ImGui.SetNextItemWidth( width ); +// if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString, +// GamePath.MaxGamePathLength, +// ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// var newValue = new FullPath( valueString.ToLowerInvariant() ); +// if( newValue.CompareTo( value ) != 0 ) +// { +// Meta.FileSwaps[ key ] = newValue; +// _selector.SaveCurrentMod(); +// _selector.Cache.TriggerListReset(); +// } +// } +// } +// } +// } +//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs b/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs new file mode 100644 index 00000000..f1b63d5a --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs @@ -0,0 +1,773 @@ +//using System; +//using System.Collections.Generic; +//using System.ComponentModel; +//using System.Linq; +//using System.Numerics; +//using Dalamud.Interface; +//using ImGuiNET; +//using Penumbra.GameData.Enums; +//using Penumbra.GameData.Structs; +//using Penumbra.Meta.Files; +//using Penumbra.Meta.Manipulations; +//using Penumbra.UI.Custom; +//using ObjectType = Penumbra.GameData.Enums.ObjectType; +// +//namespace Penumbra.UI; +// +//public partial class SettingsInterface +//{ +// private partial class PluginDetails +// { +// private int _newManipTypeIdx = 0; +// private ushort _newManipSetId = 0; +// private ushort _newManipSecondaryId = 0; +// private int _newManipSubrace = 0; +// private int _newManipRace = 0; +// private int _newManipAttribute = 0; +// private int _newManipEquipSlot = 0; +// private int _newManipObjectType = 0; +// private int _newManipGender = 0; +// private int _newManipBodySlot = 0; +// private ushort _newManipVariant = 0; +// +// +// private static readonly (string, EquipSlot)[] EqpEquipSlots = +// { +// ( "Head", EquipSlot.Head ), +// ( "Body", EquipSlot.Body ), +// ( "Hands", EquipSlot.Hands ), +// ( "Legs", EquipSlot.Legs ), +// ( "Feet", EquipSlot.Feet ), +// }; +// +// private static readonly (string, EquipSlot)[] EqdpEquipSlots = +// { +// EqpEquipSlots[ 0 ], +// EqpEquipSlots[ 1 ], +// EqpEquipSlots[ 2 ], +// EqpEquipSlots[ 3 ], +// EqpEquipSlots[ 4 ], +// ( "Ears", EquipSlot.Ears ), +// ( "Neck", EquipSlot.Neck ), +// ( "Wrist", EquipSlot.Wrists ), +// ( "Left Finger", EquipSlot.LFinger ), +// ( "Right Finger", EquipSlot.RFinger ), +// }; +// +// private static readonly (string, ModelRace)[] Races = +// { +// ( ModelRace.Midlander.ToName(), ModelRace.Midlander ), +// ( ModelRace.Highlander.ToName(), ModelRace.Highlander ), +// ( ModelRace.Elezen.ToName(), ModelRace.Elezen ), +// ( ModelRace.Miqote.ToName(), ModelRace.Miqote ), +// ( ModelRace.Roegadyn.ToName(), ModelRace.Roegadyn ), +// ( ModelRace.Lalafell.ToName(), ModelRace.Lalafell ), +// ( ModelRace.AuRa.ToName(), ModelRace.AuRa ), +// ( ModelRace.Viera.ToName(), ModelRace.Viera ), +// ( ModelRace.Hrothgar.ToName(), ModelRace.Hrothgar ), +// }; +// +// private static readonly (string, Gender)[] Genders = +// { +// ( Gender.Male.ToName(), Gender.Male ), +// ( Gender.Female.ToName(), Gender.Female ), +// ( Gender.MaleNpc.ToName(), Gender.MaleNpc ), +// ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), +// }; +// +// private static readonly (string, EstManipulation.EstType)[] EstTypes = +// { +// ( "Hair", EstManipulation.EstType.Hair ), +// ( "Face", EstManipulation.EstType.Face ), +// ( "Body", EstManipulation.EstType.Body ), +// ( "Head", EstManipulation.EstType.Head ), +// }; +// +// private static readonly (string, SubRace)[] Subraces = +// { +// ( SubRace.Midlander.ToName(), SubRace.Midlander ), +// ( SubRace.Highlander.ToName(), SubRace.Highlander ), +// ( SubRace.Wildwood.ToName(), SubRace.Wildwood ), +// ( SubRace.Duskwight.ToName(), SubRace.Duskwight ), +// ( SubRace.SeekerOfTheSun.ToName(), SubRace.SeekerOfTheSun ), +// ( SubRace.KeeperOfTheMoon.ToName(), SubRace.KeeperOfTheMoon ), +// ( SubRace.Seawolf.ToName(), SubRace.Seawolf ), +// ( SubRace.Hellsguard.ToName(), SubRace.Hellsguard ), +// ( SubRace.Plainsfolk.ToName(), SubRace.Plainsfolk ), +// ( SubRace.Dunesfolk.ToName(), SubRace.Dunesfolk ), +// ( SubRace.Raen.ToName(), SubRace.Raen ), +// ( SubRace.Xaela.ToName(), SubRace.Xaela ), +// ( SubRace.Rava.ToName(), SubRace.Rava ), +// ( SubRace.Veena.ToName(), SubRace.Veena ), +// ( SubRace.Helion.ToName(), SubRace.Helion ), +// ( SubRace.Lost.ToName(), SubRace.Lost ), +// }; +// +// private static readonly (string, RspAttribute)[] RspAttributes = +// { +// ( RspAttribute.MaleMinSize.ToFullString(), RspAttribute.MaleMinSize ), +// ( RspAttribute.MaleMaxSize.ToFullString(), RspAttribute.MaleMaxSize ), +// ( RspAttribute.FemaleMinSize.ToFullString(), RspAttribute.FemaleMinSize ), +// ( RspAttribute.FemaleMaxSize.ToFullString(), RspAttribute.FemaleMaxSize ), +// ( RspAttribute.BustMinX.ToFullString(), RspAttribute.BustMinX ), +// ( RspAttribute.BustMaxX.ToFullString(), RspAttribute.BustMaxX ), +// ( RspAttribute.BustMinY.ToFullString(), RspAttribute.BustMinY ), +// ( RspAttribute.BustMaxY.ToFullString(), RspAttribute.BustMaxY ), +// ( RspAttribute.BustMinZ.ToFullString(), RspAttribute.BustMinZ ), +// ( RspAttribute.BustMaxZ.ToFullString(), RspAttribute.BustMaxZ ), +// ( RspAttribute.MaleMinTail.ToFullString(), RspAttribute.MaleMinTail ), +// ( RspAttribute.MaleMaxTail.ToFullString(), RspAttribute.MaleMaxTail ), +// ( RspAttribute.FemaleMinTail.ToFullString(), RspAttribute.FemaleMinTail ), +// ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), +// }; +// +// +// private static readonly (string, ObjectType)[] ImcObjectType = +// { +// ( "Equipment", ObjectType.Equipment ), +// ( "Customization", ObjectType.Character ), +// ( "Weapon", ObjectType.Weapon ), +// ( "Demihuman", ObjectType.DemiHuman ), +// ( "Monster", ObjectType.Monster ), +// }; +// +// private static readonly (string, BodySlot)[] ImcBodySlots = +// { +// ( "Hair", BodySlot.Hair ), +// ( "Face", BodySlot.Face ), +// ( "Body", BodySlot.Body ), +// ( "Tail", BodySlot.Tail ), +// ( "Ears", BodySlot.Zear ), +// }; +// +// private static bool PrintCheckBox( string name, ref bool value, bool def ) +// { +// var color = value == def ? 0 : value ? ColorDarkGreen : ColorDarkRed; +// if( color == 0 ) +// { +// return ImGui.Checkbox( name, ref value ); +// } +// +// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color ); +// var ret = ImGui.Checkbox( name, ref value ); +// return ret; +// } +// +// private bool RestrictedInputInt( string name, ref ushort value, ushort min, ushort max ) +// { +// int tmp = value; +// if( ImGui.InputInt( name, ref tmp, 0, 0, _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) +// && tmp != value +// && tmp >= min +// && tmp <= max ) +// { +// value = ( ushort )tmp; +// return true; +// } +// +// return false; +// } +// +// private static bool DefaultButton< T >( string name, ref T value, T defaultValue ) where T : IComparable< T > +// { +// var compare = defaultValue.CompareTo( value ); +// var color = compare < 0 ? ColorDarkGreen : +// compare > 0 ? ColorDarkRed : ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Button ] ); +// +// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Button, color ); +// var ret = ImGui.Button( name, Vector2.UnitX * 120 ) && compare != 0; +// ImGui.SameLine(); +// return ret; +// } +// +// private bool DrawInputWithDefault( string name, ref ushort value, ushort defaultValue, ushort max ) +// => DefaultButton( $"{( _editMode ? "Set to " : "" )}Default: {defaultValue}##imc{name}", ref value, defaultValue ) +// || RestrictedInputInt( name, ref value, 0, max ); +// +// private static bool CustomCombo< T >( string label, IList< (string, T) > namesAndValues, out T value, ref int idx ) +// { +// value = idx < namesAndValues.Count ? namesAndValues[ idx ].Item2 : default!; +// +// if( !ImGui.BeginCombo( label, idx < namesAndValues.Count ? namesAndValues[ idx ].Item1 : string.Empty ) ) +// { +// return false; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); +// +// for( var i = 0; i < namesAndValues.Count; ++i ) +// { +// if( !ImGui.Selectable( $"{namesAndValues[ i ].Item1}##{label}{i}", idx == i ) || idx == i ) +// { +// continue; +// } +// +// idx = i; +// value = namesAndValues[ i ].Item2; +// return true; +// } +// +// return false; +// } +// +// private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Eqp; +// var val = id.Entry; +// +// +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var defaults = ExpandedEqpFile.GetDefault( id.SetId ); +// var attributes = Eqp.EqpAttributes[ id.Slot ]; +// +// foreach( var flag in attributes ) +// { +// var name = flag.ToLocalName(); +// var tmp = val.HasFlag( flag ); +// if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) +// { +// list[ manipIdx ] = new MetaManipulation( new EqpManipulation( tmp ? val | flag : val & ~flag, id.Slot, id.SetId ) ); +// ret = true; +// } +// } +// } +// +// ImGui.Text( ObjectType.Equipment.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.SetId.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Slot.ToString() ); +// return ret; +// } +// +// private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Gmp; +// var val = id.Entry; +// +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var defaults = ExpandedGmpFile.GetDefault( id.SetId ); +// var enabled = val.Enabled; +// var animated = val.Animated; +// var rotationA = val.RotationA; +// var rotationB = val.RotationB; +// var rotationC = val.RotationC; +// ushort unk = val.UnknownTotal; +// +// ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; +// ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); +// ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); +// ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); +// ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); +// ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); +// +// if( ret && _editMode ) +// { +// list[ manipIdx ] = new MetaManipulation( new GmpManipulation( new GmpEntry +// { +// Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, +// RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, +// }, id.SetId ) ); +// } +// } +// +// ImGui.Text( ObjectType.Equipment.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.SetId.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( EquipSlot.Head.ToString() ); +// return ret; +// } +// +// private static (bool, bool) GetEqdpBits( EquipSlot slot, EqdpEntry entry ) +// { +// return slot switch +// { +// EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), +// EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), +// EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), +// EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), +// EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), +// EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), +// EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), +// EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), +// EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), +// EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), +// _ => ( false, false ), +// }; +// } +// +// private static EqdpEntry SetEqdpBits( EquipSlot slot, EqdpEntry value, bool bit1, bool bit2 ) +// { +// switch( slot ) +// { +// case EquipSlot.Head: +// value = bit1 ? value | EqdpEntry.Head1 : value & ~EqdpEntry.Head1; +// value = bit2 ? value | EqdpEntry.Head2 : value & ~EqdpEntry.Head2; +// return value; +// case EquipSlot.Body: +// value = bit1 ? value | EqdpEntry.Body1 : value & ~EqdpEntry.Body1; +// value = bit2 ? value | EqdpEntry.Body2 : value & ~EqdpEntry.Body2; +// return value; +// case EquipSlot.Hands: +// value = bit1 ? value | EqdpEntry.Hands1 : value & ~EqdpEntry.Hands1; +// value = bit2 ? value | EqdpEntry.Hands2 : value & ~EqdpEntry.Hands2; +// return value; +// case EquipSlot.Legs: +// value = bit1 ? value | EqdpEntry.Legs1 : value & ~EqdpEntry.Legs1; +// value = bit2 ? value | EqdpEntry.Legs2 : value & ~EqdpEntry.Legs2; +// return value; +// case EquipSlot.Feet: +// value = bit1 ? value | EqdpEntry.Feet1 : value & ~EqdpEntry.Feet1; +// value = bit2 ? value | EqdpEntry.Feet2 : value & ~EqdpEntry.Feet2; +// return value; +// case EquipSlot.Neck: +// value = bit1 ? value | EqdpEntry.Neck1 : value & ~EqdpEntry.Neck1; +// value = bit2 ? value | EqdpEntry.Neck2 : value & ~EqdpEntry.Neck2; +// return value; +// case EquipSlot.Ears: +// value = bit1 ? value | EqdpEntry.Ears1 : value & ~EqdpEntry.Ears1; +// value = bit2 ? value | EqdpEntry.Ears2 : value & ~EqdpEntry.Ears2; +// return value; +// case EquipSlot.Wrists: +// value = bit1 ? value | EqdpEntry.Wrists1 : value & ~EqdpEntry.Wrists1; +// value = bit2 ? value | EqdpEntry.Wrists2 : value & ~EqdpEntry.Wrists2; +// return value; +// case EquipSlot.RFinger: +// value = bit1 ? value | EqdpEntry.RingR1 : value & ~EqdpEntry.RingR1; +// value = bit2 ? value | EqdpEntry.RingR2 : value & ~EqdpEntry.RingR2; +// return value; +// case EquipSlot.LFinger: +// value = bit1 ? value | EqdpEntry.RingL1 : value & ~EqdpEntry.RingL1; +// value = bit2 ? value | EqdpEntry.RingL2 : value & ~EqdpEntry.RingL2; +// return value; +// } +// +// return value; +// } +// +// private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Eqdp; +// var val = id.Entry; +// +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var defaults = ExpandedEqdpFile.GetDefault( id.FileIndex(), id.SetId ); +// var (bit1, bit2) = GetEqdpBits( id.Slot, val ); +// var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); +// +// ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); +// ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); +// +// if( ret && _editMode ) +// { +// list[ manipIdx ] = new MetaManipulation( new EqdpManipulation( SetEqdpBits( id.Slot, val, bit1, bit2 ), id.Slot, id.Gender, +// id.Race, id.SetId ) ); +// } +// } +// +// ImGui.Text( id.Slot.IsAccessory() +// ? ObjectType.Accessory.ToString() +// : ObjectType.Equipment.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.SetId.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Slot.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Race.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Gender.ToString() ); +// return ret; +// } +// +// private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Est; +// var val = id.Entry; +// +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var defaults = EstFile.GetDefault( id.Slot, Names.CombinedRace( id.Gender, id.Race ), id.SetId ); +// if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) +// { +// list[ manipIdx ] = new MetaManipulation( new EstManipulation( id.Gender, id.Race, id.Slot, id.SetId, val ) ); +// ret = true; +// } +// } +// +// ImGui.Text( id.Slot.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.SetId.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Race.ToName() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Gender.ToName() ); +// +// return ret; +// } +// +// private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Imc; +// var val = id.Entry; +// +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var defaults = new ImcFile( id.GamePath() ).GetEntry( ImcFile.PartIndex( id.EquipSlot ), id.Variant ); +// ushort materialId = val.MaterialId; +// ushort vfxId = val.VfxId; +// ushort decalId = val.DecalId; +// var soundId = ( ushort )val.SoundId; +// var attributeMask = val.AttributeMask; +// var materialAnimationId = ( ushort )val.MaterialAnimationId; +// ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); +// ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); +// ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); +// ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); +// ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); +// ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, +// byte.MaxValue ); +// +// if( ret && _editMode ) +// { +// var value = new ImcEntry( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, ( byte )vfxId, +// ( byte )materialAnimationId ); +// list[ manipIdx ] = new MetaManipulation( new ImcManipulation( id, value ) ); +// } +// } +// +// ImGui.Text( id.ObjectType.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.PrimaryId.ToString() ); +// ImGui.TableNextColumn(); +// if( id.ObjectType is ObjectType.Accessory or ObjectType.Equipment ) +// { +// ImGui.Text( id.ObjectType is ObjectType.Equipment or ObjectType.Accessory +// ? id.EquipSlot.ToString() +// : id.BodySlot.ToString() ); +// } +// +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// if( id.ObjectType != ObjectType.Equipment +// && id.ObjectType != ObjectType.Accessory ) +// { +// ImGui.Text( id.SecondaryId.ToString() ); +// } +// +// ImGui.TableNextColumn(); +// ImGui.Text( id.Variant.ToString() ); +// return ret; +// } +// +// private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) +// { +// var ret = false; +// var id = list[ manipIdx ].Rsp; +// var defaults = CmpFile.GetDefault( id.SubRace, id.Attribute ); +// var val = id.Entry; +// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// if( DefaultButton( +// $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) +// && _editMode ) +// { +// list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); +// ret = true; +// } +// +// ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); +// if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", +// _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) +// && val >= 0 +// && val <= 5 +// && _editMode ) +// { +// list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); +// ret = true; +// } +// } +// +// ImGui.Text( id.Attribute.ToUngenderedString() ); +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// ImGui.Text( id.SubRace.ToString() ); +// ImGui.TableNextColumn(); +// ImGui.Text( id.Attribute.ToGender().ToString() ); +// return ret; +// } +// +// private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) +// { +// var type = list[ manipIdx ].ManipulationType; +// +// if( _editMode ) +// { +// ImGui.TableNextColumn(); +// using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); +// if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##manipDelete{manipIdx}" ) ) +// { +// list.RemoveAt( manipIdx ); +// ImGui.TableNextRow(); +// --manipIdx; +// --count; +// return true; +// } +// } +// +// ImGui.TableNextColumn(); +// ImGui.Text( type.ToString() ); +// ImGui.TableNextColumn(); +// +// var changes = false; +// switch( type ) +// { +// case MetaManipulation.Type.Eqp: +// changes = DrawEqpRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// if( ImGui.Selectable( $"{list[ manipIdx ].Eqp.Entry}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// case MetaManipulation.Type.Gmp: +// changes = DrawGmpRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// if( ImGui.Selectable( $"{list[ manipIdx ].Gmp.Entry.Value}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// case MetaManipulation.Type.Eqdp: +// changes = DrawEqdpRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// var (bit1, bit2) = GetEqdpBits( list[ manipIdx ].Eqdp.Slot, list[ manipIdx ].Eqdp.Entry ); +// if( ImGui.Selectable( $"{bit1} {bit2}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// case MetaManipulation.Type.Est: +// changes = DrawEstRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// if( ImGui.Selectable( $"{list[ manipIdx ].Est.Entry}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// case MetaManipulation.Type.Imc: +// changes = DrawImcRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// if( ImGui.Selectable( $"{list[ manipIdx ].Imc.Entry.MaterialId}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// case MetaManipulation.Type.Rsp: +// changes = DrawRspRow( manipIdx, list ); +// ImGui.TableSetColumnIndex( 9 ); +// if( ImGui.Selectable( $"{list[ manipIdx ].Rsp.Entry}##{manipIdx}" ) ) +// { +// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); +// } +// +// break; +// } +// +// +// ImGui.TableNextRow(); +// return changes; +// } +// +// +// private MetaManipulation.Type DrawNewTypeSelection() +// { +// ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); +// ImGui.SameLine(); +// ImGui.RadioButton( "EQDP##newManipType", ref _newManipTypeIdx, 2 ); +// ImGui.SameLine(); +// ImGui.RadioButton( "EQP##newManipType", ref _newManipTypeIdx, 3 ); +// ImGui.SameLine(); +// ImGui.RadioButton( "EST##newManipType", ref _newManipTypeIdx, 4 ); +// ImGui.SameLine(); +// ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); +// ImGui.SameLine(); +// ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); +// return ( MetaManipulation.Type )_newManipTypeIdx; +// } +// +// private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) +// { +// var change = false; +// if( !ImGui.BeginPopup( popupName ) ) +// { +// return change; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// var manipType = DrawNewTypeSelection(); +// MetaManipulation? newManip = null; +// switch( manipType ) +// { +// case MetaManipulation.Type.Imc: +// { +// RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); +// RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); +// CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); +// ImcManipulation imc = new(); +// switch( objectType ) +// { +// case ObjectType.Equipment: +// CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); +// imc = new ImcManipulation( equipSlot, _newManipVariant, _newManipSetId, new ImcEntry() ); +// break; +// case ObjectType.DemiHuman: +// case ObjectType.Weapon: +// case ObjectType.Monster: +// RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); +// CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); +// imc = new ImcManipulation( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, +// _newManipVariant, new ImcEntry() ); +// break; +// } +// +// newManip = new MetaManipulation( new ImcManipulation( imc.ObjectType, imc.BodySlot, imc.PrimaryId, imc.SecondaryId, +// imc.Variant, imc.EquipSlot, ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ) ) ); +// +// break; +// } +// case MetaManipulation.Type.Eqdp: +// { +// RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); +// CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); +// CustomCombo( "Race", Races, out var race, ref _newManipRace ); +// CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); +// var eqdp = new EqdpManipulation( new EqdpEntry(), equipSlot, gender, race, _newManipSetId ); +// newManip = new MetaManipulation( new EqdpManipulation( ExpandedEqdpFile.GetDefault( eqdp.FileIndex(), eqdp.SetId ), +// equipSlot, gender, race, _newManipSetId ) ); +// break; +// } +// case MetaManipulation.Type.Eqp: +// { +// RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); +// CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); +// newManip = new MetaManipulation( new EqpManipulation( ExpandedEqpFile.GetDefault( _newManipSetId ) & Eqp.Mask( equipSlot ), +// equipSlot, _newManipSetId ) ); +// break; +// } +// case MetaManipulation.Type.Est: +// { +// RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); +// CustomCombo( "Est Type", EstTypes, out var estType, ref _newManipObjectType ); +// CustomCombo( "Race", Races, out var race, ref _newManipRace ); +// CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); +// newManip = new MetaManipulation( new EstManipulation( gender, race, estType, _newManipSetId, +// EstFile.GetDefault( estType, Names.CombinedRace( gender, race ), _newManipSetId ) ) ); +// break; +// } +// case MetaManipulation.Type.Gmp: +// RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); +// newManip = new MetaManipulation( new GmpManipulation( ExpandedGmpFile.GetDefault( _newManipSetId ), _newManipSetId ) ); +// break; +// case MetaManipulation.Type.Rsp: +// CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); +// CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); +// newManip = new MetaManipulation( new RspManipulation( subRace, rspAttribute, +// CmpFile.GetDefault( subRace, rspAttribute ) ) ); +// break; +// } +// +// if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) +// && newManip != null +// && list.All( m => !m.Equals( newManip ) ) ) +// { +// list.Add( newManip.Value ); +// change = true; +// ++count; +// ImGui.CloseCurrentPopup(); +// } +// +// return change; +// } +// +// private bool DrawMetaManipulationsTable( string label, IList< MetaManipulation > list, ref int count ) +// { +// var numRows = _editMode ? 11 : 10; +// var changes = false; +// +// +// if( list.Count > 0 +// && ImGui.BeginTable( label, numRows, +// ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); +// if( _editMode ) +// { +// ImGui.TableNextColumn(); +// } +// +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Type##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Object Type##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Set##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Slot##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Race##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Gender##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Secondary ID##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Variant##{label}" ); +// ImGui.TableNextColumn(); +// ImGui.TableNextColumn(); +// ImGui.TableHeader( $"Value##{label}" ); +// ImGui.TableNextRow(); +// +// for( var i = 0; i < list.Count; ++i ) +// { +// changes |= DrawManipulationRow( ref i, list, ref count ); +// } +// } +// +// var popupName = $"##newManip{label}"; +// if( _editMode ) +// { +// changes |= DrawNewManipulationPopup( $"##newManip{label}", list, ref count ); +// if( ImGui.Button( $"Add New Manipulation##{label}", Vector2.UnitX * -1 ) ) +// { +// ImGui.OpenPopup( popupName ); +// } +// +// return changes; +// } +// +// return false; +// } +// } +//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Import.cs b/Penumbra/UI/ConfigWindow.ModsTab.Import.cs new file mode 100644 index 00000000..917408fe --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.Import.cs @@ -0,0 +1,177 @@ +namespace Penumbra.UI; + +//public partial class ConfigWindow +//{ +// private class TabImport +// { +// private const string LabelTab = "Import Mods"; +// private const string LabelImportButton = "Import TexTools Modpacks"; +// private const string LabelFileDialog = "Pick one or more modpacks."; +// private const string LabelFileImportRunning = "Import in progress..."; +// private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; +// private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; +// +// private const uint ColorRed = 0xFF0000C8; +// private const uint ColorYellow = 0xFF00C8C8; +// +// private static readonly Vector2 ImportBarSize = new(-1, 0); +// +// private bool _isImportRunning; +// private string _errorMessage = string.Empty; +// private TexToolsImport? _texToolsImport; +// private readonly SettingsInterface _base; +// +// public readonly HashSet< string > NewMods = new(); +// +// public TabImport( SettingsInterface ui ) +// => _base = ui; +// +// public bool IsImporting() +// => _isImportRunning; +// +// private void RunImportTask() +// { +// _isImportRunning = true; +// Task.Run( async () => +// { +// try +// { +// var picker = new OpenFileDialog +// { +// Multiselect = true, +// Filter = FileTypeFilter, +// CheckFileExists = true, +// Title = LabelFileDialog, +// }; +// +// var result = await picker.ShowDialogAsync(); +// +// if( result == DialogResult.OK ) +// { +// _errorMessage = string.Empty; +// +// foreach( var fileName in picker.FileNames ) +// { +// PluginLog.Information( $"-> {fileName} START" ); +// +// try +// { +// _texToolsImport = new TexToolsImport( Penumbra.ModManager.BasePath ); +// var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); +// if( dir.Name.Any() ) +// { +// NewMods.Add( dir.Name ); +// } +// +// PluginLog.Information( $"-> {fileName} OK!" ); +// } +// catch( Exception ex ) +// { +// PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); +// _errorMessage = ex.Message; +// } +// } +// +// var directory = _texToolsImport?.ExtractedDirectory; +// _texToolsImport = null; +// _base.ReloadMods(); +// if( directory != null ) +// { +// _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); +// } +// } +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); +// } +// +// _isImportRunning = false; +// } ); +// } +// +// private void DrawImportButton() +// { +// if( !Penumbra.ModManager.Valid ) +// { +// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); +// ImGui.Button( LabelImportButton ); +// style.Pop(); +// +// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); +// ImGui.Text( "Can not import since the mod directory path is not valid." ); +// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); +// color.Pop(); +// +// ImGui.Text( "Please set the mod directory in the settings tab." ); +// ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); +// color.Push( ImGuiCol.Text, ColorYellow ); +// ImGui.Text( " D:\\ffxivmods" ); +// color.Pop(); +// ImGui.Text( "You can return to this tab once you've done that." ); +// } +// else if( ImGui.Button( LabelImportButton ) ) +// { +// RunImportTask(); +// } +// } +// +// private void DrawImportProgress() +// { +// ImGui.Button( LabelFileImportRunning ); +// +// if( _texToolsImport == null ) +// { +// return; +// } +// +// switch( _texToolsImport.State ) +// { +// case ImporterState.None: break; +// case ImporterState.WritingPackToDisk: +// ImGui.Text( TooltipModpack1 ); +// break; +// case ImporterState.ExtractingModFiles: +// { +// var str = +// $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; +// +// ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); +// break; +// } +// case ImporterState.Done: break; +// default: throw new ArgumentOutOfRangeException(); +// } +// } +// +// private void DrawFailedImportMessage() +// { +// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); +// ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); +// } +// +// public void Draw() +// { +// if( !ImGui.BeginTabItem( LabelTab ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); +// +// if( !_isImportRunning ) +// { +// DrawImportButton(); +// } +// else +// { +// DrawImportProgress(); +// } +// +// if( _errorMessage.Any() ) +// { +// DrawFailedImportMessage(); +// } +// } +// } +//} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs similarity index 100% rename from Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs rename to Penumbra/UI/ConfigWindow.ModsTab.Panel.cs diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs b/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs new file mode 100644 index 00000000..3190decb --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs @@ -0,0 +1,797 @@ +//using System; +//using System.Collections.Generic; +//using System.IO; +//using System.Linq; +//using System.Numerics; +//using System.Runtime.InteropServices; +//using System.Windows.Forms.VisualStyles; +//using Dalamud.Interface; +//using Dalamud.Logging; +//using ImGuiNET; +//using Penumbra.Collections; +//using Penumbra.Importer; +//using Penumbra.Mods; +//using Penumbra.UI.Classes; +//using Penumbra.UI.Custom; +//using Penumbra.Util; +// +//namespace Penumbra.UI; +// +//public partial class SettingsInterface +//{ +// // Constants +// private partial class Selector +// { +// private const string LabelSelectorList = "##availableModList"; +// private const string LabelModFilter = "##ModFilter"; +// private const string LabelAddModPopup = "AddModPopup"; +// private const string LabelModHelpPopup = "Help##Selector"; +// +// private const string TooltipModFilter = +// "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; +// +// private const string TooltipDelete = "Delete the selected mod"; +// private const string TooltipAdd = "Add an empty mod"; +// private const string DialogDeleteMod = "PenumbraDeleteMod"; +// private const string ButtonYesDelete = "Yes, delete it"; +// private const string ButtonNoDelete = "No, keep it"; +// +// private const float SelectorPanelWidth = 240f; +// +// private static readonly Vector2 SelectorButtonSizes = new(100, 0); +// private static readonly Vector2 HelpButtonSizes = new(40, 0); +// +// private static readonly Vector4 DeleteModNameColor = new(0.7f, 0.1f, 0.1f, 1); +// } +// +// // Buttons +// private partial class Selector +// { +// // === Delete === +// private int? _deleteIndex; +// +// private void DrawModTrashButton() +// { +// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); +// +// if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) +// { +// _deleteIndex = _index; +// } +// +// raii.Pop(); +// +// ImGuiCustom.HoverTooltip( TooltipDelete ); +// } +// +// private void DrawDeleteModal() +// { +// if( _deleteIndex == null ) +// { +// return; +// } +// +// ImGui.OpenPopup( DialogDeleteMod ); +// +// var _ = true; +// ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); +// var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); +// if( !ret ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// +// if( Mod == null ) +// { +// _deleteIndex = null; +// ImGui.CloseCurrentPopup(); +// return; +// } +// +// ImGui.Text( "Are you sure you want to delete the following mod:" ); +// var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); +// ImGui.Dummy( halfLine ); +// ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); +// ImGui.Dummy( halfLine ); +// +// var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); +// if( ImGui.Button( ButtonYesDelete, buttonSize ) ) +// { +// ImGui.CloseCurrentPopup(); +// var mod = Mod; +// Cache.RemoveMod( mod ); +// Penumbra.ModManager.DeleteMod( mod.Data.BasePath ); +// ModFileSystem.InvokeChange(); +// ClearSelection(); +// } +// +// ImGui.SameLine(); +// +// if( ImGui.Button( ButtonNoDelete, buttonSize ) ) +// { +// ImGui.CloseCurrentPopup(); +// _deleteIndex = null; +// } +// } +// +// // === Add === +// private bool _modAddKeyboardFocus = true; +// +// private void DrawModAddButton() +// { +// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); +// +// if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) +// { +// _modAddKeyboardFocus = true; +// ImGui.OpenPopup( LabelAddModPopup ); +// } +// +// raii.Pop(); +// +// ImGuiCustom.HoverTooltip( TooltipAdd ); +// +// DrawModAddPopup(); +// } +// +// private void DrawModAddPopup() +// { +// if( !ImGui.BeginPopup( LabelAddModPopup ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// +// if( _modAddKeyboardFocus ) +// { +// ImGui.SetKeyboardFocusHere(); +// _modAddKeyboardFocus = false; +// } +// +// var newName = ""; +// if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// try +// { +// var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), +// newName ); +// var modMeta = new ModMeta +// { +// Author = "Unknown", +// Name = newName.Replace( '/', '\\' ), +// Description = string.Empty, +// }; +// +// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); +// modMeta.SaveToFile( metaFile ); +// Penumbra.ModManager.AddMod( newDir ); +// ModFileSystem.InvokeChange(); +// SelectModOnUpdate( newDir.Name ); +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); +// } +// +// ImGui.CloseCurrentPopup(); +// } +// +// if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) +// { +// ImGui.CloseCurrentPopup(); +// } +// } +// +// // === Help === +// private void DrawModHelpButton() +// { +// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); +// if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) +// { +// ImGui.OpenPopup( LabelModHelpPopup ); +// } +// } +// +// private static void DrawModHelpPopup() +// { +// ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); +// ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), +// ImGuiCond.Appearing ); +// var _ = true; +// if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// +// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); +// ImGui.Text( "Mod Selector" ); +// ImGui.BulletText( "Select a mod to obtain more information." ); +// ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); +// ImGui.Indent(); +// ImGui.Bullet(); +// ImGui.SameLine(); +// ImGui.Text( "Enabled in the current collection." ); +// ImGui.Bullet(); +// ImGui.SameLine(); +// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), "Disabled in the current collection." ); +// ImGui.Bullet(); +// ImGui.SameLine(); +// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.NewMod.Value() ), +// "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); +// ImGui.Bullet(); +// ImGui.SameLine(); +// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.HandledConflictMod.Value() ), +// "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); +// ImGui.Bullet(); +// ImGui.SameLine(); +// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), +// "Enabled and conflicting with another enabled Mod on the same priority." ); +// ImGui.Unindent(); +// ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); +// ImGui.Indent(); +// ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); +// ImGui.BulletText( +// "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); +// ImGui.BulletText( +// "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" +// + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" +// + "where ModName will be sorted as if it was the string '1'." ); +// ImGui.Unindent(); +// ImGui.BulletText( +// "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); +// ImGui.BulletText( "Right-clicking a folder opens a context menu." ); +// ImGui.Indent(); +// ImGui.BulletText( +// "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); +// ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); +// ImGui.Unindent(); +// ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); +// ImGui.Indent(); +// ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); +// ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); +// ImGui.Unindent(); +// ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); +// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); +// ImGui.Text( "Mod Management" ); +// ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); +// ImGui.BulletText( "You can add a completely empty mod with the plus button." ); +// ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); +// ImGui.BulletText( +// "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); +// ImGui.BulletText( +// "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); +// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); +// ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); +// ImGui.SameLine(); +// if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) +// { +// ImGui.CloseCurrentPopup(); +// } +// } +// +// // === Main === +// private void DrawModsSelectorButtons() +// { +// // Selector controls +// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) +// .Push( ImGuiStyleVar.FrameRounding, 0 ); +// +// DrawModAddButton(); +// ImGui.SameLine(); +// DrawModHelpButton(); +// ImGui.SameLine(); +// DrawModTrashButton(); +// } +// } +// +// // Filters +// private partial class Selector +// { +// private string _modFilterInput = ""; +// +// private void DrawTextFilter() +// { +// ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); +// var tmp = _modFilterInput; +// if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) +// { +// Cache.SetTextFilter( tmp ); +// _modFilterInput = tmp; +// } +// +// ImGuiCustom.HoverTooltip( TooltipModFilter ); +// } +// +// private void DrawToggleFilter() +// { +// if( ImGui.BeginCombo( "##ModStateFilter", "", +// ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) +// { +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); +// var flags = ( int )Cache.StateFilter; +// foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) +// { +// ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); +// } +// +// Cache.StateFilter = ( ModFilter )flags; +// } +// +// ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); +// } +// +// private void DrawModsSelectorFilter() +// { +// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); +// DrawTextFilter(); +// ImGui.SameLine(); +// DrawToggleFilter(); +// } +// } +// +// // Drag'n Drop +// private partial class Selector +// { +// private const string DraggedModLabel = "ModIndex"; +// private const string DraggedFolderLabel = "FolderName"; +// +// private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); +// +// private static unsafe bool IsDropping( string name ) +// => ImGui.AcceptDragDropPayload( name ).NativePtr != null; +// +// private void DragDropTarget( ModFolder folder ) +// { +// if( !ImGui.BeginDragDropTarget() ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); +// +// if( IsDropping( DraggedModLabel ) ) +// { +// var payload = ImGui.GetDragDropPayload(); +// var modIndex = Marshal.ReadInt32( payload.Data ); +// var mod = Cache.GetMod( modIndex ).Item1; +// mod?.Data.Move( folder ); +// } +// else if( IsDropping( DraggedFolderLabel ) ) +// { +// var payload = ImGui.GetDragDropPayload(); +// var folderName = Marshal.PtrToStringUni( payload.Data ); +// if( ModFileSystem.Find( folderName!, out var droppedFolder ) +// && !ReferenceEquals( droppedFolder, folder ) +// && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) +// { +// droppedFolder.Move( folder ); +// } +// } +// } +// +// private void DragDropSourceFolder( ModFolder folder ) +// { +// if( !ImGui.BeginDragDropSource() ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); +// +// var folderName = folder.FullName; +// var ptr = Marshal.StringToHGlobalUni( folderName ); +// ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); +// ImGui.Text( $"Moving {folderName}..." ); +// } +// +// private void DragDropSourceMod( int modIndex, string modName ) +// { +// if( !ImGui.BeginDragDropSource() ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); +// +// Marshal.WriteInt32( _dragDropPayload, modIndex ); +// ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); +// ImGui.Text( $"Moving {modName}..." ); +// } +// +// ~Selector() +// => Marshal.FreeHGlobal( _dragDropPayload ); +// } +// +// // Selection +// private partial class Selector +// { +// public Mods.FullMod? Mod { get; private set; } +// private int _index; +// private string _nextDir = string.Empty; +// +// private void SetSelection( int idx, Mods.FullMod? info ) +// { +// Mod = info; +// if( idx != _index ) +// { +// _base._menu.InstalledTab.ModPanel.Details.ResetState(); +// } +// +// _index = idx; +// _deleteIndex = null; +// } +// +// private void SetSelection( int idx ) +// { +// if( idx >= Cache.Count ) +// { +// idx = -1; +// } +// +// if( idx < 0 ) +// { +// SetSelection( 0, null ); +// } +// else +// { +// SetSelection( idx, Cache.GetMod( idx ).Item1 ); +// } +// } +// +// public void ReloadSelection() +// => SetSelection( _index, Cache.GetMod( _index ).Item1 ); +// +// public void ClearSelection() +// => SetSelection( -1 ); +// +// public void SelectModOnUpdate( string directory ) +// => _nextDir = directory; +// +// public void SelectModByDir( string name ) +// { +// var (mod, idx) = Cache.GetModByBasePath( name ); +// SetSelection( idx, mod ); +// } +// +// public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) +// { +// if( Mod == null ) +// { +// return; +// } +// +// if( _index >= 0 && Penumbra.ModManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) +// { +// SelectModOnUpdate( Mod.Data.BasePath.Name ); +// _base._menu.InstalledTab.ModPanel.Details.ResetState(); +// } +// } +// +// public void SaveCurrentMod() +// => Mod?.Data.SaveMeta(); +// } +// +// // Right-Clicks +// private partial class Selector +// { +// // === Mod === +// private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen ) +// { +// if( !ImGui.BeginPopup( popupName ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// +// if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) ) +// { +// ImGui.CloseCurrentPopup(); +// } +// +// if( firstOpen ) +// { +// ImGui.SetKeyboardFocusHere( mod.Data.Order.FullPath.Length - 1 ); +// } +// } +// +// // === Folder === +// private string _newFolderName = string.Empty; +// private int _expandIndex = -1; +// private bool _expandCollapse; +// private bool _currentlyExpanding; +// +// private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) +// { +// var change = false; +// var metaManips = false; +// foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) ) +// { +// var (mod, _, _) = Cache.GetMod( currentIdx++ ); +// if( mod != null ) +// { +// change |= mod.Settings.Enabled != toWhat; +// mod!.Settings.Enabled = toWhat; +// metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; +// } +// } +// +// if( !change ) +// { +// return; +// } +// +// Cache.TriggerFilterReset(); +// var collection = Penumbra.CollectionManager.Current; +// if( collection.HasCache ) +// { +// collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.CollectionManager.Default ); +// } +// +// collection.Save(); +// } +// +// private void DrawRenameFolderInput( ModFolder folder ) +// { +// ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); +// if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, +// ImGuiInputTextFlags.EnterReturnsTrue ) ) +// { +// return; +// } +// +// if( _newFolderName.Any() ) +// { +// folder.Rename( _newFolderName ); +// } +// else +// { +// folder.Merge( folder.Parent! ); +// } +// +// _newFolderName = string.Empty; +// } +// +// private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) +// { +// if( !ImGui.BeginPopup( treeName ) ) +// { +// return; +// } +// +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); +// +// if( ImGui.MenuItem( "Expand All Descendants" ) ) +// { +// _expandIndex = currentIdx; +// _expandCollapse = false; +// } +// +// if( ImGui.MenuItem( "Collapse All Descendants" ) ) +// { +// _expandIndex = currentIdx; +// _expandCollapse = true; +// } +// +// if( ImGui.MenuItem( "Enable All Descendants" ) ) +// { +// ChangeStatusOfChildren( folder, currentIdx, true ); +// } +// +// if( ImGui.MenuItem( "Disable All Descendants" ) ) +// { +// ChangeStatusOfChildren( folder, currentIdx, false ); +// } +// +// ImGuiHelpers.ScaledDummy( 0, 10 ); +// DrawRenameFolderInput( folder ); +// } +// } +// +// // Main-Interface +// private partial class Selector +// { +// private readonly SettingsInterface _base; +// public readonly ModListCache Cache; +// +// private float _selectorScalingFactor = 1; +// +// public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) +// { +// _base = ui; +// Cache = new ModListCache( Penumbra.ModManager, newMods ); +// } +// +// private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) +// { +// if( collection == ModCollection.Empty +// || collection == Penumbra.CollectionManager.Current ) +// { +// using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); +// ImGui.Button( label, Vector2.UnitX * size ); +// } +// else if( ImGui.Button( label, Vector2.UnitX * size ) ) +// { +// _base._menu.CollectionsTab.SetCurrentCollection( collection ); +// } +// +// ImGuiCustom.HoverTooltip( +// $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); +// } +// +// private void DrawHeaderBar() +// { +// const float size = 200; +// +// DrawModsSelectorFilter(); +// var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; +// var comboSize = size * ImGui.GetIO().FontGlobalScale; +// var offset = comboSize + textSize; +// +// var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth() +// - offset +// - SelectorPanelWidth * _selectorScalingFactor +// - 3 * ImGui.GetStyle().ItemSpacing.X, 5f ); +// ImGui.SameLine(); +// DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default ); +// +// +// ImGui.SameLine(); +// ImGui.SetNextItemWidth( comboSize ); +// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); +// _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); +// } +// +// private void DrawFolderContent( ModFolder folder, ref int idx ) +// { +// // Collection may be manipulated. +// foreach( var item in folder.GetItems( Penumbra.ModManager.Config.SortFoldersFirst ).ToArray() ) +// { +// if( item is ModFolder sub ) +// { +// var (visible, _) = Cache.GetFolder( sub ); +// if( visible ) +// { +// DrawModFolder( sub, ref idx ); +// } +// else +// { +// idx += sub.TotalDescendantMods(); +// } +// } +// else if( item is Mods.Mod _ ) +// { +// var (mod, visible, color) = Cache.GetMod( idx ); +// if( mod != null && visible ) +// { +// DrawMod( mod, idx++, color ); +// } +// else +// { +// ++idx; +// } +// } +// } +// } +// +// private void DrawModFolder( ModFolder folder, ref int idx ) +// { +// var treeName = $"{folder.Name}##{folder.FullName}"; +// var open = ImGui.TreeNodeEx( treeName ); +// using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); +// +// if( idx == _expandIndex ) +// { +// _currentlyExpanding = true; +// } +// +// if( _currentlyExpanding ) +// { +// ImGui.SetNextItemOpen( !_expandCollapse ); +// } +// +// if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) +// { +// _newFolderName = string.Empty; +// ImGui.OpenPopup( treeName ); +// } +// +// DrawFolderContextMenu( folder, idx, treeName ); +// DragDropTarget( folder ); +// DragDropSourceFolder( folder ); +// +// if( open ) +// { +// DrawFolderContent( folder, ref idx ); +// } +// else +// { +// idx += folder.TotalDescendantMods(); +// } +// +// if( idx == _expandIndex ) +// { +// _currentlyExpanding = false; +// _expandIndex = -1; +// } +// } +// +// private void DrawMod( Mods.FullMod mod, int modIndex, uint color ) +// { +// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); +// +// var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); +// colorRaii.Pop(); +// +// var popupName = $"##SortOrderPopup{modIndex}"; +// var firstOpen = false; +// if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) +// { +// ImGui.OpenPopup( popupName ); +// firstOpen = true; +// } +// +// DragDropTarget( mod.Data.Order.ParentFolder ); +// DragDropSourceMod( modIndex, mod.Data.Meta.Name ); +// +// DrawModOrderPopup( popupName, mod, firstOpen ); +// +// if( selected ) +// { +// SetSelection( modIndex, mod ); +// } +// } +// +// public void Draw() +// { +// if( Cache.Update() ) +// { +// if( _nextDir.Any() ) +// { +// SelectModByDir( _nextDir ); +// _nextDir = string.Empty; +// } +// else if( Mod != null ) +// { +// SelectModByDir( Mod.Data.BasePath.Name ); +// } +// } +// +// _selectorScalingFactor = ImGuiHelpers.GlobalScale +// * ( Penumbra.Config.ScaleModSelector +// ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X +// : 1f ); +// // Selector pane +// DrawHeaderBar(); +// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); +// ImGui.BeginGroup(); +// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) +// .Push( ImGui.EndChild ); +// // Inlay selector list +// if( ImGui.BeginChild( LabelSelectorList, +// new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), +// true, ImGuiWindowFlags.HorizontalScrollbar ) ) +// { +// style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); +// +// var modIndex = 0; +// DrawFolderContent( Penumbra.ModManager.StructuredMods, ref modIndex ); +// style.Pop(); +// } +// +// raii.Pop(); +// +// DrawModsSelectorButtons(); +// +// style.Pop(); +// DrawModHelpPopup(); +// +// DrawDeleteModal(); +// } +// } +//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 8b5bc12e..3af34018 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -1,5 +1,9 @@ +using System; using System.Numerics; +using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using Penumbra.Collections; namespace Penumbra.UI; @@ -7,16 +11,92 @@ public partial class ConfigWindow { public void DrawModsTab() { + if( !Penumbra.ModManager.Valid ) + { + return; + } + using var tab = ImRaii.TabItem( "Mods" ); if( !tab ) { return; } - using var child = ImRaii.Child( "##ModsTab", -Vector2.One ); - if( !child ) + Selector.Draw( GetModSelectorSize() ); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawHeaderLine(); + + using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true ); + if( child ) + { + DrawModPanel(); + } + } + + private void DrawModPanel() + { + if( Selector.Selected == null ) { return; } } + + // Draw the header line that can quick switch between collections. + private void DrawHeaderLine() + { + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); + + DrawDefaultCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawInheritedCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawCollectionSelector( "##collection", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + } + + private static void DrawDefaultCollectionButton( Vector2 width ) + { + var name = $"Default Collection ({Penumbra.CollectionManager.Default.Name})"; + var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; + var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; + var tt = isCurrent ? "The current collection is already the configured default collection." + : isEmpty ? "The default collection is configured to be empty." + : "Set the current collection to the configured default collection."; + if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) + { + Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, ModCollection.Type.Current ); + } + } + + private void DrawInheritedCollectionButton( Vector2 width ) + { + var noModSelected = Selector.Selected == null; + var collection = Selector.SelectedSettingCollection; + var modInherited = collection != Penumbra.CollectionManager.Current; + var (name, tt) = ( noModSelected, modInherited ) switch + { + (true, _) => ( "Inherited Collection", "No mod selected." ), + (false, true) => ( $"Inherited Collection ({collection.Name})", + "Set the current collection to the collection the selected mod inherits its settings from." ), + (false, false) => ( "Not Inherited", "The selected mod does not inherit its settings." ), + }; + if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) + { + Penumbra.CollectionManager.SetCollection( collection, ModCollection.Type.Current ); + } + } + + // Get the correct size for the mod selector based on current config. + private static float GetModSelectorSize() + { + var absoluteSize = Math.Clamp( Penumbra.Config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, + Math.Min( Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100 ) ); + var relativeSize = Penumbra.Config.ScaleModSelector + ? Math.Clamp( Penumbra.Config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize ) + : 0; + return !Penumbra.Config.ScaleModSelector + ? absoluteSize + : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); + } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index db1e1d67..bbdef888 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -1,10 +1,22 @@ +using System; +using System.Linq; using System.Numerics; +using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.STD; +using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Loader; +using Penumbra.UI.Custom; namespace Penumbra.UI; public partial class ConfigWindow { + // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. public void DrawResourceManagerTab() { if( !DebugTabVisible ) @@ -18,10 +30,161 @@ public partial class ConfigWindow return; } + // Filter for resources containing the input string. + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); + using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); if( !child ) { return; } + + unsafe + { + ResourceLoader.IterateGraphs( DrawCategoryContainer ); + } } + + private float _hashColumnWidth; + private float _pathColumnWidth; + private float _refsColumnWidth; + private string _resourceManagerFilter = string.Empty; + + private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) + { + if( map == null ) + { + return; + } + + var label = GetNodeLabel( ( uint )category, ext, map->Count ); + using var tree = ImRaii.TreeNode( label ); + if( !tree || map->Count == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + return; + } + + ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); + ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); + ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth ); + ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); + ImGui.TableHeadersRow(); + + ResourceLoader.IterateResourceMap( map, ( hash, r ) => + { + // Filter unwanted names. + if( _resourceManagerFilter.Length != 0 + && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + var address = $"0x{( ulong )r:X}"; + ImGuiUtil.TextNextColumn( $"0x{hash:X8}" ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( address ); + + var resource = ( Interop.Structs.ResourceHandle* )r; + ImGui.TableNextColumn(); + Text( resource ); + if( ImGui.IsItemClicked() ) + { + var data = Interop.Structs.ResourceHandle.GetData( resource ); + if( data != null ) + { + var length = ( int )Interop.Structs.ResourceHandle.GetLength( resource ); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + } + + ImGuiUtil.HoverTooltip( "Click to copy byte-wise file data to clipboard, if any." ); + + ImGuiUtil.TextNextColumn( r->RefCount.ToString() ); + } ); + } + + // Draw a full category for the resource manager. + private unsafe void DrawCategoryContainer( ResourceCategory category, + StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) + { + if( map == null ) + { + return; + } + + using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}" ); + if( tree ) + { + SetTableWidths(); + ResourceLoader.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); + } + } + + // Obtain a label for an extension node. + private static string GetNodeLabel( uint label, uint type, ulong count ) + { + var (lowest, mid1, mid2, highest) = Functions.SplitBytes( type ); + return highest == 0 + ? $"({type:X8}) {( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}" + : $"({type:X8}) {( char )highest}{( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}"; + } + + // Set the widths for a resource table. + private void SetTableWidths() + { + _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; + _pathColumnWidth = ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale; + _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; + } + + //private static unsafe void DrawResourceProblems() + //{ + // if( !ImGui.CollapsingHeader( "Resource Problems##ResourceManager" ) + // || !ImGui.BeginTable( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + // { + // return; + // } + // + // using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + // + // ResourceLoader.IterateResources( ( _, r ) => + // { + // if( r->RefCount < 10000 ) + // { + // return; + // } + // + // ImGui.TableNextColumn(); + // ImGui.Text( r->Category.ToString() ); + // ImGui.TableNextColumn(); + // ImGui.Text( r->FileType.ToString( "X" ) ); + // ImGui.TableNextColumn(); + // ImGui.Text( r->Id.ToString( "X" ) ); + // ImGui.TableNextColumn(); + // ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); + // ImGui.TableNextColumn(); + // ImGui.Text( r->RefCount.ToString() ); + // ImGui.TableNextColumn(); + // ref var name = ref r->FileName; + // if( name.Capacity > 15 ) + // { + // ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + // } + // else + // { + // fixed( byte* ptr = name.Buffer ) + // { + // ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + // } + // } + // } ); + //} } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs new file mode 100644 index 00000000..5aede757 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -0,0 +1,169 @@ +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + // Sets the resource logger state when toggled, + // and the filter when entered. + private void DrawRequestedResourceLogging() + { + var tmp = Penumbra.Config.EnableResourceLogging; + if( ImGui.Checkbox( "##resourceLogging", ref tmp ) ) + { + _penumbra.ResourceLogger.SetState( tmp ); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Requested Resource Logging", "Log all game paths FFXIV requests to the plugin log.\n" + + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" + + "Red boundary indicates invalid regex." ); + + ImGui.SameLine(); + + // Red borders if the string is not a valid regex. + var tmpString = Penumbra.Config.ResourceLoggingFilter; + using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, !_penumbra.ResourceLogger.ValidRegex ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_penumbra.ResourceLogger.ValidRegex ); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) + { + _penumbra.ResourceLogger.SetFilter( tmpString ); + } + } + + // Toggling audio streaming will need to apply to the music manager + // and rediscover mods due to determining whether .scds will be loaded or not. + private void DrawDisableSoundStreamingBox() + { + var tmp = Penumbra.Config.DisableSoundStreaming; + if( ImGui.Checkbox( "##streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) + { + Penumbra.Config.DisableSoundStreaming = tmp; + Penumbra.Config.Save(); + if( tmp ) + { + _penumbra.MusicManager.DisableStreaming(); + } + else + { + _penumbra.MusicManager.EnableStreaming(); + } + + Penumbra.ModManager.DiscoverMods(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Disable Audio Streaming", + "Disable streaming in the games audio engine.\n" + + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems.\n" + + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash.\n" + + "You might need to restart your game for this to fully take effect." ); + } + + // Creates and destroys the web server when toggled. + private void DrawEnableHttpApiBox() + { + var http = Penumbra.Config.EnableHttpApi; + if( ImGui.Checkbox( "##http", ref http ) ) + { + if( http ) + { + _penumbra.CreateWebServer(); + } + else + { + _penumbra.ShutdownWebServer(); + } + + Penumbra.Config.EnableHttpApi = http; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable HTTP API", + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); + } + + // Should only be used for debugging. + private static void DrawEnableFullResourceLoggingBox() + { + var tmp = Penumbra.Config.EnableFullResourceLogging; + if( ImGui.Checkbox( "##fullLogging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableFullLogging(); + } + else + { + Penumbra.ResourceLoader.DisableFullLogging(); + } + + Penumbra.Config.EnableFullResourceLogging = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Full Resource Logging", + "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); + } + + // Should only be used for debugging. + private static void DrawEnableDebugModeBox() + { + var tmp = Penumbra.Config.DebugMode; + if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableDebug(); + } + else + { + Penumbra.ResourceLoader.DisableDebug(); + } + + Penumbra.Config.DebugMode = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Debug Mode", + "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load." ); + } + + private static void DrawReloadResourceButton() + { + if( ImGui.Button( "Reload Resident Resources" ) ) + { + Penumbra.ResidentResources.Reload(); + } + + ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" + + "You usually should not need to do this." ); + } + + private void DrawAdvancedSettings() + { + if( !Penumbra.Config.ShowAdvanced || !ImGui.CollapsingHeader( "Advanced" ) ) + { + return; + } + + DrawRequestedResourceLogging(); + DrawDisableSoundStreamingBox(); + DrawEnableHttpApiBox(); + DrawEnableDebugModeBox(); + DrawEnableFullResourceLoggingBox(); + DrawReloadResourceButton(); + ImGui.NewLine(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs b/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs new file mode 100644 index 00000000..53ef7d84 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs @@ -0,0 +1,91 @@ +using System; +using ImGuiNET; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + // Store separately to use IsItemDeactivatedAfterEdit. + private float _absoluteSelectorSize = Penumbra.Config.ModSelectorAbsoluteSize; + private int _relativeSelectorSize = Penumbra.Config.ModSelectorScaledSize; + + // Different supported sort modes as a combo. + private void DrawFolderSortType() + { + var sortMode = Penumbra.Config.SortMode; + ImGui.SetNextItemWidth( _inputTextWidth.X ); + using var combo = ImRaii.Combo( "##sortMode", sortMode.Data().Name ); + if( combo ) + { + foreach( var val in Enum.GetValues< SortMode >() ) + { + var (name, desc) = val.Data(); + if( ImGui.Selectable( name, val == sortMode ) && val != sortMode ) + { + Penumbra.Config.SortMode = val; + Selector.SetFilterDirty(); + Penumbra.Config.Save(); + } + + ImGuiUtil.HoverTooltip( desc ); + } + } + + combo.Dispose(); + ImGuiUtil.LabeledHelpMarker( "Sort Mode", "Choose the sort mode for the mod selector in the mods tab." ); + } + + private void DrawAbsoluteSizeSelector() + { + if( ImGuiUtil.DragFloat( "##absoluteSize", ref _absoluteSelectorSize, _inputTextWidth.X, 1, + Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f" ) + && _absoluteSelectorSize != Penumbra.Config.ModSelectorAbsoluteSize ) + { + Penumbra.Config.ModSelectorAbsoluteSize = _absoluteSelectorSize; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Mod Selector Absolute Size", "The minimal absolute size of the mod selector in the mod tab in pixels." ); + } + + private void DrawRelativeSizeSelector() + { + var scaleModSelector = Penumbra.Config.ScaleModSelector; + if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + { + Penumbra.Config.ScaleModSelector = scaleModSelector; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + if( ImGuiUtil.DragInt( "##relativeSize", ref _relativeSelectorSize, _inputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, + Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%" ) + && _relativeSelectorSize != Penumbra.Config.ModSelectorScaledSize ) + { + Penumbra.Config.ModSelectorScaledSize = _relativeSelectorSize; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); + } + + private void DrawModSelectorSettings() + { + if( !ImGui.CollapsingHeader( "Mod Selector" ) ) + { + return; + } + + DrawFolderSortType(); + DrawAbsoluteSizeSelector(); + DrawRelativeSizeSelector(); + + ImGui.NewLine(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 224adccf..c07d4413 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -1,29 +1,57 @@ using System; using System.Diagnostics; using System.IO; -using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Components; +using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; +using OtterGui.Widgets; using Penumbra.UI.Classes; +using Penumbra.UI.Custom; namespace Penumbra.UI; public partial class ConfigWindow { - private string _newModDirectory = string.Empty; + private string _settingsNewModDirectory = string.Empty; + private readonly FileDialogManager _dialogManager = new(); + private bool _dialogOpen; - private static bool DrawPressEnterWarning( string old ) + private static bool DrawPressEnterWarning( string old, float width ) { using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var w = new Vector2( ImGui.CalcItemWidth(), 0 ); + var w = new Vector2( width, 0 ); return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); } + private void DrawDirectoryPickerButton() + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Select a directory via dialog.", false, true ) ) + { + if( _dialogOpen ) + { + _dialogManager.Reset(); + _dialogOpen = false; + } + else + { + //_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => + //{ + // _newModDirectory = b ? s : _newModDirectory; + // _dialogOpen = false; + //}, _newModDirectory, false); + _dialogOpen = true; + } + } + + _dialogManager.Draw(); + } + private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) { using var _ = ImRaii.PushId( id ); @@ -40,41 +68,54 @@ public partial class ConfigWindow private void DrawRootFolder() { - using var group = ImRaii.Group(); - ImGui.SetNextItemWidth( _inputTextWidth.X ); - var save = ImGui.InputText( "Root Directory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + // Initialize first time. + if( _settingsNewModDirectory.Length == 0 ) + { + _settingsNewModDirectory = Penumbra.Config.ModDirectory; + } + + var spacing = 3 * ImGuiHelpers.GlobalScale; + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth( _inputTextWidth.X - spacing - ImGui.GetFrameHeight() ); + var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); ImGui.SameLine(); - ImGuiComponents.HelpMarker( "This is where Penumbra will store your extracted mod files.\n" + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Root Directory", "This is where Penumbra will store your extracted mod files.\n" + "TTMP files are not copied, just extracted.\n" + "This directory needs to be accessible and you need write access here.\n" + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); - ImGui.SameLine(); - DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); group.Dispose(); + ImGui.SameLine(); + var pos = ImGui.GetCursorPosX(); + ImGui.NewLine(); - if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 ) + if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 ) { return; } - if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory ) ) + if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) { - Penumbra.ModManager.DiscoverMods( _newModDirectory ); + Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory ); } } - private void DrawRediscoverButton() + private static void DrawRediscoverButton() { + DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); + ImGui.SameLine(); if( ImGui.Button( "Rediscover Mods" ) ) { Penumbra.ModManager.DiscoverMods(); } - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); + ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); } private void DrawEnabledBox() @@ -86,190 +127,41 @@ public partial class ConfigWindow } } - private void DrawShowAdvancedBox() + private static void DrawShowAdvancedBox() { var showAdvanced = Penumbra.Config.ShowAdvanced; - if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) + if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) { Penumbra.Config.ShowAdvanced = showAdvanced; Penumbra.Config.Save(); } ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Enable some advanced options in this window and in the mod selector.\n" + ImGuiUtil.LabeledHelpMarker( "Show Advanced Settings", "Enable some advanced options in this window and in the mod selector.\n" + "This is required to enable manually editing any mod information." ); } - private void DrawFolderSortType() + private static void DrawColorSettings() { - // TODO provide all options - var foldersFirst = Penumbra.Config.SortFoldersFirst; - if( ImGui.Checkbox( "Sort Mod-Folders Before Mods", ref foldersFirst ) ) + if( !ImGui.CollapsingHeader( "Colors" ) ) { - Penumbra.Config.SortFoldersFirst = foldersFirst; - Selector.SetFilterDirty(); - Penumbra.Config.Save(); + return; } - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Prioritizes all mod-folders in the mod-selector in the Installed Mods tab so that folders come before single mods, instead of being sorted completely alphabetically" ); - } - - private void DrawScaleModSelectorBox() - { - // TODO set scale - var scaleModSelector = Penumbra.Config.ScaleModSelector; - if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + foreach( var color in Enum.GetValues< ColorId >() ) { - Penumbra.Config.ScaleModSelector = scaleModSelector; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); - } - - private void DrawDisableSoundStreamingBox() - { - var tmp = Penumbra.Config.DisableSoundStreaming; - if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) - { - Penumbra.Config.DisableSoundStreaming = tmp; - Penumbra.Config.Save(); - if( tmp ) + var (defaultColor, name, description) = color.Data(); + var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; + if( Widget.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) { - _penumbra.MusicManager.DisableStreaming(); + Penumbra.Config.Save(); } - else - { - _penumbra.MusicManager.EnableStreaming(); - } - - Penumbra.ModManager.DiscoverMods(); } - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Disable streaming in the games audio engine.\n" - + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" - + "Only touch this if you experience sound problems.\n" - + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." ); + ImGui.NewLine(); } - private void DrawEnableHttpApiBox() - { - var http = Penumbra.Config.EnableHttpApi; - if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) - { - if( http ) - { - _penumbra.CreateWebServer(); - } - else - { - _penumbra.ShutdownWebServer(); - } - - Penumbra.Config.EnableHttpApi = http; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( "Reload Resident Resources" ) ) - { - Penumbra.ResidentResources.Reload(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Reload some specific files that the game keeps in memory at all times.\n" - + "You usually should not need to do this." ); - } - - private void DrawEnableFullResourceLoggingBox() - { - var tmp = Penumbra.Config.EnableFullResourceLogging; - if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) - { - if( tmp ) - { - Penumbra.ResourceLoader.EnableFullLogging(); - } - else - { - Penumbra.ResourceLoader.DisableFullLogging(); - } - - Penumbra.Config.EnableFullResourceLogging = tmp; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); - } - - private void DrawEnableDebugModeBox() - { - var tmp = Penumbra.Config.DebugMode; - if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) - { - if( tmp ) - { - Penumbra.ResourceLoader.EnableDebug(); - } - else - { - Penumbra.ResourceLoader.DisableDebug(); - } - - Penumbra.Config.DebugMode = tmp; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." ); - } - - private void DrawRequestedResourceLogging() - { - var tmp = Penumbra.Config.EnableResourceLogging; - if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) ) - { - _penumbra.ResourceLogger.SetState( tmp ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n" - + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" - + "Red boundary indicates invalid regex." ); - ImGui.SameLine(); - var tmpString = Penumbra.Config.ResourceLoggingFilter; - using var color = ImRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_penumbra.ResourceLogger.ValidRegex ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_penumbra.ResourceLogger.ValidRegex ); - if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) - { - _penumbra.ResourceLogger.SetFilter( tmpString ); - } - } - - private void DrawAdvancedSettings() - { - DrawRequestedResourceLogging(); - DrawDisableSoundStreamingBox(); - DrawEnableHttpApiBox(); - DrawReloadResourceButton(); - DrawEnableDebugModeBox(); - DrawEnableFullResourceLoggingBox(); - } - public void DrawSettingsTab() { using var tab = ImRaii.TabItem( "Settings" ); @@ -284,34 +176,15 @@ public partial class ConfigWindow return; } - DrawRootFolder(); - - DrawRediscoverButton(); - - ImGui.Dummy( _verticalSpace ); DrawEnabledBox(); - - ImGui.Dummy( _verticalSpace ); - DrawFolderSortType(); - DrawScaleModSelectorBox(); DrawShowAdvancedBox(); + ImGui.NewLine(); + DrawRootFolder(); + DrawRediscoverButton(); + ImGui.NewLine(); - if( Penumbra.Config.ShowAdvanced ) - { - DrawAdvancedSettings(); - } - - if( ImGui.CollapsingHeader( "Colors" ) ) - { - foreach( var color in Enum.GetValues< ColorId >() ) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; - if( ImGuiUtil.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) - { - Penumbra.Config.Save(); - } - } - } + DrawModSelectorSettings(); + DrawColorSettings(); + DrawAdvancedSettings(); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 58250e6b..338fa511 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -27,7 +27,7 @@ public sealed partial class ConfigWindow : Window, IDisposable RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2( 1024, 768 ), + MinimumSize = new Vector2( 800, 600 ), MaximumSize = new Vector2( 4096, 2160 ), }; } @@ -39,6 +39,7 @@ public sealed partial class ConfigWindow : Window, IDisposable DrawSettingsTab(); DrawModsTab(); DrawCollectionsTab(); + DrawChangedItemTab(); DrawEffectiveChangesTab(); DrawDebugTab(); DrawResourceManagerTab(); @@ -50,19 +51,14 @@ public sealed partial class ConfigWindow : Window, IDisposable } private static string GetLabel() - { - var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; - return version.Length == 0 + => Penumbra.Version.Length == 0 ? "Penumbra###PenumbraConfigWindow" - : $"Penumbra v{version}###PenumbraConfigWindow"; - } + : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; - private Vector2 _verticalSpace; private Vector2 _inputTextWidth; private void SetupSizes() { - _verticalSpace = new Vector2( 0, 20f * ImGuiHelpers.GlobalScale ); - _inputTextWidth = new Vector2( 450f * ImGuiHelpers.GlobalScale, 0 ); + _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); } } \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiFramedGroup.cs b/Penumbra/UI/Custom/ImGuiFramedGroup.cs deleted file mode 100644 index 0330eb32..00000000 --- a/Penumbra/UI/Custom/ImGuiFramedGroup.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiCustom - { - public static void BeginFramedGroup( string label ) - => BeginFramedGroupInternal( ref label, Vector2.Zero, false ); - - public static void BeginFramedGroup( string label, Vector2 minSize ) - => BeginFramedGroupInternal( ref label, minSize, false ); - - public static bool BeginFramedGroupEdit( ref string label ) - => BeginFramedGroupInternal( ref label, Vector2.Zero, true ); - - public static bool BeginFramedGroupEdit( ref string label, Vector2 minSize ) - => BeginFramedGroupInternal( ref label, minSize, true ); - - private static bool BeginFramedGroupInternal( ref string label, Vector2 minSize, bool edit ) - { - var itemSpacing = ImGui.GetStyle().ItemSpacing; - var frameHeight = ImGui.GetFrameHeight(); - var halfFrameHeight = new Vector2( ImGui.GetFrameHeight() / 2, 0 ); - - ImGui.BeginGroup(); // First group - - ImGui.PushStyleVar( ImGuiStyleVar.FramePadding, Vector2.Zero ); - ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - - ImGui.BeginGroup(); // Second group - - var effectiveSize = minSize; - if( effectiveSize.X < 0 ) - { - effectiveSize.X = ImGui.GetContentRegionAvail().X; - } - - // Ensure width. - ImGui.Dummy( Vector2.UnitX * effectiveSize.X ); - // Ensure left half boundary width/distance. - ImGui.Dummy( halfFrameHeight ); - - ImGui.SameLine(); - ImGui.BeginGroup(); // Third group. - // Ensure right half of boundary width/distance - ImGui.Dummy( halfFrameHeight ); - - // Label block - ImGui.SameLine(); - var ret = false; - if( edit ) - { - ret = ResizingTextInput( ref label, 1024 ); - } - else - { - ImGui.TextUnformatted( label ); - } - - var labelMin = ImGui.GetItemRectMin(); - var labelMax = ImGui.GetItemRectMax(); - ImGui.SameLine(); - // Ensure height and distance to label. - ImGui.Dummy( Vector2.UnitY * ( frameHeight + itemSpacing.Y ) ); - - ImGui.BeginGroup(); // Fourth Group. - - ImGui.PopStyleVar( 2 ); - - // This seems wrong? - //ImGui.SetWindowSize( new Vector2( ImGui.GetWindowSize().X - frameHeight, ImGui.GetWindowSize().Y ) ); - - var itemWidth = ImGui.CalcItemWidth(); - ImGui.PushItemWidth( Math.Max( 0f, itemWidth - frameHeight ) ); - - LabelStack.Add( ( labelMin, labelMax ) ); - return ret; - } - - private static void DrawClippedRect( Vector2 clipMin, Vector2 clipMax, Vector2 drawMin, Vector2 drawMax, uint color, float thickness ) - { - ImGui.PushClipRect( clipMin, clipMax, true ); - ImGui.GetWindowDrawList().AddRect( drawMin, drawMax, color, thickness ); - ImGui.PopClipRect(); - } - - public static void EndFramedGroup() - { - var borderColor = ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Border ] ); - var itemSpacing = ImGui.GetStyle().ItemSpacing; - var frameHeight = ImGui.GetFrameHeight(); - var halfFrameHeight = new Vector2( ImGui.GetFrameHeight() / 2, 0 ); - - ImGui.PopItemWidth(); - - ImGui.PushStyleVar( ImGuiStyleVar.FramePadding, Vector2.Zero ); - ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - - ImGui.EndGroup(); // Close fourth group - ImGui.EndGroup(); // Close third group - - ImGui.SameLine(); - // Ensure right distance. - ImGui.Dummy( halfFrameHeight ); - // Ensure bottom distance - ImGui.Dummy( Vector2.UnitY * ( frameHeight / 2 - itemSpacing.Y ) ); - ImGui.EndGroup(); // Close second group - - var itemMin = ImGui.GetItemRectMin(); - var itemMax = ImGui.GetItemRectMax(); - var (currentLabelMin, currentLabelMax) = LabelStack[ ^1 ]; - LabelStack.RemoveAt( LabelStack.Count - 1 ); - - var halfFrame = new Vector2( frameHeight / 8, frameHeight / 2 ); - currentLabelMin.X -= itemSpacing.X; - currentLabelMax.X += itemSpacing.X; - var frameMin = itemMin + halfFrame; - var frameMax = itemMax - Vector2.UnitX * halfFrame.X; - - // Left - DrawClippedRect( new Vector2( -float.MaxValue, -float.MaxValue ), new Vector2( currentLabelMin.X, float.MaxValue ), frameMin, - frameMax, borderColor, halfFrame.X ); - // Right - DrawClippedRect( new Vector2( currentLabelMax.X, -float.MaxValue ), new Vector2( float.MaxValue, float.MaxValue ), frameMin, - frameMax, borderColor, halfFrame.X ); - // Top - DrawClippedRect( new Vector2( currentLabelMin.X, -float.MaxValue ), new Vector2( currentLabelMax.X, currentLabelMin.Y ), frameMin, - frameMax, borderColor, halfFrame.X ); - // Bottom - DrawClippedRect( new Vector2( currentLabelMin.X, currentLabelMax.Y ), new Vector2( currentLabelMax.X, float.MaxValue ), frameMin, - frameMax, borderColor, halfFrame.X ); - - ImGui.PopStyleVar( 2 ); - // This seems wrong? - // ImGui.SetWindowSize( new Vector2( ImGui.GetWindowSize().X + frameHeight, ImGui.GetWindowSize().Y ) ); - ImGui.Dummy( Vector2.Zero ); - - ImGui.EndGroup(); // Close first group - } - - private static readonly List< (Vector2, Vector2) > LabelStack = new(); - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiRenameableCombo.cs b/Penumbra/UI/Custom/ImGuiRenameableCombo.cs deleted file mode 100644 index 2e81f125..00000000 --- a/Penumbra/UI/Custom/ImGuiRenameableCombo.cs +++ /dev/null @@ -1,54 +0,0 @@ -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiCustom - { - public static bool RenameableCombo( string label, ref int currentItem, out string newName, string[] items, int numItems ) - { - var ret = false; - newName = ""; - var newOption = ""; - if( !ImGui.BeginCombo( label, numItems > 0 ? items[ currentItem ] : newOption ) ) - { - return false; - } - - for( var i = 0; i < numItems; ++i ) - { - var isSelected = i == currentItem; - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( $"##{label}_{i}", ref items[ i ], 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - currentItem = i; - newName = items[ i ]; - ret = true; - ImGui.CloseCurrentPopup(); - } - - if( isSelected ) - { - ImGui.SetItemDefaultFocus(); - } - } - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( $"##{label}_new", "Add new item...", ref newOption, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - currentItem = numItems; - newName = newOption; - ret = true; - ImGui.CloseCurrentPopup(); - } - - if( numItems == 0 ) - { - ImGui.SetItemDefaultFocus(); - } - - ImGui.EndCombo(); - - return ret; - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiResizingTextInput.cs b/Penumbra/UI/Custom/ImGuiResizingTextInput.cs deleted file mode 100644 index f7ef2c53..00000000 --- a/Penumbra/UI/Custom/ImGuiResizingTextInput.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiCustom - { - public static bool InputOrText( bool editable, string label, ref string text, uint maxLength ) - { - if( editable ) - { - return ResizingTextInput( label, ref text, maxLength ); - } - - ImGui.Text( text ); - return false; - } - - public static bool ResizingTextInput( string label, ref string input, uint maxLength ) - => ResizingTextInputIntern( label, ref input, maxLength ).Item1; - - public static bool ResizingTextInput( ref string input, uint maxLength ) - { - var (ret, id) = ResizingTextInputIntern( $"##{input}", ref input, maxLength ); - if( ret ) - { - TextInputWidths.Remove( id ); - } - - return ret; - } - - private static (bool, uint) ResizingTextInputIntern( string label, ref string input, uint maxLength ) - { - var id = ImGui.GetID( label ); - if( !TextInputWidths.TryGetValue( id, out var width ) ) - { - width = ImGui.CalcTextSize( input ).X + 10; - } - - ImGui.SetNextItemWidth( width ); - var ret = ImGui.InputText( label, ref input, maxLength, ImGuiInputTextFlags.EnterReturnsTrue ); - TextInputWidths[ id ] = ImGui.CalcTextSize( input ).X + 10; - return ( ret, id ); - } - - private static readonly Dictionary< uint, float > TextInputWidths = new(); - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiUtil.cs b/Penumbra/UI/Custom/ImGuiUtil.cs deleted file mode 100644 index f2082e8d..00000000 --- a/Penumbra/UI/Custom/ImGuiUtil.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Numerics; -using System.Windows.Forms; -using Dalamud.Interface; -using ImGuiNET; -using Penumbra.GameData.ByteString; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiCustom - { - public static void CopyOnClickSelectable( string text ) - { - if( ImGui.Selectable( text ) ) - { - Clipboard.SetText( text ); - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( "Click to copy to clipboard." ); - } - } - - public static unsafe void CopyOnClickSelectable( Utf8String text ) - { - if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) - { - ImGuiNative.igSetClipboardText( text.Path ); - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( "Click to copy to clipboard." ); - } - } - } - - public static partial class ImGuiCustom - { - public static void VerticalDistance( float distance ) - { - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + distance * ImGuiHelpers.GlobalScale ); - } - - public static void RightJustifiedText( float pos, string text ) - { - ImGui.SetCursorPosX( pos - ImGui.CalcTextSize( text ).X - 2 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.Text( text ); - } - - public static void RightJustifiedLabel( float pos, string text ) - { - ImGui.SetCursorPosX( pos - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().ItemSpacing.X / 2 ); - ImGui.Text( text ); - ImGui.SameLine( pos ); - } - } - - public static partial class ImGuiCustom - { - public static void HoverTooltip( string text ) - { - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( text ); - } - } - } - - public static partial class ImGuiCustom - { - public static bool DisableButton( string label, bool condition ) - { - using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !condition ); - return ImGui.Button( label ) && condition; - } - } - - public static partial class ImGuiCustom - { - public static void PrintIcon( FontAwesomeIcon icon ) - { - ImGui.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( icon.ToIconString() ); - ImGui.PopFont(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/Raii/Color.cs b/Penumbra/UI/Custom/Raii/Color.cs deleted file mode 100644 index 95541bd8..00000000 --- a/Penumbra/UI/Custom/Raii/Color.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Numerics; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiRaii - { - public static Color PushColor( ImGuiCol idx, uint color, bool condition = true ) - => new Color().Push( idx, color, condition ); - - public static Color PushColor( ImGuiCol idx, Vector4 color, bool condition = true ) - => new Color().Push( idx, color, condition ); - - public class Color : IDisposable - { - private int _count; - - public Color Push( ImGuiCol idx, uint color, bool condition = true ) - { - if( condition ) - { - ImGui.PushStyleColor( idx, color ); - ++_count; - } - - return this; - } - - public Color Push( ImGuiCol idx, Vector4 color, bool condition = true ) - { - if( condition ) - { - ImGui.PushStyleColor( idx, color ); - ++_count; - } - - return this; - } - - public void Pop( int num = 1 ) - { - num = Math.Min( num, _count ); - _count -= num; - ImGui.PopStyleColor( num ); - } - - public void Dispose() - => Pop( _count ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/Raii/EndStack.cs b/Penumbra/UI/Custom/Raii/EndStack.cs deleted file mode 100644 index ea1e03f9..00000000 --- a/Penumbra/UI/Custom/Raii/EndStack.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiRaii - { - public static EndStack DeferredEnd( Action a, bool condition = true ) - => new EndStack().Push( a, condition ); - - public class EndStack : IDisposable - { - private readonly Stack< Action > _cleanActions = new(); - - public EndStack Push( Action a, bool condition = true ) - { - if( condition ) - { - _cleanActions.Push( a ); - } - - return this; - } - - - public EndStack Pop( int num = 1 ) - { - while( num-- > 0 && _cleanActions.TryPop( out var action ) ) - { - action.Invoke(); - } - - return this; - } - - public void Dispose() - => Pop( _cleanActions.Count ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/Raii/Font.cs b/Penumbra/UI/Custom/Raii/Font.cs deleted file mode 100644 index e9656d4d..00000000 --- a/Penumbra/UI/Custom/Raii/Font.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiRaii - { - public static Font PushFont( ImFontPtr font ) - => new( font ); - - public class Font : IDisposable - { - private int _count; - - public Font( ImFontPtr font ) - => Push( font ); - - public Font Push( ImFontPtr font ) - { - ImGui.PushFont( font ); - ++_count; - return this; - } - - public void Pop( int num = 1 ) - { - num = Math.Min( num, _count ); - _count -= num; - while( num-- > 0 ) - { - ImGui.PopFont(); - } - } - - public void Dispose() - => Pop( _count ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/Raii/Indent.cs b/Penumbra/UI/Custom/Raii/Indent.cs deleted file mode 100644 index da9abd8e..00000000 --- a/Penumbra/UI/Custom/Raii/Indent.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Diagnostics; -using Dalamud.Interface; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiRaii - { - public static Indent PushIndent( float f, bool scaled = true, bool condition = true ) - => new Indent().Push( f, condition ); - - public static Indent PushIndent( int i = 1, bool scaled = true, bool condition = true ) - => new Indent().Push( i, condition ); - - public class Indent : IDisposable - { - private float _indentation; - - public Indent Push( float indent, bool scaled = true, bool condition = true ) - { - Debug.Assert( indent >= 0f ); - if( condition ) - { - if( scaled ) - { - indent *= ImGuiHelpers.GlobalScale; - } - - ImGui.Indent( indent ); - _indentation += indent; - } - - return this; - } - - public Indent Push( uint i = 1, bool scaled = true, bool condition = true ) - { - if( condition ) - { - var spacing = i * ImGui.GetStyle().IndentSpacing * ( scaled ? ImGuiHelpers.GlobalScale : 1f ); - ImGui.Indent( spacing ); - _indentation += spacing; - } - - return this; - } - - public void Pop( float indent, bool scaled = true ) - { - if( scaled ) - { - indent *= ImGuiHelpers.GlobalScale; - } - - Debug.Assert( indent >= 0f ); - ImGui.Unindent( indent ); - _indentation -= indent; - } - - public void Pop( uint i, bool scaled = true ) - { - var spacing = i * ImGui.GetStyle().IndentSpacing * ( scaled ? ImGuiHelpers.GlobalScale : 1f ); - ImGui.Unindent( spacing ); - _indentation += spacing; - } - - public void Dispose() - => Pop( _indentation, false ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Custom/Raii/Style.cs b/Penumbra/UI/Custom/Raii/Style.cs deleted file mode 100644 index acc29ab6..00000000 --- a/Penumbra/UI/Custom/Raii/Style.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Numerics; -using ImGuiNET; - -namespace Penumbra.UI.Custom -{ - public static partial class ImGuiRaii - { - public static Style PushStyle( ImGuiStyleVar idx, float value, bool condition = true ) - => new Style().Push( idx, value, condition ); - - public static Style PushStyle( ImGuiStyleVar idx, Vector2 value, bool condition = true ) - => new Style().Push( idx, value, condition ); - - public class Style : IDisposable - { - private int _count; - - [System.Diagnostics.Conditional( "DEBUG" )] - private static void CheckStyleIdx( ImGuiStyleVar idx, Type type ) - { - var shouldThrow = idx switch - { - ImGuiStyleVar.Alpha => type != typeof( float ), - ImGuiStyleVar.WindowPadding => type != typeof( Vector2 ), - ImGuiStyleVar.WindowRounding => type != typeof( float ), - ImGuiStyleVar.WindowBorderSize => type != typeof( float ), - ImGuiStyleVar.WindowMinSize => type != typeof( Vector2 ), - ImGuiStyleVar.WindowTitleAlign => type != typeof( Vector2 ), - ImGuiStyleVar.ChildRounding => type != typeof( float ), - ImGuiStyleVar.ChildBorderSize => type != typeof( float ), - ImGuiStyleVar.PopupRounding => type != typeof( float ), - ImGuiStyleVar.PopupBorderSize => type != typeof( float ), - ImGuiStyleVar.FramePadding => type != typeof( Vector2 ), - ImGuiStyleVar.FrameRounding => type != typeof( float ), - ImGuiStyleVar.FrameBorderSize => type != typeof( float ), - ImGuiStyleVar.ItemSpacing => type != typeof( Vector2 ), - ImGuiStyleVar.ItemInnerSpacing => type != typeof( Vector2 ), - ImGuiStyleVar.IndentSpacing => type != typeof( float ), - ImGuiStyleVar.CellPadding => type != typeof( Vector2 ), - ImGuiStyleVar.ScrollbarSize => type != typeof( float ), - ImGuiStyleVar.ScrollbarRounding => type != typeof( float ), - ImGuiStyleVar.GrabMinSize => type != typeof( float ), - ImGuiStyleVar.GrabRounding => type != typeof( float ), - ImGuiStyleVar.TabRounding => type != typeof( float ), - ImGuiStyleVar.ButtonTextAlign => type != typeof( Vector2 ), - ImGuiStyleVar.SelectableTextAlign => type != typeof( Vector2 ), - _ => throw new ArgumentOutOfRangeException( nameof( idx ), idx, null ), - }; - - if( shouldThrow ) - { - throw new ArgumentException( $"Unable to push {type} to {idx}." ); - } - } - - public Style Push( ImGuiStyleVar idx, float value, bool condition = true ) - { - if( condition ) - { - CheckStyleIdx( idx, typeof( float ) ); - ImGui.PushStyleVar( idx, value ); - ++_count; - } - - return this; - } - - public Style Push( ImGuiStyleVar idx, Vector2 value, bool condition = true ) - { - if( condition ) - { - CheckStyleIdx( idx, typeof( Vector2 ) ); - ImGui.PushStyleVar( idx, value ); - ++_count; - } - - return this; - } - - public void Pop( int num = 1 ) - { - num = Math.Min( num, _count ); - _count -= num; - ImGui.PopStyleVar( num ); - } - - public void Dispose() - => Pop( _count ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 0033916f..425f7e3e 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -5,6 +5,8 @@ using ImGuiScene; namespace Penumbra.UI; +// A Launch Button used in the title screen of the game, +// using the Dalamud-provided collapsible submenu. public class LaunchButton : IDisposable { private readonly ConfigWindow _configWindow; diff --git a/Penumbra/UI/MenuTabs/TabBrowser.cs b/Penumbra/UI/MenuTabs/TabBrowser.cs deleted file mode 100644 index 1dc57a16..00000000 --- a/Penumbra/UI/MenuTabs/TabBrowser.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices; -using OtterGui.Raii; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public struct ModState -{ - public uint Color; -} - -public partial class SettingsInterface -{ - private class TabBrowser - { - private readonly ModFileSystemA _fileSystem; - private readonly ModFileSystemSelector _selector; - - public TabBrowser() - { - _fileSystem = ModFileSystemA.Load(); - _selector = new ModFileSystemSelector( _fileSystem, new HashSet() ); - } - - public void Draw() - { - using var ret = ImRaii.TabItem( "Available Mods" ); - if( !ret ) - { - return; - } - - _selector.Draw( 400 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs deleted file mode 100644 index 56bddabd..00000000 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabChangedItems - { - private const string LabelTab = "Changed Items"; - private readonly SettingsInterface _base; - - private string _filter = string.Empty; - private string _filterLower = string.Empty; - - public TabChangedItems( SettingsInterface ui ) - => _base = ui; - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - var items = Penumbra.CollectionManager.Default.ChangedItems; - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) - { - _filterLower = _filter.ToLowerInvariant(); - } - - if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - var list = items.AsEnumerable(); - - if( _filter.Length > 0 ) - { - list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); - } - - foreach( var (name, data) in list ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs deleted file mode 100644 index 760da897..00000000 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ /dev/null @@ -1,420 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Logging; -using ImGuiNET; -using Penumbra.Collections; -using Penumbra.Mods; -using Penumbra.UI.Custom; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabCollections - { - private const string CharacterCollectionHelpPopup = "Character Collection Information"; - private readonly Selector _selector; - private string _collectionNames = null!; - private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; - private int _currentCollectionIndex; - private int _currentDefaultIndex; - private readonly Dictionary< string, int > _currentCharacterIndices = new(); - private string _newCollectionName = string.Empty; - private string _newCharacterName = string.Empty; - - private void UpdateNames() - { - _collections = Penumbra.CollectionManager.Prepend( ModCollection.Empty ).ToArray(); - _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; - _collectionNamesWithNone = "None\0" + _collectionNames; - UpdateIndices(); - } - - - private int GetIndex( ModCollection collection ) - { - var ret = _collections.IndexOf( c => c.Name == collection.Name ); - if( ret < 0 ) - { - PluginLog.Error( $"Collection {collection.Name} is not found in collections." ); - return 0; - } - - return ret; - } - - private void UpdateIndex() - => _currentCollectionIndex = GetIndex( Penumbra.CollectionManager.Current ) - 1; - - public void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( Penumbra.CollectionManager.Default ); - - private void UpdateCharacterIndices() - { - _currentCharacterIndices.Clear(); - foreach( var (character, collection) in Penumbra.CollectionManager.Characters ) - { - _currentCharacterIndices[ character ] = GetIndex( collection ); - } - } - - private void UpdateIndices() - { - UpdateIndex(); - UpdateDefaultIndex(); - UpdateCharacterIndices(); - } - - public TabCollections( Selector selector ) - { - _selector = selector; - UpdateNames(); - } - - private void CreateNewCollection( bool duplicate ) - { - if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) - { - UpdateNames(); - SetCurrentCollection( Penumbra.CollectionManager[ _newCollectionName ]!, true ); - } - - _newCollectionName = string.Empty; - } - - private static void DrawCleanCollectionButton() - { - if( ImGui.Button( "Clean Settings" ) ) - { - Penumbra.CollectionManager.Current.CleanUnavailableSettings(); - } - - ImGuiCustom.HoverTooltip( - "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); - } - - private void DrawNewCollectionInput() - { - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of mods." ); - - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _newCollectionName.Length == 0 ); - - if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) - { - CreateNewCollection( false ); - } - - var hover = ImGui.IsItemHovered(); - ImGui.SameLine(); - if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) - { - CreateNewCollection( true ); - } - - hover |= ImGui.IsItemHovered(); - - style.Pop(); - if( _newCollectionName.Length == 0 && hover ) - { - ImGui.SetTooltip( "Please enter a name before creating a collection." ); - } - - var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; - ImGui.SameLine(); - if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) - { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); - SetCurrentCollection( Penumbra.CollectionManager.Current, true ); - UpdateNames(); - } - - if( !deleteCondition ) - { - ImGuiCustom.HoverTooltip( "You can not delete the default collection." ); - } - - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawCleanCollectionButton(); - } - } - - private void SetCurrentCollection( int idx, bool force ) - { - if( !force && idx == _currentCollectionIndex ) - { - return; - } - - Penumbra.CollectionManager.SetCollection( _collections[ idx + 1 ], ModCollection.Type.Current ); - _currentCollectionIndex = idx; - _selector.Cache.TriggerListReset(); - if( _selector.Mod != null ) - { - _selector.SelectModOnUpdate( _selector.Mod.Data.BasePath.Name ); - } - } - - public void SetCurrentCollection( ModCollection collection, bool force = false ) - { - var idx = Array.IndexOf( _collections, collection ) - 1; - if( idx >= 0 ) - { - SetCurrentCollection( idx, force ); - } - } - - public void DrawCurrentCollectionSelector( bool tooltip ) - { - var index = _currentCollectionIndex; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - var combo = ImGui.Combo( "Current Collection", ref index, _collectionNames ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); - - if( combo ) - { - SetCurrentCollection( index, false ); - } - } - - private static void DrawInheritance( ModCollection collection ) - { - ImGui.PushID( collection.Index ); - if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) ) - { - foreach( var inheritance in collection.Inheritance ) - { - DrawInheritance( inheritance ); - } - } - - ImGui.PopID(); - } - - private void DrawCurrentCollectionInheritance() - { - if( !ImGui.BeginListBox( "##inheritanceList", - new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) ) - { - return; - } - - using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox ); - DrawInheritance( _collections[ _currentCollectionIndex + 1 ] ); - } - - private static int _newInheritanceIdx = 0; - - private void DrawNewInheritanceSelection() - { - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); - if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) ) - { - using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); - foreach( var collection in Penumbra.CollectionManager ) - { - if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) ) - { - _newInheritanceIdx = collection.Index; - } - } - } - - ImGui.SameLine(); - var valid = _newInheritanceIdx > ModCollection.Empty.Index - && _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx - && _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid ); - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid ) - { - _collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] ); - } - - style.Pop(); - font.Pop(); - ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." ); - } - - private void DrawDefaultCollectionSelector() - { - var index = _currentDefaultIndex; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) - { - Penumbra.CollectionManager.SetCollection( _collections[ index ], ModCollection.Type.Default ); - _currentDefaultIndex = index; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" - + "They also take precedence before the forced collection." ); - - ImGui.SameLine(); - ImGui.Text( "Default Collection" ); - } - - private void DrawNewCharacterCollection() - { - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - ImGui.InputTextWithHint( "##New Character", "New Character Name", ref _newCharacterName, 32 ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Click Me for Information!" ); - ImGui.OpenPopupOnItemClick( CharacterCollectionHelpPopup, ImGuiPopupFlags.MouseButtonLeft ); - - ImGui.SameLine(); - if( ImGuiCustom.DisableButton( "Create New Character Collection", - _newCharacterName.Length > 0 && Penumbra.Config.HasReadCharacterCollectionDesc ) ) - { - Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); - _currentCharacterIndices[ _newCharacterName ] = 0; - _newCharacterName = string.Empty; - } - - ImGuiCustom.HoverTooltip( "Please enter a Character name before creating the collection.\n" - + "You also need to have read the help text for character collections." ); - - DrawCharacterCollectionHelp(); - } - - private static void DrawCharacterCollectionHelp() - { - var size = new Vector2( 700 * ImGuiHelpers.GlobalScale, 34 * ImGui.GetTextLineHeightWithSpacing() ); - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - ImGui.SetNextWindowSize( size, ImGuiCond.Appearing ); - var _ = true; - if( ImGui.BeginPopupModal( CharacterCollectionHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) - { - const string header = "Character Collections are a Hack! Use them at your own risk."; - using var end = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var textWidth = ImGui.CalcTextSize( header ).X; - ImGui.NewLine(); - ImGui.SetCursorPosX( ( size.X - textWidth ) / 2 ); - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, 0xFF0000B8 ); - ImGui.Text( header ); - color.Pop(); - ImGui.NewLine(); - ImGui.TextWrapped( - "Character Collections are collections that get applied whenever the named character gets redrawn by Penumbra," - + " whether by a manual '/penumbra redraw' command, or by the automatic redrawing feature.\n" - + "This means that they specifically require redrawing of a character to even apply, and thus can not work with mods that modify something that does not depend on characters being drawn, such as:\n" - + " - animations\n" - + " - sounds\n" - + " - most effects\n" - + " - most ui elements.\n" - + "They can also not work with actors that are not named, like the Character Preview or TryOn Actors, and they can not work in cutscenes, since redrawing in cutscenes would cancel all animations.\n" - + "They also do not work with every character customization (like skin, tattoo, hair, etc. changes) since those are not always re-requested by the game on redrawing a player. They may work, they may not, you need to test it.\n" - + "\n" - + "Due to the nature of meta manipulating mods, you can not mix meta manipulations inside a Character (or the Default) collection with meta manipulations inside the Forced collection.\n" - + "\n" - + "To verify that you have actually read this, you need to hold control and shift while clicking the Understood button for it to take effect.\n" - + "Due to the nature of redrawing being a hack, weird things (or maybe even crashes) may happen when using Character Collections. The way this works is:\n" - + " - Penumbra queues a redraw of an actor.\n" - + " - When the redraw queue reaches that actor, the actor gets undrawn (turned invisible).\n" - + " - Penumbra checks the actors name and if it matches a Character Collection, it replaces the Default collection with that one.\n" - + " - Penumbra triggers the redraw of that actor. The game requests files.\n" - + " - Penumbra potentially redirects those file requests to the modded files in the active collection, which is either Default or Character. (Or, afterwards, Forced).\n" - + " - The actor is drawn.\n" - + " - Penumbra returns the active collection to the Default Collection.\n" - + "If any of those steps fails, or if the file requests take too long, it may happen that a character is drawn with half of its models from the Default and the other half from the Character Collection, or a modded Model is loaded, but not its corresponding modded textures, which lets it stay invisible, or similar problems." ); - - var buttonSize = ImGuiHelpers.ScaledVector2( 150, 0 ); - var offset = ( size.X - buttonSize.X ) / 2; - ImGui.SetCursorPos( new Vector2( offset, size.Y - 3 * ImGui.GetTextLineHeightWithSpacing() ) ); - var state = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; - color.Push( ImGuiCol.ButtonHovered, 0xFF00A000, state ); - if( ImGui.Button( "Understood!", buttonSize ) ) - { - if( state && !Penumbra.Config.HasReadCharacterCollectionDesc ) - { - Penumbra.Config.HasReadCharacterCollectionDesc = true; - Penumbra.Config.Save(); - } - - ImGui.CloseCurrentPopup(); - } - } - } - - - private void DrawCharacterCollectionSelectors() - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); - if( !ImGui.BeginChild( "##CollectionChild", AutoFillSize, true ) ) - { - return; - } - - DrawDefaultCollectionSelector(); - - foreach( var (name, collection) in Penumbra.CollectionManager.Characters.ToArray() ) - { - var idx = _currentCharacterIndices[ name ]; - var tmp = idx; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) - { - Penumbra.CollectionManager.SetCollection( _collections[ tmp ], ModCollection.Type.Character, name ); - _currentCharacterIndices[ name ] = tmp; - } - - ImGui.SameLine(); - - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}", Vector2.One * ImGui.GetFrameHeight() ) ) - { - Penumbra.CollectionManager.RemoveCharacterCollection( name ); - } - - font.Pop(); - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( name ); - } - - DrawNewCharacterCollection(); - } - - public void Draw() - { - if( !ImGui.BeginTabItem( "Collections" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) - .Push( ImGui.EndChild ); - - if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ) ) - { - DrawCurrentCollectionSelector( true ); - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawNewCollectionInput(); - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawCurrentCollectionInheritance(); - DrawNewInheritanceSelection(); - } - - raii.Pop(); - - DrawCharacterCollectionSelectors(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.Model.cs b/Penumbra/UI/MenuTabs/TabDebug.Model.cs deleted file mode 100644 index 527a265d..00000000 --- a/Penumbra/UI/MenuTabs/TabDebug.Model.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - [StructLayout( LayoutKind.Explicit )] - private unsafe struct RenderModel - { - [FieldOffset( 0x18 )] - public RenderModel* PreviousModel; - - [FieldOffset( 0x20 )] - public RenderModel* NextModel; - - [FieldOffset( 0x30 )] - public ResourceHandle* ResourceHandle; - - [FieldOffset( 0x40 )] - public Skeleton* Skeleton; - - [FieldOffset( 0x58 )] - public void** BoneList; - - [FieldOffset( 0x60 )] - public int BoneListCount; - - [FieldOffset( 0x68 )] - private void* UnkDXBuffer1; - - [FieldOffset( 0x70 )] - private void* UnkDXBuffer2; - - [FieldOffset( 0x78 )] - private void* UnkDXBuffer3; - - [FieldOffset( 0x90 )] - public void** Materials; - - [FieldOffset( 0x98 )] - public int MaterialCount; - } - - [StructLayout( LayoutKind.Explicit )] - private unsafe struct Material - { - [FieldOffset( 0x10 )] - public ResourceHandle* ResourceHandle; - - [FieldOffset( 0x28 )] - public void* MaterialData; - - [FieldOffset( 0x48 )] - public Texture* Tex1; - - [FieldOffset( 0x60 )] - public Texture* Tex2; - - [FieldOffset( 0x78 )] - public Texture* Tex3; - } - - private static unsafe void DrawPlayerModelInfo() - { - var player = Dalamud.ClientState.LocalPlayer; - var name = player?.Name.ToString() ?? "NULL"; - if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - if( !ImGui.BeginTable( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) - { - ImGui.Text( imc->FileName.ToString() ); - } - - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - continue; - } - - ImGui.TableNextColumn(); - if( mdl != null ) - { - ImGui.Text( mdl->ResourceHandle->FileName.ToString() ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs deleted file mode 100644 index 18353834..00000000 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Numerics; -using ImGuiNET; -using OtterGui.Raii; -using Penumbra.Api; -using Penumbra.UI.Custom; -using CharacterUtility = Penumbra.Interop.CharacterUtility; -using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private string ImGuiIdTester = string.Empty; - - private void DrawImGuiIdTester() - { - ImGui.SetNextItemWidth( 200 ); - ImGui.InputText( "##abc1", ref ImGuiIdTester, 32 ); - ImGui.SameLine(); - ImGui.Text( ImGui.GetID( ImGuiIdTester ).ToString( "X" ) ); - } - - - private static void PrintValue( string name, string value ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( name ); - ImGui.TableNextColumn(); - ImGui.Text( value ); - } - - private void DrawDebugTabGeneral() - { - if( !ImGui.CollapsingHeader( "General##Debug" ) ) - { - return; - } - - if( !ImGui.BeginTable( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - var manager = Penumbra.ModManager; - PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); - PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); - PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); - PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Path Resolver Enabled", _penumbra.PathResolver.Enabled.ToString() ); - //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); - } - - private void DrawDebugTabIpc() - { - if( !ImGui.CollapsingHeader( "IPC##Debug" ) ) - { - return; - } - - var ipc = _penumbra.Ipc; - ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); - ImGui.Text( "Available subscriptions:" ); - using var indent = ImGuiRaii.PushIndent(); - if( ipc.ProviderApiVersion != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); - } - - if( ipc.ProviderRedrawName != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); - } - - if( ipc.ProviderRedrawObject != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); - } - - if( ipc.ProviderRedrawAll != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); - } - - if( ipc.ProviderResolveDefault != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); - } - - if( ipc.ProviderResolveCharacter != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); - } - - if( ipc.ProviderChangedItemTooltip != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); - } - - if( ipc.ProviderChangedItemClick != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); - } - } - - private void DrawDebugTabMissingFiles() - { - if( !ImGui.CollapsingHeader( "Missing Files##Debug" ) ) - { - return; - } - - if( !Penumbra.CollectionManager.Current.HasCache - || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - foreach( var file in Penumbra.CollectionManager.Current.MissingFiles ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - if( ImGui.Selectable( file.FullName ) ) - { - ImGui.SetClipboardText( file.FullName ); - } - - ImGuiCustom.HoverTooltip( "Click to copy to clipboard." ); - } - } - - private unsafe void DrawDebugTabReplacedResources() - { - if( !ImGui.CollapsingHeader( "Replaced Resources##Debug" ) ) - { - return; - } - - Penumbra.ResourceLoader.UpdateDebugInfo(); - - if( Penumbra.ResourceLoader.DebugList.Count == 0 - || !ImGui.BeginTable( "##ReplacedResourcesDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) - { - return; - } - - using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) - { - var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; - var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; - ImGui.TableNextColumn(); - ImGui.Text( data.ManipulatedPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( refCountManip.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( data.OriginalPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( refCountOrig.ToString() ); - } - } - - public unsafe void DrawDebugCharacterUtility() - { - if( !ImGui.CollapsingHeader( "Character Utility##Debug" ) ) - { - return; - } - - if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) - { - return; - } - - using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) - { - var idx = CharacterUtility.RelevantIndices[ i ]; - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; - ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( resource->FileName(), resource->FileName() + resource->FileNameLength ); - ImGui.TableNextColumn(); - ImGui.Text( $"0x{resource->GetData().Data:X}" ); - if( ImGui.IsItemClicked() ) - { - var (data, length) = resource->GetData(); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - - ImGui.TableNextColumn(); - ImGui.Text( $"{resource->GetData().Length}" ); - ImGui.TableNextColumn(); - ImGui.Text( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, - Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - - ImGui.TableNextColumn(); - ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); - } - } - - - public unsafe void DrawDebugResidentResources() - { - if( !ImGui.CollapsingHeader( "Resident Resources##Debug" ) ) - { - return; - } - - if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) - { - return; - } - - if( !ImGui.BeginTable( "##Resident ResourcesDebugList", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) - { - return; - } - - using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) - { - var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( resource->FileName(), resource->FileName() + resource->FileNameLength ); - } - } - - private unsafe void DrawPathResolverDebug() - { - if( !ImGui.CollapsingHeader( "Path Resolver##Debug" ) ) - { - return; - } - - if( ImGui.TreeNodeEx( "Draw Object to Object" ) ) - { - using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - if( ImGui.BeginTable( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ) ) - { - end.Push( ImGui.EndTable ); - foreach( var (ptr, (c, idx)) in _penumbra.PathResolver.DrawObjectToObject ) - { - ImGui.TableNextColumn(); - ImGui.Text( ptr.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( idx.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); - ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); - ImGui.TableNextColumn(); - ImGui.Text( c.Name ); - } - } - } - - if( ImGui.TreeNodeEx( "Path Collections" ) ) - { - using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - if( ImGui.BeginTable( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ) ) - { - end.Push( ImGui.EndTable ); - foreach( var (path, collection) in _penumbra.PathResolver.PathCollections ) - { - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); - ImGui.TableNextColumn(); - ImGui.Text( collection.Name ); - } - } - } - } - - private void DrawDebugTabUtility() - { - if( !ImGui.CollapsingHeader( "Utilities##Debug" ) ) - { - return; - } - - DrawImGuiIdTester(); - } - - private void DrawDebugTab() - { - if( !ImGui.BeginTabItem( "Debug Tab" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginChild( "##DebugChild", -Vector2.One ) ) - { - ImGui.EndChild(); - return; - } - - raii.Push( ImGui.EndChild ); - - DrawDebugTabGeneral(); - ImGui.NewLine(); - DrawDebugTabReplacedResources(); - ImGui.NewLine(); - DrawResourceProblems(); - ImGui.NewLine(); - DrawDebugTabMissingFiles(); - ImGui.NewLine(); - DrawPlayerModelInfo(); - ImGui.NewLine(); - DrawPathResolverDebug(); - ImGui.NewLine(); - DrawDebugCharacterUtility(); - ImGui.NewLine(); - DrawDebugResidentResources(); - ImGui.NewLine(); - DrawDebugTabIpc(); - ImGui.NewLine(); - DrawDebugTabUtility(); - ImGui.NewLine(); - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs deleted file mode 100644 index e9a962e7..00000000 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using Penumbra.Collections; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.UI.Custom; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabEffective - { - private const string LabelTab = "Effective Changes"; - - private LowerString _gamePathFilter = LowerString.Empty; - private LowerString _filePathFilter = LowerString.Empty; - - private const float LeftTextLength = 600; - - private float _arrowLength = 0; - - private static void DrawLine( Utf8GamePath path, FullPath name ) - { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( path.Path ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.SameLine(); - ImGuiCustom.CopyOnClickSelectable( name.InternalName ); - } - - private static void DrawLine( string path, string name ) - { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( path ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.SameLine(); - ImGuiCustom.CopyOnClickSelectable( name ); - } - - private void DrawFilters() - { - if( _arrowLength == 0 ) - { - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; - } - - ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale ); - var tmp = _gamePathFilter.Text; - if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref tmp, 256 ) ) - { - _gamePathFilter = tmp; - } - - ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - tmp = _filePathFilter.Text; - if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref tmp, 256 ) ) - { - _filePathFilter = tmp; - } - } - - private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) - { - if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) - { - return false; - } - - return _filePathFilter.Length == 0 || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilter.Lower ); - } - - private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp ) - { - if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) - { - return false; - } - - return _filePathFilter.Length == 0 || kvp.Value.ToString().Contains( _filePathFilter.Lower ); - } - - private bool CheckFilters( (string, LowerString) kvp ) - { - if( _gamePathFilter.Length > 0 && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilter.Lower ) ) - { - return false; - } - - return _filePathFilter.Length == 0 || kvp.Item2.Contains( _filePathFilter.Lower ); - } - - private void DrawFilteredRows( ModCollection active ) - { - foreach( var (gp, fp) in active.ResolvedFiles.Where( CheckFilters ) ) - { - DrawLine( gp, fp ); - } - - var cache = active.MetaCache; - if( cache == null ) - { - return; - } - - foreach( var (mp, mod) in cache.Cmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - - foreach( var (mp, mod) in cache.Eqp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - - foreach( var (mp, mod) in cache.Eqdp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - - foreach( var (mp, mod) in cache.Gmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - - foreach( var (mp, mod) in cache.Est.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - - foreach( var (mp, mod) in cache.Imc.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - } - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - DrawFilters(); - - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - var resolved = Penumbra.CollectionManager.Default.ResolvedFiles; - var meta = Penumbra.CollectionManager.Default.MetaCache; - var metaCount = meta?.Count ?? 0; - var resolvedCount = resolved.Count; - - var totalLines = resolvedCount + metaCount; - if( totalLines == 0 ) - { - return; - } - - if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, LeftTextLength * ImGuiHelpers.GlobalScale ); - - if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 ) - { - DrawFilteredRows( Penumbra.CollectionManager.Default ); - } - else - { - ImGuiListClipperPtr clipper; - unsafe - { - clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); - } - - clipper.Begin( totalLines ); - - - while( clipper.Step() ) - { - for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) - { - var row = actualRow; - ImGui.TableNextRow(); - if( row < resolvedCount ) - { - var (gamePath, file) = resolved.ElementAt( row ); - DrawLine( gamePath, file ); - } - else if( ( row -= resolved.Count ) < metaCount ) - { - // TODO - //var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( 0.ToString(), 0.ToString() ); - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabImport.cs b/Penumbra/UI/MenuTabs/TabImport.cs deleted file mode 100644 index 4b49f88a..00000000 --- a/Penumbra/UI/MenuTabs/TabImport.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; -using System.Windows.Forms; -using Dalamud.Logging; -using ImGuiNET; -using Penumbra.Importer; -using Penumbra.UI.Custom; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabImport - { - private const string LabelTab = "Import Mods"; - private const string LabelImportButton = "Import TexTools Modpacks"; - private const string LabelFileDialog = "Pick one or more modpacks."; - private const string LabelFileImportRunning = "Import in progress..."; - private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; - private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; - - private const uint ColorRed = 0xFF0000C8; - private const uint ColorYellow = 0xFF00C8C8; - - private static readonly Vector2 ImportBarSize = new(-1, 0); - - private bool _isImportRunning; - private string _errorMessage = string.Empty; - private TexToolsImport? _texToolsImport; - private readonly SettingsInterface _base; - - public readonly HashSet< string > NewMods = new(); - - public TabImport( SettingsInterface ui ) - => _base = ui; - - public bool IsImporting() - => _isImportRunning; - - private void RunImportTask() - { - _isImportRunning = true; - Task.Run( async () => - { - try - { - var picker = new OpenFileDialog - { - Multiselect = true, - Filter = FileTypeFilter, - CheckFileExists = true, - Title = LabelFileDialog, - }; - - var result = await picker.ShowDialogAsync(); - - if( result == DialogResult.OK ) - { - _errorMessage = string.Empty; - - foreach( var fileName in picker.FileNames ) - { - PluginLog.Information( $"-> {fileName} START" ); - - try - { - _texToolsImport = new TexToolsImport( Penumbra.ModManager.BasePath ); - var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); - if( dir.Name.Any() ) - { - NewMods.Add( dir.Name ); - } - - PluginLog.Information( $"-> {fileName} OK!" ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); - _errorMessage = ex.Message; - } - } - - var directory = _texToolsImport?.ExtractedDirectory; - _texToolsImport = null; - _base.ReloadMods(); - if( directory != null ) - { - _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); - } - } - } - catch( Exception e ) - { - PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); - } - - _isImportRunning = false; - } ); - } - - private void DrawImportButton() - { - if( !Penumbra.ModManager.Valid ) - { - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( LabelImportButton ); - style.Pop(); - - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( "Can not import since the mod directory path is not valid." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); - color.Pop(); - - ImGui.Text( "Please set the mod directory in the settings tab." ); - ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); - color.Push( ImGuiCol.Text, ColorYellow ); - ImGui.Text( " D:\\ffxivmods" ); - color.Pop(); - ImGui.Text( "You can return to this tab once you've done that." ); - } - else if( ImGui.Button( LabelImportButton ) ) - { - RunImportTask(); - } - } - - private void DrawImportProgress() - { - ImGui.Button( LabelFileImportRunning ); - - if( _texToolsImport == null ) - { - return; - } - - switch( _texToolsImport.State ) - { - case ImporterState.None: break; - case ImporterState.WritingPackToDisk: - ImGui.Text( TooltipModpack1 ); - break; - case ImporterState.ExtractingModFiles: - { - var str = - $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - - ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); - break; - } - case ImporterState.Done: break; - default: throw new ArgumentOutOfRangeException(); - } - } - - private void DrawFailedImportMessage() - { - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); - } - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !_isImportRunning ) - { - DrawImportButton(); - } - else - { - DrawImportProgress(); - } - - if( _errorMessage.Any() ) - { - DrawFailedImportMessage(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs deleted file mode 100644 index be4bf261..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Logging; -using OtterGui; -using Penumbra.Mods; -using Penumbra.UI.Classes; -using Penumbra.Util; - -namespace Penumbra.UI; - -public class ModListCache : IDisposable -{ - private readonly Mod.Manager _manager; - - private readonly List< FullMod > _modsInOrder = new(); - private readonly List< (bool visible, uint color) > _visibleMods = new(); - private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); - private readonly IReadOnlySet< string > _newMods; - - private LowerString _modFilter = LowerString.Empty; - private LowerString _modFilterAuthor = LowerString.Empty; - private LowerString _modFilterChanges = LowerString.Empty; - - private bool _listResetNecessary; - private bool _filterResetNecessary; - - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - - public ModFilter StateFilter - { - get => _stateFilter; - set - { - var diff = _stateFilter != value; - _stateFilter = value; - if( diff ) - { - TriggerFilterReset(); - } - } - } - - public ModListCache( Mod.Manager manager, IReadOnlySet< string > newMods ) - { - _manager = manager; - _newMods = newMods; - ResetModList(); - ModFileSystem.ModFileSystemChanged += TriggerListReset; - } - - public void Dispose() - { - ModFileSystem.ModFileSystemChanged -= TriggerListReset; - } - - public int Count - => _modsInOrder.Count; - - - public bool Update() - { - if( _listResetNecessary ) - { - ResetModList(); - return true; - } - - if( _filterResetNecessary ) - { - ResetFilters(); - return true; - } - - return false; - } - - public void TriggerListReset() - => _listResetNecessary = true; - - public void TriggerFilterReset() - => _filterResetNecessary = true; - - public void RemoveMod( FullMod mod ) - { - var idx = _modsInOrder.IndexOf( mod ); - if( idx >= 0 ) - { - _modsInOrder.RemoveAt( idx ); - _visibleMods.RemoveAt( idx ); - UpdateFolders(); - } - } - - private void SetFolderAndParentsVisible( ModFolder? folder ) - { - while( folder != null && ( !_visibleFolders.TryGetValue( folder, out var state ) || !state.visible ) ) - { - _visibleFolders[ folder ] = ( true, true ); - folder = folder.Parent; - } - } - - private void UpdateFolders() - { - _visibleFolders.Clear(); - - for( var i = 0; i < _modsInOrder.Count; ++i ) - { - if( _visibleMods[ i ].visible ) - { - SetFolderAndParentsVisible( _modsInOrder[ i ].Data.Order.ParentFolder ); - } - } - } - - public void SetTextFilter( string filter ) - { - var lower = filter.ToLowerInvariant(); - if( lower.StartsWith( "c:" ) ) - { - _modFilterChanges = lower[ 2.. ]; - _modFilter = LowerString.Empty; - _modFilterAuthor = LowerString.Empty; - } - else if( lower.StartsWith( "a:" ) ) - { - _modFilterAuthor = lower[ 2.. ]; - _modFilter = LowerString.Empty; - _modFilterChanges = LowerString.Empty; - } - else - { - _modFilter = lower; - _modFilterAuthor = LowerString.Empty; - _modFilterChanges = LowerString.Empty; - } - - ResetFilters(); - } - - private void ResetModList() - { - _modsInOrder.Clear(); - _visibleMods.Clear(); - _visibleFolders.Clear(); - - PluginLog.Debug( "Resetting mod selector list..." ); - if( _modsInOrder.Count == 0 ) - { - foreach( var modData in _manager.StructuredMods.AllMods( Penumbra.Config.SortFoldersFirst ) ) - { - var idx = Penumbra.ModManager.Mods.IndexOf( modData ); - var mod = new FullMod( Penumbra.CollectionManager.Current[ idx ].Settings ?? ModSettings.DefaultSettings( modData.Meta ), - modData ); - _modsInOrder.Add( mod ); - _visibleMods.Add( CheckFilters( mod ) ); - } - } - - _listResetNecessary = false; - _filterResetNecessary = false; - } - - private void ResetFilters() - { - _visibleMods.Clear(); - _visibleFolders.Clear(); - PluginLog.Debug( "Resetting mod selector filters..." ); - foreach( var mod in _modsInOrder ) - { - _visibleMods.Add( CheckFilters( mod ) ); - } - - _filterResetNecessary = false; - } - - public (FullMod? mod, int idx) GetModByName( string name ) - { - for( var i = 0; i < Count; ++i ) - { - if( _modsInOrder[ i ].Data.Meta.Name == name ) - { - return ( _modsInOrder[ i ], i ); - } - } - - return ( null, 0 ); - } - - public (FullMod? mod, int idx) GetModByBasePath( string basePath ) - { - for( var i = 0; i < Count; ++i ) - { - if( _modsInOrder[ i ].Data.BasePath.Name == basePath ) - { - return ( _modsInOrder[ i ], i ); - } - } - - return ( null, 0 ); - } - - public (bool visible, bool enabled) GetFolder( ModFolder folder ) - => _visibleFolders.TryGetValue( folder, out var ret ) ? ret : ( false, false ); - - public (FullMod?, bool visible, uint color) GetMod( int idx ) - => idx >= 0 && idx < _modsInOrder.Count - ? ( _modsInOrder[ idx ], _visibleMods[ idx ].visible, _visibleMods[ idx ].color ) - : ( null, false, 0 ); - - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) - { - if( count == 0 ) - { - if( StateFilter.HasFlag( hasNoFlag ) ) - { - return false; - } - } - else if( StateFilter.HasFlag( hasFlag ) ) - { - return false; - } - - return true; - } - - private (bool, uint) CheckFilters( FullMod mod ) - { - var ret = ( false, 0u ); - - if( _modFilter.Length > 0 && !mod.Data.Meta.Name.Contains( _modFilter ) ) - { - return ret; - } - - if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.Author.Contains( _modFilterAuthor ) ) - { - return ret; - } - - if( _modFilterChanges.Length > 0 && !mod.Data.LowerChangedItemsString.Contains( _modFilterChanges ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, - ModFilter.HasMetaManipulations ) ) - { - return ret; - } - - if( CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) ) - { - return ret; - } - - var isNew = _newMods.Contains( mod.Data.BasePath.Name ); - if( CheckFlags( isNew ? 1 : 0, ModFilter.IsNew, ModFilter.NotNew ) ) - { - return ret; - } - - if( !mod.Settings.Enabled ) - { - if( !StateFilter.HasFlag( ModFilter.Disabled ) || !StateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return ret; - } - - ret.Item2 = ret.Item2 == 0 ? ColorId.DisabledMod.Value() : ret.Item2; - } - - if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) - { - return ret; - } - - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Data.Index ).ToList(); - if( conflicts.Count > 0 ) - { - if( conflicts.Any( c => !c.Solved ) ) - { - if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return ret; - } - - ret.Item2 = ret.Item2 == 0 ? ColorId.ConflictingMod.Value() : ret.Item2; - } - else - { - if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return ret; - } - - ret.Item2 = ret.Item2 == 0 ? ColorId.HandledConflictMod.Value() : ret.Item2; - } - } - else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return ret; - } - - ret.Item1 = true; - if( isNew ) - { - ret.Item2 = ColorId.NewMod.Value(); - } - - SetFolderAndParentsVisible( mod.Data.Order.ParentFolder ); - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs deleted file mode 100644 index beecf9b6..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabInstalled - { - private const string LabelTab = "Installed Mods"; - - public readonly Selector Selector; - public readonly ModPanel ModPanel; - - public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) - { - Selector = new Selector( ui, newMods ); - ModPanel = new ModPanel( ui, Selector, newMods ); - } - - public void Draw() - { - var ret = ImGui.BeginTabItem( LabelTab ); - if( !ret ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - Selector.Draw(); - ImGui.SameLine(); - ModPanel.Draw(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs deleted file mode 100644 index 66d40c65..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ /dev/null @@ -1,713 +0,0 @@ -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; -using Penumbra.Meta; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.UI.Custom; -using Penumbra.Util; -using ImGui = ImGuiNET.ImGui; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private partial class PluginDetails - { - private const string LabelPluginDetails = "PenumbraPluginDetails"; - private const string LabelAboutTab = "About"; - private const string LabelChangedItemsTab = "Changed Items"; - private const string LabelChangedItemsHeader = "##changedItems"; - private const string LabelConflictsTab = "Mod Conflicts"; - private const string LabelConflictsHeader = "##conflicts"; - private const string LabelFileSwapTab = "File Swaps"; - private const string LabelFileSwapHeader = "##fileSwaps"; - private const string LabelFileListTab = "Files"; - private const string LabelFileListHeader = "##fileList"; - private const string LabelGroupSelect = "##groupSelect"; - private const string LabelOptionSelect = "##optionSelect"; - private const string LabelConfigurationTab = "Configuration"; - - private const string TooltipFilesTab = - "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" - + "Yellow files are restricted to some options."; - - private const float OptionSelectionWidth = 140f; - private const float CheckMarkSize = 50f; - private const uint ColorDarkGreen = 0xFF00A000; - private const uint ColorGreen = 0xFF00C800; - private const uint ColorYellow = 0xFF00C8C8; - private const uint ColorDarkRed = 0xFF0000A0; - private const uint ColorRed = 0xFF0000C8; - - - private bool _editMode; - private int _selectedGroupIndex; - private OptionGroup? _selectedGroup; - private int _selectedOptionIndex; - private Option? _selectedOption; - private string _currentGamePaths = ""; - - private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList; - - private readonly Selector _selector; - private readonly SettingsInterface _base; - - private void SelectGroup( int idx ) - { - // Not using the properties here because we need it to be not null forgiving in this case. - var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; - _selectedGroupIndex = idx; - if( _selectedGroupIndex >= numGroups ) - { - _selectedGroupIndex = 0; - } - - if( numGroups > 0 ) - { - _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; - } - else - { - _selectedGroup = null; - } - } - - private void SelectGroup() - => SelectGroup( _selectedGroupIndex ); - - private void SelectOption( int idx ) - { - _selectedOptionIndex = idx; - if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) - { - _selectedOptionIndex = 0; - } - - if( _selectedGroup?.Options.Count > 0 ) - { - _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; - } - else - { - _selectedOption = null; - } - } - - private void SelectOption() - => SelectOption( _selectedOptionIndex ); - - public void ResetState() - { - _fullFilenameList = null; - SelectGroup(); - SelectOption(); - } - - public PluginDetails( SettingsInterface ui, Selector s ) - { - _base = ui; - _selector = s; - ResetState(); - } - - // This is only drawn when we have a mod selected, so we can forgive nulls. - private FullMod Mod - => _selector.Mod!; - - private ModMeta Meta - => Mod.Data.Meta; - - private void DrawAboutTab() - { - if( !_editMode && Meta.Description.Length == 0 ) - { - return; - } - - if( !ImGui.BeginTabItem( LabelAboutTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var desc = Meta.Description; - var flags = _editMode - ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine - : ImGuiInputTextFlags.ReadOnly; - - if( _editMode ) - { - if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, - AutoFillSize, flags ) ) - { - Meta.Description = desc; - _selector.SaveCurrentMod(); - } - - ImGuiCustom.HoverTooltip( TooltipAboutEdit ); - } - else - { - ImGui.TextWrapped( desc ); - } - } - - private void DrawChangedItemsTab() - { - if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - foreach( var (name, data) in Mod.Data.ChangedItems ) - { - _base.DrawChangedItem( name, data ); - } - } - - private void DrawConflictTab() - { - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( Mod.Data.Index ).ToList(); - if( conflicts.Count == 0 || !ImGui.BeginTabItem( LabelConflictsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - using var indent = ImGuiRaii.PushIndent( 0 ); - Mods.Mod? oldBadMod = null; - foreach( var conflict in conflicts ) - { - var badMod = Penumbra.ModManager[ conflict.Mod2 ]; - if( badMod != oldBadMod ) - { - if( oldBadMod != null ) - { - indent.Pop( 30f ); - } - - if( ImGui.Selectable( badMod.Meta.Name ) ) - { - _selector.SelectModByDir( badMod.BasePath.Name ); - } - - ImGui.SameLine(); - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorGreen : ColorRed ); - ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); - - indent.Push( 30f ); - } - - if( conflict.Data is Utf8GamePath p ) - { - unsafe - { - ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); - } - } - else if( conflict.Data is MetaManipulation m ) - { - ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); - } - - oldBadMod = badMod; - } - } - - private void DrawFileSwapTab() - { - if( _editMode ) - { - DrawFileSwapTabEdit(); - return; - } - - if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - foreach( var (source, target) in Meta.FileSwaps ) - { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( source.Path ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); - - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( target.InternalName ); - - ImGui.TableNextRow(); - } - } - - private void UpdateFilenameList() - { - if( _fullFilenameList != null ) - { - return; - } - - _fullFilenameList = Mod.Data.Resources.ModFiles - .Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) - .ToArray(); - - if( Meta.Groups.Count == 0 ) - { - return; - } - - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - foreach( var group in Meta.Groups.Values ) - { - var inAll = true; - foreach( var option in group.Options ) - { - if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) - { - _fullFilenameList[ i ].color = ColorYellow; - } - else - { - inAll = false; - } - } - - if( inAll && group.SelectionType == SelectType.Single ) - { - _fullFilenameList[ i ].color = ColorGreen; - } - } - } - } - - private void DrawFileListTab() - { - if( !ImGui.BeginTabItem( LabelFileListTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - ImGuiCustom.HoverTooltip( TooltipFilesTab ); - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) - { - raii.Push( ImGui.EndListBox ); - UpdateFilenameList(); - using var colorRaii = new ImGuiRaii.Color(); - foreach( var (name, _, color, _) in _fullFilenameList! ) - { - colorRaii.Push( ImGuiCol.Text, color ); - ImGui.Selectable( name.FullName ); - colorRaii.Pop(); - } - } - else - { - _fullFilenameList = null; - } - } - - private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders ) - { - removeFolders = 0; - var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) ); - if( defaultIndex < 0 ) - { - return defaultIndex; - } - - var path = gamePaths[ defaultIndex ].Path; - if( path.Length == TextDefaultGamePath.Length ) - { - return defaultIndex; - } - - if( path[ TextDefaultGamePath.Length ] != ( byte )'-' - || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) ) - { - return -1; - } - - return defaultIndex; - } - - private void HandleSelectedFilesButton( bool remove ) - { - if( _selectedOption == null ) - { - return; - } - - var option = ( Option )_selectedOption; - - var gamePaths = _currentGamePaths.Split( ';' ) - .Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray(); - if( gamePaths.Length == 0 ) - { - return; - } - - var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); - var changed = false; - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - if( !_fullFilenameList![ i ].selected ) - { - continue; - } - - _fullFilenameList![ i ].selected = false; - var relName = _fullFilenameList[ i ].relName; - if( defaultIndex >= 0 ) - { - gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); - } - - if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) - { - if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) - { - changed = true; - } - - if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) - { - changed = true; - } - } - else - { - changed = gamePaths - .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); - } - } - - if( changed ) - { - _fullFilenameList = null; - _selector.SaveCurrentMod(); - var idx = Penumbra.ModManager.Mods.IndexOf( Mod.Data ); - // Since files may have changed, we need to recompute effective files. - foreach( var collection in Penumbra.CollectionManager - .Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( false, collection == Penumbra.CollectionManager.Default ); - } - - // If the mod is enabled in the current collection, its conflicts may have changed. - if( Mod.Settings.Enabled ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - private void DrawAddToGroupButton() - { - if( ImGui.Button( ButtonAddToGroup ) ) - { - HandleSelectedFilesButton( false ); - } - } - - private void DrawRemoveFromGroupButton() - { - if( ImGui.Button( ButtonRemoveFromGroup ) ) - { - HandleSelectedFilesButton( true ); - } - } - - private void DrawGamePathInput() - { - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, - 128 ); - ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); - } - - private void DrawGroupRow() - { - if( _selectedGroup == null ) - { - SelectGroup(); - } - - if( _selectedOption == null ) - { - SelectOption(); - } - - if( !DrawEditGroupSelector() ) - { - return; - } - - ImGui.SameLine(); - if( !DrawEditOptionSelector() ) - { - return; - } - - ImGui.SameLine(); - DrawAddToGroupButton(); - ImGui.SameLine(); - DrawRemoveFromGroupButton(); - ImGui.SameLine(); - DrawGamePathInput(); - } - - private void DrawFileAndGamePaths( int idx ) - { - void Selectable( uint colorNormal, uint colorReplace ) - { - var loc = _fullFilenameList![ idx ].color; - if( loc == colorNormal ) - { - loc = colorReplace; - } - - using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); - ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); - } - - const float indentWidth = 30f; - if( _selectedOption == null ) - { - Selectable( 0, ColorGreen ); - return; - } - - var fileName = _fullFilenameList![ idx ].relName; - var optionFiles = ( ( Option )_selectedOption ).OptionFiles; - if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) - { - Selectable( 0, ColorGreen ); - - using var indent = ImGuiRaii.PushIndent( indentWidth ); - foreach( var gamePath in gamePaths.ToArray() ) - { - var tmp = gamePath.ToString(); - var old = tmp; - if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) - && tmp != old ) - { - gamePaths.Remove( gamePath ); - if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) ) - { - gamePaths.Add( p ); - } - else if( gamePaths.Count == 0 ) - { - optionFiles.Remove( fileName ); - } - - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); - } - } - } - else - { - Selectable( ColorYellow, ColorRed ); - } - } - - private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) - { - var enabled = ( flag & ( 1 << idx ) ) != 0; - var oldEnabled = enabled; - if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) - { - Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, - Mod.Settings.Settings[ group.GroupName ] ^ ( 1 << idx ) ); - // If the mod is enabled, recalculate files and filters. - if( Mod.Settings.Enabled ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - private void DrawMultiSelector( OptionGroup group ) - { - if( group.Options.Count == 0 ) - { - return; - } - - ImGuiCustom.BeginFramedGroup( group.GroupName ); - using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); - for( var i = 0; i < group.Options.Count; ++i ) - { - DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], - $"{group.Options[ i ].OptionName}##{group.GroupName}" ); - } - } - - private void DrawSingleSelector( OptionGroup group ) - { - if( group.Options.Count < 2 ) - { - return; - } - - var code = Mod.Settings.Settings[ group.GroupName ]; - if( ImGui.Combo( group.GroupName, ref code - , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) - && code != Mod.Settings.Settings[ group.GroupName ] ) - { - Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, code ); - if( Mod.Settings.Enabled ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - private void DrawGroupSelectors() - { - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) - { - DrawSingleSelector( g ); - } - - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) - { - DrawMultiSelector( g ); - } - } - - private void DrawConfigurationTab() - { - if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - if( _editMode ) - { - DrawGroupSelectorsEdit(); - } - else - { - DrawGroupSelectors(); - } - } - - private void DrawMetaManipulationsTab() - { - if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - - var manips = Mod.Data.Resources.MetaManipulations; - var changes = false; - if( _editMode || manips.DefaultData.Count > 0 ) - { - if( ImGui.CollapsingHeader( "Default" ) ) - { - changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); - } - } - - foreach( var (groupName, group) in manips.GroupData ) - { - foreach( var (optionName, option) in group ) - { - if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) - { - changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); - } - } - } - - if( changes ) - { - Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); - Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); - _selector.ReloadCurrentMod( true, false ); - } - } - - public void Draw( bool editMode ) - { - _editMode = editMode; - if( !ImGui.BeginTabBar( LabelPluginDetails ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); - DrawAboutTab(); - DrawChangedItemsTab(); - - DrawConfigurationTab(); - if( _editMode ) - { - DrawFileListTabEdit(); - } - else - { - DrawFileListTab(); - } - - DrawFileSwapTab(); - DrawMetaManipulationsTab(); - DrawConflictTab(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs deleted file mode 100644 index 7a80f150..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ /dev/null @@ -1,380 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; -using Penumbra.Mods; -using Penumbra.UI.Custom; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private partial class PluginDetails - { - private const string LabelDescEdit = "##descedit"; - private const string LabelNewSingleGroupEdit = "##newSingleGroup"; - private const string LabelNewMultiGroup = "##newMultiGroup"; - private const string LabelGamePathsEditBox = "##gamePathsEdit"; - private const string ButtonAddToGroup = "Add to Group"; - private const string ButtonRemoveFromGroup = "Remove from Group"; - private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; - private const string TextNoOptionAvailable = "[Not Available]"; - private const string TextDefaultGamePath = "default"; - private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true ); - private const char GamePathsSeparator = ';'; - - private static readonly string TooltipFilesTabEdit = - $"{TooltipFilesTab}\n" - + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; - - private static readonly string TooltipGamePathsEdit = - $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" - + $"Use '{TextDefaultGamePath}' to add the original file path." - + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; - - private const float MultiEditBoxWidth = 300f; - - private bool DrawEditGroupSelector() - { - ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); - if( Meta!.Groups.Count == 0 ) - { - ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 ); - return false; - } - - if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex - , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() - , Meta.Groups.Count ) ) - { - SelectGroup(); - SelectOption( 0 ); - } - - return true; - } - - private bool DrawEditOptionSelector() - { - ImGui.SameLine(); - ImGui.SetNextItemWidth( OptionSelectionWidth ); - if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 ) - { - ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); - return false; - } - - var group = ( OptionGroup )_selectedGroup!; - if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), - group.Options.Count ) ) - { - SelectOption(); - } - - return true; - } - - private void DrawFileListTabEdit() - { - if( ImGui.BeginTabItem( LabelFileListTab ) ) - { - UpdateFilenameList(); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); - } - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) ) - { - for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) - { - DrawFileAndGamePaths( i ); - } - } - - ImGui.EndListBox(); - - DrawGroupRow(); - ImGui.EndTabItem(); - } - else - { - _fullFilenameList = null; - } - } - - private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group ) - { - var groupName = group.GroupName; - if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) - { - if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - - return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); - } - - private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) - { - var newOption = ""; - ImGui.SetCursorPosX( nameBoxStart ); - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) - && newOption.Length != 0 ) - { - group.Options.Add( new Option() - { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } ); - _selector.SaveCurrentMod(); - if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - private void DrawMultiSelectorEdit( OptionGroup group ) - { - var nameBoxStart = CheckMarkSize; - var flag = Mod!.Settings.Settings[ group.GroupName ]; - - using var raii = DrawMultiSelectorEditBegin( group ); - for( var i = 0; i < group.Options.Count; ++i ) - { - var opt = group.Options[ i ]; - var label = $"##{group.GroupName}_{i}"; - DrawMultiSelectorCheckBox( group, i, flag, label ); - - ImGui.SameLine(); - var newName = opt.OptionName; - - if( nameBoxStart == CheckMarkSize ) - { - nameBoxStart = ImGui.GetCursorPosX(); - } - - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - if( newName.Length == 0 ) - { - Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); - } - else if( newName != opt.OptionName ) - { - group.Options[ i ] = new Option() - { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; - _selector.SaveCurrentMod(); - } - - if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - DrawMultiSelectorEditAdd( group, nameBoxStart ); - } - - private void DrawSingleSelectorEditGroup( OptionGroup group ) - { - var groupName = group.GroupName; - if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - } - - private float DrawSingleSelectorEdit( OptionGroup group ) - { - var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; - var code = oldSetting; - if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, - group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) - { - if( code == group.Options.Count ) - { - if( newName.Length > 0 ) - { - Penumbra.CollectionManager.Current.SetModSetting(Mod.Data.Index, group.GroupName, code); - group.Options.Add( new Option() - { - OptionName = newName, - OptionDesc = "", - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - } ); - _selector.SaveCurrentMod(); - } - } - else - { - if( newName.Length == 0 ) - { - Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); - } - else - { - if( newName != group.Options[ code ].OptionName ) - { - group.Options[ code ] = new Option() - { - OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, - OptionFiles = group.Options[ code ].OptionFiles, - }; - _selector.SaveCurrentMod(); - } - } - } - - if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) - { - _selector.Cache.TriggerFilterReset(); - } - } - - ImGui.SameLine(); - var labelEditPos = ImGui.GetCursorPosX(); - DrawSingleSelectorEditGroup( group ); - - return labelEditPos; - } - - private void DrawAddSingleGroupField( float labelEditPos ) - { - var newGroup = ""; - ImGui.SetCursorPosX( labelEditPos ); - if( labelEditPos == CheckMarkSize ) - { - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - } - - if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); - // Adds empty group, so can not change filters. - } - } - - private void DrawAddMultiGroupField() - { - var newGroup = ""; - ImGui.SetCursorPosX( CheckMarkSize ); - ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); - // Adds empty group, so can not change filters. - } - } - - private void DrawGroupSelectorsEdit() - { - var labelEditPos = CheckMarkSize; - var groups = Meta.Groups.Values.ToArray(); - foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) - { - labelEditPos = DrawSingleSelectorEdit( g ); - } - - DrawAddSingleGroupField( labelEditPos ); - - foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) - { - DrawMultiSelectorEdit( g ); - } - - DrawAddMultiGroupField(); - } - - private void DrawFileSwapTabEdit() - { - if( !ImGui.BeginTabItem( LabelFileSwapTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - - var swaps = Meta.FileSwaps.Keys.ToArray(); - - ImGui.PushFont( UiBuilder.IconFont ); - var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X; - ImGui.PopFont(); - - var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; - for( var idx = 0; idx < swaps.Length + 1; ++idx ) - { - var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ]; - var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ]; - var keyString = key.ToString(); - var valueString = value.ToString(); - - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString, - GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 ) - { - if( idx < swaps.Length ) - { - Meta.FileSwaps.Remove( key ); - } - - if( !newKey.IsEmpty ) - { - Meta.FileSwaps[ newKey ] = value; - } - - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); - } - } - - if( idx >= swaps.Length ) - { - continue; - } - - ImGui.SameLine(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); - ImGui.SameLine(); - - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString, - GamePath.MaxGamePathLength, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - var newValue = new FullPath( valueString.ToLowerInvariant() ); - if( newValue.CompareTo( value ) != 0 ) - { - Meta.FileSwaps[ key ] = newValue; - _selector.SaveCurrentMod(); - _selector.Cache.TriggerListReset(); - } - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs deleted file mode 100644 index 8331ceb8..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ /dev/null @@ -1,773 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; -using Penumbra.UI.Custom; -using ObjectType = Penumbra.GameData.Enums.ObjectType; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private partial class PluginDetails - { - private int _newManipTypeIdx = 0; - private ushort _newManipSetId = 0; - private ushort _newManipSecondaryId = 0; - private int _newManipSubrace = 0; - private int _newManipRace = 0; - private int _newManipAttribute = 0; - private int _newManipEquipSlot = 0; - private int _newManipObjectType = 0; - private int _newManipGender = 0; - private int _newManipBodySlot = 0; - private ushort _newManipVariant = 0; - - - private static readonly (string, EquipSlot)[] EqpEquipSlots = - { - ( "Head", EquipSlot.Head ), - ( "Body", EquipSlot.Body ), - ( "Hands", EquipSlot.Hands ), - ( "Legs", EquipSlot.Legs ), - ( "Feet", EquipSlot.Feet ), - }; - - private static readonly (string, EquipSlot)[] EqdpEquipSlots = - { - EqpEquipSlots[ 0 ], - EqpEquipSlots[ 1 ], - EqpEquipSlots[ 2 ], - EqpEquipSlots[ 3 ], - EqpEquipSlots[ 4 ], - ( "Ears", EquipSlot.Ears ), - ( "Neck", EquipSlot.Neck ), - ( "Wrist", EquipSlot.Wrists ), - ( "Left Finger", EquipSlot.LFinger ), - ( "Right Finger", EquipSlot.RFinger ), - }; - - private static readonly (string, ModelRace)[] Races = - { - ( ModelRace.Midlander.ToName(), ModelRace.Midlander ), - ( ModelRace.Highlander.ToName(), ModelRace.Highlander ), - ( ModelRace.Elezen.ToName(), ModelRace.Elezen ), - ( ModelRace.Miqote.ToName(), ModelRace.Miqote ), - ( ModelRace.Roegadyn.ToName(), ModelRace.Roegadyn ), - ( ModelRace.Lalafell.ToName(), ModelRace.Lalafell ), - ( ModelRace.AuRa.ToName(), ModelRace.AuRa ), - ( ModelRace.Viera.ToName(), ModelRace.Viera ), - ( ModelRace.Hrothgar.ToName(), ModelRace.Hrothgar ), - }; - - private static readonly (string, Gender)[] Genders = - { - ( Gender.Male.ToName(), Gender.Male ), - ( Gender.Female.ToName(), Gender.Female ), - ( Gender.MaleNpc.ToName(), Gender.MaleNpc ), - ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), - }; - - private static readonly (string, EstManipulation.EstType)[] EstTypes = - { - ( "Hair", EstManipulation.EstType.Hair ), - ( "Face", EstManipulation.EstType.Face ), - ( "Body", EstManipulation.EstType.Body ), - ( "Head", EstManipulation.EstType.Head ), - }; - - private static readonly (string, SubRace)[] Subraces = - { - ( SubRace.Midlander.ToName(), SubRace.Midlander ), - ( SubRace.Highlander.ToName(), SubRace.Highlander ), - ( SubRace.Wildwood.ToName(), SubRace.Wildwood ), - ( SubRace.Duskwight.ToName(), SubRace.Duskwight ), - ( SubRace.SeekerOfTheSun.ToName(), SubRace.SeekerOfTheSun ), - ( SubRace.KeeperOfTheMoon.ToName(), SubRace.KeeperOfTheMoon ), - ( SubRace.Seawolf.ToName(), SubRace.Seawolf ), - ( SubRace.Hellsguard.ToName(), SubRace.Hellsguard ), - ( SubRace.Plainsfolk.ToName(), SubRace.Plainsfolk ), - ( SubRace.Dunesfolk.ToName(), SubRace.Dunesfolk ), - ( SubRace.Raen.ToName(), SubRace.Raen ), - ( SubRace.Xaela.ToName(), SubRace.Xaela ), - ( SubRace.Rava.ToName(), SubRace.Rava ), - ( SubRace.Veena.ToName(), SubRace.Veena ), - ( SubRace.Helion.ToName(), SubRace.Helion ), - ( SubRace.Lost.ToName(), SubRace.Lost ), - }; - - private static readonly (string, RspAttribute)[] RspAttributes = - { - ( RspAttribute.MaleMinSize.ToFullString(), RspAttribute.MaleMinSize ), - ( RspAttribute.MaleMaxSize.ToFullString(), RspAttribute.MaleMaxSize ), - ( RspAttribute.FemaleMinSize.ToFullString(), RspAttribute.FemaleMinSize ), - ( RspAttribute.FemaleMaxSize.ToFullString(), RspAttribute.FemaleMaxSize ), - ( RspAttribute.BustMinX.ToFullString(), RspAttribute.BustMinX ), - ( RspAttribute.BustMaxX.ToFullString(), RspAttribute.BustMaxX ), - ( RspAttribute.BustMinY.ToFullString(), RspAttribute.BustMinY ), - ( RspAttribute.BustMaxY.ToFullString(), RspAttribute.BustMaxY ), - ( RspAttribute.BustMinZ.ToFullString(), RspAttribute.BustMinZ ), - ( RspAttribute.BustMaxZ.ToFullString(), RspAttribute.BustMaxZ ), - ( RspAttribute.MaleMinTail.ToFullString(), RspAttribute.MaleMinTail ), - ( RspAttribute.MaleMaxTail.ToFullString(), RspAttribute.MaleMaxTail ), - ( RspAttribute.FemaleMinTail.ToFullString(), RspAttribute.FemaleMinTail ), - ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), - }; - - - private static readonly (string, ObjectType)[] ImcObjectType = - { - ( "Equipment", ObjectType.Equipment ), - ( "Customization", ObjectType.Character ), - ( "Weapon", ObjectType.Weapon ), - ( "Demihuman", ObjectType.DemiHuman ), - ( "Monster", ObjectType.Monster ), - }; - - private static readonly (string, BodySlot)[] ImcBodySlots = - { - ( "Hair", BodySlot.Hair ), - ( "Face", BodySlot.Face ), - ( "Body", BodySlot.Body ), - ( "Tail", BodySlot.Tail ), - ( "Ears", BodySlot.Zear ), - }; - - private static bool PrintCheckBox( string name, ref bool value, bool def ) - { - var color = value == def ? 0 : value ? ColorDarkGreen : ColorDarkRed; - if( color == 0 ) - { - return ImGui.Checkbox( name, ref value ); - } - - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color ); - var ret = ImGui.Checkbox( name, ref value ); - return ret; - } - - private bool RestrictedInputInt( string name, ref ushort value, ushort min, ushort max ) - { - int tmp = value; - if( ImGui.InputInt( name, ref tmp, 0, 0, _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - && tmp != value - && tmp >= min - && tmp <= max ) - { - value = ( ushort )tmp; - return true; - } - - return false; - } - - private static bool DefaultButton< T >( string name, ref T value, T defaultValue ) where T : IComparable< T > - { - var compare = defaultValue.CompareTo( value ); - var color = compare < 0 ? ColorDarkGreen : - compare > 0 ? ColorDarkRed : ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Button ] ); - - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Button, color ); - var ret = ImGui.Button( name, Vector2.UnitX * 120 ) && compare != 0; - ImGui.SameLine(); - return ret; - } - - private bool DrawInputWithDefault( string name, ref ushort value, ushort defaultValue, ushort max ) - => DefaultButton( $"{( _editMode ? "Set to " : "" )}Default: {defaultValue}##imc{name}", ref value, defaultValue ) - || RestrictedInputInt( name, ref value, 0, max ); - - private static bool CustomCombo< T >( string label, IList< (string, T) > namesAndValues, out T value, ref int idx ) - { - value = idx < namesAndValues.Count ? namesAndValues[ idx ].Item2 : default!; - - if( !ImGui.BeginCombo( label, idx < namesAndValues.Count ? namesAndValues[ idx ].Item1 : string.Empty ) ) - { - return false; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); - - for( var i = 0; i < namesAndValues.Count; ++i ) - { - if( !ImGui.Selectable( $"{namesAndValues[ i ].Item1}##{label}{i}", idx == i ) || idx == i ) - { - continue; - } - - idx = i; - value = namesAndValues[ i ].Item2; - return true; - } - - return false; - } - - private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Eqp; - var val = id.Entry; - - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ExpandedEqpFile.GetDefault( id.SetId ); - var attributes = Eqp.EqpAttributes[ id.Slot ]; - - foreach( var flag in attributes ) - { - var name = flag.ToLocalName(); - var tmp = val.HasFlag( flag ); - if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) - { - list[ manipIdx ] = new MetaManipulation( new EqpManipulation( tmp ? val | flag : val & ~flag, id.Slot, id.SetId ) ); - ret = true; - } - } - } - - ImGui.Text( ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Slot.ToString() ); - return ret; - } - - private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Gmp; - var val = id.Entry; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ExpandedGmpFile.GetDefault( id.SetId ); - var enabled = val.Enabled; - var animated = val.Animated; - var rotationA = val.RotationA; - var rotationB = val.RotationB; - var rotationC = val.RotationC; - ushort unk = val.UnknownTotal; - - ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; - ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); - ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); - ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); - ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); - ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); - - if( ret && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( new GmpManipulation( new GmpEntry - { - Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, - RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, - }, id.SetId ) ); - } - } - - ImGui.Text( ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( EquipSlot.Head.ToString() ); - return ret; - } - - private static (bool, bool) GetEqdpBits( EquipSlot slot, EqdpEntry entry ) - { - return slot switch - { - EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), - EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), - EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), - EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), - EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), - EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), - EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), - EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), - EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), - EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), - _ => ( false, false ), - }; - } - - private static EqdpEntry SetEqdpBits( EquipSlot slot, EqdpEntry value, bool bit1, bool bit2 ) - { - switch( slot ) - { - case EquipSlot.Head: - value = bit1 ? value | EqdpEntry.Head1 : value & ~EqdpEntry.Head1; - value = bit2 ? value | EqdpEntry.Head2 : value & ~EqdpEntry.Head2; - return value; - case EquipSlot.Body: - value = bit1 ? value | EqdpEntry.Body1 : value & ~EqdpEntry.Body1; - value = bit2 ? value | EqdpEntry.Body2 : value & ~EqdpEntry.Body2; - return value; - case EquipSlot.Hands: - value = bit1 ? value | EqdpEntry.Hands1 : value & ~EqdpEntry.Hands1; - value = bit2 ? value | EqdpEntry.Hands2 : value & ~EqdpEntry.Hands2; - return value; - case EquipSlot.Legs: - value = bit1 ? value | EqdpEntry.Legs1 : value & ~EqdpEntry.Legs1; - value = bit2 ? value | EqdpEntry.Legs2 : value & ~EqdpEntry.Legs2; - return value; - case EquipSlot.Feet: - value = bit1 ? value | EqdpEntry.Feet1 : value & ~EqdpEntry.Feet1; - value = bit2 ? value | EqdpEntry.Feet2 : value & ~EqdpEntry.Feet2; - return value; - case EquipSlot.Neck: - value = bit1 ? value | EqdpEntry.Neck1 : value & ~EqdpEntry.Neck1; - value = bit2 ? value | EqdpEntry.Neck2 : value & ~EqdpEntry.Neck2; - return value; - case EquipSlot.Ears: - value = bit1 ? value | EqdpEntry.Ears1 : value & ~EqdpEntry.Ears1; - value = bit2 ? value | EqdpEntry.Ears2 : value & ~EqdpEntry.Ears2; - return value; - case EquipSlot.Wrists: - value = bit1 ? value | EqdpEntry.Wrists1 : value & ~EqdpEntry.Wrists1; - value = bit2 ? value | EqdpEntry.Wrists2 : value & ~EqdpEntry.Wrists2; - return value; - case EquipSlot.RFinger: - value = bit1 ? value | EqdpEntry.RingR1 : value & ~EqdpEntry.RingR1; - value = bit2 ? value | EqdpEntry.RingR2 : value & ~EqdpEntry.RingR2; - return value; - case EquipSlot.LFinger: - value = bit1 ? value | EqdpEntry.RingL1 : value & ~EqdpEntry.RingL1; - value = bit2 ? value | EqdpEntry.RingL2 : value & ~EqdpEntry.RingL2; - return value; - } - - return value; - } - - private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Eqdp; - var val = id.Entry; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ExpandedEqdpFile.GetDefault( id.FileIndex(), id.SetId ); - var (bit1, bit2) = GetEqdpBits( id.Slot, val ); - var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); - - ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); - ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); - - if( ret && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( new EqdpManipulation( SetEqdpBits( id.Slot, val, bit1, bit2 ), id.Slot, id.Gender, - id.Race, id.SetId ) ); - } - } - - ImGui.Text( id.Slot.IsAccessory() - ? ObjectType.Accessory.ToString() - : ObjectType.Equipment.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Slot.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Race.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Gender.ToString() ); - return ret; - } - - private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Est; - var val = id.Entry; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = EstFile.GetDefault( id.Slot, Names.CombinedRace( id.Gender, id.Race ), id.SetId ); - if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( new EstManipulation( id.Gender, id.Race, id.Slot, id.SetId, val ) ); - ret = true; - } - } - - ImGui.Text( id.Slot.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.SetId.ToString() ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( id.Race.ToName() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Gender.ToName() ); - - return ret; - } - - private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Imc; - var val = id.Entry; - - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = new ImcFile( id.GamePath() ).GetEntry( ImcFile.PartIndex( id.EquipSlot ), id.Variant ); - ushort materialId = val.MaterialId; - ushort vfxId = val.VfxId; - ushort decalId = val.DecalId; - var soundId = ( ushort )val.SoundId; - var attributeMask = val.AttributeMask; - var materialAnimationId = ( ushort )val.MaterialAnimationId; - ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); - ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); - ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); - ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, - byte.MaxValue ); - - if( ret && _editMode ) - { - var value = new ImcEntry( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, ( byte )vfxId, - ( byte )materialAnimationId ); - list[ manipIdx ] = new MetaManipulation( new ImcManipulation( id, value ) ); - } - } - - ImGui.Text( id.ObjectType.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.PrimaryId.ToString() ); - ImGui.TableNextColumn(); - if( id.ObjectType is ObjectType.Accessory or ObjectType.Equipment ) - { - ImGui.Text( id.ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? id.EquipSlot.ToString() - : id.BodySlot.ToString() ); - } - - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - if( id.ObjectType != ObjectType.Equipment - && id.ObjectType != ObjectType.Accessory ) - { - ImGui.Text( id.SecondaryId.ToString() ); - } - - ImGui.TableNextColumn(); - ImGui.Text( id.Variant.ToString() ); - return ret; - } - - private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) - { - var ret = false; - var id = list[ manipIdx ].Rsp; - var defaults = CmpFile.GetDefault( id.SubRace, id.Attribute ); - var val = id.Entry; - if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - if( DefaultButton( - $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) - && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); - ret = true; - } - - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", - _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) - && val >= 0 - && val <= 5 - && _editMode ) - { - list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); - ret = true; - } - } - - ImGui.Text( id.Attribute.ToUngenderedString() ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.Text( id.SubRace.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( id.Attribute.ToGender().ToString() ); - return ret; - } - - private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) - { - var type = list[ manipIdx ].ManipulationType; - - if( _editMode ) - { - ImGui.TableNextColumn(); - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##manipDelete{manipIdx}" ) ) - { - list.RemoveAt( manipIdx ); - ImGui.TableNextRow(); - --manipIdx; - --count; - return true; - } - } - - ImGui.TableNextColumn(); - ImGui.Text( type.ToString() ); - ImGui.TableNextColumn(); - - var changes = false; - switch( type ) - { - case MetaManipulation.Type.Eqp: - changes = DrawEqpRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Eqp.Entry}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - case MetaManipulation.Type.Gmp: - changes = DrawGmpRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Gmp.Entry.Value}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - case MetaManipulation.Type.Eqdp: - changes = DrawEqdpRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - var (bit1, bit2) = GetEqdpBits( list[ manipIdx ].Eqdp.Slot, list[ manipIdx ].Eqdp.Entry ); - if( ImGui.Selectable( $"{bit1} {bit2}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - case MetaManipulation.Type.Est: - changes = DrawEstRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Est.Entry}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - case MetaManipulation.Type.Imc: - changes = DrawImcRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Imc.Entry.MaterialId}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - case MetaManipulation.Type.Rsp: - changes = DrawRspRow( manipIdx, list ); - ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Rsp.Entry}##{manipIdx}" ) ) - { - ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); - } - - break; - } - - - ImGui.TableNextRow(); - return changes; - } - - - private MetaManipulation.Type DrawNewTypeSelection() - { - ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); - ImGui.SameLine(); - ImGui.RadioButton( "EQDP##newManipType", ref _newManipTypeIdx, 2 ); - ImGui.SameLine(); - ImGui.RadioButton( "EQP##newManipType", ref _newManipTypeIdx, 3 ); - ImGui.SameLine(); - ImGui.RadioButton( "EST##newManipType", ref _newManipTypeIdx, 4 ); - ImGui.SameLine(); - ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); - ImGui.SameLine(); - ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); - return ( MetaManipulation.Type )_newManipTypeIdx; - } - - private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) - { - var change = false; - if( !ImGui.BeginPopup( popupName ) ) - { - return change; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var manipType = DrawNewTypeSelection(); - MetaManipulation? newManip = null; - switch( manipType ) - { - case MetaManipulation.Type.Imc: - { - RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); - RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); - CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); - ImcManipulation imc = new(); - switch( objectType ) - { - case ObjectType.Equipment: - CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - imc = new ImcManipulation( equipSlot, _newManipVariant, _newManipSetId, new ImcEntry() ); - break; - case ObjectType.DemiHuman: - case ObjectType.Weapon: - case ObjectType.Monster: - RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); - CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); - imc = new ImcManipulation( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, - _newManipVariant, new ImcEntry() ); - break; - } - - newManip = new MetaManipulation( new ImcManipulation( imc.ObjectType, imc.BodySlot, imc.PrimaryId, imc.SecondaryId, - imc.Variant, imc.EquipSlot, ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ) ) ); - - break; - } - case MetaManipulation.Type.Eqdp: - { - RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - CustomCombo( "Race", Races, out var race, ref _newManipRace ); - CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - var eqdp = new EqdpManipulation( new EqdpEntry(), equipSlot, gender, race, _newManipSetId ); - newManip = new MetaManipulation( new EqdpManipulation( ExpandedEqdpFile.GetDefault( eqdp.FileIndex(), eqdp.SetId ), - equipSlot, gender, race, _newManipSetId ) ); - break; - } - case MetaManipulation.Type.Eqp: - { - RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); - newManip = new MetaManipulation( new EqpManipulation( ExpandedEqpFile.GetDefault( _newManipSetId ) & Eqp.Mask( equipSlot ), - equipSlot, _newManipSetId ) ); - break; - } - case MetaManipulation.Type.Est: - { - RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); - CustomCombo( "Est Type", EstTypes, out var estType, ref _newManipObjectType ); - CustomCombo( "Race", Races, out var race, ref _newManipRace ); - CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); - newManip = new MetaManipulation( new EstManipulation( gender, race, estType, _newManipSetId, - EstFile.GetDefault( estType, Names.CombinedRace( gender, race ), _newManipSetId ) ) ); - break; - } - case MetaManipulation.Type.Gmp: - RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); - newManip = new MetaManipulation( new GmpManipulation( ExpandedGmpFile.GetDefault( _newManipSetId ), _newManipSetId ) ); - break; - case MetaManipulation.Type.Rsp: - CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); - CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); - newManip = new MetaManipulation( new RspManipulation( subRace, rspAttribute, - CmpFile.GetDefault( subRace, rspAttribute ) ) ); - break; - } - - if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) - && newManip != null - && list.All( m => !m.Equals( newManip ) ) ) - { - list.Add( newManip.Value ); - change = true; - ++count; - ImGui.CloseCurrentPopup(); - } - - return change; - } - - private bool DrawMetaManipulationsTable( string label, IList< MetaManipulation > list, ref int count ) - { - var numRows = _editMode ? 11 : 10; - var changes = false; - - - if( list.Count > 0 - && ImGui.BeginTable( label, numRows, - ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - if( _editMode ) - { - ImGui.TableNextColumn(); - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Type##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Object Type##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Set##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Slot##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Race##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Gender##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Secondary ID##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Variant##{label}" ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableHeader( $"Value##{label}" ); - ImGui.TableNextRow(); - - for( var i = 0; i < list.Count; ++i ) - { - changes |= DrawManipulationRow( ref i, list, ref count ); - } - } - - var popupName = $"##newManip{label}"; - if( _editMode ) - { - changes |= DrawNewManipulationPopup( $"##newManip{label}", list, ref count ); - if( ImGui.Button( $"Add New Manipulation##{label}", Vector2.UnitX * -1 ) ) - { - ImGui.OpenPopup( popupName ); - } - - return changes; - } - - return false; - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs deleted file mode 100644 index 9b6f7279..00000000 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ /dev/null @@ -1,797 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Windows.Forms.VisualStyles; -using Dalamud.Interface; -using Dalamud.Logging; -using ImGuiNET; -using Penumbra.Collections; -using Penumbra.Importer; -using Penumbra.Mods; -using Penumbra.UI.Classes; -using Penumbra.UI.Custom; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - // Constants - private partial class Selector - { - private const string LabelSelectorList = "##availableModList"; - private const string LabelModFilter = "##ModFilter"; - private const string LabelAddModPopup = "AddModPopup"; - private const string LabelModHelpPopup = "Help##Selector"; - - private const string TooltipModFilter = - "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; - - private const string TooltipDelete = "Delete the selected mod"; - private const string TooltipAdd = "Add an empty mod"; - private const string DialogDeleteMod = "PenumbraDeleteMod"; - private const string ButtonYesDelete = "Yes, delete it"; - private const string ButtonNoDelete = "No, keep it"; - - private const float SelectorPanelWidth = 240f; - - private static readonly Vector2 SelectorButtonSizes = new(100, 0); - private static readonly Vector2 HelpButtonSizes = new(40, 0); - - private static readonly Vector4 DeleteModNameColor = new(0.7f, 0.1f, 0.1f, 1); - } - - // Buttons - private partial class Selector - { - // === Delete === - private int? _deleteIndex; - - private void DrawModTrashButton() - { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) - { - _deleteIndex = _index; - } - - raii.Pop(); - - ImGuiCustom.HoverTooltip( TooltipDelete ); - } - - private void DrawDeleteModal() - { - if( _deleteIndex == null ) - { - return; - } - - ImGui.OpenPopup( DialogDeleteMod ); - - var _ = true; - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); - if( !ret ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( Mod == null ) - { - _deleteIndex = null; - ImGui.CloseCurrentPopup(); - return; - } - - ImGui.Text( "Are you sure you want to delete the following mod:" ); - var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); - ImGui.Dummy( halfLine ); - ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); - ImGui.Dummy( halfLine ); - - var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( ButtonYesDelete, buttonSize ) ) - { - ImGui.CloseCurrentPopup(); - var mod = Mod; - Cache.RemoveMod( mod ); - Penumbra.ModManager.DeleteMod( mod.Data.BasePath ); - ModFileSystem.InvokeChange(); - ClearSelection(); - } - - ImGui.SameLine(); - - if( ImGui.Button( ButtonNoDelete, buttonSize ) ) - { - ImGui.CloseCurrentPopup(); - _deleteIndex = null; - } - } - - // === Add === - private bool _modAddKeyboardFocus = true; - - private void DrawModAddButton() - { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) - { - _modAddKeyboardFocus = true; - ImGui.OpenPopup( LabelAddModPopup ); - } - - raii.Pop(); - - ImGuiCustom.HoverTooltip( TooltipAdd ); - - DrawModAddPopup(); - } - - private void DrawModAddPopup() - { - if( !ImGui.BeginPopup( LabelAddModPopup ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( _modAddKeyboardFocus ) - { - ImGui.SetKeyboardFocusHere(); - _modAddKeyboardFocus = false; - } - - var newName = ""; - if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - try - { - var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), - newName ); - var modMeta = new ModMeta - { - Author = "Unknown", - Name = newName.Replace( '/', '\\' ), - Description = string.Empty, - }; - - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - modMeta.SaveToFile( metaFile ); - Penumbra.ModManager.AddMod( newDir ); - ModFileSystem.InvokeChange(); - SelectModOnUpdate( newDir.Name ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); - } - - ImGui.CloseCurrentPopup(); - } - - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } - } - - // === Help === - private void DrawModHelpButton() - { - using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) - { - ImGui.OpenPopup( LabelModHelpPopup ); - } - } - - private static void DrawModHelpPopup() - { - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), - ImGuiCond.Appearing ); - var _ = true; - if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information." ); - ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); - ImGui.Indent(); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.Text( "Enabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), "Disabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.NewMod.Value() ), - "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.HandledConflictMod.Value() ), - "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), - "Enabled and conflicting with another enabled Mod on the same priority." ); - ImGui.Unindent(); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); - ImGui.Indent(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); - ImGui.BulletText( - "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" - + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" - + "where ModName will be sorted as if it was the string '1'." ); - ImGui.Unindent(); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); - ImGui.BulletText( "Right-clicking a folder opens a context menu." ); - ImGui.Indent(); - ImGui.BulletText( - "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); - ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); - ImGui.Indent(); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Management" ); - ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); - ImGui.BulletText( "You can add a completely empty mod with the plus button." ); - ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); - ImGui.BulletText( - "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); - ImGui.BulletText( - "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); - ImGui.SameLine(); - if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) - { - ImGui.CloseCurrentPopup(); - } - } - - // === Main === - private void DrawModsSelectorButtons() - { - // Selector controls - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) - .Push( ImGuiStyleVar.FrameRounding, 0 ); - - DrawModAddButton(); - ImGui.SameLine(); - DrawModHelpButton(); - ImGui.SameLine(); - DrawModTrashButton(); - } - } - - // Filters - private partial class Selector - { - private string _modFilterInput = ""; - - private void DrawTextFilter() - { - ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); - var tmp = _modFilterInput; - if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) - { - Cache.SetTextFilter( tmp ); - _modFilterInput = tmp; - } - - ImGuiCustom.HoverTooltip( TooltipModFilter ); - } - - private void DrawToggleFilter() - { - if( ImGui.BeginCombo( "##ModStateFilter", "", - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); - var flags = ( int )Cache.StateFilter; - foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) - { - ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); - } - - Cache.StateFilter = ( ModFilter )flags; - } - - ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); - } - - private void DrawModsSelectorFilter() - { - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); - DrawTextFilter(); - ImGui.SameLine(); - DrawToggleFilter(); - } - } - - // Drag'n Drop - private partial class Selector - { - private const string DraggedModLabel = "ModIndex"; - private const string DraggedFolderLabel = "FolderName"; - - private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); - - private static unsafe bool IsDropping( string name ) - => ImGui.AcceptDragDropPayload( name ).NativePtr != null; - - private void DragDropTarget( ModFolder folder ) - { - if( !ImGui.BeginDragDropTarget() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); - - if( IsDropping( DraggedModLabel ) ) - { - var payload = ImGui.GetDragDropPayload(); - var modIndex = Marshal.ReadInt32( payload.Data ); - var mod = Cache.GetMod( modIndex ).Item1; - mod?.Data.Move( folder ); - } - else if( IsDropping( DraggedFolderLabel ) ) - { - var payload = ImGui.GetDragDropPayload(); - var folderName = Marshal.PtrToStringUni( payload.Data ); - if( ModFileSystem.Find( folderName!, out var droppedFolder ) - && !ReferenceEquals( droppedFolder, folder ) - && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) - { - droppedFolder.Move( folder ); - } - } - } - - private void DragDropSourceFolder( ModFolder folder ) - { - if( !ImGui.BeginDragDropSource() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); - - var folderName = folder.FullName; - var ptr = Marshal.StringToHGlobalUni( folderName ); - ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); - ImGui.Text( $"Moving {folderName}..." ); - } - - private void DragDropSourceMod( int modIndex, string modName ) - { - if( !ImGui.BeginDragDropSource() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); - - Marshal.WriteInt32( _dragDropPayload, modIndex ); - ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); - ImGui.Text( $"Moving {modName}..." ); - } - - ~Selector() - => Marshal.FreeHGlobal( _dragDropPayload ); - } - - // Selection - private partial class Selector - { - public Mods.FullMod? Mod { get; private set; } - private int _index; - private string _nextDir = string.Empty; - - private void SetSelection( int idx, Mods.FullMod? info ) - { - Mod = info; - if( idx != _index ) - { - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - } - - _index = idx; - _deleteIndex = null; - } - - private void SetSelection( int idx ) - { - if( idx >= Cache.Count ) - { - idx = -1; - } - - if( idx < 0 ) - { - SetSelection( 0, null ); - } - else - { - SetSelection( idx, Cache.GetMod( idx ).Item1 ); - } - } - - public void ReloadSelection() - => SetSelection( _index, Cache.GetMod( _index ).Item1 ); - - public void ClearSelection() - => SetSelection( -1 ); - - public void SelectModOnUpdate( string directory ) - => _nextDir = directory; - - public void SelectModByDir( string name ) - { - var (mod, idx) = Cache.GetModByBasePath( name ); - SetSelection( idx, mod ); - } - - public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - { - if( Mod == null ) - { - return; - } - - if( _index >= 0 && Penumbra.ModManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) - { - SelectModOnUpdate( Mod.Data.BasePath.Name ); - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - } - } - - public void SaveCurrentMod() - => Mod?.Data.SaveMeta(); - } - - // Right-Clicks - private partial class Selector - { - // === Mod === - private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen ) - { - if( !ImGui.BeginPopup( popupName ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) ) - { - ImGui.CloseCurrentPopup(); - } - - if( firstOpen ) - { - ImGui.SetKeyboardFocusHere( mod.Data.Order.FullPath.Length - 1 ); - } - } - - // === Folder === - private string _newFolderName = string.Empty; - private int _expandIndex = -1; - private bool _expandCollapse; - private bool _currentlyExpanding; - - private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) - { - var change = false; - var metaManips = false; - foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) ) - { - var (mod, _, _) = Cache.GetMod( currentIdx++ ); - if( mod != null ) - { - change |= mod.Settings.Enabled != toWhat; - mod!.Settings.Enabled = toWhat; - metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; - } - } - - if( !change ) - { - return; - } - - Cache.TriggerFilterReset(); - var collection = Penumbra.CollectionManager.Current; - if( collection.HasCache ) - { - collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.CollectionManager.Default ); - } - - collection.Save(); - } - - private void DrawRenameFolderInput( ModFolder folder ) - { - ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, - ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - return; - } - - if( _newFolderName.Any() ) - { - folder.Rename( _newFolderName ); - } - else - { - folder.Merge( folder.Parent! ); - } - - _newFolderName = string.Empty; - } - - private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) - { - if( !ImGui.BeginPopup( treeName ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( ImGui.MenuItem( "Expand All Descendants" ) ) - { - _expandIndex = currentIdx; - _expandCollapse = false; - } - - if( ImGui.MenuItem( "Collapse All Descendants" ) ) - { - _expandIndex = currentIdx; - _expandCollapse = true; - } - - if( ImGui.MenuItem( "Enable All Descendants" ) ) - { - ChangeStatusOfChildren( folder, currentIdx, true ); - } - - if( ImGui.MenuItem( "Disable All Descendants" ) ) - { - ChangeStatusOfChildren( folder, currentIdx, false ); - } - - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawRenameFolderInput( folder ); - } - } - - // Main-Interface - private partial class Selector - { - private readonly SettingsInterface _base; - public readonly ModListCache Cache; - - private float _selectorScalingFactor = 1; - - public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) - { - _base = ui; - Cache = new ModListCache( Penumbra.ModManager, newMods ); - } - - private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) - { - if( collection == ModCollection.Empty - || collection == Penumbra.CollectionManager.Current ) - { - using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( label, Vector2.UnitX * size ); - } - else if( ImGui.Button( label, Vector2.UnitX * size ) ) - { - _base._menu.CollectionsTab.SetCurrentCollection( collection ); - } - - ImGuiCustom.HoverTooltip( - $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); - } - - private void DrawHeaderBar() - { - const float size = 200; - - DrawModsSelectorFilter(); - var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; - var comboSize = size * ImGui.GetIO().FontGlobalScale; - var offset = comboSize + textSize; - - var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth() - - offset - - SelectorPanelWidth * _selectorScalingFactor - - 3 * ImGui.GetStyle().ItemSpacing.X, 5f ); - ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default ); - - - ImGui.SameLine(); - ImGui.SetNextItemWidth( comboSize ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); - } - - private void DrawFolderContent( ModFolder folder, ref int idx ) - { - // Collection may be manipulated. - foreach( var item in folder.GetItems( Penumbra.ModManager.Config.SortFoldersFirst ).ToArray() ) - { - if( item is ModFolder sub ) - { - var (visible, _) = Cache.GetFolder( sub ); - if( visible ) - { - DrawModFolder( sub, ref idx ); - } - else - { - idx += sub.TotalDescendantMods(); - } - } - else if( item is Mods.Mod _ ) - { - var (mod, visible, color) = Cache.GetMod( idx ); - if( mod != null && visible ) - { - DrawMod( mod, idx++, color ); - } - else - { - ++idx; - } - } - } - } - - private void DrawModFolder( ModFolder folder, ref int idx ) - { - var treeName = $"{folder.Name}##{folder.FullName}"; - var open = ImGui.TreeNodeEx( treeName ); - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); - - if( idx == _expandIndex ) - { - _currentlyExpanding = true; - } - - if( _currentlyExpanding ) - { - ImGui.SetNextItemOpen( !_expandCollapse ); - } - - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - _newFolderName = string.Empty; - ImGui.OpenPopup( treeName ); - } - - DrawFolderContextMenu( folder, idx, treeName ); - DragDropTarget( folder ); - DragDropSourceFolder( folder ); - - if( open ) - { - DrawFolderContent( folder, ref idx ); - } - else - { - idx += folder.TotalDescendantMods(); - } - - if( idx == _expandIndex ) - { - _currentlyExpanding = false; - _expandIndex = -1; - } - } - - private void DrawMod( Mods.FullMod mod, int modIndex, uint color ) - { - using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); - - var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); - colorRaii.Pop(); - - var popupName = $"##SortOrderPopup{modIndex}"; - var firstOpen = false; - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - ImGui.OpenPopup( popupName ); - firstOpen = true; - } - - DragDropTarget( mod.Data.Order.ParentFolder ); - DragDropSourceMod( modIndex, mod.Data.Meta.Name ); - - DrawModOrderPopup( popupName, mod, firstOpen ); - - if( selected ) - { - SetSelection( modIndex, mod ); - } - } - - public void Draw() - { - if( Cache.Update() ) - { - if( _nextDir.Any() ) - { - SelectModByDir( _nextDir ); - _nextDir = string.Empty; - } - else if( Mod != null ) - { - SelectModByDir( Mod.Data.BasePath.Name ); - } - } - - _selectorScalingFactor = ImGuiHelpers.GlobalScale - * ( Penumbra.Config.ScaleModSelector - ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X - : 1f ); - // Selector pane - DrawHeaderBar(); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGui.BeginGroup(); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) - .Push( ImGui.EndChild ); - // Inlay selector list - if( ImGui.BeginChild( LabelSelectorList, - new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), - true, ImGuiWindowFlags.HorizontalScrollbar ) ) - { - style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); - - var modIndex = 0; - DrawFolderContent( Penumbra.ModManager.StructuredMods, ref modIndex ); - style.Pop(); - } - - raii.Pop(); - - DrawModsSelectorButtons(); - - style.Pop(); - DrawModHelpPopup(); - - DrawDeleteModal(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs deleted file mode 100644 index f8f8d9a4..00000000 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using FFXIVClientStructs.STD; -using ImGuiNET; -using Penumbra.GameData.ByteString; -using Penumbra.Interop.Loader; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private static string GetNodeLabel( uint label, uint type, ulong count ) - { - var byte1 = type >> 24; - var byte2 = ( type >> 16 ) & 0xFF; - var byte3 = ( type >> 8 ) & 0xFF; - var byte4 = type & 0xFF; - return byte1 == 0 - ? $"({type:X8}) {( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug" - : $"({type:X8}) {( char )byte1}{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug"; - } - - private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) - { - if( map == null ) - { - return; - } - - var label = GetNodeLabel( ( uint )category, ext, map->Count ); - if( !ImGui.TreeNodeEx( label ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - - if( map->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, - ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale ); - ImGui.TableHeadersRow(); - - ResourceLoader.IterateResourceMap( map, ( hash, r ) => - { - if( _filter.Length != 0 && !r->FileName.ToString().Contains( _filter, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.Text( $"0x{hash:X8}" ); - ImGui.TableNextColumn(); - var address = $"0x{( ulong )r:X}"; - ImGui.Text( address ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( address ); - } - - ref var name = ref r->FileName; - ImGui.TableNextColumn(); - if( name.Capacity > 15 ) - { - ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); - } - else - { - fixed( byte* ptr = name.Buffer ) - { - ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); - } - } - - if( ImGui.IsItemClicked() ) - { - var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); - if( data != null ) - { - var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); - } - - ImGui.TableNextColumn(); - ImGui.Text( r->RefCount.ToString() ); - } ); - } - - private unsafe void DrawCategoryContainer( ResourceCategory category, - StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) - { - if( map == null || !ImGui.TreeNodeEx( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}Debug" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - ResourceLoader.IterateExtMap( map, ( ext, map ) => DrawResourceMap( category, ext, map ) ); - } - - - private static unsafe void DrawResourceProblems() - { - if( !ImGui.CollapsingHeader( "Resource Problems##ResourceManager" ) - || !ImGui.BeginTable( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - - ResourceLoader.IterateResources( ( _, r ) => - { - if( r->RefCount < 10000 ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.Text( r->Category.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( r->FileType.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( r->Id.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( r->RefCount.ToString() ); - ImGui.TableNextColumn(); - ref var name = ref r->FileName; - if( name.Capacity > 15 ) - { - ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); - } - else - { - fixed( byte* ptr = name.Buffer ) - { - ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); - } - } - } ); - } - - private string _filter = string.Empty; - - private unsafe void DrawResourceManagerTab() - { - if( !ImGui.BeginTabItem( "Resource Manager Tab" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var resourceHandler = *ResourceLoader.ResourceManager; - - if( resourceHandler == null ) - { - return; - } - - ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _filter, Utf8GamePath.MaxGamePathLength ); - - raii.Push( ImGui.EndChild ); - if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) ) - { - return; - } - - ResourceLoader.IterateGraphs( DrawCategoryContainer ); - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs deleted file mode 100644 index 0589faf6..00000000 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ /dev/null @@ -1,381 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using Penumbra.GameData.ByteString; -using Penumbra.UI.Classes; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class TabSettings - { - private readonly SettingsInterface _base; - private readonly Configuration _config; - private bool _configChanged; - private string _newModDirectory; - - public TabSettings( SettingsInterface ui ) - { - _base = ui; - _config = Penumbra.Config; - _configChanged = false; - _newModDirectory = _config.ModDirectory; - } - - private static bool DrawPressEnterWarning( string old ) - { - const uint red = 0xFF202080; - using var color = ImGuiRaii.PushColor( ImGuiCol.Button, red ); - var w = Vector2.UnitX * ImGui.CalcItemWidth(); - return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); - } - - private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) - { - ImGui.PushID( id ); - var ret = ImGui.Button( "Open Directory" ); - ImGuiCustom.HoverTooltip( "Open this directory in your configured file explorer." ); - if( ret && condition && Directory.Exists( directory.FullName ) ) - { - Process.Start( new ProcessStartInfo( directory.FullName ) - { - UseShellExecute = true, - } ); - } - - ImGui.PopID(); - } - - private void DrawRootFolder() - { - ImGui.BeginGroup(); - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - var save = ImGui.InputText( "Root Directory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); - ImGui.SameLine(); - var modManager = Penumbra.ModManager; - DrawOpenDirectoryButton( 0, modManager.BasePath, modManager.Valid ); - ImGui.EndGroup(); - - if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) - { - return; - } - - if( save || DrawPressEnterWarning( _config.ModDirectory ) ) - { - _base._menu.InstalledTab.Selector.ClearSelection(); - modManager.DiscoverMods( _newModDirectory ); - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); - _newModDirectory = _config.ModDirectory; - } - } - - private void DrawRediscoverButton() - { - if( ImGui.Button( "Rediscover Mods" ) ) - { - _base._menu.InstalledTab.Selector.ClearSelection(); - Penumbra.ModManager.DiscoverMods(); - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); - } - - private void DrawEnabledBox() - { - var enabled = _config.EnableMods; - if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) - { - _base._penumbra.SetEnabled( enabled ); - } - } - - private void DrawShowAdvancedBox() - { - var showAdvanced = _config.ShowAdvanced; - if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) - { - _config.ShowAdvanced = showAdvanced; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Enable some advanced options in this window and in the mod selector.\n" - + "This is required to enable manually editing any mod information." ); - } - - private void DrawSortFoldersFirstBox() - { - var foldersFirst = _config.SortFoldersFirst; - if( ImGui.Checkbox( "Sort Mod-Folders Before Mods", ref foldersFirst ) ) - { - _config.SortFoldersFirst = foldersFirst; - _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Prioritizes all mod-folders in the mod-selector in the Installed Mods tab so that folders come before single mods, instead of being sorted completely alphabetically" ); - } - - private void DrawScaleModSelectorBox() - { - var scaleModSelector = _config.ScaleModSelector; - if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) - { - _config.ScaleModSelector = scaleModSelector; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); - } - - private void DrawDisableSoundStreamingBox() - { - var tmp = Penumbra.Config.DisableSoundStreaming; - if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) - { - Penumbra.Config.DisableSoundStreaming = tmp; - _configChanged = true; - if( tmp ) - { - _base._penumbra.MusicManager.DisableStreaming(); - } - else - { - _base._penumbra.MusicManager.EnableStreaming(); - } - - _base.ReloadMods(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Disable streaming in the games audio engine.\n" - + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" - + "Only touch this if you experience sound problems.\n" - + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." ); - } - - private void DrawLogLoadedFilesBox() - { - //ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); - //ImGui.SameLine(); - //var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; - //var tmp = regex; - //ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - //if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) - //{ - // try - // { - // var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; - // _base._penumbra.ResourceLoader.LogFileFilter = newRegex; - // } - // catch( Exception e ) - // { - // PluginLog.Debug( "Could not create regex:\n{Exception}", e ); - // } - //} - // - //ImGui.SameLine(); - //ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); - } - - private void DrawDisableNotificationsBox() - { - var fsWatch = _config.DisableFileSystemNotifications; - if( ImGui.Checkbox( "Disable Filesystem Change Notifications", ref fsWatch ) ) - { - _config.DisableFileSystemNotifications = fsWatch; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Currently does nothing." ); - } - - private void DrawEnableHttpApiBox() - { - var http = _config.EnableHttpApi; - if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) - { - if( http ) - { - _base._penumbra.CreateWebServer(); - } - else - { - _base._penumbra.ShutdownWebServer(); - } - - _config.EnableHttpApi = http; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( "Reload Resident Resources" ) ) - { - Penumbra.ResidentResources.Reload(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Reload some specific files that the game keeps in memory at all times.\n" - + "You usually should not need to do this." ); - } - - private void DrawEnableFullResourceLoggingBox() - { - var tmp = _config.EnableFullResourceLogging; - if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != _config.EnableFullResourceLogging ) - { - if( tmp ) - { - Penumbra.ResourceLoader.EnableFullLogging(); - } - else - { - Penumbra.ResourceLoader.DisableFullLogging(); - } - - _config.EnableFullResourceLogging = tmp; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); - } - - private void DrawEnableDebugModeBox() - { - var tmp = _config.DebugMode; - if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != _config.DebugMode ) - { - if( tmp ) - { - Penumbra.ResourceLoader.EnableDebug(); - } - else - { - Penumbra.ResourceLoader.DisableDebug(); - } - - _config.DebugMode = tmp; - _configChanged = true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." ); - } - - private void DrawRequestedResourceLogging() - { - var tmp = _config.EnableResourceLogging; - if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) ) - { - _base._penumbra.ResourceLogger.SetState( tmp ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n" - + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" - + "Red boundary indicates invalid regex." ); - ImGui.SameLine(); - var tmpString = Penumbra.Config.ResourceLoggingFilter; - using var color = ImGuiRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_base._penumbra.ResourceLogger.ValidRegex ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, - !_base._penumbra.ResourceLogger.ValidRegex ); - if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) - { - _base._penumbra.ResourceLogger.SetFilter( tmpString ); - } - } - - private void DrawAdvancedSettings() - { - DrawRequestedResourceLogging(); - DrawDisableSoundStreamingBox(); - DrawLogLoadedFilesBox(); - DrawDisableNotificationsBox(); - DrawEnableHttpApiBox(); - DrawReloadResourceButton(); - DrawEnableDebugModeBox(); - DrawEnableFullResourceLoggingBox(); - } - - public static unsafe void Text( Utf8String s ) - { - ImGuiNative.igTextUnformatted( ( byte* )s.Path, ( byte* )s.Path + s.Length ); - } - - public void Draw() - { - if( !ImGui.BeginTabItem( "Settings" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - DrawRootFolder(); - - DrawRediscoverButton(); - - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); - DrawEnabledBox(); - - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); - DrawScaleModSelectorBox(); - DrawSortFoldersFirstBox(); - DrawShowAdvancedBox(); - - if( _config.ShowAdvanced ) - { - DrawAdvancedSettings(); - } - - if( ImGui.CollapsingHeader( "Colors" ) ) - { - foreach( var color in Enum.GetValues< ColorId >() ) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; - if( ImGuiUtil.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) - { - _configChanged = true; - } - } - } - - if( _configChanged ) - { - _config.Save(); - _configChanged = false; - } - } - } -} \ No newline at end of file From 8dd681bdda10d94f902b2eafbab94569b080c793 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Apr 2022 11:03:19 +0200 Subject: [PATCH 0138/2451] Even almoster... --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 44 +- Penumbra/Collections/ModCollection.Changes.cs | 2 +- .../Collections/ModCollection.Inheritance.cs | 52 +- Penumbra/UI/Classes/Colors.cs | 1 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 28 +- ...ConfigWindow.CollectionsTab.Inheritance.cs | 288 ++++ Penumbra/UI/ConfigWindow.CollectionsTab.cs | 350 +++-- Penumbra/UI/ConfigWindow.DebugTab.cs | 751 ++++++----- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 362 ++--- Penumbra/UI/ConfigWindow.Misc.cs | 10 +- Penumbra/UI/ConfigWindow.ModsTab.Panel.cs | 1195 ++++++++--------- Penumbra/UI/ConfigWindow.ModsTab.cs | 389 +++++- Penumbra/UI/ConfigWindow.ResourceTab.cs | 276 ++-- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 253 ++-- .../ConfigWindow.SettingsTab.ModSelector.cs | 134 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 307 ++--- Penumbra/UI/ConfigWindow.cs | 41 +- 19 files changed, 2625 insertions(+), 1865 deletions(-) create mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs diff --git a/OtterGui b/OtterGui index 4cc10240..a832fb6c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4cc1024096905b0b20c1559c534b1dd3fe7b25ad +Subproject commit a832fb6ca5e7c6cb4e35a51a08d30d1800f405da diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 30fd44fb..a1766e96 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -81,6 +81,29 @@ public partial class ModCollection _modManager.ModPathChanged -= OnModPathChanged; } + // Returns true if the name is not empty, it is not the name of the empty collection + // and no existing collection results in the same filename as name. + public bool CanAddCollection( string name, out string fixedName ) + { + if( name.Length == 0 ) + { + fixedName = string.Empty; + return false; + } + + name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( name.Length == 0 + || name == Empty.Name.ToLowerInvariant() + || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) ) + { + fixedName = string.Empty; + return false; + } + + fixedName = name; + return true; + } + // Add a new collection of the given name. // If duplicate is not-null, the new collection will be a duplicate of it. // If the name of the collection would result in an already existing filename, skip it. @@ -88,12 +111,9 @@ public partial class ModCollection // Also sets the current collection to the new collection afterwards. public bool AddCollection( string name, ModCollection? duplicate ) { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 - || nameFixed == Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + if( !CanAddCollection( name, out var fixedName ) ) { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + PluginLog.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." ); return false; } @@ -108,6 +128,7 @@ public partial class ModCollection // Remove the given collection if it exists and is neither the empty nor the default-named collection. // If the removed collection was active, it also sets the corresponding collection to the appropriate default. + // Also removes the collection from inheritances of all other collections. public bool RemoveCollection( int idx ) { if( idx <= Empty.Index || idx >= _collections.Count ) @@ -140,9 +161,18 @@ public partial class ModCollection var collection = _collections[ idx ]; collection.Delete(); _collections.RemoveAt( idx ); - for( var i = idx; i < _collections.Count; ++i ) + foreach( var c in _collections ) { - --_collections[ i ].Index; + var inheritedIdx = c._inheritance.IndexOf( collection ); + if( inheritedIdx >= 0 ) + { + c.RemoveInheritance( inheritedIdx ); + } + + if( c.Index > idx ) + { + --c.Index; + } } CollectionChanged.Invoke( Type.Inactive, collection, null ); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 78e6ee07..4b350758 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -137,7 +137,7 @@ public partial class ModCollection return false; } - _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); + _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); return true; } diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 34156396..b9c75d9f 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -23,23 +23,57 @@ public partial class ModCollection // Iterate over all collections inherited from in depth-first order. // Skip already visited collections to avoid circular dependencies. public IEnumerable< ModCollection > GetFlattenedInheritance() - { - yield return this; + => InheritedCollections( this ).Distinct(); - foreach( var collection in _inheritance.SelectMany( c => c.GetFlattenedInheritance() ) - .Where( c => !ReferenceEquals( this, c ) ) - .Distinct() ) - { - yield return collection; - } + // All inherited collections in application order without filtering for duplicates. + private static IEnumerable< ModCollection > InheritedCollections( ModCollection collection ) + => collection.Inheritance.SelectMany( InheritedCollections ).Prepend( collection ); + + // Reasons why a collection can not be inherited from. + public enum ValidInheritance + { + Valid, + Self, // Can not inherit from self + Empty, // Can not inherit from the empty collection + Contained, // Already inherited from + Circle, // Inheritance would lead to a circle. } + // Check whether a collection can be inherited from. + public ValidInheritance CheckValidInheritance( ModCollection? collection ) + { + if( collection == null || ReferenceEquals( collection, Empty ) ) + { + return ValidInheritance.Empty; + } + + if( ReferenceEquals( collection, this ) ) + { + return ValidInheritance.Self; + } + + if( _inheritance.Contains( collection ) ) + { + return ValidInheritance.Contained; + } + + if( InheritedCollections( collection ).Any( c => c == this ) ) + { + return ValidInheritance.Circle; + } + + return ValidInheritance.Valid; + } + + private bool CheckForCircle( ModCollection collection ) + => ReferenceEquals( collection, this ) || _inheritance.Any( c => c.CheckForCircle( collection ) ); + // Add a new collection to the inheritance list. // We do not check if this collection would be visited before, // only that it is unique in the list itself. public bool AddInheritance( ModCollection collection ) { - if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) + if( CheckValidInheritance( collection ) != ValidInheritance.Valid ) { return false; } diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 7ed19b08..0a16758e 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -22,6 +22,7 @@ public static class Colors { public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; + public const uint MetaInfoText = 0xAAFFFFFF; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 3ee304cb..ca0fbaf0 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -48,6 +48,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; } + public new ModFileSystem.Leaf? SelectedLeaf + => base.SelectedLeaf; + // Customization points. public override SortMode SortMode => Penumbra.Config.SortMode; @@ -199,7 +202,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo { if( _lastSelectedDirectory.Length > 0 ) { - SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) + base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); _lastSelectedDirectory = string.Empty; } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 109bc32b..f7aedac3 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -11,8 +11,19 @@ public partial class ConfigWindow { private LowerString _changedItemFilter = LowerString.Empty; - public void DrawChangedItemTab() + // Draw a simple clipped table containing all changed items. + private void DrawChangedItemTab() { + // Functions in here for less pollution. + bool FilterChangedItem( KeyValuePair< string, object? > item ) + => item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + + void DrawChangedItemColumn( KeyValuePair< string, object? > item ) + { + ImGui.TableNextColumn(); + DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize ); + } + using var tab = ImRaii.TabItem( "Changed Items" ); if( !tab ) { @@ -36,19 +47,10 @@ public partial class ConfigWindow return; } - var items = Penumbra.CollectionManager.Default.ChangedItems; + var items = Penumbra.CollectionManager.Default.ChangedItems; var rest = _changedItemFilter.IsEmpty - ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItem, items.Count ) - : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItem ); + ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) + : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); ImGuiClip.DrawEndDummy( rest, height ); } - - private bool FilterChangedItem( KeyValuePair< string, object? > item ) - => item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); - - private void DrawChangedItem( KeyValuePair< string, object? > item ) - { - ImGui.TableNextColumn(); - DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize ); - } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs new file mode 100644 index 00000000..ac803463 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.UI.Classes; +using Penumbra.Util; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class CollectionsTab + { + private const int InheritedCollectionHeight = 10; + private const string InheritanceDragDropLabel = "##InheritanceMove"; + + // Keep for reuse. + private readonly HashSet< ModCollection > _seenInheritedCollections = new(32); + + // Execute changes only outside of loops. + private ModCollection? _newInheritance; + private ModCollection? _movedInheritance; + private (int, int)? _inheritanceAction; + private ModCollection? _newCurrentCollection; + + // Draw the whole inheritance block. + private void DrawInheritanceBlock() + { + using var id = ImRaii.PushId( "##Inheritance" ); + DrawCurrentCollectionInheritance(); + DrawInheritanceTrashButton(); + DrawNewInheritanceSelection(); + DelayedActions(); + } + + // If an inherited collection is expanded, + // draw all its flattened, distinct children in order with a tree-line. + private void DrawInheritedChildren( ModCollection collection ) + { + using var id = ImRaii.PushId( collection.Index ); + using var indent = ImRaii.PushIndent(); + + // Get start point for the lines (top of the selector). + // Tree line stuff. + var lineStart = ImGui.GetCursorScreenPos(); + var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; + var drawList = ImGui.GetWindowDrawList(); + var lineSize = Math.Max( 0, ImGui.GetStyle().IndentSpacing - 9 * ImGuiHelpers.GlobalScale ); + lineStart.X += offsetX; + lineStart.Y -= 2 * ImGuiHelpers.GlobalScale; + var lineEnd = lineStart; + + // Skip the collection itself. + foreach( var inheritance in collection.GetFlattenedInheritance().Skip( 1 ) ) + { + // Draw the child, already seen collections are colored as conflicts. + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), + _seenInheritedCollections.Contains( inheritance ) ); + _seenInheritedCollections.Add( inheritance ); + + ImRaii.TreeNode( inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + var (minRect, maxRect) = ( ImGui.GetItemRectMin(), ImGui.GetItemRectMax() ); + DrawInheritanceTreeClicks( inheritance, false ); + + // Tree line stuff. + if( minRect.X == 0 ) + { + continue; + } + + // Draw the notch and increase the line length. + var midPoint = ( minRect.Y + maxRect.Y ) / 2f - 1f; + drawList.AddLine( new Vector2( lineStart.X, midPoint ), new Vector2( lineStart.X + lineSize, midPoint ), Colors.MetaInfoText, + ImGuiHelpers.GlobalScale ); + lineEnd.Y = midPoint; + } + + // Finally, draw the folder line. + drawList.AddLine( lineStart, lineEnd, Colors.MetaInfoText, ImGuiHelpers.GlobalScale ); + } + + // Draw a single primary inherited collection. + private void DrawInheritance( ModCollection collection ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), + _seenInheritedCollections.Contains( collection ) ); + _seenInheritedCollections.Add( collection ); + using var tree = ImRaii.TreeNode( collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen ); + color.Pop(); + DrawInheritanceTreeClicks( collection, true ); + DrawInheritanceDropSource( collection ); + DrawInheritanceDropTarget( collection ); + + if( tree ) + { + DrawInheritedChildren( collection ); + } + else + { + // We still want to keep track of conflicts. + _seenInheritedCollections.UnionWith( collection.GetFlattenedInheritance() ); + } + } + + // Draw the list box containing the current inheritance information. + private void DrawCurrentCollectionInheritance() + { + using var list = ImRaii.ListBox( "##inheritanceList", + new Vector2( _window._inputTextWidth.X - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X, + ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ) ); + if( !list ) + { + return; + } + + _seenInheritedCollections.Clear(); + _seenInheritedCollections.Add( Penumbra.CollectionManager.Current ); + foreach( var collection in Penumbra.CollectionManager.Current.Inheritance.ToList() ) + { + DrawInheritance( collection ); + } + } + + // Draw a drag and drop button to delete. + private void DrawInheritanceTrashButton() + { + ImGui.SameLine(); + var size = new Vector2( ImGui.GetFrameHeight(), ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ); + var buttonColor = ImGui.GetColorU32( ImGuiCol.Button ); + // Prevent hovering from highlighting the button. + using var color = ImRaii.PushColor( ImGuiCol.ButtonActive, buttonColor ) + .Push( ImGuiCol.ButtonHovered, buttonColor ); + ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, + "Drag primary inheritance here to remove it from the list.", false, true ); + + using var target = ImRaii.DragDropTarget(); + if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) + { + _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance ), -1 ); + } + } + + // Set the current collection, or delete or move an inheritance if the action was triggered during iteration. + // Can not be done during iteration to keep collections unchanged. + private void DelayedActions() + { + if( _newCurrentCollection != null ) + { + Penumbra.CollectionManager.SetCollection( _newCurrentCollection, ModCollection.Type.Current ); + _newCurrentCollection = null; + } + + if( _inheritanceAction == null ) + { + return; + } + + if( _inheritanceAction.Value.Item1 >= 0 ) + { + if( _inheritanceAction.Value.Item2 == -1 ) + { + Penumbra.CollectionManager.Current.RemoveInheritance( _inheritanceAction.Value.Item1 ); + } + else + { + Penumbra.CollectionManager.Current.MoveInheritance( _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2 ); + } + } + + _inheritanceAction = null; + } + + // Draw the selector to add new inheritances. + // The add button is only available if the selected collection can actually be added. + private void DrawNewInheritanceSelection() + { + DrawNewInheritanceCombo(); + ImGui.SameLine(); + var inheritance = Penumbra.CollectionManager.Current.CheckValidInheritance( _newInheritance ); + var tt = inheritance switch + { + ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", + ModCollection.ValidInheritance.Valid => "Add a new inheritance to the collection.", + ModCollection.ValidInheritance.Self => "Can not inherit from itself.", + ModCollection.ValidInheritance.Contained => "Already inheriting from the selected collection.", + ModCollection.ValidInheritance.Circle => "Inheriting from selected collection would lead to cyclic inheritance.", + _ => string.Empty, + }; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, tt, + inheritance != ModCollection.ValidInheritance.Valid, true ) + && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance! ) ) + { + _newInheritance = null; + } + + if( inheritance != ModCollection.ValidInheritance.Valid ) + { + _newInheritance = null; + } + + ImGuiComponents.HelpMarker( tt ); + } + + // Draw the combo to select new potential inheritances. + // Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. + private void DrawNewInheritanceCombo() + { + ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + _newInheritance ??= Penumbra.CollectionManager.FirstOrDefault( c + => c != Penumbra.CollectionManager.Current && !Penumbra.CollectionManager.Current.Inheritance.Contains( c ) ) + ?? ModCollection.Empty; + using var combo = ImRaii.Combo( "##newInheritance", _newInheritance.Name ); + if( !combo ) + { + return; + } + + foreach( var collection in Penumbra.CollectionManager + .Where( c => Penumbra.CollectionManager.Current.CheckValidInheritance( c ) == ModCollection.ValidInheritance.Valid ) ) + { + if( ImGui.Selectable( collection.Name, _newInheritance == collection ) ) + { + _newInheritance = collection; + } + } + } + + // Move an inherited collection when dropped onto another. + // Move is delayed due to collection changes. + private void DrawInheritanceDropTarget( ModCollection collection ) + { + using var target = ImRaii.DragDropTarget(); + if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) + { + if( _movedInheritance != null ) + { + var idx1 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance ); + var idx2 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ); + if( idx1 >= 0 && idx2 >= 0 ) + { + _inheritanceAction = ( idx1, idx2 ); + } + } + + _movedInheritance = null; + } + } + + // Move an inherited collection. + private void DrawInheritanceDropSource( ModCollection collection ) + { + using var source = ImRaii.DragDropSource(); + if( source ) + { + ImGui.SetDragDropPayload( InheritanceDragDropLabel, IntPtr.Zero, 0 ); + _movedInheritance = collection; + ImGui.Text( $"Moving {_movedInheritance?.Name ?? "Unknown"}..." ); + } + } + + // Ctrl + Right-Click -> Switch current collection to this (for all). + // Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete). + // Deletion is delayed due to collection changes. + private void DrawInheritanceTreeClicks( ModCollection collection, bool withDelete ) + { + if( ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { + if( withDelete && ImGui.GetIO().KeyShift ) + { + _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ), -1 ); + } + else + { + _newCurrentCollection = collection; + } + } + + ImGuiUtil.HoverTooltip( "Control + Right-Click to switch the current collection to this one." + + ( withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty ) ); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 7f5f8ee7..6d38cb69 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -11,206 +11,180 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private string _newCollectionName = string.Empty; - private string _newCharacterName = string.Empty; - - private void CreateNewCollection( bool duplicate ) + // Encapsulate for less pollution. + private partial class CollectionsTab { - if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) + private readonly ConfigWindow _window; + + public CollectionsTab( ConfigWindow window ) + => _window = window; + + public void Draw() { - _newCollectionName = string.Empty; - } - } - - private static void DrawCleanCollectionButton() - { - if( ImGui.Button( "Clean Settings" ) ) - { - Penumbra.CollectionManager.Current.CleanUnavailableSettings(); - } - - ImGuiUtil.HoverTooltip( "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); - } - - private void DrawNewCollectionInput() - { - ImGui.SetNextItemWidth( _inputTextWidth.X ); - ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of mods." ); - - var createCondition = _newCollectionName.Length > 0; - var tt = createCondition ? string.Empty : "Please enter a name before creating a collection."; - if( ImGuiUtil.DrawDisabledButton( "Create New Empty Collection", Vector2.Zero, tt, !createCondition ) ) - { - CreateNewCollection( false ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Duplicate Current Collection", Vector2.Zero, tt, !createCondition ) ) - { - CreateNewCollection( true ); - } - - var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; - tt = deleteCondition ? string.Empty : "You can not delete the default collection."; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Delete Current Collection", Vector2.Zero, tt, !deleteCondition ) ) - { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); - } - - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawCleanCollectionButton(); - } - } - - public void DrawCurrentCollectionSelector() - { - DrawCollectionSelector( "##current", _inputTextWidth.X, ModCollection.Type.Current, false, null ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Current Collection", - "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); - } - - private void DrawDefaultCollectionSelector() - { - DrawCollectionSelector( "##default", _inputTextWidth.X, ModCollection.Type.Default, true, null ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Default Collection", - "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" - + "They also take precedence before the forced collection." ); - } - - private void DrawNewCharacterCollection() - { - const string description = "Character Collections apply specifically to game objects of the given name.\n" - + "The default collection does not apply to any character that has a character collection specified.\n" - + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; - - ImGui.SetNextItemWidth(_inputTextWidth.X ); - ImGui.InputTextWithHint( "##NewCharacter", "New Character Name", ref _newCharacterName, 32 ); - ImGui.SameLine(); - var disabled = _newCharacterName.Length == 0; - var tt = disabled ? "Please enter a Character name before creating the collection.\n\n" + description : description; - if( ImGuiUtil.DrawDisabledButton( "Create New Character Collection", Vector2.Zero, tt, disabled) ) - { - Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); - _newCharacterName = string.Empty; - } - } - - private void DrawCharacterCollectionSelectors() - { - using var child = ImRaii.Child( "##Collections", -Vector2.One, true ); - if( !child ) - return; - - DrawDefaultCollectionSelector(); - - foreach( var name in Penumbra.CollectionManager.Characters.Keys.ToArray() ) - { - using var id = ImRaii.PushId( name ); - DrawCollectionSelector( string.Empty, _inputTextWidth.X, ModCollection.Type.Character, true, name ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), Vector2.One * ImGui.GetFrameHeight(), string.Empty, false, true) ) + using var tab = ImRaii.TabItem( "Collections" ); + if( !tab ) { - Penumbra.CollectionManager.RemoveCharacterCollection( name ); + return; } + + DrawMainSelectors(); + DrawCharacterCollectionSelectors(); + } + + + // Input text fields. + private string _newCollectionName = string.Empty; + private bool _canAddCollection = false; + private string _newCharacterName = string.Empty; + + // Create a new collection that is either empty or a duplicate of the current collection. + // Resets the new collection name. + private void CreateNewCollection( bool duplicate ) + { + if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) + { + _newCollectionName = string.Empty; + } + } + + private static void DrawCleanCollectionButton() + { + if( ImGui.Button( "Clean Settings" ) ) + { + Penumbra.CollectionManager.Current.CleanUnavailableSettings(); + } + + ImGuiUtil.HoverTooltip( "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); + } + + // Draw the new collection input as well as its buttons. + private void DrawNewCollectionInput() + { + // Input for new collection name. Also checks for validity when changed. + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ) ) + { + _canAddCollection = Penumbra.CollectionManager.CanAddCollection( _newCollectionName, out _ ); + } + ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( name ); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of mods." ); + + // Creation buttons. + var tt = _canAddCollection ? string.Empty : "Please enter a unique name before creating a collection."; + if( ImGuiUtil.DrawDisabledButton( "Create New Empty Collection", Vector2.Zero, tt, !_canAddCollection ) ) + { + CreateNewCollection( false ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Duplicate Current Collection", Vector2.Zero, tt, !_canAddCollection ) ) + { + CreateNewCollection( true ); + } + + // Deletion conditions. + var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; + tt = deleteCondition ? string.Empty : "You can not delete the default collection."; + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), Vector2.Zero, tt, !deleteCondition, true ) ) + { + Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); + } + + if( Penumbra.Config.ShowAdvanced ) + { + ImGui.SameLine(); + DrawCleanCollectionButton(); + } } - DrawNewCharacterCollection(); - } - //private static void DrawInheritance( ModCollection collection ) - // { - // ImGui.PushID( collection.Index ); - // if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) ) - // { - // foreach( var inheritance in collection.Inheritance ) - // { - // DrawInheritance( inheritance ); - // } - // } - // - // ImGui.PopID(); - // } - // - // private void DrawCurrentCollectionInheritance() - // { - // if( !ImGui.BeginListBox( "##inheritanceList", - // new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) ) - // { - // return; - // } - // - // using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox ); - // DrawInheritance( _collections[ _currentCollectionIndex + 1 ] ); - // } - // - //private static int _newInheritanceIdx = 0; - // - //private void DrawNewInheritanceSelection() - //{ - // ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); - // if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) ) - // { - // using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); - // foreach( var collection in Penumbra.CollectionManager ) - // { - // if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) ) - // { - // _newInheritanceIdx = collection.Index; - // } - // } - // } - // - // ImGui.SameLine(); - // var valid = _newInheritanceIdx > ModCollection.Empty.Index - // && _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx - // && _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx ); - // using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid ); - // using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - // if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid ) - // { - // _collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] ); - // } - // - // style.Pop(); - // font.Pop(); - // ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." ); - //} - - private void DrawMainSelectors() - { - using var main = ImRaii.Child( "##CollectionsMain", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ); - if( !main ) + private void DrawCurrentCollectionSelector() { - return; + DrawCollectionSelector( "##current", _window._inputTextWidth.X, ModCollection.Type.Current, false, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Current Collection", + "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); } - DrawCurrentCollectionSelector(); - ImGuiHelpers.ScaledDummy( 0, 10 ); - DrawNewCollectionInput(); - } - - public void DrawCollectionsTab() - { - using var tab = ImRaii.TabItem( "Collections" ); - if( !tab ) + private void DrawDefaultCollectionSelector() { - return; + DrawCollectionSelector( "##default", _window._inputTextWidth.X, ModCollection.Type.Default, true, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Default Collection", + "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" + + "They also take precedence before the forced collection." ); } - - DrawMainSelectors(); - DrawCharacterCollectionSelectors(); - } + // We do not check for valid character names. + private void DrawNewCharacterCollection() + { + const string description = "Character Collections apply specifically to game objects of the given name.\n" + + "The default collection does not apply to any character that has a character collection specified.\n" + + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + ImGui.InputTextWithHint( "##NewCharacter", "New Character Name", ref _newCharacterName, 32 ); + ImGui.SameLine(); + var disabled = _newCharacterName.Length == 0; + var tt = disabled ? "Please enter a Character name before creating the collection.\n\n" + description : description; + if( ImGuiUtil.DrawDisabledButton( "Create New Character Collection", Vector2.Zero, tt, disabled ) ) + { + Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); + _newCharacterName = string.Empty; + } + } + + private void DrawCharacterCollectionSelectors() + { + using var child = ImRaii.Child( "##Collections", -Vector2.One, true ); + if( !child ) + { + return; + } + + DrawDefaultCollectionSelector(); + + foreach( var name in Penumbra.CollectionManager.Characters.Keys.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(), Vector2.One * ImGui.GetFrameHeight(), string.Empty, + false, + true ) ) + { + Penumbra.CollectionManager.RemoveCharacterCollection( name ); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( name ); + } + + DrawNewCharacterCollection(); + } + + private void DrawMainSelectors() + { + var size = new Vector2( -1, + ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight + + _window._defaultSpace.Y * 2 + + ImGui.GetFrameHeightWithSpacing() * 4 + + ImGui.GetStyle().ItemSpacing.Y * 6 ); + using var main = ImRaii.Child( "##CollectionsMain", size, true ); + if( !main ) + { + return; + } + + DrawCurrentCollectionSelector(); + ImGui.Dummy( _window._defaultSpace ); + DrawNewCollectionInput(); + ImGui.Dummy( _window._defaultSpace ); + DrawInheritanceBlock(); + } + } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 1bd4fefc..85b713c7 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,6 +9,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Api; +using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -16,387 +17,447 @@ namespace Penumbra.UI; public partial class ConfigWindow { + private class DebugTab + { + private readonly ConfigWindow _window; + + public DebugTab( ConfigWindow window ) + => _window = window; + #if DEBUG - private const string DebugVersionString = "(Debug)"; - private const bool DefaultVisibility = true; + private const string DebugVersionString = "(Debug)"; + private const bool DefaultVisibility = true; #else - private const string DebugVersionString = "(Release)"; - private const bool DefaultVisibility = false; + private const string DebugVersionString = "(Release)"; + private const bool DefaultVisibility = false; #endif - public bool DebugTabVisible = DefaultVisibility; + public bool DebugTabVisible = DefaultVisibility; - public void DrawDebugTab() - { - if( !DebugTabVisible ) + public void Draw() { - return; - } - - using var tab = ImRaii.TabItem( "Debug" ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##DebugTab", -Vector2.One ); - if( !child ) - { - return; - } - - DrawDebugTabGeneral(); - ImGui.NewLine(); - DrawDebugTabReplacedResources(); - ImGui.NewLine(); - DrawPathResolverDebug(); - ImGui.NewLine(); - DrawDebugCharacterUtility(); - ImGui.NewLine(); - DrawDebugResidentResources(); - ImGui.NewLine(); - DrawPlayerModelInfo(); - ImGui.NewLine(); - DrawDebugTabIpc(); - ImGui.NewLine(); - } - - // Draw general information about mod and collection state. - private void DrawDebugTabGeneral() - { - if( !ImGui.CollapsingHeader( "General" ) ) - { - return; - } - - using var table = ImRaii.Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); - if( !table ) - { - return; - } - - var manager = Penumbra.ModManager; - PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); - PrintValue( "Git Commit Hash", Penumbra.CommitHash ); - PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); - PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); - PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); - PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Path Resolver Enabled", _penumbra.PathResolver.Enabled.ToString() ); - PrintValue( "Music Manager Streaming Disabled", ( !_penumbra.MusicManager.StreamingEnabled ).ToString() ); - PrintValue( "Web Server Enabled", ( _penumbra.WebServer != null ).ToString() ); - } - - // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. - // Resources are collected by iterating through the - private static unsafe void DrawDebugTabReplacedResources() - { - if( !ImGui.CollapsingHeader( "Replaced Resources" ) ) - { - return; - } - - Penumbra.ResourceLoader.UpdateDebugInfo(); - - if( Penumbra.ResourceLoader.DebugList.Count == 0 ) - { - return; - } - - using var table = ImRaii.Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) - { - var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; - var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; - ImGui.TableNextColumn(); - ImGui.Text( data.ManipulatedPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( refCountManip.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( data.OriginalPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( refCountOrig.ToString() ); - } - } - - // Draw information about which draw objects correspond to which game objects - // and which paths are due to be loaded by which collection. - private unsafe void DrawPathResolverDebug() - { - if( !ImGui.CollapsingHeader( "Path Resolver" ) ) - { - return; - } - - using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); - if( drawTree ) - { - using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); - if( table ) + if( !DebugTabVisible ) { - foreach( var (ptr, (c, idx)) in _penumbra.PathResolver.DrawObjectToObject ) + return; + } + + using var tab = ImRaii.TabItem( "Debug" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##DebugTab", -Vector2.One ); + if( !child ) + { + return; + } + + DrawDebugTabGeneral(); + ImGui.NewLine(); + DrawDebugTabReplacedResources(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); + DrawDebugCharacterUtility(); + ImGui.NewLine(); + DrawDebugResidentResources(); + ImGui.NewLine(); + DrawResourceProblems(); + ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawDebugTabIpc(); + ImGui.NewLine(); + } + + // Draw general information about mod and collection state. + private void DrawDebugTabGeneral() + { + if( !ImGui.CollapsingHeader( "General" ) ) + { + return; + } + + using var table = ImRaii.Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); + if( !table ) + { + return; + } + + var manager = Penumbra.ModManager; + PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); + PrintValue( "Git Commit Hash", Penumbra.CommitHash ); + PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); + PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); + PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); + PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); + PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); + PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); + PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); + PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); + PrintValue( "Path Resolver Enabled", _window._penumbra.PathResolver.Enabled.ToString() ); + PrintValue( "Music Manager Streaming Disabled", ( !_window._penumbra.MusicManager.StreamingEnabled ).ToString() ); + PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); + } + + // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. + // Resources are collected by iterating through the + private static unsafe void DrawDebugTabReplacedResources() + { + if( !ImGui.CollapsingHeader( "Replaced Resources" ) ) + { + return; + } + + Penumbra.ResourceLoader.UpdateDebugInfo(); + + if( Penumbra.ResourceLoader.DebugList.Count == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) + { + var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; + var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; + ImGui.TableNextColumn(); + ImGui.Text( data.ManipulatedPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountManip.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( data.OriginalPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountOrig.ToString() ); + } + } + + // Draw information about which draw objects correspond to which game objects + // and which paths are due to be loaded by which collection. + private unsafe void DrawPathResolverDebug() + { + if( !ImGui.CollapsingHeader( "Path Resolver" ) ) + { + return; + } + + using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); + if( drawTree ) + { + using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); + if( table ) { - ImGui.TableNextColumn(); - ImGui.Text( ptr.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.Text( idx.ToString() ); - ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); - ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); - ImGui.TableNextColumn(); - ImGui.Text( c.Name ); + foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectToObject ) + { + ImGui.TableNextColumn(); + ImGui.Text( ptr.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( idx.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + ImGui.TableNextColumn(); + ImGui.Text( c.Name ); + } + } + } + + drawTree.Dispose(); + + using var pathTree = ImRaii.TreeNode( "Path Collections" ); + if( pathTree ) + { + using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + ImGui.TableNextColumn(); + ImGui.Text( collection.Name ); + } } } } - drawTree.Dispose(); - - using var pathTree = ImRaii.TreeNode( "Path Collections" ); - if( pathTree ) + // Draw information about the character utility class from SE, + // displaying all files, their sizes, the default files and the default sizes. + public unsafe void DrawDebugCharacterUtility() { - using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) + if( !ImGui.CollapsingHeader( "Character Utility" ) ) { - foreach( var (path, collection) in _penumbra.PathResolver.PathCollections ) - { - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); - ImGui.TableNextColumn(); - ImGui.Text( collection.Name ); - } + return; } - } - } - // Draw information about the character utility class from SE, - // displaying all files, their sizes, the default files and the default sizes. - public unsafe void DrawDebugCharacterUtility() - { - if( !ImGui.CollapsingHeader( "Character Utility" ) ) - { - return; - } - - using var table = ImRaii.Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) - { - var idx = CharacterUtility.RelevantIndices[ i ]; - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; - ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{resource->GetData().Data:X}" ); - if( ImGui.IsItemClicked() ) + using var table = ImRaii.Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) { - var (data, length) = resource->GetData(); - if( data != IntPtr.Zero && length > 0 ) + return; + } + + for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) + { + var idx = CharacterUtility.RelevantIndices[ i ]; + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + Text( resource ); + ImGui.TableNextColumn(); + ImGui.Selectable( $"0x{resource->GetData().Data:X}" ); + if( ImGui.IsItemClicked() ) + { + var (data, length) = resource->GetData(); + if( data != IntPtr.Zero && length > 0 ) + { + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + } + + ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); + + ImGui.TableNextColumn(); + ImGui.Text( $"{resource->GetData().Length}" ); + ImGui.TableNextColumn(); + ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); + if( ImGui.IsItemClicked() ) { ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, + Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + + ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); + + ImGui.TableNextColumn(); + ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); + } + } + + // Draw information about the resident resource files. + public unsafe void DrawDebugResidentResources() + { + if( !ImGui.CollapsingHeader( "Resident Resources" ) ) + { + return; + } + + if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) + { + var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TableNextColumn(); + Text( resource ); + } + } + + // Draw information about the models, materials and resources currently loaded by the local player. + private static unsafe void DrawPlayerModelInfo() + { + var player = Dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) + { + return; + } + + var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); + if( model == null ) + { + return; + } + + using var table = ImRaii.Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( "Slot" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc File" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model File" ); + + for( var i = 0; i < model->SlotCount; ++i ) + { + var imc = ( ResourceHandle* )model->IMCArray[ i ]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( $"Slot {i}" ); + ImGui.TableNextColumn(); + ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); + ImGui.TableNextColumn(); + if( imc != null ) + { + Text( imc ); + } + + var mdl = ( RenderModel* )model->ModelArray[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); + if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + { + continue; + } + + ImGui.TableNextColumn(); + { + Text( mdl->ResourceHandle ); } } + } - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.Text( $"{resource->GetData().Length}" ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); - if( ImGui.IsItemClicked() ) + // Draw resources with unusual reference count. + private static unsafe void DrawResourceProblems() + { + var header = ImGui.CollapsingHeader( "Resource Problems" ); + ImGuiUtil.HoverTooltip( "Draw resources with unusually high reference count to detect overflows." ); + if( !header ) { - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, - Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + return; } - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); - } - } - - // Draw information about the resident resource files. - public unsafe void DrawDebugResidentResources() - { - if( !ImGui.CollapsingHeader( "Resident Resources" ) ) - { - return; - } - - if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) - { - return; - } - - using var table = ImRaii.Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) - { - var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - } - } - - // Draw information about the models, materials and resources currently loaded by the local player. - private static unsafe void DrawPlayerModelInfo() - { - var player = Dalamud.ClientState.LocalPlayer; - var name = player?.Name.ToString() ?? "NULL"; - if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - using var table = ImRaii.Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) + using var table = ImRaii.Table( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + if( !table ) { - Text( imc ); + return; } - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + ResourceLoader.IterateResources( ( _, r ) => { - continue; + if( r->RefCount < 10000 ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.Text( r->Category.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( r->FileType.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->Id.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->RefCount.ToString() ); + ImGui.TableNextColumn(); + ref var name = ref r->FileName; + if( name.Capacity > 15 ) + { + ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + } + else + { + fixed( byte* ptr = name.Buffer ) + { + ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + } + } + } ); + } + + + // Draw information about IPC options and availability. + private void DrawDebugTabIpc() + { + if( !ImGui.CollapsingHeader( "IPC" ) ) + { + return; } - ImGui.TableNextColumn(); + var ipc = _window._penumbra.Ipc; + ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); + ImGui.Text( "Available subscriptions:" ); + using var indent = ImRaii.PushIndent(); + if( ipc.ProviderApiVersion != null ) { - Text( mdl->ResourceHandle ); + ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); + } + + if( ipc.ProviderRedrawName != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); + } + + if( ipc.ProviderRedrawObject != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); + } + + if( ipc.ProviderRedrawAll != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); + } + + if( ipc.ProviderResolveDefault != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); + } + + if( ipc.ProviderResolveCharacter != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); + } + + if( ipc.ProviderChangedItemTooltip != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); + } + + if( ipc.ProviderChangedItemClick != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); + } + + if( ipc.ProviderGetChangedItems != null ) + { + ImGui.Text( PenumbraIpc.LabelProviderGetChangedItems ); } } - } - // Draw information about IPC options and availability. - private void DrawDebugTabIpc() - { - if( !ImGui.CollapsingHeader( "IPC" ) ) + // Helper to print a property and its value in a 2-column table. + private static void PrintValue( string name, string value ) { - return; + ImGui.TableNextColumn(); + ImGui.Text( name ); + ImGui.TableNextColumn(); + ImGui.Text( value ); } - - var ipc = _penumbra.Ipc; - ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); - ImGui.Text( "Available subscriptions:" ); - using var indent = ImRaii.PushIndent(); - if( ipc.ProviderApiVersion != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); - } - - if( ipc.ProviderRedrawName != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); - } - - if( ipc.ProviderRedrawObject != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); - } - - if( ipc.ProviderRedrawAll != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); - } - - if( ipc.ProviderResolveDefault != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); - } - - if( ipc.ProviderResolveCharacter != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); - } - - if( ipc.ProviderChangedItemTooltip != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); - } - - if( ipc.ProviderChangedItemClick != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); - } - - if( ipc.ProviderGetChangedItems != null ) - { - ImGui.Text( PenumbraIpc.LabelProviderGetChangedItems ); - } - } - - // Helper to print a property and its value in a 2-column table. - private static void PrintValue( string name, string value ) - { - ImGui.TableNextColumn(); - ImGui.Text( name ); - ImGui.TableNextColumn(); - ImGui.Text( value ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index b097bcb5..fc1cc3d2 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -12,204 +12,208 @@ namespace Penumbra.UI; public partial class ConfigWindow { - // Draw the effective tab if ShowAdvanced is on. - public void DrawEffectiveChangesTab() + private class EffectiveTab { - if( !Penumbra.Config.ShowAdvanced ) + // Draw the effective tab if ShowAdvanced is on. + public void Draw() { - return; - } - - using var tab = ImRaii.TabItem( "Effective Changes" ); - if( !tab ) - { - return; - } - - SetupEffectiveSizes(); - DrawFilters(); - using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var table = ImRaii.Table( "##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength ); - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); - ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); - - DrawEffectiveRows( Penumbra.CollectionManager.Default, skips, height, - _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); - } - - // Sizes - private float _effectiveLeftTextLength; - private float _effectiveRightTextLength; - private float _effectiveUnscaledArrowLength; - private float _effectiveArrowLength; - - // Setup table sizes. - private void SetupEffectiveSizes() - { - if( _effectiveUnscaledArrowLength == 0 ) - { - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - _effectiveUnscaledArrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; - } - - _effectiveArrowLength = _effectiveUnscaledArrowLength * ImGuiHelpers.GlobalScale; - _effectiveLeftTextLength = 450 * ImGuiHelpers.GlobalScale; - _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; - } - - // Filters - private LowerString _effectiveGamePathFilter = LowerString.Empty; - private LowerString _effectiveFilePathFilter = LowerString.Empty; - - // Draw the header line for filters - private void DrawFilters() - { - var tmp = _effectiveGamePathFilter.Text; - ImGui.SetNextItemWidth( _effectiveLeftTextLength ); - if( ImGui.InputTextWithHint( "##gamePathFilter", "Filter game path...", ref tmp, 256 ) ) - { - _effectiveGamePathFilter = tmp; - } - - ImGui.SameLine( _effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - tmp = _effectiveFilePathFilter.Text; - if( ImGui.InputTextWithHint( "##fileFilter", "Filter file path...", ref tmp, 256 ) ) - { - _effectiveFilePathFilter = tmp; - } - } - - // Draw all rows respecting filters and using clipping. - private void DrawEffectiveRows( ModCollection active, int skips, float height, bool hasFilters ) - { - // We can use the known counts if no filters are active. - var stop = hasFilters - ? ImGuiClip.FilteredClippedDraw( active.ResolvedFiles, skips, CheckFilters, DrawLine ) - : ImGuiClip.ClippedDraw( active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count ); - - var m = active.MetaCache; - // If no meta manipulations are active, we can just draw the end dummy. - if( m is { Count: > 0 } ) - { - // We can treat all meta manipulations the same, - // we are only really interested in their ToString function here. - static (object, int) Convert< T >( KeyValuePair< T, int > kvp ) - => ( kvp.Key!, kvp.Value ); - - var it = m.Cmp.Manipulations.Select( Convert ) - .Concat( m.Eqp.Manipulations.Select( Convert ) ) - .Concat( m.Eqdp.Manipulations.Select( Convert ) ) - .Concat( m.Gmp.Manipulations.Select( Convert ) ) - .Concat( m.Est.Manipulations.Select( Convert ) ) - .Concat( m.Imc.Manipulations.Select( Convert ) ); - - // Filters mean we can not use the known counts. - if( hasFilters ) + if( !Penumbra.Config.ShowAdvanced ) { - var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager.Mods[ p.Item2 ].Name ) ); - if( stop >= 0 ) + return; + } + + using var tab = ImRaii.TabItem( "Effective Changes" ); + if( !tab ) + { + return; + } + + SetupEffectiveSizes(); + DrawFilters(); + using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); + if( !child ) + { + return; + } + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips( height ); + using var table = ImRaii.Table( "##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg ); + if( !table ) + { + return; + } + + ImGui.TableSetupColumn( "##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength ); + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); + ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); + + DrawEffectiveRows( Penumbra.CollectionManager.Default, skips, height, + _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); + } + + // Sizes + private float _effectiveLeftTextLength; + private float _effectiveRightTextLength; + private float _effectiveUnscaledArrowLength; + private float _effectiveArrowLength; + + // Filters + private LowerString _effectiveGamePathFilter = LowerString.Empty; + private LowerString _effectiveFilePathFilter = LowerString.Empty; + + // Setup table sizes. + private void SetupEffectiveSizes() + { + if( _effectiveUnscaledArrowLength == 0 ) + { + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + _effectiveUnscaledArrowLength = + ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; + } + + _effectiveArrowLength = _effectiveUnscaledArrowLength * ImGuiHelpers.GlobalScale; + _effectiveLeftTextLength = 450 * ImGuiHelpers.GlobalScale; + _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; + } + + // Draw the header line for filters + private void DrawFilters() + { + var tmp = _effectiveGamePathFilter.Text; + ImGui.SetNextItemWidth( _effectiveLeftTextLength ); + if( ImGui.InputTextWithHint( "##gamePathFilter", "Filter game path...", ref tmp, 256 ) ) + { + _effectiveGamePathFilter = tmp; + } + + ImGui.SameLine( _effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( -1 ); + tmp = _effectiveFilePathFilter.Text; + if( ImGui.InputTextWithHint( "##fileFilter", "Filter file path...", ref tmp, 256 ) ) + { + _effectiveFilePathFilter = tmp; + } + } + + // Draw all rows respecting filters and using clipping. + private void DrawEffectiveRows( ModCollection active, int skips, float height, bool hasFilters ) + { + // We can use the known counts if no filters are active. + var stop = hasFilters + ? ImGuiClip.FilteredClippedDraw( active.ResolvedFiles, skips, CheckFilters, DrawLine ) + : ImGuiClip.ClippedDraw( active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count ); + + var m = active.MetaCache; + // If no meta manipulations are active, we can just draw the end dummy. + if( m is { Count: > 0 } ) + { + // We can treat all meta manipulations the same, + // we are only really interested in their ToString function here. + static (object, int) Convert< T >( KeyValuePair< T, int > kvp ) + => ( kvp.Key!, kvp.Value ); + + var it = m.Cmp.Manipulations.Select( Convert ) + .Concat( m.Eqp.Manipulations.Select( Convert ) ) + .Concat( m.Eqdp.Manipulations.Select( Convert ) ) + .Concat( m.Gmp.Manipulations.Select( Convert ) ) + .Concat( m.Est.Manipulations.Select( Convert ) ) + .Concat( m.Imc.Manipulations.Select( Convert ) ); + + // Filters mean we can not use the known counts. + if( hasFilters ) { - ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); + var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager.Mods[ p.Item2 ].Name ) ); + if( stop >= 0 ) + { + ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); + } + else + { + stop = ImGuiClip.FilteredClippedDraw( it2, skips, CheckFilters, DrawLine, ~stop ); + ImGuiClip.DrawEndDummy( stop, height ); + } } else { - stop = ImGuiClip.FilteredClippedDraw( it2, skips, CheckFilters, DrawLine, ~stop ); - ImGuiClip.DrawEndDummy( stop, height ); + if( stop >= 0 ) + { + ImGuiClip.DrawEndDummy( stop + m.Count, height ); + } + else + { + stop = ImGuiClip.ClippedDraw( it, skips, DrawLine, m.Count, ~stop ); + ImGuiClip.DrawEndDummy( stop, height ); + } } } else { - if( stop >= 0 ) - { - ImGuiClip.DrawEndDummy( stop + m.Count, height ); - } - else - { - stop = ImGuiClip.ClippedDraw( it, skips, DrawLine, m.Count, ~stop ); - ImGuiClip.DrawEndDummy( stop, height ); - } + ImGuiClip.DrawEndDummy( stop, height ); } } - else + + // Draw a line for a game path and its redirected file. + private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair ) { - ImGuiClip.DrawEndDummy( stop, height ); - } - } + var (path, name) = pair; + ImGui.TableNextColumn(); + CopyOnClickSelectable( path.Path ); - // Draw a line for a game path and its redirected file. - private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair ) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - CopyOnClickSelectable( path.Path ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - CopyOnClickSelectable( name.InternalName ); - } - - // Draw a line for a path and its name. - private static void DrawLine( (string, LowerString) pair ) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( path ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( name ); - } - - // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, int) pair ) - { - var (manipulation, modIdx) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager.Mods[ modIdx ].Name ); - } - - // Check filters for file replacements. - private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) - { - var (gamePath, fullPath) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) - { - return false; + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + CopyOnClickSelectable( name.InternalName ); } - return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); - } - - // Check filters for meta manipulations. - private bool CheckFilters( (string, LowerString) kvp ) - { - var (name, path) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains( _effectiveGamePathFilter.Lower ) ) + // Draw a line for a path and its name. + private static void DrawLine( (string, LowerString) pair ) { - return false; + var (path, name) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( path ); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( name ); } - return _effectiveFilePathFilter.Length == 0 || path.Contains( _effectiveFilePathFilter.Lower ); + // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. + private static void DrawLine( (object, int) pair ) + { + var (manipulation, modIdx) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager.Mods[ modIdx ].Name ); + } + + // Check filters for file replacements. + private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) + { + var (gamePath, fullPath) = kvp; + if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) + { + return false; + } + + return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); + } + + // Check filters for meta manipulations. + private bool CheckFilters( (string, LowerString) kvp ) + { + var (name, path) = kvp; + if( _effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains( _effectiveGamePathFilter.Lower ) ) + { + return false; + } + + return _effectiveFilePathFilter.Length == 0 || path.Contains( _effectiveFilePathFilter.Lower ); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 8258fe33..b4cf7835 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -43,8 +43,16 @@ public partial class ConfigWindow if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) { - using var tt = ImRaii.Tooltip(); + // We can not be sure that any subscriber actually prints something in any case. + // Circumvent ugly blank tooltip with less-ugly useless tooltip. + using var tt = ImRaii.Tooltip(); + using var group = ImRaii.Group(); _penumbra.Api.InvokeTooltip( data ); + group.Dispose(); + if( ImGui.GetItemRectSize() == Vector2.Zero ) + { + ImGui.Text( "No actions available." ); + } } if( data is Item it ) diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs b/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs index 435dffd4..63434602 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs @@ -7,608 +7,607 @@ using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; using Penumbra.Mods; -using Penumbra.UI.Custom; using Penumbra.Util; namespace Penumbra.UI; public partial class SettingsInterface { - private class ModPanel - { - private const string LabelModPanel = "selectedModInfo"; - private const string LabelEditName = "##editName"; - private const string LabelEditVersion = "##editVersion"; - private const string LabelEditAuthor = "##editAuthor"; - private const string LabelEditWebsite = "##editWebsite"; - private const string LabelModEnabled = "Enabled"; - private const string LabelEditingEnabled = "Enable Editing"; - private const string LabelOverWriteDir = "OverwriteDir"; - private const string ButtonOpenWebsite = "Open Website"; - private const string ButtonOpenModFolder = "Open Mod Folder"; - private const string ButtonRenameModFolder = "Rename Mod Folder"; - private const string ButtonEditJson = "Edit JSON"; - private const string ButtonReloadJson = "Reload JSON"; - private const string ButtonDeduplicate = "Deduplicate"; - private const string ButtonNormalize = "Normalize"; - private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; - private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application."; - private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; - private const string TooltipReloadJson = "Reload the configuration of all mods."; - private const string PopupRenameFolder = "Rename Folder"; - - private const string TooltipDeduplicate = - "Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n" - + "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!"; - - private const string TooltipNormalize = - "Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!"; - - private const float HeaderLineDistance = 10f; - private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f); - - private readonly SettingsInterface _base; - private readonly Selector _selector; - private readonly HashSet< string > _newMods; - public readonly PluginDetails Details; - - private bool _editMode; - private string _currentWebsite; - private bool _validWebsite; - - private string _fromMaterial = string.Empty; - private string _toMaterial = string.Empty; - - public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) - { - _base = ui; - _selector = s; - _newMods = newMods; - Details = new PluginDetails( _base, _selector ); - _currentWebsite = Meta?.Website ?? ""; - } - - private Mods.FullMod? Mod - => _selector.Mod; - - private ModMeta? Meta - => Mod?.Data.Meta; - - private void DrawName() - { - var name = Meta!.Name.Text; - var modManager = Penumbra.ModManager; - if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) - { - _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - if( !modManager.TemporaryModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) - { - Mod.Data.Rename( name ); - } - } - } - - private void DrawVersion() - { - if( _editMode ) - { - ImGui.BeginGroup(); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - ImGui.Text( "(Version " ); - - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); - ImGui.SameLine(); - var version = Meta!.Version; - if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) - && version != Meta.Version ) - { - Meta.Version = version; - _selector.SaveCurrentMod(); - } - - ImGui.SameLine(); - ImGui.Text( ")" ); - } - else if( Meta!.Version.Length > 0 ) - { - ImGui.Text( $"(Version {Meta.Version})" ); - } - } - - private void DrawAuthor() - { - ImGui.BeginGroup(); - ImGui.TextColored( GreyColor, "by" ); - - ImGui.SameLine(); - var author = Meta!.Author.Text; - if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) - && author != Meta.Author ) - { - Meta.Author = author; - _selector.SaveCurrentMod(); - _selector.Cache.TriggerFilterReset(); - } - - ImGui.EndGroup(); - } - - private void DrawWebsite() - { - ImGui.BeginGroup(); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - if( _editMode ) - { - ImGui.TextColored( GreyColor, "from" ); - ImGui.SameLine(); - var website = Meta!.Website; - if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) - && website != Meta.Website ) - { - Meta.Website = website; - _selector.SaveCurrentMod(); - } - } - else if( Meta!.Website.Length > 0 ) - { - if( _currentWebsite != Meta.Website ) - { - _currentWebsite = Meta.Website; - _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - } - - if( _validWebsite ) - { - if( ImGui.SmallButton( ButtonOpenWebsite ) ) - { - try - { - var process = new ProcessStartInfo( Meta.Website ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch( System.ComponentModel.Win32Exception ) - { - // Do nothing. - } - } - - ImGuiCustom.HoverTooltip( Meta.Website ); - } - else - { - ImGui.TextColored( GreyColor, "from" ); - ImGui.SameLine(); - ImGui.Text( Meta.Website ); - } - } - } - - private void DrawHeaderLine() - { - DrawName(); - ImGui.SameLine(); - DrawVersion(); - ImGui.SameLine(); - DrawAuthor(); - ImGui.SameLine(); - DrawWebsite(); - } - - private void DrawPriority() - { - var priority = Mod!.Settings.Priority; - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) - { - Penumbra.CollectionManager.Current.SetModPriority( Mod.Data.Index, priority ); - _selector.Cache.TriggerFilterReset(); - } - - ImGuiCustom.HoverTooltip( - "Higher priority mods take precedence over other mods in the case of file conflicts.\n" - + "In case of identical priority, the alphabetically first mod takes precedence." ); - } - - private void DrawEnabledMark() - { - var enabled = Mod!.Settings.Enabled; - if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) - { - Penumbra.CollectionManager.Current.SetModState( Mod.Data.Index, enabled ); - if( enabled ) - { - _newMods.Remove( Mod.Data.BasePath.Name ); - } - _selector.Cache.TriggerFilterReset(); - } - } - - public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector ) - { - var currentSortOrder = mod.Order.FullPath; - ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - manager.ChangeSortOrder( mod, currentSortOrder ); - selector.SelectModOnUpdate( mod.BasePath.Name ); - return true; - } - - return false; - } - - private void DrawEditableMark() - { - ImGui.Checkbox( LabelEditingEnabled, ref _editMode ); - } - - private void DrawOpenModFolderButton() - { - Mod!.Data.BasePath.Refresh(); - if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists ) - { - Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } ); - } - - ImGuiCustom.HoverTooltip( TooltipOpenModFolder ); - } - - private string _newName = ""; - private bool _keyboardFocus = true; - - private void RenameModFolder( string newName ) - { - _newName = newName.ReplaceBadXivSymbols(); - if( _newName.Length == 0 ) - { - PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); - ImGui.CloseCurrentPopup(); - } - else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - var dir = Mod!.Data.BasePath; - DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - - if( newDir.Exists ) - { - ImGui.OpenPopup( LabelOverWriteDir ); - } - else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) - { - _selector.ReloadCurrentMod(); - ImGui.CloseCurrentPopup(); - } - } - else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCulture ) ) - { - var dir = Mod!.Data.BasePath; - DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - var sourceUri = new Uri( dir.FullName ); - var targetUri = new Uri( newDir.FullName ); - if( sourceUri.Equals( targetUri ) ) - { - var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); - if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) ) - { - if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) - { - PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); - Penumbra.ModManager.RenameModFolder( Mod.Data, dir ); - } - - _selector.ReloadCurrentMod(); - } - - ImGui.CloseCurrentPopup(); - } - else - { - ImGui.OpenPopup( LabelOverWriteDir ); - } - } - } - - private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) - { - try - { - foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - { - var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); - if( targetFile.Exists ) - { - targetFile.Delete(); - } - - targetFile.Directory?.Create(); - file.MoveTo( targetFile.FullName ); - } - - source.Delete( true ); - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); - } - - return false; - } - - private bool OverwriteDirPopup() - { - var closeParent = false; - var _ = true; - if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - { - return closeParent; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - var dir = Mod!.Data.BasePath; - DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - ImGui.Text( - $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); - var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) ) - { - Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false ); - - _selector.SelectModOnUpdate( _newName ); - - closeParent = true; - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - - if( ImGui.Button( "Cancel", buttonSize ) ) - { - _keyboardFocus = true; - ImGui.CloseCurrentPopup(); - } - - return closeParent; - } - - private void DrawRenameModFolderPopup() - { - var _ = true; - _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); - - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) ); - if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } - - var newName = Mod!.Data.BasePath.Name; - - if( _keyboardFocus ) - { - ImGui.SetKeyboardFocusHere(); - _keyboardFocus = false; - } - - if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - RenameModFolder( newName ); - } - - ImGui.TextColored( GreyColor, - "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); - - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - - - if( OverwriteDirPopup() ) - { - ImGui.CloseCurrentPopup(); - } - } - - private void DrawRenameModFolderButton() - { - DrawRenameModFolderPopup(); - if( ImGui.Button( ButtonRenameModFolder ) ) - { - ImGui.OpenPopup( PopupRenameFolder ); - } - - ImGuiCustom.HoverTooltip( TooltipRenameModFolder ); - } - - private void DrawEditJsonButton() - { - if( ImGui.Button( ButtonEditJson ) ) - { - _selector.SaveCurrentMod(); - Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } ); - } - - ImGuiCustom.HoverTooltip( TooltipEditJson ); - } - - private void DrawReloadJsonButton() - { - if( ImGui.Button( ButtonReloadJson ) ) - { - _selector.ReloadCurrentMod( true, false ); - } - - ImGuiCustom.HoverTooltip( TooltipReloadJson ); - } - - private void DrawResetMetaButton() - { - if( ImGui.Button( "Recompute Metadata" ) ) - { - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( - "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" - + "Also reloads the mod.\n" - + "Be aware that this removes all manually added metadata changes." ); - } - - private void DrawDeduplicateButton() - { - if( ImGui.Button( ButtonDeduplicate ) ) - { - ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( TooltipDeduplicate ); - } - - private void DrawNormalizeButton() - { - if( ImGui.Button( ButtonNormalize ) ) - { - ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true, true ); - } - - ImGuiCustom.HoverTooltip( TooltipNormalize ); - } - - private void DrawAutoGenerateGroupsButton() - { - if( ImGui.Button( "Auto-Generate Groups" ) ) - { - ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod( true, true ); - } - - ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" - + "First subdirectory: Option Group\n" - + "Second subdirectory: Option Name\n" - + "Afterwards: Relative file paths.\n" - + "Experimental - Use at own risk!" ); - } - - private void DrawSplitButton() - { - if( ImGui.Button( "Split Mod" ) ) - { - ModCleanup.SplitMod( Mod!.Data ); - } - - ImGuiCustom.HoverTooltip( - "Split off all options of a mod into single mods that are placed in a collective folder.\n" - + "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n" - + "Experimental - Use at own risk!" ); - } - - private void DrawMaterialChangeRow() - { - ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 ); - ImGui.SameLine(); - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() ); - font.Pop(); - ImGui.SameLine(); - ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 ); - ImGui.SameLine(); - var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial ); - using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings ); - if( ImGui.Button( "Convert" ) && validStrings ) - { - ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial ); - } - - alpha.Pop(); - - ImGuiCustom.HoverTooltip( - "Change the skin material of all models in this mod reference " - + "from the suffix given in the first text input to " - + "the suffix given in the second input.\n" - + "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n" - + "This overwrites .mdl files, use at your own risk!" ); - } - - private void DrawEditLine() - { - DrawOpenModFolderButton(); - ImGui.SameLine(); - DrawRenameModFolderButton(); - ImGui.SameLine(); - DrawEditJsonButton(); - ImGui.SameLine(); - DrawReloadJsonButton(); - - DrawResetMetaButton(); - ImGui.SameLine(); - DrawDeduplicateButton(); - ImGui.SameLine(); - DrawNormalizeButton(); - ImGui.SameLine(); - DrawAutoGenerateGroupsButton(); - ImGui.SameLine(); - DrawSplitButton(); - - DrawMaterialChangeRow(); - - DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector ); - } - - public void Draw() - { - try - { - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); - var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); - - if( !ret || Mod == null ) - { - return; - } - - DrawHeaderLine(); - - // Next line with fixed distance. - ImGuiCustom.VerticalDistance( HeaderLineDistance ); - - DrawEnabledMark(); - ImGui.SameLine(); - DrawPriority(); - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawEditableMark(); - } - - // Next line, if editable. - if( _editMode ) - { - DrawEditLine(); - } - - Details.Draw( _editMode ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Oh no" ); - } - } - } + //private class ModPanel + //{ + // private const string LabelModPanel = "selectedModInfo"; + // private const string LabelEditName = "##editName"; + // private const string LabelEditVersion = "##editVersion"; + // private const string LabelEditAuthor = "##editAuthor"; + // private const string LabelEditWebsite = "##editWebsite"; + // private const string LabelModEnabled = "Enabled"; + // private const string LabelEditingEnabled = "Enable Editing"; + // private const string LabelOverWriteDir = "OverwriteDir"; + // private const string ButtonOpenWebsite = "Open Website"; + // private const string ButtonOpenModFolder = "Open Mod Folder"; + // private const string ButtonRenameModFolder = "Rename Mod Folder"; + // private const string ButtonEditJson = "Edit JSON"; + // private const string ButtonReloadJson = "Reload JSON"; + // private const string ButtonDeduplicate = "Deduplicate"; + // private const string ButtonNormalize = "Normalize"; + // private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; + // private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application."; + // private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; + // private const string TooltipReloadJson = "Reload the configuration of all mods."; + // private const string PopupRenameFolder = "Rename Folder"; + // + // private const string TooltipDeduplicate = + // "Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n" + // + "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!"; + // + // private const string TooltipNormalize = + // "Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!"; + // + // private const float HeaderLineDistance = 10f; + // private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f); + // + // private readonly SettingsInterface _base; + // private readonly Selector _selector; + // private readonly HashSet< string > _newMods; + // public readonly PluginDetails Details; + // + // private bool _editMode; + // private string _currentWebsite; + // private bool _validWebsite; + // + // private string _fromMaterial = string.Empty; + // private string _toMaterial = string.Empty; + // + // public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) + // { + // _base = ui; + // _selector = s; + // _newMods = newMods; + // Details = new PluginDetails( _base, _selector ); + // _currentWebsite = Meta?.Website ?? ""; + // } + // + // private Mods.FullMod? Mod + // => _selector.Mod; + // + // private ModMeta? Meta + // => Mod?.Data.Meta; + // + // private void DrawName() + // { + // var name = Meta!.Name.Text; + // var modManager = Penumbra.ModManager; + // if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) + // { + // _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); + // if( !modManager.TemporaryModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) + // { + // Mod.Data.Rename( name ); + // } + // } + // } + // + // private void DrawVersion() + // { + // if( _editMode ) + // { + // ImGui.BeginGroup(); + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); + // ImGui.Text( "(Version " ); + // + // using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); + // ImGui.SameLine(); + // var version = Meta!.Version; + // if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) + // && version != Meta.Version ) + // { + // Meta.Version = version; + // _selector.SaveCurrentMod(); + // } + // + // ImGui.SameLine(); + // ImGui.Text( ")" ); + // } + // else if( Meta!.Version.Length > 0 ) + // { + // ImGui.Text( $"(Version {Meta.Version})" ); + // } + // } + // + // private void DrawAuthor() + // { + // ImGui.BeginGroup(); + // ImGui.TextColored( GreyColor, "by" ); + // + // ImGui.SameLine(); + // var author = Meta!.Author.Text; + // if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) + // && author != Meta.Author ) + // { + // Meta.Author = author; + // _selector.SaveCurrentMod(); + // _selector.Cache.TriggerFilterReset(); + // } + // + // ImGui.EndGroup(); + // } + // + // private void DrawWebsite() + // { + // ImGui.BeginGroup(); + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); + // if( _editMode ) + // { + // ImGui.TextColored( GreyColor, "from" ); + // ImGui.SameLine(); + // var website = Meta!.Website; + // if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) + // && website != Meta.Website ) + // { + // Meta.Website = website; + // _selector.SaveCurrentMod(); + // } + // } + // else if( Meta!.Website.Length > 0 ) + // { + // if( _currentWebsite != Meta.Website ) + // { + // _currentWebsite = Meta.Website; + // _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) + // && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); + // } + // + // if( _validWebsite ) + // { + // if( ImGui.SmallButton( ButtonOpenWebsite ) ) + // { + // try + // { + // var process = new ProcessStartInfo( Meta.Website ) + // { + // UseShellExecute = true, + // }; + // Process.Start( process ); + // } + // catch( System.ComponentModel.Win32Exception ) + // { + // // Do nothing. + // } + // } + // + // ImGuiCustom.HoverTooltip( Meta.Website ); + // } + // else + // { + // ImGui.TextColored( GreyColor, "from" ); + // ImGui.SameLine(); + // ImGui.Text( Meta.Website ); + // } + // } + // } + // + // private void DrawHeaderLine() + // { + // DrawName(); + // ImGui.SameLine(); + // DrawVersion(); + // ImGui.SameLine(); + // DrawAuthor(); + // ImGui.SameLine(); + // DrawWebsite(); + // } + // + // private void DrawPriority() + // { + // var priority = Mod!.Settings.Priority; + // ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + // if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) + // { + // Penumbra.CollectionManager.Current.SetModPriority( Mod.Data.Index, priority ); + // _selector.Cache.TriggerFilterReset(); + // } + // + // ImGuiCustom.HoverTooltip( + // "Higher priority mods take precedence over other mods in the case of file conflicts.\n" + // + "In case of identical priority, the alphabetically first mod takes precedence." ); + // } + // + // private void DrawEnabledMark() + // { + // var enabled = Mod!.Settings.Enabled; + // if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) + // { + // Penumbra.CollectionManager.Current.SetModState( Mod.Data.Index, enabled ); + // if( enabled ) + // { + // _newMods.Remove( Mod.Data.BasePath.Name ); + // } + // _selector.Cache.TriggerFilterReset(); + // } + // } + // + // public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector ) + // { + // var currentSortOrder = mod.Order.FullPath; + // ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); + // if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) + // { + // manager.ChangeSortOrder( mod, currentSortOrder ); + // selector.SelectModOnUpdate( mod.BasePath.Name ); + // return true; + // } + // + // return false; + // } + // + // private void DrawEditableMark() + // { + // ImGui.Checkbox( LabelEditingEnabled, ref _editMode ); + // } + // + // private void DrawOpenModFolderButton() + // { + // Mod!.Data.BasePath.Refresh(); + // if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists ) + // { + // Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipOpenModFolder ); + // } + // + // private string _newName = ""; + // private bool _keyboardFocus = true; + // + // private void RenameModFolder( string newName ) + // { + // _newName = newName.ReplaceBadXivSymbols(); + // if( _newName.Length == 0 ) + // { + // PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); + // ImGui.CloseCurrentPopup(); + // } + // else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) + // { + // var dir = Mod!.Data.BasePath; + // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + // + // if( newDir.Exists ) + // { + // ImGui.OpenPopup( LabelOverWriteDir ); + // } + // else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) + // { + // _selector.ReloadCurrentMod(); + // ImGui.CloseCurrentPopup(); + // } + // } + // else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCulture ) ) + // { + // var dir = Mod!.Data.BasePath; + // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + // var sourceUri = new Uri( dir.FullName ); + // var targetUri = new Uri( newDir.FullName ); + // if( sourceUri.Equals( targetUri ) ) + // { + // var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); + // if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) ) + // { + // if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) + // { + // PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); + // Penumbra.ModManager.RenameModFolder( Mod.Data, dir ); + // } + // + // _selector.ReloadCurrentMod(); + // } + // + // ImGui.CloseCurrentPopup(); + // } + // else + // { + // ImGui.OpenPopup( LabelOverWriteDir ); + // } + // } + // } + // + // private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) + // { + // try + // { + // foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + // { + // var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); + // if( targetFile.Exists ) + // { + // targetFile.Delete(); + // } + // + // targetFile.Directory?.Create(); + // file.MoveTo( targetFile.FullName ); + // } + // + // source.Delete( true ); + // return true; + // } + // catch( Exception e ) + // { + // PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); + // } + // + // return false; + // } + // + // private bool OverwriteDirPopup() + // { + // var closeParent = false; + // var _ = true; + // if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) + // { + // return closeParent; + // } + // + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // + // var dir = Mod!.Data.BasePath; + // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); + // ImGui.Text( + // $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); + // var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); + // if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) ) + // { + // Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false ); + // + // _selector.SelectModOnUpdate( _newName ); + // + // closeParent = true; + // ImGui.CloseCurrentPopup(); + // } + // + // ImGui.SameLine(); + // + // if( ImGui.Button( "Cancel", buttonSize ) ) + // { + // _keyboardFocus = true; + // ImGui.CloseCurrentPopup(); + // } + // + // return closeParent; + // } + // + // private void DrawRenameModFolderPopup() + // { + // var _ = true; + // _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); + // + // ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) ); + // if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) + // { + // return; + // } + // + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); + // + // if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + // { + // ImGui.CloseCurrentPopup(); + // } + // + // var newName = Mod!.Data.BasePath.Name; + // + // if( _keyboardFocus ) + // { + // ImGui.SetKeyboardFocusHere(); + // _keyboardFocus = false; + // } + // + // if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) + // { + // RenameModFolder( newName ); + // } + // + // ImGui.TextColored( GreyColor, + // "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); + // + // ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + // + // + // if( OverwriteDirPopup() ) + // { + // ImGui.CloseCurrentPopup(); + // } + // } + // + // private void DrawRenameModFolderButton() + // { + // DrawRenameModFolderPopup(); + // if( ImGui.Button( ButtonRenameModFolder ) ) + // { + // ImGui.OpenPopup( PopupRenameFolder ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipRenameModFolder ); + // } + // + // private void DrawEditJsonButton() + // { + // if( ImGui.Button( ButtonEditJson ) ) + // { + // _selector.SaveCurrentMod(); + // Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipEditJson ); + // } + // + // private void DrawReloadJsonButton() + // { + // if( ImGui.Button( ButtonReloadJson ) ) + // { + // _selector.ReloadCurrentMod( true, false ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipReloadJson ); + // } + // + // private void DrawResetMetaButton() + // { + // if( ImGui.Button( "Recompute Metadata" ) ) + // { + // _selector.ReloadCurrentMod( true, true, true ); + // } + // + // ImGuiCustom.HoverTooltip( + // "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" + // + "Also reloads the mod.\n" + // + "Be aware that this removes all manually added metadata changes." ); + // } + // + // private void DrawDeduplicateButton() + // { + // if( ImGui.Button( ButtonDeduplicate ) ) + // { + // ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); + // _selector.SaveCurrentMod(); + // _selector.ReloadCurrentMod( true, true, true ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipDeduplicate ); + // } + // + // private void DrawNormalizeButton() + // { + // if( ImGui.Button( ButtonNormalize ) ) + // { + // ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); + // _selector.SaveCurrentMod(); + // _selector.ReloadCurrentMod( true, true, true ); + // } + // + // ImGuiCustom.HoverTooltip( TooltipNormalize ); + // } + // + // private void DrawAutoGenerateGroupsButton() + // { + // if( ImGui.Button( "Auto-Generate Groups" ) ) + // { + // ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); + // _selector.SaveCurrentMod(); + // _selector.ReloadCurrentMod( true, true ); + // } + // + // ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" + // + "First subdirectory: Option Group\n" + // + "Second subdirectory: Option Name\n" + // + "Afterwards: Relative file paths.\n" + // + "Experimental - Use at own risk!" ); + // } + // + // private void DrawSplitButton() + // { + // if( ImGui.Button( "Split Mod" ) ) + // { + // ModCleanup.SplitMod( Mod!.Data ); + // } + // + // ImGuiCustom.HoverTooltip( + // "Split off all options of a mod into single mods that are placed in a collective folder.\n" + // + "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n" + // + "Experimental - Use at own risk!" ); + // } + // + // private void DrawMaterialChangeRow() + // { + // ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + // ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 ); + // ImGui.SameLine(); + // using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + // ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() ); + // font.Pop(); + // ImGui.SameLine(); + // ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); + // ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 ); + // ImGui.SameLine(); + // var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial ); + // using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings ); + // if( ImGui.Button( "Convert" ) && validStrings ) + // { + // ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial ); + // } + // + // alpha.Pop(); + // + // ImGuiCustom.HoverTooltip( + // "Change the skin material of all models in this mod reference " + // + "from the suffix given in the first text input to " + // + "the suffix given in the second input.\n" + // + "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n" + // + "This overwrites .mdl files, use at your own risk!" ); + // } + // + // private void DrawEditLine() + // { + // DrawOpenModFolderButton(); + // ImGui.SameLine(); + // DrawRenameModFolderButton(); + // ImGui.SameLine(); + // DrawEditJsonButton(); + // ImGui.SameLine(); + // DrawReloadJsonButton(); + // + // DrawResetMetaButton(); + // ImGui.SameLine(); + // DrawDeduplicateButton(); + // ImGui.SameLine(); + // DrawNormalizeButton(); + // ImGui.SameLine(); + // DrawAutoGenerateGroupsButton(); + // ImGui.SameLine(); + // DrawSplitButton(); + // + // DrawMaterialChangeRow(); + // + // DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector ); + // } + // + // public void Draw() + // { + // try + // { + // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); + // var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); + // + // if( !ret || Mod == null ) + // { + // return; + // } + // + // DrawHeaderLine(); + // + // // Next line with fixed distance. + // ImGuiCustom.VerticalDistance( HeaderLineDistance ); + // + // DrawEnabledMark(); + // ImGui.SameLine(); + // DrawPriority(); + // if( Penumbra.Config.ShowAdvanced ) + // { + // ImGui.SameLine(); + // DrawEditableMark(); + // } + // + // // Next line, if editable. + // if( _editMode ) + // { + // DrawEditLine(); + // } + // + // Details.Draw( _editMode ); + // } + // catch( Exception ex ) + // { + // PluginLog.LogError( ex, "Oh no" ); + // } + // } + //} } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 3af34018..b0d1145e 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -1,12 +1,386 @@ using System; +using System.Diagnostics; using System.Numerics; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; namespace Penumbra.UI; +public partial class ConfigWindow +{ + private class ModPanel + { + private readonly ConfigWindow _window; + private bool _valid; + private bool _emptySetting; + private bool _inherited; + private ModFileSystem.Leaf _leaf = null!; + private Mod2 _mod = null!; + private ModSettings2 _settings = null!; + private ModCollection _collection = null!; + private string _lastWebsite = string.Empty; + private bool _websiteValid; + + private string? _currentSortOrderPath; + private int? _currentPriority; + + public ModPanel( ConfigWindow window ) + => _window = window; + + private void Init( ModFileSystemSelector selector ) + { + _valid = selector.Selected != null; + if( !_valid ) + { + return; + } + + _leaf = selector.SelectedLeaf!; + _mod = selector.Selected!; + _settings = selector.SelectedSettings; + _collection = selector.SelectedSettingCollection; + _emptySetting = _settings == ModSettings2.Empty; + _inherited = _collection != Penumbra.CollectionManager.Current; + } + + public void Draw( ModFileSystemSelector selector ) + { + Init( selector ); + if( !_valid ) + { + return; + } + + DrawInheritedWarning(); + DrawHeaderLine(); + DrawFilesystemPath(); + DrawEnabledInput(); + ImGui.SameLine(); + DrawPriorityInput(); + DrawRemoveSettings(); + DrawTabBar(); + } + + private void DrawDescriptionTab() + { + if( _mod.Description.Length == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( "Description" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##tab" ); + if( !child ) + { + return; + } + + ImGui.TextWrapped( _mod.Description ); + } + + private void DrawSettingsTab() + { + if( !_mod.HasOptions ) + { + return; + } + + using var tab = ImRaii.TabItem( "Settings" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##tab" ); + if( !child ) + { + return; + } + + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + var group = _mod.Groups[ idx ]; + if( group.Type == SelectType.Single && group.IsOption ) + { + using var id = ImRaii.PushId( idx ); + var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ idx ]; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); + if( combo ) + { + for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + { + if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, ( uint )idx2 ); + } + } + } + + combo.Dispose(); + ImGui.SameLine(); + if( group.Description.Length > 0 ) + { + ImGuiUtil.LabeledHelpMarker( group.Name, group.Description ); + } + else + { + ImGui.Text( group.Name ); + } + } + } + + // TODO add description + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + var group = _mod.Groups[ idx ]; + if( group.Type == SelectType.Multi && group.IsOption ) + { + using var id = ImRaii.PushId( idx ); + var flags = _emptySetting ? 0u : _settings.Settings[ idx ]; + Widget.BeginFramedGroup( group.Name ); + for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + { + var flag = 1u << idx2; + var setting = ( flags & flag ) != 0; + if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) ) + { + flags = setting ? flags | flag : flags & ~flag; + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, flags ); + } + } + + Widget.EndFramedGroup(); + } + } + } + + private void DrawChangedItemsTab() + { + if( _mod.ChangedItems.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( "Changed Items" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.ListBox( "##changedItems", -Vector2.One ); + if( !list ) + { + return; + } + + foreach( var (name, data) in _mod.ChangedItems ) + { + _window.DrawChangedItem( name, data ); + } + } + + private void DrawTabBar() + { + using var tabBar = ImRaii.TabBar( "##ModTabs" ); + if( !tabBar ) + { + return; + } + + DrawDescriptionTab(); + DrawSettingsTab(); + DrawChangedItemsTab(); + } + + private void DrawInheritedWarning() + { + if( _inherited ) + { + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var w = new Vector2( ImGui.GetContentRegionAvail().X, 0 ); + if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", w ) ) + { + Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false ); + } + } + } + + private void DrawPriorityInput() + { + var priority = _currentPriority ?? _settings.Priority; + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "Priority", ref priority, 0, 0 ) ) + { + _currentPriority = priority; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue ) + { + if( _currentPriority != _settings.Priority ) + { + Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value ); + } + + _currentPriority = null; + } + } + + private void DrawRemoveSettings() + { + if( _inherited ) + { + return; + } + + ImGui.SameLine(); + if( ImGui.Button( "Remove Settings" ) ) + { + Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); + } + + ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n" + + "If no inherited collection has settings for this mod, it will be disabled." ); + } + + private void DrawEnabledInput() + { + var enabled = _settings.Enabled; + if( ImGui.Checkbox( "Enabled", ref enabled ) ) + { + Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); + } + } + + private void DrawFilesystemPath() + { + var fullName = _leaf.FullName(); + var path = _currentSortOrderPath ?? fullName; + ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputText( "Sort Order", ref path, 256 ) ) + { + _currentSortOrderPath = path; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentSortOrderPath != null ) + { + if( _currentSortOrderPath != fullName ) + { + _window._penumbra.ModFileSystem.RenameAndMove( _leaf, _currentSortOrderPath ); + } + + _currentSortOrderPath = null; + } + } + + + // Draw the first info line for the mod panel, + // containing all basic meta information. + private void DrawHeaderLine() + { + DrawName(); + ImGui.SameLine(); + DrawVersion(); + ImGui.SameLine(); + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + + // Draw the mod name. + private void DrawName() + { + ImGui.Text( _mod.Name.Text ); + } + + // Draw the author of the mod, if any. + private void DrawAuthor() + { + using var group = ImRaii.Group(); + ImGuiUtil.TextColored( Colors.MetaInfoText, "by" ); + ImGui.SameLine(); + ImGui.Text( _mod.Author.IsEmpty ? "Unknown" : _mod.Author.Text ); + } + + // Draw the mod version, if any. + private void DrawVersion() + { + if( _mod.Version.Length > 0 ) + { + ImGui.Text( $"(Version {_mod.Version})" ); + } + else + { + ImGui.Dummy( Vector2.Zero ); + } + } + + // Update the last seen website and check for validity. + private void UpdateWebsite( string newWebsite ) + { + if( _lastWebsite == newWebsite ) + { + return; + } + + _lastWebsite = newWebsite; + _websiteValid = Uri.TryCreate( _lastWebsite, UriKind.Absolute, out var uriResult ) + && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); + } + + // Draw the website source either as a button to open the site, + // if it is a valid http website, or as pure text. + private void DrawWebsite() + { + UpdateWebsite( _mod.Website ); + if( _lastWebsite.Length == 0 ) + { + ImGui.Dummy( Vector2.Zero ); + return; + } + + using var group = ImRaii.Group(); + if( _websiteValid ) + { + if( ImGui.Button( "Open Website" ) ) + { + try + { + var process = new ProcessStartInfo( _lastWebsite ) + { + UseShellExecute = true, + }; + Process.Start( process ); + } + catch + { + // ignored + } + } + + ImGuiUtil.HoverTooltip( _lastWebsite ); + } + else + { + ImGuiUtil.TextColored( Colors.MetaInfoText, "from" ); + ImGui.SameLine(); + ImGui.Text( _lastWebsite ); + } + } + } +} + public partial class ConfigWindow { public void DrawModsTab() @@ -22,7 +396,7 @@ public partial class ConfigWindow return; } - Selector.Draw( GetModSelectorSize() ); + _selector.Draw( GetModSelectorSize() ); ImGui.SameLine(); using var group = ImRaii.Group(); DrawHeaderLine(); @@ -30,17 +404,10 @@ public partial class ConfigWindow using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true ); if( child ) { - DrawModPanel(); + _modPanel.Draw( _selector ); } } - private void DrawModPanel() - { - if( Selector.Selected == null ) - { - return; - } - } // Draw the header line that can quick switch between collections. private void DrawHeaderLine() @@ -71,8 +438,8 @@ public partial class ConfigWindow private void DrawInheritedCollectionButton( Vector2 width ) { - var noModSelected = Selector.Selected == null; - var collection = Selector.SelectedSettingCollection; + var noModSelected = _selector.Selected == null; + var collection = _selector.SelectedSettingCollection; var modInherited = collection != Penumbra.CollectionManager.Current; var (name, tt) = ( noModSelected, modInherited ) switch { diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index bbdef888..8d5cdfae 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -10,181 +10,145 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; -using Penumbra.UI.Custom; namespace Penumbra.UI; public partial class ConfigWindow { - // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. - public void DrawResourceManagerTab() + private class ResourceTab { - if( !DebugTabVisible ) + private readonly ConfigWindow _window; + + public ResourceTab( ConfigWindow window ) + => _window = window; + + private float _hashColumnWidth; + private float _pathColumnWidth; + private float _refsColumnWidth; + private string _resourceManagerFilter = string.Empty; + + // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. + public void Draw() { - return; - } - - using var tab = ImRaii.TabItem( "Resource Manager" ); - if( !tab ) - { - return; - } - - // Filter for resources containing the input string. - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); - - using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); - if( !child ) - { - return; - } - - unsafe - { - ResourceLoader.IterateGraphs( DrawCategoryContainer ); - } - } - - private float _hashColumnWidth; - private float _pathColumnWidth; - private float _refsColumnWidth; - private string _resourceManagerFilter = string.Empty; - - private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) - { - if( map == null ) - { - return; - } - - var label = GetNodeLabel( ( uint )category, ext, map->Count ); - using var tree = ImRaii.TreeNode( label ); - if( !tree || map->Count == 0 ) - { - return; - } - - using var table = ImRaii.Table( "##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth ); - ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); - ImGui.TableHeadersRow(); - - ResourceLoader.IterateResourceMap( map, ( hash, r ) => - { - // Filter unwanted names. - if( _resourceManagerFilter.Length != 0 - && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.InvariantCultureIgnoreCase ) ) + if( !_window._debugTab.DebugTabVisible ) { return; } - var address = $"0x{( ulong )r:X}"; - ImGuiUtil.TextNextColumn( $"0x{hash:X8}" ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( address ); - - var resource = ( Interop.Structs.ResourceHandle* )r; - ImGui.TableNextColumn(); - Text( resource ); - if( ImGui.IsItemClicked() ) + using var tab = ImRaii.TabItem( "Resource Manager" ); + if( !tab ) { - var data = Interop.Structs.ResourceHandle.GetData( resource ); - if( data != null ) - { - var length = ( int )Interop.Structs.ResourceHandle.GetLength( resource ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } + return; } - ImGuiUtil.HoverTooltip( "Click to copy byte-wise file data to clipboard, if any." ); + // Filter for resources containing the input string. + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); - ImGuiUtil.TextNextColumn( r->RefCount.ToString() ); - } ); - } + using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); + if( !child ) + { + return; + } - // Draw a full category for the resource manager. - private unsafe void DrawCategoryContainer( ResourceCategory category, - StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) - { - if( map == null ) - { - return; + unsafe + { + ResourceLoader.IterateGraphs( DrawCategoryContainer ); + } } - using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}" ); - if( tree ) + private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) { - SetTableWidths(); - ResourceLoader.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); + if( map == null ) + { + return; + } + + var label = GetNodeLabel( ( uint )category, ext, map->Count ); + using var tree = ImRaii.TreeNode( label ); + if( !tree || map->Count == 0 ) + { + return; + } + + using var table = ImRaii.Table( "##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + return; + } + + ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); + ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); + ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth ); + ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); + ImGui.TableHeadersRow(); + + ResourceLoader.IterateResourceMap( map, ( hash, r ) => + { + // Filter unwanted names. + if( _resourceManagerFilter.Length != 0 + && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + var address = $"0x{( ulong )r:X}"; + ImGuiUtil.TextNextColumn( $"0x{hash:X8}" ); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable( address ); + + var resource = ( Interop.Structs.ResourceHandle* )r; + ImGui.TableNextColumn(); + Text( resource ); + if( ImGui.IsItemClicked() ) + { + var data = Interop.Structs.ResourceHandle.GetData( resource ); + if( data != null ) + { + var length = ( int )Interop.Structs.ResourceHandle.GetLength( resource ); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } + } + + ImGuiUtil.HoverTooltip( "Click to copy byte-wise file data to clipboard, if any." ); + + ImGuiUtil.TextNextColumn( r->RefCount.ToString() ); + } ); + } + + // Draw a full category for the resource manager. + private unsafe void DrawCategoryContainer( ResourceCategory category, + StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) + { + if( map == null ) + { + return; + } + + using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}" ); + if( tree ) + { + SetTableWidths(); + ResourceLoader.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); + } + } + + // Obtain a label for an extension node. + private static string GetNodeLabel( uint label, uint type, ulong count ) + { + var (lowest, mid1, mid2, highest) = Functions.SplitBytes( type ); + return highest == 0 + ? $"({type:X8}) {( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}" + : $"({type:X8}) {( char )highest}{( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}"; + } + + // Set the widths for a resource table. + private void SetTableWidths() + { + _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; + _pathColumnWidth = ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale; + _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; } } - - // Obtain a label for an extension node. - private static string GetNodeLabel( uint label, uint type, ulong count ) - { - var (lowest, mid1, mid2, highest) = Functions.SplitBytes( type ); - return highest == 0 - ? $"({type:X8}) {( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}" - : $"({type:X8}) {( char )highest}{( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}"; - } - - // Set the widths for a resource table. - private void SetTableWidths() - { - _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; - _pathColumnWidth = ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale; - _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; - } - - //private static unsafe void DrawResourceProblems() - //{ - // if( !ImGui.CollapsingHeader( "Resource Problems##ResourceManager" ) - // || !ImGui.BeginTable( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - // { - // return; - // } - // - // using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - // - // ResourceLoader.IterateResources( ( _, r ) => - // { - // if( r->RefCount < 10000 ) - // { - // return; - // } - // - // ImGui.TableNextColumn(); - // ImGui.Text( r->Category.ToString() ); - // ImGui.TableNextColumn(); - // ImGui.Text( r->FileType.ToString( "X" ) ); - // ImGui.TableNextColumn(); - // ImGui.Text( r->Id.ToString( "X" ) ); - // ImGui.TableNextColumn(); - // ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); - // ImGui.TableNextColumn(); - // ImGui.Text( r->RefCount.ToString() ); - // ImGui.TableNextColumn(); - // ref var name = ref r->FileName; - // if( name.Capacity > 15 ) - // { - // ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); - // } - // else - // { - // fixed( byte* ptr = name.Buffer ) - // { - // ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); - // } - // } - // } ); - //} } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 5aede757..79280aee 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -10,160 +9,164 @@ namespace Penumbra.UI; public partial class ConfigWindow { - // Sets the resource logger state when toggled, - // and the filter when entered. - private void DrawRequestedResourceLogging() + private partial class SettingsTab { - var tmp = Penumbra.Config.EnableResourceLogging; - if( ImGui.Checkbox( "##resourceLogging", ref tmp ) ) + private void DrawAdvancedSettings() { - _penumbra.ResourceLogger.SetState( tmp ); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Requested Resource Logging", "Log all game paths FFXIV requests to the plugin log.\n" - + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" - + "Red boundary indicates invalid regex." ); - - ImGui.SameLine(); - - // Red borders if the string is not a valid regex. - var tmpString = Penumbra.Config.ResourceLoggingFilter; - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, !_penumbra.ResourceLogger.ValidRegex ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_penumbra.ResourceLogger.ValidRegex ); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) - { - _penumbra.ResourceLogger.SetFilter( tmpString ); - } - } - - // Toggling audio streaming will need to apply to the music manager - // and rediscover mods due to determining whether .scds will be loaded or not. - private void DrawDisableSoundStreamingBox() - { - var tmp = Penumbra.Config.DisableSoundStreaming; - if( ImGui.Checkbox( "##streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) - { - Penumbra.Config.DisableSoundStreaming = tmp; - Penumbra.Config.Save(); - if( tmp ) + if( !Penumbra.Config.ShowAdvanced || !ImGui.CollapsingHeader( "Advanced" ) ) { - _penumbra.MusicManager.DisableStreaming(); - } - else - { - _penumbra.MusicManager.EnableStreaming(); + return; } - Penumbra.ModManager.DiscoverMods(); + DrawRequestedResourceLogging(); + DrawDisableSoundStreamingBox(); + DrawEnableHttpApiBox(); + DrawEnableDebugModeBox(); + DrawEnableFullResourceLoggingBox(); + DrawReloadResourceButton(); + ImGui.NewLine(); } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Disable Audio Streaming", - "Disable streaming in the games audio engine.\n" - + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" - + "Only touch this if you experience sound problems.\n" - + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash.\n" - + "You might need to restart your game for this to fully take effect." ); - } - - // Creates and destroys the web server when toggled. - private void DrawEnableHttpApiBox() - { - var http = Penumbra.Config.EnableHttpApi; - if( ImGui.Checkbox( "##http", ref http ) ) + // Sets the resource logger state when toggled, + // and the filter when entered. + private void DrawRequestedResourceLogging() { - if( http ) + var tmp = Penumbra.Config.EnableResourceLogging; + if( ImGui.Checkbox( "##resourceLogging", ref tmp ) ) { - _penumbra.CreateWebServer(); - } - else - { - _penumbra.ShutdownWebServer(); + _window._penumbra.ResourceLogger.SetState( tmp ); } - Penumbra.Config.EnableHttpApi = http; - Penumbra.Config.Save(); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Requested Resource Logging", "Log all game paths FFXIV requests to the plugin log.\n" + + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" + + "Red boundary indicates invalid regex." ); + + ImGui.SameLine(); + + // Red borders if the string is not a valid regex. + var tmpString = Penumbra.Config.ResourceLoggingFilter; + using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, !_window._penumbra.ResourceLogger.ValidRegex ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, + !_window._penumbra.ResourceLogger.ValidRegex ); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) + { + _window._penumbra.ResourceLogger.SetFilter( tmpString ); + } } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable HTTP API", - "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); - } - - // Should only be used for debugging. - private static void DrawEnableFullResourceLoggingBox() - { - var tmp = Penumbra.Config.EnableFullResourceLogging; - if( ImGui.Checkbox( "##fullLogging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) + // Toggling audio streaming will need to apply to the music manager + // and rediscover mods due to determining whether .scds will be loaded or not. + private void DrawDisableSoundStreamingBox() { - if( tmp ) + var tmp = Penumbra.Config.DisableSoundStreaming; + if( ImGui.Checkbox( "##streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) { - Penumbra.ResourceLoader.EnableFullLogging(); - } - else - { - Penumbra.ResourceLoader.DisableFullLogging(); + Penumbra.Config.DisableSoundStreaming = tmp; + Penumbra.Config.Save(); + if( tmp ) + { + _window._penumbra.MusicManager.DisableStreaming(); + } + else + { + _window._penumbra.MusicManager.EnableStreaming(); + } + + Penumbra.ModManager.DiscoverMods(); } - Penumbra.Config.EnableFullResourceLogging = tmp; - Penumbra.Config.Save(); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Disable Audio Streaming", + "Disable streaming in the games audio engine.\n" + + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems.\n" + + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash.\n" + + "You might need to restart your game for this to fully take effect." ); } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Full Resource Logging", - "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); - } - - // Should only be used for debugging. - private static void DrawEnableDebugModeBox() - { - var tmp = Penumbra.Config.DebugMode; - if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) + // Creates and destroys the web server when toggled. + private void DrawEnableHttpApiBox() { - if( tmp ) + var http = Penumbra.Config.EnableHttpApi; + if( ImGui.Checkbox( "##http", ref http ) ) { - Penumbra.ResourceLoader.EnableDebug(); - } - else - { - Penumbra.ResourceLoader.DisableDebug(); + if( http ) + { + _window._penumbra.CreateWebServer(); + } + else + { + _window._penumbra.ShutdownWebServer(); + } + + Penumbra.Config.EnableHttpApi = http; + Penumbra.Config.Save(); } - Penumbra.Config.DebugMode = tmp; - Penumbra.Config.Save(); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable HTTP API", + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Debug Mode", - "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( "Reload Resident Resources" ) ) + // Should only be used for debugging. + private static void DrawEnableFullResourceLoggingBox() { - Penumbra.ResidentResources.Reload(); + var tmp = Penumbra.Config.EnableFullResourceLogging; + if( ImGui.Checkbox( "##fullLogging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableFullLogging(); + } + else + { + Penumbra.ResourceLoader.DisableFullLogging(); + } + + Penumbra.Config.EnableFullResourceLogging = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Full Resource Logging", + "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); } - ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" - + "You usually should not need to do this." ); - } - - private void DrawAdvancedSettings() - { - if( !Penumbra.Config.ShowAdvanced || !ImGui.CollapsingHeader( "Advanced" ) ) + // Should only be used for debugging. + private static void DrawEnableDebugModeBox() { - return; + var tmp = Penumbra.Config.DebugMode; + if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableDebug(); + } + else + { + Penumbra.ResourceLoader.DisableDebug(); + } + + Penumbra.Config.DebugMode = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Enable Debug Mode", + "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load." ); } - DrawRequestedResourceLogging(); - DrawDisableSoundStreamingBox(); - DrawEnableHttpApiBox(); - DrawEnableDebugModeBox(); - DrawEnableFullResourceLoggingBox(); - DrawReloadResourceButton(); - ImGui.NewLine(); + private static void DrawReloadResourceButton() + { + if( ImGui.Button( "Reload Resident Resources" ) ) + { + Penumbra.ResidentResources.Reload(); + } + + ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" + + "You usually should not need to do this." ); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs b/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs index 53ef7d84..55d9a66d 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs @@ -8,84 +8,90 @@ namespace Penumbra.UI; public partial class ConfigWindow { - // Store separately to use IsItemDeactivatedAfterEdit. - private float _absoluteSelectorSize = Penumbra.Config.ModSelectorAbsoluteSize; - private int _relativeSelectorSize = Penumbra.Config.ModSelectorScaledSize; - - // Different supported sort modes as a combo. - private void DrawFolderSortType() + private partial class SettingsTab { - var sortMode = Penumbra.Config.SortMode; - ImGui.SetNextItemWidth( _inputTextWidth.X ); - using var combo = ImRaii.Combo( "##sortMode", sortMode.Data().Name ); - if( combo ) + private void DrawModSelectorSettings() { - foreach( var val in Enum.GetValues< SortMode >() ) + if( !ImGui.CollapsingHeader( "Mod Selector" ) ) { - var (name, desc) = val.Data(); - if( ImGui.Selectable( name, val == sortMode ) && val != sortMode ) - { - Penumbra.Config.SortMode = val; - Selector.SetFilterDirty(); - Penumbra.Config.Save(); - } - - ImGuiUtil.HoverTooltip( desc ); + return; } + + DrawFolderSortType(); + DrawAbsoluteSizeSelector(); + DrawRelativeSizeSelector(); + + ImGui.NewLine(); } - combo.Dispose(); - ImGuiUtil.LabeledHelpMarker( "Sort Mode", "Choose the sort mode for the mod selector in the mods tab." ); - } + // Store separately to use IsItemDeactivatedAfterEdit. + private float _absoluteSelectorSize = Penumbra.Config.ModSelectorAbsoluteSize; + private int _relativeSelectorSize = Penumbra.Config.ModSelectorScaledSize; - private void DrawAbsoluteSizeSelector() - { - if( ImGuiUtil.DragFloat( "##absoluteSize", ref _absoluteSelectorSize, _inputTextWidth.X, 1, - Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f" ) - && _absoluteSelectorSize != Penumbra.Config.ModSelectorAbsoluteSize ) + // Different supported sort modes as a combo. + private void DrawFolderSortType() { - Penumbra.Config.ModSelectorAbsoluteSize = _absoluteSelectorSize; - Penumbra.Config.Save(); + var sortMode = Penumbra.Config.SortMode; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + using var combo = ImRaii.Combo( "##sortMode", sortMode.Data().Name ); + if( combo ) + { + foreach( var val in Enum.GetValues< SortMode >() ) + { + var (name, desc) = val.Data(); + if( ImGui.Selectable( name, val == sortMode ) && val != sortMode ) + { + Penumbra.Config.SortMode = val; + _window._selector.SetFilterDirty(); + Penumbra.Config.Save(); + } + + ImGuiUtil.HoverTooltip( desc ); + } + } + + combo.Dispose(); + ImGuiUtil.LabeledHelpMarker( "Sort Mode", "Choose the sort mode for the mod selector in the mods tab." ); } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Absolute Size", "The minimal absolute size of the mod selector in the mod tab in pixels." ); - } - - private void DrawRelativeSizeSelector() - { - var scaleModSelector = Penumbra.Config.ScaleModSelector; - if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + // Absolute size in pixels. + private void DrawAbsoluteSizeSelector() { - Penumbra.Config.ScaleModSelector = scaleModSelector; - Penumbra.Config.Save(); + if( ImGuiUtil.DragFloat( "##absoluteSize", ref _absoluteSelectorSize, _window._inputTextWidth.X, 1, + Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f" ) + && _absoluteSelectorSize != Penumbra.Config.ModSelectorAbsoluteSize ) + { + Penumbra.Config.ModSelectorAbsoluteSize = _absoluteSelectorSize; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Mod Selector Absolute Size", + "The minimal absolute size of the mod selector in the mod tab in pixels." ); } - ImGui.SameLine(); - if( ImGuiUtil.DragInt( "##relativeSize", ref _relativeSelectorSize, _inputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, - Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%" ) - && _relativeSelectorSize != Penumbra.Config.ModSelectorScaledSize ) + // Relative size toggle and percentage. + private void DrawRelativeSizeSelector() { - Penumbra.Config.ModSelectorScaledSize = _relativeSelectorSize; - Penumbra.Config.Save(); + var scaleModSelector = Penumbra.Config.ScaleModSelector; + if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + { + Penumbra.Config.ScaleModSelector = scaleModSelector; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + if( ImGuiUtil.DragInt( "##relativeSize", ref _relativeSelectorSize, _window._inputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, + Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%" ) + && _relativeSelectorSize != Penumbra.Config.ModSelectorScaledSize ) + { + Penumbra.Config.ModSelectorScaledSize = _relativeSelectorSize; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); - } - - private void DrawModSelectorSettings() - { - if( !ImGui.CollapsingHeader( "Mod Selector" ) ) - { - return; - } - - DrawFolderSortType(); - DrawAbsoluteSizeSelector(); - DrawRelativeSizeSelector(); - - ImGui.NewLine(); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index c07d4413..0739bd5b 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -4,187 +4,188 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Components; -using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.UI.Classes; -using Penumbra.UI.Custom; namespace Penumbra.UI; public partial class ConfigWindow { - private string _settingsNewModDirectory = string.Empty; - private readonly FileDialogManager _dialogManager = new(); - private bool _dialogOpen; - - private static bool DrawPressEnterWarning( string old, float width ) + private partial class SettingsTab { - 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 ); - } + private readonly ConfigWindow _window; - private void DrawDirectoryPickerButton() - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Select a directory via dialog.", false, true ) ) + public SettingsTab( ConfigWindow window ) + => _window = window; + + public void Draw() { - if( _dialogOpen ) + using var tab = ImRaii.TabItem( "Settings" ); + if( !tab ) { - _dialogManager.Reset(); - _dialogOpen = false; + return; } - else + + using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); + if( !child ) { - //_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => - //{ - // _newModDirectory = b ? s : _newModDirectory; - // _dialogOpen = false; - //}, _newModDirectory, false); - _dialogOpen = true; + return; + } + + DrawEnabledBox(); + DrawShowAdvancedBox(); + ImGui.NewLine(); + DrawRootFolder(); + DrawRediscoverButton(); + ImGui.NewLine(); + + DrawModSelectorSettings(); + DrawColorSettings(); + DrawAdvancedSettings(); + } + + private string? _settingsNewModDirectory; + private readonly FileDialogManager _dialogManager = new(); + private bool _dialogOpen; + + private static bool DrawPressEnterWarning( string old, float width ) + { + 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 ); + } + + private void DrawDirectoryPickerButton() + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Select a directory via dialog.", false, true ) ) + { + if( _dialogOpen ) + { + _dialogManager.Reset(); + _dialogOpen = false; + } + else + { + // TODO + //_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => + //{ + // _newModDirectory = b ? s : _newModDirectory; + // _dialogOpen = false; + //}, _newModDirectory, false); + _dialogOpen = true; + } + } + + _dialogManager.Draw(); + } + + private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) + { + using var _ = ImRaii.PushId( id ); + var ret = ImGui.Button( "Open Directory" ); + ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); + if( ret && condition && Directory.Exists( directory.FullName ) ) + { + Process.Start( new ProcessStartInfo( directory.FullName ) + { + UseShellExecute = true, + } ); } } - _dialogManager.Draw(); - } - - private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) - { - using var _ = ImRaii.PushId( id ); - var ret = ImGui.Button( "Open Directory" ); - ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); - if( ret && condition && Directory.Exists( directory.FullName ) ) + private void DrawRootFolder() { - Process.Start( new ProcessStartInfo( directory.FullName ) + _settingsNewModDirectory ??= Penumbra.Config.ModDirectory; + + var spacing = 3 * ImGuiHelpers.GlobalScale; + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() ); + var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Root Directory", "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); + group.Dispose(); + ImGui.SameLine(); + var pos = ImGui.GetCursorPosX(); + ImGui.NewLine(); + + if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 ) { - UseShellExecute = true, - } ); - } - } + return; + } - private void DrawRootFolder() - { - // Initialize first time. - if( _settingsNewModDirectory.Length == 0 ) - { - _settingsNewModDirectory = Penumbra.Config.ModDirectory; - } - - var spacing = 3 * ImGuiHelpers.GlobalScale; - using var group = ImRaii.Group(); - ImGui.SetNextItemWidth( _inputTextWidth.X - spacing - ImGui.GetFrameHeight() ); - var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Root Directory", "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); - group.Dispose(); - ImGui.SameLine(); - var pos = ImGui.GetCursorPosX(); - ImGui.NewLine(); - - if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 ) - { - return; - } - - if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) - { - Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory ); - } - } - - - private static void DrawRediscoverButton() - { - DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); - ImGui.SameLine(); - if( ImGui.Button( "Rediscover Mods" ) ) - { - Penumbra.ModManager.DiscoverMods(); - } - - ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); - } - - private void DrawEnabledBox() - { - var enabled = Penumbra.Config.EnableMods; - if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) - { - _penumbra.SetEnabled( enabled ); - } - } - - private static void DrawShowAdvancedBox() - { - var showAdvanced = Penumbra.Config.ShowAdvanced; - if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) - { - Penumbra.Config.ShowAdvanced = showAdvanced; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Show Advanced Settings", "Enable some advanced options in this window and in the mod selector.\n" - + "This is required to enable manually editing any mod information." ); - } - - private static void DrawColorSettings() - { - if( !ImGui.CollapsingHeader( "Colors" ) ) - { - return; - } - - foreach( var color in Enum.GetValues< ColorId >() ) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; - if( Widget.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) + if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) { + Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory ); + } + } + + + private static void DrawRediscoverButton() + { + DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); + ImGui.SameLine(); + if( ImGui.Button( "Rediscover Mods" ) ) + { + Penumbra.ModManager.DiscoverMods(); + } + + ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); + } + + private void DrawEnabledBox() + { + var enabled = Penumbra.Config.EnableMods; + if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) + { + _window._penumbra.SetEnabled( enabled ); + } + } + + private static void DrawShowAdvancedBox() + { + var showAdvanced = Penumbra.Config.ShowAdvanced; + if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) + { + Penumbra.Config.ShowAdvanced = showAdvanced; Penumbra.Config.Save(); } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( "Show Advanced Settings", "Enable some advanced options in this window and in the mod selector.\n" + + "This is required to enable manually editing any mod information." ); } - ImGui.NewLine(); - } - - - public void DrawSettingsTab() - { - using var tab = ImRaii.TabItem( "Settings" ); - if( !tab ) + private static void DrawColorSettings() { - return; + if( !ImGui.CollapsingHeader( "Colors" ) ) + { + return; + } + + foreach( var color in Enum.GetValues< ColorId >() ) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; + if( Widget.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) + { + Penumbra.Config.Save(); + } + } + + ImGui.NewLine(); } - - using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - DrawEnabledBox(); - DrawShowAdvancedBox(); - ImGui.NewLine(); - DrawRootFolder(); - DrawRediscoverButton(); - ImGui.NewLine(); - - DrawModSelectorSettings(); - DrawColorSettings(); - DrawAdvancedSettings(); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 338fa511..59b03525 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -14,17 +14,30 @@ namespace Penumbra.UI; public sealed partial class ConfigWindow : Window, IDisposable { private readonly Penumbra _penumbra; - public readonly ModFileSystemSelector Selector; + private readonly SettingsTab _settingsTab; + private readonly ModFileSystemSelector _selector; + private readonly ModPanel _modPanel; + private readonly CollectionsTab _collectionsTab; + private readonly EffectiveTab _effectiveTab; + private readonly DebugTab _debugTab; + private readonly ResourceTab _resourceTab; public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) { - _penumbra = penumbra; - Selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; + _penumbra = penumbra; + _settingsTab = new SettingsTab( this ); + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO + _modPanel = new ModPanel( this ); + _collectionsTab = new CollectionsTab( this ); + _effectiveTab = new EffectiveTab(); + _debugTab = new DebugTab( this ); + _resourceTab = new ResourceTab( this ); + + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = true; - RespectCloseHotkey = true; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = true; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2( 800, 600 ), @@ -36,18 +49,18 @@ public sealed partial class ConfigWindow : Window, IDisposable { using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); SetupSizes(); - DrawSettingsTab(); + _settingsTab.Draw(); DrawModsTab(); - DrawCollectionsTab(); + _collectionsTab.Draw(); DrawChangedItemTab(); - DrawEffectiveChangesTab(); - DrawDebugTab(); - DrawResourceManagerTab(); + _effectiveTab.Draw(); + _debugTab.Draw(); + _resourceTab.Draw(); } public void Dispose() { - Selector.Dispose(); + _selector.Dispose(); } private static string GetLabel() @@ -55,10 +68,12 @@ public sealed partial class ConfigWindow : Window, IDisposable ? "Penumbra###PenumbraConfigWindow" : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; + private Vector2 _defaultSpace; private Vector2 _inputTextWidth; private void SetupSizes() { - _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); + _defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale ); + _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); } } \ No newline at end of file From dbb99311898b34457e6c8b33f25b9b656c194ce1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Apr 2022 21:35:09 +0200 Subject: [PATCH 0139/2451] A lot of interface stuff, some more cleanup and fixes. Main functionality should be mostly fine, importing works. Missing a lot of mod edit options. --- OtterGui | 2 +- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Api/SimpleRedirectManager.cs | 2 +- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionManager.cs | 65 ++- Penumbra/Collections/ConflictCache.cs | 13 +- .../Collections/ModCollection.Cache.Access.cs | 5 +- Penumbra/Collections/ModCollection.Cache.cs | 9 +- Penumbra/Collections/ModCollection.Changes.cs | 6 +- Penumbra/Collections/ModCollection.File.cs | 8 +- .../Collections/ModCollection.Inheritance.cs | 3 +- .../Collections/ModCollection.Migration.cs | 6 +- Penumbra/Collections/ModCollection.cs | 22 +- Penumbra/Import/ImporterState.cs | 9 + Penumbra/Import/MetaFileInfo.cs | 126 +++++ Penumbra/Import/StreamDisposer.cs | 25 + Penumbra/Import/TexToolsImport.cs | 154 ++++++ Penumbra/Import/TexToolsImporter.Gui.cs | 94 ++++ Penumbra/Import/TexToolsImporter.ModPack.cs | 235 +++++++++ .../Import/TexToolsMeta.Deserialization.cs | 174 +++++++ Penumbra/Import/TexToolsMeta.Rgsp.cs | 81 ++++ Penumbra/Import/TexToolsMeta.cs | 96 ++++ Penumbra/Import/TexToolsStructs.cs | 74 +++ Penumbra/Importer/ImporterState.cs | 10 - .../MagicTempFileStreamManagerAndDeleter.cs | 25 - Penumbra/Importer/Models/ExtendedModPack.cs | 40 -- Penumbra/Importer/Models/SimpleModPack.cs | 25 - Penumbra/Importer/TexToolsImport.cs | 371 --------------- Penumbra/Importer/TexToolsMeta.cs | 427 ----------------- .../Loader/ResourceLoader.Replacement.cs | 1 - Penumbra/Meta/Manager/MetaManager.Imc.cs | 1 - .../Meta/Manipulations/MetaManipulation.cs | 85 +--- Penumbra/MigrateConfiguration.cs | 6 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 65 +++ ...d2.Manager.Meta.cs => Mod.Manager.Meta.cs} | 15 +- ...ager.Options.cs => Mod.Manager.Options.cs} | 192 ++++++-- ...d2.Manager.Root.cs => Mod.Manager.Root.cs} | 6 +- .../{Mod2.Manager.cs => Mod.Manager.cs} | 12 +- .../Mods/Manager/Mod2.Manager.BasePath.cs | 77 --- .../Mods/Manager/Mod2.Manager.FileSystem.cs | 12 - .../{Mod2.BasePath.cs => Mod.BasePath.cs} | 8 +- ...d2.ChangedItems.cs => Mod.ChangedItems.cs} | 2 +- Penumbra/Mods/Mod.Creation.cs | 154 ++++++ Penumbra/Mods/{Mod2.Files.cs => Mod.Files.cs} | 2 +- ...eta.Migration.cs => Mod.Meta.Migration.cs} | 78 ++- Penumbra/Mods/{Mod2.Meta.cs => Mod.Meta.cs} | 3 +- Penumbra/Mods/Mod2.Creation.cs | 108 ----- Penumbra/Mods/ModCleanup.cs | 2 +- Penumbra/Mods/ModFileSystem.cs | 73 ++- Penumbra/Mods/Subclasses/IModGroup.cs | 4 + ...ModGroup.cs => Mod.Files.MultiModGroup.cs} | 24 +- ...odGroup.cs => Mod.Files.SingleModGroup.cs} | 25 +- ...d2.Files.SubMod.cs => Mod.Files.SubMod.cs} | 36 +- Penumbra/Mods/Subclasses/ModSettings.cs | 90 +++- Penumbra/Penumbra.cs | 13 +- .../Classes/ModFileSystemSelector.Filters.cs | 15 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 121 ++++- Penumbra/UI/Classes/SubModEditWindow.cs | 225 +++++++++ Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 1 + Penumbra/UI/ConfigWindow.EffectiveTab.cs | 1 + Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 444 ++++++++++++++++++ Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 214 +++++++++ Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 207 ++++++++ Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 178 +++++++ Penumbra/UI/ConfigWindow.ModsTab.cs | 416 ++-------------- Penumbra/UI/ConfigWindow.SettingsTab.cs | 35 +- Penumbra/UI/ConfigWindow.cs | 6 +- Penumbra/UI/LaunchButton.cs | 2 +- Penumbra/Util/ArrayExtensions.cs | 35 +- Penumbra/Util/Backup.cs | 4 +- Penumbra/Util/DialogExtensions.cs | 81 ---- Penumbra/Util/DictionaryExtensions.cs | 52 ++ Penumbra/Util/Functions.cs | 19 - Penumbra/Util/ModelChanger.cs | 2 +- Penumbra/Util/SingleOrArrayConverter.cs | 45 -- Penumbra/Util/StringPathExtensions.cs | 67 --- Penumbra/Util/TempFile.cs | 23 - 77 files changed, 3332 insertions(+), 2066 deletions(-) create mode 100644 Penumbra/Import/ImporterState.cs create mode 100644 Penumbra/Import/MetaFileInfo.cs create mode 100644 Penumbra/Import/StreamDisposer.cs create mode 100644 Penumbra/Import/TexToolsImport.cs create mode 100644 Penumbra/Import/TexToolsImporter.Gui.cs create mode 100644 Penumbra/Import/TexToolsImporter.ModPack.cs create mode 100644 Penumbra/Import/TexToolsMeta.Deserialization.cs create mode 100644 Penumbra/Import/TexToolsMeta.Rgsp.cs create mode 100644 Penumbra/Import/TexToolsMeta.cs create mode 100644 Penumbra/Import/TexToolsStructs.cs delete mode 100644 Penumbra/Importer/ImporterState.cs delete mode 100644 Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs delete mode 100644 Penumbra/Importer/Models/ExtendedModPack.cs delete mode 100644 Penumbra/Importer/Models/SimpleModPack.cs delete mode 100644 Penumbra/Importer/TexToolsImport.cs delete mode 100644 Penumbra/Importer/TexToolsMeta.cs create mode 100644 Penumbra/Mods/Manager/Mod.Manager.BasePath.cs rename Penumbra/Mods/Manager/{Mod2.Manager.Meta.cs => Mod.Manager.Meta.cs} (91%) rename Penumbra/Mods/Manager/{Mod2.Manager.Options.cs => Mod.Manager.Options.cs} (53%) rename Penumbra/Mods/Manager/{Mod2.Manager.Root.cs => Mod.Manager.Root.cs} (96%) rename Penumbra/Mods/Manager/{Mod2.Manager.cs => Mod.Manager.cs} (64%) delete mode 100644 Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs delete mode 100644 Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs rename Penumbra/Mods/{Mod2.BasePath.cs => Mod.BasePath.cs} (82%) rename Penumbra/Mods/{Mod2.ChangedItems.cs => Mod.ChangedItems.cs} (95%) create mode 100644 Penumbra/Mods/Mod.Creation.cs rename Penumbra/Mods/{Mod2.Files.cs => Mod.Files.cs} (99%) rename Penumbra/Mods/{Mod2.Meta.Migration.cs => Mod.Meta.Migration.cs} (61%) rename Penumbra/Mods/{Mod2.Meta.cs => Mod.Meta.cs} (98%) delete mode 100644 Penumbra/Mods/Mod2.Creation.cs rename Penumbra/Mods/Subclasses/{Mod2.Files.MultiModGroup.cs => Mod.Files.MultiModGroup.cs} (70%) rename Penumbra/Mods/Subclasses/{Mod2.Files.SingleModGroup.cs => Mod.Files.SingleModGroup.cs} (68%) rename Penumbra/Mods/Subclasses/{Mod2.Files.SubMod.cs => Mod.Files.SubMod.cs} (81%) create mode 100644 Penumbra/UI/Classes/SubModEditWindow.cs create mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Edit.cs create mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Header.cs create mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Settings.cs create mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs delete mode 100644 Penumbra/Util/DialogExtensions.cs create mode 100644 Penumbra/Util/DictionaryExtensions.cs delete mode 100644 Penumbra/Util/Functions.cs delete mode 100644 Penumbra/Util/SingleOrArrayConverter.cs delete mode 100644 Penumbra/Util/StringPathExtensions.cs delete mode 100644 Penumbra/Util/TempFile.cs diff --git a/OtterGui b/OtterGui index a832fb6c..1a3cd1f8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a832fb6ca5e7c6cb4e35a51a08d30d1800f405da +Subproject commit 1a3cd1f881f3b6c2c4d9d4b20f054d1ab5ccc014 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8610ac17..cac23ecb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -76,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mods.Mod2.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs index 1c633a65..6b4a45b3 100644 --- a/Penumbra/Api/SimpleRedirectManager.cs +++ b/Penumbra/Api/SimpleRedirectManager.cs @@ -48,7 +48,7 @@ public class SimpleRedirectManager return RedirectResult.NoPermission; } - if( Mod2.FilterFile( path ) ) + if( Mod.FilterFile( path ) ) { return RedirectResult.FilteredGamePath; } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 8674d5a3..fe396ccd 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -275,7 +275,7 @@ public partial class ModCollection } } - private void OnModRemovedActive( bool meta, IEnumerable< ModSettings2? > settings ) + private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) { foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index a1766e96..03b51406 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; +using OtterGui.Filesystem; using Penumbra.Mods; using Penumbra.Util; @@ -27,7 +28,7 @@ public partial class ModCollection public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, string? characterName = null ); - private readonly Mod2.Manager _modManager; + private readonly Mod.Manager _modManager; // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -59,7 +60,7 @@ public partial class ModCollection public IEnumerable< ModCollection > GetEnumeratorWithEmpty() => _collections; - public Manager( Mod2.Manager manager ) + public Manager( Mod.Manager manager ) { _modManager = manager; @@ -207,7 +208,7 @@ public partial class ModCollection // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChanged( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, + private void OnModPathChanged( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory ) { switch( type ) @@ -221,10 +222,10 @@ public partial class ModCollection OnModAddedActive( mod.TotalManipulations > 0 ); break; case ModPathChangeType.Deleted: - var settings = new List< ModSettings2? >( _collections.Count ); + var settings = new List< ModSettings? >( _collections.Count ); foreach( var collection in this ) { - settings.Add( collection[ mod.Index ].Settings ); + settings.Add( collection._settings[ mod.Index ] ); collection.RemoveMod( mod, mod.Index ); } @@ -242,26 +243,50 @@ public partial class ModCollection } } - - private void OnModOptionsChanged( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + // Automatically update all relevant collections when a mod is changed. + // This means saving if options change in a way where the settings may change and the collection has settings for this mod. + // And also updating effective file and meta manipulation lists if necessary. + private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { - if( type == ModOptionChangeType.DisplayChange ) + var (handleChanges, recomputeList, withMeta) = type switch { - return; + ModOptionChangeType.GroupRenamed => ( true, false, false ), + ModOptionChangeType.GroupAdded => ( true, false, false ), + ModOptionChangeType.GroupDeleted => ( true, true, true ), + ModOptionChangeType.GroupMoved => ( true, false, false ), + ModOptionChangeType.GroupTypeChanged => ( true, true, true ), + ModOptionChangeType.PriorityChanged => ( true, true, true ), + ModOptionChangeType.OptionAdded => ( true, true, true ), + ModOptionChangeType.OptionDeleted => ( true, true, true ), + ModOptionChangeType.OptionMoved => ( true, false, false ), + ModOptionChangeType.OptionFilesChanged => ( false, true, false ), + ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), + ModOptionChangeType.OptionMetaChanged => ( false, true, true ), + ModOptionChangeType.OptionUpdated => ( false, true, true ), + ModOptionChangeType.DisplayChange => ( false, false, false ), + _ => ( false, false, false ), + }; + + if( handleChanges ) + { + foreach( var collection in this ) + { + if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false ) + { + collection.Save(); + } + } } - // TODO - switch( type ) + if( recomputeList ) { - case ModOptionChangeType.GroupRenamed: - case ModOptionChangeType.GroupAdded: - case ModOptionChangeType.GroupDeleted: - case ModOptionChangeType.PriorityChanged: - case ModOptionChangeType.OptionAdded: - case ModOptionChangeType.OptionDeleted: - case ModOptionChangeType.OptionChanged: - default: - throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + foreach( var collection in this.Where( c => c.HasCache ) ) + { + if( collection[ mod.Index ].Settings is { Enabled: true } ) + { + collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default ); + } + } } } diff --git a/Penumbra/Collections/ConflictCache.cs b/Penumbra/Collections/ConflictCache.cs index 1891a8bb..7ff26f76 100644 --- a/Penumbra/Collections/ConflictCache.cs +++ b/Penumbra/Collections/ConflictCache.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -73,9 +73,16 @@ public struct ConflictCache } // Find all mod conflicts concerning the specified mod (in both directions). - public IEnumerable< Conflict > ModConflicts( int modIdx ) + public SubList< Conflict > ModConflicts( int modIdx ) { - return _conflicts.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ); + var start = _conflicts.FindIndex( c => c.Mod1 == modIdx ); + if( start < 0 ) + { + return SubList< Conflict >.Empty; + } + + var end = _conflicts.FindIndex( start, c => c.Mod1 != modIdx ); + return new SubList< Conflict >( _conflicts, start, end - start ); } private void Sort() diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8e58762a..168dbed5 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Dalamud.Logging; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; @@ -64,8 +65,8 @@ public partial class ModCollection internal IReadOnlyList< ConflictCache.Conflict > Conflicts => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); - internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); + internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty; // Update the effective file list for the given cache. // Creates a cache if necessary. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 6de9072c..5283ec4f 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -18,7 +18,7 @@ public partial class ModCollection // Shared caches to avoid allocations. private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024); private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024); - private static readonly List< ModSettings2? > ResolvedSettings = new(128); + private static readonly List< ModSettings? > ResolvedSettings = new(128); private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); @@ -225,7 +225,7 @@ public partial class ModCollection foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) ) { // Skip all filtered files - if( Mod2.FilterFile( path ) ) + if( Mod.FilterFile( path ) ) { continue; } @@ -257,6 +257,11 @@ public partial class ModCollection { var config = settings.Settings[ idx ]; var group = mod.Groups[ idx ]; + if( group.Count == 0 ) + { + continue; + } + switch( group.Type ) { case SelectType.Single: diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 4b350758..cbf0b09d 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -46,7 +46,7 @@ public partial class ModCollection } // Enable or disable the mod inheritance of every mod in mods. - public void SetMultipleModInheritances( IEnumerable< Mod2 > mods, bool inherit ) + public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) { if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) { @@ -56,7 +56,7 @@ public partial class ModCollection // Set the enabled state of every mod in mods to the new value. // If the mod is currently inherited, stop the inheritance. - public void SetMultipleModStates( IEnumerable< Mod2 > mods, bool newValue ) + public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue ) { var changes = false; foreach( var mod in mods ) @@ -137,7 +137,7 @@ public partial class ModCollection return false; } - _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); + _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); return true; } diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 0311e0b0..262208b4 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -6,8 +6,8 @@ using System.Text; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; @@ -48,7 +48,7 @@ public partial class ModCollection if( settings != null ) { j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, new ModSettings2.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); + x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); } } @@ -111,8 +111,8 @@ public partial class ModCollection var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; // Custom deserialization that is converted with the constructor. - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings2.SavedSettings > >() - ?? new Dictionary< string, ModSettings2.SavedSettings >(); + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings.SavedSettings > >() + ?? new Dictionary< string, ModSettings.SavedSettings >(); inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); return new ModCollection( name, version, settings ); diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index b9c75d9f..dfa2ce0f 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OtterGui.Filesystem; using Penumbra.Mods; using Penumbra.Util; @@ -119,7 +120,7 @@ public partial class ModCollection // Obtain the actual settings for a given mod via index. // Also returns the collection the settings are taken from. // If no collection provides settings for this mod, this collection is returned together with null. - public (ModSettings2? Settings, ModCollection Collection) this[ Index idx ] + public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] { get { diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 4bfcccda..74215dd8 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -45,13 +45,13 @@ public sealed partial class ModCollection } // We treat every completely defaulted setting as inheritance-ready. - private static bool SettingIsDefaultV0( ModSettings2.SavedSettings setting ) + private static bool SettingIsDefaultV0( ModSettings.SavedSettings setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); - private static bool SettingIsDefaultV0( ModSettings2? setting ) + private static bool SettingIsDefaultV0( ModSettings? setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 ); } - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings2.SavedSettings > allSettings ) + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings.SavedSettings > allSettings ) => new(name, 0, allSettings); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index ce3df492..a540272f 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -27,17 +27,17 @@ public partial class ModCollection // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List< ModSettings2? > _settings; + private readonly List< ModSettings? > _settings; - public IReadOnlyList< ModSettings2? > Settings + public IReadOnlyList< ModSettings? > Settings => _settings; // Evaluates the settings along the whole inheritance tree. - public IEnumerable< ModSettings2? > ActualSettings + public IEnumerable< ModSettings? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary< string, ModSettings2.SavedSettings > _unusedSettings; + private readonly Dictionary< string, ModSettings.SavedSettings > _unusedSettings; // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) @@ -52,13 +52,13 @@ public partial class ModCollection } // Constructor for reading from files. - private ModCollection( string name, int version, Dictionary< string, ModSettings2.SavedSettings > allSettings ) + private ModCollection( string name, int version, Dictionary< string, ModSettings.SavedSettings > allSettings ) { Name = name; Version = version; _unusedSettings = allSettings; - _settings = new List< ModSettings2? >(); + _settings = new List< ModSettings? >(); ApplyModSettings(); Migration.Migrate( this ); @@ -68,7 +68,7 @@ public partial class ModCollection // Create a new, unique empty collection of a given name. public static ModCollection CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings2.SavedSettings >()); + => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) @@ -86,7 +86,7 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private bool AddMod( Mod2 mod ) + private bool AddMod( Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) ) { @@ -101,12 +101,12 @@ public partial class ModCollection } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mod2 mod, int idx ) + private void RemoveMod( Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings.Add( mod.BasePath.Name, new ModSettings2.SavedSettings( settings, mod ) ); + _unusedSettings.Add( mod.BasePath.Name, new ModSettings.SavedSettings( settings, mod ) ); } _settings.RemoveAt( idx ); @@ -127,7 +127,7 @@ public partial class ModCollection { foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) { - _unusedSettings[ mod.BasePath.Name ] = new ModSettings2.SavedSettings( setting!, mod ); + _unusedSettings[ mod.BasePath.Name ] = new ModSettings.SavedSettings( setting!, mod ); } _settings.Clear(); diff --git a/Penumbra/Import/ImporterState.cs b/Penumbra/Import/ImporterState.cs new file mode 100644 index 00000000..5a9476e6 --- /dev/null +++ b/Penumbra/Import/ImporterState.cs @@ -0,0 +1,9 @@ +namespace Penumbra.Import; + +public enum ImporterState +{ + None, + WritingPackToDisk, + ExtractingModFiles, + Done, +} \ No newline at end of file diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs new file mode 100644 index 00000000..5393fa6c --- /dev/null +++ b/Penumbra/Import/MetaFileInfo.cs @@ -0,0 +1,126 @@ +using System.Text.RegularExpressions; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Util; + +namespace Penumbra.Import; + +// Obtain information what type of object is manipulated +// by the given .meta file from TexTools, using its name. +public class MetaFileInfo +{ + private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex + private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex + private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex + private const string Pir = @"\k'PrimaryId'"; // language=regex + private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex + private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex + private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex + private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex + private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex + private const string Ext = @"\.meta"; + + // These are the valid regexes for .meta files that we are able to support at the moment. + private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled); + private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled); + + public readonly ObjectType PrimaryType; + public readonly BodySlot SecondaryType; + public readonly ushort PrimaryId; + public readonly ushort SecondaryId; + public readonly EquipSlot EquipSlot = EquipSlot.Unknown; + public readonly CustomizationType CustomizationType = CustomizationType.Unknown; + + private static bool ValidType( ObjectType type ) + { + return type switch + { + ObjectType.Accessory => true, + ObjectType.Character => true, + ObjectType.Equipment => true, + ObjectType.DemiHuman => true, + ObjectType.Housing => true, + ObjectType.Monster => true, + ObjectType.Weapon => true, + ObjectType.Icon => false, + ObjectType.Font => false, + ObjectType.Interface => false, + ObjectType.LoadingScreen => false, + ObjectType.Map => false, + ObjectType.Vfx => false, + ObjectType.Unknown => false, + ObjectType.World => false, + _ => false, + }; + } + + public MetaFileInfo( string fileName ) + : this( new GamePath( fileName ) ) + { } + + public MetaFileInfo( GamePath fileName ) + { + // Set the primary type from the gamePath start. + PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); + PrimaryId = 0; + SecondaryType = BodySlot.Unknown; + SecondaryId = 0; + // Not all types of objects can have valid meta data manipulation. + if( !ValidType( PrimaryType ) ) + { + PrimaryType = ObjectType.Unknown; + return; + } + + // Housing files have a separate regex that just contains the primary id. + if( PrimaryType == ObjectType.Housing ) + { + var housingMatch = HousingMeta.Match( fileName ); + if( housingMatch.Success ) + { + PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value ); + } + + return; + } + + // Non-housing is in chara/. + var match = CharaMeta.Match( fileName ); + if( !match.Success ) + { + return; + } + + // The primary ID has to be available for every object. + PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value ); + + // Depending on slot, we can set equip slot or customization type. + if( match.Groups[ "Slot" ].Success ) + { + switch( PrimaryType ) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) ) + { + EquipSlot = tmpSlot; + } + + break; + case ObjectType.Character: + if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) ) + { + CustomizationType = tmpCustom; + } + + break; + } + } + + // Secondary type and secondary id are for weapons and demihumans. + if( match.Groups[ "SecondaryType" ].Success + && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) + { + SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/StreamDisposer.cs b/Penumbra/Import/StreamDisposer.cs new file mode 100644 index 00000000..09300ed1 --- /dev/null +++ b/Penumbra/Import/StreamDisposer.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; +using Penumbra.Util; + +namespace Penumbra.Import; + +// Create an automatically disposing SqPack stream. +public class StreamDisposer : PenumbraSqPackStream, IDisposable +{ + private readonly FileStream _fileStream; + + public StreamDisposer( FileStream stream ) + : base( stream ) + => _fileStream = stream; + + public new void Dispose() + { + var filePath = _fileStream.Name; + + base.Dispose(); + _fileStream.Dispose(); + + File.Delete( filePath ); + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs new file mode 100644 index 00000000..ed4e6cd1 --- /dev/null +++ b/Penumbra/Import/TexToolsImport.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Logging; +using ICSharpCode.SharpZipLib.Zip; +using Newtonsoft.Json; +using Penumbra.Util; +using FileMode = System.IO.FileMode; + +namespace Penumbra.Import; + +public partial class TexToolsImporter +{ + private const string TempFileName = "textools-import"; + private static readonly JsonSerializerSettings JsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; + + private readonly DirectoryInfo _baseDirectory; + private readonly string _tmpFile; + + private readonly IEnumerable< FileInfo > _modPackFiles; + private readonly int _modPackCount; + + public ImporterState State { get; private set; } + public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; + + public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files ) + : this( baseDirectory, files.Count, files ) + { } + + public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles ) + { + _baseDirectory = baseDirectory; + _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); + _modPackFiles = modPackFiles; + _modPackCount = count; + ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); + Task.Run( ImportFiles ); + } + + private void ImportFiles() + { + State = ImporterState.None; + _currentModPackIdx = 0; + foreach( var file in _modPackFiles ) + { + try + { + var directory = VerifyVersionAndImport( file ); + ExtractedMods.Add( ( file, directory, null ) ); + } + catch( Exception e ) + { + ExtractedMods.Add( ( file, null, e ) ); + _currentNumOptions = 0; + _currentOptionIdx = 0; + _currentFileIdx = 0; + _currentNumFiles = 0; + } + + ++_currentModPackIdx; + } + + State = ImporterState.Done; + } + + // Rudimentary analysis of a TTMP file by extension and version. + // Puts out warnings if extension does not correspond to data. + private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) + { + using var zfs = modPackFile.OpenRead(); + using var extractedModPack = new ZipFile( zfs ); + + var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); + if( mpl == null ) + { + throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); + } + + var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); + + // At least a better validation than going by the extension. + if( modRaw.Contains( "\"TTMPVersion\":" ) ) + { + if( modPackFile.Extension != ".ttmp2" ) + { + PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); + } + + return ImportV2ModPack( _: modPackFile, extractedModPack, modRaw ); + } + + if( modPackFile.Extension != ".ttmp" ) + { + PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); + } + + return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); + } + + + // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry + private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) + { + for( var i = 0; i < file.Count; i++ ) + { + var entry = file[ i ]; + + if( entry.Name.Contains( fileName ) ) + { + return entry; + } + } + + return null; + } + + private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) + => file.GetInputStream( entry ); + + private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) + { + using var ms = new MemoryStream(); + using var s = GetStreamFromZipEntry( file, entry ); + s.CopyTo( ms ); + return encoding.GetString( ms.ToArray() ); + } + + private void WriteZipEntryToTempFile( Stream s ) + { + using var fs = new FileStream( _tmpFile, FileMode.Create ); + s.CopyTo( fs ); + } + + private PenumbraSqPackStream GetSqPackStreamStream( ZipFile file, string entryName ) + { + State = ImporterState.WritingPackToDisk; + + // write shitty zip garbage to disk + var entry = FindZipEntry( file, entryName ); + if( entry == null ) + { + throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); + } + + using var s = file.GetInputStream( entry ); + + WriteZipEntryToTempFile( s ); + + var fs = new FileStream( _tmpFile, FileMode.Open ); + return new StreamDisposer( fs ); + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs new file mode 100644 index 00000000..e510b149 --- /dev/null +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.UI.Classes; + +namespace Penumbra.Import; + +public partial class TexToolsImporter +{ + // Progress Data + private int _currentModPackIdx; + private int _currentOptionIdx; + private int _currentFileIdx; + + private int _currentNumOptions; + private int _currentNumFiles; + private string _currentModName = string.Empty; + private string _currentGroupName = string.Empty; + private string _currentOptionName = string.Empty; + private string _currentFileName = string.Empty; + + + public void DrawProgressInfo( Vector2 size ) + { + if( _modPackCount == 0 ) + { + ImGuiUtil.Center( "Nothing to extract." ); + } + else if( _modPackCount == _currentModPackIdx ) + { + DrawEndState(); + } + else + { + ImGui.NewLine(); + var percentage = _modPackCount / ( float )_currentModPackIdx; + ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" ); + ImGui.NewLine(); + ImGui.Text( $"Extracting {_currentModName}..." ); + + if( _currentNumOptions > 1 ) + { + ImGui.NewLine(); + ImGui.NewLine(); + percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions; + ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" ); + ImGui.NewLine(); + ImGui.Text( + $"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." ); + } + + ImGui.NewLine(); + ImGui.NewLine(); + percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles; + ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" ); + ImGui.NewLine(); + ImGui.Text( $"Extracting file {_currentFileName}..." ); + } + } + + + private void DrawEndState() + { + var success = ExtractedMods.Count( t => t.Mod != null ); + + ImGui.Text( $"Successfully extracted {success} / {ExtractedMods.Count} files." ); + ImGui.NewLine(); + using var table = ImRaii.Table( "##files", 2 ); + if( !table ) + { + return; + } + + foreach( var (file, dir, ex) in ExtractedMods ) + { + ImGui.TableNextColumn(); + ImGui.Text( file.Name ); + ImGui.TableNextColumn(); + if( dir != null ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); + ImGui.Text( dir.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ); + } + else + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); + ImGui.Text( ex!.Message ); + ImGuiUtil.HoverTooltip( ex.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs new file mode 100644 index 00000000..5ac31a35 --- /dev/null +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Logging; +using ICSharpCode.SharpZipLib.Zip; +using Newtonsoft.Json; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Import; + +public partial class TexToolsImporter +{ + // Version 1 mod packs are a simple collection of files without much information. + private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + { + _currentOptionIdx = 0; + _currentNumOptions = 1; + _currentModName = modPackFile.Name.Length > 0 ? modPackFile.Name : DefaultTexToolsData.Name; + _currentGroupName = string.Empty; + _currentOptionName = DefaultTexToolsData.DefaultOption; + + PluginLog.Log( " -> Importing V1 ModPack" ); + + var modListRaw = modRaw.Split( + new[] { "\r\n", "\r", "\n" }, + StringSplitOptions.RemoveEmptyEntries + ); + + var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); + + // Open the mod data file from the mod pack as a SqPackStream + using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + + var ret = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + // Create a new ModMeta from the TTMP mod list info + Mod.CreateMeta( ret, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); + + ExtractSimpleModList( ret, modList, modData ); + + return ret; + } + + // Version 2 mod packs can either be simple or extended, import accordingly. + private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) + { + var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!; + + if( modList.TtmpVersion.EndsWith( "s" ) ) + { + return ImportSimpleV2ModPack( extractedModPack, modList ); + } + + if( modList.TtmpVersion.EndsWith( "w" ) ) + { + return ImportExtendedV2ModPack( extractedModPack, modRaw ); + } + + try + { + PluginLog.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." ); + return ImportSimpleV2ModPack( extractedModPack, modList ); + } + catch( Exception e1 ) + { + PluginLog.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" ); + try + { + return ImportExtendedV2ModPack( extractedModPack, modRaw ); + } + catch( Exception e2 ) + { + throw new IOException( "Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2 ); + } + } + } + + // Simple V2 mod packs are basically the same as V1 mod packs. + private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) + { + _currentOptionIdx = 0; + _currentNumOptions = 1; + _currentModName = modList.Name; + _currentGroupName = string.Empty; + _currentOptionName = DefaultTexToolsData.DefaultOption; + PluginLog.Log( " -> Importing Simple V2 ModPack" ); + + // Open the mod data file from the mod pack as a SqPackStream + using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + + var ret = Mod.CreateModFolder( _baseDirectory, _currentModName ); + Mod.CreateMeta( ret, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) + ? "Mod imported from TexTools mod pack" + : modList.Description, null, null ); + + ExtractSimpleModList( ret, modList.SimpleModsList, modData ); + return ret; + } + + // Obtain the number of relevant options to extract. + private static int GetOptionCount( ExtendedModPack pack ) + => ( pack.SimpleModsList.Length > 0 ? 1 : 0 ) + + pack.ModPackPages + .Sum( page => page.ModGroups + .Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 ) + .Sum( group => group.OptionList + .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) ) ); + + // Extended V2 mod packs contain multiple options that need to be handled separately. + private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) + { + _currentOptionIdx = 0; + PluginLog.Log( " -> Importing Extended V2 ModPack" ); + + var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!; + _currentNumOptions = GetOptionCount( modList ); + _currentModName = modList.Name; + // Open the mod data file from the mod pack as a SqPackStream + using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + + var ret = Mod.CreateModFolder( _baseDirectory, _currentModName ); + Mod.CreateMeta( ret, _currentModName, modList.Author, modList.Description, modList.Version, null ); + + if( _currentNumOptions == 0 ) + { + return ret; + } + + // It can contain a simple list, still. + if( modList.SimpleModsList.Length > 0 ) + { + _currentGroupName = string.Empty; + _currentOptionName = "Default"; + ExtractSimpleModList( ret, modList.SimpleModsList, modData ); + } + + // Iterate through all pages + var options = new List< ISubMod >(); + var groupPriority = 0; + foreach( var page in modList.ModPackPages ) + { + foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) ) + { + _currentGroupName = group.GroupName; + options.Clear(); + var description = new StringBuilder(); + var groupFolder = Mod.NewSubFolderName( ret, group.GroupName ) + ?? new DirectoryInfo( Path.Combine( ret.FullName, $"Group {groupPriority + 1}" ) ); + + var optionIdx = 1; + + foreach( var option in group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ) ) + { + _currentOptionName = option.Name; + var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name ) + ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {optionIdx}" ) ); + ExtractSimpleModList( optionFolder, option.ModsJsons, modData ); + options.Add( Mod.CreateSubMod( ret, optionFolder, option ) ); + description.Append( option.Description ); + if( !string.IsNullOrEmpty( option.Description ) ) + { + description.Append( '\n' ); + } + + ++optionIdx; + ++_currentOptionIdx; + } + + Mod.CreateOptionGroup( ret, group, groupPriority++, description.ToString(), options ); + } + } + + Mod.CreateDefaultFiles( ret ); + return ret; + } + + private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods, PenumbraSqPackStream dataStream ) + { + State = ImporterState.ExtractingModFiles; + + _currentFileIdx = 0; + _currentNumFiles = mods.Count; + + // Extract each SimpleMod into the new mod folder + foreach( var simpleMod in mods ) + { + ExtractMod( outDirectory, simpleMod, dataStream ); + ++_currentFileIdx; + } + } + + private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream ) + { + PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) ); + + try + { + var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); + + _currentFileName = mod.FullPath; + var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) ); + + extractedFile.Directory?.Create(); + + if( extractedFile.FullName.EndsWith( ".mdl" ) ) + { + ProcessMdl( data.Data ); + } + + File.WriteAllBytes( extractedFile.FullName, data.Data ); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "Could not extract mod." ); + } + } + + private static void ProcessMdl( byte[] mdl ) + { + const int modelHeaderLodOffset = 22; + + // Model file header LOD num + mdl[ 64 ] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32( mdl, 4 ); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs new file mode 100644 index 00000000..5e79659b --- /dev/null +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using Dalamud.Logging; +using Lumina.Extensions; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Import; + +public partial class TexToolsMeta +{ + // Deserialize and check Eqp Entries and add them to the list if they are non-default. + private void DeserializeEqpEntry( MetaFileInfo metaFileInfo, byte[]? data ) + { + // Eqp can only be valid for equipment. + if( data == null || !metaFileInfo.EquipSlot.IsEquipment() ) + { + return; + } + + var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data ); + var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); + var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); + if( def.Entry != manip.Entry ) + { + MetaManipulations.Add( manip ); + } + } + + // Deserialize and check Eqdp Entries and add them to the list if they are non-default. + private void DeserializeEqdpEntries( MetaFileInfo metaFileInfo, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 5; + using var reader = new BinaryReader( new MemoryStream( data ) ); + for( var i = 0; i < num; ++i ) + { + // Use the SE gender/race code. + var gr = ( GenderRace )reader.ReadUInt32(); + var byteValue = reader.ReadByte(); + if( !gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory() ) + { + continue; + } + + var value = Eqdp.FromSlotAndBits( metaFileInfo.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); + var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId ), + metaFileInfo.EquipSlot, + gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); + var manip = new EqdpManipulation( value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); + if( def.Entry != manip.Entry ) + { + MetaManipulations.Add( manip ); + } + } + } + + // Deserialize and check Gmp Entries and add them to the list if they are non-default. + private void DeserializeGmpEntry( MetaFileInfo metaFileInfo, byte[]? data ) + { + if( data == null ) + { + return; + } + + using var reader = new BinaryReader( new MemoryStream( data ) ); + var value = ( GmpEntry )reader.ReadUInt32(); + value.UnknownTotal = reader.ReadByte(); + var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId ); + if( value != def ) + { + MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) ); + } + } + + // Deserialize and check Est Entries and add them to the list if they are non-default. + private void DeserializeEstEntries( MetaFileInfo metaFileInfo, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 6; + using var reader = new BinaryReader( new MemoryStream( data ) ); + for( var i = 0; i < num; ++i ) + { + var gr = ( GenderRace )reader.ReadUInt16(); + var id = reader.ReadUInt16(); + var value = reader.ReadUInt16(); + var type = ( metaFileInfo.SecondaryType, metaFileInfo.EquipSlot ) switch + { + (BodySlot.Face, _) => EstManipulation.EstType.Face, + (BodySlot.Hair, _) => EstManipulation.EstType.Hair, + (_, EquipSlot.Head) => EstManipulation.EstType.Head, + (_, EquipSlot.Body) => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; + if( !gr.IsValid() || type == 0 ) + { + continue; + } + + var def = EstFile.GetDefault( type, gr, id ); + if( def != value ) + { + MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) ); + } + } + } + + // Deserialize and check IMC Entries and add them to the list if they are non-default. + // This requires requesting a file from Lumina, which may fail due to TexTools corruption or just not existing. + // TexTools creates IMC files for off-hand weapon models which may not exist in the game files. + private void DeserializeImcEntries( MetaFileInfo metaFileInfo, byte[]? data ) + { + if( data == null ) + { + return; + } + + var num = data.Length / 6; + using var reader = new BinaryReader( new MemoryStream( data ) ); + var values = reader.ReadStructures< ImcEntry >( num ); + ushort i = 0; + try + { + if( metaFileInfo.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) + { + var def = new ImcFile( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, new ImcEntry() ).GamePath() ); + var partIdx = ImcFile.PartIndex( metaFileInfo.EquipSlot ); + foreach( var value in values ) + { + if( !value.Equals( def.GetEntry( partIdx, i ) ) ) + { + MetaManipulations.Add( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, value ) ); + } + + ++i; + } + } + else + { + var def = new ImcFile( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, + metaFileInfo.SecondaryId, i, + new ImcEntry() ).GamePath() ); + foreach( var value in values ) + { + if( !value.Equals( def.GetEntry( 0, i ) ) ) + { + MetaManipulations.Add( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, + metaFileInfo.PrimaryId, + metaFileInfo.SecondaryId, i, + value ) ); + } + + ++i; + } + } + } + catch( Exception e ) + { + PluginLog.Warning( + $"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" + + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs new file mode 100644 index 00000000..97ce6915 --- /dev/null +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using Dalamud.Logging; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Import; + +public partial class TexToolsMeta +{ + // Parse a single rgsp file. + public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) + { + if( data.Length != 45 && data.Length != 42 ) + { + PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); + return Invalid; + } + + using var s = new MemoryStream( data ); + using var br = new BinaryReader( s ); + // The first value is a flag that signifies version. + // If it is byte.max, the following two bytes are the version, + // otherwise it is version 1 and signifies the sub race instead. + var flag = br.ReadByte(); + var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); + + var ret = new TexToolsMeta( filePath, version ); + + // SubRace is offset by one due to Unknown. + var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); + if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); + return Invalid; + } + + // Next byte is Gender. 1 is Female, 0 is Male. + var gender = br.ReadByte(); + if( gender != 1 && gender != 0 ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); + return Invalid; + } + + // Add the given values to the manipulations if they are not default. + void Add( RspAttribute attribute, float value ) + { + var def = CmpFile.GetDefault( subRace, attribute ); + if( value != def ) + { + ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) ); + } + } + + if( gender == 1 ) + { + Add( RspAttribute.FemaleMinSize, br.ReadSingle() ); + Add( RspAttribute.FemaleMaxSize, br.ReadSingle() ); + Add( RspAttribute.FemaleMinTail, br.ReadSingle() ); + Add( RspAttribute.FemaleMaxTail, br.ReadSingle() ); + + Add( RspAttribute.BustMinX, br.ReadSingle() ); + Add( RspAttribute.BustMinY, br.ReadSingle() ); + Add( RspAttribute.BustMinZ, br.ReadSingle() ); + Add( RspAttribute.BustMaxX, br.ReadSingle() ); + Add( RspAttribute.BustMaxY, br.ReadSingle() ); + Add( RspAttribute.BustMaxZ, br.ReadSingle() ); + } + else + { + Add( RspAttribute.MaleMinSize, br.ReadSingle() ); + Add( RspAttribute.MaleMaxSize, br.ReadSingle() ); + Add( RspAttribute.MaleMinTail, br.ReadSingle() ); + Add( RspAttribute.MaleMaxTail, br.ReadSingle() ); + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs new file mode 100644 index 00000000..1a0f05fd --- /dev/null +++ b/Penumbra/Import/TexToolsMeta.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Import; + +// TexTools provices custom generated *.meta files for its modpacks, that contain changes to +// - imc files +// - eqp files +// - gmp files +// - est files +// - eqdp files +// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. +// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. +// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. +// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. +public partial class TexToolsMeta +{ + // An empty TexToolsMeta. + public static readonly TexToolsMeta Invalid = new( string.Empty, 0 ); + + // The info class determines the files or table locations the changes need to apply to from the filename. + + public readonly uint Version; + public readonly string FilePath; + public readonly List< MetaManipulation > MetaManipulations = new(); + + public TexToolsMeta( byte[] data ) + { + try + { + using var reader = new BinaryReader( new MemoryStream( data ) ); + Version = reader.ReadUInt32(); + FilePath = ReadNullTerminated( reader ); + var metaInfo = new MetaFileInfo( FilePath ); + var numHeaders = reader.ReadUInt32(); + var headerSize = reader.ReadUInt32(); + var headerStart = reader.ReadUInt32(); + reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); + + List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); + for( var i = 0; i < numHeaders; ++i ) + { + var currentOffset = reader.BaseStream.Position; + var type = ( MetaManipulation.Type )reader.ReadUInt32(); + var offset = reader.ReadUInt32(); + var size = reader.ReadInt32(); + entries.Add( ( type, offset, size ) ); + reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); + } + + byte[]? ReadEntry( MetaManipulation.Type type ) + { + var idx = entries.FindIndex( t => t.type == type ); + if( idx < 0 ) + { + return null; + } + + reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); + return reader.ReadBytes( entries[ idx ].size ); + } + + DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); + DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); + DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); + DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); + DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); + } + catch( Exception e ) + { + FilePath = ""; + PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); + } + } + + private TexToolsMeta( string filePath, uint version ) + { + FilePath = filePath; + Version = version; + } + + // Read a null terminated string from a binary reader. + private static string ReadNullTerminated( BinaryReader reader ) + { + var builder = new System.Text.StringBuilder(); + for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) + { + builder.Append( c ); + } + + return builder.ToString(); + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/TexToolsStructs.cs new file mode 100644 index 00000000..bb2ba8d9 --- /dev/null +++ b/Penumbra/Import/TexToolsStructs.cs @@ -0,0 +1,74 @@ +using System; +using Penumbra.Mods; + +namespace Penumbra.Import; + +internal static class DefaultTexToolsData +{ + public const string Name = "New Mod"; + public const string Author = "Unknown"; + public const string Description = "Mod imported from TexTools mod pack."; + public const string DefaultOption = "Default"; +} + +[Serializable] +internal class SimpleMod +{ + public string Name = string.Empty; + public string Category = string.Empty; + public string FullPath = string.Empty; + public string DatFile = string.Empty; + public long ModOffset = 0; + public long ModSize = 0; + public object? ModPackEntry = null; +} + +[Serializable] +internal class ModPackPage +{ + public int PageIndex = 0; + public ModGroup[] ModGroups = Array.Empty< ModGroup >(); +} + +[Serializable] +internal class ModGroup +{ + public string GroupName = string.Empty; + public SelectType SelectionType = SelectType.Single; + public OptionList[] OptionList = Array.Empty< OptionList >(); +} + +[Serializable] +internal class OptionList +{ + public string Name = string.Empty; + public string Description = string.Empty; + public string ImagePath = string.Empty; + public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >(); + public string GroupName = string.Empty; + public SelectType SelectionType = SelectType.Single; + public bool IsChecked = false; +} + +[Serializable] +internal class ExtendedModPack +{ + public string PackVersion = string.Empty; + public string Name = DefaultTexToolsData.Name; + public string Author = DefaultTexToolsData.Author; + public string Version = string.Empty; + public string Description = DefaultTexToolsData.Description; + public ModPackPage[] ModPackPages = Array.Empty< ModPackPage >(); + public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); +} + +[Serializable] +internal class SimpleModPack +{ + public string TtmpVersion = string.Empty; + public string Name = DefaultTexToolsData.Name; + public string Author = DefaultTexToolsData.Author; + public string Version = string.Empty; + public string Description = DefaultTexToolsData.Description; + public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); +} \ No newline at end of file diff --git a/Penumbra/Importer/ImporterState.cs b/Penumbra/Importer/ImporterState.cs deleted file mode 100644 index 608976fc..00000000 --- a/Penumbra/Importer/ImporterState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Penumbra.Importer -{ - public enum ImporterState - { - None, - WritingPackToDisk, - ExtractingModFiles, - Done, - } -} \ No newline at end of file diff --git a/Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs b/Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs deleted file mode 100644 index 10be9f15..00000000 --- a/Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.IO; -using Penumbra.Util; - -namespace Penumbra.Importer -{ - public class MagicTempFileStreamManagerAndDeleter : PenumbraSqPackStream, IDisposable - { - private readonly FileStream _fileStream; - - public MagicTempFileStreamManagerAndDeleter( FileStream stream ) - : base( stream ) - => _fileStream = stream; - - public new void Dispose() - { - var filePath = _fileStream.Name; - - base.Dispose(); - _fileStream.Dispose(); - - File.Delete( filePath ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Importer/Models/ExtendedModPack.cs b/Penumbra/Importer/Models/ExtendedModPack.cs deleted file mode 100644 index 45593faa..00000000 --- a/Penumbra/Importer/Models/ExtendedModPack.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using Penumbra.Mods; - -namespace Penumbra.Importer.Models -{ - internal class OptionList - { - public string? Name { get; set; } - public string? Description { get; set; } - public string? ImagePath { get; set; } - public List< SimpleMod >? ModsJsons { get; set; } - public string? GroupName { get; set; } - public SelectType SelectionType { get; set; } - public bool IsChecked { get; set; } - } - - internal class ModGroup - { - public string? GroupName { get; set; } - public SelectType SelectionType { get; set; } - public List< OptionList >? OptionList { get; set; } - } - - internal class ModPackPage - { - public int PageIndex { get; set; } - public List< ModGroup >? ModGroups { get; set; } - } - - internal class ExtendedModPack - { - public string? TTMPVersion { get; set; } - public string? Name { get; set; } - public string? Author { get; set; } - public string? Version { get; set; } - public string? Description { get; set; } - public List< ModPackPage >? ModPackPages { get; set; } - public List< SimpleMod >? SimpleModsList { get; set; } - } -} \ No newline at end of file diff --git a/Penumbra/Importer/Models/SimpleModPack.cs b/Penumbra/Importer/Models/SimpleModPack.cs deleted file mode 100644 index 1b3f9e4e..00000000 --- a/Penumbra/Importer/Models/SimpleModPack.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace Penumbra.Importer.Models -{ - internal class SimpleModPack - { - public string? TTMPVersion { get; set; } - public string? Name { get; set; } - public string? Author { get; set; } - public string? Version { get; set; } - public string? Description { get; set; } - public List< SimpleMod >? SimpleModsList { get; set; } - } - - internal class SimpleMod - { - public string? Name { get; set; } - public string? Category { get; set; } - public string? FullPath { get; set; } - public long ModOffset { get; set; } - public long ModSize { get; set; } - public string? DatFile { get; set; } - public object? ModPackEntry { get; set; } - } -} \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs deleted file mode 100644 index 0caa5a05..00000000 --- a/Penumbra/Importer/TexToolsImport.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Dalamud.Logging; -using ICSharpCode.SharpZipLib.Zip; -using Newtonsoft.Json; -using Penumbra.Importer.Models; -using Penumbra.Mods; -using Penumbra.Util; -using FileMode = System.IO.FileMode; - -namespace Penumbra.Importer; - -internal class TexToolsImport -{ - private readonly DirectoryInfo _outDirectory; - - private const string TempFileName = "textools-import"; - private readonly string _resolvedTempFilePath; - - public DirectoryInfo? ExtractedDirectory { get; private set; } - - public ImporterState State { get; private set; } - - public long TotalProgress { get; private set; } - public long CurrentProgress { get; private set; } - - public float Progress - { - get - { - if( CurrentProgress != 0 ) - { - // ReSharper disable twice RedundantCast - return ( float )CurrentProgress / ( float )TotalProgress; - } - - return 0; - } - } - - public string? CurrentModPack { get; private set; } - - public TexToolsImport( DirectoryInfo outDirectory ) - { - _outDirectory = outDirectory; - _resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName ); - } - - private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new(Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() )); - - public DirectoryInfo ImportModPack( FileInfo modPackFile ) - { - CurrentModPack = modPackFile.Name; - - var dir = VerifyVersionAndImport( modPackFile ); - - State = ImporterState.Done; - return dir; - } - - private void WriteZipEntryToTempFile( Stream s ) - { - var fs = new FileStream( _resolvedTempFilePath, FileMode.Create ); - s.CopyTo( fs ); - fs.Close(); - } - - // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry - private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) - { - for( var i = 0; i < file.Count; i++ ) - { - var entry = file[ i ]; - - if( entry.Name.Contains( fileName ) ) - { - return entry; - } - } - - return null; - } - - private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName ) - { - State = ImporterState.WritingPackToDisk; - - // write shitty zip garbage to disk - var entry = FindZipEntry( file, entryName ); - if( entry == null ) - { - throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); - } - - using var s = file.GetInputStream( entry ); - - WriteZipEntryToTempFile( s ); - - var fs = new FileStream( _resolvedTempFilePath, FileMode.Open ); - return new MagicTempFileStreamManagerAndDeleter( fs ); - } - - private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) - { - using var zfs = modPackFile.OpenRead(); - using var extractedModPack = new ZipFile( zfs ); - - var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); - if( mpl == null ) - { - throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); - } - - var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); - - // At least a better validation than going by the extension. - if( modRaw.Contains( "\"TTMPVersion\":" ) ) - { - if( modPackFile.Extension != ".ttmp2" ) - { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); - } - - return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); - } - - if( modPackFile.Extension != ".ttmp" ) - { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); - } - - return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); - } - - private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) - { - PluginLog.Log( " -> Importing V1 ModPack" ); - - var modListRaw = modRaw.Split( - new[] { "\r\n", "\r", "\n" }, - StringSplitOptions.None - ); - - var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > ); - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); - // Create a new ModMeta from the TTMP modlist info - Mod2.CreateMeta( ExtractedDirectory, string.IsNullOrEmpty( modPackFile.Name ) ? "New Mod" : modPackFile.Name, "Unknown", - "Mod imported from TexTools mod pack.", null, null ); - - ExtractSimpleModList( ExtractedDirectory, modList, modData ); - - return ExtractedDirectory; - } - - private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) - { - var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw ); - - if( modList.TTMPVersion?.EndsWith( "s" ) ?? false ) - { - return ImportSimpleV2ModPack( extractedModPack, modList ); - } - - if( modList.TTMPVersion?.EndsWith( "w" ) ?? false ) - { - return ImportExtendedV2ModPack( extractedModPack, modRaw ); - } - - try - { - PluginLog.Warning( $"Unknown TTMPVersion {modList.TTMPVersion ?? "NULL"} given, trying to export as simple Modpack." ); - return ImportSimpleV2ModPack( extractedModPack, modList ); - } - catch( Exception e1 ) - { - PluginLog.Warning( $"Exporting as simple Modpack failed with following error, retrying as extended Modpack:\n{e1}" ); - try - { - return ImportExtendedV2ModPack( extractedModPack, modRaw ); - } - catch( Exception e2 ) - { - throw new IOException( "Exporting as extended Modpack failed, too. Version unsupported or file defect.", e2 ); - } - } - } - - public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) - { - var name = Path.GetFileName( modListName ); - if( !name.Any() ) - { - name = "_"; - } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase; - var i = 2; - while( newModFolder.Exists && i < 12 ) - { - newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" ); - } - - if( newModFolder.Exists ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - newModFolder.Create(); - return newModFolder; - } - - private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) - { - PluginLog.Log( " -> Importing Simple V2 ModPack" ); - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description, null, null ); - - ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); - return ExtractedDirectory; - } - - private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) - { - PluginLog.Log( " -> Importing Extended V2 ModPack" ); - - var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw ); - - // Open the mod data file from the modpack as a SqPackStream - using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - - ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", - string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, null ); - - if( modList.SimpleModsList != null ) - { - ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData ); - } - - if( modList.ModPackPages == null ) - { - return ExtractedDirectory; - } - - // Iterate through all pages - var options = new List< ISubMod >(); - var groupPriority = 0; - foreach( var page in modList.ModPackPages ) - { - if( page.ModGroups == null ) - { - continue; - } - - foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) - { - options.Clear(); - var description = new StringBuilder(); - var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! ); - if( groupFolder.Exists ) - { - groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" ); - group.GroupName += $" ({page.PageIndex})"; - } - - foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) ) - { - var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); - ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); - options.Add( Mod2.CreateSubMod( ExtractedDirectory, optionFolder, option ) ); - description.Append( option.Description ); - if( !string.IsNullOrEmpty( option.Description ) ) - { - description.Append( '\n' ); - } - } - - Mod2.CreateOptionGroup( ExtractedDirectory, group, groupPriority++, description.ToString(), options ); - } - } - Mod2.CreateDefaultFiles( ExtractedDirectory ); - return ExtractedDirectory; - } - - private void ImportMetaModPack( FileInfo file ) - { - throw new NotImplementedException(); - } - - private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream ) - { - State = ImporterState.ExtractingModFiles; - - // haha allocation go brr - var wtf = mods.ToList(); - - TotalProgress += wtf.LongCount(); - - // Extract each SimpleMod into the new mod folder - foreach( var simpleMod in wtf.Where( m => m != null ) ) - { - ExtractMod( outDirectory, simpleMod, dataStream ); - CurrentProgress++; - } - } - - private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream ) - { - PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) ); - - try - { - var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); - - var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) ); - extractedFile.Directory?.Create(); - - if( extractedFile.FullName.EndsWith( "mdl" ) ) - { - ProcessMdl( data.Data ); - } - - File.WriteAllBytes( extractedFile.FullName, data.Data ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Could not extract mod." ); - } - } - - private void ProcessMdl( byte[] mdl ) - { - // Model file header LOD num - mdl[ 64 ] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32( mdl, 4 ); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - var modelHeaderLodOffset = 22; - mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; - } - - private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) - => file.GetInputStream( entry ); - - private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) - { - using var ms = new MemoryStream(); - using var s = GetStreamFromZipEntry( file, entry ); - s.CopyTo( ms ); - return encoding.GetString( ms.ToArray() ); - } -} \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs deleted file mode 100644 index 1afd8f0f..00000000 --- a/Penumbra/Importer/TexToolsMeta.cs +++ /dev/null @@ -1,427 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; -using Penumbra.Util; -using ImcFile = Penumbra.Meta.Files.ImcFile; - -namespace Penumbra.Importer; - -// TexTools provices custom generated *.meta files for its modpacks, that contain changes to -// - imc files -// - eqp files -// - gmp files -// - est files -// - eqdp files -// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. -// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. -// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. -// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. -public class TexToolsMeta -{ - // The info class determines the files or table locations the changes need to apply to from the filename. - public class Info - { - private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex - private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex - private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex - private const string Pir = @"\k'PrimaryId'"; // language=regex - private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex - private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex - private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex - private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex - private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex - private const string Ext = @"\.meta"; - - // These are the valid regexes for .meta files that we are able to support at the moment. - private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled); - private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled); - - public readonly ObjectType PrimaryType; - public readonly BodySlot SecondaryType; - public readonly ushort PrimaryId; - public readonly ushort SecondaryId; - public readonly EquipSlot EquipSlot = EquipSlot.Unknown; - public readonly CustomizationType CustomizationType = CustomizationType.Unknown; - - private static bool ValidType( ObjectType type ) - { - return type switch - { - ObjectType.Accessory => true, - ObjectType.Character => true, - ObjectType.Equipment => true, - ObjectType.DemiHuman => true, - ObjectType.Housing => true, - ObjectType.Monster => true, - ObjectType.Weapon => true, - ObjectType.Icon => false, - ObjectType.Font => false, - ObjectType.Interface => false, - ObjectType.LoadingScreen => false, - ObjectType.Map => false, - ObjectType.Vfx => false, - ObjectType.Unknown => false, - ObjectType.World => false, - _ => false, - }; - } - - public Info( string fileName ) - : this( new GamePath( fileName ) ) - { } - - public Info( GamePath fileName ) - { - PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); - PrimaryId = 0; - SecondaryType = BodySlot.Unknown; - SecondaryId = 0; - if( !ValidType( PrimaryType ) ) - { - PrimaryType = ObjectType.Unknown; - return; - } - - if( PrimaryType == ObjectType.Housing ) - { - var housingMatch = HousingMeta.Match( fileName ); - if( housingMatch.Success ) - { - PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value ); - } - - return; - } - - var match = CharaMeta.Match( fileName ); - if( !match.Success ) - { - return; - } - - PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value ); - if( match.Groups[ "Slot" ].Success ) - { - switch( PrimaryType ) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) ) - { - EquipSlot = tmpSlot; - } - - break; - case ObjectType.Character: - if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) ) - { - CustomizationType = tmpCustom; - } - - break; - } - } - - if( match.Groups[ "SecondaryType" ].Success - && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) - { - SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); - } - } - } - - public readonly uint Version; - public readonly string FilePath; - public readonly List< EqpManipulation > EqpManipulations = new(); - public readonly List< GmpManipulation > GmpManipulations = new(); - public readonly List< EqdpManipulation > EqdpManipulations = new(); - public readonly List< EstManipulation > EstManipulations = new(); - public readonly List< RspManipulation > RspManipulations = new(); - public readonly List< ImcManipulation > ImcManipulations = new(); - - private void DeserializeEqpEntry( Info info, byte[]? data ) - { - if( data == null || !info.EquipSlot.IsEquipment() ) - { - return; - } - - var value = Eqp.FromSlotAndBytes( info.EquipSlot, data ); - var def = new EqpManipulation( ExpandedEqpFile.GetDefault( info.PrimaryId ), info.EquipSlot, info.PrimaryId ); - var manip = new EqpManipulation( value, info.EquipSlot, info.PrimaryId ); - if( def.Entry != manip.Entry ) - { - EqpManipulations.Add( manip ); - } - } - - private void DeserializeEqdpEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 5; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) - { - var gr = ( GenderRace )reader.ReadUInt32(); - var byteValue = reader.ReadByte(); - if( !gr.IsValid() || !info.EquipSlot.IsEquipment() && !info.EquipSlot.IsAccessory() ) - { - continue; - } - - var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); - var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, info.EquipSlot.IsAccessory(), info.PrimaryId ), info.EquipSlot, - gr.Split().Item1, gr.Split().Item2, info.PrimaryId ); - var manip = new EqdpManipulation( value, info.EquipSlot, gr.Split().Item1, gr.Split().Item2, info.PrimaryId ); - if( def.Entry != manip.Entry ) - { - EqdpManipulations.Add( manip ); - } - } - } - - private void DeserializeGmpEntry( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - using var reader = new BinaryReader( new MemoryStream( data ) ); - var value = ( GmpEntry )reader.ReadUInt32(); - value.UnknownTotal = reader.ReadByte(); - var def = ExpandedGmpFile.GetDefault( info.PrimaryId ); - if( value != def ) - { - GmpManipulations.Add( new GmpManipulation( value, info.PrimaryId ) ); - } - } - - private void DeserializeEstEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) - { - var gr = ( GenderRace )reader.ReadUInt16(); - var id = reader.ReadUInt16(); - var value = reader.ReadUInt16(); - var type = ( info.SecondaryType, info.EquipSlot ) switch - { - (BodySlot.Face, _) => EstManipulation.EstType.Face, - (BodySlot.Hair, _) => EstManipulation.EstType.Hair, - (_, EquipSlot.Head) => EstManipulation.EstType.Head, - (_, EquipSlot.Body) => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, - }; - if( !gr.IsValid() || type == 0 ) - { - continue; - } - - var def = EstFile.GetDefault( type, gr, id ); - if( def != value ) - { - EstManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) ); - } - } - } - - private void DeserializeImcEntries( Info info, byte[]? data ) - { - if( data == null ) - { - return; - } - - var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - var values = reader.ReadStructures< ImcEntry >( num ); - ushort i = 0; - try - { - if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) - { - var def = new ImcFile( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, new ImcEntry() ).GamePath() ); - var partIdx = ImcFile.PartIndex( info.EquipSlot ); - foreach( var value in values ) - { - if( !value.Equals( def.GetEntry( partIdx, i ) ) ) - { - ImcManipulations.Add( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, value ) ); - } - - ++i; - } - } - else - { - var def = new ImcFile( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, - new ImcEntry() ).GamePath() ); - foreach( var value in values ) - { - if( !value.Equals( def.GetEntry( 0, i ) ) ) - { - ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, - value ) ); - } - - ++i; - } - } - } - catch( Exception e ) - { - PluginLog.Warning( $"Could not compute IMC manipulation for {info.PrimaryType} {info.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" - + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); - } - } - - private static string ReadNullTerminated( BinaryReader reader ) - { - var builder = new System.Text.StringBuilder(); - for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) - { - builder.Append( c ); - } - - return builder.ToString(); - } - - public TexToolsMeta( byte[] data ) - { - try - { - using var reader = new BinaryReader( new MemoryStream( data ) ); - Version = reader.ReadUInt32(); - FilePath = ReadNullTerminated( reader ); - var metaInfo = new Info( FilePath ); - var numHeaders = reader.ReadUInt32(); - var headerSize = reader.ReadUInt32(); - var headerStart = reader.ReadUInt32(); - reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); - - List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); - for( var i = 0; i < numHeaders; ++i ) - { - var currentOffset = reader.BaseStream.Position; - var type = ( MetaManipulation.Type )reader.ReadUInt32(); - var offset = reader.ReadUInt32(); - var size = reader.ReadInt32(); - entries.Add( ( type, offset, size ) ); - reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); - } - - byte[]? ReadEntry( MetaManipulation.Type type ) - { - var idx = entries.FindIndex( t => t.type == type ); - if( idx < 0 ) - { - return null; - } - - reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); - return reader.ReadBytes( entries[ idx ].size ); - } - - DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); - DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); - DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); - DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); - DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); - } - catch( Exception e ) - { - FilePath = ""; - PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); - } - } - - private TexToolsMeta( string filePath, uint version ) - { - FilePath = filePath; - Version = version; - } - - public static TexToolsMeta Invalid = new(string.Empty, 0); - - public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) - { - if( data.Length != 45 && data.Length != 42 ) - { - PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); - return Invalid; - } - - using var s = new MemoryStream( data ); - using var br = new BinaryReader( s ); - var flag = br.ReadByte(); - var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); - - var ret = new TexToolsMeta( filePath, version ); - - var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); - if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) - { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); - return Invalid; - } - - var gender = br.ReadByte(); - if( gender != 1 && gender != 0 ) - { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); - return Invalid; - } - - void Add( RspAttribute attribute, float value ) - { - var def = CmpFile.GetDefault( subRace, attribute ); - if( value != def ) - { - ret!.RspManipulations.Add( new RspManipulation( subRace, attribute, value ) ); - } - } - - if( gender == 1 ) - { - Add( RspAttribute.FemaleMinSize, br.ReadSingle() ); - Add( RspAttribute.FemaleMaxSize, br.ReadSingle() ); - Add( RspAttribute.FemaleMinTail, br.ReadSingle() ); - Add( RspAttribute.FemaleMaxTail, br.ReadSingle() ); - - Add( RspAttribute.BustMinX, br.ReadSingle() ); - Add( RspAttribute.BustMinY, br.ReadSingle() ); - Add( RspAttribute.BustMinZ, br.ReadSingle() ); - Add( RspAttribute.BustMaxX, br.ReadSingle() ); - Add( RspAttribute.BustMaxY, br.ReadSingle() ); - Add( RspAttribute.BustMaxZ, br.ReadSingle() ); - } - else - { - Add( RspAttribute.MaleMinSize, br.ReadSingle() ); - Add( RspAttribute.MaleMaxSize, br.ReadSingle() ); - Add( RspAttribute.MaleMinTail, br.ReadSingle() ); - Add( RspAttribute.MaleMaxTail, br.ReadSingle() ); - } - - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index dd7eb89e..6aaccde5 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 23c83487..161307fa 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -22,7 +22,6 @@ public partial class MetaManager private readonly ModCollection _collection; private static int _imcManagerCount; - public MetaManagerImc( ModCollection collection ) { _collection = collection; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 421642a0..4f2439a9 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -12,91 +12,10 @@ public interface IMetaManipulation public int FileIndex(); } -public interface IMetaManipulation< T > : IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct +public interface IMetaManipulation< T > + : IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct { } -public struct ManipulationSet< T > where T : struct, IMetaManipulation< T > -{ - private List< T >? _data = null; - - public IReadOnlyList< T > Data - => ( IReadOnlyList< T >? )_data ?? Array.Empty< T >(); - - public int Count - => _data?.Count ?? 0; - - public ManipulationSet( int count = 0 ) - { - if( count > 0 ) - { - _data = new List< T >( count ); - } - } - - public bool TryAdd( T manip ) - { - if( _data == null ) - { - _data = new List< T > { manip }; - return true; - } - - var idx = _data.BinarySearch( manip ); - if( idx >= 0 ) - { - return false; - } - - _data.Insert( ~idx, manip ); - return true; - } - - public int Set( T manip ) - { - if( _data == null ) - { - _data = new List< T > { manip }; - return 0; - } - - var idx = _data.BinarySearch( manip ); - if( idx >= 0 ) - { - _data[ idx ] = manip; - return idx; - } - - idx = ~idx; - _data.Insert( idx, manip ); - return idx; - } - - public bool TryGet( T manip, out T value ) - { - var idx = _data?.BinarySearch( manip ) ?? -1; - if( idx < 0 ) - { - value = default; - return false; - } - - value = _data![ idx ]; - return true; - } - - public bool Remove( T manip ) - { - var idx = _data?.BinarySearch( manip ) ?? -1; - if( idx < 0 ) - { - return false; - } - - _data!.RemoveAt( idx ); - return true; - } -} - [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > { diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 1344bf27..deff2d29 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -103,7 +103,7 @@ public partial class Configuration private void ResettleSortOrder() { ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; - var file = Mod2.Manager.ModFileSystemFile; + var file = ModFileSystem.ModFileSystemFile; using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); using var writer = new StreamWriter( stream ); using var j = new JsonTextWriter( writer ); @@ -169,7 +169,7 @@ public partial class Configuration var data = JArray.Parse( text ); var maxPriority = 0; - var dict = new Dictionary< string, ModSettings2.SavedSettings >(); + var dict = new Dictionary< string, ModSettings.SavedSettings >(); foreach( var setting in data.Cast< JObject >() ) { var modName = ( string )setting[ "FolderName" ]!; @@ -178,7 +178,7 @@ public partial class Configuration var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); - dict[ modName ] = new ModSettings2.SavedSettings() + dict[ modName ] = new ModSettings.SavedSettings() { Enabled = enabled, Priority = priority, diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs new file mode 100644 index 00000000..ce095609 --- /dev/null +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Linq; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Manager + { + public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ); + + public event ModPathChangeDelegate? ModPathChanged; + + public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) + { + var mod = this[ idx ]; + // TODO + } + + public void DeleteMod( int idx ) + { + var mod = this[ idx ]; + if( Directory.Exists( mod.BasePath.FullName ) ) + { + try + { + Directory.Delete( mod.BasePath.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" ); + } + } + + _mods.RemoveAt( idx ); + foreach( var remainingMod in _mods.Skip( idx ) ) + { + --remainingMod.Index; + } + + ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + } + + public void AddMod( DirectoryInfo modFolder ) + { + if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) + { + return; + } + + var mod = LoadMod( modFolder ); + if( mod == null ) + { + return; + } + + mod.Index = _mods.Count; + _mods.Add( mod ); + ModPathChanged?.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod2.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs similarity index 91% rename from Penumbra/Mods/Manager/Mod2.Manager.Meta.cs rename to Penumbra/Mods/Manager/Mod.Manager.Meta.cs index 5fa8c6f8..96fb8fa0 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.Meta.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs @@ -2,11 +2,11 @@ using System; namespace Penumbra.Mods; -public sealed partial class Mod2 +public sealed partial class Mod { public partial class Manager { - public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod2 mod ); + public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod mod, string? oldName ); public event ModMetaChangeDelegate? ModMetaChanged; public void ChangeModName( Index idx, string newName ) @@ -14,9 +14,10 @@ public sealed partial class Mod2 var mod = this[ idx ]; if( mod.Name != newName ) { + var oldName = mod.Name; mod.Name = newName; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Name, mod ); + ModMetaChanged?.Invoke( MetaChangeType.Name, mod, oldName.Text ); } } @@ -27,7 +28,7 @@ public sealed partial class Mod2 { mod.Author = newAuthor; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Author, mod ); + ModMetaChanged?.Invoke( MetaChangeType.Author, mod, null ); } } @@ -38,7 +39,7 @@ public sealed partial class Mod2 { mod.Description = newDescription; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Description, mod ); + ModMetaChanged?.Invoke( MetaChangeType.Description, mod, null ); } } @@ -49,7 +50,7 @@ public sealed partial class Mod2 { mod.Version = newVersion; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Version, mod ); + ModMetaChanged?.Invoke( MetaChangeType.Version, mod, null ); } } @@ -60,7 +61,7 @@ public sealed partial class Mod2 { mod.Website = newWebsite; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Website, mod ); + ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null ); } } } diff --git a/Penumbra/Mods/Manager/Mod2.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs similarity index 53% rename from Penumbra/Mods/Manager/Mod2.Manager.Options.cs rename to Penumbra/Mods/Manager/Mod.Manager.Options.cs index 248745ee..78f3d58c 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; +using OtterGui.Filesystem; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Util; @@ -13,25 +14,43 @@ public enum ModOptionChangeType GroupRenamed, GroupAdded, GroupDeleted, + GroupMoved, + GroupTypeChanged, PriorityChanged, OptionAdded, OptionDeleted, - OptionChanged, + OptionMoved, + OptionFilesChanged, + OptionSwapsChanged, + OptionMetaChanged, + OptionUpdated, DisplayChange, } -public sealed partial class Mod2 +public sealed partial class Mod { public sealed partial class Manager { - public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ); + public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ); public event ModOptionChangeDelegate ModOptionChanged; - public void RenameModGroup( Mod2 mod, int groupIdx, string newName ) + public void ChangeModGroupType( Mod mod, int groupIdx, SelectType type ) + { + var group = mod._groups[ groupIdx ]; + if( group.Type == type ) + { + return; + } + + mod._groups[ groupIdx ] = group.Convert( type ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1 ); + } + + public void RenameModGroup( Mod mod, int groupIdx, string newName ) { var group = mod._groups[ groupIdx ]; var oldName = group.Name; - if( oldName == newName || !VerifyFileName( mod, group, newName ) ) + if( oldName == newName || !VerifyFileName( mod, group, newName, true ) ) { return; } @@ -43,33 +62,41 @@ public sealed partial class Mod2 _ => newName, }; - ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, 0 ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1 ); } - public void AddModGroup( Mod2 mod, SelectType type, string newName ) + public void AddModGroup( Mod mod, SelectType type, string newName ) { - if( !VerifyFileName( mod, null, newName ) ) + if( !VerifyFileName( mod, null, newName, true ) ) { return; } - var maxPriority = mod._groups.Max( o => o.Priority ) + 1; + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max( o => o.Priority ) + 1; mod._groups.Add( type == SelectType.Multi ? new MultiModGroup { Name = newName, Priority = maxPriority } : new SingleModGroup { Name = newName, Priority = maxPriority } ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, 0 ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1 ); } - public void DeleteModGroup( Mod2 mod, int groupIdx ) + public void DeleteModGroup( Mod mod, int groupIdx ) { var group = mod._groups[ groupIdx ]; mod._groups.RemoveAt( groupIdx ); - group.DeleteFile( BasePath ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 ); + group.DeleteFile( mod.BasePath ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); } - public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription ) + public void MoveModGroup( Mod mod, int groupIdxFrom, int groupIdxTo ) + { + if( mod._groups.Move( groupIdxFrom, groupIdxTo ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo ); + } + } + + public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription ) { var group = mod._groups[ groupIdx ]; if( group.Description == newDescription ) @@ -83,10 +110,10 @@ public sealed partial class Mod2 MultiModGroup m => m.Description = newDescription, _ => newDescription, }; - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, 0 ); + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1 ); } - public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority ) + public void ChangeGroupPriority( Mod mod, int groupIdx, int newPriority ) { var group = mod._groups[ groupIdx ]; if( group.Priority == newPriority ) @@ -100,14 +127,14 @@ public sealed partial class Mod2 MultiModGroup m => m.Priority = newPriority, _ => newPriority, }; - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1 ); + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1 ); } - public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority ) + public void ChangeOptionPriority( Mod mod, int groupIdx, int optionIdx, int newPriority ) { switch( mod._groups[ groupIdx ] ) { - case SingleModGroup s: + case SingleModGroup: ChangeGroupPriority( mod, groupIdx, newPriority ); break; case MultiModGroup m: @@ -117,12 +144,12 @@ public sealed partial class Mod2 } m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority ); - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1 ); return; } } - public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName ) + public void RenameOption( Mod mod, int groupIdx, int optionIdx, string newName ) { switch( mod._groups[ groupIdx ] ) { @@ -145,10 +172,10 @@ public sealed partial class Mod2 return; } - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); } - public void AddOption( Mod2 mod, int groupIdx, string newName ) + public void AddOption( Mod mod, int groupIdx, string newName ) { switch( mod._groups[ groupIdx ] ) { @@ -160,10 +187,30 @@ public sealed partial class Mod2 break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 ); } - public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx ) + public void AddOption( Mod mod, int groupIdx, ISubMod option, int priority = 0 ) + { + if( option is not SubMod o ) + { + return; + } + + switch( mod._groups[ groupIdx ] ) + { + case SingleModGroup s: + s.OptionData.Add( o ); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add( ( o, priority ) ); + break; + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 ); + } + + public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) { switch( mod._groups[ groupIdx ] ) { @@ -175,10 +222,19 @@ public sealed partial class Mod2 break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 ); } - public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) + public void MoveOption( Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo ) + { + var group = mod._groups[ groupIdx ]; + if( group.MoveOption( optionIdxFrom, optionIdxTo ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo ); + } + } + + public void OptionSetManipulation( Mod mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); if( delete ) @@ -206,41 +262,94 @@ public sealed partial class Mod2 } } - ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } - public void OptionSetFile( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( subMod.Manipulations.SetEquals( manipulations ) ) + { + return; + } + + subMod.ManipulationData.Clear(); + subMod.ManipulationData.UnionWith( manipulations ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); + } + + public void OptionSetFile( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); if( OptionSetFile( subMod.FileData, gamePath, newPath ) ) { - ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); } } - public void OptionSetFileSwap( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( subMod.FileData.Equals( replacements ) ) + { + return; + } + + subMod.FileData.SetTo( replacements ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + } + + public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) ) { - ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } } - private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName ) + public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( subMod.FileSwapData.Equals( swaps ) ) + { + return; + } + + subMod.FileSwapData.SetTo( swaps ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); + } + + public void OptionUpdate( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements, + HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + subMod.FileData.SetTo( replacements ); + subMod.ManipulationData.Clear(); + subMod.ManipulationData.UnionWith( manipulations ); + subMod.FileSwapData.SetTo( swaps ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 ); + } + + public static bool VerifyFileName( Mod mod, IModGroup? group, string newName, bool message ) { var path = newName.RemoveInvalidPathSymbols(); - if( mod.Groups.Any( o => !ReferenceEquals( o, group ) + if( path.Length == 0 + || mod.Groups.Any( o => !ReferenceEquals( o, group ) && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) { - PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); + if( message ) + { + PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); + } + return false; } return true; } - private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx ) + private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx ) { return mod._groups[ groupIdx ] switch { @@ -278,7 +387,7 @@ public sealed partial class Mod2 return true; } - private static void OnModOptionChange( ModOptionChangeType type, Mod2 mod, int groupIdx, int _ ) + private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) { // File deletion is handled in the actual function. if( type != ModOptionChangeType.GroupDeleted ) @@ -289,10 +398,11 @@ public sealed partial class Mod2 // State can not change on adding groups, as they have no immediate options. mod.HasOptions = type switch { - ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption, - ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - _ => mod.HasOptions, + ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption, + ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + _ => mod.HasOptions, }; } } diff --git a/Penumbra/Mods/Manager/Mod2.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs similarity index 96% rename from Penumbra/Mods/Manager/Mod2.Manager.Root.cs rename to Penumbra/Mods/Manager/Mod.Manager.Root.cs index 59f61e6c..6967f0bc 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -4,7 +4,7 @@ using Dalamud.Logging; namespace Penumbra.Mods; -public sealed partial class Mod2 +public sealed partial class Mod { public sealed partial class Manager { @@ -50,7 +50,7 @@ public sealed partial class Mod2 } BasePath = newDir; - Valid = true; + Valid = Directory.Exists( newDir.FullName ); if( Penumbra.Config.ModDirectory != BasePath.FullName ) { Penumbra.Config.ModDirectory = BasePath.FullName; @@ -74,7 +74,7 @@ public sealed partial class Mod2 { continue; } - + mod.Index = _mods.Count; _mods.Add( mod ); } diff --git a/Penumbra/Mods/Manager/Mod2.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs similarity index 64% rename from Penumbra/Mods/Manager/Mod2.Manager.cs rename to Penumbra/Mods/Manager/Mod.Manager.cs index c2250d3a..e467c733 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -4,22 +4,22 @@ using System.Collections.Generic; namespace Penumbra.Mods; -public sealed partial class Mod2 +public sealed partial class Mod { - public sealed partial class Manager : IEnumerable< Mod2 > + public sealed partial class Manager : IEnumerable< Mod > { - private readonly List< Mod2 > _mods = new(); + private readonly List< Mod > _mods = new(); - public Mod2 this[ Index idx ] + public Mod this[ Index idx ] => _mods[ idx ]; - public IReadOnlyList< Mod2 > Mods + public IReadOnlyList< Mod > Mods => _mods; public int Count => _mods.Count; - public IEnumerator< Mod2 > GetEnumerator() + public IEnumerator< Mod > GetEnumerator() => _mods.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs deleted file mode 100644 index a0c9b8ca..00000000 --- a/Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.IO; -using Dalamud.Logging; - -namespace Penumbra.Mods; - -public partial class Mod2 -{ - public partial class Manager - { - public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ); - - public event ModPathChangeDelegate? ModPathChanged; - - public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) - { - var mod = this[ idx ]; - // TODO - } - - public void DeleteMod( Index idx ) - { - var mod = this[ idx ]; - if( Directory.Exists( mod.BasePath.FullName ) ) - { - try - { - Directory.Delete( mod.BasePath.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" ); - } - } - - // TODO - // mod.Order.ParentFolder.RemoveMod( mod ); - // _mods.RemoveAt( idx ); - //for( var i = idx; i < _mods.Count; ++i ) - //{ - // --_mods[i].Index; - //} - - ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); - } - - public Mod2 AddMod( DirectoryInfo modFolder ) - { - // TODO - - //var mod = LoadMod( StructuredMods, modFolder ); - //if( mod == null ) - //{ - // return -1; - //} - // - //if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - //{ - // if( SetSortOrderPath( mod, sortOrder ) ) - // { - // Config.Save(); - // } - //} - // - //if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) - //{ - // return -1; - //} - // - //_mods.Add( mod ); - //ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); - // - return this[^1]; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs b/Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs deleted file mode 100644 index b8466c50..00000000 --- a/Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.IO; - -namespace Penumbra.Mods; - -public sealed partial class Mod2 -{ - public sealed partial class Manager - { - public static string ModFileSystemFile - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs similarity index 82% rename from Penumbra/Mods/Mod2.BasePath.cs rename to Penumbra/Mods/Mod.BasePath.cs index 765b6106..c925a4f0 100644 --- a/Penumbra/Mods/Mod2.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -10,15 +10,15 @@ public enum ModPathChangeType Moved, } -public partial class Mod2 +public partial class Mod { public DirectoryInfo BasePath { get; private set; } public int Index { get; private set; } = -1; - private Mod2( DirectoryInfo basePath ) + private Mod( DirectoryInfo basePath ) => BasePath = basePath; - public static Mod2? LoadMod( DirectoryInfo basePath ) + public static Mod? LoadMod( DirectoryInfo basePath ) { basePath.Refresh(); if( !basePath.Exists ) @@ -27,7 +27,7 @@ public partial class Mod2 return null; } - var mod = new Mod2( basePath ); + var mod = new Mod( basePath ); mod.LoadMeta(); if( mod.Name.Length == 0 ) { diff --git a/Penumbra/Mods/Mod2.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs similarity index 95% rename from Penumbra/Mods/Mod2.ChangedItems.cs rename to Penumbra/Mods/Mod.ChangedItems.cs index bcd7fc4b..9e68225c 100644 --- a/Penumbra/Mods/Mod2.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Penumbra.Mods; -public sealed partial class Mod2 +public sealed partial class Mod { public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs new file mode 100644 index 00000000..a8015c49 --- /dev/null +++ b/Penumbra/Mods/Mod.Creation.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Utility; +using OtterGui.Classes; +using OtterGui.Filesystem; +using Penumbra.GameData.ByteString; +using Penumbra.Import; + +namespace Penumbra.Mods; + +public partial class Mod +{ + // Create and return a new directory based on the given directory and name, that is + // - Not Empty + // - Unique, by appending (digit) for duplicates. + // - Containing no symbols invalid for FFXIV or windows paths. + internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) + { + var name = Path.GetFileNameWithoutExtension( modListName ); + if( name.Length == 0 ) + { + name = "_"; + } + + var newModFolderBase = NewOptionDirectory( outDirectory, name ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + if( newModFolder.Length == 0 ) + { + throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); + } + + Directory.CreateDirectory( newModFolder ); + return new DirectoryInfo( newModFolder ); + } + + // Create the name for a group or option subfolder based on its parent folder and given name. + // subFolderName should never be empty, and the result is unique and contains no invalid symbols. + internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + { + var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + } + + // Create the file containing the meta information about a mod from scratch. + internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, + string? website ) + { + var mod = new Mod( directory ); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! ); + mod.Author = author != null ? new LowerString( author ) : mod.Author; + mod.Description = description ?? mod.Description; + mod.Version = version ?? mod.Version; + mod.Website = website ?? mod.Website; + mod.SaveMeta(); + } + + // Create a file for an option group from given data. + internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, + int priority, string desc, IEnumerable< ISubMod > subMods ) + { + switch( groupData.SelectionType ) + { + case SelectType.Multi: + { + var group = new MultiModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + case SelectType.Single: + { + var group = new SingleModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + } + } + + // Create the data for a given sub mod from its data and the folder it is based on. + internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod() + { + Name = option.Name!, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + // Create the default data file from all unused files that were not handled before + // and are used in sub mods. + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod( directory ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + { + mod._default.FileData.TryAdd( gamePath, file ); + } + } + + mod._default.IncorporateMetaChanges( directory, true ); + mod.SaveDefaultMod(); + } + + // Return the name of a new valid directory based on the base directory and the given name. + private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); + + + // XIV can not deal with non-ascii symbols in a path, + // and the path must obviously be valid itself. + private static string ReplaceBadXivSymbols( string s, string replacement = "_" ) + { + StringBuilder sb = new(s.Length); + foreach( var c in s ) + { + if( c.IsInvalidAscii() || c.IsInvalidInPath() ) + { + sb.Append( replacement ); + } + else + { + sb.Append( c ); + } + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod.Files.cs similarity index 99% rename from Penumbra/Mods/Mod2.Files.cs rename to Penumbra/Mods/Mod.Files.cs index a9353bc1..4ff1bbd9 100644 --- a/Penumbra/Mods/Mod2.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -9,7 +9,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; -public partial class Mod2 +public partial class Mod { public ISubMod Default => _default; diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs similarity index 61% rename from Penumbra/Mods/Mod2.Meta.Migration.cs rename to Penumbra/Mods/Mod.Meta.Migration.cs index 5b13121f..6b2ae97b 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -6,19 +6,18 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Importer; using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class Mod2 +public sealed partial class Mod { private static class Migration { - public static bool Migrate( Mod2 mod, JObject json ) + public static bool Migrate( Mod mod, JObject json ) => MigrateV0ToV1( mod, json ); - private static bool MigrateV0ToV1( Mod2 mod, JObject json ) + private static bool MigrateV0ToV1( Mod mod, JObject json ) { if( mod.FileVersion > 0 ) { @@ -27,14 +26,15 @@ public sealed partial class Mod2 var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); var priority = 1; + var seenMetaFiles = new HashSet< FullPath >(); foreach( var group in groups.Values ) { - ConvertGroup( mod, group, ref priority ); + ConvertGroup( mod, group, ref priority, seenMetaFiles ); } - foreach( var unusedFile in mod.FindUnusedFiles() ) + foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) ) { if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) && !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) @@ -61,7 +61,7 @@ public sealed partial class Mod2 return true; } - private static void ConvertGroup( Mod2 mod, OptionGroupV0 group, ref int priority ) + private static void ConvertGroup( Mod mod, OptionGroupV0 group, ref int priority, HashSet< FullPath > seenMetaFiles ) { if( group.Options.Count == 0 ) { @@ -82,14 +82,14 @@ public sealed partial class Mod2 mod._groups.Add( newMultiGroup ); foreach( var option in group.Options ) { - newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option ), optionPriority++ ) ); + newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option, seenMetaFiles ), optionPriority++ ) ); } break; case SelectType.Single: if( group.Options.Count == 1 ) { - AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ] ); + AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ], seenMetaFiles ); return; } @@ -102,28 +102,34 @@ public sealed partial class Mod2 mod._groups.Add( newSingleGroup ); foreach( var option in group.Options ) { - newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option ) ); + newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option, seenMetaFiles ) ); } break; } } - private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option ) + private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles ) { foreach( var (relPath, gamePaths) in option.OptionFiles ) { + var fullPath = new FullPath( basePath, relPath ); foreach( var gamePath in gamePaths ) { - mod.FileData.TryAdd( gamePath, new FullPath( basePath, relPath ) ); + mod.FileData.TryAdd( gamePath, fullPath ); + } + + if( fullPath.Extension is ".meta" or ".rgsp" ) + { + seenMetaFiles.Add( fullPath ); } } } - private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option ) + private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles ) { - var subMod = new SubMod() { Name = option.OptionName }; - AddFilesToSubMod( subMod, basePath, option ); + var subMod = new SubMod { Name = option.OptionName }; + AddFilesToSubMod( subMod, basePath, option, seenMetaFiles ); subMod.IncorporateMetaChanges( basePath, false ); return subMod; } @@ -152,5 +158,45 @@ public sealed partial class Mod2 public OptionGroupV0() { } } + + // Not used anymore, but required for migration. + private class SingleOrArrayConverter< T > : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( HashSet< T > ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ); + + if( token.Type == JTokenType.Array ) + { + return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); + } + + var tmp = token.ToObject< T >(); + return tmp != null + ? new HashSet< T > { tmp } + : new HashSet< T >(); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + writer.WriteStartArray(); + if( value != null ) + { + var v = ( HashSet< T > )value; + foreach( var val in v ) + { + serializer.Serialize( writer, val?.ToString() ); + } + } + + writer.WriteEndArray(); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod.Meta.cs similarity index 98% rename from Penumbra/Mods/Mod2.Meta.cs rename to Penumbra/Mods/Mod.Meta.cs index bcc05cf8..33760dfe 100644 --- a/Penumbra/Mods/Mod2.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -4,6 +4,7 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using OtterGui.Classes; namespace Penumbra.Mods; @@ -20,7 +21,7 @@ public enum MetaChangeType : byte Migration = 0x40, } -public sealed partial class Mod2 +public sealed partial class Mod { public const uint CurrentFileVersion = 1; public uint FileVersion { get; private set; } = CurrentFileVersion; diff --git a/Penumbra/Mods/Mod2.Creation.cs b/Penumbra/Mods/Mod2.Creation.cs deleted file mode 100644 index 915ace3f..00000000 --- a/Penumbra/Mods/Mod2.Creation.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Importer.Models; - -namespace Penumbra.Mods; - -public partial class Mod2 -{ - internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, - string? website ) - { - var mod = new Mod2( directory ); - if( name is { Length: 0 } ) - { - mod.Name = name; - } - - if( author != null ) - { - mod.Author = author; - } - - if( description != null ) - { - mod.Description = description; - } - - if( version != null ) - { - mod.Version = version; - } - - if( website != null ) - { - mod.Website = website; - } - - mod.SaveMeta(); - } - - internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, - int priority, string desc, List< ISubMod > subMods ) - { - switch( groupData.SelectionType ) - { - case SelectType.Multi: - { - var group = new MultiModGroup() - { - Name = groupData.GroupName!, - Description = desc, - Priority = priority, - }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.SaveModGroup( group, baseFolder ); - break; - } - case SelectType.Single: - { - var group = new SingleModGroup() - { - Name = groupData.GroupName!, - Description = desc, - Priority = priority, - }; - group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.SaveModGroup( group, baseFolder ); - break; - } - } - } - - internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) - { - var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) - .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) - .Where( t => t.Item1 ); - - var mod = new SubMod() - { - Name = option.Name!, - }; - foreach( var (_, gamePath, file) in list ) - { - mod.FileData.TryAdd( gamePath, file ); - } - - mod.IncorporateMetaChanges( baseFolder, true ); - return mod; - } - - internal static void CreateDefaultFiles( DirectoryInfo directory ) - { - var mod = new Mod2( directory ); - foreach( var file in mod.FindUnusedFiles() ) - { - if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - { - mod._default.FileData.TryAdd( gamePath, file ); - } - } - - mod._default.IncorporateMetaChanges( directory, true ); - mod.SaveDefaultMod(); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index 80e7632b..9497c9c6 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Security.Cryptography; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.Importer; +using Penumbra.Import; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 3a65011b..d310250b 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,16 +1,21 @@ using System; using System.IO; +using System.Linq; +using System.Text.RegularExpressions; using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable +public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { + public static string ModFileSystemFile + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); + // Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. public void Save() - => SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true ); + => SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystem Load() @@ -20,18 +25,24 @@ public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable ret.Changed += ret.OnChange; Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; + Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange; + Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; return ret; } public void Dispose() - => Penumbra.ModManager.ModDiscoveryFinished -= Reload; + { + Penumbra.ModManager.ModPathChanged -= OnModPathChange; + Penumbra.ModManager.ModDiscoveryFinished -= Reload; + Penumbra.ModManager.ModMetaChanged -= OnMetaChange; + } // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() { - if( Load( new FileInfo( Mod2.Manager.ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) { Save(); } @@ -46,17 +57,61 @@ public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable } } + // Update sort order when defaulted mod names change. + private void OnMetaChange( MetaChangeType type, Mod mod, string? oldName ) + { + if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) + { + var old = oldName.FixName(); + if( Find( old, out var child ) ) + { + Rename( child, mod.Name.Text ); + } + } + } + + // Update the filesystem if a mod has been added or removed. + // Save it, if the mod directory has been moved, since this will change the save format. + private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath ) + { + switch( type ) + { + case ModPathChangeType.Added: + var originalName = mod.Name.Text.FixName(); + var name = originalName; + var counter = 1; + while( Find( name, out _ ) ) + { + name = $"{originalName} ({++counter})"; + } + + CreateLeaf( Root, name, mod ); + break; + case ModPathChangeType.Deleted: + var leaf = Root.GetAllDescendants( SortMode.Lexicographical ).OfType< Leaf >().FirstOrDefault( l => l.Value == mod ); + if( leaf != null ) + { + Delete( leaf ); + } + break; + case ModPathChangeType.Moved: + Save(); + break; + } + } + // Used for saving and loading. - private static string ModToIdentifier( Mod2 mod ) + private static string ModToIdentifier( Mod mod ) => mod.BasePath.Name; - private static string ModToName( Mod2 mod ) - => mod.Name.Text; + private static string ModToName( Mod mod ) + => mod.Name.Text.FixName(); - private static (string, bool) SaveMod( Mod2 mod, string fullPath ) + private static (string, bool) SaveMod( Mod mod, string fullPath ) { + var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?" ); // Only save pairs with non-default paths. - if( fullPath == ModToName( mod ) ) + if( regex.IsMatch( fullPath ) ) { return ( string.Empty, false ); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index ac643ecb..924e294e 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Dalamud.Logging; using Newtonsoft.Json; +using OtterGui.Filesystem; using Penumbra.Util; namespace Penumbra.Mods; @@ -76,4 +77,7 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteEndArray(); j.WriteEndObject(); } + + public IModGroup Convert( SelectType type ); + public bool MoveOption( int optionIdxFrom, int optionIdxTo ); } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs similarity index 70% rename from Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs rename to Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index cd05a844..88350364 100644 --- a/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -5,10 +5,11 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; namespace Penumbra.Mods; -public partial class Mod2 +public partial class Mod { private sealed class MultiModGroup : IModGroup { @@ -63,5 +64,26 @@ public partial class Mod2 return ret; } + + public IModGroup Convert( SelectType type ) + { + switch( type ) + { + case SelectType.Multi: return this; + case SelectType.Single: + var multi = new SingleModGroup() + { + Name = Name, + Description = Description, + Priority = Priority, + }; + multi.OptionData.AddRange( PrioritizedOptions.Select( p => p.Mod ) ); + return multi; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + => PrioritizedOptions.Move( optionIdxFrom, optionIdxTo ); } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs similarity index 68% rename from Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs rename to Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index 40ebbcce..8c0b4103 100644 --- a/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -2,12 +2,14 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; namespace Penumbra.Mods; -public partial class Mod2 +public partial class Mod { private sealed class SingleModGroup : IModGroup { @@ -62,5 +64,26 @@ public partial class Mod2 return ret; } + + public IModGroup Convert( SelectType type ) + { + switch( type ) + { + case SelectType.Single: return this; + case SelectType.Multi: + var multi = new MultiModGroup() + { + Name = Name, + Description = Description, + Priority = Priority, + }; + multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); + return multi; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + => OptionData.Move( optionIdxFrom, optionIdxTo ); } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs similarity index 81% rename from Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs rename to Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index ccbf2df8..e9be472d 100644 --- a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -6,12 +6,12 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Importer; +using Penumbra.Import; using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; -public partial class Mod2 +public partial class Mod { private string DefaultFile => Path.Combine( BasePath.FullName, "default_mod.json" ); @@ -135,31 +135,7 @@ public partial class Mod2 { File.Delete( file.FullName ); } - - foreach( var manip in meta.EqpManipulations ) - { - ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EqdpManipulations ) - { - ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EstManipulations ) - { - ManipulationData.Add( manip ); - } - - foreach( var manip in meta.GmpManipulations ) - { - ManipulationData.Add( manip ); - } - - foreach( var manip in meta.ImcManipulations ) - { - ManipulationData.Add( manip ); - } + ManipulationData.UnionWith( meta.MetaManipulations ); break; case ".rgsp": @@ -174,11 +150,7 @@ public partial class Mod2 { File.Delete( file.FullName ); } - - foreach( var manip in rgsp.RspManipulations ) - { - ManipulationData.Add( manip ); - } + ManipulationData.UnionWith( rgsp.MetaManipulations ); break; default: continue; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index d40f578b..982776f8 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -1,19 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; +using OtterGui.Filesystem; namespace Penumbra.Mods; - // Contains the settings for a given mod. -public class ModSettings2 +public class ModSettings { - public static readonly ModSettings2 Empty = new(); + public static readonly ModSettings Empty = new(); public List< uint > Settings { get; init; } = new(); public int Priority { get; set; } public bool Enabled { get; set; } - public ModSettings2 DeepCopy() + public ModSettings DeepCopy() => new() { Enabled = Enabled, @@ -21,7 +22,7 @@ public class ModSettings2 Settings = Settings.ToList(), }; - public static ModSettings2 DefaultSettings( Mod2 mod ) + public static ModSettings DefaultSettings( Mod mod ) => new() { Enabled = false, @@ -29,19 +30,31 @@ public class ModSettings2 Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(), }; - - - public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { switch( type ) { + case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: Settings.Insert( groupIdx, 0 ); - break; + return true; case ModOptionChangeType.GroupDeleted: Settings.RemoveAt( groupIdx ); - break; + return true; + case ModOptionChangeType.GroupTypeChanged: + { + var group = mod.Groups[ groupIdx ]; + var config = Settings[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => ( uint )Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), + SelectType.Multi => 1u << ( int )config, + _ => config, + }; + return config != Settings[ groupIdx ]; + } case ModOptionChangeType.OptionDeleted: + { var group = mod.Groups[ groupIdx ]; var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch @@ -50,20 +63,38 @@ public class ModSettings2 SelectType.Multi => RemoveBit( config, optionIdx ), _ => config, }; - break; + return config != Settings[ groupIdx ]; + } + case ModOptionChangeType.GroupMoved: return Settings.Move( groupIdx, movedToIdx ); + case ModOptionChangeType.OptionMoved: + { + var group = mod.Groups[ groupIdx ]; + var config = Settings[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => config == optionIdx ? ( uint )movedToIdx : config, + SelectType.Multi => MoveBit( config, optionIdx, movedToIdx ), + _ => config, + }; + return config != Settings[ groupIdx ]; + } + default: return false; } } - public void SetValue( Mod2 mod, int groupIdx, uint newValue ) + private static uint FixSetting( IModGroup group, uint value ) + => group.Type switch + { + SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), + SelectType.Multi => ( uint )( value & ( ( 1 << group.Count ) - 1 ) ), + _ => value, + }; + + public void SetValue( Mod mod, int groupIdx, uint newValue ) { AddMissingSettings( groupIdx + 1 ); var group = mod.Groups[ groupIdx ]; - Settings[ groupIdx ] = group.Type switch - { - SelectType.Single => ( uint )Math.Max( newValue, group.Count ), - SelectType.Multi => ( ( 1u << group.Count ) - 1 ) & newValue, - _ => newValue, - }; + Settings[ groupIdx ] = FixSetting( group, newValue ); } private static uint RemoveBit( uint config, int bit ) @@ -75,6 +106,16 @@ public class ModSettings2 return low | high; } + private static uint MoveBit( uint config, int bit1, int bit2 ) + { + var enabled = ( config & ( 1 << bit1 ) ) != 0 ? 1u << bit2 : 0u; + config = RemoveBit( config, bit1 ); + var lowMask = ( 1u << bit2 ) - 1u; + var low = config & lowMask; + var high = ( config & ~lowMask ) << 1; + return low | enabled | high; + } + internal bool AddMissingSettings( int totalCount ) { if( totalCount <= Settings.Count ) @@ -100,7 +141,7 @@ public class ModSettings2 Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), }; - public SavedSettings( ModSettings2 settings, Mod2 mod ) + public SavedSettings( ModSettings settings, Mod mod ) { Priority = settings.Priority; Enabled = settings.Enabled; @@ -113,7 +154,7 @@ public class ModSettings2 } } - public bool ToSettings( Mod2 mod, out ModSettings2 settings ) + public bool ToSettings( Mod mod, out ModSettings settings ) { var list = new List< uint >( mod.Groups.Count ); var changes = Settings.Count != mod.Groups.Count; @@ -121,7 +162,12 @@ public class ModSettings2 { if( Settings.TryGetValue( group.Name, out var config ) ) { - list.Add( config ); + var actualConfig = FixSetting( group, config ); + list.Add( actualConfig ); + if( actualConfig != config ) + { + changes = true; + } } else { @@ -130,7 +176,7 @@ public class ModSettings2 } } - settings = new ModSettings2 + settings = new ModSettings { Enabled = Enabled, Priority = Priority, diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f4a931fa..50ce0d07 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -40,7 +40,7 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod2.Manager ModManager { get; private set; } = null!; + public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static SimpleRedirectManager Redirects { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; @@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod2.Manager( Config.ModDirectory ); + ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); ModFileSystem = ModFileSystem.Load(); @@ -138,6 +138,7 @@ public class Penumbra : IDalamudPlugin btn = new LaunchButton( _configWindow ); system = new WindowSystem( Name ); system.AddWindow( _configWindow ); + system.AddWindow( cfg.SubModPopup ); Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } @@ -294,8 +295,7 @@ public class Penumbra : IDalamudPlugin case "reload": { ModManager.DiscoverMods(); - Dalamud.Chat.Print( - $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods." + Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods." ); break; } @@ -314,7 +314,8 @@ public class Penumbra : IDalamudPlugin } case "debug": { - // TODO + Config.DebugMode = true; + Config.Save(); break; } case "enable": @@ -370,7 +371,7 @@ public class Penumbra : IDalamudPlugin { var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); list.Add( Dalamud.PluginInterface.ConfigFile ); - list.Add( new FileInfo( Mod2.Manager.ModFileSystemFile ) ); + list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); return list; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 66473453..de9cf7b7 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Runtime.InteropServices; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Raii; using Penumbra.Collections; @@ -21,7 +22,7 @@ public partial class ModFileSystemSelector } private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; - private readonly IReadOnlySet< Mod2 > _newMods = new HashSet< Mod2 >(); + private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >(); private LowerString _modFilter = LowerString.Empty; private int _filterType = -1; private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; @@ -75,7 +76,7 @@ public partial class ModFileSystemSelector // Folders have default state and are filtered out on the direct string instead of the other options. // If any filter is set, they should be hidden by default unless their children are visible, // or they contain the path search string. - protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state ) + protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) { if( path is ModFileSystem.Folder f ) { @@ -88,7 +89,7 @@ public partial class ModFileSystemSelector } // Apply the string filters. - private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod2 mod ) + private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod ) { return _filterType switch { @@ -102,7 +103,7 @@ public partial class ModFileSystemSelector } // Only get the text color for a mod if no filters are set. - private uint GetTextColor( Mod2 mod, ModSettings2? settings, ModCollection collection ) + private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) { if( _newMods.Contains( mod ) ) { @@ -119,7 +120,7 @@ public partial class ModFileSystemSelector return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value(); } - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); if( conflicts.Count == 0 ) { return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value(); @@ -130,7 +131,7 @@ public partial class ModFileSystemSelector : ColorId.HandledConflictMod.Value(); } - private bool CheckStateFilters( Mod2 mod, ModSettings2? settings, ModCollection collection, ref ModState state ) + private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) { var isNew = _newMods.Contains( mod ); // Handle mod details. @@ -188,7 +189,7 @@ public partial class ModFileSystemSelector } // Conflicts can only be relevant if the mod is enabled. - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList(); + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); if( conflicts.Count > 0 ) { if( conflicts.Any( c => !c.Solved ) ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ca0fbaf0..4d7a236e 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,23 +1,30 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Import; using Penumbra.Mods; namespace Penumbra.UI.Classes; -public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, ModFileSystemSelector.ModState > +public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > { - public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty; + private readonly FileDialogManager _fileManager = new(); + private TexToolsImporter? _import; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod2 > newMods ) + public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod > newMods ) : base( fileSystem ) { _newMods = newMods; @@ -26,6 +33,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo SubscribeRightClickFolder( InheritDescendants, 15 ); SubscribeRightClickFolder( OwnDescendants, 15 ); AddButton( AddNewModButton, 0 ); + AddButton( AddImportModButton, 1 ); AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); @@ -33,6 +41,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; + Penumbra.ModManager.ModMetaChanged += OnModMetaChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); @@ -43,6 +52,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo base.Dispose(); Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; + Penumbra.ModManager.ModMetaChanged -= OnModMetaChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; @@ -64,10 +74,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo protected override uint FolderLineColor => ColorId.FolderLine.Value(); - protected override void DrawLeafName( FileSystem< Mod2 >.Leaf leaf, in ModState state, bool selected ) + protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); + using var id = ImRaii.PushId( leaf.Value.Index ); using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); } @@ -107,17 +118,90 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo // Add custom buttons. - private static void AddNewModButton( Vector2 size ) + private string _newModName = string.Empty; + + private void AddNewModButton( Vector2 size ) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) ) - { } + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", !Penumbra.ModManager.Valid, true ) ) + { + ImGui.OpenPopup( "Create New Mod" ); + } + + if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) + { + try + { + var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.CreateMeta( newDir, _newModName, string.Empty, string.Empty, "1.0", string.Empty ); + Penumbra.ModManager.AddMod( newDir ); + _newModName = string.Empty; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); + } + } + } + + // Add an import mods button that opens a file selector. + private void AddImportModButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) ) + { + _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => + { + if( s ) + { + _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ) ); + ImGui.OpenPopup( "Import Status" ); + } + }, 0, Penumbra.Config.ModDirectory ); + } + + _fileManager.Draw(); + DrawInfoPopup(); + } + + // Draw the progress information for import. + private void DrawInfoPopup() + { + var display = ImGui.GetIO().DisplaySize; + ImGui.SetNextWindowSize( display / 4 ); + ImGui.SetNextWindowPos( 3 * display / 8 ); + using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); + if( _import != null && popup.Success ) + { + _import.DrawProgressInfo( ImGuiHelpers.ScaledVector2( -1, ImGui.GetFrameHeight() ) ); + if( _import.State == ImporterState.Done ) + { + ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 ); + if( ImGui.Button( "Close", -Vector2.UnitX ) ) + { + _import = null; + ImGui.CloseCurrentPopup(); + } + } + } } private void DeleteModButton( Vector2 size ) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, - "Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) ) - { } + var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; + var tt = SelectedLeaf == null + ? "No mod selected." + : "Delete the currently selected mod entirely from your drive.\n" + + "This can not be undone."; + if( !keys ) + { + tt += "\nHold Control and Shift while clicking to delete the mod."; + } + + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) + && Selected != null ) + { + Penumbra.ModManager.DeleteMod( Selected.Index ); + } } @@ -146,6 +230,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo } } + private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName ) + { + switch( type ) + { + case MetaChangeType.Name: + case MetaChangeType.Author: + SetFilterDirty(); + break; + } + } + private void OnInheritanceChange( bool _ ) { SetFilterDirty(); @@ -175,17 +270,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo OnSelectionChange( Selected, Selected, default ); } - private void OnSelectionChange( Mod2? _1, Mod2? newSelection, in ModState _2 ) + private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 ) { if( newSelection == null ) { - SelectedSettings = ModSettings2.Empty; + SelectedSettings = ModSettings.Empty; SelectedSettingCollection = ModCollection.Empty; } else { ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ]; - SelectedSettings = settings ?? ModSettings2.Empty; + SelectedSettings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/UI/Classes/SubModEditWindow.cs b/Penumbra/UI/Classes/SubModEditWindow.cs new file mode 100644 index 00000000..8b562959 --- /dev/null +++ b/Penumbra/UI/Classes/SubModEditWindow.cs @@ -0,0 +1,225 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public class SubModEditWindow : Window +{ + private const string WindowBaseLabel = "###SubModEdit"; + private Mod? _mod; + private int _groupIdx = -1; + private int _optionIdx = -1; + private IModGroup? _group; + private ISubMod? _subMod; + private readonly List< FilePathInfo > _availableFiles = new(); + + private readonly struct FilePathInfo + { + public readonly FullPath File; + public readonly Utf8RelPath RelFile; + public readonly long Size; + public readonly List< (int, int, Utf8GamePath) > SubMods; + + public FilePathInfo( FileInfo file, Mod mod ) + { + File = new FullPath( file ); + RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty; + Size = file.Length; + SubMods = new List< (int, int, Utf8GamePath) >(); + var path = File; + foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) + { + foreach( var (subMod, optionIdx) in group.WithIndex() ) + { + SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) ); + } + } + SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) ); + } + } + + private readonly HashSet< MetaManipulation > _manipulations = new(); + private readonly Dictionary< Utf8GamePath, FullPath > _files = new(); + private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new(); + + public void Activate( Mod mod, int groupIdx, int optionIdx ) + { + IsOpen = true; + _mod = mod; + _groupIdx = groupIdx; + _group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null; + _optionIdx = optionIdx; + _subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default; + _availableFiles.Clear(); + _availableFiles.AddRange( mod.BasePath.EnumerateDirectories() + .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + .Select( f => new FilePathInfo( f, _mod ) ) ); + + _manipulations.Clear(); + _manipulations.UnionWith( _subMod.Manipulations ); + _files.SetTo( _subMod.Files ); + _fileSwaps.SetTo( _subMod.FileSwaps ); + + WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}"; + } + + public override bool DrawConditions() + => _subMod != null; + + public override void Draw() + { + using var tabBar = ImRaii.TabBar( "##tabs" ); + if( !tabBar ) + { + return; + } + + DrawFileTab(); + DrawMetaTab(); + DrawSwapTab(); + } + + private void Save() + { + if( _mod != null ) + { + Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps ); + } + } + + public override void OnClose() + { + _subMod = null; + } + + private void DrawFileTab() + { + using var tab = ImRaii.TabItem( "File Redirections" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##files", 3 ); + if( !list ) + { + return; + } + + foreach( var file in _availableFiles ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( file.RelFile.Path ); + ImGui.TableNextColumn(); + ImGui.Text( file.Size.ToString() ); + ImGui.TableNextColumn(); + if( file.SubMods.Count == 0 ) + { + ImGui.Text( "Unused" ); + } + + foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null; + var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default; + var text = groupIdx >= 0 + ? $"{group!.Name} - {option.Name}" + : option.Name; + ImGui.Text( text ); + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + } + } + + ImGui.TableNextRow(); + foreach( var (gamePath, fullPath) in _files ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + ImGui.Text( fullPath.FullName ); + ImGui.TableNextColumn(); + } + } + + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem( "Meta Manipulations" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##meta", 3 ); + if( !list ) + { + return; + } + + foreach( var manip in _manipulations ) + { + ImGui.TableNextColumn(); + ImGui.Text( manip.ManipulationType.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.ToString(), + MetaManipulation.Type.Est => manip.Est.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.ToString(), + _ => string.Empty, + } ); + ImGui.TableNextColumn(); + ImGui.Text( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), + MetaManipulation.Type.Est => manip.Est.Entry.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), + _ => string.Empty, + } ); + } + } + + private void DrawSwapTab() + { + using var tab = ImRaii.TabItem( "File Swaps" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##swaps", 3 ); + if( !list ) + { + return; + } + + foreach( var (from, to) in _fileSwaps ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( from.Path ); + ImGui.TableNextColumn(); + ImGui.Text( to.FullName ); + ImGui.TableNextColumn(); + } + } + + public SubModEditWindow() + : base( WindowBaseLabel ) + { } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index f7aedac3..c99cb3d4 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index fc1cc3d2..3e843231 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -4,6 +4,7 @@ using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.GameData.ByteString; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs new file mode 100644 index 00000000..0ca5dcd4 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class ModPanel + { + public readonly Queue< Action > _delayedActions = new(); + + private void DrawAddOptionGroupInput() + { + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); + ImGui.SameLine(); + + var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false ); + var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + tt, !nameValid, true ) ) + { + Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName ); + _newGroupName = string.Empty; + } + } + + private Vector2 _cellPadding = Vector2.Zero; + private Vector2 _itemSpacing = Vector2.Zero; + + private void DrawEditModTab() + { + using var tab = DrawTab( EditModTabHeader, Tabs.Edit ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##editChild", -Vector2.One ); + if( !child ) + { + return; + } + + _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale }; + _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale }; + + EditRegularMeta(); + ImGui.Dummy( _window._defaultSpace ); + + if( TextInput( "Mod Path", PathFieldIdx, NoFieldIdx, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) ) + { + _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); + } + + ImGui.Dummy( _window._defaultSpace ); + DrawAddOptionGroupInput(); + ImGui.Dummy( _window._defaultSpace ); + + for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx ) + { + EditGroup( groupIdx ); + } + + EndActions(); + EditDescriptionPopup(); + } + + + // Special field indices to reuse the same string buffer. + private const int NoFieldIdx = -1; + private const int NameFieldIdx = -2; + private const int AuthorFieldIdx = -3; + private const int VersionFieldIdx = -4; + private const int WebsiteFieldIdx = -5; + private const int PathFieldIdx = -6; + private const int DescriptionFieldIdx = -7; + + private void EditRegularMeta() + { + if( TextInput( "Name", NameFieldIdx, NoFieldIdx, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) ) + { + Penumbra.ModManager.ChangeModName( _mod.Index, newName ); + } + + if( TextInput( "Author", AuthorFieldIdx, NoFieldIdx, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) ) + { + Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor ); + } + + if( TextInput( "Version", VersionFieldIdx, NoFieldIdx, _mod.Version, out var newVersion, 32, _window._inputTextWidth.X ) ) + { + Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion ); + } + + if( TextInput( "Website", WebsiteFieldIdx, NoFieldIdx, _mod.Website, out var newWebsite, 256, _window._inputTextWidth.X ) ) + { + Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); + } + + if( ImGui.Button( "Edit Description", _window._inputTextWidth ) ) + { + _delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) ); + } + + if( ImGui.Button( "Edit Default Mod", _window._inputTextWidth ) ) + { + _window.SubModPopup.Activate( _mod, -1, 0 ); + } + } + + + // Temporary strings + private string? _currentEdit; + private int? _currentGroupPriority; + private int _currentField = -1; + private int _optionIndex = -1; + + private string _newGroupName = string.Empty; + private string _newOptionName = string.Empty; + private string _newDescription = string.Empty; + private int _newDescriptionIdx = -1; + + private void EditGroup( int groupIdx ) + { + var group = _mod.Groups[ groupIdx ]; + using var id = ImRaii.PushId( groupIdx ); + using var frame = ImRaii.FramedGroup( $"Group #{groupIdx + 1}" ); + + using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding ) + .Push( ImGuiStyleVar.ItemSpacing, _itemSpacing ); + + if( TextInput( "##Name", groupIdx, NoFieldIdx, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) ) + { + Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName ); + } + + ImGuiUtil.HoverTooltip( "Group Name" ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) + { + _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) ); + } + + ImGui.SameLine(); + + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Edit group description.", false, true ) ) + { + _delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) ); + } + + ImGui.SameLine(); + + if( PriorityInput( "##Priority", groupIdx, NoFieldIdx, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) + { + Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); + } + + ImGuiUtil.HoverTooltip( "Group Priority" ); + + ImGui.SetNextItemWidth( _window._inputTextWidth.X - 2 * ImGui.GetFrameHeight() - 8 * ImGuiHelpers.GlobalScale ); + using( var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ) ) + { + if( combo ) + { + foreach( var type in new[] { SelectType.Single, SelectType.Multi } ) + { + if( ImGui.Selectable( GroupTypeName( type ), group.Type == type ) ) + { + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type ); + } + } + } + } + + ImGui.SameLine(); + + var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + tt, groupIdx == 0, true ) ) + { + _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) ); + } + + ImGui.SameLine(); + tt = groupIdx == _mod.Groups.Count - 1 + ? "Can not move this group further downwards." + : $"Move this group down to group {groupIdx + 2}."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + tt, groupIdx == _mod.Groups.Count - 1, true ) ) + { + _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); + } + + ImGui.Dummy( _window._defaultSpace ); + + using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, _window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); + ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); + ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); + if( table ) + { + for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + { + EditOption( group, groupIdx, optionIdx ); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Add a new option to this group.", _newOptionName.Length == 0, true ) ) + { + Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName ); + _newOptionName = string.Empty; + } + } + } + + private static string GroupTypeName( SelectType type ) + => type switch + { + SelectType.Single => "Single Group", + SelectType.Multi => "Multi Group", + _ => "Unknown", + }; + + private int _dragDropGroupIdx = -1; + private int _dragDropOptionIdx = -1; + + private void OptionDragDrop( IModGroup group, int groupIdx, int optionIdx ) + { + const string label = "##DragOption"; + using( var source = ImRaii.DragDropSource() ) + { + if( source ) + { + if( ImGui.SetDragDropPayload( label, IntPtr.Zero, 0 ) ) + { + _dragDropGroupIdx = groupIdx; + _dragDropOptionIdx = optionIdx; + } + + ImGui.Text( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); + } + } + + using( var target = ImRaii.DragDropTarget() ) + { + if( target.Success && ImGuiUtil.IsDropping( label ) ) + { + if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) + { + if( _dragDropGroupIdx == groupIdx ) + { + // TODO + Dalamud.Chat.Print( + $"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" ); + } + else + { + Dalamud.Chat.Print( + $"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" ); + } + } + + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + } + } + + private void EditOption( IModGroup group, int groupIdx, int optionIdx ) + { + var option = group[ optionIdx ]; + using var id = ImRaii.PushId( optionIdx ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable( $"Option #{optionIdx + 1}" ); + OptionDragDrop( group, groupIdx, optionIdx ); + + ImGui.TableNextColumn(); + if( TextInput( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) + { + Penumbra.ModManager.RenameOption( _mod, groupIdx, optionIdx, newOptionName ); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) + { + _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( _mod, groupIdx, optionIdx ) ); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Edit this option.", false, true ) ) + { + _window.SubModPopup.Activate( _mod, groupIdx, optionIdx ); + } + + ImGui.TableNextColumn(); + if( group.Type == SelectType.Multi ) + { + if( PriorityInput( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, + 50 * ImGuiHelpers.GlobalScale ) ) + { + Penumbra.ModManager.ChangeOptionPriority( _mod, groupIdx, optionIdx, priority ); + } + + ImGuiUtil.HoverTooltip( "Option priority." ); + } + } + + private bool TextInput( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) + { + var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputText( label, ref tmp, maxLength ) ) + { + _currentEdit = tmp; + _optionIndex = option; + _currentField = field; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) + { + var ret = _currentEdit != oldValue; + value = _currentEdit; + _currentEdit = null; + _currentField = NoFieldIdx; + _optionIndex = NoFieldIdx; + return ret; + } + + value = string.Empty; + return false; + } + + private bool PriorityInput( string label, int field, int option, int oldValue, out int value, float width ) + { + var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputInt( label, ref tmp, 0, 0 ) ) + { + _currentGroupPriority = tmp; + _optionIndex = option; + _currentField = field; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) + { + var ret = _currentGroupPriority != oldValue; + value = _currentGroupPriority.Value; + _currentGroupPriority = null; + _currentField = NoFieldIdx; + _optionIndex = NoFieldIdx; + return ret; + } + + value = 0; + return false; + } + + // Delete a marked group or option outside of iteration. + private void EndActions() + { + while( _delayedActions.TryDequeue( out var action ) ) + { + action.Invoke(); + } + } + + private void OpenEditDescriptionPopup( int groupIdx ) + { + _newDescriptionIdx = groupIdx; + _newDescription = groupIdx < 0 ? _mod.Description : _mod.Groups[ groupIdx ].Description; + ImGui.OpenPopup( "Edit Description" ); + } + + private void EditDescriptionPopup() + { + using var popup = ImRaii.Popup( "Edit Description" ); + if( popup ) + { + if( ImGui.IsWindowAppearing() ) + { + ImGui.SetKeyboardFocusHere(); + } + + ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) ); + ImGui.Dummy( _window._defaultSpace ); + + var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 ); + var width = 2 * buttonSize.X + + 4 * ImGui.GetStyle().FramePadding.X + + ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 ); + + var oldDescription = _newDescriptionIdx == DescriptionFieldIdx + ? _mod.Description + : _mod.Groups[ _newDescriptionIdx ].Description; + + var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; + + if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) ) + { + if( _newDescriptionIdx == DescriptionFieldIdx ) + { + Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); + } + else if( _newDescriptionIdx >= 0 ) + { + Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); + } + + ImGui.CloseCurrentPopup(); + } + + ImGui.SameLine(); + if( ImGui.Button( "Cancel", buttonSize ) + || ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + { + _newDescriptionIdx = NoFieldIdx; + _newDescription = string.Empty; + ImGui.CloseCurrentPopup(); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs new file mode 100644 index 00000000..6b91c1b0 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs @@ -0,0 +1,214 @@ +using System; +using System.Diagnostics; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.GameFonts; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class ModPanel : IDisposable + { + // We use a big, nice game font for the title. + private readonly GameFontHandle _nameFont = + Dalamud.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); + + public void Dispose() + { + _nameFont.Dispose(); + } + + // Header data. + private string _modName = string.Empty; + private string _modAuthor = string.Empty; + private string _modVersion = string.Empty; + private string _modWebsite = string.Empty; + private string _modWebsiteButton = string.Empty; + private bool _websiteValid; + + private float _modNameWidth; + private float _modAuthorWidth; + private float _modVersionWidth; + private float _modWebsiteButtonWidth; + private float _secondRowWidth; + + // Draw the header for the current mod, + // consisting of its name, version, author and website, if they exist. + private void DrawModHeader() + { + var offset = DrawModName(); + DrawVersion( offset ); + DrawSecondRow( offset ); + } + + // Draw the mod name in the game font with a 2px border, centered, + // with at least the width of the version space to each side. + private float DrawModName() + { + var decidingWidth = Math.Max( _secondRowWidth, ImGui.GetWindowWidth() ); + var offsetWidth = ( decidingWidth - _modNameWidth ) / 2; + var offsetVersion = _modVersion.Length > 0 + ? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + : 0; + var offset = Math.Max( offsetWidth, offsetVersion ); + if( offset > 0 ) + { + ImGui.SetCursorPosX( offset ); + } + + using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.MetaInfoText ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale ); + using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); + ImGuiUtil.DrawTextButton( _modName, Vector2.Zero, 0 ); + return offset; + } + + // Draw the version in the top-right corner. + private void DrawVersion( float offset ) + { + var oldPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos( new Vector2( 2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X, + ImGui.GetStyle().FramePadding.Y ) ); + ImGuiUtil.TextColored( Colors.MetaInfoText, _modVersion ); + ImGui.SetCursorPos( oldPos ); + } + + // Draw author and website if they exist. The website is a button if it is valid. + // Usually, author begins at the left boundary of the name, + // and website ends at the right boundary of the name. + // If their combined width is larger than the name, they are combined-centered. + private void DrawSecondRow( float offset ) + { + if( _modAuthor.Length == 0 ) + { + if( _modWebsiteButton.Length == 0 ) + { + ImGui.NewLine(); + return; + } + + offset += ( _modNameWidth - _modWebsiteButtonWidth ) / 2; + ImGui.SetCursorPosX( offset ); + DrawWebsite(); + } + else if( _modWebsiteButton.Length == 0 ) + { + offset += ( _modNameWidth - _modAuthorWidth ) / 2; + ImGui.SetCursorPosX( offset ); + DrawAuthor(); + } + else if( _secondRowWidth < _modNameWidth ) + { + ImGui.SetCursorPosX( offset ); + DrawAuthor(); + ImGui.SameLine( offset + _modNameWidth - _modWebsiteButtonWidth ); + DrawWebsite(); + } + else + { + offset -= ( _secondRowWidth - _modNameWidth ) / 2; + if( offset > 0 ) + { + ImGui.SetCursorPosX( offset ); + } + + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + } + + // Draw the author text. + private void DrawAuthor() + { + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + ImGuiUtil.TextColored( Colors.MetaInfoText, "by " ); + ImGui.SameLine(); + style.Pop(); + ImGui.Text( _mod.Author ); + } + + // Draw either a website button if the source is a valid website address, + // or a source text if it is not. + private void DrawWebsite() + { + if( _websiteValid ) + { + if( ImGui.SmallButton( _modWebsiteButton ) ) + { + try + { + var process = new ProcessStartInfo( _modWebsite ) + { + UseShellExecute = true, + }; + Process.Start( process ); + } + catch + { + // ignored + } + } + + ImGuiUtil.HoverTooltip( _modWebsite ); + } + else + { + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + ImGuiUtil.TextColored( Colors.MetaInfoText, "from " ); + ImGui.SameLine(); + style.Pop(); + ImGui.Text( _mod.Website ); + } + } + + // Update all mod header data. Should someone change frame padding or item spacing, + // or his default font, this will break, but he will just have to select a different mod to restore. + private void UpdateModData() + { + // Name + var name = $" {_mod.Name} "; + if( name != _modName ) + { + using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); + _modName = name; + _modNameWidth = ImGui.CalcTextSize( name ).X + 2 * ( ImGui.GetStyle().FramePadding.X + 2 * ImGuiHelpers.GlobalScale ); + } + + // Author + var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; + if( author != _modAuthor ) + { + _modAuthor = author; + _modAuthorWidth = ImGui.CalcTextSize( author ).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + + // Version + var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; + if( version != _modVersion ) + { + _modVersion = version; + _modVersionWidth = ImGui.CalcTextSize( version ).X; + } + + // Website + if( _modWebsite != _mod.Website ) + { + _modWebsite = _mod.Website; + _websiteValid = Uri.TryCreate( _modWebsite, UriKind.Absolute, out var uriResult ) + && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); + _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; + _modWebsiteButtonWidth = _websiteValid + ? ImGui.CalcTextSize( _modWebsiteButton ).X + 2 * ImGui.GetStyle().FramePadding.X + : ImGui.CalcTextSize( _modWebsiteButton ).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs new file mode 100644 index 00000000..7ab3b496 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -0,0 +1,207 @@ +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class ModPanel + { + private ModSettings _settings = null!; + private ModCollection _collection = null!; + private bool _emptySetting; + private bool _inherited; + private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty; + + private int? _currentPriority; + + private void UpdateSettingsData( ModFileSystemSelector selector ) + { + _settings = selector.SelectedSettings; + _collection = selector.SelectedSettingCollection; + _emptySetting = _settings == ModSettings.Empty; + _inherited = _collection != Penumbra.CollectionManager.Current; + _conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); + } + + // Draw the whole settings tab as well as its contents. + private void DrawSettingsTab() + { + using var tab = DrawTab( SettingsTabHeader, Tabs.Settings ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##settings" ); + if( !child ) + { + return; + } + + DrawInheritedWarning(); + ImGui.Dummy( _window._defaultSpace ); + DrawEnabledInput(); + ImGui.SameLine(); + DrawPriorityInput(); + DrawRemoveSettings(); + ImGui.Dummy( _window._defaultSpace ); + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawSingleGroup( _mod.Groups[ idx ], idx ); + } + + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawMultiGroup( _mod.Groups[ idx ], idx ); + } + } + + + // Draw a big red bar if the current setting is inherited. + private void DrawInheritedWarning() + { + if( !_inherited ) + { + return; + } + + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var width = new Vector2( ImGui.GetContentRegionAvail().X, 0 ); + if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", width ) ) + { + Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false ); + } + + ImGuiUtil.HoverTooltip( "You can click this button to copy the current settings to the current selection.\n" + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection." ); + } + + // Draw a checkbox for the enabled status of the mod. + private void DrawEnabledInput() + { + var enabled = _settings.Enabled; + if( ImGui.Checkbox( "Enabled", ref enabled ) ) + { + Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); + } + } + + // Draw a priority input. + // Priority is changed on deactivation of the input box. + private void DrawPriorityInput() + { + var priority = _currentPriority ?? _settings.Priority; + ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) + { + _currentPriority = priority; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue ) + { + if( _currentPriority != _settings.Priority ) + { + Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value ); + } + + _currentPriority = null; + } + + ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with higher priority take precedence before Mods with lower priority.\n" + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have higher priority than Mod B." ); + } + + // Draw a button to remove the current settings and inherit them instead + // on the top-right corner of the window/tab. + private void DrawRemoveSettings() + { + const string text = "Remove Settings"; + if( _inherited || _emptySetting ) + { + return; + } + + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; + ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if( ImGui.Button( text ) ) + { + Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); + } + + ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n" + + "If no inherited collection has settings for this mod, it will be disabled." ); + } + + // Draw a single group selector as a combo box. + // If a description is provided, add a help marker besides it. + private void DrawSingleGroup( IModGroup group, int groupIdx ) + { + if( group.Type != SelectType.Single || !group.IsOption ) + { + return; + } + + using var id = ImRaii.PushId( groupIdx ); + var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ groupIdx ]; + ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); + using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); + if( combo ) + { + for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + { + if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); + } + } + } + + combo.Dispose(); + ImGui.SameLine(); + if( group.Description.Length > 0 ) + { + ImGuiUtil.LabeledHelpMarker( group.Name, group.Description ); + } + else + { + ImGui.Text( group.Name ); + } + } + + // Draw a multi group selector as a bordered set of checkboxes. + // If a description is provided, add a help marker in the title. + private void DrawMultiGroup( IModGroup group, int groupIdx ) + { + if( group.Type != SelectType.Multi || !group.IsOption ) + { + return; + } + + using var id = ImRaii.PushId( groupIdx ); + var flags = _emptySetting ? 0u : _settings.Settings[ groupIdx ]; + Widget.BeginFramedGroup( group.Name, group.Description ); + for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + { + var flag = 1u << idx2; + var setting = ( flags & flag ) != 0; + if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) ) + { + flags = setting ? flags | flag : flags & ~flag; + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); + } + } + + Widget.EndFramedGroup(); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs new file mode 100644 index 00000000..556be8dc --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -0,0 +1,178 @@ +using System; +using System.ComponentModel.Design; +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class ModPanel + { + [Flags] + private enum Tabs + { + Description = 0x01, + Settings = 0x02, + ChangedItems = 0x04, + Conflicts = 0x08, + Edit = 0x10, + }; + + // We want to keep the preferred tab selected even if switching through mods. + private Tabs _preferredTab = Tabs.Settings; + private Tabs _availableTabs = 0; + + // Required to use tabs that can not be closed but have a flag to set them open. + private static readonly Utf8String ConflictTabHeader = Utf8String.FromStringUnsafe( "Conflicts", false ); + 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", false ); + + private void DrawTabBar() + { + ImGui.Dummy( _window._defaultSpace ); + using var tabBar = ImRaii.TabBar( "##ModTabs" ); + if( !tabBar ) + { + return; + } + + _availableTabs = Tabs.Settings + | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) + | ( _mod.Description.Length > 0 ? Tabs.Description : 0 ) + | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) + | ( Penumbra.Config.ShowAdvanced ? Tabs.Edit : 0 ); + + DrawSettingsTab(); + DrawDescriptionTab(); + DrawChangedItemsTab(); + DrawConflictsTab(); + DrawEditModTab(); + } + + // Just a simple text box with the wrapped description, if it exists. + private void DrawDescriptionTab() + { + using var tab = DrawTab( DescriptionTabHeader, Tabs.Description ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##description" ); + if( !child ) + { + return; + } + + ImGui.TextWrapped( _mod.Description ); + } + + // A simple clipped list of changed items. + private void DrawChangedItemsTab() + { + using var tab = DrawTab( ChangedItemsTabHeader, Tabs.ChangedItems ); + if( !tab ) + { + return; + } + + using var list = ImRaii.ListBox( "##changedItems", -Vector2.One ); + if( !list ) + { + return; + } + + var zipList = ZipList.FromSortedList( _mod.ChangedItems ); + var height = ImGui.GetTextLineHeight(); + ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height ); + } + + // If any conflicts exist, show them in this tab. + private void DrawConflictsTab() + { + using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts ); + if( !tab ) + { + return; + } + + using var box = ImRaii.ListBox( "##conflicts" ); + if( !box ) + { + return; + } + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); + Mod? oldBadMod = null; + using var indent = ImRaii.PushIndent( 0f ); + foreach( var conflict in conflicts ) + { + var badMod = Penumbra.ModManager[ conflict.Mod2 ]; + if( badMod != oldBadMod ) + { + if( oldBadMod != null ) + { + indent.Pop( 30f ); + } + + if( ImGui.Selectable( badMod.Name ) ) + { + _window._selector.SelectByValue( badMod ); + } + + ImGui.SameLine(); + using var color = ImRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); + ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); + + indent.Push( 30f ); + } + + if( conflict.Data is Utf8GamePath p ) + { + unsafe + { + ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); + } + } + else if( conflict.Data is MetaManipulation m ) + { + ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); + } + + oldBadMod = badMod; + } + } + + + // Draw a tab by given name if it is available, and deal with changing the preferred tab. + private ImRaii.IEndObject DrawTab( Utf8String name, Tabs flag ) + { + if( !_availableTabs.HasFlag( flag ) ) + { + return ImRaii.IEndObject.Empty; + } + + var flags = _preferredTab == flag ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None; + unsafe + { + var tab = ImRaii.TabItem( name.Path, flags ); + if( ImGui.IsItemClicked() ) + { + _preferredTab = flag; + } + + return tab; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index b0d1145e..dd5dddc7 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -1,386 +1,15 @@ using System; -using System.Diagnostics; using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Mods; using Penumbra.UI.Classes; namespace Penumbra.UI; -public partial class ConfigWindow -{ - private class ModPanel - { - private readonly ConfigWindow _window; - private bool _valid; - private bool _emptySetting; - private bool _inherited; - private ModFileSystem.Leaf _leaf = null!; - private Mod2 _mod = null!; - private ModSettings2 _settings = null!; - private ModCollection _collection = null!; - private string _lastWebsite = string.Empty; - private bool _websiteValid; - - private string? _currentSortOrderPath; - private int? _currentPriority; - - public ModPanel( ConfigWindow window ) - => _window = window; - - private void Init( ModFileSystemSelector selector ) - { - _valid = selector.Selected != null; - if( !_valid ) - { - return; - } - - _leaf = selector.SelectedLeaf!; - _mod = selector.Selected!; - _settings = selector.SelectedSettings; - _collection = selector.SelectedSettingCollection; - _emptySetting = _settings == ModSettings2.Empty; - _inherited = _collection != Penumbra.CollectionManager.Current; - } - - public void Draw( ModFileSystemSelector selector ) - { - Init( selector ); - if( !_valid ) - { - return; - } - - DrawInheritedWarning(); - DrawHeaderLine(); - DrawFilesystemPath(); - DrawEnabledInput(); - ImGui.SameLine(); - DrawPriorityInput(); - DrawRemoveSettings(); - DrawTabBar(); - } - - private void DrawDescriptionTab() - { - if( _mod.Description.Length == 0 ) - { - return; - } - - using var tab = ImRaii.TabItem( "Description" ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##tab" ); - if( !child ) - { - return; - } - - ImGui.TextWrapped( _mod.Description ); - } - - private void DrawSettingsTab() - { - if( !_mod.HasOptions ) - { - return; - } - - using var tab = ImRaii.TabItem( "Settings" ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##tab" ); - if( !child ) - { - return; - } - - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) - { - var group = _mod.Groups[ idx ]; - if( group.Type == SelectType.Single && group.IsOption ) - { - using var id = ImRaii.PushId( idx ); - var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ idx ]; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); - if( combo ) - { - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) - { - if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, ( uint )idx2 ); - } - } - } - - combo.Dispose(); - ImGui.SameLine(); - if( group.Description.Length > 0 ) - { - ImGuiUtil.LabeledHelpMarker( group.Name, group.Description ); - } - else - { - ImGui.Text( group.Name ); - } - } - } - - // TODO add description - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) - { - var group = _mod.Groups[ idx ]; - if( group.Type == SelectType.Multi && group.IsOption ) - { - using var id = ImRaii.PushId( idx ); - var flags = _emptySetting ? 0u : _settings.Settings[ idx ]; - Widget.BeginFramedGroup( group.Name ); - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) - { - var flag = 1u << idx2; - var setting = ( flags & flag ) != 0; - if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) ) - { - flags = setting ? flags | flag : flags & ~flag; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, flags ); - } - } - - Widget.EndFramedGroup(); - } - } - } - - private void DrawChangedItemsTab() - { - if( _mod.ChangedItems.Count == 0 ) - { - return; - } - - using var tab = ImRaii.TabItem( "Changed Items" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.ListBox( "##changedItems", -Vector2.One ); - if( !list ) - { - return; - } - - foreach( var (name, data) in _mod.ChangedItems ) - { - _window.DrawChangedItem( name, data ); - } - } - - private void DrawTabBar() - { - using var tabBar = ImRaii.TabBar( "##ModTabs" ); - if( !tabBar ) - { - return; - } - - DrawDescriptionTab(); - DrawSettingsTab(); - DrawChangedItemsTab(); - } - - private void DrawInheritedWarning() - { - if( _inherited ) - { - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var w = new Vector2( ImGui.GetContentRegionAvail().X, 0 ); - if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", w ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false ); - } - } - } - - private void DrawPriorityInput() - { - var priority = _currentPriority ?? _settings.Priority; - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "Priority", ref priority, 0, 0 ) ) - { - _currentPriority = priority; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue ) - { - if( _currentPriority != _settings.Priority ) - { - Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value ); - } - - _currentPriority = null; - } - } - - private void DrawRemoveSettings() - { - if( _inherited ) - { - return; - } - - ImGui.SameLine(); - if( ImGui.Button( "Remove Settings" ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); - } - - ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled." ); - } - - private void DrawEnabledInput() - { - var enabled = _settings.Enabled; - if( ImGui.Checkbox( "Enabled", ref enabled ) ) - { - Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); - } - } - - private void DrawFilesystemPath() - { - var fullName = _leaf.FullName(); - var path = _currentSortOrderPath ?? fullName; - ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputText( "Sort Order", ref path, 256 ) ) - { - _currentSortOrderPath = path; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentSortOrderPath != null ) - { - if( _currentSortOrderPath != fullName ) - { - _window._penumbra.ModFileSystem.RenameAndMove( _leaf, _currentSortOrderPath ); - } - - _currentSortOrderPath = null; - } - } - - - // Draw the first info line for the mod panel, - // containing all basic meta information. - private void DrawHeaderLine() - { - DrawName(); - ImGui.SameLine(); - DrawVersion(); - ImGui.SameLine(); - DrawAuthor(); - ImGui.SameLine(); - DrawWebsite(); - } - - // Draw the mod name. - private void DrawName() - { - ImGui.Text( _mod.Name.Text ); - } - - // Draw the author of the mod, if any. - private void DrawAuthor() - { - using var group = ImRaii.Group(); - ImGuiUtil.TextColored( Colors.MetaInfoText, "by" ); - ImGui.SameLine(); - ImGui.Text( _mod.Author.IsEmpty ? "Unknown" : _mod.Author.Text ); - } - - // Draw the mod version, if any. - private void DrawVersion() - { - if( _mod.Version.Length > 0 ) - { - ImGui.Text( $"(Version {_mod.Version})" ); - } - else - { - ImGui.Dummy( Vector2.Zero ); - } - } - - // Update the last seen website and check for validity. - private void UpdateWebsite( string newWebsite ) - { - if( _lastWebsite == newWebsite ) - { - return; - } - - _lastWebsite = newWebsite; - _websiteValid = Uri.TryCreate( _lastWebsite, UriKind.Absolute, out var uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - } - - // Draw the website source either as a button to open the site, - // if it is a valid http website, or as pure text. - private void DrawWebsite() - { - UpdateWebsite( _mod.Website ); - if( _lastWebsite.Length == 0 ) - { - ImGui.Dummy( Vector2.Zero ); - return; - } - - using var group = ImRaii.Group(); - if( _websiteValid ) - { - if( ImGui.Button( "Open Website" ) ) - { - try - { - var process = new ProcessStartInfo( _lastWebsite ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( _lastWebsite ); - } - else - { - ImGuiUtil.TextColored( Colors.MetaInfoText, "from" ); - ImGui.SameLine(); - ImGui.Text( _lastWebsite ); - } - } - } -} - public partial class ConfigWindow { public void DrawModsTab() @@ -401,14 +30,13 @@ public partial class ConfigWindow using var group = ImRaii.Group(); DrawHeaderLine(); - using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true ); + using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar ); if( child ) { _modPanel.Draw( _selector ); } } - // Draw the header line that can quick switch between collections. private void DrawHeaderLine() { @@ -466,4 +94,46 @@ public partial class ConfigWindow ? absoluteSize : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); } + + // The basic setup for the mod panel. + // Details are in other files. + private partial class ModPanel + { + private readonly ConfigWindow _window; + + private bool _valid; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + + public ModPanel( ConfigWindow window ) + { + _window = window; + } + + public void Draw( ModFileSystemSelector selector ) + { + Init( selector ); + if( !_valid ) + { + return; + } + + DrawModHeader(); + DrawTabBar(); + } + + private void Init( ModFileSystemSelector selector ) + { + _valid = selector.Selected != null; + if( !_valid ) + { + return; + } + + _leaf = selector.SelectedLeaf!; + _mod = selector.Selected!; + UpdateSettingsData( selector ); + UpdateModData(); + } + } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 0739bd5b..839809e0 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -47,7 +47,7 @@ public partial class ConfigWindow DrawAdvancedSettings(); } - private string? _settingsNewModDirectory; + private string? _newModDirectory; private readonly FileDialogManager _dialogManager = new(); private bool _dialogOpen; @@ -70,12 +70,18 @@ public partial class ConfigWindow } else { - // TODO - //_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => - //{ - // _newModDirectory = b ? s : _newModDirectory; - // _dialogOpen = false; - //}, _newModDirectory, false); + _newModDirectory ??= Penumbra.Config.ModDirectory; + var startDir = Directory.Exists( _newModDirectory ) + ? _newModDirectory + : Directory.Exists( Penumbra.Config.ModDirectory ) + ? Penumbra.Config.ModDirectory + : "."; + + _dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => + { + _newModDirectory = b ? s : _newModDirectory; + _dialogOpen = false; + }, startDir ); _dialogOpen = true; } } @@ -99,12 +105,12 @@ public partial class ConfigWindow private void DrawRootFolder() { - _settingsNewModDirectory ??= Penumbra.Config.ModDirectory; + _newModDirectory ??= Penumbra.Config.ModDirectory; var spacing = 3 * ImGuiHelpers.GlobalScale; using var group = ImRaii.Group(); ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() ); - var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); ImGui.SameLine(); DrawDirectoryPickerButton(); @@ -121,14 +127,14 @@ public partial class ConfigWindow var pos = ImGui.GetCursorPosX(); ImGui.NewLine(); - if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 ) + if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 ) { return; } if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) { - Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory ); + Penumbra.ModManager.DiscoverMods( _newModDirectory ); } } @@ -137,12 +143,13 @@ public partial class ConfigWindow { DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); ImGui.SameLine(); - if( ImGui.Button( "Rediscover Mods" ) ) + var tt = Penumbra.ModManager.Valid + ? "Force Penumbra to completely re-scan your root directory as if it was restarted." + : "The currently selected folder is not valid. Please select a different folder."; + if( ImGuiUtil.DrawDisabledButton( "Rediscover Mods", Vector2.Zero, tt, !Penumbra.ModManager.Valid ) ) { Penumbra.ModManager.DiscoverMods(); } - - ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); } private void DrawEnabledBox() diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 59b03525..f98ddb0a 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -21,13 +21,14 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly EffectiveTab _effectiveTab; private readonly DebugTab _debugTab; private readonly ResourceTab _resourceTab; + public readonly SubModEditWindow SubModPopup = new(); public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) { _penumbra = penumbra; _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod >() ); // TODO _modPanel = new ModPanel( this ); _collectionsTab = new CollectionsTab( this ); _effectiveTab = new EffectiveTab(); @@ -61,6 +62,7 @@ public sealed partial class ConfigWindow : Window, IDisposable public void Dispose() { _selector.Dispose(); + _modPanel.Dispose(); } private static string GetLabel() @@ -70,10 +72,12 @@ public sealed partial class ConfigWindow : Window, IDisposable private Vector2 _defaultSpace; private Vector2 _inputTextWidth; + private Vector2 _iconButtonSize; private void SetupSizes() { _defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale ); _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); + _iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); } } \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 425f7e3e..1bcdda4d 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -9,7 +9,7 @@ namespace Penumbra.UI; // using the Dalamud-provided collapsible submenu. public class LaunchButton : IDisposable { - private readonly ConfigWindow _configWindow; + private readonly ConfigWindow _configWindow; private readonly TextureWrap? _icon; private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index a5a8bb82..8cce7065 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Penumbra.Util; public static class ArrayExtensions { + public static IEnumerable< (T, int) > WithIndex< T >( this IEnumerable< T > list ) + => list.Select( ( x, i ) => ( x, i ) ); + public static int IndexOf< T >( this IReadOnlyList< T > array, Predicate< T > predicate ) { for( var i = 0; i < array.Count; ++i ) @@ -61,35 +65,4 @@ public static class ArrayExtensions result = default; return false; } - - public static bool Move< T >( this IList< T > list, int idx1, int idx2 ) - { - idx1 = Math.Clamp( idx1, 0, list.Count - 1 ); - idx2 = Math.Clamp( idx2, 0, list.Count - 1 ); - if( idx1 == idx2 ) - { - return false; - } - - var tmp = list[ idx1 ]; - // move element down and shift other elements up - if( idx1 < idx2 ) - { - for( var i = idx1; i < idx2; i++ ) - { - list[ i ] = list[ i + 1 ]; - } - } - // move element up and shift other elements down - else - { - for( var i = idx1; i > idx2; i-- ) - { - list[ i ] = list[ i - 1 ]; - } - } - - list[ idx2 ] = tmp; - return true; - } } \ No newline at end of file diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs index a096e238..a9d4b489 100644 --- a/Penumbra/Util/Backup.cs +++ b/Penumbra/Util/Backup.cs @@ -66,12 +66,12 @@ public static class Backup { ++count; var time = file.CreationTimeUtc; - if( ( oldest?.CreationTimeUtc ?? DateTime.MinValue ) < time ) + if( ( oldest?.CreationTimeUtc ?? DateTime.MaxValue ) > time ) { oldest = file; } - if( ( newest?.CreationTimeUtc ?? DateTime.MaxValue ) > time ) + if( ( newest?.CreationTimeUtc ?? DateTime.MinValue ) < time ) { newest = file; } diff --git a/Penumbra/Util/DialogExtensions.cs b/Penumbra/Util/DialogExtensions.cs deleted file mode 100644 index 33df7bfc..00000000 --- a/Penumbra/Util/DialogExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Diagnostics; -using System.Drawing; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace Penumbra.Util; - -public static class DialogExtensions -{ - public static Task< DialogResult > ShowDialogAsync( this CommonDialog form ) - { - using var process = Process.GetCurrentProcess(); - return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) ); - } - - public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner ) - { - var taskSource = new TaskCompletionSource< DialogResult >(); - var th = new Thread( () => DialogThread( form, owner, taskSource ) ); - th.Start(); - return taskSource.Task; - } - - [STAThread] - private static void DialogThread( CommonDialog form, IWin32Window owner, - TaskCompletionSource< DialogResult > taskSource ) - { - Application.SetCompatibleTextRenderingDefault( false ); - Application.EnableVisualStyles(); - using var hiddenForm = new HiddenForm( form, owner, taskSource ); - Application.Run( hiddenForm ); - Application.ExitThread(); - } - - public class DialogHandle : IWin32Window - { - public IntPtr Handle { get; set; } - - public DialogHandle( IntPtr handle ) - => Handle = handle; - } - - public class HiddenForm : Form - { - private readonly CommonDialog _form; - private readonly IWin32Window _owner; - private readonly TaskCompletionSource< DialogResult > _taskSource; - - public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource ) - { - _form = form; - _owner = owner; - _taskSource = taskSource; - - Opacity = 0; - FormBorderStyle = FormBorderStyle.None; - ShowInTaskbar = false; - Size = new Size( 0, 0 ); - - Shown += HiddenForm_Shown; - } - - private void HiddenForm_Shown( object? sender, EventArgs _ ) - { - Hide(); - try - { - var result = _form.ShowDialog( _owner ); - _taskSource.SetResult( result ); - } - catch( Exception e ) - { - _taskSource.SetException( e ); - } - - Close(); - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs new file mode 100644 index 00000000..97e2638a --- /dev/null +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +namespace Penumbra.Util; + +public static class DictionaryExtensions +{ + // Returns whether two dictionaries contain equal keys and values. + public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + { + if( lhs.Count != rhs.Count ) + { + return false; + } + + foreach( var (key, value) in lhs ) + { + if( !rhs.TryGetValue( key, out var rhsValue ) ) + { + return false; + } + + if( value == null ) + { + if( rhsValue != null ) + { + return false; + } + + continue; + } + + if( !value.Equals( rhsValue ) ) + { + return false; + } + } + + return true; + } + + // Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand. + public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + where TKey : notnull + { + lhs.Clear(); + lhs.EnsureCapacity( rhs.Count ); + foreach( var (key, value) in rhs ) + { + lhs.Add( key, value ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Util/Functions.cs b/Penumbra/Util/Functions.cs deleted file mode 100644 index a3c50e4a..00000000 --- a/Penumbra/Util/Functions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Penumbra.Util; - -public static class Functions -{ - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - public static bool SetDifferent< T >( T oldValue, T newValue, Action< T > set ) where T : IEquatable< T > - { - if( oldValue.Equals( newValue ) ) - { - return false; - } - - set( newValue ); - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 2ff2b37d..25cc6df8 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -74,7 +74,7 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( Mod2 mod, string from, string to ) + public static bool ChangeModMaterials( Mod mod, string from, string to ) { if( ValidStrings( from, to ) ) { diff --git a/Penumbra/Util/SingleOrArrayConverter.cs b/Penumbra/Util/SingleOrArrayConverter.cs deleted file mode 100644 index 29da6249..00000000 --- a/Penumbra/Util/SingleOrArrayConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Penumbra.Util; - -public class SingleOrArrayConverter< T > : JsonConverter -{ - public override bool CanConvert( Type objectType ) - => objectType == typeof( HashSet< T > ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ); - - if( token.Type == JTokenType.Array ) - { - return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); - } - - var tmp = token.ToObject< T >(); - return tmp != null - ? new HashSet< T > { tmp } - : new HashSet< T >(); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - writer.WriteStartArray(); - if( value != null ) - { - var v = ( HashSet< T > )value; - foreach( var val in v ) - { - serializer.Serialize( writer, val?.ToString() ); - } - } - - writer.WriteEndArray(); - } -} \ No newline at end of file diff --git a/Penumbra/Util/StringPathExtensions.cs b/Penumbra/Util/StringPathExtensions.cs deleted file mode 100644 index bcce2a88..00000000 --- a/Penumbra/Util/StringPathExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Penumbra.Util; - -public static class StringPathExtensions -{ - private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars()); - - public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" ) - { - StringBuilder sb = new(s.Length); - foreach( var c in s ) - { - if( Invalid.Contains( c ) ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } - - public static string RemoveInvalidPathSymbols( this string s ) - => string.Concat( s.Split( Path.GetInvalidFileNameChars() ) ); - - public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" ) - { - StringBuilder sb = new(s.Length); - foreach( var c in s ) - { - if( c >= 128 ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } - - public static string ReplaceBadXivSymbols( this string s, string replacement = "_" ) - { - StringBuilder sb = new(s.Length); - foreach( var c in s ) - { - if( c >= 128 || Invalid.Contains( c ) ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs deleted file mode 100644 index 76fad9e2..00000000 --- a/Penumbra/Util/TempFile.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.IO; - -namespace Penumbra.Util; - -public static class TempFile -{ - public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" ) - { - const uint maxTries = 15; - for( var i = 0; i < maxTries; ++i ) - { - var name = Path.GetRandomFileName(); - var path = new FileInfo( Path.Combine( baseDir.FullName, - suffix.Length > 0 ? name[ ..name.LastIndexOf( '.' ) ] + suffix : name ) ); - if( !path.Exists ) - { - return path; - } - } - - throw new IOException(); - } -} \ No newline at end of file From c78725d7d51eb90a1ded9a5f5b7fa66b191bf6de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 17:19:33 +0200 Subject: [PATCH 0140/2451] A few comments, further cleanup. A few TODOs handled. --- Penumbra/Api/ModsController.cs | 20 +- Penumbra/Collections/ModCollection.Cache.cs | 2 +- Penumbra/Collections/ModCollection.Changes.cs | 4 +- Penumbra/Collections/ModCollection.cs | 4 + Penumbra/Configuration.Constants.cs | 16 - ...guration.cs => Configuration.Migration.cs} | 0 Penumbra/Configuration.cs | 12 + Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 27 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 8 + Penumbra/Mods/Manager/Mod.Manager.Root.cs | 7 +- Penumbra/Mods/Manager/Mod.Manager.cs | 15 +- Penumbra/Mods/Mod.BasePath.cs | 2 +- Penumbra/Mods/Mod.ChangedItems.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 2 +- Penumbra/Mods/Mod.Meta.cs | 4 +- Penumbra/Mods/ModCleanup.cs | 2 +- Penumbra/Mods/ModFileSystem.cs | 2 +- Penumbra/Mods/Subclasses/IModGroup.cs | 7 +- .../Subclasses/Mod.Files.MultiModGroup.cs | 1 + .../Subclasses/Mod.Files.SingleModGroup.cs | 1 + Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 15 +- Penumbra/Mods/Subclasses/ModSettings.cs | 27 +- Penumbra/Mods/Subclasses/SelectType.cs | 7 - Penumbra/Penumbra.cs | 6 +- .../Classes/ModFileSystemSelector.Filters.cs | 13 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 42 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 2 + ...ConfigWindow.CollectionsTab.Inheritance.cs | 10 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 21 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 4 +- Penumbra/UI/ConfigWindow.Misc.cs | 8 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 109 ++- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 6 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 16 +- Penumbra/UI/ConfigWindow.ModsTab.Details.cs | 714 ---------------- .../UI/ConfigWindow.ModsTab.DetailsEdit.cs | 381 --------- ...onfigWindow.ModsTab.DetailsManipulation.cs | 773 ----------------- Penumbra/UI/ConfigWindow.ModsTab.Import.cs | 177 ---- Penumbra/UI/ConfigWindow.ModsTab.Panel.cs | 613 -------------- Penumbra/UI/ConfigWindow.ModsTab.Selector.cs | 797 ------------------ Penumbra/UI/ConfigWindow.ModsTab.cs | 13 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 25 +- Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/Util/ArrayExtensions.cs | 32 +- Penumbra/Util/BinaryReaderExtensions.cs | 56 -- .../Util/FixedUlongStringEnumConverter.cs | 2 +- Penumbra/Util/PenumbraSqPackStream.cs | 1 + 47 files changed, 347 insertions(+), 3664 deletions(-) delete mode 100644 Penumbra/Configuration.Constants.cs rename Penumbra/{MigrateConfiguration.cs => Configuration.Migration.cs} (100%) delete mode 100644 Penumbra/Mods/Subclasses/SelectType.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Details.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Import.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Panel.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.Selector.cs delete mode 100644 Penumbra/Util/BinaryReaderExtensions.cs diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index bde6d2db..77f1b496 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,17 +16,15 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - // TODO - return null; - //return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new - //{ - // x.Second?.Enabled, - // x.Second?.Priority, - // x.First.BasePath.Name, - // x.First.Name, - // BasePath = x.First.BasePath.FullName, - // Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), - //} ); + return Penumbra.ModManager.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new + { + x.Second?.Enabled, + x.Second?.Priority, + FolderName = x.First.BasePath.Name, + x.First.Name, + BasePath = x.First.BasePath.FullName, + Files = x.First.AllFiles, + } ); } [Route( HttpVerbs.Post, "/mods" )] diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 5283ec4f..fe008820 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -251,7 +251,7 @@ public partial class ModCollection return; } - var mod = Penumbra.ModManager.Mods[ modIdx ]; + var mod = Penumbra.ModManager[ modIdx ]; AddSubMod( mod.Default, new FileRegister( modIdx, settings.Priority, 0, 0 ), withManipulations ); for( var idx = 0; idx < mod.Groups.Count; ++idx ) { diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index cbf0b09d..9ff07910 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -98,7 +98,7 @@ public partial class ModCollection if( oldValue != newValue ) { var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.SetValue( Penumbra.ModManager.Mods[ idx ], groupIdx, newValue ); + _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); } } @@ -137,7 +137,7 @@ public partial class ModCollection return false; } - _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); + _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager[ idx ] ); return true; } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index a540272f..54561930 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -32,6 +32,10 @@ public partial class ModCollection public IReadOnlyList< ModSettings? > Settings => _settings; + // Returns whether there are settings not in use by any current mod. + public bool HasUnusedSettings + => _unusedSettings.Count > 0; + // Evaluates the settings along the whole inheritance tree. public IEnumerable< ModSettings? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); diff --git a/Penumbra/Configuration.Constants.cs b/Penumbra/Configuration.Constants.cs deleted file mode 100644 index 0cdcd9ec..00000000 --- a/Penumbra/Configuration.Constants.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Penumbra; - -public partial class Configuration -{ - // Contains some default values or boundaries for config values. - public static class Constants - { - public const int CurrentVersion = 3; - public const float MaxAbsoluteSize = 600; - public const int DefaultAbsoluteSize = 250; - public const float MinAbsoluteSize = 50; - public const int MaxScaledSize = 80; - public const int DefaultScaledSize = 20; - public const int MinScaledSize = 5; - } -} \ No newline at end of file diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/Configuration.Migration.cs similarity index 100% rename from Penumbra/MigrateConfiguration.cs rename to Penumbra/Configuration.Migration.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 05d9cc81..565d7975 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -86,4 +86,16 @@ public partial class Configuration : IPluginConfiguration Save(); } } + + // Contains some default values or boundaries for config values. + public static class Constants + { + public const int CurrentVersion = 3; + public const float MaxAbsoluteSize = 600; + public const int DefaultAbsoluteSize = 250; + public const float MinAbsoluteSize = 50; + public const int MaxScaledSize = 80; + public const int DefaultScaledSize = 20; + public const int MinScaledSize = 5; + } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index ce095609..8e44edfd 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -12,14 +12,19 @@ public partial class Mod public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory ); - public event ModPathChangeDelegate? ModPathChanged; + public event ModPathChangeDelegate ModPathChanged; + // Rename/Move a mod directory. + // Updates all collection settings and sort order settings. public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) { var mod = this[ idx ]; // TODO } + // Delete a mod by its index. + // Deletes from filesystem as well as from internal data. + // Updates indices of later mods. public void DeleteMod( int idx ) { var mod = this[ idx ]; @@ -41,9 +46,10 @@ public partial class Mod --remainingMod.Index; } - ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); } + // Load a new mod and add it to the manager if successful. public void AddMod( DirectoryInfo modFolder ) { if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) @@ -59,7 +65,22 @@ public partial class Mod mod.Index = _mods.Count; _mods.Add( mod ); - ModPathChanged?.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath ); + ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath ); + } + + // Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ) + { + switch( type ) + { + case ModPathChangeType.Added: + NewMods.Add( mod ); + break; + case ModPathChangeType.Deleted: + NewMods.Remove( mod ); + break; + } } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 78f3d58c..cdfa142b 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -197,6 +197,14 @@ public sealed partial class Mod return; } + if( mod._groups[ groupIdx ].Count > 63 ) + { + PluginLog.Error( + $"Could not add option {option.Name} to {mod._groups[ groupIdx ].Name} for mod {mod.Name}, " + + "since only up to 64 options are supported in one group." ); + return; + } + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 6967f0bc..a27d5c02 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -11,16 +11,19 @@ public sealed partial class Mod public DirectoryInfo BasePath { get; private set; } = null!; public bool Valid { get; private set; } - public event Action? ModDiscoveryStarted; public event Action? ModDiscoveryFinished; + // Change the mod base directory and discover available mods. public void DiscoverMods( string newDir ) { SetBaseDirectory( newDir, false ); DiscoverMods(); } + // Set the mod base directory. + // If its not the first time, check if it is the same directory as before. + // Also checks if the directory is available and tries to create it if it is not. private void SetBaseDirectory( string newPath, bool firstTime ) { if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) @@ -59,8 +62,10 @@ public sealed partial class Mod } } + // Discover new mods. public void DiscoverMods() { + NewMods.Clear(); ModDiscoveryStarted?.Invoke(); _mods.Clear(); BasePath.Refresh(); diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index e467c733..7a21a3a8 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -6,16 +6,22 @@ namespace Penumbra.Mods; public sealed partial class Mod { - public sealed partial class Manager : IEnumerable< Mod > + public sealed partial class Manager : IReadOnlyList< Mod > { + // An easily accessible set of new mods. + // Mods are added when they are created or imported. + // Mods are removed when they are deleted or when they are toggled in any collection. + // Also gets cleared on mod rediscovery. + public readonly HashSet< Mod > NewMods = new(); + private readonly List< Mod > _mods = new(); + public Mod this[ int idx ] + => _mods[ idx ]; + public Mod this[ Index idx ] => _mods[ idx ]; - public IReadOnlyList< Mod > Mods - => _mods; - public int Count => _mods.Count; @@ -29,6 +35,7 @@ public sealed partial class Mod { SetBaseDirectory( modDirectory, true ); ModOptionChanged += OnModOptionChange; + ModPathChanged += OnModPathChange; } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index c925a4f0..24cf84f1 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -18,7 +18,7 @@ public partial class Mod private Mod( DirectoryInfo basePath ) => BasePath = basePath; - public static Mod? LoadMod( DirectoryInfo basePath ) + private static Mod? LoadMod( DirectoryInfo basePath ) { basePath.Refresh(); if( !basePath.Exists ) diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index 9e68225c..d2996b71 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -8,7 +8,7 @@ public sealed partial class Mod public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; - public void ComputeChangedItems() + private void ComputeChangedItems() { var identifier = GameData.GameData.GetIdentifier(); ChangedItems.Clear(); diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 4ff1bbd9..0eee20b0 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -78,7 +78,7 @@ public partial class Mod public List< FullPath > FindMissingFiles() => AllFiles.Where( f => !f.Exists ).ToList(); - public static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) + private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) { if( !File.Exists( file.FullName ) ) { diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 33760dfe..6e5291a6 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -25,13 +25,13 @@ public sealed partial class Mod { public const uint CurrentFileVersion = 1; public uint FileVersion { get; private set; } = CurrentFileVersion; - public LowerString Name { get; private set; } = "Mod"; + public LowerString Name { get; private set; } = "New Mod"; public LowerString Author { get; private set; } = LowerString.Empty; public string Description { get; private set; } = string.Empty; public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; - private FileInfo MetaFile + internal FileInfo MetaFile => new(Path.Combine( BasePath.FullName, "meta.json" )); private MetaChangeType LoadMeta() diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index 9497c9c6..996c6437 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -12,6 +12,7 @@ using Penumbra.Util; namespace Penumbra.Mods; +// TODO Everything //ublic class ModCleanup // // private const string Duplicates = "Duplicates"; @@ -521,7 +522,6 @@ namespace Penumbra.Mods; // } // } // -// // TODO // var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); // foreach( var collection in Penumbra.CollectionManager ) // { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index d310250b..eb22b314 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -42,7 +42,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Used on construction and on mod rediscoveries. private void Reload() { - if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) { Save(); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 924e294e..c89b2da9 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -4,10 +4,15 @@ using System.IO; using Dalamud.Logging; using Newtonsoft.Json; using OtterGui.Filesystem; -using Penumbra.Util; namespace Penumbra.Mods; +public enum SelectType +{ + Single, + Multi, +} + public interface IModGroup : IEnumerable< ISubMod > { public string Name { get; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 88350364..26d3abe3 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -11,6 +11,7 @@ namespace Penumbra.Mods; public partial class Mod { + // Groups that allow all available options to be selected at once. private sealed class MultiModGroup : IModGroup { public SelectType Type diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index 8c0b4103..352bb503 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -11,6 +11,7 @@ namespace Penumbra.Mods; public partial class Mod { + // Groups that allow only one of their available options to be selected. private sealed class SingleModGroup : IModGroup { public SelectType Type diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index e9be472d..786ac6de 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -13,9 +13,11 @@ namespace Penumbra.Mods; public partial class Mod { - private string DefaultFile + internal string DefaultFile => Path.Combine( BasePath.FullName, "default_mod.json" ); + // The default mod contains setting-independent sets of file replacements, file swaps and meta changes. + // Every mod has an default mod, though it may be empty. private void SaveDefaultMod() { var defaultFile = DefaultFile; @@ -55,6 +57,14 @@ public partial class Mod } + // A sub mod is a collection of + // - file replacements + // - file swaps + // - meta manipulations + // that can be used either as an option or as the default data for a mod. + // It can be loaded and reloaded from Json. + // Nothing is checked for existence or validity when loading. + // Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. private sealed class SubMod : ISubMod { public string Name { get; set; } = "Default"; @@ -78,6 +88,7 @@ public partial class Mod FileSwapData.Clear(); ManipulationData.Clear(); + // Every option has a name, but priorities are only relevant for multi group options. Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; @@ -115,6 +126,8 @@ public partial class Mod } } + // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. + // If delete is true, the files are deleted afterwards. public void IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) { foreach( var (key, file) in Files.ToList() ) diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 982776f8..3f6b7b53 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -14,6 +14,7 @@ public class ModSettings public int Priority { get; set; } public bool Enabled { get; set; } + // Create an independent copy of the current settings. public ModSettings DeepCopy() => new() { @@ -22,6 +23,7 @@ public class ModSettings Settings = Settings.ToList(), }; + // Create default settings for a given mod. public static ModSettings DefaultSettings( Mod mod ) => new() { @@ -30,24 +32,30 @@ public class ModSettings Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(), }; + // Automatically react to changes in a mods available options. public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { switch( type ) { case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: + // Add new empty setting for new mod. Settings.Insert( groupIdx, 0 ); return true; case ModOptionChangeType.GroupDeleted: + // Remove setting for deleted mod. Settings.RemoveAt( groupIdx ); return true; case ModOptionChangeType.GroupTypeChanged: { + // Fix settings for a changed group type. + // Single -> Multi: set single as enabled, rest as disabled + // Multi -> Single: set the first enabled option or 0. var group = mod.Groups[ groupIdx ]; var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => ( uint )Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), + SelectType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), SelectType.Multi => 1u << ( int )config, _ => config, }; @@ -55,6 +63,8 @@ public class ModSettings } case ModOptionChangeType.OptionDeleted: { + // Single -> select the previous option if any. + // Multi -> excise the corresponding bit. var group = mod.Groups[ groupIdx ]; var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch @@ -65,9 +75,13 @@ public class ModSettings }; return config != Settings[ groupIdx ]; } - case ModOptionChangeType.GroupMoved: return Settings.Move( groupIdx, movedToIdx ); + case ModOptionChangeType.GroupMoved: + // Move the group the same way. + return Settings.Move( groupIdx, movedToIdx ); case ModOptionChangeType.OptionMoved: { + // Single -> select the moved option if it was currently selected + // Multi -> move the corresponding bit var group = mod.Groups[ groupIdx ]; var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch @@ -82,6 +96,7 @@ public class ModSettings } } + // Ensure that a value is valid for a group. private static uint FixSetting( IModGroup group, uint value ) => group.Type switch { @@ -90,6 +105,7 @@ public class ModSettings _ => value, }; + // Set a setting. Ensures that there are enough settings and fixes the setting beforehand. public void SetValue( Mod mod, int groupIdx, uint newValue ) { AddMissingSettings( groupIdx + 1 ); @@ -97,6 +113,7 @@ public class ModSettings Settings[ groupIdx ] = FixSetting( group, newValue ); } + // Remove a single bit, moving all further bits one down. private static uint RemoveBit( uint config, int bit ) { var lowMask = ( 1u << bit ) - 1u; @@ -106,6 +123,7 @@ public class ModSettings return low | high; } + // Move a bit in an uint from its position to another, shifting other bits accordingly. private static uint MoveBit( uint config, int bit1, int bit2 ) { var enabled = ( config & ( 1 << bit1 ) ) != 0 ? 1u << bit2 : 0u; @@ -116,7 +134,8 @@ public class ModSettings return low | enabled | high; } - internal bool AddMissingSettings( int totalCount ) + // Add defaulted settings up to the required count. + private bool AddMissingSettings( int totalCount ) { if( totalCount <= Settings.Count ) { @@ -127,6 +146,7 @@ public class ModSettings return true; } + // A simple struct conversion to easily save settings by name instead of value. public struct SavedSettings { public Dictionary< string, uint > Settings; @@ -154,6 +174,7 @@ public class ModSettings } } + // Convert and fix. public bool ToSettings( Mod mod, out ModSettings settings ) { var list = new List< uint >( mod.Groups.Count ); diff --git a/Penumbra/Mods/Subclasses/SelectType.cs b/Penumbra/Mods/Subclasses/SelectType.cs deleted file mode 100644 index 0843729c..00000000 --- a/Penumbra/Mods/Subclasses/SelectType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Penumbra.Mods; - -public enum SelectType -{ - Single, - Multi, -} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 50ce0d07..8d11133d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -30,9 +30,9 @@ public class Penumbra : IDalamudPlugin private const string CommandName = "/penumbra"; - public static string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; + public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; - public static string CommitHash = + public static readonly string CommitHash = Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; public static Configuration Config { get; private set; } = null!; @@ -295,7 +295,7 @@ public class Penumbra : IDalamudPlugin case "reload": { ModManager.DiscoverMods(); - Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods." + Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Count} mods." ); break; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index de9cf7b7..2b342208 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -21,11 +21,10 @@ public partial class ModFileSystemSelector public uint Color; } - private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; - private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >(); - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; private void SetFilterTooltip() { @@ -105,7 +104,7 @@ public partial class ModFileSystemSelector // Only get the text color for a mod if no filters are set. private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) { - if( _newMods.Contains( mod ) ) + if( Penumbra.ModManager.NewMods.Contains( mod ) ) { return ColorId.NewMod.Value(); } @@ -133,7 +132,7 @@ public partial class ModFileSystemSelector private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) { - var isNew = _newMods.Contains( mod ); + var isNew = Penumbra.ModManager.NewMods.Contains( mod ); // Handle mod details. if( CheckFlags( mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles ) || CheckFlags( mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 4d7a236e..be36ab68 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -24,10 +24,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod > newMods ) + public ModFileSystemSelector( ModFileSystem fileSystem ) : base( fileSystem ) { - _newMods = newMods; SubscribeRightClickFolder( EnableDescendants, 10 ); SubscribeRightClickFolder( DisableDescendants, 10 ); SubscribeRightClickFolder( InheritDescendants, 15 ); @@ -122,7 +121,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void AddNewModButton( Vector2 size ) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", !Penumbra.ModManager.Valid, true ) ) + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", + !Penumbra.ModManager.Valid, true ) ) { ImGui.OpenPopup( "Create New Mod" ); } @@ -167,17 +167,18 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void DrawInfoPopup() { var display = ImGui.GetIO().DisplaySize; - ImGui.SetNextWindowSize( display / 4 ); + ImGui.SetNextWindowSize( display / 4 ); ImGui.SetNextWindowPos( 3 * display / 8 ); using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); if( _import != null && popup.Success ) { - _import.DrawProgressInfo( ImGuiHelpers.ScaledVector2( -1, ImGui.GetFrameHeight() ) ); + _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); if( _import.State == ImporterState.Done ) { ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 ); if( ImGui.Button( "Close", -Vector2.UnitX ) ) { + AddNewMods( _import.ExtractedMods ); _import = null; ImGui.CloseCurrentPopup(); } @@ -185,6 +186,37 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + // Clean up invalid directories if necessary. + // Add all successfully extracted mods. + private static void AddNewMods( IEnumerable< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > list ) + { + foreach( var (file, dir, error) in list ) + { + if( error != null ) + { + if( dir != null && Directory.Exists( dir.FullName ) ) + { + try + { + Directory.Delete( dir.FullName ); + } + catch( Exception e ) + { + PluginLog.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); + } + } + + PluginLog.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); + continue; + } + + if( dir != null ) + { + Penumbra.ModManager.AddMod( dir ); + } + } + } + private void DeleteModButton( Vector2 size ) { var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index c99cb3d4..aa7b6c1e 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -31,6 +31,7 @@ public partial class ConfigWindow return; } + // Draw filters. ImGui.SetNextItemWidth( -1 ); LowerString.InputWithHint( "##changedItemsFilter", "Filter...", ref _changedItemFilter, 64 ); @@ -40,6 +41,7 @@ public partial class ConfigWindow return; } + // Draw table of changed items. var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips( height ); using var list = ImRaii.Table( "##changedItems", 1, ImGuiTableFlags.RowBg, -Vector2.One ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index ac803463..6d4ed0ec 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -112,7 +112,7 @@ public partial class ConfigWindow private void DrawCurrentCollectionInheritance() { using var list = ImRaii.ListBox( "##inheritanceList", - new Vector2( _window._inputTextWidth.X - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X, + new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ) ); if( !list ) { @@ -131,7 +131,7 @@ public partial class ConfigWindow private void DrawInheritanceTrashButton() { ImGui.SameLine(); - var size = new Vector2( ImGui.GetFrameHeight(), ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ); + var size = new Vector2( _window._iconButtonSize.X, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ); var buttonColor = ImGui.GetColorU32( ImGuiCol.Button ); // Prevent hovering from highlighting the button. using var color = ImRaii.PushColor( ImGuiCol.ButtonActive, buttonColor ) @@ -142,7 +142,7 @@ public partial class ConfigWindow using var target = ImRaii.DragDropTarget(); if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) { - _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance ), -1 ); + _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance! ), -1 ); } } @@ -192,7 +192,7 @@ public partial class ConfigWindow ModCollection.ValidInheritance.Circle => "Inheriting from selected collection would lead to cyclic inheritance.", _ => string.Empty, }; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, tt, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, inheritance != ModCollection.ValidInheritance.Valid, true ) && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance! ) ) { @@ -211,7 +211,7 @@ public partial class ConfigWindow // Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. private void DrawNewInheritanceCombo() { - ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X ); _newInheritance ??= Penumbra.CollectionManager.FirstOrDefault( c => c != Penumbra.CollectionManager.Current && !Penumbra.CollectionManager.Current.Inheritance.Contains( c ) ) ?? ModCollection.Empty; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 6d38cb69..76024c0e 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -47,14 +47,19 @@ public partial class ConfigWindow } } + // Only gets drawn when actually relevant. private static void DrawCleanCollectionButton() { - if( ImGui.Button( "Clean Settings" ) ) + if( Penumbra.Config.ShowAdvanced && Penumbra.CollectionManager.Current.HasUnusedSettings ) { - Penumbra.CollectionManager.Current.CleanUnavailableSettings(); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Clean Settings", Vector2.Zero + , "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." + , false ) ) + { + Penumbra.CollectionManager.Current.CleanUnavailableSettings(); + } } - - ImGuiUtil.HoverTooltip( "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); } // Draw the new collection input as well as its buttons. @@ -94,11 +99,7 @@ public partial class ConfigWindow Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); } - if( Penumbra.Config.ShowAdvanced ) - { - ImGui.SameLine(); - DrawCleanCollectionButton(); - } + DrawCleanCollectionButton(); } private void DrawCurrentCollectionSelector() @@ -152,7 +153,7 @@ public partial class ConfigWindow 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(), Vector2.One * ImGui.GetFrameHeight(), string.Empty, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) { diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 3e843231..241a22bb 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -124,7 +124,7 @@ public partial class ConfigWindow // Filters mean we can not use the known counts. if( hasFilters ) { - var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager.Mods[ p.Item2 ].Name ) ); + var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager[ p.Item2 ].Name ) ); if( stop >= 0 ) { ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); @@ -190,7 +190,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager.Mods[ modIdx ].Name ); + ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager[ modIdx ].Name ); } // Check filters for file replacements. diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index b4cf7835..850d4e85 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -21,16 +21,16 @@ public partial class ConfigWindow => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); // Draw text given by a byte pointer. - internal static unsafe void Text( byte* s, int length ) + private static unsafe void Text( byte* s, int length ) => ImGuiNative.igTextUnformatted( s, s + length ); // Draw the name of a resource file. - internal static unsafe void Text( ResourceHandle* resource ) + private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); // Draw a changed item, invoking the Api-Events for clicks and tooltips. // Also draw the item Id in grey - internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) + private void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; @@ -64,7 +64,7 @@ public partial class ConfigWindow // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, // using an Utf8String. - internal static unsafe void CopyOnClickSelectable( Utf8String text ) + private static unsafe void CopyOnClickSelectable( Utf8String text ) { if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 0ca5dcd4..560dae22 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -1,6 +1,7 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -14,7 +15,7 @@ public partial class ConfigWindow { private partial class ModPanel { - public readonly Queue< Action > _delayedActions = new(); + private readonly Queue< Action > _delayedActions = new(); private void DrawAddOptionGroupInput() { @@ -24,7 +25,7 @@ public partial class ConfigWindow var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false ); var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, !nameValid, true ) ) { Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName ); @@ -73,6 +74,33 @@ public partial class ConfigWindow EditDescriptionPopup(); } + private void EditButtons() + { + var folderExists = Directory.Exists( _mod.BasePath.FullName ); + var tt = folderExists + ? $"Open {_mod.BasePath.FullName} in the file explorer of your choice." + : $"Mod directory {_mod.BasePath.FullName} does not exist."; + if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", Vector2.Zero, tt, !folderExists ) ) + { + Process.Start( new ProcessStartInfo( _mod.BasePath.FullName ) { UseShellExecute = true } ); + } + + ImGui.SameLine(); + ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", Vector2.Zero, "Not implemented yet", true ); + ImGui.SameLine(); + ImGuiUtil.DrawDisabledButton( "Reload Mod", Vector2.Zero, "Not implemented yet", true ); + + ImGuiUtil.DrawDisabledButton( "Deduplicate", Vector2.Zero, "Not implemented yet", true ); + ImGui.SameLine(); + ImGuiUtil.DrawDisabledButton( "Normalize", Vector2.Zero, "Not implemented yet", true ); + ImGui.SameLine(); + ImGuiUtil.DrawDisabledButton( "Auto-Create Groups", Vector2.Zero, "Not implemented yet", true ); + + ImGuiUtil.DrawDisabledButton( "Change Material Suffix", Vector2.Zero, "Not implemented yet", true ); + + ImGui.Dummy( _window._defaultSpace ); + } + // Special field indices to reuse the same string buffer. private const int NoFieldIdx = -1; @@ -105,15 +133,37 @@ public partial class ConfigWindow Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); } - if( ImGui.Button( "Edit Description", _window._inputTextWidth ) ) + var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X, 0 ); + + if( ImGui.Button( "Edit Description", reducedSize ) ) { _delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) ); } - if( ImGui.Button( "Edit Default Mod", _window._inputTextWidth ) ) + ImGui.SameLine(); + var fileExists = File.Exists( _mod.MetaFile.FullName ); + var tt = fileExists + ? "Open the metadata json file in the text editor of your choice." + : "The metadata json file does not exist."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) + { + Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); + } + + if( ImGui.Button( "Edit Default Mod", reducedSize ) ) { _window.SubModPopup.Activate( _mod, -1, 0 ); } + + ImGui.SameLine(); + fileExists = File.Exists( _mod.DefaultFile ); + tt = fileExists + ? "Open the default option json file in the text editor of your choice." + : "The default option json file does not exist."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) + { + Process.Start( new ProcessStartInfo( _mod.DefaultFile ) { UseShellExecute = true } ); + } } @@ -144,7 +194,7 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( "Group Name" ); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) { _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) ); @@ -152,7 +202,7 @@ public partial class ConfigWindow ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, "Edit group description.", false, true ) ) { _delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) ); @@ -167,7 +217,7 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( "Group Priority" ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - 2 * ImGui.GetFrameHeight() - 8 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); using( var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ) ) { if( combo ) @@ -185,7 +235,7 @@ public partial class ConfigWindow ImGui.SameLine(); var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), _window._iconButtonSize, tt, groupIdx == 0, true ) ) { _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) ); @@ -195,19 +245,30 @@ public partial class ConfigWindow tt = groupIdx == _mod.Groups.Count - 1 ? "Can not move this group further downwards." : $"Move this group down to group {groupIdx + 2}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), _window._iconButtonSize, tt, groupIdx == _mod.Groups.Count - 1, true ) ) { _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); } + ImGui.SameLine(); + var fileName = group.FileName( _mod.BasePath ); + var fileExists = File.Exists( fileName ); + tt = fileExists + ? $"Open the {group.Name} json file in the text editor of your choice." + : $"The {group.Name} json file does not exist."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) + { + Process.Start( new ProcessStartInfo( fileName ) { UseShellExecute = true } ); + } + ImGui.Dummy( _window._defaultSpace ); using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, _window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); - ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); + ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, _window._iconButtonSize.X ); + ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, _window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); if( table ) { @@ -221,7 +282,7 @@ public partial class ConfigWindow ImGui.SetNextItemWidth( -1 ); ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 ); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, "Add a new option to this group.", _newOptionName.Length == 0, true ) ) { Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName ); @@ -258,6 +319,7 @@ public partial class ConfigWindow } } + // TODO drag options to other groups without options. using( var target = ImRaii.DragDropTarget() ) { if( target.Success && ImGuiUtil.IsDropping( label ) ) @@ -266,14 +328,21 @@ public partial class ConfigWindow { if( _dragDropGroupIdx == groupIdx ) { - // TODO - Dalamud.Chat.Print( - $"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" ); + var sourceOption = _dragDropOptionIdx; + _delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( _mod, groupIdx, sourceOption, optionIdx ) ); } else { - Dalamud.Chat.Print( - $"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" ); + // Move from one group to another by deleting, then adding the option. + var sourceGroup = _dragDropGroupIdx; + var sourceOption = _dragDropOptionIdx; + var option = group[ _dragDropOptionIdx ]; + var priority = group.OptionPriority( _dragDropGroupIdx ); + _delayedActions.Enqueue( () => + { + Penumbra.ModManager.DeleteOption( _mod, sourceGroup, sourceOption ); + Penumbra.ModManager.AddOption( _mod, groupIdx, option, priority ); + } ); } } @@ -299,14 +368,14 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) { _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( _mod, groupIdx, optionIdx ) ); } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, "Edit this option.", false, true ) ) { _window.SubModPopup.Activate( _mod, groupIdx, optionIdx ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 7ab3b496..f4168fb3 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -16,7 +15,7 @@ public partial class ConfigWindow { private partial class ModPanel { - private ModSettings _settings = null!; + private ModSettings _settings = null!; private ModCollection _collection = null!; private bool _emptySetting; private bool _inherited; @@ -92,6 +91,7 @@ public partial class ConfigWindow var enabled = _settings.Enabled; if( ImGui.Checkbox( "Enabled", ref enabled ) ) { + Penumbra.ModManager.NewMods.Remove( _mod ); Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); } } @@ -132,7 +132,7 @@ public partial class ConfigWindow } var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll ); if( ImGui.Button( text ) ) { Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 556be8dc..f8982852 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -1,6 +1,4 @@ using System; -using System.ComponentModel.Design; -using System.Linq; using System.Numerics; using ImGuiNET; using OtterGui; @@ -112,6 +110,7 @@ public partial class ConfigWindow { return; } + var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); Mod? oldBadMod = null; using var indent = ImRaii.PushIndent( 0f ); @@ -124,19 +123,20 @@ public partial class ConfigWindow { indent.Pop( 30f ); } - + if( ImGui.Selectable( badMod.Name ) ) { _window._selector.SelectByValue( badMod ); } - + ImGui.SameLine(); - using var color = ImRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); + using var color = ImRaii.PushColor( ImGuiCol.Text, + conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); - + indent.Push( 30f ); } - + if( conflict.Data is Utf8GamePath p ) { unsafe @@ -148,7 +148,7 @@ public partial class ConfigWindow { ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); } - + oldBadMod = badMod; } } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Details.cs b/Penumbra/UI/ConfigWindow.ModsTab.Details.cs deleted file mode 100644 index 6dfb603f..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.Details.cs +++ /dev/null @@ -1,714 +0,0 @@ -//using System.IO; -//using System.Linq; -//using System.Numerics; -//using Dalamud.Interface; -//using FFXIVClientStructs.FFXIV.Client.UI.Misc; -//using ImGuiNET; -//using Lumina.Data.Parsing; -//using Lumina.Excel.GeneratedSheets; -//using Penumbra.GameData.ByteString; -//using Penumbra.GameData.Enums; -//using Penumbra.GameData.Util; -//using Penumbra.Meta; -//using Penumbra.Meta.Manipulations; -//using Penumbra.Mods; -//using Penumbra.UI.Custom; -//using Penumbra.Util; -//using ImGui = ImGuiNET.ImGui; -// -//namespace Penumbra.UI; -// -//public partial class SettingsInterface -//{ -// private partial class PluginDetails -// { -// private const string LabelPluginDetails = "PenumbraPluginDetails"; -// private const string LabelAboutTab = "About"; -// private const string LabelChangedItemsTab = "Changed Items"; -// private const string LabelChangedItemsHeader = "##changedItems"; -// private const string LabelConflictsTab = "Mod Conflicts"; -// private const string LabelConflictsHeader = "##conflicts"; -// private const string LabelFileSwapTab = "File Swaps"; -// private const string LabelFileSwapHeader = "##fileSwaps"; -// private const string LabelFileListTab = "Files"; -// private const string LabelFileListHeader = "##fileList"; -// private const string LabelGroupSelect = "##groupSelect"; -// private const string LabelOptionSelect = "##optionSelect"; -// private const string LabelConfigurationTab = "Configuration"; -// -// private const string TooltipFilesTab = -// "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" -// + "Yellow files are restricted to some options."; -// -// private const float OptionSelectionWidth = 140f; -// private const float CheckMarkSize = 50f; -// private const uint ColorDarkGreen = 0xFF00A000; -// private const uint ColorGreen = 0xFF00C800; -// private const uint ColorYellow = 0xFF00C8C8; -// private const uint ColorDarkRed = 0xFF0000A0; -// private const uint ColorRed = 0xFF0000C8; -// -// -// private bool _editMode; -// private int _selectedGroupIndex; -// private OptionGroup? _selectedGroup; -// private int _selectedOptionIndex; -// private ConfigModule.Option? _selectedOption; -// private string _currentGamePaths = ""; -// -// private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList; -// -// private readonly Selector _selector; -// private readonly SettingsInterface _base; -// -// private void SelectGroup( int idx ) -// { -// // Not using the properties here because we need it to be not null forgiving in this case. -// var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; -// _selectedGroupIndex = idx; -// if( _selectedGroupIndex >= numGroups ) -// { -// _selectedGroupIndex = 0; -// } -// -// if( numGroups > 0 ) -// { -// _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; -// } -// else -// { -// _selectedGroup = null; -// } -// } -// -// private void SelectGroup() -// => SelectGroup( _selectedGroupIndex ); -// -// private void SelectOption( int idx ) -// { -// _selectedOptionIndex = idx; -// if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) -// { -// _selectedOptionIndex = 0; -// } -// -// if( _selectedGroup?.Options.Count > 0 ) -// { -// _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; -// } -// else -// { -// _selectedOption = null; -// } -// } -// -// private void SelectOption() -// => SelectOption( _selectedOptionIndex ); -// -// public void ResetState() -// { -// _fullFilenameList = null; -// SelectGroup(); -// SelectOption(); -// } -// -// public PluginDetails( SettingsInterface ui, Selector s ) -// { -// _base = ui; -// _selector = s; -// ResetState(); -// } -// -// // This is only drawn when we have a mod selected, so we can forgive nulls. -// private FullMod Mod -// => _selector.Mod!; -// -// private ModMeta Meta -// => Mod.Data.Meta; -// -// private void DrawAboutTab() -// { -// if( !_editMode && Meta.Description.Length == 0 ) -// { -// return; -// } -// -// if( !ImGui.BeginTabItem( LabelAboutTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// var desc = Meta.Description; -// var flags = _editMode -// ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine -// : ImGuiInputTextFlags.ReadOnly; -// -// if( _editMode ) -// { -// if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, -// AutoFillSize, flags ) ) -// { -// Meta.Description = desc; -// _selector.SaveCurrentMod(); -// } -// -// ImGuiCustom.HoverTooltip( TooltipAboutEdit ); -// } -// else -// { -// ImGui.TextWrapped( desc ); -// } -// } -// -// private void DrawChangedItemsTab() -// { -// if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) -// { -// return; -// } -// -// raii.Push( ImGui.EndListBox ); -// foreach( var (name, data) in Mod.Data.ChangedItems ) -// { -// _base.DrawChangedItem( name, data ); -// } -// } -// -// private void DrawConflictTab() -// { -// var conflicts = Penumbra.CollectionManager.Current.ModConflicts( Mod.Data.Index ).ToList(); -// if( conflicts.Count == 0 || !ImGui.BeginTabItem( LabelConflictsTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// ImGui.SetNextItemWidth( -1 ); -// if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) -// { -// return; -// } -// -// raii.Push( ImGui.EndListBox ); -// using var indent = ImGuiRaii.PushIndent( 0 ); -// Mods.Mod? oldBadMod = null; -// foreach( var conflict in conflicts ) -// { -// var badMod = Penumbra.ModManager[ conflict.Mod2 ]; -// if( badMod != oldBadMod ) -// { -// if( oldBadMod != null ) -// { -// indent.Pop( 30f ); -// } -// -// if( ImGui.Selectable( badMod.Meta.Name ) ) -// { -// _selector.SelectModByDir( badMod.BasePath.Name ); -// } -// -// ImGui.SameLine(); -// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorGreen : ColorRed ); -// ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); -// -// indent.Push( 30f ); -// } -// -// if( conflict.Data is Utf8GamePath p ) -// { -// unsafe -// { -// ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); -// } -// } -// else if( conflict.Data is MetaManipulation m ) -// { -// ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); -// } -// -// oldBadMod = badMod; -// } -// } -// -// private void DrawFileSwapTab() -// { -// if( _editMode ) -// { -// DrawFileSwapTabEdit(); -// return; -// } -// -// if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; -// -// ImGui.SetNextItemWidth( -1 ); -// if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) -// { -// return; -// } -// -// raii.Push( ImGui.EndTable ); -// -// foreach( var (source, target) in Meta.FileSwaps ) -// { -// ImGui.TableNextColumn(); -// ImGuiCustom.CopyOnClickSelectable( source.Path ); -// -// ImGui.TableNextColumn(); -// ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); -// -// ImGui.TableNextColumn(); -// ImGuiCustom.CopyOnClickSelectable( target.InternalName ); -// -// ImGui.TableNextRow(); -// } -// } -// -// private void UpdateFilenameList() -// { -// if( _fullFilenameList != null ) -// { -// return; -// } -// -// _fullFilenameList = Mod.Data.Resources.ModFiles -// .Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) -// .ToArray(); -// -// if( Meta.Groups.Count == 0 ) -// { -// return; -// } -// -// for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) -// { -// foreach( var group in Meta.Groups.Values ) -// { -// var inAll = true; -// foreach( var option in group.Options ) -// { -// if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) -// { -// _fullFilenameList[ i ].color = ColorYellow; -// } -// else -// { -// inAll = false; -// } -// } -// -// if( inAll && group.SelectionType == SelectType.Single ) -// { -// _fullFilenameList[ i ].color = ColorGreen; -// } -// } -// } -// } -// -// private void DrawFileListTab() -// { -// if( !ImGui.BeginTabItem( LabelFileListTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// ImGuiCustom.HoverTooltip( TooltipFilesTab ); -// -// ImGui.SetNextItemWidth( -1 ); -// if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) -// { -// raii.Push( ImGui.EndListBox ); -// UpdateFilenameList(); -// using var colorRaii = new ImGuiRaii.Color(); -// foreach( var (name, _, color, _) in _fullFilenameList! ) -// { -// colorRaii.Push( ImGuiCol.Text, color ); -// ImGui.Selectable( name.FullName ); -// colorRaii.Pop(); -// } -// } -// else -// { -// _fullFilenameList = null; -// } -// } -// -// private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders ) -// { -// removeFolders = 0; -// var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) ); -// if( defaultIndex < 0 ) -// { -// return defaultIndex; -// } -// -// var path = gamePaths[ defaultIndex ].Path; -// if( path.Length == TextDefaultGamePath.Length ) -// { -// return defaultIndex; -// } -// -// if( path[ TextDefaultGamePath.Length ] != ( byte )'-' -// || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) ) -// { -// return -1; -// } -// -// return defaultIndex; -// } -// -// private void HandleSelectedFilesButton( bool remove ) -// { -// if( _selectedOption == null ) -// { -// return; -// } -// -// var option = ( ConfigModule.Option )_selectedOption; -// -// var gamePaths = _currentGamePaths.Split( ';' ) -// .Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray(); -// if( gamePaths.Length == 0 ) -// { -// return; -// } -// -// var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); -// var changed = false; -// for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) -// { -// if( !_fullFilenameList![ i ].selected ) -// { -// continue; -// } -// -// _fullFilenameList![ i ].selected = false; -// var relName = _fullFilenameList[ i ].relName; -// if( defaultIndex >= 0 ) -// { -// gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); -// } -// -// if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) -// { -// if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) -// { -// changed = true; -// } -// -// if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) -// { -// changed = true; -// } -// } -// else -// { -// changed = gamePaths -// .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); -// } -// } -// -// if( changed ) -// { -// _fullFilenameList = null; -// _selector.SaveCurrentMod(); -// var idx = Penumbra.ModManager.Mods.IndexOf( Mod.Data ); -// // Since files may have changed, we need to recompute effective files. -// foreach( var collection in Penumbra.CollectionManager -// .Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) -// { -// collection.CalculateEffectiveFileList( false, collection == Penumbra.CollectionManager.Default ); -// } -// -// // If the mod is enabled in the current collection, its conflicts may have changed. -// if( Mod.Settings.Enabled ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// private void DrawAddToGroupButton() -// { -// if( ImGui.Button( ButtonAddToGroup ) ) -// { -// HandleSelectedFilesButton( false ); -// } -// } -// -// private void DrawRemoveFromGroupButton() -// { -// if( ImGui.Button( ButtonRemoveFromGroup ) ) -// { -// HandleSelectedFilesButton( true ); -// } -// } -// -// private void DrawGamePathInput() -// { -// ImGui.SetNextItemWidth( -1 ); -// ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, -// 128 ); -// ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); -// } -// -// private void DrawGroupRow() -// { -// if( _selectedGroup == null ) -// { -// SelectGroup(); -// } -// -// if( _selectedOption == null ) -// { -// SelectOption(); -// } -// -// if( !DrawEditGroupSelector() ) -// { -// return; -// } -// -// ImGui.SameLine(); -// if( !DrawEditOptionSelector() ) -// { -// return; -// } -// -// ImGui.SameLine(); -// DrawAddToGroupButton(); -// ImGui.SameLine(); -// DrawRemoveFromGroupButton(); -// ImGui.SameLine(); -// DrawGamePathInput(); -// } -// -// private void DrawFileAndGamePaths( int idx ) -// { -// void Selectable( uint colorNormal, uint colorReplace ) -// { -// var loc = _fullFilenameList![ idx ].color; -// if( loc == colorNormal ) -// { -// loc = colorReplace; -// } -// -// using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); -// ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); -// } -// -// const float indentWidth = 30f; -// if( _selectedOption == null ) -// { -// Selectable( 0, ColorGreen ); -// return; -// } -// -// var fileName = _fullFilenameList![ idx ].relName; -// var optionFiles = ( ( ConfigModule.Option )_selectedOption ).OptionFiles; -// if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) -// { -// Selectable( 0, ColorGreen ); -// -// using var indent = ImGuiRaii.PushIndent( indentWidth ); -// foreach( var gamePath in gamePaths.ToArray() ) -// { -// var tmp = gamePath.ToString(); -// var old = tmp; -// if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) -// && tmp != old ) -// { -// gamePaths.Remove( gamePath ); -// if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) ) -// { -// gamePaths.Add( p ); -// } -// else if( gamePaths.Count == 0 ) -// { -// optionFiles.Remove( fileName ); -// } -// -// _selector.SaveCurrentMod(); -// _selector.ReloadCurrentMod(); -// } -// } -// } -// else -// { -// Selectable( ColorYellow, ColorRed ); -// } -// } -// -// private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) -// { -// var enabled = ( flag & ( 1 << idx ) ) != 0; -// var oldEnabled = enabled; -// if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) -// { -// Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, -// Mod.Settings.Settings[ group.GroupName ] ^ ( 1 << idx ) ); -// // If the mod is enabled, recalculate files and filters. -// if( Mod.Settings.Enabled ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// private void DrawMultiSelector( OptionGroup group ) -// { -// if( group.Options.Count == 0 ) -// { -// return; -// } -// -// ImGuiCustom.BeginFramedGroup( group.GroupName ); -// using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); -// for( var i = 0; i < group.Options.Count; ++i ) -// { -// DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], -// $"{group.Options[ i ].OptionName}##{group.GroupName}" ); -// } -// } -// -// private void DrawSingleSelector( OptionGroup group ) -// { -// if( group.Options.Count < 2 ) -// { -// return; -// } -// -// var code = Mod.Settings.Settings[ group.GroupName ]; -// if( ImGui.Combo( group.GroupName, ref code -// , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) -// && code != Mod.Settings.Settings[ group.GroupName ] ) -// { -// Penumbra.CollectionManager.Current.SetModSetting( Mod.Data.Index, group.GroupName, code ); -// if( Mod.Settings.Enabled ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// private void DrawGroupSelectors() -// { -// foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) -// { -// DrawSingleSelector( g ); -// } -// -// foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) -// { -// DrawMultiSelector( g ); -// } -// } -// -// private void DrawConfigurationTab() -// { -// if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// if( _editMode ) -// { -// DrawGroupSelectorsEdit(); -// } -// else -// { -// DrawGroupSelectors(); -// } -// } -// -// private void DrawMetaManipulationsTab() -// { -// if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) -// { -// return; -// } -// -// raii.Push( ImGui.EndListBox ); -// -// var manips = Mod.Data.Resources.MetaManipulations; -// var changes = false; -// if( _editMode || manips.DefaultData.Count > 0 ) -// { -// if( ImGui.CollapsingHeader( "Default" ) ) -// { -// changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); -// } -// } -// -// foreach( var (groupName, group) in manips.GroupData ) -// { -// foreach( var (optionName, option) in group ) -// { -// if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) -// { -// changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); -// } -// } -// } -// -// if( changes ) -// { -// Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); -// Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); -// _selector.ReloadCurrentMod( true, false ); -// } -// } -// -// public void Draw( bool editMode ) -// { -// _editMode = editMode; -// if( !ImGui.BeginTabBar( LabelPluginDetails ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); -// DrawAboutTab(); -// DrawChangedItemsTab(); -// -// DrawConfigurationTab(); -// if( _editMode ) -// { -// DrawFileListTabEdit(); -// } -// else -// { -// DrawFileListTab(); -// } -// -// DrawFileSwapTab(); -// DrawMetaManipulationsTab(); -// DrawConflictTab(); -// } -// } -//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs b/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs deleted file mode 100644 index a6024185..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.DetailsEdit.cs +++ /dev/null @@ -1,381 +0,0 @@ -//using System.Collections.Generic; -//using System.Linq; -//using System.Numerics; -//using Dalamud.Interface; -//using FFXIVClientStructs.FFXIV.Client.UI.Misc; -//using ImGuiNET; -//using Penumbra.GameData.ByteString; -//using Penumbra.GameData.Util; -//using Penumbra.Mods; -//using Penumbra.UI.Custom; -//using Penumbra.Util; -// -//namespace Penumbra.UI; -// -//public partial class SettingsInterface -//{ -// private partial class PluginDetails -// { -// private const string LabelDescEdit = "##descedit"; -// private const string LabelNewSingleGroupEdit = "##newSingleGroup"; -// private const string LabelNewMultiGroup = "##newMultiGroup"; -// private const string LabelGamePathsEditBox = "##gamePathsEdit"; -// private const string ButtonAddToGroup = "Add to Group"; -// private const string ButtonRemoveFromGroup = "Remove from Group"; -// private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; -// private const string TextNoOptionAvailable = "[Not Available]"; -// private const string TextDefaultGamePath = "default"; -// private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true ); -// private const char GamePathsSeparator = ';'; -// -// private static readonly string TooltipFilesTabEdit = -// $"{TooltipFilesTab}\n" -// + $"Red Files are replaced in another group or a different option in this group, but not contained in the current option."; -// -// private static readonly string TooltipGamePathsEdit = -// $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n" -// + $"Use '{TextDefaultGamePath}' to add the original file path." -// + $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories."; -// -// private const float MultiEditBoxWidth = 300f; -// -// private bool DrawEditGroupSelector() -// { -// ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); -// if( Meta!.Groups.Count == 0 ) -// { -// ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 ); -// return false; -// } -// -// if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex -// , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() -// , Meta.Groups.Count ) ) -// { -// SelectGroup(); -// SelectOption( 0 ); -// } -// -// return true; -// } -// -// private bool DrawEditOptionSelector() -// { -// ImGui.SameLine(); -// ImGui.SetNextItemWidth( OptionSelectionWidth ); -// if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 ) -// { -// ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); -// return false; -// } -// -// var group = ( OptionGroup )_selectedGroup!; -// if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), -// group.Options.Count ) ) -// { -// SelectOption(); -// } -// -// return true; -// } -// -// private void DrawFileListTabEdit() -// { -// if( ImGui.BeginTabItem( LabelFileListTab ) ) -// { -// UpdateFilenameList(); -// if( ImGui.IsItemHovered() ) -// { -// ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); -// } -// -// ImGui.SetNextItemWidth( -1 ); -// if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) ) -// { -// for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) -// { -// DrawFileAndGamePaths( i ); -// } -// } -// -// ImGui.EndListBox(); -// -// DrawGroupRow(); -// ImGui.EndTabItem(); -// } -// else -// { -// _fullFilenameList = null; -// } -// } -// -// private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group ) -// { -// var groupName = group.GroupName; -// if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) -// { -// if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// -// return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); -// } -// -// private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) -// { -// var newOption = ""; -// ImGui.SetCursorPosX( nameBoxStart ); -// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); -// if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, -// ImGuiInputTextFlags.EnterReturnsTrue ) -// && newOption.Length != 0 ) -// { -// group.Options.Add( new ConfigModule.Option() -// { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } ); -// _selector.SaveCurrentMod(); -// if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// private void DrawMultiSelectorEdit( OptionGroup group ) -// { -// var nameBoxStart = CheckMarkSize; -// var flag = Mod!.Settings.Settings[ group.GroupName ]; -// -// using var raii = DrawMultiSelectorEditBegin( group ); -// for( var i = 0; i < group.Options.Count; ++i ) -// { -// var opt = group.Options[ i ]; -// var label = $"##{group.GroupName}_{i}"; -// DrawMultiSelectorCheckBox( group, i, flag, label ); -// -// ImGui.SameLine(); -// var newName = opt.OptionName; -// -// if( nameBoxStart == CheckMarkSize ) -// { -// nameBoxStart = ImGui.GetCursorPosX(); -// } -// -// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); -// if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// if( newName.Length == 0 ) -// { -// Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); -// } -// else if( newName != opt.OptionName ) -// { -// group.Options[ i ] = new ConfigModule.Option() -// { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; -// _selector.SaveCurrentMod(); -// } -// -// if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// DrawMultiSelectorEditAdd( group, nameBoxStart ); -// } -// -// private void DrawSingleSelectorEditGroup( OptionGroup group ) -// { -// var groupName = group.GroupName; -// if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// } -// -// private float DrawSingleSelectorEdit( OptionGroup group ) -// { -// var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; -// var code = oldSetting; -// if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, -// group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) -// { -// if( code == group.Options.Count ) -// { -// if( newName.Length > 0 ) -// { -// Penumbra.CollectionManager.Current.SetModSetting(Mod.Data.Index, group.GroupName, code); -// group.Options.Add( new ConfigModule.Option() -// { -// OptionName = newName, -// OptionDesc = "", -// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), -// } ); -// _selector.SaveCurrentMod(); -// } -// } -// else -// { -// if( newName.Length == 0 ) -// { -// Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); -// } -// else -// { -// if( newName != group.Options[ code ].OptionName ) -// { -// group.Options[ code ] = new ConfigModule.Option() -// { -// OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, -// OptionFiles = group.Options[ code ].OptionFiles, -// }; -// _selector.SaveCurrentMod(); -// } -// } -// } -// -// if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) -// { -// _selector.Cache.TriggerFilterReset(); -// } -// } -// -// ImGui.SameLine(); -// var labelEditPos = ImGui.GetCursorPosX(); -// DrawSingleSelectorEditGroup( group ); -// -// return labelEditPos; -// } -// -// private void DrawAddSingleGroupField( float labelEditPos ) -// { -// var newGroup = ""; -// ImGui.SetCursorPosX( labelEditPos ); -// if( labelEditPos == CheckMarkSize ) -// { -// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); -// } -// -// if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, -// ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); -// // Adds empty group, so can not change filters. -// } -// } -// -// private void DrawAddMultiGroupField() -// { -// var newGroup = ""; -// ImGui.SetCursorPosX( CheckMarkSize ); -// ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); -// if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, -// ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); -// // Adds empty group, so can not change filters. -// } -// } -// -// private void DrawGroupSelectorsEdit() -// { -// var labelEditPos = CheckMarkSize; -// var groups = Meta.Groups.Values.ToArray(); -// foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) -// { -// labelEditPos = DrawSingleSelectorEdit( g ); -// } -// -// DrawAddSingleGroupField( labelEditPos ); -// -// foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) -// { -// DrawMultiSelectorEdit( g ); -// } -// -// DrawAddMultiGroupField(); -// } -// -// private void DrawFileSwapTabEdit() -// { -// if( !ImGui.BeginTabItem( LabelFileSwapTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// ImGui.SetNextItemWidth( -1 ); -// if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) ) -// { -// return; -// } -// -// raii.Push( ImGui.EndListBox ); -// -// var swaps = Meta.FileSwaps.Keys.ToArray(); -// -// ImGui.PushFont( UiBuilder.IconFont ); -// var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X; -// ImGui.PopFont(); -// -// var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; -// for( var idx = 0; idx < swaps.Length + 1; ++idx ) -// { -// var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ]; -// var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ]; -// var keyString = key.ToString(); -// var valueString = value.ToString(); -// -// ImGui.SetNextItemWidth( width ); -// if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString, -// GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 ) -// { -// if( idx < swaps.Length ) -// { -// Meta.FileSwaps.Remove( key ); -// } -// -// if( !newKey.IsEmpty ) -// { -// Meta.FileSwaps[ newKey ] = value; -// } -// -// _selector.SaveCurrentMod(); -// _selector.ReloadCurrentMod(); -// } -// } -// -// if( idx >= swaps.Length ) -// { -// continue; -// } -// -// ImGui.SameLine(); -// ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); -// ImGui.SameLine(); -// -// ImGui.SetNextItemWidth( width ); -// if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString, -// GamePath.MaxGamePathLength, -// ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// var newValue = new FullPath( valueString.ToLowerInvariant() ); -// if( newValue.CompareTo( value ) != 0 ) -// { -// Meta.FileSwaps[ key ] = newValue; -// _selector.SaveCurrentMod(); -// _selector.Cache.TriggerListReset(); -// } -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs b/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs deleted file mode 100644 index f1b63d5a..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.DetailsManipulation.cs +++ /dev/null @@ -1,773 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.ComponentModel; -//using System.Linq; -//using System.Numerics; -//using Dalamud.Interface; -//using ImGuiNET; -//using Penumbra.GameData.Enums; -//using Penumbra.GameData.Structs; -//using Penumbra.Meta.Files; -//using Penumbra.Meta.Manipulations; -//using Penumbra.UI.Custom; -//using ObjectType = Penumbra.GameData.Enums.ObjectType; -// -//namespace Penumbra.UI; -// -//public partial class SettingsInterface -//{ -// private partial class PluginDetails -// { -// private int _newManipTypeIdx = 0; -// private ushort _newManipSetId = 0; -// private ushort _newManipSecondaryId = 0; -// private int _newManipSubrace = 0; -// private int _newManipRace = 0; -// private int _newManipAttribute = 0; -// private int _newManipEquipSlot = 0; -// private int _newManipObjectType = 0; -// private int _newManipGender = 0; -// private int _newManipBodySlot = 0; -// private ushort _newManipVariant = 0; -// -// -// private static readonly (string, EquipSlot)[] EqpEquipSlots = -// { -// ( "Head", EquipSlot.Head ), -// ( "Body", EquipSlot.Body ), -// ( "Hands", EquipSlot.Hands ), -// ( "Legs", EquipSlot.Legs ), -// ( "Feet", EquipSlot.Feet ), -// }; -// -// private static readonly (string, EquipSlot)[] EqdpEquipSlots = -// { -// EqpEquipSlots[ 0 ], -// EqpEquipSlots[ 1 ], -// EqpEquipSlots[ 2 ], -// EqpEquipSlots[ 3 ], -// EqpEquipSlots[ 4 ], -// ( "Ears", EquipSlot.Ears ), -// ( "Neck", EquipSlot.Neck ), -// ( "Wrist", EquipSlot.Wrists ), -// ( "Left Finger", EquipSlot.LFinger ), -// ( "Right Finger", EquipSlot.RFinger ), -// }; -// -// private static readonly (string, ModelRace)[] Races = -// { -// ( ModelRace.Midlander.ToName(), ModelRace.Midlander ), -// ( ModelRace.Highlander.ToName(), ModelRace.Highlander ), -// ( ModelRace.Elezen.ToName(), ModelRace.Elezen ), -// ( ModelRace.Miqote.ToName(), ModelRace.Miqote ), -// ( ModelRace.Roegadyn.ToName(), ModelRace.Roegadyn ), -// ( ModelRace.Lalafell.ToName(), ModelRace.Lalafell ), -// ( ModelRace.AuRa.ToName(), ModelRace.AuRa ), -// ( ModelRace.Viera.ToName(), ModelRace.Viera ), -// ( ModelRace.Hrothgar.ToName(), ModelRace.Hrothgar ), -// }; -// -// private static readonly (string, Gender)[] Genders = -// { -// ( Gender.Male.ToName(), Gender.Male ), -// ( Gender.Female.ToName(), Gender.Female ), -// ( Gender.MaleNpc.ToName(), Gender.MaleNpc ), -// ( Gender.FemaleNpc.ToName(), Gender.FemaleNpc ), -// }; -// -// private static readonly (string, EstManipulation.EstType)[] EstTypes = -// { -// ( "Hair", EstManipulation.EstType.Hair ), -// ( "Face", EstManipulation.EstType.Face ), -// ( "Body", EstManipulation.EstType.Body ), -// ( "Head", EstManipulation.EstType.Head ), -// }; -// -// private static readonly (string, SubRace)[] Subraces = -// { -// ( SubRace.Midlander.ToName(), SubRace.Midlander ), -// ( SubRace.Highlander.ToName(), SubRace.Highlander ), -// ( SubRace.Wildwood.ToName(), SubRace.Wildwood ), -// ( SubRace.Duskwight.ToName(), SubRace.Duskwight ), -// ( SubRace.SeekerOfTheSun.ToName(), SubRace.SeekerOfTheSun ), -// ( SubRace.KeeperOfTheMoon.ToName(), SubRace.KeeperOfTheMoon ), -// ( SubRace.Seawolf.ToName(), SubRace.Seawolf ), -// ( SubRace.Hellsguard.ToName(), SubRace.Hellsguard ), -// ( SubRace.Plainsfolk.ToName(), SubRace.Plainsfolk ), -// ( SubRace.Dunesfolk.ToName(), SubRace.Dunesfolk ), -// ( SubRace.Raen.ToName(), SubRace.Raen ), -// ( SubRace.Xaela.ToName(), SubRace.Xaela ), -// ( SubRace.Rava.ToName(), SubRace.Rava ), -// ( SubRace.Veena.ToName(), SubRace.Veena ), -// ( SubRace.Helion.ToName(), SubRace.Helion ), -// ( SubRace.Lost.ToName(), SubRace.Lost ), -// }; -// -// private static readonly (string, RspAttribute)[] RspAttributes = -// { -// ( RspAttribute.MaleMinSize.ToFullString(), RspAttribute.MaleMinSize ), -// ( RspAttribute.MaleMaxSize.ToFullString(), RspAttribute.MaleMaxSize ), -// ( RspAttribute.FemaleMinSize.ToFullString(), RspAttribute.FemaleMinSize ), -// ( RspAttribute.FemaleMaxSize.ToFullString(), RspAttribute.FemaleMaxSize ), -// ( RspAttribute.BustMinX.ToFullString(), RspAttribute.BustMinX ), -// ( RspAttribute.BustMaxX.ToFullString(), RspAttribute.BustMaxX ), -// ( RspAttribute.BustMinY.ToFullString(), RspAttribute.BustMinY ), -// ( RspAttribute.BustMaxY.ToFullString(), RspAttribute.BustMaxY ), -// ( RspAttribute.BustMinZ.ToFullString(), RspAttribute.BustMinZ ), -// ( RspAttribute.BustMaxZ.ToFullString(), RspAttribute.BustMaxZ ), -// ( RspAttribute.MaleMinTail.ToFullString(), RspAttribute.MaleMinTail ), -// ( RspAttribute.MaleMaxTail.ToFullString(), RspAttribute.MaleMaxTail ), -// ( RspAttribute.FemaleMinTail.ToFullString(), RspAttribute.FemaleMinTail ), -// ( RspAttribute.FemaleMaxTail.ToFullString(), RspAttribute.FemaleMaxTail ), -// }; -// -// -// private static readonly (string, ObjectType)[] ImcObjectType = -// { -// ( "Equipment", ObjectType.Equipment ), -// ( "Customization", ObjectType.Character ), -// ( "Weapon", ObjectType.Weapon ), -// ( "Demihuman", ObjectType.DemiHuman ), -// ( "Monster", ObjectType.Monster ), -// }; -// -// private static readonly (string, BodySlot)[] ImcBodySlots = -// { -// ( "Hair", BodySlot.Hair ), -// ( "Face", BodySlot.Face ), -// ( "Body", BodySlot.Body ), -// ( "Tail", BodySlot.Tail ), -// ( "Ears", BodySlot.Zear ), -// }; -// -// private static bool PrintCheckBox( string name, ref bool value, bool def ) -// { -// var color = value == def ? 0 : value ? ColorDarkGreen : ColorDarkRed; -// if( color == 0 ) -// { -// return ImGui.Checkbox( name, ref value ); -// } -// -// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color ); -// var ret = ImGui.Checkbox( name, ref value ); -// return ret; -// } -// -// private bool RestrictedInputInt( string name, ref ushort value, ushort min, ushort max ) -// { -// int tmp = value; -// if( ImGui.InputInt( name, ref tmp, 0, 0, _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) -// && tmp != value -// && tmp >= min -// && tmp <= max ) -// { -// value = ( ushort )tmp; -// return true; -// } -// -// return false; -// } -// -// private static bool DefaultButton< T >( string name, ref T value, T defaultValue ) where T : IComparable< T > -// { -// var compare = defaultValue.CompareTo( value ); -// var color = compare < 0 ? ColorDarkGreen : -// compare > 0 ? ColorDarkRed : ImGui.ColorConvertFloat4ToU32( ImGui.GetStyle().Colors[ ( int )ImGuiCol.Button ] ); -// -// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Button, color ); -// var ret = ImGui.Button( name, Vector2.UnitX * 120 ) && compare != 0; -// ImGui.SameLine(); -// return ret; -// } -// -// private bool DrawInputWithDefault( string name, ref ushort value, ushort defaultValue, ushort max ) -// => DefaultButton( $"{( _editMode ? "Set to " : "" )}Default: {defaultValue}##imc{name}", ref value, defaultValue ) -// || RestrictedInputInt( name, ref value, 0, max ); -// -// private static bool CustomCombo< T >( string label, IList< (string, T) > namesAndValues, out T value, ref int idx ) -// { -// value = idx < namesAndValues.Count ? namesAndValues[ idx ].Item2 : default!; -// -// if( !ImGui.BeginCombo( label, idx < namesAndValues.Count ? namesAndValues[ idx ].Item1 : string.Empty ) ) -// { -// return false; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); -// -// for( var i = 0; i < namesAndValues.Count; ++i ) -// { -// if( !ImGui.Selectable( $"{namesAndValues[ i ].Item1}##{label}{i}", idx == i ) || idx == i ) -// { -// continue; -// } -// -// idx = i; -// value = namesAndValues[ i ].Item2; -// return true; -// } -// -// return false; -// } -// -// private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Eqp; -// var val = id.Entry; -// -// -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var defaults = ExpandedEqpFile.GetDefault( id.SetId ); -// var attributes = Eqp.EqpAttributes[ id.Slot ]; -// -// foreach( var flag in attributes ) -// { -// var name = flag.ToLocalName(); -// var tmp = val.HasFlag( flag ); -// if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) ) -// { -// list[ manipIdx ] = new MetaManipulation( new EqpManipulation( tmp ? val | flag : val & ~flag, id.Slot, id.SetId ) ); -// ret = true; -// } -// } -// } -// -// ImGui.Text( ObjectType.Equipment.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.SetId.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Slot.ToString() ); -// return ret; -// } -// -// private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Gmp; -// var val = id.Entry; -// -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var defaults = ExpandedGmpFile.GetDefault( id.SetId ); -// var enabled = val.Enabled; -// var animated = val.Animated; -// var rotationA = val.RotationA; -// var rotationB = val.RotationB; -// var rotationC = val.RotationC; -// ushort unk = val.UnknownTotal; -// -// ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled; -// ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated ); -// ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF ); -// ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF ); -// ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF ); -// ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF ); -// -// if( ret && _editMode ) -// { -// list[ manipIdx ] = new MetaManipulation( new GmpManipulation( new GmpEntry -// { -// Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk, -// RotationA = rotationA, RotationB = rotationB, RotationC = rotationC, -// }, id.SetId ) ); -// } -// } -// -// ImGui.Text( ObjectType.Equipment.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.SetId.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( EquipSlot.Head.ToString() ); -// return ret; -// } -// -// private static (bool, bool) GetEqdpBits( EquipSlot slot, EqdpEntry entry ) -// { -// return slot switch -// { -// EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), -// EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), -// EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), -// EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), -// EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), -// EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), -// EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), -// EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), -// EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), -// EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), -// _ => ( false, false ), -// }; -// } -// -// private static EqdpEntry SetEqdpBits( EquipSlot slot, EqdpEntry value, bool bit1, bool bit2 ) -// { -// switch( slot ) -// { -// case EquipSlot.Head: -// value = bit1 ? value | EqdpEntry.Head1 : value & ~EqdpEntry.Head1; -// value = bit2 ? value | EqdpEntry.Head2 : value & ~EqdpEntry.Head2; -// return value; -// case EquipSlot.Body: -// value = bit1 ? value | EqdpEntry.Body1 : value & ~EqdpEntry.Body1; -// value = bit2 ? value | EqdpEntry.Body2 : value & ~EqdpEntry.Body2; -// return value; -// case EquipSlot.Hands: -// value = bit1 ? value | EqdpEntry.Hands1 : value & ~EqdpEntry.Hands1; -// value = bit2 ? value | EqdpEntry.Hands2 : value & ~EqdpEntry.Hands2; -// return value; -// case EquipSlot.Legs: -// value = bit1 ? value | EqdpEntry.Legs1 : value & ~EqdpEntry.Legs1; -// value = bit2 ? value | EqdpEntry.Legs2 : value & ~EqdpEntry.Legs2; -// return value; -// case EquipSlot.Feet: -// value = bit1 ? value | EqdpEntry.Feet1 : value & ~EqdpEntry.Feet1; -// value = bit2 ? value | EqdpEntry.Feet2 : value & ~EqdpEntry.Feet2; -// return value; -// case EquipSlot.Neck: -// value = bit1 ? value | EqdpEntry.Neck1 : value & ~EqdpEntry.Neck1; -// value = bit2 ? value | EqdpEntry.Neck2 : value & ~EqdpEntry.Neck2; -// return value; -// case EquipSlot.Ears: -// value = bit1 ? value | EqdpEntry.Ears1 : value & ~EqdpEntry.Ears1; -// value = bit2 ? value | EqdpEntry.Ears2 : value & ~EqdpEntry.Ears2; -// return value; -// case EquipSlot.Wrists: -// value = bit1 ? value | EqdpEntry.Wrists1 : value & ~EqdpEntry.Wrists1; -// value = bit2 ? value | EqdpEntry.Wrists2 : value & ~EqdpEntry.Wrists2; -// return value; -// case EquipSlot.RFinger: -// value = bit1 ? value | EqdpEntry.RingR1 : value & ~EqdpEntry.RingR1; -// value = bit2 ? value | EqdpEntry.RingR2 : value & ~EqdpEntry.RingR2; -// return value; -// case EquipSlot.LFinger: -// value = bit1 ? value | EqdpEntry.RingL1 : value & ~EqdpEntry.RingL1; -// value = bit2 ? value | EqdpEntry.RingL2 : value & ~EqdpEntry.RingL2; -// return value; -// } -// -// return value; -// } -// -// private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Eqdp; -// var val = id.Entry; -// -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var defaults = ExpandedEqdpFile.GetDefault( id.FileIndex(), id.SetId ); -// var (bit1, bit2) = GetEqdpBits( id.Slot, val ); -// var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults ); -// -// ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 ); -// ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 ); -// -// if( ret && _editMode ) -// { -// list[ manipIdx ] = new MetaManipulation( new EqdpManipulation( SetEqdpBits( id.Slot, val, bit1, bit2 ), id.Slot, id.Gender, -// id.Race, id.SetId ) ); -// } -// } -// -// ImGui.Text( id.Slot.IsAccessory() -// ? ObjectType.Accessory.ToString() -// : ObjectType.Equipment.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.SetId.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Slot.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Race.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Gender.ToString() ); -// return ret; -// } -// -// private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Est; -// var val = id.Entry; -// -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var defaults = EstFile.GetDefault( id.Slot, Names.CombinedRace( id.Gender, id.Race ), id.SetId ); -// if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode ) -// { -// list[ manipIdx ] = new MetaManipulation( new EstManipulation( id.Gender, id.Race, id.Slot, id.SetId, val ) ); -// ret = true; -// } -// } -// -// ImGui.Text( id.Slot.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.SetId.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Race.ToName() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Gender.ToName() ); -// -// return ret; -// } -// -// private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Imc; -// var val = id.Entry; -// -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var defaults = new ImcFile( id.GamePath() ).GetEntry( ImcFile.PartIndex( id.EquipSlot ), id.Variant ); -// ushort materialId = val.MaterialId; -// ushort vfxId = val.VfxId; -// ushort decalId = val.DecalId; -// var soundId = ( ushort )val.SoundId; -// var attributeMask = val.AttributeMask; -// var materialAnimationId = ( ushort )val.MaterialAnimationId; -// ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue ); -// ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue ); -// ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue ); -// ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F ); -// ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF ); -// ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId, -// byte.MaxValue ); -// -// if( ret && _editMode ) -// { -// var value = new ImcEntry( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId, ( byte )vfxId, -// ( byte )materialAnimationId ); -// list[ manipIdx ] = new MetaManipulation( new ImcManipulation( id, value ) ); -// } -// } -// -// ImGui.Text( id.ObjectType.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.PrimaryId.ToString() ); -// ImGui.TableNextColumn(); -// if( id.ObjectType is ObjectType.Accessory or ObjectType.Equipment ) -// { -// ImGui.Text( id.ObjectType is ObjectType.Equipment or ObjectType.Accessory -// ? id.EquipSlot.ToString() -// : id.BodySlot.ToString() ); -// } -// -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// if( id.ObjectType != ObjectType.Equipment -// && id.ObjectType != ObjectType.Accessory ) -// { -// ImGui.Text( id.SecondaryId.ToString() ); -// } -// -// ImGui.TableNextColumn(); -// ImGui.Text( id.Variant.ToString() ); -// return ret; -// } -// -// private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) -// { -// var ret = false; -// var id = list[ manipIdx ].Rsp; -// var defaults = CmpFile.GetDefault( id.SubRace, id.Attribute ); -// var val = id.Entry; -// if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// if( DefaultButton( -// $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults ) -// && _editMode ) -// { -// list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); -// ret = true; -// } -// -// ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); -// if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f", -// _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly ) -// && val >= 0 -// && val <= 5 -// && _editMode ) -// { -// list[ manipIdx ] = new MetaManipulation( new RspManipulation( id.SubRace, id.Attribute, val ) ); -// ret = true; -// } -// } -// -// ImGui.Text( id.Attribute.ToUngenderedString() ); -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// ImGui.Text( id.SubRace.ToString() ); -// ImGui.TableNextColumn(); -// ImGui.Text( id.Attribute.ToGender().ToString() ); -// return ret; -// } -// -// private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count ) -// { -// var type = list[ manipIdx ].ManipulationType; -// -// if( _editMode ) -// { -// ImGui.TableNextColumn(); -// using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); -// if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##manipDelete{manipIdx}" ) ) -// { -// list.RemoveAt( manipIdx ); -// ImGui.TableNextRow(); -// --manipIdx; -// --count; -// return true; -// } -// } -// -// ImGui.TableNextColumn(); -// ImGui.Text( type.ToString() ); -// ImGui.TableNextColumn(); -// -// var changes = false; -// switch( type ) -// { -// case MetaManipulation.Type.Eqp: -// changes = DrawEqpRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// if( ImGui.Selectable( $"{list[ manipIdx ].Eqp.Entry}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// case MetaManipulation.Type.Gmp: -// changes = DrawGmpRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// if( ImGui.Selectable( $"{list[ manipIdx ].Gmp.Entry.Value}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// case MetaManipulation.Type.Eqdp: -// changes = DrawEqdpRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// var (bit1, bit2) = GetEqdpBits( list[ manipIdx ].Eqdp.Slot, list[ manipIdx ].Eqdp.Entry ); -// if( ImGui.Selectable( $"{bit1} {bit2}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// case MetaManipulation.Type.Est: -// changes = DrawEstRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// if( ImGui.Selectable( $"{list[ manipIdx ].Est.Entry}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// case MetaManipulation.Type.Imc: -// changes = DrawImcRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// if( ImGui.Selectable( $"{list[ manipIdx ].Imc.Entry.MaterialId}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// case MetaManipulation.Type.Rsp: -// changes = DrawRspRow( manipIdx, list ); -// ImGui.TableSetColumnIndex( 9 ); -// if( ImGui.Selectable( $"{list[ manipIdx ].Rsp.Entry}##{manipIdx}" ) ) -// { -// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); -// } -// -// break; -// } -// -// -// ImGui.TableNextRow(); -// return changes; -// } -// -// -// private MetaManipulation.Type DrawNewTypeSelection() -// { -// ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 ); -// ImGui.SameLine(); -// ImGui.RadioButton( "EQDP##newManipType", ref _newManipTypeIdx, 2 ); -// ImGui.SameLine(); -// ImGui.RadioButton( "EQP##newManipType", ref _newManipTypeIdx, 3 ); -// ImGui.SameLine(); -// ImGui.RadioButton( "EST##newManipType", ref _newManipTypeIdx, 4 ); -// ImGui.SameLine(); -// ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 ); -// ImGui.SameLine(); -// ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 ); -// return ( MetaManipulation.Type )_newManipTypeIdx; -// } -// -// private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count ) -// { -// var change = false; -// if( !ImGui.BeginPopup( popupName ) ) -// { -// return change; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// var manipType = DrawNewTypeSelection(); -// MetaManipulation? newManip = null; -// switch( manipType ) -// { -// case MetaManipulation.Type.Imc: -// { -// RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue ); -// RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue ); -// CustomCombo( "Object Type", ImcObjectType, out var objectType, ref _newManipObjectType ); -// ImcManipulation imc = new(); -// switch( objectType ) -// { -// case ObjectType.Equipment: -// CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); -// imc = new ImcManipulation( equipSlot, _newManipVariant, _newManipSetId, new ImcEntry() ); -// break; -// case ObjectType.DemiHuman: -// case ObjectType.Weapon: -// case ObjectType.Monster: -// RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue ); -// CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot ); -// imc = new ImcManipulation( objectType, bodySlot, _newManipSetId, _newManipSecondaryId, -// _newManipVariant, new ImcEntry() ); -// break; -// } -// -// newManip = new MetaManipulation( new ImcManipulation( imc.ObjectType, imc.BodySlot, imc.PrimaryId, imc.SecondaryId, -// imc.Variant, imc.EquipSlot, ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ) ) ); -// -// break; -// } -// case MetaManipulation.Type.Eqdp: -// { -// RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue ); -// CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); -// CustomCombo( "Race", Races, out var race, ref _newManipRace ); -// CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); -// var eqdp = new EqdpManipulation( new EqdpEntry(), equipSlot, gender, race, _newManipSetId ); -// newManip = new MetaManipulation( new EqdpManipulation( ExpandedEqdpFile.GetDefault( eqdp.FileIndex(), eqdp.SetId ), -// equipSlot, gender, race, _newManipSetId ) ); -// break; -// } -// case MetaManipulation.Type.Eqp: -// { -// RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue ); -// CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot ); -// newManip = new MetaManipulation( new EqpManipulation( ExpandedEqpFile.GetDefault( _newManipSetId ) & Eqp.Mask( equipSlot ), -// equipSlot, _newManipSetId ) ); -// break; -// } -// case MetaManipulation.Type.Est: -// { -// RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue ); -// CustomCombo( "Est Type", EstTypes, out var estType, ref _newManipObjectType ); -// CustomCombo( "Race", Races, out var race, ref _newManipRace ); -// CustomCombo( "Gender", Genders, out var gender, ref _newManipGender ); -// newManip = new MetaManipulation( new EstManipulation( gender, race, estType, _newManipSetId, -// EstFile.GetDefault( estType, Names.CombinedRace( gender, race ), _newManipSetId ) ) ); -// break; -// } -// case MetaManipulation.Type.Gmp: -// RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue ); -// newManip = new MetaManipulation( new GmpManipulation( ExpandedGmpFile.GetDefault( _newManipSetId ), _newManipSetId ) ); -// break; -// case MetaManipulation.Type.Rsp: -// CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace ); -// CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute ); -// newManip = new MetaManipulation( new RspManipulation( subRace, rspAttribute, -// CmpFile.GetDefault( subRace, rspAttribute ) ) ); -// break; -// } -// -// if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 ) -// && newManip != null -// && list.All( m => !m.Equals( newManip ) ) ) -// { -// list.Add( newManip.Value ); -// change = true; -// ++count; -// ImGui.CloseCurrentPopup(); -// } -// -// return change; -// } -// -// private bool DrawMetaManipulationsTable( string label, IList< MetaManipulation > list, ref int count ) -// { -// var numRows = _editMode ? 11 : 10; -// var changes = false; -// -// -// if( list.Count > 0 -// && ImGui.BeginTable( label, numRows, -// ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); -// if( _editMode ) -// { -// ImGui.TableNextColumn(); -// } -// -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Type##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Object Type##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Set##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Slot##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Race##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Gender##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Secondary ID##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Variant##{label}" ); -// ImGui.TableNextColumn(); -// ImGui.TableNextColumn(); -// ImGui.TableHeader( $"Value##{label}" ); -// ImGui.TableNextRow(); -// -// for( var i = 0; i < list.Count; ++i ) -// { -// changes |= DrawManipulationRow( ref i, list, ref count ); -// } -// } -// -// var popupName = $"##newManip{label}"; -// if( _editMode ) -// { -// changes |= DrawNewManipulationPopup( $"##newManip{label}", list, ref count ); -// if( ImGui.Button( $"Add New Manipulation##{label}", Vector2.UnitX * -1 ) ) -// { -// ImGui.OpenPopup( popupName ); -// } -// -// return changes; -// } -// -// return false; -// } -// } -//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Import.cs b/Penumbra/UI/ConfigWindow.ModsTab.Import.cs deleted file mode 100644 index 917408fe..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.Import.cs +++ /dev/null @@ -1,177 +0,0 @@ -namespace Penumbra.UI; - -//public partial class ConfigWindow -//{ -// private class TabImport -// { -// private const string LabelTab = "Import Mods"; -// private const string LabelImportButton = "Import TexTools Modpacks"; -// private const string LabelFileDialog = "Pick one or more modpacks."; -// private const string LabelFileImportRunning = "Import in progress..."; -// private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; -// private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; -// -// private const uint ColorRed = 0xFF0000C8; -// private const uint ColorYellow = 0xFF00C8C8; -// -// private static readonly Vector2 ImportBarSize = new(-1, 0); -// -// private bool _isImportRunning; -// private string _errorMessage = string.Empty; -// private TexToolsImport? _texToolsImport; -// private readonly SettingsInterface _base; -// -// public readonly HashSet< string > NewMods = new(); -// -// public TabImport( SettingsInterface ui ) -// => _base = ui; -// -// public bool IsImporting() -// => _isImportRunning; -// -// private void RunImportTask() -// { -// _isImportRunning = true; -// Task.Run( async () => -// { -// try -// { -// var picker = new OpenFileDialog -// { -// Multiselect = true, -// Filter = FileTypeFilter, -// CheckFileExists = true, -// Title = LabelFileDialog, -// }; -// -// var result = await picker.ShowDialogAsync(); -// -// if( result == DialogResult.OK ) -// { -// _errorMessage = string.Empty; -// -// foreach( var fileName in picker.FileNames ) -// { -// PluginLog.Information( $"-> {fileName} START" ); -// -// try -// { -// _texToolsImport = new TexToolsImport( Penumbra.ModManager.BasePath ); -// var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); -// if( dir.Name.Any() ) -// { -// NewMods.Add( dir.Name ); -// } -// -// PluginLog.Information( $"-> {fileName} OK!" ); -// } -// catch( Exception ex ) -// { -// PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); -// _errorMessage = ex.Message; -// } -// } -// -// var directory = _texToolsImport?.ExtractedDirectory; -// _texToolsImport = null; -// _base.ReloadMods(); -// if( directory != null ) -// { -// _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); -// } -// } -// } -// catch( Exception e ) -// { -// PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); -// } -// -// _isImportRunning = false; -// } ); -// } -// -// private void DrawImportButton() -// { -// if( !Penumbra.ModManager.Valid ) -// { -// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); -// ImGui.Button( LabelImportButton ); -// style.Pop(); -// -// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); -// ImGui.Text( "Can not import since the mod directory path is not valid." ); -// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); -// color.Pop(); -// -// ImGui.Text( "Please set the mod directory in the settings tab." ); -// ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); -// color.Push( ImGuiCol.Text, ColorYellow ); -// ImGui.Text( " D:\\ffxivmods" ); -// color.Pop(); -// ImGui.Text( "You can return to this tab once you've done that." ); -// } -// else if( ImGui.Button( LabelImportButton ) ) -// { -// RunImportTask(); -// } -// } -// -// private void DrawImportProgress() -// { -// ImGui.Button( LabelFileImportRunning ); -// -// if( _texToolsImport == null ) -// { -// return; -// } -// -// switch( _texToolsImport.State ) -// { -// case ImporterState.None: break; -// case ImporterState.WritingPackToDisk: -// ImGui.Text( TooltipModpack1 ); -// break; -// case ImporterState.ExtractingModFiles: -// { -// var str = -// $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; -// -// ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); -// break; -// } -// case ImporterState.Done: break; -// default: throw new ArgumentOutOfRangeException(); -// } -// } -// -// private void DrawFailedImportMessage() -// { -// using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); -// ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); -// } -// -// public void Draw() -// { -// if( !ImGui.BeginTabItem( LabelTab ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); -// -// if( !_isImportRunning ) -// { -// DrawImportButton(); -// } -// else -// { -// DrawImportProgress(); -// } -// -// if( _errorMessage.Any() ) -// { -// DrawFailedImportMessage(); -// } -// } -// } -//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs b/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs deleted file mode 100644 index 63434602..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.Panel.cs +++ /dev/null @@ -1,613 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Logging; -using ImGuiNET; -using Penumbra.Mods; -using Penumbra.Util; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - //private class ModPanel - //{ - // private const string LabelModPanel = "selectedModInfo"; - // private const string LabelEditName = "##editName"; - // private const string LabelEditVersion = "##editVersion"; - // private const string LabelEditAuthor = "##editAuthor"; - // private const string LabelEditWebsite = "##editWebsite"; - // private const string LabelModEnabled = "Enabled"; - // private const string LabelEditingEnabled = "Enable Editing"; - // private const string LabelOverWriteDir = "OverwriteDir"; - // private const string ButtonOpenWebsite = "Open Website"; - // private const string ButtonOpenModFolder = "Open Mod Folder"; - // private const string ButtonRenameModFolder = "Rename Mod Folder"; - // private const string ButtonEditJson = "Edit JSON"; - // private const string ButtonReloadJson = "Reload JSON"; - // private const string ButtonDeduplicate = "Deduplicate"; - // private const string ButtonNormalize = "Normalize"; - // private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; - // private const string TooltipRenameModFolder = "Rename the directory containing this mod without opening another application."; - // private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; - // private const string TooltipReloadJson = "Reload the configuration of all mods."; - // private const string PopupRenameFolder = "Rename Folder"; - // - // private const string TooltipDeduplicate = - // "Try to find identical files and remove duplicate occurences to reduce the mods disk size.\n" - // + "Introduces an invisible single-option Group \"Duplicates\".\nExperimental - use at own risk!"; - // - // private const string TooltipNormalize = - // "Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!"; - // - // private const float HeaderLineDistance = 10f; - // private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f); - // - // private readonly SettingsInterface _base; - // private readonly Selector _selector; - // private readonly HashSet< string > _newMods; - // public readonly PluginDetails Details; - // - // private bool _editMode; - // private string _currentWebsite; - // private bool _validWebsite; - // - // private string _fromMaterial = string.Empty; - // private string _toMaterial = string.Empty; - // - // public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods ) - // { - // _base = ui; - // _selector = s; - // _newMods = newMods; - // Details = new PluginDetails( _base, _selector ); - // _currentWebsite = Meta?.Website ?? ""; - // } - // - // private Mods.FullMod? Mod - // => _selector.Mod; - // - // private ModMeta? Meta - // => Mod?.Data.Meta; - // - // private void DrawName() - // { - // var name = Meta!.Name.Text; - // var modManager = Penumbra.ModManager; - // if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) - // { - // _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - // if( !modManager.TemporaryModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) - // { - // Mod.Data.Rename( name ); - // } - // } - // } - // - // private void DrawVersion() - // { - // if( _editMode ) - // { - // ImGui.BeginGroup(); - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - // ImGui.Text( "(Version " ); - // - // using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); - // ImGui.SameLine(); - // var version = Meta!.Version; - // if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) - // && version != Meta.Version ) - // { - // Meta.Version = version; - // _selector.SaveCurrentMod(); - // } - // - // ImGui.SameLine(); - // ImGui.Text( ")" ); - // } - // else if( Meta!.Version.Length > 0 ) - // { - // ImGui.Text( $"(Version {Meta.Version})" ); - // } - // } - // - // private void DrawAuthor() - // { - // ImGui.BeginGroup(); - // ImGui.TextColored( GreyColor, "by" ); - // - // ImGui.SameLine(); - // var author = Meta!.Author.Text; - // if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) - // && author != Meta.Author ) - // { - // Meta.Author = author; - // _selector.SaveCurrentMod(); - // _selector.Cache.TriggerFilterReset(); - // } - // - // ImGui.EndGroup(); - // } - // - // private void DrawWebsite() - // { - // ImGui.BeginGroup(); - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ); - // if( _editMode ) - // { - // ImGui.TextColored( GreyColor, "from" ); - // ImGui.SameLine(); - // var website = Meta!.Website; - // if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) - // && website != Meta.Website ) - // { - // Meta.Website = website; - // _selector.SaveCurrentMod(); - // } - // } - // else if( Meta!.Website.Length > 0 ) - // { - // if( _currentWebsite != Meta.Website ) - // { - // _currentWebsite = Meta.Website; - // _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) - // && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - // } - // - // if( _validWebsite ) - // { - // if( ImGui.SmallButton( ButtonOpenWebsite ) ) - // { - // try - // { - // var process = new ProcessStartInfo( Meta.Website ) - // { - // UseShellExecute = true, - // }; - // Process.Start( process ); - // } - // catch( System.ComponentModel.Win32Exception ) - // { - // // Do nothing. - // } - // } - // - // ImGuiCustom.HoverTooltip( Meta.Website ); - // } - // else - // { - // ImGui.TextColored( GreyColor, "from" ); - // ImGui.SameLine(); - // ImGui.Text( Meta.Website ); - // } - // } - // } - // - // private void DrawHeaderLine() - // { - // DrawName(); - // ImGui.SameLine(); - // DrawVersion(); - // ImGui.SameLine(); - // DrawAuthor(); - // ImGui.SameLine(); - // DrawWebsite(); - // } - // - // private void DrawPriority() - // { - // var priority = Mod!.Settings.Priority; - // ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - // if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) - // { - // Penumbra.CollectionManager.Current.SetModPriority( Mod.Data.Index, priority ); - // _selector.Cache.TriggerFilterReset(); - // } - // - // ImGuiCustom.HoverTooltip( - // "Higher priority mods take precedence over other mods in the case of file conflicts.\n" - // + "In case of identical priority, the alphabetically first mod takes precedence." ); - // } - // - // private void DrawEnabledMark() - // { - // var enabled = Mod!.Settings.Enabled; - // if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) - // { - // Penumbra.CollectionManager.Current.SetModState( Mod.Data.Index, enabled ); - // if( enabled ) - // { - // _newMods.Remove( Mod.Data.BasePath.Name ); - // } - // _selector.Cache.TriggerFilterReset(); - // } - // } - // - // public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector ) - // { - // var currentSortOrder = mod.Order.FullPath; - // ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); - // if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) - // { - // manager.ChangeSortOrder( mod, currentSortOrder ); - // selector.SelectModOnUpdate( mod.BasePath.Name ); - // return true; - // } - // - // return false; - // } - // - // private void DrawEditableMark() - // { - // ImGui.Checkbox( LabelEditingEnabled, ref _editMode ); - // } - // - // private void DrawOpenModFolderButton() - // { - // Mod!.Data.BasePath.Refresh(); - // if( ImGui.Button( ButtonOpenModFolder ) && Mod.Data.BasePath.Exists ) - // { - // Process.Start( new ProcessStartInfo( Mod!.Data.BasePath.FullName ) { UseShellExecute = true } ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipOpenModFolder ); - // } - // - // private string _newName = ""; - // private bool _keyboardFocus = true; - // - // private void RenameModFolder( string newName ) - // { - // _newName = newName.ReplaceBadXivSymbols(); - // if( _newName.Length == 0 ) - // { - // PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); - // ImGui.CloseCurrentPopup(); - // } - // else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) - // { - // var dir = Mod!.Data.BasePath; - // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - // - // if( newDir.Exists ) - // { - // ImGui.OpenPopup( LabelOverWriteDir ); - // } - // else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) - // { - // _selector.ReloadCurrentMod(); - // ImGui.CloseCurrentPopup(); - // } - // } - // else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCulture ) ) - // { - // var dir = Mod!.Data.BasePath; - // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - // var sourceUri = new Uri( dir.FullName ); - // var targetUri = new Uri( newDir.FullName ); - // if( sourceUri.Equals( targetUri ) ) - // { - // var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); - // if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) ) - // { - // if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) - // { - // PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); - // Penumbra.ModManager.RenameModFolder( Mod.Data, dir ); - // } - // - // _selector.ReloadCurrentMod(); - // } - // - // ImGui.CloseCurrentPopup(); - // } - // else - // { - // ImGui.OpenPopup( LabelOverWriteDir ); - // } - // } - // } - // - // private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) - // { - // try - // { - // foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - // { - // var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); - // if( targetFile.Exists ) - // { - // targetFile.Delete(); - // } - // - // targetFile.Directory?.Create(); - // file.MoveTo( targetFile.FullName ); - // } - // - // source.Delete( true ); - // return true; - // } - // catch( Exception e ) - // { - // PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); - // } - // - // return false; - // } - // - // private bool OverwriteDirPopup() - // { - // var closeParent = false; - // var _ = true; - // if( !ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - // { - // return closeParent; - // } - // - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // - // var dir = Mod!.Data.BasePath; - // DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName )); - // ImGui.Text( - // $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); - // var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - // if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) ) - // { - // Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false ); - // - // _selector.SelectModOnUpdate( _newName ); - // - // closeParent = true; - // ImGui.CloseCurrentPopup(); - // } - // - // ImGui.SameLine(); - // - // if( ImGui.Button( "Cancel", buttonSize ) ) - // { - // _keyboardFocus = true; - // ImGui.CloseCurrentPopup(); - // } - // - // return closeParent; - // } - // - // private void DrawRenameModFolderPopup() - // { - // var _ = true; - // _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); - // - // ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, new Vector2( 0.5f, 1f ) ); - // if( !ImGui.BeginPopupModal( PopupRenameFolder, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - // { - // return; - // } - // - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - // - // if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - // { - // ImGui.CloseCurrentPopup(); - // } - // - // var newName = Mod!.Data.BasePath.Name; - // - // if( _keyboardFocus ) - // { - // ImGui.SetKeyboardFocusHere(); - // _keyboardFocus = false; - // } - // - // if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - // { - // RenameModFolder( newName ); - // } - // - // ImGui.TextColored( GreyColor, - // "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); - // - // ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - // - // - // if( OverwriteDirPopup() ) - // { - // ImGui.CloseCurrentPopup(); - // } - // } - // - // private void DrawRenameModFolderButton() - // { - // DrawRenameModFolderPopup(); - // if( ImGui.Button( ButtonRenameModFolder ) ) - // { - // ImGui.OpenPopup( PopupRenameFolder ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipRenameModFolder ); - // } - // - // private void DrawEditJsonButton() - // { - // if( ImGui.Button( ButtonEditJson ) ) - // { - // _selector.SaveCurrentMod(); - // Process.Start( new ProcessStartInfo( Mod!.Data.MetaFile.FullName ) { UseShellExecute = true } ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipEditJson ); - // } - // - // private void DrawReloadJsonButton() - // { - // if( ImGui.Button( ButtonReloadJson ) ) - // { - // _selector.ReloadCurrentMod( true, false ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipReloadJson ); - // } - // - // private void DrawResetMetaButton() - // { - // if( ImGui.Button( "Recompute Metadata" ) ) - // { - // _selector.ReloadCurrentMod( true, true, true ); - // } - // - // ImGuiCustom.HoverTooltip( - // "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" - // + "Also reloads the mod.\n" - // + "Be aware that this removes all manually added metadata changes." ); - // } - // - // private void DrawDeduplicateButton() - // { - // if( ImGui.Button( ButtonDeduplicate ) ) - // { - // ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); - // _selector.SaveCurrentMod(); - // _selector.ReloadCurrentMod( true, true, true ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipDeduplicate ); - // } - // - // private void DrawNormalizeButton() - // { - // if( ImGui.Button( ButtonNormalize ) ) - // { - // ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); - // _selector.SaveCurrentMod(); - // _selector.ReloadCurrentMod( true, true, true ); - // } - // - // ImGuiCustom.HoverTooltip( TooltipNormalize ); - // } - // - // private void DrawAutoGenerateGroupsButton() - // { - // if( ImGui.Button( "Auto-Generate Groups" ) ) - // { - // ModCleanup.AutoGenerateGroups( Mod!.Data.BasePath, Meta! ); - // _selector.SaveCurrentMod(); - // _selector.ReloadCurrentMod( true, true ); - // } - // - // ImGuiCustom.HoverTooltip( "Automatically generate single-select groups from all folders (clears existing groups):\n" - // + "First subdirectory: Option Group\n" - // + "Second subdirectory: Option Name\n" - // + "Afterwards: Relative file paths.\n" - // + "Experimental - Use at own risk!" ); - // } - // - // private void DrawSplitButton() - // { - // if( ImGui.Button( "Split Mod" ) ) - // { - // ModCleanup.SplitMod( Mod!.Data ); - // } - // - // ImGuiCustom.HoverTooltip( - // "Split off all options of a mod into single mods that are placed in a collective folder.\n" - // + "Does not remove or change the mod itself, just create (potentially inefficient) copies.\n" - // + "Experimental - Use at own risk!" ); - // } - // - // private void DrawMaterialChangeRow() - // { - // ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - // ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 ); - // ImGui.SameLine(); - // using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - // ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() ); - // font.Pop(); - // ImGui.SameLine(); - // ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); - // ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 ); - // ImGui.SameLine(); - // var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial ); - // using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings ); - // if( ImGui.Button( "Convert" ) && validStrings ) - // { - // ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial ); - // } - // - // alpha.Pop(); - // - // ImGuiCustom.HoverTooltip( - // "Change the skin material of all models in this mod reference " - // + "from the suffix given in the first text input to " - // + "the suffix given in the second input.\n" - // + "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n" - // + "This overwrites .mdl files, use at your own risk!" ); - // } - // - // private void DrawEditLine() - // { - // DrawOpenModFolderButton(); - // ImGui.SameLine(); - // DrawRenameModFolderButton(); - // ImGui.SameLine(); - // DrawEditJsonButton(); - // ImGui.SameLine(); - // DrawReloadJsonButton(); - // - // DrawResetMetaButton(); - // ImGui.SameLine(); - // DrawDeduplicateButton(); - // ImGui.SameLine(); - // DrawNormalizeButton(); - // ImGui.SameLine(); - // DrawAutoGenerateGroupsButton(); - // ImGui.SameLine(); - // DrawSplitButton(); - // - // DrawMaterialChangeRow(); - // - // DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector ); - // } - // - // public void Draw() - // { - // try - // { - // using var raii = ImGuiRaii.DeferredEnd( ImGui.EndChild ); - // var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); - // - // if( !ret || Mod == null ) - // { - // return; - // } - // - // DrawHeaderLine(); - // - // // Next line with fixed distance. - // ImGuiCustom.VerticalDistance( HeaderLineDistance ); - // - // DrawEnabledMark(); - // ImGui.SameLine(); - // DrawPriority(); - // if( Penumbra.Config.ShowAdvanced ) - // { - // ImGui.SameLine(); - // DrawEditableMark(); - // } - // - // // Next line, if editable. - // if( _editMode ) - // { - // DrawEditLine(); - // } - // - // Details.Draw( _editMode ); - // } - // catch( Exception ex ) - // { - // PluginLog.LogError( ex, "Oh no" ); - // } - // } - //} -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs b/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs deleted file mode 100644 index 3190decb..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.Selector.cs +++ /dev/null @@ -1,797 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.IO; -//using System.Linq; -//using System.Numerics; -//using System.Runtime.InteropServices; -//using System.Windows.Forms.VisualStyles; -//using Dalamud.Interface; -//using Dalamud.Logging; -//using ImGuiNET; -//using Penumbra.Collections; -//using Penumbra.Importer; -//using Penumbra.Mods; -//using Penumbra.UI.Classes; -//using Penumbra.UI.Custom; -//using Penumbra.Util; -// -//namespace Penumbra.UI; -// -//public partial class SettingsInterface -//{ -// // Constants -// private partial class Selector -// { -// private const string LabelSelectorList = "##availableModList"; -// private const string LabelModFilter = "##ModFilter"; -// private const string LabelAddModPopup = "AddModPopup"; -// private const string LabelModHelpPopup = "Help##Selector"; -// -// private const string TooltipModFilter = -// "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; -// -// private const string TooltipDelete = "Delete the selected mod"; -// private const string TooltipAdd = "Add an empty mod"; -// private const string DialogDeleteMod = "PenumbraDeleteMod"; -// private const string ButtonYesDelete = "Yes, delete it"; -// private const string ButtonNoDelete = "No, keep it"; -// -// private const float SelectorPanelWidth = 240f; -// -// private static readonly Vector2 SelectorButtonSizes = new(100, 0); -// private static readonly Vector2 HelpButtonSizes = new(40, 0); -// -// private static readonly Vector4 DeleteModNameColor = new(0.7f, 0.1f, 0.1f, 1); -// } -// -// // Buttons -// private partial class Selector -// { -// // === Delete === -// private int? _deleteIndex; -// -// private void DrawModTrashButton() -// { -// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); -// -// if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) -// { -// _deleteIndex = _index; -// } -// -// raii.Pop(); -// -// ImGuiCustom.HoverTooltip( TooltipDelete ); -// } -// -// private void DrawDeleteModal() -// { -// if( _deleteIndex == null ) -// { -// return; -// } -// -// ImGui.OpenPopup( DialogDeleteMod ); -// -// var _ = true; -// ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); -// var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); -// if( !ret ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// -// if( Mod == null ) -// { -// _deleteIndex = null; -// ImGui.CloseCurrentPopup(); -// return; -// } -// -// ImGui.Text( "Are you sure you want to delete the following mod:" ); -// var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); -// ImGui.Dummy( halfLine ); -// ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); -// ImGui.Dummy( halfLine ); -// -// var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); -// if( ImGui.Button( ButtonYesDelete, buttonSize ) ) -// { -// ImGui.CloseCurrentPopup(); -// var mod = Mod; -// Cache.RemoveMod( mod ); -// Penumbra.ModManager.DeleteMod( mod.Data.BasePath ); -// ModFileSystem.InvokeChange(); -// ClearSelection(); -// } -// -// ImGui.SameLine(); -// -// if( ImGui.Button( ButtonNoDelete, buttonSize ) ) -// { -// ImGui.CloseCurrentPopup(); -// _deleteIndex = null; -// } -// } -// -// // === Add === -// private bool _modAddKeyboardFocus = true; -// -// private void DrawModAddButton() -// { -// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); -// -// if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) -// { -// _modAddKeyboardFocus = true; -// ImGui.OpenPopup( LabelAddModPopup ); -// } -// -// raii.Pop(); -// -// ImGuiCustom.HoverTooltip( TooltipAdd ); -// -// DrawModAddPopup(); -// } -// -// private void DrawModAddPopup() -// { -// if( !ImGui.BeginPopup( LabelAddModPopup ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// -// if( _modAddKeyboardFocus ) -// { -// ImGui.SetKeyboardFocusHere(); -// _modAddKeyboardFocus = false; -// } -// -// var newName = ""; -// if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// try -// { -// var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), -// newName ); -// var modMeta = new ModMeta -// { -// Author = "Unknown", -// Name = newName.Replace( '/', '\\' ), -// Description = string.Empty, -// }; -// -// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); -// modMeta.SaveToFile( metaFile ); -// Penumbra.ModManager.AddMod( newDir ); -// ModFileSystem.InvokeChange(); -// SelectModOnUpdate( newDir.Name ); -// } -// catch( Exception e ) -// { -// PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); -// } -// -// ImGui.CloseCurrentPopup(); -// } -// -// if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) -// { -// ImGui.CloseCurrentPopup(); -// } -// } -// -// // === Help === -// private void DrawModHelpButton() -// { -// using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); -// if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) -// { -// ImGui.OpenPopup( LabelModHelpPopup ); -// } -// } -// -// private static void DrawModHelpPopup() -// { -// ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); -// ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 34 * ImGui.GetTextLineHeightWithSpacing() ), -// ImGuiCond.Appearing ); -// var _ = true; -// if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// -// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); -// ImGui.Text( "Mod Selector" ); -// ImGui.BulletText( "Select a mod to obtain more information." ); -// ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); -// ImGui.Indent(); -// ImGui.Bullet(); -// ImGui.SameLine(); -// ImGui.Text( "Enabled in the current collection." ); -// ImGui.Bullet(); -// ImGui.SameLine(); -// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), "Disabled in the current collection." ); -// ImGui.Bullet(); -// ImGui.SameLine(); -// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.NewMod.Value() ), -// "Newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); -// ImGui.Bullet(); -// ImGui.SameLine(); -// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.HandledConflictMod.Value() ), -// "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); -// ImGui.Bullet(); -// ImGui.SameLine(); -// ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ColorId.DisabledMod.Value() ), -// "Enabled and conflicting with another enabled Mod on the same priority." ); -// ImGui.Unindent(); -// ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); -// ImGui.Indent(); -// ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); -// ImGui.BulletText( -// "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); -// ImGui.BulletText( -// "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" -// + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" -// + "where ModName will be sorted as if it was the string '1'." ); -// ImGui.Unindent(); -// ImGui.BulletText( -// "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); -// ImGui.BulletText( "Right-clicking a folder opens a context menu." ); -// ImGui.Indent(); -// ImGui.BulletText( -// "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); -// ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); -// ImGui.Unindent(); -// ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); -// ImGui.Indent(); -// ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); -// ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); -// ImGui.Unindent(); -// ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); -// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); -// ImGui.Text( "Mod Management" ); -// ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); -// ImGui.BulletText( "You can add a completely empty mod with the plus button." ); -// ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); -// ImGui.BulletText( -// "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); -// ImGui.BulletText( -// "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); -// ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); -// ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); -// ImGui.SameLine(); -// if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) -// { -// ImGui.CloseCurrentPopup(); -// } -// } -// -// // === Main === -// private void DrawModsSelectorButtons() -// { -// // Selector controls -// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) -// .Push( ImGuiStyleVar.FrameRounding, 0 ); -// -// DrawModAddButton(); -// ImGui.SameLine(); -// DrawModHelpButton(); -// ImGui.SameLine(); -// DrawModTrashButton(); -// } -// } -// -// // Filters -// private partial class Selector -// { -// private string _modFilterInput = ""; -// -// private void DrawTextFilter() -// { -// ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); -// var tmp = _modFilterInput; -// if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) -// { -// Cache.SetTextFilter( tmp ); -// _modFilterInput = tmp; -// } -// -// ImGuiCustom.HoverTooltip( TooltipModFilter ); -// } -// -// private void DrawToggleFilter() -// { -// if( ImGui.BeginCombo( "##ModStateFilter", "", -// ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) -// { -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); -// var flags = ( int )Cache.StateFilter; -// foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) -// { -// ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); -// } -// -// Cache.StateFilter = ( ModFilter )flags; -// } -// -// ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); -// } -// -// private void DrawModsSelectorFilter() -// { -// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); -// DrawTextFilter(); -// ImGui.SameLine(); -// DrawToggleFilter(); -// } -// } -// -// // Drag'n Drop -// private partial class Selector -// { -// private const string DraggedModLabel = "ModIndex"; -// private const string DraggedFolderLabel = "FolderName"; -// -// private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); -// -// private static unsafe bool IsDropping( string name ) -// => ImGui.AcceptDragDropPayload( name ).NativePtr != null; -// -// private void DragDropTarget( ModFolder folder ) -// { -// if( !ImGui.BeginDragDropTarget() ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); -// -// if( IsDropping( DraggedModLabel ) ) -// { -// var payload = ImGui.GetDragDropPayload(); -// var modIndex = Marshal.ReadInt32( payload.Data ); -// var mod = Cache.GetMod( modIndex ).Item1; -// mod?.Data.Move( folder ); -// } -// else if( IsDropping( DraggedFolderLabel ) ) -// { -// var payload = ImGui.GetDragDropPayload(); -// var folderName = Marshal.PtrToStringUni( payload.Data ); -// if( ModFileSystem.Find( folderName!, out var droppedFolder ) -// && !ReferenceEquals( droppedFolder, folder ) -// && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) -// { -// droppedFolder.Move( folder ); -// } -// } -// } -// -// private void DragDropSourceFolder( ModFolder folder ) -// { -// if( !ImGui.BeginDragDropSource() ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); -// -// var folderName = folder.FullName; -// var ptr = Marshal.StringToHGlobalUni( folderName ); -// ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); -// ImGui.Text( $"Moving {folderName}..." ); -// } -// -// private void DragDropSourceMod( int modIndex, string modName ) -// { -// if( !ImGui.BeginDragDropSource() ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); -// -// Marshal.WriteInt32( _dragDropPayload, modIndex ); -// ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); -// ImGui.Text( $"Moving {modName}..." ); -// } -// -// ~Selector() -// => Marshal.FreeHGlobal( _dragDropPayload ); -// } -// -// // Selection -// private partial class Selector -// { -// public Mods.FullMod? Mod { get; private set; } -// private int _index; -// private string _nextDir = string.Empty; -// -// private void SetSelection( int idx, Mods.FullMod? info ) -// { -// Mod = info; -// if( idx != _index ) -// { -// _base._menu.InstalledTab.ModPanel.Details.ResetState(); -// } -// -// _index = idx; -// _deleteIndex = null; -// } -// -// private void SetSelection( int idx ) -// { -// if( idx >= Cache.Count ) -// { -// idx = -1; -// } -// -// if( idx < 0 ) -// { -// SetSelection( 0, null ); -// } -// else -// { -// SetSelection( idx, Cache.GetMod( idx ).Item1 ); -// } -// } -// -// public void ReloadSelection() -// => SetSelection( _index, Cache.GetMod( _index ).Item1 ); -// -// public void ClearSelection() -// => SetSelection( -1 ); -// -// public void SelectModOnUpdate( string directory ) -// => _nextDir = directory; -// -// public void SelectModByDir( string name ) -// { -// var (mod, idx) = Cache.GetModByBasePath( name ); -// SetSelection( idx, mod ); -// } -// -// public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) -// { -// if( Mod == null ) -// { -// return; -// } -// -// if( _index >= 0 && Penumbra.ModManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) -// { -// SelectModOnUpdate( Mod.Data.BasePath.Name ); -// _base._menu.InstalledTab.ModPanel.Details.ResetState(); -// } -// } -// -// public void SaveCurrentMod() -// => Mod?.Data.SaveMeta(); -// } -// -// // Right-Clicks -// private partial class Selector -// { -// // === Mod === -// private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen ) -// { -// if( !ImGui.BeginPopup( popupName ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// -// if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) ) -// { -// ImGui.CloseCurrentPopup(); -// } -// -// if( firstOpen ) -// { -// ImGui.SetKeyboardFocusHere( mod.Data.Order.FullPath.Length - 1 ); -// } -// } -// -// // === Folder === -// private string _newFolderName = string.Empty; -// private int _expandIndex = -1; -// private bool _expandCollapse; -// private bool _currentlyExpanding; -// -// private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) -// { -// var change = false; -// var metaManips = false; -// foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) ) -// { -// var (mod, _, _) = Cache.GetMod( currentIdx++ ); -// if( mod != null ) -// { -// change |= mod.Settings.Enabled != toWhat; -// mod!.Settings.Enabled = toWhat; -// metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; -// } -// } -// -// if( !change ) -// { -// return; -// } -// -// Cache.TriggerFilterReset(); -// var collection = Penumbra.CollectionManager.Current; -// if( collection.HasCache ) -// { -// collection.CalculateEffectiveFileList( metaManips, collection == Penumbra.CollectionManager.Default ); -// } -// -// collection.Save(); -// } -// -// private void DrawRenameFolderInput( ModFolder folder ) -// { -// ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); -// if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, -// ImGuiInputTextFlags.EnterReturnsTrue ) ) -// { -// return; -// } -// -// if( _newFolderName.Any() ) -// { -// folder.Rename( _newFolderName ); -// } -// else -// { -// folder.Merge( folder.Parent! ); -// } -// -// _newFolderName = string.Empty; -// } -// -// private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) -// { -// if( !ImGui.BeginPopup( treeName ) ) -// { -// return; -// } -// -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); -// -// if( ImGui.MenuItem( "Expand All Descendants" ) ) -// { -// _expandIndex = currentIdx; -// _expandCollapse = false; -// } -// -// if( ImGui.MenuItem( "Collapse All Descendants" ) ) -// { -// _expandIndex = currentIdx; -// _expandCollapse = true; -// } -// -// if( ImGui.MenuItem( "Enable All Descendants" ) ) -// { -// ChangeStatusOfChildren( folder, currentIdx, true ); -// } -// -// if( ImGui.MenuItem( "Disable All Descendants" ) ) -// { -// ChangeStatusOfChildren( folder, currentIdx, false ); -// } -// -// ImGuiHelpers.ScaledDummy( 0, 10 ); -// DrawRenameFolderInput( folder ); -// } -// } -// -// // Main-Interface -// private partial class Selector -// { -// private readonly SettingsInterface _base; -// public readonly ModListCache Cache; -// -// private float _selectorScalingFactor = 1; -// -// public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) -// { -// _base = ui; -// Cache = new ModListCache( Penumbra.ModManager, newMods ); -// } -// -// private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) -// { -// if( collection == ModCollection.Empty -// || collection == Penumbra.CollectionManager.Current ) -// { -// using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); -// ImGui.Button( label, Vector2.UnitX * size ); -// } -// else if( ImGui.Button( label, Vector2.UnitX * size ) ) -// { -// _base._menu.CollectionsTab.SetCurrentCollection( collection ); -// } -// -// ImGuiCustom.HoverTooltip( -// $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); -// } -// -// private void DrawHeaderBar() -// { -// const float size = 200; -// -// DrawModsSelectorFilter(); -// var textSize = ImGui.CalcTextSize( "Current Collection" ).X + ImGui.GetStyle().ItemInnerSpacing.X; -// var comboSize = size * ImGui.GetIO().FontGlobalScale; -// var offset = comboSize + textSize; -// -// var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth() -// - offset -// - SelectorPanelWidth * _selectorScalingFactor -// - 3 * ImGui.GetStyle().ItemSpacing.X, 5f ); -// ImGui.SameLine(); -// DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default ); -// -// -// ImGui.SameLine(); -// ImGui.SetNextItemWidth( comboSize ); -// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); -// _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); -// } -// -// private void DrawFolderContent( ModFolder folder, ref int idx ) -// { -// // Collection may be manipulated. -// foreach( var item in folder.GetItems( Penumbra.ModManager.Config.SortFoldersFirst ).ToArray() ) -// { -// if( item is ModFolder sub ) -// { -// var (visible, _) = Cache.GetFolder( sub ); -// if( visible ) -// { -// DrawModFolder( sub, ref idx ); -// } -// else -// { -// idx += sub.TotalDescendantMods(); -// } -// } -// else if( item is Mods.Mod _ ) -// { -// var (mod, visible, color) = Cache.GetMod( idx ); -// if( mod != null && visible ) -// { -// DrawMod( mod, idx++, color ); -// } -// else -// { -// ++idx; -// } -// } -// } -// } -// -// private void DrawModFolder( ModFolder folder, ref int idx ) -// { -// var treeName = $"{folder.Name}##{folder.FullName}"; -// var open = ImGui.TreeNodeEx( treeName ); -// using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); -// -// if( idx == _expandIndex ) -// { -// _currentlyExpanding = true; -// } -// -// if( _currentlyExpanding ) -// { -// ImGui.SetNextItemOpen( !_expandCollapse ); -// } -// -// if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) -// { -// _newFolderName = string.Empty; -// ImGui.OpenPopup( treeName ); -// } -// -// DrawFolderContextMenu( folder, idx, treeName ); -// DragDropTarget( folder ); -// DragDropSourceFolder( folder ); -// -// if( open ) -// { -// DrawFolderContent( folder, ref idx ); -// } -// else -// { -// idx += folder.TotalDescendantMods(); -// } -// -// if( idx == _expandIndex ) -// { -// _currentlyExpanding = false; -// _expandIndex = -1; -// } -// } -// -// private void DrawMod( Mods.FullMod mod, int modIndex, uint color ) -// { -// using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); -// -// var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); -// colorRaii.Pop(); -// -// var popupName = $"##SortOrderPopup{modIndex}"; -// var firstOpen = false; -// if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) -// { -// ImGui.OpenPopup( popupName ); -// firstOpen = true; -// } -// -// DragDropTarget( mod.Data.Order.ParentFolder ); -// DragDropSourceMod( modIndex, mod.Data.Meta.Name ); -// -// DrawModOrderPopup( popupName, mod, firstOpen ); -// -// if( selected ) -// { -// SetSelection( modIndex, mod ); -// } -// } -// -// public void Draw() -// { -// if( Cache.Update() ) -// { -// if( _nextDir.Any() ) -// { -// SelectModByDir( _nextDir ); -// _nextDir = string.Empty; -// } -// else if( Mod != null ) -// { -// SelectModByDir( Mod.Data.BasePath.Name ); -// } -// } -// -// _selectorScalingFactor = ImGuiHelpers.GlobalScale -// * ( Penumbra.Config.ScaleModSelector -// ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X -// : 1f ); -// // Selector pane -// DrawHeaderBar(); -// using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); -// ImGui.BeginGroup(); -// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) -// .Push( ImGui.EndChild ); -// // Inlay selector list -// if( ImGui.BeginChild( LabelSelectorList, -// new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), -// true, ImGuiWindowFlags.HorizontalScrollbar ) ) -// { -// style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); -// -// var modIndex = 0; -// DrawFolderContent( Penumbra.ModManager.StructuredMods, ref modIndex ); -// style.Pop(); -// } -// -// raii.Pop(); -// -// DrawModsSelectorButtons(); -// -// style.Pop(); -// DrawModHelpPopup(); -// -// DrawDeleteModal(); -// } -// } -//} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index dd5dddc7..de60ef03 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -1,18 +1,17 @@ -using System; -using System.Numerics; -using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Mods; using Penumbra.UI.Classes; +using System; +using System.Numerics; namespace Penumbra.UI; public partial class ConfigWindow { - public void DrawModsTab() + private void DrawModsTab() { if( !Penumbra.ModManager.Valid ) { @@ -103,12 +102,10 @@ public partial class ConfigWindow private bool _valid; private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; + private Mod _mod = null!; public ModPanel( ConfigWindow window ) - { - _window = window; - } + => _window = window; public void Draw( ModFileSystemSelector selector ) { diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 839809e0..9278faa9 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -47,10 +47,13 @@ public partial class ConfigWindow DrawAdvancedSettings(); } + // Changing the base mod directory. private string? _newModDirectory; private readonly FileDialogManager _dialogManager = new(); - private bool _dialogOpen; + private bool _dialogOpen; // For toggling on/off. + // 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 ) { using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); @@ -58,9 +61,11 @@ public partial class ConfigWindow return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); } + // Draw a directory picker button that toggles the directory picker. + // Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. private void DrawDirectoryPickerButton() { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), _window._iconButtonSize, "Select a directory via dialog.", false, true ) ) { if( _dialogOpen ) @@ -71,6 +76,8 @@ public partial class ConfigWindow else { _newModDirectory ??= Penumbra.Config.ModDirectory; + // Use the current input as start directory if it exists, + // otherwise the current mod directory, otherwise the current application directory. var startDir = Directory.Exists( _newModDirectory ) ? _newModDirectory : Directory.Exists( Penumbra.Config.ModDirectory ) @@ -103,13 +110,15 @@ public partial class ConfigWindow } } + // Draw the text input for the mod directory, + // as well as the directory picker button and the enter warning. private void DrawRootFolder() { _newModDirectory ??= Penumbra.Config.ModDirectory; var spacing = 3 * ImGuiHelpers.GlobalScale; using var group = ImRaii.Group(); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() ); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - _window._iconButtonSize.X ); var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); ImGui.SameLine(); @@ -127,18 +136,14 @@ public partial class ConfigWindow var pos = ImGui.GetCursorPosX(); ImGui.NewLine(); - if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 ) - { - return; - } - - if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) + if( Penumbra.Config.ModDirectory != _newModDirectory + && _newModDirectory.Length == 0 + && ( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) ) { Penumbra.ModManager.DiscoverMods( _newModDirectory ); } } - private static void DrawRediscoverButton() { DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f98ddb0a..fec85594 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Numerics; -using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.Windowing; using ImGuiNET; @@ -28,7 +27,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _penumbra = penumbra; _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod >() ); // TODO + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _modPanel = new ModPanel( this ); _collectionsTab = new CollectionsTab( this ); _effectiveTab = new EffectiveTab(); diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index 8cce7065..8953426f 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -7,36 +7,50 @@ namespace Penumbra.Util; public static class ArrayExtensions { + // Iterate over enumerables with additional index. public static IEnumerable< (T, int) > WithIndex< T >( this IEnumerable< T > list ) => list.Select( ( x, i ) => ( x, i ) ); - public static int IndexOf< T >( this IReadOnlyList< T > array, Predicate< T > predicate ) + + // Find the index of the first object fulfilling predicate's criteria in the given list. + // Returns -1 if no such object is found. + public static int IndexOf< T >( this IEnumerable< T > array, Predicate< T > predicate ) { - for( var i = 0; i < array.Count; ++i ) + var i = 0; + foreach( var obj in array ) { - if( predicate( array[ i ] ) ) + if( predicate( obj ) ) { return i; } + + ++i; } return -1; } - public static int IndexOf< T >( this IReadOnlyList< T > array, T needle ) + // Find the index of the first occurrence of needle in the given list. + // Returns -1 if needle is not contained in the list. + public static int IndexOf< T >( this IEnumerable< T > array, T needle ) where T : notnull { - for( var i = 0; i < array.Count; ++i ) + var i = 0; + foreach( var obj in array ) { - if( needle!.Equals( array[ i ] ) ) + if( needle.Equals( obj ) ) { return i; } + + ++i; } return -1; } - public static bool FindFirst< T >( this IReadOnlyList< T > array, Predicate< T > predicate, [NotNullWhen( true )] out T? result ) + // Find the first object fulfilling predicate's criteria in the given list, if one exists. + // Returns true if an object is found, false otherwise. + public static bool FindFirst< T >( this IEnumerable< T > array, Predicate< T > predicate, [NotNullWhen( true )] out T? result ) { foreach( var obj in array ) { @@ -51,7 +65,9 @@ public static class ArrayExtensions return false; } - public static bool FindFirst< T >( this IReadOnlyList< T > array, T needle, [NotNullWhen( true )] out T? result ) where T : IEquatable< T > + // Find the first occurrence of needle in the given list and return the value contained in the list in result. + // Returns true if an object is found, false otherwise. + public static bool FindFirst< T >( this IEnumerable< T > array, T needle, [NotNullWhen( true )] out T? result ) where T : notnull { foreach( var obj in array ) { diff --git a/Penumbra/Util/BinaryReaderExtensions.cs b/Penumbra/Util/BinaryReaderExtensions.cs deleted file mode 100644 index dec2d44d..00000000 --- a/Penumbra/Util/BinaryReaderExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Penumbra.Util; - -public static class BinaryReaderExtensions -{ - /// - /// Reads a structure from the current stream position. - /// - /// - /// The structure to read in to - /// The file data as a structure - public static T ReadStructure< T >( this BinaryReader br ) where T : struct - { - ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() ); - - return MemoryMarshal.Read< T >( data ); - } - - /// - /// Reads many structures from the current stream position. - /// - /// - /// The number of T to read from the stream - /// The structure to read in to - /// A list containing the structures read from the stream - public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct - { - var size = Marshal.SizeOf< T >(); - var data = br.ReadBytes( size * count ); - - var list = new List< T >( count ); - - for( var i = 0; i < count; i++ ) - { - var offset = size * i; - var span = new ReadOnlySpan< byte >( data, offset, size ); - - list.Add( MemoryMarshal.Read< T >( span ) ); - } - - return list; - } - - /// - /// Seeks this BinaryReader's position to the given offset. Syntactic sugar. - /// - public static void Seek( this BinaryReader br, long offset ) - { - br.BaseStream.Position = offset; - } -} \ No newline at end of file diff --git a/Penumbra/Util/FixedUlongStringEnumConverter.cs b/Penumbra/Util/FixedUlongStringEnumConverter.cs index 1ab68e4e..750422b4 100644 --- a/Penumbra/Util/FixedUlongStringEnumConverter.cs +++ b/Penumbra/Util/FixedUlongStringEnumConverter.cs @@ -75,7 +75,7 @@ public static partial class JsonExtensions return reader; } - public static JsonReader ReadAndAssert( this JsonReader reader ) + private static JsonReader ReadAndAssert( this JsonReader reader ) { if( reader == null ) { diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 8e26d45b..e338a1d5 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; using Lumina.Data.Structs; +using Lumina.Extensions; namespace Penumbra.Util; From e8a0ac98ad68b2e80a100ad9bbed8f67e1013c15 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 17:36:04 +0200 Subject: [PATCH 0141/2451] Small fixes. --- Penumbra/Meta/Files/ImcFile.cs | 2 +- Penumbra/Penumbra.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 67be61e4..8a092052 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -69,7 +69,7 @@ public unsafe class ImcFile : MetaBaseFile public readonly Utf8GamePath Path; public readonly int NumParts; - public bool ChangesSinceLoad = false; + public bool ChangesSinceLoad = true; public ReadOnlySpan< ImcEntry > Span => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8d11133d..546f29dd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -160,6 +160,7 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = true; ResourceLoader.EnableReplacements(); + CollectionManager.Default.SetFiles(); ResidentResources.Reload(); Config.Save(); @@ -176,6 +177,7 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = false; ResourceLoader.DisableReplacements(); + CharacterUtility.ResetAll(); ResidentResources.Reload(); Config.Save(); From a13fccb9ac6750ad2271d0cbfde87603526279cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 17:42:48 +0200 Subject: [PATCH 0142/2451] Test: Update build scripts to checkout submodules --- .github/workflows/build.yml | 2 ++ .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2d2f887..28e8156f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - name: Setup .NET uses: actions/setup-dotnet@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbb70556..82e13457 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - name: Setup .NET uses: actions/setup-dotnet@v1 with: From fdc84836c9631250be95b6013267287308267673 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 18:25:56 +0200 Subject: [PATCH 0143/2451] Fix some migration and deletion stuff. --- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 ++ Penumbra/Mods/Mod.Meta.Migration.cs | 32 ++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index cdfa142b..98267ae7 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -55,6 +55,8 @@ public sealed partial class Mod return; } + group.DeleteFile( mod.BasePath ); + var _ = group switch { SingleModGroup s => s.Name = newName, diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 6b2ae97b..59a67cab 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -6,7 +6,6 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Util; namespace Penumbra.Mods; @@ -50,13 +49,42 @@ public sealed partial class Mod mod._default.FileSwapData.Add( gamePath, swapPath ); } - mod._default.IncorporateMetaChanges( mod.BasePath, false ); + mod._default.IncorporateMetaChanges( mod.BasePath, true ); foreach( var group in mod.Groups ) { IModGroup.SaveModGroup( group, mod.BasePath ); } + // Delete meta files. + foreach( var file in seenMetaFiles.Where( f => f.Exists ) ) + { + try + { + File.Delete( file.FullName ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not delete meta file {file.FullName} during migration:\n{e}" ); + } + } + + // Delete old meta files. + var oldMetaFile = Path.Combine( mod.BasePath.FullName, "metadata_manipulations.json" ); + if( File.Exists( oldMetaFile ) ) + { + try + { + File.Delete( oldMetaFile ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not delete old meta file {oldMetaFile} during migration:\n{e}" ); + } + } + + mod.FileVersion = 1; mod.SaveDefaultMod(); + mod.SaveMeta(); return true; } From 4d6d73abb688845a6ed7e7a9c8189bfcb3cab702 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 18:34:44 +0200 Subject: [PATCH 0144/2451] Add an import date property to mod metas --- Penumbra/Mods/Mod.Meta.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 6e5291a6..b5ccc304 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -9,7 +9,7 @@ using OtterGui.Classes; namespace Penumbra.Mods; [Flags] -public enum MetaChangeType : byte +public enum MetaChangeType : ushort { None = 0x00, Name = 0x01, @@ -19,6 +19,7 @@ public enum MetaChangeType : byte Website = 0x10, Deletion = 0x20, Migration = 0x40, + ImportDate = 0x80, } public sealed partial class Mod @@ -30,6 +31,7 @@ public sealed partial class Mod public string Description { get; private set; } = string.Empty; public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; + public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); internal FileInfo MetaFile => new(Path.Combine( BasePath.FullName, "meta.json" )); @@ -54,6 +56,7 @@ public sealed partial class Mod var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; + var importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); MetaChangeType changes = 0; if( Name != newName ) @@ -95,6 +98,12 @@ public sealed partial class Mod } } + if( ImportDate != importDate ) + { + ImportDate = importDate; + changes |= MetaChangeType.ImportDate; + } + return changes; } catch( Exception e ) @@ -117,6 +126,7 @@ public sealed partial class Mod { nameof( Description ), JToken.FromObject( Description ) }, { nameof( Version ), JToken.FromObject( Version ) }, { nameof( Website ), JToken.FromObject( Website ) }, + { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, }; File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); } From c2a030aa6b62d3b5910f5053d64d2f6dbb6c14db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 18:48:37 +0200 Subject: [PATCH 0145/2451] Small fixes. --- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 560dae22..0668d02b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -53,6 +53,7 @@ public partial class ConfigWindow _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale }; _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale }; + EditButtons(); EditRegularMeta(); ImGui.Dummy( _window._defaultSpace ); @@ -76,27 +77,29 @@ public partial class ConfigWindow private void EditButtons() { + var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 ); var folderExists = Directory.Exists( _mod.BasePath.FullName ); var tt = folderExists - ? $"Open {_mod.BasePath.FullName} in the file explorer of your choice." - : $"Mod directory {_mod.BasePath.FullName} does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", Vector2.Zero, tt, !folderExists ) ) + ? $"Open \"{_mod.BasePath.FullName}\" in the file explorer of your choice." + : $"Mod directory \"{_mod.BasePath.FullName}\" does not exist."; + if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", buttonSize, tt, !folderExists ) ) { Process.Start( new ProcessStartInfo( _mod.BasePath.FullName ) { UseShellExecute = true } ); } + ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, "Not implemented yet", true ); ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Reload Mod", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Not implemented yet", true ); - ImGuiUtil.DrawDisabledButton( "Deduplicate", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Deduplicate", buttonSize, "Not implemented yet", true ); ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Normalize", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Normalize", buttonSize, "Not implemented yet", true ); ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Auto-Create Groups", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Auto-Create Groups", buttonSize, "Not implemented yet", true ); - ImGuiUtil.DrawDisabledButton( "Change Material Suffix", Vector2.Zero, "Not implemented yet", true ); + ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, "Not implemented yet", true ); ImGui.Dummy( _window._defaultSpace ); } From 545536f66f273999619eda69099a2cf7cb2de8a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Apr 2022 23:31:56 +0200 Subject: [PATCH 0146/2451] Add memory of last mod path as well as default directory. Add default Author. Fix bugs. --- OtterGui | 2 +- Penumbra/Configuration.cs | 4 ++ Penumbra/UI/Classes/ModFileSystemSelector.cs | 13 +++- ...cs => ConfigWindow.SettingsTab.General.cs} | 69 ++++++++++++++++++- Penumbra/UI/ConfigWindow.SettingsTab.cs | 6 +- 5 files changed, 86 insertions(+), 8 deletions(-) rename Penumbra/UI/{ConfigWindow.SettingsTab.ModSelector.cs => ConfigWindow.SettingsTab.General.cs} (57%) diff --git a/OtterGui b/OtterGui index 1a3cd1f8..6b918d67 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1a3cd1f881f3b6c2c4d9d4b20f054d1ab5ccc014 +Subproject commit 6b918d67fb7370340b1b310a03dbf033b9950450 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 565d7975..e5c8a899 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Configuration; using Dalamud.Logging; using OtterGui.Filesystem; +using Penumbra.Import; using Penumbra.UI.Classes; namespace Penumbra; @@ -38,6 +39,9 @@ public partial class Configuration : IPluginConfiguration public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } + public string DefaultModImportPath { get; set; } = string.Empty; + public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public Dictionary< ColorId, uint > Colors { get; set; } = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index be36ab68..9a9d90c8 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -132,7 +132,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod try { var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, string.Empty, string.Empty, "1.0", string.Empty ); + Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); Penumbra.ModManager.AddMod( newDir ); _newModName = string.Empty; } @@ -144,11 +144,18 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } // Add an import mods button that opens a file selector. + // Only set the initial directory once. + private bool _hasSetFolder; + private void AddImportModButton( Vector2 size ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) ) { + var modPath = _hasSetFolder ? null + : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath + : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; + _hasSetFolder = true; _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => { if( s ) @@ -156,7 +163,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ) ); ImGui.OpenPopup( "Import Status" ); } - }, 0, Penumbra.Config.ModDirectory ); + }, 0, modPath ); } _fileManager.Draw(); @@ -202,7 +209,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } catch( Exception e ) { - PluginLog.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); + PluginLog.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs similarity index 57% rename from Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs rename to Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 55d9a66d..faddf3c7 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.ModSelector.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -1,4 +1,7 @@ using System; +using System.IO; +using System.Numerics; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Filesystem; @@ -12,7 +15,7 @@ public partial class ConfigWindow { private void DrawModSelectorSettings() { - if( !ImGui.CollapsingHeader( "Mod Selector" ) ) + if( !ImGui.CollapsingHeader( "General" ) ) { return; } @@ -20,6 +23,9 @@ public partial class ConfigWindow DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); + ImGui.Dummy( _window._defaultSpace ); + DrawDefaultModImportPath(); + DrawDefaultModAuthor(); ImGui.NewLine(); } @@ -93,5 +99,66 @@ public partial class ConfigWindow ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); } + + private void DrawDefaultModImportPath() + { + var tmp = Penumbra.Config.DefaultModImportPath; + var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); + if( ImGui.InputText( "##defaultModImport", ref tmp, 256 ) ) + { + Penumbra.Config.DefaultModImportPath = tmp; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##import", _window._iconButtonSize, + "Select a directory via dialog.", false, true ) ) + { + if( _dialogOpen ) + { + _dialogManager.Reset(); + _dialogOpen = false; + } + else + { + var startDir = Directory.Exists( Penumbra.Config.ModDirectory ) ? Penumbra.Config.ModDirectory : "."; + + _dialogManager.OpenFolderDialog( "Choose Default Import Directory", ( b, s ) => + { + Penumbra.Config.DefaultModImportPath = b ? s : Penumbra.Config.DefaultModImportPath; + Penumbra.Config.Save(); + _dialogOpen = false; + }, startDir ); + _dialogOpen = true; + } + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker( "Default Mod Import Directory", + "Set the directory that gets opened when using the file picker to import mods for the first time." ); + } + + private void DrawDefaultModAuthor() + { + var tmp = Penumbra.Config.DefaultModAuthor; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( ImGui.InputText( "##defaultAuthor", ref tmp, 64 ) ) + { + Penumbra.Config.DefaultModAuthor = tmp; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Penumbra.Config.Save(); + } + + ImGuiUtil.LabeledHelpMarker( "Default Mod Author", "Set the default author stored for newly created mods." ); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 9278faa9..0961f177 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -45,6 +45,8 @@ public partial class ConfigWindow DrawModSelectorSettings(); DrawColorSettings(); DrawAdvancedSettings(); + + _dialogManager.Draw(); } // Changing the base mod directory. @@ -92,8 +94,6 @@ public partial class ConfigWindow _dialogOpen = true; } } - - _dialogManager.Draw(); } private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) @@ -137,7 +137,7 @@ public partial class ConfigWindow ImGui.NewLine(); if( Penumbra.Config.ModDirectory != _newModDirectory - && _newModDirectory.Length == 0 + && _newModDirectory.Length != 0 && ( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) ) { Penumbra.ModManager.DiscoverMods( _newModDirectory ); From 60cf7e3c2eb90bf7cd89101c3f490feda25ca6f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Apr 2022 16:03:01 +0200 Subject: [PATCH 0147/2451] Fix crashes on file selector, default mod creation for simple mods, default file edit button not working. --- OtterGui | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 3 +- Penumbra/Mods/ModCleanup.cs | 182 +++++++++++++++++++ Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 23 +++ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 12 +- 5 files changed, 216 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 6b918d67..a9443d8c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6b918d67fb7370340b1b310a03dbf033b9950450 +Subproject commit a9443d8cd2b7446702d0bd6beea2a1b0ef747654 diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 5ac31a35..15bd352d 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -39,7 +39,7 @@ public partial class TexToolsImporter Mod.CreateMeta( ret, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); ExtractSimpleModList( ret, modList, modData ); - + Mod.CreateDefaultFiles( ret ); return ret; } @@ -96,6 +96,7 @@ public partial class TexToolsImporter : modList.Description, null, null ); ExtractSimpleModList( ret, modList.SimpleModsList, modData ); + Mod.CreateDefaultFiles( ret ); return ret; } diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index 996c6437..779197ef 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -5,13 +5,195 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Import; +using Penumbra.Meta.Manipulations; using Penumbra.Util; namespace Penumbra.Mods; +public partial class Mod +{ + //public partial class Manager + //{ + // public class Normalizer + // { + // private Dictionary< Utf8GamePath, List< (FullPath Path, IModGroup Group, ISubMod Option) > > Files = new(); + // private Dictionary< Utf8GamePath, List< (FullPath Path, IModGroup Group, ISubMod Option) > > Swaps = new(); + // private Dictionary< MetaManipulation, List< (MetaManipulation Value, IModGroup Group, ISubMod Option) > > Manips = new(); + // + // public Normalizer( Mod mod ) + // { + // // Default changes are irrelevant since they can only be overwritten. + // foreach( var group in mod.Groups ) + // { + // foreach( var option in group ) + // { + // foreach( var (key, value) in option.Files ) + // { + // if( !Files.TryGetValue( key, out var list ) ) + // { + // list = new List< (FullPath Path, IModGroup Group, ISubMod Option) > { ( value, @group, option ) }; + // Files[ key ] = list; + // } + // else + // { + // list.Add( ( value, @group, option ) ); + // } + // } + // } + // } + // } + // + // // Normalize a mod, this entails: + // // - If + // public static void Normalize( Mod mod ) + // { + // NormalizeOptions( mod ); + // MergeSingleGroups( mod ); + // DeleteEmptyGroups( mod ); + // } + // + // + // // Delete every option group that has either no options, + // // or exclusively empty options. + // // Triggers changes through calling ModManager. + // private static void DeleteEmptyGroups( Mod mod ) + // { + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // DeleteIdenticalOptions( mod, i ); + // var group = mod.Groups[ i ]; + // if( group.Count == 0 || group.All( o => o.FileSwaps.Count == 0 && o.Files.Count == 0 && o.Manipulations.Count == 0 ) ) + // { + // Penumbra.ModManager.DeleteModGroup( mod, i-- ); + // } + // } + // } + // + // // Merge every non-optional group into the default mod. + // // Overwrites default mod entries if necessary. + // // Deletes the non-optional group afterwards. + // // Triggers changes through calling ModManager. + // private static void MergeSingleGroup( Mod mod ) + // { + // var defaultMod = ( SubMod )mod.Default; + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // var group = mod.Groups[ i ]; + // if( group.Type == SelectType.Single && group.Count == 1 ) + // { + // defaultMod.MergeIn( group[ 0 ] ); + // + // Penumbra.ModManager.DeleteModGroup( mod, i-- ); + // } + // } + // } + // + // private static void NotifyChanges( Mod mod, int groupIdx, ModOptionChangeType type, ref bool anyChanges ) + // { + // if( anyChanges ) + // { + // for( var i = 0; i < mod.Groups[ groupIdx ].Count; ++i ) + // { + // Penumbra.ModManager.ModOptionChanged.Invoke( type, mod, groupIdx, i, -1 ); + // } + // + // anyChanges = false; + // } + // } + // + // private static void NormalizeOptions( Mod mod ) + // { + // var defaultMod = ( SubMod )mod.Default; + // + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // var group = mod.Groups[ i ]; + // if( group.Type == SelectType.Multi || group.Count < 2 ) + // { + // continue; + // } + // + // var firstOption = mod.Groups[ i ][ 0 ]; + // var anyChanges = false; + // foreach( var (key, value) in firstOption.Files.ToList() ) + // { + // if( group.Skip( 1 ).All( o => o.Files.TryGetValue( key, out var v ) && v.Equals( value ) ) ) + // { + // anyChanges = true; + // defaultMod.FileData[ key ] = value; + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.FileData.Remove( key ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionFilesChanged, ref anyChanges ); + // + // foreach( var (key, value) in firstOption.FileSwaps.ToList() ) + // { + // if( group.Skip( 1 ).All( o => o.FileSwaps.TryGetValue( key, out var v ) && v.Equals( value ) ) ) + // { + // anyChanges = true; + // defaultMod.FileData[ key ] = value; + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.FileSwapData.Remove( key ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionSwapsChanged, ref anyChanges ); + // + // anyChanges = false; + // foreach( var manip in firstOption.Manipulations.ToList() ) + // { + // if( group.Skip( 1 ).All( o => ( ( HashSet< MetaManipulation > )o.Manipulations ).TryGetValue( manip, out var m ) + // && manip.EntryEquals( m ) ) ) + // { + // anyChanges = true; + // defaultMod.ManipulationData.Remove( manip ); + // defaultMod.ManipulationData.Add( manip ); + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.ManipulationData.Remove( manip ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionMetaChanged, ref anyChanges ); + // } + // } + // + // + // // Delete all options that are entirely identical. + // // Deletes the later occurring option. + // private static void DeleteIdenticalOptions( Mod mod, int groupIdx ) + // { + // var group = mod.Groups[ groupIdx ]; + // for( var i = 0; i < group.Count; ++i ) + // { + // var option = group[ i ]; + // for( var j = i + 1; j < group.Count; ++j ) + // { + // var option2 = group[ j ]; + // if( option.Files.SetEquals( option2.Files ) + // && option.FileSwaps.SetEquals( option2.FileSwaps ) + // && option.Manipulations.SetEquals( option2.Manipulations ) ) + // { + // Penumbra.ModManager.DeleteOption( mod, groupIdx, j-- ); + // } + // } + // } + // } + // } + //} +} + // TODO Everything //ublic class ModCleanup // diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 786ac6de..3748fdeb 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -82,6 +82,27 @@ public partial class Mod public IReadOnlySet< MetaManipulation > Manipulations => ManipulationData; + // Insert all changes from the other submod. + // Overwrites already existing changes in this mod. + public void MergeIn( ISubMod other ) + { + foreach( var (key, value) in other.Files ) + { + FileData[ key ] = value; + } + + foreach( var (key, value) in other.FileSwaps ) + { + FileSwapData[ key ] = value; + } + + foreach( var manip in other.Manipulations ) + { + ManipulationData.Remove( manip ); + ManipulationData.Add( manip ); + } + } + public void Load( DirectoryInfo basePath, JToken json, out int priority ) { FileData.Clear(); @@ -148,6 +169,7 @@ public partial class Mod { File.Delete( file.FullName ); } + ManipulationData.UnionWith( meta.MetaManipulations ); break; @@ -163,6 +185,7 @@ public partial class Mod { File.Delete( file.FullName ); } + ManipulationData.UnionWith( rgsp.MetaManipulations ); break; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 0668d02b..2d751625 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -87,7 +87,7 @@ public partial class ConfigWindow Process.Start( new ProcessStartInfo( _mod.BasePath.FullName ) { UseShellExecute = true } ); } - + ImGui.SameLine(); ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, "Not implemented yet", true ); ImGui.SameLine(); @@ -136,8 +136,10 @@ public partial class ConfigWindow Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); } - var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X, 0 ); + var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); + var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X, 0 ); if( ImGui.Button( "Edit Description", reducedSize ) ) { _delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) ); @@ -148,7 +150,8 @@ public partial class ConfigWindow var tt = fileExists ? "Open the metadata json file in the text editor of your choice." : "The metadata json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) + if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", _window._iconButtonSize, tt, + !fileExists, true ) ) { Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); } @@ -163,7 +166,8 @@ public partial class ConfigWindow tt = fileExists ? "Open the default option json file in the text editor of your choice." : "The default option json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) + if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", _window._iconButtonSize, tt, + !fileExists, true ) ) { Process.Start( new ProcessStartInfo( _mod.DefaultFile ) { UseShellExecute = true } ); } From 2e1a11d16cb1cb080ea34e0ae880a0cc5a9105d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Apr 2022 16:12:40 +0200 Subject: [PATCH 0148/2451] Skip non-existing directories when migrating sort order. --- OtterGui | 2 +- Penumbra/Configuration.Migration.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index a9443d8c..79a1ad44 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a9443d8cd2b7446702d0bd6beea2a1b0ef747654 +Subproject commit 79a1ad44fd718a0deb66c9f38cdb8d9eb41520b8 diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index deff2d29..2940f4fb 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -111,7 +111,7 @@ public partial class Configuration j.WriteStartObject(); j.WritePropertyName( "Data" ); j.WriteStartObject(); - foreach( var (mod, path) in ModSortOrder ) + foreach( var (mod, path) in ModSortOrder.Where( kvp => Directory.Exists( Path.Combine( _config.ModDirectory, kvp.Key ) ) ) ) { j.WritePropertyName( mod, true ); j.WriteValue( path ); From c1859ccb2479f2e0352890e4282319b392c7f00b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Apr 2022 16:27:18 +0200 Subject: [PATCH 0149/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 79a1ad44..f53f754e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 79a1ad44fd718a0deb66c9f38cdb8d9eb41520b8 +Subproject commit f53f754ee559491abae5b70b5dc2368376b61de4 From 9af4406c8c4fa5d256da6f70ba22c7b08384e77f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Apr 2022 17:04:08 +0200 Subject: [PATCH 0150/2451] Fixes... --- OtterGui | 2 +- Penumbra/Mods/ModCleanup.cs | 352 ++++++++++++++++++------------------ Penumbra/Util/Backup.cs | 2 +- 3 files changed, 178 insertions(+), 178 deletions(-) diff --git a/OtterGui b/OtterGui index f53f754e..1a3f6237 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f53f754ee559491abae5b70b5dc2368376b61de4 +Subproject commit 1a3f6237c857562cac85de8f922dbef7bb63c870 diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index 779197ef..ed1afc6f 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -16,182 +16,182 @@ namespace Penumbra.Mods; public partial class Mod { - //public partial class Manager - //{ - // public class Normalizer - // { - // private Dictionary< Utf8GamePath, List< (FullPath Path, IModGroup Group, ISubMod Option) > > Files = new(); - // private Dictionary< Utf8GamePath, List< (FullPath Path, IModGroup Group, ISubMod Option) > > Swaps = new(); - // private Dictionary< MetaManipulation, List< (MetaManipulation Value, IModGroup Group, ISubMod Option) > > Manips = new(); - // - // public Normalizer( Mod mod ) - // { - // // Default changes are irrelevant since they can only be overwritten. - // foreach( var group in mod.Groups ) - // { - // foreach( var option in group ) - // { - // foreach( var (key, value) in option.Files ) - // { - // if( !Files.TryGetValue( key, out var list ) ) - // { - // list = new List< (FullPath Path, IModGroup Group, ISubMod Option) > { ( value, @group, option ) }; - // Files[ key ] = list; - // } - // else - // { - // list.Add( ( value, @group, option ) ); - // } - // } - // } - // } - // } - // - // // Normalize a mod, this entails: - // // - If - // public static void Normalize( Mod mod ) - // { - // NormalizeOptions( mod ); - // MergeSingleGroups( mod ); - // DeleteEmptyGroups( mod ); - // } - // - // - // // Delete every option group that has either no options, - // // or exclusively empty options. - // // Triggers changes through calling ModManager. - // private static void DeleteEmptyGroups( Mod mod ) - // { - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // DeleteIdenticalOptions( mod, i ); - // var group = mod.Groups[ i ]; - // if( group.Count == 0 || group.All( o => o.FileSwaps.Count == 0 && o.Files.Count == 0 && o.Manipulations.Count == 0 ) ) - // { - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // // Merge every non-optional group into the default mod. - // // Overwrites default mod entries if necessary. - // // Deletes the non-optional group afterwards. - // // Triggers changes through calling ModManager. - // private static void MergeSingleGroup( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Single && group.Count == 1 ) - // { - // defaultMod.MergeIn( group[ 0 ] ); - // - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // private static void NotifyChanges( Mod mod, int groupIdx, ModOptionChangeType type, ref bool anyChanges ) - // { - // if( anyChanges ) - // { - // for( var i = 0; i < mod.Groups[ groupIdx ].Count; ++i ) - // { - // Penumbra.ModManager.ModOptionChanged.Invoke( type, mod, groupIdx, i, -1 ); - // } - // - // anyChanges = false; - // } - // } - // - // private static void NormalizeOptions( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Multi || group.Count < 2 ) - // { - // continue; - // } - // - // var firstOption = mod.Groups[ i ][ 0 ]; - // var anyChanges = false; - // foreach( var (key, value) in firstOption.Files.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.Files.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionFilesChanged, ref anyChanges ); - // - // foreach( var (key, value) in firstOption.FileSwaps.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.FileSwaps.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileSwapData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionSwapsChanged, ref anyChanges ); - // - // anyChanges = false; - // foreach( var manip in firstOption.Manipulations.ToList() ) - // { - // if( group.Skip( 1 ).All( o => ( ( HashSet< MetaManipulation > )o.Manipulations ).TryGetValue( manip, out var m ) - // && manip.EntryEquals( m ) ) ) - // { - // anyChanges = true; - // defaultMod.ManipulationData.Remove( manip ); - // defaultMod.ManipulationData.Add( manip ); - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.ManipulationData.Remove( manip ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionMetaChanged, ref anyChanges ); - // } - // } - // - // - // // Delete all options that are entirely identical. - // // Deletes the later occurring option. - // private static void DeleteIdenticalOptions( Mod mod, int groupIdx ) - // { - // var group = mod.Groups[ groupIdx ]; - // for( var i = 0; i < group.Count; ++i ) - // { - // var option = group[ i ]; - // for( var j = i + 1; j < group.Count; ++j ) - // { - // var option2 = group[ j ]; - // if( option.Files.SetEquals( option2.Files ) - // && option.FileSwaps.SetEquals( option2.FileSwaps ) - // && option.Manipulations.SetEquals( option2.Manipulations ) ) - // { - // Penumbra.ModManager.DeleteOption( mod, groupIdx, j-- ); - // } - // } - // } - // } - // } - //} + public partial class Manager + { + //public class Normalizer + //{ + // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Files = new(); + // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Swaps = new(); + // private HashSet< (MetaManipulation Manipulation, int GroupPriority) > Manips = new(); + // + // public Normalizer( Mod mod ) + // { + // // Default changes are irrelevant since they can only be overwritten. + // foreach( var group in mod.Groups ) + // { + // foreach( var option in group ) + // { + // foreach( var (key, value) in option.Files ) + // { + // if( !Files.TryGetValue( key, out var list ) ) + // { + // list = new List< (FullPath Path, IModGroup Group, ISubMod Option) > { ( value, @group, option ) }; + // Files[ key ] = list; + // } + // else + // { + // list.Add( ( value, @group, option ) ); + // } + // } + // } + // } + // } + // + // // Normalize a mod, this entails: + // // - If + // public static void Normalize( Mod mod ) + // { + // NormalizeOptions( mod ); + // MergeSingleGroups( mod ); + // DeleteEmptyGroups( mod ); + // } + // + // + // // Delete every option group that has either no options, + // // or exclusively empty options. + // // Triggers changes through calling ModManager. + // private static void DeleteEmptyGroups( Mod mod ) + // { + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // DeleteIdenticalOptions( mod, i ); + // var group = mod.Groups[ i ]; + // if( group.Count == 0 || group.All( o => o.FileSwaps.Count == 0 && o.Files.Count == 0 && o.Manipulations.Count == 0 ) ) + // { + // Penumbra.ModManager.DeleteModGroup( mod, i-- ); + // } + // } + // } + // + // // Merge every non-optional group into the default mod. + // // Overwrites default mod entries if necessary. + // // Deletes the non-optional group afterwards. + // // Triggers changes through calling ModManager. + // private static void MergeSingleGroup( Mod mod ) + // { + // var defaultMod = ( SubMod )mod.Default; + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // var group = mod.Groups[ i ]; + // if( group.Type == SelectType.Single && group.Count == 1 ) + // { + // defaultMod.MergeIn( group[ 0 ] ); + // + // Penumbra.ModManager.DeleteModGroup( mod, i-- ); + // } + // } + // } + // + // private static void NotifyChanges( Mod mod, int groupIdx, ModOptionChangeType type, ref bool anyChanges ) + // { + // if( anyChanges ) + // { + // for( var i = 0; i < mod.Groups[ groupIdx ].Count; ++i ) + // { + // Penumbra.ModManager.ModOptionChanged.Invoke( type, mod, groupIdx, i, -1 ); + // } + // + // anyChanges = false; + // } + // } + // + // private static void NormalizeOptions( Mod mod ) + // { + // var defaultMod = ( SubMod )mod.Default; + // + // for( var i = 0; i < mod.Groups.Count; ++i ) + // { + // var group = mod.Groups[ i ]; + // if( group.Type == SelectType.Multi || group.Count < 2 ) + // { + // continue; + // } + // + // var firstOption = mod.Groups[ i ][ 0 ]; + // var anyChanges = false; + // foreach( var (key, value) in firstOption.Files.ToList() ) + // { + // if( group.Skip( 1 ).All( o => o.Files.TryGetValue( key, out var v ) && v.Equals( value ) ) ) + // { + // anyChanges = true; + // defaultMod.FileData[ key ] = value; + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.FileData.Remove( key ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionFilesChanged, ref anyChanges ); + // + // foreach( var (key, value) in firstOption.FileSwaps.ToList() ) + // { + // if( group.Skip( 1 ).All( o => o.FileSwaps.TryGetValue( key, out var v ) && v.Equals( value ) ) ) + // { + // anyChanges = true; + // defaultMod.FileData[ key ] = value; + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.FileSwapData.Remove( key ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionSwapsChanged, ref anyChanges ); + // + // anyChanges = false; + // foreach( var manip in firstOption.Manipulations.ToList() ) + // { + // if( group.Skip( 1 ).All( o => ( ( HashSet< MetaManipulation > )o.Manipulations ).TryGetValue( manip, out var m ) + // && manip.EntryEquals( m ) ) ) + // { + // anyChanges = true; + // defaultMod.ManipulationData.Remove( manip ); + // defaultMod.ManipulationData.Add( manip ); + // foreach( var option in group.Cast< SubMod >() ) + // { + // option.ManipulationData.Remove( manip ); + // } + // } + // } + // + // NotifyChanges( mod, i, ModOptionChangeType.OptionMetaChanged, ref anyChanges ); + // } + // } + // + // + // // Delete all options that are entirely identical. + // // Deletes the later occurring option. + // private static void DeleteIdenticalOptions( Mod mod, int groupIdx ) + // { + // var group = mod.Groups[ groupIdx ]; + // for( var i = 0; i < group.Count; ++i ) + // { + // var option = group[ i ]; + // for( var j = i + 1; j < group.Count; ++j ) + // { + // var option2 = group[ j ]; + // if( option.Files.SetEquals( option2.Files ) + // && option.FileSwaps.SetEquals( option2.FileSwaps ) + // && option.Manipulations.SetEquals( option2.Manipulations ) ) + // { + // Penumbra.ModManager.DeleteOption( mod, groupIdx, j-- ); + // } + // } + // } + // } + //} + } } // TODO Everything diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs index a9d4b489..a1e9551d 100644 --- a/Penumbra/Util/Backup.cs +++ b/Penumbra/Util/Backup.cs @@ -22,7 +22,7 @@ public static class Backup var configDirectory = Dalamud.PluginInterface.ConfigDirectory.Parent!.FullName; var directory = CreateBackupDirectory(); var (newestFile, oldestFile, numFiles) = CheckExistingBackups( directory ); - var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmss}.zip" ); + var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmmss}.zip" ); if( newestFile == null || CheckNewestBackup( newestFile, configDirectory, files.Count ) ) { CreateBackup( files, newBackupName, configDirectory ); From 7b0935750aa418bd0c6bca5a60579b77db670db6 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:18:11 +0200 Subject: [PATCH 0151/2451] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d914afaf..830e5abb 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,8 @@ Penumbra is a runtime mod loader for FINAL FANTASY XIV, with a bunch of other us * Resolve conflicts between mods by changing mod order * Files can be edited and are often replicated in-game after a map change or closing and reopening a window -## Current Status -Penumbra, in its current state, is not intended for widespread use. It is mainly aimed at developers and people who don't need their hands held (for now). - -We're working towards a 1.0 release, and you can follow it's progress [here](https://github.com/xivdev/Penumbra/projects/1). +## Support +Either open an issue here or join us in [Discord](https://discord.gg/kVva7DHV4r). ## Contributing Contributions are welcome, but please make an issue first before writing any code. It's possible what you want to implement is out of scope for this project, or could be reworked so that it would provide greater benefit. From e8ee729ec5e22cc9d2e805e8616028924cf7320d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Apr 2022 15:30:08 +0200 Subject: [PATCH 0152/2451] Added a bunch of logging, small fix. --- OtterGui | 2 +- .../Collections/CollectionManager.Active.cs | 1 + Penumbra/Collections/CollectionManager.cs | 2 + .../Collections/ModCollection.Cache.Access.cs | 11 +++-- Penumbra/Collections/ModCollection.File.cs | 1 + .../Collections/ModCollection.Inheritance.cs | 8 ++-- Penumbra/Interop/CharacterUtility.cs | 1 + Penumbra/Interop/Resolver/PathResolver.cs | 5 ++- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 3 ++ Penumbra/Mods/Manager/Mod.Manager.Root.cs | 2 + Penumbra/Mods/Mod.Creation.cs | 8 ++-- Penumbra/Mods/ModFileSystem.cs | 9 ++++- Penumbra/Mods/Subclasses/IModGroup.cs | 2 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 1 + Penumbra/UI/ConfigWindow.ModsTab.cs | 40 +++++++++++++------ Penumbra/UI/ConfigWindow.cs | 28 ++++++++----- 16 files changed, 86 insertions(+), 38 deletions(-) diff --git a/OtterGui b/OtterGui index 1a3f6237..627e3132 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1a3f6237c857562cac85de8f922dbef7bb63c870 +Subproject commit 627e313232a2e602432dcc4d090dccd5e27993a1 diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index fe396ccd..9ae03552 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -206,6 +206,7 @@ public partial class ModCollection j.WriteEndObject(); j.WriteEndObject(); + PluginLog.Verbose( "Active Collections saved." ); } catch( Exception e ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 03b51406..bb6bb826 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -122,6 +122,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); + PluginLog.Debug( "Added collection {Name:l}.", newCollection.Name ); CollectionChanged.Invoke( Type.Inactive, null, newCollection ); SetCollection( newCollection.Index, Type.Current ); return true; @@ -176,6 +177,7 @@ public partial class ModCollection } } + PluginLog.Debug( "Removed collection {Name:l}.", collection.Name ); CollectionChanged.Invoke( Type.Inactive, collection, null ); return true; } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 168dbed5..dd2fe073 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -17,24 +17,26 @@ public partial class ModCollection => _cache != null; // Only create, do not update. - public void CreateCache( bool isDefault ) + private void CreateCache( bool isDefault ) { if( _cache == null ) { CalculateEffectiveFileList( true, isDefault ); + PluginLog.Verbose( "Created new cache for collection {Name:l}.", Name ); } } // Force an update with metadata for this cache. - public void ForceCacheUpdate( bool isDefault ) + private void ForceCacheUpdate( bool isDefault ) => CalculateEffectiveFileList( true, isDefault ); // Clear the current cache. - public void ClearCache() + private void ClearCache() { _cache?.Dispose(); _cache = null; + PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); } @@ -78,7 +80,7 @@ public partial class ModCollection return; } - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + PluginLog.Debug( "Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Name, withMetaManipulations, reloadDefault ); _cache ??= new Cache( this ); _cache.CalculateEffectiveFileList( withMetaManipulations ); @@ -164,6 +166,7 @@ public partial class ModCollection else { _cache.MetaManipulations.SetFiles(); + PluginLog.Debug( "Set CharacterUtility resources for collection {Name:l}.", Name ); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 262208b4..4eb2cca0 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -87,6 +87,7 @@ public partial class ModCollection try { file.Delete(); + PluginLog.Information( "Deleted collection file {File:l} for {Name:l}.", file.FullName, Name ); } catch( Exception e ) { diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index dfa2ce0f..7bef9e09 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Logging; using OtterGui.Filesystem; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; @@ -66,9 +66,6 @@ public partial class ModCollection return ValidInheritance.Valid; } - private bool CheckForCircle( ModCollection collection ) - => ReferenceEquals( collection, this ) || _inheritance.Any( c => c.CheckForCircle( collection ) ); - // Add a new collection to the inheritance list. // We do not check if this collection would be visited before, // only that it is unique in the list itself. @@ -84,6 +81,7 @@ public partial class ModCollection collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; InheritanceChanged.Invoke( false ); + PluginLog.Debug( "Added {InheritedName:l} to {Name:l} inheritances.", collection.Name, Name ); return true; } @@ -94,6 +92,7 @@ public partial class ModCollection inheritance.InheritanceChanged -= OnInheritedInheritanceChange; _inheritance.RemoveAt( idx ); InheritanceChanged.Invoke( false ); + PluginLog.Debug( "Removed {InheritedName:l} from {Name:l} inheritances.", inheritance.Name, Name ); } // Order in the inheritance list is relevant. @@ -102,6 +101,7 @@ public partial class ModCollection if( _inheritance.Move( from, to ) ) { InheritanceChanged.Invoke( false ); + PluginLog.Debug( "Moved {Name:l}s inheritance {From} to {To}.", Name, from, to ); } } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 7923254b..ff6ca138 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -112,6 +112,7 @@ public unsafe class CharacterUtility : IDisposable { ResetResource( idx ); } + PluginLog.Debug( "Reset all CharacterUtility resources to default." ); } public void Dispose() diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4cd10e14..e9412a3a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,4 +1,5 @@ using System; +using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; @@ -70,9 +71,10 @@ public partial class PathResolver : IDisposable EnableMetaHooks(); _loader.ResolvePathCustomization += CharacterResolver; + PluginLog.Debug( "Character Path Resolver enabled." ); } - public void Disable() + private void Disable() { if( !Enabled ) { @@ -90,6 +92,7 @@ public partial class PathResolver : IDisposable PathCollections.Clear(); _loader.ResolvePathCustomization -= CharacterResolver; + PluginLog.Debug( "Character Path Resolver disabled." ); } public void Dispose() diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 8e44edfd..d82216ec 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -33,6 +33,7 @@ public partial class Mod try { Directory.Delete( mod.BasePath.FullName, true ); + PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.BasePath.FullName, mod.Name ); } catch( Exception e ) { @@ -47,6 +48,7 @@ public partial class Mod } ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + PluginLog.Debug( "Deleted mod {Name:l}.", mod.Name ); } // Load a new mod and add it to the manager if successful. @@ -66,6 +68,7 @@ public partial class Mod mod.Index = _mods.Count; _mods.Add( mod ); ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath ); + PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName ); } // Add new mods to NewMods and remove deleted mods from NewMods. diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index a27d5c02..92587bfd 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -56,6 +56,7 @@ public sealed partial class Mod Valid = Directory.Exists( newDir.FullName ); if( Penumbra.Config.ModDirectory != BasePath.FullName ) { + PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.", Penumbra.Config.ModDirectory, BasePath.FullName ); Penumbra.Config.ModDirectory = BasePath.FullName; Penumbra.Config.Save(); } @@ -86,6 +87,7 @@ public sealed partial class Mod } ModDiscoveryFinished?.Invoke(); + PluginLog.Information( "Rediscovered mods." ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index a8015c49..79056ba2 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -67,7 +67,7 @@ public partial class Mod { var group = new MultiModGroup() { - Name = groupData.GroupName!, + Name = groupData.GroupName, Description = desc, Priority = priority, }; @@ -79,7 +79,7 @@ public partial class Mod { var group = new SingleModGroup() { - Name = groupData.GroupName!, + Name = groupData.GroupName, Description = desc, Priority = priority, }; @@ -97,9 +97,9 @@ public partial class Mod .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) .Where( t => t.Item1 ); - var mod = new SubMod() + var mod = new SubMod { - Name = option.Name!, + Name = option.Name, }; foreach( var (_, gamePath, file) in list ) { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index eb22b314..db18f049 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Dalamud.Logging; using OtterGui.Filesystem; namespace Penumbra.Mods; @@ -14,8 +15,11 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. - public void Save() - => SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + private void Save() + { + SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + PluginLog.Verbose( "Saved mod filesystem." ); + } // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystem Load() @@ -46,6 +50,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { Save(); } + PluginLog.Debug( "Reloaded mod filesystem." ); } // Save the filesystem on every filesystem change except full reloading. diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index c89b2da9..5dae5481 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -48,6 +48,7 @@ public interface IModGroup : IEnumerable< ISubMod > try { File.Delete( file ); + PluginLog.Debug( "Deleted group file {File:l} for {GroupName:l}.", file, Name ); } catch( Exception e ) { @@ -81,6 +82,7 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteEndArray(); j.WriteEndObject(); + PluginLog.Debug( "Saved group file {File:l} for {GroupName:l}.", file, group.Name ); } public IModGroup Convert( SelectType type ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 9a9d90c8..930ad3de 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -338,6 +338,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); + OnSelectionChange( null, base.SelectedLeaf?.Value, default ); _lastSelectedDirectory = string.Empty; } } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index de60ef03..52d26878 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -6,6 +6,7 @@ using Penumbra.Mods; using Penumbra.UI.Classes; using System; using System.Numerics; +using Dalamud.Logging; namespace Penumbra.UI; @@ -18,21 +19,36 @@ public partial class ConfigWindow return; } - using var tab = ImRaii.TabItem( "Mods" ); - if( !tab ) + try { - return; + using var tab = ImRaii.TabItem( "Mods" ); + if( !tab ) + { + return; + } + + _selector.Draw( GetModSelectorSize() ); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawHeaderLine(); + + using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar ); + if( child ) + { + _modPanel.Draw( _selector ); + } } - - _selector.Draw( GetModSelectorSize() ); - ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawHeaderLine(); - - using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar ); - if( child ) + catch( Exception e ) { - _modPanel.Draw( _selector ); + PluginLog.Error($"Exception thrown during ModPanel Render:\n{e}" ); + PluginLog.Error($"{Penumbra.ModManager.Count} Mods\n" + + $"{Penumbra.CollectionManager.Current.Name} Current Collection\n" + + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + + $"{_selector.SortMode} Sort Mode\n" + + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join(", ", Penumbra.CollectionManager.Current.Inheritance)} Inheritances\n" + + $"{_selector.SelectedSettingCollection.Name} Collection\n"); } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index fec85594..48fd594a 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; +using Dalamud.Logging; using ImGuiNET; using OtterGui.Raii; using Penumbra.Mods; @@ -27,7 +28,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _penumbra = penumbra; _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _modPanel = new ModPanel( this ); _collectionsTab = new CollectionsTab( this ); _effectiveTab = new EffectiveTab(); @@ -47,15 +48,22 @@ public sealed partial class ConfigWindow : Window, IDisposable public override void Draw() { - using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); - SetupSizes(); - _settingsTab.Draw(); - DrawModsTab(); - _collectionsTab.Draw(); - DrawChangedItemTab(); - _effectiveTab.Draw(); - _debugTab.Draw(); - _resourceTab.Draw(); + try + { + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); + SetupSizes(); + _settingsTab.Draw(); + DrawModsTab(); + _collectionsTab.Draw(); + DrawChangedItemTab(); + _effectiveTab.Draw(); + _debugTab.Draw(); + _resourceTab.Draw(); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception thrown during UI Render:\n{e}" ); + } } public void Dispose() From 15602f5be53ea379bf4554f42771d26bb2804513 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Apr 2022 15:59:41 +0200 Subject: [PATCH 0153/2451] Fix some enabling stuff. Always use PathResolver. Add counter to materials and imcs. --- .../Collections/ModCollection.Cache.Access.cs | 3 +++ Penumbra/Collections/ModCollection.Cache.cs | 7 ++++++- .../Interop/Resolver/PathResolver.Material.cs | 6 ++++-- Penumbra/Interop/Resolver/PathResolver.cs | 21 +------------------ Penumbra/Meta/Manager/MetaManager.Imc.cs | 7 +++++-- Penumbra/Penumbra.cs | 13 +++--------- 6 files changed, 22 insertions(+), 35 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index dd2fe073..1720632a 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -16,6 +16,9 @@ public partial class ModCollection public bool HasCache => _cache != null; + public int RecomputeCounter + => _cache?.RecomputeCounter ?? 0; + // Only create, do not update. private void CreateCache( bool isDefault ) { diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index fe008820..20e3cc51 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -18,7 +18,7 @@ public partial class ModCollection // Shared caches to avoid allocations. private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024); private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024); - private static readonly List< ModSettings? > ResolvedSettings = new(128); + private static readonly List< ModSettings? > ResolvedSettings = new(128); private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); @@ -27,6 +27,10 @@ public partial class ModCollection public readonly MetaManager MetaManipulations; public ConflictCache Conflicts = new(); + // Count the number of recalculations of the effective file list. + // This is used for material and imc changes. + public int RecomputeCounter { get; private set; } = 0; + // Obtain currently changed items. Computes them if they haven't been computed before. public IReadOnlyDictionary< string, object? > ChangedItems { @@ -120,6 +124,7 @@ public partial class ModCollection } AddMetaFiles(); + ++RecomputeCounter; } // Identify and record all manipulated objects for this entire collection. diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index e2d88fee..aa6fff01 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -78,7 +78,9 @@ public unsafe partial class PathResolver return false; } - if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) + var lastUnderscore = split.LastIndexOf( ( byte )'_' ); + var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); + if( Penumbra.CollectionManager.ByName( name, out var collection ) ) { PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", split, path ); SetCollection( path, collection ); @@ -100,7 +102,7 @@ public unsafe partial class PathResolver { if( nonDefault && type == ResourceType.Mtrl ) { - var fullPath = new FullPath( $"|{collection.Name}|{path}" ); + var fullPath = new FullPath( $"|{collection.RecomputeCounter}_{collection.Name}|{path}" ); data = ( fullPath, collection ); } else diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index e9412a3a..d25517da 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -26,7 +26,6 @@ public partial class PathResolver : IDisposable SetupHumanHooks(); SetupWeaponHooks(); SetupMetaHooks(); - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; } // The modified resolver that handles game path resolving. @@ -74,7 +73,7 @@ public partial class PathResolver : IDisposable PluginLog.Debug( "Character Path Resolver enabled." ); } - private void Disable() + public void Disable() { if( !Enabled ) { @@ -103,23 +102,5 @@ public partial class PathResolver : IDisposable DisposeMtrlHooks(); DisposeDataHooks(); DisposeMetaHooks(); - Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; - } - - private void OnCollectionChange( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? characterName ) - { - if( type != ModCollection.Type.Character ) - { - return; - } - - if( Penumbra.CollectionManager.HasCharacterCollections ) - { - Enable(); - } - else - { - Disable(); - } } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 161307fa..7c829216 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -125,7 +125,7 @@ public partial class MetaManager } private FullPath CreateImcPath( Utf8GamePath path ) - => new($"|{_collection.Name}|{path}"); + => new($"|{_collection.RecomputeCounter}_{_collection.Name}|{path}"); private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) @@ -138,7 +138,10 @@ public partial class MetaManager PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) + + var lastUnderscore = split.LastIndexOf( ( byte )'_' ); + var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); + if( Penumbra.CollectionManager.ByName( name, out var collection ) && collection.HasCache && collection.MetaCache!.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 546f29dd..a60493dd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -90,11 +90,6 @@ public class Penumbra : IDalamudPlugin HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - if( Config.DebugMode ) - { - ResourceLoader.EnableDebug(); - } - ResidentResources.Reload(); Api = new PenumbraApi( this ); @@ -111,6 +106,7 @@ public class Penumbra : IDalamudPlugin if( Config.EnableMods ) { ResourceLoader.EnableReplacements(); + PathResolver.Enable(); } if( Config.DebugMode ) @@ -124,11 +120,6 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - if( CollectionManager.HasCharacterCollections ) - { - PathResolver.Enable(); - } - ResidentResources.Reload(); } @@ -162,6 +153,7 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableReplacements(); CollectionManager.Default.SetFiles(); ResidentResources.Reload(); + PathResolver.Enable(); Config.Save(); ObjectReloader.RedrawAll( RedrawType.Redraw ); @@ -179,6 +171,7 @@ public class Penumbra : IDalamudPlugin ResourceLoader.DisableReplacements(); CharacterUtility.ResetAll(); ResidentResources.Reload(); + PathResolver.Disable(); Config.Save(); ObjectReloader.RedrawAll( RedrawType.Redraw ); From c9c4447f3d810da719f4bdb350473779894610d9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Apr 2022 17:07:35 +0200 Subject: [PATCH 0154/2451] Fix dumbness (all of it, please) --- Penumbra/Interop/Resolver/PathResolver.Material.cs | 4 ++-- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index aa6fff01..ed8d5706 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -82,7 +82,7 @@ public unsafe partial class PathResolver var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); if( Penumbra.CollectionManager.ByName( name, out var collection ) ) { - PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", split, path ); + PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); SetCollection( path, collection ); } else @@ -102,7 +102,7 @@ public unsafe partial class PathResolver { if( nonDefault && type == ResourceType.Mtrl ) { - var fullPath = new FullPath( $"|{collection.RecomputeCounter}_{collection.Name}|{path}" ); + var fullPath = new FullPath( $"|{collection.Name}_{collection.RecomputeCounter}|{path}" ); data = ( fullPath, collection ); } else diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 7c829216..ed6457a1 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -125,7 +125,7 @@ public partial class MetaManager } private FullPath CreateImcPath( Utf8GamePath path ) - => new($"|{_collection.RecomputeCounter}_{_collection.Name}|{path}"); + => new($"|{_collection.Name}_{_collection.RecomputeCounter}|{path}"); private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) From c390b57b0f5d9edc43b11cd4173d9288eefc291f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Apr 2022 21:22:54 +0200 Subject: [PATCH 0155/2451] Force IsSync in certain situations. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 9 +++++++-- Penumbra/Interop/Structs/FileMode.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 6aaccde5..8969e081 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -163,13 +163,18 @@ public unsafe partial class ResourceLoader var split = gamePath.Path.Split( ( byte )'|', 3, false ); fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; + + // Force isSync = true for these calls. I don't really understand why, + // or where the difference even comes from. + // Was called with True on my client and with false on other peoples clients, + // which caused problems. var funcFound = ResourceLoadCustomization.GetInvocationList() .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, true, out ret ) ); if( !funcFound ) { - ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); + ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, true ); } // Return original resource handle path so that they can be loaded separately. diff --git a/Penumbra/Interop/Structs/FileMode.cs b/Penumbra/Interop/Structs/FileMode.cs index 13966e65..1c1914b2 100644 --- a/Penumbra/Interop/Structs/FileMode.cs +++ b/Penumbra/Interop/Structs/FileMode.cs @@ -1,6 +1,6 @@ namespace Penumbra.Interop.Structs; -public enum FileMode : uint +public enum FileMode : byte { LoadUnpackedResource = 0, LoadFileResource = 1, // The config files in MyGames use this. From 7795f9a691ab4397db130190490614530abb0976 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Apr 2022 00:46:43 +0200 Subject: [PATCH 0156/2451] Fix crash on startup if collection dir does not exist. --- Penumbra/Penumbra.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a60493dd..609048ff 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -364,7 +364,10 @@ public class Penumbra : IDalamudPlugin // Collect all relevant files for penumbra configuration. private static IReadOnlyList< FileInfo > PenumbraBackupFiles() { - var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); + var collectionDir = ModCollection.CollectionDirectory; + var list = Directory.Exists(collectionDir) + ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() + : new List< FileInfo >(); list.Add( Dalamud.PluginInterface.ConfigFile ); list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); From cf54bc7f57a274a7f6a2265a85f168e239fd1ea5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Apr 2022 00:47:06 +0200 Subject: [PATCH 0157/2451] Correspond Adventurer Plate Actor to name. --- .../Interop/Resolver/PathResolver.Data.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index abcccc9a..57d55935 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -7,6 +7,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.Collections; using Penumbra.GameData.ByteString; @@ -138,6 +139,25 @@ public unsafe partial class PathResolver return text != null ? text->NodeText.ToString() : null; } + // Obtain the name displayed in the Character Card from the agent. + private static string? GetCardName() + { + var uiModule = ( UIModule* )Dalamud.GameGui.GetUIModule(); + var agentModule = uiModule->GetAgentModule(); + var agent = (byte*) agentModule->GetAgentByInternalID( 393 ); + if( agent == null ) + { + return null; + } + + var data = *(byte**) (agent + 0x28); + if( data == null ) + return null; + + var block = data + 0x7A; + return new Utf8String( block ).ToString(); + } + // Guesstimate whether an unnamed cutscene actor corresponds to the player or not, // and if so, return the player name. private static string? GetCutsceneName( GameObject* gameObject ) @@ -167,9 +187,9 @@ public unsafe partial class PathResolver var name = gameObject->ObjectIndex switch { - 240 => GetPlayerName(), // character window - 241 => GetInspectName(), // inspect - 242 => GetPlayerName(), // try-on + 240 => GetPlayerName(), // character window + 241 => GetInspectName() ?? GetCardName(), // inspect, character card + 242 => GetPlayerName(), // try-on >= 200 => GetCutsceneName( gameObject ), _ => null, } From 5e46f43d7d5e0c784b61c18c88e6cdaea77e913f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Apr 2022 16:26:39 +0200 Subject: [PATCH 0158/2451] Make extracting mods cancelable, some fixes. --- OtterGui | 2 +- Penumbra/Configuration.Migration.cs | 5 + Penumbra/Import/TexToolsImport.cs | 81 +++++++++++-- Penumbra/Import/TexToolsImporter.Gui.cs | 3 + Penumbra/Import/TexToolsImporter.ModPack.cs | 114 +++++++++--------- .../Loader/ResourceLoader.Replacement.cs | 10 +- .../Interop/Resolver/PathResolver.Material.cs | 7 +- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 66 +++++----- 9 files changed, 182 insertions(+), 108 deletions(-) diff --git a/OtterGui b/OtterGui index 627e3132..a1ff5ca2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 627e313232a2e602432dcc4d090dccd5e27993a1 +Subproject commit a1ff5ca207080786225f716a0e2487e206923a52 diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 2940f4fb..c952e687 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -30,6 +30,11 @@ public partial class Configuration public static void Migrate( Configuration config ) { + if( !File.Exists( Dalamud.PluginInterface.ConfigFile.FullName ) ) + { + return; + } + var m = new Migration { _config = config, diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index ed4e6cd1..f239ff3c 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,7 +1,9 @@ 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; @@ -11,7 +13,7 @@ using FileMode = System.IO.FileMode; namespace Penumbra.Import; -public partial class TexToolsImporter +public partial class TexToolsImporter : IDisposable { private const string TempFileName = "textools-import"; private static readonly JsonSerializerSettings JsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; @@ -21,22 +23,59 @@ public partial class TexToolsImporter private readonly IEnumerable< FileInfo > _modPackFiles; private readonly int _modPackCount; + private FileStream? _tmpFileStream; + private StreamDisposer? _streamDisposer; + private readonly CancellationTokenSource _cancellation = new(); + private readonly CancellationToken _token; public ImporterState State { get; private set; } public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; - public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files ) - : this( baseDirectory, files.Count, files ) + public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files, + Action< FileInfo, DirectoryInfo?, Exception? > handler ) + : this( baseDirectory, files.Count, files, handler ) { } - public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles ) + public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, + Action< FileInfo, DirectoryInfo?, Exception? > handler ) { _baseDirectory = baseDirectory; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _modPackFiles = modPackFiles; _modPackCount = count; ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); - Task.Run( ImportFiles ); + _token = _cancellation.Token; + Task.Run( ImportFiles, _token ) + .ContinueWith( _ => CloseStreams() ) + .ContinueWith( _ => + { + foreach( var (file, dir, error) in ExtractedMods ) + { + handler( file, dir, error ); + } + } ); + } + + private void CloseStreams() + { + _tmpFileStream?.Dispose(); + _tmpFileStream = null; + ResetStreamDisposer(); + } + + public void Dispose() + { + _cancellation.Cancel( true ); + if( State != ImporterState.WritingPackToDisk ) + { + _tmpFileStream?.Dispose(); + _tmpFileStream = null; + } + + if( State != ImporterState.ExtractingModFiles ) + { + ResetStreamDisposer(); + } } private void ImportFiles() @@ -45,6 +84,13 @@ public partial class TexToolsImporter _currentModPackIdx = 0; foreach( var file in _modPackFiles ) { + _currentModDirectory = null; + if( _token.IsCancellationRequested ) + { + ExtractedMods.Add( ( file, null, new TaskCanceledException( "Task canceled by user." ) ) ); + continue; + } + try { var directory = VerifyVersionAndImport( file ); @@ -52,7 +98,7 @@ public partial class TexToolsImporter } catch( Exception e ) { - ExtractedMods.Add( ( file, null, e ) ); + ExtractedMods.Add( ( file, _currentModDirectory, e ) ); _currentNumOptions = 0; _currentOptionIdx = 0; _currentFileIdx = 0; @@ -88,7 +134,7 @@ public partial class TexToolsImporter PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); } - return ImportV2ModPack( _: modPackFile, extractedModPack, modRaw ); + return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); } if( modPackFile.Extension != ".ttmp" ) @@ -129,11 +175,19 @@ public partial class TexToolsImporter private void WriteZipEntryToTempFile( Stream s ) { - using var fs = new FileStream( _tmpFile, FileMode.Create ); - s.CopyTo( fs ); + _tmpFileStream?.Dispose(); // should not happen + _tmpFileStream = new FileStream( _tmpFile, FileMode.Create ); + if( _token.IsCancellationRequested ) + { + return; + } + + s.CopyTo( _tmpFileStream ); + _tmpFileStream.Dispose(); + _tmpFileStream = null; } - private PenumbraSqPackStream GetSqPackStreamStream( ZipFile file, string entryName ) + private StreamDisposer GetSqPackStreamStream( ZipFile file, string entryName ) { State = ImporterState.WritingPackToDisk; @@ -148,7 +202,14 @@ public partial class TexToolsImporter WriteZipEntryToTempFile( s ); + _streamDisposer?.Dispose(); // Should not happen. var fs = new FileStream( _tmpFile, FileMode.Open ); return new StreamDisposer( fs ); } + + private void ResetStreamDisposer() + { + _streamDisposer?.Dispose(); + _streamDisposer = null; + } } \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index e510b149..b1db1b96 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -91,4 +91,7 @@ public partial class TexToolsImporter } } } + + public bool DrawCancelButton( Vector2 size ) + => ImGuiUtil.DrawDisabledButton( "Cancel", size, string.Empty, _token.IsCancellationRequested ); } \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 15bd352d..dadfe4ac 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -13,6 +13,8 @@ namespace Penumbra.Import; public partial class TexToolsImporter { + private DirectoryInfo? _currentModDirectory; + // Version 1 mod packs are a simple collection of files without much information. private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) { @@ -31,16 +33,16 @@ public partial class TexToolsImporter var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); - // Open the mod data file from the mod pack as a SqPackStream - using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); - - var ret = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); // Create a new ModMeta from the TTMP mod list info - Mod.CreateMeta( ret, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); + Mod.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); - ExtractSimpleModList( ret, modList, modData ); - Mod.CreateDefaultFiles( ret ); - return ret; + // Open the mod data file from the mod pack as a SqPackStream + _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + ExtractSimpleModList( _currentModDirectory, modList ); + Mod.CreateDefaultFiles( _currentModDirectory ); + ResetStreamDisposer(); + return _currentModDirectory; } // Version 2 mod packs can either be simple or extended, import accordingly. @@ -87,17 +89,17 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; PluginLog.Log( " -> Importing Simple V2 ModPack" ); - // Open the mod data file from the mod pack as a SqPackStream - using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); - - var ret = Mod.CreateModFolder( _baseDirectory, _currentModName ); - Mod.CreateMeta( ret, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); + Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, null, null ); - ExtractSimpleModList( ret, modList.SimpleModsList, modData ); - Mod.CreateDefaultFiles( ret ); - return ret; + // Open the mod data file from the mod pack as a SqPackStream + _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); + Mod.CreateDefaultFiles( _currentModDirectory ); + ResetStreamDisposer(); + return _currentModDirectory; } // Obtain the number of relevant options to extract. @@ -118,23 +120,24 @@ public partial class TexToolsImporter var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!; _currentNumOptions = GetOptionCount( modList ); _currentModName = modList.Name; - // Open the mod data file from the mod pack as a SqPackStream - using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); - var ret = Mod.CreateModFolder( _baseDirectory, _currentModName ); - Mod.CreateMeta( ret, _currentModName, modList.Author, modList.Description, modList.Version, null ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); + Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, null ); if( _currentNumOptions == 0 ) { - return ret; + return _currentModDirectory; } + // Open the mod data file from the mod pack as a SqPackStream + _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + // It can contain a simple list, still. if( modList.SimpleModsList.Length > 0 ) { _currentGroupName = string.Empty; _currentOptionName = "Default"; - ExtractSimpleModList( ret, modList.SimpleModsList, modData ); + ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); } // Iterate through all pages @@ -147,18 +150,19 @@ public partial class TexToolsImporter _currentGroupName = group.GroupName; options.Clear(); var description = new StringBuilder(); - var groupFolder = Mod.NewSubFolderName( ret, group.GroupName ) - ?? new DirectoryInfo( Path.Combine( ret.FullName, $"Group {groupPriority + 1}" ) ); + var groupFolder = Mod.NewSubFolderName( _currentModDirectory, group.GroupName ) + ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, $"Group {groupPriority + 1}" ) ); var optionIdx = 1; foreach( var option in group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ) ) { + _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name ) ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {optionIdx}" ) ); - ExtractSimpleModList( optionFolder, option.ModsJsons, modData ); - options.Add( Mod.CreateSubMod( ret, optionFolder, option ) ); + ExtractSimpleModList( optionFolder, option.ModsJsons ); + options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) ); description.Append( option.Description ); if( !string.IsNullOrEmpty( option.Description ) ) { @@ -169,15 +173,16 @@ public partial class TexToolsImporter ++_currentOptionIdx; } - Mod.CreateOptionGroup( ret, group, groupPriority++, description.ToString(), options ); + Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority++, description.ToString(), options ); } } - Mod.CreateDefaultFiles( ret ); - return ret; + ResetStreamDisposer(); + Mod.CreateDefaultFiles( _currentModDirectory ); + return _currentModDirectory; } - private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods, PenumbraSqPackStream dataStream ) + private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods ) { State = ImporterState.ExtractingModFiles; @@ -187,35 +192,34 @@ public partial class TexToolsImporter // Extract each SimpleMod into the new mod folder foreach( var simpleMod in mods ) { - ExtractMod( outDirectory, simpleMod, dataStream ); + ExtractMod( outDirectory, simpleMod ); ++_currentFileIdx; } } - private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream ) + private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod ) { + if( _streamDisposer is not PenumbraSqPackStream stream ) + { + return; + } + PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) ); - try + _token.ThrowIfCancellationRequested(); + var data = stream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); + + _currentFileName = mod.FullPath; + var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) ); + + extractedFile.Directory?.Create(); + + if( extractedFile.FullName.EndsWith( ".mdl" ) ) { - var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); - - _currentFileName = mod.FullPath; - var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) ); - - extractedFile.Directory?.Create(); - - if( extractedFile.FullName.EndsWith( ".mdl" ) ) - { - ProcessMdl( data.Data ); - } - - File.WriteAllBytes( extractedFile.FullName, data.Data ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Could not extract mod." ); + ProcessMdl( data.Data ); } + + File.WriteAllBytes( extractedFile.FullName, data.Data ); } private static void ProcessMdl( byte[] mdl ) @@ -226,11 +230,11 @@ public partial class TexToolsImporter mdl[ 64 ] = 1; // Model header LOD num - var stackSize = BitConverter.ToUInt32( mdl, 4 ); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + var stackSize = BitConverter.ToUInt32( mdl, 4 ); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; } } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 8969e081..fd0a73cb 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -145,7 +145,7 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) ) + if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) { return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } @@ -164,17 +164,13 @@ public unsafe partial class ResourceLoader fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; - // Force isSync = true for these calls. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. var funcFound = ResourceLoadCustomization.GetInvocationList() .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, true, out ret ) ); + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); if( !funcFound ) { - ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, true ); + ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); } // Return original resource handle path so that they can be loaded separately. diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index ed8d5706..400d0a4c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -90,8 +90,11 @@ public unsafe partial class PathResolver PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); } - - ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); + // Force isSync = true for this call. I don't really understand why, + // or where the difference even comes from. + // Was called with True on my client and with false on other peoples clients, + // which caused problems. + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); PathCollections.TryRemove( path, out _ ); return true; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 609048ff..0e7f80d6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -365,7 +365,7 @@ public class Penumbra : IDalamudPlugin private static IReadOnlyList< FileInfo > PenumbraBackupFiles() { var collectionDir = ModCollection.CollectionDirectory; - var list = Directory.Exists(collectionDir) + var list = Directory.Exists( collectionDir ) ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() : new List< FileInfo >(); list.Add( Dalamud.PluginInterface.ConfigFile ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 930ad3de..1a85e371 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; @@ -55,6 +56,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; + _import?.Dispose(); + _import = null; } public new ModFileSystem.Leaf? SelectedLeaf @@ -160,7 +163,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { if( s ) { - _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ) ); + _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), + AddNewMod ); ImGui.OpenPopup( "Import Status" ); } }, 0, modPath ); @@ -177,51 +181,49 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod ImGui.SetNextWindowSize( display / 4 ); ImGui.SetNextWindowPos( 3 * display / 8 ); using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); - if( _import != null && popup.Success ) + if( _import == null || !popup.Success ) { - _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); - if( _import.State == ImporterState.Done ) - { - ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 ); - if( ImGui.Button( "Close", -Vector2.UnitX ) ) - { - AddNewMods( _import.ExtractedMods ); - _import = null; - ImGui.CloseCurrentPopup(); - } - } + return; + } + + _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); + ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 ); + if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX ) + || _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) ) + { + _import?.Dispose(); + _import = null; + ImGui.CloseCurrentPopup(); } } - // Clean up invalid directories if necessary. - // Add all successfully extracted mods. - private static void AddNewMods( IEnumerable< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > list ) + // Clean up invalid directory if necessary. + // Add successfully extracted mods. + private static void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) { - foreach( var (file, dir, error) in list ) + if( error != null ) { - if( error != null ) + if( dir != null && Directory.Exists( dir.FullName ) ) { - if( dir != null && Directory.Exists( dir.FullName ) ) + try { - try - { - Directory.Delete( dir.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); - } + Directory.Delete( dir.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); } - - PluginLog.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); - continue; } - if( dir != null ) + if( error is not OperationCanceledException ) { - Penumbra.ModManager.AddMod( dir ); + PluginLog.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); } } + else if( dir != null ) + { + Penumbra.ModManager.AddMod( dir ); + } } private void DeleteModButton( Vector2 size ) From f24ec8ebe22525179af789f91ff1750b36bfbfc0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Apr 2022 18:54:17 +0200 Subject: [PATCH 0159/2451] Change ImGui.Text to ImGui.TextUnformatted. --- OtterGui | 2 +- Penumbra/Import/TexToolsImporter.Gui.cs | 14 ++-- .../Loader/ResourceLoader.Replacement.cs | 5 +- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 19 ++++- Penumbra/UI/Classes/SubModEditWindow.cs | 16 ++-- ...ConfigWindow.CollectionsTab.Inheritance.cs | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 74 +++++++++---------- Penumbra/UI/ConfigWindow.Misc.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 4 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 2 +- 14 files changed, 79 insertions(+), 69 deletions(-) diff --git a/OtterGui b/OtterGui index a1ff5ca2..cce4e9ed 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a1ff5ca207080786225f716a0e2487e206923a52 +Subproject commit cce4e9ed2cf5fa0068d6c8fadff5acd0d54f8359 diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index b1db1b96..1378d2f7 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -38,7 +38,7 @@ public partial class TexToolsImporter var percentage = _modPackCount / ( float )_currentModPackIdx; ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" ); ImGui.NewLine(); - ImGui.Text( $"Extracting {_currentModName}..." ); + ImGui.TextUnformatted( $"Extracting {_currentModName}..." ); if( _currentNumOptions > 1 ) { @@ -47,7 +47,7 @@ public partial class TexToolsImporter percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions; ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" ); ImGui.NewLine(); - ImGui.Text( + ImGui.TextUnformatted( $"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." ); } @@ -56,7 +56,7 @@ public partial class TexToolsImporter percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles; ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" ); ImGui.NewLine(); - ImGui.Text( $"Extracting file {_currentFileName}..." ); + ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." ); } } @@ -65,7 +65,7 @@ public partial class TexToolsImporter { var success = ExtractedMods.Count( t => t.Mod != null ); - ImGui.Text( $"Successfully extracted {success} / {ExtractedMods.Count} files." ); + ImGui.TextUnformatted( $"Successfully extracted {success} / {ExtractedMods.Count} files." ); ImGui.NewLine(); using var table = ImRaii.Table( "##files", 2 ); if( !table ) @@ -76,17 +76,17 @@ public partial class TexToolsImporter foreach( var (file, dir, ex) in ExtractedMods ) { ImGui.TableNextColumn(); - ImGui.Text( file.Name ); + ImGui.TextUnformatted( file.Name ); ImGui.TableNextColumn(); if( dir != null ) { using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); - ImGui.Text( dir.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ); + ImGui.TextUnformatted( dir.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ); } else { using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); - ImGui.Text( ex!.Message ); + ImGui.TextUnformatted( ex!.Message ); ImGuiUtil.HoverTooltip( ex.ToString() ); } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index fd0a73cb..09572313 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -49,15 +49,14 @@ public unsafe partial class ResourceLoader { if( local != game ) { - PluginLog.Warning( "Hash function appears to have changed. {Hash1:X8} vs {Hash2:X8} for {Path}.", game, local, path ); + PluginLog.Warning( "Hash function appears to have changed. Computed {Hash1:X8} vs Game {Hash2:X8} for {Path}.", local, game, path ); } } private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, - int* resourceHash, byte* path, void* unk, bool isUnk ) + ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0e7f80d6..94b22f86 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -187,7 +187,7 @@ public class Penumbra : IDalamudPlugin { if( it is Item ) { - ImGui.Text( "Left Click to create an item link in chat." ); + ImGui.TextUnformatted( "Left Click to create an item link in chat." ); } }; Api.ChangedItemClicked += ( button, it ) => diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 1a85e371..4c061110 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -178,16 +179,26 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void DrawInfoPopup() { var display = ImGui.GetIO().DisplaySize; - ImGui.SetNextWindowSize( display / 4 ); - ImGui.SetNextWindowPos( 3 * display / 8 ); + var height = Math.Max( display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing() ); + var width = display.X / 8; + var size = new Vector2( width * 2, height ); + var pos = ( display - size ) / 2; + ImGui.SetNextWindowSize( size ); + ImGui.SetNextWindowPos( pos ); using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); if( _import == null || !popup.Success ) { return; } - _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); - ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 ); + using( var child = ImRaii.Child( "##import", new Vector2( -1, size.Y - ImGui.GetFrameHeight() * 2 ) ) ) + { + if( child ) + { + _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); + } + } + if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX ) || _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) ) { diff --git a/Penumbra/UI/Classes/SubModEditWindow.cs b/Penumbra/UI/Classes/SubModEditWindow.cs index 8b562959..b56897f5 100644 --- a/Penumbra/UI/Classes/SubModEditWindow.cs +++ b/Penumbra/UI/Classes/SubModEditWindow.cs @@ -119,11 +119,11 @@ public class SubModEditWindow : Window ImGui.TableNextColumn(); ConfigWindow.Text( file.RelFile.Path ); ImGui.TableNextColumn(); - ImGui.Text( file.Size.ToString() ); + ImGui.TextUnformatted( file.Size.ToString() ); ImGui.TableNextColumn(); if( file.SubMods.Count == 0 ) { - ImGui.Text( "Unused" ); + ImGui.TextUnformatted( "Unused" ); } foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods ) @@ -135,7 +135,7 @@ public class SubModEditWindow : Window var text = groupIdx >= 0 ? $"{group!.Name} - {option.Name}" : option.Name; - ImGui.Text( text ); + ImGui.TextUnformatted( text ); ImGui.TableNextColumn(); ConfigWindow.Text( gamePath.Path ); } @@ -147,7 +147,7 @@ public class SubModEditWindow : Window ImGui.TableNextColumn(); ConfigWindow.Text( gamePath.Path ); ImGui.TableNextColumn(); - ImGui.Text( fullPath.FullName ); + ImGui.TextUnformatted( fullPath.FullName ); ImGui.TableNextColumn(); } } @@ -169,9 +169,9 @@ public class SubModEditWindow : Window foreach( var manip in _manipulations ) { ImGui.TableNextColumn(); - ImGui.Text( manip.ManipulationType.ToString() ); + ImGui.TextUnformatted( manip.ManipulationType.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( manip.ManipulationType switch + ImGui.TextUnformatted( manip.ManipulationType switch { MetaManipulation.Type.Imc => manip.Imc.ToString(), MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), @@ -182,7 +182,7 @@ public class SubModEditWindow : Window _ => string.Empty, } ); ImGui.TableNextColumn(); - ImGui.Text( manip.ManipulationType switch + ImGui.TextUnformatted( manip.ManipulationType switch { MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), @@ -214,7 +214,7 @@ public class SubModEditWindow : Window ImGui.TableNextColumn(); ConfigWindow.Text( from.Path ); ImGui.TableNextColumn(); - ImGui.Text( to.FullName ); + ImGui.TextUnformatted( to.FullName ); ImGui.TableNextColumn(); } } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index 6d4ed0ec..949b03b0 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -260,7 +260,7 @@ public partial class ConfigWindow { ImGui.SetDragDropPayload( InheritanceDragDropLabel, IntPtr.Zero, 0 ); _movedInheritance = collection; - ImGui.Text( $"Moving {_movedInheritance?.Name ?? "Unknown"}..." ); + ImGui.TextUnformatted( $"Moving {_movedInheritance?.Name ?? "Unknown"}..." ); } } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 76024c0e..8b2ff916 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -162,7 +162,7 @@ public partial class ConfigWindow ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - ImGui.Text( name ); + ImGui.TextUnformatted( name ); } DrawNewCharacterCollection(); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 85b713c7..2b153bd3 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -131,17 +131,17 @@ public partial class ConfigWindow var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; ImGui.TableNextColumn(); - ImGui.Text( data.ManipulatedPath.ToString() ); + ImGui.TextUnformatted( data.ManipulatedPath.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); + ImGui.TextUnformatted( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( refCountManip.ToString() ); + ImGui.TextUnformatted( refCountManip.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( data.OriginalPath.ToString() ); + ImGui.TextUnformatted( data.OriginalPath.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); + ImGui.TextUnformatted( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( refCountOrig.ToString() ); + ImGui.TextUnformatted( refCountOrig.ToString() ); } } @@ -163,15 +163,15 @@ public partial class ConfigWindow foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectToObject ) { ImGui.TableNextColumn(); - ImGui.Text( ptr.ToString( "X" ) ); + ImGui.TextUnformatted( ptr.ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( idx.ToString() ); + ImGui.TextUnformatted( idx.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + ImGui.TextUnformatted( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); ImGui.TableNextColumn(); - ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + ImGui.TextUnformatted( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); ImGui.TableNextColumn(); - ImGui.Text( c.Name ); + ImGui.TextUnformatted( c.Name ); } } } @@ -189,7 +189,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); ImGui.TableNextColumn(); - ImGui.Text( collection.Name ); + ImGui.TextUnformatted( collection.Name ); } } } @@ -216,7 +216,7 @@ public partial class ConfigWindow var idx = CharacterUtility.RelevantIndices[ i ]; var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); ImGui.TableNextColumn(); Text( resource ); ImGui.TableNextColumn(); @@ -234,7 +234,7 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); ImGui.TableNextColumn(); - ImGui.Text( $"{resource->GetData().Length}" ); + ImGui.TextUnformatted( $"{resource->GetData().Length}" ); ImGui.TableNextColumn(); ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); if( ImGui.IsItemClicked() ) @@ -247,7 +247,7 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); ImGui.TableNextColumn(); - ImGui.Text( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); + ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); } } @@ -275,7 +275,7 @@ public partial class ConfigWindow { var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; ImGui.TableNextColumn(); - ImGui.Text( $"0x{( ulong )resource:X}" ); + ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); ImGui.TableNextColumn(); Text( resource ); } @@ -319,9 +319,9 @@ public partial class ConfigWindow var imc = ( ResourceHandle* )model->IMCArray[ i ]; ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.Text( $"Slot {i}" ); + ImGui.TextUnformatted( $"Slot {i}" ); ImGui.TableNextColumn(); - ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); + ImGui.TextUnformatted( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); ImGui.TableNextColumn(); if( imc != null ) { @@ -330,7 +330,7 @@ public partial class ConfigWindow var mdl = ( RenderModel* )model->ModelArray[ i ]; ImGui.TableNextColumn(); - ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); + ImGui.TextUnformatted( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) { continue; @@ -367,15 +367,15 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - ImGui.Text( r->Category.ToString() ); + ImGui.TextUnformatted( r->Category.ToString() ); ImGui.TableNextColumn(); - ImGui.Text( r->FileType.ToString( "X" ) ); + ImGui.TextUnformatted( r->FileType.ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( r->Id.ToString( "X" ) ); + ImGui.TextUnformatted( r->Id.ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); + ImGui.TextUnformatted( ( ( ulong )r ).ToString( "X" ) ); ImGui.TableNextColumn(); - ImGui.Text( r->RefCount.ToString() ); + ImGui.TextUnformatted( r->RefCount.ToString() ); ImGui.TableNextColumn(); ref var name = ref r->FileName; if( name.Capacity > 15 ) @@ -402,52 +402,52 @@ public partial class ConfigWindow } var ipc = _window._penumbra.Ipc; - ImGui.Text( $"API Version: {ipc.Api.ApiVersion}" ); - ImGui.Text( "Available subscriptions:" ); + ImGui.TextUnformatted( $"API Version: {ipc.Api.ApiVersion}" ); + ImGui.TextUnformatted( "Available subscriptions:" ); using var indent = ImRaii.PushIndent(); if( ipc.ProviderApiVersion != null ) { - ImGui.Text( PenumbraIpc.LabelProviderApiVersion ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderApiVersion ); } if( ipc.ProviderRedrawName != null ) { - ImGui.Text( PenumbraIpc.LabelProviderRedrawName ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawName ); } if( ipc.ProviderRedrawObject != null ) { - ImGui.Text( PenumbraIpc.LabelProviderRedrawObject ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawObject ); } if( ipc.ProviderRedrawAll != null ) { - ImGui.Text( PenumbraIpc.LabelProviderRedrawAll ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawAll ); } if( ipc.ProviderResolveDefault != null ) { - ImGui.Text( PenumbraIpc.LabelProviderResolveDefault ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderResolveDefault ); } if( ipc.ProviderResolveCharacter != null ) { - ImGui.Text( PenumbraIpc.LabelProviderResolveCharacter ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderResolveCharacter ); } if( ipc.ProviderChangedItemTooltip != null ) { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemTooltip ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderChangedItemTooltip ); } if( ipc.ProviderChangedItemClick != null ) { - ImGui.Text( PenumbraIpc.LabelProviderChangedItemClick ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderChangedItemClick ); } if( ipc.ProviderGetChangedItems != null ) { - ImGui.Text( PenumbraIpc.LabelProviderGetChangedItems ); + ImGui.TextUnformatted( PenumbraIpc.LabelProviderGetChangedItems ); } } @@ -455,9 +455,9 @@ public partial class ConfigWindow private static void PrintValue( string name, string value ) { ImGui.TableNextColumn(); - ImGui.Text( name ); + ImGui.TextUnformatted( name ); ImGui.TableNextColumn(); - ImGui.Text( value ); + ImGui.TextUnformatted( value ); } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 850d4e85..cae9a870 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -51,7 +51,7 @@ public partial class ConfigWindow group.Dispose(); if( ImGui.GetItemRectSize() == Vector2.Zero ) { - ImGui.Text( "No actions available." ); + ImGui.TextUnformatted( "No actions available." ); } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 2d751625..f2c9ad8d 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -322,7 +322,7 @@ public partial class ConfigWindow _dragDropOptionIdx = optionIdx; } - ImGui.Text( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); + ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs index 6b91c1b0..6830884e 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs @@ -130,7 +130,7 @@ public partial class ConfigWindow ImGuiUtil.TextColored( Colors.MetaInfoText, "by " ); ImGui.SameLine(); style.Pop(); - ImGui.Text( _mod.Author ); + ImGui.TextUnformatted( _mod.Author ); } // Draw either a website button if the source is a valid website address, @@ -163,7 +163,7 @@ public partial class ConfigWindow ImGuiUtil.TextColored( Colors.MetaInfoText, "from " ); ImGui.SameLine(); style.Pop(); - ImGui.Text( _mod.Website ); + ImGui.TextUnformatted( _mod.Website ); } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index f4168fb3..197a846d 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -174,7 +174,7 @@ public partial class ConfigWindow } else { - ImGui.Text( group.Name ); + ImGui.TextUnformatted( group.Name ); } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index f8982852..a1081ac5 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -132,7 +132,7 @@ public partial class ConfigWindow ImGui.SameLine(); using var color = ImRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); - ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); + ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); indent.Push( 30f ); } From 81e93e0664b69de2213c9d297b2976f4eba611ff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Apr 2022 23:04:39 +0200 Subject: [PATCH 0160/2451] Let options keep visual ordering. --- Penumbra.GameData/Penumbra.GameData.csproj | 5 ++- Penumbra/Import/TexToolsImporter.ModPack.cs | 3 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 33 ++++++++++++----- Penumbra/Mods/Mod.Creation.cs | 6 +-- Penumbra/Mods/Mod.Files.cs | 30 ++++++++++++++- Penumbra/Mods/Mod.Meta.Migration.cs | 37 +++++++++++++++++-- Penumbra/Mods/Subclasses/IModGroup.cs | 16 ++++---- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 1 + 9 files changed, 105 insertions(+), 28 deletions(-) diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index c7dbc908..d2470349 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -12,6 +12,7 @@ bin\$(Configuration)\ true enable + true @@ -43,6 +44,8 @@ - + + false + diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index dadfe4ac..5533b75e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -173,7 +173,8 @@ public partial class TexToolsImporter ++_currentOptionIdx; } - Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority++, description.ToString(), options ); + Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority, groupPriority, description.ToString(), options ); + ++groupPriority; } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 98267ae7..ec1a4beb 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -55,7 +55,7 @@ public sealed partial class Mod return; } - group.DeleteFile( mod.BasePath ); + group.DeleteFile( mod.BasePath, groupIdx ); var _ = group switch { @@ -86,7 +86,7 @@ public sealed partial class Mod { var group = mod._groups[ groupIdx ]; mod._groups.RemoveAt( groupIdx ); - group.DeleteFile( mod.BasePath ); + group.DeleteFile( mod.BasePath, groupIdx ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); } @@ -400,19 +400,32 @@ public sealed partial class Mod private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) { // File deletion is handled in the actual function. - if( type != ModOptionChangeType.GroupDeleted ) + if( type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved ) { - IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath ); + mod.SaveAllGroups(); + } + else + { + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); } // State can not change on adding groups, as they have no immediate options. - mod.HasOptions = type switch + var unused = type switch { - ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption, - ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - _ => mod.HasOptions, + ModOptionChangeType.GroupAdded => mod.SetCounts(), + ModOptionChangeType.GroupDeleted => mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => mod.SetCounts(), + ModOptionChangeType.OptionDeleted => mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ), + ModOptionChangeType.OptionSwapsChanged => 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ), + ModOptionChangeType.OptionMetaChanged => 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ), + ModOptionChangeType.OptionUpdated => mod.SetCounts(), + ModOptionChangeType.DisplayChange => false, + _ => false, }; } } diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 79056ba2..211d2574 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -59,7 +59,7 @@ public partial class Mod // Create a file for an option group from given data. internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, - int priority, string desc, IEnumerable< ISubMod > subMods ) + int priority, int index, string desc, IEnumerable< ISubMod > subMods ) { switch( groupData.SelectionType ) { @@ -72,7 +72,7 @@ public partial class Mod Priority = priority, }; group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.SaveModGroup( group, baseFolder ); + IModGroup.SaveModGroup( group, baseFolder, index ); break; } case SelectType.Single: @@ -84,7 +84,7 @@ public partial class Mod Priority = priority, }; group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.SaveModGroup( group, baseFolder ); + IModGroup.SaveModGroup( group, baseFolder, index ); break; } } diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 0eee20b0..8d14f2ea 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.Util; namespace Penumbra.Mods; @@ -25,7 +27,7 @@ public partial class Mod public int TotalManipulations { get; private set; } public bool HasOptions { get; private set; } - private void SetCounts() + private bool SetCounts() { TotalFileCount = 0; TotalSwapCount = 0; @@ -40,6 +42,7 @@ public partial class Mod HasOptions = _groups.Any( o => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); + return true; } public IEnumerable< ISubMod > AllSubMods @@ -114,4 +117,29 @@ public partial class Mod } } } + + // Delete all existing group files and save them anew. + // Used when indices change in complex ways. + private void SaveAllGroups() + { + foreach( var file in GroupFiles ) + { + try + { + if( file.Exists ) + { + file.Delete(); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete outdated group file {file}:\n{e}" ); + } + } + + foreach( var (group, index) in _groups.WithIndex() ) + { + IModGroup.SaveModGroup( group, BasePath, index ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 59a67cab..2eae72d1 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; +using Penumbra.Util; namespace Penumbra.Mods; @@ -14,7 +16,36 @@ public sealed partial class Mod private static class Migration { public static bool Migrate( Mod mod, JObject json ) - => MigrateV0ToV1( mod, json ); + => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ); + + private static bool MigrateV1ToV2( Mod mod ) + { + if( mod.FileVersion > 1 ) + { + return false; + } + + foreach( var (group, index) in mod.GroupFiles.WithIndex().ToArray() ) + { + var newName = Regex.Replace( group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled ); + try + { + if( newName != group.Name ) + { + group.MoveTo( Path.Combine( group.DirectoryName ?? string.Empty, newName ), false ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); + } + } + + mod.FileVersion = 2; + mod.SaveMeta(); + + return true; + } private static bool MigrateV0ToV1( Mod mod, JObject json ) { @@ -50,9 +81,9 @@ public sealed partial class Mod } mod._default.IncorporateMetaChanges( mod.BasePath, true ); - foreach( var group in mod.Groups ) + foreach( var (group, index) in mod.Groups.WithIndex() ) { - IModGroup.SaveModGroup( group, mod.BasePath ); + IModGroup.SaveModGroup( group, mod.BasePath, index ); } // Delete meta files. diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 5dae5481..bea449ee 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -34,12 +34,12 @@ public interface IModGroup : IEnumerable< ISubMod > _ => false, }; - public string FileName( DirectoryInfo basePath ) - => Path.Combine( basePath.FullName, $"group_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); + public string FileName( DirectoryInfo basePath, int groupIdx ) + => Path.Combine( basePath.FullName, $"group_{groupIdx + 1:D3}_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); - public void DeleteFile( DirectoryInfo basePath ) + public void DeleteFile( DirectoryInfo basePath, int groupIdx ) { - var file = FileName( basePath ); + var file = FileName( basePath, groupIdx ); if( !File.Exists( file ) ) { return; @@ -48,7 +48,7 @@ public interface IModGroup : IEnumerable< ISubMod > try { File.Delete( file ); - PluginLog.Debug( "Deleted group file {File:l} for {GroupName:l}.", file, Name ); + PluginLog.Debug( "Deleted group file {File:l} for group {GroupIdx}: {GroupName:l}.", file, groupIdx + 1, Name ); } catch( Exception e ) { @@ -57,9 +57,9 @@ public interface IModGroup : IEnumerable< ISubMod > } } - public static void SaveModGroup( IModGroup group, DirectoryInfo basePath ) + public static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx ) { - var file = group.FileName( basePath ); + var file = group.FileName( basePath, groupIdx ); using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); using var writer = new StreamWriter( s ); using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; @@ -82,7 +82,7 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteEndArray(); j.WriteEndObject(); - PluginLog.Debug( "Saved group file {File:l} for {GroupName:l}.", file, group.Name ); + PluginLog.Debug( "Saved group file {File:l} for group {GroupIdx}: {GroupName:l}.", file, groupIdx + 1, group.Name ); } public IModGroup Convert( SelectType type ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index f2c9ad8d..eb64503a 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -259,7 +259,7 @@ public partial class ConfigWindow } ImGui.SameLine(); - var fileName = group.FileName( _mod.BasePath ); + var fileName = group.FileName( _mod.BasePath, groupIdx ); var fileExists = File.Exists( fileName ); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 197a846d..e3574c99 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -59,6 +59,7 @@ public partial class ConfigWindow DrawSingleGroup( _mod.Groups[ idx ], idx ); } + ImGui.Dummy( _window._defaultSpace ); for( var idx = 0; idx < _mod.Groups.Count; ++idx ) { DrawMultiGroup( _mod.Groups[ idx ], idx ); From e2a6274b33ea74a6234b23a8263532bb2055a776 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 May 2022 18:06:21 +0200 Subject: [PATCH 0161/2451] Add empty option for single select groups with empty options. More Editor stuff. --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 1 + Penumbra/Import/TexToolsImporter.ModPack.cs | 15 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 207 +++++++++++ Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 43 +++ Penumbra/Mods/Editor/Mod.Editor.Files.cs | 112 ++++++ Penumbra/Mods/Editor/Mod.Editor.Groups.cs | 73 ++++ Penumbra/Mods/Editor/Mod.Editor.cs | 57 ++++ Penumbra/Mods/{ => Editor}/ModCleanup.cs | 88 +---- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 43 ++- Penumbra/Mods/Mod.Creation.cs | 7 + Penumbra/Mods/Mod.Files.cs | 3 - Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 6 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Penumbra.csproj.DotSettings | 1 + Penumbra/UI/Classes/ModEditWindow.cs | 322 ++++++++++++++++++ Penumbra/UI/Classes/SubModEditWindow.cs | 225 ------------ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 18 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 1 + Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/Util/DictionaryExtensions.cs | 44 +++ 21 files changed, 937 insertions(+), 336 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Edit.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Files.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Groups.cs create mode 100644 Penumbra/Mods/Editor/Mod.Editor.cs rename Penumbra/Mods/{ => Editor}/ModCleanup.cs (89%) create mode 100644 Penumbra/UI/Classes/ModEditWindow.cs delete mode 100644 Penumbra/UI/Classes/SubModEditWindow.cs diff --git a/OtterGui b/OtterGui index cce4e9ed..5cb708ff 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cce4e9ed2cf5fa0068d6c8fadff5acd0d54f8359 +Subproject commit 5cb708ff692d397a9e71f3315d9d054f6558f42d diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index bb6bb826..81c0aef1 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -282,6 +282,7 @@ public partial class ModCollection if( recomputeList ) { + // TODO: Does not check if the option that was changed is actually enabled. foreach( var collection in this.Where( c => c.HasCache ) ) { if( collection[ mod.Index ].Settings is { Enabled: true } ) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 5533b75e..65b757c2 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -109,7 +109,8 @@ public partial class TexToolsImporter .Sum( page => page.ModGroups .Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 ) .Sum( group => group.OptionList - .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) ) ); + .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) + + ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) ); // Extended V2 mod packs contain multiple options that need to be handled separately. private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) @@ -173,6 +174,18 @@ public partial class TexToolsImporter ++_currentOptionIdx; } + // Handle empty options for single select groups without creating a folder for them. + // We only want one of those at most, and it should usually be the first option. + if( group.SelectionType == SelectType.Single ) + { + var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); + if( empty != null ) + { + _currentOptionName = empty.Name; + options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + } + } + Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority, groupPriority, description.ToString(), options ); ++groupPriority; } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs new file mode 100644 index 00000000..e8225db4 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private readonly SHA256 _hasher = SHA256.Create(); + private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); + + public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates + => _duplicates; + + public long SavedSpace { get; private set; } = 0; + + public bool DuplicatesFinished { get; private set; } = true; + + public void DeleteDuplicates() + { + if( !DuplicatesFinished || _duplicates.Count == 0 ) + { + return; + } + + foreach( var (set, _, _) in _duplicates ) + { + if( set.Length < 2 ) + { + continue; + } + + var remaining = set[ 0 ]; + foreach( var duplicate in set.Skip( 1 ) ) + { + HandleDuplicate( duplicate, remaining ); + } + } + _availableFiles.RemoveAll( p => !p.Item1.Exists ); + _duplicates.Clear(); + } + + public void Cancel() + { + DuplicatesFinished = true; + } + + private void HandleDuplicate( FullPath duplicate, FullPath remaining ) + { + void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) + { + var changes = false; + var dict = subMod.Files.ToDictionary( kvp => kvp.Key, + kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) ); + if( changes ) + { + Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict ); + } + } + + ApplyToAllOptions( _mod, HandleSubMod ); + + try + { + File.Delete( duplicate.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" ); + } + } + + + private FullPath ChangeDuplicatePath( FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes ) + { + if( !value.Equals( from ) ) + { + return value; + } + + changes = true; + PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to); + return to; + } + + + public void StartDuplicateCheck() + { + DuplicatesFinished = false; + Task.Run( CheckDuplicates ); + } + + private void CheckDuplicates() + { + _duplicates.Clear(); + SavedSpace = 0; + var list = new List< FullPath >(); + var lastSize = -1L; + foreach( var (p, size) in AvailableFiles ) + { + if( DuplicatesFinished ) + { + return; + } + + if( size == lastSize ) + { + list.Add( p ); + continue; + } + + if( list.Count >= 2 ) + { + CheckMultiDuplicates( list, lastSize ); + } + lastSize = size; + + list.Clear(); + list.Add( p ); + } + if( list.Count >= 2 ) + { + CheckMultiDuplicates( list, lastSize ); + } + + DuplicatesFinished = true; + } + + private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size ) + { + var hashes = list.Select( f => (f, ComputeHash(f)) ).ToList(); + while( hashes.Count > 0 ) + { + if( DuplicatesFinished ) + { + return; + } + + var set = new HashSet< FullPath > { hashes[ 0 ].Item1 }; + var hash = hashes[ 0 ]; + for( var j = 1; j < hashes.Count; ++j ) + { + if( DuplicatesFinished ) + { + return; + } + + if( CompareHashes( hash.Item2, hashes[ j ].Item2 ) && CompareFilesDirectly( hashes[ 0 ].Item1, hashes[ j ].Item1 ) ) + { + set.Add( hashes[ j ].Item1 ); + } + } + + hashes.RemoveAll( p => set.Contains(p.Item1) ); + if( set.Count > 1 ) + { + _duplicates.Add( (set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2) ); + SavedSpace += ( set.Count - 1 ) * size; + } + } + } + + private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 ) + { + if( !f1.Exists || !f2.Exists ) + return false; + + using var s1 = File.OpenRead( f1.FullName ); + using var s2 = File.OpenRead( f2.FullName ); + var buffer1 = stackalloc byte[256]; + var buffer2 = stackalloc byte[256]; + var span1 = new Span< byte >( buffer1, 256 ); + var span2 = new Span< byte >( buffer2, 256 ); + + while( true ) + { + var bytes1 = s1.Read( span1 ); + var bytes2 = s2.Read( span2 ); + if( bytes1 != bytes2 ) + return false; + + if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) ) + return false; + + if( bytes1 < 256 ) + return true; + } + } + + public static bool CompareHashes( byte[] f1, byte[] f2 ) + => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); + + public byte[] ComputeHash( FullPath f ) + { + using var stream = File.OpenRead( f.FullName ); + return _hasher.ComputeHash( stream ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs new file mode 100644 index 00000000..65596021 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private int _groupIdx = -1; + private int _optionIdx = 0; + + private IModGroup? _modGroup; + private SubMod _subMod; + + public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); + public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); + public readonly HashSet< MetaManipulation > CurrentManipulations = new(); + + public void SetSubMod( int groupIdx, int optionIdx ) + { + _groupIdx = groupIdx; + _optionIdx = optionIdx; + if( groupIdx >= 0 ) + { + _modGroup = _mod.Groups[ groupIdx ]; + _subMod = ( SubMod )_modGroup![ optionIdx ]; + } + else + { + _modGroup = null; + _subMod = _mod._default; + } + + CurrentFiles.SetTo( _subMod.Files ); + CurrentSwaps.SetTo( _subMod.FileSwaps ); + CurrentManipulations.Clear(); + CurrentManipulations.UnionWith( _subMod.Manipulations ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs new file mode 100644 index 00000000..e39e9138 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + // All files in subdirectories of the mod directory. + public IReadOnlyList< (FullPath, long) > AvailableFiles + => _availableFiles; + + private readonly List< (FullPath, long) > _availableFiles; + + // All files that are available but not currently used in any option. + private readonly SortedSet< FullPath > _unusedFiles; + + public IReadOnlySet< FullPath > UnusedFiles + => _unusedFiles; + + // All paths that are used in any option in the mod. + private readonly SortedSet< FullPath > _usedPaths; + + public IReadOnlySet< FullPath > UsedPaths + => _usedPaths; + + // All paths that are used in + private readonly SortedSet< FullPath > _missingPaths; + + public IReadOnlySet< FullPath > MissingPaths + => _missingPaths; + + // Adds all currently unused paths, relative to the mod directory, to the replacements. + public void AddUnusedPathsToDefault() + { + var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count ); + foreach( var file in UnusedFiles ) + { + var gamePath = file.ToGamePath( _mod.BasePath, out var g ) ? g : Utf8GamePath.Empty; + if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) ) + { + dict.Add( gamePath, file ); + PluginLog.Debug( "[AddUnusedPaths] Adding {GamePath} -> {File} to default option of {Mod}.", gamePath, file, _mod.Name ); + } + } + + Penumbra.ModManager.OptionAddFiles( _mod, -1, 0, dict ); + _usedPaths.UnionWith( _mod.Default.Files.Values ); + _unusedFiles.RemoveWhere( f => _mod.Default.Files.Values.Contains( f ) ); + } + + // Delete all currently unused paths from your filesystem. + public void DeleteUnusedPaths() + { + foreach( var file in UnusedFiles ) + { + try + { + File.Delete( file.FullName ); + PluginLog.Debug( "[DeleteUnusedPaths] Deleted {File} from {Mod}.", file, _mod.Name ); + } + catch( Exception e ) + { + PluginLog.Error($"[DeleteUnusedPaths] Could not delete {file} from {_mod.Name}:\n{e}" ); + } + } + + _unusedFiles.RemoveWhere( f => !f.Exists ); + _availableFiles.RemoveAll( p => !p.Item1.Exists ); + } + + // Remove all path redirections where the pointed-to file does not exist. + public void RemoveMissingPaths() + { + void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx ) + { + var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key ) ) + .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); + if( newDict.Count != mod.Files.Count ) + { + Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict ); + } + } + + ApplyToAllOptions( _mod, HandleSubMod ); + _usedPaths.RemoveWhere( _missingPaths.Contains ); + _missingPaths.Clear(); + } + + private bool CheckAgainstMissing( FullPath file, Utf8GamePath key ) + { + if( !_missingPaths.Contains( file ) ) + { + return true; + } + + PluginLog.Debug( "[RemoveMissingPaths] Removing {GamePath} -> {File} from {Mod}.", key, file, _mod.Name ); + return false; + } + + + private static List<(FullPath, long)> GetAvailablePaths( Mod mod ) + => mod.BasePath.EnumerateDirectories() + .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) ) + .OrderBy( p => -p.Length ).ToList(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs new file mode 100644 index 00000000..9f2a35e9 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + public void Normalize() + {} + + public void AutoGenerateGroups() + { + //ClearEmptySubDirectories( _mod.BasePath ); + //for( var i = _mod.Groups.Count - 1; i >= 0; --i ) + //{ + // if (_mod.Groups.) + // Penumbra.ModManager.DeleteModGroup( _mod, i ); + //} + //Penumbra.ModManager.OptionSetFiles( _mod, -1, 0, new Dictionary< Utf8GamePath, FullPath >() ); + // + //foreach( var groupDir in _mod.BasePath.EnumerateDirectories() ) + //{ + // var groupName = groupDir.Name; + // foreach( var optionDir in groupDir.EnumerateDirectories() ) + // { } + //} + + //var group = new OptionGroup + // { + // GroupName = groupDir.Name, + // SelectionType = SelectType.Single, + // Options = new List< Option >(), + // }; + // + // foreach( var optionDir in groupDir.EnumerateDirectories() ) + // { + // var option = new Option + // { + // OptionDesc = string.Empty, + // OptionName = optionDir.Name, + // OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), + // }; + // foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + // { + // if( Utf8RelPath.FromFile( file, baseDir, out var rel ) + // && Utf8GamePath.FromFile( file, optionDir, out var game ) ) + // { + // option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; + // } + // } + // + // if( option.OptionFiles.Count > 0 ) + // { + // group.Options.Add( option ); + // } + // } + // + // if( group.Options.Count > 0 ) + // { + // meta.Groups.Add( groupDir.Name, group ); + // } + //} + // + //var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); + //foreach( var collection in Penumbra.CollectionManager ) + //{ + // collection.Settings[ idx ]?.FixInvalidSettings( meta ); + //} + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs new file mode 100644 index 00000000..9007bb2d --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor : IDisposable + { + private readonly Mod _mod; + + public Editor( Mod mod ) + { + _mod = mod; + _availableFiles = GetAvailablePaths( mod ); + _usedPaths = new SortedSet< FullPath >( mod.AllFiles ); + _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); + _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); + _subMod = _mod._default; + } + + public void Dispose() + { + DuplicatesFinished = true; + } + + // Does not delete the base directory itself even if it is completely empty at the end. + private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) + { + foreach( var subDir in baseDir.GetDirectories() ) + { + ClearEmptySubDirectories( subDir ); + if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) + { + subDir.Delete(); + } + } + } + + // Apply a option action to all available option in a mod, including the default option. + private static void ApplyToAllOptions( Mod mod, Action< ISubMod, int, int > action ) + { + action( mod.Default, -1, 0 ); + foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) + { + for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + { + action( @group[ optionIdx ], groupIdx, optionIdx ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/Editor/ModCleanup.cs similarity index 89% rename from Penumbra/Mods/ModCleanup.cs rename to Penumbra/Mods/Editor/ModCleanup.cs index ed1afc6f..c19c1b1f 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/Editor/ModCleanup.cs @@ -1,17 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Import; -using Penumbra.Meta.Manipulations; -using Penumbra.Util; - namespace Penumbra.Mods; public partial class Mod @@ -202,15 +188,10 @@ public partial class Mod // // private readonly DirectoryInfo _baseDir; // private readonly ModMeta _mod; -// private SHA256? _hasher; + // // private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); // -// private SHA256 Sha() -// { -// _hasher ??= SHA256.Create(); -// return _hasher; -// } // // private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) // { @@ -366,47 +347,6 @@ public partial class Mod // return meta.Groups[ Duplicates ].Options.First(); // } // -// public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) -// { -// var dedup = new ModCleanup( baseDir, mod ); -// foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) -// { -// if( value.Count == 2 ) -// { -// if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) -// { -// dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); -// } -// } -// else -// { -// var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); -// var hashes = value.Select( dedup.ComputeHash ).ToArray(); -// -// for( var i = 0; i < value.Count; ++i ) -// { -// if( deleted[ i ] ) -// { -// continue; -// } -// -// for( var j = i + 1; j < value.Count; ++j ) -// { -// if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) -// { -// continue; -// } -// -// dedup.ReplaceFile( value[ i ], value[ j ] ); -// deleted[ j ] = true; -// } -// } -// } -// } -// -// CleanUpDuplicates( mod ); -// ClearEmptySubDirectories( dedup._baseDir ); -// } // // private void ReplaceFile( FileInfo f1, FileInfo f2 ) // { @@ -458,32 +398,8 @@ public partial class Mod // f2.Delete(); // } // -// public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) -// => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); // -// public static bool CompareHashes( byte[] f1, byte[] f2 ) -// => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); -// -// public byte[] ComputeHash( FileInfo f ) -// { -// var stream = File.OpenRead( f.FullName ); -// var ret = Sha().ComputeHash( stream ); -// stream.Dispose(); -// return ret; -// } -// -// // Does not delete the base directory itself even if it is completely empty at the end. -// public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) -// { -// foreach( var subDir in baseDir.GetDirectories() ) -// { -// ClearEmptySubDirectories( subDir ); -// if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) -// { -// subDir.Delete(); -// } -// } -// } + // // private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) // { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index ec1a4beb..ea30845e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -283,8 +283,7 @@ public sealed partial class Mod return; } - subMod.ManipulationData.Clear(); - subMod.ManipulationData.UnionWith( manipulations ); + subMod.ManipulationData = manipulations; ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } @@ -300,15 +299,26 @@ public sealed partial class Mod public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileData.Equals( replacements ) ) + if( subMod.FileData.SetEquals( replacements ) ) { return; } - subMod.FileData.SetTo( replacements ); + subMod.FileData = replacements; ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); } + public void OptionAddFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > additions ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom( additions ); + if( oldCount != subMod.FileData.Count ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + } + } + public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); @@ -321,12 +331,12 @@ public sealed partial class Mod public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileSwapData.Equals( swaps ) ) + if( subMod.FileSwapData.SetEquals( swaps ) ) { return; } - subMod.FileSwapData.SetTo( swaps ); + subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } @@ -334,10 +344,9 @@ public sealed partial class Mod HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - subMod.FileData.SetTo( replacements ); - subMod.ManipulationData.Clear(); - subMod.ManipulationData.UnionWith( manipulations ); - subMod.FileSwapData.SetTo( swaps ); + subMod.FileData = replacements; + subMod.ManipulationData = manipulations; + subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 ); } @@ -361,6 +370,11 @@ public sealed partial class Mod private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx ) { + if( groupIdx == -1 && optionIdx == 0 ) + { + return mod._default; + } + return mod._groups[ groupIdx ] switch { SingleModGroup s => s.OptionData[ optionIdx ], @@ -406,7 +420,14 @@ public sealed partial class Mod } else { - IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); + if( groupIdx == -1 ) + { + mod.SaveDefaultMod(); + } + else + { + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); + } } // State can not change on adding groups, as they have no immediate options. diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 211d2574..392e7751 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -110,6 +110,13 @@ public partial class Mod return mod; } + // Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod( string name ) + => new SubMod() + { + Name = name, + }; + // Create the default data file from all unused files that were not handled before // and are used in sub mods. internal static void CreateDefaultFiles( DirectoryInfo directory ) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 8d14f2ea..a3700ec8 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -78,9 +78,6 @@ public partial class Mod => !Penumbra.Config.DisableSoundStreaming && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - public List< FullPath > FindMissingFiles() - => AllFiles.Where( f => !f.Exists ).ToList(); - private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) { if( !File.Exists( file.FullName ) ) diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 3748fdeb..85379873 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -69,9 +69,9 @@ public partial class Mod { public string Name { get; set; } = "Default"; - public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); - public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - public readonly HashSet< MetaManipulation > ManipulationData = new(); + public Dictionary< Utf8GamePath, FullPath > FileData = new(); + public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public HashSet< MetaManipulation > ManipulationData = new(); public IReadOnlyDictionary< Utf8GamePath, FullPath > Files => FileData; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 94b22f86..a0f1025b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -129,7 +129,7 @@ public class Penumbra : IDalamudPlugin btn = new LaunchButton( _configWindow ); system = new WindowSystem( Name ); system.AddWindow( _configWindow ); - system.AddWindow( cfg.SubModPopup ); + system.AddWindow( cfg.ModEditPopup ); Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings index b43e7ec2..4e906820 100644 --- a/Penumbra/Penumbra.csproj.DotSettings +++ b/Penumbra/Penumbra.csproj.DotSettings @@ -1,3 +1,4 @@  + True True True \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs new file mode 100644 index 00000000..3dacf25b --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -0,0 +1,322 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.UI.Classes; + +public class ModEditWindow : Window, IDisposable +{ + private const string WindowBaseLabel = "###SubModEdit"; + private Mod.Editor? _editor; + private Mod? _mod; + + public void ChangeMod( Mod mod ) + { + if( mod == _mod ) + { + return; + } + + _editor?.Dispose(); + _editor = new Mod.Editor( mod ); + _mod = mod; + WindowName = $"{mod.Name}{WindowBaseLabel}"; + } + + public void ChangeOption( int groupIdx, int optionIdx ) + => _editor?.SetSubMod( groupIdx, optionIdx ); + + public override bool DrawConditions() + => _editor != null; + + public override void Draw() + { + using var tabBar = ImRaii.TabBar( "##tabs" ); + if( !tabBar ) + { + return; + } + + DrawFileTab(); + DrawMetaTab(); + DrawSwapTab(); + DrawMissingFilesTab(); + DrawUnusedFilesTab(); + DrawDuplicatesTab(); + } + + private void DrawMissingFilesTab() + { + using var tab = ImRaii.TabItem( "Missing Files" ); + if( !tab ) + { + return; + } + + if( _editor!.MissingPaths.Count == 0 ) + { + ImGui.TextUnformatted( "No missing files detected." ); + } + else + { + if( ImGui.Button( "Remove Missing Files from Mod" ) ) + { + _editor.RemoveMissingPaths(); + } + + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } + + foreach( var path in _editor.MissingPaths ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); + } + } + } + + private void DrawDuplicatesTab() + { + using var tab = ImRaii.TabItem( "Duplicates" ); + if( !tab ) + { + return; + } + + var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; + if( ImGuiUtil.DrawDisabledButton( buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", + !_editor.DuplicatesFinished ) ) + { + _editor.StartDuplicateCheck(); + } + + if( !_editor.DuplicatesFinished ) + { + ImGui.SameLine(); + if( ImGui.Button( "Cancel" ) ) + { + _editor.Cancel(); + } + + return; + } + + if( _editor.Duplicates.Count == 0 ) + { + ImGui.TextUnformatted( "No duplicates found." ); + } + + if( ImGui.Button( "Delete and Redirect Duplicates" ) ) + { + _editor.DeleteDuplicates(); + } + + if( _editor.SavedSpace > 0 ) + { + ImGui.SameLine(); + ImGui.TextUnformatted( $"Frees up {Functions.HumanReadableSize( _editor.SavedSpace )} from your hard drive." ); + } + + using var child = ImRaii.Child( "##duptable", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var width = ImGui.CalcTextSize( "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN " ).X; + ImGui.TableSetupColumn( "file", ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "NNN.NNN " ).X ); + ImGui.TableSetupColumn( "hash", ImGuiTableColumnFlags.WidthFixed, + ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize( "NNNNNNNN... " ).X ); + foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) ) + { + ImGui.TableNextColumn(); + using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], + ImGuiTreeNodeFlags.NoTreePushOnOpen ); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); + ImGui.TableNextColumn(); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + if( ImGui.GetWindowWidth() > 2 * width ) + { + ImGuiUtil.RightAlign( string.Concat( hash.Select( b => b.ToString( "X2" ) ) ) ); + } + else + { + ImGuiUtil.RightAlign( string.Concat( hash.Take( 4 ).Select( b => b.ToString( "X2" ) ) ) + "..." ); + } + } + + if( !tree ) + { + continue; + } + + using var indent = ImRaii.PushIndent(); + foreach( var duplicate in set.Skip( 1 ) ) + { + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + } + } + } + + private void DrawUnusedFilesTab() + { + using var tab = ImRaii.TabItem( "Unused Files" ); + if( !tab ) + { + return; + } + + if( _editor!.UnusedFiles.Count == 0 ) + { + ImGui.TextUnformatted( "No unused files detected." ); + } + else + { + if( ImGui.Button( "Add Unused Files to Default" ) ) + { + _editor.AddUnusedPathsToDefault(); + } + + if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) + { + _editor.DeleteUnusedPaths(); + } + + using var table = ImRaii.Table( "##unusedFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } + + foreach( var path in _editor.UnusedFiles ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); + } + } + } + + + private void DrawFileTab() + { + using var tab = ImRaii.TabItem( "File Redirections" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##files", 2 ); + if( !list ) + { + return; + } + + foreach( var (gamePath, file) in _editor!.CurrentFiles ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( file.FullName ); + } + } + + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem( "Meta Manipulations" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##meta", 3 ); + if( !list ) + { + return; + } + + foreach( var manip in _editor!.CurrentManipulations ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.ToString(), + MetaManipulation.Type.Est => manip.Est.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.ToString(), + _ => string.Empty, + } ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( manip.ManipulationType switch + { + MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), + MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), + MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), + MetaManipulation.Type.Est => manip.Est.Entry.ToString(), + MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), + MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), + _ => string.Empty, + } ); + } + } + + private void DrawSwapTab() + { + using var tab = ImRaii.TabItem( "File Swaps" ); + if( !tab ) + { + return; + } + + using var list = ImRaii.Table( "##swaps", 3 ); + if( !list ) + { + return; + } + + foreach( var (gamePath, file) in _editor!.CurrentSwaps ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( file.FullName ); + } + } + + public ModEditWindow() + : base( WindowBaseLabel ) + { } + + public void Dispose() + { + _editor?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/SubModEditWindow.cs b/Penumbra/UI/Classes/SubModEditWindow.cs deleted file mode 100644 index b56897f5..00000000 --- a/Penumbra/UI/Classes/SubModEditWindow.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using OtterGui.Raii; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.Util; - -namespace Penumbra.UI.Classes; - -public class SubModEditWindow : Window -{ - private const string WindowBaseLabel = "###SubModEdit"; - private Mod? _mod; - private int _groupIdx = -1; - private int _optionIdx = -1; - private IModGroup? _group; - private ISubMod? _subMod; - private readonly List< FilePathInfo > _availableFiles = new(); - - private readonly struct FilePathInfo - { - public readonly FullPath File; - public readonly Utf8RelPath RelFile; - public readonly long Size; - public readonly List< (int, int, Utf8GamePath) > SubMods; - - public FilePathInfo( FileInfo file, Mod mod ) - { - File = new FullPath( file ); - RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty; - Size = file.Length; - SubMods = new List< (int, int, Utf8GamePath) >(); - var path = File; - foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) - { - foreach( var (subMod, optionIdx) in group.WithIndex() ) - { - SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) ); - } - } - SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) ); - } - } - - private readonly HashSet< MetaManipulation > _manipulations = new(); - private readonly Dictionary< Utf8GamePath, FullPath > _files = new(); - private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new(); - - public void Activate( Mod mod, int groupIdx, int optionIdx ) - { - IsOpen = true; - _mod = mod; - _groupIdx = groupIdx; - _group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null; - _optionIdx = optionIdx; - _subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default; - _availableFiles.Clear(); - _availableFiles.AddRange( mod.BasePath.EnumerateDirectories() - .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .Select( f => new FilePathInfo( f, _mod ) ) ); - - _manipulations.Clear(); - _manipulations.UnionWith( _subMod.Manipulations ); - _files.SetTo( _subMod.Files ); - _fileSwaps.SetTo( _subMod.FileSwaps ); - - WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}"; - } - - public override bool DrawConditions() - => _subMod != null; - - public override void Draw() - { - using var tabBar = ImRaii.TabBar( "##tabs" ); - if( !tabBar ) - { - return; - } - - DrawFileTab(); - DrawMetaTab(); - DrawSwapTab(); - } - - private void Save() - { - if( _mod != null ) - { - Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps ); - } - } - - public override void OnClose() - { - _subMod = null; - } - - private void DrawFileTab() - { - using var tab = ImRaii.TabItem( "File Redirections" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##files", 3 ); - if( !list ) - { - return; - } - - foreach( var file in _availableFiles ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( file.RelFile.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.Size.ToString() ); - ImGui.TableNextColumn(); - if( file.SubMods.Count == 0 ) - { - ImGui.TextUnformatted( "Unused" ); - } - - foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods ) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null; - var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default; - var text = groupIdx >= 0 - ? $"{group!.Name} - {option.Name}" - : option.Name; - ImGui.TextUnformatted( text ); - ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); - } - } - - ImGui.TableNextRow(); - foreach( var (gamePath, fullPath) in _files ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( fullPath.FullName ); - ImGui.TableNextColumn(); - } - } - - private void DrawMetaTab() - { - using var tab = ImRaii.TabItem( "Meta Manipulations" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##meta", 3 ); - if( !list ) - { - return; - } - - foreach( var manip in _manipulations ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.ToString(), - MetaManipulation.Type.Est => manip.Est.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.ToString(), - _ => string.Empty, - } ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), - MetaManipulation.Type.Est => manip.Est.Entry.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), - _ => string.Empty, - } ); - } - } - - private void DrawSwapTab() - { - using var tab = ImRaii.TabItem( "File Swaps" ); - if( !tab ) - { - return; - } - - using var list = ImRaii.Table( "##swaps", 3 ); - if( !list ) - { - return; - } - - foreach( var (from, to) in _fileSwaps ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( from.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( to.FullName ); - ImGui.TableNextColumn(); - } - } - - public SubModEditWindow() - : base( WindowBaseLabel ) - { } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index eb64503a..a453449b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -158,7 +158,9 @@ public partial class ConfigWindow if( ImGui.Button( "Edit Default Mod", reducedSize ) ) { - _window.SubModPopup.Activate( _mod, -1, 0 ); + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( -1, 0 ); + _window.ModEditPopup.IsOpen = true; } ImGui.SameLine(); @@ -180,6 +182,7 @@ public partial class ConfigWindow private int _currentField = -1; private int _optionIndex = -1; + private int _newOptionNameIdx = -1; private string _newGroupName = string.Empty; private string _newOptionName = string.Empty; private string _newDescription = string.Empty; @@ -287,10 +290,15 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 ); + var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; + if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) ) + { + _newOptionName = tmp; + _newOptionNameIdx = groupIdx; + } ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, - "Add a new option to this group.", _newOptionName.Length == 0, true ) ) + "Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) ) { Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName ); _newOptionName = string.Empty; @@ -385,7 +393,9 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, "Edit this option.", false, true ) ) { - _window.SubModPopup.Activate( _mod, groupIdx, optionIdx ); + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); + _window.ModEditPopup.IsOpen = true; } ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index e3574c99..b9d80489 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -105,6 +105,7 @@ public partial class ConfigWindow ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) { + _currentPriority = priority; } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 48fd594a..1652c414 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -21,7 +21,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly EffectiveTab _effectiveTab; private readonly DebugTab _debugTab; private readonly ResourceTab _resourceTab; - public readonly SubModEditWindow SubModPopup = new(); + public readonly ModEditWindow ModEditPopup = new(); public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) @@ -70,6 +70,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _selector.Dispose(); _modPanel.Dispose(); + ModEditPopup.Dispose(); } private static string GetLabel() diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 97e2638a..2aeb44e5 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; namespace Penumbra.Util; @@ -7,6 +10,12 @@ public static class DictionaryExtensions // Returns whether two dictionaries contain equal keys and values. public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) { + if( ReferenceEquals( lhs, rhs ) ) + { + return true; + } + + if( lhs.Count != rhs.Count ) { return false; @@ -42,6 +51,11 @@ public static class DictionaryExtensions public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) where TKey : notnull { + if( ReferenceEquals( lhs, rhs ) ) + { + return; + } + lhs.Clear(); lhs.EnsureCapacity( rhs.Count ); foreach( var (key, value) in rhs ) @@ -49,4 +63,34 @@ public static class DictionaryExtensions lhs.Add( key, value ); } } + + // Add all entries from the other dictionary that would not overwrite current keys. + public static void AddFrom< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + where TKey : notnull + { + if( ReferenceEquals( lhs, rhs ) ) + { + return; + } + + lhs.EnsureCapacity( lhs.Count + rhs.Count ); + foreach( var (key, value) in rhs ) + { + lhs.Add( key, value ); + } + } + + public static int ReplaceValue< TKey, TValue >( this Dictionary< TKey, TValue > dict, TValue from, TValue to ) + where TKey : notnull + where TValue : IEquatable< TValue > + { + var count = 0; + foreach( var (key, _) in dict.ToArray().Where( kvp => kvp.Value.Equals( from ) ) ) + { + dict[ key ] = to; + ++count; + } + + return count; + } } \ No newline at end of file From c416d044a45fcb00abaa9b7d194639254e9ab7d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 May 2022 12:53:53 +0200 Subject: [PATCH 0162/2451] Add material changing. --- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 82 ++++++++++++++++------- Penumbra/Util/ModelChanger.cs | 11 ++- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index a453449b..b465bab2 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -4,10 +4,12 @@ using System.Diagnostics; using System.IO; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI; @@ -15,27 +17,10 @@ public partial class ConfigWindow { private partial class ModPanel { - private readonly Queue< Action > _delayedActions = new(); - - private void DrawAddOptionGroupInput() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); - ImGui.SameLine(); - - var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false ); - var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, - tt, !nameValid, true ) ) - { - Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName ); - _newGroupName = string.Empty; - } - } - private Vector2 _cellPadding = Vector2.Zero; private Vector2 _itemSpacing = Vector2.Zero; + // Draw the edit tab that contains all things concerning editing the mod. private void DrawEditModTab() { using var tab = DrawTab( EditModTabHeader, Tabs.Edit ); @@ -75,6 +60,57 @@ public partial class ConfigWindow EditDescriptionPopup(); } + // Do some edits outside of iterations. + private readonly Queue< Action > _delayedActions = new(); + + // Text input to add a new option group at the end of the current groups. + private void DrawAddOptionGroupInput() + { + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); + ImGui.SameLine(); + + var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false ); + var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, + tt, !nameValid, true ) ) + { + Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName ); + _newGroupName = string.Empty; + } + } + + + private string _materialSuffixFrom = string.Empty; + private string _materialSuffixTo = string.Empty; + + // A row of three buttonSizes and a help marker that can be used for material suffix changing. + private void DrawChangeMaterialSuffix( Vector2 buttonSize ) + { + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SameLine(); + var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo ); + var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." + : _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical." + : disabled ? "The suffices are not valid suffices." + : _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target." + : $"Convert all skin material suffices that are currently {_materialSuffixFrom} to {_materialSuffixTo}."; + if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) + { + ModelChanger.ChangeModMaterials( _mod, _materialSuffixFrom, _materialSuffixTo ); + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + } + private void EditButtons() { var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 ); @@ -93,14 +129,7 @@ public partial class ConfigWindow ImGui.SameLine(); ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Not implemented yet", true ); - ImGuiUtil.DrawDisabledButton( "Deduplicate", buttonSize, "Not implemented yet", true ); - ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Normalize", buttonSize, "Not implemented yet", true ); - ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Auto-Create Groups", buttonSize, "Not implemented yet", true ); - - ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, "Not implemented yet", true ); - + DrawChangeMaterialSuffix( buttonSize ); ImGui.Dummy( _window._defaultSpace ); } @@ -296,6 +325,7 @@ public partial class ConfigWindow _newOptionName = tmp; _newOptionNameIdx = groupIdx; } + ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, "Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) ) diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 25cc6df8..3c900bf4 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -16,9 +16,9 @@ public static class ModelChanger public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); + // Non-ASCII encoding can not be used. public static bool ValidStrings( string from, string to ) - => from.Length != 0 - && to.Length != 0 + => to.Length != 0 && from.Length < 16 && to.Length < 16 && from != to @@ -26,10 +26,11 @@ public static class ModelChanger && Encoding.UTF8.GetByteCount( to ) == to.Length; - [Conditional( "DEBUG" )] + [Conditional( "FALSE" )] private static void WriteBackup( string name, byte[] text ) => File.WriteAllBytes( name + ".bak", text ); + // Change material suffices for a single mdl file. public static int ChangeMtrl( FullPath file, string from, string to ) { if( !file.Exists ) @@ -41,6 +42,9 @@ public static class ModelChanger { var data = File.ReadAllBytes( file.FullName ); var mdlFile = new MdlFile( data ); + + // If from is empty, match with any current material suffix, + // otherwise check for exact matches with from. Func< string, bool > compare = MaterialRegex.IsMatch; if( from.Length > 0 ) { @@ -59,6 +63,7 @@ public static class ModelChanger } } + // Only rewrite the file if anything was changed. if( replaced > 0 ) { WriteBackup( file.FullName, data ); From 65bbece9cf3973ecd75ddb3f3a51f0fe2d00da86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 May 2022 16:19:24 +0200 Subject: [PATCH 0163/2451] Rename Mod BasePath to ModPath, add simple Directory Renaming and Reloading, some fixes, Cleanup EditWindow. --- Penumbra/Api/ModsController.cs | 4 +- Penumbra/Collections/CollectionManager.cs | 3 + Penumbra/Collections/ModCollection.File.cs | 2 +- Penumbra/Collections/ModCollection.cs | 8 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 4 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 149 +++- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 6 +- Penumbra/Mods/Mod.BasePath.cs | 48 +- Penumbra/Mods/Mod.Creation.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 8 +- Penumbra/Mods/Mod.Meta.Migration.cs | 14 +- Penumbra/Mods/Mod.Meta.cs | 4 +- Penumbra/Mods/ModFileSystem.cs | 5 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.cs | 4 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 730 ++++++++++-------- 17 files changed, 636 insertions(+), 368 deletions(-) diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 77f1b496..c8254c42 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -20,9 +20,9 @@ public class ModsController : WebApiController { x.Second?.Enabled, x.Second?.Priority, - FolderName = x.First.BasePath.Name, + FolderName = x.First.ModPath.Name, x.First.Name, - BasePath = x.First.BasePath.FullName, + BasePath = x.First.ModPath.FullName, Files = x.First.AllFiles, } ); } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 81c0aef1..775a0b2c 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -239,6 +239,9 @@ public partial class ModCollection collection.Save(); } + OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); + break; + case ModPathChangeType.Reloaded: OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 4eb2cca0..156e540f 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -47,7 +47,7 @@ public partial class ModCollection var settings = _settings[ i ]; if( settings != null ) { - j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); + j.WritePropertyName( Penumbra.ModManager[ i ].ModPath.Name ); x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 54561930..7d82e28b 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -92,11 +92,11 @@ public partial class ModCollection // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. private bool AddMod( Mod mod ) { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) ) + if( _unusedSettings.TryGetValue( mod.ModPath.Name, out var save ) ) { var ret = save.ToSettings( mod, out var settings ); _settings.Add( settings ); - _unusedSettings.Remove( mod.BasePath.Name ); + _unusedSettings.Remove( mod.ModPath.Name ); return ret; } @@ -110,7 +110,7 @@ public partial class ModCollection var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings.Add( mod.BasePath.Name, new ModSettings.SavedSettings( settings, mod ) ); + _unusedSettings.Add( mod.ModPath.Name, new ModSettings.SavedSettings( settings, mod ) ); } _settings.RemoveAt( idx ); @@ -131,7 +131,7 @@ public partial class ModCollection { foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) { - _unusedSettings[ mod.BasePath.Name ] = new ModSettings.SavedSettings( setting!, mod ); + _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( setting!, mod ); } _settings.Clear(); diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index e39e9138..e823be02 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -41,7 +41,7 @@ public partial class Mod var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count ); foreach( var file in UnusedFiles ) { - var gamePath = file.ToGamePath( _mod.BasePath, out var g ) ? g : Utf8GamePath.Empty; + var gamePath = file.ToGamePath( _mod.ModPath, out var g ) ? g : Utf8GamePath.Empty; if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) ) { dict.Add( gamePath, file ); @@ -105,7 +105,7 @@ public partial class Mod private static List<(FullPath, long)> GetAvailablePaths( Mod mod ) - => mod.BasePath.EnumerateDirectories() + => mod.ModPath.EnumerateDirectories() .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) ) .OrderBy( p => -p.Length ).ToList(); } diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index d82216ec..c82f09e6 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -16,10 +16,86 @@ public partial class Mod // Rename/Move a mod directory. // Updates all collection settings and sort order settings. - public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) + public void MoveModDirectory( int idx, string newName ) { - var mod = this[ idx ]; - // TODO + var mod = this[ idx ]; + var oldName = mod.Name; + var oldDirectory = mod.ModPath; + + switch( NewDirectoryValid( oldDirectory.Name, newName, out var dir ) ) + { + case NewDirectoryState.NonExisting: + // Nothing to do + break; + case NewDirectoryState.ExistsEmpty: + try + { + Directory.Delete( dir!.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" ); + return; + } + + break; + // Should be caught beforehand. + case NewDirectoryState.ExistsNonEmpty: + case NewDirectoryState.ExistsAsFile: + case NewDirectoryState.ContainsInvalidSymbols: + // Nothing to do at all. + case NewDirectoryState.Identical: + default: + return; + } + + try + { + Directory.Move( oldDirectory.FullName, dir!.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" ); + return; + } + + dir.Refresh(); + mod.ModPath = dir; + if( !mod.Reload( out var metaChange ) ) + { + PluginLog.Error( $"Error reloading moved mod {mod.Name}." ); + return; + } + + ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath ); + if( metaChange != MetaChangeType.None ) + { + ModMetaChanged?.Invoke( metaChange, mod, oldName ); + } + } + + // Reload a mod without changing its base directory. + // If the base directory does not exist anymore, the mod will be deleted. + public void ReloadMod( int idx ) + { + var mod = this[ idx ]; + var oldName = mod.Name; + + if( !mod.Reload( out var metaChange ) ) + { + PluginLog.Warning( mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead." ); + + DeleteMod( idx ); + return; + } + + ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); + if( metaChange != MetaChangeType.None ) + { + ModMetaChanged?.Invoke( metaChange, mod, oldName ); + } } // Delete a mod by its index. @@ -28,16 +104,16 @@ public partial class Mod public void DeleteMod( int idx ) { var mod = this[ idx ]; - if( Directory.Exists( mod.BasePath.FullName ) ) + if( Directory.Exists( mod.ModPath.FullName ) ) { try { - Directory.Delete( mod.BasePath.FullName, true ); - PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.BasePath.FullName, mod.Name ); + Directory.Delete( mod.ModPath.FullName, true ); + PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.ModPath.FullName, mod.Name ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" ); + PluginLog.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" ); } } @@ -47,14 +123,14 @@ public partial class Mod --remainingMod.Index; } - ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null ); PluginLog.Debug( "Deleted mod {Name:l}.", mod.Name ); } // Load a new mod and add it to the manager if successful. public void AddMod( DirectoryInfo modFolder ) { - if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) + if( _mods.Any( m => m.ModPath.Name == modFolder.Name ) ) { return; } @@ -67,10 +143,61 @@ public partial class Mod mod.Index = _mods.Count; _mods.Add( mod ); - ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath ); - PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName ); + ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.ModPath ); + PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName ); } + public enum NewDirectoryState + { + NonExisting, + ExistsEmpty, + ExistsNonEmpty, + ExistsAsFile, + ContainsInvalidSymbols, + Identical, + Empty, + } + + // Return the state of the new potential name of a directory. + public static NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) + { + directory = null; + if( newName.Length == 0 ) + { + return NewDirectoryState.Empty; + } + + if( oldName == newName ) + { + return NewDirectoryState.Identical; + } + + var fixedNewName = ReplaceBadXivSymbols( newName ); + if( fixedNewName != newName ) + { + return NewDirectoryState.ContainsInvalidSymbols; + } + + directory = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, fixedNewName ) ); + if( File.Exists( directory.FullName ) ) + { + return NewDirectoryState.ExistsAsFile; + } + + if( !Directory.Exists( directory.FullName ) ) + { + return NewDirectoryState.NonExisting; + } + + if( directory.EnumerateFileSystemInfos().Any() ) + { + return NewDirectoryState.ExistsNonEmpty; + } + + return NewDirectoryState.ExistsEmpty; + } + + // Add new mods to NewMods and remove deleted mods from NewMods. private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index ea30845e..bfe29336 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -55,7 +55,7 @@ public sealed partial class Mod return; } - group.DeleteFile( mod.BasePath, groupIdx ); + group.DeleteFile( mod.ModPath, groupIdx ); var _ = group switch { @@ -86,7 +86,7 @@ public sealed partial class Mod { var group = mod._groups[ groupIdx ]; mod._groups.RemoveAt( groupIdx ); - group.DeleteFile( mod.BasePath, groupIdx ); + group.DeleteFile( mod.ModPath, groupIdx ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); } @@ -426,7 +426,7 @@ public sealed partial class Mod } else { - IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx ); + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.ModPath, groupIdx ); } } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 24cf84f1..1e37b143 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -8,37 +8,53 @@ public enum ModPathChangeType Added, Deleted, Moved, + Reloaded, } public partial class Mod { - public DirectoryInfo BasePath { get; private set; } + public DirectoryInfo ModPath { get; private set; } public int Index { get; private set; } = -1; - private Mod( DirectoryInfo basePath ) - => BasePath = basePath; + private Mod( DirectoryInfo modPath ) + => ModPath = modPath; - private static Mod? LoadMod( DirectoryInfo basePath ) + private static Mod? LoadMod( DirectoryInfo modPath ) { - basePath.Refresh(); - if( !basePath.Exists ) + modPath.Refresh(); + if( !modPath.Exists ) { - PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + PluginLog.Error( $"Supplied mod directory {modPath} does not exist." ); return null; } - var mod = new Mod( basePath ); - mod.LoadMeta(); - if( mod.Name.Length == 0 ) + var mod = new Mod( modPath ); + if( !mod.Reload(out _) ) { - PluginLog.Error( $"Mod at {basePath} without name is not supported." ); + // Can not be base path not existing because that is checked before. + PluginLog.Error( $"Mod at {modPath} without name is not supported." ); } - mod.LoadDefaultOption(); - mod.LoadAllGroups(); - mod.ComputeChangedItems(); - mod.SetCounts(); - return mod; } + + private bool Reload(out MetaChangeType metaChange) + { + metaChange = MetaChangeType.Deletion; + ModPath.Refresh(); + if( !ModPath.Exists ) + return false; + + metaChange = LoadMeta(); + if( metaChange.HasFlag(MetaChangeType.Deletion) || Name.Length == 0 ) + { + return false; + } + + LoadDefaultOption(); + LoadAllGroups(); + ComputeChangedItems(); + SetCounts(); + return true; + } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 392e7751..d3f8b33d 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -141,7 +141,7 @@ public partial class Mod // XIV can not deal with non-ascii symbols in a path, // and the path must obviously be valid itself. - private static string ReplaceBadXivSymbols( string s, string replacement = "_" ) + public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) { StringBuilder sb = new(s.Length); foreach( var c in s ) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a3700ec8..c66ab9a4 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -59,12 +59,12 @@ public partial class Mod .Select( p => p.Value ); public IEnumerable< FileInfo > GroupFiles - => BasePath.EnumerateFiles( "group_*.json" ); + => ModPath.EnumerateFiles( "group_*.json" ); public List< FullPath > FindUnusedFiles() { var modFiles = AllFiles.ToHashSet(); - return BasePath.EnumerateDirectories() + return ModPath.EnumerateDirectories() .SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) ) .Select( f => new FullPath( f ) ) .Where( f => !modFiles.Contains( f ) ) @@ -107,7 +107,7 @@ public partial class Mod _groups.Clear(); foreach( var file in GroupFiles ) { - var group = LoadModGroup( file, BasePath ); + var group = LoadModGroup( file, ModPath ); if( group != null ) { _groups.Add( group ); @@ -136,7 +136,7 @@ public partial class Mod foreach( var (group, index) in _groups.WithIndex() ) { - IModGroup.SaveModGroup( group, BasePath, index ); + IModGroup.SaveModGroup( group, ModPath, index ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 2eae72d1..dd4e79c8 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -66,7 +66,7 @@ public sealed partial class Mod foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) ) { - if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) + if( unusedFile.ToGamePath( mod.ModPath, out var gamePath ) && !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) { PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); @@ -80,10 +80,10 @@ public sealed partial class Mod mod._default.FileSwapData.Add( gamePath, swapPath ); } - mod._default.IncorporateMetaChanges( mod.BasePath, true ); + mod._default.IncorporateMetaChanges( mod.ModPath, true ); foreach( var (group, index) in mod.Groups.WithIndex() ) { - IModGroup.SaveModGroup( group, mod.BasePath, index ); + IModGroup.SaveModGroup( group, mod.ModPath, index ); } // Delete meta files. @@ -100,7 +100,7 @@ public sealed partial class Mod } // Delete old meta files. - var oldMetaFile = Path.Combine( mod.BasePath.FullName, "metadata_manipulations.json" ); + var oldMetaFile = Path.Combine( mod.ModPath.FullName, "metadata_manipulations.json" ); if( File.Exists( oldMetaFile ) ) { try @@ -141,14 +141,14 @@ public sealed partial class Mod mod._groups.Add( newMultiGroup ); foreach( var option in group.Options ) { - newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option, seenMetaFiles ), optionPriority++ ) ); + newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.ModPath, option, seenMetaFiles ), optionPriority++ ) ); } break; case SelectType.Single: if( group.Options.Count == 1 ) { - AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ], seenMetaFiles ); + AddFilesToSubMod( mod._default, mod.ModPath, group.Options[ 0 ], seenMetaFiles ); return; } @@ -161,7 +161,7 @@ public sealed partial class Mod mod._groups.Add( newSingleGroup ); foreach( var option in group.Options ) { - newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option, seenMetaFiles ) ); + newSingleGroup.OptionData.Add( SubModFromOption( mod.ModPath, option, seenMetaFiles ) ); } break; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index b5ccc304..c9493819 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -34,14 +34,14 @@ public sealed partial class Mod public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); internal FileInfo MetaFile - => new(Path.Combine( BasePath.FullName, "meta.json" )); + => new(Path.Combine( ModPath.FullName, "meta.json" )); private MetaChangeType LoadMeta() { var metaFile = MetaFile; if( !File.Exists( metaFile.FullName ) ) { - PluginLog.Debug( "No mod meta found for {ModLocation}.", BasePath.Name ); + PluginLog.Debug( "No mod meta found for {ModLocation}.", ModPath.Name ); return MetaChangeType.Deletion; } diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index db18f049..75034247 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -102,12 +102,15 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable case ModPathChangeType.Moved: Save(); break; + case ModPathChangeType.Reloaded: + // Nothing + break; } } // Used for saving and loading. private static string ModToIdentifier( Mod mod ) - => mod.BasePath.Name; + => mod.ModPath.Name; private static string ModToName( Mod mod ) => mod.Name.Text.FixName(); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 85379873..22d2aaee 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -14,7 +14,7 @@ namespace Penumbra.Mods; public partial class Mod { internal string DefaultFile - => Path.Combine( BasePath.FullName, "default_mod.json" ); + => Path.Combine( ModPath.FullName, "default_mod.json" ); // The default mod contains setting-independent sets of file replacements, file swaps and meta changes. // Every mod has an default mod, though it may be empty. @@ -33,7 +33,7 @@ public partial class Mod { Formatting = Formatting.Indented, }; - ISubMod.WriteSubMod( j, serializer, _default, BasePath, 0 ); + ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 ); } private void LoadDefaultOption() @@ -43,11 +43,11 @@ public partial class Mod { if( !File.Exists( defaultFile ) ) { - _default.Load( BasePath, new JObject(), out _ ); + _default.Load( ModPath, new JObject(), out _ ); } else { - _default.Load( BasePath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ ); + _default.Load( ModPath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ ); } } catch( Exception e ) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 3dacf25b..46f19f69 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -147,7 +147,7 @@ public class ModEditWindow : Window, IDisposable foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) ) { ImGui.TableNextColumn(); - using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], + using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.NoTreePushOnOpen ); ImGui.TableNextColumn(); ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); @@ -174,7 +174,7 @@ public class ModEditWindow : Window, IDisposable { ImGui.TableNextColumn(); ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); - using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); + using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); ImGui.TableNextColumn(); ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 4c061110..92b27f50 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -137,6 +137,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); + Mod.CreateDefaultFiles( newDir ); Penumbra.ModManager.AddMod( newDir ); _newModName = string.Empty; } @@ -341,7 +342,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void StoreCurrentSelection() { - _lastSelectedDirectory = Selected?.BasePath.FullName ?? string.Empty; + _lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty; ClearSelection(); } @@ -350,7 +351,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod if( _lastSelectedDirectory.Length > 0 ) { base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) - .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); + .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); OnSelectionChange( null, base.SelectedLeaf?.Value, default ); _lastSelectedDirectory = string.Empty; } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index b465bab2..3ac1b344 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -42,13 +42,14 @@ public partial class ConfigWindow EditRegularMeta(); ImGui.Dummy( _window._defaultSpace ); - if( TextInput( "Mod Path", PathFieldIdx, NoFieldIdx, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) ) + if( Input.Text( "Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, + _window._inputTextWidth.X ) ) { _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); } ImGui.Dummy( _window._defaultSpace ); - DrawAddOptionGroupInput(); + AddOptionGroup.Draw( _window, _mod ); ImGui.Dummy( _window._defaultSpace ); for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx ) @@ -57,110 +58,59 @@ public partial class ConfigWindow } EndActions(); - EditDescriptionPopup(); - } - - // Do some edits outside of iterations. - private readonly Queue< Action > _delayedActions = new(); - - // Text input to add a new option group at the end of the current groups. - private void DrawAddOptionGroupInput() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); - ImGui.SameLine(); - - var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false ); - var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, - tt, !nameValid, true ) ) - { - Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName ); - _newGroupName = string.Empty; - } - } - - - private string _materialSuffixFrom = string.Empty; - private string _materialSuffixTo = string.Empty; - - // A row of three buttonSizes and a help marker that can be used for material suffix changing. - private void DrawChangeMaterialSuffix( Vector2 buttonSize ) - { - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); - ImGui.SameLine(); - var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo ); - var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." - : _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical." - : disabled ? "The suffices are not valid suffices." - : _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target." - : $"Convert all skin material suffices that are currently {_materialSuffixFrom} to {_materialSuffixTo}."; - if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) - { - ModelChanger.ChangeModMaterials( _mod, _materialSuffixFrom, _materialSuffixTo ); - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" - + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" - + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + DescriptionEdit.DrawPopup( _window ); } + // The general edit row for non-detailed mod edits. private void EditButtons() { var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 ); - var folderExists = Directory.Exists( _mod.BasePath.FullName ); + var folderExists = Directory.Exists( _mod.ModPath.FullName ); var tt = folderExists - ? $"Open \"{_mod.BasePath.FullName}\" in the file explorer of your choice." - : $"Mod directory \"{_mod.BasePath.FullName}\" does not exist."; + ? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice." + : $"Mod directory \"{_mod.ModPath.FullName}\" does not exist."; if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", buttonSize, tt, !folderExists ) ) { - Process.Start( new ProcessStartInfo( _mod.BasePath.FullName ) { UseShellExecute = true } ); + Process.Start( new ProcessStartInfo( _mod.ModPath.FullName ) { UseShellExecute = true } ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Reload the current mod from its files.\n" + + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", + false ) ) + { + Penumbra.ModManager.ReloadMod( _mod.Index ); } - ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, "Not implemented yet", true ); - ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Not implemented yet", true ); + MoveDirectory.Draw( _mod, buttonSize ); - DrawChangeMaterialSuffix( buttonSize ); + + MaterialSuffix.Draw( _mod, buttonSize ); ImGui.Dummy( _window._defaultSpace ); } - - // Special field indices to reuse the same string buffer. - private const int NoFieldIdx = -1; - private const int NameFieldIdx = -2; - private const int AuthorFieldIdx = -3; - private const int VersionFieldIdx = -4; - private const int WebsiteFieldIdx = -5; - private const int PathFieldIdx = -6; - private const int DescriptionFieldIdx = -7; - + // Anything about editing the regular meta information about the mod. private void EditRegularMeta() { - if( TextInput( "Name", NameFieldIdx, NoFieldIdx, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) ) + if( Input.Text( "Name", Input.Name, Input.None, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) ) { Penumbra.ModManager.ChangeModName( _mod.Index, newName ); } - if( TextInput( "Author", AuthorFieldIdx, NoFieldIdx, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) ) + if( Input.Text( "Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) ) { Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor ); } - if( TextInput( "Version", VersionFieldIdx, NoFieldIdx, _mod.Version, out var newVersion, 32, _window._inputTextWidth.X ) ) + if( Input.Text( "Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, + _window._inputTextWidth.X ) ) { Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion ); } - if( TextInput( "Website", WebsiteFieldIdx, NoFieldIdx, _mod.Website, out var newWebsite, 256, _window._inputTextWidth.X ) ) + if( Input.Text( "Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, + _window._inputTextWidth.X ) ) { Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); } @@ -171,7 +121,7 @@ public partial class ConfigWindow var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X, 0 ); if( ImGui.Button( "Edit Description", reducedSize ) ) { - _delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) ); + _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, Input.Description ) ); } ImGui.SameLine(); @@ -204,18 +154,199 @@ public partial class ConfigWindow } } + // Do some edits outside of iterations. + private readonly Queue< Action > _delayedActions = new(); - // Temporary strings - private string? _currentEdit; - private int? _currentGroupPriority; - private int _currentField = -1; - private int _optionIndex = -1; + // Delete a marked group or option outside of iteration. + private void EndActions() + { + while( _delayedActions.TryDequeue( out var action ) ) + { + action.Invoke(); + } + } - private int _newOptionNameIdx = -1; - private string _newGroupName = string.Empty; - private string _newOptionName = string.Empty; - private string _newDescription = string.Empty; - private int _newDescriptionIdx = -1; + // Text input to add a new option group at the end of the current groups. + private static class AddOptionGroup + { + private static string _newGroupName = string.Empty; + + public static void Draw( ConfigWindow window, Mod mod ) + { + ImGui.SetNextItemWidth( window._inputTextWidth.X ); + ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); + ImGui.SameLine(); + + var nameValid = Mod.Manager.VerifyFileName( mod, null, _newGroupName, false ); + var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, + tt, !nameValid, true ) ) + { + Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName ); + _newGroupName = string.Empty; + } + } + } + + // A row of three buttonSizes and a help marker that can be used for material suffix changing. + private static class MaterialSuffix + { + private static string _materialSuffixFrom = string.Empty; + private static string _materialSuffixTo = string.Empty; + + public static void Draw( Mod mod, Vector2 buttonSize ) + { + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SameLine(); + var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo ); + var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." + : _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical." + : disabled ? "The suffices are not valid suffices." + : _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target." + : $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; + if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) + { + ModelChanger.ChangeModMaterials( mod, _materialSuffixFrom, _materialSuffixTo ); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + } + } + + // A text input for the new directory name and a button to apply the move. + private static class MoveDirectory + { + private static string? _currentModDirectory; + private static Mod? _modForDirectory; + private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + + public static void Draw( Mod mod, Vector2 buttonSize ) + { + ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); + var tmp = _currentModDirectory ?? mod.ModPath.Name; + if( mod != _modForDirectory ) + { + tmp = mod.ModPath.Name; + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + + if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) + { + _currentModDirectory = tmp; + _modForDirectory = mod; + _state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); + } + + var (disabled, tt) = _state switch + { + Mod.Manager.NewDirectoryState.Identical => ( true, "Current directory name is identical to new one." ), + Mod.Manager.NewDirectoryState.Empty => ( true, "Please enter a new directory name first." ), + Mod.Manager.NewDirectoryState.NonExisting => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), + Mod.Manager.NewDirectoryState.ExistsEmpty => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), + Mod.Manager.NewDirectoryState.ExistsNonEmpty => ( true, $"{_currentModDirectory} already exists and is not empty." ), + Mod.Manager.NewDirectoryState.ExistsAsFile => ( true, $"{_currentModDirectory} exists as a file." ), + Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => ( true, + $"{_currentModDirectory} contains invalid symbols for FFXIV." ), + _ => ( true, "Unknown error." ), + }; + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null ) + { + Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n" + + "This can currently not be used on pre-existing folders and does not support merges or overwriting." ); + } + } + + // Open a popup to edit a multi-line mod or option description. + private static class DescriptionEdit + { + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static Mod? _mod; + + public static void OpenPopup( Mod mod, int groupIdx ) + { + _newDescriptionIdx = groupIdx; + _newDescription = groupIdx < 0 ? mod.Description : mod.Groups[ groupIdx ].Description; + _mod = mod; + ImGui.OpenPopup( PopupName ); + } + + public static void DrawPopup( ConfigWindow window ) + { + if( _mod == null ) + { + return; + } + + using var popup = ImRaii.Popup( PopupName ); + if( !popup ) + { + return; + } + + if( ImGui.IsWindowAppearing() ) + { + ImGui.SetKeyboardFocusHere(); + } + + ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) ); + ImGui.Dummy( window._defaultSpace ); + + var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 ); + var width = 2 * buttonSize.X + + 4 * ImGui.GetStyle().FramePadding.X + + ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 ); + + var oldDescription = _newDescriptionIdx == Input.Description + ? _mod.Description + : _mod.Groups[ _newDescriptionIdx ].Description; + + var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; + + if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) ) + { + switch( _newDescriptionIdx ) + { + case Input.Description: + Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); + break; + case >= 0: + Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); + break; + } + + ImGui.CloseCurrentPopup(); + } + + ImGui.SameLine(); + if( ImGui.Button( "Cancel", buttonSize ) + || ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + { + _newDescriptionIdx = Input.None; + _newDescription = string.Empty; + ImGui.CloseCurrentPopup(); + } + } + } private void EditGroup( int groupIdx ) { @@ -226,7 +357,7 @@ public partial class ConfigWindow using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding ) .Push( ImGuiStyleVar.ItemSpacing, _itemSpacing ); - if( TextInput( "##Name", groupIdx, NoFieldIdx, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) ) + if( Input.Text( "##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) ) { Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName ); } @@ -244,33 +375,19 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, "Edit group description.", false, true ) ) { - _delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) ); + _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); } ImGui.SameLine(); - if( PriorityInput( "##Priority", groupIdx, NoFieldIdx, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) + if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) { Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); } ImGuiUtil.HoverTooltip( "Group Priority" ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); - using( var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ) ) - { - if( combo ) - { - foreach( var type in new[] { SelectType.Single, SelectType.Multi } ) - { - if( ImGui.Selectable( GroupTypeName( type ), group.Type == type ) ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type ); - } - } - } - } - + DrawGroupCombo( group, groupIdx ); ImGui.SameLine(); var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; @@ -291,7 +408,7 @@ public partial class ConfigWindow } ImGui.SameLine(); - var fileName = group.FileName( _mod.BasePath, groupIdx ); + var fileName = group.FileName( _mod.ModPath, groupIdx ); var fileExists = File.Exists( fileName ); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." @@ -303,19 +420,92 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); - ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, _window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, _window._iconButtonSize.X ); - ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, _window._iconButtonSize.X ); - ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); - if( table ) + OptionTable.Draw( this, groupIdx ); + } + + // Draw the table displaying all options and the add new option line. + private static class OptionTable + { + private const string DragDropLabel = "##DragOption"; + + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static int _dragDropGroupIdx = -1; + private static int _dragDropOptionIdx = -1; + + public static void Draw( ModPanel panel, int groupIdx ) { - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); + if( !table ) { - EditOption( group, groupIdx, optionIdx ); + return; } + ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, + panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); + ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); + ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); + + var group = panel._mod.Groups[ groupIdx ]; + for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) + { + EditOption( panel, group, groupIdx, optionIdx ); + } + + DrawNewOption( panel._mod, groupIdx, panel._window._iconButtonSize ); + } + + // Draw a line for a single option. + private static void EditOption( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) + { + var option = group[ optionIdx ]; + using var id = ImRaii.PushId( optionIdx ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable( $"Option #{optionIdx + 1}" ); + Source( group, groupIdx, optionIdx ); + Target( panel, group, groupIdx, optionIdx ); + + ImGui.TableNextColumn(); + if( Input.Text( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) + { + Penumbra.ModManager.RenameOption( panel._mod, groupIdx, optionIdx, newOptionName ); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize, + "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) + { + panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, + "Edit this option.", false, true ) ) + { + panel._window.ModEditPopup.ChangeMod( panel._mod ); + panel._window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); + panel._window.ModEditPopup.IsOpen = true; + } + + ImGui.TableNextColumn(); + if( group.Type == SelectType.Multi ) + { + if( Input.Priority( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, + 50 * ImGuiHelpers.GlobalScale ) ) + { + Penumbra.ModManager.ChangeOptionPriority( panel._mod, groupIdx, optionIdx, priority ); + } + + ImGuiUtil.HoverTooltip( "Option priority." ); + } + } + + // Draw the line to add a new option. + private static void DrawNewOption( Mod mod, int groupIdx, Vector2 iconButtonSize ) + { ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); @@ -327,233 +517,161 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, "Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) ) { - Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName ); + Penumbra.ModManager.AddOption( mod, groupIdx, _newOptionName ); _newOptionName = string.Empty; } } - } - private static string GroupTypeName( SelectType type ) - => type switch + // Handle drag and drop to move options inside a group or into another group. + private static void Source( IModGroup group, int groupIdx, int optionIdx ) { - SelectType.Single => "Single Group", - SelectType.Multi => "Multi Group", - _ => "Unknown", - }; - - private int _dragDropGroupIdx = -1; - private int _dragDropOptionIdx = -1; - - private void OptionDragDrop( IModGroup group, int groupIdx, int optionIdx ) - { - const string label = "##DragOption"; - using( var source = ImRaii.DragDropSource() ) - { - if( source ) + using var source = ImRaii.DragDropSource(); + if( !source ) { - if( ImGui.SetDragDropPayload( label, IntPtr.Zero, 0 ) ) - { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; - } - - ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); + return; } + + if( ImGui.SetDragDropPayload( DragDropLabel, IntPtr.Zero, 0 ) ) + { + _dragDropGroupIdx = groupIdx; + _dragDropOptionIdx = optionIdx; + } + + ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); } - // TODO drag options to other groups without options. - using( var target = ImRaii.DragDropTarget() ) + private static void Target( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) { - if( target.Success && ImGuiUtil.IsDropping( label ) ) + // TODO drag options to other groups without options. + using var target = ImRaii.DragDropTarget(); + if( !target.Success || !ImGuiUtil.IsDropping( DragDropLabel ) ) { - if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) + return; + } + + if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) + { + if( _dragDropGroupIdx == groupIdx ) { - if( _dragDropGroupIdx == groupIdx ) + var sourceOption = _dragDropOptionIdx; + panel._delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( panel._mod, groupIdx, sourceOption, optionIdx ) ); + } + else + { + // Move from one group to another by deleting, then adding the option. + var sourceGroup = _dragDropGroupIdx; + var sourceOption = _dragDropOptionIdx; + var option = @group[ _dragDropOptionIdx ]; + var priority = @group.OptionPriority( _dragDropGroupIdx ); + panel._delayedActions.Enqueue( () => { - var sourceOption = _dragDropOptionIdx; - _delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( _mod, groupIdx, sourceOption, optionIdx ) ); - } - else - { - // Move from one group to another by deleting, then adding the option. - var sourceGroup = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var option = group[ _dragDropOptionIdx ]; - var priority = group.OptionPriority( _dragDropGroupIdx ); - _delayedActions.Enqueue( () => - { - Penumbra.ModManager.DeleteOption( _mod, sourceGroup, sourceOption ); - Penumbra.ModManager.AddOption( _mod, groupIdx, option, priority ); - } ); - } + Penumbra.ModManager.DeleteOption( panel._mod, sourceGroup, sourceOption ); + Penumbra.ModManager.AddOption( panel._mod, groupIdx, option, priority ); + } ); } - - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; } + + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; } } - private void EditOption( IModGroup group, int groupIdx, int optionIdx ) + // Draw a combo to select single or multi group and switch between them. + private void DrawGroupCombo( IModGroup group, int groupIdx ) { - var option = group[ optionIdx ]; - using var id = ImRaii.PushId( optionIdx ); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( $"Option #{optionIdx + 1}" ); - OptionDragDrop( group, groupIdx, optionIdx ); - - ImGui.TableNextColumn(); - if( TextInput( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) - { - Penumbra.ModManager.RenameOption( _mod, groupIdx, optionIdx, newOptionName ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( _mod, groupIdx, optionIdx ) ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, - "Edit this option.", false, true ) ) - { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); - _window.ModEditPopup.IsOpen = true; - } - - ImGui.TableNextColumn(); - if( group.Type == SelectType.Multi ) - { - if( PriorityInput( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, - 50 * ImGuiHelpers.GlobalScale ) ) + static string GroupTypeName( SelectType type ) + => type switch { - Penumbra.ModManager.ChangeOptionPriority( _mod, groupIdx, optionIdx, priority ); - } + SelectType.Single => "Single Group", + SelectType.Multi => "Multi Group", + _ => "Unknown", + }; - ImGuiUtil.HoverTooltip( "Option priority." ); - } - } - - private bool TextInput( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputText( label, ref tmp, maxLength ) ) + ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); + using var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ); + if( !combo ) { - _currentEdit = tmp; - _optionIndex = option; - _currentField = field; + return; } - if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) + foreach( var type in new[] { SelectType.Single, SelectType.Multi } ) { - var ret = _currentEdit != oldValue; - value = _currentEdit; - _currentEdit = null; - _currentField = NoFieldIdx; - _optionIndex = NoFieldIdx; - return ret; - } - - value = string.Empty; - return false; - } - - private bool PriorityInput( string label, int field, int option, int oldValue, out int value, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputInt( label, ref tmp, 0, 0 ) ) - { - _currentGroupPriority = tmp; - _optionIndex = option; - _currentField = field; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) - { - var ret = _currentGroupPriority != oldValue; - value = _currentGroupPriority.Value; - _currentGroupPriority = null; - _currentField = NoFieldIdx; - _optionIndex = NoFieldIdx; - return ret; - } - - value = 0; - return false; - } - - // Delete a marked group or option outside of iteration. - private void EndActions() - { - while( _delayedActions.TryDequeue( out var action ) ) - { - action.Invoke(); - } - } - - private void OpenEditDescriptionPopup( int groupIdx ) - { - _newDescriptionIdx = groupIdx; - _newDescription = groupIdx < 0 ? _mod.Description : _mod.Groups[ groupIdx ].Description; - ImGui.OpenPopup( "Edit Description" ); - } - - private void EditDescriptionPopup() - { - using var popup = ImRaii.Popup( "Edit Description" ); - if( popup ) - { - if( ImGui.IsWindowAppearing() ) + if( ImGui.Selectable( GroupTypeName( type ), @group.Type == type ) ) { - ImGui.SetKeyboardFocusHere(); + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type ); } + } + } - ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) ); - ImGui.Dummy( _window._defaultSpace ); + // Handles input text and integers in separate fields without buffers for every single one. + private static class Input + { + // Special field indices to reuse the same string buffer. + public const int None = -1; + public const int Name = -2; + public const int Author = -3; + public const int Version = -4; + public const int Website = -5; + public const int Path = -6; + public const int Description = -7; - var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 ); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 ); + // Temporary strings + private static string? _currentEdit; + private static int? _currentGroupPriority; + private static int _currentField = -1; + private static int _optionIndex = -1; - var oldDescription = _newDescriptionIdx == DescriptionFieldIdx - ? _mod.Description - : _mod.Groups[ _newDescriptionIdx ].Description; - - var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; - - if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) ) + public static bool Text( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) + { + var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputText( label, ref tmp, maxLength ) ) { - if( _newDescriptionIdx == DescriptionFieldIdx ) - { - Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); - } - else if( _newDescriptionIdx >= 0 ) - { - Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); - } - - ImGui.CloseCurrentPopup(); + _currentEdit = tmp; + _optionIndex = option; + _currentField = field; } - ImGui.SameLine(); - if( ImGui.Button( "Cancel", buttonSize ) - || ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) { - _newDescriptionIdx = NoFieldIdx; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); + var ret = _currentEdit != oldValue; + value = _currentEdit; + _currentEdit = null; + _currentField = None; + _optionIndex = None; + return ret; } + + value = string.Empty; + return false; + } + + public static bool Priority( string label, int field, int option, int oldValue, out int value, float width ) + { + var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputInt( label, ref tmp, 0, 0 ) ) + { + _currentGroupPriority = tmp; + _optionIndex = option; + _currentField = field; + } + + if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) + { + var ret = _currentGroupPriority != oldValue; + value = _currentGroupPriority.Value; + _currentGroupPriority = null; + _currentField = None; + _optionIndex = None; + return ret; + } + + value = 0; + return false; } } } From 54460c39f3eea5ffbf9dac2dea6afda19ae03a87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 May 2022 18:15:35 +0200 Subject: [PATCH 0164/2451] Add more edit options, some small fixes. --- Penumbra.GameData/Enums/Race.cs | 8 +- .../Interop/Loader/ResourceLoader.Debug.cs | 30 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 12 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 42 +- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 179 +++++++++ Penumbra/Mods/Editor/Mod.Editor.cs | 8 +- Penumbra/Mods/Mod.Creation.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 1 + Penumbra/UI/Classes/ModEditWindow.cs | 372 +++++++++++++++++- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 124 +++--- Penumbra/UI/ConfigWindow.ModsTab.cs | 35 +- Penumbra/UI/ConfigWindow.cs | 17 +- Penumbra/Util/ModelChanger.cs | 94 ----- 13 files changed, 697 insertions(+), 227 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs delete mode 100644 Penumbra/Util/ModelChanger.cs diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index 47575a31..ba8f6337 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -163,7 +163,7 @@ public static class RaceEnumExtensions ModelRace.AuRa => Race.AuRa.ToName(), ModelRace.Hrothgar => Race.Hrothgar.ToName(), ModelRace.Viera => Race.Viera.ToName(), - _ => throw new ArgumentOutOfRangeException( nameof( modelRace ), modelRace, null ), + _ => Race.Unknown.ToName(), }; } @@ -179,7 +179,7 @@ public static class RaceEnumExtensions Race.AuRa => "Au Ra", Race.Hrothgar => "Hrothgar", Race.Viera => "Viera", - _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), + _ => "Unknown", }; } @@ -191,7 +191,7 @@ public static class RaceEnumExtensions Gender.Female => "Female", Gender.MaleNpc => "Male (NPC)", Gender.FemaleNpc => "Female (NPC)", - _ => throw new InvalidEnumArgumentException(), + _ => "Unknown", }; } @@ -215,7 +215,7 @@ public static class RaceEnumExtensions SubRace.Lost => "Lost", SubRace.Rava => "Rava", SubRace.Veena => "Veena", - _ => throw new InvalidEnumArgumentException(), + _ => "Unknown", }; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index e0dad891..3be1b0ec 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -52,18 +52,26 @@ public unsafe partial class ResourceLoader return; } - var crc = ( uint )originalPath.Path.Crc32; - var originalResource = FindResource( handle->Category, handle->FileType, crc ); - _debugList[ manipulatedPath.Value ] = new DebugData() + // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra. + try { - OriginalResource = ( Structs.ResourceHandle* )originalResource, - ManipulatedResource = handle, - Category = handle->Category, - Extension = handle->FileType, - OriginalPath = originalPath.Clone(), - ManipulatedPath = manipulatedPath.Value, - ResolverInfo = resolverInfo, - }; + var crc = ( uint )originalPath.Path.Crc32; + var originalResource = FindResource( handle->Category, handle->FileType, crc ); + _debugList[ manipulatedPath.Value ] = new DebugData() + { + OriginalResource = ( Structs.ResourceHandle* )originalResource, + ManipulatedResource = handle, + Category = handle->Category, + Extension = handle->FileType, + OriginalPath = originalPath.Clone(), + ManipulatedPath = manipulatedPath.Value, + ResolverInfo = resolverInfo, + }; + } + catch( Exception e ) + { + PluginLog.Error( e.ToString() ); + } } // Find a key in a StdMap. diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index e8225db4..3f217fab 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -48,11 +48,6 @@ public partial class Mod _duplicates.Clear(); } - public void Cancel() - { - DuplicatesFinished = true; - } - private void HandleDuplicate( FullPath duplicate, FullPath remaining ) { void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) @@ -94,8 +89,11 @@ public partial class Mod public void StartDuplicateCheck() { - DuplicatesFinished = false; - Task.Run( CheckDuplicates ); + if( DuplicatesFinished ) + { + DuplicatesFinished = false; + Task.Run( CheckDuplicates ); + } } private void CheckDuplicates() diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 65596021..e0378d26 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Util; @@ -9,20 +10,23 @@ public partial class Mod { public partial class Editor { - private int _groupIdx = -1; - private int _optionIdx = 0; + public int GroupIdx { get; private set; } = -1; + public int OptionIdx { get; private set; } = 0; private IModGroup? _modGroup; private SubMod _subMod; + public ISubMod CurrentOption + => _subMod; + public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); public readonly HashSet< MetaManipulation > CurrentManipulations = new(); public void SetSubMod( int groupIdx, int optionIdx ) { - _groupIdx = groupIdx; - _optionIdx = optionIdx; + GroupIdx = groupIdx; + OptionIdx = optionIdx; if( groupIdx >= 0 ) { _modGroup = _mod.Groups[ groupIdx ]; @@ -34,8 +38,38 @@ public partial class Mod _subMod = _mod._default; } + RevertFiles(); + RevertSwaps(); + RevertManipulations(); + } + + public void ApplyFiles() + { + Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, CurrentFiles.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + } + + public void RevertFiles() + { CurrentFiles.SetTo( _subMod.Files ); + } + + public void ApplySwaps() + { + Penumbra.ModManager.OptionSetFileSwaps( _mod, GroupIdx, OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + } + + public void RevertSwaps() + { CurrentSwaps.SetTo( _subMod.FileSwaps ); + } + + public void ApplyManipulations() + { + Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() ); + } + + public void RevertManipulations() + { CurrentManipulations.Clear(); CurrentManipulations.UnionWith( _subMod.Manipulations ); } diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs new file mode 100644 index 00000000..5e87b2dd --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled); + private readonly List< MaterialInfo > _modelFiles = new(); + public IReadOnlyList< MaterialInfo > ModelFiles + => _modelFiles; + + // Non-ASCII encoding can not be used. + public static bool ValidString( string to ) + => to.Length != 0 + && to.Length < 16 + && Encoding.UTF8.GetByteCount( to ) == to.Length; + + public void SaveAllModels() + { + foreach( var info in _modelFiles ) + { + info.Save(); + } + } + + public void RestoreAllModels() + { + foreach( var info in _modelFiles ) + { + info.Restore(); + } + } + + // Go through the currently loaded files and replace all appropriate suffices. + // Does nothing if toSuffix is invalid. + // If raceCode is Unknown, apply to all raceCodes. + // If fromSuffix is empty, apply to all suffices. + public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown ) + { + if( !ValidString( toSuffix ) ) + return; + + foreach( var info in _modelFiles ) + { + for( var i = 0; i < info.Count; ++i ) + { + var (_, def) = info[ i ]; + var match = MaterialRegex.Match( def ); + if( match.Success + && ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value ) + && ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) ) + { + info.SetMaterial( $"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i ); + } + } + } + } + + // Find all model files in the mod that contain skin materials. + private void ScanModels() + { + _modelFiles.Clear(); + foreach( var (file, _) in AvailableFiles.Where( f => f.Item1.Extension == ".mdl" ) ) + { + try + { + var bytes = File.ReadAllBytes( file.FullName ); + var mdlFile = new MdlFile( bytes ); + var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) ) + .Select( p => p.Item2 ).ToArray(); + if( materials.Length > 0 ) + { + _modelFiles.Add( new MaterialInfo( file, mdlFile, materials ) ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.FullName} for materials:\n{e}" ); + } + } + } + + // A class that collects information about skin materials in a model file and handle changes on them. + public class MaterialInfo + { + public readonly FullPath Path; + private readonly MdlFile _file; + private readonly string[] _currentMaterials; + private readonly IReadOnlyList _materialIndices; + public bool Changed { get; private set; } = false; + + public IReadOnlyList CurrentMaterials + => _currentMaterials; + + private IEnumerable DefaultMaterials + => _materialIndices.Select( i => _file.Materials[i] ); + + public (string Current, string Default) this[int idx] + => (_currentMaterials[idx], _file.Materials[_materialIndices[idx]]); + + public int Count + => _materialIndices.Count; + + // Set the skin material to a new value and flag changes appropriately. + public void SetMaterial( string value, int materialIdx ) + { + var mat = _file.Materials[_materialIndices[materialIdx]]; + _currentMaterials[materialIdx] = value; + if( mat != value ) + { + Changed = true; + } + else + { + Changed = !_currentMaterials.SequenceEqual( DefaultMaterials ); + } + } + + // Save a changed .mdl file. + public void Save() + { + if( !Changed ) + { + return; + } + + foreach( var (idx, i) in _materialIndices.WithIndex() ) + { + _file.Materials[idx] = _currentMaterials[i]; + } + + try + { + File.WriteAllBytes( Path.FullName, _file.Write() ); + Changed = false; + } + catch( Exception e ) + { + Restore(); + PluginLog.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" ); + } + } + + // Revert all current changes. + public void Restore() + { + if( !Changed ) + return; + + foreach( var (idx, i) in _materialIndices.WithIndex() ) + { + _currentMaterials[i] = _file.Materials[idx]; + } + Changed = false; + } + + public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList indices ) + { + Path = path; + _file = file; + _materialIndices = indices; + _currentMaterials = DefaultMaterials.ToArray(); + } + } + + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index 9007bb2d..d69c32f0 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -21,13 +21,17 @@ public partial class Mod _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); _subMod = _mod._default; + ScanModels(); } - public void Dispose() + public void Cancel() { DuplicatesFinished = true; } + public void Dispose() + => Cancel(); + // Does not delete the base directory itself even if it is completely empty at the end. private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) { @@ -49,7 +53,7 @@ public partial class Mod { for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) { - action( @group[ optionIdx ], groupIdx, optionIdx ); + action( group[ optionIdx ], groupIdx, optionIdx ); } } } diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index d3f8b33d..c6387fc6 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -18,7 +18,7 @@ public partial class Mod // - Containing no symbols invalid for FFXIV or windows paths. internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) { - var name = Path.GetFileNameWithoutExtension( modListName ); + var name = modListName; if( name.Length == 0 ) { name = "_"; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 0a16758e..fd55b3bb 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -23,6 +23,7 @@ public static class Colors public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; + public const uint RedTableBgTint = 0x40000080; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 46f19f69..630c07bf 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,13 +1,18 @@ using System; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -28,6 +33,11 @@ public class ModEditWindow : Window, IDisposable _editor = new Mod.Editor( mod ); _mod = mod; WindowName = $"{mod.Name}{WindowBaseLabel}"; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = ImGuiHelpers.ScaledVector2( 800, 600 ), + MaximumSize = 4000 * Vector2.One, + }; } public void ChangeOption( int groupIdx, int optionIdx ) @@ -50,6 +60,168 @@ public class ModEditWindow : Window, IDisposable DrawMissingFilesTab(); DrawUnusedFilesTab(); DrawDuplicatesTab(); + DrawMaterialChangeTab(); + } + + // A row of three buttonSizes and a help marker that can be used for material suffix changing. + private static class MaterialSuffix + { + private static string _materialSuffixFrom = string.Empty; + private static string _materialSuffixTo = string.Empty; + private static GenderRace _raceCode = GenderRace.Unknown; + + private static string RaceCodeName( GenderRace raceCode ) + { + if( raceCode == GenderRace.Unknown ) + { + return "All Races and Genders"; + } + + var (gender, race) = raceCode.Split(); + return $"({raceCode.ToRaceCode()}) {race.ToName()} {gender.ToName()} "; + } + + private static void DrawRaceCodeCombo( Vector2 buttonSize ) + { + ImGui.SetNextItemWidth( buttonSize.X ); + using var combo = ImRaii.Combo( "##RaceCode", RaceCodeName( _raceCode ) ); + if( !combo ) + { + return; + } + + foreach( var raceCode in Enum.GetValues< GenderRace >() ) + { + if( ImGui.Selectable( RaceCodeName( raceCode ), _raceCode == raceCode ) ) + { + _raceCode = raceCode; + } + } + } + + public static void Draw( Mod.Editor editor, Vector2 buttonSize ) + { + DrawRaceCodeCombo( buttonSize ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SameLine(); + var disabled = !Mod.Editor.ValidString( _materialSuffixTo ); + var tt = _materialSuffixTo.Length == 0 + ? "Please enter a target suffix." + : _materialSuffixFrom == _materialSuffixTo + ? "The source and target are identical." + : disabled + ? "The suffix is invalid." + : _materialSuffixFrom.Length == 0 + ? _raceCode == GenderRace.Unknown ? "Convert all skin material suffices to the target." + : "Convert all skin material suffices for the given race code to the target." + : _raceCode == GenderRace.Unknown + ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." + : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; + if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) + { + editor.ReplaceAllMaterials( _materialSuffixTo, _materialSuffixFrom, _raceCode ); + } + + var anyChanges = editor.ModelFiles.Any( m => m.Changed ); + if( ImGuiUtil.DrawDisabledButton( "Save All Changes", buttonSize, + anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges ) ) + { + editor.SaveAllModels(); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Revert All Changes", buttonSize, + anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges ) ) + { + editor.RestoreAllModels(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + } + } + + private void DrawMaterialChangeTab() + { + using var tab = ImRaii.TabItem( "Model Materials" ); + if( !tab ) + { + return; + } + + if( _editor!.ModelFiles.Count == 0 ) + { + ImGui.NewLine(); + ImGui.TextUnformatted( "No .mdl files detected." ); + } + else + { + ImGui.NewLine(); + MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + ImGui.NewLine(); + using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) + { + using var id = ImRaii.PushId( idx ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) + { + info.Save(); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true ) ) + { + info.Restore(); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + var tmp = info.CurrentMaterials[ 0 ]; + if( ImGui.InputText( "##0", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, 0 ); + } + + for( var i = 1; i < info.Count; ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + tmp = info.CurrentMaterials[ i ]; + if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, i ); + } + } + } + } } private void DrawMissingFilesTab() @@ -62,6 +234,7 @@ public class ModEditWindow : Window, IDisposable if( _editor!.MissingPaths.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No missing files detected." ); } else @@ -71,6 +244,12 @@ public class ModEditWindow : Window, IDisposable _editor.RemoveMissingPaths(); } + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); if( !table ) { @@ -113,7 +292,9 @@ public class ModEditWindow : Window, IDisposable if( _editor.Duplicates.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No duplicates found." ); + return; } if( ImGui.Button( "Delete and Redirect Duplicates" ) ) @@ -173,12 +354,68 @@ public class ModEditWindow : Window, IDisposable foreach( var duplicate in set.Skip( 1 ) ) { ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + } + } + } + + private void DrawOptionSelectHeader() + { + const string defaultOption = "Default Option"; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); + var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); + var isDefaultOption = _editor!.GroupIdx == -1 && _editor!.OptionIdx == 0; + if( ImGuiUtil.DrawDisabledButton( defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", + isDefaultOption ) ) + { + _editor.SetSubMod( -1, 0 ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false ) ) + { + _editor.SetSubMod( _editor.GroupIdx, _editor.OptionIdx ); + } + + ImGui.SameLine(); + + string GetLabel() + { + if( isDefaultOption ) + { + return defaultOption; + } + + var group = _mod!.Groups[ _editor!.GroupIdx ]; + return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; + } + + var groupLabel = GetLabel(); + using var combo = ImRaii.Combo( "##optionSelector", groupLabel, ImGuiComboFlags.NoArrowButton ); + if( !combo ) + { + return; + } + + if( ImGui.Selectable( $"{defaultOption}###-1_0", isDefaultOption ) ) + { + _editor.SetSubMod( -1, 0 ); + } + + foreach( var (group, groupIdx) in _mod!.Groups.WithIndex() ) + { + foreach( var (option, optionIdx) in group.WithIndex() ) + { + var name = $"{group.Name}: {option.Name}###{groupIdx}_{optionIdx}"; + if( ImGui.Selectable( name, groupIdx == _editor.GroupIdx && optionIdx == _editor.OptionIdx ) ) + { + _editor.SetSubMod( groupIdx, optionIdx ); + } } } } @@ -193,6 +430,7 @@ public class ModEditWindow : Window, IDisposable if( _editor!.UnusedFiles.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No unused files detected." ); } else @@ -202,12 +440,19 @@ public class ModEditWindow : Window, IDisposable _editor.AddUnusedPathsToDefault(); } + ImGui.SameLine(); if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) { _editor.DeleteUnusedPaths(); } - using var table = ImRaii.Table( "##unusedFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##table", 1, ImGuiTableFlags.RowBg ); if( !table ) { return; @@ -230,7 +475,14 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##files", 2 ); + DrawOptionSelectHeader(); + using var child = ImRaii.Child( "##files", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 2 ); if( !list ) { return; @@ -253,7 +505,30 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##meta", 3 ); + DrawOptionSelectHeader(); + + var setsEqual = _editor!.CurrentManipulations.SetEquals( _editor.CurrentOption.Manipulations ); + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplyManipulations(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertManipulations(); + } + + using var child = ImRaii.Child( "##meta", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 3 ); if( !list ) { return; @@ -288,6 +563,8 @@ public class ModEditWindow : Window, IDisposable } } + private string _newSwapKey = string.Empty; + private string _newSwapValue = string.Empty; private void DrawSwapTab() { using var tab = ImRaii.TabItem( "File Swaps" ); @@ -296,19 +573,94 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##swaps", 3 ); + DrawOptionSelectHeader(); + + var setsEqual = _editor!.CurrentSwaps.SetEquals( _editor.CurrentOption.FileSwaps ); + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplySwaps(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertSwaps(); + } + + using var child = ImRaii.Child( "##swaps", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg, -Vector2.One ); if( !list ) { return; } - foreach( var (gamePath, file) in _editor!.CurrentSwaps ) + var idx = 0; + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + var pathSize = ImGui.GetContentRegionAvail().X / 2 - iconSize.X; + ImGui.TableSetupColumn( "button", ImGuiTableColumnFlags.WidthFixed, iconSize.X ); + ImGui.TableSetupColumn( "source", ImGuiTableColumnFlags.WidthFixed, pathSize ); + ImGui.TableSetupColumn( "value", ImGuiTableColumnFlags.WidthFixed, pathSize ); + + foreach( var (gamePath, file) in _editor!.CurrentSwaps.ToList() ) { + using var id = ImRaii.PushId( idx++ ); ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true ) ) + { + _editor.CurrentSwaps.Remove( gamePath ); + } + ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.FullName ); + var tmp = gamePath.Path.ToString(); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputText( "##key", ref tmp, Utf8GamePath.MaxGamePathLength ) + && Utf8GamePath.FromString( tmp, out var path ) + && !_editor.CurrentSwaps.ContainsKey( path ) ) + { + _editor.CurrentSwaps.Remove( gamePath ); + if( path.Length > 0 ) + { + _editor.CurrentSwaps[ path ] = file; + } + } + + ImGui.TableNextColumn(); + tmp = file.FullName; + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputText( "##value", ref tmp, Utf8GamePath.MaxGamePathLength ) && tmp.Length > 0 ) + { + _editor.CurrentSwaps[ gamePath ] = new FullPath( tmp ); + } } + + ImGui.TableNextColumn(); + var addable = Utf8GamePath.FromString( _newSwapKey, out var newPath ) + && newPath.Length > 0 + && _newSwapValue.Length > 0 + && _newSwapValue != _newSwapKey + && !_editor.CurrentSwaps.ContainsKey( newPath ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, + true ) ) + { + _editor.CurrentSwaps[ newPath ] = new FullPath( _newSwapValue ); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##swapKey", "New Swap Source...", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##swapValue", "New Swap Target...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); } public ModEditWindow() diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 3ac1b344..7c149d19 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -5,11 +5,11 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Memory; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI; @@ -84,9 +84,6 @@ public partial class ConfigWindow MoveDirectory.Draw( _mod, buttonSize ); - - - MaterialSuffix.Draw( _mod, buttonSize ); ImGui.Dummy( _window._defaultSpace ); } @@ -135,13 +132,12 @@ public partial class ConfigWindow Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); } - if( ImGui.Button( "Edit Default Mod", reducedSize ) ) + if( ImGui.Button( "Edit Mod Details", reducedSize ) ) { _window.ModEditPopup.ChangeMod( _mod ); _window.ModEditPopup.ChangeOption( -1, 0 ); _window.ModEditPopup.IsOpen = true; } - ImGui.SameLine(); fileExists = File.Exists( _mod.DefaultFile ); tt = fileExists @@ -171,6 +167,9 @@ public partial class ConfigWindow { private static string _newGroupName = string.Empty; + public static void Reset() + => _newGroupName = string.Empty; + public static void Draw( ConfigWindow window, Mod mod ) { ImGui.SetNextItemWidth( window._inputTextWidth.X ); @@ -183,66 +182,30 @@ public partial class ConfigWindow tt, !nameValid, true ) ) { Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName ); - _newGroupName = string.Empty; + Reset(); } } } - // A row of three buttonSizes and a help marker that can be used for material suffix changing. - private static class MaterialSuffix - { - private static string _materialSuffixFrom = string.Empty; - private static string _materialSuffixTo = string.Empty; - - public static void Draw( Mod mod, Vector2 buttonSize ) - { - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); - ImGui.SameLine(); - var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo ); - var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." - : _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical." - : disabled ? "The suffices are not valid suffices." - : _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target." - : $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; - if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) - { - ModelChanger.ChangeModMaterials( mod, _materialSuffixFrom, _materialSuffixTo ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" - + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" - + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); - } - } - // A text input for the new directory name and a button to apply the move. private static class MoveDirectory { private static string? _currentModDirectory; - private static Mod? _modForDirectory; private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + public static void Reset() + { + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + public static void Draw( Mod mod, Vector2 buttonSize ) { ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); var tmp = _currentModDirectory ?? mod.ModPath.Name; - if( mod != _modForDirectory ) - { - tmp = mod.ModPath.Name; - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; - } - if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) { _currentModDirectory = tmp; - _modForDirectory = mod; _state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); } @@ -262,8 +225,7 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null ) { Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; + Reset(); } ImGui.SameLine(); @@ -372,14 +334,6 @@ public partial class ConfigWindow ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, - "Edit group description.", false, true ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) { Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); @@ -407,6 +361,14 @@ public partial class ConfigWindow _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); } + ImGui.SameLine(); + + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, + "Edit group description.", false, true ) ) + { + _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); + } + ImGui.SameLine(); var fileName = group.FileName( _mod.ModPath, groupIdx ); var fileExists = File.Exists( fileName ); @@ -433,9 +395,17 @@ public partial class ConfigWindow private static int _dragDropGroupIdx = -1; private static int _dragDropOptionIdx = -1; + public static void Reset() + { + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + public static void Draw( ModPanel panel, int groupIdx ) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table( string.Empty, 4, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; @@ -445,7 +415,6 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); var group = panel._mod.Groups[ groupIdx ]; @@ -481,15 +450,6 @@ public partial class ConfigWindow panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); } - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, - "Edit this option.", false, true ) ) - { - panel._window.ModEditPopup.ChangeMod( panel._mod ); - panel._window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); - panel._window.ModEditPopup.IsOpen = true; - } - ImGui.TableNextColumn(); if( group.Type == SelectType.Multi ) { @@ -621,8 +581,16 @@ public partial class ConfigWindow // Temporary strings private static string? _currentEdit; private static int? _currentGroupPriority; - private static int _currentField = -1; - private static int _optionIndex = -1; + private static int _currentField = None; + private static int _optionIndex = None; + + public static void Reset() + { + _currentEdit = null; + _currentGroupPriority = null; + _currentField = None; + _optionIndex = None; + } public static bool Text( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) { @@ -638,10 +606,8 @@ public partial class ConfigWindow if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) { var ret = _currentEdit != oldValue; - value = _currentEdit; - _currentEdit = null; - _currentField = None; - _optionIndex = None; + value = _currentEdit; + Reset(); return ret; } @@ -663,10 +629,8 @@ public partial class ConfigWindow if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) { var ret = _currentGroupPriority != oldValue; - value = _currentGroupPriority.Value; - _currentGroupPriority = null; - _currentField = None; - _optionIndex = None; + value = _currentGroupPriority.Value; + Reset(); return ret; } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 52d26878..f09f1423 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -40,15 +40,15 @@ public partial class ConfigWindow } catch( Exception e ) { - PluginLog.Error($"Exception thrown during ModPanel Render:\n{e}" ); - PluginLog.Error($"{Penumbra.ModManager.Count} Mods\n" + PluginLog.Error( $"Exception thrown during ModPanel Render:\n{e}" ); + PluginLog.Error( $"{Penumbra.ModManager.Count} Mods\n" + $"{Penumbra.CollectionManager.Current.Name} Current Collection\n" + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + $"{_selector.SortMode} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", Penumbra.CollectionManager.Current.Inheritance)} Inheritances\n" - + $"{_selector.SelectedSettingCollection.Name} Collection\n"); + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance )} Inheritances\n" + + $"{_selector.SelectedSettingCollection.Name} Collection\n" ); } } @@ -62,7 +62,7 @@ public partial class ConfigWindow ImGui.SameLine(); DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); - DrawCollectionSelector( "##collection", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); } private static void DrawDefaultCollectionButton( Vector2 width ) @@ -148,5 +148,28 @@ public partial class ConfigWindow UpdateSettingsData( selector ); UpdateModData(); } + + public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) + { + if( old == mod ) + { + return; + } + + if( mod == null ) + { + _window.ModEditPopup.IsOpen = false; + } + else if( _window.ModEditPopup.IsOpen ) + { + _window.ModEditPopup.ChangeMod( mod ); + } + + _currentPriority = null; + MoveDirectory.Reset(); + OptionTable.Reset(); + Input.Reset(); + AddOptionGroup.Reset(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 1652c414..793d42eb 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -26,14 +26,15 @@ public sealed partial class ConfigWindow : Window, IDisposable public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) { - _penumbra = penumbra; - _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); - _modPanel = new ModPanel( this ); - _collectionsTab = new CollectionsTab( this ); - _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab( this ); - _resourceTab = new ResourceTab( this ); + _penumbra = penumbra; + _settingsTab = new SettingsTab( this ); + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); + _modPanel = new ModPanel( this ); + _selector.SelectionChanged += _modPanel.OnSelectionChange; + _collectionsTab = new CollectionsTab( this ); + _effectiveTab = new EffectiveTab(); + _debugTab = new DebugTab( this ); + _resourceTab = new ResourceTab( this ); Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs deleted file mode 100644 index 3c900bf4..00000000 --- a/Penumbra/Util/ModelChanger.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Files; -using Penumbra.Mods; - -namespace Penumbra.Util; - -public static class ModelChanger -{ - public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; - public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); - - // Non-ASCII encoding can not be used. - public static bool ValidStrings( string from, string to ) - => to.Length != 0 - && from.Length < 16 - && to.Length < 16 - && from != to - && Encoding.UTF8.GetByteCount( from ) == from.Length - && Encoding.UTF8.GetByteCount( to ) == to.Length; - - - [Conditional( "FALSE" )] - private static void WriteBackup( string name, byte[] text ) - => File.WriteAllBytes( name + ".bak", text ); - - // Change material suffices for a single mdl file. - public static int ChangeMtrl( FullPath file, string from, string to ) - { - if( !file.Exists ) - { - return 0; - } - - try - { - var data = File.ReadAllBytes( file.FullName ); - var mdlFile = new MdlFile( data ); - - // If from is empty, match with any current material suffix, - // otherwise check for exact matches with from. - Func< string, bool > compare = MaterialRegex.IsMatch; - if( from.Length > 0 ) - { - from = string.Format( MaterialFormat, from ); - compare = s => s == from; - } - - to = string.Format( MaterialFormat, to ); - var replaced = 0; - for( var i = 0; i < mdlFile.Materials.Length; ++i ) - { - if( compare( mdlFile.Materials[ i ] ) ) - { - mdlFile.Materials[ i ] = to; - ++replaced; - } - } - - // Only rewrite the file if anything was changed. - if( replaced > 0 ) - { - WriteBackup( file.FullName, data ); - File.WriteAllBytes( file.FullName, mdlFile.Write() ); - } - - return replaced; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write .mdl data for file {file.FullName}:\n{e}" ); - return -1; - } - } - - public static bool ChangeModMaterials( Mod mod, string from, string to ) - { - if( ValidStrings( from, to ) ) - { - return mod.AllFiles - .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) - .All( file => ChangeMtrl( file, from, to ) >= 0 ); - } - - PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." ); - return false; - } -} \ No newline at end of file From fc1255661cd07a9ce19c39083411902cb91dd4b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 20:55:04 +0200 Subject: [PATCH 0165/2451] Fix collapsed folders with unfiltered entries not showing. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 5cb708ff..2539675a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5cb708ff692d397a9e71f3315d9d054f6558f42d +Subproject commit 2539675a1bff56088ea7be1f6ad9da2f20115032 From d2f84aa976f5a9131fbee1c8880790e4741da5b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 20:55:26 +0200 Subject: [PATCH 0166/2451] Maybe fix face decals not correctly reloading in character collections. --- .../Interop/Resolver/PathResolver.Data.cs | 7 ++++--- .../Interop/Resolver/PathResolver.Meta.cs | 2 +- .../Interop/Resolver/PathResolver.Resolve.cs | 1 - Penumbra/Interop/Resolver/PathResolver.cs | 21 +++++++++++++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 57d55935..9701cd2a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -24,15 +24,16 @@ public unsafe partial class PathResolver [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; + private ModCollection? _lastCreatedCollection; + private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { - using var cmp = MetaChanger.ChangeCmp( this, out var collection ); + using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection ); var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); if( LastGameObject != null ) { - DrawObjectToObject[ ret ] = ( collection!, LastGameObject->ObjectIndex ); + DrawObjectToObject[ ret ] = (_lastCreatedCollection!, LastGameObject->ObjectIndex ); } - return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 5ac6dd0e..5267ba89 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -110,7 +110,7 @@ public unsafe partial class PathResolver // RSP public delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); - [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56 ", DetourName = "RspSetupCharacterDetour" )] + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = "RspSetupCharacterDetour" )] public Hook< RspSetupCharacterDelegate >? RspSetupCharacterHook; private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index ebdb9ff0..c283e9ce 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -12,7 +12,6 @@ public unsafe partial class PathResolver // Humans private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) => ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); - private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) => ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) ); diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index d25517da..4f73e762 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -32,11 +32,14 @@ public partial class PathResolver : IDisposable private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, object?) data ) { // Check if the path was marked for a specific collection, - // or if it is a file loaded by a material, and if we are currently in a material load. + // or if it is a file loaded by a material, and if we are currently in a material load, + // or if it is a face decal path and the current mod collection is set. // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ); + var nonDefault = HandleMaterialSubFiles( type, out var collection ) + || PathCollections.TryRemove( gamePath.Path, out collection ) + || HandleDecalFile( type, gamePath, out collection ); if( !nonDefault ) { collection = Penumbra.CollectionManager.Default; @@ -53,6 +56,20 @@ public partial class PathResolver : IDisposable return true; } + private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection ) + { + if( type == ResourceType.Tex + && _lastCreatedCollection != null + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) + { + collection = _lastCreatedCollection; + return true; + } + + collection = null; + return false; + } + public void Enable() { if( Enabled ) From 3ed85b56b5dac7c9b3b2d1feddace9fd911409af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 May 2022 13:35:54 +0200 Subject: [PATCH 0167/2451] Remove mentions of forced collection. --- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a0f1025b..6a3c23ad 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -348,7 +348,7 @@ public class Penumbra : IDalamudPlugin else { Dalamud.Chat.Print( "Missing arguments, the correct command format is:" - + " /penumbra collection {default|forced} " ); + + " /penumbra collection {default} " ); } break; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 8b2ff916..a22c3151 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -115,8 +115,7 @@ public partial class ConfigWindow DrawCollectionSelector( "##default", _window._inputTextWidth.X, ModCollection.Type.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Default Collection", - "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" - + "They also take precedence before the forced collection." ); + "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" ); } // We do not check for valid character names. From 08b7f184e62320eb23f46b77fc8a3b28f483cd53 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 May 2022 18:57:55 +0200 Subject: [PATCH 0168/2451] Simplify retargeting after reload, seems to work fine. --- Penumbra/Interop/ObjectReloader.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 50556dda..8566218d 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -104,12 +104,6 @@ public sealed unsafe class ObjectReloader : IDisposable var actor = Dalamud.Objects[ _target ]; if( actor == null || Dalamud.Targets.Target != null ) - { - _target = -1; - return; - } - - if( ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->DrawObject == null ) { return; } From ba0ef577c57bb5bb84ac868ecb7ba9b570daaa1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 12:57:31 +0200 Subject: [PATCH 0169/2451] Random cleanup. --- Penumbra/Collections/CollectionManager.Active.cs | 9 +++------ Penumbra/Collections/ModCollection.File.cs | 2 +- Penumbra/Mods/ModFileSystem.cs | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 9ae03552..80c5eadc 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -24,7 +24,7 @@ public partial class ModCollection public ModCollection Default { get; private set; } = Empty; // A single collection that can not be deleted as a fallback for the current collection. - public ModCollection DefaultName { get; private set; } = Empty; + private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. private readonly Dictionary< string, ModCollection > _characters = new(); @@ -36,11 +36,8 @@ public partial class ModCollection public ModCollection Character( string name ) => _characters.TryGetValue( name, out var c ) ? c : Default; - public bool HasCharacterCollections - => _characters.Count > 0; - // Set a active collection, can be used to set Default, Current or Character collections. - public void SetCollection( int newIdx, Type type, string? characterName = null ) + private void SetCollection( int newIdx, Type type, string? characterName = null ) { var oldCollectionIdx = type switch { @@ -120,7 +117,7 @@ public partial class ModCollection // Load default, current and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - public void LoadCollections() + private void LoadCollections() { var configChanged = !ReadActiveCollections( out var jObject ); diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 156e540f..6ca21b19 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -97,7 +97,7 @@ public partial class ModCollection // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. - public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + private static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) { inheritance = Array.Empty< string >(); if( !file.Exists ) diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 75034247..f748dc90 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -16,9 +16,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. private void Save() - { - SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); - PluginLog.Verbose( "Saved mod filesystem." ); + { + SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + PluginLog.Verbose( "Saved mod filesystem." ); } // Create a new ModFileSystem from the currently loaded mods and the current sort order file. From d8e3eafa7dfcbc840f5d7439cde6e894cf977229 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 15:31:53 +0200 Subject: [PATCH 0170/2451] Fix female Hrothgar fucking up eqdp accessories --- Penumbra/Interop/Structs/CharacterUtility.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index fc4e4bee..08fd12e3 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -10,7 +10,7 @@ public unsafe struct CharacterUtility { // TODO: female Hrothgar public static readonly int[] EqdpIndices - = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).Where( i => i != EqdpStartIdx + 15 ).ToArray(); + = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).Where( i => i != EqdpStartIdx + 15 && i != EqdpStartIdx + 15 + NumEqdpFiles / 2 ).ToArray(); public const int NumResources = 85; public const int EqpIdx = 0; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 785d3d2e..96daf6a0 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -14,7 +14,7 @@ public partial class MetaManager { public struct MetaManagerEqdp : IDisposable { - public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 1]; // TODO: female Hrothgar + public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar public readonly Dictionary< EqdpManipulation, int > Manipulations = new(); From 0b9a48a485c3f9ee02965c3016855aa7e9c514eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 20:57:52 +0200 Subject: [PATCH 0171/2451] Maybe fix a weird debugging crash? --- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 3be1b0ec..41ba6c48 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -47,7 +47,7 @@ public unsafe partial class ResourceLoader private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) { - if( manipulatedPath == null ) + if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 ) { return; } From f0af9f12746b3ed01dae5ef5a35b1b03202121c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 20:58:21 +0200 Subject: [PATCH 0172/2451] A bunch of filesystem fixes. --- OtterGui | 2 +- Penumbra.GameData/ByteString/FullPath.cs | 6 +++--- Penumbra/Mods/ModFileSystem.cs | 4 ++-- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 11 ++++++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 2539675a..dd26a7f4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2539675a1bff56088ea7be1f6ad9da2f20115032 +Subproject commit dd26a7f416106a5bed4ea68c500dfcd5c0818d59 diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index 5ccf71c2..1d13e969 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -13,7 +13,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > public readonly Utf8String InternalName; public readonly ulong Crc64; - public static readonly FullPath Empty = new(string.Empty); + public static readonly FullPath Empty = new(string.Empty); public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath ) : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) @@ -71,9 +71,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > public int CompareTo( object? obj ) => obj switch { - FullPath p => InternalName.CompareTo( p.InternalName ), + FullPath p => InternalName?.CompareTo( p.InternalName ) ?? -1, FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), - Utf8String u => InternalName.CompareTo( u ), + Utf8String u => InternalName?.CompareTo( u ) ?? -1, string s => string.Compare( FullName, s, StringComparison.InvariantCultureIgnoreCase ), _ => -1, }; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index f748dc90..8f6a61a3 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -68,7 +68,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) { var old = oldName.FixName(); - if( Find( old, out var child ) ) + if( Find( old, out var child ) && child is not Folder) { Rename( child, mod.Name.Text ); } @@ -117,7 +117,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable private static (string, bool) SaveMod( Mod mod, string fullPath ) { - var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?" ); + var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?$" ); // Only save pairs with non-default paths. if( regex.IsMatch( fullPath ) ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 7c149d19..365f3308 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -5,6 +5,7 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Logging; using Dalamud.Memory; using ImGuiNET; using OtterGui; @@ -45,7 +46,14 @@ public partial class ConfigWindow if( Input.Text( "Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) ) { - _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); + try + { + _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); + } + catch( Exception e ) + { + PluginLog.Warning( e.Message ); + } } ImGui.Dummy( _window._defaultSpace ); @@ -138,6 +146,7 @@ public partial class ConfigWindow _window.ModEditPopup.ChangeOption( -1, 0 ); _window.ModEditPopup.IsOpen = true; } + ImGui.SameLine(); fileExists = File.Exists( _mod.DefaultFile ); tt = fileExists From e85d57b09479e7a6325fd7cc0f1d8c13ac389a5d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 May 2022 21:02:18 +0200 Subject: [PATCH 0173/2451] Allow extracting identically named options again. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 18 +++++++++++++++--- Penumbra/Mods/Mod.Creation.cs | 8 ++++---- Penumbra/Mods/Mod.Files.cs | 13 ++++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 65b757c2..a6339f5c 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -112,6 +112,17 @@ public partial class TexToolsImporter .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) + ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) ); + private static string GetGroupName( string groupName, ISet< string > names ) + { + var baseName = groupName; + var i = 2; + while( !names.Add( groupName ) ) + { + groupName = $"{baseName} ({i++})"; + } + return groupName; + } + // Extended V2 mod packs contain multiple options that need to be handled separately. private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) { @@ -144,14 +155,15 @@ public partial class TexToolsImporter // Iterate through all pages var options = new List< ISubMod >(); var groupPriority = 0; + var groupNames = new HashSet< string >(); foreach( var page in modList.ModPackPages ) { foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) ) { - _currentGroupName = group.GroupName; + _currentGroupName = GetGroupName( group.GroupName, groupNames ); options.Clear(); var description = new StringBuilder(); - var groupFolder = Mod.NewSubFolderName( _currentModDirectory, group.GroupName ) + var groupFolder = Mod.NewSubFolderName( _currentModDirectory, _currentGroupName ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, $"Group {groupPriority + 1}" ) ); var optionIdx = 1; @@ -186,7 +198,7 @@ public partial class TexToolsImporter } } - Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority, groupPriority, description.ToString(), options ); + Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, _currentGroupName, groupPriority, groupPriority, description.ToString(), options ); ++groupPriority; } } diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index c6387fc6..3c845490 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -58,16 +58,16 @@ public partial class Mod } // Create a file for an option group from given data. - internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, + internal static void CreateOptionGroup( DirectoryInfo baseFolder, SelectType type, string name, int priority, int index, string desc, IEnumerable< ISubMod > subMods ) { - switch( groupData.SelectionType ) + switch( type ) { case SelectType.Multi: { var group = new MultiModGroup() { - Name = groupData.GroupName, + Name = name, Description = desc, Priority = priority, }; @@ -79,7 +79,7 @@ public partial class Mod { var group = new SingleModGroup() { - Name = groupData.GroupName, + Name = name, Description = desc, Priority = priority, }; diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index c66ab9a4..c4a1257a 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -105,13 +105,24 @@ public partial class Mod private void LoadAllGroups() { _groups.Clear(); + var changes = false; foreach( var file in GroupFiles ) { var group = LoadModGroup( file, ModPath ); - if( group != null ) + if( group != null && _groups.All( g => g.Name != group.Name ) ) { + changes = changes || group.FileName( ModPath, _groups.Count ) != file.FullName; _groups.Add( group ); } + else + { + changes = true; + } + } + + if( changes ) + { + SaveAllGroups(); } } From f3d4ffc40a4b426b6946bf31411f9ad713a7f3b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 12 May 2022 16:49:29 +0200 Subject: [PATCH 0174/2451] Change Disable Audio Streaming checkbox to be clearer. --- Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 79280aee..dd3a0f01 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -78,10 +78,10 @@ public partial class ConfigWindow } ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Disable Audio Streaming", - "Disable streaming in the games audio engine.\n" - + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" - + "Only touch this if you experience sound problems.\n" + ImGuiUtil.LabeledHelpMarker( "Enable Sound Modification", + "Disable streaming in the games audio engine. The game enables this by default, and Penumbra should disable it.\n" + + "If this is unchecked, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems like audio stuttering.\n" + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash.\n" + "You might need to restart your game for this to fully take effect." ); } From b8210e094b6cb61ed17dd5dcd1b5986525433df2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 12 May 2022 16:49:49 +0200 Subject: [PATCH 0175/2451] Add zip backup options to mods. --- Penumbra/Mods/Editor/ModBackup.cs | 94 +++++++++++++++++++++++ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 36 ++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Mods/Editor/ModBackup.cs diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs new file mode 100644 index 00000000..acc9e8af --- /dev/null +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +// Utility to create and apply a zipped backup of a mod. +public class ModBackup +{ + public static bool CreatingBackup { get; private set; } + + private readonly Mod _mod; + public readonly string Name; + public readonly bool Exists; + + public ModBackup( Mod mod ) + { + _mod = mod; + Name = mod.ModPath + ".zip"; + Exists = File.Exists( Name ); + } + + // Create a backup zip without blocking the main thread. + public async void CreateAsync() + { + if( CreatingBackup ) + { + return; + } + + CreatingBackup = true; + await Task.Run( Create ); + CreatingBackup = false; + } + + + // Create a backup. Overwrites pre-existing backups. + private void Create() + { + try + { + Delete(); + ZipFile.CreateFromDirectory( _mod.ModPath.FullName, Name, CompressionLevel.Optimal, false ); + PluginLog.Debug( "Created backup file {backupName} from {modDirectory}.", Name, _mod.ModPath.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not backup mod {_mod.Name} to \"{Name}\":\n{e}" ); + } + } + + // Delete a pre-existing backup. + public void Delete() + { + if( !Exists ) + { + return; + } + + try + { + File.Delete( Name ); + PluginLog.Debug( "Deleted backup file {backupName}.", Name ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete file \"{Name}\":\n{e}" ); + } + } + + // Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar. + // Does an automatic reload after extraction. + public void Restore() + { + try + { + if( Directory.Exists( _mod.ModPath.FullName ) ) + { + Directory.Delete( _mod.ModPath.FullName, true ); + PluginLog.Debug( "Deleted mod folder {modFolder}.", _mod.ModPath.FullName ); + } + + ZipFile.ExtractToDirectory( Name, _mod.ModPath.FullName ); + PluginLog.Debug( "Extracted backup file {backupName} to {modName}.", Name, _mod.ModPath.FullName ); + Penumbra.ModManager.ReloadMod( _mod.Index ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not restore {_mod.Name} from backup \"{Name}\":\n{e}" ); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 365f3308..d2038205 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -6,7 +6,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Logging; -using Dalamud.Memory; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -90,11 +89,44 @@ public partial class ConfigWindow Penumbra.ModManager.ReloadMod( _mod.Index ); } - + BackupButtons( buttonSize ); MoveDirectory.Draw( _mod, buttonSize ); + ImGui.Dummy( _window._defaultSpace ); } + private void BackupButtons( Vector2 buttonSize ) + { + var backup = new ModBackup( _mod ); + var tt = ModBackup.CreatingBackup + ? "Already creating a backup." + : backup.Exists + ? $"Overwrite current backup \"{backup.Name}\" with current mod." + : $"Create backup archive of current mod at \"{backup.Name}\"."; + if( ImGuiUtil.DrawDisabledButton( "Create Backup", buttonSize, tt, ModBackup.CreatingBackup ) ) + { + backup.CreateAsync(); + } + + ImGui.SameLine(); + tt = backup.Exists + ? $"Delete existing backup file \"{backup.Name}\"." + : $"Backup file \"{backup.Name}\" does not exist."; + if( ImGuiUtil.DrawDisabledButton( "Delete Backup", buttonSize, tt, !backup.Exists ) ) + { + backup.Delete(); + } + + tt = backup.Exists + ? $"Restore mod from backup file \"{backup.Name}\"." + : $"Backup file \"{backup.Name}\" does not exist."; + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Restore From Backup", buttonSize, tt, !backup.Exists ) ) + { + backup.Restore(); + } + } + // Anything about editing the regular meta information about the mod. private void EditRegularMeta() { From 67de0ccf4540bc08a83d571c6bc6871155d696c4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 12 May 2022 17:33:54 +0200 Subject: [PATCH 0176/2451] Make saving files and recalculating effective files threaded/once per frame. --- .../Collections/CollectionManager.Active.cs | 7 +- .../Collections/ModCollection.Cache.Access.cs | 11 ++- Penumbra/Collections/ModCollection.Cache.cs | 48 ++++++------ Penumbra/Collections/ModCollection.File.cs | 5 +- Penumbra/Configuration.cs | 5 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Mods/Mod.Creation.cs | 4 +- Penumbra/Mods/Mod.Files.cs | 2 +- Penumbra/Mods/Mod.Meta.Migration.cs | 2 +- Penumbra/Mods/Mod.Meta.cs | 3 + Penumbra/Mods/ModFileSystem.cs | 13 +++- Penumbra/Mods/Subclasses/IModGroup.cs | 10 ++- Penumbra/Penumbra.cs | 2 + Penumbra/Util/FrameworkManager.cs | 75 +++++++++++++++++++ 14 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 Penumbra/Util/FrameworkManager.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 80c5eadc..cf6f036f 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -177,7 +177,10 @@ public partial class ModCollection public void SaveActiveCollections() - => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ) ); + { + Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), + () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ) ) ); + } internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters ) { @@ -203,7 +206,7 @@ public partial class ModCollection j.WriteEndObject(); j.WriteEndObject(); - PluginLog.Verbose( "Active Collections saved." ); + PluginLog.Verbose( "Active Collections saved." ); } catch( Exception e ) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 1720632a..8de2e1cc 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using Dalamud.Logging; using OtterGui.Classes; using Penumbra.GameData.ByteString; @@ -61,9 +62,6 @@ public partial class ModCollection internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); - internal IReadOnlySet< FullPath > MissingFiles - => _cache?.MissingFiles ?? new HashSet< FullPath >(); - internal IReadOnlyDictionary< string, object? > ChangedItems => _cache?.ChangedItems ?? new Dictionary< string, object? >(); @@ -76,6 +74,10 @@ public partial class ModCollection // Update the effective file list for the given cache. // Creates a cache if necessary. public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) + => Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name, + () => CalculateEffectiveFileListInternal( withMetaManipulations, reloadDefault ) ); + + private void CalculateEffectiveFileListInternal( bool withMetaManipulations, bool reloadDefault ) { // Skip the empty collection. if( Index == 0 ) @@ -83,7 +85,7 @@ public partial class ModCollection return; } - PluginLog.Debug( "Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Thread.CurrentThread.ManagedThreadId, Name, withMetaManipulations, reloadDefault ); _cache ??= new Cache( this ); _cache.CalculateEffectiveFileList( withMetaManipulations ); @@ -92,6 +94,7 @@ public partial class ModCollection SetFiles(); Penumbra.ResidentResources.Reload(); } + PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", Thread.CurrentThread.ManagedThreadId, Name); } // Set Metadata files. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 20e3cc51..254502f3 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using Dalamud.Logging; +using Dalamud.Utility; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; @@ -15,15 +18,12 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - // Shared caches to avoid allocations. - private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024); - private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024); - private static readonly List< ModSettings? > ResolvedSettings = new(128); + private readonly Dictionary< Utf8GamePath, FileRegister > _registeredFiles = new(); + private readonly Dictionary< MetaManipulation, FileRegister > _registeredManipulations = new(); private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); public readonly MetaManager MetaManipulations; public ConflictCache Conflicts = new(); @@ -96,14 +96,10 @@ public partial class ModCollection // Clear all local and global caches to prepare for recomputation. private void ClearStorageAndPrepare() { - ResolvedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); _changedItems.Clear(); - ResolvedSettings.Clear(); + _registeredFiles.EnsureCapacity( 2 * ResolvedFiles.Count ); + ResolvedFiles.Clear(); Conflicts.ClearFileConflicts(); - // Obtains actual settings for this collection with all inheritances. - ResolvedSettings.AddRange( _collection.ActualSettings ); } // Recalculate all file changes from current settings. Include all fixed custom redirects. @@ -113,7 +109,7 @@ public partial class ModCollection ClearStorageAndPrepare(); if( withManipulations ) { - RegisteredManipulations.Clear(); + _registeredManipulations.EnsureCapacity( 2 * MetaManipulations.Count ); MetaManipulations.Reset(); } @@ -125,6 +121,10 @@ public partial class ModCollection AddMetaFiles(); ++RecomputeCounter; + _registeredFiles.Clear(); + _registeredFiles.TrimExcess(); + _registeredManipulations.Clear(); + _registeredManipulations.TrimExcess(); } // Identify and record all manipulated objects for this entire collection. @@ -158,15 +158,15 @@ public partial class ModCollection // Inside the same mod, conflicts are not recorded. private void AddFile( Utf8GamePath path, FullPath file, FileRegister priority ) { - if( RegisteredFiles.TryGetValue( path, out var register ) ) + if( _registeredFiles.TryGetValue( path, out var register ) ) { if( register.SameMod( priority, out var less ) ) { Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path ); if( less ) { - RegisteredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; + _registeredFiles[ path ] = priority; + ResolvedFiles[ path ] = file; } } else @@ -176,14 +176,14 @@ public partial class ModCollection // Do not add conflicts. if( less ) { - RegisteredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; + _registeredFiles[ path ] = priority; + ResolvedFiles[ path ] = file; } } } else // File not seen before, just add it. { - RegisteredFiles.Add( path, priority ); + _registeredFiles.Add( path, priority ); ResolvedFiles.Add( path, file ); } } @@ -194,14 +194,14 @@ public partial class ModCollection // Inside the same mod, conflicts are not recorded. private void AddManipulation( MetaManipulation manip, FileRegister priority ) { - if( RegisteredManipulations.TryGetValue( manip, out var register ) ) + if( _registeredManipulations.TryGetValue( manip, out var register ) ) { if( register.SameMod( priority, out var less ) ) { Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, manip ); if( less ) { - RegisteredManipulations[ manip ] = priority; + _registeredManipulations[ manip ] = priority; MetaManipulations.ApplyMod( manip, priority.ModIdx ); } } @@ -212,14 +212,14 @@ public partial class ModCollection // Do not add conflicts. if( less ) { - RegisteredManipulations[ manip ] = priority; + _registeredManipulations[ manip ] = priority; MetaManipulations.ApplyMod( manip, priority.ModIdx ); } } } else // Manipulation not seen before, just add it. { - RegisteredManipulations[ manip ] = priority; + _registeredManipulations[ manip ] = priority; MetaManipulations.ApplyMod( manip, priority.ModIdx ); } } @@ -250,7 +250,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. private void AddMod( int modIdx, bool withManipulations ) { - var settings = ResolvedSettings[ modIdx ]; + var settings = _collection[ modIdx ].Settings; if( settings is not { Enabled: true } ) { return; @@ -300,7 +300,7 @@ public partial class ModCollection Penumbra.Redirects.Apply( ResolvedFiles ); foreach( var gamePath in ResolvedFiles.Keys ) { - RegisteredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); + _registeredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); } } diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 6ca21b19..5c8aa521 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -22,7 +22,7 @@ public partial class ModCollection => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); // Custom serialization due to shared mod information across managers. - public void Save() + private void SaveCollection() { try { @@ -71,6 +71,9 @@ public partial class ModCollection } } + public void Save() + => Penumbra.Framework.RegisterDelayed( nameof( SaveCollection ) + Name, SaveCollection ); + public void Delete() { if( Index == 0 ) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e5c8a899..209b726b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -64,7 +64,7 @@ public partial class Configuration : IPluginConfiguration } // Save the current configuration. - public void Save() + private void SaveConfiguration() { try { @@ -76,6 +76,9 @@ public partial class Configuration : IPluginConfiguration } } + public void Save() + => Penumbra.Framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); + // Add missing colors to the dictionary if necessary. private void AddColors( bool forceSave ) { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index bfe29336..9a68fb15 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -426,7 +426,7 @@ public sealed partial class Mod } else { - IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.ModPath, groupIdx ); + IModGroup.SaveDelayed( mod._groups[ groupIdx ], mod.ModPath, groupIdx ); } } diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 3c845490..85a5d264 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -72,7 +72,7 @@ public partial class Mod Priority = priority, }; group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.SaveModGroup( group, baseFolder, index ); + IModGroup.Save( group, baseFolder, index ); break; } case SelectType.Single: @@ -84,7 +84,7 @@ public partial class Mod Priority = priority, }; group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.SaveModGroup( group, baseFolder, index ); + IModGroup.Save( group, baseFolder, index ); break; } } diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index c4a1257a..a10df895 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -147,7 +147,7 @@ public partial class Mod foreach( var (group, index) in _groups.WithIndex() ) { - IModGroup.SaveModGroup( group, ModPath, index ); + IModGroup.SaveDelayed( group, ModPath, index ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index dd4e79c8..bf6a681a 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -83,7 +83,7 @@ public sealed partial class Mod mod._default.IncorporateMetaChanges( mod.ModPath, true ); foreach( var (group, index) in mod.Groups.WithIndex() ) { - IModGroup.SaveModGroup( group, mod.ModPath, index ); + IModGroup.Save( group, mod.ModPath, index ); } // Delete meta files. diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index c9493819..fad75f3d 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -114,6 +114,9 @@ public sealed partial class Mod } private void SaveMeta() + => Penumbra.Framework.RegisterDelayed( nameof( SaveMetaFile ) + ModPath.Name, SaveMetaFile ); + + private void SaveMetaFile() { var metaFile = MetaFile; try diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 8f6a61a3..fef26f5e 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -15,12 +15,15 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. - private void Save() - { - SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); - PluginLog.Verbose( "Saved mod filesystem." ); + private void SaveFilesystem() + { + SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + PluginLog.Verbose( "Saved mod filesystem." ); } + private void Save() + => Penumbra.Framework.RegisterDelayed( nameof( SaveFilesystem ), SaveFilesystem ); + // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystem Load() { @@ -50,6 +53,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { Save(); } + PluginLog.Debug( "Reloaded mod filesystem." ); } @@ -98,6 +102,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { Delete( leaf ); } + break; case ModPathChangeType.Moved: Save(); diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index bea449ee..03fc5685 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -57,7 +57,15 @@ public interface IModGroup : IEnumerable< ISubMod > } } - public static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx ) + public static void SaveDelayed( IModGroup group, DirectoryInfo basePath, int groupIdx ) + { + Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", () => SaveModGroup( group, basePath, groupIdx ) ); + } + + public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx ) + => SaveModGroup( group, basePath, groupIdx ); + + private static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx ) { var file = group.FileName( basePath, groupIdx ); using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6a3c23ad..8c8148aa 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -44,6 +44,7 @@ public class Penumbra : IDalamudPlugin public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static SimpleRedirectManager Redirects { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; + public static FrameworkManager Framework { get; private set; } = null!; public readonly ResourceLogger ResourceLogger; @@ -62,6 +63,7 @@ public class Penumbra : IDalamudPlugin public Penumbra( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); + Framework = new FrameworkManager(); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); diff --git a/Penumbra/Util/FrameworkManager.cs b/Penumbra/Util/FrameworkManager.cs new file mode 100644 index 00000000..393a9fe1 --- /dev/null +++ b/Penumbra/Util/FrameworkManager.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Game; + +namespace Penumbra.Util; + +// Manage certain actions to only occur on framework updates. +public class FrameworkManager : IDisposable +{ + private readonly Dictionary< string, Action > _important = new(); + private readonly Dictionary< string, Action > _delayed = new(); + + public FrameworkManager() + => Dalamud.Framework.Update += OnUpdate; + + // Register an action that is not time critical. + // One action per frame will be executed. + // On dispose, any remaining actions will be executed. + public void RegisterDelayed( string tag, Action action ) + => _delayed[ tag ] = action; + + // Register an action that should be executed on the next frame. + // All of those actions will be executed in the next frame. + // If there are more than one, they will be launched in separated tasks, but waited for. + public void RegisterImportant( string tag, Action action ) + => _important[ tag ] = action; + + public void Dispose() + { + Dalamud.Framework.Update -= OnUpdate; + HandleAll( _delayed ); + } + + private void OnUpdate( Framework _ ) + { + HandleOne(); + HandleAllTasks( _important ); + } + + private void HandleOne() + { + if( _delayed.Count > 0 ) + { + var (key, action) = _delayed.First(); + action(); + _delayed.Remove( key ); + } + } + + private static void HandleAll( IDictionary< string, Action > dict ) + { + foreach( var (_, action) in dict ) + { + action(); + } + + dict.Clear(); + } + + private static void HandleAllTasks( IDictionary< string, Action > dict ) + { + if( dict.Count < 2 ) + { + HandleAll( dict ); + } + else + { + var tasks = dict.Values.Select( Task.Run ).ToArray(); + Task.WaitAll( tasks ); + dict.Clear(); + } + } +} \ No newline at end of file From 17f9c2ee6f06713d525716fab35076dae2acadcc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 12 May 2022 17:57:40 +0200 Subject: [PATCH 0177/2451] Fix right-click popups for things with identical names but different paths. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index dd26a7f4..85e579db 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit dd26a7f416106a5bed4ea68c500dfcd5c0818d59 +Subproject commit 85e579db09a0253e97fc153cff401531a8a368af From aa0584078b5840f2f30ad849c550e1285caacf16 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 12 May 2022 18:42:53 +0200 Subject: [PATCH 0178/2451] Fix conflicts not sorting because dumb. --- Penumbra/Collections/ConflictCache.cs | 11 +++++++++++ Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/ConflictCache.cs b/Penumbra/Collections/ConflictCache.cs index 7ff26f76..e1bf9f42 100644 --- a/Penumbra/Collections/ConflictCache.cs +++ b/Penumbra/Collections/ConflictCache.cs @@ -55,6 +55,15 @@ public struct ConflictCache _ => 0, }; } + + public override string ToString() + => ( Mod1Priority, Solved ) switch + { + (true, true) => $"{Penumbra.ModManager[ Mod1 ].Name} > {Penumbra.ModManager[ Mod2 ].Name} ({Data})", + (true, false) => $"{Penumbra.ModManager[ Mod1 ].Name} >= {Penumbra.ModManager[ Mod2 ].Name} ({Data})", + (false, true) => $"{Penumbra.ModManager[ Mod1 ].Name} < {Penumbra.ModManager[ Mod2 ].Name} ({Data})", + (false, false) => $"{Penumbra.ModManager[ Mod1 ].Name} <= {Penumbra.ModManager[ Mod2 ].Name} ({Data})", + }; } private readonly List< Conflict > _conflicts = new(); @@ -75,6 +84,7 @@ public struct ConflictCache // Find all mod conflicts concerning the specified mod (in both directions). public SubList< Conflict > ModConflicts( int modIdx ) { + Sort(); var start = _conflicts.FindIndex( c => c.Mod1 == modIdx ); if( start < 0 ) { @@ -90,6 +100,7 @@ public struct ConflictCache if( !_isSorted ) { _conflicts?.Sort(); + _isSorted = true; } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index a1081ac5..2b995701 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -105,7 +105,7 @@ public partial class ConfigWindow return; } - using var box = ImRaii.ListBox( "##conflicts" ); + using var box = ImRaii.ListBox( "##conflicts", -Vector2.One ); if( !box ) { return; From 856c1d089cf9e9c9879323653f3298208da9bc08 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 14:29:50 +0200 Subject: [PATCH 0179/2451] Fix crash when changing state of multiple mods at once. --- Penumbra/Collections/ModCollection.Inheritance.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 7bef9e09..9b2706ca 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Logging; using OtterGui.Filesystem; @@ -108,9 +109,19 @@ public partial class ModCollection // Carry changes in collections inherited from forward if they are relevant for this collection. private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { - if( _settings[ modIdx ] == null ) + switch( type ) { - ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); + return; + default: + if( _settings[ modIdx ] == null ) + { + ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); + } + + return; } } From 976f7840cd238fd505025ff8de6dc288d2544ac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 15:01:05 +0200 Subject: [PATCH 0180/2451] Add right-click context to mod selector when not hovering items. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 85e579db..ddf00d61 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 85e579db09a0253e97fc153cff401531a8a368af +Subproject commit ddf00d613a058d30cb3a3559b45de3ffebf049aa From a86a73bbf53bec4918ad9a0e0a7d28c2c73dd5c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 15:01:50 +0200 Subject: [PATCH 0181/2451] Make the limit of 32 options for a multi-select group explicit and handle it better. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 73 +++++++++++-------- Penumbra/Mods/Subclasses/IModGroup.cs | 2 + .../Subclasses/Mod.Files.MultiModGroup.cs | 6 ++ Penumbra/Mods/Subclasses/ModSettings.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 27 +++++-- 5 files changed, 73 insertions(+), 37 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index a6339f5c..0b461860 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -120,6 +120,7 @@ public partial class TexToolsImporter { groupName = $"{baseName} ({i++})"; } + return groupName; } @@ -160,46 +161,58 @@ public partial class TexToolsImporter { foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) ) { + var allOptions = group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ).ToList(); + var (numGroups, maxOptions) = group.SelectionType == SelectType.Single + ? ( 1, allOptions.Count ) + : ( 1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions ); _currentGroupName = GetGroupName( group.GroupName, groupNames ); - options.Clear(); - var description = new StringBuilder(); - var groupFolder = Mod.NewSubFolderName( _currentModDirectory, _currentGroupName ) - ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, $"Group {groupPriority + 1}" ) ); - var optionIdx = 1; - - foreach( var option in group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ) ) + var optionIdx = 0; + for( var groupId = 0; groupId < numGroups; ++groupId ) { - _token.ThrowIfCancellationRequested(); - _currentOptionName = option.Name; - var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name ) - ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {optionIdx}" ) ); - ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) ); - description.Append( option.Description ); - if( !string.IsNullOrEmpty( option.Description ) ) + var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; + options.Clear(); + var description = new StringBuilder(); + var groupFolder = Mod.NewSubFolderName( _currentModDirectory, name ) + ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, + numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); + + for( var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i ) { - description.Append( '\n' ); + var option = allOptions[ i + optionIdx ]; + _token.ThrowIfCancellationRequested(); + _currentOptionName = option.Name; + var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name ) + ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); + ExtractSimpleModList( optionFolder, option.ModsJsons ); + options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) ); + description.Append( option.Description ); + if( !string.IsNullOrEmpty( option.Description ) ) + { + description.Append( '\n' ); + } + + ++_currentOptionIdx; } - ++optionIdx; - ++_currentOptionIdx; - } + optionIdx += maxOptions; - // Handle empty options for single select groups without creating a folder for them. - // We only want one of those at most, and it should usually be the first option. - if( group.SelectionType == SelectType.Single ) - { - var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); - if( empty != null ) + // Handle empty options for single select groups without creating a folder for them. + // We only want one of those at most, and it should usually be the first option. + if( group.SelectionType == SelectType.Single ) { - _currentOptionName = empty.Name; - options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); + if( empty != null ) + { + _currentOptionName = empty.Name; + options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + } } - } - Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, _currentGroupName, groupPriority, groupPriority, description.ToString(), options ); - ++groupPriority; + Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + description.ToString(), options ); + ++groupPriority; + } } } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 03fc5685..8482af4a 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -15,6 +15,8 @@ public enum SelectType public interface IModGroup : IEnumerable< ISubMod > { + public const int MaxMultiOptions = 32; + public string Name { get; } public string Description { get; } public SelectType Type { get; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 26d3abe3..d84ae760 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; @@ -57,6 +58,11 @@ public partial class Mod { foreach( var child in options.Children() ) { + if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) + { + PluginLog.Warning($"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options." ); + break; + } var subMod = new SubMod(); subMod.Load( basePath, child, out var priority ); ret.PrioritizedOptions.Add( ( subMod, priority ) ); diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 3f6b7b53..50341ae9 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -101,7 +101,7 @@ public class ModSettings => group.Type switch { SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), - SelectType.Multi => ( uint )( value & ( ( 1 << group.Count ) - 1 ) ), + SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), _ => value, }; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index d2038205..a878e293 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -518,8 +518,13 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); + var canAddGroup = mod.Groups[ groupIdx ].Type != SelectType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; + var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; + var tt = canAddGroup + ? validName ? "Add a new option to this group." : "Please enter a name for the new option." + : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - "Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) ) + tt, !( canAddGroup && validName ), true ) ) { Penumbra.ModManager.AddOption( mod, groupIdx, _newOptionName ); _newOptionName = string.Empty; @@ -598,12 +603,22 @@ public partial class ConfigWindow return; } - foreach( var type in new[] { SelectType.Single, SelectType.Multi } ) + if( ImGui.Selectable( GroupTypeName( SelectType.Single ), group.Type == SelectType.Single ) ) { - if( ImGui.Selectable( GroupTypeName( type ), @group.Type == type ) ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type ); - } + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, SelectType.Single ); + } + + var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti ); + if( ImGui.Selectable( GroupTypeName( SelectType.Multi ), group.Type == SelectType.Multi ) && canSwitchToMulti ) + { + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, SelectType.Multi ); + } + + style.Pop(); + if( !canSwitchToMulti ) + { + ImGuiUtil.HoverTooltip( $"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options." ); } } From f5591f2a4a566015b87142896d6e58077b53db44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 15:10:36 +0200 Subject: [PATCH 0182/2451] Small delay fix. --- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 9a68fb15..ea8c838e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -422,7 +422,7 @@ public sealed partial class Mod { if( groupIdx == -1 ) { - mod.SaveDefaultMod(); + mod.SaveDefaultModDelayed(); } else { diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 22d2aaee..a1500504 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -36,6 +36,9 @@ public partial class Mod ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 ); } + private void SaveDefaultModDelayed() + => Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod ); + private void LoadDefaultOption() { var defaultFile = DefaultFile; From 448a745a510ce51c4b083d53300a1cd2e3846090 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 16:06:05 +0200 Subject: [PATCH 0183/2451] Use default collection as default if no active collections exist. --- Penumbra/Collections/CollectionManager.Active.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index cf6f036f..519936a2 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -122,7 +122,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name; + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? (configChanged ? DefaultCollection : Empty.Name); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { From 1fd31f30ef600a8051011a258e953e13c3047c7f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 16:07:29 +0200 Subject: [PATCH 0184/2451] Add help popup for mod selector. --- OtterGui | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 146 ++++++++++++++----- 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/OtterGui b/OtterGui index ddf00d61..c3d87107 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ddf00d613a058d30cb3a3559b45de3ffebf049aa +Subproject commit c3d87107549b92408c6b45f9a76228d052f24c5a diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 92b27f50..ad711265 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; @@ -35,6 +32,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod SubscribeRightClickFolder( OwnDescendants, 15 ); AddButton( AddNewModButton, 0 ); AddButton( AddImportModButton, 1 ); + AddButton( AddHelpButton, 800 ); AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); @@ -77,6 +75,29 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override uint FolderLineColor => ColorId.FolderLine.Value(); + protected override void DrawPopups() + { + _fileManager.Draw(); + DrawHelpPopup(); + DrawInfoPopup(); + + if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) + { + try + { + var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); + Mod.CreateDefaultFiles( newDir ); + Penumbra.ModManager.AddMod( newDir ); + _newModName = string.Empty; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); + } + } + } + protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; @@ -123,29 +144,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod // Add custom buttons. private string _newModName = string.Empty; - private void AddNewModButton( Vector2 size ) + private static void AddNewModButton( Vector2 size ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", !Penumbra.ModManager.Valid, true ) ) { ImGui.OpenPopup( "Create New Mod" ); } - - if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) - { - try - { - var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); - Mod.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); - _newModName = string.Empty; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); - } - } } // Add an import mods button that opens a file selector. @@ -154,26 +159,25 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void AddImportModButton( Vector2 size ) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, + if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) ) { - var modPath = _hasSetFolder ? null - : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; - _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => - { - if( s ) - { - _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), - AddNewMod ); - ImGui.OpenPopup( "Import Status" ); - } - }, 0, modPath ); + return; } - _fileManager.Draw(); - DrawInfoPopup(); + var modPath = _hasSetFolder ? null + : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath + : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; + _hasSetFolder = true; + _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => + { + if( s ) + { + _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), + AddNewMod ); + ImGui.OpenPopup( "Import Status" ); + } + }, 0, modPath ); } // Draw the progress information for import. @@ -183,9 +187,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod var height = Math.Max( display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing() ); var width = display.X / 8; var size = new Vector2( width * 2, height ); - var pos = ( display - size ) / 2; + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); ImGui.SetNextWindowSize( size ); - ImGui.SetNextWindowPos( pos ); using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); if( _import == null || !popup.Success ) { @@ -257,6 +260,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + private static void AddHelpButton( Vector2 size ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true ) ) + { + ImGui.OpenPopup( "ExtendedHelp" ); + } + } // Helpers. private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) @@ -356,4 +366,64 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod _lastSelectedDirectory = string.Empty; } } + + private static void DrawHelpPopup() + { + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); + ImGui.SetNextWindowSize( new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34 * ImGui.GetTextLineHeightWithSpacing() ) ); + using var popup = ImRaii.Popup( "ExtendedHelp", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.Modal ); + if( !popup ) + { + return; + } + + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Selector" ); + ImGui.BulletText( "Select a mod to obtain more information or change settings." ); + ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); + using var indent = ImRaii.PushIndent(); + ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); + ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); + ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); + ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); + ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "disabled in all inherited collections." ); + ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); + ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); + ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(), "enabled and conflicting with another enabled Mod on the same priority." ); + ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." ); + ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" ); + indent.Pop( 1 ); + ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." ); + indent.Push(); + ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); + indent.Pop( 1 ); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); + ImGui.BulletText( "Right-clicking a folder opens a context menu." ); + ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); + ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); + indent.Push(); + ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); + ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); + ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); + indent.Pop( 1 ); + ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Management" ); + ImGui.BulletText( "You can create empty mods or import TTMP-based mods with the buttons in this row." ); + ImGui.BulletText( + "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); + ImGui.BulletText( + "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.SetCursorPosX( 400 * ImGuiHelpers.GlobalScale ); + if( ImGui.Button( "Understood", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) + { + ImGui.CloseCurrentPopup(); + } + } } \ No newline at end of file From 4a206a633e6fd3c7e237f261a809325aae105841 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 17:27:21 +0200 Subject: [PATCH 0185/2451] Add help for inheritance. --- OtterGui | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 100 ++++++++---------- ...ConfigWindow.CollectionsTab.Inheritance.cs | 31 +++++- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/OtterGui b/OtterGui index c3d87107..cbc29200 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c3d87107549b92408c6b45f9a76228d052f24c5a +Subproject commit cbc29200e8b80d264c8a326cdc62e841e12d1c53 diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ad711265..7c4c5280 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -369,61 +369,51 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private static void DrawHelpPopup() { - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); - ImGui.SetNextWindowSize( new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34 * ImGui.GetTextLineHeightWithSpacing() ) ); - using var popup = ImRaii.Popup( "ExtendedHelp", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.Modal ); - if( !popup ) + ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 33.5f * ImGui.GetTextLineHeightWithSpacing() ), () => { - return; - } - - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information or change settings." ); - ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); - using var indent = ImRaii.PushIndent(); - ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "disabled in all inherited collections." ); - ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), - "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); - ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(), "enabled and conflicting with another enabled Mod on the same priority." ); - ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." ); - ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" ); - indent.Pop( 1 ); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." ); - indent.Push(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); - indent.Pop( 1 ); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); - ImGui.BulletText( "Right-clicking a folder opens a context menu." ); - ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); - indent.Push(); - ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - indent.Pop( 1 ); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Management" ); - ImGui.BulletText( "You can create empty mods or import TTMP-based mods with the buttons in this row." ); - ImGui.BulletText( - "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); - ImGui.BulletText( - "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.SetCursorPosX( 400 * ImGuiHelpers.GlobalScale ); - if( ImGui.Button( "Understood", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) - { - ImGui.CloseCurrentPopup(); - } + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.TextUnformatted( "Mod Selector" ); + ImGui.BulletText( "Select a mod to obtain more information or change settings." ); + ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); + using var indent = ImRaii.PushIndent(); + ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); + ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); + ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); + ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); + ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "disabled in all inherited collections." ); + ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); + ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); + ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(), + "enabled and conflicting with another enabled Mod on the same priority." ); + ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." ); + ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" ); + indent.Pop( 1 ); + ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." ); + indent.Push(); + ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); + indent.Pop( 1 ); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); + ImGui.BulletText( "Right-clicking a folder opens a context menu." ); + ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); + ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); + indent.Push(); + ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); + ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); + ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); + indent.Pop( 1 ); + ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.TextUnformatted( "Mod Management" ); + ImGui.BulletText( "You can create empty mods or import TTMP-based mods with the buttons in this row." ); + ImGui.BulletText( + "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); + ImGui.BulletText( + "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); + } ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index 949b03b0..ca7becd5 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; -using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -204,7 +203,35 @@ public partial class ConfigWindow _newInheritance = null; } - ImGuiComponents.HelpMarker( tt ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), _window._iconButtonSize, "What is Inheritance?", + false, true ) ) + { + ImGui.OpenPopup( "InheritanceHelp" ); + } + + ImGuiUtil.HelpPopup( "InheritanceHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 21 * ImGui.GetTextLineHeightWithSpacing() ), () => + { + ImGui.NewLine(); + ImGui.TextWrapped( "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod." ); + ImGui.NewLine(); + ImGui.TextUnformatted( "Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'." ); + ImGui.BulletText( "If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections." ); + ImGui.BulletText( "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances." ); + ImGui.BulletText( "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used." ); + ImGui.BulletText( "If no such collection is found, the mod will be treated as disabled." ); + ImGui.BulletText( "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before." ); + ImGui.NewLine(); + ImGui.TextUnformatted( "Example" ); + ImGui.BulletText( "Collection A has the Bibo+ body and a Hempen Camise mod enabled." ); + ImGui.BulletText( "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A." ); + ImGui.BulletText( "Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured." ); + ImGui.BulletText( "Collection D inherits from C and then B and leaves everything unconfigured." ); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText( "B uses Bibo+ settings from A and its own Hempen Camise settings." ); + ImGui.BulletText( "C has Bibo+ disabled and uses A's Hempen Camise settings." ); + ImGui.BulletText( "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)." ); + } ); } // Draw the combo to select new potential inheritances. From 1874de38d08ffc35dfe98592291212b577d022b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 17:34:50 +0200 Subject: [PATCH 0186/2451] Fighting against debug info still... --- Penumbra/UI/ConfigWindow.DebugTab.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 2b153bd3..c0870782 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -128,6 +128,11 @@ public partial class ConfigWindow foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) { + if( data.ManipulatedPath.Crc64 == 0 ) + { + continue; + } + var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; ImGui.TableNextColumn(); From e5b739fc529926d157b3dd8d27b28aa949c0c5d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 May 2022 17:58:42 +0200 Subject: [PATCH 0187/2451] Add Join Discord button. --- Penumbra/UI/Classes/Colors.cs | 1 + Penumbra/UI/ConfigWindow.SettingsTab.cs | 29 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index fd55b3bb..b3ec1ea4 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,6 +24,7 @@ public static class Colors public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; public const uint RedTableBgTint = 0x40000080; + public const uint DiscordColor = 0xFFDA8972; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 0961f177..7f598c0f 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -47,6 +47,7 @@ public partial class ConfigWindow DrawAdvancedSettings(); _dialogManager.Draw(); + DrawDiscordButton(); } // Changing the base mod directory. @@ -199,5 +200,33 @@ public partial class ConfigWindow ImGui.NewLine(); } + + private static void DrawDiscordButton() + { + const string discord = "Join Discord for Support"; + const string address = @"https://discord.gg/kVva7DHV4r"; + var width = ImGui.CalcTextSize( discord ).X + ImGui.GetStyle().FramePadding.X * 2; + if( ImGui.GetScrollMaxY() > 0 ) + width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, 0 ) ); + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); + if( ImGui.Button( discord ) ) + { + try + { + var process = new ProcessStartInfo( address ) + { + UseShellExecute = true, + }; + Process.Start( process ); + } + catch + { + // ignored + } + } + + ImGuiUtil.HoverTooltip( $"Open {address}" ); + } } } \ No newline at end of file From 0c3c7ea3632ff6eb1691c3458470a94acfaac26b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 18 May 2022 17:40:41 +0200 Subject: [PATCH 0188/2451] Stop using windows forms, add extensive meta manipulation editing, fix a concurrency crash and a dumb crash. --- OtterGui | 2 +- Penumbra.GameData/Enums/BodySlot.cs | 65 +- Penumbra.GameData/Enums/EquipSlot.cs | 38 +- Penumbra.GameData/Enums/ObjectType.cs | 70 +- Penumbra.GameData/Structs/EqdpEntry.cs | 199 ++--- Penumbra.GameData/Structs/EqpEntry.cs | 16 +- .../Interop/Resolver/PathResolver.Data.cs | 2 +- Penumbra/Interop/Structs/CharacterUtility.cs | 2 +- Penumbra/Meta/Files/EstFile.cs | 1 - Penumbra/Meta/Files/ImcFile.cs | 10 +- .../Meta/Manipulations/EqdpManipulation.cs | 14 +- .../Meta/Manipulations/EqpManipulation.cs | 6 +- .../Meta/Manipulations/EstManipulation.cs | 10 +- .../Meta/Manipulations/GmpManipulation.cs | 4 +- .../Meta/Manipulations/ImcManipulation.cs | 41 +- .../Meta/Manipulations/RspManipulation.cs | 6 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 15 +- Penumbra/Mods/Editor/Mod.Editor.Meta.cs | 166 ++++ Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Penumbra.csproj | 1 - Penumbra/UI/Classes/Colors.cs | 28 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 773 ++++++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 83 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 13 +- Penumbra/Util/DictionaryExtensions.cs | 1 - 25 files changed, 1266 insertions(+), 302 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Meta.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.Meta.cs diff --git a/OtterGui b/OtterGui index cbc29200..732c9a3b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cbc29200e8b80d264c8a326cdc62e841e12d1c53 +Subproject commit 732c9a3bd7c967ca427e24f4b8df65f722fe72d2 diff --git a/Penumbra.GameData/Enums/BodySlot.cs b/Penumbra.GameData/Enums/BodySlot.cs index 0e1220ce..31e77417 100644 --- a/Penumbra.GameData/Enums/BodySlot.cs +++ b/Penumbra.GameData/Enums/BodySlot.cs @@ -1,43 +1,42 @@ using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum BodySlot : byte { - public enum BodySlot : byte - { - Unknown, - Hair, - Face, - Tail, - Body, - Zear, - } + Unknown, + Hair, + Face, + Tail, + Body, + Zear, +} - public static class BodySlotEnumExtension +public static class BodySlotEnumExtension +{ + public static string ToSuffix( this BodySlot value ) { - public static string ToSuffix( this BodySlot value ) + return value switch { - return value switch - { - BodySlot.Zear => "zear", - BodySlot.Face => "face", - BodySlot.Hair => "hair", - BodySlot.Body => "body", - BodySlot.Tail => "tail", - _ => throw new InvalidEnumArgumentException(), - }; - } - } - - public static partial class Names - { - public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() - { - { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, - { BodySlot.Face.ToSuffix(), BodySlot.Face }, - { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, - { BodySlot.Body.ToSuffix(), BodySlot.Body }, - { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, + BodySlot.Zear => "zear", + BodySlot.Face => "face", + BodySlot.Hair => "hair", + BodySlot.Body => "body", + BodySlot.Tail => "tail", + _ => throw new InvalidEnumArgumentException(), }; } +} + +public static partial class Names +{ + public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() + { + { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, + { BodySlot.Face.ToSuffix(), BodySlot.Face }, + { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, + { BodySlot.Body.ToSuffix(), BodySlot.Body }, + { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 061787e6..628acad7 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; namespace Penumbra.GameData.Enums { @@ -30,7 +32,7 @@ namespace Penumbra.GameData.Enums All = 22, // Not officially existing } - public static class EquipSlotEnumExtension + public static class EquipSlotExtensions { public static string ToSuffix( this EquipSlot value ) { @@ -79,6 +81,36 @@ namespace Penumbra.GameData.Enums }; } + public static string ToName( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => "Head", + EquipSlot.Hands => "Hands", + EquipSlot.Legs => "Legs", + EquipSlot.Feet => "Feet", + EquipSlot.Body => "Body", + EquipSlot.Ears => "Earrings", + EquipSlot.Neck => "Necklace", + EquipSlot.RFinger => "Right Ring", + EquipSlot.LFinger => "Left Ring", + EquipSlot.Wrists => "Bracelets", + EquipSlot.MainHand => "Primary Weapon", + EquipSlot.OffHand => "Secondary Weapon", + EquipSlot.Belt => "Belt", + EquipSlot.BothHand => "Primary Weapon", + EquipSlot.HeadBody => "Head and Body", + EquipSlot.BodyHandsLegsFeet => "Costume", + EquipSlot.SoulCrystal => "Soul Crystal", + EquipSlot.LegsFeet => "Bottom", + EquipSlot.FullBody => "Costume", + EquipSlot.BodyHands => "Top", + EquipSlot.BodyLegsFeet => "Costume", + EquipSlot.All => "Costume", + _ => "Unknown", + }; + } + public static bool IsEquipment( this EquipSlot value ) { return value switch @@ -104,6 +136,10 @@ namespace Penumbra.GameData.Enums _ => false, }; } + + public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray(); + public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray(); + public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray(); } public static partial class Names diff --git a/Penumbra.GameData/Enums/ObjectType.cs b/Penumbra.GameData/Enums/ObjectType.cs index 8b62e42b..414497b1 100644 --- a/Penumbra.GameData/Enums/ObjectType.cs +++ b/Penumbra.GameData/Enums/ObjectType.cs @@ -1,21 +1,55 @@ -namespace Penumbra.GameData.Enums +using System; + +namespace Penumbra.GameData.Enums; + +public enum ObjectType : byte { - public enum ObjectType : byte + Unknown, + Vfx, + DemiHuman, + Accessory, + World, + Housing, + Monster, + Icon, + LoadingScreen, + Map, + Interface, + Equipment, + Character, + Weapon, + Font, +} + +public static class ObjectTypeExtensions +{ + public static string ToName( this ObjectType type ) + => type switch + { + ObjectType.Vfx => "Visual Effect", + ObjectType.DemiHuman => "Demi Human", + ObjectType.Accessory => "Accessory", + ObjectType.World => "Doodad", + ObjectType.Housing => "Housing Object", + ObjectType.Monster => "Monster", + ObjectType.Icon => "Icon", + ObjectType.LoadingScreen => "Loading Screen", + ObjectType.Map => "Map", + ObjectType.Interface => "UI Element", + ObjectType.Equipment => "Equipment", + ObjectType.Character => "Character", + ObjectType.Weapon => "Weapon", + ObjectType.Font => "Font", + _ => "Unknown", + }; + + + public static readonly ObjectType[] ValidImcTypes = { - Unknown, - Vfx, - DemiHuman, - Accessory, - World, - Housing, - Monster, - Icon, - LoadingScreen, - Map, - Interface, - Equipment, - Character, - Weapon, - Font, - } + ObjectType.Equipment, + ObjectType.Accessory, + ObjectType.DemiHuman, + ObjectType.Monster, + ObjectType.Weapon, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqdpEntry.cs b/Penumbra.GameData/Structs/EqdpEntry.cs index 763809ae..00d05bc5 100644 --- a/Penumbra.GameData/Structs/EqdpEntry.cs +++ b/Penumbra.GameData/Structs/EqdpEntry.cs @@ -2,106 +2,119 @@ using System; using System.ComponentModel; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[Flags] +public enum EqdpEntry : ushort { - [Flags] - public enum EqdpEntry : ushort + Invalid = 0, + Head1 = 0b0000000001, + Head2 = 0b0000000010, + HeadMask = 0b0000000011, + + Body1 = 0b0000000100, + Body2 = 0b0000001000, + BodyMask = 0b0000001100, + + Hands1 = 0b0000010000, + Hands2 = 0b0000100000, + HandsMask = 0b0000110000, + + Legs1 = 0b0001000000, + Legs2 = 0b0010000000, + LegsMask = 0b0011000000, + + Feet1 = 0b0100000000, + Feet2 = 0b1000000000, + FeetMask = 0b1100000000, + + Ears1 = 0b0000000001, + Ears2 = 0b0000000010, + EarsMask = 0b0000000011, + + Neck1 = 0b0000000100, + Neck2 = 0b0000001000, + NeckMask = 0b0000001100, + + Wrists1 = 0b0000010000, + Wrists2 = 0b0000100000, + WristsMask = 0b0000110000, + + RingR1 = 0b0001000000, + RingR2 = 0b0010000000, + RingRMask = 0b0011000000, + + RingL1 = 0b0100000000, + RingL2 = 0b1000000000, + RingLMask = 0b1100000000, +} + +public static class Eqdp +{ + public static int Offset( EquipSlot slot ) + => slot switch + { + EquipSlot.Head => 0, + EquipSlot.Body => 2, + EquipSlot.Hands => 4, + EquipSlot.Legs => 6, + EquipSlot.Feet => 8, + EquipSlot.Ears => 0, + EquipSlot.Neck => 2, + EquipSlot.Wrists => 4, + EquipSlot.RFinger => 6, + EquipSlot.LFinger => 8, + _ => throw new InvalidEnumArgumentException(), + }; + + public static (bool, bool) ToBits( this EqdpEntry entry, EquipSlot slot ) + => slot switch + { + EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), + EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), + EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), + EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), + EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), + EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), + EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), + EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), + EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), + EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), + _ => throw new InvalidEnumArgumentException(), + }; + + public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 ) { - Invalid = 0, - Head1 = 0b0000000001, - Head2 = 0b0000000010, - HeadMask = 0b0000000011, + EqdpEntry ret = 0; + var offset = Offset( slot ); + if( bit1 ) + { + ret |= ( EqdpEntry )( 1 << offset ); + } - Body1 = 0b0000000100, - Body2 = 0b0000001000, - BodyMask = 0b0000001100, + if( bit2 ) + { + ret |= ( EqdpEntry )( 1 << ( offset + 1 ) ); + } - Hands1 = 0b0000010000, - Hands2 = 0b0000100000, - HandsMask = 0b0000110000, - - Legs1 = 0b0001000000, - Legs2 = 0b0010000000, - LegsMask = 0b0011000000, - - Feet1 = 0b0100000000, - Feet2 = 0b1000000000, - FeetMask = 0b1100000000, - - Ears1 = 0b0000000001, - Ears2 = 0b0000000010, - EarsMask = 0b0000000011, - - Neck1 = 0b0000000100, - Neck2 = 0b0000001000, - NeckMask = 0b0000001100, - - Wrists1 = 0b0000010000, - Wrists2 = 0b0000100000, - WristsMask = 0b0000110000, - - RingR1 = 0b0001000000, - RingR2 = 0b0010000000, - RingRMask = 0b0011000000, - - RingL1 = 0b0100000000, - RingL2 = 0b1000000000, - RingLMask = 0b1100000000, + return ret; } - public static class Eqdp + public static EqdpEntry Mask( EquipSlot slot ) { - public static int Offset( EquipSlot slot ) + return slot switch { - return slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 2, - EquipSlot.Hands => 4, - EquipSlot.Legs => 6, - EquipSlot.Feet => 8, - EquipSlot.Ears => 0, - EquipSlot.Neck => 2, - EquipSlot.Wrists => 4, - EquipSlot.RFinger => 6, - EquipSlot.LFinger => 8, - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 ) - { - EqdpEntry ret = 0; - var offset = Offset( slot ); - if( bit1 ) - { - ret |= ( EqdpEntry )( 1 << offset ); - } - - if( bit2 ) - { - ret |= ( EqdpEntry )( 1 << ( offset + 1 ) ); - } - - return ret; - } - - public static EqdpEntry Mask( EquipSlot slot ) - { - return slot switch - { - EquipSlot.Head => EqdpEntry.HeadMask, - EquipSlot.Body => EqdpEntry.BodyMask, - EquipSlot.Hands => EqdpEntry.HandsMask, - EquipSlot.Legs => EqdpEntry.LegsMask, - EquipSlot.Feet => EqdpEntry.FeetMask, - EquipSlot.Ears => EqdpEntry.EarsMask, - EquipSlot.Neck => EqdpEntry.NeckMask, - EquipSlot.Wrists => EqdpEntry.WristsMask, - EquipSlot.RFinger => EqdpEntry.RingRMask, - EquipSlot.LFinger => EqdpEntry.RingLMask, - _ => 0, - }; - } + EquipSlot.Head => EqdpEntry.HeadMask, + EquipSlot.Body => EqdpEntry.BodyMask, + EquipSlot.Hands => EqdpEntry.HandsMask, + EquipSlot.Legs => EqdpEntry.LegsMask, + EquipSlot.Feet => EqdpEntry.FeetMask, + EquipSlot.Ears => EqdpEntry.EarsMask, + EquipSlot.Neck => EqdpEntry.NeckMask, + EquipSlot.Wrists => EqdpEntry.WristsMask, + EquipSlot.RFinger => EqdpEntry.RingRMask, + EquipSlot.LFinger => EqdpEntry.RingLMask, + _ => 0, + }; } } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 18b3075a..56033d02 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -198,12 +198,14 @@ public static class Eqp EqpEntry._55 => EquipSlot.Head, EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, EqpEntry.HeadShowVieraHat => EquipSlot.Head, - EqpEntry._58 => EquipSlot.Head, - EqpEntry._59 => EquipSlot.Head, - EqpEntry._60 => EquipSlot.Head, - EqpEntry._61 => EquipSlot.Head, - EqpEntry._62 => EquipSlot.Head, - EqpEntry._63 => EquipSlot.Head, + + // Currently unused. + EqpEntry._58 => EquipSlot.Unknown, + EqpEntry._59 => EquipSlot.Unknown, + EqpEntry._60 => EquipSlot.Unknown, + EqpEntry._61 => EquipSlot.Unknown, + EqpEntry._62 => EquipSlot.Unknown, + EqpEntry._63 => EquipSlot.Unknown, _ => EquipSlot.Unknown, }; @@ -299,7 +301,7 @@ public static class Eqp public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet ); public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head ); - public static IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() + public static readonly IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() { [ EquipSlot.Body ] = EqpAttributesBody, [ EquipSlot.Legs ] = EqpAttributesLegs, diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 9701cd2a..df6e9933 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -21,7 +21,7 @@ public unsafe partial class PathResolver // and use the last game object that called EnableDraw to link them. public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); - [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] + [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour" )] public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; private ModCollection? _lastCreatedCollection; diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 08fd12e3..32770fe6 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -55,7 +55,7 @@ public unsafe struct CharacterUtility 1404 => EqdpStartIdx + 25, 9104 => EqdpStartIdx + 26, 9204 => EqdpStartIdx + 27, - _ => throw new ArgumentException(), + _ => -1, }; [FieldOffset( 0 )] diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index aacfbf00..cd67a6ba 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using System.Windows.Forms; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 8a092052..e0a5c7c9 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,23 +1,21 @@ using System; using System.Numerics; using Dalamud.Logging; -using Dalamud.Memory; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; -using Penumbra.Interop; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; public readonly struct ImcEntry : IEquatable< ImcEntry > { - public readonly byte MaterialId; - public readonly byte DecalId; + public byte MaterialId { get; init; } + public byte DecalId { get; init; } private readonly ushort _attributeAndSound; - public readonly byte VfxId; - public readonly byte MaterialAnimationId; + public byte VfxId { get; init; } + public byte MaterialAnimationId { get; init; } public ushort AttributeMask { diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 9abcd47b..287a5367 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -12,14 +12,18 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > { - public readonly EqdpEntry Entry; + public EqdpEntry Entry { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly Gender Gender; + public Gender Gender { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ModelRace Race; - public readonly ushort SetId; + public ModelRace Race { get; init; } + + public ushort SetId { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot Slot; + public EquipSlot Slot { get; init; } public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index b87d0d7a..c80696ac 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -14,12 +14,12 @@ namespace Penumbra.Meta.Manipulations; public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > { [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] - public readonly EqpEntry Entry; + public EqpEntry Entry { get; init; } - public readonly ushort SetId; + public ushort SetId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot Slot; + public EquipSlot Slot { get; init; } public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index b607e5e5..314972b3 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,18 +19,18 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.HeadEstIdx, } - public readonly ushort Entry; // SkeletonIdx. + public ushort Entry { get; init; } // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] - public readonly Gender Gender; + public Gender Gender { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ModelRace Race; + public ModelRace Race { get; init; } - public readonly ushort SetId; + public ushort SetId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EstType Slot; + public EstType Slot { get; init; } [JsonConstructor] public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index edae0b72..ad31d6b2 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -9,8 +9,8 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > { - public readonly GmpEntry Entry; - public readonly ushort SetId; + public GmpEntry Entry { get; init; } + public ushort SetId { get; init; } public GmpManipulation( GmpEntry entry, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 5ac63e99..b3acf680 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -11,19 +11,19 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { - public readonly ImcEntry Entry; - public readonly ushort PrimaryId; - public readonly ushort Variant; - public readonly ushort SecondaryId; + public ImcEntry Entry { get; init; } + public ushort PrimaryId { get; init; } + public ushort Variant { get; init; } + public ushort SecondaryId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ObjectType ObjectType; + public ObjectType ObjectType { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot EquipSlot; + public EquipSlot EquipSlot { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly BodySlot BodySlot; + public BodySlot BodySlot { get; init; } public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) { @@ -52,19 +52,24 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry ) { - Entry = entry; - ObjectType = objectType; - BodySlot = bodySlot; - PrimaryId = primaryId; - SecondaryId = secondaryId; - Variant = variant; - EquipSlot = equipSlot; + Entry = entry; + ObjectType = objectType; + PrimaryId = primaryId; + Variant = variant; + if( objectType is ObjectType.Accessory or ObjectType.Equipment ) + { + BodySlot = BodySlot.Unknown; + SecondaryId = 0; + EquipSlot = equipSlot; + } + else + { + BodySlot = bodySlot; + SecondaryId = secondaryId; + EquipSlot = EquipSlot.Unknown; + } } - public ImcManipulation( ImcManipulation copy, ImcEntry entry ) - : this( copy.ObjectType, copy.BodySlot, copy.PrimaryId, copy.SecondaryId, copy.Variant, copy.EquipSlot, entry ) - {} - public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index d25b695f..1f10cf7b 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -11,13 +11,13 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct RspManipulation : IMetaManipulation< RspManipulation > { - public readonly float Entry; + public float Entry { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly SubRace SubRace; + public SubRace SubRace { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly RspAttribute Attribute; + public RspAttribute Attribute { get; init; } public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) { diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index e0378d26..35d41003 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; using Penumbra.Util; namespace Penumbra.Mods; @@ -11,7 +10,7 @@ public partial class Mod public partial class Editor { public int GroupIdx { get; private set; } = -1; - public int OptionIdx { get; private set; } = 0; + public int OptionIdx { get; private set; } private IModGroup? _modGroup; private SubMod _subMod; @@ -21,7 +20,6 @@ public partial class Mod public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); - public readonly HashSet< MetaManipulation > CurrentManipulations = new(); public void SetSubMod( int groupIdx, int optionIdx ) { @@ -62,16 +60,5 @@ public partial class Mod { CurrentSwaps.SetTo( _subMod.FileSwaps ); } - - public void ApplyManipulations() - { - Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() ); - } - - public void RevertManipulations() - { - CurrentManipulations.Clear(); - CurrentManipulations.UnionWith( _subMod.Manipulations ); - } } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs new file mode 100644 index 00000000..19a48f95 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + public struct Manipulations + { + private readonly HashSet< ImcManipulation > _imc = new(); + private readonly HashSet< EqpManipulation > _eqp = new(); + private readonly HashSet< EqdpManipulation > _eqdp = new(); + private readonly HashSet< GmpManipulation > _gmp = new(); + private readonly HashSet< EstManipulation > _est = new(); + private readonly HashSet< RspManipulation > _rsp = new(); + + public bool Changes { get; private set; } = false; + + public IReadOnlySet< ImcManipulation > Imc + => _imc; + + public IReadOnlySet< EqpManipulation > Eqp + => _eqp; + + public IReadOnlySet< EqdpManipulation > Eqdp + => _eqdp; + + public IReadOnlySet< GmpManipulation > Gmp + => _gmp; + + public IReadOnlySet< EstManipulation > Est + => _est; + + public IReadOnlySet< RspManipulation > Rsp + => _rsp; + + public Manipulations() + { } + + public bool CanAdd( MetaManipulation m ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Imc => !_imc.Contains( m.Imc ), + MetaManipulation.Type.Eqdp => !_eqdp.Contains( m.Eqdp ), + MetaManipulation.Type.Eqp => !_eqp.Contains( m.Eqp ), + MetaManipulation.Type.Est => !_est.Contains( m.Est ), + MetaManipulation.Type.Gmp => !_gmp.Contains( m.Gmp ), + MetaManipulation.Type.Rsp => !_rsp.Contains( m.Rsp ), + _ => false, + }; + } + + public bool Add( MetaManipulation m ) + { + var added = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Add( m.Imc ), + MetaManipulation.Type.Eqdp => _eqdp.Add( m.Eqdp ), + MetaManipulation.Type.Eqp => _eqp.Add( m.Eqp ), + MetaManipulation.Type.Est => _est.Add( m.Est ), + MetaManipulation.Type.Gmp => _gmp.Add( m.Gmp ), + MetaManipulation.Type.Rsp => _rsp.Add( m.Rsp ), + _ => false, + }; + Changes |= added; + return added; + } + + public bool Delete( MetaManipulation m ) + { + var deleted = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Remove( m.Imc ), + MetaManipulation.Type.Eqdp => _eqdp.Remove( m.Eqdp ), + MetaManipulation.Type.Eqp => _eqp.Remove( m.Eqp ), + MetaManipulation.Type.Est => _est.Remove( m.Est ), + MetaManipulation.Type.Gmp => _gmp.Remove( m.Gmp ), + MetaManipulation.Type.Rsp => _rsp.Remove( m.Rsp ), + _ => false, + }; + Changes |= deleted; + return deleted; + } + + public bool Change( MetaManipulation m ) + => Delete( m ) && Add( m ); + + public bool Set( MetaManipulation m ) + => Delete( m ) | Add( m ); + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _gmp.Clear(); + _est.Clear(); + _rsp.Clear(); + Changes = true; + } + + public void Split( IEnumerable< MetaManipulation > manips ) + { + Clear(); + foreach( var manip in manips ) + { + switch( manip.ManipulationType ) + { + case MetaManipulation.Type.Imc: + _imc.Add( manip.Imc ); + break; + case MetaManipulation.Type.Eqdp: + _eqdp.Add( manip.Eqdp ); + break; + case MetaManipulation.Type.Eqp: + _eqp.Add( manip.Eqp ); + break; + case MetaManipulation.Type.Est: + _est.Add( manip.Est ); + break; + case MetaManipulation.Type.Gmp: + _gmp.Add( manip.Gmp ); + break; + case MetaManipulation.Type.Rsp: + _rsp.Add( manip.Rsp ); + break; + } + } + + Changes = false; + } + + private HashSet< MetaManipulation > Recombine() + => _imc.Select( m => ( MetaManipulation )m ) + .Concat( _eqdp.Select( m => ( MetaManipulation )m ) ) + .Concat( _eqp.Select( m => ( MetaManipulation )m ) ) + .Concat( _est.Select( m => ( MetaManipulation )m ) ) + .Concat( _gmp.Select( m => ( MetaManipulation )m ) ) + .Concat( _rsp.Select( m => ( MetaManipulation )m ) ) + .ToHashSet(); + + public void Apply( Mod mod, int groupIdx, int optionIdx ) + { + if( Changes ) + { + Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine() ); + Changes = false; + } + } + } + + public Manipulations Meta = new(); + + public void RevertManipulations() + => Meta.Split( _subMod.Manipulations ); + + public void ApplyManipulations() + { + Meta.Apply( _mod, GroupIdx, OptionIdx ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index ea8c838e..71cd0b02 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -278,7 +278,7 @@ public sealed partial class Mod public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.Manipulations.SetEquals( manipulations ) ) + if( subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) { return; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2a14edb7..0626797f 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -12,7 +12,6 @@ bin\$(Configuration)\ true enable - true true diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index b3ec1ea4..5f3844ec 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -16,6 +16,8 @@ public enum ColorId FolderCollapsed, FolderLine, ItemId, + IncreasedMetaValue, + DecreasedMetaValue, } public static class Colors @@ -30,18 +32,20 @@ public static class Colors => color switch { // @formatter:off - ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), - ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), - ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), - ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), - ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), - ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), - ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), - ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), - ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), - ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), - ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), - ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), + ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs new file mode 100644 index 00000000..60ce5b0f --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem( "Meta Manipulations" ); + if( !tab ) + { + return; + } + + DrawOptionSelectHeader(); + + var setsEqual = !_editor!.Meta.Changes; + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplyManipulations(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertManipulations(); + } + + using var child = ImRaii.Child( "##meta", -Vector2.One, true ); + if( !child ) + { + return; + } + + DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 4, EqpRow.Draw, EqpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 6, EqdpRow.Draw, EqdpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 8, ImcRow.Draw, ImcRow.DrawNew ); + DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 6, EstRow.Draw, EstRow.DrawNew ); + DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 6, GmpRow.Draw, GmpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 4, RspRow.Draw, RspRow.DrawNew ); + } + + + // The headers for the different meta changes all have basically the same structure for different types. + private void DrawEditHeader< T >( IReadOnlyCollection< T > items, string label, int numColumns, Action< T, Mod.Editor, Vector2 > draw, + Action< Mod.Editor, Vector2 > drawNew ) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + if( !ImGui.CollapsingHeader( $"{items.Count} {label}" ) ) + { + return; + } + + using( var table = ImRaii.Table( label, numColumns, flags ) ) + { + if( table ) + { + drawNew( _editor!, _iconSize ); + foreach( var (item, index) in items.ToArray().WithIndex() ) + { + using var id = ImRaii.PushId( index ); + draw( item, _editor!, _iconSize ); + } + } + } + + ImGui.NewLine(); + } + + private static class EqpRow + { + private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) + { + using var id = ImRaii.PushId( ( int )flag ); + var value = defaultEntry.HasFlag( flag ); + Checkmark( string.Empty, flag.ToLocalName(), value, value, out _ ); + ImGui.SameLine(); + } + } + + public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToName() ); + + // Values + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) + { + using var id = ImRaii.PushId( ( int )flag ); + var defaultValue = defaultEntry.HasFlag( flag ); + var currentValue = meta.Entry.HasFlag( flag ); + if( Checkmark( string.Empty, flag.ToLocalName(), currentValue, defaultValue, out var value ) ) + { + editor.Meta.Change( meta with { Entry = value ? meta.Entry | flag : meta.Entry & ~flag } ); + } + + ImGui.SameLine(); + } + } + } + + + private static class EqdpRow + { + private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var raceCode = Names.CombinedRace( _new.Gender, _new.Race ); + var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0; + var canAdd = validRaceCode && editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : + validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; + var defaultEntry = validRaceCode + ? ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ) + : 0; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( RaceCombo( "##eqdpRace", _new.Race, out var race ) ) + { + _new = _new with { Race = race }; + } + + ImGui.TableNextColumn(); + if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) ) + { + _new = _new with { Gender = gender }; + } + + ImGui.TableNextColumn(); + if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); + Checkmark( "##eqdpCheck1", string.Empty, bit1, bit1, out _ ); + ImGui.SameLine(); + Checkmark( "##eqdpCheck2", string.Empty, bit2, bit2, out _ ); + } + + public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Race.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToName() ); + + // Values + var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId ); + var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot ); + var (bit1, bit2) = meta.Entry.ToBits( meta.Slot ); + ImGui.TableNextColumn(); + if( Checkmark( "##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) + { + editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) } ); + } + + ImGui.SameLine(); + if( Checkmark( "##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) + { + editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) } ); + } + } + } + + private static class ImcRow + { + private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + + private static float IdWidth + => 80 * ImGuiHelpers.GlobalScale; + + private static float SmallIdWidth + => 45 * ImGuiHelpers.GlobalScale; + + // Convert throwing to null-return if the file does not exist. + private static ImcEntry? GetDefault( ImcManipulation imc ) + { + try + { + return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ); + } + catch + { + return null; + } + } + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var defaultEntry = GetDefault( _new ); + var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; + defaultEntry ??= new ImcEntry(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry.Value } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) ) + { + _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, + _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); + } + + ImGui.TableNextColumn(); + if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, ushort.MaxValue ) ) + { + _new = _new with { PrimaryId = setId }; + } + + ImGui.TableNextColumn(); + // Equipment and accessories are slightly different imcs than other types. + if( _new.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + { + if( EqdpEquipSlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) + { + _new = _new with { EquipSlot = slot }; + } + } + else + { + if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, ushort.MaxValue ) ) + { + _new = _new with { SecondaryId = setId2 }; + } + } + + ImGui.TableNextColumn(); + if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, byte.MaxValue ) ) + { + _new = _new with { Variant = variant }; + } + + // Values + ImGui.TableNextColumn(); + IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, + 1, byte.MaxValue, 0f ); + ImGui.SameLine(); + IntDragInput( "##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, + defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f ); + ImGui.TableNextColumn(); + IntDragInput( "##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, + byte.MaxValue, 0f ); + ImGui.SameLine(); + IntDragInput( "##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, + 0f ); + ImGui.SameLine(); + IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, + 0f ); + ImGui.TableNextColumn(); + IntDragInput( "##imcAttributes", "Attributes", IdWidth, defaultEntry.Value.AttributeMask, defaultEntry.Value.AttributeMask, out _, + 0, 0b1111111111, 0f ); + } + + public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.ObjectType.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.PrimaryId.ToString() ); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + { + ImGui.TextUnformatted( meta.EquipSlot.ToName() ); + } + else + { + ImGui.TextUnformatted( meta.SecondaryId.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Variant.ToString() ); + + // Values + ImGui.TableNextColumn(); + var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); + if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, + defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialId = ( byte )materialId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, + meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, + defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { DecalId = ( byte )decalId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, + out var vfxId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { VfxId = ( byte )vfxId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, + defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { SoundId = ( byte )soundId } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##imcAttributes", $"Attributes\nDefault Value: {defaultEntry.AttributeMask}", IdWidth, + meta.Entry.AttributeMask, defaultEntry.AttributeMask, out var attributeMask, 0, 0b1111111111, 0.1f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { AttributeMask = ( ushort )attributeMask } } ); + } + } + } + + private static class EstRow + { + private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( RaceCombo( "##estRace", _new.Race, out var race ) ) + { + _new = _new with { Race = race }; + } + + ImGui.TableNextColumn(); + if( GenderCombo( "##estGender", _new.Gender, out var gender ) ) + { + _new = _new with { Gender = gender }; + } + + ImGui.TableNextColumn(); + if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f ); + } + + public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Race.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToString() ); + + // Values + var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); + ImGui.TableNextColumn(); + if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, + out var entry, 0, ushort.MaxValue, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = ( ushort )entry } ); + } + } + } + + private static class GmpRow + { + private static GmpManipulation _new = new(GmpEntry.Default, 1); + + private static float RotationWidth + => 75 * ImGuiHelpers.GlobalScale; + + private static float UnkWidth + => 50 * ImGuiHelpers.GlobalScale; + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + // Values + ImGui.TableNextColumn(); + Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ ); + ImGui.TableNextColumn(); + Checkmark( "##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _ ); + ImGui.TableNextColumn(); + IntDragInput( "##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, + 360, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, + 360, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, + 360, 0f ); + ImGui.TableNextColumn(); + IntDragInput( "##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f ); + } + + public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + + // Values + var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId ); + ImGui.TableNextColumn(); + if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { Enabled = enabled } } ); + } + + ImGui.TableNextColumn(); + if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { Animated = animated } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, + meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationA = ( ushort )rotationA } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, + meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationB = ( ushort )rotationB } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, + meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationC = ( ushort )rotationC } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, + defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkA } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, + defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkB } } ); + } + } + } + + private static class RspRow + { + private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); + + private static float FloatWidth + => 150 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( SubRaceCombo( "##rspSubRace", _new.SubRace, out var subRace ) ) + { + _new = _new with { SubRace = subRace }; + } + + ImGui.TableNextColumn(); + if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) ) + { + _new = _new with { Attribute = attribute }; + } + + // Values + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( FloatWidth ); + ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f ); + } + + public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SubRace.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Attribute.ToFullString() ); + ImGui.TableNextColumn(); + + // Values + var def = CmpFile.GetDefault( meta.SubRace, meta.Attribute ); + var value = meta.Entry; + ImGui.SetNextItemWidth( FloatWidth ); + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), + def != value ); + if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) + { + editor.Meta.Change( meta with { Entry = value } ); + } + + ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" ); + } + } + + // Different combos to use with enums. + private static bool RaceCombo( string label, ModelRace current, out ModelRace race ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); + + private static bool GenderCombo( string label, Gender current, out Gender gender ) + => ImGuiUtil.GenericEnumCombo( label, 120 * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); + + private static bool EqdpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, + EquipSlotExtensions.ToName ); + + private static bool EqpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, + EquipSlotExtensions.ToName ); + + private static bool SubRaceCombo( string label, SubRace current, out SubRace subRace ) + => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + + private static bool RspAttributeCombo( string label, RspAttribute current, out RspAttribute attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, + RspAttributeExtensions.ToFullString, 0, 1 ); + + private static bool EstSlotCombo( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); + + private static bool ImcTypeCombo( string label, ObjectType current, out ObjectType type ) + => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, + ObjectTypeExtensions.ToName ); + + // A number input for ids with a optional max id of given width. + // Returns true if newId changed against currentId. + private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int maxId ) + { + int tmp = currentId; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputInt( label, ref tmp, 0 ) ) + { + tmp = Math.Clamp( tmp, 1, maxId ); + } + + newId = ( ushort )tmp; + return newId != currentId; + } + + // A checkmark that compares against a default value and shows a tooltip. + // Returns true if newValue is changed against currentValue. + private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue ) + { + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue ); + newValue = currentValue; + ImGui.Checkbox( label, ref newValue ); + ImGuiUtil.HoverTooltip( tooltip ); + return newValue != currentValue; + } + + // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + // Returns true if newValue changed against currentValue. + private static bool IntDragInput( string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, + int minValue, int maxValue, float speed ) + { + newValue = currentValue; + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue ); + ImGui.SetNextItemWidth( width ); + if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) ) + { + newValue = Math.Clamp( newValue, minValue, maxValue ); + } + + ImGuiUtil.HoverTooltip( tooltip ); + + return newValue != currentValue; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 630c07bf..4d919444 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -10,17 +9,18 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; +using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.UI.Classes; -public class ModEditWindow : Window, IDisposable +public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; private Mod.Editor? _editor; private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; public void ChangeMod( Mod mod ) { @@ -54,6 +54,7 @@ public class ModEditWindow : Window, IDisposable return; } + _iconSize = new Vector2( ImGui.GetFrameHeight() ); DrawFileTab(); DrawMetaTab(); DrawSwapTab(); @@ -117,8 +118,9 @@ public class ModEditWindow : Window, IDisposable : disabled ? "The suffix is invalid." : _materialSuffixFrom.Length == 0 - ? _raceCode == GenderRace.Unknown ? "Convert all skin material suffices to the target." - : "Convert all skin material suffices for the given race code to the target." + ? _raceCode == GenderRace.Unknown + ? "Convert all skin material suffices to the target." + : "Convert all skin material suffices for the given race code to the target." : _raceCode == GenderRace.Unknown ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; @@ -374,6 +376,7 @@ public class ModEditWindow : Window, IDisposable isDefaultOption ) ) { _editor.SetSubMod( -1, 0 ); + isDefaultOption = true; } ImGui.SameLine(); @@ -395,8 +398,7 @@ public class ModEditWindow : Window, IDisposable return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; } - var groupLabel = GetLabel(); - using var combo = ImRaii.Combo( "##optionSelector", groupLabel, ImGuiComboFlags.NoArrowButton ); + using var combo = ImRaii.Combo( "##optionSelector", GetLabel(), ImGuiComboFlags.NoArrowButton ); if( !combo ) { return; @@ -497,74 +499,9 @@ public class ModEditWindow : Window, IDisposable } } - private void DrawMetaTab() - { - using var tab = ImRaii.TabItem( "Meta Manipulations" ); - if( !tab ) - { - return; - } - - DrawOptionSelectHeader(); - - var setsEqual = _editor!.CurrentManipulations.SetEquals( _editor.CurrentOption.Manipulations ); - var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; - ImGui.NewLine(); - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.ApplyManipulations(); - } - - ImGui.SameLine(); - tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.RevertManipulations(); - } - - using var child = ImRaii.Child( "##meta", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var list = ImRaii.Table( "##table", 3 ); - if( !list ) - { - return; - } - - foreach( var manip in _editor!.CurrentManipulations ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.ToString(), - MetaManipulation.Type.Est => manip.Est.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.ToString(), - _ => string.Empty, - } ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), - MetaManipulation.Type.Est => manip.Est.Entry.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), - _ => string.Empty, - } ); - } - } - private string _newSwapKey = string.Empty; private string _newSwapValue = string.Empty; + private void DrawSwapTab() { using var tab = ImRaii.TabItem( "File Swaps" ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 7c4c5280..ab39f539 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; @@ -96,6 +97,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); } } + + while( _modsToAdd.TryDequeue( out var dir ) ) + { + Penumbra.ModManager.AddMod( dir ); + } } protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) @@ -212,9 +218,12 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + // Mods need to be added thread-safely outside of iteration. + private readonly ConcurrentQueue< DirectoryInfo > _modsToAdd = new(); + // Clean up invalid directory if necessary. // Add successfully extracted mods. - private static void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) + private void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) { if( error != null ) { @@ -237,7 +246,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } else if( dir != null ) { - Penumbra.ModManager.AddMod( dir ); + _modsToAdd.Enqueue( dir ); } } diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 2aeb44e5..ad832457 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Forms; namespace Penumbra.Util; From 9bceed3d571075336345514938c877578634456b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 18 May 2022 23:03:32 +0200 Subject: [PATCH 0189/2451] Fix some bugs, add clipboard options to meta changes. --- OtterGui | 2 +- Penumbra/Collections/CollectionManager.cs | 3 +- Penumbra/Mods/Editor/Mod.Editor.Meta.cs | 7 +- Penumbra/Mods/Mod.BasePath.cs | 1 + Penumbra/Mods/Mod.Creation.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 129 +++++++++++++++------- Penumbra/UI/Classes/ModEditWindow.cs | 7 ++ 7 files changed, 106 insertions(+), 45 deletions(-) diff --git a/OtterGui b/OtterGui index 732c9a3b..d6fcf1f5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 732c9a3bd7c967ca427e24f4b8df65f722fe72d2 +Subproject commit d6fcf1f53888d5eec8196eaba2dbb3853534d3bf diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 775a0b2c..ce8fa113 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -224,10 +224,9 @@ public partial class ModCollection OnModAddedActive( mod.TotalManipulations > 0 ); break; case ModPathChangeType.Deleted: - var settings = new List< ModSettings? >( _collections.Count ); + var settings = this.Select( c => c[mod.Index].Settings ).ToList(); foreach( var collection in this ) { - settings.Add( collection._settings[ mod.Index ] ); collection.RemoveMod( mod, mod.Index ); } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs index 19a48f95..c0908a9c 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs @@ -134,20 +134,19 @@ public partial class Mod Changes = false; } - private HashSet< MetaManipulation > Recombine() + public IEnumerable< MetaManipulation > Recombine() => _imc.Select( m => ( MetaManipulation )m ) .Concat( _eqdp.Select( m => ( MetaManipulation )m ) ) .Concat( _eqp.Select( m => ( MetaManipulation )m ) ) .Concat( _est.Select( m => ( MetaManipulation )m ) ) .Concat( _gmp.Select( m => ( MetaManipulation )m ) ) - .Concat( _rsp.Select( m => ( MetaManipulation )m ) ) - .ToHashSet(); + .Concat( _rsp.Select( m => ( MetaManipulation )m ) ); public void Apply( Mod mod, int groupIdx, int optionIdx ) { if( Changes ) { - Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine() ); + Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine().ToHashSet() ); Changes = false; } } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 1e37b143..16efdf12 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -33,6 +33,7 @@ public partial class Mod { // Can not be base path not existing because that is checked before. PluginLog.Error( $"Mod at {modPath} without name is not supported." ); + return null; } return mod; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 85a5d264..de4f9f88 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -54,7 +54,7 @@ public partial class Mod mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - mod.SaveMeta(); + mod.SaveMetaFile(); // Not delayed. } // Create a file for an option group from given data. diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 60ce5b0f..9b20e847 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -43,18 +43,25 @@ public partial class ModEditWindow _editor.RevertManipulations(); } + ImGui.SameLine(); + AddFromClipboardButton(); + ImGui.SameLine(); + SetFromClipboardButton(); + ImGui.SameLine(); + CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() ); + using var child = ImRaii.Child( "##meta", -Vector2.One, true ); if( !child ) { return; } - DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 4, EqpRow.Draw, EqpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 6, EqdpRow.Draw, EqdpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 8, ImcRow.Draw, ImcRow.DrawNew ); - DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 6, EstRow.Draw, EstRow.DrawNew ); - DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 6, GmpRow.Draw, GmpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 4, RspRow.Draw, RspRow.DrawNew ); + DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 9, ImcRow.Draw, ImcRow.DrawNew ); + DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew ); + DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew ); } @@ -67,12 +74,12 @@ public partial class ModEditWindow { return; } - using( var table = ImRaii.Table( label, numColumns, flags ) ) { if( table ) { drawNew( _editor!, _iconSize ); + ImGui.Separator(); foreach( var (item, index) in items.ToArray().WithIndex() ) { using var id = ImRaii.PushId( index ); @@ -93,6 +100,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize, editor.Meta.Eqp.Select( m => (MetaManipulation) m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -125,15 +134,12 @@ public partial class ModEditWindow Checkmark( string.Empty, flag.ToLocalName(), value, value, out _ ); ImGui.SameLine(); } + ImGui.NewLine(); } public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -172,6 +178,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize, editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var raceCode = Names.CombinedRace( _new.Gender, _new.Race ); var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0; @@ -221,11 +229,7 @@ public partial class ModEditWindow public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -284,6 +288,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize, editor.Meta.Imc.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( _new ); var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); @@ -354,11 +360,7 @@ public partial class ModEditWindow public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -438,6 +440,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize, editor.Meta.Est.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -479,11 +483,7 @@ public partial class ModEditWindow public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -525,6 +525,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize, editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -563,11 +565,7 @@ public partial class ModEditWindow public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -634,6 +632,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize, editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -664,11 +664,7 @@ public partial class ModEditWindow public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize ) { - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) - { - editor.Meta.Delete( meta ); - } + DrawMetaButtons( meta, editor, iconSize ); // Identifier ImGui.TableNextColumn(); @@ -770,4 +766,63 @@ public partial class ModEditWindow return newValue != currentValue; } + + private const byte CurrentManipulationVersion = 0; + + private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations ) + { + if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) ) + { + return; + } + + var text = Functions.ToCompressedBase64( manipulations, CurrentManipulationVersion ); + if( text.Length > 0 ) + { + ImGui.SetClipboardText( text ); + } + } + + private void AddFromClipboardButton( ) + { + if( ImGui.Button( "Add from Clipboard" ) ) + { + var clipboard = ImGui.GetClipboardText(); + var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); + if( version == CurrentManipulationVersion && manips != null) + { + foreach( var manip in manips ) + _editor!.Meta.Set( manip ); + } + } + ImGuiUtil.HoverTooltip( "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." ); + } + + private void SetFromClipboardButton() + { + if( ImGui.Button( "Set from Clipboard" ) ) + { + var clipboard = ImGui.GetClipboardText(); + var version = Functions.FromCompressedBase64( clipboard, out var manips ); + if( version == CurrentManipulationVersion && manips != null ) + { + _editor!.Meta.Clear(); + foreach( var manip in manips ) + _editor!.Meta.Set( manip ); + } + } + ImGuiUtil.HoverTooltip( "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." ); + } + + private static void DrawMetaButtons( MetaManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty().Append( meta ) ); + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 4d919444..5f9d6be7 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -430,6 +430,12 @@ public partial class ModEditWindow : Window, IDisposable return; } + if( ImGui.Button( "Refresh" ) ) + { + _editor!.Dispose(); + _editor = new Mod.Editor( _mod! ); + } + if( _editor!.UnusedFiles.Count == 0 ) { ImGui.NewLine(); @@ -437,6 +443,7 @@ public partial class ModEditWindow : Window, IDisposable } else { + ImGui.SameLine(); if( ImGui.Button( "Add Unused Files to Default" ) ) { _editor.AddUnusedPathsToDefault(); From 12837bbdca9c4a91f83f49569e4576fc38ce9b79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 20 May 2022 13:58:14 +0200 Subject: [PATCH 0190/2451] Order collections by name, order character collections by character. --- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- Penumbra/UI/ConfigWindow.Misc.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index a22c3151..563017f7 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -147,7 +147,7 @@ public partial class ConfigWindow DrawDefaultCollectionSelector(); - foreach( var name in Penumbra.CollectionManager.Characters.Keys.ToArray() ) + 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 ); diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index cae9a870..d866a8d6 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -95,7 +95,7 @@ public partial class ConfigWindow return; } - foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ) ) + foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) { using var id = ImRaii.PushId( collection.Index ); if( ImGui.Selectable( collection.Name, collection == current ) ) From d15ebddf18fcaa35bd8d6ef2c80ada21e0ccd221 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 20 May 2022 15:59:25 +0200 Subject: [PATCH 0191/2451] CharacterUtility bug test --- Penumbra/Interop/CharacterUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index ff6ca138..6123bb22 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -16,7 +16,7 @@ public unsafe class CharacterUtility : IDisposable // The initial function in which all the character resources get loaded. public delegate void LoadDataFilesDelegate( Structs.CharacterUtility* characterUtility ); - [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] + [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", DetourName = "LoadDataFilesDetour")] public Hook< LoadDataFilesDelegate > LoadDataFilesHook = null!; public Structs.CharacterUtility* Address @@ -57,7 +57,7 @@ public unsafe class CharacterUtility : IDisposable { SignatureHelper.Initialise( this ); - if( Address->EqpResource != null ) + if( Address->EqpResource != null && Address->EqpResource->Data != null ) { LoadDefaultResources(); } From 46134611540e2d6d93eda56cf2014471e8c4f849 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 21 May 2022 20:49:11 +0200 Subject: [PATCH 0192/2451] Add Initialized / Disposed IPC, start the rest of the plugin only after obtaining the default meta files, --- Penumbra/Api/PenumbraIpc.cs | 309 +++++++++++++++------------ Penumbra/Interop/CharacterUtility.cs | 85 +++++--- Penumbra/Penumbra.cs | 40 +++- 3 files changed, 252 insertions(+), 182 deletions(-) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 2f77cc61..8d2575ce 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -6,163 +6,188 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Penumbra.GameData.Enums; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class PenumbraIpc : IDisposable { - public class PenumbraIpc : IDisposable + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + + public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + + internal readonly IPenumbraApi Api; + + private static RedrawType CheckRedrawType( int value ) { - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - - public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; - - internal readonly IPenumbraApi Api; - - private static RedrawType CheckRedrawType( int value ) + var type = ( RedrawType )value; + if( Enum.IsDefined( type ) ) { - var type = ( RedrawType )value; - if( Enum.IsDefined( type ) ) - { - return type; - } - - throw new Exception( "The integer provided for a Redraw Function was not a valid RedrawType." ); + return type; } - private void OnClick( MouseButton click, object? item ) + throw new Exception( "The integer provided for a Redraw Function was not a valid RedrawType." ); + } + + private void OnClick( MouseButton click, object? item ) + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); + ProviderChangedItemClick?.SendMessage( click, type, id ); + } + + private void OnTooltip( object? item ) + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); + ProviderChangedItemTooltip?.SendMessage( type, id ); + } + + + public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) + { + Api = api; + + try { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ProviderChangedItemClick?.SendMessage( click, type, id ); + ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); } - private void OnTooltip( object? item ) + try { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ProviderChangedItemTooltip?.SendMessage( type, id ); + ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); } - - public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) + try { - Api = api; - - try - { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); - ProviderApiVersion.RegisterFunc( () => api.ApiVersion ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); - } - - try - { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); - ProviderRedrawName.RegisterAction( ( s, i ) => api.RedrawObject( s, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); - } - - try - { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); - ProviderRedrawObject.RegisterAction( ( o, i ) => api.RedrawObject( o, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); - } - - try - { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); - ProviderRedrawAll.RegisterAction( i => api.RedrawAll( CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); - } - - try - { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); - ProviderResolveDefault.RegisterFunc( api.ResolvePath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); - } - - try - { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); - ProviderResolveCharacter.RegisterFunc( api.ResolvePath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); - } - - try - { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); - api.ChangedItemTooltip += OnTooltip; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" ); - } - - try - { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); - api.ChangedItemClicked += OnClick; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); - } - - try - { - ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); - ProviderGetChangedItems.RegisterFunc( api.GetChangedItemsForCollection ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); - } + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion.RegisterFunc( () => api.ApiVersion ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); } - public void Dispose() + try { - ProviderApiVersion?.UnregisterFunc(); - ProviderRedrawName?.UnregisterAction(); - ProviderRedrawObject?.UnregisterAction(); - ProviderRedrawAll?.UnregisterAction(); - ProviderResolveDefault?.UnregisterFunc(); - ProviderResolveCharacter?.UnregisterFunc(); - ProviderGetChangedItems?.UnregisterFunc(); - Api.ChangedItemClicked -= OnClick; - Api.ChangedItemTooltip -= OnTooltip; + ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName.RegisterAction( ( s, i ) => api.RedrawObject( s, CheckRedrawType( i ) ) ); } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); + } + + try + { + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); + ProviderRedrawObject.RegisterAction( ( o, i ) => api.RedrawObject( o, CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); + } + + try + { + ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll.RegisterAction( i => api.RedrawAll( CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); + } + + try + { + ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); + ProviderResolveDefault.RegisterFunc( api.ResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); + } + + try + { + ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); + ProviderResolveCharacter.RegisterFunc( api.ResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); + } + + try + { + ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); + api.ChangedItemTooltip += OnTooltip; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" ); + } + + try + { + ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); + api.ChangedItemClicked += OnClick; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + + try + { + ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); + ProviderGetChangedItems.RegisterFunc( api.GetChangedItemsForCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + + ProviderInitialized?.SendMessage(); + } + + public void Dispose() + { + ProviderDisposed?.SendMessage(); + ProviderInitialized?.UnregisterFunc(); + ProviderApiVersion?.UnregisterFunc(); + ProviderRedrawName?.UnregisterAction(); + ProviderRedrawObject?.UnregisterAction(); + ProviderRedrawAll?.UnregisterAction(); + ProviderResolveDefault?.UnregisterFunc(); + ProviderResolveCharacter?.UnregisterFunc(); + ProviderGetChangedItems?.UnregisterFunc(); + Api.ChangedItemClicked -= OnClick; + Api.ChangedItemTooltip -= OnTooltip; } } \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 6123bb22..c57d6043 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,9 +1,7 @@ using System; using System.Linq; -using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; -using ImGuiScene; namespace Penumbra.Interop; @@ -13,15 +11,12 @@ public unsafe class CharacterUtility : IDisposable [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; - // The initial function in which all the character resources get loaded. - public delegate void LoadDataFilesDelegate( Structs.CharacterUtility* characterUtility ); - - [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", DetourName = "LoadDataFilesDetour")] - public Hook< LoadDataFilesDelegate > LoadDataFilesHook = null!; - public Structs.CharacterUtility* Address => *_characterUtilityAddress; + public bool Ready { get; private set; } + public event Action LoadingFinished; + // The relevant indices depend on which meta manipulations we allow for. // The defines are set in the project configuration. public static readonly int[] RelevantIndices @@ -33,7 +28,8 @@ public unsafe class CharacterUtility : IDisposable .Append( Structs.CharacterUtility.GmpIdx ) #endif #if USE_EQDP - .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ).Where( i => i != 17 ) ) // TODO: Female Hrothgar + .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) + .Where( i => i != 17 ) ) // TODO: Female Hrothgar #endif #if USE_CMP .Append( Structs.CharacterUtility.HumanCmpIdx ) @@ -57,38 +53,53 @@ public unsafe class CharacterUtility : IDisposable { SignatureHelper.Initialise( this ); - if( Address->EqpResource != null && Address->EqpResource->Data != null ) - { - LoadDefaultResources(); - } - else - { - LoadDataFilesHook.Enable(); - } - } - - // Self-disabling hook to set default resources after loading them. - private void LoadDataFilesDetour( Structs.CharacterUtility* characterUtility ) - { - LoadDataFilesHook.Original( characterUtility ); - LoadDefaultResources(); - PluginLog.Debug( "Character Utility resources loaded and defaults stored, disabling hook." ); - LoadDataFilesHook.Disable(); + Dalamud.Framework.Update += LoadDefaultResources; + LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); } // We store the default data of the resources so we can always restore them. - private void LoadDefaultResources() + private void LoadDefaultResources( object _ ) { + var missingCount = 0; + if( Address == null ) + { + return; + } + for( var i = 0; i < RelevantIndices.Length; ++i ) { - var resource = ( Structs.ResourceHandle* )Address->Resources[ RelevantIndices[ i ] ]; - DefaultResources[ i ] = resource->GetData(); + if( DefaultResources[ i ].Size == 0 ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ RelevantIndices[ i ] ]; + var data = resource->GetData(); + if( data.Data != IntPtr.Zero && data.Length != 0 ) + { + DefaultResources[ i ] = data; + } + else + { + ++missingCount; + } + } + } + + if( missingCount == 0 ) + { + Dalamud.Framework.Update -= LoadDefaultResources; + Ready = true; + LoadingFinished.Invoke(); } } // Set the data of one of the stored resources to a given pointer and length. public bool SetResource( int resourceIdx, IntPtr data, int length ) { + if( !Ready ) + { + PluginLog.Error( $"Can not set resource {resourceIdx}: CharacterUtility not ready yet." ); + return false; + } + var resource = Address->Resource( resourceIdx ); var ret = resource->SetData( data, length ); PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); @@ -98,6 +109,12 @@ public unsafe class CharacterUtility : IDisposable // Reset the data of one of the stored resources to its default values. public void ResetResource( int resourceIdx ) { + if( !Ready ) + { + PluginLog.Error( $"Can not reset {resourceIdx}: CharacterUtility not ready yet." ); + return; + } + var relevantIdx = ReverseIndices[ resourceIdx ]; var (data, length) = DefaultResources[ relevantIdx ]; var resource = Address->Resource( resourceIdx ); @@ -108,16 +125,22 @@ public unsafe class CharacterUtility : IDisposable // Return all relevant resources to the default resource. public void ResetAll() { + if( !Ready ) + { + PluginLog.Error( "Can not reset all resources: CharacterUtility not ready yet." ); + return; + } + foreach( var idx in RelevantIndices ) { ResetResource( idx ); } - PluginLog.Debug( "Reset all CharacterUtility resources to default." ); + + PluginLog.Debug( "Reset all CharacterUtility resources to default." ); } public void Dispose() { ResetAll(); - LoadDataFilesHook.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8c8148aa..98848d63 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,11 +23,32 @@ using Penumbra.Mods; namespace Penumbra; -public class Penumbra : IDalamudPlugin +public class MainClass : IDalamudPlugin { - public string Name - => "Penumbra"; + private Penumbra? _penumbra; + private readonly CharacterUtility _characterUtility; + public MainClass( DalamudPluginInterface pluginInterface ) + { + Dalamud.Initialize( pluginInterface ); + _characterUtility = new CharacterUtility(); + _characterUtility.LoadingFinished += () + => _penumbra = new Penumbra( _characterUtility ); + } + + public void Dispose() + { + _penumbra?.Dispose(); + _characterUtility.Dispose(); + } + + public string Name + => Penumbra.Name; +} + +public class Penumbra : IDisposable +{ + public const string Name = "Penumbra"; private const string CommandName = "/penumbra"; public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; @@ -60,9 +81,10 @@ public class Penumbra : IDalamudPlugin internal WebServer? WebServer; - public Penumbra( DalamudPluginInterface pluginInterface ) + public Penumbra( CharacterUtility characterUtility ) { - Dalamud.Initialize( pluginInterface ); + CharacterUtility = characterUtility; + Framework = new FrameworkManager(); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); Backup.CreateBackup( PenumbraBackupFiles() ); @@ -75,7 +97,6 @@ public class Penumbra : IDalamudPlugin } ResidentResources = new ResidentResourceManager(); - CharacterUtility = new CharacterUtility(); Redirects = new SimpleRedirectManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); @@ -94,9 +115,6 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); - Api = new PenumbraApi( this ); - Ipc = new PenumbraIpc( pluginInterface, Api ); - SubscribeItemLinks(); SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); if( Config.EnableHttpApi ) @@ -123,6 +141,10 @@ public class Penumbra : IDalamudPlugin } ResidentResources.Reload(); + + Api = new PenumbraApi( this ); + Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); + SubscribeItemLinks(); } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) From ced5e344cfbd215b4795b3a1d82ac98bda62619d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 21 May 2022 20:50:13 +0200 Subject: [PATCH 0193/2451] Remove single object change functions and update from mod manager. --- Penumbra/Collections/CollectionManager.cs | 1 - Penumbra/Mods/Manager/Mod.Manager.Options.cs | 94 +------------------- 2 files changed, 3 insertions(+), 92 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index ce8fa113..13b0fdf3 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -266,7 +266,6 @@ public partial class ModCollection ModOptionChangeType.OptionFilesChanged => ( false, true, false ), ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), ModOptionChangeType.OptionMetaChanged => ( false, true, true ), - ModOptionChangeType.OptionUpdated => ( false, true, true ), ModOptionChangeType.DisplayChange => ( false, false, false ), _ => ( false, false, false ), }; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 71cd0b02..eee4744f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -23,7 +23,6 @@ public enum ModOptionChangeType OptionFilesChanged, OptionSwapsChanged, OptionMetaChanged, - OptionUpdated, DisplayChange, } @@ -171,7 +170,7 @@ public sealed partial class Mod } option.Name = newName; - return; + break; } ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); @@ -244,41 +243,11 @@ public sealed partial class Mod } } - public void OptionSetManipulation( Mod mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) - { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( delete ) - { - if( !subMod.ManipulationData.Remove( manip ) ) - { - return; - } - } - else - { - if( subMod.ManipulationData.TryGetValue( manip, out var oldManip ) ) - { - if( manip.EntryEquals( oldManip ) ) - { - return; - } - - subMod.ManipulationData.Remove( oldManip ); - subMod.ManipulationData.Add( manip ); - } - else - { - subMod.ManipulationData.Add( manip ); - } - } - - ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); - } - public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) + if( subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) { return; } @@ -287,15 +256,6 @@ public sealed partial class Mod ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } - public void OptionSetFile( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) - { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( OptionSetFile( subMod.FileData, gamePath, newPath ) ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); - } - } - public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); @@ -319,15 +279,6 @@ public sealed partial class Mod } } - public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) - { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); - } - } - public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); @@ -340,16 +291,6 @@ public sealed partial class Mod ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } - public void OptionUpdate( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements, - HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps ) - { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - subMod.FileData = replacements; - subMod.ManipulationData = manipulations; - subMod.FileSwapData = swaps; - ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 ); - } - public static bool VerifyFileName( Mod mod, IModGroup? group, string newName, bool message ) { var path = newName.RemoveInvalidPathSymbols(); @@ -383,34 +324,6 @@ public sealed partial class Mod }; } - private static bool OptionSetFile( IDictionary< Utf8GamePath, FullPath > dict, Utf8GamePath gamePath, FullPath? newPath ) - { - if( dict.TryGetValue( gamePath, out var oldPath ) ) - { - if( newPath == null ) - { - dict.Remove( gamePath ); - return true; - } - - if( newPath.Value.Equals( oldPath ) ) - { - return false; - } - - dict[ gamePath ] = newPath.Value; - return true; - } - - if( newPath == null ) - { - return false; - } - - dict.Add( gamePath, newPath.Value ); - return true; - } - private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) { // File deletion is handled in the actual function. @@ -444,7 +357,6 @@ public sealed partial class Mod ModOptionChangeType.OptionFilesChanged => 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ), ModOptionChangeType.OptionSwapsChanged => 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ), ModOptionChangeType.OptionMetaChanged => 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ), - ModOptionChangeType.OptionUpdated => mod.SetCounts(), ModOptionChangeType.DisplayChange => false, _ => false, }; From c247446ba6a2e0366ca817dfb94e29556362eb05 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 May 2022 19:36:11 +0200 Subject: [PATCH 0194/2451] Add copy help text button. --- Penumbra/Penumbra.cs | 58 +++++++++++++++++++++++++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 15 ++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 98848d63..6a73e27b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; @@ -397,4 +398,61 @@ public class Penumbra : IDisposable list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); return list; } + + public static string GatherSupportInformation() + { + var sb = new StringBuilder( 10240 ); + sb.AppendLine( "**Settings**" ); + sb.AppendFormat( "> **`Plugin Version: `** {0}\n", Version ); + sb.AppendFormat( "> **`Commit Hash: `** {0}\n", CommitHash ); + sb.AppendFormat( "> **`Enable Mods: `** {0}\n", Config.EnableMods ); + sb.AppendFormat( "> **`Enable Sound Modification: `** {0}\n", Config.DisableSoundStreaming ); + sb.AppendFormat( "> **`Enable HTTP API: `** {0}\n", Config.EnableHttpApi ); + sb.AppendFormat( "> **`Root Directory: `** {0}, {1}\n", Config.ModDirectory, + Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ) ? "Exists" : "Not Existing" ); + sb.AppendLine( "**Mods**" ); + sb.AppendFormat( "> **`Installed Mods: `** {0}\n", ModManager.Count ); + sb.AppendFormat( "> **`Mods with Config: `** {0}\n", ModManager.Count( m => m.HasOptions ) ); + sb.AppendFormat( "> **`Mods with File Redirections: `** {0}, Total: {1}\n", ModManager.Count( m => m.TotalFileCount > 0 ), + ModManager.Sum( m => m.TotalFileCount ) ); + sb.AppendFormat( "> **`Mods with FileSwaps: `** {0}, Total: {1}\n", ModManager.Count( m => m.TotalSwapCount > 0 ), + ModManager.Sum( m => m.TotalSwapCount ) ); + sb.AppendFormat( "> **`Mods with Meta Manipulations:`** {0}, Total {1}\n", ModManager.Count( m => m.TotalManipulations > 0 ), + ModManager.Sum( m => m.TotalManipulations ) ); + + string CollectionName( ModCollection c ) + => c == ModCollection.Empty ? ModCollection.Empty.Name : c.Name.Length >= 2 ? c.Name[ ..2 ] : c.Name; + + string CharacterName( string name ) + => string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ) + ':'; + + void PrintCollection( ModCollection c ) + => sb.AppendFormat( "**Collection {0}... ({1})**\n" + + "> **`Inheritances: `** {2}\n" + + "> **`Enabled Mods: `** {3}\n" + + "> **`Total Conflicts: `** {4}\n" + + "> **`Solved Conflicts: `** {5}\n", + CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), c.Conflicts.Count, + c.Conflicts.Count( con => con.Solved ) ); + + sb.AppendLine( "**Collections**" ); + sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count ); + sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); + sb.AppendFormat( "> **`Default Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Default ), + CollectionManager.Default.Index ); + sb.AppendFormat( "> **`Current Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Current ), + CollectionManager.Current.Index ); + foreach( var (name, collection) in CollectionManager.Characters ) + { + sb.AppendFormat( "> **`{2,-29}`** {0}... ({1})\n", CollectionName( collection ), + collection.Index, CharacterName( name ) ); + } + + foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) + { + PrintCollection( collection ); + } + + return sb.ToString(); + } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 7f598c0f..6db24860 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -203,14 +203,25 @@ public partial class ConfigWindow private static void DrawDiscordButton() { + const string help = "Copy Support Info to Clipboard"; const string discord = "Join Discord for Support"; const string address = @"https://discord.gg/kVva7DHV4r"; - var width = ImGui.CalcTextSize( discord ).X + ImGui.GetStyle().FramePadding.X * 2; + var width = ImGui.CalcTextSize( help ).X + ImGui.GetStyle().FramePadding.X * 2; if( ImGui.GetScrollMaxY() > 0 ) + { width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; + } + + ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, ImGui.GetFrameHeightWithSpacing() ) ); + if( ImGui.Button( help ) ) + { + var text = Penumbra.GatherSupportInformation(); + ImGui.SetClipboardText( text ); + } + ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, 0 ) ); using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); - if( ImGui.Button( discord ) ) + if( ImGui.Button( discord, new Vector2( width, 0 ) ) ) { try { From 4189d240de976a1963793bd8057a35efc017fedf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 May 2022 19:36:28 +0200 Subject: [PATCH 0195/2451] Turn imc attributes to checkmarks, small fixes. --- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 91 +++++++++++++++++------ Penumbra/UI/Classes/ModEditWindow.cs | 3 +- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 9b20e847..5139d7a3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -74,6 +74,7 @@ public partial class ModEditWindow { return; } + using( var table = ImRaii.Table( label, numColumns, flags ) ) { if( table ) @@ -101,7 +102,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize, editor.Meta.Eqp.Select( m => (MetaManipulation) m ) ); + CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize, + editor.Meta.Eqp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -126,14 +128,15 @@ public partial class ModEditWindow // Values ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) { - using var id = ImRaii.PushId( ( int )flag ); - var value = defaultEntry.HasFlag( flag ); - Checkmark( string.Empty, flag.ToLocalName(), value, value, out _ ); + var value = defaultEntry.HasFlag( flag ); + Checkmark( "##eqp", flag.ToLocalName(), value, value, out _ ); ImGui.SameLine(); } + ImGui.NewLine(); } @@ -152,19 +155,23 @@ public partial class ModEditWindow // Values ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + var idx = 0; foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) { - using var id = ImRaii.PushId( ( int )flag ); + using var id = ImRaii.PushId( idx++ ); var defaultValue = defaultEntry.HasFlag( flag ); var currentValue = meta.Entry.HasFlag( flag ); - if( Checkmark( string.Empty, flag.ToLocalName(), currentValue, defaultValue, out var value ) ) + if( Checkmark( "##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value ) ) { editor.Meta.Change( meta with { Entry = value ? meta.Entry | flag : meta.Entry & ~flag } ); } ImGui.SameLine(); } + + ImGui.NewLine(); } } @@ -179,7 +186,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize, editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) ); + CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize, + editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var raceCode = Names.CombinedRace( _new.Gender, _new.Race ); var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0; @@ -289,7 +297,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize, editor.Meta.Imc.Select( m => ( MetaManipulation )m ) ); + CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize, + editor.Meta.Imc.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( _new ); var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); @@ -314,6 +323,9 @@ public partial class ModEditWindow _new = _new with { PrimaryId = setId }; } + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. if( _new.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) @@ -354,8 +366,16 @@ public partial class ModEditWindow IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, 0f ); ImGui.TableNextColumn(); - IntDragInput( "##imcAttributes", "Attributes", IdWidth, defaultEntry.Value.AttributeMask, defaultEntry.Value.AttributeMask, out _, - 0, 0b1111111111, 0f ); + for( var i = 0; i < 10; ++i ) + { + using var id = ImRaii.PushId( i ); + var flag = 1 << i; + Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( defaultEntry.Value.AttributeMask & flag ) != 0, + ( defaultEntry.Value.AttributeMask & flag ) != 0, out _ ); + ImGui.SameLine(); + } + + ImGui.NewLine(); } public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize ) @@ -386,6 +406,8 @@ public partial class ModEditWindow ImGui.TextUnformatted( meta.Variant.ToString() ); // Values + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, @@ -423,11 +445,21 @@ public partial class ModEditWindow } ImGui.TableNextColumn(); - if( IntDragInput( "##imcAttributes", $"Attributes\nDefault Value: {defaultEntry.AttributeMask}", IdWidth, - meta.Entry.AttributeMask, defaultEntry.AttributeMask, out var attributeMask, 0, 0b1111111111, 0.1f ) ) + for( var i = 0; i < 10; ++i ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { AttributeMask = ( ushort )attributeMask } } ); + using var id = ImRaii.PushId( i ); + var flag = 1 << i; + if( Checkmark( "##attribute", $"{( char )( 'A' + i )}\nDefault Value: ", ( meta.Entry.AttributeMask & flag ) != 0, + ( defaultEntry.AttributeMask & flag ) != 0, out var val ) ) + { + var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; + editor.Meta.Change( meta with { Entry = meta.Entry with { AttributeMask = ( ushort )attributes } } ); + } + + ImGui.SameLine(); } + + ImGui.NewLine(); } } @@ -441,7 +473,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize, editor.Meta.Est.Select( m => ( MetaManipulation )m ) ); + CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize, + editor.Meta.Est.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -526,7 +559,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize, editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) ); + CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize, + editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -633,7 +667,8 @@ public partial class ModEditWindow public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize, editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) ); + CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize, + editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var canAdd = editor.Meta.CanAdd( _new ); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; @@ -783,19 +818,23 @@ public partial class ModEditWindow } } - private void AddFromClipboardButton( ) + private void AddFromClipboardButton() { if( ImGui.Button( "Add from Clipboard" ) ) { var clipboard = ImGui.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == CurrentManipulationVersion && manips != null) + if( version == CurrentManipulationVersion && manips != null ) { foreach( var manip in manips ) + { _editor!.Meta.Set( manip ); + } } } - ImGuiUtil.HoverTooltip( "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." ); + + ImGuiUtil.HoverTooltip( + "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." ); } private void SetFromClipboardButton() @@ -803,21 +842,25 @@ public partial class ModEditWindow if( ImGui.Button( "Set from Clipboard" ) ) { var clipboard = ImGui.GetClipboardText(); - var version = Functions.FromCompressedBase64( clipboard, out var manips ); + var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); if( version == CurrentManipulationVersion && manips != null ) { _editor!.Meta.Clear(); foreach( var manip in manips ) + { _editor!.Meta.Set( manip ); + } } } - ImGuiUtil.HoverTooltip( "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." ); + + ImGuiUtil.HoverTooltip( + "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." ); } private static void DrawMetaButtons( MetaManipulation meta, Mod.Editor editor, Vector2 iconSize ) { ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty().Append( meta ) ); + CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty< MetaManipulation >().Append( meta ) ); ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true ) ) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 5f9d6be7..4f902467 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -9,7 +9,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Util; @@ -35,7 +34,7 @@ public partial class ModEditWindow : Window, IDisposable WindowName = $"{mod.Name}{WindowBaseLabel}"; SizeConstraints = new WindowSizeConstraints { - MinimumSize = ImGuiHelpers.ScaledVector2( 800, 600 ), + MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ), MaximumSize = 4000 * Vector2.One, }; } From 46c8b811ad9d59fd0262956b7e60111e8d0373c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 May 2022 13:28:50 +0200 Subject: [PATCH 0196/2451] Add font reloading button. --- Penumbra/Interop/FontReloader.cs | 56 +++++++++++++++++++ .../UI/ConfigWindow.SettingsTab.Advanced.cs | 11 ++++ 2 files changed, 67 insertions(+) create mode 100644 Penumbra/Interop/FontReloader.cs diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/FontReloader.cs new file mode 100644 index 00000000..c962ae37 --- /dev/null +++ b/Penumbra/Interop/FontReloader.cs @@ -0,0 +1,56 @@ +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Penumbra.Interop; + +// Handle font reloading via +public static unsafe class FontReloader +{ + private static readonly AtkModule* AtkModule = null; + private static readonly delegate* unmanaged ReloadFontsFunc = null; + + public static bool Valid + => ReloadFontsFunc != null; + + public static void Reload() + { + if( Valid ) + { + ReloadFontsFunc( AtkModule, false, true ); + } + else + { + PluginLog.Error( "Could not reload fonts, function could not be found." ); + } + } + + static FontReloader() + { + if( ReloadFontsFunc != null ) + { + return; + } + + var framework = Framework.Instance(); + if( framework == null ) + { + return; + } + + var uiModule = framework->GetUiModule(); + if( uiModule == null ) + { + return; + } + + var atkModule = uiModule->GetRaptureAtkModule(); + if( atkModule == null ) + { + return; + } + + AtkModule = &atkModule->AtkModule; + ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ 43 ]; + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index dd3a0f01..d992f8ff 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -1,8 +1,10 @@ +using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; +using Penumbra.Interop; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -24,6 +26,7 @@ public partial class ConfigWindow DrawEnableDebugModeBox(); DrawEnableFullResourceLoggingBox(); DrawReloadResourceButton(); + DrawReloadFontsButton(); ImGui.NewLine(); } @@ -168,5 +171,13 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" + "You usually should not need to do this." ); } + + private static void DrawReloadFontsButton() + { + if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !FontReloader.Valid ) ) + { + FontReloader.Reload(); + } + } } } \ No newline at end of file From 2500512a6af1297989e9a0bf96506bc7f2622925 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 May 2022 13:28:57 +0200 Subject: [PATCH 0197/2451] Cleanup --- Penumbra/Collections/ModCollection.Cache.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 254502f3..40704785 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -308,11 +308,6 @@ public partial class ModCollection // Struct to keep track of all priorities involved in a mod and register and compare accordingly. private readonly record struct FileRegister( int ModIdx, int ModPriority, int GroupPriority, int OptionPriority ) { - public readonly int ModIdx = ModIdx; - public readonly int ModPriority = ModPriority; - public readonly int GroupPriority = GroupPriority; - public readonly int OptionPriority = OptionPriority; - public bool SameMod( FileRegister other, out bool less ) { if( ModIdx != other.ModIdx ) From e5c4ddf45db4a2ce529f068fa0a50b81357c2f87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 May 2022 13:29:12 +0200 Subject: [PATCH 0198/2451] Move help button in mod selector to center. --- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ab39f539..bc72fd12 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -33,7 +33,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod SubscribeRightClickFolder( OwnDescendants, 15 ); AddButton( AddNewModButton, 0 ); AddButton( AddImportModButton, 1 ); - AddButton( AddHelpButton, 800 ); + AddButton( AddHelpButton, 2 ); AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); From 0ff46fa8607aed239dd73729660ea2420e40d85e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 May 2022 15:23:46 +0200 Subject: [PATCH 0199/2451] blep --- Penumbra/Interop/FontReloader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/FontReloader.cs index c962ae37..aad57199 100644 --- a/Penumbra/Interop/FontReloader.cs +++ b/Penumbra/Interop/FontReloader.cs @@ -4,7 +4,8 @@ using FFXIVClientStructs.FFXIV.Component.GUI; namespace Penumbra.Interop; -// Handle font reloading via +// Handle font reloading via game functions. +// May cause a interface flicker while reloading. public static unsafe class FontReloader { private static readonly AtkModule* AtkModule = null; @@ -43,7 +44,7 @@ public static unsafe class FontReloader { return; } - + var atkModule = uiModule->GetRaptureAtkModule(); if( atkModule == null ) { From ee87098386de2dd0c26c5a6a19084331644a96e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 28 May 2022 16:18:47 +0200 Subject: [PATCH 0200/2451] Fix all files getting added to the default option on import. --- Penumbra/Mods/Mod.Creation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index de4f9f88..b42a3b7d 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -122,6 +122,7 @@ public partial class Mod internal static void CreateDefaultFiles( DirectoryInfo directory ) { var mod = new Mod( directory ); + mod.Reload( out _ ); foreach( var file in mod.FindUnusedFiles() ) { if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) From 4b036c6c2667e32b9486e748751e2aa527ad928e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 29 May 2022 19:00:34 +0200 Subject: [PATCH 0201/2451] Change cache reloading and conflicts to actually keep the effective mod and not force full recalculations on every change. --- OtterGui | 2 +- Penumbra.GameData/GameData.cs | 62 +- Penumbra/Api/ModsController.cs | 2 +- Penumbra/Api/PenumbraApi.cs | 3 +- Penumbra/Api/SimpleRedirectManager.cs | 7 +- .../Collections/CollectionManager.Active.cs | 28 +- Penumbra/Collections/CollectionManager.cs | 59 +- .../Collections/ModCollection.Cache.Access.cs | 50 +- Penumbra/Collections/ModCollection.Cache.cs | 584 +++++++++++------- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 23 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 29 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 37 +- Penumbra/Meta/Manager/MetaManager.cs | 49 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 1 + Penumbra/Mods/Manager/Mod.Manager.Options.cs | 24 +- Penumbra/Mods/Manager/ModOptionChangeType.cs | 50 ++ Penumbra/Mods/Mod.BasePath.cs | 1 + Penumbra/Mods/Mod.Files.cs | 1 - Penumbra/Mods/Mod.Meta.cs | 9 + Penumbra/Penumbra.cs | 5 +- .../Classes/ModFileSystemSelector.Filters.cs | 4 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 53 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 20 +- Penumbra/UI/ConfigWindow.Misc.cs | 6 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 13 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 53 +- 29 files changed, 778 insertions(+), 457 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModOptionChangeType.cs diff --git a/OtterGui b/OtterGui index d6fcf1f5..3679cb37 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d6fcf1f53888d5eec8196eaba2dbb3853534d3bf +Subproject commit 3679cb37d5cc04351c064b1372a6eac51c7375a8 diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index cb294ca4..55b53aec 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -7,45 +7,43 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +public static class GameData { - public static class GameData - { - internal static ObjectIdentification? Identification; - internal static readonly GamePathParser GamePathParser = new(); + internal static ObjectIdentification? Identification; + internal static readonly GamePathParser GamePathParser = new(); - public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + { + Identification ??= new ObjectIdentification( dataManager, clientLanguage ); + return Identification; + } + + public static IObjectIdentifier GetIdentifier() + { + if( Identification == null ) { - Identification ??= new ObjectIdentification( dataManager, clientLanguage ); - return Identification; + throw new Exception( "Object Identification was not initialized." ); } - public static IObjectIdentifier GetIdentifier() - { - if( Identification == null ) - { - throw new Exception( "Object Identification was not initialized." ); - } - - return Identification; - } - - public static IGamePathParser GetGamePathParser() - => GamePathParser; + return Identification; } - public interface IObjectIdentifier - { - public void Identify( IDictionary< string, object? > set, GamePath path ); + public static IGamePathParser GetGamePathParser() + => GamePathParser; +} - public Dictionary< string, object? > Identify( GamePath path ); - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ); - } +public interface IObjectIdentifier +{ + public void Identify( IDictionary< string, object? > set, GamePath path ); + public Dictionary< string, object? > Identify( GamePath path ); + public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ); +} - public interface IGamePathParser - { - public ObjectType PathToObjectType( GamePath path ); - public GameObjectInfo GetFileInfo( GamePath path ); - public string VfxToKey( GamePath path ); - } +public interface IGamePathParser +{ + public ObjectType PathToObjectType( GamePath path ); + public GameObjectInfo GetFileInfo( GamePath path ); + public string VfxToKey( GamePath path ); } \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index c8254c42..a909aab4 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -36,7 +36,7 @@ public class ModsController : WebApiController { return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), - o => o.Value.FullName + o => o.Value.Path.FullName ) ?? new Dictionary< string, string >(); } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index cac23ecb..e0d85731 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; @@ -138,7 +139,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( collection.HasCache ) { - return collection.ChangedItems; + return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 ); } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs index 6b4a45b3..2f3e219f 100644 --- a/Penumbra/Api/SimpleRedirectManager.cs +++ b/Penumbra/Api/SimpleRedirectManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dalamud.Logging; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Mods; @@ -22,13 +23,13 @@ public enum RedirectResult public class SimpleRedirectManager { internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); - public readonly HashSet< string > AllowedTags = new(); + public readonly HashSet< string > AllowedTags = new(); - public void Apply( IDictionary< Utf8GamePath, FullPath > dict ) + public void Apply( IDictionary< Utf8GamePath, ModPath > dict ) { foreach( var (gamePath, (file, _)) in Replacements ) { - dict.TryAdd( gamePath, file ); + dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) ); } } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 519936a2..d21ad8f9 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -59,7 +59,7 @@ public partial class ModCollection var newCollection = this[ newIdx ]; if( newIdx > Empty.Index ) { - newCollection.CreateCache( false ); + newCollection.CreateCache(); } RemoveCache( oldCollectionIdx ); @@ -122,7 +122,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? (configChanged ? DefaultCollection : Empty.Name); + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { @@ -250,12 +250,12 @@ public partial class ModCollection // Cache handling. private void CreateNecessaryCaches() { - Default.CreateCache( true ); - Current.CreateCache( false ); + Default.CreateCache(); + Current.CreateCache(); foreach( var collection in _characters.Values ) { - collection.CreateCache( false ); + collection.CreateCache(); } } @@ -268,27 +268,27 @@ public partial class ModCollection } // Recalculate effective files for active collections on events. - private void OnModAddedActive( bool meta ) + private void OnModAddedActive( Mod mod ) { - foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.AddMod( mod, true ); } } - private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + private void OnModRemovedActive( Mod mod ) { - foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.RemoveMod( mod, true ); } } - private void OnModChangedActive( bool meta, int modIdx ) + private void OnModMovedActive( Mod mod ) { - foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.ReloadMod( mod, true ); } } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 13b0fdf3..36bb2599 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -204,7 +204,7 @@ public partial class ModCollection // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. foreach( var collection in this ) { - collection.ForceCacheUpdate( collection == Default ); + collection.ForceCacheUpdate(); } } @@ -221,27 +221,29 @@ public partial class ModCollection collection.AddMod( mod ); } - OnModAddedActive( mod.TotalManipulations > 0 ); + OnModAddedActive( mod ); break; case ModPathChangeType.Deleted: - var settings = this.Select( c => c[mod.Index].Settings ).ToList(); + OnModRemovedActive( mod ); foreach( var collection in this ) { collection.RemoveMod( mod, mod.Index ); } - OnModRemovedActive( mod.TotalManipulations > 0, settings ); break; case ModPathChangeType.Moved: + OnModMovedActive( mod ); foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) { collection.Save(); } - OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); + break; + case ModPathChangeType.StartingReload: + OnModRemovedActive( mod ); break; case ModPathChangeType.Reloaded: - OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); + OnModAddedActive( mod ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } @@ -252,25 +254,24 @@ public partial class ModCollection // And also updating effective file and meta manipulation lists if necessary. private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { - var (handleChanges, recomputeList, withMeta) = type switch + // Handle changes that break revertability. + if( type == ModOptionChangeType.PrepareChange ) { - ModOptionChangeType.GroupRenamed => ( true, false, false ), - ModOptionChangeType.GroupAdded => ( true, false, false ), - ModOptionChangeType.GroupDeleted => ( true, true, true ), - ModOptionChangeType.GroupMoved => ( true, false, false ), - ModOptionChangeType.GroupTypeChanged => ( true, true, true ), - ModOptionChangeType.PriorityChanged => ( true, true, true ), - ModOptionChangeType.OptionAdded => ( true, true, true ), - ModOptionChangeType.OptionDeleted => ( true, true, true ), - ModOptionChangeType.OptionMoved => ( true, false, false ), - ModOptionChangeType.OptionFilesChanged => ( false, true, false ), - ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), - ModOptionChangeType.OptionMetaChanged => ( false, true, true ), - ModOptionChangeType.DisplayChange => ( false, false, false ), - _ => ( false, false, false ), - }; + foreach( var collection in this.Where( c => c.HasCache ) ) + { + if( collection[ mod.Index ].Settings is { Enabled: true } ) + { + collection._cache!.RemoveMod( mod, false ); + } + } - if( handleChanges ) + return; + } + + type.HandlingInfo( out var requiresSaving, out var recomputeList, out var reload ); + + // Handle changes that require overwriting the collection. + if( requiresSaving ) { foreach( var collection in this ) { @@ -281,14 +282,22 @@ public partial class ModCollection } } + // Handle changes that reload the mod if the changes did not need to be prepared, + // or re-add the mod if they were prepared. if( recomputeList ) { - // TODO: Does not check if the option that was changed is actually enabled. foreach( var collection in this.Where( c => c.HasCache ) ) { if( collection[ mod.Index ].Settings is { Enabled: true } ) { - collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default ); + if( reload ) + { + collection._cache!.ReloadMod( mod, true ); + } + else + { + collection._cache!.AddMod( mod, true ); + } } } } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8de2e1cc..2e3bda92 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -6,6 +6,7 @@ using Dalamud.Logging; using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; +using Penumbra.Mods; namespace Penumbra.Collections; @@ -18,21 +19,21 @@ public partial class ModCollection => _cache != null; public int RecomputeCounter - => _cache?.RecomputeCounter ?? 0; + => _cache?.ChangeCounter ?? 0; // Only create, do not update. - private void CreateCache( bool isDefault ) + private void CreateCache() { if( _cache == null ) { - CalculateEffectiveFileList( true, isDefault ); + CalculateEffectiveFileList(); PluginLog.Verbose( "Created new cache for collection {Name:l}.", Name ); } } // Force an update with metadata for this cache. - private void ForceCacheUpdate( bool isDefault ) - => CalculateEffectiveFileList( true, isDefault ); + private void ForceCacheUpdate() + => CalculateEffectiveFileList(); // Clear the current cache. @@ -49,7 +50,7 @@ public partial class ModCollection // Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile( Utf8GamePath path, FullPath fullPath ) - => _cache!.ResolvedFiles[ path ] = fullPath; + => _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath ); // Force a file resolve to be removed. internal void RemoveFile( Utf8GamePath path ) @@ -59,25 +60,25 @@ public partial class ModCollection internal MetaManager? MetaCache => _cache?.MetaManipulations; - internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); - internal IReadOnlyDictionary< string, object? > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >(); - internal IReadOnlyList< ConflictCache.Conflict > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); + internal IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); - internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty; + internal SingleArray< ModConflicts > Conflicts( Mod mod ) + => _cache?.Conflicts( mod ) ?? new SingleArray< ModConflicts >(); // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) + public void CalculateEffectiveFileList() => Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name, - () => CalculateEffectiveFileListInternal( withMetaManipulations, reloadDefault ) ); + CalculateEffectiveFileListInternal ); - private void CalculateEffectiveFileListInternal( bool withMetaManipulations, bool reloadDefault ) + private void CalculateEffectiveFileListInternal() { // Skip the empty collection. if( Index == 0 ) @@ -85,16 +86,13 @@ public partial class ModCollection return; } - PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Thread.CurrentThread.ManagedThreadId, Name, - withMetaManipulations, reloadDefault ); + PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l}", + Thread.CurrentThread.ManagedThreadId, Name ); _cache ??= new Cache( this ); - _cache.CalculateEffectiveFileList( withMetaManipulations ); - if( reloadDefault ) - { - SetFiles(); - Penumbra.ResidentResources.Reload(); - } - PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", Thread.CurrentThread.ManagedThreadId, Name); + _cache.FullRecalculation(); + + PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", + Thread.CurrentThread.ManagedThreadId, Name ); } // Set Metadata files. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 40704785..d7c15b22 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,38 +1,44 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; using Dalamud.Logging; -using Dalamud.Utility; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Collections; +public record struct ModPath( Mod Mod, FullPath Path ); +public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); + public partial class ModCollection { // The Cache contains all required temporary data to use a collection. // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly Dictionary< Utf8GamePath, FileRegister > _registeredFiles = new(); - private readonly Dictionary< MetaManipulation, FileRegister > _registeredManipulations = new(); + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); - private readonly ModCollection _collection; - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - public ConflictCache Conflicts = new(); + public IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _conflicts.Values; - // Count the number of recalculations of the effective file list. + public SingleArray< ModConflicts > Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + + // Count the number of changes of the effective file list. // This is used for material and imc changes. - public int RecomputeCounter { get; private set; } = 0; + public int ChangeCounter { get; private set; } + private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, object? > ChangedItems + public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems { get { @@ -64,85 +70,376 @@ public partial class ModCollection return null; } - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) + if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } - return candidate; + return candidate.Path; } private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { - // Recompute the file list if it was not just a non-conflicting priority change - // or a setting change for a disabled mod. - if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any() - || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) + var mod = Penumbra.ModManager[ modIdx ]; + switch( type ) { - return; - } + case ModSettingChange.Inheritance: + ReloadMod( mod, true ); + break; + case ModSettingChange.EnableState: + if( oldValue != 1 ) + { + AddMod( mod, true ); + } + else + { + RemoveMod( mod, true ); + } - var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance - || Penumbra.ModManager[ modIdx ].AllManipulations.Any(); - _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); + break; + case ModSettingChange.Priority: + if( Conflicts( mod ).Count > 0 ) + { + ReloadMod( mod, true ); + } + + break; + case ModSettingChange.Setting: + if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( mod, true ); + } + + break; + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + FullRecalculation(); + break; + } } // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => _collection.CalculateEffectiveFileList( true, true ); + => FullRecalculation(); - // Clear all local and global caches to prepare for recomputation. - private void ClearStorageAndPrepare() + public void FullRecalculation() { - _changedItems.Clear(); - _registeredFiles.EnsureCapacity( 2 * ResolvedFiles.Count ); ResolvedFiles.Clear(); - Conflicts.ClearFileConflicts(); - } + MetaManipulations.Reset(); + _conflicts.Clear(); - // Recalculate all file changes from current settings. Include all fixed custom redirects. - // Recalculate meta manipulations only if withManipulations is true. - public void CalculateEffectiveFileList( bool withManipulations ) - { - ClearStorageAndPrepare(); - if( withManipulations ) - { - _registeredManipulations.EnsureCapacity( 2 * MetaManipulations.Count ); - MetaManipulations.Reset(); - } + // Add all forced redirects. + Penumbra.Redirects.Apply( ResolvedFiles ); - AddCustomRedirects(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) + foreach( var mod in Penumbra.ModManager ) { - AddMod( i, withManipulations ); + AddMod( mod, false ); } AddMetaFiles(); - ++RecomputeCounter; - _registeredFiles.Clear(); - _registeredFiles.TrimExcess(); - _registeredManipulations.Clear(); - _registeredManipulations.TrimExcess(); + + ++ChangeCounter; + + if( _collection == Penumbra.CollectionManager.Default ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } } + public void ReloadMod( Mod mod, bool addMetaChanges ) + { + RemoveMod( mod, addMetaChanges ); + AddMod( mod, addMetaChanges ); + } + + public void RemoveMod( Mod mod, bool addMetaChanges ) + { + var conflicts = Conflicts( mod ); + + foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) + { + if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) + { + continue; + } + + if( modPath.Mod == mod ) + { + ResolvedFiles.Remove( path ); + } + } + + foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) + { + if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) + { + MetaManipulations.RevertMod( manipulation ); + } + } + + _conflicts.Remove( mod ); + foreach( var conflict in conflicts ) + { + if( conflict.HasPriority ) + { + ReloadMod( conflict.Mod2, false ); + } + else + { + var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); + if( newConflicts.Count > 0 ) + { + _conflicts[ conflict.Mod2 ] = newConflicts; + } + else + { + _conflicts.Remove( conflict.Mod2 ); + } + } + } + + if( addMetaChanges ) + { + ++ChangeCounter; + if( _collection == Penumbra.CollectionManager.Default ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + } + + + // Add all files and possibly manipulations of a given mod according to its settings in this collection. + public void AddMod( Mod mod, bool addMetaChanges ) + { + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) + { + return; + } + + AddSubMod( mod.Default, mod ); + foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) + { + if( group.Count == 0 ) + { + continue; + } + + var config = settings.Settings[ groupIndex ]; + switch( group.Type ) + { + case SelectType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } + + break; + } + } + } + + if( addMetaChanges ) + { + ++ChangeCounter; + if( _collection == Penumbra.CollectionManager.Default ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + + if( mod.TotalManipulations > 0 ) + { + AddMetaFiles(); + } + } + } + + // Add all files and possibly manipulations of a specific submod + private void AddSubMod( ISubMod subMod, Mod parentMod ) + { + foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) + { + // Skip all filtered files + if( Mod.FilterFile( path ) ) + { + continue; + } + + AddFile( path, file, parentMod ); + } + + foreach( var manip in subMod.Manipulations ) + { + AddManipulation( manip, parentMod ); + } + } + + // Add a specific file redirection, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddFile( Utf8GamePath path, FullPath file, Mod mod ) + { + if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) + { + return; + } + + var modPath = ResolvedFiles[ path ]; + // Lower prioritized option in the same mod. + if( mod == modPath.Mod ) + { + return; + } + + if( AddConflict( path, mod, modPath.Mod ) ) + { + ResolvedFiles[ path ] = new ModPath( mod, file ); + } + } + + + // Remove all empty conflict sets for a given mod with the given conflicts. + // If transitive is true, also removes the corresponding version of the other mod. + private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + { + var changedConflicts = oldConflicts.Remove( c => + { + if( c.Conflicts.Count == 0 ) + { + if( transitive ) + { + RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); + } + + return true; + } + + return false; + } ); + if( changedConflicts.Count == 0 ) + { + _conflicts.Remove( mod ); + } + else + { + _conflicts[ mod ] = changedConflicts; + } + } + + // Add a new conflict between the added mod and the existing mod. + // Update all other existing conflicts between the existing mod and other mods if necessary. + // Returns if the added mod takes priority before the existing mod. + private bool AddConflict( object data, Mod addedMod, Mod existingMod ) + { + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + + if( existingPriority < addedPriority ) + { + var tmpConflicts = Conflicts( existingMod ); + foreach( var conflict in tmpConflicts ) + { + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals(path) ) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals(meta) ) > 0 ) + { + AddConflict( data, addedMod, conflict.Mod2 ); + } + } + + RemoveEmptyConflicts( existingMod, tmpConflicts, true ); + } + + var addedConflicts = Conflicts( addedMod ); + var existingConflicts = Conflicts( existingMod ); + if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) + { + // Only need to change one list since both conflict lists refer to the same list. + oldConflicts.Conflicts.Add( data ); + } + else + { + // Add the same conflict list to both conflict directions. + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + existingPriority != addedPriority ) ); + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + existingPriority >= addedPriority, + existingPriority != addedPriority ) ); + } + + return existingPriority < addedPriority; + } + + // Add a specific manipulation, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddManipulation( MetaManipulation manip, Mod mod ) + { + if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + return; + } + + // Lower prioritized option in the same mod. + if( mod == existingMod ) + { + return; + } + + if( AddConflict( manip, mod, existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + } + } + + + // Add all necessary meta file redirects. + private void AddMetaFiles() + => MetaManipulations.Imc.SetFiles(); + // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) + if( _changedItemsSaveCounter == ChangeCounter ) { return; } try { + _changedItemsSaveCounter = ChangeCounter; + _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( 'i', 'm', 'c' ) ) ) { - identifier.Identify( _changedItems, resolved.ToGamePath() ); + foreach( var (name, obj) in identifier.Identify( resolved.ToGamePath() ) ) + { + if( !_changedItems.TryGetValue( name, out var data ) ) + { + _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + } + else if( !data.Item1.Contains( modPath.Mod ) ) + { + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj ); + } + } } // TODO: Meta Manipulations } @@ -151,186 +448,5 @@ public partial class ModCollection PluginLog.Error( $"Unknown Error:\n{e}" ); } } - - // Add a specific file redirection, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, FileRegister priority ) - { - if( _registeredFiles.TryGetValue( path, out var register ) ) - { - if( register.SameMod( priority, out var less ) ) - { - Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path ); - if( less ) - { - _registeredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; - } - } - else - { - // File seen before in the same mod: - // use higher priority or earlier recurrences in case of same priority. - // Do not add conflicts. - if( less ) - { - _registeredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; - } - } - } - else // File not seen before, just add it. - { - _registeredFiles.Add( path, priority ); - ResolvedFiles.Add( path, file ); - } - } - - // Add a specific manipulation, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, FileRegister priority ) - { - if( _registeredManipulations.TryGetValue( manip, out var register ) ) - { - if( register.SameMod( priority, out var less ) ) - { - Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, manip ); - if( less ) - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - else - { - // Manipulation seen before in the same mod: - // use higher priority or earlier occurrences in case of same priority. - // Do not add conflicts. - if( less ) - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - } - else // Manipulation not seen before, just add it. - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - - // Add all files and possibly manipulations of a specific submod with the given priorities. - private void AddSubMod( ISubMod mod, FileRegister priority, bool withManipulations ) - { - foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) ) - { - // Skip all filtered files - if( Mod.FilterFile( path ) ) - { - continue; - } - - AddFile( path, file, priority ); - } - - if( withManipulations ) - { - foreach( var manip in mod.Manipulations ) - { - AddManipulation( manip, priority ); - } - } - } - - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - private void AddMod( int modIdx, bool withManipulations ) - { - var settings = _collection[ modIdx ].Settings; - if( settings is not { Enabled: true } ) - { - return; - } - - var mod = Penumbra.ModManager[ modIdx ]; - AddSubMod( mod.Default, new FileRegister( modIdx, settings.Priority, 0, 0 ), withManipulations ); - for( var idx = 0; idx < mod.Groups.Count; ++idx ) - { - var config = settings.Settings[ idx ]; - var group = mod.Groups[ idx ]; - if( group.Count == 0 ) - { - continue; - } - - switch( group.Type ) - { - case SelectType.Single: - var singlePriority = new FileRegister( modIdx, settings.Priority, group.Priority, group.Priority ); - AddSubMod( group[ ( int )config ], singlePriority, withManipulations ); - break; - case SelectType.Multi: - { - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) - { - if( ( ( 1 << optionIdx ) & config ) != 0 ) - { - var priority = new FileRegister( modIdx, settings.Priority, group.Priority, group.OptionPriority( optionIdx ) ); - AddSubMod( group[ optionIdx ], priority, withManipulations ); - } - } - - break; - } - } - } - } - - // Add all necessary meta file redirects. - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - // Add all API redirects. - private void AddCustomRedirects() - { - Penumbra.Redirects.Apply( ResolvedFiles ); - foreach( var gamePath in ResolvedFiles.Keys ) - { - _registeredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); - } - } - - - // Struct to keep track of all priorities involved in a mod and register and compare accordingly. - private readonly record struct FileRegister( int ModIdx, int ModPriority, int GroupPriority, int OptionPriority ) - { - public bool SameMod( FileRegister other, out bool less ) - { - if( ModIdx != other.ModIdx ) - { - less = ModPriority < other.ModPriority; - return true; - } - - if( GroupPriority < other.GroupPriority ) - { - less = true; - } - else if( GroupPriority == other.GroupPriority ) - { - less = OptionPriority < other.OptionPriority; - } - else - { - less = false; - } - - return false; - } - }; } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index e591a0b6..eae00788 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerCmp : IDisposable { public CmpFile? File = null; - public readonly Dictionary< RspManipulation, int > Manipulations = new(); + public readonly Dictionary< RspManipulation, Mod > Manipulations = new(); public MetaManagerCmp() { } @@ -38,10 +39,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, int modIdx ) + public bool ApplyMod( RspManipulation m, Mod mod ) { #if USE_CMP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new CmpFile(); return m.Apply( File ); #else @@ -49,6 +50,19 @@ public partial class MetaManager #endif } + public bool RevertMod( RspManipulation m ) + { +#if USE_CMP + if( Manipulations.Remove( m ) ) + { + var def = CmpFile.GetDefault( m.SubRace, m.Attribute ); + var manip = new RspManipulation( m.SubRace, m.Attribute, def ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 96daf6a0..bb5ec9d0 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -6,6 +6,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Meta.Manager; @@ -14,9 +15,9 @@ public partial class MetaManager { public struct MetaManagerEqdp : IDisposable { - public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar + public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, int > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new(); public MetaManagerEqdp() { } @@ -50,10 +51,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, int modIdx ) + public bool ApplyMod( EqdpManipulation m, Mod mod ) { #if USE_EQDP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar return m.Apply( file ); @@ -62,6 +63,20 @@ public partial class MetaManager #endif } + public bool RevertMod( EqdpManipulation m ) + { +#if USE_EQDP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory(), m.SetId ); + var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ]!; + var manip = new EqdpManipulation( def, m.Slot, m.Gender, m.Race, m.SetId ); + return manip.Apply( file ); + } +#endif + return false; + } + public ExpandedEqdpFile? File( GenderRace race, bool accessory ) => Files[ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 053e8228..9c980d67 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerEqp : IDisposable { public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, int > Manipulations = new(); + public readonly Dictionary< EqpManipulation, Mod > Manipulations = new(); public MetaManagerEqp() { } @@ -38,10 +39,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, int modIdx ) + public bool ApplyMod( EqpManipulation m, Mod mod ) { #if USE_EQP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new ExpandedEqpFile(); return m.Apply( File ); #else @@ -49,6 +50,19 @@ public partial class MetaManager #endif } + public bool RevertMod( EqpManipulation m ) + { +#if USE_EQP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedEqpFile.GetDefault( m.SetId ); + var manip = new EqpManipulation( def, m.Slot, m.SetId ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index c6901f2f..ccc5f926 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -16,7 +18,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, int > Manipulations = new(); + public readonly Dictionary< EstManipulation, Mod > Manipulations = new(); public MetaManagerEst() { } @@ -49,10 +51,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, int modIdx ) + public bool ApplyMod( EstManipulation m, Mod mod ) { #if USE_EST - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var file = m.Slot switch { EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), @@ -67,6 +69,27 @@ public partial class MetaManager #endif } + public bool RevertMod( EstManipulation m ) + { +#if USE_EST + if( Manipulations.Remove( m ) ) + { + var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); + var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); + var file = m.Slot switch + { + EstManipulation.EstType.Hair => HairFile!, + EstManipulation.EstType.Face => FaceFile!, + EstManipulation.EstType.Body => BodyFile!, + EstManipulation.EstType.Head => HeadFile!, + _ => throw new ArgumentOutOfRangeException(), + }; + return manip.Apply( file ); + } +#endif + return false; + } + public void Dispose() { FaceFile?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 4bdee5c3..49c29076 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerGmp : IDisposable { public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, int > Manipulations = new(); + public readonly Dictionary< GmpManipulation, Mod > Manipulations = new(); public MetaManagerGmp() { } @@ -37,10 +38,10 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, int modIdx ) + public bool ApplyMod( GmpManipulation m, Mod mod ) { #if USE_GMP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new ExpandedGmpFile(); return m.Apply( File ); #else @@ -48,6 +49,19 @@ public partial class MetaManager #endif } + public bool RevertMod( GmpManipulation m ) + { +#if USE_GMP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedGmpFile.GetDefault( m.SetId ); + var manip = new GmpManipulation( def, m.SetId ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index ed6457a1..c3858564 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -17,7 +18,7 @@ public partial class MetaManager public readonly struct MetaManagerImc : IDisposable { public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, int > Manipulations = new(); + public readonly Dictionary< ImcManipulation, Mod > Manipulations = new(); private readonly ModCollection _collection; private static int _imcManagerCount; @@ -64,10 +65,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, int modIdx ) + public bool ApplyMod( ImcManipulation m, Mod mod ) { #if USE_IMC - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var path = m.GamePath(); if( !Files.TryGetValue( path, out var file ) ) { @@ -92,6 +93,36 @@ public partial class MetaManager #endif } + public bool RevertMod( ImcManipulation m ) + { +#if USE_IMC + if( Manipulations.Remove( m ) ) + { + var path = m.GamePath(); + if( !Files.TryGetValue( path, out var file ) ) + { + return false; + } + + var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant ); + var manip = m with { Entry = def }; + if( !manip.Apply( file ) ) + { + return false; + } + + var fullPath = CreateImcPath( path ); + if( _collection.HasCache ) + { + _collection.ForceFile( path, fullPath ); + } + + return true; + } +#endif + return false; + } + public void Dispose() { foreach( var file in Files.Values ) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 06c97baf..5d6c0a5b 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -28,19 +30,19 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, out int modIdx ) + public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod ) { - modIdx = manip.ManipulationType switch + mod = manip.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : -1, - MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : -1, - MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : -1, - MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : -1, - MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : -1, - MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : -1, + MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, + MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, + MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, + MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, + MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, + MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, _ => throw new ArgumentOutOfRangeException(), }; - return modIdx != -1; + return mod != null; } public int Count @@ -84,16 +86,31 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, int modIdx ) + public bool ApplyMod( MetaManipulation m, Mod mod ) { return m.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, modIdx ), - MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, modIdx ), - MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, modIdx ), - MetaManipulation.Type.Est => Est.ApplyMod( m.Est, modIdx ), - MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, modIdx ), - MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, modIdx ), + MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), + MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), + MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), + MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), + MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), + MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } + + public bool RevertMod( MetaManipulation m ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Eqp => Eqp.RevertMod( m.Eqp ), + MetaManipulation.Type.Gmp => Gmp.RevertMod( m.Gmp ), + MetaManipulation.Type.Eqdp => Eqdp.RevertMod( m.Eqdp ), + MetaManipulation.Type.Est => Est.RevertMod( m.Est ), + MetaManipulation.Type.Rsp => Cmp.RevertMod( m.Rsp ), + MetaManipulation.Type.Imc => Imc.RevertMod( m.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index c82f09e6..abb28f0a 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -81,6 +81,7 @@ public partial class Mod var mod = this[ idx ]; var oldName = mod.Name; + ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); if( !mod.Reload( out var metaChange ) ) { PluginLog.Warning( mod.Name.Length == 0 diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index eee4744f..16df63e0 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -9,23 +9,6 @@ using Penumbra.Util; namespace Penumbra.Mods; -public enum ModOptionChangeType -{ - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, -} - public sealed partial class Mod { public sealed partial class Manager @@ -84,6 +67,7 @@ public sealed partial class Mod public void DeleteModGroup( Mod mod, int groupIdx ) { var group = mod._groups[ groupIdx ]; + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 ); mod._groups.RemoveAt( groupIdx ); group.DeleteFile( mod.ModPath, groupIdx ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); @@ -221,6 +205,7 @@ public sealed partial class Mod public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) { + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: @@ -252,6 +237,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.ManipulationData = manipulations; ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } @@ -264,6 +250,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.FileData = replacements; ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); } @@ -275,7 +262,7 @@ public sealed partial class Mod subMod.FileData.AddFrom( additions ); if( oldCount != subMod.FileData.Count ) { - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1 ); } } @@ -287,6 +274,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } diff --git a/Penumbra/Mods/Manager/ModOptionChangeType.cs b/Penumbra/Mods/Manager/ModOptionChangeType.cs new file mode 100644 index 00000000..080c2983 --- /dev/null +++ b/Penumbra/Mods/Manager/ModOptionChangeType.cs @@ -0,0 +1,50 @@ +namespace Penumbra.Mods; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, +} + +public static class ModOptionChangeTypeExtension +{ + // Give information for each type of change. + // If requiresSaving, collections need to be re-saved after this change. + // If requiresReloading, caches need to be manipulated after this change. + // If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. + // Otherwise, caches need to reload the mod itself. + public static void HandlingInfo( this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared ) + { + ( requiresSaving, requiresReloading, wasPrepared ) = type switch + { + ModOptionChangeType.GroupRenamed => ( true, false, false ), + ModOptionChangeType.GroupAdded => ( true, false, false ), + ModOptionChangeType.GroupDeleted => ( true, true, false ), + ModOptionChangeType.GroupMoved => ( true, false, false ), + ModOptionChangeType.GroupTypeChanged => ( true, true, true ), + ModOptionChangeType.PriorityChanged => ( true, true, true ), + ModOptionChangeType.OptionAdded => ( true, true, true ), + ModOptionChangeType.OptionDeleted => ( true, true, false ), + ModOptionChangeType.OptionMoved => ( true, false, false ), + ModOptionChangeType.OptionFilesChanged => ( false, true, false ), + ModOptionChangeType.OptionFilesAdded => ( false, true, true ), + ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), + ModOptionChangeType.OptionMetaChanged => ( false, true, false ), + ModOptionChangeType.DisplayChange => ( false, false, false ), + _ => ( false, false, false ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 16efdf12..43581735 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -9,6 +9,7 @@ public enum ModPathChangeType Deleted, Moved, Reloaded, + StartingReload, } public partial class Mod diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a10df895..439634c5 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index fad75f3d..3e052b3c 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -24,6 +24,12 @@ public enum MetaChangeType : ushort public sealed partial class Mod { + public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." )) + { + Name = "Forced Files", + Index = -1, + }; + public const uint CurrentFileVersion = 1; public uint FileVersion { get; private set; } = CurrentFileVersion; public LowerString Name { get; private set; } = "New Mod"; @@ -138,4 +144,7 @@ public sealed partial class Mod PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); } } + + public override string ToString() + => Name.Text; } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6a73e27b..a9bd7c7b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -432,8 +432,9 @@ public class Penumbra : IDisposable + "> **`Enabled Mods: `** {3}\n" + "> **`Total Conflicts: `** {4}\n" + "> **`Solved Conflicts: `** {5}\n", - CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), c.Conflicts.Count, - c.Conflicts.Count( con => con.Solved ) ); + CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), + c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count ), + c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); sb.AppendLine( "**Collections**" ); sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 2b342208..cc10eb12 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -119,7 +119,7 @@ public partial class ModFileSystemSelector return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value(); } - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); + var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); if( conflicts.Count == 0 ) { return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value(); @@ -188,7 +188,7 @@ public partial class ModFileSystemSelector } // Conflicts can only be relevant if the mod is enabled. - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); + var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); if( conflicts.Count > 0 ) { if( conflicts.Any( c => !c.Solved ) ) diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index aa7b6c1e..96253526 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -1,28 +1,52 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; +using Dalamud.Interface; using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { - private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemModFilter = LowerString.Empty; // Draw a simple clipped table containing all changed items. private void DrawChangedItemTab() { // Functions in here for less pollution. - bool FilterChangedItem( KeyValuePair< string, object? > item ) - => item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + => ( _changedItemFilter.IsEmpty || item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) + && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - void DrawChangedItemColumn( KeyValuePair< string, object? > item ) + void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) { ImGui.TableNextColumn(); - DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize ); + DrawChangedItem( item.Key, item.Value.Item2, false ); + ImGui.TableNextColumn(); + if( item.Value.Item1.Count > 0 ) + { + ImGui.TextUnformatted( item.Value.Item1[ 0 ].Name ); + if( item.Value.Item1.Count > 1 ) + { + ImGuiUtil.HoverTooltip( string.Join( "\n", item.Value.Item1.Skip( 1 ).Select( m => m.Name ) ) ); + } + } + + ImGui.TableNextColumn(); + if( item.Value.Item2 is Item it ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); + ImGuiUtil.RightAlign( $"({( ( Quad )it.ModelMain ).A})" ); + } } using var tab = ImRaii.TabItem( "Changed Items" ); @@ -32,8 +56,14 @@ public partial class ConfigWindow } // Draw filters. - ImGui.SetNextItemWidth( -1 ); - LowerString.InputWithHint( "##changedItemsFilter", "Filter...", ref _changedItemFilter, 64 ); + var varWidth = ImGui.GetContentRegionAvail().X + - 400 * ImGuiHelpers.GlobalScale + - ImGui.GetStyle().ItemSpacing.X; + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( varWidth ); + LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); if( !child ) @@ -44,14 +74,19 @@ public partial class ConfigWindow // Draw table of changed items. var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips( height ); - using var list = ImRaii.Table( "##changedItems", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); if( !list ) { return; } + const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; + ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "id", flags, 100 * ImGuiHelpers.GlobalScale ); + var items = Penumbra.CollectionManager.Default.ChangedItems; - var rest = _changedItemFilter.IsEmpty + var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); ImGuiClip.DrawEndDummy( rest, height ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 241a22bb..4d96423c 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.GameData.ByteString; +using Penumbra.Mods; namespace Penumbra.UI; @@ -111,7 +112,7 @@ public partial class ConfigWindow { // We can treat all meta manipulations the same, // we are only really interested in their ToString function here. - static (object, int) Convert< T >( KeyValuePair< T, int > kvp ) + static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp ) => ( kvp.Key!, kvp.Value ); var it = m.Cmp.Manipulations.Select( Convert ) @@ -124,7 +125,7 @@ public partial class ConfigWindow // Filters mean we can not use the known counts. if( hasFilters ) { - var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager[ p.Item2 ].Name ) ); + var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, p.Item2.Name ) ); if( stop >= 0 ) { ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); @@ -155,7 +156,7 @@ public partial class ConfigWindow } // Draw a line for a game path and its redirected file. - private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair ) + private static void DrawLine( KeyValuePair< Utf8GamePath, ModPath > pair ) { var (path, name) = pair; ImGui.TableNextColumn(); @@ -164,7 +165,8 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.TableNextColumn(); - CopyOnClickSelectable( name.InternalName ); + CopyOnClickSelectable( name.Path.InternalName ); + ImGuiUtil.HoverTooltip( $"\nChanged by {name.Mod.Name}." ); } // Draw a line for a path and its name. @@ -181,20 +183,20 @@ public partial class ConfigWindow } // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, int) pair ) + private static void DrawLine( (object, Mod) pair ) { - var (manipulation, modIdx) = pair; + var (manipulation, mod) = pair; ImGui.TableNextColumn(); ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); ImGui.TableNextColumn(); ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager[ modIdx ].Name ); + ImGuiUtil.CopyOnClickSelectable( mod.Name ); } // Check filters for file replacements. - private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) + private bool CheckFilters( KeyValuePair< Utf8GamePath, ModPath > kvp ) { var (gamePath, fullPath) = kvp; if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) @@ -202,7 +204,7 @@ public partial class ConfigWindow return false; } - return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); + return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); } // Check filters for meta manipulations. diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index d866a8d6..995ad308 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -29,8 +29,8 @@ public partial class ConfigWindow => Text( resource->FileName(), resource->FileNameLength ); // Draw a changed item, invoking the Api-Events for clicks and tooltips. - // Also draw the item Id in grey - private void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) + // Also draw the item Id in grey if requested + private void DrawChangedItem( string name, object? data, bool drawId ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; @@ -55,7 +55,7 @@ public partial class ConfigWindow } } - if( data is Item it ) + if( data is Item it && drawId ) { ImGui.SameLine( ImGui.GetContentRegionAvail().X ); ImGuiUtil.RightJustify( $"({( ( Quad )it.ModelMain ).A})", ColorId.ItemId.Value() ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index b9d80489..7acbbfb2 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -15,11 +15,11 @@ public partial class ConfigWindow { private partial class ModPanel { - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private bool _emptySetting; - private bool _inherited; - private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty; + private ModSettings _settings = null!; + private ModCollection _collection = null!; + private bool _emptySetting; + private bool _inherited; + private SingleArray< ModConflicts > _conflicts = new(); private int? _currentPriority; @@ -29,7 +29,7 @@ public partial class ConfigWindow _collection = selector.SelectedSettingCollection; _emptySetting = _settings == ModSettings.Empty; _inherited = _collection != Penumbra.CollectionManager.Current; - _conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); + _conflicts = Penumbra.CollectionManager.Current.Conflicts( _mod ); } // Draw the whole settings tab as well as its contents. @@ -105,7 +105,6 @@ public partial class ConfigWindow ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) { - _currentPriority = priority; } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 2b995701..7c57ea9a 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -93,11 +93,11 @@ public partial class ConfigWindow var zipList = ZipList.FromSortedList( _mod.ChangedItems ); var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height ); + ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2, true ), height ); } // If any conflicts exist, show them in this tab. - private void DrawConflictsTab() + private unsafe void DrawConflictsTab() { using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts ); if( !tab ) @@ -111,45 +111,30 @@ public partial class ConfigWindow return; } - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); - Mod? oldBadMod = null; - using var indent = ImRaii.PushIndent( 0f ); - foreach( var conflict in conflicts ) + foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) { - var badMod = Penumbra.ModManager[ conflict.Mod2 ]; - if( badMod != oldBadMod ) + if( ImGui.Selectable( conflict.Mod2.Name ) ) { - if( oldBadMod != null ) - { - indent.Pop( 30f ); - } - - if( ImGui.Selectable( badMod.Name ) ) - { - _window._selector.SelectByValue( badMod ); - } - - ImGui.SameLine(); - using var color = ImRaii.PushColor( ImGuiCol.Text, - conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); - ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); - - indent.Push( 30f ); + _window._selector.SelectByValue( conflict.Mod2 ); } - if( conflict.Data is Utf8GamePath p ) + ImGui.SameLine(); + using( var color = ImRaii.PushColor( ImGuiCol.Text, + conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) { - unsafe - { - ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); - } - } - else if( conflict.Data is MetaManipulation m ) - { - ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); + ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" ); } - oldBadMod = badMod; + using var indent = ImRaii.PushIndent( 30f ); + foreach( var data in conflict.Conflicts ) + { + var _ = data switch + { + Utf8GamePath p => ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) > 0, + MetaManipulation m => ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ), + _ => false, + }; + } } } From 50a3a20718dc7e21acd4a395aac57abe32878806 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 May 2022 16:11:10 +0200 Subject: [PATCH 0202/2451] Make default submod lowest priority again. --- Penumbra/Collections/ModCollection.Cache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index d7c15b22..44df1ae2 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -222,7 +222,6 @@ public partial class ModCollection return; } - AddSubMod( mod.Default, mod ); foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) { if( group.Count == 0 ) @@ -249,6 +248,7 @@ public partial class ModCollection } } } + AddSubMod( mod.Default, mod ); if( addMetaChanges ) { From 3d7faad2aee38aae8b811d44b0ba8c990390fb7c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 May 2022 16:11:35 +0200 Subject: [PATCH 0203/2451] Add options for UI hiding. --- Penumbra/Configuration.cs | 3 ++ .../UI/ConfigWindow.SettingsTab.General.cs | 36 +++++++++++++++++++ Penumbra/UI/ConfigWindow.cs | 6 ++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 209b726b..896295c8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -17,6 +17,9 @@ public partial class Configuration : IPluginConfiguration public bool EnableMods { get; set; } = true; public string ModDirectory { get; set; } = string.Empty; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiWhenUiHidden { get; set; } = false; #if DEBUG public bool DebugMode { get; set; } = true; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index faddf3c7..5fe02deb 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -13,6 +13,20 @@ public partial class ConfigWindow { private partial class SettingsTab { + private static void Checkbox( string label, string tooltip, bool current, Action< bool > setter ) + { + using var id = ImRaii.PushId( label ); + var tmp = current; + if( ImGui.Checkbox( string.Empty, ref tmp ) && tmp != current ) + { + setter( tmp ); + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( label, tooltip ); + } + private void DrawModSelectorSettings() { if( !ImGui.CollapsingHeader( "General" ) ) @@ -20,6 +34,28 @@ public partial class ConfigWindow return; } + Checkbox( "Hide Config Window when UI is Hidden", + "Hide the penumbra main window when you manually hide the in-game user interface.", Penumbra.Config.HideUiWhenUiHidden, + v => + { + Penumbra.Config.HideUiWhenUiHidden = v; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !v; + } ); + Checkbox( "Hide Config Window when in Cutscenes", + "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, + v => + { + Penumbra.Config.HideUiInCutscenes = v; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !v; + } ); + Checkbox( "Hide Config Window when in GPose", + "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, + v => + { + Penumbra.Config.HideUiInGPose = v; + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; + } ); + ImGui.Dummy( _window._defaultSpace ); DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 793d42eb..e8f610de 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -36,9 +36,9 @@ public sealed partial class ConfigWindow : Window, IDisposable _debugTab = new DebugTab( this ); _resourceTab = new ResourceTab( this ); - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; - Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = true; + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { From 81435b4ff28e76029acc91cdb61b74f55dad40d4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 May 2022 16:19:43 +0200 Subject: [PATCH 0204/2451] Change when the Deletion event is fired so that all mods are still there at invoke. --- Penumbra/Collections/CollectionManager.cs | 6 +++--- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 36bb2599..b222d445 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -68,7 +68,7 @@ public partial class ModCollection _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; _modManager.ModOptionChanged += OnModOptionsChanged; - _modManager.ModPathChanged += OnModPathChanged; + _modManager.ModPathChanged += OnModPathChange; CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); @@ -79,7 +79,7 @@ public partial class ModCollection _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; _modManager.ModOptionChanged -= OnModOptionsChanged; - _modManager.ModPathChanged -= OnModPathChanged; + _modManager.ModPathChanged -= OnModPathChange; } // Returns true if the name is not empty, it is not the name of the empty collection @@ -210,7 +210,7 @@ public partial class ModCollection // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChanged( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory ) { switch( type ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index abb28f0a..e9a2d104 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -99,7 +99,7 @@ public partial class Mod } } - // Delete a mod by its index. + // Delete a mod by its index. The event is invoked before the mod is removed from the list. // Deletes from filesystem as well as from internal data. // Updates indices of later mods. public void DeleteMod( int idx ) @@ -118,13 +118,13 @@ public partial class Mod } } + ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null ); _mods.RemoveAt( idx ); foreach( var remainingMod in _mods.Skip( idx ) ) { --remainingMod.Index; } - ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null ); PluginLog.Debug( "Deleted mod {Name:l}.", mod.Name ); } From 630469fc0ea0bb712a43098bcef8b9e02dd0b59b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 May 2022 22:06:09 +0200 Subject: [PATCH 0205/2451] Stop window actors from redrawing/unloading. --- Penumbra/Interop/ObjectReloader.cs | 45 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 8566218d..bf89f061 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -36,48 +36,61 @@ public sealed unsafe class ObjectReloader : IDisposable private static int ObjectTableIndex( GameObject actor ) => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; - private static void WriteInvisible( GameObject? actor ) + 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 ) ) { return; } - *ActorDrawState( actor ) |= DrawState.Invisibility; + *ActorDrawState( actor! ) |= DrawState.Invisibility; - if( ObjectTableIndex( actor ) is >= GPosePlayerIdx and < GPoseEndIdx ) + if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx ) { - DisableDraw( actor ); + DisableDraw( actor! ); } } private static void WriteVisible( GameObject? actor ) { - if( actor == null ) + if( BadRedrawIndices( actor, out var tableIndex ) ) { return; } - *ActorDrawState( actor ) &= ~DrawState.Invisibility; + *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - if( ObjectTableIndex( actor ) is >= GPosePlayerIdx and < GPoseEndIdx ) + if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx ) { - EnableDraw( actor ); + EnableDraw( actor! ); } } private void ReloadActor( GameObject? actor ) { - if( actor != null ) + if( BadRedrawIndices( actor, out var tableIndex ) ) { - var idx = ObjectTableIndex( actor ); - if( actor.Address == Dalamud.Targets.Target?.Address ) - { - _target = idx; - } - - _queue.Add( ~idx ); + return; } + + if( actor!.Address == Dalamud.Targets.Target?.Address ) + { + _target = tableIndex; + } + + _queue.Add( ~tableIndex ); } private void ReloadActorAfterGPose( GameObject? actor ) From 1ad7787e4c293613dc292efd4d7534535f13738b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 May 2022 22:08:35 +0200 Subject: [PATCH 0206/2451] Let the glamour plate actor use the players character collection. --- .../Interop/Resolver/PathResolver.Data.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index df6e9933..d6b37dc9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -32,8 +32,9 @@ public unsafe partial class PathResolver var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); if( LastGameObject != null ) { - DrawObjectToObject[ ret ] = (_lastCreatedCollection!, LastGameObject->ObjectIndex ); + DrawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); } + return ret; } @@ -145,20 +146,29 @@ public unsafe partial class PathResolver { var uiModule = ( UIModule* )Dalamud.GameGui.GetUIModule(); var agentModule = uiModule->GetAgentModule(); - var agent = (byte*) agentModule->GetAgentByInternalID( 393 ); + var agent = ( byte* )agentModule->GetAgentByInternalID( 393 ); if( agent == null ) { return null; } - var data = *(byte**) (agent + 0x28); + var data = *( byte** )( agent + 0x28 ); if( data == null ) + { return null; + } var block = data + 0x7A; return new Utf8String( block ).ToString(); } + // Obtain the name of the player character if the glamour plate edit window is open. + private static string? GetGlamourName() + { + var addon = Dalamud.GameGui.GetAddonByName( "MiragePrismMiragePlate", 1 ); + return addon == IntPtr.Zero ? null : GetPlayerName(); + } + // Guesstimate whether an unnamed cutscene actor corresponds to the player or not, // and if so, return the player name. private static string? GetCutsceneName( GameObject* gameObject ) @@ -188,9 +198,9 @@ public unsafe partial class PathResolver var name = gameObject->ObjectIndex switch { - 240 => GetPlayerName(), // character window - 241 => GetInspectName() ?? GetCardName(), // inspect, character card - 242 => GetPlayerName(), // try-on + 240 => GetPlayerName(), // character window + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => GetPlayerName(), // try-on >= 200 => GetCutsceneName( gameObject ), _ => null, } From 06deddcd8a826b0eae3f0cbfe8cb7a93c45985d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 1 Jun 2022 18:04:11 +0200 Subject: [PATCH 0207/2451] Extend the item identification a bit to count unidentifiable items and handle icons, demihumans and monsters. --- Penumbra.GameData/GamePathParser.cs | 472 +++++++------- Penumbra.GameData/ObjectIdentification.cs | 615 +++++++++--------- Penumbra.GameData/Structs/GameObjectInfo.cs | 281 ++++---- Penumbra/Collections/ModCollection.Cache.cs | 6 +- .../Classes/ModFileSystemSelector.Filters.cs | 2 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 4 +- Penumbra/UI/ConfigWindow.Misc.cs | 6 + 7 files changed, 714 insertions(+), 672 deletions(-) diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/GamePathParser.cs index 8afa3d1b..a054ab39 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/GamePathParser.cs @@ -7,34 +7,34 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +internal class GamePathParser : IGamePathParser { - internal class GamePathParser : IGamePathParser - { - private const string CharacterFolder = "chara"; - private const string EquipmentFolder = "equipment"; - private const string PlayerFolder = "human"; - private const string WeaponFolder = "weapon"; - private const string AccessoryFolder = "accessory"; - private const string DemiHumanFolder = "demihuman"; - private const string MonsterFolder = "monster"; - private const string CommonFolder = "common"; - private const string UiFolder = "ui"; - private const string IconFolder = "icon"; - private const string LoadingFolder = "loadingimage"; - private const string MapFolder = "map"; - private const string InterfaceFolder = "uld"; - private const string FontFolder = "font"; - private const string HousingFolder = "hou"; - private const string VfxFolder = "vfx"; - private const string WorldFolder1 = "bgcommon"; - private const string WorldFolder2 = "bg"; + private const string CharacterFolder = "chara"; + private const string EquipmentFolder = "equipment"; + private const string PlayerFolder = "human"; + private const string WeaponFolder = "weapon"; + private const string AccessoryFolder = "accessory"; + private const string DemiHumanFolder = "demihuman"; + private const string MonsterFolder = "monster"; + private const string CommonFolder = "common"; + private const string UiFolder = "ui"; + private const string IconFolder = "icon"; + private const string LoadingFolder = "loadingimage"; + private const string MapFolder = "map"; + private const string InterfaceFolder = "uld"; + private const string FontFolder = "font"; + private const string HousingFolder = "hou"; + private const string VfxFolder = "vfx"; + private const string WorldFolder1 = "bgcommon"; + private const string WorldFolder2 = "bg"; // @formatter:off private readonly Dictionary> _regexes = new() { { FileType.Font, new Dictionary< ObjectType, Regex[] >(){ { ObjectType.Font, new Regex[]{ new(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt") } } } } , { FileType.Texture, new Dictionary< ObjectType, Regex[] >() - { { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)\.tex") } } + { { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex") } } , { ObjectType.Map, new Regex[]{ new(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex") } } , { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex") } } , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex") } } @@ -58,7 +58,7 @@ namespace Penumbra.GameData , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } - , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl") } } } } + , { ObjectType.Character, new Regex[]{ new( @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl" ) } } } } , { FileType.Imc, new Dictionary< ObjectType, Regex[] >() { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc") } } , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } @@ -66,267 +66,267 @@ namespace Penumbra.GameData , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } } , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } }, }; - // @formatter:on + // @formatter:on - public ObjectType PathToObjectType( GamePath path ) + public ObjectType PathToObjectType( GamePath path ) + { + if( path.Empty ) { - if( path.Empty ) - { - return ObjectType.Unknown; - } - - string p = path; - var folders = p.Split( '/' ); - if( folders.Length < 2 ) - { - return ObjectType.Unknown; - } - - return folders[ 0 ] switch - { - CharacterFolder => folders[ 1 ] switch - { - EquipmentFolder => ObjectType.Equipment, - AccessoryFolder => ObjectType.Accessory, - WeaponFolder => ObjectType.Weapon, - PlayerFolder => ObjectType.Character, - DemiHumanFolder => ObjectType.DemiHuman, - MonsterFolder => ObjectType.Monster, - CommonFolder => ObjectType.Character, - _ => ObjectType.Unknown, - }, - UiFolder => folders[ 1 ] switch - { - IconFolder => ObjectType.Icon, - LoadingFolder => ObjectType.LoadingScreen, - MapFolder => ObjectType.Map, - InterfaceFolder => ObjectType.Interface, - _ => ObjectType.Unknown, - }, - CommonFolder => folders[ 1 ] switch - { - FontFolder => ObjectType.Font, - _ => ObjectType.Unknown, - }, - HousingFolder => ObjectType.Housing, - WorldFolder1 => folders[ 1 ] switch - { - HousingFolder => ObjectType.Housing, - _ => ObjectType.World, - }, - WorldFolder2 => ObjectType.World, - VfxFolder => ObjectType.Vfx, - _ => ObjectType.Unknown, - }; + return ObjectType.Unknown; } - private (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + string p = path; + var folders = p.Split( '/' ); + if( folders.Length < 2 ) { - if( !Names.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) ) + return ObjectType.Unknown; + } + + return folders[ 0 ] switch + { + CharacterFolder => folders[ 1 ] switch { - fileType = FileType.Unknown; - } - - var objectType = PathToObjectType( path ); - - if( !_regexes.TryGetValue( fileType, out var objectDict ) ) + EquipmentFolder => ObjectType.Equipment, + AccessoryFolder => ObjectType.Accessory, + WeaponFolder => ObjectType.Weapon, + PlayerFolder => ObjectType.Character, + DemiHumanFolder => ObjectType.DemiHuman, + MonsterFolder => ObjectType.Monster, + CommonFolder => ObjectType.Character, + _ => ObjectType.Unknown, + }, + UiFolder => folders[ 1 ] switch { - return ( fileType, objectType, null ); - } - - if( !objectDict.TryGetValue( objectType, out var regexes ) ) + IconFolder => ObjectType.Icon, + LoadingFolder => ObjectType.LoadingScreen, + MapFolder => ObjectType.Map, + InterfaceFolder => ObjectType.Interface, + _ => ObjectType.Unknown, + }, + CommonFolder => folders[ 1 ] switch { - return ( fileType, objectType, null ); - } - - foreach( var regex in regexes ) + FontFolder => ObjectType.Font, + _ => ObjectType.Unknown, + }, + HousingFolder => ObjectType.Housing, + WorldFolder1 => folders[ 1 ] switch { - var match = regex.Match( path ); - if( match.Success ) - { - return ( fileType, objectType, match ); - } - } + HousingFolder => ObjectType.Housing, + _ => ObjectType.World, + }, + WorldFolder2 => ObjectType.World, + VfxFolder => ObjectType.Vfx, + _ => ObjectType.Unknown, + }; + } + private (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + { + if( !Names.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) ) + { + fileType = FileType.Unknown; + } + + var objectType = PathToObjectType( path ); + + if( !_regexes.TryGetValue( fileType, out var objectDict ) ) + { return ( fileType, objectType, null ); } - private static string Extension( string filename ) + if( !objectDict.TryGetValue( objectType, out var regexes ) ) { - var extIdx = filename.LastIndexOf( '.' ); - return extIdx < 0 ? "" : filename.Substring( extIdx ); + return ( fileType, objectType, null ); } - private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups ) + foreach( var regex in regexes ) { - var setId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc ) + var match = regex.Match( path ); + if( match.Success ) { - return GameObjectInfo.Equipment( fileType, setId ); + return ( fileType, objectType, match ); } - - var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; - if( fileType == FileType.Model ) - { - return GameObjectInfo.Equipment( fileType, setId, gr, slot ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant ); } - private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups ) - { - var weaponId = ushort.Parse( groups[ "weapon" ].Value ); - var setId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc || fileType == FileType.Model ) - { - return GameObjectInfo.Weapon( fileType, setId, weaponId ); - } + return ( fileType, objectType, null ); + } - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + private static string Extension( string filename ) + { + var extIdx = filename.LastIndexOf( '.' ); + return extIdx < 0 ? "" : filename.Substring( extIdx ); + } + + private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups ) + { + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.Equipment( fileType, setId ); } - private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups ) + var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); + var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) { - var monsterId = ushort.Parse( groups[ "monster" ].Value ); - var bodyId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc || fileType == FileType.Model ) - { - return GameObjectInfo.Monster( fileType, monsterId, bodyId ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant ); + return GameObjectInfo.Equipment( fileType, setId, gr, slot ); } - private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant ); + } + + private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups ) + { + var weaponId = ushort.Parse( groups[ "weapon" ].Value ); + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) { - var demiHumanId = ushort.Parse( groups[ "id" ].Value ); - var equipId = ushort.Parse( groups[ "equip" ].Value ); - if( fileType == FileType.Imc ) - { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); - } - - var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; - if( fileType == FileType.Model ) - { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); + return GameObjectInfo.Weapon( fileType, setId, weaponId ); } - private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + } + + private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups ) + { + var monsterId = ushort.Parse( groups[ "monster" ].Value ); + var bodyId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) { - if( groups[ "skin" ].Success ) - { - return GameObjectInfo.Customization( fileType, CustomizationType.Skin ); - } - - var id = ushort.Parse( groups[ "id" ].Value ); - if( groups[ "location" ].Success ) - { - var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace - : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; - return GameObjectInfo.Customization( fileType, tmpType, id ); - } - - var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ]; - var type = groups[ "slot" ].Success - ? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ] - : CustomizationType.Skin; - if( fileType == FileType.Material ) - { - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant ); - } - - return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot ); + return GameObjectInfo.Monster( fileType, monsterId, bodyId ); } - private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups ) - { - var hq = groups[ "hq" ].Success; - var id = uint.Parse( groups[ "id" ].Value ); - if( !groups[ "lang" ].Success ) - { - return GameObjectInfo.Icon( fileType, id, hq ); - } + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant ); + } - var language = groups[ "lang" ].Value switch - { - "en" => Dalamud.ClientLanguage.English, - "ja" => Dalamud.ClientLanguage.Japanese, - "de" => Dalamud.ClientLanguage.German, - "fr" => Dalamud.ClientLanguage.French, - _ => Dalamud.ClientLanguage.English, - }; - return GameObjectInfo.Icon( fileType, id, hq, language ); + private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups ) + { + var demiHumanId = ushort.Parse( groups[ "id" ].Value ); + var equipId = ushort.Parse( groups[ "equip" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); } - private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups ) + var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) { - var map = Encoding.ASCII.GetBytes( groups[ "id" ].Value ); - var variant = byte.Parse( groups[ "variant" ].Value ); - if( groups[ "suffix" ].Success ) - { - var suffix = Encoding.ASCII.GetBytes( groups[ "suffix" ].Value )[ 0 ]; - return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant, suffix ); - } - - return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); } - public GameObjectInfo GetFileInfo( GamePath path ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); + } + + private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups ) + { + if( groups[ "skin" ].Success ) { - var (fileType, objectType, match) = ParseGamePath( path ); - if( match == null || !match.Success ) - { - return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; - } + return GameObjectInfo.Customization( fileType, CustomizationType.Skin ); + } - try - { - var groups = match.Groups; - switch( objectType ) - { - case ObjectType.Accessory: return HandleEquipment( fileType, groups ); - case ObjectType.Equipment: return HandleEquipment( fileType, groups ); - case ObjectType.Weapon: return HandleWeapon( fileType, groups ); - case ObjectType.Map: return HandleMap( fileType, groups ); - case ObjectType.Monster: return HandleMonster( fileType, groups ); - case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups ); - case ObjectType.Character: return HandleCustomization( fileType, groups ); - case ObjectType.Icon: return HandleIcon( fileType, groups ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse {path}:\n{e}" ); - } + var id = ushort.Parse( groups[ "id" ].Value ); + if( groups[ "location" ].Success ) + { + var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace + : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; + return GameObjectInfo.Customization( fileType, tmpType, id ); + } + var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); + var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ]; + var type = groups[ "slot" ].Success + ? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ] + : CustomizationType.Skin; + if( fileType == FileType.Material ) + { + var variant = groups[ "variant" ].Success ? byte.Parse( groups[ "variant" ].Value ) : ( byte )0; + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant ); + } + + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot ); + } + + private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups ) + { + var hq = groups[ "hq" ].Success; + var hr = groups[ "hr" ].Success; + var id = uint.Parse( groups[ "id" ].Value ); + if( !groups[ "lang" ].Success ) + { + return GameObjectInfo.Icon( fileType, id, hq, hr ); + } + + var language = groups[ "lang" ].Value switch + { + "en" => Dalamud.ClientLanguage.English, + "ja" => Dalamud.ClientLanguage.Japanese, + "de" => Dalamud.ClientLanguage.German, + "fr" => Dalamud.ClientLanguage.French, + _ => Dalamud.ClientLanguage.English, + }; + return GameObjectInfo.Icon( fileType, id, hq, hr, language ); + } + + private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups ) + { + var map = Encoding.ASCII.GetBytes( groups[ "id" ].Value ); + var variant = byte.Parse( groups[ "variant" ].Value ); + if( groups[ "suffix" ].Success ) + { + var suffix = Encoding.ASCII.GetBytes( groups[ "suffix" ].Value )[ 0 ]; + return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant, suffix ); + } + + return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant ); + } + + public GameObjectInfo GetFileInfo( GamePath path ) + { + var (fileType, objectType, match) = ParseGamePath( path ); + if( match == null || !match.Success ) + { return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; } - private readonly Regex _vfxRegexTmb = new( @"chara/action/(?'key'[^\s]+?)\.tmb" ); - private readonly Regex _vfxRegexPap = new( @"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap" ); - - public string VfxToKey( GamePath path ) + try { - var match = _vfxRegexTmb.Match( path ); - if( match.Success ) + var groups = match.Groups; + switch( objectType ) { - return match.Groups[ "key" ].Value.ToLowerInvariant(); + case ObjectType.Accessory: return HandleEquipment( fileType, groups ); + case ObjectType.Equipment: return HandleEquipment( fileType, groups ); + case ObjectType.Weapon: return HandleWeapon( fileType, groups ); + case ObjectType.Map: return HandleMap( fileType, groups ); + case ObjectType.Monster: return HandleMonster( fileType, groups ); + case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups ); + case ObjectType.Character: return HandleCustomization( fileType, groups ); + case ObjectType.Icon: return HandleIcon( fileType, groups ); } - - match = _vfxRegexPap.Match( path ); - return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse {path}:\n{e}" ); + } + + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + + private readonly Regex _vfxRegexTmb = new(@"chara/action/(?'key'[^\s]+?)\.tmb"); + private readonly Regex _vfxRegexPap = new(@"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap"); + + public string VfxToKey( GamePath path ) + { + var match = _vfxRegexTmb.Match( path ); + if( match.Success ) + { + return match.Groups[ "key" ].Value.ToLowerInvariant(); + } + + match = _vfxRegexPap.Match( path ); + return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; } } \ No newline at end of file diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 93e69b1d..e25e1ca2 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -1,324 +1,355 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using Action = Lumina.Excel.GeneratedSheets.Action; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +internal class ObjectIdentification : IObjectIdentifier { - internal class ObjectIdentification : IObjectIdentifier + public static DataManager? DataManager = null!; + private readonly List< (ulong, HashSet< Item >) > _weapons; + private readonly List< (ulong, HashSet< Item >) > _equipment; + private readonly Dictionary< string, HashSet< Action > > _actions; + + private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item ) { - public static DataManager? DataManager = null!; - private readonly List< (ulong, HashSet< Item >) > _weapons; - private readonly List< (ulong, HashSet< Item >) > _equipment; - private readonly Dictionary< string, HashSet< Action > > _actions; - - private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item ) + if( dict.TryGetValue( key, out var list ) ) { - if( dict.TryGetValue( key, out var list ) ) - { - return list.Add( item ); - } - - dict[ key ] = new HashSet< Item > { item }; - return true; + return list.Add( item ); } - private static ulong EquipmentKey( Item i ) + dict[ key ] = new HashSet< Item > { item }; + return true; + } + + private static ulong EquipmentKey( Item i ) + { + var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A; + var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B; + var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); + return ( model << 32 ) | ( slot << 16 ) | variant; + } + + private static ulong WeaponKey( Item i, bool offhand ) + { + var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain; + var model = ( ulong )quad.A; + var type = ( ulong )quad.B; + var variant = ( ulong )quad.C; + + return ( model << 32 ) | ( type << 16 ) | variant; + } + + private void AddAction( string key, Action action ) + { + if( key.Length == 0 ) { - var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A; - var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B; - var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - return ( model << 32 ) | ( slot << 16 ) | variant; + return; } - private static ulong WeaponKey( Item i, bool offhand ) + key = key.ToLowerInvariant(); + if( _actions.TryGetValue( key, out var actions ) ) { - var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain; - var model = ( ulong )quad.A; - var type = ( ulong )quad.B; - var variant = ( ulong )quad.C; - - return ( model << 32 ) | ( type << 16 ) | variant; + actions.Add( action ); } - - private void AddAction( string key, Action action ) + else { - if( key.Length == 0 ) - { - return; - } - - key = key.ToLowerInvariant(); - if( _actions.TryGetValue( key, out var actions ) ) - { - actions.Add( action ); - } - else - { - _actions[ key ] = new HashSet< Action > { action }; - } + _actions[ key ] = new HashSet< Action > { action }; } + } - public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage ) + public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage ) + { + DataManager = dataManager; + var items = dataManager.GetExcelSheet< Item >( clientLanguage )!; + SortedList< ulong, HashSet< Item > > weapons = new(); + SortedList< ulong, HashSet< Item > > equipment = new(); + foreach( var item in items ) { - DataManager = dataManager; - var items = dataManager.GetExcelSheet< Item >( clientLanguage )!; - SortedList< ulong, HashSet< Item > > weapons = new(); - SortedList< ulong, HashSet< Item > > equipment = new(); - foreach( var item in items ) - { - switch( ( EquipSlot )item.EquipSlotCategory.Row ) - { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - case EquipSlot.BothHand: - if( item.ModelMain != 0 ) - { - Add( weapons, WeaponKey( item, false ), item ); - } - - if( item.ModelSub != 0 ) - { - Add( weapons, WeaponKey( item, true ), item ); - } - - break; - // Accessories - case EquipSlot.RFinger: - case EquipSlot.Wrists: - case EquipSlot.Ears: - case EquipSlot.Neck: - Add( equipment, EquipmentKey( item ), item ); - break; - // Equipment - case EquipSlot.Head: - case EquipSlot.Body: - case EquipSlot.Hands: - case EquipSlot.Legs: - case EquipSlot.Feet: - case EquipSlot.BodyHands: - case EquipSlot.BodyHandsLegsFeet: - case EquipSlot.BodyLegsFeet: - case EquipSlot.FullBody: - case EquipSlot.HeadBody: - case EquipSlot.LegsFeet: - Add( equipment, EquipmentKey( item ), item ); - break; - default: continue; - } - } - - _actions = new Dictionary< string, HashSet< Action > >(); - foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )! - .Where( a => a.Name.ToString().Any() ) ) - { - var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty; - var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty; - var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty; - AddAction( startKey, action ); - AddAction( endKey, action ); - AddAction( hitKey, action ); - } - - _weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); - _equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); - } - - private class Comparer : IComparer< (ulong, HashSet< Item >) > - { - public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y ) - => x.Item1.CompareTo( y.Item1 ); - } - - private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask ) - { - var maskedKey = key & mask; - var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() ); - if( idx < 0 ) - { - if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) ) - { - return ( -1, -1 ); - } - - idx = ~idx; - } - - var endIdx = idx + 1; - while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) ) - { - ++endIdx; - } - - return ( idx, endIdx ); - } - - private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info ) - { - var key = ( ulong )info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if( info.EquipSlot != EquipSlot.Unknown ) - { - key |= ( ulong )info.EquipSlot.ToSlot() << 16; - mask |= 0xFFFF0000; - } - - if( info.Variant != 0 ) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange( _equipment, key, mask ); - if( start == -1 ) - { - return; - } - - for( ; start < end; ++start ) - { - foreach( var item in _equipment[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } - } - } - - private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info ) - { - var key = ( ulong )info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if( info.SecondaryId != 0 ) - { - key |= ( ulong )info.SecondaryId << 16; - mask |= 0xFFFF0000; - } - - if( info.Variant != 0 ) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange( _weapons, key, mask ); - if( start == -1 ) - { - return; - } - - for( ; start < end; ++start ) - { - foreach( var item in _weapons[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } - } - } - - - private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info ) - { - switch( info.ObjectType ) - { - case ObjectType.Unknown: - case ObjectType.LoadingScreen: - case ObjectType.Map: - case ObjectType.Interface: - case ObjectType.Vfx: - case ObjectType.World: - case ObjectType.Housing: - case ObjectType.DemiHuman: - case ObjectType.Monster: - case ObjectType.Icon: - case ObjectType.Font: - // Don't do anything for these cases. - break; - case ObjectType.Accessory: - case ObjectType.Equipment: - FindEquipment( set, info ); - break; - case ObjectType.Weapon: - FindWeapon( set, info ); - break; - case ObjectType.Character: - var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; - var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; - if( info.CustomizationType == CustomizationType.Skin ) - { - set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; - } - else - { - var customizationString = - $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; - set[ customizationString ] = null; - } - - break; - - default: throw new InvalidEnumArgumentException(); - } - } - - private void IdentifyVfx( IDictionary< string, object? > set, GamePath path ) - { - var key = GameData.GamePathParser.VfxToKey( path ); - if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) ) - { - return; - } - - foreach( var action in actions ) - { - set[ $"Action: {action.Name}" ] = action; - } - } - - public void Identify( IDictionary< string, object? > set, GamePath path ) - { - if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) ) - { - IdentifyVfx( set, path ); - } - else - { - var info = GameData.GamePathParser.GetFileInfo( path ); - IdentifyParsed( set, info ); - } - } - - public Dictionary< string, object? > Identify( GamePath path ) - { - Dictionary< string, object? > ret = new(); - Identify( ret, path ); - return ret; - } - - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ) - { - switch( slot ) + switch( ( EquipSlot )item.EquipSlotCategory.Row ) { case EquipSlot.MainHand: case EquipSlot.OffHand: + case EquipSlot.BothHand: + if( item.ModelMain != 0 ) + { + Add( weapons, WeaponKey( item, false ), item ); + } + + if( item.ModelSub != 0 ) + { + Add( weapons, WeaponKey( item, true ), item ); + } + + break; + // Accessories + case EquipSlot.RFinger: + case EquipSlot.Wrists: + case EquipSlot.Ears: + case EquipSlot.Neck: + Add( equipment, EquipmentKey( item ), item ); + break; + // Equipment + case EquipSlot.Head: + case EquipSlot.Body: + case EquipSlot.Hands: + case EquipSlot.Legs: + case EquipSlot.Feet: + case EquipSlot.BodyHands: + case EquipSlot.BodyHandsLegsFeet: + case EquipSlot.BodyLegsFeet: + case EquipSlot.FullBody: + case EquipSlot.HeadBody: + case EquipSlot.LegsFeet: + Add( equipment, EquipmentKey( item ), item ); + break; + default: continue; + } + } + + _actions = new Dictionary< string, HashSet< Action > >(); + foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )! + .Where( a => a.Name.ToString().Any() ) ) + { + var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty; + var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty; + var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty; + AddAction( startKey, action ); + AddAction( endKey, action ); + AddAction( hitKey, action ); + } + + _weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); + _equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); + } + + private class Comparer : IComparer< (ulong, HashSet< Item >) > + { + public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y ) + => x.Item1.CompareTo( y.Item1 ); + } + + private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask ) + { + var maskedKey = key & mask; + var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() ); + if( idx < 0 ) + { + if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) ) + { + return ( -1, -1 ); + } + + idx = ~idx; + } + + var endIdx = idx + 1; + while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) ) + { + ++endIdx; + } + + return ( idx, endIdx ); + } + + private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info ) + { + var key = ( ulong )info.PrimaryId << 32; + var mask = 0xFFFF00000000ul; + if( info.EquipSlot != EquipSlot.Unknown ) + { + key |= ( ulong )info.EquipSlot.ToSlot() << 16; + mask |= 0xFFFF0000; + } + + if( info.Variant != 0 ) + { + key |= info.Variant; + mask |= 0xFFFF; + } + + var (start, end) = FindIndexRange( _equipment, key, mask ); + if( start == -1 ) + { + return; + } + + for( ; start < end; ++start ) + { + foreach( var item in _equipment[ start ].Item2 ) + { + set[ item.Name.ToString() ] = item; + } + } + } + + private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info ) + { + var key = ( ulong )info.PrimaryId << 32; + var mask = 0xFFFF00000000ul; + if( info.SecondaryId != 0 ) + { + key |= ( ulong )info.SecondaryId << 16; + mask |= 0xFFFF0000; + } + + if( info.Variant != 0 ) + { + key |= info.Variant; + mask |= 0xFFFF; + } + + var (start, end) = FindIndexRange( _weapons, key, mask ); + if( start == -1 ) + { + return; + } + + for( ; start < end; ++start ) + { + foreach( var item in _weapons[ start ].Item2 ) + { + set[ item.Name.ToString() ] = item; + } + } + } + + private static void AddCounterString( IDictionary< string, object? > set, string data ) + { + if( set.TryGetValue( data, out var obj ) && obj is int counter ) + { + set[ data ] = counter + 1; + } + else + { + set[ data ] = 1; + } + } + + private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info ) + { + switch( info.ObjectType ) + { + case ObjectType.Unknown: + switch( info.FileType ) { - var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null; + case FileType.Sound: + AddCounterString( set, FileType.Sound.ToString() ); + break; + case FileType.Animation: + case FileType.Pap: + AddCounterString( set, FileType.Animation.ToString() ); + break; + case FileType.Shader: + AddCounterString( set, FileType.Shader.ToString() ); + break; } - default: + + break; + case ObjectType.LoadingScreen: + case ObjectType.Map: + case ObjectType.Interface: + case ObjectType.Vfx: + case ObjectType.World: + case ObjectType.Housing: + case ObjectType.Font: + AddCounterString( set, info.ObjectType.ToString() ); + break; + case ObjectType.DemiHuman: + set[ $"Demi Human: {info.PrimaryId}" ] = null; + break; + case ObjectType.Monster: + set[ $"Monster: {info.PrimaryId}" ] = null; + break; + case ObjectType.Icon: + set[ $"Icon: {info.IconId}" ] = null; + break; + case ObjectType.Accessory: + case ObjectType.Equipment: + FindEquipment( set, info ); + break; + case ObjectType.Weapon: + FindWeapon( set, info ); + break; + case ObjectType.Character: + var (gender, race) = info.GenderRace.Split(); + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; + if( info.CustomizationType == CustomizationType.Skin ) { - var (begin, _) = FindIndexRange( _equipment, - ( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null; + set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; } + else + { + var customizationString = + $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; + set[ customizationString ] = null; + } + + break; + + default: throw new InvalidEnumArgumentException(); + } + } + + private void IdentifyVfx( IDictionary< string, object? > set, GamePath path ) + { + var key = GameData.GamePathParser.VfxToKey( path ); + if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) ) + { + return; + } + + foreach( var action in actions ) + { + set[ $"Action: {action.Name}" ] = action; + } + } + + public void Identify( IDictionary< string, object? > set, GamePath path ) + { + if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) ) + { + IdentifyVfx( set, path ); + } + else + { + var info = GameData.GamePathParser.GetFileInfo( path ); + IdentifyParsed( set, info ); + } + } + + public Dictionary< string, object? > Identify( GamePath path ) + { + Dictionary< string, object? > ret = new(); + Identify( ret, path ); + return ret; + } + + public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ) + { + switch( slot ) + { + case EquipSlot.MainHand: + case EquipSlot.OffHand: + { + var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant, + 0xFFFFFFFFFFFF ); + return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null; + } + default: + { + var (begin, _) = FindIndexRange( _equipment, + ( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant, + 0xFFFFFFFFFFFF ); + return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null; } } } diff --git a/Penumbra.GameData/Structs/GameObjectInfo.cs b/Penumbra.GameData/Structs/GameObjectInfo.cs index 80b6c792..fae17494 100644 --- a/Penumbra.GameData/Structs/GameObjectInfo.cs +++ b/Penumbra.GameData/Structs/GameObjectInfo.cs @@ -3,158 +3,157 @@ using System.Runtime.InteropServices; using Dalamud; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[StructLayout( LayoutKind.Explicit )] +public struct GameObjectInfo : IComparable { - [StructLayout( LayoutKind.Explicit )] - public struct GameObjectInfo : IComparable - { - public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown - , EquipSlot slot = EquipSlot.Unknown, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, - PrimaryId = setId, - GenderRace = gr, - Variant = variant, - EquipSlot = slot, - }; - - public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Weapon, - PrimaryId = setId, - SecondaryId = weaponId, - Variant = variant, - }; - - public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 - , GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Character, - PrimaryId = id, - GenderRace = gr, - BodySlot = bodySlot, - Variant = variant, - CustomizationType = customizationType, - }; - - public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Monster, - PrimaryId = monsterId, - SecondaryId = bodyId, - Variant = variant, - }; - - public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown, - byte variant = 0 - ) - => new() - { - FileType = type, - ObjectType = ObjectType.DemiHuman, - PrimaryId = demiHumanId, - SecondaryId = bodyId, - Variant = variant, - EquipSlot = slot, - }; - - public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Map, - MapC1 = c1, - MapC2 = c2, - MapC3 = c3, - MapC4 = c4, - MapSuffix = suffix, - Variant = variant, - }; - - public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, ClientLanguage lang = ClientLanguage.English ) - => new() - { - FileType = type, - ObjectType = ObjectType.Map, - IconId = iconId, - IconHq = hq, - Language = lang, - }; - - - [FieldOffset( 0 )] - public readonly ulong Identifier; - - [FieldOffset( 0 )] - public FileType FileType; - - [FieldOffset( 1 )] - public ObjectType ObjectType; - - - [FieldOffset( 2 )] - public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman - - [FieldOffset( 2 )] - public uint IconId; // Icon - - [FieldOffset( 2 )] - public byte MapC1; // Map - - [FieldOffset( 3 )] - public byte MapC2; // Map - - [FieldOffset( 4 )] - public ushort SecondaryId; // Weapon, Monster, Demihuman - - [FieldOffset( 4 )] - public byte MapC3; // Map - - [FieldOffset( 4 )] - private byte _genderRaceByte; // Equipment, Customization - - public GenderRace GenderRace + public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown + , EquipSlot slot = EquipSlot.Unknown, byte variant = 0 ) + => new() { - get => Names.GenderRaceFromByte( _genderRaceByte ); - set => _genderRaceByte = value.ToByte(); - } + FileType = type, + ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, + PrimaryId = setId, + GenderRace = gr, + Variant = variant, + EquipSlot = slot, + }; - [FieldOffset( 5 )] - public BodySlot BodySlot; // Customization + public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Weapon, + PrimaryId = setId, + SecondaryId = weaponId, + Variant = variant, + }; - [FieldOffset( 5 )] - public byte MapC4; // Map + public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 + , GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Character, + PrimaryId = id, + GenderRace = gr, + BodySlot = bodySlot, + Variant = variant, + CustomizationType = customizationType, + }; - [FieldOffset( 6 )] - public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman + public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Monster, + PrimaryId = monsterId, + SecondaryId = bodyId, + Variant = variant, + }; - [FieldOffset( 6 )] - public bool IconHq; // Icon + public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown, + byte variant = 0 + ) + => new() + { + FileType = type, + ObjectType = ObjectType.DemiHuman, + PrimaryId = demiHumanId, + SecondaryId = bodyId, + Variant = variant, + EquipSlot = slot, + }; - [FieldOffset( 7 )] - public EquipSlot EquipSlot; // Equipment, Demihuman + public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Map, + MapC1 = c1, + MapC2 = c2, + MapC3 = c3, + MapC4 = c4, + MapSuffix = suffix, + Variant = variant, + }; - [FieldOffset( 7 )] - public CustomizationType CustomizationType; // Customization + public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, bool hr, ClientLanguage lang = ClientLanguage.English ) + => new() + { + FileType = type, + ObjectType = ObjectType.Icon, + IconId = iconId, + IconHqHr = ( byte )( hq ? hr ? 3 : 1 : hr ? 2 : 0 ), + Language = lang, + }; - [FieldOffset( 7 )] - public ClientLanguage Language; // Icon - [FieldOffset( 7 )] - public byte MapSuffix; + [FieldOffset( 0 )] + public readonly ulong Identifier; - public override int GetHashCode() - => Identifier.GetHashCode(); + [FieldOffset( 0 )] + public FileType FileType; - public int CompareTo( object? r ) - => Identifier.CompareTo( r ); + [FieldOffset( 1 )] + public ObjectType ObjectType; + + + [FieldOffset( 2 )] + public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman + + [FieldOffset( 2 )] + public uint IconId; // Icon + + [FieldOffset( 2 )] + public byte MapC1; // Map + + [FieldOffset( 3 )] + public byte MapC2; // Map + + [FieldOffset( 4 )] + public ushort SecondaryId; // Weapon, Monster, Demihuman + + [FieldOffset( 4 )] + public byte MapC3; // Map + + [FieldOffset( 4 )] + private byte _genderRaceByte; // Equipment, Customization + + public GenderRace GenderRace + { + get => Names.GenderRaceFromByte( _genderRaceByte ); + set => _genderRaceByte = value.ToByte(); } + + [FieldOffset( 5 )] + public BodySlot BodySlot; // Customization + + [FieldOffset( 5 )] + public byte MapC4; // Map + + [FieldOffset( 6 )] + public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman + + [FieldOffset( 6 )] + public byte IconHqHr; // Icon + + [FieldOffset( 7 )] + public EquipSlot EquipSlot; // Equipment, Demihuman + + [FieldOffset( 7 )] + public CustomizationType CustomizationType; // Customization + + [FieldOffset( 7 )] + public ClientLanguage Language; // Icon + + [FieldOffset( 7 )] + public byte MapSuffix; + + public override int GetHashCode() + => Identifier.GetHashCode(); + + public int CompareTo( object? r ) + => Identifier.CompareTo( r ); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 44df1ae2..31cd55a3 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -437,7 +437,11 @@ public partial class ModCollection } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj ); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + } + else if (obj is int x && data.Item2 is int y) + { + _changedItems[name] = (data.Item1, x + y); } } } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index cc10eb12..25da4f4f 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -96,7 +96,7 @@ public partial class ModFileSystemSelector 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), 1 => !mod.Name.Contains( _modFilter ), 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter ), + 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower, IgnoreCase ), _ => false, // Should never happen }; } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 96253526..596a48f0 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -24,7 +24,9 @@ public partial class ConfigWindow { // Functions in here for less pollution. bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) - => ( _changedItemFilter.IsEmpty || item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) + => ( _changedItemFilter.IsEmpty + || ChangedItemName( item.Key, item.Value.Item2 ) + .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 995ad308..d9ab33e9 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -28,10 +28,16 @@ public partial class ConfigWindow private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); + + // Apply Changed Item Counters to the Name if necessary. + private static string ChangedItemName( string name, object? data ) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + // Draw a changed item, invoking the Api-Events for clicks and tooltips. // Also draw the item Id in grey if requested private void DrawChangedItem( string name, object? data, bool drawId ) { + name = ChangedItemName( name, data ); var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; From 385ce4c7e95dafd47817fdf21fb2c59dfec30843 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Jun 2022 13:04:00 +0200 Subject: [PATCH 0208/2451] Add file redirection editing. --- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 50 ++-- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 32 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 281 ++++++++++++++---- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 8 +- Penumbra/Mods/Editor/Mod.Editor.cs | 13 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 276 +++++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 92 +----- Penumbra/UI/ConfigWindow.Misc.cs | 6 + 8 files changed, 569 insertions(+), 189 deletions(-) create mode 100644 Penumbra/UI/Classes/ModEditWindow.Files.cs diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 3f217fab..4722c353 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -14,8 +14,8 @@ public partial class Mod { public partial class Editor { - private readonly SHA256 _hasher = SHA256.Create(); - private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); + private readonly SHA256 _hasher = SHA256.Create(); + private readonly List< (FullPath[] Paths, long Size, byte[] Hash) > _duplicates = new(); public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates => _duplicates; @@ -44,7 +44,8 @@ public partial class Mod HandleDuplicate( duplicate, remaining ); } } - _availableFiles.RemoveAll( p => !p.Item1.Exists ); + + _availableFiles.RemoveAll( p => !p.File.Exists ); _duplicates.Clear(); } @@ -82,7 +83,7 @@ public partial class Mod } changes = true; - PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to); + PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to ); return to; } @@ -92,26 +93,28 @@ public partial class Mod if( DuplicatesFinished ) { DuplicatesFinished = false; - Task.Run( CheckDuplicates ); + UpdateFiles(); + var files = _availableFiles.OrderByDescending(f => f.FileSize).ToArray(); + Task.Run( () => CheckDuplicates( files ) ); } } - private void CheckDuplicates() + private void CheckDuplicates( IReadOnlyList< FileRegistry > files ) { _duplicates.Clear(); SavedSpace = 0; var list = new List< FullPath >(); var lastSize = -1L; - foreach( var (p, size) in AvailableFiles ) + foreach( var file in files ) { if( DuplicatesFinished ) { return; } - if( size == lastSize ) + if( file.FileSize == lastSize ) { - list.Add( p ); + list.Add( file.File ); continue; } @@ -119,22 +122,25 @@ public partial class Mod { CheckMultiDuplicates( list, lastSize ); } - lastSize = size; + + lastSize = file.FileSize; list.Clear(); - list.Add( p ); + list.Add( file.File ); } + if( list.Count >= 2 ) { CheckMultiDuplicates( list, lastSize ); } + _duplicates.Sort( ( a, b ) => a.Size != b.Size ? b.Size.CompareTo( a.Size ) : a.Paths[ 0 ].CompareTo( b.Paths[ 0 ] ) ); DuplicatesFinished = true; } private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size ) { - var hashes = list.Select( f => (f, ComputeHash(f)) ).ToList(); + var hashes = list.Select( f => ( f, ComputeHash( f ) ) ).ToList(); while( hashes.Count > 0 ) { if( DuplicatesFinished ) @@ -157,10 +163,10 @@ public partial class Mod } } - hashes.RemoveAll( p => set.Contains(p.Item1) ); + hashes.RemoveAll( p => set.Contains( p.Item1 ) ); if( set.Count > 1 ) { - _duplicates.Add( (set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2) ); + _duplicates.Add( ( set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2 ) ); SavedSpace += ( set.Count - 1 ) * size; } } @@ -169,27 +175,35 @@ public partial class Mod private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 ) { if( !f1.Exists || !f2.Exists ) + { return false; + } using var s1 = File.OpenRead( f1.FullName ); using var s2 = File.OpenRead( f2.FullName ); var buffer1 = stackalloc byte[256]; var buffer2 = stackalloc byte[256]; - var span1 = new Span< byte >( buffer1, 256 ); - var span2 = new Span< byte >( buffer2, 256 ); + var span1 = new Span< byte >( buffer1, 256 ); + var span2 = new Span< byte >( buffer2, 256 ); while( true ) { - var bytes1 = s1.Read( span1 ); - var bytes2 = s2.Read( span2 ); + var bytes1 = s1.Read( span1 ); + var bytes2 = s2.Read( span2 ); if( bytes1 != bytes2 ) + { return false; + } if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) ) + { return false; + } if( bytes1 < 256 ) + { return true; + } } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 35d41003..1e702b67 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -18,38 +18,52 @@ public partial class Mod public ISubMod CurrentOption => _subMod; - public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); - public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); + public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); public void SetSubMod( int groupIdx, int optionIdx ) { GroupIdx = groupIdx; OptionIdx = optionIdx; - if( groupIdx >= 0 ) + if( groupIdx >= 0 && groupIdx < _mod.Groups.Count && optionIdx >= 0 && optionIdx < _mod.Groups[ groupIdx ].Count ) { _modGroup = _mod.Groups[ groupIdx ]; _subMod = ( SubMod )_modGroup![ optionIdx ]; } else { + GroupIdx = -1; + OptionIdx = 0; _modGroup = null; _subMod = _mod._default; } - RevertFiles(); + UpdateFiles(); RevertSwaps(); RevertManipulations(); } - public void ApplyFiles() + public int ApplyFiles() { - Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, CurrentFiles.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + var dict = new Dictionary< Utf8GamePath, FullPath >(); + var num = 0; + foreach( var file in _availableFiles ) + { + foreach( var path in file.SubModUsage.Where( p => p.Item1 == CurrentOption ) ) + { + num += dict.TryAdd( path.Item2, file.File ) ? 0 : 1; + } + } + + Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, dict ); + if( num > 0 ) + RevertFiles(); + else + FileChanges = false; + return num; } public void RevertFiles() - { - CurrentFiles.SetTo( _subMod.Files ); - } + => UpdateFiles(); public void ApplySwaps() { diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index e823be02..5d56c871 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; @@ -11,68 +12,75 @@ public partial class Mod { public partial class Editor { + public class FileRegistry : IEquatable< FileRegistry > + { + public readonly List< (ISubMod, Utf8GamePath) > SubModUsage = new(); + public FullPath File { get; private init; } + public Utf8RelPath RelPath { get; private init; } + public long FileSize { get; private init; } + public int CurrentUsage; + + public static bool FromFile( Mod mod, FileInfo file, [NotNullWhen( true )] out FileRegistry? registry ) + { + var fullPath = new FullPath( file.FullName ); + if( !fullPath.ToRelPath( mod.ModPath, out var relPath ) ) + { + registry = null; + return false; + } + + registry = new FileRegistry + { + File = fullPath, + RelPath = relPath, + FileSize = file.Length, + CurrentUsage = 0, + }; + return true; + } + + public bool Equals( FileRegistry? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + return ReferenceEquals( this, other ) || File.Equals( other.File ); + } + + public override bool Equals( object? obj ) + { + if( ReferenceEquals( null, obj ) ) + { + return false; + } + + if( ReferenceEquals( this, obj ) ) + { + return true; + } + + return obj.GetType() == GetType() && Equals( ( FileRegistry )obj ); + } + + public override int GetHashCode() + => File.GetHashCode(); + } + // All files in subdirectories of the mod directory. - public IReadOnlyList< (FullPath, long) > AvailableFiles + public IReadOnlyList< FileRegistry > AvailableFiles => _availableFiles; - private readonly List< (FullPath, long) > _availableFiles; - - // All files that are available but not currently used in any option. - private readonly SortedSet< FullPath > _unusedFiles; - - public IReadOnlySet< FullPath > UnusedFiles - => _unusedFiles; - - // All paths that are used in any option in the mod. - private readonly SortedSet< FullPath > _usedPaths; - - public IReadOnlySet< FullPath > UsedPaths - => _usedPaths; + public bool FileChanges { get; private set; } + private List< FileRegistry > _availableFiles = null!; + private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in - private readonly SortedSet< FullPath > _missingPaths; + private readonly SortedSet< FullPath > _missingFiles = new(); - public IReadOnlySet< FullPath > MissingPaths - => _missingPaths; - - // Adds all currently unused paths, relative to the mod directory, to the replacements. - public void AddUnusedPathsToDefault() - { - var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count ); - foreach( var file in UnusedFiles ) - { - var gamePath = file.ToGamePath( _mod.ModPath, out var g ) ? g : Utf8GamePath.Empty; - if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) ) - { - dict.Add( gamePath, file ); - PluginLog.Debug( "[AddUnusedPaths] Adding {GamePath} -> {File} to default option of {Mod}.", gamePath, file, _mod.Name ); - } - } - - Penumbra.ModManager.OptionAddFiles( _mod, -1, 0, dict ); - _usedPaths.UnionWith( _mod.Default.Files.Values ); - _unusedFiles.RemoveWhere( f => _mod.Default.Files.Values.Contains( f ) ); - } - - // Delete all currently unused paths from your filesystem. - public void DeleteUnusedPaths() - { - foreach( var file in UnusedFiles ) - { - try - { - File.Delete( file.FullName ); - PluginLog.Debug( "[DeleteUnusedPaths] Deleted {File} from {Mod}.", file, _mod.Name ); - } - catch( Exception e ) - { - PluginLog.Error($"[DeleteUnusedPaths] Could not delete {file} from {_mod.Name}:\n{e}" ); - } - } - - _unusedFiles.RemoveWhere( f => !f.Exists ); - _availableFiles.RemoveAll( p => !p.Item1.Exists ); - } + public IReadOnlySet< FullPath > MissingFiles + => _missingFiles; // Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths() @@ -88,13 +96,12 @@ public partial class Mod } ApplyToAllOptions( _mod, HandleSubMod ); - _usedPaths.RemoveWhere( _missingPaths.Contains ); - _missingPaths.Clear(); + _missingFiles.Clear(); } private bool CheckAgainstMissing( FullPath file, Utf8GamePath key ) { - if( !_missingPaths.Contains( file ) ) + if( !_missingFiles.Contains( file ) ) { return true; } @@ -104,9 +111,157 @@ public partial class Mod } - private static List<(FullPath, long)> GetAvailablePaths( Mod mod ) - => mod.ModPath.EnumerateDirectories() - .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) ) - .OrderBy( p => -p.Length ).ToList(); + // Fetch all files inside subdirectories of the main mod directory. + // Then check which options use them and how often. + private void UpdateFiles() + { + _availableFiles = _mod.ModPath.EnumerateDirectories() + .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => FileRegistry.FromFile( _mod, f, out var r ) ? r : null ) + .OfType< FileRegistry >() ) + .ToList(); + + _usedPaths.Clear(); + FileChanges = false; + foreach( var subMod in _mod.AllSubMods ) + { + foreach( var (gamePath, file) in subMod.Files ) + { + if( !file.Exists ) + { + _missingFiles.Add( file ); + if( subMod == _subMod ) + { + _usedPaths.Add( gamePath ); + } + } + else + { + var registry = _availableFiles.FirstOrDefault( x => x.File.Equals( file ) ); + if( registry != null ) + { + if( subMod == _subMod ) + { + ++registry.CurrentUsage; + _usedPaths.Add( gamePath ); + } + + registry.SubModUsage.Add( ( subMod, gamePath ) ); + } + } + } + } + } + + // Return whether the given path is already used in the current option. + public bool CanAddGamePath( Utf8GamePath path ) + => !_usedPaths.Contains( path ); + + // Try to set a given path for a given file. + // Returns false if this is not possible. + // If path is empty, it will be deleted instead. + // 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 ) + { + return false; + } + + var registry = _availableFiles[ fileIdx ]; + if( pathIdx > registry.SubModUsage.Count ) + { + return false; + } + + if( pathIdx == registry.SubModUsage.Count ) + { + registry.SubModUsage.Add( ( CurrentOption, path ) ); + ++registry.CurrentUsage; + _usedPaths.Add( path ); + } + else + { + _usedPaths.Remove( registry.SubModUsage[ pathIdx ].Item2 ); + if( path.IsEmpty ) + { + registry.SubModUsage.RemoveAt( pathIdx ); + --registry.CurrentUsage; + } + else + { + registry.SubModUsage[ pathIdx ] = ( registry.SubModUsage[ pathIdx ].Item1, path ); + } + } + + FileChanges = true; + + return true; + } + + // Transform a set of files to the appropriate game paths with the given number of folders skipped, + // and add them to the given option. + public int AddPathsToSelected( IEnumerable< FileRegistry > files, int skipFolders = 0 ) + { + var failed = 0; + foreach( var file in files ) + { + var gamePath = file.RelPath.ToGamePath( skipFolders ); + if( gamePath.IsEmpty ) + { + ++failed; + continue; + } + + if( CanAddGamePath( gamePath ) ) + { + ++file.CurrentUsage; + file.SubModUsage.Add( ( CurrentOption, gamePath ) ); + _usedPaths.Add( gamePath ); + FileChanges = true; + } + else + { + ++failed; + } + } + + return failed; + } + + // Remove all paths in the current option from the given files. + public void RemovePathsFromSelected( IEnumerable< FileRegistry > files ) + { + foreach( var file in files ) + { + file.CurrentUsage = 0; + FileChanges |= file.SubModUsage.RemoveAll( p => p.Item1 == CurrentOption && _usedPaths.Remove( p.Item2 ) ) > 0; + } + } + + // Delete all given files from your filesystem + public void DeleteFiles( IEnumerable< FileRegistry > files ) + { + var deletions = 0; + foreach( var file in files ) + { + try + { + File.Delete( file.File.FullName ); + PluginLog.Debug( "[DeleteFiles] Deleted {File} from {Mod}.", file.File.FullName, _mod.Name ); + ++deletions; + } + catch( Exception e ) + { + PluginLog.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" ); + } + } + + if( deletions > 0 ) + { + _mod.Reload( out _ ); + UpdateFiles(); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 5e87b2dd..81417fdb 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -72,22 +72,22 @@ public partial class Mod private void ScanModels() { _modelFiles.Clear(); - foreach( var (file, _) in AvailableFiles.Where( f => f.Item1.Extension == ".mdl" ) ) + foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) ) { try { - var bytes = File.ReadAllBytes( file.FullName ); + var bytes = File.ReadAllBytes( file.File.FullName ); var mdlFile = new MdlFile( bytes ); var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) ) .Select( p => p.Item2 ).ToArray(); if( materials.Length > 0 ) { - _modelFiles.Add( new MaterialInfo( file, mdlFile, materials ) ); + _modelFiles.Add( new MaterialInfo( file.File, mdlFile, materials ) ); } } catch( Exception e ) { - PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.FullName} for materials:\n{e}" ); + PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" ); } } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index d69c32f0..74158a4e 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -13,14 +13,13 @@ public partial class Mod { private readonly Mod _mod; - public Editor( Mod mod ) + public Editor( Mod mod, int groupIdx, int optionIdx ) { - _mod = mod; - _availableFiles = GetAvailablePaths( mod ); - _usedPaths = new SortedSet< FullPath >( mod.AllFiles ); - _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); - _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); - _subMod = _mod._default; + _mod = mod; + SetSubMod( groupIdx, optionIdx ); + GroupIdx = groupIdx; + _subMod = _mod._default; + UpdateFiles(); ScanModels(); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs new file mode 100644 index 00000000..47a2178c --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Logging; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256); + private LowerString _fileFilter = LowerString.Empty; + private bool _showGamePaths = true; + private string _gamePathEdit = string.Empty; + private int _fileIdx = -1; + private int _pathIdx = -1; + private int _folderSkip = 0; + + private bool CheckFilter( Mod.Editor.FileRegistry registry ) + => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + + private bool CheckFilter( (Mod.Editor.FileRegistry, int) p ) + => CheckFilter( p.Item1 ); + + private void DrawFileTab() + { + using var tab = ImRaii.TabItem( "File Redirections" ); + if( !tab ) + { + return; + } + + DrawOptionSelectHeader(); + DrawButtonHeader(); + + using var child = ImRaii.Child( "##files", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 1 ); + if( !list ) + { + return; + } + + foreach( var (registry, i) in _editor!.AvailableFiles.WithIndex().Where( CheckFilter ) ) + { + using var id = ImRaii.PushId( i ); + ImGui.TableNextColumn(); + + DrawSelectable( registry ); + + if( !_showGamePaths ) + { + continue; + } + + using var indent = ImRaii.PushIndent( 50f ); + for( var j = 0; j < registry.SubModUsage.Count; ++j ) + { + var (subMod, gamePath) = registry.SubModUsage[ j ]; + if( subMod != _editor.CurrentOption ) + { + continue; + } + + PrintGamePath( i, j, registry, subMod, gamePath ); + } + + PrintNewGamePath( i, registry, _editor.CurrentOption ); + } + } + + private string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color ) + { + (string, int) GetMulti() + { + var groups = registry.SubModUsage.GroupBy( s => s.Item1 ).ToArray(); + return ( string.Join( "\n", groups.Select( g => g.Key.Name ) ), groups.Length ); + } + + var (text, groupCount) = color switch + { + ColorId.ConflictingMod => ( string.Empty, 0 ), + ColorId.NewMod => ( registry.SubModUsage[ 0 ].Item1.Name, 1 ), + ColorId.InheritedMod => GetMulti(), + _ => ( string.Empty, 0 ), + }; + + if( text.Length > 0 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( text ); + } + + + return ( groupCount, registry.SubModUsage.Count ) switch + { + (0, 0) => "(unused)", + (1, 1) => "(used 1 time)", + (1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)", + _ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)", + }; + } + + private void DrawSelectable( Mod.Editor.FileRegistry registry ) + { + var selected = _selectedFiles.Contains( registry ); + var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : + registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; + using var c = ImRaii.PushColor( ImGuiCol.Text, color.Value() ); + if( ConfigWindow.Selectable( registry.RelPath.Path, selected ) ) + { + if( selected ) + { + _selectedFiles.Remove( registry ); + } + else + { + _selectedFiles.Add( registry ); + } + } + + var rightText = DrawFileTooltip( registry, color ); + + ImGui.SameLine(); + ImGuiUtil.RightAlign( rightText ); + } + + private void PrintGamePath( int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath ) + { + using var id = ImRaii.PushId( j ); + ImGui.TableNextColumn(); + var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength ) ) + { + _fileIdx = i; + _pathIdx = j; + _gamePathEdit = tmp; + } + + ImGuiUtil.HoverTooltip( "Clear completely to remove the path from this mod." ); + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + _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 ); + } + } + } + + private void PrintNewGamePath( int i, Mod.Editor.FileRegistry registry, ISubMod subMod ) + { + var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength ) ) + { + _fileIdx = i; + _pathIdx = -1; + _gamePathEdit = tmp; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + _fileIdx = -1; + _pathIdx = -1; + if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty ) + { + registry.SubModUsage.Add( ( subMod, path ) ); + ++registry.CurrentUsage; + } + } + } + + private void DrawButtonHeader() + { + ImGui.NewLine(); + + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); + ImGui.SetNextItemWidth( 30 * ImGuiHelpers.GlobalScale ); + 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( ); + 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." ); + + + 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." ); + + + ImGui.SameLine(); + if( ImGui.Button( "Delete Selected Files" ) ) + { + _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!!!" ); + ImGui.SameLine(); + var changes = _editor!.FileChanges; + var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) ) + { + var failedFiles = _editor!.ApplyFiles(); + PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.Name}." ); + } + + + ImGui.SameLine(); + var label = changes ? "Revert Changes" : "Reload Files"; + var length = new Vector2( ImGui.CalcTextSize( "Revert Changes" ).X, 0 ); + if( ImGui.Button( label, length ) ) + { + _editor!.RevertFiles(); + } + ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." ); + + ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); + LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength ); + ImGui.SameLine(); + ImGui.Checkbox( "Show Game Paths", ref _showGamePaths ); + ImGui.SameLine(); + if( ImGui.Button( "Unselect All" ) ) + { + _selectedFiles.Clear(); + } + + ImGui.SameLine(); + if( ImGui.Button( "Select Visible" ) ) + { + _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( CheckFilter ) ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Select Unused" ) ) + { + _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.SubModUsage.Count == 0 ) ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Select Used Here" ) ) + { + _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.CurrentUsage > 0 ) ); + } + + ImGui.SameLine(); + + ImGuiUtil.RightAlign( $"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected" ); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 4f902467..67c3b183 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -29,7 +29,7 @@ public partial class ModEditWindow : Window, IDisposable } _editor?.Dispose(); - _editor = new Mod.Editor( mod ); + _editor = new Mod.Editor( mod, -1, 0 ); _mod = mod; WindowName = $"{mod.Name}{WindowBaseLabel}"; SizeConstraints = new WindowSizeConstraints @@ -37,6 +37,7 @@ public partial class ModEditWindow : Window, IDisposable MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ), MaximumSize = 4000 * Vector2.One, }; + _selectedFiles.Clear(); } public void ChangeOption( int groupIdx, int optionIdx ) @@ -58,7 +59,6 @@ public partial class ModEditWindow : Window, IDisposable DrawMetaTab(); DrawSwapTab(); DrawMissingFilesTab(); - DrawUnusedFilesTab(); DrawDuplicatesTab(); DrawMaterialChangeTab(); } @@ -233,7 +233,7 @@ public partial class ModEditWindow : Window, IDisposable return; } - if( _editor!.MissingPaths.Count == 0 ) + if( _editor!.MissingFiles.Count == 0 ) { ImGui.NewLine(); ImGui.TextUnformatted( "No missing files detected." ); @@ -257,7 +257,7 @@ public partial class ModEditWindow : Window, IDisposable return; } - foreach( var path in _editor.MissingPaths ) + foreach( var path in _editor.MissingFiles ) { ImGui.TableNextColumn(); ImGui.TextUnformatted( path.FullName ); @@ -421,90 +421,6 @@ public partial class ModEditWindow : Window, IDisposable } } - private void DrawUnusedFilesTab() - { - using var tab = ImRaii.TabItem( "Unused Files" ); - if( !tab ) - { - return; - } - - if( ImGui.Button( "Refresh" ) ) - { - _editor!.Dispose(); - _editor = new Mod.Editor( _mod! ); - } - - if( _editor!.UnusedFiles.Count == 0 ) - { - ImGui.NewLine(); - ImGui.TextUnformatted( "No unused files detected." ); - } - else - { - ImGui.SameLine(); - if( ImGui.Button( "Add Unused Files to Default" ) ) - { - _editor.AddUnusedPathsToDefault(); - } - - ImGui.SameLine(); - if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) - { - _editor.DeleteUnusedPaths(); - } - - using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var table = ImRaii.Table( "##table", 1, ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - foreach( var path in _editor.UnusedFiles ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( path.FullName ); - } - } - } - - - private void DrawFileTab() - { - using var tab = ImRaii.TabItem( "File Redirections" ); - if( !tab ) - { - return; - } - - DrawOptionSelectHeader(); - using var child = ImRaii.Child( "##files", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var list = ImRaii.Table( "##table", 2 ); - if( !list ) - { - return; - } - - foreach( var (gamePath, file) in _editor!.CurrentFiles ) - { - ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.FullName ); - } - } - private string _newSwapKey = string.Empty; private string _newSwapValue = string.Empty; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index d9ab33e9..17cc424d 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -28,6 +28,12 @@ public partial class ConfigWindow private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); + // Draw a Utf8String as a selectable. + internal static unsafe bool Selectable( Utf8String s, bool selected ) + { + var tmp = ( byte )( selected ? 1 : 0 ); + return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0; + } // Apply Changed Item Counters to the Name if necessary. private static string ChangedItemName( string name, object? data ) From eeaaecb855a9a5c14325b2c22061da099de32e6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Jun 2022 14:15:34 +0200 Subject: [PATCH 0209/2451] Let companions, combat pets and mounts use their owners character collection. --- .../Interop/Resolver/PathResolver.Data.cs | 25 +++++- .../Interop/Resolver/PathResolver.Monster.cs | 88 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Resolve.cs | 37 ++++++++ .../Interop/Resolver/PathResolver.Weapon.cs | 1 - Penumbra/Interop/Resolver/PathResolver.cs | 4 + 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Interop/Resolver/PathResolver.Monster.cs diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index d6b37dc9..f2e81cad 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -188,6 +188,29 @@ public unsafe partial class PathResolver return pc->ClassJob == ( ( Character* )gameObject )->ClassJob ? player.Name.ToString() : null; } + // Identify the owner of a companion, mount or monster and apply the corresponding collection. + // Companions and mounts get set to the actor before them in the table if it exists. + // Monsters with a owner use that owner if it exists. + private static string? GetOwnerName( GameObject* gameObject ) + { + GameObject* owner = null; + if( ( ObjectKind )gameObject->GetObjectKind() is ObjectKind.Companion or ObjectKind.MountType && gameObject->ObjectIndex > 0 ) + { + owner = ( GameObject* )Dalamud.Objects[ gameObject->ObjectIndex - 1 ]?.Address; + } + else if( gameObject->OwnerID != 0xE0000000 ) + { + owner = ( GameObject* )Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address; + } + + if( owner != null ) + { + return new Utf8String( owner->Name ).ToString(); + } + + return null; + } + // Identify the correct collection for a GameObject by index and name. private static ModCollection IdentifyCollection( GameObject* gameObject ) { @@ -204,7 +227,7 @@ public unsafe partial class PathResolver >= 200 => GetCutsceneName( gameObject ), _ => null, } - ?? new Utf8String( gameObject->Name ).ToString(); + ?? GetOwnerName( gameObject ) ?? new Utf8String( gameObject->Name ).ToString(); return Penumbra.CollectionManager.Character( name ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Monster.cs b/Penumbra/Interop/Resolver/PathResolver.Monster.cs new file mode 100644 index 00000000..f7baa229 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Monster.cs @@ -0,0 +1,88 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] + public IntPtr* DrawObjectMonsterVTable; + + public Hook? ResolveMonsterDecalPathHook; + public Hook? ResolveMonsterEidPathHook; + public Hook? ResolveMonsterImcPathHook; + public Hook? ResolveMonsterMPapPathHook; + public Hook? ResolveMonsterMdlPathHook; + public Hook? ResolveMonsterMtrlPathHook; + public Hook? ResolveMonsterPapPathHook; + public Hook? ResolveMonsterPhybPathHook; + public Hook? ResolveMonsterSklbPathHook; + public Hook? ResolveMonsterSkpPathHook; + public Hook? ResolveMonsterTmbPathHook; + public Hook? ResolveMonsterVfxPathHook; + + private void SetupMonsterHooks() + { + ResolveMonsterDecalPathHook = new Hook( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour ); + ResolveMonsterEidPathHook = new Hook( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour ); + ResolveMonsterImcPathHook = new Hook( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour ); + ResolveMonsterMPapPathHook = new Hook( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour ); + ResolveMonsterMdlPathHook = new Hook( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour ); + ResolveMonsterMtrlPathHook = new Hook( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour ); + ResolveMonsterPapPathHook = new Hook( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour ); + ResolveMonsterPhybPathHook = new Hook( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour ); + ResolveMonsterSklbPathHook = new Hook( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour ); + ResolveMonsterSkpPathHook = new Hook( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour ); + ResolveMonsterTmbPathHook = new Hook( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour ); + ResolveMonsterVfxPathHook = new Hook( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour ); + } + + private void EnableMonsterHooks() + { + ResolveMonsterDecalPathHook?.Enable(); + ResolveMonsterEidPathHook?.Enable(); + ResolveMonsterImcPathHook?.Enable(); + ResolveMonsterMPapPathHook?.Enable(); + ResolveMonsterMdlPathHook?.Enable(); + ResolveMonsterMtrlPathHook?.Enable(); + ResolveMonsterPapPathHook?.Enable(); + ResolveMonsterPhybPathHook?.Enable(); + ResolveMonsterSklbPathHook?.Enable(); + ResolveMonsterSkpPathHook?.Enable(); + ResolveMonsterTmbPathHook?.Enable(); + ResolveMonsterVfxPathHook?.Enable(); + } + + private void DisableMonsterHooks() + { + ResolveMonsterDecalPathHook?.Disable(); + ResolveMonsterEidPathHook?.Disable(); + ResolveMonsterImcPathHook?.Disable(); + ResolveMonsterMPapPathHook?.Disable(); + ResolveMonsterMdlPathHook?.Disable(); + ResolveMonsterMtrlPathHook?.Disable(); + ResolveMonsterPapPathHook?.Disable(); + ResolveMonsterPhybPathHook?.Disable(); + ResolveMonsterSklbPathHook?.Disable(); + ResolveMonsterSkpPathHook?.Disable(); + ResolveMonsterTmbPathHook?.Disable(); + ResolveMonsterVfxPathHook?.Disable(); + } + + private void DisposeMonsterHooks() + { + ResolveMonsterDecalPathHook?.Dispose(); + ResolveMonsterEidPathHook?.Dispose(); + ResolveMonsterImcPathHook?.Dispose(); + ResolveMonsterMPapPathHook?.Dispose(); + ResolveMonsterMdlPathHook?.Dispose(); + ResolveMonsterMtrlPathHook?.Dispose(); + ResolveMonsterPapPathHook?.Dispose(); + ResolveMonsterPhybPathHook?.Dispose(); + ResolveMonsterSklbPathHook?.Dispose(); + ResolveMonsterSkpPathHook?.Dispose(); + ResolveMonsterTmbPathHook?.Dispose(); + ResolveMonsterVfxPathHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index c283e9ce..7338f9e4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -98,6 +98,43 @@ public unsafe partial class PathResolver private IntPtr ResolveWeaponVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) => ResolveWeaponPathDetour( drawObject, ResolveWeaponVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // Monsters + private IntPtr ResolveMonsterDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMonsterDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMonsterEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveMonsterEidPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveMonsterImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMonsterImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMonsterMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolvePathDetour( drawObject, ResolveMonsterMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMonsterMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + => ResolvePathDetour( drawObject, ResolveMonsterMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); + + private IntPtr ResolveMonsterMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveMonsterMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMonsterPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveMonsterPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMonsterPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMonsterPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMonsterSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMonsterSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMonsterSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMonsterSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMonsterTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveMonsterTmbPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveMonsterVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveMonsterVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] diff --git a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs index 7fc1f766..0a9ac900 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs @@ -23,7 +23,6 @@ public unsafe partial class PathResolver public Hook< EidResolveDelegate >? ResolveWeaponTmbPathHook; public Hook< MaterialResolveDetour >? ResolveWeaponVfxPathHook; - private void SetupWeaponHooks() { ResolveWeaponDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour ); diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4f73e762..ad3e7b41 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -25,6 +25,7 @@ public partial class PathResolver : IDisposable SignatureHelper.Initialise( this ); SetupHumanHooks(); SetupWeaponHooks(); + SetupMonsterHooks(); SetupMetaHooks(); } @@ -82,6 +83,7 @@ public partial class PathResolver : IDisposable EnableHumanHooks(); EnableWeaponHooks(); + EnableMonsterHooks(); EnableMtrlHooks(); EnableDataHooks(); EnableMetaHooks(); @@ -100,6 +102,7 @@ public partial class PathResolver : IDisposable Enabled = false; DisableHumanHooks(); DisableWeaponHooks(); + DisableMonsterHooks(); DisableMtrlHooks(); DisableDataHooks(); DisableMetaHooks(); @@ -116,6 +119,7 @@ public partial class PathResolver : IDisposable Disable(); DisposeHumanHooks(); DisposeWeaponHooks(); + DisposeMonsterHooks(); DisposeMtrlHooks(); DisposeDataHooks(); DisposeMetaHooks(); From 6776a7fa7e8cac5fadd3a23f01dbc5de3234889f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Jun 2022 15:35:42 +0200 Subject: [PATCH 0210/2451] Compute GameData on plugin initialize instead of on Penumbra initialize. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a9bd7c7b..5d2dabda 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -32,6 +32,7 @@ public class MainClass : IDalamudPlugin public MainClass( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); + GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); _characterUtility = new CharacterUtility(); _characterUtility.LoadingFinished += () => _penumbra = new Penumbra( _characterUtility ); @@ -87,7 +88,6 @@ public class Penumbra : IDisposable CharacterUtility = characterUtility; Framework = new FrameworkManager(); - GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); From 4ef8eeb042a504741b32b9939c97658251e82cbe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Jun 2022 11:04:11 +0200 Subject: [PATCH 0211/2451] Fix crash. --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index f2e81cad..c384878a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -200,7 +200,7 @@ public unsafe partial class PathResolver } else if( gameObject->OwnerID != 0xE0000000 ) { - owner = ( GameObject* )Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address; + owner = ( GameObject* )(Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address ?? IntPtr.Zero); } if( owner != null ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 5139d7a3..96cfd632 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -449,7 +449,7 @@ public partial class ModEditWindow { using var id = ImRaii.PushId( i ); var flag = 1 << i; - if( Checkmark( "##attribute", $"{( char )( 'A' + i )}\nDefault Value: ", ( meta.Entry.AttributeMask & flag ) != 0, + if( Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( meta.Entry.AttributeMask & flag ) != 0, ( defaultEntry.AttributeMask & flag ) != 0, out var val ) ) { var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; From ea4d087ae929538158698b627d85f74f69784f85 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 00:52:07 +0200 Subject: [PATCH 0212/2451] Fix enable/disable/inherit all descendants. --- Penumbra/Collections/ModCollection.Cache.cs | 30 +++++++++---------- Penumbra/Collections/ModCollection.Changes.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 31cd55a3..efc10988 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -81,34 +81,33 @@ public partial class ModCollection private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { - var mod = Penumbra.ModManager[ modIdx ]; switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( mod, true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: if( oldValue != 1 ) { - AddMod( mod, true ); + AddMod( Penumbra.ModManager[ modIdx ], true ); } else { - RemoveMod( mod, true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Priority: - if( Conflicts( mod ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) { - ReloadMod( mod, true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Setting: if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( mod, true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; @@ -248,6 +247,7 @@ public partial class ModCollection } } } + AddSubMod( mod.Default, mod ); if( addMetaChanges ) @@ -344,16 +344,16 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { var tmpConflicts = Conflicts( existingMod ); foreach( var conflict in tmpConflicts ) { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals(path) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals(meta) ) > 0 ) + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); } @@ -362,7 +362,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -437,11 +437,11 @@ public partial class ModCollection } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); } - else if (obj is int x && data.Item2 is int y) + else if( obj is int x && data.Item2 is int y ) { - _changedItems[name] = (data.Item1, x + y); + _changedItems[ name ] = ( data.Item1, x + y ); } } } diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 9ff07910..8009dc14 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -61,7 +61,7 @@ public partial class ModCollection var changes = false; foreach( var mod in mods ) { - var oldValue = _settings[ mod.Index ]?.Enabled ?? this[ mod.Index ].Settings?.Enabled ?? false; + var oldValue = _settings[ mod.Index ]?.Enabled; if( newValue != oldValue ) { FixInheritance( mod.Index, false ); From 4b58213597b5bf24d1c9cc9de0d1b9236fec7ecb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 00:52:33 +0200 Subject: [PATCH 0213/2451] Fixed colors in mod selector not automatically updating --- .../Classes/ModFileSystemSelector.Filters.cs | 30 +++++++++---------- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 25da4f4f..143e7df1 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -18,7 +18,7 @@ public partial class ModFileSystemSelector [StructLayout( LayoutKind.Sequential, Pack = 1 )] public struct ModState { - public uint Color; + public ColorId Color; } private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; @@ -102,32 +102,32 @@ public partial class ModFileSystemSelector } // Only get the text color for a mod if no filters are set. - private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) + private static ColorId GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) { if( Penumbra.ModManager.NewMods.Contains( mod ) ) { - return ColorId.NewMod.Value(); + return ColorId.NewMod; } if( settings == null ) { - return ColorId.UndefinedMod.Value(); + return ColorId.UndefinedMod; } if( !settings.Enabled ) { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value(); + return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod; } var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); if( conflicts.Count == 0 ) { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value(); + return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod; } return conflicts.Any( c => !c.Solved ) - ? ColorId.ConflictingMod.Value() - : ColorId.HandledConflictMod.Value(); + ? ColorId.ConflictingMod + : ColorId.HandledConflictMod; } private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) @@ -153,7 +153,7 @@ public partial class ModFileSystemSelector } else { - state.Color = ColorId.InheritedMod.Value(); + state.Color = ColorId.InheritedMod; if( !_stateFilter.HasFlag( ModFilter.Inherited ) ) { return true; @@ -163,7 +163,7 @@ public partial class ModFileSystemSelector // Handle settings. if( settings == null ) { - state.Color = ColorId.UndefinedMod.Value(); + state.Color = ColorId.UndefinedMod; if( !_stateFilter.HasFlag( ModFilter.Undefined ) || !_stateFilter.HasFlag( ModFilter.Disabled ) || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) @@ -173,7 +173,7 @@ public partial class ModFileSystemSelector } else if( !settings.Enabled ) { - state.Color = collection == Penumbra.CollectionManager.Current ? ColorId.DisabledMod.Value() : ColorId.InheritedDisabledMod.Value(); + state.Color = collection == Penumbra.CollectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod; if( !_stateFilter.HasFlag( ModFilter.Disabled ) || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) { @@ -198,7 +198,7 @@ public partial class ModFileSystemSelector return true; } - state.Color = ColorId.ConflictingMod.Value(); + state.Color = ColorId.ConflictingMod; } else { @@ -207,7 +207,7 @@ public partial class ModFileSystemSelector return true; } - state.Color = ColorId.HandledConflictMod.Value(); + state.Color = ColorId.HandledConflictMod; } } else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) @@ -219,7 +219,7 @@ public partial class ModFileSystemSelector // isNew color takes precedence before other colors. if( isNew ) { - state.Color = ColorId.NewMod.Value(); + state.Color = ColorId.NewMod; } return false; @@ -228,7 +228,7 @@ public partial class ModFileSystemSelector // Combined wrapper for handling all filters and setting state. private bool ApplyFiltersAndState( ModFileSystem.Leaf leaf, out ModState state ) { - state = new ModState { Color = ColorId.EnabledMod.Value() }; + state = new ModState { Color = ColorId.EnabledMod }; var mod = leaf.Value; var (settings, collection) = Penumbra.CollectionManager.Current[ mod.Index ]; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index bc72fd12..ff606509 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -107,7 +107,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); + using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); using var id = ImRaii.PushId( leaf.Value.Index ); using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); } From c0102368c302d6dbcb30c50c4837fb734afe396d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 00:54:01 +0200 Subject: [PATCH 0214/2451] Add some options to which special actors use which character collection and fix Inspect Actor recognition. --- Penumbra/Configuration.cs | 7 +++ .../Interop/Resolver/PathResolver.Data.cs | 52 ++++++++++++++++--- .../UI/ConfigWindow.SettingsTab.General.cs | 20 +++++++ 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 896295c8..2e39f4d3 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -21,6 +21,13 @@ public partial class Configuration : IPluginConfiguration public bool HideUiInCutscenes { get; set; } = true; public bool HideUiWhenUiHidden { get; set; } = false; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool PreferNamedCollectionsOverOwners { get; set; } = false; + #if DEBUG public bool DebugMode { get; set; } = true; #else diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index c384878a..f32c290e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -125,6 +125,11 @@ public unsafe partial class PathResolver // Obtain the name of the inspect target from its window, if it exists. private static string? GetInspectName() { + if( !Penumbra.Config.UseCharacterCollectionInInspect ) + { + return null; + } + var addon = Dalamud.GameGui.GetAddonByName( "CharacterInspect", 1 ); if( addon == IntPtr.Zero ) { @@ -137,13 +142,23 @@ public unsafe partial class PathResolver return null; } - var text = ( AtkTextNode* )ui->UldManager.NodeList[ 60 ]; + var text = ( AtkTextNode* )ui->UldManager.NodeList[ 59 ]; + if( text == null || !text->AtkResNode.IsVisible ) + { + text = ( AtkTextNode* )ui->UldManager.NodeList[ 60 ]; + } + return text != null ? text->NodeText.ToString() : null; } // Obtain the name displayed in the Character Card from the agent. private static string? GetCardName() { + if( !Penumbra.Config.UseCharacterCollectionsInCards ) + { + return null; + } + var uiModule = ( UIModule* )Dalamud.GameGui.GetUIModule(); var agentModule = uiModule->GetAgentModule(); var agent = ( byte* )agentModule->GetAgentByInternalID( 393 ); @@ -165,6 +180,11 @@ public unsafe partial class PathResolver // Obtain the name of the player character if the glamour plate edit window is open. private static string? GetGlamourName() { + if( !Penumbra.Config.UseCharacterCollectionInTryOn ) + { + return null; + } + var addon = Dalamud.GameGui.GetAddonByName( "MiragePrismMiragePlate", 1 ); return addon == IntPtr.Zero ? null : GetPlayerName(); } @@ -193,6 +213,11 @@ public unsafe partial class PathResolver // Monsters with a owner use that owner if it exists. private static string? GetOwnerName( GameObject* gameObject ) { + if( !Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + return null; + } + GameObject* owner = null; if( ( ObjectKind )gameObject->GetObjectKind() is ObjectKind.Companion or ObjectKind.MountType && gameObject->ObjectIndex > 0 ) { @@ -200,7 +225,7 @@ public unsafe partial class PathResolver } else if( gameObject->OwnerID != 0xE0000000 ) { - owner = ( GameObject* )(Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address ?? IntPtr.Zero); + owner = ( GameObject* )( Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address ?? IntPtr.Zero ); } if( owner != null ) @@ -219,17 +244,30 @@ public unsafe partial class PathResolver return Penumbra.CollectionManager.Default; } - var name = gameObject->ObjectIndex switch + string? actorName = null; + if( Penumbra.Config.PreferNamedCollectionsOverOwners ) + { + // Early return if we prefer the actors own name over its owner. + actorName = new Utf8String( gameObject->Name ).ToString(); + if( actorName.Length > 0 && Penumbra.CollectionManager.Characters.TryGetValue( actorName, out var actorCollection ) ) { - 240 => GetPlayerName(), // character window + return actorCollection; + } + } + + // All these special cases are relevant for an empty name, so never collide with the above setting. + // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. + var actualName = gameObject->ObjectIndex switch + { + 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => GetPlayerName(), // try-on + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on >= 200 => GetCutsceneName( gameObject ), _ => null, } - ?? GetOwnerName( gameObject ) ?? new Utf8String( gameObject->Name ).ToString(); + ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.CollectionManager.Character( name ); + return Penumbra.CollectionManager.Character( actualName ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 5fe02deb..3f71f6d8 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -56,6 +56,26 @@ public partial class ConfigWindow Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); + Checkbox( "Use Character Collections in Character Window", + "Use the character collection for your character's name in your main character window, if it is set.", + Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); + Checkbox( "Use Character Collections in Adventurer Cards", + "Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.", + Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); + Checkbox( "Use Character Collections in Try-On Window", + "Use the character collection for your character's name in your try-on window, if it is set.", + Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); + Checkbox( "Use Character Collections in Inspect Windows", + "Use the appropriate character collection for the character you are currently inspecting, based on their name.", + Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); + Checkbox( "Use Character Collections based on Ownership", + "Use the owner's name to determine the appropriate character collection for mounts, companions and combat pets.", + Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); + Checkbox( "Prefer Named Collections over Ownership", + "If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owner's collection.\n" + + "That is, if you have a 'Topaz Carbuncle' collection, it will use this one instead of the one for its owner.", + Penumbra.Config.PreferNamedCollectionsOverOwners, v => Penumbra.Config.PreferNamedCollectionsOverOwners = v ); + ImGui.Dummy( _window._defaultSpace ); DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); From f0131dd5ba937c5455012c6ebcb76a84bb08b1cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 19:02:58 +0200 Subject: [PATCH 0215/2451] Add preliminary pap handling to character collections. --- .../Resolver/PathResolver.Animation.cs | 62 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Data.cs | 7 ++- Penumbra/Interop/Resolver/PathResolver.cs | 16 ++++- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Interop/Resolver/PathResolver.Animation.cs diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs new file mode 100644 index 00000000..997ee6bb --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Collections; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + + // Probably used when the base idle animation gets loaded. + // Make it aware of the correct collection to load the correct pap files. + [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] + public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; + + private ModCollection? _animationLoadCollection; + + private void CharacterBaseLoadAnimationDetour( IntPtr address ) + { + _animationLoadCollection = _lastCreatedCollection ?? IdentifyCollection( ( GameObject* )address ); + CharacterBaseLoadAnimationHook!.Original( address ); + _animationLoadCollection = null; + } + + // Probably used when action paps are loaded. + // Make it aware of the correct collection to load the correct pap files. + public delegate void PapLoadFunction( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ); + + [Signature( "E8 ?? ?? ?? ?? 0F 10 00 0F 11 06", DetourName = "RandomPapDetour" )] + public Hook< PapLoadFunction >? RandomPapHook; + private void RandomPapDetour( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ) + { + _animationLoadCollection = _lastCreatedCollection ?? IdentifyCollection( ( GameObject* )drawObject ); + RandomPapHook!.Original( drawObject, a2, a3, a4, a5, a6, a7 ); + _animationLoadCollection = null; + } + + //private void TestFunction() + //{ + // var p = Dalamud.Objects.FirstOrDefault( o => o.Name.ToString() == "Demi-Phoenix" ); + // if( p != null ) + // { + // var draw = ( ( GameObject* )p.Address )->DrawObject; + // PluginLog.Information( $"{p.Address:X} {( draw != null ? ( ( IntPtr )draw ).ToString( "X" ) : "NULL" )}" ); + // } + //} + // + //public delegate void TmbLoadFunction(IntPtr drawObject, ushort a2, uint a3, IntPtr a4, IntPtr a5 ); + // + //[Signature( "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 44 38 75 ?? 74 ?? 44 89 B3 ", DetourName ="RandomTmbDetour" )] + //public Hook< TmbLoadFunction > UnkHook = null; + // + //private void RandomTmbDetour( IntPtr drawObject, ushort a2, uint a3, IntPtr a4, IntPtr a5 ) + //{ + // //PluginLog.Information($"{drawObject:X} {a2:X}, {a3:X} {a4:X} {a5:X}" ); + // //TestFunction(); + // UnkHook!.Original( drawObject, a2, a3, a4, a5); + //} +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index f32c290e..e4d884f1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -72,12 +72,16 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Enable(); EnableDrawHook?.Enable(); CharacterBaseDestructorHook?.Enable(); + CharacterBaseLoadAnimationHook?.Enable(); + RandomPapHook?.Enable(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; } private void DisableDataHooks() { Penumbra.CollectionManager.CollectionChanged -= CheckCollections; + RandomPapHook?.Disable(); + CharacterBaseLoadAnimationHook?.Disable(); CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); @@ -85,12 +89,13 @@ public unsafe partial class PathResolver private void DisposeDataHooks() { + CharacterBaseLoadAnimationHook?.Dispose(); CharacterBaseCreateHook?.Dispose(); EnableDrawHook?.Dispose(); CharacterBaseDestructorHook?.Dispose(); + RandomPapHook?.Dispose(); } - // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index ad3e7b41..97cecc57 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -40,6 +40,7 @@ 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 ) || HandleDecalFile( type, gamePath, out collection ); if( !nonDefault ) { @@ -59,7 +60,7 @@ public partial class PathResolver : IDisposable private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection ) { - if( type == ResourceType.Tex + if( type == ResourceType.Tex && _lastCreatedCollection != null && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) { @@ -71,6 +72,19 @@ public partial class PathResolver : IDisposable return false; } + private bool HandlePapFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection ) + { + if( type is ResourceType.Pap or ResourceType.Tmb + && _animationLoadCollection != null) + { + collection = _animationLoadCollection; + return true; + } + + collection = null; + return false; + } + public void Enable() { if( Enabled ) From cf79f47e08081b3171628c50ba24dcef2604e370 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 20:46:50 +0200 Subject: [PATCH 0216/2451] Extend some more IPC functions. --- Penumbra/Api/IPenumbraApi.cs | 96 +++-- Penumbra/Api/PenumbraApi.cs | 57 ++- Penumbra/Api/PenumbraIpc.cs | 410 +++++++++++++++------- Penumbra/Interop/Resolver/PathResolver.cs | 6 + 4 files changed, 411 insertions(+), 158 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 487cd568..14c3b8ea 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,51 +1,77 @@ +using System; using System.Collections.Generic; +using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.GameData.Enums; -namespace Penumbra.Api +namespace Penumbra.Api; + +public interface IPenumbraApiBase { - public interface IPenumbraApiBase - { - public int ApiVersion { get; } - public bool Valid { get; } - } + public int ApiVersion { get; } + public bool Valid { get; } +} - public delegate void ChangedItemHover( object? item ); - public delegate void ChangedItemClick( MouseButton button, object? item ); +public delegate void ChangedItemHover( object? item ); +public delegate void ChangedItemClick( MouseButton button, object? item ); - public interface IPenumbraApi : IPenumbraApiBase - { - // Triggered when the user hovers over a listed changed object in a mod tab. - // Can be used to append tooltips. - public event ChangedItemHover? ChangedItemTooltip; - // Triggered when the user clicks a listed changed object in a mod tab. - public event ChangedItemClick? ChangedItemClicked; - - // Queue redrawing of all actors of the given name with the given RedrawType. - public void RedrawObject( string name, RedrawType setting ); +public interface IPenumbraApi : IPenumbraApiBase +{ + // Obtain the currently set mod directory from the configuration. + public string GetModDirectory(); - // 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 ); + // Obtain the entire current penumbra configuration. + public IPluginConfiguration GetConfiguration(); - // Queue redrawing of all currently available actors with the given RedrawType. - public void RedrawAll( RedrawType setting ); + // Triggered when the user hovers over a listed changed object in a mod tab. + // Can be used to append tooltips. + public event ChangedItemHover? ChangedItemTooltip; - // Resolve a given gamePath via Penumbra using the Default and Forced collections. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePath(string gamePath); + // Triggered when the user clicks a listed changed object in a mod tab. + public event ChangedItemClick? ChangedItemClicked; - // Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePath( string gamePath, string characterName ); + // Queue redrawing of all actors of the given name with the given RedrawType. + public void RedrawObject( string name, RedrawType setting ); - // Try to load a given gamePath with the resolved path from Penumbra. - public T? GetFile< T >( string gamePath ) where T : FileResource; + // 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 ); - // Try to load a given gamePath with the resolved path from Penumbra. - public T? GetFile( string gamePath, string characterName ) where T : FileResource; + // Queue redrawing of all currently available actors with the given RedrawType. + public void RedrawAll( RedrawType setting ); - // Gets a dictionary of effected items from a collection - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection(string collectionName); - } + // Resolve a given gamePath via Penumbra using the Default and Forced collections. + // Returns the given gamePath if penumbra would not manipulate it. + public string ResolvePath( string gamePath ); + + // Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections. + // Returns the given gamePath if penumbra would not manipulate it. + public string ResolvePath( string gamePath, string characterName ); + + // Try to load a given gamePath with the resolved path from Penumbra. + public T? GetFile< T >( string gamePath ) where T : FileResource; + + // Try to load a given gamePath with the resolved path from Penumbra. + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource; + + // Gets a dictionary of effected items from a collection + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ); + + // Obtain a list of the names of all currently installed collections. + public IList< string > GetCollections(); + + // Obtain the name of the currently selected collection. + public string GetCurrentCollection(); + + // Obtain the name of the default collection. + public string GetDefaultCollection(); + + // Obtain the name of the collection associated with characterName and whether it is configured or inferred from default. + public (string, bool) GetCharacterCollection( string characterName ); + + // Obtain the game object associated with a given draw object and the name of the collection associated with this game object. + public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); + + // 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(); } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index e0d85731..9238607c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Newtonsoft.Json; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -36,6 +38,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public event ChangedItemClick? ChangedItemClicked; + + public string GetModDirectory() + { + CheckInitialized(); + return Penumbra.Config.ModDirectory; + } + + public IPluginConfiguration GetConfiguration() + { + CheckInitialized(); + return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); + } + public event ChangedItemHover? ChangedItemTooltip; internal bool HasTooltip @@ -59,21 +74,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void RedrawObject( string name, RedrawType setting ) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( name, setting ); } public void RedrawObject( GameObject? gameObject, RedrawType setting ) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); } public void RedrawAll( RedrawType setting ) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawAll( setting ); } @@ -151,4 +163,43 @@ public class PenumbraApi : IDisposable, IPenumbraApi throw; } } + + public IList< string > GetCollections() + { + CheckInitialized(); + return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); + } + + public string GetCurrentCollection() + { + CheckInitialized(); + return Penumbra.CollectionManager.Current.Name; + } + + public string GetDefaultCollection() + { + CheckInitialized(); + return Penumbra.CollectionManager.Default.Name; + } + + public (string, bool) GetCharacterCollection( string characterName ) + { + CheckInitialized(); + return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) + ? ( collection.Name, true ) + : ( Penumbra.CollectionManager.Default.Name, false ); + } + + public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) + { + CheckInitialized(); + var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); + return ( obj, collection.Name ); + } + + public IList< (string, string) > GetModList() + { + CheckInitialized(); + return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 8d2575ce..d19d13b8 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin; @@ -8,34 +9,115 @@ using Penumbra.GameData.Enums; namespace Penumbra.Api; -public class PenumbraIpc : IDisposable +public partial class PenumbraIpc : IDisposable +{ + internal readonly IPenumbraApi Api; + + public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) + { + Api = api; + + InitializeGeneralProviders( pi ); + InitializeResolveProviders( pi ); + InitializeRedrawProviders( pi ); + InitializeChangedItemProviders( pi ); + InitializeDataProviders( pi ); + ProviderInitialized?.SendMessage(); + } + + public void Dispose() + { + DisposeDataProviders(); + DisposeChangedItemProviders(); + DisposeRedrawProviders(); + DisposeResolveProviders(); + DisposeGeneralProviders(); + ProviderDisposed?.SendMessage(); + } +} + +public partial class PenumbraIpc { public const string LabelProviderInitialized = "Penumbra.Initialized"; public const string LabelProviderDisposed = "Penumbra.Disposed"; public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + private void InitializeGeneralProviders( DalamudPluginInterface pi ) + { + try + { + ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); + } - internal readonly IPenumbraApi Api; + try + { + ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); + } + + try + { + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); + } + + try + { + ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); + ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" ); + } + + try + { + ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); + ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetConfiguration}:\n{e}" ); + } + } + + private void DisposeGeneralProviders() + { + ProviderGetConfiguration?.UnregisterFunc(); + ProviderGetModDirectory?.UnregisterFunc(); + ProviderApiVersion?.UnregisterFunc(); + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; private static RedrawType CheckRedrawType( int value ) { @@ -48,6 +130,108 @@ public class PenumbraIpc : IDisposable throw new Exception( "The integer provided for a Redraw Function was not a valid RedrawType." ); } + private void InitializeRedrawProviders( DalamudPluginInterface pi ) + { + try + { + ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); + } + + try + { + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); + ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); + } + + try + { + ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); + } + } + + private void DisposeRedrawProviders() + { + ProviderRedrawName?.UnregisterAction(); + ProviderRedrawObject?.UnregisterAction(); + ProviderRedrawAll?.UnregisterAction(); + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + + private void InitializeResolveProviders( DalamudPluginInterface pi ) + { + try + { + ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); + ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); + } + + try + { + ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); + ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); + } + + try + { + ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); + } + } + + private void DisposeResolveProviders() + { + ProviderGetDrawObjectInfo?.UnregisterFunc(); + ProviderResolveDefault?.UnregisterFunc(); + ProviderResolveCharacter?.UnregisterFunc(); + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + private void OnClick( MouseButton click, object? item ) { var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); @@ -60,93 +244,12 @@ public class PenumbraIpc : IDisposable ProviderChangedItemTooltip?.SendMessage( type, id ); } - - public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) + private void InitializeChangedItemProviders( DalamudPluginInterface pi ) { - Api = api; - - try - { - ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); - } - - try - { - ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); - } - - try - { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); - ProviderApiVersion.RegisterFunc( () => api.ApiVersion ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); - } - - try - { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); - ProviderRedrawName.RegisterAction( ( s, i ) => api.RedrawObject( s, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); - } - - try - { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); - ProviderRedrawObject.RegisterAction( ( o, i ) => api.RedrawObject( o, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); - } - - try - { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); - ProviderRedrawAll.RegisterAction( i => api.RedrawAll( CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); - } - - try - { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); - ProviderResolveDefault.RegisterFunc( api.ResolvePath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); - } - - try - { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); - ProviderResolveCharacter.RegisterFunc( api.ResolvePath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); - } - try { ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); - api.ChangedItemTooltip += OnTooltip; + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -156,7 +259,7 @@ public class PenumbraIpc : IDisposable try { ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); - api.ChangedItemClicked += OnClick; + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -165,29 +268,96 @@ public class PenumbraIpc : IDisposable try { - ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); - ProviderGetChangedItems.RegisterFunc( api.GetChangedItemsForCollection ); + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + } + + private void DisposeChangedItemProviders() + { + ProviderGetChangedItems?.UnregisterFunc(); + Api.ChangedItemClicked -= OnClick; + Api.ChangedItemTooltip -= OnTooltip; + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; + + internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; + internal ICallGateProvider< IList< string > >? ProviderGetCollections; + internal ICallGateProvider< string >? ProviderCurrentCollectionName; + internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + + private void InitializeDataProviders( DalamudPluginInterface pi ) + { + try + { + ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); + ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } - ProviderInitialized?.SendMessage(); + try + { + ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); + ProviderGetCollections.RegisterFunc( Api.GetCollections ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + + try + { + ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + + try + { + ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + + try + { + ProviderCharacterCollectionName = pi.GetIpcProvider< string, ( string, bool) >( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } } - public void Dispose() + private void DisposeDataProviders() { - ProviderDisposed?.SendMessage(); - ProviderInitialized?.UnregisterFunc(); - ProviderApiVersion?.UnregisterFunc(); - ProviderRedrawName?.UnregisterAction(); - ProviderRedrawObject?.UnregisterAction(); - ProviderRedrawAll?.UnregisterAction(); - ProviderResolveDefault?.UnregisterFunc(); - ProviderResolveCharacter?.UnregisterFunc(); - ProviderGetChangedItems?.UnregisterFunc(); - Api.ChangedItemClicked -= OnClick; - Api.ChangedItemTooltip -= OnTooltip; + ProviderGetMods?.UnregisterFunc(); + ProviderGetCollections?.UnregisterFunc(); + ProviderCurrentCollectionName?.UnregisterFunc(); + ProviderDefaultCollectionName?.UnregisterFunc(); + ProviderCharacterCollectionName?.UnregisterFunc(); } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 97cecc57..b8d76c47 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -138,4 +138,10 @@ public partial class PathResolver : IDisposable DisposeDataHooks(); DisposeMetaHooks(); } + + public unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject ) + { + var parent = FindParent( drawObject, out var collection ); + return ( ( IntPtr )parent, collection ); + } } \ No newline at end of file From be84b363195dfbe13345e99f84a470b38f051202 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 20:47:29 +0200 Subject: [PATCH 0217/2451] Small pap resolving fixes. --- .../Interop/Resolver/PathResolver.Animation.cs | 14 +++++++------- Penumbra/Interop/Resolver/PathResolver.cs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 997ee6bb..3c58519f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; @@ -10,7 +8,6 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - // Probably used when the base idle animation gets loaded. // Make it aware of the correct collection to load the correct pap files. [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] @@ -18,10 +15,11 @@ public unsafe partial class PathResolver private ModCollection? _animationLoadCollection; - private void CharacterBaseLoadAnimationDetour( IntPtr address ) + private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) { - _animationLoadCollection = _lastCreatedCollection ?? IdentifyCollection( ( GameObject* )address ); - CharacterBaseLoadAnimationHook!.Original( address ); + _animationLoadCollection = _lastCreatedCollection + ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + CharacterBaseLoadAnimationHook!.Original( drawObject ); _animationLoadCollection = null; } @@ -31,9 +29,11 @@ public unsafe partial class PathResolver [Signature( "E8 ?? ?? ?? ?? 0F 10 00 0F 11 06", DetourName = "RandomPapDetour" )] public Hook< PapLoadFunction >? RandomPapHook; + private void RandomPapDetour( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ) { - _animationLoadCollection = _lastCreatedCollection ?? IdentifyCollection( ( GameObject* )drawObject ); + _animationLoadCollection = _lastCreatedCollection + ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); RandomPapHook!.Original( drawObject, a2, a3, a4, a5, a6, a7 ); _animationLoadCollection = null; } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index b8d76c47..c3b04436 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -72,10 +72,10 @@ public partial class PathResolver : IDisposable return false; } - private bool HandlePapFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection ) + private bool HandlePapFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) { if( type is ResourceType.Pap or ResourceType.Tmb - && _animationLoadCollection != null) + && _animationLoadCollection != null ) { collection = _animationLoadCollection; return true; From 135c067fa71b81d547bac2a9d95ae7a212d2e59f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 20:56:28 +0200 Subject: [PATCH 0218/2451] Change debug tab ipc handling. --- Penumbra/UI/ConfigWindow.DebugTab.cs | 50 +++++----------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index c0870782..879b4385 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Numerics; +using System.Reflection; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -410,49 +411,16 @@ public partial class ConfigWindow ImGui.TextUnformatted( $"API Version: {ipc.Api.ApiVersion}" ); ImGui.TextUnformatted( "Available subscriptions:" ); using var indent = ImRaii.PushIndent(); - if( ipc.ProviderApiVersion != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderApiVersion ); - } - if( ipc.ProviderRedrawName != null ) + var dict = ipc.GetType().GetFields( BindingFlags.Static | BindingFlags.Public ).Where( f => f.IsLiteral ) + .ToDictionary( f => f.Name, f => f.GetValue( ipc ) as string ); + foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawName ); - } - - if( ipc.ProviderRedrawObject != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawObject ); - } - - if( ipc.ProviderRedrawAll != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderRedrawAll ); - } - - if( ipc.ProviderResolveDefault != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderResolveDefault ); - } - - if( ipc.ProviderResolveCharacter != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderResolveCharacter ); - } - - if( ipc.ProviderChangedItemTooltip != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderChangedItemTooltip ); - } - - if( ipc.ProviderChangedItemClick != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderChangedItemClick ); - } - - if( ipc.ProviderGetChangedItems != null ) - { - ImGui.TextUnformatted( PenumbraIpc.LabelProviderGetChangedItems ); + var value = provider.GetValue( ipc ); + if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label )) + { + ImGui.TextUnformatted( label ); + } } } From d7215adbc3c28a2c64eef35a40e6596295b51d00 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 21:08:43 +0200 Subject: [PATCH 0219/2451] Add dye preview actor and extend tooltip for UseCharacterCollectionInTryOn --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 1 + Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index e4d884f1..2dd3bd85 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -267,6 +267,7 @@ public unsafe partial class PathResolver 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview >= 200 => GetCutsceneName( gameObject ), _ => null, } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 3f71f6d8..88c1fc8d 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -63,7 +63,7 @@ public partial class ConfigWindow "Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.", Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); Checkbox( "Use Character Collections in Try-On Window", - "Use the character collection for your character's name in your try-on window, if it is set.", + "Use the character collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); Checkbox( "Use Character Collections in Inspect Windows", "Use the appropriate character collection for the character you are currently inspecting, based on their name.", From cd8523a75fb6aaf3656c276c813184fd7fc73365 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 21:35:27 +0200 Subject: [PATCH 0220/2451] Update Inheritance tooltips. --- Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs | 7 ++++--- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index ca7becd5..bc9af12f 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -16,7 +16,7 @@ public partial class ConfigWindow { private partial class CollectionsTab { - private const int InheritedCollectionHeight = 10; + private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; // Keep for reuse. @@ -32,6 +32,7 @@ public partial class ConfigWindow private void DrawInheritanceBlock() { using var id = ImRaii.PushId( "##Inheritance" ); + ImGui.TextUnformatted( "The current collection inherits from:" ); DrawCurrentCollectionInheritance(); DrawInheritanceTrashButton(); DrawNewInheritanceSelection(); @@ -185,8 +186,8 @@ public partial class ConfigWindow var tt = inheritance switch { ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", - ModCollection.ValidInheritance.Valid => "Add a new inheritance to the collection.", - ModCollection.ValidInheritance.Self => "Can not inherit from itself.", + ModCollection.ValidInheritance.Valid => "Let the current collection inherit from the selected collection.", + ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", ModCollection.ValidInheritance.Contained => "Already inheriting from the selected collection.", ModCollection.ValidInheritance.Circle => "Inheriting from selected collection would lead to cyclic inheritance.", _ => string.Empty, diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 563017f7..45771ae7 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -170,7 +170,7 @@ public partial class ConfigWindow private void DrawMainSelectors() { var size = new Vector2( -1, - ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight + ImGui.GetTextLineHeightWithSpacing() * (InheritedCollectionHeight + 1) + _window._defaultSpace.Y * 2 + ImGui.GetFrameHeightWithSpacing() * 4 + ImGui.GetStyle().ItemSpacing.Y * 6 ); From 2d200bcabbe254f42b5a638f7c0dd96f1586df0e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 4 Jun 2022 19:39:59 +0000 Subject: [PATCH 0221/2451] [CI] Updating repo.json for refs/tags/0.5.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2847dcff..0e517604 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.4.8.2", - "TestingAssemblyVersion": "0.4.8.2", + "AssemblyVersion": "0.5.0.0", + "TestingAssemblyVersion": "0.5.0.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.4.8.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.4.8.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From afb758e61a9e658d1a30bc3ba18dacd980855330 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Jun 2022 23:10:26 +0200 Subject: [PATCH 0222/2451] Disable additional pap handling for now. --- .../Resolver/PathResolver.Animation.cs | 44 +++++++++---------- .../Interop/Resolver/PathResolver.Data.cs | 13 +++--- Penumbra/Interop/Resolver/PathResolver.cs | 26 +++++------ 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 3c58519f..798929a7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -10,33 +10,33 @@ public unsafe partial class PathResolver { // Probably used when the base idle animation gets loaded. // Make it aware of the correct collection to load the correct pap files. - [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] - public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; - - private ModCollection? _animationLoadCollection; - - private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) - { - _animationLoadCollection = _lastCreatedCollection - ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - CharacterBaseLoadAnimationHook!.Original( drawObject ); - _animationLoadCollection = null; - } + //[Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] + //public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; + // + //private ModCollection? _animationLoadCollection; + // + //private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) + //{ + // _animationLoadCollection = _lastCreatedCollection + // ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + // CharacterBaseLoadAnimationHook!.Original( drawObject ); + // _animationLoadCollection = null; + //} // Probably used when action paps are loaded. // Make it aware of the correct collection to load the correct pap files. - public delegate void PapLoadFunction( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ); + //public delegate void PapLoadFunction( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ); - [Signature( "E8 ?? ?? ?? ?? 0F 10 00 0F 11 06", DetourName = "RandomPapDetour" )] - public Hook< PapLoadFunction >? RandomPapHook; + //[Signature( "E8 ?? ?? ?? ?? 0F 10 00 0F 11 06", DetourName = "RandomPapDetour" )] + //public Hook< PapLoadFunction >? RandomPapHook; - private void RandomPapDetour( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ) - { - _animationLoadCollection = _lastCreatedCollection - ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - RandomPapHook!.Original( drawObject, a2, a3, a4, a5, a6, a7 ); - _animationLoadCollection = null; - } + //private void RandomPapDetour( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ) + //{ + // _animationLoadCollection = _lastCreatedCollection + // ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + // RandomPapHook!.Original( drawObject, a2, a3, a4, a5, a6, a7 ); + // _animationLoadCollection = null; + //} //private void TestFunction() //{ diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 2dd3bd85..97ff113c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -72,28 +72,29 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Enable(); EnableDrawHook?.Enable(); CharacterBaseDestructorHook?.Enable(); - CharacterBaseLoadAnimationHook?.Enable(); - RandomPapHook?.Enable(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; + //CharacterBaseLoadAnimationHook?.Enable(); + //RandomPapHook?.Enable(); } private void DisableDataHooks() { Penumbra.CollectionManager.CollectionChanged -= CheckCollections; - RandomPapHook?.Disable(); - CharacterBaseLoadAnimationHook?.Disable(); CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); + //RandomPapHook?.Disable(); + //CharacterBaseLoadAnimationHook?.Disable(); } private void DisposeDataHooks() { - CharacterBaseLoadAnimationHook?.Dispose(); + CharacterBaseCreateHook?.Dispose(); EnableDrawHook?.Dispose(); CharacterBaseDestructorHook?.Dispose(); - RandomPapHook?.Dispose(); + //RandomPapHook?.Dispose(); + //CharacterBaseLoadAnimationHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index c3b04436..98800933 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -40,7 +40,7 @@ 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 ) + //|| HandlePapFile( type, gamePath, out collection ) || HandleDecalFile( type, gamePath, out collection ); if( !nonDefault ) { @@ -72,18 +72,18 @@ public partial class PathResolver : IDisposable return false; } - private bool HandlePapFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) - { - if( type is ResourceType.Pap or ResourceType.Tmb - && _animationLoadCollection != null ) - { - collection = _animationLoadCollection; - return true; - } - - collection = null; - return false; - } + //private bool HandlePapFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) + //{ + // if( type is ResourceType.Pap or ResourceType.Tmb + // && _animationLoadCollection != null ) + // { + // collection = _animationLoadCollection; + // return true; + // } + // + // collection = null; + // return false; + //} public void Enable() { From cca616bc15e788093ac777d07cb2e727ebb0ac07 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 4 Jun 2022 21:13:57 +0000 Subject: [PATCH 0223/2451] [CI] Updating repo.json for refs/tags/0.5.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 0e517604..03c037cc 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.0", - "TestingAssemblyVersion": "0.5.0.0", + "AssemblyVersion": "0.5.0.1", + "TestingAssemblyVersion": "0.5.0.1", "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.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8cfc605ed30f7a7ce679f20f51663e00d1758827 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 12:27:48 +0200 Subject: [PATCH 0224/2451] Fix face decal changed item identification. --- Penumbra.GameData/ObjectIdentification.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index e25e1ca2..16362e37 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -282,15 +282,21 @@ internal class ObjectIdentification : IObjectIdentifier var (gender, race) = info.GenderRace.Split(); var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; - if( info.CustomizationType == CustomizationType.Skin ) + switch( info.CustomizationType ) { - set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; - } - else - { - var customizationString = - $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; - set[ customizationString ] = null; + case CustomizationType.Skin: + set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; + break; + case CustomizationType.DecalFace: + set[ $"Customization: Face Decal {info.PrimaryId}" ] = null; + break; + default: + { + var customizationString = + $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; + set[ customizationString ] = null; + break; + } } break; From 7409d0bc2f3ee836b3c763a2dbb3c6895be76f15 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 12:28:11 +0200 Subject: [PATCH 0225/2451] Fix breaking on empty mod paths and add Url field --- Penumbra/Collections/ModCollection.Inheritance.cs | 1 - Penumbra/Import/TexToolsImporter.ModPack.cs | 8 ++++---- Penumbra/Import/TexToolsStructs.cs | 12 +++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 9b2706ca..ba621337 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Logging; using OtterGui.Filesystem; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 0b461860..f15053cf 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -92,7 +92,7 @@ public partial class TexToolsImporter _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" - : modList.Description, null, null ); + : modList.Description, modList.Version, modList.Url ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); @@ -135,7 +135,7 @@ public partial class TexToolsImporter _currentModName = modList.Name; _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); - Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, null ); + Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); if( _currentNumOptions == 0 ) { @@ -226,10 +226,10 @@ public partial class TexToolsImporter State = ImporterState.ExtractingModFiles; _currentFileIdx = 0; - _currentNumFiles = mods.Count; + _currentNumFiles = mods.Count(m => m.FullPath.Length > 0); // Extract each SimpleMod into the new mod folder - foreach( var simpleMod in mods ) + foreach( var simpleMod in mods.Where(m => m.FullPath.Length > 0 ) ) { ExtractMod( outDirectory, simpleMod ); ++_currentFileIdx; diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/TexToolsStructs.cs index bb2ba8d9..51b73b90 100644 --- a/Penumbra/Import/TexToolsStructs.cs +++ b/Penumbra/Import/TexToolsStructs.cs @@ -58,6 +58,7 @@ internal class ExtendedModPack public string Author = DefaultTexToolsData.Author; public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; + public string Url = string.Empty; public ModPackPage[] ModPackPages = Array.Empty< ModPackPage >(); public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); } @@ -65,10 +66,11 @@ internal class ExtendedModPack [Serializable] internal class SimpleModPack { - public string TtmpVersion = string.Empty; - public string Name = DefaultTexToolsData.Name; - public string Author = DefaultTexToolsData.Author; - public string Version = string.Empty; - public string Description = DefaultTexToolsData.Description; + public string TtmpVersion = string.Empty; + public string Name = DefaultTexToolsData.Name; + public string Author = DefaultTexToolsData.Author; + public string Version = string.Empty; + public string Description = DefaultTexToolsData.Description; + public string Url = string.Empty; public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); } \ No newline at end of file From 3b2876a6e4657a025e3ed7230febbc9e48521357 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 12:33:50 +0200 Subject: [PATCH 0226/2451] Maybe fix migration for metadata containing mods, also fix negative values in mod collection settings causing problems. --- Penumbra/Configuration.Migration.cs | 6 ++++-- Penumbra/Interop/CharacterUtility.cs | 7 +++++++ Penumbra/Mods/Subclasses/ModSettings.cs | 7 ++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index c952e687..2b0b1586 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -70,6 +70,8 @@ public partial class Configuration return; } + // Ensure the right meta files are loaded. + Penumbra.CharacterUtility.LoadCharacterResources(); ResettleSortOrder(); ResettleCollectionSettings(); ResettleForcedCollection(); @@ -180,8 +182,8 @@ public partial class Configuration var modName = ( string )setting[ "FolderName" ]!; var enabled = ( bool )setting[ "Enabled" ]!; var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, long > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, long > >(); dict[ modName ] = new ModSettings.SavedSettings() { diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index c57d6043..5cd71682 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -11,6 +11,13 @@ public unsafe class CharacterUtility : IDisposable [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; + // Only required for migration anymore. + [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] + public readonly Action< IntPtr >? LoadCharacterResourcesFunc; + + public void LoadCharacterResources() + => LoadCharacterResourcesFunc?.Invoke( ( IntPtr )_characterUtilityAddress ); + public Structs.CharacterUtility* Address => *_characterUtilityAddress; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 50341ae9..a6bd27de 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -149,7 +149,7 @@ public class ModSettings // A simple struct conversion to easily save settings by name instead of value. public struct SavedSettings { - public Dictionary< string, uint > Settings; + public Dictionary< string, long > Settings; public int Priority; public bool Enabled; @@ -165,7 +165,7 @@ public class ModSettings { Priority = settings.Priority; Enabled = settings.Enabled; - Settings = new Dictionary< string, uint >( mod.Groups.Count ); + Settings = new Dictionary< string, long >( mod.Groups.Count ); settings.AddMissingSettings( mod.Groups.Count ); foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) @@ -183,7 +183,8 @@ public class ModSettings { if( Settings.TryGetValue( group.Name, out var config ) ) { - var actualConfig = FixSetting( group, config ); + var castConfig = ( uint )Math.Clamp( config, 0, uint.MaxValue ); + var actualConfig = FixSetting( group, castConfig ); list.Add( actualConfig ); if( actualConfig != config ) { From b71552607e9d13d17840b01a836f0d1707164232 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 12:38:11 +0200 Subject: [PATCH 0227/2451] Delete the penumbrametatmp folder on migration. --- Penumbra/Configuration.Migration.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 2b0b1586..3df24ae7 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -63,6 +63,7 @@ public partial class Configuration // The forced collection was removed due to general inheritance. // Sort Order was moved to a separate file and may contain empty folders. // Active collections in general were moved to their own file. + // Delete the penumbrametatmp folder if it exists. private void Version1To2() { if( _config.Version != 1 ) @@ -71,6 +72,7 @@ public partial class Configuration } // Ensure the right meta files are loaded. + DeleteMetaTmp(); Penumbra.CharacterUtility.LoadCharacterResources(); ResettleSortOrder(); ResettleCollectionSettings(); @@ -78,6 +80,22 @@ public partial class Configuration _config.Version = 2; } + private void DeleteMetaTmp() + { + var path = Path.Combine( _config.ModDirectory, "penumbrametatmp" ); + if( Directory.Exists( path ) ) + { + try + { + Directory.Delete( path, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" ); + } + } + } + private void ResettleForcedCollection() { ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; From 3014f7b24654964147e67d28ce4b5634d180bea3 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Sun, 5 Jun 2022 12:50:39 +0200 Subject: [PATCH 0228/2451] Update release.yml --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82e13457..32c21a39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Build run: | $ver = '${{ github.ref }}' -replace 'refs/tags/','' - invoke-expression 'dotnet build --no-restore --configuration Release --nologo -p:Version=$ver -p:FileVersion=$ver' + invoke-expression 'dotnet build --no-restore --configuration Release --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver' - name: write version into json run: | $ver = '${{ github.ref }}' -replace 'refs/tags/','' @@ -79,4 +79,4 @@ jobs: git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true - git push origin master || true \ No newline at end of file + git push origin master || true From e52fca05d99ba2a5915a23e1ef2f34e56a0809ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 13:10:29 +0200 Subject: [PATCH 0229/2451] Add an option to allow file selector folders to be expanded or collapsed by default. --- OtterGui | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 3 +++ Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 6 ++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3679cb37..f401cea4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3679cb37d5cc04351c064b1372a6eac51c7375a8 +Subproject commit f401cea47e45d12e09d1668187e3bb2781af21dd diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 2e39f4d3..4848d337 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -43,6 +43,7 @@ public partial class Configuration : IPluginConfiguration public bool ScaleModSelector { get; set; } = false; public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool OpenFoldersByDefault { get; set; } = false; public bool ShowAdvanced { get; set; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ff606509..20422f26 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -76,6 +76,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override uint FolderLineColor => ColorId.FolderLine.Value(); + protected override bool FoldersDefaultOpen + => Penumbra.Config.OpenFoldersByDefault; + protected override void DrawPopups() { _fileManager.Draw(); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 88c1fc8d..05527732 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -79,6 +79,12 @@ public partial class ConfigWindow DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); + Checkbox( "Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", + Penumbra.Config.OpenFoldersByDefault, v => + { + Penumbra.Config.OpenFoldersByDefault = v; + _window._selector.SetFilterDirty(); + } ); ImGui.Dummy( _window._defaultSpace ); DrawDefaultModImportPath(); DrawDefaultModAuthor(); From 0a81e39690eeb26c83b3a44a176dcfc1b0425d9b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 13:10:44 +0200 Subject: [PATCH 0230/2451] fixup! Maybe fix migration for metadata containing mods, also fix negative values in mod collection settings causing problems. --- Penumbra/Interop/CharacterUtility.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 5cd71682..d16b3094 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -11,12 +11,15 @@ public unsafe class CharacterUtility : IDisposable [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; + // Only required for migration anymore. + public delegate void LoadResources( Structs.CharacterUtility* address ); + [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] - public readonly Action< IntPtr >? LoadCharacterResourcesFunc; + public readonly LoadResources? LoadCharacterResourcesFunc; public void LoadCharacterResources() - => LoadCharacterResourcesFunc?.Invoke( ( IntPtr )_characterUtilityAddress ); + => LoadCharacterResourcesFunc?.Invoke( Address ); public Structs.CharacterUtility* Address => *_characterUtilityAddress; From 2746f7ea4f31a903327a18bd2049ac85805e8921 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 13:16:16 +0200 Subject: [PATCH 0231/2451] Add option to always open mod importer at default path. --- Penumbra/Configuration.cs | 1 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 6 +++--- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 4848d337..a4dc6be1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -51,6 +51,7 @@ public partial class Configuration : IPluginConfiguration public bool EnableHttpApi { get; set; } public string DefaultModImportPath { get; set; } = string.Empty; + public bool AlwaysOpenDefaultImport { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; public Dictionary< ColorId, uint > Colors { get; set; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 20422f26..560fe5be 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -174,9 +174,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod return; } - var modPath = _hasSetFolder ? null - : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; + var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null + : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath + : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => { diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 05527732..7a735d06 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -86,6 +86,9 @@ public partial class ConfigWindow _window._selector.SetFilterDirty(); } ); ImGui.Dummy( _window._defaultSpace ); + Checkbox( "Always Open Import at Default Directory", + "Open the import window at the location specified here every time, forgetting your previous path.", + Penumbra.Config.AlwaysOpenDefaultImport, v => Penumbra.Config.AlwaysOpenDefaultImport = v ); DrawDefaultModImportPath(); DrawDefaultModAuthor(); From 82ac63985420607f5126ad46f8288adc89a6392a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 5 Jun 2022 11:18:52 +0000 Subject: [PATCH 0232/2451] [CI] Updating repo.json for refs/tags/0.5.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 03c037cc..ec673964 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.1", - "TestingAssemblyVersion": "0.5.0.1", + "AssemblyVersion": "0.5.0.2", + "TestingAssemblyVersion": "0.5.0.2", "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.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6dbf487c99408942486d518ddc93f9abbd7be9f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 14:49:12 +0200 Subject: [PATCH 0233/2451] Fix collections wrongly becoming active on rediscover. --- Penumbra/Collections/CollectionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index b222d445..76515f0c 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -202,7 +202,7 @@ public partial class ModCollection } // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach( var collection in this ) + foreach( var collection in this.Where( c => c.HasCache ) ) { collection.ForceCacheUpdate(); } From a798eabf67968357928b038a0fadf373705e3c05 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 14:49:24 +0200 Subject: [PATCH 0234/2451] Select last imported mod. --- Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 560fe5be..8ade533f 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -104,6 +104,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod while( _modsToAdd.TryDequeue( out var dir ) ) { Penumbra.ModManager.AddMod( dir ); + var mod = Penumbra.ModManager.LastOrDefault(); + if( mod != null ) + { + SelectByValue( mod ); + } } } From 48a443921edbba0fcf8f5f651645b0d1edcaaf61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 15:38:38 +0200 Subject: [PATCH 0235/2451] Fix bug with expanding IMC files. --- Penumbra/Collections/ModCollection.Cache.cs | 9 ++++----- Penumbra/Meta/Files/ImcFile.cs | 12 +++++++++--- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index efc10988..925a37d4 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -253,16 +253,15 @@ public partial class ModCollection if( addMetaChanges ) { ++ChangeCounter; + if( mod.TotalManipulations > 0 ) + { + AddMetaFiles(); + } if( _collection == Penumbra.CollectionManager.Default ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); } - - if( mod.TotalManipulations > 0 ) - { - AddMetaFiles(); - } } } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index e0a5c7c9..965df0f9 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -134,7 +134,7 @@ public unsafe class ImcFile : MetaBaseFile var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); for( var i = oldCount + 1; i < numVariants + 1; ++i ) { - Functions.MemCpyUnchecked( defaultPtr + i, defaultPtr, NumParts * sizeof( ImcEntry ) ); + Functions.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); } PluginLog.Verbose( "Expanded IMC {Path} from {Count} to {NewCount} variants.", Path, oldCount, numVariants ); @@ -197,9 +197,10 @@ public unsafe class ImcFile : MetaBaseFile } } - public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx ) + public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists ) { var file = Dalamud.GameData.GetFile( path.ToString() ); + exists = false; if( file == null ) { throw new Exception(); @@ -208,7 +209,12 @@ public unsafe class ImcFile : MetaBaseFile fixed( byte* ptr = file.Data ) { var entry = VariantPtr( ptr, PartIndex( slot ), variantIdx ); - return entry == null ? new ImcEntry() : *entry; + if( entry != null ) + { + exists = true; + return *entry; + } + return new ImcEntry(); } } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index c3858564..1c0279d5 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -104,7 +104,7 @@ public partial class MetaManager return false; } - var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant ); + var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); var manip = m with { Entry = def }; if( !manip.Apply( file ) ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 96cfd632..7cdb10ea 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -286,7 +286,7 @@ public partial class ModEditWindow { try { - return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ); + return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant, out _ ); } catch { From 78b931ec445a19e902149b7b3b08f12c0409843a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 16:46:41 +0200 Subject: [PATCH 0236/2451] Disallow docking for the penumbra main window. --- Penumbra/UI/Classes/ModFileSystemSelector.cs | 1 + Penumbra/UI/ConfigWindow.cs | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 8ade533f..4063850d 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -183,6 +183,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; + _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => { if( s ) diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index e8f610de..099c19b0 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -35,11 +35,12 @@ public sealed partial class ConfigWindow : Window, IDisposable _effectiveTab = new EffectiveTab(); _debugTab = new DebugTab( this ); _resourceTab = new ResourceTab( this ); + Flags |= ImGuiWindowFlags.NoDocking; - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; - Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2( 800, 600 ), From 40a6ec6010e19a51288a5094c29d703463788fe5 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 5 Jun 2022 14:50:54 +0000 Subject: [PATCH 0237/2451] [CI] Updating repo.json for refs/tags/0.5.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index ec673964..720e1838 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.2", - "TestingAssemblyVersion": "0.5.0.2", + "AssemblyVersion": "0.5.0.3", + "TestingAssemblyVersion": "0.5.0.3", "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.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 513a2780f14a3ea104ae1d64022dca08d070db36 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 18:38:32 +0200 Subject: [PATCH 0238/2451] Add updating of changed items to mod changes. --- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 35 ++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 16df63e0..38ddc97f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -331,22 +331,31 @@ public sealed partial class Mod } } + bool ComputeChangedItems() + { + mod.ComputeChangedItems(); + return true; + } + // State can not change on adding groups, as they have no immediate options. var unused = type switch { - ModOptionChangeType.GroupAdded => mod.SetCounts(), - ModOptionChangeType.GroupDeleted => mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => mod.SetCounts(), - ModOptionChangeType.OptionDeleted => mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ), - ModOptionChangeType.OptionSwapsChanged => 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ), - ModOptionChangeType.OptionMetaChanged => 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ), - ModOptionChangeType.DisplayChange => false, - _ => false, + ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() + & ( 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ) ), + ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() + & ( 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ) ), + ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() + & ( 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ) ), + ModOptionChangeType.DisplayChange => false, + _ => false, }; } } From f4ba14de3c8ddf9b6977c57b25d7bd03addea3d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 18:39:02 +0200 Subject: [PATCH 0239/2451] Add Everything option and coloring to active filters. --- Penumbra/UI/Classes/Colors.cs | 1 + .../Classes/ModFileSystemSelector.Filters.cs | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 5f3844ec..567bd542 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -27,6 +27,7 @@ public static class Colors public const uint MetaInfoText = 0xAAFFFFFF; public const uint RedTableBgTint = 0x40000080; public const uint DiscordColor = 0xFFDA8972; + public const uint FilterActive = 0x807070FF; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 143e7df1..039262fb 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -21,10 +22,10 @@ public partial class ModFileSystemSelector public ColorId Color; } - private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; private void SetFilterTooltip() { @@ -252,13 +253,30 @@ public partial class ModFileSystemSelector var pos = ImGui.GetCursorPos(); var remainingWidth = width - ImGui.GetFrameHeight(); var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); + + var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; + ImGui.SetCursorPos( comboPos ); + // Draw combo button + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.FilterActive, !everything ); using var combo = ImRaii.Combo( "##filterCombo", string.Empty, ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); + color.Pop(); if( combo ) { + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { Y = 3 * ImGuiHelpers.GlobalScale } ); var flags = ( int )_stateFilter; + + + if( ImGui.Checkbox( "Everything", ref everything ) ) + { + _stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0; + SetFilterDirty(); + } + + ImGui.Dummy( new Vector2( 0, 5 * ImGuiHelpers.GlobalScale ) ); foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) { if( ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ) ) @@ -270,7 +288,13 @@ public partial class ModFileSystemSelector } combo.Dispose(); - ImGuiUtil.HoverTooltip( "Filter mods for their activation status." ); + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { + _stateFilter = ModFilterExtensions.UnfilteredStateMods; + SetFilterDirty(); + } + + ImGuiUtil.HoverTooltip( "Filter mods for their activation status.\nRight-Click to clear all filters." ); ImGui.SetCursorPos( pos ); return remainingWidth; } From 10c4dbc1f88d5516bfcba7bd756f43846038c229 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Jun 2022 19:28:52 +0200 Subject: [PATCH 0240/2451] Change default for preferring owners. --- Penumbra/Configuration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a4dc6be1..5888a8e1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -26,7 +26,7 @@ public partial class Configuration : IPluginConfiguration public bool UseCharacterCollectionInInspect { get; set; } = true; public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool PreferNamedCollectionsOverOwners { get; set; } = false; + public bool PreferNamedCollectionsOverOwners { get; set; } = true; #if DEBUG public bool DebugMode { get; set; } = true; From d2b969d9967a7142cd8747869f6d826b1c88486c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 6 Jun 2022 20:35:29 +0200 Subject: [PATCH 0241/2451] Add Weapon Reload resolving. --- .../Interop/Resolver/PathResolver.Data.cs | 23 ++++++++++++++++-- .../Interop/Resolver/PathResolver.Resolve.cs | 24 ++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 97ff113c..53ed569f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -62,16 +62,34 @@ public unsafe partial class PathResolver private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) { + var oldObject = LastGameObject; LastGameObject = ( GameObject* )gameObject; EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); - LastGameObject = null; + LastGameObject = oldObject; } + // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, + // so we use that. + public delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); + + [Signature( "E8 ?? ?? ?? ?? 44 8B 9F" )] + public Hook< WeaponReloadFunc >? WeaponReloadHook; + + public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) + { + var oldGame = LastGameObject; + LastGameObject = *( GameObject** )( a1 + 8 ); + WeaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); + LastGameObject = oldGame; + } + + private void EnableDataHooks() { CharacterBaseCreateHook?.Enable(); EnableDrawHook?.Enable(); CharacterBaseDestructorHook?.Enable(); + WeaponReloadHook?.Enable(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; //CharacterBaseLoadAnimationHook?.Enable(); //RandomPapHook?.Enable(); @@ -80,6 +98,7 @@ public unsafe partial class PathResolver private void DisableDataHooks() { Penumbra.CollectionManager.CollectionChanged -= CheckCollections; + WeaponReloadHook?.Disable(); CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); @@ -89,7 +108,7 @@ public unsafe partial class PathResolver private void DisposeDataHooks() { - + WeaponReloadHook?.Dispose(); CharacterBaseCreateHook?.Dispose(); EnableDrawHook?.Dispose(); CharacterBaseDestructorHook?.Dispose(); diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index 7338f9e4..a6124a93 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -12,6 +12,7 @@ public unsafe partial class PathResolver // Humans private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) => ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); + private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) => ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) ); @@ -149,19 +150,24 @@ public unsafe partial class PathResolver [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path ) { + var parent = FindParent( drawObject, out var collection ); + if( parent != null ) + { + return ResolvePathDetour( collection, path ); + } + var parentObject = ( ( DrawObject* )drawObject )->Object.ParentObject; if( parentObject == null && LastGameObject != null ) { - var collection = IdentifyCollection( LastGameObject ); - return ResolvePathDetour( collection, path ); - } - else - { - var parent = FindParent( ( IntPtr )parentObject, out var collection ); - return ResolvePathDetour( parent == null - ? Penumbra.CollectionManager.Default - : collection, path ); + var c2 = IdentifyCollection( LastGameObject ); + DrawObjectToObject[ drawObject ] = ( c2, LastGameObject->ObjectIndex ); + return ResolvePathDetour( c2, path ); } + + parent = FindParent( ( IntPtr )parentObject, out collection ); + return ResolvePathDetour( parent == null + ? Penumbra.CollectionManager.Default + : collection, path ); } // Just add or remove the resolved path. From caf19f24cbc5b3e5bb1f2095d5db296cfcc2774a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 6 Jun 2022 21:17:27 +0200 Subject: [PATCH 0242/2451] Fix removal of EST bones. --- Penumbra/Meta/Files/EstFile.cs | 9 +++++++-- Penumbra/UI/ConfigWindow.DebugTab.cs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index cd67a6ba..822286d0 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -88,9 +88,14 @@ public sealed unsafe class EstFile : MetaBaseFile control[ i ] = control[ i + 1 ]; } - for( var i = 0; i < Count; ++i ) + for( var i = 0; i < idx; ++i ) { - entries[ i - 2 ] = entries[ i + 1 ]; + entries[ i - 2 ] = entries[ i ]; + } + + for( var i = idx; i < Count - 1; ++i ) + { + entries[i - 2] = entries[i + 1]; } entries[ Count - 3 ] = 0; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 879b4385..a54bf137 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -232,7 +232,7 @@ public partial class ConfigWindow var (data, length) = resource->GetData(); if( data != IntPtr.Zero && length > 0 ) { - ImGui.SetClipboardText( string.Join( " ", + ImGui.SetClipboardText( string.Join( "\n", new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); } } @@ -245,7 +245,7 @@ public partial class ConfigWindow ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); if( ImGui.IsItemClicked() ) { - ImGui.SetClipboardText( string.Join( " ", + ImGui.SetClipboardText( string.Join( "\n", new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); } From c97b8e8e9aaff77eb18adb762a6f8c1c6a39c60b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 6 Jun 2022 22:23:51 +0200 Subject: [PATCH 0243/2451] Add option to fix the penumbra window. --- Penumbra/Configuration.cs | 2 +- Penumbra/Penumbra.cs | 6 ++++++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 8 ++++++++ Penumbra/UI/ConfigWindow.cs | 12 ++++++++---- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 5888a8e1..83f0982d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -45,7 +45,7 @@ public partial class Configuration : IPluginConfiguration public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; - + public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5d2dabda..8ded28f1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -360,6 +360,12 @@ public class Penumbra : IDisposable : modsDisabled ); break; } + case "unfix": + { + Config.FixMainWindow = false; + _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + break; + } case "collection": { if( args.Length == 2 ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 6db24860..7a3080ef 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -37,6 +37,14 @@ public partial class ConfigWindow DrawEnabledBox(); DrawShowAdvancedBox(); + Checkbox( "Fix Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => + { + Penumbra.Config.FixMainWindow = v; + _window.Flags = v + ? _window.Flags | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize + : _window.Flags & ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + } ); + ImGui.NewLine(); DrawRootFolder(); DrawRediscoverButton(); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 099c19b0..fa51d332 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -36,11 +36,15 @@ public sealed partial class ConfigWindow : Window, IDisposable _debugTab = new DebugTab( this ); _resourceTab = new ResourceTab( this ); Flags |= ImGuiWindowFlags.NoDocking; + if( Penumbra.Config.FixMainWindow ) + { + Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; + } - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; - Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2( 800, 600 ), From 7390f97d811280fbcbad3c2102016eb04ea2434e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 6 Jun 2022 22:24:16 +0200 Subject: [PATCH 0244/2451] Add free drive space to support info. --- Penumbra/Penumbra.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ded28f1..aacc1fd7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -12,6 +12,7 @@ using EmbedIO; using EmbedIO.WebApi; using ImGuiNET; using Lumina.Excel.GeneratedSheets; +using OtterGui; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; @@ -407,15 +408,18 @@ public class Penumbra : IDisposable public static string GatherSupportInformation() { - var sb = new StringBuilder( 10240 ); + var sb = new StringBuilder( 10240 ); + var exists = Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ); + var drive = exists ? new DriveInfo( new DirectoryInfo( Config.ModDirectory ).Root.FullName ) : null; sb.AppendLine( "**Settings**" ); sb.AppendFormat( "> **`Plugin Version: `** {0}\n", Version ); sb.AppendFormat( "> **`Commit Hash: `** {0}\n", CommitHash ); sb.AppendFormat( "> **`Enable Mods: `** {0}\n", Config.EnableMods ); sb.AppendFormat( "> **`Enable Sound Modification: `** {0}\n", Config.DisableSoundStreaming ); sb.AppendFormat( "> **`Enable HTTP API: `** {0}\n", Config.EnableHttpApi ); - sb.AppendFormat( "> **`Root Directory: `** {0}, {1}\n", Config.ModDirectory, - Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ) ? "Exists" : "Not Existing" ); + sb.AppendFormat( "> **`Root Directory: `** `{0}`, {1}\n", Config.ModDirectory, exists ? "Exists" : "Not Existing" ); + sb.AppendFormat( "> **`Free Drive Space: `** {0}\n", + drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" ); sb.AppendLine( "**Mods**" ); sb.AppendFormat( "> **`Installed Mods: `** {0}\n", ModManager.Count ); sb.AppendFormat( "> **`Mods with Config: `** {0}\n", ModManager.Count( m => m.HasOptions ) ); @@ -443,7 +447,7 @@ public class Penumbra : IDisposable c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); sb.AppendLine( "**Collections**" ); - sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count ); + sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); sb.AppendFormat( "> **`Default Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Default ), CollectionManager.Default.Index ); From 2aed2528206755c91b032274965c47b4fb3f8bd2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jun 2022 12:39:57 +0200 Subject: [PATCH 0245/2451] Add default mod import folder. --- OtterGui | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 63 +++++++++++++++++-- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 2 +- .../UI/ConfigWindow.SettingsTab.General.cs | 19 ++++++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index f401cea4..a9a5b2a4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f401cea47e45d12e09d1668187e3bb2781af21dd +Subproject commit a9a5b2a4bbf061d9cfed5234ca731bd2d94bcb96 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 83f0982d..8d120b67 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -44,6 +44,7 @@ public partial class Configuration : IPluginConfiguration public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; + public string DefaultImportFolder { get; set; } = string.Empty; public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 4063850d..14dfd804 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; @@ -14,6 +9,11 @@ using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Import; using Penumbra.Mods; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Numerics; namespace Penumbra.UI.Classes; @@ -31,6 +31,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod SubscribeRightClickFolder( DisableDescendants, 10 ); SubscribeRightClickFolder( InheritDescendants, 15 ); SubscribeRightClickFolder( OwnDescendants, 15 ); + SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); + SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); AddButton( AddNewModButton, 0 ); AddButton( AddImportModButton, 1 ); AddButton( AddHelpButton, 2 ); @@ -107,6 +109,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod var mod = Penumbra.ModManager.LastOrDefault(); if( mod != null ) { + MoveModToDefaultDirectory( mod ); SelectByValue( mod ); } } @@ -154,6 +157,28 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) + { + if( ImGui.MenuItem( "Set As Default Import Folder" ) ) + { + var newName = folder.FullName(); + if( newName != Penumbra.Config.DefaultImportFolder ) + { + Penumbra.Config.DefaultImportFolder = newName; + Penumbra.Config.Save(); + } + } + } + + private static void ClearDefaultImportFolder() + { + if( ImGui.MenuItem( "Clear Default Import Folder" ) && Penumbra.Config.DefaultImportFolder.Length > 0 ) + { + Penumbra.Config.DefaultImportFolder = string.Empty; + Penumbra.Config.Save(); + } + } + // Add custom buttons. private string _newModName = string.Empty; @@ -385,6 +410,34 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + // If a default import folder is setup, try to move the given mod in there. + // If the folder does not exist, create it if possible. + private void MoveModToDefaultDirectory( Mod mod ) + { + if( Penumbra.Config.DefaultImportFolder.Length == 0 ) + { + return; + } + + try + { + var leaf = FileSystem.Root.GetChildren( SortMode.Lexicographical ) + .FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod ); + if( leaf == null ) + { + throw new Exception( "Mod was not found at root." ); + } + + var folder = FileSystem.FindOrCreateAllFolders( Penumbra.Config.DefaultImportFolder ); + FileSystem.Move( leaf, folder ); + } + catch( Exception e ) + { + PluginLog.Warning( + $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}" ); + } + } + private static void DrawHelpPopup() { ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 33.5f * ImGui.GetTextLineHeightWithSpacing() ), () => diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 7acbbfb2..20954498 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -126,7 +126,7 @@ public partial class ConfigWindow // on the top-right corner of the window/tab. private void DrawRemoveSettings() { - const string text = "Remove Settings"; + const string text = "Inherit Settings"; if( _inherited || _emptySetting ) { return; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 7a735d06..e69fa219 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -91,6 +91,7 @@ public partial class ConfigWindow Penumbra.Config.AlwaysOpenDefaultImport, v => Penumbra.Config.AlwaysOpenDefaultImport = v ); DrawDefaultModImportPath(); DrawDefaultModAuthor(); + DrawDefaultModImportFolder(); ImGui.NewLine(); } @@ -225,5 +226,23 @@ public partial class ConfigWindow ImGuiUtil.LabeledHelpMarker( "Default Mod Author", "Set the default author stored for newly created mods." ); } + + private void DrawDefaultModImportFolder() + { + var tmp = Penumbra.Config.DefaultImportFolder; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( ImGui.InputText( "##defaultImportFolder", ref tmp, 64 ) ) + { + Penumbra.Config.DefaultImportFolder = tmp; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Penumbra.Config.Save(); + } + + ImGuiUtil.LabeledHelpMarker( "Default Mod Import Folder", + "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root." ); + } } } \ No newline at end of file From ecedacfddbd979a6dd95e427ed35e406087b1f70 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 7 Jun 2022 10:42:10 +0000 Subject: [PATCH 0246/2451] [CI] Updating repo.json for refs/tags/0.5.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 720e1838..34df39e1 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.3", - "TestingAssemblyVersion": "0.5.0.3", + "AssemblyVersion": "0.5.0.4", + "TestingAssemblyVersion": "0.5.0.4", "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.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 765da6d5184f0ef9381a8e64fef9d819d8ef0244 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jun 2022 15:37:36 +0200 Subject: [PATCH 0247/2451] Actually bind debug tab to debug mode toggle. --- Penumbra/UI/ConfigWindow.DebugTab.cs | 6 +----- Penumbra/UI/ConfigWindow.ResourceTab.cs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index a54bf137..31b4b405 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -27,17 +27,13 @@ public partial class ConfigWindow #if DEBUG private const string DebugVersionString = "(Debug)"; - private const bool DefaultVisibility = true; #else private const string DebugVersionString = "(Release)"; - private const bool DefaultVisibility = false; #endif - public bool DebugTabVisible = DefaultVisibility; - public void Draw() { - if( !DebugTabVisible ) + if( !Penumbra.Config.DebugMode ) { return; } diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 8d5cdfae..b4deea18 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -30,7 +30,7 @@ public partial class ConfigWindow // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. public void Draw() { - if( !_window._debugTab.DebugTabVisible ) + if( !Penumbra.Config.DebugMode ) { return; } From fcb29f23c69f1a6422730fc350a34f7ac27ec290 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 7 Jun 2022 13:42:25 +0000 Subject: [PATCH 0248/2451] [CI] Updating repo.json for refs/tags/0.5.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 34df39e1..07e12e72 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.4", - "TestingAssemblyVersion": "0.5.0.4", + "AssemblyVersion": "0.5.0.5", + "TestingAssemblyVersion": "0.5.0.5", "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.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.4/Penumbra.zip", + "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", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 35cff163f8667c02de688c655294db62347b813f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 8 Jun 2022 15:22:10 +0200 Subject: [PATCH 0249/2451] Give error information on IMC problems. --- Penumbra/Meta/Manager/MetaManager.Imc.cs | 35 +++++++++++------- Penumbra/Penumbra.cs | 6 +++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 47 +++++++++++++++--------- Penumbra/UI/ConfigWindow.cs | 21 ++++++++++- 4 files changed, 76 insertions(+), 33 deletions(-) diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 1c0279d5..23bdf125 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -70,24 +70,33 @@ public partial class MetaManager #if USE_IMC Manipulations[ m ] = mod; var path = m.GamePath(); - if( !Files.TryGetValue( path, out var file ) ) + try { - file = new ImcFile( path ); - } + if( !Files.TryGetValue( path, out var file ) ) + { + file = new ImcFile( path ); + } - if( !m.Apply( file ) ) + if( !m.Apply( file ) ) + { + return false; + } + + Files[ path ] = file; + var fullPath = CreateImcPath( path ); + if( _collection.HasCache ) + { + _collection.ForceFile( path, fullPath ); + } + + return true; + } + catch( Exception e ) { + ++Penumbra.ImcExceptions; + PluginLog.Error( $"Could not apply IMC Manipulation:\n{e}" ); return false; } - - Files[ path ] = file; - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } - - return true; #else return false; #endif diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index aacc1fd7..1ae066db 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -69,6 +69,7 @@ public class Penumbra : IDisposable public static SimpleRedirectManager Redirects { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; + public static int ImcExceptions = 0; public readonly ResourceLogger ResourceLogger; @@ -147,6 +148,10 @@ public class Penumbra : IDisposable Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); SubscribeItemLinks(); + if( ImcExceptions > 0 ) + { + PluginLog.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); + } } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) @@ -429,6 +434,7 @@ public class Penumbra : IDisposable ModManager.Sum( m => m.TotalSwapCount ) ); sb.AppendFormat( "> **`Mods with Meta Manipulations:`** {0}, Total {1}\n", ModManager.Count( m => m.TotalManipulations > 0 ), ModManager.Sum( m => m.TotalManipulations ) ); + sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions ); string CollectionName( ModCollection c ) => c == ModCollection.Empty ? ModCollection.Empty.Name : c.Name.Length >= 2 ? c.Name[ ..2 ] : c.Name; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 7a3080ef..a7085b0f 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -55,7 +55,7 @@ public partial class ConfigWindow DrawAdvancedSettings(); _dialogManager.Draw(); - DrawDiscordButton(); + DrawSupportButtons(); } // Changing the base mod directory. @@ -209,26 +209,11 @@ public partial class ConfigWindow ImGui.NewLine(); } - private static void DrawDiscordButton() + public static void DrawDiscordButton( float width ) { - const string help = "Copy Support Info to Clipboard"; const string discord = "Join Discord for Support"; const string address = @"https://discord.gg/kVva7DHV4r"; - var width = ImGui.CalcTextSize( help ).X + ImGui.GetStyle().FramePadding.X * 2; - if( ImGui.GetScrollMaxY() > 0 ) - { - width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; - } - - ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( help ) ) - { - var text = Penumbra.GatherSupportInformation(); - ImGui.SetClipboardText( text ); - } - - ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, 0 ) ); - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); if( ImGui.Button( discord, new Vector2( width, 0 ) ) ) { try @@ -247,5 +232,31 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( $"Open {address}" ); } + + private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; + + public static void DrawSupportButton() + { + if( ImGui.Button( SupportInfoButtonText ) ) + { + var text = Penumbra.GatherSupportInformation(); + ImGui.SetClipboardText( text ); + } + } + + private static void DrawSupportButtons() + { + var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; + if( ImGui.GetScrollMaxY() > 0 ) + { + width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; + } + + ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, ImGui.GetFrameHeightWithSpacing() ) ); + DrawSupportButton(); + + ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, 0 ) ); + DrawDiscordButton( width ); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index fa51d332..ff7fa751 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Logging; using ImGuiNET; using OtterGui.Raii; -using Penumbra.Mods; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -56,6 +54,25 @@ public sealed partial class ConfigWindow : Window, IDisposable { try { + if( Penumbra.ImcExceptions > 0 ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextWrapped( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + + "Please use the Launcher's Repair Game Files function to repair your client installation." ); + color.Pop(); + + ImGui.NewLine(); + ImGui.NewLine(); + SettingsTab.DrawDiscordButton( 0 ); + ImGui.SameLine(); + SettingsTab.DrawSupportButton(); + return; + } + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); SetupSizes(); _settingsTab.Draw(); From d89c1abc3b5b0405a89fa6d560ff74b6c40f2f32 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 8 Jun 2022 15:22:28 +0200 Subject: [PATCH 0250/2451] Move advanced editing to a tab button. --- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 34 ++++++++++------------- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 16 ++++++++++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index a878e293..c075af5a 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -171,24 +171,6 @@ public partial class ConfigWindow { Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); } - - if( ImGui.Button( "Edit Mod Details", reducedSize ) ) - { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( -1, 0 ); - _window.ModEditPopup.IsOpen = true; - } - - ImGui.SameLine(); - fileExists = File.Exists( _mod.DefaultFile ); - tt = fileExists - ? "Open the default option json file in the text editor of your choice." - : "The default option json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", _window._iconButtonSize, tt, - !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( _mod.DefaultFile ) { UseShellExecute = true } ); - } } // Do some edits outside of iterations. @@ -213,12 +195,24 @@ public partial class ConfigWindow public static void Draw( ConfigWindow window, Mod mod ) { - ImGui.SetNextItemWidth( window._inputTextWidth.X ); + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale ) ); + ImGui.SetNextItemWidth( window._inputTextWidth.X - window._iconButtonSize.X - 3 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); + ImGui.SameLine(); + var fileExists = File.Exists( mod.DefaultFile ); + var tt = fileExists + ? "Open the default option json file in the text editor of your choice." + : "The default option json file does not exist."; + if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", window._iconButtonSize, tt, + !fileExists, true ) ) + { + Process.Start( new ProcessStartInfo( mod.DefaultFile ) { UseShellExecute = true } ); + } + ImGui.SameLine(); var nameValid = Mod.Manager.VerifyFileName( mod, null, _newGroupName, false ); - var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, tt, !nameValid, true ) ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 7c57ea9a..6c72a00a 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", false ); + private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod Meta", false ); private void DrawTabBar() { @@ -56,6 +56,20 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); + if( ImGui.TabItemButton( "Open Advanced Edit Window", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) + { + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( -1, 0 ); + _window.ModEditPopup.IsOpen = true; + } + + ImGuiUtil.HoverTooltip( + "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" + + "\t\t- file redirections\n" + + "\t\t- file swaps\n" + + "\t\t- metadata manipulations\n" + + "\t\t- model materials\n" + + "\t\t- duplicates" ); } // Just a simple text box with the wrapped description, if it exists. From d45e98a25405c7278a86a833cff485ab8c28a0e4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Jun 2022 12:37:49 +0200 Subject: [PATCH 0251/2451] Fix errors not being shown in the importer status window. --- Penumbra/Import/TexToolsImporter.Gui.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 1378d2f7..5ada0f46 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -63,7 +63,7 @@ public partial class TexToolsImporter private void DrawEndState() { - var success = ExtractedMods.Count( t => t.Mod != null ); + var success = ExtractedMods.Count( t => t.Error == null ); ImGui.TextUnformatted( $"Successfully extracted {success} / {ExtractedMods.Count} files." ); ImGui.NewLine(); @@ -78,15 +78,15 @@ public partial class TexToolsImporter ImGui.TableNextColumn(); ImGui.TextUnformatted( file.Name ); ImGui.TableNextColumn(); - if( dir != null ) + if( ex == null ) { using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); - ImGui.TextUnformatted( dir.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ); + ImGui.TextUnformatted( dir?.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ?? "Unknown Directory" ); } else { using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); - ImGui.TextUnformatted( ex!.Message ); + ImGui.TextUnformatted( ex.Message ); ImGuiUtil.HoverTooltip( ex.ToString() ); } } From 34445eb9499057019201d7865ac90bc5e10357a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Jun 2022 12:38:21 +0200 Subject: [PATCH 0252/2451] Another try at character collection animations. --- .../Resolver/PathResolver.Animation.cs | 95 +++++++++---------- .../Interop/Resolver/PathResolver.Data.cs | 13 ++- Penumbra/Interop/Resolver/PathResolver.cs | 33 ++++--- 3 files changed, 73 insertions(+), 68 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 798929a7..026c4a5a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -8,55 +8,54 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { + private ModCollection? _animationLoadCollection; + + public delegate byte LoadTimelineResourcesDelegate( IntPtr timeline ); + + // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. + // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. + [Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )] + public Hook< LoadTimelineResourcesDelegate >? LoadTimelineResourcesHook; + + private byte LoadTimelineResourcesDetour( IntPtr timeline ) + { + byte ret; + var old = _animationLoadCollection; + try + { + var getGameObjectIdx = ( ( delegate* unmanaged < IntPtr, int>** )timeline )[ 0 ][ 28 ]; + var idx = getGameObjectIdx( timeline ); + if( idx >= 0 && idx < Dalamud.Objects.Length ) + { + var obj = Dalamud.Objects[ idx ]; + _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; + } + else + { + _animationLoadCollection = null; + } + } + finally + { + ret = LoadTimelineResourcesHook!.Original( timeline ); + } + + _animationLoadCollection = old; + + return ret; + } + // Probably used when the base idle animation gets loaded. // Make it aware of the correct collection to load the correct pap files. - //[Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] - //public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; - // - //private ModCollection? _animationLoadCollection; - // - //private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) - //{ - // _animationLoadCollection = _lastCreatedCollection - // ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - // CharacterBaseLoadAnimationHook!.Original( drawObject ); - // _animationLoadCollection = null; - //} + [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] + public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; - // Probably used when action paps are loaded. - // Make it aware of the correct collection to load the correct pap files. - //public delegate void PapLoadFunction( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ); - - //[Signature( "E8 ?? ?? ?? ?? 0F 10 00 0F 11 06", DetourName = "RandomPapDetour" )] - //public Hook< PapLoadFunction >? RandomPapHook; - - //private void RandomPapDetour( IntPtr drawObject, IntPtr a2, uint a3, IntPtr a4, uint a5, uint a6, uint a7 ) - //{ - // _animationLoadCollection = _lastCreatedCollection - // ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - // RandomPapHook!.Original( drawObject, a2, a3, a4, a5, a6, a7 ); - // _animationLoadCollection = null; - //} - - //private void TestFunction() - //{ - // var p = Dalamud.Objects.FirstOrDefault( o => o.Name.ToString() == "Demi-Phoenix" ); - // if( p != null ) - // { - // var draw = ( ( GameObject* )p.Address )->DrawObject; - // PluginLog.Information( $"{p.Address:X} {( draw != null ? ( ( IntPtr )draw ).ToString( "X" ) : "NULL" )}" ); - // } - //} - // - //public delegate void TmbLoadFunction(IntPtr drawObject, ushort a2, uint a3, IntPtr a4, IntPtr a5 ); - // - //[Signature( "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 44 38 75 ?? 74 ?? 44 89 B3 ", DetourName ="RandomTmbDetour" )] - //public Hook< TmbLoadFunction > UnkHook = null; - // - //private void RandomTmbDetour( IntPtr drawObject, ushort a2, uint a3, IntPtr a4, IntPtr a5 ) - //{ - // //PluginLog.Information($"{drawObject:X} {a2:X}, {a3:X} {a4:X} {a5:X}" ); - // //TestFunction(); - // UnkHook!.Original( drawObject, a2, a3, a4, a5); - //} + private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) + { + var last = _animationLoadCollection; + _animationLoadCollection = _lastCreatedCollection + ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + CharacterBaseLoadAnimationHook!.Original( drawObject ); + _animationLoadCollection = last; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 53ed569f..11d09ad2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -83,7 +83,6 @@ public unsafe partial class PathResolver LastGameObject = oldGame; } - private void EnableDataHooks() { CharacterBaseCreateHook?.Enable(); @@ -91,8 +90,8 @@ public unsafe partial class PathResolver CharacterBaseDestructorHook?.Enable(); WeaponReloadHook?.Enable(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; - //CharacterBaseLoadAnimationHook?.Enable(); - //RandomPapHook?.Enable(); + LoadTimelineResourcesHook?.Enable(); + CharacterBaseLoadAnimationHook?.Enable(); } private void DisableDataHooks() @@ -102,8 +101,8 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Disable(); EnableDrawHook?.Disable(); CharacterBaseDestructorHook?.Disable(); - //RandomPapHook?.Disable(); - //CharacterBaseLoadAnimationHook?.Disable(); + LoadTimelineResourcesHook?.Disable(); + CharacterBaseLoadAnimationHook?.Disable(); } private void DisposeDataHooks() @@ -112,8 +111,8 @@ public unsafe partial class PathResolver CharacterBaseCreateHook?.Dispose(); EnableDrawHook?.Dispose(); CharacterBaseDestructorHook?.Dispose(); - //RandomPapHook?.Dispose(); - //CharacterBaseLoadAnimationHook?.Dispose(); + LoadTimelineResourcesHook?.Dispose(); + CharacterBaseLoadAnimationHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 98800933..77f413f6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -40,7 +40,8 @@ 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 ) + //|| HandlePapFile( type, gamePath, out collection ) + || HandleAnimationFile( type, gamePath, out collection ) || HandleDecalFile( type, gamePath, out collection ); if( !nonDefault ) { @@ -72,18 +73,24 @@ public partial class PathResolver : IDisposable return false; } - //private bool HandlePapFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) - //{ - // if( type is ResourceType.Pap or ResourceType.Tmb - // && _animationLoadCollection != null ) - // { - // collection = _animationLoadCollection; - // return true; - // } - // - // collection = null; - // return false; - //} + private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) + { + if( _animationLoadCollection != null ) + { + switch( type ) + { + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Avfx: + case ResourceType.Atex: + collection = _animationLoadCollection; + return true; + } + } + + collection = null; + return false; + } public void Enable() { From 19e7d1bf5025d25cafbc0f3206f18beb884703f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Jun 2022 12:58:25 +0200 Subject: [PATCH 0253/2451] Add tooltips to meta manipulation identifiers. --- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 7cdb10ea..73d1ff6e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -120,12 +120,16 @@ public partial class ModEditWindow _new = _new with { SetId = setId }; } + ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGui.TableNextColumn(); if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) { _new = _new with { Slot = slot }; } + ImGuiUtil.HoverTooltip( "Equip Slot" ); + // Values ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, @@ -148,10 +152,12 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGuiUtil.HoverTooltip( "Model Set ID" ); var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToName() ); + ImGuiUtil.HoverTooltip( "Equip Slot" ); // Values ImGui.TableNextColumn(); @@ -209,24 +215,32 @@ public partial class ModEditWindow _new = _new with { SetId = setId }; } + ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGui.TableNextColumn(); if( RaceCombo( "##eqdpRace", _new.Race, out var race ) ) { _new = _new with { Race = race }; } + ImGuiUtil.HoverTooltip( "Model Race" ); + ImGui.TableNextColumn(); if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) ) { _new = _new with { Gender = gender }; } + ImGuiUtil.HoverTooltip( "Gender" ); + ImGui.TableNextColumn(); if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) ) { _new = _new with { Slot = slot }; } + ImGuiUtil.HoverTooltip( "Equip Slot" ); + // Values ImGui.TableNextColumn(); var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); @@ -243,15 +257,19 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGuiUtil.HoverTooltip( "Model Set ID" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Race.ToName() ); + ImGuiUtil.HoverTooltip( "Model Race" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGuiUtil.HoverTooltip( "Gender" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToName() ); + ImGuiUtil.HoverTooltip( "Equip Slot" ); // Values var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId ); @@ -317,12 +335,16 @@ public partial class ModEditWindow _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); } + ImGuiUtil.HoverTooltip( "Object Type" ); + ImGui.TableNextColumn(); if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, ushort.MaxValue ) ) { _new = _new with { PrimaryId = setId }; } + ImGuiUtil.HoverTooltip( "Model Set ID" ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); @@ -334,6 +356,8 @@ public partial class ModEditWindow { _new = _new with { EquipSlot = slot }; } + + ImGuiUtil.HoverTooltip( "Equip Slot" ); } else { @@ -341,6 +365,8 @@ public partial class ModEditWindow { _new = _new with { SecondaryId = setId2 }; } + + ImGuiUtil.HoverTooltip( "Secondary ID" ); } ImGui.TableNextColumn(); @@ -349,6 +375,8 @@ public partial class ModEditWindow _new = _new with { Variant = variant }; } + ImGuiUtil.HoverTooltip( "Variant ID" ); + // Values ImGui.TableNextColumn(); IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, @@ -386,24 +414,29 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.ObjectType.ToName() ); + ImGuiUtil.HoverTooltip( "Object Type" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.PrimaryId.ToString() ); + ImGuiUtil.HoverTooltip( "Model Set ID" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) { ImGui.TextUnformatted( meta.EquipSlot.ToName() ); + ImGuiUtil.HoverTooltip( "Equip Slot" ); } else { ImGui.TextUnformatted( meta.SecondaryId.ToString() ); + ImGuiUtil.HoverTooltip( "Secondary ID" ); } ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Variant.ToString() ); + ImGuiUtil.HoverTooltip( "Variant ID" ); // Values using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, @@ -491,24 +524,32 @@ public partial class ModEditWindow _new = _new with { SetId = setId }; } + ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGui.TableNextColumn(); if( RaceCombo( "##estRace", _new.Race, out var race ) ) { _new = _new with { Race = race }; } + ImGuiUtil.HoverTooltip( "Model Race" ); + ImGui.TableNextColumn(); if( GenderCombo( "##estGender", _new.Gender, out var gender ) ) { _new = _new with { Gender = gender }; } + ImGuiUtil.HoverTooltip( "Gender" ); + ImGui.TableNextColumn(); if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) ) { _new = _new with { Slot = slot }; } + ImGuiUtil.HoverTooltip( "EST Type" ); + // Values ImGui.TableNextColumn(); IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f ); @@ -522,15 +563,19 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGuiUtil.HoverTooltip( "Model Set ID" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Race.ToName() ); + ImGuiUtil.HoverTooltip( "Model Race" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGuiUtil.HoverTooltip( "Gender" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToString() ); + ImGuiUtil.HoverTooltip( "EST Type" ); // Values var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); @@ -577,6 +622,8 @@ public partial class ModEditWindow _new = _new with { SetId = setId }; } + ImGuiUtil.HoverTooltip( "Model Set ID" ); + // Values ImGui.TableNextColumn(); Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ ); @@ -605,6 +652,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGuiUtil.HoverTooltip( "Model Set ID" ); // Values var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId ); @@ -685,12 +733,16 @@ public partial class ModEditWindow _new = _new with { SubRace = subRace }; } + ImGuiUtil.HoverTooltip( "Racial Tribe" ); + ImGui.TableNextColumn(); if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) ) { _new = _new with { Attribute = attribute }; } + ImGuiUtil.HoverTooltip( "Scaling Type" ); + // Values ImGui.TableNextColumn(); ImGui.SetNextItemWidth( FloatWidth ); @@ -705,9 +757,11 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SubRace.ToName() ); + ImGuiUtil.HoverTooltip( "Racial Tribe" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Attribute.ToFullString() ); + ImGuiUtil.HoverTooltip( "Scaling Type" ); ImGui.TableNextColumn(); // Values From 1d935def5865ee2c8460d1a5600b0b4bd36adf57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jun 2022 14:37:05 +0200 Subject: [PATCH 0254/2451] 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 0255/2451] 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 0256/2451] 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 0257/2451] 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 0258/2451] 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 0259/2451] 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 0260/2451] 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 0261/2451] 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 0262/2451] 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 0263/2451] 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 0264/2451] 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 0265/2451] 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 0266/2451] 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 0267/2451] 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 0268/2451] 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 0269/2451] 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 0270/2451] 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 0271/2451] 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 0272/2451] 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 0273/2451] 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 0274/2451] 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 0275/2451] [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" } ] From 45d8d58ce2fc680dfb7c511fec44ea3bd9e5865e Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Thu, 16 Jun 2022 23:33:30 +0200 Subject: [PATCH 0276/2451] add Penumbra.ObjectIsRedrawn and Penumbra.ReverseResolvePath to API --- Penumbra/Api/IPenumbraApi.cs | 4 + Penumbra/Api/PenumbraApi.cs | 64 +++++--- Penumbra/Api/PenumbraIpc.cs | 142 +++++++++++------- .../Collections/ModCollection.Cache.Access.cs | 1 + Penumbra/Collections/ModCollection.Cache.cs | 107 +++++++------ Penumbra/Interop/ObjectReloader.cs | 3 + 6 files changed, 194 insertions(+), 127 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 37265ce9..e14df92b 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -51,6 +51,7 @@ public interface IPenumbraApi : IPenumbraApiBase // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; + event EventHandler? ObjectIsRedrawn; // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); @@ -72,6 +73,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Returns the given gamePath if penumbra would not manipulate it. public string ResolvePath( string gamePath, string characterName ); + // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character + public string[] ReverseResolvePath( string moddedPath, string characterName ); + // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index a81ab204..2fd52b99 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,8 +18,9 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; + public event EventHandler? ObjectIsRedrawn; public bool Valid => _penumbra != null; @@ -30,12 +31,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( Dalamud.GameData ); + _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; + } + + private void ObjectReloader_ObjectIsRedrawn( object? sender, EventArgs e ) + { + ObjectIsRedrawn?.Invoke( sender, e ); } public void Dispose() { + _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; _penumbra = null; - _lumina = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -49,7 +57,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -104,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -121,17 +129,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.Character( characterName ) ); } - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + public string[] ReverseResolvePath( string path, string characterName ) + { + CheckInitialized(); + if( !Penumbra.Config.EnableMods ) + { + return new[] { path }; + } + + var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; + var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); + if( ret.Count == 0 ) ret.Add( gamePath ); + return ret.Select( r => r.ToString() ).ToArray(); + } + + private T? GetFileIntern( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); + return _lumina?.GetFileFromDisk( resolvedPath ); } - return Dalamud.GameData.GetFile< T >( resolvedPath ); + return Dalamud.GameData.GetFile( resolvedPath ); } catch( Exception e ) { @@ -140,13 +162,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); + public T? GetFile( string gamePath ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath ) ); - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + public T? GetFile( string gamePath, string characterName ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -162,7 +184,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); + return new Dictionary(); } catch( Exception e ) { @@ -171,7 +193,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList< string > GetCollections() + public IList GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -193,27 +215,27 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? ( collection.Name, true ) - : ( Penumbra.CollectionManager.Default.Name, false ); + ? (collection.Name, true) + : (Penumbra.CollectionManager.Default.Name, false); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.Name ); + return (obj, collection.Name); } - public IList< (string, string) > GetModList() + public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); } - public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + public Dictionary? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, Dictionary, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index d8dc6db0..f5c59830 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; + internal ICallGateProvider? ProviderInitialized; + internal ICallGateProvider? ProviderDisposed; + internal ICallGateProvider? ProviderApiVersion; + internal ICallGateProvider? ProviderGetModDirectory; + internal ICallGateProvider? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,15 +111,17 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider? ProviderRedrawName; + internal ICallGateProvider? ProviderRedrawIndex; + internal ICallGateProvider? ProviderRedrawObject; + internal ICallGateProvider? ProviderRedrawAll; + internal ICallGateProvider ProviderObjectIsRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -136,7 +138,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -156,7 +158,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -166,13 +168,28 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); } + + try + { + ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); + Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderObjectIsRedrawn}:\n{e}" ); + } + } + + private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) + { + ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -181,24 +198,27 @@ public partial class PenumbraIpc ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); + Api.ObjectIsRedrawn -= Api_ObjectIsRedrawn; } } public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider? ProviderResolveDefault; + internal ICallGateProvider? ProviderResolveCharacter; + internal ICallGateProvider? ProviderGetDrawObjectInfo; + internal ICallGateProvider? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -208,7 +228,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -218,13 +238,23 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } + + try + { + ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); + } } private void DisposeResolveProviders() @@ -238,12 +268,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + internal ICallGateProvider? ProviderChangedItemTooltip; + internal ICallGateProvider? ProviderChangedItemClick; + internal ICallGateProvider>? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -261,8 +291,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -271,8 +301,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -281,7 +311,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -300,23 +330,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; - internal ICallGateProvider< IList< string > >? ProviderGetCollections; - internal ICallGateProvider< string >? ProviderCurrentCollectionName; - internal ICallGateProvider< string >? ProviderDefaultCollectionName; - internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + internal ICallGateProvider>? ProviderGetMods; + internal ICallGateProvider>? ProviderGetCollections; + internal ICallGateProvider? ProviderCurrentCollectionName; + internal ICallGateProvider? ProviderDefaultCollectionName; + internal ICallGateProvider? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -326,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -336,7 +366,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -346,7 +376,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -356,7 +386,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider< string, ( string, bool) >( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 2e3bda92..871280a2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -44,6 +44,7 @@ public partial class ModCollection PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); } + public List? ResolveReversePath( FullPath path ) => _cache?.ReverseResolvePath( path ); public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 6e959e69..c1ed8f1c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,7 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); +public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -20,17 +20,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary> _conflicts = new(); - public IEnumerable< SingleArray< ModConflicts > > AllConflicts + public IEnumerable> AllConflicts => _conflicts.Values; - public SingleArray< ModConflicts > Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + public SingleArray Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -38,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + public IReadOnlyDictionary, object?)> ChangedItems { get { @@ -50,15 +50,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -79,43 +79,50 @@ public partial class ModCollection return candidate.Path; } + public List ReverseResolvePath( FullPath localFilePath ) + { + string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); + return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) + .Select( kvp => kvp.Key ).ToList(); + } + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[ modIdx ], true ); + AddMod( Penumbra.ModManager[modIdx], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } - else if( _collection[ modIdx ].Settings?.Enabled == true ) + else if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } else { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) + if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; @@ -199,7 +206,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[ conflict.Mod2 ] = newConflicts; + _conflicts[conflict.Mod2] = newConflicts; } else { @@ -223,7 +230,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[ mod.Index ].Settings; + var settings = _collection[mod.Index].Settings; if( settings is not { Enabled: true } ) { return; @@ -236,23 +243,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[ groupIndex ]; + var config = settings.Settings[groupIndex]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[ ( int )config ], mod ); + AddSubMod( group[( int )config], mod ); break; case SelectType.Multi: - { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - AddSubMod( option, mod ); - } + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } - break; - } + break; + } } } @@ -305,7 +312,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[ path ]; + var modPath = ResolvedFiles[path]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -314,14 +321,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[ path ] = new ModPath( mod, file ); + ResolvedFiles[path] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -343,7 +350,7 @@ public partial class ModCollection } else { - _conflicts[ mod ] = changedConflicts; + _conflicts[mod] = changedConflicts; } } @@ -352,15 +359,15 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { var tmpConflicts = Conflicts( existingMod ); foreach( var conflict in tmpConflicts ) { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); @@ -370,7 +377,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -380,10 +387,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List { data }; + _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -441,15 +448,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[ name ] = ( data.Item1, x + y ); + _changedItems[name] = (data.Item1, x + y); } } } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 058fc87c..d58d434c 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -25,6 +25,7 @@ public unsafe partial class ObjectReloader private static void EnableDraw( GameObject actor ) => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); + public event EventHandler? ObjectIsRedrawn; // Check whether we currently are in GPose. // Also clear the name list. @@ -281,6 +282,8 @@ public sealed unsafe partial class ObjectReloader : IDisposable break; default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); } + + ObjectIsRedrawn?.Invoke( actor, new EventArgs() ); } private static GameObject? GetLocalPlayer() From 1504af9f3dc95c624557cdbfcc4f34041a466d00 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Thu, 16 Jun 2022 23:51:16 +0200 Subject: [PATCH 0277/2451] formating fixes --- Penumbra/Api/PenumbraApi.cs | 55 +++++---- Penumbra/Api/PenumbraIpc.cs | 130 ++++++++++---------- Penumbra/Collections/ModCollection.Cache.cs | 108 ++++++++-------- 3 files changed, 149 insertions(+), 144 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2fd52b99..9fcd20cc 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,7 +18,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; public event EventHandler? ObjectIsRedrawn; @@ -29,8 +29,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; } @@ -42,8 +42,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void Dispose() { _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; - _penumbra = null; - _lumina = null; + _penumbra = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -57,7 +57,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -112,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -138,22 +138,23 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); + var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? + new List< Utf8GamePath >(); if( ret.Count == 0 ) ret.Add( gamePath ); return ret.Select( r => r.ToString() ).ToArray(); } - private T? GetFileIntern( string resolvedPath ) where T : FileResource + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk( resolvedPath ); + return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - return Dalamud.GameData.GetFile( resolvedPath ); + return Dalamud.GameData.GetFile< T >( resolvedPath ); } catch( Exception e ) { @@ -162,13 +163,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile( string gamePath ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath ) ); + public T? GetFile< T >( string gamePath ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath ) ); - public T? GetFile( string gamePath, string characterName ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath, characterName ) ); + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -184,7 +185,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary(); + return new Dictionary< string, object? >(); } catch( Exception e ) { @@ -193,7 +194,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList GetCollections() + public IList< string > GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -215,27 +216,28 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? (collection.Name, true) - : (Penumbra.CollectionManager.Default.Name, false); + ? ( collection.Name, true ) + : ( Penumbra.CollectionManager.Default.Name, false ); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return (obj, collection.Name); + return ( obj, collection.Name ); } - public IList<(string, string)> GetModList() + public IList< (string, string) > GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); + return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } - public Dictionary? GetAvailableModSettings( string modDirectory, string modName ) + public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, Dictionary, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, + string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); @@ -251,7 +253,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi 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 ) + 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 ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index f5c59830..1d197b07 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider? ProviderInitialized; - internal ICallGateProvider? ProviderDisposed; - internal ICallGateProvider? ProviderApiVersion; - internal ICallGateProvider? ProviderGetModDirectory; - internal ICallGateProvider? ProviderGetConfiguration; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,17 +111,17 @@ 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"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; - internal ICallGateProvider? ProviderRedrawName; - internal ICallGateProvider? ProviderRedrawIndex; - internal ICallGateProvider? ProviderRedrawObject; - internal ICallGateProvider? ProviderRedrawAll; - internal ICallGateProvider ProviderObjectIsRedrawn; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider< string, string >? ProviderObjectIsRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -138,7 +138,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -148,7 +148,7 @@ public partial class PenumbraIpc try { - ProviderRedrawIndex = pi.GetIpcProvider( LabelProviderRedrawIndex ); + ProviderRedrawIndex = pi.GetIpcProvider< int, int, object >( LabelProviderRedrawIndex ); ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -158,7 +158,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -168,7 +168,7 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -178,8 +178,8 @@ public partial class PenumbraIpc try { - ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); - Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + ProviderObjectIsRedrawn = pi.GetIpcProvider< string, string >( LabelProviderObjectIsRedrawn ); + Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; } catch( Exception e ) { @@ -189,7 +189,7 @@ public partial class PenumbraIpc private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) { - ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); + ProviderObjectIsRedrawn?.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -204,21 +204,21 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider? ProviderResolveDefault; - internal ICallGateProvider? ProviderResolveCharacter; - internal ICallGateProvider? ProviderGetDrawObjectInfo; - internal ICallGateProvider? ProviderReverseResolvePath; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -228,7 +228,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -238,7 +238,7 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) @@ -248,7 +248,7 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); } catch( Exception e ) @@ -268,12 +268,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider? ProviderChangedItemTooltip; - internal ICallGateProvider? ProviderChangedItemClick; - internal ICallGateProvider>? ProviderGetChangedItems; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -291,8 +291,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -301,8 +301,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -311,7 +311,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -330,23 +330,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider>? ProviderGetMods; - internal ICallGateProvider>? ProviderGetCollections; - internal ICallGateProvider? ProviderCurrentCollectionName; - internal ICallGateProvider? ProviderDefaultCollectionName; - internal ICallGateProvider? ProviderCharacterCollectionName; + internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; + internal ICallGateProvider< IList< string > >? ProviderGetCollections; + internal ICallGateProvider< string >? ProviderCurrentCollectionName; + internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -356,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -366,7 +366,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -376,7 +376,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -386,7 +386,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index c1ed8f1c..4b280697 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,8 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); + +public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -20,17 +21,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList, object?)> _changedItems = new(); - public readonly Dictionary ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary> _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); - public IEnumerable> AllConflicts + public IEnumerable< SingleArray< ModConflicts > > AllConflicts => _conflicts.Values; - public SingleArray Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); + public SingleArray< ModConflicts > Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -38,7 +39,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems { get { @@ -50,15 +51,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -71,7 +72,7 @@ public partial class ModCollection } if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } @@ -79,10 +80,10 @@ public partial class ModCollection return candidate.Path; } - public List ReverseResolvePath( FullPath localFilePath ) + public List< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) { string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); - return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) + return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) .Select( kvp => kvp.Key ).ToList(); } @@ -91,38 +92,38 @@ public partial class ModCollection switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[modIdx], true ); + AddMod( Penumbra.ModManager[ modIdx ], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } - else if( _collection[modIdx].Settings?.Enabled == true ) + else if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } else { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Setting: - if( _collection[modIdx].Settings?.Enabled == true ) + if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; @@ -206,7 +207,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[conflict.Mod2] = newConflicts; + _conflicts[ conflict.Mod2 ] = newConflicts; } else { @@ -230,7 +231,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[mod.Index].Settings; + var settings = _collection[ mod.Index ].Settings; if( settings is not { Enabled: true } ) { return; @@ -243,23 +244,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[groupIndex]; + var config = settings.Settings[ groupIndex ]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[( int )config], mod ); + AddSubMod( group[ ( int )config ], mod ); break; case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) - { - AddSubMod( option, mod ); - } - - break; + AddSubMod( option, mod ); } + + break; + } } } @@ -312,7 +313,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[path]; + var modPath = ResolvedFiles[ path ]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -321,14 +322,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[path] = new ModPath( mod, file ); + ResolvedFiles[ path ] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -350,7 +351,7 @@ public partial class ModCollection } else { - _conflicts[mod] = changedConflicts; + _conflicts[ mod ] = changedConflicts; } } @@ -359,8 +360,8 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { @@ -368,7 +369,8 @@ public partial class ModCollection foreach( var conflict in tmpConflicts ) { if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + || data is MetaManipulation meta && + conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); } @@ -377,7 +379,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -387,10 +389,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List { data }; - _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -448,15 +450,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); + _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[name] = (data.Item1, x + y); + _changedItems[ name ] = ( data.Item1, x + y ); } } } From abd1fd14f55ddd0994b32e9da451a8436cf36c6f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jun 2022 16:18:08 +0200 Subject: [PATCH 0278/2451] Add config to use default or owner collection for housing retainers. --- Penumbra/Configuration.cs | 1 + Penumbra/Interop/Resolver/PathResolver.Data.cs | 4 ++++ Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8cb3e746..dfa22fd2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -27,6 +27,7 @@ public partial class Configuration : IPluginConfiguration public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; public bool PreferNamedCollectionsOverOwners { get; set; } = true; + public bool UseDefaultCollectionForRetainers { get; set; } = false; #if DEBUG public bool DebugMode { get; set; } = true; diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 00b92905..71cda6c5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -271,6 +271,10 @@ public unsafe partial class PathResolver return Penumbra.CollectionManager.Default; } + // Housing Retainers + if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == (byte) ObjectKind.EventNpc && gameObject->DataID == 1011832 ) + return Penumbra.CollectionManager.Default; + string? actorName = null; if( Penumbra.Config.PreferNamedCollectionsOverOwners ) { diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index e69fa219..b545b1eb 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -75,6 +75,10 @@ public partial class ConfigWindow "If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owner's collection.\n" + "That is, if you have a 'Topaz Carbuncle' collection, it will use this one instead of the one for its owner.", Penumbra.Config.PreferNamedCollectionsOverOwners, v => Penumbra.Config.PreferNamedCollectionsOverOwners = v ); + Checkbox( "Use Default Collection for Housing Retainers", + "Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the default collection.\n" + + "It is not possible to make them have their own collection, since they have no connection to their actual name.", + Penumbra.Config.UseDefaultCollectionForRetainers, v => Penumbra.Config.UseDefaultCollectionForRetainers = v ); ImGui.Dummy( _window._defaultSpace ); DrawFolderSortType(); DrawAbsoluteSizeSelector(); From 61680f0afbac35d8e0144a5a4d028a7cb4685a28 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jun 2022 16:18:56 +0200 Subject: [PATCH 0279/2451] Add warning if the currently edited collection is not in use anywhere. --- .../Collections/CollectionManager.Active.cs | 5 +++++ Penumbra/UI/ConfigWindow.Misc.cs | 18 ++++++++---------- Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 7 +------ Penumbra/UI/ConfigWindow.ModsTab.cs | 11 ++++++++++- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index d21ad8f9..f06a574f 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -20,6 +20,9 @@ public partial class ModCollection // The collection currently selected for changing settings. public ModCollection Current { get; private set; } = Empty; + // The collection currently selected is in use either as an active collection or through inheritance. + public bool CurrentCollectionInUse { get; private set; } + // The collection used for general file redirections and all characters not specifically named. public ModCollection Default { get; private set; } = Empty; @@ -78,6 +81,8 @@ public partial class ModCollection break; } + CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 17cc424d..7d8969ee 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -101,18 +101,16 @@ public partial class ConfigWindow _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; - using var combo = ImRaii.Combo( label, current.Name ); - if( !combo ) + using var combo = ImRaii.Combo( label, current.Name ); + if( combo ) { - return; - } - - foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) - { - using var id = ImRaii.PushId( collection.Index ); - if( ImGui.Selectable( collection.Name, collection == current ) ) + foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) { - Penumbra.CollectionManager.SetCollection( collection, type, characterName ); + using var id = ImRaii.PushId( collection.Index ); + if( ImGui.Selectable( collection.Name, collection == current ) ) + { + Penumbra.CollectionManager.SetCollection( collection, type, characterName ); + } } } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs index 6830884e..4893c6da 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs @@ -12,17 +12,12 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private partial class ModPanel : IDisposable + private partial class ModPanel { // We use a big, nice game font for the title. private readonly GameFontHandle _nameFont = Dalamud.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); - public void Dispose() - { - _nameFont.Dispose(); - } - // Header data. private string _modName = string.Empty; private string _modAuthor = string.Empty; diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index f09f1423..d85ff38a 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -63,6 +63,10 @@ public partial class ConfigWindow DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + if( !Penumbra.CollectionManager.CurrentCollectionInUse ) + { + ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); + } } private static void DrawDefaultCollectionButton( Vector2 width ) @@ -112,7 +116,7 @@ public partial class ConfigWindow // The basic setup for the mod panel. // Details are in other files. - private partial class ModPanel + private partial class ModPanel : IDisposable { private readonly ConfigWindow _window; @@ -123,6 +127,11 @@ public partial class ConfigWindow public ModPanel( ConfigWindow window ) => _window = window; + public void Dispose() + { + _nameFont.Dispose(); + } + public void Draw( ModFileSystemSelector selector ) { Init( selector ); From f579933dd7c35d3f0b43501f548931389d8cf145 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jun 2022 16:30:02 +0200 Subject: [PATCH 0280/2451] Change planned API to use interfaces. --- Penumbra/Api/IPenumbraApi.cs | 6 +++--- Penumbra/Api/PenumbraApi.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 37265ce9..dd4b1562 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -104,12 +104,12 @@ public interface IPenumbraApi : IPenumbraApiBase // 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 ); + public IDictionary< string, (IList, 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, + public (PenumbraApiEc, (bool, int, IDictionary< string, IList >, 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. @@ -131,7 +131,7 @@ public interface IPenumbraApi : IPenumbraApiBase 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 ); + IReadOnlyList options ); // Create a temporary collection without actual settings but with a cache. diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index a81ab204..4cce0f2c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -210,10 +210,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } - public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + public IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, IDictionary< string, IList >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); @@ -229,7 +229,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi 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 ) + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList options ) => throw new NotImplementedException(); public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) From 1c7037416c29dbdf2a77bd730fa30cfdce746672 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Thu, 16 Jun 2022 23:33:30 +0200 Subject: [PATCH 0281/2451] add Penumbra.ObjectIsRedrawn and Penumbra.ReverseResolvePath to API --- Penumbra/Api/IPenumbraApi.cs | 4 + Penumbra/Api/PenumbraApi.cs | 60 +++++--- Penumbra/Api/PenumbraIpc.cs | 142 +++++++++++------- .../Collections/ModCollection.Cache.Access.cs | 1 + Penumbra/Collections/ModCollection.Cache.cs | 107 +++++++------ Penumbra/Interop/ObjectReloader.cs | 3 + 6 files changed, 192 insertions(+), 125 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index dd4b1562..b50a98ba 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -51,6 +51,7 @@ public interface IPenumbraApi : IPenumbraApiBase // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; + event EventHandler? ObjectIsRedrawn; // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); @@ -72,6 +73,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Returns the given gamePath if penumbra would not manipulate it. public string ResolvePath( string gamePath, string characterName ); + // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character + public string[] ReverseResolvePath( string moddedPath, string characterName ); + // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 4cce0f2c..eb8f57b2 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,8 +18,9 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; + public event EventHandler? ObjectIsRedrawn; public bool Valid => _penumbra != null; @@ -30,12 +31,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( Dalamud.GameData ); + _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; + } + + private void ObjectReloader_ObjectIsRedrawn( object? sender, EventArgs e ) + { + ObjectIsRedrawn?.Invoke( sender, e ); } public void Dispose() { + _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; _penumbra = null; - _lumina = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -49,7 +57,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -104,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -121,17 +129,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.Character( characterName ) ); } - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + public string[] ReverseResolvePath( string path, string characterName ) + { + CheckInitialized(); + if( !Penumbra.Config.EnableMods ) + { + return new[] { path }; + } + + var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; + var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); + if( ret.Count == 0 ) ret.Add( gamePath ); + return ret.Select( r => r.ToString() ).ToArray(); + } + + private T? GetFileIntern( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); + return _lumina?.GetFileFromDisk( resolvedPath ); } - return Dalamud.GameData.GetFile< T >( resolvedPath ); + return Dalamud.GameData.GetFile( resolvedPath ); } catch( Exception e ) { @@ -140,13 +162,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); + public T? GetFile( string gamePath ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath ) ); - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + public T? GetFile( string gamePath, string characterName ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -162,7 +184,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); + return new Dictionary(); } catch( Exception e ) { @@ -171,7 +193,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList< string > GetCollections() + public IList GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -193,21 +215,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? ( collection.Name, true ) - : ( Penumbra.CollectionManager.Default.Name, false ); + ? (collection.Name, true) + : (Penumbra.CollectionManager.Default.Name, false); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.Name ); + return (obj, collection.Name); } - public IList< (string, string) > GetModList() + public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); } public IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index d8dc6db0..f5c59830 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; + internal ICallGateProvider? ProviderInitialized; + internal ICallGateProvider? ProviderDisposed; + internal ICallGateProvider? ProviderApiVersion; + internal ICallGateProvider? ProviderGetModDirectory; + internal ICallGateProvider? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,15 +111,17 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider? ProviderRedrawName; + internal ICallGateProvider? ProviderRedrawIndex; + internal ICallGateProvider? ProviderRedrawObject; + internal ICallGateProvider? ProviderRedrawAll; + internal ICallGateProvider ProviderObjectIsRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -136,7 +138,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -156,7 +158,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -166,13 +168,28 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); } + + try + { + ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); + Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderObjectIsRedrawn}:\n{e}" ); + } + } + + private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) + { + ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -181,24 +198,27 @@ public partial class PenumbraIpc ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); + Api.ObjectIsRedrawn -= Api_ObjectIsRedrawn; } } public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider? ProviderResolveDefault; + internal ICallGateProvider? ProviderResolveCharacter; + internal ICallGateProvider? ProviderGetDrawObjectInfo; + internal ICallGateProvider? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -208,7 +228,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -218,13 +238,23 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } + + try + { + ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); + } } private void DisposeResolveProviders() @@ -238,12 +268,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + internal ICallGateProvider? ProviderChangedItemTooltip; + internal ICallGateProvider? ProviderChangedItemClick; + internal ICallGateProvider>? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -261,8 +291,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -271,8 +301,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -281,7 +311,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -300,23 +330,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; - internal ICallGateProvider< IList< string > >? ProviderGetCollections; - internal ICallGateProvider< string >? ProviderCurrentCollectionName; - internal ICallGateProvider< string >? ProviderDefaultCollectionName; - internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + internal ICallGateProvider>? ProviderGetMods; + internal ICallGateProvider>? ProviderGetCollections; + internal ICallGateProvider? ProviderCurrentCollectionName; + internal ICallGateProvider? ProviderDefaultCollectionName; + internal ICallGateProvider? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -326,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -336,7 +366,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -346,7 +376,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -356,7 +386,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider< string, ( string, bool) >( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 2e3bda92..871280a2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -44,6 +44,7 @@ public partial class ModCollection PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); } + public List? ResolveReversePath( FullPath path ) => _cache?.ReverseResolvePath( path ); public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 6e959e69..c1ed8f1c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,7 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); +public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -20,17 +20,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary> _conflicts = new(); - public IEnumerable< SingleArray< ModConflicts > > AllConflicts + public IEnumerable> AllConflicts => _conflicts.Values; - public SingleArray< ModConflicts > Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + public SingleArray Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -38,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + public IReadOnlyDictionary, object?)> ChangedItems { get { @@ -50,15 +50,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -79,43 +79,50 @@ public partial class ModCollection return candidate.Path; } + public List ReverseResolvePath( FullPath localFilePath ) + { + string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); + return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) + .Select( kvp => kvp.Key ).ToList(); + } + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[ modIdx ], true ); + AddMod( Penumbra.ModManager[modIdx], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } - else if( _collection[ modIdx ].Settings?.Enabled == true ) + else if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } else { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) + if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; @@ -199,7 +206,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[ conflict.Mod2 ] = newConflicts; + _conflicts[conflict.Mod2] = newConflicts; } else { @@ -223,7 +230,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[ mod.Index ].Settings; + var settings = _collection[mod.Index].Settings; if( settings is not { Enabled: true } ) { return; @@ -236,23 +243,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[ groupIndex ]; + var config = settings.Settings[groupIndex]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[ ( int )config ], mod ); + AddSubMod( group[( int )config], mod ); break; case SelectType.Multi: - { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - AddSubMod( option, mod ); - } + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } - break; - } + break; + } } } @@ -305,7 +312,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[ path ]; + var modPath = ResolvedFiles[path]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -314,14 +321,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[ path ] = new ModPath( mod, file ); + ResolvedFiles[path] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -343,7 +350,7 @@ public partial class ModCollection } else { - _conflicts[ mod ] = changedConflicts; + _conflicts[mod] = changedConflicts; } } @@ -352,15 +359,15 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { var tmpConflicts = Conflicts( existingMod ); foreach( var conflict in tmpConflicts ) { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); @@ -370,7 +377,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -380,10 +387,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List { data }; + _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -441,15 +448,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[ name ] = ( data.Item1, x + y ); + _changedItems[name] = (data.Item1, x + y); } } } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 058fc87c..d58d434c 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -25,6 +25,7 @@ public unsafe partial class ObjectReloader private static void EnableDraw( GameObject actor ) => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); + public event EventHandler? ObjectIsRedrawn; // Check whether we currently are in GPose. // Also clear the name list. @@ -281,6 +282,8 @@ public sealed unsafe partial class ObjectReloader : IDisposable break; default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); } + + ObjectIsRedrawn?.Invoke( actor, new EventArgs() ); } private static GameObject? GetLocalPlayer() From bcd62cbe69eb2e10465f3300fda67f1ceaf6c8cc Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Thu, 16 Jun 2022 23:51:16 +0200 Subject: [PATCH 0282/2451] formating fixes --- Penumbra/Api/PenumbraApi.cs | 47 +++---- Penumbra/Api/PenumbraIpc.cs | 130 ++++++++++---------- Penumbra/Collections/ModCollection.Cache.cs | 108 ++++++++-------- 3 files changed, 144 insertions(+), 141 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index eb8f57b2..87904e0a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,7 +18,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; public event EventHandler? ObjectIsRedrawn; @@ -29,8 +29,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; } @@ -42,8 +42,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void Dispose() { _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; - _penumbra = null; - _lumina = null; + _penumbra = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -57,7 +57,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -112,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -138,22 +138,23 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); + var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? + new List< Utf8GamePath >(); if( ret.Count == 0 ) ret.Add( gamePath ); return ret.Select( r => r.ToString() ).ToArray(); } - private T? GetFileIntern( string resolvedPath ) where T : FileResource + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk( resolvedPath ); + return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - return Dalamud.GameData.GetFile( resolvedPath ); + return Dalamud.GameData.GetFile< T >( resolvedPath ); } catch( Exception e ) { @@ -162,13 +163,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile( string gamePath ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath ) ); + public T? GetFile< T >( string gamePath ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath ) ); - public T? GetFile( string gamePath, string characterName ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath, characterName ) ); + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -184,7 +185,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary(); + return new Dictionary< string, object? >(); } catch( Exception e ) { @@ -193,7 +194,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList GetCollections() + public IList< string > GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -215,21 +216,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? (collection.Name, true) - : (Penumbra.CollectionManager.Default.Name, false); + ? ( collection.Name, true ) + : ( Penumbra.CollectionManager.Default.Name, false ); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return (obj, collection.Name); + return ( obj, collection.Name ); } - public IList<(string, string)> GetModList() + public IList< (string, string) > GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); + return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } public IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index f5c59830..1d197b07 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider? ProviderInitialized; - internal ICallGateProvider? ProviderDisposed; - internal ICallGateProvider? ProviderApiVersion; - internal ICallGateProvider? ProviderGetModDirectory; - internal ICallGateProvider? ProviderGetConfiguration; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,17 +111,17 @@ 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"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; - internal ICallGateProvider? ProviderRedrawName; - internal ICallGateProvider? ProviderRedrawIndex; - internal ICallGateProvider? ProviderRedrawObject; - internal ICallGateProvider? ProviderRedrawAll; - internal ICallGateProvider ProviderObjectIsRedrawn; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider< string, string >? ProviderObjectIsRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -138,7 +138,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -148,7 +148,7 @@ public partial class PenumbraIpc try { - ProviderRedrawIndex = pi.GetIpcProvider( LabelProviderRedrawIndex ); + ProviderRedrawIndex = pi.GetIpcProvider< int, int, object >( LabelProviderRedrawIndex ); ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -158,7 +158,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -168,7 +168,7 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -178,8 +178,8 @@ public partial class PenumbraIpc try { - ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); - Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + ProviderObjectIsRedrawn = pi.GetIpcProvider< string, string >( LabelProviderObjectIsRedrawn ); + Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; } catch( Exception e ) { @@ -189,7 +189,7 @@ public partial class PenumbraIpc private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) { - ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); + ProviderObjectIsRedrawn?.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -204,21 +204,21 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider? ProviderResolveDefault; - internal ICallGateProvider? ProviderResolveCharacter; - internal ICallGateProvider? ProviderGetDrawObjectInfo; - internal ICallGateProvider? ProviderReverseResolvePath; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -228,7 +228,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -238,7 +238,7 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) @@ -248,7 +248,7 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); } catch( Exception e ) @@ -268,12 +268,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider? ProviderChangedItemTooltip; - internal ICallGateProvider? ProviderChangedItemClick; - internal ICallGateProvider>? ProviderGetChangedItems; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -291,8 +291,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -301,8 +301,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -311,7 +311,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -330,23 +330,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider>? ProviderGetMods; - internal ICallGateProvider>? ProviderGetCollections; - internal ICallGateProvider? ProviderCurrentCollectionName; - internal ICallGateProvider? ProviderDefaultCollectionName; - internal ICallGateProvider? ProviderCharacterCollectionName; + internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; + internal ICallGateProvider< IList< string > >? ProviderGetCollections; + internal ICallGateProvider< string >? ProviderCurrentCollectionName; + internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -356,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -366,7 +366,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -376,7 +376,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -386,7 +386,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index c1ed8f1c..4b280697 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,8 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); + +public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -20,17 +21,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList, object?)> _changedItems = new(); - public readonly Dictionary ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary> _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); - public IEnumerable> AllConflicts + public IEnumerable< SingleArray< ModConflicts > > AllConflicts => _conflicts.Values; - public SingleArray Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); + public SingleArray< ModConflicts > Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -38,7 +39,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems { get { @@ -50,15 +51,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -71,7 +72,7 @@ public partial class ModCollection } if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } @@ -79,10 +80,10 @@ public partial class ModCollection return candidate.Path; } - public List ReverseResolvePath( FullPath localFilePath ) + public List< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) { string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); - return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) + return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) .Select( kvp => kvp.Key ).ToList(); } @@ -91,38 +92,38 @@ public partial class ModCollection switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[modIdx], true ); + AddMod( Penumbra.ModManager[ modIdx ], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } - else if( _collection[modIdx].Settings?.Enabled == true ) + else if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } else { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Setting: - if( _collection[modIdx].Settings?.Enabled == true ) + if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; @@ -206,7 +207,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[conflict.Mod2] = newConflicts; + _conflicts[ conflict.Mod2 ] = newConflicts; } else { @@ -230,7 +231,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[mod.Index].Settings; + var settings = _collection[ mod.Index ].Settings; if( settings is not { Enabled: true } ) { return; @@ -243,23 +244,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[groupIndex]; + var config = settings.Settings[ groupIndex ]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[( int )config], mod ); + AddSubMod( group[ ( int )config ], mod ); break; case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) - { - AddSubMod( option, mod ); - } - - break; + AddSubMod( option, mod ); } + + break; + } } } @@ -312,7 +313,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[path]; + var modPath = ResolvedFiles[ path ]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -321,14 +322,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[path] = new ModPath( mod, file ); + ResolvedFiles[ path ] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -350,7 +351,7 @@ public partial class ModCollection } else { - _conflicts[mod] = changedConflicts; + _conflicts[ mod ] = changedConflicts; } } @@ -359,8 +360,8 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { @@ -368,7 +369,8 @@ public partial class ModCollection foreach( var conflict in tmpConflicts ) { if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + || data is MetaManipulation meta && + conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); } @@ -377,7 +379,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -387,10 +389,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List { data }; - _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -448,15 +450,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); + _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[name] = (data.Item1, x + y); + _changedItems[ name ] = ( data.Item1, x + y ); } } } From 3c5cff141848a41d86cedd75f5d906cd71d51973 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jun 2022 17:10:48 +0200 Subject: [PATCH 0283/2451] Some cleanup, slight changes. --- Penumbra/Api/IPenumbraApi.cs | 5 ++- Penumbra/Api/PenumbraApi.cs | 43 +++++++++---------- Penumbra/Api/PenumbraIpc.cs | 37 ++++++++-------- .../Collections/ModCollection.Cache.Access.cs | 3 +- Penumbra/Collections/ModCollection.Cache.cs | 37 ++++++++++------ Penumbra/Interop/ObjectReloader.cs | 11 +++-- 6 files changed, 74 insertions(+), 62 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index b50a98ba..25c828e1 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -20,6 +20,7 @@ public interface IPenumbraApiBase public delegate void ChangedItemHover( object? item ); public delegate void ChangedItemClick( MouseButton button, object? item ); +public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); public enum PenumbraApiEc { @@ -51,7 +52,7 @@ public interface IPenumbraApi : IPenumbraApiBase // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; - event EventHandler? ObjectIsRedrawn; + public event GameObjectRedrawn? GameObjectRedrawn; // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); @@ -74,7 +75,7 @@ public interface IPenumbraApi : IPenumbraApiBase public string ResolvePath( string gamePath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public string[] ReverseResolvePath( string moddedPath, string characterName ); + public IList ReverseResolvePath( string moddedPath, string characterName ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 87904e0a..33b4dedf 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -20,7 +20,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public int ApiVersion { get; } = 4; private Penumbra? _penumbra; private Lumina.GameData? _lumina; - public event EventHandler? ObjectIsRedrawn; + public event GameObjectRedrawn? GameObjectRedrawn; public bool Valid => _penumbra != null; @@ -29,21 +29,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); - _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; - } - - private void ObjectReloader_ObjectIsRedrawn( object? sender, EventArgs e ) - { - ObjectIsRedrawn?.Invoke( sender, e ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); + _penumbra.ObjectReloader.GameObjectRedrawn += OnGameObjectRedrawn; } public void Dispose() { - _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; - _penumbra = null; - _lumina = null; + _penumbra!.ObjectReloader.GameObjectRedrawn -= OnGameObjectRedrawn; + _penumbra = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -98,13 +93,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); } + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) + { + GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); + } + public void RedrawAll( RedrawType setting ) { CheckInitialized(); _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { @@ -129,7 +129,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.Character( characterName ) ); } - public string[] ReverseResolvePath( string path, string characterName ) + public IList< string > ReverseResolvePath( string path, string characterName ) { CheckInitialized(); if( !Penumbra.Config.EnableMods ) @@ -137,11 +137,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return new[] { path }; } - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? - new List< Utf8GamePath >(); - if( ret.Count == 0 ) ret.Add( gamePath ); - return ret.Select( r => r.ToString() ).ToArray(); + var ret = Penumbra.CollectionManager.Character( characterName ).ReverseResolvePath( new FullPath( path ) ); + return ret.Select( r => r.ToString() ).ToList(); } private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource @@ -233,10 +230,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } - public IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, IDictionary< string, IList >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, + string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); @@ -252,7 +250,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi 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, IReadOnlyList options ) + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + IReadOnlyList< string > options ) => throw new NotImplementedException(); public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 1d197b07..14f3ea11 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -111,17 +111,17 @@ 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"; - public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; + public const string LabelProviderGameObjectRedrawn = "Penumbra.GameObjectRedrawn"; internal ICallGateProvider< string, int, object >? ProviderRedrawName; internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderObjectIsRedrawn; + internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -178,19 +178,17 @@ public partial class PenumbraIpc try { - ProviderObjectIsRedrawn = pi.GetIpcProvider< string, string >( LabelProviderObjectIsRedrawn ); - Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + ProviderGameObjectRedrawn = pi.GetIpcProvider< IntPtr, int, object? >( LabelProviderGameObjectRedrawn ); + Api.GameObjectRedrawn += OnGameObjectRedrawn; } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderObjectIsRedrawn}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGameObjectRedrawn}:\n{e}" ); } } - private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) - { - ProviderObjectIsRedrawn?.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); - } + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) + => ProviderGameObjectRedrawn?.SendMessage( objectAddress, objectTableIndex ); private void DisposeRedrawProviders() { @@ -198,7 +196,7 @@ public partial class PenumbraIpc ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); - Api.ObjectIsRedrawn -= Api_ObjectIsRedrawn; + Api.GameObjectRedrawn -= OnGameObjectRedrawn; } } @@ -209,10 +207,10 @@ public partial class PenumbraIpc public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; - internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< string, string, IList< string > >? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -248,7 +246,7 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath = pi.GetIpcProvider< string, string, IList< string > >( LabelProviderReverseResolvePath ); ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); } catch( Exception e ) @@ -262,6 +260,7 @@ public partial class PenumbraIpc ProviderGetDrawObjectInfo?.UnregisterFunc(); ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); + ProviderReverseResolvePath?.UnregisterFunc(); } } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 871280a2..5cf71fc2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -44,7 +44,8 @@ public partial class ModCollection PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); } - public List? ResolveReversePath( FullPath path ) => _cache?.ReverseResolvePath( path ); + public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath path ) + => _cache?.ReverseResolvePath( path ) ?? Array.Empty< Utf8GamePath >(); public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 4b280697..d4e75a2c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,6 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); - public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection @@ -72,7 +71,7 @@ public partial class ModCollection } if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } @@ -80,11 +79,26 @@ public partial class ModCollection return candidate.Path; } - public List< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) + // For a given full path, find all game paths that currently use this file. + public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) { - string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); - return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) - .Select( kvp => kvp.Key ).ToList(); + var needle = localFilePath.FullName.ToLower(); + if( localFilePath.IsRooted ) + { + needle = needle.Replace( '/', '\\' ); + } + + var iterator = ResolvedFiles + .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.InvariantCultureIgnoreCase ) ) + .Select( kvp => kvp.Key ); + + // For files that are not rooted, try to add themselves. + if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) + { + iterator = iterator.Prepend( utf8 ); + } + + return iterator; } private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) @@ -253,8 +267,8 @@ public partial class ModCollection case SelectType.Multi: { foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { AddSubMod( option, mod ); } @@ -360,7 +374,7 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) @@ -368,9 +382,8 @@ public partial class ModCollection var tmpConflicts = Conflicts( existingMod ); foreach( var conflict in tmpConflicts ) { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && - conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index d58d434c..daf4ae7f 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -25,8 +25,6 @@ public unsafe partial class ObjectReloader private static void EnableDraw( GameObject actor ) => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); - public event EventHandler? ObjectIsRedrawn; - // Check whether we currently are in GPose. // Also clear the name list. private void SetGPose() @@ -107,6 +105,8 @@ public sealed unsafe partial class ObjectReloader : IDisposable private readonly List< int > _afterGPoseQueue = new(GPoseSlots); private int _target = -1; + public event Action< IntPtr, int >? GameObjectRedrawn; + public ObjectReloader() => Dalamud.Framework.Update += OnUpdateEvent; @@ -134,7 +134,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable } } - private static void WriteVisible( GameObject? actor ) + private void WriteVisible( GameObject? actor ) { if( BadRedrawIndices( actor, out var tableIndex ) ) { @@ -142,11 +142,12 @@ public sealed unsafe partial class ObjectReloader : IDisposable } *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - if( IsGPoseActor( tableIndex ) ) { EnableDraw( actor! ); } + + GameObjectRedrawn?.Invoke( actor!.Address, tableIndex ); } private void ReloadActor( GameObject? actor ) @@ -282,8 +283,6 @@ public sealed unsafe partial class ObjectReloader : IDisposable break; default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); } - - ObjectIsRedrawn?.Invoke( actor, new EventArgs() ); } private static GameObject? GetLocalPlayer() From 35e68e74f4694fd2a47ec703d630ebe17fdf4f1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jun 2022 17:33:22 +0200 Subject: [PATCH 0284/2451] File Selector improvements. --- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ConfigWindow.Misc.cs | 31 +++++++++++++++++++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 2 +- Penumbra/UI/ConfigWindow.cs | 1 - 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 6f0ec77f..e18ec33c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -41,7 +41,7 @@ public partial class ModEditWindow private int _offsetX = 0; private int _offsetY = 0; - private readonly FileDialogManager _dialogManager = new(); + private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); private static bool DragFloat( string label, float width, ref float value ) { diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 14dfd804..51490733 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -19,7 +19,7 @@ namespace Penumbra.UI.Classes; public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > { - private readonly FileDialogManager _fileManager = new(); + private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); private TexToolsImporter? _import; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 7d8969ee..6f70126a 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; @@ -11,6 +13,7 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.UI.Classes; +using Penumbra.Util; namespace Penumbra.UI; @@ -114,4 +117,32 @@ public partial class ConfigWindow } } } + + // Set up the file selector with the right flags and custom side bar items. + public static FileDialogManager SetupFileManager() + { + var fileManager = new FileDialogManager + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, + }; + + if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) + { + fileManager.CustomSideBarItems.Add( ("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1) ); + } + + if( Functions.GetQuickAccessFolders( out var folders ) ) + { + foreach( var ((name, path), idx) in folders.WithIndex() ) + { + fileManager.CustomSideBarItems.Add( ($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1) ); + } + } + + // Remove Videos and Music. + fileManager.CustomSideBarItems.Add( ("Videos", string.Empty, 0, -1) ); + fileManager.CustomSideBarItems.Add( ("Music", string.Empty, 0, -1) ); + + return fileManager; + } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index de377896..32859822 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -62,7 +62,7 @@ public partial class ConfigWindow // Changing the base mod directory. private string? _newModDirectory; - private readonly FileDialogManager _dialogManager = new(); + private readonly FileDialogManager _dialogManager = SetupFileManager(); private bool _dialogOpen; // For toggling on/off. // Do not change the directory without explicitly pressing enter or this button. diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ff7fa751..ccfac393 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -33,7 +33,6 @@ public sealed partial class ConfigWindow : Window, IDisposable _effectiveTab = new EffectiveTab(); _debugTab = new DebugTab( this ); _resourceTab = new ResourceTab( this ); - Flags |= ImGuiWindowFlags.NoDocking; if( Penumbra.Config.FixMainWindow ) { Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; From 1fb2ddaa5e1865a57af55468132584edb7523358 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 10:21:49 +0200 Subject: [PATCH 0285/2451] Fix advanced editing always showing. --- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index d0ad872d..d1c4a838 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -56,7 +56,7 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); - if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) + if( Penumbra.Config.ShowAdvanced && ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { _window.ModEditPopup.ChangeMod( _mod ); _window.ModEditPopup.ChangeOption( -1, 0 ); From 1ee4cb99d02a7d16b487584824ab6952167e36da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 11:10:51 +0200 Subject: [PATCH 0286/2451] Add demihuman hooks for resolving. --- .../Resolver/PathResolver.Demihuman.cs | 88 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Human.cs | 6 -- .../Interop/Resolver/PathResolver.Resolve.cs | 37 ++++++++ Penumbra/Interop/Resolver/PathResolver.cs | 4 + 4 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Interop/Resolver/PathResolver.Demihuman.cs diff --git a/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs b/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs new file mode 100644 index 00000000..c938bb72 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs @@ -0,0 +1,88 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] + public IntPtr* DrawObjectDemiVTable; + + public Hook< GeneralResolveDelegate >? ResolveDemiDecalPathHook; + public Hook< EidResolveDelegate >? ResolveDemiEidPathHook; + public Hook< GeneralResolveDelegate >? ResolveDemiImcPathHook; + public Hook< MPapResolveDelegate >? ResolveDemiMPapPathHook; + public Hook< GeneralResolveDelegate >? ResolveDemiMdlPathHook; + public Hook< MaterialResolveDetour >? ResolveDemiMtrlPathHook; + public Hook< MaterialResolveDetour >? ResolveDemiPapPathHook; + public Hook< GeneralResolveDelegate >? ResolveDemiPhybPathHook; + public Hook< GeneralResolveDelegate >? ResolveDemiSklbPathHook; + public Hook< GeneralResolveDelegate >? ResolveDemiSkpPathHook; + public Hook< EidResolveDelegate >? ResolveDemiTmbPathHook; + public Hook< MaterialResolveDetour >? ResolveDemiVfxPathHook; + + private void SetupDemiHooks() + { + ResolveDemiDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour ); + ResolveDemiEidPathHook = new Hook< EidResolveDelegate >( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour ); + ResolveDemiImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour ); + ResolveDemiMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour ); + ResolveDemiMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour ); + ResolveDemiMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour ); + ResolveDemiPapPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour ); + ResolveDemiPhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour ); + ResolveDemiSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour ); + ResolveDemiSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour ); + ResolveDemiTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour ); + ResolveDemiVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour ); + } + + private void EnableDemiHooks() + { + ResolveDemiDecalPathHook?.Enable(); + ResolveDemiEidPathHook?.Enable(); + ResolveDemiImcPathHook?.Enable(); + ResolveDemiMPapPathHook?.Enable(); + ResolveDemiMdlPathHook?.Enable(); + ResolveDemiMtrlPathHook?.Enable(); + ResolveDemiPapPathHook?.Enable(); + ResolveDemiPhybPathHook?.Enable(); + ResolveDemiSklbPathHook?.Enable(); + ResolveDemiSkpPathHook?.Enable(); + ResolveDemiTmbPathHook?.Enable(); + ResolveDemiVfxPathHook?.Enable(); + } + + private void DisableDemiHooks() + { + ResolveDemiDecalPathHook?.Disable(); + ResolveDemiEidPathHook?.Disable(); + ResolveDemiImcPathHook?.Disable(); + ResolveDemiMPapPathHook?.Disable(); + ResolveDemiMdlPathHook?.Disable(); + ResolveDemiMtrlPathHook?.Disable(); + ResolveDemiPapPathHook?.Disable(); + ResolveDemiPhybPathHook?.Disable(); + ResolveDemiSklbPathHook?.Disable(); + ResolveDemiSkpPathHook?.Disable(); + ResolveDemiTmbPathHook?.Disable(); + ResolveDemiVfxPathHook?.Disable(); + } + + private void DisposeDemiHooks() + { + ResolveDemiDecalPathHook?.Dispose(); + ResolveDemiEidPathHook?.Dispose(); + ResolveDemiImcPathHook?.Dispose(); + ResolveDemiMPapPathHook?.Dispose(); + ResolveDemiMdlPathHook?.Dispose(); + ResolveDemiMtrlPathHook?.Dispose(); + ResolveDemiPapPathHook?.Dispose(); + ResolveDemiPhybPathHook?.Dispose(); + ResolveDemiSklbPathHook?.Dispose(); + ResolveDemiSkpPathHook?.Dispose(); + ResolveDemiTmbPathHook?.Dispose(); + ResolveDemiVfxPathHook?.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Human.cs b/Penumbra/Interop/Resolver/PathResolver.Human.cs index 1ce81342..b4bb8fcd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Human.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Human.cs @@ -14,12 +14,6 @@ public unsafe partial class PathResolver // [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )] // public IntPtr* DrawObjectVTable; // - // [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectDemihumanVTable; - // - // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectMonsterVTable; - // // public const int ResolveRootIdx = 71; public const int ResolveSklbIdx = 72; diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index a6124a93..3892fcbd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -136,6 +136,43 @@ public unsafe partial class PathResolver private IntPtr ResolveMonsterVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) => ResolvePathDetour( drawObject, ResolveMonsterVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // Demihumans + private IntPtr ResolveDemiDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDemiDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveDemiEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveDemiEidPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveDemiImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDemiImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveDemiMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolvePathDetour( drawObject, ResolveDemiMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveDemiMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + => ResolvePathDetour( drawObject, ResolveDemiMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); + + private IntPtr ResolveDemiMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveDemiMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveDemiPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveDemiPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveDemiPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDemiPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveDemiSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDemiSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveDemiSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveDemiSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveDemiTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePathDetour( drawObject, ResolveDemiTmbPathHook!.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveDemiVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePathDetour( drawObject, ResolveDemiVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index c634755a..d12e6d00 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -27,6 +27,7 @@ public partial class PathResolver : IDisposable SetupHumanHooks(); SetupWeaponHooks(); SetupMonsterHooks(); + SetupDemiHooks(); SetupMetaHooks(); } @@ -105,6 +106,7 @@ public partial class PathResolver : IDisposable EnableHumanHooks(); EnableWeaponHooks(); EnableMonsterHooks(); + EnableDemiHooks(); EnableMtrlHooks(); EnableDataHooks(); EnableMetaHooks(); @@ -124,6 +126,7 @@ public partial class PathResolver : IDisposable DisableHumanHooks(); DisableWeaponHooks(); DisableMonsterHooks(); + DisableDemiHooks(); DisableMtrlHooks(); DisableDataHooks(); DisableMetaHooks(); @@ -141,6 +144,7 @@ public partial class PathResolver : IDisposable DisposeHumanHooks(); DisposeWeaponHooks(); DisposeMonsterHooks(); + DisposeDemiHooks(); DisposeMtrlHooks(); DisposeDataHooks(); DisposeMetaHooks(); From 018be13216c1a363b7cf4d309b4f2cd4d56ac0ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 11:11:13 +0200 Subject: [PATCH 0287/2451] Use Current collection instead of Default for Effective Files and Changed Items. --- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 2 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 596a48f0..6eb6c5ca 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -87,7 +87,7 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "id", flags, 100 * ImGuiHelpers.GlobalScale ); - var items = Penumbra.CollectionManager.Default.ChangedItems; + var items = Penumbra.CollectionManager.Current.ChangedItems; var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 4d96423c..38c37485 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -50,7 +50,7 @@ public partial class ConfigWindow ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); - DrawEffectiveRows( Penumbra.CollectionManager.Default, skips, height, + DrawEffectiveRows( Penumbra.CollectionManager.Current, skips, height, _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); } From c097b634abcd2bd325146b90cbf8e9e38237452f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 11:11:55 +0200 Subject: [PATCH 0288/2451] Print a log message when Penumbra finished loading containing version and hash. --- Penumbra/Penumbra.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 1ae066db..e4beecf0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -152,6 +152,10 @@ public class Penumbra : IDisposable { PluginLog.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); } + else + { + PluginLog.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); + } } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) From 27650708f02ff77b1758b52b9878b548d9025b7e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 12:45:04 +0200 Subject: [PATCH 0289/2451] Fix collection in use not updating on first load. --- Penumbra/Collections/CollectionManager.Active.cs | 7 +++++-- Penumbra/Collections/CollectionManager.cs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index f06a574f..c75b7e44 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -81,11 +81,14 @@ public partial class ModCollection break; } - CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); - + + UpdateCurrentCollectionInUse(); CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + public void SetCollection( ModCollection collection, Type type, string? characterName = null ) => SetCollection( collection.Index, type, characterName ); diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 76515f0c..411e7de5 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -72,6 +72,7 @@ public partial class ModCollection CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); + UpdateCurrentCollectionInUse(); } public void Dispose() From 58e46accae34a87215b726c3fc2b72135add4959 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 12:46:06 +0200 Subject: [PATCH 0290/2451] Add errors when loading from elsewhere than installedPlugins or devPlugins/Penumbra existing and containing dlls in Release mode --- Penumbra/Penumbra.cs | 37 +++++++++++++++++- Penumbra/UI/ConfigWindow.Misc.cs | 13 ++++--- Penumbra/UI/ConfigWindow.cs | 66 +++++++++++++++++++++----------- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e4beecf0..215261b0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,10 +29,14 @@ public class MainClass : IDalamudPlugin { private Penumbra? _penumbra; private readonly CharacterUtility _characterUtility; + public static bool DevPenumbraExists; + public static bool IsNotInstalledPenumbra; public MainClass( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); + DevPenumbraExists = CheckDevPluginPenumbra(); + IsNotInstalledPenumbra = CheckIsNotInstalled(); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); _characterUtility = new CharacterUtility(); _characterUtility.LoadingFinished += () @@ -47,6 +51,38 @@ public class MainClass : IDalamudPlugin public string Name => Penumbra.Name; + + // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. + private static bool CheckDevPluginPenumbra() + { +#if !DEBUG + var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); + var dir = new DirectoryInfo( path ); + + try + { + return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + return true; + } +#else + return false; +#endif + } + + // Check if the loaded version of penumbra itself is in devPlugins. + private static bool CheckIsNotInstalled() + { +#if !DEBUG + return !Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Name.Equals( "installedPlugins", + StringComparison.InvariantCultureIgnoreCase ) ?? true; +#else + return false; +#endif + } } public class Penumbra : IDisposable @@ -71,7 +107,6 @@ public class Penumbra : IDisposable public static FrameworkManager Framework { get; private set; } = null!; public static int ImcExceptions = 0; - public readonly ResourceLogger ResourceLogger; public readonly PathResolver PathResolver; public readonly MusicManager MusicManager; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 6f70126a..92689787 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -104,7 +104,7 @@ public partial class ConfigWindow _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; - using var combo = ImRaii.Combo( label, current.Name ); + using var combo = ImRaii.Combo( label, current.Name ); if( combo ) { foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) @@ -128,20 +128,23 @@ public partial class ConfigWindow if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) { - fileManager.CustomSideBarItems.Add( ("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1) ); + fileManager.CustomSideBarItems.Add( ( "Downloads", downloadsFolder, FontAwesomeIcon.Download, -1 ) ); } if( Functions.GetQuickAccessFolders( out var folders ) ) { foreach( var ((name, path), idx) in folders.WithIndex() ) { - fileManager.CustomSideBarItems.Add( ($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1) ); + fileManager.CustomSideBarItems.Add( ( $"{name}##{idx}", path, FontAwesomeIcon.Folder, -1 ) ); } } + // Add Penumbra Root. This is not updated if the root changes right now. + fileManager.CustomSideBarItems.Add( ("Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0) ); + // Remove Videos and Music. - fileManager.CustomSideBarItems.Add( ("Videos", string.Empty, 0, -1) ); - fileManager.CustomSideBarItems.Add( ("Music", string.Empty, 0, -1) ); + fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); + fileManager.CustomSideBarItems.Add( ( "Music", string.Empty, 0, -1 ) ); return fileManager; } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ccfac393..edaaddcf 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -55,32 +55,39 @@ public sealed partial class ConfigWindow : Window, IDisposable { if( Penumbra.ImcExceptions > 0 ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); - ImGui.NewLine(); - ImGui.NewLine(); - ImGui.TextWrapped( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "Please use the Launcher's Repair Game Files function to repair your client installation." ); - color.Pop(); - - ImGui.NewLine(); - ImGui.NewLine(); - SettingsTab.DrawDiscordButton( 0 ); - ImGui.SameLine(); - SettingsTab.DrawSupportButton(); - return; } - - using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); - SetupSizes(); - _settingsTab.Draw(); - DrawModsTab(); - _collectionsTab.Draw(); - DrawChangedItemTab(); - _effectiveTab.Draw(); - _debugTab.Draw(); - _resourceTab.Draw(); + else if( MainClass.IsNotInstalledPenumbra ) + { + DrawProblemWindow( + $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." ); + } + else if( MainClass.DevPenumbraExists ) + { + DrawProblemWindow( + $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode." ); + } + else + { + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); + SetupSizes(); + _settingsTab.Draw(); + DrawModsTab(); + _collectionsTab.Draw(); + DrawChangedItemTab(); + _effectiveTab.Draw(); + _debugTab.Draw(); + _resourceTab.Draw(); + } } catch( Exception e ) { @@ -88,6 +95,21 @@ public sealed partial class ConfigWindow : Window, IDisposable } } + private static void DrawProblemWindow( string text ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextWrapped( text ); + color.Pop(); + + ImGui.NewLine(); + ImGui.NewLine(); + SettingsTab.DrawDiscordButton( 0 ); + ImGui.SameLine(); + SettingsTab.DrawSupportButton(); + } + public void Dispose() { _selector.Dispose(); From 2103ae3053b4f3ec7af404e0a4a35dbb1a54d570 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 18 Jun 2022 13:11:25 +0200 Subject: [PATCH 0291/2451] Revert "formating fixes" This reverts commit 1504af9f3dc95c624557cdbfcc4f34041a466d00. --- Penumbra/Api/PenumbraApi.cs | 55 ++++----- Penumbra/Api/PenumbraIpc.cs | 130 ++++++++++---------- Penumbra/Collections/ModCollection.Cache.cs | 108 ++++++++-------- 3 files changed, 144 insertions(+), 149 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9fcd20cc..2fd52b99 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,7 +18,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; public event EventHandler? ObjectIsRedrawn; @@ -29,8 +29,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; } @@ -42,8 +42,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void Dispose() { _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; - _penumbra = null; - _lumina = null; + _penumbra = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -57,7 +57,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -112,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -138,23 +138,22 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? - new List< Utf8GamePath >(); + var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); if( ret.Count == 0 ) ret.Add( gamePath ); return ret.Select( r => r.ToString() ).ToArray(); } - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + private T? GetFileIntern( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); + return _lumina?.GetFileFromDisk( resolvedPath ); } - return Dalamud.GameData.GetFile< T >( resolvedPath ); + return Dalamud.GameData.GetFile( resolvedPath ); } catch( Exception e ) { @@ -163,13 +162,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); + public T? GetFile( string gamePath ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath ) ); - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + public T? GetFile( string gamePath, string characterName ) where T : FileResource + => GetFileIntern( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -185,7 +184,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); + return new Dictionary(); } catch( Exception e ) { @@ -194,7 +193,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList< string > GetCollections() + public IList GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -216,28 +215,27 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? ( collection.Name, true ) - : ( Penumbra.CollectionManager.Default.Name, false ); + ? (collection.Name, true) + : (Penumbra.CollectionManager.Default.Name, false); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.Name ); + return (obj, collection.Name); } - public IList< (string, string) > GetModList() + public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); } - public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + public Dictionary? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, - string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, Dictionary, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); @@ -253,8 +251,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi 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 ) + 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 ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 1d197b07..f5c59830 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; + internal ICallGateProvider? ProviderInitialized; + internal ICallGateProvider? ProviderDisposed; + internal ICallGateProvider? ProviderApiVersion; + internal ICallGateProvider? ProviderGetModDirectory; + internal ICallGateProvider? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,17 +111,17 @@ 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"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderObjectIsRedrawn; + internal ICallGateProvider? ProviderRedrawName; + internal ICallGateProvider? ProviderRedrawIndex; + internal ICallGateProvider? ProviderRedrawObject; + internal ICallGateProvider? ProviderRedrawAll; + internal ICallGateProvider ProviderObjectIsRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -138,7 +138,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -148,7 +148,7 @@ public partial class PenumbraIpc try { - ProviderRedrawIndex = pi.GetIpcProvider< int, int, object >( LabelProviderRedrawIndex ); + ProviderRedrawIndex = pi.GetIpcProvider( LabelProviderRedrawIndex ); ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -158,7 +158,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -168,7 +168,7 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -178,8 +178,8 @@ public partial class PenumbraIpc try { - ProviderObjectIsRedrawn = pi.GetIpcProvider< string, string >( LabelProviderObjectIsRedrawn ); - Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; + ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); + Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; } catch( Exception e ) { @@ -189,7 +189,7 @@ public partial class PenumbraIpc private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) { - ProviderObjectIsRedrawn?.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); + ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -204,21 +204,21 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; - internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; + internal ICallGateProvider? ProviderResolveDefault; + internal ICallGateProvider? ProviderResolveCharacter; + internal ICallGateProvider? ProviderGetDrawObjectInfo; + internal ICallGateProvider? ProviderReverseResolvePath; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -228,7 +228,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -238,7 +238,7 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) @@ -248,7 +248,7 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); } catch( Exception e ) @@ -268,12 +268,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; + internal ICallGateProvider? ProviderChangedItemTooltip; + internal ICallGateProvider? ProviderChangedItemClick; + internal ICallGateProvider>? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -291,8 +291,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -301,8 +301,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -311,7 +311,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -330,23 +330,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; - internal ICallGateProvider< IList< string > >? ProviderGetCollections; - internal ICallGateProvider< string >? ProviderCurrentCollectionName; - internal ICallGateProvider< string >? ProviderDefaultCollectionName; - internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + internal ICallGateProvider>? ProviderGetMods; + internal ICallGateProvider>? ProviderGetCollections; + internal ICallGateProvider? ProviderCurrentCollectionName; + internal ICallGateProvider? ProviderDefaultCollectionName; + internal ICallGateProvider? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -356,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -366,7 +366,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -376,7 +376,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -386,7 +386,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 4b280697..c1ed8f1c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,8 +12,7 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); - -public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); +public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -21,17 +20,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary> _conflicts = new(); - public IEnumerable< SingleArray< ModConflicts > > AllConflicts + public IEnumerable> AllConflicts => _conflicts.Values; - public SingleArray< ModConflicts > Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + public SingleArray Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -39,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + public IReadOnlyDictionary, object?)> ChangedItems { get { @@ -51,15 +50,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -72,7 +71,7 @@ public partial class ModCollection } if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } @@ -80,10 +79,10 @@ public partial class ModCollection return candidate.Path; } - public List< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) + public List ReverseResolvePath( FullPath localFilePath ) { string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); - return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) + return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) .Select( kvp => kvp.Key ).ToList(); } @@ -92,38 +91,38 @@ public partial class ModCollection switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[ modIdx ], true ); + AddMod( Penumbra.ModManager[modIdx], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } - else if( _collection[ modIdx ].Settings?.Enabled == true ) + else if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } else { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) + if( _collection[modIdx].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); + ReloadMod( Penumbra.ModManager[modIdx], true ); } break; @@ -207,7 +206,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[ conflict.Mod2 ] = newConflicts; + _conflicts[conflict.Mod2] = newConflicts; } else { @@ -231,7 +230,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[ mod.Index ].Settings; + var settings = _collection[mod.Index].Settings; if( settings is not { Enabled: true } ) { return; @@ -244,23 +243,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[ groupIndex ]; + var config = settings.Settings[groupIndex]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[ ( int )config ], mod ); + AddSubMod( group[( int )config], mod ); break; case SelectType.Multi: - { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - AddSubMod( option, mod ); - } + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } - break; - } + break; + } } } @@ -313,7 +312,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[ path ]; + var modPath = ResolvedFiles[path]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -322,14 +321,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[ path ] = new ModPath( mod, file ); + ResolvedFiles[path] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -351,7 +350,7 @@ public partial class ModCollection } else { - _conflicts[ mod ] = changedConflicts; + _conflicts[mod] = changedConflicts; } } @@ -360,8 +359,8 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { @@ -369,8 +368,7 @@ public partial class ModCollection foreach( var conflict in tmpConflicts ) { if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && - conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); } @@ -379,7 +377,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -389,10 +387,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List { data }; + _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -450,15 +448,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[ name ] = ( data.Item1, x + y ); + _changedItems[name] = (data.Item1, x + y); } } } From 54f2e5c58f648274be6560322c556e555a7c4b05 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 18 Jun 2022 13:11:34 +0200 Subject: [PATCH 0292/2451] Revert "add Penumbra.ObjectIsRedrawn and Penumbra.ReverseResolvePath to API" This reverts commit 45d8d58ce2fc680dfb7c511fec44ea3bd9e5865e. --- Penumbra/Api/IPenumbraApi.cs | 4 - Penumbra/Api/PenumbraApi.cs | 64 +++----- Penumbra/Api/PenumbraIpc.cs | 142 +++++++----------- .../Collections/ModCollection.Cache.Access.cs | 1 - Penumbra/Collections/ModCollection.Cache.cs | 107 ++++++------- Penumbra/Interop/ObjectReloader.cs | 3 - 6 files changed, 127 insertions(+), 194 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index e14df92b..37265ce9 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -51,7 +51,6 @@ public interface IPenumbraApi : IPenumbraApiBase // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; - event EventHandler? ObjectIsRedrawn; // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); @@ -73,9 +72,6 @@ public interface IPenumbraApi : IPenumbraApiBase // Returns the given gamePath if penumbra would not manipulate it. public string ResolvePath( string gamePath, string characterName ); - // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public string[] ReverseResolvePath( string moddedPath, string characterName ); - // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2fd52b99..a81ab204 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,9 +18,8 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion { get; } = 4; - private Penumbra? _penumbra; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; - public event EventHandler? ObjectIsRedrawn; public bool Valid => _penumbra != null; @@ -31,19 +30,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( Dalamud.GameData ); - _penumbra.ObjectReloader.ObjectIsRedrawn += ObjectReloader_ObjectIsRedrawn; - } - - private void ObjectReloader_ObjectIsRedrawn( object? sender, EventArgs e ) - { - ObjectIsRedrawn?.Invoke( sender, e ); } public void Dispose() { - _penumbra!.ObjectReloader.ObjectIsRedrawn -= ObjectReloader_ObjectIsRedrawn; _penumbra = null; - _lumina = null; + _lumina = null; } public event ChangedItemClick? ChangedItemClicked; @@ -57,7 +49,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IPluginConfiguration GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); } public event ChangedItemHover? ChangedItemTooltip; @@ -112,7 +104,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } @@ -129,31 +121,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.Character( characterName ) ); } - public string[] ReverseResolvePath( string path, string characterName ) - { - CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } - - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = Penumbra.CollectionManager.Character( characterName ).ResolveReversePath( new FullPath( path ) ) ?? new List(); - if( ret.Count == 0 ) ret.Add( gamePath ); - return ret.Select( r => r.ToString() ).ToArray(); - } - - private T? GetFileIntern( string resolvedPath ) where T : FileResource + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource { CheckInitialized(); try { if( Path.IsPathRooted( resolvedPath ) ) { - return _lumina?.GetFileFromDisk( resolvedPath ); + return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - return Dalamud.GameData.GetFile( resolvedPath ); + return Dalamud.GameData.GetFile< T >( resolvedPath ); } catch( Exception e ) { @@ -162,13 +140,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public T? GetFile( string gamePath ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath ) ); + public T? GetFile< T >( string gamePath ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath ) ); - public T? GetFile( string gamePath, string characterName ) where T : FileResource - => GetFileIntern( ResolvePath( gamePath, characterName ) ); + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); - public IReadOnlyDictionary GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) { CheckInitialized(); try @@ -184,7 +162,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary(); + return new Dictionary< string, object? >(); } catch( Exception e ) { @@ -193,7 +171,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public IList GetCollections() + public IList< string > GetCollections() { CheckInitialized(); return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); @@ -215,27 +193,27 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) - ? (collection.Name, true) - : (Penumbra.CollectionManager.Default.Name, false); + ? ( collection.Name, true ) + : ( Penumbra.CollectionManager.Default.Name, false ); } public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); - return (obj, collection.Name); + return ( obj, collection.Name ); } - public IList<(string, string)> GetModList() + public IList< (string, string) > GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => (m.ModPath.Name, m.Name.Text) ).ToArray(); + return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } - public Dictionary? GetAvailableModSettings( string modDirectory, string modName ) + public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) => throw new NotImplementedException(); - public (PenumbraApiEc, (bool, int, Dictionary, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, bool allowInheritance ) => throw new NotImplementedException(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index f5c59830..d8dc6db0 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -38,23 +38,23 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider? ProviderInitialized; - internal ICallGateProvider? ProviderDisposed; - internal ICallGateProvider? ProviderApiVersion; - internal ICallGateProvider? ProviderGetModDirectory; - internal ICallGateProvider? ProviderGetConfiguration; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { try { - ProviderInitialized = pi.GetIpcProvider( LabelProviderInitialized ); + ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); } catch( Exception e ) { @@ -63,7 +63,7 @@ public partial class PenumbraIpc try { - ProviderDisposed = pi.GetIpcProvider( LabelProviderDisposed ); + ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); } catch( Exception e ) { @@ -72,7 +72,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderGetModDirectory = pi.GetIpcProvider( LabelProviderGetModDirectory ); + ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); } catch( Exception e ) @@ -92,7 +92,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -111,17 +111,15 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; + public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; - public const string LabelProviderObjectIsRedrawn = "Penumbra.ObjectIsRedrawn"; + public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; - internal ICallGateProvider? ProviderRedrawName; - internal ICallGateProvider? ProviderRedrawIndex; - internal ICallGateProvider? ProviderRedrawObject; - internal ICallGateProvider? ProviderRedrawAll; - internal ICallGateProvider ProviderObjectIsRedrawn; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; private static RedrawType CheckRedrawType( int value ) { @@ -138,7 +136,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -158,7 +156,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider( LabelProviderRedrawObject ); + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -168,28 +166,13 @@ public partial class PenumbraIpc try { - ProviderRedrawAll = pi.GetIpcProvider( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); } - - try - { - ProviderObjectIsRedrawn = pi.GetIpcProvider( LabelProviderObjectIsRedrawn ); - Api.ObjectIsRedrawn += Api_ObjectIsRedrawn; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderObjectIsRedrawn}:\n{e}" ); - } - } - - private void Api_ObjectIsRedrawn( object? sender, EventArgs e ) - { - ProviderObjectIsRedrawn.SendMessage( ( ( GameObject? )sender )?.Name.ToString() ?? "" ); } private void DisposeRedrawProviders() @@ -198,27 +181,24 @@ public partial class PenumbraIpc ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); - Api.ObjectIsRedrawn -= Api_ObjectIsRedrawn; } } public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; - public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - internal ICallGateProvider? ProviderResolveDefault; - internal ICallGateProvider? ProviderResolveCharacter; - internal ICallGateProvider? ProviderGetDrawObjectInfo; - internal ICallGateProvider? ProviderReverseResolvePath; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; private void InitializeResolveProviders( DalamudPluginInterface pi ) { try { - ProviderResolveDefault = pi.GetIpcProvider( LabelProviderResolveDefault ); + ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -228,7 +208,7 @@ public partial class PenumbraIpc try { - ProviderResolveCharacter = pi.GetIpcProvider( LabelProviderResolveCharacter ); + ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); } catch( Exception e ) @@ -238,23 +218,13 @@ public partial class PenumbraIpc try { - ProviderGetDrawObjectInfo = pi.GetIpcProvider( LabelProviderGetDrawObjectInfo ); + ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } - - try - { - ProviderReverseResolvePath = pi.GetIpcProvider( LabelProviderReverseResolvePath ); - ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); - } } private void DisposeResolveProviders() @@ -268,12 +238,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; + public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider? ProviderChangedItemTooltip; - internal ICallGateProvider? ProviderChangedItemClick; - internal ICallGateProvider>? ProviderGetChangedItems; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) { @@ -291,8 +261,8 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; + ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); + Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) { @@ -301,8 +271,8 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; + ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); + Api.ChangedItemClicked += OnClick; } catch( Exception e ) { @@ -311,7 +281,7 @@ public partial class PenumbraIpc try { - ProviderGetChangedItems = pi.GetIpcProvider>( LabelProviderGetChangedItems ); + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); } catch( Exception e ) @@ -330,23 +300,23 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - internal ICallGateProvider>? ProviderGetMods; - internal ICallGateProvider>? ProviderGetCollections; - internal ICallGateProvider? ProviderCurrentCollectionName; - internal ICallGateProvider? ProviderDefaultCollectionName; - internal ICallGateProvider? ProviderCharacterCollectionName; + internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; + internal ICallGateProvider< IList< string > >? ProviderGetCollections; + internal ICallGateProvider< string >? ProviderCurrentCollectionName; + internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; private void InitializeDataProviders( DalamudPluginInterface pi ) { try { - ProviderGetMods = pi.GetIpcProvider>( LabelProviderGetMods ); + ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); ProviderGetMods.RegisterFunc( Api.GetModList ); } catch( Exception e ) @@ -356,7 +326,7 @@ public partial class PenumbraIpc try { - ProviderGetCollections = pi.GetIpcProvider>( LabelProviderGetCollections ); + ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); ProviderGetCollections.RegisterFunc( Api.GetCollections ); } catch( Exception e ) @@ -366,7 +336,7 @@ public partial class PenumbraIpc try { - ProviderCurrentCollectionName = pi.GetIpcProvider( LabelProviderCurrentCollectionName ); + ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); } catch( Exception e ) @@ -376,7 +346,7 @@ public partial class PenumbraIpc try { - ProviderDefaultCollectionName = pi.GetIpcProvider( LabelProviderDefaultCollectionName ); + ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); } catch( Exception e ) @@ -386,7 +356,7 @@ public partial class PenumbraIpc try { - ProviderCharacterCollectionName = pi.GetIpcProvider( LabelProviderCharacterCollectionName ); + ProviderCharacterCollectionName = pi.GetIpcProvider< string, ( string, bool) >( LabelProviderCharacterCollectionName ); ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); } catch( Exception e ) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 871280a2..2e3bda92 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -44,7 +44,6 @@ public partial class ModCollection PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); } - public List? ResolveReversePath( FullPath path ) => _cache?.ReverseResolvePath( path ); public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index c1ed8f1c..6e959e69 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -12,7 +12,7 @@ using Penumbra.Util; namespace Penumbra.Collections; public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List Conflicts, bool HasPriority, bool Solved ); +public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -20,17 +20,17 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList, object?)> _changedItems = new(); - public readonly Dictionary ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary> _conflicts = new(); + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); - public IEnumerable> AllConflicts + public IEnumerable< SingleArray< ModConflicts > > AllConflicts => _conflicts.Values; - public SingleArray Conflicts( Mod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray(); + public SingleArray< ModConflicts > Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); // Count the number of changes of the effective file list. // This is used for material and imc changes. @@ -38,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems { get { @@ -50,15 +50,15 @@ public partial class ModCollection // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { - _collection = collection; - MetaManipulations = new MetaManager( collection ); - _collection.ModSettingChanged += OnModSettingChange; + _collection = collection; + MetaManipulations = new MetaManager( collection ); + _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; + _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; } @@ -79,50 +79,43 @@ public partial class ModCollection return candidate.Path; } - public List ReverseResolvePath( FullPath localFilePath ) - { - string strToSearchFor = localFilePath.FullName.Replace( '/', '\\' ).ToLower(); - return ResolvedFiles.Where( f => f.Value.Path.FullName.ToLower() == strToSearchFor ) - .Select( kvp => kvp.Key ).ToList(); - } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { switch( type ) { case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[modIdx], true ); + AddMod( Penumbra.ModManager[ modIdx ], true ); } else if( oldValue == 1 ) { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } - else if( _collection[modIdx].Settings?.Enabled == true ) + else if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } else { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[modIdx] ).Count > 0 ) + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; case ModSettingChange.Setting: - if( _collection[modIdx].Settings?.Enabled == true ) + if( _collection[ modIdx ].Settings?.Enabled == true ) { - ReloadMod( Penumbra.ModManager[modIdx], true ); + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } break; @@ -206,7 +199,7 @@ public partial class ModCollection var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); if( newConflicts.Count > 0 ) { - _conflicts[conflict.Mod2] = newConflicts; + _conflicts[ conflict.Mod2 ] = newConflicts; } else { @@ -230,7 +223,7 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. public void AddMod( Mod mod, bool addMetaChanges ) { - var settings = _collection[mod.Index].Settings; + var settings = _collection[ mod.Index ].Settings; if( settings is not { Enabled: true } ) { return; @@ -243,23 +236,23 @@ public partial class ModCollection continue; } - var config = settings.Settings[groupIndex]; + var config = settings.Settings[ groupIndex ]; switch( group.Type ) { case SelectType.Single: - AddSubMod( group[( int )config], mod ); + AddSubMod( group[ ( int )config ], mod ); break; case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) - { - AddSubMod( option, mod ); - } - - break; + AddSubMod( option, mod ); } + + break; + } } } @@ -312,7 +305,7 @@ public partial class ModCollection return; } - var modPath = ResolvedFiles[path]; + var modPath = ResolvedFiles[ path ]; // Lower prioritized option in the same mod. if( mod == modPath.Mod ) { @@ -321,14 +314,14 @@ public partial class ModCollection if( AddConflict( path, mod, modPath.Mod ) ) { - ResolvedFiles[path] = new ModPath( mod, file ); + ResolvedFiles[ path ] = new ModPath( mod, file ); } } // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -350,7 +343,7 @@ public partial class ModCollection } else { - _conflicts[mod] = changedConflicts; + _conflicts[ mod ] = changedConflicts; } } @@ -359,15 +352,15 @@ public partial class ModCollection // Returns if the added mod takes priority before the existing mod. private bool AddConflict( object data, Mod addedMod, Mod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; if( existingPriority < addedPriority ) { var tmpConflicts = Conflicts( existingMod ); foreach( var conflict in tmpConflicts ) { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) { AddConflict( data, addedMod, conflict.Mod2 ); @@ -377,7 +370,7 @@ public partial class ModCollection RemoveEmptyConflicts( existingMod, tmpConflicts, true ); } - var addedConflicts = Conflicts( addedMod ); + var addedConflicts = Conflicts( addedMod ); var existingConflicts = Conflicts( existingMod ); if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) { @@ -387,10 +380,10 @@ public partial class ModCollection else { // Add the same conflict list to both conflict directions. - var conflictList = new List { data }; - _conflicts[addedMod] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority ) ); - _conflicts[existingMod] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); } @@ -448,15 +441,15 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, (new SingleArray( modPath.Mod ), obj) ); + _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[name] = (data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); } else if( obj is int x && data.Item2 is int y ) { - _changedItems[name] = (data.Item1, x + y); + _changedItems[ name ] = ( data.Item1, x + y ); } } } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index d58d434c..058fc87c 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -25,7 +25,6 @@ public unsafe partial class ObjectReloader private static void EnableDraw( GameObject actor ) => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); - public event EventHandler? ObjectIsRedrawn; // Check whether we currently are in GPose. // Also clear the name list. @@ -282,8 +281,6 @@ public sealed unsafe partial class ObjectReloader : IDisposable break; default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); } - - ObjectIsRedrawn?.Invoke( actor, new EventArgs() ); } private static GameObject? GetLocalPlayer() From df1a75b58a994306155c2e93fed9669203905211 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 14:23:28 +0200 Subject: [PATCH 0293/2451] Add another hook for animations in character collections. --- .../Resolver/PathResolver.Animation.cs | 21 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Data.cs | 9 +++++++- Penumbra/Interop/Resolver/PathResolver.cs | 4 ++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 363ce1e2..53cb7be5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -72,4 +72,25 @@ public unsafe partial class PathResolver _animationLoadCollection = last; return ret; } + + public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); + + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )] + public Hook< LoadSomePap >? LoadSomePapHook; + + private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) + { + var timelinePtr = a1 + 0x50; + var last = _animationLoadCollection; + if( timelinePtr != IntPtr.Zero ) + { + var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); + if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) + { + _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + } + } + LoadSomePapHook!.Original( a1, a2, a3, a4 ); + _animationLoadCollection = last; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 71cda6c5..28241e9c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -93,6 +93,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Enable(); CharacterBaseLoadAnimationHook?.Enable(); LoadSomeAvfxHook?.Enable(); + LoadSomePapHook?.Enable(); } private void DisableDataHooks() @@ -105,6 +106,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Disable(); CharacterBaseLoadAnimationHook?.Disable(); LoadSomeAvfxHook?.Disable(); + LoadSomePapHook?.Disable(); } private void DisposeDataHooks() @@ -116,6 +118,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Dispose(); CharacterBaseLoadAnimationHook?.Dispose(); LoadSomeAvfxHook?.Dispose(); + LoadSomePapHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. @@ -272,8 +275,12 @@ public unsafe partial class PathResolver } // Housing Retainers - if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == (byte) ObjectKind.EventNpc && gameObject->DataID == 1011832 ) + if( Penumbra.Config.UseDefaultCollectionForRetainers + && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc + && gameObject->DataID == 1011832 ) + { return Penumbra.CollectionManager.Default; + } string? actorName = null; if( Penumbra.Config.PreferNamedCollectionsOverOwners ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index d12e6d00..ab4d208b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -76,6 +76,9 @@ public partial class PathResolver : IDisposable private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { + if( type == ResourceType.Pap && _.Path.EndsWith( '0', '1', '0', '.', 'p', 'a', 'p' ) ) + PluginLog.Information( $"PAPPITY PAP {_}" ); + if( _animationLoadCollection != null ) { switch( type ) @@ -84,6 +87,7 @@ public partial class PathResolver : IDisposable case ResourceType.Pap: case ResourceType.Avfx: case ResourceType.Atex: + case ResourceType.Scd: collection = _animationLoadCollection; return true; } From c578bd3a495fe639f418a9d0d0529e304ebf8a2c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 15:18:35 +0200 Subject: [PATCH 0294/2451] Add mod setting API functions. --- Penumbra/Api/IPenumbraApi.cs | 11 +- Penumbra/Api/PenumbraApi.cs | 173 ++++++++++++++++-- Penumbra/Collections/ModCollection.Changes.cs | 44 +++-- Penumbra/Mods/Manager/Mod.Manager.cs | 24 +++ Penumbra/Mods/Subclasses/ModSettings.cs | 32 +++- 5 files changed, 245 insertions(+), 39 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 25c828e1..5107562c 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -29,13 +29,14 @@ public enum PenumbraApiEc CollectionMissing = 2, ModMissing = 3, OptionGroupMissing = 4, - SettingMissing = 5, + OptionMissing = 5, CharacterCollectionExists = 6, LowerPriority = 7, InvalidGamePath = 8, FileMissing = 9, InvalidManipulation = 10, + InvalidArgument = 11, } public interface IPenumbraApi : IPenumbraApiBase @@ -75,7 +76,7 @@ public interface IPenumbraApi : IPenumbraApiBase public string ResolvePath( string gamePath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public IList ReverseResolvePath( string moddedPath, string characterName ); + public IList< string > ReverseResolvePath( string moddedPath, string characterName ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; @@ -109,12 +110,12 @@ public interface IPenumbraApi : IPenumbraApiBase // 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 IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); + public IDictionary< string, (IList< 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, IDictionary< string, IList >, bool)?) GetCurrentModSettings( string collectionName, + public (PenumbraApiEc, (bool, int, IDictionary< string, IList< 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. @@ -136,7 +137,7 @@ public interface IPenumbraApi : IPenumbraApiBase public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ); public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList options ); + IReadOnlyList< string > options ); // Create a temporary collection without actual settings but with a cache. diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 33b4dedf..19af38d8 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -12,12 +13,15 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { - public int ApiVersion { get; } = 4; + public int ApiVersion + => 4; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; public event GameObjectRedrawn? GameObjectRedrawn; @@ -231,28 +235,173 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) - => throw new NotImplementedException(); + { + CheckInitialized(); + return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) + ? mod.Groups.ToDictionary( g => g.Name, g => ( ( IList< string > )g.Select( o => o.Name ).ToList(), g.Type ) ) + : null; + } public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, - string modDirectory, string modName, - bool allowInheritance ) - => throw new NotImplementedException(); + string modDirectory, string modName, bool allowInheritance ) + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return ( PenumbraApiEc.CollectionMissing, null ); + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return ( PenumbraApiEc.ModMissing, null ); + } + + var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; + if( settings == null ) + { + return ( PenumbraApiEc.Okay, null ); + } + + var shareSettings = settings.ConvertToShareable( mod ); + return ( PenumbraApiEc.Okay, + ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); + } public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + + return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ) - => throw new NotImplementedException(); + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList< string > options ) - => throw new NotImplementedException(); + string optionName ) + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); + if( groupIdx < 0 ) + { + return PenumbraApiEc.OptionGroupMissing; + } + + var optionIdx = mod.Groups[ groupIdx ].IndexOf( o => o.Name == optionName ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx; + + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } + + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + IReadOnlyList< string > optionNames ) + { + CheckInitialized(); + if( optionNames.Count == 0 ) + { + return PenumbraApiEc.InvalidArgument; + } + + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); + if( groupIdx < 0 ) + { + return PenumbraApiEc.OptionGroupMissing; + } + + var group = mod.Groups[ groupIdx ]; + + uint setting = 0; + if( group.Type == SelectType.Single ) + { + var name = optionNames[ ^1 ]; + var optionIdx = group.IndexOf( o => o.Name == name ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + setting = ( uint )optionIdx; + } + else + { + foreach( var name in optionNames ) + { + var optionIdx = group.IndexOf( o => o.Name == name ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + setting |= 1u << optionIdx; + } + } + + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) => throw new NotImplementedException(); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 8009dc14..b2c9ffd5 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -24,17 +24,20 @@ public partial class ModCollection public event ModSettingChangeDelegate ModSettingChanged; // Enable or disable the mod inheritance of mod idx. - public void SetModInheritance( int idx, bool inherit ) + public bool SetModInheritance( int idx, bool inherit ) { if( FixInheritance( idx, inherit ) ) { ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); + return true; } + + return false; } // Set the enabled state mod idx to newValue if it differs from the current enabled state. // If mod idx is currently inherited, stop the inheritance. - public void SetModState( int idx, bool newValue ) + public bool SetModState( int idx, bool newValue ) { var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; if( newValue != oldValue ) @@ -42,7 +45,10 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Enabled = newValue; ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); + return true; } + + return false; } // Enable or disable the mod inheritance of every mod in mods. @@ -78,7 +84,7 @@ public partial class ModCollection // Set the priority of mod idx to newValue if it differs from the current priority. // If mod idx is currently inherited, stop the inheritance. - public void SetModPriority( int idx, int newValue ) + public bool SetModPriority( int idx, int newValue ) { var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; if( newValue != oldValue ) @@ -86,12 +92,15 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Priority = newValue; ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); + return true; } + + return false; } // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. // If mod idx is currently inherited, stop the inheritance. - public void SetModSetting( int idx, int groupIdx, uint newValue ) + public bool SetModSetting( int idx, int groupIdx, uint newValue ) { var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; var oldValue = settings?[ groupIdx ] ?? 0; @@ -100,31 +109,26 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); + return true; } + + return false; } // Change one of the available mod settings for mod idx discerned by type. // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. // The setting will also be automatically fixed if it is invalid for that setting group. // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - public void ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) + public bool ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) { - switch( type ) + return type switch { - case ModSettingChange.Inheritance: - SetModInheritance( idx, newValue != 0 ); - break; - case ModSettingChange.EnableState: - SetModState( idx, newValue != 0 ); - break; - case ModSettingChange.Priority: - SetModPriority( idx, newValue ); - break; - case ModSettingChange.Setting: - SetModSetting( idx, groupIdx, ( uint )newValue ); - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } + ModSettingChange.Inheritance => SetModInheritance( idx, newValue != 0 ), + ModSettingChange.EnableState => SetModState( idx, newValue != 0 ), + ModSettingChange.Priority => SetModPriority( idx, newValue ), + ModSettingChange.Setting => SetModSetting( idx, groupIdx, ( uint )newValue ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; } // Set inheritance of a mod without saving, diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 7a21a3a8..1306be5f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Penumbra.Mods; @@ -37,5 +38,28 @@ public sealed partial class Mod ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; } + + + // Try to obtain a mod by its directory name (unique identifier, preferred), + // or the first mod of the given name if no directory fits. + public bool TryGetMod( string modDirectory, string modName, [NotNullWhen( true )] out Mod? mod ) + { + mod = null; + foreach( var m in _mods ) + { + if( m.ModPath.Name == modDirectory ) + { + mod = m; + return true; + } + + if( m.Name == modName ) + { + mod ??= m; + } + } + + return mod != null; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index a6bd27de..98922fdd 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using OtterGui.Filesystem; +using Penumbra.Util; namespace Penumbra.Mods; @@ -10,7 +11,7 @@ namespace Penumbra.Mods; public class ModSettings { public static readonly ModSettings Empty = new(); - public List< uint > Settings { get; init; } = new(); + public List< uint > Settings { get; private init; } = new(); public int Priority { get; set; } public bool Enabled { get; set; } @@ -100,7 +101,7 @@ public class ModSettings private static uint FixSetting( IModGroup group, uint value ) => group.Type switch { - SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), + SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), _ => value, }; @@ -208,4 +209,31 @@ public class ModSettings return changes; } } + + // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. + // Does not repair settings but ignores settings not fitting to the given mod. + public (bool Enabled, int Priority, Dictionary< string, IList< string > > Settings) ConvertToShareable( Mod mod ) + { + var dict = new Dictionary< string, IList< string > >( Settings.Count ); + foreach( var (setting, idx) in Settings.WithIndex() ) + { + if( idx >= mod.Groups.Count ) + { + break; + } + + var group = mod.Groups[ idx ]; + if( group.Type == SelectType.Single && setting < group.Count ) + { + dict.Add( group.Name, new[] { group[ ( int )setting ].Name } ); + } + else + { + var list = group.Where( ( _, optionIdx ) => ( setting & ( 1 << optionIdx ) ) != 0 ).Select( o => o.Name ).ToList(); + dict.Add( group.Name, list ); + } + } + + return ( Enabled, Priority, dict ); + } } \ No newline at end of file From fc767589a21af0b4babe389538d818af63f2fa6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 16:00:20 +0200 Subject: [PATCH 0295/2451] Change everything in collection caches to use IMod and introduce TemporaryMod. --- Penumbra/Api/PenumbraApi.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Collections/ModCollection.Cache.cs | 81 ++++++++++--------- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 4 +- Penumbra/Meta/Manager/MetaManager.cs | 4 +- Penumbra/Mods/Editor/IMod.cs | 19 +++++ Penumbra/Mods/Editor/Mod.Editor.cs | 7 +- Penumbra/Mods/Mod.BasePath.cs | 12 ++- Penumbra/Mods/Mod.Meta.cs | 7 +- Penumbra/Mods/Mod.TemporaryMod.cs | 26 ++++++ Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 4 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 4 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 9 ++- 18 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 Penumbra/Mods/Editor/IMod.cs create mode 100644 Penumbra/Mods/Mod.TemporaryMod.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 19af38d8..895d6af9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -378,7 +378,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( group.Type == SelectType.Single ) { var name = optionNames[ ^1 ]; - var optionIdx = group.IndexOf( o => o.Name == name ); + var optionIdx = group.IndexOf( o => o.Name == optionNames[^1] ); if( optionIdx < 0 ) { return PenumbraApiEc.OptionMissing; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 5cf71fc2..24324553 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -65,8 +65,8 @@ public partial class ModCollection internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); - internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >(); + internal IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< IMod >, object?) >(); internal IEnumerable< SingleArray< ModConflicts > > AllConflicts => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index d4e75a2c..7ffdfb61 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -11,8 +11,8 @@ using Penumbra.Util; namespace Penumbra.Collections; -public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); +public record struct ModPath( IMod Mod, FullPath Path ); +public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -21,15 +21,15 @@ public partial class ModCollection private class Cache : IDisposable { private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); public readonly MetaManager MetaManipulations; - private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); + private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); public IEnumerable< SingleArray< ModConflicts > > AllConflicts => _conflicts.Values; - public SingleArray< ModConflicts > Conflicts( Mod mod ) + public SingleArray< ModConflicts > Conflicts( IMod mod ) => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); // Count the number of changes of the effective file list. @@ -38,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems { get { @@ -178,13 +178,13 @@ public partial class ModCollection } } - public void ReloadMod( Mod mod, bool addMetaChanges ) + public void ReloadMod( IMod mod, bool addMetaChanges ) { RemoveMod( mod, addMetaChanges ); AddMod( mod, addMetaChanges ); } - public void RemoveMod( Mod mod, bool addMetaChanges ) + public void RemoveMod( IMod mod, bool addMetaChanges ) { var conflicts = Conflicts( mod ); @@ -243,37 +243,40 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod( Mod mod, bool addMetaChanges ) + public void AddMod( IMod mod, bool addMetaChanges ) { - var settings = _collection[ mod.Index ].Settings; - if( settings is not { Enabled: true } ) + if( mod.Index >= 0 ) { - return; - } - - foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) - { - if( group.Count == 0 ) + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) { - continue; + return; } - var config = settings.Settings[ groupIndex ]; - switch( group.Type ) + foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) { - case SelectType.Single: - AddSubMod( group[ ( int )config ], mod ); - break; - case SelectType.Multi: + if( group.Count == 0 ) { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) - { - AddSubMod( option, mod ); - } + continue; + } - break; + var config = settings.Settings[ groupIndex ]; + switch( group.Type ) + { + case SelectType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } + + break; + } } } } @@ -297,7 +300,7 @@ public partial class ModCollection } // Add all files and possibly manipulations of a specific submod - private void AddSubMod( ISubMod subMod, Mod parentMod ) + private void AddSubMod( ISubMod subMod, IMod parentMod ) { foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) { @@ -320,7 +323,7 @@ public partial class ModCollection // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, Mod mod ) + private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) { if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) { @@ -343,7 +346,7 @@ public partial class ModCollection // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -372,10 +375,10 @@ public partial class ModCollection // Add a new conflict between the added mod and the existing mod. // Update all other existing conflicts between the existing mod and other mods if necessary. // Returns if the added mod takes priority before the existing mod. - private bool AddConflict( object data, Mod addedMod, Mod existingMod ) + private bool AddConflict( object data, IMod addedMod, IMod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; if( existingPriority < addedPriority ) { @@ -417,7 +420,7 @@ public partial class ModCollection // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, Mod mod ) + private void AddManipulation( MetaManipulation manip, IMod mod ) { if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) { @@ -463,7 +466,7 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, ( new SingleArray< IMod >( modPath.Mod ), obj ) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index eae00788..d9371c41 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerCmp : IDisposable { public CmpFile? File = null; - public readonly Dictionary< RspManipulation, Mod > Manipulations = new(); + public readonly Dictionary< RspManipulation, IMod > Manipulations = new(); public MetaManagerCmp() { } @@ -39,7 +39,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, Mod mod ) + public bool ApplyMod( RspManipulation m, IMod mod ) { #if USE_CMP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index bb5ec9d0..92a60470 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -17,7 +17,7 @@ public partial class MetaManager { public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, IMod > Manipulations = new(); public MetaManagerEqdp() { } @@ -51,7 +51,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, Mod mod ) + public bool ApplyMod( EqdpManipulation m, IMod mod ) { #if USE_EQDP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 9c980d67..831e26d9 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerEqp : IDisposable { public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EqpManipulation, IMod > Manipulations = new(); public MetaManagerEqp() { } @@ -39,7 +39,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, Mod mod ) + public bool ApplyMod( EqpManipulation m, IMod mod ) { #if USE_EQP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index ccc5f926..7af2609c 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -18,7 +18,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EstManipulation, IMod > Manipulations = new(); public MetaManagerEst() { } @@ -51,7 +51,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, Mod mod ) + public bool ApplyMod( EstManipulation m, IMod mod ) { #if USE_EST Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 49c29076..82833017 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerGmp : IDisposable { public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< GmpManipulation, IMod > Manipulations = new(); public MetaManagerGmp() { } @@ -38,7 +38,7 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, Mod mod ) + public bool ApplyMod( GmpManipulation m, IMod mod ) { #if USE_GMP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 23bdf125..35e7434f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -18,7 +18,7 @@ public partial class MetaManager public readonly struct MetaManagerImc : IDisposable { public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, Mod > Manipulations = new(); + public readonly Dictionary< ImcManipulation, IMod > Manipulations = new(); private readonly ModCollection _collection; private static int _imcManagerCount; @@ -65,7 +65,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, Mod mod ) + public bool ApplyMod( ImcManipulation m, IMod mod ) { #if USE_IMC Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 5d6c0a5b..a71e37b1 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -30,7 +30,7 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod ) + public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out IMod? mod ) { mod = manip.ManipulationType switch { @@ -86,7 +86,7 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, Mod mod ) + public bool ApplyMod( MetaManipulation m, IMod mod ) { return m.ManipulationType switch { diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs new file mode 100644 index 00000000..5b7b0f20 --- /dev/null +++ b/Penumbra/Mods/Editor/IMod.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public interface IMod +{ + LowerString Name { get; } + + public int Index { get; } + public int Priority { get; } + + public int TotalManipulations { get; } + + public ISubMod Default { get; } + public IReadOnlyList< IModGroup > Groups { get; } + + public IEnumerable< ISubMod > AllSubMods { get; } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index 74158a4e..455dcf8e 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; using Penumbra.Util; namespace Penumbra.Mods; -public partial class Mod +public partial class Mod : IMod { public partial class Editor : IDisposable { @@ -15,7 +12,7 @@ public partial class Mod public Editor( Mod mod, int groupIdx, int optionIdx ) { - _mod = mod; + _mod = mod; SetSubMod( groupIdx, optionIdx ); GroupIdx = groupIdx; _subMod = _mod._default; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 43581735..0add9356 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -17,6 +17,10 @@ public partial class Mod public DirectoryInfo ModPath { get; private set; } public int Index { get; private set; } = -1; + // Unused if Index < 0 but used for special temporary mods. + public int Priority + => 0; + private Mod( DirectoryInfo modPath ) => ModPath = modPath; @@ -30,7 +34,7 @@ public partial class Mod } var mod = new Mod( modPath ); - if( !mod.Reload(out _) ) + if( !mod.Reload( out _ ) ) { // Can not be base path not existing because that is checked before. PluginLog.Error( $"Mod at {modPath} without name is not supported." ); @@ -40,15 +44,17 @@ public partial class Mod return mod; } - private bool Reload(out MetaChangeType metaChange) + private bool Reload( out MetaChangeType metaChange ) { metaChange = MetaChangeType.Deletion; ModPath.Refresh(); if( !ModPath.Exists ) + { return false; + } metaChange = LoadMeta(); - if( metaChange.HasFlag(MetaChangeType.Deletion) || Name.Length == 0 ) + if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 ) { return false; } diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 3e052b3c..615e0dc1 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -24,10 +24,11 @@ public enum MetaChangeType : ushort public sealed partial class Mod { - public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." )) + public static readonly TemporaryMod ForcedFiles = new() { - Name = "Forced Files", - Index = -1, + Name = "Forced Files", + Index = -1, + Priority = int.MaxValue, }; public const uint CurrentFileVersion = 1; diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs new file mode 100644 index 00000000..6662f826 --- /dev/null +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public class TemporaryMod : IMod + { + public LowerString Name { get; init; } = LowerString.Empty; + public int Index { get; init; } = -2; + public int Priority { get; init; } = int.MaxValue; + + public int TotalManipulations + => Default.Manipulations.Count; + + public ISubMod Default { get; } = new SubMod(); + + public IReadOnlyList< IModGroup > Groups + => Array.Empty< IModGroup >(); + + public IEnumerable< ISubMod > AllSubMods + => new[] { Default }; + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 6eb6c5ca..b9105bda 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -23,13 +23,13 @@ public partial class ConfigWindow private void DrawChangedItemTab() { // Functions in here for less pollution. - bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) => ( _changedItemFilter.IsEmpty || ChangedItemName( item.Key, item.Value.Item2 ) .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) { ImGui.TableNextColumn(); DrawChangedItem( item.Key, item.Value.Item2, false ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 38c37485..b9a2213f 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -112,7 +112,7 @@ public partial class ConfigWindow { // We can treat all meta manipulations the same, // we are only really interested in their ToString function here. - static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp ) + static (object, IMod) Convert< T >( KeyValuePair< T, IMod > kvp ) => ( kvp.Key!, kvp.Value ); var it = m.Cmp.Manipulations.Select( Convert ) @@ -183,7 +183,7 @@ public partial class ConfigWindow } // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, Mod) pair ) + private static void DrawLine( (object, IMod) pair ) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index d1c4a838..a7af5ae4 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -128,16 +128,19 @@ public partial class ConfigWindow foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) { - if( ImGui.Selectable( conflict.Mod2.Name ) ) + if( ImGui.Selectable( conflict.Mod2.Name ) && conflict.Mod2 is Mod mod ) { - _window._selector.SelectByValue( conflict.Mod2 ); + _window._selector.SelectByValue( mod ); } ImGui.SameLine(); using( var color = ImRaii.PushColor( ImGuiCol.Text, conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) { - ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" ); + var priority = conflict.Mod2.Index < 0 + ? conflict.Mod2.Priority + : Penumbra.CollectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + ImGui.TextUnformatted( $"(Priority {priority})" ); } using var indent = ImRaii.PushIndent( 30f ); From 8b7dc8fa5b4a8e8b7412954faf89dd4310320b69 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 18 Jun 2022 21:44:26 +0000 Subject: [PATCH 0296/2451] [CI] Updating repo.json for refs/tags/0.5.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 3fca4ca9..fa95b882 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.1.0", - "TestingAssemblyVersion": "0.5.1.0", + "AssemblyVersion": "0.5.1.1", + "TestingAssemblyVersion": "0.5.1.1", "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.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", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c6e6c0098c6934a496b8f4e3264b71b5295264dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 23:54:52 +0200 Subject: [PATCH 0297/2451] Emergency Fix --- Penumbra/Penumbra.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 215261b0..4470d29a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,8 +77,11 @@ public class MainClass : IDalamudPlugin private static bool CheckIsNotInstalled() { #if !DEBUG - return !Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Name.Equals( "installedPlugins", - StringComparison.InvariantCultureIgnoreCase ) ?? true; + var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; + if (!ret) + PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + return !ret; #else return false; #endif From 37dcd0ba55784ffae8eeb55f0d647272f7865248 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 18 Jun 2022 21:57:17 +0000 Subject: [PATCH 0298/2451] [CI] Updating repo.json for refs/tags/0.5.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index fa95b882..08da37ac 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.1.1", - "TestingAssemblyVersion": "0.5.1.1", + "AssemblyVersion": "0.5.1.2", + "TestingAssemblyVersion": "0.5.1.2", "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.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9d43895f388fdff344e47aae6167c415ca1f6f6c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 02:43:25 +0200 Subject: [PATCH 0299/2451] Destroy clippers to stop leaking. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index d97a2692..729f12c5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d97a26923981db2a27d0172367c9e2841767f9b1 +Subproject commit 729f12c5560536fe77f2a09f3b76c1c3c4cdb1ff From c330859abc9b235f65332d662da0ab1390fd205c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 11:42:38 +0200 Subject: [PATCH 0300/2451] Add customizable hotkeys for mod deletion. --- OtterGui | 2 +- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +++-- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 10 ++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 729f12c5..6ce8ca81 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 729f12c5560536fe77f2a09f3b76c1c3c4cdb1ff +Subproject commit 6ce8ca816678e7a363f9f4a6f43f009f8d79c070 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index dfa22fd2..3d38a069 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration; using Dalamud.Logging; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Import; using Penumbra.UI.Classes; @@ -46,6 +47,7 @@ public partial class Configuration : IPluginConfiguration public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; public string DefaultImportFolder { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 51490733..a062ff89 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,6 +14,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; +using OtterGui.Classes; namespace Penumbra.UI.Classes; @@ -286,14 +287,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void DeleteModButton( Vector2 size ) { - var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; + var keys = Penumbra.Config.DeleteModModifier.IsActive(); var tt = SelectedLeaf == null ? "No mod selected." : "Delete the currently selected mod entirely from your drive.\n" + "This can not be undone."; if( !keys ) { - tt += "\nHold Control and Shift while clicking to delete the mod."; + tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; } if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index b545b1eb..22319bfc 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Filesystem; using OtterGui.Raii; +using OtterGui.Widgets; namespace Penumbra.UI; @@ -89,6 +90,15 @@ public partial class ConfigWindow Penumbra.Config.OpenFoldersByDefault = v; _window._selector.SetFilterDirty(); } ); + + Widget.DoubleModifierSelector( "Mod Deletion Modifier", + "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", _window._inputTextWidth.X, + Penumbra.Config.DeleteModModifier, + v => + { + Penumbra.Config.DeleteModModifier = v; + Penumbra.Config.Save(); + } ); ImGui.Dummy( _window._defaultSpace ); Checkbox( "Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", From c64743ee98446d9e028c75b3a9e6df25db3cebcc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 11:47:14 +0200 Subject: [PATCH 0301/2451] actually reset test branch to master release, not before master release. --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbad3c82..3b1efa01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,10 +75,10 @@ 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 branch -f test origin/master && git checkout master + git fetch origin master && git fetch origin test && 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 branch -f test origin/master && git checkout test git push origin test -f || true From 8422d36e4e927e5476c7ac2f24508a4a8e1bb36e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 15:00:24 +0200 Subject: [PATCH 0302/2451] Add another another animation hook. I hate animations. --- Penumbra/Interop/Resolver/PathResolver.Animation.cs | 13 +++++++++++++ Penumbra/Interop/Resolver/PathResolver.Data.cs | 3 +++ Penumbra/Interop/Resolver/PathResolver.cs | 3 --- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 53cb7be5..1d51b8df 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -73,6 +73,7 @@ public unsafe partial class PathResolver return ret; } + // Unknown what exactly this is but it seems to load a bunch of paps. public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )] @@ -93,4 +94,16 @@ public unsafe partial class PathResolver LoadSomePapHook!.Original( a1, a2, a3, a4 ); _animationLoadCollection = last; } + + // Seems to load character actions when zoning or changing class, maybe. + [Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )] + public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook; + + private void SomeActionLoadDetour( IntPtr gameObject ) + { + var last = _animationLoadCollection; + _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + SomeActionLoadHook!.Original( gameObject ); + _animationLoadCollection = last; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 28241e9c..7f12e600 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -94,6 +94,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Enable(); LoadSomeAvfxHook?.Enable(); LoadSomePapHook?.Enable(); + SomeActionLoadHook?.Enable(); } private void DisableDataHooks() @@ -107,6 +108,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Disable(); LoadSomeAvfxHook?.Disable(); LoadSomePapHook?.Disable(); + SomeActionLoadHook?.Disable(); } private void DisposeDataHooks() @@ -119,6 +121,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Dispose(); LoadSomeAvfxHook?.Dispose(); LoadSomePapHook?.Dispose(); + SomeActionLoadHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index ab4d208b..9c8111b7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -76,9 +76,6 @@ public partial class PathResolver : IDisposable private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { - if( type == ResourceType.Pap && _.Path.EndsWith( '0', '1', '0', '.', 'p', 'a', 'p' ) ) - PluginLog.Information( $"PAPPITY PAP {_}" ); - if( _animationLoadCollection != null ) { switch( type ) From d6d13594e0d61b10e1e698f07125266aec683c30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 19:20:02 +0200 Subject: [PATCH 0303/2451] Add Mare Synchronos and MUI API/IPC functions for testing. Not tested myself because how. --- Penumbra/Api/IPenumbraApi.cs | 45 +-- Penumbra/Api/ModsController.cs | 3 +- Penumbra/Api/PenumbraApi.cs | 287 +++++++++++++----- Penumbra/Api/PenumbraIpc.cs | 217 +++++++++++++ Penumbra/Api/SimpleRedirectManager.cs | 125 -------- Penumbra/Api/TempModManager.cs | 261 ++++++++++++++++ .../Collections/ModCollection.Cache.Access.cs | 18 ++ Penumbra/Collections/ModCollection.Cache.cs | 12 +- Penumbra/Collections/ModCollection.cs | 11 + .../Interop/Resolver/PathResolver.Data.cs | 3 +- .../Meta/Manipulations/MetaManipulation.cs | 2 + Penumbra/Mods/Mod.TemporaryMod.cs | 19 +- Penumbra/Penumbra.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 8 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 78 ++++- 15 files changed, 857 insertions(+), 240 deletions(-) delete mode 100644 Penumbra/Api/SimpleRedirectManager.cs create mode 100644 Penumbra/Api/TempModManager.cs diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 5107562c..9ba916d4 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,13 +1,9 @@ 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; @@ -24,7 +20,7 @@ public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ) public enum PenumbraApiEc { - Okay = 0, + Success = 0, NothingChanged = 1, CollectionMissing = 2, ModMissing = 3, @@ -37,6 +33,7 @@ public enum PenumbraApiEc FileMissing = 9, InvalidManipulation = 10, InvalidArgument = 11, + UnknownError = 255, } public interface IPenumbraApi : IPenumbraApiBase @@ -136,32 +133,36 @@ public interface IPenumbraApi : IPenumbraApiBase // 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, + public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList< 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, + // If 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 ); + // Can return Okay, CharacterCollectionExists or NothingChanged, as well as the name of the new temporary collection on success. + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ); - // Remove a temporary collection if it exists. + // Remove the temporary collection associated with characterName if it exists. // Can return Okay or NothingChanged. - public PenumbraApiEc RemoveTemporaryCollection( string collectionName ); + public PenumbraApiEc RemoveTemporaryCollection( string characterName ); + // Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections. + // Can return Okay, InvalidGamePath, or InvalidManipulation. + public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, + int priority ); - // 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 ); + // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary. + // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation. + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, + IReadOnlySet< string > manipCodes, + int priority ); - // Can return Okay, CollectionMissing, InvalidManipulation, LowerPriority, or NothingChanged. - public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ); + // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. + // Can return Okay or NothingDone. + public PenumbraApiEc RemoveTemporaryModAll( string tag, 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 ); + // Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary. + // Can return Okay or NothingDone. + public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ); } \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index a909aab4..082bbb25 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -37,7 +37,6 @@ public class ModsController : WebApiController return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), o => o.Value.Path.FullName - ) - ?? new Dictionary< string, string >(); + ); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 895d6af9..99898ba5 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -9,9 +9,11 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; using Newtonsoft.Json; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Util; @@ -61,24 +63,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemHover? ChangedItemTooltip; - internal bool HasTooltip - => ChangedItemTooltip != null; - - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); - - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); - - - private void CheckInitialized() - { - if( !Valid ) - { - throw new Exception( "PluginShare is not initialized." ); - } - } - public void RedrawObject( int tableIndex, RedrawType setting ) { CheckInitialized(); @@ -97,29 +81,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); } - private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) - { - GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); - } - public void RedrawAll( RedrawType setting ) { CheckInitialized(); _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) - { - if( !Penumbra.Config.EnableMods ) - { - return path; - } - - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); - return ret?.ToString() ?? path; - } - public string ResolvePath( string path ) { CheckInitialized(); @@ -145,25 +112,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ret.Select( r => r.ToString() ).ToList(); } - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource - { - CheckInitialized(); - try - { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } - - return Dalamud.GameData.GetFile< T >( resolvedPath ); - } - catch( Exception e ) - { - PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); - return null; - } - } - public T? GetFile< T >( string gamePath ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath ) ); @@ -259,11 +207,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; if( settings == null ) { - return ( PenumbraApiEc.Okay, null ); + return ( PenumbraApiEc.Success, null ); } var shareSettings = settings.ConvertToShareable( mod ); - return ( PenumbraApiEc.Okay, + return ( PenumbraApiEc.Success, ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); } @@ -281,7 +229,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } - return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) @@ -297,7 +245,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; } - return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) @@ -313,7 +261,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; } - return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, @@ -344,10 +292,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx; - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList< string > optionNames ) { CheckInitialized(); @@ -378,7 +326,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( group.Type == SelectType.Single ) { var name = optionNames[ ^1 ]; - var optionIdx = group.IndexOf( o => o.Name == optionNames[^1] ); + var optionIdx = group.IndexOf( o => o.Name == optionNames[ ^1 ] ); if( optionIdx < 0 ) { return PenumbraApiEc.OptionMissing; @@ -400,24 +348,213 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) - => throw new NotImplementedException(); + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) + { + CheckInitialized(); + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) + || Penumbra.TempMods.Collections.ContainsKey( character ) ) + { + return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); + } - public PenumbraApiEc RemoveTemporaryCollection( string collectionName ) - => throw new NotImplementedException(); + var name = Penumbra.TempMods.SetTemporaryCollection( tag, character ); + return ( PenumbraApiEc.Success, name ); + } - public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ) - => throw new NotImplementedException(); + public PenumbraApiEc RemoveTemporaryCollection( string character ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.ContainsKey( character ) ) + { + return PenumbraApiEc.NothingChanged; + } - public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ) - => throw new NotImplementedException(); + Penumbra.TempMods.RemoveTemporaryCollection( character ); + return PenumbraApiEc.Success; + } - public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ) - => throw new NotImplementedException(); + public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, + int priority ) + { + CheckInitialized(); + if( !ConvertPaths( paths, out var p ) ) + { + return PenumbraApiEc.InvalidGamePath; + } - public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ) - => throw new NotImplementedException(); + if( !ConvertManips( manipCodes, out var m ) ) + { + return PenumbraApiEc.InvalidManipulation; + } + + return Penumbra.TempMods.Register( tag, null, p, m, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, + IReadOnlySet< string > manipCodes, int priority ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !ConvertPaths( paths, out var p ) ) + { + return PenumbraApiEc.InvalidGamePath; + } + + if( !ConvertManips( manipCodes, out var m ) ) + { + return PenumbraApiEc.InvalidManipulation; + } + + return Penumbra.TempMods.Register( tag, collection, p, m, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ) + { + CheckInitialized(); + return Penumbra.TempMods.Unregister( tag, null, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + return Penumbra.TempMods.Unregister( tag, collection, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + } + + internal bool HasTooltip + => ChangedItemTooltip != null; + + internal void InvokeTooltip( object? it ) + => ChangedItemTooltip?.Invoke( it ); + + internal void InvokeClick( MouseButton button, object? it ) + => ChangedItemClicked?.Invoke( button, it ); + + + private void CheckInitialized() + { + if( !Valid ) + { + throw new Exception( "PluginShare is not initialized." ); + } + } + + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) + { + GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); + } + + // Resolve a path given by string for a specific collection. + private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) + { + if( !Penumbra.Config.EnableMods ) + { + return path; + } + + var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath( gamePath ); + return ret?.ToString() ?? path; + } + + // Get a file for a resolved path. + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + { + CheckInitialized(); + try + { + if( Path.IsPathRooted( resolvedPath ) ) + { + return _lumina?.GetFileFromDisk< T >( resolvedPath ); + } + + return Dalamud.GameData.GetFile< T >( resolvedPath ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + return null; + } + } + + + // Convert a dictionary of strings to a dictionary of gamepaths to full paths. + // Only returns true if all paths can successfully be converted and added. + private static bool ConvertPaths( IReadOnlyDictionary< string, string > redirections, + [NotNullWhen( true )] out Dictionary< Utf8GamePath, FullPath >? paths ) + { + paths = new Dictionary< Utf8GamePath, FullPath >( redirections.Count ); + foreach( var (gString, fString) in redirections ) + { + if( !Utf8GamePath.FromString( gString, out var path, false ) ) + { + paths = null; + return false; + } + + var fullPath = new FullPath( fString ); + if( !paths.TryAdd( path, fullPath ) ) + { + paths = null; + return false; + } + } + + return true; + } + + // Convert manipulations from transmitted base64 strings to actual manipulations. + // Only returns true if all conversions are successful and distinct. + private static bool ConvertManips( IReadOnlyCollection< string > manipStrings, + [NotNullWhen( true )] out HashSet< MetaManipulation >? manips ) + { + manips = new HashSet< MetaManipulation >( manipStrings.Count ); + foreach( var m in manipStrings ) + { + if( Functions.FromCompressedBase64< MetaManipulation >( m, out var manip ) != MetaManipulation.CurrentVersion ) + { + manips = null; + return false; + } + + if( !manips.Add( manip ) ) + { + manips = null; + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 14f3ea11..1aa4ce24 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -22,6 +22,8 @@ public partial class PenumbraIpc : IDisposable InitializeRedrawProviders( pi ); InitializeChangedItemProviders( pi ); InitializeDataProviders( pi ); + InitializeSettingProviders( pi ); + InitializeTempProviders( pi ); ProviderInitialized?.SendMessage(); } @@ -32,6 +34,8 @@ public partial class PenumbraIpc : IDisposable DisposeRedrawProviders(); DisposeResolveProviders(); DisposeGeneralProviders(); + DisposeSettingProviders(); + DisposeTempProviders(); ProviderDisposed?.SendMessage(); } } @@ -402,4 +406,217 @@ public partial class PenumbraIpc ProviderDefaultCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc(); } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderGetAvailableModSettings = "Penumbra.GetAvailableModSettings"; + public const string LabelProviderGetCurrentModSettings = "Penumbra.GetCurrentModSettings"; + public const string LabelProviderTryInheritMod = "Penumbra.TryInheritMod"; + public const string LabelProviderTrySetMod = "Penumbra.TrySetMod"; + public const string LabelProviderTrySetModPriority = "Penumbra.TrySetModPriority"; + public const string LabelProviderTrySetModSetting = "Penumbra.TrySetModSetting"; + public const string LabelProviderTrySetModSettings = "Penumbra.TrySetModSettings"; + + internal ICallGateProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >? ProviderGetAvailableModSettings; + + internal ICallGateProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >? + ProviderGetCurrentModSettings; + + internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTryInheritMod; + internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTrySetMod; + internal ICallGateProvider< string, string, string, int, PenumbraApiEc >? ProviderTrySetModPriority; + internal ICallGateProvider< string, string, string, string, string, PenumbraApiEc >? ProviderTrySetModSetting; + internal ICallGateProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >? ProviderTrySetModSettings; + + private void InitializeSettingProviders( DalamudPluginInterface pi ) + { + try + { + ProviderGetAvailableModSettings = + pi.GetIpcProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >( + LabelProviderGetAvailableModSettings ); + ProviderGetAvailableModSettings.RegisterFunc( Api.GetAvailableModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); + } + + try + { + ProviderGetCurrentModSettings = + pi.GetIpcProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( + LabelProviderGetCurrentModSettings ); + ProviderGetCurrentModSettings.RegisterFunc( Api.GetCurrentModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" ); + } + + try + { + ProviderTryInheritMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTryInheritMod ); + ProviderTryInheritMod.RegisterFunc( Api.TryInheritMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" ); + } + + try + { + ProviderTrySetMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTrySetMod ); + ProviderTrySetMod.RegisterFunc( Api.TrySetMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" ); + } + + try + { + ProviderTrySetModPriority = pi.GetIpcProvider< string, string, string, int, PenumbraApiEc >( LabelProviderTrySetModPriority ); + ProviderTrySetModPriority.RegisterFunc( Api.TrySetModPriority ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" ); + } + + try + { + ProviderTrySetModSetting = + pi.GetIpcProvider< string, string, string, string, string, PenumbraApiEc >( LabelProviderTrySetModSetting ); + ProviderTrySetModSetting.RegisterFunc( Api.TrySetModSetting ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" ); + } + + try + { + ProviderTrySetModSettings = + pi.GetIpcProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >( LabelProviderTrySetModSettings ); + ProviderTrySetModSettings.RegisterFunc( Api.TrySetModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" ); + } + } + + private void DisposeSettingProviders() + { + ProviderGetAvailableModSettings?.UnregisterFunc(); + ProviderGetCurrentModSettings?.UnregisterFunc(); + ProviderTryInheritMod?.UnregisterFunc(); + ProviderTrySetMod?.UnregisterFunc(); + ProviderTrySetModPriority?.UnregisterFunc(); + ProviderTrySetModSetting?.UnregisterFunc(); + ProviderTrySetModSettings?.UnregisterFunc(); + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderCreateTemporaryCollection = "Penumbra.CreateTemporaryCollection"; + public const string LabelProviderRemoveTemporaryCollection = "Penumbra.RemoveTemporaryCollection"; + public const string LabelProviderAddTemporaryModAll = "Penumbra.AddTemporaryModAll"; + public const string LabelProviderAddTemporaryMod = "Penumbra.AddTemporaryMod"; + public const string LabelProviderRemoveTemporaryModAll = "Penumbra.RemoveTemporaryModAll"; + public const string LabelProviderRemoveTemporaryMod = "Penumbra.RemoveTemporaryMod"; + + internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection; + internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection; + + internal ICallGateProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + ProviderAddTemporaryModAll; + + internal ICallGateProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + ProviderAddTemporaryMod; + + internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll; + internal ICallGateProvider< string, string, int, PenumbraApiEc >? ProviderRemoveTemporaryMod; + + private void InitializeTempProviders( DalamudPluginInterface pi ) + { + try + { + ProviderCreateTemporaryCollection = + pi.GetIpcProvider< string, string, bool, (PenumbraApiEc, string) >( LabelProviderCreateTemporaryCollection ); + ProviderCreateTemporaryCollection.RegisterFunc( Api.CreateTemporaryCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryCollection = + pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderRemoveTemporaryCollection ); + ProviderRemoveTemporaryCollection.RegisterFunc( Api.RemoveTemporaryCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" ); + } + + try + { + ProviderAddTemporaryModAll = + pi.GetIpcProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + LabelProviderAddTemporaryModAll ); + ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" ); + } + + try + { + ProviderAddTemporaryMod = + pi.GetIpcProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + LabelProviderAddTemporaryMod ); + ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryModAll = pi.GetIpcProvider< string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryModAll ); + ProviderRemoveTemporaryModAll.RegisterFunc( Api.RemoveTemporaryModAll ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryMod = pi.GetIpcProvider< string, string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryMod ); + ProviderRemoveTemporaryMod.RegisterFunc( Api.RemoveTemporaryMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" ); + } + } + + private void DisposeTempProviders() + { + ProviderCreateTemporaryCollection?.UnregisterFunc(); + ProviderRemoveTemporaryCollection?.UnregisterFunc(); + ProviderAddTemporaryModAll?.UnregisterFunc(); + ProviderAddTemporaryMod?.UnregisterFunc(); + ProviderRemoveTemporaryModAll?.UnregisterFunc(); + ProviderRemoveTemporaryMod?.UnregisterFunc(); + } } \ No newline at end of file diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs deleted file mode 100644 index 2f3e219f..00000000 --- a/Penumbra/Api/SimpleRedirectManager.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Logging; -using Penumbra.Collections; -using Penumbra.GameData.ByteString; -using Penumbra.Mods; - -namespace Penumbra.Api; - -public enum RedirectResult -{ - Registered = 0, - Success = 0, - IdenticalFileRegistered = 1, - InvalidGamePath = 2, - OtherOwner = 3, - NotRegistered = 4, - NoPermission = 5, - FilteredGamePath = 6, - UnknownError = 7, -} - -public class SimpleRedirectManager -{ - internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); - public readonly HashSet< string > AllowedTags = new(); - - public void Apply( IDictionary< Utf8GamePath, ModPath > dict ) - { - foreach( var (gamePath, (file, _)) in Replacements ) - { - dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) ); - } - } - - private RedirectResult? CheckPermission( string tag ) - => AllowedTags.Contains( tag ) ? null : RedirectResult.NoPermission; - - public RedirectResult IsRegistered( Utf8GamePath path, string tag ) - => CheckPermission( tag ) - ?? ( Replacements.TryGetValue( path, out var pair ) - ? pair.Tag == tag ? RedirectResult.Registered : RedirectResult.OtherOwner - : RedirectResult.NotRegistered ); - - public RedirectResult Register( Utf8GamePath path, FullPath file, string tag ) - { - if( CheckPermission( tag ) != null ) - { - return RedirectResult.NoPermission; - } - - if( Mod.FilterFile( path ) ) - { - return RedirectResult.FilteredGamePath; - } - - try - { - if( Replacements.TryGetValue( path, out var pair ) ) - { - if( file.Equals( pair.File ) ) - { - return RedirectResult.IdenticalFileRegistered; - } - - if( tag != pair.Tag ) - { - return RedirectResult.OtherOwner; - } - } - - Replacements[ path ] = ( file, tag ); - return RedirectResult.Success; - } - catch( Exception e ) - { - PluginLog.Error( $"[{tag}] Unknown Error registering simple redirect {path} -> {file}:\n{e}" ); - return RedirectResult.UnknownError; - } - } - - public RedirectResult Unregister( Utf8GamePath path, string tag ) - { - if( CheckPermission( tag ) != null ) - { - return RedirectResult.NoPermission; - } - - try - { - if( !Replacements.TryGetValue( path, out var pair ) ) - { - return RedirectResult.NotRegistered; - } - - if( tag != pair.Tag ) - { - return RedirectResult.OtherOwner; - } - - Replacements.Remove( path ); - return RedirectResult.Success; - } - catch( Exception e ) - { - PluginLog.Error( $"[{tag}] Unknown Error unregistering simple redirect {path}:\n{e}" ); - return RedirectResult.UnknownError; - } - } - - public RedirectResult Register( string path, string file, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? Register( gamePath, new FullPath( file ), tag ) - : RedirectResult.InvalidGamePath; - - public RedirectResult Unregister( string path, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? Unregister( gamePath, tag ) - : RedirectResult.InvalidGamePath; - - public RedirectResult IsRegistered( string path, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? IsRegistered( gamePath, tag ) - : RedirectResult.InvalidGamePath; -} \ No newline at end of file diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs new file mode 100644 index 00000000..b3be6577 --- /dev/null +++ b/Penumbra/Api/TempModManager.cs @@ -0,0 +1,261 @@ +using System.Collections.Generic; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.Api; + +public enum RedirectResult +{ + Success = 0, + IdenticalFileRegistered = 1, + NotRegistered = 2, + FilteredGamePath = 3, +} + +public class TempModManager +{ + private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); + private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); + private readonly Dictionary< string, ModCollection > _collections = new(); + + public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods + => _mods; + + public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections + => _modsForAllCollections; + + public IReadOnlyDictionary< string, ModCollection > Collections + => _collections; + + // These functions to check specific redirections or meta manipulations for existence are currently unused. + //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) + //{ + // var mod = GetExistingMod( tag, collection, null ); + // if( mod == null ) + // { + // priority = 0; + // fullPath = null; + // return false; + // } + // + // priority = mod.Priority; + // if( mod.Default.Files.TryGetValue( gamePath, out var f ) ) + // { + // fullPath = f; + // return true; + // } + // + // fullPath = null; + // return false; + //} + // + //public bool IsRegistered( string tag, ModCollection? collection, MetaManipulation meta, out MetaManipulation? manipulation, + // out int priority ) + //{ + // var mod = GetExistingMod( tag, collection, null ); + // if( mod == null ) + // { + // priority = 0; + // manipulation = null; + // return false; + // } + // + // priority = mod.Priority; + // // IReadOnlySet has no TryGetValue for some reason. + // if( ( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var manip ) ) + // { + // manipulation = manip; + // return true; + // } + // + // manipulation = null; + // return false; + //} + + // These functions for setting single redirections or manips are currently unused. + //public RedirectResult Register( string tag, ModCollection? collection, Utf8GamePath path, FullPath file, int priority ) + //{ + // if( Mod.FilterFile( path ) ) + // { + // return RedirectResult.FilteredGamePath; + // } + // + // var mod = GetOrCreateMod( tag, collection, priority, out var created ); + // + // var changes = !mod.Default.Files.TryGetValue( path, out var oldFile ) || !oldFile.Equals( file ); + // mod.SetFile( path, file ); + // ApplyModChange( mod, collection, created, false ); + // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; + //} + // + //public RedirectResult Register( string tag, ModCollection? collection, MetaManipulation meta, int priority ) + //{ + // var mod = GetOrCreateMod( tag, collection, priority, out var created ); + // var changes = !( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var oldMeta ) + // || !oldMeta.Equals( meta ); + // mod.SetManipulation( meta ); + // ApplyModChange( mod, collection, created, false ); + // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; + //} + + public RedirectResult Register( string tag, ModCollection? collection, Dictionary< Utf8GamePath, FullPath > dict, + HashSet< MetaManipulation > manips, int priority ) + { + var mod = GetOrCreateMod( tag, collection, priority, out var created ); + mod.SetAll( dict, manips ); + ApplyModChange( mod, collection, created, false ); + return RedirectResult.Success; + } + + public RedirectResult Unregister( string tag, ModCollection? collection, int? priority ) + { + var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; + if( list == null ) + { + return RedirectResult.NotRegistered; + } + + var removed = _modsForAllCollections.RemoveAll( m => + { + if( m.Name != tag || priority != null && m.Priority != priority.Value ) + { + return false; + } + + ApplyModChange( m, collection, false, true ); + return true; + } ); + + if( removed == 0 ) + { + return RedirectResult.NotRegistered; + } + + if( list.Count == 0 && collection != null ) + { + _mods.Remove( collection ); + } + + return RedirectResult.Success; + } + + public string SetTemporaryCollection( string tag, string characterName ) + { + var collection = ModCollection.CreateNewTemporary( tag, characterName ); + _collections[ characterName ] = collection; + return collection.Name; + } + + public bool RemoveTemporaryCollection( string characterName ) + { + if( _collections.Remove( characterName, out var c ) ) + { + _mods.Remove( c ); + return true; + } + + return false; + } + + + // Apply any new changes to the temporary mod. + private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) + { + if( collection == null ) + { + if( removed ) + { + foreach( var c in Penumbra.CollectionManager ) + { + c.Remove( mod ); + } + } + else + { + foreach( var c in Penumbra.CollectionManager ) + { + c.Apply( mod, created ); + } + } + } + else + { + if( removed ) + { + collection.Remove( mod ); + } + else + { + collection.Apply( mod, created ); + } + } + } + + // Only find already existing mods, currently unused. + //private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) + //{ + // var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; + // if( list == null ) + // { + // return null; + // } + // + // if( priority != null ) + // { + // return list.Find( m => m.Priority == priority.Value && m.Name == tag ); + // } + // + // Mod.TemporaryMod? highestMod = null; + // var highestPriority = int.MinValue; + // foreach( var m in list ) + // { + // if( highestPriority < m.Priority && m.Name == tag ) + // { + // highestPriority = m.Priority; + // highestMod = m; + // } + // } + // + // return highestMod; + //} + + // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). + // Returns the found or created mod and whether it was newly created. + private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created ) + { + List< Mod.TemporaryMod > list; + if( collection == null ) + { + list = _modsForAllCollections; + } + else if( _mods.TryGetValue( collection, out var l ) ) + { + list = l; + } + else + { + list = new List< Mod.TemporaryMod >(); + _mods.Add( collection, list ); + } + + var mod = list.Find( m => m.Priority == priority && m.Name == tag ); + if( mod == null ) + { + mod = new Mod.TemporaryMod() + { + Name = tag, + Priority = priority, + }; + list.Add( mod ); + created = true; + } + else + { + created = false; + } + + return mod; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 24324553..a6fa0ffe 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -35,6 +35,24 @@ public partial class ModCollection private void ForceCacheUpdate() => CalculateEffectiveFileList(); + // Handle temporary mods for this collection. + public void Apply( Mod.TemporaryMod tempMod, bool created ) + { + if( created ) + { + _cache?.AddMod( tempMod, tempMod.TotalManipulations > 0 ); + } + else + { + _cache?.ReloadMod( tempMod, tempMod.TotalManipulations > 0 ); + } + } + + public void Remove( Mod.TemporaryMod tempMod ) + { + _cache?.RemoveMod( tempMod, tempMod.TotalManipulations > 0 ); + } + // Clear the current cache. private void ClearCache() diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 7ffdfb61..62b92f7f 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -20,10 +20,10 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; + private readonly ModCollection _collection; private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); public IEnumerable< SingleArray< ModConflicts > > AllConflicts @@ -160,7 +160,11 @@ public partial class ModCollection _conflicts.Clear(); // Add all forced redirects. - Penumbra.Redirects.Apply( ResolvedFiles ); + foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( + Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< Mod.TemporaryMod >() ) ) + { + AddMod( tempMod, false ); + } foreach( var mod in Penumbra.ModManager ) { diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 7d82e28b..1c913a11 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -74,6 +74,17 @@ public partial class ModCollection public static ModCollection CreateNewEmpty( string name ) => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); + // Create a new temporary collection that does not save and has a negative index. + public static ModCollection CreateNewTemporary(string tag, string characterName) + { + var collection = new ModCollection($"{tag}_{characterName}_temporary", Empty); + collection.ModSettingChanged -= collection.SaveOnChange; + collection.InheritanceChanged -= collection.SaveOnChange; + collection.Index = ~Penumbra.TempMods.Collections.Count; + collection.CreateCache(); + return collection; + } + // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) => new(name, this); diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7f12e600..afc4f7d1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -309,7 +309,8 @@ public unsafe partial class PathResolver } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.CollectionManager.Character( actualName ); + // First check temporary character collections, then the own configuration. + return Penumbra.TempMods.Collections.TryGetValue(actualName, out var c) ? c : Penumbra.CollectionManager.Character( actualName ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 4f2439a9..5732522d 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -19,6 +19,8 @@ public interface IMetaManipulation< T > [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > { + public const int CurrentVersion = 0; + public enum Type : byte { Unknown = 0, diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 6662f826..35a4795a 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using OtterGui.Classes; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; @@ -15,12 +17,27 @@ public sealed partial class Mod public int TotalManipulations => Default.Manipulations.Count; - public ISubMod Default { get; } = new SubMod(); + public ISubMod Default + => _default; public IReadOnlyList< IModGroup > Groups => Array.Empty< IModGroup >(); public IEnumerable< ISubMod > AllSubMods => new[] { Default }; + + private readonly SubMod _default = new(); + + public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) + => _default.FileData[ gamePath ] = fullPath; + + public bool SetManipulation( MetaManipulation manip ) + => _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip ); + + public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) + { + _default.FileData = dict; + _default.ManipulationData = manips; + } } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4470d29a..b892a602 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -57,7 +57,7 @@ public class MainClass : IDalamudPlugin { #if !DEBUG var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var dir = new DirectoryInfo( path ); try { @@ -78,7 +78,7 @@ public class MainClass : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; if (!ret) PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); return !ret; @@ -105,7 +105,7 @@ public class Penumbra : IDisposable public static MetaFileManager MetaFileManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; - public static SimpleRedirectManager Redirects { get; private set; } = null!; + public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; public static int ImcExceptions = 0; @@ -138,7 +138,7 @@ public class Penumbra : IDisposable } ResidentResources = new ResidentResourceManager(); - Redirects = new SimpleRedirectManager(); + TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 73d1ff6e..d3a44af2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -856,8 +856,6 @@ public partial class ModEditWindow return newValue != currentValue; } - private const byte CurrentManipulationVersion = 0; - private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations ) { if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) ) @@ -865,7 +863,7 @@ public partial class ModEditWindow return; } - var text = Functions.ToCompressedBase64( manipulations, CurrentManipulationVersion ); + var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion ); if( text.Length > 0 ) { ImGui.SetClipboardText( text ); @@ -878,7 +876,7 @@ public partial class ModEditWindow { var clipboard = ImGui.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == CurrentManipulationVersion && manips != null ) + if( version == MetaManipulation.CurrentVersion && manips != null ) { foreach( var manip in manips ) { @@ -897,7 +895,7 @@ public partial class ModEditWindow { var clipboard = ImGui.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == CurrentManipulationVersion && manips != null ) + if( version == MetaManipulation.CurrentVersion && manips != null ) { _editor!.Meta.Clear(); foreach( var manip in manips ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 31b4b405..95be2069 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -10,8 +11,10 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Api; +using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; +using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; @@ -413,11 +416,84 @@ public partial class ConfigWindow foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) { var value = provider.GetValue( ipc ); - if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label )) + if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) ) { ImGui.TextUnformatted( label ); } } + + using( var collTree = ImRaii.TreeNode( "Collections" ) ) + { + if( collTree ) + { + using var table = ImRaii.Table( "##collTree", 4 ); + if( table ) + { + foreach( var (character, collection) in Penumbra.TempMods.Collections ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( character ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); + } + } + } + } + + using( var modTree = ImRaii.TreeNode( "Mods" ) ) + { + if( modTree ) + { + using var table = ImRaii.Table( "##modTree", 5 ); + + void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) + { + foreach( var mod in list ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collectionName ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var (path, file) in mod.Default.Files ) + { + ImGui.TextUnformatted( $"{path} -> {file}" ); + } + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var manip in mod.Default.Manipulations ) + { + ImGui.TextUnformatted( manip.ToString() ); + } + } + } + } + + if( table ) + { + PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); + foreach( var (collection, list) in Penumbra.TempMods.Mods ) + { + PrintList( collection.Name, list ); + } + } + } + } } // Helper to print a property and its value in a 2-column table. From 9dd12f4a71aba6f10d8364024cab3db8e1da5d5c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 20 Jun 2022 10:46:39 +0900 Subject: [PATCH 0304/2451] Take segmented read into consideration when changing CRC32 values after path replacement --- .../Loader/ResourceLoader.Replacement.cs | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 09572313..5720c089 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,12 +1,14 @@ using System; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -17,31 +19,44 @@ public unsafe partial class ResourceLoader { // Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. // Both work basically the same, so we can reduce the main work to one function used by both hooks. + + [StructLayout( LayoutKind.Explicit )] + public struct GetResourceParameters + { + [FieldOffset( 16 )] + public uint SegmentOffset; + + [FieldOffset( 20 )] + public uint SegmentLength; + + public bool IsPartialRead => SegmentLength != 0; + } + public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams ); [Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )] public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown ); [Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )] public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, void* unk ) - => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false ); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams ) + => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false ); private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, void* unk, bool isUnk ) - => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) + => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) => isSync - ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk ) - : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams ) + : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); [Conditional( "DEBUG" )] @@ -56,15 +71,15 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { PluginLog.Error( "Could not create GamePath from resource path." ); - return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); } - CompareHash( gamePath.Path.Crc32, *resourceHash, gamePath ); + CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); ResourceRequested?.Invoke( gamePath, isSync ); @@ -73,15 +88,15 @@ public unsafe partial class ResourceLoader PathResolved?.Invoke( gamePath, resolvedPath, data ); if( resolvedPath == null ) { - var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); return retUnmodified; } // Replace the hash and path with the correct one for the replacement. - *resourceHash = resolvedPath.Value.InternalName.Crc32; + *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); path = resolvedPath.Value.InternalName.Path; - var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retModified, gamePath, resolvedPath.Value, data ); return retModified; } @@ -228,4 +243,22 @@ public unsafe partial class ResourceLoader GetResourceSyncHook.Dispose(); GetResourceAsyncHook.Dispose(); } + + private int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) + { + if( pGetResParams == null || !pGetResParams->IsPartialRead ) + return path.Crc32; + + // When the game requests file only partially, crc32 includes that information, in format of: + // path/to/file.ext.hex_offset.hex_size + // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 + var pathWithSegmentInfo = Utf8String.Join( + 0x2e, + path, + Utf8String.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), + Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) + ); + Functions.ComputeCrc32AsciiLowerAndSize( pathWithSegmentInfo.Path, out var crc32, out _, out _ ); + return crc32; + } } \ No newline at end of file From c49fce4487e319b634c7ea873f4cb2115063ae30 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 20 Jun 2022 10:56:28 +0900 Subject: [PATCH 0305/2451] Remove MusicManager/DisableSoundStreaming --- Penumbra/Collections/ModCollection.Cache.cs | 6 --- Penumbra/Configuration.cs | 1 - Penumbra/Interop/MusicManager.cs | 41 ------------------- Penumbra/Mods/Mod.Files.cs | 7 ---- Penumbra/Penumbra.cs | 8 ---- Penumbra/UI/ConfigWindow.DebugTab.cs | 3 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 31 -------------- 7 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 Penumbra/Interop/MusicManager.cs diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 62b92f7f..55a92c91 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -308,12 +308,6 @@ public partial class ModCollection { foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) { - // Skip all filtered files - if( Mod.FilterFile( path ) ) - { - continue; - } - AddFile( path, file, parentMod ); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 3d38a069..14879c2e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -52,7 +52,6 @@ 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; } public string DefaultModImportPath { get; set; } = string.Empty; diff --git a/Penumbra/Interop/MusicManager.cs b/Penumbra/Interop/MusicManager.cs deleted file mode 100644 index 7e7568d8..00000000 --- a/Penumbra/Interop/MusicManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Utility.Signatures; - -namespace Penumbra.Interop; - -// Use this to disable streaming of specific soundfiles, -// which will allow replacement of .scd files. -public unsafe class MusicManager -{ - // The wildcard is the offset in framework to the MusicManager in Framework. - [Signature( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0", ScanType = ScanType.Text )] - private readonly IntPtr _musicInitCallLocation = IntPtr.Zero; - - private readonly IntPtr _musicManager; - - public MusicManager() - { - SignatureHelper.Initialise( this ); - var framework = Dalamud.Framework.Address.BaseAddress; - var musicManagerOffset = *( int* )( _musicInitCallLocation + 3 ); - _musicManager = *( IntPtr* )( framework + musicManagerOffset ); - PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); - } - - public bool StreamingEnabled - { - get => *( bool* )( _musicManager + 50 ); - private set - { - PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); - *( bool* )( _musicManager + 50 ) = value; - } - } - - public void EnableStreaming() - => StreamingEnabled = true; - - public void DisableStreaming() - => StreamingEnabled = false; -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 439634c5..e1004978 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -70,13 +70,6 @@ public partial class Mod .ToList(); } - // Filter invalid files. - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - public static bool FilterFile( Utf8GamePath gamePath ) - => !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) { if( !File.Exists( file.FullName ) ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b892a602..4d0c253f 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -112,7 +112,6 @@ public class Penumbra : IDisposable public readonly ResourceLogger ResourceLogger; public readonly PathResolver PathResolver; - public readonly MusicManager MusicManager; public readonly ObjectReloader ObjectReloader; public readonly ModFileSystem ModFileSystem; public readonly PenumbraApi Api; @@ -131,12 +130,6 @@ public class Penumbra : IDisposable Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); - MusicManager = new MusicManager(); - if( Config.DisableSoundStreaming ) - { - MusicManager.DisableStreaming(); - } - ResidentResources = new ResidentResourceManager(); TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); @@ -462,7 +455,6 @@ public class Penumbra : IDisposable sb.AppendFormat( "> **`Plugin Version: `** {0}\n", Version ); sb.AppendFormat( "> **`Commit Hash: `** {0}\n", CommitHash ); sb.AppendFormat( "> **`Enable Mods: `** {0}\n", Config.EnableMods ); - sb.AppendFormat( "> **`Enable Sound Modification: `** {0}\n", Config.DisableSoundStreaming ); sb.AppendFormat( "> **`Enable HTTP API: `** {0}\n", Config.EnableHttpApi ); sb.AppendFormat( "> **`Root Directory: `** `{0}`, {1}\n", Config.ModDirectory, exists ? "Exists" : "Not Existing" ); sb.AppendFormat( "> **`Free Drive Space: `** {0}\n", diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 95be2069..ebd1b67f 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -99,12 +99,11 @@ public partial class ConfigWindow PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); PrintValue( "Path Resolver Enabled", _window._penumbra.PathResolver.Enabled.ToString() ); - PrintValue( "Music Manager Streaming Disabled", ( !_window._penumbra.MusicManager.StreamingEnabled ).ToString() ); PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); } // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. - // Resources are collected by iterating through the + // Resources are collected by iterating through the private static unsafe void DrawDebugTabReplacedResources() { if( !ImGui.CollapsingHeader( "Replaced Resources" ) ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 13fcce3e..8b52adfb 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -24,7 +24,6 @@ public partial class ConfigWindow "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(); DrawEnableDebugModeBox(); DrawEnableFullResourceLoggingBox(); @@ -62,36 +61,6 @@ public partial class ConfigWindow } } - // Toggling audio streaming will need to apply to the music manager - // and rediscover mods due to determining whether .scds will be loaded or not. - private void DrawDisableSoundStreamingBox() - { - var tmp = Penumbra.Config.DisableSoundStreaming; - if( ImGui.Checkbox( "##streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) - { - Penumbra.Config.DisableSoundStreaming = tmp; - Penumbra.Config.Save(); - if( tmp ) - { - _window._penumbra.MusicManager.DisableStreaming(); - } - else - { - _window._penumbra.MusicManager.EnableStreaming(); - } - - Penumbra.ModManager.DiscoverMods(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Sound Modification", - "Disable streaming in the games audio engine. The game enables this by default, and Penumbra should disable it.\n" - + "If this is unchecked, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" - + "Only touch this if you experience sound problems like audio stuttering.\n" - + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash.\n" - + "You might need to restart your game for this to fully take effect." ); - } - // Creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { From 30335fb5d7d9b0b5fbce2fcf4c09195800005920 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Jun 2022 00:29:52 +0900 Subject: [PATCH 0306/2451] Remove redundant crc32 calc --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 5720c089..6a9fb9f3 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -252,13 +252,11 @@ public unsafe partial class ResourceLoader // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - var pathWithSegmentInfo = Utf8String.Join( - 0x2e, + return Utf8String.Join( + (byte)'.', path, Utf8String.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) - ); - Functions.ComputeCrc32AsciiLowerAndSize( pathWithSegmentInfo.Path, out var crc32, out _, out _ ); - return crc32; + ).Crc32; } } \ No newline at end of file From 6ebf550284030a9c4e333613ed11bfdd2fd36a11 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jun 2022 18:26:40 +0200 Subject: [PATCH 0307/2451] Use Ordinal comparisons --- OtterGui | 2 +- Penumbra.GameData/ByteString/FullPath.cs | 4 ++-- Penumbra.GameData/ByteString/Utf8String.Construction.cs | 4 ++-- Penumbra.GameData/Util/GamePath.cs | 4 ++-- Penumbra/Collections/CollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 2 +- Penumbra/Interop/Loader/ResourceLogger.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 2 +- Penumbra/Penumbra.cs | 6 +++--- Penumbra/UI/Classes/ModEditWindow.Files.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs | 3 +-- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 2 +- Penumbra/UI/ConfigWindow.ResourceTab.cs | 2 +- 14 files changed, 19 insertions(+), 20 deletions(-) diff --git a/OtterGui b/OtterGui index 6ce8ca81..fa833869 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6ce8ca816678e7a363f9f4a6f43f009f8d79c070 +Subproject commit fa83386909ad0034f5ed7ea90d8bcedf6e8ba748 diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index 1d13e969..2284bf98 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -72,9 +72,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > => obj switch { FullPath p => InternalName?.CompareTo( p.InternalName ) ?? -1, - FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), + FileInfo f => string.Compare( FullName, f.FullName, StringComparison.OrdinalIgnoreCase ), Utf8String u => InternalName?.CompareTo( u ) ?? -1, - string s => string.Compare( FullName, s, StringComparison.InvariantCultureIgnoreCase ), + string s => string.Compare( FullName, s, StringComparison.OrdinalIgnoreCase ), _ => -1, }; diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs index f6c47a8b..1e6ab325 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -38,14 +38,14 @@ public sealed unsafe partial class Utf8String : IDisposable // Can throw ArgumentOutOfRange if length is higher than max length. // The Crc32 will be computed. public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) - => FromSpanUnsafe( new ReadOnlySpan< byte >( path, length ), isNullTerminated, isLower, isAscii ); + => new Utf8String().Setup( path, length, null, isNullTerminated, false, isLower, isAscii ); // Same as above, just with a span. public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) { fixed( byte* ptr = path ) { - return new Utf8String().Setup( ptr, path.Length, null, isNullTerminated, false, isLower, isAscii ); + return FromByteStringUnsafe( ptr, path.Length, isNullTerminated, isLower, isAscii ); } } diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index ded35038..591b79fc 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -70,8 +70,8 @@ public readonly struct GamePath : IComparable { return rhs switch { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), + string path => string.Compare( _path, path, StringComparison.Ordinal ), + GamePath path => string.Compare( _path, path._path, StringComparison.Ordinal ), _ => -1, }; } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 411e7de5..d1c5b2a2 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -48,7 +48,7 @@ public partial class ModCollection // Obtain a collection case-independently by name. public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); // Default enumeration skips the empty collection. public IEnumerator< ModCollection > GetEnumerator() diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 55a92c91..e8f72e32 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -89,7 +89,7 @@ public partial class ModCollection } var iterator = ResolvedFiles - .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.InvariantCultureIgnoreCase ) ) + .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) .Select( kvp => kvp.Key ); // For files that are not rooted, try to add themselves. diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs index 47be8a68..278aea7d 100644 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ b/Penumbra/Interop/Loader/ResourceLogger.cs @@ -88,7 +88,7 @@ public class ResourceLogger : IDisposable private string? Match( Utf8String data ) { var s = data.ToString(); - return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.InvariantCultureIgnoreCase ) ) + return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ) ? s : null; } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 38ddc97f..86d97fd0 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -284,7 +284,7 @@ public sealed partial class Mod var path = newName.RemoveInvalidPathSymbols(); if( path.Length == 0 || mod.Groups.Any( o => !ReferenceEquals( o, group ) - && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) + && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase ) ) ) { if( message ) { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 92587bfd..b331daca 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -26,7 +26,7 @@ public sealed partial class Mod // Also checks if the directory is available and tries to create it if it is not. private void SetBaseDirectory( string newPath, bool firstTime ) { - if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) + if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) ) { return; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4d0c253f..c715eba5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -78,7 +78,7 @@ public class MainClass : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; if (!ret) PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); return !ret; @@ -310,12 +310,12 @@ public class Penumbra : IDisposable ShutdownWebServer(); } - public bool SetCollection( string type, string collectionName ) + public static bool SetCollection( string type, string collectionName ) { type = type.ToLowerInvariant(); collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) + var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) ? ModCollection.Empty : CollectionManager[ collectionName ]; if( collection == null ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 78f27e3c..72f69ae6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -25,7 +25,7 @@ public partial class ModEditWindow private int _folderSkip = 0; private bool CheckFilter( Mod.Editor.FileRegistry registry ) - => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); private bool CheckFilter( (Mod.Editor.FileRegistry, int) p ) => CheckFilter( p.Item1 ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 039262fb..91d64c6e 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; @@ -22,7 +21,7 @@ public partial class ModFileSystemSelector public ColorId Color; } - private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; private LowerString _modFilter = LowerString.Empty; private int _filterType = -1; private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index b9105bda..c763c505 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -26,7 +26,7 @@ public partial class ConfigWindow bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) => ( _changedItemFilter.IsEmpty || ChangedItemName( item.Key, item.Value.Item2 ) - .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) + .Contains( _changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index b4deea18..662a98dd 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -87,7 +87,7 @@ public partial class ConfigWindow { // Filter unwanted names. if( _resourceManagerFilter.Length != 0 - && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.InvariantCultureIgnoreCase ) ) + && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.OrdinalIgnoreCase ) ) { return; } From c4f82435bf472c6577ddf298bf251b8eecf1b2e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jun 2022 18:27:13 +0200 Subject: [PATCH 0308/2451] Fix for temporary collections. --- Penumbra/Collections/ModCollection.Inheritance.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index ba621337..395da60a 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -134,6 +134,11 @@ public partial class ModCollection { get { + if( Index <= 0 ) + { + return ( ModSettings.Empty, this ); + } + foreach( var collection in GetFlattenedInheritance() ) { var settings = collection._settings[ idx ]; From 00c11b49f0fcf053b2dbde86bfcb5d676f34f94b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jun 2022 18:27:41 +0200 Subject: [PATCH 0309/2451] Use file type enum for Crc64 handling. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 4 ++-- Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 6a9fb9f3..8390d653 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -68,7 +68,7 @@ public unsafe partial class ResourceLoader } } - private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; + private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) @@ -85,7 +85,7 @@ public unsafe partial class ResourceLoader // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, resolvedPath, data ); + PathResolved?.Invoke( gamePath, *resourceType, resolvedPath, data ); if( resolvedPath == null ) { var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 42332f16..2393a87d 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -4,6 +4,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; namespace Penumbra.Interop.Loader; @@ -67,11 +68,11 @@ public unsafe partial class ResourceLoader : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); - private void AddCrc( Utf8GamePath _, FullPath? path, object? _2 ) + private void AddCrc( Utf8GamePath _, ResourceType type, FullPath? path, object? _2 ) { - if( path is { Extension: ".mdl" or ".tex" } p ) + if( path.HasValue && type is ResourceType.Mdl or ResourceType.Tex ) { - _customFileCrc.Add( p.Crc64 ); + _customFileCrc.Add( path.Value.Crc64 ); } } From 1b4eea4d1ec8bd0fd5f93a12c6cb94061f94050f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 10:19:11 +0200 Subject: [PATCH 0310/2451] Add IPC Test stuff, only temp missing. --- OtterGui | 2 +- Penumbra/Api/IPenumbraApi.cs | 11 +- Penumbra/Api/IpcTester.cs | 726 +++++++++++++++++++++++++++ Penumbra/Api/PenumbraApi.cs | 18 +- Penumbra/Api/PenumbraIpc.cs | 54 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 90 +--- 6 files changed, 755 insertions(+), 146 deletions(-) create mode 100644 Penumbra/Api/IpcTester.cs diff --git a/OtterGui b/OtterGui index fa833869..03934d3a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fa83386909ad0034f5ed7ea90d8bcedf6e8ba748 +Subproject commit 03934d3a19cb610898412045ad5ea7dad9766a59 diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 9ba916d4..01ecc868 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Configuration; -using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -41,8 +39,8 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the currently set mod directory from the configuration. public string GetModDirectory(); - // Obtain the entire current penumbra configuration. - public IPluginConfiguration GetConfiguration(); + // Obtain the entire current penumbra configuration as a json encoded string. + public string GetConfiguration(); // Triggered when the user hovers over a listed changed object in a mod tab. // Can be used to append tooltips. @@ -58,9 +56,6 @@ public interface IPenumbraApi : IPenumbraApiBase // 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 ); - // Queue redrawing of all currently available actors with the given RedrawType. public void RedrawAll( RedrawType setting ); @@ -73,7 +68,7 @@ public interface IPenumbraApi : IPenumbraApiBase public string ResolvePath( string gamePath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public IList< string > ReverseResolvePath( string moddedPath, string characterName ); + public IList ReverseResolvePath( string moddedPath, string characterName ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs new file mode 100644 index 00000000..069d4c3e --- /dev/null +++ b/Penumbra/Api/IpcTester.cs @@ -0,0 +1,726 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Reflection; +using Dalamud.Interface; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Api; + +public class IpcTester : IDisposable +{ + private readonly PenumbraIpc _ipc; + private readonly DalamudPluginInterface _pi; + + private readonly ICallGateSubscriber< object? > _initialized; + private readonly ICallGateSubscriber< object? > _disposed; + private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; + + private readonly List< DateTimeOffset > _initializedList = new(); + private readonly List< DateTimeOffset > _disposedList = new(); + + public IpcTester( DalamudPluginInterface pi, PenumbraIpc ipc ) + { + _ipc = ipc; + _pi = pi; + _initialized = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderInitialized ); + _disposed = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderDisposed ); + _redrawn = _pi.GetIpcSubscriber< IntPtr, int, object? >( PenumbraIpc.LabelProviderGameObjectRedrawn ); + _initialized.Subscribe( AddInitialized ); + _disposed.Subscribe( AddDisposed ); + _redrawn.Subscribe( SetLastRedrawn ); + } + + public void Dispose() + { + _initialized.Unsubscribe( AddInitialized ); + _disposed.Unsubscribe( AddDisposed ); + _redrawn.Subscribe( SetLastRedrawn ); + _tooltip?.Unsubscribe( AddedTooltip ); + _click?.Unsubscribe( AddedClick ); + } + + private void AddInitialized() + => _initializedList.Add( DateTimeOffset.UtcNow ); + + private void AddDisposed() + => _disposedList.Add( DateTimeOffset.UtcNow ); + + public void Draw() + { + try + { + DrawAvailable(); + DrawGeneral(); + DrawResolve(); + DrawRedraw(); + DrawChangedItems(); + DrawData(); + DrawSetting(); + DrawTemp(); + DrawTempCollections(); + DrawTempMods(); + } + catch( Exception e ) + { + PluginLog.Error( $"Error during IPC Tests:\n{e}" ); + } + } + + private void DrawAvailable() + { + using var _ = ImRaii.TreeNode( "Availability" ); + if( !_ ) + { + return; + } + + ImGui.TextUnformatted( $"API Version: {_ipc.Api.ApiVersion}" ); + ImGui.TextUnformatted( "Available subscriptions:" ); + using var indent = ImRaii.PushIndent(); + + var dict = _ipc.GetType().GetFields( BindingFlags.Static | BindingFlags.Public ).Where( f => f.IsLiteral ) + .ToDictionary( f => f.Name, f => f.GetValue( _ipc ) as string ); + foreach( var provider in _ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) + { + var value = provider.GetValue( _ipc ); + if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) ) + { + ImGui.TextUnformatted( label ); + } + } + } + + private static void DrawIntro( string label, string info ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( label ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( info ); + ImGui.TableNextColumn(); + } + + private string _currentConfiguration = string.Empty; + + private void DrawGeneral() + { + using var _ = ImRaii.TreeNode( "General IPC" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + + void DrawList( string label, string text, List< DateTimeOffset > list ) + { + DrawIntro( label, text ); + if( list.Count == 0 ) + { + ImGui.TextUnformatted( "Never" ); + } + else + { + ImGui.TextUnformatted( list[ ^1 ].LocalDateTime.ToString( CultureInfo.CurrentCulture ) ); + if( list.Count > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", + list.SkipLast( 1 ).Select( t => t.LocalDateTime.ToString( CultureInfo.CurrentCulture ) ) ) ); + } + } + } + + DrawList( PenumbraIpc.LabelProviderInitialized, "Last Initialized", _initializedList ); + DrawList( PenumbraIpc.LabelProviderDisposed, "Last Disposed", _disposedList ); + DrawIntro( PenumbraIpc.LabelProviderApiVersion, "Current Version" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< int >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc().ToString() ); + DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); + if( ImGui.Button( "Get" ) ) + { + _currentConfiguration = _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetConfiguration ).InvokeFunc(); + ImGui.OpenPopup( "Config Popup" ); + } + + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var popup = ImRaii.Popup( "Config Popup" ); + if( popup ) + { + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGuiUtil.TextWrapped( _currentConfiguration ); + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + { + ImGui.CloseCurrentPopup(); + } + } + } + + private string _currentResolvePath = string.Empty; + private string _currentResolveCharacter = string.Empty; + private string _currentDrawObjectString = string.Empty; + private string _currentReversePath = string.Empty; + private IntPtr _currentDrawObject = IntPtr.Zero; + + private void DrawResolve() + { + using var _ = ImRaii.TreeNode( "Resolve IPC" ); + if( !_ ) + { + return; + } + + ImGui.InputTextWithHint( "##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength ); + ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 ); + ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, + Utf8GamePath.MaxGamePathLength ); + if( ImGui.InputTextWithHint( "##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, + ImGuiInputTextFlags.CharsHexadecimal ) ) + { + _currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp ) + ? tmp + : IntPtr.Zero; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( PenumbraIpc.LabelProviderResolveDefault, "Default Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveDefault ) + .InvokeFunc( _currentResolvePath ) ); + } + + DrawIntro( PenumbraIpc.LabelProviderResolveCharacter, "Character Collection Resolve" ); + if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) + { + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string, string >( PenumbraIpc.LabelProviderResolveCharacter ) + .InvokeFunc( _currentResolvePath, _currentResolveCharacter ) ); + } + + DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Draw Object Info" ); + if( _currentDrawObject == IntPtr.Zero ) + { + ImGui.TextUnformatted( "Invalid" ); + } + else + { + var (ptr, collection) = _pi.GetIpcSubscriber< IntPtr, (IntPtr, string) >( PenumbraIpc.LabelProviderGetDrawObjectInfo ) + .InvokeFunc( _currentDrawObject ); + ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); + } + + DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" ); + if( _currentReversePath.Length > 0 ) + { + var list = _pi.GetIpcSubscriber< string, string, string[] >( PenumbraIpc.LabelProviderReverseResolvePath ) + .InvokeFunc( _currentReversePath, _currentResolveCharacter ); + if( list.Length > 0 ) + { + ImGui.TextUnformatted( list[ 0 ] ); + if( list.Length > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + } + } + } + } + + private string _redrawName = string.Empty; + private int _redrawIndex = 0; + private string _lastRedrawnString = "None"; + + private void SetLastRedrawn( IntPtr address, int index ) + { + if( index < 0 || index > Dalamud.Objects.Length || address == IntPtr.Zero || Dalamud.Objects[ index ]?.Address != address ) + { + _lastRedrawnString = "Invalid"; + } + + _lastRedrawnString = $"{Dalamud.Objects[ index ]!.Name} (0x{address:X}, {index})"; + } + + private void DrawRedraw() + { + using var _ = ImRaii.TreeNode( "Redraw IPC" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( PenumbraIpc.LabelProviderRedrawName, "Redraw by Name" ); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); + ImGui.SameLine(); + if( ImGui.Button( "Redraw##Name" ) ) + { + _pi.GetIpcSubscriber< string, int, object? >( PenumbraIpc.LabelProviderRedrawName ) + .InvokeAction( _redrawName, ( int )RedrawType.Redraw ); + } + + DrawIntro( PenumbraIpc.LabelProviderRedrawIndex, "Redraw by Index" ); + var tmp = _redrawIndex; + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, Dalamud.Objects.Length ) ) + { + _redrawIndex = Math.Clamp( tmp, 0, Dalamud.Objects.Length ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Redraw##Index" ) ) + { + _pi.GetIpcSubscriber< int, int, object? >( PenumbraIpc.LabelProviderRedrawIndex ) + .InvokeAction( _redrawIndex, ( int )RedrawType.Redraw ); + } + + DrawIntro( PenumbraIpc.LabelProviderRedrawAll, "Redraw All" ); + if( ImGui.Button( "Redraw##All" ) ) + { + _pi.GetIpcSubscriber< int, object? >( PenumbraIpc.LabelProviderRedrawAll ).InvokeAction( ( int )RedrawType.Redraw ); + } + + DrawIntro( PenumbraIpc.LabelProviderGameObjectRedrawn, "Last Redrawn Object:" ); + ImGui.TextUnformatted( _lastRedrawnString ); + } + + private bool _subscribedToTooltip = false; + private bool _subscribedToClick = false; + private string _changedItemCollection = string.Empty; + private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >(); + private string _lastClicked = string.Empty; + private string _lastHovered = string.Empty; + private ICallGateSubscriber< ChangedItemType, uint, object? >? _tooltip; + private ICallGateSubscriber< MouseButton, ChangedItemType, uint, object? >? _click; + + private void DrawChangedItems() + { + using var _ = ImRaii.TreeNode( "Changed Item IPC" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( PenumbraIpc.LabelProviderChangedItemTooltip, "Add Tooltip" ); + if( ImGui.Checkbox( "##tooltip", ref _subscribedToTooltip ) ) + { + _tooltip = _pi.GetIpcSubscriber< ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemTooltip ); + if( _subscribedToTooltip ) + { + _tooltip.Subscribe( AddedTooltip ); + } + else + { + _tooltip.Unsubscribe( AddedTooltip ); + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastHovered ); + + DrawIntro( PenumbraIpc.LabelProviderChangedItemClick, "Subscribe Click" ); + if( ImGui.Checkbox( "##click", ref _subscribedToClick ) ) + { + _click = _pi.GetIpcSubscriber< MouseButton, ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemClick ); + if( _subscribedToClick ) + { + _click.Subscribe( AddedClick ); + } + else + { + _click.Unsubscribe( AddedClick ); + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastClicked ); + + DrawIntro( PenumbraIpc.LabelProviderGetChangedItems, "Changed Item List" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); + ImGui.SameLine(); + if( ImGui.Button( "Get" ) ) + { + _changedItems = _pi.GetIpcSubscriber< string, IReadOnlyDictionary< string, object? > >( PenumbraIpc.LabelProviderGetChangedItems ) + .InvokeFunc( _changedItemCollection ); + ImGui.OpenPopup( "Changed Item List" ); + } + + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var p = ImRaii.Popup( "Changed Item List" ); + if( p ) + { + foreach( var item in _changedItems ) + { + ImGui.TextUnformatted( item.Key ); + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + { + ImGui.CloseCurrentPopup(); + } + } + } + + private void AddedTooltip( ChangedItemType type, uint id ) + { + _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; + ImGui.TextUnformatted( "IPC Test Successful" ); + } + + private void AddedClick( MouseButton button, ChangedItemType type, uint id ) + { + _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; + } + + private string _characterCollectionName = string.Empty; + private IList< (string, string) > _mods = new List< (string, string) >(); + private IList< string > _collections = new List< string >(); + private bool _collectionMode = false; + + private void DrawData() + { + using var _ = ImRaii.TreeNode( "Data IPC" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( PenumbraIpc.LabelProviderCurrentCollectionName, "Current Collection" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderCurrentCollectionName ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderDefaultCollectionName, "Default Collection" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderDefaultCollectionName ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderCharacterCollectionName, "Character" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); + var (c, s) = _pi.GetIpcSubscriber< string, (string, bool) >( PenumbraIpc.LabelProviderCharacterCollectionName ) + .InvokeFunc( _characterCollectionName ); + ImGui.SameLine(); + ImGui.TextUnformatted( $"{c}, {( s ? "Custom" : "Default" )}" ); + + DrawIntro( PenumbraIpc.LabelProviderGetCollections, "Collections" ); + if( ImGui.Button( "Get##Collections" ) ) + { + _collectionMode = true; + _collections = _pi.GetIpcSubscriber< IList< string > >( PenumbraIpc.LabelProviderGetCollections ).InvokeFunc(); + ImGui.OpenPopup( "Ipc Data" ); + } + + DrawIntro( PenumbraIpc.LabelProviderGetMods, "Mods" ); + if( ImGui.Button( "Get##Mods" ) ) + { + _collectionMode = false; + _mods = _pi.GetIpcSubscriber< IList< (string, string) > >( PenumbraIpc.LabelProviderGetMods ).InvokeFunc(); + ImGui.OpenPopup( "Ipc Data" ); + } + + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var p = ImRaii.Popup( "Ipc Data" ); + if( p ) + { + if( _collectionMode ) + { + foreach( var collection in _collections ) + { + ImGui.TextUnformatted( collection ); + } + } + else + { + foreach( var (modDir, modName) in _mods ) + { + ImGui.TextUnformatted( $"{modDir}: {modName}" ); + } + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + { + ImGui.CloseCurrentPopup(); + } + } + } + + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private string _settingsCollection = string.Empty; + private bool _settingsAllowInheritance = true; + private bool _settingsInherit = false; + private bool _settingsEnabled = false; + private int _settingsPriority = 0; + private IDictionary< string, (IList< string >, SelectType) >? _availableSettings; + private IDictionary< string, IList< string > >? _currentSettings = null; + private PenumbraApiEc _lastError = PenumbraApiEc.Success; + + + private void DrawSetting() + { + using var _ = ImRaii.TreeNode( "Settings IPC" ); + if( !_ ) + { + return; + } + + ImGui.InputTextWithHint( "##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100 ); + ImGui.InputTextWithHint( "##settingsName", "Mod Name...", ref _settingsModName, 100 ); + ImGui.InputTextWithHint( "##settingsCollection", "Collection...", ref _settingsCollection, 100 ); + ImGui.Checkbox( "Allow Inheritance", ref _settingsAllowInheritance ); + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( "Last Error", _lastError.ToString() ); + + DrawIntro( PenumbraIpc.LabelProviderGetAvailableModSettings, "Get Available Settings" ); + if( ImGui.Button( "Get##Available" ) ) + { + _availableSettings = _pi + .GetIpcSubscriber< string, string, IDictionary< string, (IList< string >, SelectType) >? >( + PenumbraIpc.LabelProviderGetAvailableModSettings ).InvokeFunc( _settingsModDirectory, _settingsModName ); + _lastError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; + } + + DrawIntro( PenumbraIpc.LabelProviderGetCurrentModSettings, "Get Current Settings" ); + if( ImGui.Button( "Get##Current" ) ) + { + var ret = _pi + .GetIpcSubscriber< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( + PenumbraIpc.LabelProviderGetCurrentModSettings ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, + _settingsAllowInheritance ); + _lastError = ret.Item1; + if( ret.Item1 == PenumbraApiEc.Success ) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + DrawIntro( PenumbraIpc.LabelProviderTryInheritMod, "Inherit Mod" ); + ImGui.Checkbox( "##inherit", ref _settingsInherit ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Inherit" ) ) + { + _lastError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTryInheritMod ) + .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit ); + } + + DrawIntro( PenumbraIpc.LabelProviderTrySetMod, "Set Enabled" ); + ImGui.Checkbox( "##enabled", ref _settingsEnabled ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Enabled" ) ) + { + _lastError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetMod ) + .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled ); + } + + DrawIntro( PenumbraIpc.LabelProviderTrySetModPriority, "Set Priority" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.DragInt( "##Priority", ref _settingsPriority ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Priority" ) ) + { + _lastError = _pi.GetIpcSubscriber< string, string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModPriority ) + .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); + } + + DrawIntro( PenumbraIpc.LabelProviderTrySetModSetting, "Set Setting(s)" ); + if( _availableSettings != null ) + { + foreach( var (group, (list, type)) in _availableSettings ) + { + using var id = ImRaii.PushId( group ); + var preview = list.Count > 0 ? list[ 0 ] : string.Empty; + IList< string > current; + if( _currentSettings != null && _currentSettings.TryGetValue( group, out current! ) && current.Count > 0 ) + { + preview = current[ 0 ]; + } + else + { + current = new List< string >(); + if( _currentSettings != null ) + { + _currentSettings[ group ] = current; + } + } + + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + using( var c = ImRaii.Combo( "##group", preview ) ) + { + if( c ) + { + foreach( var s in list ) + { + var contained = current.Contains( s ); + if( ImGui.Checkbox( s, ref contained ) ) + { + if( contained ) + { + current.Add( s ); + } + else + { + current.Remove( s ); + } + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Set##setting" ) ) + { + if( type == SelectType.Single ) + { + _lastError = _pi + .GetIpcSubscriber< string, string, string, string, string, + PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSetting ).InvokeFunc( _settingsCollection, + _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty ); + } + else + { + _lastError = _pi + .GetIpcSubscriber< string, string, string, string, IReadOnlyList< string >, + PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSettings ).InvokeFunc( _settingsCollection, + _settingsModDirectory, _settingsModName, group, current.ToArray() ); + } + } + ImGui.SameLine(); + ImGui.TextUnformatted( group ); + } + } + } + + private void DrawTemp() + { + using var _ = ImRaii.TreeNode( "Temp IPC" ); + if( !_ ) + { + return; + } + } + + private void DrawTempCollections() + { + using var collTree = ImRaii.TreeNode( "Collections" ); + if( !collTree ) + { + return; + } + + using var table = ImRaii.Table( "##collTree", 4 ); + if( !table ) + { + return; + } + + foreach( var (character, collection) in Penumbra.TempMods.Collections ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( character ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); + } + } + + private void DrawTempMods() + { + using var modTree = ImRaii.TreeNode( "Mods" ); + if( !modTree ) + { + return; + } + + using var table = ImRaii.Table( "##modTree", 5 ); + + void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) + { + foreach( var mod in list ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collectionName ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var (path, file) in mod.Default.Files ) + { + ImGui.TextUnformatted( $"{path} -> {file}" ); + } + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var manip in mod.Default.Manipulations ) + { + ImGui.TextUnformatted( manip.ToString() ); + } + } + } + } + + if( table ) + { + PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); + foreach( var (collection, list) in Penumbra.TempMods.Mods ) + { + PrintList( collection.Name, list ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 99898ba5..0cbf419e 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -55,10 +55,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } - public IPluginConfiguration GetConfiguration() + public string GetConfiguration() { CheckInitialized(); - return JsonConvert.DeserializeObject< Configuration >( JsonConvert.SerializeObject( Penumbra.Config ) ); + return JsonConvert.SerializeObject( Penumbra.Config, Formatting.Indented ); } public event ChangedItemHover? ChangedItemTooltip; @@ -75,12 +75,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawObject( name, setting ); } - public void RedrawObject( GameObject? gameObject, RedrawType setting ) - { - CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); - } - public void RedrawAll( RedrawType setting ) { CheckInitialized(); @@ -299,11 +293,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi IReadOnlyList< string > optionNames ) { CheckInitialized(); - if( optionNames.Count == 0 ) - { - return PenumbraApiEc.InvalidArgument; - } - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { return PenumbraApiEc.CollectionMissing; @@ -325,8 +314,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi uint setting = 0; if( group.Type == SelectType.Single ) { - var name = optionNames[ ^1 ]; - var optionIdx = group.IndexOf( o => o.Name == optionNames[ ^1 ] ); + var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf( o => o.Name == optionNames[ ^1 ] ); if( optionIdx < 0 ) { return PenumbraApiEc.OptionMissing; diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 1aa4ce24..d146f0c3 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin; @@ -12,11 +11,12 @@ namespace Penumbra.Api; public partial class PenumbraIpc : IDisposable { internal readonly IPenumbraApi Api; + internal readonly IpcTester Tester; public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) { - Api = api; - + Api = api; + Tester = new IpcTester( pi, this ); InitializeGeneralProviders( pi ); InitializeResolveProviders( pi ); InitializeRedrawProviders( pi ); @@ -37,6 +37,7 @@ public partial class PenumbraIpc : IDisposable DisposeSettingProviders(); DisposeTempProviders(); ProviderDisposed?.SendMessage(); + Tester.Dispose(); } } @@ -48,11 +49,11 @@ public partial class PenumbraIpc public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< IPluginConfiguration >? ProviderGetConfiguration; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string >? ProviderGetConfiguration; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { @@ -96,7 +97,7 @@ public partial class PenumbraIpc try { - ProviderGetConfiguration = pi.GetIpcProvider< IPluginConfiguration >( LabelProviderGetConfiguration ); + ProviderGetConfiguration = pi.GetIpcProvider< string >( LabelProviderGetConfiguration ); ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); } catch( Exception e ) @@ -117,15 +118,13 @@ 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"; public const string LabelProviderGameObjectRedrawn = "Penumbra.GameObjectRedrawn"; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; + internal ICallGateProvider< string, int, object? >? ProviderRedrawName; + internal ICallGateProvider< int, int, object? >? ProviderRedrawIndex; + internal ICallGateProvider< int, object? >? ProviderRedrawAll; + internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -142,7 +141,7 @@ public partial class PenumbraIpc { try { - ProviderRedrawName = pi.GetIpcProvider< string, int, object >( LabelProviderRedrawName ); + ProviderRedrawName = pi.GetIpcProvider< string, int, object? >( LabelProviderRedrawName ); ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -152,7 +151,7 @@ public partial class PenumbraIpc try { - ProviderRedrawIndex = pi.GetIpcProvider< int, int, object >( LabelProviderRedrawIndex ); + ProviderRedrawIndex = pi.GetIpcProvider< int, int, object? >( LabelProviderRedrawIndex ); ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -162,17 +161,7 @@ public partial class PenumbraIpc try { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); - ProviderRedrawObject.RegisterAction( ( o, i ) => Api.RedrawObject( o, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); - } - - try - { - ProviderRedrawAll = pi.GetIpcProvider< int, object >( LabelProviderRedrawAll ); + ProviderRedrawAll = pi.GetIpcProvider< int, object? >( LabelProviderRedrawAll ); ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); } catch( Exception e ) @@ -198,7 +187,6 @@ public partial class PenumbraIpc { ProviderRedrawName?.UnregisterAction(); ProviderRedrawIndex?.UnregisterAction(); - ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); Api.GameObjectRedrawn -= OnGameObjectRedrawn; } @@ -274,8 +262,8 @@ public partial class PenumbraIpc public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< ChangedItemType, uint, object? >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object? >? ProviderChangedItemClick; internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; private void OnClick( MouseButton click, object? item ) @@ -294,7 +282,7 @@ public partial class PenumbraIpc { try { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object >( LabelProviderChangedItemTooltip ); + ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object? >( LabelProviderChangedItemTooltip ); Api.ChangedItemTooltip += OnTooltip; } catch( Exception e ) @@ -304,7 +292,7 @@ public partial class PenumbraIpc try { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object >( LabelProviderChangedItemClick ); + ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object? >( LabelProviderChangedItemClick ); Api.ChangedItemClicked += OnClick; } catch( Exception e ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index ebd1b67f..aa58ad32 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -404,95 +404,7 @@ public partial class ConfigWindow { return; } - - var ipc = _window._penumbra.Ipc; - ImGui.TextUnformatted( $"API Version: {ipc.Api.ApiVersion}" ); - ImGui.TextUnformatted( "Available subscriptions:" ); - using var indent = ImRaii.PushIndent(); - - var dict = ipc.GetType().GetFields( BindingFlags.Static | BindingFlags.Public ).Where( f => f.IsLiteral ) - .ToDictionary( f => f.Name, f => f.GetValue( ipc ) as string ); - foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) - { - var value = provider.GetValue( ipc ); - if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) ) - { - ImGui.TextUnformatted( label ); - } - } - - using( var collTree = ImRaii.TreeNode( "Collections" ) ) - { - if( collTree ) - { - using var table = ImRaii.Table( "##collTree", 4 ); - if( table ) - { - foreach( var (character, collection) in Penumbra.TempMods.Collections ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( character ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); - } - } - } - } - - using( var modTree = ImRaii.TreeNode( "Mods" ) ) - { - if( modTree ) - { - using var table = ImRaii.Table( "##modTree", 5 ); - - void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) - { - foreach( var mod in list ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Priority.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collectionName ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); - if( ImGui.IsItemHovered() ) - { - using var tt = ImRaii.Tooltip(); - foreach( var (path, file) in mod.Default.Files ) - { - ImGui.TextUnformatted( $"{path} -> {file}" ); - } - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); - if( ImGui.IsItemHovered() ) - { - using var tt = ImRaii.Tooltip(); - foreach( var manip in mod.Default.Manipulations ) - { - ImGui.TextUnformatted( manip.ToString() ); - } - } - } - } - - if( table ) - { - PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); - foreach( var (collection, list) in Penumbra.TempMods.Mods ) - { - PrintList( collection.Name, list ); - } - } - } - } + _window._penumbra.Ipc.Tester.Draw(); } // Helper to print a property and its value in a 2-column table. From 8ecf7e2381d3727bb59efa8594dcfd3b87a98511 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 11:56:01 +0200 Subject: [PATCH 0311/2451] Change return value of LoadTimelineResources --- Penumbra/Interop/Resolver/PathResolver.Animation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 1d51b8df..604b40af 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -10,16 +10,16 @@ public unsafe partial class PathResolver { private ModCollection? _animationLoadCollection; - public delegate byte LoadTimelineResourcesDelegate( IntPtr timeline ); + public delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. [Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )] public Hook< LoadTimelineResourcesDelegate >? LoadTimelineResourcesHook; - private byte LoadTimelineResourcesDetour( IntPtr timeline ) + private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { - byte ret; + ulong ret; var old = _animationLoadCollection; try { From f17e9be824759dc007b5e8607237e9fa075ab30a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 15:26:01 +0200 Subject: [PATCH 0312/2451] Fix incorrectly disposed VFX. --- .../Resolver/PathResolver.Animation.cs | 7 +- .../Interop/Resolver/PathResolver.Data.cs | 71 +++++++++++-------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 604b40af..fcab01e3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -1,5 +1,6 @@ using System; using Dalamud.Hooking; +using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; @@ -59,16 +60,16 @@ public unsafe partial class PathResolver _animationLoadCollection = last; } - public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2 ); + public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] public Hook< LoadSomeAvfx >? LoadSomeAvfxHook; - private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2 ) + private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) { var last = _animationLoadCollection; _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); - var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2 ); + var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); _animationLoadCollection = last; return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index afc4f7d1..14a876b4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; +using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -277,40 +278,48 @@ public unsafe partial class PathResolver return Penumbra.CollectionManager.Default; } - // Housing Retainers - if( Penumbra.Config.UseDefaultCollectionForRetainers - && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc - && gameObject->DataID == 1011832 ) + try { + // Housing Retainers + if( Penumbra.Config.UseDefaultCollectionForRetainers + && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc + && gameObject->DataID == 1011832 ) + { + return Penumbra.CollectionManager.Default; + } + + string? actorName = null; + if( Penumbra.Config.PreferNamedCollectionsOverOwners ) + { + // Early return if we prefer the actors own name over its owner. + actorName = new Utf8String( gameObject->Name ).ToString(); + if( actorName.Length > 0 && Penumbra.CollectionManager.Characters.TryGetValue( actorName, out var actorCollection ) ) + { + return actorCollection; + } + } + + // All these special cases are relevant for an empty name, so never collide with the above setting. + // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. + var actualName = gameObject->ObjectIndex switch + { + 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview + >= 200 => GetCutsceneName( gameObject ), + _ => null, + } + ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); + + // First check temporary character collections, then the own configuration. + return Penumbra.TempMods.Collections.TryGetValue( actualName, out var c ) ? c : Penumbra.CollectionManager.Character( actualName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error identifying collection:\n{e}" ); return Penumbra.CollectionManager.Default; } - - string? actorName = null; - if( Penumbra.Config.PreferNamedCollectionsOverOwners ) - { - // Early return if we prefer the actors own name over its owner. - actorName = new Utf8String( gameObject->Name ).ToString(); - if( actorName.Length > 0 && Penumbra.CollectionManager.Characters.TryGetValue( actorName, out var actorCollection ) ) - { - return actorCollection; - } - } - - // All these special cases are relevant for an empty name, so never collide with the above setting. - // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. - var actualName = gameObject->ObjectIndex switch - { - 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - >= 200 => GetCutsceneName( gameObject ), - _ => null, - } - ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - - // First check temporary character collections, then the own configuration. - return Penumbra.TempMods.Collections.TryGetValue(actualName, out var c) ? c : Penumbra.CollectionManager.Character( actualName ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. From 311882948a39288f029b7773a6302ea11681291e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 17:02:31 +0200 Subject: [PATCH 0313/2451] Add last IPC tests, fix some problems with them, increment API Version to 5. --- Penumbra/Api/IPenumbraApi.cs | 7 +- Penumbra/Api/IpcTester.cs | 99 +++++++++++++++++-- Penumbra/Api/PenumbraApi.cs | 32 +++--- Penumbra/Api/PenumbraIpc.cs | 8 +- Penumbra/Api/TempModManager.cs | 2 +- .../Interop/Resolver/PathResolver.Data.cs | 12 ++- 6 files changed, 126 insertions(+), 34 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 01ecc868..1fa7b576 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -68,7 +68,7 @@ public interface IPenumbraApi : IPenumbraApiBase public string ResolvePath( string gamePath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public IList ReverseResolvePath( string moddedPath, string characterName ); + public IList< string > ReverseResolvePath( string moddedPath, string characterName ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; @@ -144,13 +144,12 @@ public interface IPenumbraApi : IPenumbraApiBase // Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections. // Can return Okay, InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, + public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, HashSet< string > manipCodes, int priority ); // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary. // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, - IReadOnlySet< string > manipCodes, + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, HashSet< string > manipCodes, int priority ); // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 069d4c3e..30059913 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -483,8 +483,8 @@ public class IpcTester : IDisposable private bool _settingsEnabled = false; private int _settingsPriority = 0; private IDictionary< string, (IList< string >, SelectType) >? _availableSettings; - private IDictionary< string, IList< string > >? _currentSettings = null; - private PenumbraApiEc _lastError = PenumbraApiEc.Success; + private IDictionary< string, IList< string > >? _currentSettings = null; + private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; private void DrawSetting() @@ -506,7 +506,7 @@ public class IpcTester : IDisposable return; } - DrawIntro( "Last Error", _lastError.ToString() ); + DrawIntro( "Last Error", _lastSettingsError.ToString() ); DrawIntro( PenumbraIpc.LabelProviderGetAvailableModSettings, "Get Available Settings" ); if( ImGui.Button( "Get##Available" ) ) @@ -514,7 +514,7 @@ public class IpcTester : IDisposable _availableSettings = _pi .GetIpcSubscriber< string, string, IDictionary< string, (IList< string >, SelectType) >? >( PenumbraIpc.LabelProviderGetAvailableModSettings ).InvokeFunc( _settingsModDirectory, _settingsModName ); - _lastError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; + _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; } DrawIntro( PenumbraIpc.LabelProviderGetCurrentModSettings, "Get Current Settings" ); @@ -524,7 +524,7 @@ public class IpcTester : IDisposable .GetIpcSubscriber< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( PenumbraIpc.LabelProviderGetCurrentModSettings ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance ); - _lastError = ret.Item1; + _lastSettingsError = ret.Item1; if( ret.Item1 == PenumbraApiEc.Success ) { _settingsEnabled = ret.Item2?.Item1 ?? false; @@ -543,7 +543,7 @@ public class IpcTester : IDisposable ImGui.SameLine(); if( ImGui.Button( "Set##Inherit" ) ) { - _lastError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTryInheritMod ) + _lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTryInheritMod ) .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit ); } @@ -552,7 +552,7 @@ public class IpcTester : IDisposable ImGui.SameLine(); if( ImGui.Button( "Set##Enabled" ) ) { - _lastError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetMod ) + _lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetMod ) .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled ); } @@ -562,7 +562,8 @@ public class IpcTester : IDisposable ImGui.SameLine(); if( ImGui.Button( "Set##Priority" ) ) { - _lastError = _pi.GetIpcSubscriber< string, string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModPriority ) + _lastSettingsError = _pi + .GetIpcSubscriber< string, string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModPriority ) .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); } @@ -615,25 +616,37 @@ public class IpcTester : IDisposable { if( type == SelectType.Single ) { - _lastError = _pi + _lastSettingsError = _pi .GetIpcSubscriber< string, string, string, string, string, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSetting ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty ); } else { - _lastError = _pi + _lastSettingsError = _pi .GetIpcSubscriber< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSettings ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, group, current.ToArray() ); } } + ImGui.SameLine(); ImGui.TextUnformatted( group ); } } } + private string _tempCollectionName = string.Empty; + private string _tempCharacterName = string.Empty; + private bool _forceOverwrite = true; + private string _tempModName = string.Empty; + private PenumbraApiEc _lastTempError = PenumbraApiEc.Success; + private string _lastCreatedCollectionName = string.Empty; + private string _tempGamePath = "test/game/path.mtrl"; + private string _tempFilePath = "test/success.mtrl"; + private string _tempManipulation = string.Empty; + + private void DrawTemp() { using var _ = ImRaii.TreeNode( "Temp IPC" ); @@ -641,6 +654,72 @@ public class IpcTester : IDisposable { return; } + + ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 ); + ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 ); + ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 ); + ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 ); + ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 ); + ImGui.InputTextWithHint( "##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256 ); + ImGui.Checkbox( "Force Character Collection Overwrite", ref _forceOverwrite ); + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( "Last Error", _lastTempError.ToString() ); + DrawIntro( "Last Created Collection", _lastCreatedCollectionName ); + DrawIntro( PenumbraIpc.LabelProviderCreateTemporaryCollection, "Create Temporary Collection" ); + if( ImGui.Button( "Create##Collection" ) ) + { + ( _lastTempError, _lastCreatedCollectionName ) = + _pi.GetIpcSubscriber< string, string, bool, (PenumbraApiEc, string) >( PenumbraIpc.LabelProviderCreateTemporaryCollection ) + .InvokeFunc( _tempCollectionName, _tempCharacterName, _forceOverwrite ); + } + + DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryCollection, "Remove Temporary Collection from Character" ); + if( ImGui.Button( "Delete##Collection" ) ) + { + _lastTempError = _pi.GetIpcSubscriber< string, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryCollection ) + .InvokeFunc( _tempCharacterName ); + } + + DrawIntro( PenumbraIpc.LabelProviderAddTemporaryMod, "Add Temporary Mod to specific Collection" ); + if( ImGui.Button( "Add##Mod" ) ) + { + _lastTempError = _pi + .GetIpcSubscriber< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + PenumbraIpc.LabelProviderAddTemporaryMod ) + .InvokeFunc( _tempModName, _tempCollectionName, + new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? new HashSet< string > { _tempManipulation } : new HashSet< string >(), int.MaxValue ); + } + + DrawIntro( PenumbraIpc.LabelProviderAddTemporaryModAll, "Add Temporary Mod to all Collections" ); + if( ImGui.Button( "Add##All" ) ) + { + _lastTempError = _pi + .GetIpcSubscriber< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + PenumbraIpc.LabelProviderAddTemporaryModAll ) + .InvokeFunc( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? new HashSet< string > { _tempManipulation } : new HashSet< string >(), int.MaxValue ); + } + + DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryMod, "Remove Temporary Mod from specific Collection" ); + if( ImGui.Button( "Remove##Mod" ) ) + { + _lastTempError = _pi.GetIpcSubscriber< string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryMod ) + .InvokeFunc( _tempModName, _tempCollectionName, int.MaxValue ); + } + + DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryModAll, "Remove Temporary Mod from all Collections" ); + if( ImGui.Button( "Remove##ModAll" ) ) + { + _lastTempError = _pi.GetIpcSubscriber< string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryModAll ) + .InvokeFunc( _tempModName, int.MaxValue ); + } } private void DrawTempCollections() diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0cbf419e..2efeaaa9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -4,8 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; -using Dalamud.Configuration; -using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; using Newtonsoft.Json; @@ -22,7 +20,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion - => 4; + => 5; private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -342,6 +340,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) { CheckInitialized(); + + if( character.Length is 0 or > 32 || tag.Length == 0 ) + { + return ( PenumbraApiEc.InvalidArgument, string.Empty ); + } + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) || Penumbra.TempMods.Collections.ContainsKey( character ) ) { @@ -364,8 +368,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } - public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, - int priority ) + public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, HashSet< string > manipCodes, int priority ) { CheckInitialized(); if( !ConvertPaths( paths, out var p ) ) @@ -385,11 +388,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, - IReadOnlySet< string > manipCodes, int priority ) + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, HashSet< string > manipCodes, + int priority ) { CheckInitialized(); - if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; @@ -426,7 +429,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) { CheckInitialized(); - if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; @@ -530,16 +533,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi manips = new HashSet< MetaManipulation >( manipStrings.Count ); foreach( var m in manipStrings ) { - if( Functions.FromCompressedBase64< MetaManipulation >( m, out var manip ) != MetaManipulation.CurrentVersion ) + if( Functions.FromCompressedBase64< MetaManipulation[] >( m, out var manipArray ) != MetaManipulation.CurrentVersion ) { manips = null; return false; } - if( !manips.Add( manip ) ) + foreach( var manip in manipArray! ) { - manips = null; - return false; + if( !manips.Add( manip ) ) + { + manips = null; + return false; + } } } diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index d146f0c3..600dcf9e 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -520,10 +520,10 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection; internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection; - internal ICallGateProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + internal ICallGateProvider< string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >? ProviderAddTemporaryModAll; - internal ICallGateProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + internal ICallGateProvider< string, string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >? ProviderAddTemporaryMod; internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll; @@ -556,7 +556,7 @@ public partial class PenumbraIpc try { ProviderAddTemporaryModAll = - pi.GetIpcProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + pi.GetIpcProvider< string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >( LabelProviderAddTemporaryModAll ); ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll ); } @@ -568,7 +568,7 @@ public partial class PenumbraIpc try { ProviderAddTemporaryMod = - pi.GetIpcProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + pi.GetIpcProvider< string, string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >( LabelProviderAddTemporaryMod ); ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod ); } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index b3be6577..cebc6de9 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -117,7 +117,7 @@ public class TempModManager return RedirectResult.NotRegistered; } - var removed = _modsForAllCollections.RemoveAll( m => + var removed = list.RemoveAll( m => { if( m.Name != tag || priority != null && m.Priority != priority.Value ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 14a876b4..8614bbc4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; @@ -293,7 +294,8 @@ public unsafe partial class PathResolver { // Early return if we prefer the actors own name over its owner. actorName = new Utf8String( gameObject->Name ).ToString(); - if( actorName.Length > 0 && Penumbra.CollectionManager.Characters.TryGetValue( actorName, out var actorCollection ) ) + if( actorName.Length > 0 + && CollectionByActorName( actorName, out var actorCollection ) ) { return actorCollection; } @@ -313,7 +315,7 @@ public unsafe partial class PathResolver ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); // First check temporary character collections, then the own configuration. - return Penumbra.TempMods.Collections.TryGetValue( actualName, out var c ) ? c : Penumbra.CollectionManager.Character( actualName ); + return CollectionByActorName( actualName, out var c ) ? c : Penumbra.CollectionManager.Default; } catch( Exception e ) { @@ -322,6 +324,12 @@ public unsafe partial class PathResolver } } + // Check both temporary and permanent character collections. Temporary first. + private static bool CollectionByActorName( string name, [NotNullWhen( true )] out ModCollection? collection ) + => Penumbra.TempMods.Collections.TryGetValue( name, out collection ) + || Penumbra.CollectionManager.Characters.TryGetValue( name, out collection ); + + // Update collections linked to Game/DrawObjects due to a change in collection configuration. private void CheckCollections( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? name ) { From 95e7febd3846ad09a893e6cb010e9b763709b585 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 18:43:36 +0200 Subject: [PATCH 0314/2451] Let Material Path Resolver search through temporary collections. --- Penumbra.GameData/Structs/CharacterArmor.cs | 21 +++++++++---------- Penumbra/Collections/ModCollection.cs | 4 ++-- .../Interop/Resolver/PathResolver.Material.cs | 7 +++++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index c61ac7ab..6b3f60a8 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -1,15 +1,14 @@ using System.Runtime.InteropServices; -namespace Penumbra.GameData.Structs -{ - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public readonly struct CharacterArmor - { - public readonly SetId Set; - public readonly byte Variant; - public readonly StainId Stain; +namespace Penumbra.GameData.Structs; - public override string ToString() - => $"{Set},{Variant},{Stain}"; - } +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct CharacterArmor +{ + public readonly SetId Set; + public readonly byte Variant; + public readonly StainId Stain; + + public override string ToString() + => $"{Set},{Variant},{Stain}"; } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 1c913a11..0d485116 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -75,9 +75,9 @@ public partial class ModCollection => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary(string tag, string characterName) + public static ModCollection CreateNewTemporary( string tag, string characterName ) { - var collection = new ModCollection($"{tag}_{characterName}_temporary", Empty); + var collection = new ModCollection( $"{tag}_{characterName}", Empty ); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; collection.Index = ~Penumbra.TempMods.Collections.Count; diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index e3f1ab56..64e82ab7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -8,6 +8,7 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Util; namespace Penumbra.Interop.Resolver; @@ -56,7 +57,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, [NotNullWhen(true)] out ModCollection? collection ) + private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { @@ -81,7 +82,9 @@ public unsafe partial class PathResolver var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.CollectionManager.ByName( name, out var collection ) ) + if( Penumbra.TempMods.Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), + out var collection ) + || Penumbra.CollectionManager.ByName( name, out collection ) ) { #if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); From 4381b9ef644f20661a92ac28952924549dad2d9f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jun 2022 22:34:10 +0200 Subject: [PATCH 0315/2451] Add a function to obtain all meta manipulations for a given collection. --- Penumbra/Api/IPenumbraApi.cs | 4 ++++ Penumbra/Api/IpcTester.cs | 9 ++++++++- Penumbra/Api/PenumbraApi.cs | 10 ++++++++++ Penumbra/Api/PenumbraIpc.cs | 13 +++++++++++++ Penumbra/Meta/Manager/MetaManager.cs | 12 +++++++++++- 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 1fa7b576..cc9c7e56 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -97,6 +97,10 @@ 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(); + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + // for the given collection associated with the character name, or the default collection. + public string GetMetaManipulations( string characterName ); + // ############## Mod Settings ################# diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 30059913..9a9794b5 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -14,7 +14,6 @@ using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Api; @@ -449,6 +448,14 @@ public class IpcTester : IDisposable ImGui.OpenPopup( "Ipc Data" ); } + DrawIntro(PenumbraIpc.LabelProviderGetMetaManipulations, "Meta Manipulations" ); + if( ImGui.Button( "Copy to Clipboard" ) ) + { + var base64 = _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderGetMetaManipulations ) + .InvokeFunc( _characterCollectionName ); + ImGui.SetClipboardText( base64 ); + } + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); using var p = ImRaii.Popup( "Ipc Data" ); if( p ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2efeaaa9..e56f0b7a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -443,6 +443,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } + public string GetMetaManipulations( string characterName ) + { + CheckInitialized(); + var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) + ? c + : Penumbra.CollectionManager.Character( characterName ); + var set = collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >(); + return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + } + internal bool HasTooltip => ChangedItemTooltip != null; diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 600dcf9e..a60d63f4 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -326,12 +326,14 @@ public partial class PenumbraIpc public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; + public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; internal ICallGateProvider< IList< string > >? ProviderGetCollections; internal ICallGateProvider< string >? ProviderCurrentCollectionName; internal ICallGateProvider< string >? ProviderDefaultCollectionName; internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + internal ICallGateProvider< string, string >? ProviderGetMetaManipulations; private void InitializeDataProviders( DalamudPluginInterface pi ) { @@ -384,6 +386,16 @@ public partial class PenumbraIpc { PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } + + try + { + ProviderGetMetaManipulations = pi.GetIpcProvider< string, string >( LabelProviderGetMetaManipulations ); + ProviderGetMetaManipulations.RegisterFunc( Api.GetMetaManipulations ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } } private void DisposeDataProviders() @@ -393,6 +405,7 @@ public partial class PenumbraIpc ProviderCurrentCollectionName?.UnregisterFunc(); ProviderDefaultCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc(); + ProviderGetMetaManipulations?.UnregisterFunc(); } } diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index a71e37b1..50404234 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using Penumbra.Collections; using Penumbra.Meta.Files; @@ -30,7 +31,7 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out IMod? mod ) + public bool TryGetValue( MetaManipulation manip, [NotNullWhen( true )] out IMod? mod ) { mod = manip.ManipulationType switch { @@ -53,6 +54,15 @@ public partial class MetaManager : IDisposable + Est.Manipulations.Count + Eqp.Manipulations.Count; + public MetaManipulation[] Manipulations + => Imc.Manipulations.Keys.Select( m => ( MetaManipulation )m ) + .Concat( Eqdp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) + .Concat( Cmp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) + .Concat( Gmp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) + .Concat( Est.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) + .Concat( Eqp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) + .ToArray(); + public MetaManager( ModCollection collection ) => Imc = new MetaManagerImc( collection ); From f0bdecd472c34f44fc549b70e63944d406497343 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Jun 2022 15:55:40 +0200 Subject: [PATCH 0316/2451] Let manipulations use a single string and re-add redrawing by object. --- Penumbra/Api/IPenumbraApi.cs | 9 ++++--- Penumbra/Api/IpcTester.cs | 18 ++++++++++---- Penumbra/Api/PenumbraApi.cs | 47 ++++++++++++++++++++++-------------- Penumbra/Api/PenumbraIpc.cs | 29 ++++++++++++++++------ 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index cc9c7e56..1d81b973 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -53,6 +54,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 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 ); + // 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 ); @@ -148,12 +152,11 @@ public interface IPenumbraApi : IPenumbraApiBase // Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections. // Can return Okay, InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, HashSet< string > manipCodes, - int priority ); + public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ); // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary. // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, HashSet< string > manipCodes, + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString, int priority ); // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 9a9794b5..da087527 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Logging; using Dalamud.Plugin; @@ -281,6 +282,13 @@ public class IpcTester : IDisposable .InvokeAction( _redrawName, ( int )RedrawType.Redraw ); } + DrawIntro( PenumbraIpc.LabelProviderRedrawObject, "Redraw Player Character" ); + if( ImGui.Button( "Redraw##pc" ) && Dalamud.ClientState.LocalPlayer != null ) + { + _pi.GetIpcSubscriber< GameObject, int, object? >( PenumbraIpc.LabelProviderRedrawObject ) + .InvokeAction( Dalamud.ClientState.LocalPlayer, ( int )RedrawType.Redraw ); + } + DrawIntro( PenumbraIpc.LabelProviderRedrawIndex, "Redraw by Index" ); var tmp = _redrawIndex; ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); @@ -448,7 +456,7 @@ public class IpcTester : IDisposable ImGui.OpenPopup( "Ipc Data" ); } - DrawIntro(PenumbraIpc.LabelProviderGetMetaManipulations, "Meta Manipulations" ); + DrawIntro( PenumbraIpc.LabelProviderGetMetaManipulations, "Meta Manipulations" ); if( ImGui.Button( "Copy to Clipboard" ) ) { var base64 = _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderGetMetaManipulations ) @@ -697,21 +705,21 @@ public class IpcTester : IDisposable if( ImGui.Button( "Add##Mod" ) ) { _lastTempError = _pi - .GetIpcSubscriber< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + .GetIpcSubscriber< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderAddTemporaryMod ) .InvokeFunc( _tempModName, _tempCollectionName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? new HashSet< string > { _tempManipulation } : new HashSet< string >(), int.MaxValue ); + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); } DrawIntro( PenumbraIpc.LabelProviderAddTemporaryModAll, "Add Temporary Mod to all Collections" ); if( ImGui.Button( "Add##All" ) ) { _lastTempError = _pi - .GetIpcSubscriber< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + .GetIpcSubscriber< string, Dictionary< string, string >, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderAddTemporaryModAll ) .InvokeFunc( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? new HashSet< string > { _tempManipulation } : new HashSet< string >(), int.MaxValue ); + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); } DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryMod, "Remove Temporary Mod from specific Collection" ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index e56f0b7a..f4403e50 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; using Newtonsoft.Json; @@ -73,6 +74,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawObject( name, setting ); } + public void RedrawObject( GameObject? gameObject, RedrawType setting ) + { + CheckInitialized(); + _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); + } + public void RedrawAll( RedrawType setting ) { CheckInitialized(); @@ -368,7 +375,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } - public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, HashSet< string > manipCodes, int priority ) + public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ) { CheckInitialized(); if( !ConvertPaths( paths, out var p ) ) @@ -376,7 +383,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidGamePath; } - if( !ConvertManips( manipCodes, out var m ) ) + if( !ConvertManips( manipString, out var m ) ) { return PenumbraApiEc.InvalidManipulation; } @@ -388,7 +395,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, HashSet< string > manipCodes, + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString, int priority ) { CheckInitialized(); @@ -403,7 +410,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidGamePath; } - if( !ConvertManips( manipCodes, out var m ) ) + if( !ConvertManips( manipString, out var m ) ) { return PenumbraApiEc.InvalidManipulation; } @@ -535,28 +542,32 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } - // Convert manipulations from transmitted base64 strings to actual manipulations. + // Convert manipulations from a transmitted base64 string to actual manipulations. + // The empty string is treated as an empty set. // Only returns true if all conversions are successful and distinct. - private static bool ConvertManips( IReadOnlyCollection< string > manipStrings, + private static bool ConvertManips( string manipString, [NotNullWhen( true )] out HashSet< MetaManipulation >? manips ) { - manips = new HashSet< MetaManipulation >( manipStrings.Count ); - foreach( var m in manipStrings ) + if( manipString.Length == 0 ) { - if( Functions.FromCompressedBase64< MetaManipulation[] >( m, out var manipArray ) != MetaManipulation.CurrentVersion ) + manips = new HashSet< MetaManipulation >(); + return true; + } + + if( Functions.FromCompressedBase64< MetaManipulation[] >( manipString, out var manipArray ) != MetaManipulation.CurrentVersion ) + { + manips = null; + return false; + } + + manips = new HashSet< MetaManipulation >( manipArray!.Length ); + foreach( var manip in manipArray ) + { + if( !manips.Add( manip ) ) { manips = null; return false; } - - foreach( var manip in manipArray! ) - { - if( !manips.Add( manip ) ) - { - manips = null; - return false; - } - } } return true; diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index a60d63f4..ceaacfc4 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -116,15 +116,17 @@ public partial class PenumbraIpc public partial class PenumbraIpc { + public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; public const string LabelProviderGameObjectRedrawn = "Penumbra.GameObjectRedrawn"; - internal ICallGateProvider< string, int, object? >? ProviderRedrawName; - internal ICallGateProvider< int, int, object? >? ProviderRedrawIndex; - internal ICallGateProvider< int, object? >? ProviderRedrawAll; - internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; + internal ICallGateProvider< string, int, object? >? ProviderRedrawName; + internal ICallGateProvider< GameObject, int, object? >? ProviderRedrawObject; + internal ICallGateProvider< int, int, object? >? ProviderRedrawIndex; + internal ICallGateProvider< int, object? >? ProviderRedrawAll; + internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; private static RedrawType CheckRedrawType( int value ) { @@ -149,6 +151,16 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); } + try + { + ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object? >( LabelProviderRedrawObject ); + ProviderRedrawObject.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); + } + try { ProviderRedrawIndex = pi.GetIpcProvider< int, int, object? >( LabelProviderRedrawIndex ); @@ -186,6 +198,7 @@ public partial class PenumbraIpc private void DisposeRedrawProviders() { ProviderRedrawName?.UnregisterAction(); + ProviderRedrawObject?.UnregisterAction(); ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); Api.GameObjectRedrawn -= OnGameObjectRedrawn; @@ -533,10 +546,10 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection; internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection; - internal ICallGateProvider< string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >? + internal ICallGateProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc >? ProviderAddTemporaryModAll; - internal ICallGateProvider< string, string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >? + internal ICallGateProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >? ProviderAddTemporaryMod; internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll; @@ -569,7 +582,7 @@ public partial class PenumbraIpc try { ProviderAddTemporaryModAll = - pi.GetIpcProvider< string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >( + pi.GetIpcProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc >( LabelProviderAddTemporaryModAll ); ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll ); } @@ -581,7 +594,7 @@ public partial class PenumbraIpc try { ProviderAddTemporaryMod = - pi.GetIpcProvider< string, string, Dictionary< string, string >, HashSet< string >, int, PenumbraApiEc >( + pi.GetIpcProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >( LabelProviderAddTemporaryMod ); ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod ); } From c975336e659730199e4ae61c7323a037b15e9f66 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 23 Jun 2022 14:39:39 +0000 Subject: [PATCH 0317/2451] [CI] Updating repo.json for refs/tags/0.5.2.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 08da37ac..54c32c39 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.1.2", - "TestingAssemblyVersion": "0.5.1.2", + "AssemblyVersion": "0.5.2.0", + "TestingAssemblyVersion": "0.5.2.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.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ec91755065d0d086054955d7994778b3e18ed9d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Jun 2022 18:13:03 +0200 Subject: [PATCH 0318/2451] Add date sort methods. --- OtterGui | 2 +- Penumbra/Configuration.Migration.cs | 40 ++++++++++- Penumbra/Configuration.cs | 72 +++++++++++++++++-- .../Resolver/PathResolver.Animation.cs | 30 +++++++- .../Interop/Resolver/PathResolver.Data.cs | 6 ++ Penumbra/Interop/Resolver/PathResolver.cs | 7 ++ Penumbra/Mods/ModFileSystem.cs | 29 +++++++- Penumbra/UI/Classes/ModFileSystemSelector.cs | 10 +-- .../UI/ConfigWindow.SettingsTab.General.cs | 11 ++- 9 files changed, 185 insertions(+), 22 deletions(-) diff --git a/OtterGui b/OtterGui index 03934d3a..8053b24b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 03934d3a19cb610898412045ad5ea7dad9766a59 +Subproject commit 8053b24bb6b77a13853455c08a89df4894b3f2ee diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 3df24ae7..dc93d966 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -27,6 +27,7 @@ public partial class Configuration public Dictionary< string, string > ModSortOrder = new(); public bool InvertModListOrder; public bool SortFoldersFirst; + public SortModeV3 SortMode = SortModeV3.FoldersFirst; public static void Migrate( Configuration config ) { @@ -45,6 +46,31 @@ public partial class Configuration m.Version0To1(); m.Version1To2(); m.Version2To3(); + m.Version3To4(); + } + + // SortMode was changed from an enum to a type. + private void Version3To4() + { + if( _config.Version != 3 ) + { + return; + } + + SortMode = _data[ nameof( SortMode ) ]?.ToObject< SortModeV3 >() ?? SortMode; + _config.SortMode = SortMode switch + { + SortModeV3.FoldersFirst => ISortMode< Mod >.FoldersFirst, + SortModeV3.Lexicographical => ISortMode< Mod >.Lexicographical, + SortModeV3.InverseFoldersFirst => ISortMode< Mod >.InverseFoldersFirst, + SortModeV3.InverseLexicographical => ISortMode< Mod >.InverseLexicographical, + SortModeV3.FoldersLast => ISortMode< Mod >.FoldersLast, + SortModeV3.InverseFoldersLast => ISortMode< Mod >.InverseFoldersLast, + SortModeV3.InternalOrder => ISortMode< Mod >.InternalOrder, + SortModeV3.InternalOrderInverse => ISortMode< Mod >.InverseInternalOrder, + _ => ISortMode< Mod >.FoldersFirst, + }; + _config.Version = 4; } // SortFoldersFirst was changed from a bool to the enum SortMode. @@ -56,7 +82,7 @@ public partial class Configuration } SortFoldersFirst = _data[ nameof( SortFoldersFirst ) ]?.ToObject< bool >() ?? false; - _config.SortMode = SortFoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical; + SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; _config.Version = 3; } @@ -242,5 +268,17 @@ public partial class Configuration PluginLog.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); } } + + public enum SortModeV3 : byte + { + FoldersFirst = 0x00, + Lexicographical = 0x01, + InverseFoldersFirst = 0x02, + InverseLexicographical = 0x03, + FoldersLast = 0x04, + InverseFoldersLast = 0x05, + InternalOrder = 0x06, + InternalOrderInverse = 0x07, + } } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 14879c2e..8ef57b86 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Dalamud.Configuration; using Dalamud.Logging; +using Newtonsoft.Json; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Import; +using Penumbra.Mods; using Penumbra.UI.Classes; +using Penumbra.Util; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; @@ -40,8 +45,10 @@ public partial class Configuration : IPluginConfiguration public bool EnableResourceLogging { get; set; } = false; public string ResourceLoggingFilter { get; set; } = string.Empty; + [JsonConverter( typeof( SortModeConverter ) )] + [JsonProperty( Order = int.MaxValue )] + public ISortMode< Mod > SortMode = ISortMode< Mod >.FoldersFirst; - public SortMode SortMode { get; set; } = SortMode.FoldersFirst; public bool ScaleModSelector { get; set; } = false; public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; @@ -65,9 +72,25 @@ public partial class Configuration : IPluginConfiguration // Includes adding new colors and migrating from old versions. public static Configuration Load() { - var iConfiguration = Dalamud.PluginInterface.GetPluginConfig(); - var configuration = iConfiguration as Configuration ?? new Configuration(); - if( iConfiguration is { Version: Constants.CurrentVersion } ) + void HandleDeserializationError( object? sender, ErrorEventArgs errorArgs ) + { + PluginLog.Error( + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}" ); + errorArgs.ErrorContext.Handled = true; + } + + Configuration? configuration = null; + if( File.Exists( Dalamud.PluginInterface.ConfigFile.FullName ) ) + { + var text = File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ); + configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings + { + Error = HandleDeserializationError, + } ); + } + + configuration ??= new Configuration(); + if( configuration.Version == Constants.CurrentVersion ) { configuration.AddColors( false ); return configuration; @@ -84,7 +107,8 @@ public partial class Configuration : IPluginConfiguration { try { - Dalamud.PluginInterface.SavePluginConfig( this ); + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( Dalamud.PluginInterface.ConfigFile.FullName, text ); } catch( Exception e ) { @@ -113,12 +137,48 @@ public partial class Configuration : IPluginConfiguration // Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 3; + public const int CurrentVersion = 4; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; public const int MaxScaledSize = 80; public const int DefaultScaledSize = 20; public const int MinScaledSize = 5; + + public static readonly ISortMode< Mod >[] ValidSortModes = + { + ISortMode< Mod >.FoldersFirst, + ISortMode< Mod >.Lexicographical, + new ModFileSystem.ImportDate(), + new ModFileSystem.InverseImportDate(), + ISortMode< Mod >.InverseFoldersFirst, + ISortMode< Mod >.InverseLexicographical, + ISortMode< Mod >.FoldersLast, + ISortMode< Mod >.InverseFoldersLast, + ISortMode< Mod >.InternalOrder, + ISortMode< Mod >.InverseInternalOrder, + }; + } + + private class SortModeConverter : JsonConverter< ISortMode< Mod > > + { + public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) + { + value ??= ISortMode< Mod >.FoldersFirst; + serializer.Serialize( writer, value.GetType().Name ); + } + + public override ISortMode< Mod > ReadJson( JsonReader reader, Type objectType, ISortMode< Mod >? existingValue, + bool hasExistingValue, + JsonSerializer serializer ) + { + var name = serializer.Deserialize< string >( reader ); + if( name == null || !Constants.ValidSortModes.FindFirst( s => s.GetType().Name == name, out var mode ) ) + { + return existingValue ?? ISortMode< Mod >.FoldersFirst; + } + + return mode; + } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index fcab01e3..4b2992a2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -1,9 +1,12 @@ using System; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace Penumbra.Interop.Resolver; @@ -21,7 +24,7 @@ public unsafe partial class PathResolver private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { ulong ret; - var old = _animationLoadCollection; + var old = _animationLoadCollection; try { var getGameObjectIdx = ( ( delegate* unmanaged < IntPtr, int>** )timeline )[ 0 ][ 28 ]; @@ -92,6 +95,7 @@ public unsafe partial class PathResolver _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); } } + LoadSomePapHook!.Original( a1, a2, a3, a4 ); _animationLoadCollection = last; } @@ -107,4 +111,28 @@ public unsafe partial class PathResolver SomeActionLoadHook!.Original( gameObject ); _animationLoadCollection = last; } + + [Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )] + public Hook< CharacterBaseDestructorDelegate >? SomeOtherAvfxHook; + + private void SomeOtherAvfxDetour( IntPtr unk ) + { + var last = _animationLoadCollection; + var gameObject = ( GameObject* )( unk - 0x8B0 ); + _animationLoadCollection = IdentifyCollection( gameObject ); + SomeOtherAvfxHook!.Original( unk ); + _animationLoadCollection = last; + } + + public delegate IntPtr SomeAtexDelegate( IntPtr a1, IntPtr a2, IntPtr a3, IntPtr a4, uint a5, IntPtr a6 ); + + [Signature( "E8 ?? ?? ?? ?? 84 C0 75 ?? 48 8B CE 41 B6" )] + public Hook< SomeAtexDelegate >? SomeAtexHook; + + public IntPtr SomeAtexDetour( IntPtr a1, IntPtr a2, IntPtr a3, IntPtr a4, uint a5, IntPtr a6 ) + { + var ret = SomeAtexHook!.Original( a1, a2, a3, a4, a5, a6 ); + PluginLog.Information( $"{a1:X} {a2:X} {a3:X} {a4:X} {a5:X} {a6:X} {ret}" ); + 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 8614bbc4..e7f95a6d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -97,6 +97,8 @@ public unsafe partial class PathResolver LoadSomeAvfxHook?.Enable(); LoadSomePapHook?.Enable(); SomeActionLoadHook?.Enable(); + SomeOtherAvfxHook?.Enable(); + SomeAtexHook?.Enable(); } private void DisableDataHooks() @@ -111,6 +113,8 @@ public unsafe partial class PathResolver LoadSomeAvfxHook?.Disable(); LoadSomePapHook?.Disable(); SomeActionLoadHook?.Disable(); + SomeOtherAvfxHook?.Disable(); + SomeAtexHook?.Disable(); } private void DisposeDataHooks() @@ -124,6 +128,8 @@ public unsafe partial class PathResolver LoadSomeAvfxHook?.Dispose(); LoadSomePapHook?.Dispose(); SomeActionLoadHook?.Dispose(); + SomeOtherAvfxHook?.Dispose(); + SomeAtexHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 9c8111b7..7af31623 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -76,6 +76,13 @@ public partial class PathResolver : IDisposable private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { + if( type == ResourceType.Atex ) + if (_animationLoadCollection == null) + PluginLog.Information( $"ATEX {_} Default" ); + else + { + PluginLog.Information( $"ATEX {_} {_animationLoadCollection?.Name}" ); + } if( _animationLoadCollection != null ) { switch( type ) diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index fef26f5e..0b186ea4 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -45,6 +46,30 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable Penumbra.ModManager.ModMetaChanged -= OnMetaChange; } + public struct ImportDate : ISortMode< Mod > + { + public string Name + => "Import Date (Older First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; + + public IEnumerable< IPath > GetChildren( Folder f ) + => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderBy( l => l.Value.ImportDate ) ); + } + + public struct InverseImportDate : ISortMode< Mod > + { + public string Name + => "Import Date (Newer First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; + + public IEnumerable< IPath > GetChildren( Folder f ) + => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderByDescending( l => l.Value.ImportDate ) ); + } + // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() @@ -72,7 +97,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) { var old = oldName.FixName(); - if( Find( old, out var child ) && child is not Folder) + if( Find( old, out var child ) && child is not Folder ) { Rename( child, mod.Name.Text ); } @@ -97,7 +122,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable CreateLeaf( Root, name, mod ); break; case ModPathChangeType.Deleted: - var leaf = Root.GetAllDescendants( SortMode.Lexicographical ).OfType< Leaf >().FirstOrDefault( l => l.Value == mod ); + var leaf = Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< Leaf >().FirstOrDefault( l => l.Value == mod ); if( leaf != null ) { Delete( leaf ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index a062ff89..ab5de6b5 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,7 +14,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; -using OtterGui.Classes; +using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -67,7 +67,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod => base.SelectedLeaf; // Customization points. - public override SortMode SortMode + public override ISortMode< Mod > SortMode => Penumbra.Config.SortMode; protected override uint ExpandedFolderColor @@ -315,7 +315,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod // Helpers. private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) { - var mods = folder.GetAllDescendants( SortMode.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => l.Value ); + var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => l.Value ); if( inherit ) { Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); @@ -404,7 +404,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { if( _lastSelectedDirectory.Length > 0 ) { - base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) + base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); OnSelectionChange( null, base.SelectedLeaf?.Value, default ); _lastSelectedDirectory = string.Empty; @@ -422,7 +422,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod try { - var leaf = FileSystem.Root.GetChildren( SortMode.Lexicographical ) + var leaf = FileSystem.Root.GetChildren( ISortMode< Mod >.Lexicographical ) .FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod ); if( leaf == null ) { diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 22319bfc..56e1a7e9 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -4,9 +4,9 @@ using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; -using OtterGui.Filesystem; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Util; namespace Penumbra.UI; @@ -119,20 +119,19 @@ public partial class ConfigWindow { var sortMode = Penumbra.Config.SortMode; ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using var combo = ImRaii.Combo( "##sortMode", sortMode.Data().Name ); + using var combo = ImRaii.Combo( "##sortMode", sortMode.Name ); if( combo ) { - foreach( var val in Enum.GetValues< SortMode >() ) + foreach( var val in Configuration.Constants.ValidSortModes ) { - var (name, desc) = val.Data(); - if( ImGui.Selectable( name, val == sortMode ) && val != sortMode ) + if( ImGui.Selectable( val.Name, val.GetType() == sortMode.GetType() ) && val.GetType() != sortMode.GetType() ) { Penumbra.Config.SortMode = val; _window._selector.SetFilterDirty(); Penumbra.Config.Save(); } - ImGuiUtil.HoverTooltip( desc ); + ImGuiUtil.HoverTooltip( val.Description ); } } From 549f8ce4b427b004b78de42983238febad9006b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 Jun 2022 13:44:15 +0200 Subject: [PATCH 0319/2451] Add special collections. --- .../Collections/CollectionManager.Active.cs | 198 +++++++++++++++--- Penumbra/Collections/CollectionManager.cs | 26 +-- Penumbra/Collections/CollectionType.cs | 115 ++++++++++ Penumbra/Configuration.Migration.cs | 2 +- .../Interop/Resolver/PathResolver.Data.cs | 77 ++++++- Penumbra/Penumbra.cs | 37 ++-- Penumbra/UI/Classes/ModFileSystemSelector.cs | 6 +- ...ConfigWindow.CollectionsTab.Inheritance.cs | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 86 +++++++- Penumbra/UI/ConfigWindow.Misc.cs | 17 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 6 +- 11 files changed, 486 insertions(+), 86 deletions(-) create mode 100644 Penumbra/Collections/CollectionType.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index c75b7e44..5e950228 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -39,19 +39,59 @@ public partial class ModCollection public ModCollection Character( string name ) => _characters.TryGetValue( name, out var c ) ? c : Default; - // Set a active collection, can be used to set Default, Current or Character collections. - private void SetCollection( int newIdx, Type type, string? characterName = null ) + // Special Collections + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; + + // Return the configured collection for the given type or null. + public ModCollection? ByType( CollectionType type, string? name = null ) { - var oldCollectionIdx = type switch + if( type.IsSpecial() ) { - Type.Default => Default.Index, - Type.Current => Current.Index, - Type.Character => characterName?.Length > 0 + return _specialCollections[ ( int )type ]; + } + + return type switch + { + CollectionType.Default => Default, + CollectionType.Current => Current, + CollectionType.Character => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, + CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, + _ => null, + }; + } + + // Set a active collection, can be used to set Default, Current or Character collections. + private void SetCollection( int newIdx, CollectionType collectionType, string? characterName = null ) + { + var oldCollectionIdx = collectionType switch + { + CollectionType.Default => Default.Index, + CollectionType.Current => Current.Index, + CollectionType.Character => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index : Default.Index : -1, - _ => -1, + CollectionType.Yourself => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.PlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.NonPlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Midlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Highlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Wildwood => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Duskwight => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Plainsfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Dunesfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.SeekerOfTheSun => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.KeeperOfTheMoon => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Seawolf => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Hellsguard => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Raen => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Xaela => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Helion => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Lost => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Rava => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Veena => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + _ => -1, }; if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) @@ -66,31 +106,109 @@ public partial class ModCollection } RemoveCache( oldCollectionIdx ); - switch( type ) + switch( collectionType ) { - case Type.Default: + case CollectionType.Default: Default = newCollection; Penumbra.ResidentResources.Reload(); Default.SetFiles(); break; - case Type.Current: + case CollectionType.Current: Current = newCollection; break; - case Type.Character: + case CollectionType.Character: _characters[ characterName! ] = newCollection; break; + default: + _specialCollections[ ( int )collectionType ] = newCollection; + break; } - + UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); + CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, characterName ); } private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + => CurrentCollectionInUse = _specialCollections + .OfType< ModCollection >() + .Prepend( Default ) + .Concat( Characters.Values ) + .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); - public void SetCollection( ModCollection collection, Type type, string? characterName = null ) - => SetCollection( collection.Index, type, characterName ); + public void SetCollection( ModCollection collection, CollectionType collectionType, string? characterName = null ) + => SetCollection( collection.Index, collectionType, characterName ); + + // Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection( CollectionType collectionType ) + { + switch( collectionType ) + { + case CollectionType.Yourself: + case CollectionType.PlayerCharacter: + case CollectionType.NonPlayerCharacter: + case CollectionType.Midlander: + case CollectionType.Highlander: + case CollectionType.Wildwood: + case CollectionType.Duskwight: + case CollectionType.Plainsfolk: + case CollectionType.Dunesfolk: + case CollectionType.SeekerOfTheSun: + case CollectionType.KeeperOfTheMoon: + case CollectionType.Seawolf: + case CollectionType.Hellsguard: + case CollectionType.Raen: + case CollectionType.Xaela: + case CollectionType.Helion: + case CollectionType.Lost: + case CollectionType.Rava: + case CollectionType.Veena: + if( _specialCollections[ ( int )collectionType ] != null ) + { + return false; + } + + _specialCollections[ ( int )collectionType ] = Empty; + CollectionChanged.Invoke( collectionType, null, Empty, null ); + return true; + default: return false; + } + } + + // Remove a special collection if it exists + public void RemoveSpecialCollection( CollectionType collectionType ) + { + switch( collectionType ) + { + case CollectionType.Yourself: + case CollectionType.PlayerCharacter: + case CollectionType.NonPlayerCharacter: + case CollectionType.Midlander: + case CollectionType.Highlander: + case CollectionType.Wildwood: + case CollectionType.Duskwight: + case CollectionType.Plainsfolk: + case CollectionType.Dunesfolk: + case CollectionType.SeekerOfTheSun: + case CollectionType.KeeperOfTheMoon: + case CollectionType.Seawolf: + case CollectionType.Hellsguard: + case CollectionType.Raen: + case CollectionType.Xaela: + case CollectionType.Helion: + case CollectionType.Lost: + case CollectionType.Rava: + case CollectionType.Veena: + var old = _specialCollections[ ( int )collectionType ]; + if( old != null ) + { + _specialCollections[ ( int )collectionType ] = null; + CollectionChanged.Invoke( collectionType, old, null, null ); + } + + return; + } + } // Create a new character collection. Returns false if the character name already has a collection. public bool CreateCharacterCollection( string characterName ) @@ -101,7 +219,7 @@ public partial class ModCollection } _characters[ characterName ] = Empty; - CollectionChanged.Invoke( Type.Character, null, Empty, characterName ); + CollectionChanged.Invoke( CollectionType.Character, null, Empty, characterName ); return true; } @@ -112,7 +230,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged.Invoke( Type.Character, collection, null, characterName ); + CollectionChanged.Invoke( CollectionType.Character, collection, null, characterName ); } } @@ -157,6 +275,25 @@ public partial class ModCollection Current = this[ currentIdx ]; } + // Load special collections. + foreach( var type in CollectionTypeExtensions.Special ) + { + var typeName = jObject[ type.ToString() ]?.ToObject< string >(); + if( typeName != null ) + { + var idx = GetIndexForCollectionName( typeName ); + if( idx < 0 ) + { + PluginLog.Error( $"Last choice of {type.ToName()} Collection {typeName} is not available, removed." ); + configChanged = true; + } + else + { + _specialCollections[ ( int )type ] = this[ idx ]; + } + } + } + // Load character collections. If a player name comes up multiple times, the last one is applied. var characters = jObject[ nameof( Characters ) ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); foreach( var (player, collectionName) in characters ) @@ -187,10 +324,14 @@ public partial class ModCollection public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ) ) ); + () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ), + _specialCollections.WithIndex() + .Where( c => c.Item1 != null ) + .Select( c => ( ( CollectionType )c.Item2, c.Item1!.Name ) ) ) ); } - internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters ) + internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters, + IEnumerable< (CollectionType, string) > special ) { var file = ActiveCollectionFile; try @@ -204,6 +345,12 @@ public partial class ModCollection j.WriteValue( def ); j.WritePropertyName( nameof( Current ) ); j.WriteValue( current ); + foreach( var (type, collection) in special ) + { + j.WritePropertyName( type.ToString() ); + j.WriteValue( collection ); + } + j.WritePropertyName( nameof( Characters ) ); j.WriteStartObject(); foreach( var (character, collection) in characters ) @@ -246,9 +393,9 @@ public partial class ModCollection // Save if any of the active collections is changed. - private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 ) + private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) { - if( type != Type.Inactive ) + if( collectionType != CollectionType.Inactive ) { SaveActiveCollections(); } @@ -261,7 +408,7 @@ public partial class ModCollection Default.CreateCache(); Current.CreateCache(); - foreach( var collection in _characters.Values ) + foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) { collection.CreateCache(); } @@ -269,7 +416,10 @@ public partial class ModCollection private void RemoveCache( int idx ) { - if( idx != Default.Index && idx != Current.Index && _characters.Values.All( c => c.Index != idx ) ) + if( idx != Default.Index + && idx != Current.Index + && _specialCollections.All( c => c == null || c.Index != idx ) + && _characters.Values.All( c => c.Index != idx ) ) { _collections[ idx ].ClearCache(); } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index d1c5b2a2..0715d987 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -13,20 +13,12 @@ namespace Penumbra.Collections; public partial class ModCollection { - public enum Type : byte - { - Inactive, // A collection was added or removed - Default, // The default collection was changed - Character, // A character collection was changed - Current, // The current collection was changed. - } - public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { // On addition, oldCollection is null. On deletion, newCollection is null. - // CharacterName is onls set for type == Character. - public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, - string? characterName = null ); + // CharacterName is only set for type == Character. + public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, + ModCollection? newCollection, string? characterName = null ); private readonly Mod.Manager _modManager; @@ -124,8 +116,8 @@ public partial class ModCollection _collections.Add( newCollection ); newCollection.Save(); PluginLog.Debug( "Added collection {Name:l}.", newCollection.Name ); - CollectionChanged.Invoke( Type.Inactive, null, newCollection ); - SetCollection( newCollection.Index, Type.Current ); + CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); + SetCollection( newCollection.Index, CollectionType.Current ); return true; } @@ -148,17 +140,17 @@ public partial class ModCollection if( idx == Current.Index ) { - SetCollection( DefaultName, Type.Current ); + SetCollection( DefaultName, CollectionType.Current ); } if( idx == Default.Index ) { - SetCollection( Empty, Type.Default ); + SetCollection( Empty, CollectionType.Default ); } foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) { - SetCollection( Empty, Type.Character, characterName ); + SetCollection( Empty, CollectionType.Character, characterName ); } var collection = _collections[ idx ]; @@ -179,7 +171,7 @@ public partial class ModCollection } PluginLog.Debug( "Removed collection {Name:l}.", collection.Name ); - CollectionChanged.Invoke( Type.Inactive, collection, null ); + CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); return true; } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs new file mode 100644 index 00000000..f8fc6d48 --- /dev/null +++ b/Penumbra/Collections/CollectionType.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq; +using Penumbra.GameData.Enums; + +namespace Penumbra.Collections; + +public enum CollectionType : byte +{ + // Special Collections + Yourself = 0, + PlayerCharacter, + NonPlayerCharacter, + Midlander, + Highlander, + Wildwood, + Duskwight, + Plainsfolk, + Dunesfolk, + SeekerOfTheSun, + KeeperOfTheMoon, + Seawolf, + Hellsguard, + Raen, + Xaela, + Helion, + Lost, + Rava, + Veena, + + Inactive, // A collection was added or removed + Default, // The default collection was changed + Character, // A character collection was changed + Current, // The current collection was changed. +} + +public static class CollectionTypeExtensions +{ + public static bool IsSpecial( this CollectionType collectionType ) + => collectionType is >= CollectionType.Yourself and < CollectionType.Inactive; + + public static readonly CollectionType[] Special = Enum.GetValues< CollectionType >().Where( IsSpecial ).ToArray(); + + public static string ToName( this CollectionType collectionType ) + => collectionType switch + { + CollectionType.Yourself => "Your Character", + CollectionType.PlayerCharacter => "Player Characters", + CollectionType.NonPlayerCharacter => "Non-Player Characters", + CollectionType.Midlander => SubRace.Midlander.ToName(), + CollectionType.Highlander => SubRace.Highlander.ToName(), + CollectionType.Wildwood => SubRace.Wildwood.ToName(), + CollectionType.Duskwight => SubRace.Duskwight.ToName(), + CollectionType.Plainsfolk => SubRace.Plainsfolk.ToName(), + CollectionType.Dunesfolk => SubRace.Dunesfolk.ToName(), + CollectionType.SeekerOfTheSun => SubRace.SeekerOfTheSun.ToName(), + CollectionType.KeeperOfTheMoon => SubRace.KeeperOfTheMoon.ToName(), + CollectionType.Seawolf => SubRace.Seawolf.ToName(), + CollectionType.Hellsguard => SubRace.Hellsguard.ToName(), + CollectionType.Raen => SubRace.Raen.ToName(), + CollectionType.Xaela => SubRace.Xaela.ToName(), + CollectionType.Helion => SubRace.Helion.ToName(), + CollectionType.Lost => SubRace.Lost.ToName(), + CollectionType.Rava => SubRace.Rava.ToName(), + CollectionType.Veena => SubRace.Veena.ToName(), + CollectionType.Inactive => "Collection", + CollectionType.Default => "Default", + CollectionType.Character => "Character", + CollectionType.Current => "Current", + _ => string.Empty, + }; + + public static string ToDescription( this CollectionType collectionType ) + => collectionType switch + { + CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" + + "It takes precedence before all other collections except for explicitly named character collections.", + CollectionType.PlayerCharacter => + "This collection applies to all player characters that do not have a more specific character or racial collections associated.", + CollectionType.NonPlayerCharacter => + "This collection applies to all human non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.Midlander => + "This collection applies to all player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.Highlander => + "This collection applies to all player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.Wildwood => + "This collection applies to all player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.Duskwight => + "This collection applies to all player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.Plainsfolk => + "This collection applies to all player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.Dunesfolk => + "This collection applies to all player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.SeekerOfTheSun => + "This collection applies to all player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.KeeperOfTheMoon => + "This collection applies to all player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.Seawolf => + "This collection applies to all player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.Hellsguard => + "This collection applies to all player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.Raen => + "This collection applies to all player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.Xaela => + "This collection applies to all player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.Helion => + "This collection applies to all player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.Lost => + "This collection applies to all player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.Rava => + "This collection applies to all player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.Veena => + "This collection applies to all player character Veena Viera that do not have a more specific character collection associated.", + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index dc93d966..b1714470 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -182,7 +182,7 @@ public partial class Configuration DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ) ); + CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty<(CollectionType, string)>() ); } // Collections were introduced and the previous CurrentCollection got put into ModDirectory. diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index e7f95a6d..f51d1371 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -13,6 +13,7 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.Collections; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; @@ -320,8 +321,12 @@ public unsafe partial class PathResolver } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - // First check temporary character collections, then the own configuration. - return CollectionByActorName( actualName, out var c ) ? c : Penumbra.CollectionManager.Default; + // First check temporary character collections, then the own configuration, then special collections. + return CollectionByActorName( actualName, out var c ) + ? c + : CollectionByActor( actualName, gameObject, out c ) + ? c + : Penumbra.CollectionManager.Default; } catch( Exception e ) { @@ -335,11 +340,75 @@ public unsafe partial class PathResolver => Penumbra.TempMods.Collections.TryGetValue( name, out collection ) || Penumbra.CollectionManager.Characters.TryGetValue( name, out collection ); + // Check special collections given the actor. + private static bool CollectionByActor( string name, GameObject* actor, [NotNullWhen( true )] out ModCollection? collection ) + { + collection = null; + // Check for the Yourself collection. + if( actor->ObjectIndex == 0 || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + { + collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); + if( collection != null ) + { + return true; + } + } + + if( actor->IsCharacter() ) + { + var character = ( Character* )actor; + // Only handle human models. + if( character->ModelCharaId == 0 ) + { + // Check if the object is a non-player human NPC. + if( actor->ObjectKind != ( byte )ObjectKind.Player ) + { + collection = Penumbra.CollectionManager.ByType( CollectionType.NonPlayerCharacter ); + if( collection != null ) + { + return true; + } + } + else + { + // Check the subrace. If it does not fit any or no subrace collection is set, check the player character collection. + collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch + { + SubRace.Midlander => Penumbra.CollectionManager.ByType( CollectionType.Midlander ), + SubRace.Highlander => Penumbra.CollectionManager.ByType( CollectionType.Highlander ), + SubRace.Wildwood => Penumbra.CollectionManager.ByType( CollectionType.Wildwood ), + SubRace.Duskwight => Penumbra.CollectionManager.ByType( CollectionType.Duskwight ), + SubRace.Plainsfolk => Penumbra.CollectionManager.ByType( CollectionType.Plainsfolk ), + SubRace.Dunesfolk => Penumbra.CollectionManager.ByType( CollectionType.Dunesfolk ), + SubRace.SeekerOfTheSun => Penumbra.CollectionManager.ByType( CollectionType.SeekerOfTheSun ), + SubRace.KeeperOfTheMoon => Penumbra.CollectionManager.ByType( CollectionType.KeeperOfTheMoon ), + SubRace.Seawolf => Penumbra.CollectionManager.ByType( CollectionType.Seawolf ), + SubRace.Hellsguard => Penumbra.CollectionManager.ByType( CollectionType.Hellsguard ), + SubRace.Raen => Penumbra.CollectionManager.ByType( CollectionType.Raen ), + SubRace.Xaela => Penumbra.CollectionManager.ByType( CollectionType.Xaela ), + SubRace.Helion => Penumbra.CollectionManager.ByType( CollectionType.Helion ), + SubRace.Lost => Penumbra.CollectionManager.ByType( CollectionType.Lost ), + SubRace.Rava => Penumbra.CollectionManager.ByType( CollectionType.Rava ), + SubRace.Veena => Penumbra.CollectionManager.ByType( CollectionType.Veena ), + _ => null, + }; + collection ??= Penumbra.CollectionManager.ByType( CollectionType.PlayerCharacter ); + if( collection != null ) + { + return true; + } + } + } + } + + return false; + } + // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection.Type type, ModCollection? _1, ModCollection? _2, string? name ) + private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) { - if( type is not (ModCollection.Type.Character or ModCollection.Type.Default) ) + if( type is not (CollectionType.Character or CollectionType.Default) ) { return; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c715eba5..82c6bcf1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -324,23 +324,34 @@ public class Penumbra : IDisposable return false; } - switch( type ) + foreach( var t in Enum.GetValues< CollectionType >() ) { - case "default": - if( collection == CollectionManager.Default ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); - return false; - } + if( t is CollectionType.Inactive or CollectionType.Character + || !string.Equals( t.ToString(), type, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } - CollectionManager.SetCollection( collection, ModCollection.Type.Default ); - Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); - return true; - default: - Dalamud.Chat.Print( - "Second command argument is not default, the correct command format is: /penumbra collection default " ); + var oldCollection = CollectionManager.ByType( t ); + if( collection == oldCollection ) + { + Dalamud.Chat.Print( $"{collection.Name} already is the {t.ToName()} Collection." ); return false; + } + + if( oldCollection == null && t.IsSpecial() ) + { + CollectionManager.CreateSpecialCollection( t ); + } + + CollectionManager.SetCollection( collection, t, null ); + Dalamud.Chat.Print( $"Set {collection.Name} as {t.ToName()} Collection." ); + return true; } + + Dalamud.Chat.Print( + "Second command argument is not default, the correct command format is: /penumbra collection " ); + return false; } private void OnCommand( string command, string rawArgs ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ab5de6b5..888a385e 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -47,7 +47,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.ModManager.ModMetaChanged += OnModMetaChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); + OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); } public override void Dispose() @@ -354,9 +354,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod OnSelectionChange( Selected, Selected, default ); } - private void OnCollectionChange( ModCollection.Type type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) { - if( type != ModCollection.Type.Current || oldCollection == newCollection ) + if( collectionType != CollectionType.Current || oldCollection == newCollection ) { return; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index bc9af12f..be2ba20a 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -152,7 +152,7 @@ public partial class ConfigWindow { if( _newCurrentCollection != null ) { - Penumbra.CollectionManager.SetCollection( _newCurrentCollection, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( _newCurrentCollection, CollectionType.Current ); _newCurrentCollection = null; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index efbb6532..02e2c578 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Util; namespace Penumbra.UI; @@ -33,9 +34,10 @@ public partial class ConfigWindow // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection = false; - private string _newCharacterName = string.Empty; + private string _newCollectionName = string.Empty; + private bool _canAddCollection = false; + private string _newCharacterName = string.Empty; + private CollectionType? _currentType = CollectionType.Yourself; // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -104,7 +106,7 @@ public partial class ConfigWindow private void DrawCurrentCollectionSelector() { - DrawCollectionSelector( "##current", _window._inputTextWidth.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Current Collection", "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); @@ -112,12 +114,56 @@ public partial class ConfigWindow private void DrawDefaultCollectionSelector() { - DrawCollectionSelector( "##default", _window._inputTextWidth.X, ModCollection.Type.Default, true, null ); + DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Default Collection", "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" ); } + // We do not check for valid character names. + private void DrawNewSpecialCollection() + { + const string description = "Special Collections apply to certain types of characters.\n" + + "All of them take precedence before the Default collection,\n" + + "but all character collections take precedence before them."; + + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value ) != null ) + { + _currentType = CollectionTypeExtensions.Special.FindFirst( t => Penumbra.CollectionManager.ByType( t ) == null, out var t2 ) + ? t2 + : null; + } + + if( _currentType == null ) + { + return; + } + + using( var combo = ImRaii.Combo( "##NewSpecial", _currentType.Value.ToName() ) ) + { + if( combo ) + { + foreach( var type in CollectionTypeExtensions.Special.Where( t => Penumbra.CollectionManager.ByType( t ) == null ) ) + { + if( ImGui.Selectable( type.ToName(), type == _currentType.Value ) ) + { + _currentType = type; + } + } + } + } + + ImGui.SameLine(); + var disabled = _currentType == null; + var tt = disabled ? "Please select a special collection type before creating the collection.\n\n" + description : description; + if( ImGuiUtil.DrawDisabledButton( "Create New Special Collection", Vector2.Zero, tt, disabled ) ) + { + Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value ); + _currentType = null; + } + } + // We do not check for valid character names. private void DrawNewCharacterCollection() { @@ -145,14 +191,36 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawDefaultCollectionSelector(); ImGui.Dummy( _window._defaultSpace ); + foreach( var type in CollectionTypeExtensions.Special ) + { + var collection = Penumbra.CollectionManager.ByType( type ); + if( collection != null ) + { + using var id = ImRaii.PushId( ( int )type ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, + false, true ) ) + { + Penumbra.CollectionManager.RemoveSpecialCollection( type ); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); + } + } + + DrawNewSpecialCollection(); + ImGui.Dummy( _window._defaultSpace ); + 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 ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, - true ) ) + false, true ) ) { Penumbra.CollectionManager.RemoveCharacterCollection( name ); } @@ -163,7 +231,7 @@ public partial class ConfigWindow } DrawNewCharacterCollection(); - ImGui.NewLine(); + ImGui.Dummy( _window._defaultSpace ); } } diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 92689787..c6f1727f 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -93,18 +93,13 @@ public partial class ConfigWindow } // Draw a collection selector of a certain width for a certain type. - private static void DrawCollectionSelector( string label, float width, ModCollection.Type type, bool withEmpty, string? characterName ) + private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty, + string? characterName ) { ImGui.SetNextItemWidth( width ); - var current = type switch - { - ModCollection.Type.Default => Penumbra.CollectionManager.Default, - ModCollection.Type.Character => Penumbra.CollectionManager.Character( characterName ?? string.Empty ), - ModCollection.Type.Current => Penumbra.CollectionManager.Current, - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - using var combo = ImRaii.Combo( label, current.Name ); + var current = Penumbra.CollectionManager.ByType( collectionType, characterName ); + using var combo = ImRaii.Combo( label, current?.Name ?? string.Empty ); if( combo ) { foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) @@ -112,7 +107,7 @@ public partial class ConfigWindow using var id = ImRaii.PushId( collection.Index ); if( ImGui.Selectable( collection.Name, collection == current ) ) { - Penumbra.CollectionManager.SetCollection( collection, type, characterName ); + Penumbra.CollectionManager.SetCollection( collection, collectionType, characterName ); } } } @@ -140,7 +135,7 @@ public partial class ConfigWindow } // Add Penumbra Root. This is not updated if the root changes right now. - fileManager.CustomSideBarItems.Add( ("Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0) ); + fileManager.CustomSideBarItems.Add( ( "Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0 ) ); // Remove Videos and Music. fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index d85ff38a..e5f3b5c3 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -62,7 +62,7 @@ public partial class ConfigWindow ImGui.SameLine(); DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); if( !Penumbra.CollectionManager.CurrentCollectionInUse ) { ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); @@ -79,7 +79,7 @@ public partial class ConfigWindow : "Set the current collection to the configured default collection."; if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) { - Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); } } @@ -97,7 +97,7 @@ public partial class ConfigWindow }; if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) { - Penumbra.CollectionManager.SetCollection( collection, ModCollection.Type.Current ); + Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); } } From 32cf729aa89630cfcd19f98dc64e87c17492b982 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 Jun 2022 14:39:59 +0200 Subject: [PATCH 0320/2451] Load Atex based on the last loaded Avfx, testing? --- .../Resolver/PathResolver.Animation.cs | 17 +----- .../Interop/Resolver/PathResolver.Data.cs | 3 -- Penumbra/Interop/Resolver/PathResolver.cs | 54 ++++++++++++------- Penumbra/Penumbra.cs | 6 +++ 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 4b2992a2..563b32ca 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -1,10 +1,6 @@ using System; -using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -13,6 +9,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { private ModCollection? _animationLoadCollection; + private ModCollection? _lastAvfxCollection = null; public delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); @@ -123,16 +120,4 @@ public unsafe partial class PathResolver SomeOtherAvfxHook!.Original( unk ); _animationLoadCollection = last; } - - public delegate IntPtr SomeAtexDelegate( IntPtr a1, IntPtr a2, IntPtr a3, IntPtr a4, uint a5, IntPtr a6 ); - - [Signature( "E8 ?? ?? ?? ?? 84 C0 75 ?? 48 8B CE 41 B6" )] - public Hook< SomeAtexDelegate >? SomeAtexHook; - - public IntPtr SomeAtexDetour( IntPtr a1, IntPtr a2, IntPtr a3, IntPtr a4, uint a5, IntPtr a6 ) - { - var ret = SomeAtexHook!.Original( a1, a2, a3, a4, a5, a6 ); - PluginLog.Information( $"{a1:X} {a2:X} {a3:X} {a4:X} {a5:X} {a6:X} {ret}" ); - 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 f51d1371..42e201ae 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -99,7 +99,6 @@ public unsafe partial class PathResolver LoadSomePapHook?.Enable(); SomeActionLoadHook?.Enable(); SomeOtherAvfxHook?.Enable(); - SomeAtexHook?.Enable(); } private void DisableDataHooks() @@ -115,7 +114,6 @@ public unsafe partial class PathResolver LoadSomePapHook?.Disable(); SomeActionLoadHook?.Disable(); SomeOtherAvfxHook?.Disable(); - SomeAtexHook?.Disable(); } private void DisposeDataHooks() @@ -130,7 +128,6 @@ public unsafe partial class PathResolver LoadSomePapHook?.Dispose(); SomeActionLoadHook?.Dispose(); SomeOtherAvfxHook?.Dispose(); - SomeAtexHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 7af31623..f8c99e1c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -44,7 +44,7 @@ public partial class PathResolver : IDisposable || PathCollections.TryRemove( gamePath.Path, out collection ) || HandleAnimationFile( type, gamePath, out collection ) || HandleDecalFile( type, gamePath, out collection ); - if( !nonDefault || collection == null) + if( !nonDefault || collection == null ) { collection = Penumbra.CollectionManager.Default; } @@ -60,7 +60,7 @@ public partial class PathResolver : IDisposable return true; } - private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen(true)] out ModCollection? collection ) + private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection ) { if( type == ResourceType.Tex && _lastCreatedCollection != null @@ -74,27 +74,43 @@ public partial class PathResolver : IDisposable return false; } - private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) + private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection ) { - if( type == ResourceType.Atex ) - if (_animationLoadCollection == null) - PluginLog.Information( $"ATEX {_} Default" ); - else - { - PluginLog.Information( $"ATEX {_} {_animationLoadCollection?.Name}" ); - } - if( _animationLoadCollection != null ) + switch( type ) { - switch( type ) - { - case ResourceType.Tmb: - case ResourceType.Pap: - case ResourceType.Avfx: - case ResourceType.Atex: - case ResourceType.Scd: + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Scd: + if( _animationLoadCollection != null ) + { collection = _animationLoadCollection; return true; - } + } + + break; + case ResourceType.Avfx: + _lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default; + if( _animationLoadCollection != null ) + { + collection = _animationLoadCollection; + return true; + } + + break; + case ResourceType.Atex: + if( _lastAvfxCollection != null ) + { + collection = _lastAvfxCollection; + return true; + } + + if( _animationLoadCollection != null ) + { + collection = _animationLoadCollection; + return true; + } + + break; } collection = null; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 82c6bcf1..993b5edc 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -504,6 +504,12 @@ public class Penumbra : IDisposable CollectionManager.Default.Index ); sb.AppendFormat( "> **`Current Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Current ), CollectionManager.Current.Index ); + foreach( var type in CollectionTypeExtensions.Special ) + { + var collection = CollectionManager.ByType( type ); + if( collection != null ) + sb.AppendFormat( "> **`{0,-29}`** {1}... ({2})\n", type.ToName(), CollectionName( collection ), collection.Index ); + } foreach( var (name, collection) in CollectionManager.Characters ) { sb.AppendFormat( "> **`{2,-29}`** {0}... ({1})\n", CollectionName( collection ), From 5aeff6d40f9caa0e23965193f454a1070542eb39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 Jun 2022 15:19:58 +0200 Subject: [PATCH 0321/2451] Test Penumbra loading CharacterUtility. --- Penumbra/Interop/CharacterUtility.cs | 16 ++- .../Loader/ResourceLoader.Replacement.cs | 22 ++- Penumbra/Penumbra.cs | 132 ++++++++---------- Penumbra/Penumbra.json | 2 + Penumbra/UI/ConfigWindow.cs | 4 +- 5 files changed, 92 insertions(+), 84 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 1c26ebb8..0db41cd7 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -62,13 +62,12 @@ public unsafe class CharacterUtility : IDisposable public CharacterUtility() { SignatureHelper.Initialise( this ); - - Dalamud.Framework.Update += LoadDefaultResources; - LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); + LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); + LoadDefaultResources( true ); } // We store the default data of the resources so we can always restore them. - private void LoadDefaultResources( object _ ) + private void LoadDefaultResources( bool repeat ) { var missingCount = 0; if( Address == null ) @@ -95,10 +94,15 @@ public unsafe class CharacterUtility : IDisposable if( missingCount == 0 ) { - Dalamud.Framework.Update -= LoadDefaultResources; - Ready = true; + Ready = true; LoadingFinished.Invoke(); } + else if( repeat ) + { + PluginLog.Debug( "Custom load of character resources triggered." ); + LoadCharacterResources(); + LoadDefaultResources( false ); + } } // Set the data of one of the stored resources to a given pointer and length. diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 8390d653..ee70d33c 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -29,7 +29,8 @@ public unsafe partial class ResourceLoader [FieldOffset( 20 )] public uint SegmentLength; - public bool IsPartialRead => SegmentLength != 0; + public bool IsPartialRead + => SegmentLength != 0; } public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, @@ -83,12 +84,25 @@ public unsafe partial class ResourceLoader ResourceRequested?.Invoke( gamePath, isSync ); + // Force metadata tables to load synchronously and not be able to be replaced. + switch( *resourceType ) + { + case ResourceType.Eqp: + case ResourceType.Gmp: + case ResourceType.Eqdp: + case ResourceType.Cmp: + case ResourceType.Est: + PluginLog.Verbose( "Forced resource {gamePath} to be loaded synchronously.", gamePath ); + return CallOriginalHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + } + // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); PathResolved?.Invoke( gamePath, *resourceType, resolvedPath, data ); if( resolvedPath == null ) { - var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + var retUnmodified = + CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); return retUnmodified; } @@ -247,13 +261,15 @@ public unsafe partial class ResourceLoader private int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) { if( pGetResParams == null || !pGetResParams->IsPartialRead ) + { return path.Crc32; + } // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 return Utf8String.Join( - (byte)'.', + ( byte )'.', path, Utf8String.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 993b5edc..696ab08b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,75 +22,16 @@ using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; +using CharacterUtility = Penumbra.Interop.CharacterUtility; +using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; namespace Penumbra; -public class MainClass : IDalamudPlugin +public class Penumbra : IDalamudPlugin { - private Penumbra? _penumbra; - private readonly CharacterUtility _characterUtility; - public static bool DevPenumbraExists; - public static bool IsNotInstalledPenumbra; - - public MainClass( DalamudPluginInterface pluginInterface ) - { - Dalamud.Initialize( pluginInterface ); - DevPenumbraExists = CheckDevPluginPenumbra(); - IsNotInstalledPenumbra = CheckIsNotInstalled(); - GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); - _characterUtility = new CharacterUtility(); - _characterUtility.LoadingFinished += () - => _penumbra = new Penumbra( _characterUtility ); - } - - public void Dispose() - { - _penumbra?.Dispose(); - _characterUtility.Dispose(); - } - public string Name - => Penumbra.Name; + => "Penumbra"; - // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. - private static bool CheckDevPluginPenumbra() - { -#if !DEBUG - var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); - - try - { - return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); - return true; - } -#else - return false; -#endif - } - - // Check if the loaded version of penumbra itself is in devPlugins. - private static bool CheckIsNotInstalled() - { -#if !DEBUG - var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; - if (!ret) - PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); - return !ret; -#else - return false; -#endif - } -} - -public class Penumbra : IDisposable -{ - public const string Name = "Penumbra"; private const string CommandName = "/penumbra"; public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; @@ -98,6 +39,9 @@ public class Penumbra : IDisposable public static readonly string CommitHash = Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; + public static bool DevPenumbraExists; + public static bool IsNotInstalledPenumbra; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; @@ -122,9 +66,12 @@ public class Penumbra : IDisposable internal WebServer? WebServer; - public Penumbra( CharacterUtility characterUtility ) + public Penumbra( DalamudPluginInterface pluginInterface ) { - CharacterUtility = characterUtility; + Dalamud.Initialize( pluginInterface ); + GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + DevPenumbraExists = CheckDevPluginPenumbra(); + IsNotInstalledPenumbra = CheckIsNotInstalled(); Framework = new FrameworkManager(); Backup.CreateBackup( PenumbraBackupFiles() ); @@ -134,8 +81,10 @@ public class Penumbra : IDisposable TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); - ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod.Manager( Config.ModDirectory ); + ResourceLoader.EnableHooks(); + ResourceLogger = new ResourceLogger( ResourceLoader ); + CharacterUtility = new CharacterUtility(); + ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); ModFileSystem = ModFileSystem.Load(); @@ -151,18 +100,17 @@ public class Penumbra : IDisposable SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); - if( Config.EnableHttpApi ) - { - CreateWebServer(); - } - - ResourceLoader.EnableHooks(); if( Config.EnableMods ) { ResourceLoader.EnableReplacements(); PathResolver.Enable(); } + if( Config.EnableHttpApi ) + { + CreateWebServer(); + } + if( Config.DebugMode ) { ResourceLoader.EnableDebug(); @@ -508,8 +456,11 @@ public class Penumbra : IDisposable { var collection = CollectionManager.ByType( type ); if( collection != null ) + { sb.AppendFormat( "> **`{0,-29}`** {1}... ({2})\n", type.ToName(), CollectionName( collection ), collection.Index ); + } } + foreach( var (name, collection) in CollectionManager.Characters ) { sb.AppendFormat( "> **`{2,-29}`** {0}... ({1})\n", CollectionName( collection ), @@ -523,4 +474,39 @@ public class Penumbra : IDisposable return sb.ToString(); } + + // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. + private static bool CheckDevPluginPenumbra() + { +#if !DEBUG + var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); + var dir = new DirectoryInfo( path ); + + try + { + return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + return true; + } +#else + return false; +#endif + } + + // Check if the loaded version of penumbra itself is in devPlugins. + private static bool CheckIsNotInstalled() + { +#if !DEBUG + var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; + if (!ret) + PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + return !ret; +#else + return false; +#endif + } } \ No newline at end of file diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 0e05326e..d17fe004 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -9,5 +9,7 @@ "Tags": [ "modding" ], "DalamudApiLevel": 6, "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index edaaddcf..8025cf3c 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -60,7 +60,7 @@ public sealed partial class ConfigWindow : Window, IDisposable + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "Please use the Launcher's Repair Game Files function to repair your client installation." ); } - else if( MainClass.IsNotInstalledPenumbra ) + else if( Penumbra.IsNotInstalledPenumbra ) { DrawProblemWindow( $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" @@ -68,7 +68,7 @@ public sealed partial class ConfigWindow : Window, IDisposable + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." ); } - else if( MainClass.DevPenumbraExists ) + else if( Penumbra.DevPenumbraExists ) { DrawProblemWindow( $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " From 60229f8b45cc57411ad73b934ba8dcfc7ada69f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 Jun 2022 12:28:51 +0200 Subject: [PATCH 0322/2451] Rename EQP bit _2. --- Penumbra.GameData/Structs/EqpEntry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 56033d02..ab5a6851 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -11,7 +11,7 @@ public enum EqpEntry : ulong { BodyEnabled = 0x00_01ul, BodyHideWaist = 0x00_02ul, - _2 = 0x00_04ul, + BodyHideThighs = 0x00_04ul, BodyHideGlovesS = 0x00_08ul, _4 = 0x00_10ul, BodyHideGlovesM = 0x00_20ul, @@ -138,7 +138,7 @@ public static class Eqp { EqpEntry.BodyEnabled => EquipSlot.Body, EqpEntry.BodyHideWaist => EquipSlot.Body, - EqpEntry._2 => EquipSlot.Body, + EqpEntry.BodyHideThighs => EquipSlot.Body, EqpEntry.BodyHideGlovesS => EquipSlot.Body, EqpEntry._4 => EquipSlot.Body, EqpEntry.BodyHideGlovesM => EquipSlot.Body, @@ -217,7 +217,7 @@ public static class Eqp { EqpEntry.BodyEnabled => "Enabled", EqpEntry.BodyHideWaist => "Hide Waist", - EqpEntry._2 => "Unknown 2", + EqpEntry.BodyHideThighs => "Hide Thigh Pads", EqpEntry.BodyHideGlovesS => "Hide Small Gloves", EqpEntry._4 => "Unknown 4", EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", From f13893cf7745c3372b986bc0237e27fcc7dbd021 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 Jun 2022 13:27:13 +0200 Subject: [PATCH 0323/2451] Rename EQDP checkmarks. 50/50 chance. --- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index d3a44af2..e9a75e60 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -244,9 +244,9 @@ public partial class ModEditWindow // Values ImGui.TableNextColumn(); var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); - Checkmark( "##eqdpCheck1", string.Empty, bit1, bit1, out _ ); + Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ ); ImGui.SameLine(); - Checkmark( "##eqdpCheck2", string.Empty, bit2, bit2, out _ ); + Checkmark( "Model##eqdpCheck2", string.Empty, bit2, bit2, out _ ); } public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize ) @@ -276,13 +276,13 @@ public partial class ModEditWindow var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot ); var (bit1, bit2) = meta.Entry.ToBits( meta.Slot ); ImGui.TableNextColumn(); - if( Checkmark( "##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) + if( Checkmark( "Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) { editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) } ); } ImGui.SameLine(); - if( Checkmark( "##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) + if( Checkmark( "Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) { editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) } ); } From 9ae843731daed1b87f8bd09e1c733fee9a88b8b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Jun 2022 11:42:55 +0200 Subject: [PATCH 0324/2451] Make penumbra initialization before game code has run possible. --- OtterGui | 2 +- Penumbra/Api/PenumbraApi.cs | 2 +- .../Collections/CollectionManager.Active.cs | 8 +- .../Collections/ModCollection.Cache.Access.cs | 25 +- Penumbra/Collections/ModCollection.Cache.cs | 8 +- Penumbra/Interop/CharacterUtility.cs | 14 +- Penumbra/Interop/ResidentResourceManager.cs | 9 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 90 ++--- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 139 ++++--- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 91 ++--- Penumbra/Meta/Manager/MetaManager.Est.cs | 143 ++++---- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 86 ++--- Penumbra/Meta/Manager/MetaManager.Imc.cs | 338 ++++++++---------- Penumbra/Meta/Manager/MetaManager.cs | 232 ++++++------ .../Meta/Manipulations/MetaManipulation.cs | 13 +- Penumbra/Penumbra.cs | 37 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 19 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 2 +- Penumbra/UI/LaunchButton.cs | 24 +- 19 files changed, 610 insertions(+), 672 deletions(-) diff --git a/OtterGui b/OtterGui index 8053b24b..c5e54db0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8053b24bb6b77a13853455c08a89df4894b3f2ee +Subproject commit c5e54db0ae54b2eab943be9920e2888b0b4b1d13 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index f4403e50..f84cbc58 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -456,7 +456,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) ? c : Penumbra.CollectionManager.Character( characterName ); - var set = collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 5e950228..ceca52be 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -110,8 +110,12 @@ public partial class ModCollection { case CollectionType.Default: Default = newCollection; - Penumbra.ResidentResources.Reload(); - Default.SetFiles(); + if( Penumbra.CharacterUtility.Ready ) + { + Penumbra.ResidentResources.Reload(); + Default.SetFiles(); + } + break; case CollectionType.Current: Current = newCollection; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index a6fa0ffe..b5ea41f1 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -116,68 +116,63 @@ public partial class ModCollection } // Set Metadata files. - [Conditional( "USE_EQP" )] public void SetEqpFiles() { if( _cache == null ) { - MetaManager.MetaManagerEqp.ResetFiles(); + MetaManager.ResetEqpFiles(); } else { - _cache.MetaManipulations.Eqp.SetFiles(); + _cache.MetaManipulations.SetEqpFiles(); } } - [Conditional( "USE_EQDP" )] public void SetEqdpFiles() { if( _cache == null ) { - MetaManager.MetaManagerEqdp.ResetFiles(); + MetaManager.ResetEqdpFiles(); } else { - _cache.MetaManipulations.Eqdp.SetFiles(); + _cache.MetaManipulations.SetEqdpFiles(); } } - [Conditional( "USE_GMP" )] public void SetGmpFiles() { if( _cache == null ) { - MetaManager.MetaManagerGmp.ResetFiles(); + MetaManager.ResetGmpFiles(); } else { - _cache.MetaManipulations.Gmp.SetFiles(); + _cache.MetaManipulations.SetGmpFiles(); } } - [Conditional( "USE_EST" )] public void SetEstFiles() { if( _cache == null ) { - MetaManager.MetaManagerEst.ResetFiles(); + MetaManager.ResetEstFiles(); } else { - _cache.MetaManipulations.Est.SetFiles(); + _cache.MetaManipulations.SetEstFiles(); } } - [Conditional( "USE_CMP" )] public void SetCmpFiles() { if( _cache == null ) { - MetaManager.MetaManagerCmp.ResetFiles(); + MetaManager.ResetCmpFiles(); } else { - _cache.MetaManipulations.Cmp.SetFiles(); + _cache.MetaManipulations.SetCmpFiles(); } } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index e8f72e32..34ef8924 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -175,7 +175,7 @@ public partial class ModCollection ++ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -237,7 +237,7 @@ public partial class ModCollection if( addMetaChanges ) { ++ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -295,7 +295,7 @@ public partial class ModCollection AddMetaFiles(); } - if( _collection == Penumbra.CollectionManager.Default ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -441,7 +441,7 @@ public partial class ModCollection // Add all necessary meta file redirects. private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); + => MetaManipulations.SetImcFiles(); // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 0db41cd7..d96f948e 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -11,7 +11,6 @@ public unsafe class CharacterUtility : IDisposable [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; - // Only required for migration anymore. public delegate void LoadResources( Structs.CharacterUtility* address ); @@ -62,12 +61,12 @@ public unsafe class CharacterUtility : IDisposable public CharacterUtility() { SignatureHelper.Initialise( this ); - LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); - LoadDefaultResources( true ); + Dalamud.Framework.Update += LoadDefaultResources; + LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); } // We store the default data of the resources so we can always restore them. - private void LoadDefaultResources( bool repeat ) + private void LoadDefaultResources( object _ ) { var missingCount = 0; if( Address == null ) @@ -96,12 +95,7 @@ public unsafe class CharacterUtility : IDisposable { Ready = true; LoadingFinished.Invoke(); - } - else if( repeat ) - { - PluginLog.Debug( "Custom load of character resources triggered." ); - LoadCharacterResources(); - LoadDefaultResources( false ); + Dalamud.Framework.Update -= LoadDefaultResources; } } diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs index 8e75dcde..f533a7ba 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -29,8 +29,11 @@ public unsafe class ResidentResourceManager // Reload certain player resources by force. public void Reload() { - PluginLog.Debug( "Reload of resident resources triggered." ); - UnloadPlayerResources.Invoke( Address ); - LoadPlayerResources.Invoke( Address ); + if( Address != null && Address->NumResources > 0 ) + { + PluginLog.Debug( "Reload of resident resources triggered." ); + UnloadPlayerResources.Invoke( Address ); + LoadPlayerResources.Invoke( Address ); + } } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index d9371c41..2a916c5b 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -1,73 +1,57 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using OtterGui.Filesystem; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; public partial class MetaManager { - public struct MetaManagerCmp : IDisposable + private CmpFile? _cmpFile = null; + private readonly List< RspManipulation > _cmpManipulations = new(); + + public void SetCmpFiles() + => SetFile( _cmpFile, CharacterUtility.HumanCmpIdx ); + + public static void ResetCmpFiles() + => SetFile( null, CharacterUtility.HumanCmpIdx ); + + public void ResetCmp() { - public CmpFile? File = null; - public readonly Dictionary< RspManipulation, IMod > Manipulations = new(); - - public MetaManagerCmp() - { } - - [Conditional( "USE_CMP" )] - public void SetFiles() - => SetFile( File, CharacterUtility.HumanCmpIdx ); - - [Conditional( "USE_CMP" )] - public static void ResetFiles() - => SetFile( null, CharacterUtility.HumanCmpIdx ); - - [Conditional( "USE_CMP" )] - public void Reset() + if( _cmpFile == null ) { - if( File == null ) - { - return; - } - - File.Reset( Manipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) ); - Manipulations.Clear(); + return; } - public bool ApplyMod( RspManipulation m, IMod mod ) + _cmpFile.Reset( _cmpManipulations.Select( m => ( m.SubRace, m.Attribute ) ) ); + _cmpManipulations.Clear(); + } + + public bool ApplyMod( RspManipulation manip ) + { + _cmpManipulations.AddOrReplace( manip ); + _cmpFile ??= new CmpFile(); + return manip.Apply( _cmpFile ); + } + + public bool RevertMod( RspManipulation manip ) + { + if( _cmpManipulations.Remove( manip ) ) { -#if USE_CMP - Manipulations[ m ] = mod; - File ??= new CmpFile(); - return m.Apply( File ); -#else - return false; -#endif + var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute ); + manip = new RspManipulation( manip.SubRace, manip.Attribute, def ); + return manip.Apply( _cmpFile! ); } - public bool RevertMod( RspManipulation m ) - { -#if USE_CMP - if( Manipulations.Remove( m ) ) - { - var def = CmpFile.GetDefault( m.SubRace, m.Attribute ); - var manip = new RspManipulation( m.SubRace, m.Attribute, def ); - return manip.Apply( File! ); - } -#endif - return false; - } + return false; + } - public void Dispose() - { - File?.Dispose(); - File = null; - Manipulations.Clear(); - } + public void DisposeCmp() + { + _cmpFile?.Dispose(); + _cmpFile = null; + _cmpManipulations.Clear(); } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 92a60470..e89bd67e 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -1,94 +1,79 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Meta.Manager; public partial class MetaManager { - public struct MetaManagerEqdp : IDisposable + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar + + private readonly List< EqdpManipulation > _eqdpManipulations = new(); + + public void SetEqdpFiles() { - public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar - - public readonly Dictionary< EqdpManipulation, IMod > Manipulations = new(); - - public MetaManagerEqdp() - { } - - [Conditional( "USE_EQDP" )] - public void SetFiles() + for( var i = 0; i < CharacterUtility.EqdpIndices.Length; ++i ) { - for( var i = 0; i < CharacterUtility.EqdpIndices.Length; ++i ) - { - SetFile( Files[ i ], CharacterUtility.EqdpIndices[ i ] ); - } - } - - [Conditional( "USE_EQDP" )] - public static void ResetFiles() - { - foreach( var idx in CharacterUtility.EqdpIndices ) - { - SetFile( null, idx ); - } - } - - [Conditional( "USE_EQDP" )] - public void Reset() - { - foreach( var file in Files ) - { - file?.Reset( Manipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); - } - - Manipulations.Clear(); - } - - public bool ApplyMod( EqdpManipulation m, IMod mod ) - { -#if USE_EQDP - Manipulations[ m ] = mod; - var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= - new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar - return m.Apply( file ); -#else - return false; -#endif - } - - public bool RevertMod( EqdpManipulation m ) - { -#if USE_EQDP - if( Manipulations.Remove( m ) ) - { - var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory(), m.SetId ); - var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ]!; - var manip = new EqdpManipulation( def, m.Slot, m.Gender, m.Race, m.SetId ); - return manip.Apply( file ); - } -#endif - return false; - } - - public ExpandedEqdpFile? File( GenderRace race, bool accessory ) - => Files[ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar - - public void Dispose() - { - for( var i = 0; i < Files.Length; ++i ) - { - Files[ i ]?.Dispose(); - Files[ i ] = null; - } - - Manipulations.Clear(); + SetFile( _eqdpFiles[ i ], CharacterUtility.EqdpIndices[ i ] ); } } + + public static void ResetEqdpFiles() + { + foreach( var idx in CharacterUtility.EqdpIndices ) + { + SetFile( null, idx ); + } + } + + public void ResetEqdp() + { + foreach( var file in _eqdpFiles ) + { + file?.Reset( _eqdpManipulations.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); + } + + _eqdpManipulations.Clear(); + } + + public bool ApplyMod( EqdpManipulation manip ) + { + _eqdpManipulations.AddOrReplace( manip ); + var file = _eqdpFiles[ Array.IndexOf( CharacterUtility.EqdpIndices, manip.FileIndex() ) ] ??= + new ExpandedEqdpFile( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory() ); // TODO: female Hrothgar + return manip.Apply( file ); + } + + public bool RevertMod( EqdpManipulation manip ) + { + if( _eqdpManipulations.Remove( manip ) ) + { + var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory(), manip.SetId ); + var file = _eqdpFiles[ Array.IndexOf( CharacterUtility.EqdpIndices, manip.FileIndex() ) ]!; + manip = new EqdpManipulation( def, manip.Slot, manip.Gender, manip.Race, manip.SetId ); + return manip.Apply( file ); + } + + return false; + } + + public ExpandedEqdpFile? EqdpFile( GenderRace race, bool accessory ) + => _eqdpFiles + [ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar + + public void DisposeEqdp() + { + for( var i = 0; i < _eqdpFiles.Length; ++i ) + { + _eqdpFiles[ i ]?.Dispose(); + _eqdpFiles[ i ] = null; + } + + _eqdpManipulations.Clear(); + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 831e26d9..4c75615f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -1,73 +1,58 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using OtterGui.Filesystem; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; public partial class MetaManager { - public struct MetaManagerEqp : IDisposable + private ExpandedEqpFile? _eqpFile = null; + private readonly List< EqpManipulation > _eqpManipulations = new(); + + public void SetEqpFiles() + => SetFile( _eqpFile, CharacterUtility.EqpIdx ); + + public static void ResetEqpFiles() + => SetFile( null, CharacterUtility.EqpIdx ); + + public void ResetEqp() { - public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, IMod > Manipulations = new(); - - public MetaManagerEqp() - { } - - [Conditional( "USE_EQP" )] - public void SetFiles() - => SetFile( File, CharacterUtility.EqpIdx ); - - [Conditional( "USE_EQP" )] - public static void ResetFiles() - => SetFile( null, CharacterUtility.EqpIdx ); - - [Conditional( "USE_EQP" )] - public void Reset() + if( _eqpFile == null ) { - if( File == null ) - { - return; - } - - File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) ); - Manipulations.Clear(); + return; } - public bool ApplyMod( EqpManipulation m, IMod mod ) + _eqpFile.Reset( _eqpManipulations.Select( m => ( int )m.SetId ) ); + _eqpManipulations.Clear(); + } + + public bool ApplyMod( EqpManipulation manip ) + { + _eqpManipulations.AddOrReplace( manip ); + _eqpFile ??= new ExpandedEqpFile(); + return manip.Apply( _eqpFile ); + } + + public bool RevertMod( EqpManipulation manip ) + { + var idx = _eqpManipulations.FindIndex( manip.Equals ); + if( idx >= 0 ) { -#if USE_EQP - Manipulations[ m ] = mod; - File ??= new ExpandedEqpFile(); - return m.Apply( File ); -#else - return false; -#endif + var def = ExpandedEqpFile.GetDefault( manip.SetId ); + manip = new EqpManipulation( def, manip.Slot, manip.SetId ); + return manip.Apply( _eqpFile! ); } - public bool RevertMod( EqpManipulation m ) - { -#if USE_EQP - if( Manipulations.Remove( m ) ) - { - var def = ExpandedEqpFile.GetDefault( m.SetId ); - var manip = new EqpManipulation( def, m.Slot, m.SetId ); - return manip.Apply( File! ); - } -#endif - return false; - } + return false; + } - public void Dispose() - { - File?.Dispose(); - File = null; - Manipulations.Clear(); - } + public void DisposeEqp() + { + _eqpFile?.Dispose(); + _eqpFile = null; + _eqpManipulations.Clear(); } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index 7af2609c..702b53b6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -1,106 +1,91 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; public partial class MetaManager { - public struct MetaManagerEst : IDisposable + private EstFile? _estFaceFile = null; + private EstFile? _estHairFile = null; + private EstFile? _estBodyFile = null; + private EstFile? _estHeadFile = null; + + private readonly List< EstManipulation > _estManipulations = new(); + + public void SetEstFiles() { - public EstFile? FaceFile = null; - public EstFile? HairFile = null; - public EstFile? BodyFile = null; - public EstFile? HeadFile = null; + SetFile( _estFaceFile, CharacterUtility.FaceEstIdx ); + SetFile( _estHairFile, CharacterUtility.HairEstIdx ); + SetFile( _estBodyFile, CharacterUtility.BodyEstIdx ); + SetFile( _estHeadFile, CharacterUtility.HeadEstIdx ); + } - public readonly Dictionary< EstManipulation, IMod > Manipulations = new(); + public static void ResetEstFiles() + { + SetFile( null, CharacterUtility.FaceEstIdx ); + SetFile( null, CharacterUtility.HairEstIdx ); + SetFile( null, CharacterUtility.BodyEstIdx ); + SetFile( null, CharacterUtility.HeadEstIdx ); + } - public MetaManagerEst() - { } + public void ResetEst() + { + _estFaceFile?.Reset(); + _estHairFile?.Reset(); + _estBodyFile?.Reset(); + _estHeadFile?.Reset(); + _estManipulations.Clear(); + } - [Conditional( "USE_EST" )] - public void SetFiles() + public bool ApplyMod( EstManipulation m ) + { + _estManipulations.AddOrReplace( m ); + var file = m.Slot switch { - SetFile( FaceFile, CharacterUtility.FaceEstIdx ); - SetFile( HairFile, CharacterUtility.HairEstIdx ); - SetFile( BodyFile, CharacterUtility.BodyEstIdx ); - SetFile( HeadFile, CharacterUtility.HeadEstIdx ); - } + EstManipulation.EstType.Hair => _estHairFile ??= new EstFile( EstManipulation.EstType.Hair ), + EstManipulation.EstType.Face => _estFaceFile ??= new EstFile( EstManipulation.EstType.Face ), + EstManipulation.EstType.Body => _estBodyFile ??= new EstFile( EstManipulation.EstType.Body ), + EstManipulation.EstType.Head => _estHeadFile ??= new EstFile( EstManipulation.EstType.Head ), + _ => throw new ArgumentOutOfRangeException(), + }; + return m.Apply( file ); + } - [Conditional( "USE_EST" )] - public static void ResetFiles() + public bool RevertMod( EstManipulation m ) + { + if( _estManipulations.Remove( m ) ) { - SetFile( null, CharacterUtility.FaceEstIdx ); - SetFile( null, CharacterUtility.HairEstIdx ); - SetFile( null, CharacterUtility.BodyEstIdx ); - SetFile( null, CharacterUtility.HeadEstIdx ); - } - - [Conditional( "USE_EST" )] - public void Reset() - { - FaceFile?.Reset(); - HairFile?.Reset(); - BodyFile?.Reset(); - HeadFile?.Reset(); - Manipulations.Clear(); - } - - public bool ApplyMod( EstManipulation m, IMod mod ) - { -#if USE_EST - Manipulations[ m ] = mod; + var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); + var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); var file = m.Slot switch { - EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), - EstManipulation.EstType.Face => FaceFile ??= new EstFile( EstManipulation.EstType.Face ), - EstManipulation.EstType.Body => BodyFile ??= new EstFile( EstManipulation.EstType.Body ), - EstManipulation.EstType.Head => HeadFile ??= new EstFile( EstManipulation.EstType.Head ), + EstManipulation.EstType.Hair => _estHairFile!, + EstManipulation.EstType.Face => _estFaceFile!, + EstManipulation.EstType.Body => _estBodyFile!, + EstManipulation.EstType.Head => _estHeadFile!, _ => throw new ArgumentOutOfRangeException(), }; - return m.Apply( file ); -#else - return false; -#endif + return manip.Apply( file ); } - public bool RevertMod( EstManipulation m ) - { -#if USE_EST - if( Manipulations.Remove( m ) ) - { - var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); - var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); - var file = m.Slot switch - { - EstManipulation.EstType.Hair => HairFile!, - EstManipulation.EstType.Face => FaceFile!, - EstManipulation.EstType.Body => BodyFile!, - EstManipulation.EstType.Head => HeadFile!, - _ => throw new ArgumentOutOfRangeException(), - }; - return manip.Apply( file ); - } -#endif - return false; - } + return false; + } - public void Dispose() - { - FaceFile?.Dispose(); - HairFile?.Dispose(); - BodyFile?.Dispose(); - HeadFile?.Dispose(); - FaceFile = null; - HairFile = null; - BodyFile = null; - HeadFile = null; - Manipulations.Clear(); - } + public void DisposeEst() + { + _estFaceFile?.Dispose(); + _estHairFile?.Dispose(); + _estBodyFile?.Dispose(); + _estHeadFile?.Dispose(); + _estFaceFile = null; + _estHairFile = null; + _estBodyFile = null; + _estHeadFile = null; + _estManipulations.Clear(); } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 82833017..1aaba52f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using OtterGui.Filesystem; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -11,62 +12,49 @@ namespace Penumbra.Meta.Manager; public partial class MetaManager { - public struct MetaManagerGmp : IDisposable + private ExpandedGmpFile? _gmpFile = null; + private readonly List< GmpManipulation > _gmpManipulations = new(); + + public void SetGmpFiles() + => SetFile( _gmpFile, CharacterUtility.GmpIdx ); + + public static void ResetGmpFiles() + => SetFile( null, CharacterUtility.GmpIdx ); + + public void ResetGmp() { - public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, IMod > Manipulations = new(); - - public MetaManagerGmp() - { } - - - [Conditional( "USE_GMP" )] - public void SetFiles() - => SetFile( File, CharacterUtility.GmpIdx ); - - [Conditional( "USE_GMP" )] - public static void ResetFiles() - => SetFile( null, CharacterUtility.GmpIdx ); - - [Conditional( "USE_GMP" )] - public void Reset() + if( _gmpFile == null ) { - if( File != null ) - { - File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) ); - Manipulations.Clear(); - } + return; } - public bool ApplyMod( GmpManipulation m, IMod mod ) + _gmpFile.Reset( _gmpManipulations.Select( m => ( int )m.SetId ) ); + _gmpManipulations.Clear(); + } + + public bool ApplyMod( GmpManipulation manip ) + { + _gmpManipulations.AddOrReplace( manip ); + _gmpFile ??= new ExpandedGmpFile(); + return manip.Apply( _gmpFile ); + } + + public bool RevertMod( GmpManipulation manip ) + { + if( _gmpManipulations.Remove( manip ) ) { -#if USE_GMP - Manipulations[ m ] = mod; - File ??= new ExpandedGmpFile(); - return m.Apply( File ); -#else - return false; -#endif + var def = ExpandedGmpFile.GetDefault( manip.SetId ); + manip = new GmpManipulation( def, manip.SetId ); + return manip.Apply( _gmpFile! ); } - public bool RevertMod( GmpManipulation m ) - { -#if USE_GMP - if( Manipulations.Remove( m ) ) - { - var def = ExpandedGmpFile.GetDefault( m.SetId ); - var manip = new GmpManipulation( def, m.SetId ); - return manip.Apply( File! ); - } -#endif - return false; - } + return false; + } - public void Dispose() - { - File?.Dispose(); - File = null; - Manipulations.Clear(); - } + public void DisposeGmp() + { + _gmpFile?.Dispose(); + _gmpFile = null; + _gmpManipulations.Clear(); } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 35e7434f..1621090d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,215 +1,195 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; public partial class MetaManager { - public readonly struct MetaManagerImc : IDisposable + private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); + private readonly List< ImcManipulation > _imcManipulations = new(); + private static int _imcManagerCount; + + public void SetImcFiles() { - public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, IMod > Manipulations = new(); - - private readonly ModCollection _collection; - private static int _imcManagerCount; - - public MetaManagerImc( ModCollection collection ) + if( !_collection.HasCache ) { - _collection = collection; - SetupDelegate(); + return; } - [Conditional( "USE_IMC" )] - public void SetFiles() + foreach( var path in _imcFiles.Keys ) { - if( !_collection.HasCache ) - { - return; - } + _collection.ForceFile( path, CreateImcPath( path ) ); + } + } - foreach( var path in Files.Keys ) + public void ResetImc() + { + if( _collection.HasCache ) + { + foreach( var (path, file) in _imcFiles ) { - _collection.ForceFile( path, CreateImcPath( path ) ); + _collection.RemoveFile( path ); + file.Reset(); + } + } + else + { + foreach( var (_, file) in _imcFiles ) + { + file.Reset(); } } - [Conditional( "USE_IMC" )] - public void Reset() + _imcManipulations.Clear(); + } + + public bool ApplyMod( ImcManipulation manip ) + { + _imcManipulations.AddOrReplace( manip ); + var path = manip.GamePath(); + try { + if( !_imcFiles.TryGetValue( path, out var file ) ) + { + file = new ImcFile( path ); + } + + if( !manip.Apply( file ) ) + { + return false; + } + + _imcFiles[ path ] = file; + var fullPath = CreateImcPath( path ); if( _collection.HasCache ) { - foreach( var (path, file) in Files ) - { - _collection.RemoveFile( path ); - file.Reset(); - } - } - else - { - foreach( var (_, file) in Files ) - { - file.Reset(); - } - } - - Manipulations.Clear(); - } - - public bool ApplyMod( ImcManipulation m, IMod mod ) - { -#if USE_IMC - Manipulations[ m ] = mod; - var path = m.GamePath(); - try - { - if( !Files.TryGetValue( path, out var file ) ) - { - file = new ImcFile( path ); - } - - if( !m.Apply( file ) ) - { - return false; - } - - Files[ path ] = file; - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } - - return true; - } - catch( Exception e ) - { - ++Penumbra.ImcExceptions; - PluginLog.Error( $"Could not apply IMC Manipulation:\n{e}" ); - return false; - } -#else - return false; -#endif - } - - public bool RevertMod( ImcManipulation m ) - { -#if USE_IMC - if( Manipulations.Remove( m ) ) - { - var path = m.GamePath(); - if( !Files.TryGetValue( path, out var file ) ) - { - return false; - } - - var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); - var manip = m with { Entry = def }; - if( !manip.Apply( file ) ) - { - return false; - } - - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } - - return true; - } -#endif - return false; - } - - public void Dispose() - { - foreach( var file in Files.Values ) - { - file.Dispose(); - } - - Files.Clear(); - Manipulations.Clear(); - RestoreDelegate(); - } - - [Conditional( "USE_IMC" )] - private static unsafe void SetupDelegate() - { - if( _imcManagerCount++ == 0 ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; - Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; - } - } - - [Conditional( "USE_IMC" )] - private static unsafe void RestoreDelegate() - { - if( --_imcManagerCount == 0 ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; - Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; - } - } - - private FullPath CreateImcPath( Utf8GamePath path ) - => new($"|{_collection.Name}_{_collection.RecomputeCounter}|{path}"); - - private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) - { - ret = 0; - if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc ) - { - return false; - } - - PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); - ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - - var lastUnderscore = split.LastIndexOf( ( byte )'_' ); - var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.CollectionManager.ByName( name, out var collection ) - && collection.HasCache - && collection.MetaCache!.Imc.Files.TryGetValue( - Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) - { - PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, - collection.Name ); - file.Replace( fileDescriptor->ResourceHandle, true ); - file.ChangesSinceLoad = false; + _collection.ForceFile( path, fullPath ); } return true; } - - private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) + catch( Exception e ) { - // Only check imcs. - if( resource->FileType != ResourceType.Imc - || resolveData is not ModCollection { HasCache: true } collection - || !collection.MetaCache!.Imc.Files.TryGetValue( gamePath, out var file ) - || !file.ChangesSinceLoad ) - { - return; - } - - PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, - collection.Name ); - file.Replace( resource, false ); - file.ChangesSinceLoad = false; + ++Penumbra.ImcExceptions; + PluginLog.Error( $"Could not apply IMC Manipulation:\n{e}" ); + return false; } } + + public bool RevertMod( ImcManipulation m ) + { +#if USE_IMC + if( _imcManipulations.Remove( m ) ) + { + var path = m.GamePath(); + if( !_imcFiles.TryGetValue( path, out var file ) ) + { + return false; + } + + var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); + var manip = m with { Entry = def }; + if( !manip.Apply( file ) ) + { + return false; + } + + var fullPath = CreateImcPath( path ); + if( _collection.HasCache ) + { + _collection.ForceFile( path, fullPath ); + } + + return true; + } +#endif + return false; + } + + public void DisposeImc() + { + foreach( var file in _imcFiles.Values ) + { + file.Dispose(); + } + + _imcFiles.Clear(); + _imcManipulations.Clear(); + RestoreImcDelegate(); + } + + private static unsafe void SetupImcDelegate() + { + if( _imcManagerCount++ == 0 ) + { + Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; + } + } + + private static unsafe void RestoreImcDelegate() + { + if( --_imcManagerCount == 0 ) + { + Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; + } + } + + private FullPath CreateImcPath( Utf8GamePath path ) + => new($"|{_collection.Name}_{_collection.RecomputeCounter}|{path}"); + + + private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) + { + ret = 0; + if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc ) + { + return false; + } + + PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); + ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + + var lastUnderscore = split.LastIndexOf( ( byte )'_' ); + var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); + if( Penumbra.CollectionManager.ByName( name, out var collection ) + && collection.HasCache + && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) + { + PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, + collection.Name ); + file.Replace( fileDescriptor->ResourceHandle, true ); + file.ChangesSinceLoad = false; + } + + return true; + } + + private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) + { + // Only check imcs. + if( resource->FileType != ResourceType.Imc + || resolveData is not ModCollection { HasCache: true } collection + || !collection.MetaCache!._imcFiles.TryGetValue( gamePath, out var file ) + || !file.ChangesSinceLoad ) + { + return; + } + + PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, + collection.Name ); + file.Replace( resource, false ); + file.ChangesSinceLoad = false; + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 50404234..699273dd 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,6 +1,7 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using Penumbra.Collections; using Penumbra.Meta.Files; @@ -9,17 +10,135 @@ using Penumbra.Mods; namespace Penumbra.Meta.Manager; -public partial class MetaManager : IDisposable +public partial class MetaManager : IDisposable, IEnumerable> { - public MetaManagerEqp Eqp = new(); - public MetaManagerEqdp Eqdp = new(); - public MetaManagerGmp Gmp = new(); - public MetaManagerEst Est = new(); - public MetaManagerCmp Cmp = new(); - public MetaManagerImc Imc; + private readonly Dictionary< MetaManipulation, IMod > _manipulations = new(); + private readonly ModCollection _collection; + + public bool TryGetValue( MetaManipulation manip, [NotNullWhen( true )] out IMod? mod ) + => _manipulations.TryGetValue( manip, out mod ); + + public int Count + => _manipulations.Count; + + public IReadOnlyCollection< MetaManipulation > Manipulations + => _manipulations.Keys; + + + public IEnumerator> GetEnumerator() + => _manipulations.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public MetaManager( ModCollection collection ) + { + _collection = collection; + SetupImcDelegate(); + Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; + } + + public void SetFiles() + { + SetEqpFiles(); + SetEqdpFiles(); + SetGmpFiles(); + SetEstFiles(); + SetCmpFiles(); + SetImcFiles(); + } + + public void Reset() + { + ResetEqp(); + ResetEqdp(); + ResetGmp(); + ResetEst(); + ResetCmp(); + ResetImc(); + } + + public void Dispose() + { + Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; + DisposeEqp(); + DisposeEqdp(); + DisposeCmp(); + DisposeGmp(); + DisposeEst(); + DisposeImc(); + } + + public bool ApplyMod( MetaManipulation manip, IMod mod ) + { + _manipulations[ manip ] = mod; + if( !Penumbra.CharacterUtility.Ready ) + { + return true; + } + + return manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), + MetaManipulation.Type.Gmp => ApplyMod( manip.Gmp ), + MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), + MetaManipulation.Type.Est => ApplyMod( manip.Est ), + MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } + + public bool RevertMod( MetaManipulation manip ) + { + var ret = _manipulations.Remove( manip ); + if( !Penumbra.CharacterUtility.Ready ) + { + return ret; + } + + return manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => RevertMod( manip.Eqp ), + MetaManipulation.Type.Gmp => RevertMod( manip.Gmp ), + MetaManipulation.Type.Eqdp => RevertMod( manip.Eqdp ), + MetaManipulation.Type.Est => RevertMod( manip.Est ), + MetaManipulation.Type.Rsp => RevertMod( manip.Rsp ), + MetaManipulation.Type.Imc => RevertMod( manip.Imc ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } + + // Use this when CharacterUtility becomes ready. + private void ApplyStoredManipulations() + { + if( !Penumbra.CharacterUtility.Ready ) + { + return; + } + + foreach( var manip in Manipulations ) + { + var _ = manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), + MetaManipulation.Type.Gmp => ApplyMod( manip.Gmp ), + MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), + MetaManipulation.Type.Est => ApplyMod( manip.Est ), + MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } + + Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; + } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - public static unsafe void SetFile( MetaBaseFile? file, int index ) + private static unsafe void SetFile( MetaBaseFile? file, int index ) { if( file == null ) { @@ -30,99 +149,4 @@ public partial class MetaManager : IDisposable Penumbra.CharacterUtility.SetResource( index, ( IntPtr )file.Data, file.Length ); } } - - public bool TryGetValue( MetaManipulation manip, [NotNullWhen( true )] out IMod? mod ) - { - mod = manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, - MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, - MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, - MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, - MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, - MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, - _ => throw new ArgumentOutOfRangeException(), - }; - return mod != null; - } - - public int Count - => Imc.Manipulations.Count - + Eqdp.Manipulations.Count - + Cmp.Manipulations.Count - + Gmp.Manipulations.Count - + Est.Manipulations.Count - + Eqp.Manipulations.Count; - - public MetaManipulation[] Manipulations - => Imc.Manipulations.Keys.Select( m => ( MetaManipulation )m ) - .Concat( Eqdp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) - .Concat( Cmp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) - .Concat( Gmp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) - .Concat( Est.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) - .Concat( Eqp.Manipulations.Keys.Select( m => ( MetaManipulation )m ) ) - .ToArray(); - - public MetaManager( ModCollection collection ) - => Imc = new MetaManagerImc( collection ); - - public void SetFiles() - { - Eqp.SetFiles(); - Eqdp.SetFiles(); - Gmp.SetFiles(); - Est.SetFiles(); - Cmp.SetFiles(); - Imc.SetFiles(); - } - - public void Reset() - { - Eqp.Reset(); - Eqdp.Reset(); - Gmp.Reset(); - Est.Reset(); - Cmp.Reset(); - Imc.Reset(); - } - - public void Dispose() - { - Eqp.Dispose(); - Eqdp.Dispose(); - Gmp.Dispose(); - Est.Dispose(); - Cmp.Dispose(); - Imc.Dispose(); - } - - public bool ApplyMod( MetaManipulation m, IMod mod ) - { - return m.ManipulationType switch - { - MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), - MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), - MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), - MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), - MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), - MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } - - public bool RevertMod( MetaManipulation m ) - { - return m.ManipulationType switch - { - MetaManipulation.Type.Eqp => Eqp.RevertMod( m.Eqp ), - MetaManipulation.Type.Gmp => Gmp.RevertMod( m.Gmp ), - MetaManipulation.Type.Eqdp => Eqdp.RevertMod( m.Eqdp ), - MetaManipulation.Type.Est => Est.RevertMod( m.Est ), - MetaManipulation.Type.Rsp => Cmp.RevertMod( m.Rsp ), - MetaManipulation.Type.Imc => Imc.RevertMod( m.Imc ), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 5732522d..bd0c132c 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -220,4 +219,16 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa return Functions.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); } } + + public override string ToString() + => ManipulationType switch + { + Type.Eqp => Eqp.ToString(), + Type.Gmp => Gmp.ToString(), + Type.Eqdp => Eqdp.ToString(), + Type.Est => Est.ToString(), + Type.Rsp => Rsp.ToString(), + Type.Imc => Imc.ToString(), + _ => throw new ArgumentOutOfRangeException(), + }; } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 696ab08b..f61805f8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,14 +77,14 @@ public class Penumbra : IDalamudPlugin Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); - ResidentResources = new ResidentResourceManager(); TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLoader.EnableHooks(); - ResourceLogger = new ResourceLogger( ResourceLoader ); - CharacterUtility = new CharacterUtility(); - ModManager = new Mod.Manager( Config.ModDirectory ); + ResourceLogger = new ResourceLogger( ResourceLoader ); + CharacterUtility = new CharacterUtility(); + ResidentResources = new ResidentResourceManager(); + ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); ModFileSystem = ModFileSystem.Load(); @@ -96,8 +96,6 @@ public class Penumbra : IDalamudPlugin HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - ResidentResources.Reload(); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); if( Config.EnableMods ) @@ -122,7 +120,10 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - ResidentResources.Reload(); + if( CharacterUtility.Ready ) + { + ResidentResources.Reload(); + } Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); @@ -165,12 +166,15 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = true; ResourceLoader.EnableReplacements(); - CollectionManager.Default.SetFiles(); - ResidentResources.Reload(); PathResolver.Enable(); - Config.Save(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + if( CharacterUtility.Ready ) + { + CollectionManager.Default.SetFiles(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + return true; } @@ -183,12 +187,15 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = false; ResourceLoader.DisableReplacements(); - CharacterUtility.ResetAll(); - ResidentResources.Reload(); PathResolver.Disable(); - Config.Save(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + if( CharacterUtility.Ready ) + { + CharacterUtility.ResetAll(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + return true; } diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index b9a2213f..c45668ca 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; namespace Penumbra.UI; @@ -110,22 +111,10 @@ public partial class ConfigWindow // If no meta manipulations are active, we can just draw the end dummy. if( m is { Count: > 0 } ) { - // We can treat all meta manipulations the same, - // we are only really interested in their ToString function here. - static (object, IMod) Convert< T >( KeyValuePair< T, IMod > kvp ) - => ( kvp.Key!, kvp.Value ); - - var it = m.Cmp.Manipulations.Select( Convert ) - .Concat( m.Eqp.Manipulations.Select( Convert ) ) - .Concat( m.Eqdp.Manipulations.Select( Convert ) ) - .Concat( m.Gmp.Manipulations.Select( Convert ) ) - .Concat( m.Est.Manipulations.Select( Convert ) ) - .Concat( m.Imc.Manipulations.Select( Convert ) ); - // Filters mean we can not use the known counts. if( hasFilters ) { - var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, p.Item2.Name ) ); + var it2 = m.Select( p => ( p.Key.ToString(), p.Value.Name ) ); if( stop >= 0 ) { ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); @@ -144,7 +133,7 @@ public partial class ConfigWindow } else { - stop = ImGuiClip.ClippedDraw( it, skips, DrawLine, m.Count, ~stop ); + stop = ImGuiClip.ClippedDraw( m, skips, DrawLine, m.Count, ~stop ); ImGuiClip.DrawEndDummy( stop, height ); } } @@ -183,7 +172,7 @@ public partial class ConfigWindow } // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, IMod) pair ) + private static void DrawLine( KeyValuePair< MetaManipulation, IMod > pair ) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 8b52adfb..1166bd58 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -135,7 +135,7 @@ public partial class ConfigWindow private static void DrawReloadResourceButton() { - if( ImGui.Button( "Reload Resident Resources" ) ) + if( ImGui.Button( "Reload Resident Resources" ) && Penumbra.CharacterUtility.Ready ) { Penumbra.ResidentResources.Reload(); } diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 1bcdda4d..84d89e3e 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -9,20 +9,24 @@ namespace Penumbra.UI; // using the Dalamud-provided collapsible submenu. public class LaunchButton : IDisposable { - private readonly ConfigWindow _configWindow; - private readonly TextureWrap? _icon; - private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; + private readonly ConfigWindow _configWindow; + private TextureWrap? _icon; + private TitleScreenMenu.TitleScreenMenuEntry? _entry; public LaunchButton( ConfigWindow ui ) { _configWindow = ui; - - _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, - "tsmLogo.png" ) ); - if( _icon != null ) - { - _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - } + _icon = null; + _entry = null; + //Dalamud.Framework.RunOnTick( () => + //{ + // _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + // "tsmLogo.png" ) ); + // if( _icon != null ) + // { + // _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); + // } + //} ); } private void OnTriggered() From 71a7520e58d0bf7f876a2b387599cd18cb74736c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Jun 2022 15:29:23 +0200 Subject: [PATCH 0325/2451] Lock, not fix the main window --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 32859822..c8bfd663 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -39,7 +39,7 @@ public partial class ConfigWindow DrawEnabledBox(); DrawShowAdvancedBox(); - Checkbox( "Fix Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => + Checkbox( "Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => { Penumbra.Config.FixMainWindow = v; _window.Flags = v From f00fe54bb3d389953766e5f163196bf86dcf044b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 Jun 2022 13:35:27 +0200 Subject: [PATCH 0326/2451] Add logging of destructed resource handles. --- .../Interop/Loader/ResourceLoader.Debug.cs | 18 ++++++++++++++++++ .../Loader/ResourceLoader.Replacement.cs | 1 + Penumbra/Interop/Loader/ResourceLoader.cs | 2 ++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 1 - 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 41ba6c48..ce95d523 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -13,6 +14,23 @@ namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { + public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); + + [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", + DetourName = nameof( ResourceHandleDestructorDetour ) )] + public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; + + private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) + { + if( handle != null ) + { + PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}) .", handle->FileName, + ( ulong )handle, handle->RefCount ); + } + + return ResourceHandleDestructorHook!.Original( handle ); + } + // A static pointer to the SE Resource Manager [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0", ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] public static ResourceManager** ResourceManager; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index ee70d33c..eac70a20 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -256,6 +256,7 @@ public unsafe partial class ResourceLoader ReadSqPackHook.Dispose(); GetResourceSyncHook.Dispose(); GetResourceAsyncHook.Dispose(); + ResourceHandleDestructorHook?.Dispose(); } private int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index ad721b5e..bc575d2e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -30,6 +30,7 @@ public unsafe partial class ResourceLoader : IDisposable ResourceRequested += LogPath; ResourceLoaded += LogResource; FileLoaded += LogLoadedFile; + ResourceHandleDestructorHook?.Enable(); EnableHooks(); } @@ -44,6 +45,7 @@ public unsafe partial class ResourceLoader : IDisposable ResourceRequested -= LogPath; ResourceLoaded -= LogResource; FileLoaded -= LogLoadedFile; + ResourceHandleDestructorHook?.Disable(); } public void EnableReplacements() diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index c8bfd663..d40d3e33 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -77,7 +77,6 @@ public partial class ConfigWindow : 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; } From c66a09dea30715b9dda7a58979a1ef17a73805c1 Mon Sep 17 00:00:00 2001 From: Det Date: Thu, 30 Jun 2022 19:06:54 -0300 Subject: [PATCH 0327/2451] PluginLog at OnFrameworkUpdate When using Fixed Designs with Glamourer, this line goes really wild for being on OnFrameworkUpdate, so I thought it would be nice to have it only when debugging. --- Penumbra.PlayerWatch/PlayerWatchBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index dd777043..5b670484 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -301,7 +301,11 @@ internal class PlayerWatchBase : IDisposable var id = GetId( character ); SeenActors.Add( id ); + +#if DEBUG PluginLog.Verbose( "Comparing Gear for {PlayerName:l} ({Id}) at 0x{Address:X}...", character.Name, id, character.Address.ToInt64() ); +#endif + if( !watch.FoundActors.TryGetValue( id, out var equip ) ) { equip = new CharacterEquipment( character ); From d445df256b9fdd8f6d2f04c37eed4edb3f5889e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 16:18:40 +0200 Subject: [PATCH 0328/2451] Add synchronous load to base_repo. --- base_repo.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/base_repo.json b/base_repo.json index 71b15948..514e4e35 100644 --- a/base_repo.json +++ b/base_repo.json @@ -14,6 +14,8 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", From 1d7829593e7cc59f319f133407bf4edac10cfc7e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 16:20:09 +0200 Subject: [PATCH 0329/2451] Add IncRef mode and debug DecRef check. --- .../Interop/Loader/ResourceLoader.Debug.cs | 22 +++++++++++-- .../Loader/ResourceLoader.Replacement.cs | 33 +++++++++++-------- Penumbra/Interop/Loader/ResourceLoader.cs | 7 ++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index ce95d523..6258dc13 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -14,6 +14,10 @@ namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { + // If in debug mode, this logs any resource at refcount 0 that gets decremented again, and skips the decrement instead. + private delegate byte ResourceHandleDecRef( ResourceHandle* handle ); + private readonly Hook _decRefHook; + public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", @@ -24,8 +28,8 @@ public unsafe partial class ResourceLoader { if( handle != null ) { - PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}) .", handle->FileName, - ( ulong )handle, handle->RefCount ); + PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}) .", + handle->FileName, ( ulong )handle, handle->RefCount ); } return ResourceHandleDestructorHook!.Original( handle ); @@ -54,11 +58,13 @@ public unsafe partial class ResourceLoader public void EnableDebug() { + _decRefHook?.Enable(); ResourceLoaded += AddModifiedDebugInfo; } public void DisableDebug() { + _decRefHook?.Disable(); ResourceLoaded -= AddModifiedDebugInfo; } @@ -207,6 +213,18 @@ public unsafe partial class ResourceLoader } } + // Prevent resource management weirdness. + private byte ResourceHandleDecRefDetour( ResourceHandle* handle ) + { + if( handle->RefCount != 0 ) + { + return _decRefHook!.Original( handle ); + } + + PluginLog.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); + return 1; + } + // Logging functions for EnableFullLogging. private static void LogPath( Utf8GamePath path, bool synchronous ) => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index eac70a20..0c9f6c7b 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -84,18 +84,6 @@ public unsafe partial class ResourceLoader ResourceRequested?.Invoke( gamePath, isSync ); - // Force metadata tables to load synchronously and not be able to be replaced. - switch( *resourceType ) - { - case ResourceType.Eqp: - case ResourceType.Gmp: - case ResourceType.Eqdp: - case ResourceType.Cmp: - case ResourceType.Est: - PluginLog.Verbose( "Forced resource {gamePath} to be loaded synchronously.", gamePath ); - return CallOriginalHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - } - // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); PathResolved?.Invoke( gamePath, *resourceType, resolvedPath, data ); @@ -126,7 +114,7 @@ public unsafe partial class ResourceLoader // Try all resolve path subscribers or use the default replacer. private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { - if( !DoReplacements ) + if( !DoReplacements || IsInIncRef ) { return ( null, null ); } @@ -259,7 +247,7 @@ public unsafe partial class ResourceLoader ResourceHandleDestructorHook?.Dispose(); } - private int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) + private static int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) { if( pGetResParams == null || !pGetResParams->IsPartialRead ) { @@ -276,4 +264,21 @@ public unsafe partial class ResourceLoader Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) ).Crc32; } + + + // A resource with ref count 0 that gets incremented goes through GetResourceAsync again. + // This means, that if the path determined from that is different than the resources path, + // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. + // This causes some problems and is hopefully prevented with this. + public bool IsInIncRef { get; private set; } = false; + private readonly Hook< ResourceHandleDestructor > _incRefHook; + + private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) + { + var tmp = IsInIncRef; + IsInIncRef = true; + var ret = _incRefHook.Original( handle ); + IsInIncRef = tmp; + return ret; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index bc575d2e..f36b90c5 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -1,4 +1,5 @@ using System; +using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; @@ -82,6 +83,7 @@ public unsafe partial class ResourceLoader : IDisposable ReadSqPackHook.Enable(); GetResourceSyncHook.Enable(); GetResourceAsyncHook.Enable(); + _incRefHook.Enable(); } public void DisableHooks() @@ -95,11 +97,16 @@ public unsafe partial class ResourceLoader : IDisposable ReadSqPackHook.Disable(); GetResourceSyncHook.Disable(); GetResourceAsyncHook.Disable(); + _incRefHook.Disable(); } public ResourceLoader( Penumbra _ ) { SignatureHelper.Initialise( this ); + _decRefHook = new Hook< ResourceHandleDecRef >( ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpDecRef, + ResourceHandleDecRefDetour ); + _incRefHook = new Hook< ResourceHandleDestructor >( + ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpIncRef, ResourceHandleIncRefDetour ); } // Event fired whenever a resource is requested. From 53ff9a8ab3fc93e8b9d3697ae11c0b05ba9135cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 16:20:47 +0200 Subject: [PATCH 0330/2451] Fix Self-Actor for GPose. --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 42e201ae..253cb0e0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -342,7 +342,7 @@ public unsafe partial class PathResolver { collection = null; // Check for the Yourself collection. - if( actor->ObjectIndex == 0 || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + if( actor->ObjectIndex is 0 or ObjectReloader.GPosePlayerIdx || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) { collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); if( collection != null ) From 653e21e2379e389c7557944454479c74b695f1c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 16:21:41 +0200 Subject: [PATCH 0331/2451] Add MaterialUI IPC and increase API Version to 6. No breaking changes, only additions. --- Penumbra/Api/IPenumbraApi.cs | 20 ++++- Penumbra/Api/IpcTester.cs | 67 +++++++++++++-- Penumbra/Api/PenumbraApi.cs | 86 ++++++++++++++++++- Penumbra/Api/PenumbraIpc.cs | 86 +++++++++++++++++-- Penumbra/UI/ConfigWindow.DebugTab.cs | 5 -- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 3 + 6 files changed, 242 insertions(+), 25 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 1d81b973..44eb92ee 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; +using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -16,7 +17,7 @@ public interface IPenumbraApiBase public delegate void ChangedItemHover( object? item ); public delegate void ChangedItemClick( MouseButton button, object? item ); public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); - +public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); public enum PenumbraApiEc { Success = 0, @@ -47,6 +48,11 @@ public interface IPenumbraApi : IPenumbraApiBase // Can be used to append tooltips. public event ChangedItemHover? ChangedItemTooltip; + // Events that are fired before and after the content of a mod settings panel are drawn. + // Both are fired inside the child window of the settings panel itself. + public event Action? PreSettingsPanelDraw; + public event Action? PostSettingsPanelDraw; + // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; public event GameObjectRedrawn? GameObjectRedrawn; @@ -101,6 +107,16 @@ 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(); + // Try to reload an existing mod by its directory name or mod name. + // Can return ModMissing or success. + // Reload is the same as if triggered by button press and might delete the mod if it is not valid anymore. + public PenumbraApiEc ReloadMod( string modDirectory, string modName ); + + // Try to add a new mod inside the mod root directory (modDirectory should only be the name, not the full name). + // Returns FileMissing if the directory does not exist or success otherwise. + // Note that success does only imply a successful call, not a successful mod load. + public PenumbraApiEc AddMod( string modDirectory ); + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations // for the given collection associated with the character name, or the default collection. public string GetMetaManipulations( string characterName ); @@ -139,6 +155,8 @@ public interface IPenumbraApi : IPenumbraApiBase public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList< string > options ); + // This event gets fired when any setting in any collection changes. + public event ModSettingChanged? ModSettingChanged; // Create a temporary collection without actual settings but with a cache. // If no character collection for this character exists or forceOverwriteCharacter is true, diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index da087527..fc50f1aa 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -12,6 +13,7 @@ using Dalamud.Plugin.Ipc; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -23,23 +25,32 @@ public class IpcTester : IDisposable private readonly PenumbraIpc _ipc; private readonly DalamudPluginInterface _pi; - private readonly ICallGateSubscriber< object? > _initialized; - private readonly ICallGateSubscriber< object? > _disposed; - private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; + private readonly ICallGateSubscriber< object? > _initialized; + private readonly ICallGateSubscriber< object? > _disposed; + private readonly ICallGateSubscriber< string, object? > _preSettingsDraw; + private readonly ICallGateSubscriber< string, object? > _postSettingsDraw; + private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; + private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); public IpcTester( DalamudPluginInterface pi, PenumbraIpc ipc ) { - _ipc = ipc; - _pi = pi; + _ipc = ipc; + _pi = pi; _initialized = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderInitialized ); - _disposed = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderDisposed ); - _redrawn = _pi.GetIpcSubscriber< IntPtr, int, object? >( PenumbraIpc.LabelProviderGameObjectRedrawn ); + _disposed = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderDisposed ); + _redrawn = _pi.GetIpcSubscriber< IntPtr, int, object? >( PenumbraIpc.LabelProviderGameObjectRedrawn ); + _preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw ); + _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); + _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); _initialized.Subscribe( AddInitialized ); _disposed.Subscribe( AddDisposed ); _redrawn.Subscribe( SetLastRedrawn ); + _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); + _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); + _settingChanged.Subscribe( UpdateLastModSetting ); } public void Dispose() @@ -49,6 +60,9 @@ public class IpcTester : IDisposable _redrawn.Subscribe( SetLastRedrawn ); _tooltip?.Unsubscribe( AddedTooltip ); _click?.Unsubscribe( AddedClick ); + _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); + _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); + _settingChanged.Unsubscribe( UpdateLastModSetting ); } private void AddInitialized() @@ -111,7 +125,12 @@ public class IpcTester : IDisposable ImGui.TableNextColumn(); } - private string _currentConfiguration = string.Empty; + private string _currentConfiguration = string.Empty; + private string _lastDrawnMod = string.Empty; + private DateTimeOffset _lastDrawnModTime; + + private void UpdateLastDrawnMod( string name ) + => ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now ); private void DrawGeneral() { @@ -143,6 +162,8 @@ public class IpcTester : IDisposable DrawList( PenumbraIpc.LabelProviderInitialized, "Last Initialized", _initializedList ); DrawList( PenumbraIpc.LabelProviderDisposed, "Last Disposed", _disposedList ); + DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" ); + ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); DrawIntro( PenumbraIpc.LabelProviderApiVersion, "Current Version" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< int >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc().ToString() ); DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); @@ -500,8 +521,23 @@ public class IpcTester : IDisposable private IDictionary< string, (IList< string >, SelectType) >? _availableSettings; private IDictionary< string, IList< string > >? _currentSettings = null; private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; + private ModSettingChange _lastSettingChangeType; + private string _lastSettingChangeCollection = string.Empty; + private string _lastSettingChangeMod = string.Empty; + private bool _lastSettingChangeInherited; + private DateTimeOffset _lastSettingChange; + private PenumbraApiEc _lastReloadEc = PenumbraApiEc.Success; + private void UpdateLastModSetting( ModSettingChange type, string collection, string mod, bool inherited ) + { + _lastSettingChangeType = type; + _lastSettingChangeCollection = collection; + _lastSettingChangeMod = mod; + _lastSettingChangeInherited = inherited; + _lastSettingChange = DateTimeOffset.Now; + } + private void DrawSetting() { using var _ = ImRaii.TreeNode( "Settings IPC" ); @@ -522,7 +558,10 @@ public class IpcTester : IDisposable } DrawIntro( "Last Error", _lastSettingsError.ToString() ); - + DrawIntro( PenumbraIpc.LabelProviderModSettingChanged, "Last Mod Setting Changed" ); + ImGui.TextUnformatted( _lastSettingChangeMod.Length > 0 + ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{( _lastSettingChangeInherited ? " (Inherited)" : string.Empty )} at {_lastSettingChange}" + : "None" ); DrawIntro( PenumbraIpc.LabelProviderGetAvailableModSettings, "Get Available Settings" ); if( ImGui.Button( "Get##Available" ) ) { @@ -532,6 +571,16 @@ public class IpcTester : IDisposable _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; } + DrawIntro( PenumbraIpc.LabelProviderReloadMod, "Reload Mod" ); + if( ImGui.Button( "Reload" ) ) + { + _lastReloadEc = _pi.GetIpcSubscriber< string, string, PenumbraApiEc >( PenumbraIpc.LabelProviderReloadMod ) + .InvokeFunc( _settingsModDirectory, _settingsModName ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastReloadEc.ToString() ); + DrawIntro( PenumbraIpc.LabelProviderGetCurrentModSettings, "Get Current Settings" ); if( ImGui.Button( "Get##Current" ) ) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index f84cbc58..b81335b9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -21,11 +21,17 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public int ApiVersion - => 5; + => 6; private Penumbra? _penumbra; private Lumina.GameData? _lumina; + + private readonly Dictionary< ModCollection, ModCollection.ModSettingChangeDelegate > _delegates = new(); + + public event Action? PreSettingsPanelDraw; + public event Action? PostSettingsPanelDraw; public event GameObjectRedrawn? GameObjectRedrawn; + public event ModSettingChanged? ModSettingChanged; public bool Valid => _penumbra != null; @@ -37,13 +43,27 @@ public class PenumbraApi : IDisposable, IPenumbraApi .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( Dalamud.GameData ); _penumbra.ObjectReloader.GameObjectRedrawn += OnGameObjectRedrawn; + foreach( var collection in Penumbra.CollectionManager ) + { + SubscribeToCollection( collection ); + } + + Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; } public void Dispose() { - _penumbra!.ObjectReloader.GameObjectRedrawn -= OnGameObjectRedrawn; - _penumbra = null; - _lumina = null; + Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; + _penumbra!.ObjectReloader.GameObjectRedrawn -= OnGameObjectRedrawn; + _penumbra = null; + _lumina = null; + foreach( var collection in Penumbra.CollectionManager ) + { + if( _delegates.TryGetValue( collection, out var del ) ) + { + collection.ModSettingChanged -= del; + } + } } public event ChangedItemClick? ChangedItemClicked; @@ -214,6 +234,29 @@ public class PenumbraApi : IDisposable, IPenumbraApi ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); } + public PenumbraApiEc ReloadMod( string modDirectory, string modName ) + { + CheckInitialized(); + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + Penumbra.ModManager.ReloadMod( mod.Index ); + return PenumbraApiEc.Success; + } + + public PenumbraApiEc AddMod( string modDirectory ) + { + CheckInitialized(); + var dir = new DirectoryInfo( Path.Combine(Penumbra.ModManager.BasePath.FullName, modDirectory) ); + if( !dir.Exists ) + return PenumbraApiEc.FileMissing; + + Penumbra.ModManager.AddMod( dir ); + return PenumbraApiEc.Success; + } + public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) { CheckInitialized(); @@ -572,4 +615,39 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } + + private void SubscribeToCollection( ModCollection c ) + { + var name = c.Name; + + void Del( ModSettingChange type, int idx, int _, int _2, bool inherited ) + => ModSettingChanged?.Invoke( type, name, Penumbra.ModManager[ idx ].ModPath.Name, inherited ); + + _delegates[ c ] = Del; + c.ModSettingChanged += Del; + } + + private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + { + if( type != CollectionType.Inactive ) + { + return; + } + + if( oldCollection != null && _delegates.TryGetValue( oldCollection, out var del ) ) + { + oldCollection.ModSettingChanged -= del; + } + + if( newCollection != null ) + { + SubscribeToCollection( newCollection ); + } + } + + public void InvokePreSettingsPanel(string modDirectory) + => PreSettingsPanelDraw?.Invoke(modDirectory); + + public void InvokePostSettingsPanel(string modDirectory) + => PostSettingsPanelDraw?.Invoke(modDirectory); } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index ceaacfc4..6d2cd98e 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -4,6 +4,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using Penumbra.Collections; using Penumbra.GameData.Enums; namespace Penumbra.Api; @@ -48,12 +49,16 @@ public partial class PenumbraIpc public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; + public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; + public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< string >? ProviderGetConfiguration; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string >? ProviderGetConfiguration; + internal ICallGateProvider? ProviderPreSettingsDraw; + internal ICallGateProvider? ProviderPostSettingsDraw; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { @@ -104,6 +109,26 @@ public partial class PenumbraIpc { PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetConfiguration}:\n{e}" ); } + + try + { + ProviderPreSettingsDraw = pi.GetIpcProvider( LabelProviderPreSettingsDraw ); + Api.PreSettingsPanelDraw += InvokeSettingsPreDraw; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderPreSettingsDraw}:\n{e}" ); + } + + try + { + ProviderPostSettingsDraw = pi.GetIpcProvider( LabelProviderPostSettingsDraw ); + Api.PostSettingsPanelDraw += InvokeSettingsPostDraw; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderPostSettingsDraw}:\n{e}" ); + } } private void DisposeGeneralProviders() @@ -195,13 +220,19 @@ public partial class PenumbraIpc private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) => ProviderGameObjectRedrawn?.SendMessage( objectAddress, objectTableIndex ); + private void InvokeSettingsPreDraw( string modDirectory ) + => ProviderPreSettingsDraw!.SendMessage( modDirectory ); + + private void InvokeSettingsPostDraw( string modDirectory ) + => ProviderPostSettingsDraw!.SendMessage( modDirectory ); + private void DisposeRedrawProviders() { ProviderRedrawName?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); - Api.GameObjectRedrawn -= OnGameObjectRedrawn; + Api.GameObjectRedrawn -= OnGameObjectRedrawn; } } @@ -425,14 +456,21 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderGetAvailableModSettings = "Penumbra.GetAvailableModSettings"; + public const string LabelProviderReloadMod = "Penumbra.ReloadMod"; + public const string LabelProviderAddMod = "Penumbra.AddMod"; public const string LabelProviderGetCurrentModSettings = "Penumbra.GetCurrentModSettings"; public const string LabelProviderTryInheritMod = "Penumbra.TryInheritMod"; public const string LabelProviderTrySetMod = "Penumbra.TrySetMod"; public const string LabelProviderTrySetModPriority = "Penumbra.TrySetModPriority"; public const string LabelProviderTrySetModSetting = "Penumbra.TrySetModSetting"; public const string LabelProviderTrySetModSettings = "Penumbra.TrySetModSettings"; + public const string LabelProviderModSettingChanged = "Penumbra.ModSettingChanged"; + + internal ICallGateProvider< ModSettingChange, string, string, bool, object? >? ProviderModSettingChanged; internal ICallGateProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >? ProviderGetAvailableModSettings; + internal ICallGateProvider< string, string, PenumbraApiEc >? ProviderReloadMod; + internal ICallGateProvider< string, PenumbraApiEc >? ProviderAddMod; internal ICallGateProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >? ProviderGetCurrentModSettings; @@ -445,6 +483,16 @@ public partial class PenumbraIpc private void InitializeSettingProviders( DalamudPluginInterface pi ) { + try + { + ProviderModSettingChanged = pi.GetIpcProvider< ModSettingChange, string, string, bool, object? >( LabelProviderModSettingChanged ); + Api.ModSettingChanged += InvokeModSettingChanged; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderModSettingChanged}:\n{e}" ); + } + try { ProviderGetAvailableModSettings = @@ -457,6 +505,26 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); } + try + { + ProviderReloadMod = pi.GetIpcProvider< string, string, PenumbraApiEc >( LabelProviderReloadMod ); + ProviderReloadMod.RegisterFunc( Api.ReloadMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderReloadMod}:\n{e}" ); + } + + try + { + ProviderAddMod = pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderAddMod ); + ProviderAddMod.RegisterFunc( Api.AddMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } + try { ProviderGetCurrentModSettings = @@ -524,7 +592,10 @@ public partial class PenumbraIpc private void DisposeSettingProviders() { + Api.ModSettingChanged -= InvokeModSettingChanged; ProviderGetAvailableModSettings?.UnregisterFunc(); + ProviderReloadMod?.UnregisterFunc(); + ProviderAddMod?.UnregisterFunc(); ProviderGetCurrentModSettings?.UnregisterFunc(); ProviderTryInheritMod?.UnregisterFunc(); ProviderTrySetMod?.UnregisterFunc(); @@ -532,6 +603,9 @@ public partial class PenumbraIpc ProviderTrySetModSetting?.UnregisterFunc(); ProviderTrySetModSettings?.UnregisterFunc(); } + + private void InvokeModSettingChanged( ModSettingChange type, string collection, string mod, bool inherited ) + => ProviderModSettingChanged?.SendMessage( type, collection, mod, inherited ); } public partial class PenumbraIpc diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index aa58ad32..612fb69e 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,20 +1,15 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using System.Reflection; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.Api; -using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; -using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 20954498..96e25e98 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -49,6 +49,7 @@ public partial class ConfigWindow DrawInheritedWarning(); ImGui.Dummy( _window._defaultSpace ); + _window._penumbra.Api.InvokePreSettingsPanel( _mod.ModPath.Name ); DrawEnabledInput(); ImGui.SameLine(); DrawPriorityInput(); @@ -64,6 +65,8 @@ public partial class ConfigWindow { DrawMultiGroup( _mod.Groups[ idx ], idx ); } + + _window._penumbra.Api.InvokePostSettingsPanel(_mod.ModPath.Name); } From 99eb08958cd800c5c03f126a5b7850ee4bde8ecd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 20:37:33 +0200 Subject: [PATCH 0332/2451] Add some API --- Penumbra/Api/IPenumbraApi.cs | 10 +++- Penumbra/Api/IpcTester.cs | 34 ++++++++--- Penumbra/Api/PenumbraApi.cs | 37 +++++++----- Penumbra/Api/PenumbraIpc.cs | 56 +++++++++++++------ .../Loader/ResourceLoader.Replacement.cs | 10 ++-- Penumbra/Interop/ObjectReloader.cs | 3 +- .../Interop/Resolver/PathResolver.Data.cs | 11 +++- Penumbra/UI/ConfigWindow.DebugTab.cs | 1 + 8 files changed, 112 insertions(+), 50 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 44eb92ee..65b406ca 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -18,6 +18,8 @@ public delegate void ChangedItemHover( object? item ); public delegate void ChangedItemClick( MouseButton button, object? item ); public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); +public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr customize, IntPtr equipData ); + public enum PenumbraApiEc { Success = 0, @@ -50,13 +52,17 @@ public interface IPenumbraApi : IPenumbraApiBase // Events that are fired before and after the content of a mod settings panel are drawn. // Both are fired inside the child window of the settings panel itself. - public event Action? PreSettingsPanelDraw; - public event Action? PostSettingsPanelDraw; + public event Action< string >? PreSettingsPanelDraw; + public event Action< string >? PostSettingsPanelDraw; // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; public event GameObjectRedrawn? GameObjectRedrawn; + // Triggered when a character base is created and a corresponding gameObject could be found, + // before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand. + public event CreatingCharacterBaseDelegate? CreatingCharacterBase; + // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index fc50f1aa..40f22ce3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -31,6 +30,7 @@ public class IpcTester : IDisposable private readonly ICallGateSubscriber< string, object? > _postSettingsDraw; private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; + private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, object? > _characterBaseCreated; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); @@ -45,12 +45,15 @@ public class IpcTester : IDisposable _preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw ); _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); + _characterBaseCreated = + _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); _initialized.Subscribe( AddInitialized ); _disposed.Subscribe( AddDisposed ); _redrawn.Subscribe( SetLastRedrawn ); _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); _settingChanged.Subscribe( UpdateLastModSetting ); + _characterBaseCreated.Subscribe( UpdateLastCreated ); } public void Dispose() @@ -63,6 +66,7 @@ public class IpcTester : IDisposable _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); _settingChanged.Unsubscribe( UpdateLastModSetting ); + _characterBaseCreated.Unsubscribe( UpdateLastCreated ); } private void AddInitialized() @@ -165,7 +169,8 @@ public class IpcTester : IDisposable DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" ); ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); DrawIntro( PenumbraIpc.LabelProviderApiVersion, "Current Version" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< int >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc().ToString() ); + var (breaking, features) = _pi.GetIpcSubscriber< (int, int) >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc(); + ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); @@ -191,11 +196,20 @@ public class IpcTester : IDisposable } } - private string _currentResolvePath = string.Empty; - private string _currentResolveCharacter = string.Empty; - private string _currentDrawObjectString = string.Empty; - private string _currentReversePath = string.Empty; - private IntPtr _currentDrawObject = IntPtr.Zero; + private string _currentResolvePath = string.Empty; + private string _currentResolveCharacter = string.Empty; + private string _currentDrawObjectString = string.Empty; + private string _currentReversePath = string.Empty; + private IntPtr _currentDrawObject = IntPtr.Zero; + private string _lastCreatedGameObjectName = string.Empty; + private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; + + private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3 ) + { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + } private void DrawResolve() { @@ -263,6 +277,12 @@ public class IpcTester : IDisposable } } } + + DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" ); + if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) + { + ImGui.TextUnformatted( $"for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); + } } private string _redrawName = string.Empty; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index b81335b9..b15ff691 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -28,11 +28,23 @@ public class PenumbraApi : IDisposable, IPenumbraApi private readonly Dictionary< ModCollection, ModCollection.ModSettingChangeDelegate > _delegates = new(); - public event Action? PreSettingsPanelDraw; - public event Action? PostSettingsPanelDraw; - public event GameObjectRedrawn? GameObjectRedrawn; + public event Action< string >? PreSettingsPanelDraw; + public event Action< string >? PostSettingsPanelDraw; + + public event GameObjectRedrawn? GameObjectRedrawn + { + add => _penumbra!.ObjectReloader.GameObjectRedrawn += value; + remove => _penumbra!.ObjectReloader.GameObjectRedrawn -= value; + } + public event ModSettingChanged? ModSettingChanged; + public event CreatingCharacterBaseDelegate? CreatingCharacterBase + { + add => _penumbra!.PathResolver.CreatingCharacterBase += value; + remove => _penumbra!.PathResolver.CreatingCharacterBase -= value; + } + public bool Valid => _penumbra != null; @@ -42,7 +54,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) ?.GetValue( Dalamud.GameData ); - _penumbra.ObjectReloader.GameObjectRedrawn += OnGameObjectRedrawn; foreach( var collection in Penumbra.CollectionManager ) { SubscribeToCollection( collection ); @@ -54,7 +65,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void Dispose() { Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; - _penumbra!.ObjectReloader.GameObjectRedrawn -= OnGameObjectRedrawn; _penumbra = null; _lumina = null; foreach( var collection in Penumbra.CollectionManager ) @@ -249,9 +259,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc AddMod( string modDirectory ) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Combine(Penumbra.ModManager.BasePath.FullName, modDirectory) ); + var dir = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, modDirectory ) ); if( !dir.Exists ) + { return PenumbraApiEc.FileMissing; + } Penumbra.ModManager.AddMod( dir ); return PenumbraApiEc.Success; @@ -521,11 +533,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) - { - GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); - } - // Resolve a path given by string for a specific collection. private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) { @@ -645,9 +652,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public void InvokePreSettingsPanel(string modDirectory) - => PreSettingsPanelDraw?.Invoke(modDirectory); + public void InvokePreSettingsPanel( string modDirectory ) + => PreSettingsPanelDraw?.Invoke( modDirectory ); - public void InvokePostSettingsPanel(string modDirectory) - => PostSettingsPanelDraw?.Invoke(modDirectory); + public void InvokePostSettingsPanel( string modDirectory ) + => PostSettingsPanelDraw?.Invoke( modDirectory ); } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 6d2cd98e..017b988b 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -52,13 +52,13 @@ public partial class PenumbraIpc public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< string >? ProviderGetConfiguration; - internal ICallGateProvider? ProviderPreSettingsDraw; - internal ICallGateProvider? ProviderPostSettingsDraw; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string >? ProviderGetConfiguration; + internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; + internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider< (int Breaking, int Features) >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) @@ -112,7 +112,7 @@ public partial class PenumbraIpc try { - ProviderPreSettingsDraw = pi.GetIpcProvider( LabelProviderPreSettingsDraw ); + ProviderPreSettingsDraw = pi.GetIpcProvider< string, object? >( LabelProviderPreSettingsDraw ); Api.PreSettingsPanelDraw += InvokeSettingsPreDraw; } catch( Exception e ) @@ -122,7 +122,7 @@ public partial class PenumbraIpc try { - ProviderPostSettingsDraw = pi.GetIpcProvider( LabelProviderPostSettingsDraw ); + ProviderPostSettingsDraw = pi.GetIpcProvider< string, object? >( LabelProviderPostSettingsDraw ); Api.PostSettingsPanelDraw += InvokeSettingsPostDraw; } catch( Exception e ) @@ -136,6 +136,8 @@ public partial class PenumbraIpc ProviderGetConfiguration?.UnregisterFunc(); ProviderGetModDirectory?.UnregisterFunc(); ProviderApiVersion?.UnregisterFunc(); + Api.PreSettingsPanelDraw -= InvokeSettingsPreDraw; + Api.PostSettingsPanelDraw -= InvokeSettingsPostDraw; } } @@ -232,21 +234,23 @@ public partial class PenumbraIpc ProviderRedrawObject?.UnregisterAction(); ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); - Api.GameObjectRedrawn -= OnGameObjectRedrawn; + Api.GameObjectRedrawn -= OnGameObjectRedrawn; } } public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; - public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; + public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; - internal ICallGateProvider< string, string, IList< string > >? ProviderReverseResolvePath; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< string, string, IList< string > >? ProviderReverseResolvePath; + internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -289,6 +293,16 @@ public partial class PenumbraIpc { PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } + + try + { + ProviderCreatingCharacterBase = pi.GetIpcProvider< IntPtr, string, IntPtr, IntPtr, object? >( LabelProviderCreatingCharacterBase ); + Api.CreatingCharacterBase += CreatingCharacterBaseEvent; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" ); + } } private void DisposeResolveProviders() @@ -297,6 +311,12 @@ public partial class PenumbraIpc ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); + Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; + } + + private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr customize, IntPtr equipData ) + { + ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, customize, equipData ); } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 0c9f6c7b..c5d74ce7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,7 +8,6 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -114,7 +113,7 @@ public unsafe partial class ResourceLoader // Try all resolve path subscribers or use the default replacer. private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { - if( !DoReplacements || IsInIncRef ) + if( !DoReplacements || IsInIncRef > 0 ) { return ( null, null ); } @@ -270,15 +269,14 @@ public unsafe partial class ResourceLoader // This means, that if the path determined from that is different than the resources path, // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. // This causes some problems and is hopefully prevented with this. - public bool IsInIncRef { get; private set; } = false; + public int IsInIncRef { get; private set; } = 0; private readonly Hook< ResourceHandleDestructor > _incRefHook; private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) { - var tmp = IsInIncRef; - IsInIncRef = true; + ++IsInIncRef; var ret = _incRefHook.Original( handle ); - IsInIncRef = tmp; + --IsInIncRef; return ret; } } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index daf4ae7f..0fcab13e 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -105,7 +106,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable private readonly List< int > _afterGPoseQueue = new(GPoseSlots); private int _target = -1; - public event Action< IntPtr, int >? GameObjectRedrawn; + public event GameObjectRedrawn? GameObjectRedrawn; public ObjectReloader() => Dalamud.Framework.Update += OnUpdateEvent; diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 253cb0e0..ed0bbec4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -11,6 +11,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; +using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -28,11 +29,19 @@ public unsafe partial class PathResolver public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; private ModCollection? _lastCreatedCollection; + public event CreatingCharacterBaseDelegate? CreatingCharacterBase; + private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection ); - var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); + + if( LastGameObject != null ) + { + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, b, c ); + } + + var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); if( LastGameObject != null ) { DrawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 612fb69e..806fbf94 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -153,6 +153,7 @@ public partial class ConfigWindow return; } + ImGui.TextUnformatted( $"In Increment RefCounter Mode: {Penumbra.ResourceLoader.IsInIncRef}" ); using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); if( drawTree ) { From 885dcbdf048dfb4212c1afb5029576eb9260c485 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 20:46:55 +0200 Subject: [PATCH 0333/2451] Add Draw after setting up everything else to prevent exception. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f61805f8..ea5ec046 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -136,6 +136,7 @@ public class Penumbra : IDalamudPlugin { PluginLog.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); } + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) @@ -145,7 +146,6 @@ public class Penumbra : IDalamudPlugin system = new WindowSystem( Name ); system.AddWindow( _configWindow ); system.AddWindow( cfg.ModEditPopup ); - Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } From adfe6816312a12b645c6d51d08447b70ddf96ebe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 20:59:11 +0200 Subject: [PATCH 0334/2451] Fix early commit. --- Penumbra/Api/IpcTester.cs | 3 +-- Penumbra/Api/PenumbraIpc.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 40f22ce3..b457aaf1 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -169,8 +169,7 @@ public class IpcTester : IDisposable DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" ); ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); DrawIntro( PenumbraIpc.LabelProviderApiVersion, "Current Version" ); - var (breaking, features) = _pi.GetIpcSubscriber< (int, int) >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc(); - ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< int >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc().ToString() ); DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 017b988b..4b14650d 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -52,13 +52,13 @@ public partial class PenumbraIpc public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< string >? ProviderGetConfiguration; - internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; - internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string >? ProviderGetConfiguration; + internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; + internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { @@ -82,7 +82,7 @@ public partial class PenumbraIpc try { - ProviderApiVersion = pi.GetIpcProvider< (int Breaking, int Features) >( LabelProviderApiVersion ); + ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); } catch( Exception e ) From ab2ca472fc44a9cd5e520c74b33682057fd37b5f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Jul 2022 22:10:11 +0200 Subject: [PATCH 0335/2451] Another try with atomic increment/decrement? --- .../Interop/Loader/ResourceLoader.Replacement.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index c5d74ce7..cbbcb3c4 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; @@ -113,7 +114,7 @@ public unsafe partial class ResourceLoader // Try all resolve path subscribers or use the default replacer. private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { - if( !DoReplacements || IsInIncRef > 0 ) + if( !DoReplacements || _isInIncRef > 0 ) { return ( null, null ); } @@ -269,14 +270,17 @@ public unsafe partial class ResourceLoader // This means, that if the path determined from that is different than the resources path, // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. // This causes some problems and is hopefully prevented with this. - public int IsInIncRef { get; private set; } = 0; + private int _isInIncRef = 0; + public int IsInIncRef + => _isInIncRef; + private readonly Hook< ResourceHandleDestructor > _incRefHook; private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) { - ++IsInIncRef; + Interlocked.Increment(ref _isInIncRef); var ret = _incRefHook.Original( handle ); - --IsInIncRef; + Interlocked.Decrement(ref _isInIncRef); return ret; } } \ No newline at end of file From 958ff5d8035d9fc91c758cee7ef8be466bb8eca5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 00:16:26 +0200 Subject: [PATCH 0336/2451] Fix sorting. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index c5e54db0..bec5e2b1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c5e54db0ae54b2eab943be9920e2888b0b4b1d13 +Subproject commit bec5e2b1b9c9fa6c32c9e1aa0aec62210261b99d From 57a38aeb94d8e227a9da88fe80e9b4e244efe7fc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 00:31:27 +0200 Subject: [PATCH 0337/2451] Potentially more secure version for mod loading. --- .../Loader/ResourceLoader.Replacement.cs | 18 +++++++++++------- Penumbra/UI/ConfigWindow.DebugTab.cs | 1 - 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index cbbcb3c4..0f990fc3 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -114,7 +116,7 @@ public unsafe partial class ResourceLoader // Try all resolve path subscribers or use the default replacer. private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { - if( !DoReplacements || _isInIncRef > 0 ) + if( !DoReplacements || _incMode.Value ) { return ( null, null ); } @@ -270,17 +272,19 @@ public unsafe partial class ResourceLoader // This means, that if the path determined from that is different than the resources path, // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. // This causes some problems and is hopefully prevented with this. - private int _isInIncRef = 0; - public int IsInIncRef - => _isInIncRef; - + private readonly ThreadLocal< bool > _incMode = new(); private readonly Hook< ResourceHandleDestructor > _incRefHook; private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) { - Interlocked.Increment(ref _isInIncRef); + if( handle->RefCount > 0 ) + { + return _incRefHook.Original( handle ); + } + + _incMode.Value = true; var ret = _incRefHook.Original( handle ); - Interlocked.Decrement(ref _isInIncRef); + _incMode.Value = false; return ret; } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 806fbf94..612fb69e 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -153,7 +153,6 @@ public partial class ConfigWindow return; } - ImGui.TextUnformatted( $"In Increment RefCounter Mode: {Penumbra.ResourceLoader.IsInIncRef}" ); using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); if( drawTree ) { From d9418b67433aa13ce7e0c09e169e559051b79033 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 15:35:29 +0200 Subject: [PATCH 0338/2451] Add Serenity's guides to readme and guide button to main interface. --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 38 ++++++++++++++++++++++--- README.md | 7 +++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index d40d3e33..58cb4c28 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -219,10 +219,9 @@ public partial class ConfigWindow public static void DrawDiscordButton( float width ) { - const string discord = "Join Discord for Support"; const string address = @"https://discord.gg/kVva7DHV4r"; using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); - if( ImGui.Button( discord, new Vector2( width, 0 ) ) ) + if( ImGui.Button( "Join Discord for Support", new Vector2( width, 0 ) ) ) { try { @@ -252,6 +251,33 @@ public partial class ConfigWindow } } + private static void DrawGuideButton( float width ) + { + const string address = @"https://penumbra.ju.mp"; + using var color = ImRaii.PushColor( ImGuiCol.Button, 0xFFCC648D ) + .Push( ImGuiCol.ButtonHovered, 0xFFB070B0 ) + .Push( ImGuiCol.ButtonActive, 0xFF9070E0 ); + if( ImGui.Button( "Beginner's Guides", new Vector2( width, 0 ) ) ) + { + try + { + var process = new ProcessStartInfo( address ) + { + UseShellExecute = true, + }; + Process.Start( process ); + } + catch + { + // ignored + } + } + + ImGuiUtil.HoverTooltip( + $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" + + "Not directly affiliated and potentially, but not usually out of date." ); + } + private static void DrawSupportButtons() { var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; @@ -260,11 +286,15 @@ public partial class ConfigWindow width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; } - ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, ImGui.GetFrameHeightWithSpacing() ) ); + var xPos = ImGui.GetWindowWidth() - width; + ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); DrawSupportButton(); - ImGui.SetCursorPos( new Vector2( ImGui.GetWindowWidth() - width, 0 ) ); + ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); DrawDiscordButton( width ); + + ImGui.SetCursorPos( new Vector2( xPos, 2 * ImGui.GetFrameHeightWithSpacing() ) ); + DrawGuideButton( width ); } } } \ No newline at end of file diff --git a/README.md b/README.md index 830e5abb..755280b5 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,15 @@ Contributions are welcome, but please make an issue first before writing any cod Penumbra has support for most TexTools modpacks however this is provided on a best-effort basis and support is not guaranteed. Built in tooling will be added to Penumbra over time to avoid many common TexTools use cases. ## Installing -While this project is still a work in progress, you can use it by addin the following URL to the custom plugin repositories list in your Dalamud settings +While this project is still a work in progress, you can use it by adding the following URL to the custom plugin repositories list in your Dalamud settings +An image-based install (and usage) guide to do this is provided by unaffiliated user Serenity: https://penumbra.ju.mp/ + 1. `/xlsettings` -> Experimental tab 2. Copy and paste the repo.json link below 3. Click on the + button 4. Click on the "Save and Close" button -5. You will now see Penumbra listed in the Dalamud Plugin Installer +5. You will now see Penumbra listed in the Available Plugins tab in the Dalamud Plugin Installer +6. Do not forget to actually install Penumbra from this tab. Please do not install Penumbra manually by downloading a release zip and unpacking it into your devPlugins folder. That will require manually updating Penumbra and you will miss out on features and bug fixes as you won't get update notifications automatically. Any manually installed copies of Penumbra should be removed before switching to the custom plugin respository method, as they will conflict. - https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json From 062c69385f4a7948b10e36479c169ddad7924570 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 15:35:51 +0200 Subject: [PATCH 0339/2451] Add NPC special collections. --- Penumbra/Collections/CollectionType.cs | 64 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Data.cs | 39 ++++++++--- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index f8fc6d48..3aabea1f 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -26,6 +26,22 @@ public enum CollectionType : byte Lost, Rava, Veena, + MidlanderNpc, + HighlanderNpc, + WildwoodNpc, + DuskwightNpc, + PlainsfolkNpc, + DunesfolkNpc, + SeekerOfTheSunNpc, + KeeperOfTheMoonNpc, + SeawolfNpc, + HellsguardNpc, + RaenNpc, + XaelaNpc, + HelionNpc, + LostNpc, + RavaNpc, + VeenaNpc, Inactive, // A collection was added or removed Default, // The default collection was changed @@ -62,6 +78,22 @@ public static class CollectionTypeExtensions CollectionType.Lost => SubRace.Lost.ToName(), CollectionType.Rava => SubRace.Rava.ToName(), CollectionType.Veena => SubRace.Veena.ToName(), + CollectionType.MidlanderNpc => SubRace.Midlander.ToName() + " (NPC)", + CollectionType.HighlanderNpc => SubRace.Highlander.ToName() + " (NPC)", + CollectionType.WildwoodNpc => SubRace.Wildwood.ToName() + " (NPC)", + CollectionType.DuskwightNpc => SubRace.Duskwight.ToName() + " (NPC)", + CollectionType.PlainsfolkNpc => SubRace.Plainsfolk.ToName() + " (NPC)", + CollectionType.DunesfolkNpc => SubRace.Dunesfolk.ToName() + " (NPC)", + CollectionType.SeekerOfTheSunNpc => SubRace.SeekerOfTheSun.ToName() + " (NPC)", + CollectionType.KeeperOfTheMoonNpc => SubRace.KeeperOfTheMoon.ToName() + " (NPC)", + CollectionType.SeawolfNpc => SubRace.Seawolf.ToName() + " (NPC)", + CollectionType.HellsguardNpc => SubRace.Hellsguard.ToName() + " (NPC)", + CollectionType.RaenNpc => SubRace.Raen.ToName() + " (NPC)", + CollectionType.XaelaNpc => SubRace.Xaela.ToName() + " (NPC)", + CollectionType.HelionNpc => SubRace.Helion.ToName() + " (NPC)", + CollectionType.LostNpc => SubRace.Lost.ToName() + " (NPC)", + CollectionType.RavaNpc => SubRace.Rava.ToName() + " (NPC)", + CollectionType.VeenaNpc => SubRace.Veena.ToName() + " (NPC)", CollectionType.Inactive => "Collection", CollectionType.Default => "Default", CollectionType.Character => "Character", @@ -110,6 +142,38 @@ public static class CollectionTypeExtensions "This collection applies to all player character Rava Viera that do not have a more specific character collection associated.", CollectionType.Veena => "This collection applies to all player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.MidlanderNpc => + "This collection applies to all non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.HighlanderNpc => + "This collection applies to all non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.WildwoodNpc => + "This collection applies to all non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.DuskwightNpc => + "This collection applies to all non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.PlainsfolkNpc => + "This collection applies to all non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.DunesfolkNpc => + "This collection applies to all non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.SeekerOfTheSunNpc => + "This collection applies to all non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.KeeperOfTheMoonNpc => + "This collection applies to all non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.SeawolfNpc => + "This collection applies to all non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.HellsguardNpc => + "This collection applies to all non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.RaenNpc => + "This collection applies to all non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.XaelaNpc => + "This collection applies to all non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.HelionNpc => + "This collection applies to all non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.LostNpc => + "This collection applies to all non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.RavaNpc => + "This collection applies to all non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.VeenaNpc => + "This collection applies to all non-player character Veena Viera that do not have a more specific character collection associated.", _ => string.Empty, }; } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index ed0bbec4..0da5d586 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -367,15 +367,7 @@ public unsafe partial class PathResolver if( character->ModelCharaId == 0 ) { // Check if the object is a non-player human NPC. - if( actor->ObjectKind != ( byte )ObjectKind.Player ) - { - collection = Penumbra.CollectionManager.ByType( CollectionType.NonPlayerCharacter ); - if( collection != null ) - { - return true; - } - } - else + if( actor->ObjectKind == ( byte )ObjectKind.Player ) { // Check the subrace. If it does not fit any or no subrace collection is set, check the player character collection. collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch @@ -404,6 +396,35 @@ public unsafe partial class PathResolver return true; } } + else + { + // Check the subrace. If it does not fit any or no subrace collection is set, check the npn-player character collection. + collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch + { + SubRace.Midlander => Penumbra.CollectionManager.ByType( CollectionType.MidlanderNpc ), + SubRace.Highlander => Penumbra.CollectionManager.ByType( CollectionType.HighlanderNpc ), + SubRace.Wildwood => Penumbra.CollectionManager.ByType( CollectionType.WildwoodNpc ), + SubRace.Duskwight => Penumbra.CollectionManager.ByType( CollectionType.DuskwightNpc ), + SubRace.Plainsfolk => Penumbra.CollectionManager.ByType( CollectionType.PlainsfolkNpc ), + SubRace.Dunesfolk => Penumbra.CollectionManager.ByType( CollectionType.DunesfolkNpc ), + SubRace.SeekerOfTheSun => Penumbra.CollectionManager.ByType( CollectionType.SeekerOfTheSunNpc ), + SubRace.KeeperOfTheMoon => Penumbra.CollectionManager.ByType( CollectionType.KeeperOfTheMoonNpc ), + SubRace.Seawolf => Penumbra.CollectionManager.ByType( CollectionType.SeawolfNpc ), + SubRace.Hellsguard => Penumbra.CollectionManager.ByType( CollectionType.HellsguardNpc ), + SubRace.Raen => Penumbra.CollectionManager.ByType( CollectionType.RaenNpc ), + SubRace.Xaela => Penumbra.CollectionManager.ByType( CollectionType.XaelaNpc ), + SubRace.Helion => Penumbra.CollectionManager.ByType( CollectionType.HelionNpc ), + SubRace.Lost => Penumbra.CollectionManager.ByType( CollectionType.LostNpc ), + SubRace.Rava => Penumbra.CollectionManager.ByType( CollectionType.RavaNpc ), + SubRace.Veena => Penumbra.CollectionManager.ByType( CollectionType.VeenaNpc ), + _ => null, + }; + collection ??= Penumbra.CollectionManager.ByType( CollectionType.NonPlayerCharacter ); + if( collection != null ) + { + return true; + } + } } } From d97e9f37a81b419b26e6b725b3beb7e0edc0f91d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 16:34:20 +0200 Subject: [PATCH 0340/2451] Add breaking and feature version with backwards compatibility and warning. --- Penumbra/Api/IPenumbraApi.cs | 6 +++++- Penumbra/Api/IpcTester.cs | 7 ++++--- Penumbra/Api/PenumbraApi.cs | 4 ++-- Penumbra/Api/PenumbraIpc.cs | 33 +++++++++++++++++++++++++-------- Penumbra/Import/Dds/DdsFile.cs | 2 +- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 65b406ca..43ccd0c5 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -10,7 +10,11 @@ namespace Penumbra.Api; public interface IPenumbraApiBase { - public int ApiVersion { get; } + // The API version is staggered in two parts. + // The major/Breaking version only increments if there are changes breaking backwards compatibility. + // The minor/Feature version increments any time there is something added + // and resets when Breaking is incremented. + public (int Breaking, int Feature) ApiVersion { get; } public bool Valid { get; } } diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index b457aaf1..87669449 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -104,7 +104,7 @@ public class IpcTester : IDisposable return; } - ImGui.TextUnformatted( $"API Version: {_ipc.Api.ApiVersion}" ); + ImGui.TextUnformatted( $"API Version: {_ipc.Api.ApiVersion.Breaking}.{_ipc.Api.ApiVersion.Feature:D4}" ); ImGui.TextUnformatted( "Available subscriptions:" ); using var indent = ImRaii.PushIndent(); @@ -168,8 +168,9 @@ public class IpcTester : IDisposable DrawList( PenumbraIpc.LabelProviderDisposed, "Last Disposed", _disposedList ); DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" ); ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); - DrawIntro( PenumbraIpc.LabelProviderApiVersion, "Current Version" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< int >( PenumbraIpc.LabelProviderApiVersion ).InvokeFunc().ToString() ); + DrawIntro( PenumbraIpc.LabelProviderApiVersions, "Current Version" ); + var (breaking, features) = _pi.GetIpcSubscriber< (int, int) >( PenumbraIpc.LabelProviderApiVersions ).InvokeFunc(); + ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index b15ff691..7797b2c9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -20,8 +20,8 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { - public int ApiVersion - => 6; + public (int, int) ApiVersion + => ( 4, 8 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 4b14650d..ac443531 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -47,18 +47,20 @@ public partial class PenumbraIpc public const string LabelProviderInitialized = "Penumbra.Initialized"; public const string LabelProviderDisposed = "Penumbra.Disposed"; public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderApiVersions = "Penumbra.ApiVersions"; public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< string >? ProviderGetConfiguration; - internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; - internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; + internal ICallGateProvider< object? >? ProviderInitialized; + internal ICallGateProvider< object? >? ProviderDisposed; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< (int Breaking, int Features) >? ProviderApiVersions; + internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string >? ProviderGetConfiguration; + internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; + internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; private void InitializeGeneralProviders( DalamudPluginInterface pi ) { @@ -83,13 +85,27 @@ public partial class PenumbraIpc try { ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); - ProviderApiVersion.RegisterFunc( () => Api.ApiVersion ); + ProviderApiVersion.RegisterFunc( () => + { + PluginLog.Warning( $"{LabelProviderApiVersion} is outdated. Please use {LabelProviderApiVersions} instead." ); + return Api.ApiVersion.Breaking; + } ); } catch( Exception e ) { PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); } + try + { + ProviderApiVersions = pi.GetIpcProvider< ( int, int ) >( LabelProviderApiVersions ); + ProviderApiVersions.RegisterFunc( () => Api.ApiVersion ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersions}:\n{e}" ); + } + try { ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); @@ -136,6 +152,7 @@ public partial class PenumbraIpc ProviderGetConfiguration?.UnregisterFunc(); ProviderGetModDirectory?.UnregisterFunc(); ProviderApiVersion?.UnregisterFunc(); + ProviderApiVersions?.UnregisterFunc(); Api.PreSettingsPanelDraw -= InvokeSettingsPreDraw; Api.PostSettingsPanelDraw -= InvokeSettingsPostDraw; } diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs index 1b0f973a..ec67ea3b 100644 --- a/Penumbra/Import/Dds/DdsFile.cs +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -229,7 +229,7 @@ public class DdsFile public class TmpTexFile { public TexFile.TexHeader Header; - public byte[] RgbaData; + public byte[] RgbaData = Array.Empty< byte >(); public void Load( BinaryReader br ) { From f99bfae4bb586a2f1425a8c4afd7bf2ca71d3795 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 16:37:44 +0200 Subject: [PATCH 0341/2451] Use temporary mechanism to add main menu button. --- Penumbra/UI/LaunchButton.cs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 84d89e3e..b25e4646 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -18,15 +18,20 @@ public class LaunchButton : IDisposable _configWindow = ui; _icon = null; _entry = null; - //Dalamud.Framework.RunOnTick( () => - //{ - // _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, - // "tsmLogo.png" ) ); - // if( _icon != null ) - // { - // _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - // } - //} ); + + void CreateEntry() + { + _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + "tsmLogo.png" ) ); + if( _icon != null ) + { + _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); + } + + Dalamud.PluginInterface.UiBuilder.Draw -= CreateEntry; + } + + Dalamud.PluginInterface.UiBuilder.Draw += CreateEntry; } private void OnTriggered() From 8133e5927f221541d80d2e1e3e253a5ef178013f Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 2 Jul 2022 14:47:51 +0000 Subject: [PATCH 0342/2451] [CI] Updating repo.json for refs/tags/0.5.3.0 --- repo.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 54c32c39..fdfa6e26 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.2.0", - "TestingAssemblyVersion": "0.5.2.0", + "AssemblyVersion": "0.5.3.0", + "TestingAssemblyVersion": "0.5.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -14,9 +14,11 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.2.0/Penumbra.zip", + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e1f1a7f378b4fd7428a32bc4b064f79454e7f98d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Jul 2022 19:06:10 +0200 Subject: [PATCH 0343/2451] Fixes. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 1 + Penumbra/Meta/Manager/MetaManager.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 0f990fc3..c14a6720 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -247,6 +247,7 @@ public unsafe partial class ResourceLoader GetResourceSyncHook.Dispose(); GetResourceAsyncHook.Dispose(); ResourceHandleDestructorHook?.Dispose(); + _incRefHook.Dispose(); } private static int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 699273dd..4657f443 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -56,6 +56,7 @@ public partial class MetaManager : IDisposable, IEnumerable Date: Sat, 2 Jul 2022 17:09:33 +0000 Subject: [PATCH 0344/2451] [CI] Updating repo.json for refs/tags/0.5.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index fdfa6e26..e2765579 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.0", - "TestingAssemblyVersion": "0.5.3.0", + "AssemblyVersion": "0.5.3.1", + "TestingAssemblyVersion": "0.5.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6902ef48d18d88183f3f96df5e2af1d95922b439 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 3 Jul 2022 22:47:15 +0200 Subject: [PATCH 0345/2451] Fix crash during migration --- Penumbra/Configuration.Migration.cs | 2 +- Penumbra/Penumbra.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index b1714470..a62c091b 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -182,7 +182,7 @@ public partial class Configuration DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty<(CollectionType, string)>() ); + CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); } // Collections were introduced and the previous CurrentCollection got put into ModDirectory. diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ea5ec046..7aa61f30 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -73,7 +73,8 @@ public class Penumbra : IDalamudPlugin DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); - Framework = new FrameworkManager(); + Framework = new FrameworkManager(); + CharacterUtility = new CharacterUtility(); Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); @@ -82,7 +83,6 @@ public class Penumbra : IDalamudPlugin ResourceLoader = new ResourceLoader( this ); ResourceLoader.EnableHooks(); ResourceLogger = new ResourceLogger( ResourceLoader ); - CharacterUtility = new CharacterUtility(); ResidentResources = new ResidentResourceManager(); ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); From 32e817d793141a045b6076a0269e8e6e144450cf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 3 Jul 2022 23:01:38 +0200 Subject: [PATCH 0346/2451] Some glamourer additions. --- OtterGui | 2 +- Penumbra.GameData/Enums/EquipSlot.cs | 316 ++++++++++---------- Penumbra.GameData/Structs/CharacterArmor.cs | 14 +- 3 files changed, 178 insertions(+), 154 deletions(-) diff --git a/OtterGui b/OtterGui index bec5e2b1..fcc5031f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bec5e2b1b9c9fa6c32c9e1aa0aec62210261b99d +Subproject commit fcc5031fcf14b54f090d5bb789c580567b3f0023 diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 628acad7..67a1da6d 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -2,160 +2,176 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection.Metadata.Ecma335; -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum EquipSlot : byte { - public enum EquipSlot : byte + Unknown = 0, + MainHand = 1, + OffHand = 2, + Head = 3, + Body = 4, + Hands = 5, + Belt = 6, + Legs = 7, + Feet = 8, + Ears = 9, + Neck = 10, + Wrists = 11, + RFinger = 12, + BothHand = 13, + LFinger = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game. + HeadBody = 15, + BodyHandsLegsFeet = 16, + SoulCrystal = 17, + LegsFeet = 18, + FullBody = 19, + BodyHands = 20, + BodyLegsFeet = 21, + All = 22, // Not officially existing +} + +public static class EquipSlotExtensions +{ + public static EquipSlot ToEquipSlot( this uint value ) + => value switch + { + 0 => EquipSlot.Head, + 1 => EquipSlot.Body, + 2 => EquipSlot.Hands, + 3 => EquipSlot.Legs, + 4 => EquipSlot.Feet, + 5 => EquipSlot.Ears, + 6 => EquipSlot.Neck, + 7 => EquipSlot.Wrists, + 8 => EquipSlot.RFinger, + 9 => EquipSlot.LFinger, + _ => EquipSlot.Unknown, + }; + + public static string ToSuffix( this EquipSlot value ) { - Unknown = 0, - MainHand = 1, - OffHand = 2, - Head = 3, - Body = 4, - Hands = 5, - Belt = 6, - Legs = 7, - Feet = 8, - Ears = 9, - Neck = 10, - Wrists = 11, - RFinger = 12, - BothHand = 13, - LFinger = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game. - HeadBody = 15, - BodyHandsLegsFeet = 16, - SoulCrystal = 17, - LegsFeet = 18, - FullBody = 19, - BodyHands = 20, - BodyLegsFeet = 21, - All = 22, // Not officially existing - } - - public static class EquipSlotExtensions - { - public static string ToSuffix( this EquipSlot value ) + return value switch { - return value switch - { - EquipSlot.Head => "met", - EquipSlot.Hands => "glv", - EquipSlot.Legs => "dwn", - EquipSlot.Feet => "sho", - EquipSlot.Body => "top", - EquipSlot.Ears => "ear", - EquipSlot.Neck => "nek", - EquipSlot.RFinger => "rir", - EquipSlot.LFinger => "ril", - EquipSlot.Wrists => "wrs", - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static EquipSlot ToSlot( this EquipSlot value ) - { - return value switch - { - EquipSlot.MainHand => EquipSlot.MainHand, - EquipSlot.OffHand => EquipSlot.OffHand, - EquipSlot.Head => EquipSlot.Head, - EquipSlot.Body => EquipSlot.Body, - EquipSlot.Hands => EquipSlot.Hands, - EquipSlot.Belt => EquipSlot.Belt, - EquipSlot.Legs => EquipSlot.Legs, - EquipSlot.Feet => EquipSlot.Feet, - EquipSlot.Ears => EquipSlot.Ears, - EquipSlot.Neck => EquipSlot.Neck, - EquipSlot.Wrists => EquipSlot.Wrists, - EquipSlot.RFinger => EquipSlot.RFinger, - EquipSlot.BothHand => EquipSlot.MainHand, - EquipSlot.LFinger => EquipSlot.RFinger, - EquipSlot.HeadBody => EquipSlot.Body, - EquipSlot.BodyHandsLegsFeet => EquipSlot.Body, - EquipSlot.SoulCrystal => EquipSlot.SoulCrystal, - EquipSlot.LegsFeet => EquipSlot.Legs, - EquipSlot.FullBody => EquipSlot.Body, - EquipSlot.BodyHands => EquipSlot.Body, - EquipSlot.BodyLegsFeet => EquipSlot.Body, - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static string ToName( this EquipSlot value ) - { - return value switch - { - EquipSlot.Head => "Head", - EquipSlot.Hands => "Hands", - EquipSlot.Legs => "Legs", - EquipSlot.Feet => "Feet", - EquipSlot.Body => "Body", - EquipSlot.Ears => "Earrings", - EquipSlot.Neck => "Necklace", - EquipSlot.RFinger => "Right Ring", - EquipSlot.LFinger => "Left Ring", - EquipSlot.Wrists => "Bracelets", - EquipSlot.MainHand => "Primary Weapon", - EquipSlot.OffHand => "Secondary Weapon", - EquipSlot.Belt => "Belt", - EquipSlot.BothHand => "Primary Weapon", - EquipSlot.HeadBody => "Head and Body", - EquipSlot.BodyHandsLegsFeet => "Costume", - EquipSlot.SoulCrystal => "Soul Crystal", - EquipSlot.LegsFeet => "Bottom", - EquipSlot.FullBody => "Costume", - EquipSlot.BodyHands => "Top", - EquipSlot.BodyLegsFeet => "Costume", - EquipSlot.All => "Costume", - _ => "Unknown", - }; - } - - public static bool IsEquipment( this EquipSlot value ) - { - return value switch - { - EquipSlot.Head => true, - EquipSlot.Hands => true, - EquipSlot.Legs => true, - EquipSlot.Feet => true, - EquipSlot.Body => true, - _ => false, - }; - } - - public static bool IsAccessory( this EquipSlot value ) - { - return value switch - { - EquipSlot.Ears => true, - EquipSlot.Neck => true, - EquipSlot.RFinger => true, - EquipSlot.LFinger => true, - EquipSlot.Wrists => true, - _ => false, - }; - } - - public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray(); - public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray(); - public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray(); - } - - public static partial class Names - { - public static readonly Dictionary< string, EquipSlot > SuffixToEquipSlot = new() - { - { EquipSlot.Head.ToSuffix(), EquipSlot.Head }, - { EquipSlot.Hands.ToSuffix(), EquipSlot.Hands }, - { EquipSlot.Legs.ToSuffix(), EquipSlot.Legs }, - { EquipSlot.Feet.ToSuffix(), EquipSlot.Feet }, - { EquipSlot.Body.ToSuffix(), EquipSlot.Body }, - { EquipSlot.Ears.ToSuffix(), EquipSlot.Ears }, - { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, - { EquipSlot.RFinger.ToSuffix(), EquipSlot.RFinger }, - { EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger }, - { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists }, + EquipSlot.Head => "met", + EquipSlot.Hands => "glv", + EquipSlot.Legs => "dwn", + EquipSlot.Feet => "sho", + EquipSlot.Body => "top", + EquipSlot.Ears => "ear", + EquipSlot.Neck => "nek", + EquipSlot.RFinger => "rir", + EquipSlot.LFinger => "ril", + EquipSlot.Wrists => "wrs", + _ => throw new InvalidEnumArgumentException(), }; } + + public static EquipSlot ToSlot( this EquipSlot value ) + { + return value switch + { + EquipSlot.MainHand => EquipSlot.MainHand, + EquipSlot.OffHand => EquipSlot.OffHand, + EquipSlot.Head => EquipSlot.Head, + EquipSlot.Body => EquipSlot.Body, + EquipSlot.Hands => EquipSlot.Hands, + EquipSlot.Belt => EquipSlot.Belt, + EquipSlot.Legs => EquipSlot.Legs, + EquipSlot.Feet => EquipSlot.Feet, + EquipSlot.Ears => EquipSlot.Ears, + EquipSlot.Neck => EquipSlot.Neck, + EquipSlot.Wrists => EquipSlot.Wrists, + EquipSlot.RFinger => EquipSlot.RFinger, + EquipSlot.BothHand => EquipSlot.MainHand, + EquipSlot.LFinger => EquipSlot.RFinger, + EquipSlot.HeadBody => EquipSlot.Body, + EquipSlot.BodyHandsLegsFeet => EquipSlot.Body, + EquipSlot.SoulCrystal => EquipSlot.SoulCrystal, + EquipSlot.LegsFeet => EquipSlot.Legs, + EquipSlot.FullBody => EquipSlot.Body, + EquipSlot.BodyHands => EquipSlot.Body, + EquipSlot.BodyLegsFeet => EquipSlot.Body, + _ => throw new InvalidEnumArgumentException(), + }; + } + + public static string ToName( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => "Head", + EquipSlot.Hands => "Hands", + EquipSlot.Legs => "Legs", + EquipSlot.Feet => "Feet", + EquipSlot.Body => "Body", + EquipSlot.Ears => "Earrings", + EquipSlot.Neck => "Necklace", + EquipSlot.RFinger => "Right Ring", + EquipSlot.LFinger => "Left Ring", + EquipSlot.Wrists => "Bracelets", + EquipSlot.MainHand => "Primary Weapon", + EquipSlot.OffHand => "Secondary Weapon", + EquipSlot.Belt => "Belt", + EquipSlot.BothHand => "Primary Weapon", + EquipSlot.HeadBody => "Head and Body", + EquipSlot.BodyHandsLegsFeet => "Costume", + EquipSlot.SoulCrystal => "Soul Crystal", + EquipSlot.LegsFeet => "Bottom", + EquipSlot.FullBody => "Costume", + EquipSlot.BodyHands => "Top", + EquipSlot.BodyLegsFeet => "Costume", + EquipSlot.All => "Costume", + _ => "Unknown", + }; + } + + public static bool IsEquipment( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => true, + EquipSlot.Hands => true, + EquipSlot.Legs => true, + EquipSlot.Feet => true, + EquipSlot.Body => true, + _ => false, + }; + } + + public static bool IsAccessory( this EquipSlot value ) + { + return value switch + { + EquipSlot.Ears => true, + EquipSlot.Neck => true, + EquipSlot.RFinger => true, + EquipSlot.LFinger => true, + EquipSlot.Wrists => true, + _ => false, + }; + } + + public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray(); + public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray(); + public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray(); +} + +public static partial class Names +{ + public static readonly Dictionary< string, EquipSlot > SuffixToEquipSlot = new() + { + { EquipSlot.Head.ToSuffix(), EquipSlot.Head }, + { EquipSlot.Hands.ToSuffix(), EquipSlot.Hands }, + { EquipSlot.Legs.ToSuffix(), EquipSlot.Legs }, + { EquipSlot.Feet.ToSuffix(), EquipSlot.Feet }, + { EquipSlot.Body.ToSuffix(), EquipSlot.Body }, + { EquipSlot.Ears.ToSuffix(), EquipSlot.Ears }, + { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, + { EquipSlot.RFinger.ToSuffix(), EquipSlot.RFinger }, + { EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger }, + { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists }, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index 6b3f60a8..ffe421cf 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -2,13 +2,21 @@ using System.Runtime.InteropServices; namespace Penumbra.GameData.Structs; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] +[StructLayout( LayoutKind.Explicit, Pack = 1 )] public readonly struct CharacterArmor { - public readonly SetId Set; - public readonly byte Variant; + [FieldOffset( 0 )] + public readonly SetId Set; + + [FieldOffset( 2 )] + public readonly byte Variant; + + [FieldOffset( 3 )] public readonly StainId Stain; + [FieldOffset( 0 )] + public readonly uint Value; + public override string ToString() => $"{Set},{Variant},{Stain}"; } \ No newline at end of file From 7a3c23d8c9a02f066f5ab1624b41b876b43a5b79 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 3 Jul 2022 21:04:35 +0000 Subject: [PATCH 0347/2451] [CI] Updating repo.json for refs/tags/0.5.3.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e2765579..39723727 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.1", - "TestingAssemblyVersion": "0.5.3.1", + "AssemblyVersion": "0.5.3.2", + "TestingAssemblyVersion": "0.5.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 0ce41f82a62841b513ebbe5e1e34bf4d764b6596 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 4 Jul 2022 11:10:10 +0200 Subject: [PATCH 0348/2451] Fix invalid API call when manipulating multiple mods at once. --- Penumbra/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7797b2c9..588fda8b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -628,7 +628,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var name = c.Name; void Del( ModSettingChange type, int idx, int _, int _2, bool inherited ) - => ModSettingChanged?.Invoke( type, name, Penumbra.ModManager[ idx ].ModPath.Name, inherited ); + => ModSettingChanged?.Invoke( type, name, idx >= 0 ? Penumbra.ModManager[ idx ].ModPath.Name : string.Empty, inherited ); _delegates[ c ] = Del; c.ModSettingChanged += Del; From 8fd95695083c75350cd61a80d4f0f2697bdec5c7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 4 Jul 2022 11:10:25 +0200 Subject: [PATCH 0349/2451] Add second ID to retainer check. --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 0da5d586..d7584648 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -297,7 +297,7 @@ public unsafe partial class PathResolver // Housing Retainers if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc - && gameObject->DataID == 1011832 ) + && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45" { return Penumbra.CollectionManager.Default; } From 477d61b6abb096fb6f4ed110fb8cd930cadf558b Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 4 Jul 2022 09:13:09 +0000 Subject: [PATCH 0350/2451] [CI] Updating repo.json for refs/tags/0.5.3.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 39723727..cf54025a 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.2", - "TestingAssemblyVersion": "0.5.3.2", + "AssemblyVersion": "0.5.3.3", + "TestingAssemblyVersion": "0.5.3.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From abce14dfdd63116b3af7e66b6c673e10053fa192 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 5 Jul 2022 23:28:14 +0200 Subject: [PATCH 0351/2451] Some fixes. --- .../Collections/CollectionManager.Active.cs | 91 ++++--------------- Penumbra/Interop/CharacterUtility.cs | 10 -- Penumbra/Interop/MetaFileManager.cs | 7 -- .../Interop/Resolver/PathResolver.Data.cs | 2 +- .../Interop/Resolver/PathResolver.Meta.cs | 28 ------ Penumbra/Meta/Manager/MetaManager.Imc.cs | 47 +++++----- Penumbra/Meta/Manager/MetaManager.cs | 10 +- ...ConfigWindow.CollectionsTab.Inheritance.cs | 3 +- 8 files changed, 50 insertions(+), 148 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index ceca52be..392a5a9f 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -72,25 +72,7 @@ public partial class ModCollection ? c.Index : Default.Index : -1, - CollectionType.Yourself => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.PlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.NonPlayerCharacter => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Midlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Highlander => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Wildwood => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Duskwight => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Plainsfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Dunesfolk => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.SeekerOfTheSun => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.KeeperOfTheMoon => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Seawolf => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Hellsguard => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Raen => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Xaela => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Helion => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Lost => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Rava => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, - CollectionType.Veena => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + _ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, _ => -1, }; @@ -146,71 +128,30 @@ public partial class ModCollection // Create a special collection if it does not exist and set it to Empty. public bool CreateSpecialCollection( CollectionType collectionType ) { - switch( collectionType ) + if( !collectionType.IsSpecial() || _specialCollections[( int )collectionType] != null ) { - case CollectionType.Yourself: - case CollectionType.PlayerCharacter: - case CollectionType.NonPlayerCharacter: - case CollectionType.Midlander: - case CollectionType.Highlander: - case CollectionType.Wildwood: - case CollectionType.Duskwight: - case CollectionType.Plainsfolk: - case CollectionType.Dunesfolk: - case CollectionType.SeekerOfTheSun: - case CollectionType.KeeperOfTheMoon: - case CollectionType.Seawolf: - case CollectionType.Hellsguard: - case CollectionType.Raen: - case CollectionType.Xaela: - case CollectionType.Helion: - case CollectionType.Lost: - case CollectionType.Rava: - case CollectionType.Veena: - if( _specialCollections[ ( int )collectionType ] != null ) - { - return false; - } - - _specialCollections[ ( int )collectionType ] = Empty; - CollectionChanged.Invoke( collectionType, null, Empty, null ); - return true; - default: return false; + return false; } + + _specialCollections[ ( int )collectionType ] = Empty; + CollectionChanged.Invoke( collectionType, null, Empty, null ); + return true; + } // Remove a special collection if it exists public void RemoveSpecialCollection( CollectionType collectionType ) { - switch( collectionType ) + if( !collectionType.IsSpecial() ) { - case CollectionType.Yourself: - case CollectionType.PlayerCharacter: - case CollectionType.NonPlayerCharacter: - case CollectionType.Midlander: - case CollectionType.Highlander: - case CollectionType.Wildwood: - case CollectionType.Duskwight: - case CollectionType.Plainsfolk: - case CollectionType.Dunesfolk: - case CollectionType.SeekerOfTheSun: - case CollectionType.KeeperOfTheMoon: - case CollectionType.Seawolf: - case CollectionType.Hellsguard: - case CollectionType.Raen: - case CollectionType.Xaela: - case CollectionType.Helion: - case CollectionType.Lost: - case CollectionType.Rava: - case CollectionType.Veena: - var old = _specialCollections[ ( int )collectionType ]; - if( old != null ) - { - _specialCollections[ ( int )collectionType ] = null; - CollectionChanged.Invoke( collectionType, old, null, null ); - } + return; + } - return; + var old = _specialCollections[ ( int )collectionType ]; + if( old != null ) + { + _specialCollections[ ( int )collectionType ] = null; + CollectionChanged.Invoke( collectionType, old, null, null ); } } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index d96f948e..9ff778c3 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -30,22 +30,12 @@ public unsafe class CharacterUtility : IDisposable // The defines are set in the project configuration. public static readonly int[] RelevantIndices = Array.Empty< int >() -#if USE_EQP .Append( Structs.CharacterUtility.EqpIdx ) -#endif -#if USE_GMP .Append( Structs.CharacterUtility.GmpIdx ) -#endif -#if USE_EQDP .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) .Where( i => i != 17 ) ) // TODO: Female Hrothgar -#endif -#if USE_CMP .Append( Structs.CharacterUtility.HumanCmpIdx ) -#endif -#if USE_EST .Concat( Enumerable.Range( Structs.CharacterUtility.FaceEstIdx, 4 ) ) -#endif .ToArray(); public static readonly int[] ReverseIndices diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs index c6b5241a..51a6dc19 100644 --- a/Penumbra/Interop/MetaFileManager.cs +++ b/Penumbra/Interop/MetaFileManager.cs @@ -26,10 +26,8 @@ public unsafe class MetaFileManager : IDisposable // Allocate in the games space for file storage. // We only need this if using any meta file. -#if USE_IMC || USE_CMP || USE_EQDP || USE_EQP || USE_EST || USE_GMP [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] public IntPtr GetFileSpaceAddress; -#endif public IMemorySpace* GetFileSpace() => ( ( delegate* unmanaged< IMemorySpace* > )GetFileSpaceAddress )(); @@ -41,10 +39,8 @@ public unsafe class MetaFileManager : IDisposable // We only need this for IMC files, since we need to hook their cleanup function. -#if USE_IMC [Signature( "48 8D 05 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 89 03", ScanType = ScanType.StaticAddress )] public IntPtr* DefaultResourceHandleVTable; -#endif public delegate void ClearResource( ResourceHandle* resource ); public Hook< ClearResource > ClearDefaultResourceHook = null!; @@ -67,7 +63,6 @@ public unsafe class MetaFileManager : IDisposable } // Called when a new IMC is manipulated to store its data. - [Conditional( "USE_IMC" )] public void AddImcFile( ResourceHandle* resource, IntPtr data, int length ) { PluginLog.Debug( "Storing data 0x{Data:X} of Length {Length} for {$Name:l} (0x{Resource:X}).", ( ulong )data, length, @@ -76,14 +71,12 @@ public unsafe class MetaFileManager : IDisposable } // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. - [Conditional( "USE_IMC" )] private void InitImc() { ClearDefaultResourceHook = new Hook< ClearResource >( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); ClearDefaultResourceHook.Enable(); } - [Conditional( "USE_IMC" )] private void DisposeImc() { ClearDefaultResourceHook.Disable(); diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index d7584648..1f07dd85 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -297,7 +297,7 @@ public unsafe partial class PathResolver // Housing Retainers if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc - && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45" + && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer { return Penumbra.CollectionManager.Default; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 5267ba89..0ccfb3e1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -127,19 +127,11 @@ public unsafe partial class PathResolver private void EnableMetaHooks() { -#if USE_EQP GetEqpIndirectHook?.Enable(); -#endif -#if USE_EQP || USE_EQDP UpdateModelsHook?.Enable(); OnModelLoadCompleteHook?.Enable(); -#endif -#if USE_GMP SetupVisorHook?.Enable(); -#endif -#if USE_CMP RspSetupCharacterHook?.Enable(); -#endif } private void DisableMetaHooks() @@ -196,30 +188,23 @@ public unsafe partial class PathResolver public static MetaChanger ChangeEqp( ModCollection collection ) { -#if USE_EQP collection.SetEqpFiles(); return new MetaChanger( MetaManipulation.Type.Eqp ); -#else - return new MetaChanger( MetaManipulation.Type.Unknown ); -#endif } public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject ) { -#if USE_EQP var collection = resolver.GetCollection( drawObject ); if( collection != null ) { return ChangeEqp( collection ); } -#endif return new MetaChanger( MetaManipulation.Type.Unknown ); } // We only need to change anything if it is actually equipment here. public static MetaChanger ChangeEqdp( PathResolver resolver, IntPtr drawObject, uint modelType ) { -#if USE_EQDP if( modelType < 10 ) { var collection = resolver.GetCollection( drawObject ); @@ -228,43 +213,34 @@ public unsafe partial class PathResolver return ChangeEqdp( collection ); } } -#endif return new MetaChanger( MetaManipulation.Type.Unknown ); } public static MetaChanger ChangeEqdp( ModCollection collection ) { -#if USE_EQDP collection.SetEqdpFiles(); return new MetaChanger( MetaManipulation.Type.Eqdp ); -#else - return new MetaChanger( MetaManipulation.Type.Unknown ); -#endif } public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) { -#if USE_GMP var collection = resolver.GetCollection( drawObject ); if( collection != null ) { collection.SetGmpFiles(); return new MetaChanger( MetaManipulation.Type.Gmp ); } -#endif return new MetaChanger( MetaManipulation.Type.Unknown ); } public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) { -#if USE_EST var collection = resolver.GetCollection( drawObject ); if( collection != null ) { collection.SetEstFiles(); return new MetaChanger( MetaManipulation.Type.Est ); } -#endif return new MetaChanger( MetaManipulation.Type.Unknown ); } @@ -273,13 +249,11 @@ public unsafe partial class PathResolver if( resolver.LastGameObject != null ) { collection = IdentifyCollection( resolver.LastGameObject ); -#if USE_CMP if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } -#endif } else { @@ -291,14 +265,12 @@ public unsafe partial class PathResolver public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) { -#if USE_CMP var collection = resolver.GetCollection( drawObject ); if( collection != null ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } -#endif return new MetaChanger( MetaManipulation.Type.Unknown ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 1621090d..96ff29f6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -87,32 +87,31 @@ public partial class MetaManager public bool RevertMod( ImcManipulation m ) { -#if USE_IMC - if( _imcManipulations.Remove( m ) ) + if( !_imcManipulations.Remove( m ) ) { - var path = m.GamePath(); - if( !_imcFiles.TryGetValue( path, out var file ) ) - { - return false; - } - - var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); - var manip = m with { Entry = def }; - if( !manip.Apply( file ) ) - { - return false; - } - - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } - - return true; + return false; } -#endif - return false; + + var path = m.GamePath(); + if( !_imcFiles.TryGetValue( path, out var file ) ) + { + return false; + } + + var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); + var manip = m with { Entry = def }; + if( !manip.Apply( file ) ) + { + return false; + } + + var fullPath = CreateImcPath( path ); + if( _collection.HasCache ) + { + _collection.ForceFile( path, fullPath ); + } + + return true; } public void DisposeImc() diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 4657f443..1d664039 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -10,7 +10,7 @@ using Penumbra.Mods; namespace Penumbra.Meta.Manager; -public partial class MetaManager : IDisposable, IEnumerable> +public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaManipulation, IMod > > { private readonly Dictionary< MetaManipulation, IMod > _manipulations = new(); private readonly ModCollection _collection; @@ -25,7 +25,7 @@ public partial class MetaManager : IDisposable, IEnumerable _manipulations.Keys; - public IEnumerator> GetEnumerator() + public IEnumerator< KeyValuePair< MetaManipulation, IMod > > GetEnumerator() => _manipulations.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -135,6 +135,12 @@ public partial class MetaManager : IDisposable, IEnumerable Penumbra.CollectionManager.Current.CheckValidInheritance( c ) == ModCollection.ValidInheritance.Valid ) ) + .Where( c => Penumbra.CollectionManager.Current.CheckValidInheritance( c ) == ModCollection.ValidInheritance.Valid ) + .OrderBy( c => c.Name )) { if( ImGui.Selectable( collection.Name, _newInheritance == collection ) ) { From 95de9ea48a81c9c8673a794f190dd467a1a6c216 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 5 Jul 2022 21:37:08 +0000 Subject: [PATCH 0352/2451] [CI] Updating repo.json for refs/tags/0.5.3.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cf54025a..ed61be5d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.3", - "TestingAssemblyVersion": "0.5.3.3", + "AssemblyVersion": "0.5.3.4", + "TestingAssemblyVersion": "0.5.3.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9115cbaac1cbb50f088dcec94f57506a04b5cbe1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Jul 2022 10:45:57 +0200 Subject: [PATCH 0353/2451] Add ReverseResolvePlayer. --- Penumbra/Api/IPenumbraApi.cs | 12 +++-- Penumbra/Api/IpcTester.cs | 21 +++++++-- Penumbra/Api/PenumbraApi.cs | 19 ++++++-- Penumbra/Api/PenumbraIpc.cs | 46 ++++++++++++------- .../Interop/Resolver/PathResolver.Data.cs | 28 ++++++++++- 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 43ccd0c5..da11cfb7 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -22,7 +22,9 @@ public delegate void ChangedItemHover( object? item ); public delegate void ChangedItemClick( MouseButton button, object? item ); public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); -public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr customize, IntPtr equipData ); + +public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, + IntPtr equipData ); public enum PenumbraApiEc { @@ -87,8 +89,12 @@ public interface IPenumbraApi : IPenumbraApiBase // Returns the given gamePath if penumbra would not manipulate it. public string ResolvePath( string gamePath, string characterName ); - // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public IList< string > ReverseResolvePath( string moddedPath, string characterName ); + // Reverse resolves a given modded local path into its replacement in form of all applicable game paths for given character collection. + public string[] ReverseResolvePath( string moddedPath, string characterName ); + + // Reverse resolves a given modded local path into its replacement in form of all applicable game paths + // using the collection applying to the player character. + public string[] ReverseResolvePathPlayer( string moddedPath ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 87669449..cc098c98 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -30,7 +30,7 @@ public class IpcTester : IDisposable private readonly ICallGateSubscriber< string, object? > _postSettingsDraw; private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; - private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, object? > _characterBaseCreated; + private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreated; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); @@ -46,7 +46,7 @@ public class IpcTester : IDisposable _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); _characterBaseCreated = - _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); + _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); _initialized.Subscribe( AddInitialized ); _disposed.Subscribe( AddDisposed ); _redrawn.Subscribe( SetLastRedrawn ); @@ -204,7 +204,7 @@ public class IpcTester : IDisposable private string _lastCreatedGameObjectName = string.Empty; private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; - private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3 ) + private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); @@ -278,6 +278,21 @@ public class IpcTester : IDisposable } } + DrawIntro( PenumbraIpc.LabelProviderReverseResolvePathPlayer, "Reversed Game Paths (Player)" ); + if( _currentReversePath.Length > 0 ) + { + var list = _pi.GetIpcSubscriber< string, string[] >( PenumbraIpc.LabelProviderReverseResolvePathPlayer ) + .InvokeFunc( _currentReversePath ); + if( list.Length > 0 ) + { + ImGui.TextUnformatted( list[ 0 ] ); + if( list.Length > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + } + } + } + DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" ); if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 588fda8b..39adf0be 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -12,6 +12,7 @@ using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Util; @@ -21,7 +22,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 8 ); + => ( 4, 9 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -129,7 +130,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.Character( characterName ) ); } - public IList< string > ReverseResolvePath( string path, string characterName ) + public string[] ReverseResolvePath( string path, string characterName ) { CheckInitialized(); if( !Penumbra.Config.EnableMods ) @@ -138,7 +139,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var ret = Penumbra.CollectionManager.Character( characterName ).ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToList(); + return ret.Select( r => r.ToString() ).ToArray(); + } + + public string[] ReverseResolvePathPlayer( string path ) + { + CheckInitialized(); + if( !Penumbra.Config.EnableMods ) + { + return new[] { path }; + } + + var ret = PathResolver.PlayerCollection().ReverseResolvePath( new FullPath( path ) ); + return ret.Select( r => r.ToString() ).ToArray(); } public T? GetFile< T >( string gamePath ) where T : FileResource diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index ac443531..c97cca85 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -257,17 +257,19 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; - public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; + public const string LabelProviderReverseResolvePathPlayer = "Penumbra.ReverseResolvePathPlayer"; + public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; - internal ICallGateProvider< string, string, IList< string > >? ProviderReverseResolvePath; - internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; + internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; + internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -303,18 +305,29 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePath = pi.GetIpcProvider< string, string, IList< string > >( LabelProviderReverseResolvePath ); + ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePath}:\n{e}" ); } try { - ProviderCreatingCharacterBase = pi.GetIpcProvider< IntPtr, string, IntPtr, IntPtr, object? >( LabelProviderCreatingCharacterBase ); - Api.CreatingCharacterBase += CreatingCharacterBaseEvent; + ProviderReverseResolvePathPlayer = pi.GetIpcProvider< string, string[] >( LabelProviderReverseResolvePathPlayer ); + ProviderReverseResolvePathPlayer.RegisterFunc( Api.ReverseResolvePathPlayer ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePathPlayer}:\n{e}" ); + } + + try + { + ProviderCreatingCharacterBase = + pi.GetIpcProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( LabelProviderCreatingCharacterBase ); + Api.CreatingCharacterBase += CreatingCharacterBaseEvent; } catch( Exception e ) { @@ -328,12 +341,13 @@ public partial class PenumbraIpc ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); + ProviderReverseResolvePathPlayer?.UnregisterFunc(); Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; } - private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr customize, IntPtr equipData ) + private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData ) { - ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, customize, equipData ); + ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, modelId, customize, equipData ); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 1f07dd85..e5ac64cd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -31,14 +31,14 @@ public unsafe partial class PathResolver private ModCollection? _lastCreatedCollection; public event CreatingCharacterBaseDelegate? CreatingCharacterBase; - private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection ); if( LastGameObject != null ) { - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, b, c ); + var modelPtr = &a; + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c ); } var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); @@ -341,6 +341,30 @@ public unsafe partial class PathResolver } } + // Get the collection applying to the current player character + // or the default collection if no player exists. + public static ModCollection PlayerCollection() + { + var player = Dalamud.ClientState.LocalPlayer; + if( player == null ) + { + return Penumbra.CollectionManager.Default; + } + + var name = player.Name.TextValue; + if( CollectionByActorName( name, out var c ) ) + { + return c; + } + + if( CollectionByActor( name, ( GameObject* )player.Address, out c ) ) + { + return c; + } + + return Penumbra.CollectionManager.Default; + } + // Check both temporary and permanent character collections. Temporary first. private static bool CollectionByActorName( string name, [NotNullWhen( true )] out ModCollection? collection ) => Penumbra.TempMods.Collections.TryGetValue( name, out collection ) From f98428323160f73550d3327d35d705308d38b2ed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Jul 2022 10:47:11 +0200 Subject: [PATCH 0354/2451] Increment the collection change counter when character utility is ready. --- Penumbra/Collections/ModCollection.Cache.cs | 17 ++++++++++++++--- Penumbra/Meta/Manager/MetaManager.cs | 6 ++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 34ef8924..fa9669b6 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -51,15 +51,18 @@ public partial class ModCollection public Cache( ModCollection collection ) { _collection = collection; - MetaManipulations = new MetaManager( collection ); + MetaManipulations = new MetaManager( _collection ); _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; + if( !Penumbra.CharacterUtility.Ready ) + Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; } public void Dispose() { - _collection.ModSettingChanged -= OnModSettingChange; - _collection.InheritanceChanged -= OnInheritanceChange; + _collection.ModSettingChanged -= OnModSettingChange; + _collection.InheritanceChanged -= OnInheritanceChange; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; } // Resolve a given game path according to this collection. @@ -443,6 +446,14 @@ public partial class ModCollection private void AddMetaFiles() => MetaManipulations.SetImcFiles(); + // Increment the counter to ensure new files are loaded after applying meta changes. + private void IncrementCounter() + { + ++ChangeCounter; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 1d664039..4eb46e82 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -24,7 +24,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public IReadOnlyCollection< MetaManipulation > Manipulations => _manipulations.Keys; - public IEnumerator< KeyValuePair< MetaManipulation, IMod > > GetEnumerator() => _manipulations.GetEnumerator(); @@ -35,7 +34,10 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM { _collection = collection; SetupImcDelegate(); - Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; + if( !Penumbra.CharacterUtility.Ready ) + { + Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; + } } public void SetFiles() From 70bae7737e53b2e7561a6ccb3f002b7f18bf7cec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Jul 2022 10:50:57 +0200 Subject: [PATCH 0355/2451] Move Collection Change Counter to Collection instead of Cache so it does not reset if cache is destroyed. --- .../Collections/ModCollection.Cache.Access.cs | 5 +++-- Penumbra/Collections/ModCollection.Cache.cs | 17 ++++++++--------- .../Interop/Resolver/PathResolver.Material.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index b5ea41f1..e289096b 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -18,8 +18,9 @@ public partial class ModCollection public bool HasCache => _cache != null; - public int RecomputeCounter - => _cache?.ChangeCounter ?? 0; + // Count the number of changes of the effective file list. + // This is used for material and imc changes. + public int ChangeCounter { get; private set; } // Only create, do not update. private void CreateCache() diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index fa9669b6..332d0fa5 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -32,9 +32,6 @@ public partial class ModCollection public SingleArray< ModConflicts > Conflicts( IMod mod ) => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); - // Count the number of changes of the effective file list. - // This is used for material and imc changes. - public int ChangeCounter { get; private set; } private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. @@ -55,7 +52,9 @@ public partial class ModCollection _collection.ModSettingChanged += OnModSettingChange; _collection.InheritanceChanged += OnInheritanceChange; if( !Penumbra.CharacterUtility.Ready ) + { Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; + } } public void Dispose() @@ -176,7 +175,7 @@ public partial class ModCollection AddMetaFiles(); - ++ChangeCounter; + ++_collection.ChangeCounter; if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) { @@ -239,7 +238,7 @@ public partial class ModCollection if( addMetaChanges ) { - ++ChangeCounter; + ++_collection.ChangeCounter; if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) { Penumbra.ResidentResources.Reload(); @@ -292,7 +291,7 @@ public partial class ModCollection if( addMetaChanges ) { - ++ChangeCounter; + ++_collection.ChangeCounter; if( mod.TotalManipulations > 0 ) { AddMetaFiles(); @@ -449,7 +448,7 @@ public partial class ModCollection // Increment the counter to ensure new files are loaded after applying meta changes. private void IncrementCounter() { - ++ChangeCounter; + ++_collection.ChangeCounter; Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; } @@ -457,14 +456,14 @@ public partial class ModCollection // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { - if( _changedItemsSaveCounter == ChangeCounter ) + if( _changedItemsSaveCounter == _collection.ChangeCounter ) { return; } try { - _changedItemsSaveCounter = ChangeCounter; + _changedItemsSaveCounter = _collection.ChangeCounter; _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 64e82ab7..f52b4389 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -113,7 +113,7 @@ public unsafe partial class PathResolver { if( nonDefault && type == ResourceType.Mtrl ) { - var fullPath = new FullPath( $"|{collection.Name}_{collection.RecomputeCounter}|{path}" ); + var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" ); data = ( fullPath, collection ); } else diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 96ff29f6..2c6f656c 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -145,7 +145,7 @@ public partial class MetaManager } private FullPath CreateImcPath( Utf8GamePath path ) - => new($"|{_collection.Name}_{_collection.RecomputeCounter}|{path}"); + => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, From 74cb08e55196ef9d7d8ca4f5a86f6bd040458592 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 8 Jul 2022 22:47:16 +0000 Subject: [PATCH 0356/2451] [CI] Updating repo.json for refs/tags/0.5.3.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index ed61be5d..edfd9460 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.4", - "TestingAssemblyVersion": "0.5.3.4", + "AssemblyVersion": "0.5.3.5", + "TestingAssemblyVersion": "0.5.3.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 720a1dce7bf874224d862b43292976f39f9a7853 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 9 Jul 2022 13:29:10 +0200 Subject: [PATCH 0357/2451] add ReversePlayerPath to API, rename ReverseResolvePathPlayer to ReverseResolvePlayerPath --- Penumbra/Api/IPenumbraApi.cs | 6 +++++- Penumbra/Api/PenumbraApi.cs | 11 +++++++++-- Penumbra/Api/PenumbraIpc.cs | 16 ++++++++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index da11cfb7..0ba87fca 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -89,12 +89,16 @@ public interface IPenumbraApi : IPenumbraApiBase // Returns the given gamePath if penumbra would not manipulate it. public string ResolvePath( string gamePath, string characterName ); + // Resolve a given gamePath via Penumbra using any applicable character collections for the current character. + // Returns the given gamePath if penumbra would not manipulate it. + public string ResolvePlayerPath( string gamePath ); + // Reverse resolves a given modded local path into its replacement in form of all applicable game paths for given character collection. public string[] ReverseResolvePath( string moddedPath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game paths // using the collection applying to the player character. - public string[] ReverseResolvePathPlayer( string moddedPath ); + public string[] ReverseResolvePlayerPath( string moddedPath ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 39adf0be..9ce0395f 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Common.Configuration; using Lumina.Data; using Newtonsoft.Json; using OtterGui; @@ -22,7 +23,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 9 ); + => ( 4, 10 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -123,6 +124,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); } + public string ResolvePlayerPath( string path ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, PathResolver.PlayerCollection() ); + } + public string ResolvePath( string path, string characterName ) { CheckInitialized(); @@ -142,7 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ret.Select( r => r.ToString() ).ToArray(); } - public string[] ReverseResolvePathPlayer( string path ) + public string[] ReverseResolvePlayerPath( string path ) { CheckInitialized(); if( !Penumbra.Config.EnableMods ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index c97cca85..d330be61 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -259,13 +259,15 @@ public partial class PenumbraIpc { public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - public const string LabelProviderReverseResolvePathPlayer = "Penumbra.ReverseResolvePathPlayer"; + public const string LabelProviderReverseResolvePathPlayer = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; internal ICallGateProvider< string, string >? ProviderResolveDefault; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< string, string >? ProviderResolvePlayer; internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; @@ -293,6 +295,16 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); } + try + { + ProviderResolvePlayer = pi.GetIpcProvider< string, string >( LabelProviderResolvePlayer ); + ProviderResolvePlayer.RegisterFunc( Api.ResolvePlayerPath ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); + } + try { ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); @@ -316,7 +328,7 @@ public partial class PenumbraIpc try { ProviderReverseResolvePathPlayer = pi.GetIpcProvider< string, string[] >( LabelProviderReverseResolvePathPlayer ); - ProviderReverseResolvePathPlayer.RegisterFunc( Api.ReverseResolvePathPlayer ); + ProviderReverseResolvePathPlayer.RegisterFunc( Api.ReverseResolvePlayerPath ); } catch( Exception e ) { From da9969d3ddff3291b5bce11b88f2c611481cbce1 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 9 Jul 2022 13:30:39 +0200 Subject: [PATCH 0358/2451] forgot to rename const string --- Penumbra/Api/IpcTester.cs | 4 ++-- Penumbra/Api/PenumbraIpc.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index cc098c98..e311161a 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -278,10 +278,10 @@ public class IpcTester : IDisposable } } - DrawIntro( PenumbraIpc.LabelProviderReverseResolvePathPlayer, "Reversed Game Paths (Player)" ); + DrawIntro( PenumbraIpc.LabelProviderReverseResolvePlayerPath, "Reversed Game Paths (Player)" ); if( _currentReversePath.Length > 0 ) { - var list = _pi.GetIpcSubscriber< string, string[] >( PenumbraIpc.LabelProviderReverseResolvePathPlayer ) + var list = _pi.GetIpcSubscriber< string, string[] >( PenumbraIpc.LabelProviderReverseResolvePlayerPath ) .InvokeFunc( _currentReversePath ); if( list.Length > 0 ) { diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index d330be61..fa3f677e 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -262,7 +262,7 @@ public partial class PenumbraIpc public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - public const string LabelProviderReverseResolvePathPlayer = "Penumbra.ReverseResolvePlayerPath"; + public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; internal ICallGateProvider< string, string >? ProviderResolveDefault; @@ -327,12 +327,12 @@ public partial class PenumbraIpc try { - ProviderReverseResolvePathPlayer = pi.GetIpcProvider< string, string[] >( LabelProviderReverseResolvePathPlayer ); + ProviderReverseResolvePathPlayer = pi.GetIpcProvider< string, string[] >( LabelProviderReverseResolvePlayerPath ); ProviderReverseResolvePathPlayer.RegisterFunc( Api.ReverseResolvePlayerPath ); } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePathPlayer}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePlayerPath}:\n{e}" ); } try From 4c90cc84f1d53e0b3ad4fab36f8f01dc2d4bca57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Jul 2022 15:23:39 +0200 Subject: [PATCH 0359/2451] Do not delay IMC manipulations. --- Penumbra/Meta/Manager/MetaManager.cs | 19 +++++++++++++++---- Penumbra/UI/ConfigWindow.DebugTab.cs | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 4eb46e82..6bec0c19 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using Penumbra.Collections; using Penumbra.Meta.Files; @@ -63,6 +64,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public void Dispose() { + _manipulations.Clear(); Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; DisposeEqp(); DisposeEqdp(); @@ -75,6 +77,12 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public bool ApplyMod( MetaManipulation manip, IMod mod ) { _manipulations[ manip ] = mod; + // Imc manipulations do not require character utility. + if( manip.ManipulationType == MetaManipulation.Type.Imc ) + { + return ApplyMod( manip.Imc ); + } + if( !Penumbra.CharacterUtility.Ready ) { return true; @@ -87,7 +95,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), - MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -96,6 +103,12 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public bool RevertMod( MetaManipulation manip ) { var ret = _manipulations.Remove( manip ); + // Imc manipulations do not require character utility. + if( manip.ManipulationType == MetaManipulation.Type.Imc ) + { + return RevertMod( manip.Imc ); + } + if( !Penumbra.CharacterUtility.Ready ) { return ret; @@ -108,7 +121,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => RevertMod( manip.Eqdp ), MetaManipulation.Type.Est => RevertMod( manip.Est ), MetaManipulation.Type.Rsp => RevertMod( manip.Rsp ), - MetaManipulation.Type.Imc => RevertMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -122,7 +134,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM return; } - foreach( var manip in Manipulations ) + foreach( var manip in Manipulations.Where( m => m.ManipulationType != MetaManipulation.Type.Imc ) ) { var _ = manip.ManipulationType switch { @@ -131,7 +143,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), - MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 612fb69e..f7a3d53b 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -196,7 +196,7 @@ public partial class ConfigWindow // Draw information about the character utility class from SE, // displaying all files, their sizes, the default files and the default sizes. - public unsafe void DrawDebugCharacterUtility() + public static unsafe void DrawDebugCharacterUtility() { if( !ImGui.CollapsingHeader( "Character Utility" ) ) { From 50d042c1045f6c1744543b2e89577d11c8bc3389 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Jul 2022 16:39:22 +0200 Subject: [PATCH 0360/2451] Fix metadata conflicts causing problems. --- Penumbra/Meta/Files/EqpGmpFile.cs | 24 ++++++++++++++++++++++-- Penumbra/Meta/Manager/MetaManager.cs | 14 ++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index c85799a6..0a7fe90f 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -109,7 +110,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } } -public sealed class ExpandedEqpFile : ExpandedEqpGmpBase +public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable { public ExpandedEqpFile() : base( false ) @@ -121,6 +122,7 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase set => SetInternal( idx, ( ulong )value ); } + public static EqpEntry GetDefault( int setIdx ) => ( EqpEntry )GetDefaultInternal( CharacterUtility.EqpIdx, setIdx, ( ulong )Eqp.DefaultEntry ); @@ -141,9 +143,18 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase this[ entry ] = GetDefault( entry ); } } + + public IEnumerator< EqpEntry > GetEnumerator() + { + for( var idx = 1; idx < Count; ++idx ) + yield return this[ idx ]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); } -public sealed class ExpandedGmpFile : ExpandedEqpGmpBase +public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable { public ExpandedGmpFile() : base( true ) @@ -165,4 +176,13 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase this[ entry ] = GetDefault( entry ); } } + + public IEnumerator GetEnumerator() + { + for( var idx = 1; idx < Count; ++idx ) + yield return this[idx]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 6bec0c19..7f83e98f 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using Dalamud.Logging; using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -76,6 +77,11 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public bool ApplyMod( MetaManipulation manip, IMod mod ) { + if( _manipulations.ContainsKey( manip ) ) + { + _manipulations.Remove( manip ); + } + _manipulations[ manip ] = mod; // Imc manipulations do not require character utility. if( manip.ManipulationType == MetaManipulation.Type.Imc ) @@ -134,9 +140,10 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM return; } + var loaded = 0; foreach( var manip in Manipulations.Where( m => m.ManipulationType != MetaManipulation.Type.Imc ) ) { - var _ = manip.ManipulationType switch + loaded += manip.ManipulationType switch { MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), MetaManipulation.Type.Gmp => ApplyMod( manip.Gmp ), @@ -145,7 +152,9 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), MetaManipulation.Type.Unknown => false, _ => false, - }; + } + ? 1 + : 0; } if( Penumbra.CollectionManager.Default == _collection ) @@ -155,6 +164,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; + PluginLog.Debug( "{Collection}: Loaded {Num} delayed meta manipulations.", _collection.Name, loaded ); } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] From 78bb869e67ef565b42e0d1a99d08af21b5a676c2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 9 Jul 2022 14:43:40 +0000 Subject: [PATCH 0361/2451] [CI] Updating repo.json for refs/tags/0.5.3.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index edfd9460..6a39ef47 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.5", - "TestingAssemblyVersion": "0.5.3.5", + "AssemblyVersion": "0.5.3.6", + "TestingAssemblyVersion": "0.5.3.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b9ae34852925f45d3e7d4a1c07cd159c41ce8655 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 10 Jul 2022 14:53:22 +0200 Subject: [PATCH 0362/2451] add GetPlayerMetaManipulations --- Penumbra/Api/IPenumbraApi.cs | 4 ++++ Penumbra/Api/PenumbraApi.cs | 20 ++++++++++++++------ Penumbra/Api/PenumbraIpc.cs | 36 ++++++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 0ba87fca..292d822c 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -137,6 +137,10 @@ public interface IPenumbraApi : IPenumbraApiBase // Note that success does only imply a successful call, not a successful mod load. public PenumbraApiEc AddMod( string modDirectory ); + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + // for the collection currently associated with the player. + public string GetPlayerMetaManipulations(); + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations // for the given collection associated with the character name, or the default collection. public string GetMetaManipulations( string characterName ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9ce0395f..a620ff54 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,7 +23,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 10 ); + => ( 4, 11 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -54,8 +54,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); foreach( var collection in Penumbra.CollectionManager ) { SubscribeToCollection( collection ); @@ -429,7 +429,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) - || Penumbra.TempMods.Collections.ContainsKey( character ) ) + || Penumbra.TempMods.Collections.ContainsKey( character ) ) { return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); } @@ -475,7 +475,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; } @@ -512,7 +512,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; } @@ -525,6 +525,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } + public string GetPlayerMetaManipulations() + { + CheckInitialized(); + var collection = PathResolver.PlayerCollection(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); + return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + } + public string GetMetaManipulations( string characterName ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index fa3f677e..de179d66 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -428,18 +428,20 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; - public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; + public const string LabelProviderGetMods = "Penumbra.GetMods"; + public const string LabelProviderGetCollections = "Penumbra.GetCollections"; + public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; + public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; + public const string LabelProviderGetPlayerMetaManipulations = "Penumbra.GetPlayerMetaManipulations"; + public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; internal ICallGateProvider< IList< string > >? ProviderGetCollections; internal ICallGateProvider< string >? ProviderCurrentCollectionName; internal ICallGateProvider< string >? ProviderDefaultCollectionName; internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; + internal ICallGateProvider< string >? ProviderGetPlayerMetaManipulations; internal ICallGateProvider< string, string >? ProviderGetMetaManipulations; private void InitializeDataProviders( DalamudPluginInterface pi ) @@ -451,7 +453,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetMods}:\n{e}" ); } try @@ -461,7 +463,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCollections}:\n{e}" ); } try @@ -471,7 +473,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCurrentCollectionName}:\n{e}" ); } try @@ -481,7 +483,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); } try @@ -491,7 +493,17 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCharacterCollectionName}:\n{e}" ); + } + + try + { + ProviderGetPlayerMetaManipulations = pi.GetIpcProvider< string >( LabelProviderGetPlayerMetaManipulations ); + ProviderGetPlayerMetaManipulations.RegisterFunc( Api.GetPlayerMetaManipulations ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetPlayerMetaManipulations}:\n{e}" ); } try @@ -501,7 +513,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetMetaManipulations}:\n{e}" ); } } From 4030487472146220cf99bd685e2a9b1299aacab5 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 10 Jul 2022 17:40:52 +0000 Subject: [PATCH 0363/2451] [CI] Updating repo.json for refs/tags/0.5.3.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6a39ef47..e9e67c91 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.6", - "TestingAssemblyVersion": "0.5.3.6", + "AssemblyVersion": "0.5.3.7", + "TestingAssemblyVersion": "0.5.3.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b09a736a852457889a51b5457670d9e7fcb50fde Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Jul 2022 14:08:52 +0200 Subject: [PATCH 0364/2451] Actually clear cache and restore imc files. --- Penumbra/Api/TempModManager.cs | 1 + .../Collections/CollectionManager.Active.cs | 8 ++-- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 1 + Penumbra/Interop/MetaFileManager.cs | 48 +++++++++++++++---- Penumbra/Meta/Files/ImcFile.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 3 +- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index cebc6de9..996306e8 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -153,6 +153,7 @@ public class TempModManager if( _collections.Remove( characterName, out var c ) ) { _mods.Remove( c ); + c.ClearCache(); return true; } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 392a5a9f..486080a5 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -87,7 +87,6 @@ public partial class ModCollection newCollection.CreateCache(); } - RemoveCache( oldCollectionIdx ); switch( collectionType ) { case CollectionType.Default: @@ -110,6 +109,7 @@ public partial class ModCollection break; } + RemoveCache( oldCollectionIdx ); UpdateCurrentCollectionInUse(); CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, characterName ); @@ -128,7 +128,7 @@ public partial class ModCollection // Create a special collection if it does not exist and set it to Empty. public bool CreateSpecialCollection( CollectionType collectionType ) { - if( !collectionType.IsSpecial() || _specialCollections[( int )collectionType] != null ) + if( !collectionType.IsSpecial() || _specialCollections[ ( int )collectionType ] != null ) { return false; } @@ -136,7 +136,6 @@ public partial class ModCollection _specialCollections[ ( int )collectionType ] = Empty; CollectionChanged.Invoke( collectionType, null, Empty, null ); return true; - } // Remove a special collection if it exists @@ -361,7 +360,8 @@ public partial class ModCollection private void RemoveCache( int idx ) { - if( idx != Default.Index + if( idx != Empty.Index + && idx != Default.Index && idx != Current.Index && _specialCollections.All( c => c == null || c.Index != idx ) && _characters.Values.All( c => c.Index != idx ) ) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index e289096b..73d2c7ca 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -56,7 +56,7 @@ public partial class ModCollection // Clear the current cache. - private void ClearCache() + internal void ClearCache() { _cache?.Dispose(); _cache = null; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 332d0fa5..a3654608 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -59,6 +59,7 @@ public partial class ModCollection public void Dispose() { + MetaManipulations.Dispose(); _collection.ModSettingChanged -= OnModSettingChange; _collection.InheritanceChanged -= OnInheritanceChange; Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs index 51a6dc19..38413124 100644 --- a/Penumbra/Interop/MetaFileManager.cs +++ b/Penumbra/Interop/MetaFileManager.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; using Penumbra.GameData.ByteString; using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; namespace Penumbra.Interop; @@ -28,6 +30,7 @@ public unsafe class MetaFileManager : IDisposable // We only need this if using any meta file. [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] public IntPtr GetFileSpaceAddress; + public IMemorySpace* GetFileSpace() => ( ( delegate* unmanaged< IMemorySpace* > )GetFileSpaceAddress )(); @@ -45,7 +48,8 @@ public unsafe class MetaFileManager : IDisposable public delegate void ClearResource( ResourceHandle* resource ); public Hook< ClearResource > ClearDefaultResourceHook = null!; - private readonly Dictionary< IntPtr, (IntPtr, int) > _originalImcData = new(); + private readonly Dictionary< IntPtr, (ImcFile, IntPtr, int) > _originalImcData = new(); + private readonly Dictionary< ImcFile, IntPtr > _currentUse = new(); // We store the original data of loaded IMCs so that we can restore it before they get destroyed, // similar to the other meta files, just with arbitrary destruction. @@ -53,21 +57,48 @@ public unsafe class MetaFileManager : IDisposable { if( _originalImcData.TryGetValue( ( IntPtr )resource, out var data ) ) { - PluginLog.Debug( "Restoring data of {$Name:l} (0x{Resource}) to 0x{Data:X} and Length {Length} before deletion.", - Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource, ( ulong )data.Item1, data.Item2 ); - resource->SetData( data.Item1, data.Item2 ); - _originalImcData.Remove( ( IntPtr )resource ); + ClearImcData( resource, data.Item1, data.Item2, data.Item3); } ClearDefaultResourceHook.Original( resource ); } + // Reset all files from a given IMC cache if they exist. + public void ResetByFile( ImcFile file ) + { + if( !_currentUse.TryGetValue( file, out var resource ) ) + { + return; + } + + if( _originalImcData.TryGetValue( resource, out var data ) ) + { + ClearImcData((ResourceHandle*) resource, file, data.Item2, data.Item3 ); + } + else + { + _currentUse.Remove( file ); + } + } + + // Clear a single IMC resource and reset it to its original data. + private void ClearImcData( ResourceHandle* resource, ImcFile file, IntPtr data, int length) + { + var name = new FullPath( Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true ).ToString() ); + PluginLog.Debug( "Restoring data of {$Name:l} (0x{Resource}) to 0x{Data:X} and Length {Length} before deletion.", name, + ( ulong )resource, ( ulong )data, length ); + resource->SetData( data, length ); + _originalImcData.Remove( ( IntPtr )resource ); + _currentUse.Remove( file ); + } + // Called when a new IMC is manipulated to store its data. - public void AddImcFile( ResourceHandle* resource, IntPtr data, int length ) + public void AddImcFile( ResourceHandle* resource, ImcFile file, IntPtr data, int length) { PluginLog.Debug( "Storing data 0x{Data:X} of Length {Length} for {$Name:l} (0x{Resource:X}).", ( ulong )data, length, Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource ); - _originalImcData[ ( IntPtr )resource ] = ( data, length ); + _originalImcData[ ( IntPtr )resource ] = ( file, data, length ); + _currentUse[ file ] = ( IntPtr )resource; } // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. @@ -83,12 +114,13 @@ public unsafe class MetaFileManager : IDisposable ClearDefaultResourceHook.Dispose(); // Restore all IMCs to their default values on dispose. // This should only be relevant when testing/disabling/reenabling penumbra. - foreach( var (resourcePtr, (data, length)) in _originalImcData ) + foreach( var (resourcePtr, (file, data, length)) in _originalImcData ) { var resource = ( ResourceHandle* )resourcePtr; resource->SetData( data, length ); } _originalImcData.Clear(); + _currentUse.Clear(); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 965df0f9..17ced337 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -229,7 +229,7 @@ public unsafe class ImcFile : MetaBaseFile resource->SetData( ( IntPtr )Data, Length ); if( firstTime ) { - Penumbra.MetaFileManager.AddImcFile( resource, data, length ); + Penumbra.MetaFileManager.AddImcFile( resource, this, data, length ); } } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 2c6f656c..3dd7752c 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Filesystem; @@ -118,9 +119,9 @@ public partial class MetaManager { foreach( var file in _imcFiles.Values ) { + Penumbra.MetaFileManager.ResetByFile( file ); file.Dispose(); } - _imcFiles.Clear(); _imcManipulations.Clear(); RestoreImcDelegate(); From aed1474db897c55a132212b3f2838a085ab00e53 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Jul 2022 14:09:10 +0200 Subject: [PATCH 0365/2451] Rephrase some settings. --- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 56e1a7e9..37d2722d 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -57,19 +57,19 @@ public partial class ConfigWindow Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); - Checkbox( "Use Character Collections in Character Window", + Checkbox( "Use Special Collections in Character Window", "Use the character collection for your character's name in your main character window, if it is set.", Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); - Checkbox( "Use Character Collections in Adventurer Cards", + Checkbox( "Use Special Collections in Adventurer Cards", "Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.", Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); - Checkbox( "Use Character Collections in Try-On Window", + Checkbox( "Use Special Collections in Try-On Window", "Use the character collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); - Checkbox( "Use Character Collections in Inspect Windows", + Checkbox( "Use Special Collections in Inspect Windows", "Use the appropriate character collection for the character you are currently inspecting, based on their name.", Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); - Checkbox( "Use Character Collections based on Ownership", + Checkbox( "Use Special Collections based on Ownership", "Use the owner's name to determine the appropriate character collection for mounts, companions and combat pets.", Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); Checkbox( "Prefer Named Collections over Ownership", From 2412e3be085e7b7c8e643441a4830f2b2282160b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Jul 2022 17:25:17 +0200 Subject: [PATCH 0366/2451] Change default values of AutoDeduplicate and EnableHttp to true. --- Penumbra/Configuration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8ef57b86..6294f42b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -58,8 +58,8 @@ public partial class Configuration : IPluginConfiguration public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } - public bool AutoDeduplicateOnImport { get; set; } = false; - public bool EnableHttpApi { get; set; } + public bool AutoDeduplicateOnImport { get; set; } = true; + public bool EnableHttpApi { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; From 769f54e8dd37a06afcf172cf1babe9a24271b490 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Jul 2022 17:27:22 +0200 Subject: [PATCH 0367/2451] Add Tutorials. --- OtterGui | 2 +- Penumbra/Configuration.cs | 2 + Penumbra/UI/Classes/Colors.cs | 2 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 7 +- ...ConfigWindow.CollectionsTab.Inheritance.cs | 3 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 86 ++++++++++++------- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 6 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 1 + .../UI/ConfigWindow.SettingsTab.General.cs | 4 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 45 ++++++++-- Penumbra/UI/ConfigWindow.Tutorial.cs | 80 +++++++++++++++++ Penumbra/UI/ConfigWindow.cs | 5 ++ 12 files changed, 197 insertions(+), 46 deletions(-) create mode 100644 Penumbra/UI/ConfigWindow.Tutorial.cs diff --git a/OtterGui b/OtterGui index fcc5031f..70d9f814 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fcc5031fcf14b54f090d5bb789c580567b3f0023 +Subproject commit 70d9f81436c82cc51734f0661d10f461da4f1840 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 6294f42b..b025403f 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -41,6 +41,8 @@ public partial class Configuration : IPluginConfiguration public bool DebugMode { get; set; } = false; #endif + public int TutorialStep { get; set; } = 0; + public bool EnableFullResourceLogging { get; set; } = false; public bool EnableResourceLogging { get; set; } = false; public string ResourceLoggingFilter { get; set; } = string.Empty; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 567bd542..a41dc28b 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -28,6 +28,8 @@ public static class Colors public const uint RedTableBgTint = 0x40000080; public const uint DiscordColor = 0xFFDA8972; public const uint FilterActive = 0x807070FF; + public const uint TutorialMarker = 0xFF20FFFF; + public const uint TutorialBorder = 0xD00000FF; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 888a385e..4f6c722d 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -199,8 +199,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void AddImportModButton( Vector2 size ) { - if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) ) + var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ); + ConfigWindow.OpenTutorial( 13 ); + if (!button) { return; } @@ -310,6 +312,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { ImGui.OpenPopup( "ExtendedHelp" ); } + ConfigWindow.OpenTutorial( 14 ); } // Helpers. diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index 0ce6eaf7..92f50d7e 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -31,7 +31,8 @@ public partial class ConfigWindow // Draw the whole inheritance block. private void DrawInheritanceBlock() { - using var id = ImRaii.PushId( "##Inheritance" ); + using var group = ImRaii.Group(); + using var id = ImRaii.PushId( "##Inheritance" ); ImGui.TextUnformatted( "The current collection inherits from:" ); DrawCurrentCollectionInheritance(); DrawInheritanceTrashButton(); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 02e2c578..14b8f838 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -23,13 +23,18 @@ public partial class ConfigWindow public void Draw() { using var tab = ImRaii.TabItem( "Collections" ); + OpenTutorial( 5 ); if( !tab ) { return; } - DrawCharacterCollectionSelectors(); - DrawMainSelectors(); + using var child = ImRaii.Child( "##collections", -Vector2.One ); + if( child ) + { + DrawActiveCollectionSelectors(); + DrawMainSelectors(); + } } @@ -106,6 +111,7 @@ public partial class ConfigWindow private void DrawCurrentCollectionSelector() { + using var group = ImRaii.Group(); DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Current Collection", @@ -114,6 +120,7 @@ public partial class ConfigWindow private void DrawDefaultCollectionSelector() { + using var group = ImRaii.Group(); DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( "Default Collection", @@ -183,34 +190,42 @@ public partial class ConfigWindow } } - private void DrawCharacterCollectionSelectors() + private void DrawActiveCollectionSelectors() { ImGui.Dummy( _window._defaultSpace ); - if( ImGui.CollapsingHeader( "Active Collections", ImGuiTreeNodeFlags.DefaultOpen ) ) + var open = ImGui.CollapsingHeader( "Active Collections" ); + OpenTutorial( 9 ); + if( !open ) { - ImGui.Dummy( _window._defaultSpace ); - DrawDefaultCollectionSelector(); - ImGui.Dummy( _window._defaultSpace ); - foreach( var type in CollectionTypeExtensions.Special ) + return; + } + + ImGui.Dummy( _window._defaultSpace ); + DrawDefaultCollectionSelector(); + OpenTutorial( 10 ); + ImGui.Dummy( _window._defaultSpace ); + foreach( var type in CollectionTypeExtensions.Special ) + { + var collection = Penumbra.CollectionManager.ByType( type ); + if( collection != null ) { - var collection = Penumbra.CollectionManager.ByType( type ); - if( collection != null ) + using var id = ImRaii.PushId( ( int )type ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, + false, true ) ) { - using var id = ImRaii.PushId( ( int )type ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, true ) ) - { - Penumbra.CollectionManager.RemoveSpecialCollection( type ); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); + Penumbra.CollectionManager.RemoveSpecialCollection( type ); } - } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); + } + } + + using( var group = ImRaii.Group() ) + { DrawNewSpecialCollection(); ImGui.Dummy( _window._defaultSpace ); @@ -231,22 +246,31 @@ public partial class ConfigWindow } DrawNewCharacterCollection(); - ImGui.Dummy( _window._defaultSpace ); } + + OpenTutorial( 11 ); + + ImGui.Dummy( _window._defaultSpace ); } private void DrawMainSelectors() { ImGui.Dummy( _window._defaultSpace ); - if( ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ) ) + var open = ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ); + OpenTutorial( 6 ); + if( !open ) { - ImGui.Dummy( _window._defaultSpace ); - DrawCurrentCollectionSelector(); - ImGui.Dummy( _window._defaultSpace ); - DrawNewCollectionInput(); - ImGui.Dummy( _window._defaultSpace ); - DrawInheritanceBlock(); + return; } + + ImGui.Dummy( _window._defaultSpace ); + DrawCurrentCollectionSelector(); + OpenTutorial( 7 ); + ImGui.Dummy( _window._defaultSpace ); + DrawNewCollectionInput(); + ImGui.Dummy( _window._defaultSpace ); + DrawInheritanceBlock(); + OpenTutorial( 8 ); } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 96e25e98..2689f3bd 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -36,6 +36,7 @@ public partial class ConfigWindow private void DrawSettingsTab() { using var tab = DrawTab( SettingsTabHeader, Tabs.Settings ); + OpenTutorial( 17 ); if( !tab ) { return; @@ -51,8 +52,10 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); _window._penumbra.Api.InvokePreSettingsPanel( _mod.ModPath.Name ); DrawEnabledInput(); + OpenTutorial( 15 ); ImGui.SameLine(); DrawPriorityInput(); + OpenTutorial( 16 ); DrawRemoveSettings(); ImGui.Dummy( _window._defaultSpace ); for( var idx = 0; idx < _mod.Groups.Count; ++idx ) @@ -104,7 +107,8 @@ public partial class ConfigWindow // Priority is changed on deactivation of the input box. private void DrawPriorityInput() { - var priority = _currentPriority ?? _settings.Priority; + using var group = ImRaii.Group(); + var priority = _currentPriority ?? _settings.Priority; ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) { diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index e5f3b5c3..8bd9e34c 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -22,6 +22,7 @@ public partial class ConfigWindow try { using var tab = ImRaii.TabItem( "Mods" ); + OpenTutorial( 12 ); if( !tab ) { return; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 37d2722d..224828c4 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -6,7 +6,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Util; namespace Penumbra.UI; @@ -32,9 +31,12 @@ public partial class ConfigWindow { if( !ImGui.CollapsingHeader( "General" ) ) { + OpenTutorial( 4 ); return; } + OpenTutorial( 4 ); + Checkbox( "Hide Config Window when UI is Hidden", "Hide the penumbra main window when you manually hide the in-game user interface.", Penumbra.Config.HideUiWhenUiHidden, v => diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 58cb4c28..2c2356ea 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -4,7 +4,9 @@ using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -131,7 +133,10 @@ public partial class ConfigWindow // as well as the directory picker button and the enter warning. private void DrawRootFolder() { - _newModDirectory ??= Penumbra.Config.ModDirectory; + if( _newModDirectory.IsNullOrEmpty() ) + { + _newModDirectory = Penumbra.Config.ModDirectory; + } var spacing = 3 * ImGuiHelpers.GlobalScale; using var group = ImRaii.Group(); @@ -149,6 +154,7 @@ public partial class ConfigWindow + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); group.Dispose(); + OpenTutorial( 1 ); ImGui.SameLine(); var pos = ImGui.GetCursorPosX(); ImGui.NewLine(); @@ -181,20 +187,34 @@ public partial class ConfigWindow { _window._penumbra.SetEnabled( enabled ); } + + OpenTutorial( 2 ); } private static void DrawShowAdvancedBox() { var showAdvanced = Penumbra.Config.ShowAdvanced; - if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) + using( var _ = ImRaii.Group() ) { - Penumbra.Config.ShowAdvanced = showAdvanced; - Penumbra.Config.Save(); + if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) + { + Penumbra.Config.ShowAdvanced = showAdvanced; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + const string tt = "Enable some advanced options in this window and in the mod selector.\n" + + "This is required to enable manually editing any mod information."; + + // Manually split due to tutorial. + ImGuiComponents.HelpMarker( tt ); + OpenTutorial( 0 ); + ImGui.SameLine(); + ImGui.TextUnformatted( "Show Advanced Settings" ); + ImGuiUtil.HoverTooltip( tt ); } - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Show Advanced Settings", "Enable some advanced options in this window and in the mod selector.\n" - + "This is required to enable manually editing any mod information." ); + OpenTutorial( 3 ); } private static void DrawColorSettings() @@ -281,12 +301,12 @@ public partial class ConfigWindow private static void DrawSupportButtons() { var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; + var xPos = ImGui.GetWindowWidth() - width; if( ImGui.GetScrollMaxY() > 0 ) { - width += ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemSpacing.X; + xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; } - var xPos = ImGui.GetWindowWidth() - width; ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); DrawSupportButton(); @@ -295,6 +315,13 @@ public partial class ConfigWindow ImGui.SetCursorPos( new Vector2( xPos, 2 * ImGui.GetFrameHeightWithSpacing() ) ); DrawGuideButton( width ); + + ImGui.SetCursorPos( new Vector2( xPos, 3 * ImGui.GetFrameHeightWithSpacing() ) ); + if( ImGui.Button( "Restart Tutorial", new Vector2( width, 0 ) ) ) + { + Penumbra.Config.TutorialStep = 0; + Penumbra.Config.Save(); + } } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs new file mode 100644 index 00000000..9e879c51 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -0,0 +1,80 @@ +using OtterGui.Widgets; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private static void UpdateTutorialStep() + { + var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); + if( tutorial != Penumbra.Config.TutorialStep ) + { + Penumbra.Config.TutorialStep = tutorial; + Penumbra.Config.Save(); + } + } + + public static void OpenTutorial( int id ) + => Tutorial.Open( id, Penumbra.Config.TutorialStep, v => + { + Penumbra.Config.TutorialStep = v; + Penumbra.Config.Save(); + } ); + + public static readonly Tutorial Tutorial = new Tutorial() + { + BorderColor = Colors.TutorialBorder, + HighlightColor = Colors.TutorialMarker, + PopupLabel = "Settings Tutorial", + } + .Register( "General Tooltips", "This symbol gives you further information about whatever setting it appears next to.\n\n" + + "Hover over them when you are unsure what something does or how to do something." ) + .Register( "Initial Setup, Step 1: Mod Directory", + "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n" + + "The mod directory should be a short path - like 'D:\\FFXIVMods', if you want to store your mods on D: - and be on a fast hard drive." ) + .Register( "Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not." ) + .Register( "Advanced Settings", "When you are just starting, you should leave this off.\n\n" + + "If you need to do any editing of your mods, you will have to turn it on later." ) + .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + + "If you do not know what some of these do yet, return to this later!" ) + .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + + "This is our next stop!" ) + .Register( "Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" + + "In here, we can create new collections, delete collections, or make them inherit from each other." ) + .Register( "Initial Setup, Step 5: Current Collection", + "We should already have a Default Collection, and for our simple setup, we do not need to do anything here.\n\n" + + "The current collection is the one we are currently editing. Any changes we make in our mod settings later will edit this collection." ) + .Register( "Inheritance", + "This is a more advanced feature. Click the 'What is Inheritance?' button for more information, but we will ignore this for now." ) + .Register( "Initial Setup, Step 6: Active Collections", "Active Collections are those that are actually in use at the moment.\n\n" + + "Any collection in use will apply to the game under certain conditions." ) + .Register( "Initial Setup, Step 7: Default Collection", + "The Default Collection - which should currently also be set to a collection named Default - is the main one.\n\n" + + "As long as no more specific collection applies to something, the mods from the Default Collection will be used.\n\n" + + "This is also the collection you need to use for all UI mods." ) + .Register( "Special Collections", + "Special Collections are those that are used only for special characters in the game, either by specific conditions, or by name.\n\n" + + "We will skip this for now, but hovering over the creation buttons should explain how they work." ) + .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods." ) + .Register( "Initial Setup, Step 9: Mod Import", + "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" + + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress." ) // TODO + .Register( "Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" + + "Import and select a mod now to continue." ) + .Register( "Initial Setup, Step 11: Enabling Mods", + "Enable a mod here. Disabled mods will not apply to anything in the current collection (which can be seen and changed in the top-right corner).\n\n" + + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance." ) + .Register( "Initial Setup, Step 12: Priority", "If two enabled mods in one collection change the same files, there is a conflict.\n\n" + + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" + + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible." ) + .Register( "Mod Options", "Many mods have options themselves. You can also choose those here.\n\n" + + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately." ) + .Register( "Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n" + + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page." ) + .Register( "FAQ 1", "Penumbra can not easily change which items a mod applies to." ) + .Register( "FAQ 2", + "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) + .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ); +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 8025cf3c..124c0350 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -47,6 +47,7 @@ public sealed partial class ConfigWindow : Window, IDisposable MinimumSize = new Vector2( 800, 600 ), MaximumSize = new Vector2( 4096, 2160 ), }; + UpdateTutorialStep(); } public override void Draw() @@ -78,6 +79,10 @@ public sealed partial class ConfigWindow : Window, IDisposable } else { + OpenTutorial( 18 ); + OpenTutorial( 19 ); + OpenTutorial( 20 ); + OpenTutorial( 21 ); using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); SetupSizes(); _settingsTab.Draw(); From b3ee62239608cf5840f7ad9db4afed3855de0a19 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Jul 2022 17:49:49 +0200 Subject: [PATCH 0368/2451] Update workflows to use dalamud release. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28e8156f..3729baff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" - name: Build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b1efa01..69063dda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index bc242bf1..ad4375f0 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From 57b9f60ba3d76b0309f267c7ebd8f31e69e16170 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Jul 2022 16:26:37 +0200 Subject: [PATCH 0369/2451] Some Tutorial updates. --- OtterGui | 2 +- .../Classes/ModFileSystemSelector.Filters.cs | 30 +++++----- Penumbra/UI/Classes/ModFileSystemSelector.cs | 4 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 14 ++--- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 6 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 18 ++++-- .../UI/ConfigWindow.SettingsTab.General.cs | 4 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 12 ++-- Penumbra/UI/ConfigWindow.Tutorial.cs | 60 +++++++++++++++---- Penumbra/UI/ConfigWindow.cs | 4 -- 10 files changed, 101 insertions(+), 53 deletions(-) diff --git a/OtterGui b/OtterGui index 70d9f814..b6c4877c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 70d9f81436c82cc51734f0661d10f461da4f1840 +Subproject commit b6c4877cc586f47931e71f284f865833846d9baa diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 91d64c6e..79156cb5 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -246,22 +246,10 @@ public partial class ModFileSystemSelector return false; } - // Add the state filter combo-button to the right of the filter box. - protected override float CustomFilters( float width ) + private void DrawFilterCombo( ref bool everything ) { - var pos = ImGui.GetCursorPos(); - var remainingWidth = width - ImGui.GetFrameHeight(); - var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); - - var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; - - ImGui.SetCursorPos( comboPos ); - // Draw combo button - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.FilterActive, !everything ); using var combo = ImRaii.Combo( "##filterCombo", string.Empty, ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); - color.Pop(); - if( combo ) { using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, @@ -285,8 +273,22 @@ public partial class ModFileSystemSelector } } } + } - combo.Dispose(); + // Add the state filter combo-button to the right of the filter box. + protected override float CustomFilters( float width ) + { + var pos = ImGui.GetCursorPos(); + var remainingWidth = width - ImGui.GetFrameHeight(); + var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); + + var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; + + ImGui.SetCursorPos( comboPos ); + // Draw combo button + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.FilterActive, !everything ); + DrawFilterCombo( ref everything ); + ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModFilters ); if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) { _stateFilter = ModFilterExtensions.UnfilteredStateMods; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 4f6c722d..9db6abb2 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -201,7 +201,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ); - ConfigWindow.OpenTutorial( 13 ); + ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); if (!button) { return; @@ -312,7 +312,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { ImGui.OpenPopup( "ExtendedHelp" ); } - ConfigWindow.OpenTutorial( 14 ); + ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp ); } // Helpers. diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 14b8f838..75d08bd2 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -23,7 +23,7 @@ public partial class ConfigWindow public void Draw() { using var tab = ImRaii.TabItem( "Collections" ); - OpenTutorial( 5 ); + OpenTutorial( BasicTutorialSteps.Collections ); if( !tab ) { return; @@ -194,7 +194,7 @@ public partial class ConfigWindow { ImGui.Dummy( _window._defaultSpace ); var open = ImGui.CollapsingHeader( "Active Collections" ); - OpenTutorial( 9 ); + OpenTutorial( BasicTutorialSteps.ActiveCollections ); if( !open ) { return; @@ -202,7 +202,7 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawDefaultCollectionSelector(); - OpenTutorial( 10 ); + OpenTutorial( BasicTutorialSteps.DefaultCollection ); ImGui.Dummy( _window._defaultSpace ); foreach( var type in CollectionTypeExtensions.Special ) { @@ -248,7 +248,7 @@ public partial class ConfigWindow DrawNewCharacterCollection(); } - OpenTutorial( 11 ); + OpenTutorial( BasicTutorialSteps.SpecialCollections ); ImGui.Dummy( _window._defaultSpace ); } @@ -257,7 +257,7 @@ public partial class ConfigWindow { ImGui.Dummy( _window._defaultSpace ); var open = ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( 6 ); + OpenTutorial( BasicTutorialSteps.EditingCollections ); if( !open ) { return; @@ -265,12 +265,12 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawCurrentCollectionSelector(); - OpenTutorial( 7 ); + OpenTutorial( BasicTutorialSteps.CurrentCollection ); ImGui.Dummy( _window._defaultSpace ); DrawNewCollectionInput(); ImGui.Dummy( _window._defaultSpace ); DrawInheritanceBlock(); - OpenTutorial( 8 ); + OpenTutorial( BasicTutorialSteps.Inheritance ); } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 2689f3bd..e76c145d 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -36,7 +36,7 @@ public partial class ConfigWindow private void DrawSettingsTab() { using var tab = DrawTab( SettingsTabHeader, Tabs.Settings ); - OpenTutorial( 17 ); + OpenTutorial( BasicTutorialSteps.ModOptions ); if( !tab ) { return; @@ -52,10 +52,10 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); _window._penumbra.Api.InvokePreSettingsPanel( _mod.ModPath.Name ); DrawEnabledInput(); - OpenTutorial( 15 ); + OpenTutorial( BasicTutorialSteps.EnablingMods ); ImGui.SameLine(); DrawPriorityInput(); - OpenTutorial( 16 ); + OpenTutorial( BasicTutorialSteps.Priority ); DrawRemoveSettings(); ImGui.Dummy( _window._defaultSpace ); for( var idx = 0; idx < _mod.Groups.Count; ++idx ) diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 8bd9e34c..adcd512d 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -22,7 +22,7 @@ public partial class ConfigWindow try { using var tab = ImRaii.TabItem( "Mods" ); - OpenTutorial( 12 ); + OpenTutorial( BasicTutorialSteps.Mods ); if( !tab ) { return; @@ -59,11 +59,17 @@ public partial class ConfigWindow using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - DrawDefaultCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawInheritedCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); + using( var group = ImRaii.Group() ) + { + DrawDefaultCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawInheritedCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); + } + + OpenTutorial( BasicTutorialSteps.CollectionSelectors ); + if( !Penumbra.CollectionManager.CurrentCollectionInUse ) { ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 224828c4..badf9f22 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -31,11 +31,11 @@ public partial class ConfigWindow { if( !ImGui.CollapsingHeader( "General" ) ) { - OpenTutorial( 4 ); + OpenTutorial( BasicTutorialSteps.GeneralSettings ); return; } - OpenTutorial( 4 ); + OpenTutorial( BasicTutorialSteps.GeneralSettings ); Checkbox( "Hide Config Window when UI is Hidden", "Hide the penumbra main window when you manually hide the in-game user interface.", Penumbra.Config.HideUiWhenUiHidden, diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 2c2356ea..bcb2e4f1 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -28,6 +28,10 @@ public partial class ConfigWindow public void Draw() { using var tab = ImRaii.TabItem( "Settings" ); + OpenTutorial( BasicTutorialSteps.Fin ); + OpenTutorial( BasicTutorialSteps.Faq1 ); + OpenTutorial( BasicTutorialSteps.Faq2 ); + OpenTutorial( BasicTutorialSteps.Faq3 ); if( !tab ) { return; @@ -154,7 +158,7 @@ public partial class ConfigWindow + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); group.Dispose(); - OpenTutorial( 1 ); + OpenTutorial( BasicTutorialSteps.ModDirectory ); ImGui.SameLine(); var pos = ImGui.GetCursorPosX(); ImGui.NewLine(); @@ -188,7 +192,7 @@ public partial class ConfigWindow _window._penumbra.SetEnabled( enabled ); } - OpenTutorial( 2 ); + OpenTutorial( BasicTutorialSteps.EnableMods ); } private static void DrawShowAdvancedBox() @@ -208,13 +212,13 @@ public partial class ConfigWindow // Manually split due to tutorial. ImGuiComponents.HelpMarker( tt ); - OpenTutorial( 0 ); + OpenTutorial( BasicTutorialSteps.GeneralTooltips ); ImGui.SameLine(); ImGui.TextUnformatted( "Show Advanced Settings" ); ImGuiUtil.HoverTooltip( tt ); } - OpenTutorial( 3 ); + OpenTutorial( BasicTutorialSteps.AdvancedSettings ); } private static void DrawColorSettings() diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 9e879c51..157a9d9c 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -1,3 +1,4 @@ +using System; using OtterGui.Widgets; using Penumbra.UI.Classes; @@ -15,13 +16,41 @@ public partial class ConfigWindow } } - public static void OpenTutorial( int id ) - => Tutorial.Open( id, Penumbra.Config.TutorialStep, v => + public static void OpenTutorial( BasicTutorialSteps step ) + => Tutorial.Open( ( int )step, Penumbra.Config.TutorialStep, v => { Penumbra.Config.TutorialStep = v; Penumbra.Config.Save(); } ); + public enum BasicTutorialSteps + { + GeneralTooltips, + ModDirectory, + EnableMods, + AdvancedSettings, + GeneralSettings, + Collections, + EditingCollections, + CurrentCollection, + Inheritance, + ActiveCollections, + DefaultCollection, + SpecialCollections, + Mods, + ModImport, + AdvancedHelp, + ModFilters, + CollectionSelectors, + EnablingMods, + Priority, + ModOptions, + Fin, + Faq1, + Faq2, + Faq3, + } + public static readonly Tutorial Tutorial = new Tutorial() { BorderColor = Colors.TutorialBorder, @@ -32,23 +61,27 @@ public partial class ConfigWindow + "Hover over them when you are unsure what something does or how to do something." ) .Register( "Initial Setup, Step 1: Mod Directory", "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n" - + "The mod directory should be a short path - like 'D:\\FFXIVMods', if you want to store your mods on D: - and be on a fast hard drive." ) + + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n" + + "The folder should be an empty folder no other applications write to." ) .Register( "Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not." ) .Register( "Advanced Settings", "When you are just starting, you should leave this off.\n\n" + "If you need to do any editing of your mods, you will have to turn it on later." ) .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + "If you do not know what some of these do yet, return to this later!" ) .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" - + "This is our next stop!" ) + + "This is our next stop!\n\n" + + "Go here after setting up your root folder to continue the tutorial!" ) .Register( "Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" + "In here, we can create new collections, delete collections, or make them inherit from each other." ) .Register( "Initial Setup, Step 5: Current Collection", "We should already have a Default Collection, and for our simple setup, we do not need to do anything here.\n\n" - + "The current collection is the one we are currently editing. Any changes we make in our mod settings later will edit this collection." ) + + "The current collection is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." ) .Register( "Inheritance", - "This is a more advanced feature. Click the 'What is Inheritance?' button for more information, but we will ignore this for now." ) + "This is a more advanced feature. Click the help button for more information, but we will ignore this for now." ) .Register( "Initial Setup, Step 6: Active Collections", "Active Collections are those that are actually in use at the moment.\n\n" - + "Any collection in use will apply to the game under certain conditions." ) + + "Any collection in use will apply to the game under certain conditions.\n\n" + + "The Current Collection is also active for technical reasons.\n\n" + + "Open this now to continue." ) .Register( "Initial Setup, Step 7: Default Collection", "The Default Collection - which should currently also be set to a collection named Default - is the main one.\n\n" + "As long as no more specific collection applies to something, the mods from the Default Collection will be used.\n\n" @@ -56,15 +89,21 @@ public partial class ConfigWindow .Register( "Special Collections", "Special Collections are those that are used only for special characters in the game, either by specific conditions, or by name.\n\n" + "We will skip this for now, but hovering over the creation buttons should explain how they work." ) - .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods." ) + .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" + + "Please go there after verifying that your Current Collection and Default Collection are setup to your liking." ) .Register( "Initial Setup, Step 9: Mod Import", "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress." ) // TODO .Register( "Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" + "Import and select a mod now to continue." ) + .Register( "Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here." ) + .Register( "Collection Selectors", "This row provides shortcuts to set your Current Collection.\n\n" + + "The first button sets it to your Default Collection (if any).\n\n" + + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" + + "The third is a regular collection selector to let you choose among all your collections." ) .Register( "Initial Setup, Step 11: Enabling Mods", - "Enable a mod here. Disabled mods will not apply to anything in the current collection (which can be seen and changed in the top-right corner).\n\n" + "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance." ) .Register( "Initial Setup, Step 12: Priority", "If two enabled mods in one collection change the same files, there is a conflict.\n\n" + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" @@ -76,5 +115,6 @@ public partial class ConfigWindow .Register( "FAQ 1", "Penumbra can not easily change which items a mod applies to." ) .Register( "FAQ 2", "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) - .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ); + .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ) + .EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length ); } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 124c0350..30465fb6 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -79,10 +79,6 @@ public sealed partial class ConfigWindow : Window, IDisposable } else { - OpenTutorial( 18 ); - OpenTutorial( 19 ); - OpenTutorial( 20 ); - OpenTutorial( 21 ); using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); SetupSizes(); _settingsTab.Draw(); From b5698acebfda1e731cc437f94fdaa83c6658baeb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Jul 2022 17:41:21 +0200 Subject: [PATCH 0370/2451] Do a bunch of AggressiveInlining. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.Tutorial.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b6c4877c..30dc6a53 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b6c4877cc586f47931e71f284f865833846d9baa +Subproject commit 30dc6a538ce56213cb59802869b54247c18a7372 diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 157a9d9c..e352a91c 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using OtterGui.Widgets; using Penumbra.UI.Classes; @@ -16,6 +17,7 @@ public partial class ConfigWindow } } + [MethodImpl( MethodImplOptions.AggressiveInlining )] public static void OpenTutorial( BasicTutorialSteps step ) => Tutorial.Open( ( int )step, Penumbra.Config.TutorialStep, v => { From 1c60a61f7943abba90a22d9a3f8d82c4ab8fcf57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Jul 2022 16:58:27 +0200 Subject: [PATCH 0371/2451] Anonymized collection names for log --- .../Collections/ModCollection.Cache.Access.cs | 4 +-- .../Collections/ModCollection.Inheritance.cs | 6 ++--- Penumbra/Collections/ModCollection.cs | 5 ++++ .../Interop/Loader/ResourceLoader.Debug.cs | 2 +- Penumbra/Penumbra.cs | 26 +++++++------------ 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 73d2c7ca..3d7e2c9c 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -108,12 +108,12 @@ public partial class ModCollection } PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l}", - Thread.CurrentThread.ManagedThreadId, Name ); + Thread.CurrentThread.ManagedThreadId, AnonymizedName ); _cache ??= new Cache( this ); _cache.FullRecalculation(); PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", - Thread.CurrentThread.ManagedThreadId, Name ); + Thread.CurrentThread.ManagedThreadId, AnonymizedName ); } // Set Metadata files. diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 395da60a..b778655b 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -81,7 +81,7 @@ public partial class ModCollection collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Added {InheritedName:l} to {Name:l} inheritances.", collection.Name, Name ); + PluginLog.Debug( "Added {InheritedName:l} to {Name:l} inheritances.", collection.AnonymizedName, AnonymizedName ); return true; } @@ -92,7 +92,7 @@ public partial class ModCollection inheritance.InheritanceChanged -= OnInheritedInheritanceChange; _inheritance.RemoveAt( idx ); InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Removed {InheritedName:l} from {Name:l} inheritances.", inheritance.Name, Name ); + PluginLog.Debug( "Removed {InheritedName:l} from {Name:l} inheritances.", inheritance.AnonymizedName, AnonymizedName ); } // Order in the inheritance list is relevant. @@ -101,7 +101,7 @@ public partial class ModCollection if( _inheritance.Move( from, to ) ) { InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Moved {Name:l}s inheritance {From} to {To}.", Name, from, to ); + PluginLog.Debug( "Moved {Name:l}s inheritance {From} to {To}.", AnonymizedName, from, to ); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 0d485116..c31a6fb6 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -22,6 +22,11 @@ public partial class ModCollection // The collection name can contain invalid path characters, // but after removing those and going to lower case it has to be unique. public string Name { get; private init; } + + // Get the first two letters of a collection name and its Index (or None if it is the empty collection). + public string AnonymizedName + => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; + public int Version { get; private set; } public int Index { get; private set; } = -1; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 6258dc13..87b79fe9 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -28,7 +28,7 @@ public unsafe partial class ResourceLoader { if( handle != null ) { - PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}) .", + PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}).", handle->FileName, ( ulong )handle, handle->RefCount ); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 7aa61f30..985a73ca 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -436,42 +436,36 @@ public class Penumbra : IDalamudPlugin ModManager.Sum( m => m.TotalManipulations ) ); sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions ); - string CollectionName( ModCollection c ) - => c == ModCollection.Empty ? ModCollection.Empty.Name : c.Name.Length >= 2 ? c.Name[ ..2 ] : c.Name; - string CharacterName( string name ) => string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ) + ':'; void PrintCollection( ModCollection c ) - => sb.AppendFormat( "**Collection {0}... ({1})**\n" - + "> **`Inheritances: `** {2}\n" - + "> **`Enabled Mods: `** {3}\n" - + "> **`Total Conflicts: `** {4}\n" - + "> **`Solved Conflicts: `** {5}\n", - CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), + => sb.AppendFormat( "**Collection {0}**\n" + + "> **`Inheritances: `** {1}\n" + + "> **`Enabled Mods: `** {2}\n" + + "> **`Total Conflicts: `** {3}\n" + + "> **`Solved Conflicts: `** {4}\n", + c.AnonymizedName, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count ), c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); sb.AppendLine( "**Collections**" ); sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); - sb.AppendFormat( "> **`Default Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Default ), - CollectionManager.Default.Index ); - sb.AppendFormat( "> **`Current Collection: `** {0}... ({1})\n", CollectionName( CollectionManager.Current ), - CollectionManager.Current.Index ); + sb.AppendFormat( "> **`Default Collection: `** {0}\n", CollectionManager.Default.AnonymizedName); + sb.AppendFormat( "> **`Current Collection: `** {0}\n", CollectionManager.Current.AnonymizedName); foreach( var type in CollectionTypeExtensions.Special ) { var collection = CollectionManager.ByType( type ); if( collection != null ) { - sb.AppendFormat( "> **`{0,-29}`** {1}... ({2})\n", type.ToName(), CollectionName( collection ), collection.Index ); + sb.AppendFormat( "> **`{0,-29}`** {1}\n", type.ToName(), collection.AnonymizedName ); } } foreach( var (name, collection) in CollectionManager.Characters ) { - sb.AppendFormat( "> **`{2,-29}`** {0}... ({1})\n", CollectionName( collection ), - collection.Index, CharacterName( name ) ); + sb.AppendFormat( "> **`{2,-29}`** {0}\n", collection.AnonymizedName, CharacterName( name ) ); } foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) From d1f0f4490c58b880a3de8557f565138b831b19cf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Jul 2022 16:58:52 +0200 Subject: [PATCH 0372/2451] Fix tooltip when no advanced editing. --- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 29 +++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index a7af5ae4..0d90693a 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -56,21 +56,24 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); - if( Penumbra.Config.ShowAdvanced && ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) + if( Penumbra.Config.ShowAdvanced ) { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( -1, 0 ); - _window.ModEditPopup.IsOpen = true; - } + if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) + { + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( -1, 0 ); + _window.ModEditPopup.IsOpen = true; + } - ImGuiUtil.HoverTooltip( - "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" - + "\t\t- file redirections\n" - + "\t\t- file swaps\n" - + "\t\t- metadata manipulations\n" - + "\t\t- model materials\n" - + "\t\t- duplicates\n" - + "\t\t- textures" ); + ImGuiUtil.HoverTooltip( + "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" + + "\t\t- file redirections\n" + + "\t\t- file swaps\n" + + "\t\t- metadata manipulations\n" + + "\t\t- model materials\n" + + "\t\t- duplicates\n" + + "\t\t- textures" ); + } } // Just a simple text box with the wrapped description, if it exists. From be2260dc5184cfbc7425a00c7c2d9e8d06f51c86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Jul 2022 17:02:22 +0200 Subject: [PATCH 0373/2451] Updates for Dalamud update. --- OtterGui | 2 +- Penumbra/Import/Dds/DdsFile.cs | 54 +++++++++---------- Penumbra/Import/Dds/TextureImporter.cs | 2 +- Penumbra/Interop/Loader/ResourceLoader.cs | 5 +- Penumbra/Interop/MetaFileManager.cs | 2 +- .../Resolver/PathResolver.Demihuman.cs | 24 ++++----- .../Interop/Resolver/PathResolver.Human.cs | 24 ++++----- .../Interop/Resolver/PathResolver.Meta.cs | 2 +- .../Interop/Resolver/PathResolver.Monster.cs | 24 ++++----- .../Interop/Resolver/PathResolver.Weapon.cs | 24 ++++----- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 4 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 2 +- Penumbra/UI/ConfigWindow.ResourceTab.cs | 2 +- 13 files changed, 86 insertions(+), 85 deletions(-) diff --git a/OtterGui b/OtterGui index 30dc6a53..7e3b42b4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 30dc6a538ce56213cb59802869b54247c18a7372 +Subproject commit 7e3b42b43b1203d17427d67c85ebd3712506cc71 diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs index ec67ea3b..f9eca15f 100644 --- a/Penumbra/Import/Dds/DdsFile.cs +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -142,7 +142,7 @@ public class DdsFile using( var bw = new BinaryWriter( mem ) ) { var (format, mipLength) = WriteTexHeader( bw ); - var bytes = format == TexFile.TextureFormat.R8G8B8X8 ? RgbaData : _data; + var bytes = format == TexFile.TextureFormat.B8G8R8X8 ? RgbaData : _data; if( bytes.Length < mipLength ) { @@ -165,7 +165,7 @@ public class DdsFile } var mipCount = Header.MipMapCount; - if( format == TexFile.TextureFormat.R8G8B8X8 && ParseType != ParseType.R8G8B8A8 ) + if( format == TexFile.TextureFormat.B8G8R8X8 && ParseType != ParseType.R8G8B8A8 ) { mipCount = 1; } @@ -206,21 +206,21 @@ public class DdsFile 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.BC4 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.BC5 => ( TexFile.TextureFormat.B8G8R8X8, 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 ), + ParseType.R4G4B4A4 => ( TexFile.TextureFormat.B4G4R4A4, height * width * 2 ), + ParseType.B4G4R4A4 => ( TexFile.TextureFormat.B4G4R4A4, height * width * 2 ), + ParseType.R5G5B5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.B5G5R5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.R5G6B5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.B5G6R5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.R5G5B5A1 => ( TexFile.TextureFormat.B5G5R5A1, height * width * 2 ), + ParseType.B5G5R5A1 => ( TexFile.TextureFormat.B5G5R5A1, height * width * 2 ), + ParseType.R8G8B8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.B8G8R8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.R8G8B8A8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), + ParseType.B8G8R8A8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; } @@ -237,17 +237,17 @@ public class TmpTexFile 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(), + 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.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.B8G8R8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + //TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.B4G4R4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ), + TexFile.TextureFormat.B5G5R5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), }; } } \ No newline at end of file diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs index 7a28ee7a..8a194999 100644 --- a/Penumbra/Import/Dds/TextureImporter.cs +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -13,7 +13,7 @@ public class TextureImporter 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( ( uint )TexFile.TextureFormat.B8G8R8X8 ); bw.Write( ( ushort )width ); bw.Write( ( ushort )height ); bw.Write( ( ushort )1 ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index f36b90c5..059c1b5d 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -103,9 +103,10 @@ public unsafe partial class ResourceLoader : IDisposable public ResourceLoader( Penumbra _ ) { SignatureHelper.Initialise( this ); - _decRefHook = new Hook< ResourceHandleDecRef >( ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpDecRef, + _decRefHook = Hook< ResourceHandleDecRef >.FromAddress( + ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpDecRef, ResourceHandleDecRefDetour ); - _incRefHook = new Hook< ResourceHandleDestructor >( + _incRefHook = Hook< ResourceHandleDestructor >.FromAddress( ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpIncRef, ResourceHandleIncRefDetour ); } diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs index 38413124..71b31c72 100644 --- a/Penumbra/Interop/MetaFileManager.cs +++ b/Penumbra/Interop/MetaFileManager.cs @@ -104,7 +104,7 @@ public unsafe class MetaFileManager : IDisposable // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. private void InitImc() { - ClearDefaultResourceHook = new Hook< ClearResource >( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); + ClearDefaultResourceHook = Hook< ClearResource >.FromAddress( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); ClearDefaultResourceHook.Enable(); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs b/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs index c938bb72..dbe57a7b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs @@ -24,18 +24,18 @@ public unsafe partial class PathResolver private void SetupDemiHooks() { - ResolveDemiDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour ); - ResolveDemiEidPathHook = new Hook< EidResolveDelegate >( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour ); - ResolveDemiImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour ); - ResolveDemiMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour ); - ResolveDemiMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour ); - ResolveDemiMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour ); - ResolveDemiPapPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour ); - ResolveDemiPhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour ); - ResolveDemiSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour ); - ResolveDemiSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour ); - ResolveDemiTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour ); - ResolveDemiVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour ); + ResolveDemiDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour ); + ResolveDemiEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour ); + ResolveDemiImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour ); + ResolveDemiMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour ); + ResolveDemiMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour ); + ResolveDemiMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour ); + ResolveDemiPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour ); + ResolveDemiPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour ); + ResolveDemiSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour ); + ResolveDemiSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour ); + ResolveDemiTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour ); + ResolveDemiVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour ); } private void EnableDemiHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Human.cs b/Penumbra/Interop/Resolver/PathResolver.Human.cs index b4bb8fcd..c1278436 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Human.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Human.cs @@ -54,18 +54,18 @@ public unsafe partial class PathResolver private void SetupHumanHooks() { - ResolveDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour ); - ResolveEidPathHook = new Hook< EidResolveDelegate >( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour ); - ResolveImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour ); - ResolveMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour ); - ResolveMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour ); - ResolveMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour ); - ResolvePapPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour ); - ResolvePhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour ); - ResolveSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour ); - ResolveSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour ); - ResolveTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour ); - ResolveVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour ); + ResolveDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour ); + ResolveEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour ); + ResolveImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour ); + ResolveMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour ); + ResolveMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour ); + ResolveMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour ); + ResolvePapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour ); + ResolvePhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour ); + ResolveSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour ); + ResolveSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour ); + ResolveTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour ); + ResolveVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour ); } private void EnableHumanHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 0ccfb3e1..670c4044 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -122,7 +122,7 @@ public unsafe partial class PathResolver private void SetupMetaHooks() { OnModelLoadCompleteHook = - new Hook< OnModelLoadCompleteDelegate >( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour ); + Hook< OnModelLoadCompleteDelegate >.FromAddress( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour ); } private void EnableMetaHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Monster.cs b/Penumbra/Interop/Resolver/PathResolver.Monster.cs index f7baa229..5ef487a6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Monster.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Monster.cs @@ -24,18 +24,18 @@ public unsafe partial class PathResolver private void SetupMonsterHooks() { - ResolveMonsterDecalPathHook = new Hook( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour ); - ResolveMonsterEidPathHook = new Hook( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour ); - ResolveMonsterImcPathHook = new Hook( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour ); - ResolveMonsterMPapPathHook = new Hook( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour ); - ResolveMonsterMdlPathHook = new Hook( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour ); - ResolveMonsterMtrlPathHook = new Hook( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour ); - ResolveMonsterPapPathHook = new Hook( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour ); - ResolveMonsterPhybPathHook = new Hook( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour ); - ResolveMonsterSklbPathHook = new Hook( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour ); - ResolveMonsterSkpPathHook = new Hook( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour ); - ResolveMonsterTmbPathHook = new Hook( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour ); - ResolveMonsterVfxPathHook = new Hook( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour ); + ResolveMonsterDecalPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour ); + ResolveMonsterEidPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour ); + ResolveMonsterImcPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour ); + ResolveMonsterMPapPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour ); + ResolveMonsterMdlPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour ); + ResolveMonsterMtrlPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour ); + ResolveMonsterPapPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour ); + ResolveMonsterPhybPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour ); + ResolveMonsterSklbPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour ); + ResolveMonsterSkpPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour ); + ResolveMonsterTmbPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour ); + ResolveMonsterVfxPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour ); } private void EnableMonsterHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs index 0a9ac900..364bfa9f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs @@ -25,18 +25,18 @@ public unsafe partial class PathResolver private void SetupWeaponHooks() { - ResolveWeaponDecalPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour ); - ResolveWeaponEidPathHook = new Hook< EidResolveDelegate >( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour ); - ResolveWeaponImcPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour ); - ResolveWeaponMPapPathHook = new Hook< MPapResolveDelegate >( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour ); - ResolveWeaponMdlPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour ); - ResolveWeaponMtrlPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour ); - ResolveWeaponPapPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour ); - ResolveWeaponPhybPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour ); - ResolveWeaponSklbPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour ); - ResolveWeaponSkpPathHook = new Hook< GeneralResolveDelegate >( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour ); - ResolveWeaponTmbPathHook = new Hook< EidResolveDelegate >( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour ); - ResolveWeaponVfxPathHook = new Hook< MaterialResolveDetour >( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour ); + ResolveWeaponDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour ); + ResolveWeaponEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour ); + ResolveWeaponImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour ); + ResolveWeaponMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour ); + ResolveWeaponMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour ); + ResolveWeaponMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour ); + ResolveWeaponPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour ); + ResolveWeaponPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour ); + ResolveWeaponSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour ); + ResolveWeaponSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour ); + ResolveWeaponTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour ); + ResolveWeaponVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour ); } private void EnableWeaponHooks() diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index e18ec33c..4b8f9d7f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -181,7 +181,7 @@ public partial class ModEditWindow return ( null, 0, 0 ); } - var rgba = tex.Header.Format == TexFile.TextureFormat.A8R8G8B8 + var rgba = tex.Header.Format == TexFile.TextureFormat.B8G8R8A8 ? ImageParsing.DecodeUncompressedR8G8B8A8( tex.ImageData, tex.Header.Height, tex.Header.Width ) : tex.GetRgbaImageData(); return ( rgba, tex.Header.Width, tex.Header.Height ); @@ -436,7 +436,7 @@ public partial class ModEditWindow return; } - var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionWidth() - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 ); + var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 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 ) ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index c075af5a..56b4a58e 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -336,7 +336,7 @@ public partial class ConfigWindow ImGui.SameLine(); if( ImGui.Button( "Cancel", buttonSize ) - || ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) + || ImGui.IsKeyPressed( ImGuiKey.Escape ) ) { _newDescriptionIdx = Input.None; _newDescription = string.Empty; diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 662a98dd..f72a5dc6 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -147,7 +147,7 @@ public partial class ConfigWindow private void SetTableWidths() { _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; - _pathColumnWidth = ImGui.GetWindowContentRegionWidth() - 300 * ImGuiHelpers.GlobalScale; + _pathColumnWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 300 * ImGuiHelpers.GlobalScale; _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; } } From a47a14fe95f097e6116883ad3984ff1c8622c806 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Jul 2022 18:16:04 +0200 Subject: [PATCH 0374/2451] Use correct collections on login screen. --- .../Interop/Resolver/PathResolver.Data.cs | 90 ++++++++++--------- Penumbra/UI/ConfigWindow.DebugTab.cs | 10 ++- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index e5ac64cd..84272421 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -11,6 +11,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Data.Parsing.Uld; using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.ByteString; @@ -151,14 +152,11 @@ public unsafe partial class PathResolver // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) { - var tmp = Dalamud.Objects[ gameObjectIdx ]; - if( tmp != null ) + gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + var draw = ( DrawObject* )drawObject; + if( gameObject != null && gameObject->DrawObject == draw || gameObject->DrawObject == draw->Object.ParentObject ) { - gameObject = ( GameObject* )tmp.Address; - if( gameObject->DrawObject == ( DrawObject* )drawObject ) - { - return true; - } + return true; } gameObject = null; @@ -294,45 +292,56 @@ public unsafe partial class PathResolver try { - // Housing Retainers - if( Penumbra.Config.UseDefaultCollectionForRetainers - && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc - && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer + // Login screen. Names are populated after actors are drawn, + // so it is not possible to fetch names from the ui list. + // Actors are also not named. So use Yourself > Players > Racial > Default. + if( !Dalamud.ClientState.IsLoggedIn ) { - return Penumbra.CollectionManager.Default; + return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default ); } - - string? actorName = null; - if( Penumbra.Config.PreferNamedCollectionsOverOwners ) + else { - // Early return if we prefer the actors own name over its owner. - actorName = new Utf8String( gameObject->Name ).ToString(); - if( actorName.Length > 0 - && CollectionByActorName( actorName, out var actorCollection ) ) + // Housing Retainers + if( Penumbra.Config.UseDefaultCollectionForRetainers + && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc + && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer { - return actorCollection; + return Penumbra.CollectionManager.Default; } - } - // All these special cases are relevant for an empty name, so never collide with the above setting. - // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. - var actualName = gameObject->ObjectIndex switch + string? actorName = null; + if( Penumbra.Config.PreferNamedCollectionsOverOwners ) { - 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - >= 200 => GetCutsceneName( gameObject ), - _ => null, + // Early return if we prefer the actors own name over its owner. + actorName = new Utf8String( gameObject->Name ).ToString(); + if( actorName.Length > 0 + && CollectionByActorName( actorName, out var actorCollection ) ) + { + return actorCollection; + } } - ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - // First check temporary character collections, then the own configuration, then special collections. - return CollectionByActorName( actualName, out var c ) - ? c - : CollectionByActor( actualName, gameObject, out c ) + // All these special cases are relevant for an empty name, so never collide with the above setting. + // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. + var actualName = gameObject->ObjectIndex switch + { + 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview + >= 200 => GetCutsceneName( gameObject ), + _ => null, + } + ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); + + // First check temporary character collections, then the own configuration, then special collections. + return CollectionByActorName( actualName, out var c ) ? c - : Penumbra.CollectionManager.Default; + : CollectionByActor( actualName, gameObject, out c ) + ? c + : Penumbra.CollectionManager.Default; + } } catch( Exception e ) { @@ -459,7 +468,7 @@ public unsafe partial class PathResolver // Update collections linked to Game/DrawObjects due to a change in collection configuration. private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) { - if( type is not (CollectionType.Character or CollectionType.Default) ) + if( type is CollectionType.Inactive or CollectionType.Current ) { return; } @@ -515,12 +524,13 @@ public unsafe partial class PathResolver } // Find all current DrawObjects used in the GameObject table. + // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - foreach( var gameObject in Dalamud.Objects ) + for( var i = 0; i < Dalamud.Objects.Length; ++i ) { - var ptr = ( GameObject* )gameObject.Address; - if( ptr->IsCharacter() && ptr->DrawObject != null ) + var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); + if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) { DrawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); } diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index f7a3d53b..62d136ff 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -3,11 +3,13 @@ using System.IO; using System.Linq; using System.Numerics; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -166,9 +168,12 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( idx.ToString() ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); + var (address, name) = + obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); + ImGui.TextUnformatted( address ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + ImGui.TextUnformatted( name ); ImGui.TableNextColumn(); ImGui.TextUnformatted( c.Name ); } @@ -399,6 +404,7 @@ public partial class ConfigWindow { return; } + _window._penumbra.Ipc.Tester.Draw(); } From ac85c491fd8e03552e37a3b8dd19217691ca3219 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Jul 2022 23:27:19 +0200 Subject: [PATCH 0375/2451] Fix problem. --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 84272421..b4d7a476 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -154,7 +154,7 @@ public unsafe partial class PathResolver { gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); var draw = ( DrawObject* )drawObject; - if( gameObject != null && gameObject->DrawObject == draw || gameObject->DrawObject == draw->Object.ParentObject ) + if( gameObject != null && gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) { return true; } From 438f18f1b89653d3e38e4a203ba6f2c22f568fcb Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 13 Jul 2022 21:30:34 +0000 Subject: [PATCH 0376/2451] [CI] Updating repo.json for refs/tags/0.5.4.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e9e67c91..8ea9dde7 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.3.7", - "TestingAssemblyVersion": "0.5.3.7", + "AssemblyVersion": "0.5.4.0", + "TestingAssemblyVersion": "0.5.4.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.3.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 3434c437ce6e2579d4ea8bcff7b166fdb33236d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 11:02:18 +0200 Subject: [PATCH 0377/2451] Fix support info bug. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 985a73ca..a20637d4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -465,7 +465,7 @@ public class Penumbra : IDalamudPlugin foreach( var (name, collection) in CollectionManager.Characters ) { - sb.AppendFormat( "> **`{2,-29}`** {0}\n", collection.AnonymizedName, CharacterName( name ) ); + sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( name ) ); } foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) From 55b9531d935f295ae1a574e4e7ead27ce19c5f30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 11:02:37 +0200 Subject: [PATCH 0378/2451] Make both collection headers default open again --- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 75d08bd2..dbf9b02b 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -193,7 +193,7 @@ public partial class ConfigWindow private void DrawActiveCollectionSelectors() { ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( "Active Collections" ); + var open = ImGui.CollapsingHeader( "Active Collections", ImGuiTreeNodeFlags.DefaultOpen ); OpenTutorial( BasicTutorialSteps.ActiveCollections ); if( !open ) { From fff3f6d1cb37f17b8b05416da257fb55fc790652 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 11:03:30 +0200 Subject: [PATCH 0379/2451] Tutorial improvements. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 7e3b42b4..1525b782 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 7e3b42b43b1203d17427d67c85ebd3712506cc71 +Subproject commit 1525b782fffb63fa3ccab8286c335c2279f853a1 From e9a274413129f711aa47b1ecfb5521c4d3f9b5c8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 11:07:23 +0200 Subject: [PATCH 0380/2451] Fix crash. --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index b4d7a476..32bf5fbc 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -154,7 +154,7 @@ public unsafe partial class PathResolver { gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); var draw = ( DrawObject* )drawObject; - if( gameObject != null && gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) + if( gameObject != null && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) { return true; } From 08ae14222bb3ecf9f565f3a3e4cef4fc9a164624 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 17:32:13 +0200 Subject: [PATCH 0381/2451] Add filtering selecting single results --- OtterGui | 2 +- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 2 +- Penumbra/Configuration.cs | 2 +- Penumbra/Dalamud.cs | 3 +- .../Interop/Resolver/PathResolver.Material.cs | 2 +- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 4 +- Penumbra/Mods/Editor/Mod.Editor.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 2 +- Penumbra/Mods/Mod.Meta.Migration.cs | 2 +- Penumbra/Mods/Subclasses/ModSettings.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 6 +- Penumbra/Util/ArrayExtensions.cs | 84 ------------------- 14 files changed, 17 insertions(+), 100 deletions(-) delete mode 100644 Penumbra/Util/ArrayExtensions.cs diff --git a/OtterGui b/OtterGui index 1525b782..69a8ee3a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1525b782fffb63fa3ccab8286c335c2279f853a1 +Subproject commit 69a8ee3ae21480123881bc93ac0458671e7d0c46 diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 486080a5..8ab2c22b 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -5,8 +5,8 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 0715d987..1ab16c6a 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -5,9 +5,9 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; +using OtterGui; using OtterGui.Filesystem; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index a3654608..4d4c0c31 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; +using OtterGui; using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b025403f..71297304 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -5,12 +5,12 @@ using System.Linq; using Dalamud.Configuration; using Dalamud.Logging; using Newtonsoft.Json; +using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Import; using Penumbra.Mods; using Penumbra.UI.Classes; -using Penumbra.Util; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index 443c0ec0..9aa29806 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -2,6 +2,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; @@ -21,7 +22,6 @@ public class Dalamud // @formatter:off [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; @@ -31,5 +31,6 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; // @formatter:on } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index f52b4389..a93024b2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,11 +4,11 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Util; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 81417fdb..1f601ef4 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Dalamud.Logging; +using OtterGui; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; -using Penumbra.Util; namespace Penumbra.Mods; @@ -99,7 +99,7 @@ public partial class Mod private readonly MdlFile _file; private readonly string[] _currentMaterials; private readonly IReadOnlyList _materialIndices; - public bool Changed { get; private set; } = false; + public bool Changed { get; private set; } public IReadOnlyList CurrentMaterials => _currentMaterials; diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index 455dcf8e..a1394401 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using Penumbra.Util; +using OtterGui; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index e1004978..e22f887f 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -4,9 +4,9 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index bf6a681a..3f360ada 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -6,8 +6,8 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.GameData.ByteString; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 98922fdd..6d6f2f3d 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using OtterGui; using OtterGui.Filesystem; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 9db6abb2..57580c05 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -26,7 +26,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModFileSystemSelector( ModFileSystem fileSystem ) - : base( fileSystem ) + : base( fileSystem, Dalamud.KeyState ) { SubscribeRightClickFolder( EnableDescendants, 10 ); SubscribeRightClickFolder( DisableDescendants, 10 ); @@ -407,9 +407,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { if( _lastSelectedDirectory.Length > 0 ) { - base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) + var leaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); - OnSelectionChange( null, base.SelectedLeaf?.Value, default ); + Select( leaf ); _lastSelectedDirectory = string.Empty; } } diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs deleted file mode 100644 index 8953426f..00000000 --- a/Penumbra/Util/ArrayExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace Penumbra.Util; - -public static class ArrayExtensions -{ - // Iterate over enumerables with additional index. - public static IEnumerable< (T, int) > WithIndex< T >( this IEnumerable< T > list ) - => list.Select( ( x, i ) => ( x, i ) ); - - - // Find the index of the first object fulfilling predicate's criteria in the given list. - // Returns -1 if no such object is found. - public static int IndexOf< T >( this IEnumerable< T > array, Predicate< T > predicate ) - { - var i = 0; - foreach( var obj in array ) - { - if( predicate( obj ) ) - { - return i; - } - - ++i; - } - - return -1; - } - - // Find the index of the first occurrence of needle in the given list. - // Returns -1 if needle is not contained in the list. - public static int IndexOf< T >( this IEnumerable< T > array, T needle ) where T : notnull - { - var i = 0; - foreach( var obj in array ) - { - if( needle.Equals( obj ) ) - { - return i; - } - - ++i; - } - - return -1; - } - - // Find the first object fulfilling predicate's criteria in the given list, if one exists. - // Returns true if an object is found, false otherwise. - public static bool FindFirst< T >( this IEnumerable< T > array, Predicate< T > predicate, [NotNullWhen( true )] out T? result ) - { - foreach( var obj in array ) - { - if( predicate( obj ) ) - { - result = obj!; - return true; - } - } - - result = default; - return false; - } - - // Find the first occurrence of needle in the given list and return the value contained in the list in result. - // Returns true if an object is found, false otherwise. - public static bool FindFirst< T >( this IEnumerable< T > array, T needle, [NotNullWhen( true )] out T? result ) where T : notnull - { - foreach( var obj in array ) - { - if( obj.Equals( needle ) ) - { - result = obj!; - return true; - } - } - - result = default; - return false; - } -} \ No newline at end of file From 23a08f30c4bd21fd214ecd46b4fb7e8c7a166edf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 17:32:24 +0200 Subject: [PATCH 0382/2451] Fix clipboard crashes --- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index e9a75e60..33f01f64 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -874,8 +874,9 @@ public partial class ModEditWindow { if( ImGui.Button( "Add from Clipboard" ) ) { - var clipboard = ImGui.GetClipboardText(); - var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); + var clipboard = ImGuiUtil.GetClipboardText(); + + var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); if( version == MetaManipulation.CurrentVersion && manips != null ) { foreach( var manip in manips ) @@ -893,7 +894,7 @@ public partial class ModEditWindow { if( ImGui.Button( "Set from Clipboard" ) ) { - var clipboard = ImGui.GetClipboardText(); + var clipboard = ImGuiUtil.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); if( version == MetaManipulation.CurrentVersion && manips != null ) { From e261b4c0c5f75e1ed05bfbab3a5e8c3bd90932c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jul 2022 20:27:47 +0200 Subject: [PATCH 0383/2451] Maybe improve IMC handling. --- Penumbra/Interop/MetaFileManager.cs | 108 ++--------------------- Penumbra/Meta/Files/ImcFile.cs | 15 ++-- Penumbra/Meta/Manager/MetaManager.Imc.cs | 29 ++---- Penumbra/Penumbra.cs | 1 - 4 files changed, 21 insertions(+), 132 deletions(-) diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs index 71b31c72..880a0ee8 100644 --- a/Penumbra/Interop/MetaFileManager.cs +++ b/Penumbra/Interop/MetaFileManager.cs @@ -1,38 +1,23 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; -using Penumbra.GameData.ByteString; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; namespace Penumbra.Interop; -public unsafe class MetaFileManager : IDisposable +public unsafe class MetaFileManager { public MetaFileManager() { SignatureHelper.Initialise( this ); - InitImc(); } - public void Dispose() - { - DisposeImc(); - } - - // Allocate in the games space for file storage. // We only need this if using any meta file. [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] - public IntPtr GetFileSpaceAddress; + private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero; public IMemorySpace* GetFileSpace() - => ( ( delegate* unmanaged< IMemorySpace* > )GetFileSpaceAddress )(); + => ( ( delegate* unmanaged< IMemorySpace* > )_getFileSpaceAddress )(); public void* AllocateFileMemory( ulong length, ulong alignment = 0 ) => GetFileSpace()->Malloc( length, alignment ); @@ -40,87 +25,12 @@ public unsafe class MetaFileManager : IDisposable public void* AllocateFileMemory( int length, int alignment = 0 ) => AllocateFileMemory( ( ulong )length, ( ulong )alignment ); + public void* AllocateDefaultMemory( ulong length, ulong alignment = 0 ) + => GetFileSpace()->Malloc( length, alignment ); - // We only need this for IMC files, since we need to hook their cleanup function. - [Signature( "48 8D 05 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 89 03", ScanType = ScanType.StaticAddress )] - public IntPtr* DefaultResourceHandleVTable; + public void* AllocateDefaultMemory( int length, int alignment = 0 ) + => IMemorySpace.GetDefaultSpace()->Malloc( ( ulong )length, ( ulong )alignment ); - public delegate void ClearResource( ResourceHandle* resource ); - public Hook< ClearResource > ClearDefaultResourceHook = null!; - - private readonly Dictionary< IntPtr, (ImcFile, IntPtr, int) > _originalImcData = new(); - private readonly Dictionary< ImcFile, IntPtr > _currentUse = new(); - - // We store the original data of loaded IMCs so that we can restore it before they get destroyed, - // similar to the other meta files, just with arbitrary destruction. - private void ClearDefaultResourceDetour( ResourceHandle* resource ) - { - if( _originalImcData.TryGetValue( ( IntPtr )resource, out var data ) ) - { - ClearImcData( resource, data.Item1, data.Item2, data.Item3); - } - - ClearDefaultResourceHook.Original( resource ); - } - - // Reset all files from a given IMC cache if they exist. - public void ResetByFile( ImcFile file ) - { - if( !_currentUse.TryGetValue( file, out var resource ) ) - { - return; - } - - if( _originalImcData.TryGetValue( resource, out var data ) ) - { - ClearImcData((ResourceHandle*) resource, file, data.Item2, data.Item3 ); - } - else - { - _currentUse.Remove( file ); - } - } - - // Clear a single IMC resource and reset it to its original data. - private void ClearImcData( ResourceHandle* resource, ImcFile file, IntPtr data, int length) - { - var name = new FullPath( Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true ).ToString() ); - PluginLog.Debug( "Restoring data of {$Name:l} (0x{Resource}) to 0x{Data:X} and Length {Length} before deletion.", name, - ( ulong )resource, ( ulong )data, length ); - resource->SetData( data, length ); - _originalImcData.Remove( ( IntPtr )resource ); - _currentUse.Remove( file ); - } - - // Called when a new IMC is manipulated to store its data. - public void AddImcFile( ResourceHandle* resource, ImcFile file, IntPtr data, int length) - { - PluginLog.Debug( "Storing data 0x{Data:X} of Length {Length} for {$Name:l} (0x{Resource:X}).", ( ulong )data, length, - Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource ); - _originalImcData[ ( IntPtr )resource ] = ( file, data, length ); - _currentUse[ file ] = ( IntPtr )resource; - } - - // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. - private void InitImc() - { - ClearDefaultResourceHook = Hook< ClearResource >.FromAddress( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); - ClearDefaultResourceHook.Enable(); - } - - private void DisposeImc() - { - ClearDefaultResourceHook.Disable(); - ClearDefaultResourceHook.Dispose(); - // Restore all IMCs to their default values on dispose. - // This should only be relevant when testing/disabling/reenabling penumbra. - foreach( var (resourcePtr, (file, data, length)) in _originalImcData ) - { - var resource = ( ResourceHandle* )resourcePtr; - resource->SetData( data, length ); - } - - _originalImcData.Clear(); - _currentUse.Clear(); - } + public void Free( IntPtr ptr, int length ) + => IMemorySpace.Free( ( void* )ptr, ( ulong )length ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 17ced337..e8fd8416 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -128,7 +128,6 @@ public unsafe class ImcFile : MetaBaseFile var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; PluginLog.Verbose( "Resized IMC {Path} from {Length} to {NewLength}.", Path, Length, newLength ); ResizeResources( newLength ); - ChangesSinceLoad = true; } var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); @@ -218,18 +217,18 @@ public unsafe class ImcFile : MetaBaseFile } } - public void Replace( ResourceHandle* resource, bool firstTime ) + public void Replace( ResourceHandle* resource ) { var (data, length) = resource->GetData(); - if( data == IntPtr.Zero ) + var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( Length, 8 ); + if( newData == null ) { + PluginLog.Error("Could not replace loaded IMC data at 0x{Data:X}, allocation failed." ); return; } + Functions.MemCpyUnchecked( newData, Data, Length ); - resource->SetData( ( IntPtr )Data, Length ); - if( firstTime ) - { - Penumbra.MetaFileManager.AddImcFile( resource, this, data, length ); - } + Penumbra.MetaFileManager.Free( data, length ); + resource->SetData( ( IntPtr )newData, Length ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 3dd7752c..77c90023 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -119,9 +119,9 @@ public partial class MetaManager { foreach( var file in _imcFiles.Values ) { - Penumbra.MetaFileManager.ResetByFile( file ); file.Dispose(); } + _imcFiles.Clear(); _imcManipulations.Clear(); RestoreImcDelegate(); @@ -132,7 +132,6 @@ public partial class MetaManager if( _imcManagerCount++ == 0 ) { Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; - Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } } @@ -141,7 +140,6 @@ public partial class MetaManager if( --_imcManagerCount == 0 ) { Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; - Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; } } @@ -163,33 +161,16 @@ public partial class MetaManager var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.CollectionManager.ByName( name, out var collection ) + if( ( Penumbra.TempMods.Collections.TryGetValue( name, out var collection ) + || Penumbra.CollectionManager.ByName( name, out collection ) ) && collection.HasCache && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, - collection.Name ); - file.Replace( fileDescriptor->ResourceHandle, true ); - file.ChangesSinceLoad = false; + collection.AnonymizedName ); + file.Replace( fileDescriptor->ResourceHandle ); } return true; } - - private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) - { - // Only check imcs. - if( resource->FileType != ResourceType.Imc - || resolveData is not ModCollection { HasCache: true } collection - || !collection.MetaCache!._imcFiles.TryGetValue( gamePath, out var file ) - || !file.ChangesSinceLoad ) - { - return; - } - - PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, - collection.Name ); - file.Replace( resource, false ); - file.ChangesSinceLoad = false; - } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a20637d4..3a4753dd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -258,7 +258,6 @@ public class Penumbra : IDalamudPlugin PathResolver.Dispose(); ResourceLogger.Dispose(); - MetaFileManager.Dispose(); ResourceLoader.Dispose(); CharacterUtility.Dispose(); From 9508b8d9b30ae31db72fc6db3140087533eabcaf Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 14 Jul 2022 18:30:22 +0000 Subject: [PATCH 0384/2451] [CI] Updating repo.json for refs/tags/0.5.4.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8ea9dde7..faafcc28 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.4.0", - "TestingAssemblyVersion": "0.5.4.0", + "AssemblyVersion": "0.5.4.1", + "TestingAssemblyVersion": "0.5.4.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 3e9f9289e5074e904888c0e26124c7a59116495a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Jul 2022 10:19:52 +0200 Subject: [PATCH 0385/2451] Some fixes with collection inheritance. --- Penumbra/Collections/CollectionManager.cs | 13 +++++++++++-- Penumbra/Collections/ModCollection.File.cs | 10 +++++----- .../Collections/ModCollection.Inheritance.cs | 16 ++++++++++++++-- Penumbra/UI/ConfigWindow.ModsTab.cs | 9 +++++---- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 1ab16c6a..05e221d3 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -115,7 +115,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); - PluginLog.Debug( "Added collection {Name:l}.", newCollection.Name ); + PluginLog.Debug( "Added collection {Name:l}.", newCollection.AnonymizedName ); CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); SetCollection( newCollection.Index, CollectionType.Current ); return true; @@ -154,8 +154,17 @@ public partial class ModCollection } var collection = _collections[ idx ]; + + // Clear own inheritances. + foreach(var inheritance in collection.Inheritance) + { + collection.ClearSubscriptions( inheritance ); + } + collection.Delete(); _collections.RemoveAt( idx ); + + // Clear external inheritances. foreach( var c in _collections ) { var inheritedIdx = c._inheritance.IndexOf( collection ); @@ -170,7 +179,7 @@ public partial class ModCollection } } - PluginLog.Debug( "Removed collection {Name:l}.", collection.Name ); + PluginLog.Debug( "Removed collection {Name:l}.", collection.AnonymizedName ); CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); return true; } diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 5c8aa521..6f76199b 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -67,7 +67,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + PluginLog.Error( $"Could not save collection {AnonymizedName}:\n{e}" ); } } @@ -90,11 +90,11 @@ public partial class ModCollection try { file.Delete(); - PluginLog.Information( "Deleted collection file {File:l} for {Name:l}.", file.FullName, Name ); + PluginLog.Information( "Deleted collection file for {Name:l}.", AnonymizedName ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); + PluginLog.Error( $"Could not delete collection file for {AnonymizedName}:\n{e}" ); } } @@ -105,7 +105,7 @@ public partial class ModCollection inheritance = Array.Empty< string >(); if( !file.Exists ) { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + PluginLog.Error( $"Could not read collection because file does not exist." ); return null; } @@ -123,7 +123,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + PluginLog.Error( $"Could not read collection information from file:\n{e}" ); } return null; diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index b778655b..12b5c054 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -88,13 +88,18 @@ public partial class ModCollection public void RemoveInheritance( int idx ) { var inheritance = _inheritance[ idx ]; - inheritance.ModSettingChanged -= OnInheritedModSettingChange; - inheritance.InheritanceChanged -= OnInheritedInheritanceChange; + ClearSubscriptions( inheritance ); _inheritance.RemoveAt( idx ); InheritanceChanged.Invoke( false ); PluginLog.Debug( "Removed {InheritedName:l} from {Name:l} inheritances.", inheritance.AnonymizedName, AnonymizedName ); } + private void ClearSubscriptions( ModCollection other ) + { + other.ModSettingChanged -= OnInheritedModSettingChange; + other.InheritanceChanged -= OnInheritedInheritanceChange; + } + // Order in the inheritance list is relevant. public void MoveInheritance( int from, int to ) { @@ -115,6 +120,13 @@ public partial class ModCollection ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); return; default: + if( modIdx < 0 || modIdx >= _settings.Count ) + { + PluginLog.Warning( + $"Collection state broken, Mod {modIdx} in inheritance does not exist. ({_settings.Count} mods exist)." ); + return; + } + if( _settings[ modIdx ] == null ) { ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index adcd512d..5c9cb0e6 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -5,6 +5,7 @@ using Penumbra.Collections; using Penumbra.Mods; using Penumbra.UI.Classes; using System; +using System.Linq; using System.Numerics; using Dalamud.Logging; @@ -43,13 +44,13 @@ public partial class ConfigWindow { PluginLog.Error( $"Exception thrown during ModPanel Render:\n{e}" ); PluginLog.Error( $"{Penumbra.ModManager.Count} Mods\n" - + $"{Penumbra.CollectionManager.Current.Name} Current Collection\n" + + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" - + $"{_selector.SortMode} Sort Mode\n" + + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance )} Inheritances\n" - + $"{_selector.SelectedSettingCollection.Name} Collection\n" ); + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select(c => c.AnonymizedName) )} Inheritances\n" + + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); } } From 4121baac3386128d911b48cdf9649fb1ca684eba Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 15 Jul 2022 10:16:31 +0000 Subject: [PATCH 0386/2451] [CI] Updating repo.json for refs/tags/0.5.4.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index faafcc28..4900e34e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.4.1", - "TestingAssemblyVersion": "0.5.4.1", + "AssemblyVersion": "0.5.4.4", + "TestingAssemblyVersion": "0.5.4.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f915b73f8d1ad1ef121e69bb1e01fc39dcadb169 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Jul 2022 01:34:01 +0200 Subject: [PATCH 0387/2451] Fix garbage IMC files --- Penumbra/Meta/Files/ImcFile.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index e8fd8416..30c21dc1 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -220,15 +220,15 @@ public unsafe class ImcFile : MetaBaseFile public void Replace( ResourceHandle* resource ) { var (data, length) = resource->GetData(); - var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( Length, 8 ); + var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( ActualLength, 8 ); if( newData == null ) { PluginLog.Error("Could not replace loaded IMC data at 0x{Data:X}, allocation failed." ); return; } - Functions.MemCpyUnchecked( newData, Data, Length ); + Functions.MemCpyUnchecked( newData, Data, ActualLength ); Penumbra.MetaFileManager.Free( data, length ); - resource->SetData( ( IntPtr )newData, Length ); + resource->SetData( ( IntPtr )newData, ActualLength ); } } \ No newline at end of file From 3d7cf9fc93fbcbf5f0e468c4667b3edbd2a917f5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Jul 2022 23:35:33 +0200 Subject: [PATCH 0388/2451] Update Serenitys guide address. --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index bcb2e4f1..48adefa4 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -277,7 +277,7 @@ public partial class ConfigWindow private static void DrawGuideButton( float width ) { - const string address = @"https://penumbra.ju.mp"; + const string address = @"https://reniguide.info/"; using var color = ImRaii.PushColor( ImGuiCol.Button, 0xFFCC648D ) .Push( ImGuiCol.ButtonHovered, 0xFFB070B0 ) .Push( ImGuiCol.ButtonActive, 0xFF9070E0 ); From cfc441b9b1acaaf1bf2da758bd875cfb5229ab60 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 16 Jul 2022 21:37:47 +0000 Subject: [PATCH 0389/2451] [CI] Updating repo.json for refs/tags/0.5.4.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4900e34e..6fa040f3 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.4.4", - "TestingAssemblyVersion": "0.5.4.4", + "AssemblyVersion": "0.5.4.6", + "TestingAssemblyVersion": "0.5.4.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 00e1736d13c66aa0844dbd5e38e23bcecec80186 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Sun, 17 Jul 2022 12:49:52 +0200 Subject: [PATCH 0390/2451] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 755280b5..d4be2605 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Penumbra has support for most TexTools modpacks however this is provided on a be ## Installing While this project is still a work in progress, you can use it by adding the following URL to the custom plugin repositories list in your Dalamud settings -An image-based install (and usage) guide to do this is provided by unaffiliated user Serenity: https://penumbra.ju.mp/ +An image-based install (and usage) guide to do this is provided by unaffiliated user Serenity: https://reniguide.info/ 1. `/xlsettings` -> Experimental tab 2. Copy and paste the repo.json link below From 39d339a3d821488eb3348fdd0c717218d92729c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Jul 2022 16:51:03 +0200 Subject: [PATCH 0391/2451] Make IMC handler load temp collections correctly. --- Penumbra/Api/PenumbraApi.cs | 4 ++-- Penumbra/Api/TempModManager.cs | 6 ++++++ Penumbra/Interop/Resolver/PathResolver.Material.cs | 3 +-- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index a620ff54..b9b18c70 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -474,7 +474,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi int priority ) { CheckInitialized(); - if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) + if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; @@ -511,7 +511,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) { CheckInitialized(); - if( !Penumbra.TempMods.Collections.Values.FindFirst( c => c.Name == collectionName, out var collection ) + if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 996306e8..9fb40ec6 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -29,6 +32,9 @@ public class TempModManager public IReadOnlyDictionary< string, ModCollection > Collections => _collections; + public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) + => Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); + // These functions to check specific redirections or meta manipulations for existence are currently unused. //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) //{ diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index a93024b2..450eceab 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -82,8 +82,7 @@ public unsafe partial class PathResolver var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.TempMods.Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), - out var collection ) + if( Penumbra.TempMods.CollectionByName( name, out var collection ) || Penumbra.CollectionManager.ByName( name, out collection ) ) { #if DEBUG diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 77c90023..517681ff 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -161,7 +161,7 @@ public partial class MetaManager var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( ( Penumbra.TempMods.Collections.TryGetValue( name, out var collection ) + if( ( Penumbra.TempMods.CollectionByName( name, out var collection ) || Penumbra.CollectionManager.ByName( name, out collection ) ) && collection.HasCache && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) From 28e0affbb4152a6151f4a34e561ba5577d4dbd0f Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 18 Jul 2022 10:17:01 +0000 Subject: [PATCH 0392/2451] [CI] Updating repo.json for refs/tags/0.5.4.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6fa040f3..5af070ce 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.4.6", - "TestingAssemblyVersion": "0.5.4.6", + "AssemblyVersion": "0.5.4.8", + "TestingAssemblyVersion": "0.5.4.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f808c8a47178362deaa54ca727d02d5536c85cab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 10:01:24 +0200 Subject: [PATCH 0393/2451] Compare cutscene actors by customization. --- Penumbra.GameData/Structs/CharacterEquip.cs | 105 +++++++++++++ .../Structs/CharacterEquipment.cs | 148 ------------------ Penumbra.GameData/Structs/CustomizeData.cs | 55 +++++++ .../Interop/Resolver/PathResolver.Data.cs | 7 +- 4 files changed, 164 insertions(+), 151 deletions(-) create mode 100644 Penumbra.GameData/Structs/CharacterEquip.cs delete mode 100644 Penumbra.GameData/Structs/CharacterEquipment.cs create mode 100644 Penumbra.GameData/Structs/CustomizeData.cs diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs new file mode 100644 index 00000000..c16a69a5 --- /dev/null +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -0,0 +1,105 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer; + +public readonly unsafe struct CharacterEquip +{ + public static readonly CharacterEquip Null = new(null); + + private readonly CharacterArmor* _armor; + + public IntPtr Address + => (IntPtr)_armor; + + public ref CharacterArmor this[int idx] + => ref _armor[idx]; + + public ref CharacterArmor this[uint idx] + => ref _armor[idx]; + + public ref CharacterArmor this[EquipSlot slot] + => ref _armor[IndexOf(slot)]; + + + public ref CharacterArmor Head + => ref _armor[0]; + + public ref CharacterArmor Body + => ref _armor[1]; + + public ref CharacterArmor Hands + => ref _armor[2]; + + public ref CharacterArmor Legs + => ref _armor[3]; + + public ref CharacterArmor Feet + => ref _armor[4]; + + public ref CharacterArmor Ears + => ref _armor[5]; + + public ref CharacterArmor Neck + => ref _armor[6]; + + public ref CharacterArmor Wrists + => ref _armor[7]; + + public ref CharacterArmor RFinger + => ref _armor[8]; + + public ref CharacterArmor LFinger + => ref _armor[9]; + + public CharacterEquip(CharacterArmor* val) + => _armor = val; + + public static implicit operator CharacterEquip(CharacterArmor* val) + => new(val); + + public static implicit operator CharacterEquip(IntPtr val) + => new((CharacterArmor*)val); + + public static implicit operator CharacterEquip(ReadOnlySpan val) + { + if (val.Length != 10) + throw new ArgumentException("Invalid number of equipment pieces in span."); + + fixed (CharacterArmor* ptr = val) + { + return new CharacterEquip(ptr); + } + } + + public static implicit operator bool(CharacterEquip equip) + => equip._armor != null; + + public static bool operator true(CharacterEquip equip) + => equip._armor != null; + + public static bool operator false(CharacterEquip equip) + => equip._armor == null; + + public static bool operator !(CharacterEquip equip) + => equip._armor == null; + + private static int IndexOf(EquipSlot slot) + { + return slot switch + { + EquipSlot.Head => 0, + EquipSlot.Body => 1, + EquipSlot.Hands => 2, + EquipSlot.Legs => 3, + EquipSlot.Feet => 4, + EquipSlot.Ears => 5, + EquipSlot.Neck => 6, + EquipSlot.Wrists => 7, + EquipSlot.RFinger => 8, + EquipSlot.LFinger => 9, + _ => throw new ArgumentOutOfRangeException(nameof(slot), slot, null), + }; + } +} diff --git a/Penumbra.GameData/Structs/CharacterEquipment.cs b/Penumbra.GameData/Structs/CharacterEquipment.cs deleted file mode 100644 index 357ce36b..00000000 --- a/Penumbra.GameData/Structs/CharacterEquipment.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Objects.Types; - -// Read the customization data regarding weapons and displayable equipment from an actor struct. -// Stores the data in a 56 bytes, i.e. 7 longs for easier comparison. -namespace Penumbra.GameData.Structs; - -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public class CharacterEquipment -{ - public const int MainWeaponOffset = 0x6D0; - public const int OffWeaponOffset = 0x738; - public const int EquipmentOffset = 0x808; - public const int EquipmentSlots = 10; - public const int WeaponSlots = 2; - - public CharacterWeapon MainHand; - public CharacterWeapon OffHand; - public CharacterArmor Head; - public CharacterArmor Body; - public CharacterArmor Hands; - public CharacterArmor Legs; - public CharacterArmor Feet; - public CharacterArmor Ears; - public CharacterArmor Neck; - public CharacterArmor Wrists; - public CharacterArmor RFinger; - public CharacterArmor LFinger; - public ushort IsSet; // Also fills struct size to 56, a multiple of 8. - - public CharacterEquipment() - => Clear(); - - public CharacterEquipment( Character actor ) - : this( actor.Address ) - { } - - public override string ToString() - => IsSet == 0 - ? "(Not Set)" - : $"({MainHand}) | ({OffHand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | " - + $"({Feet}) | ({Ears}) | ({Neck}) | ({Wrists}) | ({LFinger}) | ({RFinger})"; - - public bool Equal( Character rhs ) - => CompareData( new CharacterEquipment( rhs ) ); - - public bool Equal( CharacterEquipment rhs ) - => CompareData( rhs ); - - public bool CompareAndUpdate( Character rhs ) - => CompareAndOverwrite( new CharacterEquipment( rhs ) ); - - public bool CompareAndUpdate( CharacterEquipment rhs ) - => CompareAndOverwrite( rhs ); - - private unsafe CharacterEquipment( IntPtr actorAddress ) - { - IsSet = 1; - var actorPtr = ( byte* )actorAddress.ToPointer(); - fixed( CharacterWeapon* main = &MainHand, off = &OffHand ) - { - Buffer.MemoryCopy( actorPtr + MainWeaponOffset, main, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); - Buffer.MemoryCopy( actorPtr + OffWeaponOffset, off, sizeof( CharacterWeapon ), sizeof( CharacterWeapon ) ); - } - - fixed( CharacterArmor* equipment = &Head ) - { - Buffer.MemoryCopy( actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof( CharacterArmor ), - EquipmentSlots * sizeof( CharacterArmor ) ); - } - } - - public unsafe void Clear() - { - fixed( CharacterWeapon* main = &MainHand ) - { - var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - for( ulong* ptr = ( ulong* )main, end = ptr + structSizeEights; ptr != end; ++ptr ) - { - *ptr = 0; - } - } - } - - private unsafe bool CompareAndOverwrite( CharacterEquipment rhs ) - { - var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - var ret = true; - fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) - { - var ptr1 = ( ulong* )data1; - var ptr2 = ( ulong* )data2; - for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) - { - if( *ptr1 != *ptr2 ) - { - *ptr1 = *ptr2; - ret = false; - } - } - } - - return ret; - } - - private unsafe bool CompareData( CharacterEquipment rhs ) - { - var structSizeEights = ( 2 + EquipmentSlots * sizeof( CharacterArmor ) + WeaponSlots * sizeof( CharacterWeapon ) ) / 8; - fixed( CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand ) - { - var ptr1 = ( ulong* )data1; - var ptr2 = ( ulong* )data2; - for( var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2 ) - { - if( *ptr1 != *ptr2 ) - { - return false; - } - } - } - - return true; - } - - public unsafe void WriteBytes( byte[] array, int offset = 0 ) - { - fixed( CharacterWeapon* data = &MainHand ) - { - Marshal.Copy( new IntPtr( data ), array, offset, 56 ); - } - } - - public byte[] ToBytes() - { - var ret = new byte[56]; - WriteBytes( ret ); - return ret; - } - - public unsafe void FromBytes( byte[] array, int offset = 0 ) - { - fixed( CharacterWeapon* data = &MainHand ) - { - Marshal.Copy( array, offset, new IntPtr( data ), 56 ); - } - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs new file mode 100644 index 00000000..0c7fcbf1 --- /dev/null +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -0,0 +1,55 @@ +using System; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.Structs; + +public unsafe struct CustomizeData : IEquatable< CustomizeData > +{ + public const int Size = 26; + + public fixed byte Data[Size]; + + public void Read( void* source ) + { + fixed( byte* ptr = Data ) + { + Functions.MemCpyUnchecked( ptr, source, Size ); + } + } + + public void Write( void* target ) + { + fixed( byte* ptr = Data ) + { + Functions.MemCpyUnchecked( target, ptr, Size ); + } + } + + public CustomizeData Clone() + { + var ret = new CustomizeData(); + Write( ret.Data ); + return ret; + } + + public bool Equals( CustomizeData other ) + { + fixed( byte* ptr = Data ) + { + return Functions.MemCmpUnchecked( ptr, other.Data, Size ) == 0; + } + } + + public override bool Equals( object? obj ) + => obj is CustomizeData other && Equals( other ); + + public override int GetHashCode() + { + fixed( byte* ptr = Data ) + { + var p = ( int* )ptr; + var u = *( ushort* )( p + 6 ); + return HashCode.Combine( *p, p[ 1 ], p[ 2 ], p[ 3 ], p[ 4 ], p[ 5 ], u ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 32bf5fbc..0a3d7fa2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -11,11 +11,11 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Data.Parsing.Uld; using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; @@ -250,8 +250,9 @@ public unsafe partial class PathResolver return null; } - var pc = ( Character* )player.Address; - return pc->ClassJob == ( ( Character* )gameObject )->ClassJob ? player.Name.ToString() : null; + var customize1 = ( CustomizeData* )( ( Character* )gameObject )->CustomizeData; + var customize2 = ( CustomizeData* )( ( Character* )player.Address )->CustomizeData; + return customize1->Equals( *customize2 ) ? player.Name.ToString() : null; } // Identify the owner of a companion, mount or monster and apply the corresponding collection. From c2bc8252f1affa818c5b64eebcb1e06e42cb6b1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 10:07:20 +0200 Subject: [PATCH 0394/2451] Allow only valid characters when creating collections. --- Penumbra/Collections/CollectionManager.cs | 4 ++-- Penumbra/Collections/ModCollection.cs | 8 +++++++- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 05e221d3..9bc2138c 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -79,7 +79,7 @@ public partial class ModCollection // and no existing collection results in the same filename as name. public bool CanAddCollection( string name, out string fixedName ) { - if( name.Length == 0 ) + if( !IsValidName( name ) ) { fixedName = string.Empty; return false; @@ -156,7 +156,7 @@ public partial class ModCollection var collection = _collections[ idx ]; // Clear own inheritances. - foreach(var inheritance in collection.Inheritance) + foreach( var inheritance in collection.Inheritance ) { collection.ClearSubscriptions( inheritance ); } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index c31a6fb6..5394efcc 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OtterGui.Filesystem; using Penumbra.Mods; namespace Penumbra.Collections; @@ -25,7 +26,7 @@ public partial class ModCollection // Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; + => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[ ..2 ]}... ({Index})" : $"{Name} ({Index})"; public int Version { get; private set; } public int Index { get; private set; } = -1; @@ -94,6 +95,11 @@ public partial class ModCollection public ModCollection Duplicate( string name ) => new(name, this); + // Check if a name is valid to use for a collection. + // Does not check for uniqueness. + public static bool IsValidName( string name ) + => name.Length > 0 && name.All( c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath() ); + // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index dbf9b02b..09ad7796 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -85,7 +85,7 @@ public partial class ConfigWindow + "You can use multiple collections to quickly switch between sets of mods." ); // Creation buttons. - var tt = _canAddCollection ? string.Empty : "Please enter a unique name before creating a collection."; + var tt = _canAddCollection ? string.Empty : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; if( ImGuiUtil.DrawDisabledButton( "Create New Empty Collection", Vector2.Zero, tt, !_canAddCollection ) ) { CreateNewCollection( false ); From 9dee0862cc780a764c34e78c94d01db4076040a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 10:07:52 +0200 Subject: [PATCH 0395/2451] Some early glamourer changes. --- Penumbra.GameData/Enums/EquipSlot.cs | 17 + Penumbra.GameData/Structs/CharacterArmor.cs | 32 +- Penumbra.GameData/Structs/CharacterWeapon.cs | 57 +++- Penumbra.GameData/Structs/EqpEntry.cs | 12 +- Penumbra.GameData/Structs/RspEntry.cs | 85 +++-- Penumbra.GameData/Structs/SetId.cs | 29 +- Penumbra.GameData/Structs/StainId.cs | 37 +- Penumbra.GameData/Structs/WeaponType.cs | 37 +- Penumbra.PlayerWatch/CharacterFactory.cs | 63 ---- Penumbra.PlayerWatch/IPlayerWatcher.cs | 30 -- .../Penumbra.PlayerWatch.csproj | 40 --- Penumbra.PlayerWatch/PlayerWatchBase.cs | 323 ------------------ Penumbra.PlayerWatch/PlayerWatcher.cs | 103 ------ Penumbra.sln | 6 - Penumbra/Penumbra.cs | 3 +- Penumbra/Penumbra.csproj | 11 - Penumbra/Util/Backup.cs | 147 -------- 17 files changed, 192 insertions(+), 840 deletions(-) delete mode 100644 Penumbra.PlayerWatch/CharacterFactory.cs delete mode 100644 Penumbra.PlayerWatch/IPlayerWatcher.cs delete mode 100644 Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj delete mode 100644 Penumbra.PlayerWatch/PlayerWatchBase.cs delete mode 100644 Penumbra.PlayerWatch/PlayerWatcher.cs delete mode 100644 Penumbra/Util/Backup.cs diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 67a1da6d..a0bab5ea 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection.Metadata.Ecma335; +using Newtonsoft.Json; namespace Penumbra.GameData.Enums; @@ -51,6 +52,22 @@ public static class EquipSlotExtensions _ => EquipSlot.Unknown, }; + public static uint ToIndex( this EquipSlot slot ) + => slot switch + { + EquipSlot.Head => 0, + EquipSlot.Body => 1, + EquipSlot.Hands => 2, + EquipSlot.Legs => 3, + EquipSlot.Feet => 4, + EquipSlot.Ears => 5, + EquipSlot.Neck => 6, + EquipSlot.Wrists => 7, + EquipSlot.RFinger => 8, + EquipSlot.LFinger => 9, + _ => uint.MaxValue, + }; + public static string ToSuffix( this EquipSlot value ) { return value switch diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index ffe421cf..ae0f9494 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -1,10 +1,14 @@ +using System; using System.Runtime.InteropServices; namespace Penumbra.GameData.Structs; [StructLayout( LayoutKind.Explicit, Pack = 1 )] -public readonly struct CharacterArmor +public readonly struct CharacterArmor : IEquatable< CharacterArmor > { + [FieldOffset( 0 )] + public readonly uint Value; + [FieldOffset( 0 )] public readonly SetId Set; @@ -14,9 +18,31 @@ public readonly struct CharacterArmor [FieldOffset( 3 )] public readonly StainId Stain; - [FieldOffset( 0 )] - public readonly uint Value; + public CharacterArmor( SetId set, byte variant, StainId stain ) + { + Value = 0; + Set = set; + Variant = variant; + Stain = stain; + } public override string ToString() => $"{Set},{Variant},{Stain}"; + + public static readonly CharacterArmor Empty; + + public bool Equals( CharacterArmor other ) + => Value == other.Value; + + public override bool Equals( object? obj ) + => obj is CharacterArmor other && Equals( other ); + + public override int GetHashCode() + => ( int )Value; + + public static bool operator ==( CharacterArmor left, CharacterArmor right ) + => left.Value == right.Value; + + public static bool operator !=( CharacterArmor left, CharacterArmor right ) + => left.Value != right.Value; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index 5a742073..62752ed1 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -1,16 +1,51 @@ +using System; using System.Runtime.InteropServices; -namespace Penumbra.GameData.Structs -{ - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public readonly struct CharacterWeapon - { - public readonly SetId Set; - public readonly WeaponType Type; - public readonly ushort Variant; - public readonly StainId Stain; +namespace Penumbra.GameData.Structs; - public override string ToString() - => $"{Set},{Type},{Variant},{Stain}"; +[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 7 )] +public readonly struct CharacterWeapon : IEquatable< CharacterWeapon > +{ + [FieldOffset( 0 )] + public readonly SetId Set; + + [FieldOffset( 2 )] + public readonly WeaponType Type; + + [FieldOffset( 4 )] + public readonly ushort Variant; + + [FieldOffset( 6 )] + public readonly StainId Stain; + + public ulong Value + => ( ulong )Set | ( ( ulong )Type << 16 ) | ( ( ulong )Variant << 32 ) | ( ( ulong )Stain << 48 ); + + public override string ToString() + => $"{Set},{Type},{Variant},{Stain}"; + + public CharacterWeapon( SetId set, WeaponType type, ushort variant, StainId stain ) + { + Set = set; + Type = type; + Variant = variant; + Stain = stain; } + + public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); + + public bool Equals( CharacterWeapon other ) + => Value == other.Value; + + public override bool Equals( object? obj ) + => obj is CharacterWeapon other && Equals( other ); + + public override int GetHashCode() + => Value.GetHashCode(); + + public static bool operator ==( CharacterWeapon left, CharacterWeapon right ) + => left.Value == right.Value; + + public static bool operator !=( CharacterWeapon left, CharacterWeapon right ) + => left.Value != right.Value; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index ab5a6851..8ea20ecc 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -200,12 +200,12 @@ public static class Eqp EqpEntry.HeadShowVieraHat => EquipSlot.Head, // Currently unused. - EqpEntry._58 => EquipSlot.Unknown, - EqpEntry._59 => EquipSlot.Unknown, - EqpEntry._60 => EquipSlot.Unknown, - EqpEntry._61 => EquipSlot.Unknown, - EqpEntry._62 => EquipSlot.Unknown, - EqpEntry._63 => EquipSlot.Unknown, + EqpEntry._58 => EquipSlot.Unknown, + EqpEntry._59 => EquipSlot.Unknown, + EqpEntry._60 => EquipSlot.Unknown, + EqpEntry._61 => EquipSlot.Unknown, + EqpEntry._62 => EquipSlot.Unknown, + EqpEntry._63 => EquipSlot.Unknown, _ => EquipSlot.Unknown, }; diff --git a/Penumbra.GameData/Structs/RspEntry.cs b/Penumbra.GameData/Structs/RspEntry.cs index 80f02144..98f85da3 100644 --- a/Penumbra.GameData/Structs/RspEntry.cs +++ b/Penumbra.GameData/Structs/RspEntry.cs @@ -4,55 +4,54 @@ using System.IO; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[StructLayout( LayoutKind.Sequential, Pack = 1 )] +public readonly struct RspEntry { - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public readonly struct RspEntry + public const int ByteSize = ( int )RspAttribute.NumAttributes * 4; + + private readonly float[] Attributes; + + public RspEntry( RspEntry copy ) + => Attributes = ( float[] )copy.Attributes.Clone(); + + public RspEntry( byte[] bytes, int offset ) { - public const int ByteSize = ( int )RspAttribute.NumAttributes * 4; - - private readonly float[] Attributes; - - public RspEntry( RspEntry copy ) - => Attributes = ( float[] )copy.Attributes.Clone(); - - public RspEntry( byte[] bytes, int offset ) + if( offset < 0 || offset + ByteSize > bytes.Length ) { - if( offset < 0 || offset + ByteSize > bytes.Length ) - { - throw new ArgumentOutOfRangeException(); - } - - Attributes = new float[( int )RspAttribute.NumAttributes]; - using MemoryStream s = new( bytes ) { Position = offset }; - using BinaryReader br = new( s ); - for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i ) - { - Attributes[ i ] = br.ReadSingle(); - } + throw new ArgumentOutOfRangeException(); } - private static int ToIndex( RspAttribute attribute ) - => attribute < RspAttribute.NumAttributes && attribute >= 0 - ? ( int )attribute - : throw new InvalidEnumArgumentException(); - - public float this[ RspAttribute attribute ] + Attributes = new float[( int )RspAttribute.NumAttributes]; + using MemoryStream s = new(bytes) { Position = offset }; + using BinaryReader br = new(s); + for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i ) { - get => Attributes[ ToIndex( attribute ) ]; - set => Attributes[ ToIndex( attribute ) ] = value; - } - - public byte[] ToBytes() - { - using var s = new MemoryStream( ByteSize ); - using var bw = new BinaryWriter( s ); - foreach( var attribute in Attributes ) - { - bw.Write( attribute ); - } - - return s.ToArray(); + Attributes[ i ] = br.ReadSingle(); } } + + private static int ToIndex( RspAttribute attribute ) + => attribute < RspAttribute.NumAttributes && attribute >= 0 + ? ( int )attribute + : throw new InvalidEnumArgumentException(); + + public float this[ RspAttribute attribute ] + { + get => Attributes[ ToIndex( attribute ) ]; + set => Attributes[ ToIndex( attribute ) ] = value; + } + + public byte[] ToBytes() + { + using var s = new MemoryStream( ByteSize ); + using var bw = new BinaryWriter( s ); + foreach( var attribute in Attributes ) + { + bw.Write( attribute ); + } + + return s.ToArray(); + } } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/SetId.cs b/Penumbra.GameData/Structs/SetId.cs index a2483538..79674fac 100644 --- a/Penumbra.GameData/Structs/SetId.cs +++ b/Penumbra.GameData/Structs/SetId.cs @@ -1,24 +1,23 @@ using System; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +public readonly struct SetId : IComparable< SetId > { - public readonly struct SetId : IComparable< SetId > - { - public readonly ushort Value; + public readonly ushort Value; - public SetId( ushort value ) - => Value = value; + public SetId( ushort value ) + => Value = value; - public static implicit operator SetId( ushort id ) - => new( id ); + public static implicit operator SetId( ushort id ) + => new(id); - public static explicit operator ushort( SetId id ) - => id.Value; + public static explicit operator ushort( SetId id ) + => id.Value; - public override string ToString() - => Value.ToString(); + public override string ToString() + => Value.ToString(); - public int CompareTo( SetId other ) - => Value.CompareTo( other.Value ); - } + public int CompareTo( SetId other ) + => Value.CompareTo( other.Value ); } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/StainId.cs b/Penumbra.GameData/Structs/StainId.cs index 74a479a1..d97b5a94 100644 --- a/Penumbra.GameData/Structs/StainId.cs +++ b/Penumbra.GameData/Structs/StainId.cs @@ -1,30 +1,29 @@ using System; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +public readonly struct StainId : IEquatable< StainId > { - public readonly struct StainId : IEquatable< StainId > - { - public readonly byte Value; + public readonly byte Value; - public StainId( byte value ) - => Value = value; + public StainId( byte value ) + => Value = value; - public static implicit operator StainId( byte id ) - => new( id ); + public static implicit operator StainId( byte id ) + => new(id); - public static explicit operator byte( StainId id ) - => id.Value; + public static explicit operator byte( StainId id ) + => id.Value; - public override string ToString() - => Value.ToString(); + public override string ToString() + => Value.ToString(); - public bool Equals( StainId other ) - => Value == other.Value; + public bool Equals( StainId other ) + => Value == other.Value; - public override bool Equals( object? obj ) - => obj is StainId other && Equals( other ); + public override bool Equals( object? obj ) + => obj is StainId other && Equals( other ); - public override int GetHashCode() - => Value.GetHashCode(); - } + public override int GetHashCode() + => Value.GetHashCode(); } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/WeaponType.cs b/Penumbra.GameData/Structs/WeaponType.cs index a5fa6107..17bb3dd5 100644 --- a/Penumbra.GameData/Structs/WeaponType.cs +++ b/Penumbra.GameData/Structs/WeaponType.cs @@ -1,30 +1,29 @@ using System; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +public readonly struct WeaponType : IEquatable< WeaponType > { - public readonly struct WeaponType : IEquatable< WeaponType > - { - public readonly ushort Value; + public readonly ushort Value; - public WeaponType( ushort value ) - => Value = value; + public WeaponType( ushort value ) + => Value = value; - public static implicit operator WeaponType( ushort id ) - => new( id ); + public static implicit operator WeaponType( ushort id ) + => new(id); - public static explicit operator ushort( WeaponType id ) - => id.Value; + public static explicit operator ushort( WeaponType id ) + => id.Value; - public override string ToString() - => Value.ToString(); + public override string ToString() + => Value.ToString(); - public bool Equals( WeaponType other ) - => Value == other.Value; + public bool Equals( WeaponType other ) + => Value == other.Value; - public override bool Equals( object? obj ) - => obj is WeaponType other && Equals( other ); + public override bool Equals( object? obj ) + => obj is WeaponType other && Equals( other ); - public override int GetHashCode() - => Value.GetHashCode(); - } + public override int GetHashCode() + => Value.GetHashCode(); } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/CharacterFactory.cs b/Penumbra.PlayerWatch/CharacterFactory.cs deleted file mode 100644 index b88cb28f..00000000 --- a/Penumbra.PlayerWatch/CharacterFactory.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Reflection; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; - -namespace Penumbra.PlayerWatch -{ - public static class CharacterFactory - { - private static ConstructorInfo? _characterConstructor; - - private static void Initialize() - { - _characterConstructor ??= typeof( Character ).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new[] - { - typeof( IntPtr ), - }, null )!; - } - - private static Character Character( IntPtr address ) - { - Initialize(); - return ( Character )_characterConstructor?.Invoke( new object[] - { - address, - } )!; - } - - public static Character? Convert( GameObject? actor ) - { - if( actor == null ) - { - return null; - } - - return actor switch - { - PlayerCharacter p => p, - BattleChara b => b, - _ => actor.ObjectKind switch - { - ObjectKind.BattleNpc => Character( actor.Address ), - ObjectKind.Companion => Character( actor.Address ), - ObjectKind.Retainer => Character( actor.Address ), - ObjectKind.EventNpc => Character( actor.Address ), - _ => null, - }, - }; - } - } - - public static class GameObjectExtensions - { - private const int ModelTypeOffset = 0x01B4; - - public static unsafe int ModelType( this GameObject actor ) - => *( int* )( actor.Address + ModelTypeOffset ); - - public static unsafe void SetModelType( this GameObject actor, int value ) - => *( int* )( actor.Address + ModelTypeOffset ) = value; - } -} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/IPlayerWatcher.cs b/Penumbra.PlayerWatch/IPlayerWatcher.cs deleted file mode 100644 index bfdff17e..00000000 --- a/Penumbra.PlayerWatch/IPlayerWatcher.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects.Types; -using Penumbra.GameData.Structs; - -namespace Penumbra.PlayerWatch; - -public delegate void PlayerChange( Character actor ); - -public interface IPlayerWatcherBase : IDisposable -{ - public int Version { get; } - public bool Valid { get; } -} - -public interface IPlayerWatcher : IPlayerWatcherBase -{ - public event PlayerChange? PlayerChanged; - public bool Active { get; } - - public void Enable(); - public void Disable(); - public void SetStatus( bool enabled ); - - public void AddPlayerToWatch( string playerName ); - public void RemovePlayerFromWatch( string playerName ); - public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); - - public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers(); -} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj b/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj deleted file mode 100644 index f3449ad3..00000000 --- a/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - net5.0-windows - preview - x64 - Penumbra.PlayerWatch - absolute gangstas - Penumbra - Copyright © 2020 - 1.0.0.0 - 1.0.0.0 - bin\$(Configuration)\ - true - enable - - - - full - DEBUG;TRACE - - - - pdbonly - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - - - - - \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs deleted file mode 100644 index 5b670484..00000000 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ /dev/null @@ -1,323 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; -using Penumbra.GameData.Structs; - -namespace Penumbra.PlayerWatch; - -internal readonly struct WatchedPlayer -{ - public readonly Dictionary< ulong, CharacterEquipment > FoundActors; - public readonly HashSet< PlayerWatcher > RegisteredWatchers; - - public WatchedPlayer( PlayerWatcher watcher ) - { - FoundActors = new Dictionary< ulong, CharacterEquipment >( 4 ); - RegisteredWatchers = new HashSet< PlayerWatcher > { watcher }; - } -} - -internal class PlayerWatchBase : IDisposable -{ - public const int GPosePlayerIdx = 201; - public const int GPoseTableEnd = GPosePlayerIdx + 40; - private const int ObjectsPerFrame = 32; - - private readonly Framework _framework; - private readonly ClientState _clientState; - private readonly ObjectTable _objects; - internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); - internal readonly Dictionary< string, WatchedPlayer > Equip = new(); - internal HashSet< ulong > SeenActors; - private int _frameTicker; - private bool _inGPose; - private bool _enabled; - private bool _cancel; - - internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) - { - _framework = framework; - _clientState = clientState; - _objects = objects; - SeenActors = new HashSet< ulong >( _objects.Length ); - } - - internal void RegisterWatcher( PlayerWatcher watcher ) - { - RegisteredWatchers.Add( watcher ); - if( watcher.Active ) - { - EnablePlayerWatch(); - } - } - - internal void UnregisterWatcher( PlayerWatcher watcher ) - { - if( RegisteredWatchers.Remove( watcher ) ) - { - foreach( var (key, value) in Equip.ToArray() ) - { - if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 ) - { - Equip.Remove( key ); - } - } - } - - CheckActiveStatus(); - } - - internal void CheckActiveStatus() - { - if( RegisteredWatchers.Any( w => w.Active ) ) - { - EnablePlayerWatch(); - } - else - { - DisablePlayerWatch(); - } - } - - private static ulong GetId( GameObject actor ) - => actor.ObjectId | ( ( ulong )actor.OwnerId << 32 ); - - internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) - { - var name = actor.Name.ToString(); - var equipment = new CharacterEquipment( actor ); - if( Equip.TryGetValue( name, out var watched ) ) - { - watched.FoundActors[ GetId( actor ) ] = equipment; - } - - return equipment; - } - - internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher ) - { - if( Equip.TryGetValue( playerName, out var items ) ) - { - items.RegisteredWatchers.Add( watcher ); - } - else - { - Equip[ playerName ] = new WatchedPlayer( watcher ); - } - } - - public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher ) - { - if( Equip.TryGetValue( playerName, out var items ) ) - { - if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 ) - { - Equip.Remove( playerName ); - } - } - } - - internal void EnablePlayerWatch() - { - if( !_enabled ) - { - _enabled = true; - _framework.Update += OnFrameworkUpdate; - _clientState.TerritoryChanged += OnTerritoryChange; - _clientState.Logout += OnLogout; - } - } - - internal void DisablePlayerWatch() - { - if( _enabled ) - { - _enabled = false; - _framework.Update -= OnFrameworkUpdate; - _clientState.TerritoryChanged -= OnTerritoryChange; - _clientState.Logout -= OnLogout; - } - } - - public void Dispose() - => DisablePlayerWatch(); - - private void OnTerritoryChange( object? _1, ushort _2 ) - => Clear(); - - private void OnLogout( object? _1, object? _2 ) - => Clear(); - - internal void Clear() - { - PluginLog.Debug( "Clearing PlayerWatcher Store." ); - _cancel = true; - foreach( var kvp in Equip ) - { - kvp.Value.FoundActors.Clear(); - } - - _frameTicker = 0; - } - - private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player ) - { - PluginLog.Debug( "Triggering events for {PlayerName} at {Address}.", player.Name, player.Address ); - foreach( var watcher in watchers.Where( w => w.Active ) ) - { - watcher.Trigger( player ); - } - } - - internal void TriggerGPose() - { - for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) - { - var player = _objects[ i ]; - if( player == null ) - { - return; - } - - if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) - { - TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); - } - } - } - - private Character? CheckGPoseObject( GameObject player ) - { - if( !_inGPose ) - { - return CharacterFactory.Convert( player ); - } - - for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) - { - var a = _objects[ i ]; - if( a == null ) - { - return CharacterFactory.Convert( player ); - } - - if( a.Name == player.Name ) - { - return CharacterFactory.Convert( a ); - } - } - - return CharacterFactory.Convert( player )!; - } - - private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) - { - watch = default; - var name = gameObject.Name.ToString(); - return name.Length != 0 && Equip.TryGetValue( name, out watch ); - } - - private static bool InvalidObjectKind( ObjectKind kind ) - { - return kind switch - { - ObjectKind.BattleNpc => false, - ObjectKind.EventNpc => false, - ObjectKind.Player => false, - ObjectKind.Retainer => false, - _ => true, - }; - } - - private GameObject? GetNextObject() - { - if( _frameTicker == GPosePlayerIdx - 1 ) - { - _frameTicker = GPoseTableEnd; - } - else if( _frameTicker == _objects.Length - 1 ) - { - _frameTicker = 0; - foreach( var (_, equip) in Equip.Values.SelectMany( d => d.FoundActors.Where( p => !SeenActors.Contains( p.Key ) ) ) ) - { - equip.Clear(); - } - - SeenActors.Clear(); - } - else - { - ++_frameTicker; - } - - return _objects[ _frameTicker ]; - } - - private void OnFrameworkUpdate( object framework ) - { - var newInGPose = _objects[ GPosePlayerIdx ] != null; - - if( newInGPose != _inGPose ) - { - if( newInGPose ) - { - TriggerGPose(); - } - else - { - Clear(); - } - - _inGPose = newInGPose; - } - - for( var i = 0; i < ObjectsPerFrame; ++i ) - { - var actor = GetNextObject(); - if( actor == null - || InvalidObjectKind( actor.ObjectKind ) - || !TryGetPlayer( actor, out var watch ) ) - { - continue; - } - - var character = CheckGPoseObject( actor ); - if( _cancel ) - { - _cancel = false; - return; - } - - if( character == null || character.ModelType() != 0 ) - { - continue; - } - - var id = GetId( character ); - SeenActors.Add( id ); - -#if DEBUG - PluginLog.Verbose( "Comparing Gear for {PlayerName:l} ({Id}) at 0x{Address:X}...", character.Name, id, character.Address.ToInt64() ); -#endif - - if( !watch.FoundActors.TryGetValue( id, out var equip ) ) - { - equip = new CharacterEquipment( character ); - watch.FoundActors[ id ] = equip; - TriggerEvents( watch.RegisteredWatchers, character ); - } - else if( !equip.CompareAndUpdate( character ) ) - { - TriggerEvents( watch.RegisteredWatchers, character ); - } - - break; // Only one comparison per frame. - } - } -} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatcher.cs b/Penumbra.PlayerWatch/PlayerWatcher.cs deleted file mode 100644 index 7ae94f79..00000000 --- a/Penumbra.PlayerWatch/PlayerWatcher.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.Types; -using Penumbra.GameData.Structs; - -namespace Penumbra.PlayerWatch; - -public class PlayerWatcher : IPlayerWatcher -{ - public int Version - => 3; - - private static PlayerWatchBase? _playerWatch; - - public event PlayerChange? PlayerChanged; - - public bool Active { get; set; } = true; - - public bool Valid - => _playerWatch != null; - - internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) - { - _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); - _playerWatch.RegisterWatcher( this ); - } - - public void Enable() - => SetStatus( true ); - - public void Disable() - => SetStatus( false ); - - public void SetStatus( bool enabled ) - { - Active = enabled && Valid; - _playerWatch?.CheckActiveStatus(); - } - - internal void Trigger( Character actor ) - => PlayerChanged?.Invoke( actor ); - - public void Dispose() - { - if( _playerWatch == null ) - { - return; - } - - Active = false; - PlayerChanged = null; - _playerWatch.UnregisterWatcher( this ); - if( _playerWatch.RegisteredWatchers.Count == 0 ) - { - _playerWatch.Dispose(); - _playerWatch = null; - } - } - - private void CheckValidity() - { - if( !Valid ) - { - throw new Exception( $"PlayerWatch was already disposed." ); - } - } - - public void AddPlayerToWatch( string name ) - { - CheckValidity(); - _playerWatch!.AddPlayerToWatch( name, this ); - } - - public void RemovePlayerFromWatch( string playerName ) - { - CheckValidity(); - _playerWatch!.RemovePlayerFromWatch( playerName, this ); - } - - public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) - { - CheckValidity(); - return _playerWatch!.UpdatePlayerWithoutEvent( actor ); - } - - public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers() - { - CheckValidity(); - return _playerWatch!.Equip - .Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) ) - .Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) ); - } -} - -public static class PlayerWatchFactory -{ - public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) - => new PlayerWatcher( framework, clientState, objects ); -} \ No newline at end of file diff --git a/Penumbra.sln b/Penumbra.sln index 57a5cff2..b43c7565 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -12,8 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{01685BD8-8847-4B49-BF90-1683B4C76B0E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{87750518-1A20-40B4-9FC1-22F906EFB290}" EndProject Global @@ -30,10 +28,6 @@ Global {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU - {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.Build.0 = Release|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3a4753dd..8ecb69ca 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -13,6 +13,7 @@ using EmbedIO.WebApi; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; +using OtterGui.Classes; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; @@ -75,7 +76,7 @@ public class Penumbra : IDalamudPlugin Framework = new FrameworkManager(); CharacterUtility = new CharacterUtility(); - Backup.CreateBackup( PenumbraBackupFiles() ); + Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); Config = Configuration.Load(); TempMods = new TempModManager(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index eb2f204f..26e8d088 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -15,16 +15,6 @@ true - - full - DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - - - pdbonly - $(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC - - $(MSBuildWarningsAsMessages);MSB3277 @@ -72,7 +62,6 @@ - diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs deleted file mode 100644 index a1e9551d..00000000 --- a/Penumbra/Util/Backup.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using Dalamud.Logging; - -namespace Penumbra.Util; - -public static class Backup -{ - public const int MaxNumBackups = 10; - - // Create a backup named by ISO 8601 of the current time. - // If the newest previously existing backup equals the current state of files, - // do not create a new backup. - // If the maximum number of backups is exceeded afterwards, delete the oldest backup. - public static void CreateBackup( IReadOnlyCollection< FileInfo > files ) - { - try - { - var configDirectory = Dalamud.PluginInterface.ConfigDirectory.Parent!.FullName; - var directory = CreateBackupDirectory(); - var (newestFile, oldestFile, numFiles) = CheckExistingBackups( directory ); - var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmmss}.zip" ); - if( newestFile == null || CheckNewestBackup( newestFile, configDirectory, files.Count ) ) - { - CreateBackup( files, newBackupName, configDirectory ); - if( numFiles > MaxNumBackups ) - { - oldestFile!.Delete(); - } - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create backups:\n{e}" ); - } - } - - - // Obtain the backup directory. Create it if it does not exist. - private static DirectoryInfo CreateBackupDirectory() - { - var path = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!.FullName, "backups", - Dalamud.PluginInterface.ConfigDirectory.Name ); - var dir = new DirectoryInfo( path ); - if( !dir.Exists ) - { - dir = Directory.CreateDirectory( dir.FullName ); - } - - return dir; - } - - // Check the already existing backups. - // Only keep MaxNumBackups at once, and delete the oldest if the number would be exceeded. - // Return the newest backup. - private static (FileInfo? Newest, FileInfo? Oldest, int Count) CheckExistingBackups( DirectoryInfo backupDirectory ) - { - var count = 0; - FileInfo? newest = null; - FileInfo? oldest = null; - - foreach( var file in backupDirectory.EnumerateFiles( "*.zip" ) ) - { - ++count; - var time = file.CreationTimeUtc; - if( ( oldest?.CreationTimeUtc ?? DateTime.MaxValue ) > time ) - { - oldest = file; - } - - if( ( newest?.CreationTimeUtc ?? DateTime.MinValue ) < time ) - { - newest = file; - } - } - - return ( newest, oldest, count ); - } - - // Compare the newest backup against the currently existing files. - // If there are any differences, return false, and if they are completely identical, return true. - private static bool CheckNewestBackup( FileInfo newestFile, string configDirectory, int fileCount ) - { - using var oldFileStream = File.Open( newestFile.FullName, FileMode.Open ); - using var oldZip = new ZipArchive( oldFileStream, ZipArchiveMode.Read ); - // Number of stored files is different. - if( fileCount != oldZip.Entries.Count ) - { - return true; - } - - // Since number of files is identical, - // the backups are identical if every file in the old backup - // still exists and is identical. - foreach( var entry in oldZip.Entries ) - { - var file = Path.Combine( configDirectory, entry.FullName ); - if( !File.Exists( file ) ) - { - return true; - } - - using var currentData = File.OpenRead( file ); - using var oldData = entry.Open(); - - if( !Equals( currentData, oldData ) ) - { - return true; - } - } - - return false; - } - - // Create the actual backup, storing all the files relative to the given configDirectory in the zip. - private static void CreateBackup( IEnumerable< FileInfo > files, string fileName, string configDirectory ) - { - using var fileStream = File.Open( fileName, FileMode.Create ); - using var zip = new ZipArchive( fileStream, ZipArchiveMode.Create ); - foreach( var file in files.Where( f => File.Exists( f.FullName ) ) ) - { - zip.CreateEntryFromFile( file.FullName, Path.GetRelativePath( configDirectory, file.FullName ), CompressionLevel.Optimal ); - } - } - - // Compare two streams per byte and return if they are equal. - private static bool Equals( Stream lhs, Stream rhs ) - { - while( true ) - { - var current = lhs.ReadByte(); - var old = rhs.ReadByte(); - if( current != old ) - { - return false; - } - - if( current == -1 ) - { - return true; - } - } - } -} \ No newline at end of file From f1d9757077a3f837d5a14780655d2df088f0133a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 11:57:18 +0200 Subject: [PATCH 0396/2451] Redraw mounts when redrawing actors. --- Penumbra/Interop/ObjectReloader.cs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 0fcab13e..a04801b9 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.Api; using Penumbra.GameData.Enums; @@ -129,10 +131,20 @@ public sealed unsafe partial class ObjectReloader : IDisposable *ActorDrawState( actor! ) |= DrawState.Invisibility; - if( IsGPoseActor( tableIndex ) ) + var gPose = IsGPoseActor( tableIndex ); + if( gPose ) { DisableDraw( actor! ); } + + if( actor is PlayerCharacter && Dalamud.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) + { + *ActorDrawState( mount ) |= DrawState.Invisibility; + if( gPose ) + { + DisableDraw( mount ); + } + } } private void WriteVisible( GameObject? actor ) @@ -143,11 +155,22 @@ public sealed unsafe partial class ObjectReloader : IDisposable } *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - if( IsGPoseActor( tableIndex ) ) + + var gPose = IsGPoseActor( tableIndex ); + if( gPose ) { EnableDraw( actor! ); } + if( actor is PlayerCharacter && Dalamud.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) + { + *ActorDrawState( mount ) &= ~DrawState.Invisibility; + if( gPose ) + { + EnableDraw( mount ); + } + } + GameObjectRedrawn?.Invoke( actor!.Address, tableIndex ); } From 9cb6084d3135500b1b4d68f996f604b3be244b15 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 22:54:27 +0200 Subject: [PATCH 0397/2451] Fix an exception on broken mods. --- Penumbra/Util/PenumbraSqPackStream.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index e338a1d5..017d70c6 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -282,6 +282,11 @@ public class PenumbraSqPackStream : IDisposable private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) { + if( resource.FileInfo!.BlockCount == 0 ) + { + return; + } + var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount ); // if there is a mipmap header, the comp_offset From 714e8e862ff927749018e28aa5882ae060dcea87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Jul 2022 22:55:19 +0200 Subject: [PATCH 0398/2451] Change a bunch of names and tooltips. --- Penumbra/Collections/ModCollection.cs | 3 + ...ConfigWindow.CollectionsTab.Inheritance.cs | 10 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 172 +++++++++++------- Penumbra/UI/ConfigWindow.Tutorial.cs | 50 +++-- 4 files changed, 144 insertions(+), 91 deletions(-) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 5394efcc..6ace180e 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -42,6 +42,9 @@ public partial class ModCollection public bool HasUnusedSettings => _unusedSettings.Count > 0; + public int NumUnusedSettings + => _unusedSettings.Count; + // Evaluates the settings along the whole inheritance tree. public IEnumerable< ModSettings? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index 92f50d7e..366a5b4f 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -33,7 +33,7 @@ public partial class ConfigWindow { using var group = ImRaii.Group(); using var id = ImRaii.PushId( "##Inheritance" ); - ImGui.TextUnformatted( "The current collection inherits from:" ); + ImGui.TextUnformatted( $"The {SelectedCollection} inherits from:" ); DrawCurrentCollectionInheritance(); DrawInheritanceTrashButton(); DrawNewInheritanceSelection(); @@ -187,10 +187,10 @@ public partial class ConfigWindow var tt = inheritance switch { ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", - ModCollection.ValidInheritance.Valid => "Let the current collection inherit from the selected collection.", + ModCollection.ValidInheritance.Valid => $"Let the {SelectedCollection} inherit from this collection.", ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", - ModCollection.ValidInheritance.Contained => "Already inheriting from the selected collection.", - ModCollection.ValidInheritance.Circle => "Inheriting from selected collection would lead to cyclic inheritance.", + ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", + ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", _ => string.Empty, }; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, @@ -311,7 +311,7 @@ public partial class ConfigWindow } } - ImGuiUtil.HoverTooltip( "Control + Right-Click to switch the current collection to this one." + ImGuiUtil.HoverTooltip( $"Control + Right-Click to switch the {SelectedCollection} to this one." + ( withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty ) ); } } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 09ad7796..58397133 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -55,13 +55,14 @@ public partial class ConfigWindow } // Only gets drawn when actually relevant. - private static void DrawCleanCollectionButton() + private static void DrawCleanCollectionButton( Vector2 width ) { if( Penumbra.Config.ShowAdvanced && Penumbra.CollectionManager.Current.HasUnusedSettings ) { ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Clean Settings", Vector2.Zero - , "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." + if( ImGuiUtil.DrawDisabledButton( + $"Clean {Penumbra.CollectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." , false ) ) { Penumbra.CollectionManager.Current.CleanUnavailableSettings(); @@ -70,11 +71,11 @@ public partial class ConfigWindow } // Draw the new collection input as well as its buttons. - private void DrawNewCollectionInput() + private void DrawNewCollectionInput( Vector2 width ) { // Input for new collection name. Also checks for validity when changed. ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputTextWithHint( "##New Collection", "New Collection Name", ref _newCollectionName, 64 ) ) + if( ImGui.InputTextWithHint( "##New Collection", "New Collection Name...", ref _newCollectionName, 64 ) ) { _canAddCollection = Penumbra.CollectionManager.CanAddCollection( _newCollectionName, out _ ); } @@ -82,40 +83,45 @@ public partial class ConfigWindow ImGui.SameLine(); ImGuiComponents.HelpMarker( "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of mods." ); + + "You can use multiple collections to quickly switch between sets of enabled mods." ); // Creation buttons. - var tt = _canAddCollection ? string.Empty : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; - if( ImGuiUtil.DrawDisabledButton( "Create New Empty Collection", Vector2.Zero, tt, !_canAddCollection ) ) + var tt = _canAddCollection + ? string.Empty + : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; + if( ImGuiUtil.DrawDisabledButton( "Create Empty Collection", width, tt, !_canAddCollection ) ) { CreateNewCollection( false ); } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Duplicate Current Collection", Vector2.Zero, tt, !_canAddCollection ) ) + if( ImGuiUtil.DrawDisabledButton( $"Duplicate {SelectedCollection}", width, tt, !_canAddCollection ) ) { CreateNewCollection( true ); } - - // Deletion conditions. - var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; - tt = deleteCondition ? string.Empty : "You can not delete the default collection."; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), Vector2.Zero, tt, !deleteCondition, true ) ) - { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); - } - - DrawCleanCollectionButton(); } - private void DrawCurrentCollectionSelector() + private void DrawCurrentCollectionSelector( Vector2 width ) { using var group = ImRaii.Group(); DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Current Collection", - "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); + ImGuiUtil.LabeledHelpMarker( SelectedCollection, + "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." ); + + // Deletion conditions. + var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; + var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); + var tt = deleteCondition + ? modifierHeld ? string.Empty : $"Hold {Penumbra.Config.DeleteModModifier} while clicking to delete the collection." + : $"You can not delete the collection {ModCollection.DefaultCollection}."; + + if( ImGuiUtil.DrawDisabledButton( $"Delete {SelectedCollection}", width, tt, !deleteCondition || !modifierHeld ) ) + { + Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); + } + + DrawCleanCollectionButton( width ); } private void DrawDefaultCollectionSelector() @@ -123,16 +129,17 @@ public partial class ConfigWindow using var group = ImRaii.Group(); DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Default Collection", - "Mods in the default collection are loaded for any character that is not explicitly named in the character collections below.\n" ); + ImGuiUtil.LabeledHelpMarker( DefaultCollection, + $"Mods in the {DefaultCollection} are loaded for anything that is not associated with a character in the game " + + "as well as any character for whom no more specific conditions from below apply." ); } // We do not check for valid character names. private void DrawNewSpecialCollection() { - const string description = "Special Collections apply to certain types of characters.\n" - + "All of them take precedence before the Default collection,\n" - + "but all character collections take precedence before them."; + const string description = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {DefaultCollection},\n" + + $"but all {IndividualAssignments} take precedence before them."; ImGui.SetNextItemWidth( _window._inputTextWidth.X ); if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value ) != null ) @@ -163,8 +170,10 @@ public partial class ConfigWindow ImGui.SameLine(); var disabled = _currentType == null; - var tt = disabled ? "Please select a special collection type before creating the collection.\n\n" + description : description; - if( ImGuiUtil.DrawDisabledButton( "Create New Special Collection", Vector2.Zero, tt, disabled ) ) + var tt = disabled + ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + description + : description; + if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) { Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value ); _currentType = null; @@ -174,36 +183,27 @@ public partial class ConfigWindow // We do not check for valid character names. private void DrawNewCharacterCollection() { - const string description = "Character Collections apply specifically to game objects of the given name.\n" - + "The default collection does not apply to any character that has a character collection specified.\n" + const string description = "Character Collections apply specifically to individual game objects of the given name.\n" + + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an .\n" + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - ImGui.InputTextWithHint( "##NewCharacter", "New Character Name", ref _newCharacterName, 32 ); + ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); ImGui.SameLine(); var disabled = _newCharacterName.Length == 0; - var tt = disabled ? "Please enter a Character name before creating the collection.\n\n" + description : description; - if( ImGuiUtil.DrawDisabledButton( "Create New Character Collection", Vector2.Zero, tt, disabled ) ) + var tt = disabled + ? $"Please enter the name of a {ConditionalIndividual} before assigning the collection.\n\n" + description + : description; + if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalIndividual}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, + disabled ) ) { Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); _newCharacterName = string.Empty; } } - private void DrawActiveCollectionSelectors() + private void DrawSpecialCollections() { - ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( "Active Collections", ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( BasicTutorialSteps.ActiveCollections ); - if( !open ) - { - return; - } - - ImGui.Dummy( _window._defaultSpace ); - DrawDefaultCollectionSelector(); - OpenTutorial( BasicTutorialSteps.DefaultCollection ); - ImGui.Dummy( _window._defaultSpace ); foreach( var type in CollectionTypeExtensions.Special ) { var collection = Penumbra.CollectionManager.ByType( type ); @@ -223,32 +223,65 @@ public partial class ConfigWindow ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); } } + } - using( var group = ImRaii.Group() ) + private void DrawSpecialAssignments() + { + using var _ = ImRaii.Group(); + ImGui.TextUnformatted( CharacterGroups ); + ImGui.Separator(); + DrawSpecialCollections(); + ImGui.Dummy( Vector2.Zero ); + DrawNewSpecialCollection(); + } + + private void DrawIndividualAssignments() + { + using var _ = ImRaii.Group(); + ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); + ImGui.Separator(); + foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) { - DrawNewSpecialCollection(); - ImGui.Dummy( _window._defaultSpace ); - - foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) + using var id = ImRaii.PushId( name ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, + false, true ) ) { - using var id = ImRaii.PushId( name ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.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 ); + Penumbra.CollectionManager.RemoveCharacterCollection( name ); } - DrawNewCharacterCollection(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( name ); } - OpenTutorial( BasicTutorialSteps.SpecialCollections ); + ImGui.Dummy( Vector2.Zero ); + DrawNewCharacterCollection(); + } + + private void DrawActiveCollectionSelectors() + { + ImGui.Dummy( _window._defaultSpace ); + var open = ImGui.CollapsingHeader( ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen ); + OpenTutorial( BasicTutorialSteps.ActiveCollections ); + if( !open ) + { + return; + } + + ImGui.Dummy( _window._defaultSpace ); + DrawDefaultCollectionSelector(); + OpenTutorial( BasicTutorialSteps.DefaultCollection ); + ImGui.Dummy( _window._defaultSpace ); + + DrawSpecialAssignments(); + OpenTutorial( BasicTutorialSteps.SpecialCollections1 ); + + ImGui.Dummy( _window._defaultSpace ); + + DrawIndividualAssignments(); + OpenTutorial( BasicTutorialSteps.SpecialCollections2 ); ImGui.Dummy( _window._defaultSpace ); } @@ -263,11 +296,12 @@ public partial class ConfigWindow return; } + var width = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); ImGui.Dummy( _window._defaultSpace ); - DrawCurrentCollectionSelector(); + DrawCurrentCollectionSelector( width ); OpenTutorial( BasicTutorialSteps.CurrentCollection ); ImGui.Dummy( _window._defaultSpace ); - DrawNewCollectionInput(); + DrawNewCollectionInput( width ); ImGui.Dummy( _window._defaultSpace ); DrawInheritanceBlock(); OpenTutorial( BasicTutorialSteps.Inheritance ); diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index e352a91c..6962a84e 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -1,12 +1,22 @@ using System; using System.Runtime.CompilerServices; using OtterGui.Widgets; +using Penumbra.Collections; using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { + public const string SelectedCollection = "Selected Collection"; + public const string DefaultCollection = "Base Collection"; + public const string ActiveCollections = "Active Collections"; + public const string GroupAssignment = "Group Assignment"; + public const string CharacterGroups = "Character Groups"; + public const string ConditionalGroup = "Group"; + public const string ConditionalIndividual = "Character"; + public const string IndividualAssignments = "Individual Assignments"; + private static void UpdateTutorialStep() { var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); @@ -38,7 +48,8 @@ public partial class ConfigWindow Inheritance, ActiveCollections, DefaultCollection, - SpecialCollections, + SpecialCollections1, + SpecialCollections2, Mods, ModImport, AdvancedHelp, @@ -75,24 +86,29 @@ public partial class ConfigWindow + "Go here after setting up your root folder to continue the tutorial!" ) .Register( "Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" + "In here, we can create new collections, delete collections, or make them inherit from each other." ) - .Register( "Initial Setup, Step 5: Current Collection", - "We should already have a Default Collection, and for our simple setup, we do not need to do anything here.\n\n" - + "The current collection is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." ) + .Register( $"Initial Setup, Step 5: {SelectedCollection}", + $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." + + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n" ) .Register( "Inheritance", "This is a more advanced feature. Click the help button for more information, but we will ignore this for now." ) - .Register( "Initial Setup, Step 6: Active Collections", "Active Collections are those that are actually in use at the moment.\n\n" - + "Any collection in use will apply to the game under certain conditions.\n\n" - + "The Current Collection is also active for technical reasons.\n\n" + .Register( $"Initial Setup, Step 6: {ActiveCollections}", + $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" + + "Any collection assigned here will apply to the game under certain conditions.\n\n" + + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" + "Open this now to continue." ) - .Register( "Initial Setup, Step 7: Default Collection", - "The Default Collection - which should currently also be set to a collection named Default - is the main one.\n\n" - + "As long as no more specific collection applies to something, the mods from the Default Collection will be used.\n\n" - + "This is also the collection you need to use for all UI mods." ) - .Register( "Special Collections", - "Special Collections are those that are used only for special characters in the game, either by specific conditions, or by name.\n\n" - + "We will skip this for now, but hovering over the creation buttons should explain how they work." ) + .Register( $"Initial Setup, Step 7: {DefaultCollection}", + $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" + + "This is also the collection you need to use for all UI mods, music mods or any mods not associated with a character in the game at all." ) + .Register( GroupAssignment + 's', + "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" + + $"{IndividualAssignments} always take precedence before groups.") + .Register( IndividualAssignments, + "Collections assigned here are used only for individual characters or NPCs that have the specified name.\n\n" + + "They may also apply to objects 'owned' by those characters, e.g. minions or mounts - see the general settings for options on this.\n\n" ) .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" - + "Please go there after verifying that your Current Collection and Default Collection are setup to your liking." ) + + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking." ) .Register( "Initial Setup, Step 9: Mod Import", "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" @@ -100,8 +116,8 @@ public partial class ConfigWindow .Register( "Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" + "Import and select a mod now to continue." ) .Register( "Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here." ) - .Register( "Collection Selectors", "This row provides shortcuts to set your Current Collection.\n\n" - + "The first button sets it to your Default Collection (if any).\n\n" + .Register( "Collection Selectors", $"This row provides shortcuts to set your {SelectedCollection}.\n\n" + + $"The first button sets it to your {DefaultCollection} (if any).\n\n" + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" + "The third is a regular collection selector to let you choose among all your collections." ) .Register( "Initial Setup, Step 11: Enabling Mods", From 842b1c1fe543bcd1ff57db9cf5f170da06af0b59 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Jul 2022 15:42:41 +0200 Subject: [PATCH 0399/2451] Add some collected info to advanced edit tab. --- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 5 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.cs | 91 ++++++++++++++++--- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 8e5b1e95..299d644b 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -45,9 +45,9 @@ public partial class Mod } } - _availableFiles.RemoveAll( p => !p.File.Exists ); _duplicates.Clear(); DeleteEmptyDirectories( _mod.ModPath ); + UpdateFiles(); } private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager ) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 029c93fa..53c14729 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -120,7 +120,6 @@ public partial class Mod .Select( f => FileRegistry.FromFile( _mod, f, out var r ) ? r : null ) .OfType< FileRegistry >() ) .ToList(); - _usedPaths.Clear(); FileChanges = false; foreach( var subMod in _mod.AllSubMods ) @@ -137,7 +136,7 @@ public partial class Mod } else { - var registry = _availableFiles.FirstOrDefault( x => x.File.Equals( file ) ); + var registry = _availableFiles.Find( x => x.File.Equals( file ) ); if( registry != null ) { if( subMod == _subMod ) @@ -174,7 +173,7 @@ public partial class Mod return false; } - if( (pathIdx == - 1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty ) + 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 72f69ae6..2756489c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow } } - private string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color ) + private static string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color ) { (string, int) GetMulti() { diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 5d2fe47d..08709fd9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Numerics; +using System.Text; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; @@ -11,15 +12,16 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; using Penumbra.Util; +using static Penumbra.Mods.Mod; namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - private Mod.Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private const string WindowBaseLabel = "###SubModEdit"; + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; public void ChangeMod( Mod mod ) { @@ -29,9 +31,9 @@ public partial class ModEditWindow : Window, IDisposable } _editor?.Dispose(); - _editor = new Mod.Editor( mod, -1, 0 ); - _mod = mod; - WindowName = $"{mod.Name}{WindowBaseLabel}"; + _editor = new Editor( mod, -1, 0 ); + _mod = mod; + SizeConstraints = new WindowSizeConstraints { MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ), @@ -46,6 +48,73 @@ public partial class ModEditWindow : Window, IDisposable public override bool DrawConditions() => _editor != null; + public override void PreDraw() + { + var sb = new StringBuilder( 256 ); + + var redirections = 0; + var unused = 0; + var size = _editor!.AvailableFiles.Sum( f => + { + if( f.SubModUsage.Count > 0 ) + { + redirections += f.SubModUsage.Count; + } + else + { + ++unused; + } + + return f.FileSize; + } ); + var manipulations = 0; + var subMods = 0; + var swaps = _mod!.AllSubMods.Sum( m => + { + ++subMods; + manipulations += m.Manipulations.Count; + return m.FileSwaps.Count; + } ); + sb.Append( _mod!.Name ); + if( subMods > 1 ) + { + sb.AppendFormat( " | {0} Options", subMods ); + } + + if( size > 0 ) + { + sb.AppendFormat( " | {0} Files ({1})", _editor.AvailableFiles.Count, Functions.HumanReadableSize( size ) ); + } + + if( unused > 0 ) + { + sb.AppendFormat( " | {0} Unused Files", unused ); + } + + if( _editor.MissingFiles.Count > 0 ) + { + sb.AppendFormat( " | {0} Missing Files", _editor.MissingFiles.Count ); + } + + if( redirections > 0 ) + { + sb.AppendFormat( " | {0} Redirections", redirections ); + } + + if( manipulations > 0 ) + { + sb.AppendFormat( " | {0} Manipulations", manipulations ); + } + + if( swaps > 0 ) + { + sb.AppendFormat( " | {0} Swaps", swaps ); + } + + sb.Append( WindowBaseLabel ); + WindowName = sb.ToString(); + } + public override void Draw() { using var tabBar = ImRaii.TabBar( "##tabs" ); @@ -100,7 +169,7 @@ public partial class ModEditWindow : Window, IDisposable } } - public static void Draw( Mod.Editor editor, Vector2 buttonSize ) + public static void Draw( Editor editor, Vector2 buttonSize ) { DrawRaceCodeCombo( buttonSize ); ImGui.SameLine(); @@ -110,7 +179,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SetNextItemWidth( buttonSize.X ); ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); ImGui.SameLine(); - var disabled = !Mod.Editor.ValidString( _materialSuffixTo ); + var disabled = !Editor.ValidString( _materialSuffixTo ); var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." : _materialSuffixFrom == _materialSuffixTo @@ -335,7 +404,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.TableNextColumn(); ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); ImGui.TableNextColumn(); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { if( ImGui.GetWindowWidth() > 2 * width ) { @@ -398,7 +467,7 @@ public partial class ModEditWindow : Window, IDisposable return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; } - using var combo = ImRaii.Combo( "##optionSelector", GetLabel(), ImGuiComboFlags.NoArrowButton ); + using var combo = ImRaii.Combo( "##optionSelector", GetLabel(), ImGuiComboFlags.NoArrowButton ); if( !combo ) { return; From 0b2b0d1beb83dd70945a46ee288f2ddd03be83a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Jul 2022 16:25:20 +0200 Subject: [PATCH 0400/2451] Deal with multiple resource containers in resource manager. --- .../Interop/Loader/ResourceLoader.Debug.cs | 23 ++++++++++++------- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 4 ++-- Penumbra/UI/ConfigWindow.ResourceTab.cs | 4 ++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 87b79fe9..4fb934f4 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -15,8 +15,8 @@ namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { // If in debug mode, this logs any resource at refcount 0 that gets decremented again, and skips the decrement instead. - private delegate byte ResourceHandleDecRef( ResourceHandle* handle ); - private readonly Hook _decRefHook; + private delegate byte ResourceHandleDecRef( ResourceHandle* handle ); + private readonly Hook< ResourceHandleDecRef > _decRefHook; public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); @@ -146,8 +146,8 @@ public unsafe partial class ResourceLoader // Find a resource in the resource manager by its category, extension and crc-hash public static ResourceHandle* FindResource( ResourceCategory cat, ResourceType ext, uint crc32 ) { - var manager = *ResourceManager; - var catIdx = ( uint )cat >> 0x18; + ref var manager = ref *ResourceManager; + var catIdx = ( uint )cat >> 0x18; cat = ( ResourceCategory )( ushort )cat; var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ], @@ -161,18 +161,25 @@ public unsafe partial class ResourceLoader return ret == null ? null : ret->Value; } - public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph ); + public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph, int idx ); public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph ); public delegate void ResourceAction( uint crc32, ResourceHandle* graph ); // Iteration functions through the resource manager. public static void IterateGraphs( ExtMapAction action ) { - var manager = *ResourceManager; + ref var manager = ref *ResourceManager; foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) ) { var graph = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )resourceType; - action( resourceType, graph->MainMap ); + for( var i = 0; i < 20; ++i ) + { + var map = ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )graph->CategoryMaps[ i ]; + if( map != null ) + { + action( resourceType, map, i ); + } + } } } @@ -184,7 +191,7 @@ public unsafe partial class ResourceLoader public static void IterateResources( ResourceAction action ) { - IterateGraphs( ( _, extMap ) + IterateGraphs( ( _, extMap, _ ) => IterateExtMap( extMap, ( _, resourceMap ) => IterateResourceMap( resourceMap, action ) ) ); } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 299d644b..4dbb9bff 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -235,7 +235,7 @@ public partial class Mod // 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 ) + private static void DeleteEmptyDirectories( DirectoryInfo baseDir ) { try { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 56b4a58e..2377bb77 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -564,8 +564,8 @@ public partial class ConfigWindow // Move from one group to another by deleting, then adding the option. var sourceGroup = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; - var option = @group[ _dragDropOptionIdx ]; - var priority = @group.OptionPriority( _dragDropGroupIdx ); + var option = group[ _dragDropOptionIdx ]; + var priority = group.OptionPriority( _dragDropGroupIdx ); panel._delayedActions.Enqueue( () => { Penumbra.ModManager.DeleteOption( panel._mod, sourceGroup, sourceOption ); diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index f72a5dc6..801878cf 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -119,14 +119,14 @@ public partial class ConfigWindow // Draw a full category for the resource manager. private unsafe void DrawCategoryContainer( ResourceCategory category, - StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) + StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, int idx ) { if( map == null ) { return; } - using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}" ); + using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} (Ex {idx}) - {map->Count}###{( uint )category}_{idx}" ); if( tree ) { SetTableWidths(); From d6c03624045337128ba3a9e903a52fb2fe496c56 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 10:37:45 +0200 Subject: [PATCH 0401/2451] Let SubMods know their location in a mod. --- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 33 +++----- Penumbra/Mods/Editor/Mod.Editor.Meta.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.cs | 9 +-- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 36 ++++++--- Penumbra/Mods/Mod.BasePath.cs | 5 +- Penumbra/Mods/Mod.Creation.cs | 4 +- Penumbra/Mods/Mod.Files.cs | 10 +-- Penumbra/Mods/Mod.Meta.Migration.cs | 12 +-- Penumbra/Mods/Mod.TemporaryMod.cs | 5 +- Penumbra/Mods/Subclasses/IModGroup.cs | 1 + Penumbra/Mods/Subclasses/ISubMod.cs | 3 + .../Subclasses/Mod.Files.MultiModGroup.cs | 30 +++++-- .../Subclasses/Mod.Files.SingleModGroup.cs | 26 +++++- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 35 ++++---- Penumbra/UI/Classes/ModEditWindow.Files.cs | 79 ++++++++++++++++++- Penumbra/UI/Classes/ModEditWindow.cs | 48 +++-------- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 4 +- 18 files changed, 223 insertions(+), 121 deletions(-) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 4dbb9bff..b5bd538c 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -270,7 +270,7 @@ public partial class Mod { var mod = new Mod( modDirectory ); mod.Reload( out _ ); - var editor = new Editor( mod, 0, 0 ); + var editor = new Editor( mod, mod.Default ); editor.DuplicatesFinished = false; editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() ); editor.DeleteDuplicates( false ); diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 1e702b67..3b27f573 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -9,34 +9,16 @@ public partial class Mod { public partial class Editor { - public int GroupIdx { get; private set; } = -1; - public int OptionIdx { get; private set; } - - private IModGroup? _modGroup; - private SubMod _subMod; + private SubMod _subMod; public ISubMod CurrentOption => _subMod; public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); - public void SetSubMod( int groupIdx, int optionIdx ) + public void SetSubMod( ISubMod? subMod ) { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - if( groupIdx >= 0 && groupIdx < _mod.Groups.Count && optionIdx >= 0 && optionIdx < _mod.Groups[ groupIdx ].Count ) - { - _modGroup = _mod.Groups[ groupIdx ]; - _subMod = ( SubMod )_modGroup![ optionIdx ]; - } - else - { - GroupIdx = -1; - OptionIdx = 0; - _modGroup = null; - _subMod = _mod._default; - } - + _subMod = subMod as SubMod ?? _mod._default; UpdateFiles(); RevertSwaps(); RevertManipulations(); @@ -54,11 +36,16 @@ public partial class Mod } } - Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, dict ); + Penumbra.ModManager.OptionSetFiles( _mod, _subMod.GroupIdx, _subMod.OptionIdx, dict ); if( num > 0 ) + { RevertFiles(); + } else + { FileChanges = false; + } + return num; } @@ -67,7 +54,7 @@ public partial class Mod public void ApplySwaps() { - Penumbra.ModManager.OptionSetFileSwaps( _mod, GroupIdx, OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + Penumbra.ModManager.OptionSetFileSwaps( _mod, _subMod.GroupIdx, _subMod.OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); } public void RevertSwaps() diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs index c0908a9c..0eec38ea 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs @@ -159,7 +159,7 @@ public partial class Mod public void ApplyManipulations() { - Meta.Apply( _mod, GroupIdx, OptionIdx ); + Meta.Apply( _mod, _subMod.GroupIdx, _subMod.OptionIdx ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index a1394401..3120bcf3 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -10,12 +10,11 @@ public partial class Mod : IMod { private readonly Mod _mod; - public Editor( Mod mod, int groupIdx, int optionIdx ) + public Editor( Mod mod, ISubMod? option ) { - _mod = mod; - SetSubMod( groupIdx, optionIdx ); - GroupIdx = groupIdx; - _subMod = _mod._default; + _mod = mod; + _subMod = null!; + SetSubMod( option ); UpdateFiles(); ScanModels(); } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 86d97fd0..6d35b5d1 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; +using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -77,6 +78,14 @@ public sealed partial class Mod { if( mod._groups.Move( groupIdxFrom, groupIdxTo ) ) { + foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( Math.Min( groupIdxFrom, groupIdxTo ) ) ) + { + foreach( var (o, optionIdx) in group.OfType().WithIndex() ) + { + o.SetPosition( groupIdx, optionIdx ); + } + } + ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo ); } } @@ -162,17 +171,19 @@ public sealed partial class Mod public void AddOption( Mod mod, int groupIdx, string newName ) { - switch( mod._groups[ groupIdx ] ) + var group = mod._groups[groupIdx]; + switch( group ) { case SingleModGroup s: - s.OptionData.Add( new SubMod { Name = newName } ); + s.OptionData.Add( new SubMod(mod) { Name = newName } ); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( new SubMod { Name = newName }, 0 ) ); + m.PrioritizedOptions.Add( ( new SubMod(mod) { Name = newName }, 0 ) ); break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 ); + group.UpdatePositions( group.Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); } public void AddOption( Mod mod, int groupIdx, ISubMod option, int priority = 0 ) @@ -182,15 +193,16 @@ public sealed partial class Mod return; } - if( mod._groups[ groupIdx ].Count > 63 ) + var group = mod._groups[ groupIdx ]; + if( group.Count > 63 ) { PluginLog.Error( - $"Could not add option {option.Name} to {mod._groups[ groupIdx ].Name} for mod {mod.Name}, " + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + "since only up to 64 options are supported in one group." ); return; } - switch( mod._groups[ groupIdx ] ) + switch( group ) { case SingleModGroup s: s.OptionData.Add( o ); @@ -199,23 +211,25 @@ public sealed partial class Mod m.PrioritizedOptions.Add( ( o, priority ) ); break; } - - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 ); + group.UpdatePositions( group.Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); } public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) { + var group = mod._groups[groupIdx]; ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); - switch( mod._groups[ groupIdx ] ) + switch( group ) { case SingleModGroup s: s.OptionData.RemoveAt( optionIdx ); + break; case MultiModGroup m: m.PrioritizedOptions.RemoveAt( optionIdx ); break; } - + group.UpdatePositions( optionIdx ); ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 ); } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 0add9356..2e01396f 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -22,7 +22,10 @@ public partial class Mod => 0; private Mod( DirectoryInfo modPath ) - => ModPath = modPath; + { + ModPath = modPath; + _default = new SubMod( this ); + } private static Mod? LoadMod( DirectoryInfo modPath ) { diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index b42a3b7d..abe11f25 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -97,7 +97,7 @@ public partial class Mod .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) .Where( t => t.Item1 ); - var mod = new SubMod + var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. { Name = option.Name, }; @@ -112,7 +112,7 @@ public partial class Mod // Create an empty sub mod for single groups with None options. internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod() + => new SubMod(null! ) // Mod is irrelevant here, only used for saving. { Name = name, }; diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index e22f887f..87c69a82 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -18,7 +18,7 @@ public partial class Mod public IReadOnlyList< IModGroup > Groups => _groups; - private readonly SubMod _default = new(); + private readonly SubMod _default; private readonly List< IModGroup > _groups = new(); public int TotalFileCount { get; private set; } @@ -70,7 +70,7 @@ public partial class Mod .ToList(); } - private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) + private static IModGroup? LoadModGroup( Mod mod, FileInfo file, int groupIdx ) { if( !File.Exists( file.FullName ) ) { @@ -82,8 +82,8 @@ public partial class Mod var json = JObject.Parse( File.ReadAllText( file.FullName ) ); switch( json[ nameof( Type ) ]?.ToObject< SelectType >() ?? SelectType.Single ) { - case SelectType.Multi: return MultiModGroup.Load( json, basePath ); - case SelectType.Single: return SingleModGroup.Load( json, basePath ); + case SelectType.Multi: return MultiModGroup.Load( mod, json, groupIdx ); + case SelectType.Single: return SingleModGroup.Load( mod, json, groupIdx ); } } catch( Exception e ) @@ -100,7 +100,7 @@ public partial class Mod var changes = false; foreach( var file in GroupFiles ) { - var group = LoadModGroup( file, ModPath ); + var group = LoadModGroup( this, file, _groups.Count ); if( group != null && _groups.All( g => g.Name != group.Name ) ) { changes = changes || group.FileName( ModPath, _groups.Count ) != file.FullName; diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 3f360ada..1587864f 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -141,7 +141,7 @@ public sealed partial class Mod mod._groups.Add( newMultiGroup ); foreach( var option in group.Options ) { - newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.ModPath, option, seenMetaFiles ), optionPriority++ ) ); + newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod, option, seenMetaFiles ), optionPriority++ ) ); } break; @@ -161,7 +161,7 @@ public sealed partial class Mod mod._groups.Add( newSingleGroup ); foreach( var option in group.Options ) { - newSingleGroup.OptionData.Add( SubModFromOption( mod.ModPath, option, seenMetaFiles ) ); + newSingleGroup.OptionData.Add( SubModFromOption( mod, option, seenMetaFiles ) ); } break; @@ -185,11 +185,11 @@ public sealed partial class Mod } } - private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles ) + private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) { - var subMod = new SubMod { Name = option.OptionName }; - AddFilesToSubMod( subMod, basePath, option, seenMetaFiles ); - subMod.IncorporateMetaChanges( basePath, false ); + var subMod = new SubMod(mod) { Name = option.OptionName }; + AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); + subMod.IncorporateMetaChanges( mod.ModPath, false ); return subMod; } diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 35a4795a..10d90979 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -26,7 +26,10 @@ public sealed partial class Mod public IEnumerable< ISubMod > AllSubMods => new[] { Default }; - private readonly SubMod _default = new(); + private readonly SubMod _default; + + public TemporaryMod() + => _default = new SubMod( this ); public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) => _default.FileData[ gamePath ] = fullPath; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 8482af4a..190ca399 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -97,4 +97,5 @@ public interface IModGroup : IEnumerable< ISubMod > public IModGroup Convert( SelectType type ); public bool MoveOption( int optionIdxFrom, int optionIdxTo ); + public void UpdatePositions(int from = 0); } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index f6781566..01bfefc6 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -9,11 +9,14 @@ namespace Penumbra.Mods; public interface ISubMod { public string Name { get; } + public string FullName { get; } public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } public IReadOnlySet< MetaManipulation > Manipulations { get; } + public bool IsDefault { get; } + public static void WriteSubMod( JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority ) { j.WriteStartObject(); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index d84ae760..fc735981 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -6,6 +6,7 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Filesystem; namespace Penumbra.Mods; @@ -40,7 +41,7 @@ public partial class Mod IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static MultiModGroup? Load( JObject json, DirectoryInfo basePath ) + public static MultiModGroup? Load( Mod mod, JObject json, int groupIdx ) { var options = json[ "Options" ]; var ret = new MultiModGroup() @@ -60,11 +61,14 @@ public partial class Mod { if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) { - PluginLog.Warning($"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options." ); + PluginLog.Warning( + $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options." ); break; } - var subMod = new SubMod(); - subMod.Load( basePath, child, out var priority ); + + var subMod = new SubMod( mod ); + subMod.SetPosition( groupIdx, ret.PrioritizedOptions.Count ); + subMod.Load( mod.ModPath, child, out var priority ); ret.PrioritizedOptions.Add( ( subMod, priority ) ); } } @@ -91,6 +95,22 @@ public partial class Mod } public bool MoveOption( int optionIdxFrom, int optionIdxTo ) - => PrioritizedOptions.Move( optionIdxFrom, optionIdxTo ); + { + if( !PrioritizedOptions.Move( optionIdxFrom, optionIdxTo ) ) + { + return false; + } + + UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + return true; + } + + public void UpdatePositions( int from = 0 ) + { + foreach( var ((o, _), i) in PrioritizedOptions.WithIndex().Skip( from ) ) + { + o.SetPosition( o.GroupIdx, i ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index 352bb503..8cfb775c 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Filesystem; namespace Penumbra.Mods; @@ -39,7 +40,7 @@ public partial class Mod IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static SingleModGroup? Load( JObject json, DirectoryInfo basePath ) + public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) { var options = json[ "Options" ]; var ret = new SingleModGroup @@ -57,8 +58,9 @@ public partial class Mod { foreach( var child in options.Children() ) { - var subMod = new SubMod(); - subMod.Load( basePath, child, out _ ); + var subMod = new SubMod( mod ); + subMod.SetPosition( groupIdx, ret.OptionData.Count ); + subMod.Load( mod.ModPath, child, out _ ); ret.OptionData.Add( subMod ); } } @@ -85,6 +87,22 @@ public partial class Mod } public bool MoveOption( int optionIdxFrom, int optionIdxTo ) - => OptionData.Move( optionIdxFrom, optionIdxTo ); + { + if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) + { + return false; + } + + UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + return true; + } + + public void UpdatePositions( int from = 0 ) + { + foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) + { + o.SetPosition( o.GroupIdx, i ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index a1500504..9a13e642 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -42,6 +42,7 @@ public partial class Mod private void LoadDefaultOption() { var defaultFile = DefaultFile; + _default.SetPosition( -1, 0 ); try { if( !File.Exists( defaultFile ) ) @@ -72,10 +73,23 @@ public partial class Mod { public string Name { get; set; } = "Default"; + public string FullName + => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + + internal IMod ParentMod { get; private init; } + internal int GroupIdx { get; private set; } + internal int OptionIdx { get; private set; } + + public bool IsDefault + => GroupIdx < 0; + public Dictionary< Utf8GamePath, FullPath > FileData = new(); public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); public HashSet< MetaManipulation > ManipulationData = new(); + public SubMod( IMod parentMod ) + => ParentMod = parentMod; + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files => FileData; @@ -85,25 +99,10 @@ public partial class Mod public IReadOnlySet< MetaManipulation > Manipulations => ManipulationData; - // Insert all changes from the other submod. - // Overwrites already existing changes in this mod. - public void MergeIn( ISubMod other ) + public void SetPosition( int groupIdx, int optionIdx ) { - foreach( var (key, value) in other.Files ) - { - FileData[ key ] = value; - } - - foreach( var (key, value) in other.FileSwaps ) - { - FileSwapData[ key ] = value; - } - - foreach( var manip in other.Manipulations ) - { - ManipulationData.Remove( manip ); - ManipulationData.Add( manip ); - } + GroupIdx = groupIdx; + OptionIdx = optionIdx; } public void Load( DirectoryInfo basePath, JToken json, out int priority ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 2756489c..400b528b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -23,6 +24,7 @@ public partial class ModEditWindow private int _fileIdx = -1; private int _pathIdx = -1; private int _folderSkip = 0; + private bool _overviewMode = false; private bool CheckFilter( Mod.Editor.FileRegistry registry ) => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); @@ -47,7 +49,73 @@ public partial class ModEditWindow return; } + if( _overviewMode ) + DrawFilesOverviewMode(); + else + DrawFilesNormalMode(); + + } + + private void DrawFilesOverviewMode() + { + using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + + if( !list ) + { + return; + } + + var idx = 0; + void Draw( Mod.Editor.FileRegistry registry ) + { + if( registry.SubModUsage.Count == 0 ) + { + using var id = ImRaii.PushId( idx++ ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.Selectable( registry.RelPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TextUnformatted( "Unused" ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableNextColumn(); + } + else + { + foreach( var (mod, path) in registry.SubModUsage ) + { + using var id = ImRaii.PushId( idx++ ); + var color = mod == _editor.CurrentOption && _mod!.HasOptions; + ImGui.TableNextColumn(); + if( color ) + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); + ImGui.Selectable( registry.RelPath.ToString() ); + ImGui.TableNextColumn(); + if( color ) + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); + ImGui.Selectable( path.ToString() ); + ImGui.TableNextColumn(); + if( color ) + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); + ImGui.TextUnformatted( mod.Name ); + } + } + } + + bool Filter( Mod.Editor.FileRegistry registry ) + { + return true; + } + + var skips = ImGuiClip.GetNecessarySkips( ImGui.GetTextLineHeight() ); + var end = ImGuiClip.FilteredClippedDraw( _editor!.AvailableFiles, skips, Filter, Draw, 0 ); + ImGuiClip.DrawEndDummy( end, ImGui.GetTextLineHeight() ); + } + + private void DrawFilesNormalMode() + { using var list = ImRaii.Table( "##table", 1 ); + if( !list ) { return; @@ -68,7 +136,7 @@ public partial class ModEditWindow using var indent = ImRaii.PushIndent( 50f ); for( var j = 0; j < registry.SubModUsage.Count; ++j ) { - var (subMod, gamePath) = registry.SubModUsage[ j ]; + var (subMod, gamePath) = registry.SubModUsage[j]; if( subMod != _editor.CurrentOption ) { continue; @@ -158,6 +226,7 @@ public partial class ModEditWindow { _editor!.SetGamePath( _fileIdx, _pathIdx, path ); } + _fileIdx = -1; _pathIdx = -1; } @@ -180,6 +249,7 @@ public partial class ModEditWindow { _editor!.SetGamePath( _fileIdx, _pathIdx, path ); } + _fileIdx = -1; _pathIdx = -1; } @@ -241,6 +311,13 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." ); + ImGui.SameLine(); + ImGui.Checkbox( "Overview Mode", ref _overviewMode ); + if( _overviewMode ) + { + return; + } + ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength ); ImGui.SameLine(); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 08709fd9..898a98f2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -31,7 +31,7 @@ public partial class ModEditWindow : Window, IDisposable } _editor?.Dispose(); - _editor = new Editor( mod, -1, 0 ); + _editor = new Editor( mod, mod.Default ); _mod = mod; SizeConstraints = new WindowSizeConstraints @@ -42,8 +42,8 @@ public partial class ModEditWindow : Window, IDisposable _selectedFiles.Clear(); } - public void ChangeOption( int groupIdx, int optionIdx ) - => _editor?.SetSubMod( groupIdx, optionIdx ); + public void ChangeOption( ISubMod? subMod ) + => _editor?.SetSubMod( subMod ); public override bool DrawConditions() => _editor != null; @@ -437,56 +437,34 @@ public partial class ModEditWindow : Window, IDisposable private void DrawOptionSelectHeader() { - const string defaultOption = "Default Option"; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); - var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); - var isDefaultOption = _editor!.GroupIdx == -1 && _editor!.OptionIdx == 0; + const string defaultOption = "Default Option"; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); + var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); if( ImGuiUtil.DrawDisabledButton( defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - isDefaultOption ) ) + _editor!.CurrentOption.IsDefault ) ) { - _editor.SetSubMod( -1, 0 ); - isDefaultOption = true; + _editor.SetSubMod( _mod!.Default ); } ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( "Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false ) ) { - _editor.SetSubMod( _editor.GroupIdx, _editor.OptionIdx ); + _editor.SetSubMod( _editor.CurrentOption ); } ImGui.SameLine(); - string GetLabel() - { - if( isDefaultOption ) - { - return defaultOption; - } - - var group = _mod!.Groups[ _editor!.GroupIdx ]; - return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; - } - - using var combo = ImRaii.Combo( "##optionSelector", GetLabel(), ImGuiComboFlags.NoArrowButton ); + using var combo = ImRaii.Combo( "##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton ); if( !combo ) { return; } - if( ImGui.Selectable( $"{defaultOption}###-1_0", isDefaultOption ) ) + foreach( var option in _mod!.AllSubMods ) { - _editor.SetSubMod( -1, 0 ); - } - - foreach( var (group, groupIdx) in _mod!.Groups.WithIndex() ) - { - foreach( var (option, optionIdx) in group.WithIndex() ) + if( ImGui.Selectable( option.FullName, option == _editor.CurrentOption ) ) { - var name = $"{group.Name}: {option.Name}###{groupIdx}_{optionIdx}"; - if( ImGui.Selectable( name, groupIdx == _editor.GroupIdx && optionIdx == _editor.OptionIdx ) ) - { - _editor.SetSubMod( groupIdx, optionIdx ); - } + _editor.SetSubMod( option ); } } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 0d90693a..5d9cc3f4 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -61,7 +61,7 @@ public partial class ConfigWindow if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( -1, 0 ); + _window.ModEditPopup.ChangeOption( _mod.Default ); _window.ModEditPopup.IsOpen = true; } @@ -142,7 +142,7 @@ public partial class ConfigWindow { var priority = conflict.Mod2.Index < 0 ? conflict.Mod2.Priority - : Penumbra.CollectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + : Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority; ImGui.TextUnformatted( $"(Priority {priority})" ); } From 7a7093369f6a7bc961dea42a226fcccfd542cc7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 12:59:33 +0200 Subject: [PATCH 0402/2451] Add Overview mode to file redirection edit. --- OtterGui | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 4 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 145 +++++++++++++------- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 1 - 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/OtterGui b/OtterGui index 69a8ee3a..f137f521 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 69a8ee3ae21480123881bc93ac0458671e7d0c46 +Subproject commit f137f521c588472247510a3fd4183bd651477618 diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 4d4c0c31..c329b3de 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -276,8 +276,8 @@ public partial class ModCollection case SelectType.Multi: { foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) { AddSubMod( option, mod ); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 400b528b..fd46e0ae 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -11,20 +10,23 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256); - private LowerString _fileFilter = LowerString.Empty; - private bool _showGamePaths = true; - private string _gamePathEdit = string.Empty; - private int _fileIdx = -1; - private int _pathIdx = -1; - private int _folderSkip = 0; - private bool _overviewMode = false; + private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256); + private LowerString _fileFilter = LowerString.Empty; + private bool _showGamePaths = true; + private string _gamePathEdit = string.Empty; + private int _fileIdx = -1; + private int _pathIdx = -1; + private int _folderSkip = 0; + private bool _overviewMode = false; + private LowerString _fileOverviewFilter1 = LowerString.Empty; + private LowerString _fileOverviewFilter2 = LowerString.Empty; + private LowerString _fileOverviewFilter3 = LowerString.Empty; + private bool CheckFilter( Mod.Editor.FileRegistry registry ) => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); @@ -43,6 +45,15 @@ public partial class ModEditWindow DrawOptionSelectHeader(); DrawButtonHeader(); + if( _overviewMode ) + { + DrawFileManagementOverview(); + } + else + { + DrawFileManagementNormal(); + } + using var child = ImRaii.Child( "##files", -Vector2.One, true ); if( !child ) { @@ -50,66 +61,77 @@ public partial class ModEditWindow } if( _overviewMode ) + { DrawFilesOverviewMode(); + } else + { DrawFilesNormalMode(); - + } } private void DrawFilesOverviewMode() { - using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips( height ); + + using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One ); if( !list ) { return; } + var width = ImGui.GetContentRegionAvail().X / 8; + + ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, width * 3 ); + ImGui.TableSetupColumn( "##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize ); + ImGui.TableSetupColumn( "##option", ImGuiTableColumnFlags.WidthFixed, width * 2 ); + var idx = 0; - void Draw( Mod.Editor.FileRegistry registry ) + + var files = _editor!.AvailableFiles.SelectMany( f => { - if( registry.SubModUsage.Count == 0 ) + var file = f.RelPath.ToString(); + return f.SubModUsage.Count == 0 + ? Enumerable.Repeat( ( file, "Unused", string.Empty, 0x40000080u ), 1 ) + : f.SubModUsage.Select( s => ( file, s.Item2.ToString(), s.Item1.FullName, + _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u ) ); + } ); + + void DrawLine( (string, string, string, uint) data ) + { + using var id = ImRaii.PushId( idx++ ); + ImGui.TableNextColumn(); + if( data.Item4 != 0 ) { - using var id = ImRaii.PushId( idx++ ); - ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); - ImGui.Selectable( registry.RelPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); - ImGui.TextUnformatted( "Unused" ); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); - ImGui.TableNextColumn(); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); } - else + + ImGuiUtil.CopyOnClickSelectable( data.Item1 ); + ImGui.TableNextColumn(); + if( data.Item4 != 0 ) { - foreach( var (mod, path) in registry.SubModUsage ) - { - using var id = ImRaii.PushId( idx++ ); - var color = mod == _editor.CurrentOption && _mod!.HasOptions; - ImGui.TableNextColumn(); - if( color ) - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); - ImGui.Selectable( registry.RelPath.ToString() ); - ImGui.TableNextColumn(); - if( color ) - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); - ImGui.Selectable( path.ToString() ); - ImGui.TableNextColumn(); - if( color ) - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40008000 ); - ImGui.TextUnformatted( mod.Name ); - } + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); } + + ImGuiUtil.CopyOnClickSelectable( data.Item2 ); + ImGui.TableNextColumn(); + if( data.Item4 != 0 ) + { + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); + } + + ImGuiUtil.CopyOnClickSelectable( data.Item3 ); } - bool Filter( Mod.Editor.FileRegistry registry ) - { - return true; - } + bool Filter( (string, string, string, uint) data ) + => _fileOverviewFilter1.IsContained( data.Item1 ) + && _fileOverviewFilter2.IsContained( data.Item2 ) + && _fileOverviewFilter3.IsContained( data.Item3 ); - var skips = ImGuiClip.GetNecessarySkips( ImGui.GetTextLineHeight() ); - var end = ImGuiClip.FilteredClippedDraw( _editor!.AvailableFiles, skips, Filter, Draw, 0 ); - ImGuiClip.DrawEndDummy( end, ImGui.GetTextLineHeight() ); + var end = ImGuiClip.FilteredClippedDraw( files, skips, Filter, DrawLine ); + ImGuiClip.DrawEndDummy( end, height ); } private void DrawFilesNormalMode() @@ -136,7 +158,7 @@ public partial class ModEditWindow using var indent = ImRaii.PushIndent( 50f ); for( var j = 0; j < registry.SubModUsage.Count; ++j ) { - var (subMod, gamePath) = registry.SubModUsage[j]; + var (subMod, gamePath) = registry.SubModUsage[ j ]; if( subMod != _editor.CurrentOption ) { continue; @@ -313,11 +335,10 @@ public partial class ModEditWindow ImGui.SameLine(); ImGui.Checkbox( "Overview Mode", ref _overviewMode ); - if( _overviewMode ) - { - return; - } + } + private void DrawFileManagementNormal() + { ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength ); ImGui.SameLine(); @@ -350,4 +371,22 @@ public partial class ModEditWindow ImGuiUtil.RightAlign( $"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected" ); } + + private void DrawFileManagementOverview() + { + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ) + .Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ) + .Push( ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize ); + + var width = ImGui.GetContentRegionAvail().X / 8; + + ImGui.SetNextItemWidth( width * 3 ); + LowerString.InputWithHint( "##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( width * 3 ); + LowerString.InputWithHint( "##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( width * 2 ); + LowerString.InputWithHint( "##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength ); + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 33f01f64..35a9d956 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -12,7 +12,6 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI.Classes; From ee48c7803c2b21a19eb4b0584986eb5ba1a90c09 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 13:04:57 +0200 Subject: [PATCH 0403/2451] Some character equip changes. --- Penumbra.GameData/Structs/CharacterEquip.cs | 216 ++++++++++---------- 1 file changed, 111 insertions(+), 105 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index c16a69a5..5e8489a1 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -1,105 +1,111 @@ -using System; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer; - -public readonly unsafe struct CharacterEquip -{ - public static readonly CharacterEquip Null = new(null); - - private readonly CharacterArmor* _armor; - - public IntPtr Address - => (IntPtr)_armor; - - public ref CharacterArmor this[int idx] - => ref _armor[idx]; - - public ref CharacterArmor this[uint idx] - => ref _armor[idx]; - - public ref CharacterArmor this[EquipSlot slot] - => ref _armor[IndexOf(slot)]; - - - public ref CharacterArmor Head - => ref _armor[0]; - - public ref CharacterArmor Body - => ref _armor[1]; - - public ref CharacterArmor Hands - => ref _armor[2]; - - public ref CharacterArmor Legs - => ref _armor[3]; - - public ref CharacterArmor Feet - => ref _armor[4]; - - public ref CharacterArmor Ears - => ref _armor[5]; - - public ref CharacterArmor Neck - => ref _armor[6]; - - public ref CharacterArmor Wrists - => ref _armor[7]; - - public ref CharacterArmor RFinger - => ref _armor[8]; - - public ref CharacterArmor LFinger - => ref _armor[9]; - - public CharacterEquip(CharacterArmor* val) - => _armor = val; - - public static implicit operator CharacterEquip(CharacterArmor* val) - => new(val); - - public static implicit operator CharacterEquip(IntPtr val) - => new((CharacterArmor*)val); - - public static implicit operator CharacterEquip(ReadOnlySpan val) - { - if (val.Length != 10) - throw new ArgumentException("Invalid number of equipment pieces in span."); - - fixed (CharacterArmor* ptr = val) - { - return new CharacterEquip(ptr); - } - } - - public static implicit operator bool(CharacterEquip equip) - => equip._armor != null; - - public static bool operator true(CharacterEquip equip) - => equip._armor != null; - - public static bool operator false(CharacterEquip equip) - => equip._armor == null; - - public static bool operator !(CharacterEquip equip) - => equip._armor == null; - - private static int IndexOf(EquipSlot slot) - { - return slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 1, - EquipSlot.Hands => 2, - EquipSlot.Legs => 3, - EquipSlot.Feet => 4, - EquipSlot.Ears => 5, - EquipSlot.Neck => 6, - EquipSlot.Wrists => 7, - EquipSlot.RFinger => 8, - EquipSlot.LFinger => 9, - _ => throw new ArgumentOutOfRangeException(nameof(slot), slot, null), - }; - } -} +using System; +using Penumbra.GameData.Enums; + +namespace Penumbra.GameData.Structs; + +public unsafe struct CharacterArmorData +{ + public fixed byte Data[40]; +} + +public readonly unsafe struct CharacterEquip +{ + public static readonly CharacterEquip Null = new(null); + + private readonly CharacterArmor* _armor; + + public IntPtr Address + => ( IntPtr )_armor; + + public ref CharacterArmor this[ int idx ] + => ref _armor[ idx ]; + + public ref CharacterArmor this[ uint idx ] + => ref _armor[ idx ]; + + public ref CharacterArmor this[ EquipSlot slot ] + => ref _armor[ IndexOf( slot ) ]; + + + public ref CharacterArmor Head + => ref _armor[ 0 ]; + + public ref CharacterArmor Body + => ref _armor[ 1 ]; + + public ref CharacterArmor Hands + => ref _armor[ 2 ]; + + public ref CharacterArmor Legs + => ref _armor[ 3 ]; + + public ref CharacterArmor Feet + => ref _armor[ 4 ]; + + public ref CharacterArmor Ears + => ref _armor[ 5 ]; + + public ref CharacterArmor Neck + => ref _armor[ 6 ]; + + public ref CharacterArmor Wrists + => ref _armor[ 7 ]; + + public ref CharacterArmor RFinger + => ref _armor[ 8 ]; + + public ref CharacterArmor LFinger + => ref _armor[ 9 ]; + + public CharacterEquip( CharacterArmor* val ) + => _armor = val; + + public static implicit operator CharacterEquip( CharacterArmor* val ) + => new(val); + + public static implicit operator CharacterEquip( IntPtr val ) + => new(( CharacterArmor* )val); + + public static implicit operator CharacterEquip( ReadOnlySpan< CharacterArmor > val ) + { + if( val.Length != 10 ) + { + throw new ArgumentException( "Invalid number of equipment pieces in span." ); + } + + fixed( CharacterArmor* ptr = val ) + { + return new CharacterEquip( ptr ); + } + } + + public static implicit operator bool( CharacterEquip equip ) + => equip._armor != null; + + public static bool operator true( CharacterEquip equip ) + => equip._armor != null; + + public static bool operator false( CharacterEquip equip ) + => equip._armor == null; + + public static bool operator !( CharacterEquip equip ) + => equip._armor == null; + + private static int IndexOf( EquipSlot slot ) + { + return slot switch + { + EquipSlot.Head => 0, + EquipSlot.Body => 1, + EquipSlot.Hands => 2, + EquipSlot.Legs => 3, + EquipSlot.Feet => 4, + EquipSlot.Ears => 5, + EquipSlot.Neck => 6, + EquipSlot.Wrists => 7, + EquipSlot.RFinger => 8, + EquipSlot.LFinger => 9, + _ => throw new ArgumentOutOfRangeException( nameof( slot ), slot, null ), + }; + } +} \ No newline at end of file From 7305ad41acfc006cc60ecda10e735bf858c075af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 16:00:37 +0200 Subject: [PATCH 0404/2451] Allow Penumbra to import regular archives for penumbra mods. --- Penumbra/Import/TexToolsImport.cs | 41 +++---- Penumbra/Import/TexToolsImporter.Archives.cs | 123 +++++++++++++++++++ Penumbra/Import/TexToolsImporter.ModPack.cs | 10 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- 5 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 Penumbra/Import/TexToolsImporter.Archives.cs diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 73cb9029..3dcf5d47 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,14 +1,16 @@ 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.Mods; using FileMode = System.IO.FileMode; +using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; +using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry; namespace Penumbra.Import; @@ -119,8 +121,13 @@ public partial class TexToolsImporter : IDisposable // Puts out warnings if extension does not correspond to data. private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) { + if( modPackFile.Extension is ".zip" or ".7z" or ".rar" ) + { + return HandleRegularArchive( modPackFile ); + } + using var zfs = modPackFile.OpenRead(); - using var extractedModPack = new ZipFile( zfs ); + using var extractedModPack = ZipArchive.Open( zfs ); var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); if( mpl == null ) @@ -128,7 +135,7 @@ public partial class TexToolsImporter : IDisposable throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); } - var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); + var modRaw = GetStringFromZipEntry( mpl, Encoding.UTF8 ); // At least a better validation than going by the extension. if( modRaw.Contains( "\"TTMPVersion\":" ) ) @@ -149,30 +156,14 @@ public partial class TexToolsImporter : IDisposable return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); } - // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry - private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) - { - for( var i = 0; i < file.Count; i++ ) - { - var entry = file[ i ]; + private static ZipArchiveEntry? FindZipEntry( ZipArchive file, string fileName ) + => file.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.Contains( fileName ) ); - if( entry.Name.Contains( fileName ) ) - { - return entry; - } - } - - return null; - } - - private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) - => file.GetInputStream( entry ); - - private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) + private static string GetStringFromZipEntry( ZipArchiveEntry entry, Encoding encoding ) { using var ms = new MemoryStream(); - using var s = GetStreamFromZipEntry( file, entry ); + using var s = entry.OpenEntryStream(); s.CopyTo( ms ); return encoding.GetString( ms.ToArray() ); } @@ -191,7 +182,7 @@ public partial class TexToolsImporter : IDisposable _tmpFileStream = null; } - private StreamDisposer GetSqPackStreamStream( ZipFile file, string entryName ) + private StreamDisposer GetSqPackStreamStream( ZipArchive file, string entryName ) { State = ImporterState.WritingPackToDisk; @@ -202,7 +193,7 @@ public partial class TexToolsImporter : IDisposable throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); } - using var s = file.GetInputStream( entry ); + using var s = entry.OpenEntryStream(); WriteZipEntryToTempFile( s ); diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs new file mode 100644 index 00000000..0f461c79 --- /dev/null +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.Mods; +using SharpCompress.Archives; +using SharpCompress.Common; + +namespace Penumbra.Import; + +public partial class TexToolsImporter +{ + // Extract regular compressed archives that are folders containing penumbra-formatted mods. + // The mod has to either contain a meta.json at top level, or one folder deep. + // If the meta.json is one folder deep, all other files have to be in the same folder. + // The extracted folder gets its name either from that one top-level folder or from the mod name. + // All data is extracted without manipulation of the files or metadata. + private DirectoryInfo HandleRegularArchive( FileInfo modPackFile ) + { + using var zfs = modPackFile.OpenRead(); + using var archive = ArchiveFactory.Open( zfs ); + + var baseName = FindArchiveModMeta( archive, out var leadDir ); + _currentOptionIdx = 0; + _currentNumOptions = 1; + _currentModName = modPackFile.Name; + _currentGroupName = string.Empty; + _currentOptionName = DefaultTexToolsData.Name; + _currentNumFiles = archive.Entries.Count( e => !e.IsDirectory ); + PluginLog.Log( $" -> Importing {archive.Type} Archive." ); + + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); + var options = new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + }; + + State = ImporterState.ExtractingModFiles; + _currentFileIdx = 0; + foreach( var entry in archive.Entries ) + { + _token.ThrowIfCancellationRequested(); + + if( entry.IsDirectory ) + { + ++_currentFileIdx; + continue; + } + + PluginLog.Log( " -> Extracting {0}", entry.Key ); + entry.WriteToDirectory( _currentModDirectory.FullName, options ); + + ++_currentFileIdx; + } + + if( leadDir ) + { + _token.ThrowIfCancellationRequested(); + var oldName = _currentModDirectory.FullName; + var tmpName = oldName + "__tmp"; + Directory.Move( oldName, tmpName ); + Directory.Move( Path.Combine( tmpName, baseName ), oldName ); + Directory.Delete( tmpName ); + _currentModDirectory = new DirectoryInfo( oldName ); + } + + return _currentModDirectory; + } + + // Search the archive for the meta.json file which needs to exist. + private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) + { + var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.EndsWith( "meta.json" ) ); + // None found. + if( entry == null ) + { + throw new Exception( "Invalid mod archive: No meta.json contained." ); + } + + var ret = string.Empty; + leadDir = false; + + // If the file is not at top-level. + if( entry.Key != "meta.json" ) + { + leadDir = true; + var directory = Path.GetDirectoryName( entry.Key ); + // Should not happen. + if( directory.IsNullOrEmpty() ) + { + throw new Exception( "Invalid mod archive: Unknown error fetching meta.json." ); + } + + ret = directory; + // Check that all other files are also contained in the top-level directory. + if( ret.IndexOfAny( new[] { '/', '\\' } ) >= 0 + || !archive.Entries.All( e => e.Key.StartsWith( ret ) && ( e.Key.Length == ret.Length || e.Key[ ret.Length ] is '/' or '\\' ) ) ) + { + throw new Exception( + "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too." ); + } + } + + // Check that the mod has a valid name in the meta.json file. + using var e = entry.OpenEntryStream(); + using var t = new StreamReader( e ); + using var j = new JsonTextReader( t ); + var obj = JObject.Load( j ); + var name = obj[ nameof( Mod.Name ) ]?.Value< string >().RemoveInvalidPathSymbols() ?? string.Empty; + if( name.Length == 0 ) + { + throw new Exception( "Invalid mod archive: mod meta has no name." ); + } + + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. + return ret.Length == 0 ? name : ret; + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index f15053cf..574efb1f 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,10 +4,10 @@ using System.IO; using System.Linq; using System.Text; using Dalamud.Logging; -using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.Mods; using Penumbra.Util; +using SharpCompress.Archives.Zip; namespace Penumbra.Import; @@ -16,7 +16,7 @@ public partial class TexToolsImporter private DirectoryInfo? _currentModDirectory; // Version 1 mod packs are a simple collection of files without much information. - private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipArchive extractedModPack, string modRaw ) { _currentOptionIdx = 0; _currentNumOptions = 1; @@ -46,7 +46,7 @@ public partial class TexToolsImporter } // Version 2 mod packs can either be simple or extended, import accordingly. - private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV2ModPack( FileInfo _, ZipArchive extractedModPack, string modRaw ) { var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!; @@ -80,7 +80,7 @@ public partial class TexToolsImporter } // Simple V2 mod packs are basically the same as V1 mod packs. - private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) + private DirectoryInfo ImportSimpleV2ModPack( ZipArchive extractedModPack, SimpleModPack modList ) { _currentOptionIdx = 0; _currentNumOptions = 1; @@ -125,7 +125,7 @@ public partial class TexToolsImporter } // Extended V2 mod packs contain multiple options that need to be handled separately. - private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw ) { _currentOptionIdx = 0; PluginLog.Log( " -> Importing Extended V2 ModPack" ); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 26e8d088..f6a4be96 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -55,8 +55,8 @@ - + diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 57580c05..eced30cb 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -212,7 +212,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => + _fileManager.OpenFileDialog( "Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) => { if( s ) { From 1af0517f363f5a38be1110c5d580a64028099fd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 16:00:57 +0200 Subject: [PATCH 0405/2451] Fix character utility loading not registering on first load. --- Penumbra/Interop/CharacterUtility.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 9ff778c3..9f41050b 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -51,8 +51,12 @@ public unsafe class CharacterUtility : IDisposable public CharacterUtility() { SignatureHelper.Initialise( this ); - Dalamud.Framework.Update += LoadDefaultResources; - LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); + LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); + LoadDefaultResources( null! ); + if( !Ready ) + { + Dalamud.Framework.Update += LoadDefaultResources; + } } // We store the default data of the resources so we can always restore them. From f37ad11ab86dbdaf73c65badcfeed75e86816458 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 16:54:20 +0200 Subject: [PATCH 0406/2451] User protection. --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 58 ++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 48adefa4..bc0c9321 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -6,6 +6,7 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Logging; using Dalamud.Utility; using ImGuiNET; using OtterGui; @@ -78,14 +79,61 @@ public partial class ConfigWindow 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 ); + var (text, valid) = CheckPath( newName, old ); + return ( ImGui.Button( text, w ) || saved ) && valid; } + private static (string Text, bool Valid) CheckPath( string newName, string old ) + { + static bool IsSubPathOf( string basePath, string subPath ) + { + var rel = Path.GetRelativePath( basePath, subPath ); + return rel == "." || (!rel.StartsWith( '.' ) && !Path.IsPathRooted( rel )); + } + + if( newName.Length > RootDirectoryMaxLength ) + { + return ( $"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false ); + } + + if( Path.GetDirectoryName( newName ) == null ) + { + return ( "Path may not be a drive root. Please add a directory.", false ); + } + + var symbol = '\0'; + if( newName.Any( c => ( symbol = c ) > ( char )0x7F ) ) + { + return ( $"Path contains invalid symbol {symbol}. Only ASCII is allowed.", false ); + } + + var desktop = Environment.GetFolderPath( Environment.SpecialFolder.Desktop ); + if( IsSubPathOf( desktop, newName ) ) + { + return ( "Path may not be on your Desktop.", false ); + } + + var dalamud = Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; + if( IsSubPathOf( dalamud.FullName, newName ) ) + { + return ( "Path may not be inside your Dalamud directories.", false ); + } + + if( Functions.GetDownloadsFolder( out var downloads ) && IsSubPathOf( downloads, newName ) ) + { + return ( "Path may not be inside your Downloads folder.", false ); + } + + var gameDir = Dalamud.GameData.GameData.DataPath.Parent!.Parent!.FullName; + if( IsSubPathOf( gameDir, newName ) ) + { + return ( "Path may not be inside your game folder.", false ); + } + + return ( $"Press Enter or Click Here to Save (Current Directory: {old})", true ); + } + // Draw a directory picker button that toggles the directory picker. // Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. private void DrawDirectoryPickerButton() From ff5e72e979d1620978b56e0bf009d3c5f68534de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Jul 2022 18:38:58 +0200 Subject: [PATCH 0407/2451] Add enable all for option groups --- .../Collections/ModCollection.Cache.Access.cs | 1 - Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 24 ++++++++++++++++++- Penumbra/UI/ConfigWindow.SettingsTab.cs | 1 - 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3d7e2c9c..8c2bddd3 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using Dalamud.Logging; using OtterGui.Classes; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index e76c145d..be6f5170 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -69,7 +69,7 @@ public partial class ConfigWindow DrawMultiGroup( _mod.Groups[ idx ], idx ); } - _window._penumbra.Api.InvokePostSettingsPanel(_mod.ModPath.Name); + _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); } @@ -210,6 +210,28 @@ public partial class ConfigWindow } Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { + ImGui.OpenPopup( $"##multi{groupIdx}" ); + } + + using var style = ImRaii.PushStyle( ImGuiStyleVar.PopupBorderSize, 1 ); + using var popup = ImRaii.Popup( label ); + if( popup ) + { + ImGui.TextUnformatted( group.Name ); + ImGui.Separator(); + if( ImGui.Selectable( "Enable All" ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( 1u << group.Count ) - 1u ); + } + + if( ImGui.Selectable( "Disable All" ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, 0 ); + } + } } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index bc0c9321..0735fbda 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -78,7 +78,6 @@ public partial class ConfigWindow { using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); var w = new Vector2( width, 0 ); - var symbol = '\0'; var (text, valid) = CheckPath( newName, old ); return ( ImGui.Button( text, w ) || saved ) && valid; From 2ca90f25186a495f1417aa5c8d2ae6300fd0b27e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Jul 2022 18:39:39 +0200 Subject: [PATCH 0408/2451] Add try-catch and locking to framework manager. --- Penumbra/Util/FrameworkManager.cs | 65 +++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/Penumbra/Util/FrameworkManager.cs b/Penumbra/Util/FrameworkManager.cs index 393a9fe1..d19179da 100644 --- a/Penumbra/Util/FrameworkManager.cs +++ b/Penumbra/Util/FrameworkManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Logging; namespace Penumbra.Util; @@ -19,57 +20,81 @@ public class FrameworkManager : IDisposable // One action per frame will be executed. // On dispose, any remaining actions will be executed. public void RegisterDelayed( string tag, Action action ) - => _delayed[ tag ] = action; + { + lock( _delayed ) + { + _delayed[ tag ] = action; + } + } // Register an action that should be executed on the next frame. // All of those actions will be executed in the next frame. // If there are more than one, they will be launched in separated tasks, but waited for. public void RegisterImportant( string tag, Action action ) - => _important[ tag ] = action; + { + lock( _important ) + { + _important[ tag ] = action; + } + } public void Dispose() { Dalamud.Framework.Update -= OnUpdate; - HandleAll( _delayed ); + foreach( var (_, action) in _delayed ) + { + action(); + } + + _delayed.Clear(); } private void OnUpdate( Framework _ ) { - HandleOne(); - HandleAllTasks( _important ); - } - - private void HandleOne() - { - if( _delayed.Count > 0 ) + try { - var (key, action) = _delayed.First(); - action(); - _delayed.Remove( key ); + HandleOne( _delayed ); + HandleAllTasks( _important ); + } + catch( Exception e ) + { + PluginLog.Error( $"Problem saving data:\n{e}" ); } } - private static void HandleAll( IDictionary< string, Action > dict ) + private static void HandleOne( IDictionary< string, Action > dict ) { - foreach( var (_, action) in dict ) + if( dict.Count == 0 ) { - action(); + return; } - dict.Clear(); + Action action; + lock( dict ) + { + ( var key, action ) = dict.First(); + dict.Remove( key ); + } + + action(); } private static void HandleAllTasks( IDictionary< string, Action > dict ) { if( dict.Count < 2 ) { - HandleAll( dict ); + HandleOne( dict ); } else { - var tasks = dict.Values.Select( Task.Run ).ToArray(); + Task[] tasks; + lock( dict ) + { + tasks = dict.Values.Select( Task.Run ).ToArray(); + dict.Clear(); + } + Task.WaitAll( tasks ); - dict.Clear(); } } } \ No newline at end of file From 24aa7eac24999638c22e67ea7c41268e55e7bf35 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 30 Jul 2022 16:54:53 +0000 Subject: [PATCH 0409/2451] [CI] Updating repo.json for refs/tags/0.5.5.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5af070ce..7591dc2f 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.4.8", - "TestingAssemblyVersion": "0.5.4.8", + "AssemblyVersion": "0.5.5.0", + "TestingAssemblyVersion": "0.5.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.4.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 70a5ee9485bc21133a3d3edcac12c23feffe985d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 31 Jul 2022 11:34:44 +0200 Subject: [PATCH 0410/2451] Small fix for Enable All. --- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index be6f5170..80dd0fad 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -224,7 +225,8 @@ public partial class ConfigWindow ImGui.Separator(); if( ImGui.Selectable( "Enable All" ) ) { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( 1u << group.Count ) - 1u ); + flags = group.Count == 32 ? uint.MaxValue : ( 1u << group.Count ) - 1u; + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); } if( ImGui.Selectable( "Disable All" ) ) From 4881e4ef09a84cfa953e64d22e0e55eea66ba1bc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 Aug 2022 18:24:20 +0200 Subject: [PATCH 0411/2451] Remove Show Advanced, forbid ProgramFiles root folders. --- OtterGui | 2 +- Penumbra/Configuration.cs | 1 - Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 5 -- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 31 +++++------ .../UI/ConfigWindow.SettingsTab.Advanced.cs | 8 +-- Penumbra/UI/ConfigWindow.SettingsTab.cs | 52 +++++++------------ 7 files changed, 39 insertions(+), 62 deletions(-) diff --git a/OtterGui b/OtterGui index f137f521..ea6ebcc0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f137f521c588472247510a3fd4183bd651477618 +Subproject commit ea6ebcc073412419a051ac73a697980da20233e2 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 71297304..7f2e7848 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -59,7 +59,6 @@ public partial class Configuration : IPluginConfiguration public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool FixMainWindow { get; set; } = false; - public bool ShowAdvanced { get; set; } public bool AutoDeduplicateOnImport { get; set; } = true; public bool EnableHttpApi { get; set; } = true; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 58397133..1299f068 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -57,7 +57,7 @@ public partial class ConfigWindow // Only gets drawn when actually relevant. private static void DrawCleanCollectionButton( Vector2 width ) { - if( Penumbra.Config.ShowAdvanced && Penumbra.CollectionManager.Current.HasUnusedSettings ) + if( Penumbra.CollectionManager.Current.HasUnusedSettings ) { ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index c45668ca..d096d3ee 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -20,11 +20,6 @@ public partial class ConfigWindow // Draw the effective tab if ShowAdvanced is on. public void Draw() { - if( !Penumbra.Config.ShowAdvanced ) - { - return; - } - using var tab = ImRaii.TabItem( "Effective Changes" ); if( !tab ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 5d9cc3f4..f250035b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -49,31 +49,28 @@ public partial class ConfigWindow | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) | ( _mod.Description.Length > 0 ? Tabs.Description : 0 ) | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) - | ( Penumbra.Config.ShowAdvanced ? Tabs.Edit : 0 ); + | Tabs.Edit; DrawSettingsTab(); DrawDescriptionTab(); DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); - if( Penumbra.Config.ShowAdvanced ) + if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { - if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) - { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( _mod.Default ); - _window.ModEditPopup.IsOpen = true; - } - - ImGuiUtil.HoverTooltip( - "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" - + "\t\t- file redirections\n" - + "\t\t- file swaps\n" - + "\t\t- metadata manipulations\n" - + "\t\t- model materials\n" - + "\t\t- duplicates\n" - + "\t\t- textures" ); + _window.ModEditPopup.ChangeMod( _mod ); + _window.ModEditPopup.ChangeOption( _mod.Default ); + _window.ModEditPopup.IsOpen = true; } + + ImGuiUtil.HoverTooltip( + "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" + + "\t\t- file redirections\n" + + "\t\t- file swaps\n" + + "\t\t- metadata manipulations\n" + + "\t\t- model materials\n" + + "\t\t- duplicates\n" + + "\t\t- textures" ); } // Just a simple text box with the wrapped description, if it exists. diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 1166bd58..bf8c0de9 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -15,11 +15,11 @@ public partial class ConfigWindow { private void DrawAdvancedSettings() { - if( !Penumbra.Config.ShowAdvanced || !ImGui.CollapsingHeader( "Advanced" ) ) - { - return; - } + var header = ImGui.CollapsingHeader( "Advanced" ); + OpenTutorial( BasicTutorialSteps.AdvancedSettings ); + if( !header ) + 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 ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 0735fbda..b256b04a 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -6,7 +6,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Logging; using Dalamud.Utility; using ImGuiNET; using OtterGui; @@ -45,7 +44,6 @@ public partial class ConfigWindow } DrawEnabledBox(); - DrawShowAdvancedBox(); Checkbox( "Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => { Penumbra.Config.FixMainWindow = v; @@ -76,8 +74,8 @@ public partial class ConfigWindow // Shows up only if the current input does not correspond to the current directory. 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 ); + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var w = new Vector2( width, 0 ); var (text, valid) = CheckPath( newName, old ); return ( ImGui.Button( text, w ) || saved ) && valid; @@ -88,7 +86,7 @@ public partial class ConfigWindow static bool IsSubPathOf( string basePath, string subPath ) { var rel = Path.GetRelativePath( basePath, subPath ); - return rel == "." || (!rel.StartsWith( '.' ) && !Path.IsPathRooted( rel )); + return rel == "." || !rel.StartsWith( '.' ) && !Path.IsPathRooted( rel ); } if( newName.Length > RootDirectoryMaxLength ) @@ -113,6 +111,13 @@ public partial class ConfigWindow return ( "Path may not be on your Desktop.", false ); } + var programFiles = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ); + var programFilesX86 = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFilesX86 ); + if( IsSubPathOf( programFiles, newName ) || IsSubPathOf( programFilesX86, newName ) ) + { + return ( "Path may not be in ProgramFiles.", false ); + } + var dalamud = Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; if( IsSubPathOf( dalamud.FullName, newName ) ) { @@ -198,12 +203,19 @@ public partial class ConfigWindow DrawDirectoryPickerButton(); style.Pop(); ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Root Directory", "This is where Penumbra will store your extracted mod files.\n" + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + "TTMP files are not copied, just extracted.\n" + "This directory needs to be accessible and you need write access here.\n" + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker( tt ); + OpenTutorial( BasicTutorialSteps.GeneralTooltips ); + ImGui.SameLine(); + ImGui.TextUnformatted( "Root Directory" ); + ImGuiUtil.HoverTooltip( tt ); + group.Dispose(); OpenTutorial( BasicTutorialSteps.ModDirectory ); ImGui.SameLine(); @@ -242,32 +254,6 @@ public partial class ConfigWindow OpenTutorial( BasicTutorialSteps.EnableMods ); } - private static void DrawShowAdvancedBox() - { - var showAdvanced = Penumbra.Config.ShowAdvanced; - using( var _ = ImRaii.Group() ) - { - if( ImGui.Checkbox( "##showAdvanced", ref showAdvanced ) ) - { - Penumbra.Config.ShowAdvanced = showAdvanced; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - const string tt = "Enable some advanced options in this window and in the mod selector.\n" - + "This is required to enable manually editing any mod information."; - - // Manually split due to tutorial. - ImGuiComponents.HelpMarker( tt ); - OpenTutorial( BasicTutorialSteps.GeneralTooltips ); - ImGui.SameLine(); - ImGui.TextUnformatted( "Show Advanced Settings" ); - ImGuiUtil.HoverTooltip( tt ); - } - - OpenTutorial( BasicTutorialSteps.AdvancedSettings ); - } - private static void DrawColorSettings() { if( !ImGui.CollapsingHeader( "Colors" ) ) From 7936c43b0b6d3b081a66c6fc0146bfbc59eb9aad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Jun 2022 11:43:46 +0200 Subject: [PATCH 0412/2451] net6 --- OtterGui | 2 +- Penumbra.GameData/Penumbra.GameData.csproj | 2 +- Penumbra/Penumbra.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index ea6ebcc0..4c924791 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ea6ebcc073412419a051ac73a697980da20233e2 +Subproject commit 4c92479175161617161d8faf844c8f683aa2d5d2 diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index d2470349..9e0ec95c 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -1,6 +1,6 @@ - net5.0-windows + net6.0-windows preview x64 Penumbra.GameData diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f6a4be96..aed24408 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,6 +1,6 @@ - net5.0-windows + net6.0-windows preview x64 Penumbra From 4a008fbc3e42c2ab999506336383c93cad635d29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 3 Aug 2022 11:38:13 +0200 Subject: [PATCH 0413/2451] Further net6 --- .github/workflows/build.yml | 6 ++--- .github/workflows/release.yml | 8 +++---- .github/workflows/test_release.yml | 8 +++---- Penumbra.GameData/Penumbra.GameData.csproj | 24 +++++++++++-------- Penumbra/Import/TexToolsImporter.Archives.cs | 2 +- Penumbra/Penumbra.csproj | 25 +++++++++++++------- 6 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3729baff..6d18be4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,15 +21,15 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" - name: Build run: | dotnet build --no-restore --configuration Release --nologo - name: Archive - run: Compress-Archive -Path Penumbra/bin/Release/net5.0-windows/* -DestinationPath Penumbra.zip + run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 with: path: | - ./Penumbra/bin/Release/net5.0-windows/* + ./Penumbra/bin/Release/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69063dda..dcbf42ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | @@ -29,17 +29,17 @@ jobs: - name: write version into json run: | $ver = '${{ github.ref }}' -replace 'refs/tags/','' - $path = './Penumbra/bin/Release/net5.0-windows/Penumbra.json' + $path = './Penumbra/bin/Release/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/Release/net5.0-windows/* -DestinationPath Penumbra.zip + run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 with: path: | - ./Penumbra/bin/Release/net5.0-windows/* + ./Penumbra/bin/Release/* - name: Create Release id: create_release uses: actions/create-release@v1 diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index ad4375f0..106a976a 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | @@ -29,17 +29,17 @@ jobs: - name: write version into json run: | $ver = '${{ github.ref }}' -replace 'refs/tags/t','' - $path = './Penumbra/bin/Debug/net5.0-windows/Penumbra.json' + $path = './Penumbra/bin/Debug/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 + run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 with: path: | - ./Penumbra/bin/Debug/net5.0-windows/* + ./Penumbra/bin/Debug/* - name: Create Release id: create_release uses: actions/create-release@v1 diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index 9e0ec95c..ad466524 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -6,13 +6,15 @@ Penumbra.GameData absolute gangstas Penumbra - Copyright © 2020 + Copyright © 2022 1.0.0.0 1.0.0.0 bin\$(Configuration)\ true enable true + false + false @@ -28,24 +30,26 @@ $(MSBuildWarningsAsMessages);MSB3277 + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + $(DalamudLibPath)Dalamud.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + $(DalamudLibPath)Lumina.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + $(DalamudLibPath)Lumina.Excel.dll + False + + + $(DalamudLibPath)Newtonsoft.Json.dll False - - - - false - - diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 0f461c79..1b8a45fa 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -111,7 +111,7 @@ public partial class TexToolsImporter using var t = new StreamReader( e ); using var j = new JsonTextReader( t ); var obj = JObject.Load( j ); - var name = obj[ nameof( Mod.Name ) ]?.Value< string >().RemoveInvalidPathSymbols() ?? string.Empty; + var name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; if( name.Length == 0 ) { throw new Exception( "Invalid mod archive: mod meta has no name." ); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index aed24408..2689f37d 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -6,13 +6,15 @@ Penumbra absolute gangstas Penumbra - Copyright © 2020 + Copyright © 2022 1.0.0.0 1.0.0.0 bin\$(Configuration)\ true enable true + false + false @@ -25,36 +27,43 @@ + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + $(DalamudLibPath)Dalamud.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + $(DalamudLibPath)ImGui.NET.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + $(DalamudLibPath)ImGuiScene.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + $(DalamudLibPath)Lumina.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + $(DalamudLibPath)Lumina.Excel.dll False - $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll + $(DalamudLibPath)FFXIVClientStructs.dll + False + + + $(DalamudLibPath)Newtonsoft.Json.dll False - From dc61f362fd3e4f5f9ba362abca8b99edac33e57c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 4 Aug 2022 14:49:14 +0200 Subject: [PATCH 0414/2451] Fix bug in cutscene character identification --- Penumbra/Interop/Resolver/PathResolver.Data.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 0a3d7fa2..5d1125c0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -385,7 +385,9 @@ public unsafe partial class PathResolver { collection = null; // Check for the Yourself collection. - if( actor->ObjectIndex is 0 or ObjectReloader.GPosePlayerIdx || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + if( actor->ObjectIndex == 0 + || actor->ObjectIndex == ObjectReloader.GPosePlayerIdx && name.Length > 0 + || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) { collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); if( collection != null ) From 8fdd17338805ca848c845fe28f43df7d1c92a563 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Aug 2022 22:12:15 +0200 Subject: [PATCH 0415/2451] Add cutscene identification and IPC, reorder PathResolver stuff. --- OtterGui | 2 +- Penumbra/Api/IPenumbraApi.cs | 3 + Penumbra/Api/IpcTester.cs | 7 + Penumbra/Api/PenumbraApi.cs | 26 +- Penumbra/Api/PenumbraIpc.cs | 13 + .../Interop/Resolver/CutsceneCharacters.cs | 85 ++++++ .../Resolver/PathResolver.Animation.cs | 123 --------- .../Resolver/PathResolver.AnimationState.cs | 215 +++++++++++++++ .../Resolver/PathResolver.Demihuman.cs | 88 ------ .../Resolver/PathResolver.DrawObjectState.cs | 234 ++++++++++++++++ .../Interop/Resolver/PathResolver.Human.cs | 118 -------- ...Data.cs => PathResolver.Identification.cs} | 243 +--------------- .../Interop/Resolver/PathResolver.Material.cs | 213 +++++++------- .../Interop/Resolver/PathResolver.Meta.cs | 259 +++++++++--------- .../Interop/Resolver/PathResolver.Monster.cs | 88 ------ .../Resolver/PathResolver.PathState.cs | 109 ++++++++ .../Interop/Resolver/PathResolver.Resolve.cs | 223 --------------- .../Resolver/PathResolver.ResolverHooks.cs | 258 +++++++++++++++++ .../Interop/Resolver/PathResolver.Weapon.cs | 89 ------ Penumbra/Interop/Resolver/PathResolver.cs | 182 ++++++------ Penumbra/UI/ConfigWindow.DebugTab.cs | 71 +++-- 21 files changed, 1326 insertions(+), 1323 deletions(-) create mode 100644 Penumbra/Interop/Resolver/CutsceneCharacters.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Animation.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.AnimationState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Demihuman.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Human.cs rename Penumbra/Interop/Resolver/{PathResolver.Data.cs => PathResolver.Identification.cs} (60%) delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Monster.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.PathState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Resolve.cs create mode 100644 Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Weapon.cs diff --git a/OtterGui b/OtterGui index 4c924791..09dcd012 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4c92479175161617161d8faf844c8f683aa2d5d2 +Subproject commit 09dcd012a3106862f20f045b9ff9e33d168047c4 diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 292d822c..2472b3e2 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -124,6 +124,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the game object associated with a given draw object and the name of the collection associated with this game object. public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); + // Obtain the parent game object index for an unnamed cutscene actor by its index. + public int GetCutsceneParentIndex( int actor ); + // 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(); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index e311161a..81ef08a8 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -201,6 +201,7 @@ public class IpcTester : IDisposable private string _currentDrawObjectString = string.Empty; private string _currentReversePath = string.Empty; private IntPtr _currentDrawObject = IntPtr.Zero; + private int _currentCutsceneActor = 0; private string _lastCreatedGameObjectName = string.Empty; private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; @@ -231,6 +232,8 @@ public class IpcTester : IDisposable : IntPtr.Zero; } + ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { @@ -263,6 +266,10 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); } + DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Cutscene Parent" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< int, int >( PenumbraIpc.LabelProviderGetCutsceneParentIndex ) + .InvokeFunc( _currentCutsceneActor ).ToString() ); + DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" ); if( _currentReversePath.Length > 0 ) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index b9b18c70..55bf0472 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; -using FFXIVClientStructs.FFXIV.Common.Configuration; using Lumina.Data; using Newtonsoft.Json; using OtterGui; @@ -16,14 +15,13 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Resolver; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 11 ); + => ( 4, 12 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -43,8 +41,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event CreatingCharacterBaseDelegate? CreatingCharacterBase { - add => _penumbra!.PathResolver.CreatingCharacterBase += value; - remove => _penumbra!.PathResolver.CreatingCharacterBase -= value; + add => PathResolver.DrawObjectState.CreatingCharacterBase += value; + remove => PathResolver.DrawObjectState.CreatingCharacterBase -= value; } public bool Valid @@ -54,8 +52,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); foreach( var collection in Penumbra.CollectionManager ) { SubscribeToCollection( collection ); @@ -221,10 +219,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) { CheckInitialized(); - var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); + var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject ); return ( obj, collection.Name ); } + public int GetCutsceneParentIndex( int actor ) + { + CheckInitialized(); + return _penumbra!.PathResolver.CutsceneActor( actor ); + } + public IList< (string, string) > GetModList() { CheckInitialized(); @@ -429,7 +433,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) - || Penumbra.TempMods.Collections.ContainsKey( character ) ) + || Penumbra.TempMods.Collections.ContainsKey( character ) ) { return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); } @@ -475,7 +479,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; } @@ -512,7 +516,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) { return PenumbraApiEc.CollectionMissing; } diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index de179d66..63b71322 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -261,6 +261,7 @@ public partial class PenumbraIpc public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; @@ -269,6 +270,7 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; internal ICallGateProvider< string, string >? ProviderResolvePlayer; internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; + internal ICallGateProvider< int, int >? ProviderGetCutsceneParentIndex; internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; @@ -315,6 +317,16 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } + try + { + ProviderGetCutsceneParentIndex = pi.GetIpcProvider( LabelProviderGetCutsceneParentIndex ); + ProviderGetCutsceneParentIndex.RegisterFunc( Api.GetCutsceneParentIndex ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" ); + } + try { ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); @@ -350,6 +362,7 @@ public partial class PenumbraIpc private void DisposeResolveProviders() { ProviderGetDrawObjectInfo?.UnregisterFunc(); + ProviderGetCutsceneParentIndex?.UnregisterFunc(); ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs new file mode 100644 index 00000000..a701c293 --- /dev/null +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; + +namespace Penumbra.Interop.Resolver; + +public class CutsceneCharacters : IDisposable +{ + public const int CutsceneStartIdx = 200; + public const int CutsceneSlots = 40; + public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; + + private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, ObjectReloader.CutsceneSlots ).ToArray(); + + public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors + => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) + .Where( i => Dalamud.Objects[ i ] != null ) + .Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) ); + + public CutsceneCharacters() + => SignatureHelper.Initialise( this ); + + // Get the related actor to a cutscene actor. + // Does not check for valid input index. + // Returns null if no connected actor is set or the actor does not exist anymore. + public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ] + { + get + { + Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx ); + idx = _copiedCharacters[ idx - CutsceneStartIdx ]; + return idx < 0 ? null : Dalamud.Objects[ idx ]; + } + } + + // Return the currently set index of a parent or -1 if none is set or the index is invalid. + public int GetParentIndex( int idx ) + { + if( idx is >= CutsceneStartIdx and < CutsceneEndIdx ) + { + return _copiedCharacters[ idx - CutsceneStartIdx ]; + } + + return -1; + } + + public void Enable() + => _copyCharacterHook.Enable(); + + public void Disable() + => _copyCharacterHook.Disable(); + + public void Dispose() + => _copyCharacterHook.Dispose(); + + + private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); + + [Signature( "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F", DetourName = nameof( CopyCharacterDetour ) )] + private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; + + private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + { + try + { + if( target != null && target->ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + { + var parent = source == null || source->ObjectIndex is < 0 or >= CutsceneStartIdx + ? -1 + : source->ObjectIndex; + _copiedCharacters[ target->ObjectIndex - CutsceneStartIdx ] = ( short )parent; + } + } + catch + { + // ignored + } + + return _copyCharacterHook.Original( target, source, unk ); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs deleted file mode 100644 index 563b32ca..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using Penumbra.Collections; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - private ModCollection? _animationLoadCollection; - private ModCollection? _lastAvfxCollection = null; - - public delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); - - // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. - // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. - [Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )] - public Hook< LoadTimelineResourcesDelegate >? LoadTimelineResourcesHook; - - private ulong LoadTimelineResourcesDetour( IntPtr timeline ) - { - ulong ret; - var old = _animationLoadCollection; - try - { - var getGameObjectIdx = ( ( delegate* unmanaged < IntPtr, int>** )timeline )[ 0 ][ 28 ]; - var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < Dalamud.Objects.Length ) - { - var obj = Dalamud.Objects[ idx ]; - _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; - } - else - { - _animationLoadCollection = null; - } - } - finally - { - ret = LoadTimelineResourcesHook!.Original( timeline ); - } - - _animationLoadCollection = old; - - return ret; - } - - // Probably used when the base idle animation gets loaded. - // Make it aware of the correct collection to load the correct pap files. - [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )] - public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook; - - private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) - { - var last = _animationLoadCollection; - _animationLoadCollection = _lastCreatedCollection - ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - CharacterBaseLoadAnimationHook!.Original( drawObject ); - _animationLoadCollection = last; - } - - public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); - - [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] - public Hook< LoadSomeAvfx >? LoadSomeAvfxHook; - - private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) - { - var last = _animationLoadCollection; - _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); - var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); - _animationLoadCollection = last; - return ret; - } - - // Unknown what exactly this is but it seems to load a bunch of paps. - public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); - - [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )] - public Hook< LoadSomePap >? LoadSomePapHook; - - private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) - { - var timelinePtr = a1 + 0x50; - var last = _animationLoadCollection; - if( timelinePtr != IntPtr.Zero ) - { - var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); - if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) - { - _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); - } - } - - LoadSomePapHook!.Original( a1, a2, a3, a4 ); - _animationLoadCollection = last; - } - - // Seems to load character actions when zoning or changing class, maybe. - [Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )] - public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook; - - private void SomeActionLoadDetour( IntPtr gameObject ) - { - var last = _animationLoadCollection; - _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); - SomeActionLoadHook!.Original( gameObject ); - _animationLoadCollection = last; - } - - [Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )] - public Hook< CharacterBaseDestructorDelegate >? SomeOtherAvfxHook; - - private void SomeOtherAvfxDetour( IntPtr unk ) - { - var last = _animationLoadCollection; - var gameObject = ( GameObject* )( unk - 0x8B0 ); - _animationLoadCollection = IdentifyCollection( gameObject ); - SomeOtherAvfxHook!.Original( unk ); - _animationLoadCollection = last; - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs new file mode 100644 index 00000000..b7ba3617 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -0,0 +1,215 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + public class AnimationState + { + private readonly DrawObjectState _drawObjectState; + + private ModCollection? _animationLoadCollection; + private ModCollection? _lastAvfxCollection; + + public AnimationState( DrawObjectState drawObjectState ) + { + _drawObjectState = drawObjectState; + SignatureHelper.Initialise( this ); + } + + public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection ) + { + switch( type ) + { + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Scd: + if( _animationLoadCollection != null ) + { + collection = _animationLoadCollection; + return true; + } + + break; + case ResourceType.Avfx: + _lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default; + if( _animationLoadCollection != null ) + { + collection = _animationLoadCollection; + return true; + } + + break; + case ResourceType.Atex: + if( _lastAvfxCollection != null ) + { + collection = _lastAvfxCollection; + return true; + } + + if( _animationLoadCollection != null ) + { + collection = _animationLoadCollection; + return true; + } + + break; + } + + collection = null; + return false; + } + + public void Enable() + { + _loadTimelineResourcesHook.Enable(); + _characterBaseLoadAnimationHook.Enable(); + _loadSomeAvfxHook.Enable(); + _loadSomePapHook.Enable(); + _someActionLoadHook.Enable(); + _someOtherAvfxHook.Enable(); + } + + public void Disable() + { + _loadTimelineResourcesHook.Disable(); + _characterBaseLoadAnimationHook.Disable(); + _loadSomeAvfxHook.Disable(); + _loadSomePapHook.Disable(); + _someActionLoadHook.Disable(); + _someOtherAvfxHook.Disable(); + } + + public void Dispose() + { + _loadTimelineResourcesHook.Dispose(); + _characterBaseLoadAnimationHook.Dispose(); + _loadSomeAvfxHook.Dispose(); + _loadSomePapHook.Dispose(); + _someActionLoadHook.Dispose(); + _someOtherAvfxHook.Dispose(); + } + + // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. + // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. + private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); + + [Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )] + private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!; + + private ulong LoadTimelineResourcesDetour( IntPtr timeline ) + { + ulong ret; + var old = _animationLoadCollection; + try + { + var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; + var idx = getGameObjectIdx( timeline ); + if( idx >= 0 && idx < Dalamud.Objects.Length ) + { + var obj = Dalamud.Objects[ idx ]; + _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; + } + else + { + _animationLoadCollection = null; + } + } + finally + { + ret = _loadTimelineResourcesHook!.Original( timeline ); + } + + _animationLoadCollection = old; + + return ret; + } + + // Probably used when the base idle animation gets loaded. + // Make it aware of the correct collection to load the correct pap files. + private delegate void CharacterBaseNoArgumentDelegate( IntPtr drawBase ); + + [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", + DetourName = nameof( CharacterBaseLoadAnimationDetour ) )] + private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!; + + private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) + { + var last = _animationLoadCollection; + _animationLoadCollection = _drawObjectState.LastCreatedCollection + ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + _characterBaseLoadAnimationHook!.Original( drawObject ); + _animationLoadCollection = last; + } + + + public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); + + [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] + private readonly Hook< LoadSomeAvfx > _loadSomeAvfxHook = null!; + + private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) + { + var last = _animationLoadCollection; + _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + var ret = _loadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); + _animationLoadCollection = last; + return ret; + } + + // Unknown what exactly this is but it seems to load a bunch of paps. + private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); + + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51", + DetourName = nameof( LoadSomePapDetour ) )] + private readonly Hook< LoadSomePap > _loadSomePapHook = null!; + + private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) + { + var timelinePtr = a1 + 0x50; + var last = _animationLoadCollection; + if( timelinePtr != IntPtr.Zero ) + { + var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); + if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) + { + _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + } + } + + _loadSomePapHook!.Original( a1, a2, a3, a4 ); + _animationLoadCollection = last; + } + + // Seems to load character actions when zoning or changing class, maybe. + [Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )] + private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!; + + private void SomeActionLoadDetour( IntPtr gameObject ) + { + var last = _animationLoadCollection; + _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + _someActionLoadHook!.Original( gameObject ); + _animationLoadCollection = last; + } + + [Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )] + private readonly Hook< CharacterBaseNoArgumentDelegate > _someOtherAvfxHook = null!; + + private void SomeOtherAvfxDetour( IntPtr unk ) + { + var last = _animationLoadCollection; + var gameObject = ( GameObject* )( unk - 0x8B0 ); + _animationLoadCollection = IdentifyCollection( gameObject ); + _someOtherAvfxHook!.Original( unk ); + _animationLoadCollection = last; + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs b/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs deleted file mode 100644 index dbe57a7b..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Demihuman.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] - public IntPtr* DrawObjectDemiVTable; - - public Hook< GeneralResolveDelegate >? ResolveDemiDecalPathHook; - public Hook< EidResolveDelegate >? ResolveDemiEidPathHook; - public Hook< GeneralResolveDelegate >? ResolveDemiImcPathHook; - public Hook< MPapResolveDelegate >? ResolveDemiMPapPathHook; - public Hook< GeneralResolveDelegate >? ResolveDemiMdlPathHook; - public Hook< MaterialResolveDetour >? ResolveDemiMtrlPathHook; - public Hook< MaterialResolveDetour >? ResolveDemiPapPathHook; - public Hook< GeneralResolveDelegate >? ResolveDemiPhybPathHook; - public Hook< GeneralResolveDelegate >? ResolveDemiSklbPathHook; - public Hook< GeneralResolveDelegate >? ResolveDemiSkpPathHook; - public Hook< EidResolveDelegate >? ResolveDemiTmbPathHook; - public Hook< MaterialResolveDetour >? ResolveDemiVfxPathHook; - - private void SetupDemiHooks() - { - ResolveDemiDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour ); - ResolveDemiEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour ); - ResolveDemiImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour ); - ResolveDemiMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour ); - ResolveDemiMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour ); - ResolveDemiMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour ); - ResolveDemiPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour ); - ResolveDemiPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour ); - ResolveDemiSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour ); - ResolveDemiSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour ); - ResolveDemiTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour ); - ResolveDemiVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour ); - } - - private void EnableDemiHooks() - { - ResolveDemiDecalPathHook?.Enable(); - ResolveDemiEidPathHook?.Enable(); - ResolveDemiImcPathHook?.Enable(); - ResolveDemiMPapPathHook?.Enable(); - ResolveDemiMdlPathHook?.Enable(); - ResolveDemiMtrlPathHook?.Enable(); - ResolveDemiPapPathHook?.Enable(); - ResolveDemiPhybPathHook?.Enable(); - ResolveDemiSklbPathHook?.Enable(); - ResolveDemiSkpPathHook?.Enable(); - ResolveDemiTmbPathHook?.Enable(); - ResolveDemiVfxPathHook?.Enable(); - } - - private void DisableDemiHooks() - { - ResolveDemiDecalPathHook?.Disable(); - ResolveDemiEidPathHook?.Disable(); - ResolveDemiImcPathHook?.Disable(); - ResolveDemiMPapPathHook?.Disable(); - ResolveDemiMdlPathHook?.Disable(); - ResolveDemiMtrlPathHook?.Disable(); - ResolveDemiPapPathHook?.Disable(); - ResolveDemiPhybPathHook?.Disable(); - ResolveDemiSklbPathHook?.Disable(); - ResolveDemiSkpPathHook?.Disable(); - ResolveDemiTmbPathHook?.Disable(); - ResolveDemiVfxPathHook?.Disable(); - } - - private void DisposeDemiHooks() - { - ResolveDemiDecalPathHook?.Dispose(); - ResolveDemiEidPathHook?.Dispose(); - ResolveDemiImcPathHook?.Dispose(); - ResolveDemiMPapPathHook?.Dispose(); - ResolveDemiMdlPathHook?.Dispose(); - ResolveDemiMtrlPathHook?.Dispose(); - ResolveDemiPapPathHook?.Dispose(); - ResolveDemiPhybPathHook?.Dispose(); - ResolveDemiSklbPathHook?.Dispose(); - ResolveDemiSkpPathHook?.Dispose(); - ResolveDemiTmbPathHook?.Dispose(); - ResolveDemiVfxPathHook?.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs new file mode 100644 index 00000000..41e55e2e --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -0,0 +1,234 @@ +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Api; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + public class DrawObjectState + { + public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; + + public IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjects + => _drawObjectToObject; + + public int Count + => _drawObjectToObject.Count; + + public bool TryGetValue( IntPtr drawObject, out (ModCollection, int) value, out GameObject* gameObject ) + { + gameObject = null; + if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) + { + return false; + } + + var gameObjectIdx = value.Item2; + return VerifyEntry( drawObject, gameObjectIdx, out gameObject ); + } + + + // Set and update a parent object if it exists and a last game object is set. + public ModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) + { + if( parentObject == IntPtr.Zero && LastGameObject != null ) + { + var collection = IdentifyCollection( LastGameObject ); + _drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); + return collection; + } + + return null; + } + + + public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection ) + { + if( type == ResourceType.Tex + && LastCreatedCollection != null + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) + { + collection = LastCreatedCollection!; + return true; + } + + collection = null; + return false; + } + + + public ModCollection? LastCreatedCollection + => _lastCreatedCollection; + + public GameObject* LastGameObject { get; private set; } + + public DrawObjectState() + { + SignatureHelper.Initialise( this ); + } + + public void Enable() + { + _characterBaseCreateHook.Enable(); + _characterBaseDestructorHook.Enable(); + _enableDrawHook.Enable(); + _weaponReloadHook.Enable(); + InitializeDrawObjects(); + Penumbra.CollectionManager.CollectionChanged += CheckCollections; + } + + public void Disable() + { + _characterBaseCreateHook.Disable(); + _characterBaseDestructorHook.Disable(); + _enableDrawHook.Disable(); + _weaponReloadHook.Disable(); + Penumbra.CollectionManager.CollectionChanged -= CheckCollections; + } + + public void Dispose() + { + Disable(); + _characterBaseCreateHook.Dispose(); + _characterBaseDestructorHook.Dispose(); + _enableDrawHook.Dispose(); + _weaponReloadHook.Dispose(); + } + + // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. + private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + { + gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + var draw = ( DrawObject* )drawObject; + if( gameObject != null + && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) + { + return true; + } + + gameObject = null; + _drawObjectToObject.Remove( drawObject ); + return false; + } + + // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. + // It contains any DrawObjects that correspond to a human actor, even those without specific collections. + private readonly Dictionary< IntPtr, (ModCollection, int) > _drawObjectToObject = new(); + private ModCollection? _lastCreatedCollection; + + // Keep track of created DrawObjects that are CharacterBase, + // and use the last game object that called EnableDraw to link them. + private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + + [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = nameof( CharacterBaseCreateDetour ) )] + private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!; + + private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + { + using var cmp = MetaChanger.ChangeCmp( LastGameObject, out _lastCreatedCollection ); + + if( LastGameObject != null ) + { + var modelPtr = &a; + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c ); + } + + var ret = _characterBaseCreateHook!.Original( a, b, c, d ); + if( LastGameObject != null ) + { + _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); + } + + return ret; + } + + + // Remove DrawObjects from the list when they are destroyed. + private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + + [Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", + DetourName = nameof( CharacterBaseDestructorDetour ) )] + private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!; + + private void CharacterBaseDestructorDetour( IntPtr drawBase ) + { + _drawObjectToObject.Remove( drawBase ); + _characterBaseDestructorHook!.Original.Invoke( drawBase ); + } + + + // EnableDraw is what creates DrawObjects for gameObjects, + // so we always keep track of the current GameObject to be able to link it to the DrawObject. + private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + + [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0", DetourName = nameof( EnableDrawDetour ) )] + private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; + + private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + { + var oldObject = LastGameObject; + LastGameObject = ( GameObject* )gameObject; + _enableDrawHook!.Original.Invoke( gameObject, b, c, d ); + LastGameObject = oldObject; + } + + // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, + // so we use that. + private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); + + [Signature( "E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof( WeaponReloadDetour ) )] + private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!; + + public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) + { + var oldGame = LastGameObject; + LastGameObject = *( GameObject** )( a1 + 8 ); + _weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); + LastGameObject = oldGame; + } + + // Update collections linked to Game/DrawObjects due to a change in collection configuration. + private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) + { + if( type is CollectionType.Inactive or CollectionType.Current ) + { + return; + } + + foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() ) + { + if( !VerifyEntry( key, idx, out var obj ) ) + { + _drawObjectToObject.Remove( key ); + } + + var newCollection = IdentifyCollection( obj ); + _drawObjectToObject[ key ] = ( newCollection, idx ); + } + } + + // Find all current DrawObjects used in the GameObject table. + // We do not iterate the Dalamud table because it does not work when not logged in. + private void InitializeDrawObjects() + { + for( var i = 0; i < Dalamud.Objects.Length; ++i ) + { + var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); + if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) + { + _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Human.cs b/Penumbra/Interop/Resolver/PathResolver.Human.cs deleted file mode 100644 index c1278436..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Human.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; - -namespace Penumbra.Interop.Resolver; - -// We can hook the different Resolve-Functions using just the VTable of Human. -// The other DrawObject VTables and the ResolveRoot function are currently unused. -public unsafe partial class PathResolver -{ - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )] - public IntPtr* DrawObjectHumanVTable; - - // [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectVTable; - // - // public const int ResolveRootIdx = 71; - - public const int ResolveSklbIdx = 72; - public const int ResolveMdlIdx = 73; - public const int ResolveSkpIdx = 74; - public const int ResolvePhybIdx = 75; - public const int ResolvePapIdx = 76; - public const int ResolveTmbIdx = 77; - public const int ResolveMPapIdx = 79; - public const int ResolveImcIdx = 81; - public const int ResolveMtrlIdx = 82; - public const int ResolveDecalIdx = 83; - public const int ResolveVfxIdx = 84; - public const int ResolveEidIdx = 85; - - public const int OnModelLoadCompleteIdx = 58; - - public delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); - public delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ); - public delegate IntPtr MaterialResolveDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ); - public delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 ); - - public delegate void OnModelLoadCompleteDelegate( IntPtr drawObject ); - - public Hook< GeneralResolveDelegate >? ResolveDecalPathHook; - public Hook< EidResolveDelegate >? ResolveEidPathHook; - public Hook< GeneralResolveDelegate >? ResolveImcPathHook; - public Hook< MPapResolveDelegate >? ResolveMPapPathHook; - public Hook< GeneralResolveDelegate >? ResolveMdlPathHook; - public Hook< MaterialResolveDetour >? ResolveMtrlPathHook; - public Hook< MaterialResolveDetour >? ResolvePapPathHook; - public Hook< GeneralResolveDelegate >? ResolvePhybPathHook; - public Hook< GeneralResolveDelegate >? ResolveSklbPathHook; - public Hook< GeneralResolveDelegate >? ResolveSkpPathHook; - public Hook< EidResolveDelegate >? ResolveTmbPathHook; - public Hook< MaterialResolveDetour >? ResolveVfxPathHook; - - - private void SetupHumanHooks() - { - ResolveDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour ); - ResolveEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour ); - ResolveImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour ); - ResolveMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour ); - ResolveMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour ); - ResolveMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour ); - ResolvePapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour ); - ResolvePhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour ); - ResolveSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour ); - ResolveSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour ); - ResolveTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour ); - ResolveVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour ); - } - - private void EnableHumanHooks() - { - ResolveDecalPathHook?.Enable(); - ResolveEidPathHook?.Enable(); - ResolveImcPathHook?.Enable(); - ResolveMPapPathHook?.Enable(); - ResolveMdlPathHook?.Enable(); - ResolveMtrlPathHook?.Enable(); - ResolvePapPathHook?.Enable(); - ResolvePhybPathHook?.Enable(); - ResolveSklbPathHook?.Enable(); - ResolveSkpPathHook?.Enable(); - ResolveTmbPathHook?.Enable(); - ResolveVfxPathHook?.Enable(); - } - - private void DisableHumanHooks() - { - ResolveDecalPathHook?.Disable(); - ResolveEidPathHook?.Disable(); - ResolveImcPathHook?.Disable(); - ResolveMPapPathHook?.Disable(); - ResolveMdlPathHook?.Disable(); - ResolveMtrlPathHook?.Disable(); - ResolvePapPathHook?.Disable(); - ResolvePhybPathHook?.Disable(); - ResolveSklbPathHook?.Disable(); - ResolveSkpPathHook?.Disable(); - ResolveTmbPathHook?.Disable(); - ResolveVfxPathHook?.Disable(); - } - - private void DisposeHumanHooks() - { - ResolveDecalPathHook?.Dispose(); - ResolveEidPathHook?.Dispose(); - ResolveImcPathHook?.Dispose(); - ResolveMPapPathHook?.Dispose(); - ResolveMdlPathHook?.Dispose(); - ResolveMtrlPathHook?.Dispose(); - ResolvePapPathHook?.Dispose(); - ResolvePhybPathHook?.Dispose(); - ResolveSklbPathHook?.Dispose(); - ResolveSkpPathHook?.Dispose(); - ResolveTmbPathHook?.Dispose(); - ResolveVfxPathHook?.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs similarity index 60% rename from Penumbra/Interop/Resolver/PathResolver.Data.cs rename to Penumbra/Interop/Resolver/PathResolver.Identification.cs index 5d1125c0..d9c21999 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -1,17 +1,10 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Dalamud.Hooking; using Dalamud.Logging; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; -using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -22,148 +15,6 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - // Keep track of created DrawObjects that are CharacterBase, - // and use the last game object that called EnableDraw to link them. - public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); - - [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour" )] - public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; - - private ModCollection? _lastCreatedCollection; - public event CreatingCharacterBaseDelegate? CreatingCharacterBase; - - private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) - { - using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection ); - - if( LastGameObject != null ) - { - var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c ); - } - - var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); - if( LastGameObject != null ) - { - DrawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - } - - return ret; - } - - - // Remove DrawObjects from the list when they are destroyed. - public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); - - [Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", - DetourName = "CharacterBaseDestructorDetour" )] - public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook; - - private void CharacterBaseDestructorDetour( IntPtr drawBase ) - { - DrawObjectToObject.Remove( drawBase ); - CharacterBaseDestructorHook!.Original.Invoke( drawBase ); - } - - - // EnableDraw is what creates DrawObjects for gameObjects, - // so we always keep track of the current GameObject to be able to link it to the DrawObject. - public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); - - [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0" )] - public Hook< EnableDrawDelegate >? EnableDrawHook; - - private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) - { - var oldObject = LastGameObject; - LastGameObject = ( GameObject* )gameObject; - EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); - LastGameObject = oldObject; - } - - // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, - // so we use that. - public delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); - - [Signature( "E8 ?? ?? ?? ?? 44 8B 9F" )] - public Hook< WeaponReloadFunc >? WeaponReloadHook; - - public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) - { - var oldGame = LastGameObject; - LastGameObject = *( GameObject** )( a1 + 8 ); - WeaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); - LastGameObject = oldGame; - } - - private void EnableDataHooks() - { - CharacterBaseCreateHook?.Enable(); - EnableDrawHook?.Enable(); - CharacterBaseDestructorHook?.Enable(); - WeaponReloadHook?.Enable(); - Penumbra.CollectionManager.CollectionChanged += CheckCollections; - LoadTimelineResourcesHook?.Enable(); - CharacterBaseLoadAnimationHook?.Enable(); - LoadSomeAvfxHook?.Enable(); - LoadSomePapHook?.Enable(); - SomeActionLoadHook?.Enable(); - SomeOtherAvfxHook?.Enable(); - } - - private void DisableDataHooks() - { - Penumbra.CollectionManager.CollectionChanged -= CheckCollections; - WeaponReloadHook?.Disable(); - CharacterBaseCreateHook?.Disable(); - EnableDrawHook?.Disable(); - CharacterBaseDestructorHook?.Disable(); - LoadTimelineResourcesHook?.Disable(); - CharacterBaseLoadAnimationHook?.Disable(); - LoadSomeAvfxHook?.Disable(); - LoadSomePapHook?.Disable(); - SomeActionLoadHook?.Disable(); - SomeOtherAvfxHook?.Disable(); - } - - private void DisposeDataHooks() - { - WeaponReloadHook?.Dispose(); - CharacterBaseCreateHook?.Dispose(); - EnableDrawHook?.Dispose(); - CharacterBaseDestructorHook?.Dispose(); - LoadTimelineResourcesHook?.Dispose(); - CharacterBaseLoadAnimationHook?.Dispose(); - LoadSomeAvfxHook?.Dispose(); - LoadSomePapHook?.Dispose(); - SomeActionLoadHook?.Dispose(); - SomeOtherAvfxHook?.Dispose(); - } - - // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. - // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); - - // This map links files to their corresponding collection, if it is non-default. - internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); - - internal GameObject* LastGameObject = null; - - // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. - private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) - { - gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); - var draw = ( DrawObject* )drawObject; - if( gameObject != null && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) - { - return true; - } - - gameObject = null; - DrawObjectToObject.Remove( drawObject ); - return false; - } - // Obtain the name of the current player, if one exists. private static string? GetPlayerName() => Dalamud.Objects[ 0 ]?.Name.ToString(); @@ -244,6 +95,13 @@ public unsafe partial class PathResolver return null; } + var parent = Cutscenes[ gameObject->ObjectIndex ]; + if( parent != null ) + { + return parent.Name.ToString(); + } + + // should not really happen but keep it in as a emergency case. var player = Dalamud.Objects[ 0 ]; if( player == null ) { @@ -327,12 +185,14 @@ public unsafe partial class PathResolver // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. var actualName = gameObject->ObjectIndex switch { - 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - >= 200 => GetCutsceneName( gameObject ), - _ => null, + 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview + + >= ObjectReloader.CutsceneStartIdx and < ObjectReloader.CutsceneEndIdx => GetCutsceneName( gameObject ), + + _ => null, } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); @@ -466,77 +326,4 @@ public unsafe partial class PathResolver return false; } - - - // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) - { - if( type is CollectionType.Inactive or CollectionType.Current ) - { - return; - } - - foreach( var (key, (_, idx)) in DrawObjectToObject.ToArray() ) - { - if( !VerifyEntry( key, idx, out var obj ) ) - { - DrawObjectToObject.Remove( key ); - } - - var newCollection = IdentifyCollection( obj ); - DrawObjectToObject[ key ] = ( newCollection, idx ); - } - } - - // Use the stored information to find the GameObject and Collection linked to a DrawObject. - private GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) - { - if( DrawObjectToObject.TryGetValue( drawObject, out var data ) ) - { - var gameObjectIdx = data.Item2; - if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) ) - { - collection = data.Item1; - return gameObject; - } - } - - if( LastGameObject != null && ( LastGameObject->DrawObject == null || LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) - { - collection = IdentifyCollection( LastGameObject ); - return LastGameObject; - } - - - collection = IdentifyCollection( null ); - return null; - } - - - // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection collection ) - { - if( PathCollections.ContainsKey( path ) || path.IsOwned ) - { - PathCollections[ path ] = collection; - } - else - { - PathCollections[ path.Clone() ] = collection; - } - } - - // Find all current DrawObjects used in the GameObject table. - // We do not iterate the Dalamud table because it does not work when not logged in. - private void InitializeDrawObjects() - { - for( var i = 0; i < Dalamud.Objects.Length; ++i ) - { - var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); - if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) - { - DrawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); - } - } - } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 450eceab..9688257c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,7 +4,6 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -12,132 +11,144 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop.Resolver; -// Materials do contain their own paths to textures and shader packages. -// Those are loaded synchronously. -// Thus, we need to ensure the correct files are loaded when a material is loaded. public unsafe partial class PathResolver { - public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); - - [Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour" )] - public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; - - private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + // Materials do contain their own paths to textures and shader packages. + // Those are loaded synchronously. + // Thus, we need to ensure the correct files are loaded when a material is loaded. + public class MaterialState : IDisposable { - LoadMtrlHelper( mtrlResourceHandle ); - var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle ); - _mtrlCollection = null; - return ret; - } + private readonly PathState _paths; - [Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", - DetourName = "LoadMtrlShpkDetour" )] - public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook; + private ModCollection? _mtrlCollection; - private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) - { - LoadMtrlHelper( mtrlResourceHandle ); - var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle ); - _mtrlCollection = null; - return ret; - } - - private ModCollection? _mtrlCollection; - - private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) - { - if( mtrlResourceHandle == IntPtr.Zero ) + public MaterialState( PathState paths ) { - return; + SignatureHelper.Initialise( this ); + _paths = paths; } - var mtrl = ( MtrlResource* )mtrlResourceHandle; - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; - } - - // Check specifically for shpk and tex files whether we are currently in a material load. - private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection ) - { - if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) + // Check specifically for shpk and tex files whether we are currently in a material load. + public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection ) { - collection = _mtrlCollection; - return true; - } + if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) + { + collection = _mtrlCollection; + return true; + } - collection = null; - return false; - } - - // We need to set the correct collection for the actual material path that is loaded - // before actually loading the file. - private bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) - { - ret = 0; - if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl ) - { + collection = null; return false; } - var lastUnderscore = split.LastIndexOf( ( byte )'_' ); - var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.TempMods.CollectionByName( name, out var collection ) - || Penumbra.CollectionManager.ByName( name, out collection ) ) + // Materials need to be set per collection so they can load their textures independently from each other. + public static void HandleCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, object?) data ) { + if( nonDefault && type == ResourceType.Mtrl ) + { + var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" ); + data = ( fullPath, collection ); + } + else + { + data = ( resolved, collection ); + } + } + + public void Enable() + { + _loadMtrlShpkHook.Enable(); + _loadMtrlTexHook.Enable(); + Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; + } + + public void Disable() + { + _loadMtrlShpkHook.Disable(); + _loadMtrlTexHook.Disable(); + Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler; + } + + public void Dispose() + { + Disable(); + _loadMtrlShpkHook?.Dispose(); + _loadMtrlTexHook?.Dispose(); + } + + // We need to set the correct collection for the actual material path that is loaded + // before actually loading the file. + public bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) + { + ret = 0; + if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl ) + { + return false; + } + + var lastUnderscore = split.LastIndexOf( ( byte )'_' ); + var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); + if( Penumbra.TempMods.CollectionByName( name, out var collection ) + || Penumbra.CollectionManager.ByName( name, out collection ) ) + { #if DEBUG - PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); + PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); #endif - SetCollection( path, collection ); - } - else - { + _paths.SetCollection( path, collection ); + } + else + { #if DEBUG - PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); + 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, + // or where the difference even comes from. + // Was called with True on my client and with false on other peoples clients, + // which caused problems. + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); + _paths.Consume( path, out _ ); + return true; } - // Force isSync = true for this call. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. - ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); - PathCollections.TryRemove( path, out _ ); - return true; - } + private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); - // Materials need to be set per collection so they can load their textures independently from each other. - private static void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, object?) data ) - { - if( nonDefault && type == ResourceType.Mtrl ) + [Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = nameof( LoadMtrlTexDetour ) )] + private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!; + + private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { - var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" ); - data = ( fullPath, collection ); + LoadMtrlHelper( mtrlResourceHandle ); + var ret = _loadMtrlTexHook!.Original( mtrlResourceHandle ); + _mtrlCollection = null; + return ret; } - else + + [Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", + DetourName = nameof( LoadMtrlShpkDetour ) )] + private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!; + + private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { - data = ( resolved, collection ); + LoadMtrlHelper( mtrlResourceHandle ); + var ret = _loadMtrlShpkHook!.Original( mtrlResourceHandle ); + _mtrlCollection = null; + return ret; } - } - private void EnableMtrlHooks() - { - LoadMtrlShpkHook?.Enable(); - LoadMtrlTexHook?.Enable(); - Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; - } + private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) + { + if( mtrlResourceHandle == IntPtr.Zero ) + { + return; + } - private void DisableMtrlHooks() - { - LoadMtrlShpkHook?.Disable(); - LoadMtrlTexHook?.Disable(); - Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler; - } - - private void DisposeMtrlHooks() - { - LoadMtrlShpkHook?.Dispose(); - LoadMtrlTexHook?.Dispose(); + var mtrl = ( MtrlResource* )mtrlResourceHandle; + var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + _mtrlCollection = _paths.TryGetValue( mtrlPath, out var c ) ? c : null; + } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 670c4044..73390514 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -1,9 +1,8 @@ using System; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Resolver; @@ -33,136 +32,133 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - public delegate void UpdateModelDelegate( IntPtr drawObject ); - - [Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )] - public Hook< UpdateModelDelegate >? UpdateModelsHook; - - private void UpdateModelsDetour( IntPtr drawObject ) + public unsafe class MetaState : IDisposable { - // Shortcut because this is called all the time. - // Same thing is checked at the beginning of the original function. - if( *( int* )( drawObject + 0x90c ) == 0 ) + private readonly PathResolver _parent; + + public MetaState( PathResolver parent, IntPtr* humanVTable ) { - return; + SignatureHelper.Initialise( this ); + _parent = parent; + _onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour ); } - var collection = GetCollection( drawObject ); - if( collection != null ) + public void Enable() { - using var eqp = MetaChanger.ChangeEqp( collection ); - using var eqdp = MetaChanger.ChangeEqdp( collection ); - UpdateModelsHook!.Original.Invoke( drawObject ); - } - else - { - UpdateModelsHook!.Original.Invoke( drawObject ); - } - } - - [Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", - DetourName = "GetEqpIndirectDetour" )] - public Hook< OnModelLoadCompleteDelegate >? GetEqpIndirectHook; - - private void GetEqpIndirectDetour( IntPtr drawObject ) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) - { - return; + _getEqpIndirectHook.Enable(); + _updateModelsHook.Enable(); + _onModelLoadCompleteHook.Enable(); + _setupVisorHook.Enable(); + _rspSetupCharacterHook.Enable(); } - using var eqp = MetaChanger.ChangeEqp( this, drawObject ); - GetEqpIndirectHook!.Original( drawObject ); - } - - public Hook< OnModelLoadCompleteDelegate >? OnModelLoadCompleteHook; - - private void OnModelLoadCompleteDetour( IntPtr drawObject ) - { - var collection = GetCollection( drawObject ); - if( collection != null ) + public void Disable() { - using var eqp = MetaChanger.ChangeEqp( collection ); - using var eqdp = MetaChanger.ChangeEqdp( collection ); - OnModelLoadCompleteHook!.Original.Invoke( drawObject ); - } - else - { - OnModelLoadCompleteHook!.Original.Invoke( drawObject ); - } - } - - // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, - // but it only applies a changed gmp file after a redraw for some reason. - public delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState ); - - [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "SetupVisorDetour" )] - public Hook< SetupVisorDelegate >? SetupVisorHook; - - private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) - { - using var gmp = MetaChanger.ChangeGmp( this, drawObject ); - return SetupVisorHook!.Original( drawObject, modelId, visorState ); - } - - // RSP - public delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); - - [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = "RspSetupCharacterDetour" )] - public Hook< RspSetupCharacterDelegate >? RspSetupCharacterHook; - - private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) - { - using var rsp = MetaChanger.ChangeCmp( this, drawObject ); - RspSetupCharacterHook!.Original( drawObject, unk2, unk3, unk4, unk5 ); - } - - private void SetupMetaHooks() - { - OnModelLoadCompleteHook = - Hook< OnModelLoadCompleteDelegate >.FromAddress( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour ); - } - - private void EnableMetaHooks() - { - GetEqpIndirectHook?.Enable(); - UpdateModelsHook?.Enable(); - OnModelLoadCompleteHook?.Enable(); - SetupVisorHook?.Enable(); - RspSetupCharacterHook?.Enable(); - } - - private void DisableMetaHooks() - { - GetEqpIndirectHook?.Disable(); - UpdateModelsHook?.Disable(); - OnModelLoadCompleteHook?.Disable(); - SetupVisorHook?.Disable(); - RspSetupCharacterHook?.Disable(); - } - - private void DisposeMetaHooks() - { - GetEqpIndirectHook?.Dispose(); - UpdateModelsHook?.Dispose(); - OnModelLoadCompleteHook?.Dispose(); - SetupVisorHook?.Dispose(); - RspSetupCharacterHook?.Dispose(); - } - - private ModCollection? GetCollection( IntPtr drawObject ) - { - var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection == Penumbra.CollectionManager.Default ) - { - return null; + _getEqpIndirectHook.Disable(); + _updateModelsHook.Disable(); + _onModelLoadCompleteHook.Disable(); + _setupVisorHook.Disable(); + _rspSetupCharacterHook.Disable(); } - return collection.HasCache ? collection : null; - } + public void Dispose() + { + _getEqpIndirectHook.Dispose(); + _updateModelsHook.Dispose(); + _onModelLoadCompleteHook.Dispose(); + _setupVisorHook.Dispose(); + _rspSetupCharacterHook.Dispose(); + } + private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject ); + private readonly Hook< OnModelLoadCompleteDelegate > _onModelLoadCompleteHook; + + private void OnModelLoadCompleteDetour( IntPtr drawObject ) + { + var collection = GetCollection( drawObject ); + if( collection != null ) + { + using var eqp = MetaChanger.ChangeEqp( collection ); + using var eqdp = MetaChanger.ChangeEqdp( collection ); + _onModelLoadCompleteHook.Original.Invoke( drawObject ); + } + else + { + _onModelLoadCompleteHook.Original.Invoke( drawObject ); + } + } + + + private delegate void UpdateModelDelegate( IntPtr drawObject ); + + [Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = nameof( UpdateModelsDetour ) )] + private readonly Hook< UpdateModelDelegate > _updateModelsHook = null!; + + private void UpdateModelsDetour( IntPtr drawObject ) + { + // Shortcut because this is called all the time. + // Same thing is checked at the beginning of the original function. + if( *( int* )( drawObject + 0x90c ) == 0 ) + { + return; + } + + var collection = GetCollection( drawObject ); + if( collection != null ) + { + using var eqp = MetaChanger.ChangeEqp( collection ); + using var eqdp = MetaChanger.ChangeEqdp( collection ); + _updateModelsHook.Original.Invoke( drawObject ); + } + else + { + _updateModelsHook.Original.Invoke( drawObject ); + } + } + + [Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", + DetourName = nameof( GetEqpIndirectDetour ) )] + private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!; + + private void GetEqpIndirectDetour( IntPtr drawObject ) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) + { + return; + } + + using var eqp = MetaChanger.ChangeEqp( _parent, drawObject ); + _getEqpIndirectHook.Original( drawObject ); + } + + + // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, + // but it only applies a changed gmp file after a redraw for some reason. + private delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState ); + + [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = nameof( SetupVisorDetour ) )] + private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!; + + private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) + { + using var gmp = MetaChanger.ChangeGmp( _parent, drawObject ); + return _setupVisorHook.Original( drawObject, modelId, visorState ); + } + + // RSP + private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); + + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = nameof( RspSetupCharacterDetour ) )] + private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!; + + private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) + { + using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); + _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); + } + } // Small helper to handle setting metadata and reverting it at the end of the function. // Since eqp and eqdp may be called multiple times in a row, we need to count them, @@ -194,11 +190,12 @@ public unsafe partial class PathResolver public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject ) { - var collection = resolver.GetCollection( drawObject ); + var collection = GetCollection( drawObject ); if( collection != null ) { return ChangeEqp( collection ); } + return new MetaChanger( MetaManipulation.Type.Unknown ); } @@ -207,12 +204,13 @@ public unsafe partial class PathResolver { if( modelType < 10 ) { - var collection = resolver.GetCollection( drawObject ); + var collection = GetCollection( drawObject ); if( collection != null ) { return ChangeEqdp( collection ); } } + return new MetaChanger( MetaManipulation.Type.Unknown ); } @@ -224,31 +222,33 @@ public unsafe partial class PathResolver public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) { - var collection = resolver.GetCollection( drawObject ); + var collection = GetCollection( drawObject ); if( collection != null ) { collection.SetGmpFiles(); return new MetaChanger( MetaManipulation.Type.Gmp ); } + return new MetaChanger( MetaManipulation.Type.Unknown ); } public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) { - var collection = resolver.GetCollection( drawObject ); + var collection = GetCollection( drawObject ); if( collection != null ) { collection.SetEstFiles(); return new MetaChanger( MetaManipulation.Type.Est ); } + return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) + public static MetaChanger ChangeCmp( GameObject* gameObject, out ModCollection? collection ) { - if( resolver.LastGameObject != null ) + if( gameObject != null ) { - collection = IdentifyCollection( resolver.LastGameObject ); + collection = IdentifyCollection( gameObject ); if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) { collection.SetCmpFiles(); @@ -265,12 +265,13 @@ public unsafe partial class PathResolver public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) { - var collection = resolver.GetCollection( drawObject ); + var collection = GetCollection( drawObject ); if( collection != null ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } + return new MetaChanger( MetaManipulation.Type.Unknown ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Monster.cs b/Penumbra/Interop/Resolver/PathResolver.Monster.cs deleted file mode 100644 index 5ef487a6..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Monster.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] - public IntPtr* DrawObjectMonsterVTable; - - public Hook? ResolveMonsterDecalPathHook; - public Hook? ResolveMonsterEidPathHook; - public Hook? ResolveMonsterImcPathHook; - public Hook? ResolveMonsterMPapPathHook; - public Hook? ResolveMonsterMdlPathHook; - public Hook? ResolveMonsterMtrlPathHook; - public Hook? ResolveMonsterPapPathHook; - public Hook? ResolveMonsterPhybPathHook; - public Hook? ResolveMonsterSklbPathHook; - public Hook? ResolveMonsterSkpPathHook; - public Hook? ResolveMonsterTmbPathHook; - public Hook? ResolveMonsterVfxPathHook; - - private void SetupMonsterHooks() - { - ResolveMonsterDecalPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour ); - ResolveMonsterEidPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour ); - ResolveMonsterImcPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour ); - ResolveMonsterMPapPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour ); - ResolveMonsterMdlPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour ); - ResolveMonsterMtrlPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour ); - ResolveMonsterPapPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour ); - ResolveMonsterPhybPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour ); - ResolveMonsterSklbPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour ); - ResolveMonsterSkpPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour ); - ResolveMonsterTmbPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour ); - ResolveMonsterVfxPathHook = Hook.FromAddress( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour ); - } - - private void EnableMonsterHooks() - { - ResolveMonsterDecalPathHook?.Enable(); - ResolveMonsterEidPathHook?.Enable(); - ResolveMonsterImcPathHook?.Enable(); - ResolveMonsterMPapPathHook?.Enable(); - ResolveMonsterMdlPathHook?.Enable(); - ResolveMonsterMtrlPathHook?.Enable(); - ResolveMonsterPapPathHook?.Enable(); - ResolveMonsterPhybPathHook?.Enable(); - ResolveMonsterSklbPathHook?.Enable(); - ResolveMonsterSkpPathHook?.Enable(); - ResolveMonsterTmbPathHook?.Enable(); - ResolveMonsterVfxPathHook?.Enable(); - } - - private void DisableMonsterHooks() - { - ResolveMonsterDecalPathHook?.Disable(); - ResolveMonsterEidPathHook?.Disable(); - ResolveMonsterImcPathHook?.Disable(); - ResolveMonsterMPapPathHook?.Disable(); - ResolveMonsterMdlPathHook?.Disable(); - ResolveMonsterMtrlPathHook?.Disable(); - ResolveMonsterPapPathHook?.Disable(); - ResolveMonsterPhybPathHook?.Disable(); - ResolveMonsterSklbPathHook?.Disable(); - ResolveMonsterSkpPathHook?.Disable(); - ResolveMonsterTmbPathHook?.Disable(); - ResolveMonsterVfxPathHook?.Disable(); - } - - private void DisposeMonsterHooks() - { - ResolveMonsterDecalPathHook?.Dispose(); - ResolveMonsterEidPathHook?.Dispose(); - ResolveMonsterImcPathHook?.Dispose(); - ResolveMonsterMPapPathHook?.Dispose(); - ResolveMonsterMdlPathHook?.Dispose(); - ResolveMonsterMtrlPathHook?.Dispose(); - ResolveMonsterPapPathHook?.Dispose(); - ResolveMonsterPhybPathHook?.Dispose(); - ResolveMonsterSklbPathHook?.Dispose(); - ResolveMonsterSkpPathHook?.Dispose(); - ResolveMonsterTmbPathHook?.Dispose(); - ResolveMonsterVfxPathHook?.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs new file mode 100644 index 00000000..ab40fe1e --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Interop.Resolver; + +public unsafe partial class PathResolver +{ + public class PathState : IDisposable + { + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )] + public readonly IntPtr* HumanVTable = null!; + + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", + ScanType = ScanType.StaticAddress )] + private readonly IntPtr* _weaponVTable = null!; + + [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] + private readonly IntPtr* _demiHumanVTable = null!; + + [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] + private readonly IntPtr* _monsterVTable = null!; + + private readonly ResolverHooks _human; + private readonly ResolverHooks _weapon; + private readonly ResolverHooks _demiHuman; + private readonly ResolverHooks _monster; + + // This map links files to their corresponding collection, if it is non-default. + private readonly ConcurrentDictionary< Utf8String, ModCollection > _pathCollections = new(); + + public PathState( PathResolver parent ) + { + SignatureHelper.Initialise( this ); + _human = new ResolverHooks( parent, HumanVTable, ResolverHooks.Type.Human ); + _weapon = new ResolverHooks( parent, _weaponVTable, ResolverHooks.Type.Weapon ); + _demiHuman = new ResolverHooks( parent, _demiHumanVTable, ResolverHooks.Type.Other ); + _monster = new ResolverHooks( parent, _monsterVTable, ResolverHooks.Type.Other ); + } + + public void Enable() + { + _human.Enable(); + _weapon.Enable(); + _demiHuman.Enable(); + _monster.Enable(); + } + + public void Disable() + { + _human.Disable(); + _weapon.Disable(); + _demiHuman.Disable(); + _monster.Disable(); + } + + public void Dispose() + { + _human.Dispose(); + _weapon.Dispose(); + _demiHuman.Dispose(); + _monster.Dispose(); + } + + public int Count + => _pathCollections.Count; + + public IEnumerable< KeyValuePair< Utf8String, ModCollection > > Paths + => _pathCollections; + + public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out ModCollection? collection ) + => _pathCollections.TryGetValue( path, out collection ); + + public bool Consume( Utf8String path, [NotNullWhen( true )] out ModCollection? collection ) + => _pathCollections.TryRemove( path, out collection ); + + // Just add or remove the resolved path. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + public IntPtr ResolvePath( ModCollection collection, IntPtr path ) + { + if( path == IntPtr.Zero ) + { + return path; + } + + var gamePath = new Utf8String( ( byte* )path ); + SetCollection( gamePath, collection ); + return path; + } + + // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. + public void SetCollection( Utf8String path, ModCollection collection ) + { + if( _pathCollections.ContainsKey( path ) || path.IsOwned ) + { + _pathCollections[ path ] = collection; + } + else + { + _pathCollections[ path.Clone() ] = collection; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs deleted file mode 100644 index 3892fcbd..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Collections; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Interop.Resolver; - -// The actual resolve detours are basically all the same. -public unsafe partial class PathResolver -{ - // Humans - private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolvePathDetour( drawObject, ResolveMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - { - using var eqdp = MetaChanger.ChangeEqdp( this, drawObject, modelType ); - return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); - } - - private IntPtr ResolveMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolvePapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - { - using var est = MetaChanger.ChangeEst( this, drawObject ); - return ResolvePathDetour( drawObject, ResolvePapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - } - - private IntPtr ResolvePhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = MetaChanger.ChangeEst( this, drawObject ); - return ResolvePathDetour( drawObject, ResolvePhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); - } - - private IntPtr ResolveSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = MetaChanger.ChangeEst( this, drawObject ); - return ResolvePathDetour( drawObject, ResolveSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); - } - - private IntPtr ResolveSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = MetaChanger.ChangeEst( this, drawObject ); - return ResolvePathDetour( drawObject, ResolveSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); - } - - private IntPtr ResolveTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveTmbPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - - // Weapons - private IntPtr ResolveWeaponDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveWeaponEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponEidPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveWeaponImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveWeaponMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveWeaponMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); - - private IntPtr ResolveWeaponMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveWeaponPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveWeaponPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveWeaponSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveWeaponSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveWeaponTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponTmbPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveWeaponVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPathDetour( drawObject, ResolveWeaponVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - // Monsters - private IntPtr ResolveMonsterDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMonsterDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMonsterEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveMonsterEidPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveMonsterImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMonsterImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMonsterMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolvePathDetour( drawObject, ResolveMonsterMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMonsterMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - => ResolvePathDetour( drawObject, ResolveMonsterMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); - - private IntPtr ResolveMonsterMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveMonsterMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMonsterPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveMonsterPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMonsterPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMonsterPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMonsterSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMonsterSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMonsterSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveMonsterSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMonsterTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveMonsterTmbPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveMonsterVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveMonsterVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - // Demihumans - private IntPtr ResolveDemiDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDemiDecalPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveDemiEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveDemiEidPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveDemiImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDemiImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveDemiMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolvePathDetour( drawObject, ResolveDemiMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveDemiMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - => ResolvePathDetour( drawObject, ResolveDemiMdlPathHook!.Original( drawObject, path, unk3, modelType ) ); - - private IntPtr ResolveDemiMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveDemiMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveDemiPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveDemiPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveDemiPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDemiPhybPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveDemiSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDemiSklbPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveDemiSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePathDetour( drawObject, ResolveDemiSkpPathHook!.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveDemiTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePathDetour( drawObject, ResolveDemiTmbPathHook!.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveDemiVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePathDetour( drawObject, ResolveDemiVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - - // Implementation - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) - => ResolvePathDetour( FindParent( drawObject, out var collection ) == null - ? Penumbra.CollectionManager.Default - : collection, path ); - - // Weapons have the characters DrawObject as a parent, - // but that may not be set yet when creating a new object, so we have to do the same detour - // as for Human DrawObjects that are just being created. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path ) - { - var parent = FindParent( drawObject, out var collection ); - if( parent != null ) - { - return ResolvePathDetour( collection, path ); - } - - var parentObject = ( ( DrawObject* )drawObject )->Object.ParentObject; - if( parentObject == null && LastGameObject != null ) - { - var c2 = IdentifyCollection( LastGameObject ); - DrawObjectToObject[ drawObject ] = ( c2, LastGameObject->ObjectIndex ); - return ResolvePathDetour( c2, path ); - } - - parent = FindParent( ( IntPtr )parentObject, out collection ); - return ResolvePathDetour( parent == null - ? Penumbra.CollectionManager.Default - : collection, path ); - } - - // Just add or remove the resolved path. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path ) - { - if( path == IntPtr.Zero ) - { - return path; - } - - var gamePath = new Utf8String( ( byte* )path ); - SetCollection( gamePath, collection ); - return path; - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs new file mode 100644 index 00000000..79c53d31 --- /dev/null +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -0,0 +1,258 @@ +using System; +using System.Runtime.CompilerServices; +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Resolver; + +public partial class PathResolver +{ + public unsafe class ResolverHooks : IDisposable + { + public enum Type + { + Human, + Weapon, + Other, + } + + private delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + private delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ); + private delegate IntPtr MaterialResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ); + private delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 ); + + private readonly Hook< GeneralResolveDelegate > _resolveDecalPathHook; + private readonly Hook< EidResolveDelegate > _resolveEidPathHook; + private readonly Hook< GeneralResolveDelegate > _resolveImcPathHook; + private readonly Hook< MPapResolveDelegate > _resolveMPapPathHook; + private readonly Hook< GeneralResolveDelegate > _resolveMdlPathHook; + private readonly Hook< MaterialResolveDelegate > _resolveMtrlPathHook; + private readonly Hook< MaterialResolveDelegate > _resolvePapPathHook; + private readonly Hook< GeneralResolveDelegate > _resolvePhybPathHook; + private readonly Hook< GeneralResolveDelegate > _resolveSklbPathHook; + private readonly Hook< GeneralResolveDelegate > _resolveSkpPathHook; + private readonly Hook< EidResolveDelegate > _resolveTmbPathHook; + private readonly Hook< MaterialResolveDelegate > _resolveVfxPathHook; + + private readonly PathResolver _parent; + + public ResolverHooks( PathResolver parent, IntPtr* vTable, Type type ) + { + _parent = parent; + _resolveDecalPathHook = Create< GeneralResolveDelegate >( vTable[ 83 ], type, ResolveDecalWeapon, ResolveDecal ); + _resolveEidPathHook = Create< EidResolveDelegate >( vTable[ 85 ], type, ResolveEidWeapon, ResolveEid ); + _resolveImcPathHook = Create< GeneralResolveDelegate >( vTable[ 81 ], type, ResolveImcWeapon, ResolveImc ); + _resolveMPapPathHook = Create< MPapResolveDelegate >( vTable[ 79 ], type, ResolveMPapWeapon, ResolveMPap ); + _resolveMdlPathHook = Create< GeneralResolveDelegate >( vTable[ 73 ], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman ); + _resolveMtrlPathHook = Create< MaterialResolveDelegate >( vTable[ 82 ], type, ResolveMtrlWeapon, ResolveMtrl ); + _resolvePapPathHook = Create< MaterialResolveDelegate >( vTable[ 76 ], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman ); + _resolvePhybPathHook = Create< GeneralResolveDelegate >( vTable[ 75 ], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman ); + _resolveSklbPathHook = Create< GeneralResolveDelegate >( vTable[ 72 ], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman ); + _resolveSkpPathHook = Create< GeneralResolveDelegate >( vTable[ 74 ], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman ); + _resolveTmbPathHook = Create< EidResolveDelegate >( vTable[ 77 ], type, ResolveTmbWeapon, ResolveTmb ); + _resolveVfxPathHook = Create< MaterialResolveDelegate >( vTable[ 84 ], type, ResolveVfxWeapon, ResolveVfx ); + } + + public void Enable() + { + _resolveDecalPathHook.Enable(); + _resolveEidPathHook.Enable(); + _resolveImcPathHook.Enable(); + _resolveMPapPathHook.Enable(); + _resolveMdlPathHook.Enable(); + _resolveMtrlPathHook.Enable(); + _resolvePapPathHook.Enable(); + _resolvePhybPathHook.Enable(); + _resolveSklbPathHook.Enable(); + _resolveSkpPathHook.Enable(); + _resolveTmbPathHook.Enable(); + _resolveVfxPathHook.Enable(); + } + + public void Disable() + { + _resolveDecalPathHook.Disable(); + _resolveEidPathHook.Disable(); + _resolveImcPathHook.Disable(); + _resolveMPapPathHook.Disable(); + _resolveMdlPathHook.Disable(); + _resolveMtrlPathHook.Disable(); + _resolvePapPathHook.Disable(); + _resolvePhybPathHook.Disable(); + _resolveSklbPathHook.Disable(); + _resolveSkpPathHook.Disable(); + _resolveTmbPathHook.Disable(); + _resolveVfxPathHook.Disable(); + } + + public void Dispose() + { + _resolveDecalPathHook.Dispose(); + _resolveEidPathHook.Dispose(); + _resolveImcPathHook.Dispose(); + _resolveMPapPathHook.Dispose(); + _resolveMdlPathHook.Dispose(); + _resolveMtrlPathHook.Dispose(); + _resolvePapPathHook.Dispose(); + _resolvePhybPathHook.Dispose(); + _resolveSklbPathHook.Dispose(); + _resolveSkpPathHook.Dispose(); + _resolveTmbPathHook.Dispose(); + _resolveVfxPathHook.Dispose(); + } + + private IntPtr ResolveDecal( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveEid( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveImc( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMPap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolvePath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMdl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + => ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); + + private IntPtr ResolveMtrl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolvePap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolvePhyb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveSklb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveSkp( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveTmb( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolvePath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveVfx( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolvePath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + + private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + { + using var eqdp = MetaChanger.ChangeEqdp( _parent, drawObject, modelType ); + return ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); + } + + private IntPtr ResolvePapHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + { + using var est = MetaChanger.ChangeEst( _parent, drawObject ); + return ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + } + + private IntPtr ResolvePhybHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( _parent, drawObject ); + return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( _parent, drawObject ); + return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); + } + + private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + { + using var est = MetaChanger.ChangeEst( _parent, drawObject ); + return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); + } + + + private IntPtr ResolveDecalWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveEidWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolveWeaponPath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveImcWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveMPapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) + => ResolveWeaponPath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolveMdlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) + => ResolveWeaponPath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); + + private IntPtr ResolveMtrlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolvePapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + private IntPtr ResolvePhybWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveSklbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveSkpWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolveWeaponPath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); + + private IntPtr ResolveTmbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 ) + => ResolveWeaponPath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) ); + + private IntPtr ResolveVfxWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) + => ResolveWeaponPath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); + + + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other, T human ) where T : Delegate + { + var del = type switch + { + Type.Human => human, + Type.Weapon => weapon, + _ => other, + }; + return Hook< T >.FromAddress( address, del ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other ) where T : Delegate + => Create( address, type, weapon, other, other ); + + + // Implementation + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private IntPtr ResolvePath( IntPtr drawObject, IntPtr path ) + => _parent._paths.ResolvePath( FindParent( drawObject, out var collection ) == null + ? Penumbra.CollectionManager.Default + : collection, path ); + + // Weapons have the characters DrawObject as a parent, + // but that may not be set yet when creating a new object, so we have to do the same detour + // as for Human DrawObjects that are just being created. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private IntPtr ResolveWeaponPath( IntPtr drawObject, IntPtr path ) + { + var parent = FindParent( drawObject, out var collection ); + if( parent != null ) + { + return _parent._paths.ResolvePath( collection, path ); + } + + var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; + var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); + if( parentCollection != null ) + { + return _parent._paths.ResolvePath( parentCollection, path ); + } + + parent = FindParent( parentObject, out collection ); + return _parent._paths.ResolvePath( parent == null + ? Penumbra.CollectionManager.Default + : collection, path ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs b/Penumbra/Interop/Resolver/PathResolver.Weapon.cs deleted file mode 100644 index 364bfa9f..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Weapon.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", - ScanType = ScanType.StaticAddress )] - public IntPtr* DrawObjectWeaponVTable; - - public Hook< GeneralResolveDelegate >? ResolveWeaponDecalPathHook; - public Hook< EidResolveDelegate >? ResolveWeaponEidPathHook; - public Hook< GeneralResolveDelegate >? ResolveWeaponImcPathHook; - public Hook< MPapResolveDelegate >? ResolveWeaponMPapPathHook; - public Hook< GeneralResolveDelegate >? ResolveWeaponMdlPathHook; - public Hook< MaterialResolveDetour >? ResolveWeaponMtrlPathHook; - public Hook< MaterialResolveDetour >? ResolveWeaponPapPathHook; - public Hook< GeneralResolveDelegate >? ResolveWeaponPhybPathHook; - public Hook< GeneralResolveDelegate >? ResolveWeaponSklbPathHook; - public Hook< GeneralResolveDelegate >? ResolveWeaponSkpPathHook; - public Hook< EidResolveDelegate >? ResolveWeaponTmbPathHook; - public Hook< MaterialResolveDetour >? ResolveWeaponVfxPathHook; - - private void SetupWeaponHooks() - { - ResolveWeaponDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour ); - ResolveWeaponEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour ); - ResolveWeaponImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour ); - ResolveWeaponMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour ); - ResolveWeaponMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour ); - ResolveWeaponMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour ); - ResolveWeaponPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour ); - ResolveWeaponPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour ); - ResolveWeaponSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour ); - ResolveWeaponSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour ); - ResolveWeaponTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour ); - ResolveWeaponVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour ); - } - - private void EnableWeaponHooks() - { - ResolveWeaponDecalPathHook?.Enable(); - ResolveWeaponEidPathHook?.Enable(); - ResolveWeaponImcPathHook?.Enable(); - ResolveWeaponMPapPathHook?.Enable(); - ResolveWeaponMdlPathHook?.Enable(); - ResolveWeaponMtrlPathHook?.Enable(); - ResolveWeaponPapPathHook?.Enable(); - ResolveWeaponPhybPathHook?.Enable(); - ResolveWeaponSklbPathHook?.Enable(); - ResolveWeaponSkpPathHook?.Enable(); - ResolveWeaponTmbPathHook?.Enable(); - ResolveWeaponVfxPathHook?.Enable(); - } - - private void DisableWeaponHooks() - { - ResolveWeaponDecalPathHook?.Disable(); - ResolveWeaponEidPathHook?.Disable(); - ResolveWeaponImcPathHook?.Disable(); - ResolveWeaponMPapPathHook?.Disable(); - ResolveWeaponMdlPathHook?.Disable(); - ResolveWeaponMtrlPathHook?.Disable(); - ResolveWeaponPapPathHook?.Disable(); - ResolveWeaponPhybPathHook?.Disable(); - ResolveWeaponSklbPathHook?.Disable(); - ResolveWeaponSkpPathHook?.Disable(); - ResolveWeaponTmbPathHook?.Disable(); - ResolveWeaponVfxPathHook?.Disable(); - } - - private void DisposeWeaponHooks() - { - ResolveWeaponDecalPathHook?.Dispose(); - ResolveWeaponEidPathHook?.Dispose(); - ResolveWeaponImcPathHook?.Dispose(); - ResolveWeaponMPapPathHook?.Dispose(); - ResolveWeaponMdlPathHook?.Dispose(); - ResolveWeaponMtrlPathHook?.Dispose(); - ResolveWeaponPapPathHook?.Dispose(); - ResolveWeaponPhybPathHook?.Dispose(); - ResolveWeaponSklbPathHook?.Dispose(); - ResolveWeaponSkpPathHook?.Dispose(); - ResolveWeaponTmbPathHook?.Dispose(); - ResolveWeaponVfxPathHook?.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index f8c99e1c..300cf1f3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,7 +1,9 @@ using System; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; using Dalamud.Logging; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.ByteString; @@ -17,18 +19,24 @@ namespace Penumbra.Interop.Resolver; // to resolve paths for character collections. public partial class PathResolver : IDisposable { - private readonly ResourceLoader _loader; public bool Enabled { get; private set; } - public PathResolver( ResourceLoader loader ) + private readonly ResourceLoader _loader; + private static readonly CutsceneCharacters Cutscenes = new(); + private static readonly DrawObjectState DrawObjects = new(); + private readonly AnimationState _animations; + private readonly PathState _paths; + private readonly MetaState _meta; + private readonly MaterialState _materials; + + public unsafe PathResolver( ResourceLoader loader ) { - _loader = loader; SignatureHelper.Initialise( this ); - SetupHumanHooks(); - SetupWeaponHooks(); - SetupMonsterHooks(); - SetupDemiHooks(); - SetupMetaHooks(); + _loader = loader; + _animations = new AnimationState( DrawObjects ); + _paths = new PathState( this ); + _meta = new MetaState( this, _paths.HumanVTable ); + _materials = new MaterialState( _paths ); } // The modified resolver that handles game path resolving. @@ -40,10 +48,10 @@ public partial class PathResolver : IDisposable // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = HandleMaterialSubFiles( type, out var collection ) - || PathCollections.TryRemove( gamePath.Path, out collection ) - || HandleAnimationFile( type, gamePath, out collection ) - || HandleDecalFile( type, gamePath, out collection ); + var nonDefault = _materials.HandleSubFiles( type, out var collection ) + || _paths.Consume( gamePath.Path, out collection ) + || _animations.HandleFiles( type, gamePath, out collection ) + || DrawObjects.HandleDecalFile( type, gamePath, out collection ); if( !nonDefault || collection == null ) { collection = Penumbra.CollectionManager.Default; @@ -56,67 +64,10 @@ public partial class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; - HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data ); + MaterialState.HandleCollection( collection, path, nonDefault, type, resolved, out data ); return true; } - private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection ) - { - if( type == ResourceType.Tex - && _lastCreatedCollection != null - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) - { - collection = _lastCreatedCollection; - return true; - } - - collection = null; - return false; - } - - private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection ) - { - switch( type ) - { - case ResourceType.Tmb: - case ResourceType.Pap: - case ResourceType.Scd: - if( _animationLoadCollection != null ) - { - collection = _animationLoadCollection; - return true; - } - - break; - case ResourceType.Avfx: - _lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default; - if( _animationLoadCollection != null ) - { - collection = _animationLoadCollection; - return true; - } - - break; - case ResourceType.Atex: - if( _lastAvfxCollection != null ) - { - collection = _lastAvfxCollection; - return true; - } - - if( _animationLoadCollection != null ) - { - collection = _animationLoadCollection; - return true; - } - - break; - } - - collection = null; - return false; - } - public void Enable() { if( Enabled ) @@ -125,15 +76,12 @@ public partial class PathResolver : IDisposable } Enabled = true; - InitializeDrawObjects(); - - EnableHumanHooks(); - EnableWeaponHooks(); - EnableMonsterHooks(); - EnableDemiHooks(); - EnableMtrlHooks(); - EnableDataHooks(); - EnableMetaHooks(); + Cutscenes.Enable(); + DrawObjects.Enable(); + _animations.Enable(); + _paths.Enable(); + _meta.Enable(); + _materials.Enable(); _loader.ResolvePathCustomization += CharacterResolver; PluginLog.Debug( "Character Path Resolver enabled." ); @@ -147,16 +95,12 @@ public partial class PathResolver : IDisposable } Enabled = false; - DisableHumanHooks(); - DisableWeaponHooks(); - DisableMonsterHooks(); - DisableDemiHooks(); - DisableMtrlHooks(); - DisableDataHooks(); - DisableMetaHooks(); - - DrawObjectToObject.Clear(); - PathCollections.Clear(); + _animations.Disable(); + DrawObjects.Disable(); + Cutscenes.Disable(); + _paths.Disable(); + _meta.Disable(); + _materials.Disable(); _loader.ResolvePathCustomization -= CharacterResolver; PluginLog.Debug( "Character Path Resolver disabled." ); @@ -165,18 +109,60 @@ public partial class PathResolver : IDisposable public void Dispose() { Disable(); - DisposeHumanHooks(); - DisposeWeaponHooks(); - DisposeMonsterHooks(); - DisposeDemiHooks(); - DisposeMtrlHooks(); - DisposeDataHooks(); - DisposeMetaHooks(); + _paths.Dispose(); + _animations.Dispose(); + DrawObjects.Dispose(); + Cutscenes.Dispose(); + _meta.Dispose(); + _materials.Dispose(); } - public unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject ) + public static unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); return ( ( IntPtr )parent, collection ); } + + public int CutsceneActor( int idx ) + => Cutscenes.GetParentIndex( idx ); + + // Use the stored information to find the GameObject and Collection linked to a DrawObject. + public static unsafe GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) + { + if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) + { + collection = data.Item1; + return gameObject; + } + + if( DrawObjects.LastGameObject != null + && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + { + collection = IdentifyCollection( DrawObjects.LastGameObject ); + return DrawObjects.LastGameObject; + } + + collection = IdentifyCollection( null ); + return null; + } + + private static unsafe ModCollection? GetCollection( IntPtr drawObject ) + { + var parent = FindParent( drawObject, out var collection ); + if( parent == null || collection == Penumbra.CollectionManager.Default ) + { + return null; + } + + return collection.HasCache ? collection : null; + } + + internal IEnumerable< KeyValuePair< Utf8String, ModCollection > > PathCollections + => _paths.Paths; + + internal IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjectMap + => DrawObjects.DrawObjects; + + internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors + => Cutscenes.Actors; } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 62d136ff..5ac267c3 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -11,6 +11,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -155,45 +156,63 @@ public partial class ConfigWindow return; } - using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); - if( drawTree ) + using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) ) { - using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); - if( table ) + if( drawTree ) { - foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectToObject ) + using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); + if( table ) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ptr.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( idx.ToString() ); - ImGui.TableNextColumn(); - var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); - var (address, name) = - obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); - ImGui.TextUnformatted( address ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( c.Name ); + foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( ptr.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( idx.ToString() ); + ImGui.TableNextColumn(); + var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); + var (address, name) = + obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); + ImGui.TextUnformatted( address ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( c.Name ); + } } } } - drawTree.Dispose(); - - using var pathTree = ImRaii.TreeNode( "Path Collections" ); - if( pathTree ) + using( var pathTree = ImRaii.TreeNode( "Path Collections" ) ) { - using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( pathTree ) + { + using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.Name ); + } + } + } + } + + using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ); + if( cutsceneTree ) + { + using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); if( table ) { - foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) + foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) { ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + ImGui.TextUnformatted( $"Cutscene Actor {idx}" ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.Name ); + ImGui.TextUnformatted( actor.Name.ToString() ); } } } From c0542d0e941079f5711a3097ea43aca95b3b211d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Aug 2022 22:12:40 +0200 Subject: [PATCH 0416/2451] Fix some texture stuff. --- Penumbra/Import/Dds/DdsFile.cs | 26 ------------------- Penumbra/Import/Dds/ImageParsing.cs | 4 +-- Penumbra/Import/Dds/TextureImporter.cs | 14 +++------- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 11 -------- 4 files changed, 6 insertions(+), 49 deletions(-) diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs index f9eca15f..c8ad8cab 100644 --- a/Penumbra/Import/Dds/DdsFile.cs +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -224,30 +224,4 @@ public class DdsFile _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; } -} - -public class TmpTexFile -{ - public TexFile.TexHeader Header; - public byte[] RgbaData = Array.Empty< byte >(); - - 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.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ), - TexFile.TextureFormat.B8G8R8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), - //TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), - TexFile.TextureFormat.B4G4R4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ), - TexFile.TextureFormat.B5G5R5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ), - _ => throw new ArgumentOutOfRangeException(), - }; - } } \ No newline at end of file diff --git a/Penumbra/Import/Dds/ImageParsing.cs b/Penumbra/Import/Dds/ImageParsing.cs index c3bdd482..5170448e 100644 --- a/Penumbra/Import/Dds/ImageParsing.cs +++ b/Penumbra/Import/Dds/ImageParsing.cs @@ -12,9 +12,9 @@ public static partial class ImageParsing { var ret = new Rgba32 { - R = ( byte )( c & 0x1F ), + R = ( byte )( c >> 11 ), G = ( byte )( ( c >> 5 ) & 0x3F ), - B = ( byte )( c >> 11 ), + B = ( byte )( c & 0x1F ), A = 0xFF, }; diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs index 8a194999..1c71f526 100644 --- a/Penumbra/Import/Dds/TextureImporter.cs +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -1,8 +1,10 @@ using System; using System.IO; using Lumina.Data.Files; +using OtterGui; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using Functions = Penumbra.GameData.Util.Functions; namespace Penumbra.Import.Dds; @@ -13,7 +15,7 @@ public class TextureImporter using var mem = new MemoryStream( target ); using var bw = new BinaryWriter( mem ); bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )TexFile.TextureFormat.B8G8R8X8 ); + bw.Write( ( uint )TexFile.TextureFormat.B8G8R8A8 ); bw.Write( ( ushort )width ); bw.Write( ( ushort )height ); bw.Write( ( ushort )1 ); @@ -71,15 +73,7 @@ public class TextureImporter 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 ]; - } - + rgba.CopyTo( texData.AsSpan( 80 ) ); return true; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 4b8f9d7f..b0c77058 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Numerics; -using System.Reflection; using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -165,16 +164,6 @@ 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 ); if( tex == null ) { From 5b07245cd9b55cfb5ea77d946e66fd9e796bbad4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Aug 2022 22:50:54 +0200 Subject: [PATCH 0417/2451] Add event for changing mod directory. --- Penumbra/Api/IPenumbraApi.cs | 4 ++ Penumbra/Api/IpcTester.cs | 17 ++++++- Penumbra/Api/PenumbraApi.cs | 6 +++ Penumbra/Api/PenumbraIpc.cs | 47 +++++++++++++------ .../Interop/Resolver/CutsceneCharacters.cs | 2 +- .../Resolver/PathResolver.Identification.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 17 +++++-- Penumbra/Mods/Manager/Mod.Manager.cs | 1 + 8 files changed, 75 insertions(+), 21 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 2472b3e2..a9288829 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -49,6 +49,10 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the currently set mod directory from the configuration. public string GetModDirectory(); + // Fired whenever a mod directory change is finished. + // Gives the full path of the mod directory and whether Penumbra treats it as valid. + public event Action< string, bool >? ModDirectoryChanged; + // Obtain the entire current penumbra configuration as a json encoded string. public string GetConfiguration(); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 81ef08a8..3b3390ee 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -28,6 +28,7 @@ public class IpcTester : IDisposable private readonly ICallGateSubscriber< object? > _disposed; private readonly ICallGateSubscriber< string, object? > _preSettingsDraw; private readonly ICallGateSubscriber< string, object? > _postSettingsDraw; + private readonly ICallGateSubscriber< string, bool, object? > _modDirectoryChanged; private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreated; @@ -45,6 +46,7 @@ public class IpcTester : IDisposable _preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw ); _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); + _modDirectoryChanged = _pi.GetIpcSubscriber< string, bool, object? >( PenumbraIpc.LabelProviderModDirectoryChanged ); _characterBaseCreated = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); _initialized.Subscribe( AddInitialized ); @@ -54,6 +56,7 @@ public class IpcTester : IDisposable _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); _settingChanged.Subscribe( UpdateLastModSetting ); _characterBaseCreated.Subscribe( UpdateLastCreated ); + _modDirectoryChanged.Subscribe( UpdateModDirectoryChanged ); } public void Dispose() @@ -67,6 +70,7 @@ public class IpcTester : IDisposable _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); _settingChanged.Unsubscribe( UpdateLastModSetting ); _characterBaseCreated.Unsubscribe( UpdateLastCreated ); + _modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged ); } private void AddInitialized() @@ -131,11 +135,18 @@ public class IpcTester : IDisposable private string _currentConfiguration = string.Empty; private string _lastDrawnMod = string.Empty; - private DateTimeOffset _lastDrawnModTime; + private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; private void UpdateLastDrawnMod( string name ) => ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now ); + private string _lastModDirectory = string.Empty; + private bool _lastModDirectoryValid = false; + private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; + + private void UpdateModDirectoryChanged( string path, bool valid ) + => ( _lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime ) = ( path, valid, DateTimeOffset.Now ); + private void DrawGeneral() { using var _ = ImRaii.TreeNode( "General IPC" ); @@ -173,6 +184,10 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderModDirectoryChanged, "Last Mod Directory Change" ); + ImGui.TextUnformatted( _lastModDirectoryTime > DateTimeOffset.MinValue + ? $"{_lastModDirectory} ({( _lastModDirectoryValid ? "Valid" : "Invalid" )}) at {_lastModDirectoryTime}" + : "None" ); DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); if( ImGui.Button( "Get" ) ) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 55bf0472..9e938322 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -84,6 +84,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } + public event Action< string, bool >? ModDirectoryChanged + { + add => Penumbra.ModManager.ModDirectoryChanged += value; + remove => Penumbra.ModManager.ModDirectoryChanged -= value; + } + public string GetConfiguration() { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 63b71322..98b3b7f9 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -26,6 +26,7 @@ public partial class PenumbraIpc : IDisposable InitializeSettingProviders( pi ); InitializeTempProviders( pi ); ProviderInitialized?.SendMessage(); + InvokeModDirectoryChanged( Penumbra.ModManager.BasePath.FullName, Penumbra.ModManager.Valid ); } public void Dispose() @@ -44,20 +45,22 @@ public partial class PenumbraIpc : IDisposable public partial class PenumbraIpc { - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderApiVersions = "Penumbra.ApiVersions"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; - public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; - public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; + public const string LabelProviderInitialized = "Penumbra.Initialized"; + public const string LabelProviderDisposed = "Penumbra.Disposed"; + public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; + public const string LabelProviderApiVersions = "Penumbra.ApiVersions"; + public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; + public const string LabelProviderModDirectoryChanged = "Penumbra.ModDirectoryChanged"; + public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; + public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; + public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; internal ICallGateProvider< object? >? ProviderInitialized; internal ICallGateProvider< object? >? ProviderDisposed; internal ICallGateProvider< int >? ProviderApiVersion; internal ICallGateProvider< (int Breaking, int Features) >? ProviderApiVersions; internal ICallGateProvider< string >? ProviderGetModDirectory; + internal ICallGateProvider< string, bool, object? >? ProviderModDirectoryChanged; internal ICallGateProvider< string >? ProviderGetConfiguration; internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; @@ -116,6 +119,16 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" ); } + try + { + ProviderModDirectoryChanged = pi.GetIpcProvider< string, bool, object? >( LabelProviderModDirectoryChanged ); + Api.ModDirectoryChanged += InvokeModDirectoryChanged; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderModDirectoryChanged}:\n{e}" ); + } + try { ProviderGetConfiguration = pi.GetIpcProvider< string >( LabelProviderGetConfiguration ); @@ -155,7 +168,17 @@ public partial class PenumbraIpc ProviderApiVersions?.UnregisterFunc(); Api.PreSettingsPanelDraw -= InvokeSettingsPreDraw; Api.PostSettingsPanelDraw -= InvokeSettingsPostDraw; + Api.ModDirectoryChanged -= InvokeModDirectoryChanged; } + + private void InvokeSettingsPreDraw( string modDirectory ) + => ProviderPreSettingsDraw!.SendMessage( modDirectory ); + + private void InvokeSettingsPostDraw( string modDirectory ) + => ProviderPostSettingsDraw!.SendMessage( modDirectory ); + + private void InvokeModDirectoryChanged( string modDirectory, bool valid ) + => ProviderModDirectoryChanged?.SendMessage( modDirectory, valid ); } public partial class PenumbraIpc @@ -239,12 +262,6 @@ public partial class PenumbraIpc private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) => ProviderGameObjectRedrawn?.SendMessage( objectAddress, objectTableIndex ); - private void InvokeSettingsPreDraw( string modDirectory ) - => ProviderPreSettingsDraw!.SendMessage( modDirectory ); - - private void InvokeSettingsPostDraw( string modDirectory ) - => ProviderPostSettingsDraw!.SendMessage( modDirectory ); - private void DisposeRedrawProviders() { ProviderRedrawName?.UnregisterAction(); @@ -319,7 +336,7 @@ public partial class PenumbraIpc try { - ProviderGetCutsceneParentIndex = pi.GetIpcProvider( LabelProviderGetCutsceneParentIndex ); + ProviderGetCutsceneParentIndex = pi.GetIpcProvider< int, int >( LabelProviderGetCutsceneParentIndex ); ProviderGetCutsceneParentIndex.RegisterFunc( Api.GetCutsceneParentIndex ); } catch( Exception e ) diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index a701c293..e8c1f05e 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -14,7 +14,7 @@ public class CutsceneCharacters : IDisposable public const int CutsceneSlots = 40; public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; - private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, ObjectReloader.CutsceneSlots ).ToArray(); + private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index d9c21999..222276c7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -190,7 +190,7 @@ public unsafe partial class PathResolver 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - >= ObjectReloader.CutsceneStartIdx and < ObjectReloader.CutsceneEndIdx => GetCutsceneName( gameObject ), + >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), _ => null, } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index b331daca..92a8c51e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -13,6 +13,7 @@ public sealed partial class Mod public event Action? ModDiscoveryStarted; public event Action? ModDiscoveryFinished; + public event Action< string, bool > ModDirectoryChanged; // Change the mod base directory and discover available mods. public void DiscoverMods( string newDir ) @@ -35,6 +36,10 @@ public sealed partial class Mod { Valid = false; BasePath = new DirectoryInfo( "." ); + if( Penumbra.Config.ModDirectory != BasePath.FullName ) + { + ModDirectoryChanged.Invoke( string.Empty, false ); + } } else { @@ -56,13 +61,19 @@ public sealed partial class Mod Valid = Directory.Exists( newDir.FullName ); if( Penumbra.Config.ModDirectory != BasePath.FullName ) { - PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.", Penumbra.Config.ModDirectory, BasePath.FullName ); - Penumbra.Config.ModDirectory = BasePath.FullName; - Penumbra.Config.Save(); + ModDirectoryChanged.Invoke( BasePath.FullName, Valid ); } } } + private static void OnModDirectoryChange( string newPath, bool _ ) + { + PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.", + Penumbra.Config.ModDirectory, newPath ); + Penumbra.Config.ModDirectory = newPath; + Penumbra.Config.Save(); + } + // Discover new mods. public void DiscoverMods() { diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 1306be5f..ff800473 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -34,6 +34,7 @@ public sealed partial class Mod public Manager( string modDirectory ) { + ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory( modDirectory, true ); ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; From f264725c456d2b694268796b595a7f3a99eeb8e3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Aug 2022 15:45:14 +0200 Subject: [PATCH 0418/2451] Add an Update Bibo Materials button. --- .../Resolver/PathResolver.DrawObjectState.cs | 2 +- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.cs | 3 +++ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 20 +++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 41e55e2e..d51ffadd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -143,7 +143,7 @@ public unsafe partial class PathResolver CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c ); } - var ret = _characterBaseCreateHook!.Original( a, b, c, d ); + var ret = _characterBaseCreateHook.Original( a, b, c, d ); if( LastGameObject != null ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 1f601ef4..f9ac3708 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -69,7 +69,7 @@ public partial class Mod } // Find all model files in the mod that contain skin materials. - private void ScanModels() + public void ScanModels() { _modelFiles.Clear(); foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) ) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 898a98f2..0c7d8b9f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -45,6 +45,9 @@ public partial class ModEditWindow : Window, IDisposable public void ChangeOption( ISubMod? subMod ) => _editor?.SetSubMod( subMod ); + public void UpdateModels() + => _editor?.ScanModels(); + public override bool DrawConditions() => _editor != null; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 2377bb77..b24a3f4e 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -93,6 +93,26 @@ public partial class ConfigWindow MoveDirectory.Draw( _mod, buttonSize ); ImGui.Dummy( _window._defaultSpace ); + DrawUpdateBibo( buttonSize ); + + ImGui.Dummy( _window._defaultSpace ); + } + + private void DrawUpdateBibo( Vector2 buttonSize) + { + if( ImGui.Button( "Update Bibo Material", buttonSize ) ) + { + var editor = new Mod.Editor( _mod, null ); + editor.ReplaceAllMaterials( "bibo", "b" ); + editor.ReplaceAllMaterials( "bibopube", "c" ); + editor.SaveAllModels(); + _window.ModEditPopup.UpdateModels(); + } + + ImGuiUtil.HoverTooltip( "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" + + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" + + "Use this for outdated mods made for old Bibo bodies.\n" + + "Go to Advanced Editing for more fine-tuned control over material assignment." ); } private void BackupButtons( Vector2 buttonSize ) From 8aefdbd94856fea1a58c7b9457a89281688c0326 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Aug 2022 21:18:21 +0200 Subject: [PATCH 0419/2451] Rename some collection stuff. --- Penumbra.GameData/Structs/CharacterEquip.cs | 15 +++++++++------ Penumbra.GameData/Structs/CustomizeData.cs | 9 ++++++--- .../Collections/CollectionManager.Active.cs | 5 +++-- .../Interop/Loader/ResourceLoader.Debug.cs | 12 ++++++++++-- Penumbra/Penumbra.cs | 14 +++++++------- Penumbra/UI/ConfigWindow.DebugTab.cs | 4 ++-- Penumbra/UI/ConfigWindow.ModsTab.cs | 8 ++++---- .../UI/ConfigWindow.SettingsTab.General.cs | 18 +++++++++--------- Penumbra/UI/ConfigWindow.Tutorial.cs | 1 + 9 files changed, 51 insertions(+), 35 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index 5e8489a1..c354cd8a 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -1,13 +1,9 @@ using System; using Penumbra.GameData.Enums; +using Penumbra.GameData.Util; namespace Penumbra.GameData.Structs; -public unsafe struct CharacterArmorData -{ - public fixed byte Data[40]; -} - public readonly unsafe struct CharacterEquip { public static readonly CharacterEquip Null = new(null); @@ -26,7 +22,6 @@ public readonly unsafe struct CharacterEquip public ref CharacterArmor this[ EquipSlot slot ] => ref _armor[ IndexOf( slot ) ]; - public ref CharacterArmor Head => ref _armor[ 0 ]; @@ -108,4 +103,12 @@ public readonly unsafe struct CharacterEquip _ => throw new ArgumentOutOfRangeException( nameof( slot ), slot, null ), }; } + + public void Write( IntPtr target ) + { + Functions.MemCpyUnchecked( ( void* )target, _armor, sizeof( CharacterArmor ) * 10 ); + } + + public bool Equals( CharacterEquip other ) + => Functions.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 0c7fcbf1..4bce24cf 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -17,7 +17,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } - public void Write( void* target ) + public readonly void Write( void* target ) { fixed( byte* ptr = Data ) { @@ -25,14 +25,14 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } - public CustomizeData Clone() + public readonly CustomizeData Clone() { var ret = new CustomizeData(); Write( ret.Data ); return ret; } - public bool Equals( CustomizeData other ) + public readonly bool Equals( CustomizeData other ) { fixed( byte* ptr = Data ) { @@ -40,6 +40,9 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } + public static bool Equals( CustomizeData* lhs, CustomizeData* rhs ) + => Functions.MemCmpUnchecked( lhs, rhs, Size ) == 0; + public override bool Equals( object? obj ) => obj is CustomizeData other && Equals( other ); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 8ab2c22b..81b3d7db 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Mods; +using Penumbra.UI; namespace Penumbra.Collections; @@ -196,7 +197,7 @@ public partial class ModCollection var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { - PluginLog.Error( $"Last choice of Default Collection {defaultName} is not available, reset to {Empty.Name}." ); + PluginLog.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." ); Default = Empty; configChanged = true; } @@ -210,7 +211,7 @@ public partial class ModCollection var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - PluginLog.Error( $"Last choice of Current Collection {currentName} is not available, reset to {DefaultCollection}." ); + PluginLog.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); Current = DefaultName; configChanged = true; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 4fb934f4..bdcf1220 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; @@ -161,7 +162,9 @@ public unsafe partial class ResourceLoader return ret == null ? null : ret->Value; } - public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph, int idx ); + public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph, + int idx ); + public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph ); public delegate void ResourceAction( uint crc32, ResourceHandle* graph ); @@ -223,9 +226,14 @@ public unsafe partial class ResourceLoader // Prevent resource management weirdness. private byte ResourceHandleDecRefDetour( ResourceHandle* handle ) { + if( handle == null ) + { + return 0; + } + if( handle->RefCount != 0 ) { - return _decRefHook!.Original( handle ); + return _decRefHook.Original( handle ); } PluginLog.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ecb69ca..a5d4e5c8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -79,9 +79,9 @@ public class Penumbra : IDalamudPlugin Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); Config = Configuration.Load(); - TempMods = new TempModManager(); - MetaFileManager = new MetaFileManager(); - ResourceLoader = new ResourceLoader( this ); + TempMods = new TempModManager(); + MetaFileManager = new MetaFileManager(); + ResourceLoader = new ResourceLoader( this ); ResourceLoader.EnableHooks(); ResourceLogger = new ResourceLogger( ResourceLoader ); ResidentResources = new ResidentResourceManager(); @@ -450,10 +450,10 @@ public class Penumbra : IDalamudPlugin c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); sb.AppendLine( "**Collections**" ); - sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); - sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); - sb.AppendFormat( "> **`Default Collection: `** {0}\n", CollectionManager.Default.AnonymizedName); - sb.AppendFormat( "> **`Current Collection: `** {0}\n", CollectionManager.Current.AnonymizedName); + sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); + sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); + sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName ); + sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName ); foreach( var type in CollectionTypeExtensions.Special ) { var collection = CollectionManager.ByType( type ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 5ac267c3..081b0e79 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -87,9 +87,9 @@ public partial class ConfigWindow var manager = Penumbra.ModManager; PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); PrintValue( "Git Commit Hash", Penumbra.CommitHash ); - PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name ); + PrintValue( SelectedCollection, Penumbra.CollectionManager.Current.Name ); PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); - PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name ); + PrintValue( DefaultCollection, Penumbra.CollectionManager.Default.Name ); PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 5c9cb0e6..fbafb806 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -79,12 +79,12 @@ public partial class ConfigWindow private static void DrawDefaultCollectionButton( Vector2 width ) { - var name = $"Default Collection ({Penumbra.CollectionManager.Default.Name})"; + var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})"; var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; - var tt = isCurrent ? "The current collection is already the configured default collection." - : isEmpty ? "The default collection is configured to be empty." - : "Set the current collection to the configured default collection."; + var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}." + : isEmpty ? $"The {DefaultCollection} is configured to be empty." + : $"Set the {SelectedCollection} to the configured {DefaultCollection}."; if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) { Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index badf9f22..fe9983a6 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -59,27 +59,27 @@ public partial class ConfigWindow Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); - Checkbox( "Use Special Collections in Character Window", - "Use the character collection for your character's name in your main character window, if it is set.", + Checkbox( $"Use {AssignedCollections} in Character Window", + "Use the character collection for your characters name or the Your Character collection in your main character window, if it is set.", Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); - Checkbox( "Use Special Collections in Adventurer Cards", + Checkbox( $"Use {AssignedCollections} in Adventurer Cards", "Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.", Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); - Checkbox( "Use Special Collections in Try-On Window", + Checkbox( $"Use {AssignedCollections} in Try-On Window", "Use the character collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); - Checkbox( "Use Special Collections in Inspect Windows", + Checkbox( $"Use {AssignedCollections} in Inspect Windows", "Use the appropriate character collection for the character you are currently inspecting, based on their name.", Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); - Checkbox( "Use Special Collections based on Ownership", + Checkbox( $"Use {AssignedCollections} based on Ownership", "Use the owner's name to determine the appropriate character collection for mounts, companions and combat pets.", Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); Checkbox( "Prefer Named Collections over Ownership", - "If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owner's collection.\n" + "If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owners collection.\n" + "That is, if you have a 'Topaz Carbuncle' collection, it will use this one instead of the one for its owner.", Penumbra.Config.PreferNamedCollectionsOverOwners, v => Penumbra.Config.PreferNamedCollectionsOverOwners = v ); - Checkbox( "Use Default Collection for Housing Retainers", - "Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the default collection.\n" + Checkbox( $"Use {DefaultCollection} for Housing Retainers", + $"Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the {DefaultCollection}.\n" + "It is not possible to make them have their own collection, since they have no connection to their actual name.", Penumbra.Config.UseDefaultCollectionForRetainers, v => Penumbra.Config.UseDefaultCollectionForRetainers = v ); ImGui.Dummy( _window._defaultSpace ); diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 6962a84e..ef75566d 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -11,6 +11,7 @@ public partial class ConfigWindow public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; public const string ActiveCollections = "Active Collections"; + public const string AssignedCollections = "Assigned Collections"; public const string GroupAssignment = "Group Assignment"; public const string CharacterGroups = "Character Groups"; public const string ConditionalGroup = "Group"; From 5ac3a903f6a50275c0ae2c5f4d29e79e9d6cab96 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Aug 2022 21:18:34 +0200 Subject: [PATCH 0420/2451] Update submod positions on group deletions. --- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 6d35b5d1..b5d3f36a 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -70,6 +70,7 @@ public sealed partial class Mod var group = mod._groups[ groupIdx ]; ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 ); mod._groups.RemoveAt( groupIdx ); + UpdateSubModPositions( mod, groupIdx ); group.DeleteFile( mod.ModPath, groupIdx ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); } @@ -78,18 +79,22 @@ public sealed partial class Mod { if( mod._groups.Move( groupIdxFrom, groupIdxTo ) ) { - foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( Math.Min( groupIdxFrom, groupIdxTo ) ) ) - { - foreach( var (o, optionIdx) in group.OfType().WithIndex() ) - { - o.SetPosition( groupIdx, optionIdx ); - } - } - + UpdateSubModPositions( mod, Math.Min( groupIdxFrom, groupIdxTo ) ); ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo ); } } + private static void UpdateSubModPositions( Mod mod, int fromGroup ) + { + foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( fromGroup ) ) + { + foreach( var (o, optionIdx) in group.OfType().WithIndex() ) + { + o.SetPosition( groupIdx, optionIdx ); + } + } + } + public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription ) { var group = mod._groups[ groupIdx ]; From 5e9cb77415113bb0fbf2ad359db53a9e55fd8ac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Aug 2022 21:20:32 +0200 Subject: [PATCH 0421/2451] Add material file parsing and writing. --- Penumbra.GameData/Files/MtrlFile.Write.cs | 96 ++++++++++++++++ Penumbra.GameData/Files/MtrlFile.cs | 133 ++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 Penumbra.GameData/Files/MtrlFile.Write.cs create mode 100644 Penumbra.GameData/Files/MtrlFile.cs diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs new file mode 100644 index 00000000..910854b9 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Linq; +using System.Text; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public byte[] Write() + { + using var stream = new MemoryStream(); + using( var w = new BinaryWriter( stream ) ) + { + const int materialHeaderSize = 4 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1; + + w.BaseStream.Seek( materialHeaderSize, SeekOrigin.Begin ); + ushort cumulativeStringOffset = 0; + foreach( var texture in Textures ) + { + w.Write( cumulativeStringOffset ); + w.Write( texture.Flags ); + cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 ); + } + + foreach( var colorSet in UvColorSets.Concat( ColorSets ) ) + { + w.Write( cumulativeStringOffset ); + w.Write( colorSet.Index ); + cumulativeStringOffset += ( ushort )( colorSet.Name.Length + 1 ); + } + + foreach( var text in Textures.Select( t => t.Path ) + .Concat( UvColorSets.Concat( ColorSets ).Select( c => c.Name ).Append( ShaderPackage.Name ) ) ) + { + w.Write( Encoding.UTF8.GetBytes( text ) ); + w.Write( ( byte )'\0' ); + } + + w.Write( AdditionalData ); + foreach( var color in ColorSetData ) + { + w.Write( color ); + } + + w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); + w.Write( ( ushort )ShaderPackage.ShaderKeys.Length ); + w.Write( ( ushort )ShaderPackage.Constants.Length ); + w.Write( ( ushort )ShaderPackage.Samplers.Length ); + w.Write( ShaderPackage.Unk ); + + foreach( var key in ShaderPackage.ShaderKeys ) + { + w.Write( key.Category ); + w.Write( key.Value ); + } + + foreach( var constant in ShaderPackage.Constants ) + { + w.Write( constant.Id ); + w.Write( constant.Value ); + } + + foreach( var sampler in ShaderPackage.Samplers ) + { + w.Write( sampler.SamplerId ); + w.Write( sampler.Flags ); + w.Write( sampler.TextureIndex ); + w.Write( ( ushort )0 ); + w.Write( ( byte )0 ); + } + + foreach( var value in ShaderPackage.ShaderValues ) + { + w.Write( value ); + } + + WriteHeader( w, ( ushort )w.BaseStream.Position, cumulativeStringOffset ); + } + + return stream.ToArray(); + } + + private void WriteHeader( BinaryWriter w, ushort fileSize, ushort shaderPackageNameOffset ) + { + w.BaseStream.Seek( 0, SeekOrigin.Begin ); + w.Write( Version ); + w.Write( fileSize ); + w.Write( ( ushort )( ColorSetData.Length * 2 ) ); + w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); + w.Write( shaderPackageNameOffset ); + w.Write( ( byte )Textures.Length ); + w.Write( ( byte )UvColorSets.Length ); + w.Write( ( byte )ColorSets.Length ); + w.Write( ( byte )AdditionalData.Length ); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs new file mode 100644 index 00000000..5d6d81a0 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using System.Text; +using Lumina.Data.Parsing; +using Lumina.Extensions; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public struct ColorSet + { + public string Name; + public ushort Index; + } + + public struct Texture + { + public string Path; + public ushort Flags; + } + + public struct Constant + { + public uint Id; + public uint Value; + } + + public struct ShaderPackageData + { + public string Name; + public ShaderKey[] ShaderKeys; + public Constant[] Constants; + public Sampler[] Samplers; + public float[] ShaderValues; + public uint Unk; + } + + + public uint Version; + + public Texture[] Textures; + public ColorSet[] UvColorSets; + public ColorSet[] ColorSets; + public ushort[] ColorSetData; + public ShaderPackageData ShaderPackage; + public byte[] AdditionalData; + + public MtrlFile( byte[] data ) + { + using var stream = new MemoryStream( data ); + using var r = new BinaryReader( stream ); + + Version = r.ReadUInt32(); + r.ReadUInt16(); // file size + var dataSetSize = r.ReadUInt16(); + var stringTableSize = r.ReadUInt16(); + var shaderPackageNameOffset = r.ReadUInt16(); + var textureCount = r.ReadByte(); + var uvSetCount = r.ReadByte(); + var colorSetCount = r.ReadByte(); + var additionalDataSize = r.ReadByte(); + + Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); + UvColorSets = ReadColorSetOffsets( r, uvSetCount, out var uvOffsets ); + ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + + var strings = r.ReadBytes( stringTableSize ); + for( var i = 0; i < textureCount; ++i ) + { + Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] ); + } + + for( var i = 0; i < uvSetCount; ++i ) + { + UvColorSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); + } + + for( var i = 0; i < colorSetCount; ++i ) + { + ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); + } + + ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); + + AdditionalData = r.ReadBytes( additionalDataSize ); + ColorSetData = r.ReadStructuresAsArray< ushort >( dataSetSize / 2 ); + + var shaderValueListSize = r.ReadUInt16(); + var shaderKeyCount = r.ReadUInt16(); + var constantCount = r.ReadUInt16(); + var samplerCount = r.ReadUInt16(); + ShaderPackage.Unk = r.ReadUInt32(); + + ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount ); + ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount ); + ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount ); + ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 ); + } + + private static Texture[] ReadTextureOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new Texture[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Flags = r.ReadUInt16(); + } + + return ret; + } + + private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new ColorSet[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Index = r.ReadUInt16(); + } + + return ret; + } + + private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset ) + { + strings = strings[ offset.. ]; + var end = strings.IndexOf( ( byte )'\0' ); + return Encoding.UTF8.GetString( strings[ ..end ] ); + } +} \ No newline at end of file From df9f791395f1ded6311f2c50f676cd3897d17e69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 15 Aug 2022 12:57:57 +0200 Subject: [PATCH 0422/2451] Check for too long paths when building cache. --- .../Collections/ModCollection.Cache.Access.cs | 21 ++++++++++++++++++- Penumbra/Collections/ModCollection.Cache.cs | 3 +++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8c2bddd3..332c7f81 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using Dalamud.Logging; using OtterGui.Classes; @@ -70,7 +72,24 @@ public partial class ModCollection // Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile( Utf8GamePath path, FullPath fullPath ) - => _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath ); + { + if( CheckFullPath( path, fullPath ) ) + { + _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath ); + } + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool CheckFullPath( Utf8GamePath path, FullPath fullPath ) + { + if( fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength ) + { + return true; + } + + PluginLog.Error( $"Could not add the redirection {path} to {fullPath}, the redirected path is too long." ); + return false; + } // Force a file resolve to be removed. internal void RemoveFile( Utf8GamePath path ) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index c329b3de..c525edd1 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -326,6 +326,9 @@ public partial class ModCollection // Inside the same mod, conflicts are not recorded. private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) { + if (!CheckFullPath( path, file )) + return; + if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) { return; From 6773fe093237f4ad6e3a1c82d7f65f686e75c93f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 15 Aug 2022 12:58:14 +0200 Subject: [PATCH 0423/2451] Set minimal model set Id values to 0 for a bunch of meta edits --- Penumbra/Meta/Files/EqdpFile.cs | 1 - Penumbra/Meta/Files/ImcFile.cs | 1 - Penumbra/UI/Classes/ModEditWindow.Meta.cs | 18 +++++++++--------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 6de0007d..b2504bbf 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -47,7 +47,6 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile throw new IndexOutOfRangeException(); } - var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 ); return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) ); } set diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 30c21dc1..b6e631a3 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -67,7 +67,6 @@ public unsafe class ImcFile : MetaBaseFile public readonly Utf8GamePath Path; public readonly int NumParts; - public bool ChangesSinceLoad = true; public ReadOnlySpan< ImcEntry > Span => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 35a9d956..00623ae4 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -114,7 +114,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) ) { _new = _new with { SetId = setId }; } @@ -209,7 +209,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) ) { _new = _new with { SetId = setId }; } @@ -337,7 +337,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Object Type" ); ImGui.TableNextColumn(); - if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, ushort.MaxValue ) ) + if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue ) ) { _new = _new with { PrimaryId = setId }; } @@ -360,7 +360,7 @@ public partial class ModEditWindow } else { - if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, ushort.MaxValue ) ) + if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue ) ) { _new = _new with { SecondaryId = setId2 }; } @@ -369,7 +369,7 @@ public partial class ModEditWindow } ImGui.TableNextColumn(); - if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, byte.MaxValue ) ) + if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue ) ) { _new = _new with { Variant = variant }; } @@ -518,7 +518,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) ) { _new = _new with { SetId = setId }; } @@ -616,7 +616,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) ) { _new = _new with { SetId = setId }; } @@ -810,13 +810,13 @@ public partial class ModEditWindow // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. - private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int maxId ) + private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId ) { int tmp = currentId; ImGui.SetNextItemWidth( width ); if( ImGui.InputInt( label, ref tmp, 0 ) ) { - tmp = Math.Clamp( tmp, 1, maxId ); + tmp = Math.Clamp( tmp, minId, maxId ); } newId = ( ushort )tmp; From 09417bd6c136ab9b05372dbbe7549ad15d279f07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 15 Aug 2022 12:58:44 +0200 Subject: [PATCH 0424/2451] Resolve actor 244 to player collection. --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 222276c7..81d05950 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -186,12 +186,11 @@ public unsafe partial class PathResolver var actualName = gameObject->ObjectIndex switch { 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview + 244 => Penumbra.Config.UseCharacterCollectionsInCards ? GetPlayerName() : null, // portrait list and editor >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), - _ => null, } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); From 5b5a1e2fd866c772dee5395191c4eb61ff9c47de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 15 Aug 2022 13:15:26 +0200 Subject: [PATCH 0425/2451] Check path length on adding them to the cache and log error if a path is too long. --- .../ByteString/ByteStringFunctions.Construction.cs | 2 +- Penumbra.GameData/ByteString/Utf8RelPath.cs | 2 +- Penumbra/Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 4 +++- Penumbra/UI/Classes/ModEditWindow.Files.cs | 5 ++++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs index ca4cadd0..18cc3a81 100644 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs @@ -35,7 +35,7 @@ public static unsafe partial class ByteStringFunctions var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); fixed( char* ptr = s ) { - Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); + Encoding.UTF8.GetBytes( ptr, s.Length, path, length + 1 ); } path[ length ] = 0; diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs index da7e9332..cef27b6f 100644 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ b/Penumbra.GameData/ByteString/Utf8RelPath.cs @@ -36,7 +36,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf return true; } - var substring = s!.Replace( '/', '\\' ).TrimStart('\\'); + var substring = s.Replace( '/', '\\' ).TrimStart('\\'); if( substring.Length > MaxRelPathLength ) { return false; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 332c7f81..fdc65e4b 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -87,7 +87,7 @@ public partial class ModCollection return true; } - PluginLog.Error( $"Could not add the redirection {path} to {fullPath}, the redirected path is too long." ); + PluginLog.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" ); return false; } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index c525edd1..dbb07b95 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -326,8 +326,10 @@ public partial class ModCollection // Inside the same mod, conflicts are not recorded. private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) { - if (!CheckFullPath( path, file )) + if( !CheckFullPath( path, file ) ) + { return; + } if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index fd46e0ae..8390850b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -319,7 +319,10 @@ public partial class ModEditWindow if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) ) { var failedFiles = _editor!.ApplyFiles(); - PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.Name}." ); + if( failedFiles > 0 ) + { + PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." ); + } } From 80edfe7804e5d5d65957dbe095f4e87b89f821d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Aug 2022 15:30:05 +0200 Subject: [PATCH 0426/2451] Added an event when a newly created draw object finishes CharacterBase.Create. --- Penumbra/Api/IPenumbraApi.cs | 6 + Penumbra/Api/IpcTester.cs | 26 ++- Penumbra/Api/PenumbraApi.cs | 6 + Penumbra/Api/PenumbraIpc.cs | 19 +++ Penumbra/Collections/ConflictCache.cs | 148 ------------------ .../Resolver/PathResolver.DrawObjectState.cs | 2 + 6 files changed, 54 insertions(+), 153 deletions(-) delete mode 100644 Penumbra/Collections/ConflictCache.cs diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index a9288829..69b7479e 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -26,6 +26,8 @@ public delegate void ModSettingChanged( ModSettingChange type, string collection public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData ); +public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr drawObject ); + public enum PenumbraApiEc { Success = 0, @@ -73,6 +75,10 @@ public interface IPenumbraApi : IPenumbraApiBase // before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand. public event CreatingCharacterBaseDelegate? CreatingCharacterBase; + // Triggered after a character base was created if a corresponding gameObject could be found, + // so you can apply flag changes after finishing. + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; + // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 3b3390ee..c251eb35 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -31,7 +31,8 @@ public class IpcTester : IDisposable private readonly ICallGateSubscriber< string, bool, object? > _modDirectoryChanged; private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; - private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreated; + private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreating; + private readonly ICallGateSubscriber< IntPtr, string, IntPtr, object? > _characterBaseCreated; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); @@ -47,15 +48,17 @@ public class IpcTester : IDisposable _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); _modDirectoryChanged = _pi.GetIpcSubscriber< string, bool, object? >( PenumbraIpc.LabelProviderModDirectoryChanged ); - _characterBaseCreated = + _characterBaseCreating = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); + _characterBaseCreated = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, object? >( PenumbraIpc.LabelProviderCreatedCharacterBase ); _initialized.Subscribe( AddInitialized ); _disposed.Subscribe( AddDisposed ); _redrawn.Subscribe( SetLastRedrawn ); _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); _settingChanged.Subscribe( UpdateLastModSetting ); - _characterBaseCreated.Subscribe( UpdateLastCreated ); + _characterBaseCreating.Subscribe( UpdateLastCreated ); + _characterBaseCreated.Subscribe( UpdateLastCreated2 ); _modDirectoryChanged.Subscribe( UpdateModDirectoryChanged ); } @@ -69,7 +72,8 @@ public class IpcTester : IDisposable _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); _settingChanged.Unsubscribe( UpdateLastModSetting ); - _characterBaseCreated.Unsubscribe( UpdateLastCreated ); + _characterBaseCreating.Unsubscribe( UpdateLastCreated ); + _characterBaseCreated.Unsubscribe( UpdateLastCreated2 ); _modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged ); } @@ -218,6 +222,7 @@ public class IpcTester : IDisposable private IntPtr _currentDrawObject = IntPtr.Zero; private int _currentCutsceneActor = 0; private string _lastCreatedGameObjectName = string.Empty; + private IntPtr _lastCreatedDrawObject = IntPtr.Zero; private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) @@ -225,6 +230,15 @@ public class IpcTester : IDisposable var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = IntPtr.Zero; + } + + private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) + { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = drawObject; } private void DrawResolve() @@ -318,7 +332,9 @@ public class IpcTester : IDisposable DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" ); if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) { - ImGui.TextUnformatted( $"for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); + ImGui.TextUnformatted( _lastCreatedDrawObject != IntPtr.Zero + ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" + : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9e938322..fb708bbe 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -45,6 +45,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi remove => PathResolver.DrawObjectState.CreatingCharacterBase -= value; } + public event CreatedCharacterBaseDelegate? CreatedCharacterBase + { + add => PathResolver.DrawObjectState.CreatedCharacterBase += value; + remove => PathResolver.DrawObjectState.CreatedCharacterBase -= value; + } + public bool Valid => _penumbra != null; diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 98b3b7f9..34172c92 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -282,6 +282,7 @@ public partial class PenumbraIpc public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; + public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase"; internal ICallGateProvider< string, string >? ProviderResolveDefault; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; @@ -291,6 +292,7 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; + internal ICallGateProvider< IntPtr, string, IntPtr, object? >? ProviderCreatedCharacterBase; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -374,6 +376,17 @@ public partial class PenumbraIpc { PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" ); } + + try + { + ProviderCreatedCharacterBase = + pi.GetIpcProvider< IntPtr, string, IntPtr, object? >( LabelProviderCreatedCharacterBase ); + Api.CreatedCharacterBase += CreatedCharacterBaseEvent; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" ); + } } private void DisposeResolveProviders() @@ -385,12 +398,18 @@ public partial class PenumbraIpc ProviderReverseResolvePath?.UnregisterFunc(); ProviderReverseResolvePathPlayer?.UnregisterFunc(); Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; + Api.CreatedCharacterBase -= CreatedCharacterBaseEvent; } private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData ) { ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, modelId, customize, equipData ); } + + private void CreatedCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr drawObject ) + { + ProviderCreatedCharacterBase?.SendMessage( gameObject, collection.Name, drawObject ); + } } public partial class PenumbraIpc diff --git a/Penumbra/Collections/ConflictCache.cs b/Penumbra/Collections/ConflictCache.cs deleted file mode 100644 index e1bf9f42..00000000 --- a/Penumbra/Collections/ConflictCache.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using OtterGui.Classes; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections; - -public struct ConflictCache -{ - // A conflict stores all data about a mod conflict. - public readonly struct Conflict : IComparable< Conflict > - { - public readonly object Data; - public readonly int Mod1; - public readonly int Mod2; - public readonly bool Mod1Priority; - public readonly bool Solved; - - public Conflict( int modIdx1, int modIdx2, bool priority, bool solved, object data ) - { - Mod1 = modIdx1; - Mod2 = modIdx2; - Data = data; - Mod1Priority = priority; - Solved = solved; - } - - // Order: Mod1 -> Mod1 overwritten -> Mod2 -> File > MetaManipulation - public int CompareTo( Conflict other ) - { - var idxComp = Mod1.CompareTo( other.Mod1 ); - if( idxComp != 0 ) - { - return idxComp; - } - - if( Mod1Priority != other.Mod1Priority ) - { - return Mod1Priority ? 1 : -1; - } - - idxComp = Mod2.CompareTo( other.Mod2 ); - if( idxComp != 0 ) - { - return idxComp; - } - - return Data switch - { - Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ), - Utf8GamePath => -1, - MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ), - MetaManipulation => 1, - _ => 0, - }; - } - - public override string ToString() - => ( Mod1Priority, Solved ) switch - { - (true, true) => $"{Penumbra.ModManager[ Mod1 ].Name} > {Penumbra.ModManager[ Mod2 ].Name} ({Data})", - (true, false) => $"{Penumbra.ModManager[ Mod1 ].Name} >= {Penumbra.ModManager[ Mod2 ].Name} ({Data})", - (false, true) => $"{Penumbra.ModManager[ Mod1 ].Name} < {Penumbra.ModManager[ Mod2 ].Name} ({Data})", - (false, false) => $"{Penumbra.ModManager[ Mod1 ].Name} <= {Penumbra.ModManager[ Mod2 ].Name} ({Data})", - }; - } - - private readonly List< Conflict > _conflicts = new(); - private bool _isSorted = true; - - public ConflictCache() - { } - - public IReadOnlyList< Conflict > Conflicts - { - get - { - Sort(); - return _conflicts; - } - } - - // Find all mod conflicts concerning the specified mod (in both directions). - public SubList< Conflict > ModConflicts( int modIdx ) - { - Sort(); - var start = _conflicts.FindIndex( c => c.Mod1 == modIdx ); - if( start < 0 ) - { - return SubList< Conflict >.Empty; - } - - var end = _conflicts.FindIndex( start, c => c.Mod1 != modIdx ); - return new SubList< Conflict >( _conflicts, start, end - start ); - } - - private void Sort() - { - if( !_isSorted ) - { - _conflicts?.Sort(); - _isSorted = true; - } - } - - // Add both directions for the mod. - // On same priority, it is assumed that mod1 is the earlier one. - // Also update older conflicts to refer to the highest-prioritized conflict. - private void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, object data ) - { - var solved = priority1 != priority2; - var priority = priority1 >= priority2; - var prioritizedMod = priority ? modIdx1 : modIdx2; - _conflicts.Add( new Conflict( modIdx1, modIdx2, priority, solved, data ) ); - _conflicts.Add( new Conflict( modIdx2, modIdx1, !priority, solved, data ) ); - for( var i = 0; i < _conflicts.Count; ++i ) - { - var c = _conflicts[ i ]; - if( data.Equals( c.Data ) ) - { - _conflicts[ i ] = c.Mod1Priority - ? new Conflict( prioritizedMod, c.Mod2, true, c.Solved || solved, data ) - : new Conflict( c.Mod1, prioritizedMod, false, c.Solved || solved, data ); - } - } - - _isSorted = false; - } - - public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath ) - => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath ); - - public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) - => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation ); - - public void ClearConflicts() - => _conflicts?.Clear(); - - public void ClearFileConflicts() - => _conflicts?.RemoveAll( m => m.Data is Utf8GamePath ); - - public void ClearMetaConflicts() - => _conflicts?.RemoveAll( m => m.Data is MetaManipulation ); - - public void ClearConflictsWithMod( int modIdx ) - => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == modIdx ); -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index d51ffadd..ae0fc049 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -18,6 +18,7 @@ public unsafe partial class PathResolver public class DrawObjectState { public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; + public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; public IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjects => _drawObjectToObject; @@ -147,6 +148,7 @@ public unsafe partial class PathResolver if( LastGameObject != null ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); + CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ret ); } return ret; From fee3f500c5b4c2a32222c7c11a35cd0ee024a393 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Aug 2022 22:59:56 +0200 Subject: [PATCH 0427/2451] Some cleanup --- Penumbra.GameData/Structs/CharacterEquip.cs | 5 +++-- Penumbra.GameData/Structs/CharacterWeapon.cs | 8 ++++++++ .../Interop/Resolver/PathResolver.AnimationState.cs | 12 ++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index c354cd8a..a8744fff 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -104,9 +104,10 @@ public readonly unsafe struct CharacterEquip }; } - public void Write( IntPtr target ) + + public void Load( CharacterEquip source ) { - Functions.MemCpyUnchecked( ( void* )target, _armor, sizeof( CharacterArmor ) * 10 ); + Functions.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); } public bool Equals( CharacterEquip other ) diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index 62752ed1..0c25d66f 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -32,6 +32,14 @@ public readonly struct CharacterWeapon : IEquatable< CharacterWeapon > Stain = stain; } + public CharacterWeapon( ulong value ) + { + Set = ( SetId )value; + Type = ( WeaponType )( value >> 16 ); + Variant = ( ushort )( value >> 32 ); + Stain = ( StainId )( value >> 48 ); + } + public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); public bool Equals( CharacterWeapon other ) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index b7ba3617..b24bb84c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -124,7 +124,7 @@ public unsafe partial class PathResolver } finally { - ret = _loadTimelineResourcesHook!.Original( timeline ); + ret = _loadTimelineResourcesHook.Original( timeline ); } _animationLoadCollection = old; @@ -145,7 +145,7 @@ public unsafe partial class PathResolver var last = _animationLoadCollection; _animationLoadCollection = _drawObjectState.LastCreatedCollection ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); - _characterBaseLoadAnimationHook!.Original( drawObject ); + _characterBaseLoadAnimationHook.Original( drawObject ); _animationLoadCollection = last; } @@ -159,7 +159,7 @@ public unsafe partial class PathResolver { var last = _animationLoadCollection; _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); - var ret = _loadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); + var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); _animationLoadCollection = last; return ret; } @@ -184,7 +184,7 @@ public unsafe partial class PathResolver } } - _loadSomePapHook!.Original( a1, a2, a3, a4 ); + _loadSomePapHook.Original( a1, a2, a3, a4 ); _animationLoadCollection = last; } @@ -196,7 +196,7 @@ public unsafe partial class PathResolver { var last = _animationLoadCollection; _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); - _someActionLoadHook!.Original( gameObject ); + _someActionLoadHook.Original( gameObject ); _animationLoadCollection = last; } @@ -208,7 +208,7 @@ public unsafe partial class PathResolver var last = _animationLoadCollection; var gameObject = ( GameObject* )( unk - 0x8B0 ); _animationLoadCollection = IdentifyCollection( gameObject ); - _someOtherAvfxHook!.Original( unk ); + _someOtherAvfxHook.Original( unk ); _animationLoadCollection = last; } } From 5a278d4424d5037d467bf0924850a9207b77de69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Aug 2022 23:00:13 +0200 Subject: [PATCH 0428/2451] Add disabled sections to selector in meta edit --- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 00623ae4..eef21256 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -79,7 +79,6 @@ public partial class ModEditWindow if( table ) { drawNew( _editor!, _iconSize ); - ImGui.Separator(); foreach( var (item, index) in items.ToArray().WithIndex() ) { using var id = ImRaii.PushId( index ); @@ -119,7 +118,7 @@ public partial class ModEditWindow _new = _new with { SetId = setId }; } - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( "Model Set ID"); ImGui.TableNextColumn(); if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) @@ -127,9 +126,10 @@ public partial class ModEditWindow _new = _new with { Slot = slot }; } - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( "Equip Slot"); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); @@ -241,6 +241,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Equip Slot" ); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ ); @@ -377,6 +378,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Variant ID" ); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, 1, byte.MaxValue, 0f ); @@ -550,6 +552,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "EST Type" ); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f ); } @@ -577,7 +580,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "EST Type" ); // Values - var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); + var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); ImGui.TableNextColumn(); if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, out var entry, 0, ushort.MaxValue, 0.05f ) ) @@ -624,6 +627,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Model Set ID" ); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ ); ImGui.TableNextColumn(); @@ -743,6 +747,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Scaling Type" ); // Values + using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( FloatWidth ); ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f ); @@ -831,7 +836,7 @@ public partial class ModEditWindow defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue ); newValue = currentValue; ImGui.Checkbox( label, ref newValue ); - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); return newValue != currentValue; } @@ -850,7 +855,7 @@ public partial class ModEditWindow newValue = Math.Clamp( newValue, minValue, maxValue ); } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); return newValue != currentValue; } From 18df9894204048c670cbf934024b0530215af598 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Aug 2022 23:00:28 +0200 Subject: [PATCH 0429/2451] Check folder names for '.' and '..' --- Penumbra/Mods/Mod.Creation.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index abe11f25..d9c64cc1 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -97,7 +97,7 @@ public partial class Mod .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) .Where( t => t.Item1 ); - var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. + var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. { Name = option.Name, }; @@ -112,7 +112,7 @@ public partial class Mod // Create an empty sub mod for single groups with None options. internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod(null! ) // Mod is irrelevant here, only used for saving. + => new SubMod( null! ) // Mod is irrelevant here, only used for saving. { Name = name, }; @@ -144,6 +144,16 @@ public partial class Mod // and the path must obviously be valid itself. public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) { + if( s == "." ) + { + return replacement; + } + + if( s == ".." ) + { + return replacement + replacement; + } + StringBuilder sb = new(s.Length); foreach( var c in s ) { From e0a171051d5d952f75334f784bf39dec0fb911a7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 18 Aug 2022 11:52:18 +0200 Subject: [PATCH 0430/2451] Cleanup --- Penumbra/Interop/Resolver/PathResolver.Material.cs | 4 ++-- Penumbra/Interop/Resolver/PathResolver.Meta.cs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 9688257c..1d92f96d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -122,7 +122,7 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { LoadMtrlHelper( mtrlResourceHandle ); - var ret = _loadMtrlTexHook!.Original( mtrlResourceHandle ); + var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; } @@ -134,7 +134,7 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { LoadMtrlHelper( mtrlResourceHandle ); - var ret = _loadMtrlShpkHook!.Original( mtrlResourceHandle ); + var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 73390514..171d3cfb 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -88,7 +88,6 @@ public unsafe partial class PathResolver } } - private delegate void UpdateModelDelegate( IntPtr drawObject ); [Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = nameof( UpdateModelsDetour ) )] From 4efdd6d83462aea64012e7358d0b35599c138582 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Aug 2022 16:05:52 +0200 Subject: [PATCH 0431/2451] Add improved WIP edit windows for materials and models --- OtterGui | 2 +- Penumbra.GameData/Files/IWritable.cs | 6 + Penumbra.GameData/Files/MdlFile.cs | 2 +- Penumbra.GameData/Files/MtrlFile.Write.cs | 32 +- Penumbra.GameData/Files/MtrlFile.cs | 260 +++++++- Penumbra.GameData/GameData.cs | 4 +- Penumbra.GameData/Structs/WeaponType.cs | 6 + Penumbra/Mods/Editor/Mod.Editor.Files.cs | 10 + .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 55 +- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 607 ++++++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 129 +--- 12 files changed, 969 insertions(+), 146 deletions(-) create mode 100644 Penumbra.GameData/Files/IWritable.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.FileEdit.cs diff --git a/OtterGui b/OtterGui index 09dcd012..88bf2218 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 09dcd012a3106862f20f045b9ff9e33d168047c4 +Subproject commit 88bf221852d4a1ac26f5ffbfb5e497220aef75c4 diff --git a/Penumbra.GameData/Files/IWritable.cs b/Penumbra.GameData/Files/IWritable.cs new file mode 100644 index 00000000..afad2e94 --- /dev/null +++ b/Penumbra.GameData/Files/IWritable.cs @@ -0,0 +1,6 @@ +namespace Penumbra.GameData.Files; + +public interface IWritable +{ + public byte[] Write(); +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index cf829a65..5073b80c 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -7,7 +7,7 @@ using Lumina.Extensions; namespace Penumbra.GameData.Files; -public partial class MdlFile +public partial class MdlFile : IWritable { public const uint NumVertices = 17; public const uint FileHeaderSize = 0x44; diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 910854b9..8a3df6f5 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Text; @@ -22,24 +23,38 @@ public partial class MtrlFile cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 ); } - foreach( var colorSet in UvColorSets.Concat( ColorSets ) ) + foreach( var set in UvSets ) { w.Write( cumulativeStringOffset ); - w.Write( colorSet.Index ); - cumulativeStringOffset += ( ushort )( colorSet.Name.Length + 1 ); + w.Write( set.Index ); + cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); + } + + foreach( var set in ColorSets ) + { + w.Write( cumulativeStringOffset ); + w.Write( set.Index ); + cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); } foreach( var text in Textures.Select( t => t.Path ) - .Concat( UvColorSets.Concat( ColorSets ).Select( c => c.Name ).Append( ShaderPackage.Name ) ) ) + .Concat( UvSets.Select( c => c.Name ) ) + .Concat( ColorSets.Select( c => c.Name ) ) + .Append( ShaderPackage.Name ) ) { w.Write( Encoding.UTF8.GetBytes( text ) ); w.Write( ( byte )'\0' ); } w.Write( AdditionalData ); - foreach( var color in ColorSetData ) + foreach( var row in ColorSets.Select( c => c.Rows ) ) { - w.Write( color ); + w.Write( row.AsBytes() ); + } + + foreach( var row in ColorDyeSets.Select( c => c.Rows ) ) + { + w.Write( row.AsBytes() ); } w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); @@ -85,11 +100,12 @@ public partial class MtrlFile w.BaseStream.Seek( 0, SeekOrigin.Begin ); w.Write( Version ); w.Write( fileSize ); - w.Write( ( ushort )( ColorSetData.Length * 2 ) ); + w.Write( ( ushort )( ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size + + ColorDyeSets.Length * ColorDyeSet.RowArray.NumRows * 2 ) ); w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); w.Write( shaderPackageNameOffset ); w.Write( ( byte )Textures.Length ); - w.Write( ( byte )UvColorSets.Length ); + w.Write( ( byte )UvSets.Length ); w.Write( ( byte )ColorSets.Length ); w.Write( ( byte )AdditionalData.Length ); } diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 5d6d81a0..76257c1f 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -1,19 +1,234 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Numerics; using System.Text; using Lumina.Data.Parsing; using Lumina.Extensions; namespace Penumbra.GameData.Files; -public partial class MtrlFile +public partial class MtrlFile : IWritable { - public struct ColorSet + public struct UvSet { public string Name; public ushort Index; } + public unsafe struct ColorSet + { + public struct Row + { + public const int Size = 32; + + private fixed ushort _data[16]; + + public Vector3 Diffuse + { + get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 )); + set + { + _data[ 0 ] = FromFloat( value.X ); + _data[ 1 ] = FromFloat( value.Y ); + _data[ 2 ] = FromFloat( value.Z ); + } + } + + public Vector3 Specular + { + get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 )); + set + { + _data[ 4 ] = FromFloat( value.X ); + _data[ 5 ] = FromFloat( value.Y ); + _data[ 6 ] = FromFloat( value.Z ); + } + } + + public Vector3 Emissive + { + get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 )); + set + { + _data[ 8 ] = FromFloat( value.X ); + _data[ 9 ] = FromFloat( value.Y ); + _data[ 10 ] = FromFloat( value.Z ); + } + } + + public Vector2 MaterialRepeat + { + get => new(ToFloat( 12 ), ToFloat( 15 )); + set + { + _data[ 12 ] = FromFloat( value.X ); + _data[ 15 ] = FromFloat( value.Y ); + } + } + + public Vector2 MaterialSkew + { + get => new(ToFloat( 13 ), ToFloat( 14 )); + set + { + _data[ 13 ] = FromFloat( value.X ); + _data[ 14 ] = FromFloat( value.Y ); + } + } + + public float SpecularStrength + { + get => ToFloat( 3 ); + set => _data[ 3 ] = FromFloat( value ); + } + + public float GlossStrength + { + get => ToFloat( 7 ); + set => _data[ 7 ] = FromFloat( value ); + } + + public ushort TileSet + { + get => (ushort) (ToFloat(11) * 64f); + set => _data[ 11 ] = FromFloat(value / 64f); + } + + private float ToFloat( int idx ) + => ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] ); + + private static ushort FromFloat( float x ) + => BitConverter.HalfToUInt16Bits( ( Half )x ); + } + + public struct RowArray : IEnumerable< Row > + { + public const int NumRows = 16; + private fixed byte _rowData[NumRows * Row.Size]; + + public ref Row this[ int i ] + { + get + { + fixed( byte* ptr = _rowData ) + { + return ref ( ( Row* )ptr )[ i ]; + } + } + } + + public IEnumerator< Row > GetEnumerator() + { + for( var i = 0; i < NumRows; ++i ) + { + yield return this[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan< byte > AsBytes() + { + fixed( byte* ptr = _rowData ) + { + return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size ); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + } + + public unsafe struct ColorDyeSet + { + public struct Row + { + private ushort _data; + + public ushort Template + { + get => ( ushort )( _data >> 5 ); + set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) ); + } + + public bool Diffuse + { + get => ( _data & 0x01 ) != 0; + set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE ); + } + + public bool Specular + { + get => ( _data & 0x02 ) != 0; + set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD ); + } + + public bool Emissive + { + get => ( _data & 0x04 ) != 0; + set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB ); + } + + public bool Gloss + { + get => ( _data & 0x08 ) != 0; + set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 ); + } + + public bool SpecularStrength + { + get => ( _data & 0x10 ) != 0; + set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF ); + } + } + + public struct RowArray : IEnumerable< Row > + { + public const int NumRows = 16; + private fixed ushort _rowData[NumRows]; + + public ref Row this[ int i ] + { + get + { + fixed( ushort* ptr = _rowData ) + { + return ref ( ( Row* )ptr )[ i ]; + } + } + } + + public IEnumerator< Row > GetEnumerator() + { + for( var i = 0; i < NumRows; ++i ) + { + yield return this[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan< byte > AsBytes() + { + fixed( ushort* ptr = _rowData ) + { + return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) ); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + } + public struct Texture { public string Path; @@ -37,12 +252,12 @@ public partial class MtrlFile } - public uint Version; + public readonly uint Version; public Texture[] Textures; - public ColorSet[] UvColorSets; + public UvSet[] UvSets; public ColorSet[] ColorSets; - public ushort[] ColorSetData; + public ColorDyeSet[] ColorDyeSets; public ShaderPackageData ShaderPackage; public byte[] AdditionalData; @@ -61,9 +276,9 @@ public partial class MtrlFile var colorSetCount = r.ReadByte(); var additionalDataSize = r.ReadByte(); - Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); - UvColorSets = ReadColorSetOffsets( r, uvSetCount, out var uvOffsets ); - ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); + UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets ); + ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); var strings = r.ReadBytes( stringTableSize ); for( var i = 0; i < textureCount; ++i ) @@ -73,7 +288,7 @@ public partial class MtrlFile for( var i = 0; i < uvSetCount; ++i ) { - UvColorSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); + UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); } for( var i = 0; i < colorSetCount; ++i ) @@ -81,10 +296,22 @@ public partial class MtrlFile ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); } + ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize + ? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray() + : Array.Empty< ColorDyeSet >(); + ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); AdditionalData = r.ReadBytes( additionalDataSize ); - ColorSetData = r.ReadStructuresAsArray< ushort >( dataSetSize / 2 ); + for( var i = 0; i < ColorSets.Length; ++i ) + { + ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); + } + + for( var i = 0; i < ColorDyeSets.Length; ++i ) + { + ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >(); + } var shaderValueListSize = r.ReadUInt16(); var shaderKeyCount = r.ReadUInt16(); @@ -111,6 +338,19 @@ public partial class MtrlFile return ret; } + private static UvSet[] ReadUvSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new UvSet[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Index = r.ReadUInt16(); + } + + return ret; + } + private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) { var ret = new ColorSet[count]; diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 55b53aec..421b0031 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -14,9 +14,9 @@ public static class GameData internal static ObjectIdentification? Identification; internal static readonly GamePathParser GamePathParser = new(); - public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + public static IObjectIdentifier GetIdentifier( DataManager dataManager ) { - Identification ??= new ObjectIdentification( dataManager, clientLanguage ); + Identification ??= new ObjectIdentification( dataManager, dataManager.Language ); return Identification; } diff --git a/Penumbra.GameData/Structs/WeaponType.cs b/Penumbra.GameData/Structs/WeaponType.cs index 17bb3dd5..ea310bd7 100644 --- a/Penumbra.GameData/Structs/WeaponType.cs +++ b/Penumbra.GameData/Structs/WeaponType.cs @@ -26,4 +26,10 @@ public readonly struct WeaponType : IEquatable< WeaponType > public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==( WeaponType lhs, WeaponType rhs ) + => lhs.Value == rhs.Value; + + public static bool operator !=( WeaponType lhs, WeaponType rhs ) + => lhs.Value != rhs.Value; } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 53c14729..ab610f5d 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -74,6 +74,8 @@ public partial class Mod public bool FileChanges { get; private set; } private List< FileRegistry > _availableFiles = null!; + private List< FileRegistry > _mtrlFiles = null!; + private List< FileRegistry > _mdlFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -82,6 +84,12 @@ public partial class Mod public IReadOnlySet< FullPath > MissingFiles => _missingFiles; + public IReadOnlyList< FileRegistry > MtrlFiles + => _mtrlFiles; + + public IReadOnlyList< FileRegistry > MdlFiles + => _mdlFiles; + // Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths() { @@ -121,6 +129,8 @@ public partial class Mod .OfType< FileRegistry >() ) .ToList(); _usedPaths.Clear(); + _mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList(); + _mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList(); FileChanges = false; foreach( var subMod in _mod.AllSubMods ) { diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index f9ac3708..5c1f2c7e 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -17,8 +17,9 @@ public partial class Mod public partial class Editor { private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled); - private readonly List< MaterialInfo > _modelFiles = new(); - public IReadOnlyList< MaterialInfo > ModelFiles + private readonly List< ModelMaterialInfo > _modelFiles = new(); + + public IReadOnlyList< ModelMaterialInfo > ModelFiles => _modelFiles; // Non-ASCII encoding can not be used. @@ -50,7 +51,9 @@ public partial class Mod public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown ) { if( !ValidString( toSuffix ) ) + { return; + } foreach( var info in _modelFiles ) { @@ -62,7 +65,7 @@ public partial class Mod && ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value ) && ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) ) { - info.SetMaterial( $"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i ); + info.SetMaterial( $"/mt_c{match.Groups[ "RaceCode" ].Value}b0001_{toSuffix}.mtrl", i ); } } } @@ -72,7 +75,7 @@ public partial class Mod public void ScanModels() { _modelFiles.Clear(); - foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) ) + foreach( var file in _mdlFiles.Where( f => f.File.Extension == ".mdl" ) ) { try { @@ -82,7 +85,7 @@ public partial class Mod .Select( p => p.Item2 ).ToArray(); if( materials.Length > 0 ) { - _modelFiles.Add( new MaterialInfo( file.File, mdlFile, materials ) ); + _modelFiles.Add( new ModelMaterialInfo( file.File, mdlFile, materials ) ); } } catch( Exception e ) @@ -93,22 +96,22 @@ public partial class Mod } // A class that collects information about skin materials in a model file and handle changes on them. - public class MaterialInfo + public class ModelMaterialInfo { - public readonly FullPath Path; - private readonly MdlFile _file; - private readonly string[] _currentMaterials; - private readonly IReadOnlyList _materialIndices; + public readonly FullPath Path; + public readonly MdlFile File; + private readonly string[] _currentMaterials; + private readonly IReadOnlyList< int > _materialIndices; public bool Changed { get; private set; } - public IReadOnlyList CurrentMaterials + public IReadOnlyList< string > CurrentMaterials => _currentMaterials; - private IEnumerable DefaultMaterials - => _materialIndices.Select( i => _file.Materials[i] ); + private IEnumerable< string > DefaultMaterials + => _materialIndices.Select( i => File.Materials[ i ] ); - public (string Current, string Default) this[int idx] - => (_currentMaterials[idx], _file.Materials[_materialIndices[idx]]); + public (string Current, string Default) this[ int idx ] + => ( _currentMaterials[ idx ], File.Materials[ _materialIndices[ idx ] ] ); public int Count => _materialIndices.Count; @@ -116,8 +119,8 @@ public partial class Mod // Set the skin material to a new value and flag changes appropriately. public void SetMaterial( string value, int materialIdx ) { - var mat = _file.Materials[_materialIndices[materialIdx]]; - _currentMaterials[materialIdx] = value; + var mat = File.Materials[ _materialIndices[ materialIdx ] ]; + _currentMaterials[ materialIdx ] = value; if( mat != value ) { Changed = true; @@ -138,12 +141,12 @@ public partial class Mod foreach( var (idx, i) in _materialIndices.WithIndex() ) { - _file.Materials[idx] = _currentMaterials[i]; + File.Materials[ idx ] = _currentMaterials[ i ]; } try { - File.WriteAllBytes( Path.FullName, _file.Write() ); + System.IO.File.WriteAllBytes( Path.FullName, File.Write() ); Changed = false; } catch( Exception e ) @@ -157,23 +160,25 @@ public partial class Mod public void Restore() { if( !Changed ) + { return; + } foreach( var (idx, i) in _materialIndices.WithIndex() ) { - _currentMaterials[i] = _file.Materials[idx]; + _currentMaterials[ i ] = File.Materials[ idx ]; } + Changed = false; } - public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList indices ) + public ModelMaterialInfo( FullPath path, MdlFile file, IReadOnlyList< int > indices ) { - Path = path; - _file = file; - _materialIndices = indices; + Path = path; + File = file; + _materialIndices = indices; _currentMaterials = DefaultMaterials.ToArray(); } } - } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a5d4e5c8..77c60420 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -70,7 +70,7 @@ public class Penumbra : IDalamudPlugin public Penumbra( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); - GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + GameData.GameData.GetIdentifier( Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs new file mode 100644 index 00000000..2be39b93 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -0,0 +1,607 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Logging; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Files; +using Penumbra.Mods; +using Functions = Penumbra.GameData.Util.Functions; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileEditor< MtrlFile > _materialTab; + private readonly FileEditor< MdlFile > _modelTab; + + private class FileEditor< T > where T : class, IWritable + { + private readonly string _tabName; + private readonly string _fileType; + private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; + private readonly Func< T, bool, bool > _drawEdit; + + private Mod.Editor.FileRegistry? _currentPath; + private T? _currentFile; + private bool _changed; + + private string _defaultPath = string.Empty; + private bool _inInput = false; + private T? _defaultFile; + + private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + + public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, + Func< T, bool, bool > drawEdit ) + { + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + } + + public void Draw() + { + _list = _getFiles(); + if( _list.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( _tabName ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + DrawFileSelectCombo(); + SaveButton(); + ImGui.SameLine(); + ResetButton(); + ImGui.SameLine(); + DefaultInput(); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + + DrawFilePanel(); + } + + private void DefaultInput() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + _inInput = ImGui.IsItemActive(); + if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + { + try + { + var file = Dalamud.GameData.GetFile( _defaultPath ); + if( file != null ) + { + _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + } + } + catch + { + _defaultFile = null; + } + } + } + + public void Reset() + { + _currentPath = null; + _currentFile = null; + _changed = false; + } + + private void DrawFileSelectCombo() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); + if( !combo ) + { + return; + } + + foreach( var file in _list ) + { + if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) + { + UpdateCurrentFile( file ); + } + } + } + + private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) + { + if( ReferenceEquals( _currentPath, path ) ) + { + return; + } + + _changed = false; + _currentPath = path; + try + { + var bytes = File.ReadAllBytes( _currentPath.File.FullName ); + _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" ); + _currentFile = null; + } + } + + private void SaveButton() + { + if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) + { + File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); + _changed = false; + } + } + + private void ResetButton() + { + if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed ) ) + { + var tmp = _currentPath; + _currentPath = null; + UpdateCurrentFile( tmp! ); + } + } + + private void DrawFilePanel() + { + using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); + if( !child ) + { + return; + } + + if( _currentPath != null ) + { + if( _currentFile == null ) + { + ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); + } + else + { + using var id = ImRaii.PushId( 0 ); + _changed |= _drawEdit( _currentFile, false ); + } + } + + if( !_inInput && _defaultPath.Length > 0 ) + { + if( _currentPath != null ) + { + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); + ImGui.Separator(); + } + + if( _defaultFile == null ) + { + ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file." ); + } + else + { + using var id = ImRaii.PushId( 1 ); + _drawEdit( _defaultFile, true ); + } + } + } + } + + private static bool DrawModelPanel( MdlFile file, bool disabled ) + { + var ret = false; + for( var i = 0; i < file.Materials.Length; ++i ) + { + using var id = ImRaii.PushId( i ); + var tmp = file.Materials[ i ]; + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) + && tmp.Length > 0 + && tmp != file.Materials[ i ] ) + { + file.Materials[ i ] = tmp; + ret = true; + } + } + + return disabled && ret; + } + + + private static bool DrawMaterialPanel( MtrlFile file, bool disabled ) + { + var ret = DrawMaterialTextureChange( file, disabled ); + + + ImGui.NewLine(); + ret |= DrawMaterialColorSetChange( file, disabled ); + + return disabled && ret; + } + + private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + { + using var id = ImRaii.PushId( "Textures" ); + var ret = false; + for( var i = 0; i < file.Textures.Length; ++i ) + { + using var _ = ImRaii.PushId( i ); + var tmp = file.Textures[ i ].Path; + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) + && tmp.Length > 0 + && tmp != file.Textures[ i ].Path ) + { + ret = true; + file.Textures[ i ].Path = tmp; + } + } + + return ret; + } + + private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + { + if( file.ColorSets.Length == 0 ) + { + return false; + } + + using var table = ImRaii.Table( "##ColorSets", 10, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); + if( !table ) + { + return false; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( string.Empty ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Row" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Diffuse" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Specular" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Emissive" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Gloss" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Tile" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Repeat" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Skew" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye" ); + + var ret = false; + for( var j = 0; j < file.ColorSets.Length; ++j ) + { + using var _ = ImRaii.PushId( j ); + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= DrawColorSetRow( file, j, i, disabled ); + ImGui.TableNextRow(); + } + } + + return ret; + } + + private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true ) ) + { + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed( byte* ptr = data ) + { + Functions.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + Functions.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + } + + var text = Convert.ToBase64String( data ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + } + + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true ) ) + { + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length != MtrlFile.ColorSet.Row.Size + 2 + || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + fixed( byte* ptr = data ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( file.ColorDyeSets.Length <= colorSetIdx ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + } + } + + return true; + } + catch + { + // ignored + } + } + + return false; + } + + private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + using var id = ImRaii.PushId( rowIdx ); + var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = file.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var floatSize = 70 * ImGuiHelpers.GlobalScale; + var intSize = 45 * ImGuiHelpers.GlobalScale; + ImGui.TableNextColumn(); + ColorSetCopyClipboardButton( row, dye ); + ImGui.SameLine(); + var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ImGui.SameLine(); + var tmpFloat = row.SpecularStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.SpecularStrength ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + tmpFloat = row.GlossStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.GlossStrength ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + int tmpInt = row.TileSet; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialRepeat.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.X ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + tmpFloat = row.MaterialRepeat.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.Y ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialSkew.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.X ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.SameLine(); + tmpFloat = row.MaterialSkew.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.Y ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + if( hasDye ) + { + tmpInt = dye.Template; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##DyeTemplate", ref tmpInt, 0, 0 ) + && tmpInt != dye.Template + && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + } + + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter ) + { + var ret = false; + var tmp = input; + if( ImGui.ColorEdit3( label, ref tmp, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + && tmp != input ) + { + setter( tmp ); + ret = true; + } + + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + + return ret; + } + + private void DrawMaterialReassignmentTab() + { + if( _editor!.ModelFiles.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( "Material Reassignment" ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + + ImGui.NewLine(); + using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) + { + using var id = ImRaii.PushId( idx ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) + { + info.Save(); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true ) ) + { + info.Restore(); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + var tmp = info.CurrentMaterials[ 0 ]; + if( ImGui.InputText( "##0", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, 0 ); + } + + for( var i = 1; i < info.Count; ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + tmp = info.CurrentMaterials[ i ]; + if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, i ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 0c7d8b9f..4e3693a4 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -10,6 +10,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.Mods; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -40,6 +41,8 @@ public partial class ModEditWindow : Window, IDisposable MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear(); + _modelTab.Reset(); + _materialTab.Reset(); } public void ChangeOption( ISubMod? subMod ) @@ -132,7 +135,9 @@ public partial class ModEditWindow : Window, IDisposable DrawSwapTab(); DrawMissingFilesTab(); DrawDuplicatesTab(); - DrawMaterialChangeTab(); + DrawMaterialReassignmentTab(); + _modelTab.Draw(); + _materialTab.Draw(); DrawTextureTab(); } @@ -223,118 +228,41 @@ public partial class ModEditWindow : Window, IDisposable } } - private void DrawMaterialChangeTab() + private void DrawMissingFilesTab() { - using var tab = ImRaii.TabItem( "Model Materials" ); - if( !tab ) + if( _editor!.MissingFiles.Count == 0 ) { return; } - if( _editor!.ModelFiles.Count == 0 ) - { - ImGui.NewLine(); - ImGui.TextUnformatted( "No .mdl files detected." ); - } - else - { - ImGui.NewLine(); - MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); - ImGui.NewLine(); - using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { - return; - } - - var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) - { - using var id = ImRaii.PushId( idx ); - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) - { - info.Save(); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true ) ) - { - info.Restore(); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - var tmp = info.CurrentMaterials[ 0 ]; - if( ImGui.InputText( "##0", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, 0 ); - } - - for( var i = 1; i < info.Count; ++i ) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - tmp = info.CurrentMaterials[ i ]; - if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, i ); - } - } - } - } - } - - private void DrawMissingFilesTab() - { using var tab = ImRaii.TabItem( "Missing Files" ); if( !tab ) { return; } - if( _editor!.MissingFiles.Count == 0 ) + ImGui.NewLine(); + if( ImGui.Button( "Remove Missing Files from Mod" ) ) { - ImGui.NewLine(); - ImGui.TextUnformatted( "No missing files detected." ); + _editor.RemoveMissingPaths(); } - else + + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) { - if( ImGui.Button( "Remove Missing Files from Mod" ) ) - { - _editor.RemoveMissingPaths(); - } + return; + } - using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); - if( !child ) - { - return; - } + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } - using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !table ) - { - return; - } - - foreach( var path in _editor.MissingFiles ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( path.FullName ); - } + foreach( var path in _editor.MissingFiles ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); } } @@ -575,7 +503,12 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) - { } + { + _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawMaterialPanel ); + _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawModelPanel ); + } public void Dispose() { From cfeb20a18efc41221ca5aaa2f3562c93536a8f4d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Aug 2022 16:06:07 +0200 Subject: [PATCH 0432/2451] Hook ChangeCustomize to include RSP changes. --- .../Interop/Resolver/PathResolver.Meta.cs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 171d3cfb..f041fbd7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -2,6 +2,7 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.Meta.Manipulations; @@ -26,7 +27,7 @@ namespace Penumbra.Interop.Resolver; // RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05" // RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" // they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, -// and RspSetupCharacter, which is hooked here. +// ChangeCustomize and RspSetupCharacter, which is hooked here. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. @@ -50,6 +51,7 @@ public unsafe partial class PathResolver _onModelLoadCompleteHook.Enable(); _setupVisorHook.Enable(); _rspSetupCharacterHook.Enable(); + _changeCustomize.Enable(); } public void Disable() @@ -59,6 +61,7 @@ public unsafe partial class PathResolver _onModelLoadCompleteHook.Disable(); _setupVisorHook.Disable(); _rspSetupCharacterHook.Disable(); + _changeCustomize.Disable(); } public void Dispose() @@ -68,6 +71,7 @@ public unsafe partial class PathResolver _onModelLoadCompleteHook.Dispose(); _setupVisorHook.Dispose(); _rspSetupCharacterHook.Dispose(); + _changeCustomize.Dispose(); } private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject ); @@ -154,8 +158,29 @@ public unsafe partial class PathResolver private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) { - using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); - _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); + if( _inChangeCustomize ) + { + using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); + _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); + } + else + { + _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); + } + } + + // ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. + private bool _inChangeCustomize = false; + private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment ); + + [Signature( "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86", DetourName = nameof( ChangeCustomizeDetour ) )] + private readonly Hook< ChangeCustomizeDelegate > _changeCustomize = null!; + + private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment ) + { + _inChangeCustomize = true; + using var rsp = MetaChanger.ChangeCmp( _parent, human ); + return _changeCustomize.Original( human, data, skipEquipment ); } } From 674dc03f46662760c5b81c37f9811ba8129c24d9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Aug 2022 17:49:14 +0200 Subject: [PATCH 0433/2451] Fix some stuff. --- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/Import/Dds/DdsFile.cs | 49 ++++++++------- Penumbra/Import/Dds/ImageParsing.cs | 21 +++++++ Penumbra/Import/Dds/PixelFormat.cs | 59 ++++++++++--------- Penumbra/Interop/CharacterUtility.cs | 2 +- .../Resolver/PathResolver.DrawObjectState.cs | 2 +- .../Resolver/PathResolver.Identification.cs | 14 +++-- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 20 +++++-- Penumbra/UI/Classes/ModFileSystemSelector.cs | 30 ++++++---- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 4 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 2 +- 11 files changed, 130 insertions(+), 75 deletions(-) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 6ace180e..0ffff1b5 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -135,7 +135,7 @@ public partial class ModCollection var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings.Add( mod.ModPath.Name, new ModSettings.SavedSettings( settings, mod ) ); + _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings( settings, mod ); } _settings.RemoveAt( idx ); diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs index c8ad8cab..612b8c00 100644 --- a/Penumbra/Import/Dds/DdsFile.cs +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -46,7 +46,9 @@ public class DdsFile ParseType.R8G8B8A8 => Header.Height * Header.Width * 4, ParseType.B8G8R8A8 => Header.Height * Header.Width * 4, - _ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ), + + ParseType.A16B16G16R16F => Header.Height * Header.Width * 8, + _ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ), }; if( Header.MipMapCount < level ) @@ -89,25 +91,26 @@ public class DdsFile 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(), + 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 ), + ParseType.A16B16G16R16F => ImageParsing.DecodeUncompressedA16B16G16R16F( MipMap( 0 ), Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), }; } @@ -126,6 +129,12 @@ public class DdsFile var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; var type = header.PixelFormat.ToParseType( dxt10 ); + if( type == ParseType.Unsupported ) + { + PluginLog.Error( "DDS format unsupported." ); + return false; + } + file = new DdsFile( type, header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), dxt10 ); return true; } diff --git a/Penumbra/Import/Dds/ImageParsing.cs b/Penumbra/Import/Dds/ImageParsing.cs index 5170448e..7e279f59 100644 --- a/Penumbra/Import/Dds/ImageParsing.cs +++ b/Penumbra/Import/Dds/ImageParsing.cs @@ -577,4 +577,25 @@ public static partial class ImageParsing return ret; } + + public static unsafe byte[] DecodeUncompressedA16B16G16R16F( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 8 ); + var ret = new byte[data.Length / 2]; + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var input = ( Half* )d; + var end = (Half*) (d + data.Length); + while( input != end ) + { + *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); + *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); + *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); + *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); + } + } + + return ret; + } } \ No newline at end of file diff --git a/Penumbra/Import/Dds/PixelFormat.cs b/Penumbra/Import/Dds/PixelFormat.cs index 6c99860e..457859f6 100644 --- a/Penumbra/Import/Dds/PixelFormat.cs +++ b/Penumbra/Import/Dds/PixelFormat.cs @@ -26,6 +26,8 @@ public enum ParseType B8G8R8, R8G8B8A8, B8G8R8A8, + + A16B16G16R16F, } [StructLayout( LayoutKind.Sequential )] @@ -54,23 +56,23 @@ public struct PixelFormat 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 ), + 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 ), + D3D_A16B16G16R16 = 113, } - public void Write( BinaryWriter bw ) { bw.Write( Size ); @@ -87,20 +89,21 @@ public struct PixelFormat { 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, + 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, + FourCCType.D3D_A16B16G16R16 => ParseType.A16B16G16R16F, + _ => ParseType.Unsupported, }; } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 9f41050b..fc1de17b 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -8,7 +8,7 @@ namespace Penumbra.Interop; public unsafe class CharacterUtility : IDisposable { // A static pointer to the CharacterUtility address. - [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] + [Signature( "48 8B 05 ?? ?? ?? ?? 83 B9", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; // Only required for migration anymore. diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index ae0fc049..c3c40238 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -173,7 +173,7 @@ public unsafe partial class PathResolver // so we always keep track of the current GameObject to be able to link it to the DrawObject. private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); - [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0", DetourName = nameof( EnableDrawDetour ) )] + [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0", DetourName = nameof( EnableDrawDetour ) )] private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 81d05950..de48833b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -1,10 +1,12 @@ using System; using System.Diagnostics.CodeAnalysis; using Dalamud.Logging; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -15,6 +17,9 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { + [Signature( "0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress )] + private static ushort* _inspectTitleId = null!; + // Obtain the name of the current player, if one exists. private static string? GetPlayerName() => Dalamud.Objects[ 0 ]?.Name.ToString(); @@ -34,17 +39,14 @@ public unsafe partial class PathResolver } var ui = ( AtkUnitBase* )addon; - if( ui->UldManager.NodeListCount < 60 ) + if( ui->UldManager.NodeListCount <= 60 ) { return null; } - var text = ( AtkTextNode* )ui->UldManager.NodeList[ 59 ]; - if( text == null || !text->AtkResNode.IsVisible ) - { - text = ( AtkTextNode* )ui->UldManager.NodeList[ 60 ]; - } + var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 59 : 60; + var text = ( AtkTextNode* )ui->UldManager.NodeList[ nodeId ]; return text != null ? text->NodeText.ToString() : null; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index b0c77058..371f3bc7 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -242,7 +242,14 @@ public partial class ModEditWindow if( data != null ) { - wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 ); + try + { + wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load raw image:\n{e}" ); + } } UpdateCenter(); @@ -356,7 +363,10 @@ 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 }; + size = size.X < wrap.Width + ? size with { Y = wrap.Height * size.X / wrap.Width } + : new Vector2( wrap.Width, wrap.Height ); + ImGui.Image( wrap.ImGuiHandle, size ); } else if( path.Length > 0 ) @@ -425,8 +435,10 @@ public partial class ModEditWindow return; } - var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 ); - var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); + var leftRightWidth = + new Vector2( + ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 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( child ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index eced30cb..be0d838b 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -200,9 +200,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void AddImportModButton( Vector2 size ) { var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ); + "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ); ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); - if (!button) + if( !button ) { return; } @@ -212,15 +212,16 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) => - { - if( s ) + _fileManager.OpenFileDialog( "Import Mod Pack", + "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) => { - _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), - AddNewMod ); - ImGui.OpenPopup( "Import Status" ); - } - }, 0, modPath ); + if( s ) + { + _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), + AddNewMod ); + ImGui.OpenPopup( "Import Status" ); + } + }, 0, modPath ); } // Draw the progress information for import. @@ -312,13 +313,20 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { ImGui.OpenPopup( "ExtendedHelp" ); } + ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp ); } // Helpers. private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) { - var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => l.Value ); + var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => + { + // Any mod handled here should not stay new. + Penumbra.ModManager.NewMods.Remove( l.Value ); + return l.Value; + } ); + if( inherit ) { Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 80dd0fad..8fc57460 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -126,8 +126,8 @@ public partial class ConfigWindow _currentPriority = null; } - ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with higher priority take precedence before Mods with lower priority.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have higher priority than Mod B." ); + ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B." ); } // Draw a button to remove the current settings and inherit them instead diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index f250035b..c920b366 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -88,7 +88,7 @@ public partial class ConfigWindow return; } - ImGui.TextWrapped( _mod.Description ); + ImGuiUtil.TextWrapped( _mod.Description ); } // A simple clipped list of changed items. From e66ca7c5805bf23a82b51649e7f5f75baabe4bc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Aug 2022 17:50:52 +0200 Subject: [PATCH 0434/2451] Further fixes. --- Penumbra.GameData/Files/MdlFile.cs | 6 +++--- Penumbra/Interop/CharacterUtility.cs | 6 +++--- Penumbra/Interop/Resolver/PathResolver.AnimationState.cs | 4 ++-- Penumbra/Penumbra.json | 2 +- base_repo.json | 2 +- repo.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index 5073b80c..e39f612c 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Reflection; using System.Text; +using Lumina.Data; using Lumina.Data.Parsing; using Lumina.Extensions; @@ -87,7 +88,7 @@ public partial class MdlFile : IWritable public MdlFile( byte[] data ) { using var stream = new MemoryStream( data ); - using var r = new BinaryReader( stream ); + using var r = new LuminaBinaryReader( stream ); var header = LoadModelFileHeader( r ); LodCount = header.LodCount; @@ -197,7 +198,7 @@ public partial class MdlFile : IWritable RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) ); } - private MdlStructs.ModelFileHeader LoadModelFileHeader( BinaryReader r ) + private MdlStructs.ModelFileHeader LoadModelFileHeader( LuminaBinaryReader r ) { var header = MdlStructs.ModelFileHeader.Read( r ); Version = header.Version; @@ -255,5 +256,4 @@ public partial class MdlFile : IWritable public unsafe uint StackSize => ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) ); - } \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index fc1de17b..b4632ba6 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -14,11 +14,11 @@ public unsafe class CharacterUtility : IDisposable // Only required for migration anymore. public delegate void LoadResources( Structs.CharacterUtility* address ); - [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] - public readonly LoadResources? LoadCharacterResourcesFunc; + [Signature( "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0" )] + public readonly LoadResources LoadCharacterResourcesFunc = null!; public void LoadCharacterResources() - => LoadCharacterResourcesFunc?.Invoke( Address ); + => LoadCharacterResourcesFunc.Invoke( Address ); public Structs.CharacterUtility* Address => *_characterUtilityAddress; diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index b24bb84c..ca282732 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -200,13 +200,13 @@ public unsafe partial class PathResolver _animationLoadCollection = last; } - [Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )] + [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )] private readonly Hook< CharacterBaseNoArgumentDelegate > _someOtherAvfxHook = null!; private void SomeOtherAvfxDetour( IntPtr unk ) { var last = _animationLoadCollection; - var gameObject = ( GameObject* )( unk - 0x8B0 ); + var gameObject = ( GameObject* )( unk - 0x8D0 ); _animationLoadCollection = IdentifyCollection( gameObject ); _someOtherAvfxHook.Original( unk ); _animationLoadCollection = last; diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index d17fe004..38d0c056 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -7,7 +7,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 6, + "DalamudApiLevel": 7, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/base_repo.json b/base_repo.json index 514e4e35..5016b6e4 100644 --- a/base_repo.json +++ b/base_repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "1.0.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 6, + "DalamudApiLevel": 7, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, diff --git a/repo.json b/repo.json index 7591dc2f..a4a8c36d 100644 --- a/repo.json +++ b/repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "0.5.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 6, + "DalamudApiLevel": 7, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 901b54805ab49a75cd887bbdee65e4427213fa8c Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 24 Aug 2022 16:23:52 +0000 Subject: [PATCH 0435/2451] [CI] Updating repo.json for refs/tags/0.5.6.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a4a8c36d..f45ed99b 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.5.0", - "TestingAssemblyVersion": "0.5.5.0", + "AssemblyVersion": "0.5.6.0", + "TestingAssemblyVersion": "0.5.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.5.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 62eb032765ad96db0b96088c253209b0d24397b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Aug 2022 22:39:19 +0200 Subject: [PATCH 0436/2451] Fix CharacterUtility. --- Penumbra/Interop/CharacterUtility.cs | 2 +- Penumbra/Interop/Structs/CharacterUtility.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index b4632ba6..0f6db067 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -33,7 +33,7 @@ public unsafe class CharacterUtility : IDisposable .Append( Structs.CharacterUtility.EqpIdx ) .Append( Structs.CharacterUtility.GmpIdx ) .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) - .Where( i => i != 17 ) ) // TODO: Female Hrothgar + .Where( i => i is not Structs.CharacterUtility.EqdpStartIdx + 15 or Structs.CharacterUtility.EqdpStartIdx + 15 + Structs.CharacterUtility.NumEqdpFiles / 2 ) ) // TODO: Female Hrothgar .Append( Structs.CharacterUtility.HumanCmpIdx ) .Concat( Enumerable.Range( Structs.CharacterUtility.FaceEstIdx, 4 ) ) .ToArray(); diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 32770fe6..02426db7 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -12,15 +12,15 @@ public unsafe struct CharacterUtility public static readonly int[] EqdpIndices = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).Where( i => i != EqdpStartIdx + 15 && i != EqdpStartIdx + 15 + NumEqdpFiles / 2 ).ToArray(); - public const int NumResources = 85; + public const int NumResources = 86; public const int EqpIdx = 0; - public const int GmpIdx = 1; - public const int HumanCmpIdx = 63; - public const int FaceEstIdx = 64; - public const int HairEstIdx = 65; - public const int HeadEstIdx = 66; - public const int BodyEstIdx = 67; - public const int EqdpStartIdx = 2; + public const int GmpIdx = 2; + public const int HumanCmpIdx = 64; + public const int FaceEstIdx = 65; + public const int HairEstIdx = 66; + public const int HeadEstIdx = 67; + public const int BodyEstIdx = 68; + public const int EqdpStartIdx = 3; public const int NumEqdpFiles = 2 * 28; public static int EqdpIdx( GenderRace raceCode, bool accessory ) From 5c8471fe3fca75b8d39b4f4c21f45b5d7ac0a709 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Aug 2022 22:39:33 +0200 Subject: [PATCH 0437/2451] Fix edits not applying changes. --- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 2be39b93..0f91ec56 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -221,7 +221,7 @@ public partial class ModEditWindow } } - return disabled && ret; + return !disabled && ret; } @@ -233,7 +233,7 @@ public partial class ModEditWindow ImGui.NewLine(); ret |= DrawMaterialColorSetChange( file, disabled ); - return disabled && ret; + return !disabled && ret; } private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) From 53818f35565ef49628e659acb0d5a9b5979cfa05 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Aug 2022 22:57:14 +0200 Subject: [PATCH 0438/2451] Fix against new client structs --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index de48833b..abd40704 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -11,6 +11,7 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; @@ -188,10 +189,10 @@ public unsafe partial class PathResolver var actualName = gameObject->ObjectIndex switch { 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - 244 => Penumbra.Config.UseCharacterCollectionsInCards ? GetPlayerName() : null, // portrait list and editor + 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. + 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on + 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview + 244 => Penumbra.Config.UseCharacterCollectionsInCards ? GetPlayerName() : null, // portrait list and editor >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), _ => null, } From f05529c7d274d919b942aff15dcae6c7d22cfc06 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 24 Aug 2022 21:00:04 +0000 Subject: [PATCH 0439/2451] [CI] Updating repo.json for refs/tags/0.5.6.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f45ed99b..988d6076 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.6.0", - "TestingAssemblyVersion": "0.5.6.0", + "AssemblyVersion": "0.5.6.1", + "TestingAssemblyVersion": "0.5.6.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f0b970c10285a7e9c6ab8cf281865f20bcc434e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Aug 2022 00:47:03 +0200 Subject: [PATCH 0440/2451] Rework some metastuff. --- Penumbra.GameData/Enums/EquipSlot.cs | 9 +- Penumbra/Import/Dds/TextureImporter.cs | 7 +- Penumbra/Interop/CharacterUtility.cs | 40 ++-- .../Interop/Resolver/PathResolver.Meta.cs | 2 +- Penumbra/Interop/Structs/CharacterUtility.cs | 180 ++++++++++++------ Penumbra/Meta/Files/CmpFile.cs | 7 +- Penumbra/Meta/Files/EqdpFile.cs | 6 +- Penumbra/Meta/Files/EqpGmpFile.cs | 16 +- Penumbra/Meta/Files/EstFile.cs | 19 +- Penumbra/Meta/Files/MetaBaseFile.cs | 7 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 7 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 16 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.cs | 3 +- .../Meta/Manipulations/EqdpManipulation.cs | 2 +- .../Meta/Manipulations/EqpManipulation.cs | 4 +- .../Meta/Manipulations/EstManipulation.cs | 12 +- .../Meta/Manipulations/GmpManipulation.cs | 4 +- .../Meta/Manipulations/ImcManipulation.cs | 5 +- .../Meta/Manipulations/MetaManipulation.cs | 3 +- .../Meta/Manipulations/RspManipulation.cs | 4 +- Penumbra/Penumbra.cs | 157 ++++++++------- Penumbra/UI/ConfigWindow.DebugTab.cs | 11 +- 25 files changed, 312 insertions(+), 221 deletions(-) diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index a0bab5ea..a68cacb9 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reflection.Metadata.Ecma335; -using Newtonsoft.Json; namespace Penumbra.GameData.Enums; @@ -31,7 +29,9 @@ public enum EquipSlot : byte FullBody = 19, BodyHands = 20, BodyLegsFeet = 21, - All = 22, // Not officially existing + ChestHands = 22, + Nothing = 23, + All = 24, // Not officially existing } public static class EquipSlotExtensions @@ -111,7 +111,8 @@ public static class EquipSlotExtensions EquipSlot.FullBody => EquipSlot.Body, EquipSlot.BodyHands => EquipSlot.Body, EquipSlot.BodyLegsFeet => EquipSlot.Body, - _ => throw new InvalidEnumArgumentException(), + EquipSlot.ChestHands => EquipSlot.Body, + _ => throw new InvalidEnumArgumentException($"{value} ({(int) value}) is not valid."), }; } diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs index 1c71f526..81162465 100644 --- a/Penumbra/Import/Dds/TextureImporter.cs +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -1,14 +1,12 @@ using System; using System.IO; using Lumina.Data.Files; -using OtterGui; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using Functions = Penumbra.GameData.Util.Functions; namespace Penumbra.Import.Dds; -public class TextureImporter +public static class TextureImporter { private static void WriteHeader( byte[] target, int width, int height ) { @@ -91,7 +89,4 @@ public class TextureImporter texData = buffer; return true; } - - public void Import( string inputFile ) - { } } \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 0f6db067..1175eb52 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -7,6 +7,8 @@ namespace Penumbra.Interop; public unsafe class CharacterUtility : IDisposable { + public record struct InternalIndex( int Value ); + // A static pointer to the CharacterUtility address. [Signature( "48 8B 05 ?? ?? ?? ?? 83 B9", ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; @@ -28,25 +30,22 @@ public unsafe class CharacterUtility : IDisposable // The relevant indices depend on which meta manipulations we allow for. // The defines are set in the project configuration. - public static readonly int[] RelevantIndices - = Array.Empty< int >() - .Append( Structs.CharacterUtility.EqpIdx ) - .Append( Structs.CharacterUtility.GmpIdx ) - .Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) - .Where( i => i is not Structs.CharacterUtility.EqdpStartIdx + 15 or Structs.CharacterUtility.EqdpStartIdx + 15 + Structs.CharacterUtility.NumEqdpFiles / 2 ) ) // TODO: Female Hrothgar - .Append( Structs.CharacterUtility.HumanCmpIdx ) - .Concat( Enumerable.Range( Structs.CharacterUtility.FaceEstIdx, 4 ) ) + public static readonly Structs.CharacterUtility.Index[] + RelevantIndices = Enum.GetValues< Structs.CharacterUtility.Index >(); + + public static readonly InternalIndex[] ReverseIndices + = Enumerable.Range( 0, Structs.CharacterUtility.TotalNumResources ) + .Select( i => new InternalIndex( Array.IndexOf( RelevantIndices, (Structs.CharacterUtility.Index) i ) ) ) .ToArray(); - public static readonly int[] ReverseIndices - = Enumerable.Range( 0, Structs.CharacterUtility.NumResources ) - .Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray(); + private readonly (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( Structs.CharacterUtility.Index idx ) + => _defaultResources[ ReverseIndices[ ( int )idx ].Value ]; - public (IntPtr Address, int Size) DefaultResource( int fullIdx ) - => DefaultResources[ ReverseIndices[ fullIdx ] ]; + public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) + => _defaultResources[ idx.Value ]; public CharacterUtility() { @@ -70,13 +69,13 @@ public unsafe class CharacterUtility : IDisposable for( var i = 0; i < RelevantIndices.Length; ++i ) { - if( DefaultResources[ i ].Size == 0 ) + if( _defaultResources[ i ].Size == 0 ) { - var resource = ( Structs.ResourceHandle* )Address->Resources[ RelevantIndices[ i ] ]; + var resource = Address->Resource( RelevantIndices[i] ); var data = resource->GetData(); if( data.Data != IntPtr.Zero && data.Length != 0 ) { - DefaultResources[ i ] = data; + _defaultResources[ i ] = data; } else { @@ -94,7 +93,7 @@ public unsafe class CharacterUtility : IDisposable } // Set the data of one of the stored resources to a given pointer and length. - public bool SetResource( int resourceIdx, IntPtr data, int length ) + public bool SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { if( !Ready ) { @@ -109,7 +108,7 @@ public unsafe class CharacterUtility : IDisposable } // Reset the data of one of the stored resources to its default values. - public void ResetResource( int resourceIdx ) + public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) { if( !Ready ) { @@ -117,8 +116,7 @@ public unsafe class CharacterUtility : IDisposable return; } - var relevantIdx = ReverseIndices[ resourceIdx ]; - var (data, length) = DefaultResources[ relevantIdx ]; + var (data, length) = DefaultResource( resourceIdx); var resource = Address->Resource( resourceIdx ); PluginLog.Verbose( "Reset resource {Idx} to default at 0x{DefaultData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); resource->SetData( data, length ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index f041fbd7..44fe74f2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -160,11 +160,11 @@ public unsafe partial class PathResolver { if( _inChangeCustomize ) { - using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); } else { + using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); } } diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 02426db7..40e346c1 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -8,87 +8,153 @@ namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] public unsafe struct CharacterUtility { - // TODO: female Hrothgar - public static readonly int[] EqdpIndices - = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).Where( i => i != EqdpStartIdx + 15 && i != EqdpStartIdx + 15 + NumEqdpFiles / 2 ).ToArray(); + public enum Index : int + { + Eqp = 0, + Gmp = 2, - public const int NumResources = 86; - public const int EqpIdx = 0; - public const int GmpIdx = 2; - public const int HumanCmpIdx = 64; - public const int FaceEstIdx = 65; - public const int HairEstIdx = 66; - public const int HeadEstIdx = 67; - public const int BodyEstIdx = 68; - public const int EqdpStartIdx = 3; - public const int NumEqdpFiles = 2 * 28; + Eqdp0101 = 3, + Eqdp0201, + Eqdp0301, + Eqdp0401, + Eqdp0501, + Eqdp0601, + Eqdp0701, + Eqdp0801, + Eqdp0901, + Eqdp1001, + Eqdp1101, + Eqdp1201, + Eqdp1301, + Eqdp1401, + Eqdp1501, - public static int EqdpIdx( GenderRace raceCode, bool accessory ) - => ( accessory ? NumEqdpFiles / 2 : 0 ) - + ( int )raceCode switch - { - 0101 => EqdpStartIdx, - 0201 => EqdpStartIdx + 1, - 0301 => EqdpStartIdx + 2, - 0401 => EqdpStartIdx + 3, - 0501 => EqdpStartIdx + 4, - 0601 => EqdpStartIdx + 5, - 0701 => EqdpStartIdx + 6, - 0801 => EqdpStartIdx + 7, - 0901 => EqdpStartIdx + 8, - 1001 => EqdpStartIdx + 9, - 1101 => EqdpStartIdx + 10, - 1201 => EqdpStartIdx + 11, - 1301 => EqdpStartIdx + 12, - 1401 => EqdpStartIdx + 13, - 1501 => EqdpStartIdx + 14, - 1601 => EqdpStartIdx + 15, // TODO: female Hrothgar - 1701 => EqdpStartIdx + 16, - 1801 => EqdpStartIdx + 17, - 0104 => EqdpStartIdx + 18, - 0204 => EqdpStartIdx + 19, - 0504 => EqdpStartIdx + 20, - 0604 => EqdpStartIdx + 21, - 0704 => EqdpStartIdx + 22, - 0804 => EqdpStartIdx + 23, - 1304 => EqdpStartIdx + 24, - 1404 => EqdpStartIdx + 25, - 9104 => EqdpStartIdx + 26, - 9204 => EqdpStartIdx + 27, - _ => -1, - }; + //Eqdp1601, // TODO: female Hrothgar + Eqdp1701 = Eqdp1501 + 2, + Eqdp1801, + Eqdp0104, + Eqdp0204, + Eqdp0504, + Eqdp0604, + Eqdp0704, + Eqdp0804, + Eqdp1304, + Eqdp1404, + Eqdp9104, + Eqdp9204, + + Eqdp0101Acc, + Eqdp0201Acc, + Eqdp0301Acc, + Eqdp0401Acc, + Eqdp0501Acc, + Eqdp0601Acc, + Eqdp0701Acc, + Eqdp0801Acc, + Eqdp0901Acc, + Eqdp1001Acc, + Eqdp1101Acc, + Eqdp1201Acc, + Eqdp1301Acc, + Eqdp1401Acc, + Eqdp1501Acc, + + //Eqdp1601Acc, // TODO: female Hrothgar + Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1801Acc, + Eqdp0104Acc, + Eqdp0204Acc, + Eqdp0504Acc, + Eqdp0604Acc, + Eqdp0704Acc, + Eqdp0804Acc, + Eqdp1304Acc, + Eqdp1404Acc, + Eqdp9104Acc, + Eqdp9204Acc, + + HumanCmp = 64, + FaceEst, + HairEst, + HeadEst, + BodyEst, + } + + public static readonly Index[] EqdpIndices = Enum.GetNames< Index >() + .Zip( Enum.GetValues< Index >() ) + .Where( n => n.First.StartsWith( "Eqdp" ) ) + .Select( n => n.Second ).ToArray(); + + public const int TotalNumResources = 87; + + public static Index EqdpIdx( GenderRace raceCode, bool accessory ) + => +( int )raceCode switch + { + 0101 => accessory ? Index.Eqdp0101Acc : Index.Eqdp0101, + 0201 => accessory ? Index.Eqdp0201Acc : Index.Eqdp0201, + 0301 => accessory ? Index.Eqdp0301Acc : Index.Eqdp0301, + 0401 => accessory ? Index.Eqdp0401Acc : Index.Eqdp0401, + 0501 => accessory ? Index.Eqdp0501Acc : Index.Eqdp0501, + 0601 => accessory ? Index.Eqdp0601Acc : Index.Eqdp0601, + 0701 => accessory ? Index.Eqdp0701Acc : Index.Eqdp0701, + 0801 => accessory ? Index.Eqdp0801Acc : Index.Eqdp0801, + 0901 => accessory ? Index.Eqdp0901Acc : Index.Eqdp0901, + 1001 => accessory ? Index.Eqdp1001Acc : Index.Eqdp1001, + 1101 => accessory ? Index.Eqdp1101Acc : Index.Eqdp1101, + 1201 => accessory ? Index.Eqdp1201Acc : Index.Eqdp1201, + 1301 => accessory ? Index.Eqdp1301Acc : Index.Eqdp1301, + 1401 => accessory ? Index.Eqdp1401Acc : Index.Eqdp1401, + 1501 => accessory ? Index.Eqdp1501Acc : Index.Eqdp1501, + //1601 => accessory ? RelevantIndex.Eqdp1601Acc : RelevantIndex.Eqdp1601, Female Hrothgar + 1701 => accessory ? Index.Eqdp1701Acc : Index.Eqdp1701, + 1801 => accessory ? Index.Eqdp1801Acc : Index.Eqdp1801, + 0104 => accessory ? Index.Eqdp0104Acc : Index.Eqdp0104, + 0204 => accessory ? Index.Eqdp0204Acc : Index.Eqdp0204, + 0504 => accessory ? Index.Eqdp0504Acc : Index.Eqdp0504, + 0604 => accessory ? Index.Eqdp0604Acc : Index.Eqdp0604, + 0704 => accessory ? Index.Eqdp0704Acc : Index.Eqdp0704, + 0804 => accessory ? Index.Eqdp0804Acc : Index.Eqdp0804, + 1304 => accessory ? Index.Eqdp1304Acc : Index.Eqdp1304, + 1404 => accessory ? Index.Eqdp1404Acc : Index.Eqdp1404, + 9104 => accessory ? Index.Eqdp9104Acc : Index.Eqdp9104, + 9204 => accessory ? Index.Eqdp9204Acc : Index.Eqdp9204, + _ => ( Index )( -1 ), + }; [FieldOffset( 0 )] public void* VTable; [FieldOffset( 8 )] - public fixed ulong Resources[NumResources]; + public fixed ulong Resources[TotalNumResources]; - [FieldOffset( 8 + EqpIdx * 8 )] + [FieldOffset( 8 + ( int )Index.Eqp * 8 )] public ResourceHandle* EqpResource; - [FieldOffset( 8 + GmpIdx * 8 )] + [FieldOffset( 8 + ( int )Index.Gmp * 8 )] public ResourceHandle* GmpResource; public ResourceHandle* Resource( int idx ) => ( ResourceHandle* )Resources[ idx ]; - public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) - => Resource( EqdpIdx( raceCode, accessory ) ); + public ResourceHandle* Resource( Index idx ) + => Resource( ( int )idx ); - [FieldOffset( 8 + HumanCmpIdx * 8 )] + public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) + => Resource( ( int )EqdpIdx( raceCode, accessory ) ); + + [FieldOffset( 8 + ( int )Index.HumanCmp * 8 )] public ResourceHandle* HumanCmpResource; - [FieldOffset( 8 + FaceEstIdx * 8 )] + [FieldOffset( 8 + ( int )Index.FaceEst * 8 )] public ResourceHandle* FaceEstResource; - [FieldOffset( 8 + HairEstIdx * 8 )] + [FieldOffset( 8 + ( int )Index.HairEst * 8 )] public ResourceHandle* HairEstResource; - [FieldOffset( 8 + BodyEstIdx * 8 )] + [FieldOffset( 8 + ( int )Index.BodyEst * 8 )] public ResourceHandle* BodyEstResource; - [FieldOffset( 8 + HeadEstIdx * 8 )] + [FieldOffset( 8 + ( int )Index.HeadEst * 8 )] public ResourceHandle* HeadEstResource; // not included resources have no known use case. diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 356d826d..0f308bd6 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -11,6 +11,9 @@ namespace Penumbra.Meta.Files; // We only support manipulating the racial scaling parameters at the moment. public sealed unsafe class CmpFile : MetaBaseFile { + public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = + Interop.CharacterUtility.ReverseIndices[ ( int )CharacterUtility.Index.HumanCmp ]; + private const int RacialScalingStart = 0x2A800; public float this[ SubRace subRace, RspAttribute attribute ] @@ -31,7 +34,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public CmpFile() - : base( CharacterUtility.HumanCmpIdx ) + : base( CharacterUtility.Index.HumanCmp ) { AllocateData( DefaultData.Length ); Reset(); @@ -39,7 +42,7 @@ public sealed unsafe class CmpFile : MetaBaseFile public static float GetDefault( SubRace subRace, RspAttribute attribute ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( CharacterUtility.HumanCmpIdx ).Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( InternalIndex ).Address; return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); } diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index b2504bbf..d9a22c41 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -114,8 +114,8 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public EqdpEntry GetDefault( int setIdx ) => GetDefault( Index, setIdx ); - public static EqdpEntry GetDefault( int fileIdx, int setIdx ) - => GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( fileIdx ).Address, setIdx ); + public static EqdpEntry GetDefault( Interop.CharacterUtility.InternalIndex idx, int setIdx ) + => GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( idx ).Address, setIdx ); public static EqdpEntry GetDefault( byte* data, int setIdx ) { @@ -139,5 +139,5 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) - => GetDefault( CharacterUtility.EqdpIdx( raceCode, accessory ), setIdx ); + => GetDefault( Interop.CharacterUtility.ReverseIndices[ ( int )CharacterUtility.EqdpIdx( raceCode, accessory ) ], setIdx ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 0a7fe90f..e6f48a3d 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -76,15 +76,15 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } public ExpandedEqpGmpBase( bool gmp ) - : base( gmp ? CharacterUtility.GmpIdx : CharacterUtility.EqpIdx ) + : base( gmp ? CharacterUtility.Index.Gmp : CharacterUtility.Index.Eqp ) { AllocateData( MaxSize ); Reset(); } - protected static ulong GetDefaultInternal( int fileIdx, int setIdx, ulong def ) + protected static ulong GetDefaultInternal( Interop.CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource(fileIndex).Address; if( setIdx == 0 ) { setIdx = 1; @@ -112,6 +112,9 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable { + public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = + Interop.CharacterUtility.ReverseIndices[ (int) CharacterUtility.Index.Eqp ]; + public ExpandedEqpFile() : base( false ) { } @@ -124,7 +127,7 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable public static EqpEntry GetDefault( int setIdx ) - => ( EqpEntry )GetDefaultInternal( CharacterUtility.EqpIdx, setIdx, ( ulong )Eqp.DefaultEntry ); + => ( EqpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )Eqp.DefaultEntry ); protected override unsafe void SetEmptyBlock( int idx ) { @@ -156,6 +159,9 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable { + public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = + Interop.CharacterUtility.ReverseIndices[( int )CharacterUtility.Index.Gmp]; + public ExpandedGmpFile() : base( true ) { } @@ -167,7 +173,7 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable } public static GmpEntry GetDefault( int setIdx ) - => ( GmpEntry )GetDefaultInternal( CharacterUtility.GmpIdx, setIdx, ( ulong )GmpEntry.Default ); + => ( GmpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )GmpEntry.Default ); public void Reset( IEnumerable< int > entries ) { diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 822286d0..7c6b3591 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -53,7 +54,7 @@ public sealed unsafe class EstFile : MetaBaseFile ResizeResources( Length + IncreaseSize ); } - var control = ( Info* )( Data + 4 ); + var control = ( Info* )( Data + 4 ); var entries = ( ushort* )( control + Count ); for( var i = Count - 1; i >= idx; --i ) @@ -95,7 +96,7 @@ public sealed unsafe class EstFile : MetaBaseFile for( var i = idx; i < Count - 1; ++i ) { - entries[i - 2] = entries[i + 1]; + entries[ i - 2 ] = entries[ i + 1 ]; } entries[ Count - 3 ] = 0; @@ -174,7 +175,7 @@ public sealed unsafe class EstFile : MetaBaseFile } public EstFile( EstManipulation.EstType estType ) - : base( ( int )estType ) + : base( ( CharacterUtility.Index )estType ) { var length = DefaultData.Length; AllocateData( length + IncreaseSize ); @@ -182,11 +183,11 @@ public sealed unsafe class EstFile : MetaBaseFile } public ushort GetDefault( GenderRace genderRace, ushort setId ) - => GetDefault( ( EstManipulation.EstType )Index, genderRace, setId ); + => GetDefault( Index, genderRace, setId ); - public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) + public static ushort GetDefault( Interop.CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId ) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( ( int )estType ).Address; + var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( index ).Address; var count = *( int* )data; var span = new ReadOnlySpan< Info >( data + 4, count ); var (idx, found) = FindEntry( span, genderRace, setId ); @@ -197,4 +198,10 @@ public sealed unsafe class EstFile : MetaBaseFile return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize ); } + + public static ushort GetDefault( CharacterUtility.Index index, GenderRace genderRace, ushort setId ) + => GetDefault( Interop.CharacterUtility.ReverseIndices[ ( int )index ], genderRace, setId ); + + public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) + => GetDefault( ( CharacterUtility.Index )estType, genderRace, setId ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 303499a5..8bc827f5 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Memory; using Penumbra.GameData.Util; +using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.Meta.Files; @@ -8,10 +9,10 @@ public unsafe class MetaBaseFile : IDisposable { public byte* Data { get; private set; } public int Length { get; private set; } - public int Index { get; } + public CharacterUtility.InternalIndex Index { get; } - public MetaBaseFile( int idx ) - => Index = idx; + public MetaBaseFile( Interop.Structs.CharacterUtility.Index idx ) + => Index = CharacterUtility.ReverseIndices[(int) idx]; protected (IntPtr Data, int Length) DefaultData => Penumbra.CharacterUtility.DefaultResource( Index ); diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index 2a916c5b..b624a360 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -13,10 +13,10 @@ public partial class MetaManager private readonly List< RspManipulation > _cmpManipulations = new(); public void SetCmpFiles() - => SetFile( _cmpFile, CharacterUtility.HumanCmpIdx ); + => SetFile( _cmpFile, CharacterUtility.Index.HumanCmp ); public static void ResetCmpFiles() - => SetFile( null, CharacterUtility.HumanCmpIdx ); + => SetFile( null, CharacterUtility.Index.HumanCmp ); public void ResetCmp() { diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index e89bd67e..dc9a31d6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -11,7 +11,7 @@ namespace Penumbra.Meta.Manager; public partial class MetaManager { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtility.EqdpIndices.Length]; // TODO: female Hrothgar private readonly List< EqdpManipulation > _eqdpManipulations = new(); @@ -33,9 +33,10 @@ public partial class MetaManager public void ResetEqdp() { - foreach( var file in _eqdpFiles ) + foreach( var file in _eqdpFiles.OfType() ) { - file?.Reset( _eqdpManipulations.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) ); + var relevant = Interop.CharacterUtility.RelevantIndices[ file.Index.Value ]; + file.Reset( _eqdpManipulations.Where( m => m.FileIndex() == relevant ).Select( m => ( int )m.SetId ) ); } _eqdpManipulations.Clear(); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 4c75615f..adae4378 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -13,10 +13,10 @@ public partial class MetaManager private readonly List< EqpManipulation > _eqpManipulations = new(); public void SetEqpFiles() - => SetFile( _eqpFile, CharacterUtility.EqpIdx ); + => SetFile( _eqpFile, CharacterUtility.Index.Eqp ); public static void ResetEqpFiles() - => SetFile( null, CharacterUtility.EqpIdx ); + => SetFile( null, CharacterUtility.Index.Eqp ); public void ResetEqp() { diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index 702b53b6..728a024d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -19,18 +19,18 @@ public partial class MetaManager public void SetEstFiles() { - SetFile( _estFaceFile, CharacterUtility.FaceEstIdx ); - SetFile( _estHairFile, CharacterUtility.HairEstIdx ); - SetFile( _estBodyFile, CharacterUtility.BodyEstIdx ); - SetFile( _estHeadFile, CharacterUtility.HeadEstIdx ); + SetFile( _estFaceFile, CharacterUtility.Index.FaceEst ); + SetFile( _estHairFile, CharacterUtility.Index.HairEst ); + SetFile( _estBodyFile, CharacterUtility.Index.BodyEst ); + SetFile( _estHeadFile, CharacterUtility.Index.HeadEst ); } public static void ResetEstFiles() { - SetFile( null, CharacterUtility.FaceEstIdx ); - SetFile( null, CharacterUtility.HairEstIdx ); - SetFile( null, CharacterUtility.BodyEstIdx ); - SetFile( null, CharacterUtility.HeadEstIdx ); + SetFile( null, CharacterUtility.Index.FaceEst ); + SetFile( null, CharacterUtility.Index.HairEst ); + SetFile( null, CharacterUtility.Index.BodyEst ); + SetFile( null, CharacterUtility.Index.HeadEst ); } public void ResetEst() diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 1aaba52f..df35cace 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -16,10 +16,10 @@ public partial class MetaManager private readonly List< GmpManipulation > _gmpManipulations = new(); public void SetGmpFiles() - => SetFile( _gmpFile, CharacterUtility.GmpIdx ); + => SetFile( _gmpFile, CharacterUtility.Index.Gmp ); public static void ResetGmpFiles() - => SetFile( null, CharacterUtility.GmpIdx ); + => SetFile( null, CharacterUtility.Index.Gmp ); public void ResetGmp() { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 7f83e98f..aa46e7f7 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Logging; using Penumbra.Collections; +using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -168,7 +169,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe void SetFile( MetaBaseFile? file, int index ) + private static unsafe void SetFile( MetaBaseFile? file, CharacterUtility.Index index ) { if( file == null ) { diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 287a5367..bd800a64 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -67,7 +67,7 @@ public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > return set != 0 ? set : Slot.CompareTo( other.Slot ); } - public int FileIndex() + public CharacterUtility.Index FileIndex() => CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); public bool Apply( ExpandedEqdpFile file ) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index c80696ac..dd2c8875 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -47,8 +47,8 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > return set != 0 ? set : Slot.CompareTo( other.Slot ); } - public int FileIndex() - => CharacterUtility.EqpIdx; + public CharacterUtility.Index FileIndex() + => CharacterUtility.Index.Eqp; public bool Apply( ExpandedEqpFile file ) { diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 314972b3..1abe3220 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -13,10 +13,10 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > { public enum EstType : byte { - Hair = CharacterUtility.HairEstIdx, - Face = CharacterUtility.FaceEstIdx, - Body = CharacterUtility.BodyEstIdx, - Head = CharacterUtility.HeadEstIdx, + Hair = CharacterUtility.Index.HairEst, + Face = CharacterUtility.Index.FaceEst, + Body = CharacterUtility.Index.BodyEst, + Head = CharacterUtility.Index.HeadEst, } public ushort Entry { get; init; } // SkeletonIdx. @@ -76,8 +76,8 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > return s != 0 ? s : SetId.CompareTo( other.SetId ); } - public int FileIndex() - => ( int )Slot; + public CharacterUtility.Index FileIndex() + => ( CharacterUtility.Index )Slot; public bool Apply( EstFile file ) { diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index ad31d6b2..f686ec31 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -33,8 +33,8 @@ public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > public int CompareTo( GmpManipulation other ) => SetId.CompareTo( other.SetId ); - public int FileIndex() - => CharacterUtility.GmpIdx; + public CharacterUtility.Index FileIndex() + => CharacterUtility.Index.Gmp; public bool Apply( ExpandedGmpFile file ) { diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index b3acf680..e9c01c0b 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; @@ -119,8 +120,8 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return b != 0 ? b : Variant.CompareTo( other.Variant ); } - public int FileIndex() - => -1; + public CharacterUtility.Index FileIndex() + => ( CharacterUtility.Index )( -1 ); public Utf8GamePath GamePath() { diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index bd0c132c..4380e0a0 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -3,12 +3,13 @@ using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; public interface IMetaManipulation { - public int FileIndex(); + public CharacterUtility.Index FileIndex(); } public interface IMetaManipulation< T > diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 1f10cf7b..70908324 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -45,8 +45,8 @@ public readonly struct RspManipulation : IMetaManipulation< RspManipulation > return s != 0 ? s : Attribute.CompareTo( other.Attribute ); } - public int FileIndex() - => CharacterUtility.HumanCmpIdx; + public CharacterUtility.Index FileIndex() + => CharacterUtility.Index.HumanCmp; public bool Apply( CmpFile file ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 77c60420..06c3799e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -69,75 +69,84 @@ public class Penumbra : IDalamudPlugin public Penumbra( DalamudPluginInterface pluginInterface ) { - Dalamud.Initialize( pluginInterface ); - GameData.GameData.GetIdentifier( Dalamud.GameData ); - DevPenumbraExists = CheckDevPluginPenumbra(); - IsNotInstalledPenumbra = CheckIsNotInstalled(); - - Framework = new FrameworkManager(); - CharacterUtility = new CharacterUtility(); - Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); - Config = Configuration.Load(); - - TempMods = new TempModManager(); - MetaFileManager = new MetaFileManager(); - ResourceLoader = new ResourceLoader( this ); - ResourceLoader.EnableHooks(); - ResourceLogger = new ResourceLogger( ResourceLoader ); - ResidentResources = new ResidentResourceManager(); - ModManager = new Mod.Manager( Config.ModDirectory ); - ModManager.DiscoverMods(); - CollectionManager = new ModCollection.Manager( ModManager ); - ModFileSystem = ModFileSystem.Load(); - ObjectReloader = new ObjectReloader(); - PathResolver = new PathResolver( ResourceLoader ); - - Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) + try { - HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", - } ); + Dalamud.Initialize( pluginInterface ); + GameData.GameData.GetIdentifier( Dalamud.GameData ); + DevPenumbraExists = CheckDevPluginPenumbra(); + IsNotInstalledPenumbra = CheckIsNotInstalled(); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); + Framework = new FrameworkManager(); + CharacterUtility = new CharacterUtility(); + Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); + Config = Configuration.Load(); - if( Config.EnableMods ) - { - ResourceLoader.EnableReplacements(); - PathResolver.Enable(); + TempMods = new TempModManager(); + MetaFileManager = new MetaFileManager(); + ResourceLoader = new ResourceLoader( this ); + ResourceLoader.EnableHooks(); + ResourceLogger = new ResourceLogger( ResourceLoader ); + ResidentResources = new ResidentResourceManager(); + ModManager = new Mod.Manager( Config.ModDirectory ); + ModManager.DiscoverMods(); + CollectionManager = new ModCollection.Manager( ModManager ); + ModFileSystem = ModFileSystem.Load(); + ObjectReloader = new ObjectReloader(); + PathResolver = new PathResolver( ResourceLoader ); + + Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) + { + HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", + } ); + + SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); + + if( Config.EnableMods ) + { + ResourceLoader.EnableReplacements(); + PathResolver.Enable(); + } + + if( Config.EnableHttpApi ) + { + CreateWebServer(); + } + + if( Config.DebugMode ) + { + ResourceLoader.EnableDebug(); + _configWindow.IsOpen = true; + } + + if( Config.EnableFullResourceLogging ) + { + ResourceLoader.EnableFullLogging(); + } + + if( CharacterUtility.Ready ) + { + ResidentResources.Reload(); + } + + Api = new PenumbraApi( this ); + Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); + SubscribeItemLinks(); + if( ImcExceptions > 0 ) + { + PluginLog.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); + } + else + { + PluginLog.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); + } + + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; } - - if( Config.EnableHttpApi ) + catch { - CreateWebServer(); + Dispose(); + throw; } - - if( Config.DebugMode ) - { - ResourceLoader.EnableDebug(); - _configWindow.IsOpen = true; - } - - if( Config.EnableFullResourceLogging ) - { - ResourceLoader.EnableFullLogging(); - } - - if( CharacterUtility.Ready ) - { - ResidentResources.Reload(); - } - - Api = new PenumbraApi( this ); - Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); - SubscribeItemLinks(); - if( ImcExceptions > 0 ) - { - PluginLog.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); - } - else - { - PluginLog.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); - } - Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) @@ -154,8 +163,8 @@ public class Penumbra : IDalamudPlugin { Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; - _launchButton.Dispose(); - _configWindow.Dispose(); + _launchButton?.Dispose(); + _configWindow?.Dispose(); } public bool Enable() @@ -249,18 +258,18 @@ public class Penumbra : IDalamudPlugin public void Dispose() { DisposeInterface(); - Ipc.Dispose(); - Api.Dispose(); - ObjectReloader.Dispose(); - ModFileSystem.Dispose(); - CollectionManager.Dispose(); + Ipc?.Dispose(); + Api?.Dispose(); + ObjectReloader?.Dispose(); + ModFileSystem?.Dispose(); + CollectionManager?.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); - PathResolver.Dispose(); - ResourceLogger.Dispose(); - ResourceLoader.Dispose(); - CharacterUtility.Dispose(); + PathResolver?.Dispose(); + ResourceLogger?.Dispose(); + ResourceLoader?.Dispose(); + CharacterUtility?.Dispose(); ShutdownWebServer(); } diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 081b0e79..764dc452 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -237,7 +237,8 @@ public partial class ConfigWindow for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) { var idx = CharacterUtility.RelevantIndices[ i ]; - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ]; + var intern = new CharacterUtility.InternalIndex( i ); + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resource(idx); ImGui.TableNextColumn(); ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); ImGui.TableNextColumn(); @@ -259,18 +260,18 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( $"{resource->GetData().Length}" ); ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResources[ i ].Address:X}" ); + ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResource(intern).Address:X}" ); if( ImGui.IsItemClicked() ) { ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResources[ i ].Address, - Penumbra.CharacterUtility.DefaultResources[ i ].Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResource(intern).Address, + Penumbra.CharacterUtility.DefaultResource(intern).Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); } ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResources[ i ].Size}" ); + ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResource(intern).Size}" ); } } From 9f3871eb6d61cc51f0543b4aa17fcc73c9c303a8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Aug 2022 23:38:59 +0200 Subject: [PATCH 0441/2451] Prepare Texture import rework with OtterTex. --- .gitmodules | 8 +- Penumbra/Import/Dds/DXT10Header.cs | 322 ---------- Penumbra/Import/Dds/DdsFile.cs | 236 ------- Penumbra/Import/Dds/DdsHeader.cs | 109 ---- Penumbra/Import/Dds/ImageParsing.cs | 601 ------------------ Penumbra/Import/Dds/PixelFormat.cs | 137 ---- Penumbra/Import/Dds/TextureImporter.cs | 35 +- Penumbra/Penumbra.csproj | 10 +- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 232 ++++++- Penumbra/lib/DirectXTexC.dll | Bin 0 -> 1417216 bytes Penumbra/lib/OtterTex.dll | Bin 0 -> 30720 bytes Penumbra/packages.lock.json | 84 +++ 12 files changed, 318 insertions(+), 1456 deletions(-) delete mode 100644 Penumbra/Import/Dds/DXT10Header.cs delete mode 100644 Penumbra/Import/Dds/DdsFile.cs delete mode 100644 Penumbra/Import/Dds/DdsHeader.cs delete mode 100644 Penumbra/Import/Dds/ImageParsing.cs delete mode 100644 Penumbra/Import/Dds/PixelFormat.cs create mode 100644 Penumbra/lib/DirectXTexC.dll create mode 100644 Penumbra/lib/OtterTex.dll create mode 100644 Penumbra/packages.lock.json diff --git a/.gitmodules b/.gitmodules index 685aaa2e..df7b5848 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ -[submodule "OtterGui"] - path = OtterGui - url = git@github.com:Ottermandias/OtterGui.git - branch = main +[submodule "OtterGui"] + path = OtterGui + url = git@github.com:Ottermandias/OtterGui.git + branch = main diff --git a/Penumbra/Import/Dds/DXT10Header.cs b/Penumbra/Import/Dds/DXT10Header.cs deleted file mode 100644 index 005cf24c..00000000 --- a/Penumbra/Import/Dds/DXT10Header.cs +++ /dev/null @@ -1,322 +0,0 @@ -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 deleted file mode 100644 index 612b8c00..00000000 --- a/Penumbra/Import/Dds/DdsFile.cs +++ /dev/null @@ -1,236 +0,0 @@ -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, - - ParseType.A16B16G16R16F => Header.Height * Header.Width * 8, - _ => 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 ), - ParseType.A16B16G16R16F => ImageParsing.DecodeUncompressedA16B16G16R16F( 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 ); - - if( type == ParseType.Unsupported ) - { - PluginLog.Error( "DDS format unsupported." ); - return false; - } - - 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.B8G8R8X8 ? 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.B8G8R8X8 && 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.B8G8R8X8, height * width * 4 ), - ParseType.BC5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.Greyscale => ( TexFile.TextureFormat.A8, height * width ), - ParseType.R4G4B4A4 => ( TexFile.TextureFormat.B4G4R4A4, height * width * 2 ), - ParseType.B4G4R4A4 => ( TexFile.TextureFormat.B4G4R4A4, height * width * 2 ), - ParseType.R5G5B5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.B5G5R5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.R5G6B5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.B5G6R5 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.R5G5B5A1 => ( TexFile.TextureFormat.B5G5R5A1, height * width * 2 ), - ParseType.B5G5R5A1 => ( TexFile.TextureFormat.B5G5R5A1, height * width * 2 ), - ParseType.R8G8B8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.B8G8R8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.R8G8B8A8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - ParseType.B8G8R8A8 => ( TexFile.TextureFormat.B8G8R8X8, height * width * 4 ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - } -} \ No newline at end of file diff --git a/Penumbra/Import/Dds/DdsHeader.cs b/Penumbra/Import/Dds/DdsHeader.cs deleted file mode 100644 index 30f976d4..00000000 --- a/Penumbra/Import/Dds/DdsHeader.cs +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 7e279f59..00000000 --- a/Penumbra/Import/Dds/ImageParsing.cs +++ /dev/null @@ -1,601 +0,0 @@ -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 >> 11 ), - G = ( byte )( ( c >> 5 ) & 0x3F ), - B = ( byte )( c & 0x1F ), - 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; - } - - public static unsafe byte[] DecodeUncompressedA16B16G16R16F( ReadOnlySpan< byte > data, int height, int width ) - { - Verify( data, height, width, 1, 8 ); - var ret = new byte[data.Length / 2]; - fixed( byte* r = ret, d = data ) - { - var ptr = r; - var input = ( Half* )d; - var end = (Half*) (d + data.Length); - while( input != end ) - { - *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); - *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); - *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); - *ptr++ = ( byte )( byte.MaxValue * (float) *input++ ); - } - } - - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/Import/Dds/PixelFormat.cs b/Penumbra/Import/Dds/PixelFormat.cs deleted file mode 100644 index 457859f6..00000000 --- a/Penumbra/Import/Dds/PixelFormat.cs +++ /dev/null @@ -1,137 +0,0 @@ -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, - - A16B16G16R16F, -} - -[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 ), - D3D_A16B16G16R16 = 113, - } - - - 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, - FourCCType.D3D_A16B16G16R16 => ParseType.A16B16G16R16F, - _ => 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 index 81162465..e029632a 100644 --- a/Penumbra/Import/Dds/TextureImporter.cs +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -28,39 +28,6 @@ public static class TextureImporter } } - 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 >(); @@ -72,6 +39,8 @@ public static class TextureImporter texData = new byte[80 + width * height * 4]; WriteHeader( texData, width, height ); rgba.CopyTo( texData.AsSpan( 80 ) ); + for( var i = 80; i < texData.Length; i += 4 ) + (texData[ i ], texData[i + 2]) = (texData[ i + 2], texData[i]); return true; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2689f37d..624558c1 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -15,6 +15,7 @@ true false false + true @@ -60,6 +61,9 @@ $(DalamudLibPath)Newtonsoft.Json.dll False + + lib\OtterTex.dll + @@ -74,8 +78,12 @@ + + PreserveNewest + DirectXTexC.dll + - Always + PreserveNewest diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 371f3bc7..2762179c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -12,14 +12,221 @@ using ImGuiScene; using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; +using OtterTex; using Penumbra.GameData.ByteString; using Penumbra.Import.Dds; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; namespace Penumbra.UI.Classes; +public struct Texture : IDisposable +{ + // Path to the file we tried to load. + public string Path = string.Empty; + + // If the load failed, an exception is stored. + public Exception? LoadError = null; + + // The pixels of the main image in RGBA order. + // Empty if LoadError != null or Path is empty. + public byte[] RGBAPixels = Array.Empty< byte >(); + + // The ImGui wrapper to load the image. + // null if LoadError != null or Path is empty. + public TextureWrap? TextureWrap = null; + + // The base image in whatever format it has. + public object? BaseImage = null; + + public Texture() + { } + + private void Clean() + { + RGBAPixels = Array.Empty< byte >(); + TextureWrap?.Dispose(); + TextureWrap = null; + ( BaseImage as IDisposable )?.Dispose(); + BaseImage = null; + } + + public void Dispose() + => Clean(); + + public bool Load( string path ) + { + if( path == Path ) + { + return false; + } + + Path = path; + Clean(); + return System.IO.Path.GetExtension( Path ) switch + { + ".dds" => LoadDds(), + ".png" => LoadPng(), + ".tex" => LoadTex(), + _ => true, + }; + } + + private bool LoadDds() + => true; + + private bool LoadPng() + => true; + + private bool LoadTex() + => true; + + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) + { + 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 ); + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Load( tmp ); + } + + ImGuiUtil.HoverTooltip( tooltip ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, + true ) ) + { + if( Penumbra.Config.DefaultModImportPath.Length > 0 ) + { + startPath = Penumbra.Config.DefaultModImportPath; + } + + var texture = this; + + void UpdatePath( bool success, List< string > paths ) + { + if( success && paths.Count > 0 ) + { + texture.Load( paths[ 0 ] ); + } + } + + manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + } + } + + public static Texture Combined( Texture left, Texture right, InputManipulations leftManips, InputManipulations rightManips ) + => new Texture(); +} + +public struct InputManipulations +{ + public InputManipulations() + { } + + public Matrix4x4 _multiplier = Matrix4x4.Identity; + public bool _invert = false; + public int _offsetX = 0; + public int _offsetY = 0; + public int _outputWidth = 0; + public int _outputHeight = 0; + + + private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) + { + 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 ); + 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 ); + transformed.W = Math.Clamp( transformed.W, 0, 1 ); + return transformed; + } + + 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(); + } + + + public bool DrawMatrixInput( float width ) + { + 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 " ); + changes |= DragFloat( "##RR", inputWidth, ref _multiplier.M11 ); + changes |= DragFloat( "##RG", inputWidth, ref _multiplier.M12 ); + changes |= DragFloat( "##RB", inputWidth, ref _multiplier.M13 ); + changes |= DragFloat( "##RA", inputWidth, ref _multiplier.M14 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "G " ); + changes |= DragFloat( "##GR", inputWidth, ref _multiplier.M21 ); + changes |= DragFloat( "##GG", inputWidth, ref _multiplier.M22 ); + changes |= DragFloat( "##GB", inputWidth, ref _multiplier.M23 ); + changes |= DragFloat( "##GA", inputWidth, ref _multiplier.M24 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "B " ); + changes |= DragFloat( "##BR", inputWidth, ref _multiplier.M31 ); + changes |= DragFloat( "##BG", inputWidth, ref _multiplier.M32 ); + changes |= DragFloat( "##BB", inputWidth, ref _multiplier.M33 ); + changes |= DragFloat( "##BA", inputWidth, ref _multiplier.M34 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "A " ); + changes |= DragFloat( "##AR", inputWidth, ref _multiplier.M41 ); + changes |= DragFloat( "##AG", inputWidth, ref _multiplier.M42 ); + changes |= DragFloat( "##AB", inputWidth, ref _multiplier.M43 ); + changes |= DragFloat( "##AA", inputWidth, ref _multiplier.M44 ); + + return changes; + } +} + public partial class ModEditWindow { private string _pathLeft = string.Empty; @@ -145,13 +352,13 @@ public partial class ModEditWindow { try { - using var stream = File.OpenRead( path ); - if( !DdsFile.Load( stream, out var f ) ) - { + if (!ScratchImage.LoadDDS( path, out var f )) return ( null, 0, 0 ); - } - return ( f.RgbaData.ToArray(), f.Header.Width, f.Header.Height ); + if(!f.GetRGBA( out f )) + return ( null, 0, 0 ); + + return ( f.Pixels[ ..(f.Meta.Width * f.Meta.Height * 4) ].ToArray(), f.Meta.Width, f.Meta.Height ); } catch( Exception e ) { @@ -170,9 +377,7 @@ public partial class ModEditWindow return ( null, 0, 0 ); } - var rgba = tex.Header.Format == TexFile.TextureFormat.B8G8R8A8 - ? ImageParsing.DecodeUncompressedR8G8B8A8( tex.ImageData, tex.Header.Height, tex.Header.Width ) - : tex.GetRgbaImageData(); + var rgba = tex.GetRgbaImageData(); return ( rgba, tex.Header.Width, tex.Header.Height ); } catch( Exception e ) @@ -402,10 +607,11 @@ public partial class ModEditWindow break; case 2: - if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) - { - File.WriteAllBytes( path, dds ); - } + //ScratchImage.LoadDDS( _imageCenter, ) + //if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) + //{ + // File.WriteAllBytes( path, dds ); + //} break; } @@ -509,4 +715,4 @@ public partial class ModEditWindow } } } -} \ No newline at end of file +} diff --git a/Penumbra/lib/DirectXTexC.dll b/Penumbra/lib/DirectXTexC.dll new file mode 100644 index 0000000000000000000000000000000000000000..5a919f902ccfb86f5e26155035aaa8f500c9a63e GIT binary patch literal 1417216 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

oXPdZL_($L$J6@& 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�QG(Qisn?yH~(VfS|IDXh@9nOl%KxY$@o#Q$0 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ξ%Ju4NOXh0o zxA!RJG4*48R+o=$k@bD#82Ob7zxCin9j;ojwtfu;W$JrT^VZg_t_WE3=2(_mOHhI# z0cebEFb({EprU$BWu4Hjt_)OQ1fjk=dwWGqy_25&o$&xIEaO#2ezPu@8E;gfz${;e z;0plf0De>CW&gr>TVNYu`l~|%??pHrXNSOBC4QH{9CtE)hrmk&-YzihoAKKOMlf}p zKpJq{FSci#qlYNE5XL@&pVM|{?4wrT29VLdo^;p;+u*@}5PP6)^JtUuu3KAQx1v%H z-)dLQudiNdsdn$w8}_)Y;X6b02eqpf3uIZQott#z>r6-ZO(>6f_J*{+4@vudMBp8O z*?zkOZkG7H0S1&0r&(+3(tF({h`%d1y>*UoQ)P(>}Wdwh_nj9+dKT z3cOe1w*zK-d{SW3UUv!XM;!UTD(MkS9q*wAD%Zr$2i4ZCtz8HHYirdO*ysc5mdS8W z<$46awCC>owH>r0oQLy}z*`Y!eRm1mAaJL^Wde5y%<&lO|A^E#f~lhq4Mgt$MSrFp zntH61g?g)Hz-`9erd|($H|uA|KS%42@HT zfT_P-DSh>!rBqZ(11SxpH1NS`pmUw9TL_;VO-ZE5=2dy>9(57$>q z;C#83%C%Om+vI9xX!e=|FMA{y@?O}~$4UW@NhN`}~ zLm&N31FV|6*sjLXuPLAR8@jxv zd|3h??~(=DjEcQ_KO|3sfsrjsOf!{Cv z)k=PD`PxOEjBjcAwhC+s{C>%oCHWV;nyi}hrrDO?-IC5;B5i}JfRQH1@q{Z^7N;#{^E~-R|Zb+ zPF=r;1kMsT(q05^koew*j?NyCd`KNepZQY0AthW&n3M+IV-2jFwsDs8bhH0p$K(8F z67Px^;l7fyA$GmNL@%%2MLgLX zBa5udtt&CF$KToNw^04+Z%{2%@0CP#!O)4Wk5v6T-xykiuNO>0{quLW`Ylwysrolo zKh*7grSfBmyzN*MRq#OkNx>l7EB5 z7a+{D_9*j1(m@^Oe)|7s@BL%!I?wygGj~YNa7amvLOTkpI@gqWD^Us~D-bDiD2_vsli|=}0wW@XiI$5s@ zs9qsk1O}Sj3W2fZ0%?}jx`~Q)<^6u%^M3Dr?wNaM?r+QXdSHt}8(;Uj($Km;E_p^XFE?g~xJ3rgP zh4a}kzy0Lb_jkB=!u8=j-&@)JqrI8Ge*fr1mj80bZxPJ>qn9pIpIDdGe1GOb*5~W% z`;R^|bM(m6k+}zs96TzC;k_rR=h*R?nWM9F$9B)|J92DBujV+*i5M|__L z@Art`@5j*HBh~f!m;SdZzIO@cdix({cw8{?Ka=5$+6VtahF{6>=?uRpnCtx~GJGoI z4`ujJhWBLH*YhVcEcCElMG^3Oo-XA4^}4@!!=F_3H7VcHn9lIq?W)IDGwaj1=y=|g zu}(d|beZ;Oc%*irhy446MgE5WeP#1kGxJCL4F`&&RL% zKdth2PJQI_OBp_u;lmj|l;N2S-=E>V85a1yYOAxir{Aakzps)#baohlVFZQ|STq9j zpDK#@SwJkjF8#MZ9Rz=H{`{xnrvoYA56<5veEU_cJrGV~c=sedhnq zm94i=_sL(^f3tL_bAo@rGW{F+2yc3eP7sjiDFOnTwj|yK2u`)&b~TFy#8#CrxfX9ifr%i{k(JI zbEjg#UD^E_qkHuGExZe%zxejPbGYyg3WMrTb)CKQsWxZHa}7_e#4Ivy2i2cm<28

y+6NizJK)ATs-6!-9R<&-e7XkUc~jE| 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

ZnD(H&S#^ml9#4Qu`{%TDAJd1vWt~_T< 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 M!0-(GYi8iz0U`>_od5s; literal 0 HcmV?d00001 diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll new file mode 100644 index 0000000000000000000000000000000000000000..86bddb838d6e0a11f5a3956f2a3bd4433685bb69 GIT binary patch literal 30720 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 DPwzYa literal 0 HcmV?d00001 diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json new file mode 100644 index 00000000..3b072b89 --- /dev/null +++ b/Penumbra/packages.lock.json @@ -0,0 +1,84 @@ +{ + "version": 1, + "dependencies": { + "net6.0-windows7.0": { + "EmbedIO": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "SharpCompress": { + "type": "Direct", + "requested": "[0.32.1, )", + "resolved": "0.32.1", + "contentHash": "9Cwj3lK/p7wkiBaQPCvaKINuHYuZ0ACDldA4M3o5ISSq7cjbbq3yqigTDBUoKjtyyXpqmQHUkw6fhLnjNF30ow==" + }, + "SixLabors.ImageSharp": { + "type": "Direct", + "requested": "[2.1.2, )", + "resolved": "2.1.2", + "contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==", + "dependencies": { + "System.ValueTuple": "4.5.0" + } + }, + "DirectXTex": { + "type": "Project" + }, + "directxtexc": { + "type": "Project", + "dependencies": { + "DirectXTex": "[1.0.0, )" + } + }, + "ottergui": { + "type": "Project" + }, + "ottertex": { + "type": "Project", + "dependencies": { + "DirectXTexC": "[1.0.0, )" + } + }, + "penumbra.gamedata": { + "type": "Project" + } + } + } +} \ No newline at end of file From 301d3b27360c9aae4ee44b518da23c711acbf485 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 30 Aug 2022 22:11:53 +0000 Subject: [PATCH 0442/2451] [CI] Updating repo.json for refs/tags/0.5.6.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 988d6076..11558334 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.6.1", - "TestingAssemblyVersion": "0.5.6.1", + "AssemblyVersion": "0.5.6.2", + "TestingAssemblyVersion": "0.5.6.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6b76337ec4a6ea75604bb4ee95f7f6f6c47f6e58 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 Aug 2022 14:36:21 +0200 Subject: [PATCH 0443/2451] Use custom initialization for native lib. --- Penumbra/Penumbra.cs | 3 +++ Penumbra/lib/DirectXTexC.dll | Bin 1417216 -> 585728 bytes Penumbra/lib/OtterTex.dll | Bin 30720 -> 31744 bytes 3 files changed, 3 insertions(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 06c3799e..0e2310f2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -141,6 +141,9 @@ public class Penumbra : IDalamudPlugin } Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + + OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); + PluginLog.Information( $"Loading native assembly from {OtterTex.NativeDll.Directory}." ); } catch { diff --git a/Penumbra/lib/DirectXTexC.dll b/Penumbra/lib/DirectXTexC.dll index 5a919f902ccfb86f5e26155035aaa8f500c9a63e..7a2d217a94bf5c1870c7bb81552beb7306102d9f 100644 GIT binary patch literal 585728 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

V0J){vCUT#D@PVyX4s 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 qcHp;GJJCU zS9_Ztx(5E^S1;j{+tIk4o~Vs{d8vN3#e{FmHYLf625 zXU41?SaJ;PgFebrJvl&fTY$kHmuERVCCBu8(8rHAA4}xGZmf)1VVxaH!OO2QW;$D( zjIio9M=wLU99WO)OY)K3T|f@F1ke-e<7Xs&(4dfz++9Ep$OC!?$ib6bDiz7Odarmx zJzzc3dvi!da>|z*uDfB-aYl0T&&Bw<8?L_ra=^17=Hx;grBwEX_1UY`QwNb#n3Hl5 zb;haWBJc3OL_YkX8!qaYe(uGyKJ|W@XxGKn zon^LlkMoHzRUNH*`qL9SUAvXP14-pq7k)=^?!}i}uWhpE6&afb_0g7Cv>EnSeqI2Y(4%ue{{?J=*14u{1G~iszk%~aqIKSAY6ahGgw{Ex?*q?>4<4J1 z=pHa1R=30OtHFz>a@MOgV|nnKrgL-?x&hoYgYl<@cLd-@I`k9Bw}Brxo9~~%_rEdX ze}6>ZC20m)Hf2KG4!s>(_+7wmXyM;8C_Vx${0bnx|E(eaJ4BaXz5wY1-={5H zZ7=xOfW8?d?7sWse-miPa?WirgFk|d@IK%rXyF6EtI)!Sf#DlChXj6(McHI%;mp;P z2ekjKqx<0@k+lguh;;Zk5RwD1^UCA9EF;3jC{bAb)e!ndxbenJc135f51Tgd+o zk-k6lA@;?$W1H|#fxXbey}(n@!hZ%1Lfab{kAX+f*#-W{8@a|rhZtFqwn(U zTF1{FWCM6F@G7+M-vh%5EBq&5GPH1NeZ%CBP%l!cVthKeX^cK>QxC`xb=%oduo(XXa^gd|e&AMAnA^MT=8NITel z3&Q`-gTDW8^>)e$8-(8u7-;DyDh@*yDR=TS@Q2PdndqmA42?I=p3~0 z13)#j@TY-1wD8kF19Ug|$2&OkPTlAQ?|(P>&_=m{j{_CZ!XxjdpMe&>7gz=@yeH=G z1-tJ_2%mQkZJRt4p8Q@WBe!t=8axxIhOP#m`#xx7@?iIEAmLp=1Nw!F?`Mtn>%(TZeT3N>Hg|zvjcI$p?2KIT0jl69D zKh8IM_CfpK-SNN4qi^;63}`||_!qzqXyM1^rfiSGfePembc60Z=f0w`R^#9}v?lgoi zybo9g?SFs6|2Bud*)ijalywoW@IqiWwD2{+BhaiW zwEqnX|N9jFw=47wi_`h;#*teyA+Cac30im#@G7+MQeb!|Hh|rCLHzG^_}}vIzw@E* zecZb$Wf}Ac_W(I);a;E`TDT9$Lks^GPzx)Eun8tR9#2; zQZB;!ZbSvN|NRJkSK>;(JFytPu)agF8ruIJg}zI18=$bl<8Po`poR5K4e|YNYxv*Y z@W08SZ*{B#ZX(}=Zvon%h2IWzK?{Ef*aa>82(TAgSl{G$3fliRhrZFV50Fd`c*adp zUESZ9vgPZz%an1gwk2f`1DcEOZ>1dHoU&)A57pp@fkV*NPW}Lr@$bRc{rEd>p$)z* z6XJc)`=N#Z$6LAg6k2%6?GfL(zh)jr~H$XRlKm0E4`-QIF!Ed_nrre?Z z??mYP5wF}!o5D8X>i1G-ZddOK-(=9Y8Q%4AzGFgI;ST~w zp@sELh~v=yw;}v*O6c1Xdw~_m2=4FbuS#|KAfy2PA1fY7%fp8hQPf{gHUz)EQ0UjR2j3%>$v zfEKpTMSS6Uejl>;Zu$i9Gl0rnzb$z+ruDm!HGuf~c)-8KboG-dYX?++{QJ!PyUzW4 z&~-oh{x8x`-bopOulo{h8(R2{fZfG!0l?dVGHBu706A#kS7W|CO`8C;hVt*v*S-2H zzQTCW#oQiz6Ho!&0G`xMe%?X70=v8Mg|9kD8%IX?UBEVI;d_Cd(7V9rd>y-?{X4k* z`@a3Vz;#FXx4((}JNUf=`1?Q)wD9kMUTEQw-y-eM!sCF++xe}6Ft7;PzX#jDpW45> zT6bD6IZRooR6{swRpbPxDb z$0*0Y!)CC%li9yZ*}rF5_borvM>~2KcLsqU1CB!re-%jY$b|Uk(B;s={{k$A7XB?z z2QB;>&;sq>5$xX|tb2s-&IGpN-K-744*@%%g`WZTKnwpK*at2A$C&Tmh3wy}toxOZ z3=eEC@e2P5I0!9VQ_kH_(85cBG&=peto^&Pb!Yb7+y`Dx+J!#>EP@vP8n6Od_<5iM zTKH$cPUv3n@R|<_@pj4r{6oHDvKQL_Hi`e;5`Dj9Jy3>>@Md5-wD3JZ z4YcqR^SGZDTKJa>B7Pru@&(*m1V0D9W(hL1{aWzDmj||=_UnJ=!T)xIz9I2@!01ne zORo&90$O+?Pz5bK3s?ayd?C;P?SK2j|CWfpF;aaM^295A<<-~*Eqnv80b2MSz+Pzo zy9oN8!d={>z7zX}-vc}jE&KrR47Bj$G2g#G-M@cb_pm<=?4te){}b>yv~Vx*47Bje zazoqexGx=0-`ETO;3{mOZ{G`^aV>o4YH%%ZgmG55QGDdvz`Zdoe0p8P?*YG)dyDtn zLmIt1j{Uogb*J$cfaAyre+|g&q>h1K1S+6~ciu!$w72_(?!~|GsD41HG~Vo8b$u16rVkm#yKBCTQWS0rCAihIM!G6-~4?>=C{Z z=z|t+2PVIVy(#crKsB^~kFI}LukP%K$->+a{@wNu8}FZ^d<8?%1Hevb;pc$|poNbCk3$Qm z?jmi_!lQxQ`}p=RxB^%VEj$%aSpVK&-6#Bh-~{#y?*oRvm%IgkAD9d+{A$ei?*`VL z!A}7zkr6%&G(ihL`gin)(87NL#P{#|_3r`J{lJd^`;Za-JkSj-Ty;17CbWMqsejk0 z?m%t5hyLpQ>|KEG04kt`-wRYh3x5n)0WJI)paI&yW3&&RyT?>`g(83RV41gAX6cB$u`0qYWdxrM!G1dL1 zKLn1!7yfs^?q@9n-u*Ch0BGTTfcX2tllP$q+P{C(znfEcbS`-uJ3q*r7JLoR1ueW0 z*ah7Np7mM!SoBnb|Ma=2-uic;>WZ zf`6HK;g^BG@(lbv)NSyzuTrMSRD-WS$UV={{ymod9h(0An*QCJx|8!5umPRIeZY2T z;XeVpp@qkMo%RJSJPUXVTKGJm7g~4)kp2*38MyrrbpW~x{0g9TnSC>`uYZd&{Rs6C zyye@_8<7EjA2^J>e-EYZto#Y^5`5t|e227uI1}QJ#RPhcCgaOqDNm!ZqRKlv&35S_i?#-BxH+6Ml1 zO!t6y{v19syTFehqu+ZlWBK1wb}wTaeE$wf-7T5>6LsbR+93EjgNnn@5zDB1WrKv_b~c* zH0rL#E6?VhCS-)`fevWluL9elyTSjyh&#qeqn%e|Yk`%}ZQw5fn#Xp7FFl{T?%>yg zzW{`M@YgOVvNrfV;2$iG=w9&Ni?|~SzHmc6qVr3O>`lvyZ1~4A)&QP=IdA--^We2B zx%Wr$f_GhAWDh_Ke|%Mu9YdxUeE#Yp+wpPa!8dSMkbMF@-km|ZJLu+S?x_4^Cd5Bm zM}9&Jr@5=}1hjuoq3$oN-Gt5P7j6Z1K?^?*?1dI?+8puyd(Qkj&~!K2b)7}l@-TZZ z;9mjTpoRYn*a_ta;l7wqn(6u#s@kxeFD z!Yx2GbQ^ftmq_OwlndD1$>-mr=ijlXyZ450PhRIkyq^V+0v>=C)}48eL;H8<=}x@| zpP`K53qK1ShxYFf)7@fU1zsgB!v6-8J(>yee?aG;h5r=u{rewvFXUUgsRN{4_(5P7 z^j`2M{*ksr`R)fF{AQ6ICZBu2OTWu~mGJ$$x^#!vj29_W>aDQu)|w6N-?62;w=Vu2 ze(;6A0PKPG?}qg6jnw^-eSO>whCRZ={y^HHg)4zHHu!hp>5ja|{+)QSQ}{1Ht?I^c z^7%i>Tjc#a7IpXH9R|C5*8Ka^bdTEHQQWIV zJ`3BJwC#o#elze0bQ^d&_vbF7&UAx+&3E0~u%{1f72LaoOc!`Q-#J!Y>IOem$!{t@ z1r2t0g9(qF&iAACad!!LE>HvQ-?8Q2zvbV}<=@-o-|3~hzShs=9(eQ!Zw9)dg*$*A zXyMy{UTEQW0DaKH_XCEV!Vd#w(832}eh>IR0qr4MRoXrP>_TQQcn|lObt;YEe+KqJ z_kycuarZa!!ppe}>v8xiz|RAH(8B-4oma~qLnrrmT?l9_SOI|&o=On z0KGTVePXYli+#wf2EPN4ZF|AL1A5{2fg8AE>KL@}w}J9c6EAoU_f0K^t_E)a6t)AL zn@_y(h3f$Eg=Z`vUieku4*@%&9{{hYA^p(8Hv_VB2l(NbegyntO!tDz&xvG&&y4BW z;JTO=ZjI>;;O7PC7k)M73zu=fmEsb<0g#`S3WQ27um}p_$=qijq zIyqWB2!AL|YX!m$xuNzy%Q<7#grOh+XOu$0v17KlXyYcLHB)iS&=$v7ZBZ~)no{DH zF=Ix@N8a7#fdL&eW^~M$(b3VdV=qIY;6G?@x%ZCa-n|F!hWGf6zw0lgH}Kts&IFjp z;8x#gw@gFl<~PeJV`F{e@!`JNG4u-0nT{8X$Muc%i)Re(uCIDyZlB{mFV;E-c=P%O zBY$0QGGnOSYr6Bv`_fy!el(dX?2bLR_1QLbclN(4kMTLSJBUjRwp^T)EN@g;bg)HSso}~sgxB(qZ=)e$ca9{>L1Q5Xn z6zm~~1C(%tGkie}>!@P`J7}SeBb?z10}OG40{58V0Si3h3BORII^9r%OtNT5BXVd( zK7|yKAVnGFq}g28IM*PF5|QkaJSmc6a!xc=SGqElrEKM>l?u`@jnaLZrFnWti?pmZ wX!Cm=s}6e&*)Q7_yK=_{hx_GKO86oWp@_srXa(P6wrN{)JK6@@YWP>>8>W^ynE(I) literal 1417216 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

oXPdZL_($L$J6@& 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�QG(Qisn?yH~(VfS|IDXh@9nOl%KxY$@o#Q$0 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ξ%Ju4NOXh0o zxA!RJG4*48R+o=$k@bD#82Ob7zxCin9j;ojwtfu;W$JrT^VZg_t_WE3=2(_mOHhI# z0cebEFb({EprU$BWu4Hjt_)OQ1fjk=dwWGqy_25&o$&xIEaO#2ezPu@8E;gfz${;e z;0plf0De>CW&gr>TVNYu`l~|%??pHrXNSOBC4QH{9CtE)hrmk&-YzihoAKKOMlf}p zKpJq{FSci#qlYNE5XL@&pVM|{?4wrT29VLdo^;p;+u*@}5PP6)^JtUuu3KAQx1v%H z-)dLQudiNdsdn$w8}_)Y;X6b02eqpf3uIZQott#z>r6-ZO(>6f_J*{+4@vudMBp8O z*?zkOZkG7H0S1&0r&(+3(tF({h`%d1y>*UoQ)P(>}Wdwh_nj9+dKT z3cOe1w*zK-d{SW3UUv!XM;!UTD(MkS9q*wAD%Zr$2i4ZCtz8HHYirdO*ysc5mdS8W z<$46awCC>owH>r0oQLy}z*`Y!eRm1mAaJL^Wde5y%<&lO|A^E#f~lhq4Mgt$MSrFp zntH61g?g)Hz-`9erd|($H|uA|KS%42@HT zfT_P-DSh>!rBqZ(11SxpH1NS`pmUw9TL_;VO-ZE5=2dy>9(57$>q z;C#83%C%Om+vI9xX!e=|FMA{y@?O}~$4UW@NhN`}~ zLm&N31FV|6*sjLXuPLAR8@jxv zd|3h??~(=DjEcQ_KO|3sfsrjsOf!{Cv z)k=PD`PxOEjBjcAwhC+s{C>%oCHWV;nyi}hrrDO?-IC5;B5i}JfRQH1@q{Z^7N;#{^E~-R|Zb+ zPF=r;1kMsT(q05^koew*j?NyCd`KNepZQY0AthW&n3M+IV-2jFwsDs8bhH0p$K(8F z67Px^;l7fyA$GmNL@%%2MLgLX zBa5udtt&CF$KToNw^04+Z%{2%@0CP#!O)4Wk5v6T-xykiuNO>0{quLW`Ylwysrolo zKh*7grSfBmyzN*MRq#OkNx>l7EB5 z7a+{D_9*j1(m@^Oe)|7s@BL%!I?wygGj~YNa7amvLOTkpI@gqWD^Us~D-bDiD2_vsli|=}0wW@XiI$5s@ zs9qsk1O}Sj3W2fZ0%?}jx`~Q)<^6u%^M3Dr?wNaM?r+QXdSHt}8(;Uj($Km;E_p^XFE?g~xJ3rgP zh4a}kzy0Lb_jkB=!u8=j-&@)JqrI8Ge*fr1mj80bZxPJ>qn9pIpIDdGe1GOb*5~W% z`;R^|bM(m6k+}zs96TzC;k_rR=h*R?nWM9F$9B)|J92DBujV+*i5M|__L z@Art`@5j*HBh~f!m;SdZzIO@cdix({cw8{?Ka=5$+6VtahF{6>=?uRpnCtx~GJGoI z4`ujJhWBLH*YhVcEcCElMG^3Oo-XA4^}4@!!=F_3H7VcHn9lIq?W)IDGwaj1=y=|g zu}(d|beZ;Oc%*irhy446MgE5WeP#1kGxJCL4F`&&RL% zKdth2PJQI_OBp_u;lmj|l;N2S-=E>V85a1yYOAxir{Aakzps)#baohlVFZQ|STq9j zpDK#@SwJkjF8#MZ9Rz=H{`{xnrvoYA56<5veEU_cJrGV~c=sedhnq zm94i=_sL(^f3tL_bAo@rGW{F+2yc3eP7sjiDFOnTwj|yK2u`)&b~TFy#8#CrxfX9ifr%i{k(JI zbEjg#UD^E_qkHuGExZe%zxejPbGYyg3WMrTb)CKQsWxZHa}7_e#4Ivy2i2cm<28

y+6NizJK)ATs-6!-9R<&-e7XkUc~jE| z9cmpFzn!A#P=9|AwggYTk3$cC&x1J!`>>0izwbgnWEtR%Mp?>;y{CW21#3f-@B6Zk zuniyZ@9&WV%m?HkY^&=PzJMn%P|q_OPxl{oL4$8xJJ$rg;8DLUL;q!2*#}(12gU(; zn3{j!{Tu&KH=q|jvxk2eLkIB%kN(NBvJbeH{li`$w!jVRV*J6Qe^`&0r{hPQXyWVqFHoF4D{C>sZ%x_+4LL1vwpf*<&xw*UTo6Pnt<_3g+bL>1KKbksexU}SaeqJ- zHOQv<9E2?-i7hbs1$ueDpIv_l8vgMju~7wYqO$gYv)z^60I~M_J56 z0}pm751c^*Ua}oAOx7q5y~I$qlT26)guMRu6x3nvb=e+epi`ESZ4@!Y2Yy-=@n&e0 z!`kSdRWS&OVKJ?SA}@shY7OAp(UXsJhR~z(kzdp)?5MfKa}D(HK0_YPIKT!v$YT!j zjDdM}O49xzPd?+2$Gqb_^j0y?BEQ6ta@2v|s)#rFC59sRRR3jtI8S1Yp z^j1aWm*MkQtQ)#1Pb|<|m6YXSkMdev)O`zGv?D~4HOlim$ewnR35x-rQRzSIkVYS& zL+Jfv|H2RYh53|`_b=m(9{MPQoWkFJnE$LZphJfD0?MFvLoe?GvYy4#ceRcChX`DtHcXfB3@QSycrt(0rtckJ;qi=ycrtv0s7>c zi81X#=A4KSa*i_02lR*&>m}@Po>dV8L!%tyh`ClpycrtnCa@R!)Vhh>qHglFeWahg zxMtQ)?qld-?Icg$yW}wkxrZ?iIf$M-);icGk9o&==&g!)lV4&;IqE=fRm7Y85<`)D zYF(A}v38Ot>mZNu=ep5bm6YowbW@&KptmY1%flY!fd{Z=PdkE~tWlozjy>%p6BYy3 zQ2G!3q|ry{5PH@9%RFZ<`$HM#8hhCX${@E`Gr3=wYm`B*$}-dkI%U)|bP9D9bjm=U zbrrvV###7f<_-A3h5vp7JdtPWbEhgNeOFGOBk6&Ukb@rh$A4P^n#g(G9{fe4jwMkdL5aRRNZ_BS4luhN7S-6rJV2jz()bPyNX=I31Cfrf@2<4T%N z#EJ6g?Zueiy@MU{pj-a#oy_CBg4$;*GEd}*`nwyX!5;2A6%4w;M@})8(3#Sj1pcZF zdBQD=`2Vv|aip&$gbaVTmpH=ii#bVuUx2NmQmlX_avJw{=q0wSZ`6U_s;FlIi_MbM zi%$P8fGh{RkgLKM{WpZwJka8775u`2&zHE=^>F_~o_d@NXMOsDd2*BO$^KXsi-o@( zj2?5!h#bK4m@Ef-*mtE;$V19=9rO?KtOwM^dcrYexdzS$&#H(wL!)h3hsfoB^(<=i zgSKQ{vJ7h%Vou!Qm#EL`_wkvJ`2|jB#=Q>x%sXJG+B58R%?C9{`S~VjWi^a7zB<>q z*V70&_y(EJ+3ToV+(Rktb*u-q_H*|-=lwVCbzp@19QIvGd!0DjgD>rM(7D$sgWjs7 zzSm_r?ooN~F_)-^`y4$Q_Y(6G^3=n9!uvmUs1v0(*`Dl=RY`rX%W|+M?{%3+|F{nN zC+~I2%YJenaSfagS*x&TY~1Uz4(@f*&|4Mp7Aw8PCHFD?pe@b=oif7tUeuTecznPJsHm2H>lNyeUo#C7*R)RxkLWH$sPJ@%pI;>#*n!~9@nm|SFJlPVhxA`)=J&X z9r9$ILR8Hi>Lic4|0#E%6YB+zu*-R@Nu&vuDueUS67Bg0xN{!XkXMX7q~AD?(sc5S zDSGvsig`GTGN-|Z9rof^vBW(YdNIaaoN>YDZ=6Gy(4qde znko}(6O^PzJ&E{Rh2b|iN(mA8hdH>X^EWu)1J1ev6ZJR3?ZvhHyW0Hy1z}VDH-v(% z^N|n0@}}k|ut$R*u$S&NKHW)ro<^L|AcHl*E^MLhm(h68g|&nKi7stDsjUs4uV5b5 zCa({=|I_t32z~$U>qCyn>%;ZGjUVvh`VfmxFi$0ZYK~}jQ;J`Ty8cD{Fi(zOpjb)= zVFwNMAGvE)#G7bXC&p;-fxe6NeD-hMYxt>ojBDz$Q$Ee(IL#l(s91|0e(?MvE>%pC z$0_Ri7dT*^jDrE=&t4z=W1Lg;DU~=I*Js?PDeC%+^}#3BI@AL+V2+kf`wd{mQ}so$ zeylB;9X!7~X>*~&=rdypJyC)WEad&cde8ee`1AqKBJlAk=V^Ax$M3~L99hWS z1-wLcZ*6HH{{OrpEL|-jmS9V?rH7z+i2oW`!YnbC&f+(W!&O?K;6)3WC`%LZ-djky z30{;otNA}rT`aE2n6C-Xg}RFjus3Z;k(>Hulo^C$w>1ZG1o*UljQV zeLJ*qq8LNp25lTK#<-s*YGXHTJXISbf03^V+W3{Y#`z*v8y^wZ&=;wV5vOwEl~>F6 z=HeRi?KQr*G@FTliNOCQ1n@U#v}{`a36BX2YwFxg)W5JWR7vzMF`YVvL?y}LeuMqF zCa+eX#5MMNqBgFn@#kt|G5oJ;qI{}t&wr_Z|5ad_&X@?82$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?%2$%?% z2$%?%2$%?%2>izggvW%1HFa*L&#x)29K(! zT|#UloUpQ%IDUl!B59 znH^JJ@!wOOg{mIWw&)09zejiBKgQaKj%Gvrv#MpX>Acb|bbr0&%O^tq!h?uBg6?5c*YEoS^MnRFlUSvmfh^<3t zH0B8P9uZNtklsPvBf~;!*=(5|GX!@Din0aU>e@<$+Dh0e_V(~7T|2X*YnPz#&cIyN z8__XD_(H$>h-?W9vmv@c0=QtYOkwU@5x3CjK3EBcNzsFbh{)*B?not(mK{U>Lp+Ig zQvP=j=^ha!+>lc}d7PXDtRihbZV$++fOiP#6cHts6RjbvXL>RP$?Ry`t(^#?Q2KXf zEltC_g-7%Xw>63paqAvpbL|o$_N#kTR76xV{f#U!mT*h7CDhX05@NAgTrFKhA8hF+ zuH7wB;=c&-s$j`%aTJuMLN>_K!4js*Mhl+J(oxLqVTly9AWI)HZe(d9wAd{ELZ*lK zFUZnaT`BPr#NXa7)p)FKciV;4++oG1@!oJN?Uc8D6Tc0-K;1L=Y5+2k& zgc79tM|6x4MJs?Tfmit^9|7|S3Jr@9Q7T%zqpiHH_(uy#anb9WEj&bIZ;S>yYaxw^ zzIcVo&26G*-3$q0@nkf$ToU;j5`_xYp-)nt>jg^eMcv*fwE%>A!hoZj*uAxEb;M9@ zj?QYXsAVWTrh5mm{jo8*iKx7mO_OKm*jVfoiR$Rwv7>OUM=h~4DTDt1(0XK*>UHrR z;=f4o`mUpR!%O>5lXU*LqXaIWe*@`qFX{d^OzZb({SB?pdq0U!$CB2`!mY(Y+&0K} z)v7FOtXf`CT2XoA%E}2^pQKld{ikXze#xsxvdY1mV-TX7qFzJ6(9`FmaICd}dOon( zC-vlX2om>5J#|R6h`k>wYCvnT54($D!ZTN+s0EH0KxVq8(|bvKKj_z$g_ zs0EEg-3Smn+FMAvYcg&^s-xJ+VINhecoifnOQ_i2-No)MZ|S94FE8p7s#1uc;Jve; z_7F8JO4Oh7LSrTIju$*h%|?*m^c1Ur_9~XjpGYo*o>v2J&P&Nsr>UMfQay6 zs(FrC{)+b<;&!9mtJ^C!lAd_&q-Dm0LB0!=#e?FNgx+*2UaR1J4%*7n{r>32im-MG+e{&wU zsBhr>+kefBuU9Y!>BFuzDiyyWli#MbopTSi+#%VLaqFb3?VfZE+!<1Q_qvWdkX zrrOi^tM9$H_Fa1A-H8H$q_52#_iI9(DqhPnIHudwX7Ulz4{VR|c{b;w&z!u=SH)jx z_muQwT`zrKZ|)KAiOub+p39Q{6zLQFess#W(8Yaz)T$C0>YX}G`WKBK+*?*#N73D9__<7>1 zKi2rHtKM&Jo-CKvlHS50ZPQ|kpXbo$9i#3)cDqk{wPXFROfKT@Jg0F`?|Az%?MWZo zJ8r+%?Z5mcpKdmMX^pJ0q_62^n~}HOL;qd(>Kt}@e*FgN*87JFRR2BDV|S(^$G*Ou zdj#nVJGbaKK4&)9B~xP?pQ<;8~^;L?|k-`vShvSn{7^o|p+m%ZuytLvmpi-)wD-}*;RC{e6q@ln=jw<&$I z7g+nn`+B6eJAR;0jSK#sEAO>FeRM~`qohB~n@h1=&h9azbeSx^)BhSw`ZCKk`>3bC zI!`-pfADVhfVrfHJ!tIckk7?4;X}E{6(+n+D)NFE>~r#FR9P?o&0kNt)wAKGZInOV zpLkbe(o|*{Xv% z4>l$JLrlH(es_=g&u?92%N0wNXwttcpC#AcOL6WK`;{4UVPxD%((6w?=}_;x)`2rF zo*%zGO`TSx4;Z90`QUroXWrUgP7e=NDMNbh$5A1-+f{Pi-o59-naytL^`!rT;q6B} zDDSm1;`Ou8G-tb0zTye5xAg+&1;*B1nI&J>L3;l8k8s)D_Pg_LgEB1`dw*hw1(ZKK zZB(7zfdicr%dPITyYIOG(vNQT$nfHUr?S1rkU=;5Z7)oE`ci2Ndt^M}Gh%r4z&W!POWoGg={45Tu}~h;uXLMn(AFzjnO8l} zyk_>%k)%HgpSTzKhZ@k48W@mx`F>d;m#E9iLIg`Z59&t+%8&XPFD#-~+1cUg z;?ogF^nKB7h-bo}n9S}^o^)H8IBB&n<^501&J_NYU0}lGyR)kl@YLgLDK)y-geAp1 zmoF>2_*#J*V<}(0XU1)%Mm==hdgZHlx7z8xAbrzi*ATA)&s~LkRG{x)`cSNs{3!>_n}eiB6Y5i zex#sp*_8`lIS+C=Hso!+LouZ9`fJFErk4sSgMa(==(SS4?vb8v)XGQIMs)Wd+^NF+ zo+p0^B7J<+-1)1AySwhHaIo&2?csAsUwk1?j;|XeIInS;xvO~BcY2+yd^xbdP8r>;=E#J<#{NQi zYbx^gsnFA;Lci+zbN{=IHY$l1dvDUxD;}w{433 z7p1+2)xUYy`>(x|NT1#_W>}w}hAGP)H1^MbF<>(3hZpwmbn=fOt{aPfZymd(&Oy?h zPTZ;U^zt*e;r_4p`&9Y%Ytk>3c(UZxqM>e!I_CP-^_!3{Nl%-K|Mk~ck1lDy&3jt) zGPTx!*S`wo8y31$x=79@Zn3`4-_99QI4kK-^EC44()6_J#;q?3HECQ>uMef)_|E;^ z;k@g@nM-%>Jbq^gW;m$9b$ca<;`IpAH$Qr*f;zE}6D2^xyTQ z+MKS%5^Is(x8w4vN9wHbh%c~bax?cm9;A0ZHEuwr44Zv7kA5`f>92+6lK$8AbhB=s zP47P_vB27*PhaVEqk!F4KZS3s<36|Ll0UN_d4?mY+WcFWYce?|&^2K~z+as^J?%`o zLn{6^j4RTx%9nXOW*+yi<=XkT?UWys``b*7OL=(BSo7zdbYI`&%=7o3*M*g zpWS=@@?vBKmFV3;qGkJ$C9J_{e=fQ1?xqj{Bwx)Q_CGju%Y$kok z)7O>a8@zYfY%6Q+IITu7=@qlLuh6jZEdSAIikI%R?MxHWm-Q)opw=>9ucc$k{aI$I zqh3EBzqI~zxX`jNHUZCAF)Xv(+SnmeZF%Z_ddjnm~Gpq!XSdXYUD>m(jY zAF#AXw=a$#FWaB=@~1Dybv}7e*?nbB{?;cw^mScN^YdeuwS%13W^9>#%7WhdeQL)1 zWqDjjPV`;&zS;}_Wq&-T9;aE8#?Cvx)qC}X9d`$>tfHS2a%Jmb&E{UhecGMd^ZM`1 zf1mPkZ6B@7ur;5@H1FpZ3a!Yf*X^6{-u%&iX^8WjVGTO<$$m<&4~K5PA7fX3m&eMj zUyrzYaKi`c>9p+UH;pGvQMS1~3ZE5ne+%i&Cf$kfiJa>>rr5qw6Cy5ol74B+siht# z-?%LuX-lwgx$p~RXp&wpFZ%Ok6=JMt&>-1}>4mX^;*4LL-`VmaVCt1}}a>uAAaZWl;c{X^D zrTqBE!H4gDThclH!HY*}YSq)9-*5jm#-n-mhaSUrw3>YMR^S=RFUo%0`)0E@KC^1p zebzhbW&zSSb?xUkXvJBdxVVRlHV$&o^D1BQx62)>m+_l;KTm-yKY8f)oe|&G-Wi*2 zmj8%#znt5!B33_d@84N9f4BY>+?Jl{xas#AqvNUn`J}ZAi^pB`jLk92x%!kLH%WI) z1?N4@Gu+6z@08DqUhhvls_=Lb<%?z*bgjdp)y{K={5t>8wsU&ik2RirXTa&Sfy;Uv zA5?#-R;^2~ycsYnW7*;*vwk;_^xETKGg5SM*&b(SLU}L8SuS?hL5KH==Zqp_=SozR%eC-J5q5eNVAiZ&? zbW6fUB`V{x%zN$|fA}`(ovS3ab`PK9KEJPLw?cMDtCRj)whz|*{I)tFI?<2{bk_mfQ|E8 zRt}rLFqC?R?aCc|vWcQBce8mEI8wL^>2to{-sWD-bIKI&z!6#Ibkxr$$IC}w>{_pm z@A~&OJc^z?osRO;w+$MY?cOD?p`l-_>b^IRBk2u)zP6)p-n9NZPQ1LGul>H}q}SY5 z&}DS~fnFb^s851P}cNnTx;c{0)LTy;K{iGo8K?+ADHLYH;)d4 z#F75Sx-(*q?SRLc78TAm9kO@@={ZYy#>P%*>%QJ0>IX}mr>{s~{L|{@SNEOsoU{A) z$P=wwgphu^PEe1(F86U6)Xclui`^acJa1UJag$wf#k?l(_1u&(Iy1#BX?j?{dEt}B zd3^g{e{SUUR*%=qnhkc`DB9k8@}(-vSGTD4kQ3}Sb@8^$y5={?WAuQ|>&{-5~#n z^wE#&4*$LE4d>y9Hnz!7db0kUZlL>9zW%B;F*kNBaZeb2G^k+lUr4qX=yqhh`!P-J z!v%{5E-!etO7DCJ^mEaY4o|xjc@e5?UE~#=d&W)u?+{dA8bSh`0YN4O0yd8+U0EbH5(LT^jD+w!?NO1)nixPGT^$7kN{7pFpBLVAP3Z9TGW zs^z)7x#ev7eh2cBo-vjBV35y8`NO}zQD#*r?musErgwv*w;f7_ewg&}&-|u7sy)mt z@wLm4qkW$*ONFl2y%`T4T|7N$Qs9z0Yl5a!pP=722S+dM+9dd`-`Ip(O9DF_4Wpin z{_*>U)cwt4@U})_Y++56Vo(!_S632WhBe?Z5U)t_L*> zcG1rX2D*M<-s4@q%xRy)t_fYg_bEBlza;gaS@7uel%Ad3mpUzV2>WrCUN^oRbw6QE zuOZ&+w|x3}_=TZN8 z`@-W3cD(L6xcr9gL5)}G&&6vdln(P6Q`l?AA8L$8IA1Kado^^3k=26;L4J+Z!z|G+teqH~qo^*!ZJ zMhqD{+RN8{b-LSW3Uu19=d*L^FRuS-yB;vzXXcX;0v&iyG)ZZuFuETjsh5FC3Hk{tL|Cx{b`e2E$8&%(W%zSq{u130>r6})Q!1}HC z;UfOKqUUC<(x!P?(rtD21%<8}?LYB!gY=_b)YLzpuVhPiziO600%vr8kgY`(Mb~4X zm!_TrZWEP!2j)7j*_&`?&%Dw4IW4sNgTi;dI_i_qywHFNzb)3!EuNbWX1cz&hR5*y z*Y>xrG_ek|17KCRT-IKjLXpN zRX0t#=Ahqy`-WC&HS2Co|79Mt`@Q-8+-v%?Ie&5I2N_>^%v#doUi}lduaW*oXNTXu z`YRxC)jaxol=s_x{OfFX%WqmquVbB-qtp(^z=02cDBW_@K)ud% zwKNWE;5N;9S*w+~o4r4!-;c(R-aEa{vXTMo)7!18QSn4&RDxsB?&$8U78*{dVBhU@tpw4nc?;Jl#$!!L)nsW-iqULVqC zA2G0UpJ#5%hB&P1`&GFKspu(3dc^XI#cYe_yG^!x`LuS!s%uIA(q+}7COvET%-Ztp zioLtm?-wQy_2-~fo64S^KJ0?ew$k(8^(Z$-zdt&! zI6Wng(*w_G<%34%>vc3e{TY5>`Wx|)sq?gH+k-vNX47$7vaMLZ_>5V-;xiRU@46>z zJIWjA?Mb)$ef_krV&-|ozUfubu5eMkF1>nTu|L0jsr%+YZ->zQivIV|xThC- zWpE95cgI09^!-@rx|748nFZbE_Q`zfv}I;r>Q5UpBdE3|E@1Ow|8p}dX81N0dKBr4 zE`)vU19m@&&FAONBn1^ol(!H-i_(&AkRM2Tvzu;pouYENEqyf8{ja(g zE2cuPMEaa*A`yAG(49%Y+^tWR%-PO+ z&j0DdRa+x@>fxmkav{LkxF zcbW7!(Q`}#=Ufi4J=T)mVtTzcKi#wVF5dZ}>W79QUZneHt+y@DmWj&zvp)s*xieRP zj{76fZ&lC_&s<}dZQu03{@O&!JIAhE(sIKpzo82rAK%br=`W%Sq2wzeK-sddAuFYD%>`!4(3gmHKOAYFM{ z^mLOl7roYgaV=Ng4{0lqUTJ)n{L^|&4;V9TbKz;T*6Zh}oc{S6tad5wHs@?&k%SVX zZIn;+N)=X{k-mu~-+|C{d5N$=3D)}V#g&w9kYx-q-znbPM;ulZGHuj${d^BwuB z?%-Ydt_6_(y4c&Y^V`~a#pSHK?MD6uvq<;9bFzo+!aCo?_J^CU{;mI0((S#sosXzl z&Tqu!){&Z6ufh18>4iMsj1CL(4jlV3N8ioCf9d;lRrAT2-5<^NSX^jImV<+iZJ_-3eY)q#vAG=+d0V>zsFY%RY5(?d~N=Up8h_OuwovoX5D& z?U>c>xS9C)$ka%u3?rr zely09ZkLMwv-jSuepzU`M ze;&EjveD)9UF&#mThe0PuaR*&J~a|r&pmXmvgi7_GjFW9GW`zy`7?O*m9l3Wd2i_3 z*4^55NO{taeK}>~<+7ok!%kO^xgXPi2k8TxOFk~uHY8xz{X!)+I;_;sr8(M#4#`qA zP2ij##y7rNIl4XNyX+cR;PBiZvTu#I>=M8oBM*pW*4e+?n>!?u{qk zWy}A2KL6#3o#zFoudC9vnN9iMy$>%u9lp#f-mAfqdAZx^ zd3b-?ma!dbrS%-Pe$*4+l5TpQ9QWI`rSa))9-~^E-?I2pn=;gs^+Wa0weG>rGxv53 ze=#x0f%JK0+GTRSJzUw|cWQRWd>4+8o*^nPuCUTd0Nx#V(y^uZaw@p^prWZ?V;52~eiT2q5`>&ba|`5-YjE>|&yd`w^MChaHXY~Q5%X`RUv(p3LDo}Ep0}KMl6Jq|`Q7F>?_DF1X>t_X*7!vm&o@S0TZhrpSn$DUfzYjki(z_3Ys zpH>+%q#o&CF5FhJ)!NpcbGPSzesGg(DbmA-^tbQ+{2SNA)w7oG3NN08^xQdS4XUuc ztn=n)OD{jTFj%_`*Yh%jix^ijY%Ch5KCmhU{)BV7*sie>S z>*mt0kB@PlyZPd>AFp-R_gCJ!jpyxkj8_J49lkUucdY(Al_%rG0xl&A`!6rNb;im} zU+d4UHzIDV89y+8z}jQgFF1QxjFDUe)^H+bxuTHu-$m z@TVt~*l{hx95c++>(#ul#-om9YUZ6-qjzX63a4Myx=MQWx@YRI4e)VU z6z3gqd}N|tuZBg(xCB;t?>V(X{*W^X2RcxGQk@qC8kA_GOxs(%`p!ag=aK$0{cMkx zX@2rrmZQtU-X~wSA$?-kL!GW(?clq(>yt}W_H8&r`X*bJA78#&>9XAByLwEn>-ygp zI?ug)eN@H}uc56jY-kv^w>ZwebKZYqNMcOnARh`g6gCm98$8YjyINQYX`iX2q|#QU91t!y6R(t)~C3(SHVSZ83wr8*%NiK zkZ){{-}kNfaqU~u8{Ut&mt$db=SkmN zr@^$tosPOE#*DbQWSpBF={1+WTtC4to9p73^_jchPOI0?{h5xHI=XMWvT;D?%{6kA z%|rQRVUUP={4_r2KdAnOD%VXVok^b=ZpGEJE ztLwkgarMf*79YJ1>|ETtue06n{v#R%H+!6E=TDSxRd`Qy<}wM&u$zY$9M0Fvh4iuy z4zBgGf8shYz4tfAj?Z63`d_x}?7Zv7hsAlawNFF(gBw??7q4-{Wl;A7K3KmPi- zHcMuFtK)op=$4}&4j%NM(x+tI)6U=bqkN9O3;h@LSrZsnWBdMf3$N*QU}*b1!(WeZ z@L9EXc)OaH^K_#8E+x~R&TSX?P1^8mO}Xq1tC4M%Y@{#jQ)pnRYfao@Cm!+`{A#Cu zpL%jS%aNbzrwtrb>fqTZ-#|yo-;H^mW37KXuj%pjbNl@eH;42*Wh+&``tl29T$>Na zdL}&0PWt6z^W3h6<@Fl#ti;~a_gd-ob5wN3zS%s6xGl{zZPxS(Wp#P~F&&$YJmKcH zXywKQ7sAJ_r=B<73E|%^i1gf`ly+$EwAq>TKI7wKryZW+nNTH7`nmmPrz3sYfRl?i zuldSnV)Y>%7JoD4IO*HIe}2_<(k+j9WuK;>`+mS9($607oj<$hOV`+(Q$I8d%d6+@ zM5mL5yI)@wFl2p}aqUm9=}Y;B`*Yr_+xwW;yt9E7JH)SSMtbc$fg|sK73({uxoi4- z`M0(qy-QKA)s9sbc}#C@cR2r3zfq(wPOLE~_k16>bURlh#XTb5fg%5&uW-w&<%{qAVWXPL9Q$p6RQcYsBabnVs*FueQ*!-P1EOKX6mfqA5*w51wfv>}yf4&zw=stKzk)!^&>$ zZUhMZD8?7d#<#GK=;(g%uhHjYuWvcZ%0J`#q~y<+(>yn{|HCwDP@~3-|LQoVUcRic z*Y>sj|1wE?)ROUISFLC}D&$T;{-5=ZbQylK%RlkfScJ1z$b=o3p>cKt2JncQd zT2b0&BgL{S14piUSV0&kQMO)#509&;Sb1||$BSkSMzQkmeyM)Ktl2idjn|3>?|6Rh z8{<>fcAr0^_?i2PB$KAghUK1NyvvQ6ZJ#c8_F3Jj_Tmp;LJu+i_bEZU>V8}4J0pG2 z`3;qZ3+-+FB7Sv1-4NgHRuBBPKB-xe*{_TX%Wz$j<+dW@5BG5&DkL%f=NX6MhFm@6 zo!0ARm1*v6QW+n&WPXD|Q?I$s_wGJtP^-1K7=PrQvrR9*TVDAdwLJ$uSwDjDi@OC| z*qLN2(pH`7w06O^gN!#n)IuV$jR@G3b2j}`en2tfe|-?J^~k+*9;sD&S;d5`5c+Zc zoto><4>;nNUVT`sLHe|Q%-&*t&BZG+Hn`2{yTKc`MTL19)#qib^SjZ$c{^hVS?28& z)(82W7dXM>@TNfAa$WCMS6Ry9KfBTZ|dhq#%jOby%K4ARYPvfe09_He` z|6F7nw}|aK7=K*3am}2TRRiWO{k(Qp)SNnu?=#xSBC)|H|GbS?qHj%TAo#g?-9ziB z?!N?Q7gP>bZ`uzGsmBzR%XHBZ0pPG7_LlfOB}JT)BVwzk@n^ZpkfrZ8U8 zeR}P{@dpEzr&d38<&V9>e!Y3znjT|XD?H~8el_VANn}W8-U30(HZfpN)tgye-srUHa)PcPfYubevR@pV8Gb`tQ&ita+19y7n zJhKREb7tHd#w*V+`L_I(i~E}GWb2=s&IoZxx%}YM+HZBeW)44*Z(*K4k=eJ2{e42o zLQ9`Ki&rl8785QreqrM9t#+*}f)+;|8T)9}hzX20d*ao2!?{l0)85}&=(xI_3FC{O zwHj6V@I~Kw=fm17#@hKX{`&kK2Fb%N1sw8owclgGFye;F7thsW{dtHx~ z%bI<@BG@;sx@$SPy3cFN$dUUev?s#nAbXr#t9@{q>^--|?L_-L=eDf;rf=)Lkf|C6 z@9ZQ!+j4l45NG>iw}yu{SoQ9c}j!ZD|c>OAYyMU!VCLI<8dzx9>0q8-R5=g>_qw0OCt8d zIMMR6f{4Aau4>tvir5S1KU((YBKE@gSul9!@83FI_1<6ac~ze+6@>BAblb^yvj*LE z->`A@l2-L?p0aX6)*p9iu&zzuCaV^1C!fFE$9S!Jt1y1@8iU8NKcDy7->mhkl*jIs z7;jOh+NvSd+yV~_T0Ti{r;FkZ{vT7Vn;kX;) zwaORcJBZkK6yZCG@WQ&6IIC{Gk6-LOcUA5->e04tMa+Kb;5$Te#NU0Fw9u;GJe5xzd-r#=7NM`lyxm6!V9ZA5tCd_b$5cFex(&7bFfY+B;>vu{J={T)9D*I)Lp+g18DxKqG9>BBec3(pAs z6&6x?k5!!#-}F{5C;Qv>>B7oM*tg(&kwt&E{hpG<+Rnn-#V{4eSHpm~bbb+^nGPh1RQ4j*CV6gRQFdHQ0eTV5Oc zmmQ09h4b(kXL7ojys-A1T5C2@L14j$-ui*S+%7+c-zOC<6fTGF6{eR@#DUXKdSFLsqebi#tA1P zSUD|g&RbmEYp(ZZ7n`Kj^MZyk-l)a5xV4gFep}|gtJP*q)K)xd055wa5cBCmWelnc{)eLzPg>s$%l%ne$#eHdl@!ZFI<-e z8RbNop4=R`%GmMytX}%5%%7`GUtRq?B0(`N(rw`%U-X6Zr7(XIxnT65yt0LGJTc88zoWq75G^ls^8yG%IW7W+KZ`edh}t=gP* zOX(kT)@A1a;XFa?bJA#$&nsg@_yHpPKoMT-bJ#c$`;WmTzg2U-?zte+X~UrHO>eU} z{C9nBn<7&0L=iqigr6(I?|)n^)8pfW;0=9p^G@!TykY*&7qQP4;a7?9t3~+#^yksN zXDrzg?zYKe<@5QE7Omg6owe(K+UL|`?D}OndPKUW@9cDDaBhNdUfSVB>qAqXJ@;GN z>*=*<{k6h%Q&_~Jo)L#%yX^_?=RB@fM|T#tX-Um{#MXJ>yKvssyWztQ4`KX}eac^wi0Nc9Oxud0Lm=%U${|x#+WO)~7z(?_BM}>?e)%Jm$6Wy!Qgz^XuOyBzrU7 z;@aS4$-$>xSBy2ef9>}U!Hf^ydUe`s>C8^xp}_z>EJz* z5})IPei8WDExJzKblKH&@rCH#A-8;znSC2oREsTP#4RJz;M1lpp2GEx)e`08UG~%5 zQrmoeeeBEL*UbLORqv}N`AWqikMkG4{dV~i#yX#CY3fg9#Hn-Q;ZGwH4fFq83C4r7}3tMJ8h`I8=|{mwl-&G z@9mpWAzR^^^{#RBnH6h<`(pzJ`~7k^u{dbPqfK6`+%hXO`!AWxiCI&?pqf9wxpfcs^0ePIv?92oS$4ZX)cL+oa}Mv>*laWm0g7S z-mkHK6T_Lmd2K#4XZWw%0)+GAHMg=R1uc8%niDpy@ugL@gt%S$y>e6&_x*k=&(}$K zR)2+XeK$a5w$sJd(0|^S^P9g$+6&j4;XT`bNPBw2|4@!=?LlKCyP5wd=T?1j`9_uC z6~{hp>ZrU@!uXs17P7jn(&2D zlPi9y_7}t3+!@Je;$4_%sHk4-vwZ!ocjk9iN?1bL>+h~bgWLnNx~)xE@wLiy#_wD3 zV$sPF6+Ne&e`TWA&1f#;qn@S&IF%@a53G28^uns^E{rd^H%#SU+|qwhu;nM;J2Ag7 zKB-pZluasA--VSoJ&=wZEv#2>1M?3yf2Io96S;YdW%&NS%)Z5&j9UutPwr_6179`S zl`32h?oRh`_KNlQ-aD<7>JHGXw9k^#( zl?<}OU8whX%=&qGYx)M|XRaP}FR1i)nMQR#{8HI)V$kGe*ispVBJ-;9FsBurYZ{U1x z=V0rKTRa!Nyk@(vOzt7@&ur!$ozloQIL++ys;~7D zg!^3)yf-6U#W`VS9Y@o3tQ=L<-J6ya#`vvY-0=@{saZPXZ(n`hImvgb_r&=R zBUU(M3ioG@{Q5#UtIeyxX#pR$_IY?s7!NZy^}Gbbn#xX*;LTVi{Jpf>G6vF?+3gJ2PIfCAH&?s0$Wb+7a6w z!yXFPm98Q@ejrMxy}JnSA;No#@LnRkSos%*@7gX4I^?sj!|{+W3uBtGc7=|=D_@=& z8MN)@_m_@$2K>qRJ0+;*Rmpq=kDvr2==4(I(5ABF+6a8e6_0$GheM|_CL3e z&}-%s67+NJ=|9c5zEHSco@O~aYv;8HuZi{RnL021Agp7(17Dc;S~SgT@592)$IKoG z@r>W~<;<}`(|itluc*8KlJW&BKhyECUxWVD15&mhzSnF`<21%M@7ZtdsEl!b+ja%r z3cgxhxc_!yVN%%iruqKMJ~WrlZq)uRvv;_+I(%=_9zpvncNsaY)@5P;$_jR=;kReJ z*Zg^{FC6UoOgK-NzQuW1mwDSfc0_s1%HJgw{Fyy8_GRtZ836}fH*UN6e&-jVd=(iz ztNvBbHAhAY~>JF~kv2{jrNlsmUi%dNfj z9a%Yj9akpZocOEz)_&b2oj#W6G5+2Y3;V5>7Xl7eUbMJ4EU0eVG05^+sbi&#msWVR7IlUn6`~PW`H-*ZuyIsD{3KXZ1QYdB;BC zJTon)@xjo*+dd0N-t_4-W&0dvfBj6oM$hlW1#Cz+Jz9UrnXimL+EKsRz|#gnQ~O2E zct2r^us&R<*zvCu=cfcOGp*6Cd+wpXn7v_2M%tH`N}pN&L63_&DZ?4xGQ!mE@{SK{_&qLT=v`2a_P3Z-PQ=_hwEAym-MZ9F<@feJo{@eHcVvY3~Lp5 z&wKE*z$wNnDO-A}}HjP@aerq-1Jhpet&MY^FogOIzEuZcj-fa-G zzmdB1m*kskeGU%FU+&xDPhq{?;_Lo=O1_!r++D@e;><8%e`)=-%8{XGI|ohbAL>8# zM9%N5oST10x3@Z(;WaC?*Q}$`N}CzqbUPXBe_!skIDhJlLdQfQo_by~zm_wVLCYIf zifW;1YQ*gCy_qmE-SVyH#?bl|SByJ9iSg5C&FCIeYo`0O*sG@ImWTE-e*M?uLmrQk zx^8cE;8U$zCx!b_e;6Eb@342jUyfe(%!*U>hcWx3o{QoJgrxd!d~ij7)Xf*d_)I&q zJbPb*cEK58mbKU3G7{$R>Xs+oobsCOJ7@pdp^4QG++pRk^mlTfv%$<~uEoN(o6nwd zV*Je^LvN1UnjVxIHZkkt{#Odd$0=L?;Z)7cZRbS&(c5H0h3oE0tsdMC8hOrt`)KLg zZQr7VdaXBEkG;C-k?%oI>9L@ofZtd-Auba(2c+qH9nha)GU9QY_l)oEHFu6YD=#R+ z+T`Pq=_?*H-r>vh+=HiAd(UdQ*Js3{k;3_maq}x77hkyg=l8u)`^JXu{h57Q*q!Oi zW(NdseO*GP%JSMWUfEXh`%evh{SGf1Ic;LdS~teu2v~Kx-7yodj8Rwa9Qbr-F5^u` zFKXSvF34v|z2j+_)m{nr!yi_f`RlHzg#l}43|_zW!SWHze(|1DyOPOD&)p=gqF0w^ z!npN3(adtfx$7ReX-#I@9@{0HU-fJ#`TYLvHIFRg8`)Dg-4y0gg*@NF!oJ@;a(2$i zJoUIRg_RF`JrR3LUG~>5-@0=2=ij~8AKtTU&BC>7SUIh%`}Aogb@oU(P_O!t7wt|k ze&zskA~9MYxOQ!7j9vaEVLhpzYEE4LSnaW5x&Oo8axV$zpJOT+g!?=g7rgWAki83g z87yJtn0|X5?Du4t|Kj6Y#vd7IxR3F*B2uKAf9vg<`b%U%%T4L)8Q=KvuBHPkM)=Oj zI$BaK&3h~3&+h2ye{9cmMZRj(-e~QePgrDsf-m!nCs0zS6j5 zx31XY$t8KfX8l1~b@!wT>s8nj%b$jQx#qsEX6BooBew|q;GK)HL!7Rd`DeB_wwSc( zpwQlMjM-5A4eEYov ztJcYR=8oFQMKucM}d~I5hXkYdI^a-Kq<-n7w~SZpfV1$)5Axnrtg>68Dtx zB?I1d9-MH+cj4pR>1lCIq8Ojvd}yon1`ghP*H-_0UjMo<-w(78uW35Hhu4zSH7RGp zKl?NLan1u)9#;kiYze>ADs9SpL&guSoAfBR+l`=!&v4&JnsJ%&9maQax)rw|cv8Z< zMU9(B_GJ8*MGdmIAD`*EW5MG#C!bwf#rSq-GFzt>g}AS%-A4acBNt)at29LKyMN!? zUb$xG1LpnyEtuJ_U;Cs&v7XW|tBOOjH_e{hVEotD4`R2saP`T!kR0gGabS3Z(V7xtYBtredqbnWiD zX73f(^q%_$F#8j&{#gHF>Rs=Zb~Sr#n_MB4@zT3Nzd!o3V(^qx@7B+7BtrRQ@@|_o zyXFQ?Ki~4pMc-wQnf;mc_y#S9r}^cN7_d{Ba%D2(56+1F`m)9Dzzuch`8K#bX&~b} zf4RN&#gqfC2mh>oH0t3^;Xc*qeh=FpP)IzsKDzCan=t4bv+sDPt^M!wi~T2W41C(F zN5A2Ww^%aN&}U5_uZ@PQL*KlR0gj&w(l9nEi_Z!^WPhv)v=l z=Ja+an+I`>|1|PN*O8STd(3o7{~FO@%J|4RdJWsP zt?xDWt5NJ|iH+dD*`k{xM?~%O+2>#=asRnp6J~F7IP2N$Q)wPk{1$JPw0%>7@h$Bx zjBY*L$2Y5WEw9X5J(P@p(B@68C*hNV*Js|etKIL)1jY|LX<*yCW6m z^|4PitX-zjDJM+uX-x*(Jz+63#6Ej_xy&ZnD z_hXlA#*b~W#J}Cv^?v(J_GNod9xd!gw->J6QZ;{1z{UXwJgz;jy_(sNuC&n6*vr*# z$?UG*x=$z)?lV+PBL-hTcTsG}sO&TO=l8;S{)XP^Co?`i3C?UfrOLyRx2Ldjc70A$ z*1Lb+YlG^@{>w#u$1?sePx)xKiX(g$x0iRj@c6B8en_H5eLTF*%x|XsXRCJ?H}7Zm zA!Ft}TYq}I|1>|}-*itzR#dY`_BUXDn=!x)-BIUFY;hjWy9}(VPgbxtmgGBf) zB7AQVzK;k$J#S&|&--l5^Ouv&#JMJ0*6_zT4@~CHId75m0KEU6F8s8|e+7#0QW3sr z^T7$W6E9dyTZwXFXPC2c?uqdCMff^Kb3R8)s+&)KX+#<~JIR(uVkg4uiTI;0!V~w9 z0cMA6%xCO}pNs|-QhCwUSFP^j5=7WBBS)R}OJwx_=t1jO5;ne-Q_-re; zi@NxN_|TiW_>fUGi*)hki!VOZ#j7k17wh74{2kxx;)%DzH=X=}&&yu@X@hx$_G-*` zi3l$%(jB+!F<;JKA8^rP)=WY^=;sLcVXCz|u_i11qQ#`A`2C8bC$6w~CNF9z59;Lh zrDj&*`FjRu1p8wluX8`_UK6-{R@?g05_@}Qf28*9!DbT+Ee@T;e7|qpobgvfUJsCr ziuTKG_s2%Lr-@)+@cGaL;&jR4=c6S1bliAh9gBY2IC{Dwl_TN5!8=D*e$4Bk1;*P;d?q%r8s2f-zDbPlTC0EJ+EIlT2mV6bcJFM&_`g+EV_VZ)E)Gj?%d~7+* zjPWrdeuib)l)UeJ$!F!FQyYl?J>j}zfQbE3k+=;Mv5ytuyH`*2SupUDMeatDa_y*8VDthd2>`5^q0w9UI*`DF0P-`r9VeLs|JLBKF5b;xI_WKD%nK zH9 z2%j7NB4R&S#Gk$fXJ`MUf6-!H8;sB4oi4I^<3;SZx9fTS&htWx+=-n1ac1u~e@V#R z_dOKZ-Dh@_I&=+S{1B0H5=8i+BD_k3A11=@iO&eJ+;`D}{?eo7yh{|ZPZHrTF8y<= zYR(_Nc~!R5K6C4k3u{-hi2ZO8{z&7wqh|l=;J>Ef8@uP*4hi#dgoyn}5q`oS;a(vn z7cKG+V%=MlxPkdIO2mG&2tP)I|G58BtrCmLioDv}CT)Ct>lQ2j*m%p3)@J=&*Pjj8 zd1$Ul4C4=to_NLf1^n4l)}P$?g<)QS16&W+{l!kYrG3ue~;_VTjDf@8$|mLg;wE64fr@GirS$$WP18}>EqbUoqzW=dXGmD+#x%F6W^Q$8#q%wQG3p>Ml%}Mv3n}2b| z?_bUyX1r6&fIflFEDbpM_jU5tR$a^u z{$;fL#G8knT%VOJwBY#j%RV0hzWeNnTOPa3?fFV(|Dp2cVcUk zWt}^@oC^2cUw^Ufz31zGVZ6m@y(uTxTl>x#9pdmVZLqMfC5PoUO;ELP%|3bU@Bzjh1`f3tblF5g-4 z25}An{RKY@Bpb3V)_r%|951`2x1fixo)@I``s=!R`+&TbFO?6^ecH;(88urzX#M`< z!5fTgwLE8hRk)A0e#eUs7YD3!owjU1c;7^;-OT>RvBrh7D|q@Y`|VY_i(ULo#=jam zYMSNp=8EMjst$Mg$z8brncZ(hoA92egLC|w-~DsZqZQ14^U~^l?@M-gOrOt5|HXCmo}1rKy65NG<~x<-vfFVawd1yg7xfN!?f!bCNG zEf;t2-<{HAaKyTTD8|53BK8>cAzse&}9)a=*lt-XE0_71X zk3e|@$|F!7f$|8HN1!|c|1U>?Ugqyfv=Ll9rJ+-@UJ?!O!`lxU8rCDFNMp=9+|IU1) zM=Ip~K-(c=tC>$92jnmw?4AE)Q^0W@srlC>!s;u!8aR{jrvp?_3O$*Ij;XT z>`rOuG@X{kUYhZu^?vj^GP!wtwY!NqV?)}Y5JFg@fAJ-F@yZmi{?{l*rjOb zG%qcSy)@%Q%P)WZy9k7ad4#P_3d^rQd9&}FC)K*GzPD;Sjk{0#J|T%i2gUSHPU_?3 z6%s#uU|d2{pWx)=n8dCzqxyKnB_b&ts(YWPxP(3)F;T+@YO>8#(NWBUj!>x32cm?B z5?Eq6mPbG&0-Y>nW$x>jAOC$kM8a53mq(yH0{n26m=M?WULNdTvda5RqI;9 zKNFQ)%dX}>ZBW=|eTz{9w;$BlT z?mEhslygK^46FkTTG_R%DQQfsR8m2j{H)3jMP|vPz zO^jvUKRVlVwc}rN|8m?~Yt@@4m62v?^m<&`62Qg8x~4(BnsuweHw%?HVn=O4d7B=8 zXETDg87tUm`9#ZE%G<0IY;^c@h_^W=*y!-*K5z3xu(9L&ySX}^v@Y3Hu3aX8GVN-r z(XK|kO><_$;$25`JyEx#j>fvED@G&uvc|Hq5NmD>*4FT8DQ~kY25RE}I-lnBsqhq{<@-{sL8y$T+g0~qf*yt!{DQ~k< zu+dS@A>QVghRqN0zR%k{5o~nSC7aIoKY(w$j``S#w`u-&HbJ~im|&yB=S1FSq=wB8 zJ}=~LmI*dGeBQ^~9R53-JG{*U!A9+m=G>Rg;M#5oC^Ppp*R=Y)O=H1EN4xxZo8Z5* zN#JdgG;DsD)0w=@BEhDvL94ngYvG)ys>({e#^&=j`vn_2gCEMd!Q0#wY;^SLH{M2X zCTqLaT%q&T4vARv8)(@4Fz>y18~?wv8N}NR{X3i4yv@A7v&rLacK@BtRo>>NV54LF zeBy1s3pP6XwAL)H?R5eC*wE1zZoG}RV56fi2Jkk6HEe!};dI_+j)u(-eY%~u*(uoQ zXxAm)=9*xmqh0TLo6mxcj&|A1=GtBxz}CYb{OQEoxHB7xv6L8F8mL4O--E7bX)K;z zU5`|k*pkXBoa2~EiD{x-+*Z3TYbmjIs-lvZNr{=0Nh!*uMD8Txk3+$mNN`nv@>za$ zx^;8d<;hg^{IA*^8`7JO`kGMCdI_}&MH!W(q*ADv%Gw!q$r(S3%&uA~&)GSa9&47L$Lt%Grla4ue$~W8*xAtfYYBGN zp7hv>JErYOXYHW*Xt@lze0Ey-S_}C$YUT@7=cD*<>yt@|%t@+})RdB%PSz@kIgUed z{BJN*ZATG5JYX!bBvOnkF1|J>ClS0D;)fiJCCy6hwexf78s$-cXgy-(H9&e^X}Yx+ zEx#65K8b@neRT&o~KP zGZL|8n84o2P$eAOs{552Pj*6o^5e2}T>QJ^|I_?dLjBgQ`1wKGFp9OSEghScg)z2R z%}dI(L3=LQ3uW?c_^0E4YD0ak^0Z_0Px-a&wU0|x6?pZ}|Ea?xs& z#`?#V&#!BxG0;xe7(1N3_OZ6T_Hk)U_?T(?Xnj9-9!EDs7EXj3wn-1+q!m<4cQM~xd{r51($9qq#p-OzksJa ztxo6swk!1wKl)_th&W#|5Y|I(y$dx|{lK$xr>as?HL;SqU+N3};<(rbF(JgE9TX3U zzG;c0C23Zvg(YzyzJs$qM>}ff4-2Ly?!}4h( zy>6rPiXn?BwWITc@vObthylDnEvn*#vB_Xnq!B?S{)Ms)qTLl2y>14 zFN|Nd-m$W%trPZnSH$@eZ>xj{{5p{+tP|;YAd+vxW}#gQ1904250}vU3$&g+a>m&S z^Pm2fAa6HQ;KK2MtZ;2WeMyw5_r?UFe5FvQQsC%1#I-ZjTv$tZU#$dRH^P5zK3QVi z(|sV6dZ+vK!#xBx?$|h1AMI?~@sGlaaB2N0Nxp#%^s(uZs$rMH>WHor*C`+nVn;2QCHHVF;VGqRN zG2Rrb@C>ZTelN!J@vqOtpZXt!avH&Zndu|ytceren&GVpAv2{-1aA+h{VY)XIakUl zO=MOa8ku&b8OPAKEc$i~Zw?RP-`jubBOg}<*T2-S9`NfE{0h;IT+qLWT%ev1#J#0K zjAB7lt+pX!A!0ES|FKKLEOfI7{w6| ztKieEf9V(ROAW!7d5H533~hbWaO%bcyxCJXn&E8G58|vp8(r8$-&M->#5#IGp zSJ4b7@g~}CZq0HK`lc~_Y>#%Mg4t3Rp@R8b$-n7qRp8*U#)bq#97f~qXNYJ$+#15) zqoiX%3foUyo1Re@&7HYZ;hEC=HZ?;aewO76F zv2{>;k4QlbtlMCHzQo^ijK#5}iiGPA3EOYkzRGxpY>%MpmnHU1a|yS<3FjGfeM^LI z%#Pk0c9N^CJ@kl;#D!GCib-R01F^ZmjrDOftgG?X{S@Bn6OzuYY;O^-Gmx!nISN$h z- zzskK2rvzfzHw@6{JIGgB1FUsT=x* z+`GC#tvIj2I6R=vY$)r8wYNU5=i6bdRp#yw)P)UQ^Yl&6(&)X$n~`a?)=)dqw@CUn z8E=|?YU=#Dk_hQbUlD7W^f7rD* zyY6R5&(9NapGGsD8u1XuB}>;vHa;1$bk3tc@)He>&fPwVwa9J&VKtt$Nt_ z{IAxh?I$~Ci1Qrob1b%JDsk?_UDsO)*Y(+Sp3w1@Sy^T6E+y4pSLUvbM}gNq7VNll zMgBfIcYRB3Ij-z=d)afi(zGI4CmU1r94i!`zteLvC*1SqK4;=TU!mt(F57L$P1q}4 zp$PZU?72OY){Rp*b=~~^W6Ifr6MG4BM;&YaI=Px~ot%S={QYw7DLExAub6V&J;G*! zT`a9v2gmBwzJ})adPiZex7uMtk~lw=)uEzg8Zlx0$HoOaX2`|~`#iA7GK0DV3 zR@}KT^_yPv^KGds-210r6y<&K5q!yp11;f!avkr9u6A*snTHyy_nEzJ)Bm zq8{Zv&Abj=oM``j0o?(0DkV95|8X(vB&62>{hX%P(M-P~q4#3B{s(=`6zQ&->D`6= zeL!!azDM%u+&wltJ3-?q_7e6nw%*dQhxgxl#FWp1+ zMERI^0u>A6o4c3GkA3QoP&X@!^(jNjTWai4jAwmL)BkQS__ucIJym-?usu%THTOi# zy@L6v?Vs9S=W`+2ZT^Qc1RpfpL}MdTrx0`I^S?pX&ra(82I}-(``#@(X2`BRHS45h zGhfA!j;%zjd))QDpRlg^(6x^H+-{`sd8E%y8?qYLZ4OObrvn8#ic+8-Q^mlr+9P$UG8$GZ>Qbz+Q_2|(G2<8Q^2#lsu zpI%&$pK*&1Qs(@cwj~p7;npGk^G!O3e}$_@G4`dVNpQD4A@yWs+`UIDr+&|bq^pq~ zmm;lKnW0rDUsu_^sLZ`k%xC_(ZI*D|=8iJL^x+R?>?v9v8;7*-XrIx05|+51uCb@l zcFcqA1KJMcD~7JVmyV~m`L*g-aE-B+(RA}&D3_L(hw1=&vsGUxq>!QzJX{KkQKDNHHHHxKE#9pKMnV&)(4qfrJhcneSVz@93F|7eW92er<*;Kqr|IV-tX?&5tuBx5;p_I$J$y6X zF=Xn1E@|7Ox8{xA&B7F~CDoIIc&s`M0l`6`t zm$VL6p0EZ{f9M{^+R2V-eTj$zL$=n^^h_D&N6cR8{~)~c{!7?%eCm6w*6k33Omptp z(}l2dqYe;MA$9aD7D7<=^CQr$kv zvoX!=)pLY>CP&wSUi)mwAifR#p~gXQLjY|+^ELM+YEmYi@R#d9)I<9(3mJ)WHOnLEFC}8uUTKk{j@$yyt95% zKYPpC%3`REv=8zyhS_>S`(88l+A&v;4WaGyIxq^)!zj6C;=Z{asS<_nRIy{$$J+IZ z9c$;;wxj1pdF=Tkt>>k<&y8(`>j?V;HsmwhQZ|Hwu<+L)_QH9-2WYyFDSg%HTKS_{ z{y5$FC$s$1_;fx3rSHP^8at0;&qdRDmWOi)?p&;F8@OwYOxC_i!ZlVpisi3k{kUi1 z=vdW1$nT>{V<<+1)X|sflQ?s!gw?}P{n;$Pw|5uzBoAccu5)~$(D?Y#XUiIK>2!Tk zAU5pz5&jxYA;dNXS>DjtO@V?RYmSkjZag%lPY$BTQ5JU|QTFrt(#Vw6Se*Dgl?DGE zqUd=rRX$bw$Hl9KaPF#q2Unu>lt!W`621)j$|`G@dtgTcvjO8Jbg*3Nk}#5!g-LD1mhORtX_(j&>s8|AbV!g z8h?gYQ=;JPXxSeyw)t~!7vbL8b%Bc&INaVUt;ZJa&tuPc3@V+=)TSzZ{-!mT&)QcX z*AH3rn_1lbs6w3EYQMO>J{0F)bkC>Qimnf~T$$8Xc=nvOB~iW4GR#vS1Nk;o7W`d| zyyBdLVCN{bwJgW>QVyNe^;iPwytY)Ik0q-4P#x(Q*Yuev-UmyeK5mWW?q%8u9DSyS z-A5FV>ofj2R=)puoyJ7$#hzRJA%@&s()gYLZ9^j3z!3N0sw7&J-Zx_7xq<}W)P-+2 zN49dZEIr@W>Ismm{e67HH%7_Y1zZB(x{)-L=SO2=s|8ZA7B#>O|RhuT}> z^KQ)hiiyVD^=MmauW6U5(Oz~A>ZXse+8O;|#@{ccV}ZNR7sB1gqxGur43uzPZYd>} ziKZ$EMN6Ri`&yd!7HOUW)Xnd^TF)-)*splI>cSp;O32$(;6kWh+`iOO;L_=sq3zFO zdrXKh4@c2`O5L~C9{gI!wS|_=oqu!{o_ounw)A}HD1Sc1?`uv%St+#cPWq&hqy_!f zHW#N+0_Q1k^s>R4i)W04#j~0T%4U75zBeFgq#k>;&*=Sg^)X*BzfV$M3#qSMy!iVu zLR>kHe~yK6ZK*FDXD|4S)65_8a&hJCO4~+b$mivsAED)OvF7aB3gzVpc4-3VfVE7g zf85+w33FRT*F@SEp%{O3-*CdZNv~(nZ?ryoeI~3i?EVQ?f0R%cK^c7iuN3;f2=(*N z=JV%L5)(aQvKqhpp+tfI!Iz}6dJeO>#gNV^+_MtyMYCg?Zi)4gt?z2od9RY~r)tz? zL(fmmkMh?7Tz{$Cs?*jPb!Uov$4{#+VK3L_S)Xz9#!8qs8&NOc|DA+>Dx%|r>;D5> z|I>30k5f3`!?TqRfl$IB`lh4yM-%;}QhohqoHmAg z|IL7dR&owr@i4p+cT|ObSzxlX%({!n#-$Jg)~7<>sju@`Y-mh0 z&wtc067p->VZ8rqUOOLKI~dQ9^@-N=@U(ftT>7^@vO3IU#H_0kF>^Mg*Y`?VRviN= zF-XSw*I^5?AM)@tu74>H(dHzEdc@ETPmMcrCZtXUeNy4AJ~2Ru@hjp!#M9qkwZIiOLtHpk_O zLM&-siuCh-ij^d!(rREeoOimZF%^hrupsjxN{n;%;?@8sX@~_* zc#NWWTPYIPp0pr^kQBA;GU)FhOLgRBWk@U~#1dt2=%z*-8&f{D)Q`wh7NqNG3sQix zDAJdDEea9x(ssg4uAQ_j+7=D8rpG9o-gB`yi#{Ax8}D|I7W7DcKcx|L+)uG}!P8WP z)WAT%5G34vIwx5O%^OIqhWM*+kjMb zo`&yNrg8TR=z46cKF7DVHzbzPGl-@0bX(n$NH6QAe zD#;Z|#jX{IfwP=sT(KYrAx7s*{nq9rbPm8@iG_q%z<BMDDdh&`QJwBBQ0M&|0Krr`_7e1?*Xt_XxCGLey8yep}!A6UqTGz z`b6$5@KUG~1#p=emSBURE6jdxVDKJ(U zDGeRj@3>jFG9mU4!-#!yD6x<3LhPMGh`l6;(BI^uV^k^Sno9UNc^Px-yf7z~_{@&J ztZ`0eD4D_8E7&~6wIA-kIMOl{oR!)iJ0Lq&V>y&ZpgaQQ5h#y9X$0=#8jrr(Ugh`t z(iB23+etY16v##$>C~3~#{hk?-}SEs{S4M(&6hb2=zk^9m!{n|oKtGQ*nd1|+D)qT zqupaU&z0!#i*6-<=d*U5<;OopfW`mPO*~J7R!TXuW$ah?K%JgN# z*jz@go65*riHtmgJ{El&W+5YWk)9~Scl>a?2>nqG5Xqb=-wM9(J41u8Q#C9c<=X;T|2@ zH4e68;LjWQ`V8q|sCOyax(D?oA^&-lTZptb;EiE>9d))r{^yX#XzM*lCj6TOejV(s zQBMH!w1(|-q_slbA5q?E)YTJZKE&}c@b?ktFA%F+Hl#Tu01^oq0ZE6ff*gcgf;@+i z+cu;Uq&XxA5(OCnnFU!5*$=q^`3Nz(V?$~}T0=Y_QII6a6v%Q&KIA;)Cge55j)WEm_LlcKYCPzfYB_~CwViF_z4~-w1s53xUM5yAD`^Rc#VF^k7BNO7|5@M1x6Qd)OBO{VWt70_C*vO=a$aqz3q$U}c6cIJB zj4xnl*@-bpNioq{rqMB~sCTxJYe>MnuNPX*Xa*Ty)G(%_@dO4vvYS@hj^L zM{B#ok*v}m3SkKm1LB89CW|KzPrzs(aYG^p#t4DdI*f>J8Qr3V21!Q%%@!FSF(gtY zBqWT8NlY$#6p=hMA}KCmU_6ElLL8$`jgDb`swM`+$EhL`hbBiRGn0Wa2{DN{j2IH9 zLM2IRLv1>uWwbg4LuAPC_~bZM{Al&K;v0pLgAP|4B}I;ip*}@NqYLBWb(X4!)tb0nFQ@lDqEa(D(yn8?8y;aT*}gM32}5<(atPmUdp%d5KD0qO=&5b64BBl zVnE{1A-pe1Vns(pCMHIX{#Ta$hewI}9zhfTRVk%DX}P3cPci!ABZjIXhYi=N3=2Ut zUxP6G%LKA)x~_3rY9UMrkrFg!hKPkQMgAuJ?|m2<*I(ys8Zs1c2H-!(EXyRf0tY6dY!p7 z7D=7ywA;D0TQpTi-Sjj96O3+Gn%U@yg0OGI#YC%j4lR=rL*s{|Os%|0WuyG#l~FlU z9)a=*lt1f1nD>4-{lb*`ML)0B7MjGaeP|keMgYIW$i3r@KU5#Fy!4RjpUT5WJG*S!ocKMMN)FKLQz_VXukOPq5ZXukxiF<%&7kO zm@sZ=f)4xAn*L{MI^xyMw_C#CgrOr7{-Gbb`G&>d6NUu(kxW=}Voc-^jSM=={lQ9i ze+6=%zwySw32yO2qZFN89Y?i}iE8E4qD5m+z@d{5O6-%o{}JpepJH3>Gr zS_5UI0(3{$PG;~=)+Q-Iqc2cQ=M^^;`e%4FC8yFv<}tAPEI@qJ|I*}x)* z{S+Hg0<1qA|5XEC0ZfK$g`NRC4pBib0#+V@wxfJUU^K)BdzI-$w*&_^Hk&o-iF*lUNTiihCm#tJDDZ9AN$FXczPl;3ddf=wybBq(V+n8=%EZeD5529f8Xs&d>{hZnI?M1#DEn zi;!^eCBV?xhy!#La2@0nwFlbH!T3Ym6u@ne67Yqpb1oq3sIES7A`~vZT54Q6#jvznNdV#kg`ZPZC@tt-^Huw<)r2WYVr^+I<9u7Q}t{{o=fGQuK?30oGkcZF06h-T@rO77M-^Z_ft~_1IEuLg-5j_VqJXYAOMhby{!n}1Qb;lM9N;HN z3AI0m^#Sq$x-GCNWI1$4;8DmG=p{ho^N0hr2R4HIOl^SPka*}Jz=4p-&{e?kkonM4 zfQul}(6fQNA<57SfR7+Ep_c%wT)-TGZVPM*IRM=eI1#cIdNy!BTO6WL(c(5^))08&{e=$KO15`!8hRgd_&?vZGdloF(Lub$q6H}`=k+3 zLN5TCpEe?~&~1V5AS!Be#)up~XGBIpF9cd&G$J#p4e;V+BeE5G5pcK6n4E=P06b!7 zOp2&IaHWwk<}T_5?l3kcAE*t`PHs$$79)nh!V1R39C{IOw}mmO2E70nVr@*)p(}wK zARD3Q0H4=4COOpRfHAoZ8MOp`1gv`y_Rt-Hkq{+x74Rq|9C{J3!XcCk-4+-HNr9dM zd<6Lbog6kMj*wH(6+jiFfZ70aAcfEifF%&4rLZr6J!B8H2M&j1K+gbXL9(G|1GhmI zL(c&ogs7kw0F90qlk3oJfrBB%&{Ke?AVtuNfDT7dH*^JX2BZXfHgFq+z=s^*X^0Q> zLf}(KHRvTk`7yK?x;d~B#1^_EFcwmc)&<-IafDs~ybmE6$P2VQ4*#Ir0=q%Xp{szI zkZRDgfxkg)sXdVV0$*ueKu3rX^%-b(0(}QP1gM0B!$t)hPmiIe0B1oQp=SWIAq4h0 zz$1_k=<7nD*-7{f-WE6vqM-J`dk}Nz Checks SE naming rules. + private static bool VerifyPlayerName( ByteString name ) + { + // Total no more than 20 characters + space. + if( name.Length is < 5 or > 21 ) + { + return false; + } + + var split = name.Split( ( byte )' ' ); + + // Forename and surname, no more spaces. + if( split.Count != 2 ) + { + return false; + } + + static bool CheckNamePart( ByteString part ) + { + // Each name part at least 2 and at most 15 characters. + if( part.Length is < 2 or > 15 ) + { + return false; + } + + // Each part starting with capitalized letter. + if( part[ 0 ] is < ( byte )'A' or > ( byte )'Z' ) + { + return false; + } + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + if( part.Skip( 1 ).Any( c => c != ( byte )'\'' && c != ( byte )'-' && c is < ( byte )'a' or > ( byte )'z' ) ) + { + return false; + } + + var hyphens = part.Split( ( byte )'-' ); + // Apostrophes can not be used in succession, after or before apostrophes. + return !hyphens.Any( p => p.Length == 0 || p[ 0 ] == ( byte )'\'' || p.Last() == ( byte )'\'' ); + } + + return CheckNamePart( split[ 0 ] ) && CheckNamePart( split[ 1 ] ); + } + + /// Checks if the world is a valid public world or ushort.MaxValue (any world). + private bool VerifyWorld( ushort worldId ) + => Worlds.ContainsKey( worldId ); + + /// Verify that the enum value is a specific actor and return the name if it is. + private static bool VerifySpecial( SpecialActor actor ) + => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; + + /// Verify that the object index is a valid index for an NPC. + private static bool VerifyIndex( ushort index ) + { + return index switch + { + < 200 => index % 2 == 0, + > ( ushort )SpecialActor.Portrait => index < 426, + _ => false, + }; + } + + /// Verify that the object kind is a valid owned object, and the corresponding data Id. + private bool VerifyOwnedData( ObjectKind kind, uint dataId ) + { + return kind switch + { + ObjectKind.MountType => Mounts.ContainsKey( dataId ), + ObjectKind.Companion => Companions.ContainsKey( dataId ), + ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), + _ => false, + }; + } + + private bool VerifyNpcData( ObjectKind kind, uint dataId ) + => kind switch + { + ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), + ObjectKind.EventNpc => ENpcs.ContainsKey( dataId ), + _ => false, + }; +} \ No newline at end of file diff --git a/Penumbra.GameData/Actors/IdentifierType.cs b/Penumbra.GameData/Actors/IdentifierType.cs new file mode 100644 index 00000000..a582aa14 --- /dev/null +++ b/Penumbra.GameData/Actors/IdentifierType.cs @@ -0,0 +1,10 @@ +namespace Penumbra.GameData.Actors; + +public enum IdentifierType : byte +{ + Invalid, + Player, + Owned, + Special, + Npc, +}; \ No newline at end of file diff --git a/Penumbra.GameData/Actors/SpecialActor.cs b/Penumbra.GameData/Actors/SpecialActor.cs new file mode 100644 index 00000000..5319c747 --- /dev/null +++ b/Penumbra.GameData/Actors/SpecialActor.cs @@ -0,0 +1,12 @@ +namespace Penumbra.GameData.Actors; + +public enum SpecialActor : ushort +{ + CutsceneStart = 200, + CutsceneEnd = 240, + CharacterScreen = 240, + ExamineScreen = 241, + FittingRoom = 242, + DyePreview = 243, + Portrait = 244, +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs deleted file mode 100644 index aeda4bfd..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - private static readonly byte[] AsciiLowerCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) - .ToArray(); - - private static readonly byte[] AsciiUpperCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToUpperInvariant( ( char )i ) ) - .ToArray(); - - // Convert a byte to its ASCII-lowercase version. - public static byte AsciiToLower( byte b ) - => AsciiLowerCaseBytes[ b ]; - - // Check if a byte is ASCII-lowercase. - public static bool AsciiIsLower( byte b ) - => AsciiToLower( b ) == b; - - // Convert a byte to its ASCII-uppercase version. - public static byte AsciiToUpper( byte b ) - => AsciiUpperCaseBytes[ b ]; - - // Check if a byte is ASCII-uppercase. - public static bool AsciiIsUpper( byte b ) - => AsciiToUpper( b ) == b; - - // Check if a byte array of given length is ASCII-lowercase. - public static bool IsAsciiLowerCase( byte* path, int length ) - { - var end = path + length; - for( ; path < end; ++path ) - { - if( *path != AsciiLowerCaseBytes[*path] ) - { - return false; - } - } - - return true; - } - - // Compare two byte arrays of given lengths ASCII-case-insensitive. - public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - // Check two byte arrays of given lengths for ASCII-case-insensitive equality. - public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; - } - - // Check if a byte array of given length consists purely of ASCII characters. - public static bool IsAscii( byte* path, int length ) - { - var length8 = length / 8; - var end8 = ( ulong* )path + length8; - for( var ptr8 = ( ulong* )path; ptr8 < end8; ++ptr8 ) - { - if( ( *ptr8 & 0x8080808080808080ul ) != 0 ) - { - return false; - } - } - - var end = path + length; - for( path += length8 * 8; path < end; ++path ) - { - if( *path > 127 ) - { - return false; - } - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs deleted file mode 100644 index 3e7382c9..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Lexicographically compare two byte arrays of given length. - public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength == rhsLength ) - { - return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - } - - if( lhsLength < rhsLength ) - { - var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); - return cmp != 0 ? cmp : -1; - } - - var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); - return cmp2 != 0 ? cmp2 : 1; - } - - // Lexicographically compare one byte array of given length with a null-terminated byte array of unknown length. - public static int Compare( byte* lhs, int lhsLength, byte* rhs ) - { - var end = lhs + lhsLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - // Lexicographically compare two null-terminated byte arrays of unknown length not larger than maxLength. - public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - { - var end = lhs + maxLength; - for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) - { - if( *lhs == 0 ) - { - return *rhs == 0 ? 0 : -1; - } - - if( *rhs == 0 ) - { - return 1; - } - - var diff = *tmp - *rhs; - if( diff != 0 ) - { - return diff; - } - } - - return 0; - } - - // Check two byte arrays of given length for equality. - public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) - { - if( lhsLength != rhsLength ) - { - return false; - } - - if( lhs == rhs || lhsLength == 0 ) - { - return true; - } - - return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; - } - - // Check one byte array of given length for equality against a null-terminated byte array of unknown length. - private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) - => Compare( lhs, lhsLength, rhs ) == 0; - - // Check two null-terminated byte arrays of unknown length not larger than maxLength for equality. - private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) - => Compare( lhs, rhs, maxLength ) == 0; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs deleted file mode 100644 index 18cc3a81..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Used for static null-terminators. - public class NullTerminator - { - public readonly byte* NullBytePtr; - - public NullTerminator() - { - NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); - *NullBytePtr = 0; - } - - ~NullTerminator() - => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); - } - - // Convert a C# unicode-string to an unmanaged UTF8-byte array and return the pointer. - // If the length would exceed the given maxLength, return a nullpointer instead. - public static byte* Utf8FromString( string s, out int length, int maxLength = int.MaxValue ) - { - length = Encoding.UTF8.GetByteCount( s ); - if( length >= maxLength ) - { - return null; - } - - var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); - fixed( char* ptr = s ) - { - Encoding.UTF8.GetBytes( ptr, s.Length, path, length + 1 ); - } - - path[ length ] = 0; - return path; - } - - // Create a copy of a given string and return the pointer. - public static byte* CopyString( byte* path, int length ) - { - var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); - Functions.MemCpyUnchecked( ret, path, length ); - ret[ length ] = 0; - return ret; - } - - // Check the length of a null-terminated byte array no longer than the given maxLength. - public static int CheckLength( byte* path, int maxLength = int.MaxValue ) - { - var end = path + maxLength; - for( var it = path; it < end; ++it ) - { - if( *it == 0 ) - { - return ( int )( it - path ); - } - } - - throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs deleted file mode 100644 index 7d21593a..00000000 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.GameData.ByteString; - -public static unsafe partial class ByteStringFunctions -{ - // Replace all occurrences of from in a byte array of known length with to. - public static int Replace( byte* ptr, int length, byte from, byte to ) - { - var end = ptr + length; - var numReplaced = 0; - for( ; ptr < end; ++ptr ) - { - if( *ptr == from ) - { - *ptr = to; - ++numReplaced; - } - } - - return numReplaced; - } - - // Convert a byte array of given length to ASCII-lowercase. - public static void AsciiToLowerInPlace( byte* path, int length ) - { - for( var i = 0; i < length; ++i ) - { - path[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; - } - } - - // Copy a byte array and convert the copy to ASCII-lowercase. - public static byte* AsciiToLower( byte* path, int length ) - { - var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); - ptr[ length ] = 0; - for( var i = 0; i < length; ++i ) - { - ptr[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; - } - - return ptr; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs deleted file mode 100644 index 6d3cc0bd..00000000 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -[JsonConverter( typeof( FullPathConverter ) )] -public readonly struct FullPath : IComparable, IEquatable< FullPath > -{ - public readonly string FullName; - public readonly Utf8String InternalName; - public readonly ulong Crc64; - - public static readonly FullPath Empty = new(string.Empty); - - public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath ) - : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) - { } - - public FullPath( FileInfo file ) - : this( file.FullName ) - { } - - - public FullPath( string s ) - { - FullName = s; - InternalName = Utf8String.FromString( FullName.Replace( '\\', '/' ), out var name, true ) ? name : Utf8String.Empty; - Crc64 = Functions.ComputeCrc64( InternalName.Span ); - } - - public FullPath( Utf8GamePath path ) - { - FullName = path.ToString().Replace( '/', '\\' ); - InternalName = path.Path; - Crc64 = Functions.ComputeCrc64( InternalName.Span ); - } - - public bool Exists - => File.Exists( FullName ); - - public string Extension - => Path.GetExtension( FullName ); - - public string Name - => Path.GetFileName( FullName ); - - public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path ) - { - path = Utf8GamePath.Empty; - if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) - { - return false; - } - - var substring = InternalName.Substring( dir.FullName.Length + 1 ); - - path = new Utf8GamePath( substring ); - return true; - } - - public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path ) - { - path = Utf8RelPath.Empty; - if( !FullName.StartsWith( dir.FullName ) ) - { - return false; - } - - var substring = InternalName.Substring( dir.FullName.Length + 1 ); - - path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) ); - return true; - } - - public int CompareTo( object? obj ) - => obj switch - { - FullPath p => InternalName?.CompareTo( p.InternalName ) ?? -1, - FileInfo f => string.Compare( FullName, f.FullName, StringComparison.OrdinalIgnoreCase ), - Utf8String u => InternalName?.CompareTo( u ) ?? -1, - string s => string.Compare( FullName, s, StringComparison.OrdinalIgnoreCase ), - _ => -1, - }; - - public bool Equals( FullPath other ) - { - if( Crc64 != other.Crc64 ) - { - return false; - } - - if( FullName.Length == 0 || other.FullName.Length == 0 ) - { - return true; - } - - return InternalName.Equals( other.InternalName ); - } - - public bool IsRooted - => new Utf8GamePath( InternalName ).IsRooted(); - - public override int GetHashCode() - => InternalName.Crc32; - - public override string ToString() - => FullName; - - public class FullPathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( FullPath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return new FullPath( token ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is FullPath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8GamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs deleted file mode 100644 index 79386002..00000000 --- a/Penumbra.GameData/ByteString/Utf8GamePath.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.IO; -using Dalamud.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -// NewGamePath wrap some additional validity checking around Utf8String, -// provide some filesystem helpers, and conversion to Json. -[JsonConverter( typeof( Utf8GamePathConverter ) )] -public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable -{ - public const int MaxGamePathLength = 256; - - public readonly Utf8String Path; - public static readonly Utf8GamePath Empty = new(Utf8String.Empty); - - internal Utf8GamePath( Utf8String s ) - => Path = s; - - public int Length - => Path.Length; - - public bool IsEmpty - => Path.IsEmpty; - - public Utf8GamePath ToLower() - => new(Path.AsciiToLower()); - - public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false ) - { - var utf = new Utf8String( ptr ); - return ReturnChecked( utf, out path, lower ); - } - - public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false ) - { - var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); - return ReturnChecked( utf, out path, lower ); - } - - // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. - // Does not check for initial slashes either, since they are assumed to be by choice. - // Checks for maxlength, ASCII and lowercase. - private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false ) - { - path = Empty; - if( !utf.IsAscii || utf.Length > MaxGamePathLength ) - { - return false; - } - - path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf ); - return true; - } - - public Utf8GamePath Clone() - => new(Path.Clone()); - - public static explicit operator Utf8GamePath( string s ) - => FromString( s, out var p, true ) ? p : Empty; - - public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false ) - { - path = Empty; - if( s.IsNullOrEmpty() ) - { - return true; - } - - var substring = s!.Replace( '\\', '/' ).TrimStart( '/' ); - if( substring.Length > MaxGamePathLength ) - { - return false; - } - - if( substring.Length == 0 ) - { - return true; - } - - if( !Utf8String.FromString( substring, out var ascii, toLower ) || !ascii.IsAscii ) - { - return false; - } - - path = new Utf8GamePath( ascii ); - return true; - } - - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ ( baseDir.FullName.Length + 1 ).. ]; - return FromString( substring, out path, toLower ); - } - - public Utf8String Filename() - { - var idx = Path.LastIndexOf( ( byte )'/' ); - return idx == -1 ? Path : Path.Substring( idx + 1 ); - } - - public Utf8String Extension() - { - var idx = Path.LastIndexOf( ( byte )'.' ); - return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); - } - - public bool Equals( Utf8GamePath other ) - => Path.Equals( other.Path ); - - public override int GetHashCode() - => Path.GetHashCode(); - - public int CompareTo( Utf8GamePath other ) - => Path.CompareTo( other.Path ); - - public override string ToString() - => Path.ToString(); - - public void Dispose() - => Path.Dispose(); - - public bool IsRooted() - => IsRooted( Path ); - - public static bool IsRooted( Utf8String path ) - => path.Length >= 1 && ( path[ 0 ] == '/' || path[ 0 ] == '\\' ) - || path.Length >= 2 - && ( path[ 0 ] >= 'A' && path[ 0 ] <= 'Z' || path[ 0 ] >= 'a' && path[ 0 ] <= 'z' ) - && path[ 1 ] == ':'; - - public class Utf8GamePathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( Utf8GamePath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return FromString( token, out var p, true ) - ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is Utf8GamePath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } - - public GamePath ToGamePath() - => GamePath.GenerateUnchecked( ToString() ); -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8RelPath.cs b/Penumbra.GameData/ByteString/Utf8RelPath.cs deleted file mode 100644 index cef27b6f..00000000 --- a/Penumbra.GameData/ByteString/Utf8RelPath.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.IO; -using Dalamud.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Penumbra.GameData.ByteString; - -[JsonConverter( typeof( Utf8RelPathConverter ) )] -public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable -{ - public const int MaxRelPathLength = 250; - - public readonly Utf8String Path; - public static readonly Utf8RelPath Empty = new(Utf8String.Empty); - - internal Utf8RelPath( Utf8String path ) - => Path = path; - - - public static explicit operator Utf8RelPath( string s ) - { - if( !FromString( s, out var p ) ) - { - return Empty; - } - - return new Utf8RelPath( p.Path.AsciiToLower() ); - } - - public static bool FromString( string? s, out Utf8RelPath path ) - { - path = Empty; - if( s.IsNullOrEmpty() ) - { - return true; - } - - var substring = s.Replace( '/', '\\' ).TrimStart('\\'); - if( substring.Length > MaxRelPathLength ) - { - return false; - } - - if( substring.Length == 0 ) - { - return true; - } - - if( !Utf8String.FromString( substring, out var ascii, true ) || !ascii.IsAscii ) - { - return false; - } - - path = new Utf8RelPath( ascii ); - return true; - } - - public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; - return FromString( substring, out path ); - } - - public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path ) - { - path = Empty; - if( !file.FullName.StartsWith( baseDir.FullName ) ) - { - return false; - } - - var substring = file.FullName[ (baseDir.FullName.Length + 1).. ]; - return FromString( substring, out path ); - } - - public Utf8RelPath( Utf8GamePath gamePath ) - => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); - - public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 ) - { - var idx = 0; - while( skipFolders > 0 ) - { - idx = Path.IndexOf( ( byte )'\\', idx ) + 1; - --skipFolders; - if( idx <= 0 ) - { - return Utf8GamePath.Empty; - } - } - - var length = Path.Length - idx; - var ptr = ByteStringFunctions.CopyString( Path.Path + idx, length ); - ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); - ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); - var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); - return new Utf8GamePath( utf ); - } - - public int CompareTo( Utf8RelPath rhs ) - => Path.CompareTo( rhs.Path ); - - public bool Equals( Utf8RelPath other ) - => Path.Equals( other.Path ); - - public override string ToString() - => Path.ToString(); - - public void Dispose() - => Path.Dispose(); - - public class Utf8RelPathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( Utf8RelPath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ).ToString(); - return FromString( token, out var p ) - ? p - : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." ); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value is Utf8RelPath p ) - { - serializer.Serialize( writer, p.ToString() ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Access.cs b/Penumbra.GameData/ByteString/Utf8String.Access.cs deleted file mode 100644 index a74dd672..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Access.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Penumbra.GameData.ByteString; - -// Utf8String is a wrapper around unsafe byte strings. -// It may be used to store owned strings in unmanaged space, -// as well as refer to unowned strings. -// Unowned strings may change their value and thus become corrupt, -// so they should never be stored, just used locally or with great care. -// The string keeps track of whether it is owned or not, it also can keep track -// of some other information, like the string being pure ASCII, ASCII-lowercase or null-terminated. -// Owned strings are always null-terminated. -// Any constructed string will compute its own CRC32-value (as long as the string itself is not changed). -public sealed unsafe partial class Utf8String : IEnumerable< byte > -{ - // We keep information on some of the state of the Utf8String in specific bits. - // This costs some potential max size, but that is not relevant for our case. - // Except for destruction/dispose, or if the non-owned pointer changes values, - // the CheckedFlag, AsciiLowerCaseFlag and AsciiFlag are the only things that are mutable. - private const uint NullTerminatedFlag = 0x80000000; - private const uint OwnedFlag = 0x40000000; - private const uint AsciiCheckedFlag = 0x04000000; - private const uint AsciiFlag = 0x08000000; - private const uint AsciiLowerCheckedFlag = 0x10000000; - private const uint AsciiLowerFlag = 0x20000000; - private const uint FlagMask = 0x03FFFFFF; - - public bool IsNullTerminated - => ( _length & NullTerminatedFlag ) != 0; - - public bool IsOwned - => ( _length & OwnedFlag ) != 0; - - public bool IsAscii - => CheckAscii(); - - public bool IsAsciiLowerCase - => CheckAsciiLower(); - - public byte* Path - => _path; - - public int Crc32 - => _crc32; - - public int Length - => ( int )( _length & FlagMask ); - - public bool IsEmpty - => Length == 0; - - public ReadOnlySpan< byte > Span - => new(_path, Length); - - public byte this[ int idx ] - => ( uint )idx < Length ? _path[ idx ] : throw new IndexOutOfRangeException(); - - public IEnumerator< byte > GetEnumerator() - { - for( var i = 0; i < Length; ++i ) - { - yield return Span[ i ]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - // Only not readonly due to dispose. - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() - => _crc32; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs deleted file mode 100644 index 6d96dce5..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, IComparable< Utf8String > -{ - public bool Equals( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return false; - } - - if( ReferenceEquals( this, other ) ) - { - return true; - } - - return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); - } - - public bool EqualsCi( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return false; - } - - if( ReferenceEquals( this, other ) ) - { - return true; - } - - if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) - { - return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); - } - - return ByteStringFunctions.AsciiCaselessEquals( _path, Length, other._path, other.Length ); - } - - public int CompareTo( Utf8String? other ) - { - if( ReferenceEquals( this, other ) ) - { - return 0; - } - - if( ReferenceEquals( null, other ) ) - { - return 1; - } - - return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); - } - - public int CompareToCi( Utf8String? other ) - { - if( ReferenceEquals( null, other ) ) - { - return 0; - } - - if( ReferenceEquals( this, other ) ) - { - return 1; - } - - if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) - { - return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); - } - - return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); - } - - public bool StartsWith( Utf8String other ) - { - var otherLength = other.Length; - return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength ); - } - - public bool EndsWith( Utf8String other ) - { - var otherLength = other.Length; - var offset = Length - otherLength; - return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength ); - } - - public bool StartsWith( params char[] chars ) - { - if( chars.Length > Length ) - { - return false; - } - - var ptr = _path; - return chars.All( t => *ptr++ == ( byte )t ); - } - - public bool EndsWith( params char[] chars ) - { - if( chars.Length > Length ) - { - return false; - } - - var ptr = _path + Length - chars.Length; - return chars.All( c => *ptr++ == ( byte )c ); - } - - public int IndexOf( byte b, int from = 0 ) - { - var end = _path + Length; - for( var tmp = _path + from; tmp < end; ++tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - _path ); - } - } - - return -1; - } - - public int LastIndexOf( byte b, int to = 0 ) - { - var end = _path + to; - for( var tmp = _path + Length - 1; tmp >= end; --tmp ) - { - if( *tmp == b ) - { - return ( int )( tmp - _path ); - } - } - - return -1; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs deleted file mode 100644 index 1e6ab325..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String : IDisposable -{ - // statically allocated null-terminator for empty strings to point to. - private static readonly ByteStringFunctions.NullTerminator Null = new(); - - public static readonly Utf8String Empty = new(); - - // actual data members. - private byte* _path; - private uint _length; - private int _crc32; - - // Create an empty string. - public Utf8String() - { - _path = Null.NullBytePtr; - _length |= AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag | AsciiFlag; - _crc32 = 0; - } - - // Create a temporary Utf8String from a byte pointer. - // This computes CRC, checks for ASCII and AsciiLower and assumes Null-Termination. - public Utf8String( byte* path ) - { - var length = Functions.ComputeCrc32AsciiLowerAndSize( path, out var crc32, out var lower, out var ascii ); - Setup( path, length, crc32, true, false, lower, ascii ); - } - - // Construct a temporary Utf8String from a given byte string of known size. - // Other known attributes can also be provided and are not computed. - // Can throw ArgumentOutOfRange if length is higher than max length. - // The Crc32 will be computed. - public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) - => new Utf8String().Setup( path, length, null, isNullTerminated, false, isLower, isAscii ); - - // Same as above, just with a span. - public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) - { - fixed( byte* ptr = path ) - { - return FromByteStringUnsafe( ptr, path.Length, isNullTerminated, isLower, isAscii ); - } - } - - // Construct a Utf8String from a given unicode string, possibly converted to ascii lowercase. - // Only returns false if the length exceeds the max length. - public static bool FromString( string? path, out Utf8String ret, bool toAsciiLower = false ) - { - if( string.IsNullOrEmpty( path ) ) - { - ret = Empty; - return true; - } - - var p = ByteStringFunctions.Utf8FromString( path, out var l, ( int )FlagMask ); - if( p == null ) - { - ret = Empty; - return false; - } - - if( toAsciiLower ) - { - ByteStringFunctions.AsciiToLowerInPlace( p, l ); - } - - ret = new Utf8String().Setup( p, l, null, true, true, toAsciiLower ? true : null, l == path.Length ); - return true; - } - - // Does not check for length and just assumes the isLower state from the second argument. - public static Utf8String FromStringUnsafe( string? path, bool? isLower ) - { - if( string.IsNullOrEmpty( path ) ) - { - return Empty; - } - - var p = ByteStringFunctions.Utf8FromString( path, out var l ); - var ret = new Utf8String().Setup( p, l, null, true, true, isLower, l == path.Length ); - return ret; - } - - // Free memory if the string is owned. - private void ReleaseUnmanagedResources() - { - if( !IsOwned ) - { - return; - } - - Marshal.FreeHGlobal( ( IntPtr )_path ); - GC.RemoveMemoryPressure( Length + 1 ); - _length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag; - _path = Null.NullBytePtr; - _crc32 = 0; - } - - // Manually free memory. Sets the string to an empty string. - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize( this ); - } - - ~Utf8String() - { - ReleaseUnmanagedResources(); - } - - // Setup from all given values. - // Only called from constructors or factory functions in this library. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - internal Utf8String Setup( byte* path, int length, int? crc32, bool isNullTerminated, bool isOwned, - bool? isLower = null, bool? isAscii = null ) - { - if( length > FlagMask ) - { - throw new ArgumentOutOfRangeException( nameof( length ) ); - } - - _path = path; - _length = ( uint )length; - _crc32 = crc32 ?? ( int )~Lumina.Misc.Crc32.Get( new ReadOnlySpan< byte >( path, length ) ); - if( isNullTerminated ) - { - _length |= NullTerminatedFlag; - } - - if( isOwned ) - { - GC.AddMemoryPressure( length + 1 ); - _length |= OwnedFlag; - } - - if( isLower != null ) - { - _length |= AsciiLowerCheckedFlag; - if( isLower.Value ) - { - _length |= AsciiLowerFlag; - } - } - - if( isAscii != null ) - { - _length |= AsciiCheckedFlag; - if( isAscii.Value ) - { - _length |= AsciiFlag; - } - } - - return this; - } - - private bool CheckAscii() - { - switch( _length & ( AsciiCheckedFlag | AsciiFlag ) ) - { - case AsciiCheckedFlag: return false; - case AsciiCheckedFlag | AsciiFlag: return true; - default: - _length |= AsciiCheckedFlag; - var isAscii = ByteStringFunctions.IsAscii( _path, Length ); - if( isAscii ) - { - _length |= AsciiFlag; - } - - return isAscii; - } - } - - private bool CheckAsciiLower() - { - switch( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) - { - case AsciiLowerCheckedFlag: return false; - case AsciiLowerCheckedFlag | AsciiLowerFlag: return true; - default: - _length |= AsciiLowerCheckedFlag; - var isAsciiLower = ByteStringFunctions.IsAsciiLowerCase( _path, Length ); - if( isAsciiLower ) - { - _length |= AsciiLowerFlag; - } - - return isAsciiLower; - } - } - - private bool? IsAsciiInternal - => ( _length & ( AsciiCheckedFlag | AsciiFlag ) ) switch - { - AsciiCheckedFlag => false, - AsciiFlag => true, - _ => null, - }; - - private bool? IsAsciiLowerInternal - => ( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) switch - { - AsciiLowerCheckedFlag => false, - AsciiLowerFlag => true, - _ => null, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs deleted file mode 100644 index c4332a1d..00000000 --- a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.GameData.Util; - -namespace Penumbra.GameData.ByteString; - -public sealed unsafe partial class Utf8String -{ - // Create a C# Unicode string from this string. - // If the string is known to be pure ASCII, use that encoding, otherwise UTF8. - public override string ToString() - => Length == 0 - ? string.Empty - : ( _length & AsciiFlag ) != 0 - ? Encoding.ASCII.GetString( _path, Length ) - : Encoding.UTF8.GetString( _path, Length ); - - - // Convert the ascii portion of the string to lowercase. - // Only creates a new string and copy if the string is not already known to be lowercase. - public Utf8String AsciiToLower() - => ( _length & AsciiLowerFlag ) == 0 - ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) - : this; - - // Convert the ascii portion of the string to mixed case (i.e. capitalize every first letter in a word) - // Clones the string. - public Utf8String AsciiToMixed() - { - var length = Length; - if( length == 0 ) - { - return Empty; - } - - var ret = Clone(); - var previousWhitespace = true; - var end = ret.Path + length; - for( var ptr = ret.Path; ptr < end; ++ptr ) - { - if( previousWhitespace ) - { - *ptr = ByteStringFunctions.AsciiToUpper( *ptr ); - } - - previousWhitespace = char.IsWhiteSpace( ( char )*ptr ); - } - - return ret; - } - - // Convert the ascii portion of the string to lowercase. - // Guaranteed to create an owned copy. - public Utf8String AsciiToLowerClone() - => ( _length & AsciiLowerFlag ) == 0 - ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) - : Clone(); - - // Create an owned copy of the given string. - public Utf8String Clone() - { - var ret = new Utf8String(); - ret._length = _length | OwnedFlag | NullTerminatedFlag; - ret._path = ByteStringFunctions.CopyString( Path, Length ); - ret._crc32 = Crc32; - return ret; - } - - // Create a non-owning substring from the given position. - // If from is negative or too large, the returned string will be the empty string. - public Utf8String Substring( int from ) - => ( uint )from < Length - ? FromByteStringUnsafe( _path + from, Length - from, IsNullTerminated, IsAsciiLowerInternal, IsAsciiInternal ) - : Empty; - - // Create a non-owning substring from the given position of the given length. - // If from is negative or too large, the returned string will be the empty string. - // If from + length is too large, it will be the same as if length was not specified. - public Utf8String Substring( int from, int length ) - { - var maxLength = Length - ( uint )from; - if( maxLength <= 0 ) - { - return Empty; - } - - return length < maxLength - ? FromByteStringUnsafe( _path + from, length, false, IsAsciiLowerInternal, IsAsciiInternal ) - : Substring( from ); - } - - // Create a owned copy of the string and replace all occurences of from with to in it. - public Utf8String Replace( byte from, byte to ) - { - var length = Length; - var newPtr = ByteStringFunctions.CopyString( _path, length ); - var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to ); - return new Utf8String().Setup( newPtr, length, numReplaced == 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); - } - - // Join a number of strings with a given byte between them. - public static Utf8String Join( byte splitter, params Utf8String[] strings ) - { - var length = strings.Sum( s => s.Length ) + strings.Length; - var data = ( byte* )Marshal.AllocHGlobal( length ); - - var ptr = data; - bool? isLower = ByteStringFunctions.AsciiIsLower( splitter ); - bool? isAscii = splitter < 128; - foreach( var s in strings ) - { - Functions.MemCpyUnchecked( ptr, s.Path, s.Length ); - ptr += s.Length; - *ptr++ = splitter; - isLower = Combine( isLower, s.IsAsciiLowerInternal ); - isAscii &= s.IsAscii; - } - - --length; - data[ length ] = 0; - var ret = FromByteStringUnsafe( data, length, true, isLower, isAscii ); - ret._length |= OwnedFlag; - return ret; - } - - // Split a string and return a list of the substrings delimited by b. - // You can specify the maximum number of splits (if the maximum is reached, the last substring may contain delimiters). - // You can also specify to ignore empty substrings inside delimiters. Those are also not counted for max splits. - public List< Utf8String > Split( byte b, int maxSplits = int.MaxValue, bool removeEmpty = true ) - { - var ret = new List< Utf8String >(); - var start = 0; - for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) - { - if( start != idx || !removeEmpty ) - { - ret.Add( Substring( start, idx - start ) ); - } - - start = idx + 1; - if( ret.Count == maxSplits - 1 ) - { - break; - } - } - - ret.Add( Substring( start ) ); - return ret; - } - - private static bool? Combine( bool? val1, bool? val2 ) - { - return ( val1, val2 ) switch - { - (null, null) => null, - (null, true) => null, - (null, false) => false, - (true, null) => null, - (true, true) => true, - (true, false) => false, - (false, null) => false, - (false, true) => false, - (false, false) => false, - }; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs index dedaf3c2..42783c97 100644 --- a/Penumbra.GameData/Enums/ResourceType.cs +++ b/Penumbra.GameData/Enums/ResourceType.cs @@ -1,6 +1,7 @@ using System; using System.IO; -using Penumbra.GameData.ByteString; +using Penumbra.String; +using Penumbra.String.Functions; namespace Penumbra.GameData.Enums; @@ -93,10 +94,10 @@ public static class ResourceTypeExtensions }; } - public static ResourceType FromString( Utf8String path ) + public static ResourceType FromString( ByteString path ) { var extIdx = path.LastIndexOf( ( byte )'.' ); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? Utf8String.Empty : path.Substring( extIdx + 1 ); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring( extIdx + 1 ); return ext.Length switch { diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index cb51d6dc..0aaf9c89 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -36,6 +36,7 @@ + @@ -51,6 +52,10 @@ $(DalamudLibPath)Lumina.Excel.dll False + + $(DalamudLibPath)FFXIVClientStructs.dll + False + $(DalamudLibPath)Newtonsoft.Json.dll False diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index a8744fff..58a3b317 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -1,6 +1,7 @@ using System; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; @@ -107,9 +108,9 @@ public readonly unsafe struct CharacterEquip public void Load( CharacterEquip source ) { - Functions.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); + MemoryUtility.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); } public bool Equals( CharacterEquip other ) - => Functions.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; + => MemoryUtility.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 3f184bb5..1524ae11 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.GameData.Util; +using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; @@ -13,7 +13,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - Functions.MemCpyUnchecked( ptr, source, Size ); + MemoryUtility.MemCpyUnchecked( ptr, source, Size ); } } @@ -21,7 +21,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - Functions.MemCpyUnchecked( target, ptr, Size ); + MemoryUtility.MemCpyUnchecked( target, ptr, Size ); } } @@ -36,12 +36,12 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { fixed( byte* ptr = Data ) { - return Functions.MemCmpUnchecked( ptr, other.Data, Size ) == 0; + return MemoryUtility.MemCmpUnchecked( ptr, other.Data, Size ) == 0; } } public static bool Equals( CustomizeData* lhs, CustomizeData* rhs ) - => Functions.MemCmpUnchecked( lhs, rhs, Size ) == 0; + => MemoryUtility.MemCmpUnchecked( lhs, rhs, Size ) == 0; public override bool Equals( object? obj ) => obj is CustomizeData other && Equals( other ); diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs deleted file mode 100644 index 81e3dc28..00000000 --- a/Penumbra.GameData/Util/Functions.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.InteropServices; -using ByteStringFunctions = Penumbra.GameData.ByteString.ByteStringFunctions; - -namespace Penumbra.GameData.Util; - -public static class Functions -{ - public static ulong ComputeCrc64( string name ) - { - if( name.Length == 0 ) - { - return 0; - } - - var lastSlash = name.LastIndexOf( '/' ); - if( lastSlash == -1 ) - { - return Lumina.Misc.Crc32.Get( name ); - } - - var folder = name[ ..lastSlash ]; - var file = name[ ( lastSlash + 1 ).. ]; - return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); - } - - public static ulong ComputeCrc64( ReadOnlySpan< byte > name ) - { - if( name.Length == 0 ) - { - return 0; - } - - var lastSlash = name.LastIndexOf( ( byte )'/' ); - if( lastSlash == -1 ) - { - return Lumina.Misc.Crc32.Get( name ); - } - - var folder = name[ ..lastSlash ]; - var file = name[ ( lastSlash + 1 ).. ]; - return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); - } - - private static readonly uint[] CrcTable = - typeof( Lumina.Misc.Crc32 ).GetField( "CrcTable", BindingFlags.Static | BindingFlags.NonPublic )?.GetValue( null ) as uint[] - ?? throw new Exception( "Could not fetch CrcTable from Lumina." ); - - - public static unsafe int ComputeCrc64LowerAndSize( byte* ptr, out ulong crc64, out int crc32Ret, out bool isLower, out bool isAscii ) - { - var tmp = ptr; - uint crcFolder = 0; - uint crcFile = 0; - var crc32 = uint.MaxValue; - crc64 = 0; - isLower = true; - isAscii = true; - while( true ) - { - var value = *tmp; - if( value == 0 ) - { - break; - } - - if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) - { - isLower = false; - } - - if( value > 0x80 ) - { - isAscii = false; - } - - if( value == ( byte )'/' ) - { - crcFolder = crc32; - crcFile = uint.MaxValue; - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - } - else - { - crcFile = CrcTable[ ( byte )( crcFolder ^ value ) ] ^ ( crcFolder >> 8 ); - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - } - - ++tmp; - } - - var size = ( int )( tmp - ptr ); - crc64 = ~( ( ulong )crcFolder << 32 ) | crcFile; - crc32Ret = ( int )~crc32; - return size; - } - - public static unsafe int ComputeCrc32AsciiLowerAndSize( byte* ptr, out int crc32Ret, out bool isLower, out bool isAscii ) - { - var tmp = ptr; - var crc32 = uint.MaxValue; - isLower = true; - isAscii = true; - while( true ) - { - var value = *tmp; - if( value == 0 ) - { - break; - } - - if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) - { - isLower = false; - } - - if( value > 0x80 ) - { - isAscii = false; - } - - crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); - ++tmp; - } - - var size = ( int )( tmp - ptr ); - crc32Ret = ( int )~crc32; - return size; - } - - [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe IntPtr memcpy( void* dest, void* src, int count ); - - public static unsafe void MemCpyUnchecked( void* dest, void* src, int count ) - => memcpy( dest, src, count ); - - - [DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memcmp( void* b1, void* b2, int count ); - - public static unsafe int MemCmpUnchecked( void* ptr1, void* ptr2, int count ) - => memcmp( ptr1, ptr2, count ); - - - [DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe int memicmp( void* b1, void* b2, int count ); - - public static unsafe int MemCmpCaseInsensitiveUnchecked( void* ptr1, void* ptr2, int count ) - => memicmp( ptr1, ptr2, count ); - - [DllImport( "msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] - private static extern unsafe void* memset( void* dest, int c, int count ); - - public static unsafe void* MemSet( void* dest, byte value, int count ) - => memset( dest, value, count ); -} \ No newline at end of file diff --git a/Penumbra.String b/Penumbra.String new file mode 160000 index 00000000..f41af0fb --- /dev/null +++ b/Penumbra.String @@ -0,0 +1 @@ +Subproject commit f41af0fb88626f1579d3c4370b32b901f3c4d3c2 diff --git a/Penumbra.sln b/Penumbra.sln index 33e5a03d..5c11aaea 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -16,6 +16,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterG EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7ccad1ce..d6b0b51c 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -3,7 +3,6 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Mods; using System; using System.Collections.Generic; @@ -12,6 +11,8 @@ using System.Linq; using System.Numerics; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Api; @@ -536,7 +537,7 @@ public class IpcTester : IDisposable private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = IntPtr.Zero; } @@ -544,7 +545,7 @@ public class IpcTester : IDisposable private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = drawObject; } @@ -552,7 +553,7 @@ public class IpcTester : IDisposable private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) { var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastResolvedObject = obj != null ? new Utf8String( obj->GetName() ).ToString() : "Unknown"; + _lastResolvedObject = obj != null ? new ByteString( obj->GetName() ).ToString() : "Unknown"; _lastResolvedGamePath = gamePath; _lastResolvedFullPath = fullPath; _lastResolvedGamePathTime = DateTimeOffset.Now; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0484f986..8cc799f1 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -3,7 +3,6 @@ using Lumina.Data; using Newtonsoft.Json; using OtterGui; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -15,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; using Penumbra.Api.Enums; +using Penumbra.String.Classes; namespace Penumbra.Api; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 263b892f..bb41f6f1 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,11 +1,11 @@ using OtterGui; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Penumbra.String.Classes; namespace Penumbra.Api; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 75bfcef5..1e211076 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -143,8 +143,8 @@ public partial class ModCollection return false; } - _specialCollections[ ( int )collectionType ] = Empty; - CollectionChanged.Invoke( collectionType, null, Empty, null ); + _specialCollections[ ( int )collectionType ] = Default; + CollectionChanged.Invoke( collectionType, null, Default, null ); return true; } @@ -172,8 +172,8 @@ public partial class ModCollection return false; } - _characters[ characterName ] = Empty; - CollectionChanged.Invoke( CollectionType.Character, null, Empty, characterName ); + _characters[ characterName ] = Default; + CollectionChanged.Invoke( CollectionType.Character, null, Default, characterName ); return true; } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 5b636966..a4d1619c 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Meta.Manager; using Penumbra.Mods; @@ -8,8 +7,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 2d2262cc..acf3b9b6 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,6 +1,5 @@ using OtterGui; using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -8,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; using Penumbra.Api.Enums; +using Penumbra.GameData.Util; +using Penumbra.String.Classes; namespace Penumbra.Collections; @@ -240,7 +241,7 @@ public partial class ModCollection if( addMetaChanges ) { ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -413,7 +414,7 @@ public partial class ModCollection // Add the same conflict list to both conflict directions. var conflictList = new List< object > { data }; _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, - existingPriority != addedPriority ) ); + existingPriority != addedPriority ) ); _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority ) ); @@ -474,9 +475,9 @@ public partial class ModCollection // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = GameData.GameData.GetIdentifier(); - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( 'i', 'm', 'c' ) ) ) + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) { - foreach( var (name, obj) in identifier.Identify( resolved.ToGamePath() ) ) + foreach( var (name, obj) in identifier.Identify( new GamePath( resolved.ToString() ) ) ) { if( !_changedItems.TryGetValue( name, out var data ) ) { diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index b3df03f0..c54218d5 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -11,7 +10,7 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; using Penumbra.UI.Classes; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; diff --git a/Penumbra/Interop/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs index a439ac48..5aee657a 100644 --- a/Penumbra/Interop/CharacterUtility.DecalReverter.cs +++ b/Penumbra/Interop/CharacterUtility.DecalReverter.cs @@ -1,8 +1,8 @@ using System; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index f04f2930..ba2f2962 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,8 +7,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -19,7 +20,7 @@ public unsafe partial class ResourceLoader private readonly Hook< ResourceHandleDecRef > _decRefHook; public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); - + [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", DetourName = nameof( ResourceHandleDestructorDetour ) )] public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; @@ -28,7 +29,7 @@ public unsafe partial class ResourceLoader { if( handle != null ) { - Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})."); + Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})." ); } return ResourceHandleDestructorHook!.Original( handle ); @@ -248,7 +249,7 @@ public unsafe partial class ResourceLoader Penumbra.Log.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); } - private static void LogLoadedFile( Utf8String path, bool success, bool custom ) + private static void LogLoadedFile( ByteString path, bool success, bool custom ) => Penumbra.Log.Information( success ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}" : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index a0ca377f..8e28d2ce 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -7,9 +7,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -71,7 +72,7 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, Utf8String path ) + public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, ByteString path ) { var hash = path.Crc32; return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); @@ -209,7 +210,7 @@ public unsafe partial class ResourceLoader } // Load the resource from an SqPack and trigger the FileLoaded event. - private byte DefaultResourceLoad( Utf8String path, ResourceManager* resourceManager, + private byte DefaultResourceLoad( ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); @@ -218,7 +219,7 @@ public unsafe partial class ResourceLoader } // Load the resource from a path on the users hard drives. - private byte DefaultRootedResourceLoad( Utf8String gamePath, ResourceManager* resourceManager, + private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { // Specify that we are loading unpacked files from the drive. @@ -246,7 +247,7 @@ public unsafe partial class ResourceLoader } // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - internal byte DefaultLoadResource( Utf8String gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + internal byte DefaultLoadResource( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) => Utf8GamePath.IsRooted( gamePath ) ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) @@ -262,7 +263,7 @@ public unsafe partial class ResourceLoader _incRefHook.Dispose(); } - private static int ComputeHash( Utf8String path, GetResourceParameters* pGetResParams ) + private static int ComputeHash( ByteString path, GetResourceParameters* pGetResParams ) { if( pGetResParams == null || !pGetResParams->IsPartialRead ) { @@ -272,11 +273,11 @@ public unsafe partial class ResourceLoader // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - return Utf8String.Join( + return ByteString.Join( ( byte )'.', path, - Utf8String.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), - Utf8String.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) + ByteString.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), + ByteString.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) ).Crc32; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 67454300..415b18f7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -26,7 +26,7 @@ public unsafe partial class ResourceLoader // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof(CheckFileStateDetour) )] + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof( CheckFileStateDetour ) )] public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) @@ -48,7 +48,7 @@ public unsafe partial class ResourceLoader // We hook the extern functions to just return the local one if given the custom flag as last argument. public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof(LoadTexFileExternDetour) )] + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof( LoadTexFileExternDetour ) )] public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) @@ -59,7 +59,7 @@ public unsafe partial class ResourceLoader public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof(LoadMdlFileExternDetour) )] + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof( LoadMdlFileExternDetour ) )] public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 44d55214..de91fb2a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -3,9 +3,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -127,7 +128,7 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is newly loaded. // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) // custom is true if the file was loaded from local files instead of the default SqPacks. - public delegate void FileLoadedDelegate( Utf8String path, bool success, bool custom ); + public delegate void FileLoadedDelegate( ByteString path, bool success, bool custom ); public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. @@ -140,7 +141,7 @@ public unsafe partial class ResourceLoader : IDisposable // Customize file loading for any GamePaths that start with "|". // Same procedure as above. - public delegate bool ResourceLoadCustomizationDelegate( Utf8String split, Utf8String path, ResourceManager* resourceManager, + public delegate bool ResourceLoadCustomizationDelegate( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue ); public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization; diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs index 25d8ea59..dceaad36 100644 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ b/Penumbra/Interop/Loader/ResourceLogger.cs @@ -1,6 +1,7 @@ using System; using System.Text.RegularExpressions; -using Penumbra.GameData.ByteString; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; @@ -84,7 +85,7 @@ public class ResourceLogger : IDisposable // Returns the converted string if the filter matches, and null otherwise. // The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it. - private string? Match( Utf8String data ) + private string? Match( ByteString data ) { var s = data.ToString(); return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 9a38bf56..3744bb67 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -2,8 +2,8 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index aa6e7ed4..3742a51e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -8,8 +8,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -57,7 +57,7 @@ public unsafe partial class PathResolver { if( type == ResourceType.Tex && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l' ) ) + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) ) { resolveData = LastCreatedCollection; return true; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index ef09c616..ec975dfa 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -7,8 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.String; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -66,7 +66,7 @@ public unsafe partial class PathResolver } var block = data + 0x7A; - return new Utf8String( block ).ToString(); + return new ByteString( block ).ToString(); } // Obtain the name of the player character if the glamour plate edit window is open. @@ -130,7 +130,7 @@ public unsafe partial class PathResolver if( owner != null ) { - return new Utf8String( owner->Name ).ToString(); + return new ByteString( owner->Name ).ToString(); } return null; @@ -169,7 +169,7 @@ public unsafe partial class PathResolver if( Penumbra.Config.PreferNamedCollectionsOverOwners ) { // Early return if we prefer the actors own name over its owner. - actorName = new Utf8String( gameObject->Name ).ToString(); + actorName = new ByteString( gameObject->Name ).ToString(); if( actorName.Length > 0 && CollectionByActorName( actorName, out var actorCollection ) ) { @@ -189,7 +189,7 @@ public unsafe partial class PathResolver >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), _ => null, } - ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); + ?? GetOwnerName( gameObject ) ?? actorName ?? new ByteString( gameObject->Name ).ToString(); // First check temporary character collections, then the own configuration, then special collections. var collection = CollectionByActorName( actualName, out var c ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index baf333f2..90893c20 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,9 +4,10 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -78,7 +79,7 @@ public unsafe partial class PathResolver // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. - public bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + public bool MtrlLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; @@ -149,7 +150,7 @@ public unsafe partial class PathResolver } var mtrl = ( MtrlResource* )mtrlResourceHandle; - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); + var mtrlPath = ByteString.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); _mtrlData = _paths.TryGetValue( mtrlPath, out var c ) ? c : ResolveData.Invalid; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index a9bd1ee0..42c458c3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Dalamud.Utility.Signatures; using Penumbra.Collections; -using Penumbra.GameData.ByteString; +using Penumbra.String; namespace Penumbra.Interop.Resolver; @@ -31,7 +31,7 @@ public unsafe partial class PathResolver private readonly ResolverHooks _monster; // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< Utf8String, ResolveData > _pathCollections = new(); + private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); public PathState( PathResolver parent ) { @@ -69,13 +69,13 @@ public unsafe partial class PathResolver public int Count => _pathCollections.Count; - public IEnumerable< KeyValuePair< Utf8String, ResolveData > > Paths + public IEnumerable< KeyValuePair< ByteString, ResolveData > > Paths => _pathCollections; - public bool TryGetValue( Utf8String path, out ResolveData collection ) + public bool TryGetValue( ByteString path, out ResolveData collection ) => _pathCollections.TryGetValue( path, out collection ); - public bool Consume( Utf8String path, out ResolveData collection ) + public bool Consume( ByteString path, out ResolveData collection ) => _pathCollections.TryRemove( path, out collection ); // Just add or remove the resolved path. @@ -87,13 +87,13 @@ public unsafe partial class PathResolver return path; } - var gamePath = new Utf8String( ( byte* )path ); + var gamePath = new ByteString( ( byte* )path ); SetCollection( gameObject, gamePath, collection ); return path; } // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - public void SetCollection( IntPtr gameObject, Utf8String path, ModCollection collection ) + public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection ) { if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 17311ed3..c2f4f3ef 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,9 +5,10 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Resolver; @@ -151,7 +152,7 @@ public partial class PathResolver : IDisposable return resolveData; } - internal IEnumerable< KeyValuePair< Utf8String, ResolveData > > PathCollections + internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections => _paths.Paths; internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 0f308bd6..5a4919bf 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using System.Collections.Generic; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -23,7 +24,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public override void Reset() - => Functions.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); + => MemoryUtility.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); public void Reset( IEnumerable< (SubRace, RspAttribute) > entries ) { diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index d9a22c41..5290fb04 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -63,7 +64,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public override void Reset() { var def = ( byte* )DefaultData.Data; - Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); + MemoryUtility.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); var dataBasePtr = controlPtr + BlockCount; @@ -73,18 +74,18 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile { if( controlPtr[ i ] == CollapsedBlock ) { - Functions.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); + MemoryUtility.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); } else { - Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); + MemoryUtility.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); } myControlPtr[ i ] = ( ushort )( i * BlockSize ); myDataPtr += BlockSize; } - Functions.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); + MemoryUtility.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); } public void Reset( IEnumerable< int > entries ) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 60649952..5d15031b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -5,6 +5,7 @@ using System.Numerics; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -49,7 +50,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile protected virtual void SetEmptyBlock( int idx ) { - Functions.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); + MemoryUtility.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); } public sealed override void Reset() @@ -62,7 +63,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; if( !collapsed ) { - Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); + MemoryUtility.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); expandedBlocks++; } else diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 7c6b3591..5c76ff71 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -170,8 +171,8 @@ public sealed unsafe class EstFile : MetaBaseFile { var (d, length) = DefaultData; var data = ( byte* )d; - Functions.MemCpyUnchecked( Data, data, length ); - Functions.MemSet( Data + length, 0, Length - length ); + MemoryUtility.MemCpyUnchecked( Data, data, length ); + MemoryUtility.MemSet( Data + length, 0, Length - length ); } public EstFile( EstManipulation.EstType estType ) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index f1309e64..8f36507e 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,11 +1,12 @@ using System; using System.Numerics; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; +using OtterGui; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; +using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -150,7 +151,7 @@ public unsafe class ImcFile : MetaBaseFile var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); for( var i = oldCount + 1; i < numVariants + 1; ++i ) { - Functions.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); + MemoryUtility.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); } Penumbra.Log.Verbose( $"Expanded IMC {Path} from {oldCount} to {numVariants} variants." ); @@ -188,8 +189,8 @@ public unsafe class ImcFile : MetaBaseFile var file = Dalamud.GameData.GetFile( Path.ToString() ); fixed( byte* ptr = file!.Data ) { - Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); - Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); + MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); + MemoryUtility.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); } } @@ -207,7 +208,7 @@ public unsafe class ImcFile : MetaBaseFile { NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); AllocateData( file.Data.Length ); - Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); + MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); } } @@ -243,7 +244,7 @@ public unsafe class ImcFile : MetaBaseFile return; } - Functions.MemCpyUnchecked( newData, Data, ActualLength ); + MemoryUtility.MemCpyUnchecked( newData, Data, ActualLength ); Penumbra.MetaFileManager.Free( data, length ); resource->SetData( ( IntPtr )newData, ActualLength ); diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 8a167216..dbe8a35b 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Memory; using Penumbra.GameData.Util; +using Penumbra.String.Functions; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.Meta.Files; @@ -57,12 +58,12 @@ public unsafe class MetaBaseFile : IDisposable var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); if( newLength > Length ) { - Functions.MemCpyUnchecked( data, Data, Length ); - Functions.MemSet( data + Length, 0, newLength - Length ); + MemoryUtility.MemCpyUnchecked( data, Data, Length ); + MemoryUtility.MemSet( data + Length, 0, newLength - Length ); } else { - Functions.MemCpyUnchecked( data, Data, newLength ); + MemoryUtility.MemCpyUnchecked( data, Data, newLength ); } ReleaseUnmanagedResources(); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 5887aba2..d61f158f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Filesystem; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Meta.Manager; @@ -149,7 +150,7 @@ public partial class MetaManager => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); - private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + private static unsafe bool ImcLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 5bd505df..682fe3af 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -2,10 +2,10 @@ using System; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.String.Classes; namespace Penumbra.Meta.Manipulations; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 4380e0a0..9029800a 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.String.Functions; namespace Penumbra.Meta.Manipulations; @@ -217,7 +218,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa { fixed( MetaManipulation* lhs = &this ) { - return Functions.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); + return MemoryUtility.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index b0a136af..d60356ac 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -124,7 +124,7 @@ public partial class Mod foreach( var file in files ) { // Skip any UI Files because deduplication causes weird crashes for those. - if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( 'u', 'i', '/' ) ) ) + if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( "ui/"u8 ) ) ) { continue; } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 29f5389b..c2221ecc 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 114359db..583442fd 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 3061d390..45c6707c 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using OtterGui; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 704d1285..82e171eb 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -4,8 +4,8 @@ using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index d2996b71..1057bf0d 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Penumbra.GameData.Util; namespace Penumbra.Mods; @@ -14,7 +15,7 @@ public sealed partial class Mod ChangedItems.Clear(); foreach( var gamePath in AllRedirects ) { - identifier.Identify( ChangedItems, gamePath.ToGamePath() ); + identifier.Identify( ChangedItems, new GamePath(gamePath.ToString()) ); } // TODO: manipulations diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 699fceaa..a566b795 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -6,8 +6,8 @@ using Dalamud.Utility; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Import; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index b09f680c..a12b29fc 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -5,8 +5,8 @@ using System.Linq; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 8d907be9..fa16f4c2 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.GameData.ByteString; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index c41dcf57..802852e9 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -59,7 +59,7 @@ public sealed partial class Mod var defaultMod = mod._default; foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) { - if( gamePath.Path.EndsWith( '.', 'i', 'm', 'c' ) ) + if( gamePath.Path.EndsWith( ".imc"u8 ) ) { continue; } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 01bfefc6..89bf2053 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.IO; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 47c777cd..883f74e6 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -4,9 +4,9 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Penumbra.GameData.ByteString; using Penumbra.Import; using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 791bac47..e0ed235b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; @@ -21,6 +22,8 @@ using Penumbra.Interop; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; @@ -55,6 +58,8 @@ public class Penumbra : IDalamudPlugin public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; + public static readonly List< Exception > ImcExceptions = new(); public readonly ResourceLogger ResourceLogger; @@ -98,6 +103,7 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); + Actors = new ActorManager( Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -144,7 +150,7 @@ public class Penumbra : IDalamudPlugin { Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); } - + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); @@ -283,6 +289,8 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + Dalamud.PluginInterface.RelinquishData( "test1" ); + Framework?.Dispose(); ShutdownWebServer(); DisposeInterface(); IpcProviders?.Dispose(); @@ -290,7 +298,6 @@ public class Penumbra : IDalamudPlugin ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); - Framework?.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 42727936..d8c13c81 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -9,10 +9,10 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; using Penumbra.Mods; -using Functions = Penumbra.GameData.Util.Functions; +using Penumbra.String.Classes; +using Penumbra.String.Functions; namespace Penumbra.UI.Classes; @@ -459,14 +459,14 @@ public partial class ModEditWindow ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; fixed( void* ptr = data, output = &rows ) { - Functions.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() && file.ColorDyeSets.Length > colorSetIdx ) { ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; fixed( void* output2 = &dyeRows ) { - Functions.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); } } } @@ -489,8 +489,8 @@ public partial class ModEditWindow var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; fixed( byte* ptr = data ) { - Functions.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - Functions.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); } var text = Convert.ToBase64String( data ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index dff8eaa6..9b9c16d2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -7,9 +7,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Util; using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 36aae29d..8a00234a 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -8,11 +8,11 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Mods; +using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.Mods.Mod; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index f5ffdcf9..10311595 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -32,6 +32,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod SubscribeRightClickFolder( InheritDescendants, 15 ); SubscribeRightClickFolder( OwnDescendants, 15 ); SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); + SubscribeRightClickLeaf( ToggleLeafFavorite, 0 ); SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); AddButton( AddNewModButton, 0 ); AddButton( AddImportModButton, 1 ); @@ -117,9 +118,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) { - var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); - using var id = ImRaii.PushId( leaf.Value.Index ); + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ) + .Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite ); + using var id = ImRaii.PushId( leaf.Value.Index ); ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); } @@ -157,6 +159,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + private static void ToggleLeafFavorite( FileSystem< Mod >.Leaf mod ) + { + if( ImGui.MenuItem( mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite" ) ) + { + Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite ); + } + } + private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) { if( ImGui.MenuItem( "Set As Default Import Folder" ) ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index c9cf6284..0d64c1b8 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,9 +9,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; +using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; @@ -56,6 +56,8 @@ public partial class ConfigWindow ImGui.NewLine(); DrawPathResolverDebug(); ImGui.NewLine(); + DrawActorsDebug(); + ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); DrawDebugTabMetaLists(); @@ -148,6 +150,31 @@ public partial class ConfigWindow } } + private static unsafe void DrawActorsDebug() + { + if( !ImGui.CollapsingHeader( "Actors" ) ) + { + return; + } + + using var table = ImRaii.Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX ); + if( !table ) + { + return; + } + + foreach( var obj in Dalamud.Objects ) + { + ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); + ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); + var identifier = Penumbra.Actors.FromObject( obj ); + ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); + ImGuiUtil.DrawTableColumn( identifier.DataId.ToString() ); + + } + } + // Draw information about which draw objects correspond to which game objects // and which paths are due to be loaded by which collection. private unsafe void DrawPathResolverDebug() @@ -173,7 +200,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); var (address, name) = - obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); + obj != null ? ( $"0x{( ulong )obj:X}", new ByteString( obj->Name ).ToString() ) : ( "NULL", "NULL" ); ImGui.TextUnformatted( address ); ImGui.TableNextColumn(); ImGui.TextUnformatted( name ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index d096d3ee..a27307d9 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -7,9 +7,9 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index e767f755..bf4f44b6 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -9,17 +9,16 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.String; using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { - // Draw text given by a Utf8String. - internal static unsafe void Text( Utf8String s ) + // Draw text given by a ByteString. + internal static unsafe void Text( ByteString s ) => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); // Draw text given by a byte pointer. @@ -30,8 +29,8 @@ public partial class ConfigWindow private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); - // Draw a Utf8String as a selectable. - internal static unsafe bool Selectable( Utf8String s, bool selected ) + // Draw a ByteString as a selectable. + internal static unsafe bool Selectable( ByteString s, bool selected ) { var tmp = ( byte )( selected ? 1 : 0 ); return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0; @@ -77,8 +76,8 @@ public partial class ConfigWindow } // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, - // using an Utf8String. - private static unsafe void CopyOnClickSelectable( Utf8String text ) + // using an ByteString. + private static unsafe void CopyOnClickSelectable( ByteString text ) { if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 5fc8dcea..28015b79 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -6,9 +6,10 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.String; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -32,11 +33,11 @@ public partial class ConfigWindow private Tabs _availableTabs = 0; // Required to use tabs that can not be closed but have a flag to set them open. - private static readonly Utf8String ConflictTabHeader = Utf8String.FromStringUnsafe( "Conflicts", false ); - 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", false ); + private static readonly ByteString ConflictTabHeader = ByteString.FromSpanUnsafe( "Conflicts"u8, true, false, true ); + private static readonly ByteString DescriptionTabHeader = ByteString.FromSpanUnsafe( "Description"u8, true, false, true ); + private static readonly ByteString SettingsTabHeader = ByteString.FromSpanUnsafe( "Settings"u8, true, false, true ); + private static readonly ByteString ChangedItemsTabHeader = ByteString.FromSpanUnsafe( "Changed Items"u8, true, false, true ); + private static readonly ByteString EditModTabHeader = ByteString.FromSpanUnsafe( "Edit Mod"u8, true, false, true ); private readonly TagButtons _modTags = new(); @@ -147,6 +148,7 @@ public partial class ConfigWindow { Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); } + if( _mod.ModTags.Count > 0 ) { _modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false, @@ -226,7 +228,7 @@ public partial class ConfigWindow // Draw a tab by given name if it is available, and deal with changing the preferred tab. - private ImRaii.IEndObject DrawTab( Utf8String name, Tabs flag ) + private ImRaii.IEndObject DrawTab( ByteString name, Tabs flag ) { if( !_availableTabs.HasFlag( flag ) ) { diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 801878cf..ad128ee9 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -8,8 +8,8 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; +using Penumbra.String.Classes; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index bf8c0de9..2d47e290 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -3,8 +3,8 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.ByteString; using Penumbra.Interop; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index b68a4adc..38fcd25f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -67,8 +67,12 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.0, )" + "Penumbra.Api": "[1.0.0, )", + "Penumbra.String": "[1.0.0, )" } + }, + "penumbra.string": { + "type": "Project" } } } From 3c0cdc3d0a9ac3048bcb129dd67346c91af438a7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Oct 2022 16:02:18 +0200 Subject: [PATCH 0555/2451] Oops. --- tmp/.editorconfig | 85 ---- tmp/.gitignore | 3 - tmp/Delegates.cs | 16 - tmp/Enums/ChangedItemType.cs | 9 - tmp/Enums/GroupType.cs | 7 - tmp/Enums/ModSettingChange.cs | 12 - tmp/Enums/MouseButton.cs | 9 - tmp/Enums/PenumbraApiEc.cs | 20 - tmp/Enums/RedrawType.cs | 7 - tmp/Helpers/ActionProvider.cs | 66 --- tmp/Helpers/ActionSubscriber.cs | 54 --- tmp/Helpers/EventProvider.cs | 376 ----------------- tmp/Helpers/EventSubscriber.cs | 607 ---------------------------- tmp/Helpers/FuncProvider.cs | 186 --------- tmp/Helpers/FuncSubscriber.cs | 163 -------- tmp/IPenumbraApi.cs | 250 ------------ tmp/IPenumbraApiBase.cs | 11 - tmp/Ipc/Collection.cs | 76 ---- tmp/Ipc/Configuration.cs | 42 -- tmp/Ipc/GameState.cs | 63 --- tmp/Ipc/Meta.cs | 30 -- tmp/Ipc/ModSettings.cs | 116 ------ tmp/Ipc/Mods.cs | 81 ---- tmp/Ipc/PluginState.cs | 68 ---- tmp/Ipc/Redraw.cs | 65 --- tmp/Ipc/Resolve.cs | 74 ---- tmp/Ipc/Temporary.cs | 83 ---- tmp/Ipc/Ui.cs | 55 --- tmp/Penumbra.Api.csproj | 47 --- tmp/Penumbra.Api.csproj.DotSettings | 2 - tmp/README.md | 4 - 31 files changed, 2687 deletions(-) delete mode 100644 tmp/.editorconfig delete mode 100644 tmp/.gitignore delete mode 100644 tmp/Delegates.cs delete mode 100644 tmp/Enums/ChangedItemType.cs delete mode 100644 tmp/Enums/GroupType.cs delete mode 100644 tmp/Enums/ModSettingChange.cs delete mode 100644 tmp/Enums/MouseButton.cs delete mode 100644 tmp/Enums/PenumbraApiEc.cs delete mode 100644 tmp/Enums/RedrawType.cs delete mode 100644 tmp/Helpers/ActionProvider.cs delete mode 100644 tmp/Helpers/ActionSubscriber.cs delete mode 100644 tmp/Helpers/EventProvider.cs delete mode 100644 tmp/Helpers/EventSubscriber.cs delete mode 100644 tmp/Helpers/FuncProvider.cs delete mode 100644 tmp/Helpers/FuncSubscriber.cs delete mode 100644 tmp/IPenumbraApi.cs delete mode 100644 tmp/IPenumbraApiBase.cs delete mode 100644 tmp/Ipc/Collection.cs delete mode 100644 tmp/Ipc/Configuration.cs delete mode 100644 tmp/Ipc/GameState.cs delete mode 100644 tmp/Ipc/Meta.cs delete mode 100644 tmp/Ipc/ModSettings.cs delete mode 100644 tmp/Ipc/Mods.cs delete mode 100644 tmp/Ipc/PluginState.cs delete mode 100644 tmp/Ipc/Redraw.cs delete mode 100644 tmp/Ipc/Resolve.cs delete mode 100644 tmp/Ipc/Temporary.cs delete mode 100644 tmp/Ipc/Ui.cs delete mode 100644 tmp/Penumbra.Api.csproj delete mode 100644 tmp/Penumbra.Api.csproj.DotSettings delete mode 100644 tmp/README.md diff --git a/tmp/.editorconfig b/tmp/.editorconfig deleted file mode 100644 index 238bb1dc..00000000 --- a/tmp/.editorconfig +++ /dev/null @@ -1,85 +0,0 @@ - -[*] -charset=utf-8 -end_of_line=lf -trim_trailing_whitespace=true -insert_final_newline=false -indent_style=space -indent_size=4 - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers=false -csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_prefer_braces=true:none -csharp_space_after_cast=false -csharp_space_after_keywords_in_control_flow_statements=false -csharp_space_between_method_call_parameter_list_parentheses=true -csharp_space_between_method_declaration_parameter_list_parentheses=true -csharp_space_between_parentheses=control_flow_statements,expressions,type_casts -csharp_style_var_elsewhere=true:suggestion -csharp_style_var_for_built_in_types=true:suggestion -csharp_style_var_when_type_is_apparent=true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion -dotnet_style_predefined_type_for_member_access=true:suggestion -dotnet_style_qualification_for_event=false:suggestion -dotnet_style_qualification_for_field=false:suggestion -dotnet_style_qualification_for_method=false:suggestion -dotnet_style_qualification_for_property=false:suggestion -dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion - -# ReSharper properties -resharper_align_multiline_binary_expressions_chain=false -resharper_align_multiline_calls_chain=false -resharper_autodetect_indent_settings=true -resharper_braces_redundant=true -resharper_constructor_or_destructor_body=expression_body -resharper_csharp_empty_block_style=together -resharper_csharp_max_line_length=180 -resharper_csharp_space_within_array_access_brackets=true -resharper_enforce_line_ending_style=true -resharper_int_align_assignments=true -resharper_int_align_comments=true -resharper_int_align_fields=true -resharper_int_align_invocations=false -resharper_int_align_nested_ternary=true -resharper_int_align_properties=false -resharper_int_align_switch_expressions=true -resharper_int_align_switch_sections=true -resharper_int_align_variables=true -resharper_local_function_body=expression_body -resharper_method_or_operator_body=expression_body -resharper_place_attribute_on_same_line=false -resharper_space_after_cast=false -resharper_space_within_checked_parentheses=true -resharper_space_within_default_parentheses=true -resharper_space_within_nameof_parentheses=true -resharper_space_within_single_line_array_initializer_braces=true -resharper_space_within_sizeof_parentheses=true -resharper_space_within_typeof_parentheses=true -resharper_space_within_type_argument_angles=true -resharper_space_within_type_parameter_angles=true -resharper_use_indent_from_vs=false -resharper_wrap_lines=true - -# ReSharper inspection severities -resharper_arrange_redundant_parentheses_highlighting=hint -resharper_arrange_this_qualifier_highlighting=hint -resharper_arrange_type_member_modifiers_highlighting=hint -resharper_arrange_type_modifiers_highlighting=hint -resharper_built_in_type_reference_style_for_member_access_highlighting=hint -resharper_built_in_type_reference_style_highlighting=hint -resharper_redundant_base_qualifier_highlighting=warning -resharper_suggest_var_or_type_built_in_types_highlighting=hint -resharper_suggest_var_or_type_elsewhere_highlighting=hint -resharper_suggest_var_or_type_simple_types_highlighting=hint -resharper_web_config_module_not_resolved_highlighting=warning -resharper_web_config_type_not_resolved_highlighting=warning -resharper_web_config_wrong_module_highlighting=warning - -[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] -indent_style=space -indent_size=4 -tab_width=4 diff --git a/tmp/.gitignore b/tmp/.gitignore deleted file mode 100644 index 3e168525..00000000 --- a/tmp/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -.vs/ \ No newline at end of file diff --git a/tmp/Delegates.cs b/tmp/Delegates.cs deleted file mode 100644 index 97726e81..00000000 --- a/tmp/Delegates.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Penumbra.Api.Enums; - -namespace Penumbra.Api; - -// Delegates used by different events. -public delegate void ChangedItemHover( object? item ); -public delegate void ChangedItemClick( MouseButton button, object? item ); -public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); -public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); - -public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, - IntPtr equipData ); - -public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, string collectionName, IntPtr drawObject ); -public delegate void GameObjectResourceResolvedDelegate( IntPtr gameObject, string gamePath, string localPath ); \ No newline at end of file diff --git a/tmp/Enums/ChangedItemType.cs b/tmp/Enums/ChangedItemType.cs deleted file mode 100644 index 5cc1b5a7..00000000 --- a/tmp/Enums/ChangedItemType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Penumbra.Api.Enums; - -public enum ChangedItemType -{ - None, - Item, - Action, - Customization, -} \ No newline at end of file diff --git a/tmp/Enums/GroupType.cs b/tmp/Enums/GroupType.cs deleted file mode 100644 index 65a8ed39..00000000 --- a/tmp/Enums/GroupType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Penumbra.Api.Enums; - -public enum GroupType -{ - Single, - Multi, -} \ No newline at end of file diff --git a/tmp/Enums/ModSettingChange.cs b/tmp/Enums/ModSettingChange.cs deleted file mode 100644 index 5e556d50..00000000 --- a/tmp/Enums/ModSettingChange.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Penumbra.Api.Enums; - -// Different types a mod setting can change: -public enum ModSettingChange -{ - Inheritance, // it was set to inherit from other collections or not inherit anymore - EnableState, // it was enabled or disabled - Priority, // its priority was changed - Setting, // a specific setting was changed - MultiInheritance, // multiple mods were set to inherit from other collections or not inherit anymore. - MultiEnableState, // multiple mods were enabled or disabled at once. -} \ No newline at end of file diff --git a/tmp/Enums/MouseButton.cs b/tmp/Enums/MouseButton.cs deleted file mode 100644 index 2917c0f8..00000000 --- a/tmp/Enums/MouseButton.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Penumbra.Api.Enums; - -public enum MouseButton -{ - None, - Left, - Right, - Middle, -} \ No newline at end of file diff --git a/tmp/Enums/PenumbraApiEc.cs b/tmp/Enums/PenumbraApiEc.cs deleted file mode 100644 index a37aefa8..00000000 --- a/tmp/Enums/PenumbraApiEc.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Penumbra.Api.Enums; - -public enum PenumbraApiEc -{ - Success = 0, - NothingChanged = 1, - CollectionMissing = 2, - ModMissing = 3, - OptionGroupMissing = 4, - OptionMissing = 5, - - CharacterCollectionExists = 6, - LowerPriority = 7, - InvalidGamePath = 8, - FileMissing = 9, - InvalidManipulation = 10, - InvalidArgument = 11, - PathRenameFailed = 12, - UnknownError = 255, -} \ No newline at end of file diff --git a/tmp/Enums/RedrawType.cs b/tmp/Enums/RedrawType.cs deleted file mode 100644 index 0295554f..00000000 --- a/tmp/Enums/RedrawType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Penumbra.Api.Enums; - -public enum RedrawType -{ - Redraw, - AfterGPose, -} \ No newline at end of file diff --git a/tmp/Helpers/ActionProvider.cs b/tmp/Helpers/ActionProvider.cs deleted file mode 100644 index c070dca8..00000000 --- a/tmp/Helpers/ActionProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; - -namespace Penumbra.Api.Helpers; - -public sealed class ActionProvider : IDisposable -{ - private ICallGateProvider? _provider; - - public ActionProvider( DalamudPluginInterface pi, string label, Action action ) - { - try - { - _provider = pi.GetIpcProvider( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterAction( action ); - } - - public void Dispose() - { - _provider?.UnregisterAction(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~ActionProvider() - => Dispose(); -} - -public sealed class ActionProvider< T1, T2 > : IDisposable -{ - private ICallGateProvider< T1, T2, object? >? _provider; - - public ActionProvider( DalamudPluginInterface pi, string label, Action< T1, T2 > action ) - { - try - { - _provider = pi.GetIpcProvider< T1, T2, object? >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterAction( action ); - } - - public void Dispose() - { - _provider?.UnregisterAction(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~ActionProvider() - => Dispose(); -} \ No newline at end of file diff --git a/tmp/Helpers/ActionSubscriber.cs b/tmp/Helpers/ActionSubscriber.cs deleted file mode 100644 index e924e4eb..00000000 --- a/tmp/Helpers/ActionSubscriber.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; - -namespace Penumbra.Api.Helpers; - -public readonly struct ActionSubscriber< T1 > -{ - private readonly ICallGateSubscriber< T1, object? >? _subscriber; - - public bool Valid - => _subscriber != null; - - public ActionSubscriber( DalamudPluginInterface pi, string label ) - { - try - { - _subscriber = pi.GetIpcSubscriber< T1, object? >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Invoke( T1 a ) - => _subscriber?.InvokeAction( a ); -} - -public readonly struct ActionSubscriber< T1, T2 > -{ - private readonly ICallGateSubscriber< T1, T2, object? >? _subscriber; - - public bool Valid - => _subscriber != null; - - public ActionSubscriber( DalamudPluginInterface pi, string label ) - { - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, object? >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Invoke( T1 a, T2 b ) - => _subscriber?.InvokeAction( a, b ); -} \ No newline at end of file diff --git a/tmp/Helpers/EventProvider.cs b/tmp/Helpers/EventProvider.cs deleted file mode 100644 index b623d41e..00000000 --- a/tmp/Helpers/EventProvider.cs +++ /dev/null @@ -1,376 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; - -namespace Penumbra.Api.Helpers; - -public sealed class EventProvider : IDisposable -{ - private ICallGateProvider< object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, (Action< Action > Add, Action< Action > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke() - => _provider?.SendMessage(); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} - -public sealed class EventProvider< T1 > : IDisposable -{ - private ICallGateProvider< T1, object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, (Action< Action< T1 > > Add, Action< Action< T1 > > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke( T1 a ) - => _provider?.SendMessage( a ); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action< T1 > > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} - -public sealed class EventProvider< T1, T2 > : IDisposable -{ - private ICallGateProvider< T1, T2, object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, - (Action< Action< T1, T2 > > Add, Action< Action< T1, T2 > > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke( T1 a, T2 b ) - => _provider?.SendMessage( a, b ); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action< T1, T2 > > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} - -public sealed class EventProvider< T1, T2, T3 > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, - (Action< Action< T1, T2, T3 > > Add, Action< Action< T1, T2, T3 > > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke( T1 a, T2 b, T3 c ) - => _provider?.SendMessage( a, b, c ); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action< T1, T2, T3 > > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} - -public sealed class EventProvider< T1, T2, T3, T4 > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, T4, object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, - (Action< Action< T1, T2, T3, T4 > > Add, Action< Action< T1, T2, T3, T4 > > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke( T1 a, T2 b, T3 c, T4 d ) - => _provider?.SendMessage( a, b, c, d ); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action< T1, T2, T3, T4 > > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} - -public sealed class EventProvider< T1, T2, T3, T4, T5 > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, T4, T5, object? >? _provider; - private Delegate? _unsubscriber; - - public EventProvider( DalamudPluginInterface pi, string label, - (Action< Action< T1, T2, T3, T4, T5 > > Add, Action< Action< T1, T2, T3, T4, T5 > > Del)? subscribe = null ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, object? >( label ); - subscribe?.Add( Invoke ); - _unsubscriber = subscribe?.Del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) - { - _unsubscriber = null; - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, object? >( label ); - add(); - _unsubscriber = del; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - } - - public void Invoke( T1 a, T2 b, T3 c, T4 d, T5 e ) - => _provider?.SendMessage( a, b, c, d, e ); - - public void Dispose() - { - switch( _unsubscriber ) - { - case Action< Action< T1, T2, T3, T4, T5 > > a: - a( Invoke ); - break; - case Action b: - b(); - break; - } - - _unsubscriber = null; - _provider = null; - GC.SuppressFinalize( this ); - } - - ~EventProvider() - => Dispose(); -} \ No newline at end of file diff --git a/tmp/Helpers/EventSubscriber.cs b/tmp/Helpers/EventSubscriber.cs deleted file mode 100644 index 0df6bc11..00000000 --- a/tmp/Helpers/EventSubscriber.cs +++ /dev/null @@ -1,607 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; - -namespace Penumbra.Api.Helpers; - -public sealed class EventSubscriber : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action, Action > _delegates = new(); - private ICallGateSubscriber< object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action() - { - try - { - value(); - } - catch( Exception e ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} - -public sealed class EventSubscriber< T1 > : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action< T1 >, Action< T1 > > _delegates = new(); - private ICallGateSubscriber< T1, object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1 >[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action< T1 > Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action( T1 a ) - { - try - { - value( a ); - } - catch( Exception e ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} - -public sealed class EventSubscriber< T1, T2 > : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action< T1, T2 >, Action< T1, T2 > > _delegates = new(); - private ICallGateSubscriber< T1, T2, object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2 >[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action< T1, T2 > Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action( T1 a, T2 b ) - { - try - { - value( a, b ); - } - catch( Exception e ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} - -public sealed class EventSubscriber< T1, T2, T3 > : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action< T1, T2, T3 >, Action< T1, T2, T3 > > _delegates = new(); - private ICallGateSubscriber< T1, T2, T3, object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3 >[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action< T1, T2, T3 > Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action( T1 a, T2 b, T3 c ) - { - try - { - value( a, b, c ); - } - catch( Exception e ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} - -public sealed class EventSubscriber< T1, T2, T3, T4 > : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action< T1, T2, T3, T4 >, Action< T1, T2, T3, T4 > > _delegates = new(); - private ICallGateSubscriber< T1, T2, T3, T4, object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3, T4 >[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action< T1, T2, T3, T4 > Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action( T1 a, T2 b, T3 c, T4 d ) - { - try - { - value( a, b, c, d ); - } - catch( Exception e ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} - -public sealed class EventSubscriber< T1, T2, T3, T4, T5 > : IDisposable -{ - private readonly string _label; - private readonly Dictionary< Action< T1, T2, T3, T4, T5 >, Action< T1, T2, T3, T4, T5 > > _delegates = new(); - private ICallGateSubscriber< T1, T2, T3, T4, T5, object? >? _subscriber; - private bool _disabled; - - public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3, T4, T5 >[] actions ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, T5, object? >( label ); - foreach( var action in actions ) - { - Event += action; - } - - _disabled = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public void Enable() - { - if( _disabled && _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Subscribe( action ); - } - - _disabled = false; - } - } - - public void Disable() - { - if( !_disabled ) - { - if( _subscriber != null ) - { - foreach( var action in _delegates.Keys ) - { - _subscriber.Unsubscribe( action ); - } - } - - _disabled = true; - } - } - - public event Action< T1, T2, T3, T4, T5 > Event - { - add - { - if( _subscriber != null && !_delegates.ContainsKey( value ) ) - { - void Action( T1 a, T2 b, T3 c, T4 d, T5 e ) - { - try - { - value( a, b, c, d, e ); - } - catch( Exception ex ) - { - PluginLog.Error( $"Exception invoking IPC event {_label}:\n{ex}" ); - } - } - - if( _delegates.TryAdd( value, Action ) && !_disabled ) - { - _subscriber.Subscribe( Action ); - } - } - } - remove - { - if( _subscriber != null && _delegates.Remove( value, out var action ) ) - { - _subscriber.Unsubscribe( action ); - } - } - } - - public void Dispose() - { - Disable(); - _subscriber = null; - _delegates.Clear(); - } - - ~EventSubscriber() - => Dispose(); -} \ No newline at end of file diff --git a/tmp/Helpers/FuncProvider.cs b/tmp/Helpers/FuncProvider.cs deleted file mode 100644 index fac61ce3..00000000 --- a/tmp/Helpers/FuncProvider.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; - -namespace Penumbra.Api.Helpers; - -public sealed class FuncProvider< TRet > : IDisposable -{ - private ICallGateProvider< TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} - -public sealed class FuncProvider< T1, TRet > : IDisposable -{ - private ICallGateProvider< T1, TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< T1, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} - -public sealed class FuncProvider< T1, T2, TRet > : IDisposable -{ - private ICallGateProvider< T1, T2, TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< T1, T2, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} - -public sealed class FuncProvider< T1, T2, T3, TRet > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} - -public sealed class FuncProvider< T1, T2, T3, T4, TRet > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, T4, TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, T4, TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} - -public sealed class FuncProvider< T1, T2, T3, T4, T5, TRet > : IDisposable -{ - private ICallGateProvider< T1, T2, T3, T4, T5, TRet >? _provider; - - public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, T4, T5, TRet > func ) - { - try - { - _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); - _provider = null; - } - - _provider?.RegisterFunc( func ); - } - - public void Dispose() - { - _provider?.UnregisterFunc(); - _provider = null; - GC.SuppressFinalize( this ); - } - - ~FuncProvider() - => Dispose(); -} \ No newline at end of file diff --git a/tmp/Helpers/FuncSubscriber.cs b/tmp/Helpers/FuncSubscriber.cs deleted file mode 100644 index 3f8cbdda..00000000 --- a/tmp/Helpers/FuncSubscriber.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; -using Dalamud.Plugin.Ipc.Exceptions; - -namespace Penumbra.Api.Helpers; - -public readonly struct FuncSubscriber< TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke() - => _subscriber != null ? _subscriber.InvokeFunc() : throw new IpcNotReadyError( _label ); -} - -public readonly struct FuncSubscriber< T1, TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< T1, TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke( T1 a ) - => _subscriber != null ? _subscriber.InvokeFunc( a ) : throw new IpcNotReadyError( _label ); -} - -public readonly struct FuncSubscriber< T1, T2, TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< T1, T2, TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke( T1 a, T2 b ) - => _subscriber != null ? _subscriber.InvokeFunc( a, b ) : throw new IpcNotReadyError( _label ); -} - -public readonly struct FuncSubscriber< T1, T2, T3, TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< T1, T2, T3, TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke( T1 a, T2 b, T3 c ) - => _subscriber != null ? _subscriber.InvokeFunc( a, b, c ) : throw new IpcNotReadyError( _label ); -} - -public readonly struct FuncSubscriber< T1, T2, T3, T4, TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< T1, T2, T3, T4, TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke( T1 a, T2 b, T3 c, T4 d ) - => _subscriber != null ? _subscriber.InvokeFunc( a, b, c, d ) : throw new IpcNotReadyError( _label ); -} - -public readonly struct FuncSubscriber< T1, T2, T3, T4, T5, TRet > -{ - private readonly string _label; - private readonly ICallGateSubscriber< T1, T2, T3, T4, T5, TRet >? _subscriber; - - public bool Valid - => _subscriber != null; - - public FuncSubscriber( DalamudPluginInterface pi, string label ) - { - _label = label; - try - { - _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, T5, TRet >( label ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); - _subscriber = null; - } - } - - public TRet Invoke( T1 a, T2 b, T3 c, T4 d, T5 e ) - => _subscriber != null ? _subscriber.InvokeFunc( a, b, c, d, e ) : throw new IpcNotReadyError( _label ); -} \ No newline at end of file diff --git a/tmp/IPenumbraApi.cs b/tmp/IPenumbraApi.cs deleted file mode 100644 index 51860312..00000000 --- a/tmp/IPenumbraApi.cs +++ /dev/null @@ -1,250 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Lumina.Data; -using System; -using System.Collections.Generic; -using Penumbra.Api.Enums; - -namespace Penumbra.Api; - -public interface IPenumbraApi : IPenumbraApiBase -{ - #region Game State - - // Obtain the currently set mod directory from the configuration. - public string GetModDirectory(); - - // Obtain the entire current penumbra configuration as a json encoded string. - public string GetConfiguration(); - - // Fired whenever a mod directory change is finished. - // Gives the full path of the mod directory and whether Penumbra treats it as valid. - public event Action< string, bool >? ModDirectoryChanged; - - #endregion - - #region UI - - // Triggered when the user hovers over a listed changed object in a mod tab. - // Can be used to append tooltips. - public event ChangedItemHover? ChangedItemTooltip; - - // Events that are fired before and after the content of a mod settings panel are drawn. - // Both are fired inside the child window of the settings panel itself. - public event Action< string >? PreSettingsPanelDraw; - public event Action< string >? PostSettingsPanelDraw; - - // Triggered when the user clicks a listed changed object in a mod tab. - public event ChangedItemClick? ChangedItemClicked; - - #endregion - - #region Redrawing - - // Queue redrawing of all actors of the given name with the given RedrawType. - public void RedrawObject( string name, 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 ); - - // 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 all currently available actors with the given RedrawType. - public void RedrawAll( RedrawType setting ); - - // Triggered whenever a game object is redrawn via Penumbra. - public event GameObjectRedrawn? GameObjectRedrawn; - - #endregion - - #region Game State - - // Obtain the game object associated with a given draw object and the name of the collection associated with this game object. - public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); - - // Obtain the parent game object index for an unnamed cutscene actor by its index. - public int GetCutsceneParentIndex( int actor ); - - // Triggered when a character base is created and a corresponding gameObject could be found, - // before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand. - public event CreatingCharacterBaseDelegate? CreatingCharacterBase; - - // Triggered after a character base was created if a corresponding gameObject could be found, - // so you can apply flag changes after finishing. - public event CreatedCharacterBaseDelegate? CreatedCharacterBase; - - // Triggered whenever a resource is redirected by Penumbra for a specific, identified game object. - // Does not trigger if the resource is not requested for a known game object. - public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - - #endregion - - #region Resolving - - // Resolve a given gamePath via Penumbra using the Default collection. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolveDefaultPath( string gamePath ); - - // Resolve a given gamePath via Penumbra using the Interface collection. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolveInterfacePath( string gamePath ); - - // Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePath( string gamePath, string characterName ); - - // Resolve a given gamePath via Penumbra using any applicable character collections for the current character. - // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePlayerPath( string gamePath ); - - // Reverse resolves a given modded local path into its replacement in form of all applicable game paths for given character collection. - public string[] ReverseResolvePath( string moddedPath, string characterName ); - - // Reverse resolves a given modded local path into its replacement in form of all applicable game paths - // using the collection applying to the player character. - public string[] ReverseResolvePlayerPath( string moddedPath ); - - // Try to load a given gamePath with the resolved path from Penumbra. - public T? GetFile< T >( string gamePath ) where T : FileResource; - - // Try to load a given gamePath with the resolved path from Penumbra. - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource; - - #endregion - - #region Collections - - // Obtain a list of the names of all currently installed collections. - public IList< string > GetCollections(); - - // Obtain the name of the currently selected collection. - public string GetCurrentCollection(); - - // Obtain the name of the default collection. - public string GetDefaultCollection(); - - // Obtain the name of the interface collection. - public string GetInterfaceCollection(); - - // Obtain the name of the collection associated with characterName and whether it is configured or inferred from default. - public (string, bool) GetCharacterCollection( string characterName ); - - // Gets a dictionary of effected items from a collection - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ); - - #endregion - - #region Meta - - // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations - // for the collection currently associated with the player. - public string GetPlayerMetaManipulations(); - - // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations - // for the given collection associated with the character name, or the default collection. - public string GetMetaManipulations( string characterName ); - - #endregion - - #region Mods - - // 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(); - - // Try to reload an existing mod by its directory name or mod name. - // Can return ModMissing or success. - // Reload is the same as if triggered by button press and might delete the mod if it is not valid anymore. - public PenumbraApiEc ReloadMod( string modDirectory, string modName ); - - // Try to add a new mod inside the mod root directory (modDirectory should only be the name, not the full name). - // Returns FileMissing if the directory does not exist or success otherwise. - // Note that success does only imply a successful call, not a successful mod load. - public PenumbraApiEc AddMod( string modDirectory ); - - // Try to delete a mod given by its modDirectory or its name. - // Returns NothingDone if the mod can not be found or success otherwise. - // Note that success does only imply a successful call, not successful deletion. - public PenumbraApiEc DeleteMod( string modDirectory, string modName ); - - // Get the internal full filesystem path including search order for the specified mod. - // If success is returned, the second return value contains the full path - // and a bool indicating whether this is the default path (false) or a manually set one (true). - // Can return ModMissing or Success. - public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ); - - // Set the internal search order and filesystem path of the specified mod to the given path. - // Returns InvalidArgument if newPath is empty, ModMissing if the mod can not be found, - // PathRenameFailed if newPath could not be set and Success otherwise. - public PenumbraApiEc SetModPath( string modDirectory, string modName, string newPath ); - - #endregion - - #region 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 IDictionary< string, (IList< string >, GroupType) >? 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, IDictionary< string, IList< 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 TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList< string > options ); - - // This event gets fired when any setting in any collection changes. - public event ModSettingChanged? ModSettingChanged; - - #endregion - - #region Temporary - - // Create a temporary collection without actual settings but with a cache. - // If no character collection for this character exists or forceOverwriteCharacter is true, - // associate this collection to a specific character. - // Can return Okay, CharacterCollectionExists or NothingChanged, as well as the name of the new temporary collection on success. - public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ); - - // Remove the temporary collection associated with characterName if it exists. - // Can return Okay or NothingChanged. - public PenumbraApiEc RemoveTemporaryCollection( string characterName ); - - // Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections. - // Can return Okay, InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ); - - // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary. - // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation. - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString, - int priority ); - - // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. - // Can return Okay or NothingDone. - public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ); - - // Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary. - // Can return Okay or NothingDone. - public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ); - - #endregion -} \ No newline at end of file diff --git a/tmp/IPenumbraApiBase.cs b/tmp/IPenumbraApiBase.cs deleted file mode 100644 index e4c452b4..00000000 --- a/tmp/IPenumbraApiBase.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Penumbra.Api; - -public interface IPenumbraApiBase -{ - // The API version is staggered in two parts. - // The major/Breaking version only increments if there are changes breaking backwards compatibility. - // The minor/Feature version increments any time there is something added - // and resets when Breaking is incremented. - public (int Breaking, int Feature) ApiVersion { get; } - public bool Valid { get; } -} \ No newline at end of file diff --git a/tmp/Ipc/Collection.cs b/tmp/Ipc/Collection.cs deleted file mode 100644 index 3c40cd17..00000000 --- a/tmp/Ipc/Collection.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class GetCollections - { - public const string Label = $"Penumbra.{nameof( GetCollections )}"; - - public static FuncProvider< IList< string > > Provider( DalamudPluginInterface pi, Func< IList< string > > func ) - => new(pi, Label, func); - - public static FuncSubscriber< IList< string > > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetCurrentCollectionName - { - public const string Label = $"Penumbra.{nameof( GetCurrentCollectionName )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetDefaultCollectionName - { - public const string Label = $"Penumbra.{nameof( GetDefaultCollectionName )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetInterfaceCollectionName - { - public const string Label = $"Penumbra.{nameof( GetInterfaceCollectionName )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetCharacterCollectionName - { - public const string Label = $"Penumbra.{nameof( GetCharacterCollectionName )}"; - - public static FuncProvider< string, (string, bool) > Provider( DalamudPluginInterface pi, Func< string, (string, bool) > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, (string, bool) > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetChangedItems - { - public const string Label = $"Penumbra.{nameof( GetChangedItems )}"; - - public static FuncProvider< string, IReadOnlyDictionary< string, object? > > Provider( DalamudPluginInterface pi, - Func< string, IReadOnlyDictionary< string, object? > > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, IReadOnlyDictionary< string, object? > > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Configuration.cs b/tmp/Ipc/Configuration.cs deleted file mode 100644 index 67033458..00000000 --- a/tmp/Ipc/Configuration.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class GetModDirectory - { - public const string Label = $"Penumbra.{nameof( GetModDirectory )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetConfiguration - { - public const string Label = $"Penumbra.{nameof( GetConfiguration )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ModDirectoryChanged - { - public const string Label = $"Penumbra.{nameof( ModDirectoryChanged )}"; - - public static EventProvider< string, bool > Provider( DalamudPluginInterface pi, - Action< Action< string, bool > > sub, Action< Action< string, bool > > unsub ) - => new(pi, Label, ( sub, unsub )); - - public static EventSubscriber< string, bool > Subscriber( DalamudPluginInterface pi, params Action< string, bool >[] actions ) - => new(pi, Label, actions); - } -} \ No newline at end of file diff --git a/tmp/Ipc/GameState.cs b/tmp/Ipc/GameState.cs deleted file mode 100644 index 89889fda..00000000 --- a/tmp/Ipc/GameState.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class GetDrawObjectInfo - { - public const string Label = $"Penumbra.{nameof( GetDrawObjectInfo )}"; - - public static FuncProvider< nint, (nint, string) > Provider( DalamudPluginInterface pi, Func< nint, (nint, string) > func ) - => new(pi, Label, func); - - public static FuncSubscriber< nint, (nint, string) > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetCutsceneParentIndex - { - public const string Label = $"Penumbra.{nameof( GetCutsceneParentIndex )}"; - - public static FuncProvider< int, int > Provider( DalamudPluginInterface pi, Func< int, int > func ) - => new(pi, Label, func); - - public static FuncSubscriber< int, int > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class CreatingCharacterBase - { - public const string Label = $"Penumbra.{nameof( CreatingCharacterBase )}"; - - public static EventProvider< nint, string, nint, nint, nint > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< nint, string, nint, nint, nint > Subscriber( DalamudPluginInterface pi, params Action< nint, string, nint, nint, nint >[] actions ) - => new(pi, Label, actions); - } - - public static class CreatedCharacterBase - { - public const string Label = $"Penumbra.{nameof( CreatedCharacterBase )}"; - - public static EventProvider< nint, string, nint > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< nint, string, nint > Subscriber( DalamudPluginInterface pi, params Action< nint, string, nint >[] actions ) - => new(pi, Label, actions); - } - - public static class GameObjectResourcePathResolved - { - public const string Label = $"Penumbra.{nameof( GameObjectResourcePathResolved )}"; - - public static EventProvider< nint, string, string > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< nint, string, string > Subscriber( DalamudPluginInterface pi, params Action< nint, string, string >[] actions ) - => new(pi, Label, actions); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Meta.cs b/tmp/Ipc/Meta.cs deleted file mode 100644 index ef889f43..00000000 --- a/tmp/Ipc/Meta.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class GetPlayerMetaManipulations - { - public const string Label = $"Penumbra.{nameof( GetPlayerMetaManipulations )}"; - - public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetMetaManipulations - { - public const string Label = $"Penumbra.{nameof( GetMetaManipulations )}"; - - public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/ModSettings.cs b/tmp/Ipc/ModSettings.cs deleted file mode 100644 index 8a0e9cf8..00000000 --- a/tmp/Ipc/ModSettings.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Plugin; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -using CurrentSettings = ValueTuple< PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)? >; - -public static partial class Ipc -{ - public static class GetAvailableModSettings - { - public const string Label = $"Penumbra.{nameof( GetAvailableModSettings )}"; - - public static FuncProvider< string, string, IDictionary< string, (IList< string >, GroupType) >? > Provider( - DalamudPluginInterface pi, Func< string, string, IDictionary< string, (IList< string >, GroupType) >? > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, IDictionary< string, (IList< string >, GroupType) >? > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetCurrentModSettings - { - public const string Label = $"Penumbra.{nameof( GetCurrentModSettings )}"; - - public static FuncProvider< string, string, string, bool, CurrentSettings > Provider( DalamudPluginInterface pi, - Func< string, string, string, bool, CurrentSettings > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, bool, CurrentSettings > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class TryInheritMod - { - public const string Label = $"Penumbra.{nameof( TryInheritMod )}"; - - public static FuncProvider< string, string, string, bool, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, string, bool, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, bool, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class TrySetMod - { - public const string Label = $"Penumbra.{nameof( TrySetMod )}"; - - public static FuncProvider< string, string, string, bool, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, string, bool, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, bool, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class TrySetModPriority - { - public const string Label = $"Penumbra.{nameof( TrySetModPriority )}"; - - public static FuncProvider< string, string, string, int, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, string, int, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, int, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class TrySetModSetting - { - public const string Label = $"Penumbra.{nameof( TrySetModSetting )}"; - - public static FuncProvider< string, string, string, string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, string, string, string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, string, string, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class TrySetModSettings - { - public const string Label = $"Penumbra.{nameof( TrySetModSettings )}"; - - public static FuncProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > Provider( - DalamudPluginInterface pi, - Func< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ModSettingChanged - { - public const string Label = $"Penumbra.{nameof( ModSettingChanged )}"; - - public static EventProvider< ModSettingChange, string, string, bool > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< ModSettingChange, string, string, bool > Subscriber( DalamudPluginInterface pi, - params Action< ModSettingChange, string, string, bool >[] actions ) - => new(pi, Label, actions); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Mods.cs b/tmp/Ipc/Mods.cs deleted file mode 100644 index d5d09036..00000000 --- a/tmp/Ipc/Mods.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Plugin; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class GetMods - { - public const string Label = $"Penumbra.{nameof( GetMods )}"; - - public static FuncProvider< IList< (string, string) > > Provider( DalamudPluginInterface pi, Func< IList< (string, string) > > func ) - => new(pi, Label, func); - - public static FuncSubscriber< IList< (string, string) > > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ReloadMod - { - public const string Label = $"Penumbra.{nameof( ReloadMod )}"; - - public static FuncProvider< string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class AddMod - { - public const string Label = $"Penumbra.{nameof( AddMod )}"; - - public static FuncProvider< string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class DeleteMod - { - public const string Label = $"Penumbra.{nameof( DeleteMod )}"; - - public static FuncProvider< string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GetModPath - { - public const string Label = $"Penumbra.{nameof( GetModPath )}"; - - public static FuncProvider< string, string, (PenumbraApiEc, string, bool) > Provider( DalamudPluginInterface pi, - Func< string, string, (PenumbraApiEc, string, bool) > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, (PenumbraApiEc, string, bool) > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class SetModPath - { - public const string Label = $"Penumbra.{nameof( SetModPath )}"; - - public static FuncProvider< string, string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, string, string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/PluginState.cs b/tmp/Ipc/PluginState.cs deleted file mode 100644 index 500bf1ee..00000000 --- a/tmp/Ipc/PluginState.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class Initialized - { - public const string Label = $"Penumbra.{nameof( Initialized )}"; - - public static EventProvider Provider( DalamudPluginInterface pi ) - => new(pi, Label); - - public static EventSubscriber Subscriber( DalamudPluginInterface pi, params Action[] actions ) - { - var ret = new EventSubscriber( pi, Label ); - foreach( var action in actions ) - { - ret.Event += action; - } - - return ret; - } - } - - public static class Disposed - { - public const string Label = $"Penumbra.{nameof( Disposed )}"; - - public static EventProvider Provider( DalamudPluginInterface pi ) - => new(pi, Label); - - public static EventSubscriber Subscriber( DalamudPluginInterface pi, params Action[] actions ) - { - var ret = new EventSubscriber( pi, Label ); - foreach( var action in actions ) - { - ret.Event += action; - } - - return ret; - } - } - - public static class ApiVersion - { - public const string Label = $"Penumbra.{nameof( ApiVersion )}"; - - public static FuncProvider< int > Provider( DalamudPluginInterface pi, Func< int > func ) - => new(pi, Label, func); - - public static FuncSubscriber< int > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ApiVersions - { - public const string Label = $"Penumbra.{nameof( ApiVersions )}"; - - public static FuncProvider< (int Breaking, int Features) > Provider( DalamudPluginInterface pi, Func< (int, int) > func ) - => new(pi, Label, func); - - public static FuncSubscriber< (int Breaking, int Features) > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Redraw.cs b/tmp/Ipc/Redraw.cs deleted file mode 100644 index 396dfe8a..00000000 --- a/tmp/Ipc/Redraw.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class RedrawAll - { - public const string Label = $"Penumbra.{nameof( RedrawAll )}"; - - public static ActionProvider< RedrawType > Provider( DalamudPluginInterface pi, Action< RedrawType > action ) - => new(pi, Label, action); - - public static ActionSubscriber< RedrawType > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RedrawObject - { - public const string Label = $"Penumbra.{nameof( RedrawObject )}"; - - public static ActionProvider< GameObject, RedrawType > Provider( DalamudPluginInterface pi, Action< GameObject, RedrawType > action ) - => new(pi, Label, action); - - public static ActionSubscriber< GameObject, RedrawType > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RedrawObjectByIndex - { - public const string Label = $"Penumbra.{nameof( RedrawObjectByIndex )}"; - - public static ActionProvider< int, RedrawType > Provider( DalamudPluginInterface pi, Action< int, RedrawType > action ) - => new(pi, Label, action); - - public static ActionSubscriber< int, RedrawType > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RedrawObjectByName - { - public const string Label = $"Penumbra.{nameof( RedrawObjectByName )}"; - - public static ActionProvider< string, RedrawType > Provider( DalamudPluginInterface pi, Action< string, RedrawType > action ) - => new(pi, Label, action); - - public static ActionSubscriber< string, RedrawType > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class GameObjectRedrawn - { - public const string Label = $"Penumbra.{nameof( GameObjectRedrawn )}"; - - public static EventProvider< nint, int > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< nint, int > Subscriber( DalamudPluginInterface pi, params Action< nint, int >[] actions ) - => new(pi, Label, actions); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Resolve.cs b/tmp/Ipc/Resolve.cs deleted file mode 100644 index 8b9eb953..00000000 --- a/tmp/Ipc/Resolve.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class ResolveDefaultPath - { - public const string Label = $"Penumbra.{nameof( ResolveDefaultPath )}"; - - public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ResolveInterfacePath - { - public const string Label = $"Penumbra.{nameof( ResolveInterfacePath )}"; - - public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ResolvePlayerPath - { - public const string Label = $"Penumbra.{nameof( ResolvePlayerPath )}"; - - public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ResolveCharacterPath - { - public const string Label = $"Penumbra.{nameof( ResolveCharacterPath )}"; - - public static FuncProvider< string, string, string > Provider( DalamudPluginInterface pi, Func< string, string, string > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ReverseResolvePath - { - public const string Label = $"Penumbra.{nameof( ReverseResolvePath )}"; - - public static FuncProvider< string, string, string[] > Provider( DalamudPluginInterface pi, Func< string, string, string[] > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, string[] > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class ReverseResolvePlayerPath - { - public const string Label = $"Penumbra.{nameof( ReverseResolvePlayerPath )}"; - - public static FuncProvider< string, string[] > Provider( DalamudPluginInterface pi, Func< string, string[] > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string[] > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Temporary.cs b/tmp/Ipc/Temporary.cs deleted file mode 100644 index 55af6f22..00000000 --- a/tmp/Ipc/Temporary.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Plugin; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class CreateTemporaryCollection - { - public const string Label = $"Penumbra.{nameof( CreateTemporaryCollection )}"; - - public static FuncProvider< string, string, bool, (PenumbraApiEc, string) > Provider( DalamudPluginInterface pi, - Func< string, string, bool, (PenumbraApiEc, string) > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, bool, (PenumbraApiEc, string) > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RemoveTemporaryCollection - { - public const string Label = $"Penumbra.{nameof( RemoveTemporaryCollection )}"; - - public static FuncProvider< string, PenumbraApiEc > Provider( DalamudPluginInterface pi, - Func< string, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class AddTemporaryModAll - { - public const string Label = $"Penumbra.{nameof( AddTemporaryModAll )}"; - - public static FuncProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc > Provider( - DalamudPluginInterface pi, Func< string, Dictionary< string, string >, string, int, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, Dictionary< string, string >, string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class AddTemporaryMod - { - public const string Label = $"Penumbra.{nameof( AddTemporaryMod )}"; - - public static FuncProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > Provider( - DalamudPluginInterface pi, Func< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > Subscriber( - DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RemoveTemporaryModAll - { - public const string Label = $"Penumbra.{nameof( RemoveTemporaryModAll )}"; - - public static FuncProvider< string, int, PenumbraApiEc > Provider( - DalamudPluginInterface pi, Func< string, int, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } - - public static class RemoveTemporaryMod - { - public const string Label = $"Penumbra.{nameof( RemoveTemporaryMod )}"; - - public static FuncProvider< string, string, int, PenumbraApiEc > Provider( - DalamudPluginInterface pi, Func< string, string, int, PenumbraApiEc > func ) - => new(pi, Label, func); - - public static FuncSubscriber< string, string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) - => new(pi, Label); - } -} \ No newline at end of file diff --git a/tmp/Ipc/Ui.cs b/tmp/Ipc/Ui.cs deleted file mode 100644 index d88d6718..00000000 --- a/tmp/Ipc/Ui.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; - -namespace Penumbra.Api; - -public static partial class Ipc -{ - public static class PreSettingsDraw - { - public const string Label = $"Penumbra.{nameof( PreSettingsDraw )}"; - - public static EventProvider< string > Provider( DalamudPluginInterface pi, Action< Action< string > > sub, - Action< Action< string > > unsub ) - => new(pi, Label, ( sub, unsub )); - - public static EventSubscriber< string > Subscriber( DalamudPluginInterface pi, params Action< string >[] actions ) - => new(pi, Label, actions); - } - - public static class PostSettingsDraw - { - public const string Label = $"Penumbra.{nameof( PostSettingsDraw )}"; - - public static EventProvider< string > Provider( DalamudPluginInterface pi, Action< Action< string > > sub, - Action< Action< string > > unsub ) - => new(pi, Label, ( sub, unsub )); - - public static EventSubscriber< string > Subscriber( DalamudPluginInterface pi, params Action< string >[] actions ) - => new(pi, Label, actions); - } - - public static class ChangedItemTooltip - { - public const string Label = $"Penumbra.{nameof( ChangedItemTooltip )}"; - - public static EventProvider< ChangedItemType, uint > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< ChangedItemType, uint > Subscriber( DalamudPluginInterface pi, params Action< ChangedItemType, uint >[] actions ) - => new(pi, Label, actions); - } - - public static class ChangedItemClick - { - public const string Label = $"Penumbra.{nameof( ChangedItemClick )}"; - - public static EventProvider< MouseButton, ChangedItemType, uint > Provider( DalamudPluginInterface pi, Action add, Action del ) - => new(pi, Label, add, del); - - public static EventSubscriber< MouseButton, ChangedItemType, uint > Subscriber( DalamudPluginInterface pi, params Action< MouseButton, ChangedItemType, uint >[] actions ) - => new(pi, Label, actions); - } -} \ No newline at end of file diff --git a/tmp/Penumbra.Api.csproj b/tmp/Penumbra.Api.csproj deleted file mode 100644 index 8962883b..00000000 --- a/tmp/Penumbra.Api.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - net6.0-windows - preview - x64 - Penumbra.Api - absolute gangstas - Penumbra - Copyright © 2022 - 1.0.0.0 - 1.0.0.0 - bin\$(Configuration)\ - true - enable - true - false - false - - - - full - DEBUG;TRACE - - - - pdbonly - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - diff --git a/tmp/Penumbra.Api.csproj.DotSettings b/tmp/Penumbra.Api.csproj.DotSettings deleted file mode 100644 index 7d7508cb..00000000 --- a/tmp/Penumbra.Api.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/tmp/README.md b/tmp/README.md deleted file mode 100644 index 1e9bdf1a..00000000 --- a/tmp/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Penumbra - -This is an auxiliary repository for Penumbras external API. -For more information, see the [main repo](https://github.com/xivdev/Penumbra). \ No newline at end of file From 1046c4e991be2c2338a7191b4231b4fea978d316 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Oct 2022 16:03:05 +0200 Subject: [PATCH 0556/2451] Add Penumbra.String. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index f41af0fb..99f3b4f3 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit f41af0fb88626f1579d3c4370b32b901f3c4d3c2 +Subproject commit 99f3b4f3c7fd9f83b0741089e8808566a0bbdde5 From 52b2b66cd74b789426705b74046669afa3c34e9f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Oct 2022 16:07:54 +0200 Subject: [PATCH 0557/2451] Update dotnet version for builds. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d18be4e..57401edb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.100 + dotnet-version: '7.0.100-preview.7.22377.5' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcbf42ab..cf60c03a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.100 + dotnet-version: '7.0.100-preview.7.22377.5' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 106a976a..435da017 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.100 + dotnet-version: '7.0.100-preview.7.22377.5' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From ef3ffb5f10caf6b9abe2619e605fb3043108dfb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Oct 2022 12:42:09 +0100 Subject: [PATCH 0558/2451] Use DataShare in ObjectIdentifier --- Penumbra.GameData/.editorconfig | 3623 +++++++++++++++++ .../Enums/ChangedItemExtensions.cs | 7 +- Penumbra.GameData/GameData.cs | 81 +- Penumbra.GameData/GamePathParser.cs | 418 +- Penumbra.GameData/ObjectIdentification.cs | 450 +- Penumbra.GameData/Structs/CharacterEquip.cs | 1 - Penumbra.GameData/Util/GamePath.cs | 106 - Penumbra/Collections/ModCollection.Cache.cs | 5 +- Penumbra/Import/MetaFileInfo.cs | 5 - Penumbra/Meta/Files/CmpFile.cs | 1 - Penumbra/Meta/Files/EqdpFile.cs | 1 - Penumbra/Meta/Files/EqpGmpFile.cs | 1 - Penumbra/Meta/Files/EstFile.cs | 1 - Penumbra/Meta/Files/MetaBaseFile.cs | 1 - .../Meta/Manipulations/MetaManipulation.cs | 1 - Penumbra/Mods/Mod.ChangedItems.cs | 4 +- Penumbra/Penumbra.cs | 5 +- 17 files changed, 4138 insertions(+), 573 deletions(-) create mode 100644 Penumbra.GameData/.editorconfig delete mode 100644 Penumbra.GameData/Util/GamePath.cs diff --git a/Penumbra.GameData/.editorconfig b/Penumbra.GameData/.editorconfig new file mode 100644 index 00000000..0bbaa114 --- /dev/null +++ b/Penumbra.GameData/.editorconfig @@ -0,0 +1,3623 @@ + +[*.proto] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + +[*] + +# Microsoft .NET properties +csharp_indent_braces=false +csharp_indent_switch_labels=true +csharp_new_line_before_catch=true +csharp_new_line_before_else=true +csharp_new_line_before_finally=true +csharp_new_line_before_members_in_object_initializers=true +csharp_new_line_before_open_brace=all +csharp_new_line_between_query_expression_clauses=true +csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_preserve_single_line_blocks=true +csharp_space_after_cast=false +csharp_space_after_colon_in_inheritance_clause=true +csharp_space_after_comma=true +csharp_space_after_dot=false +csharp_space_after_keywords_in_control_flow_statements=true +csharp_space_after_semicolon_in_for_statement=true +csharp_space_around_binary_operators=before_and_after +csharp_space_before_colon_in_inheritance_clause=true +csharp_space_before_comma=false +csharp_space_before_dot=false +csharp_space_before_open_square_brackets=false +csharp_space_before_semicolon_in_for_statement=false +csharp_space_between_empty_square_brackets=false +csharp_space_between_method_call_empty_parameter_list_parentheses=false +csharp_space_between_method_call_name_and_opening_parenthesis=false +csharp_space_between_method_call_parameter_list_parentheses=false +csharp_space_between_method_declaration_empty_parameter_list_parentheses=false +csharp_space_between_method_declaration_name_and_open_parenthesis=false +csharp_space_between_method_declaration_parameter_list_parentheses=false +csharp_space_between_parentheses=false +csharp_space_between_square_brackets=false +csharp_style_namespace_declarations= file_scoped:suggestion +csharp_style_var_elsewhere=true:suggestion +csharp_style_var_for_built_in_types=true:suggestion +csharp_style_var_when_type_is_apparent=true:suggestion +csharp_using_directive_placement= outside_namespace:silent +dotnet_diagnostic.bc40000.severity=warning +dotnet_diagnostic.bc400005.severity=warning +dotnet_diagnostic.bc40008.severity=warning +dotnet_diagnostic.bc40056.severity=warning +dotnet_diagnostic.bc42016.severity=warning +dotnet_diagnostic.bc42024.severity=warning +dotnet_diagnostic.bc42025.severity=warning +dotnet_diagnostic.bc42104.severity=warning +dotnet_diagnostic.bc42105.severity=warning +dotnet_diagnostic.bc42106.severity=warning +dotnet_diagnostic.bc42107.severity=warning +dotnet_diagnostic.bc42304.severity=warning +dotnet_diagnostic.bc42309.severity=warning +dotnet_diagnostic.bc42322.severity=warning +dotnet_diagnostic.bc42349.severity=warning +dotnet_diagnostic.bc42353.severity=warning +dotnet_diagnostic.bc42354.severity=warning +dotnet_diagnostic.bc42355.severity=warning +dotnet_diagnostic.bc42356.severity=warning +dotnet_diagnostic.bc42358.severity=warning +dotnet_diagnostic.bc42504.severity=warning +dotnet_diagnostic.bc42505.severity=warning +dotnet_diagnostic.cs0067.severity=warning +dotnet_diagnostic.cs0078.severity=warning +dotnet_diagnostic.cs0108.severity=warning +dotnet_diagnostic.cs0109.severity=warning +dotnet_diagnostic.cs0114.severity=warning +dotnet_diagnostic.cs0162.severity=warning +dotnet_diagnostic.cs0164.severity=warning +dotnet_diagnostic.cs0168.severity=warning +dotnet_diagnostic.cs0169.severity=warning +dotnet_diagnostic.cs0183.severity=warning +dotnet_diagnostic.cs0184.severity=warning +dotnet_diagnostic.cs0197.severity=warning +dotnet_diagnostic.cs0219.severity=warning +dotnet_diagnostic.cs0252.severity=warning +dotnet_diagnostic.cs0253.severity=warning +dotnet_diagnostic.cs0414.severity=warning +dotnet_diagnostic.cs0420.severity=warning +dotnet_diagnostic.cs0465.severity=warning +dotnet_diagnostic.cs0469.severity=warning +dotnet_diagnostic.cs0612.severity=warning +dotnet_diagnostic.cs0618.severity=warning +dotnet_diagnostic.cs0628.severity=warning +dotnet_diagnostic.cs0642.severity=warning +dotnet_diagnostic.cs0649.severity=warning +dotnet_diagnostic.cs0652.severity=warning +dotnet_diagnostic.cs0657.severity=warning +dotnet_diagnostic.cs0658.severity=warning +dotnet_diagnostic.cs0659.severity=warning +dotnet_diagnostic.cs0660.severity=warning +dotnet_diagnostic.cs0661.severity=warning +dotnet_diagnostic.cs0665.severity=warning +dotnet_diagnostic.cs0672.severity=warning +dotnet_diagnostic.cs0675.severity=warning +dotnet_diagnostic.cs0693.severity=warning +dotnet_diagnostic.cs1030.severity=warning +dotnet_diagnostic.cs1058.severity=warning +dotnet_diagnostic.cs1066.severity=warning +dotnet_diagnostic.cs1522.severity=warning +dotnet_diagnostic.cs1570.severity=warning +dotnet_diagnostic.cs1571.severity=warning +dotnet_diagnostic.cs1572.severity=warning +dotnet_diagnostic.cs1573.severity=warning +dotnet_diagnostic.cs1574.severity=warning +dotnet_diagnostic.cs1580.severity=warning +dotnet_diagnostic.cs1581.severity=warning +dotnet_diagnostic.cs1584.severity=warning +dotnet_diagnostic.cs1587.severity=warning +dotnet_diagnostic.cs1589.severity=warning +dotnet_diagnostic.cs1590.severity=warning +dotnet_diagnostic.cs1591.severity=warning +dotnet_diagnostic.cs1592.severity=warning +dotnet_diagnostic.cs1710.severity=warning +dotnet_diagnostic.cs1711.severity=warning +dotnet_diagnostic.cs1712.severity=warning +dotnet_diagnostic.cs1717.severity=warning +dotnet_diagnostic.cs1723.severity=warning +dotnet_diagnostic.cs1911.severity=warning +dotnet_diagnostic.cs1957.severity=warning +dotnet_diagnostic.cs1981.severity=warning +dotnet_diagnostic.cs1998.severity=warning +dotnet_diagnostic.cs4014.severity=warning +dotnet_diagnostic.cs7022.severity=warning +dotnet_diagnostic.cs7023.severity=warning +dotnet_diagnostic.cs7095.severity=warning +dotnet_diagnostic.cs8094.severity=warning +dotnet_diagnostic.cs8123.severity=warning +dotnet_diagnostic.cs8321.severity=warning +dotnet_diagnostic.cs8383.severity=warning +dotnet_diagnostic.cs8416.severity=warning +dotnet_diagnostic.cs8417.severity=warning +dotnet_diagnostic.cs8424.severity=warning +dotnet_diagnostic.cs8425.severity=warning +dotnet_diagnostic.cs8509.severity=warning +dotnet_diagnostic.cs8524.severity=warning +dotnet_diagnostic.cs8597.severity=warning +dotnet_diagnostic.cs8600.severity=warning +dotnet_diagnostic.cs8601.severity=warning +dotnet_diagnostic.cs8602.severity=warning +dotnet_diagnostic.cs8603.severity=warning +dotnet_diagnostic.cs8604.severity=warning +dotnet_diagnostic.cs8605.severity=warning +dotnet_diagnostic.cs8607.severity=warning +dotnet_diagnostic.cs8608.severity=warning +dotnet_diagnostic.cs8609.severity=warning +dotnet_diagnostic.cs8610.severity=warning +dotnet_diagnostic.cs8611.severity=warning +dotnet_diagnostic.cs8612.severity=warning +dotnet_diagnostic.cs8613.severity=warning +dotnet_diagnostic.cs8614.severity=warning +dotnet_diagnostic.cs8615.severity=warning +dotnet_diagnostic.cs8616.severity=warning +dotnet_diagnostic.cs8617.severity=warning +dotnet_diagnostic.cs8618.severity=warning +dotnet_diagnostic.cs8619.severity=warning +dotnet_diagnostic.cs8620.severity=warning +dotnet_diagnostic.cs8621.severity=warning +dotnet_diagnostic.cs8622.severity=warning +dotnet_diagnostic.cs8624.severity=warning +dotnet_diagnostic.cs8625.severity=warning +dotnet_diagnostic.cs8629.severity=warning +dotnet_diagnostic.cs8631.severity=warning +dotnet_diagnostic.cs8632.severity=none +dotnet_diagnostic.cs8633.severity=warning +dotnet_diagnostic.cs8634.severity=warning +dotnet_diagnostic.cs8643.severity=warning +dotnet_diagnostic.cs8644.severity=warning +dotnet_diagnostic.cs8645.severity=warning +dotnet_diagnostic.cs8655.severity=warning +dotnet_diagnostic.cs8656.severity=warning +dotnet_diagnostic.cs8667.severity=warning +dotnet_diagnostic.cs8669.severity=none +dotnet_diagnostic.cs8670.severity=warning +dotnet_diagnostic.cs8714.severity=warning +dotnet_diagnostic.cs8762.severity=warning +dotnet_diagnostic.cs8763.severity=warning +dotnet_diagnostic.cs8764.severity=warning +dotnet_diagnostic.cs8765.severity=warning +dotnet_diagnostic.cs8766.severity=warning +dotnet_diagnostic.cs8767.severity=warning +dotnet_diagnostic.cs8768.severity=warning +dotnet_diagnostic.cs8769.severity=warning +dotnet_diagnostic.cs8770.severity=warning +dotnet_diagnostic.cs8774.severity=warning +dotnet_diagnostic.cs8775.severity=warning +dotnet_diagnostic.cs8776.severity=warning +dotnet_diagnostic.cs8777.severity=warning +dotnet_diagnostic.cs8794.severity=warning +dotnet_diagnostic.cs8819.severity=warning +dotnet_diagnostic.cs8824.severity=warning +dotnet_diagnostic.cs8825.severity=warning +dotnet_diagnostic.cs8846.severity=warning +dotnet_diagnostic.cs8847.severity=warning +dotnet_diagnostic.cs8851.severity=warning +dotnet_diagnostic.cs8860.severity=warning +dotnet_diagnostic.cs8892.severity=warning +dotnet_diagnostic.cs8907.severity=warning +dotnet_diagnostic.cs8947.severity=warning +dotnet_diagnostic.cs8960.severity=warning +dotnet_diagnostic.cs8961.severity=warning +dotnet_diagnostic.cs8962.severity=warning +dotnet_diagnostic.cs8963.severity=warning +dotnet_diagnostic.cs8965.severity=warning +dotnet_diagnostic.cs8966.severity=warning +dotnet_diagnostic.cs8971.severity=warning +dotnet_diagnostic.wme006.severity=warning +dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols=constants_symbols +dotnet_naming_rule.event_rule.import_to_resharper=as_predefined +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols=event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols +dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined +dotnet_naming_rule.locals_rule.severity = warning +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols=locals_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols +dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_functions_rule.severity = warning +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols +dotnet_naming_rule.method_rule.import_to_resharper=as_predefined +dotnet_naming_rule.method_rule.severity = warning +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols=method_symbols +dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.parameters_rule.symbols=parameters_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols +dotnet_naming_rule.property_rule.import_to_resharper=as_predefined +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols=property_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = warning +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix=I +dotnet_naming_style.lower_camel_case_style.capitalization=camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix=_ +dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix=T +dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds=field +dotnet_naming_symbols.constants_symbols.required_modifiers=const +dotnet_naming_symbols.event_symbols.applicable_accessibilities=* +dotnet_naming_symbols.event_symbols.applicable_kinds=event +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface +dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* +dotnet_naming_symbols.locals_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.required_modifiers=const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function +dotnet_naming_symbols.method_symbols.applicable_accessibilities=* +dotnet_naming_symbols.method_symbols.applicable_kinds=method +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field +dotnet_naming_symbols.private_constants_symbols.required_modifiers=const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.property_symbols.applicable_accessibilities=* +dotnet_naming_symbols.property_symbols.applicable_kinds=property +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter +dotnet_separate_import_directive_groups=false +dotnet_sort_system_directives_first=true +dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion +dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion +dotnet_style_predefined_type_for_member_access=true:suggestion +dotnet_style_qualification_for_event=false:suggestion +dotnet_style_qualification_for_field=false:suggestion +dotnet_style_qualification_for_method=false:suggestion +dotnet_style_qualification_for_property=false:suggestion +dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +file_header_template= + +# ReSharper properties +resharper_accessor_owner_body=expression_body +resharper_alignment_tab_fill_style=use_spaces +resharper_align_first_arg_by_paren=false +resharper_align_linq_query=false +resharper_align_multiline_argument=true +resharper_align_multiline_array_and_object_initializer=false +resharper_align_multiline_array_initializer=true +resharper_align_multiline_binary_expressions_chain=false +resharper_align_multiline_binary_patterns=false +resharper_align_multiline_ctor_init=true +resharper_align_multiline_expression_braces=false +resharper_align_multiline_implements_list=true +resharper_align_multiline_property_pattern=false +resharper_align_multiline_statement_conditions=true +resharper_align_multiline_switch_expression=false +resharper_align_multiline_type_argument=true +resharper_align_multiline_type_parameter=true +resharper_align_multline_type_parameter_constrains=true +resharper_align_multline_type_parameter_list=false +resharper_align_tuple_components=false +resharper_align_union_type_usage=true +resharper_allow_alias=true +resharper_allow_comment_after_lbrace=false +resharper_allow_far_alignment=false +resharper_always_use_end_of_line_brace_style=false +resharper_apply_auto_detected_rules=false +resharper_apply_on_completion=false +resharper_arguments_anonymous_function=positional +resharper_arguments_literal=positional +resharper_arguments_named=positional +resharper_arguments_other=positional +resharper_arguments_skip_single=false +resharper_arguments_string_literal=positional +resharper_attribute_style=do_not_touch +resharper_autodetect_indent_settings=false +resharper_blank_lines_after_block_statements=1 +resharper_blank_lines_after_case=0 +resharper_blank_lines_after_control_transfer_statements=1 +resharper_blank_lines_after_file_scoped_namespace_directive=1 +resharper_blank_lines_after_imports=1 +resharper_blank_lines_after_multiline_statements=0 +resharper_blank_lines_after_options=1 +resharper_blank_lines_after_start_comment=1 +resharper_blank_lines_after_using_list=1 +resharper_blank_lines_around_accessor=0 +resharper_blank_lines_around_auto_property=1 +resharper_blank_lines_around_block_case_section=0 +resharper_blank_lines_around_class_definition=1 +resharper_blank_lines_around_field=1 +resharper_blank_lines_around_function_declaration=0 +resharper_blank_lines_around_function_definition=1 +resharper_blank_lines_around_global_attribute=0 +resharper_blank_lines_around_invocable=1 +resharper_blank_lines_around_local_method=1 +resharper_blank_lines_around_multiline_case_section=0 +resharper_blank_lines_around_namespace=1 +resharper_blank_lines_around_other_declaration=0 +resharper_blank_lines_around_property=1 +resharper_blank_lines_around_razor_functions=1 +resharper_blank_lines_around_razor_helpers=1 +resharper_blank_lines_around_razor_sections=1 +resharper_blank_lines_around_region=1 +resharper_blank_lines_around_single_line_accessor=0 +resharper_blank_lines_around_single_line_auto_property=0 +resharper_blank_lines_around_single_line_field=0 +resharper_blank_lines_around_single_line_function_definition=0 +resharper_blank_lines_around_single_line_invocable=0 +resharper_blank_lines_around_single_line_local_method=0 +resharper_blank_lines_around_single_line_property=0 +resharper_blank_lines_around_single_line_type=0 +resharper_blank_lines_around_type=1 +resharper_blank_lines_before_block_statements=0 +resharper_blank_lines_before_case=0 +resharper_blank_lines_before_control_transfer_statements=0 +resharper_blank_lines_before_multiline_statements=0 +resharper_blank_lines_before_single_line_comment=0 +resharper_blank_lines_inside_namespace=0 +resharper_blank_lines_inside_region=1 +resharper_blank_lines_inside_type=0 +resharper_blank_line_after_pi=true +resharper_braces_for_dowhile=required +resharper_braces_for_fixed=required +resharper_braces_for_for=required_for_multiline +resharper_braces_for_foreach=required_for_multiline +resharper_braces_for_ifelse=not_required_for_both +resharper_braces_for_lock=required +resharper_braces_for_using=required +resharper_braces_for_while=required_for_multiline +resharper_braces_redundant=true +resharper_break_template_declaration=line_break +resharper_can_use_global_alias=true +resharper_configure_await_analysis_mode=disabled +resharper_constructor_or_destructor_body=expression_body +resharper_continuous_indent_multiplier=1 +resharper_continuous_line_indent=single +resharper_cpp_align_multiline_argument=true +resharper_cpp_align_multiline_calls_chain=true +resharper_cpp_align_multiline_extends_list=true +resharper_cpp_align_multiline_for_stmt=true +resharper_cpp_align_multiline_parameter=true +resharper_cpp_align_multiple_declaration=true +resharper_cpp_align_ternary=align_not_nested +resharper_cpp_anonymous_method_declaration_braces=next_line +resharper_cpp_case_block_braces=next_line_shifted_2 +resharper_cpp_empty_block_style=multiline +resharper_cpp_indent_switch_labels=false +resharper_cpp_insert_final_newline=false +resharper_cpp_int_align_comments=false +resharper_cpp_invocable_declaration_braces=next_line +resharper_cpp_max_line_length=120 +resharper_cpp_new_line_before_catch=true +resharper_cpp_new_line_before_else=true +resharper_cpp_new_line_before_while=true +resharper_cpp_other_braces=next_line +resharper_cpp_space_around_binary_operator=true +resharper_cpp_type_declaration_braces=next_line +resharper_cpp_wrap_arguments_style=wrap_if_long +resharper_cpp_wrap_lines=true +resharper_cpp_wrap_parameters_style=wrap_if_long +resharper_csharp_align_multiline_argument=false +resharper_csharp_align_multiline_calls_chain=false +resharper_csharp_align_multiline_expression=false +resharper_csharp_align_multiline_extends_list=false +resharper_csharp_align_multiline_for_stmt=false +resharper_csharp_align_multiline_parameter=false +resharper_csharp_align_multiple_declaration=true +resharper_csharp_empty_block_style=together +resharper_csharp_insert_final_newline=true +resharper_csharp_int_align_comments=true +resharper_csharp_max_line_length=144 +resharper_csharp_naming_rule.enum_member=AaBb +resharper_csharp_naming_rule.method_property_event=AaBb +resharper_csharp_naming_rule.other=AaBb +resharper_csharp_new_line_before_while=false +resharper_csharp_prefer_qualified_reference=false +resharper_csharp_space_after_unary_operator=false +resharper_csharp_wrap_arguments_style=wrap_if_long +resharper_csharp_wrap_before_binary_opsign=true +resharper_csharp_wrap_for_stmt_header_style=wrap_if_long +resharper_csharp_wrap_lines=true +resharper_csharp_wrap_parameters_style=wrap_if_long +resharper_css_brace_style=end_of_line +resharper_css_insert_final_newline=false +resharper_css_keep_blank_lines_between_declarations=1 +resharper_css_max_line_length=120 +resharper_css_wrap_lines=true +resharper_cxxcli_property_declaration_braces=next_line +resharper_declarations_style=separate_lines +resharper_default_exception_variable_name=e +resharper_default_value_when_type_evident=default_literal +resharper_default_value_when_type_not_evident=default_literal +resharper_delete_quotes_from_solid_values=false +resharper_disable_blank_line_changes=false +resharper_disable_formatter=false +resharper_disable_indenter=false +resharper_disable_int_align=false +resharper_disable_line_break_changes=false +resharper_disable_line_break_removal=false +resharper_disable_space_changes=false +resharper_disable_space_changes_before_trailing_comment=false +resharper_dont_remove_extra_blank_lines=false +resharper_enable_wrapping=false +resharper_enforce_line_ending_style=false +resharper_event_handler_pattern_long=$object$On$event$ +resharper_event_handler_pattern_short=On$event$ +resharper_expression_braces=inside +resharper_expression_pars=inside +resharper_extra_spaces=remove_all +resharper_force_attribute_style=separate +resharper_force_chop_compound_do_expression=false +resharper_force_chop_compound_if_expression=false +resharper_force_chop_compound_while_expression=false +resharper_force_control_statements_braces=do_not_change +resharper_force_linebreaks_inside_complex_literals=true +resharper_force_variable_declarations_on_new_line=false +resharper_format_leading_spaces_decl=false +resharper_free_block_braces=next_line +resharper_function_declaration_return_type_style=do_not_change +resharper_function_definition_return_type_style=do_not_change +resharper_generator_mode=false +resharper_html_attribute_indent=align_by_first_attribute +resharper_html_insert_final_newline=false +resharper_html_linebreak_before_elements=body,div,p,form,h1,h2,h3 +resharper_html_max_blank_lines_between_tags=2 +resharper_html_max_line_length=120 +resharper_html_pi_attribute_style=on_single_line +resharper_html_space_before_self_closing=false +resharper_html_wrap_lines=true +resharper_ignore_space_preservation=false +resharper_include_prefix_comment_in_indent=false +resharper_indent_access_specifiers_from_class=false +resharper_indent_aligned_ternary=true +resharper_indent_anonymous_method_block=false +resharper_indent_braces_inside_statement_conditions=true +resharper_indent_case_from_select=true +resharper_indent_child_elements=OneIndent +resharper_indent_class_members_from_access_specifiers=false +resharper_indent_comment=true +resharper_indent_inside_namespace=true +resharper_indent_invocation_pars=inside +resharper_indent_left_par_inside_expression=false +resharper_indent_method_decl_pars=inside +resharper_indent_nested_fixed_stmt=false +resharper_indent_nested_foreach_stmt=true +resharper_indent_nested_for_stmt=true +resharper_indent_nested_lock_stmt=false +resharper_indent_nested_usings_stmt=false +resharper_indent_nested_while_stmt=true +resharper_indent_pars=inside +resharper_indent_preprocessor_directives=none +resharper_indent_preprocessor_if=no_indent +resharper_indent_preprocessor_other=no_indent +resharper_indent_preprocessor_region=usual_indent +resharper_indent_statement_pars=inside +resharper_indent_text=OneIndent +resharper_indent_typearg_angles=inside +resharper_indent_typeparam_angles=inside +resharper_indent_type_constraints=true +resharper_indent_wrapped_function_names=false +resharper_instance_members_qualify_declared_in=this_class, base_class +resharper_int_align=true +resharper_int_align_assignments=true +resharper_int_align_binary_expressions=false +resharper_int_align_declaration_names=false +resharper_int_align_eq=false +resharper_int_align_fields=true +resharper_int_align_fix_in_adjacent=true +resharper_int_align_invocations=true +resharper_int_align_methods=true +resharper_int_align_nested_ternary=true +resharper_int_align_parameters=false +resharper_int_align_properties=true +resharper_int_align_property_patterns=true +resharper_int_align_switch_expressions=true +resharper_int_align_switch_sections=true +resharper_int_align_variables=true +resharper_js_align_multiline_parameter=false +resharper_js_align_multiple_declaration=false +resharper_js_align_ternary=none +resharper_js_brace_style=end_of_line +resharper_js_empty_block_style=multiline +resharper_js_indent_switch_labels=false +resharper_js_insert_final_newline=false +resharper_js_keep_blank_lines_between_declarations=2 +resharper_js_max_line_length=120 +resharper_js_new_line_before_catch=false +resharper_js_new_line_before_else=false +resharper_js_new_line_before_finally=false +resharper_js_new_line_before_while=false +resharper_js_space_around_binary_operator=true +resharper_js_wrap_arguments_style=chop_if_long +resharper_js_wrap_before_binary_opsign=false +resharper_js_wrap_for_stmt_header_style=chop_if_long +resharper_js_wrap_lines=true +resharper_js_wrap_parameters_style=chop_if_long +resharper_keep_blank_lines_in_code=2 +resharper_keep_blank_lines_in_declarations=2 +resharper_keep_existing_attribute_arrangement=false +resharper_keep_existing_declaration_block_arrangement=false +resharper_keep_existing_declaration_parens_arrangement=true +resharper_keep_existing_embedded_arrangement=false +resharper_keep_existing_embedded_block_arrangement=false +resharper_keep_existing_enum_arrangement=false +resharper_keep_existing_expr_member_arrangement=false +resharper_keep_existing_initializer_arrangement=false +resharper_keep_existing_invocation_parens_arrangement=true +resharper_keep_existing_property_patterns_arrangement=true +resharper_keep_existing_switch_expression_arrangement=false +resharper_keep_nontrivial_alias=true +resharper_keep_user_linebreaks=true +resharper_keep_user_wrapping=true +resharper_linebreaks_around_razor_statements=true +resharper_linebreaks_inside_tags_for_elements_longer_than=2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements=true +resharper_linebreaks_inside_tags_for_multiline_elements=true +resharper_linebreak_before_all_elements=false +resharper_linebreak_before_multiline_elements=true +resharper_linebreak_before_singleline_elements=false +resharper_line_break_after_colon_in_member_initializer_lists=do_not_change +resharper_line_break_after_comma_in_member_initializer_lists=false +resharper_line_break_before_comma_in_member_initializer_lists=false +resharper_line_break_before_requires_clause=do_not_change +resharper_linkage_specification_braces=end_of_line +resharper_linkage_specification_indentation=none +resharper_local_function_body=expression_body +resharper_macro_block_begin= +resharper_macro_block_end= +resharper_max_array_initializer_elements_on_line=10000 +resharper_max_attribute_length_for_same_line=38 +resharper_max_enum_members_on_line=1 +resharper_max_formal_parameters_on_line=10000 +resharper_max_initializer_elements_on_line=1 +resharper_max_invocation_arguments_on_line=10000 +resharper_media_query_style=same_line +resharper_member_initializer_list_style=do_not_change +resharper_method_or_operator_body=expression_body +resharper_min_blank_lines_after_imports=0 +resharper_min_blank_lines_around_fields=0 +resharper_min_blank_lines_around_functions=1 +resharper_min_blank_lines_around_types=1 +resharper_min_blank_lines_between_declarations=1 +resharper_namespace_declaration_braces=next_line +resharper_namespace_indentation=all +resharper_nested_ternary_style=autodetect +resharper_new_line_before_enumerators=true +resharper_normalize_tag_names=false +resharper_no_indent_inside_elements=html,body,thead,tbody,tfoot +resharper_no_indent_inside_if_element_longer_than=200 +resharper_object_creation_when_type_evident=target_typed +resharper_object_creation_when_type_not_evident=explicitly_typed +resharper_old_engine=false +resharper_options_braces_pointy=false +resharper_outdent_binary_ops=true +resharper_outdent_binary_pattern_ops=false +resharper_outdent_commas=false +resharper_outdent_dots=false +resharper_outdent_namespace_member=false +resharper_outdent_statement_labels=false +resharper_outdent_ternary_ops=false +resharper_parentheses_non_obvious_operations=none, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, bitwise_and +resharper_parentheses_redundancy_style=remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations=false +resharper_pi_attributes_indent=align_by_first_attribute +resharper_place_attribute_on_same_line=false +resharper_place_class_decorator_on_the_same_line=false +resharper_place_comments_at_first_column=false +resharper_place_constructor_initializer_on_same_line=false +resharper_place_each_decorator_on_new_line=false +resharper_place_event_attribute_on_same_line=false +resharper_place_expr_accessor_on_single_line=true +resharper_place_expr_method_on_single_line=false +resharper_place_expr_property_on_single_line=false +resharper_place_field_decorator_on_the_same_line=false +resharper_place_linq_into_on_new_line=true +resharper_place_method_decorator_on_the_same_line=false +resharper_place_namespace_definitions_on_same_line=false +resharper_place_property_attribute_on_same_line=false +resharper_place_property_decorator_on_the_same_line=false +resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line +resharper_place_simple_embedded_statement_on_same_line=false +resharper_place_simple_enum_on_single_line=true +resharper_place_simple_initializer_on_single_line=true +resharper_place_simple_property_pattern_on_single_line=true +resharper_place_simple_switch_expression_on_single_line=true +resharper_place_template_args_on_new_line=false +resharper_place_type_constraints_on_same_line=true +resharper_prefer_explicit_discard_declaration=false +resharper_prefer_separate_deconstructed_variables_declaration=false +resharper_preserve_spaces_inside_tags=pre,textarea +resharper_properties_style=separate_lines_for_nonsingle +resharper_protobuf_brace_style=end_of_line +resharper_protobuf_empty_block_style=together_same_line +resharper_protobuf_insert_final_newline=false +resharper_protobuf_max_line_length=120 +resharper_protobuf_wrap_lines=true +resharper_qualified_using_at_nested_scope=false +resharper_quote_style=doublequoted +resharper_razor_prefer_qualified_reference=true +resharper_remove_blank_lines_near_braces=false +resharper_remove_blank_lines_near_braces_in_code=true +resharper_remove_blank_lines_near_braces_in_declarations=true +resharper_remove_this_qualifier=true +resharper_requires_expression_braces=next_line +resharper_resx_attribute_indent=single_indent +resharper_resx_insert_final_newline=false +resharper_resx_linebreak_before_elements= +resharper_resx_max_blank_lines_between_tags=0 +resharper_resx_max_line_length=2147483647 +resharper_resx_pi_attribute_style=do_not_touch +resharper_resx_space_before_self_closing=false +resharper_resx_wrap_lines=false +resharper_resx_wrap_tags_and_pi=false +resharper_resx_wrap_text=false +resharper_selector_style=same_line +resharper_show_autodetect_configure_formatting_tip=true +resharper_simple_blocks=do_not_change +resharper_simple_block_style=do_not_change +resharper_simple_case_statement_style=do_not_change +resharper_simple_embedded_statement_style=do_not_change +resharper_single_statement_function_style=do_not_change +resharper_sort_attributes=false +resharper_sort_class_selectors=false +resharper_sort_usings=true +resharper_sort_usings_lowercase_first=false +resharper_spaces_around_eq_in_attribute=false +resharper_spaces_around_eq_in_pi_attribute=false +resharper_spaces_inside_tags=false +resharper_space_after_arrow=true +resharper_space_after_attributes=true +resharper_space_after_attribute_target_colon=true +resharper_space_after_cast=false +resharper_space_after_colon=true +resharper_space_after_colon_in_case=true +resharper_space_after_colon_in_inheritance_clause=true +resharper_space_after_colon_in_type_annotation=true +resharper_space_after_comma=true +resharper_space_after_for_colon=true +resharper_space_after_function_comma=true +resharper_space_after_keywords_in_control_flow_statements=true +resharper_space_after_last_attribute=false +resharper_space_after_last_pi_attribute=false +resharper_space_after_media_colon=true +resharper_space_after_media_comma=true +resharper_space_after_operator_keyword=true +resharper_space_after_property_colon=true +resharper_space_after_property_semicolon=true +resharper_space_after_ptr_in_data_member=true +resharper_space_after_ptr_in_data_members=false +resharper_space_after_ptr_in_method=true +resharper_space_after_ref_in_data_member=true +resharper_space_after_ref_in_data_members=false +resharper_space_after_ref_in_method=true +resharper_space_after_selector_comma=true +resharper_space_after_semicolon_in_for_statement=true +resharper_space_after_separator=false +resharper_space_after_ternary_colon=true +resharper_space_after_ternary_quest=true +resharper_space_after_triple_slash=true +resharper_space_after_type_parameter_constraint_colon=true +resharper_space_around_additive_op=true +resharper_space_around_alias_eq=true +resharper_space_around_assignment_op=true +resharper_space_around_assignment_operator=true +resharper_space_around_attribute_match_operator=false +resharper_space_around_deref_in_trailing_return_type=true +resharper_space_around_lambda_arrow=true +resharper_space_around_member_access_operator=false +resharper_space_around_operator=true +resharper_space_around_pipe_or_amper_in_type_usage=true +resharper_space_around_relational_op=true +resharper_space_around_selector_operator=true +resharper_space_around_shift_op=true +resharper_space_around_stmt_colon=true +resharper_space_around_ternary_operator=true +resharper_space_before_array_rank_parentheses=false +resharper_space_before_arrow=true +resharper_space_before_attribute_target_colon=false +resharper_space_before_checked_parentheses=false +resharper_space_before_colon=false +resharper_space_before_colon_in_case=false +resharper_space_before_colon_in_inheritance_clause=true +resharper_space_before_colon_in_type_annotation=false +resharper_space_before_comma=false +resharper_space_before_default_parentheses=false +resharper_space_before_empty_invocation_parentheses=false +resharper_space_before_empty_method_parentheses=false +resharper_space_before_for_colon=true +resharper_space_before_function_comma=false +resharper_space_before_initializer_braces=false +resharper_space_before_invocation_parentheses=false +resharper_space_before_label_colon=false +resharper_space_before_lambda_parentheses=false +resharper_space_before_media_colon=false +resharper_space_before_media_comma=false +resharper_space_before_method_parentheses=false +resharper_space_before_nameof_parentheses=false +resharper_space_before_new_parentheses=false +resharper_space_before_nullable_mark=false +resharper_space_before_open_square_brackets=false +resharper_space_before_pointer_asterik_declaration=false +resharper_space_before_property_colon=false +resharper_space_before_property_semicolon=false +resharper_space_before_ptr_in_abstract_decl=false +resharper_space_before_ptr_in_data_member=false +resharper_space_before_ptr_in_data_members=true +resharper_space_before_ptr_in_method=false +resharper_space_before_ref_in_abstract_decl=false +resharper_space_before_ref_in_data_member=false +resharper_space_before_ref_in_data_members=true +resharper_space_before_ref_in_method=false +resharper_space_before_selector_comma=false +resharper_space_before_semicolon=false +resharper_space_before_semicolon_in_for_statement=false +resharper_space_before_separator=false +resharper_space_before_singleline_accessorholder=true +resharper_space_before_sizeof_parentheses=false +resharper_space_before_template_args=false +resharper_space_before_template_params=true +resharper_space_before_ternary_colon=true +resharper_space_before_ternary_quest=true +resharper_space_before_trailing_comment=true +resharper_space_before_typeof_parentheses=false +resharper_space_before_type_argument_angle=false +resharper_space_before_type_parameters_brackets=false +resharper_space_before_type_parameter_angle=false +resharper_space_before_type_parameter_constraint_colon=true +resharper_space_before_type_parameter_parentheses=true +resharper_space_between_accessors_in_singleline_property=true +resharper_space_between_attribute_sections=true +resharper_space_between_closing_angle_brackets_in_template_args=false +resharper_space_between_empty_square_brackets=false +resharper_space_between_keyword_and_expression=true +resharper_space_between_keyword_and_type=true +resharper_space_between_method_call_empty_parameter_list_parentheses=false +resharper_space_between_method_call_name_and_opening_parenthesis=false +resharper_space_between_method_call_parameter_list_parentheses=false +resharper_space_between_method_declaration_empty_parameter_list_parentheses=false +resharper_space_between_method_declaration_name_and_open_parenthesis=false +resharper_space_between_method_declaration_parameter_list_parentheses=false +resharper_space_between_parentheses_of_control_flow_statements=false +resharper_space_between_square_brackets=false +resharper_space_between_typecast_parentheses=false +resharper_space_colon_after=true +resharper_space_colon_before=false +resharper_space_comma=true +resharper_space_equals=true +resharper_space_inside_braces=true +resharper_space_in_singleline_accessorholder=true +resharper_space_in_singleline_anonymous_method=true +resharper_space_in_singleline_method=true +resharper_space_near_postfix_and_prefix_op=false +resharper_space_within_array_initialization_braces=false +resharper_space_within_array_rank_empty_parentheses=false +resharper_space_within_array_rank_parentheses=false +resharper_space_within_attribute_angles=false +resharper_space_within_attribute_match_brackets=false +resharper_space_within_checked_parentheses=false +resharper_space_within_default_parentheses=false +resharper_space_within_empty_braces=true +resharper_space_within_empty_initializer_braces=false +resharper_space_within_empty_invocation_parentheses=false +resharper_space_within_empty_method_parentheses=false +resharper_space_within_empty_object_literal_braces=false +resharper_space_within_empty_template_params=false +resharper_space_within_expression_parentheses=false +resharper_space_within_function_parentheses=false +resharper_space_within_import_braces=true +resharper_space_within_initializer_braces=false +resharper_space_within_invocation_parentheses=false +resharper_space_within_media_block=true +resharper_space_within_media_parentheses=false +resharper_space_within_method_parentheses=false +resharper_space_within_nameof_parentheses=false +resharper_space_within_new_parentheses=false +resharper_space_within_object_literal_braces=true +resharper_space_within_parentheses=false +resharper_space_within_property_block=true +resharper_space_within_single_line_array_initializer_braces=true +resharper_space_within_sizeof_parentheses=false +resharper_space_within_template_args=false +resharper_space_within_template_argument=false +resharper_space_within_template_params=false +resharper_space_within_tuple_parentheses=false +resharper_space_within_typeof_parentheses=false +resharper_space_within_type_argument_angles=false +resharper_space_within_type_parameters_brackets=false +resharper_space_within_type_parameter_angles=false +resharper_space_within_type_parameter_parentheses=false +resharper_special_else_if_treatment=true +resharper_static_members_qualify_members=none +resharper_static_members_qualify_with=declared_type +resharper_stick_comment=true +resharper_support_vs_event_naming_pattern=true +resharper_termination_style=ensure_semicolon +resharper_toplevel_function_declaration_return_type_style=do_not_change +resharper_toplevel_function_definition_return_type_style=do_not_change +resharper_trailing_comma_in_multiline_lists=true +resharper_trailing_comma_in_singleline_lists=false +resharper_types_braces=end_of_line +resharper_use_continuous_indent_inside_initializer_braces=true +resharper_use_continuous_indent_inside_parens=true +resharper_use_continuous_line_indent_in_expression_braces=false +resharper_use_continuous_line_indent_in_method_pars=false +resharper_use_heuristics_for_body_style=true +resharper_use_indents_from_main_language_in_file=true +resharper_use_indent_from_previous_element=true +resharper_use_indent_from_vs=false +resharper_use_roslyn_logic_for_evident_types=false +resharper_vb_align_multiline_argument=true +resharper_vb_align_multiline_expression=true +resharper_vb_align_multiline_parameter=true +resharper_vb_align_multiple_declaration=true +resharper_vb_insert_final_newline=false +resharper_vb_max_line_length=120 +resharper_vb_place_field_attribute_on_same_line=true +resharper_vb_place_method_attribute_on_same_line=false +resharper_vb_place_type_attribute_on_same_line=false +resharper_vb_prefer_qualified_reference=false +resharper_vb_space_after_unary_operator=true +resharper_vb_space_around_multiplicative_op=false +resharper_vb_wrap_arguments_style=wrap_if_long +resharper_vb_wrap_before_binary_opsign=false +resharper_vb_wrap_lines=true +resharper_vb_wrap_parameters_style=wrap_if_long +resharper_wrap_after_binary_opsign=true +resharper_wrap_after_declaration_lpar=false +resharper_wrap_after_dot=false +resharper_wrap_after_dot_in_method_calls=false +resharper_wrap_after_expression_lbrace=true +resharper_wrap_after_invocation_lpar=false +resharper_wrap_around_elements=true +resharper_wrap_array_initializer_style=chop_always +resharper_wrap_array_literals=chop_if_long +resharper_wrap_base_clause_style=wrap_if_long +resharper_wrap_before_arrow_with_expressions=true +resharper_wrap_before_binary_pattern_op=true +resharper_wrap_before_colon=false +resharper_wrap_before_comma=false +resharper_wrap_before_comma_in_base_clause=false +resharper_wrap_before_declaration_lpar=false +resharper_wrap_before_declaration_rpar=false +resharper_wrap_before_dot=true +resharper_wrap_before_eq=false +resharper_wrap_before_expression_rbrace=true +resharper_wrap_before_extends_colon=false +resharper_wrap_before_first_type_parameter_constraint=false +resharper_wrap_before_invocation_lpar=false +resharper_wrap_before_invocation_rpar=false +resharper_wrap_before_linq_expression=false +resharper_wrap_before_ternary_opsigns=true +resharper_wrap_before_type_parameter_langle=false +resharper_wrap_braced_init_list_style=wrap_if_long +resharper_wrap_chained_binary_expressions=chop_if_long +resharper_wrap_chained_binary_patterns=wrap_if_long +resharper_wrap_chained_method_calls=wrap_if_long +resharper_wrap_ctor_initializer_style=wrap_if_long +resharper_wrap_enumeration_style=chop_if_long +resharper_wrap_enum_declaration=chop_always +resharper_wrap_enum_style=do_not_change +resharper_wrap_extends_list_style=wrap_if_long +resharper_wrap_imports=chop_if_long +resharper_wrap_multiple_declaration_style=chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style=chop_if_long +resharper_wrap_object_literals=chop_if_long +resharper_wrap_property_pattern=chop_if_long +resharper_wrap_switch_expression=chop_always +resharper_wrap_ternary_expr_style=chop_if_long +resharper_wrap_union_type_usage=chop_if_long +resharper_wrap_verbatim_interpolated_strings=no_wrap +resharper_xmldoc_attribute_indent=single_indent +resharper_xmldoc_insert_final_newline=false +resharper_xmldoc_linebreak_before_elements=summary,remarks,example,returns,param,typeparam,value,para +resharper_xmldoc_max_blank_lines_between_tags=0 +resharper_xmldoc_max_line_length=120 +resharper_xmldoc_pi_attribute_style=do_not_touch +resharper_xmldoc_space_before_self_closing=true +resharper_xmldoc_wrap_lines=true +resharper_xmldoc_wrap_tags_and_pi=true +resharper_xmldoc_wrap_text=true +resharper_xml_attribute_indent=align_by_first_attribute +resharper_xml_insert_final_newline=false +resharper_xml_linebreak_before_elements= +resharper_xml_max_blank_lines_between_tags=2 +resharper_xml_max_line_length=120 +resharper_xml_pi_attribute_style=do_not_touch +resharper_xml_space_before_self_closing=true +resharper_xml_wrap_lines=true +resharper_xml_wrap_tags_and_pi=true +resharper_xml_wrap_text=false + +# ReSharper inspection severities +resharper_abstract_class_constructor_can_be_made_protected_highlighting=hint +resharper_access_rights_in_text_highlighting=warning +resharper_access_to_disposed_closure_highlighting=warning +resharper_access_to_for_each_variable_in_closure_highlighting=warning +resharper_access_to_modified_closure_highlighting=warning +resharper_access_to_static_member_via_derived_type_highlighting=warning +resharper_address_of_marshal_by_ref_object_highlighting=warning +resharper_amd_dependency_path_problem_highlighting=none +resharper_amd_external_module_highlighting=suggestion +resharper_angular_html_banana_highlighting=warning +resharper_annotate_can_be_null_parameter_highlighting=none +resharper_annotate_can_be_null_type_member_highlighting=none +resharper_annotate_not_null_parameter_highlighting=none +resharper_annotate_not_null_type_member_highlighting=none +resharper_annotation_conflict_in_hierarchy_highlighting=warning +resharper_annotation_redundancy_at_value_type_highlighting=warning +resharper_annotation_redundancy_in_hierarchy_highlighting=warning +resharper_arguments_style_anonymous_function_highlighting=hint +resharper_arguments_style_literal_highlighting=hint +resharper_arguments_style_named_expression_highlighting=hint +resharper_arguments_style_other_highlighting=hint +resharper_arguments_style_string_literal_highlighting=hint +resharper_arrange_accessor_owner_body_highlighting=suggestion +resharper_arrange_attributes_highlighting=none +resharper_arrange_constructor_or_destructor_body_highlighting=hint +resharper_arrange_default_value_when_type_evident_highlighting=suggestion +resharper_arrange_default_value_when_type_not_evident_highlighting=hint +resharper_arrange_local_function_body_highlighting=hint +resharper_arrange_method_or_operator_body_highlighting=hint +resharper_arrange_missing_parentheses_highlighting=hint +resharper_arrange_namespace_body_highlighting=hint +resharper_arrange_object_creation_when_type_evident_highlighting=suggestion +resharper_arrange_object_creation_when_type_not_evident_highlighting=hint +resharper_arrange_redundant_parentheses_highlighting=hint +resharper_arrange_static_member_qualifier_highlighting=hint +resharper_arrange_this_qualifier_highlighting=hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting=hint +resharper_arrange_trailing_comma_in_singleline_lists_highlighting=hint +resharper_arrange_type_member_modifiers_highlighting=hint +resharper_arrange_type_modifiers_highlighting=hint +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting=suggestion +resharper_asp_content_placeholder_not_resolved_highlighting=error +resharper_asp_custom_page_parser_filter_type_highlighting=warning +resharper_asp_dead_code_highlighting=warning +resharper_asp_entity_highlighting=warning +resharper_asp_image_highlighting=warning +resharper_asp_invalid_control_type_highlighting=error +resharper_asp_not_resolved_highlighting=error +resharper_asp_ods_method_reference_resolve_error_highlighting=error +resharper_asp_resolve_warning_highlighting=warning +resharper_asp_skin_not_resolved_highlighting=error +resharper_asp_tag_attribute_with_optional_value_highlighting=warning +resharper_asp_theme_not_resolved_highlighting=error +resharper_asp_unused_register_directive_highlighting_highlighting=warning +resharper_asp_warning_highlighting=warning +resharper_assigned_value_is_never_used_highlighting=warning +resharper_assigned_value_wont_be_assigned_to_corresponding_field_highlighting=warning +resharper_assignment_in_conditional_expression_highlighting=warning +resharper_assignment_in_condition_expression_highlighting=warning +resharper_assignment_is_fully_discarded_highlighting=warning +resharper_assign_null_to_not_null_attribute_highlighting=warning +resharper_assign_to_constant_highlighting=error +resharper_assign_to_implicit_global_in_function_scope_highlighting=warning +resharper_asxx_path_error_highlighting=warning +resharper_async_iterator_invocation_without_await_foreach_highlighting=warning +resharper_async_void_lambda_highlighting=warning +resharper_async_void_method_highlighting=none +resharper_auto_property_can_be_made_get_only_global_highlighting=suggestion +resharper_auto_property_can_be_made_get_only_local_highlighting=suggestion +resharper_bad_attribute_brackets_spaces_highlighting=none +resharper_bad_braces_spaces_highlighting=none +resharper_bad_child_statement_indent_highlighting=warning +resharper_bad_colon_spaces_highlighting=none +resharper_bad_comma_spaces_highlighting=none +resharper_bad_control_braces_indent_highlighting=suggestion +resharper_bad_control_braces_line_breaks_highlighting=none +resharper_bad_declaration_braces_indent_highlighting=none +resharper_bad_declaration_braces_line_breaks_highlighting=none +resharper_bad_empty_braces_line_breaks_highlighting=none +resharper_bad_expression_braces_indent_highlighting=none +resharper_bad_expression_braces_line_breaks_highlighting=none +resharper_bad_generic_brackets_spaces_highlighting=none +resharper_bad_indent_highlighting=none +resharper_bad_linq_line_breaks_highlighting=none +resharper_bad_list_line_breaks_highlighting=none +resharper_bad_member_access_spaces_highlighting=none +resharper_bad_namespace_braces_indent_highlighting=none +resharper_bad_parens_line_breaks_highlighting=none +resharper_bad_parens_spaces_highlighting=none +resharper_bad_preprocessor_indent_highlighting=none +resharper_bad_semicolon_spaces_highlighting=none +resharper_bad_spaces_after_keyword_highlighting=none +resharper_bad_square_brackets_spaces_highlighting=none +resharper_bad_switch_braces_indent_highlighting=none +resharper_bad_symbol_spaces_highlighting=none +resharper_base_member_has_params_highlighting=warning +resharper_base_method_call_with_default_parameter_highlighting=warning +resharper_base_object_equals_is_object_equals_highlighting=warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting=warning +resharper_bitwise_operator_on_enum_without_flags_highlighting=warning +resharper_block_scope_redeclaration_highlighting=error +resharper_built_in_type_reference_style_for_member_access_highlighting=hint +resharper_built_in_type_reference_style_highlighting=hint +resharper_by_ref_argument_is_volatile_field_highlighting=warning +resharper_caller_callee_using_error_highlighting=error +resharper_caller_callee_using_highlighting=warning +resharper_cannot_apply_equality_operator_to_type_highlighting=warning +resharper_center_tag_is_obsolete_highlighting=warning +resharper_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_check_for_reference_equality_instead_3_highlighting=suggestion +resharper_check_for_reference_equality_instead_4_highlighting=suggestion +resharper_check_namespace_highlighting=warning +resharper_class_cannot_be_instantiated_highlighting=warning +resharper_class_can_be_sealed_global_highlighting=none +resharper_class_can_be_sealed_local_highlighting=none +resharper_class_highlighting=suggestion +resharper_class_never_instantiated_global_highlighting=suggestion +resharper_class_never_instantiated_local_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_global_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_local_highlighting=suggestion +resharper_clear_attribute_is_obsolete_all_highlighting=warning +resharper_clear_attribute_is_obsolete_highlighting=warning +resharper_closure_on_modified_variable_highlighting=warning +resharper_coerced_equals_using_highlighting=warning +resharper_coerced_equals_using_with_null_undefined_highlighting=none +resharper_collection_never_queried_global_highlighting=warning +resharper_collection_never_queried_local_highlighting=warning +resharper_collection_never_updated_global_highlighting=warning +resharper_collection_never_updated_local_highlighting=warning +resharper_comma_not_valid_here_highlighting=error +resharper_comment_typo_highlighting=suggestion +resharper_common_js_external_module_highlighting=suggestion +resharper_compare_non_constrained_generic_with_null_highlighting=none +resharper_compare_of_floats_by_equality_operator_highlighting=none +resharper_conditional_ternary_equal_branch_highlighting=warning +resharper_condition_is_always_const_highlighting=warning +resharper_condition_is_always_true_or_false_highlighting=warning +resharper_confusing_char_as_integer_in_constructor_highlighting=warning +resharper_constant_conditional_access_qualifier_highlighting=warning +resharper_constant_null_coalescing_condition_highlighting=warning +resharper_constructor_call_not_used_highlighting=warning +resharper_constructor_initializer_loop_highlighting=warning +resharper_container_annotation_redundancy_highlighting=warning +resharper_context_value_is_provided_highlighting=none +resharper_contract_annotation_not_parsed_highlighting=warning +resharper_convert_closure_to_method_group_highlighting=suggestion +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting=hint +resharper_convert_if_do_to_while_highlighting=suggestion +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_expression_highlighting=suggestion +resharper_convert_if_statement_to_return_statement_highlighting=hint +resharper_convert_if_statement_to_switch_expression_highlighting=hint +resharper_convert_if_statement_to_switch_statement_highlighting=hint +resharper_convert_if_to_or_expression_highlighting=suggestion +resharper_convert_nullable_to_short_form_highlighting=suggestion +resharper_convert_switch_statement_to_switch_expression_highlighting=hint +resharper_convert_to_auto_property_highlighting=suggestion +resharper_convert_to_auto_property_when_possible_highlighting=hint +resharper_convert_to_auto_property_with_private_setter_highlighting=hint +resharper_convert_to_compound_assignment_highlighting=hint +resharper_convert_to_constant_global_highlighting=hint +resharper_convert_to_constant_local_highlighting=hint +resharper_convert_to_lambda_expression_highlighting=suggestion +resharper_convert_to_lambda_expression_when_possible_highlighting=none +resharper_convert_to_local_function_highlighting=suggestion +resharper_convert_to_null_coalescing_compound_assignment_highlighting=suggestion +resharper_convert_to_primary_constructor_highlighting=suggestion +resharper_convert_to_static_class_highlighting=suggestion +resharper_convert_to_using_declaration_highlighting=suggestion +resharper_convert_to_vb_auto_property_highlighting=suggestion +resharper_convert_to_vb_auto_property_when_possible_highlighting=hint +resharper_convert_to_vb_auto_property_with_private_setter_highlighting=hint +resharper_convert_type_check_pattern_to_null_check_highlighting=warning +resharper_convert_type_check_to_null_check_highlighting=warning +resharper_co_variant_array_conversion_highlighting=warning +resharper_cpp_abstract_class_without_specifier_highlighting=warning +resharper_cpp_abstract_final_class_highlighting=warning +resharper_cpp_abstract_virtual_function_call_in_ctor_highlighting=error +resharper_cpp_access_specifier_with_no_declarations_highlighting=suggestion +resharper_cpp_assigned_value_is_never_used_highlighting=warning +resharper_cpp_awaiter_type_is_not_class_highlighting=warning +resharper_cpp_bad_angle_brackets_spaces_highlighting=none +resharper_cpp_bad_braces_spaces_highlighting=none +resharper_cpp_bad_child_statement_indent_highlighting=none +resharper_cpp_bad_colon_spaces_highlighting=none +resharper_cpp_bad_comma_spaces_highlighting=none +resharper_cpp_bad_control_braces_indent_highlighting=none +resharper_cpp_bad_control_braces_line_breaks_highlighting=none +resharper_cpp_bad_declaration_braces_indent_highlighting=none +resharper_cpp_bad_declaration_braces_line_breaks_highlighting=none +resharper_cpp_bad_empty_braces_line_breaks_highlighting=none +resharper_cpp_bad_expression_braces_indent_highlighting=none +resharper_cpp_bad_expression_braces_line_breaks_highlighting=none +resharper_cpp_bad_indent_highlighting=none +resharper_cpp_bad_list_line_breaks_highlighting=none +resharper_cpp_bad_member_access_spaces_highlighting=none +resharper_cpp_bad_namespace_braces_indent_highlighting=none +resharper_cpp_bad_parens_line_breaks_highlighting=none +resharper_cpp_bad_parens_spaces_highlighting=none +resharper_cpp_bad_semicolon_spaces_highlighting=none +resharper_cpp_bad_spaces_after_keyword_highlighting=none +resharper_cpp_bad_square_brackets_spaces_highlighting=none +resharper_cpp_bad_switch_braces_indent_highlighting=none +resharper_cpp_bad_symbol_spaces_highlighting=none +resharper_cpp_boolean_increment_expression_highlighting=warning +resharper_cpp_boost_format_bad_code_highlighting=warning +resharper_cpp_boost_format_legacy_code_highlighting=suggestion +resharper_cpp_boost_format_mixed_args_highlighting=error +resharper_cpp_boost_format_too_few_args_highlighting=error +resharper_cpp_boost_format_too_many_args_highlighting=warning +resharper_cpp_clang_tidy_abseil_duration_addition_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_conversion_cast_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_division_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_float_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_scale_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_unnecessary_conversion_highlighting=none +resharper_cpp_clang_tidy_abseil_faster_strsplit_delimiter_highlighting=none +resharper_cpp_clang_tidy_abseil_no_internal_dependencies_highlighting=none +resharper_cpp_clang_tidy_abseil_no_namespace_highlighting=none +resharper_cpp_clang_tidy_abseil_redundant_strcat_calls_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_startswith_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_str_contains_highlighting=none +resharper_cpp_clang_tidy_abseil_str_cat_append_highlighting=none +resharper_cpp_clang_tidy_abseil_time_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_time_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_upgrade_duration_conversions_highlighting=none +resharper_cpp_clang_tidy_altera_id_dependent_backward_branch_highlighting=none +resharper_cpp_clang_tidy_altera_kernel_name_restriction_highlighting=none +resharper_cpp_clang_tidy_altera_single_work_item_barrier_highlighting=none +resharper_cpp_clang_tidy_altera_struct_pack_align_highlighting=none +resharper_cpp_clang_tidy_altera_unroll_loops_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept4_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_creat_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_dup_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_fopen_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_memfd_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_open_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe2_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_socket_highlighting=none +resharper_cpp_clang_tidy_android_comparison_in_temp_failure_retry_highlighting=none +resharper_cpp_clang_tidy_boost_use_to_string_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_argument_comment_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_assert_side_effect_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bad_signal_to_kill_thread_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bool_pointer_implicit_conversion_highlighting=none +resharper_cpp_clang_tidy_bugprone_branch_clone_highlighting=warning +resharper_cpp_clang_tidy_bugprone_copy_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dangling_handle_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dynamic_static_initializers_highlighting=warning +resharper_cpp_clang_tidy_bugprone_easily_swappable_parameters_highlighting=none +resharper_cpp_clang_tidy_bugprone_exception_escape_highlighting=none +resharper_cpp_clang_tidy_bugprone_fold_init_type_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forwarding_reference_overload_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forward_declaration_namespace_highlighting=warning +resharper_cpp_clang_tidy_bugprone_implicit_widening_of_multiplication_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_inaccurate_erase_highlighting=warning +resharper_cpp_clang_tidy_bugprone_incorrect_roundings_highlighting=warning +resharper_cpp_clang_tidy_bugprone_infinite_loop_highlighting=warning +resharper_cpp_clang_tidy_bugprone_integer_division_highlighting=warning +resharper_cpp_clang_tidy_bugprone_lambda_function_name_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_parentheses_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_repeated_side_effects_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_operator_in_strlen_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_pointer_arithmetic_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_widening_cast_highlighting=warning +resharper_cpp_clang_tidy_bugprone_move_forwarding_reference_highlighting=warning +resharper_cpp_clang_tidy_bugprone_multiple_statement_macro_highlighting=warning +resharper_cpp_clang_tidy_bugprone_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_not_null_terminated_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_no_escape_highlighting=warning +resharper_cpp_clang_tidy_bugprone_parent_virtual_call_highlighting=warning +resharper_cpp_clang_tidy_bugprone_posix_return_highlighting=warning +resharper_cpp_clang_tidy_bugprone_redundant_branch_condition_highlighting=warning +resharper_cpp_clang_tidy_bugprone_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signal_handler_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signed_char_misuse_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_container_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_expression_highlighting=warning +resharper_cpp_clang_tidy_bugprone_spuriously_wake_up_functions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_integer_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_literal_with_embedded_nul_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_enum_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_memset_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_missing_comma_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_semicolon_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_string_compare_highlighting=warning +resharper_cpp_clang_tidy_bugprone_swapped_arguments_highlighting=warning +resharper_cpp_clang_tidy_bugprone_terminating_continue_highlighting=warning +resharper_cpp_clang_tidy_bugprone_throw_keyword_missing_highlighting=warning +resharper_cpp_clang_tidy_bugprone_too_small_loop_variable_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undefined_memory_manipulation_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undelegated_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unhandled_exception_at_new_highlighting=none +resharper_cpp_clang_tidy_bugprone_unhandled_self_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_raii_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_return_value_highlighting=warning +resharper_cpp_clang_tidy_bugprone_use_after_move_highlighting=warning +resharper_cpp_clang_tidy_bugprone_virtual_near_miss_highlighting=suggestion +resharper_cpp_clang_tidy_cert_con36_c_highlighting=none +resharper_cpp_clang_tidy_cert_con54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl03_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl16_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl21_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl37_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl51_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_dcl59_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_env33_c_highlighting=none +resharper_cpp_clang_tidy_cert_err09_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err34_c_highlighting=suggestion +resharper_cpp_clang_tidy_cert_err52_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err58_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err60_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_err61_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_fio38_c_highlighting=none +resharper_cpp_clang_tidy_cert_flp30_c_highlighting=warning +resharper_cpp_clang_tidy_cert_mem57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_msc30_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc32_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_msc51_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop11_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_pos44_c_highlighting=none +resharper_cpp_clang_tidy_cert_pos47_c_highlighting=none +resharper_cpp_clang_tidy_cert_sig30_c_highlighting=none +resharper_cpp_clang_tidy_cert_str34_c_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_google_g_test_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_cast_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_std_c_library_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_trust_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_builtin_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_no_return_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_divide_zero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_dynamic_type_propagation_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_nonnil_string_constants_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_non_null_param_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_null_dereference_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_address_escape_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_addr_escape_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_undefined_binary_operator_result_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_array_subscript_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_assign_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_branch_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_captured_block_variable_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_undef_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_vla_size_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_inner_pointer_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_move_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_leaks_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_placement_new_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_pure_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_self_assignment_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_smart_ptr_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_virtual_call_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_deadcode_dead_stores_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_fuchsia_handle_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullability_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_dereferenced_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_uninitialized_object_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_mpi_mpi_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_empty_localization_context_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_non_localized_string_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_os_object_c_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_gcd_antipattern_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_padding_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_portability_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_at_sync_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_autorelease_write_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_class_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_incompatible_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_loops_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_missing_super_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_nil_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_non_nil_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_autorelease_pool_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_obj_c_generics_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_run_loop_autorelease_leak_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_self_init_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_super_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_unused_ivars_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_variadic_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_number_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_retain_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_out_of_bounds_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_pointer_sized_values_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_mig_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_ns_or_cf_error_deref_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_number_object_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_obj_c_property_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_os_object_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_sec_keychain_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_float_loop_counter_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcmp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcopy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bzero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_decode_value_of_obj_c_type_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_deprecated_or_unsafe_buffer_handling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_getpw_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_gets_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mkstemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mktemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_rand_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_security_syntax_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_strcpy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_unchecked_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_bad_size_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_c_string_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_null_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_dynamic_memory_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_sizeof_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_mismatched_deallocator_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_copy_to_self_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_uninitialized_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_unterminated_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_valist_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_no_uncounted_member_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_ref_cntbl_base_virtual_dtor_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_uncounted_lambda_captures_checker_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_absolute_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_abstract_final_class_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_abstract_vbase_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_packed_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_temporary_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_aix_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_align_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_with_align_alignof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_ellipsis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_member_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_reversed_operator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_analyzer_incompatible_plugin_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anonymous_pack_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_bridge_casts_disallowed_in_nonarc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_maybe_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_non_pod_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_perform_selector_leaks_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_retain_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_unsafe_retained_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_argument_outside_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_asm_operand_widths_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assign_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assume_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atimport_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_implicit_seq_cst_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_atomic_memory_ordering_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_property_with_user_defined_accessor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_attribute_packed_for_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_at_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_disable_vptr_sanitizer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_import_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_storage_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_var_id_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_avr_rtlib_linking_quirks_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_backslash_newline_escape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bad_function_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_binding_in_condition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bind_to_temporary_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_conditional_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_block_capture_autoreleasing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_operation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_braced_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bridge_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_assume_aligned_alignment_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_builtin_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_memcpy_chk_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_requires_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c2x_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_c99_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_called_once_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_call_to_pure_virtual_from_ctor_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_align_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_calling_convention_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_function_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_of_sel_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_unrelated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cf_string_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_char_subscripts_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_clang_cl_pch_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_class_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_class_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cmse_union_leak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comma_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compare_distinct_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_completion_handler_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_complex_component_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_space_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_concepts_ts_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_conditional_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conditional_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_config_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_evaluated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_logical_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constexpr_not_const_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_consumed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_coroutine_missing_unhandled_exception_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_covered_switch_default_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_deprecated_writable_strings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_reserved_user_defined_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_inline_namespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_narrowing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_mangling_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2b_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_bind_to_temporary_copy_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_extra_semi_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_local_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_unnamed_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_binary_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cstring_format_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ctad_maybe_unsupported_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ctu_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cuda_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_custom_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cxx_attribute_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_else_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_gsl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_initializer_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_darwin_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_debug_compression_unavailable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_declaration_after_statement_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_defaulted_function_deleted_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_delegating_ctor_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_incomplete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_altivec_src_compat_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_array_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_comma_subscript_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_implementations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_isa_usage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_perform_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_this_capture_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_volatile_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_direct_ivar_access_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_disabled_macro_expansion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_distributed_object_modifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_division_by_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllexport_explicit_instantiation_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllimport_static_field_def_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dll_attribute_on_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_deprecated_sync_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_html_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_unknown_command_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_dollar_in_identifier_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_double_promotion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_decl_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_arg_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_class_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_embedded_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_decomposition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_init_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_translation_unit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_encode_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_too_large_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_error_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_exceptions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_excess_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_exit_time_destructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_expansion_to_defined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_initialize_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_ownership_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_unnamed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_using_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extern_c_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_extern_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_tokens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_final_dtor_non_final_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fixed_enum_extension_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_fixed_point_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flag_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flexible_array_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_equal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_overflow_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_zero_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_extra_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_insufficient_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_invalid_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_nonliteral_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_non_iso_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_security_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_type_confusion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_zero_length_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fortify_source_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_for_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_four_char_constants_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_framework_include_private_from_public_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_larger_than_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_free_nonheap_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_def_in_objc_container_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_multiversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gcc_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_constructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_isel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_alignof_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_anonymous_struct_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_gnu_array_member_paren_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_auto_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_case_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_complex_integer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_compound_literal_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_conditional_omitted_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_union_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_folding_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_imaginary_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_include_next_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_inline_cpp_without_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_label_as_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_redeclared_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_statement_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_string_literal_operator_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_union_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_variable_sized_type_not_at_end_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_zero_variadic_macro_arguments_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_hygiene_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_hip_only_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_idiomatic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_availability_without_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_optimization_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_intrinsic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_optimize_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_qualifiers_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_implicitly_unsigned_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_const_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_conversion_floating_point_to_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_exception_spec_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_per_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fixed_point_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_function_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_retain_self_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_import_preprocessor_directive_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_absolute_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_function_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_library_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_ms_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_discards_qualifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_property_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_framework_module_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_implementation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_setjmp_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_umbrella_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_dllimport_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_destructor_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_independent_class_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_infinite_recursion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_initializer_overrides_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_injected_class_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_asm_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_namespace_reopened_noninline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_instantiation_after_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_integer_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_interrupt_service_routine_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_in_bool_context_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_void_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_constexpr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_iboutlet_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_initializer_from_system_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_ios_deployment_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_no_builtin_names_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_offsetof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_or_nonexistent_directory_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_pp_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_source_encoding_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_token_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_jump_seh_finally_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_keyword_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_keyword_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_knr_promoted_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_language_extension_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_large_by_value_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_local_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_not_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_op_parentheses_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_malformed_warning_check_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_many_braces_around_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_max_tokens_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_max_unsigned_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memset_transposed_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memsize_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_method_signatures_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_abstract_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_anon_tag_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_charize_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_comment_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_const_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cpp_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_default_arg_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_drectve_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_end_of_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_forward_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exists_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_explicit_constructor_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_fixed_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_flexible_array_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_goto_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_mutable_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_pure_definition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_redeclare_static_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_sealed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_static_assert_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_union_member_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_unqualified_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_using_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_void_pseudo_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_misleading_indentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_parameter_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_return_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_tags_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_braces_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_constinit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_field_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_method_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noescape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototypes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototype_for_cc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_selector_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_variable_declarations_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_misspelled_assumption_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_ambiguous_internal_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_import_nested_redundant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_conflict_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_config_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_import_in_extern_c_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_msvc_not_found_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multichar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multiple_move_vbase_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nested_anon_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_newline_eof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_new_returns_null_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_noderef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonnull_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_system_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_vector_initialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nontrivial_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_c_typedef_for_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_literal_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_framework_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_pod_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_power_of_two_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsconsumed_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsreturns_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ns_object_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_on_arrays_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_declspec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_inferred_on_nested_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullable_to_nonnull_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_character_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_subtraction_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_odr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_old_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_opencl_unsupported_rgba_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp51_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_clauses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_loop_form_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_mapping_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_option_ignored_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ordered_compare_function_pointers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_line_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_scope_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overlength_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_virtual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_t_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_over_aligned_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_packed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_padded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_equality_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pass_failed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pch_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_core_features_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pessimizing_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_arith_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_integer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_sign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_poison_system_directories_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_potentially_evaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_clang_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_messages_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_once_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_system_header_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_predefined_identifier_outside_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_openmp51_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_private_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_out_of_date_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_unprofiled_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_access_dot_syntax_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_attribute_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_property_synthesis_ambiguity_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_psabi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_qualified_void_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_quoted_include_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_bind_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_construct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_readonly_iboutlet_property_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_expr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_forward_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redeclared_class_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reinterpret_base_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_ctor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_init_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_super_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_id_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_macro_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_user_defined_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_retained_language_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_stack_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_std_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_c_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_rewrite_not_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_overloaded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_semicolon_before_method_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sentinel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_serialized_diagnostics_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_modified_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_uncaptured_local_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_negative_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_sign_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shorten64_to32_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_enum_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_unsigned_wchar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_decay_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slash_u_filename_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slh_asm_goto_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sometimes_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_source_uses_openmp_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_spir_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_inline_explicit_instantiation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_local_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_self_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_stdlibcxx_not_found_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_strict_prototypes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strict_selector_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_concatenation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_char_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strlcpy_strlcat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strncat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suggest_destructor_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_suggest_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_super_class_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suspicious_bzero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sync_fetch_and_nand_semantics_changed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_bitwise_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_in_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_out_of_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_objc_bool_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_overlap_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_type_limit_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_undefined_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_char_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_enum_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_zero_compare_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_tautological_value_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tentative_definition_incomplete_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_beta_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_precise_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_verbose_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_trigraphs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typedef_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typename_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_type_safety_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unable_to_open_stats_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unavailable_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undeclared_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_func_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_reinterpret_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_var_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_prefix_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_underaligned_exception_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unevaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_new_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_homoglyph_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_whitespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_zero_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_const_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_cuda_version_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_escape_sequence_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_pragmas_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_sanitizers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_warning_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unnamed_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_internal_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_break_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_loop_increment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_return_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsequenced_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_abs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_availability_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_cb_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_dll_base_class_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_gpopt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_nan_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_target_opt_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unusable_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_const_variable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_exception_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_getter_return_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_label_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_lambda_capture_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_local_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_private_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_property_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_result_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_volatile_lvalue_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_used_but_marked_unused_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_literals_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_variadic_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vector_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vec_elem_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vexing_parse_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_ptr_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_wasm_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_template_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_writable_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_xor_used_as_pow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_zero_as_null_pointer_constant_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_zero_length_array_highlighting=warning +resharper_cpp_clang_tidy_concurrency_mt_unsafe_highlighting=warning +resharper_cpp_clang_tidy_concurrency_thread_canceltype_asynchronous_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_non_const_global_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_c_copy_assignment_signature_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_explicit_virtual_functions_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_init_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_interfaces_global_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_macro_usage_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_no_malloc_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_owning_memory_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_prefer_member_initializer_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_array_to_pointer_decay_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_constant_array_index_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_pointer_arithmetic_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_const_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_cstyle_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_member_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_reinterpret_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_static_cast_downcast_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_union_access_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_vararg_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_slicing_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_special_member_functions_highlighting=suggestion +resharper_cpp_clang_tidy_darwin_avoid_spinlock_highlighting=none +resharper_cpp_clang_tidy_darwin_dispatch_once_nonstatic_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_calls_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_declarations_highlighting=none +resharper_cpp_clang_tidy_fuchsia_header_anon_namespaces_highlighting=none +resharper_cpp_clang_tidy_fuchsia_multiple_inheritance_highlighting=none +resharper_cpp_clang_tidy_fuchsia_overloaded_operator_highlighting=none +resharper_cpp_clang_tidy_fuchsia_statically_constructed_objects_highlighting=none +resharper_cpp_clang_tidy_fuchsia_trailing_return_highlighting=none +resharper_cpp_clang_tidy_fuchsia_virtual_inheritance_highlighting=none +resharper_cpp_clang_tidy_google_build_explicit_make_pair_highlighting=none +resharper_cpp_clang_tidy_google_build_namespaces_highlighting=none +resharper_cpp_clang_tidy_google_build_using_namespace_highlighting=none +resharper_cpp_clang_tidy_google_default_arguments_highlighting=none +resharper_cpp_clang_tidy_google_explicit_constructor_highlighting=none +resharper_cpp_clang_tidy_google_global_names_in_headers_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_nsobject_new_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_throwing_exception_highlighting=none +resharper_cpp_clang_tidy_google_objc_function_naming_highlighting=none +resharper_cpp_clang_tidy_google_objc_global_variable_declaration_highlighting=none +resharper_cpp_clang_tidy_google_readability_avoid_underscore_in_googletest_name_highlighting=none +resharper_cpp_clang_tidy_google_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_google_readability_casting_highlighting=none +resharper_cpp_clang_tidy_google_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_google_readability_namespace_comments_highlighting=none +resharper_cpp_clang_tidy_google_readability_todo_highlighting=none +resharper_cpp_clang_tidy_google_runtime_int_highlighting=none +resharper_cpp_clang_tidy_google_runtime_operator_highlighting=warning +resharper_cpp_clang_tidy_google_upgrade_googletest_case_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_hicpp_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_hicpp_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_hicpp_deprecated_headers_highlighting=none +resharper_cpp_clang_tidy_hicpp_exception_baseclass_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_explicit_conversions_highlighting=none +resharper_cpp_clang_tidy_hicpp_function_size_highlighting=none +resharper_cpp_clang_tidy_hicpp_invalid_access_moved_highlighting=none +resharper_cpp_clang_tidy_hicpp_member_init_highlighting=none +resharper_cpp_clang_tidy_hicpp_move_const_arg_highlighting=none +resharper_cpp_clang_tidy_hicpp_multiway_paths_covered_highlighting=warning +resharper_cpp_clang_tidy_hicpp_named_parameter_highlighting=none +resharper_cpp_clang_tidy_hicpp_new_delete_operators_highlighting=none +resharper_cpp_clang_tidy_hicpp_noexcept_move_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_array_decay_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_assembler_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_malloc_highlighting=none +resharper_cpp_clang_tidy_hicpp_signed_bitwise_highlighting=none +resharper_cpp_clang_tidy_hicpp_special_member_functions_highlighting=none +resharper_cpp_clang_tidy_hicpp_static_assert_highlighting=none +resharper_cpp_clang_tidy_hicpp_undelegated_constructor_highlighting=none +resharper_cpp_clang_tidy_hicpp_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_auto_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_emplace_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_default_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_delete_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_noexcept_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_override_highlighting=none +resharper_cpp_clang_tidy_hicpp_vararg_highlighting=none +resharper_cpp_clang_tidy_highlighting_highlighting=suggestion +resharper_cpp_clang_tidy_linuxkernel_must_check_errs_highlighting=warning +resharper_cpp_clang_tidy_llvmlibc_callee_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_implementation_in_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_restrict_system_libc_headers_highlighting=none +resharper_cpp_clang_tidy_llvm_else_after_return_highlighting=none +resharper_cpp_clang_tidy_llvm_header_guard_highlighting=none +resharper_cpp_clang_tidy_llvm_include_order_highlighting=none +resharper_cpp_clang_tidy_llvm_namespace_comment_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_isa_or_dyn_cast_in_conditionals_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_register_over_unsigned_highlighting=suggestion +resharper_cpp_clang_tidy_llvm_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_llvm_twine_local_highlighting=none +resharper_cpp_clang_tidy_misc_definitions_in_headers_highlighting=none +resharper_cpp_clang_tidy_misc_misplaced_const_highlighting=warning +resharper_cpp_clang_tidy_misc_new_delete_overloads_highlighting=warning +resharper_cpp_clang_tidy_misc_non_copyable_objects_highlighting=warning +resharper_cpp_clang_tidy_misc_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_misc_no_recursion_highlighting=none +resharper_cpp_clang_tidy_misc_redundant_expression_highlighting=warning +resharper_cpp_clang_tidy_misc_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_misc_throw_by_value_catch_by_reference_highlighting=warning +resharper_cpp_clang_tidy_misc_unconventional_assign_operator_highlighting=warning +resharper_cpp_clang_tidy_misc_uniqueptr_reset_release_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_alias_decls_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_parameters_highlighting=none +resharper_cpp_clang_tidy_misc_unused_using_decls_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_bind_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_modernize_concat_nested_namespaces_highlighting=none +resharper_cpp_clang_tidy_modernize_deprecated_headers_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_deprecated_ios_base_aliases_highlighting=warning +resharper_cpp_clang_tidy_modernize_loop_convert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_make_shared_highlighting=none +resharper_cpp_clang_tidy_modernize_make_unique_highlighting=none +resharper_cpp_clang_tidy_modernize_pass_by_value_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_raw_string_literal_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_redundant_void_arg_highlighting=none +resharper_cpp_clang_tidy_modernize_replace_auto_ptr_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_disallow_copy_and_assign_macro_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_random_shuffle_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_return_braced_init_list_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_shrink_to_fit_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_unary_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_auto_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_bool_literals_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_default_member_init_highlighting=none +resharper_cpp_clang_tidy_modernize_use_emplace_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_default_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_delete_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nodiscard_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_noexcept_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_modernize_use_override_highlighting=none +resharper_cpp_clang_tidy_modernize_use_trailing_return_type_highlighting=none +resharper_cpp_clang_tidy_modernize_use_transparent_functors_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_uncaught_exceptions_highlighting=warning +resharper_cpp_clang_tidy_modernize_use_using_highlighting=none +resharper_cpp_clang_tidy_mpi_buffer_deref_highlighting=warning +resharper_cpp_clang_tidy_mpi_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_objc_avoid_nserror_init_highlighting=warning +resharper_cpp_clang_tidy_objc_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_objc_forbidden_subclassing_highlighting=warning +resharper_cpp_clang_tidy_objc_missing_hash_highlighting=warning +resharper_cpp_clang_tidy_objc_nsinvocation_argument_lifetime_highlighting=warning +resharper_cpp_clang_tidy_objc_property_declaration_highlighting=warning +resharper_cpp_clang_tidy_objc_super_self_highlighting=warning +resharper_cpp_clang_tidy_openmp_exception_escape_highlighting=warning +resharper_cpp_clang_tidy_openmp_use_default_none_highlighting=warning +resharper_cpp_clang_tidy_performance_faster_string_find_highlighting=suggestion +resharper_cpp_clang_tidy_performance_for_range_copy_highlighting=suggestion +resharper_cpp_clang_tidy_performance_implicit_conversion_in_loop_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_algorithm_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_string_concatenation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_vector_operation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_move_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_performance_move_const_arg_highlighting=suggestion +resharper_cpp_clang_tidy_performance_noexcept_move_constructor_highlighting=none +resharper_cpp_clang_tidy_performance_no_automatic_move_highlighting=warning +resharper_cpp_clang_tidy_performance_no_int_to_ptr_highlighting=warning +resharper_cpp_clang_tidy_performance_trivially_destructible_highlighting=suggestion +resharper_cpp_clang_tidy_performance_type_promotion_in_math_fn_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_copy_initialization_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_value_param_highlighting=suggestion +resharper_cpp_clang_tidy_portability_restrict_system_includes_highlighting=none +resharper_cpp_clang_tidy_portability_simd_intrinsics_highlighting=none +resharper_cpp_clang_tidy_readability_avoid_const_params_in_decls_highlighting=none +resharper_cpp_clang_tidy_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_readability_const_return_type_highlighting=none +resharper_cpp_clang_tidy_readability_container_size_empty_highlighting=suggestion +resharper_cpp_clang_tidy_readability_convert_member_functions_to_static_highlighting=none +resharper_cpp_clang_tidy_readability_delete_null_pointer_highlighting=suggestion +resharper_cpp_clang_tidy_readability_else_after_return_highlighting=none +resharper_cpp_clang_tidy_readability_function_cognitive_complexity_highlighting=none +resharper_cpp_clang_tidy_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_readability_identifier_naming_highlighting=none +resharper_cpp_clang_tidy_readability_implicit_bool_conversion_highlighting=none +resharper_cpp_clang_tidy_readability_inconsistent_declaration_parameter_name_highlighting=suggestion +resharper_cpp_clang_tidy_readability_isolate_declaration_highlighting=none +resharper_cpp_clang_tidy_readability_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_readability_make_member_function_const_highlighting=none +resharper_cpp_clang_tidy_readability_misleading_indentation_highlighting=none +resharper_cpp_clang_tidy_readability_misplaced_array_index_highlighting=suggestion +resharper_cpp_clang_tidy_readability_named_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_non_const_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_access_specifiers_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_control_flow_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_declaration_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_function_ptr_dereference_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_member_init_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_preprocessor_highlighting=warning +resharper_cpp_clang_tidy_readability_redundant_smartptr_get_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_cstr_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_init_highlighting=suggestion +resharper_cpp_clang_tidy_readability_simplify_boolean_expr_highlighting=none +resharper_cpp_clang_tidy_readability_simplify_subscript_expr_highlighting=warning +resharper_cpp_clang_tidy_readability_static_accessed_through_instance_highlighting=suggestion +resharper_cpp_clang_tidy_readability_static_definition_in_anonymous_namespace_highlighting=none +resharper_cpp_clang_tidy_readability_string_compare_highlighting=warning +resharper_cpp_clang_tidy_readability_suspicious_call_argument_highlighting=warning +resharper_cpp_clang_tidy_readability_uniqueptr_delete_release_highlighting=suggestion +resharper_cpp_clang_tidy_readability_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_readability_use_anyofallof_highlighting=suggestion +resharper_cpp_clang_tidy_zircon_temporary_objects_highlighting=none +resharper_cpp_class_can_be_final_highlighting=hint +resharper_cpp_class_disallow_lazy_merging_highlighting=warning +resharper_cpp_class_is_incomplete_highlighting=warning +resharper_cpp_class_needs_constructor_because_of_uninitialized_member_highlighting=warning +resharper_cpp_class_never_used_highlighting=warning +resharper_cpp_compile_time_constant_can_be_replaced_with_boolean_constant_highlighting=suggestion +resharper_cpp_const_parameter_in_declaration_highlighting=suggestion +resharper_cpp_const_value_function_return_type_highlighting=suggestion +resharper_cpp_coroutine_call_resolve_error_highlighting=warning +resharper_cpp_cv_qualifier_can_not_be_applied_to_reference_highlighting=warning +resharper_cpp_c_style_cast_highlighting=suggestion +resharper_cpp_declaration_hides_local_highlighting=warning +resharper_cpp_declaration_hides_uncaptured_local_highlighting=hint +resharper_cpp_declaration_specifier_without_declarators_highlighting=warning +resharper_cpp_declarator_disambiguated_as_function_highlighting=warning +resharper_cpp_declarator_never_used_highlighting=warning +resharper_cpp_declarator_used_before_initialization_highlighting=error +resharper_cpp_defaulted_special_member_function_is_implicitly_deleted_highlighting=warning +resharper_cpp_default_case_not_handled_in_switch_statement_highlighting=warning +resharper_cpp_default_initialization_with_no_user_constructor_highlighting=warning +resharper_cpp_default_is_used_as_identifier_highlighting=warning +resharper_cpp_deleting_void_pointer_highlighting=warning +resharper_cpp_dependent_template_without_template_keyword_highlighting=warning +resharper_cpp_dependent_type_without_typename_keyword_highlighting=warning +resharper_cpp_deprecated_entity_highlighting=warning +resharper_cpp_deprecated_register_storage_class_specifier_highlighting=warning +resharper_cpp_dereference_operator_limit_exceeded_highlighting=warning +resharper_cpp_discarded_postfix_operator_result_highlighting=suggestion +resharper_cpp_doxygen_syntax_error_highlighting=warning +resharper_cpp_doxygen_undocumented_parameter_highlighting=suggestion +resharper_cpp_doxygen_unresolved_reference_highlighting=warning +resharper_cpp_empty_declaration_highlighting=warning +resharper_cpp_enforce_cv_qualifiers_order_highlighting=none +resharper_cpp_enforce_cv_qualifiers_placement_highlighting=none +resharper_cpp_enforce_do_statement_braces_highlighting=none +resharper_cpp_enforce_for_statement_braces_highlighting=none +resharper_cpp_enforce_function_declaration_style_highlighting=none +resharper_cpp_enforce_if_statement_braces_highlighting=none +resharper_cpp_enforce_nested_namespaces_style_highlighting=hint +resharper_cpp_enforce_overriding_destructor_style_highlighting=suggestion +resharper_cpp_enforce_overriding_function_style_highlighting=suggestion +resharper_cpp_enforce_type_alias_code_style_highlighting=none +resharper_cpp_enforce_while_statement_braces_highlighting=none +resharper_cpp_entity_assigned_but_no_read_highlighting=warning +resharper_cpp_entity_used_only_in_unevaluated_context_highlighting=warning +resharper_cpp_enumerator_never_used_highlighting=warning +resharper_cpp_equal_operands_in_binary_expression_highlighting=warning +resharper_cpp_explicit_specialization_in_non_namespace_scope_highlighting=warning +resharper_cpp_expression_without_side_effects_highlighting=warning +resharper_cpp_final_function_in_final_class_highlighting=suggestion +resharper_cpp_final_non_overriding_virtual_function_highlighting=suggestion +resharper_cpp_for_loop_can_be_replaced_with_while_highlighting=suggestion +resharper_cpp_functional_style_cast_highlighting=suggestion +resharper_cpp_function_doesnt_return_value_highlighting=warning +resharper_cpp_function_is_not_implemented_highlighting=warning +resharper_cpp_header_has_been_already_included_highlighting=hint +resharper_cpp_hidden_function_highlighting=warning +resharper_cpp_hiding_function_highlighting=warning +resharper_cpp_identical_operands_in_binary_expression_highlighting=warning +resharper_cpp_if_can_be_replaced_by_constexpr_if_highlighting=suggestion +resharper_cpp_implicit_default_constructor_not_available_highlighting=warning +resharper_cpp_incompatible_pointer_conversion_highlighting=warning +resharper_cpp_incomplete_switch_statement_highlighting=warning +resharper_cpp_inconsistent_naming_highlighting=hint +resharper_cpp_incorrect_blank_lines_near_braces_highlighting=none +resharper_cpp_initialized_value_is_always_rewritten_highlighting=warning +resharper_cpp_integral_to_pointer_conversion_highlighting=warning +resharper_cpp_invalid_line_continuation_highlighting=warning +resharper_cpp_join_declaration_and_assignment_highlighting=suggestion +resharper_cpp_lambda_capture_never_used_highlighting=warning +resharper_cpp_local_variable_may_be_const_highlighting=suggestion +resharper_cpp_local_variable_might_not_be_initialized_highlighting=warning +resharper_cpp_local_variable_with_non_trivial_dtor_is_never_used_highlighting=none +resharper_cpp_long_float_highlighting=warning +resharper_cpp_member_function_may_be_const_highlighting=suggestion +resharper_cpp_member_function_may_be_static_highlighting=suggestion +resharper_cpp_member_initializers_order_highlighting=suggestion +resharper_cpp_mismatched_class_tags_highlighting=warning +resharper_cpp_missing_blank_lines_highlighting=none +resharper_cpp_missing_include_guard_highlighting=warning +resharper_cpp_missing_indent_highlighting=none +resharper_cpp_missing_keyword_throw_highlighting=warning +resharper_cpp_missing_linebreak_highlighting=none +resharper_cpp_missing_space_highlighting=none +resharper_cpp_ms_ext_address_of_class_r_value_highlighting=warning +resharper_cpp_ms_ext_binding_r_value_to_lvalue_reference_highlighting=warning +resharper_cpp_ms_ext_copy_elision_in_copy_init_declarator_highlighting=warning +resharper_cpp_ms_ext_double_user_conversion_in_copy_init_highlighting=warning +resharper_cpp_ms_ext_not_initialized_static_const_local_var_highlighting=warning +resharper_cpp_ms_ext_reinterpret_cast_from_nullptr_highlighting=warning +resharper_cpp_multiple_spaces_highlighting=none +resharper_cpp_must_be_public_virtual_to_implement_interface_highlighting=warning +resharper_cpp_mutable_specifier_on_reference_member_highlighting=warning +resharper_cpp_nodiscard_function_without_return_value_highlighting=warning +resharper_cpp_non_exception_safe_resource_acquisition_highlighting=hint +resharper_cpp_non_explicit_conversion_operator_highlighting=hint +resharper_cpp_non_explicit_converting_constructor_highlighting=hint +resharper_cpp_non_inline_function_definition_in_header_file_highlighting=warning +resharper_cpp_non_inline_variable_definition_in_header_file_highlighting=warning +resharper_cpp_not_all_paths_return_value_highlighting=warning +resharper_cpp_no_discard_expression_highlighting=warning +resharper_cpp_object_member_might_not_be_initialized_highlighting=warning +resharper_cpp_outdent_is_off_prev_level_highlighting=none +resharper_cpp_out_parameter_must_be_written_highlighting=warning +resharper_cpp_parameter_may_be_const_highlighting=hint +resharper_cpp_parameter_may_be_const_ptr_or_ref_highlighting=suggestion +resharper_cpp_parameter_names_mismatch_highlighting=hint +resharper_cpp_parameter_never_used_highlighting=hint +resharper_cpp_parameter_value_is_reassigned_highlighting=warning +resharper_cpp_pointer_conversion_drops_qualifiers_highlighting=warning +resharper_cpp_pointer_to_integral_conversion_highlighting=warning +resharper_cpp_polymorphic_class_with_non_virtual_public_destructor_highlighting=warning +resharper_cpp_possibly_erroneous_empty_statements_highlighting=warning +resharper_cpp_possibly_uninitialized_member_highlighting=warning +resharper_cpp_possibly_unintended_object_slicing_highlighting=warning +resharper_cpp_precompiled_header_is_not_included_highlighting=error +resharper_cpp_precompiled_header_not_found_highlighting=error +resharper_cpp_printf_bad_format_highlighting=warning +resharper_cpp_printf_extra_arg_highlighting=warning +resharper_cpp_printf_missed_arg_highlighting=error +resharper_cpp_printf_risky_format_highlighting=warning +resharper_cpp_private_special_member_function_is_not_implemented_highlighting=warning +resharper_cpp_range_based_for_incompatible_reference_highlighting=warning +resharper_cpp_redefinition_of_default_argument_in_override_function_highlighting=warning +resharper_cpp_redundant_access_specifier_highlighting=hint +resharper_cpp_redundant_base_class_access_specifier_highlighting=hint +resharper_cpp_redundant_blank_lines_highlighting=none +resharper_cpp_redundant_boolean_expression_argument_highlighting=warning +resharper_cpp_redundant_cast_expression_highlighting=hint +resharper_cpp_redundant_const_specifier_highlighting=hint +resharper_cpp_redundant_control_flow_jump_highlighting=hint +resharper_cpp_redundant_elaborated_type_specifier_highlighting=hint +resharper_cpp_redundant_else_keyword_highlighting=hint +resharper_cpp_redundant_else_keyword_inside_compound_statement_highlighting=hint +resharper_cpp_redundant_empty_declaration_highlighting=hint +resharper_cpp_redundant_empty_statement_highlighting=hint +resharper_cpp_redundant_explicit_template_arguments_highlighting=hint +resharper_cpp_redundant_inline_specifier_highlighting=hint +resharper_cpp_redundant_lambda_parameter_list_highlighting=hint +resharper_cpp_redundant_linebreak_highlighting=none +resharper_cpp_redundant_member_initializer_highlighting=suggestion +resharper_cpp_redundant_namespace_definition_highlighting=suggestion +resharper_cpp_redundant_parentheses_highlighting=hint +resharper_cpp_redundant_qualifier_highlighting=hint +resharper_cpp_redundant_space_highlighting=none +resharper_cpp_redundant_static_specifier_on_member_allocation_function_highlighting=hint +resharper_cpp_redundant_template_keyword_highlighting=warning +resharper_cpp_redundant_typename_keyword_highlighting=warning +resharper_cpp_redundant_void_argument_list_highlighting=suggestion +resharper_cpp_reinterpret_cast_from_void_ptr_highlighting=suggestion +resharper_cpp_remove_redundant_braces_highlighting=none +resharper_cpp_replace_memset_with_zero_initialization_highlighting=suggestion +resharper_cpp_replace_tie_with_structured_binding_highlighting=suggestion +resharper_cpp_return_no_value_in_non_void_function_highlighting=warning +resharper_cpp_smart_pointer_vs_make_function_highlighting=suggestion +resharper_cpp_some_object_members_might_not_be_initialized_highlighting=warning +resharper_cpp_special_function_without_noexcept_specification_highlighting=warning +resharper_cpp_static_data_member_in_unnamed_struct_highlighting=warning +resharper_cpp_static_specifier_on_anonymous_namespace_member_highlighting=suggestion +resharper_cpp_string_literal_to_char_pointer_conversion_highlighting=warning +resharper_cpp_syntax_warning_highlighting=warning +resharper_cpp_tabs_and_spaces_mismatch_highlighting=none +resharper_cpp_tabs_are_disallowed_highlighting=none +resharper_cpp_tabs_outside_indent_highlighting=none +resharper_cpp_template_parameter_shadowing_highlighting=warning +resharper_cpp_this_arg_member_func_delegate_ctor_is_unsuported_by_dot_net_core_highlighting=none +resharper_cpp_throw_expression_can_be_replaced_with_rethrow_highlighting=warning +resharper_cpp_too_wide_scope_highlighting=suggestion +resharper_cpp_too_wide_scope_init_statement_highlighting=hint +resharper_cpp_type_alias_never_used_highlighting=warning +resharper_cpp_ue4_blueprint_callable_function_may_be_const_highlighting=hint +resharper_cpp_ue4_blueprint_callable_function_may_be_static_highlighting=hint +resharper_cpp_ue4_coding_standard_naming_violation_warning_highlighting=hint +resharper_cpp_ue4_coding_standard_u_class_naming_violation_error_highlighting=error +resharper_cpp_ue4_probable_memory_issues_with_u_objects_in_container_highlighting=warning +resharper_cpp_ue4_probable_memory_issues_with_u_object_highlighting=warning +resharper_cpp_ue_blueprint_callable_function_unused_highlighting=warning +resharper_cpp_ue_blueprint_implementable_event_not_implemented_highlighting=warning +resharper_cpp_ue_incorrect_engine_directory_highlighting=error +resharper_cpp_ue_non_existent_input_action_highlighting=warning +resharper_cpp_ue_non_existent_input_axis_highlighting=warning +resharper_cpp_ue_source_file_without_predefined_macros_highlighting=warning +resharper_cpp_ue_source_file_without_standard_library_highlighting=error +resharper_cpp_ue_version_file_doesnt_exist_highlighting=error +resharper_cpp_uninitialized_dependent_base_class_highlighting=warning +resharper_cpp_uninitialized_non_static_data_member_highlighting=warning +resharper_cpp_union_member_of_reference_type_highlighting=warning +resharper_cpp_unnamed_namespace_in_header_file_highlighting=warning +resharper_cpp_unnecessary_whitespace_highlighting=none +resharper_cpp_unreachable_code_highlighting=warning +resharper_cpp_unsigned_zero_comparison_highlighting=warning +resharper_cpp_unused_include_directive_highlighting=warning +resharper_cpp_user_defined_literal_suffix_does_not_start_with_underscore_highlighting=warning +resharper_cpp_use_algorithm_with_count_highlighting=suggestion +resharper_cpp_use_associative_contains_highlighting=suggestion +resharper_cpp_use_auto_for_numeric_highlighting=hint +resharper_cpp_use_auto_highlighting=hint +resharper_cpp_use_elements_view_highlighting=suggestion +resharper_cpp_use_erase_algorithm_highlighting=suggestion +resharper_cpp_use_familiar_template_syntax_for_generic_lambdas_highlighting=suggestion +resharper_cpp_use_range_algorithm_highlighting=suggestion +resharper_cpp_use_std_size_highlighting=suggestion +resharper_cpp_use_structured_binding_highlighting=hint +resharper_cpp_use_type_trait_alias_highlighting=suggestion +resharper_cpp_using_result_of_assignment_as_condition_highlighting=warning +resharper_cpp_u_function_macro_call_has_no_effect_highlighting=warning +resharper_cpp_u_property_macro_call_has_no_effect_highlighting=warning +resharper_cpp_variable_can_be_made_constexpr_highlighting=suggestion +resharper_cpp_virtual_function_call_inside_ctor_highlighting=warning +resharper_cpp_virtual_function_in_final_class_highlighting=warning +resharper_cpp_volatile_parameter_in_declaration_highlighting=suggestion +resharper_cpp_wrong_includes_order_highlighting=hint +resharper_cpp_wrong_indent_size_highlighting=none +resharper_cpp_wrong_slashes_in_include_directive_highlighting=hint +resharper_cpp_zero_constant_can_be_replaced_with_nullptr_highlighting=suggestion +resharper_cpp_zero_valued_expression_used_as_null_pointer_highlighting=warning +resharper_create_specialized_overload_highlighting=hint +resharper_css_browser_compatibility_highlighting=warning +resharper_css_caniuse_feature_requires_prefix_highlighting=hint +resharper_css_caniuse_unsupported_feature_highlighting=hint +resharper_css_not_resolved_highlighting=error +resharper_css_obsolete_highlighting=hint +resharper_css_property_does_not_override_vendor_property_highlighting=warning +resharper_cyclic_reference_comment_highlighting=none +resharper_c_declaration_with_implicit_int_type_highlighting=warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting=warning +resharper_c_sharp_missing_plugin_dependency_highlighting=warning +resharper_declaration_hides_highlighting=hint +resharper_declaration_is_empty_highlighting=warning +resharper_declaration_visibility_error_highlighting=error +resharper_default_value_attribute_for_optional_parameter_highlighting=warning +resharper_deleting_non_qualified_reference_highlighting=error +resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting=hint +resharper_double_colons_expected_highlighting=error +resharper_double_colons_preferred_highlighting=suggestion +resharper_double_negation_in_pattern_highlighting=suggestion +resharper_double_negation_of_boolean_highlighting=warning +resharper_double_negation_operator_highlighting=suggestion +resharper_duplicate_identifier_error_highlighting=error +resharper_duplicate_reference_comment_highlighting=warning +resharper_duplicate_resource_highlighting=warning +resharper_duplicating_local_declaration_highlighting=warning +resharper_duplicating_parameter_declaration_error_highlighting=error +resharper_duplicating_property_declaration_error_highlighting=error +resharper_duplicating_property_declaration_highlighting=warning +resharper_duplicating_switch_label_highlighting=warning +resharper_dynamic_shift_right_op_is_not_int_highlighting=warning +resharper_elided_trailing_element_highlighting=warning +resharper_empty_constructor_highlighting=warning +resharper_empty_destructor_highlighting=warning +resharper_empty_embedded_statement_highlighting=warning +resharper_empty_for_statement_highlighting=warning +resharper_empty_general_catch_clause_highlighting=warning +resharper_empty_namespace_highlighting=warning +resharper_empty_object_property_declaration_highlighting=error +resharper_empty_return_value_for_type_annotated_function_highlighting=warning +resharper_empty_statement_highlighting=warning +resharper_empty_title_tag_highlighting=hint +resharper_enforce_do_while_statement_braces_highlighting=none +resharper_enforce_fixed_statement_braces_highlighting=none +resharper_enforce_foreach_statement_braces_highlighting=none +resharper_enforce_for_statement_braces_highlighting=none +resharper_enforce_if_statement_braces_highlighting=none +resharper_enforce_lock_statement_braces_highlighting=none +resharper_enforce_using_statement_braces_highlighting=none +resharper_enforce_while_statement_braces_highlighting=none +resharper_entity_name_captured_only_global_highlighting=warning +resharper_entity_name_captured_only_local_highlighting=warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting=warning +resharper_enum_underlying_type_is_int_highlighting=warning +resharper_equal_expression_comparison_highlighting=warning +resharper_error_in_xml_doc_reference_highlighting=error +resharper_es6_feature_highlighting=error +resharper_es7_feature_highlighting=error +resharper_eval_arguments_name_error_highlighting=error +resharper_event_never_invoked_global_highlighting=suggestion +resharper_event_never_subscribed_to_global_highlighting=suggestion +resharper_event_never_subscribed_to_local_highlighting=suggestion +resharper_event_unsubscription_via_anonymous_delegate_highlighting=warning +resharper_experimental_feature_highlighting=error +resharper_explicit_caller_info_argument_highlighting=warning +resharper_expression_is_always_const_highlighting=warning +resharper_expression_is_always_null_highlighting=warning +resharper_field_can_be_made_read_only_global_highlighting=suggestion +resharper_field_can_be_made_read_only_local_highlighting=suggestion +resharper_field_hides_interface_property_with_default_implementation_highlighting=warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_format_string_placeholders_mismatch_highlighting=warning +resharper_format_string_problem_highlighting=warning +resharper_for_can_be_converted_to_foreach_highlighting=suggestion +resharper_for_statement_condition_is_true_highlighting=warning +resharper_functions_used_before_declared_highlighting=none +resharper_function_complexity_overflow_highlighting=none +resharper_function_never_returns_highlighting=warning +resharper_function_parameter_named_arguments_highlighting=warning +resharper_function_recursive_on_all_paths_highlighting=warning +resharper_function_used_out_of_scope_highlighting=warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting=warning +resharper_generic_enumerator_not_disposed_highlighting=warning +resharper_heuristically_unreachable_code_highlighting=warning +resharper_heuristic_unreachable_code_highlighting=warning +resharper_hex_color_value_with_alpha_highlighting=error +resharper_html_attributes_quotes_highlighting=hint +resharper_html_attribute_not_resolved_highlighting=warning +resharper_html_attribute_value_not_resolved_highlighting=warning +resharper_html_dead_code_highlighting=warning +resharper_html_event_not_resolved_highlighting=warning +resharper_html_id_duplication_highlighting=warning +resharper_html_id_not_resolved_highlighting=warning +resharper_html_obsolete_highlighting=warning +resharper_html_path_error_highlighting=warning +resharper_html_tag_not_closed_highlighting=error +resharper_html_tag_not_resolved_highlighting=warning +resharper_html_tag_should_be_self_closed_highlighting=warning +resharper_html_tag_should_not_be_self_closed_highlighting=warning +resharper_html_warning_highlighting=warning +resharper_identifier_typo_highlighting=suggestion +resharper_implicit_any_error_highlighting=error +resharper_implicit_any_type_warning_highlighting=warning +resharper_import_keyword_not_with_invocation_highlighting=error +resharper_inactive_preprocessor_branch_highlighting=warning +resharper_inconsistently_synchronized_field_highlighting=warning +resharper_inconsistent_function_returns_highlighting=warning +resharper_inconsistent_naming_highlighting=warning +resharper_inconsistent_order_of_locks_highlighting=warning +resharper_incorrect_blank_lines_near_braces_highlighting=none +resharper_incorrect_operand_in_type_of_comparison_highlighting=warning +resharper_incorrect_triple_slash_location_highlighting=warning +resharper_indexing_by_invalid_range_highlighting=warning +resharper_inheritdoc_consider_usage_highlighting=none +resharper_inheritdoc_invalid_usage_highlighting=warning +resharper_inline_out_variable_declaration_highlighting=suggestion +resharper_inline_temporary_variable_highlighting=hint +resharper_internal_module_highlighting=suggestion +resharper_internal_or_private_member_not_documented_highlighting=none +resharper_interpolated_string_expression_is_not_i_formattable_highlighting=warning +resharper_introduce_optional_parameters_global_highlighting=suggestion +resharper_introduce_optional_parameters_local_highlighting=suggestion +resharper_introduce_variable_to_apply_guard_highlighting=hint +resharper_int_division_by_zero_highlighting=warning +resharper_int_variable_overflow_highlighting=warning +resharper_int_variable_overflow_in_checked_context_highlighting=warning +resharper_int_variable_overflow_in_unchecked_context_highlighting=warning +resharper_invalid_attribute_value_highlighting=warning +resharper_invalid_json_syntax_highlighting=error +resharper_invalid_task_element_highlighting=none +resharper_invalid_value_highlighting=error +resharper_invalid_value_type_highlighting=warning +resharper_invalid_xml_doc_comment_highlighting=warning +resharper_invert_condition_1_highlighting=hint +resharper_invert_if_highlighting=hint +resharper_invocation_is_skipped_highlighting=hint +resharper_invocation_of_non_function_highlighting=warning +resharper_invoked_expression_maybe_non_function_highlighting=warning +resharper_invoke_as_extension_method_highlighting=suggestion +resharper_is_expression_always_false_highlighting=warning +resharper_is_expression_always_true_highlighting=warning +resharper_iterator_method_result_is_ignored_highlighting=warning +resharper_iterator_never_returns_highlighting=warning +resharper_join_declaration_and_initializer_highlighting=suggestion +resharper_join_declaration_and_initializer_js_highlighting=suggestion +resharper_join_null_check_with_usage_highlighting=suggestion +resharper_join_null_check_with_usage_when_possible_highlighting=none +resharper_json_validation_failed_highlighting=error +resharper_js_path_not_found_highlighting=error +resharper_js_unreachable_code_highlighting=warning +resharper_jump_must_be_in_loop_highlighting=warning +resharper_label_or_semicolon_expected_highlighting=error +resharper_lambda_expression_can_be_made_static_highlighting=none +resharper_lambda_expression_must_be_static_highlighting=suggestion +resharper_lambda_highlighting=suggestion +resharper_lambda_should_not_capture_context_highlighting=warning +resharper_less_specific_overload_than_main_signature_highlighting=warning +resharper_lexical_declaration_needs_block_highlighting=error +resharper_localizable_element_highlighting=warning +resharper_local_function_can_be_made_static_highlighting=none +resharper_local_function_hides_method_highlighting=warning +resharper_local_function_redefined_later_highlighting=warning +resharper_local_variable_hides_member_highlighting=warning +resharper_long_literal_ending_lower_l_highlighting=warning +resharper_loop_can_be_converted_to_query_highlighting=hint +resharper_loop_can_be_partly_converted_to_query_highlighting=none +resharper_loop_variable_is_never_changed_inside_loop_highlighting=warning +resharper_l_value_is_expected_highlighting=error +resharper_markup_attribute_typo_highlighting=suggestion +resharper_markup_text_typo_highlighting=suggestion +resharper_math_abs_method_is_redundant_highlighting=warning +resharper_math_clamp_min_greater_than_max_highlighting=warning +resharper_meaningless_default_parameter_value_highlighting=warning +resharper_member_can_be_internal_highlighting=none +resharper_member_can_be_made_static_global_highlighting=hint +resharper_member_can_be_made_static_local_highlighting=hint +resharper_member_can_be_private_global_highlighting=suggestion +resharper_member_can_be_private_local_highlighting=suggestion +resharper_member_can_be_protected_global_highlighting=suggestion +resharper_member_can_be_protected_local_highlighting=suggestion +resharper_member_hides_interface_member_with_default_implementation_highlighting=warning +resharper_member_hides_static_from_outer_class_highlighting=warning +resharper_member_initializer_value_ignored_highlighting=warning +resharper_merge_and_pattern_highlighting=suggestion +resharper_merge_cast_with_type_check_highlighting=suggestion +resharper_merge_conditional_expression_highlighting=suggestion +resharper_merge_conditional_expression_when_possible_highlighting=none +resharper_merge_into_logical_pattern_highlighting=hint +resharper_merge_into_negated_pattern_highlighting=hint +resharper_merge_into_pattern_highlighting=suggestion +resharper_merge_nested_property_patterns_highlighting=suggestion +resharper_merge_sequential_checks_highlighting=hint +resharper_merge_sequential_checks_when_possible_highlighting=none +resharper_method_has_async_overload_highlighting=suggestion +resharper_method_has_async_overload_with_cancellation_highlighting=suggestion +resharper_method_overload_with_optional_parameter_highlighting=warning +resharper_method_safe_this_highlighting=suggestion +resharper_method_supports_cancellation_highlighting=suggestion +resharper_missing_alt_attribute_in_img_tag_highlighting=hint +resharper_missing_attribute_highlighting=warning +resharper_missing_blank_lines_highlighting=none +resharper_missing_body_tag_highlighting=warning +resharper_missing_has_own_property_in_foreach_highlighting=warning +resharper_missing_head_and_body_tags_highlighting=warning +resharper_missing_head_tag_highlighting=warning +resharper_missing_indent_highlighting=none +resharper_missing_linebreak_highlighting=none +resharper_missing_space_highlighting=none +resharper_missing_title_tag_highlighting=hint +resharper_misuse_of_owner_function_this_highlighting=warning +resharper_more_specific_foreach_variable_type_available_highlighting=suggestion +resharper_more_specific_signature_after_less_specific_highlighting=warning +resharper_move_to_existing_positional_deconstruction_pattern_highlighting=hint +resharper_multiple_declarations_in_foreach_highlighting=error +resharper_multiple_nullable_attributes_usage_highlighting=warning +resharper_multiple_order_by_highlighting=warning +resharper_multiple_output_tags_highlighting=warning +resharper_multiple_resolve_candidates_in_text_highlighting=warning +resharper_multiple_spaces_highlighting=none +resharper_multiple_statements_on_one_line_highlighting=none +resharper_multiple_type_members_on_one_line_highlighting=none +resharper_must_use_return_value_highlighting=warning +resharper_mvc_action_not_resolved_highlighting=error +resharper_mvc_area_not_resolved_highlighting=error +resharper_mvc_controller_not_resolved_highlighting=error +resharper_mvc_invalid_model_type_highlighting=error +resharper_mvc_masterpage_not_resolved_highlighting=error +resharper_mvc_partial_view_not_resolved_highlighting=error +resharper_mvc_template_not_resolved_highlighting=error +resharper_mvc_view_component_not_resolved_highlighting=error +resharper_mvc_view_component_view_not_resolved_highlighting=error +resharper_mvc_view_not_resolved_highlighting=error +resharper_native_type_prototype_extending_highlighting=warning +resharper_native_type_prototype_overwriting_highlighting=warning +resharper_negation_of_relational_pattern_highlighting=suggestion +resharper_negative_equality_expression_highlighting=suggestion +resharper_negative_index_highlighting=warning +resharper_nested_string_interpolation_highlighting=suggestion +resharper_non_assigned_constant_highlighting=error +resharper_non_atomic_compound_operator_highlighting=warning +resharper_non_constant_equality_expression_has_constant_result_highlighting=warning +resharper_non_parsable_element_highlighting=warning +resharper_non_readonly_member_in_get_hash_code_highlighting=warning +resharper_non_volatile_field_in_double_check_locking_highlighting=warning +resharper_not_accessed_field_global_highlighting=suggestion +resharper_not_accessed_field_local_highlighting=warning +resharper_not_accessed_positional_property_global_highlighting=warning +resharper_not_accessed_positional_property_local_highlighting=warning +resharper_not_accessed_variable_highlighting=warning +resharper_not_all_paths_return_value_highlighting=warning +resharper_not_assigned_out_parameter_highlighting=warning +resharper_not_declared_in_parent_culture_highlighting=warning +resharper_not_null_member_is_not_initialized_highlighting=warning +resharper_not_observable_annotation_redundancy_highlighting=warning +resharper_not_overridden_in_specific_culture_highlighting=warning +resharper_not_resolved_highlighting=warning +resharper_not_resolved_in_text_highlighting=warning +resharper_nullable_warning_suppression_is_used_highlighting=none +resharper_n_unit_async_method_must_be_task_highlighting=warning +resharper_n_unit_attribute_produces_too_many_tests_highlighting=none +resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_attribute_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting=warning +resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting=warning +resharper_n_unit_duplicate_values_highlighting=warning +resharper_n_unit_ignored_parameter_attribute_highlighting=warning +resharper_n_unit_implicit_unspecified_null_values_highlighting=warning +resharper_n_unit_incorrect_argument_type_highlighting=warning +resharper_n_unit_incorrect_expected_result_type_highlighting=warning +resharper_n_unit_incorrect_range_bounds_highlighting=warning +resharper_n_unit_method_with_parameters_and_test_attribute_highlighting=warning +resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting=warning +resharper_n_unit_non_public_method_with_test_attribute_highlighting=warning +resharper_n_unit_no_values_provided_highlighting=warning +resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting=warning +resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting=warning +resharper_n_unit_range_step_sign_mismatch_highlighting=warning +resharper_n_unit_range_step_value_must_not_be_zero_highlighting=warning +resharper_n_unit_range_to_value_is_not_reachable_highlighting=warning +resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting=warning +resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting=warning +resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting=warning +resharper_n_unit_test_case_attribute_requires_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_is_obsolete_highlighting=warning +resharper_n_unit_test_case_source_cannot_be_resolved_highlighting=warning +resharper_n_unit_test_case_source_must_be_field_property_method_highlighting=warning +resharper_n_unit_test_case_source_must_be_static_highlighting=warning +resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting=warning +resharper_object_creation_as_statement_highlighting=warning +resharper_object_destructuring_without_parentheses_highlighting=error +resharper_object_literals_are_not_comma_free_highlighting=error +resharper_obsolete_element_error_highlighting=error +resharper_obsolete_element_highlighting=warning +resharper_octal_literals_not_allowed_error_highlighting=error +resharper_ol_tag_contains_non_li_elements_highlighting=hint +resharper_one_way_operation_contract_with_return_type_highlighting=warning +resharper_operation_contract_without_service_contract_highlighting=warning +resharper_operator_is_can_be_used_highlighting=warning +resharper_optional_parameter_hierarchy_mismatch_highlighting=warning +resharper_optional_parameter_ref_out_highlighting=warning +resharper_other_tags_inside_script1_highlighting=error +resharper_other_tags_inside_script2_highlighting=error +resharper_other_tags_inside_unclosed_script_highlighting=error +resharper_outdent_is_off_prev_level_highlighting=none +resharper_output_tag_required_highlighting=warning +resharper_out_parameter_value_is_always_discarded_global_highlighting=suggestion +resharper_out_parameter_value_is_always_discarded_local_highlighting=warning +resharper_overload_signature_inferring_highlighting=hint +resharper_overridden_with_empty_value_highlighting=warning +resharper_overridden_with_same_value_highlighting=suggestion +resharper_parameter_doesnt_make_any_sense_highlighting=warning +resharper_parameter_hides_member_highlighting=warning +resharper_parameter_only_used_for_precondition_check_global_highlighting=suggestion +resharper_parameter_only_used_for_precondition_check_local_highlighting=warning +resharper_parameter_type_can_be_enumerable_global_highlighting=hint +resharper_parameter_type_can_be_enumerable_local_highlighting=hint +resharper_parameter_value_is_not_used_highlighting=warning +resharper_partial_method_parameter_name_mismatch_highlighting=warning +resharper_partial_method_with_single_part_highlighting=warning +resharper_partial_type_with_single_part_highlighting=warning +resharper_pass_string_interpolation_highlighting=hint +resharper_path_not_resolved_highlighting=error +resharper_pattern_always_matches_highlighting=warning +resharper_pattern_is_always_true_or_false_highlighting=warning +resharper_pattern_never_matches_highlighting=warning +resharper_polymorphic_field_like_event_invocation_highlighting=warning +resharper_possible_infinite_inheritance_highlighting=warning +resharper_possible_intended_rethrow_highlighting=warning +resharper_possible_interface_member_ambiguity_highlighting=warning +resharper_possible_invalid_cast_exception_highlighting=warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting=warning +resharper_possible_invalid_operation_exception_highlighting=warning +resharper_possible_loss_of_fraction_highlighting=warning +resharper_possible_mistaken_argument_highlighting=warning +resharper_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_possible_multiple_enumeration_highlighting=warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting=warning +resharper_possible_null_reference_exception_highlighting=warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting=warning +resharper_possible_unintended_linear_search_in_set_highlighting=warning +resharper_possible_unintended_queryable_as_enumerable_highlighting=suggestion +resharper_possible_unintended_reference_comparison_highlighting=warning +resharper_possible_write_to_me_highlighting=warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting=warning +resharper_possibly_incorrectly_broken_statement_highlighting=warning +resharper_possibly_missing_indexer_initializer_comma_highlighting=warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting=warning +resharper_possibly_mistaken_use_of_params_method_highlighting=warning +resharper_possibly_unassigned_property_highlighting=hint +resharper_private_field_can_be_converted_to_local_variable_highlighting=warning +resharper_private_variable_can_be_made_readonly_highlighting=hint +resharper_property_can_be_made_init_only_global_highlighting=suggestion +resharper_property_can_be_made_init_only_local_highlighting=suggestion +resharper_property_getter_cannot_have_parameters_highlighting=error +resharper_property_not_resolved_highlighting=error +resharper_property_setter_must_have_single_parameter_highlighting=error +resharper_public_constructor_in_abstract_class_highlighting=suggestion +resharper_pure_attribute_on_void_method_highlighting=warning +resharper_qualified_expression_is_null_highlighting=warning +resharper_qualified_expression_maybe_null_highlighting=warning +resharper_razor_layout_not_resolved_highlighting=error +resharper_razor_section_not_resolved_highlighting=error +resharper_read_access_in_double_check_locking_highlighting=warning +resharper_redundant_abstract_modifier_highlighting=warning +resharper_redundant_always_match_subpattern_highlighting=suggestion +resharper_redundant_anonymous_type_property_name_highlighting=warning +resharper_redundant_argument_default_value_highlighting=warning +resharper_redundant_array_creation_expression_highlighting=hint +resharper_redundant_array_lower_bound_specification_highlighting=warning +resharper_redundant_assignment_highlighting=warning +resharper_redundant_attribute_parentheses_highlighting=hint +resharper_redundant_attribute_usage_property_highlighting=suggestion +resharper_redundant_base_constructor_call_highlighting=warning +resharper_redundant_base_qualifier_highlighting=warning +resharper_redundant_blank_lines_highlighting=none +resharper_redundant_block_highlighting=warning +resharper_redundant_bool_compare_highlighting=warning +resharper_redundant_case_label_highlighting=warning +resharper_redundant_cast_highlighting=warning +resharper_redundant_catch_clause_highlighting=warning +resharper_redundant_check_before_assignment_highlighting=warning +resharper_redundant_collection_initializer_element_braces_highlighting=hint +resharper_redundant_comparison_with_boolean_highlighting=warning +resharper_redundant_configure_await_highlighting=suggestion +resharper_redundant_css_hack_highlighting=warning +resharper_redundant_declaration_semicolon_highlighting=hint +resharper_redundant_default_member_initializer_highlighting=warning +resharper_redundant_delegate_creation_highlighting=warning +resharper_redundant_disable_warning_comment_highlighting=warning +resharper_redundant_discard_designation_highlighting=suggestion +resharper_redundant_else_block_highlighting=warning +resharper_redundant_empty_case_else_highlighting=warning +resharper_redundant_empty_constructor_highlighting=warning +resharper_redundant_empty_finally_block_highlighting=warning +resharper_redundant_empty_object_creation_argument_list_highlighting=hint +resharper_redundant_empty_object_or_collection_initializer_highlighting=warning +resharper_redundant_empty_switch_section_highlighting=warning +resharper_redundant_enumerable_cast_call_highlighting=warning +resharper_redundant_enum_case_label_for_default_section_highlighting=none +resharper_redundant_explicit_array_creation_highlighting=warning +resharper_redundant_explicit_array_size_highlighting=warning +resharper_redundant_explicit_nullable_creation_highlighting=warning +resharper_redundant_explicit_params_array_creation_highlighting=suggestion +resharper_redundant_explicit_positional_property_declaration_highlighting=warning +resharper_redundant_explicit_tuple_component_name_highlighting=warning +resharper_redundant_extends_list_entry_highlighting=warning +resharper_redundant_fixed_pointer_declaration_highlighting=suggestion +resharper_redundant_highlighting=warning +resharper_redundant_if_else_block_highlighting=hint +resharper_redundant_if_statement_then_keyword_highlighting=none +resharper_redundant_immediate_delegate_invocation_highlighting=suggestion +resharper_redundant_intermediate_variable_highlighting=hint +resharper_redundant_is_before_relational_pattern_highlighting=suggestion +resharper_redundant_iterator_keyword_highlighting=warning +resharper_redundant_jump_statement_highlighting=warning +resharper_redundant_lambda_parameter_type_highlighting=warning +resharper_redundant_lambda_signature_parentheses_highlighting=hint +resharper_redundant_linebreak_highlighting=none +resharper_redundant_local_class_name_highlighting=hint +resharper_redundant_local_function_name_highlighting=hint +resharper_redundant_logical_conditional_expression_operand_highlighting=warning +resharper_redundant_me_qualifier_highlighting=warning +resharper_redundant_my_base_qualifier_highlighting=warning +resharper_redundant_my_class_qualifier_highlighting=warning +resharper_redundant_name_qualifier_highlighting=warning +resharper_redundant_not_null_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting=warning +resharper_redundant_nullable_flow_attribute_highlighting=warning +resharper_redundant_nullable_type_mark_highlighting=warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting=warning +resharper_redundant_overflow_checking_context_highlighting=warning +resharper_redundant_overload_global_highlighting=suggestion +resharper_redundant_overload_local_highlighting=suggestion +resharper_redundant_overridden_member_highlighting=warning +resharper_redundant_params_highlighting=warning +resharper_redundant_parentheses_highlighting=none +resharper_redundant_parent_type_declaration_highlighting=warning +resharper_redundant_pattern_parentheses_highlighting=hint +resharper_redundant_property_parentheses_highlighting=hint +resharper_redundant_property_pattern_clause_highlighting=suggestion +resharper_redundant_qualifier_highlighting=warning +resharper_redundant_query_order_by_ascending_keyword_highlighting=hint +resharper_redundant_range_bound_highlighting=suggestion +resharper_redundant_readonly_modifier_highlighting=suggestion +resharper_redundant_record_body_highlighting=warning +resharper_redundant_record_class_keyword_highlighting=warning +resharper_redundant_setter_value_parameter_declaration_highlighting=hint +resharper_redundant_space_highlighting=none +resharper_redundant_string_format_call_highlighting=warning +resharper_redundant_string_interpolation_highlighting=suggestion +resharper_redundant_string_to_char_array_call_highlighting=warning +resharper_redundant_string_type_highlighting=suggestion +resharper_redundant_suppress_nullable_warning_expression_highlighting=warning +resharper_redundant_ternary_expression_highlighting=warning +resharper_redundant_to_string_call_for_value_type_highlighting=hint +resharper_redundant_to_string_call_highlighting=warning +resharper_redundant_type_arguments_of_method_highlighting=warning +resharper_redundant_type_cast_highlighting=warning +resharper_redundant_type_cast_structural_highlighting=warning +resharper_redundant_type_check_in_pattern_highlighting=warning +resharper_redundant_units_highlighting=warning +resharper_redundant_unsafe_context_highlighting=warning +resharper_redundant_using_directive_global_highlighting=warning +resharper_redundant_using_directive_highlighting=warning +resharper_redundant_variable_type_specification_highlighting=hint +resharper_redundant_verbatim_prefix_highlighting=suggestion +resharper_redundant_verbatim_string_prefix_highlighting=suggestion +resharper_redundant_with_expression_highlighting=suggestion +resharper_reference_equals_with_value_type_highlighting=warning +resharper_reg_exp_inspections_highlighting=warning +resharper_remove_constructor_invocation_highlighting=none +resharper_remove_redundant_braces_highlighting=none +resharper_remove_redundant_or_statement_false_highlighting=suggestion +resharper_remove_redundant_or_statement_true_highlighting=suggestion +resharper_remove_to_list_1_highlighting=suggestion +resharper_remove_to_list_2_highlighting=suggestion +resharper_replace_auto_property_with_computed_property_highlighting=hint +resharper_replace_indicing_with_array_destructuring_highlighting=hint +resharper_replace_indicing_with_short_hand_properties_after_destructuring_highlighting=hint +resharper_replace_object_pattern_with_var_pattern_highlighting=suggestion +resharper_replace_slice_with_range_indexer_highlighting=hint +resharper_replace_substring_with_range_indexer_highlighting=hint +resharper_replace_undefined_checking_series_with_object_destructuring_highlighting=hint +resharper_replace_with_destructuring_swap_highlighting=hint +resharper_replace_with_first_or_default_1_highlighting=suggestion +resharper_replace_with_first_or_default_2_highlighting=suggestion +resharper_replace_with_first_or_default_3_highlighting=suggestion +resharper_replace_with_first_or_default_4_highlighting=suggestion +resharper_replace_with_last_or_default_1_highlighting=suggestion +resharper_replace_with_last_or_default_2_highlighting=suggestion +resharper_replace_with_last_or_default_3_highlighting=suggestion +resharper_replace_with_last_or_default_4_highlighting=suggestion +resharper_replace_with_of_type_1_highlighting=suggestion +resharper_replace_with_of_type_2_highlighting=suggestion +resharper_replace_with_of_type_3_highlighting=suggestion +resharper_replace_with_of_type_any_1_highlighting=suggestion +resharper_replace_with_of_type_any_2_highlighting=suggestion +resharper_replace_with_of_type_count_1_highlighting=suggestion +resharper_replace_with_of_type_count_2_highlighting=suggestion +resharper_replace_with_of_type_first_1_highlighting=suggestion +resharper_replace_with_of_type_first_2_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_last_1_highlighting=suggestion +resharper_replace_with_of_type_last_2_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_long_count_highlighting=suggestion +resharper_replace_with_of_type_single_1_highlighting=suggestion +resharper_replace_with_of_type_single_2_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_where_highlighting=suggestion +resharper_replace_with_simple_assignment_false_highlighting=suggestion +resharper_replace_with_simple_assignment_true_highlighting=suggestion +resharper_replace_with_single_assignment_false_highlighting=suggestion +resharper_replace_with_single_assignment_true_highlighting=suggestion +resharper_replace_with_single_call_to_any_highlighting=suggestion +resharper_replace_with_single_call_to_count_highlighting=suggestion +resharper_replace_with_single_call_to_first_highlighting=suggestion +resharper_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_last_highlighting=suggestion +resharper_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_single_highlighting=suggestion +resharper_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_replace_with_single_or_default_1_highlighting=suggestion +resharper_replace_with_single_or_default_2_highlighting=suggestion +resharper_replace_with_single_or_default_3_highlighting=suggestion +resharper_replace_with_single_or_default_4_highlighting=suggestion +resharper_replace_with_string_is_null_or_empty_highlighting=suggestion +resharper_required_base_types_conflict_highlighting=warning +resharper_required_base_types_direct_conflict_highlighting=warning +resharper_required_base_types_is_not_inherited_highlighting=warning +resharper_requires_fallback_color_highlighting=warning +resharper_resource_item_not_resolved_highlighting=error +resharper_resource_not_resolved_highlighting=error +resharper_resx_not_resolved_highlighting=warning +resharper_return_from_global_scopet_with_value_highlighting=warning +resharper_return_type_can_be_enumerable_global_highlighting=hint +resharper_return_type_can_be_enumerable_local_highlighting=hint +resharper_return_type_can_be_not_nullable_highlighting=warning +resharper_return_value_of_pure_method_is_not_used_highlighting=warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting=hint +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting=warning +resharper_route_templates_ambiguous_route_match_highlighting=warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting=warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting=hint +resharper_route_templates_duplicated_parameter_highlighting=warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting=warning +resharper_route_templates_method_missing_route_parameters_highlighting=hint +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting=warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting=warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting=hint +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting=warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting=suggestion +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting=warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting=hint +resharper_route_templates_route_token_not_resolved_highlighting=warning +resharper_route_templates_symbol_not_resolved_highlighting=warning +resharper_route_templates_syntax_error_highlighting=warning +resharper_safe_cast_is_used_as_type_check_highlighting=suggestion +resharper_same_imports_with_different_name_highlighting=warning +resharper_same_variable_assignment_highlighting=warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting=error +resharper_script_tag_with_content_before_includes_highlighting=hint +resharper_sealed_member_in_sealed_class_highlighting=warning +resharper_separate_control_transfer_statement_highlighting=none +resharper_service_contract_without_operations_highlighting=warning +resharper_shift_expression_real_shift_count_is_zero_highlighting=warning +resharper_shift_expression_result_equals_zero_highlighting=warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting=warning +resharper_shift_expression_zero_left_operand_highlighting=warning +resharper_similar_anonymous_type_nearby_highlighting=hint +resharper_similar_expressions_comparison_highlighting=warning +resharper_simplify_conditional_operator_highlighting=suggestion +resharper_simplify_conditional_ternary_expression_highlighting=suggestion +resharper_simplify_i_if_highlighting=suggestion +resharper_simplify_linq_expression_use_all_highlighting=suggestion +resharper_simplify_linq_expression_use_any_highlighting=suggestion +resharper_simplify_string_interpolation_highlighting=suggestion +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting=warning +resharper_specify_string_comparison_highlighting=hint +resharper_specify_variable_type_explicitly_highlighting=hint +resharper_spin_lock_in_readonly_field_highlighting=warning +resharper_stack_alloc_inside_loop_highlighting=warning +resharper_statement_termination_highlighting=warning +resharper_static_member_initializer_referes_to_member_below_highlighting=warning +resharper_static_member_in_generic_type_highlighting=none +resharper_static_problem_in_text_highlighting=warning +resharper_string_compare_is_culture_specific_1_highlighting=warning +resharper_string_compare_is_culture_specific_2_highlighting=warning +resharper_string_compare_is_culture_specific_3_highlighting=warning +resharper_string_compare_is_culture_specific_4_highlighting=warning +resharper_string_compare_is_culture_specific_5_highlighting=warning +resharper_string_compare_is_culture_specific_6_highlighting=warning +resharper_string_compare_to_is_culture_specific_highlighting=warning +resharper_string_concatenation_to_template_string_highlighting=hint +resharper_string_ends_with_is_culture_specific_highlighting=none +resharper_string_index_of_is_culture_specific_1_highlighting=warning +resharper_string_index_of_is_culture_specific_2_highlighting=warning +resharper_string_index_of_is_culture_specific_3_highlighting=warning +resharper_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_string_literal_as_interpolation_argument_highlighting=suggestion +resharper_string_literal_typo_highlighting=suggestion +resharper_string_literal_wrong_quotes_highlighting=hint +resharper_string_starts_with_is_culture_specific_highlighting=none +resharper_structured_message_template_problem_highlighting=warning +resharper_struct_can_be_made_read_only_highlighting=suggestion +resharper_struct_member_can_be_made_read_only_highlighting=none +resharper_suggest_base_type_for_parameter_highlighting=hint +resharper_suggest_base_type_for_parameter_in_constructor_highlighting=hint +resharper_suggest_discard_declaration_var_style_highlighting=hint +resharper_suggest_var_or_type_built_in_types_highlighting=hint +resharper_suggest_var_or_type_deconstruction_declarations_highlighting=hint +resharper_suggest_var_or_type_elsewhere_highlighting=hint +resharper_suggest_var_or_type_simple_types_highlighting=hint +resharper_super_call_highlighting=suggestion +resharper_super_call_prohibits_this_highlighting=error +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting=warning +resharper_suspicious_instanceof_check_highlighting=warning +resharper_suspicious_lambda_block_highlighting=warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting=warning +resharper_suspicious_math_sign_method_highlighting=warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting=warning +resharper_suspicious_this_usage_highlighting=warning +resharper_suspicious_typeof_check_highlighting=warning +resharper_suspicious_type_conversion_global_highlighting=warning +resharper_swap_via_deconstruction_highlighting=suggestion +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting=hint +resharper_switch_statement_for_enum_misses_default_section_highlighting=hint +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting=hint +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting=none +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting=warning +resharper_syntax_is_not_allowed_highlighting=warning +resharper_tabs_and_spaces_mismatch_highlighting=none +resharper_tabs_are_disallowed_highlighting=none +resharper_tabs_outside_indent_highlighting=none +resharper_tail_recursive_call_highlighting=hint +resharper_tasks_not_loaded_highlighting=warning +resharper_ternary_can_be_replaced_by_its_condition_highlighting=warning +resharper_this_in_global_context_highlighting=warning +resharper_thread_static_at_instance_field_highlighting=warning +resharper_thread_static_field_has_initializer_highlighting=warning +resharper_throw_must_be_followed_by_expression_highlighting=error +resharper_too_wide_local_variable_scope_highlighting=suggestion +resharper_try_cast_always_succeeds_highlighting=suggestion +resharper_try_statements_can_be_merged_highlighting=hint +resharper_ts_not_resolved_highlighting=error +resharper_ts_resolved_from_inaccessible_module_highlighting=error +resharper_type_guard_doesnt_affect_anything_highlighting=warning +resharper_type_guard_produces_never_type_highlighting=warning +resharper_type_parameter_can_be_variant_highlighting=suggestion +resharper_type_parameter_hides_type_param_from_outer_scope_highlighting=warning +resharper_ul_tag_contains_non_li_elements_highlighting=hint +resharper_unassigned_field_global_highlighting=suggestion +resharper_unassigned_field_local_highlighting=warning +resharper_unassigned_get_only_auto_property_highlighting=warning +resharper_unassigned_readonly_field_highlighting=warning +resharper_unclosed_script_highlighting=error +resharper_undeclared_global_variable_using_highlighting=warning +resharper_unexpected_value_highlighting=error +resharper_unknown_css_class_highlighting=warning +resharper_unknown_css_variable_highlighting=warning +resharper_unknown_css_vendor_extension_highlighting=hint +resharper_unknown_item_group_highlighting=warning +resharper_unknown_metadata_highlighting=warning +resharper_unknown_output_parameter_highlighting=warning +resharper_unknown_property_highlighting=warning +resharper_unknown_target_highlighting=warning +resharper_unknown_task_attribute_highlighting=warning +resharper_unknown_task_highlighting=warning +resharper_unnecessary_whitespace_highlighting=none +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting=warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting=warning +resharper_unreal_header_tool_error_highlighting=error +resharper_unreal_header_tool_parser_error_highlighting=error +resharper_unreal_header_tool_warning_highlighting=warning +resharper_unsafe_comma_in_object_properties_list_highlighting=warning +resharper_unsupported_required_base_type_highlighting=warning +resharper_unused_anonymous_method_signature_highlighting=warning +resharper_unused_auto_property_accessor_global_highlighting=warning +resharper_unused_auto_property_accessor_local_highlighting=warning +resharper_unused_import_clause_highlighting=warning +resharper_unused_inherited_parameter_highlighting=hint +resharper_unused_locals_highlighting=warning +resharper_unused_local_function_highlighting=warning +resharper_unused_local_function_parameter_highlighting=warning +resharper_unused_local_function_return_value_highlighting=warning +resharper_unused_local_import_highlighting=warning +resharper_unused_member_global_highlighting=suggestion +resharper_unused_member_hierarchy_global_highlighting=suggestion +resharper_unused_member_hierarchy_local_highlighting=warning +resharper_unused_member_in_super_global_highlighting=suggestion +resharper_unused_member_in_super_local_highlighting=warning +resharper_unused_member_local_highlighting=warning +resharper_unused_method_return_value_global_highlighting=suggestion +resharper_unused_method_return_value_local_highlighting=warning +resharper_unused_parameter_global_highlighting=suggestion +resharper_unused_parameter_highlighting=warning +resharper_unused_parameter_in_partial_method_highlighting=warning +resharper_unused_parameter_local_highlighting=warning +resharper_unused_property_highlighting=warning +resharper_unused_tuple_component_in_return_value_highlighting=warning +resharper_unused_type_global_highlighting=suggestion +resharper_unused_type_local_highlighting=warning +resharper_unused_type_parameter_highlighting=warning +resharper_unused_variable_highlighting=warning +resharper_usage_of_definitely_unassigned_value_highlighting=warning +resharper_usage_of_possibly_unassigned_value_highlighting=warning +resharper_useless_binary_operation_highlighting=warning +resharper_useless_comparison_to_integral_constant_highlighting=warning +resharper_use_array_creation_expression_1_highlighting=suggestion +resharper_use_array_creation_expression_2_highlighting=suggestion +resharper_use_array_empty_method_highlighting=suggestion +resharper_use_as_instead_of_type_cast_highlighting=hint +resharper_use_await_using_highlighting=suggestion +resharper_use_cancellation_token_for_i_async_enumerable_highlighting=suggestion +resharper_use_collection_count_property_highlighting=suggestion +resharper_use_configure_await_false_for_async_disposable_highlighting=none +resharper_use_configure_await_false_highlighting=suggestion +resharper_use_deconstruction_highlighting=hint +resharper_use_deconstruction_on_parameter_highlighting=hint +resharper_use_empty_types_field_highlighting=suggestion +resharper_use_event_args_empty_field_highlighting=suggestion +resharper_use_format_specifier_in_format_string_highlighting=suggestion +resharper_use_implicitly_typed_variable_evident_highlighting=hint +resharper_use_implicitly_typed_variable_highlighting=none +resharper_use_implicit_by_val_modifier_highlighting=hint +resharper_use_indexed_property_highlighting=suggestion +resharper_use_index_from_end_expression_highlighting=suggestion +resharper_use_is_operator_1_highlighting=suggestion +resharper_use_is_operator_2_highlighting=suggestion +resharper_use_method_any_0_highlighting=suggestion +resharper_use_method_any_1_highlighting=suggestion +resharper_use_method_any_2_highlighting=suggestion +resharper_use_method_any_3_highlighting=suggestion +resharper_use_method_any_4_highlighting=suggestion +resharper_use_method_is_instance_of_type_highlighting=suggestion +resharper_use_nameof_expression_for_part_of_the_string_highlighting=none +resharper_use_nameof_expression_highlighting=suggestion +resharper_use_name_of_instead_of_type_of_highlighting=suggestion +resharper_use_negated_pattern_in_is_expression_highlighting=hint +resharper_use_negated_pattern_matching_highlighting=hint +resharper_use_nullable_annotation_instead_of_attribute_highlighting=suggestion +resharper_use_nullable_attributes_supported_by_compiler_highlighting=suggestion +resharper_use_nullable_reference_types_annotation_syntax_highlighting=warning +resharper_use_null_propagation_highlighting=hint +resharper_use_null_propagation_when_possible_highlighting=none +resharper_use_object_or_collection_initializer_highlighting=suggestion +resharper_use_of_implicit_global_in_function_scope_highlighting=warning +resharper_use_of_possibly_unassigned_property_highlighting=warning +resharper_use_pattern_matching_highlighting=suggestion +resharper_use_positional_deconstruction_pattern_highlighting=none +resharper_use_string_interpolation_highlighting=suggestion +resharper_use_switch_case_pattern_variable_highlighting=suggestion +resharper_use_throw_if_null_method_highlighting=none +resharper_use_verbatim_string_highlighting=hint +resharper_using_of_reserved_word_error_highlighting=error +resharper_using_of_reserved_word_highlighting=warning +resharper_value_parameter_not_used_highlighting=warning +resharper_value_range_attribute_violation_highlighting=warning +resharper_value_should_have_units_highlighting=error +resharper_variable_can_be_made_const_highlighting=hint +resharper_variable_can_be_made_let_highlighting=hint +resharper_variable_can_be_moved_to_inner_block_highlighting=hint +resharper_variable_can_be_not_nullable_highlighting=warning +resharper_variable_hides_outer_variable_highlighting=warning +resharper_variable_used_before_declared_highlighting=warning +resharper_variable_used_in_inner_scope_before_declared_highlighting=warning +resharper_variable_used_out_of_scope_highlighting=warning +resharper_vb_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_vb_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_vb_possible_mistaken_argument_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_vb_remove_to_list_1_highlighting=suggestion +resharper_vb_remove_to_list_2_highlighting=suggestion +resharper_vb_replace_with_first_or_default_highlighting=suggestion +resharper_vb_replace_with_last_or_default_highlighting=suggestion +resharper_vb_replace_with_of_type_1_highlighting=suggestion +resharper_vb_replace_with_of_type_2_highlighting=suggestion +resharper_vb_replace_with_of_type_any_1_highlighting=suggestion +resharper_vb_replace_with_of_type_any_2_highlighting=suggestion +resharper_vb_replace_with_of_type_count_1_highlighting=suggestion +resharper_vb_replace_with_of_type_count_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_where_highlighting=suggestion +resharper_vb_replace_with_single_assignment_1_highlighting=suggestion +resharper_vb_replace_with_single_assignment_2_highlighting=suggestion +resharper_vb_replace_with_single_call_to_any_highlighting=suggestion +resharper_vb_replace_with_single_call_to_count_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_vb_replace_with_single_or_default_highlighting=suggestion +resharper_vb_simplify_linq_expression_10_highlighting=hint +resharper_vb_simplify_linq_expression_1_highlighting=suggestion +resharper_vb_simplify_linq_expression_2_highlighting=suggestion +resharper_vb_simplify_linq_expression_3_highlighting=suggestion +resharper_vb_simplify_linq_expression_4_highlighting=suggestion +resharper_vb_simplify_linq_expression_5_highlighting=suggestion +resharper_vb_simplify_linq_expression_6_highlighting=suggestion +resharper_vb_simplify_linq_expression_7_highlighting=hint +resharper_vb_simplify_linq_expression_8_highlighting=hint +resharper_vb_simplify_linq_expression_9_highlighting=hint +resharper_vb_string_compare_is_culture_specific_1_highlighting=warning +resharper_vb_string_compare_is_culture_specific_2_highlighting=warning +resharper_vb_string_compare_is_culture_specific_3_highlighting=warning +resharper_vb_string_compare_is_culture_specific_4_highlighting=warning +resharper_vb_string_compare_is_culture_specific_5_highlighting=warning +resharper_vb_string_compare_is_culture_specific_6_highlighting=warning +resharper_vb_string_compare_to_is_culture_specific_highlighting=warning +resharper_vb_string_ends_with_is_culture_specific_highlighting=none +resharper_vb_string_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_starts_with_is_culture_specific_highlighting=none +resharper_vb_unreachable_code_highlighting=warning +resharper_vb_use_array_creation_expression_1_highlighting=suggestion +resharper_vb_use_array_creation_expression_2_highlighting=suggestion +resharper_vb_use_first_instead_highlighting=warning +resharper_vb_use_method_any_1_highlighting=suggestion +resharper_vb_use_method_any_2_highlighting=suggestion +resharper_vb_use_method_any_3_highlighting=suggestion +resharper_vb_use_method_any_4_highlighting=suggestion +resharper_vb_use_method_any_5_highlighting=suggestion +resharper_vb_use_method_is_instance_of_type_highlighting=suggestion +resharper_vb_use_type_of_is_operator_1_highlighting=suggestion +resharper_vb_use_type_of_is_operator_2_highlighting=suggestion +resharper_virtual_member_call_in_constructor_highlighting=warning +resharper_virtual_member_never_overridden_global_highlighting=suggestion +resharper_virtual_member_never_overridden_local_highlighting=suggestion +resharper_void_method_with_must_use_return_value_attribute_highlighting=warning +resharper_web_config_module_not_resolved_highlighting=error +resharper_web_config_module_qualification_resolve_highlighting=warning +resharper_web_config_redundant_add_namespace_tag_highlighting=warning +resharper_web_config_redundant_location_tag_highlighting=warning +resharper_web_config_tag_prefix_redundand_highlighting=warning +resharper_web_config_type_not_resolved_highlighting=error +resharper_web_config_unused_add_tag_highlighting=warning +resharper_web_config_unused_element_due_to_config_source_attribute_highlighting=warning +resharper_web_config_unused_remove_or_clear_tag_highlighting=warning +resharper_web_config_web_config_path_warning_highlighting=warning +resharper_web_config_wrong_module_highlighting=error +resharper_web_ignored_path_highlighting=none +resharper_web_mapped_path_highlighting=hint +resharper_with_expression_instead_of_initializer_highlighting=suggestion +resharper_with_statement_using_error_highlighting=error +resharper_wrong_expression_statement_highlighting=warning +resharper_wrong_indent_size_highlighting=none +resharper_wrong_metadata_use_highlighting=none +resharper_wrong_public_modifier_specification_highlighting=hint +resharper_wrong_require_relative_path_highlighting=hint +resharper_xaml_assign_null_to_not_null_attribute_highlighting=warning +resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting=warning +resharper_xaml_binding_without_context_not_resolved_highlighting=hint +resharper_xaml_binding_with_context_not_resolved_highlighting=warning +resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting=error +resharper_xaml_constructor_warning_highlighting=warning +resharper_xaml_decimal_parsing_is_culture_dependent_highlighting=warning +resharper_xaml_dependency_property_resolve_error_highlighting=warning +resharper_xaml_duplicate_style_setter_highlighting=warning +resharper_xaml_dynamic_resource_error_highlighting=error +resharper_xaml_element_name_reference_not_resolved_highlighting=error +resharper_xaml_empty_grid_length_definition_highlighting=error +resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting=hint +resharper_xaml_ignored_path_highlighting_highlighting=none +resharper_xaml_index_out_of_grid_definition_highlighting=warning +resharper_xaml_invalid_member_type_highlighting=error +resharper_xaml_invalid_resource_target_type_highlighting=error +resharper_xaml_invalid_resource_type_highlighting=error +resharper_xaml_invalid_type_highlighting=error +resharper_xaml_language_level_highlighting=error +resharper_xaml_mapped_path_highlighting_highlighting=hint +resharper_xaml_method_arguments_will_be_ignored_highlighting=warning +resharper_xaml_missing_grid_index_highlighting=warning +resharper_xaml_overloads_collision_highlighting=warning +resharper_xaml_parent_is_out_of_current_component_tree_highlighting=warning +resharper_xaml_path_error_highlighting=warning +resharper_xaml_possible_null_reference_exception_highlighting=suggestion +resharper_xaml_redundant_attached_property_highlighting=warning +resharper_xaml_redundant_binding_mode_attribute_highlighting=warning +resharper_xaml_redundant_collection_property_highlighting=warning +resharper_xaml_redundant_freeze_attribute_highlighting=warning +resharper_xaml_redundant_grid_definitions_highlighting=warning +resharper_xaml_redundant_grid_span_highlighting=warning +resharper_xaml_redundant_modifiers_attribute_highlighting=warning +resharper_xaml_redundant_namespace_alias_highlighting=warning +resharper_xaml_redundant_name_attribute_highlighting=warning +resharper_xaml_redundant_property_type_qualifier_highlighting=warning +resharper_xaml_redundant_resource_highlighting=warning +resharper_xaml_redundant_styled_value_highlighting=warning +resharper_xaml_redundant_update_source_trigger_attribute_highlighting=warning +resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting=warning +resharper_xaml_resource_file_path_case_mismatch_highlighting=warning +resharper_xaml_routed_event_resolve_error_highlighting=warning +resharper_xaml_static_resource_not_resolved_highlighting=warning +resharper_xaml_style_class_not_found_highlighting=warning +resharper_xaml_style_invalid_target_type_highlighting=error +resharper_xaml_unexpected_text_token_highlighting=error +resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting=error +resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting=warning +resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting=warning +resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting=warning +resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting=warning +resharper_xaml_x_key_attribute_disallowed_highlighting=error +resharper_xml_doc_comment_syntax_problem_highlighting=warning +resharper_xunit_xunit_test_with_console_output_highlighting=warning + +# Standard properties +end_of_line= crlf +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +[*.{cshtml,htm,html,proto,razor}] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,c,c++,cc,cginc,compute,cp,cpp,cs,css,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,js,jsx,master,mpp,mq4,mq5,mqh,paml,skin,tpp,ts,tsx,usf,ush,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] +indent_style=space +indent_size= 4 +tab_width= 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs index 68674268..85fb078e 100644 --- a/Penumbra.GameData/Enums/ChangedItemExtensions.cs +++ b/Penumbra.GameData/Enums/ChangedItemExtensions.cs @@ -1,4 +1,5 @@ using System; +using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.Api.Enums; using Action = Lumina.Excel.GeneratedSheets.Action; @@ -18,13 +19,13 @@ public static class ChangedItemExtensions }; } - public static object? GetObject( this ChangedItemType type, uint id ) + public static object? GetObject( this ChangedItemType type, DataManager manager, uint id ) { return type switch { ChangedItemType.None => null, - ChangedItemType.Item => ObjectIdentification.DataManager?.GetExcelSheet< Item >()?.GetRow( id ), - ChangedItemType.Action => ObjectIdentification.DataManager?.GetExcelSheet< Action >()?.GetRow( id ), + ChangedItemType.Item => manager.GetExcelSheet< Item >()?.GetRow( id ), + ChangedItemType.Action => manager.GetExcelSheet< Action >()?.GetRow( id ), ChangedItemType.Customization => null, _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index c8885c3c..7eace70e 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -1,48 +1,73 @@ using System; using System.Collections.Generic; +using Dalamud; using Dalamud.Data; +using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; namespace Penumbra.GameData; public static class GameData { - internal static ObjectIdentification? Identification; - internal static readonly GamePathParser GamePathParser = new(); + /// + /// Obtain an object identifier that can link a game path to game objects that use it, using your client language. + /// + public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager) + => new ObjectIdentification(pluginInterface, dataManager, dataManager.Language); - public static IObjectIdentifier GetIdentifier( DataManager dataManager ) - { - Identification ??= new ObjectIdentification( dataManager, dataManager.Language ); - return Identification; - } - - public static IObjectIdentifier GetIdentifier() - { - if( Identification == null ) - { - throw new Exception( "Object Identification was not initialized." ); - } - - return Identification; - } + /// + /// Obtain an object identifier that can link a game path to game objects that use it using the given language. + /// + public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + => new ObjectIdentification(pluginInterface, dataManager, language); + /// + /// Obtain a parser for game paths. + /// public static IGamePathParser GetGamePathParser() - => GamePathParser; + => new GamePathParser(); } -public interface IObjectIdentifier -{ - public void Identify( IDictionary< string, object? > set, GamePath path ); - public Dictionary< string, object? > Identify( GamePath path ); - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ); +public interface IObjectIdentifier : IDisposable +{ + /// + /// An accessible parser for game paths. + /// + public IGamePathParser GamePathParser { get; } + + /// + /// Add all known game objects using the given game path to the dictionary. + /// + /// A pre-existing dictionary to which names (and optional linked objects) can be added. + /// The game path to identify. + public void Identify(IDictionary set, string path); + + /// + /// Return named information and possibly linked objects for all known game objects using the given path. + /// + /// The game path to identify. + public Dictionary Identify(string path); + + /// + /// Identify an equippable item by its model values. + /// + /// The primary model ID for the piece of equipment. + /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. + /// The variant ID of the model. + /// The equipment slot the piece of equipment uses. + /// + public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + + /// + public IReadOnlyList? Identify(SetId setId, ushort variant, EquipSlot slot) + => Identify(setId, 0, variant, slot); } public interface IGamePathParser { - public ObjectType PathToObjectType( GamePath path ); - public GameObjectInfo GetFileInfo( GamePath path ); - public string VfxToKey( GamePath path ); -} \ No newline at end of file + public ObjectType PathToObjectType(string path); + public GameObjectInfo GetFileInfo(string path); + public string VfxToKey(string path); +} diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/GamePathParser.cs index 3a255bdd..d1475a10 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/GamePathParser.cs @@ -1,16 +1,66 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; namespace Penumbra.GameData; internal class GamePathParser : IGamePathParser { + public GameObjectInfo GetFileInfo(string path) + { + path = path.ToLowerInvariant().Replace('\\', '/'); + + var (fileType, objectType, match) = ParseGamePath(path); + if (match == null || !match.Success) + return new GameObjectInfo + { + FileType = fileType, + ObjectType = objectType, + }; + + try + { + var groups = match.Groups; + switch (objectType) + { + case ObjectType.Accessory: return HandleEquipment(fileType, groups); + case ObjectType.Equipment: return HandleEquipment(fileType, groups); + case ObjectType.Weapon: return HandleWeapon(fileType, groups); + case ObjectType.Map: return HandleMap(fileType, groups); + case ObjectType.Monster: return HandleMonster(fileType, groups); + case ObjectType.DemiHuman: return HandleDemiHuman(fileType, groups); + case ObjectType.Character: return HandleCustomization(fileType, groups); + case ObjectType.Icon: return HandleIcon(fileType, groups); + } + } + catch (Exception e) + { + PluginLog.Error($"Could not parse {path}:\n{e}"); + } + + return new GameObjectInfo + { + FileType = fileType, + ObjectType = objectType, + }; + } + + public string VfxToKey(string path) + { + var match = _vfxRegexTmb.Match(path); + if (match.Success) + return match.Groups["key"].Value.ToLowerInvariant(); + + match = _vfxRegexPap.Match(path); + return match.Success ? match.Groups["key"].Value.ToLowerInvariant() : string.Empty; + } + private const string CharacterFolder = "chara"; private const string EquipmentFolder = "equipment"; private const string PlayerFolder = "human"; @@ -30,62 +80,74 @@ internal class GamePathParser : IGamePathParser private const string WorldFolder1 = "bgcommon"; private const string WorldFolder2 = "bg"; - // @formatter:off - private readonly Dictionary> _regexes = new() - { { FileType.Font, new Dictionary< ObjectType, Regex[] >(){ { ObjectType.Font, new Regex[]{ new(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt") } } } } - , { FileType.Texture, new Dictionary< ObjectType, Regex[] >() - { { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex") } } - , { ObjectType.Map, new Regex[]{ new(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex") } } - , { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex") } } - , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex") } } - , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } } - , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } } - , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex") } } - , { 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") } } - , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl") } } - , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl") } } - , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl") } } - , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl") } } - , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl") } } } } - , { FileType.Material, new Dictionary< ObjectType, Regex[] >() - { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl") } } - , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl") } } - , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } - , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } - , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } - , { ObjectType.Character, new Regex[]{ new( @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl" ) } } } } - , { FileType.Imc, new Dictionary< ObjectType, Regex[] >() - { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc") } } - , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } - , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } } - , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } } - , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } }, - }; + // @formatter:off + // language=regex + private readonly IReadOnlyDictionary>> _regexes = new Dictionary>>() + { + [FileType.Font] = new Dictionary> + { + [ObjectType.Font] = CreateRegexes( @"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), + }, + [FileType.Texture] = new Dictionary> + { + [ObjectType.Icon] = CreateRegexes( @"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex"), + [ObjectType.Map] = CreateRegexes( @"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex"), + [ObjectType.Weapon] = CreateRegexes( @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex"), + [ObjectType.Monster] = CreateRegexes( @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex"), + [ObjectType.Equipment] = CreateRegexes( @"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), + [ObjectType.DemiHuman] = CreateRegexes( @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), + [ObjectType.Accessory] = CreateRegexes( @"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex"), + [ObjectType.Character] = CreateRegexes( @"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" + , @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture" + , @"chara/common/texture/skin(?'skin'.*)\.tex" + , @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex" + , @"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex"), + }, + [FileType.Model] = new Dictionary> + { + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl"), + [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl"), + [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl"), + [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl"), + [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl"), + }, + [FileType.Material] = new Dictionary> + { + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl"), + [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), + [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), + [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), + [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl"), + }, + [FileType.Imc] = new Dictionary> + { + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc"), + [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc"), + [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc"), + [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc"), + }, + }; // @formatter:on - public ObjectType PathToObjectType( GamePath path ) + + private static IReadOnlyList CreateRegexes(params string[] regexes) + => regexes.Select(s => new Regex(s, RegexOptions.Compiled)).ToArray(); + + public ObjectType PathToObjectType(string path) { - if( path.Empty ) - { + if (path.Length == 0) return ObjectType.Unknown; - } - string p = path; - var folders = p.Split( '/' ); - if( folders.Length < 2 ) - { + var folders = path.Split('/'); + if (folders.Length < 2) return ObjectType.Unknown; - } - return folders[ 0 ] switch + return folders[0] switch { - CharacterFolder => folders[ 1 ] switch + CharacterFolder => folders[1] switch { EquipmentFolder => ObjectType.Equipment, AccessoryFolder => ObjectType.Accessory, @@ -96,7 +158,7 @@ internal class GamePathParser : IGamePathParser CommonFolder => ObjectType.Character, _ => ObjectType.Unknown, }, - UiFolder => folders[ 1 ] switch + UiFolder => folders[1] switch { IconFolder => ObjectType.Icon, LoadingFolder => ObjectType.LoadingScreen, @@ -104,13 +166,13 @@ internal class GamePathParser : IGamePathParser InterfaceFolder => ObjectType.Interface, _ => ObjectType.Unknown, }, - CommonFolder => folders[ 1 ] switch + CommonFolder => folders[1] switch { FontFolder => ObjectType.Font, _ => ObjectType.Unknown, }, HousingFolder => ObjectType.Housing, - WorldFolder1 => folders[ 1 ] switch + WorldFolder1 => folders[1] switch { HousingFolder => ObjectType.Housing, _ => ObjectType.World, @@ -121,152 +183,120 @@ internal class GamePathParser : IGamePathParser }; } - private (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + private (FileType, ObjectType, Match?) ParseGamePath(string path) { - if( !Names.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) ) - { + if (!Names.ExtensionToFileType.TryGetValue(Path.GetExtension(path), out var fileType)) fileType = FileType.Unknown; - } - var objectType = PathToObjectType( path ); + var objectType = PathToObjectType(path); - if( !_regexes.TryGetValue( fileType, out var objectDict ) ) + if (!_regexes.TryGetValue(fileType, out var objectDict)) + return (fileType, objectType, null); + + if (!objectDict.TryGetValue(objectType, out var regexes)) + return (fileType, objectType, null); + + foreach (var regex in regexes) { - return ( fileType, objectType, null ); + var match = regex.Match(path); + if (match.Success) + return (fileType, objectType, match); } - if( !objectDict.TryGetValue( objectType, out var regexes ) ) - { - return ( fileType, objectType, null ); - } - - foreach( var regex in regexes ) - { - var match = regex.Match( path ); - if( match.Success ) - { - return ( fileType, objectType, match ); - } - } - - return ( fileType, objectType, null ); + return (fileType, objectType, null); } - private static string Extension( string filename ) + private static GameObjectInfo HandleEquipment(FileType fileType, GroupCollection groups) { - var extIdx = filename.LastIndexOf( '.' ); - return extIdx < 0 ? "" : filename.Substring( extIdx ); + var setId = ushort.Parse(groups["id"].Value); + if (fileType == FileType.Imc) + return GameObjectInfo.Equipment(fileType, setId); + + var gr = Names.GenderRaceFromCode(groups["race"].Value); + var slot = Names.SuffixToEquipSlot[groups["slot"].Value]; + if (fileType == FileType.Model) + return GameObjectInfo.Equipment(fileType, setId, gr, slot); + + var variant = byte.Parse(groups["variant"].Value); + return GameObjectInfo.Equipment(fileType, setId, gr, slot, variant); } - private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleWeapon(FileType fileType, GroupCollection groups) { - var setId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc ) - { - return GameObjectInfo.Equipment( fileType, setId ); - } + var weaponId = ushort.Parse(groups["weapon"].Value); + var setId = ushort.Parse(groups["id"].Value); + if (fileType == FileType.Imc || fileType == FileType.Model) + return GameObjectInfo.Weapon(fileType, setId, weaponId); - var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; - if( fileType == FileType.Model ) - { - return GameObjectInfo.Equipment( fileType, setId, gr, slot ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant ); + var variant = byte.Parse(groups["variant"].Value); + return GameObjectInfo.Weapon(fileType, setId, weaponId, variant); } - private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleMonster(FileType fileType, GroupCollection groups) { - var weaponId = ushort.Parse( groups[ "weapon" ].Value ); - var setId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc || fileType == FileType.Model ) - { - return GameObjectInfo.Weapon( fileType, setId, weaponId ); - } + var monsterId = ushort.Parse(groups["monster"].Value); + var bodyId = ushort.Parse(groups["id"].Value); + if (fileType == FileType.Imc || fileType == FileType.Model) + return GameObjectInfo.Monster(fileType, monsterId, bodyId); - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + var variant = byte.Parse(groups["variant"].Value); + return GameObjectInfo.Monster(fileType, monsterId, bodyId, variant); } - private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleDemiHuman(FileType fileType, GroupCollection groups) { - var monsterId = ushort.Parse( groups[ "monster" ].Value ); - var bodyId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc || fileType == FileType.Model ) - { - return GameObjectInfo.Monster( fileType, monsterId, bodyId ); - } + var demiHumanId = ushort.Parse(groups["id"].Value); + var equipId = ushort.Parse(groups["equip"].Value); + if (fileType == FileType.Imc) + return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId); - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant ); + var slot = Names.SuffixToEquipSlot[groups["slot"].Value]; + if (fileType == FileType.Model) + return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot); + + var variant = byte.Parse(groups["variant"].Value); + return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot, variant); } - private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleCustomization(FileType fileType, GroupCollection groups) { - var demiHumanId = ushort.Parse( groups[ "id" ].Value ); - var equipId = ushort.Parse( groups[ "equip" ].Value ); - if( fileType == FileType.Imc ) + if (groups["catchlight"].Success) + return GameObjectInfo.Customization(fileType, CustomizationType.Iris); + + if (groups["skin"].Success) + return GameObjectInfo.Customization(fileType, CustomizationType.Skin); + + var id = ushort.Parse(groups["id"].Value); + if (groups["location"].Success) { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); + var tmpType = groups["location"].Value == "face" ? CustomizationType.DecalFace + : groups["location"].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; + return GameObjectInfo.Customization(fileType, tmpType, id); } - var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; - if( fileType == FileType.Model ) - { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); - } - - 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 ); - } - - var id = ushort.Parse( groups[ "id" ].Value ); - if( groups[ "location" ].Success ) - { - var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace - : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; - return GameObjectInfo.Customization( fileType, tmpType, id ); - } - - var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ]; - var type = groups[ "slot" ].Success - ? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ] + var gr = Names.GenderRaceFromCode(groups["race"].Value); + var bodySlot = Names.StringToBodySlot[groups["type"].Value]; + var type = groups["slot"].Success + ? Names.SuffixToCustomizationType[groups["slot"].Value] : CustomizationType.Skin; - if( fileType == FileType.Material ) + if (fileType == FileType.Material) { - var variant = groups[ "variant" ].Success ? byte.Parse( groups[ "variant" ].Value ) : ( byte )0; - return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant ); + var variant = groups["variant"].Success ? byte.Parse(groups["variant"].Value) : (byte)0; + return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot, variant); } - return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot ); + return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot); } - private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleIcon(FileType fileType, GroupCollection groups) { - var hq = groups[ "hq" ].Success; - var hr = groups[ "hr" ].Success; - var id = uint.Parse( groups[ "id" ].Value ); - if( !groups[ "lang" ].Success ) - { - return GameObjectInfo.Icon( fileType, id, hq, hr ); - } + var hq = groups["hq"].Success; + var hr = groups["hr"].Success; + var id = uint.Parse(groups["id"].Value); + if (!groups["lang"].Success) + return GameObjectInfo.Icon(fileType, id, hq, hr); - var language = groups[ "lang" ].Value switch + var language = groups["lang"].Value switch { "en" => Dalamud.ClientLanguage.English, "ja" => Dalamud.ClientLanguage.Japanese, @@ -274,65 +304,23 @@ internal class GamePathParser : IGamePathParser "fr" => Dalamud.ClientLanguage.French, _ => Dalamud.ClientLanguage.English, }; - return GameObjectInfo.Icon( fileType, id, hq, hr, language ); + return GameObjectInfo.Icon(fileType, id, hq, hr, language); } - private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups ) + private static GameObjectInfo HandleMap(FileType fileType, GroupCollection groups) { - var map = Encoding.ASCII.GetBytes( groups[ "id" ].Value ); - var variant = byte.Parse( groups[ "variant" ].Value ); - if( groups[ "suffix" ].Success ) + var map = Encoding.ASCII.GetBytes(groups["id"].Value); + var variant = byte.Parse(groups["variant"].Value); + if (groups["suffix"].Success) { - var suffix = Encoding.ASCII.GetBytes( groups[ "suffix" ].Value )[ 0 ]; - return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant, suffix ); + var suffix = Encoding.ASCII.GetBytes(groups["suffix"].Value)[0]; + return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant, suffix); } - return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant ); + return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant); } - public GameObjectInfo GetFileInfo( GamePath path ) - { - var (fileType, objectType, match) = ParseGamePath( path ); - if( match == null || !match.Success ) - { - return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; - } - try - { - var groups = match.Groups; - switch( objectType ) - { - case ObjectType.Accessory: return HandleEquipment( fileType, groups ); - case ObjectType.Equipment: return HandleEquipment( fileType, groups ); - case ObjectType.Weapon: return HandleWeapon( fileType, groups ); - case ObjectType.Map: return HandleMap( fileType, groups ); - case ObjectType.Monster: return HandleMonster( fileType, groups ); - case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups ); - case ObjectType.Character: return HandleCustomization( fileType, groups ); - case ObjectType.Icon: return HandleIcon( fileType, groups ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse {path}:\n{e}" ); - } - - return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; - } - - private readonly Regex _vfxRegexTmb = new(@"chara/action/(?'key'[^\s]+?)\.tmb"); - private readonly Regex _vfxRegexPap = new(@"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap"); - - public string VfxToKey( GamePath path ) - { - var match = _vfxRegexTmb.Match( path ); - if( match.Success ) - { - return match.Groups[ "key" ].Value.ToLowerInvariant(); - } - - match = _vfxRegexPap.Match( path ); - return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; - } -} \ No newline at end of file + private readonly Regex _vfxRegexTmb = new(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.Compiled | RegexOptions.IgnoreCase); +} diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index fa137006..20642156 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -1,101 +1,193 @@ +using System; using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Utility; using Action = Lumina.Excel.GeneratedSheets.Action; namespace Penumbra.GameData; internal class ObjectIdentification : IObjectIdentifier { - public static DataManager? DataManager = null!; - private readonly List< (ulong, HashSet< Item >) > _weapons; - private readonly List< (ulong, HashSet< Item >) > _equipment; - private readonly Dictionary< string, HashSet< Action > > _actions; + public IGamePathParser GamePathParser { get; } = new GamePathParser(); - private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item ) + public void Identify(IDictionary set, string path) { - if( dict.TryGetValue( key, out var list ) ) + if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)) { - return list.Add( item ); - } - - dict[ key ] = new HashSet< Item > { item }; - return true; - } - - private static ulong EquipmentKey( Item i ) - { - var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A; - var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B; - var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - return ( model << 32 ) | ( slot << 16 ) | variant; - } - - private static ulong WeaponKey( Item i, bool offhand ) - { - var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain; - var model = ( ulong )quad.A; - var type = ( ulong )quad.B; - var variant = ( ulong )quad.C; - - return ( model << 32 ) | ( type << 16 ) | variant; - } - - private void AddAction( string key, Action action ) - { - if( key.Length == 0 ) - { - return; - } - - key = key.ToLowerInvariant(); - if( _actions.TryGetValue( key, out var actions ) ) - { - actions.Add( action ); + IdentifyVfx(set, path); } else { - _actions[ key ] = new HashSet< Action > { action }; + var info = GamePathParser.GetFileInfo(path); + IdentifyParsed(set, info); } } - public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage ) + public Dictionary Identify(string path) { - DataManager = dataManager; - var items = dataManager.GetExcelSheet< Item >( clientLanguage )!; - SortedList< ulong, HashSet< Item > > weapons = new(); - SortedList< ulong, HashSet< Item > > equipment = new(); - foreach( var item in items ) + Dictionary ret = new(); + Identify(ret, path); + return ret; + } + + public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + { + switch (slot) { - switch( ( EquipSlot )item.EquipSlotCategory.Row ) + case EquipSlot.MainHand: + case EquipSlot.OffHand: { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - case EquipSlot.BothHand: - if( item.ModelMain != 0 ) - { - Add( weapons, WeaponKey( item, false ), item ); - } + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, + ((ulong)setId << 32) | ((ulong)weaponType << 16) | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _weapons[begin].Item2 : null; + } + default: + { + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, + ((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _equipment[begin].Item2 : null; + } + } + } - if( item.ModelSub != 0 ) - { - Add( weapons, WeaponKey( item, true ), item ); - } + private const int Version = 1; - break; + private readonly DataManager _dataManager; + private readonly DalamudPluginInterface _pluginInterface; + private readonly ClientLanguage _language; + + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; + private readonly IReadOnlyDictionary> _actions; + + private readonly string _weaponsTag; + private readonly string _equipmentTag; + private readonly string _actionsTag; + private bool _disposed = false; + + public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + { + _pluginInterface = pluginInterface; + _dataManager = dataManager; + _language = language; + + _weaponsTag = $"Penumbra.Identification.Weapons.{_language}.V{Version}"; + _equipmentTag = $"Penumbra.Identification.Equipment.{_language}.V{Version}"; + _actionsTag = $"Penumbra.Identification.Actions.{_language}.V{Version}"; + + try + { + _weapons = pluginInterface.GetOrCreateData(_weaponsTag, CreateWeaponList); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared identification data for weapons:\n{ex}"); + _weapons = CreateWeaponList(); + } + + try + { + _equipment = pluginInterface.GetOrCreateData(_equipmentTag, CreateEquipmentList); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared identification data for equipment:\n{ex}"); + _equipment = CreateEquipmentList(); + } + + try + { + _actions = pluginInterface.GetOrCreateData(_actionsTag, CreateActionList); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared identification data for actions:\n{ex}"); + _actions = CreateActionList(); + } + } + + public void Dispose() + { + if (_disposed) + return; + + GC.SuppressFinalize(this); + _pluginInterface.RelinquishData(_weaponsTag); + _pluginInterface.RelinquishData(_equipmentTag); + _pluginInterface.RelinquishData(_actionsTag); + _disposed = true; + } + + ~ObjectIdentification() + => Dispose(); + + private static bool Add(IDictionary> dict, ulong key, Item item) + { + if (dict.TryGetValue(key, out var list)) + return list.Add(item); + + dict[key] = new HashSet { item }; + return true; + } + + private static ulong EquipmentKey(Item i) + { + var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; + var variant = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).B; + var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); + return (model << 32) | (slot << 16) | variant; + } + + private static ulong WeaponKey(Item i, bool offhand) + { + var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; + var model = (ulong)quad.A; + var type = (ulong)quad.B; + var variant = (ulong)quad.C; + + return (model << 32) | (type << 16) | variant; + } + + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList() + { + var items = _dataManager.GetExcelSheet(_language)!; + var storage = new SortedList>(); + foreach (var item in items.Where(i + => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) + { + if (item.ModelMain != 0) + Add(storage, WeaponKey(item, false), item); + + if (item.ModelSub != 0) + Add(storage, WeaponKey(item, true), item); + } + + return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); + } + + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList() + { + var items = _dataManager.GetExcelSheet(_language)!; + var storage = new SortedList>(); + foreach (var item in items) + { + switch (((EquipSlot)item.EquipSlotCategory.Row).ToSlot()) + { // Accessories case EquipSlot.RFinger: case EquipSlot.Wrists: case EquipSlot.Ears: case EquipSlot.Neck: - Add( equipment, EquipmentKey( item ), item ); - break; // Equipment case EquipSlot.Head: case EquipSlot.Body: @@ -108,147 +200,148 @@ internal class ObjectIdentification : IObjectIdentifier case EquipSlot.FullBody: case EquipSlot.HeadBody: case EquipSlot.LegsFeet: - Add( equipment, EquipmentKey( item ), item ); + case EquipSlot.ChestHands: + Add(storage, EquipmentKey(item), item); break; - default: continue; } } - _actions = new Dictionary< string, HashSet< Action > >(); - foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )! - .Where( a => a.Name.ToString().Any() ) ) + return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); + } + + private IReadOnlyDictionary> CreateActionList() + { + var sheet = _dataManager.GetExcelSheet(_language)!; + var storage = new Dictionary>((int)sheet.RowCount); + + void AddAction(string? key, Action action) { - var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty; - var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty; - var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty; - AddAction( startKey, action ); - AddAction( endKey, action ); - AddAction( hitKey, action ); + if (key.IsNullOrEmpty()) + return; + + key = key.ToLowerInvariant(); + if (storage.TryGetValue(key, out var actions)) + actions.Add(action); + else + storage[key] = new HashSet { action }; } - _weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); - _equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); + foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty)) + { + var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); + var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); + var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); + AddAction(startKey, action); + AddAction(endKey, action); + AddAction(hitKey, action); + } + + return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } - private class Comparer : IComparer< (ulong, HashSet< Item >) > + private class Comparer : IComparer<(ulong, IReadOnlyList)> { - public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y ) - => x.Item1.CompareTo( y.Item1 ); + public int Compare((ulong, IReadOnlyList) x, (ulong, IReadOnlyList) y) + => x.Item1.CompareTo(y.Item1); } - private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask ) + private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList)> list, ulong key, ulong mask) { var maskedKey = key & mask; - var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() ); - if( idx < 0 ) + var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); + if (idx < 0) { - if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) ) - { - return ( -1, -1 ); - } + if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask)) + return (-1, -1); idx = ~idx; } var endIdx = idx + 1; - while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) ) - { + while (endIdx < list.Count && maskedKey == (list[endIdx].Item1 & mask)) ++endIdx; - } - return ( idx, endIdx ); + return (idx, endIdx); } - private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info ) + private void FindEquipment(IDictionary set, GameObjectInfo info) { - var key = ( ulong )info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; - if( info.EquipSlot != EquipSlot.Unknown ) + if (info.EquipSlot != EquipSlot.Unknown) { - key |= ( ulong )info.EquipSlot.ToSlot() << 16; + key |= (ulong)info.EquipSlot.ToSlot() << 16; mask |= 0xFFFF0000; } - if( info.Variant != 0 ) + if (info.Variant != 0) { key |= info.Variant; mask |= 0xFFFF; } - var (start, end) = FindIndexRange( _equipment, key, mask ); - if( start == -1 ) - { + var (start, end) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, key, mask); + if (start == -1) return; - } - for( ; start < end; ++start ) + for (; start < end; ++start) { - foreach( var item in _equipment[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } + foreach (var item in _equipment[start].Item2) + set[item.Name.ToString()] = item; } } - private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info ) + private void FindWeapon(IDictionary set, GameObjectInfo info) { - var key = ( ulong )info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; - if( info.SecondaryId != 0 ) + if (info.SecondaryId != 0) { - key |= ( ulong )info.SecondaryId << 16; + key |= (ulong)info.SecondaryId << 16; mask |= 0xFFFF0000; } - if( info.Variant != 0 ) + if (info.Variant != 0) { key |= info.Variant; mask |= 0xFFFF; } - var (start, end) = FindIndexRange( _weapons, key, mask ); - if( start == -1 ) - { + var (start, end) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, key, mask); + if (start == -1) return; - } - for( ; start < end; ++start ) + for (; start < end; ++start) { - foreach( var item in _weapons[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } + foreach (var item in _weapons[start].Item2) + set[item.Name.ToString()] = item; } } - private static void AddCounterString( IDictionary< string, object? > set, string data ) + private static void AddCounterString(IDictionary set, string data) { - if( set.TryGetValue( data, out var obj ) && obj is int counter ) - { - set[ data ] = counter + 1; - } + if (set.TryGetValue(data, out var obj) && obj is int counter) + set[data] = counter + 1; else - { - set[ data ] = 1; - } + set[data] = 1; } - private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info ) + private void IdentifyParsed(IDictionary set, GameObjectInfo info) { - switch( info.ObjectType ) + switch (info.ObjectType) { case ObjectType.Unknown: - switch( info.FileType ) + switch (info.FileType) { case FileType.Sound: - AddCounterString( set, FileType.Sound.ToString() ); + AddCounterString(set, FileType.Sound.ToString()); break; case FileType.Animation: case FileType.Pap: - AddCounterString( set, FileType.Animation.ToString() ); + AddCounterString(set, FileType.Animation.ToString()); break; case FileType.Shader: - AddCounterString( set, FileType.Shader.ToString() ); + AddCounterString(set, FileType.Shader.ToString()); break; } @@ -260,50 +353,50 @@ internal class ObjectIdentification : IObjectIdentifier case ObjectType.World: case ObjectType.Housing: case ObjectType.Font: - AddCounterString( set, info.ObjectType.ToString() ); + AddCounterString(set, info.ObjectType.ToString()); break; case ObjectType.DemiHuman: - set[ $"Demi Human: {info.PrimaryId}" ] = null; + set[$"Demi Human: {info.PrimaryId}"] = null; break; case ObjectType.Monster: - set[ $"Monster: {info.PrimaryId}" ] = null; + set[$"Monster: {info.PrimaryId}"] = null; break; case ObjectType.Icon: - set[ $"Icon: {info.IconId}" ] = null; + set[$"Icon: {info.IconId}"] = null; break; case ObjectType.Accessory: case ObjectType.Equipment: - FindEquipment( set, info ); + FindEquipment(set, info); break; case ObjectType.Weapon: - FindWeapon( set, info ); + FindWeapon(set, info); break; case ObjectType.Character: var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; - var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; - switch( info.CustomizationType ) + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; + switch (info.CustomizationType) { case CustomizationType.Skin: - set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; + set[$"Customization: {raceString}{genderString}Skin Textures"] = null; break; case CustomizationType.DecalFace: - set[ $"Customization: Face Decal {info.PrimaryId}" ] = null; + set[$"Customization: Face Decal {info.PrimaryId}"] = null; break; case CustomizationType.Iris when race == ModelRace.Unknown: - set[ $"Customization: All Eyes (Catchlight)" ] = null; + set[$"Customization: All Eyes (Catchlight)"] = null; break; case CustomizationType.DecalEquip: - set[ $"Equipment Decal {info.PrimaryId}" ] = null; + set[$"Equipment Decal {info.PrimaryId}"] = null; break; default: { var customizationString = race == ModelRace.Unknown - || info.BodySlot == BodySlot.Unknown - || info.CustomizationType == CustomizationType.Unknown + || info.BodySlot == BodySlot.Unknown + || info.CustomizationType == CustomizationType.Unknown ? "Customization: Unknown" : $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; - set[ customizationString ] = null; + set[customizationString] = null; break; } } @@ -314,58 +407,13 @@ internal class ObjectIdentification : IObjectIdentifier } } - private void IdentifyVfx( IDictionary< string, object? > set, GamePath path ) + private void IdentifyVfx(IDictionary set, string path) { - var key = GameData.GamePathParser.VfxToKey( path ); - if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) ) - { + var key = GamePathParser.VfxToKey(path); + if (key.Length == 0 || !_actions.TryGetValue(key, out var actions)) return; - } - foreach( var action in actions ) - { - set[ $"Action: {action.Name}" ] = action; - } + foreach (var action in actions) + set[$"Action: {action.Name}"] = action; } - - public void Identify( IDictionary< string, object? > set, GamePath path ) - { - if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) ) - { - IdentifyVfx( set, path ); - } - else - { - var info = GameData.GamePathParser.GetFileInfo( path ); - IdentifyParsed( set, info ); - } - } - - public Dictionary< string, object? > Identify( GamePath path ) - { - Dictionary< string, object? > ret = new(); - Identify( ret, path ); - return ret; - } - - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ) - { - switch( slot ) - { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - { - var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null; - } - default: - { - var (begin, _) = FindIndexRange( _equipment, - ( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null; - } - } - } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index 58a3b317..dc8801d8 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -1,6 +1,5 @@ using System; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs deleted file mode 100644 index 591b79fc..00000000 --- a/Penumbra.GameData/Util/GamePath.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using JsonSerializer = Newtonsoft.Json.JsonSerializer; - -namespace Penumbra.GameData.Util; - -public readonly struct GamePath : IComparable -{ - public const int MaxGamePathLength = 256; - - private readonly string _path; - - private GamePath( string path, bool _ ) - => _path = path; - - public GamePath( string? path ) - { - if( path is { Length: < MaxGamePathLength } ) - { - _path = Lower( Trim( ReplaceSlash( path ) ) ); - } - else - { - _path = string.Empty; - } - } - - public GamePath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : ""; - - private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength; - - private static string Substring( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '\\', '/' ); - - private static string Trim( string path ) - => path.TrimStart( '/' ); - - private static string Lower( string path ) - => path.ToLowerInvariant(); - - public static GamePath GenerateUnchecked( string path ) - => new(path, true); - - public static GamePath GenerateUncheckedLower( string path ) - => new(Lower( path ), true); - - public static implicit operator string( GamePath gamePath ) - => gamePath._path; - - public static explicit operator GamePath( string gamePath ) - => new(gamePath); - - public bool Empty - => _path.Length == 0; - - public string Filename() - { - var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); - return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path[ ( idx + 1 ).. ]; - } - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.Ordinal ), - GamePath path => string.Compare( _path, path._path, StringComparison.Ordinal ), - _ => -1, - }; - } - - public override string ToString() - => _path; -} - -public class GamePathConverter : JsonConverter -{ - public override bool CanConvert( Type objectType ) - => objectType == typeof( GamePath ); - - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) - { - var token = JToken.Load( reader ); - return token.ToObject< GamePath >(); - } - - public override bool CanWrite - => true; - - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) - { - if( value != null ) - { - var v = ( GamePath )value; - serializer.Serialize( writer, v.ToString() ); - } - } -} - diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index acf3b9b6..055d9f43 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using Penumbra.Api.Enums; -using Penumbra.GameData.Util; using Penumbra.String.Classes; namespace Penumbra.Collections; @@ -474,10 +473,10 @@ public partial class ModCollection _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. - var identifier = GameData.GameData.GetIdentifier(); + var identifier = Penumbra.Identifier; foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) { - foreach( var (name, obj) in identifier.Identify( new GamePath( resolved.ToString() ) ) ) + foreach( var (name, obj) in identifier.Identify( resolved.ToString() ) ) { if( !_changedItems.TryGetValue( name, out var data ) ) { diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs index 53c23496..3fe881bc 100644 --- a/Penumbra/Import/MetaFileInfo.cs +++ b/Penumbra/Import/MetaFileInfo.cs @@ -1,5 +1,4 @@ using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using System.Text.RegularExpressions; namespace Penumbra.Import; @@ -54,10 +53,6 @@ public class MetaFileInfo } public MetaFileInfo( string fileName ) - : this( new GamePath( fileName ) ) - { } - - public MetaFileInfo( GamePath fileName ) { // Set the primary type from the gamePath start. PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 5a4919bf..6f4b4842 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -1,7 +1,6 @@ using System; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using System.Collections.Generic; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 5290fb04..f3c54ca6 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 5d15031b..8134ed60 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Numerics; using Penumbra.GameData.Structs; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 5c76ff71..d0ab8510 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index dbe8a35b..f90ac48d 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,6 +1,5 @@ using System; using Dalamud.Memory; -using Penumbra.GameData.Util; using Penumbra.String.Functions; using CharacterUtility = Penumbra.Interop.CharacterUtility; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 9029800a..f388ea8e 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -2,7 +2,6 @@ using System; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Penumbra.GameData.Util; using Penumbra.Interop.Structs; using Penumbra.String.Functions; diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index 1057bf0d..549422a1 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Penumbra.GameData.Util; namespace Penumbra.Mods; @@ -11,11 +10,10 @@ public sealed partial class Mod private void ComputeChangedItems() { - var identifier = GameData.GameData.GetIdentifier(); ChangedItems.Clear(); foreach( var gamePath in AllRedirects ) { - identifier.Identify( ChangedItems, new GamePath(gamePath.ToString()) ); + Penumbra.Identifier.Identify( ChangedItems, gamePath.ToString() ); } // TODO: manipulations diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e0ed235b..0ede3d33 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -59,6 +59,7 @@ public class Penumbra : IDalamudPlugin public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; + public static IObjectIdentifier Identifier { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -80,8 +81,8 @@ public class Penumbra : IDalamudPlugin try { Dalamud.Initialize( pluginInterface ); - Log = new Logger(); - GameData.GameData.GetIdentifier( Dalamud.GameData ); + Log = new Logger(); + Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); From 8dab9407adc5a8fb9bff233d464584e7c638689c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 15:16:50 +0100 Subject: [PATCH 0559/2451] Fix bug in option dragging. --- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 6b09d7dc..d05d1ab2 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -626,7 +626,7 @@ public partial class ConfigWindow var sourceGroup = panel._mod.Groups[ sourceGroupIdx ]; var currentCount = group.Count; var option = sourceGroup[ sourceOption ]; - var priority = sourceGroup.OptionPriority( _dragDropGroupIdx ); + var priority = sourceGroup.OptionPriority( _dragDropOptionIdx ); panel._delayedActions.Enqueue( () => { Penumbra.ModManager.DeleteOption( panel._mod, sourceGroupIdx, sourceOption ); From 1353e591b89362338ea1c40268fa3b61ab138da9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 16:08:08 +0100 Subject: [PATCH 0560/2451] Use normalization before replacing symbols. --- Penumbra/Mods/Mod.Creation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index a566b795..5c7cd701 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -160,9 +160,8 @@ public partial class Mod { return replacement + replacement; } - StringBuilder sb = new(s.Length); - foreach( var c in s ) + foreach( var c in s.Normalize(NormalizationForm.FormKC) ) { if( c.IsInvalidAscii() || c.IsInvalidInPath() ) { From 878f69fd9117ff783a9ffb117302c0e40f2217da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 16:09:29 +0100 Subject: [PATCH 0561/2451] Actor Stuff. --- OtterGui | 2 +- Penumbra.GameData/Actors/ActorIdentifier.cs | 108 +++--- Penumbra.GameData/Actors/ActorManager.Data.cs | 124 ++++++ .../Actors/ActorManager.Identifiers.cs | 302 +++++++++++++++ Penumbra.GameData/Actors/ActorManager.cs | 352 ------------------ Penumbra.GameData/GameData.cs | 5 +- Penumbra.GameData/ObjectIdentification.cs | 74 ++-- Penumbra/Penumbra.cs | 5 +- 8 files changed, 513 insertions(+), 459 deletions(-) create mode 100644 Penumbra.GameData/Actors/ActorManager.Data.cs create mode 100644 Penumbra.GameData/Actors/ActorManager.Identifiers.cs delete mode 100644 Penumbra.GameData/Actors/ActorManager.cs diff --git a/OtterGui b/OtterGui index 0d2284a8..49f5aaa7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834 +Subproject commit 49f5aaa7733fc74d77435e9b84ce347eb06f61be diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 5651382e..7872bb65 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -6,8 +6,8 @@ using Penumbra.String; namespace Penumbra.GameData.Actors; -[StructLayout( LayoutKind.Explicit )] -public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > +[StructLayout(LayoutKind.Explicit)] +public readonly struct ActorIdentifier : IEquatable { public static ActorManager? Manager; @@ -26,36 +26,40 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public ActorIdentifier CreatePermanent() => new(Type, Kind, Index, DataId, PlayerName.Clone()); - public bool Equals( ActorIdentifier other ) + public bool Equals(ActorIdentifier other) { - if( Type != other.Type ) - { + if (Type != other.Type) return false; - } return Type switch { - IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ), - IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ) && Manager.DataIdEquals( this, other ), + IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), + IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, - IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals( this, other ), - _ => false, + IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals(this, other), + _ => false, }; } - public override bool Equals( object? obj ) - => obj is ActorIdentifier other && Equals( other ); + public override bool Equals(object? obj) + => obj is ActorIdentifier other && Equals(other); + + public static bool operator ==(ActorIdentifier lhs, ActorIdentifier rhs) + => lhs.Equals(rhs); + + public static bool operator !=(ActorIdentifier lhs, ActorIdentifier rhs) + => !lhs.Equals(rhs); public bool IsValid => Type != IdentifierType.Invalid; public override string ToString() - => Manager?.ToString( this ) + => Manager?.ToString(this) ?? Type switch { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", - IdentifierType.Special => ActorManager.ToName( Special ), + IdentifierType.Special => ActorManager.ToName(Special), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind} #{DataId}" @@ -66,18 +70,18 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public override int GetHashCode() => Type switch { - IdentifierType.Player => HashCode.Combine( IdentifierType.Player, PlayerName, HomeWorld ), - IdentifierType.Owned => HashCode.Combine( IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId ), - IdentifierType.Special => HashCode.Combine( IdentifierType.Special, Special ), - IdentifierType.Npc => HashCode.Combine( IdentifierType.Npc, Kind, Index, DataId ), + IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), + IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), + IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), + IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, Index, DataId), _ => 0, }; - internal ActorIdentifier( IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName ) + internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName) { Type = type; Kind = kind; - Special = ( SpecialActor )index; + Special = (SpecialActor)index; HomeWorld = Index = index; DataId = data; PlayerName = playerName; @@ -86,26 +90,26 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public JObject ToJson() { - var ret = new JObject { { nameof( Type ), Type.ToString() } }; - switch( Type ) + var ret = new JObject { { nameof(Type), Type.ToString() } }; + switch (Type) { case IdentifierType.Player: - ret.Add( nameof( PlayerName ), PlayerName.ToString() ); - ret.Add( nameof( HomeWorld ), HomeWorld ); + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(HomeWorld), HomeWorld); return ret; case IdentifierType.Owned: - ret.Add( nameof( PlayerName ), PlayerName.ToString() ); - ret.Add( nameof( HomeWorld ), HomeWorld ); - ret.Add( nameof( Kind ), Kind.ToString() ); - ret.Add( nameof( DataId ), DataId ); + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(HomeWorld), HomeWorld); + ret.Add(nameof(Kind), Kind.ToString()); + ret.Add(nameof(DataId), DataId); return ret; case IdentifierType.Special: - ret.Add( nameof( Special ), Special.ToString() ); + ret.Add(nameof(Special), Special.ToString()); return ret; case IdentifierType.Npc: - ret.Add( nameof( Kind ), Kind.ToString() ); - ret.Add( nameof( Index ), Index ); - ret.Add( nameof( DataId ), DataId ); + ret.Add(nameof(Kind), Kind.ToString()); + ret.Add(nameof(Index), Index); + ret.Add(nameof(DataId), DataId); return ret; } @@ -115,38 +119,32 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public static class ActorManagerExtensions { - public static bool DataIdEquals( this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs ) + public static bool DataIdEquals(this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs) { - if( lhs.Kind != rhs.Kind ) - { + if (lhs.Kind != rhs.Kind) return false; - } - if( lhs.DataId == rhs.DataId ) - { + if (lhs.DataId == rhs.DataId) return true; - } - if( manager == null ) - { + if (manager == null) return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; - } return lhs.Kind switch { - ObjectKind.MountType => manager.Mounts.TryGetValue( lhs.DataId, out var lhsName ) - && manager.Mounts.TryGetValue( rhs.DataId, out var rhsName ) - && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), - ObjectKind.Companion => manager.Companions.TryGetValue( lhs.DataId, out var lhsName ) - && manager.Companions.TryGetValue( rhs.DataId, out var rhsName ) - && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), - ObjectKind.BattleNpc => manager.BNpcs.TryGetValue( lhs.DataId, out var lhsName ) - && manager.BNpcs.TryGetValue( rhs.DataId, out var rhsName ) - && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), - ObjectKind.EventNpc => manager.ENpcs.TryGetValue( lhs.DataId, out var lhsName ) - && manager.ENpcs.TryGetValue( rhs.DataId, out var rhsName ) - && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.MountType => manager.Mounts.TryGetValue(lhs.DataId, out var lhsName) + && manager.Mounts.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), + ObjectKind.Companion => manager.Companions.TryGetValue(lhs.DataId, out var lhsName) + && manager.Companions.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), + ObjectKind.BattleNpc => manager.BNpcs.TryGetValue(lhs.DataId, out var lhsName) + && manager.BNpcs.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), + ObjectKind.EventNpc => manager.ENpcs.TryGetValue(lhs.DataId, out var lhsName) + && manager.ENpcs.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), _ => false, }; } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs new file mode 100644 index 00000000..9cf233c4 --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.GameData.Actors; + +public partial class ActorManager : IDisposable +{ + /// Worlds available for players. + public IReadOnlyDictionary Worlds { get; } + + /// Valid Mount names in title case by mount id. + public IReadOnlyDictionary Mounts { get; } + + /// Valid Companion names in title case by companion id. + public IReadOnlyDictionary Companions { get; } + + /// Valid BNPC names in title case by BNPC Name id. + public IReadOnlyDictionary BNpcs { get; } + + /// Valid ENPC names in title case by ENPC id. + public IReadOnlyDictionary ENpcs { get; } + + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, Func toParentIdx) + : this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx) + {} + + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + ClientLanguage language, Func toParentIdx) + { + _pluginInterface = pluginInterface; + _objects = objects; + _clientState = state; + _gameData = gameData; + _language = language; + _toParentIdx = toParentIdx; + + Worlds = TryCatchData("Worlds", CreateWorldData); + Mounts = TryCatchData("Mounts", CreateMountData); + Companions = TryCatchData("Companions", CreateCompanionData); + BNpcs = TryCatchData("BNpcs", CreateBNpcData); + ENpcs = TryCatchData("ENpcs", CreateENpcData); + + ActorIdentifier.Manager = this; + } + + public void Dispose() + { + if (_disposed) + return; + + GC.SuppressFinalize(this); + _pluginInterface.RelinquishData(GetVersionedTag("Worlds")); + _pluginInterface.RelinquishData(GetVersionedTag("Mounts")); + _pluginInterface.RelinquishData(GetVersionedTag("Companions")); + _pluginInterface.RelinquishData(GetVersionedTag("BNpcs")); + _pluginInterface.RelinquishData(GetVersionedTag("ENpcs")); + _disposed = true; + } + + ~ActorManager() + => Dispose(); + + private const int Version = 1; + + private readonly DalamudPluginInterface _pluginInterface; + private readonly ObjectTable _objects; + private readonly ClientState _clientState; + private readonly DataManager _gameData; + private readonly ClientLanguage _language; + private bool _disposed; + + private readonly Func _toParentIdx; + + private IReadOnlyDictionary CreateWorldData() + => _gameData.GetExcelSheet(_language)! + .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) + .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + + private IReadOnlyDictionary CreateMountData() + => _gameData.GetExcelSheet(_language)! + .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) + .ToDictionary(m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(m.Singular.ToDalamudString().ToString())); + + private IReadOnlyDictionary CreateCompanionData() + => _gameData.GetExcelSheet(_language)! + .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) + .ToDictionary(c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(c.Singular.ToDalamudString().ToString())); + + private IReadOnlyDictionary CreateBNpcData() + => _gameData.GetExcelSheet(_language)! + .Where(n => n.Singular.RawData.Length > 0) + .ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString())); + + private IReadOnlyDictionary CreateENpcData() + => _gameData.GetExcelSheet(_language)! + .Where(e => e.Singular.RawData.Length > 0) + .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + + private string GetVersionedTag(string tag) + => $"Penumbra.Actors.{tag}.{_language}.V{Version}"; + + private T TryCatchData(string tag, Func func) where T : class + { + try + { + return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); + return func(); + } + } +} diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs new file mode 100644 index 00000000..b8c9640b --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -0,0 +1,302 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Newtonsoft.Json.Linq; +using Penumbra.String; + +namespace Penumbra.GameData.Actors; + +public partial class ActorManager +{ + /// + /// Try to create an ActorIdentifier from a already parsed JObject . + /// + /// A parsed JObject + /// ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise. + public ActorIdentifier FromJson(JObject data) + { + var type = data[nameof(ActorIdentifier.Type)]?.Value() ?? IdentifierType.Invalid; + switch (type) + { + case IdentifierType.Player: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; + return CreatePlayer(name, homeWorld); + } + case IdentifierType.Owned: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; + var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + return CreateOwned(name, homeWorld, kind, dataId); + } + case IdentifierType.Special: + { + var special = data[nameof(ActorIdentifier.Special)]?.Value() ?? 0; + return CreateSpecial(special); + } + case IdentifierType.Npc: + { + var index = data[nameof(ActorIdentifier.Index)]?.Value() ?? ushort.MaxValue; + var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + return CreateNpc(kind, dataId, index); + } + case IdentifierType.Invalid: + default: + return ActorIdentifier.Invalid; + } + } + + /// + /// Use stored data to convert an ActorIdentifier to a string. + /// + public string ToString(ActorIdentifier id) + { + return id.Type switch + { + IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id + ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})" + : id.PlayerName.ToString(), + IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id + ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})'s {ToName(id.Kind, id.DataId)}" + : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", + IdentifierType.Special => ToName(id.Special), + IdentifierType.Npc => + id.Index == ushort.MaxValue + ? ToName(id.Kind, id.DataId) + : $"{ToName(id.Kind, id.DataId)} at {id.Index}", + _ => "Invalid", + }; + } + + + /// + /// Fixed names for special actors. + /// + public static string ToName(SpecialActor actor) + => actor switch + { + SpecialActor.CharacterScreen => "Character Screen Actor", + SpecialActor.ExamineScreen => "Examine Screen Actor", + SpecialActor.FittingRoom => "Fitting Room Actor", + SpecialActor.DyePreview => "Dye Preview Actor", + SpecialActor.Portrait => "Portrait Actor", + _ => "Invalid", + }; + + /// + /// Convert a given ID for a certain ObjectKind to a name. + /// + /// Invalid or a valid name. + public string ToName(ObjectKind kind, uint dataId) + => TryGetName(kind, dataId, out var ret) ? ret : "Invalid"; + + + /// + /// Convert a given ID for a certain ObjectKind to a name. + /// + public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name) + { + name = null; + return kind switch + { + ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), + ObjectKind.Companion => Companions.TryGetValue(dataId, out name), + ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), + ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), + _ => false, + }; + } + + /// + /// Compute an ActorIdentifier from a GameObject. + /// + public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor) + { + if (actor == null) + return ActorIdentifier.Invalid; + + var idx = actor->ObjectIndex; + if (idx is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd) + { + var parentIdx = _toParentIdx(idx); + if (parentIdx >= 0) + return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx)); + } + else if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) + { + return CreateSpecial((SpecialActor)idx); + } + + switch ((ObjectKind)actor->ObjectKind) + { + case ObjectKind.Player: + { + var name = new ByteString(actor->Name); + var homeWorld = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->HomeWorld; + return CreatePlayer(name, homeWorld); + } + case ObjectKind.BattleNpc: + { + var ownerId = actor->OwnerID; + if (ownerId != 0xE0000000) + { + var owner = + (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero); + if (owner == null) + return ActorIdentifier.Invalid; + + var name = new ByteString(owner->GameObject.Name); + var homeWorld = owner->HomeWorld; + return CreateOwned(name, homeWorld, ObjectKind.BattleNpc, + ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); + } + + return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, actor->ObjectIndex); + } + case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); + case ObjectKind.MountType: + case ObjectKind.Companion: + { + if (actor->ObjectIndex % 2 == 0) + return ActorIdentifier.Invalid; + + var owner = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_objects.GetObjectAddress(actor->ObjectIndex - 1); + if (owner == null) + return ActorIdentifier.Invalid; + + var dataId = GetCompanionId(actor, &owner->GameObject); + return CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId); + } + default: return ActorIdentifier.Invalid; + } + } + + /// + /// Obtain the current companion ID for an object by its actor and owner. + /// + private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, + FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) + { + return (ObjectKind)actor->ObjectKind switch + { + ObjectKind.MountType => *(ushort*)((byte*)owner + 0x668), + ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC), + _ => actor->DataID, + }; + } + + public unsafe ActorIdentifier FromObject(GameObject? actor) + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero)); + + public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) + { + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); + } + + public ActorIdentifier CreateSpecial(SpecialActor actor) + { + if (!VerifySpecial(actor)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Special, ObjectKind.Player, (ushort)actor, 0, ByteString.Empty); + } + + public ActorIdentifier CreateNpc(ObjectKind kind, uint data, ushort index = ushort.MaxValue) + { + if (!VerifyIndex(index) || !VerifyNpcData(kind, data)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Npc, kind, index, data, ByteString.Empty); + } + + public ActorIdentifier CreateOwned(ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId) + { + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName) || !VerifyOwnedData(kind, dataId)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName); + } + + /// Checks SE naming rules. + private static bool VerifyPlayerName(ByteString name) + { + // Total no more than 20 characters + space. + if (name.Length is < 5 or > 21) + return false; + + var split = name.Split((byte)' '); + + // Forename and surname, no more spaces. + if (split.Count != 2) + return false; + + static bool CheckNamePart(ByteString part) + { + // Each name part at least 2 and at most 15 characters. + if (part.Length is < 2 or > 15) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < (byte)'A' or > (byte)'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + if (part.Skip(1).Any(c => c != (byte)'\'' && c != (byte)'-' && c is < (byte)'a' or > (byte)'z')) + return false; + + var hyphens = part.Split((byte)'-'); + // Apostrophes can not be used in succession, after or before apostrophes. + return !hyphens.Any(p => p.Length == 0 || p[0] == (byte)'\'' || p.Last() == (byte)'\''); + } + + return CheckNamePart(split[0]) && CheckNamePart(split[1]); + } + + /// Checks if the world is a valid public world or ushort.MaxValue (any world). + private bool VerifyWorld(ushort worldId) + => Worlds.ContainsKey(worldId); + + /// Verify that the enum value is a specific actor and return the name if it is. + private static bool VerifySpecial(SpecialActor actor) + => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; + + /// Verify that the object index is a valid index for an NPC. + private static bool VerifyIndex(ushort index) + { + return index switch + { + < 200 => index % 2 == 0, + > (ushort)SpecialActor.Portrait => index < 426, + _ => false, + }; + } + + /// Verify that the object kind is a valid owned object, and the corresponding data Id. + private bool VerifyOwnedData(ObjectKind kind, uint dataId) + { + return kind switch + { + ObjectKind.MountType => Mounts.ContainsKey(dataId), + ObjectKind.Companion => Companions.ContainsKey(dataId), + ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), + _ => false, + }; + } + + private bool VerifyNpcData(ObjectKind kind, uint dataId) + => kind switch + { + ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), + ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), + _ => false, + }; +} diff --git a/Penumbra.GameData/Actors/ActorManager.cs b/Penumbra.GameData/Actors/ActorManager.cs deleted file mode 100644 index f3a1936e..00000000 --- a/Penumbra.GameData/Actors/ActorManager.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using Dalamud.Data; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json.Linq; -using Penumbra.String; - -namespace Penumbra.GameData.Actors; - -public class ActorManager -{ - private readonly ObjectTable _objects; - private readonly ClientState _clientState; - - public readonly IReadOnlyDictionary< ushort, string > Worlds; - public readonly IReadOnlyDictionary< uint, string > Mounts; - public readonly IReadOnlyDictionary< uint, string > Companions; - public readonly IReadOnlyDictionary< uint, string > BNpcs; - public readonly IReadOnlyDictionary< uint, string > ENpcs; - - public IEnumerable< KeyValuePair< ushort, string > > AllWorlds - => Worlds.OrderBy( kvp => kvp.Key ).Prepend( new KeyValuePair< ushort, string >( ushort.MaxValue, "Any World" ) ); - - private readonly Func< ushort, short > _toParentIdx; - - public ActorManager( ObjectTable objects, ClientState state, DataManager gameData, Func< ushort, short > toParentIdx ) - { - _objects = objects; - _clientState = state; - Worlds = gameData.GetExcelSheet< World >()! - .Where( w => w.IsPublic && !w.Name.RawData.IsEmpty ) - .ToDictionary( w => ( ushort )w.RowId, w => w.Name.ToString() ); - - Mounts = gameData.GetExcelSheet< Mount >()! - .Where( m => m.Singular.RawData.Length > 0 && m.Order >= 0 ) - .ToDictionary( m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( m.Singular.ToDalamudString().ToString() ) ); - Companions = gameData.GetExcelSheet< Companion >()! - .Where( c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue ) - .ToDictionary( c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( c.Singular.ToDalamudString().ToString() ) ); - - BNpcs = gameData.GetExcelSheet< BNpcName >()! - .Where( n => n.Singular.RawData.Length > 0 ) - .ToDictionary( n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( n.Singular.ToDalamudString().ToString() ) ); - - ENpcs = gameData.GetExcelSheet< ENpcResident >()! - .Where( e => e.Singular.RawData.Length > 0 ) - .ToDictionary( e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( e.Singular.ToDalamudString().ToString() ) ); - - _toParentIdx = toParentIdx; - - ActorIdentifier.Manager = this; - } - - public ActorIdentifier FromJson( JObject data ) - { - var type = data[ nameof( ActorIdentifier.Type ) ]?.Value< IdentifierType >() ?? IdentifierType.Invalid; - switch( type ) - { - case IdentifierType.Player: - { - var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); - var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; - return CreatePlayer( name, homeWorld ); - } - case IdentifierType.Owned: - { - var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); - var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; - var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; - var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; - return CreateOwned( name, homeWorld, kind, dataId ); - } - case IdentifierType.Special: - { - var special = data[ nameof( ActorIdentifier.Special ) ]?.Value< SpecialActor >() ?? 0; - return CreateSpecial( special ); - } - case IdentifierType.Npc: - { - var index = data[ nameof( ActorIdentifier.Index ) ]?.Value< ushort >() ?? 0; - var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; - var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; - return CreateNpc( kind, index, dataId ); - } - case IdentifierType.Invalid: - default: - return ActorIdentifier.Invalid; - } - } - - public string ToString( ActorIdentifier id ) - { - return id.Type switch - { - IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})" - : id.PlayerName.ToString(), - IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})'s {ToName( id.Kind, id.DataId )}" - : $"{id.PlayerName}s {ToName( id.Kind, id.DataId )}", - IdentifierType.Special => ToName( id.Special ), - IdentifierType.Npc => - id.Index == ushort.MaxValue - ? ToName( id.Kind, id.DataId ) - : $"{ToName( id.Kind, id.DataId )} at {id.Index}", - _ => "Invalid", - }; - } - - public static string ToName( SpecialActor actor ) - => actor switch - { - SpecialActor.CharacterScreen => "Character Screen Actor", - SpecialActor.ExamineScreen => "Examine Screen Actor", - SpecialActor.FittingRoom => "Fitting Room Actor", - SpecialActor.DyePreview => "Dye Preview Actor", - SpecialActor.Portrait => "Portrait Actor", - _ => "Invalid", - }; - - public string ToName( ObjectKind kind, uint dataId ) - => TryGetName( kind, dataId, out var ret ) ? ret : "Invalid"; - - public bool TryGetName( ObjectKind kind, uint dataId, [NotNullWhen( true )] out string? name ) - { - name = null; - return kind switch - { - ObjectKind.MountType => Mounts.TryGetValue( dataId, out name ), - ObjectKind.Companion => Companions.TryGetValue( dataId, out name ), - ObjectKind.BattleNpc => BNpcs.TryGetValue( dataId, out name ), - ObjectKind.EventNpc => ENpcs.TryGetValue( dataId, out name ), - _ => false, - }; - } - - public unsafe ActorIdentifier FromObject( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor ) - { - if( actor == null ) - { - return ActorIdentifier.Invalid; - } - - var idx = actor->ObjectIndex; - if( idx is >= ( ushort )SpecialActor.CutsceneStart and < ( ushort )SpecialActor.CutsceneEnd ) - { - var parentIdx = _toParentIdx( idx ); - if( parentIdx >= 0 ) - { - return FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )_objects.GetObjectAddress( parentIdx ) ); - } - } - else if( idx is >= ( ushort )SpecialActor.CharacterScreen and <= ( ushort )SpecialActor.Portrait ) - { - return CreateSpecial( ( SpecialActor )idx ); - } - - switch( ( ObjectKind )actor->ObjectKind ) - { - case ObjectKind.Player: - { - var name = new ByteString( actor->Name ); - var homeWorld = ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->HomeWorld; - return CreatePlayer( name, homeWorld ); - } - case ObjectKind.BattleNpc: - { - var ownerId = actor->OwnerID; - if( ownerId != 0xE0000000 ) - { - var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )( _objects.SearchById( ownerId )?.Address ?? IntPtr.Zero ); - if( owner == null ) - { - return ActorIdentifier.Invalid; - } - - var name = new ByteString( owner->GameObject.Name ); - var homeWorld = owner->HomeWorld; - return CreateOwned( name, homeWorld, ObjectKind.BattleNpc, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); - } - - return CreateNpc( ObjectKind.BattleNpc, actor->ObjectIndex, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); - } - case ObjectKind.EventNpc: return CreateNpc( ObjectKind.EventNpc, actor->ObjectIndex, actor->DataID ); - case ObjectKind.MountType: - case ObjectKind.Companion: - { - if( actor->ObjectIndex % 2 == 0 ) - { - return ActorIdentifier.Invalid; - } - - var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )_objects.GetObjectAddress( actor->ObjectIndex - 1 ); - if( owner == null ) - { - return ActorIdentifier.Invalid; - } - - var dataId = GetCompanionId( actor, &owner->GameObject ); - return CreateOwned( new ByteString( owner->GameObject.Name ), owner->HomeWorld, ( ObjectKind )actor->ObjectKind, dataId ); - } - default: return ActorIdentifier.Invalid; - } - } - - private unsafe uint GetCompanionId( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner ) - { - return ( ObjectKind )actor->ObjectKind switch - { - ObjectKind.MountType => *( ushort* )( ( byte* )owner + 0x668 ), - ObjectKind.Companion => *( ushort* )( ( byte* )actor + 0x1AAC ), - _ => actor->DataID, - }; - } - - public unsafe ActorIdentifier FromObject( GameObject? actor ) - => FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )( actor?.Address ?? IntPtr.Zero ) ); - - - public ActorIdentifier CreatePlayer( ByteString name, ushort homeWorld ) - { - if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( name ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name ); - } - - public ActorIdentifier CreateSpecial( SpecialActor actor ) - { - if( !VerifySpecial( actor ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Special, ObjectKind.Player, ( ushort )actor, 0, ByteString.Empty ); - } - - public ActorIdentifier CreateNpc( ObjectKind kind, ushort index = ushort.MaxValue, uint data = uint.MaxValue ) - { - if( !VerifyIndex( index ) || !VerifyNpcData( kind, data ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Npc, kind, index, data, ByteString.Empty ); - } - - public ActorIdentifier CreateOwned( ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId ) - { - if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( ownerName ) || !VerifyOwnedData( kind, dataId ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Owned, kind, homeWorld, dataId, ownerName ); - } - - - /// Checks SE naming rules. - private static bool VerifyPlayerName( ByteString name ) - { - // Total no more than 20 characters + space. - if( name.Length is < 5 or > 21 ) - { - return false; - } - - var split = name.Split( ( byte )' ' ); - - // Forename and surname, no more spaces. - if( split.Count != 2 ) - { - return false; - } - - static bool CheckNamePart( ByteString part ) - { - // Each name part at least 2 and at most 15 characters. - if( part.Length is < 2 or > 15 ) - { - return false; - } - - // Each part starting with capitalized letter. - if( part[ 0 ] is < ( byte )'A' or > ( byte )'Z' ) - { - return false; - } - - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - if( part.Skip( 1 ).Any( c => c != ( byte )'\'' && c != ( byte )'-' && c is < ( byte )'a' or > ( byte )'z' ) ) - { - return false; - } - - var hyphens = part.Split( ( byte )'-' ); - // Apostrophes can not be used in succession, after or before apostrophes. - return !hyphens.Any( p => p.Length == 0 || p[ 0 ] == ( byte )'\'' || p.Last() == ( byte )'\'' ); - } - - return CheckNamePart( split[ 0 ] ) && CheckNamePart( split[ 1 ] ); - } - - /// Checks if the world is a valid public world or ushort.MaxValue (any world). - private bool VerifyWorld( ushort worldId ) - => Worlds.ContainsKey( worldId ); - - /// Verify that the enum value is a specific actor and return the name if it is. - private static bool VerifySpecial( SpecialActor actor ) - => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; - - /// Verify that the object index is a valid index for an NPC. - private static bool VerifyIndex( ushort index ) - { - return index switch - { - < 200 => index % 2 == 0, - > ( ushort )SpecialActor.Portrait => index < 426, - _ => false, - }; - } - - /// Verify that the object kind is a valid owned object, and the corresponding data Id. - private bool VerifyOwnedData( ObjectKind kind, uint dataId ) - { - return kind switch - { - ObjectKind.MountType => Mounts.ContainsKey( dataId ), - ObjectKind.Companion => Companions.ContainsKey( dataId ), - ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), - _ => false, - }; - } - - private bool VerifyNpcData( ObjectKind kind, uint dataId ) - => kind switch - { - ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), - ObjectKind.EventNpc => ENpcs.ContainsKey( dataId ), - _ => false, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 7eace70e..639d13d1 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -57,11 +57,10 @@ public interface IObjectIdentifier : IDisposable /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. /// The variant ID of the model. /// The equipment slot the piece of equipment uses. - /// - public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); /// - public IReadOnlyList? Identify(SetId setId, ushort variant, EquipSlot slot) + public IReadOnlyList Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); } diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 20642156..6d1e8595 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -16,7 +16,7 @@ namespace Penumbra.GameData; internal class ObjectIdentification : IObjectIdentifier { - public IGamePathParser GamePathParser { get; } = new GamePathParser(); + public IGamePathParser GamePathParser { get; } = new GamePathParser(); public void Identify(IDictionary set, string path) { @@ -38,7 +38,7 @@ internal class ObjectIdentification : IObjectIdentifier return ret; } - public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) { switch (slot) { @@ -55,7 +55,7 @@ internal class ObjectIdentification : IObjectIdentifier var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, ((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant, 0xFFFFFFFFFFFF); - return begin >= 0 ? _equipment[begin].Item2 : null; + return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); } } } @@ -68,12 +68,8 @@ internal class ObjectIdentification : IObjectIdentifier private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; - private readonly IReadOnlyDictionary> _actions; - - private readonly string _weaponsTag; - private readonly string _equipmentTag; - private readonly string _actionsTag; - private bool _disposed = false; + private readonly IReadOnlyDictionary> _actions; + private bool _disposed = false; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) { @@ -81,39 +77,9 @@ internal class ObjectIdentification : IObjectIdentifier _dataManager = dataManager; _language = language; - _weaponsTag = $"Penumbra.Identification.Weapons.{_language}.V{Version}"; - _equipmentTag = $"Penumbra.Identification.Equipment.{_language}.V{Version}"; - _actionsTag = $"Penumbra.Identification.Actions.{_language}.V{Version}"; - - try - { - _weapons = pluginInterface.GetOrCreateData(_weaponsTag, CreateWeaponList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for weapons:\n{ex}"); - _weapons = CreateWeaponList(); - } - - try - { - _equipment = pluginInterface.GetOrCreateData(_equipmentTag, CreateEquipmentList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for equipment:\n{ex}"); - _equipment = CreateEquipmentList(); - } - - try - { - _actions = pluginInterface.GetOrCreateData(_actionsTag, CreateActionList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for actions:\n{ex}"); - _actions = CreateActionList(); - } + _weapons = TryCatchData("Weapons", CreateWeaponList); + _equipment = TryCatchData("Equipment", CreateEquipmentList); + _actions = TryCatchData("Actions", CreateActionList); } public void Dispose() @@ -122,15 +88,31 @@ internal class ObjectIdentification : IObjectIdentifier return; GC.SuppressFinalize(this); - _pluginInterface.RelinquishData(_weaponsTag); - _pluginInterface.RelinquishData(_equipmentTag); - _pluginInterface.RelinquishData(_actionsTag); + _pluginInterface.RelinquishData(GetVersionedTag("Weapons")); + _pluginInterface.RelinquishData(GetVersionedTag("Equipment")); + _pluginInterface.RelinquishData(GetVersionedTag("Actions")); _disposed = true; } ~ObjectIdentification() => Dispose(); + private string GetVersionedTag(string tag) + => $"Penumbra.Identification.{tag}.{_language}.V{Version}"; + + private T TryCatchData(string tag, Func func) where T : class + { + try + { + return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared identification data for {tag}:\n{ex}"); + return func(); + } + } + private static bool Add(IDictionary> dict, ulong key, Item item) { if (dict.TryGetValue(key, out var list)) @@ -181,7 +163,7 @@ internal class ObjectIdentification : IObjectIdentifier var storage = new SortedList>(); foreach (var item in items) { - switch (((EquipSlot)item.EquipSlotCategory.Row).ToSlot()) + switch ((EquipSlot)item.EquipSlotCategory.Row) { // Accessories case EquipSlot.RFinger: diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0ede3d33..3633adbd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -104,7 +104,7 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Actors = new ActorManager( Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); + Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -290,7 +290,8 @@ public class Penumbra : IDalamudPlugin public void Dispose() { - Dalamud.PluginInterface.RelinquishData( "test1" ); + Actors?.Dispose(); + Identifier?.Dispose(); Framework?.Dispose(); ShutdownWebServer(); DisposeInterface(); From 68a725d51d7048f989650b71d318e14bab5494af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 16:10:29 +0100 Subject: [PATCH 0562/2451] Further Identification stuff. --- OtterGui | 2 +- .../Actors/ActorManager.Identifiers.cs | 10 +++++ Penumbra.GameData/ObjectIdentification.cs | 37 +++++++++++++++++-- .../Collections/CollectionManager.Active.cs | 25 +++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index 49f5aaa7..3a9cafae 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 49f5aaa7733fc74d77435e9b84ce347eb06f61be +Subproject commit 3a9cafaeb21e156d1bb96e94125205fcdb331189 diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index b8c9640b..4caba6e5 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -194,6 +194,16 @@ public partial class ActorManager public unsafe ActorIdentifier FromObject(GameObject? actor) => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero)); + public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) + => type switch + { + IdentifierType.Player => CreatePlayer(name, homeWorld), + IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), + IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), + IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), + _ => ActorIdentifier.Invalid, + }; + public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) { if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name)) diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 6d1e8595..d958f62c 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -7,6 +7,7 @@ using Penumbra.GameData.Structs; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; @@ -66,10 +67,11 @@ internal class ObjectIdentification : IObjectIdentifier private readonly DalamudPluginInterface _pluginInterface; private readonly ClientLanguage _language; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; - private readonly IReadOnlyDictionary> _actions; - private bool _disposed = false; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models; + private readonly IReadOnlyDictionary> _actions; + private bool _disposed = false; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) { @@ -80,6 +82,7 @@ internal class ObjectIdentification : IObjectIdentifier _weapons = TryCatchData("Weapons", CreateWeaponList); _equipment = TryCatchData("Equipment", CreateEquipmentList); _actions = TryCatchData("Actions", CreateActionList); + _models = TryCatchData("Models", CreateModelList); } public void Dispose() @@ -91,6 +94,7 @@ internal class ObjectIdentification : IObjectIdentifier _pluginInterface.RelinquishData(GetVersionedTag("Weapons")); _pluginInterface.RelinquishData(GetVersionedTag("Equipment")); _pluginInterface.RelinquishData(GetVersionedTag("Actions")); + _pluginInterface.RelinquishData(GetVersionedTag("Models")); _disposed = true; } @@ -221,6 +225,31 @@ internal class ObjectIdentification : IObjectIdentifier return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } + private static ulong ModelValue(ModelChara row) + => row.Type | ((ulong) row.Model << 8) | ((ulong) row.Base << 24) | ((ulong) row.Variant << 32); + + private static IEnumerable<(ulong, ObjectKind, uint)> BattleNpcToName(ulong model, uint bNpc) + => Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1); + + private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList() + { + var sheetBNpc = _dataManager.GetExcelSheet(_language)!; + var sheetENpc = _dataManager.GetExcelSheet(_language)!; + var sheetCompanion = _dataManager.GetExcelSheet(_language)!; + var sheetMount = _dataManager.GetExcelSheet(_language)!; + var sheetModel = _dataManager.GetExcelSheet(_language)!; + + var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue); + + return sheetENpc.Select(e => (modelCharaToModel[e.ModelChara.Row], ObjectKind.EventNpc, e.RowId)) + .Concat(sheetCompanion.Select(c => (modelCharaToModel[c.Model.Row], ObjectKind.Companion, c.RowId))) + .Concat(sheetMount.Select(c => (modelCharaToModel[c.ModelChara.Row], ObjectKind.MountType, c.RowId))) + .Concat(sheetBNpc.SelectMany(c => BattleNpcToName(modelCharaToModel[c.ModelChara.Row], c.RowId))) + .GroupBy(t => t.Item1) + .Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray())) + .ToArray(); + } + private class Comparer : IComparer<(ulong, IReadOnlyList)> { public int Compare((ulong, IReadOnlyList) x, (ulong, IReadOnlyList) y) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 1e211076..624be9cc 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,9 +8,34 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Dalamud.Game.ClientState.Objects.Enums; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; +public class IndividualCollections +{ + private readonly ActorManager _manager; + public readonly List< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > Assignments; + public readonly Dictionary< ActorIdentifier, ModCollection > Individuals; + + public IndividualCollections( ActorManager manager ) + { + _manager = manager; + } + + public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, uint dataId ) + { + _manager. + } + + public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IReadOnlyList< uint > dataIds ) + { + + } + +} + public partial class ModCollection { public sealed partial class Manager From cbdac759b3bf1fae7a9cb5b120576f56b4e634ac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 17:09:01 +0100 Subject: [PATCH 0563/2451] Some small fixes. --- Penumbra/Import/MetaFileInfo.cs | 2 +- Penumbra/Import/TexToolsStructs.cs | 1 - Penumbra/Import/Textures/CombinedTexture.cs | 4 ++-- Penumbra/Penumbra.cs | 2 ++ 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs index 3fe881bc..11a6664b 100644 --- a/Penumbra/Import/MetaFileInfo.cs +++ b/Penumbra/Import/MetaFileInfo.cs @@ -55,7 +55,7 @@ public class MetaFileInfo public MetaFileInfo( string fileName ) { // Set the primary type from the gamePath start. - PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName ); + PrimaryType = Penumbra.GamePathParser.PathToObjectType( fileName ); PrimaryId = 0; SecondaryType = BodySlot.Unknown; SecondaryId = 0; diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/TexToolsStructs.cs index bc85893c..1e92d1a4 100644 --- a/Penumbra/Import/TexToolsStructs.cs +++ b/Penumbra/Import/TexToolsStructs.cs @@ -1,6 +1,5 @@ using System; using Penumbra.Api.Enums; -using Penumbra.Mods; namespace Penumbra.Import; diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index ecbf9caa..c3065186 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -16,7 +16,7 @@ public partial class CombinedTexture : IDisposable { AsIs, Bitmap, - BC5, + BC3, BC7, } @@ -93,7 +93,7 @@ public partial class CombinedTexture : IDisposable { TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s, TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), - TextureSaveType.BC5 => CreateCompressed( s, mipMaps, false ), + TextureSaveType.BC3 => CreateCompressed( s, mipMaps, false ), TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3633adbd..f54b189f 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -60,6 +60,7 @@ public class Penumbra : IDalamudPlugin public static FrameworkManager Framework { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!; + public static IGamePathParser GamePathParser { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -83,6 +84,7 @@ public class Penumbra : IDalamudPlugin Dalamud.Initialize( pluginInterface ); Log = new Logger(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); + GamePathParser = GameData.GameData.GetGamePathParser(); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); From 732ca561a191b6bb1619123ea07d90a11aca6f4b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Nov 2022 22:41:50 +0100 Subject: [PATCH 0564/2451] Some stuff --- Penumbra.Api | 2 +- .../Actors/ActorManager.Identifiers.cs | 96 +++++++++++++++---- Penumbra.GameData/ObjectIdentification.cs | 2 +- Penumbra.String | 2 +- .../Collections/CollectionManager.Active.cs | 27 ++++-- 5 files changed, 101 insertions(+), 28 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index f41af0fb..27e8873e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f41af0fb88626f1579d3c4370b32b901f3c4d3c2 +Subproject commit 27e8873e9f4633421e736757574b502a8d65e79d diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 4caba6e5..c504470e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -1,8 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.Serialization; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; +using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json.Linq; using Penumbra.String; @@ -157,7 +159,8 @@ public partial class ActorManager ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); } - return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, actor->ObjectIndex); + return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, + actor->ObjectIndex); } case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); case ObjectKind.MountType: @@ -206,7 +209,7 @@ public partial class ActorManager public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) { - if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name)) + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name.Span)) return ActorIdentifier.Invalid; return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); @@ -230,26 +233,25 @@ public partial class ActorManager public ActorIdentifier CreateOwned(ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId) { - if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName) || !VerifyOwnedData(kind, dataId)) + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName.Span) || !VerifyOwnedData(kind, dataId)) return ActorIdentifier.Invalid; return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName); } /// Checks SE naming rules. - private static bool VerifyPlayerName(ByteString name) + public static bool VerifyPlayerName(ReadOnlySpan name) { // Total no more than 20 characters + space. if (name.Length is < 5 or > 21) return false; - var split = name.Split((byte)' '); - // Forename and surname, no more spaces. - if (split.Count != 2) + var splitIndex = name.IndexOf((byte)' '); + if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0) return false; - static bool CheckNamePart(ByteString part) + static bool CheckNamePart(ReadOnlySpan part) { // Each name part at least 2 and at most 15 characters. if (part.Length is < 2 or > 15) @@ -260,27 +262,83 @@ public partial class ActorManager return false; // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - if (part.Skip(1).Any(c => c != (byte)'\'' && c != (byte)'-' && c is < (byte)'a' or > (byte)'z')) - return false; + var last = (byte)'\0'; + for (var i = 1; i < part.Length; ++i) + { + var current = part[i]; + if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) + return false; - var hyphens = part.Split((byte)'-'); - // Apostrophes can not be used in succession, after or before apostrophes. - return !hyphens.Any(p => p.Length == 0 || p[0] == (byte)'\'' || p.Last() == (byte)'\''); + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is (byte)'\'' && current is (byte)'-') + return false; + if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') + return false; + + last = current; + } + + return part[^1] != (byte)'-'; } - return CheckNamePart(split[0]) && CheckNamePart(split[1]); + return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); + } + + /// Checks SE naming rules. + public static bool VerifyPlayerName(ReadOnlySpan name) + { + // Total no more than 20 characters + space. + if (name.Length is < 5 or > 21) + return false; + + // Forename and surname, no more spaces. + var splitIndex = name.IndexOf(' '); + if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0) + return false; + + static bool CheckNamePart(ReadOnlySpan part) + { + // Each name part at least 2 and at most 15 characters. + if (part.Length is < 2 or > 15) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < 'A' or > 'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + var last = '\0'; + for (var i = 1; i < part.Length; ++i) + { + var current = part[i]; + if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) + return false; + + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is '\'' && current is '-') + return false; + if (last is '-' && current is '-' or '\'') + return false; + + last = current; + } + + return part[^1] != '-'; + } + + return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); } /// Checks if the world is a valid public world or ushort.MaxValue (any world). - private bool VerifyWorld(ushort worldId) + public bool VerifyWorld(ushort worldId) => Worlds.ContainsKey(worldId); /// Verify that the enum value is a specific actor and return the name if it is. - private static bool VerifySpecial(SpecialActor actor) + public static bool VerifySpecial(SpecialActor actor) => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; /// Verify that the object index is a valid index for an NPC. - private static bool VerifyIndex(ushort index) + public static bool VerifyIndex(ushort index) { return index switch { @@ -291,7 +349,7 @@ public partial class ActorManager } /// Verify that the object kind is a valid owned object, and the corresponding data Id. - private bool VerifyOwnedData(ObjectKind kind, uint dataId) + public bool VerifyOwnedData(ObjectKind kind, uint dataId) { return kind switch { @@ -302,7 +360,7 @@ public partial class ActorManager }; } - private bool VerifyNpcData(ObjectKind kind, uint dataId) + public bool VerifyNpcData(ObjectKind kind, uint dataId) => kind switch { ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index d958f62c..ddffe9e2 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -49,7 +49,7 @@ internal class ObjectIdentification : IObjectIdentifier var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, ((ulong)setId << 32) | ((ulong)weaponType << 16) | variant, 0xFFFFFFFFFFFF); - return begin >= 0 ? _weapons[begin].Item2 : null; + return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); } default: { diff --git a/Penumbra.String b/Penumbra.String index 99f3b4f3..81539a96 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 99f3b4f3c7fd9f83b0741089e8808566a0bbdde5 +Subproject commit 81539a968f6bfbb78c34ae1094cce88ae4c9ac88 diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 624be9cc..0e6e1730 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.Enums; using Penumbra.GameData.Actors; +using Penumbra.String; namespace Penumbra.Collections; @@ -20,20 +21,34 @@ public class IndividualCollections public readonly Dictionary< ActorIdentifier, ModCollection > Individuals; public IndividualCollections( ActorManager manager ) - { - _manager = manager; - } + => _manager = manager; public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, uint dataId ) { - _manager. + return false; } - public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IReadOnlyList< uint > dataIds ) + public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection ) + => Add( displayName, identifier, collection, Array.Empty< uint >() ); + + public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection, IEnumerable< uint > additionalIds ) { + if( Individuals.ContainsKey( identifier ) ) + { + return false; + } + //var identifiers = additionalIds + // .Select( id => CanAdd( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, id, out var value ) ? value : ActorIdentifier.Invalid ) + // .Prepend( identifier ) + // .ToArray(); + //if( identifiers.Any( i => !i.IsValid || i.DataId == identifier.DataId ) ) + //{ + // return false; + //} + + return true; } - } public partial class ModCollection From 7e167cf0cf0eb06f27167090dadc1c28b87a1058 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Nov 2022 13:30:39 +0100 Subject: [PATCH 0565/2451] Add export options to mdl and material editing. --- OtterGui | 2 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 91 +++++++++++++++---- Penumbra/UI/Classes/ModEditWindow.cs | 12 ++- 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/OtterGui b/OtterGui index 3a9cafae..87debfd2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3a9cafaeb21e156d1bb96e94125205fcdb331189 +Subproject commit 87debfd2eceaee8a93fecc0565c454372bcef1f3 diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index d8c13c81..ed9ddd10 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -27,24 +28,30 @@ public partial class ModEditWindow private readonly string _fileType; private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; private readonly Func< T, bool, bool > _drawEdit; + private readonly Func< string > _getInitialPath; private Mod.Editor.FileRegistry? _currentPath; private T? _currentFile; + private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; + private string _defaultPath = string.Empty; + private bool _inInput; + private T? _defaultFile; + private Exception? _defaultException; private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); + public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit ) + Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) { - _tabName = tabName; - _fileType = fileType; - _getFiles = getFiles; - _drawEdit = drawEdit; + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + _getInitialPath = getInitialPath; } public void Draw() @@ -75,31 +82,64 @@ public partial class ModEditWindow private void DefaultInput() { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); _inInput = ImGui.IsItemActive(); if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) { + _fileDialog.Reset(); try { var file = Dalamud.GameData.GetFile( _defaultPath ); if( file != null ) { - _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + _defaultException = null; + _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + } + else + { + _defaultFile = null; + _defaultException = new Exception( "File does not exist." ); } } - catch + catch( Exception e ) { - _defaultFile = null; + _defaultFile = null; + _defaultException = e; } } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) + { + _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); + } + }, _getInitialPath() ); + } + + _fileDialog.Draw(); } public void Reset() { - _currentPath = null; - _currentFile = null; - _changed = false; + _currentException = null; + _currentPath = null; + _currentFile = null; + _changed = false; } private void DrawFileSelectCombo() @@ -127,8 +167,9 @@ public partial class ModEditWindow return; } - _changed = false; - _currentPath = path; + _changed = false; + _currentPath = path; + _currentException = null; try { var bytes = File.ReadAllBytes( _currentPath.File.FullName ); @@ -136,8 +177,8 @@ public partial class ModEditWindow } catch( Exception e ) { - Penumbra.Log.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" ); - _currentFile = null; + _currentFile = null; + _currentException = e; } } @@ -175,6 +216,11 @@ public partial class ModEditWindow if( _currentFile == null ) { ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); + if( _currentException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _currentException.ToString() ); + } } else { @@ -195,7 +241,12 @@ public partial class ModEditWindow if( _defaultFile == null ) { - ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file." ); + ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); + if( _defaultException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _defaultException.ToString() ); + } } else { diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 8a00234a..123d0b58 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -511,10 +511,14 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) { - _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), - DrawMaterialPanel ); - _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), - DrawModelPanel ); + _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", + () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawMaterialPanel, + () => _mod?.ModPath.FullName ?? string.Empty ); + _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", + () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawModelPanel, + () => _mod?.ModPath.FullName ?? string.Empty ); _center = new CombinedTexture( _left, _right ); } From 8d11e1075dbb48949a34c7902023522a13b9b8f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 13:53:52 +0100 Subject: [PATCH 0566/2451] Add some refactoring of data, Stains and STM files. --- OtterGui | 2 +- Penumbra.GameData/Data/DataSharer.cs | 56 +++++ .../{ => Data}/GamePathParser.cs | 44 ++-- .../{ => Data}/ObjectIdentification.cs | 183 +++++++--------- Penumbra.GameData/Data/StainData.cs | 68 ++++++ Penumbra.GameData/Files/MdlFile.cs | 166 +++++++-------- Penumbra.GameData/Files/StmFile.cs | 197 ++++++++++++++++++ Penumbra.GameData/GameData.cs | 11 +- Penumbra.GameData/Structs/Stain.cs | 52 +++++ Penumbra/Penumbra.cs | 3 + Penumbra/Util/StainManager.cs | 25 +++ 11 files changed, 580 insertions(+), 227 deletions(-) create mode 100644 Penumbra.GameData/Data/DataSharer.cs rename Penumbra.GameData/{ => Data}/GamePathParser.cs (83%) rename Penumbra.GameData/{ => Data}/ObjectIdentification.cs (69%) create mode 100644 Penumbra.GameData/Data/StainData.cs create mode 100644 Penumbra.GameData/Files/StmFile.cs create mode 100644 Penumbra.GameData/Structs/Stain.cs create mode 100644 Penumbra/Util/StainManager.cs diff --git a/OtterGui b/OtterGui index 87debfd2..77ecf97a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 87debfd2eceaee8a93fecc0565c454372bcef1f3 +Subproject commit 77ecf97a620e20a1bd65d2e76c784f6f569f4643 diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs new file mode 100644 index 00000000..140006c7 --- /dev/null +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -0,0 +1,56 @@ +using System; +using Dalamud; +using Dalamud.Logging; +using Dalamud.Plugin; + +namespace Penumbra.GameData.Data; + +public abstract class DataSharer : IDisposable +{ + private readonly DalamudPluginInterface _pluginInterface; + private readonly int _version; + protected readonly ClientLanguage Language; + private bool _disposed; + + protected DataSharer(DalamudPluginInterface pluginInterface, ClientLanguage language, int version) + { + _pluginInterface = pluginInterface; + Language = language; + _version = version; + } + + protected virtual void DisposeInternal() + { } + + public void Dispose() + { + if (_disposed) + return; + + DisposeInternal(); + GC.SuppressFinalize(this); + _disposed = true; + } + + ~DataSharer() + => Dispose(); + + protected void DisposeTag(string tag) + => _pluginInterface.RelinquishData(GetVersionedTag(tag)); + + private string GetVersionedTag(string tag) + => $"Penumbra.GameData.{tag}.{Language}.V{_version}"; + + protected T TryCatchData(string tag, Func func) where T : class + { + try + { + return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); + return func(); + } + } +} diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs similarity index 83% rename from Penumbra.GameData/GamePathParser.cs rename to Penumbra.GameData/Data/GamePathParser.cs index d1475a10..affe8704 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -8,7 +8,7 @@ using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -namespace Penumbra.GameData; +namespace Penumbra.GameData.Data; internal class GamePathParser : IGamePathParser { @@ -17,7 +17,7 @@ internal class GamePathParser : IGamePathParser path = path.ToLowerInvariant().Replace('\\', '/'); var (fileType, objectType, match) = ParseGamePath(path); - if (match == null || !match.Success) + if (match is not { Success: true }) return new GameObjectInfo { FileType = fileType, @@ -84,20 +84,20 @@ internal class GamePathParser : IGamePathParser // language=regex private readonly IReadOnlyDictionary>> _regexes = new Dictionary>>() { - [FileType.Font] = new Dictionary> + [FileType.Font] = new Dictionary> { - [ObjectType.Font] = CreateRegexes( @"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), + [ObjectType.Font] = CreateRegexes(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), }, [FileType.Texture] = new Dictionary> { - [ObjectType.Icon] = CreateRegexes( @"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex"), - [ObjectType.Map] = CreateRegexes( @"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex"), - [ObjectType.Weapon] = CreateRegexes( @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex"), - [ObjectType.Monster] = CreateRegexes( @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex"), - [ObjectType.Equipment] = CreateRegexes( @"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), - [ObjectType.DemiHuman] = CreateRegexes( @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), - [ObjectType.Accessory] = CreateRegexes( @"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex"), - [ObjectType.Character] = CreateRegexes( @"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" + [ObjectType.Icon] = CreateRegexes(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex"), + [ObjectType.Map] = CreateRegexes(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex"), + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex"), + [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), + [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), + [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex"), + [ObjectType.Character] = CreateRegexes(@"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" , @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture" , @"chara/common/texture/skin(?'skin'.*)\.tex" , @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex" @@ -105,8 +105,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Model] = new Dictionary> { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl"), + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl"), [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl"), [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl"), [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl"), @@ -114,8 +114,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Material] = new Dictionary> { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl"), + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl"), [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), @@ -123,8 +123,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Imc] = new Dictionary> { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc"), + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc"), [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc"), [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc"), [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc"), @@ -225,7 +225,7 @@ internal class GamePathParser : IGamePathParser { var weaponId = ushort.Parse(groups["weapon"].Value); var setId = ushort.Parse(groups["id"].Value); - if (fileType == FileType.Imc || fileType == FileType.Model) + if (fileType is FileType.Imc or FileType.Model) return GameObjectInfo.Weapon(fileType, setId, weaponId); var variant = byte.Parse(groups["variant"].Value); @@ -236,7 +236,7 @@ internal class GamePathParser : IGamePathParser { var monsterId = ushort.Parse(groups["monster"].Value); var bodyId = ushort.Parse(groups["id"].Value); - if (fileType == FileType.Imc || fileType == FileType.Model) + if (fileType is FileType.Imc or FileType.Model) return GameObjectInfo.Monster(fileType, monsterId, bodyId); var variant = byte.Parse(groups["variant"].Value); @@ -322,5 +322,7 @@ internal class GamePathParser : IGamePathParser private readonly Regex _vfxRegexTmb = new(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", + RegexOptions.Compiled | RegexOptions.IgnoreCase); } diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs similarity index 69% rename from Penumbra.GameData/ObjectIdentification.cs rename to Penumbra.GameData/Data/ObjectIdentification.cs index ddffe9e2..e0a1ec40 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -8,14 +8,13 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; using Action = Lumina.Excel.GeneratedSheets.Action; + +namespace Penumbra.GameData.Data; -namespace Penumbra.GameData; - -internal class ObjectIdentification : IObjectIdentifier +internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { public IGamePathParser GamePathParser { get; } = new GamePathParser(); @@ -44,79 +43,45 @@ internal class ObjectIdentification : IObjectIdentifier switch (slot) { case EquipSlot.MainHand: - case EquipSlot.OffHand: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, - ((ulong)setId << 32) | ((ulong)weaponType << 16) | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); - } - default: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, - ((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); - } + case EquipSlot.OffHand: + { + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, + (ulong)setId << 32 | (ulong)weaponType << 16 | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); + } + default: + { + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, + (ulong)setId << 32 | (ulong)slot.ToSlot() << 16 | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); + } } } - private const int Version = 1; - - private readonly DataManager _dataManager; - private readonly DalamudPluginInterface _pluginInterface; - private readonly ClientLanguage _language; - - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; private readonly IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models; - private readonly IReadOnlyDictionary> _actions; - private bool _disposed = false; + private readonly IReadOnlyDictionary> _actions; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, 1) { - _pluginInterface = pluginInterface; - _dataManager = dataManager; - _language = language; - - _weapons = TryCatchData("Weapons", CreateWeaponList); - _equipment = TryCatchData("Equipment", CreateEquipmentList); - _actions = TryCatchData("Actions", CreateActionList); - _models = TryCatchData("Models", CreateModelList); + _weapons = TryCatchData("Weapons", () => CreateWeaponList(dataManager)); + _equipment = TryCatchData("Equipment", () => CreateEquipmentList(dataManager)); + _actions = TryCatchData("Actions", () => CreateActionList(dataManager)); + _models = TryCatchData("Models", () => CreateModelList(dataManager)); } - public void Dispose() + protected override void DisposeInternal() { - if (_disposed) - return; - - GC.SuppressFinalize(this); - _pluginInterface.RelinquishData(GetVersionedTag("Weapons")); - _pluginInterface.RelinquishData(GetVersionedTag("Equipment")); - _pluginInterface.RelinquishData(GetVersionedTag("Actions")); - _pluginInterface.RelinquishData(GetVersionedTag("Models")); - _disposed = true; + DisposeTag("Weapons"); + DisposeTag("Equipment"); + DisposeTag("Actions"); + DisposeTag("Models"); } - ~ObjectIdentification() - => Dispose(); - - private string GetVersionedTag(string tag) - => $"Penumbra.Identification.{tag}.{_language}.V{Version}"; - - private T TryCatchData(string tag, Func func) where T : class - { - try - { - return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for {tag}:\n{ex}"); - return func(); - } - } - private static bool Add(IDictionary> dict, ulong key, Item item) { if (dict.TryGetValue(key, out var list)) @@ -128,25 +93,25 @@ internal class ObjectIdentification : IObjectIdentifier private static ulong EquipmentKey(Item i) { - var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; + var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; var variant = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).B; - var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - return (model << 32) | (slot << 16) | variant; + var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); + return model << 32 | slot << 16 | variant; } private static ulong WeaponKey(Item i, bool offhand) { - var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; - var model = (ulong)quad.A; - var type = (ulong)quad.B; + var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; + var model = (ulong)quad.A; + var type = (ulong)quad.B; var variant = (ulong)quad.C; - return (model << 32) | (type << 16) | variant; + return model << 32 | type << 16 | variant; } - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList() + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList(DataManager gameData) { - var items = _dataManager.GetExcelSheet(_language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) @@ -161,9 +126,9 @@ internal class ObjectIdentification : IObjectIdentifier return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); } - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList() + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList(DataManager gameData) { - var items = _dataManager.GetExcelSheet(_language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items) { @@ -195,9 +160,9 @@ internal class ObjectIdentification : IObjectIdentifier return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); } - private IReadOnlyDictionary> CreateActionList() + private IReadOnlyDictionary> CreateActionList(DataManager gameData) { - var sheet = _dataManager.GetExcelSheet(_language)!; + var sheet = gameData.GetExcelSheet(Language)!; var storage = new Dictionary>((int)sheet.RowCount); void AddAction(string? key, Action action) @@ -215,29 +180,29 @@ internal class ObjectIdentification : IObjectIdentifier foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty)) { var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); - var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); - var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); + var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); + var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); AddAction(startKey, action); - AddAction(endKey, action); - AddAction(hitKey, action); + AddAction(endKey, action); + AddAction(hitKey, action); } return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } private static ulong ModelValue(ModelChara row) - => row.Type | ((ulong) row.Model << 8) | ((ulong) row.Base << 24) | ((ulong) row.Variant << 32); + => row.Type | (ulong)row.Model << 8 | (ulong)row.Base << 24 | (ulong)row.Variant << 32; private static IEnumerable<(ulong, ObjectKind, uint)> BattleNpcToName(ulong model, uint bNpc) - => Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1); + => Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1); - private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList() + private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList(DataManager gameData) { - var sheetBNpc = _dataManager.GetExcelSheet(_language)!; - var sheetENpc = _dataManager.GetExcelSheet(_language)!; - var sheetCompanion = _dataManager.GetExcelSheet(_language)!; - var sheetMount = _dataManager.GetExcelSheet(_language)!; - var sheetModel = _dataManager.GetExcelSheet(_language)!; + var sheetBNpc = gameData.GetExcelSheet(Language)!; + var sheetENpc = gameData.GetExcelSheet(Language)!; + var sheetCompanion = gameData.GetExcelSheet(Language)!; + var sheetMount = gameData.GetExcelSheet(Language)!; + var sheetModel = gameData.GetExcelSheet(Language)!; var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue); @@ -248,7 +213,7 @@ internal class ObjectIdentification : IObjectIdentifier .GroupBy(t => t.Item1) .Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray())) .ToArray(); - } + } private class Comparer : IComparer<(ulong, IReadOnlyList)> { @@ -259,7 +224,7 @@ internal class ObjectIdentification : IObjectIdentifier private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList)> list, ulong key, ulong mask) { var maskedKey = key & mask; - var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); + var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); if (idx < 0) { if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask)) @@ -277,17 +242,17 @@ internal class ObjectIdentification : IObjectIdentifier private void FindEquipment(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; if (info.EquipSlot != EquipSlot.Unknown) { - key |= (ulong)info.EquipSlot.ToSlot() << 16; + key |= (ulong)info.EquipSlot.ToSlot() << 16; mask |= 0xFFFF0000; } if (info.Variant != 0) { - key |= info.Variant; + key |= info.Variant; mask |= 0xFFFF; } @@ -304,17 +269,17 @@ internal class ObjectIdentification : IObjectIdentifier private void FindWeapon(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; if (info.SecondaryId != 0) { - key |= (ulong)info.SecondaryId << 16; + key |= (ulong)info.SecondaryId << 16; mask |= 0xFFFF0000; } if (info.Variant != 0) { - key |= info.Variant; + key |= info.Variant; mask |= 0xFFFF; } @@ -384,7 +349,7 @@ internal class ObjectIdentification : IObjectIdentifier break; case ObjectType.Character: var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; switch (info.CustomizationType) { @@ -400,16 +365,16 @@ internal class ObjectIdentification : IObjectIdentifier case CustomizationType.DecalEquip: set[$"Equipment Decal {info.PrimaryId}"] = null; break; - default: - { - 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; - } + default: + { + 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; + } } break; diff --git a/Penumbra.GameData/Data/StainData.cs b/Penumbra.GameData/Data/StainData.cs new file mode 100644 index 00000000..f4c4080e --- /dev/null +++ b/Penumbra.GameData/Data/StainData.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +public sealed class StainData : DataSharer, IReadOnlyDictionary +{ + public readonly IReadOnlyDictionary Data; + + public StainData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, 1) + { + Data = TryCatchData("Stains", () => CreateStainData(dataManager)); + } + + protected override void DisposeInternal() + => DisposeTag("Stains"); + + private IReadOnlyDictionary CreateStainData(DataManager dataManager) + { + var stainSheet = dataManager.GetExcelSheet(Language)!; + return stainSheet.Where(s => s.Color != 0 && s.Name.RawData.Length > 0) + .ToDictionary(s => (byte)s.RowId, s => + { + var stain = new Stain(s); + return (stain.Name, stain.RgbaColor, stain.Gloss); + }); + } + + public IEnumerator> GetEnumerator() + => Data.Select(kvp => new KeyValuePair(new StainId(kvp.Key), new Stain())).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => Data.Count; + + public bool ContainsKey(StainId key) + => Data.ContainsKey(key.Value); + + public bool TryGetValue(StainId key, out Stain value) + { + if (!Data.TryGetValue(key.Value, out var data)) + { + value = default; + return false; + } + + value = new Stain(data.Name, data.Dye, key.Value, data.Gloss); + return true; + } + + public Stain this[StainId key] + => TryGetValue(key, out var data) ? data : throw new ArgumentOutOfRangeException(nameof(key)); + + public IEnumerable Keys + => Data.Keys.Select(k => new StainId(k)); + + public IEnumerable Values + => Data.Select(kvp => new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss)); +} diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index e39f612c..09efb624 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -20,10 +20,10 @@ public partial class MdlFile : IWritable public ushort[] ShapeMeshStartIndex; public ushort[] ShapeMeshCount; - public Shape( MdlStructs.ShapeStruct data, uint[] offsets, string[] strings ) + public Shape(MdlStructs.ShapeStruct data, uint[] offsets, string[] strings) { - var idx = offsets.AsSpan().IndexOf( data.StringOffset ); - ShapeName = idx >= 0 ? strings[ idx ] : string.Empty; + var idx = offsets.AsSpan().IndexOf(data.StringOffset); + ShapeName = idx >= 0 ? strings[idx] : string.Empty; ShapeMeshStartIndex = data.ShapeMeshStartIndex; ShapeMeshCount = data.ShapeMeshCount; } @@ -85,144 +85,128 @@ public partial class MdlFile : IWritable // Raw, unparsed data. public byte[] RemainingData; - public MdlFile( byte[] data ) + public MdlFile(byte[] data) { - using var stream = new MemoryStream( data ); - using var r = new LuminaBinaryReader( stream ); + using var stream = new MemoryStream(data); + using var r = new LuminaBinaryReader(stream); - var header = LoadModelFileHeader( r ); + var header = LoadModelFileHeader(r); LodCount = header.LodCount; VertexBufferSize = header.VertexBufferSize; IndexBufferSize = header.IndexBufferSize; VertexOffset = header.VertexOffset; IndexOffset = header.IndexOffset; - for( var i = 0; i < 3; ++i ) + for (var i = 0; i < 3; ++i) { - if( VertexOffset[ i ] > 0 ) - { - VertexOffset[ i ] -= header.RuntimeSize; - } + if (VertexOffset[i] > 0) + VertexOffset[i] -= header.RuntimeSize; - if( IndexOffset[ i ] > 0 ) - { - IndexOffset[ i ] -= header.RuntimeSize; - } + if (IndexOffset[i] > 0) + IndexOffset[i] -= header.RuntimeSize; } VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount]; - for( var i = 0; i < header.VertexDeclarationCount; ++i ) - { - VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( r ); - } + for (var i = 0; i < header.VertexDeclarationCount; ++i) + VertexDeclarations[i] = MdlStructs.VertexDeclarationStruct.Read(r); - var (offsets, strings) = LoadStrings( r ); + var (offsets, strings) = LoadStrings(r); - var modelHeader = LoadModelHeader( r ); + var modelHeader = LoadModelHeader(r); ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount]; - for( var i = 0; i < modelHeader.ElementIdCount; i++ ) - { - ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r ); - } + for (var i = 0; i < modelHeader.ElementIdCount; i++) + ElementIds[i] = MdlStructs.ElementIdStruct.Read(r); - Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 ); + Lods = r.ReadStructuresAsArray(3); ExtraLods = modelHeader.ExtraLodEnabled - ? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 ) - : Array.Empty< MdlStructs.ExtraLodStruct >(); + ? r.ReadStructuresAsArray(3) + : Array.Empty(); Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount]; - for( var i = 0; i < modelHeader.MeshCount; i++ ) - { - Meshes[ i ] = MdlStructs.MeshStruct.Read( r ); - } + for (var i = 0; i < modelHeader.MeshCount; i++) + Meshes[i] = MdlStructs.MeshStruct.Read(r); Attributes = new string[modelHeader.AttributeCount]; - for( var i = 0; i < modelHeader.AttributeCount; ++i ) + for (var i = 0; i < modelHeader.AttributeCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Attributes[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } - TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount ); - SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount ); - TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount ); + TerrainShadowMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowMeshCount); + SubMeshes = r.ReadStructuresAsArray(modelHeader.SubmeshCount); + TerrainShadowSubMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowSubmeshCount); Materials = new string[modelHeader.MaterialCount]; - for( var i = 0; i < modelHeader.MaterialCount; ++i ) + for (var i = 0; i < modelHeader.MaterialCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Materials[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } Bones = new string[modelHeader.BoneCount]; - for( var i = 0; i < modelHeader.BoneCount; ++i ) + for (var i = 0; i < modelHeader.BoneCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Bones[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount]; - for( var i = 0; i < modelHeader.BoneTableCount; i++ ) - { - BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r ); - } + for (var i = 0; i < modelHeader.BoneTableCount; i++) + BoneTables[i] = MdlStructs.BoneTableStruct.Read(r); Shapes = new Shape[modelHeader.ShapeCount]; - for( var i = 0; i < modelHeader.ShapeCount; i++ ) - { - Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings ); - } + for (var i = 0; i < modelHeader.ShapeCount; i++) + Shapes[i] = new Shape(MdlStructs.ShapeStruct.Read(r), offsets, strings); - ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount ); - ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount ); + ShapeMeshes = r.ReadStructuresAsArray(modelHeader.ShapeMeshCount); + ShapeValues = r.ReadStructuresAsArray(modelHeader.ShapeValueCount); var submeshBoneMapSize = r.ReadUInt32(); - SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray(); + SubMeshBoneMap = r.ReadStructures((int)submeshBoneMapSize / 2).ToArray(); var paddingAmount = r.ReadByte(); - r.Seek( r.BaseStream.Position + paddingAmount ); + r.Seek(r.BaseStream.Position + paddingAmount); // Dunno what this first one is for? - BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + BoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount]; - for( var i = 0; i < modelHeader.BoneCount; i++ ) - { - BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( r ); - } + for (var i = 0; i < modelHeader.BoneCount; i++) + BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r); - RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) ); + RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position)); } - private MdlStructs.ModelFileHeader LoadModelFileHeader( LuminaBinaryReader r ) + private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) { - var header = MdlStructs.ModelFileHeader.Read( r ); + var header = MdlStructs.ModelFileHeader.Read(r); Version = header.Version; EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; EnableEdgeGeometry = header.EnableEdgeGeometry; return header; } - private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r ) + private MdlStructs.ModelHeader LoadModelHeader(BinaryReader r) { - var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >(); + var modelHeader = r.ReadStructure(); Radius = modelHeader.Radius; - Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType() - .GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); - Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType() - .GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); + Flags1 = (MdlStructs.ModelFlags1)(modelHeader.GetType() + .GetField("Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); + Flags2 = (MdlStructs.ModelFlags2)(modelHeader.GetType() + .GetField("Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); ModelClipOutDistance = modelHeader.ModelClipOutDistance; ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; Unknown4 = modelHeader.Unknown4; - Unknown5 = ( byte )( modelHeader.GetType() - .GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); + Unknown5 = (byte)(modelHeader.GetType() + .GetField("Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); Unknown6 = modelHeader.Unknown6; Unknown7 = modelHeader.Unknown7; Unknown8 = modelHeader.Unknown8; @@ -233,27 +217,27 @@ public partial class MdlFile : IWritable return modelHeader; } - private static (uint[], string[]) LoadStrings( BinaryReader r ) + private static (uint[], string[]) LoadStrings(BinaryReader r) { var stringCount = r.ReadUInt16(); r.ReadUInt16(); - var stringSize = ( int )r.ReadUInt32(); - var stringData = r.ReadBytes( stringSize ); + var stringSize = (int)r.ReadUInt32(); + var stringData = r.ReadBytes(stringSize); var start = 0; var strings = new string[stringCount]; var offsets = new uint[stringCount]; - for( var i = 0; i < stringCount; ++i ) + for (var i = 0; i < stringCount; ++i) { - var span = stringData.AsSpan( start ); - var idx = span.IndexOf( ( byte )'\0' ); - strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] ); - offsets[ i ] = ( uint )start; - start = start + idx + 1; + var span = stringData.AsSpan(start); + var idx = span.IndexOf((byte)'\0'); + strings[i] = Encoding.UTF8.GetString(span[..idx]); + offsets[i] = (uint)start; + start = start + idx + 1; } - return ( offsets, strings ); + return (offsets, strings); } public unsafe uint StackSize - => ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) ); -} \ No newline at end of file + => (uint)(VertexDeclarations.Length * NumVertices * sizeof(MdlStructs.VertexElement)); +} diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs new file mode 100644 index 00000000..38f0bc47 --- /dev/null +++ b/Penumbra.GameData/Files/StmFile.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Dalamud.Data; +using Lumina.Extensions; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Files; + +public partial class StmFile +{ + public const string Path = "chara/base_material/stainingtemplate.stm"; + + public record struct DyePack + { + public uint Diffuse; + public uint Specular; + public uint Emissive; + public float SpecularPower; + public float Gloss; + } + + public readonly struct StainingTemplateEntry + { + public const int NumElements = 128; + + public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries; + public readonly IReadOnlyList SpecularPowerEntries; + public readonly IReadOnlyList GlossEntries; + + private static uint HalfToByte(Half value) + => (byte)((float)value * byte.MaxValue + 0.5f); + + public DyePack this[StainId idx] + => this[(int)idx.Value]; + + public DyePack this[int idx] + { + get + { + var (dr, dg, db) = DiffuseEntries[idx]; + var (sr, sg, sb) = SpecularEntries[idx]; + var (er, eg, eb) = EmissiveEntries[idx]; + var sp = SpecularPowerEntries[idx]; + var g = GlossEntries[idx]; + return new DyePack() + { + Diffuse = 0xFF000000u | HalfToByte(dr) | (HalfToByte(dg) << 8) | (HalfToByte(db) << 16), + Emissive = 0xFF000000u | HalfToByte(sr) | (HalfToByte(sg) << 8) | (HalfToByte(sb) << 16), + Specular = 0xFF000000u | HalfToByte(er) | (HalfToByte(eg) << 8) | (HalfToByte(eb) << 16), + SpecularPower = (float)sp, + Gloss = (float)g, + }; + } + } + + private class RepeatingList : IReadOnlyList + { + private readonly T _value; + public int Count { get; } + + public RepeatingList(T value, int size) + { + _value = value; + Count = size; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + yield return _value; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public T this[int index] + => index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException(); + } + + private class IndexedList : IReadOnlyList + { + private readonly T[] _values; + private readonly byte[] _indices; + + public IndexedList(BinaryReader br, int count, int indexCount, Func read, int entrySize) + { + _values = new T[count + 1]; + _indices = new byte[indexCount]; + _values[0] = default!; + for (var i = 1; i <= count; ++i) + _values[i] = read(br); + for (var i = 0; i < indexCount; ++i) + { + _indices[i] = br.ReadByte(); + if (_indices[i] > count) + _indices[i] = 0; + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < NumElements; ++i) + yield return _values[_indices[i]]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _indices.Length; + + public T this[int index] + => index >= 0 && index < Count ? _values[_indices[index]] : throw new IndexOutOfRangeException(); + } + + private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) + { + br.Seek(offset); + var arraySize = size / entrySize; + switch (arraySize) + { + case 0: return new RepeatingList(default!, NumElements); + case 1: return new RepeatingList(read(br), NumElements); + case NumElements: + var ret = new T[NumElements]; + for (var i = 0; i < NumElements; ++i) + ret[i] = read(br); + return ret; + case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize / 2, NumElements, read, entrySize); + case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); + } + } + + private static (Half, Half, Half) ReadTriple(BinaryReader br) + => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); + + private static Half ReadSingle(BinaryReader br) + => br.ReadHalf(); + + public unsafe StainingTemplateEntry(BinaryReader br, int offset) + { + br.Seek(offset); + Span ends = stackalloc ushort[5]; + for (var i = 0; i < ends.Length; ++i) + ends[i] = br.ReadUInt16(); + + offset += ends.Length * 2; + DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 3); + SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 3); + EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 3); + SpecularPowerEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 1); + GlossEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 1); + } + } + + public readonly IReadOnlyDictionary Entries; + + public DyePack this[ushort template, int idx] + => Entries.TryGetValue(template, out var entry) ? entry[idx] : default; + + public DyePack this[ushort template, StainId idx] + => this[template, (int)idx.Value]; + + public StmFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var br = new BinaryReader(stream); + br.ReadUInt32(); + var numEntries = br.ReadInt32(); + + var keys = new ushort[numEntries]; + var offsets = new ushort[numEntries]; + + for (var i = 0; i < numEntries; ++i) + keys[i] = br.ReadUInt16(); + + for (var i = 0; i < numEntries; ++i) + offsets[i] = br.ReadUInt16(); + + var entries = new Dictionary(numEntries); + Entries = entries; + + for (var i = 0; i < numEntries; ++i) + { + var offset = offsets[i] * 2 + 8 + 4 * numEntries; + entries.Add(keys[i], new StainingTemplateEntry(br, offset)); + } + } + + public StmFile(DataManager gameData) + : this(gameData.GetFile(Path)?.Data ?? Array.Empty()) + { } +} diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 639d13d1..e29499a6 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -4,6 +4,7 @@ using Dalamud; using Dalamud.Data; using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -31,11 +32,11 @@ public static class GameData } public interface IObjectIdentifier : IDisposable -{ - /// - /// An accessible parser for game paths. - /// - public IGamePathParser GamePathParser { get; } +{ + /// + /// An accessible parser for game paths. + /// + public IGamePathParser GamePathParser { get; } /// /// Add all known game objects using the given game path to the dictionary. diff --git a/Penumbra.GameData/Structs/Stain.cs b/Penumbra.GameData/Structs/Stain.cs new file mode 100644 index 00000000..c8b47b42 --- /dev/null +++ b/Penumbra.GameData/Structs/Stain.cs @@ -0,0 +1,52 @@ +using Dalamud.Utility; + +namespace Penumbra.GameData.Structs; + +// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name. +public readonly struct Stain +{ + // An empty stain with transparent color. + public static readonly Stain None = new("None"); + + public readonly string Name; + public readonly uint RgbaColor; + public readonly byte RowIndex; + public readonly bool Gloss; + + public byte R + => (byte)(RgbaColor & 0xFF); + + public byte G + => (byte)((RgbaColor >> 8) & 0xFF); + + public byte B + => (byte)((RgbaColor >> 16) & 0xFF); + + public byte Intensity + => (byte)((1 + R + G + B) / 3); + + // R and B need to be shuffled and Alpha set to max. + private static uint SeColorToRgba(uint color) + => ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000; + + public Stain(Lumina.Excel.GeneratedSheets.Stain stain) + : this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown4) + { } + + internal Stain(string name, uint dye, byte index, bool gloss) + { + Name = name; + RowIndex = index; + Gloss = gloss; + RgbaColor = dye; + } + + // Only used by None. + private Stain(string name) + { + Name = name; + RowIndex = 0; + RgbaColor = 0; + Gloss = false; + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f54b189f..9335c32d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -61,6 +61,7 @@ public class Penumbra : IDalamudPlugin public static ActorManager Actors { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; + public static StainManager StainManager { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -85,6 +86,7 @@ public class Penumbra : IDalamudPlugin Log = new Logger(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); + StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); @@ -292,6 +294,7 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + StainManager?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); diff --git a/Penumbra/Util/StainManager.cs b/Penumbra/Util/StainManager.cs new file mode 100644 index 00000000..fc42e78e --- /dev/null +++ b/Penumbra/Util/StainManager.cs @@ -0,0 +1,25 @@ +using System; +using Dalamud.Data; +using Dalamud.Plugin; +using OtterGui.Widgets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; + +namespace Penumbra.Util; + +public class StainManager : IDisposable +{ + public readonly StainData StainData; + public readonly FilterComboColors Combo; + public readonly StmFile StmFile; + + public StainManager(DalamudPluginInterface pluginInterface, DataManager dataManager) + { + StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); + Combo = new FilterComboColors( 140, StainData.Data ); + StmFile = new StmFile( dataManager ); + } + + public void Dispose() + => StainData.Dispose(); +} \ No newline at end of file From 0e7c564d1468a0cdc80e01eeddf0e6d909f937d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 13:57:34 +0100 Subject: [PATCH 0567/2451] Misc. Changes. --- Penumbra/UI/Classes/ModEditWindow.Files.cs | 1 - Penumbra/UI/Classes/ModEditWindow.cs | 22 +- .../ConfigWindow.CollectionsTab.Individual.cs | 212 ++++++++++++++++++ Penumbra/UI/ConfigWindow.CollectionsTab.cs | 49 ---- Penumbra/UI/ConfigWindow.DebugTab.cs | 45 +++- 5 files changed, 267 insertions(+), 62 deletions(-) create mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 9b9c16d2..18f17711 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -26,7 +26,6 @@ public partial class ModEditWindow private LowerString _fileOverviewFilter2 = LowerString.Empty; private LowerString _fileOverviewFilter3 = LowerString.Empty; - private bool CheckFilter( Mod.Editor.FileRegistry registry ) => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 123d0b58..83d58be4 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -20,10 +20,10 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - private Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private const string WindowBaseLabel = "###SubModEdit"; + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; public void ChangeMod( Mod mod ) { @@ -85,37 +85,37 @@ public partial class ModEditWindow : Window, IDisposable sb.Append( _mod!.Name ); if( subMods > 1 ) { - sb.AppendFormat( " | {0} Options", subMods ); + sb.Append( $" | {subMods} Options" ); } if( size > 0 ) { - sb.AppendFormat( " | {0} Files ({1})", _editor.AvailableFiles.Count, Functions.HumanReadableSize( size ) ); + sb.Append( $" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize( size )})" ); } if( unused > 0 ) { - sb.AppendFormat( " | {0} Unused Files", unused ); + sb.Append( $" | {unused} Unused Files" ); } if( _editor.MissingFiles.Count > 0 ) { - sb.AppendFormat( " | {0} Missing Files", _editor.MissingFiles.Count ); + sb.Append( $" | {_editor.MissingFiles.Count} Missing Files" ); } if( redirections > 0 ) { - sb.AppendFormat( " | {0} Redirections", redirections ); + sb.Append( $" | {redirections} Redirections" ); } if( manipulations > 0 ) { - sb.AppendFormat( " | {0} Manipulations", manipulations ); + sb.Append( $" | {manipulations} Manipulations" ); } if( swaps > 0 ) { - sb.AppendFormat( " | {0} Swaps", swaps ); + sb.Append( $" | {swaps} Swaps" ); } sb.Append( WindowBaseLabel ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs new file mode 100644 index 00000000..8d7eba65 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using System.Linq; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Enums; +using OtterGui.Widgets; +using Penumbra.GameData.Actors; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private partial class CollectionsTab + { + private sealed class WorldCombo : FilterComboCache< KeyValuePair< ushort, string > > + { + private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "All Worlds"); + + public WorldCombo( IReadOnlyDictionary< ushort, string > worlds ) + : base( worlds.OrderBy( kvp => kvp.Value ).Prepend( AllWorldPair ) ) + { + CurrentSelection = AllWorldPair; + CurrentSelectionIdx = 0; + } + + protected override string ToString( KeyValuePair< ushort, string > obj ) + => obj.Value; + + public void Draw( float width ) + => Draw( "##worldCombo", CurrentSelection.Value, width, ImGui.GetTextLineHeightWithSpacing() ); + } + + private sealed class NpcCombo : FilterComboCache< (string Name, uint[] Ids) > + { + private readonly string _label; + + public NpcCombo( string label, IReadOnlyDictionary< uint, string > names ) + : base( () => names.GroupBy( kvp => kvp.Value ).Select( g => ( g.Key, g.Select( g => g.Key ).ToArray() ) ).OrderBy( g => g.Key ).ToList() ) + => _label = label; + + protected override string ToString( (string Name, uint[] Ids) obj ) + => obj.Name; + + protected override bool DrawSelectable( int globalIdx, bool selected ) + { + var (name, ids) = Items[ globalIdx ]; + var ret = ImGui.Selectable( name, selected ); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( '\n', ids.Select( i => i.ToString() ) ) ); + } + + return ret; + } + + public void Draw( float width ) + => Draw( _label, CurrentSelection.Name, width, ImGui.GetTextLineHeightWithSpacing() ); + } + + + // Input Selections. + private string _newCharacterName = string.Empty; + private IdentifierType _newType = IdentifierType.Player; + private ObjectKind _newKind = ObjectKind.BattleNpc; + + private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Worlds); + private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Mounts); + private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Companions); + private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); + private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); + + private void DrawNewIdentifierOptions( float width ) + { + ImGui.SetNextItemWidth( width ); + using var combo = ImRaii.Combo( "##newType", _newType.ToString() ); + if( combo ) + { + if( ImGui.Selectable( IdentifierType.Player.ToString(), _newType == IdentifierType.Player ) ) + { + _newType = IdentifierType.Player; + } + + if( ImGui.Selectable( IdentifierType.Owned.ToString(), _newType == IdentifierType.Owned ) ) + { + _newType = IdentifierType.Owned; + } + + if( ImGui.Selectable( IdentifierType.Npc.ToString(), _newType == IdentifierType.Npc ) ) + { + _newType = IdentifierType.Npc; + } + } + } + + private void DrawNewObjectKindOptions( float width ) + { + ImGui.SetNextItemWidth( width ); + using var combo = ImRaii.Combo( "##newKind", _newKind.ToString() ); + if( combo ) + { + if( ImGui.Selectable( ObjectKind.BattleNpc.ToString(), _newKind == ObjectKind.BattleNpc ) ) + { + _newKind = ObjectKind.BattleNpc; + } + + if( ImGui.Selectable( ObjectKind.EventNpc.ToString(), _newKind == ObjectKind.EventNpc ) ) + { + _newKind = ObjectKind.EventNpc; + } + + if( ImGui.Selectable( ObjectKind.Companion.ToString(), _newKind == ObjectKind.Companion ) ) + { + _newKind = ObjectKind.Companion; + } + + if( ImGui.Selectable( ObjectKind.MountType.ToString(), _newKind == ObjectKind.MountType ) ) + { + _newKind = ObjectKind.MountType; + } + } + } + + // We do not check for valid character names. + private void DrawNewCharacterCollection() + { + const string description = "Character Collections apply specifically to individual game objects of the given name.\n" + + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an .\n" + + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; + + var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; + DrawNewIdentifierOptions( width ); + ImGui.SameLine(); + using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) + { + _worldCombo.Draw( width ); + } + + ImGui.SameLine(); + + using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) + { + DrawNewObjectKindOptions( width ); + } + + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) + { + ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); + } + + ImGui.SameLine(); + var disabled = _newCharacterName.Length == 0; + var tt = disabled + ? $"Please enter the name of a {ConditionalIndividual} before assigning the collection.\n\n" + description + : description; + if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalIndividual}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, + disabled ) ) + { + Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); + _newCharacterName = string.Empty; + } + + using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) + { + switch( _newKind ) + { + case ObjectKind.BattleNpc: + _bnpcCombo.Draw( _window._inputTextWidth.X ); + break; + case ObjectKind.EventNpc: + _enpcCombo.Draw( _window._inputTextWidth.X ); + break; + case ObjectKind.Companion: + _companionCombo.Draw( _window._inputTextWidth.X ); + break; + case ObjectKind.MountType: + _mountCombo.Draw( _window._inputTextWidth.X ); + break; + } + } + } + + private void DrawIndividualAssignments() + { + using var _ = ImRaii.Group(); + ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); + ImGui.Separator(); + foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) + { + using var id = ImRaii.PushId( name ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.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.Dummy( Vector2.Zero ); + DrawNewCharacterCollection(); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index bf8b0107..6025cb70 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -39,11 +39,9 @@ public partial class ConfigWindow } } - // Input text fields. private string _newCollectionName = string.Empty; private bool _canAddCollection = false; - private string _newCharacterName = string.Empty; // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -212,28 +210,6 @@ public partial class ConfigWindow } } - // We do not check for valid character names. - private void DrawNewCharacterCollection() - { - const string description = "Character Collections apply specifically to individual game objects of the given name.\n" - + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an .\n" - + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; - - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); - ImGui.SameLine(); - var disabled = _newCharacterName.Length == 0; - var tt = disabled - ? $"Please enter the name of a {ConditionalIndividual} before assigning the collection.\n\n" + description - : description; - if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalIndividual}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, - disabled ) ) - { - Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); - _newCharacterName = string.Empty; - } - } - private void DrawSpecialCollections() { foreach( var (type, name, desc) in CollectionTypeExtensions.Special ) @@ -268,31 +244,6 @@ public partial class ConfigWindow DrawNewSpecialCollection(); } - private void DrawIndividualAssignments() - { - using var _ = ImRaii.Group(); - ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); - ImGui.Separator(); - foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) - { - using var id = ImRaii.PushId( name ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.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.Dummy( Vector2.Zero ); - DrawNewCharacterCollection(); - } - private void DrawActiveCollectionSelectors() { ImGui.Dummy( _window._defaultSpace ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 0d64c1b8..5cf59e80 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Files; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using Penumbra.String; @@ -60,6 +61,8 @@ public partial class ConfigWindow ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawStainTemplates(); + ImGui.NewLine(); DrawDebugTabMetaLists(); ImGui.NewLine(); DrawDebugResidentResources(); @@ -171,7 +174,6 @@ public partial class ConfigWindow var identifier = Penumbra.Actors.FromObject( obj ); ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); ImGuiUtil.DrawTableColumn( identifier.DataId.ToString() ); - } } @@ -246,6 +248,47 @@ public partial class ConfigWindow } } + private static unsafe void DrawStainTemplates() + { + if( !ImGui.CollapsingHeader( "Staining Templates" ) ) + { + return; + } + + foreach( var (key, data) in Penumbra.StainManager.StmFile.Entries ) + { + using var tree = ImRaii.TreeNode( $"Template {key}" ); + if( !tree ) + { + continue; + } + + using var table = ImRaii.Table( "##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + continue; + } + + for( var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i ) + { + var (r, g, b) = data.DiffuseEntries[ i ]; + ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); + + ( r, g, b ) = data.SpecularEntries[ i ]; + ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); + + ( r, g, b ) = data.EmissiveEntries[ i ]; + ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); + + var a = data.SpecularPowerEntries[ i ]; + ImGuiUtil.DrawTableColumn( $"{a:F6}" ); + + a = data.GlossEntries[ i ]; + ImGuiUtil.DrawTableColumn( $"{a:F6}" ); + } + } + } + // Draw information about the character utility class from SE, // displaying all files, their sizes, the default files and the default sizes. public static unsafe void DrawDebugCharacterUtility() From 76fc235cb708a0c3be645ff234d7cf1eb1fdd362 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 13:58:16 +0100 Subject: [PATCH 0568/2451] Make collection selectors filterable. --- Penumbra/UI/ConfigWindow.Misc.cs | 45 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index bf4f44b6..1207fa57 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -7,6 +8,7 @@ using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Structs; @@ -90,29 +92,32 @@ public partial class ConfigWindow } } + private sealed class CollectionSelector : FilterComboCache< ModCollection > + { + public CollectionSelector( IEnumerable< ModCollection > items ) + : base( items ) + { } + + public void Draw( string label, float width, CollectionType type, string? characterName ) + { + var current = Penumbra.CollectionManager.ByType( type, characterName ); + if( Draw( label, current?.Name ?? string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) + { + Penumbra.CollectionManager.SetCollection( CurrentSelection, type, characterName ); + } + } + + protected override string ToString( ModCollection obj ) + => obj.Name; + } + + private static readonly CollectionSelector CollectionsWithEmpty = new(Penumbra.CollectionManager.OrderBy( c => c.Name ).Prepend( ModCollection.Empty )); + private static readonly CollectionSelector Collections = new(Penumbra.CollectionManager.OrderBy( c => c.Name )); + // Draw a collection selector of a certain width for a certain type. private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty, string? characterName ) - { - ImGui.SetNextItemWidth( width ); - - var current = Penumbra.CollectionManager.ByType( collectionType, characterName ); - using var combo = ImRaii.Combo( label, current?.Name ?? string.Empty ); - if( combo ) - { - var enumerator = Penumbra.CollectionManager.OrderBy( c => c.Name ).AsEnumerable(); - if( withEmpty ) - enumerator = enumerator.Prepend( ModCollection.Empty ); - foreach( var collection in enumerator ) - { - using var id = ImRaii.PushId( collection.Index ); - if( ImGui.Selectable( collection.Name, collection == current ) ) - { - Penumbra.CollectionManager.SetCollection( collection, collectionType, characterName ); - } - } - } - } + => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType, characterName ); // Set up the file selector with the right flags and custom side bar items. public static FileDialogManager SetupFileManager() From e6fce3297560db29559269f00def79b3d52f67ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 14:07:07 +0100 Subject: [PATCH 0569/2451] Fix dummy situation for group settings. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 30 ++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/OtterGui b/OtterGui index 77ecf97a..79237e1e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 77ecf97a620e20a1bd65d2e76c784f6f569f4643 +Subproject commit 79237e1ed87dbad96bc6b7bd259b59c99975355d diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index dfe692b5..7b19243d 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -61,19 +62,24 @@ public partial class ConfigWindow if( _mod.Groups.Count > 0 ) { - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + var useDummy = true; + foreach(var (group, idx) in _mod.Groups.WithIndex().Where(g => g.Value.Type == GroupType.Single && g.Value.IsOption )) { - DrawSingleGroup( _mod.Groups[ idx ], idx ); + ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); + useDummy = false; + DrawSingleGroup( group, idx ); } - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + useDummy = true; + foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Multi && g.Value.IsOption ) ) { - DrawMultiGroup( _mod.Groups[ idx ], idx ); + ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); + useDummy = false; + DrawMultiGroup( group, idx ); } } + ImGui.Dummy( _window._defaultSpace ); _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); } @@ -159,13 +165,8 @@ public partial class ConfigWindow // If a description is provided, add a help marker besides it. private void DrawSingleGroup( IModGroup group, int groupIdx ) { - if( group.Type != GroupType.Single || !group.IsOption ) - { - return; - } - using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? (int) group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; + var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); if( combo ) @@ -198,11 +199,6 @@ public partial class ConfigWindow // If a description is provided, add a help marker in the title. private void DrawMultiGroup( IModGroup group, int groupIdx ) { - if( group.Type != GroupType.Multi || !group.IsOption ) - { - return; - } - using var id = ImRaii.PushId( groupIdx ); var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; Widget.BeginFramedGroup( group.Name, group.Description ); From cbc27d31da7e0a57b50f11ee4a667a5c4761262a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 14:17:18 +0100 Subject: [PATCH 0570/2451] Fix a missed OtterGui commit. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 79237e1e..f2d99609 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 79237e1ed87dbad96bc6b7bd259b59c99975355d +Subproject commit f2d996094058059b67b27a208303627c83b26d69 From 0b1a11132b3ea596d8535ca4a0998a122762d1af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 16:55:08 +0100 Subject: [PATCH 0571/2451] Update to current state of ActorIdentification, add start of collection management. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 92 +++++++------------ .../Actors/ActorManager.Identifiers.cs | 3 - .../Collections/CollectionManager.Active.cs | 57 +++++++++++- 3 files changed, 85 insertions(+), 67 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 9cf233c4..c00e6dd2 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -6,14 +6,14 @@ using Dalamud; using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; namespace Penumbra.GameData.Actors; -public partial class ActorManager : IDisposable +public sealed partial class ActorManager : DataSharer { /// Worlds available for players. public IReadOnlyDictionary Worlds { get; } @@ -30,95 +30,67 @@ public partial class ActorManager : IDisposable /// Valid ENPC names in title case by ENPC id. public IReadOnlyDictionary ENpcs { get; } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, Func toParentIdx) + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + Func toParentIdx) : this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx) - {} + { } public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, ClientLanguage language, Func toParentIdx) + : base(pluginInterface, language, 1) { - _pluginInterface = pluginInterface; - _objects = objects; - _clientState = state; - _gameData = gameData; - _language = language; - _toParentIdx = toParentIdx; + _objects = objects; + _clientState = state; + _toParentIdx = toParentIdx; - Worlds = TryCatchData("Worlds", CreateWorldData); - Mounts = TryCatchData("Mounts", CreateMountData); - Companions = TryCatchData("Companions", CreateCompanionData); - BNpcs = TryCatchData("BNpcs", CreateBNpcData); - ENpcs = TryCatchData("ENpcs", CreateENpcData); + Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); + Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); + Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); + BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); + ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); ActorIdentifier.Manager = this; } - public void Dispose() + protected override void DisposeInternal() { - if (_disposed) - return; - - GC.SuppressFinalize(this); - _pluginInterface.RelinquishData(GetVersionedTag("Worlds")); - _pluginInterface.RelinquishData(GetVersionedTag("Mounts")); - _pluginInterface.RelinquishData(GetVersionedTag("Companions")); - _pluginInterface.RelinquishData(GetVersionedTag("BNpcs")); - _pluginInterface.RelinquishData(GetVersionedTag("ENpcs")); - _disposed = true; + DisposeTag("Worlds"); + DisposeTag("Mounts"); + DisposeTag("Companions"); + DisposeTag("BNpcs"); + DisposeTag("ENpcs"); } ~ActorManager() => Dispose(); - private const int Version = 1; - - private readonly DalamudPluginInterface _pluginInterface; - private readonly ObjectTable _objects; - private readonly ClientState _clientState; - private readonly DataManager _gameData; - private readonly ClientLanguage _language; - private bool _disposed; + private readonly ObjectTable _objects; + private readonly ClientState _clientState; private readonly Func _toParentIdx; - private IReadOnlyDictionary CreateWorldData() - => _gameData.GetExcelSheet(_language)! + private IReadOnlyDictionary CreateWorldData(DataManager gameData) + => gameData.GetExcelSheet(Language)! .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); - private IReadOnlyDictionary CreateMountData() - => _gameData.GetExcelSheet(_language)! + private IReadOnlyDictionary CreateMountData(DataManager gameData) + => gameData.GetExcelSheet(Language)! .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) .ToDictionary(m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(m.Singular.ToDalamudString().ToString())); - private IReadOnlyDictionary CreateCompanionData() - => _gameData.GetExcelSheet(_language)! + private IReadOnlyDictionary CreateCompanionData(DataManager gameData) + => gameData.GetExcelSheet(Language)! .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) .ToDictionary(c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(c.Singular.ToDalamudString().ToString())); - private IReadOnlyDictionary CreateBNpcData() - => _gameData.GetExcelSheet(_language)! + private IReadOnlyDictionary CreateBNpcData(DataManager gameData) + => gameData.GetExcelSheet(Language)! .Where(n => n.Singular.RawData.Length > 0) .ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString())); - private IReadOnlyDictionary CreateENpcData() - => _gameData.GetExcelSheet(_language)! + private IReadOnlyDictionary CreateENpcData(DataManager gameData) + => gameData.GetExcelSheet(Language)! .Where(e => e.Singular.RawData.Length > 0) .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); - - private string GetVersionedTag(string tag) - => $"Penumbra.Actors.{tag}.{_language}.V{Version}"; - - private T TryCatchData(string tag, Func func) where T : class - { - try - { - return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); - return func(); - } - } } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index c504470e..eb2a73b4 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -1,10 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.Serialization; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; -using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json.Linq; using Penumbra.String; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 0e6e1730..99ba3398 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -17,15 +17,64 @@ namespace Penumbra.Collections; public class IndividualCollections { private readonly ActorManager _manager; - public readonly List< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > Assignments; - public readonly Dictionary< ActorIdentifier, ModCollection > Individuals; + private readonly List< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > _assignments = new(); + private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); + + public IReadOnlyList< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > Assignments + => _assignments; + + public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals + => _individuals; public IndividualCollections( ActorManager manager ) => _manager = manager; - public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, uint dataId ) + public bool CanAdd( ActorIdentifier identifier ) + => identifier.IsValid && !Individuals.ContainsKey( identifier ); + + public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) { - return false; + identifiers = Array.Empty< ActorIdentifier >(); + + switch( type ) + { + case IdentifierType.Player: + { + if( !ByteString.FromString( name, out var playerName ) ) + { + return false; + } + + var identifier = _manager.CreatePlayer( playerName, homeWorld ); + if( !CanAdd( identifier ) ) + { + return false; + } + + identifiers = new[] { identifier }; + return true; + } + //case IdentifierType.Owned: + //{ + // if( !ByteString.FromString( name, out var ownerName ) ) + // { + // return false; + // } + // + // identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); + // return + // identifier = _manager.CreateIndividual( type, byteName, homeWorld, kind, dataId ); + // return CanAdd( identifier ); + //} + //case IdentifierType.Npc: + //{ + // identifier = _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, dataId ); + // return CanAdd( identifier ); + //} + default: + identifiers = Array.Empty< ActorIdentifier >(); + return false; + } } public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection ) From b3a993a2bc6c34fd3a9e531fc0c2b1958592aa84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Nov 2022 15:59:35 +0100 Subject: [PATCH 0572/2451] Further work on Dye Template previews --- OtterGui | 2 +- Penumbra.GameData/Files/StmFile.cs | 26 ++++++----- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 46 ++++++++++++++++--- Penumbra/Util/StainManager.cs | 27 +++++++---- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/OtterGui b/OtterGui index f2d99609..cbf21c63 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f2d996094058059b67b27a208303627c83b26d69 +Subproject commit cbf21c639f91d39422b2d4b7244bd8d8c5d5d4d7 diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs index 38f0bc47..288c8600 100644 --- a/Penumbra.GameData/Files/StmFile.cs +++ b/Penumbra.GameData/Files/StmFile.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Numerics; using Dalamud.Data; using Lumina.Extensions; using Penumbra.GameData.Structs; @@ -14,11 +15,11 @@ public partial class StmFile public record struct DyePack { - public uint Diffuse; - public uint Specular; - public uint Emissive; - public float SpecularPower; - public float Gloss; + public Vector3 Diffuse; + public Vector3 Specular; + public Vector3 Emissive; + public float SpecularPower; + public float Gloss; } public readonly struct StainingTemplateEntry @@ -31,9 +32,6 @@ public partial class StmFile public readonly IReadOnlyList SpecularPowerEntries; public readonly IReadOnlyList GlossEntries; - private static uint HalfToByte(Half value) - => (byte)((float)value * byte.MaxValue + 0.5f); - public DyePack this[StainId idx] => this[(int)idx.Value]; @@ -41,16 +39,20 @@ public partial class StmFile { get { + if (idx is <= 0 or > NumElements) + return default; + + --idx; var (dr, dg, db) = DiffuseEntries[idx]; var (sr, sg, sb) = SpecularEntries[idx]; var (er, eg, eb) = EmissiveEntries[idx]; var sp = SpecularPowerEntries[idx]; var g = GlossEntries[idx]; - return new DyePack() + return new DyePack { - Diffuse = 0xFF000000u | HalfToByte(dr) | (HalfToByte(dg) << 8) | (HalfToByte(db) << 16), - Emissive = 0xFF000000u | HalfToByte(sr) | (HalfToByte(sg) << 8) | (HalfToByte(sb) << 16), - Specular = 0xFF000000u | HalfToByte(er) | (HalfToByte(eg) << 8) | (HalfToByte(eb) << 16), + Diffuse = new Vector3((float)dr, (float)dg, (float)db), + Emissive = new Vector3((float)sr, (float)sg, (float)sb), + Specular = new Vector3((float)er, (float)eg, (float)eb), SpecularPower = (float)sp, Gloss = (float)g, }; diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index ed9ddd10..3f6ba35b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -8,9 +8,12 @@ using System.Runtime.InteropServices; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; +using Lumina.Data.Parsing.Layer; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -326,7 +329,14 @@ public partial class ModEditWindow ColorSetCopyAllClipboardButton( file, 0 ); ImGui.SameLine(); var ret = ColorSetPasteAllClipboardButton( file, 0 ); - using var table = ImRaii.Table( "##ColorSets", 10, + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 10, 0 ) ); + ImGui.SameLine(); + Penumbra.StainManager.StainCombo.Draw( "Preview Dye", Penumbra.StainManager.StainCombo.CurrentSelection.Value.Color, true ); + ImGui.SameLine(); + ImGui.Button( "Apply Preview Dyes." ); + + using var table = ImRaii.Table( "##ColorSets", 11, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); if( !table ) { @@ -353,6 +363,8 @@ public partial class ModEditWindow ImGui.TableHeader( "Skew" ); ImGui.TableNextColumn(); ImGui.TableHeader( "Dye" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye Preview" ); for( var j = 0; j < file.ColorSets.Length; ++j ) { @@ -731,17 +743,37 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( hasDye ) { - tmpInt = dye.Template; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##DyeTemplate", ref tmpInt, 0, 0 ) - && tmpInt != dye.Template - && tmpInt is >= 0 and <= ushort.MaxValue ) + if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = ( ushort )tmpInt; + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; ret = true; } ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; + if( stain != 0 && Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + { + var values = entry[ ( int )stain ]; + using var _ = ImRaii.Disabled(); + ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, c => { } ); + ImGui.SameLine(); + ColorPicker( "##specularPreview", string.Empty, values.Specular, c => { } ); + ImGui.SameLine(); + ColorPicker( "##emissivePreview", string.Empty, values.Emissive, c => { } ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##gloss", ref values.Gloss ); + } + } + else + { + ImGui.TableNextColumn(); } diff --git a/Penumbra/Util/StainManager.cs b/Penumbra/Util/StainManager.cs index fc42e78e..91c9a731 100644 --- a/Penumbra/Util/StainManager.cs +++ b/Penumbra/Util/StainManager.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Dalamud.Data; using Dalamud.Plugin; using OtterGui.Widgets; @@ -9,15 +11,24 @@ namespace Penumbra.Util; public class StainManager : IDisposable { - public readonly StainData StainData; - public readonly FilterComboColors Combo; - public readonly StmFile StmFile; - - public StainManager(DalamudPluginInterface pluginInterface, DataManager dataManager) + public sealed class StainTemplateCombo : FilterComboCache< ushort > { - StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); - Combo = new FilterComboColors( 140, StainData.Data ); - StmFile = new StmFile( dataManager ); + public StainTemplateCombo( IEnumerable< ushort > items ) + : base( items ) + { } + } + + public readonly StainData StainData; + public readonly FilterComboColors StainCombo; + public readonly StmFile StmFile; + public readonly StainTemplateCombo TemplateCombo; + + public StainManager( DalamudPluginInterface pluginInterface, DataManager dataManager ) + { + StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); + StainCombo = new FilterComboColors( 140, StainData.Data.Prepend( new KeyValuePair< byte, (string Name, uint Dye, bool Gloss) >( 0, ( "None", 0, false ) ) ) ); + StmFile = new StmFile( dataManager ); + TemplateCombo = new StainTemplateCombo( StmFile.Entries.Keys.Prepend( ( ushort )0 ) ); } public void Dispose() From 4df8f720f57ccc573b5668ae51e9e1da5b1135ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Nov 2022 13:33:14 +0100 Subject: [PATCH 0573/2451] Fix small bug when using text commands to enable or disable Penumbra. --- Penumbra/Penumbra.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9335c32d..d266339b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -410,15 +410,15 @@ public class Penumbra : IDalamudPlugin case "enable": { Dalamud.Chat.Print( Enable() - ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" - : modsEnabled ); + ? modsEnabled + : "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" ); break; } case "disable": { Dalamud.Chat.Print( Disable() - ? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" - : modsDisabled ); + ? modsDisabled + : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); break; } case "toggle": From 17a8e06c1d0567fae47b4a83ec63d4aa2b3da8eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Nov 2022 17:15:41 +0100 Subject: [PATCH 0574/2451] Finish work on dye previews. --- Penumbra.GameData/Files/MtrlFile.cs | 274 ++++++++------ .../Files/StmFile.StainingTemplateEntry.cs | 174 +++++++++ Penumbra.GameData/Files/StmFile.cs | 179 ++------- .../UI/Classes/ModEditWindow.FileEditor.cs | 252 +++++++++++++ ...FileEdit.cs => ModEditWindow.Materials.cs} | 357 ++++-------------- Penumbra/UI/Classes/ModEditWindow.Models.cs | 31 ++ Penumbra/UI/Classes/ModEditWindow.cs | 2 +- 7 files changed, 722 insertions(+), 547 deletions(-) create mode 100644 Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.FileEditor.cs rename Penumbra/UI/Classes/{ModEditWindow.FileEdit.cs => ModEditWindow.Materials.cs} (68%) create mode 100644 Penumbra/UI/Classes/ModEditWindow.Models.cs diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index cbefed8d..b9f46c1f 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Text; using Lumina.Data.Parsing; using Lumina.Extensions; +using Penumbra.GameData.Structs; namespace Penumbra.GameData.Files; @@ -28,114 +29,112 @@ public partial class MtrlFile : IWritable public Vector3 Diffuse { - get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 )); + get => new(ToFloat(0), ToFloat(1), ToFloat(2)); set { - _data[ 0 ] = FromFloat( value.X ); - _data[ 1 ] = FromFloat( value.Y ); - _data[ 2 ] = FromFloat( value.Z ); + _data[0] = FromFloat(value.X); + _data[1] = FromFloat(value.Y); + _data[2] = FromFloat(value.Z); } } public Vector3 Specular { - get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 )); + get => new(ToFloat(4), ToFloat(5), ToFloat(6)); set { - _data[ 4 ] = FromFloat( value.X ); - _data[ 5 ] = FromFloat( value.Y ); - _data[ 6 ] = FromFloat( value.Z ); + _data[4] = FromFloat(value.X); + _data[5] = FromFloat(value.Y); + _data[6] = FromFloat(value.Z); } } public Vector3 Emissive { - get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 )); + get => new(ToFloat(8), ToFloat(9), ToFloat(10)); set { - _data[ 8 ] = FromFloat( value.X ); - _data[ 9 ] = FromFloat( value.Y ); - _data[ 10 ] = FromFloat( value.Z ); + _data[8] = FromFloat(value.X); + _data[9] = FromFloat(value.Y); + _data[10] = FromFloat(value.Z); } } public Vector2 MaterialRepeat { - get => new(ToFloat( 12 ), ToFloat( 15 )); + get => new(ToFloat(12), ToFloat(15)); set { - _data[ 12 ] = FromFloat( value.X ); - _data[ 15 ] = FromFloat( value.Y ); + _data[12] = FromFloat(value.X); + _data[15] = FromFloat(value.Y); } } public Vector2 MaterialSkew { - get => new(ToFloat( 13 ), ToFloat( 14 )); + get => new(ToFloat(13), ToFloat(14)); set { - _data[ 13 ] = FromFloat( value.X ); - _data[ 14 ] = FromFloat( value.Y ); + _data[13] = FromFloat(value.X); + _data[14] = FromFloat(value.Y); } } public float SpecularStrength { - get => ToFloat( 3 ); - set => _data[ 3 ] = FromFloat( value ); + get => ToFloat(3); + set => _data[3] = FromFloat(value); } public float GlossStrength { - get => ToFloat( 7 ); - set => _data[ 7 ] = FromFloat( value ); + get => ToFloat(7); + set => _data[7] = FromFloat(value); } public ushort TileSet { - get => (ushort) (ToFloat(11) * 64f); - set => _data[ 11 ] = FromFloat(value / 64f); + get => (ushort)(ToFloat(11) * 64f); + set => _data[11] = FromFloat(value / 64f); } - private float ToFloat( int idx ) - => ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] ); + private float ToFloat(int idx) + => (float)BitConverter.UInt16BitsToHalf(_data[idx]); - private static ushort FromFloat( float x ) - => BitConverter.HalfToUInt16Bits( ( Half )x ); + private static ushort FromFloat(float x) + => BitConverter.HalfToUInt16Bits((Half)x); } - public struct RowArray : IEnumerable< Row > + public struct RowArray : IEnumerable { public const int NumRows = 16; private fixed byte _rowData[NumRows * Row.Size]; - public ref Row this[ int i ] + public ref Row this[int i] { get { - fixed( byte* ptr = _rowData ) + fixed (byte* ptr = _rowData) { - return ref ( ( Row* )ptr )[ i ]; + return ref ((Row*)ptr)[i]; } } } - public IEnumerator< Row > GetEnumerator() + public IEnumerator GetEnumerator() { - for( var i = 0; i < NumRows; ++i ) - { - yield return this[ i ]; - } + for (var i = 0; i < NumRows; ++i) + yield return this[i]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public ReadOnlySpan< byte > AsBytes() + public ReadOnlySpan AsBytes() { - fixed( byte* ptr = _rowData ) + fixed (byte* ptr = _rowData) { - return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size ); + return new ReadOnlySpan(ptr, NumRows * Row.Size); } } } @@ -154,73 +153,71 @@ public partial class MtrlFile : IWritable public ushort Template { - get => ( ushort )( _data >> 5 ); - set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) ); + get => (ushort)(_data >> 5); + set => _data = (ushort)((_data & 0x1F) | (value << 5)); } public bool Diffuse { - get => ( _data & 0x01 ) != 0; - set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE ); + get => (_data & 0x01) != 0; + set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); } public bool Specular { - get => ( _data & 0x02 ) != 0; - set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD ); + get => (_data & 0x02) != 0; + set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); } public bool Emissive { - get => ( _data & 0x04 ) != 0; - set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB ); + get => (_data & 0x04) != 0; + set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); } public bool Gloss { - get => ( _data & 0x08 ) != 0; - set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 ); + get => (_data & 0x08) != 0; + set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); } public bool SpecularStrength { - get => ( _data & 0x10 ) != 0; - set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF ); + get => (_data & 0x10) != 0; + set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); } } - public struct RowArray : IEnumerable< Row > + public struct RowArray : IEnumerable { public const int NumRows = 16; private fixed ushort _rowData[NumRows]; - public ref Row this[ int i ] + public ref Row this[int i] { get { - fixed( ushort* ptr = _rowData ) + fixed (ushort* ptr = _rowData) { - return ref ( ( Row* )ptr )[ i ]; + return ref ((Row*)ptr)[i]; } } } - public IEnumerator< Row > GetEnumerator() + public IEnumerator GetEnumerator() { - for( var i = 0; i < NumRows; ++i ) - { - yield return this[ i ]; - } + for (var i = 0; i < NumRows; ++i) + yield return this[i]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public ReadOnlySpan< byte > AsBytes() + public ReadOnlySpan AsBytes() { - fixed( ushort* ptr = _rowData ) + fixed (ushort* ptr = _rowData) { - return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) ); + return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); } } } @@ -262,10 +259,53 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; - public MtrlFile( byte[] data ) + public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { - using var stream = new MemoryStream( data ); - using var r = new BinaryReader( stream ); + if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) + return false; + + var dyeSet = ColorDyeSets[colorSetIdx].Rows[rowIdx]; + if (!stm.TryGetValue(dyeSet.Template, stainId, out var dyes)) + return false; + + var ret = false; + if (dyeSet.Diffuse && ColorSets[colorSetIdx].Rows[rowIdx].Diffuse != dyes.Diffuse) + { + ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = dyes.Diffuse; + ret = true; + } + + if (dyeSet.Specular && ColorSets[colorSetIdx].Rows[rowIdx].Specular != dyes.Specular) + { + ColorSets[colorSetIdx].Rows[rowIdx].Specular = dyes.Specular; + ret = true; + } + + if (dyeSet.SpecularStrength && ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength != dyes.SpecularPower) + { + ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = dyes.SpecularPower; + ret = true; + } + + if (dyeSet.Emissive && ColorSets[colorSetIdx].Rows[rowIdx].Emissive != dyes.Emissive) + { + ColorSets[colorSetIdx].Rows[rowIdx].Emissive = dyes.Emissive; + ret = true; + } + + if (dyeSet.Gloss && ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength != dyes.Gloss) + { + ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = dyes.Gloss; + ret = true; + } + + return ret; + } + + public MtrlFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var r = new BinaryReader(stream); Version = r.ReadUInt32(); r.ReadUInt16(); // file size @@ -277,39 +317,37 @@ public partial class MtrlFile : IWritable var colorSetCount = r.ReadByte(); var additionalDataSize = r.ReadByte(); - Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); - UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets ); - ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + Textures = ReadTextureOffsets(r, textureCount, out var textureOffsets); + UvSets = ReadUvSetOffsets(r, uvSetCount, out var uvOffsets); + ColorSets = ReadColorSetOffsets(r, colorSetCount, out var colorOffsets); - var strings = r.ReadBytes( stringTableSize ); - for( var i = 0; i < textureCount; ++i ) - { - Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] ); - } + var strings = r.ReadBytes(stringTableSize); + for (var i = 0; i < textureCount; ++i) + Textures[i].Path = UseOffset(strings, textureOffsets[i]); - for( var i = 0; i < uvSetCount; ++i ) - { - UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); - } + for (var i = 0; i < uvSetCount; ++i) + UvSets[i].Name = UseOffset(strings, uvOffsets[i]); - for( var i = 0; i < colorSetCount; ++i ) - { - ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); - } + for (var i = 0; i < colorSetCount; ++i) + ColorSets[i].Name = UseOffset(strings, colorOffsets[i]); ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize - ? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray() - : Array.Empty< ColorDyeSet >(); - - ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); - - AdditionalData = r.ReadBytes( additionalDataSize ); - for( var i = 0; i < ColorSets.Length; ++i ) - { - if( stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length ) + ? ColorSets.Select(c => new ColorDyeSet { - ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); - ColorSets[ i ].HasRows = true; + Index = c.Index, + Name = c.Name, + }).ToArray() + : Array.Empty(); + + ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset); + + AdditionalData = r.ReadBytes(additionalDataSize); + for (var i = 0; i < ColorSets.Length; ++i) + { + if (stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length) + { + ColorSets[i].Rows = r.ReadStructure(); + ColorSets[i].HasRows = true; } else { @@ -317,10 +355,8 @@ public partial class MtrlFile : IWritable } } - for( var i = 0; i < ColorDyeSets.Length; ++i ) - { - ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >(); - } + for (var i = 0; i < ColorDyeSets.Length; ++i) + ColorDyeSets[i].Rows = r.ReadStructure(); var shaderValueListSize = r.ReadUInt16(); var shaderKeyCount = r.ReadUInt16(); @@ -328,55 +364,55 @@ public partial class MtrlFile : IWritable var samplerCount = r.ReadUInt16(); ShaderPackage.Flags = r.ReadUInt32(); - ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount ); - ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount ); - ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount ); - ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 ); + ShaderPackage.ShaderKeys = r.ReadStructuresAsArray(shaderKeyCount); + ShaderPackage.Constants = r.ReadStructuresAsArray(constantCount); + ShaderPackage.Samplers = r.ReadStructuresAsArray(samplerCount); + ShaderPackage.ShaderValues = r.ReadStructuresAsArray(shaderValueListSize / 4); } - private static Texture[] ReadTextureOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new Texture[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Flags = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Flags = r.ReadUInt16(); } return ret; } - private static UvSet[] ReadUvSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static UvSet[] ReadUvSetOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new UvSet[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Index = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Index = r.ReadUInt16(); } return ret; } - private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static ColorSet[] ReadColorSetOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new ColorSet[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Index = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Index = r.ReadUInt16(); } return ret; } - private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset ) + private static string UseOffset(ReadOnlySpan strings, ushort offset) { - strings = strings[ offset.. ]; - var end = strings.IndexOf( ( byte )'\0' ); - return Encoding.UTF8.GetString( strings[ ..end ] ); + strings = strings[offset..]; + var end = strings.IndexOf((byte)'\0'); + return Encoding.UTF8.GetString(strings[..end]); } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs b/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs new file mode 100644 index 00000000..6da0ab2e --- /dev/null +++ b/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Lumina.Extensions; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Files; + +public partial class StmFile +{ + public readonly struct StainingTemplateEntry + { + /// + /// The number of stains is capped at 128 at the moment + /// + public const int NumElements = 128; + + // ColorSet row information for each stain. + public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries; + public readonly IReadOnlyList GlossEntries; + public readonly IReadOnlyList SpecularPowerEntries; + + public DyePack this[StainId idx] + => this[(int)idx.Value]; + + public DyePack this[int idx] + { + get + { + // The 0th index is skipped. + if (idx is <= 0 or > NumElements) + return default; + + --idx; + var (dr, dg, db) = DiffuseEntries[idx]; + var (sr, sg, sb) = SpecularEntries[idx]; + var (er, eg, eb) = EmissiveEntries[idx]; + var g = GlossEntries[idx]; + var sp = SpecularPowerEntries[idx]; + // Convert to DyePack using floats. + return new DyePack + { + Diffuse = new Vector3((float)dr, (float)dg, (float)db), + Specular = new Vector3((float)sr, (float)sg, (float)sb), + Emissive = new Vector3((float)er, (float)eg, (float)eb), + Gloss = (float)g, + SpecularPower = (float)sp, + }; + } + } + + private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) + { + br.Seek(offset); + var arraySize = size / entrySize; + // The actual amount of entries informs which type of list we use. + switch (arraySize) + { + case 0: return new RepeatingList(default!, NumElements); // All default + case 1: return new RepeatingList(read(br), NumElements); // All single entry + case NumElements: // 1-to-1 entries + var ret = new T[NumElements]; + for (var i = 0; i < NumElements; ++i) + ret[i] = read(br); + return ret; + // Indexed access. + case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize, NumElements, read); + // Should not happen. + case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); + } + } + + // Read functions + private static (Half, Half, Half) ReadTriple(BinaryReader br) + => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); + + private static Half ReadSingle(BinaryReader br) + => br.ReadHalf(); + + // Actually parse an entry. + public unsafe StainingTemplateEntry(BinaryReader br, int offset) + { + br.Seek(offset); + // 5 different lists of values. + Span ends = stackalloc ushort[5]; + for (var i = 0; i < ends.Length; ++i) + ends[i] = (ushort)(br.ReadUInt16() * 2); // because the ends are in terms of ushort. + offset += ends.Length * 2; + + DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 6); + SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 6); + EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 6); + GlossEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 2); + SpecularPowerEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 2); + } + + /// + /// Used if a single value is used for all entries of a list. + /// + private sealed class RepeatingList : IReadOnlyList + { + private readonly T _value; + public int Count { get; } + + public RepeatingList(T value, int size) + { + _value = value; + Count = size; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + yield return _value; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public T this[int index] + => index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException(); + } + + /// + /// Used if there is a small set of values for a bigger list, accessed via index information. + /// + private sealed class IndexedList : IReadOnlyList + { + private readonly T[] _values; + private readonly byte[] _indices; + + /// + /// Reads values from via , then reads byte indices. + /// + public IndexedList(BinaryReader br, int count, int indexCount, Func read) + { + _values = new T[count + 1]; + _indices = new byte[indexCount]; + _values[0] = default!; + for (var i = 1; i < count + 1; ++i) + _values[i] = read(br); + + // Seems to be an unused 0xFF byte marker. + // Necessary for correct offsets. + br.ReadByte(); + for (var i = 0; i < indexCount; ++i) + { + _indices[i] = br.ReadByte(); + if (_indices[i] > count) + _indices[i] = 0; + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < NumElements; ++i) + yield return _values[_indices[i]]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _indices.Length; + + public T this[int index] + => index >= 0 && index < Count ? _values[_indices[index]] : default!; + } + } +} diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs index 288c8600..7ee4f0d3 100644 --- a/Penumbra.GameData/Files/StmFile.cs +++ b/Penumbra.GameData/Files/StmFile.cs @@ -1,10 +1,8 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Numerics; using Dalamud.Data; -using Lumina.Extensions; using Penumbra.GameData.Structs; namespace Penumbra.GameData.Files; @@ -13,160 +11,58 @@ public partial class StmFile { public const string Path = "chara/base_material/stainingtemplate.stm"; + /// + /// All dye-able color set information for a row. + /// public record struct DyePack { public Vector3 Diffuse; public Vector3 Specular; public Vector3 Emissive; - public float SpecularPower; public float Gloss; + public float SpecularPower; } - public readonly struct StainingTemplateEntry - { - public const int NumElements = 128; - - public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries; - public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries; - public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries; - public readonly IReadOnlyList SpecularPowerEntries; - public readonly IReadOnlyList GlossEntries; - - public DyePack this[StainId idx] - => this[(int)idx.Value]; - - public DyePack this[int idx] - { - get - { - if (idx is <= 0 or > NumElements) - return default; - - --idx; - var (dr, dg, db) = DiffuseEntries[idx]; - var (sr, sg, sb) = SpecularEntries[idx]; - var (er, eg, eb) = EmissiveEntries[idx]; - var sp = SpecularPowerEntries[idx]; - var g = GlossEntries[idx]; - return new DyePack - { - Diffuse = new Vector3((float)dr, (float)dg, (float)db), - Emissive = new Vector3((float)sr, (float)sg, (float)sb), - Specular = new Vector3((float)er, (float)eg, (float)eb), - SpecularPower = (float)sp, - Gloss = (float)g, - }; - } - } - - private class RepeatingList : IReadOnlyList - { - private readonly T _value; - public int Count { get; } - - public RepeatingList(T value, int size) - { - _value = value; - Count = size; - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < Count; ++i) - yield return _value; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public T this[int index] - => index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException(); - } - - private class IndexedList : IReadOnlyList - { - private readonly T[] _values; - private readonly byte[] _indices; - - public IndexedList(BinaryReader br, int count, int indexCount, Func read, int entrySize) - { - _values = new T[count + 1]; - _indices = new byte[indexCount]; - _values[0] = default!; - for (var i = 1; i <= count; ++i) - _values[i] = read(br); - for (var i = 0; i < indexCount; ++i) - { - _indices[i] = br.ReadByte(); - if (_indices[i] > count) - _indices[i] = 0; - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumElements; ++i) - yield return _values[_indices[i]]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _indices.Length; - - public T this[int index] - => index >= 0 && index < Count ? _values[_indices[index]] : throw new IndexOutOfRangeException(); - } - - private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) - { - br.Seek(offset); - var arraySize = size / entrySize; - switch (arraySize) - { - case 0: return new RepeatingList(default!, NumElements); - case 1: return new RepeatingList(read(br), NumElements); - case NumElements: - var ret = new T[NumElements]; - for (var i = 0; i < NumElements; ++i) - ret[i] = read(br); - return ret; - case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize / 2, NumElements, read, entrySize); - case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); - } - } - - private static (Half, Half, Half) ReadTriple(BinaryReader br) - => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); - - private static Half ReadSingle(BinaryReader br) - => br.ReadHalf(); - - public unsafe StainingTemplateEntry(BinaryReader br, int offset) - { - br.Seek(offset); - Span ends = stackalloc ushort[5]; - for (var i = 0; i < ends.Length; ++i) - ends[i] = br.ReadUInt16(); - - offset += ends.Length * 2; - DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 3); - SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 3); - EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 3); - SpecularPowerEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 1); - GlossEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 1); - } - } - + /// + /// All currently available dyeing templates with their IDs. + /// public readonly IReadOnlyDictionary Entries; + /// + /// Access a specific dye pack. + /// + /// The ID of the accessed template. + /// The ID of the Stain. + /// The corresponding color set information or a defaulted DyePack of 0-entries. public DyePack this[ushort template, int idx] => Entries.TryGetValue(template, out var entry) ? entry[idx] : default; + /// public DyePack this[ushort template, StainId idx] => this[template, (int)idx.Value]; + /// + /// Try to access a specific dye pack. + /// + /// The ID of the accessed template. + /// The ID of the Stain. + /// On success, the corresponding color set information, otherwise a defaulted DyePack. + /// True on success, false otherwise. + public bool TryGetValue(ushort template, StainId idx, out DyePack dyes) + { + if (idx.Value is > 0 and <= StainingTemplateEntry.NumElements && Entries.TryGetValue(template, out var entry)) + { + dyes = entry[idx]; + return true; + } + + dyes = default; + return false; + } + + /// + /// Create a STM file from the given data array. + /// public StmFile(byte[] data) { using var stream = new MemoryStream(data); @@ -193,6 +89,9 @@ public partial class StmFile } } + /// + /// Try to read and parse the default STM file given by Lumina. + /// public StmFile(DataManager gameData) : this(gameData.GetFile(Path)?.Data ?? Array.Empty()) { } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs new file mode 100644 index 00000000..a4042065 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private class FileEditor< T > where T : class, IWritable + { + private readonly string _tabName; + private readonly string _fileType; + private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; + private readonly Func< T, bool, bool > _drawEdit; + private readonly Func< string > _getInitialPath; + + private Mod.Editor.FileRegistry? _currentPath; + private T? _currentFile; + private Exception? _currentException; + private bool _changed; + + private string _defaultPath = string.Empty; + private bool _inInput; + private T? _defaultFile; + private Exception? _defaultException; + + private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + + private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); + + public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, + Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) + { + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + _getInitialPath = getInitialPath; + } + + public void Draw() + { + _list = _getFiles(); + if( _list.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( _tabName ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + DrawFileSelectCombo(); + SaveButton(); + ImGui.SameLine(); + ResetButton(); + ImGui.SameLine(); + DefaultInput(); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + + DrawFilePanel(); + } + + private void DefaultInput() + { + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); + ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + _inInput = ImGui.IsItemActive(); + if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + { + _fileDialog.Reset(); + try + { + var file = Dalamud.GameData.GetFile( _defaultPath ); + if( file != null ) + { + _defaultException = null; + _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + } + else + { + _defaultFile = null; + _defaultException = new Exception( "File does not exist." ); + } + } + catch( Exception e ) + { + _defaultFile = null; + _defaultException = e; + } + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) + { + _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); + } + }, _getInitialPath() ); + } + + _fileDialog.Draw(); + } + + public void Reset() + { + _currentException = null; + _currentPath = null; + _currentFile = null; + _changed = false; + } + + private void DrawFileSelectCombo() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); + if( !combo ) + { + return; + } + + foreach( var file in _list ) + { + if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) + { + UpdateCurrentFile( file ); + } + } + } + + private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) + { + if( ReferenceEquals( _currentPath, path ) ) + { + return; + } + + _changed = false; + _currentPath = path; + _currentException = null; + try + { + var bytes = File.ReadAllBytes( _currentPath.File.FullName ); + _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; + } + catch( Exception e ) + { + _currentFile = null; + _currentException = e; + } + } + + private void SaveButton() + { + if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) + { + File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); + _changed = false; + } + } + + private void ResetButton() + { + if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed ) ) + { + var tmp = _currentPath; + _currentPath = null; + UpdateCurrentFile( tmp! ); + } + } + + private void DrawFilePanel() + { + using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); + if( !child ) + { + return; + } + + if( _currentPath != null ) + { + if( _currentFile == null ) + { + ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); + if( _currentException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _currentException.ToString() ); + } + } + else + { + using var id = ImRaii.PushId( 0 ); + _changed |= _drawEdit( _currentFile, false ); + } + } + + if( !_inInput && _defaultPath.Length > 0 ) + { + if( _currentPath != null ) + { + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); + ImGui.Separator(); + } + + if( _defaultFile == null ) + { + ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); + if( _defaultException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _defaultException.ToString() ); + } + } + else + { + using var id = ImRaii.PushId( 1 ); + _drawEdit( _defaultFile, true ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs similarity index 68% rename from Penumbra/UI/Classes/ModEditWindow.FileEdit.cs rename to Penumbra/UI/Classes/ModEditWindow.Materials.cs index 3f6ba35b..c59f3f6d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,20 +1,13 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; -using Lumina.Data.Parsing.Layer; using OtterGui; using OtterGui.Raii; -using OtterGui.Widgets; using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; -using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -23,263 +16,6 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { private readonly FileEditor< MtrlFile > _materialTab; - private readonly FileEditor< MdlFile > _modelTab; - - private class FileEditor< T > where T : class, IWritable - { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; - private readonly Func< T, bool, bool > _drawEdit; - private readonly Func< string > _getInitialPath; - - private Mod.Editor.FileRegistry? _currentPath; - private T? _currentFile; - private Exception? _currentException; - private bool _changed; - - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; - - private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; - - private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); - - public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) - { - _tabName = tabName; - _fileType = fileType; - _getFiles = getFiles; - _drawEdit = drawEdit; - _getInitialPath = getInitialPath; - } - - public void Draw() - { - _list = _getFiles(); - if( _list.Count == 0 ) - { - return; - } - - using var tab = ImRaii.TabItem( _tabName ); - if( !tab ) - { - return; - } - - ImGui.NewLine(); - DrawFileSelectCombo(); - SaveButton(); - ImGui.SameLine(); - ResetButton(); - ImGui.SameLine(); - DefaultInput(); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - - DrawFilePanel(); - } - - private void DefaultInput() - { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); - ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); - _inInput = ImGui.IsItemActive(); - if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) - { - _fileDialog.Reset(); - try - { - var file = Dalamud.GameData.GetFile( _defaultPath ); - if( file != null ) - { - _defaultException = null; - _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; - } - else - { - _defaultFile = null; - _defaultException = new Exception( "File does not exist." ); - } - } - catch( Exception e ) - { - _defaultFile = null; - _defaultException = e; - } - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) - { - _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); - } - }, _getInitialPath() ); - } - - _fileDialog.Draw(); - } - - public void Reset() - { - _currentException = null; - _currentPath = null; - _currentFile = null; - _changed = false; - } - - private void DrawFileSelectCombo() - { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); - if( !combo ) - { - return; - } - - foreach( var file in _list ) - { - if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) - { - UpdateCurrentFile( file ); - } - } - } - - private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) - { - if( ReferenceEquals( _currentPath, path ) ) - { - return; - } - - _changed = false; - _currentPath = path; - _currentException = null; - try - { - var bytes = File.ReadAllBytes( _currentPath.File.FullName ); - _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; - } - catch( Exception e ) - { - _currentFile = null; - _currentException = e; - } - } - - private void SaveButton() - { - if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) - { - File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); - _changed = false; - } - } - - private void ResetButton() - { - if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed ) ) - { - var tmp = _currentPath; - _currentPath = null; - UpdateCurrentFile( tmp! ); - } - } - - private void DrawFilePanel() - { - using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); - if( !child ) - { - return; - } - - if( _currentPath != null ) - { - if( _currentFile == null ) - { - ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); - if( _currentException != null ) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _currentException.ToString() ); - } - } - else - { - using var id = ImRaii.PushId( 0 ); - _changed |= _drawEdit( _currentFile, false ); - } - } - - if( !_inInput && _defaultPath.Length > 0 ) - { - if( _currentPath != null ) - { - ImGui.NewLine(); - ImGui.NewLine(); - ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); - ImGui.Separator(); - } - - if( _defaultFile == null ) - { - ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); - if( _defaultException != null ) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _defaultException.ToString() ); - } - } - else - { - using var id = ImRaii.PushId( 1 ); - _drawEdit( _defaultFile, true ); - } - } - } - } - - private static bool DrawModelPanel( MdlFile file, bool disabled ) - { - var ret = false; - for( var i = 0; i < file.Materials.Length; ++i ) - { - using var id = ImRaii.PushId( i ); - var tmp = file.Materials[ i ]; - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != file.Materials[ i ] ) - { - file.Materials[ i ] = tmp; - ret = true; - } - } - - return !disabled && ret; - } - private static bool DrawMaterialPanel( MtrlFile file, bool disabled ) { @@ -330,11 +66,9 @@ public partial class ModEditWindow ImGui.SameLine(); var ret = ColorSetPasteAllClipboardButton( file, 0 ); ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 10, 0 ) ); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); ImGui.SameLine(); - Penumbra.StainManager.StainCombo.Draw( "Preview Dye", Penumbra.StainManager.StainCombo.CurrentSelection.Value.Color, true ); - ImGui.SameLine(); - ImGui.Button( "Apply Preview Dyes." ); + ret |= DrawPreviewDye( file, disabled ); using var table = ImRaii.Table( "##ColorSets", 11, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); @@ -503,6 +237,30 @@ public partial class ModEditWindow } } + private static bool DrawPreviewDye( MtrlFile file, bool disabled ) + { + var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + { + var ret = false; + for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + { + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + } + } + + return ret; + } + + ImGui.SameLine(); + var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; + Penumbra.StainManager.StainCombo.Draw( label, dyeColor, true ); + return false; + } + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) { if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) @@ -744,7 +502,7 @@ public partial class ModEditWindow if( hasDye ) { if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; ret = true; @@ -753,23 +511,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.TableNextColumn(); - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain != 0 && Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { - var values = entry[ ( int )stain ]; - using var _ = ImRaii.Disabled(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, c => { } ); - ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, c => { } ); - ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, c => { } ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss ); - } + ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); } else { @@ -780,7 +522,40 @@ public partial class ModEditWindow return ret; } - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter ) + private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + { + var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + { + return false; + } + + var values = entry[ ( int )stain ]; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + + var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), + "Apply the selected dye to this row.", disabled, true ); + + ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + + ImGui.SameLine(); + ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ImGui.SameLine(); + ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ImGui.SameLine(); + ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ImGui.SameLine(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; var tmp = input; @@ -792,6 +567,14 @@ public partial class ModEditWindow ret = true; } + if( letter.Length > 0 && ImGui.IsItemVisible() ) + { + var textSize = ImGui.CalcTextSize( letter ); + var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + } + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); return ret; diff --git a/Penumbra/UI/Classes/ModEditWindow.Models.cs b/Penumbra/UI/Classes/ModEditWindow.Models.cs new file mode 100644 index 00000000..5b6ad685 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Models.cs @@ -0,0 +1,31 @@ +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileEditor< MdlFile > _modelTab; + + private static bool DrawModelPanel( MdlFile file, bool disabled ) + { + var ret = false; + for( var i = 0; i < file.Materials.Length; ++i ) + { + using var id = ImRaii.PushId( i ); + var tmp = file.Materials[ i ]; + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) + && tmp.Length > 0 + && tmp != file.Materials[ i ] ) + { + file.Materials[ i ] = tmp; + ret = true; + } + } + + return !disabled && ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 83d58be4..bbe21025 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -38,7 +38,7 @@ public partial class ModEditWindow : Window, IDisposable SizeConstraints = new WindowSizeConstraints { - MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ), + MinimumSize = new Vector2( 1240, 600 ), MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear(); From 0444c2818740ec2f781fe4f49b831658be3f3555 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 15 Nov 2022 21:07:14 +0100 Subject: [PATCH 0575/2451] More Actor stuff. --- .../Collections/CollectionManager.Active.cs | 120 +++------- Penumbra/Collections/IndividualCollections.cs | 225 ++++++++++++++++++ Penumbra/Penumbra.cs | 30 ++- Penumbra/UI/ConfigWindow.cs | 32 ++- 4 files changed, 302 insertions(+), 105 deletions(-) create mode 100644 Penumbra/Collections/IndividualCollections.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 99ba3398..1afcdf3f 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,102 +8,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using Dalamud.Game.ClientState.Objects.Enums; -using Penumbra.GameData.Actors; -using Penumbra.String; namespace Penumbra.Collections; -public class IndividualCollections -{ - private readonly ActorManager _manager; - private readonly List< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > _assignments = new(); - private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); - - public IReadOnlyList< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > Assignments - => _assignments; - - public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals - => _individuals; - - public IndividualCollections( ActorManager manager ) - => _manager = manager; - - public bool CanAdd( ActorIdentifier identifier ) - => identifier.IsValid && !Individuals.ContainsKey( identifier ); - - public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) - { - identifiers = Array.Empty< ActorIdentifier >(); - - switch( type ) - { - case IdentifierType.Player: - { - if( !ByteString.FromString( name, out var playerName ) ) - { - return false; - } - - var identifier = _manager.CreatePlayer( playerName, homeWorld ); - if( !CanAdd( identifier ) ) - { - return false; - } - - identifiers = new[] { identifier }; - return true; - } - //case IdentifierType.Owned: - //{ - // if( !ByteString.FromString( name, out var ownerName ) ) - // { - // return false; - // } - // - // identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); - // return - // identifier = _manager.CreateIndividual( type, byteName, homeWorld, kind, dataId ); - // return CanAdd( identifier ); - //} - //case IdentifierType.Npc: - //{ - // identifier = _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, dataId ); - // return CanAdd( identifier ); - //} - default: - identifiers = Array.Empty< ActorIdentifier >(); - return false; - } - } - - public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection ) - => Add( displayName, identifier, collection, Array.Empty< uint >() ); - - public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection, IEnumerable< uint > additionalIds ) - { - if( Individuals.ContainsKey( identifier ) ) - { - return false; - } - - //var identifiers = additionalIds - // .Select( id => CanAdd( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, id, out var value ) ? value : ActorIdentifier.Invalid ) - // .Prepend( identifier ) - // .ToArray(); - //if( identifiers.Any( i => !i.IsValid || i.DataId == identifier.DataId ) ) - //{ - // return false; - //} - - return true; - } -} - public partial class ModCollection { public sealed partial class Manager { + public const int Version = 1; + // Is invoked after the collections actually changed. public event CollectionChangeDelegate CollectionChanged; @@ -406,6 +319,35 @@ public partial class ModCollection jObject.WriteTo( j ); } + // Migrate individual collections to Identifiers for 0.6.0. + private bool MigrateIndividualCollections(JObject jObject, out IndividualCollections collections) + { + var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; + collections = new IndividualCollections( Penumbra.Actors ); + if( version > 0 ) + return false; + + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject[nameof( Characters )]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary< string, ModCollection >( characters.Count ); + foreach( var (player, collectionName) in characters ) + { + var idx = GetIndexForCollectionName( collectionName ); + if( idx < 0 ) + { + Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); + dict.Add( player, Empty ); + } + else + { + dict.Add( player, this[idx] ); + } + } + + collections.Migrate0To1( dict ); + return true; + } + public void SaveActiveCollections() { diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs new file mode 100644 index 00000000..c6516f2c --- /dev/null +++ b/Penumbra/Collections/IndividualCollections.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Actors; +using Penumbra.String; + +namespace Penumbra.Collections; + +public partial class IndividualCollections +{ + public const int Version = 1; + + internal void Migrate0To1( Dictionary< string, ModCollection > old ) + { + foreach( var (name, collection) in old ) + { + if( ActorManager.VerifyPlayerName( name ) ) + { + var identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); + if( Add( name, new[] { identifier }, collection ) ) + { + var shortName = string.Join( " ", name.Split().Select( n => $"{n[0]}." ) ); + Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); + continue; + } + } + + } + } +} + +public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +{ + private readonly ActorManager _manager; + private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); + private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); + + public IReadOnlyDictionary< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments + => _assignments; + + public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals + => _individuals; + + public IndividualCollections( ActorManager manager ) + => _manager = manager; + + public bool CanAdd( params ActorIdentifier[] identifiers ) + => identifiers.Length > 0 && identifiers.All( i => i.IsValid && !Individuals.ContainsKey( i ) ); + + public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) + { + identifiers = Array.Empty< ActorIdentifier >(); + + switch( type ) + { + case IdentifierType.Player: + if( !ByteString.FromString( name, out var playerName ) ) + { + return false; + } + + var identifier = _manager.CreatePlayer( playerName, homeWorld ); + identifiers = new[] { identifier }; + break; + case IdentifierType.Owned: + if( !ByteString.FromString( name, out var ownerName ) ) + { + return false; + } + + identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); + break; + case IdentifierType.Npc: + identifiers = dataIds.Select( id => _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray(); + break; + default: + identifiers = Array.Empty< ActorIdentifier >(); + break; + } + + return CanAdd( identifiers ); + } + + public ActorIdentifier[] GetGroup( ActorIdentifier identifier ) + { + if( !identifier.IsValid ) + { + return Array.Empty< ActorIdentifier >(); + } + + static ActorIdentifier[] CreateNpcs( ActorManager manager, ActorIdentifier identifier ) + { + var name = manager.ToName( identifier.Kind, identifier.DataId ); + var table = identifier.Kind switch + { + ObjectKind.BattleNpc => manager.BNpcs, + ObjectKind.EventNpc => manager.ENpcs, + ObjectKind.Companion => manager.Companions, + ObjectKind.MountType => manager.Mounts, + _ => throw new NotImplementedException(), + }; + return table.Where( kvp => kvp.Value == name ) + .Select( kvp => manager.CreateIndividual( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); + } + + return identifier.Type switch + { + IdentifierType.Player => new[] { identifier.CreatePermanent() }, + IdentifierType.Special => new[] { identifier }, + IdentifierType.Owned => CreateNpcs( _manager, identifier.CreatePermanent() ), + IdentifierType.Npc => CreateNpcs( _manager, identifier ), + _ => Array.Empty< ActorIdentifier >(), + }; + } + + public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + { + if( !CanAdd( identifiers ) || _assignments.ContainsKey( displayName ) ) + { + return false; + } + + _assignments.Add( displayName, ( identifiers, collection ) ); + foreach( var identifier in identifiers ) + { + _individuals.Add( identifier, collection ); + } + + return true; + } + + public bool ChangeCollection( string displayName, ModCollection newCollection ) + { + var displayIndex = _assignments.IndexOfKey( displayName ); + return ChangeCollection( displayIndex, newCollection ); + } + + public bool ChangeCollection( int displayIndex, ModCollection newCollection ) + { + if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments.Values[ displayIndex ].Collection == newCollection ) + { + return false; + } + + _assignments.Values[ displayIndex ] = _assignments.Values[ displayIndex ] with { Collection = newCollection }; + foreach( var identifier in _assignments.Values[ displayIndex ].Identifiers ) + { + _individuals[ identifier ] = newCollection; + } + + return true; + } + + public bool Delete( string displayName ) + { + var displayIndex = _assignments.IndexOfKey( displayName ); + return Delete( displayIndex ); + } + + public bool Delete( int displayIndex ) + { + if( displayIndex < 0 || displayIndex >= _assignments.Count ) + { + return false; + } + + var (identifiers, _) = _assignments.Values[ displayIndex ]; + _assignments.RemoveAt( displayIndex ); + foreach( var identifier in identifiers ) + { + _individuals.Remove( identifier ); + } + + return true; + } + + public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() + => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _assignments.Count; + + public (string DisplayName, ModCollection Collection) this[ int index ] + => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); + + public bool TryGetCollection( ActorIdentifier identifier, out ModCollection? collection ) + { + collection = null; + if( !identifier.IsValid ) + { + return false; + } + + if( _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + if( identifier.Type is not (IdentifierType.Player or IdentifierType.Owned) ) + { + return false; + } + + identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + return false; + } + + public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + + public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d266339b..a2f167ce 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -8,6 +8,7 @@ using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using Dalamud.Utility; using EmbedIO; using EmbedIO.WebApi; using ImGuiNET; @@ -34,6 +35,7 @@ namespace Penumbra; public class Penumbra : IDalamudPlugin { + public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; public string Name => "Penumbra"; @@ -46,6 +48,7 @@ public class Penumbra : IDalamudPlugin public static bool DevPenumbraExists; public static bool IsNotInstalledPenumbra; + public static bool IsValidSourceRepo; public static Logger Log { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; @@ -84,11 +87,12 @@ public class Penumbra : IDalamudPlugin { Dalamud.Initialize( pluginInterface ); Log = new Logger(); + DevPenumbraExists = CheckDevPluginPenumbra(); + IsNotInstalledPenumbra = CheckIsNotInstalled(); + IsValidSourceRepo = CheckSourceRepo(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); - DevPenumbraExists = CheckDevPluginPenumbra(); - IsNotInstalledPenumbra = CheckIsNotInstalled(); Framework = new FrameworkManager(); CharacterUtility = new CharacterUtility(); @@ -153,9 +157,9 @@ public class Penumbra : IDalamudPlugin } else { - Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); + Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); } - + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); @@ -561,7 +565,7 @@ public class Penumbra : IDalamudPlugin #endif } - // Check if the loaded version of penumbra itself is in devPlugins. + // Check if the loaded version of Penumbra itself is in devPlugins. private static bool CheckIsNotInstalled() { #if !DEBUG @@ -572,6 +576,22 @@ public class Penumbra : IDalamudPlugin return !ret; #else return false; +#endif + } + + // Check if the loaded version of Penumbra is installed from a valid source repo. + private static bool CheckSourceRepo() + { +#if !DEBUG + return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch + { + null => false, + Repository => true, + "https://raw.githubusercontent.com/xivdev/Penumbra/test/repo.json" => true, + _ => false, + }; +#else + return true; #endif } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index e9ea7eff..afe8c9be 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -59,7 +59,14 @@ public sealed partial class ConfigWindow : Window, IDisposable DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" - + "Please use the Launcher's Repair Game Files function to repair your client installation." ); + + "Please use the Launcher's Repair Game Files function to repair your client installation.", true ); + } + else if( !Penumbra.IsValidSourceRepo ) + { + DrawProblemWindow( + $"You are loading a release version of Penumbra from the repository \"{Dalamud.PluginInterface.SourceRepository}\" instead of the official repository.\n" + + $"Please use the official repository at {Penumbra.Repository}.\n\n" + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); } else if( Penumbra.IsNotInstalledPenumbra ) { @@ -67,7 +74,7 @@ public sealed partial class ConfigWindow : Window, IDisposable $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." ); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); } else if( Penumbra.DevPenumbraExists ) { @@ -75,7 +82,7 @@ public sealed partial class ConfigWindow : Window, IDisposable $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" - + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode." ); + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); } else { @@ -96,12 +103,12 @@ public sealed partial class ConfigWindow : Window, IDisposable } } - private static void DrawProblemWindow( string text ) + private static void DrawProblemWindow( string text, bool withExceptions ) { using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); ImGui.NewLine(); ImGui.NewLine(); - ImGui.TextWrapped( text ); + ImGuiUtil.TextWrapped( text ); color.Pop(); ImGui.NewLine(); @@ -112,14 +119,17 @@ public sealed partial class ConfigWindow : Window, IDisposable ImGui.NewLine(); ImGui.NewLine(); - ImGui.TextUnformatted( "Exceptions" ); - ImGui.Separator(); - using var box = ImRaii.ListBox( "##Exceptions", new Vector2(-1, -1) ); - foreach( var exception in Penumbra.ImcExceptions ) + if( withExceptions ) { - ImGuiUtil.TextWrapped( exception.ToString() ); + ImGui.TextUnformatted( "Exceptions" ); ImGui.Separator(); - ImGui.NewLine(); + using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) ); + foreach( var exception in Penumbra.ImcExceptions ) + { + ImGuiUtil.TextWrapped( exception.ToString() ); + ImGui.Separator(); + ImGui.NewLine(); + } } } From bda3c1f1ac4592030aca265f3e4ea867f121ae5d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Nov 2022 15:33:41 +0100 Subject: [PATCH 0576/2451] Continued work on actor identification, migration seems to work. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 37 +++++- Penumbra.GameData/Actors/ActorManager.Data.cs | 64 +++++++++- .../Actors/ActorManager.Identifiers.cs | 33 +++-- .../Collections/CollectionManager.Active.cs | 15 +-- Penumbra/Collections/CollectionType.cs | 11 +- .../IndividualCollections.Access.cs | 117 ++++++++++++++++++ .../IndividualCollections.Files.cs | 89 +++++++++++++ Penumbra/Collections/IndividualCollections.cs | 109 +++++----------- Penumbra/Penumbra.cs | 8 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 19 +++ Penumbra/Util/ChatUtil.cs | 20 +++ 11 files changed, 407 insertions(+), 115 deletions(-) create mode 100644 Penumbra/Collections/IndividualCollections.Access.cs create mode 100644 Penumbra/Collections/IndividualCollections.Files.cs diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 7872bb65..14742b9c 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -59,7 +59,7 @@ public readonly struct ActorIdentifier : IEquatable { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", - IdentifierType.Special => ActorManager.ToName(Special), + IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind} #{DataId}" @@ -147,4 +147,39 @@ public static class ActorManagerExtensions _ => false, }; } + + public static string ToName(this ObjectKind kind) + => kind switch + { + ObjectKind.None => "Unknown", + ObjectKind.BattleNpc => "Battle NPC", + ObjectKind.EventNpc => "Event NPC", + ObjectKind.MountType => "Mount", + ObjectKind.Companion => "Companion", + _ => kind.ToString(), + }; + + public static string ToName(this IdentifierType type) + => type switch + { + IdentifierType.Player => "Player", + IdentifierType.Owned => "Owned NPC", + IdentifierType.Special => "Special Actor", + IdentifierType.Npc => "NPC", + _ => "Invalid", + }; + + /// + /// Fixed names for special actors. + /// + public static string ToName(this SpecialActor actor) + => actor switch + { + SpecialActor.CharacterScreen => "Character Screen Actor", + SpecialActor.ExamineScreen => "Examine Screen Actor", + SpecialActor.FittingRoom => "Fitting Room Actor", + SpecialActor.DyePreview => "Dye Preview Actor", + SpecialActor.Portrait => "Portrait Actor", + _ => "Invalid", + }; } diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index c00e6dd2..a1ef4b68 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -6,10 +6,17 @@ using Dalamud; using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; using Dalamud.Plugin; using Dalamud.Utility; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Excel; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Data; +using Penumbra.String; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.GameData.Actors; @@ -30,16 +37,17 @@ public sealed partial class ActorManager : DataSharer /// Valid ENPC names in title case by ENPC id. public IReadOnlyDictionary ENpcs { get; } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, Func toParentIdx) - : this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx) + : this(pluginInterface, objects, state, gameData, gameGui, gameData.Language, toParentIdx) { } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, ClientLanguage language, Func toParentIdx) : base(pluginInterface, language, 1) { _objects = objects; + _gameGui = gameGui; _clientState = state; _toParentIdx = toParentIdx; @@ -50,6 +58,39 @@ public sealed partial class ActorManager : DataSharer ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); ActorIdentifier.Manager = this; + + SignatureHelper.Initialise(this); + } + + public unsafe ActorIdentifier GetCurrentPlayer() + { + var address = (Character*)(_objects[0]?.Address ?? IntPtr.Zero); + return address == null ? ActorIdentifier.Invalid : CreatePlayer(new ByteString(address->GameObject.Name), address->HomeWorld); + } + + public ActorIdentifier GetInspectPlayer() + { + var addon = _gameGui.GetAddonByName("CharacterInspect", 1); + if (addon == IntPtr.Zero) + return ActorIdentifier.Invalid; + + return CreatePlayer(InspectName, InspectWorldId); + } + + public unsafe ActorIdentifier GetCardPlayer() + { + var agent = AgentCharaCard.Instance(); + if (agent == null || agent->Data == null) + return ActorIdentifier.Invalid; + + var worldId = *(ushort*)((byte*)agent->Data + 0xC0); + return CreatePlayer(new ByteString(agent->Data->Name.StringPtr), worldId); + } + + public ActorIdentifier GetGlamourPlayer() + { + var addon = _gameGui.GetAddonByName("MiragePrismMiragePlate", 1); + return addon == IntPtr.Zero ? ActorIdentifier.Invalid : GetCurrentPlayer(); } protected override void DisposeInternal() @@ -66,6 +107,7 @@ public sealed partial class ActorManager : DataSharer private readonly ObjectTable _objects; private readonly ClientState _clientState; + private readonly GameGui _gameGui; private readonly Func _toParentIdx; @@ -93,4 +135,20 @@ public sealed partial class ActorManager : DataSharer => gameData.GetExcelSheet(Language)! .Where(e => e.Singular.RawData.Length > 0) .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + + + [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] + private static unsafe ushort* _inspectTitleId = null!; + + [Signature("0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0", ScanType = ScanType.StaticAddress)] + private static unsafe ushort* _inspectWorldId = null!; + + private static unsafe ushort InspectTitleId + => *_inspectTitleId; + + private static unsafe ByteString InspectName + => new((byte*)(_inspectWorldId + 1)); + + private static unsafe ushort InspectWorldId + => *_inspectWorldId; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index eb2a73b4..6b5ebb25 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -51,6 +51,12 @@ public partial class ActorManager } } + /// + /// Return the world name including the All Worlds option. + /// + public string ToWorldName(ushort worldId) + => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; + /// /// Use stored data to convert an ActorIdentifier to a string. /// @@ -59,12 +65,12 @@ public partial class ActorManager return id.Type switch { IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})" + ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" : id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})'s {ToName(id.Kind, id.DataId)}" - : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", - IdentifierType.Special => ToName(id.Special), + ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" + : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", + IdentifierType.Special => id.Special.ToName(), IdentifierType.Npc => id.Index == ushort.MaxValue ? ToName(id.Kind, id.DataId) @@ -74,20 +80,6 @@ public partial class ActorManager } - /// - /// Fixed names for special actors. - /// - public static string ToName(SpecialActor actor) - => actor switch - { - SpecialActor.CharacterScreen => "Character Screen Actor", - SpecialActor.ExamineScreen => "Examine Screen Actor", - SpecialActor.FittingRoom => "Fitting Room Actor", - SpecialActor.DyePreview => "Dye Preview Actor", - SpecialActor.Portrait => "Portrait Actor", - _ => "Invalid", - }; - /// /// Convert a given ID for a certain ObjectKind to a name. /// @@ -328,7 +320,7 @@ public partial class ActorManager /// Checks if the world is a valid public world or ushort.MaxValue (any world). public bool VerifyWorld(ushort worldId) - => Worlds.ContainsKey(worldId); + => worldId == ushort.MaxValue || Worlds.ContainsKey(worldId); /// Verify that the enum value is a specific actor and return the name if it is. public static bool VerifySpecial(SpecialActor actor) @@ -339,6 +331,7 @@ public partial class ActorManager { return index switch { + ushort.MaxValue => true, < 200 => index % 2 == 0, > (ushort)SpecialActor.Portrait => index < 426, _ => false, @@ -360,6 +353,8 @@ public partial class ActorManager public bool VerifyNpcData(ObjectKind kind, uint dataId) => kind switch { + ObjectKind.MountType => Mounts.ContainsKey(dataId), + ObjectKind.Companion => Companions.ContainsKey(dataId), ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), _ => false, diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 1afcdf3f..836ed3ec 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -37,6 +37,7 @@ public partial class ModCollection // The list of character collections. private readonly Dictionary< string, ModCollection > _characters = new(); + public readonly IndividualCollections Individuals = new(Penumbra.Actors); public IReadOnlyDictionary< string, ModCollection > Characters => _characters; @@ -288,6 +289,8 @@ public partial class ModCollection { SaveActiveCollections(); } + + MigrateIndividualCollections( jObject ); } // Migrate ungendered collections to Male and Female for 0.5.9.0. @@ -320,13 +323,12 @@ public partial class ModCollection } // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject, out IndividualCollections collections) + private bool MigrateIndividualCollections(JObject jObject) { var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; - collections = new IndividualCollections( Penumbra.Actors ); if( version > 0 ) return false; - + // Load character collections. If a player name comes up multiple times, the last one is applied. var characters = jObject[nameof( Characters )]?.ToObject>() ?? new Dictionary(); var dict = new Dictionary< string, ModCollection >( characters.Count ); @@ -340,15 +342,14 @@ public partial class ModCollection } else { - dict.Add( player, this[idx] ); + dict.Add( player, this[ idx ] ); } } - - collections.Migrate0To1( dict ); + + Individuals.Migrate0To1( dict ); return true; } - public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 01ab5ab0..9e9c0f61 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -94,11 +94,12 @@ public enum CollectionType : byte MaleVeenaNpc, FemaleVeenaNpc, - Inactive, // A collection was added or removed - Default, // The default collection was changed - Interface, // The ui collection was changed - Character, // A character collection was changed - Current, // The current collection was changed + Inactive, // A collection was added or removed + Default, // The default collection was changed + Interface, // The ui collection was changed + Character, // A character collection was changed + Individual, // An Individual collection was changed + Current, // The current collection was changed } public static class CollectionTypeExtensions diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs new file mode 100644 index 00000000..0d295c3b --- /dev/null +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.GameData.Actors; + +namespace Penumbra.Collections; + +public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +{ + public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() + => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _assignments.Count; + + public (string DisplayName, ModCollection Collection) this[ int index ] + => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); + + public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) + { + switch( identifier.Type ) + { + case IdentifierType.Player: return CheckWorlds( identifier, out collection ); + case IdentifierType.Owned: + { + if( CheckWorlds( identifier, out collection! ) ) + { + return true; + } + + // Handle generic NPC + var npcIdentifier = _manager.CreateNpc( identifier.Kind, identifier.DataId ); + if( npcIdentifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + // Handle Ownership. + if( Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + identifier = _manager.CreatePlayer( identifier.PlayerName, identifier.HomeWorld ); + return CheckWorlds( identifier, out collection ); + } + + return false; + } + case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); + case IdentifierType.Special: + switch( identifier.Special ) + { + case SpecialActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: + case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: + return CheckWorlds( _manager.GetCurrentPlayer(), out collection ); + case SpecialActor.ExamineScreen: + { + if( CheckWorlds( _manager.GetInspectPlayer(), out collection! ) ) + { + return true; + } + + if( CheckWorlds( _manager.GetCardPlayer(), out collection! ) ) + { + return true; + } + + if( CheckWorlds( _manager.GetGlamourPlayer(), out collection! ) ) + { + return true; + } + + break; + } + } + + break; + } + + collection = null; + return false; + } + + public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + + public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + + private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) + { + if( !identifier.IsValid ) + { + collection = null; + return false; + } + + if( _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + collection = null; + return false; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs new file mode 100644 index 00000000..8c0997f0 --- /dev/null +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.GameData.Actors; +using Penumbra.String; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public partial class IndividualCollections +{ + public const int Version = 1; + + internal void Migrate0To1( Dictionary< string, ModCollection > old ) + { + static bool FindDataId( string name, IReadOnlyDictionary< uint, string > data, out uint dataId ) + { + var kvp = data.FirstOrDefault( kvp => kvp.Value.Equals( name, StringComparison.OrdinalIgnoreCase ), + new KeyValuePair< uint, string >( uint.MaxValue, string.Empty ) ); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + foreach( var (name, collection) in old ) + { + var kind = ObjectKind.None; + var lowerName = name.ToLowerInvariant(); + // Prefer matching NPC names, fewer false positives than preferring players. + if( FindDataId( lowerName, _manager.Companions, out var dataId ) ) + { + kind = ObjectKind.Companion; + } + else if( FindDataId( lowerName, _manager.Mounts, out dataId ) ) + { + kind = ObjectKind.MountType; + } + else if( FindDataId( lowerName, _manager.BNpcs, out dataId ) ) + { + kind = ObjectKind.BattleNpc; + } + else if( FindDataId( lowerName, _manager.ENpcs, out dataId ) ) + { + kind = ObjectKind.EventNpc; + } + + var identifier = _manager.CreateNpc( kind, dataId ); + if( identifier.IsValid ) + { + // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. + var group = GetGroup( identifier ); + var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); + if( Add( $"{_manager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) + { + Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); + } + else + { + ChatUtil.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + // If it is not a valid NPC name, check if it can be a player name. + else if( ActorManager.VerifyPlayerName( name ) ) + { + identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); + var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); + // Try to migrate the player name without logging full names. + if( Add( $"{name} ({_manager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) + { + Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); + } + else + { + ChatUtil.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + else + { + ChatUtil.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index c6516f2c..86a7410c 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -1,39 +1,13 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; using Penumbra.String; namespace Penumbra.Collections; -public partial class IndividualCollections -{ - public const int Version = 1; - - internal void Migrate0To1( Dictionary< string, ModCollection > old ) - { - foreach( var (name, collection) in old ) - { - if( ActorManager.VerifyPlayerName( name ) ) - { - var identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); - if( Add( name, new[] { identifier }, collection ) ) - { - var shortName = string.Join( " ", name.Split().Select( n => $"{n[0]}." ) ); - Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); - continue; - } - } - - } - } -} - -public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +public sealed partial class IndividualCollections { private readonly ActorManager _manager; private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); @@ -48,10 +22,34 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public IndividualCollections( ActorManager manager ) => _manager = manager; - public bool CanAdd( params ActorIdentifier[] identifiers ) - => identifiers.Length > 0 && identifiers.All( i => i.IsValid && !Individuals.ContainsKey( i ) ); + public enum AddResult + { + Valid, + AlreadySet, + Invalid, + } - public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) + public AddResult CanAdd( params ActorIdentifier[] identifiers ) + { + if( identifiers.Length == 0 ) + { + return AddResult.Invalid; + } + + if( identifiers.Any( i => !i.IsValid ) ) + { + return AddResult.Invalid; + } + + if( identifiers.Any( Individuals.ContainsKey ) ) + { + return AddResult.AlreadySet; + } + + return AddResult.Valid; + } + + public AddResult CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) { identifiers = Array.Empty< ActorIdentifier >(); @@ -60,7 +58,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case IdentifierType.Player: if( !ByteString.FromString( name, out var playerName ) ) { - return false; + return AddResult.Invalid; } var identifier = _manager.CreatePlayer( playerName, homeWorld ); @@ -69,7 +67,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case IdentifierType.Owned: if( !ByteString.FromString( name, out var ownerName ) ) { - return false; + return AddResult.Invalid; } identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); @@ -119,7 +117,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) { - if( !CanAdd( identifiers ) || _assignments.ContainsKey( displayName ) ) + if( CanAdd( identifiers ) != AddResult.Valid || _assignments.ContainsKey( displayName ) ) { return false; } @@ -177,49 +175,4 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - - public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() - => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _assignments.Count; - - public (string DisplayName, ModCollection Collection) this[ int index ] - => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); - - public bool TryGetCollection( ActorIdentifier identifier, out ModCollection? collection ) - { - collection = null; - if( !identifier.IsValid ) - { - return false; - } - - if( _individuals.TryGetValue( identifier, out collection ) ) - { - return true; - } - - if( identifier.Type is not (IdentifierType.Player or IdentifierType.Owned) ) - { - return false; - } - - identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); - if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) - { - return true; - } - - return false; - } - - public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); - - public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a2f167ce..6744462d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -36,6 +36,7 @@ namespace Penumbra; public class Penumbra : IDalamudPlugin { public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public string Name => "Penumbra"; @@ -93,6 +94,7 @@ public class Penumbra : IDalamudPlugin Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); + Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); Framework = new FrameworkManager(); CharacterUtility = new CharacterUtility(); @@ -112,7 +114,6 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -296,6 +297,9 @@ public class Penumbra : IDalamudPlugin WebServer = null; } + private short ResolveCutscene( ushort index ) + => ( short )PathResolver.CutsceneActor( index ); + public void Dispose() { StainManager?.Dispose(); @@ -586,7 +590,7 @@ public class Penumbra : IDalamudPlugin return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch { null => false, - Repository => true, + Repository => true, "https://raw.githubusercontent.com/xivdev/Penumbra/test/repo.json" => true, _ => false, }; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 5cf59e80..1818fbe8 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; @@ -167,6 +168,24 @@ public partial class ConfigWindow return; } + static void DrawSpecial( string name, ActorIdentifier id ) + { + if( !id.IsValid ) + { + return; + } + + ImGuiUtil.DrawTableColumn( name ); + ImGuiUtil.DrawTableColumn( string.Empty ); + ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( id ) ); + ImGuiUtil.DrawTableColumn( string.Empty ); + } + + DrawSpecial( "Current Player", Penumbra.Actors.GetCurrentPlayer() ); + DrawSpecial( "Current Inspect", Penumbra.Actors.GetInspectPlayer() ); + DrawSpecial( "Current Card", Penumbra.Actors.GetCardPlayer() ); + DrawSpecial( "Current Glamour", Penumbra.Actors.GetGlamourPlayer() ); + foreach( var obj in Dalamud.Objects ) { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs index 0b500f17..220da9bf 100644 --- a/Penumbra/Util/ChatUtil.cs +++ b/Penumbra/Util/ChatUtil.cs @@ -1,8 +1,13 @@ +using System; using System.Collections.Generic; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; +using OtterGui.Log; namespace Penumbra.Util; @@ -32,4 +37,19 @@ public static class ChatUtil Message = payload, } ); } + + public static void NotificationMessage( string content, string? title = null, NotificationType type = NotificationType.None ) + { + var logLevel = type switch + { + NotificationType.None => Logger.LogLevel.Information, + NotificationType.Success => Logger.LogLevel.Information, + NotificationType.Warning => Logger.LogLevel.Warning, + NotificationType.Error => Logger.LogLevel.Error, + NotificationType.Info => Logger.LogLevel.Information, + _ => Logger.LogLevel.Debug, + }; + Dalamud.PluginInterface.UiBuilder.AddNotification( content, title, type ); + Penumbra.Log.Message( logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}" ); + } } \ No newline at end of file From f8c07024329f1fba92c5c48b771525cc1b3573ad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Nov 2022 13:49:15 +0100 Subject: [PATCH 0577/2451] Add Ornaments, further work. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 35 ++- Penumbra.GameData/Actors/ActorManager.Data.cs | 46 ++- .../Actors/ActorManager.Identifiers.cs | 17 +- .../Collections/CollectionManager.Active.cs | 15 +- Penumbra/Collections/CollectionType.cs | 5 +- .../IndividualCollections.Access.cs | 2 +- Penumbra/Collections/IndividualCollections.cs | 24 +- .../ConfigWindow.CollectionsTab.Individual.cs | 271 ++++++++++-------- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 19 +- 9 files changed, 273 insertions(+), 161 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 14742b9c..dc3ffff6 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Enums; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using Newtonsoft.Json.Linq; using Penumbra.String; @@ -58,12 +60,12 @@ public readonly struct ActorIdentifier : IEquatable ?? Type switch { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", - IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", + IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue - ? $"{Kind} #{DataId}" - : $"{Kind} #{DataId} at {Index}", + ? $"{Kind.ToName()} #{DataId}" + : $"{Kind.ToName()} #{DataId} at {Index}", _ => "Invalid", }; @@ -87,7 +89,6 @@ public readonly struct ActorIdentifier : IEquatable PlayerName = playerName; } - public JObject ToJson() { var ret = new JObject { { nameof(Type), Type.ToString() } }; @@ -130,22 +131,19 @@ public static class ActorManagerExtensions if (manager == null) return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; - return lhs.Kind switch + var dict = lhs.Kind switch { - ObjectKind.MountType => manager.Mounts.TryGetValue(lhs.DataId, out var lhsName) - && manager.Mounts.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.Companion => manager.Companions.TryGetValue(lhs.DataId, out var lhsName) - && manager.Companions.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.BattleNpc => manager.BNpcs.TryGetValue(lhs.DataId, out var lhsName) - && manager.BNpcs.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.EventNpc => manager.ENpcs.TryGetValue(lhs.DataId, out var lhsName) - && manager.ENpcs.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - _ => false, + ObjectKind.MountType => manager.Mounts, + ObjectKind.Companion => manager.Companions, + (ObjectKind)15 => manager.Ornaments, // TODO: CS Update + ObjectKind.BattleNpc => manager.BNpcs, + ObjectKind.EventNpc => manager.ENpcs, + _ => new Dictionary(), }; + + return dict.TryGetValue(lhs.DataId, out var lhsName) + && dict.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase); } public static string ToName(this ObjectKind kind) @@ -156,6 +154,7 @@ public static class ActorManagerExtensions ObjectKind.EventNpc => "Event NPC", ObjectKind.MountType => "Mount", ObjectKind.Companion => "Companion", + (ObjectKind)15 => "Accessory", // TODO: CS update _ => kind.ToString(), }; diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index a1ef4b68..f1c3c4bf 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -2,18 +2,19 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using Dalamud; using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Gui; using Dalamud.Plugin; using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Excel; using Lumina.Excel.GeneratedSheets; +using Lumina.Text; using Penumbra.GameData.Data; using Penumbra.String; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -31,6 +32,9 @@ public sealed partial class ActorManager : DataSharer /// Valid Companion names in title case by companion id. public IReadOnlyDictionary Companions { get; } + /// Valid ornament names by id. + public IReadOnlyDictionary Ornaments { get; } + /// Valid BNPC names in title case by BNPC Name id. public IReadOnlyDictionary BNpcs { get; } @@ -54,6 +58,7 @@ public sealed partial class ActorManager : DataSharer Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); + Ornaments = TryCatchData("Ornaments", () => CreateOrnamentData(gameData)); BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); @@ -98,6 +103,7 @@ public sealed partial class ActorManager : DataSharer DisposeTag("Worlds"); DisposeTag("Mounts"); DisposeTag("Companions"); + DisposeTag("Ornaments"); DisposeTag("BNpcs"); DisposeTag("ENpcs"); } @@ -119,22 +125,50 @@ public sealed partial class ActorManager : DataSharer private IReadOnlyDictionary CreateMountData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) - .ToDictionary(m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(m.Singular.ToDalamudString().ToString())); + .ToDictionary(m => m.RowId, m => ToTitleCaseExtended(m.Singular, m.Article)); private IReadOnlyDictionary CreateCompanionData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) - .ToDictionary(c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(c.Singular.ToDalamudString().ToString())); + .ToDictionary(c => c.RowId, c => ToTitleCaseExtended(c.Singular, c.Article)); + + private IReadOnlyDictionary CreateOrnamentData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(o => o.Singular.RawData.Length > 0) + .ToDictionary(o => o.RowId, o => ToTitleCaseExtended(o.Singular, o.Article)); private IReadOnlyDictionary CreateBNpcData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(n => n.Singular.RawData.Length > 0) - .ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString())); + .ToDictionary(n => n.RowId, n => ToTitleCaseExtended(n.Singular, n.Article)); private IReadOnlyDictionary CreateENpcData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(e => e.Singular.RawData.Length > 0) - .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + .ToDictionary(e => e.RowId, e => ToTitleCaseExtended(e.Singular, e.Article)); + + private static string ToTitleCaseExtended(SeString s, sbyte article) + { + if (article == 1) + return s.ToDalamudString().ToString(); + + var sb = new StringBuilder(s.ToDalamudString().ToString()); + var lastSpace = true; + for (var i = 0; i < sb.Length; ++i) + { + if (sb[i] == ' ') + { + lastSpace = true; + } + else if (lastSpace) + { + lastSpace = false; + sb[i] = char.ToUpperInvariant(sb[i]); + } + } + + return sb.ToString(); + } [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 6b5ebb25..ca5731f5 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -52,7 +52,7 @@ public partial class ActorManager } /// - /// Return the world name including the All Worlds option. + /// Return the world name including the Any World option. /// public string ToWorldName(ushort worldId) => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; @@ -98,6 +98,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), ObjectKind.Companion => Companions.TryGetValue(dataId, out name), + (ObjectKind)15 => Ornaments.TryGetValue(dataId, out name), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), _ => false, @@ -154,6 +155,7 @@ public partial class ActorManager case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); case ObjectKind.MountType: case ObjectKind.Companion: + case (ObjectKind)15: // TODO: CS Update { if (actor->ObjectIndex % 2 == 0) return ActorIdentifier.Invalid; @@ -173,11 +175,12 @@ public partial class ActorManager /// Obtain the current companion ID for an object by its actor and owner. /// private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) + FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) // TODO: CS Update { return (ObjectKind)actor->ObjectKind switch { - ObjectKind.MountType => *(ushort*)((byte*)owner + 0x668), + ObjectKind.MountType => *(ushort*)((byte*)owner + 0x650 + 0x18), + (ObjectKind)15 => *(ushort*)((byte*)owner + 0x860 + 0x18), ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC), _ => actor->DataID, }; @@ -196,6 +199,12 @@ public partial class ActorManager _ => ActorIdentifier.Invalid, }; + /// + /// Only use this if you are sure the input is valid. + /// + public ActorIdentifier CreateIndividualUnchecked(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) + => new(type, kind, homeWorld, dataId, name); + public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) { if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name.Span)) @@ -345,6 +354,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.ContainsKey(dataId), ObjectKind.Companion => Companions.ContainsKey(dataId), + (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), _ => false, }; @@ -355,6 +365,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.ContainsKey(dataId), ObjectKind.Companion => Companions.ContainsKey(dataId), + (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), _ => false, diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 836ed3ec..53db52ee 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -43,8 +44,8 @@ public partial class ModCollection => _characters; // If a name does not correspond to a character, return the default collection instead. - public ModCollection Character( string name ) - => _characters.TryGetValue( name, out var c ) ? c : Default; + public ModCollection Individual( ActorIdentifier identifier ) + => Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; @@ -62,7 +63,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Character => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, + CollectionType.Individual => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, _ => null, }; @@ -76,7 +77,7 @@ public partial class ModCollection CollectionType.Default => Default.Index, CollectionType.Interface => Interface.Index, CollectionType.Current => Current.Index, - CollectionType.Character => characterName?.Length > 0 + CollectionType.Individual => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index : Default.Index @@ -113,7 +114,7 @@ public partial class ModCollection case CollectionType.Current: Current = newCollection; break; - case CollectionType.Character: + case CollectionType.Individual: _characters[ characterName! ] = newCollection; break; default: @@ -176,7 +177,7 @@ public partial class ModCollection } _characters[ characterName ] = Default; - CollectionChanged.Invoke( CollectionType.Character, null, Default, characterName ); + CollectionChanged.Invoke( CollectionType.Individual, null, Default, characterName ); return true; } @@ -187,7 +188,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged.Invoke( CollectionType.Character, collection, null, characterName ); + CollectionChanged.Invoke( CollectionType.Individual, collection, null, characterName ); } } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 9e9c0f61..fed83db5 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -97,8 +97,7 @@ public enum CollectionType : byte Inactive, // A collection was added or removed Default, // The default collection was changed Interface, // The ui collection was changed - Character, // A character collection was changed - Individual, // An Individual collection was changed + Individual, // An individual collection was changed Current, // The current collection was changed } @@ -288,7 +287,7 @@ public static class CollectionTypeExtensions CollectionType.Inactive => "Collection", CollectionType.Default => "Default", CollectionType.Interface => "Interface", - CollectionType.Character => "Character", + CollectionType.Individual => "Character", CollectionType.Current => "Current", _ => string.Empty, }; diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 0d295c3b..933121f7 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -105,7 +105,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + identifier = _manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) { return true; diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 86a7410c..fc4a83b2 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -102,7 +102,7 @@ public sealed partial class IndividualCollections _ => throw new NotImplementedException(), }; return table.Where( kvp => kvp.Value == name ) - .Select( kvp => manager.CreateIndividual( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); + .Select( kvp => manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); } return identifier.Type switch @@ -115,9 +115,27 @@ public sealed partial class IndividualCollections }; } - public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + public bool Add( ActorIdentifier[] identifiers, ModCollection collection ) { - if( CanAdd( identifiers ) != AddResult.Valid || _assignments.ContainsKey( displayName ) ) + if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid ) + { + return false; + } + + var name = identifiers[ 0 ].Type switch + { + IdentifierType.Player => $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})", + IdentifierType.Owned => + $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})'s {_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )}", + IdentifierType.Npc => $"{_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )} ({identifiers[ 0 ].Kind})", + _ => string.Empty, + }; + return Add( name, identifiers, collection ); + } + + private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + { + if( CanAdd( identifiers ) != AddResult.Valid || displayName.Length == 0 || _assignments.ContainsKey( displayName ) ) { return false; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 8d7eba65..c850b418 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Dalamud.Interface; using ImGuiNET; @@ -7,8 +8,10 @@ using Penumbra.Collections; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Components; using OtterGui.Widgets; using Penumbra.GameData.Actors; +using Lumina.Data.Parsing; namespace Penumbra.UI; @@ -18,7 +21,7 @@ public partial class ConfigWindow { private sealed class WorldCombo : FilterComboCache< KeyValuePair< ushort, string > > { - private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "All Worlds"); + private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "Any World"); public WorldCombo( IReadOnlyDictionary< ushort, string > worlds ) : base( worlds.OrderBy( kvp => kvp.Value ).Prepend( AllWorldPair ) ) @@ -30,7 +33,7 @@ public partial class ConfigWindow protected override string ToString( KeyValuePair< ushort, string > obj ) => obj.Value; - public void Draw( float width ) + public bool Draw( float width ) => Draw( "##worldCombo", CurrentSelection.Value, width, ImGui.GetTextLineHeightWithSpacing() ); } @@ -57,147 +60,78 @@ public partial class ConfigWindow return ret; } - public void Draw( float width ) + public bool Draw( float width ) => Draw( _label, CurrentSelection.Name, width, ImGui.GetTextLineHeightWithSpacing() ); } // Input Selections. - private string _newCharacterName = string.Empty; - private IdentifierType _newType = IdentifierType.Player; - private ObjectKind _newKind = ObjectKind.BattleNpc; + private string _newCharacterName = string.Empty; + private ObjectKind _newKind = ObjectKind.BattleNpc; private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Worlds); private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Mounts); private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Companions); + private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Ornaments); private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); - private void DrawNewIdentifierOptions( float width ) + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + + private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newPlayerTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newNpcTooltip = NewNpcTooltipEmpty; + private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newOwnedTooltip = NewPlayerTooltipEmpty; + + private bool DrawNewObjectKindOptions( float width ) { ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newType", _newType.ToString() ); - if( combo ) + using var combo = ImRaii.Combo( "##newKind", _newKind.ToName() ); + if( !combo ) { - if( ImGui.Selectable( IdentifierType.Player.ToString(), _newType == IdentifierType.Player ) ) - { - _newType = IdentifierType.Player; - } + return false; + } - if( ImGui.Selectable( IdentifierType.Owned.ToString(), _newType == IdentifierType.Owned ) ) + var ret = false; + foreach( var kind in new[] { ObjectKind.BattleNpc, ObjectKind.EventNpc, ObjectKind.Companion, ObjectKind.MountType, ( ObjectKind )15 } ) // TODO: CS Update + { + if( ImGui.Selectable( kind.ToName(), _newKind == kind ) ) { - _newType = IdentifierType.Owned; - } - - if( ImGui.Selectable( IdentifierType.Npc.ToString(), _newType == IdentifierType.Npc ) ) - { - _newType = IdentifierType.Npc; + _newKind = kind; + ret = true; } } + + return ret; } - private void DrawNewObjectKindOptions( float width ) - { - ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newKind", _newKind.ToString() ); - if( combo ) - { - if( ImGui.Selectable( ObjectKind.BattleNpc.ToString(), _newKind == ObjectKind.BattleNpc ) ) - { - _newKind = ObjectKind.BattleNpc; - } - - if( ImGui.Selectable( ObjectKind.EventNpc.ToString(), _newKind == ObjectKind.EventNpc ) ) - { - _newKind = ObjectKind.EventNpc; - } - - if( ImGui.Selectable( ObjectKind.Companion.ToString(), _newKind == ObjectKind.Companion ) ) - { - _newKind = ObjectKind.Companion; - } - - if( ImGui.Selectable( ObjectKind.MountType.ToString(), _newKind == ObjectKind.MountType ) ) - { - _newKind = ObjectKind.MountType; - } - } - } - - // We do not check for valid character names. - private void DrawNewCharacterCollection() - { - const string description = "Character Collections apply specifically to individual game objects of the given name.\n" - + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an .\n" - + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; - - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - DrawNewIdentifierOptions( width ); - ImGui.SameLine(); - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) - { - _worldCombo.Draw( width ); - } - - ImGui.SameLine(); - - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) - { - DrawNewObjectKindOptions( width ); - } - - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) - { - ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); - } - - ImGui.SameLine(); - var disabled = _newCharacterName.Length == 0; - var tt = disabled - ? $"Please enter the name of a {ConditionalIndividual} before assigning the collection.\n\n" + description - : description; - if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalIndividual}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, - disabled ) ) - { - Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); - _newCharacterName = string.Empty; - } - - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) - { - switch( _newKind ) - { - case ObjectKind.BattleNpc: - _bnpcCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.EventNpc: - _enpcCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.Companion: - _companionCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.MountType: - _mountCombo.Draw( _window._inputTextWidth.X ); - break; - } - } - } private void DrawIndividualAssignments() { - using var _ = ImRaii.Group(); + using var _ = ImRaii.Group(); + using var mainId = ImRaii.PushId( "Individual" ); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" + + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an Individual Collection takes effect.\n" + + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections." ); ImGui.Separator(); - foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) + for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) { - using var id = ImRaii.PushId( name ); + var (name, collection) = Penumbra.CollectionManager.Individuals[ i ]; + using var id = ImRaii.PushId( i ); DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) { - Penumbra.CollectionManager.RemoveCharacterCollection( name ); + Penumbra.CollectionManager.Individuals.Delete( i ); } ImGui.SameLine(); @@ -206,7 +140,120 @@ public partial class ConfigWindow } ImGui.Dummy( Vector2.Zero ); - DrawNewCharacterCollection(); + DrawNewIndividualCollection(); + } + + private bool DrawNewPlayerCollection( Vector2 buttonWidth, float width ) + { + var change = _worldCombo.Draw( width ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width ); + change |= ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newPlayerIdentifiers, Penumbra.CollectionManager.Default ); + change = true; + } + + return change; + } + + private bool DrawNewNpcCollection( NpcCombo combo, Vector2 buttonWidth, float width ) + { + var comboWidth = _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; + var change = DrawNewObjectKindOptions( width ); + ImGui.SameLine(); + change |= combo.Draw( comboWidth ); + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newNpcIdentifiers, Penumbra.CollectionManager.Default ); + change = true; + } + + return change; + } + + private bool DrawNewOwnedCollection( Vector2 buttonWidth ) + { + var oldPos = ImGui.GetCursorPos(); + ImGui.SameLine(); + ImGui.SetCursorPos( ImGui.GetCursorPos() + new Vector2( -ImGui.GetStyle().ItemSpacing.X / 2, ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); + if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default ); + return true; + } + + ImGui.SetCursorPos( oldPos ); + + return false; + } + + private NpcCombo GetNpcCombo( ObjectKind kind ) + => kind switch + { + ObjectKind.BattleNpc => _bnpcCombo, + ObjectKind.EventNpc => _enpcCombo, + ObjectKind.MountType => _mountCombo, + ObjectKind.Companion => _companionCombo, + ( ObjectKind )15 => _ornamentCombo, // TODO: CS update + _ => throw new NotImplementedException(), + }; + + private void DrawNewIndividualCollection() + { + var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; + var buttonWidth = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + + var combo = GetNpcCombo( _newKind ); + var change = DrawNewPlayerCollection( buttonWidth, width ); + change |= DrawNewOwnedCollection( Vector2.Zero ); + change |= DrawNewNpcCollection( combo, buttonWidth, width ); + + if( change ) + { + UpdateIdentifiers(); + } + } + + private void UpdateIdentifiers() + { + var combo = GetNpcCombo( _newKind ); + _newPlayerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty< uint >(), out _newPlayerIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + if( combo.CurrentSelection.Ids != null ) + { + _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + combo.CurrentSelection.Ids, out _newNpcIdentifiers ) switch + { + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newOwnedTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, + combo.CurrentSelection.Ids, out _newOwnedIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + } + else + { + _newNpcTooltip = NewNpcTooltipEmpty; + _newOwnedTooltip = NewNpcTooltipEmpty; + _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); + _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); + } } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 6025cb70..1fd968c5 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -161,7 +161,7 @@ public partial class ConfigWindow public void Draw() { var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; - Draw(_label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing()); + Draw( _label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); } protected override string ToString( (CollectionType, string, string) obj ) @@ -176,15 +176,16 @@ public partial class ConfigWindow private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); + private const string CharacterGroupDescription = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {DefaultCollection},\n" + + $"but all {IndividualAssignments} take precedence before them."; + + // We do not check for valid character names. private void DrawNewSpecialCollection() { - const string description = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {DefaultCollection},\n" - + $"but all {IndividualAssignments} take precedence before them."; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _specialCollectionCombo.CurrentIdx == -1 + if( _specialCollectionCombo.CurrentIdx == -1 || Penumbra.CollectionManager.ByType( _specialCollectionCombo.CurrentType!.Value.Item1 ) != null ) { _specialCollectionCombo.ResetFilter(); @@ -201,8 +202,8 @@ public partial class ConfigWindow ImGui.SameLine(); var disabled = _specialCollectionCombo.CurrentType == null; var tt = disabled - ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + description - : description; + ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + CharacterGroupDescription + : CharacterGroupDescription; if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) { Penumbra.CollectionManager.CreateSpecialCollection( _specialCollectionCombo.CurrentType!.Value.Item1 ); @@ -237,7 +238,9 @@ public partial class ConfigWindow private void DrawSpecialAssignments() { using var _ = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( CharacterGroups ); + ImGuiComponents.HelpMarker( CharacterGroupDescription ); ImGui.Separator(); DrawSpecialCollections(); ImGui.Dummy( Vector2.Zero ); From 6a6eac1c3b0425d476fcbdb2ba44bc1fed4843b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Nov 2022 15:22:31 +0100 Subject: [PATCH 0578/2451] Use IndividualCollections in PathResolver. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 5 +- Penumbra.GameData/Actors/ActorManager.Data.cs | 6 +- .../Collections/CollectionManager.Active.cs | 5 +- Penumbra/Collections/CollectionManager.cs | 2 +- .../IndividualCollections.Access.cs | 27 +- Penumbra/Collections/ModCollection.cs | 3 + .../Resolver/PathResolver.Identification.cs | 310 ++++++------------ Penumbra/Interop/Resolver/PathResolver.cs | 9 + Penumbra/Penumbra.cs | 4 +- .../ConfigWindow.CollectionsTab.Individual.cs | 2 +- 10 files changed, 128 insertions(+), 245 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index dc3ffff6..0903c312 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Enums; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using Newtonsoft.Json.Linq; using Penumbra.String; @@ -38,7 +37,7 @@ public readonly struct ActorIdentifier : IEquatable IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, - IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals(this, other), + IdentifierType.Npc => Manager.DataIdEquals(this, other) && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), _ => false, }; } @@ -75,7 +74,7 @@ public readonly struct ActorIdentifier : IEquatable IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), - IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, Index, DataId), + IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), _ => 0, }; diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index f1c3c4bf..0907d2ff 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text; using Dalamud; @@ -70,7 +69,10 @@ public sealed partial class ActorManager : DataSharer public unsafe ActorIdentifier GetCurrentPlayer() { var address = (Character*)(_objects[0]?.Address ?? IntPtr.Zero); - return address == null ? ActorIdentifier.Invalid : CreatePlayer(new ByteString(address->GameObject.Name), address->HomeWorld); + return address == null + ? ActorIdentifier.Invalid + : CreateIndividualUnchecked(IdentifierType.Player, new ByteString(address->GameObject.Name), address->HomeWorld, + ObjectKind.None, uint.MaxValue); } public ActorIdentifier GetInspectPlayer() diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 53db52ee..47977d08 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -44,8 +43,8 @@ public partial class ModCollection => _characters; // If a name does not correspond to a character, return the default collection instead. - public ModCollection Individual( ActorIdentifier identifier ) - => Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : Default; + public ModCollection Character( string name ) + => _characters.TryGetValue( name, out var c ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 961478b5..05dc797f 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -149,7 +149,7 @@ public partial class ModCollection foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) { - SetCollection( Empty, CollectionType.Character, characterName ); + SetCollection( Empty, CollectionType.Individual, characterName ); } var collection = _collections[ idx ]; diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 933121f7..f6d927ad 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -2,8 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; +using Penumbra.String; namespace Penumbra.Collections; @@ -34,8 +36,8 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } // Handle generic NPC - var npcIdentifier = _manager.CreateNpc( identifier.Kind, identifier.DataId ); - if( npcIdentifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + var npcIdentifier = _manager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId ); + if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) ) { return true; } @@ -43,7 +45,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ // Handle Ownership. if( Penumbra.Config.UseOwnerNameForCharacterCollection ) { - identifier = _manager.CreatePlayer( identifier.PlayerName, identifier.HomeWorld ); + identifier = _manager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); return CheckWorlds( identifier, out collection ); } @@ -60,22 +62,9 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return CheckWorlds( _manager.GetCurrentPlayer(), out collection ); case SpecialActor.ExamineScreen: { - if( CheckWorlds( _manager.GetInspectPlayer(), out collection! ) ) - { - return true; - } - - if( CheckWorlds( _manager.GetCardPlayer(), out collection! ) ) - { - return true; - } - - if( CheckWorlds( _manager.GetGlamourPlayer(), out collection! ) ) - { - return true; - } - - break; + return CheckWorlds( _manager.GetInspectPlayer(), out collection! ) + || CheckWorlds( _manager.GetCardPlayer(), out collection! ) + || CheckWorlds( _manager.GetGlamourPlayer(), out collection! ); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index b01a8fad..e6a71fdf 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -172,4 +172,7 @@ public partial class ModCollection Save(); } } + + public override string ToString() + => Name; } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index ec975dfa..d7b25cd3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -1,141 +1,21 @@ using System; -using System.Diagnostics.CodeAnalysis; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Component.GUI; +using System.Collections; +using System.Linq; +using Dalamud.Data; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Lumina.Excel.GeneratedSheets; +using OtterGui; using Penumbra.Collections; +using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; -using Penumbra.String; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - [Signature( "0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress )] - private static ushort* _inspectTitleId = null!; - - // Obtain the name of the current player, if one exists. - private static string? GetPlayerName() - => Dalamud.Objects[ 0 ]?.Name.ToString(); - - // Obtain the name of the inspect target from its window, if it exists. - private static string? GetInspectName() - { - if( !Penumbra.Config.UseCharacterCollectionInInspect ) - { - return null; - } - - var addon = Dalamud.GameGui.GetAddonByName( "CharacterInspect", 1 ); - if( addon == IntPtr.Zero ) - { - return null; - } - - var ui = ( AtkUnitBase* )addon; - var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 7u : 6u; - - var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); - return text != null && text->AtkResNode.Type == NodeType.Text ? text->NodeText.ToString() : null; - } - - // Obtain the name displayed in the Character Card from the agent. - private static string? GetCardName() - { - // TODO: Update to ClientStructs when merged. - if( !Penumbra.Config.UseCharacterCollectionsInCards ) - { - return null; - } - - var agent = AgentCharaCard.Instance(); - if( agent == null ) - { - return null; - } - - var data = *( byte** )( ( byte* )agent + 0x28 ); - if( data == null ) - { - return null; - } - - var block = data + 0x7A; - return new ByteString( block ).ToString(); - } - - // Obtain the name of the player character if the glamour plate edit window is open. - private static string? GetGlamourName() - { - if( !Penumbra.Config.UseCharacterCollectionInTryOn ) - { - return null; - } - - var addon = Dalamud.GameGui.GetAddonByName( "MiragePrismMiragePlate", 1 ); - return addon == IntPtr.Zero ? null : GetPlayerName(); - } - - // Guesstimate whether an unnamed cutscene actor corresponds to the player or not, - // and if so, return the player name. - private static string? GetCutsceneName( GameObject* gameObject ) - { - if( gameObject->Name[ 0 ] != 0 || gameObject->ObjectKind != ( byte )ObjectKind.Player ) - { - return null; - } - - var parent = Cutscenes[ gameObject->ObjectIndex ]; - if( parent != null ) - { - return parent.Name.ToString(); - } - - // should not really happen but keep it in as a emergency case. - var player = Dalamud.Objects[ 0 ]; - if( player == null ) - { - return null; - } - - var customize1 = ( CustomizeData* )( ( Character* )gameObject )->CustomizeData; - var customize2 = ( CustomizeData* )( ( Character* )player.Address )->CustomizeData; - return customize1->Equals( *customize2 ) ? player.Name.ToString() : null; - } - - // Identify the owner of a companion, mount or monster and apply the corresponding collection. - // Companions and mounts get set to the actor before them in the table if it exists. - // Monsters with a owner use that owner if it exists. - private static string? GetOwnerName( GameObject* gameObject ) - { - if( !Penumbra.Config.UseOwnerNameForCharacterCollection ) - { - return null; - } - - GameObject* owner = null; - if( ( ObjectKind )gameObject->GetObjectKind() is ObjectKind.Companion or ObjectKind.MountType && gameObject->ObjectIndex > 0 ) - { - owner = ( GameObject* )Dalamud.Objects[ gameObject->ObjectIndex - 1 ]?.Address; - } - else if( gameObject->OwnerID != 0xE0000000 ) - { - owner = ( GameObject* )( Dalamud.Objects.SearchById( gameObject->OwnerID )?.Address ?? IntPtr.Zero ); - } - - if( owner != null ) - { - return new ByteString( owner->Name ).ToString(); - } - - return null; - } - // Identify the correct collection for a GameObject by index and name. private static ResolveData IdentifyCollection( GameObject* gameObject ) { @@ -152,51 +32,18 @@ public unsafe partial class PathResolver if( !Dalamud.ClientState.IsLoggedIn ) { var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default ); + ?? CollectionByAttributes( gameObject ) + ?? Penumbra.CollectionManager.Default; return collection.ToResolveData( gameObject ); } else { - // Housing Retainers - if( Penumbra.Config.UseDefaultCollectionForRetainers - && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc - && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer - { - return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); - } - - string? actorName = null; - if( Penumbra.Config.PreferNamedCollectionsOverOwners ) - { - // Early return if we prefer the actors own name over its owner. - actorName = new ByteString( gameObject->Name ).ToString(); - if( actorName.Length > 0 - && CollectionByActorName( actorName, out var actorCollection ) ) - { - return actorCollection.ToResolveData( gameObject ); - } - } - - // All these special cases are relevant for an empty name, so never collide with the above setting. - // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. - var actualName = gameObject->ObjectIndex switch - { - 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on - 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview - 244 => Penumbra.Config.UseCharacterCollectionsInCards ? GetPlayerName() : null, // portrait list and editor - >= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ), - _ => null, - } - ?? GetOwnerName( gameObject ) ?? actorName ?? new ByteString( gameObject->Name ).ToString(); - - // First check temporary character collections, then the own configuration, then special collections. - var collection = CollectionByActorName( actualName, out var c ) - ? c - : CollectionByActor( actualName, gameObject, out c ) - ? c - : Penumbra.CollectionManager.Default; + var identifier = Penumbra.Actors.FromObject( gameObject ); + var collection = CollectionByIdentifier( identifier ) + ?? CheckYourself( identifier, gameObject ) + ?? CollectionByAttributes( gameObject ) + ?? CheckOwnedCollection( identifier, gameObject ) + ?? Penumbra.CollectionManager.Default; return collection.ToResolveData( gameObject ); } } @@ -211,67 +58,102 @@ public unsafe partial class PathResolver // or the default collection if no player exists. public static ModCollection PlayerCollection() { - var player = Dalamud.ClientState.LocalPlayer; - if( player == null ) + var player = Penumbra.Actors.GetCurrentPlayer(); + if( !player.IsValid ) { return Penumbra.CollectionManager.Default; } - var name = player.Name.TextValue; - if( CollectionByActorName( name, out var c ) ) - { - return c; - } - - if( CollectionByActor( name, ( GameObject* )player.Address, out c ) ) - { - return c; - } - - return Penumbra.CollectionManager.Default; + return CollectionByIdentifier( player ) + ?? CollectionByAttributes( ( GameObject* )Dalamud.Objects[ 0 ]!.Address ) + ?? Penumbra.CollectionManager.Default; } // Check both temporary and permanent character collections. Temporary first. - private static bool CollectionByActorName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => Penumbra.TempMods.Collections.TryGetValue( name, out collection ) - || Penumbra.CollectionManager.Characters.TryGetValue( name, out collection ); + private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) + => Penumbra.TempMods.Collections.TryGetValue( identifier.ToString(), out var collection ) + || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) + ? collection + : null; - // Check special collections given the actor. - private static bool CollectionByActor( string name, GameObject* actor, [NotNullWhen( true )] out ModCollection? collection ) + + // Check for the Yourself collection. + private static ModCollection? CheckYourself( ActorIdentifier identifier, GameObject* actor ) { - collection = null; - // Check for the Yourself collection. if( actor->ObjectIndex == 0 || Cutscenes.GetParentIndex( actor->ObjectIndex ) == 0 - || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + || identifier.Equals( Penumbra.Actors.GetCurrentPlayer() ) ) { - collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); - if( collection != null ) - { - return true; - } + return Penumbra.CollectionManager.ByType( CollectionType.Yourself ); } - if( actor->IsCharacter() ) - { - var character = ( Character* )actor; - // Only handle human models. - if( character->ModelCharaId == 0 ) - { - var race = ( SubRace )character->CustomizeData[ 4 ]; - var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); - var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; + return null; + } - var type = CollectionTypeExtensions.FromParts( race, gender, isNpc ); - collection = Penumbra.CollectionManager.ByType( type ); - collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) ); - if( collection != null ) - { - return true; - } - } + // Check special collections given the actor. + private static ModCollection? CollectionByAttributes( GameObject* actor ) + { + if( !actor->IsCharacter() ) + { + return null; } - return false; + // Only handle human models. + var character = ( Character* )actor; + if( character->ModelCharaId >= 0 && character->ModelCharaId < ValidHumanModels.Count && ValidHumanModels[ character->ModelCharaId ] ) + { + var race = ( SubRace )character->CustomizeData[ 4 ]; + var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); + var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; + + var type = CollectionTypeExtensions.FromParts( race, gender, isNpc ); + var collection = Penumbra.CollectionManager.ByType( type ); + collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) ); + return collection; + } + + return null; + } + + // Get the collection applying to the owner if it is available. + private static ModCollection? CheckOwnedCollection( ActorIdentifier identifier, GameObject* obj ) + { + if( identifier.Type != IdentifierType.Owned || !Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + return null; + } + + var owner = identifier.Kind switch + { + ObjectKind.BattleNpc when obj->OwnerID != 0xE0000000 => ( GameObject* )( Dalamud.Objects.SearchById( obj->OwnerID )?.Address ?? IntPtr.Zero ), + ObjectKind.MountType when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), + ObjectKind.Companion when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), + ( ObjectKind )15 when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), // TODO: CS Update + _ => null, + }; + + if( owner == null ) + { + return null; + } + + var id = Penumbra.Actors.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); + return CheckYourself( id, owner ) + ?? CollectionByAttributes( owner ); + } + + /// + /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. + /// + private static BitArray GetValidHumanModels( DataManager gameData ) + { + var sheet = gameData.GetExcelSheet< ModelChara >()!; + var ret = new BitArray( ( int )sheet.RowCount, false ); + foreach( var (row, idx) in sheet.WithIndex().Where( p => p.Value.Type == ( byte )CharacterBase.ModelType.Human ) ) + { + ret[ idx ] = true; + } + + return ret; } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index c2f4f3ef..4f9b5059 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,9 +1,14 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Lumina.Excel.GeneratedSheets; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; @@ -24,11 +29,15 @@ public partial class PathResolver : IDisposable private readonly ResourceLoader _loader; private static readonly CutsceneCharacters Cutscenes = new(); private static readonly DrawObjectState DrawObjects = new(); + private static readonly BitArray ValidHumanModels; private readonly AnimationState _animations; private readonly PathState _paths; private readonly MetaState _meta; private readonly MaterialState _materials; + static PathResolver() + => ValidHumanModels = GetValidHumanModels( Dalamud.GameData ); + public unsafe PathResolver( ResourceLoader loader ) { SignatureHelper.Initialise( this ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6744462d..91561644 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -332,7 +332,7 @@ public class Penumbra : IDalamudPlugin } string? characterName = null; - if( type is CollectionType.Character ) + if( type is CollectionType.Individual ) { var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); if( split.Length < 2 || split[ 0 ].Length == 0 || split[ 1 ].Length == 0 ) @@ -368,7 +368,7 @@ public class Penumbra : IDalamudPlugin { CollectionManager.CreateSpecialCollection( type ); } - else if( type is CollectionType.Character ) + else if( type is CollectionType.Individual ) { CollectionManager.CreateCharacterCollection( characterName! ); } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index c850b418..a7a0f65b 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -126,7 +126,7 @@ public partial class ConfigWindow { var (name, collection) = Penumbra.CollectionManager.Individuals[ i ]; using var id = ImRaii.PushId( i ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Individual, true, name ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) From 4309ae8ce267489c7ebd9c9032909aca4feee8a6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Nov 2022 18:17:23 +0100 Subject: [PATCH 0579/2451] Update everything except for IPC and temp collections to new system. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 8 +- .../Actors/ActorManager.Identifiers.cs | 31 +-- Penumbra/Api/PenumbraApi.cs | 21 +- .../Collections/CollectionManager.Active.cs | 205 ++++++++---------- Penumbra/Collections/CollectionManager.cs | 17 +- .../IndividualCollections.Access.cs | 22 +- .../IndividualCollections.Files.cs | 78 ++++++- Penumbra/Collections/IndividualCollections.cs | 87 ++++---- Penumbra/Configuration.Migration.cs | 44 +++- .../Resolver/PathResolver.DrawObjectState.cs | 5 +- Penumbra/Penumbra.cs | 56 +++-- Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +- .../ConfigWindow.CollectionsTab.Individual.cs | 33 ++- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 14 +- Penumbra/UI/ConfigWindow.Misc.cs | 21 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 2 +- 16 files changed, 400 insertions(+), 249 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 0903c312..74e6d5f1 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -37,7 +37,8 @@ public readonly struct ActorIdentifier : IEquatable IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, - IdentifierType.Npc => Manager.DataIdEquals(this, other) && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), + IdentifierType.Npc => Manager.DataIdEquals(this, other) + && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), _ => false, }; } @@ -107,8 +108,9 @@ public readonly struct ActorIdentifier : IEquatable ret.Add(nameof(Special), Special.ToString()); return ret; case IdentifierType.Npc: - ret.Add(nameof(Kind), Kind.ToString()); - ret.Add(nameof(Index), Index); + ret.Add(nameof(Kind), Kind.ToString()); + if (Index != ushort.MaxValue) + ret.Add(nameof(Index), Index); ret.Add(nameof(DataId), DataId); return ret; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index ca5731f5..ec0782fb 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -14,40 +14,41 @@ public partial class ActorManager /// /// A parsed JObject /// ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise. - public ActorIdentifier FromJson(JObject data) + public ActorIdentifier FromJson(JObject? data) { - var type = data[nameof(ActorIdentifier.Type)]?.Value() ?? IdentifierType.Invalid; + if (data == null) + return ActorIdentifier.Invalid; + + var type = data[nameof(ActorIdentifier.Type)]?.ToObject() ?? IdentifierType.Invalid; switch (type) { case IdentifierType.Player: { - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); - var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; return CreatePlayer(name, homeWorld); } case IdentifierType.Owned: { - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); - var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; - var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; - var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; + var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; return CreateOwned(name, homeWorld, kind, dataId); } case IdentifierType.Special: { - var special = data[nameof(ActorIdentifier.Special)]?.Value() ?? 0; + var special = data[nameof(ActorIdentifier.Special)]?.ToObject() ?? 0; return CreateSpecial(special); } case IdentifierType.Npc: { - var index = data[nameof(ActorIdentifier.Index)]?.Value() ?? ushort.MaxValue; - var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; - var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + var index = data[nameof(ActorIdentifier.Index)]?.ToObject() ?? ushort.MaxValue; + var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; return CreateNpc(kind, dataId, index); } - case IdentifierType.Invalid: - default: - return ActorIdentifier.Invalid; + default: return ActorIdentifier.Invalid; } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8cc799f1..3521369c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -14,6 +14,8 @@ using System.IO; using System.Linq; using System.Reflection; using Penumbra.Api.Enums; +using Penumbra.GameData.Actors; +using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Api; @@ -212,7 +214,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.Character( characterName ) ); + Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ) ); } public string[] ReverseResolvePath( string path, string characterName ) @@ -223,7 +225,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return new[] { path }; } - var ret = Penumbra.CollectionManager.Character( characterName ).ReverseResolvePath( new FullPath( path ) ); + var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ).ReverseResolvePath( new FullPath( path ) ); return ret.Select( r => r.ToString() ).ToArray(); } @@ -297,7 +299,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection( string characterName ) { CheckInitialized(); - return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) + return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName ), out var collection ) ? ( collection.Name, true ) : ( Penumbra.CollectionManager.Default.Name, false ); } @@ -570,7 +572,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.InvalidArgument, string.Empty ); } - if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( NameToIdentifier(character) ) || Penumbra.TempMods.Collections.ContainsKey( character ) ) { return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); @@ -680,7 +682,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) ? c - : Penumbra.CollectionManager.Character( characterName ); + : Penumbra.CollectionManager.Individual( NameToIdentifier(characterName) ); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } @@ -804,7 +806,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi c.ModSettingChanged += Del; } - private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _ ) { if( type != CollectionType.Inactive ) { @@ -827,4 +829,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void InvokePostSettingsPanel( string modDirectory ) => PostSettingsPanelDraw?.Invoke( modDirectory ); + + // TODO + private static ActorIdentifier NameToIdentifier( string name ) + { + var b = ByteString.FromStringUnsafe( name, false ); + return Penumbra.Actors.CreatePlayer( b, ( ushort )( Dalamud.ClientState.LocalPlayer?.HomeWorld.Id ?? ushort.MaxValue ) ); + } } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 47977d08..e30dcece 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,6 +8,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.GameData.Actors; +using Penumbra.Util; namespace Penumbra.Collections; @@ -36,21 +39,20 @@ public partial class ModCollection private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. - private readonly Dictionary< string, ModCollection > _characters = new(); - public readonly IndividualCollections Individuals = new(Penumbra.Actors); + public readonly IndividualCollections Individuals = new(Penumbra.Actors); - public IReadOnlyDictionary< string, ModCollection > Characters - => _characters; - - // If a name does not correspond to a character, return the default collection instead. - public ModCollection Character( string name ) - => _characters.TryGetValue( name, out var c ) ? c : Default; + public ModCollection Individual( ActorIdentifier identifier ) + => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; // Return the configured collection for the given type or null. - public ModCollection? ByType( CollectionType type, string? name = null ) + // Does not handle Inactive, use ByName instead. + public ModCollection? ByType( CollectionType type ) + => ByType( type, ActorIdentifier.Invalid ); + + public ModCollection? ByType( CollectionType type, ActorIdentifier identifier ) { if( type.IsSpecial() ) { @@ -59,28 +61,23 @@ public partial class ModCollection return type switch { - CollectionType.Default => Default, - CollectionType.Interface => Interface, - CollectionType.Current => Current, - CollectionType.Individual => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, - CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, - _ => null, + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null, + _ => null, }; } - // Set a active collection, can be used to set Default, Current or Character collections. - private void SetCollection( int newIdx, CollectionType collectionType, string? characterName = null ) + // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + private void SetCollection( int newIdx, CollectionType collectionType, int individualIndex = -1 ) { var oldCollectionIdx = collectionType switch { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => characterName?.Length > 0 - ? _characters.TryGetValue( characterName, out var c ) - ? c.Index - : Default.Index - : -1, + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, + CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index, _ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, _ => -1, }; @@ -114,7 +111,12 @@ public partial class ModCollection Current = newCollection; break; case CollectionType.Individual: - _characters[ characterName! ] = newCollection; + if( !Individuals.ChangeCollection( individualIndex, newCollection ) ) + { + RemoveCache( newIdx ); + return; + } + break; default: _specialCollections[ ( int )collectionType ] = newCollection; @@ -124,7 +126,7 @@ public partial class ModCollection RemoveCache( oldCollectionIdx ); UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, characterName ); + CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, Individuals[ individualIndex ].DisplayName ); } private void UpdateCurrentCollectionInUse() @@ -132,11 +134,11 @@ public partial class ModCollection .OfType< ModCollection >() .Prepend( Interface ) .Prepend( Default ) - .Concat( Characters.Values ) + .Concat( Individuals.Assignments.Select( kvp => kvp.Collection ) ) .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); - public void SetCollection( ModCollection collection, CollectionType collectionType, string? characterName = null ) - => SetCollection( collection.Index, collectionType, characterName ); + public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 ) + => SetCollection( collection.Index, collectionType, individualIndex ); // Create a special collection if it does not exist and set it to Empty. public bool CreateSpecialCollection( CollectionType collectionType ) @@ -147,7 +149,7 @@ public partial class ModCollection } _specialCollections[ ( int )collectionType ] = Default; - CollectionChanged.Invoke( collectionType, null, Default, null ); + CollectionChanged.Invoke( collectionType, null, Default ); return true; } @@ -163,31 +165,38 @@ public partial class ModCollection if( old != null ) { _specialCollections[ ( int )collectionType ] = null; - CollectionChanged.Invoke( collectionType, old, null, null ); + CollectionChanged.Invoke( collectionType, old, null ); } } - // Create a new character collection. Returns false if the character name already has a collection. - public bool CreateCharacterCollection( string characterName ) + // Wrappers around Individual Collection handling. + public void CreateIndividualCollection( ActorIdentifier[] identifiers ) { - if( _characters.ContainsKey( characterName ) ) + if( Individuals.Add( identifiers, Default ) ) { - return false; + CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName ); } - - _characters[ characterName ] = Default; - CollectionChanged.Invoke( CollectionType.Individual, null, Default, characterName ); - return true; } - // Remove a character collection if it exists. - public void RemoveCharacterCollection( string characterName ) + public void RemoveIndividualCollection( int individualIndex ) { - if( _characters.TryGetValue( characterName, out var collection ) ) + if( individualIndex < 0 || individualIndex >= Individuals.Count ) { - RemoveCache( collection.Index ); - _characters.Remove( characterName ); - CollectionChanged.Invoke( CollectionType.Individual, collection, null, characterName ); + return; + } + + var (name, old) = Individuals[ individualIndex ]; + if( Individuals.Delete( individualIndex ) ) + { + CollectionChanged.Invoke( CollectionType.Individual, old, null, name ); + } + } + + public void MoveIndividualCollection( int from, int to ) + { + if( Individuals.Move( from, to ) ) + { + SaveActiveCollections(); } } @@ -209,7 +218,8 @@ public partial class ModCollection var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { - Penumbra.Log.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." ); + ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning ); Default = Empty; configChanged = true; } @@ -223,8 +233,8 @@ public partial class ModCollection var interfaceIdx = GetIndexForCollectionName( interfaceName ); if( interfaceIdx < 0 ) { - Penumbra.Log.Error( - $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}." ); + ChatUtil.NotificationMessage( + $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning ); Interface = Empty; configChanged = true; } @@ -238,8 +248,8 @@ public partial class ModCollection var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - Penumbra.Log.Error( - $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); + ChatUtil.NotificationMessage( + $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning ); Current = DefaultName; configChanged = true; } @@ -257,7 +267,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName( typeName ); if( idx < 0 ) { - Penumbra.Log.Error( $"Last choice of {name} Collection {typeName} is not available, removed." ); + ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning ); configChanged = true; } else @@ -267,30 +277,14 @@ public partial class ModCollection } } - // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject[ nameof( Characters ) ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); - foreach( var (player, collectionName) in characters ) - { - var idx = GetIndexForCollectionName( collectionName ); - if( idx < 0 ) - { - Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); - _characters.Add( player, Empty ); - configChanged = true; - } - else - { - _characters.Add( player, this[ idx ] ); - } - } + configChanged |= MigrateIndividualCollections( jObject ); + configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this ); // Save any changes and create all required caches. if( configChanged ) { SaveActiveCollections(); } - - MigrateIndividualCollections( jObject ); } // Migrate ungendered collections to Male and Female for 0.5.9.0. @@ -323,21 +317,24 @@ public partial class ModCollection } // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject) + private bool MigrateIndividualCollections( JObject jObject ) { var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; if( version > 0 ) + { return false; - + } + // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject[nameof( Characters )]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary< string, ModCollection >( characters.Count ); + var characters = jObject[ "Characters" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); + var dict = new Dictionary< string, ModCollection >( characters.Count ); foreach( var (player, collectionName) in characters ) { var idx = GetIndexForCollectionName( collectionName ); if( idx < 0 ) { - Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); + ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning ); dict.Add( player, Empty ); } else @@ -345,7 +342,7 @@ public partial class ModCollection dict.Add( player, this[ idx ] ); } } - + Individuals.Migrate0To1( dict ); return true; } @@ -353,46 +350,32 @@ public partial class ModCollection public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - () => SaveActiveCollections( Default.Name, Interface.Name, Current.Name, - Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ), - _specialCollections.WithIndex() - .Where( c => c.Item1 != null ) - .Select( c => ( ( CollectionType )c.Item2, c.Item1!.Name ) ) ) ); + SaveActiveCollectionsInternal ); } - internal static void SaveActiveCollections( string def, string ui, string current, IEnumerable< (string, string) > characters, - IEnumerable< (CollectionType, string) > special ) + internal void SaveActiveCollectionsInternal() { var file = ActiveCollectionFile; try { + var jObj = new JObject + { + { nameof( Version ), Version }, + { nameof( Default ), Default.Name }, + { nameof( Interface ), Interface.Name }, + { nameof( Current ), Current.Name }, + }; + foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) ) + { + jObj.Add( type.ToString(), collection.Name ); + } + + jObj.Add( nameof( Individuals ), Individuals.ToJObject() ); using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( nameof( Default ) ); - j.WriteValue( def ); - j.WritePropertyName( nameof( Interface ) ); - j.WriteValue( ui ); - j.WritePropertyName( nameof( Current ) ); - j.WriteValue( current ); - foreach( var (type, collection) in special ) - { - j.WritePropertyName( type.ToString() ); - j.WriteValue( collection ); - } - - j.WritePropertyName( nameof( Characters ) ); - j.WriteStartObject(); - foreach( var (character, collection) in characters ) - { - j.WritePropertyName( character, true ); - j.WriteValue( collection ); - } - - j.WriteEndObject(); - j.WriteEndObject(); + using var j = new JsonTextWriter( writer ) + { Formatting = Formatting.Indented }; + jObj.WriteTo( j ); Penumbra.Log.Verbose( "Active Collections saved." ); } catch( Exception e ) @@ -424,7 +407,7 @@ public partial class ModCollection } // Save if any of the active collections is changed. - private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) + private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3 ) { if( collectionType != CollectionType.Inactive ) { @@ -437,7 +420,7 @@ public partial class ModCollection public void CreateNecessaryCaches() { var tasks = _specialCollections.OfType< ModCollection >() - .Concat( _characters.Values ) + .Concat( Individuals.Select( p => p.Collection ) ) .Prepend( Current ) .Prepend( Default ) .Prepend( Interface ) @@ -455,7 +438,7 @@ public partial class ModCollection && idx != Interface.Index && idx != Current.Index && _specialCollections.All( c => c == null || c.Index != idx ) - && _characters.Values.All( c => c.Index != idx ) ) + && Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) ) { _collections[ idx ].ClearCache(); } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 05dc797f..66db1d1d 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -15,9 +16,9 @@ public partial class ModCollection public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { // On addition, oldCollection is null. On deletion, newCollection is null. - // CharacterName is only set for type == Character. + // displayName is only set for type == Individual. public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, - ModCollection? newCollection, string? characterName = null ); + ModCollection? newCollection, string displayName = "" ); private readonly Mod.Manager _modManager; @@ -139,19 +140,21 @@ public partial class ModCollection if( idx == Current.Index ) { - SetCollection( DefaultName, CollectionType.Current ); + SetCollection( DefaultName.Index, CollectionType.Current ); } if( idx == Default.Index ) { - SetCollection( Empty, CollectionType.Default ); + SetCollection( Empty.Index, CollectionType.Default ); } - foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) + for( var i = 0; i < Individuals.Count; ++i ) { - SetCollection( Empty, CollectionType.Individual, characterName ); + if( Individuals[ i ].Collection.Index == idx ) + { + Individuals.ChangeCollection( i, Empty ); + } } - var collection = _collections[ idx ]; // Clear own inheritances. diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index f6d927ad..29c403d7 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -12,7 +12,7 @@ namespace Penumbra.Collections; public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > { public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() - => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); + => _assignments.Select( t => ( t.DisplayName, t.Collection ) ).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -21,7 +21,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ => _assignments.Count; public (string DisplayName, ModCollection Collection) this[ int index ] - => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); + => ( _assignments[ index ].DisplayName, _assignments[ index ].Collection ); public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) { @@ -36,7 +36,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } // Handle generic NPC - var npcIdentifier = _manager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId ); + var npcIdentifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId ); if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) ) { return true; @@ -45,7 +45,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ // Handle Ownership. if( Penumbra.Config.UseOwnerNameForCharacterCollection ) { - identifier = _manager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); + identifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); return CheckWorlds( identifier, out collection ); } @@ -59,12 +59,12 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: - return CheckWorlds( _manager.GetCurrentPlayer(), out collection ); + return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); case SpecialActor.ExamineScreen: { - return CheckWorlds( _manager.GetInspectPlayer(), out collection! ) - || CheckWorlds( _manager.GetCardPlayer(), out collection! ) - || CheckWorlds( _manager.GetGlamourPlayer(), out collection! ); + return CheckWorlds( _actorManager.GetInspectPlayer(), out collection! ) + || CheckWorlds( _actorManager.GetCardPlayer(), out collection! ) + || CheckWorlds( _actorManager.GetGlamourPlayer(), out collection! ); } } @@ -76,10 +76,10 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject ), out collection ); public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject ), out collection ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { @@ -94,7 +94,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - identifier = _manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + identifier = _actorManager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) { return true; diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 8c0997f0..16581e9c 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; using Penumbra.String; using Penumbra.Util; @@ -11,7 +12,66 @@ namespace Penumbra.Collections; public partial class IndividualCollections { - public const int Version = 1; + public JArray ToJObject() + { + var ret = new JArray(); + foreach( var (name, identifiers, collection) in Assignments ) + { + var tmp = identifiers[0].ToJson(); + tmp.Add( "Collection", collection.Name ); + tmp.Add( "Display", name ); + ret.Add( tmp ); + } + + return ret; + } + + public bool ReadJObject( JArray? obj, ModCollection.Manager manager ) + { + if( obj == null ) + { + return true; + } + + var changes = false; + foreach( var data in obj ) + { + try + { + var identifier = Penumbra.Actors.FromJson( data as JObject ); + var group = GetGroup( identifier ); + if( group.Length == 0 || group.Any( i => !i.IsValid ) ) + { + changes = true; + ChatUtil.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); + continue; + } + + var collectionName = data[ "Collection" ]?.ToObject< string >() ?? string.Empty; + if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) ) + { + changes = true; + ChatUtil.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", + NotificationType.Warning ); + continue; + } + + if( !Add( group, collection ) ) + { + changes = true; + ChatUtil.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", + NotificationType.Warning ); + } + } + catch( Exception e ) + { + changes = true; + ChatUtil.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); + } + } + + return changes; + } internal void Migrate0To1( Dictionary< string, ModCollection > old ) { @@ -28,30 +88,30 @@ public partial class IndividualCollections var kind = ObjectKind.None; var lowerName = name.ToLowerInvariant(); // Prefer matching NPC names, fewer false positives than preferring players. - if( FindDataId( lowerName, _manager.Companions, out var dataId ) ) + if( FindDataId( lowerName, _actorManager.Companions, out var dataId ) ) { kind = ObjectKind.Companion; } - else if( FindDataId( lowerName, _manager.Mounts, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.Mounts, out dataId ) ) { kind = ObjectKind.MountType; } - else if( FindDataId( lowerName, _manager.BNpcs, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.BNpcs, out dataId ) ) { kind = ObjectKind.BattleNpc; } - else if( FindDataId( lowerName, _manager.ENpcs, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.ENpcs, out dataId ) ) { kind = ObjectKind.EventNpc; } - var identifier = _manager.CreateNpc( kind, dataId ); + var identifier = _actorManager.CreateNpc( kind, dataId ); if( identifier.IsValid ) { // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. var group = GetGroup( identifier ); var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); - if( Add( $"{_manager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) + if( Add( $"{_actorManager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) { Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); } @@ -65,10 +125,10 @@ public partial class IndividualCollections // If it is not a valid NPC name, check if it can be a player name. else if( ActorManager.VerifyPlayerName( name ) ) { - identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); + identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); // Try to migrate the player name without logging full names. - if( Add( $"{name} ({_manager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) + if( Add( $"{name} ({_actorManager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) { Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); } diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index fc4a83b2..a8239eb5 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; +using OtterGui.Filesystem; using Penumbra.GameData.Actors; using Penumbra.String; @@ -9,18 +10,18 @@ namespace Penumbra.Collections; public sealed partial class IndividualCollections { - private readonly ActorManager _manager; - private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); - private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); + private readonly ActorManager _actorManager; + private readonly List< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); + private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); - public IReadOnlyDictionary< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments + public IReadOnlyList< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments => _assignments; public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals => _individuals; - public IndividualCollections( ActorManager manager ) - => _manager = manager; + public IndividualCollections( ActorManager actorManager ) + => _actorManager = actorManager; public enum AddResult { @@ -61,7 +62,7 @@ public sealed partial class IndividualCollections return AddResult.Invalid; } - var identifier = _manager.CreatePlayer( playerName, homeWorld ); + var identifier = _actorManager.CreatePlayer( playerName, homeWorld ); identifiers = new[] { identifier }; break; case IdentifierType.Owned: @@ -70,10 +71,10 @@ public sealed partial class IndividualCollections return AddResult.Invalid; } - identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); + identifiers = dataIds.Select( id => _actorManager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); break; case IdentifierType.Npc: - identifiers = dataIds.Select( id => _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray(); + identifiers = dataIds.Select( id => _actorManager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray(); break; default: identifiers = Array.Empty< ActorIdentifier >(); @@ -109,38 +110,33 @@ public sealed partial class IndividualCollections { IdentifierType.Player => new[] { identifier.CreatePermanent() }, IdentifierType.Special => new[] { identifier }, - IdentifierType.Owned => CreateNpcs( _manager, identifier.CreatePermanent() ), - IdentifierType.Npc => CreateNpcs( _manager, identifier ), + IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), + IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), _ => Array.Empty< ActorIdentifier >(), }; } - public bool Add( ActorIdentifier[] identifiers, ModCollection collection ) + internal bool Add( ActorIdentifier[] identifiers, ModCollection collection ) { if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid ) { return false; } - var name = identifiers[ 0 ].Type switch - { - IdentifierType.Player => $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})", - IdentifierType.Owned => - $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})'s {_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )}", - IdentifierType.Npc => $"{_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )} ({identifiers[ 0 ].Kind})", - _ => string.Empty, - }; + var name = DisplayString( identifiers[ 0 ] ); return Add( name, identifiers, collection ); } private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) { - if( CanAdd( identifiers ) != AddResult.Valid || displayName.Length == 0 || _assignments.ContainsKey( displayName ) ) + if( CanAdd( identifiers ) != AddResult.Valid + || displayName.Length == 0 + || _assignments.Any( a => a.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) ) { return false; } - _assignments.Add( displayName, ( identifiers, collection ) ); + _assignments.Add( ( displayName, identifiers, collection ) ); foreach( var identifier in identifiers ) { _individuals.Add( identifier, collection ); @@ -149,21 +145,21 @@ public sealed partial class IndividualCollections return true; } - public bool ChangeCollection( string displayName, ModCollection newCollection ) - { - var displayIndex = _assignments.IndexOfKey( displayName ); - return ChangeCollection( displayIndex, newCollection ); - } + internal bool ChangeCollection( ActorIdentifier identifier, ModCollection newCollection ) + => ChangeCollection( DisplayString( identifier ), newCollection ); - public bool ChangeCollection( int displayIndex, ModCollection newCollection ) + internal bool ChangeCollection( string displayName, ModCollection newCollection ) + => ChangeCollection( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ), newCollection ); + + internal bool ChangeCollection( int displayIndex, ModCollection newCollection ) { - if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments.Values[ displayIndex ].Collection == newCollection ) + if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments[ displayIndex ].Collection == newCollection ) { return false; } - _assignments.Values[ displayIndex ] = _assignments.Values[ displayIndex ] with { Collection = newCollection }; - foreach( var identifier in _assignments.Values[ displayIndex ].Identifiers ) + _assignments[ displayIndex ] = _assignments[ displayIndex ] with { Collection = newCollection }; + foreach( var identifier in _assignments[ displayIndex ].Identifiers ) { _individuals[ identifier ] = newCollection; } @@ -171,20 +167,20 @@ public sealed partial class IndividualCollections return true; } - public bool Delete( string displayName ) - { - var displayIndex = _assignments.IndexOfKey( displayName ); - return Delete( displayIndex ); - } + internal bool Delete( ActorIdentifier identifier ) + => Delete( DisplayString( identifier ) ); - public bool Delete( int displayIndex ) + internal bool Delete( string displayName ) + => Delete( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) ); + + internal bool Delete( int displayIndex ) { if( displayIndex < 0 || displayIndex >= _assignments.Count ) { return false; } - var (identifiers, _) = _assignments.Values[ displayIndex ]; + var (name, identifiers, _) = _assignments[ displayIndex ]; _assignments.RemoveAt( displayIndex ); foreach( var identifier in identifiers ) { @@ -193,4 +189,19 @@ public sealed partial class IndividualCollections return true; } + + internal bool Move( int from, int to ) + => _assignments.Move( from, to ); + + private string DisplayString( ActorIdentifier identifier ) + { + return identifier.Type switch + { + IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Owned => + $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}", + IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", + _ => string.Empty, + }; + } } \ No newline at end of file diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 20df5d04..ee1ee6ac 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -224,10 +224,52 @@ public partial class Configuration CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, DefaultCollection, + SaveActiveCollectionsV0( DefaultCollection, CurrentCollection, DefaultCollection, CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); } + // Outdated saving using the Characters list. + private static void SaveActiveCollectionsV0( string def, string ui, string current, IEnumerable<(string, string)> characters, + IEnumerable<(CollectionType, string)> special ) + { + var file = ModCollection.Manager.ActiveCollectionFile; + try + { + using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); + using var writer = new StreamWriter( stream ); + using var j = new JsonTextWriter( writer ); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName( nameof( ModCollection.Manager.Default ) ); + j.WriteValue( def ); + j.WritePropertyName( nameof( ModCollection.Manager.Interface ) ); + j.WriteValue( ui ); + j.WritePropertyName( nameof( ModCollection.Manager.Current ) ); + j.WriteValue( current ); + foreach( var (type, collection) in special ) + { + j.WritePropertyName( type.ToString() ); + j.WriteValue( collection ); + } + + j.WritePropertyName( "Characters" ); + j.WriteStartObject(); + foreach( var (character, collection) in characters ) + { + j.WritePropertyName( character, true ); + j.WriteValue( collection ); + } + + j.WriteEndObject(); + j.WriteEndObject(); + Penumbra.Log.Verbose( "Active Collections saved." ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); + } + } + // Collections were introduced and the previous CurrentCollection got put into ModDirectory. private void Version0To1() { diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 3742a51e..3f53c089 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.String.Classes; @@ -221,9 +222,9 @@ public unsafe partial class PathResolver } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name ) + private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) { - if( type is CollectionType.Inactive or CollectionType.Current ) + if( type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface ) { return; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 91561644..b7f45c4b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -355,26 +355,27 @@ public class Penumbra : IDalamudPlugin return false; } - var oldCollection = CollectionManager.ByType( type, characterName ); - if( collection == oldCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); - return false; - } - - if( oldCollection == null ) - { - if( type.IsSpecial() ) - { - CollectionManager.CreateSpecialCollection( type ); - } - else if( type is CollectionType.Individual ) - { - CollectionManager.CreateCharacterCollection( characterName! ); - } - } - - CollectionManager.SetCollection( collection, type, characterName ); + // TODO + //var oldCollection = CollectionManager.ByType( type, characterName ); + //if( collection == oldCollection ) + //{ + // Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); + // return false; + //} + // + //if( oldCollection == null ) + //{ + // if( type.IsSpecial() ) + // { + // CollectionManager.CreateSpecialCollection( type ); + // } + // else if( type is CollectionType.Individual ) + // { + // CollectionManager.CreateIndividualCollection( characterName! ); + // } + //} + // + //CollectionManager.SetCollection( collection, type, characterName ); Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); return true; } @@ -507,8 +508,15 @@ public class Penumbra : IDalamudPlugin ModManager.Sum( m => m.TotalManipulations ) ); sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions ); - string CharacterName( string name ) - => string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ) + ':'; + string CharacterName( ActorIdentifier id, string name ) + { + if( id.Type is IdentifierType.Player or IdentifierType.Owned ) + { + return string.Join( " ", name.Split( ' ', 3 ).Select( n => $"{n[ 0 ]}." ) ) + ':'; + } + + return name + ':'; + } void PrintCollection( ModCollection c ) => sb.AppendFormat( "**Collection {0}**\n" @@ -535,9 +543,9 @@ public class Penumbra : IDalamudPlugin } } - foreach( var (name, collection) in CollectionManager.Characters ) + foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments ) { - sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( name ) ); + sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( id[ 0 ], name ) ); } foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 10311595..ac8beade 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; +using Penumbra.GameData.Actors; namespace Penumbra.UI.Classes; @@ -47,7 +48,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); + OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" ); } public override void Dispose() @@ -377,7 +378,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod OnSelectionChange( Selected, Selected, default ); } - private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) + private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _ ) { if( collectionType != CollectionType.Current || oldCollection == newCollection ) { diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index a7a0f65b..9737969d 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -11,7 +11,6 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Components; using OtterGui.Widgets; using Penumbra.GameData.Actors; -using Lumina.Data.Parsing; namespace Penumbra.UI; @@ -110,6 +109,7 @@ public partial class ConfigWindow return ret; } + private int _individualDragDropIdx = -1; private void DrawIndividualAssignments() { @@ -124,19 +124,42 @@ public partial class ConfigWindow ImGui.Separator(); for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) { - var (name, collection) = Penumbra.CollectionManager.Individuals[ i ]; + var (name, _) = Penumbra.CollectionManager.Individuals[ i ]; using var id = ImRaii.PushId( i ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Individual, true, name ); + CollectionsWithEmpty.Draw( string.Empty, _window._inputTextWidth.X, i ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) { - Penumbra.CollectionManager.Individuals.Delete( i ); + Penumbra.CollectionManager.RemoveIndividualCollection( i ); } ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( name ); + ImGui.Selectable( name ); + using( var source = ImRaii.DragDropSource() ) + { + if( source ) + { + ImGui.SetDragDropPayload( "Individual", IntPtr.Zero, 0 ); + _individualDragDropIdx = i; + } + } + + using( var target = ImRaii.DragDropTarget() ) + { + if( !target.Success || !ImGuiUtil.IsDropping( "Individual" ) ) + { + continue; + } + + if( _individualDragDropIdx >= 0 ) + { + Penumbra.CollectionManager.MoveIndividualCollection( _individualDragDropIdx, i ); + } + + _individualDragDropIdx = -1; + } } ImGui.Dummy( Vector2.Zero ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 1fd968c5..aa6b6a4e 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; @@ -41,7 +39,7 @@ public partial class ConfigWindow // Input text fields. private string _newCollectionName = string.Empty; - private bool _canAddCollection = false; + private bool _canAddCollection; // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -103,7 +101,7 @@ public partial class ConfigWindow private void DrawCurrentCollectionSelector( Vector2 width ) { using var group = ImRaii.Group(); - DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); + DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( SelectedCollection, "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." ); @@ -126,7 +124,7 @@ public partial class ConfigWindow private void DrawDefaultCollectionSelector() { using var group = ImRaii.Group(); - DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); + DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( DefaultCollection, $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," @@ -136,7 +134,7 @@ public partial class ConfigWindow private void DrawInterfaceCollectionSelector() { using var group = ImRaii.Group(); - DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true, null ); + DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( InterfaceCollection, $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); @@ -147,7 +145,7 @@ public partial class ConfigWindow public (CollectionType, string, string)? CurrentType => CollectionTypeExtensions.Special[ CurrentIdx ]; - public int CurrentIdx = 0; + public int CurrentIdx; private readonly float _unscaledWidth; private readonly string _label; @@ -219,7 +217,7 @@ public partial class ConfigWindow if( collection != null ) { using var id = ImRaii.PushId( ( int )type ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 1207fa57..394f78fe 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.UI.Classes; @@ -98,12 +99,21 @@ public partial class ConfigWindow : base( items ) { } - public void Draw( string label, float width, CollectionType type, string? characterName ) + public void Draw( string label, float width, int individualIdx ) { - var current = Penumbra.CollectionManager.ByType( type, characterName ); + var (_, collection) = Penumbra.CollectionManager.Individuals[ individualIdx ]; + if( Draw( label, collection.Name, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) + { + Penumbra.CollectionManager.SetCollection( CurrentSelection, CollectionType.Individual, individualIdx ); + } + } + + public void Draw( string label, float width, CollectionType type ) + { + var current = Penumbra.CollectionManager.ByType( type, ActorIdentifier.Invalid ); if( Draw( label, current?.Name ?? string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) { - Penumbra.CollectionManager.SetCollection( CurrentSelection, type, characterName ); + Penumbra.CollectionManager.SetCollection( CurrentSelection, type ); } } @@ -115,9 +125,8 @@ public partial class ConfigWindow private static readonly CollectionSelector Collections = new(Penumbra.CollectionManager.OrderBy( c => c.Name )); // Draw a collection selector of a certain width for a certain type. - private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty, - string? characterName ) - => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType, characterName ); + private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty ) + => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType ); // Set up the file selector with the right flags and custom side bar items. public static FileDialogManager SetupFileManager() diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index eb3a3a8a..b520dd91 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -136,7 +136,7 @@ public partial class ConfigWindow ImGui.SameLine(); DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false ); } OpenTutorial( BasicTutorialSteps.CollectionSelectors ); From 353694177e978f3e5f78d9efd18c0d6ddb839d98 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Nov 2022 11:42:23 +0100 Subject: [PATCH 0580/2451] Use Path.Join and Path.GetFileName for adding mods to not allow arbitrary folder but only those in the penumbra root directory. --- Penumbra/Api/PenumbraApi.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 3521369c..dd83e2f0 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -371,7 +371,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc AddMod( string modDirectory ) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, modDirectory ) ); + var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, modDirectory ) ); if( !dir.Exists ) { return PenumbraApiEc.FileMissing; @@ -572,7 +572,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.InvalidArgument, string.Empty ); } - if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( NameToIdentifier(character) ) + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( NameToIdentifier( character ) ) || Penumbra.TempMods.Collections.ContainsKey( character ) ) { return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); @@ -682,7 +682,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) ? c - : Penumbra.CollectionManager.Individual( NameToIdentifier(characterName) ); + : Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } From 03bbba67359ec836791d0d39beb5fe75b94c1981 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Nov 2022 11:42:23 +0100 Subject: [PATCH 0581/2451] Use Path.Join and Path.GetFileName for adding mods to not allow arbitrary folder but only those in the penumbra root directory. --- Penumbra/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index dd83e2f0..ace2b9f4 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -371,7 +371,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc AddMod( string modDirectory ) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, modDirectory ) ); + var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory) ) ); if( !dir.Exists ) { return PenumbraApiEc.FileMissing; From f676bd18895edd313ba67deac857460bd7716dc3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Nov 2022 15:53:40 +0100 Subject: [PATCH 0582/2451] Do not check every identifier. --- .../Actors/ActorManager.Identifiers.cs | 41 +++++++++++++------ .../IndividualCollections.Access.cs | 4 +- .../Resolver/PathResolver.DrawObjectState.cs | 1 - .../Resolver/PathResolver.Identification.cs | 2 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index ec0782fb..c5066174 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -107,9 +107,9 @@ public partial class ActorManager } /// - /// Compute an ActorIdentifier from a GameObject. + /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// - public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor) + public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, bool check = true) { if (actor == null) return ActorIdentifier.Invalid; @@ -119,11 +119,11 @@ public partial class ActorManager { var parentIdx = _toParentIdx(idx); if (parentIdx >= 0) - return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx)); + return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx), check); } else if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) { - return CreateSpecial((SpecialActor)idx); + return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue); } switch ((ObjectKind)actor->ObjectKind) @@ -132,7 +132,9 @@ public partial class ActorManager { var name = new ByteString(actor->Name); var homeWorld = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->HomeWorld; - return CreatePlayer(name, homeWorld); + return check + ? CreatePlayer(name, homeWorld) + : CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue); } case ObjectKind.BattleNpc: { @@ -146,14 +148,24 @@ public partial class ActorManager var name = new ByteString(owner->GameObject.Name); var homeWorld = owner->HomeWorld; - return CreateOwned(name, homeWorld, ObjectKind.BattleNpc, - ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); + return check + ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, + ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID) + : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, + ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); } - return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, - actor->ObjectIndex); + return check + ? CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, + actor->ObjectIndex) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.BattleNpc, + ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); } - case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); + case ObjectKind.EventNpc: + return check + ? CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, + actor->ObjectIndex); case ObjectKind.MountType: case ObjectKind.Companion: case (ObjectKind)15: // TODO: CS Update @@ -166,7 +178,10 @@ public partial class ActorManager return ActorIdentifier.Invalid; var dataId = GetCompanionId(actor, &owner->GameObject); - return CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId); + return check + ? CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId) + : CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld, + (ObjectKind)actor->ObjectKind, dataId); } default: return ActorIdentifier.Invalid; } @@ -187,8 +202,8 @@ public partial class ActorManager }; } - public unsafe ActorIdentifier FromObject(GameObject? actor) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero)); + public unsafe ActorIdentifier FromObject(GameObject? actor, bool check = true) + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), check); public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 29c403d7..f09dadb0 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -76,10 +76,10 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 3f53c089..bf335f55 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; -using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index d7b25cd3..18c55ca8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -38,7 +38,7 @@ public unsafe partial class PathResolver } else { - var identifier = Penumbra.Actors.FromObject( gameObject ); + var identifier = Penumbra.Actors.FromObject( gameObject, false ); var collection = CollectionByIdentifier( identifier ) ?? CheckYourself( identifier, gameObject ) ?? CollectionByAttributes( gameObject ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 1818fbe8..4142faa5 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -190,7 +190,7 @@ public partial class ConfigWindow { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); - var identifier = Penumbra.Actors.FromObject( obj ); + var identifier = Penumbra.Actors.FromObject( obj, true ); ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); ImGuiUtil.DrawTableColumn( identifier.DataId.ToString() ); } From 2fac9234526d3d9b4f26c0078cbcee9c3cc31964 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Nov 2022 19:53:06 +0100 Subject: [PATCH 0583/2451] Cache collections instead of looking them up for every single file. --- .../Actors/ActorManager.Identifiers.cs | 11 +- Penumbra/Api/TempModManager.cs | 4 + .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionType.cs | 1 + .../Resolver/IdentifiedCollectionCache.cs | 129 ++++++++++++++++++ .../Resolver/PathResolver.AnimationState.cs | 10 +- .../Resolver/PathResolver.DrawObjectState.cs | 10 +- .../Resolver/PathResolver.Identification.cs | 11 +- .../Resolver/PathResolver.ResolverHooks.cs | 1 - Penumbra/Interop/Resolver/PathResolver.cs | 24 ++-- Penumbra/UI/Classes/ModFileSystemSelector.cs | 1 - Penumbra/UI/ConfigWindow.DebugTab.cs | 18 +++ 12 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index c5066174..48c8a2e2 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -162,10 +162,15 @@ public partial class ActorManager ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); } case ObjectKind.EventNpc: + { + var dataId = actor->DataID; + // Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF + if (dataId == 0xf845d) + dataId = actor->GetNpcID(); return check - ? CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, - actor->ObjectIndex); + ? CreateNpc(ObjectKind.EventNpc, dataId, actor->ObjectIndex) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, dataId); + } case ObjectKind.MountType: case ObjectKind.Companion: case (ObjectKind)15: // TODO: CS Update diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index bb41f6f1..132b772a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -35,6 +35,8 @@ public class TempModManager public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) => Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); + public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; + // These functions to check specific redirections or meta manipulations for existence are currently unused. //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) //{ @@ -151,6 +153,7 @@ public class TempModManager { var collection = ModCollection.CreateNewTemporary( tag, characterName ); _collections[ characterName ] = collection; + CollectionChanged?.Invoke(CollectionType.Temporary, null, collection ); return collection.Name; } @@ -160,6 +163,7 @@ public class TempModManager { _mods.Remove( c ); c.ClearCache(); + CollectionChanged?.Invoke( CollectionType.Temporary, c, null ); return true; } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index e30dcece..a8feb7f0 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -126,7 +126,7 @@ public partial class ModCollection RemoveCache( oldCollectionIdx ); UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, Individuals[ individualIndex ].DisplayName ); + CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); } private void UpdateCurrentCollectionInUse() diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index fed83db5..30da217d 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -99,6 +99,7 @@ public enum CollectionType : byte Interface, // The ui collection was changed Individual, // An individual collection was changed Current, // The current collection was changed + Temporary, // A temporary collections was set or deleted via IPC } public static class CollectionTypeExtensions diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs new file mode 100644 index 00000000..a6692839 --- /dev/null +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Collections; +using Penumbra.GameData.Actors; + +namespace Penumbra.Interop.Resolver; + +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > +{ + private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); + private bool _dirty = false; + private bool _enabled = false; + + public IdentifiedCollectionCache() + { + SignatureHelper.Initialise( this ); + } + + public void Enable() + { + if( _enabled ) + { + return; + } + + Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; + Penumbra.TempMods.CollectionChanged += CollectionChangeClear; + Dalamud.ClientState.TerritoryChanged += TerritoryClear; + _characterDtorHook.Enable(); + _enabled = true; + } + + public void Disable() + { + if( !_enabled ) + { + return; + } + + Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; + Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; + Dalamud.ClientState.TerritoryChanged -= TerritoryClear; + _characterDtorHook.Disable(); + _enabled = false; + } + + public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) + { + if( _dirty ) + { + _dirty = false; + _cache.Clear(); + } + + _cache[ ( IntPtr )data ] = ( identifier, collection ); + return collection.ToResolveData( data ); + } + + public bool TryGetValue( GameObject* gameObject, out ResolveData resolve ) + { + if( _dirty ) + { + _dirty = false; + _cache.Clear(); + } + else if( _cache.TryGetValue( ( IntPtr )gameObject, out var p ) ) + { + resolve = p.Item2.ToResolveData( gameObject ); + return true; + } + + resolve = default; + return false; + } + + public void Dispose() + { + Disable(); + _characterDtorHook.Dispose(); + GC.SuppressFinalize( this ); + } + + public IEnumerator< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > GetEnumerator() + { + foreach( var (address, (identifier, collection)) in _cache ) + { + if( _dirty ) + { + yield break; + } + + yield return ( address, identifier, collection ); + } + } + + ~IdentifiedCollectionCache() + => Dispose(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private void CollectionChangeClear( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + { + if( type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive) ) + { + _dirty = _cache.Count > 0; + } + } + + private void TerritoryClear( object? _1, ushort _2 ) + => _dirty = _cache.Count > 0; + + private delegate void CharacterDestructorDelegate( Character* character ); + + [Signature( "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05", + DetourName = nameof( CharacterDestructorDetour ) )] + private Hook< CharacterDestructorDelegate > _characterDtorHook = null!; + + private void CharacterDestructorDetour( Character* character ) + { + _cache.Remove( ( IntPtr )character ); + _characterDtorHook.Original( character ); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 3744bb67..3c842250 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -118,7 +118,7 @@ public unsafe partial class PathResolver if( idx >= 0 && idx < Dalamud.Objects.Length ) { var obj = Dalamud.Objects[ idx ]; - _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : ResolveData.Invalid; + _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; } else { @@ -165,7 +165,7 @@ public unsafe partial class PathResolver private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) { var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); _animationLoadData = last; return ret; @@ -187,7 +187,7 @@ public unsafe partial class PathResolver var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) { - _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); } } @@ -202,7 +202,7 @@ public unsafe partial class PathResolver private void SomeActionLoadDetour( IntPtr gameObject ) { var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); _someActionLoadHook.Original( gameObject ); _animationLoadData = last; } @@ -214,7 +214,7 @@ public unsafe partial class PathResolver { var last = _animationLoadData; var gameObject = ( GameObject* )( unk - 0x8D0 ); - _animationLoadData = IdentifyCollection( gameObject ); + _animationLoadData = IdentifyCollection( gameObject, true ); _someOtherAvfxHook.Original( unk ); _animationLoadData = last; } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index bf335f55..47cd5e6d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -44,7 +44,7 @@ public unsafe partial class PathResolver { if( parentObject == IntPtr.Zero && LastGameObject != null ) { - var collection = IdentifyCollection( LastGameObject ); + var collection = IdentifyCollection( LastGameObject, true ); _drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); return collection; } @@ -86,6 +86,7 @@ public unsafe partial class PathResolver _weaponReloadHook.Enable(); InitializeDrawObjects(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; + Penumbra.TempMods.CollectionChanged += CheckCollections; } public void Disable() @@ -95,6 +96,7 @@ public unsafe partial class PathResolver _enableDrawHook.Disable(); _weaponReloadHook.Disable(); Penumbra.CollectionManager.CollectionChanged -= CheckCollections; + Penumbra.TempMods.CollectionChanged -= CheckCollections; } public void Dispose() @@ -139,7 +141,7 @@ public unsafe partial class PathResolver var meta = DisposableContainer.Empty; if( LastGameObject != null ) { - _lastCreatedCollection = IdentifyCollection( LastGameObject ); + _lastCreatedCollection = IdentifyCollection( LastGameObject, false ); // Change the transparent or 1.0 Decal if necessary. var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); // Change the rsp parameters if necessary. @@ -235,7 +237,7 @@ public unsafe partial class PathResolver _drawObjectToObject.Remove( key ); } - var newCollection = IdentifyCollection( obj ); + var newCollection = IdentifyCollection( obj, false ); _drawObjectToObject[ key ] = ( newCollection, idx ); } } @@ -249,7 +251,7 @@ public unsafe partial class PathResolver var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) { - _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); + _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex ); } } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 18c55ca8..d031b153 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -17,7 +17,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { // Identify the correct collection for a GameObject by index and name. - private static ResolveData IdentifyCollection( GameObject* gameObject ) + private static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { if( gameObject == null ) { @@ -26,6 +26,11 @@ public unsafe partial class PathResolver try { + if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) ) + { + return data; + } + // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. @@ -34,7 +39,7 @@ public unsafe partial class PathResolver var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; - return collection.ToResolveData( gameObject ); + return IdentifiedCache.Set( collection, ActorIdentifier.Invalid, gameObject ); } else { @@ -44,7 +49,7 @@ public unsafe partial class PathResolver ?? CollectionByAttributes( gameObject ) ?? CheckOwnedCollection( identifier, gameObject ) ?? Penumbra.CollectionManager.Default; - return collection.ToResolveData( gameObject ); + return IdentifiedCache.Set( collection, identifier, gameObject ); } } catch( Exception e ) diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 0e0716ea..e3fb47e0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; -using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4f9b5059..aa1f9055 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -26,14 +26,15 @@ public partial class PathResolver : IDisposable { public bool Enabled { get; private set; } - private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(); - private static readonly DrawObjectState DrawObjects = new(); - private static readonly BitArray ValidHumanModels; - private readonly AnimationState _animations; - private readonly PathState _paths; - private readonly MetaState _meta; - private readonly MaterialState _materials; + private readonly ResourceLoader _loader; + private static readonly CutsceneCharacters Cutscenes = new(); + private static readonly DrawObjectState DrawObjects = new(); + private static readonly BitArray ValidHumanModels; + internal static readonly IdentifiedCollectionCache IdentifiedCache = new(); + private readonly AnimationState _animations; + private readonly PathState _paths; + private readonly MetaState _meta; + private readonly MaterialState _materials; static PathResolver() => ValidHumanModels = GetValidHumanModels( Dalamud.GameData ); @@ -87,6 +88,7 @@ public partial class PathResolver : IDisposable Enabled = true; Cutscenes.Enable(); DrawObjects.Enable(); + IdentifiedCache.Enable(); _animations.Enable(); _paths.Enable(); _meta.Enable(); @@ -107,6 +109,7 @@ public partial class PathResolver : IDisposable _animations.Disable(); DrawObjects.Disable(); Cutscenes.Disable(); + IdentifiedCache.Disable(); _paths.Disable(); _meta.Disable(); _materials.Disable(); @@ -122,6 +125,7 @@ public partial class PathResolver : IDisposable _animations.Dispose(); DrawObjects.Dispose(); Cutscenes.Dispose(); + IdentifiedCache.Dispose(); _meta.Dispose(); _materials.Dispose(); } @@ -147,11 +151,11 @@ public partial class PathResolver : IDisposable if( DrawObjects.LastGameObject != null && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) { - resolveData = IdentifyCollection( DrawObjects.LastGameObject ); + resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); return DrawObjects.LastGameObject; } - resolveData = IdentifyCollection( null ); + resolveData = IdentifyCollection( null, true ); return null; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ac8beade..7681d09c 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,7 +14,6 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 4142faa5..57351b71 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -250,6 +251,23 @@ public partial class ConfigWindow } } + using( var identifiedTree = ImRaii.TreeNode( "Identified Collections" ) ) + { + if( identifiedTree ) + { + using var table = ImRaii.Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (address, identifier, collection) in PathResolver.IdentifiedCache ) + { + ImGuiUtil.DrawTableColumn( $"0x{address:X}" ); + ImGuiUtil.DrawTableColumn( identifier.ToString() ); + ImGuiUtil.DrawTableColumn( collection.Name ); + } + } + } + } + using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ); if( cutsceneTree ) { From 893e0a13bdb489ac514c8caf37ecf745ef705e1a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Nov 2022 20:12:15 +0100 Subject: [PATCH 0584/2451] Change entirely backward compatible API functions to do reasonable things in new system. --- Penumbra/Api/PenumbraApi.cs | 76 +++++++++++---- Penumbra/Api/TempModManager.cs | 96 +++++++++++++++---- .../Resolver/PathResolver.Identification.cs | 2 +- 3 files changed, 135 insertions(+), 39 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index ace2b9f4..94e921de 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -210,14 +210,22 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ResolvePath( path, Penumbra.ModManager, PathResolver.PlayerCollection() ); } + // TODO: cleanup when incrementing API level public string ResolvePath( string path, string characterName ) + => ResolvePath( path, characterName, ushort.MaxValue ); + + public string ResolvePath( string path, string characterName, ushort worldId ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ) ); + Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ) ); } + // TODO: cleanup when incrementing API level public string[] ReverseResolvePath( string path, string characterName ) + => ReverseResolvePath( path, characterName, ushort.MaxValue ); + + public string[] ReverseResolvePath( string path, string characterName, ushort worldId ) { CheckInitialized(); if( !Penumbra.Config.EnableMods ) @@ -225,7 +233,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return new[] { path }; } - var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ).ReverseResolvePath( new FullPath( path ) ); + var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ).ReverseResolvePath( new FullPath( path ) ); return ret.Select( r => r.ToString() ).ToArray(); } @@ -296,10 +304,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.CollectionManager.Interface.Name; } + // TODO: cleanup when incrementing API level public (string, bool) GetCharacterCollection( string characterName ) + => GetCharacterCollection( characterName, ushort.MaxValue ); + + public (string, bool) GetCharacterCollection( string characterName, ushort worldId ) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName ), out var collection ) + return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection ) ? ( collection.Name, true ) : ( Penumbra.CollectionManager.Default.Name, false ); } @@ -371,7 +383,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc AddMod( string modDirectory ) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory) ) ); + var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, Path.GetFileName( modDirectory ) ) ); if( !dir.Exists ) { return PenumbraApiEc.FileMissing; @@ -564,34 +576,50 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) + => CreateTemporaryCollection( tag, character, forceOverwriteCharacter, ushort.MaxValue ); + + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter, ushort worldId ) { CheckInitialized(); - if( character.Length is 0 or > 32 || tag.Length == 0 ) + if( !ActorManager.VerifyPlayerName( character.AsSpan() ) || tag.Length == 0 ) { return ( PenumbraApiEc.InvalidArgument, string.Empty ); } - if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( NameToIdentifier( character ) ) - || Penumbra.TempMods.Collections.ContainsKey( character ) ) + var identifier = NameToIdentifier( character, worldId ); + if( !identifier.IsValid ) + { + return ( PenumbraApiEc.InvalidArgument, string.Empty ); + } + + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) + || Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) ) { return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); } - var name = Penumbra.TempMods.SetTemporaryCollection( tag, character ); - return ( PenumbraApiEc.Success, name ); + var name = Penumbra.TempMods.CreateTemporaryCollection( tag, character ); + if( name.Length == 0 ) + { + return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); + } + + if( Penumbra.TempMods.AddIdentifier( name, identifier ) ) + { + return ( PenumbraApiEc.Success, name ); + } + + Penumbra.TempMods.RemoveTemporaryCollection( name ); + return ( PenumbraApiEc.UnknownError, string.Empty ); } public PenumbraApiEc RemoveTemporaryCollection( string character ) { CheckInitialized(); - if( !Penumbra.TempMods.Collections.ContainsKey( character ) ) - { - return PenumbraApiEc.NothingChanged; - } - - Penumbra.TempMods.RemoveTemporaryCollection( character ); - return PenumbraApiEc.Success; + return Penumbra.TempMods.RemoveByCharacterName( character ) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; } public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ) @@ -677,12 +705,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } + // TODO: cleanup when incrementing API public string GetMetaManipulations( string characterName ) + => GetMetaManipulations( characterName, ushort.MaxValue ); + + public string GetMetaManipulations( string characterName, ushort worldId ) { CheckInitialized(); - var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) + var identifier = NameToIdentifier( characterName, worldId ); + var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) ? c - : Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ); + : Penumbra.CollectionManager.Individual( identifier ); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } @@ -830,10 +863,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void InvokePostSettingsPanel( string modDirectory ) => PostSettingsPanelDraw?.Invoke( modDirectory ); - // TODO - private static ActorIdentifier NameToIdentifier( string name ) + // TODO: replace all usages with ActorIdentifier stuff when incrementing API + private static ActorIdentifier NameToIdentifier( string name, ushort worldId ) { + // Verified to be valid name beforehand. var b = ByteString.FromStringUnsafe( name, false ); - return Penumbra.Actors.CreatePlayer( b, ( ushort )( Dalamud.ClientState.LocalPlayer?.HomeWorld.Id ?? ushort.MaxValue ) ); + return Penumbra.Actors.CreatePlayer( b, worldId ); } } \ No newline at end of file diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 132b772a..db087f9d 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,10 +1,11 @@ -using OtterGui; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Penumbra.GameData.Actors; +using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Api; @@ -21,7 +22,10 @@ public class TempModManager { private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); - private readonly Dictionary< string, ModCollection > _collections = new(); + private readonly Dictionary< string, ModCollection > _customCollections = new(); + public readonly IndividualCollections Collections = new(Penumbra.Actors); + + public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods => _mods; @@ -29,13 +33,11 @@ public class TempModManager public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections => _modsForAllCollections; - public IReadOnlyDictionary< string, ModCollection > Collections - => _collections; + public IReadOnlyDictionary< string, ModCollection > CustomCollections + => _customCollections; public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); - - public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; + => _customCollections.TryGetValue( name, out collection ); // These functions to check specific redirections or meta manipulations for existence are currently unused. //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) @@ -149,27 +151,87 @@ public class TempModManager return RedirectResult.Success; } - public string SetTemporaryCollection( string tag, string characterName ) + public string CreateTemporaryCollection( string tag, string customName ) { - var collection = ModCollection.CreateNewTemporary( tag, characterName ); - _collections[ characterName ] = collection; - CollectionChanged?.Invoke(CollectionType.Temporary, null, collection ); + var collection = ModCollection.CreateNewTemporary( tag, customName ); + if( _customCollections.ContainsKey( collection.Name ) ) + { + collection.ClearCache(); + return string.Empty; + } + _customCollections.Add( collection.Name, collection ); return collection.Name; } - public bool RemoveTemporaryCollection( string characterName ) + public bool RemoveTemporaryCollection( string collectionName ) { - if( _collections.Remove( characterName, out var c ) ) + if( !_customCollections.Remove( collectionName, out var collection ) ) { - _mods.Remove( c ); - c.ClearCache(); - CollectionChanged?.Invoke( CollectionType.Temporary, c, null ); + return false; + } + + _mods.Remove( collection ); + collection.ClearCache(); + for( var i = 0; i < Collections.Count; ++i ) + { + if( Collections[ i ].Collection == collection ) + { + CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName ); + Collections.Delete( i ); + } + } + + return true; + } + + public bool AddIdentifier( ModCollection collection, params ActorIdentifier[] identifiers ) + { + if( Collections.Add( identifiers, collection ) ) + { + CollectionChanged?.Invoke( CollectionType.Temporary, null, collection, Collections.Last().DisplayName ); return true; } return false; } + public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers ) + { + if( !_customCollections.TryGetValue( collectionName, out var collection ) ) + { + return false; + } + + return AddIdentifier( collection, identifiers ); + } + + public bool AddIdentifier( string collectionName, string characterName, ushort worldId = ushort.MaxValue ) + { + if( !ByteString.FromString( characterName, out var byteString, false ) ) + { + return false; + } + + var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); + if( !identifier.IsValid ) + { + return false; + } + + return AddIdentifier( collectionName, identifier ); + } + + internal bool RemoveByCharacterName( string characterName, ushort worldId = ushort.MaxValue ) + { + if( !ByteString.FromString( characterName, out var byteString, false ) ) + { + return false; + } + + var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); + return Collections.Individuals.TryGetValue( identifier, out var collection ) && RemoveTemporaryCollection( collection.Name ); + } + // Apply any new changes to the temporary mod. private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index d031b153..b5ce0e41 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -76,7 +76,7 @@ public unsafe partial class PathResolver // Check both temporary and permanent character collections. Temporary first. private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) - => Penumbra.TempMods.Collections.TryGetValue( identifier.ToString(), out var collection ) + => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) ? collection : null; From c8edd87df89d39ea73c80fb8ddcf1cffa3f33c12 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Nov 2022 21:16:38 +0100 Subject: [PATCH 0585/2451] Add changelog, improve Support Info, fix bug with folder checking, remove obsolete ownership settings. --- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Configuration.cs | 2 - Penumbra/Penumbra.cs | 114 +++++++++--------- Penumbra/UI/ConfigWindow.Changelog.cs | 28 +++++ .../UI/ConfigWindow.SettingsTab.General.cs | 18 +-- Penumbra/UI/ConfigWindow.SettingsTab.cs | 5 + 6 files changed, 99 insertions(+), 70 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index a8feb7f0..355c9297 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -170,7 +170,7 @@ public partial class ModCollection } // Wrappers around Individual Collection handling. - public void CreateIndividualCollection( ActorIdentifier[] identifiers ) + public void CreateIndividualCollection( params ActorIdentifier[] identifiers ) { if( Individuals.Add( identifiers, Default ) ) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 711164dd..b51251a1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -37,8 +37,6 @@ public partial class Configuration : IPluginConfiguration public bool UseCharacterCollectionInInspect { get; set; } = true; public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool PreferNamedCollectionsOverOwners { get; set; } = true; - public bool UseDefaultCollectionForRetainers { get; set; } = false; public bool HideRedrawBar { get; set; } = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b7f45c4b..eb04510a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -28,6 +28,7 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; +using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -337,7 +338,7 @@ public class Penumbra : IDalamudPlugin var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); if( split.Length < 2 || split[ 0 ].Length == 0 || split[ 1 ].Length == 0 ) { - Dalamud.Chat.Print( "You need to provide a collection and a character name in the form of 'collection | character' to set a character collection." ); + Dalamud.Chat.Print( "You need to provide a collection and a character name in the form of 'collection | name' to set an individual collection." ); return false; } @@ -355,27 +356,33 @@ public class Penumbra : IDalamudPlugin return false; } - // TODO - //var oldCollection = CollectionManager.ByType( type, characterName ); - //if( collection == oldCollection ) - //{ - // Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); - // return false; - //} - // - //if( oldCollection == null ) - //{ - // if( type.IsSpecial() ) - // { - // CollectionManager.CreateSpecialCollection( type ); - // } - // else if( type is CollectionType.Individual ) - // { - // CollectionManager.CreateIndividualCollection( characterName! ); - // } - //} - // - //CollectionManager.SetCollection( collection, type, characterName ); + var identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); + if( !identifier.IsValid ) + { + Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); + return false; + } + + var oldCollection = CollectionManager.ByType( type, identifier ); + if( collection == oldCollection ) + { + Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); + return false; + } + + if( oldCollection == null ) + { + if( type.IsSpecial() ) + { + CollectionManager.CreateSpecialCollection( type ); + } + else if( type is CollectionType.Individual ) + { + CollectionManager.CreateIndividualCollection( identifier ); + } + } + + CollectionManager.SetCollection( collection, type, CollectionManager.Individuals.Count - 1 ); Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); return true; } @@ -490,62 +497,61 @@ public class Penumbra : IDalamudPlugin var exists = Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ); var drive = exists ? new DriveInfo( new DirectoryInfo( Config.ModDirectory ).Root.FullName ) : null; sb.AppendLine( "**Settings**" ); - sb.AppendFormat( "> **`Plugin Version: `** {0}\n", Version ); - sb.AppendFormat( "> **`Commit Hash: `** {0}\n", CommitHash ); - sb.AppendFormat( "> **`Enable Mods: `** {0}\n", Config.EnableMods ); - sb.AppendFormat( "> **`Enable HTTP API: `** {0}\n", Config.EnableHttpApi ); - sb.AppendFormat( "> **`Root Directory: `** `{0}`, {1}\n", Config.ModDirectory, exists ? "Exists" : "Not Existing" ); - sb.AppendFormat( "> **`Free Drive Space: `** {0}\n", - drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" ); + sb.Append( $"> **`Plugin Version: `** {Version}\n" ); + sb.Append( $"> **`Commit Hash: `** {CommitHash}\n" ); + sb.Append( $"> **`Enable Mods: `** {Config.EnableMods}\n" ); + sb.Append( $"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n" ); + sb.Append( $"> **`Root Directory: `** `{Config.ModDirectory}`, {( exists ? "Exists" : "Not Existing" )}\n" ); + sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); + sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); + sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); + sb.Append( $"> **`Logging: `** Full: {Config.EnableFullResourceLogging}, Resource: {Config.EnableResourceLogging}\n" ); + sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.AppendLine( "**Mods**" ); - sb.AppendFormat( "> **`Installed Mods: `** {0}\n", ModManager.Count ); - sb.AppendFormat( "> **`Mods with Config: `** {0}\n", ModManager.Count( m => m.HasOptions ) ); - sb.AppendFormat( "> **`Mods with File Redirections: `** {0}, Total: {1}\n", ModManager.Count( m => m.TotalFileCount > 0 ), - ModManager.Sum( m => m.TotalFileCount ) ); - sb.AppendFormat( "> **`Mods with FileSwaps: `** {0}, Total: {1}\n", ModManager.Count( m => m.TotalSwapCount > 0 ), - ModManager.Sum( m => m.TotalSwapCount ) ); - sb.AppendFormat( "> **`Mods with Meta Manipulations:`** {0}, Total {1}\n", ModManager.Count( m => m.TotalManipulations > 0 ), - ModManager.Sum( m => m.TotalManipulations ) ); - sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions ); + sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\n" ); + sb.Append( $"> **`Mods with Config: `** {ModManager.Count( m => m.HasOptions )}\n" ); + sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); + sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); + sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); + sb.Append( $"> **`IMC Exceptions Thrown: `** {ImcExceptions.Count}\n" ); + sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" ); string CharacterName( ActorIdentifier id, string name ) { if( id.Type is IdentifierType.Player or IdentifierType.Owned ) { - return string.Join( " ", name.Split( ' ', 3 ).Select( n => $"{n[ 0 ]}." ) ) + ':'; + var parts = name.Split( ' ', 3 ); + return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[2] ) ); } return name + ':'; } void PrintCollection( ModCollection c ) - => sb.AppendFormat( "**Collection {0}**\n" - + "> **`Inheritances: `** {1}\n" - + "> **`Enabled Mods: `** {2}\n" - + "> **`Total Conflicts: `** {3}\n" - + "> **`Solved Conflicts: `** {4}\n", - c.AnonymizedName, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), - c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count ), - c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); + => sb.Append( $"**Collection {c.AnonymizedName}**\n" + + $"> **`Inheritances: `** {c.Inheritance.Count}\n" + + $"> **`Enabled Mods: `** {c.ActualSettings.Count( s => s is { Enabled: true } )}\n" + + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count )}/{c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count )}\n" ); sb.AppendLine( "**Collections**" ); - sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); - sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); - sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName ); - sb.AppendFormat( "> **`Interface Collection: `** {0}\n", CollectionManager.Interface.AnonymizedName ); - sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName ); + sb.Append( $"> **`#Collections: `** {CollectionManager.Count - 1}\n" ); + sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" ); + sb.Append( $"> **`Active Collections: `** {CollectionManager.Count( c => c.HasCache )}\n" ); + sb.Append( $"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n" ); + sb.Append( $"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n" ); + sb.Append( $"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n" ); foreach( var (type, name, _) in CollectionTypeExtensions.Special ) { var collection = CollectionManager.ByType( type ); if( collection != null ) { - sb.AppendFormat( "> **`{0,-29}`** {1}\n", name, collection.AnonymizedName ); + sb.Append( $"> **`{name,-30}`** {collection.AnonymizedName}\n" ); } } foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments ) { - sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( id[ 0 ], name ) ); + sb.Append( $"> **`{CharacterName( id[ 0 ], name ),-30}`** {collection.AnonymizedName}\n" ); } foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index ea677f6c..a5f6204a 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -24,10 +24,38 @@ public partial class ConfigWindow Add5_10_0( ret ); Add5_11_0( ret ); Add5_11_1( ret ); + Add6_0_0( ret ); return ret; } + private static void Add6_0_0( Changelog log ) + => log.NextVersion( "Version 0.6.0.0" ) + .RegisterEntry( "Revamped Individual Collections:" ) + .RegisterEntry( "You can now specify individual collections for players (by name) of specific worlds or any world.", 1 ) + .RegisterEntry( "You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", 1 ) + .RegisterHighlight( + "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", + 1 ) + .RegisterHighlight( + "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", + 1 ) + .RegisterEntry( "You can also manually sort your Individual Collections by drag and drop now.", 1 ) + .RegisterEntry( "This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1 ) + .RegisterEntry( "These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", 1 ) + .RegisterEntry( "General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", 1 ) + .RegisterEntry( "Added Dye Previews for in-game dyes and dyeing templates in Material Editing." ) + .RegisterEntry( "Added Export buttons to .mdl and .mtrl previews in Advanced Editing." ) + .RegisterEntry( "Collection selectors can now be filtered by name." ) + .RegisterEntry( "Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths." ) + .RegisterEntry( "Improved interface for group settings (minimally)." ) + .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) + .RegisterEntry( "Improved Support Info somewhat." ) + .RegisterEntry( "Fixed a bug when dragging options during mod edit." ) + .RegisterEntry( "Fixed a bug where sometimes the valid folder check caused issues." ) + .RegisterEntry( "Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged)." ) + .RegisterEntry( "A lot of big backend changes." ); + private static void Add5_11_1( Changelog log ) => log.NextVersion( "Version 0.5.11.1" ) .RegisterEntry( diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index cc46d52a..a1aee0e4 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -63,28 +63,20 @@ public partial class ConfigWindow Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); ImGui.Dummy( _window._defaultSpace ); Checkbox( $"Use {AssignedCollections} in Character Window", - "Use the character collection for your characters name or the Your Character collection in your main character window, if it is set.", + "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); Checkbox( $"Use {AssignedCollections} in Adventurer Cards", - "Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.", + "Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.", Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); Checkbox( $"Use {AssignedCollections} in Try-On Window", - "Use the character collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", + "Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); Checkbox( $"Use {AssignedCollections} in Inspect Windows", - "Use the appropriate character collection for the character you are currently inspecting, based on their name.", + "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); Checkbox( $"Use {AssignedCollections} based on Ownership", - "Use the owner's name to determine the appropriate character collection for mounts, companions and combat pets.", + "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); - Checkbox( "Prefer Named Collections over Ownership", - "If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owners collection.\n" - + "That is, if you have a 'Topaz Carbuncle' collection, it will use this one instead of the one for its owner.", - Penumbra.Config.PreferNamedCollectionsOverOwners, v => Penumbra.Config.PreferNamedCollectionsOverOwners = v ); - Checkbox( $"Use {DefaultCollection} for Housing Retainers", - $"Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the {DefaultCollection}.\n" - + "It is not possible to make them have their own collection, since they have no connection to their actual name.", - Penumbra.Config.UseDefaultCollectionForRetainers, v => Penumbra.Config.UseDefaultCollectionForRetainers = v ); ImGui.Dummy( _window._defaultSpace ); DrawFolderSortType(); DrawAbsoluteSizeSelector(); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index ab14c749..3a251e23 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -85,6 +85,11 @@ public partial class ConfigWindow { static bool IsSubPathOf( string basePath, string subPath ) { + if( basePath.Length == 0 ) + { + return false; + } + var rel = Path.GetRelativePath( basePath, subPath ); return rel == "." || !rel.StartsWith( '.' ) && !Path.IsPathRooted( rel ); } From 41ed873eaf3314d261d36f20270a844f6448f33f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Nov 2022 21:28:27 +0100 Subject: [PATCH 0586/2451] Formatting... --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index eb04510a..94aa3368 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -521,7 +521,7 @@ public class Penumbra : IDalamudPlugin if( id.Type is IdentifierType.Player or IdentifierType.Owned ) { var parts = name.Split( ' ', 3 ); - return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[2] ) ); + return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); } return name + ':'; From e47ca842b2a765b73eafbb08eb683195fbc1e71a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 20 Nov 2022 15:48:52 +0100 Subject: [PATCH 0587/2451] Fix bug in temporary collection names and extend IPC Tester for temp mods. --- Penumbra/Api/IpcTester.cs | 35 ++++++++++++++++++--------- Penumbra/Api/TempModManager.cs | 16 ++++++------ Penumbra/UI/ConfigWindow.Changelog.cs | 1 + Penumbra/UI/ConfigWindow.Tutorial.cs | 4 +-- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index d6b0b51c..6c48fd2a 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -13,6 +13,8 @@ using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.String; using Penumbra.String.Classes; +using Swan; +using Penumbra.Meta.Manipulations; namespace Penumbra.Api; @@ -1132,6 +1134,17 @@ public class IpcTester : IDisposable _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); } + DrawIntro( Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection" ); + if( ImGuiUtil.DrawDisabledButton( "Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...", + !Penumbra.CollectionManager.ByName( _tempModName, out var copyCollection ) ) + && copyCollection is { HasCache: true } ) + { + var files = copyCollection.ResolvedFiles.ToDictionary( kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString() ); + var manips = Functions.ToCompressedBase64( copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(), + MetaManipulation.CurrentVersion ); + _lastTempError = Ipc.AddTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, files, manips, 999 ); + } + DrawIntro( Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections" ); if( ImGui.Button( "Add##All" ) ) { @@ -1154,7 +1167,7 @@ public class IpcTester : IDisposable public void DrawCollections() { - using var collTree = ImRaii.TreeNode( "Collections" ); + using var collTree = ImRaii.TreeNode( "Collections##TempCollections" ); if( !collTree ) { return; @@ -1166,27 +1179,25 @@ public class IpcTester : IDisposable return; } - foreach( var (character, collection) in Penumbra.TempMods.Collections ) + foreach( var collection in Penumbra.TempMods.CustomCollections.Values ) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( character ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); - ImGui.TableNextColumn(); - if( ImGui.Button( $"Save##{character}" ) ) + var character = Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; + if( ImGui.Button( $"Save##{collection.Name}" ) ) { Mod.TemporaryMod.SaveTempCollection( collection, character ); } + + ImGuiUtil.DrawTableColumn( collection.Name ); + ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() ); + ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" ); + ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); } } public void DrawMods() { - using var modTree = ImRaii.TreeNode( "Mods" ); + using var modTree = ImRaii.TreeNode( "Mods##TempMods" ); if( !modTree ) { return; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index db087f9d..650b476e 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -37,7 +37,7 @@ public class TempModManager => _customCollections; public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _customCollections.TryGetValue( name, out collection ); + => _customCollections.TryGetValue( name.ToLowerInvariant(), out collection ); // These functions to check specific redirections or meta manipulations for existence are currently unused. //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) @@ -154,18 +154,18 @@ public class TempModManager public string CreateTemporaryCollection( string tag, string customName ) { var collection = ModCollection.CreateNewTemporary( tag, customName ); - if( _customCollections.ContainsKey( collection.Name ) ) + if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) ) { - collection.ClearCache(); - return string.Empty; + return collection.Name; } - _customCollections.Add( collection.Name, collection ); - return collection.Name; + + collection.ClearCache(); + return string.Empty; } public bool RemoveTemporaryCollection( string collectionName ) { - if( !_customCollections.Remove( collectionName, out var collection ) ) + if( !_customCollections.Remove( collectionName.ToLowerInvariant(), out var collection ) ) { return false; } @@ -197,7 +197,7 @@ public class TempModManager public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers ) { - if( !_customCollections.TryGetValue( collectionName, out var collection ) ) + if( !_customCollections.TryGetValue( collectionName.ToLowerInvariant(), out var collection ) ) { return false; } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index a5f6204a..4a6ab8b1 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -50,6 +50,7 @@ public partial class ConfigWindow .RegisterEntry( "Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths." ) .RegisterEntry( "Improved interface for group settings (minimally)." ) .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) + .RegisterEntry( "New Special or Individual Assignments now default to your current Base assignment instead of None." ) .RegisterEntry( "Improved Support Info somewhat." ) .RegisterEntry( "Fixed a bug when dragging options during mod edit." ) .RegisterEntry( "Fixed a bug where sometimes the valid folder check caused issues." ) diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 5826d3f1..6c10f740 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -128,8 +128,8 @@ public partial class ConfigWindow + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" + $"{IndividualAssignments} always take precedence before groups." ) .Register( IndividualAssignments, - "Collections assigned here are used only for individual characters or NPCs that have the specified name.\n\n" - + "They may also apply to objects 'owned' by those characters, e.g. minions or mounts - see the general settings for options on this.\n\n" ) + "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" + + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n" ) .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking." ) .Register( "Initial Setup, Step 9: Mod Import", From 304b75e7d26790dcd6bde164ceb11746a814147b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Nov 2022 15:33:33 +0100 Subject: [PATCH 0588/2451] Add support for retainer collections, fix deleted assignments not updating identifiers. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 49 +++-- .../Actors/ActorManager.Identifiers.cs | 167 +++++++++++------- Penumbra.GameData/Actors/IdentifierType.cs | 4 +- .../IndividualCollections.Access.cs | 14 ++ Penumbra/Collections/IndividualCollections.cs | 25 ++- .../ConfigWindow.CollectionsTab.Individual.cs | 68 +++++-- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 12 +- Penumbra/UI/ConfigWindow.cs | 1 + 8 files changed, 233 insertions(+), 107 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 74e6d5f1..c45821e9 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -25,7 +25,7 @@ public readonly struct ActorIdentifier : IEquatable // @formatter:on public ActorIdentifier CreatePermanent() - => new(Type, Kind, Index, DataId, PlayerName.Clone()); + => new(Type, Kind, Index, DataId, PlayerName.IsEmpty ? PlayerName : PlayerName.Clone()); public bool Equals(ActorIdentifier other) { @@ -35,11 +35,13 @@ public readonly struct ActorIdentifier : IEquatable return Type switch { IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), + IdentifierType.Retainer => PlayerName.EqualsCi(other.PlayerName), IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, IdentifierType.Npc => Manager.DataIdEquals(this, other) && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), - _ => false, + IdentifierType.UnkObject => PlayerName.EqualsCi(other.PlayerName) && Index == other.Index, + _ => false, }; } @@ -53,30 +55,36 @@ public readonly struct ActorIdentifier : IEquatable => !lhs.Equals(rhs); public bool IsValid - => Type != IdentifierType.Invalid; + => Type is not (IdentifierType.UnkObject or IdentifierType.Invalid); public override string ToString() => Manager?.ToString(this) ?? Type switch { - IdentifierType.Player => $"{PlayerName} ({HomeWorld})", - IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", - IdentifierType.Special => Special.ToName(), + IdentifierType.Player => $"{PlayerName} ({HomeWorld})", + IdentifierType.Retainer => $"{PlayerName} (Retainer)", + IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", + IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind.ToName()} #{DataId}" : $"{Kind.ToName()} #{DataId} at {Index}", + IdentifierType.UnkObject => PlayerName.IsEmpty + ? $"Unknown Object at {Index}" + : $"{PlayerName} at {Index}", _ => "Invalid", }; public override int GetHashCode() => Type switch { - IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), - IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), - IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), - IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), - _ => 0, + IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), + IdentifierType.Retainer => HashCode.Combine(IdentifierType.Player, PlayerName), + IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), + IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), + IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), + IdentifierType.UnkObject => HashCode.Combine(IdentifierType.UnkObject, PlayerName, Index), + _ => 0, }; internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName) @@ -98,6 +106,9 @@ public readonly struct ActorIdentifier : IEquatable ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(HomeWorld), HomeWorld); return ret; + case IdentifierType.Retainer: + ret.Add(nameof(PlayerName), PlayerName.ToString()); + return ret; case IdentifierType.Owned: ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(HomeWorld), HomeWorld); @@ -113,6 +124,10 @@ public readonly struct ActorIdentifier : IEquatable ret.Add(nameof(Index), Index); ret.Add(nameof(DataId), DataId); return ret; + case IdentifierType.UnkObject: + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(Index), Index); + return ret; } return ret; @@ -162,11 +177,13 @@ public static class ActorManagerExtensions public static string ToName(this IdentifierType type) => type switch { - IdentifierType.Player => "Player", - IdentifierType.Owned => "Owned NPC", - IdentifierType.Special => "Special Actor", - IdentifierType.Npc => "NPC", - _ => "Invalid", + IdentifierType.Player => "Player", + IdentifierType.Retainer => "Retainer (Bell)", + IdentifierType.Owned => "Owned NPC", + IdentifierType.Special => "Special Actor", + IdentifierType.Npc => "NPC", + IdentifierType.UnkObject => "Unknown Object", + _ => "Invalid", }; /// diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 48c8a2e2..eb21861b 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -28,6 +28,11 @@ public partial class ActorManager var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; return CreatePlayer(name, homeWorld); } + case IdentifierType.Retainer: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + return CreateRetainer(name); + } case IdentifierType.Owned: { var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); @@ -48,6 +53,12 @@ public partial class ActorManager var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; return CreateNpc(kind, dataId, index); } + case IdentifierType.UnkObject: + { + var index = data[nameof(ActorIdentifier.Index)]?.ToObject() ?? ushort.MaxValue; + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); + } default: return ActorIdentifier.Invalid; } } @@ -68,6 +79,7 @@ public partial class ActorManager IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" : id.PlayerName.ToString(), + IdentifierType.Retainer => id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", @@ -76,6 +88,9 @@ public partial class ActorManager id.Index == ushort.MaxValue ? ToName(id.Kind, id.DataId) : $"{ToName(id.Kind, id.DataId)} at {id.Index}", + IdentifierType.UnkObject => id.PlayerName.IsEmpty + ? $"Unknown Object at {id.Index}" + : $"{id.PlayerName} at {id.Index}", _ => "Invalid", }; } @@ -188,7 +203,19 @@ public partial class ActorManager : CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId); } - default: return ActorIdentifier.Invalid; + case ObjectKind.Retainer: + { + var name = new ByteString(actor->Name); + return check + ? CreateRetainer(name) + : CreateIndividualUnchecked(IdentifierType.Retainer, name, 0, ObjectKind.None, uint.MaxValue); + } + default: + { + var name = new ByteString(actor->Name); + var index = actor->ObjectIndex; + return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); + } } } @@ -213,11 +240,13 @@ public partial class ActorManager public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch { - IdentifierType.Player => CreatePlayer(name, homeWorld), - IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), - IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), - IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), - _ => ActorIdentifier.Invalid, + IdentifierType.Player => CreatePlayer(name, homeWorld), + IdentifierType.Retainer => CreateRetainer(name), + IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), + IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), + IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), + IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0), + _ => ActorIdentifier.Invalid, }; /// @@ -234,6 +263,14 @@ public partial class ActorManager return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); } + public ActorIdentifier CreateRetainer(ByteString name) + { + if (!VerifyRetainerName(name.Span)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, 0, 0, name); + } + public ActorIdentifier CreateSpecial(SpecialActor actor) { if (!VerifySpecial(actor)) @@ -270,37 +307,7 @@ public partial class ActorManager if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0) return false; - static bool CheckNamePart(ReadOnlySpan part) - { - // Each name part at least 2 and at most 15 characters. - if (part.Length is < 2 or > 15) - return false; - - // Each part starting with capitalized letter. - if (part[0] is < (byte)'A' or > (byte)'Z') - return false; - - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = (byte)'\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is (byte)'\'' && current is (byte)'-') - return false; - if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') - return false; - - last = current; - } - - return part[^1] != (byte)'-'; - } - - return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); + return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); } /// Checks SE naming rules. @@ -315,37 +322,75 @@ public partial class ActorManager if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0) return false; - static bool CheckNamePart(ReadOnlySpan part) + return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); + } + + /// Checks SE naming rules. + public static bool VerifyRetainerName(ReadOnlySpan name) + => CheckNamePart(name, 3, 20); + + /// Checks SE naming rules. + public static bool VerifyRetainerName(ReadOnlySpan name) + => CheckNamePart(name, 3, 20); + + private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) + { + // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. + if (part.Length < minLength || part.Length > maxLength) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < 'A' or > 'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + var last = '\0'; + for (var i = 1; i < part.Length; ++i) { - // Each name part at least 2 and at most 15 characters. - if (part.Length is < 2 or > 15) + var current = part[i]; + if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) return false; - // Each part starting with capitalized letter. - if (part[0] is < 'A' or > 'Z') + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is '\'' && current is '-') + return false; + if (last is '-' && current is '-' or '\'') return false; - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = '\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is '\'' && current is '-') - return false; - if (last is '-' && current is '-' or '\'') - return false; - - last = current; - } - - return part[^1] != '-'; + last = current; } - return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); + return part[^1] != '-'; + } + + private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) + { + // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. + if (part.Length < minLength || part.Length > maxLength) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < (byte)'A' or > (byte)'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + var last = (byte)'\0'; + for (var i = 1; i < part.Length; ++i) + { + var current = part[i]; + if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) + return false; + + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is (byte)'\'' && current is (byte)'-') + return false; + if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') + return false; + + last = current; + } + + return part[^1] != (byte)'-'; } /// Checks if the world is a valid public world or ushort.MaxValue (any world). diff --git a/Penumbra.GameData/Actors/IdentifierType.cs b/Penumbra.GameData/Actors/IdentifierType.cs index a582aa14..8fe1ee4f 100644 --- a/Penumbra.GameData/Actors/IdentifierType.cs +++ b/Penumbra.GameData/Actors/IdentifierType.cs @@ -6,5 +6,7 @@ public enum IdentifierType : byte Player, Owned, Special, - Npc, + Npc, + Retainer, + UnkObject, }; \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index f09dadb0..01858010 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -28,6 +28,20 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ switch( identifier.Type ) { case IdentifierType.Player: return CheckWorlds( identifier, out collection ); + case IdentifierType.Retainer: + { + if( _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + if( Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); + } + + break; + } case IdentifierType.Owned: { if( CheckWorlds( identifier, out collection! ) ) diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index a8239eb5..57f7f824 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -62,8 +62,15 @@ public sealed partial class IndividualCollections return AddResult.Invalid; } - var identifier = _actorManager.CreatePlayer( playerName, homeWorld ); - identifiers = new[] { identifier }; + identifiers = new[] { _actorManager.CreatePlayer( playerName, homeWorld ) }; + break; + case IdentifierType.Retainer: + if( !ByteString.FromString( name, out var retainerName ) ) + { + return AddResult.Invalid; + } + + identifiers = new[] { _actorManager.CreateRetainer( retainerName ) }; break; case IdentifierType.Owned: if( !ByteString.FromString( name, out var ownerName ) ) @@ -108,11 +115,12 @@ public sealed partial class IndividualCollections return identifier.Type switch { - IdentifierType.Player => new[] { identifier.CreatePermanent() }, - IdentifierType.Special => new[] { identifier }, - IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), - IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), - _ => Array.Empty< ActorIdentifier >(), + IdentifierType.Player => new[] { identifier.CreatePermanent() }, + IdentifierType.Special => new[] { identifier }, + IdentifierType.Retainer => new[] { identifier.CreatePermanent() }, + IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), + IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), + _ => Array.Empty< ActorIdentifier >(), }; } @@ -197,7 +205,8 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}", IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 9737969d..5e8c4967 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -75,17 +75,21 @@ public partial class ConfigWindow private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); - private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; - private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; - private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; - private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; - private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newPlayerTooltip = NewPlayerTooltipEmpty; - private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newNpcTooltip = NewNpcTooltipEmpty; - private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newOwnedTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newPlayerTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newRetainerTooltip = NewRetainerTooltipEmpty; + private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newNpcTooltip = NewNpcTooltipEmpty; + private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newOwnedTooltip = NewPlayerTooltipEmpty; private bool DrawNewObjectKindOptions( float width ) { @@ -201,16 +205,22 @@ public partial class ConfigWindow private bool DrawNewOwnedCollection( Vector2 buttonWidth ) { - var oldPos = ImGui.GetCursorPos(); - ImGui.SameLine(); - ImGui.SetCursorPos( ImGui.GetCursorPos() + new Vector2( -ImGui.GetStyle().ItemSpacing.X / 2, ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) { Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default ); return true; } - ImGui.SetCursorPos( oldPos ); + return false; + } + + private bool DrawNewRetainerCollection( Vector2 buttonWidth ) + { + if( ImGuiUtil.DrawDisabledButton( "Assign Bell Retainer", buttonWidth, _newRetainerTooltip, _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newRetainerIdentifiers, Penumbra.CollectionManager.Default ); + return true; + } return false; } @@ -228,13 +238,19 @@ public partial class ConfigWindow private void DrawNewIndividualCollection() { - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - var buttonWidth = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; + + var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); var combo = GetNpcCombo( _newKind ); - var change = DrawNewPlayerCollection( buttonWidth, width ); - change |= DrawNewOwnedCollection( Vector2.Zero ); - change |= DrawNewNpcCollection( combo, buttonWidth, width ); + var change = DrawNewPlayerCollection( buttonWidth1, width ); + ImGui.SameLine(); + change |= DrawNewRetainerCollection( buttonWidth2 ); + + change |= DrawNewNpcCollection( combo, buttonWidth1, width ); + ImGui.SameLine(); + change |= DrawNewOwnedCollection( buttonWidth2 ); if( change ) { @@ -253,6 +269,14 @@ public partial class ConfigWindow IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; + _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty< uint >(), out _newRetainerIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; if( combo.CurrentSelection.Ids != null ) { _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, @@ -278,5 +302,11 @@ public partial class ConfigWindow _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); } } + + private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + { + if( type == CollectionType.Individual ) + UpdateIdentifiers(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index aa6b6a4e..d1769cd3 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; @@ -13,12 +14,19 @@ namespace Penumbra.UI; public partial class ConfigWindow { // Encapsulate for less pollution. - private partial class CollectionsTab + private partial class CollectionsTab : IDisposable { private readonly ConfigWindow _window; public CollectionsTab( ConfigWindow window ) - => _window = window; + { + _window = window; + + Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; + } + + public void Dispose() + => Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; public void Draw() { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index afe8c9be..f367a92b 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -137,6 +137,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _selector.Dispose(); _modPanel.Dispose(); + _collectionsTab.Dispose(); ModEditPopup.Dispose(); } From 16a56eb5d0b4bc5f6388e1a4cce8130fcd2e26cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Nov 2022 15:33:51 +0100 Subject: [PATCH 0589/2451] Turn mods without names to warnings. --- Penumbra/Mods/Mod.BasePath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 42ebb23e..9ce8d478 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -40,7 +40,7 @@ public partial class Mod if( !mod.Reload( incorporateMetaChanges, out _ ) ) { // Can not be base path not existing because that is checked before. - Penumbra.Log.Error( $"Mod at {modPath} without name is not supported." ); + Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." ); return null; } From 74ed6edd6fae4abb3027bf8fba553f4945d77474 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Nov 2022 16:57:17 +0100 Subject: [PATCH 0590/2451] Update IPC to use better mechanisms for temporary collections without breaking backwards compatibility. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 21 +++++++++ Penumbra/Api/PenumbraApi.cs | 68 +++++++++++++++++++++++---- Penumbra/Api/PenumbraIpcProviders.cs | 21 ++++++--- Penumbra/Api/TempModManager.cs | 9 +++- Penumbra/Collections/ModCollection.cs | 4 +- 6 files changed, 106 insertions(+), 19 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 27e8873e..15f782dd 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 27e8873e9f4633421e736757574b502a8d65e79d +Subproject commit 15f782dd7d3b823db5203769578b9edd7b92e309 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 6c48fd2a..ac532c1b 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1087,6 +1087,7 @@ public class IpcTester : IDisposable private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; private PenumbraApiEc _lastTempError; + private int _tempActorIndex = 0; private bool _forceOverwrite; public void Draw() @@ -1099,6 +1100,7 @@ public class IpcTester : IDisposable ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 ); ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 ); + ImGui.InputInt( "##tempActorIndex", ref _tempActorIndex, 0, 0 ); ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 ); ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 ); ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 ); @@ -1115,16 +1117,35 @@ public class IpcTester : IDisposable DrawIntro( "Last Error", _lastTempError.ToString() ); DrawIntro( "Last Created Collection", LastCreatedCollectionName ); DrawIntro( Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection" ); +#pragma warning disable 0612 if( ImGui.Button( "Create##Collection" ) ) { ( _lastTempError, LastCreatedCollectionName ) = Ipc.CreateTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempCharacterName, _forceOverwrite ); } + DrawIntro( Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection" ); + if( ImGui.Button( "Create##NamedCollection" ) ) + { + _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName ); + } + DrawIntro( Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character" ); if( ImGui.Button( "Delete##Collection" ) ) { _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber( _pi ).Invoke( _tempCharacterName ); } +#pragma warning restore 0612 + DrawIntro( Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection" ); + if( ImGui.Button( "Delete##NamedCollection" ) ) + { + _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber( _pi ).Invoke( _tempCollectionName ); + } + + DrawIntro( Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection" ); + if( ImGui.Button( "Assign##NamedCollection" ) ) + { + _lastTempError = Ipc.AssignTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempActorIndex, _forceOverwrite ); + } DrawIntro( Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection" ); if( ImGui.Button( "Add##Mod" ) ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 94e921de..7266c884 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,7 +23,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 15 ); + => ( 4, 16 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -575,10 +575,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) - => CreateTemporaryCollection( tag, character, forceOverwriteCharacter, ushort.MaxValue ); - public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter, ushort worldId ) + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) { CheckInitialized(); @@ -587,7 +585,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.InvalidArgument, string.Empty ); } - var identifier = NameToIdentifier( character, worldId ); + var identifier = NameToIdentifier( character, ushort.MaxValue ); if( !identifier.IsValid ) { return ( PenumbraApiEc.InvalidArgument, string.Empty ); @@ -599,10 +597,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); } - var name = Penumbra.TempMods.CreateTemporaryCollection( tag, character ); - if( name.Length == 0 ) + var name = $"{tag}_{character}"; + var ret = CreateNamedTemporaryCollection( name ); + if( ret != PenumbraApiEc.Success ) { - return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); + return ( ret, name ); } if( Penumbra.TempMods.AddIdentifier( name, identifier ) ) @@ -614,6 +613,51 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.UnknownError, string.Empty ); } + public PenumbraApiEc CreateNamedTemporaryCollection( string name ) + { + CheckInitialized(); + if( name.Length == 0 || Mod.ReplaceBadXivSymbols( name ) != name ) + { + return PenumbraApiEc.InvalidArgument; + } + + return Penumbra.TempMods.CreateTemporaryCollection( name ).Length > 0 + ? PenumbraApiEc.Success + : PenumbraApiEc.CollectionExists; + } + + public PenumbraApiEc AssignTemporaryCollection( string collectionName, int actorIndex, bool forceAssignment ) + { + CheckInitialized(); + + if( actorIndex < 0 || actorIndex >= Dalamud.Objects.Length ) + { + return PenumbraApiEc.InvalidArgument; + } + + var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ] ); + if( !identifier.IsValid ) + { + return PenumbraApiEc.InvalidArgument; + } + + if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !forceAssignment + && ( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) ) + { + return PenumbraApiEc.CharacterCollectionExists; + } + + var group = Penumbra.TempMods.Collections.GetGroup( identifier ); + return Penumbra.TempMods.AddIdentifier( collection, group ) + ? PenumbraApiEc.Success + : PenumbraApiEc.UnknownError; + } + public PenumbraApiEc RemoveTemporaryCollection( string character ) { CheckInitialized(); @@ -622,6 +666,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi : PenumbraApiEc.NothingChanged; } + public PenumbraApiEc RemoveTemporaryCollectionByName( string name ) + { + CheckInitialized(); + return Penumbra.TempMods.RemoveTemporaryCollection( name ) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + } + public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 0db02dbd..518d3638 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -89,6 +89,9 @@ public class PenumbraIpcProviders : IDisposable // Temporary internal readonly FuncProvider< string, string, bool, (PenumbraApiEc, string) > CreateTemporaryCollection; internal readonly FuncProvider< string, PenumbraApiEc > RemoveTemporaryCollection; + internal readonly FuncProvider< string, PenumbraApiEc > CreateNamedTemporaryCollection; + internal readonly FuncProvider< string, PenumbraApiEc > RemoveTemporaryCollectionByName; + internal readonly FuncProvider< string, int, bool, PenumbraApiEc > AssignTemporaryCollection; internal readonly FuncProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryModAll; internal readonly FuncProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryMod; internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; @@ -178,12 +181,15 @@ public class PenumbraIpcProviders : IDisposable () => Api.ModSettingChanged -= ModSettingChangedEvent ); // Temporary - CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider( pi, Api.CreateTemporaryCollection ); - RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider( pi, Api.RemoveTemporaryCollection ); - AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider( pi, Api.AddTemporaryModAll ); - AddTemporaryMod = Ipc.AddTemporaryMod.Provider( pi, Api.AddTemporaryMod ); - RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); - RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); + CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider( pi, Api.CreateTemporaryCollection ); + RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider( pi, Api.RemoveTemporaryCollection ); + CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider( pi, Api.CreateNamedTemporaryCollection ); + RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider( pi, Api.RemoveTemporaryCollectionByName ); + AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider( pi, Api.AssignTemporaryCollection ); + AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider( pi, Api.AddTemporaryModAll ); + AddTemporaryMod = Ipc.AddTemporaryMod.Provider( pi, Api.AddTemporaryMod ); + RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); + RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); Tester = new IpcTester( pi, this ); @@ -267,6 +273,9 @@ public class PenumbraIpcProviders : IDisposable // Temporary CreateTemporaryCollection.Dispose(); RemoveTemporaryCollection.Dispose(); + CreateNamedTemporaryCollection.Dispose(); + RemoveTemporaryCollectionByName.Dispose(); + AssignTemporaryCollection.Dispose(); AddTemporaryModAll.Dispose(); AddTemporaryMod.Dispose(); RemoveTemporaryModAll.Dispose(); diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 650b476e..511cb861 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -151,9 +151,14 @@ public class TempModManager return RedirectResult.Success; } - public string CreateTemporaryCollection( string tag, string customName ) + public string CreateTemporaryCollection( string name ) { - var collection = ModCollection.CreateNewTemporary( tag, customName ); + if( Penumbra.CollectionManager.ByName( name, out _ ) ) + { + return string.Empty; + } + + var collection = ModCollection.CreateNewTemporary( name ); if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) ) { return collection.Name; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index e6a71fdf..0b165b1f 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -84,9 +84,9 @@ public partial class ModCollection => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary( string tag, string characterName ) + public static ModCollection CreateNewTemporary( string name ) { - var collection = new ModCollection( $"{tag}_{characterName}", Empty ); + var collection = new ModCollection( name, Empty ); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; collection.Index = ~Penumbra.TempMods.Collections.Count; From 29af320092453f7b049546c6573bea8bbc9514c4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Nov 2022 17:10:14 +0100 Subject: [PATCH 0591/2451] API update. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 15f782dd..744698d1 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 15f782dd7d3b823db5203769578b9edd7b92e309 +Subproject commit 744698d1295f4e629ab41d77f8872b1ff98fe501 From 776d993589f122ef47b9e48f6b2b4d9447b3a49a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 21 Nov 2022 17:31:11 +0100 Subject: [PATCH 0592/2451] Trying to understand why test builds fail. --- .github/workflows/test_release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 435da017..10ff26d9 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -77,7 +77,10 @@ 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 branch -f test origin/master && git checkout test + 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 From a64273bd735c09ba39ac2dae5c897df271dae400 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Nov 2022 16:57:40 +0100 Subject: [PATCH 0593/2451] Fix chat command not working. --- Penumbra/Penumbra.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 94aa3368..354a026a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -333,6 +333,7 @@ public class Penumbra : IDalamudPlugin } string? characterName = null; + var identifier = ActorIdentifier.Invalid; if( type is CollectionType.Individual ) { var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); @@ -344,6 +345,13 @@ public class Penumbra : IDalamudPlugin collectionName = split[ 0 ]; characterName = split[ 1 ]; + + identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); + if( !identifier.IsValid ) + { + Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); + return false; + } } collectionName = collectionName.ToLowerInvariant(); @@ -356,13 +364,6 @@ public class Penumbra : IDalamudPlugin return false; } - var identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); - if( !identifier.IsValid ) - { - Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); - return false; - } - var oldCollection = CollectionManager.ByType( type, identifier ); if( collection == oldCollection ) { From eedd3e2dac5c79964332b51f7efe3c1b0e546d84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Nov 2022 18:25:51 +0100 Subject: [PATCH 0594/2451] Add Model Parsing and display them under Changed Items, also display variants there, and rework Data Sharing a bunch. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 10 +- Penumbra.GameData/Actors/ActorManager.Data.cs | 211 +- .../Actors/ActorManager.Identifiers.cs | 62 +- Penumbra.GameData/Data/BNpcNames.cs | 15814 ++++++++++++++++ Penumbra.GameData/Data/DataSharer.cs | 45 +- .../Data/EquipmentIdentificationList.cs | 60 + Penumbra.GameData/Data/GamePathParser.cs | 23 +- Penumbra.GameData/Data/KeyList.cs | 101 + .../Data/ModelIdentificationList.cs | 52 + .../Data/ObjectIdentification.cs | 288 +- Penumbra.GameData/Data/RestrictedGear.cs | 464 + .../Data/WeaponIdentificationList.cs | 74 + Penumbra.GameData/Enums/EquipSlot.cs | 52 +- .../Enums/ModelTypeExtensions.cs | 26 + Penumbra.GameData/GameData.cs | 4 +- .../IndividualCollections.Files.cs | 12 +- Penumbra/Collections/IndividualCollections.cs | 17 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 10 +- .../ConfigWindow.CollectionsTab.Individual.cs | 12 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 4 +- Penumbra/UI/ConfigWindow.Misc.cs | 23 +- 21 files changed, 17032 insertions(+), 332 deletions(-) create mode 100644 Penumbra.GameData/Data/BNpcNames.cs create mode 100644 Penumbra.GameData/Data/EquipmentIdentificationList.cs create mode 100644 Penumbra.GameData/Data/KeyList.cs create mode 100644 Penumbra.GameData/Data/ModelIdentificationList.cs create mode 100644 Penumbra.GameData/Data/RestrictedGear.cs create mode 100644 Penumbra.GameData/Data/WeaponIdentificationList.cs create mode 100644 Penumbra.GameData/Enums/ModelTypeExtensions.cs diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index c45821e9..376a5314 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -149,11 +149,11 @@ public static class ActorManagerExtensions var dict = lhs.Kind switch { - ObjectKind.MountType => manager.Mounts, - ObjectKind.Companion => manager.Companions, - (ObjectKind)15 => manager.Ornaments, // TODO: CS Update - ObjectKind.BattleNpc => manager.BNpcs, - ObjectKind.EventNpc => manager.ENpcs, + ObjectKind.MountType => manager.Data.Mounts, + ObjectKind.Companion => manager.Data.Companions, + (ObjectKind)15 => manager.Data.Ornaments, // TODO: CS Update + ObjectKind.BattleNpc => manager.Data.BNpcs, + ObjectKind.EventNpc => manager.Data.ENpcs, _ => new Dictionary(), }; diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 0907d2ff..36097a37 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Dalamud; @@ -20,25 +21,135 @@ using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.GameData.Actors; -public sealed partial class ActorManager : DataSharer +public sealed partial class ActorManager : IDisposable { - /// Worlds available for players. - public IReadOnlyDictionary Worlds { get; } + public sealed class ActorManagerData : DataSharer + { + /// Worlds available for players. + public IReadOnlyDictionary Worlds { get; } - /// Valid Mount names in title case by mount id. - public IReadOnlyDictionary Mounts { get; } + /// Valid Mount names in title case by mount id. + public IReadOnlyDictionary Mounts { get; } - /// Valid Companion names in title case by companion id. - public IReadOnlyDictionary Companions { get; } + /// Valid Companion names in title case by companion id. + public IReadOnlyDictionary Companions { get; } - /// Valid ornament names by id. - public IReadOnlyDictionary Ornaments { get; } + /// Valid ornament names by id. + public IReadOnlyDictionary Ornaments { get; } - /// Valid BNPC names in title case by BNPC Name id. - public IReadOnlyDictionary BNpcs { get; } + /// Valid BNPC names in title case by BNPC Name id. + public IReadOnlyDictionary BNpcs { get; } - /// Valid ENPC names in title case by ENPC id. - public IReadOnlyDictionary ENpcs { get; } + /// Valid ENPC names in title case by ENPC id. + public IReadOnlyDictionary ENpcs { get; } + + public ActorManagerData(DalamudPluginInterface pluginInterface, DataManager gameData, ClientLanguage language) + : base(pluginInterface, language, 1) + { + Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); + Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); + Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); + Ornaments = TryCatchData("Ornaments", () => CreateOrnamentData(gameData)); + BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); + ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); + } + + /// + /// Return the world name including the Any World option. + /// + public string ToWorldName(ushort worldId) + => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; + + /// + /// Convert a given ID for a certain ObjectKind to a name. + /// + /// Invalid or a valid name. + public string ToName(ObjectKind kind, uint dataId) + => TryGetName(kind, dataId, out var ret) ? ret : "Invalid"; + + + /// + /// Convert a given ID for a certain ObjectKind to a name. + /// + public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name) + { + name = null; + return kind switch + { + ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), + ObjectKind.Companion => Companions.TryGetValue(dataId, out name), + (ObjectKind)15 => Ornaments.TryGetValue(dataId, out name), // TODO: CS Update + ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), + ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), + _ => false, + }; + } + + protected override void DisposeInternal() + { + DisposeTag("Worlds"); + DisposeTag("Mounts"); + DisposeTag("Companions"); + DisposeTag("Ornaments"); + DisposeTag("BNpcs"); + DisposeTag("ENpcs"); + } + + private IReadOnlyDictionary CreateWorldData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) + .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + + private IReadOnlyDictionary CreateMountData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) + .ToDictionary(m => m.RowId, m => ToTitleCaseExtended(m.Singular, m.Article)); + + private IReadOnlyDictionary CreateCompanionData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) + .ToDictionary(c => c.RowId, c => ToTitleCaseExtended(c.Singular, c.Article)); + + private IReadOnlyDictionary CreateOrnamentData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(o => o.Singular.RawData.Length > 0) + .ToDictionary(o => o.RowId, o => ToTitleCaseExtended(o.Singular, o.Article)); + + private IReadOnlyDictionary CreateBNpcData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(n => n.Singular.RawData.Length > 0) + .ToDictionary(n => n.RowId, n => ToTitleCaseExtended(n.Singular, n.Article)); + + private IReadOnlyDictionary CreateENpcData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(e => e.Singular.RawData.Length > 0) + .ToDictionary(e => e.RowId, e => ToTitleCaseExtended(e.Singular, e.Article)); + + private static string ToTitleCaseExtended(SeString s, sbyte article) + { + if (article == 1) + return s.ToDalamudString().ToString(); + + var sb = new StringBuilder(s.ToDalamudString().ToString()); + var lastSpace = true; + for (var i = 0; i < sb.Length; ++i) + { + if (sb[i] == ' ') + { + lastSpace = true; + } + else if (lastSpace) + { + lastSpace = false; + sb[i] = char.ToUpperInvariant(sb[i]); + } + } + + return sb.ToString(); + } + } + + public readonly ActorManagerData Data; public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, Func toParentIdx) @@ -47,19 +158,12 @@ public sealed partial class ActorManager : DataSharer public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, ClientLanguage language, Func toParentIdx) - : base(pluginInterface, language, 1) { _objects = objects; _gameGui = gameGui; _clientState = state; _toParentIdx = toParentIdx; - - Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); - Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); - Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); - Ornaments = TryCatchData("Ornaments", () => CreateOrnamentData(gameData)); - BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); - ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); + Data = new ActorManagerData(pluginInterface, gameData, language); ActorIdentifier.Manager = this; @@ -100,14 +204,11 @@ public sealed partial class ActorManager : DataSharer return addon == IntPtr.Zero ? ActorIdentifier.Invalid : GetCurrentPlayer(); } - protected override void DisposeInternal() + public void Dispose() { - DisposeTag("Worlds"); - DisposeTag("Mounts"); - DisposeTag("Companions"); - DisposeTag("Ornaments"); - DisposeTag("BNpcs"); - DisposeTag("ENpcs"); + Data.Dispose(); + if (ActorIdentifier.Manager == this) + ActorIdentifier.Manager = null; } ~ActorManager() @@ -119,60 +220,6 @@ public sealed partial class ActorManager : DataSharer private readonly Func _toParentIdx; - private IReadOnlyDictionary CreateWorldData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) - .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); - - private IReadOnlyDictionary CreateMountData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) - .ToDictionary(m => m.RowId, m => ToTitleCaseExtended(m.Singular, m.Article)); - - private IReadOnlyDictionary CreateCompanionData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) - .ToDictionary(c => c.RowId, c => ToTitleCaseExtended(c.Singular, c.Article)); - - private IReadOnlyDictionary CreateOrnamentData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(o => o.Singular.RawData.Length > 0) - .ToDictionary(o => o.RowId, o => ToTitleCaseExtended(o.Singular, o.Article)); - - private IReadOnlyDictionary CreateBNpcData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(n => n.Singular.RawData.Length > 0) - .ToDictionary(n => n.RowId, n => ToTitleCaseExtended(n.Singular, n.Article)); - - private IReadOnlyDictionary CreateENpcData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(e => e.Singular.RawData.Length > 0) - .ToDictionary(e => e.RowId, e => ToTitleCaseExtended(e.Singular, e.Article)); - - private static string ToTitleCaseExtended(SeString s, sbyte article) - { - if (article == 1) - return s.ToDalamudString().ToString(); - - var sb = new StringBuilder(s.ToDalamudString().ToString()); - var lastSpace = true; - for (var i = 0; i < sb.Length; ++i) - { - if (sb[i] == ' ') - { - lastSpace = true; - } - else if (lastSpace) - { - lastSpace = false; - sb[i] = char.ToUpperInvariant(sb[i]); - } - } - - return sb.ToString(); - } - - [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] private static unsafe ushort* _inspectTitleId = null!; diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index eb21861b..8b7ea937 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -63,12 +63,6 @@ public partial class ActorManager } } - /// - /// Return the world name including the Any World option. - /// - public string ToWorldName(ushort worldId) - => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; - /// /// Use stored data to convert an ActorIdentifier to a string. /// @@ -77,17 +71,17 @@ public partial class ActorManager return id.Type switch { IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" + ? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})" : id.PlayerName.ToString(), IdentifierType.Retainer => id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" - : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", + ? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})'s {Data.ToName(id.Kind, id.DataId)}" + : $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", IdentifierType.Special => id.Special.ToName(), IdentifierType.Npc => id.Index == ushort.MaxValue - ? ToName(id.Kind, id.DataId) - : $"{ToName(id.Kind, id.DataId)} at {id.Index}", + ? Data.ToName(id.Kind, id.DataId) + : $"{Data.ToName(id.Kind, id.DataId)} at {id.Index}", IdentifierType.UnkObject => id.PlayerName.IsEmpty ? $"Unknown Object at {id.Index}" : $"{id.PlayerName} at {id.Index}", @@ -95,32 +89,6 @@ public partial class ActorManager }; } - - /// - /// Convert a given ID for a certain ObjectKind to a name. - /// - /// Invalid or a valid name. - public string ToName(ObjectKind kind, uint dataId) - => TryGetName(kind, dataId, out var ret) ? ret : "Invalid"; - - - /// - /// Convert a given ID for a certain ObjectKind to a name. - /// - public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name) - { - name = null; - return kind switch - { - ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), - ObjectKind.Companion => Companions.TryGetValue(dataId, out name), - (ObjectKind)15 => Ornaments.TryGetValue(dataId, out name), // TODO: CS Update - ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), - ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), - _ => false, - }; - } - /// /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// @@ -395,7 +363,7 @@ public partial class ActorManager /// Checks if the world is a valid public world or ushort.MaxValue (any world). public bool VerifyWorld(ushort worldId) - => worldId == ushort.MaxValue || Worlds.ContainsKey(worldId); + => worldId == ushort.MaxValue || Data.Worlds.ContainsKey(worldId); /// Verify that the enum value is a specific actor and return the name if it is. public static bool VerifySpecial(SpecialActor actor) @@ -418,10 +386,10 @@ public partial class ActorManager { return kind switch { - ObjectKind.MountType => Mounts.ContainsKey(dataId), - ObjectKind.Companion => Companions.ContainsKey(dataId), - (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update - ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), + ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), + ObjectKind.Companion => Data.Companions.ContainsKey(dataId), + (ObjectKind)15 => Data.Ornaments.ContainsKey(dataId), // TODO: CS Update + ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), _ => false, }; } @@ -429,11 +397,11 @@ public partial class ActorManager public bool VerifyNpcData(ObjectKind kind, uint dataId) => kind switch { - ObjectKind.MountType => Mounts.ContainsKey(dataId), - ObjectKind.Companion => Companions.ContainsKey(dataId), - (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update - ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), - ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), + ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), + ObjectKind.Companion => Data.Companions.ContainsKey(dataId), + (ObjectKind)15 => Data.Ornaments.ContainsKey(dataId), // TODO: CS Update + ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), + ObjectKind.EventNpc => Data.ENpcs.ContainsKey(dataId), _ => false, }; } diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs new file mode 100644 index 00000000..d8a773a5 --- /dev/null +++ b/Penumbra.GameData/Data/BNpcNames.cs @@ -0,0 +1,15814 @@ +using System; +using System.Collections.Generic; + +namespace Penumbra.GameData.Data; + +public static class NpcNames +{ + /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2022-11-24. + public static IReadOnlyList> CreateNames() + => new IReadOnlyList[] + { + new uint[]{0}, + new uint[]{6373}, + new uint[]{411, 412, 965, 1064, 1863, 2012}, + new uint[]{3, 176}, + new uint[]{4, 460}, + new uint[]{5, 184, 408}, + new uint[]{6, 183, 407, 2020}, + new uint[]{7, 125, 1121, 2157}, + new uint[]{8, 481}, + new uint[]{9, 571}, + new uint[]{10, 572, 589}, + new uint[]{11, 2033}, + new uint[]{12, 13, 299, 301, 590, 1039, 1122, 1216, 1315, 1743, 2011, 2757}, + new uint[]{397, 398, 566, 1067}, + new uint[]{14, 195, 202}, + new uint[]{16, 502}, + new uint[]{15}, + new uint[]{17, 132, 270, 316, 963, 1034, 1120, 1742, 2688, 2707, 2743, 2745}, + new uint[]{182, 319, 320, 321, 493, 1026, 1218, 2018}, + new uint[]{19, 322, 323, 324, 1025, 1219, 2019}, + new uint[]{20, 110, 494, 1854}, + new uint[]{21, 492, 606, 1028, 1029, 1086, 1198, 1349, 2039, 2169, 2223, 3687}, + new uint[]{22, 180, 400, 573, 1085, 4064}, + new uint[]{23, 162, 175, 1085, 3341}, + new uint[]{24, 163, 232, 233, 508, 607, 1136, 3343}, + new uint[]{25}, + new uint[]{26, 216, 217, 500, 592, 1040}, + new uint[]{27, 164, 1757}, + new uint[]{28, 966, 1070, 1071}, + new uint[]{29}, + new uint[]{30, 131, 280, 365, 552, 2996, 3621, 4108, 4286}, + Array.Empty(), + new uint[]{32, 203}, + new uint[]{33, 165, 214, 215, 497}, + new uint[]{34, 506}, + Array.Empty(), + new uint[]{36, 296, 485, 1051}, + new uint[]{37, 198, 1101}, + new uint[]{38, 279, 363, 364, 612, 1078, 1146}, + new uint[]{39, 177, 188, 962, 1350, 4046}, + new uint[]{40, 1145}, + new uint[]{41, 168, 201, 486, 1199}, + new uint[]{227, 284, 285, 489, 603, 1043, 1084}, + new uint[]{43, 181}, + new uint[]{44, 587, 1147, 2225, 4069}, + new uint[]{45, 491, 2918}, + new uint[]{46, 1703, 2056}, + new uint[]{47, 141, 199, 220, 699}, + new uint[]{48, 166}, + new uint[]{49, 200, 1046, 1222}, + Array.Empty(), + Array.Empty(), + new uint[]{50, 283, 464, 551, 1148}, + new uint[]{130, 206, 462}, + Array.Empty(), + new uint[]{53, 57, 1113, 1364, 1809, 1815, 1821, 2234, 2983, 3110, 3176}, + Array.Empty(), + new uint[]{54, 179, 201, 395, 564, 696, 1142}, + new uint[]{55, 2032}, + new uint[]{56, 7134}, + Array.Empty(), + new uint[]{58, 1810, 1816, 1822, 2173, 2351, 3181}, + new uint[]{59, 1811, 1817, 1823, 2120, 2173, 2238, 3177}, + new uint[]{60, 1812, 1818, 1824, 2235, 3178}, + new uint[]{61, 1365, 1813, 1819, 1825, 2173, 2236, 3180, 3334}, + new uint[]{62}, + new uint[]{1021}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{67, 229, 538, 1139}, + new uint[]{68, 230}, + new uint[]{69, 231, 2313}, + new uint[]{1138}, + Array.Empty(), + Array.Empty(), + new uint[]{73}, + new uint[]{74}, + Array.Empty(), + new uint[]{140, 239, 2362}, + new uint[]{2363}, + new uint[]{139, 240, 675, 2361, 4067, 4851}, + new uint[]{79}, + new uint[]{79}, + new uint[]{52, 80, 540}, + new uint[]{81}, + new uint[]{82, 172, 540}, + new uint[]{83}, + new uint[]{84}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{91}, + new uint[]{91}, + new uint[]{91, 317}, + new uint[]{91, 317}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{0}, + new uint[]{548}, + Array.Empty(), + new uint[]{310}, + new uint[]{314}, + new uint[]{312}, + new uint[]{101}, + new uint[]{102, 190, 209, 660, 1777}, + new uint[]{103, 208, 662, 1783}, + new uint[]{104, 210, 661, 1782}, + new uint[]{663, 1784}, + new uint[]{106, 352, 1066}, + new uint[]{107, 405, 1092, 1093, 2182}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{456}, + new uint[]{110}, + new uint[]{111}, + Array.Empty(), + Array.Empty(), + new uint[]{112, 171, 3342}, + new uint[]{113}, + new uint[]{114}, + new uint[]{115}, + new uint[]{116, 912}, + new uint[]{117}, + new uint[]{118, 298, 396, 490, 696, 1042}, + new uint[]{119}, + new uint[]{302, 399, 1033, 1088, 1089, 2756}, + new uint[]{391, 645, 950, 1134, 1758, 1831}, + new uint[]{237}, + new uint[]{174, 1366, 1846, 3994}, + new uint[]{120, 605}, + new uint[]{170, 1763, 2156}, + new uint[]{121}, + new uint[]{83}, + new uint[]{122}, + new uint[]{84}, + new uint[]{84}, + new uint[]{123}, + Array.Empty(), + new uint[]{125}, + new uint[]{185}, + new uint[]{186}, + new uint[]{186}, + new uint[]{189}, + Array.Empty(), + Array.Empty(), + new uint[]{139}, + new uint[]{197, 226, 381, 382}, + new uint[]{196, 289}, + new uint[]{228, 1758}, + Array.Empty(), + new uint[]{191}, + Array.Empty(), + new uint[]{140}, + new uint[]{245, 259, 1177, 1353, 2155, 2300}, + new uint[]{253, 256}, + new uint[]{249, 251, 252}, + new uint[]{259, 260, 1725}, + new uint[]{219, 268, 351, 570, 1072, 1115, 1170, 1244, 2987, 2989}, + new uint[]{1114, 1244}, + new uint[]{305, 1037, 1119}, + new uint[]{304, 1736}, + new uint[]{303}, + new uint[]{287, 288, 1045}, + new uint[]{286}, + new uint[]{1019}, + new uint[]{221, 222, 366, 1065, 1869}, + new uint[]{223, 224, 1316}, + new uint[]{282, 1036}, + new uint[]{281, 1116, 1181}, + new uint[]{207, 242, 467, 1738, 1768}, + new uint[]{242, 1739, 1759}, + Array.Empty(), + Array.Empty(), + new uint[]{0}, + new uint[]{276, 277, 278, 575, 901, 1041}, + new uint[]{275, 1741, 2008}, + new uint[]{1671, 1836}, + new uint[]{264, 1024, 1744, 1841, 3622, 4285}, + new uint[]{788, 1761, 3627, 3699}, + new uint[]{1344}, + new uint[]{273, 1030, 1125, 2049}, + new uint[]{274, 2050}, + Array.Empty(), + new uint[]{6733}, + new uint[]{292, 293, 1022}, + Array.Empty(), + Array.Empty(), + new uint[]{269, 1865}, + new uint[]{1814, 1820, 1826, 3179, 3545}, + Array.Empty(), + new uint[]{272, 1023}, + new uint[]{271}, + new uint[]{318, 409, 410, 574, 577, 958, 1049, 1082, 1083}, + new uint[]{306, 403, 1047, 1317}, + new uint[]{1185}, + new uint[]{1186}, + new uint[]{1185}, + new uint[]{1186}, + new uint[]{1185}, + new uint[]{1186}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{718}, + new uint[]{719}, + new uint[]{720}, + new uint[]{721}, + new uint[]{722}, + new uint[]{723}, + new uint[]{724}, + new uint[]{725}, + new uint[]{718}, + new uint[]{719}, + new uint[]{720}, + new uint[]{721}, + new uint[]{722}, + new uint[]{723}, + new uint[]{724}, + new uint[]{2752}, + new uint[]{1649}, + new uint[]{1647}, + new uint[]{1644}, + new uint[]{1649}, + new uint[]{1647}, + new uint[]{1644}, + new uint[]{1649}, + new uint[]{1647}, + new uint[]{1644}, + new uint[]{1801}, + new uint[]{1801}, + new uint[]{1801}, + new uint[]{1375}, + new uint[]{1376}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{422}, + new uint[]{424}, + new uint[]{425}, + new uint[]{428}, + new uint[]{444}, + new uint[]{443}, + new uint[]{437}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{440}, + new uint[]{988}, + Array.Empty(), + new uint[]{1208}, + new uint[]{442}, + new uint[]{441}, + new uint[]{423}, + new uint[]{426}, + new uint[]{427}, + new uint[]{428}, + new uint[]{633}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{134}, + new uint[]{553}, + Array.Empty(), + new uint[]{136}, + new uint[]{142}, + new uint[]{135}, + Array.Empty(), + new uint[]{596}, + new uint[]{234, 235, 308, 1135}, + new uint[]{1180, 1735}, + new uint[]{238, 505}, + new uint[]{236, 504, 1123, 1785, 2038, 3344}, + new uint[]{129, 211, 591, 1027, 1740}, + Array.Empty(), + new uint[]{104}, + Array.Empty(), + Array.Empty(), + new uint[]{77, 169, 241}, + new uint[]{360, 361, 1069, 1161}, + new uint[]{265, 349, 1100}, + new uint[]{341, 1068}, + new uint[]{1075, 1168, 2733}, + new uint[]{344}, + new uint[]{342, 979, 1107, 2732}, + new uint[]{978, 1107}, + Array.Empty(), + new uint[]{353, 687, 1091, 1167, 1748}, + new uint[]{355, 4114}, + new uint[]{357, 1076, 1313}, + new uint[]{358, 1077}, + new uint[]{563, 1069, 1102, 1103}, + new uint[]{560, 1081}, + new uint[]{561, 825, 1079, 1080, 1164}, + new uint[]{270, 1073, 1742, 1749}, + new uint[]{378, 379, 380, 1158}, + new uint[]{368, 369, 370, 1157}, + new uint[]{376, 1156, 1235}, + new uint[]{372, 373, 1155}, + Array.Empty(), + new uint[]{2165, 2167}, + new uint[]{1163, 2165, 2166}, + new uint[]{1162, 2166}, + new uint[]{2166}, + Array.Empty(), + new uint[]{392, 1098}, + new uint[]{393, 1096}, + new uint[]{394, 1087}, + new uint[]{401, 402, 1097}, + new uint[]{309, 404, 1035, 1123, 1791}, + new uint[]{414}, + new uint[]{413}, + new uint[]{416, 676}, + new uint[]{415}, + new uint[]{417, 1236}, + new uint[]{420, 421, 674, 1075, 1719}, + new uint[]{418, 420, 1719}, + new uint[]{419, 1173}, + new uint[]{262, 1050, 1101}, + new uint[]{326, 1309}, + new uint[]{329, 330, 1311}, + new uint[]{1310}, + new uint[]{328}, + new uint[]{234, 307, 503}, + new uint[]{290}, + new uint[]{243, 2511}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1115}, + new uint[]{331, 1997, 3332}, + new uint[]{333, 3332}, + new uint[]{332}, + Array.Empty(), + new uint[]{337}, + new uint[]{338}, + new uint[]{339}, + Array.Empty(), + Array.Empty(), + new uint[]{3332}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1769}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{636, 638, 1032, 1153}, + new uint[]{640, 642, 2021, 2533}, + new uint[]{684}, + new uint[]{632, 641, 893, 953, 1140}, + new uint[]{639, 1063, 3604}, + new uint[]{637, 685, 1638, 3338, 3572, 3589}, + new uint[]{658, 1017, 1141, 1780, 2232}, + new uint[]{635, 643}, + new uint[]{1182, 1760}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1850}, + new uint[]{784, 1755}, + new uint[]{1852}, + new uint[]{644, 2755}, + new uint[]{657, 1756, 1849, 3336, 3567, 3963}, + new uint[]{634, 3533}, + Array.Empty(), + new uint[]{1185}, + Array.Empty(), + new uint[]{567, 1183, 1789, 1853}, + new uint[]{1753}, + new uint[]{1021}, + new uint[]{480}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108, 1486, 1694, 2136, 2137, 2564, 2655, 2667, 2824, 3234, 3725, 3726, 3734, 3740, 3741, 4687, 4696, 4805, 5186, 5187, 5199, 5278, 5279, 5280, 5515, 5517, 5526, 5529, 5530, 5553, 5559, 5561, 5562, 5570, 5588, 6126, 6155, 6173, 6193, 6197, 6198, 6199}, + new uint[]{1204}, + new uint[]{342}, + new uint[]{1382}, + new uint[]{1205}, + new uint[]{1205}, + new uint[]{1382}, + new uint[]{1206}, + new uint[]{1207}, + Array.Empty(), + new uint[]{1298}, + new uint[]{1299}, + new uint[]{1300}, + new uint[]{1280}, + new uint[]{1281}, + new uint[]{1282}, + new uint[]{1283}, + new uint[]{1286}, + new uint[]{1279}, + new uint[]{1279}, + Array.Empty(), + new uint[]{282, 1036, 1038}, + new uint[]{294}, + new uint[]{1262}, + new uint[]{108, 148, 157, 444, 510, 686, 1185, 1459, 1466, 1644, 1645, 1646, 1680, 1801, 2137, 2143, 2154, 2160, 2193, 2265, 2294, 2345, 2547, 2549, 2595, 2598, 2605, 2633, 2814, 2815, 2821, 2846, 2903, 2907, 3091, 3093, 3227, 3231, 3234, 3240, 3252, 3287, 3376, 3380, 3381, 3382, 3423, 3764, 3791, 3798, 3818, 3821, 3822, 3823, 4613, 4624, 4631, 4657, 4658, 4698, 4888, 4896, 4897, 4943, 4951, 4952, 4954, 4956, 5199, 5204, 5218, 5219, 5220, 5281, 5282}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{453}, + new uint[]{453}, + new uint[]{524}, + Array.Empty(), + Array.Empty(), + new uint[]{527}, + Array.Empty(), + new uint[]{526}, + Array.Empty(), + new uint[]{520}, + Array.Empty(), + new uint[]{533}, + Array.Empty(), + new uint[]{526}, + new uint[]{453}, + new uint[]{453}, + Array.Empty(), + Array.Empty(), + new uint[]{453}, + new uint[]{453}, + new uint[]{453}, + new uint[]{526}, + new uint[]{529}, + new uint[]{453}, + new uint[]{453}, + new uint[]{453}, + new uint[]{526}, + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{491}, + new uint[]{203}, + Array.Empty(), + Array.Empty(), + new uint[]{230}, + new uint[]{526}, + new uint[]{51}, + new uint[]{512}, + new uint[]{446}, + new uint[]{517}, + new uint[]{518}, + new uint[]{445}, + new uint[]{447}, + new uint[]{448}, + new uint[]{513}, + new uint[]{449}, + new uint[]{514}, + Array.Empty(), + new uint[]{515}, + new uint[]{450}, + new uint[]{451}, + new uint[]{516}, + new uint[]{452}, + new uint[]{498}, + new uint[]{507}, + new uint[]{479}, + new uint[]{471}, + new uint[]{472}, + new uint[]{473}, + new uint[]{474}, + new uint[]{475}, + new uint[]{476}, + new uint[]{477}, + new uint[]{478}, + new uint[]{1131}, + new uint[]{1130}, + Array.Empty(), + Array.Empty(), + new uint[]{501, 1029, 1117, 1788}, + new uint[]{509}, + new uint[]{72}, + new uint[]{20}, + new uint[]{19}, + new uint[]{14}, + new uint[]{52}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{464}, + new uint[]{464}, + Array.Empty(), + new uint[]{465}, + new uint[]{466}, + new uint[]{467}, + new uint[]{468}, + Array.Empty(), + new uint[]{470}, + new uint[]{598}, + new uint[]{600}, + new uint[]{598}, + new uint[]{599}, + new uint[]{608}, + new uint[]{609}, + new uint[]{610}, + new uint[]{611}, + new uint[]{614}, + new uint[]{939}, + new uint[]{30}, + new uint[]{621}, + new uint[]{622}, + new uint[]{621}, + new uint[]{622}, + new uint[]{193}, + new uint[]{194}, + new uint[]{192}, + new uint[]{947}, + new uint[]{628}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + new uint[]{0}, + Array.Empty(), + new uint[]{613}, + new uint[]{455}, + Array.Empty(), + Array.Empty(), + new uint[]{655}, + new uint[]{541}, + new uint[]{437}, + new uint[]{1536}, + new uint[]{6947, 6948}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{453}, + new uint[]{453}, + new uint[]{453}, + new uint[]{453}, + new uint[]{454}, + new uint[]{454}, + new uint[]{454}, + new uint[]{511}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{553}, + new uint[]{543}, + new uint[]{169}, + new uint[]{84}, + new uint[]{1385}, + Array.Empty(), + new uint[]{627}, + new uint[]{439}, + new uint[]{0}, + new uint[]{448}, + new uint[]{448}, + new uint[]{521, 902, 905, 1859, 4396}, + new uint[]{522}, + new uint[]{550}, + new uint[]{542}, + new uint[]{536}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{523}, + new uint[]{535}, + new uint[]{528}, + new uint[]{531, 1728}, + new uint[]{532}, + new uint[]{4}, + new uint[]{523}, + Array.Empty(), + Array.Empty(), + new uint[]{10}, + new uint[]{5}, + Array.Empty(), + new uint[]{140}, + new uint[]{139}, + new uint[]{924}, + new uint[]{923}, + new uint[]{923}, + new uint[]{944}, + new uint[]{690}, + new uint[]{925}, + new uint[]{943}, + new uint[]{925}, + new uint[]{929}, + new uint[]{1306}, + new uint[]{1304}, + new uint[]{928}, + new uint[]{1307}, + new uint[]{689}, + Array.Empty(), + new uint[]{698}, + Array.Empty(), + Array.Empty(), + new uint[]{1694}, + new uint[]{789, 790, 1762, 1774, 1790}, + new uint[]{789, 1448, 1746, 1774, 1775}, + new uint[]{682, 794, 1111, 1737, 1754, 1770, 1781}, + new uint[]{793, 1770, 6725}, + Array.Empty(), + Array.Empty(), + new uint[]{1625}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1301}, + new uint[]{690}, + new uint[]{929}, + new uint[]{928}, + new uint[]{1305}, + new uint[]{1306}, + new uint[]{1307}, + new uint[]{929}, + new uint[]{1305}, + new uint[]{689}, + new uint[]{934}, + new uint[]{934}, + new uint[]{709}, + new uint[]{925}, + new uint[]{925}, + new uint[]{781}, + new uint[]{1306}, + new uint[]{758}, + new uint[]{756}, + new uint[]{757}, + new uint[]{765}, + new uint[]{766}, + new uint[]{767}, + new uint[]{782}, + new uint[]{783}, + Array.Empty(), + new uint[]{841}, + new uint[]{842}, + new uint[]{843}, + new uint[]{844}, + new uint[]{845}, + new uint[]{708}, + new uint[]{1647}, + Array.Empty(), + new uint[]{262}, + new uint[]{932}, + new uint[]{1305}, + new uint[]{1305}, + new uint[]{823}, + new uint[]{751}, + new uint[]{751}, + new uint[]{751}, + new uint[]{828}, + new uint[]{823}, + new uint[]{829}, + new uint[]{830}, + new uint[]{831}, + new uint[]{823}, + new uint[]{828}, + new uint[]{751}, + new uint[]{907}, + new uint[]{756}, + new uint[]{932}, + new uint[]{1306}, + new uint[]{932}, + new uint[]{757}, + new uint[]{1307}, + new uint[]{824}, + new uint[]{825}, + new uint[]{826}, + new uint[]{262}, + new uint[]{937}, + Array.Empty(), + new uint[]{795, 1750, 1751}, + new uint[]{1611, 1612, 1747}, + new uint[]{785, 1447, 1630}, + new uint[]{786, 1631}, + new uint[]{787}, + new uint[]{650, 1989, 2230, 2990, 3182, 3894, 5214}, + new uint[]{651, 1989, 2228, 2990, 3182, 3989, 5215}, + new uint[]{652, 1989, 2229, 3182, 4296}, + new uint[]{646, 647, 1767}, + new uint[]{646, 647, 1766}, + new uint[]{648, 1771}, + new uint[]{649, 1772}, + new uint[]{653, 659, 1745, 3473}, + new uint[]{3576, 4054}, + new uint[]{655}, + Array.Empty(), + new uint[]{246, 247}, + new uint[]{254}, + new uint[]{250}, + Array.Empty(), + new uint[]{245, 1837, 2297, 2300, 2301, 2304}, + new uint[]{253, 1388, 1731, 1839, 1880, 2305, 2369}, + new uint[]{249, 1389, 1838, 2296}, + new uint[]{258, 1840, 1879}, + new uint[]{192}, + new uint[]{193}, + new uint[]{194}, + Array.Empty(), + new uint[]{436, 1842, 2986}, + new uint[]{103, 662, 1843}, + new uint[]{662, 1844, 2985}, + new uint[]{663, 1845}, + new uint[]{378}, + new uint[]{368}, + Array.Empty(), + new uint[]{372}, + new uint[]{377, 1832, 2518, 2702, 2711}, + new uint[]{562, 1833, 2519, 2694}, + new uint[]{375, 1834, 2521, 2704}, + new uint[]{371, 1835, 2520, 2703}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{64, 67, 2319, 2321}, + new uint[]{65, 68, 2317, 2320}, + new uint[]{66, 69, 2313, 2318}, + new uint[]{386, 1827, 2525, 2534, 2543}, + new uint[]{384, 1828, 2524, 2540, 2679}, + new uint[]{389, 1829, 2523, 2537, 2721}, + new uint[]{565, 1830, 2526, 2541}, + new uint[]{225, 283, 367, 1360, 3099}, + new uint[]{0}, + new uint[]{218, 266, 350}, + Array.Empty(), + new uint[]{347, 1168, 1705, 2539}, + new uint[]{345, 1705}, + new uint[]{979, 1866, 2536, 2542}, + new uint[]{559, 1705, 2535}, + new uint[]{1851, 2239, 4094}, + new uint[]{914}, + new uint[]{914}, + new uint[]{914}, + new uint[]{0}, + new uint[]{1329}, + new uint[]{1272}, + new uint[]{1272}, + new uint[]{1272}, + new uint[]{1273}, + new uint[]{1273}, + new uint[]{1273}, + new uint[]{1274}, + new uint[]{1274}, + new uint[]{1275}, + new uint[]{1275}, + new uint[]{1275}, + new uint[]{1276}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1255}, + new uint[]{1252}, + new uint[]{1253}, + new uint[]{1254}, + new uint[]{1243}, + new uint[]{1242}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1240}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1239}, + new uint[]{1240}, + new uint[]{1240}, + new uint[]{1238}, + new uint[]{1372}, + new uint[]{1234}, + Array.Empty(), + new uint[]{1263}, + Array.Empty(), + new uint[]{405}, + new uint[]{1261}, + new uint[]{1260}, + new uint[]{1259}, + new uint[]{6}, + new uint[]{1043}, + Array.Empty(), + Array.Empty(), + new uint[]{402}, + new uint[]{1258}, + Array.Empty(), + new uint[]{1257}, + new uint[]{289}, + new uint[]{1008}, + new uint[]{1009}, + new uint[]{1010}, + new uint[]{1011}, + new uint[]{1012}, + new uint[]{1013}, + new uint[]{1015}, + new uint[]{1014}, + new uint[]{479}, + new uint[]{1016}, + new uint[]{1024}, + new uint[]{1039, 1122}, + new uint[]{1047}, + new uint[]{1052}, + new uint[]{1053}, + new uint[]{1054}, + new uint[]{1055}, + new uint[]{1056}, + new uint[]{1057}, + new uint[]{1058}, + new uint[]{1059}, + new uint[]{1061}, + new uint[]{1062}, + new uint[]{479}, + new uint[]{1060}, + new uint[]{1092}, + new uint[]{1079}, + new uint[]{1075}, + new uint[]{117}, + new uint[]{1175}, + new uint[]{1174}, + new uint[]{1099}, + new uint[]{934}, + new uint[]{933}, + new uint[]{933}, + new uint[]{933}, + new uint[]{1304}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{531}, + new uint[]{539}, + new uint[]{621}, + new uint[]{751}, + new uint[]{751}, + new uint[]{751}, + new uint[]{887}, + new uint[]{1377}, + new uint[]{1547}, + new uint[]{942}, + new uint[]{938}, + new uint[]{936}, + new uint[]{935}, + new uint[]{713}, + new uint[]{35}, + new uint[]{949}, + new uint[]{974}, + new uint[]{973}, + new uint[]{970}, + new uint[]{970}, + new uint[]{972}, + new uint[]{971}, + new uint[]{945}, + new uint[]{980}, + new uint[]{617}, + new uint[]{541}, + new uint[]{941}, + new uint[]{940}, + new uint[]{1374}, + new uint[]{404}, + new uint[]{1373}, + new uint[]{978}, + new uint[]{977}, + new uint[]{836}, + new uint[]{982}, + new uint[]{981}, + new uint[]{981}, + new uint[]{729}, + new uint[]{975}, + new uint[]{976}, + new uint[]{945}, + new uint[]{980}, + new uint[]{617}, + new uint[]{1556}, + new uint[]{619}, + new uint[]{620}, + new uint[]{948}, + new uint[]{517}, + new uint[]{946}, + new uint[]{193}, + new uint[]{194}, + new uint[]{947}, + new uint[]{945}, + new uint[]{980}, + new uint[]{617}, + new uint[]{1550}, + new uint[]{752}, + new uint[]{945}, + new uint[]{811}, + new uint[]{812}, + new uint[]{813}, + new uint[]{814}, + new uint[]{815}, + new uint[]{816}, + new uint[]{817}, + new uint[]{818}, + new uint[]{1209}, + new uint[]{1532}, + new uint[]{716}, + new uint[]{983}, + new uint[]{985}, + new uint[]{555}, + new uint[]{554}, + new uint[]{554}, + new uint[]{554}, + new uint[]{554}, + new uint[]{780}, + new uint[]{1378}, + new uint[]{1381}, + new uint[]{1380}, + new uint[]{1277}, + new uint[]{827}, + Array.Empty(), + new uint[]{1306}, + new uint[]{991}, + new uint[]{989}, + new uint[]{990}, + new uint[]{993}, + new uint[]{994}, + new uint[]{1007}, + new uint[]{1005}, + new uint[]{992}, + new uint[]{256}, + new uint[]{251}, + new uint[]{248}, + new uint[]{1187}, + new uint[]{1195}, + new uint[]{1194}, + new uint[]{117}, + new uint[]{1196}, + new uint[]{1187}, + new uint[]{1195}, + new uint[]{116}, + new uint[]{1197}, + new uint[]{1193}, + new uint[]{346}, + new uint[]{344}, + new uint[]{342}, + new uint[]{348}, + new uint[]{346}, + Array.Empty(), + new uint[]{342}, + new uint[]{348}, + new uint[]{1279}, + new uint[]{1284}, + new uint[]{1285}, + new uint[]{824}, + new uint[]{826}, + new uint[]{906}, + new uint[]{833}, + new uint[]{834}, + new uint[]{824}, + new uint[]{835}, + new uint[]{824}, + new uint[]{838}, + new uint[]{839}, + new uint[]{1314}, + new uint[]{837}, + new uint[]{1221}, + new uint[]{1343}, + new uint[]{342}, + new uint[]{968}, + new uint[]{1398}, + new uint[]{1399}, + new uint[]{8141}, + Array.Empty(), + new uint[]{1401}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{269}, + new uint[]{1001}, + new uint[]{1176}, + new uint[]{999}, + new uint[]{996}, + new uint[]{998}, + new uint[]{997}, + new uint[]{995}, + new uint[]{994}, + new uint[]{1006}, + new uint[]{1005}, + new uint[]{1267}, + new uint[]{1268}, + Array.Empty(), + new uint[]{1003}, + new uint[]{1002}, + new uint[]{1004}, + new uint[]{346}, + new uint[]{344}, + new uint[]{342}, + new uint[]{348}, + new uint[]{38}, + new uint[]{1210}, + new uint[]{1211}, + new uint[]{1212}, + new uint[]{1287}, + new uint[]{1288}, + new uint[]{1289}, + new uint[]{1290}, + new uint[]{1292}, + new uint[]{1291}, + new uint[]{1293}, + new uint[]{1294}, + new uint[]{1295}, + new uint[]{1296}, + new uint[]{1297}, + new uint[]{1548}, + new uint[]{1549}, + new uint[]{1551}, + new uint[]{1552}, + new uint[]{1553}, + new uint[]{1554}, + new uint[]{1555}, + Array.Empty(), + new uint[]{1566}, + new uint[]{1557}, + new uint[]{1558}, + new uint[]{1559}, + new uint[]{1383}, + new uint[]{1205}, + new uint[]{1205}, + Array.Empty(), + Array.Empty(), + new uint[]{346}, + new uint[]{1560}, + new uint[]{1561}, + new uint[]{1562}, + new uint[]{1563}, + Array.Empty(), + new uint[]{1565}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{891}, + new uint[]{888}, + new uint[]{889}, + new uint[]{890}, + new uint[]{1154}, + new uint[]{1169}, + new uint[]{1165}, + new uint[]{1160}, + new uint[]{1143}, + new uint[]{1133}, + new uint[]{1159}, + new uint[]{1073}, + new uint[]{1137}, + new uint[]{1127}, + new uint[]{1126}, + new uint[]{1109}, + new uint[]{1108}, + new uint[]{1110}, + new uint[]{1105}, + new uint[]{1166}, + new uint[]{1033, 1088}, + new uint[]{1167}, + new uint[]{1151}, + new uint[]{1150}, + new uint[]{542}, + new uint[]{1144}, + new uint[]{1136}, + new uint[]{497}, + new uint[]{1129}, + new uint[]{1128}, + new uint[]{1125}, + new uint[]{1124}, + new uint[]{1107}, + new uint[]{1106}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{200}, + new uint[]{1037}, + new uint[]{1264}, + new uint[]{1262}, + new uint[]{1262}, + new uint[]{1265}, + new uint[]{1266}, + new uint[]{1269}, + new uint[]{1270}, + new uint[]{116}, + Array.Empty(), + Array.Empty(), + new uint[]{1367, 1584}, + Array.Empty(), + Array.Empty(), + new uint[]{1584}, + new uint[]{1584}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1584}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{621}, + new uint[]{1861}, + new uint[]{981, 1337}, + new uint[]{981}, + new uint[]{1273, 1331, 1332, 1868}, + new uint[]{892, 1332, 1868}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + Array.Empty(), + new uint[]{1275, 1343, 1371, 1711, 1733}, + new uint[]{1275, 1343, 1371}, + new uint[]{1275, 1371}, + Array.Empty(), + new uint[]{1584}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1584, 1856}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1341, 1584}, + new uint[]{1711}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{526}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1729}, + Array.Empty(), + new uint[]{904, 981}, + Array.Empty(), + new uint[]{981, 1338}, + Array.Empty(), + new uint[]{1359}, + new uint[]{903, 1273}, + Array.Empty(), + new uint[]{1273}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + new uint[]{1277}, + Array.Empty(), + Array.Empty(), + new uint[]{1275, 1343}, + new uint[]{1275, 1371}, + new uint[]{1275, 1371}, + new uint[]{1275, 1371}, + Array.Empty(), + Array.Empty(), + new uint[]{1320, 1328, 1334, 1355, 1358, 1659}, + new uint[]{519}, + new uint[]{1352, 1356}, + new uint[]{1324}, + new uint[]{1336, 1339, 1340, 1361, 1867}, + new uint[]{1321, 1335, 1363, 1370}, + new uint[]{1322, 1323}, + new uint[]{1322, 1351, 1711}, + Array.Empty(), + new uint[]{2175}, + new uint[]{849}, + new uint[]{850}, + new uint[]{851}, + new uint[]{852}, + new uint[]{853}, + new uint[]{854}, + new uint[]{855}, + new uint[]{856}, + new uint[]{857}, + new uint[]{858}, + new uint[]{859}, + new uint[]{860}, + new uint[]{861}, + new uint[]{862}, + Array.Empty(), + Array.Empty(), + new uint[]{1177}, + new uint[]{1213}, + new uint[]{1304}, + new uint[]{929}, + new uint[]{974}, + new uint[]{1305}, + new uint[]{1177}, + new uint[]{923}, + new uint[]{1179}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{2971}, + new uint[]{895}, + Array.Empty(), + new uint[]{1319}, + Array.Empty(), + new uint[]{896}, + new uint[]{897}, + Array.Empty(), + new uint[]{898}, + new uint[]{899}, + new uint[]{865}, + new uint[]{866}, + new uint[]{867}, + new uint[]{868}, + new uint[]{869}, + new uint[]{870}, + new uint[]{871}, + new uint[]{872}, + new uint[]{873}, + new uint[]{874}, + new uint[]{875}, + new uint[]{876}, + new uint[]{877}, + new uint[]{878}, + new uint[]{879}, + new uint[]{880}, + new uint[]{881}, + new uint[]{882}, + new uint[]{883}, + new uint[]{884}, + new uint[]{885}, + new uint[]{886}, + new uint[]{1333, 1369, 1660, 1720, 1723, 1867}, + new uint[]{520, 1711}, + Array.Empty(), + Array.Empty(), + new uint[]{900, 1730}, + new uint[]{1347}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1325}, + new uint[]{1348}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1172}, + new uint[]{1171}, + Array.Empty(), + new uint[]{1237}, + new uint[]{1241}, + new uint[]{1256}, + new uint[]{1244}, + new uint[]{1251}, + new uint[]{1245}, + new uint[]{1246}, + new uint[]{1250}, + new uint[]{1249}, + new uint[]{1248}, + new uint[]{1247}, + new uint[]{346}, + new uint[]{344}, + new uint[]{346}, + new uint[]{344}, + new uint[]{344}, + new uint[]{344}, + new uint[]{969}, + new uint[]{1232}, + new uint[]{1231}, + new uint[]{1225}, + new uint[]{1230}, + new uint[]{1229}, + new uint[]{1228}, + new uint[]{1227}, + new uint[]{1226}, + new uint[]{1225}, + new uint[]{1589}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{1345}, + new uint[]{1278}, + new uint[]{1245}, + new uint[]{1330}, + new uint[]{109}, + new uint[]{143}, + new uint[]{144}, + new uint[]{145}, + new uint[]{146}, + new uint[]{111}, + new uint[]{284}, + new uint[]{147}, + new uint[]{151}, + new uint[]{153}, + new uint[]{441}, + new uint[]{158}, + new uint[]{438}, + new uint[]{117}, + new uint[]{154}, + new uint[]{155}, + new uint[]{157}, + new uint[]{430}, + new uint[]{431}, + new uint[]{432}, + new uint[]{433}, + new uint[]{434}, + new uint[]{435}, + new uint[]{625}, + new uint[]{1202}, + new uint[]{1203}, + new uint[]{1200}, + new uint[]{1201}, + new uint[]{918}, + new uint[]{919}, + new uint[]{920}, + new uint[]{921}, + new uint[]{922}, + new uint[]{1326}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1271}, + new uint[]{1331}, + new uint[]{908, 1178}, + new uint[]{833}, + Array.Empty(), + new uint[]{497}, + new uint[]{967}, + new uint[]{629}, + new uint[]{631}, + new uint[]{117}, + new uint[]{97}, + new uint[]{984}, + new uint[]{1303}, + new uint[]{987}, + new uint[]{986}, + new uint[]{1302}, + new uint[]{1357}, + new uint[]{1362}, + new uint[]{1223}, + new uint[]{1224}, + new uint[]{894}, + Array.Empty(), + new uint[]{1188}, + new uint[]{1189}, + new uint[]{1190}, + new uint[]{1191}, + new uint[]{1192}, + new uint[]{1193}, + new uint[]{117}, + new uint[]{1276}, + new uint[]{1346}, + new uint[]{1312}, + new uint[]{1608}, + new uint[]{1342}, + new uint[]{621}, + new uint[]{622}, + new uint[]{1354}, + new uint[]{1704}, + new uint[]{193}, + new uint[]{194}, + new uint[]{602}, + new uint[]{602}, + new uint[]{601}, + new uint[]{139}, + new uint[]{140}, + new uint[]{212}, + new uint[]{2163}, + new uint[]{130}, + new uint[]{108}, + new uint[]{2135}, + new uint[]{2118}, + new uint[]{2160}, + Array.Empty(), + new uint[]{2136}, + new uint[]{1471}, + new uint[]{1803}, + new uint[]{1804}, + new uint[]{244}, + new uint[]{128}, + new uint[]{354}, + new uint[]{1907}, + new uint[]{139}, + new uint[]{599}, + new uint[]{597}, + new uint[]{35}, + new uint[]{192}, + new uint[]{517}, + new uint[]{1680}, + new uint[]{1680}, + new uint[]{1678}, + new uint[]{1678}, + new uint[]{108}, + new uint[]{1676}, + new uint[]{1677}, + new uint[]{1672}, + new uint[]{1673}, + new uint[]{1674}, + new uint[]{114}, + new uint[]{1675}, + new uint[]{1535}, + new uint[]{1536}, + Array.Empty(), + new uint[]{1533}, + new uint[]{1534}, + new uint[]{1538}, + new uint[]{1535}, + new uint[]{1539}, + new uint[]{1540}, + new uint[]{1541}, + new uint[]{1542}, + new uint[]{1543}, + new uint[]{1544}, + new uint[]{1545}, + new uint[]{1546}, + new uint[]{108}, + Array.Empty(), + new uint[]{1696}, + new uint[]{1697}, + new uint[]{1698}, + Array.Empty(), + new uint[]{1689}, + new uint[]{1690}, + new uint[]{1691}, + new uint[]{1692}, + new uint[]{1693}, + new uint[]{1681}, + new uint[]{1681}, + new uint[]{1681}, + new uint[]{1681}, + new uint[]{1682}, + new uint[]{1683}, + new uint[]{1684}, + new uint[]{1685}, + new uint[]{1686}, + new uint[]{1687}, + new uint[]{29}, + new uint[]{1803}, + new uint[]{1804}, + new uint[]{1804}, + new uint[]{1802}, + new uint[]{1802}, + new uint[]{1802}, + new uint[]{1566}, + new uint[]{1566}, + new uint[]{1566}, + new uint[]{1402}, + new uint[]{1640}, + new uint[]{1970}, + new uint[]{2104}, + new uint[]{1883}, + new uint[]{1884}, + new uint[]{1885}, + new uint[]{1531, 2147}, + new uint[]{1886}, + new uint[]{1650}, + new uint[]{1613}, + new uint[]{1887}, + new uint[]{1888}, + new uint[]{2147}, + new uint[]{1889}, + new uint[]{1650}, + new uint[]{1650}, + new uint[]{2146}, + new uint[]{1890}, + new uint[]{1890}, + new uint[]{108}, + Array.Empty(), + new uint[]{1648}, + new uint[]{2091}, + new uint[]{1648}, + Array.Empty(), + new uint[]{108}, + new uint[]{1397}, + new uint[]{1397}, + new uint[]{1497}, + new uint[]{1498}, + new uint[]{1415}, + new uint[]{1499}, + new uint[]{1396}, + new uint[]{2154}, + new uint[]{2154}, + new uint[]{1657}, + new uint[]{2152}, + new uint[]{1654}, + new uint[]{1655}, + new uint[]{1656}, + new uint[]{1652}, + new uint[]{1449}, + new uint[]{1653}, + new uint[]{1808}, + new uint[]{1421}, + new uint[]{1892}, + new uint[]{1893}, + new uint[]{1894}, + new uint[]{1895}, + Array.Empty(), + new uint[]{1897}, + new uint[]{1805}, + new uint[]{1646}, + new uint[]{1645}, + new uint[]{1646}, + new uint[]{1645}, + new uint[]{1644}, + new uint[]{1644}, + new uint[]{1644}, + new uint[]{1644}, + new uint[]{1644}, + new uint[]{2100}, + new uint[]{2098}, + new uint[]{2099}, + new uint[]{1453}, + new uint[]{2101}, + new uint[]{2102}, + new uint[]{2103}, + new uint[]{583}, + new uint[]{68}, + new uint[]{69}, + new uint[]{2063}, + new uint[]{619}, + new uint[]{620}, + new uint[]{1391}, + new uint[]{1392}, + new uint[]{1393}, + new uint[]{1394}, + new uint[]{1639}, + new uint[]{750}, + new uint[]{1395}, + new uint[]{436}, + new uint[]{103}, + new uint[]{104}, + new uint[]{1901}, + new uint[]{653}, + new uint[]{1416}, + new uint[]{2204}, + new uint[]{1903}, + new uint[]{1904}, + new uint[]{1905}, + new uint[]{1906}, + new uint[]{1585}, + new uint[]{1586}, + new uint[]{1587}, + new uint[]{1588}, + new uint[]{1589}, + new uint[]{1589}, + new uint[]{1590}, + new uint[]{1848}, + Array.Empty(), + new uint[]{1592}, + new uint[]{1591}, + new uint[]{1593}, + new uint[]{1799}, + new uint[]{1596}, + new uint[]{1597}, + new uint[]{1595}, + new uint[]{1594}, + new uint[]{1599}, + new uint[]{1598}, + new uint[]{1600}, + new uint[]{1601}, + Array.Empty(), + new uint[]{1806}, + new uint[]{1805}, + new uint[]{1805}, + new uint[]{1858}, + new uint[]{1417}, + new uint[]{2201}, + new uint[]{2201}, + new uint[]{598}, + new uint[]{1907}, + new uint[]{550}, + new uint[]{614}, + new uint[]{2064}, + new uint[]{122}, + new uint[]{139}, + new uint[]{546}, + new uint[]{2065}, + new uint[]{2066}, + new uint[]{57}, + new uint[]{80}, + new uint[]{2200}, + new uint[]{2199}, + new uint[]{2198}, + new uint[]{619}, + new uint[]{729}, + new uint[]{2087}, + new uint[]{2088}, + new uint[]{1810}, + new uint[]{2089}, + new uint[]{1418}, + new uint[]{269}, + new uint[]{67}, + new uint[]{115}, + new uint[]{1910}, + new uint[]{1419}, + new uint[]{1420}, + new uint[]{1421}, + new uint[]{1911}, + new uint[]{1912}, + new uint[]{1582, 1605, 1847}, + Array.Empty(), + new uint[]{1581}, + new uint[]{1603}, + new uint[]{1582}, + Array.Empty(), + new uint[]{1581}, + new uint[]{1603}, + new uint[]{11}, + new uint[]{1422}, + new uint[]{1423}, + new uint[]{40}, + new uint[]{130}, + new uint[]{56}, + new uint[]{201}, + new uint[]{56}, + new uint[]{1424}, + new uint[]{1919}, + new uint[]{1920}, + new uint[]{1921}, + new uint[]{113}, + new uint[]{1923}, + new uint[]{1419}, + new uint[]{1425}, + new uint[]{1426}, + new uint[]{115}, + new uint[]{117}, + new uint[]{56}, + new uint[]{1924}, + new uint[]{1925}, + new uint[]{1926}, + new uint[]{2161}, + new uint[]{656}, + new uint[]{2162}, + new uint[]{1848}, + new uint[]{1927}, + new uint[]{1450}, + new uint[]{213}, + new uint[]{1451}, + new uint[]{1929}, + new uint[]{1422}, + new uint[]{1423}, + new uint[]{1452}, + new uint[]{1930}, + new uint[]{1424}, + new uint[]{1453}, + new uint[]{1931}, + new uint[]{2096}, + new uint[]{1932}, + new uint[]{1933}, + new uint[]{2153}, + new uint[]{1907}, + new uint[]{1935}, + new uint[]{1936}, + new uint[]{1937}, + new uint[]{361}, + new uint[]{1939}, + new uint[]{824}, + new uint[]{1391}, + new uint[]{1863}, + new uint[]{2186}, + new uint[]{2186}, + new uint[]{1941}, + new uint[]{2068}, + new uint[]{2202}, + new uint[]{2076}, + new uint[]{2077}, + new uint[]{2078}, + new uint[]{2079}, + new uint[]{1454}, + new uint[]{2080}, + new uint[]{656}, + new uint[]{2082}, + new uint[]{2081}, + new uint[]{1688}, + new uint[]{1942}, + new uint[]{15}, + new uint[]{56}, + new uint[]{116}, + new uint[]{614}, + new uint[]{2083}, + new uint[]{2084}, + new uint[]{1293}, + new uint[]{656}, + new uint[]{2085}, + new uint[]{2086}, + new uint[]{2086}, + new uint[]{1567}, + new uint[]{1568}, + new uint[]{1569}, + new uint[]{1570}, + new uint[]{1798}, + new uint[]{1572}, + new uint[]{1573}, + Array.Empty(), + new uint[]{1571}, + new uint[]{1574}, + new uint[]{1575}, + new uint[]{1576}, + new uint[]{1577}, + new uint[]{1578}, + new uint[]{1579}, + new uint[]{1580}, + new uint[]{2073}, + new uint[]{2074}, + new uint[]{2075}, + new uint[]{1385}, + new uint[]{1581}, + new uint[]{1582}, + new uint[]{1583}, + new uint[]{1584}, + new uint[]{1584}, + new uint[]{1584}, + new uint[]{1607}, + new uint[]{1427}, + new uint[]{1428}, + new uint[]{1429}, + new uint[]{1430}, + new uint[]{1431}, + new uint[]{1432}, + new uint[]{1433}, + new uint[]{1434}, + new uint[]{1435}, + new uint[]{1436}, + new uint[]{1437}, + new uint[]{1438}, + new uint[]{1439}, + new uint[]{1440}, + new uint[]{1445}, + new uint[]{1441}, + new uint[]{1442}, + new uint[]{1443}, + new uint[]{1444}, + new uint[]{1610}, + new uint[]{1446}, + new uint[]{1614}, + new uint[]{1615}, + new uint[]{1616}, + new uint[]{1617}, + new uint[]{1618}, + new uint[]{1619}, + new uint[]{1620}, + new uint[]{1621}, + new uint[]{1622}, + new uint[]{1623}, + new uint[]{1624}, + new uint[]{1625}, + new uint[]{646}, + new uint[]{647}, + new uint[]{648}, + new uint[]{1629}, + new uint[]{1630}, + new uint[]{1631}, + new uint[]{1632}, + new uint[]{1633}, + new uint[]{1634}, + new uint[]{1635}, + new uint[]{1636}, + new uint[]{1637}, + new uint[]{1638}, + new uint[]{1796}, + new uint[]{1797}, + new uint[]{1734}, + new uint[]{1734}, + new uint[]{1734}, + new uint[]{1747}, + new uint[]{1750}, + new uint[]{1752}, + new uint[]{1763}, + new uint[]{1764}, + new uint[]{1765}, + new uint[]{1767}, + new uint[]{1766}, + new uint[]{1773}, + new uint[]{1776}, + new uint[]{1778}, + new uint[]{114}, + new uint[]{1777}, + new uint[]{1783}, + new uint[]{491}, + new uint[]{1789}, + new uint[]{1638}, + new uint[]{1631}, + new uint[]{1787}, + new uint[]{117}, + new uint[]{1762}, + new uint[]{1770}, + new uint[]{1779}, + new uint[]{1786}, + new uint[]{1793}, + new uint[]{1792}, + new uint[]{1794}, + new uint[]{1232}, + new uint[]{1795}, + new uint[]{1170}, + new uint[]{115}, + new uint[]{1148}, + new uint[]{113}, + new uint[]{1609}, + new uint[]{2105}, + new uint[]{2106}, + new uint[]{2107, 2120}, + new uint[]{2108}, + new uint[]{2109}, + new uint[]{2110}, + new uint[]{2111}, + new uint[]{2159}, + new uint[]{1810}, + new uint[]{1811}, + new uint[]{1812}, + new uint[]{1486}, + new uint[]{2092}, + new uint[]{2113}, + new uint[]{269}, + Array.Empty(), + new uint[]{2114}, + new uint[]{557}, + new uint[]{2116}, + new uint[]{2117}, + new uint[]{2206}, + new uint[]{1975}, + new uint[]{1976}, + new uint[]{34}, + new uint[]{1977}, + new uint[]{1978}, + new uint[]{1979}, + new uint[]{1980}, + Array.Empty(), + new uint[]{1982}, + new uint[]{58}, + new uint[]{59}, + new uint[]{60}, + new uint[]{61}, + new uint[]{1985}, + new uint[]{1986}, + Array.Empty(), + new uint[]{1988}, + Array.Empty(), + new uint[]{1991}, + new uint[]{331}, + new uint[]{333}, + new uint[]{332}, + new uint[]{1996}, + new uint[]{2001}, + new uint[]{2002}, + new uint[]{2003}, + new uint[]{2004}, + new uint[]{2005}, + new uint[]{2006}, + new uint[]{2007}, + new uint[]{236}, + new uint[]{2010}, + new uint[]{2013}, + new uint[]{2014}, + new uint[]{2015}, + new uint[]{2016}, + new uint[]{2017}, + new uint[]{2022}, + new uint[]{2023}, + new uint[]{2024}, + new uint[]{2025}, + new uint[]{2026}, + new uint[]{2029}, + new uint[]{2030}, + new uint[]{2031}, + new uint[]{2034}, + new uint[]{2035}, + new uint[]{2036}, + new uint[]{2037}, + new uint[]{2041}, + new uint[]{2042}, + new uint[]{2043}, + new uint[]{2043, 2044}, + new uint[]{2045}, + new uint[]{2046}, + new uint[]{2047}, + new uint[]{2048}, + new uint[]{2051}, + new uint[]{2051}, + new uint[]{2051}, + new uint[]{2052}, + new uint[]{2053}, + new uint[]{2054}, + new uint[]{2055}, + new uint[]{2115}, + new uint[]{2123}, + new uint[]{2106, 2124}, + new uint[]{2125}, + new uint[]{2126}, + new uint[]{2109, 2127}, + new uint[]{2128}, + new uint[]{2129}, + new uint[]{2089}, + new uint[]{2113}, + new uint[]{2205}, + new uint[]{1727}, + new uint[]{1946}, + new uint[]{2149}, + new uint[]{104}, + new uint[]{105}, + new uint[]{2089}, + new uint[]{2130}, + Array.Empty(), + new uint[]{2131}, + new uint[]{2132}, + new uint[]{2133}, + new uint[]{2134}, + new uint[]{2121}, + new uint[]{2137}, + new uint[]{2137}, + new uint[]{2143}, + new uint[]{2067}, + new uint[]{253}, + Array.Empty(), + new uint[]{245}, + new uint[]{1126}, + new uint[]{2069}, + new uint[]{2070}, + new uint[]{2071}, + new uint[]{2072}, + new uint[]{2072}, + new uint[]{1486}, + new uint[]{2092}, + new uint[]{2088}, + new uint[]{1811}, + new uint[]{1809}, + new uint[]{2090}, + Array.Empty(), + Array.Empty(), + new uint[]{1486}, + new uint[]{2208, 2209}, + new uint[]{1464}, + new uint[]{1461}, + Array.Empty(), + new uint[]{2174}, + new uint[]{1479}, + new uint[]{1481}, + new uint[]{1463}, + new uint[]{1480}, + new uint[]{1462}, + new uint[]{1465}, + new uint[]{1466}, + new uint[]{1467}, + new uint[]{1459}, + new uint[]{1468}, + new uint[]{1469}, + new uint[]{1470}, + new uint[]{1471}, + new uint[]{1472}, + new uint[]{1473}, + new uint[]{1474}, + new uint[]{1475}, + new uint[]{1477}, + new uint[]{1476}, + new uint[]{1478}, + new uint[]{1482}, + new uint[]{2171}, + new uint[]{2176}, + new uint[]{1483}, + new uint[]{1484}, + new uint[]{1485}, + new uint[]{2118}, + new uint[]{2119}, + new uint[]{2120}, + new uint[]{2158}, + new uint[]{297}, + new uint[]{2136}, + new uint[]{108}, + new uint[]{2170}, + new uint[]{2210}, + new uint[]{2138}, + new uint[]{2142}, + new uint[]{2139}, + new uint[]{2140}, + new uint[]{2141}, + new uint[]{108}, + new uint[]{1490}, + new uint[]{108}, + new uint[]{620}, + new uint[]{1492}, + new uint[]{1949}, + new uint[]{1950}, + new uint[]{1493}, + new uint[]{1400}, + new uint[]{1951}, + new uint[]{1952}, + new uint[]{1953}, + new uint[]{1494}, + new uint[]{1495}, + new uint[]{1493}, + new uint[]{1400}, + new uint[]{1954}, + new uint[]{1955}, + new uint[]{1956}, + new uint[]{1957}, + new uint[]{1496}, + new uint[]{1400}, + new uint[]{1494}, + new uint[]{1495}, + new uint[]{1958}, + new uint[]{1958}, + new uint[]{1960}, + new uint[]{1961}, + new uint[]{1962}, + new uint[]{1963}, + new uint[]{1951}, + new uint[]{1952}, + new uint[]{1954}, + new uint[]{1967}, + new uint[]{1493}, + new uint[]{1400}, + new uint[]{1501}, + new uint[]{1968}, + new uint[]{1969}, + new uint[]{1500}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1994}, + new uint[]{417}, + new uint[]{678}, + new uint[]{270}, + new uint[]{2009}, + new uint[]{637}, + new uint[]{346}, + new uint[]{342}, + Array.Empty(), + new uint[]{347}, + Array.Empty(), + Array.Empty(), + new uint[]{345}, + new uint[]{2181}, + new uint[]{1862}, + new uint[]{1714}, + new uint[]{1716}, + new uint[]{1718}, + new uint[]{1721}, + new uint[]{1722}, + new uint[]{1604}, + new uint[]{1724}, + Array.Empty(), + new uint[]{1707}, + new uint[]{248}, + new uint[]{260}, + new uint[]{1710}, + new uint[]{343}, + new uint[]{1661}, + new uint[]{1662}, + new uint[]{1663}, + new uint[]{1664}, + new uint[]{1665}, + new uint[]{1666}, + new uint[]{1667}, + new uint[]{1668}, + new uint[]{1669}, + new uint[]{1670}, + Array.Empty(), + new uint[]{1503}, + new uint[]{1504}, + new uint[]{1505}, + new uint[]{257}, + new uint[]{1506}, + new uint[]{1699}, + new uint[]{1507}, + new uint[]{1508}, + new uint[]{1509}, + Array.Empty(), + new uint[]{1511}, + new uint[]{1512}, + new uint[]{1513}, + new uint[]{1514}, + new uint[]{1515}, + new uint[]{1516}, + new uint[]{1517}, + new uint[]{1700}, + new uint[]{1701}, + new uint[]{1518}, + new uint[]{1519}, + new uint[]{1520}, + new uint[]{1521}, + new uint[]{1522}, + new uint[]{1523}, + new uint[]{1524}, + new uint[]{1702}, + new uint[]{1525}, + new uint[]{1526}, + new uint[]{1527}, + Array.Empty(), + new uint[]{1529}, + new uint[]{1530}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{1717, 2172}, + new uint[]{1713}, + new uint[]{1715}, + new uint[]{1712}, + new uint[]{1726}, + new uint[]{2093}, + new uint[]{1811}, + new uint[]{1813}, + new uint[]{1418}, + new uint[]{1502}, + new uint[]{2095}, + new uint[]{2090}, + new uint[]{1809}, + new uint[]{2089}, + new uint[]{297}, + new uint[]{2094}, + new uint[]{2093}, + Array.Empty(), + Array.Empty(), + new uint[]{1459}, + new uint[]{1460}, + new uint[]{1469}, + new uint[]{1472}, + new uint[]{1992}, + new uint[]{1993}, + new uint[]{1995}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2057}, + Array.Empty(), + new uint[]{2059}, + new uint[]{2060}, + new uint[]{2061}, + new uint[]{2062}, + Array.Empty(), + new uint[]{2197}, + new uint[]{108}, + new uint[]{2196}, + new uint[]{1403}, + new uint[]{1882}, + new uint[]{1971}, + new uint[]{2211}, + new uint[]{1881}, + new uint[]{1640}, + new uint[]{1404}, + new uint[]{2203}, + new uint[]{1640}, + new uint[]{1972}, + new uint[]{1973}, + new uint[]{1681, 1870}, + new uint[]{1681, 1870}, + new uint[]{1681, 1870}, + new uint[]{1681, 1690, 1870}, + new uint[]{1651}, + new uint[]{2148}, + new uint[]{1640}, + new uint[]{1732}, + new uint[]{1848}, + new uint[]{399}, + new uint[]{67}, + new uint[]{68}, + new uint[]{69}, + Array.Empty(), + new uint[]{2144}, + new uint[]{2145}, + new uint[]{2168}, + new uint[]{1870}, + Array.Empty(), + Array.Empty(), + new uint[]{2183}, + new uint[]{1855}, + new uint[]{1974}, + new uint[]{1860}, + new uint[]{331}, + new uint[]{417}, + new uint[]{678}, + new uint[]{43}, + new uint[]{680}, + new uint[]{681}, + new uint[]{630}, + new uint[]{2097}, + new uint[]{2164}, + new uint[]{2185}, + new uint[]{2092}, + new uint[]{297}, + new uint[]{2121}, + new uint[]{2092}, + new uint[]{297}, + new uint[]{2121}, + new uint[]{2092}, + new uint[]{2089}, + new uint[]{2121}, + new uint[]{1984}, + new uint[]{1984}, + new uint[]{1998}, + new uint[]{1999}, + new uint[]{2180}, + Array.Empty(), + Array.Empty(), + new uint[]{2187}, + new uint[]{2188}, + new uint[]{2189}, + new uint[]{2190}, + new uint[]{2191}, + new uint[]{2192}, + new uint[]{1468}, + new uint[]{1470}, + new uint[]{1472}, + new uint[]{1473}, + new uint[]{1459}, + new uint[]{1459}, + new uint[]{2105}, + new uint[]{2109}, + new uint[]{2110}, + new uint[]{2106}, + new uint[]{2111}, + new uint[]{1382}, + new uint[]{1474}, + new uint[]{1482}, + new uint[]{1472}, + new uint[]{2212}, + new uint[]{108, 1482, 3234, 3240}, + new uint[]{2265}, + new uint[]{2266}, + new uint[]{2513}, + new uint[]{2325}, + new uint[]{108, 750, 9380}, + new uint[]{2261}, + new uint[]{2262}, + new uint[]{2263}, + new uint[]{2267}, + new uint[]{2267}, + new uint[]{2261}, + new uint[]{108}, + new uint[]{2259}, + new uint[]{2260}, + new uint[]{2256}, + new uint[]{2252}, + new uint[]{2251}, + new uint[]{2245}, + new uint[]{2249}, + new uint[]{2257}, + new uint[]{3984, 6290}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1185}, + new uint[]{2246}, + new uint[]{2247}, + new uint[]{2248}, + new uint[]{2250}, + new uint[]{2255}, + new uint[]{2253}, + new uint[]{2137}, + new uint[]{2139}, + new uint[]{2140}, + new uint[]{2141}, + new uint[]{2138}, + new uint[]{2324}, + new uint[]{2142}, + Array.Empty(), + new uint[]{2254}, + new uint[]{2264}, + new uint[]{2258}, + new uint[]{2256}, + new uint[]{1186}, + new uint[]{2268}, + new uint[]{2269}, + new uint[]{2270}, + new uint[]{2271}, + new uint[]{2272}, + new uint[]{2273}, + new uint[]{2274}, + new uint[]{2275}, + new uint[]{2276}, + new uint[]{2277}, + new uint[]{2278}, + new uint[]{2279}, + new uint[]{2280}, + new uint[]{2281}, + Array.Empty(), + new uint[]{706}, + new uint[]{706}, + new uint[]{707}, + new uint[]{710}, + new uint[]{710}, + new uint[]{711}, + new uint[]{712}, + new uint[]{727}, + new uint[]{727}, + new uint[]{728}, + new uint[]{730}, + new uint[]{731}, + new uint[]{2510}, + new uint[]{732}, + new uint[]{732}, + new uint[]{733}, + new uint[]{108}, + new uint[]{2370}, + new uint[]{2371}, + new uint[]{2371}, + new uint[]{2333}, + new uint[]{2334}, + new uint[]{2335}, + new uint[]{428}, + Array.Empty(), + new uint[]{2337}, + new uint[]{2338}, + new uint[]{2339}, + new uint[]{2282}, + new uint[]{2283}, + new uint[]{2283}, + new uint[]{2283}, + new uint[]{983}, + new uint[]{2284}, + new uint[]{2285}, + new uint[]{2286}, + new uint[]{1303}, + new uint[]{2287}, + new uint[]{2288}, + new uint[]{2289}, + new uint[]{2290}, + new uint[]{2290}, + new uint[]{2281}, + new uint[]{2332}, + new uint[]{2332}, + new uint[]{736}, + new uint[]{737}, + new uint[]{738}, + new uint[]{739}, + new uint[]{740}, + new uint[]{741}, + new uint[]{820}, + new uint[]{821}, + new uint[]{822}, + new uint[]{1864}, + new uint[]{1871}, + new uint[]{1872}, + new uint[]{820}, + new uint[]{822}, + new uint[]{1864}, + new uint[]{1873}, + new uint[]{1874}, + new uint[]{1875}, + new uint[]{1876}, + Array.Empty(), + new uint[]{426}, + new uint[]{2340}, + new uint[]{427}, + new uint[]{2341}, + new uint[]{2342}, + new uint[]{2343}, + new uint[]{2344}, + new uint[]{2346}, + new uint[]{2917}, + new uint[]{633}, + new uint[]{428}, + new uint[]{2347}, + new uint[]{108, 2348}, + new uint[]{2349}, + new uint[]{2291}, + new uint[]{2292}, + new uint[]{2292}, + new uint[]{2292}, + new uint[]{2293}, + new uint[]{2293}, + new uint[]{2293}, + new uint[]{2286}, + new uint[]{913}, + new uint[]{2000}, + new uint[]{730}, + new uint[]{2375}, + new uint[]{2376}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{727}, + new uint[]{730}, + new uint[]{8143}, + new uint[]{1205}, + new uint[]{909}, + new uint[]{910}, + new uint[]{911}, + new uint[]{2360}, + new uint[]{1390, 1877}, + new uint[]{1878}, + new uint[]{258, 2298}, + new uint[]{2300, 2309}, + new uint[]{2302}, + new uint[]{2303}, + new uint[]{2306}, + new uint[]{2307}, + Array.Empty(), + new uint[]{2309}, + new uint[]{2306}, + new uint[]{2310}, + new uint[]{2315}, + new uint[]{2316}, + new uint[]{2312}, + Array.Empty(), + Array.Empty(), + new uint[]{2508}, + new uint[]{2509}, + new uint[]{2364}, + new uint[]{2365}, + new uint[]{2366}, + new uint[]{2326}, + new uint[]{2299}, + Array.Empty(), + new uint[]{692}, + new uint[]{693}, + new uint[]{694}, + new uint[]{695}, + new uint[]{697}, + new uint[]{2359}, + new uint[]{700}, + new uint[]{701}, + new uint[]{702}, + new uint[]{703}, + new uint[]{704}, + new uint[]{705}, + new uint[]{2224}, + new uint[]{2226}, + new uint[]{2227}, + new uint[]{2233}, + new uint[]{2241}, + new uint[]{2242}, + new uint[]{2243}, + new uint[]{2244}, + new uint[]{1756}, + new uint[]{2237}, + new uint[]{2240}, + new uint[]{2231}, + new uint[]{2368}, + new uint[]{2350}, + new uint[]{64}, + new uint[]{2367}, + new uint[]{2353}, + new uint[]{2354}, + new uint[]{2355}, + new uint[]{2356}, + new uint[]{2357}, + new uint[]{2377}, + new uint[]{2378}, + new uint[]{2379}, + new uint[]{2380}, + new uint[]{2381}, + new uint[]{2382}, + new uint[]{2383}, + new uint[]{2384}, + new uint[]{2385}, + new uint[]{2386}, + new uint[]{2387}, + new uint[]{2388}, + new uint[]{2389}, + new uint[]{2390}, + new uint[]{2390}, + new uint[]{2391}, + new uint[]{2392}, + new uint[]{2393}, + new uint[]{2394}, + new uint[]{2395}, + new uint[]{2396}, + new uint[]{2397}, + new uint[]{2379}, + new uint[]{2398}, + new uint[]{2399}, + new uint[]{2400}, + new uint[]{2400}, + new uint[]{2401}, + new uint[]{2402}, + new uint[]{2403}, + new uint[]{2404}, + new uint[]{2405}, + new uint[]{2406}, + new uint[]{2407}, + new uint[]{2408}, + new uint[]{2409}, + new uint[]{2410}, + new uint[]{2411}, + new uint[]{2411}, + new uint[]{2412}, + new uint[]{2413}, + new uint[]{2414}, + new uint[]{2415}, + new uint[]{2416}, + new uint[]{2417}, + new uint[]{2418}, + new uint[]{2419}, + new uint[]{2420}, + new uint[]{2421}, + new uint[]{2422}, + new uint[]{2423}, + new uint[]{2424}, + new uint[]{2425}, + new uint[]{2426}, + new uint[]{2427}, + new uint[]{2428}, + new uint[]{2429}, + new uint[]{2430}, + new uint[]{2431}, + new uint[]{2432}, + new uint[]{2406}, + new uint[]{2433}, + new uint[]{2434}, + new uint[]{2435}, + new uint[]{2436}, + new uint[]{2437}, + new uint[]{2438}, + new uint[]{2439}, + new uint[]{2440}, + new uint[]{2439}, + new uint[]{2440}, + new uint[]{2441}, + new uint[]{2442}, + new uint[]{2443}, + new uint[]{2444}, + new uint[]{2445}, + new uint[]{2407}, + new uint[]{2446}, + new uint[]{2447}, + new uint[]{2448}, + new uint[]{2380}, + new uint[]{2449}, + new uint[]{2450}, + new uint[]{2451}, + new uint[]{2452}, + new uint[]{2414}, + new uint[]{2453}, + new uint[]{2454}, + new uint[]{2455}, + new uint[]{2405}, + new uint[]{2456}, + new uint[]{2457}, + new uint[]{2458}, + new uint[]{2459}, + new uint[]{2460}, + new uint[]{2461}, + new uint[]{2462}, + new uint[]{2463}, + new uint[]{2394}, + new uint[]{2464}, + new uint[]{2465}, + new uint[]{2466}, + new uint[]{2411}, + new uint[]{2429}, + new uint[]{2467}, + new uint[]{2468}, + new uint[]{2469}, + new uint[]{2470}, + new uint[]{2404}, + new uint[]{2471}, + new uint[]{2472}, + new uint[]{2473}, + new uint[]{2474}, + new uint[]{2475}, + new uint[]{2476}, + new uint[]{2477}, + new uint[]{2404}, + new uint[]{2471}, + new uint[]{2478}, + new uint[]{2479}, + new uint[]{2480}, + new uint[]{2481}, + new uint[]{2482}, + new uint[]{2482}, + new uint[]{2483}, + new uint[]{2484}, + new uint[]{2485}, + new uint[]{2486}, + new uint[]{2388}, + new uint[]{2460}, + new uint[]{2488}, + new uint[]{2489}, + new uint[]{2487}, + new uint[]{2490}, + new uint[]{2491}, + new uint[]{2492}, + new uint[]{2493}, + new uint[]{2494}, + new uint[]{2452}, + new uint[]{2495}, + new uint[]{2496}, + new uint[]{2497}, + new uint[]{2498}, + new uint[]{2499}, + new uint[]{2500}, + new uint[]{2501}, + new uint[]{2502}, + new uint[]{2503}, + new uint[]{2504}, + new uint[]{2422}, + new uint[]{2314}, + new uint[]{2374}, + Array.Empty(), + new uint[]{2550}, + Array.Empty(), + new uint[]{2334}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2291}, + new uint[]{2292}, + new uint[]{2292}, + new uint[]{2292}, + Array.Empty(), + new uint[]{2273}, + new uint[]{2295}, + new uint[]{2564}, + new uint[]{2309}, + new uint[]{2307}, + new uint[]{2313}, + new uint[]{2556}, + new uint[]{2557}, + Array.Empty(), + new uint[]{2552}, + new uint[]{2553}, + new uint[]{2554}, + new uint[]{2551}, + new uint[]{2550}, + new uint[]{2550}, + Array.Empty(), + new uint[]{0}, + new uint[]{2567}, + Array.Empty(), + new uint[]{2560}, + new uint[]{2561}, + Array.Empty(), + Array.Empty(), + new uint[]{2572}, + new uint[]{2573}, + new uint[]{2574}, + new uint[]{2575}, + new uint[]{2576}, + new uint[]{2577}, + new uint[]{2578}, + new uint[]{2579}, + new uint[]{2580}, + new uint[]{2581}, + new uint[]{2582}, + new uint[]{2583}, + new uint[]{2566}, + new uint[]{2594}, + new uint[]{2596}, + new uint[]{2595}, + new uint[]{2604}, + new uint[]{2605}, + new uint[]{2606}, + new uint[]{2602}, + new uint[]{2607}, + new uint[]{2609}, + new uint[]{2619}, + new uint[]{2620}, + new uint[]{2621}, + new uint[]{2622}, + new uint[]{2609, 2621}, + new uint[]{2610}, + new uint[]{2624}, + new uint[]{2625}, + new uint[]{2626}, + new uint[]{2627}, + new uint[]{2610, 2613}, + new uint[]{2611}, + new uint[]{1478}, + new uint[]{2660}, + new uint[]{2611, 2614, 2615, 2616}, + new uint[]{2612}, + new uint[]{2628}, + new uint[]{2629}, + new uint[]{2630}, + new uint[]{2631}, + new uint[]{2632}, + new uint[]{2634}, + Array.Empty(), + Array.Empty(), + new uint[]{2636}, + new uint[]{2637}, + new uint[]{2638}, + Array.Empty(), + new uint[]{2612}, + new uint[]{2623}, + new uint[]{2597}, + Array.Empty(), + Array.Empty(), + new uint[]{2590}, + new uint[]{2598}, + Array.Empty(), + Array.Empty(), + new uint[]{2603}, + new uint[]{2505}, + Array.Empty(), + new uint[]{2599}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2660}, + new uint[]{2659}, + new uint[]{1289}, + new uint[]{2547}, + new uint[]{1288}, + new uint[]{1287}, + new uint[]{1300}, + new uint[]{108}, + new uint[]{2661}, + new uint[]{2656}, + new uint[]{1297}, + new uint[]{2658}, + new uint[]{2662}, + new uint[]{2663}, + new uint[]{1300}, + new uint[]{2653}, + new uint[]{2548}, + new uint[]{2654}, + new uint[]{2549}, + new uint[]{2656}, + new uint[]{2660}, + new uint[]{2547}, + new uint[]{2654}, + new uint[]{1385}, + new uint[]{2650}, + new uint[]{2655}, + Array.Empty(), + new uint[]{2550}, + new uint[]{2551}, + new uint[]{2550}, + new uint[]{2550}, + Array.Empty(), + new uint[]{2552}, + new uint[]{2553}, + new uint[]{2554}, + new uint[]{2555}, + new uint[]{2652}, + new uint[]{2651}, + new uint[]{2591}, + new uint[]{2589}, + Array.Empty(), + new uint[]{2584}, + new uint[]{2585}, + new uint[]{2750}, + new uint[]{2593}, + new uint[]{2586}, + new uint[]{2587}, + new uint[]{2588}, + new uint[]{2665}, + new uint[]{2666}, + new uint[]{2667}, + new uint[]{2668}, + new uint[]{2669}, + new uint[]{2516}, + new uint[]{1528}, + new uint[]{374}, + Array.Empty(), + new uint[]{2527, 2538}, + new uint[]{2528}, + new uint[]{2529}, + new uint[]{2530}, + new uint[]{2531}, + new uint[]{2532}, + new uint[]{2522}, + new uint[]{2721, 2725}, + new uint[]{2698}, + new uint[]{2706}, + new uint[]{2722}, + new uint[]{2728, 3101, 3894}, + new uint[]{2729, 2737, 3333}, + new uint[]{2746}, + new uint[]{2709}, + new uint[]{2725}, + new uint[]{2672}, + new uint[]{2700, 2710}, + Array.Empty(), + Array.Empty(), + new uint[]{2865, 2868}, + new uint[]{2670, 3021}, + new uint[]{2684, 2702}, + new uint[]{2685, 2702}, + new uint[]{2702, 2712}, + Array.Empty(), + new uint[]{2680}, + new uint[]{2681}, + Array.Empty(), + new uint[]{2677, 3106}, + new uint[]{2677}, + new uint[]{2677, 3022, 3101}, + new uint[]{2686, 3020, 3185}, + new uint[]{2686, 3020, 3185}, + new uint[]{2686, 3020, 3185}, + new uint[]{2716}, + new uint[]{2717}, + new uint[]{2718}, + new uint[]{2689, 2715}, + new uint[]{2691, 2692, 2693, 2702, 2705}, + new uint[]{2749}, + Array.Empty(), + new uint[]{2723}, + Array.Empty(), + Array.Empty(), + new uint[]{2670}, + new uint[]{2738}, + new uint[]{2739}, + new uint[]{2740}, + new uint[]{2687}, + new uint[]{2708}, + new uint[]{2699}, + new uint[]{2713}, + new uint[]{2714}, + new uint[]{2724, 2735}, + new uint[]{2734}, + new uint[]{2736}, + new uint[]{2727}, + new uint[]{2678}, + new uint[]{2679}, + new uint[]{2680}, + new uint[]{2678}, + new uint[]{2679}, + new uint[]{2681}, + new uint[]{2680}, + new uint[]{2681}, + new uint[]{2682, 3894}, + new uint[]{2695}, + new uint[]{2695}, + new uint[]{2696}, + new uint[]{2719}, + new uint[]{2726}, + new uint[]{2681}, + new uint[]{2679}, + new uint[]{2680}, + new uint[]{2678}, + new uint[]{2679}, + new uint[]{2680}, + new uint[]{2741}, + new uint[]{539}, + new uint[]{2640}, + new uint[]{2742}, + new uint[]{2742}, + new uint[]{2641}, + new uint[]{2642}, + new uint[]{2643}, + new uint[]{2174}, + new uint[]{2644}, + new uint[]{2645}, + new uint[]{2646}, + new uint[]{1474}, + new uint[]{2647}, + new uint[]{2648}, + new uint[]{2649}, + new uint[]{2570}, + new uint[]{2569}, + new uint[]{2571, 3379}, + new uint[]{2505}, + new uint[]{2665}, + Array.Empty(), + new uint[]{108}, + new uint[]{2617, 2618, 2632, 2634}, + new uint[]{2592}, + new uint[]{2751}, + new uint[]{2754}, + new uint[]{2753}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{720, 721, 722, 725}, + new uint[]{108}, + new uint[]{108, 2568, 2608, 2891}, + new uint[]{2730, 4050}, + new uint[]{2827}, + new uint[]{2828}, + new uint[]{2829}, + new uint[]{2830}, + new uint[]{2831}, + new uint[]{2887}, + new uint[]{2888}, + new uint[]{2889}, + new uint[]{2890}, + new uint[]{2892}, + Array.Empty(), + new uint[]{2894}, + new uint[]{2895}, + new uint[]{2896}, + new uint[]{2897}, + new uint[]{2898}, + new uint[]{2899}, + new uint[]{2900}, + new uint[]{2901}, + new uint[]{2902}, + new uint[]{2904}, + new uint[]{2906}, + new uint[]{2905}, + new uint[]{2993}, + new uint[]{2790}, + new uint[]{2788}, + new uint[]{2789}, + new uint[]{2787}, + new uint[]{2786}, + new uint[]{2786}, + new uint[]{2782}, + new uint[]{2783}, + new uint[]{2784}, + new uint[]{2785}, + new uint[]{2781}, + new uint[]{2780}, + new uint[]{108}, + new uint[]{2832}, + new uint[]{2832}, + new uint[]{2833}, + new uint[]{2832}, + new uint[]{2832}, + new uint[]{2833}, + new uint[]{2891}, + new uint[]{2903}, + new uint[]{2916}, + new uint[]{2168}, + new uint[]{2833}, + new uint[]{2778}, + new uint[]{2779}, + new uint[]{2775}, + new uint[]{2776}, + new uint[]{2777}, + new uint[]{2086}, + new uint[]{2086}, + new uint[]{2086}, + new uint[]{2086}, + new uint[]{2774}, + new uint[]{2774}, + new uint[]{2809}, + new uint[]{2808}, + new uint[]{2801}, + new uint[]{2806}, + new uint[]{2805}, + new uint[]{2804}, + new uint[]{2800}, + new uint[]{2803}, + new uint[]{2802}, + new uint[]{2801}, + new uint[]{2800}, + new uint[]{2799}, + new uint[]{2796}, + new uint[]{2795}, + new uint[]{2798}, + new uint[]{2794}, + new uint[]{2797}, + new uint[]{2797}, + new uint[]{2792}, + new uint[]{2807}, + new uint[]{2791}, + new uint[]{2793}, + new uint[]{2815}, + new uint[]{2814}, + new uint[]{2813}, + new uint[]{2812}, + new uint[]{2824}, + new uint[]{2823}, + new uint[]{2822}, + new uint[]{108}, + new uint[]{2825}, + new uint[]{2821}, + new uint[]{2820}, + new uint[]{2819}, + new uint[]{2818}, + new uint[]{2817}, + new uint[]{2816}, + new uint[]{2826}, + new uint[]{2886}, + new uint[]{2851, 2970}, + new uint[]{2851, 2970}, + new uint[]{2852}, + new uint[]{2854}, + new uint[]{2853}, + new uint[]{2855}, + new uint[]{2856}, + Array.Empty(), + new uint[]{2857}, + new uint[]{2858}, + new uint[]{2859}, + new uint[]{2860}, + new uint[]{2861}, + Array.Empty(), + new uint[]{2758}, + new uint[]{2759}, + new uint[]{2760}, + new uint[]{2761}, + new uint[]{2762}, + new uint[]{2763}, + new uint[]{2764}, + new uint[]{2765}, + new uint[]{2766}, + new uint[]{2767}, + new uint[]{2768}, + new uint[]{2769}, + new uint[]{2770}, + new uint[]{2772}, + new uint[]{2771}, + new uint[]{2773}, + new uint[]{2810}, + new uint[]{2834}, + new uint[]{2836}, + new uint[]{2835}, + new uint[]{2837}, + Array.Empty(), + new uint[]{2839}, + new uint[]{2840}, + new uint[]{2841}, + new uint[]{2842}, + new uint[]{2843}, + new uint[]{2844}, + new uint[]{2845}, + new uint[]{2846}, + new uint[]{2847}, + new uint[]{2848}, + new uint[]{2849}, + new uint[]{2850}, + new uint[]{2884}, + new uint[]{2885}, + Array.Empty(), + new uint[]{2168}, + new uint[]{2994}, + new uint[]{2872}, + new uint[]{2873}, + new uint[]{3218}, + new uint[]{2995}, + new uint[]{2994}, + new uint[]{2994}, + new uint[]{2994}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2868}, + new uint[]{2868}, + new uint[]{2869}, + new uint[]{2870}, + Array.Empty(), + new uint[]{2864, 2875, 3894}, + new uint[]{2863, 2875, 3021}, + new uint[]{2862}, + new uint[]{3182}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2914}, + new uint[]{2915}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2609}, + new uint[]{2619}, + new uint[]{2620}, + new uint[]{2621}, + new uint[]{2622}, + new uint[]{2623}, + new uint[]{2609, 2621}, + new uint[]{2610}, + new uint[]{2624}, + new uint[]{2625}, + new uint[]{2626}, + new uint[]{2627}, + new uint[]{2610, 2613}, + new uint[]{2611}, + Array.Empty(), + new uint[]{2611, 2614, 2615, 2616}, + new uint[]{2612}, + new uint[]{2628}, + new uint[]{2629}, + new uint[]{2630}, + new uint[]{2631}, + new uint[]{2632}, + new uint[]{2634}, + new uint[]{2635}, + new uint[]{2636}, + new uint[]{2637}, + new uint[]{2638}, + new uint[]{2612}, + new uint[]{2617, 2618, 2632, 2634}, + new uint[]{539}, + new uint[]{2640}, + new uint[]{2742}, + new uint[]{2742}, + new uint[]{2641}, + new uint[]{2642}, + new uint[]{2643}, + new uint[]{2174}, + new uint[]{2644}, + new uint[]{2645}, + new uint[]{2646}, + new uint[]{1474}, + new uint[]{2647}, + new uint[]{2648}, + new uint[]{2649}, + new uint[]{2919}, + new uint[]{2920}, + new uint[]{2921}, + new uint[]{2922}, + new uint[]{2923}, + new uint[]{2924}, + new uint[]{2925}, + new uint[]{2926}, + new uint[]{2927}, + new uint[]{2928}, + new uint[]{2929}, + new uint[]{2930}, + new uint[]{2931}, + new uint[]{2932}, + new uint[]{2933}, + new uint[]{2934}, + new uint[]{2935}, + new uint[]{2936}, + new uint[]{2937}, + new uint[]{2938}, + new uint[]{2939}, + new uint[]{2940}, + new uint[]{2941}, + new uint[]{2942}, + new uint[]{2943}, + new uint[]{2944}, + new uint[]{2945}, + new uint[]{2946}, + new uint[]{2947}, + new uint[]{2948}, + new uint[]{2949}, + new uint[]{2950}, + new uint[]{2951}, + new uint[]{2952}, + new uint[]{2953}, + new uint[]{2954}, + new uint[]{2955}, + new uint[]{2956}, + new uint[]{2957}, + new uint[]{2958}, + new uint[]{2959}, + new uint[]{2960}, + new uint[]{2961}, + new uint[]{2962}, + new uint[]{2963}, + new uint[]{2964}, + new uint[]{2965}, + new uint[]{2966}, + new uint[]{2967}, + new uint[]{2968}, + new uint[]{2969}, + new uint[]{3330}, + new uint[]{2866}, + new uint[]{2972}, + new uint[]{2973}, + new uint[]{2974}, + new uint[]{2975}, + new uint[]{2976}, + new uint[]{2977}, + new uint[]{2978}, + new uint[]{2979}, + new uint[]{2980}, + new uint[]{2981}, + Array.Empty(), + new uint[]{2984}, + new uint[]{2988}, + new uint[]{2992}, + new uint[]{3050}, + new uint[]{3051}, + new uint[]{3052}, + new uint[]{3053}, + new uint[]{3054}, + new uint[]{3055}, + new uint[]{3330}, + new uint[]{5763}, + new uint[]{3056}, + new uint[]{3057}, + new uint[]{3058}, + new uint[]{3059}, + new uint[]{3060}, + new uint[]{3061}, + new uint[]{3666}, + Array.Empty(), + new uint[]{3014}, + new uint[]{3015}, + new uint[]{3016}, + new uint[]{3017}, + new uint[]{3018}, + new uint[]{3019}, + new uint[]{2904}, + new uint[]{2906}, + new uint[]{2905}, + new uint[]{2997}, + new uint[]{2998}, + new uint[]{2999}, + new uint[]{3000}, + new uint[]{3001}, + new uint[]{3002}, + new uint[]{3003}, + new uint[]{3004}, + new uint[]{3005}, + new uint[]{3006}, + new uint[]{3007}, + new uint[]{3008}, + new uint[]{3009}, + new uint[]{3010}, + new uint[]{3011}, + new uint[]{3012}, + new uint[]{3013}, + new uint[]{3065}, + new uint[]{3192}, + new uint[]{3193}, + new uint[]{3194}, + new uint[]{3192, 3193}, + new uint[]{3197}, + new uint[]{3198}, + new uint[]{3199}, + new uint[]{3200}, + new uint[]{3201}, + new uint[]{3197, 3199, 3200, 3201}, + new uint[]{3204}, + new uint[]{3205}, + new uint[]{3206}, + new uint[]{3207}, + new uint[]{3208}, + new uint[]{3209}, + new uint[]{3209}, + new uint[]{3209}, + new uint[]{3204}, + new uint[]{3210}, + new uint[]{3211}, + new uint[]{3212}, + new uint[]{3213}, + Array.Empty(), + new uint[]{3214}, + new uint[]{3215}, + new uint[]{3216}, + new uint[]{3217}, + new uint[]{3210}, + new uint[]{261}, + new uint[]{267}, + new uint[]{3190}, + new uint[]{3191}, + new uint[]{1467}, + new uint[]{3196}, + new uint[]{3195}, + new uint[]{2810}, + new uint[]{3240}, + new uint[]{3242}, + new uint[]{3069}, + new uint[]{3070}, + new uint[]{3062}, + new uint[]{3063}, + new uint[]{3064}, + new uint[]{2994}, + new uint[]{3218}, + new uint[]{2995}, + new uint[]{2994}, + new uint[]{2994}, + new uint[]{2994}, + new uint[]{3038}, + new uint[]{3014}, + new uint[]{3039}, + new uint[]{3040}, + new uint[]{3041}, + new uint[]{3043}, + new uint[]{3042}, + new uint[]{3044}, + new uint[]{3044}, + new uint[]{3045}, + new uint[]{3072}, + new uint[]{3073}, + new uint[]{3074}, + new uint[]{3071}, + new uint[]{3075}, + new uint[]{3066}, + new uint[]{3067}, + new uint[]{3068}, + new uint[]{3028}, + new uint[]{3288}, + new uint[]{3030}, + new uint[]{3032}, + new uint[]{3037}, + new uint[]{114}, + new uint[]{3031}, + new uint[]{3027}, + new uint[]{3034}, + new uint[]{3035}, + new uint[]{3219}, + new uint[]{3029}, + new uint[]{108, 1644, 2775, 3271, 3272, 3408, 3428, 3434, 3437, 3634, 3639, 3642, 3744, 3851, 3852, 4555, 4567, 4568, 4571, 4739, 4745, 4747, 5259, 5273}, + new uint[]{3046}, + new uint[]{3047}, + new uint[]{3046}, + new uint[]{3047}, + new uint[]{3048}, + new uint[]{3046}, + new uint[]{3046}, + new uint[]{3047}, + new uint[]{3038}, + new uint[]{3049}, + new uint[]{3033}, + new uint[]{3220}, + Array.Empty(), + new uint[]{3022}, + new uint[]{3021}, + new uint[]{887}, + new uint[]{1858}, + new uint[]{1858}, + new uint[]{887}, + new uint[]{3026}, + new uint[]{3076}, + new uint[]{3164}, + new uint[]{3169}, + new uint[]{3172}, + new uint[]{3168}, + new uint[]{3163}, + new uint[]{3162}, + new uint[]{3164}, + new uint[]{3129}, + new uint[]{3130}, + new uint[]{3131}, + new uint[]{3045}, + new uint[]{3133}, + new uint[]{3134}, + new uint[]{3135}, + new uint[]{3136}, + new uint[]{3137}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3139}, + new uint[]{3140}, + new uint[]{3141}, + new uint[]{3133}, + new uint[]{3142}, + new uint[]{3138}, + new uint[]{3129}, + new uint[]{3143}, + new uint[]{3144}, + new uint[]{3145}, + new uint[]{3146}, + new uint[]{3133}, + new uint[]{3129}, + new uint[]{3148}, + new uint[]{3147}, + new uint[]{3149}, + new uint[]{3272}, + new uint[]{3273}, + new uint[]{3274}, + new uint[]{3275}, + new uint[]{3276}, + new uint[]{3277}, + Array.Empty(), + new uint[]{3279}, + new uint[]{3280}, + new uint[]{3281}, + new uint[]{3282}, + new uint[]{3283}, + new uint[]{3284}, + new uint[]{3285}, + new uint[]{3286}, + new uint[]{1695}, + Array.Empty(), + new uint[]{3255}, + new uint[]{3256}, + new uint[]{3257}, + new uint[]{3258}, + new uint[]{3259}, + new uint[]{3260}, + new uint[]{3261}, + new uint[]{3262}, + new uint[]{3263}, + new uint[]{3264}, + new uint[]{3265}, + new uint[]{3266}, + new uint[]{3267}, + new uint[]{3268}, + new uint[]{3269}, + new uint[]{3270}, + new uint[]{3271}, + new uint[]{3150}, + new uint[]{3151}, + new uint[]{3152}, + new uint[]{3133}, + new uint[]{3153}, + new uint[]{3154}, + new uint[]{3155}, + new uint[]{3156}, + new uint[]{3159}, + new uint[]{3157}, + new uint[]{3158}, + new uint[]{3189}, + new uint[]{3160}, + new uint[]{3133}, + new uint[]{3138}, + new uint[]{3129}, + new uint[]{3165}, + new uint[]{3164}, + new uint[]{3166}, + new uint[]{3167}, + new uint[]{3170}, + new uint[]{3166}, + new uint[]{3167}, + new uint[]{3165}, + new uint[]{3164}, + new uint[]{3169}, + new uint[]{3243}, + new uint[]{3244}, + new uint[]{3246}, + new uint[]{3245}, + new uint[]{3247}, + new uint[]{3248}, + new uint[]{3249}, + new uint[]{3250}, + new uint[]{3380, 3381, 3382}, + new uint[]{3252}, + new uint[]{3119}, + new uint[]{3120}, + new uint[]{3121}, + new uint[]{3122}, + new uint[]{3123}, + new uint[]{3124}, + new uint[]{3125}, + new uint[]{3126}, + new uint[]{3110}, + new uint[]{2120}, + new uint[]{3114}, + new uint[]{3111}, + new uint[]{3112}, + new uint[]{3113}, + new uint[]{3118}, + new uint[]{3115}, + new uint[]{3116}, + new uint[]{3117}, + new uint[]{3127}, + new uint[]{3128}, + new uint[]{3132}, + new uint[]{3221}, + new uint[]{3152}, + new uint[]{3161}, + new uint[]{3165}, + new uint[]{3167}, + new uint[]{3164}, + new uint[]{3170}, + new uint[]{3172}, + new uint[]{3172}, + new uint[]{3173, 3174, 3175}, + new uint[]{3210}, + new uint[]{3213}, + new uint[]{3172}, + new uint[]{3172}, + new uint[]{3171}, + new uint[]{3091}, + new uint[]{3092}, + new uint[]{3093}, + Array.Empty(), + new uint[]{3095}, + new uint[]{3096}, + new uint[]{3097}, + new uint[]{3098}, + new uint[]{3077}, + new uint[]{3078}, + new uint[]{3079}, + new uint[]{3080}, + new uint[]{3081}, + new uint[]{108, 3082}, + new uint[]{3083}, + new uint[]{3084}, + new uint[]{3085}, + new uint[]{3086}, + new uint[]{3087}, + new uint[]{3088}, + new uint[]{3089}, + new uint[]{3090}, + new uint[]{3100}, + new uint[]{3100}, + new uint[]{3104}, + new uint[]{3104}, + new uint[]{3102}, + new uint[]{3103}, + new uint[]{3101}, + new uint[]{3107}, + new uint[]{3108}, + Array.Empty(), + new uint[]{3106}, + new uint[]{3105}, + Array.Empty(), + new uint[]{3188}, + new uint[]{3118, 3179}, + new uint[]{3183}, + new uint[]{3184}, + new uint[]{3153}, + new uint[]{3153}, + new uint[]{3154}, + new uint[]{3166}, + new uint[]{3166}, + new uint[]{3234}, + new uint[]{3235}, + new uint[]{3236}, + new uint[]{3237}, + new uint[]{3238}, + new uint[]{3239}, + new uint[]{3227}, + new uint[]{3228}, + new uint[]{3229}, + new uint[]{3230}, + new uint[]{3231}, + new uint[]{3232}, + new uint[]{3233}, + new uint[]{3241}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{108, 749, 4571, 8395}, + new uint[]{2993}, + new uint[]{108, 8395}, + new uint[]{3077}, + new uint[]{3222}, + new uint[]{3223}, + new uint[]{3224}, + new uint[]{3225}, + new uint[]{3301}, + new uint[]{3345}, + new uint[]{3345}, + new uint[]{3345}, + new uint[]{3082}, + new uint[]{3302}, + new uint[]{3303}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3078}, + new uint[]{2665}, + new uint[]{2665}, + new uint[]{3304}, + new uint[]{3307}, + new uint[]{2665, 3305}, + new uint[]{3304}, + new uint[]{3305}, + new uint[]{3306}, + new uint[]{3321}, + new uint[]{3322}, + new uint[]{3323}, + new uint[]{3324}, + Array.Empty(), + new uint[]{3325}, + new uint[]{3326}, + Array.Empty(), + new uint[]{3329}, + new uint[]{2168}, + new uint[]{3331}, + new uint[]{3314}, + Array.Empty(), + new uint[]{3315}, + new uint[]{3316}, + new uint[]{3317}, + new uint[]{3318}, + new uint[]{3319}, + new uint[]{3320}, + new uint[]{3309, 3310, 3311}, + Array.Empty(), + new uint[]{3287}, + new uint[]{3386}, + new uint[]{3289}, + new uint[]{3290}, + new uint[]{3291}, + new uint[]{3292}, + new uint[]{3294}, + new uint[]{3293}, + new uint[]{3335}, + new uint[]{3337}, + new uint[]{3339}, + new uint[]{3340}, + Array.Empty(), + new uint[]{3300}, + new uint[]{3046}, + new uint[]{108}, + new uint[]{108}, + new uint[]{3355}, + new uint[]{3354}, + new uint[]{3353}, + new uint[]{3368}, + new uint[]{3367}, + new uint[]{3366}, + new uint[]{3365}, + new uint[]{3358}, + new uint[]{3357}, + Array.Empty(), + new uint[]{3363}, + new uint[]{3362}, + new uint[]{3361}, + new uint[]{3360}, + new uint[]{3359}, + Array.Empty(), + new uint[]{3352}, + new uint[]{3351}, + new uint[]{3350}, + new uint[]{3349}, + new uint[]{3369}, + new uint[]{3370}, + new uint[]{3373}, + new uint[]{3357}, + new uint[]{3374}, + new uint[]{3375}, + new uint[]{3375}, + Array.Empty(), + new uint[]{3251}, + new uint[]{3362}, + new uint[]{3361}, + new uint[]{3360}, + new uint[]{3359}, + new uint[]{3298}, + new uint[]{3298}, + new uint[]{3378}, + new uint[]{3372}, + new uint[]{3371}, + new uint[]{3095}, + new uint[]{3, 5, 9, 11, 19, 20, 52, 83, 90, 131, 159, 165, 242, 312, 451, 471, 540, 567, 584, 588, 593, 598, 599, 606, 653, 658, 665, 668, 719, 737, 744, 750, 834, 839, 906, 939, 959, 962, 964, 993, 1023, 1033, 1043, 1052, 1056, 1058, 1102, 1103, 1105, 1106, 1114, 1126, 1133, 1142, 1143, 1144, 1215, 1227, 1244, 1248, 1276, 1279, 1280, 1373, 1374, 1375, 1376, 1377, 1380, 1381, 1382, 1384, 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, 1395, 1396, 1397, 1399, 1402, 1403, 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, 1413, 1414, 1417, 1418, 1420, 1565, 1566, 1567, 1568, 1569, 1570, 1571, 1573, 1574, 1575, 1577, 1578, 1589, 1603, 1604, 1605, 1607}, + new uint[]{108}, + new uint[]{108, 3373, 3374, 3375, 3387}, + new uint[]{3370}, + new uint[]{2665}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{3923}, + new uint[]{3925}, + new uint[]{3930}, + new uint[]{3931}, + new uint[]{3932}, + new uint[]{3933}, + new uint[]{3789}, + new uint[]{3405}, + new uint[]{3406}, + new uint[]{3407}, + new uint[]{3408}, + new uint[]{3409}, + new uint[]{3410}, + new uint[]{3791}, + Array.Empty(), + new uint[]{3793}, + new uint[]{3794}, + Array.Empty(), + new uint[]{3796}, + new uint[]{3797}, + new uint[]{3798}, + new uint[]{3818}, + new uint[]{3819}, + new uint[]{3820}, + new uint[]{3821}, + new uint[]{2143}, + new uint[]{3822}, + new uint[]{4383}, + new uint[]{4384}, + new uint[]{3823}, + new uint[]{4382}, + new uint[]{4383}, + new uint[]{4384}, + new uint[]{3293}, + new uint[]{3930}, + new uint[]{2343}, + new uint[]{3452}, + new uint[]{3453}, + new uint[]{3454}, + new uint[]{3455}, + new uint[]{3456}, + new uint[]{3457}, + new uint[]{3458}, + new uint[]{3459}, + new uint[]{3460}, + new uint[]{3461}, + new uint[]{3462}, + new uint[]{3463}, + new uint[]{3464}, + new uint[]{3465}, + new uint[]{3660}, + new uint[]{3660}, + new uint[]{3661}, + new uint[]{3661}, + new uint[]{3662}, + new uint[]{3662}, + new uint[]{3663}, + new uint[]{3663}, + new uint[]{3664}, + new uint[]{3664}, + new uint[]{3745}, + new uint[]{3746}, + new uint[]{3747}, + new uint[]{3748}, + new uint[]{3749}, + new uint[]{3747, 3748, 3750}, + new uint[]{4492}, + new uint[]{4420}, + new uint[]{1385}, + new uint[]{4133}, + new uint[]{3735}, + new uint[]{3921}, + new uint[]{3918}, + new uint[]{3910}, + new uint[]{3922}, + new uint[]{3913}, + new uint[]{3912}, + new uint[]{3915}, + new uint[]{3916}, + new uint[]{3911}, + new uint[]{3917}, + new uint[]{3920}, + new uint[]{3909}, + new uint[]{3914}, + new uint[]{3923}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3388}, + new uint[]{3389}, + new uint[]{3390}, + new uint[]{3391}, + new uint[]{3392}, + new uint[]{3393}, + new uint[]{3394}, + new uint[]{3395}, + new uint[]{3396}, + new uint[]{3397}, + new uint[]{3398}, + new uint[]{3399}, + new uint[]{3400}, + new uint[]{3401}, + new uint[]{3402}, + new uint[]{3403}, + new uint[]{3404}, + new uint[]{3409}, + new uint[]{3649}, + new uint[]{3649, 3658}, + new uint[]{4606}, + new uint[]{108}, + new uint[]{3650}, + new uint[]{3651}, + new uint[]{3652}, + new uint[]{3653}, + new uint[]{3654}, + new uint[]{3655}, + new uint[]{3658}, + Array.Empty(), + new uint[]{3649}, + new uint[]{108, 3649}, + new uint[]{3650}, + new uint[]{8144}, + new uint[]{3652}, + new uint[]{3653}, + new uint[]{3654}, + new uint[]{3658}, + new uint[]{3655}, + new uint[]{3754}, + new uint[]{3755}, + new uint[]{4142}, + new uint[]{4490}, + new uint[]{1300}, + new uint[]{3753}, + new uint[]{3757}, + new uint[]{8145}, + new uint[]{3758}, + new uint[]{3759}, + new uint[]{3760}, + new uint[]{3761}, + new uint[]{2667}, + Array.Empty(), + new uint[]{3758}, + new uint[]{3754}, + new uint[]{3755}, + new uint[]{3753}, + new uint[]{3757}, + new uint[]{8146}, + new uint[]{3758}, + new uint[]{3759}, + new uint[]{3760}, + new uint[]{3761}, + new uint[]{2667}, + Array.Empty(), + Array.Empty(), + new uint[]{3818}, + new uint[]{3799}, + new uint[]{3800}, + new uint[]{3801}, + new uint[]{3802}, + new uint[]{3803}, + new uint[]{3804}, + Array.Empty(), + new uint[]{3805}, + new uint[]{3807}, + new uint[]{3808}, + new uint[]{3809, 3815}, + new uint[]{3809}, + new uint[]{3810, 3816}, + new uint[]{3810}, + new uint[]{3811}, + new uint[]{3812}, + new uint[]{3026}, + new uint[]{3813}, + new uint[]{3814}, + new uint[]{3806}, + new uint[]{3815}, + new uint[]{3816}, + Array.Empty(), + new uint[]{3772}, + new uint[]{3772}, + new uint[]{3773}, + new uint[]{3778}, + new uint[]{3779}, + new uint[]{3774}, + new uint[]{3775}, + new uint[]{3759}, + new uint[]{3776}, + new uint[]{3777}, + new uint[]{3772}, + new uint[]{3772}, + new uint[]{3773}, + new uint[]{3778}, + new uint[]{3774}, + new uint[]{3759}, + new uint[]{6305}, + new uint[]{4259}, + Array.Empty(), + new uint[]{3824}, + new uint[]{3825}, + new uint[]{3826}, + new uint[]{3827}, + new uint[]{3828}, + new uint[]{3829}, + new uint[]{3830}, + new uint[]{3831}, + new uint[]{3832}, + new uint[]{3833}, + new uint[]{3834}, + new uint[]{4336}, + new uint[]{3835}, + new uint[]{3836}, + new uint[]{3837}, + new uint[]{3838}, + new uint[]{3839}, + new uint[]{3840}, + new uint[]{4340}, + new uint[]{4339}, + new uint[]{3818}, + new uint[]{3825}, + new uint[]{4489}, + new uint[]{3765}, + Array.Empty(), + new uint[]{3378}, + new uint[]{3766}, + new uint[]{3766}, + new uint[]{8958}, + new uint[]{3767}, + new uint[]{3770}, + new uint[]{3769}, + new uint[]{3771}, + new uint[]{3768}, + new uint[]{3765}, + new uint[]{4141}, + new uint[]{3726}, + new uint[]{3372}, + Array.Empty(), + new uint[]{3119}, + new uint[]{3120}, + new uint[]{4135}, + new uint[]{4136}, + new uint[]{4137}, + new uint[]{4138}, + new uint[]{3721}, + new uint[]{3723}, + new uint[]{3724}, + new uint[]{3722}, + new uint[]{3731}, + new uint[]{3732}, + new uint[]{3733}, + new uint[]{3736}, + new uint[]{3737}, + new uint[]{108}, + new uint[]{3739}, + new uint[]{3278}, + new uint[]{3727}, + new uint[]{3728}, + new uint[]{3734}, + new uint[]{3740}, + new uint[]{3741}, + new uint[]{3742}, + new uint[]{4130}, + new uint[]{3127}, + new uint[]{4139}, + new uint[]{4140}, + new uint[]{2064}, + new uint[]{4116}, + new uint[]{4125}, + new uint[]{4117, 4506}, + new uint[]{4126}, + new uint[]{4130}, + new uint[]{1394}, + new uint[]{4116}, + new uint[]{3481, 3489}, + Array.Empty(), + new uint[]{3479}, + Array.Empty(), + new uint[]{3488}, + new uint[]{3492}, + new uint[]{3483, 3490}, + new uint[]{3485}, + Array.Empty(), + Array.Empty(), + new uint[]{3484}, + new uint[]{3487}, + new uint[]{3486}, + new uint[]{3582, 3620, 3670, 4289}, + new uint[]{3574, 3584, 3591, 3900}, + new uint[]{3573, 3583, 3590, 3902, 4049}, + new uint[]{3575, 3585, 3592, 3901}, + new uint[]{4337}, + new uint[]{4338}, + new uint[]{4390}, + new uint[]{3581}, + new uint[]{3577}, + new uint[]{3565, 3588, 3688}, + new uint[]{3578, 3691}, + Array.Empty(), + new uint[]{3564}, + new uint[]{3571, 3696}, + new uint[]{3579, 3693}, + new uint[]{3580}, + new uint[]{3568, 3668, 4052, 4284}, + new uint[]{3608, 4070}, + new uint[]{3597}, + new uint[]{3593, 3715}, + new uint[]{3598, 3706, 3945}, + new uint[]{3595, 3712}, + new uint[]{3611}, + Array.Empty(), + new uint[]{3612}, + new uint[]{3603, 3714}, + new uint[]{3607, 3710}, + new uint[]{3610}, + new uint[]{3605, 3713}, + new uint[]{3596, 3711}, + new uint[]{3566, 3606}, + new uint[]{3599}, + new uint[]{3600, 3707}, + new uint[]{3601, 3708}, + new uint[]{3602, 3709}, + new uint[]{56}, + new uint[]{3609}, + new uint[]{3626, 3977}, + new uint[]{3624, 4045}, + new uint[]{3613}, + new uint[]{3614, 4277}, + new uint[]{3559}, + new uint[]{3615, 3700, 4042}, + new uint[]{3617, 3701}, + Array.Empty(), + new uint[]{3629, 4294}, + new uint[]{3625, 4291}, + new uint[]{3542}, + new uint[]{117}, + new uint[]{3526}, + new uint[]{3507, 3703}, + new uint[]{3512}, + new uint[]{3501, 3515, 3527}, + new uint[]{3502, 3516, 3528, 4061}, + new uint[]{3503, 3517, 3529}, + new uint[]{3511, 3705}, + new uint[]{3671, 4059}, + new uint[]{3704, 4071}, + new uint[]{3506, 3674}, + new uint[]{3500, 3514, 3531, 3672}, + new uint[]{3509}, + new uint[]{3510, 4288}, + new uint[]{3594, 4283, 4301}, + new uint[]{3611}, + new uint[]{3504, 3673}, + new uint[]{3524, 4293}, + Array.Empty(), + new uint[]{3523, 3702, 4060}, + Array.Empty(), + new uint[]{3513, 3530}, + new uint[]{3505, 3675}, + new uint[]{3525}, + new uint[]{3544}, + new uint[]{3541}, + Array.Empty(), + Array.Empty(), + new uint[]{3554}, + new uint[]{3619, 3698, 4053, 4278, 4290}, + Array.Empty(), + new uint[]{3623}, + new uint[]{3555}, + new uint[]{3556}, + new uint[]{3537}, + Array.Empty(), + new uint[]{3552}, + new uint[]{4399}, + new uint[]{3538}, + new uint[]{3539}, + new uint[]{3543}, + new uint[]{3561}, + new uint[]{3551}, + new uint[]{3534}, + new uint[]{3535}, + new uint[]{3536}, + new uint[]{3532}, + Array.Empty(), + new uint[]{3557}, + new uint[]{4128}, + new uint[]{3502}, + new uint[]{3503}, + new uint[]{4129}, + new uint[]{2082}, + new uint[]{1990}, + new uint[]{108}, + new uint[]{4346}, + new uint[]{4347}, + new uint[]{4348}, + new uint[]{4349}, + new uint[]{4346}, + new uint[]{4347}, + new uint[]{4348}, + new uint[]{4349}, + new uint[]{4130}, + new uint[]{3850}, + new uint[]{4131}, + new uint[]{3428}, + new uint[]{3429}, + new uint[]{3430}, + new uint[]{3431}, + new uint[]{3432}, + new uint[]{3433}, + new uint[]{3434}, + new uint[]{3435}, + new uint[]{3436}, + new uint[]{3411}, + new uint[]{3412}, + new uint[]{3413}, + new uint[]{3414}, + new uint[]{3415}, + new uint[]{3416}, + new uint[]{3417}, + new uint[]{3418}, + new uint[]{3419}, + new uint[]{3420}, + new uint[]{3421}, + new uint[]{3422}, + new uint[]{3423}, + new uint[]{3424}, + new uint[]{3425}, + new uint[]{3426}, + new uint[]{3427}, + new uint[]{4132}, + new uint[]{3540}, + new uint[]{3586}, + Array.Empty(), + new uint[]{3817}, + new uint[]{4154}, + new uint[]{4154}, + new uint[]{1391}, + new uint[]{4155}, + new uint[]{4156}, + new uint[]{4157}, + new uint[]{4145}, + new uint[]{4158}, + new uint[]{4159}, + new uint[]{4160}, + new uint[]{4161}, + new uint[]{4162}, + new uint[]{1392}, + new uint[]{4150}, + new uint[]{4151}, + new uint[]{4152}, + new uint[]{4153}, + new uint[]{4130}, + new uint[]{1394}, + new uint[]{4130}, + new uint[]{4179}, + new uint[]{3563, 3690}, + new uint[]{4130}, + new uint[]{2082}, + new uint[]{4144}, + new uint[]{2077}, + new uint[]{4143}, + new uint[]{2080}, + new uint[]{2076}, + new uint[]{3849}, + new uint[]{3841}, + new uint[]{3843}, + new uint[]{3634}, + new uint[]{4385}, + new uint[]{3850}, + new uint[]{3639}, + new uint[]{3293}, + new uint[]{3642}, + new uint[]{3851}, + new uint[]{3852}, + new uint[]{4400}, + new uint[]{3841}, + new uint[]{3841}, + new uint[]{3319}, + new uint[]{3319}, + new uint[]{4401}, + new uint[]{3841}, + new uint[]{3841}, + new uint[]{3842}, + new uint[]{3842}, + new uint[]{3843}, + new uint[]{3843}, + new uint[]{3844}, + new uint[]{3845}, + new uint[]{3846}, + new uint[]{3847}, + new uint[]{3848}, + new uint[]{2234}, + new uint[]{4148}, + new uint[]{4147}, + new uint[]{4146}, + new uint[]{2098}, + new uint[]{2099}, + new uint[]{1453}, + new uint[]{4427}, + new uint[]{4178}, + new uint[]{3632}, + new uint[]{3633}, + new uint[]{3634}, + new uint[]{3635}, + new uint[]{3636}, + new uint[]{3637}, + new uint[]{3638}, + new uint[]{3639}, + new uint[]{3640}, + new uint[]{3641}, + new uint[]{3642}, + new uint[]{3643}, + new uint[]{3644}, + new uint[]{3645}, + new uint[]{4385}, + new uint[]{4386}, + new uint[]{4387}, + new uint[]{3632, 3634, 3635, 3639, 3640, 3641, 3642, 3643, 3645}, + new uint[]{3223}, + new uint[]{3224}, + new uint[]{1848}, + new uint[]{4173}, + new uint[]{4174}, + new uint[]{2347}, + new uint[]{4180}, + new uint[]{4185}, + new uint[]{4184}, + new uint[]{4186}, + new uint[]{4187}, + new uint[]{3438}, + new uint[]{3439}, + new uint[]{3440}, + new uint[]{3441}, + new uint[]{3442}, + new uint[]{3443}, + new uint[]{3445, 11994}, + new uint[]{3446, 11993}, + new uint[]{3447}, + new uint[]{3448}, + new uint[]{3449}, + new uint[]{3450}, + new uint[]{3451}, + new uint[]{3445, 11994}, + new uint[]{4188}, + new uint[]{1422}, + new uint[]{1423}, + new uint[]{3665}, + new uint[]{3665}, + new uint[]{4381}, + new uint[]{4163}, + new uint[]{4164}, + new uint[]{4165}, + new uint[]{4166}, + new uint[]{4167}, + new uint[]{4168}, + new uint[]{4109}, + new uint[]{4169}, + new uint[]{4170}, + new uint[]{4171}, + new uint[]{4172}, + new uint[]{4426}, + new uint[]{4342}, + new uint[]{4343}, + new uint[]{4344}, + new uint[]{4345}, + new uint[]{4388}, + new uint[]{4389}, + new uint[]{3919}, + new uint[]{3915}, + Array.Empty(), + new uint[]{4190}, + new uint[]{4190}, + new uint[]{4191}, + new uint[]{3045}, + new uint[]{4192}, + new uint[]{4193}, + new uint[]{365}, + new uint[]{398}, + new uint[]{106}, + new uint[]{117}, + new uint[]{4194}, + new uint[]{3929}, + new uint[]{3928}, + new uint[]{4195}, + new uint[]{4196}, + new uint[]{407}, + new uint[]{171}, + new uint[]{45}, + new uint[]{170}, + new uint[]{24}, + new uint[]{3854}, + new uint[]{3855}, + new uint[]{3856}, + new uint[]{3857}, + new uint[]{4424}, + new uint[]{3859}, + new uint[]{3860}, + new uint[]{3861}, + new uint[]{3862}, + new uint[]{3863}, + new uint[]{114}, + new uint[]{3476}, + new uint[]{3475}, + new uint[]{3478}, + new uint[]{3743}, + new uint[]{4116}, + new uint[]{4423}, + new uint[]{4126}, + new uint[]{4127}, + new uint[]{4197, 4198, 4199}, + new uint[]{4200}, + new uint[]{4201}, + new uint[]{4202}, + new uint[]{1440}, + new uint[]{4203}, + new uint[]{3888}, + new uint[]{4193}, + new uint[]{4204}, + Array.Empty(), + new uint[]{2147}, + new uint[]{4107}, + new uint[]{4173}, + new uint[]{4174}, + new uint[]{2096}, + new uint[]{4175}, + new uint[]{4176}, + new uint[]{1391}, + new uint[]{1392}, + Array.Empty(), + new uint[]{1393}, + new uint[]{1990}, + new uint[]{4184}, + new uint[]{4181}, + new uint[]{4182}, + new uint[]{4183}, + new uint[]{445}, + new uint[]{4189}, + new uint[]{3338}, + new uint[]{3326}, + new uint[]{3339}, + new uint[]{4193}, + new uint[]{4205}, + new uint[]{4206}, + new uint[]{4207}, + new uint[]{4208}, + new uint[]{4209}, + new uint[]{4220}, + new uint[]{4221}, + new uint[]{4222}, + new uint[]{4079, 4097, 4117}, + new uint[]{4101}, + new uint[]{4102}, + new uint[]{4035}, + new uint[]{3900, 4048, 4393}, + new uint[]{3902}, + new uint[]{3901}, + new uint[]{3900}, + new uint[]{3901}, + new uint[]{3902}, + new uint[]{3901}, + new uint[]{4109}, + Array.Empty(), + new uint[]{4068, 4100}, + new uint[]{4112}, + new uint[]{3908}, + new uint[]{3893}, + new uint[]{3894, 4123}, + new uint[]{3894, 4123}, + new uint[]{3894}, + new uint[]{3896}, + new uint[]{3895}, + new uint[]{3897}, + new uint[]{3898}, + new uint[]{3899}, + new uint[]{3900}, + new uint[]{3901}, + new uint[]{3901}, + new uint[]{3900}, + new uint[]{3900}, + new uint[]{3902}, + new uint[]{4030}, + new uint[]{4031}, + new uint[]{4032}, + new uint[]{4030}, + new uint[]{4033}, + new uint[]{4031}, + new uint[]{4034}, + new uint[]{4035}, + new uint[]{4107}, + new uint[]{4110}, + new uint[]{4111}, + new uint[]{4103}, + new uint[]{4104}, + new uint[]{4105}, + new uint[]{4106}, + new uint[]{3729}, + new uint[]{3730}, + new uint[]{3562}, + new uint[]{1273, 1275, 5660, 6159, 6399, 6402, 6403, 6533, 6544, 6560}, + new uint[]{1273, 6159, 6402, 6405, 6533, 6544, 8475, 8478}, + new uint[]{1275, 6398, 6399, 6402, 6404, 6535, 6560, 8478}, + new uint[]{1273, 1275, 6159, 6399, 6403, 6533, 6544}, + new uint[]{3995, 6159, 6399, 6405, 6533, 6535, 6560, 8477}, + new uint[]{108, 1275, 3889, 4005, 5660, 5797, 6156, 6406, 6478, 6479, 6534, 6541, 6547, 6556, 6654, 8675, 8676}, + new uint[]{3991, 4254, 6150, 6409, 6410, 6452, 6484, 6486, 6502, 6520, 8470, 8472}, + new uint[]{3976, 4410, 5660, 5791, 6156, 6399, 6412, 6481, 6484, 6520, 6537, 6543, 6559, 6655, 8470, 8471, 8473, 8479, 8480, 8481, 8675, 8676}, + new uint[]{5660, 6411}, + new uint[]{3891, 5660, 5791, 6398, 6399, 6417, 6421, 6520, 6557, 6558, 6653, 8470, 8471, 8473, 8479, 8480, 8482, 8483}, + new uint[]{4402}, + new uint[]{108, 3452}, + new uint[]{3656}, + new uint[]{3657}, + new uint[]{3656}, + new uint[]{3657}, + new uint[]{2085}, + new uint[]{2086}, + new uint[]{4269}, + new uint[]{4270}, + new uint[]{4271}, + new uint[]{4272}, + new uint[]{2086}, + new uint[]{3763}, + new uint[]{3762}, + new uint[]{3764}, + new uint[]{4193}, + new uint[]{4192}, + new uint[]{4204}, + new uint[]{4209}, + new uint[]{4212}, + new uint[]{4223}, + new uint[]{4224}, + new uint[]{3339}, + new uint[]{4225}, + new uint[]{4220}, + new uint[]{4149}, + new uint[]{4392}, + new uint[]{108, 9374, 9379, 9380, 9381, 9382, 9383, 9386, 9387, 9388, 9543, 10187}, + new uint[]{3745}, + new uint[]{3746}, + new uint[]{3747}, + new uint[]{3748}, + new uint[]{3749}, + new uint[]{3747, 3748, 3750}, + new uint[]{3493}, + new uint[]{3761}, + new uint[]{2667}, + Array.Empty(), + new uint[]{3602}, + new uint[]{3599}, + new uint[]{3600}, + new uint[]{3601}, + new uint[]{729}, + new uint[]{4130}, + new uint[]{3482, 4408}, + new uint[]{3569, 3694}, + new uint[]{3695}, + new uint[]{3570}, + new uint[]{657, 3587, 3631, 4044}, + new uint[]{3630}, + new uint[]{3616, 4043}, + new uint[]{3894}, + new uint[]{3178}, + new uint[]{3177}, + Array.Empty(), + Array.Empty(), + new uint[]{4113}, + new uint[]{4113}, + new uint[]{4085}, + new uint[]{3907}, + new uint[]{4287}, + new uint[]{3906}, + new uint[]{4089}, + new uint[]{3903, 4078}, + new uint[]{4057, 4062, 4334}, + new uint[]{4280}, + new uint[]{4087}, + new uint[]{3904, 4076}, + new uint[]{3904}, + new uint[]{3617}, + new uint[]{3905}, + new uint[]{3905}, + new uint[]{4047}, + new uint[]{4073, 4292}, + new uint[]{3623}, + new uint[]{3623}, + new uint[]{3623}, + new uint[]{4302}, + new uint[]{4279, 4297}, + new uint[]{4281}, + new uint[]{4058, 4063}, + new uint[]{4304}, + new uint[]{4303}, + new uint[]{4072}, + new uint[]{4086}, + new uint[]{4295}, + new uint[]{4298}, + new uint[]{4299}, + new uint[]{4236}, + new uint[]{4237}, + new uint[]{4238}, + new uint[]{4239}, + new uint[]{4324}, + new uint[]{4325}, + new uint[]{4326}, + new uint[]{4327}, + new uint[]{4328}, + new uint[]{4329}, + new uint[]{4330}, + new uint[]{4331}, + new uint[]{1808}, + new uint[]{4233}, + new uint[]{4234}, + new uint[]{3508}, + new uint[]{3558}, + new uint[]{3751, 3758}, + new uint[]{3469}, + new uint[]{4350}, + new uint[]{4351}, + new uint[]{4352}, + new uint[]{4353}, + new uint[]{4354}, + new uint[]{4355}, + new uint[]{4356}, + new uint[]{4357}, + new uint[]{4358}, + new uint[]{4359}, + new uint[]{4360}, + new uint[]{4361}, + new uint[]{4362}, + new uint[]{4363}, + new uint[]{4364}, + new uint[]{4365}, + new uint[]{4366}, + new uint[]{4367}, + new uint[]{4368}, + new uint[]{4369}, + new uint[]{4370}, + new uint[]{4371}, + new uint[]{4372}, + new uint[]{4373}, + new uint[]{4374}, + new uint[]{4375}, + new uint[]{4376}, + new uint[]{4377}, + new uint[]{4378}, + new uint[]{4380}, + new uint[]{729}, + new uint[]{4231}, + new uint[]{1893}, + new uint[]{1895}, + new uint[]{4232}, + new uint[]{2145}, + new uint[]{4099}, + new uint[]{1416}, + new uint[]{4226}, + new uint[]{4227}, + new uint[]{2204}, + new uint[]{4235}, + new uint[]{4236}, + new uint[]{4237}, + new uint[]{108}, + new uint[]{4316}, + new uint[]{4317}, + new uint[]{4318}, + new uint[]{4319}, + Array.Empty(), + new uint[]{3164}, + new uint[]{4241}, + new uint[]{4243}, + new uint[]{3171}, + new uint[]{3170}, + new uint[]{4493}, + new uint[]{3860}, + new uint[]{3861}, + new uint[]{4247}, + new uint[]{4247}, + new uint[]{4248}, + new uint[]{3863}, + new uint[]{4254}, + new uint[]{3860}, + new uint[]{3861}, + new uint[]{4253}, + new uint[]{4253}, + new uint[]{4253}, + new uint[]{3858}, + new uint[]{4255}, + new uint[]{3860}, + new uint[]{3861}, + new uint[]{3870}, + new uint[]{4249}, + new uint[]{4250}, + new uint[]{4251, 4421}, + new uint[]{794}, + new uint[]{1849}, + new uint[]{108}, + new uint[]{4491}, + new uint[]{4250}, + new uint[]{4251}, + new uint[]{3660}, + new uint[]{3660}, + new uint[]{4251}, + Array.Empty(), + Array.Empty(), + new uint[]{4029}, + new uint[]{4093}, + new uint[]{4090}, + new uint[]{4040, 4335}, + new uint[]{4041}, + new uint[]{1240, 1893, 3186, 4091, 4092, 4116, 4123}, + new uint[]{4038}, + new uint[]{4056}, + Array.Empty(), + new uint[]{4029}, + new uint[]{4036}, + new uint[]{4037}, + new uint[]{4039}, + new uint[]{4095}, + new uint[]{108}, + new uint[]{3725}, + new uint[]{3553}, + new uint[]{3628, 3669}, + Array.Empty(), + new uint[]{3164}, + new uint[]{4251}, + new uint[]{4242}, + new uint[]{4243}, + new uint[]{4244}, + new uint[]{4091}, + new uint[]{4245}, + new uint[]{4246}, + new uint[]{3171}, + new uint[]{3170}, + new uint[]{4256}, + new uint[]{4257}, + new uint[]{2077}, + new uint[]{2080}, + new uint[]{2076}, + new uint[]{4258}, + new uint[]{4259}, + new uint[]{4228}, + new uint[]{2204}, + new uint[]{4229}, + new uint[]{4230}, + new uint[]{1416}, + new uint[]{4226}, + new uint[]{4227}, + new uint[]{4379}, + new uint[]{4193}, + new uint[]{4204}, + new uint[]{4209}, + new uint[]{4210}, + new uint[]{4211}, + new uint[]{4212}, + new uint[]{4212}, + new uint[]{3888}, + new uint[]{1990}, + new uint[]{4203}, + new uint[]{3560, 3618, 3697}, + new uint[]{3518, 3546, 4018}, + new uint[]{3519, 3547}, + new uint[]{3520, 3548}, + new uint[]{3521, 3549}, + new uint[]{3522, 3550}, + new uint[]{4228}, + new uint[]{4312}, + new uint[]{4118, 4582}, + new uint[]{4081}, + new uint[]{4394}, + new uint[]{4395}, + new uint[]{4075}, + new uint[]{4077}, + new uint[]{4077}, + new uint[]{4080}, + new uint[]{4074}, + new uint[]{4311}, + new uint[]{4096}, + new uint[]{4098}, + new uint[]{4099}, + new uint[]{4081}, + new uint[]{4082}, + new uint[]{4083}, + new uint[]{4084}, + new uint[]{4119}, + new uint[]{4120}, + new uint[]{4121}, + new uint[]{787}, + new uint[]{2086}, + new uint[]{4218}, + new uint[]{4214}, + new uint[]{4213}, + new uint[]{4216}, + new uint[]{4219}, + new uint[]{4215}, + new uint[]{4217}, + new uint[]{3860}, + new uint[]{3861}, + new uint[]{3870}, + new uint[]{3871}, + new uint[]{3872}, + new uint[]{3873}, + new uint[]{3874}, + new uint[]{3875}, + new uint[]{3876}, + new uint[]{3877}, + new uint[]{3878}, + new uint[]{3879}, + new uint[]{3880}, + new uint[]{1724}, + new uint[]{4260}, + new uint[]{4261}, + new uint[]{4262}, + new uint[]{4263, 4264}, + new uint[]{4259}, + new uint[]{108}, + new uint[]{4265}, + new uint[]{4266}, + new uint[]{4267}, + new uint[]{4259}, + new uint[]{4322}, + new uint[]{3318}, + new uint[]{3317}, + new uint[]{4323}, + new uint[]{4321}, + new uint[]{108}, + new uint[]{4322}, + new uint[]{4322}, + new uint[]{4322}, + new uint[]{4322}, + new uint[]{4333}, + new uint[]{4333}, + new uint[]{3319}, + new uint[]{3319}, + new uint[]{3317}, + new uint[]{3317}, + new uint[]{4323}, + new uint[]{4332}, + new uint[]{4331}, + new uint[]{4321}, + new uint[]{3888}, + new uint[]{3773}, + new uint[]{3958, 3959, 4407}, + new uint[]{3960}, + Array.Empty(), + Array.Empty(), + new uint[]{3957}, + new uint[]{3956}, + new uint[]{3955}, + new uint[]{4412}, + new uint[]{3949, 3954}, + new uint[]{3953}, + Array.Empty(), + new uint[]{4417}, + new uint[]{3950}, + new uint[]{3948}, + new uint[]{3951, 3952}, + new uint[]{3861}, + new uint[]{3860}, + new uint[]{3864}, + new uint[]{4425}, + new uint[]{1400}, + new uint[]{3865}, + new uint[]{3129}, + new uint[]{3866}, + new uint[]{3867}, + new uint[]{3868}, + new uint[]{3869}, + new uint[]{4313}, + new uint[]{4313}, + new uint[]{4300}, + new uint[]{5533}, + new uint[]{4314}, + new uint[]{4055}, + new uint[]{4066}, + new uint[]{4065}, + new uint[]{4051}, + new uint[]{4315}, + new uint[]{4124}, + new uint[]{1402}, + new uint[]{3887}, + new uint[]{3892}, + new uint[]{3888, 3889, 3890}, + new uint[]{3891}, + new uint[]{3965}, + new uint[]{3964}, + new uint[]{3983}, + new uint[]{3962}, + new uint[]{3946}, + new uint[]{3783}, + Array.Empty(), + Array.Empty(), + new uint[]{114}, + new uint[]{3474}, + new uint[]{3471, 3684}, + new uint[]{3475, 3488}, + new uint[]{3478, 3685}, + new uint[]{3470}, + new uint[]{3476}, + new uint[]{3477, 3680}, + new uint[]{3472}, + new uint[]{3473, 3682}, + new uint[]{3499, 4406}, + new uint[]{3495, 4011}, + new uint[]{3494}, + new uint[]{3497}, + new uint[]{3496}, + new uint[]{3498}, + new uint[]{56}, + Array.Empty(), + new uint[]{3937, 4012, 4416}, + new uint[]{3944}, + new uint[]{3943}, + Array.Empty(), + new uint[]{3942}, + new uint[]{3941}, + new uint[]{3940}, + new uint[]{3939}, + Array.Empty(), + new uint[]{3938}, + new uint[]{3936}, + new uint[]{3935}, + new uint[]{3934}, + new uint[]{1640}, + new uint[]{3881}, + new uint[]{3882}, + new uint[]{3883}, + new uint[]{3884}, + new uint[]{3885}, + new uint[]{3886}, + new uint[]{4192}, + new uint[]{3477}, + new uint[]{3474}, + new uint[]{3952}, + new uint[]{3659}, + Array.Empty(), + new uint[]{3781}, + new uint[]{3410}, + new uint[]{3410}, + Array.Empty(), + Array.Empty(), + new uint[]{3982}, + new uint[]{3981}, + new uint[]{3980}, + new uint[]{3984}, + new uint[]{3984}, + new uint[]{3979}, + new uint[]{3978}, + new uint[]{3975}, + new uint[]{3974}, + Array.Empty(), + new uint[]{3973}, + new uint[]{3972}, + new uint[]{3971}, + new uint[]{3970}, + new uint[]{3969}, + new uint[]{3968}, + new uint[]{3967}, + new uint[]{108}, + new uint[]{4419}, + new uint[]{3968}, + new uint[]{538}, + new uint[]{4009}, + new uint[]{4008}, + new uint[]{4006}, + Array.Empty(), + new uint[]{4002, 4004}, + new uint[]{4003}, + new uint[]{4001, 4418}, + new uint[]{4000}, + new uint[]{3999}, + new uint[]{3998}, + new uint[]{3997}, + new uint[]{4405}, + new uint[]{4404}, + new uint[]{4403}, + new uint[]{3996}, + new uint[]{3564}, + new uint[]{3993}, + new uint[]{3992}, + new uint[]{3990}, + new uint[]{3988}, + new uint[]{3988}, + new uint[]{3987}, + new uint[]{3986}, + new uint[]{3985}, + new uint[]{1640}, + new uint[]{3881}, + new uint[]{3882}, + new uint[]{1402}, + new uint[]{1403}, + new uint[]{1404}, + new uint[]{1402}, + new uint[]{1403}, + new uint[]{1404}, + new uint[]{3882}, + new uint[]{3789}, + new uint[]{3780}, + new uint[]{108}, + new uint[]{3780}, + new uint[]{3765}, + new uint[]{3766}, + new uint[]{3770}, + new uint[]{3769}, + new uint[]{3768}, + new uint[]{3765}, + new uint[]{108}, + new uint[]{108}, + new uint[]{3660}, + new uint[]{3660}, + new uint[]{3753}, + new uint[]{3752}, + new uint[]{3758}, + new uint[]{115}, + Array.Empty(), + Array.Empty(), + new uint[]{3716}, + new uint[]{3717}, + new uint[]{3718}, + new uint[]{3719}, + new uint[]{3720}, + new uint[]{3679}, + new uint[]{3679}, + new uint[]{3686}, + new uint[]{3686}, + new uint[]{3689}, + new uint[]{3687}, + new uint[]{3692}, + new uint[]{3692}, + new uint[]{3701}, + new uint[]{3709}, + new uint[]{2838}, + new uint[]{3773}, + new uint[]{3853}, + new uint[]{3853}, + new uint[]{4341}, + new uint[]{4398}, + new uint[]{4428}, + new uint[]{4429}, + new uint[]{4430}, + new uint[]{4431}, + new uint[]{4432}, + new uint[]{4433}, + new uint[]{4434}, + new uint[]{4435}, + new uint[]{4436}, + new uint[]{4437}, + new uint[]{4438}, + new uint[]{4439}, + new uint[]{4440}, + new uint[]{4441}, + new uint[]{4442}, + new uint[]{4443}, + new uint[]{4444}, + new uint[]{4445}, + new uint[]{4446}, + new uint[]{4447}, + new uint[]{4448}, + new uint[]{4449}, + new uint[]{4450}, + new uint[]{4451}, + new uint[]{4452}, + new uint[]{4453}, + new uint[]{4454}, + new uint[]{4455}, + new uint[]{4456}, + new uint[]{4457}, + new uint[]{4458}, + new uint[]{4459}, + new uint[]{4460}, + new uint[]{4461}, + new uint[]{4462}, + new uint[]{4463}, + new uint[]{4464}, + new uint[]{4465}, + new uint[]{4466}, + new uint[]{4467}, + new uint[]{4468}, + new uint[]{4469}, + new uint[]{4470}, + new uint[]{4471}, + new uint[]{4472}, + new uint[]{4473}, + new uint[]{4474}, + new uint[]{4475}, + new uint[]{4476}, + new uint[]{4477}, + new uint[]{4478}, + new uint[]{4479}, + new uint[]{4480}, + new uint[]{4481}, + new uint[]{4482}, + new uint[]{4483}, + new uint[]{4484}, + new uint[]{4485}, + new uint[]{4486}, + new uint[]{4487}, + new uint[]{4488}, + new uint[]{4897}, + new uint[]{5046}, + new uint[]{5047}, + new uint[]{4026}, + new uint[]{4025}, + new uint[]{4024}, + new uint[]{4023}, + new uint[]{4022}, + new uint[]{4021}, + new uint[]{4020}, + new uint[]{3545, 4019}, + new uint[]{4017}, + new uint[]{4016}, + new uint[]{4015}, + new uint[]{4014}, + new uint[]{4013}, + new uint[]{4243}, + new uint[]{4243}, + new uint[]{4243}, + new uint[]{4243}, + new uint[]{4010}, + new uint[]{4397}, + new uint[]{114}, + new uint[]{795}, + new uint[]{3410}, + new uint[]{3410}, + new uint[]{3410}, + new uint[]{4239}, + new uint[]{108}, + new uint[]{108, 3789}, + new uint[]{731}, + new uint[]{3790}, + new uint[]{3180, 4034}, + new uint[]{3181}, + new uint[]{4115}, + new uint[]{1239, 4091, 4092, 4305, 4309, 4310}, + new uint[]{1895, 4422}, + new uint[]{3187, 4123}, + new uint[]{4123, 4310}, + new uint[]{1239, 4067, 4850}, + new uint[]{4307, 4422}, + new uint[]{1240, 1893, 4306}, + new uint[]{4079, 4088, 4097, 4308}, + Array.Empty(), + new uint[]{4067, 4851}, + new uint[]{4282}, + new uint[]{4027}, + new uint[]{4028}, + new uint[]{3738}, + new uint[]{3948, 4415}, + Array.Empty(), + new uint[]{3444}, + new uint[]{3654, 3655}, + new uint[]{4414}, + new uint[]{4413}, + Array.Empty(), + new uint[]{3784}, + Array.Empty(), + new uint[]{4411}, + new uint[]{4007}, + Array.Empty(), + Array.Empty(), + new uint[]{3676}, + new uint[]{3677}, + new uint[]{3678}, + new uint[]{3681}, + new uint[]{3683}, + new uint[]{3660}, + new uint[]{3660}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2901}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3983}, + new uint[]{116}, + new uint[]{113}, + new uint[]{114}, + new uint[]{115, 3961}, + new uint[]{3480, 3491, 4409}, + new uint[]{4624}, + new uint[]{4624}, + new uint[]{4623}, + new uint[]{4625}, + new uint[]{4631}, + new uint[]{4632}, + new uint[]{4633}, + new uint[]{4635}, + new uint[]{4613}, + new uint[]{4609}, + new uint[]{4610}, + new uint[]{4612}, + new uint[]{4613}, + Array.Empty(), + new uint[]{4607}, + new uint[]{4608}, + new uint[]{4609}, + new uint[]{4611}, + new uint[]{108}, + new uint[]{4551}, + new uint[]{4552}, + new uint[]{4553}, + new uint[]{4554}, + new uint[]{4568}, + new uint[]{4555}, + new uint[]{4556}, + new uint[]{4557}, + new uint[]{4558}, + new uint[]{4559}, + new uint[]{4560}, + new uint[]{4561}, + new uint[]{4562}, + new uint[]{4563}, + new uint[]{4564}, + new uint[]{4565}, + new uint[]{4566}, + new uint[]{4626}, + new uint[]{4627}, + new uint[]{4630}, + new uint[]{4628}, + new uint[]{4629}, + new uint[]{4626}, + new uint[]{2564}, + new uint[]{4622}, + new uint[]{4620}, + new uint[]{4621}, + Array.Empty(), + new uint[]{4636}, + new uint[]{4637}, + new uint[]{4638}, + new uint[]{4639}, + new uint[]{4640}, + new uint[]{4641}, + new uint[]{4642}, + new uint[]{4643}, + new uint[]{4644}, + new uint[]{4645}, + new uint[]{4646}, + new uint[]{4646}, + new uint[]{4647}, + new uint[]{4648}, + new uint[]{4649}, + new uint[]{4650}, + new uint[]{4651}, + new uint[]{4653}, + new uint[]{4654}, + new uint[]{4655}, + new uint[]{4656}, + new uint[]{4657}, + new uint[]{4658}, + new uint[]{4659}, + new uint[]{4660}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4568}, + new uint[]{4568}, + new uint[]{4567}, + new uint[]{4568}, + new uint[]{4568}, + new uint[]{4569}, + new uint[]{4570}, + new uint[]{4571}, + Array.Empty(), + new uint[]{4572}, + new uint[]{4573}, + new uint[]{4574}, + new uint[]{4570}, + new uint[]{4575}, + new uint[]{4576}, + new uint[]{4577}, + new uint[]{4578}, + new uint[]{4579}, + new uint[]{4580}, + new uint[]{108}, + new uint[]{4614}, + new uint[]{4616}, + new uint[]{4619}, + new uint[]{4617}, + new uint[]{4618}, + new uint[]{3632}, + new uint[]{3633}, + new uint[]{3634}, + new uint[]{3635}, + new uint[]{3636}, + new uint[]{3637}, + new uint[]{3638}, + new uint[]{3639}, + new uint[]{3640}, + new uint[]{3641}, + new uint[]{3642}, + new uint[]{3643}, + new uint[]{3644}, + new uint[]{3645}, + new uint[]{4386}, + new uint[]{4387}, + new uint[]{3632, 3639, 3640, 3641, 3642, 3644, 3645}, + new uint[]{4634}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4652}, + new uint[]{108, 6291, 6292, 6431, 6432, 6439, 6442, 6444, 6448, 6451, 6476, 6494, 6504, 6508, 6510, 6512, 6526, 6527, 6547, 8479, 9401, 10808, 10809, 10829}, + new uint[]{4334, 4583}, + new uint[]{4335, 4584}, + new uint[]{4585}, + new uint[]{4587}, + new uint[]{4586}, + new uint[]{4603}, + new uint[]{4588}, + new uint[]{4589}, + new uint[]{4590}, + new uint[]{4591}, + new uint[]{4592, 4682}, + new uint[]{4593}, + new uint[]{4594}, + new uint[]{4562}, + new uint[]{4581}, + new uint[]{4563}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4666}, + new uint[]{4671}, + new uint[]{4672}, + new uint[]{4673}, + new uint[]{4597}, + new uint[]{3841}, + new uint[]{3841}, + new uint[]{4401}, + new uint[]{3843}, + new uint[]{4401}, + new uint[]{3842}, + new uint[]{3842}, + new uint[]{3843}, + new uint[]{3843}, + new uint[]{4598}, + new uint[]{4599}, + new uint[]{4130}, + new uint[]{729}, + new uint[]{713}, + new uint[]{4595}, + new uint[]{4596}, + new uint[]{4670}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4600}, + new uint[]{4602}, + new uint[]{4602}, + new uint[]{4600}, + new uint[]{4601}, + new uint[]{4600}, + Array.Empty(), + new uint[]{4653}, + new uint[]{4654}, + new uint[]{3842}, + new uint[]{108}, + new uint[]{4578}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4744}, + new uint[]{4839}, + new uint[]{4745}, + new uint[]{4746}, + new uint[]{4747}, + new uint[]{4748}, + new uint[]{4749}, + new uint[]{4750}, + new uint[]{4751}, + new uint[]{4752}, + new uint[]{4753}, + new uint[]{4754}, + new uint[]{4755}, + new uint[]{4756}, + new uint[]{4757}, + new uint[]{4758}, + new uint[]{4759}, + new uint[]{4760}, + new uint[]{4761}, + new uint[]{4776}, + new uint[]{4776}, + new uint[]{4777}, + new uint[]{4778}, + new uint[]{4779}, + new uint[]{4780}, + new uint[]{4776}, + new uint[]{4687}, + new uint[]{3646}, + new uint[]{4791}, + new uint[]{4791}, + new uint[]{4688}, + Array.Empty(), + new uint[]{4690}, + new uint[]{4762}, + new uint[]{4794}, + new uint[]{4798}, + new uint[]{4799}, + new uint[]{4800}, + new uint[]{4795}, + new uint[]{4801}, + new uint[]{736}, + new uint[]{4802}, + new uint[]{4803}, + new uint[]{4804}, + new uint[]{4796}, + new uint[]{4805}, + new uint[]{4806}, + new uint[]{4807}, + new uint[]{4808}, + new uint[]{4810}, + new uint[]{4809}, + new uint[]{4813}, + new uint[]{4811}, + new uint[]{4812}, + new uint[]{3376}, + new uint[]{4692}, + new uint[]{4775}, + new uint[]{4694}, + new uint[]{3749}, + new uint[]{3774}, + new uint[]{3746}, + new uint[]{4698}, + new uint[]{4695}, + new uint[]{3745}, + new uint[]{4709}, + new uint[]{4838}, + new uint[]{4699}, + new uint[]{4700}, + new uint[]{4700}, + new uint[]{4700}, + new uint[]{4773}, + new uint[]{4703}, + new uint[]{4704}, + new uint[]{4818}, + new uint[]{4817}, + new uint[]{4816}, + new uint[]{4705}, + new uint[]{4769}, + new uint[]{4770}, + new uint[]{4772}, + new uint[]{4691}, + new uint[]{4771}, + new uint[]{4706}, + new uint[]{4768}, + new uint[]{4820}, + new uint[]{4699, 4703, 4704, 4705, 4706, 4764}, + new uint[]{4707}, + new uint[]{3778}, + new uint[]{3776}, + new uint[]{3777}, + new uint[]{4699}, + new uint[]{4700}, + new uint[]{4706}, + new uint[]{4705}, + new uint[]{4703}, + new uint[]{4704}, + new uint[]{4817}, + new uint[]{4816}, + new uint[]{4708}, + Array.Empty(), + Array.Empty(), + new uint[]{4773}, + new uint[]{4819}, + new uint[]{4703, 4705, 4706, 4707, 4708, 4764}, + new uint[]{4813}, + new uint[]{4730}, + new uint[]{4729}, + new uint[]{4733}, + new uint[]{4734}, + new uint[]{4732}, + new uint[]{4732}, + new uint[]{4729}, + new uint[]{4733}, + new uint[]{4731}, + new uint[]{4728}, + new uint[]{4727}, + new uint[]{4725, 4726}, + new uint[]{4725}, + new uint[]{4728}, + new uint[]{4725}, + new uint[]{108}, + new uint[]{108}, + new uint[]{4735}, + new uint[]{4736}, + new uint[]{4737}, + new uint[]{4738}, + new uint[]{4739}, + new uint[]{4739}, + new uint[]{4740}, + new uint[]{4741}, + new uint[]{4742}, + new uint[]{4743}, + new uint[]{541}, + new uint[]{4765}, + new uint[]{4766}, + new uint[]{4767}, + new uint[]{4784}, + new uint[]{4785}, + new uint[]{4957}, + new uint[]{4956}, + new uint[]{4955}, + new uint[]{4952}, + new uint[]{4953}, + new uint[]{5043}, + new uint[]{4815}, + new uint[]{4896}, + new uint[]{4954}, + new uint[]{2667}, + new uint[]{4782}, + new uint[]{541}, + new uint[]{4784}, + new uint[]{4782}, + new uint[]{4814}, + new uint[]{4784}, + new uint[]{4785}, + new uint[]{4786}, + new uint[]{4784}, + new uint[]{4786}, + new uint[]{4782}, + new uint[]{4784}, + new uint[]{4781}, + new uint[]{4782}, + new uint[]{4784}, + new uint[]{4785}, + new uint[]{4786}, + new uint[]{4781}, + new uint[]{4782}, + new uint[]{4784}, + new uint[]{4785}, + new uint[]{4781}, + new uint[]{4782}, + new uint[]{4784}, + Array.Empty(), + new uint[]{4786}, + new uint[]{4781}, + new uint[]{4782}, + new uint[]{4784}, + Array.Empty(), + new uint[]{4814}, + new uint[]{4781}, + new uint[]{4782}, + new uint[]{4784}, + new uint[]{4781}, + new uint[]{4784}, + new uint[]{4786}, + new uint[]{4781}, + new uint[]{4783}, + new uint[]{4784}, + new uint[]{4785}, + new uint[]{4781}, + new uint[]{4783}, + new uint[]{4781}, + new uint[]{4783}, + new uint[]{4782}, + new uint[]{4787}, + new uint[]{4788}, + new uint[]{4789}, + Array.Empty(), + new uint[]{2667}, + new uint[]{4687}, + new uint[]{3646}, + new uint[]{4688}, + Array.Empty(), + new uint[]{4690}, + new uint[]{4797}, + new uint[]{4701}, + new uint[]{4702}, + new uint[]{4697}, + Array.Empty(), + new uint[]{4710}, + new uint[]{4711}, + new uint[]{4712}, + new uint[]{4712}, + new uint[]{4713}, + new uint[]{4714}, + new uint[]{4715}, + new uint[]{4716}, + new uint[]{4717}, + new uint[]{4718}, + new uint[]{4719}, + new uint[]{4721}, + new uint[]{4720}, + new uint[]{4724}, + new uint[]{4713}, + new uint[]{4723}, + new uint[]{4722}, + new uint[]{3745}, + new uint[]{4709}, + new uint[]{4776}, + new uint[]{4776}, + new uint[]{4777}, + new uint[]{4778}, + new uint[]{4779}, + new uint[]{4780}, + new uint[]{4776}, + new uint[]{4782}, + Array.Empty(), + new uint[]{3376}, + new uint[]{4692}, + new uint[]{4775}, + new uint[]{4694}, + new uint[]{3749}, + new uint[]{3774}, + new uint[]{3746}, + new uint[]{4698}, + new uint[]{4695}, + new uint[]{2667}, + new uint[]{4691}, + new uint[]{4693}, + new uint[]{3758}, + new uint[]{4772}, + new uint[]{4774}, + new uint[]{4699}, + new uint[]{4700}, + new uint[]{4773}, + new uint[]{4703}, + new uint[]{4705}, + new uint[]{4706}, + new uint[]{4768}, + new uint[]{4820}, + new uint[]{4699, 4703, 4705, 4706, 4764}, + new uint[]{4707}, + new uint[]{3778}, + new uint[]{3776}, + new uint[]{3777}, + new uint[]{4699}, + new uint[]{4700}, + new uint[]{4706}, + new uint[]{4705}, + new uint[]{4703}, + new uint[]{4708}, + new uint[]{4773}, + new uint[]{4705, 4706, 4707, 4708, 4764}, + new uint[]{4811}, + new uint[]{4812}, + new uint[]{4811}, + new uint[]{4812}, + new uint[]{4787}, + new uint[]{4788}, + new uint[]{4821}, + new uint[]{4822, 4823, 4824, 4825}, + new uint[]{4826, 4827, 4828, 4829, 4830, 4831, 4832, 4833, 4834, 4835, 4836, 4837, 4840, 4841, 4842, 4843, 4844, 4845}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3458}, + new uint[]{3458}, + Array.Empty(), + new uint[]{4963}, + new uint[]{4962}, + new uint[]{3460}, + new uint[]{3459}, + new uint[]{4960}, + new uint[]{4961}, + new uint[]{4959}, + new uint[]{3458, 3459, 4962, 4963}, + new uint[]{4943}, + new uint[]{4944}, + new uint[]{4945}, + new uint[]{4946}, + new uint[]{4947}, + new uint[]{4948}, + new uint[]{4949}, + new uint[]{4950}, + new uint[]{4951}, + new uint[]{4928}, + new uint[]{4929}, + new uint[]{4932}, + new uint[]{4933}, + new uint[]{4931}, + new uint[]{4930}, + new uint[]{4936}, + new uint[]{4935}, + new uint[]{4937}, + new uint[]{4934}, + new uint[]{4939}, + new uint[]{4938}, + new uint[]{4940}, + new uint[]{4942}, + new uint[]{4954}, + new uint[]{4941}, + new uint[]{4855}, + new uint[]{4856}, + new uint[]{4857}, + new uint[]{4858}, + new uint[]{4859}, + new uint[]{4860}, + new uint[]{4861}, + new uint[]{4854}, + new uint[]{4853}, + new uint[]{4852}, + new uint[]{3818}, + new uint[]{3819}, + new uint[]{3820}, + new uint[]{4489}, + new uint[]{5045}, + new uint[]{1492}, + new uint[]{729}, + new uint[]{4846}, + new uint[]{4847}, + new uint[]{4878}, + new uint[]{4879}, + new uint[]{4880}, + new uint[]{4881}, + new uint[]{4882}, + Array.Empty(), + new uint[]{4884}, + new uint[]{4885}, + new uint[]{4886}, + new uint[]{4887}, + new uint[]{4878}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{4942}, + new uint[]{4871}, + new uint[]{4848}, + new uint[]{4907}, + new uint[]{5589}, + new uint[]{4908}, + Array.Empty(), + Array.Empty(), + new uint[]{4909}, + new uint[]{5048}, + new uint[]{5049}, + new uint[]{4910}, + new uint[]{4849}, + new uint[]{4911}, + new uint[]{4911}, + new uint[]{4912}, + new uint[]{4913}, + new uint[]{4914}, + new uint[]{4915}, + new uint[]{4916}, + new uint[]{4888}, + new uint[]{4889}, + new uint[]{4890}, + new uint[]{4891}, + new uint[]{4892}, + new uint[]{4893}, + new uint[]{4894}, + new uint[]{4958}, + new uint[]{2095}, + new uint[]{4895}, + new uint[]{5050}, + new uint[]{5051}, + new uint[]{108, 4965, 5055}, + new uint[]{5052}, + new uint[]{5053}, + new uint[]{4966}, + new uint[]{4967}, + new uint[]{4968}, + new uint[]{4969}, + new uint[]{4970}, + new uint[]{4971}, + new uint[]{4972}, + new uint[]{5044}, + new uint[]{4897}, + new uint[]{4898}, + new uint[]{4899}, + new uint[]{4900}, + new uint[]{4901}, + new uint[]{4902}, + new uint[]{4903}, + new uint[]{4904}, + new uint[]{4862}, + new uint[]{4863}, + new uint[]{4864}, + new uint[]{4865}, + new uint[]{4866}, + new uint[]{4867}, + new uint[]{4868}, + new uint[]{4869}, + new uint[]{4870}, + new uint[]{4872}, + new uint[]{4873}, + new uint[]{4874}, + new uint[]{4875}, + new uint[]{4877}, + new uint[]{4871}, + new uint[]{5056}, + new uint[]{4911}, + new uint[]{5057}, + new uint[]{4876}, + new uint[]{4973}, + new uint[]{4974}, + new uint[]{4905}, + new uint[]{4905}, + new uint[]{4905}, + new uint[]{4906}, + new uint[]{4901}, + new uint[]{108}, + new uint[]{5054}, + Array.Empty(), + new uint[]{5479}, + new uint[]{4975}, + new uint[]{4976}, + new uint[]{4977}, + new uint[]{4978}, + new uint[]{4979}, + new uint[]{4980}, + new uint[]{4981}, + new uint[]{4982}, + new uint[]{4983}, + new uint[]{4984}, + new uint[]{4985}, + new uint[]{4986}, + new uint[]{4987}, + new uint[]{4988}, + new uint[]{4989}, + new uint[]{4990}, + new uint[]{4991}, + new uint[]{4992}, + new uint[]{4993}, + new uint[]{4994}, + new uint[]{4995}, + new uint[]{4996}, + new uint[]{4997}, + new uint[]{4998}, + new uint[]{4999}, + new uint[]{5000}, + new uint[]{5001}, + new uint[]{5002}, + new uint[]{5003}, + new uint[]{5004}, + new uint[]{5005}, + new uint[]{5006}, + new uint[]{5007}, + new uint[]{5008}, + new uint[]{5009}, + new uint[]{5010}, + new uint[]{5011}, + new uint[]{5012}, + new uint[]{5013}, + new uint[]{5014}, + new uint[]{5015}, + new uint[]{5016}, + new uint[]{5017}, + new uint[]{5018}, + new uint[]{5019}, + new uint[]{5020}, + new uint[]{5021}, + new uint[]{5022}, + new uint[]{5023}, + new uint[]{5024}, + new uint[]{5025}, + new uint[]{5026}, + new uint[]{5027}, + new uint[]{5028}, + new uint[]{5029}, + new uint[]{5030}, + new uint[]{5031}, + new uint[]{5032}, + new uint[]{5033}, + new uint[]{5034}, + new uint[]{5035}, + new uint[]{5036}, + new uint[]{5037}, + new uint[]{5038}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + Array.Empty(), + new uint[]{5041}, + new uint[]{5041}, + new uint[]{5041}, + new uint[]{5041}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3458}, + new uint[]{3458}, + Array.Empty(), + Array.Empty(), + new uint[]{3459}, + new uint[]{4960}, + new uint[]{4961}, + new uint[]{4959}, + new uint[]{3458, 3459}, + new uint[]{5039}, + new uint[]{5040}, + new uint[]{5058}, + new uint[]{5059}, + new uint[]{5060}, + new uint[]{5060}, + new uint[]{5060}, + new uint[]{5060}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5062}, + new uint[]{5063}, + new uint[]{5064}, + new uint[]{5065}, + new uint[]{5066}, + new uint[]{5066}, + new uint[]{5066}, + new uint[]{5066}, + new uint[]{5067}, + new uint[]{5068}, + new uint[]{5069}, + new uint[]{5070}, + Array.Empty(), + new uint[]{5072}, + new uint[]{5073}, + new uint[]{5074}, + new uint[]{5075}, + new uint[]{5076}, + new uint[]{5077}, + new uint[]{5078}, + new uint[]{5079}, + new uint[]{5080}, + new uint[]{5081}, + new uint[]{5082}, + new uint[]{5083}, + new uint[]{5084}, + new uint[]{5085}, + new uint[]{5086}, + new uint[]{5087}, + new uint[]{5088}, + new uint[]{5089}, + new uint[]{5090}, + new uint[]{5091}, + new uint[]{5092}, + new uint[]{5093}, + new uint[]{5094}, + new uint[]{5095}, + new uint[]{5096}, + new uint[]{5097}, + new uint[]{5098}, + new uint[]{5099}, + new uint[]{5100}, + new uint[]{5101}, + new uint[]{5102}, + new uint[]{5103}, + new uint[]{5104}, + new uint[]{5105}, + new uint[]{5106}, + new uint[]{5107}, + new uint[]{5108}, + new uint[]{5109}, + new uint[]{5110}, + new uint[]{5111}, + new uint[]{5112}, + new uint[]{5113}, + new uint[]{5114}, + new uint[]{5115}, + new uint[]{5116}, + new uint[]{5117}, + new uint[]{5118}, + new uint[]{5119}, + new uint[]{5120}, + new uint[]{5121}, + new uint[]{5122}, + new uint[]{5123}, + new uint[]{5124}, + new uint[]{5125}, + new uint[]{5126}, + new uint[]{5127}, + new uint[]{5128}, + new uint[]{5129}, + new uint[]{5130}, + new uint[]{5131}, + new uint[]{5132}, + new uint[]{5133}, + new uint[]{5134}, + new uint[]{5135}, + new uint[]{5136}, + new uint[]{5137}, + new uint[]{5138}, + new uint[]{5139}, + new uint[]{5140}, + new uint[]{5141}, + new uint[]{5142}, + new uint[]{5143}, + new uint[]{5144}, + new uint[]{5145}, + new uint[]{5146}, + new uint[]{5147}, + new uint[]{5148}, + new uint[]{5149}, + new uint[]{5150}, + new uint[]{5151}, + new uint[]{5152}, + new uint[]{5153}, + new uint[]{5154}, + new uint[]{5155}, + new uint[]{5156}, + new uint[]{5157}, + new uint[]{5158}, + new uint[]{5159}, + new uint[]{5160}, + new uint[]{5161}, + new uint[]{5162}, + new uint[]{5163}, + new uint[]{5164}, + new uint[]{5165}, + new uint[]{5166}, + new uint[]{5167}, + new uint[]{4981}, + new uint[]{5030}, + Array.Empty(), + new uint[]{108}, + new uint[]{5216}, + new uint[]{5278}, + new uint[]{5279}, + new uint[]{5280}, + new uint[]{5218}, + Array.Empty(), + Array.Empty(), + new uint[]{5219}, + new uint[]{5220}, + new uint[]{4895}, + new uint[]{5221}, + new uint[]{5222}, + new uint[]{5223}, + new uint[]{5224}, + new uint[]{5225}, + new uint[]{5226}, + new uint[]{5227}, + new uint[]{5228}, + new uint[]{5229}, + new uint[]{5230}, + new uint[]{5231}, + new uint[]{5232}, + new uint[]{5233}, + new uint[]{5234}, + new uint[]{5235}, + new uint[]{5236}, + new uint[]{5237}, + new uint[]{5238}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5061}, + new uint[]{5168}, + new uint[]{5085}, + new uint[]{5085}, + new uint[]{5085}, + new uint[]{5199}, + new uint[]{5201}, + new uint[]{5200}, + new uint[]{5202}, + new uint[]{5203}, + new uint[]{5204}, + new uint[]{4914}, + new uint[]{5199}, + new uint[]{5201}, + new uint[]{5202}, + new uint[]{5203}, + new uint[]{5204}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5265}, + new uint[]{5266}, + new uint[]{5267}, + new uint[]{5268}, + new uint[]{5269}, + new uint[]{5270}, + Array.Empty(), + new uint[]{5272}, + new uint[]{5259}, + new uint[]{1644}, + new uint[]{5186}, + new uint[]{5186}, + new uint[]{5187}, + new uint[]{5188}, + new uint[]{5189}, + new uint[]{5190}, + new uint[]{5246}, + new uint[]{5247}, + new uint[]{5251}, + new uint[]{5252}, + new uint[]{5253}, + Array.Empty(), + new uint[]{5257}, + new uint[]{5255}, + new uint[]{5256}, + new uint[]{5258}, + new uint[]{5254}, + new uint[]{5260}, + new uint[]{5259}, + new uint[]{5264}, + new uint[]{5262}, + new uint[]{5261}, + new uint[]{5263}, + new uint[]{5193}, + new uint[]{5194}, + new uint[]{5195}, + new uint[]{5196}, + new uint[]{5197}, + new uint[]{5193}, + new uint[]{108}, + new uint[]{5195}, + new uint[]{5196}, + new uint[]{5197}, + new uint[]{5198}, + new uint[]{5174, 5193}, + new uint[]{4130}, + new uint[]{4392}, + new uint[]{5239}, + new uint[]{713}, + new uint[]{1492}, + new uint[]{5240}, + new uint[]{5241}, + new uint[]{5244}, + new uint[]{5242}, + new uint[]{5243}, + Array.Empty(), + new uint[]{3306}, + new uint[]{5274}, + new uint[]{3647}, + new uint[]{3648}, + Array.Empty(), + new uint[]{5169}, + new uint[]{5170}, + new uint[]{5171}, + new uint[]{5172}, + new uint[]{5173}, + new uint[]{1385}, + new uint[]{3749}, + new uint[]{5176}, + new uint[]{5277}, + new uint[]{5178}, + new uint[]{5170}, + new uint[]{5180}, + new uint[]{5181}, + new uint[]{5182}, + new uint[]{5183}, + new uint[]{5184}, + new uint[]{5185}, + new uint[]{4698, 5180, 5182, 5209}, + new uint[]{5186}, + new uint[]{5186}, + new uint[]{5187}, + new uint[]{5188}, + new uint[]{5189}, + Array.Empty(), + new uint[]{5192}, + Array.Empty(), + Array.Empty(), + new uint[]{5179}, + new uint[]{5191}, + new uint[]{541}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{5181}, + Array.Empty(), + new uint[]{5207}, + new uint[]{5208}, + new uint[]{4691}, + new uint[]{5206}, + new uint[]{5175}, + new uint[]{3468}, + new uint[]{5205}, + new uint[]{5180}, + new uint[]{5182}, + new uint[]{5185}, + Array.Empty(), + new uint[]{5184}, + new uint[]{4698, 5180, 5182, 5209}, + new uint[]{5356}, + new uint[]{5357}, + new uint[]{5358}, + new uint[]{5359}, + Array.Empty(), + Array.Empty(), + new uint[]{5193}, + new uint[]{5194}, + new uint[]{5195}, + new uint[]{5197}, + new uint[]{5194}, + new uint[]{5174, 5193}, + new uint[]{5309}, + new uint[]{5321}, + new uint[]{5333}, + new uint[]{5345}, + new uint[]{5371}, + new uint[]{5384}, + new uint[]{5397}, + new uint[]{5410}, + new uint[]{5424}, + new uint[]{5438}, + new uint[]{5449}, + new uint[]{5461}, + new uint[]{5471}, + new uint[]{4996}, + new uint[]{5299}, + new uint[]{5300}, + new uint[]{5301}, + new uint[]{5302}, + new uint[]{5303}, + new uint[]{5304}, + new uint[]{5305}, + new uint[]{5306}, + new uint[]{5307}, + new uint[]{5308}, + new uint[]{5311}, + new uint[]{5312}, + new uint[]{5313}, + new uint[]{5004}, + new uint[]{5314}, + new uint[]{5315}, + new uint[]{5316}, + new uint[]{5317}, + new uint[]{5318}, + new uint[]{5319}, + new uint[]{5320}, + new uint[]{5322}, + new uint[]{5323}, + new uint[]{5324}, + new uint[]{5325}, + new uint[]{5326}, + new uint[]{5327}, + new uint[]{5328}, + new uint[]{5329}, + new uint[]{5330}, + new uint[]{5331}, + new uint[]{5332}, + new uint[]{5334}, + new uint[]{5335}, + new uint[]{5336}, + new uint[]{5337}, + new uint[]{5338}, + new uint[]{5339}, + new uint[]{5340}, + new uint[]{5341}, + new uint[]{5342}, + new uint[]{5343}, + new uint[]{5344}, + new uint[]{5346}, + new uint[]{5347}, + new uint[]{5348}, + new uint[]{5349}, + new uint[]{5350}, + new uint[]{5351}, + new uint[]{4979}, + new uint[]{5352}, + new uint[]{5353}, + new uint[]{5354}, + new uint[]{5355}, + new uint[]{5480}, + new uint[]{5360}, + new uint[]{5361}, + new uint[]{5362}, + new uint[]{5363}, + new uint[]{5364}, + new uint[]{5365}, + new uint[]{5366}, + new uint[]{5367}, + new uint[]{5368}, + new uint[]{5369}, + new uint[]{5370}, + new uint[]{5372}, + new uint[]{5373}, + new uint[]{5374}, + new uint[]{5375}, + new uint[]{5376}, + new uint[]{5377}, + new uint[]{5378}, + new uint[]{5379}, + new uint[]{5380}, + new uint[]{5381}, + new uint[]{5382}, + new uint[]{5383}, + new uint[]{5385}, + new uint[]{5386}, + new uint[]{5387}, + new uint[]{5388}, + new uint[]{5389}, + new uint[]{5390}, + new uint[]{5391}, + new uint[]{5392}, + new uint[]{5393}, + new uint[]{5394}, + new uint[]{5395}, + new uint[]{5396}, + new uint[]{5398}, + new uint[]{5399}, + new uint[]{5400}, + new uint[]{5401}, + new uint[]{5402}, + new uint[]{5403}, + new uint[]{5404}, + new uint[]{5405}, + new uint[]{5406}, + new uint[]{5407}, + new uint[]{5408}, + new uint[]{5409}, + new uint[]{5412}, + new uint[]{5413}, + new uint[]{5414}, + new uint[]{5415}, + new uint[]{5416}, + new uint[]{5417}, + new uint[]{5418}, + new uint[]{5419}, + new uint[]{5420}, + new uint[]{5421}, + new uint[]{5422}, + new uint[]{5423}, + new uint[]{5381}, + new uint[]{5429}, + new uint[]{5430}, + new uint[]{5431}, + new uint[]{5432}, + new uint[]{5433}, + new uint[]{5406}, + new uint[]{5434}, + new uint[]{5435}, + new uint[]{5436}, + new uint[]{5437}, + new uint[]{5439}, + new uint[]{5440}, + new uint[]{5441}, + new uint[]{5389}, + new uint[]{5442}, + new uint[]{5443}, + new uint[]{5444}, + new uint[]{5445}, + new uint[]{5446}, + new uint[]{5447}, + new uint[]{5448}, + new uint[]{5450}, + new uint[]{5451}, + new uint[]{5452}, + new uint[]{5453}, + new uint[]{5454}, + new uint[]{5455}, + new uint[]{5456}, + new uint[]{5457}, + new uint[]{5458}, + new uint[]{5459}, + new uint[]{5460}, + new uint[]{5462}, + new uint[]{5463}, + new uint[]{5440}, + new uint[]{5464}, + new uint[]{5465}, + new uint[]{5466}, + new uint[]{5467}, + new uint[]{5480}, + new uint[]{5468}, + new uint[]{5469}, + new uint[]{5470}, + new uint[]{5401}, + new uint[]{5474}, + new uint[]{5420}, + new uint[]{5415}, + new uint[]{5472}, + new uint[]{5473}, + new uint[]{5364}, + new uint[]{5409}, + new uint[]{5475}, + new uint[]{5422}, + new uint[]{5423}, + new uint[]{3467}, + new uint[]{5169}, + new uint[]{5170}, + new uint[]{5171}, + new uint[]{5172}, + new uint[]{5173}, + new uint[]{5176}, + new uint[]{5277}, + new uint[]{5179}, + Array.Empty(), + new uint[]{5217}, + new uint[]{108, 510, 1482, 2210, 2564, 2618, 2632, 3210, 3305, 3308, 5629, 5631, 5633, 5640, 5641, 5642, 6037, 6038, 6039, 6052, 6070, 6071, 6072, 6087, 6088, 6089, 6090, 6104, 6115, 6117, 6118, 6119, 6120, 6133, 6153, 6177, 6216, 6237, 6241, 6243, 6263, 6266, 6267, 6268, 6274, 6275, 6307, 6308, 6309, 6311, 6321, 6333, 6338, 6345, 6347, 6352, 6385, 6795, 6907, 6908, 6910, 6922, 6924, 6925, 6929, 6941, 6950, 6970, 6994, 6995, 6996, 7055, 7056, 7057, 7058, 7070, 7092, 7093, 7096, 7097, 7101, 7102, 7103, 7104, 7107, 7117, 7118, 7126, 7127, 7131, 7132, 7133, 7135, 7179, 7180, 7181, 7202, 7203, 7206, 7221, 7223, 7225, 7237, 7244, 7245, 7400, 7640, 7689, 7863, 7958, 9225, 9650, 9958, 9990, 10315, 11072, 11143}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{5310}, + Array.Empty(), + new uint[]{4578}, + new uint[]{4580}, + new uint[]{5477}, + new uint[]{5425}, + new uint[]{5426}, + new uint[]{5427}, + new uint[]{5428}, + new uint[]{4170}, + new uint[]{5333, 5461}, + new uint[]{4579}, + new uint[]{4580}, + new uint[]{5477}, + new uint[]{5042, 5625, 7395}, + new uint[]{5292}, + new uint[]{5293}, + new uint[]{5294}, + new uint[]{5297}, + new uint[]{5290}, + new uint[]{5291}, + new uint[]{5295}, + new uint[]{5283}, + new uint[]{5296}, + new uint[]{5298}, + new uint[]{5289}, + new uint[]{5288}, + new uint[]{5286}, + new uint[]{5285}, + new uint[]{5287}, + new uint[]{5284}, + new uint[]{5366}, + new uint[]{5411}, + new uint[]{2564}, + new uint[]{5526}, + new uint[]{2564}, + new uint[]{5523}, + new uint[]{2568}, + new uint[]{5522}, + new uint[]{5524}, + new uint[]{5525}, + new uint[]{5507}, + new uint[]{5508}, + new uint[]{5507}, + new uint[]{3725}, + new uint[]{5529}, + new uint[]{5585}, + new uint[]{5586}, + new uint[]{5530}, + new uint[]{5587}, + new uint[]{5531}, + new uint[]{5532}, + new uint[]{5534}, + new uint[]{5535}, + new uint[]{5536}, + new uint[]{5537}, + new uint[]{5538}, + new uint[]{5539}, + new uint[]{3805}, + new uint[]{5540}, + new uint[]{5541}, + new uint[]{5542}, + new uint[]{5543}, + new uint[]{3797}, + new uint[]{5544}, + new uint[]{5545}, + new uint[]{5509}, + new uint[]{5510}, + new uint[]{5511}, + new uint[]{5512}, + new uint[]{5513}, + new uint[]{5514}, + new uint[]{5509}, + new uint[]{2096}, + new uint[]{5567}, + new uint[]{5567}, + new uint[]{5568}, + new uint[]{5569}, + new uint[]{5570}, + new uint[]{5571}, + new uint[]{5572}, + new uint[]{5567}, + new uint[]{5560}, + new uint[]{5561}, + new uint[]{5562}, + new uint[]{5563}, + new uint[]{5554}, + new uint[]{5564}, + new uint[]{5565}, + new uint[]{5566}, + new uint[]{5547}, + new uint[]{5548}, + Array.Empty(), + new uint[]{5550}, + new uint[]{5551}, + new uint[]{5552}, + new uint[]{5546}, + new uint[]{5553}, + new uint[]{5557}, + new uint[]{5554}, + new uint[]{5555}, + new uint[]{5556}, + new uint[]{5558}, + new uint[]{5559}, + new uint[]{3281}, + new uint[]{5515}, + new uint[]{5516}, + Array.Empty(), + new uint[]{5517}, + new uint[]{5519}, + new uint[]{5518}, + new uint[]{5521}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4686}, + new uint[]{5476}, + new uint[]{5573}, + new uint[]{5574}, + new uint[]{5575}, + new uint[]{5576}, + new uint[]{5576}, + new uint[]{5577}, + new uint[]{5578}, + new uint[]{5579}, + new uint[]{5580}, + new uint[]{5581}, + new uint[]{5582}, + new uint[]{5583}, + new uint[]{3780}, + new uint[]{3782}, + new uint[]{3781}, + new uint[]{3780}, + new uint[]{5276}, + new uint[]{3780}, + new uint[]{5515}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5640}, + new uint[]{5549}, + new uint[]{5550}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5502}, + new uint[]{5503}, + new uint[]{5504}, + new uint[]{5505}, + Array.Empty(), + Array.Empty(), + new uint[]{5567}, + new uint[]{5567}, + new uint[]{5568}, + new uint[]{5569}, + new uint[]{5570}, + Array.Empty(), + new uint[]{5572}, + new uint[]{5567}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{2096}, + Array.Empty(), + Array.Empty(), + new uint[]{628}, + new uint[]{6014}, + new uint[]{6015}, + new uint[]{6017}, + new uint[]{6018}, + new uint[]{6019}, + new uint[]{108}, + new uint[]{5520}, + new uint[]{108}, + new uint[]{5689, 6353}, + new uint[]{5690}, + new uint[]{5691}, + new uint[]{6145}, + new uint[]{5718, 5767}, + new uint[]{5696}, + new uint[]{5698, 5731}, + new uint[]{5699, 5706, 6429}, + new uint[]{5704, 5709}, + Array.Empty(), + Array.Empty(), + new uint[]{5716}, + new uint[]{5717}, + new uint[]{5681, 5694}, + Array.Empty(), + Array.Empty(), + new uint[]{5700, 5710}, + Array.Empty(), + new uint[]{5672, 5692}, + Array.Empty(), + new uint[]{5684}, + new uint[]{5787, 6485}, + new uint[]{5719, 6797}, + new uint[]{5683}, + new uint[]{5674}, + new uint[]{5711}, + new uint[]{5705}, + new uint[]{5701}, + new uint[]{5712}, + new uint[]{5721}, + new uint[]{5720}, + new uint[]{5688}, + new uint[]{5707}, + new uint[]{5722}, + new uint[]{113}, + new uint[]{115}, + new uint[]{56}, + new uint[]{117}, + new uint[]{116}, + new uint[]{5733}, + new uint[]{108}, + new uint[]{5743, 5832, 6477, 6483}, + new uint[]{5744, 6477, 6483, 6489}, + new uint[]{5745, 6483, 6489}, + new uint[]{5747, 5766, 5849}, + new uint[]{5739, 5897}, + Array.Empty(), + new uint[]{5768, 6528}, + new uint[]{5755, 6518}, + new uint[]{5776}, + new uint[]{5769, 6509, 6525, 6731}, + Array.Empty(), + new uint[]{5782}, + Array.Empty(), + new uint[]{5756, 5770, 6529}, + new uint[]{5773, 6799}, + new uint[]{6141}, + new uint[]{5752, 6519}, + new uint[]{5753, 6519}, + new uint[]{5754, 6519}, + new uint[]{5734}, + new uint[]{5735, 5899}, + new uint[]{5775}, + Array.Empty(), + new uint[]{5758, 5786, 6288}, + Array.Empty(), + new uint[]{5783, 6521}, + new uint[]{5777}, + new uint[]{5736}, + new uint[]{5737, 5900}, + new uint[]{5738, 6798}, + new uint[]{5746, 6729}, + new uint[]{5749, 5774, 6550}, + new uint[]{5778}, + new uint[]{5728}, + new uint[]{5714}, + new uint[]{5715}, + new uint[]{5685}, + new uint[]{5693, 6796}, + Array.Empty(), + Array.Empty(), + new uint[]{5686}, + new uint[]{6775}, + Array.Empty(), + new uint[]{5726}, + new uint[]{5732, 6741}, + new uint[]{5680}, + new uint[]{5697, 6280}, + new uint[]{5687}, + new uint[]{5713}, + new uint[]{5708}, + new uint[]{5702}, + new uint[]{5723}, + new uint[]{5724}, + new uint[]{5725}, + new uint[]{5729}, + new uint[]{5730}, + new uint[]{5751, 6730}, + new uint[]{5750}, + new uint[]{5740, 6505, 6722}, + new uint[]{5741, 5784, 5887}, + Array.Empty(), + new uint[]{5748, 5759, 5771}, + new uint[]{5760, 5772}, + new uint[]{5761, 6728}, + new uint[]{5762}, + Array.Empty(), + new uint[]{5764}, + new uint[]{5836, 6507}, + new uint[]{5765}, + new uint[]{5779, 5941}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5785}, + new uint[]{5780}, + new uint[]{5781}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6426}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5727}, + new uint[]{5675, 6776}, + Array.Empty(), + Array.Empty(), + new uint[]{5954}, + new uint[]{6378}, + new uint[]{6381}, + new uint[]{6382}, + new uint[]{6383}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5590}, + new uint[]{5591}, + new uint[]{5592}, + new uint[]{5650}, + new uint[]{5576}, + new uint[]{5651}, + new uint[]{5652}, + new uint[]{5670}, + new uint[]{108}, + new uint[]{6154}, + new uint[]{4133}, + new uint[]{5239}, + new uint[]{5656}, + new uint[]{5657}, + new uint[]{5658}, + new uint[]{5659}, + new uint[]{5660}, + new uint[]{5661}, + new uint[]{5662}, + new uint[]{5239}, + new uint[]{4130}, + new uint[]{6146}, + new uint[]{6143}, + new uint[]{6144}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5540}, + new uint[]{6144}, + new uint[]{6147}, + Array.Empty(), + new uint[]{5643}, + new uint[]{5629}, + new uint[]{5629}, + new uint[]{5630}, + new uint[]{108}, + new uint[]{6142}, + new uint[]{6085}, + new uint[]{6086}, + new uint[]{6087}, + new uint[]{6088}, + new uint[]{6089}, + new uint[]{6090}, + new uint[]{6675}, + new uint[]{6676}, + new uint[]{6691, 6787}, + new uint[]{6691, 6787}, + new uint[]{6786}, + new uint[]{108}, + new uint[]{3237}, + new uint[]{6677}, + new uint[]{5970}, + new uint[]{5659}, + new uint[]{5964}, + new uint[]{5965}, + new uint[]{5966}, + new uint[]{5967}, + new uint[]{5953}, + new uint[]{5968}, + new uint[]{5969}, + new uint[]{5950}, + new uint[]{5951}, + new uint[]{1768}, + new uint[]{6342}, + new uint[]{6091}, + new uint[]{3305}, + new uint[]{6093}, + new uint[]{6075}, + new uint[]{6075}, + new uint[]{6076}, + new uint[]{6077}, + new uint[]{6078}, + new uint[]{6079}, + new uint[]{6080}, + new uint[]{6081}, + new uint[]{6082}, + new uint[]{6083}, + new uint[]{6084}, + new uint[]{5630}, + new uint[]{6173}, + new uint[]{6174}, + new uint[]{6175}, + new uint[]{6175}, + new uint[]{6176}, + new uint[]{6177}, + new uint[]{6178}, + new uint[]{6180}, + new uint[]{6155}, + new uint[]{6181}, + new uint[]{6182}, + new uint[]{6263}, + new uint[]{6264}, + new uint[]{6265}, + new uint[]{6266}, + new uint[]{6267}, + new uint[]{6268}, + new uint[]{6269}, + new uint[]{6270}, + new uint[]{5984}, + new uint[]{5985}, + new uint[]{5986}, + new uint[]{5987}, + new uint[]{5988}, + new uint[]{5989}, + new uint[]{5990}, + new uint[]{5991}, + new uint[]{5992}, + new uint[]{5993}, + new uint[]{5994}, + new uint[]{5995}, + new uint[]{5996}, + new uint[]{5997}, + new uint[]{5998}, + new uint[]{5999}, + new uint[]{6000}, + new uint[]{6001}, + new uint[]{6002}, + new uint[]{6003}, + new uint[]{6004}, + new uint[]{6005}, + new uint[]{6006}, + new uint[]{6007}, + new uint[]{6008}, + new uint[]{6009}, + new uint[]{6010}, + new uint[]{6011}, + new uint[]{6012}, + new uint[]{6013}, + new uint[]{6674}, + new uint[]{9083, 9088, 9090, 9095, 9096, 9099, 9103, 9107, 9109, 9113, 9117}, + new uint[]{9070, 9074, 9083, 9089, 9092, 9094, 9104, 9107, 9109, 9114, 9119, 9123, 9125}, + new uint[]{9070, 9079, 9087, 9088, 9089, 9093, 9099, 9100, 9105, 9106, 9107, 9111, 9114, 9117, 9120, 9122, 9124}, + new uint[]{9068, 9079, 9085, 9086, 9091, 9098, 9102, 9105, 9110, 9122}, + new uint[]{9072, 9077, 9088, 9090, 9097, 9113, 9118}, + new uint[]{9082, 9093, 9094, 9100, 9103, 9104, 9111, 9125}, + new uint[]{9069, 9080, 9084, 9089, 9093, 9095, 9101, 9108, 9111, 9115, 9119, 9123}, + new uint[]{9075, 9076, 9081, 9087, 9088, 9099, 9102, 9103, 9112, 9121}, + new uint[]{9080, 9081, 9084, 9087, 9088, 9093, 9102, 9103, 9105, 9107, 9113, 9114, 9115, 9120, 9125}, + new uint[]{6352}, + new uint[]{6049}, + new uint[]{6050}, + new uint[]{6051}, + new uint[]{5645}, + new uint[]{6045}, + new uint[]{6342}, + new uint[]{6341}, + new uint[]{6672}, + new uint[]{6673}, + new uint[]{1854}, + new uint[]{6691, 6787}, + new uint[]{108, 3649}, + new uint[]{6346}, + new uint[]{6341}, + new uint[]{5648}, + new uint[]{6662}, + new uint[]{6658}, + new uint[]{6659}, + new uint[]{6660}, + new uint[]{1501}, + new uint[]{108}, + new uint[]{4865}, + new uint[]{4866}, + new uint[]{4869}, + new uint[]{6391}, + new uint[]{6341}, + new uint[]{6669}, + new uint[]{6670}, + new uint[]{5972}, + new uint[]{6221}, + new uint[]{6221}, + new uint[]{108}, + new uint[]{6671}, + new uint[]{6071}, + new uint[]{6072}, + new uint[]{6073}, + new uint[]{6074}, + new uint[]{6058}, + new uint[]{6059}, + new uint[]{6060}, + new uint[]{6061}, + new uint[]{6062}, + new uint[]{6063}, + new uint[]{6064}, + new uint[]{6065}, + new uint[]{6066}, + new uint[]{6067}, + new uint[]{6068}, + new uint[]{6058}, + new uint[]{6069}, + new uint[]{6237}, + new uint[]{6238}, + new uint[]{6241}, + new uint[]{6243}, + new uint[]{6244}, + new uint[]{6246}, + new uint[]{5789}, + new uint[]{6275}, + new uint[]{6272}, + new uint[]{6279}, + new uint[]{6278}, + new uint[]{6277}, + Array.Empty(), + new uint[]{5641}, + new uint[]{5642}, + new uint[]{6226}, + new uint[]{6227}, + new uint[]{6228}, + new uint[]{6229}, + new uint[]{6384}, + new uint[]{6230}, + new uint[]{6231}, + new uint[]{6231}, + new uint[]{6232}, + new uint[]{6231}, + new uint[]{6233}, + new uint[]{6234}, + new uint[]{6235}, + new uint[]{6236}, + new uint[]{6335}, + new uint[]{6239}, + new uint[]{6146}, + new uint[]{5575}, + new uint[]{6148}, + new uint[]{6149}, + new uint[]{6156, 6158, 6790}, + new uint[]{6156, 6159}, + new uint[]{6157, 6158, 6160}, + new uint[]{6157, 6160}, + new uint[]{6161}, + new uint[]{6162, 6165}, + new uint[]{6163, 6165, 6791}, + new uint[]{6164}, + new uint[]{6163}, + new uint[]{6153}, + new uint[]{6152}, + new uint[]{5576}, + new uint[]{5562}, + new uint[]{6170}, + new uint[]{6208}, + new uint[]{6172}, + new uint[]{6336}, + new uint[]{6336}, + new uint[]{6153}, + new uint[]{6152}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6566}, + Array.Empty(), + new uint[]{6166}, + new uint[]{6151}, + new uint[]{5973}, + Array.Empty(), + new uint[]{6307}, + new uint[]{6308}, + new uint[]{6309}, + new uint[]{4385}, + new uint[]{6310}, + new uint[]{6311}, + new uint[]{1420}, + new uint[]{6119}, + new uint[]{6120}, + new uint[]{6118}, + new uint[]{6117}, + new uint[]{6116}, + new uint[]{6115}, + new uint[]{6665}, + new uint[]{6242}, + new uint[]{6185}, + new uint[]{6186}, + new uint[]{6187}, + new uint[]{6188}, + new uint[]{6189}, + Array.Empty(), + new uint[]{6190}, + new uint[]{6191}, + new uint[]{6192}, + new uint[]{6193}, + Array.Empty(), + new uint[]{6194}, + new uint[]{6195}, + new uint[]{6196}, + new uint[]{713}, + new uint[]{4392}, + new uint[]{6146}, + new uint[]{4130}, + new uint[]{6102}, + new uint[]{6103}, + new uint[]{6097, 6250}, + new uint[]{6101}, + new uint[]{6098}, + new uint[]{6099}, + new uint[]{6100}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6094}, + new uint[]{6094}, + new uint[]{6094}, + new uint[]{6097}, + new uint[]{6101}, + new uint[]{6098}, + new uint[]{6104}, + new uint[]{6108}, + Array.Empty(), + new uint[]{6140}, + new uint[]{6138}, + new uint[]{6139}, + new uint[]{6136}, + new uint[]{6137}, + new uint[]{6134}, + new uint[]{6135}, + new uint[]{6132}, + new uint[]{6131}, + new uint[]{6130}, + new uint[]{6129}, + new uint[]{6127}, + new uint[]{6128}, + new uint[]{6125}, + new uint[]{6124}, + new uint[]{6123}, + new uint[]{6122}, + new uint[]{6121}, + new uint[]{3920}, + new uint[]{3921}, + new uint[]{3922}, + new uint[]{3913}, + new uint[]{3912}, + new uint[]{6342}, + new uint[]{6668}, + new uint[]{3930}, + Array.Empty(), + new uint[]{6222}, + new uint[]{6221}, + new uint[]{6221}, + Array.Empty(), + new uint[]{6222}, + new uint[]{6223}, + new uint[]{6221, 6224}, + new uint[]{108}, + new uint[]{4420}, + new uint[]{6037}, + new uint[]{108}, + new uint[]{6038}, + new uint[]{6389}, + new uint[]{6389}, + new uint[]{6390}, + new uint[]{6039}, + new uint[]{6039}, + new uint[]{6041}, + new uint[]{6042}, + new uint[]{6040}, + new uint[]{6666}, + new uint[]{6114}, + new uint[]{331}, + new uint[]{6021}, + new uint[]{332}, + new uint[]{5666}, + Array.Empty(), + new uint[]{6342}, + new uint[]{6343}, + new uint[]{6248}, + new uint[]{6249}, + new uint[]{6250}, + new uint[]{6251}, + new uint[]{6252}, + Array.Empty(), + new uint[]{6254}, + new uint[]{6255}, + new uint[]{6256}, + new uint[]{6258}, + new uint[]{6259}, + new uint[]{6260}, + new uint[]{6261}, + new uint[]{6262}, + new uint[]{6257}, + new uint[]{5980}, + new uint[]{6088}, + new uint[]{3165}, + new uint[]{108, 6104}, + new uint[]{5667}, + new uint[]{6200}, + new uint[]{6200}, + new uint[]{6202}, + new uint[]{6201}, + new uint[]{6203}, + new uint[]{6203}, + new uint[]{6204}, + new uint[]{6204}, + new uint[]{6205}, + new uint[]{6205}, + Array.Empty(), + new uint[]{6206}, + new uint[]{6207}, + new uint[]{6170, 6171}, + new uint[]{6208}, + new uint[]{6209}, + new uint[]{6210}, + new uint[]{6211}, + new uint[]{6212}, + new uint[]{6213}, + new uint[]{6214}, + new uint[]{6215}, + new uint[]{6216}, + new uint[]{6170}, + new uint[]{6217}, + new uint[]{6218}, + new uint[]{6219}, + new uint[]{6220}, + new uint[]{6112}, + new uint[]{5764}, + new uint[]{6113}, + new uint[]{6113}, + new uint[]{6111}, + new uint[]{6289}, + new uint[]{6111}, + new uint[]{6111}, + new uint[]{6111}, + new uint[]{6710}, + new uint[]{6095}, + new uint[]{6095}, + new uint[]{6110}, + new uint[]{6105}, + new uint[]{6106}, + new uint[]{6107}, + new uint[]{6096}, + new uint[]{6096}, + new uint[]{6110}, + new uint[]{6105}, + new uint[]{6107}, + new uint[]{6114}, + new uint[]{331}, + new uint[]{6021}, + new uint[]{332}, + new uint[]{5666}, + new uint[]{6022}, + new uint[]{1416}, + new uint[]{4226}, + new uint[]{4227}, + new uint[]{823}, + new uint[]{828}, + new uint[]{751}, + new uint[]{108, 8395}, + new uint[]{6183}, + new uint[]{6184}, + new uint[]{331}, + new uint[]{6021}, + new uint[]{6328}, + new uint[]{6329}, + new uint[]{6330}, + new uint[]{6331}, + new uint[]{6324}, + new uint[]{6332}, + new uint[]{6333}, + new uint[]{6334}, + new uint[]{6332}, + new uint[]{6321}, + new uint[]{6322}, + new uint[]{6323}, + new uint[]{6324}, + new uint[]{6325}, + new uint[]{6326}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6667}, + new uint[]{6224}, + new uint[]{6224}, + new uint[]{5790, 5797, 5798, 6355}, + new uint[]{5791, 6793, 6794}, + new uint[]{5793, 5794, 5795, 6792}, + new uint[]{5796, 6732}, + new uint[]{5799}, + new uint[]{5800}, + new uint[]{5801}, + new uint[]{5802}, + new uint[]{5806}, + new uint[]{5807}, + new uint[]{5808}, + new uint[]{5809}, + new uint[]{5810}, + new uint[]{5811}, + new uint[]{5812}, + new uint[]{2235, 5813}, + new uint[]{5814, 6785}, + new uint[]{5814, 6785}, + Array.Empty(), + new uint[]{2234}, + new uint[]{5815}, + new uint[]{5814, 6785}, + new uint[]{5814, 5872, 5946, 6785}, + new uint[]{5816}, + new uint[]{5819}, + new uint[]{5216}, + new uint[]{1402}, + new uint[]{1402}, + new uint[]{1403}, + new uint[]{3825}, + new uint[]{5822}, + new uint[]{5823}, + new uint[]{5824}, + new uint[]{5825}, + new uint[]{5826}, + new uint[]{1255, 5827, 5855, 5872, 6739}, + new uint[]{5828}, + Array.Empty(), + new uint[]{5975}, + new uint[]{5976}, + new uint[]{5977}, + new uint[]{4130}, + new uint[]{4392}, + new uint[]{6565}, + Array.Empty(), + new uint[]{108}, + new uint[]{6700}, + new uint[]{6701}, + new uint[]{1640}, + new uint[]{3881}, + new uint[]{6694}, + new uint[]{6702}, + new uint[]{6703}, + new uint[]{6704}, + new uint[]{6705}, + new uint[]{108}, + new uint[]{5979}, + new uint[]{5979}, + new uint[]{4331}, + new uint[]{4321}, + new uint[]{6300}, + new uint[]{4332, 5240, 6300, 6303, 6320}, + new uint[]{6301}, + new uint[]{6302}, + new uint[]{6303}, + new uint[]{6306}, + new uint[]{4332}, + new uint[]{6304}, + new uint[]{6304}, + new uint[]{6304}, + new uint[]{6304}, + new uint[]{5241}, + new uint[]{5243}, + new uint[]{5242}, + new uint[]{5244}, + new uint[]{6313}, + new uint[]{3849}, + new uint[]{4142}, + new uint[]{6314}, + new uint[]{6315}, + new uint[]{6316}, + new uint[]{4131}, + new uint[]{6317}, + new uint[]{3850}, + new uint[]{6318}, + new uint[]{6319}, + new uint[]{6320}, + new uint[]{5240}, + new uint[]{4130}, + new uint[]{4392}, + new uint[]{5239}, + new uint[]{6146}, + new uint[]{3204}, + new uint[]{5649}, + new uint[]{6306}, + new uint[]{6661}, + new uint[]{6656}, + new uint[]{6273}, + new uint[]{6225}, + new uint[]{6225}, + new uint[]{6276}, + new uint[]{6245}, + new uint[]{6706}, + new uint[]{5631}, + new uint[]{5632}, + new uint[]{6711}, + new uint[]{6712}, + Array.Empty(), + new uint[]{6385}, + new uint[]{6690}, + new uint[]{6386}, + Array.Empty(), + new uint[]{6388}, + new uint[]{6387}, + Array.Empty(), + new uint[]{6705}, + new uint[]{6705}, + new uint[]{6707}, + new uint[]{4130}, + new uint[]{4392}, + new uint[]{5239}, + new uint[]{4846}, + new uint[]{4740}, + new uint[]{5947}, + new uint[]{5948}, + new uint[]{5949}, + new uint[]{5950}, + new uint[]{5951}, + new uint[]{5952}, + new uint[]{5953}, + new uint[]{5954}, + new uint[]{5955}, + new uint[]{5956}, + new uint[]{5957}, + new uint[]{5958}, + new uint[]{5959}, + new uint[]{5960}, + new uint[]{5961}, + new uint[]{108}, + new uint[]{2147}, + new uint[]{6344}, + new uint[]{6345}, + new uint[]{4759}, + new uint[]{4760}, + new uint[]{4761}, + new uint[]{4762}, + new uint[]{6346}, + new uint[]{6408}, + new uint[]{6709}, + new uint[]{1640}, + new uint[]{3881}, + new uint[]{6694}, + new uint[]{6695}, + new uint[]{5682, 5695}, + Array.Empty(), + new uint[]{4236}, + new uint[]{4237}, + new uint[]{6686}, + new uint[]{6687}, + new uint[]{6688}, + new uint[]{6407}, + Array.Empty(), + new uint[]{6413}, + new uint[]{6414}, + new uint[]{6415}, + new uint[]{6416}, + new uint[]{6418}, + new uint[]{6422}, + new uint[]{6424}, + new uint[]{4427}, + new uint[]{5648}, + new uint[]{6419, 6420, 6423}, + Array.Empty(), + Array.Empty(), + new uint[]{6425}, + new uint[]{6696}, + new uint[]{5788, 5919}, + new uint[]{5742}, + new uint[]{5757}, + new uint[]{6697}, + new uint[]{6698}, + new uint[]{108}, + new uint[]{6699}, + new uint[]{6693}, + new uint[]{5962}, + new uint[]{5963}, + new uint[]{6347}, + new uint[]{6347}, + new uint[]{6351}, + new uint[]{6348}, + new uint[]{108}, + new uint[]{541}, + new uint[]{541}, + new uint[]{108}, + new uint[]{5633}, + new uint[]{5634}, + new uint[]{5635}, + new uint[]{5626}, + new uint[]{6724}, + new uint[]{6056}, + new uint[]{5636}, + new uint[]{5637}, + new uint[]{5633, 6056}, + new uint[]{6379}, + new uint[]{5954}, + new uint[]{5664}, + new uint[]{2096}, + new uint[]{628}, + new uint[]{5663}, + new uint[]{5665}, + new uint[]{5663}, + new uint[]{6385}, + new uint[]{6261}, + new uint[]{6392}, + new uint[]{6393}, + new uint[]{2096}, + new uint[]{6020}, + new uint[]{6016}, + new uint[]{6394}, + new uint[]{6395}, + new uint[]{6290}, + new uint[]{6290}, + new uint[]{6298}, + new uint[]{6293, 6294}, + new uint[]{2343}, + new uint[]{108, 6325, 6327, 8395}, + new uint[]{6453}, + new uint[]{6349}, + new uint[]{6350}, + new uint[]{6231}, + new uint[]{6231}, + new uint[]{6231}, + new uint[]{6231}, + new uint[]{5671}, + new uint[]{5678}, + new uint[]{5672}, + new uint[]{5673}, + new uint[]{5674}, + new uint[]{5675}, + new uint[]{5676}, + new uint[]{5677}, + new uint[]{5679}, + new uint[]{5680}, + new uint[]{5698}, + new uint[]{5699}, + new uint[]{5700}, + new uint[]{5701}, + new uint[]{5702}, + new uint[]{5703}, + new uint[]{113}, + new uint[]{115}, + new uint[]{56}, + Array.Empty(), + new uint[]{6052}, + new uint[]{6052}, + new uint[]{6053}, + new uint[]{6054}, + new uint[]{6055}, + new uint[]{6055}, + new uint[]{4815}, + new uint[]{6052, 6055}, + new uint[]{6385}, + Array.Empty(), + new uint[]{6686}, + new uint[]{6687}, + new uint[]{6688}, + new uint[]{6685}, + new uint[]{108}, + new uint[]{6689}, + new uint[]{5655}, + new uint[]{5653}, + new uint[]{5654}, + new uint[]{6337}, + new uint[]{6338}, + new uint[]{6339}, + new uint[]{6340}, + new uint[]{6340}, + new uint[]{6340}, + new uint[]{4237}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{5850}, + new uint[]{5851}, + new uint[]{5853}, + new uint[]{5854}, + new uint[]{5855}, + new uint[]{5856}, + new uint[]{3102}, + new uint[]{104, 105}, + new uint[]{5842, 5855, 5862, 5880, 5894, 6713, 6720, 6789}, + new uint[]{5860}, + new uint[]{5861, 6354}, + new uint[]{5861, 5913, 5938}, + new uint[]{5861, 5915}, + new uint[]{5842, 5863, 6714, 6740}, + new uint[]{3107}, + new uint[]{5865}, + new uint[]{3107}, + new uint[]{5865}, + new uint[]{5866}, + new uint[]{5866}, + new uint[]{5867}, + new uint[]{5868}, + new uint[]{5869}, + new uint[]{5838, 5870}, + new uint[]{5871}, + new uint[]{5777}, + new uint[]{5872, 5946}, + new uint[]{5873}, + new uint[]{5829, 5895, 5934}, + new uint[]{5874}, + new uint[]{5875}, + new uint[]{5800}, + new uint[]{5876}, + new uint[]{5805}, + new uint[]{5878}, + new uint[]{5806}, + Array.Empty(), + new uint[]{5882}, + new uint[]{5883}, + new uint[]{5883}, + new uint[]{5884}, + new uint[]{5885}, + new uint[]{3930}, + new uint[]{5839, 5886}, + new uint[]{5888}, + new uint[]{5889}, + new uint[]{5890}, + new uint[]{5891}, + new uint[]{5892}, + new uint[]{5916}, + new uint[]{5893}, + new uint[]{5896}, + Array.Empty(), + new uint[]{5901}, + new uint[]{5902}, + new uint[]{5903}, + new uint[]{5904}, + new uint[]{5905}, + new uint[]{5906}, + new uint[]{5907}, + new uint[]{5909}, + new uint[]{5841, 5981}, + Array.Empty(), + new uint[]{5910}, + new uint[]{5911}, + new uint[]{5912}, + new uint[]{5877}, + new uint[]{2234}, + new uint[]{6736}, + new uint[]{5830}, + Array.Empty(), + new uint[]{5833}, + new uint[]{5832}, + new uint[]{5834}, + new uint[]{5835}, + new uint[]{5837}, + new uint[]{5838}, + new uint[]{5804}, + Array.Empty(), + new uint[]{5841, 5893, 5946}, + new uint[]{5982}, + new uint[]{5843, 5983}, + new uint[]{5847}, + new uint[]{5848}, + new uint[]{108}, + new uint[]{6023}, + new uint[]{6024}, + new uint[]{6026}, + new uint[]{6025}, + new uint[]{6027}, + new uint[]{6028}, + new uint[]{6029}, + new uint[]{6030}, + new uint[]{6031}, + new uint[]{6032}, + new uint[]{6033}, + new uint[]{6034}, + new uint[]{6035}, + new uint[]{6035}, + new uint[]{6036}, + new uint[]{6737}, + new uint[]{6738}, + new uint[]{5660}, + new uint[]{6735}, + new uint[]{6471}, + new uint[]{6472}, + new uint[]{6474}, + new uint[]{6475}, + new uint[]{6480}, + new uint[]{6482}, + new uint[]{6492}, + new uint[]{6497}, + new uint[]{6455}, + new uint[]{6454}, + new uint[]{5644}, + new uint[]{6050}, + new uint[]{6051}, + new uint[]{5645}, + new uint[]{6045}, + new uint[]{5643}, + new uint[]{3860}, + new uint[]{5646}, + new uint[]{5647}, + new uint[]{5971}, + new uint[]{6043}, + new uint[]{6044}, + new uint[]{6146}, + new uint[]{5575}, + new uint[]{6148}, + new uint[]{6149}, + new uint[]{6159, 6162}, + new uint[]{6156}, + new uint[]{6046}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6493}, + new uint[]{6556, 6557, 6558, 6559, 6653, 6654, 6655}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6157}, + new uint[]{6163}, + new uint[]{6743}, + new uint[]{6744}, + new uint[]{6745}, + new uint[]{6746}, + new uint[]{6747}, + new uint[]{6748}, + new uint[]{6749}, + new uint[]{6750}, + new uint[]{6751}, + new uint[]{6752}, + new uint[]{6753}, + new uint[]{6754}, + new uint[]{6755}, + new uint[]{6756}, + new uint[]{6757}, + new uint[]{6758}, + new uint[]{6759}, + new uint[]{6760}, + new uint[]{6761}, + new uint[]{6762}, + new uint[]{6763}, + new uint[]{6764}, + new uint[]{6765}, + new uint[]{6766}, + new uint[]{6767}, + new uint[]{6768}, + new uint[]{6769}, + new uint[]{6770}, + new uint[]{6771}, + new uint[]{6772}, + new uint[]{6773}, + new uint[]{6774}, + new uint[]{6433}, + new uint[]{6434}, + new uint[]{6435}, + Array.Empty(), + new uint[]{6438}, + new uint[]{6440}, + new uint[]{6441}, + new uint[]{6443}, + new uint[]{6445}, + new uint[]{6446}, + new uint[]{6447}, + new uint[]{6449}, + new uint[]{6450}, + new uint[]{6503}, + new uint[]{6506}, + Array.Empty(), + new uint[]{6513}, + new uint[]{6514}, + new uint[]{6515}, + new uint[]{6516}, + new uint[]{6517}, + new uint[]{6522}, + new uint[]{6523}, + new uint[]{6524}, + new uint[]{6047}, + new uint[]{5633}, + new uint[]{5634}, + new uint[]{6724}, + new uint[]{3293}, + new uint[]{6048}, + new uint[]{5644}, + new uint[]{6377}, + new uint[]{6364}, + new uint[]{6365}, + new uint[]{6366}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6385}, + new uint[]{6690}, + new uint[]{6386}, + new uint[]{6385}, + new uint[]{108}, + new uint[]{6473}, + new uint[]{4144}, + new uint[]{4192}, + new uint[]{6367}, + new uint[]{6368}, + new uint[]{6369}, + new uint[]{6156, 6790}, + new uint[]{6159}, + new uint[]{6163, 6791}, + new uint[]{6715}, + new uint[]{2147}, + new uint[]{4760}, + new uint[]{5978}, + new uint[]{6716}, + new uint[]{3257}, + new uint[]{3258}, + new uint[]{3259}, + new uint[]{3262}, + new uint[]{3264}, + new uint[]{1486}, + new uint[]{1418}, + new uint[]{1502}, + new uint[]{6057}, + new uint[]{3374}, + new uint[]{5625}, + new uint[]{5625}, + new uint[]{5626}, + new uint[]{5627}, + Array.Empty(), + new uint[]{108}, + new uint[]{6718}, + new uint[]{108}, + new uint[]{6719}, + new uint[]{6530}, + new uint[]{6531}, + new uint[]{6536}, + new uint[]{6538}, + new uint[]{6539}, + new uint[]{6542}, + new uint[]{6545}, + new uint[]{6546}, + new uint[]{6548}, + new uint[]{6551}, + new uint[]{6552}, + new uint[]{6553}, + new uint[]{6555}, + new uint[]{6549}, + new uint[]{5777}, + Array.Empty(), + new uint[]{6499}, + new uint[]{5753}, + new uint[]{6500}, + new uint[]{6495}, + new uint[]{5748}, + Array.Empty(), + Array.Empty(), + new uint[]{5631}, + new uint[]{6711}, + new uint[]{6712}, + new uint[]{3164}, + new uint[]{3133}, + new uint[]{6678}, + new uint[]{3138}, + new uint[]{3129}, + new uint[]{6679}, + new uint[]{6680}, + new uint[]{6680}, + new uint[]{6680}, + new uint[]{3165}, + new uint[]{4144}, + new uint[]{4192}, + new uint[]{4204}, + new uint[]{6375}, + new uint[]{6370, 6376}, + new uint[]{6371}, + new uint[]{6372}, + new uint[]{6372}, + new uint[]{6373}, + new uint[]{6374}, + new uint[]{6681}, + new uint[]{1501}, + new uint[]{108}, + new uint[]{6052}, + new uint[]{6052}, + new uint[]{6053}, + new uint[]{6054}, + new uint[]{6566}, + new uint[]{5970}, + new uint[]{4130}, + new uint[]{713}, + new uint[]{6357}, + new uint[]{6358}, + new uint[]{6470}, + new uint[]{6356}, + new uint[]{5721}, + new uint[]{6359}, + new uint[]{6281}, + new uint[]{5841}, + new uint[]{6283}, + new uint[]{6284}, + new uint[]{6285}, + new uint[]{6286}, + new uint[]{6287}, + new uint[]{5792}, + new uint[]{5914}, + new uint[]{5818}, + new uint[]{5916}, + new uint[]{5918}, + new uint[]{5920}, + new uint[]{5921}, + new uint[]{5922}, + new uint[]{5923}, + new uint[]{5924}, + new uint[]{5925}, + new uint[]{5926}, + new uint[]{5927}, + new uint[]{5798}, + new uint[]{5929}, + new uint[]{5930}, + new uint[]{5931}, + new uint[]{5932}, + new uint[]{5933}, + new uint[]{5934, 6282}, + new uint[]{5935}, + Array.Empty(), + new uint[]{5937}, + new uint[]{5939}, + new uint[]{5940}, + new uint[]{5942}, + new uint[]{5943}, + Array.Empty(), + new uint[]{5944}, + new uint[]{5945}, + Array.Empty(), + new uint[]{6111}, + new uint[]{6683}, + new uint[]{6684}, + new uint[]{6684}, + new uint[]{108}, + new uint[]{6360}, + new uint[]{6361}, + new uint[]{6362}, + new uint[]{6363}, + new uint[]{6657}, + new uint[]{6102}, + new uint[]{6097, 6250}, + new uint[]{6098}, + new uint[]{6101}, + new uint[]{5719}, + new uint[]{5723}, + new uint[]{4392}, + new uint[]{5954}, + new uint[]{108}, + new uint[]{6567}, + new uint[]{6568}, + new uint[]{6569}, + new uint[]{6570}, + new uint[]{6571}, + new uint[]{6572}, + new uint[]{6573}, + new uint[]{6574}, + new uint[]{6575}, + new uint[]{6576}, + new uint[]{6577}, + new uint[]{6578}, + new uint[]{6579}, + new uint[]{6580}, + new uint[]{6581}, + new uint[]{6582}, + new uint[]{6583}, + new uint[]{6584}, + new uint[]{6585}, + new uint[]{6586}, + new uint[]{6587}, + new uint[]{6588}, + new uint[]{6589}, + new uint[]{6590}, + new uint[]{6591}, + new uint[]{6592}, + new uint[]{6593}, + new uint[]{6594}, + new uint[]{6595}, + new uint[]{6596}, + new uint[]{6597}, + new uint[]{6598}, + new uint[]{6599}, + new uint[]{6600}, + new uint[]{6601}, + new uint[]{6602}, + new uint[]{6582}, + new uint[]{6603}, + new uint[]{6604}, + new uint[]{6605}, + new uint[]{6606}, + new uint[]{6607}, + new uint[]{6608}, + new uint[]{6609}, + new uint[]{6610}, + new uint[]{6611}, + new uint[]{6612}, + new uint[]{6585}, + new uint[]{6613}, + new uint[]{6614}, + new uint[]{6615}, + new uint[]{6616}, + new uint[]{6617}, + new uint[]{6618}, + new uint[]{6619}, + new uint[]{6620}, + new uint[]{6621}, + new uint[]{6622}, + new uint[]{6623}, + new uint[]{6624}, + new uint[]{6625}, + new uint[]{6626}, + new uint[]{6627}, + new uint[]{6628}, + new uint[]{6629}, + new uint[]{6630}, + new uint[]{6631}, + new uint[]{6632}, + new uint[]{6633}, + new uint[]{6634}, + new uint[]{6635}, + new uint[]{6636}, + new uint[]{6637}, + new uint[]{6638}, + new uint[]{6639}, + new uint[]{6640}, + new uint[]{6641}, + new uint[]{6642}, + new uint[]{6585}, + new uint[]{6643}, + new uint[]{6644}, + new uint[]{6645}, + new uint[]{6586}, + new uint[]{6646}, + new uint[]{6647}, + new uint[]{6648}, + new uint[]{6649}, + new uint[]{6650}, + new uint[]{6651}, + new uint[]{6652}, + new uint[]{6562}, + new uint[]{6563}, + new uint[]{6554}, + new uint[]{6540}, + new uint[]{6532}, + new uint[]{6456}, + new uint[]{6457}, + new uint[]{6458}, + new uint[]{6459}, + new uint[]{6460}, + new uint[]{6461}, + new uint[]{6462}, + new uint[]{6463}, + new uint[]{6464}, + new uint[]{6465}, + new uint[]{6466}, + new uint[]{6468}, + new uint[]{6469}, + new uint[]{6734}, + new uint[]{6467}, + new uint[]{6727}, + new uint[]{6427}, + new uint[]{6430}, + new uint[]{6428}, + new uint[]{6511}, + new uint[]{6487}, + Array.Empty(), + new uint[]{6495}, + new uint[]{6491}, + new uint[]{6493}, + new uint[]{6493}, + new uint[]{6402, 6404, 6405}, + new uint[]{6396}, + new uint[]{6397}, + new uint[]{6400}, + new uint[]{6401}, + new uint[]{6564}, + new uint[]{6564}, + new uint[]{6501}, + new uint[]{6501}, + new uint[]{6501}, + new uint[]{108}, + new uint[]{6111}, + new uint[]{108}, + new uint[]{6680}, + new uint[]{6680}, + new uint[]{6682}, + Array.Empty(), + new uint[]{108, 6095}, + new uint[]{6664}, + new uint[]{6663}, + Array.Empty(), + new uint[]{5632}, + new uint[]{6692}, + new uint[]{6437}, + new uint[]{6498}, + new uint[]{541}, + new uint[]{3306}, + new uint[]{6296}, + new uint[]{4953}, + new uint[]{6395}, + new uint[]{108}, + new uint[]{6708}, + new uint[]{5628}, + new uint[]{108}, + new uint[]{108}, + new uint[]{5640}, + Array.Empty(), + new uint[]{6529}, + new uint[]{6721, 6723}, + new uint[]{5923}, + new uint[]{5846}, + new uint[]{5845}, + new uint[]{5844}, + new uint[]{5898}, + new uint[]{5936}, + new uint[]{2234}, + new uint[]{5801}, + new uint[]{6285}, + new uint[]{5817}, + new uint[]{2234}, + new uint[]{5832}, + new uint[]{5833}, + new uint[]{5834}, + new uint[]{2236}, + new uint[]{5578}, + new uint[]{5852}, + new uint[]{5829}, + new uint[]{2236}, + new uint[]{5578}, + new uint[]{5578}, + new uint[]{6284}, + new uint[]{5970}, + new uint[]{5659}, + new uint[]{5964}, + new uint[]{5660}, + new uint[]{5660}, + new uint[]{5967}, + Array.Empty(), + new uint[]{6726}, + new uint[]{6742}, + Array.Empty(), + Array.Empty(), + new uint[]{6857, 6858}, + new uint[]{6859, 6860, 6861, 6862}, + new uint[]{541}, + Array.Empty(), + new uint[]{6922}, + new uint[]{3069}, + new uint[]{6923}, + new uint[]{6950}, + new uint[]{5789}, + new uint[]{6275}, + new uint[]{6272}, + Array.Empty(), + new uint[]{6278}, + new uint[]{6277}, + new uint[]{6271}, + new uint[]{5641}, + new uint[]{5642}, + new uint[]{6273}, + new uint[]{6276}, + new uint[]{6856}, + new uint[]{6869}, + new uint[]{6870}, + new uint[]{6871}, + new uint[]{6872}, + new uint[]{6925}, + new uint[]{6926}, + new uint[]{6927}, + new uint[]{6928}, + new uint[]{6929}, + new uint[]{6930}, + new uint[]{6931}, + new uint[]{6932}, + new uint[]{6933}, + new uint[]{6934}, + new uint[]{6935}, + new uint[]{6936}, + new uint[]{6937}, + new uint[]{6938}, + new uint[]{6939}, + new uint[]{6940}, + new uint[]{5640, 5641, 5642, 6275}, + new uint[]{5640, 5641, 5642, 6856}, + new uint[]{5641}, + new uint[]{5642}, + new uint[]{6907}, + new uint[]{6908}, + new uint[]{6909}, + new uint[]{6910}, + new uint[]{6941}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6897}, + new uint[]{6896}, + new uint[]{6898}, + new uint[]{6899}, + new uint[]{6900}, + new uint[]{6901}, + new uint[]{6902}, + new uint[]{6903}, + new uint[]{6905}, + new uint[]{6904}, + new uint[]{6906}, + new uint[]{6945}, + new uint[]{6953}, + new uint[]{6952}, + new uint[]{6951}, + new uint[]{6942}, + new uint[]{108, 6943}, + new uint[]{6944}, + new uint[]{6945}, + new uint[]{6946}, + new uint[]{6853}, + new uint[]{6847}, + new uint[]{6848}, + new uint[]{6849}, + new uint[]{6850}, + new uint[]{6851}, + new uint[]{6385}, + new uint[]{6911}, + new uint[]{4130}, + new uint[]{5978}, + new uint[]{4133}, + new uint[]{6912}, + new uint[]{6971}, + new uint[]{5970}, + Array.Empty(), + new uint[]{6913}, + new uint[]{5964}, + new uint[]{6386}, + new uint[]{6914}, + new uint[]{1482}, + new uint[]{2210}, + new uint[]{2612}, + new uint[]{2628}, + new uint[]{2630}, + new uint[]{2631}, + new uint[]{2632}, + new uint[]{6958}, + new uint[]{6957}, + new uint[]{3210}, + new uint[]{3204}, + Array.Empty(), + new uint[]{6854}, + new uint[]{6855}, + Array.Empty(), + new uint[]{6817}, + new uint[]{6808}, + new uint[]{6811}, + new uint[]{6810}, + new uint[]{6807}, + new uint[]{6812}, + new uint[]{6813}, + new uint[]{6814}, + new uint[]{6815}, + new uint[]{6809}, + new uint[]{6816}, + new uint[]{6818}, + new uint[]{6819}, + new uint[]{6820}, + new uint[]{6821}, + new uint[]{6822}, + new uint[]{6823}, + new uint[]{6824}, + new uint[]{6825}, + new uint[]{6826}, + new uint[]{6827}, + new uint[]{6828}, + new uint[]{6829}, + new uint[]{6830}, + new uint[]{6831}, + new uint[]{6832}, + new uint[]{6835}, + new uint[]{6834}, + new uint[]{6833}, + new uint[]{6836}, + new uint[]{6837}, + new uint[]{6838}, + new uint[]{6839}, + new uint[]{6840}, + new uint[]{6841}, + new uint[]{6842}, + new uint[]{6843}, + new uint[]{6844}, + new uint[]{6845}, + new uint[]{6846}, + new uint[]{6863}, + new uint[]{6864}, + Array.Empty(), + new uint[]{6915}, + new uint[]{6916}, + new uint[]{6917}, + new uint[]{6918}, + new uint[]{6919}, + new uint[]{6920}, + new uint[]{6921}, + new uint[]{7147}, + new uint[]{7167}, + new uint[]{7168}, + Array.Empty(), + new uint[]{6865}, + new uint[]{6866}, + new uint[]{6941}, + Array.Empty(), + new uint[]{6873}, + new uint[]{6874}, + new uint[]{6875}, + new uint[]{6876, 6894}, + new uint[]{6876}, + new uint[]{6877}, + new uint[]{6878}, + new uint[]{6878}, + new uint[]{6879}, + new uint[]{6880}, + new uint[]{6881}, + new uint[]{6881, 6895}, + new uint[]{6882, 6889}, + new uint[]{6884}, + new uint[]{6883, 6890}, + new uint[]{6882, 6886}, + new uint[]{6884, 6888}, + new uint[]{6883, 6887}, + new uint[]{6885}, + new uint[]{6889}, + new uint[]{6890}, + new uint[]{6891}, + new uint[]{6961}, + new uint[]{6962}, + new uint[]{6963}, + new uint[]{6964}, + new uint[]{6965}, + new uint[]{6966}, + new uint[]{7184}, + new uint[]{7147}, + new uint[]{5746}, + new uint[]{5743}, + new uint[]{5744}, + new uint[]{5745}, + new uint[]{6967}, + new uint[]{5746}, + new uint[]{5743}, + new uint[]{5744}, + new uint[]{7036}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6949}, + new uint[]{6954}, + new uint[]{6955}, + new uint[]{6956}, + new uint[]{6961}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6967}, + new uint[]{7143}, + new uint[]{7160}, + new uint[]{7144}, + new uint[]{6959}, + new uint[]{6567}, + new uint[]{6568}, + new uint[]{6569}, + new uint[]{6570}, + new uint[]{6571}, + new uint[]{6572}, + new uint[]{6573}, + new uint[]{6574}, + new uint[]{6575}, + new uint[]{6576}, + new uint[]{6577}, + new uint[]{6578}, + new uint[]{6579}, + new uint[]{6580}, + new uint[]{6581}, + new uint[]{6582}, + new uint[]{6583}, + new uint[]{6584}, + new uint[]{6585}, + new uint[]{6586}, + new uint[]{6587}, + new uint[]{6588}, + new uint[]{6589}, + new uint[]{6590}, + new uint[]{6591}, + new uint[]{6592}, + new uint[]{6593}, + new uint[]{6594}, + new uint[]{6595}, + new uint[]{6596}, + new uint[]{6597}, + new uint[]{6598}, + new uint[]{6599}, + new uint[]{6600}, + new uint[]{6601}, + new uint[]{6602}, + new uint[]{6582}, + new uint[]{6603}, + new uint[]{6604}, + new uint[]{6605}, + new uint[]{6606}, + new uint[]{6607}, + new uint[]{6608}, + new uint[]{6609}, + new uint[]{6610}, + new uint[]{6611}, + new uint[]{6612}, + new uint[]{6585}, + new uint[]{6613}, + new uint[]{6614}, + new uint[]{6615}, + new uint[]{6616}, + new uint[]{6617}, + new uint[]{6618}, + new uint[]{6619}, + new uint[]{6620}, + new uint[]{6621}, + new uint[]{6622}, + new uint[]{6623}, + new uint[]{6624}, + new uint[]{6625}, + new uint[]{6626}, + new uint[]{6627}, + new uint[]{6628}, + new uint[]{6629}, + new uint[]{6630}, + new uint[]{6631}, + new uint[]{6632}, + new uint[]{6633}, + new uint[]{6634}, + new uint[]{6635}, + new uint[]{6636}, + new uint[]{6637}, + new uint[]{6638}, + new uint[]{6639}, + new uint[]{6640}, + new uint[]{6641}, + new uint[]{6642}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6960}, + new uint[]{6968}, + new uint[]{6969}, + new uint[]{7114}, + new uint[]{7115}, + new uint[]{6994}, + new uint[]{6995}, + new uint[]{7016}, + new uint[]{6996}, + new uint[]{7001}, + new uint[]{6999}, + new uint[]{7015}, + new uint[]{7074}, + new uint[]{7055}, + new uint[]{108}, + new uint[]{7056}, + new uint[]{7057}, + new uint[]{7058}, + new uint[]{7058}, + new uint[]{7144}, + new uint[]{7107}, + new uint[]{7108}, + new uint[]{7109}, + new uint[]{7110}, + new uint[]{7111}, + new uint[]{7112}, + new uint[]{7107}, + new uint[]{7108}, + new uint[]{7109}, + new uint[]{7110}, + new uint[]{7111}, + new uint[]{7112}, + new uint[]{7116}, + new uint[]{7059}, + new uint[]{7060}, + new uint[]{7061}, + new uint[]{7062}, + new uint[]{7063}, + new uint[]{7064}, + new uint[]{7065}, + new uint[]{7066}, + new uint[]{7067}, + new uint[]{7068}, + new uint[]{7069}, + new uint[]{2642}, + new uint[]{7071}, + new uint[]{6972}, + new uint[]{6973}, + new uint[]{6974}, + new uint[]{6975}, + new uint[]{6976}, + new uint[]{6977}, + new uint[]{6978}, + new uint[]{6979}, + new uint[]{6980}, + new uint[]{6981}, + new uint[]{7113}, + new uint[]{7113}, + new uint[]{7182}, + new uint[]{7181}, + new uint[]{7092}, + new uint[]{7093}, + new uint[]{7094}, + new uint[]{7095}, + new uint[]{7092}, + new uint[]{7093}, + new uint[]{7094}, + Array.Empty(), + new uint[]{7000}, + new uint[]{6998}, + new uint[]{7002}, + new uint[]{7007}, + new uint[]{7005}, + new uint[]{7004}, + new uint[]{7003}, + new uint[]{7006}, + new uint[]{7009}, + new uint[]{7008}, + new uint[]{7010}, + new uint[]{7011}, + new uint[]{7012}, + new uint[]{7013}, + new uint[]{7014}, + new uint[]{7096}, + new uint[]{7097}, + new uint[]{7098}, + new uint[]{7183}, + new uint[]{7105}, + new uint[]{7099}, + Array.Empty(), + new uint[]{7106}, + new uint[]{108}, + new uint[]{108}, + new uint[]{6200}, + new uint[]{6203}, + new uint[]{6997}, + new uint[]{6982}, + new uint[]{6982}, + new uint[]{6982}, + new uint[]{6983}, + new uint[]{6983}, + new uint[]{6983}, + new uint[]{6986}, + new uint[]{6984}, + new uint[]{6985}, + new uint[]{6987}, + new uint[]{6988}, + new uint[]{6989}, + new uint[]{7148}, + new uint[]{7149}, + new uint[]{7150}, + new uint[]{7166}, + new uint[]{7151}, + new uint[]{6990}, + new uint[]{7221}, + new uint[]{6173}, + new uint[]{6174}, + new uint[]{6175}, + new uint[]{6175}, + new uint[]{6176}, + new uint[]{7221}, + new uint[]{6173}, + new uint[]{6174}, + new uint[]{6175}, + new uint[]{6175}, + new uint[]{6176}, + new uint[]{6991}, + new uint[]{5574}, + new uint[]{5239}, + new uint[]{6992}, + new uint[]{108}, + new uint[]{6891}, + new uint[]{5834}, + new uint[]{5834}, + new uint[]{5834}, + new uint[]{7127}, + new uint[]{7126}, + new uint[]{7125}, + new uint[]{7127}, + new uint[]{7126}, + new uint[]{7125}, + new uint[]{7124}, + new uint[]{7122}, + new uint[]{7123}, + Array.Empty(), + new uint[]{7126}, + new uint[]{7126}, + new uint[]{7124}, + new uint[]{7122}, + new uint[]{7123}, + new uint[]{7120}, + Array.Empty(), + new uint[]{7127}, + new uint[]{7131}, + new uint[]{7131}, + new uint[]{7130}, + new uint[]{7131}, + new uint[]{7080}, + new uint[]{7081}, + new uint[]{7082}, + new uint[]{7083}, + new uint[]{7084}, + new uint[]{7085}, + new uint[]{7086}, + new uint[]{7087}, + new uint[]{7088}, + new uint[]{7089}, + new uint[]{7090}, + new uint[]{6993}, + new uint[]{7119}, + new uint[]{7093}, + new uint[]{7093}, + new uint[]{7169}, + new uint[]{7170}, + new uint[]{7171}, + new uint[]{7172}, + new uint[]{7149}, + new uint[]{7149}, + new uint[]{7149}, + new uint[]{7177}, + new uint[]{7178}, + new uint[]{7149}, + new uint[]{7149}, + new uint[]{7149}, + new uint[]{7145}, + new uint[]{7146}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7129}, + new uint[]{7129}, + new uint[]{7091}, + new uint[]{7152}, + new uint[]{7153}, + new uint[]{7154}, + new uint[]{7155}, + Array.Empty(), + new uint[]{7156}, + new uint[]{7158}, + new uint[]{7160}, + new uint[]{7161}, + new uint[]{7157}, + new uint[]{7017}, + new uint[]{7018}, + new uint[]{7019}, + new uint[]{7020}, + new uint[]{7021}, + new uint[]{7022}, + new uint[]{7023}, + new uint[]{7024}, + new uint[]{7025}, + new uint[]{7026}, + new uint[]{7027}, + new uint[]{7028}, + new uint[]{7029}, + new uint[]{7030}, + new uint[]{7031}, + new uint[]{7032}, + new uint[]{7033}, + new uint[]{7034}, + new uint[]{7035}, + new uint[]{7152}, + new uint[]{7173}, + new uint[]{7174}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{7176}, + new uint[]{7150}, + new uint[]{7142}, + new uint[]{7141}, + new uint[]{6057}, + new uint[]{1486}, + new uint[]{3374}, + new uint[]{5625}, + new uint[]{5625}, + new uint[]{7110}, + new uint[]{7140}, + new uint[]{7139}, + Array.Empty(), + new uint[]{7087}, + new uint[]{108}, + new uint[]{7128}, + new uint[]{4130}, + new uint[]{5978}, + new uint[]{6381}, + new uint[]{7036}, + new uint[]{7054}, + new uint[]{4148}, + new uint[]{5579}, + new uint[]{5577}, + new uint[]{5581}, + new uint[]{7137}, + new uint[]{7138}, + new uint[]{6203}, + new uint[]{7036}, + new uint[]{7136}, + Array.Empty(), + Array.Empty(), + new uint[]{6984}, + new uint[]{7037}, + new uint[]{7038}, + new uint[]{7039}, + new uint[]{7040}, + new uint[]{6993}, + new uint[]{7041}, + new uint[]{7042}, + new uint[]{6993}, + new uint[]{7043}, + new uint[]{6985}, + new uint[]{7044}, + new uint[]{7045}, + new uint[]{7046}, + new uint[]{7047}, + new uint[]{108}, + new uint[]{108}, + new uint[]{7052}, + new uint[]{7048}, + new uint[]{17}, + new uint[]{7096}, + new uint[]{7097}, + new uint[]{7098}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7200}, + new uint[]{7201}, + new uint[]{7202}, + new uint[]{7203}, + new uint[]{7204}, + new uint[]{7050}, + new uint[]{7049}, + new uint[]{6993}, + new uint[]{7222}, + new uint[]{7051}, + new uint[]{7222}, + new uint[]{6982}, + new uint[]{7053}, + new uint[]{6983}, + new uint[]{7186}, + new uint[]{6982}, + new uint[]{6983}, + new uint[]{7126}, + new uint[]{7126}, + new uint[]{7053}, + new uint[]{7187}, + new uint[]{6982}, + new uint[]{7162}, + new uint[]{7161}, + new uint[]{7161}, + new uint[]{6983}, + new uint[]{7188}, + new uint[]{7163}, + new uint[]{7164}, + new uint[]{7165}, + new uint[]{7189}, + new uint[]{6982}, + new uint[]{7185}, + new uint[]{7159}, + new uint[]{7190}, + new uint[]{6982}, + new uint[]{7185}, + new uint[]{7191}, + new uint[]{7185}, + new uint[]{7192}, + new uint[]{6988}, + new uint[]{6983}, + new uint[]{6988}, + new uint[]{7193}, + new uint[]{7194}, + new uint[]{7195}, + new uint[]{6993}, + new uint[]{7196}, + new uint[]{7197}, + new uint[]{6983}, + new uint[]{7198}, + new uint[]{7199}, + new uint[]{6982}, + new uint[]{6983}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{7225}, + new uint[]{7225}, + new uint[]{1644}, + new uint[]{1645, 1646}, + new uint[]{1647}, + new uint[]{1648}, + new uint[]{2091}, + new uint[]{1801}, + new uint[]{1803}, + new uint[]{1804}, + new uint[]{1185}, + new uint[]{1186}, + new uint[]{2143}, + new uint[]{5563}, + new uint[]{2137}, + new uint[]{2138}, + new uint[]{2324}, + new uint[]{7206}, + new uint[]{7207}, + new uint[]{7208}, + new uint[]{7209}, + new uint[]{7210}, + new uint[]{7211}, + new uint[]{108}, + new uint[]{7212}, + new uint[]{7213}, + new uint[]{7214}, + new uint[]{7215}, + new uint[]{7216}, + new uint[]{7217}, + new uint[]{7218}, + new uint[]{7219}, + new uint[]{7220}, + Array.Empty(), + new uint[]{7205}, + new uint[]{7229}, + Array.Empty(), + new uint[]{7231}, + new uint[]{7232}, + new uint[]{7227}, + new uint[]{7228}, + new uint[]{7476}, + new uint[]{7537}, + new uint[]{7477}, + new uint[]{7226}, + new uint[]{7233}, + new uint[]{7233}, + new uint[]{7234}, + new uint[]{7234}, + new uint[]{7229}, + new uint[]{7230}, + Array.Empty(), + new uint[]{7227}, + new uint[]{7228}, + new uint[]{7476}, + new uint[]{7537}, + new uint[]{7477}, + new uint[]{7226}, + new uint[]{7233}, + new uint[]{7234}, + new uint[]{7206}, + new uint[]{1185}, + new uint[]{1801}, + new uint[]{1644}, + new uint[]{887}, + new uint[]{7478}, + new uint[]{7479}, + new uint[]{7070}, + new uint[]{7100}, + new uint[]{7235}, + new uint[]{108}, + new uint[]{6945}, + new uint[]{7494}, + new uint[]{7236}, + new uint[]{7245}, + new uint[]{7246}, + new uint[]{7247}, + new uint[]{7237}, + new uint[]{7238}, + new uint[]{7239}, + new uint[]{7244}, + new uint[]{6943}, + new uint[]{7403}, + new uint[]{7404}, + new uint[]{7405}, + new uint[]{7406}, + new uint[]{7407}, + new uint[]{7408}, + new uint[]{7409}, + new uint[]{7410}, + new uint[]{7411}, + new uint[]{7412}, + new uint[]{7413}, + new uint[]{7414}, + new uint[]{7415}, + new uint[]{7413}, + new uint[]{7416}, + new uint[]{7417}, + new uint[]{7418}, + new uint[]{7419}, + new uint[]{7420}, + new uint[]{7421}, + new uint[]{7422}, + new uint[]{7423}, + new uint[]{7231}, + new uint[]{7424}, + new uint[]{7425}, + new uint[]{7426}, + new uint[]{7427}, + new uint[]{7428}, + new uint[]{7429}, + new uint[]{7430}, + new uint[]{7431}, + new uint[]{7432}, + new uint[]{7433}, + new uint[]{7434}, + new uint[]{7435}, + new uint[]{7436}, + new uint[]{7437}, + new uint[]{7438}, + new uint[]{7439}, + new uint[]{7440}, + new uint[]{7441}, + new uint[]{7442}, + new uint[]{7443}, + new uint[]{7444}, + new uint[]{7445}, + new uint[]{7446}, + new uint[]{7447}, + new uint[]{7448}, + new uint[]{7449}, + new uint[]{7450}, + new uint[]{7451}, + new uint[]{7452}, + new uint[]{7453}, + new uint[]{7454}, + new uint[]{7455}, + new uint[]{7456}, + new uint[]{7457}, + new uint[]{7458}, + new uint[]{7459}, + new uint[]{7460}, + new uint[]{7461}, + new uint[]{7462}, + new uint[]{7463}, + new uint[]{7582}, + new uint[]{7244}, + new uint[]{7244}, + new uint[]{7223}, + new uint[]{7224}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{7262}, + new uint[]{7263}, + new uint[]{7264}, + new uint[]{7265}, + new uint[]{7266}, + new uint[]{7267}, + new uint[]{7268}, + new uint[]{7269}, + new uint[]{7270}, + new uint[]{7271}, + new uint[]{7272}, + new uint[]{7273}, + new uint[]{7274}, + new uint[]{7275}, + new uint[]{7276}, + new uint[]{7277}, + new uint[]{7278}, + new uint[]{7279}, + new uint[]{7280}, + new uint[]{7281}, + new uint[]{7282}, + new uint[]{7283}, + new uint[]{7284}, + new uint[]{7285}, + new uint[]{7286}, + new uint[]{7287}, + new uint[]{7288}, + new uint[]{7289}, + new uint[]{7290}, + new uint[]{7291}, + new uint[]{7292}, + new uint[]{7293}, + new uint[]{7294}, + new uint[]{7295}, + new uint[]{7296}, + new uint[]{7297}, + new uint[]{7298}, + new uint[]{7299}, + new uint[]{7300}, + new uint[]{7301}, + new uint[]{7302}, + new uint[]{7303}, + new uint[]{7304}, + new uint[]{7305}, + new uint[]{7306}, + new uint[]{7307}, + new uint[]{7308}, + new uint[]{7309}, + new uint[]{7310}, + new uint[]{7305}, + new uint[]{7312}, + new uint[]{7313}, + new uint[]{7314}, + new uint[]{7315}, + new uint[]{7316}, + new uint[]{7317}, + new uint[]{7318}, + new uint[]{7319}, + new uint[]{7320}, + new uint[]{7321}, + new uint[]{7322}, + new uint[]{7323}, + new uint[]{7324}, + new uint[]{7325}, + new uint[]{7326}, + new uint[]{7327}, + new uint[]{7328}, + new uint[]{7329}, + new uint[]{7330}, + new uint[]{7331}, + new uint[]{7332}, + new uint[]{7333}, + new uint[]{7334}, + new uint[]{7335}, + new uint[]{7336}, + new uint[]{7337}, + new uint[]{7338}, + new uint[]{7339}, + new uint[]{7340}, + new uint[]{7341}, + new uint[]{7265}, + new uint[]{7342}, + new uint[]{7343}, + new uint[]{7344}, + new uint[]{7345}, + new uint[]{7346}, + new uint[]{7347}, + new uint[]{7348}, + new uint[]{7349}, + new uint[]{7350}, + new uint[]{7351}, + new uint[]{7352}, + new uint[]{7353}, + new uint[]{7354}, + new uint[]{7355}, + new uint[]{7356}, + new uint[]{7357}, + new uint[]{7358}, + new uint[]{7359}, + new uint[]{7360}, + new uint[]{7361}, + new uint[]{7362}, + new uint[]{7363}, + new uint[]{7364}, + new uint[]{7365}, + new uint[]{7366}, + new uint[]{7367}, + new uint[]{7368}, + new uint[]{7369}, + new uint[]{7370}, + new uint[]{7371}, + new uint[]{7372}, + new uint[]{7373}, + new uint[]{7374}, + new uint[]{7375}, + new uint[]{7376}, + new uint[]{7377}, + new uint[]{7378}, + new uint[]{7379}, + new uint[]{7380}, + new uint[]{7381}, + new uint[]{7382}, + new uint[]{7584}, + new uint[]{7384}, + new uint[]{7385}, + new uint[]{7386}, + new uint[]{7387}, + new uint[]{7388}, + new uint[]{7389}, + new uint[]{7390}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7258}, + new uint[]{7259}, + new uint[]{7260}, + new uint[]{7260}, + new uint[]{7261}, + Array.Empty(), + new uint[]{7240}, + new uint[]{6996}, + new uint[]{7241}, + new uint[]{101, 108, 510, 548, 557, 718, 1279, 1680, 2116, 2118, 2134, 2135, 2136, 2137, 2143, 2160, 3040, 3042, 3044, 3046, 3047, 3330, 3374, 3455, 3458, 3642, 4739, 6039, 6148, 6153, 6649, 6650, 6651, 6652, 6853, 7570, 7585, 7586, 7587, 7588, 7589, 7590, 7591, 7593, 7594, 7595, 7597, 7599, 7600, 7601, 7627, 7628, 7629, 7641, 7657, 7659, 7660, 7662, 7667, 7672, 7691, 7695, 7699, 7702, 7855, 7856, 7857, 7858, 7885, 7886, 7888, 7899, 7900, 7906, 7911, 7912, 7914, 7919, 7922, 7931, 7939, 7947, 7950, 8084, 8087, 8099, 8102, 8107, 8109, 8113, 8117, 8121, 8123, 8124, 8125, 8128, 8129, 8141, 8146, 8162, 8165, 8167, 8201, 8202, 8210, 8211, 8231, 8232, 8233, 8235, 8236, 8250, 8252, 8258, 8260, 8261, 8262, 8267, 8270, 8272, 8273, 8300, 8301, 8338, 8339, 8345, 8350, 8352, 8353, 8361, 8363, 8374, 8379, 8381, 8382, 8393, 8397, 8486, 8826, 8953, 8955, 9029, 9041, 9044, 9046, 9063, 9138, 9140, 9141, 9142, 9143, 9147, 9152, 9153, 9155, 9161, 9162, 9189, 9208, 9220, 9230, 9231, 9233, 9239, 9241, 9245, 9250, 9260, 9261, 9263, 9264, 9265, 9270, 9281, 9287, 9288, 9289, 9296, 9298, 9300, 9331, 9340, 9341, 9353, 9355, 9364, 9384, 9390, 9391, 9394, 9396, 9398, 9400, 9405, 9407, 9408, 9409, 9411, 9416, 9417, 9419, 9422, 9424, 9426, 9427, 9436, 9439, 9442, 9458, 9461, 9462, 9505, 9508, 9511, 9617, 9618, 9642, 9644, 9646, 9648, 9650, 9652, 9678, 9693, 9694, 9695, 9696, 9707, 9709, 9735, 9738, 9741, 9751, 9755, 9759, 9764, 9768, 9769, 9776, 9778, 9780, 9782, 9784, 9786, 9788, 9790, 9793, 9795, 9797, 9806, 9807, 9808, 9811, 9812, 9813, 9834, 9838, 9853, 9854, 9855, 9856, 9857, 9858, 9859, 9860, 9861, 9862, 9863, 9881, 9902, 9908, 9909, 9918, 9921, 9922, 9925, 9929, 9930, 9931, 9935, 9937, 9941, 9942, 9945, 9946, 9948, 9949, 9950, 9953, 9955, 9958, 9961, 9964, 9965, 9966, 9969, 9973, 9988, 9989, 9992, 10004, 10006, 10007, 10013, 10037, 10041, 10059, 10064, 10067, 10075, 10077, 10095, 10189, 10192, 10205, 10207, 10212, 10246, 10247, 10256, 10259, 10279, 10282, 10285, 10288, 10290, 10292, 10293, 10298, 10313, 10314, 10315, 10316, 10317, 10331, 10333, 10336, 10337, 10341, 10345, 10348, 10393, 10394, 10395, 10396, 10399, 10401, 10403, 10404, 10438, 10445, 10446, 10448, 10453, 10456, 10489, 10559, 10572, 10581, 10586, 10647, 10717, 10718, 10719, 10720, 10730, 10731, 10732, 10733, 10742, 10744, 10831, 10832, 10905, 10932, 10933, 10935, 11070, 11195, 11218, 11227, 11238, 11239, 11241, 11253, 11254, 11274, 11277, 11278, 11280, 11281, 11286, 11288, 11289, 11292, 11293, 11302, 11322, 11352, 11369, 11372, 11374, 11378, 11381, 11384, 11387, 11393, 11399, 11402, 11404, 11405, 11407, 11413, 11419, 11440, 11442, 11517}, + new uint[]{7396}, + new uint[]{7397}, + new uint[]{7398}, + new uint[]{7250}, + new uint[]{7251}, + new uint[]{7252}, + new uint[]{5978}, + new uint[]{7253}, + new uint[]{7254}, + new uint[]{7254}, + new uint[]{7255}, + new uint[]{7256}, + new uint[]{7402}, + new uint[]{7402}, + new uint[]{7257}, + new uint[]{7401}, + new uint[]{7248}, + new uint[]{7250}, + new uint[]{7242}, + new uint[]{7243}, + new uint[]{7240}, + new uint[]{7392}, + new uint[]{7392}, + new uint[]{7392}, + new uint[]{7393}, + new uint[]{7393}, + new uint[]{7393}, + new uint[]{7394}, + new uint[]{7394}, + new uint[]{7394}, + new uint[]{7394}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + new uint[]{7610}, + Array.Empty(), + new uint[]{7526}, + new uint[]{7527}, + new uint[]{7528}, + new uint[]{7512}, + new uint[]{7513}, + new uint[]{7512}, + new uint[]{7464}, + new uint[]{7464}, + new uint[]{7464}, + new uint[]{7464}, + new uint[]{7465}, + new uint[]{7465}, + new uint[]{7465}, + new uint[]{7465}, + new uint[]{7466}, + new uint[]{7466}, + new uint[]{7466}, + new uint[]{7466}, + new uint[]{7467}, + new uint[]{7467}, + new uint[]{7467}, + new uint[]{7467}, + new uint[]{7468}, + new uint[]{7468}, + new uint[]{7468}, + new uint[]{7468}, + new uint[]{7468}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7471}, + new uint[]{7472}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7471}, + new uint[]{7472}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7471}, + new uint[]{7472}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7471}, + new uint[]{7472}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7471}, + new uint[]{7472}, + new uint[]{7469}, + new uint[]{7520}, + new uint[]{7521}, + new uint[]{7391}, + new uint[]{7516}, + new uint[]{7517}, + new uint[]{7517}, + new uint[]{7399}, + new uint[]{7497}, + new uint[]{7498}, + new uint[]{7514}, + new uint[]{7515}, + new uint[]{7473}, + new uint[]{7508}, + new uint[]{7509}, + new uint[]{7510}, + new uint[]{7511}, + new uint[]{7508}, + new uint[]{7508}, + new uint[]{7495}, + new uint[]{7669}, + new uint[]{7670}, + new uint[]{7671}, + new uint[]{7701}, + new uint[]{7496}, + new uint[]{7495}, + new uint[]{7524}, + new uint[]{7525}, + new uint[]{7524}, + new uint[]{7518}, + new uint[]{7519}, + new uint[]{7518}, + new uint[]{7518}, + new uint[]{7518}, + new uint[]{7503}, + new uint[]{7504}, + new uint[]{7503}, + new uint[]{7523}, + Array.Empty(), + new uint[]{7523}, + new uint[]{7523}, + new uint[]{7524}, + new uint[]{7475}, + new uint[]{7522}, + new uint[]{7249}, + new uint[]{7499}, + new uint[]{7501}, + new uint[]{7502}, + new uint[]{7499}, + new uint[]{7505}, + new uint[]{7505}, + new uint[]{7505}, + new uint[]{7505}, + new uint[]{7533}, + new uint[]{7647}, + new uint[]{7648}, + new uint[]{7649}, + new uint[]{7650}, + new uint[]{7651}, + new uint[]{7652}, + new uint[]{7653}, + new uint[]{7654}, + new uint[]{7655}, + new uint[]{7656}, + new uint[]{7658}, + new uint[]{7506}, + new uint[]{7507}, + new uint[]{7475}, + new uint[]{7567}, + new uint[]{7248}, + new uint[]{7534}, + new uint[]{7248}, + new uint[]{7532}, + new uint[]{7531}, + new uint[]{7536}, + new uint[]{7480}, + new uint[]{7483}, + new uint[]{7484}, + new uint[]{7487}, + new uint[]{7488}, + new uint[]{7474}, + new uint[]{7481}, + new uint[]{7482}, + new uint[]{7485}, + new uint[]{7486}, + new uint[]{7489}, + new uint[]{7490}, + new uint[]{7491}, + new uint[]{7492}, + new uint[]{7493}, + new uint[]{7535}, + new uint[]{7529}, + new uint[]{7530}, + new uint[]{7568}, + new uint[]{7569}, + new uint[]{3045}, + new uint[]{7529}, + new uint[]{7529}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{7583}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7667}, + new uint[]{7668}, + new uint[]{7675}, + new uint[]{7676}, + new uint[]{7677}, + new uint[]{7678}, + new uint[]{7679}, + new uint[]{7680}, + new uint[]{7681}, + new uint[]{7682}, + new uint[]{7683}, + new uint[]{7684}, + new uint[]{7685}, + new uint[]{7686}, + new uint[]{7687}, + new uint[]{7688}, + new uint[]{7660}, + new uint[]{7661}, + new uint[]{7662}, + new uint[]{7665}, + new uint[]{8099}, + new uint[]{7663}, + new uint[]{7659}, + new uint[]{7650}, + new uint[]{7672}, + new uint[]{7673}, + new uint[]{7674}, + new uint[]{7672}, + new uint[]{7691}, + new uint[]{7694}, + new uint[]{7692}, + new uint[]{7693}, + new uint[]{7691}, + new uint[]{7694}, + new uint[]{7692}, + new uint[]{7693}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7889}, + new uint[]{7890}, + Array.Empty(), + new uint[]{7641}, + new uint[]{7641}, + new uint[]{7643}, + Array.Empty(), + new uint[]{7645}, + new uint[]{7646}, + new uint[]{7641}, + new uint[]{7641}, + new uint[]{7643}, + Array.Empty(), + new uint[]{7645}, + new uint[]{7646}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7702}, + new uint[]{7725}, + new uint[]{7703}, + new uint[]{7705}, + Array.Empty(), + Array.Empty(), + new uint[]{7702}, + new uint[]{7707}, + new uint[]{7708}, + new uint[]{7709}, + new uint[]{7710}, + new uint[]{7702}, + new uint[]{7725}, + new uint[]{7703}, + new uint[]{7704}, + new uint[]{7705}, + Array.Empty(), + Array.Empty(), + new uint[]{7702}, + new uint[]{7711}, + new uint[]{7714}, + new uint[]{7713}, + new uint[]{7712}, + Array.Empty(), + new uint[]{7695}, + new uint[]{7696}, + new uint[]{7697}, + new uint[]{7698}, + Array.Empty(), + new uint[]{7700}, + new uint[]{7695}, + Array.Empty(), + new uint[]{7699}, + Array.Empty(), + new uint[]{7633}, + new uint[]{7635}, + new uint[]{7633}, + new uint[]{7635}, + new uint[]{7636}, + new uint[]{7637}, + new uint[]{7638}, + new uint[]{7639}, + new uint[]{7726}, + new uint[]{7727}, + new uint[]{7729}, + new uint[]{7731}, + new uint[]{7736}, + new uint[]{7739}, + new uint[]{7740}, + new uint[]{7742}, + new uint[]{7746}, + new uint[]{7748}, + new uint[]{7750}, + new uint[]{7753}, + new uint[]{7754}, + new uint[]{7756}, + new uint[]{7759}, + new uint[]{7760}, + new uint[]{7763}, + Array.Empty(), + new uint[]{8345}, + new uint[]{8344}, + new uint[]{8343}, + new uint[]{8342}, + new uint[]{8345}, + new uint[]{8344}, + new uint[]{8343}, + new uint[]{8341}, + new uint[]{8341}, + new uint[]{8272}, + new uint[]{7718}, + new uint[]{6152}, + new uint[]{6153}, + new uint[]{7716}, + new uint[]{6148}, + new uint[]{7771}, + new uint[]{7772}, + new uint[]{7772}, + new uint[]{7773}, + new uint[]{7773}, + new uint[]{7771}, + new uint[]{7772}, + new uint[]{7771}, + new uint[]{7773}, + new uint[]{7773}, + new uint[]{7771}, + new uint[]{7772}, + new uint[]{7774}, + new uint[]{7771}, + new uint[]{7771}, + new uint[]{7774}, + new uint[]{7772}, + new uint[]{7774}, + new uint[]{7773}, + new uint[]{7772}, + new uint[]{7771}, + new uint[]{7775}, + new uint[]{7775}, + new uint[]{7775}, + new uint[]{7776}, + new uint[]{7776}, + new uint[]{7776}, + new uint[]{7777}, + new uint[]{7778}, + new uint[]{7777}, + new uint[]{7779}, + new uint[]{7779}, + new uint[]{7779}, + new uint[]{7780}, + new uint[]{7780}, + new uint[]{7781}, + new uint[]{7783}, + new uint[]{7782}, + new uint[]{7782}, + new uint[]{7785}, + new uint[]{7784}, + new uint[]{7784}, + new uint[]{7786}, + new uint[]{7787}, + new uint[]{7788}, + new uint[]{7789}, + new uint[]{7790}, + new uint[]{7791}, + new uint[]{7792}, + new uint[]{7793}, + new uint[]{7794}, + new uint[]{7795}, + new uint[]{7796}, + new uint[]{7797}, + new uint[]{7798}, + new uint[]{7799}, + new uint[]{7800}, + new uint[]{7801}, + new uint[]{7802}, + new uint[]{7803}, + new uint[]{7804}, + new uint[]{7805}, + new uint[]{7806}, + new uint[]{7807}, + new uint[]{7808}, + new uint[]{7809}, + new uint[]{7810}, + new uint[]{7811}, + new uint[]{7812}, + new uint[]{7813}, + new uint[]{7814}, + new uint[]{7815}, + new uint[]{7816}, + new uint[]{7817}, + new uint[]{7818}, + new uint[]{7819}, + new uint[]{7820}, + new uint[]{7821}, + new uint[]{7822}, + new uint[]{7823}, + new uint[]{7824}, + new uint[]{7825}, + new uint[]{7826}, + new uint[]{7827}, + new uint[]{7828}, + new uint[]{7829}, + new uint[]{7830}, + new uint[]{7831}, + new uint[]{7832}, + new uint[]{7833}, + new uint[]{7834}, + new uint[]{7835}, + new uint[]{7836}, + new uint[]{7837}, + new uint[]{7838}, + new uint[]{7839}, + new uint[]{7840}, + new uint[]{7841}, + new uint[]{7842}, + new uint[]{7843}, + new uint[]{7844}, + new uint[]{7845}, + new uint[]{7846}, + new uint[]{7847}, + new uint[]{7848}, + new uint[]{7849}, + new uint[]{7658}, + new uint[]{7578}, + new uint[]{7579}, + new uint[]{7580}, + new uint[]{7581}, + new uint[]{7939}, + Array.Empty(), + new uint[]{7856}, + new uint[]{108}, + new uint[]{7871}, + new uint[]{7872}, + new uint[]{7873}, + new uint[]{7874}, + new uint[]{8264}, + new uint[]{7876}, + new uint[]{7877}, + new uint[]{7878}, + new uint[]{7879}, + new uint[]{7880}, + new uint[]{7881}, + new uint[]{7882}, + Array.Empty(), + new uint[]{7884}, + new uint[]{7885}, + new uint[]{7886}, + new uint[]{7887}, + new uint[]{7888}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{6154}, + new uint[]{7715}, + new uint[]{6196}, + new uint[]{6336}, + new uint[]{2823}, + new uint[]{7718}, + new uint[]{7585}, + new uint[]{7586}, + new uint[]{7587}, + new uint[]{7588}, + new uint[]{7589}, + new uint[]{7590}, + new uint[]{7591}, + new uint[]{7627}, + new uint[]{7593}, + new uint[]{7622}, + new uint[]{7595}, + new uint[]{7596}, + new uint[]{7597}, + new uint[]{7598}, + new uint[]{7599}, + new uint[]{7600}, + new uint[]{7601}, + new uint[]{7602}, + new uint[]{7603}, + new uint[]{7604}, + new uint[]{7605}, + new uint[]{7606}, + new uint[]{7607}, + new uint[]{7608}, + new uint[]{7609}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7759}, + new uint[]{8265}, + new uint[]{7732}, + new uint[]{7733}, + Array.Empty(), + Array.Empty(), + new uint[]{7731}, + new uint[]{7731}, + new uint[]{9147}, + new uint[]{7628}, + new uint[]{7727}, + new uint[]{7728}, + new uint[]{7620}, + new uint[]{7592}, + new uint[]{7594}, + new uint[]{7621}, + new uint[]{7743}, + new uint[]{7744}, + new uint[]{7629}, + new uint[]{7742}, + new uint[]{7630}, + new uint[]{7747}, + new uint[]{7623}, + new uint[]{7631}, + new uint[]{7719}, + new uint[]{7745}, + new uint[]{7720}, + new uint[]{7721}, + new uint[]{7730}, + new uint[]{7756}, + new uint[]{7757}, + new uint[]{7758}, + new uint[]{7722}, + new uint[]{7702}, + new uint[]{7723}, + new uint[]{7598}, + new uint[]{7598}, + new uint[]{7598}, + new uint[]{7764}, + new uint[]{7724}, + new uint[]{7248}, + new uint[]{7765}, + new uint[]{7766}, + Array.Empty(), + new uint[]{7751}, + new uint[]{7752}, + new uint[]{7248}, + new uint[]{7768}, + new uint[]{7769}, + new uint[]{7770}, + new uint[]{7767}, + Array.Empty(), + new uint[]{7750}, + new uint[]{7750}, + new uint[]{7750}, + new uint[]{7748}, + new uint[]{7749}, + Array.Empty(), + Array.Empty(), + new uint[]{7248}, + Array.Empty(), + new uint[]{7737}, + new uint[]{7738}, + Array.Empty(), + new uint[]{7761}, + new uint[]{7762}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{7741}, + new uint[]{7696}, + new uint[]{7696}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{7755}, + new uint[]{7718}, + new uint[]{7642}, + new uint[]{7642}, + new uint[]{7726}, + Array.Empty(), + new uint[]{7850}, + new uint[]{7851}, + new uint[]{7739}, + new uint[]{7739}, + new uint[]{7852}, + new uint[]{7739}, + new uint[]{7769}, + new uint[]{7909}, + new uint[]{7853}, + new uint[]{9066}, + new uint[]{108}, + new uint[]{108}, + new uint[]{7664}, + new uint[]{7857}, + new uint[]{7858}, + new uint[]{7861}, + new uint[]{7862}, + Array.Empty(), + Array.Empty(), + new uint[]{7861}, + new uint[]{7664}, + new uint[]{7919}, + new uint[]{7920}, + new uint[]{7921}, + Array.Empty(), + new uint[]{8336}, + new uint[]{7919}, + new uint[]{8076}, + new uint[]{8077}, + Array.Empty(), + new uint[]{8078}, + new uint[]{8079}, + new uint[]{8080}, + new uint[]{8081}, + new uint[]{8082}, + new uint[]{8083}, + Array.Empty(), + Array.Empty(), + new uint[]{8086}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{8089}, + new uint[]{8090}, + new uint[]{8092}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{8085}, + Array.Empty(), + new uint[]{8084}, + new uint[]{8087}, + new uint[]{8088}, + new uint[]{7899}, + new uint[]{7900}, + new uint[]{7901}, + new uint[]{7855}, + new uint[]{7946}, + new uint[]{7976}, + new uint[]{7977}, + new uint[]{7978}, + new uint[]{7979}, + new uint[]{7980}, + new uint[]{7947}, + new uint[]{7948}, + new uint[]{7950}, + new uint[]{7949}, + new uint[]{7951}, + new uint[]{7952}, + new uint[]{7953}, + new uint[]{7981}, + new uint[]{7982}, + new uint[]{7983}, + new uint[]{7984}, + new uint[]{7922}, + new uint[]{7930}, + new uint[]{7923}, + new uint[]{7924}, + new uint[]{7925}, + new uint[]{7927}, + new uint[]{7928}, + new uint[]{7929}, + new uint[]{7922}, + new uint[]{7930}, + new uint[]{7927}, + new uint[]{7928}, + new uint[]{7926}, + new uint[]{7929}, + new uint[]{7906}, + new uint[]{7891}, + new uint[]{7892}, + new uint[]{7570}, + new uint[]{7571}, + new uint[]{7572}, + new uint[]{7573}, + new uint[]{7574}, + new uint[]{7575}, + new uint[]{7576}, + new uint[]{7657}, + new uint[]{7973}, + new uint[]{7974}, + new uint[]{7975}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{7879}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{7875}, + new uint[]{8922}, + new uint[]{7871}, + new uint[]{7872}, + Array.Empty(), + new uint[]{7874}, + new uint[]{7875}, + new uint[]{7879}, + new uint[]{7880}, + new uint[]{108}, + new uint[]{7973}, + new uint[]{10216}, + new uint[]{8922}, + new uint[]{750}, + new uint[]{7931}, + new uint[]{7932}, + new uint[]{7933}, + new uint[]{7934}, + new uint[]{7935}, + new uint[]{7936}, + new uint[]{7937}, + new uint[]{8133}, + Array.Empty(), + new uint[]{7976}, + new uint[]{7981, 7982}, + new uint[]{7915, 7919}, + new uint[]{108, 7916}, + new uint[]{108, 7917}, + new uint[]{7865}, + new uint[]{7866}, + new uint[]{7867}, + new uint[]{7868}, + new uint[]{7869}, + new uint[]{7870}, + new uint[]{8922}, + new uint[]{7912}, + new uint[]{7913}, + new uint[]{7914}, + new uint[]{7911}, + new uint[]{7918}, + new uint[]{7910}, + new uint[]{7908}, + new uint[]{7985}, + new uint[]{7986}, + new uint[]{7987}, + new uint[]{7988}, + new uint[]{7989}, + new uint[]{7990}, + new uint[]{7991}, + new uint[]{8922}, + new uint[]{7992}, + new uint[]{7993}, + new uint[]{7994}, + new uint[]{7995}, + new uint[]{7996}, + new uint[]{7997}, + new uint[]{7998}, + new uint[]{7999}, + new uint[]{8000}, + new uint[]{8001}, + new uint[]{8002}, + new uint[]{8003}, + new uint[]{8004}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{8922}, + new uint[]{7930}, + new uint[]{7930}, + new uint[]{7930}, + new uint[]{7930}, + new uint[]{7968}, + new uint[]{7969}, + new uint[]{7968}, + new uint[]{7970}, + new uint[]{7971}, + new uint[]{7970}, + new uint[]{7970}, + new uint[]{7968}, + new uint[]{7970}, + new uint[]{7871}, + new uint[]{7872}, + new uint[]{7873}, + new uint[]{7874}, + new uint[]{7875}, + new uint[]{7879}, + new uint[]{8922}, + new uint[]{8922}, + new uint[]{7972}, + new uint[]{7902}, + new uint[]{7903}, + new uint[]{5915}, + new uint[]{5915}, + new uint[]{7904}, + new uint[]{7970}, + new uint[]{7930}, + Array.Empty(), + Array.Empty(), + new uint[]{7985}, + new uint[]{7986}, + new uint[]{7991}, + new uint[]{8129}, + new uint[]{8252}, + new uint[]{8250}, + new uint[]{8249}, + new uint[]{8248}, + new uint[]{7915, 7916, 7917}, + new uint[]{8251}, + new uint[]{8129}, + new uint[]{8129}, + new uint[]{8132}, + new uint[]{8130}, + new uint[]{9040}, + Array.Empty(), + new uint[]{8060}, + new uint[]{8061}, + new uint[]{8061}, + new uint[]{8063}, + new uint[]{7664}, + new uint[]{6039}, + new uint[]{7537}, + new uint[]{6039}, + new uint[]{7537}, + Array.Empty(), + new uint[]{6042}, + new uint[]{6040}, + Array.Empty(), + new uint[]{6040}, + new uint[]{3293}, + new uint[]{3211}, + new uint[]{5574}, + new uint[]{7036}, + new uint[]{7867}, + new uint[]{6094}, + new uint[]{6094}, + new uint[]{7941}, + new uint[]{8015}, + new uint[]{8011}, + new uint[]{8014}, + new uint[]{8008}, + new uint[]{8012}, + new uint[]{8013}, + new uint[]{8009}, + new uint[]{8010}, + new uint[]{8016}, + new uint[]{8017}, + new uint[]{8018}, + new uint[]{8019}, + new uint[]{8020}, + new uint[]{8021}, + new uint[]{8022}, + new uint[]{8023}, + new uint[]{8024}, + new uint[]{8025}, + new uint[]{8026}, + new uint[]{8027}, + new uint[]{8028}, + new uint[]{8029}, + new uint[]{8030}, + new uint[]{8031}, + new uint[]{8032}, + new uint[]{8033}, + new uint[]{8034}, + new uint[]{8035}, + new uint[]{8036}, + new uint[]{8037}, + new uint[]{8038}, + new uint[]{8039}, + new uint[]{8040}, + new uint[]{8041}, + new uint[]{8042}, + new uint[]{8043}, + new uint[]{8044}, + new uint[]{8045}, + new uint[]{8046}, + new uint[]{8047}, + new uint[]{8048}, + new uint[]{8049}, + new uint[]{8050}, + new uint[]{8051}, + new uint[]{5945}, + new uint[]{8052}, + new uint[]{8053}, + new uint[]{8054}, + new uint[]{7176}, + new uint[]{7471}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7176}, + new uint[]{7471}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7176}, + new uint[]{7471}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{7176}, + new uint[]{7471}, + new uint[]{7469}, + new uint[]{7470}, + new uint[]{8055}, + new uint[]{8055}, + new uint[]{8055}, + new uint[]{8056}, + new uint[]{8056}, + new uint[]{8056}, + new uint[]{8057}, + new uint[]{8057}, + new uint[]{8057}, + new uint[]{8058}, + new uint[]{8058}, + new uint[]{8058}, + new uint[]{8059}, + new uint[]{8059}, + new uint[]{8059}, + new uint[]{8059}, + new uint[]{7871}, + new uint[]{7872}, + new uint[]{7873}, + new uint[]{7874}, + new uint[]{8487}, + new uint[]{8064}, + new uint[]{8065}, + new uint[]{8112}, + new uint[]{8112}, + new uint[]{8113}, + new uint[]{8104}, + new uint[]{8105}, + new uint[]{8106}, + new uint[]{8107}, + new uint[]{8109}, + new uint[]{8110}, + new uint[]{8111}, + new uint[]{8122}, + new uint[]{8101}, + new uint[]{8123}, + new uint[]{8090}, + new uint[]{8091}, + new uint[]{8091}, + new uint[]{8093}, + new uint[]{8094}, + new uint[]{8093}, + new uint[]{8094}, + new uint[]{8095}, + new uint[]{8094}, + new uint[]{8140}, + new uint[]{8096}, + new uint[]{8097}, + new uint[]{8098}, + new uint[]{8140}, + new uint[]{8097}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{8078}, + new uint[]{8079}, + new uint[]{8080}, + new uint[]{8081}, + new uint[]{8082}, + new uint[]{8083}, + new uint[]{8100}, + new uint[]{8101}, + new uint[]{8101}, + new uint[]{8103}, + new uint[]{8102}, + new uint[]{8103}, + new uint[]{8108}, + new uint[]{8108}, + new uint[]{8114}, + new uint[]{8115}, + new uint[]{8087}, + new uint[]{8114}, + new uint[]{8115}, + new uint[]{8116}, + new uint[]{8116}, + new uint[]{4916}, + new uint[]{8117}, + new uint[]{8117}, + new uint[]{8118}, + new uint[]{3046}, + new uint[]{3047}, + new uint[]{3046}, + new uint[]{3047}, + new uint[]{8119}, + new uint[]{8120}, + new uint[]{8121}, + new uint[]{8120}, + new uint[]{8124}, + new uint[]{8132}, + new uint[]{8126}, + new uint[]{8127}, + new uint[]{8128}, + new uint[]{8126}, + new uint[]{8127}, + new uint[]{8125}, + new uint[]{8127}, + new uint[]{7155}, + new uint[]{8068}, + Array.Empty(), + new uint[]{7954}, + new uint[]{108}, + new uint[]{8066}, + new uint[]{8067}, + new uint[]{8069}, + new uint[]{8069}, + new uint[]{7967}, + new uint[]{7966}, + new uint[]{7965}, + new uint[]{7248}, + new uint[]{8005}, + new uint[]{8005}, + new uint[]{8006}, + new uint[]{8007}, + new uint[]{7248}, + Array.Empty(), + Array.Empty(), + new uint[]{8778}, + new uint[]{7248}, + new uint[]{7248}, + new uint[]{4130, 11264}, + new uint[]{5239, 11265}, + new uint[]{713, 11266}, + new uint[]{8917}, + new uint[]{1492, 11267}, + new uint[]{8378, 11268}, + new uint[]{8889, 11269}, + new uint[]{8919}, + new uint[]{8650}, + new uint[]{8650}, + new uint[]{8650}, + new uint[]{8070}, + new uint[]{8071}, + new uint[]{6041}, + new uint[]{6042}, + new uint[]{8072}, + new uint[]{8073}, + new uint[]{8074}, + new uint[]{8075}, + new uint[]{7955}, + new uint[]{7956}, + new uint[]{7957}, + new uint[]{8061}, + new uint[]{8062}, + new uint[]{7967}, + new uint[]{7967}, + new uint[]{7964}, + new uint[]{7964}, + new uint[]{7964}, + new uint[]{7966}, + new uint[]{7961}, + new uint[]{7962}, + new uint[]{7963}, + new uint[]{7965}, + new uint[]{7960}, + new uint[]{7959}, + new uint[]{7965}, + new uint[]{7926}, + new uint[]{108}, + new uint[]{8925}, + Array.Empty(), + new uint[]{8061}, + new uint[]{8131}, + Array.Empty(), + new uint[]{8061}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{7956}, + new uint[]{7956}, + new uint[]{7923}, + new uint[]{7924}, + new uint[]{7925}, + new uint[]{7965}, + new uint[]{8299}, + new uint[]{8300}, + new uint[]{8301}, + new uint[]{7176}, + new uint[]{7471}, + new uint[]{7469}, + new uint[]{8135}, + new uint[]{8134}, + new uint[]{8136}, + new uint[]{8137}, + new uint[]{8139}, + new uint[]{8138}, + new uint[]{8183}, + new uint[]{8184}, + new uint[]{8185}, + new uint[]{8186}, + new uint[]{8187}, + new uint[]{8188}, + new uint[]{8189}, + new uint[]{8190}, + new uint[]{8191}, + new uint[]{8192}, + new uint[]{8193}, + new uint[]{8194}, + new uint[]{8195}, + new uint[]{8196}, + new uint[]{8197}, + new uint[]{8198}, + new uint[]{8199}, + new uint[]{8200}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108, 8210}, + new uint[]{8235}, + new uint[]{8236}, + new uint[]{8234}, + new uint[]{8826}, + new uint[]{8231}, + new uint[]{8232}, + new uint[]{8233}, + new uint[]{8935}, + new uint[]{8981}, + new uint[]{8455}, + Array.Empty(), + new uint[]{8201}, + new uint[]{8826}, + new uint[]{8202}, + new uint[]{8203}, + new uint[]{8204}, + new uint[]{8205}, + new uint[]{8206}, + new uint[]{8207}, + new uint[]{8208}, + new uint[]{8209}, + new uint[]{8456}, + new uint[]{8210}, + new uint[]{8479, 8480, 8483}, + new uint[]{8211}, + new uint[]{8484}, + new uint[]{8325}, + new uint[]{8469}, + new uint[]{541}, + new uint[]{5978}, + new uint[]{8141}, + new uint[]{8260}, + new uint[]{108}, + new uint[]{8261}, + new uint[]{7864}, + new uint[]{8262}, + new uint[]{8262}, + new uint[]{108}, + new uint[]{8162}, + new uint[]{8163}, + new uint[]{8164}, + new uint[]{8147}, + new uint[]{8148}, + new uint[]{8149}, + new uint[]{8150}, + new uint[]{8151}, + new uint[]{8152}, + new uint[]{8153}, + new uint[]{8154}, + new uint[]{8856}, + new uint[]{8156}, + new uint[]{8157}, + new uint[]{8158}, + new uint[]{8159}, + new uint[]{8160}, + new uint[]{8161}, + new uint[]{8361}, + new uint[]{8357}, + new uint[]{8356}, + new uint[]{8355}, + new uint[]{8359}, + new uint[]{8358}, + new uint[]{8357}, + new uint[]{8356}, + new uint[]{8360}, + new uint[]{8354}, + new uint[]{8361}, + new uint[]{8357}, + new uint[]{8356}, + new uint[]{8360}, + new uint[]{8359}, + new uint[]{8357}, + new uint[]{8356}, + new uint[]{8360}, + new uint[]{8169}, + new uint[]{8170}, + new uint[]{7062}, + new uint[]{8172}, + new uint[]{8173}, + new uint[]{8174}, + new uint[]{8175}, + new uint[]{8176}, + new uint[]{8177}, + new uint[]{8178}, + new uint[]{8179}, + new uint[]{8180}, + new uint[]{8181}, + new uint[]{8182}, + new uint[]{8165}, + new uint[]{8166}, + new uint[]{8167}, + new uint[]{8569}, + new uint[]{8155}, + new uint[]{8571}, + new uint[]{8572}, + new uint[]{8573}, + new uint[]{8574}, + new uint[]{8575}, + new uint[]{8576}, + new uint[]{8577}, + new uint[]{8578}, + new uint[]{8579}, + new uint[]{8788}, + new uint[]{8581}, + new uint[]{8582}, + new uint[]{8583}, + new uint[]{8584}, + new uint[]{8585}, + new uint[]{8586}, + new uint[]{8587}, + new uint[]{8588}, + new uint[]{8589}, + new uint[]{8590}, + new uint[]{8459}, + new uint[]{8592}, + new uint[]{8653}, + new uint[]{8654}, + new uint[]{8655}, + new uint[]{8656}, + new uint[]{8596}, + new uint[]{8597}, + new uint[]{8598}, + new uint[]{8599}, + new uint[]{8600}, + new uint[]{8601}, + new uint[]{8789}, + new uint[]{8603}, + new uint[]{8604}, + new uint[]{8605}, + new uint[]{8606}, + new uint[]{8607}, + new uint[]{8608}, + new uint[]{8609}, + new uint[]{8610}, + new uint[]{8611}, + new uint[]{8612}, + new uint[]{8613}, + new uint[]{8614}, + new uint[]{8615}, + new uint[]{8616}, + new uint[]{8591}, + new uint[]{8890}, + new uint[]{8891}, + new uint[]{8892}, + new uint[]{8893}, + new uint[]{8894}, + new uint[]{8618}, + new uint[]{8619}, + new uint[]{8620}, + new uint[]{8621}, + new uint[]{8622}, + new uint[]{8638}, + new uint[]{8623}, + new uint[]{8624}, + new uint[]{8625}, + new uint[]{8626}, + new uint[]{8627}, + new uint[]{8628}, + new uint[]{8629}, + Array.Empty(), + new uint[]{8630}, + new uint[]{8631}, + new uint[]{8632}, + new uint[]{8633}, + new uint[]{8634}, + new uint[]{8635}, + Array.Empty(), + new uint[]{8895}, + new uint[]{8896}, + new uint[]{8897}, + new uint[]{8898}, + new uint[]{8899}, + new uint[]{8657}, + new uint[]{8543}, + new uint[]{8544}, + Array.Empty(), + new uint[]{8545}, + new uint[]{8546}, + new uint[]{8547}, + new uint[]{8548}, + new uint[]{8549}, + new uint[]{8550}, + new uint[]{8551}, + new uint[]{8552}, + new uint[]{8553}, + new uint[]{8554}, + new uint[]{8555}, + new uint[]{8556}, + new uint[]{8557}, + new uint[]{8558}, + new uint[]{8559}, + new uint[]{8560}, + new uint[]{8561}, + new uint[]{8562}, + new uint[]{8563}, + new uint[]{8564}, + new uint[]{8565}, + new uint[]{8566}, + new uint[]{8567}, + new uint[]{8568}, + new uint[]{8900}, + new uint[]{8901}, + new uint[]{8902}, + new uint[]{8903}, + new uint[]{8904}, + new uint[]{8358}, + new uint[]{8213}, + new uint[]{8498}, + new uint[]{8499}, + new uint[]{8500}, + new uint[]{8501}, + Array.Empty(), + new uint[]{8502}, + new uint[]{8503}, + new uint[]{8504}, + new uint[]{8505}, + new uint[]{8506}, + new uint[]{8507}, + new uint[]{8508}, + new uint[]{8509}, + new uint[]{8786}, + new uint[]{8511}, + new uint[]{8512}, + new uint[]{8513}, + new uint[]{8514}, + new uint[]{8515}, + new uint[]{8516}, + new uint[]{8905}, + new uint[]{8906}, + new uint[]{8907}, + new uint[]{8908}, + new uint[]{8909}, + Array.Empty(), + Array.Empty(), + new uint[]{8570}, + new uint[]{8299}, + new uint[]{8517}, + new uint[]{8518}, + new uint[]{8519}, + new uint[]{8520}, + new uint[]{8521}, + new uint[]{8522}, + new uint[]{8523}, + new uint[]{8524}, + new uint[]{8525}, + new uint[]{8791}, + new uint[]{8526}, + new uint[]{8527}, + new uint[]{8528}, + new uint[]{8529}, + new uint[]{8787}, + new uint[]{8531}, + new uint[]{8532}, + new uint[]{8533}, + new uint[]{8534}, + new uint[]{8535}, + new uint[]{8536}, + new uint[]{8537}, + new uint[]{8538}, + new uint[]{8539}, + new uint[]{8540}, + new uint[]{8541}, + new uint[]{8542}, + new uint[]{8913}, + new uint[]{8914}, + new uint[]{8911}, + new uint[]{8912}, + new uint[]{8915}, + new uint[]{8310}, + new uint[]{8264}, + new uint[]{8388}, + new uint[]{8308}, + new uint[]{8303}, + new uint[]{8302}, + new uint[]{8306}, + new uint[]{4130}, + new uint[]{5978}, + new uint[]{5239}, + new uint[]{729}, + new uint[]{1492}, + new uint[]{713}, + new uint[]{8917}, + new uint[]{8918}, + new uint[]{8279}, + new uint[]{8275}, + new uint[]{8278}, + new uint[]{8274}, + new uint[]{8276}, + new uint[]{8277}, + new uint[]{8288}, + new uint[]{8280}, + new uint[]{8281}, + new uint[]{8287}, + new uint[]{8285}, + new uint[]{8283}, + new uint[]{8284}, + new uint[]{8282}, + new uint[]{8286}, + new uint[]{8292}, + new uint[]{8291}, + new uint[]{8289}, + new uint[]{8293}, + new uint[]{8290}, + new uint[]{8273}, + new uint[]{108}, + new uint[]{8214}, + new uint[]{8215}, + new uint[]{8216}, + new uint[]{8217}, + new uint[]{8218}, + new uint[]{8219}, + Array.Empty(), + new uint[]{8221}, + new uint[]{8222}, + new uint[]{8223}, + new uint[]{8224}, + new uint[]{8225}, + new uint[]{8226}, + new uint[]{8154}, + new uint[]{8263}, + new uint[]{8264}, + new uint[]{8265}, + new uint[]{8262}, + new uint[]{8271}, + new uint[]{8266}, + new uint[]{8267}, + new uint[]{8268}, + new uint[]{8269}, + new uint[]{8270}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{8227}, + new uint[]{8228}, + new uint[]{8229}, + new uint[]{8230}, + new uint[]{8353}, + new uint[]{8394}, + new uint[]{8268}, + new uint[]{8353}, + Array.Empty(), + new uint[]{8391}, + new uint[]{8390}, + new uint[]{8389}, + new uint[]{8353}, + Array.Empty(), + new uint[]{8391}, + new uint[]{8389}, + new uint[]{8389}, + new uint[]{8394}, + new uint[]{8268}, + new uint[]{8353}, + new uint[]{8379}, + new uint[]{8382}, + new uint[]{8381}, + new uint[]{8380}, + new uint[]{8382}, + new uint[]{8381}, + new uint[]{8380}, + new uint[]{8382}, + new uint[]{8382}, + new uint[]{8258}, + new uint[]{8256}, + new uint[]{8255}, + new uint[]{8254}, + new uint[]{8253}, + new uint[]{8339}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8338}, + new uint[]{8337}, + new uint[]{8922}, + new uint[]{8923}, + new uint[]{8924}, + Array.Empty(), + new uint[]{8238}, + new uint[]{8239}, + Array.Empty(), + new uint[]{8241}, + Array.Empty(), + new uint[]{8243}, + new uint[]{8244}, + new uint[]{8245}, + new uint[]{8246}, + new uint[]{8247}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8302}, + new uint[]{8303}, + new uint[]{8304}, + Array.Empty(), + new uint[]{8306}, + new uint[]{8307}, + new uint[]{8308}, + new uint[]{8309}, + new uint[]{8310}, + new uint[]{8311}, + new uint[]{108}, + new uint[]{7864}, + new uint[]{8304}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8351}, + new uint[]{8352}, + new uint[]{8822}, + new uint[]{8378}, + new uint[]{8377}, + new uint[]{8376}, + new uint[]{8375}, + new uint[]{8374}, + new uint[]{8373}, + new uint[]{8372}, + new uint[]{8371}, + new uint[]{8645}, + new uint[]{8154}, + new uint[]{8649}, + new uint[]{8648}, + new uint[]{8374}, + new uint[]{8647}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8399}, + new uint[]{8398}, + new uint[]{729}, + new uint[]{713}, + new uint[]{8889}, + new uint[]{1492}, + new uint[]{4130}, + new uint[]{5239}, + new uint[]{8645}, + new uint[]{8645}, + new uint[]{8925}, + new uint[]{8778}, + new uint[]{8778}, + new uint[]{8486}, + new uint[]{8486}, + new uint[]{8264}, + new uint[]{8258}, + new uint[]{8312}, + new uint[]{8254}, + new uint[]{8369}, + new uint[]{8368}, + Array.Empty(), + Array.Empty(), + new uint[]{8365}, + new uint[]{8364}, + new uint[]{8363}, + new uint[]{8362}, + new uint[]{8643}, + new uint[]{8644}, + new uint[]{8643}, + new uint[]{8644}, + new uint[]{8643}, + new uint[]{8644}, + new uint[]{8318}, + new uint[]{8314}, + new uint[]{8330}, + new uint[]{8316}, + new uint[]{8329}, + new uint[]{8315}, + new uint[]{8328}, + new uint[]{8317}, + new uint[]{8331}, + new uint[]{8332}, + new uint[]{8333}, + new uint[]{8910}, + new uint[]{8370}, + new uint[]{8350}, + new uint[]{8348}, + new uint[]{8347}, + new uint[]{8350}, + new uint[]{8349}, + new uint[]{8348}, + new uint[]{8347}, + new uint[]{8346}, + new uint[]{8379}, + new uint[]{8234}, + new uint[]{8778}, + new uint[]{8374}, + new uint[]{108}, + new uint[]{8919}, + new uint[]{4130}, + new uint[]{5239}, + new uint[]{8917}, + new uint[]{8374}, + new uint[]{8399}, + new uint[]{5978}, + new uint[]{5978}, + new uint[]{8651}, + new uint[]{8650}, + new uint[]{8308}, + new uint[]{8264}, + new uint[]{8310}, + new uint[]{8219}, + new uint[]{8269}, + new uint[]{8311}, + new uint[]{8396}, + new uint[]{8395}, + new uint[]{8394}, + new uint[]{8307}, + new uint[]{8258}, + new uint[]{8255}, + new uint[]{8312}, + new uint[]{8393}, + new uint[]{8214}, + new uint[]{8476, 8478, 8593, 8959}, + new uint[]{8594, 8960}, + new uint[]{8595, 8961}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + Array.Empty(), + new uint[]{8645}, + new uint[]{8921}, + new uint[]{8645}, + new uint[]{8645}, + new uint[]{8920}, + new uint[]{8921}, + new uint[]{8645, 8920, 8921}, + new uint[]{8646}, + new uint[]{8929}, + new uint[]{8930}, + new uint[]{8931}, + new uint[]{8932}, + Array.Empty(), + new uint[]{8933}, + new uint[]{8486}, + new uint[]{8486}, + new uint[]{8486}, + new uint[]{8486}, + new uint[]{8776}, + new uint[]{8777}, + new uint[]{8778}, + new uint[]{8777}, + new uint[]{8488}, + new uint[]{8489}, + new uint[]{8490}, + new uint[]{8491}, + new uint[]{8492}, + new uint[]{8489}, + new uint[]{8776}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8748}, + new uint[]{8781, 8782, 8784}, + new uint[]{8780}, + new uint[]{8780}, + new uint[]{8781}, + new uint[]{8782}, + new uint[]{8783}, + new uint[]{8784}, + new uint[]{8785}, + new uint[]{108}, + new uint[]{8374}, + new uint[]{8931}, + new uint[]{8652}, + new uint[]{8258}, + new uint[]{8257}, + new uint[]{8256}, + new uint[]{8254}, + new uint[]{8312}, + new uint[]{8397}, + new uint[]{8636}, + new uint[]{8637}, + new uint[]{8858}, + new uint[]{8859}, + new uint[]{8860}, + new uint[]{8861}, + new uint[]{8862}, + new uint[]{8863}, + new uint[]{8864}, + new uint[]{8865}, + new uint[]{8866}, + new uint[]{8867}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8374}, + new uint[]{8916}, + new uint[]{8682}, + new uint[]{8683}, + new uint[]{8684}, + new uint[]{8685}, + new uint[]{8686}, + new uint[]{8687}, + new uint[]{8688}, + new uint[]{8489}, + new uint[]{8395, 8777}, + Array.Empty(), + new uint[]{108}, + new uint[]{8803}, + new uint[]{8868}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8310}, + new uint[]{8308}, + new uint[]{8306}, + new uint[]{8869}, + new uint[]{8870}, + new uint[]{8871}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8640}, + new uint[]{8641}, + new uint[]{8642}, + new uint[]{8689}, + new uint[]{8690}, + new uint[]{8691}, + new uint[]{8692}, + new uint[]{8693}, + new uint[]{8694}, + new uint[]{8695}, + new uint[]{8696}, + new uint[]{8697}, + new uint[]{8698}, + new uint[]{8699}, + new uint[]{8700}, + new uint[]{8701}, + new uint[]{8702}, + new uint[]{8703}, + new uint[]{8704}, + new uint[]{8705}, + new uint[]{8706}, + new uint[]{8707}, + new uint[]{8708}, + new uint[]{8709}, + new uint[]{8710}, + new uint[]{8711}, + new uint[]{8712}, + Array.Empty(), + new uint[]{8713}, + new uint[]{8714}, + new uint[]{8715}, + new uint[]{8785}, + new uint[]{8639}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8872}, + new uint[]{8872}, + new uint[]{8874}, + new uint[]{8875}, + new uint[]{8864}, + new uint[]{8876}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8858}, + new uint[]{8346}, + new uint[]{8400}, + new uint[]{8401}, + new uint[]{8402}, + new uint[]{8403}, + new uint[]{8404}, + new uint[]{8405}, + new uint[]{8406}, + new uint[]{8407}, + new uint[]{8408}, + new uint[]{8409}, + new uint[]{8410}, + new uint[]{8411}, + new uint[]{8412}, + new uint[]{8413}, + new uint[]{8414}, + new uint[]{8414}, + new uint[]{8414}, + new uint[]{8417}, + new uint[]{8418}, + new uint[]{8419}, + new uint[]{8420}, + new uint[]{8421}, + new uint[]{8422}, + new uint[]{8423}, + new uint[]{8424}, + new uint[]{8425}, + new uint[]{8426}, + new uint[]{8427}, + new uint[]{8428}, + new uint[]{8429}, + new uint[]{8430}, + new uint[]{8431}, + new uint[]{8432}, + new uint[]{8433}, + new uint[]{8434}, + new uint[]{8435}, + new uint[]{8823}, + new uint[]{8436}, + new uint[]{8437}, + new uint[]{8438}, + new uint[]{8439}, + new uint[]{8440}, + new uint[]{8441}, + new uint[]{8442}, + new uint[]{8443}, + new uint[]{8444}, + new uint[]{8445}, + new uint[]{8446}, + new uint[]{8447}, + new uint[]{8448}, + new uint[]{8449}, + new uint[]{8450}, + new uint[]{8451}, + new uint[]{8452}, + new uint[]{8323}, + new uint[]{8324}, + new uint[]{8485}, + new uint[]{8319}, + new uint[]{8817}, + new uint[]{8818}, + new uint[]{8816}, + new uint[]{8320}, + new uint[]{8934}, + new uint[]{8322}, + new uint[]{108}, + new uint[]{8313}, + new uint[]{8815}, + new uint[]{8814}, + new uint[]{8813}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8302}, + new uint[]{8821}, + new uint[]{8819}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{8234}, + Array.Empty(), + new uint[]{8352}, + new uint[]{8453}, + new uint[]{8454}, + new uint[]{8457}, + new uint[]{8458}, + new uint[]{8463}, + new uint[]{8467}, + new uint[]{8662, 8663, 8664}, + new uint[]{8662}, + new uint[]{8663}, + new uint[]{9029}, + new uint[]{9029}, + new uint[]{8670}, + new uint[]{8673}, + new uint[]{8680}, + new uint[]{8234}, + new uint[]{8922}, + new uint[]{8922}, + new uint[]{8327}, + new uint[]{8334}, + new uint[]{8335}, + new uint[]{8842, 8843, 8844, 8845}, + new uint[]{8842, 8843, 8845}, + new uint[]{8842, 8843, 8844, 8845}, + new uint[]{8842, 8843, 8844, 8845}, + new uint[]{8842, 8843, 8845}, + new uint[]{8842, 8845}, + new uint[]{8848}, + new uint[]{8849}, + new uint[]{8850}, + new uint[]{8851}, + new uint[]{8854}, + new uint[]{8852}, + new uint[]{8853}, + new uint[]{8855}, + new uint[]{108}, + new uint[]{8434}, + new uint[]{8493}, + new uint[]{8494}, + new uint[]{8495}, + new uint[]{8496}, + new uint[]{8497}, + new uint[]{8488}, + new uint[]{8489}, + new uint[]{108, 8493}, + new uint[]{8493}, + new uint[]{8795}, + new uint[]{8782}, + Array.Empty(), + new uint[]{8872}, + new uint[]{8872}, + new uint[]{8872}, + new uint[]{8872}, + new uint[]{8872}, + new uint[]{8872}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{8795}, + new uint[]{108}, + new uint[]{8796}, + new uint[]{108, 8796}, + new uint[]{8825}, + new uint[]{8824}, + new uint[]{108, 2186}, + new uint[]{8488}, + new uint[]{8846}, + new uint[]{8838}, + new uint[]{8839}, + new uint[]{8840}, + new uint[]{8841}, + new uint[]{8305}, + new uint[]{8847}, + new uint[]{8493}, + new uint[]{8485}, + new uint[]{8799}, + new uint[]{8798, 8823}, + new uint[]{108}, + new uint[]{8374}, + new uint[]{8779}, + new uint[]{8796}, + new uint[]{8797}, + new uint[]{8951}, + new uint[]{8952}, + new uint[]{8964}, + new uint[]{8830}, + new uint[]{8834}, + new uint[]{8835}, + new uint[]{108}, + new uint[]{8323}, + new uint[]{8812}, + new uint[]{8810}, + new uint[]{8809}, + new uint[]{8808}, + new uint[]{8807}, + new uint[]{8806}, + new uint[]{8800}, + new uint[]{8805}, + new uint[]{108, 8395}, + new uint[]{8927}, + new uint[]{8926}, + new uint[]{8716}, + new uint[]{8717}, + new uint[]{8718}, + new uint[]{8719}, + new uint[]{8720}, + new uint[]{8721}, + new uint[]{8722}, + new uint[]{8723}, + new uint[]{8724}, + new uint[]{8725}, + new uint[]{8726}, + new uint[]{8727}, + new uint[]{8728}, + new uint[]{8729}, + new uint[]{8730}, + new uint[]{8731}, + new uint[]{8732}, + new uint[]{8733}, + new uint[]{8734}, + new uint[]{8735}, + new uint[]{8736}, + new uint[]{8737}, + new uint[]{8738}, + new uint[]{8739}, + new uint[]{8740}, + new uint[]{8741}, + new uint[]{8742}, + new uint[]{8743}, + new uint[]{8744}, + new uint[]{8745}, + new uint[]{8746}, + new uint[]{8747}, + new uint[]{8965}, + new uint[]{8749}, + new uint[]{8750}, + new uint[]{8751}, + new uint[]{8752}, + new uint[]{8753}, + new uint[]{8754}, + new uint[]{8755}, + new uint[]{8756}, + new uint[]{8757}, + new uint[]{8758}, + new uint[]{8759}, + new uint[]{8760}, + new uint[]{8761}, + new uint[]{8762}, + new uint[]{8763}, + new uint[]{8764}, + new uint[]{8765}, + new uint[]{8766}, + new uint[]{8767}, + new uint[]{8768}, + new uint[]{8769}, + new uint[]{8770}, + new uint[]{8771}, + new uint[]{8772}, + new uint[]{8773}, + new uint[]{8774}, + new uint[]{8775}, + new uint[]{8228}, + new uint[]{8227}, + new uint[]{8827}, + new uint[]{8828}, + new uint[]{8829}, + new uint[]{8831}, + new uint[]{8832}, + new uint[]{8833}, + new uint[]{8836}, + new uint[]{8837}, + new uint[]{8826}, + new uint[]{8224}, + new uint[]{8966}, + new uint[]{8967}, + new uint[]{8968}, + new uint[]{9034}, + new uint[]{9033}, + new uint[]{9035}, + new uint[]{8969}, + new uint[]{8970}, + new uint[]{8971}, + new uint[]{8972}, + new uint[]{8973}, + new uint[]{8974}, + new uint[]{8975}, + new uint[]{8976}, + new uint[]{8977, 9026}, + new uint[]{8978}, + new uint[]{8979}, + new uint[]{8980}, + new uint[]{8460}, + new uint[]{8982}, + new uint[]{8983}, + new uint[]{8984}, + new uint[]{8985}, + new uint[]{8986}, + new uint[]{8987, 9025}, + new uint[]{8988}, + new uint[]{9039}, + new uint[]{8990}, + new uint[]{8991}, + new uint[]{8992}, + new uint[]{8993}, + new uint[]{8994}, + new uint[]{8995}, + new uint[]{8996}, + new uint[]{8997}, + new uint[]{9036}, + new uint[]{8998}, + new uint[]{8999}, + new uint[]{8877}, + new uint[]{8878}, + new uint[]{8879}, + new uint[]{8880}, + new uint[]{8881}, + new uint[]{8882}, + new uint[]{8883}, + new uint[]{8884}, + new uint[]{8885}, + new uint[]{8886}, + new uint[]{8887}, + new uint[]{8888}, + new uint[]{8947}, + new uint[]{8948}, + new uint[]{9000}, + new uint[]{9001}, + new uint[]{9002}, + new uint[]{9003, 9012}, + new uint[]{9004}, + new uint[]{9005}, + new uint[]{9006}, + new uint[]{9007}, + new uint[]{9008}, + new uint[]{8427}, + new uint[]{9027}, + new uint[]{9010}, + new uint[]{9011}, + new uint[]{9013}, + new uint[]{9037}, + new uint[]{9014}, + new uint[]{9015}, + new uint[]{8936, 8937, 8940, 8943, 8946}, + new uint[]{8937, 8938, 8944}, + new uint[]{8939, 8941, 8942, 8945}, + new uint[]{9016}, + new uint[]{9017}, + new uint[]{9018}, + new uint[]{9019}, + new uint[]{9020}, + new uint[]{9021}, + new uint[]{9022}, + new uint[]{9023}, + new uint[]{9024}, + new uint[]{9027}, + new uint[]{9027}, + new uint[]{9027}, + new uint[]{9038}, + new uint[]{9028}, + new uint[]{8953}, + new uint[]{8954}, + new uint[]{8955}, + new uint[]{8956}, + new uint[]{8957}, + new uint[]{2201}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8822}, + new uint[]{8822}, + new uint[]{8154}, + Array.Empty(), + new uint[]{8470, 8474}, + new uint[]{8461}, + new uint[]{8462}, + new uint[]{8464}, + new uint[]{8465}, + new uint[]{8466}, + new uint[]{8468}, + new uint[]{8666}, + new uint[]{8669}, + new uint[]{8671}, + new uint[]{8672}, + new uint[]{8674}, + new uint[]{8677}, + new uint[]{8678}, + new uint[]{8679}, + Array.Empty(), + new uint[]{8659, 8660, 8661}, + new uint[]{8662}, + new uint[]{8663}, + new uint[]{8665}, + new uint[]{8667}, + new uint[]{8668}, + new uint[]{8668}, + new uint[]{108}, + new uint[]{8848}, + new uint[]{8645}, + new uint[]{8918}, + new uint[]{8645, 8921}, + new uint[]{8313}, + new uint[]{8872}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8918}, + new uint[]{8921}, + new uint[]{8645}, + new uint[]{8645}, + new uint[]{8820}, + new uint[]{108}, + new uint[]{8323}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{8819}, + new uint[]{8485}, + new uint[]{8294}, + new uint[]{8295}, + new uint[]{8294}, + new uint[]{8295}, + new uint[]{8296}, + new uint[]{8296}, + new uint[]{8949}, + new uint[]{8950}, + new uint[]{8646}, + new uint[]{8962}, + new uint[]{8962}, + new uint[]{8962}, + new uint[]{8962}, + new uint[]{8962}, + new uint[]{8963}, + new uint[]{8353}, + new uint[]{8394}, + new uint[]{8268}, + new uint[]{8952}, + new uint[]{8838}, + new uint[]{8918}, + new uint[]{8790}, + new uint[]{8234}, + new uint[]{8485}, + new uint[]{8485}, + new uint[]{8485}, + new uint[]{8324}, + new uint[]{8811}, + new uint[]{8804}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9044}, + new uint[]{9045}, + new uint[]{9064}, + new uint[]{9148}, + new uint[]{9149}, + new uint[]{9150}, + new uint[]{9151}, + new uint[]{9152}, + new uint[]{9152}, + new uint[]{9041}, + new uint[]{9049}, + new uint[]{9047}, + new uint[]{9048}, + new uint[]{9050}, + new uint[]{9065}, + new uint[]{9046}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9041}, + new uint[]{9063}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8352}, + new uint[]{8826}, + new uint[]{9180}, + new uint[]{9181}, + new uint[]{9182}, + new uint[]{9183}, + new uint[]{8351}, + new uint[]{9184}, + new uint[]{9185}, + new uint[]{9143}, + new uint[]{9143}, + new uint[]{9143}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9051}, + new uint[]{9051}, + new uint[]{9051}, + new uint[]{9052}, + new uint[]{9063}, + new uint[]{9053}, + new uint[]{9053}, + new uint[]{9053}, + new uint[]{9054}, + new uint[]{9055}, + new uint[]{9056}, + new uint[]{9062}, + new uint[]{9057}, + new uint[]{9179}, + new uint[]{9058}, + new uint[]{9059}, + new uint[]{9127}, + new uint[]{9128}, + new uint[]{9129}, + new uint[]{9060}, + new uint[]{9130}, + new uint[]{9061}, + new uint[]{9211}, + new uint[]{9212}, + new uint[]{9213}, + new uint[]{9214}, + new uint[]{9215}, + new uint[]{9216}, + new uint[]{9217}, + new uint[]{9218}, + new uint[]{8658}, + new uint[]{9221}, + new uint[]{9222}, + new uint[]{9223}, + new uint[]{9220}, + new uint[]{9224}, + new uint[]{9042}, + new uint[]{108}, + new uint[]{9186}, + new uint[]{9231}, + new uint[]{9232}, + Array.Empty(), + new uint[]{9239}, + new uint[]{9240}, + new uint[]{9241}, + new uint[]{9242}, + new uint[]{9243}, + new uint[]{9244}, + new uint[]{9141}, + new uint[]{9146}, + new uint[]{9142}, + Array.Empty(), + new uint[]{9142}, + new uint[]{9131}, + new uint[]{9245}, + new uint[]{2667}, + Array.Empty(), + new uint[]{9245}, + Array.Empty(), + new uint[]{9245}, + new uint[]{9245}, + new uint[]{9245}, + new uint[]{9245}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9143}, + new uint[]{9153}, + new uint[]{9154}, + new uint[]{9155}, + new uint[]{9156}, + new uint[]{9157}, + new uint[]{9136}, + new uint[]{9134}, + new uint[]{9135}, + new uint[]{9136}, + new uint[]{9137}, + new uint[]{9138}, + new uint[]{9139}, + new uint[]{9140}, + new uint[]{9219}, + new uint[]{9132}, + new uint[]{9133}, + new uint[]{9230}, + new uint[]{9189}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9190}, + new uint[]{9190}, + new uint[]{9191}, + new uint[]{9192}, + new uint[]{9193}, + new uint[]{9194}, + new uint[]{9195}, + new uint[]{9196}, + new uint[]{9197}, + Array.Empty(), + new uint[]{9199}, + new uint[]{9200}, + new uint[]{9201}, + new uint[]{7857}, + new uint[]{7860}, + new uint[]{7857, 9189}, + new uint[]{9202}, + new uint[]{9203}, + new uint[]{9204}, + new uint[]{9205}, + new uint[]{9218}, + new uint[]{9220}, + new uint[]{9042}, + new uint[]{9178}, + new uint[]{9177}, + new uint[]{9176}, + new uint[]{9160}, + new uint[]{9159}, + new uint[]{9162}, + new uint[]{9161}, + new uint[]{9207}, + new uint[]{9208}, + new uint[]{9209}, + new uint[]{9210}, + new uint[]{9158}, + new uint[]{9158}, + new uint[]{9159}, + new uint[]{9160}, + new uint[]{9161}, + new uint[]{9162}, + new uint[]{8126}, + new uint[]{8105}, + new uint[]{9163}, + new uint[]{9164}, + new uint[]{9165}, + new uint[]{9166}, + new uint[]{9167}, + new uint[]{9168}, + new uint[]{9169}, + new uint[]{9170}, + new uint[]{9171}, + new uint[]{9172}, + new uint[]{9172}, + new uint[]{9172}, + new uint[]{9173}, + new uint[]{9173}, + new uint[]{9174}, + new uint[]{9175}, + Array.Empty(), + new uint[]{9131}, + new uint[]{9229}, + new uint[]{9247}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{8845}, + new uint[]{8845}, + new uint[]{8845}, + new uint[]{9131}, + new uint[]{108, 9147}, + Array.Empty(), + Array.Empty(), + new uint[]{9233}, + new uint[]{9234}, + new uint[]{9235}, + new uint[]{9236}, + new uint[]{9237}, + new uint[]{9238}, + Array.Empty(), + new uint[]{9260}, + new uint[]{9261}, + new uint[]{9262}, + new uint[]{9250}, + Array.Empty(), + new uint[]{731}, + new uint[]{9250}, + new uint[]{9254}, + new uint[]{9254}, + new uint[]{9250}, + new uint[]{9259}, + new uint[]{9253}, + new uint[]{731}, + new uint[]{9250}, + new uint[]{9254}, + new uint[]{9254}, + new uint[]{9255}, + new uint[]{9256}, + Array.Empty(), + new uint[]{9246}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{9829}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3046}, + new uint[]{2667}, + Array.Empty(), + new uint[]{9248}, + new uint[]{9249}, + Array.Empty(), + new uint[]{9263}, + new uint[]{9264}, + new uint[]{9265}, + new uint[]{9266}, + new uint[]{9281}, + new uint[]{9282}, + new uint[]{9283}, + new uint[]{9284}, + new uint[]{9285}, + Array.Empty(), + new uint[]{9281}, + new uint[]{9282}, + new uint[]{9283}, + new uint[]{9284}, + new uint[]{9285}, + new uint[]{9286}, + new uint[]{9287}, + new uint[]{9288}, + new uint[]{9289}, + new uint[]{9290}, + new uint[]{9291}, + new uint[]{9287}, + new uint[]{9288}, + new uint[]{9289}, + new uint[]{9290}, + new uint[]{9291}, + new uint[]{9292}, + new uint[]{9267}, + new uint[]{9268}, + new uint[]{9269}, + new uint[]{9271}, + new uint[]{108}, + new uint[]{9272}, + new uint[]{9273}, + new uint[]{9274}, + new uint[]{9275}, + new uint[]{9276}, + new uint[]{9277}, + new uint[]{9278}, + new uint[]{9316}, + new uint[]{9830}, + new uint[]{9288}, + new uint[]{9289}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9279}, + new uint[]{9280}, + new uint[]{9298}, + new uint[]{9299}, + Array.Empty(), + new uint[]{9301}, + new uint[]{9301}, + new uint[]{9298}, + new uint[]{9299}, + new uint[]{9300}, + new uint[]{9301}, + new uint[]{9301}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{9296}, + new uint[]{8378}, + new uint[]{8377}, + new uint[]{8376}, + new uint[]{8375}, + new uint[]{9293}, + new uint[]{9294}, + new uint[]{9295}, + new uint[]{9297}, + new uint[]{9341}, + new uint[]{9365}, + Array.Empty(), + new uint[]{9360}, + new uint[]{9361}, + new uint[]{9362}, + new uint[]{9355}, + new uint[]{3819}, + new uint[]{3820}, + new uint[]{9342}, + new uint[]{9356}, + new uint[]{9331}, + new uint[]{9331}, + Array.Empty(), + new uint[]{9329}, + new uint[]{9328}, + new uint[]{9332}, + new uint[]{9332}, + new uint[]{9332}, + new uint[]{9332}, + new uint[]{9332}, + new uint[]{9333}, + new uint[]{9333}, + new uint[]{9333}, + new uint[]{9334}, + new uint[]{9335}, + new uint[]{9336}, + new uint[]{9337}, + new uint[]{9338}, + new uint[]{108, 9339}, + new uint[]{9340}, + new uint[]{9341}, + new uint[]{9365}, + new uint[]{9360}, + new uint[]{9361}, + new uint[]{9362}, + new uint[]{9355}, + new uint[]{9342}, + new uint[]{9353}, + Array.Empty(), + new uint[]{9318}, + new uint[]{9319}, + new uint[]{9320}, + new uint[]{9321}, + new uint[]{9322}, + new uint[]{9323}, + new uint[]{9353}, + Array.Empty(), + Array.Empty(), + new uint[]{9319}, + new uint[]{9320}, + new uint[]{9321}, + new uint[]{9324}, + new uint[]{9346}, + new uint[]{5239}, + new uint[]{9348}, + new uint[]{9347}, + new uint[]{9349}, + new uint[]{8378}, + Array.Empty(), + new uint[]{9333}, + Array.Empty(), + new uint[]{9358}, + new uint[]{9357}, + new uint[]{9278}, + new uint[]{9325}, + new uint[]{9491}, + new uint[]{9490}, + new uint[]{9502}, + new uint[]{9503}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9326}, + new uint[]{9299}, + new uint[]{9299}, + Array.Empty(), + new uint[]{731}, + Array.Empty(), + Array.Empty(), + new uint[]{10119}, + Array.Empty(), + new uint[]{9317}, + new uint[]{9317}, + new uint[]{9288}, + new uint[]{9289}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9400}, + new uint[]{9513}, + new uint[]{9390}, + new uint[]{9402}, + new uint[]{9350}, + new uint[]{9359}, + new uint[]{9254}, + new uint[]{9254}, + new uint[]{9329}, + new uint[]{9327}, + new uint[]{9278}, + new uint[]{9333}, + new uint[]{9334}, + new uint[]{9339}, + new uint[]{9424}, + new uint[]{9425}, + new uint[]{9398}, + new uint[]{9399}, + new uint[]{9403}, + new uint[]{9404}, + new uint[]{9405}, + new uint[]{9406}, + new uint[]{9396}, + new uint[]{9475}, + new uint[]{9475}, + new uint[]{9476, 9501}, + new uint[]{9477}, + new uint[]{9477}, + new uint[]{9478, 9641}, + new uint[]{9478, 9641}, + new uint[]{9479}, + new uint[]{9480}, + new uint[]{9481}, + new uint[]{9482}, + new uint[]{9482}, + new uint[]{108}, + new uint[]{9427}, + new uint[]{9428}, + new uint[]{9462}, + new uint[]{9829}, + new uint[]{9830}, + new uint[]{9702}, + new uint[]{9702}, + new uint[]{9693}, + Array.Empty(), + new uint[]{9465}, + new uint[]{9466}, + new uint[]{9467}, + new uint[]{9468}, + new uint[]{9469}, + new uint[]{9470}, + new uint[]{9471}, + new uint[]{9472}, + new uint[]{9473}, + new uint[]{9462}, + new uint[]{9462}, + new uint[]{9822, 9823, 9824}, + new uint[]{541}, + Array.Empty(), + Array.Empty(), + new uint[]{9465}, + new uint[]{9466}, + new uint[]{9467}, + new uint[]{9468}, + new uint[]{9469}, + new uint[]{9470}, + new uint[]{9471}, + new uint[]{9472}, + new uint[]{9473}, + new uint[]{9505}, + new uint[]{9506}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{9508}, + new uint[]{9510}, + new uint[]{9510}, + new uint[]{9510}, + new uint[]{9510}, + new uint[]{9509}, + new uint[]{9509}, + new uint[]{9509}, + new uint[]{9509}, + new uint[]{9458}, + new uint[]{9459}, + new uint[]{9460}, + new uint[]{5045}, + new uint[]{9461}, + new uint[]{9461}, + new uint[]{9461}, + new uint[]{9442}, + new uint[]{9443}, + new uint[]{9444}, + new uint[]{9445}, + new uint[]{9446}, + new uint[]{9447}, + new uint[]{9448}, + new uint[]{9449}, + new uint[]{9450}, + new uint[]{9299}, + new uint[]{9299}, + Array.Empty(), + Array.Empty(), + new uint[]{9451}, + new uint[]{108}, + new uint[]{9364}, + new uint[]{9617}, + new uint[]{9618}, + new uint[]{9322}, + new uint[]{9417}, + new uint[]{9515}, + new uint[]{9516}, + new uint[]{9517}, + new uint[]{9518}, + new uint[]{9519}, + new uint[]{9520}, + new uint[]{9521}, + new uint[]{9522}, + new uint[]{9523}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11320}, + Array.Empty(), + new uint[]{9530}, + new uint[]{9531}, + new uint[]{9532}, + new uint[]{9533}, + new uint[]{9534}, + new uint[]{9535}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{10015}, + new uint[]{9391}, + new uint[]{9392}, + new uint[]{9393}, + new uint[]{9536}, + new uint[]{9537}, + new uint[]{9538}, + new uint[]{9931}, + new uint[]{9539}, + new uint[]{9540}, + new uint[]{9541}, + new uint[]{9542}, + new uint[]{9408}, + new uint[]{9544}, + new uint[]{9533}, + new uint[]{9545}, + new uint[]{9546}, + new uint[]{9547}, + new uint[]{9536}, + new uint[]{9548}, + new uint[]{9549}, + new uint[]{9550}, + new uint[]{9551}, + new uint[]{9537}, + new uint[]{10107}, + new uint[]{10116}, + new uint[]{108}, + new uint[]{9538}, + new uint[]{9555}, + new uint[]{9556}, + new uint[]{9557}, + new uint[]{9558}, + new uint[]{9559}, + new uint[]{9560}, + new uint[]{9561}, + new uint[]{9562}, + new uint[]{9563}, + new uint[]{9564}, + new uint[]{9536}, + new uint[]{9565}, + new uint[]{9430}, + new uint[]{9567}, + new uint[]{9533}, + new uint[]{9568}, + new uint[]{9569}, + new uint[]{9570}, + new uint[]{9571}, + new uint[]{9538}, + new uint[]{9572}, + new uint[]{9573}, + new uint[]{9679}, + new uint[]{9537}, + new uint[]{9575}, + new uint[]{9576}, + new uint[]{9577}, + new uint[]{9578}, + new uint[]{9394}, + new uint[]{9395}, + new uint[]{9407}, + new uint[]{9408}, + new uint[]{9650}, + Array.Empty(), + new uint[]{9664}, + new uint[]{9651}, + new uint[]{9384}, + new uint[]{9422}, + new uint[]{9423}, + new uint[]{9507}, + new uint[]{9411}, + new uint[]{9411}, + new uint[]{9412}, + new uint[]{9413}, + new uint[]{9414}, + new uint[]{9415}, + new uint[]{9416}, + new uint[]{108, 9489}, + new uint[]{9492}, + new uint[]{9493}, + new uint[]{9494}, + new uint[]{9495}, + new uint[]{9496}, + new uint[]{9497}, + new uint[]{9498}, + new uint[]{9499}, + new uint[]{9500}, + new uint[]{9656}, + new uint[]{9664}, + new uint[]{9657}, + new uint[]{9366}, + new uint[]{9367}, + new uint[]{9369}, + new uint[]{9371}, + new uint[]{9372}, + new uint[]{9373}, + new uint[]{9426}, + new uint[]{9409}, + new uint[]{9390}, + new uint[]{9410}, + new uint[]{9375}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9389}, + new uint[]{9386}, + new uint[]{9386}, + new uint[]{9386}, + new uint[]{9387}, + new uint[]{9387}, + new uint[]{9388}, + new uint[]{9388}, + new uint[]{9384}, + new uint[]{108, 9384}, + new uint[]{9642}, + new uint[]{9381}, + new uint[]{9588}, + new uint[]{9382}, + new uint[]{9383}, + new uint[]{9390}, + new uint[]{9390}, + Array.Empty(), + new uint[]{9390}, + new uint[]{9390}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{9403}, + new uint[]{9404}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9409}, + Array.Empty(), + new uint[]{9452}, + new uint[]{9453}, + new uint[]{9454}, + new uint[]{9455}, + new uint[]{9456}, + new uint[]{9457}, + new uint[]{9513}, + new uint[]{9646}, + new uint[]{9647}, + new uint[]{9644}, + new uint[]{9645}, + new uint[]{9648}, + new uint[]{9368}, + new uint[]{9370}, + new uint[]{9140}, + new uint[]{9436}, + new uint[]{9437}, + new uint[]{9438}, + new uint[]{9419}, + new uint[]{9420}, + new uint[]{9421}, + new uint[]{108}, + new uint[]{9439}, + new uint[]{9439}, + new uint[]{9439}, + new uint[]{9436}, + new uint[]{9440}, + Array.Empty(), + new uint[]{9441}, + new uint[]{9366}, + new uint[]{9367}, + new uint[]{9374}, + new uint[]{9543}, + new uint[]{9429}, + new uint[]{9430}, + Array.Empty(), + new uint[]{9431}, + new uint[]{9543}, + new uint[]{9432}, + new uint[]{9652}, + new uint[]{9655}, + new uint[]{9653}, + new uint[]{9654}, + new uint[]{510, 9384, 9398, 9400, 9419, 9426, 9929, 9931, 9967}, + new uint[]{9132}, + new uint[]{9133}, + new uint[]{9649}, + new uint[]{9140}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9608}, + new uint[]{9607}, + new uint[]{9368}, + new uint[]{9369}, + new uint[]{9370}, + new uint[]{9371}, + new uint[]{9372}, + new uint[]{9373}, + new uint[]{9384}, + new uint[]{9659}, + new uint[]{9662}, + new uint[]{9660}, + new uint[]{9661}, + new uint[]{9663}, + new uint[]{9511}, + new uint[]{9512}, + new uint[]{9363, 11271}, + new uint[]{9363, 11271}, + new uint[]{9363, 11271}, + new uint[]{9418}, + new uint[]{108, 9485, 9486, 9487, 9488}, + new uint[]{108, 9483, 9484}, + new uint[]{9477}, + new uint[]{9433}, + new uint[]{9680}, + new uint[]{9433}, + new uint[]{9681}, + new uint[]{9434}, + Array.Empty(), + new uint[]{9595}, + new uint[]{9668}, + new uint[]{9348}, + new uint[]{8378}, + new uint[]{5573}, + new uint[]{9666}, + new uint[]{2118}, + new uint[]{2160}, + new uint[]{2135}, + new uint[]{2136}, + new uint[]{9667}, + new uint[]{9671}, + new uint[]{9672}, + new uint[]{9674}, + new uint[]{3639}, + new uint[]{3642}, + new uint[]{3633}, + new uint[]{3632}, + new uint[]{4739}, + new uint[]{7869}, + new uint[]{9675}, + new uint[]{3458}, + new uint[]{3458}, + new uint[]{5576}, + new uint[]{9676}, + new uint[]{9677}, + new uint[]{9678}, + new uint[]{6148}, + new uint[]{6039}, + new uint[]{7537}, + new uint[]{6041}, + new uint[]{6042}, + new uint[]{6040}, + new uint[]{9673}, + new uint[]{8258}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9669}, + new uint[]{8826}, + new uint[]{9476}, + new uint[]{9479}, + new uint[]{9479}, + new uint[]{9479}, + new uint[]{9475}, + new uint[]{9479}, + new uint[]{9480}, + new uint[]{8826}, + new uint[]{9670}, + new uint[]{9586}, + new uint[]{9587}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9514}, + new uint[]{9432}, + new uint[]{9596}, + Array.Empty(), + new uint[]{9597}, + Array.Empty(), + new uint[]{9377}, + new uint[]{9378}, + new uint[]{108, 9379}, + new uint[]{9385}, + new uint[]{9425}, + new uint[]{9378}, + new uint[]{9589}, + new uint[]{9589}, + new uint[]{9589}, + new uint[]{9589}, + new uint[]{9590}, + new uint[]{9591}, + new uint[]{9592}, + new uint[]{9593}, + new uint[]{9594}, + Array.Empty(), + Array.Empty(), + new uint[]{9388}, + new uint[]{9606}, + new uint[]{9604}, + new uint[]{9375}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9605}, + new uint[]{9598}, + new uint[]{9599}, + new uint[]{9600}, + new uint[]{9386}, + new uint[]{9386}, + new uint[]{9386}, + new uint[]{9387}, + new uint[]{9388}, + new uint[]{9376}, + new uint[]{9390}, + new uint[]{9603}, + new uint[]{9602}, + new uint[]{9579}, + new uint[]{9580}, + new uint[]{9581}, + new uint[]{9579}, + new uint[]{9580}, + new uint[]{9582}, + new uint[]{9582}, + new uint[]{9582}, + Array.Empty(), + new uint[]{9582}, + new uint[]{9582}, + new uint[]{9582}, + Array.Empty(), + new uint[]{9543}, + new uint[]{9609}, + new uint[]{9559}, + new uint[]{9375}, + new uint[]{9639}, + new uint[]{9543}, + new uint[]{9566}, + Array.Empty(), + Array.Empty(), + new uint[]{9612}, + Array.Empty(), + new uint[]{9589}, + new uint[]{9589}, + new uint[]{9408}, + new uint[]{9407}, + new uint[]{9632}, + new uint[]{9601}, + new uint[]{9629}, + new uint[]{9630}, + new uint[]{9631}, + new uint[]{9374}, + new uint[]{9386}, + new uint[]{9636}, + new uint[]{9602}, + new uint[]{9635}, + new uint[]{9377}, + new uint[]{9647}, + new uint[]{9647}, + new uint[]{9385}, + new uint[]{9377}, + new uint[]{9637}, + new uint[]{9604}, + new uint[]{9385}, + new uint[]{9390}, + new uint[]{9633}, + new uint[]{9384}, + new uint[]{9634}, + Array.Empty(), + new uint[]{7974}, + new uint[]{9388}, + new uint[]{9386}, + new uint[]{9543}, + new uint[]{9638}, + new uint[]{9376}, + new uint[]{9457}, + new uint[]{9636}, + new uint[]{9602}, + new uint[]{9390}, + new uint[]{9366}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9640}, + new uint[]{9599}, + new uint[]{9636}, + new uint[]{9602}, + new uint[]{9564}, + new uint[]{9366}, + new uint[]{9367}, + new uint[]{6945}, + new uint[]{9425}, + new uint[]{9378}, + new uint[]{9607}, + new uint[]{9608}, + new uint[]{9368}, + new uint[]{108}, + new uint[]{9390}, + new uint[]{9636}, + new uint[]{9602}, + new uint[]{9585}, + new uint[]{9714}, + new uint[]{9715}, + new uint[]{9716}, + new uint[]{9717}, + new uint[]{9718}, + new uint[]{9719}, + new uint[]{9720}, + new uint[]{9721}, + new uint[]{9722}, + new uint[]{9723}, + new uint[]{9724}, + new uint[]{9725}, + new uint[]{9726}, + new uint[]{9727}, + new uint[]{9728}, + new uint[]{9729}, + new uint[]{9730}, + new uint[]{9731}, + new uint[]{9732}, + new uint[]{9733}, + new uint[]{9734}, + new uint[]{9665}, + Array.Empty(), + Array.Empty(), + new uint[]{9371}, + new uint[]{9372}, + new uint[]{9373}, + new uint[]{9389}, + new uint[]{9386}, + new uint[]{9386}, + new uint[]{9387}, + new uint[]{9388}, + new uint[]{10096}, + new uint[]{10099}, + new uint[]{9682}, + new uint[]{9683}, + new uint[]{9684}, + new uint[]{9603}, + new uint[]{9685}, + new uint[]{9686}, + new uint[]{9390}, + new uint[]{9390}, + new uint[]{9411}, + new uint[]{9503}, + new uint[]{9916}, + new uint[]{9439}, + new uint[]{8826}, + new uint[]{8826}, + new uint[]{541}, + new uint[]{9737}, + new uint[]{9688}, + new uint[]{9741}, + new uint[]{9735}, + new uint[]{9736}, + new uint[]{9737}, + new uint[]{9788}, + new uint[]{9789}, + new uint[]{9790}, + new uint[]{9791}, + new uint[]{9792}, + new uint[]{9795}, + new uint[]{9796}, + new uint[]{9793}, + new uint[]{9794}, + new uint[]{9797}, + new uint[]{9799}, + new uint[]{9808}, + new uint[]{9810}, + new uint[]{9800}, + new uint[]{9801}, + new uint[]{9802}, + new uint[]{9803}, + new uint[]{9804}, + new uint[]{9805}, + new uint[]{9806}, + new uint[]{9807}, + new uint[]{9809}, + new uint[]{9776}, + new uint[]{9777}, + new uint[]{9778}, + new uint[]{9779}, + new uint[]{9780}, + new uint[]{9781}, + new uint[]{9782}, + new uint[]{9783}, + new uint[]{9784}, + new uint[]{9785}, + new uint[]{9786}, + new uint[]{9808}, + new uint[]{9787}, + new uint[]{9808}, + new uint[]{9774}, + new uint[]{9773}, + new uint[]{9811}, + new uint[]{9775}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9696}, + new uint[]{9697}, + new uint[]{9698}, + new uint[]{9699}, + new uint[]{9697}, + new uint[]{9701}, + new uint[]{9702}, + new uint[]{9696}, + new uint[]{9697}, + Array.Empty(), + new uint[]{9699}, + new uint[]{9697}, + new uint[]{9701}, + new uint[]{9702}, + new uint[]{9703}, + new uint[]{9704}, + new uint[]{9705}, + new uint[]{9706}, + new uint[]{9798}, + new uint[]{9798}, + Array.Empty(), + new uint[]{9812}, + new uint[]{9808}, + new uint[]{9808}, + new uint[]{9742}, + new uint[]{9743}, + new uint[]{9744}, + new uint[]{9745}, + new uint[]{9764}, + new uint[]{9765}, + new uint[]{9766}, + new uint[]{9767}, + new uint[]{9768}, + new uint[]{9764}, + new uint[]{9766}, + new uint[]{9767}, + new uint[]{9765}, + new uint[]{9768}, + Array.Empty(), + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + Array.Empty(), + Array.Empty(), + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9769}, + new uint[]{9772}, + new uint[]{9770}, + new uint[]{9771}, + new uint[]{9707}, + new uint[]{9708}, + Array.Empty(), + new uint[]{9709}, + new uint[]{9710}, + new uint[]{9711}, + new uint[]{9708}, + new uint[]{9707}, + new uint[]{9708}, + Array.Empty(), + new uint[]{9709}, + new uint[]{9710}, + new uint[]{9711}, + new uint[]{9712}, + new uint[]{9713}, + new uint[]{9738}, + new uint[]{9739}, + new uint[]{9740}, + new uint[]{9808}, + new uint[]{9619, 9885}, + new uint[]{9886}, + new uint[]{9887}, + new uint[]{9888}, + new uint[]{9889}, + new uint[]{9890}, + new uint[]{9891}, + new uint[]{9892}, + new uint[]{9893}, + new uint[]{9894}, + new uint[]{9813}, + new uint[]{9813}, + new uint[]{9815}, + new uint[]{9816}, + new uint[]{9817}, + new uint[]{9818}, + new uint[]{9819}, + new uint[]{9829}, + new uint[]{9830}, + new uint[]{9826}, + new uint[]{9827}, + new uint[]{9828}, + new uint[]{9813}, + new uint[]{9814}, + new uint[]{9815}, + new uint[]{9821}, + new uint[]{9818}, + new uint[]{9819}, + new uint[]{9820}, + new uint[]{9830}, + new uint[]{9831}, + new uint[]{9832}, + new uint[]{9747}, + new uint[]{9748}, + new uint[]{9746}, + new uint[]{9751}, + new uint[]{9752}, + new uint[]{9751}, + new uint[]{9752}, + new uint[]{9753}, + new uint[]{9754}, + new uint[]{9755}, + new uint[]{9756}, + new uint[]{9755}, + new uint[]{9756}, + new uint[]{9757}, + new uint[]{9758}, + new uint[]{9759}, + new uint[]{9760}, + new uint[]{9761}, + Array.Empty(), + new uint[]{9763}, + new uint[]{9328}, + Array.Empty(), + new uint[]{9750}, + new uint[]{9838}, + new uint[]{9839}, + new uint[]{9840}, + new uint[]{9841}, + new uint[]{9842}, + new uint[]{9843}, + new uint[]{9844}, + new uint[]{9849}, + new uint[]{9847}, + new uint[]{9838}, + new uint[]{9839}, + new uint[]{9851}, + new uint[]{9852}, + new uint[]{9840}, + new uint[]{9841}, + new uint[]{9842}, + new uint[]{9850}, + new uint[]{9843}, + new uint[]{9844}, + new uint[]{9845}, + new uint[]{9846}, + new uint[]{9849}, + new uint[]{9847}, + new uint[]{9848}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9853}, + new uint[]{9855}, + new uint[]{9856}, + new uint[]{9857}, + new uint[]{9858}, + new uint[]{9859}, + new uint[]{9860}, + new uint[]{9861}, + new uint[]{9862}, + new uint[]{9854}, + new uint[]{9853}, + new uint[]{9855}, + new uint[]{9856}, + new uint[]{9857}, + new uint[]{9858}, + new uint[]{9859}, + new uint[]{9860}, + new uint[]{9861}, + new uint[]{9862}, + new uint[]{9854}, + new uint[]{9796}, + new uint[]{9796}, + new uint[]{9796}, + new uint[]{9796}, + new uint[]{9796}, + new uint[]{9908}, + new uint[]{9909}, + new uint[]{9908}, + Array.Empty(), + new uint[]{9910}, + new uint[]{9869}, + new uint[]{9870}, + new uint[]{9871}, + new uint[]{9872}, + new uint[]{9346}, + new uint[]{9873}, + new uint[]{9875}, + new uint[]{9876}, + new uint[]{9874}, + new uint[]{9877}, + new uint[]{9878}, + new uint[]{316}, + new uint[]{9879}, + new uint[]{9881}, + new uint[]{9880}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9863}, + new uint[]{9838}, + new uint[]{9840}, + new uint[]{9841}, + new uint[]{9842}, + new uint[]{9843}, + new uint[]{9844}, + new uint[]{9849}, + new uint[]{9847}, + new uint[]{9863}, + new uint[]{9838}, + Array.Empty(), + new uint[]{9851}, + new uint[]{9840}, + new uint[]{9841}, + new uint[]{9842}, + Array.Empty(), + new uint[]{9843}, + new uint[]{9844}, + new uint[]{9849}, + new uint[]{9847}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9895}, + new uint[]{9896}, + new uint[]{9897}, + new uint[]{9332, 9898}, + new uint[]{9332, 9898}, + new uint[]{9898}, + new uint[]{9332}, + new uint[]{9398}, + new uint[]{9902}, + new uint[]{9903}, + new uint[]{9904}, + new uint[]{9905}, + new uint[]{11321}, + new uint[]{3633}, + new uint[]{9834}, + new uint[]{9836}, + Array.Empty(), + Array.Empty(), + new uint[]{9834}, + new uint[]{9835}, + new uint[]{9836}, + Array.Empty(), + new uint[]{3634}, + new uint[]{3639}, + new uint[]{3642}, + new uint[]{3632}, + new uint[]{3458}, + new uint[]{3458}, + new uint[]{11315}, + new uint[]{11316}, + new uint[]{108, 11317}, + new uint[]{11318}, + new uint[]{3632}, + new uint[]{3458}, + new uint[]{4954}, + Array.Empty(), + new uint[]{3458}, + new uint[]{11319}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9696}, + new uint[]{9696}, + new uint[]{9868}, + new uint[]{3983}, + new uint[]{11314}, + new uint[]{3635}, + new uint[]{3636}, + new uint[]{3637}, + new uint[]{3638}, + new uint[]{3640}, + new uint[]{3641}, + new uint[]{3643}, + new uint[]{3644}, + new uint[]{9696}, + new uint[]{9696}, + new uint[]{9700}, + new uint[]{9700}, + new uint[]{9700}, + new uint[]{9700}, + new uint[]{3984}, + new uint[]{3983}, + new uint[]{11314}, + new uint[]{10075}, + new uint[]{10077}, + new uint[]{10074}, + new uint[]{10013, 11270}, + new uint[]{9696}, + new uint[]{9696}, + Array.Empty(), + Array.Empty(), + new uint[]{9696}, + Array.Empty(), + new uint[]{9696}, + new uint[]{9696}, + new uint[]{4093}, + new uint[]{9864}, + new uint[]{9865}, + new uint[]{9866}, + new uint[]{9867}, + new uint[]{9871}, + new uint[]{9911}, + new uint[]{9912}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9879}, + new uint[]{9917}, + new uint[]{10103}, + new uint[]{108}, + new uint[]{9902}, + new uint[]{9879}, + new uint[]{9948}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9949}, + new uint[]{9950}, + new uint[]{9951}, + new uint[]{9918}, + new uint[]{9919}, + new uint[]{9921}, + new uint[]{9922}, + new uint[]{9664}, + new uint[]{9136}, + new uint[]{9923}, + new uint[]{10064}, + new uint[]{10063}, + new uint[]{9941}, + new uint[]{9942}, + new uint[]{9943}, + new uint[]{9944}, + new uint[]{9945}, + new uint[]{9946}, + new uint[]{10065}, + new uint[]{10066}, + new uint[]{9989}, + new uint[]{9988}, + new uint[]{9992}, + new uint[]{9993}, + new uint[]{9649}, + new uint[]{10059}, + new uint[]{9133}, + new uint[]{10004}, + new uint[]{10076}, + new uint[]{10068}, + new uint[]{10069}, + new uint[]{10070}, + new uint[]{10071}, + new uint[]{10072}, + new uint[]{10073}, + new uint[]{10007}, + new uint[]{10008}, + new uint[]{10009}, + new uint[]{108}, + Array.Empty(), + new uint[]{9955}, + new uint[]{9938}, + new uint[]{9947}, + new uint[]{9514}, + new uint[]{9429}, + new uint[]{9939}, + new uint[]{9940}, + new uint[]{10095}, + Array.Empty(), + new uint[]{9409}, + new uint[]{9390}, + new uint[]{9694}, + new uint[]{9695}, + new uint[]{9924}, + new uint[]{9928}, + new uint[]{9929}, + new uint[]{9930}, + new uint[]{9925}, + new uint[]{9926}, + new uint[]{9927}, + new uint[]{9969}, + new uint[]{9970}, + new uint[]{9971}, + new uint[]{9972}, + new uint[]{9973}, + new uint[]{9974}, + new uint[]{10057}, + new uint[]{10100}, + new uint[]{108}, + new uint[]{10097}, + new uint[]{10098}, + new uint[]{10109}, + new uint[]{10113}, + new uint[]{10108}, + new uint[]{10101}, + new uint[]{10107}, + new uint[]{108}, + new uint[]{10192}, + new uint[]{10006}, + new uint[]{9409}, + new uint[]{9694}, + new uint[]{9453}, + new uint[]{10005}, + new uint[]{9346}, + new uint[]{10010}, + new uint[]{9869}, + new uint[]{9870}, + new uint[]{10011}, + new uint[]{10013}, + new uint[]{10014}, + new uint[]{9349}, + new uint[]{9348}, + new uint[]{8378}, + new uint[]{10016}, + new uint[]{10017}, + new uint[]{10018}, + new uint[]{10019}, + new uint[]{10020}, + new uint[]{10021}, + new uint[]{10022}, + new uint[]{10024}, + new uint[]{10026}, + new uint[]{10025}, + new uint[]{10026}, + Array.Empty(), + new uint[]{10028}, + new uint[]{10029}, + new uint[]{10030}, + new uint[]{3573}, + new uint[]{10031}, + new uint[]{10032}, + new uint[]{10033}, + new uint[]{10034}, + new uint[]{10037}, + new uint[]{10041}, + new uint[]{10041}, + new uint[]{10042}, + new uint[]{10043}, + new uint[]{10044}, + new uint[]{108, 7941, 10028, 10045, 10046, 10051, 10052}, + new uint[]{10024}, + new uint[]{10026}, + new uint[]{10030}, + new uint[]{3573}, + new uint[]{10047}, + new uint[]{10048}, + new uint[]{10049}, + new uint[]{10050}, + new uint[]{10053}, + new uint[]{10054}, + new uint[]{10055}, + new uint[]{10056}, + new uint[]{10021}, + new uint[]{10016}, + new uint[]{10017}, + new uint[]{10018}, + new uint[]{10036}, + new uint[]{10038}, + new uint[]{10039}, + new uint[]{10040}, + new uint[]{10035}, + new uint[]{10012}, + new uint[]{10377}, + new uint[]{10378}, + new uint[]{10379}, + new uint[]{10380}, + new uint[]{10381}, + new uint[]{10382}, + new uint[]{10383}, + new uint[]{10384}, + new uint[]{10939}, + new uint[]{10386}, + new uint[]{10387}, + new uint[]{10388}, + new uint[]{10389}, + new uint[]{10390}, + new uint[]{10058}, + new uint[]{9956}, + new uint[]{9664}, + Array.Empty(), + new uint[]{10191}, + Array.Empty(), + new uint[]{9932}, + new uint[]{9933}, + new uint[]{9934}, + new uint[]{9935}, + new uint[]{9936}, + new uint[]{9937}, + new uint[]{108}, + new uint[]{10057}, + new uint[]{10057}, + new uint[]{9961}, + new uint[]{9962}, + new uint[]{9963}, + new uint[]{9963}, + new uint[]{9961}, + new uint[]{9963}, + new uint[]{9962}, + new uint[]{9964}, + new uint[]{9965}, + new uint[]{9966}, + new uint[]{9967}, + new uint[]{10079}, + new uint[]{10080}, + new uint[]{10081}, + new uint[]{10082}, + new uint[]{10086}, + new uint[]{11214}, + new uint[]{10456}, + new uint[]{10456}, + Array.Empty(), + Array.Empty(), + new uint[]{9384}, + new uint[]{9423}, + new uint[]{9384}, + new uint[]{9361}, + new uint[]{9388}, + new uint[]{9682}, + new uint[]{9388}, + new uint[]{10160}, + new uint[]{9429}, + new uint[]{9432}, + new uint[]{10001}, + new uint[]{9559}, + new uint[]{2142}, + new uint[]{10002}, + new uint[]{10000}, + new uint[]{10003}, + Array.Empty(), + new uint[]{9958}, + new uint[]{9959}, + new uint[]{9960}, + new uint[]{9366}, + new uint[]{9367}, + new uint[]{7954}, + new uint[]{3164}, + new uint[]{3169}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10214}, + new uint[]{9682}, + new uint[]{9564}, + new uint[]{9366}, + new uint[]{9367}, + new uint[]{7954}, + new uint[]{10177}, + new uint[]{10120}, + new uint[]{10121}, + new uint[]{10122}, + new uint[]{10123}, + new uint[]{10124}, + new uint[]{10125}, + new uint[]{10126}, + new uint[]{10130}, + new uint[]{10131}, + new uint[]{10132}, + new uint[]{10127}, + new uint[]{10133}, + new uint[]{10128}, + new uint[]{10129}, + new uint[]{10134}, + new uint[]{10135}, + new uint[]{10136}, + new uint[]{10137}, + new uint[]{10138}, + new uint[]{10139}, + new uint[]{10140}, + new uint[]{10141}, + new uint[]{10142}, + new uint[]{10143}, + new uint[]{10129}, + new uint[]{10144}, + new uint[]{10145}, + new uint[]{10146}, + new uint[]{10128}, + new uint[]{10147}, + new uint[]{10148}, + new uint[]{10149}, + new uint[]{10150}, + new uint[]{10126}, + new uint[]{10127}, + new uint[]{10151}, + new uint[]{10152}, + new uint[]{10153}, + new uint[]{10154}, + new uint[]{10155}, + new uint[]{10156}, + new uint[]{10157}, + new uint[]{10158}, + new uint[]{10159}, + new uint[]{10160}, + new uint[]{10127}, + new uint[]{10161}, + new uint[]{10162}, + new uint[]{10163}, + new uint[]{10126}, + new uint[]{10164}, + new uint[]{10165}, + new uint[]{10166}, + new uint[]{10167}, + new uint[]{10129}, + new uint[]{10168}, + new uint[]{10169}, + new uint[]{10170}, + new uint[]{10128}, + new uint[]{10171}, + new uint[]{10172}, + new uint[]{10173}, + new uint[]{10174}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{9950}, + new uint[]{9920}, + new uint[]{9950}, + new uint[]{9950}, + new uint[]{10212}, + new uint[]{10211}, + new uint[]{9947}, + Array.Empty(), + Array.Empty(), + new uint[]{9380}, + new uint[]{10210}, + new uint[]{10209}, + new uint[]{10208}, + new uint[]{10207}, + Array.Empty(), + new uint[]{10205}, + new uint[]{10204}, + new uint[]{10057}, + new uint[]{10057}, + new uint[]{10213}, + new uint[]{10203}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{9975}, + new uint[]{9976}, + new uint[]{9977}, + new uint[]{9978}, + new uint[]{9979}, + new uint[]{9980}, + new uint[]{9957}, + new uint[]{9953}, + new uint[]{9954}, + new uint[]{9954}, + new uint[]{9953}, + new uint[]{9954}, + new uint[]{9954}, + new uint[]{9432}, + new uint[]{9981}, + new uint[]{9982}, + new uint[]{9983}, + new uint[]{108}, + new uint[]{9984}, + new uint[]{9983}, + new uint[]{9985}, + new uint[]{9985}, + new uint[]{9986}, + new uint[]{9987}, + new uint[]{9366, 9896}, + new uint[]{9367, 9897}, + new uint[]{3164}, + new uint[]{3169}, + new uint[]{10175}, + new uint[]{10176}, + new uint[]{10177}, + new uint[]{9404, 9936}, + new uint[]{9375, 10178}, + new uint[]{9374, 9898}, + new uint[]{9374}, + new uint[]{9374}, + new uint[]{9374, 9901}, + new uint[]{9374, 9898, 9900}, + new uint[]{9374, 9898, 9901}, + new uint[]{9374, 9901}, + new uint[]{9946}, + new uint[]{10067}, + new uint[]{10182}, + new uint[]{10215}, + new uint[]{6529}, + new uint[]{10087}, + Array.Empty(), + new uint[]{10089}, + new uint[]{10090}, + Array.Empty(), + new uint[]{10092}, + new uint[]{10093}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{4385}, + new uint[]{3293}, + new uint[]{9975}, + new uint[]{9976}, + new uint[]{9978}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{9380}, + new uint[]{9397}, + new uint[]{10252}, + new uint[]{9425}, + new uint[]{10892}, + new uint[]{10062}, + Array.Empty(), + new uint[]{10396}, + new uint[]{10397}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10083}, + new uint[]{10084}, + new uint[]{10085}, + new uint[]{10219}, + new uint[]{10224}, + new uint[]{10242}, + new uint[]{9503}, + new uint[]{9503}, + Array.Empty(), + new uint[]{9378}, + new uint[]{10251}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{1455}, + new uint[]{11313}, + new uint[]{11312}, + new uint[]{10099}, + new uint[]{10099}, + new uint[]{10099}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10104}, + new uint[]{10105}, + new uint[]{10106}, + new uint[]{10240}, + new uint[]{10177}, + new uint[]{10184}, + new uint[]{10222}, + new uint[]{10223}, + new uint[]{10110}, + new uint[]{10111}, + new uint[]{10112}, + new uint[]{10116}, + new uint[]{108}, + new uint[]{10114}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10115}, + new uint[]{10102}, + new uint[]{10117}, + new uint[]{10118}, + new uint[]{10240}, + new uint[]{10179}, + new uint[]{10180}, + new uint[]{10183}, + new uint[]{10186}, + new uint[]{10183}, + new uint[]{10239}, + new uint[]{10229}, + new uint[]{10220}, + new uint[]{10393}, + new uint[]{10393}, + new uint[]{10394}, + Array.Empty(), + new uint[]{9425}, + new uint[]{9665}, + new uint[]{10024}, + new uint[]{10026}, + new uint[]{10030}, + new uint[]{3573}, + new uint[]{10049}, + new uint[]{10050}, + new uint[]{10055}, + new uint[]{10056}, + new uint[]{10019}, + new uint[]{10020}, + new uint[]{10186}, + new uint[]{10221}, + new uint[]{10189}, + Array.Empty(), + new uint[]{10356}, + new uint[]{10357}, + new uint[]{108}, + Array.Empty(), + new uint[]{10184}, + new uint[]{10222}, + new uint[]{10223}, + new uint[]{10234}, + new uint[]{10235}, + new uint[]{10236}, + new uint[]{10237}, + new uint[]{10243}, + new uint[]{10244}, + new uint[]{10185}, + new uint[]{9386}, + new uint[]{9967}, + new uint[]{10225}, + new uint[]{10226}, + new uint[]{10227}, + new uint[]{10228}, + new uint[]{9595}, + new uint[]{10245}, + new uint[]{10241}, + new uint[]{9936}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{10184}, + new uint[]{10182}, + new uint[]{9386}, + new uint[]{10182}, + new uint[]{9377}, + new uint[]{10187}, + new uint[]{10187}, + new uint[]{10185}, + new uint[]{10217}, + new uint[]{10181}, + new uint[]{10181}, + new uint[]{10181}, + new uint[]{10177}, + new uint[]{3164}, + new uint[]{3169}, + new uint[]{9939}, + new uint[]{10124}, + new uint[]{10216}, + new uint[]{10218}, + new uint[]{10183}, + new uint[]{10239}, + new uint[]{9519}, + new uint[]{9902}, + new uint[]{10186}, + new uint[]{6945}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9633}, + new uint[]{5851}, + new uint[]{10230}, + new uint[]{10231}, + new uint[]{10232}, + new uint[]{3459}, + new uint[]{9388}, + new uint[]{9386}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{9388}, + new uint[]{9387}, + new uint[]{9380, 9595}, + new uint[]{10216}, + new uint[]{10252}, + new uint[]{9425}, + new uint[]{9939}, + new uint[]{10238}, + new uint[]{108}, + new uint[]{4130, 11264}, + new uint[]{5239, 11265}, + new uint[]{713, 11266}, + new uint[]{1492, 11267}, + new uint[]{8378, 11268}, + new uint[]{9363, 11271}, + new uint[]{9363, 11271}, + new uint[]{9363, 11271}, + new uint[]{10586}, + new uint[]{10586}, + new uint[]{10586}, + new uint[]{10898}, + new uint[]{10898}, + new uint[]{10899}, + new uint[]{10243}, + new uint[]{10244}, + new uint[]{9695}, + new uint[]{9408}, + new uint[]{9695}, + new uint[]{108}, + new uint[]{10231}, + new uint[]{108}, + new uint[]{10207}, + new uint[]{10250}, + new uint[]{10249}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{10717}, + new uint[]{10718}, + new uint[]{0}, + new uint[]{10719}, + Array.Empty(), + new uint[]{9425}, + new uint[]{10256}, + new uint[]{10257}, + new uint[]{10258}, + new uint[]{10259}, + new uint[]{10257}, + new uint[]{10256}, + new uint[]{108}, + new uint[]{10249}, + new uint[]{9543}, + new uint[]{10279}, + new uint[]{10280}, + new uint[]{10281}, + new uint[]{10282}, + new uint[]{10283}, + new uint[]{10284}, + new uint[]{10285}, + new uint[]{10286}, + new uint[]{10287}, + new uint[]{10288}, + new uint[]{10289}, + new uint[]{10419}, + new uint[]{10420}, + new uint[]{10421}, + new uint[]{10422}, + new uint[]{10423}, + new uint[]{10424}, + new uint[]{10425}, + new uint[]{10426}, + new uint[]{10427}, + new uint[]{10428}, + new uint[]{10429}, + new uint[]{10430}, + new uint[]{10431}, + new uint[]{10432}, + new uint[]{10433}, + new uint[]{10434}, + new uint[]{10435}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10403}, + new uint[]{10401}, + new uint[]{10402}, + new uint[]{10364}, + new uint[]{10365}, + new uint[]{10366}, + new uint[]{10367}, + Array.Empty(), + new uint[]{10369}, + new uint[]{11166}, + new uint[]{10371}, + new uint[]{10372}, + new uint[]{10373}, + new uint[]{10374}, + new uint[]{10375}, + new uint[]{10398}, + new uint[]{10395}, + Array.Empty(), + new uint[]{10404}, + new uint[]{10405}, + new uint[]{10406}, + new uint[]{10407}, + new uint[]{10586}, + new uint[]{10587}, + new uint[]{10588}, + new uint[]{10589}, + new uint[]{1492}, + new uint[]{10457}, + new uint[]{10458}, + new uint[]{10459}, + new uint[]{10460}, + new uint[]{10461}, + new uint[]{10462}, + new uint[]{10463}, + Array.Empty(), + new uint[]{10464}, + new uint[]{10465}, + Array.Empty(), + new uint[]{10467}, + new uint[]{10468}, + new uint[]{10469}, + new uint[]{10470}, + new uint[]{10471}, + Array.Empty(), + new uint[]{10473}, + new uint[]{10474}, + new uint[]{10475}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{10668}, + new uint[]{10669}, + new uint[]{10670}, + new uint[]{10671}, + new uint[]{10672}, + new uint[]{10673}, + new uint[]{10674}, + new uint[]{10675}, + new uint[]{10676}, + new uint[]{10677}, + new uint[]{10678}, + new uint[]{10679}, + new uint[]{108}, + new uint[]{10680}, + new uint[]{10681}, + new uint[]{10682}, + new uint[]{108}, + new uint[]{10683}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10684}, + new uint[]{10685}, + new uint[]{10686}, + new uint[]{10687}, + new uint[]{10688}, + new uint[]{10689}, + new uint[]{10690}, + new uint[]{10290}, + new uint[]{10291}, + new uint[]{10292}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10293}, + new uint[]{10762}, + new uint[]{108}, + new uint[]{10590}, + new uint[]{10591}, + new uint[]{10592}, + Array.Empty(), + new uint[]{10594}, + new uint[]{10595}, + new uint[]{10596}, + new uint[]{10597}, + new uint[]{10598}, + new uint[]{10599}, + new uint[]{10600}, + new uint[]{10601}, + new uint[]{10602}, + new uint[]{10603}, + new uint[]{10604}, + new uint[]{10605}, + new uint[]{10606}, + new uint[]{10607}, + new uint[]{10608}, + new uint[]{10609}, + new uint[]{10610}, + new uint[]{10611}, + new uint[]{10612}, + new uint[]{10613}, + new uint[]{10614}, + new uint[]{10399}, + new uint[]{10494}, + new uint[]{10495}, + new uint[]{108}, + new uint[]{10408}, + new uint[]{10409}, + new uint[]{10410}, + new uint[]{10411}, + new uint[]{10412}, + new uint[]{10413}, + new uint[]{10414}, + new uint[]{10415}, + new uint[]{10407}, + new uint[]{10406}, + new uint[]{10648}, + new uint[]{10649}, + new uint[]{10650}, + new uint[]{10651}, + new uint[]{10652}, + new uint[]{10653}, + new uint[]{10654}, + new uint[]{10655}, + new uint[]{10656}, + new uint[]{10657}, + new uint[]{10658}, + new uint[]{10659}, + new uint[]{10942}, + new uint[]{10660}, + new uint[]{10661}, + new uint[]{10662}, + new uint[]{10663}, + new uint[]{10664}, + new uint[]{10665}, + new uint[]{10666}, + new uint[]{9349}, + new uint[]{10012}, + new uint[]{8378}, + new uint[]{4846}, + new uint[]{10259}, + new uint[]{10257}, + new uint[]{10256}, + new uint[]{10409}, + new uint[]{10412}, + new uint[]{10411}, + new uint[]{10413}, + new uint[]{10416}, + new uint[]{10417}, + new uint[]{10376}, + new uint[]{10261}, + new uint[]{4149}, + new uint[]{1400}, + new uint[]{1401}, + new uint[]{1402}, + new uint[]{1403}, + new uint[]{1404}, + new uint[]{10262}, + new uint[]{10263}, + new uint[]{10264}, + new uint[]{10313}, + new uint[]{10314}, + new uint[]{10315}, + new uint[]{10316}, + new uint[]{10317}, + new uint[]{10450}, + new uint[]{10450}, + new uint[]{10400}, + new uint[]{10400}, + new uint[]{10400}, + Array.Empty(), + new uint[]{10331}, + new uint[]{10332}, + new uint[]{10333}, + new uint[]{10335}, + new uint[]{10336}, + new uint[]{10697}, + new uint[]{11217}, + new uint[]{10698}, + new uint[]{10699}, + new uint[]{10700}, + new uint[]{10701}, + new uint[]{10702}, + new uint[]{10703}, + new uint[]{10704}, + new uint[]{10705}, + new uint[]{10706}, + new uint[]{10707}, + new uint[]{10708}, + new uint[]{10709}, + new uint[]{10710}, + new uint[]{10711}, + new uint[]{10712}, + new uint[]{10713}, + new uint[]{10714}, + new uint[]{10715}, + new uint[]{10716}, + new uint[]{10334}, + new uint[]{10453}, + new uint[]{10454}, + new uint[]{10452}, + new uint[]{10453}, + new uint[]{10454}, + Array.Empty(), + new uint[]{10452}, + new uint[]{10451}, + new uint[]{10451}, + new uint[]{10448}, + new uint[]{5640}, + new uint[]{10448}, + new uint[]{10448}, + new uint[]{10447}, + new uint[]{11195}, + new uint[]{10561}, + new uint[]{10443}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10449}, + new uint[]{10449}, + new uint[]{10400}, + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10906}, + new uint[]{10907}, + new uint[]{10908}, + new uint[]{10909}, + Array.Empty(), + new uint[]{10911}, + new uint[]{10912}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10905}, + new uint[]{10267}, + new uint[]{10267}, + new uint[]{10913}, + new uint[]{10914}, + new uint[]{10915}, + new uint[]{10916}, + new uint[]{10268}, + new uint[]{10918}, + new uint[]{11070}, + new uint[]{10345}, + new uint[]{10345}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10920}, + new uint[]{10910}, + new uint[]{10497}, + new uint[]{10921}, + new uint[]{10922}, + new uint[]{10500}, + new uint[]{10501}, + new uint[]{10502}, + new uint[]{10503}, + new uint[]{10504}, + new uint[]{10505}, + new uint[]{10506}, + new uint[]{10507}, + new uint[]{10508}, + new uint[]{10509}, + new uint[]{10510}, + new uint[]{10511}, + new uint[]{10518, 11201}, + new uint[]{10497}, + new uint[]{10920}, + new uint[]{10496}, + new uint[]{10720}, + new uint[]{10721}, + Array.Empty(), + new uint[]{10722}, + new uint[]{10720}, + new uint[]{10721}, + new uint[]{10724}, + Array.Empty(), + new uint[]{10722}, + new uint[]{10725}, + new uint[]{10726}, + new uint[]{10727}, + new uint[]{10728}, + new uint[]{10318}, + new uint[]{10319}, + new uint[]{10320}, + new uint[]{10321}, + new uint[]{10322}, + new uint[]{10323}, + new uint[]{10324}, + new uint[]{10325}, + new uint[]{10326}, + new uint[]{10327}, + new uint[]{10328}, + new uint[]{10329}, + new uint[]{10330}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10871}, + new uint[]{10869}, + new uint[]{10870}, + new uint[]{10873}, + new uint[]{10874}, + new uint[]{10875, 10901}, + new uint[]{10876}, + new uint[]{10877}, + new uint[]{10872}, + new uint[]{108}, + new uint[]{10878}, + new uint[]{10879}, + new uint[]{10881}, + new uint[]{10880}, + new uint[]{10882}, + new uint[]{10883}, + new uint[]{10884}, + new uint[]{10885}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10269}, + new uint[]{10926}, + new uint[]{10271}, + new uint[]{10271}, + new uint[]{10272}, + new uint[]{10272}, + new uint[]{10269}, + new uint[]{10269}, + new uint[]{10269}, + new uint[]{10479}, + new uint[]{10480}, + new uint[]{10481}, + new uint[]{10482}, + new uint[]{10483}, + new uint[]{10484}, + new uint[]{10485}, + new uint[]{10486}, + new uint[]{10919}, + new uint[]{10488}, + new uint[]{10490}, + new uint[]{10489}, + new uint[]{10492}, + new uint[]{10491}, + new uint[]{10479}, + new uint[]{11215}, + new uint[]{10731}, + new uint[]{10732}, + new uint[]{10730}, + new uint[]{10729}, + new uint[]{4636}, + new uint[]{4637}, + new uint[]{3799}, + new uint[]{10734}, + new uint[]{10735}, + new uint[]{10455}, + new uint[]{10455}, + new uint[]{10455}, + new uint[]{10455}, + new uint[]{10455}, + new uint[]{10455}, + new uint[]{10800}, + new uint[]{10341}, + new uint[]{10343}, + new uint[]{10344}, + new uint[]{10348}, + new uint[]{10348}, + new uint[]{10348}, + new uint[]{10348}, + new uint[]{10342}, + new uint[]{10266}, + new uint[]{10295}, + new uint[]{541}, + new uint[]{10438}, + Array.Empty(), + new uint[]{10441}, + new uint[]{10439}, + new uint[]{10436}, + new uint[]{10437}, + new uint[]{10294}, + new uint[]{10295}, + new uint[]{10296}, + new uint[]{10297}, + new uint[]{10908}, + new uint[]{10299}, + new uint[]{10911}, + new uint[]{10301}, + new uint[]{10302}, + new uint[]{10303}, + new uint[]{10304}, + new uint[]{10305}, + new uint[]{10306}, + new uint[]{10307}, + new uint[]{11196}, + new uint[]{11197}, + new uint[]{10525, 11204}, + new uint[]{11198}, + new uint[]{10312}, + new uint[]{10354}, + new uint[]{108}, + new uint[]{10278}, + new uint[]{10620}, + new uint[]{10619}, + new uint[]{10625}, + new uint[]{10634}, + new uint[]{10633}, + new uint[]{10729}, + new uint[]{10730}, + new uint[]{10731}, + new uint[]{10732}, + new uint[]{10733}, + new uint[]{10734}, + new uint[]{10735}, + new uint[]{4130}, + new uint[]{10337}, + new uint[]{10337}, + new uint[]{10338}, + new uint[]{10339}, + new uint[]{10340}, + new uint[]{10615}, + new uint[]{10273}, + new uint[]{10274}, + new uint[]{10270}, + new uint[]{10274}, + new uint[]{10270}, + new uint[]{10270}, + new uint[]{10274}, + Array.Empty(), + new uint[]{10277}, + new uint[]{10275}, + new uint[]{108}, + new uint[]{10622}, + new uint[]{10621}, + new uint[]{10624}, + new uint[]{10623}, + new uint[]{10629}, + new uint[]{10923}, + new uint[]{10924}, + new uint[]{10925}, + new uint[]{10926}, + new uint[]{10927}, + new uint[]{10928}, + new uint[]{10929}, + new uint[]{10930}, + new uint[]{10931}, + new uint[]{10932}, + new uint[]{10931}, + new uint[]{10928}, + new uint[]{10933}, + new uint[]{10934}, + new uint[]{10933}, + new uint[]{10935}, + new uint[]{10936}, + new uint[]{10904, 10937}, + new uint[]{10937}, + new uint[]{10937}, + new uint[]{10938}, + new uint[]{10939}, + new uint[]{10940}, + new uint[]{10940}, + new uint[]{10940}, + new uint[]{10941}, + new uint[]{10942}, + new uint[]{10632}, + new uint[]{10626}, + new uint[]{10742}, + new uint[]{108}, + new uint[]{10743}, + new uint[]{10744}, + new uint[]{10745}, + new uint[]{10742}, + new uint[]{10743}, + new uint[]{10943}, + new uint[]{10944}, + new uint[]{10418}, + new uint[]{10414}, + new uint[]{10415}, + new uint[]{10627}, + new uint[]{10617}, + new uint[]{10630}, + new uint[]{10647}, + new uint[]{6148}, + new uint[]{9678}, + new uint[]{10772}, + new uint[]{10259}, + new uint[]{2667}, + new uint[]{2667}, + new uint[]{108}, + new uint[]{10886}, + new uint[]{10887}, + new uint[]{10888}, + new uint[]{10889}, + new uint[]{10890}, + new uint[]{10891}, + new uint[]{10945}, + new uint[]{10631}, + new uint[]{10731}, + new uint[]{10732}, + new uint[]{10400}, + new uint[]{10513}, + new uint[]{10519, 10520}, + new uint[]{10523}, + new uint[]{10528, 10529}, + new uint[]{10530, 10531, 10532}, + new uint[]{11208, 11209}, + new uint[]{10537, 10538, 10539, 10540, 10541, 10543, 10544, 10545, 10546, 11200}, + new uint[]{10548}, + new uint[]{10549, 10550, 11211}, + new uint[]{10550, 11212}, + new uint[]{10551, 11213}, + new uint[]{10552}, + new uint[]{10553}, + new uint[]{10549}, + new uint[]{10550}, + new uint[]{11213}, + new uint[]{10552}, + new uint[]{5239}, + new uint[]{4130}, + new uint[]{9363}, + new uint[]{10557}, + new uint[]{10558, 11202}, + new uint[]{10559}, + new uint[]{10549}, + new uint[]{10550, 11212}, + new uint[]{10551, 11213}, + new uint[]{10552}, + new uint[]{10560}, + new uint[]{10561}, + new uint[]{10562}, + new uint[]{6146}, + new uint[]{4740}, + new uint[]{6153}, + new uint[]{6152}, + new uint[]{6149}, + new uint[]{11210}, + new uint[]{10013}, + new uint[]{8378}, + new uint[]{1492}, + new uint[]{10572}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + new uint[]{10575}, + new uint[]{108}, + new uint[]{10400}, + new uint[]{10400}, + new uint[]{10498}, + new uint[]{4736}, + new uint[]{10555}, + new uint[]{10554}, + new uint[]{10011}, + new uint[]{10013}, + new uint[]{10571}, + new uint[]{9348}, + new uint[]{10570}, + new uint[]{10278}, + new uint[]{10278}, + new uint[]{655}, + new uint[]{11165}, + new uint[]{10526, 11205}, + new uint[]{11206}, + new uint[]{11207}, + new uint[]{10444}, + new uint[]{10512}, + new uint[]{4386}, + new uint[]{10578}, + new uint[]{10579}, + new uint[]{10580}, + new uint[]{10581}, + new uint[]{10582}, + new uint[]{10584}, + new uint[]{10585}, + new uint[]{10893}, + new uint[]{10894}, + new uint[]{10895}, + new uint[]{10896}, + new uint[]{10897}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10618}, + new uint[]{10628}, + new uint[]{10616}, + new uint[]{10476}, + new uint[]{10477}, + new uint[]{10478}, + new uint[]{10937}, + new uint[]{10277}, + new uint[]{10273}, + new uint[]{10350}, + Array.Empty(), + new uint[]{10350}, + new uint[]{10667}, + new uint[]{10691}, + new uint[]{10692}, + new uint[]{10693}, + new uint[]{10694}, + new uint[]{10695}, + new uint[]{10696}, + new uint[]{10351}, + new uint[]{10352}, + new uint[]{10353}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10349}, + new uint[]{10355}, + new uint[]{108}, + new uint[]{10359}, + new uint[]{10360}, + new uint[]{10361}, + new uint[]{10358}, + new uint[]{10635}, + new uint[]{10636}, + new uint[]{10637}, + new uint[]{10638}, + new uint[]{10639}, + new uint[]{10640}, + Array.Empty(), + new uint[]{10642}, + new uint[]{10643}, + new uint[]{10644}, + new uint[]{10645}, + new uint[]{10646}, + new uint[]{11056}, + new uint[]{11057}, + new uint[]{11056}, + new uint[]{11055}, + new uint[]{11049}, + new uint[]{11050}, + new uint[]{11052}, + new uint[]{11053}, + new uint[]{11051}, + new uint[]{11054}, + new uint[]{11053}, + new uint[]{11052}, + new uint[]{11051}, + new uint[]{11050}, + new uint[]{11049}, + new uint[]{11048}, + new uint[]{11048}, + new uint[]{11048}, + new uint[]{11047}, + new uint[]{11046}, + new uint[]{11045}, + new uint[]{11044}, + new uint[]{11043}, + new uint[]{11042}, + new uint[]{11042}, + new uint[]{11042}, + new uint[]{11041}, + new uint[]{11041}, + new uint[]{11041}, + new uint[]{11040}, + new uint[]{11039}, + new uint[]{11038}, + new uint[]{11037}, + new uint[]{11036}, + new uint[]{11036}, + new uint[]{11036}, + new uint[]{11035}, + new uint[]{11238}, + new uint[]{11033}, + new uint[]{11033}, + new uint[]{11032}, + new uint[]{11032}, + new uint[]{11031}, + new uint[]{11030}, + new uint[]{11030}, + new uint[]{11029}, + new uint[]{11028}, + new uint[]{11027}, + new uint[]{11026}, + new uint[]{11025}, + new uint[]{11024}, + new uint[]{11023}, + new uint[]{10408}, + new uint[]{10409}, + new uint[]{10408}, + new uint[]{10409}, + new uint[]{10412}, + new uint[]{10409}, + new uint[]{541}, + new uint[]{11022}, + new uint[]{11021}, + new uint[]{11020}, + new uint[]{11019}, + new uint[]{11018}, + new uint[]{11017}, + new uint[]{11016}, + new uint[]{11015}, + new uint[]{11014}, + new uint[]{11013}, + new uint[]{11012}, + new uint[]{11011}, + new uint[]{11010}, + new uint[]{11009}, + new uint[]{11008}, + new uint[]{11007}, + new uint[]{11006}, + new uint[]{11005}, + new uint[]{11004}, + new uint[]{11003}, + new uint[]{11002}, + new uint[]{11001}, + new uint[]{11000}, + new uint[]{10999}, + new uint[]{10998}, + new uint[]{10997}, + new uint[]{10996}, + new uint[]{10995}, + new uint[]{10994}, + new uint[]{10947}, + new uint[]{10993}, + new uint[]{10992}, + new uint[]{10990}, + new uint[]{10991}, + new uint[]{10990}, + new uint[]{10989}, + new uint[]{10989}, + new uint[]{10988}, + new uint[]{10988}, + new uint[]{10987}, + new uint[]{10986}, + new uint[]{10986}, + new uint[]{10985}, + new uint[]{10984}, + new uint[]{10984}, + new uint[]{10984}, + new uint[]{10983}, + new uint[]{10983}, + new uint[]{10982}, + new uint[]{10981}, + new uint[]{10980}, + new uint[]{10979}, + new uint[]{10978}, + new uint[]{10977}, + new uint[]{11047}, + new uint[]{10976}, + new uint[]{10975}, + new uint[]{10974}, + new uint[]{10973}, + new uint[]{10972}, + new uint[]{10971}, + new uint[]{10970}, + new uint[]{10969}, + new uint[]{10968}, + new uint[]{10967}, + new uint[]{10966}, + new uint[]{10965}, + new uint[]{10964}, + new uint[]{10963}, + new uint[]{10962}, + new uint[]{10961}, + new uint[]{10960}, + new uint[]{10959}, + new uint[]{10958}, + new uint[]{10957}, + new uint[]{10956}, + new uint[]{10955}, + new uint[]{10954}, + new uint[]{10953}, + new uint[]{10952}, + new uint[]{10951}, + new uint[]{10950}, + new uint[]{10949}, + new uint[]{10948}, + new uint[]{10947}, + new uint[]{10946}, + new uint[]{108}, + new uint[]{108}, + Array.Empty(), + new uint[]{2095}, + new uint[]{1383}, + Array.Empty(), + new uint[]{10276}, + new uint[]{10276}, + new uint[]{10273}, + new uint[]{11193}, + new uint[]{10270, 10274}, + new uint[]{10276}, + new uint[]{10385}, + new uint[]{10900}, + new uint[]{10391}, + new uint[]{10370}, + new uint[]{10368}, + new uint[]{10793}, + new uint[]{10794}, + new uint[]{10804}, + new uint[]{11216}, + new uint[]{10749}, + new uint[]{10750}, + new uint[]{10756}, + new uint[]{10757}, + new uint[]{10760}, + new uint[]{10758}, + new uint[]{10754}, + new uint[]{10759}, + new uint[]{10755}, + new uint[]{10761}, + new uint[]{10796}, + new uint[]{10795}, + new uint[]{10798}, + Array.Empty(), + new uint[]{10805}, + new uint[]{10807}, + new uint[]{10801}, + new uint[]{10802}, + new uint[]{10830}, + new uint[]{10803}, + new uint[]{10797}, + new uint[]{10806}, + new uint[]{10641}, + new uint[]{10923}, + new uint[]{10924}, + new uint[]{10927}, + new uint[]{10926}, + new uint[]{10490}, + new uint[]{10746}, + new uint[]{10747}, + new uint[]{10748}, + new uint[]{10751}, + new uint[]{10752}, + new uint[]{10274}, + new uint[]{10274}, + new uint[]{11144}, + new uint[]{11188}, + new uint[]{11145}, + new uint[]{11189}, + new uint[]{11189}, + new uint[]{11146}, + new uint[]{11147}, + new uint[]{11148}, + new uint[]{10819}, + new uint[]{10818}, + new uint[]{10811}, + new uint[]{10816}, + new uint[]{10812}, + new uint[]{10817}, + new uint[]{10814}, + new uint[]{10815}, + new uint[]{10810}, + new uint[]{10823}, + new uint[]{10820}, + new uint[]{10821}, + new uint[]{10822}, + new uint[]{11149}, + new uint[]{10824}, + new uint[]{11168}, + new uint[]{11169}, + new uint[]{10753}, + new uint[]{10831}, + new uint[]{10832}, + new uint[]{10773}, + new uint[]{10774}, + new uint[]{10775}, + new uint[]{10776}, + new uint[]{10777}, + new uint[]{10778}, + new uint[]{10779}, + new uint[]{10780}, + new uint[]{10781}, + new uint[]{10782}, + new uint[]{10783}, + new uint[]{10784}, + new uint[]{10736}, + new uint[]{10737}, + new uint[]{10738}, + new uint[]{10739}, + new uint[]{10740}, + new uint[]{10741}, + new uint[]{11153}, + new uint[]{11181}, + new uint[]{11180}, + new uint[]{11179}, + new uint[]{11154}, + new uint[]{11182}, + new uint[]{11183}, + new uint[]{11155}, + new uint[]{11156}, + new uint[]{11157}, + new uint[]{11158}, + new uint[]{11159}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{10785}, + new uint[]{11093}, + new uint[]{11094}, + new uint[]{11095}, + new uint[]{11096}, + new uint[]{11097}, + new uint[]{11098}, + new uint[]{11099}, + new uint[]{11100}, + new uint[]{11101}, + new uint[]{11102}, + new uint[]{11103}, + new uint[]{11104}, + new uint[]{11105}, + new uint[]{11106}, + new uint[]{11107}, + new uint[]{11108}, + new uint[]{11109}, + new uint[]{11110}, + new uint[]{11111}, + new uint[]{11112}, + new uint[]{11113}, + new uint[]{11114}, + new uint[]{11115}, + new uint[]{11116}, + new uint[]{11117}, + new uint[]{11118}, + new uint[]{11119}, + new uint[]{11120}, + new uint[]{11121}, + new uint[]{11122}, + new uint[]{11123}, + new uint[]{11124}, + new uint[]{11125}, + new uint[]{11126}, + new uint[]{11127}, + new uint[]{11128}, + new uint[]{11129}, + new uint[]{11130}, + new uint[]{11131}, + new uint[]{11132}, + new uint[]{11133}, + new uint[]{11134}, + new uint[]{11135}, + new uint[]{11136}, + new uint[]{11137}, + new uint[]{11138}, + new uint[]{11139}, + new uint[]{11140}, + new uint[]{11141}, + new uint[]{11142}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{11150}, + new uint[]{11151}, + new uint[]{11152}, + Array.Empty(), + Array.Empty(), + new uint[]{10813}, + new uint[]{10771}, + new uint[]{10790}, + new uint[]{10791}, + new uint[]{10786}, + new uint[]{10787}, + new uint[]{10788}, + new uint[]{10789}, + new uint[]{10813}, + new uint[]{10376}, + new uint[]{11077}, + new uint[]{11184}, + new uint[]{11078}, + new uint[]{11185}, + new uint[]{11186}, + new uint[]{11079}, + new uint[]{11080}, + new uint[]{11081}, + new uint[]{11082}, + new uint[]{11083}, + new uint[]{11084}, + new uint[]{11085}, + new uint[]{11187}, + new uint[]{11086}, + new uint[]{11087}, + new uint[]{11088}, + new uint[]{11089}, + new uint[]{11090}, + new uint[]{11091}, + new uint[]{11092}, + new uint[]{10825}, + new uint[]{10826}, + new uint[]{10721}, + new uint[]{10721}, + new uint[]{10827, 10828}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10833}, + new uint[]{10834}, + new uint[]{10835}, + new uint[]{10836}, + new uint[]{10837}, + new uint[]{10838}, + new uint[]{10839}, + new uint[]{10840}, + new uint[]{10841}, + new uint[]{10842}, + new uint[]{10843}, + new uint[]{10844}, + new uint[]{10845}, + new uint[]{10846}, + new uint[]{10847}, + new uint[]{10848}, + new uint[]{10849}, + new uint[]{10850}, + new uint[]{10851}, + new uint[]{10852}, + new uint[]{10853}, + new uint[]{10854}, + new uint[]{10855}, + new uint[]{10856}, + new uint[]{10857}, + new uint[]{10858}, + new uint[]{10859}, + new uint[]{10860}, + new uint[]{10861}, + new uint[]{10862}, + new uint[]{10863}, + new uint[]{10864}, + new uint[]{10865}, + new uint[]{10866}, + new uint[]{10867}, + new uint[]{10868}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{10347}, + new uint[]{11176}, + new uint[]{11177}, + new uint[]{11178}, + new uint[]{11174}, + new uint[]{11175}, + new uint[]{655}, + new uint[]{11165}, + new uint[]{8273}, + Array.Empty(), + new uint[]{10577}, + new uint[]{11073}, + new uint[]{11074}, + new uint[]{11075}, + new uint[]{11076}, + new uint[]{11072}, + new uint[]{11167}, + new uint[]{10365}, + new uint[]{11143}, + new uint[]{11160}, + new uint[]{11143}, + new uint[]{11161}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11172}, + new uint[]{11173}, + new uint[]{10542, 10547, 11203}, + new uint[]{11191}, + new uint[]{5964}, + new uint[]{108}, + new uint[]{108}, + new uint[]{10310}, + new uint[]{10536}, + new uint[]{11195}, + new uint[]{108}, + new uint[]{10315}, + new uint[]{10908}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11192}, + new uint[]{11192}, + new uint[]{11192}, + new uint[]{108}, + Array.Empty(), + new uint[]{10726}, + new uint[]{11194}, + new uint[]{10908}, + new uint[]{2137}, + new uint[]{2139}, + new uint[]{2140}, + new uint[]{2141}, + new uint[]{2138}, + new uint[]{2324}, + new uint[]{2142}, + Array.Empty(), + new uint[]{10270, 10274}, + new uint[]{10499}, + new uint[]{11199}, + new uint[]{10559}, + new uint[]{510, 2137}, + new uint[]{108}, + new uint[]{10348}, + new uint[]{10348}, + new uint[]{10904}, + new uint[]{11239}, + new uint[]{11240}, + new uint[]{11171}, + new uint[]{11170}, + new uint[]{554}, + new uint[]{11307}, + new uint[]{2134}, + new uint[]{2135}, + new uint[]{2121}, + new uint[]{2136}, + new uint[]{11285}, + new uint[]{11285}, + new uint[]{9947}, + new uint[]{11241}, + new uint[]{11242}, + new uint[]{11243}, + new uint[]{548}, + new uint[]{108}, + new uint[]{10893}, + new uint[]{10894}, + new uint[]{10895}, + new uint[]{10896}, + new uint[]{10893}, + new uint[]{10894}, + new uint[]{10895}, + new uint[]{10896}, + new uint[]{11350}, + new uint[]{11351}, + new uint[]{11353}, + new uint[]{11244}, + new uint[]{11245}, + new uint[]{11246}, + new uint[]{11247}, + new uint[]{11248}, + new uint[]{11245}, + new uint[]{11246}, + new uint[]{11249}, + new uint[]{11248}, + new uint[]{11250}, + new uint[]{11251}, + new uint[]{11252}, + new uint[]{11253}, + new uint[]{11254}, + new uint[]{11255}, + new uint[]{11256}, + new uint[]{11257}, + new uint[]{11258}, + new uint[]{11259}, + new uint[]{11260}, + new uint[]{11261}, + new uint[]{11262}, + new uint[]{11272}, + new uint[]{11286}, + new uint[]{11290}, + new uint[]{11244}, + new uint[]{11245}, + new uint[]{11246}, + new uint[]{11247}, + new uint[]{11248}, + new uint[]{11308}, + new uint[]{422}, + new uint[]{428}, + new uint[]{424}, + new uint[]{11218}, + new uint[]{11219}, + new uint[]{11220}, + new uint[]{11221}, + new uint[]{11222}, + new uint[]{11223}, + new uint[]{11224}, + new uint[]{11225}, + new uint[]{11226}, + new uint[]{11227}, + new uint[]{11228}, + new uint[]{11229}, + new uint[]{11230}, + new uint[]{11231}, + new uint[]{11232}, + new uint[]{11233}, + new uint[]{11234}, + new uint[]{11235}, + new uint[]{11236}, + new uint[]{11237}, + new uint[]{10448}, + new uint[]{5640}, + new uint[]{10448}, + new uint[]{10446}, + new uint[]{10445}, + new uint[]{10444}, + new uint[]{1279}, + new uint[]{11352}, + new uint[]{1678}, + new uint[]{9910}, + new uint[]{101}, + new uint[]{557}, + new uint[]{2106}, + new uint[]{2109}, + new uint[]{2116}, + new uint[]{2118}, + new uint[]{11216}, + new uint[]{9910}, + new uint[]{11277}, + new uint[]{11278}, + new uint[]{11322}, + new uint[]{11279}, + new uint[]{11280}, + new uint[]{11302}, + new uint[]{11303}, + new uint[]{11304}, + new uint[]{11305}, + new uint[]{11292}, + new uint[]{11293}, + new uint[]{11294}, + new uint[]{2124}, + new uint[]{2125}, + new uint[]{2126}, + new uint[]{2128}, + new uint[]{2130}, + new uint[]{2092}, + new uint[]{2121}, + new uint[]{2089}, + new uint[]{297}, + new uint[]{2133}, + new uint[]{1486}, + new uint[]{2105}, + new uint[]{2106}, + new uint[]{2107}, + new uint[]{2108}, + new uint[]{2109}, + new uint[]{2110}, + new uint[]{2111}, + new uint[]{2112}, + new uint[]{2113}, + new uint[]{2121}, + new uint[]{269}, + Array.Empty(), + new uint[]{1486}, + new uint[]{2160}, + new uint[]{11275}, + new uint[]{11276}, + new uint[]{11273}, + new uint[]{11274}, + new uint[]{11274}, + new uint[]{2137}, + new uint[]{2141}, + new uint[]{2140}, + new uint[]{1804}, + new uint[]{2139}, + new uint[]{2137}, + new uint[]{2142}, + new uint[]{2138}, + new uint[]{11286}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{11290}, + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{11281}, + new uint[]{11282}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{11284}, + new uint[]{108}, + new uint[]{11419}, + new uint[]{2132}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11446}, + new uint[]{718}, + new uint[]{719}, + new uint[]{720}, + new uint[]{721}, + new uint[]{722}, + new uint[]{723}, + new uint[]{724}, + new uint[]{725}, + new uint[]{11258}, + new uint[]{11288}, + new uint[]{11289}, + new uint[]{3369}, + new uint[]{3370}, + new uint[]{3371}, + new uint[]{3374}, + new uint[]{3375}, + new uint[]{3375}, + new uint[]{3375}, + new uint[]{3375}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{2143}, + new uint[]{4384}, + new uint[]{2183}, + new uint[]{11326}, + new uint[]{11327}, + new uint[]{11328}, + new uint[]{11329}, + new uint[]{11330}, + new uint[]{11331}, + new uint[]{11332}, + new uint[]{11333}, + new uint[]{11330}, + new uint[]{11331}, + new uint[]{11332}, + new uint[]{11333}, + new uint[]{11334}, + new uint[]{11331, 11335}, + new uint[]{11332, 11336}, + new uint[]{11333, 11337}, + Array.Empty(), + new uint[]{269}, + new uint[]{297}, + new uint[]{11420}, + new uint[]{11421}, + new uint[]{11233}, + new uint[]{297}, + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{108}, + new uint[]{3667}, + new uint[]{11338}, + new uint[]{11339}, + new uint[]{11340}, + new uint[]{11341}, + new uint[]{10961}, + new uint[]{11342}, + new uint[]{11343}, + new uint[]{11344}, + new uint[]{11345}, + new uint[]{11346}, + new uint[]{11346}, + new uint[]{11346}, + new uint[]{11347}, + new uint[]{11348}, + new uint[]{11349}, + new uint[]{11323}, + new uint[]{11324}, + new uint[]{11325}, + new uint[]{11277}, + new uint[]{11302}, + new uint[]{11382}, + new uint[]{11382}, + new uint[]{11382}, + new uint[]{11354}, + new uint[]{428}, + new uint[]{11355}, + new uint[]{11355}, + new uint[]{11384}, + Array.Empty(), + new uint[]{11355}, + new uint[]{3455}, + Array.Empty(), + new uint[]{11386}, + new uint[]{108}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + new uint[]{3040}, + new uint[]{3042}, + new uint[]{3044}, + new uint[]{3045}, + Array.Empty(), + new uint[]{11357}, + new uint[]{11358}, + new uint[]{11359}, + new uint[]{11360}, + new uint[]{11361}, + new uint[]{11362}, + new uint[]{11363}, + new uint[]{11364}, + new uint[]{11365}, + new uint[]{11366}, + new uint[]{11367}, + new uint[]{11368}, + new uint[]{11382}, + new uint[]{11422}, + Array.Empty(), + new uint[]{11442}, + new uint[]{11443}, + new uint[]{11330}, + new uint[]{11313}, + new uint[]{11313}, + new uint[]{10013}, + new uint[]{4130}, + new uint[]{4130}, + new uint[]{11431}, + new uint[]{11433}, + new uint[]{2077}, + new uint[]{1455}, + new uint[]{1455}, + new uint[]{10261}, + new uint[]{10261}, + new uint[]{4130}, + new uint[]{4130}, + new uint[]{4149}, + new uint[]{4149}, + new uint[]{11447}, + new uint[]{11387}, + new uint[]{11389}, + new uint[]{11390}, + new uint[]{11391}, + new uint[]{11392}, + Array.Empty(), + new uint[]{11387}, + new uint[]{11388}, + new uint[]{11389}, + new uint[]{11390}, + new uint[]{11393}, + new uint[]{11393}, + new uint[]{11394}, + new uint[]{11394}, + new uint[]{11395}, + new uint[]{11395}, + new uint[]{11396}, + new uint[]{11396}, + new uint[]{11397}, + new uint[]{11397}, + new uint[]{11407}, + new uint[]{11408}, + new uint[]{11409}, + new uint[]{11410}, + new uint[]{11407}, + new uint[]{11411}, + Array.Empty(), + new uint[]{11413}, + new uint[]{11414}, + new uint[]{11415}, + new uint[]{11415}, + new uint[]{11416}, + new uint[]{11417}, + new uint[]{11418}, + new uint[]{11418}, + Array.Empty(), + new uint[]{108}, + Array.Empty(), + new uint[]{11372}, + new uint[]{11373}, + new uint[]{11373}, + new uint[]{108}, + new uint[]{11443}, + new uint[]{3458}, + new uint[]{3460}, + new uint[]{3462}, + new uint[]{3464}, + new uint[]{10298}, + new uint[]{11398}, + new uint[]{10300}, + new uint[]{10298}, + new uint[]{11398}, + new uint[]{10300}, + new uint[]{108}, + new uint[]{8378}, + new uint[]{11416}, + new uint[]{11416}, + Array.Empty(), + Array.Empty(), + new uint[]{4776}, + new uint[]{4776}, + new uint[]{4777}, + new uint[]{4778}, + new uint[]{4779}, + new uint[]{4780}, + new uint[]{4776}, + new uint[]{11393}, + new uint[]{11394}, + new uint[]{11395}, + new uint[]{11396}, + new uint[]{11397}, + new uint[]{11440}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11441}, + new uint[]{11440}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11441}, + Array.Empty(), + new uint[]{11369}, + new uint[]{11370}, + new uint[]{11371}, + new uint[]{11369}, + new uint[]{11370}, + new uint[]{11371}, + new uint[]{11369}, + new uint[]{11370}, + new uint[]{11371}, + new uint[]{11469}, + new uint[]{11470}, + new uint[]{11471}, + new uint[]{11472}, + new uint[]{11473}, + Array.Empty(), + new uint[]{11474}, + Array.Empty(), + new uint[]{11476}, + new uint[]{11477}, + new uint[]{11478}, + new uint[]{11479}, + new uint[]{11480}, + new uint[]{11481}, + Array.Empty(), + Array.Empty(), + new uint[]{11484}, + new uint[]{11485}, + new uint[]{11486}, + new uint[]{11487}, + new uint[]{11488}, + new uint[]{11490}, + new uint[]{11491}, + new uint[]{11492}, + new uint[]{11387}, + new uint[]{11388}, + new uint[]{11389}, + new uint[]{11390}, + new uint[]{11493}, + new uint[]{11494}, + new uint[]{11495}, + Array.Empty(), + new uint[]{11496}, + new uint[]{11497}, + new uint[]{11498}, + new uint[]{11499}, + new uint[]{11500}, + new uint[]{11501}, + new uint[]{11502}, + new uint[]{11503}, + new uint[]{11504}, + new uint[]{11506}, + new uint[]{11507}, + new uint[]{11508}, + new uint[]{11509}, + new uint[]{11489}, + new uint[]{11505}, + Array.Empty(), + new uint[]{11374}, + new uint[]{11375}, + new uint[]{11376}, + new uint[]{11378}, + new uint[]{11379}, + new uint[]{11374}, + new uint[]{11375}, + new uint[]{11376}, + new uint[]{11377}, + new uint[]{11378}, + new uint[]{11379}, + new uint[]{11380}, + new uint[]{108}, + new uint[]{11381}, + new uint[]{11462}, + new uint[]{11381}, + new uint[]{11462}, + new uint[]{11462}, + new uint[]{392}, + new uint[]{795}, + new uint[]{5}, + new uint[]{6}, + new uint[]{341}, + new uint[]{11448}, + new uint[]{37}, + new uint[]{262}, + new uint[]{11449}, + new uint[]{11450}, + new uint[]{393}, + new uint[]{11451}, + new uint[]{11452}, + new uint[]{11453}, + new uint[]{780}, + new uint[]{6942}, + new uint[]{11454}, + new uint[]{10173}, + new uint[]{357}, + new uint[]{11455}, + new uint[]{11456}, + new uint[]{11457}, + new uint[]{1281}, + new uint[]{2769}, + new uint[]{11458}, + new uint[]{11459}, + new uint[]{3499}, + new uint[]{11460}, + new uint[]{353}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3330}, + new uint[]{11423}, + new uint[]{11424}, + new uint[]{11425}, + new uint[]{11426}, + new uint[]{11427}, + new uint[]{11428}, + new uint[]{11429}, + new uint[]{11430}, + new uint[]{11987}, + new uint[]{11988}, + new uint[]{11989}, + new uint[]{11990}, + new uint[]{11991}, + new uint[]{11439}, + new uint[]{11439}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11438}, + new uint[]{11436}, + new uint[]{11431}, + new uint[]{11432}, + Array.Empty(), + Array.Empty(), + new uint[]{11435}, + new uint[]{11436}, + Array.Empty(), + Array.Empty(), + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{2168}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11438}, + new uint[]{11436}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{3323}, + new uint[]{3323}, + new uint[]{3323}, + new uint[]{3323}, + new uint[]{3323}, + new uint[]{3321}, + new uint[]{3323}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3326}, + new uint[]{3329}, + new uint[]{3321}, + new uint[]{3329}, + new uint[]{3323}, + new uint[]{3321}, + new uint[]{3323}, + new uint[]{3326}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3329}, + new uint[]{3324}, + new uint[]{3323}, + new uint[]{3323}, + new uint[]{3327}, + new uint[]{3326}, + new uint[]{3321}, + new uint[]{3326}, + new uint[]{3322}, + new uint[]{3329}, + new uint[]{11431}, + new uint[]{11436}, + new uint[]{11399}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11517}, + new uint[]{11399}, + new uint[]{108}, + new uint[]{108}, + new uint[]{11517}, + new uint[]{11405}, + new uint[]{11405}, + new uint[]{11404}, + new uint[]{11404}, + new uint[]{11513}, + new uint[]{11481}, + new uint[]{0, 11511}, + new uint[]{11512}, + new uint[]{11514}, + new uint[]{11510}, + new uint[]{11506}, + new uint[]{11515}, + new uint[]{108}, + new uint[]{11513}, + new uint[]{11481}, + new uint[]{11511}, + new uint[]{11512}, + new uint[]{11514}, + new uint[]{11510}, + new uint[]{11506}, + new uint[]{11515}, + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{11402}, + new uint[]{11406}, + new uint[]{11406}, + Array.Empty(), + Array.Empty(), + new uint[]{11462}, + new uint[]{11462}, + new uint[]{11391}, + new uint[]{11468}, + Array.Empty(), + new uint[]{11405}, + Array.Empty(), + Array.Empty(), + new uint[]{11521}, + new uint[]{11522}, + new uint[]{11523}, + new uint[]{11524}, + new uint[]{11525}, + new uint[]{11526}, + new uint[]{11527}, + new uint[]{11528}, + new uint[]{11529}, + new uint[]{11530}, + new uint[]{11531}, + new uint[]{11532}, + new uint[]{11533}, + new uint[]{11534}, + new uint[]{11535}, + new uint[]{11536}, + new uint[]{11537}, + new uint[]{11538}, + new uint[]{11539}, + new uint[]{11540}, + new uint[]{11541}, + new uint[]{11542}, + new uint[]{11543}, + new uint[]{11544}, + new uint[]{11545}, + new uint[]{11546}, + new uint[]{11547}, + new uint[]{11548}, + new uint[]{11549}, + new uint[]{11550}, + new uint[]{11551}, + new uint[]{11552}, + new uint[]{11553}, + new uint[]{11554}, + new uint[]{11555}, + new uint[]{11556}, + new uint[]{11557}, + new uint[]{11558}, + new uint[]{11559}, + new uint[]{11560}, + new uint[]{11561}, + new uint[]{11562}, + new uint[]{11563}, + new uint[]{11564}, + new uint[]{11565}, + new uint[]{11566}, + new uint[]{11567}, + new uint[]{11568}, + new uint[]{11569}, + new uint[]{11570}, + new uint[]{11571}, + new uint[]{11572}, + new uint[]{11573}, + new uint[]{11574}, + new uint[]{11575}, + new uint[]{11576}, + new uint[]{11577}, + new uint[]{11578}, + new uint[]{11579}, + new uint[]{11580}, + new uint[]{11581}, + new uint[]{11582}, + new uint[]{11583}, + new uint[]{11584}, + new uint[]{11585}, + new uint[]{11586}, + Array.Empty(), + new uint[]{11588}, + new uint[]{11589}, + new uint[]{11590}, + Array.Empty(), + new uint[]{11592}, + new uint[]{11593}, + new uint[]{11594}, + new uint[]{11595}, + new uint[]{11596}, + new uint[]{11597}, + new uint[]{11598}, + new uint[]{11599}, + new uint[]{11600}, + new uint[]{11601}, + new uint[]{11602}, + new uint[]{11603}, + new uint[]{11604}, + new uint[]{11605}, + new uint[]{11606}, + new uint[]{11607}, + new uint[]{11608}, + new uint[]{11609}, + new uint[]{11610}, + new uint[]{11611}, + new uint[]{11612}, + new uint[]{11613}, + new uint[]{11614}, + new uint[]{11615}, + new uint[]{11616}, + new uint[]{11617}, + new uint[]{11618}, + new uint[]{11619}, + new uint[]{11620}, + new uint[]{11621}, + new uint[]{11622}, + new uint[]{11623}, + new uint[]{11624}, + new uint[]{11625}, + new uint[]{11626}, + new uint[]{11627}, + new uint[]{11628}, + new uint[]{11629}, + new uint[]{11630}, + new uint[]{11631}, + new uint[]{11632}, + new uint[]{11633}, + new uint[]{11634}, + new uint[]{11635}, + new uint[]{11636}, + new uint[]{11637}, + new uint[]{11638}, + new uint[]{11639}, + Array.Empty(), + new uint[]{11641}, + new uint[]{11642}, + new uint[]{11643}, + new uint[]{11644}, + new uint[]{11645}, + new uint[]{11646}, + new uint[]{11647}, + new uint[]{11648}, + new uint[]{11649}, + new uint[]{11650}, + new uint[]{11651}, + new uint[]{11652}, + new uint[]{11653}, + new uint[]{11654}, + new uint[]{11655}, + new uint[]{11656}, + new uint[]{11657}, + new uint[]{11658}, + new uint[]{11659}, + new uint[]{11660}, + new uint[]{11661}, + new uint[]{11662}, + new uint[]{11663}, + new uint[]{11664}, + new uint[]{11665}, + new uint[]{11666}, + new uint[]{11667}, + new uint[]{11668}, + new uint[]{11669}, + new uint[]{11670}, + new uint[]{11671}, + new uint[]{11672}, + Array.Empty(), + new uint[]{11674}, + new uint[]{11675}, + new uint[]{11676}, + new uint[]{11677}, + new uint[]{11678}, + new uint[]{11679}, + new uint[]{11680}, + new uint[]{11681}, + new uint[]{11682}, + new uint[]{11683}, + new uint[]{11684}, + new uint[]{11685}, + new uint[]{11686}, + new uint[]{11687}, + new uint[]{11688}, + new uint[]{11689}, + new uint[]{11690}, + new uint[]{11691}, + new uint[]{11692}, + new uint[]{11693}, + new uint[]{11694}, + new uint[]{11695}, + new uint[]{11696}, + new uint[]{11697}, + new uint[]{11698}, + new uint[]{11699}, + new uint[]{11700}, + new uint[]{11701}, + new uint[]{11702}, + new uint[]{11703}, + new uint[]{11704}, + new uint[]{11705}, + new uint[]{11706}, + new uint[]{11707}, + new uint[]{11708}, + new uint[]{11709}, + new uint[]{11710}, + new uint[]{11711}, + new uint[]{11712}, + new uint[]{11713}, + new uint[]{11714}, + new uint[]{11715}, + new uint[]{11716}, + new uint[]{11717}, + new uint[]{11718}, + new uint[]{11719}, + new uint[]{11720}, + new uint[]{11721}, + new uint[]{11722}, + new uint[]{11723}, + new uint[]{11724}, + new uint[]{11725}, + new uint[]{11726}, + new uint[]{11727}, + new uint[]{11728}, + new uint[]{11729}, + new uint[]{11730}, + new uint[]{11731}, + new uint[]{11732}, + Array.Empty(), + new uint[]{11734}, + new uint[]{11735}, + new uint[]{11736}, + new uint[]{11737}, + new uint[]{11738}, + new uint[]{11739}, + new uint[]{11740}, + new uint[]{11741}, + new uint[]{11742}, + Array.Empty(), + new uint[]{11744}, + new uint[]{11745}, + new uint[]{11746}, + new uint[]{11747}, + new uint[]{11748}, + new uint[]{11749}, + new uint[]{11750}, + new uint[]{11751}, + new uint[]{11752}, + new uint[]{11753}, + new uint[]{11754}, + new uint[]{11755}, + new uint[]{11756}, + new uint[]{11757}, + new uint[]{11758}, + new uint[]{11759}, + new uint[]{11760}, + new uint[]{11761}, + new uint[]{11762}, + new uint[]{11763}, + new uint[]{11764}, + new uint[]{11765}, + new uint[]{11766}, + new uint[]{11767}, + new uint[]{11768}, + new uint[]{11769}, + new uint[]{11770}, + new uint[]{11771}, + new uint[]{11772}, + new uint[]{11773}, + new uint[]{11774}, + new uint[]{11775}, + new uint[]{11776}, + new uint[]{11777}, + new uint[]{11778}, + new uint[]{11779}, + new uint[]{11780}, + new uint[]{11781}, + new uint[]{11782}, + new uint[]{11783}, + new uint[]{11784}, + new uint[]{11785}, + new uint[]{11786}, + new uint[]{11787}, + new uint[]{11788}, + new uint[]{11789}, + new uint[]{11790}, + new uint[]{11791}, + new uint[]{11792}, + new uint[]{11793}, + new uint[]{11794}, + new uint[]{11795}, + new uint[]{11796}, + new uint[]{11797}, + new uint[]{11798}, + new uint[]{11799}, + new uint[]{11800}, + new uint[]{11801}, + new uint[]{11802}, + new uint[]{11803}, + new uint[]{11804}, + new uint[]{11805}, + new uint[]{11806}, + new uint[]{11807}, + new uint[]{11808}, + new uint[]{11809}, + new uint[]{11810}, + new uint[]{11811}, + new uint[]{11812}, + new uint[]{11813}, + new uint[]{11814}, + new uint[]{11815}, + new uint[]{11816}, + new uint[]{11817}, + new uint[]{11818}, + new uint[]{11819}, + new uint[]{11820}, + new uint[]{11821}, + new uint[]{11822}, + new uint[]{11823}, + new uint[]{11824}, + new uint[]{11825}, + new uint[]{11826}, + new uint[]{11827}, + new uint[]{11828}, + new uint[]{11829}, + new uint[]{11830}, + new uint[]{11831}, + new uint[]{11832}, + new uint[]{11833}, + new uint[]{11834}, + new uint[]{11835}, + new uint[]{11836}, + new uint[]{11837}, + new uint[]{11838}, + new uint[]{11839}, + new uint[]{11840}, + new uint[]{11841}, + new uint[]{11842}, + new uint[]{11843}, + new uint[]{11844}, + new uint[]{11845}, + new uint[]{11846}, + new uint[]{11847}, + new uint[]{11848}, + new uint[]{11849}, + new uint[]{11850}, + new uint[]{11851}, + new uint[]{11852}, + new uint[]{11853}, + new uint[]{11854}, + new uint[]{11855}, + new uint[]{11856}, + new uint[]{11857}, + new uint[]{11858}, + new uint[]{11859}, + new uint[]{11860}, + new uint[]{11861}, + new uint[]{11862}, + new uint[]{11863}, + new uint[]{11864}, + new uint[]{11865}, + new uint[]{11866}, + new uint[]{11867}, + new uint[]{11868}, + new uint[]{11869}, + new uint[]{11870}, + new uint[]{11871}, + new uint[]{11872}, + new uint[]{11873}, + new uint[]{11874}, + new uint[]{11875}, + new uint[]{11876}, + new uint[]{11877}, + new uint[]{11878}, + new uint[]{11879}, + new uint[]{11880}, + new uint[]{11881}, + new uint[]{11882}, + new uint[]{11883}, + new uint[]{11884}, + new uint[]{11885}, + new uint[]{11886}, + new uint[]{11887}, + new uint[]{11888}, + new uint[]{11889}, + new uint[]{11890}, + new uint[]{11891}, + new uint[]{11892}, + new uint[]{11893}, + new uint[]{11894}, + new uint[]{11895}, + new uint[]{11896}, + new uint[]{11897}, + new uint[]{11898}, + new uint[]{11899}, + new uint[]{11900}, + new uint[]{11901}, + new uint[]{11902}, + new uint[]{11903}, + new uint[]{11904}, + new uint[]{11905}, + new uint[]{11906}, + new uint[]{11907}, + new uint[]{11908}, + new uint[]{11909}, + new uint[]{11910}, + new uint[]{11911}, + new uint[]{11912}, + new uint[]{11913}, + new uint[]{11914}, + new uint[]{11915}, + new uint[]{11916}, + new uint[]{11917}, + new uint[]{11918}, + new uint[]{11919}, + new uint[]{11920}, + new uint[]{11921}, + new uint[]{11922}, + new uint[]{11923}, + new uint[]{11924}, + new uint[]{11925}, + new uint[]{11926}, + new uint[]{11927}, + new uint[]{11928}, + new uint[]{11929}, + new uint[]{11930}, + new uint[]{11931}, + new uint[]{11932}, + new uint[]{11933}, + new uint[]{11934}, + new uint[]{11935}, + new uint[]{11936}, + new uint[]{11937}, + new uint[]{11938}, + new uint[]{11939}, + new uint[]{11940}, + new uint[]{11941}, + new uint[]{11942}, + new uint[]{11943}, + new uint[]{11944}, + new uint[]{11945}, + new uint[]{11946}, + new uint[]{11947}, + new uint[]{11948}, + new uint[]{11949}, + new uint[]{11950}, + new uint[]{11951}, + new uint[]{11952}, + new uint[]{11953}, + new uint[]{11954}, + new uint[]{11955}, + new uint[]{11956}, + new uint[]{11957}, + new uint[]{11958}, + new uint[]{11959}, + new uint[]{11960}, + new uint[]{11961}, + new uint[]{11962}, + new uint[]{11963}, + new uint[]{11964}, + new uint[]{11965}, + new uint[]{11966}, + new uint[]{11967}, + new uint[]{11968}, + new uint[]{11969}, + new uint[]{11970}, + new uint[]{11971}, + new uint[]{11972}, + new uint[]{11973}, + Array.Empty(), + new uint[]{11975}, + new uint[]{11976}, + new uint[]{11977}, + new uint[]{11978}, + new uint[]{11979}, + new uint[]{11980}, + Array.Empty(), + new uint[]{11982}, + new uint[]{11983}, + new uint[]{11984}, + Array.Empty(), + new uint[]{11986}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{11433}, + new uint[]{3321}, + new uint[]{3321}, + new uint[]{3326}, + new uint[]{3326}, + new uint[]{11463}, + new uint[]{11464}, + new uint[]{11465}, + new uint[]{11466}, + new uint[]{11467}, + new uint[]{11519}, + new uint[]{11520}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{3329}, + new uint[]{3329}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + new uint[]{11431}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + }; +} diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs index 140006c7..e1d118c8 100644 --- a/Penumbra.GameData/Data/DataSharer.cs +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -5,18 +5,22 @@ using Dalamud.Plugin; namespace Penumbra.GameData.Data; +/// +/// A container base class that shares data through Dalamud but cares about the used language and version. +/// Inheritors should dispose their Dalamud Shares in DisposeInternal via DisposeTag and add them in their constructor via TryCatchData. +/// public abstract class DataSharer : IDisposable { - private readonly DalamudPluginInterface _pluginInterface; - private readonly int _version; - protected readonly ClientLanguage Language; - private bool _disposed; + protected readonly DalamudPluginInterface PluginInterface; + protected readonly int Version; + protected readonly ClientLanguage Language; + private bool _disposed; protected DataSharer(DalamudPluginInterface pluginInterface, ClientLanguage language, int version) { - _pluginInterface = pluginInterface; - Language = language; - _version = version; + PluginInterface = pluginInterface; + Language = language; + Version = version; } protected virtual void DisposeInternal() @@ -36,16 +40,13 @@ public abstract class DataSharer : IDisposable => Dispose(); protected void DisposeTag(string tag) - => _pluginInterface.RelinquishData(GetVersionedTag(tag)); - - private string GetVersionedTag(string tag) - => $"Penumbra.GameData.{tag}.{Language}.V{_version}"; + => PluginInterface.RelinquishData(GetVersionedTag(tag, Language, Version)); protected T TryCatchData(string tag, Func func) where T : class { try { - return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + return PluginInterface.GetOrCreateData(GetVersionedTag(tag, Language, Version), func); } catch (Exception ex) { @@ -53,4 +54,24 @@ public abstract class DataSharer : IDisposable return func(); } } + + public static void DisposeTag(DalamudPluginInterface pi, string tag, ClientLanguage language, int version) + => pi.RelinquishData(GetVersionedTag(tag, language, version)); + + public static T TryCatchData(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, Func func) + where T : class + { + try + { + return pi.GetOrCreateData(GetVersionedTag(tag, language, version), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); + return func(); + } + } + + private static string GetVersionedTag(string tag, ClientLanguage language, int version) + => $"Penumbra.GameData.{tag}.{language}.V{version}"; } diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs new file mode 100644 index 00000000..f26cfc1a --- /dev/null +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +internal sealed class EquipmentIdentificationList : KeyList +{ + private const string Tag = "EquipmentIdentification"; + + public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) + : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language)) + { } + + public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) + { + if (slot == EquipSlot.Unknown) + return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)); + if (variant == 0) + return Between(ToKey(modelId, slot, 0), ToKey(modelId, slot, 0xFF)); + + return Between(ToKey(modelId, slot, variant), ToKey(modelId, slot, variant)); + } + + public void Dispose(DalamudPluginInterface pi, ClientLanguage language) + => DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion); + + public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant) + => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; + + public static ulong ToKey(Item i) + { + var model = (SetId)((Lumina.Data.Parsing.Quad)i.ModelMain).A; + var slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); + var variant = (byte)((Lumina.Data.Parsing.Quad)i.ModelMain).B; + return ToKey(model, slot, variant); + } + + protected override IEnumerable ToKeys(Item i) + { + yield return ToKey(i); + } + + protected override bool ValidKey(ulong key) + => key != 0; + + protected override int ValueKeySelector(Item data) + => (int)data.RowId; + + private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) + { + var items = gameData.GetExcelSheet(language)!; + return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()); + } +} diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index affe8704..4725ed1f 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String; namespace Penumbra.GameData.Data; @@ -97,7 +98,7 @@ internal class GamePathParser : IGamePathParser [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex"), - [ObjectType.Character] = CreateRegexes(@"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" + [ObjectType.Character] = CreateRegexes( @"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" , @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture" , @"chara/common/texture/skin(?'skin'.*)\.tex" , @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex" @@ -114,12 +115,12 @@ internal class GamePathParser : IGamePathParser }, [FileType.Material] = new Dictionary> { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]\.mtrl"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl"), - [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), - [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), - [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl"), - [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]\.mtrl"), + [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl"), + [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl"), + [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), + [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), + [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), + [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl"), }, [FileType.Imc] = new Dictionary> { @@ -129,12 +130,12 @@ internal class GamePathParser : IGamePathParser [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc"), [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc"), }, - }; - // @formatter:on - + }; private static IReadOnlyList CreateRegexes(params string[] regexes) - => regexes.Select(s => new Regex(s, RegexOptions.Compiled)).ToArray(); + => regexes.Select(s => new Regex(s, RegexOptions.Compiled)).ToArray(); + // @formatter:on + public ObjectType PathToObjectType(string path) { diff --git a/Penumbra.GameData/Data/KeyList.cs b/Penumbra.GameData/Data/KeyList.cs new file mode 100644 index 00000000..a6109674 --- /dev/null +++ b/Penumbra.GameData/Data/KeyList.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Plugin; + +namespace Penumbra.GameData.Data; + +/// +/// A list sorting objects based on a key which then allows efficiently finding all objects between a pair of keys via binary search. +/// +public abstract class KeyList +{ + private readonly List<(ulong Key, T Data)> _list; + + public IReadOnlyList<(ulong Key, T Data)> List + => _list; + + /// + /// Iterate over all objects between the given minimal and maximal keys (inclusive). + /// + protected IEnumerable Between(ulong minKey, ulong maxKey) + { + var (minIdx, maxIdx) = GetMinMax(minKey, maxKey); + if (minIdx < 0) + yield break; + + for (var i = minIdx; i <= maxIdx; ++i) + yield return _list[i].Data; + } + + private (int MinIdx, int MaxIdx) GetMinMax(ulong minKey, ulong maxKey) + { + var idx = _list.BinarySearch((minKey, default!), ListComparer); + var minIdx = idx; + if (minIdx < 0) + { + minIdx = ~minIdx; + if (minIdx == _list.Count || _list[minIdx].Key > maxKey) + return (-1, -1); + + idx = minIdx; + } + else + { + while (minIdx > 0 && _list[minIdx - 1].Key >= minKey) + --minIdx; + } + + if (_list[minIdx].Key < minKey || _list[minIdx].Key > maxKey) + return (-1, -1); + + + var maxIdx = _list.BinarySearch(idx, _list.Count - idx, (maxKey, default!), ListComparer); + if (maxIdx < 0) + { + maxIdx = ~maxIdx; + return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); + } + + while (maxIdx < _list.Count - 1 && _list[maxIdx + 1].Key <= maxKey) + ++maxIdx; + + if (_list[maxIdx].Key < minKey || _list[maxIdx].Key > maxKey) + return (-1, -1); + + return (minIdx, maxIdx); + } + + /// + /// The function turning an object to (potentially multiple) keys. Only used during construction. + /// + protected abstract IEnumerable ToKeys(T data); + + /// + /// Whether a returned key is valid. Only used during construction. + /// + protected abstract bool ValidKey(ulong key); + + /// + /// How multiple items with the same key should be sorted. + /// + protected abstract int ValueKeySelector(T data); + + protected KeyList(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, IEnumerable data) + { + _list = DataSharer.TryCatchData(pi, tag, language, version, + () => data.SelectMany(d => ToKeys(d).Select(k => (k, d))) + .Where(p => ValidKey(p.k)) + .OrderBy(p => p.k) + .ThenBy(p => ValueKeySelector(p.d)) + .ToList()); + } + + private class Comparer : IComparer<(ulong, T)> + { + public int Compare((ulong, T) x, (ulong, T) y) + => x.Item1.CompareTo(y.Item1); + } + + private static readonly Comparer ListComparer = new(); +} diff --git a/Penumbra.GameData/Data/ModelIdentificationList.cs b/Penumbra.GameData/Data/ModelIdentificationList.cs new file mode 100644 index 00000000..e1179898 --- /dev/null +++ b/Penumbra.GameData/Data/ModelIdentificationList.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +internal sealed class ModelIdentificationList : KeyList +{ + private const string Tag = "ModelIdentification"; + + public ModelIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) + : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateModelList(gameData, language)) + { } + + public IEnumerable Between(CharacterBase.ModelType type, SetId modelId, byte modelBase = 0, byte variant = 0) + { + if (modelBase == 0) + return Between(ToKey(type, modelId, 0, 0), ToKey(type, modelId, 0xFF, 0xFF)); + if (variant == 0) + return Between(ToKey(type, modelId, modelBase, 0), ToKey(type, modelId, modelBase, 0xFF)); + + return Between(ToKey(type, modelId, modelBase, variant), ToKey(type, modelId, modelBase, variant)); + } + + public void Dispose(DalamudPluginInterface pi, ClientLanguage language) + => DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion); + + + public static ulong ToKey(CharacterBase.ModelType type, SetId model, byte modelBase, byte variant) + => ((ulong)type << 32) | ((ulong)model << 16) | ((ulong)modelBase << 8) | variant; + + private static ulong ToKey(ModelChara row) + => ToKey((CharacterBase.ModelType)row.Type, row.Model, row.Base, row.Variant); + + protected override IEnumerable ToKeys(ModelChara row) + { + yield return ToKey(row); + } + + protected override bool ValidKey(ulong key) + => key != 0; + + protected override int ValueKeySelector(ModelChara data) + => (int)data.RowId; + + private static IEnumerable CreateModelList(DataManager gameData, ClientLanguage language) + => gameData.GetExcelSheet(language)!; +} diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index e0a1ec40..0f5ec4aa 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -10,13 +10,41 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Plugin; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Actors; using Action = Lumina.Excel.GeneratedSheets.Action; - +using ObjectType = Penumbra.GameData.Enums.ObjectType; + namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { - public IGamePathParser GamePathParser { get; } = new GamePathParser(); + public const int IdentificationVersion = 1; + + public IGamePathParser GamePathParser { get; } = new GamePathParser(); + public readonly IReadOnlyList> BnpcNames; + public readonly IReadOnlyList> ModelCharaToObjects; + public readonly IReadOnlyDictionary> Actions; + private readonly ActorManager.ActorManagerData _actorData; + + + private readonly EquipmentIdentificationList _equipment; + private readonly WeaponIdentificationList _weapons; + private readonly ModelIdentificationList _modelIdentifierToModelChara; + + public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, IdentificationVersion) + { + _actorData = new ActorManager.ActorManagerData(pluginInterface, dataManager, language); + _equipment = new EquipmentIdentificationList(pluginInterface, language, dataManager); + _weapons = new WeaponIdentificationList(pluginInterface, language, dataManager); + Actions = TryCatchData("Actions", () => CreateActionList(dataManager)); + _equipment = new EquipmentIdentificationList(pluginInterface, language, dataManager); + + _modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager); + BnpcNames = TryCatchData("BNpcNames", NpcNames.CreateNames); + ModelCharaToObjects = TryCatchData("ModelObjects", () => CreateModelObjects(_actorData, dataManager, language)); + } public void Identify(IDictionary set, string path) { @@ -38,48 +66,25 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return ret; } - public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) - { - switch (slot) + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + => slot switch { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, - (ulong)setId << 32 | (ulong)weaponType << 16 | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); - } - default: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, - (ulong)setId << 32 | (ulong)slot.ToSlot() << 16 | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); - } - } - } - - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models; - private readonly IReadOnlyDictionary> _actions; - - public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) - : base(pluginInterface, language, 1) - { - _weapons = TryCatchData("Weapons", () => CreateWeaponList(dataManager)); - _equipment = TryCatchData("Equipment", () => CreateEquipmentList(dataManager)); - _actions = TryCatchData("Actions", () => CreateActionList(dataManager)); - _models = TryCatchData("Models", () => CreateModelList(dataManager)); - } + EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), + EquipSlot.OffHand => _weapons.Between(setId, weaponType, (byte)variant), + _ => _equipment.Between(setId, slot, (byte)variant), + }; protected override void DisposeInternal() { - DisposeTag("Weapons"); - DisposeTag("Equipment"); + _actorData.Dispose(); + _weapons.Dispose(PluginInterface, Language); + _equipment.Dispose(PluginInterface, Language); DisposeTag("Actions"); DisposeTag("Models"); + + _modelIdentifierToModelChara.Dispose(PluginInterface, Language); + DisposeTag("BNpcNames"); + DisposeTag("ModelObjects"); } private static bool Add(IDictionary> dict, ulong key, Item item) @@ -93,25 +98,25 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private static ulong EquipmentKey(Item i) { - var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; + var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; var variant = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).B; - var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - return model << 32 | slot << 16 | variant; + var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); + return (model << 32) | (slot << 16) | variant; } private static ulong WeaponKey(Item i, bool offhand) { - var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; - var model = (ulong)quad.A; - var type = (ulong)quad.B; + var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; + var model = (ulong)quad.A; + var type = (ulong)quad.B; var variant = (ulong)quad.C; - return model << 32 | type << 16 | variant; + return (model << 32) | (type << 16) | variant; } private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList(DataManager gameData) { - var items = gameData.GetExcelSheet(Language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) @@ -128,7 +133,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList(DataManager gameData) { - var items = gameData.GetExcelSheet(Language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items) { @@ -162,7 +167,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private IReadOnlyDictionary> CreateActionList(DataManager gameData) { - var sheet = gameData.GetExcelSheet(Language)!; + var sheet = gameData.GetExcelSheet(Language)!; var storage = new Dictionary>((int)sheet.RowCount); void AddAction(string? key, Action action) @@ -180,51 +185,29 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty)) { var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); - var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); - var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); + var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); + var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); AddAction(startKey, action); - AddAction(endKey, action); - AddAction(hitKey, action); + AddAction(endKey, action); + AddAction(hitKey, action); } return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } - private static ulong ModelValue(ModelChara row) - => row.Type | (ulong)row.Model << 8 | (ulong)row.Base << 24 | (ulong)row.Variant << 32; - - private static IEnumerable<(ulong, ObjectKind, uint)> BattleNpcToName(ulong model, uint bNpc) - => Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1); - - private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList(DataManager gameData) - { - var sheetBNpc = gameData.GetExcelSheet(Language)!; - var sheetENpc = gameData.GetExcelSheet(Language)!; - var sheetCompanion = gameData.GetExcelSheet(Language)!; - var sheetMount = gameData.GetExcelSheet(Language)!; - var sheetModel = gameData.GetExcelSheet(Language)!; - - var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue); - - return sheetENpc.Select(e => (modelCharaToModel[e.ModelChara.Row], ObjectKind.EventNpc, e.RowId)) - .Concat(sheetCompanion.Select(c => (modelCharaToModel[c.Model.Row], ObjectKind.Companion, c.RowId))) - .Concat(sheetMount.Select(c => (modelCharaToModel[c.ModelChara.Row], ObjectKind.MountType, c.RowId))) - .Concat(sheetBNpc.SelectMany(c => BattleNpcToName(modelCharaToModel[c.ModelChara.Row], c.RowId))) - .GroupBy(t => t.Item1) - .Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray())) - .ToArray(); - } - private class Comparer : IComparer<(ulong, IReadOnlyList)> { public int Compare((ulong, IReadOnlyList) x, (ulong, IReadOnlyList) y) => x.Item1.CompareTo(y.Item1); } + private static readonly Comparer _arrayComparer = new(); + + private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList)> list, ulong key, ulong mask) { var maskedKey = key & mask; - var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); + var idx = list.BinarySearch(0, list.Count, (key, null!), _arrayComparer); if (idx < 0) { if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask)) @@ -242,55 +225,30 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private void FindEquipment(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if (info.EquipSlot != EquipSlot.Unknown) - { - key |= (ulong)info.EquipSlot.ToSlot() << 16; - mask |= 0xFFFF0000; - } - - if (info.Variant != 0) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, key, mask); - if (start == -1) - return; - - for (; start < end; ++start) - { - foreach (var item in _equipment[start].Item2) - set[item.Name.ToString()] = item; - } + var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant); + foreach (var item in items) + set[item.Name.ToString()] = item; } private void FindWeapon(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if (info.SecondaryId != 0) - { - key |= (ulong)info.SecondaryId << 16; - mask |= 0xFFFF0000; - } + var items = _weapons.Between(info.PrimaryId, info.SecondaryId, info.Variant); + foreach (var item in items) + set[item.Name.ToString()] = item; + } - if (info.Variant != 0) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, key, mask); - if (start == -1) + private void FindModel(IDictionary set, GameObjectInfo info) + { + var type = info.ObjectType.ToModelType(); + if (type is 0 or CharacterBase.ModelType.Weapon) return; - for (; start < end; ++start) + var models = _modelIdentifierToModelChara.Between(type, info.PrimaryId, (byte)info.SecondaryId, info.Variant); + foreach (var model in models.Where(m => m.RowId < ModelCharaToObjects.Count)) { - foreach (var item in _weapons[start].Item2) - set[item.Name.ToString()] = item; + var objectList = ModelCharaToObjects[(int)model.RowId]; + foreach (var (name, kind) in objectList) + set[$"{name} ({kind.ToName()})"] = model; } } @@ -332,10 +290,10 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier AddCounterString(set, info.ObjectType.ToString()); break; case ObjectType.DemiHuman: - set[$"Demi Human: {info.PrimaryId}"] = null; + FindModel(set, info); break; case ObjectType.Monster: - set[$"Monster: {info.PrimaryId}"] = null; + FindModel(set, info); break; case ObjectType.Icon: set[$"Icon: {info.IconId}"] = null; @@ -349,7 +307,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier break; case ObjectType.Character: var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; switch (info.CustomizationType) { @@ -365,16 +323,16 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier case CustomizationType.DecalEquip: set[$"Equipment Decal {info.PrimaryId}"] = null; break; - default: - { - 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; - } + default: + { + 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; + } } break; @@ -386,10 +344,74 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private void IdentifyVfx(IDictionary set, string path) { var key = GamePathParser.VfxToKey(path); - if (key.Length == 0 || !_actions.TryGetValue(key, out var actions)) + if (key.Length == 0 || !Actions.TryGetValue(key, out var actions)) return; foreach (var action in actions) set[$"Action: {action.Name}"] = action; } + + private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, + DataManager gameData, + ClientLanguage language) + { + var modelSheet = gameData.GetExcelSheet(language)!; + var bnpcSheet = gameData.GetExcelSheet(language)!; + var enpcSheet = gameData.GetExcelSheet(language)!; + var ornamentSheet = gameData.GetExcelSheet(language)!; + var mountSheet = gameData.GetExcelSheet(language)!; + var companionSheet = gameData.GetExcelSheet(language)!; + var ret = new List>((int)modelSheet.RowCount); + + for (var i = -1; i < modelSheet.Last().RowId; ++i) + ret.Add(new HashSet<(string Name, ObjectKind Kind)>()); + + void Add(int modelChara, ObjectKind kind, uint dataId) + { + if (modelChara == 0 || modelChara >= ret.Count) + return; + + if (actors.TryGetName(kind, dataId, out var name)) + ret[modelChara].Add((name, kind)); + } + + foreach (var ornament in ornamentSheet) + Add(ornament.Model, (ObjectKind)15, ornament.RowId); + + foreach (var mount in mountSheet) + Add((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId); + + foreach (var companion in companionSheet) + Add((int)companion.Model.Row, ObjectKind.Companion, companion.RowId); + + foreach (var enpc in enpcSheet) + Add((int)enpc.ModelChara.Row, ObjectKind.EventNpc, enpc.RowId); + + foreach (var bnpc in bnpcSheet.Where(b => b.RowId < BnpcNames.Count)) + { + foreach (var name in BnpcNames[(int)bnpc.RowId]) + Add((int)bnpc.ModelChara.Row, ObjectKind.BattleNpc, name); + } + + return ret.Select(s => s.Count > 0 + ? s.ToArray() + : Array.Empty<(string Name, ObjectKind Kind)>()).ToArray(); + } + + public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) + { + var type = (*(delegate* unmanaged**)drawObject)[50](drawObject); + var unk = (ulong)*((byte*)drawObject + 0x8E8) << 8; + return type switch + { + 1 => type | unk, + 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x908) << 16), + 3 => type + | unk + | ((ulong)*(ushort*)((byte*)drawObject + 0x8F0) << 16) + | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 32) + | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 40), + _ => 0u, + }; + } } diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs new file mode 100644 index 00000000..21dff7cf --- /dev/null +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer; + +/// +/// Handle gender- or race-locked gear in the draw model itself. +/// Racial gear gets swapped to the correct current race and gender (it is one set each). +/// Gender-locked gear gets swapped to the equivalent set if it exists (most of them do), +/// with some items getting send to emperor's new clothes and a few funny entries. +/// +public sealed class RestrictedGear : DataSharer +{ + private readonly ExcelSheet _items; + private readonly ExcelSheet _categories; + + public readonly IReadOnlySet RaceGenderSet; + public readonly IReadOnlyDictionary MaleToFemale; + public readonly IReadOnlyDictionary FemaleToMale; + + internal RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) + : base(pi, language, 1) + { + _items = gameData.GetExcelSheet()!; + _categories = gameData.GetExcelSheet()!; + (RaceGenderSet, MaleToFemale, FemaleToMale) = TryCatchData("RestrictedGear", CreateRestrictedGear); + } + + protected override void DisposeInternal() + => DisposeTag("RestrictedGear"); + + /// + /// Resolve a model given by its model id, variant and slot for your current race and gender. + /// + /// The equipment piece. + /// The equipment slot. + /// The intended race. + /// The intended gender. + /// True and the changed-to piece of gear or false and the same piece of gear. + public (bool Replaced, CharacterArmor Armor) ResolveRestricted(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) + { + var quad = armor.Set.Value | ((uint)armor.Variant << 16); + // Check racial gear, this does not need slots. + if (RaceGenderGroup.Contains(quad)) + { + var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0); + var value = RaceGenderGroup[idx]; + return (value != quad, new CharacterArmor((ushort)value, (byte)(value >> 16), armor.Stain)); + } + + // Check gender slots. If current gender is female, check if anything needs to be changed from male to female, + // and vice versa. + // Some items lead to the exact same model- and variant id just gender specified, + // so check for actual difference in the Replaced bool. + var needle = quad | ((uint)slot.ToSlot() << 24); + if (gender is Gender.Female or Gender.FemaleNpc && MaleToFemale.TryGetValue(needle, out var newValue) + || gender is Gender.Male or Gender.MaleNpc && FemaleToMale.TryGetValue(needle, out newValue)) + return (quad != newValue, new CharacterArmor((ushort)newValue, (byte)(newValue >> 16), armor.Stain)); + + // The gear is not restricted. + return (false, armor); + } + + private Tuple, IReadOnlyDictionary, IReadOnlyDictionary> CreateRestrictedGear() + { + var m2f = new Dictionary(); + var f2m = new Dictionary(); + var rg = RaceGenderGroup.Where(c => c is not 0 and not uint.MaxValue).ToHashSet(); + AddKnown(m2f, f2m); + UnhandledRestrictedGear(m2f, f2m, false); // Set this to true to create a print of unassigned gear on launch. + return new Tuple, IReadOnlyDictionary, IReadOnlyDictionary>(rg, m2f, f2m); + } + + + // Add all unknown restricted gear and pair it with emperor's new gear on start up. + // Can also print unhandled items. + private void UnhandledRestrictedGear(Dictionary m2f, Dictionary f2m, bool print) + { + if (print) + PluginLog.Information("#### MALE ONLY ######"); + + void AddEmperor(Item item, bool male, bool female) + { + var slot = ((EquipSlot)item.EquipSlotCategory.Row).ToSlot(); + var emperor = slot switch + { + EquipSlot.Head => 10032u, + EquipSlot.Body => 10033u, + EquipSlot.Hands => 10034u, + EquipSlot.Legs => 10035u, + EquipSlot.Feet => 10036u, + EquipSlot.Ears => 09293u, + EquipSlot.Neck => 09292u, + EquipSlot.Wrists => 09294u, + EquipSlot.RFinger => 09295u, + EquipSlot.LFinger => 09295u, + _ => 0u, + }; + if (emperor == 0) + return; + + if (male) + AddItem(m2f, f2m, item.RowId, emperor, true, false); + if (female) + AddItem(m2f, f2m, emperor, item.RowId, false, true); + } + + var unhandled = 0; + foreach (var item in _items.Where(i => i.EquipRestriction == 2)) + { + if (m2f.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24))) + continue; + + ++unhandled; + AddEmperor(item, true, false); + + if (print) + PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}"); + } + + if (print) + PluginLog.Information("#### FEMALE ONLY ####"); + foreach (var item in _items.Where(i => i.EquipRestriction == 3)) + { + if (f2m.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24))) + continue; + + ++unhandled; + AddEmperor(item, false, true); + + if (print) + PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}"); + } + + if (print) + PluginLog.Information("#### OTHER #########"); + + foreach (var item in _items.Where(i => i.EquipRestriction > 3)) + { + if (RaceGenderSet.Contains((uint)item.ModelMain)) + continue; + + ++unhandled; + if (print) + PluginLog.Information( + $"{item.RowId:D5} {item.Name.ToDalamudString().TextValue} RestrictionGroup {_categories.GetRow(item.EquipRestriction)!.RowId:D2}"); + } + + if (unhandled > 0) + PluginLog.Warning($"There were {unhandled} restricted items not handled and directed to Emperor's New Set."); + } + + // Add a item redirection by its item - NOT MODEL - id. + // This uses the items model as well as its slot. + // Creates a <-> redirection by default but can add -> or <- redirections by setting the corresponding bools to false. + // Prints warnings if anything does not make sense. + private void AddItem(Dictionary m2f, Dictionary f2m, uint itemIdMale, uint itemIdFemale, bool addMale = true, + bool addFemale = true) + { + if (!addMale && !addFemale) + return; + + var mItem = _items.GetRow(itemIdMale); + var fItem = _items.GetRow(itemIdFemale); + if (mItem == null || fItem == null) + { + PluginLog.Warning($"Could not add item {itemIdMale} or {itemIdFemale} to restricted items."); + return; + } + + if (mItem.EquipRestriction != 2 && addMale) + { + PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not restricted anymore."); + return; + } + + if (fItem.EquipRestriction != 3 && addFemale) + { + PluginLog.Warning($"{fItem.Name.ToDalamudString().TextValue} is not restricted anymore."); + return; + } + + var mSlot = ((EquipSlot)mItem.EquipSlotCategory.Row).ToSlot(); + var fSlot = ((EquipSlot)fItem.EquipSlotCategory.Row).ToSlot(); + if (!mSlot.IsAccessory() && !mSlot.IsEquipment()) + { + PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not equippable to a known slot."); + return; + } + + if (mSlot != fSlot) + { + PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} and {fItem.Name.ToDalamudString().TextValue} are not compatible."); + return; + } + + var mModelIdSlot = (uint)mItem.ModelMain | ((uint)mSlot << 24); + var fModelIdSlot = (uint)fItem.ModelMain | ((uint)fSlot << 24); + + if (addMale) + m2f.TryAdd(mModelIdSlot, fModelIdSlot); + if (addFemale) + f2m.TryAdd(fModelIdSlot, mModelIdSlot); + } + + // @formatter:off + // Add all currently existing and known gender restricted items. + private void AddKnown(Dictionary m2f, Dictionary f2m) + { + AddItem(m2f, f2m, 02967, 02970); // Lord's Yukata (Blue) <-> Lady's Yukata (Red) + AddItem(m2f, f2m, 02968, 02971); // Lord's Yukata (Green) <-> Lady's Yukata (Blue) + AddItem(m2f, f2m, 02969, 02972); // Lord's Yukata (Grey) <-> Lady's Yukata (Black) + AddItem(m2f, f2m, 02973, 02978); // Red Summer Top <-> Red Summer Halter + AddItem(m2f, f2m, 02974, 02979); // Green Summer Top <-> Green Summer Halter + AddItem(m2f, f2m, 02975, 02980); // Blue Summer Top <-> Blue Summer Halter + AddItem(m2f, f2m, 02976, 02981); // Solar Summer Top <-> Solar Summer Halter + AddItem(m2f, f2m, 02977, 02982); // Lunar Summer Top <-> Lunar Summer Halter + AddItem(m2f, f2m, 02996, 02997); // Hempen Undershirt <-> Hempen Camise + AddItem(m2f, f2m, 03280, 03283); // Lord's Drawers (Black) <-> Lady's Knickers (Black) + AddItem(m2f, f2m, 03281, 03284); // Lord's Drawers (White) <-> Lady's Knickers (White) + AddItem(m2f, f2m, 03282, 03285); // Lord's Drawers (Gold) <-> Lady's Knickers (Gold) + AddItem(m2f, f2m, 03286, 03291); // Red Summer Trunks <-> Red Summer Tanga + AddItem(m2f, f2m, 03287, 03292); // Green Summer Trunks <-> Green Summer Tanga + AddItem(m2f, f2m, 03288, 03293); // Blue Summer Trunks <-> Blue Summer Tanga + AddItem(m2f, f2m, 03289, 03294); // Solar Summer Trunks <-> Solar Summer Tanga + AddItem(m2f, f2m, 03290, 03295); // Lunar Summer Trunks <-> Lunar Summer Tanga + AddItem(m2f, f2m, 03307, 03308); // Hempen Underpants <-> Hempen Pantalettes + AddItem(m2f, f2m, 03748, 03749); // Lord's Clogs <-> Lady's Clogs + AddItem(m2f, f2m, 06045, 06041); // Bohemian's Coat <-> Guardian Corps Coat + AddItem(m2f, f2m, 06046, 06042); // Bohemian's Gloves <-> Guardian Corps Gauntlets + AddItem(m2f, f2m, 06047, 06043); // Bohemian's Trousers <-> Guardian Corps Skirt + AddItem(m2f, f2m, 06048, 06044); // Bohemian's Boots <-> Guardian Corps Boots + AddItem(m2f, f2m, 06094, 06098); // Summer Evening Top <-> Summer Morning Halter + AddItem(m2f, f2m, 06095, 06099); // Summer Evening Trunks <-> Summer Morning Tanga + AddItem(m2f, f2m, 06096, 06100); // Striped Summer Top <-> Striped Summer Halter + AddItem(m2f, f2m, 06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga + AddItem(m2f, f2m, 06102, 06104); // Black Summer Top <-> Black Summer Halter + AddItem(m2f, f2m, 06103, 06105); // Black Summer Trunks <-> Black Summer Tanga + AddItem(m2f, f2m, 06972, 06973); // Valentione Apron <-> Valentione Apron Dress + AddItem(m2f, f2m, 06975, 06976); // Valentione Trousers <-> Valentione Skirt + AddItem(m2f, f2m, 08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly) + AddItem(m2f, f2m, 08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly) + AddItem(m2f, f2m, 08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly) + AddItem(m2f, f2m, 08542, 08549); // Ti Leaf Lei <-> Coronal Summer Halter + AddItem(m2f, f2m, 08543, 08550); // Red Summer Maro <-> Red Summer Pareo + AddItem(m2f, f2m, 08544, 08551); // South Seas Talisman <-> Sea Breeze Summer Halter + AddItem(m2f, f2m, 08545, 08552); // Blue Summer Maro <-> Sea Breeze Summer Pareo + AddItem(m2f, f2m, 08546, 08553); // Coeurl Talisman <-> Coeurl Beach Halter + AddItem(m2f, f2m, 08547, 08554); // Coeurl Beach Maro <-> Coeurl Beach Pareo + AddItem(m2f, f2m, 08548, 08555); // Coeurl Beach Briefs <-> Coeurl Beach Tanga + AddItem(m2f, f2m, 10316, 10317); // Southern Seas Vest <-> Southern Seas Swimsuit + AddItem(m2f, f2m, 10318, 10319); // Southern Seas Trunks <-> Southern Seas Tanga + AddItem(m2f, f2m, 10320, 10321); // Striped Southern Seas Vest <-> Striped Southern Seas Swimsuit + AddItem(m2f, f2m, 13298, 13567); // Black-feathered Flat Hat <-> Red-feathered Flat Hat + AddItem(m2f, f2m, 13300, 13639); // Lord's Suikan <-> Lady's Suikan + AddItem(m2f, f2m, 13724, 13725); // Little Lord's Clogs <-> Little Lady's Clogs + AddItem(m2f, f2m, 14854, 14857); // Eastern Lord's Togi <-> Eastern Lady's Togi + AddItem(m2f, f2m, 14855, 14858); // Eastern Lord's Trousers <-> Eastern Lady's Loincloth + AddItem(m2f, f2m, 14856, 14859); // Eastern Lord's Crakows <-> Eastern Lady's Crakows + AddItem(m2f, f2m, 15639, 15642); // Far Eastern Patriarch's Hat <-> Far Eastern Matriarch's Sun Hat + AddItem(m2f, f2m, 15640, 15643); // Far Eastern Patriarch's Tunic <-> Far Eastern Matriarch's Dress + AddItem(m2f, f2m, 15641, 15644); // Far Eastern Patriarch's Longboots <-> Far Eastern Matriarch's Boots + AddItem(m2f, f2m, 15922, 15925); // Moonfire Vest <-> Moonfire Halter + AddItem(m2f, f2m, 15923, 15926); // Moonfire Trunks <-> Moonfire Tanga + AddItem(m2f, f2m, 15924, 15927); // Moonfire Caligae <-> Moonfire Sandals + AddItem(m2f, f2m, 16106, 16111); // Makai Mauler's Facemask <-> Makai Manhandler's Facemask + AddItem(m2f, f2m, 16107, 16112); // Makai Mauler's Oilskin <-> Makai Manhandler's Jerkin + AddItem(m2f, f2m, 16108, 16113); // Makai Mauler's Fingerless Gloves <-> Makai Manhandler's Fingerless Gloves + AddItem(m2f, f2m, 16109, 16114); // Makai Mauler's Leggings <-> Makai Manhandler's Quartertights + AddItem(m2f, f2m, 16110, 16115); // Makai Mauler's Boots <-> Makai Manhandler's Longboots + AddItem(m2f, f2m, 16116, 16121); // Makai Marksman's Eyepatch <-> Makai Markswoman's Ribbon + AddItem(m2f, f2m, 16117, 16122); // Makai Marksman's Battlegarb <-> Makai Markswoman's Battledress + AddItem(m2f, f2m, 16118, 16123); // Makai Marksman's Fingerless Gloves <-> Makai Markswoman's Fingerless Gloves + AddItem(m2f, f2m, 16119, 16124); // Makai Marksman's Slops <-> Makai Markswoman's Quartertights + AddItem(m2f, f2m, 16120, 16125); // Makai Marksman's Boots <-> Makai Markswoman's Longboots + AddItem(m2f, f2m, 16126, 16131); // Makai Sun Guide's Circlet <-> Makai Moon Guide's Circlet + AddItem(m2f, f2m, 16127, 16132); // Makai Sun Guide's Oilskin <-> Makai Moon Guide's Gown + AddItem(m2f, f2m, 16128, 16133); // Makai Sun Guide's Fingerless Gloves <-> Makai Moon Guide's Fingerless Gloves + AddItem(m2f, f2m, 16129, 16134); // Makai Sun Guide's Slops <-> Makai Moon Guide's Quartertights + AddItem(m2f, f2m, 16130, 16135); // Makai Sun Guide's Boots <-> Makai Moon Guide's Longboots + AddItem(m2f, f2m, 16136, 16141); // Makai Priest's Coronet <-> Makai Priestess's Headdress + AddItem(m2f, f2m, 16137, 16142); // Makai Priest's Doublet Robe <-> Makai Priestess's Jerkin + AddItem(m2f, f2m, 16138, 16143); // Makai Priest's Fingerless Gloves <-> Makai Priestess's Fingerless Gloves + AddItem(m2f, f2m, 16139, 16144); // Makai Priest's Slops <-> Makai Priestess's Skirt + AddItem(m2f, f2m, 16140, 16145); // Makai Priest's Boots <-> Makai Priestess's Longboots + AddItem(m2f, f2m, 16588, 16592); // Far Eastern Gentleman's Hat <-> Far Eastern Beauty's Hairpin + AddItem(m2f, f2m, 16589, 16593); // Far Eastern Gentleman's Robe <-> Far Eastern Beauty's Robe + AddItem(m2f, f2m, 16590, 16594); // Far Eastern Gentleman's Haidate <-> Far Eastern Beauty's Koshita + AddItem(m2f, f2m, 16591, 16595); // Far Eastern Gentleman's Boots <-> Far Eastern Beauty's Boots + AddItem(m2f, f2m, 17204, 17209); // Common Makai Mauler's Facemask <-> Common Makai Manhandler's Facemask + AddItem(m2f, f2m, 17205, 17210); // Common Makai Mauler's Oilskin <-> Common Makai Manhandler's Jerkin + AddItem(m2f, f2m, 17206, 17211); // Common Makai Mauler's Fingerless Gloves <-> Common Makai Manhandler's Fingerless Glove + AddItem(m2f, f2m, 17207, 17212); // Common Makai Mauler's Leggings <-> Common Makai Manhandler's Quartertights + AddItem(m2f, f2m, 17208, 17213); // Common Makai Mauler's Boots <-> Common Makai Manhandler's Longboots + AddItem(m2f, f2m, 17214, 17219); // Common Makai Marksman's Eyepatch <-> Common Makai Markswoman's Ribbon + AddItem(m2f, f2m, 17215, 17220); // Common Makai Marksman's Battlegarb <-> Common Makai Markswoman's Battledress + AddItem(m2f, f2m, 17216, 17221); // Common Makai Marksman's Fingerless Gloves <-> Common Makai Markswoman's Fingerless Glove + AddItem(m2f, f2m, 17217, 17222); // Common Makai Marksman's Slops <-> Common Makai Markswoman's Quartertights + AddItem(m2f, f2m, 17218, 17223); // Common Makai Marksman's Boots <-> Common Makai Markswoman's Longboots + AddItem(m2f, f2m, 17224, 17229); // Common Makai Sun Guide's Circlet <-> Common Makai Moon Guide's Circlet + AddItem(m2f, f2m, 17225, 17230); // Common Makai Sun Guide's Oilskin <-> Common Makai Moon Guide's Gown + AddItem(m2f, f2m, 17226, 17231); // Common Makai Sun Guide's Fingerless Gloves <-> Common Makai Moon Guide's Fingerless Glove + AddItem(m2f, f2m, 17227, 17232); // Common Makai Sun Guide's Slops <-> Common Makai Moon Guide's Quartertights + AddItem(m2f, f2m, 17228, 17233); // Common Makai Sun Guide's Boots <-> Common Makai Moon Guide's Longboots + AddItem(m2f, f2m, 17234, 17239); // Common Makai Priest's Coronet <-> Common Makai Priestess's Headdress + AddItem(m2f, f2m, 17235, 17240); // Common Makai Priest's Doublet Robe <-> Common Makai Priestess's Jerkin + AddItem(m2f, f2m, 17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves + AddItem(m2f, f2m, 17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt + AddItem(m2f, f2m, 17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots + AddItem(m2f, f2m, 17481, 17476); // Royal Seneschal's Chapeau <-> Songbird Hat + AddItem(m2f, f2m, 17482, 17477); // Royal Seneschal's Coat <-> Songbird Jacket + AddItem(m2f, f2m, 17483, 17478); // Royal Seneschal's Fingerless Gloves <-> Songbird Gloves + AddItem(m2f, f2m, 17484, 17479); // Royal Seneschal's Breeches <-> Songbird Skirt + AddItem(m2f, f2m, 17485, 17480); // Royal Seneschal's Boots <-> Songbird Boots + AddItem(m2f, f2m, 20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady + AddItem(m2f, f2m, 20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi + AddItem(m2f, f2m, 20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves + AddItem(m2f, f2m, 20482, 20487); // Nezha Lord's Slops <-> Nezha Lady's Slops + AddItem(m2f, f2m, 20483, 20488); // Nezha Lord's Boots <-> Nezha Lady's Kneeboots + AddItem(m2f, f2m, 22367, 22372); // Faerie Tale Prince's Circlet <-> Faerie Tale Princess's Tiara + AddItem(m2f, f2m, 22368, 22373); // Faerie Tale Prince's Vest <-> Faerie Tale Princess's Dress + AddItem(m2f, f2m, 22369, 22374); // Faerie Tale Prince's Gloves <-> Faerie Tale Princess's Gloves + AddItem(m2f, f2m, 22370, 22375); // Faerie Tale Prince's Slops <-> Faerie Tale Princess's Long Skirt + AddItem(m2f, f2m, 22371, 22376); // Faerie Tale Prince's Boots <-> Faerie Tale Princess's Heels + AddItem(m2f, f2m, 24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon + AddItem(m2f, f2m, 24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama + AddItem(m2f, f2m, 24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots + AddItem(m2f, f2m, 28558, 28573); // Valentione Rose Hat <-> Valentione Rose Ribboned Hat + AddItem(m2f, f2m, 28559, 28574); // Valentione Rose Waistcoat <-> Valentione Rose Dress + AddItem(m2f, f2m, 28560, 28575); // Valentione Rose Gloves <-> Valentione Rose Ribboned Gloves + AddItem(m2f, f2m, 28561, 28576); // Valentione Rose Slacks <-> Valentione Rose Tights + AddItem(m2f, f2m, 28562, 28577); // Valentione Rose Shoes <-> Valentione Rose Heels + AddItem(m2f, f2m, 28563, 28578); // Valentione Forget-me-not Hat <-> Valentione Forget-me-not Ribboned Hat + AddItem(m2f, f2m, 28564, 28579); // Valentione Forget-me-not Waistcoat <-> Valentione Forget-me-not Dress + AddItem(m2f, f2m, 28565, 28580); // Valentione Forget-me-not Gloves <-> Valentione Forget-me-not Ribboned Gloves + AddItem(m2f, f2m, 28566, 28581); // Valentione Forget-me-not Slacks <-> Valentione Forget-me-not Tights + AddItem(m2f, f2m, 28567, 28582); // Valentione Forget-me-not Shoes <-> Valentione Forget-me-not Heels + AddItem(m2f, f2m, 28568, 28583); // Valentione Acacia Hat <-> Valentione Acacia Ribboned Hat + AddItem(m2f, f2m, 28569, 28584); // Valentione Acacia Waistcoat <-> Valentione Acacia Dress + AddItem(m2f, f2m, 28570, 28585); // Valentione Acacia Gloves <-> Valentione Acacia Ribboned Gloves + AddItem(m2f, f2m, 28571, 28586); // Valentione Acacia Slacks <-> Valentione Acacia Tights + AddItem(m2f, f2m, 28572, 28587); // Valentione Acacia Shoes <-> Valentione Acacia Heels + AddItem(m2f, f2m, 28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat + AddItem(m2f, f2m, 28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat + AddItem(m2f, f2m, 28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves + AddItem(m2f, f2m, 28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt + AddItem(m2f, f2m, 28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots + AddItem(m2f, f2m, 31408, 31413); // Bergsteiger's Hat <-> Dirndl's Hat + AddItem(m2f, f2m, 31409, 31414); // Bergsteiger's Jacket <-> Dirndl's Bodice + AddItem(m2f, f2m, 31410, 31415); // Bergsteiger's Halfgloves <-> Dirndl's Wrist Torque + AddItem(m2f, f2m, 31411, 31416); // Bergsteiger's Halfslops <-> Dirndl's Long Skirt + AddItem(m2f, f2m, 31412, 31417); // Bergsteiger's Boots <-> Dirndl's Pumps + AddItem(m2f, f2m, 36336, 36337); // Omega-M Attire <-> Omega-F Attire + AddItem(m2f, f2m, 36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings + AddItem(m2f, f2m, 37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon + AddItem(m2f, f2m, 37443, 37448); // Makai Vanguard's Battlegarb <-> Makai Vanbreaker's Battledress + AddItem(m2f, f2m, 37444, 37449); // Makai Vanguard's Fingerless Gloves <-> Makai Vanbreaker's Fingerless Gloves + AddItem(m2f, f2m, 37445, 37450); // Makai Vanguard's Leggings <-> Makai Vanbreaker's Quartertights + AddItem(m2f, f2m, 37446, 37451); // Makai Vanguard's Boots <-> Makai Vanbreaker's Longboots + AddItem(m2f, f2m, 37452, 37457); // Makai Harbinger's Facemask <-> Makai Harrower's Facemask + AddItem(m2f, f2m, 37453, 37458); // Makai Harbinger's Battlegarb <-> Makai Harrower's Jerkin + AddItem(m2f, f2m, 37454, 37459); // Makai Harbinger's Fingerless Gloves <-> Makai Harrower's Fingerless Gloves + AddItem(m2f, f2m, 37455, 37460); // Makai Harbinger's Leggings <-> Makai Harrower's Quartertights + AddItem(m2f, f2m, 37456, 37461); // Makai Harbinger's Boots <-> Makai Harrower's Longboots + AddItem(m2f, f2m, 37462, 37467); // Common Makai Vanguard's Monocle <-> Common Makai Vanbreaker's Ribbon + AddItem(m2f, f2m, 37463, 37468); // Common Makai Vanguard's Battlegarb <-> Common Makai Vanbreaker's Battledress + AddItem(m2f, f2m, 37464, 37469); // Common Makai Vanguard's Fingerless Gloves <-> Common Makai Vanbreaker's Fingerless Gloves + AddItem(m2f, f2m, 37465, 37470); // Common Makai Vanguard's Leggings <-> Common Makai Vanbreaker's Quartertights + AddItem(m2f, f2m, 37466, 37471); // Common Makai Vanguard's Boots <-> Common Makai Vanbreaker's Longboots + AddItem(m2f, f2m, 37472, 37477); // Common Makai Harbinger's Facemask <-> Common Makai Harrower's Facemask + AddItem(m2f, f2m, 37473, 37478); // Common Makai Harbinger's Battlegarb <-> Common Makai Harrower's Jerkin + AddItem(m2f, f2m, 37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves + AddItem(m2f, f2m, 37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights + AddItem(m2f, f2m, 37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots + AddItem(m2f, f2m, 13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica + AddItem(m2f, f2m, 13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves + AddItem(m2f, f2m, 13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses + AddItem(m2f, f2m, 13695, 13692); // Scion Thief's Armored Caligae <-> Scion Conjurer's Pattens + AddItem(m2f, f2m, 13326, 30063); // Scion Thaumaturge's Robe <-> Scion Sorceress's Headdress + AddItem(m2f, f2m, 13696, 30062); // Scion Thaumaturge's Monocle <-> Scion Sorceress's Robe + AddItem(m2f, f2m, 13697, 30064); // Scion Thaumaturge's Gauntlets <-> Scion Sorceress's Shadowtalons + AddItem(m2f, f2m, 13698, 10035, true, false); // Scion Thaumaturge's Gaskins -> The Emperor's New Breeches + AddItem(m2f, f2m, 13699, 30065); // Scion Thaumaturge's Moccasins <-> Scion Sorceress's High Boots + AddItem(m2f, f2m, 13327, 15942); // Scion Chronocler's Cowl <-> Scion Healer's Robe + AddItem(m2f, f2m, 13700, 10034, true, false); // Scion Chronocler's Ringbands -> The Emperor's New Gloves + AddItem(m2f, f2m, 13701, 15943); // Scion Chronocler's Tights <-> Scion Healer's Halftights + AddItem(m2f, f2m, 13702, 15944); // Scion Chronocler's Caligae <-> Scion Healer's Highboots + AddItem(m2f, f2m, 14861, 13324); // Head Engineer's Goggles <-> Scion Striker's Visor + AddItem(m2f, f2m, 14862, 13325); // Head Engineer's Attire <-> Scion Striker's Attire + AddItem(m2f, f2m, 15938, 33751); // Scion Rogue's Jacket <-> Oracle Top + AddItem(m2f, f2m, 15939, 10034, true, false); // Scion Rogue's Armguards -> The Emperor's New Gloves + AddItem(m2f, f2m, 15940, 33752); // Scion Rogue's Gaskins <-> Oracle Leggings + AddItem(m2f, f2m, 15941, 33753); // Scion Rogue's Boots <-> Oracle Pantalettes + AddItem(m2f, f2m, 16042, 16046); // Abes Jacket <-> High Summoner's Dress + AddItem(m2f, f2m, 16043, 16047); // Abes Gloves <-> High Summoner's Armlets + AddItem(m2f, f2m, 16044, 10035, true, false); // Abes Halfslops -> The Emperor's New Breeches + AddItem(m2f, f2m, 16045, 16048); // Abes Boots <-> High Summoner's Boots + AddItem(m2f, f2m, 17473, 28553); // Lord Commander's Coat <-> Majestic Dress + AddItem(m2f, f2m, 17474, 28554); // Lord Commander's Gloves <-> Majestic Wristdresses + AddItem(m2f, f2m, 10036, 28555, false); // Emperor's New Boots <- Majestic Boots + AddItem(m2f, f2m, 21021, 21026); // Werewolf Feet <-> Werewolf Legs + AddItem(m2f, f2m, 22452, 20633); // Cracked Manderville Monocle <-> Blackbosom Hat + AddItem(m2f, f2m, 22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress + AddItem(m2f, f2m, 22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves + AddItem(m2f, f2m, 22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches + AddItem(m2f, f2m, 22456, 20636); // Scuffed Manderville Gaiters <-> lackbosom Boots + AddItem(m2f, f2m, 23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket + AddItem(m2f, f2m, 23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves + AddItem(m2f, f2m, 23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes + AddItem(m2f, f2m, 23016, 21305); // Doman Liege's Kyahan <-> Scion Liberator's Sabatons + AddItem(m2f, f2m, 09293, 21306, false); // The Emperor's New Earrings <- Scion Liberator's Earrings + AddItem(m2f, f2m, 24158, 23008, true, false); // Leal Samurai's Kasa -> Eastern Socialite's Hat + AddItem(m2f, f2m, 24159, 23009, true, false); // Leal Samurai's Dogi -> Eastern Socialite's Cheongsam + AddItem(m2f, f2m, 24160, 23010, true, false); // Leal Samurai's Tekko -> Eastern Socialite's Gloves + AddItem(m2f, f2m, 24161, 23011, true, false); // Leal Samurai's Tsutsu-hakama -> Eastern Socialite's Skirt + AddItem(m2f, f2m, 24162, 23012, true, false); // Leal Samurai's Geta -> Eastern Socialite's Boots + AddItem(m2f, f2m, 02966, 13321, false); // Reindeer Suit <- Antecedent's Attire + AddItem(m2f, f2m, 15479, 36843, false); // Swine Body <- Lyse's Leadership Attire + AddItem(m2f, f2m, 21941, 24999, false); // Ala Mhigan Gown <- Gown of Light + AddItem(m2f, f2m, 30757, 25000, false); // Southern Seas Skirt <- Skirt of Light + AddItem(m2f, f2m, 36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood + AddItem(m2f, f2m, 36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat + AddItem(m2f, f2m, 36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes + AddItem(m2f, f2m, 38253, 38257); // Valentione Emissary's Hat <-> Valentione Emissary's Dress Hat + AddItem(m2f, f2m, 38254, 38258); // Valentione Emissary's Jacket <-> Valentione Emissary's Ruffled Dress + AddItem(m2f, f2m, 38255, 38259); // Valentione Emissary's Bottoms <-> Valentione Emissary's Culottes + AddItem(m2f, f2m, 38256, 38260); // Valentione Emissary's Boots <-> Valentione Emissary's Boots + } + + // The racial starter sets are available for all 4 slots each, + // but have no associated accessories or hats. + private static readonly uint[] RaceGenderGroup = + { + 0x020054, + 0x020055, + 0x020056, + 0x020057, + 0x02005C, + 0x02005D, + 0x020058, + 0x020059, + 0x02005A, + 0x02005B, + 0x020101, + 0x020102, + 0x010255, + uint.MaxValue, // TODO: Female Hrothgar + 0x0102E8, + 0x010245, + }; + // @Formatter:on +} diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs new file mode 100644 index 00000000..1b58d39b --- /dev/null +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +internal sealed class WeaponIdentificationList : KeyList +{ + private const string Tag = "WeaponIdentification"; + private const int Version = 1; + + public WeaponIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) + : base(pi, Tag, language, Version, CreateWeaponList(gameData, language)) + { } + + public IEnumerable Between(SetId modelId) + => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); + + public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) + { + if (type == 0) + return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); + if (variant == 0) + return Between(ToKey(modelId, type, 0), ToKey(modelId, type, 0xFF)); + + return Between(ToKey(modelId, type, variant), ToKey(modelId, type, variant)); + } + + public void Dispose(DalamudPluginInterface pi, ClientLanguage language) + => DataSharer.DisposeTag(pi, Tag, language, Version); + + public static ulong ToKey(SetId modelId, WeaponType type, byte variant) + => ((ulong)modelId << 32) | ((ulong)type << 16) | variant; + + public static ulong ToKey(Item i, bool offhand) + { + var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; + return ToKey(quad.A, quad.B, (byte)quad.C); + } + + protected override IEnumerable ToKeys(Item i) + { + var key1 = 0ul; + if (i.ModelMain != 0) + { + key1 = ToKey(i, false); + yield return key1; + } + + if (i.ModelSub != 0) + { + var key2 = ToKey(i, true); + if (key1 != key2) + yield return key2; + } + } + + protected override bool ValidKey(ulong key) + => key != 0; + + protected override int ValueKeySelector(Item data) + => (int)data.RowId; + + private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + { + var items = gameData.GetExcelSheet(language)!; + return items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand); + } +} diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index c8004f94..2ab5b002 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -36,7 +36,7 @@ public enum EquipSlot : byte public static class EquipSlotExtensions { - public static EquipSlot ToEquipSlot( this uint value ) + public static EquipSlot ToEquipSlot(this uint value) => value switch { 0 => EquipSlot.Head, @@ -54,7 +54,7 @@ public static class EquipSlotExtensions _ => EquipSlot.Unknown, }; - public static uint ToIndex( this EquipSlot slot ) + public static uint ToIndex(this EquipSlot slot) => slot switch { EquipSlot.Head => 0, @@ -72,7 +72,7 @@ public static class EquipSlotExtensions _ => uint.MaxValue, }; - public static string ToSuffix( this EquipSlot value ) + public static string ToSuffix(this EquipSlot value) { return value switch { @@ -90,7 +90,7 @@ public static class EquipSlotExtensions }; } - public static EquipSlot ToSlot( this EquipSlot value ) + public static EquipSlot ToSlot(this EquipSlot value) { return value switch { @@ -116,11 +116,11 @@ public static class EquipSlotExtensions EquipSlot.BodyHands => EquipSlot.Body, EquipSlot.BodyLegsFeet => EquipSlot.Body, EquipSlot.ChestHands => EquipSlot.Body, - _ => throw new InvalidEnumArgumentException( $"{value} ({( int )value}) is not valid." ), + _ => throw new InvalidEnumArgumentException($"{value} ({(int)value}) is not valid."), }; } - public static string ToName( this EquipSlot value ) + public static string ToName(this EquipSlot value) { return value switch { @@ -150,7 +150,7 @@ public static class EquipSlotExtensions }; } - public static bool IsEquipment( this EquipSlot value ) + public static bool IsEquipment(this EquipSlot value) { return value switch { @@ -163,7 +163,7 @@ public static class EquipSlotExtensions }; } - public static bool IsAccessory( this EquipSlot value ) + public static bool IsAccessory(this EquipSlot value) { return value switch { @@ -176,14 +176,40 @@ public static class EquipSlotExtensions }; } - public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray(); - public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray(); - public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray(); + public static bool IsEquipmentPiece(this EquipSlot value) + { + return value switch + { + // Accessories + EquipSlot.RFinger => true, + EquipSlot.Wrists => true, + EquipSlot.Ears => true, + EquipSlot.Neck => true, + // Equipment + EquipSlot.Head => true, + EquipSlot.Body => true, + EquipSlot.Hands => true, + EquipSlot.Legs => true, + EquipSlot.Feet => true, + EquipSlot.BodyHands => true, + EquipSlot.BodyHandsLegsFeet => true, + EquipSlot.BodyLegsFeet => true, + EquipSlot.FullBody => true, + EquipSlot.HeadBody => true, + EquipSlot.LegsFeet => true, + EquipSlot.ChestHands => true, + _ => false, + }; + } + + public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues().Where(e => e.IsEquipment()).ToArray(); + public static readonly EquipSlot[] AccessorySlots = Enum.GetValues().Where(e => e.IsAccessory()).ToArray(); + public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat(AccessorySlots).ToArray(); } public static partial class Names { - public static readonly Dictionary< string, EquipSlot > SuffixToEquipSlot = new() + public static readonly Dictionary SuffixToEquipSlot = new() { { EquipSlot.Head.ToSuffix(), EquipSlot.Head }, { EquipSlot.Hands.ToSuffix(), EquipSlot.Hands }, @@ -196,4 +222,4 @@ public static partial class Names { EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger }, { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists }, }; -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Enums/ModelTypeExtensions.cs b/Penumbra.GameData/Enums/ModelTypeExtensions.cs new file mode 100644 index 00000000..e872aef8 --- /dev/null +++ b/Penumbra.GameData/Enums/ModelTypeExtensions.cs @@ -0,0 +1,26 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.GameData.Enums; + +public static class ModelTypeExtensions +{ + public static string ToName(this CharacterBase.ModelType type) + => type switch + { + CharacterBase.ModelType.DemiHuman => "Demihuman", + CharacterBase.ModelType.Monster => "Monster", + CharacterBase.ModelType.Human => "Human", + CharacterBase.ModelType.Weapon => "Weapon", + _ => string.Empty, + }; + + public static CharacterBase.ModelType ToModelType(this ObjectType type) + => type switch + { + ObjectType.DemiHuman => CharacterBase.ModelType.DemiHuman, + ObjectType.Monster => CharacterBase.ModelType.Monster, + ObjectType.Character => CharacterBase.ModelType.Human, + ObjectType.Weapon => CharacterBase.ModelType.Weapon, + _ => 0, + }; +} diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index e29499a6..f5fc1d05 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -58,10 +58,10 @@ public interface IObjectIdentifier : IDisposable /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. /// The variant ID of the model. /// The equipment slot the piece of equipment uses. - public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); /// - public IReadOnlyList Identify(SetId setId, ushort variant, EquipSlot slot) + public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); } diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 16581e9c..2cfbb711 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -88,19 +88,19 @@ public partial class IndividualCollections var kind = ObjectKind.None; var lowerName = name.ToLowerInvariant(); // Prefer matching NPC names, fewer false positives than preferring players. - if( FindDataId( lowerName, _actorManager.Companions, out var dataId ) ) + if( FindDataId( lowerName, _actorManager.Data.Companions, out var dataId ) ) { kind = ObjectKind.Companion; } - else if( FindDataId( lowerName, _actorManager.Mounts, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.Data.Mounts, out dataId ) ) { kind = ObjectKind.MountType; } - else if( FindDataId( lowerName, _actorManager.BNpcs, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.Data.BNpcs, out dataId ) ) { kind = ObjectKind.BattleNpc; } - else if( FindDataId( lowerName, _actorManager.ENpcs, out dataId ) ) + else if( FindDataId( lowerName, _actorManager.Data.ENpcs, out dataId ) ) { kind = ObjectKind.EventNpc; } @@ -111,7 +111,7 @@ public partial class IndividualCollections // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. var group = GetGroup( identifier ); var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); - if( Add( $"{_actorManager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) + if( Add( $"{_actorManager.Data.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) { Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); } @@ -128,7 +128,7 @@ public partial class IndividualCollections identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); // Try to migrate the player name without logging full names. - if( Add( $"{name} ({_actorManager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) + if( Add( $"{name} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) { Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); } diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 57f7f824..d2d8bb70 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -100,13 +100,14 @@ public sealed partial class IndividualCollections static ActorIdentifier[] CreateNpcs( ActorManager manager, ActorIdentifier identifier ) { - var name = manager.ToName( identifier.Kind, identifier.DataId ); + var name = manager.Data.ToName( identifier.Kind, identifier.DataId ); var table = identifier.Kind switch { - ObjectKind.BattleNpc => manager.BNpcs, - ObjectKind.EventNpc => manager.ENpcs, - ObjectKind.Companion => manager.Companions, - ObjectKind.MountType => manager.Mounts, + ObjectKind.BattleNpc => manager.Data.BNpcs, + ObjectKind.EventNpc => manager.Data.ENpcs, + ObjectKind.Companion => manager.Data.Companions, + ObjectKind.MountType => manager.Data.Mounts, + ( ObjectKind )15 => manager.Data.Ornaments, _ => throw new NotImplementedException(), }; return table.Where( kvp => kvp.Value == name ) @@ -205,11 +206,11 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => - $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}", - IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", + $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})'s {_actorManager.Data.ToName( identifier.Kind, identifier.DataId )}", + IdentifierType.Npc => $"{_actorManager.Data.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", _ => string.Empty, }; } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index c763c505..245dc63b 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -44,10 +46,10 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - if( item.Value.Item2 is Item it ) + if( DrawChangedItemObject( item.Value.Item2, out var text ) ) { using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGuiUtil.RightAlign( $"({( ( Quad )it.ModelMain ).A})" ); + ImGuiUtil.RightAlign( text ); } } @@ -84,8 +86,8 @@ public partial class ConfigWindow const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "id", flags, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "mods", flags, varWidth - 120 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "id", flags, 120 * ImGuiHelpers.GlobalScale ); var items = Penumbra.CollectionManager.Current.ChangedItems; var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 5e8c4967..57ff05a8 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -68,12 +68,12 @@ public partial class ConfigWindow private string _newCharacterName = string.Empty; private ObjectKind _newKind = ObjectKind.BattleNpc; - private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Worlds); - private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Mounts); - private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Companions); - private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Ornaments); - private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); - private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); + private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Data.Worlds); + private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Data.Mounts ); + private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Data.Companions ); + private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Data.Ornaments ); + private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.Data.BNpcs ); + private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.Data.ENpcs ); private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 57351b71..8036863e 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -16,6 +16,7 @@ using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.UI; @@ -193,7 +194,8 @@ public partial class ConfigWindow ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); var identifier = Penumbra.Actors.FromObject( obj, true ); ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); - ImGuiUtil.DrawTableColumn( identifier.DataId.ToString() ); + var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn( id ); } } diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 394f78fe..9cc18bf4 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; @@ -12,6 +13,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.UI.Classes; @@ -71,10 +73,27 @@ public partial class ConfigWindow } } - if( data is Item it && drawId ) + if( drawId && DrawChangedItemObject( data, out var text ) ) { ImGui.SameLine( ImGui.GetContentRegionAvail().X ); - ImGuiUtil.RightJustify( $"({( ( Quad )it.ModelMain ).A})", ColorId.ItemId.Value() ); + ImGuiUtil.RightJustify( text, ColorId.ItemId.Value() ); + } + } + + private static bool DrawChangedItemObject( object? obj, out string text ) + { + switch( obj ) + { + case Item it: + var quad = ( Quad )it.ModelMain; + text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + return true; + case ModelChara m: + text = $"({( ( CharacterBase.ModelType )m.Type ).ToName()} {m.Model}-{m.Base}-{m.Variant})"; + return true; + default: + text = string.Empty; + return false; } } From 7033b65d335356122be7b53b5d56448a3940aed5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 24 Nov 2022 21:54:02 +0100 Subject: [PATCH 0595/2451] Add an option to reduplicate and normalize a mod. --- Penumbra/Mods/Editor/Mod.Editor.Groups.cs | 70 --- Penumbra/Mods/Editor/Mod.Normalization.cs | 262 +++++++++ Penumbra/Mods/Editor/ModCleanup.cs | 629 ---------------------- Penumbra/Mods/Mod.Creation.cs | 18 +- Penumbra/UI/Classes/ModEditWindow.cs | 19 +- 5 files changed, 290 insertions(+), 708 deletions(-) delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.Groups.cs create mode 100644 Penumbra/Mods/Editor/Mod.Normalization.cs delete mode 100644 Penumbra/Mods/Editor/ModCleanup.cs diff --git a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs deleted file mode 100644 index 2e1a4ab2..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - public void Normalize() - {} - - public void AutoGenerateGroups() - { - //ClearEmptySubDirectories( _mod.BasePath ); - //for( var i = _mod.Groups.Count - 1; i >= 0; --i ) - //{ - // if (_mod.Groups.) - // Penumbra.ModManager.DeleteModGroup( _mod, i ); - //} - //Penumbra.ModManager.OptionSetFiles( _mod, -1, 0, new Dictionary< Utf8GamePath, FullPath >() ); - // - //foreach( var groupDir in _mod.BasePath.EnumerateDirectories() ) - //{ - // var groupName = groupDir.Name; - // foreach( var optionDir in groupDir.EnumerateDirectories() ) - // { } - //} - - //var group = new OptionGroup - // { - // GroupName = groupDir.Name, - // SelectionType = SelectType.Single, - // Options = new List< Option >(), - // }; - // - // foreach( var optionDir in groupDir.EnumerateDirectories() ) - // { - // var option = new Option - // { - // OptionDesc = string.Empty, - // OptionName = optionDir.Name, - // OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - // }; - // foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - // { - // if( Utf8RelPath.FromFile( file, baseDir, out var rel ) - // && Utf8GamePath.FromFile( file, optionDir, out var game ) ) - // { - // option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; - // } - // } - // - // if( option.OptionFiles.Count > 0 ) - // { - // group.Options.Add( option ); - // } - // } - // - // if( group.Options.Count > 0 ) - // { - // meta.Groups.Add( groupDir.Name, group ); - // } - //} - // - //var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); - //foreach( var collection in Penumbra.CollectionManager ) - //{ - // collection.Settings[ idx ]?.FixInvalidSettings( meta ); - //} - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs new file mode 100644 index 00000000..73a92a09 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Normalization.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public void Normalize( Manager manager ) + => ModNormalizer.Normalize( manager, this ); + + private struct ModNormalizer + { + private readonly Mod _mod; + private readonly string _normalizationDirName; + private readonly string _oldDirName; + private Dictionary< Utf8GamePath, FullPath >[][]? _redirections = null; + + private ModNormalizer( Mod mod ) + { + _mod = mod; + _normalizationDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalization" ); + _oldDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalizationOld" ); + } + + public static void Normalize( Manager manager, Mod mod ) + { + var normalizer = new ModNormalizer( mod ); + try + { + Penumbra.Log.Debug( $"[Normalization] Starting Normalization of {mod.ModPath.Name}..." ); + if( !normalizer.CheckDirectories() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Copying files to temporary directory structure..." ); + if( !normalizer.CopyNewFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Moving old files out of the way..." ); + if( !normalizer.MoveOldFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Moving new directory structure in place..." ); + if( !normalizer.MoveNewFiles() ) + { + return; + } + + Penumbra.Log.Debug( "[Normalization] Applying new redirections..." ); + normalizer.ApplyRedirections( manager ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + } + finally + { + Penumbra.Log.Debug( "[Normalization] Cleaning up remaining directories..." ); + normalizer.Cleanup(); + } + } + + private bool CheckDirectories() + { + if( Directory.Exists( _normalizationDirName ) ) + { + ChatUtil.NotificationMessage( "Could not normalize mod:\n" + + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", + NotificationType.Error ); + return false; + } + + if( Directory.Exists( _oldDirName ) ) + { + ChatUtil.NotificationMessage( "Could not normalize mod:\n" + + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", + NotificationType.Error ); + return false; + } + + return true; + } + + private void Cleanup() + { + if( Directory.Exists( _normalizationDirName ) ) + { + try + { + Directory.Delete( _normalizationDirName, true ); + } + catch + { + // ignored + } + } + + if( Directory.Exists( _oldDirName ) ) + { + try + { + foreach( var dir in new DirectoryInfo( _oldDirName ).EnumerateDirectories() ) + { + dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); + } + + Directory.Delete( _oldDirName, true ); + } + catch + { + // ignored + } + } + } + + private bool CopyNewFiles() + { + // We copy all files to a temporary folder to ensure that we can revert the operation on failure. + try + { + var directory = Directory.CreateDirectory( _normalizationDirName ); + _redirections = new Dictionary< Utf8GamePath, FullPath >[_mod.Groups.Count + 1][]; + _redirections[ 0 ] = new Dictionary< Utf8GamePath, FullPath >[] { new(_mod.Default.Files.Count) }; + + // Normalize the default option. + var newDict = new Dictionary< Utf8GamePath, FullPath >( _mod.Default.Files.Count ); + _redirections[ 0 ][ 0 ] = newDict; + foreach( var (gamePath, fullPath) in _mod._default.FileData ) + { + var relPath = new Utf8RelPath( gamePath ).ToString(); + var newFullPath = Path.Combine( directory.FullName, relPath ); + var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, relPath ) ); + Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); + File.Copy( fullPath.FullName, newFullPath, true ); + newDict.Add( gamePath, redirectPath ); + } + + // Normalize all other options. + foreach( var (group, groupIdx) in _mod.Groups.WithIndex() ) + { + _redirections[ groupIdx + 1 ] = new Dictionary< Utf8GamePath, FullPath >[group.Count]; + var groupDir = CreateModFolder( directory, group.Name ); + + foreach( var option in group.OfType< SubMod >() ) + { + var optionDir = CreateModFolder( groupDir, option.Name ); + newDict = new Dictionary< Utf8GamePath, FullPath >( option.FileData.Count ); + _redirections[ groupIdx + 1 ][ option.OptionIdx ] = newDict; + foreach( var (gamePath, fullPath) in option.FileData ) + { + var relPath = new Utf8RelPath( gamePath ).ToString(); + var newFullPath = Path.Combine( optionDir.FullName, relPath ); + var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath ) ); + Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); + File.Copy( fullPath.FullName, newFullPath, true ); + newDict.Add( gamePath, redirectPath ); + } + } + } + + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + _redirections = null; + } + + return false; + } + + private bool MoveOldFiles() + { + try + { + // Clean old directories and files. + var oldDirectory = Directory.CreateDirectory( _oldDirName ); + foreach( var dir in _mod.ModPath.EnumerateDirectories() ) + { + if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) + || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } + + dir.MoveTo( Path.Combine( oldDirectory.FullName, dir.Name ) ); + } + + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + } + + return false; + } + + private bool MoveNewFiles() + { + try + { + var mainDir = new DirectoryInfo( _normalizationDirName ); + foreach( var dir in mainDir.EnumerateDirectories() ) + { + dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); + } + + mainDir.Delete(); + Directory.Delete( _oldDirName, true ); + return true; + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + foreach( var dir in _mod.ModPath.EnumerateDirectories() ) + { + if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) + || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } + + try + { + dir.Delete( true ); + } + catch + { + // ignored + } + } + } + + return false; + } + + private void ApplyRedirections( Manager manager ) + { + if( _redirections == null ) + { + return; + } + + foreach( var option in _mod.AllSubMods.OfType< SubMod >() ) + { + manager.OptionSetFiles( _mod, option.GroupIdx, option.OptionIdx, _redirections[ option.GroupIdx + 1 ][ option.OptionIdx ] ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModCleanup.cs b/Penumbra/Mods/Editor/ModCleanup.cs deleted file mode 100644 index a2e7dec0..00000000 --- a/Penumbra/Mods/Editor/ModCleanup.cs +++ /dev/null @@ -1,629 +0,0 @@ -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Manager - { - //public class Normalizer - //{ - // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Files = new(); - // private Dictionary< Utf8GamePath, (FullPath Path, int GroupPriority) > Swaps = new(); - // private HashSet< (MetaManipulation Manipulation, int GroupPriority) > Manips = new(); - // - // public Normalizer( Mod mod ) - // { - // // Default changes are irrelevant since they can only be overwritten. - // foreach( var group in mod.Groups ) - // { - // foreach( var option in group ) - // { - // foreach( var (key, value) in option.Files ) - // { - // if( !Files.TryGetValue( key, out var list ) ) - // { - // list = new List< (FullPath Path, IModGroup Group, ISubMod Option) > { ( value, @group, option ) }; - // Files[ key ] = list; - // } - // else - // { - // list.Add( ( value, @group, option ) ); - // } - // } - // } - // } - // } - // - // // Normalize a mod, this entails: - // // - If - // public static void Normalize( Mod mod ) - // { - // NormalizeOptions( mod ); - // MergeSingleGroups( mod ); - // DeleteEmptyGroups( mod ); - // } - // - // - // // Delete every option group that has either no options, - // // or exclusively empty options. - // // Triggers changes through calling ModManager. - // private static void DeleteEmptyGroups( Mod mod ) - // { - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // DeleteIdenticalOptions( mod, i ); - // var group = mod.Groups[ i ]; - // if( group.Count == 0 || group.All( o => o.FileSwaps.Count == 0 && o.Files.Count == 0 && o.Manipulations.Count == 0 ) ) - // { - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // // Merge every non-optional group into the default mod. - // // Overwrites default mod entries if necessary. - // // Deletes the non-optional group afterwards. - // // Triggers changes through calling ModManager. - // private static void MergeSingleGroup( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Single && group.Count == 1 ) - // { - // defaultMod.MergeIn( group[ 0 ] ); - // - // Penumbra.ModManager.DeleteModGroup( mod, i-- ); - // } - // } - // } - // - // private static void NotifyChanges( Mod mod, int groupIdx, ModOptionChangeType type, ref bool anyChanges ) - // { - // if( anyChanges ) - // { - // for( var i = 0; i < mod.Groups[ groupIdx ].Count; ++i ) - // { - // Penumbra.ModManager.ModOptionChanged.Invoke( type, mod, groupIdx, i, -1 ); - // } - // - // anyChanges = false; - // } - // } - // - // private static void NormalizeOptions( Mod mod ) - // { - // var defaultMod = ( SubMod )mod.Default; - // - // for( var i = 0; i < mod.Groups.Count; ++i ) - // { - // var group = mod.Groups[ i ]; - // if( group.Type == SelectType.Multi || group.Count < 2 ) - // { - // continue; - // } - // - // var firstOption = mod.Groups[ i ][ 0 ]; - // var anyChanges = false; - // foreach( var (key, value) in firstOption.Files.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.Files.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionFilesChanged, ref anyChanges ); - // - // foreach( var (key, value) in firstOption.FileSwaps.ToList() ) - // { - // if( group.Skip( 1 ).All( o => o.FileSwaps.TryGetValue( key, out var v ) && v.Equals( value ) ) ) - // { - // anyChanges = true; - // defaultMod.FileData[ key ] = value; - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.FileSwapData.Remove( key ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionSwapsChanged, ref anyChanges ); - // - // anyChanges = false; - // foreach( var manip in firstOption.Manipulations.ToList() ) - // { - // if( group.Skip( 1 ).All( o => ( ( HashSet< MetaManipulation > )o.Manipulations ).TryGetValue( manip, out var m ) - // && manip.EntryEquals( m ) ) ) - // { - // anyChanges = true; - // defaultMod.ManipulationData.Remove( manip ); - // defaultMod.ManipulationData.Add( manip ); - // foreach( var option in group.Cast< SubMod >() ) - // { - // option.ManipulationData.Remove( manip ); - // } - // } - // } - // - // NotifyChanges( mod, i, ModOptionChangeType.OptionMetaChanged, ref anyChanges ); - // } - // } - // - // - // // Delete all options that are entirely identical. - // // Deletes the later occurring option. - // private static void DeleteIdenticalOptions( Mod mod, int groupIdx ) - // { - // var group = mod.Groups[ groupIdx ]; - // for( var i = 0; i < group.Count; ++i ) - // { - // var option = group[ i ]; - // for( var j = i + 1; j < group.Count; ++j ) - // { - // var option2 = group[ j ]; - // if( option.Files.SetEquals( option2.Files ) - // && option.FileSwaps.SetEquals( option2.FileSwaps ) - // && option.Manipulations.SetEquals( option2.Manipulations ) ) - // { - // Penumbra.ModManager.DeleteOption( mod, groupIdx, j-- ); - // } - // } - // } - // } - //} - } -} - -// TODO Everything -//ublic class ModCleanup -// -// private const string Duplicates = "Duplicates"; -// private const string Required = "Required"; -// -// private readonly DirectoryInfo _baseDir; -// private readonly ModMeta _mod; - -// -// private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); -// -// -// private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) -// { -// _baseDir = baseDir; -// _mod = mod; -// BuildDict(); -// } -// -// private void BuildDict() -// { -// foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) -// { -// var fileLength = file.Length; -// if( _filesBySize.TryGetValue( fileLength, out var files ) ) -// { -// files.Add( file ); -// } -// else -// { -// _filesBySize[ fileLength ] = new List< FileInfo > { file }; -// } -// } -// } -// -// private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) -// { -// var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; -// return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); -// } -// -// private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) -// { -// var idx = Penumbra.ModManager.AddMod( newDir ); -// var newMod = Penumbra.ModManager.Mods[ idx ]; -// newMod.Move( newSortOrder ); -// newMod.ComputeChangedItems(); -// ModFileSystem.InvokeChange(); -// return newMod; -// } -// -// private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) -// { -// var newMeta = new ModMeta -// { -// Author = mod.Meta.Author, -// Name = name, -// Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", -// }; -// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); -// newMeta.SaveToFile( metaFile ); -// return newMeta; -// } -// -// private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) -// { -// try -// { -// var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); -// var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; -// var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); -// foreach( var (fileName, paths) in option.OptionFiles ) -// { -// var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); -// unseenPaths.Remove( oldPath ); -// if( File.Exists( oldPath ) ) -// { -// foreach( var path in paths ) -// { -// var newPath = Path.Combine( newDir.FullName, path.ToString() ); -// Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); -// File.Copy( oldPath, newPath, true ); -// } -// } -// } -// -// var newSortOrder = group.SelectionType == SelectType.Single -// ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" -// : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; -// CreateNewMod( newDir, newSortOrder ); -// } -// catch( Exception e ) -// { -// Penumbra.Log.Error( $"Could not split Mod:\n{e}" ); -// } -// } -// -// public static void SplitMod( Mod mod ) -// { -// if( mod.Meta.Groups.Count == 0 ) -// { -// return; -// } -// -// var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); -// foreach( var group in mod.Meta.Groups.Values ) -// { -// foreach( var option in group.Options ) -// { -// CreateModSplit( unseenPaths, mod, group, option ); -// } -// } -// -// if( unseenPaths.Count == 0 ) -// { -// return; -// } -// -// var defaultGroup = new OptionGroup() -// { -// GroupName = "Default", -// SelectionType = SelectType.Multi, -// }; -// var defaultOption = new Option() -// { -// OptionName = "Files", -// OptionFiles = unseenPaths.ToDictionary( -// p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, -// p => new HashSet< Utf8GamePath >() -// { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), -// }; -// CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); -// } -// -// private static Option FindOrCreateDuplicates( ModMeta meta ) -// { -// static Option RequiredOption() -// => new() -// { -// OptionName = Required, -// OptionDesc = "", -// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), -// }; -// -// if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) -// { -// var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); -// if( idx >= 0 ) -// { -// return duplicates.Options[ idx ]; -// } -// -// duplicates.Options.Add( RequiredOption() ); -// return duplicates.Options.Last(); -// } -// -// meta.Groups.Add( Duplicates, new OptionGroup -// { -// GroupName = Duplicates, -// SelectionType = SelectType.Single, -// Options = new List< Option > { RequiredOption() }, -// } ); -// -// return meta.Groups[ Duplicates ].Options.First(); -// } -// -// -// private void ReplaceFile( FileInfo f1, FileInfo f2 ) -// { -// if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) -// || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) -// { -// return; -// } -// -// var inOption1 = false; -// var inOption2 = false; -// foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) -// { -// if( option.OptionFiles.ContainsKey( relName1 ) ) -// { -// inOption1 = true; -// } -// -// if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) -// { -// continue; -// } -// -// inOption2 = true; -// -// foreach( var value in values ) -// { -// option.AddFile( relName1, value ); -// } -// -// option.OptionFiles.Remove( relName2 ); -// } -// -// if( !inOption1 || !inOption2 ) -// { -// var duplicates = FindOrCreateDuplicates( _mod ); -// if( !inOption1 ) -// { -// duplicates.AddFile( relName1, relName2.ToGamePath() ); -// } -// -// if( !inOption2 ) -// { -// duplicates.AddFile( relName1, relName1.ToGamePath() ); -// } -// } -// -// Penumbra.Log.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); -// f2.Delete(); -// } -// -// - -// -// private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) -// { -// var groupEnumerator = exceptDuplicates -// ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) -// : meta.Groups.Values; -// return groupEnumerator.SelectMany( group => group.Options ) -// .Any( option => option.OptionFiles.ContainsKey( relPath ) ); -// } -// -// private static void CleanUpDuplicates( ModMeta meta ) -// { -// if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) -// { -// return; -// } -// -// var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); -// if( requiredIdx >= 0 ) -// { -// var required = info.Options[ requiredIdx ]; -// foreach( var (key, value) in required.OptionFiles.ToArray() ) -// { -// if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) -// { -// continue; -// } -// -// if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) -// { -// required.OptionFiles.Remove( key ); -// } -// } -// -// if( required.OptionFiles.Count == 0 ) -// { -// info.Options.RemoveAt( requiredIdx ); -// } -// } -// -// if( info.Options.Count == 0 ) -// { -// meta.Groups.Remove( Duplicates ); -// } -// } -// -// public enum GroupType -// { -// Both = 0, -// Single = 1, -// Multi = 2, -// }; -// -// private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, -// bool skipDuplicates = true ) -// { -// if( meta.Groups.Count == 0 ) -// { -// return; -// } -// -// var enumerator = type switch -// { -// GroupType.Both => meta.Groups.Values, -// GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), -// GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), -// _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), -// }; -// foreach( var group in enumerator ) -// { -// var optionEnum = skipDuplicates -// ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) -// : group.Options; -// foreach( var option in optionEnum ) -// { -// if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) -// { -// option.OptionFiles.Remove( relPath ); -// } -// } -// } -// } -// -// public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) -// { -// if( oldRelPath.Equals( newRelPath ) ) -// { -// return true; -// } -// -// try -// { -// var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); -// new FileInfo( newFullPath ).Directory!.Create(); -// File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); -// } -// catch( Exception e ) -// { -// Penumbra.Log.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); -// return false; -// } -// -// foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) -// { -// if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) -// { -// option.OptionFiles.Add( newRelPath, gamePaths ); -// option.OptionFiles.Remove( oldRelPath ); -// } -// } -// -// return true; -// } -// -// -// private static void RemoveUselessGroups( ModMeta meta ) -// { -// meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) -// .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); -// } -// -// // Goes through all Single-Select options and checks if file links are in each of them. -// // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). -// public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) -// { -// foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) -// { -// var firstOption = true; -// HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); -// foreach( var option in group.Options ) -// { -// HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); -// foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) -// { -// optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); -// } -// -// if( firstOption ) -// { -// groupList = optionList; -// } -// else -// { -// groupList.IntersectWith( optionList ); -// } -// -// firstOption = false; -// } -// -// var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); -// foreach( var (path, gamePath) in groupList ) -// { -// var relPath = new Utf8RelPath( gamePath ); -// if( newPath.TryGetValue( path, out var usedGamePath ) ) -// { -// var required = FindOrCreateDuplicates( meta ); -// var usedRelPath = new Utf8RelPath( usedGamePath ); -// required.AddFile( usedRelPath, gamePath ); -// required.AddFile( usedRelPath, usedGamePath ); -// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); -// } -// else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) -// { -// newPath[ path ] = gamePath; -// if( FileIsInAnyGroup( meta, relPath ) ) -// { -// FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); -// } -// -// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); -// } -// } -// } -// -// RemoveUselessGroups( meta ); -// ClearEmptySubDirectories( baseDir ); -// } -// -// public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) -// { -// meta.Groups.Clear(); -// ClearEmptySubDirectories( baseDir ); -// foreach( var groupDir in baseDir.EnumerateDirectories() ) -// { -// var group = new OptionGroup -// { -// GroupName = groupDir.Name, -// SelectionType = SelectType.Single, -// Options = new List< Option >(), -// }; -// -// foreach( var optionDir in groupDir.EnumerateDirectories() ) -// { -// var option = new Option -// { -// OptionDesc = string.Empty, -// OptionName = optionDir.Name, -// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), -// }; -// foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) -// { -// if( Utf8RelPath.FromFile( file, baseDir, out var rel ) -// && Utf8GamePath.FromFile( file, optionDir, out var game ) ) -// { -// option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; -// } -// } -// -// if( option.OptionFiles.Count > 0 ) -// { -// group.Options.Add( option ); -// } -// } -// -// if( group.Options.Count > 0 ) -// { -// meta.Groups.Add( groupDir.Name, group ); -// } -// } -// -// var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); -// foreach( var collection in Penumbra.CollectionManager ) -// { -// collection.Settings[ idx ]?.FixInvalidSettings( meta ); -// } -// } -// \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 5c7cd701..afb59c94 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -13,10 +13,17 @@ namespace Penumbra.Mods; public partial class Mod { - // Create and return a new directory based on the given directory and name, that is - // - Not Empty - // - Unique, by appending (digit) for duplicates. - // - Containing no symbols invalid for FFXIV or windows paths. + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ /// + /// + /// + /// + /// internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { var name = modListName; @@ -160,8 +167,9 @@ public partial class Mod { return replacement + replacement; } + StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize(NormalizationForm.FormKC) ) + foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) { if( c.IsInvalidAscii() || c.IsInvalidInPath() ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index bbe21025..f33127b3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -20,10 +20,11 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - private Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private const string WindowBaseLabel = "###SubModEdit"; + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; + private bool _allowReduplicate = false; public void ChangeMod( Mod mod ) { @@ -118,6 +119,7 @@ public partial class ModEditWindow : Window, IDisposable sb.Append( $" | {swaps} Swaps" ); } + _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; sb.Append( WindowBaseLabel ); WindowName = sb.ToString(); } @@ -288,6 +290,15 @@ public partial class ModEditWindow : Window, IDisposable _editor.StartDuplicateCheck(); } + const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + + "This will also delete all unused files and directories if it succeeds.\n" + + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; + if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, desc, !_allowReduplicate ) ) + { + _mod!.Normalize( Penumbra.ModManager ); + _editor.RevertFiles(); + } + if( !_editor.DuplicatesFinished ) { ImGui.SameLine(); From 7a09d561e90c74894ca0247c76213f6d9e0106c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Nov 2022 12:25:52 +0100 Subject: [PATCH 0596/2451] Fix a bug with RSP changes on non-base collections. --- .../Resolver/PathResolver.DrawObjectState.cs | 15 ++++++++------- Penumbra/Interop/Resolver/PathResolver.Meta.cs | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 47cd5e6d..18e04041 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -144,10 +144,8 @@ public unsafe partial class PathResolver _lastCreatedCollection = IdentifyCollection( LastGameObject, false ); // Change the transparent or 1.0 Decal if necessary. var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); - // Change the rsp parameters if necessary. - meta = new DisposableContainer( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default - ? _lastCreatedCollection.ModCollection.TemporarilySetCmpFile() - : null, decal ); + // Change the rsp parameters. + meta = new DisposableContainer( _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal ); try { var modelPtr = &a; @@ -160,16 +158,19 @@ public unsafe partial class PathResolver } var ret = _characterBaseCreateHook.Original( a, b, c, d ); - using( meta ) + try { if( LastGameObject != null && ret != IntPtr.Zero ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret ); } - - return ret; } + finally + { + meta.Dispose(); + } + return ret; } // Check the customize array for the FaceCustomization byte and the last bit of that. diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index dc1e46d2..a15db301 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -188,7 +188,9 @@ public unsafe partial class PathResolver var resolveData = GetResolveData( human ); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); - return _changeCustomize.Original( human, data, skipEquipment ); + var ret = _changeCustomize.Original( human, data, skipEquipment ); + _inChangeCustomize = false; + return ret; } public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory ) From 3391a8ce714acf095f58d6e2ecc45f23d7af1410 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Nov 2022 01:54:09 +0100 Subject: [PATCH 0597/2451] Add functions to re-export meta changes to TexTools .meta and .rgsp formats. --- Penumbra.GameData/Data/GamePathParser.cs | 1 - Penumbra/Import/TexToolsMeta.Export.cs | 242 ++++++++++++++++++ Penumbra/Import/TexToolsMeta.cs | 5 +- Penumbra/Meta/Files/ImcFile.cs | 17 +- .../Meta/Manipulations/MetaManipulation.cs | 12 + Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 97 +++++++ Penumbra/UI/Classes/ModEditWindow.Meta.cs | 33 ++- 7 files changed, 383 insertions(+), 24 deletions(-) create mode 100644 Penumbra/Import/TexToolsMeta.Export.cs diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index 4725ed1f..58817c28 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -7,7 +7,6 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.String; namespace Penumbra.GameData.Data; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs new file mode 100644 index 00000000..03aae64a --- /dev/null +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Import; + +public partial class TexToolsMeta +{ + public static Dictionary< string, byte[] > ConvertToTexTools( IEnumerable< MetaManipulation > manips ) + { + var ret = new Dictionary< string, byte[] >(); + foreach( var group in manips.GroupBy( ManipToPath ) ) + { + if( group.Key.Length == 0 ) + { + continue; + } + + var bytes = group.Key.EndsWith( ".rgsp" ) + ? WriteRgspFile( group.Key, group ) + : WriteMetaFile( group.Key, group ); + if( bytes.Length == 0 ) + { + continue; + } + + ret.Add( group.Key, bytes ); + } + + return ret; + } + + private static byte[] WriteRgspFile( string path, IEnumerable< MetaManipulation > manips ) + { + var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp ); + using var m = new MemoryStream( 45 ); + using var b = new BinaryWriter( m ); + // Version + b.Write( byte.MaxValue ); + b.Write( ( ushort )2 ); + + var race = list.First().Value.SubRace; + var gender = list.First().Value.Attribute.ToGender(); + b.Write( ( byte )(race - 1) ); // offset by one due to Unknown + b.Write( ( byte )(gender - 1) ); // offset by one due to Unknown + + void Add( params RspAttribute[] attributes ) + { + foreach( var attribute in attributes ) + { + var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( race, attribute ); + b.Write( value ); + } + } + + if( gender == Gender.Male ) + { + Add( RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail ); + } + else + { + Add( RspAttribute.FemaleMinSize, RspAttribute.FemaleMaxSize, RspAttribute.FemaleMinTail, RspAttribute.FemaleMaxTail ); + Add( RspAttribute.BustMinX, RspAttribute.BustMinY, RspAttribute.BustMinZ, RspAttribute.BustMaxX, RspAttribute.BustMaxY, RspAttribute.BustMaxZ ); + } + + return m.GetBuffer(); + } + + private static byte[] WriteMetaFile( string path, IEnumerable< MetaManipulation > manips ) + { + var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) ); + + using var m = new MemoryStream(); + using var b = new BinaryWriter( m ); + + // Header + // Current TT Metadata version. + b.Write( 2u ); + + // Null-terminated ASCII path. + var utf8Path = Encoding.ASCII.GetBytes( path ); + b.Write( utf8Path ); + b.Write( ( byte )0 ); + + // Number of Headers + b.Write( ( uint )filteredManips.Count ); + // Current TT Size of Headers + b.Write( ( uint )12 ); + + // Start of Header Entries for some reason, which is absolutely useless. + var headerStart = b.BaseStream.Position + 4; + b.Write( ( uint )headerStart ); + + var offset = ( uint )( b.BaseStream.Position + 12 * filteredManips.Count ); + foreach( var (header, data) in filteredManips ) + { + b.Write( ( uint )header ); + b.Write( offset ); + + var size = WriteData( b, offset, header, data ); + b.Write( size ); + offset += size; + } + + return m.ToArray(); + } + + private static uint WriteData( BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) + { + var oldPos = b.BaseStream.Position; + b.Seek( ( int )offset, SeekOrigin.Begin ); + + switch( type ) + { + case MetaManipulation.Type.Imc: + var allManips = manips.ToList(); + var baseFile = new ImcFile( allManips[ 0 ].Imc ); + foreach( var manip in allManips ) + { + manip.Imc.Apply( baseFile ); + } + + var partIdx = allManips[ 0 ].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? ImcFile.PartIndex( allManips[ 0 ].Imc.EquipSlot ) + : 0; + + for( var i = 0; i <= baseFile.Count; ++i ) + { + var entry = baseFile.GetEntry( partIdx, i ); + b.Write( entry.MaterialId ); + b.Write( entry.DecalId ); + b.Write( entry.AttributeAndSound ); + b.Write( entry.VfxId ); + b.Write( entry.MaterialAnimationId ); + } + + break; + case MetaManipulation.Type.Eqdp: + foreach( var manip in manips ) + { + b.Write( ( uint )Names.CombinedRace( manip.Eqdp.Gender, manip.Eqdp.Race ) ); + var entry = ( byte )(( ( uint )manip.Eqdp.Entry >> Eqdp.Offset( manip.Eqdp.Slot ) ) & 0x03); + b.Write( entry ); + } + + break; + case MetaManipulation.Type.Eqp: + foreach( var manip in manips ) + { + var bytes = BitConverter.GetBytes( (ulong) manip.Eqp.Entry ); + var (numBytes, byteOffset) = Eqp.BytesAndOffset( manip.Eqp.Slot ); + for( var i = byteOffset; i < numBytes + byteOffset; ++i ) + b.Write( bytes[ i ] ); + } + + break; + case MetaManipulation.Type.Est: + foreach( var manip in manips ) + { + b.Write( ( ushort )Names.CombinedRace( manip.Est.Gender, manip.Est.Race ) ); + b.Write( manip.Est.SetId ); + b.Write( manip.Est.Entry ); + } + + break; + case MetaManipulation.Type.Gmp: + foreach( var manip in manips ) + { + b.Write( ( uint )manip.Gmp.Entry.Value ); + b.Write( manip.Gmp.Entry.UnknownTotal ); + } + + break; + } + + var size = b.BaseStream.Position - offset; + b.Seek( ( int )oldPos, SeekOrigin.Begin ); + return ( uint )size; + } + + private static string ManipToPath( MetaManipulation manip ) + => manip.ManipulationType switch + { + MetaManipulation.Type.Imc => ManipToPath( manip.Imc ), + MetaManipulation.Type.Eqdp => ManipToPath( manip.Eqdp ), + MetaManipulation.Type.Eqp => ManipToPath( manip.Eqp ), + MetaManipulation.Type.Est => ManipToPath( manip.Est ), + MetaManipulation.Type.Gmp => ManipToPath( manip.Gmp ), + MetaManipulation.Type.Rsp => ManipToPath( manip.Rsp ), + _ => string.Empty, + }; + + private static string ManipToPath( ImcManipulation manip ) + { + var path = manip.GamePath().ToString(); + var replacement = manip.ObjectType switch + { + ObjectType.Accessory => $"_{manip.EquipSlot.ToSuffix()}.meta", + ObjectType.Equipment => $"_{manip.EquipSlot.ToSuffix()}.meta", + ObjectType.Character => $"_{manip.BodySlot.ToSuffix()}.meta", + _ => ".meta", + }; + + return path.Replace( ".imc", replacement ); + } + + private static string ManipToPath( EqdpManipulation manip ) + => manip.Slot.IsAccessory() + ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + + private static string ManipToPath( EqpManipulation manip ) + => manip.Slot.IsAccessory() + ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + + private static string ManipToPath( EstManipulation manip ) + { + var raceCode = Names.CombinedRace( manip.Gender, manip.Race ).ToRaceCode(); + return manip.Slot switch + { + EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", + EstManipulation.EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", + EstManipulation.EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstManipulation.EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private static string ManipToPath( GmpManipulation manip ) + => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; + + + private static string ManipToPath( RspManipulation manip ) + => $"chara/xls/charamake/rgsp/{( int )manip.SubRace - 1}-{( int )manip.Attribute.ToGender() - 1}.rgsp"; +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 0058764c..2a2c98e5 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -18,7 +19,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // An empty TexToolsMeta. - public static readonly TexToolsMeta Invalid = new( string.Empty, 0 ); + public static readonly TexToolsMeta Invalid = new(string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. @@ -84,7 +85,7 @@ public partial class TexToolsMeta // Read a null terminated string from a binary reader. private static string ReadNullTerminated( BinaryReader reader ) { - var builder = new System.Text.StringBuilder(); + var builder = new StringBuilder(); for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) { builder.Append( c ); diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 8f36507e..5b769b7f 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,7 +1,6 @@ using System; using System.Numerics; using Newtonsoft.Json; -using OtterGui; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -14,26 +13,26 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > { public byte MaterialId { get; init; } public byte DecalId { get; init; } - private readonly ushort _attributeAndSound; + public readonly ushort AttributeAndSound; public byte VfxId { get; init; } public byte MaterialAnimationId { get; init; } public ushort AttributeMask { - get => ( ushort )( _attributeAndSound & 0x3FF ); - init => _attributeAndSound = ( ushort )( ( _attributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); + get => ( ushort )( AttributeAndSound & 0x3FF ); + init => AttributeAndSound = ( ushort )( ( AttributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); } public byte SoundId { - get => ( byte )( _attributeAndSound >> 10 ); - init => _attributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); + get => ( byte )( AttributeAndSound >> 10 ); + init => AttributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); } public bool Equals( ImcEntry other ) => MaterialId == other.MaterialId && DecalId == other.DecalId - && _attributeAndSound == other._attributeAndSound + && AttributeAndSound == other.AttributeAndSound && VfxId == other.VfxId && MaterialAnimationId == other.MaterialAnimationId; @@ -41,14 +40,14 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > => obj is ImcEntry other && Equals( other ); public override int GetHashCode() - => HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId ); + => HashCode.Combine( MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId ); [JsonConstructor] public ImcEntry( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId ) { MaterialId = materialId; DecalId = decalId; - _attributeAndSound = 0; + AttributeAndSound = 0; VfxId = vfxId; MaterialAnimationId = materialAnimationId; AttributeMask = attributeMask; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f388ea8e..f6ff2d94 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -232,4 +232,16 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Type.Imc => Imc.ToString(), _ => throw new ArgumentOutOfRangeException(), }; + + public string EntryToString() + => ManipulationType switch + { + Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", + Type.Eqdp => $"{(ushort) Eqdp.Entry:X}", + Type.Eqp => $"{(ulong)Eqp.Entry:X}", + Type.Est => $"{Est.Entry}", + Type.Gmp => $"{Gmp.Entry.Value}", + Type.Rsp => $"{Rsp.Entry}", + _ => string.Empty, + }; } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 883f74e6..fe0de7d0 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -59,6 +60,37 @@ public partial class Mod } } + public void WriteAllTexToolsMeta() + { + try + { + _default.WriteTexToolsMeta( ModPath ); + foreach( var group in Groups ) + { + var dir = NewOptionDirectory( ModPath, group.Name ); + if( !dir.Exists ) + { + dir.Create(); + } + + foreach( var option in group.OfType< SubMod >() ) + { + var optionDir = NewOptionDirectory( dir, option.Name ); + if( !optionDir.Exists ) + { + optionDir.Create(); + } + + option.WriteTexToolsMeta( optionDir ); + } + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error writing TexToolsMeta:\n{e}" ); + } + } + // A sub mod is a collection of // - file replacements @@ -197,5 +229,70 @@ public partial class Mod } } } + + public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + { + var files = TexToolsMeta.ConvertToTexTools( Manipulations ); + + foreach( var (file, data) in files ) + { + var path = Path.Combine( basePath.FullName, file ); + try + { + Directory.CreateDirectory( Path.GetDirectoryName( path )! ); + File.WriteAllBytes( path, data ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); + } + } + + if( test ) + { + TestMetaWriting( files ); + } + } + + [Conditional("DEBUG")] + private void TestMetaWriting( Dictionary< string, byte[] > files ) + { + var meta = new HashSet< MetaManipulation >( Manipulations.Count ); + foreach( var (file, data) in files ) + { + try + { + var x = file.EndsWith( "rgsp" ) ? TexToolsMeta.FromRgspFile( file, data ) : new TexToolsMeta( data ); + meta.UnionWith( x.MetaManipulations ); + } + catch + { + // ignored + } + } + + if( !Manipulations.SetEquals( meta ) ) + { + Penumbra.Log.Information( "Meta Sets do not equal." ); + foreach( var (m1, m2) in Manipulations.Zip( meta ) ) + { + Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); + } + + foreach( var m in Manipulations.Skip( meta.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + + foreach( var m in meta.Skip( Manipulations.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + } + else + { + Penumbra.Log.Information( "Meta Sets are equal." ); + } + } } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 9a725685..8c2e8340 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -17,18 +17,22 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string PrimaryIdTooltip = "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + private const string ModelSetIdTooltip = + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string PrimaryIdTooltip = + "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; private void DrawMetaTab() { @@ -61,6 +65,11 @@ public partial class ModEditWindow SetFromClipboardButton(); ImGui.SameLine(); CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() ); + ImGui.SameLine(); + if( ImGui.Button( "Write as TexTools Files" ) ) + { + _mod!.WriteAllTexToolsMeta(); + } using var child = ImRaii.Child( "##meta", -Vector2.One, true ); if( !child ) From 4435bb035a6ab58fe7a562f27e01c9c7063bb59c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Nov 2022 11:51:31 +0100 Subject: [PATCH 0598/2451] Allow re-duplicating/normalizing even with no duplicates by hotkey. --- Penumbra/UI/Classes/ModEditWindow.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index f33127b3..f527c555 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -293,7 +293,12 @@ public partial class ModEditWindow : Window, IDisposable const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + "This will also delete all unused files and directories if it succeeds.\n" + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; - if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, desc, !_allowReduplicate ) ) + + var modifier = Penumbra.Config.DeleteModModifier.IsActive(); + + var tt = _allowReduplicate ? desc : modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; + + if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier ) ) { _mod!.Normalize( Penumbra.ModManager ); _editor.RevertFiles(); From 95d7bc0023c825af93b7346ab41ef23a5eb3fb2e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Nov 2022 11:52:04 +0100 Subject: [PATCH 0599/2451] Save groups after incorporating meta files and do not delete meta files that may still be used for other redirections. --- Penumbra/Mods/Mod.BasePath.cs | 8 ++++- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 34 +++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 9ce8d478..851ed943 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -80,9 +80,15 @@ public partial class Mod // Deletes the source files if delete is true. private void IncorporateAllMetaChanges( bool delete ) { + var changes = false; foreach( var subMod in AllSubMods.OfType< SubMod >() ) { - subMod.IncorporateMetaChanges( ModPath, delete ); + changes |= subMod.IncorporateMetaChanges( ModPath, delete ); + } + + if( changes ) + { + SaveAllGroups(); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index fe0de7d0..0cdb9aa8 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -182,8 +182,11 @@ public partial class Mod // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. // If delete is true, the files are deleted afterwards. - public void IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + public bool IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) { + var deleteList = new List< string >(); + var oldSize = ManipulationData.Count; + var deleteString = delete ? "with deletion." : "without deletion."; foreach( var (key, file) in Files.ToList() ) { var ext1 = key.Extension().AsciiToLower().ToString(); @@ -199,11 +202,8 @@ public partial class Mod } var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); - if( delete ) - { - File.Delete( file.FullName ); - } - + Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); ManipulationData.UnionWith( meta.MetaManipulations ); } else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) @@ -215,10 +215,8 @@ public partial class Mod } var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); - if( delete ) - { - File.Delete( file.FullName ); - } + Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); ManipulationData.UnionWith( rgsp.MetaManipulations ); } @@ -228,6 +226,20 @@ public partial class Mod Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); } } + + foreach( var file in deleteList ) + { + try + { + File.Delete( file ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + } + } + + return oldSize < ManipulationData.Count; } public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) @@ -254,7 +266,7 @@ public partial class Mod } } - [Conditional("DEBUG")] + [Conditional( "DEBUG" )] private void TestMetaWriting( Dictionary< string, byte[] > files ) { var meta = new HashSet< MetaManipulation >( Manipulations.Count ); From 69703ed97f5c3d9ff20a40ee40dd4edd65018d6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Nov 2022 21:16:56 +0100 Subject: [PATCH 0600/2451] Remove checking for negative values in colorset editing, show gamepath info in file selection. --- Penumbra/Penumbra.cs | 2 -- .../UI/Classes/ModEditWindow.FileEditor.cs | 24 +++++++++++++++++++ .../UI/Classes/ModEditWindow.Materials.cs | 6 +---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 354a026a..dfb794ea 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using Dalamud.Utility; using EmbedIO; using EmbedIO.WebApi; using ImGuiNET; diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index a4042065..22cb1398 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -150,6 +151,29 @@ public partial class ModEditWindow { UpdateCurrentFile( file ); } + + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted( "All Game Paths" ); + ImGui.Separator(); + using var t = ImRaii.Table( "##Tooltip", 2, ImGuiTableFlags.SizingFixedFit ); + foreach( var (option, gamePath) in file.SubModUsage ) + { + ImGui.TableNextColumn(); + ConfigWindow.Text( gamePath.Path ); + ImGui.TableNextColumn(); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); + ImGui.TextUnformatted( option.FullName ); + } + } + + if( file.SubModUsage.Count > 0 ) + { + ImGui.SameLine(); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); + ImGuiUtil.RightAlign( file.SubModUsage[ 0 ].Item2.Path.ToString() ); + } } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index c59f3f6d..ab63ac79 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -363,11 +363,7 @@ public partial class ModEditWindow { static bool FixFloat( ref float val, float current ) { - if( val < 0 ) - { - val = 0; - } - + val = ( float )( Half )val; return val != current; } From 2900351b9a0b6cabb3e1f9545cb89201a72d4fe4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Dec 2022 17:15:24 +0100 Subject: [PATCH 0601/2451] Fix Player Collection identification. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 2 +- .../Interop/Resolver/PathResolver.Identification.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 36097a37..c54fafe4 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -172,7 +172,7 @@ public sealed partial class ActorManager : IDisposable public unsafe ActorIdentifier GetCurrentPlayer() { - var address = (Character*)(_objects[0]?.Address ?? IntPtr.Zero); + var address = (Character*)_objects.GetObjectAddress(0); return address == null ? ActorIdentifier.Invalid : CreateIndividualUnchecked(IdentifierType.Player, new ByteString(address->GameObject.Name), address->HomeWorld, diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index b5ce0e41..7895e9fa 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -63,14 +63,17 @@ public unsafe partial class PathResolver // or the default collection if no player exists. public static ModCollection PlayerCollection() { - var player = Penumbra.Actors.GetCurrentPlayer(); - if( !player.IsValid ) + var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); + if( gameObject == null ) { - return Penumbra.CollectionManager.Default; + return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + ?? Penumbra.CollectionManager.Default; } + var player = Penumbra.Actors.GetCurrentPlayer(); return CollectionByIdentifier( player ) - ?? CollectionByAttributes( ( GameObject* )Dalamud.Objects[ 0 ]!.Address ) + ?? CheckYourself( player, gameObject ) + ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; } From b50ed4b99aa36b947dfbdce71275e4ddeb000a5c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Dec 2022 17:16:48 +0100 Subject: [PATCH 0602/2451] Add API events for mod deletion, addition or move. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 80 ++++++++++++++++--- Penumbra/Api/PenumbraApi.cs | 22 +++++ Penumbra/Api/PenumbraIpcProviders.cs | 18 +++++ Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 4 +- 5 files changed, 113 insertions(+), 13 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 744698d1..1a3f9d50 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 744698d1295f4e629ab41d77f8872b1ff98fe501 +Subproject commit 1a3f9d501ad44e020500eb7d0a79f91a04e46c93 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index ac532c1b..d6daf14c 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -93,6 +93,9 @@ public class IpcTester : IDisposable _gameState.CharacterBaseCreated.Enable(); _configuration.ModDirectoryChanged.Enable(); _gameState.GameObjectResourcePathResolved.Enable(); + _mods.DeleteSubscriber.Enable(); + _mods.AddSubscriber.Enable(); + _mods.MoveSubscriber.Enable(); _subscribed = true; } } @@ -114,6 +117,9 @@ public class IpcTester : IDisposable _gameState.CharacterBaseCreated.Disable(); _configuration.ModDirectoryChanged.Disable(); _gameState.GameObjectResourcePathResolved.Disable(); + _mods.DeleteSubscriber.Disable(); + _mods.AddSubscriber.Disable(); + _mods.MoveSubscriber.Disable(); _subscribed = false; } } @@ -133,6 +139,9 @@ public class IpcTester : IDisposable _gameState.CharacterBaseCreated.Dispose(); _configuration.ModDirectoryChanged.Dispose(); _gameState.GameObjectResourcePathResolved.Dispose(); + _mods.DeleteSubscriber.Dispose(); + _mods.AddSubscriber.Dispose(); + _mods.MoveSubscriber.Dispose(); _subscribed = false; } @@ -536,30 +545,34 @@ public class IpcTester : IDisposable } } - private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) + private void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = GetObjectName( gameObject ); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = IntPtr.Zero; } - private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) + private void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new ByteString( obj->GetName() ).ToString(); + _lastCreatedGameObjectName = GetObjectName( gameObject ); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = drawObject; } - private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) + private void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastResolvedObject = obj != null ? new ByteString( obj->GetName() ).ToString() : "Unknown"; + _lastResolvedObject = GetObjectName( gameObject ); _lastResolvedGamePath = gamePath; _lastResolvedFullPath = fullPath; _lastResolvedGamePathTime = DateTimeOffset.Now; } + + private static unsafe string GetObjectName( IntPtr gameObject ) + { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + var name = obj != null ? obj->GetName() : null; + return name != null ? new ByteString( name ).ToString() : "Unknown"; + } } private class Resolve @@ -799,8 +812,38 @@ public class IpcTester : IDisposable private PenumbraApiEc _lastSetPathEc; private IList< (string, string) > _mods = new List< (string, string) >(); + public readonly EventSubscriber< string > DeleteSubscriber; + public readonly EventSubscriber< string > AddSubscriber; + public readonly EventSubscriber< string, string > MoveSubscriber; + + private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; + private string _lastDeletedMod = string.Empty; + private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch; + private string _lastAddedMod = string.Empty; + private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch; + private string _lastMovedModFrom = string.Empty; + private string _lastMovedModTo = string.Empty; + public Mods( DalamudPluginInterface pi ) - => _pi = pi; + { + _pi = pi; + DeleteSubscriber = Ipc.ModDeleted.Subscriber( pi, s => + { + _lastDeletedModTime = DateTimeOffset.UtcNow; + _lastDeletedMod = s; + } ); + AddSubscriber = Ipc.ModAdded.Subscriber( pi, s => + { + _lastAddedModTime = DateTimeOffset.UtcNow; + _lastAddedMod = s; + } ); + MoveSubscriber = Ipc.ModMoved.Subscriber( pi, ( s1, s2 ) => + { + _lastMovedModTime = DateTimeOffset.UtcNow; + _lastMovedModFrom = s1; + _lastMovedModTo = s2; + } ); + } public void Draw() { @@ -866,6 +909,23 @@ public class IpcTester : IDisposable ImGui.SameLine(); ImGui.TextUnformatted( _lastSetPathEc.ToString() ); + DrawIntro( Ipc.ModDeleted.Label, "Last Mod Deleted" ); + if( _lastDeletedModTime > DateTimeOffset.UnixEpoch ) + { + ImGui.TextUnformatted( $"{_lastDeletedMod} at {_lastDeletedModTime}" ); + } + + DrawIntro( Ipc.ModAdded.Label, "Last Mod Added" ); + if( _lastAddedModTime > DateTimeOffset.UnixEpoch ) + { + ImGui.TextUnformatted( $"{_lastAddedMod} at {_lastAddedModTime}" ); + } + + DrawIntro( Ipc.ModMoved.Label, "Last Mod Moved" ); + if( _lastMovedModTime > DateTimeOffset.UnixEpoch ) + { + ImGui.TextUnformatted( $"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}" ); + } DrawModsPopup(); } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7266c884..aa6bf84a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -93,12 +93,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; + Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; } public unsafe void Dispose() { Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; + Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; _penumbra = null; _lumina = null; foreach( var collection in Penumbra.CollectionManager ) @@ -405,6 +407,26 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } + public event Action< string >? ModDeleted; + public event Action< string >? ModAdded; + public event Action< string, string >? ModMoved; + + private void ModPathChangeSubscriber( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ) + { + switch( type ) + { + case ModPathChangeType.Deleted when oldDirectory != null: + ModDeleted?.Invoke( oldDirectory.Name ); + break; + case ModPathChangeType.Added when newDirectory != null: + ModAdded?.Invoke( newDirectory.Name ); + break; + case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: + ModMoved?.Invoke( oldDirectory.Name, newDirectory.Name ); + break; + } + } public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ) { diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 518d3638..dd042b99 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -75,6 +75,9 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, string, PenumbraApiEc > DeleteMod; internal readonly FuncProvider< string, string, (PenumbraApiEc, string, bool) > GetModPath; internal readonly FuncProvider< string, string, string, PenumbraApiEc > SetModPath; + internal readonly EventProvider< string > ModDeleted; + internal readonly EventProvider< string > ModAdded; + internal readonly EventProvider< string, string > ModMoved; // ModSettings internal readonly FuncProvider< string, string, IDictionary< string, (IList< string >, GroupType) >? > GetAvailableModSettings; @@ -167,6 +170,9 @@ public class PenumbraIpcProviders : IDisposable DeleteMod = Ipc.DeleteMod.Provider( pi, Api.DeleteMod ); GetModPath = Ipc.GetModPath.Provider( pi, Api.GetModPath ); SetModPath = Ipc.SetModPath.Provider( pi, Api.SetModPath ); + ModDeleted = Ipc.ModDeleted.Provider( pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent ); + ModAdded = Ipc.ModAdded.Provider( pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent ); + ModMoved = Ipc.ModMoved.Provider( pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent ); // ModSettings GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider( pi, Api.GetAvailableModSettings ); @@ -259,6 +265,9 @@ public class PenumbraIpcProviders : IDisposable DeleteMod.Dispose(); GetModPath.Dispose(); SetModPath.Dispose(); + ModDeleted.Dispose(); + ModAdded.Dispose(); + ModMoved.Dispose(); // ModSettings GetAvailableModSettings.Dispose(); @@ -321,4 +330,13 @@ public class PenumbraIpcProviders : IDisposable private void ModSettingChangedEvent( ModSettingChange type, string collection, string mod, bool inherited ) => ModSettingChanged.Invoke( type, collection, mod, inherited ); + + private void ModDeletedEvent( string name ) + => ModDeleted.Invoke( name ); + + private void ModAddedEvent( string name ) + => ModAdded.Invoke( name ); + + private void ModMovedEvent( string from, string to ) + => ModMoved.Invoke( from, to ); } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 9902ed57..8f092ed4 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -58,7 +58,7 @@ public partial class Mod return; } - MoveDataFile( oldDirectory, BasePath ); + MoveDataFile( oldDirectory, dir ); new ModBackup( mod ).Move( null, dir.Name ); dir.Refresh(); @@ -69,7 +69,7 @@ public partial class Mod return; } - ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath ); + ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, dir ); if( metaChange != ModDataChangeType.None ) { ModDataChanged?.Invoke( metaChange, mod, oldName ); From f1b495dff440cd8c0452d76634f6b772fc5142af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Dec 2022 17:18:23 +0100 Subject: [PATCH 0603/2451] Add some improvements to game path stuff, move the race inheritance tree to game data, etc. --- Penumbra.GameData/Data/GamePaths.cs | 276 ++++++++++++ Penumbra.GameData/Enums/BodySlot.cs | 16 +- Penumbra.GameData/Enums/Race.cs | 420 ++++++++++-------- Penumbra.GameData/Structs/ImcEntry.cs | 50 +++ Penumbra.String | 2 +- .../Interop/Resolver/PathResolver.Meta.cs | 84 +--- Penumbra/Meta/Files/ImcFile.cs | 52 +-- .../Meta/Manipulations/ImcManipulation.cs | 27 +- 8 files changed, 607 insertions(+), 320 deletions(-) create mode 100644 Penumbra.GameData/Data/GamePaths.cs create mode 100644 Penumbra.GameData/Structs/ImcEntry.cs diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs new file mode 100644 index 00000000..21f2d66a --- /dev/null +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -0,0 +1,276 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +public static partial class GamePaths +{ + public static partial class Monster + { + public static partial class Imc + { + // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc")] + // public static partial Regex Regex(); + + public static string Path(SetId monsterId, SetId bodyId) + => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; + } + + public static partial class Mdl + { + // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl")] + // public static partial Regex Regex(); + + public static string Path(SetId monsterId, SetId bodyId) + => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/m{monsterId.Value:D4}b{bodyId.Value:D4}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(SetId monsterId, SetId bodyId, byte variant, string suffix) + => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_m{monsterId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex")] + // public static partial Regex Regex(); + + public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') + => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + } + + public static partial class Weapon + { + public static partial class Imc + { + // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc")] + // public static partial Regex Regex(); + + public static string Path(SetId weaponId, SetId bodyId) + => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; + } + + public static partial class Mdl + { + // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl")] + // public static partial Regex Regex(); + + public static string Path(SetId weaponId, SetId bodyId) + => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/w{weaponId.Value:D4}b{bodyId.Value:D4}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(SetId weaponId, SetId bodyId, byte variant, string suffix) + => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_w{weaponId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex")] + // public static partial Regex Regex(); + + public static string Path(SetId weaponId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') + => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_w{weaponId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + } + + public static partial class DemiHuman + { + public static partial class Imc + { + // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc")] + // public static partial Regex Regex(); + + public static string Path(SetId demiId, SetId equipId) + => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/b{equipId.Value:D4}.imc"; + } + + public static partial class Mdl + { + // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl")] + // public static partial Regex Regex(); + + public static string Path(SetId demiId, SetId equipId, EquipSlot slot) + => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/model/d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, string suffix) + => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/material/v{variant:D4}/mt_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + // public static partial Regex Regex(); + + public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') + => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + } + + public static partial class Equipment + { + public static partial class Imc + { + // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc")] + // public static partial Regex Regex(); + + public static string Path(SetId equipId) + => $"chara/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; + } + + public static partial class Mdl + { + // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl")] + // public static partial Regex Regex(); + + public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot) + => $"chara/equipment/e{equipId.Value:D4}/model/c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) + => $"{FolderPath(equipId, variant)}/mt_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; + + public static string FolderPath(SetId equipId, byte variant) + => $"chara/equipment/e{equipId.Value:D4}/material/v{variant:D4}"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + // public static partial Regex Regex(); + + public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') + => $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + } + + public static partial class Accessory + { + public static partial class Imc + { + // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc")] + // public static partial Regex Regex(); + + public static string Path(SetId accessoryId) + => $"chara/accessory/a{accessoryId.Value:D4}/a{accessoryId.Value:D4}.imc"; + } + + public static partial class Mdl + { + // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl")] + // public static partial Regex Regex(); + + public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot) + => $"chara/accessory/a{accessoryId.Value:D4}/model/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) + => $"{FolderPath(accessoryId, variant)}/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; + + public static string FolderPath(SetId accessoryId, byte variant) + => $"chara/accessory/a{accessoryId.Value:D4}/material/v{variant:D4}"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + // public static partial Regex Regex(); + + public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') + => $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + } + + public static partial class Character + { + public static partial class Mdl + { + // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl")] + // public static partial Regex Regex(); + + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type) + => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; + } + + public static partial class Mtrl + { + // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] + // public static partial Regex Regex(); + + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string suffix, + CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue) + => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material/" + + (variant != byte.MaxValue ? $"v{variant:D4}/" : string.Empty) + + $"mt_c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}_{suffix}.mtrl"; + } + + public static partial class Tex + { + // [GeneratedRegex(@"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")] + // public static partial Regex Regex(); + + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false, + CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0') + => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/" + + (minus ? "--" : string.Empty) + + (variant != byte.MaxValue ? $"v{variant:D2}_" : string.Empty) + + $"c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + + + // [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")] + // public static partial Regex CatchlightRegex(); + + // [GeneratedRegex(@"chara/common/texture/skin(?'skin'.*)\.tex")] + // public static partial Regex SkinRegex(); + + // [GeneratedRegex(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex")] + // public static partial Regex DecalRegex(); + + // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")] + // public static partial Regex FolderRegex(); + } + } + + public static partial class Icon + { + // [GeneratedRegex(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex")] + // public static partial Regex Regex(); + } + + public static partial class Map + { + // [GeneratedRegex(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex")] + // public static partial Regex Regex(); + } + + public static partial class Font + { + // [GeneratedRegex(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt")] + // public static partial Regex Regex(); + } +} diff --git a/Penumbra.GameData/Enums/BodySlot.cs b/Penumbra.GameData/Enums/BodySlot.cs index 31e77417..8eb6513b 100644 --- a/Penumbra.GameData/Enums/BodySlot.cs +++ b/Penumbra.GameData/Enums/BodySlot.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel; @@ -16,8 +17,7 @@ public enum BodySlot : byte public static class BodySlotEnumExtension { public static string ToSuffix( this BodySlot value ) - { - return value switch + => value switch { BodySlot.Zear => "zear", BodySlot.Face => "face", @@ -26,7 +26,17 @@ public static class BodySlotEnumExtension BodySlot.Tail => "tail", _ => throw new InvalidEnumArgumentException(), }; - } + + public static char ToAbbreviation(this BodySlot value) + => value switch + { + BodySlot.Hair => 'h', + BodySlot.Face => 'f', + BodySlot.Tail => 't', + BodySlot.Body => 'b', + BodySlot.Zear => 'z', + _ => throw new InvalidEnumArgumentException(), + }; } public static partial class Names diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index f7b5ce7b..1cf4f1ff 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.GameData.Enums; @@ -107,7 +109,7 @@ public enum GenderRace : ushort public static class RaceEnumExtensions { - public static Race ToRace( this ModelRace race ) + public static Race ToRace(this ModelRace race) { return race switch { @@ -121,11 +123,11 @@ public static class RaceEnumExtensions ModelRace.AuRa => Race.AuRa, ModelRace.Hrothgar => Race.Hrothgar, ModelRace.Viera => Race.Viera, - _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), + _ => throw new ArgumentOutOfRangeException(nameof(race), race, null), }; } - public static Race ToRace( this SubRace subRace ) + public static Race ToRace(this SubRace subRace) { return subRace switch { @@ -146,11 +148,11 @@ public static class RaceEnumExtensions SubRace.Lost => Race.Hrothgar, SubRace.Rava => Race.Viera, SubRace.Veena => Race.Viera, - _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), + _ => throw new ArgumentOutOfRangeException(nameof(subRace), subRace, null), }; } - public static string ToName( this ModelRace modelRace ) + public static string ToName(this ModelRace modelRace) { return modelRace switch { @@ -167,7 +169,7 @@ public static class RaceEnumExtensions }; } - public static string ToName( this Race race ) + public static string ToName(this Race race) { return race switch { @@ -183,7 +185,7 @@ public static class RaceEnumExtensions }; } - public static string ToName( this Gender gender ) + public static string ToName(this Gender gender) { return gender switch { @@ -195,7 +197,7 @@ public static class RaceEnumExtensions }; } - public static string ToName( this SubRace subRace ) + public static string ToName(this SubRace subRace) { return subRace switch { @@ -219,230 +221,280 @@ public static class RaceEnumExtensions }; } - public static bool FitsRace( this SubRace subRace, Race race ) + public static bool FitsRace(this SubRace subRace, Race race) => subRace.ToRace() == race; - public static byte ToByte( this Gender gender, ModelRace modelRace ) - => ( byte )( ( int )gender | ( ( int )modelRace << 3 ) ); + public static byte ToByte(this Gender gender, ModelRace modelRace) + => (byte)((int)gender | ((int)modelRace << 3)); - public static byte ToByte( this ModelRace modelRace, Gender gender ) - => gender.ToByte( modelRace ); + public static byte ToByte(this ModelRace modelRace, Gender gender) + => gender.ToByte(modelRace); - public static byte ToByte( this GenderRace value ) + public static byte ToByte(this GenderRace value) { var (gender, race) = value.Split(); - return gender.ToByte( race ); + return gender.ToByte(race); } - public static (Gender, ModelRace) Split( this GenderRace value ) + public static (Gender, ModelRace) Split(this GenderRace value) { return value switch { - GenderRace.Unknown => ( Gender.Unknown, ModelRace.Unknown ), - GenderRace.MidlanderMale => ( Gender.Male, ModelRace.Midlander ), - GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Midlander ), - GenderRace.MidlanderFemale => ( Gender.Female, ModelRace.Midlander ), - GenderRace.MidlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Midlander ), - GenderRace.HighlanderMale => ( Gender.Male, ModelRace.Highlander ), - GenderRace.HighlanderMaleNpc => ( Gender.MaleNpc, ModelRace.Highlander ), - GenderRace.HighlanderFemale => ( Gender.Female, ModelRace.Highlander ), - GenderRace.HighlanderFemaleNpc => ( Gender.FemaleNpc, ModelRace.Highlander ), - GenderRace.ElezenMale => ( Gender.Male, ModelRace.Elezen ), - GenderRace.ElezenMaleNpc => ( Gender.MaleNpc, ModelRace.Elezen ), - GenderRace.ElezenFemale => ( Gender.Female, ModelRace.Elezen ), - GenderRace.ElezenFemaleNpc => ( Gender.FemaleNpc, ModelRace.Elezen ), - GenderRace.LalafellMale => ( Gender.Male, ModelRace.Lalafell ), - GenderRace.LalafellMaleNpc => ( Gender.MaleNpc, ModelRace.Lalafell ), - GenderRace.LalafellFemale => ( Gender.Female, ModelRace.Lalafell ), - GenderRace.LalafellFemaleNpc => ( Gender.FemaleNpc, ModelRace.Lalafell ), - GenderRace.MiqoteMale => ( Gender.Male, ModelRace.Miqote ), - GenderRace.MiqoteMaleNpc => ( Gender.MaleNpc, ModelRace.Miqote ), - GenderRace.MiqoteFemale => ( Gender.Female, ModelRace.Miqote ), - GenderRace.MiqoteFemaleNpc => ( Gender.FemaleNpc, ModelRace.Miqote ), - GenderRace.RoegadynMale => ( Gender.Male, ModelRace.Roegadyn ), - GenderRace.RoegadynMaleNpc => ( Gender.MaleNpc, ModelRace.Roegadyn ), - GenderRace.RoegadynFemale => ( Gender.Female, ModelRace.Roegadyn ), - GenderRace.RoegadynFemaleNpc => ( Gender.FemaleNpc, ModelRace.Roegadyn ), - GenderRace.AuRaMale => ( Gender.Male, ModelRace.AuRa ), - GenderRace.AuRaMaleNpc => ( Gender.MaleNpc, ModelRace.AuRa ), - GenderRace.AuRaFemale => ( Gender.Female, ModelRace.AuRa ), - GenderRace.AuRaFemaleNpc => ( Gender.FemaleNpc, ModelRace.AuRa ), - GenderRace.HrothgarMale => ( Gender.Male, ModelRace.Hrothgar ), - GenderRace.HrothgarMaleNpc => ( Gender.MaleNpc, ModelRace.Hrothgar ), - GenderRace.HrothgarFemale => ( Gender.Female, ModelRace.Hrothgar ), - GenderRace.HrothgarFemaleNpc => ( Gender.FemaleNpc, ModelRace.Hrothgar ), - GenderRace.VieraMale => ( Gender.Male, ModelRace.Viera ), - GenderRace.VieraMaleNpc => ( Gender.Male, ModelRace.Viera ), - GenderRace.VieraFemale => ( Gender.Female, ModelRace.Viera ), - GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, ModelRace.Viera ), - GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, ModelRace.Unknown ), - GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, ModelRace.Unknown ), - _ => throw new InvalidEnumArgumentException(), + Unknown => (Gender.Unknown, ModelRace.Unknown), + MidlanderMale => (Gender.Male, ModelRace.Midlander), + MidlanderMaleNpc => (Gender.MaleNpc, ModelRace.Midlander), + MidlanderFemale => (Gender.Female, ModelRace.Midlander), + MidlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Midlander), + HighlanderMale => (Gender.Male, ModelRace.Highlander), + HighlanderMaleNpc => (Gender.MaleNpc, ModelRace.Highlander), + HighlanderFemale => (Gender.Female, ModelRace.Highlander), + HighlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Highlander), + ElezenMale => (Gender.Male, ModelRace.Elezen), + ElezenMaleNpc => (Gender.MaleNpc, ModelRace.Elezen), + ElezenFemale => (Gender.Female, ModelRace.Elezen), + ElezenFemaleNpc => (Gender.FemaleNpc, ModelRace.Elezen), + LalafellMale => (Gender.Male, ModelRace.Lalafell), + LalafellMaleNpc => (Gender.MaleNpc, ModelRace.Lalafell), + LalafellFemale => (Gender.Female, ModelRace.Lalafell), + LalafellFemaleNpc => (Gender.FemaleNpc, ModelRace.Lalafell), + MiqoteMale => (Gender.Male, ModelRace.Miqote), + MiqoteMaleNpc => (Gender.MaleNpc, ModelRace.Miqote), + MiqoteFemale => (Gender.Female, ModelRace.Miqote), + MiqoteFemaleNpc => (Gender.FemaleNpc, ModelRace.Miqote), + RoegadynMale => (Gender.Male, ModelRace.Roegadyn), + RoegadynMaleNpc => (Gender.MaleNpc, ModelRace.Roegadyn), + RoegadynFemale => (Gender.Female, ModelRace.Roegadyn), + RoegadynFemaleNpc => (Gender.FemaleNpc, ModelRace.Roegadyn), + AuRaMale => (Gender.Male, ModelRace.AuRa), + AuRaMaleNpc => (Gender.MaleNpc, ModelRace.AuRa), + AuRaFemale => (Gender.Female, ModelRace.AuRa), + AuRaFemaleNpc => (Gender.FemaleNpc, ModelRace.AuRa), + HrothgarMale => (Gender.Male, ModelRace.Hrothgar), + HrothgarMaleNpc => (Gender.MaleNpc, ModelRace.Hrothgar), + HrothgarFemale => (Gender.Female, ModelRace.Hrothgar), + HrothgarFemaleNpc => (Gender.FemaleNpc, ModelRace.Hrothgar), + VieraMale => (Gender.Male, ModelRace.Viera), + VieraMaleNpc => (Gender.Male, ModelRace.Viera), + VieraFemale => (Gender.Female, ModelRace.Viera), + VieraFemaleNpc => (Gender.FemaleNpc, ModelRace.Viera), + UnknownMaleNpc => (Gender.MaleNpc, ModelRace.Unknown), + UnknownFemaleNpc => (Gender.FemaleNpc, ModelRace.Unknown), + _ => throw new InvalidEnumArgumentException(), }; } - public static bool IsValid( this GenderRace value ) - => value != GenderRace.Unknown && Enum.IsDefined( typeof( GenderRace ), value ); + public static bool IsValid(this GenderRace value) + => value != Unknown && Enum.IsDefined(typeof(GenderRace), value); - public static string ToRaceCode( this GenderRace value ) + public static string ToRaceCode(this GenderRace value) { return value switch { - GenderRace.MidlanderMale => "0101", - GenderRace.MidlanderMaleNpc => "0104", - GenderRace.MidlanderFemale => "0201", - GenderRace.MidlanderFemaleNpc => "0204", - GenderRace.HighlanderMale => "0301", - GenderRace.HighlanderMaleNpc => "0304", - GenderRace.HighlanderFemale => "0401", - GenderRace.HighlanderFemaleNpc => "0404", - GenderRace.ElezenMale => "0501", - GenderRace.ElezenMaleNpc => "0504", - GenderRace.ElezenFemale => "0601", - GenderRace.ElezenFemaleNpc => "0604", - GenderRace.MiqoteMale => "0701", - GenderRace.MiqoteMaleNpc => "0704", - GenderRace.MiqoteFemale => "0801", - GenderRace.MiqoteFemaleNpc => "0804", - GenderRace.RoegadynMale => "0901", - GenderRace.RoegadynMaleNpc => "0904", - GenderRace.RoegadynFemale => "1001", - GenderRace.RoegadynFemaleNpc => "1004", - GenderRace.LalafellMale => "1101", - GenderRace.LalafellMaleNpc => "1104", - GenderRace.LalafellFemale => "1201", - GenderRace.LalafellFemaleNpc => "1204", - GenderRace.AuRaMale => "1301", - GenderRace.AuRaMaleNpc => "1304", - GenderRace.AuRaFemale => "1401", - GenderRace.AuRaFemaleNpc => "1404", - GenderRace.HrothgarMale => "1501", - GenderRace.HrothgarMaleNpc => "1504", - GenderRace.HrothgarFemale => "1601", - GenderRace.HrothgarFemaleNpc => "1604", - GenderRace.VieraMale => "1701", - GenderRace.VieraMaleNpc => "1704", - GenderRace.VieraFemale => "1801", - GenderRace.VieraFemaleNpc => "1804", - GenderRace.UnknownMaleNpc => "9104", - GenderRace.UnknownFemaleNpc => "9204", - _ => throw new InvalidEnumArgumentException(), + MidlanderMale => "0101", + MidlanderMaleNpc => "0104", + MidlanderFemale => "0201", + MidlanderFemaleNpc => "0204", + HighlanderMale => "0301", + HighlanderMaleNpc => "0304", + HighlanderFemale => "0401", + HighlanderFemaleNpc => "0404", + ElezenMale => "0501", + ElezenMaleNpc => "0504", + ElezenFemale => "0601", + ElezenFemaleNpc => "0604", + MiqoteMale => "0701", + MiqoteMaleNpc => "0704", + MiqoteFemale => "0801", + MiqoteFemaleNpc => "0804", + RoegadynMale => "0901", + RoegadynMaleNpc => "0904", + RoegadynFemale => "1001", + RoegadynFemaleNpc => "1004", + LalafellMale => "1101", + LalafellMaleNpc => "1104", + LalafellFemale => "1201", + LalafellFemaleNpc => "1204", + AuRaMale => "1301", + AuRaMaleNpc => "1304", + AuRaFemale => "1401", + AuRaFemaleNpc => "1404", + HrothgarMale => "1501", + HrothgarMaleNpc => "1504", + HrothgarFemale => "1601", + HrothgarFemaleNpc => "1604", + VieraMale => "1701", + VieraMaleNpc => "1704", + VieraFemale => "1801", + VieraFemaleNpc => "1804", + UnknownMaleNpc => "9104", + UnknownFemaleNpc => "9204", + _ => throw new InvalidEnumArgumentException(), }; } + + public static GenderRace[] Dependencies(this GenderRace raceCode) + => DependencyList.TryGetValue(raceCode, out var dep) ? dep : Array.Empty(); + + public static IEnumerable OnlyDependencies(this GenderRace raceCode) + => DependencyList.TryGetValue(raceCode, out var dep) ? dep.Skip(1) : Array.Empty(); + + private static readonly Dictionary DependencyList = new() + { + // @formatter:off + [MidlanderMale] = new[]{ MidlanderMale }, + [HighlanderMale] = new[]{ HighlanderMale, MidlanderMale }, + [ElezenMale] = new[]{ ElezenMale, MidlanderMale }, + [MiqoteMale] = new[]{ MiqoteMale, MidlanderMale }, + [RoegadynMale] = new[]{ RoegadynMale, MidlanderMale }, + [LalafellMale] = new[]{ LalafellMale, MidlanderMale }, + [AuRaMale] = new[]{ AuRaMale, MidlanderMale }, + [HrothgarMale] = new[]{ HrothgarMale, RoegadynMale, MidlanderMale }, + [VieraMale] = new[]{ VieraMale, MidlanderMale }, + [MidlanderFemale] = new[]{ MidlanderFemale, MidlanderMale }, + [HighlanderFemale] = new[]{ HighlanderFemale, MidlanderFemale, MidlanderMale }, + [ElezenFemale] = new[]{ ElezenFemale, MidlanderFemale, MidlanderMale }, + [MiqoteFemale] = new[]{ MiqoteFemale, MidlanderFemale, MidlanderMale }, + [RoegadynFemale] = new[]{ RoegadynFemale, MidlanderFemale, MidlanderMale }, + [LalafellFemale] = new[]{ LalafellFemale, LalafellMale, MidlanderMale }, + [AuRaFemale] = new[]{ AuRaFemale, MidlanderFemale, MidlanderMale }, + [HrothgarFemale] = new[]{ HrothgarFemale, RoegadynFemale, MidlanderFemale, MidlanderMale }, + [VieraFemale] = new[]{ VieraFemale, MidlanderFemale, MidlanderMale }, + [MidlanderMaleNpc] = new[]{ MidlanderMaleNpc, MidlanderMale }, + [HighlanderMaleNpc] = new[]{ HighlanderMaleNpc, HighlanderMale, MidlanderMaleNpc, MidlanderMale }, + [ElezenMaleNpc] = new[]{ ElezenMaleNpc, ElezenMale, MidlanderMaleNpc, MidlanderMale }, + [MiqoteMaleNpc] = new[]{ MiqoteMaleNpc, MiqoteMale, MidlanderMaleNpc, MidlanderMale }, + [RoegadynMaleNpc] = new[]{ RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale }, + [LalafellMaleNpc] = new[]{ LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale }, + [AuRaMaleNpc] = new[]{ AuRaMaleNpc, AuRaMale, MidlanderMaleNpc, MidlanderMale }, + [HrothgarMaleNpc] = new[]{ HrothgarMaleNpc, HrothgarMale, RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale }, + [VieraMaleNpc] = new[]{ VieraMaleNpc, VieraMale, MidlanderMaleNpc, MidlanderMale }, + [MidlanderFemaleNpc] = new[]{ MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [HighlanderFemaleNpc] = new[]{ HighlanderFemaleNpc, HighlanderFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [ElezenFemaleNpc] = new[]{ ElezenFemaleNpc, ElezenFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [MiqoteFemaleNpc] = new[]{ MiqoteFemaleNpc, MiqoteFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [RoegadynFemaleNpc] = new[]{ RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [LalafellFemaleNpc] = new[]{ LalafellFemaleNpc, LalafellFemale, LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale }, + [AuRaFemaleNpc] = new[]{ AuRaFemaleNpc, AuRaFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [HrothgarFemaleNpc] = new[]{ HrothgarFemaleNpc, HrothgarFemale, RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [VieraFemaleNpc] = new[]{ VieraFemaleNpc, VieraFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + [UnknownMaleNpc] = new[]{ UnknownMaleNpc, MidlanderMaleNpc, MidlanderMale }, + [UnknownFemaleNpc] = new[]{ UnknownFemaleNpc, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, + // @formatter:on + }; } public static partial class Names { - public static GenderRace GenderRaceFromCode( string code ) + public static GenderRace GenderRaceFromCode(string code) { return code switch { - "0101" => GenderRace.MidlanderMale, - "0104" => GenderRace.MidlanderMaleNpc, - "0201" => GenderRace.MidlanderFemale, - "0204" => GenderRace.MidlanderFemaleNpc, - "0301" => GenderRace.HighlanderMale, - "0304" => GenderRace.HighlanderMaleNpc, - "0401" => GenderRace.HighlanderFemale, - "0404" => GenderRace.HighlanderFemaleNpc, - "0501" => GenderRace.ElezenMale, - "0504" => GenderRace.ElezenMaleNpc, - "0601" => GenderRace.ElezenFemale, - "0604" => GenderRace.ElezenFemaleNpc, - "0701" => GenderRace.MiqoteMale, - "0704" => GenderRace.MiqoteMaleNpc, - "0801" => GenderRace.MiqoteFemale, - "0804" => GenderRace.MiqoteFemaleNpc, - "0901" => GenderRace.RoegadynMale, - "0904" => GenderRace.RoegadynMaleNpc, - "1001" => GenderRace.RoegadynFemale, - "1004" => GenderRace.RoegadynFemaleNpc, - "1101" => GenderRace.LalafellMale, - "1104" => GenderRace.LalafellMaleNpc, - "1201" => GenderRace.LalafellFemale, - "1204" => GenderRace.LalafellFemaleNpc, - "1301" => GenderRace.AuRaMale, - "1304" => GenderRace.AuRaMaleNpc, - "1401" => GenderRace.AuRaFemale, - "1404" => GenderRace.AuRaFemaleNpc, - "1501" => GenderRace.HrothgarMale, - "1504" => GenderRace.HrothgarMaleNpc, - "1601" => GenderRace.HrothgarFemale, - "1604" => GenderRace.HrothgarFemaleNpc, - "1701" => GenderRace.VieraMale, - "1704" => GenderRace.VieraMaleNpc, - "1801" => GenderRace.VieraFemale, - "1804" => GenderRace.VieraFemaleNpc, - "9104" => GenderRace.UnknownMaleNpc, - "9204" => GenderRace.UnknownFemaleNpc, + "0101" => MidlanderMale, + "0104" => MidlanderMaleNpc, + "0201" => MidlanderFemale, + "0204" => MidlanderFemaleNpc, + "0301" => HighlanderMale, + "0304" => HighlanderMaleNpc, + "0401" => HighlanderFemale, + "0404" => HighlanderFemaleNpc, + "0501" => ElezenMale, + "0504" => ElezenMaleNpc, + "0601" => ElezenFemale, + "0604" => ElezenFemaleNpc, + "0701" => MiqoteMale, + "0704" => MiqoteMaleNpc, + "0801" => MiqoteFemale, + "0804" => MiqoteFemaleNpc, + "0901" => RoegadynMale, + "0904" => RoegadynMaleNpc, + "1001" => RoegadynFemale, + "1004" => RoegadynFemaleNpc, + "1101" => LalafellMale, + "1104" => LalafellMaleNpc, + "1201" => LalafellFemale, + "1204" => LalafellFemaleNpc, + "1301" => AuRaMale, + "1304" => AuRaMaleNpc, + "1401" => AuRaFemale, + "1404" => AuRaFemaleNpc, + "1501" => HrothgarMale, + "1504" => HrothgarMaleNpc, + "1601" => HrothgarFemale, + "1604" => HrothgarFemaleNpc, + "1701" => VieraMale, + "1704" => VieraMaleNpc, + "1801" => VieraFemale, + "1804" => VieraFemaleNpc, + "9104" => UnknownMaleNpc, + "9204" => UnknownFemaleNpc, _ => throw new KeyNotFoundException(), }; } - public static GenderRace GenderRaceFromByte( byte value ) + public static GenderRace GenderRaceFromByte(byte value) { - var gender = ( Gender )( value & 0b111 ); - var race = ( ModelRace )( value >> 3 ); - return CombinedRace( gender, race ); + var gender = (Gender)(value & 0b111); + var race = (ModelRace)(value >> 3); + return CombinedRace(gender, race); } - public static GenderRace CombinedRace( Gender gender, ModelRace modelRace ) + public static GenderRace CombinedRace(Gender gender, ModelRace modelRace) { return gender switch { Gender.Male => modelRace switch { - ModelRace.Midlander => GenderRace.MidlanderMale, - ModelRace.Highlander => GenderRace.HighlanderMale, - ModelRace.Elezen => GenderRace.ElezenMale, - ModelRace.Lalafell => GenderRace.LalafellMale, - ModelRace.Miqote => GenderRace.MiqoteMale, - ModelRace.Roegadyn => GenderRace.RoegadynMale, - ModelRace.AuRa => GenderRace.AuRaMale, - ModelRace.Hrothgar => GenderRace.HrothgarMale, - ModelRace.Viera => GenderRace.VieraMale, - _ => GenderRace.Unknown, + ModelRace.Midlander => MidlanderMale, + ModelRace.Highlander => HighlanderMale, + ModelRace.Elezen => ElezenMale, + ModelRace.Lalafell => LalafellMale, + ModelRace.Miqote => MiqoteMale, + ModelRace.Roegadyn => RoegadynMale, + ModelRace.AuRa => AuRaMale, + ModelRace.Hrothgar => HrothgarMale, + ModelRace.Viera => VieraMale, + _ => Unknown, }, Gender.MaleNpc => modelRace switch { - ModelRace.Midlander => GenderRace.MidlanderMaleNpc, - ModelRace.Highlander => GenderRace.HighlanderMaleNpc, - ModelRace.Elezen => GenderRace.ElezenMaleNpc, - ModelRace.Lalafell => GenderRace.LalafellMaleNpc, - ModelRace.Miqote => GenderRace.MiqoteMaleNpc, - ModelRace.Roegadyn => GenderRace.RoegadynMaleNpc, - ModelRace.AuRa => GenderRace.AuRaMaleNpc, - ModelRace.Hrothgar => GenderRace.HrothgarMaleNpc, - ModelRace.Viera => GenderRace.VieraMaleNpc, - _ => GenderRace.Unknown, + ModelRace.Midlander => MidlanderMaleNpc, + ModelRace.Highlander => HighlanderMaleNpc, + ModelRace.Elezen => ElezenMaleNpc, + ModelRace.Lalafell => LalafellMaleNpc, + ModelRace.Miqote => MiqoteMaleNpc, + ModelRace.Roegadyn => RoegadynMaleNpc, + ModelRace.AuRa => AuRaMaleNpc, + ModelRace.Hrothgar => HrothgarMaleNpc, + ModelRace.Viera => VieraMaleNpc, + _ => Unknown, }, Gender.Female => modelRace switch { - ModelRace.Midlander => GenderRace.MidlanderFemale, - ModelRace.Highlander => GenderRace.HighlanderFemale, - ModelRace.Elezen => GenderRace.ElezenFemale, - ModelRace.Lalafell => GenderRace.LalafellFemale, - ModelRace.Miqote => GenderRace.MiqoteFemale, - ModelRace.Roegadyn => GenderRace.RoegadynFemale, - ModelRace.AuRa => GenderRace.AuRaFemale, - ModelRace.Hrothgar => GenderRace.HrothgarFemale, - ModelRace.Viera => GenderRace.VieraFemale, - _ => GenderRace.Unknown, + ModelRace.Midlander => MidlanderFemale, + ModelRace.Highlander => HighlanderFemale, + ModelRace.Elezen => ElezenFemale, + ModelRace.Lalafell => LalafellFemale, + ModelRace.Miqote => MiqoteFemale, + ModelRace.Roegadyn => RoegadynFemale, + ModelRace.AuRa => AuRaFemale, + ModelRace.Hrothgar => HrothgarFemale, + ModelRace.Viera => VieraFemale, + _ => Unknown, }, Gender.FemaleNpc => modelRace switch { - ModelRace.Midlander => GenderRace.MidlanderFemaleNpc, - ModelRace.Highlander => GenderRace.HighlanderFemaleNpc, - ModelRace.Elezen => GenderRace.ElezenFemaleNpc, - ModelRace.Lalafell => GenderRace.LalafellFemaleNpc, - ModelRace.Miqote => GenderRace.MiqoteFemaleNpc, - ModelRace.Roegadyn => GenderRace.RoegadynFemaleNpc, - ModelRace.AuRa => GenderRace.AuRaFemaleNpc, - ModelRace.Hrothgar => GenderRace.HrothgarFemaleNpc, - ModelRace.Viera => GenderRace.VieraFemaleNpc, - _ => GenderRace.Unknown, + ModelRace.Midlander => MidlanderFemaleNpc, + ModelRace.Highlander => HighlanderFemaleNpc, + ModelRace.Elezen => ElezenFemaleNpc, + ModelRace.Lalafell => LalafellFemaleNpc, + ModelRace.Miqote => MiqoteFemaleNpc, + ModelRace.Roegadyn => RoegadynFemaleNpc, + ModelRace.AuRa => AuRaFemaleNpc, + ModelRace.Hrothgar => HrothgarFemaleNpc, + ModelRace.Viera => VieraFemaleNpc, + _ => Unknown, }, - _ => GenderRace.Unknown, + _ => Unknown, }; } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Structs/ImcEntry.cs b/Penumbra.GameData/Structs/ImcEntry.cs new file mode 100644 index 00000000..9cabe54f --- /dev/null +++ b/Penumbra.GameData/Structs/ImcEntry.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; + +namespace Penumbra.GameData.Structs; + +public readonly struct ImcEntry : IEquatable +{ + public byte MaterialId { get; init; } + public byte DecalId { get; init; } + public readonly ushort AttributeAndSound; + public byte VfxId { get; init; } + public byte MaterialAnimationId { get; init; } + + public ushort AttributeMask + { + get => (ushort)(AttributeAndSound & 0x3FF); + init => AttributeAndSound = (ushort)((AttributeAndSound & ~0x3FF) | (value & 0x3FF)); + } + + public byte SoundId + { + get => (byte)(AttributeAndSound >> 10); + init => AttributeAndSound = (ushort)(AttributeMask | (value << 10)); + } + + public bool Equals(ImcEntry other) + => MaterialId == other.MaterialId + && DecalId == other.DecalId + && AttributeAndSound == other.AttributeAndSound + && VfxId == other.VfxId + && MaterialAnimationId == other.MaterialAnimationId; + + public override bool Equals(object? obj) + => obj is ImcEntry other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId); + + [JsonConstructor] + public ImcEntry(byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId) + { + MaterialId = materialId; + DecalId = decalId; + AttributeAndSound = 0; + VfxId = vfxId; + MaterialAnimationId = materialAnimationId; + AttributeMask = attributeMask; + SoundId = soundId; + } +} diff --git a/Penumbra.String b/Penumbra.String index 81539a96..944e712d 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 81539a968f6bfbb78c34ae1094cce88ae4c9ac88 +Subproject commit 944e712d404c22ef4ce945ff35cf08af3f67ca12 diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index a15db301..1faa3b76 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -78,9 +78,9 @@ public unsafe partial class PathResolver private void OnModelLoadCompleteDetour( IntPtr drawObject ) { - var collection = GetResolveData( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); + var collection = GetResolveData( drawObject ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } @@ -98,9 +98,9 @@ public unsafe partial class PathResolver return; } - var collection = GetResolveData( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); + var collection = GetResolveData( drawObject ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _updateModelsHook.Original.Invoke( drawObject ); } @@ -187,75 +187,27 @@ public unsafe partial class PathResolver _inChangeCustomize = true; var resolveData = GetResolveData( human ); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); - using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); - var ret = _changeCustomize.Original( human, data, skipEquipment ); + using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); + var ret = _changeCustomize.Original( human, data, skipEquipment ); _inChangeCustomize = false; return ret; } public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory ) { - DisposableContainer Convert( params GenderRace[] races ) + var races = race.Dependencies(); + if( races.Length == 0 ) { - var equipmentEnumerable = - equipment - ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) - : Array.Empty< IDisposable? >().AsEnumerable(); - var accessoryEnumerable = - accessory - ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) - : Array.Empty< IDisposable? >().AsEnumerable(); - return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); + return DisposableContainer.Empty; } - return race switch - { - // @formatter:off - MidlanderMale => Convert( MidlanderMale ), - HighlanderMale => Convert( MidlanderMale, HighlanderMale ), - ElezenMale => Convert( MidlanderMale, ElezenMale ), - MiqoteMale => Convert( MidlanderMale, MiqoteMale ), - RoegadynMale => Convert( MidlanderMale, RoegadynMale ), - LalafellMale => Convert( MidlanderMale, LalafellMale ), - AuRaMale => Convert( MidlanderMale, AuRaMale ), - HrothgarMale => Convert( MidlanderMale, RoegadynMale, HrothgarMale ), - VieraMale => Convert( MidlanderMale, VieraMale ), - - MidlanderFemale => Convert( MidlanderMale, MidlanderFemale ), - HighlanderFemale => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale ), - ElezenFemale => Convert( MidlanderMale, MidlanderFemale, ElezenFemale ), - MiqoteFemale => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale ), - RoegadynFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale ), - LalafellFemale => Convert( MidlanderMale, LalafellMale, LalafellFemale ), - AuRaFemale => Convert( MidlanderMale, MidlanderFemale, AuRaFemale ), - HrothgarFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale ), - VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), - - MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), - HighlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, HighlanderMale, HighlanderMaleNpc ), - ElezenMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, ElezenMale, ElezenMaleNpc ), - MiqoteMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MiqoteMale, MiqoteMaleNpc ), - RoegadynMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, RoegadynMale, RoegadynMaleNpc ), - LalafellMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, LalafellMale, LalafellMaleNpc ), - AuRaMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, AuRaMale, AuRaMaleNpc ), - HrothgarMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, RoegadynMaleNpc, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), - VieraMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, VieraMale, VieraMaleNpc ), - - MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc ), - HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, HighlanderFemale, HighlanderFemaleNpc ), - ElezenFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, ElezenFemale, ElezenFemaleNpc ), - MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, MiqoteFemale, MiqoteFemaleNpc ), - RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, RoegadynFemale, RoegadynFemaleNpc ), - LalafellFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, LalafellMale, LalafellMaleNpc, LalafellFemale, LalafellFemaleNpc ), - AuRaFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, AuRaFemale, AuRaFemaleNpc ), - HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, RoegadynFemale, RoegadynFemaleNpc, HrothgarFemale, HrothgarFemaleNpc ), - VieraFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, VieraFemale, VieraFemaleNpc ), - - UnknownMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, UnknownMaleNpc ), - UnknownFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, UnknownFemaleNpc ), - _ => DisposableContainer.Empty, - // @formatter:on - }; + var equipmentEnumerable = equipment + ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) + : Array.Empty< IDisposable? >().AsEnumerable(); + var accessoryEnumerable = accessory + ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) + : Array.Empty< IDisposable? >().AsEnumerable(); + return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); } } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 5b769b7f..07dc0aff 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -2,6 +2,7 @@ using System; using System.Numerics; using Newtonsoft.Json; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -9,52 +10,6 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -public readonly struct ImcEntry : IEquatable< ImcEntry > -{ - public byte MaterialId { get; init; } - public byte DecalId { get; init; } - public readonly ushort AttributeAndSound; - public byte VfxId { get; init; } - public byte MaterialAnimationId { get; init; } - - public ushort AttributeMask - { - get => ( ushort )( AttributeAndSound & 0x3FF ); - init => AttributeAndSound = ( ushort )( ( AttributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); - } - - public byte SoundId - { - get => ( byte )( AttributeAndSound >> 10 ); - init => AttributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); - } - - public bool Equals( ImcEntry other ) - => MaterialId == other.MaterialId - && DecalId == other.DecalId - && AttributeAndSound == other.AttributeAndSound - && VfxId == other.VfxId - && MaterialAnimationId == other.MaterialAnimationId; - - public override bool Equals( object? obj ) - => obj is ImcEntry other && Equals( other ); - - public override int GetHashCode() - => HashCode.Combine( MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId ); - - [JsonConstructor] - public ImcEntry( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId ) - { - MaterialId = materialId; - DecalId = decalId; - AttributeAndSound = 0; - VfxId = vfxId; - MaterialAnimationId = materialAnimationId; - AttributeMask = attributeMask; - SoundId = soundId; - } -} - public class ImcException : Exception { public readonly ImcManipulation Manipulation; @@ -212,8 +167,11 @@ public unsafe class ImcFile : MetaBaseFile } public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists ) + => GetDefault( path.ToString(), slot, variantIdx, out exists ); + + public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) { - var file = Dalamud.GameData.GetFile( path.ToString() ); + var file = Dalamud.GameData.GetFile( path ); exists = false; if( file == null ) { diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 682fe3af..ed9822a8 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -2,7 +2,9 @@ using System; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.String.Classes; @@ -130,25 +132,12 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { return ObjectType switch { - ObjectType.Accessory => Utf8GamePath.FromString( $"chara/accessory/a{PrimaryId:D4}/a{PrimaryId:D4}.imc", out var p ) - ? p - : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString( $"chara/equipment/e{PrimaryId:D4}/e{PrimaryId:D4}.imc", out var p ) - ? p - : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString( - $"chara/demihuman/d{PrimaryId:D4}/obj/equipment/e{SecondaryId:D4}/e{SecondaryId:D4}.imc", out var p ) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString( $"chara/monster/m{PrimaryId:D4}/obj/body/b{SecondaryId:D4}/b{SecondaryId:D4}.imc", - out var p ) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString( $"chara/weapon/w{PrimaryId:D4}/obj/body/b{SecondaryId:D4}/b{SecondaryId:D4}.imc", - out var p ) - ? p - : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), + ObjectType.Accessory => Utf8GamePath.FromString( GamePaths.Accessory.Imc.Path( PrimaryId ), out var p ) ? p : Utf8GamePath.Empty, + ObjectType.Equipment => Utf8GamePath.FromString( GamePaths.Equipment.Imc.Path( PrimaryId ), out var p ) ? p : Utf8GamePath.Empty, + ObjectType.DemiHuman => Utf8GamePath.FromString( GamePaths.DemiHuman.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, + ObjectType.Monster => Utf8GamePath.FromString( GamePaths.Monster.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, + ObjectType.Weapon => Utf8GamePath.FromString( GamePaths.Weapon.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, + _ => throw new NotImplementedException(), }; } From ca51c3b107aa8579da39342b32d0c3aa646f7564 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 14:06:47 +0100 Subject: [PATCH 0604/2451] Add object-specific IPC for resolving paths and meta. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 34 ++++++++++++- Penumbra/Api/PenumbraApi.cs | 50 ++++++++++++++++++- Penumbra/Api/PenumbraIpcProviders.cs | 29 +++++++---- .../Resolver/PathResolver.Identification.cs | 2 +- Penumbra/packages.lock.json | 2 +- 6 files changed, 103 insertions(+), 16 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 1a3f9d50..891eb195 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1a3f9d501ad44e020500eb7d0a79f91a04e46c93 +Subproject commit 891eb195c29904824004c45f84b92a9e1dd98ddf diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index d6daf14c..fea10f95 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -13,7 +13,6 @@ using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.String; using Penumbra.String.Classes; -using Swan; using Penumbra.Meta.Manipulations; namespace Penumbra.Api; @@ -582,6 +581,7 @@ public class IpcTester : IDisposable private string _currentResolvePath = string.Empty; private string _currentResolveCharacter = string.Empty; private string _currentReversePath = string.Empty; + private int _currentReverseIdx = 0; public Resolve( DalamudPluginInterface pi ) => _pi = pi; @@ -598,6 +598,7 @@ public class IpcTester : IDisposable ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 ); ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, Utf8GamePath.MaxGamePathLength ); + ImGui.InputInt( "##resolveIdx", ref _currentReverseIdx, 0, 0 ); using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { @@ -628,6 +629,12 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( Ipc.ResolveCharacterPath.Subscriber( _pi ).Invoke( _currentResolvePath, _currentResolveCharacter ) ); } + DrawIntro( Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( Ipc.ResolveGameObjectPath.Subscriber( _pi ).Invoke( _currentResolvePath, _currentReverseIdx ) ); + } + DrawIntro( Ipc.ReverseResolvePath.Label, "Reversed Game Paths" ); if( _currentReversePath.Length > 0 ) { @@ -655,6 +662,20 @@ public class IpcTester : IDisposable } } } + + DrawIntro( Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)" ); + if( _currentReversePath.Length > 0 ) + { + var list = Ipc.ReverseResolveGameObjectPath.Subscriber( _pi ).Invoke( _currentReversePath, _currentReverseIdx ); + if( list.Length > 0 ) + { + ImGui.TextUnformatted( list[ 0 ] ); + if( list.Length > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + } + } + } } } @@ -763,7 +784,8 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private string _characterName = string.Empty; + private string _characterName = string.Empty; + private int _gameObjectIndex = 0; public Meta( DalamudPluginInterface pi ) => _pi = pi; @@ -777,6 +799,7 @@ public class IpcTester : IDisposable } ImGui.InputTextWithHint( "##characterName", "Character Name...", ref _characterName, 64 ); + ImGui.InputInt( "##metaIdx", ref _gameObjectIndex, 0, 0 ); using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { @@ -796,6 +819,13 @@ public class IpcTester : IDisposable var base64 = Ipc.GetPlayerMetaManipulations.Subscriber( _pi ).Invoke(); ImGui.SetClipboardText( base64 ); } + + DrawIntro( Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations" ); + if( ImGui.Button( "Copy to Clipboard##GameObject" ) ) + { + var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber( _pi ).Invoke( _gameObjectIndex ); + ImGui.SetClipboardText( base64 ); + } } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index aa6bf84a..49ad61cd 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,7 +23,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 16 ); + => ( 4, 17 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -216,6 +216,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolvePath( string path, string characterName ) => ResolvePath( path, characterName, ushort.MaxValue ); + public string ResolveGameObjectPath( string path, int gameObjectIdx ) + { + CheckInitialized(); + AssociatedCollection( gameObjectIdx, out var collection ); + return ResolvePath( path, Penumbra.ModManager, collection ); + } + public string ResolvePath( string path, string characterName, ushort worldId ) { CheckInitialized(); @@ -239,6 +246,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ret.Select( r => r.ToString() ).ToArray(); } + public string[] ReverseResolveGameObjectPath( string path, int gameObjectIdx ) + { + CheckInitialized(); + if( !Penumbra.Config.EnableMods ) + { + return new[] { path }; + } + + AssociatedCollection( gameObjectIdx, out var collection ); + var ret = collection.ReverseResolvePath( new FullPath( path ) ); + return ret.Select( r => r.ToString() ).ToArray(); + } + public string[] ReverseResolvePlayerPath( string path ) { CheckInitialized(); @@ -794,6 +814,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); } + public string GetGameObjectMetaManipulations( int gameObjectIdx ) + { + CheckInitialized(); + AssociatedCollection( gameObjectIdx, out var collection ); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); + return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + } + internal bool HasTooltip => ChangedItemTooltip != null; @@ -812,6 +840,26 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } + // Return the collection associated to a current game object. If it does not exist, return the default collection. + // If the index is invalid, returns false and the default collection. + private unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) + { + collection = Penumbra.CollectionManager.Default; + if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) + { + return false; + } + + var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + var data = PathResolver.IdentifyCollection( ptr, false ); + if( data.Valid ) + { + collection = data.ModCollection; + } + + return true; + } + // Resolve a path given by string for a specific collection. private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) { diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index dd042b99..69db9c99 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -52,9 +52,11 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, string > ResolveDefaultPath; internal readonly FuncProvider< string, string > ResolveInterfacePath; internal readonly FuncProvider< string, string > ResolvePlayerPath; + internal readonly FuncProvider< string, int, string > ResolveGameObjectPath; internal readonly FuncProvider< string, string, string > ResolveCharacterPath; internal readonly FuncProvider< string, string, string[] > ReverseResolvePath; - internal readonly FuncProvider< string, string[] > ReverseResolvePathPlayer; + internal readonly FuncProvider< string, int, string[] > ReverseResolveGameObjectPath; + internal readonly FuncProvider< string, string[] > ReverseResolvePlayerPath; // Collections internal readonly FuncProvider< IList< string > > GetCollections; @@ -67,6 +69,7 @@ public class PenumbraIpcProviders : IDisposable // Meta internal readonly FuncProvider< string > GetPlayerMetaManipulations; internal readonly FuncProvider< string, string > GetMetaManipulations; + internal readonly FuncProvider< int, string > GetGameObjectMetaManipulations; // Mods internal readonly FuncProvider< IList< (string, string) > > GetMods; @@ -144,12 +147,14 @@ public class PenumbraIpcProviders : IDisposable () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent ); // Resolve - ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider( pi, Api.ResolveDefaultPath ); - ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider( pi, Api.ResolveInterfacePath ); - ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider( pi, Api.ResolvePlayerPath ); - ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider( pi, Api.ResolvePath ); - ReverseResolvePath = Ipc.ReverseResolvePath.Provider( pi, Api.ReverseResolvePath ); - ReverseResolvePathPlayer = Ipc.ReverseResolvePlayerPath.Provider( pi, Api.ReverseResolvePlayerPath ); + ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider( pi, Api.ResolveDefaultPath ); + ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider( pi, Api.ResolveInterfacePath ); + ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider( pi, Api.ResolvePlayerPath ); + ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider( pi, Api.ResolveGameObjectPath ); + ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider( pi, Api.ResolvePath ); + ReverseResolvePath = Ipc.ReverseResolvePath.Provider( pi, Api.ReverseResolvePath ); + ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider( pi, Api.ReverseResolveGameObjectPath ); + ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider( pi, Api.ReverseResolvePlayerPath ); // Collections GetCollections = Ipc.GetCollections.Provider( pi, Api.GetCollections ); @@ -160,8 +165,9 @@ public class PenumbraIpcProviders : IDisposable GetChangedItems = Ipc.GetChangedItems.Provider( pi, Api.GetChangedItemsForCollection ); // Meta - GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider( pi, Api.GetPlayerMetaManipulations ); - GetMetaManipulations = Ipc.GetMetaManipulations.Provider( pi, Api.GetMetaManipulations ); + GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider( pi, Api.GetPlayerMetaManipulations ); + GetMetaManipulations = Ipc.GetMetaManipulations.Provider( pi, Api.GetMetaManipulations ); + GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider( pi, Api.GetGameObjectMetaManipulations ); // Mods GetMods = Ipc.GetMods.Provider( pi, Api.GetModList ); @@ -242,9 +248,11 @@ public class PenumbraIpcProviders : IDisposable ResolveDefaultPath.Dispose(); ResolveInterfacePath.Dispose(); ResolvePlayerPath.Dispose(); + ResolveGameObjectPath.Dispose(); ResolveCharacterPath.Dispose(); ReverseResolvePath.Dispose(); - ReverseResolvePathPlayer.Dispose(); + ReverseResolveGameObjectPath.Dispose(); + ReverseResolvePlayerPath.Dispose(); // Collections GetCollections.Dispose(); @@ -257,6 +265,7 @@ public class PenumbraIpcProviders : IDisposable // Meta GetPlayerMetaManipulations.Dispose(); GetMetaManipulations.Dispose(); + GetGameObjectMetaManipulations.Dispose(); // Mods GetMods.Dispose(); diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 7895e9fa..991027e4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -17,7 +17,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { // Identify the correct collection for a GameObject by index and name. - private static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) + public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { if( gameObject == null ) { diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 38fcd25f..aea52c7e 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -67,7 +67,7 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.0, )", + "Penumbra.Api": "[1.0.3, )", "Penumbra.String": "[1.0.0, )" } }, From 37a56c56af417c84a31cc0d89ac5d9b8c5c48c9b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 14:37:48 +0100 Subject: [PATCH 0605/2451] Fix bug with collections with inheritance saving on every launch. --- Penumbra/Collections/CollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.File.cs | 1 + Penumbra/Collections/ModCollection.Inheritance.cs | 8 ++++++-- Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 66db1d1d..261a91be 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -339,7 +339,7 @@ public partial class ModCollection changes = true; Penumbra.Log.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); } - else if( !collection.AddInheritance( subCollection ) ) + else if( !collection.AddInheritance( subCollection, false ) ) { changes = true; Penumbra.Log.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index a9cef608..150382ad 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -25,6 +25,7 @@ public partial class ModCollection { try { + Penumbra.Log.Debug( $"Saving collection {AnonymizedName}..." ); var file = FileName; file.Directory?.Create(); using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew ); diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 44403758..d9eecdbd 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -69,7 +69,7 @@ public partial class ModCollection // Add a new collection to the inheritance list. // We do not check if this collection would be visited before, // only that it is unique in the list itself. - public bool AddInheritance( ModCollection collection ) + public bool AddInheritance( ModCollection collection, bool invokeEvent ) { if( CheckValidInheritance( collection ) != ValidInheritance.Valid ) { @@ -80,7 +80,11 @@ public partial class ModCollection // Changes in inherited collections may need to trigger further changes here. collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; - InheritanceChanged.Invoke( false ); + if( invokeEvent ) + { + InheritanceChanged.Invoke( false ); + } + Penumbra.Log.Debug( $"Added {collection.AnonymizedName} to {AnonymizedName} inheritances." ); return true; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index e344a303..fd64093f 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -194,7 +194,7 @@ public partial class ConfigWindow }; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, inheritance != ModCollection.ValidInheritance.Valid, true ) - && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance! ) ) + && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance!, true ) ) { _newInheritance = null; } From 0534fecc0c429a6ef2c10753bba91f30803fa9b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 14:41:04 +0100 Subject: [PATCH 0606/2451] Added button to immediately assign collection to current player. --- .../ConfigWindow.CollectionsTab.Individual.cs | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 57ff05a8..275e358d 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -69,11 +69,11 @@ public partial class ConfigWindow private ObjectKind _newKind = ObjectKind.BattleNpc; private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Data.Worlds); - private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Data.Mounts ); - private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Data.Companions ); - private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Data.Ornaments ); - private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.Data.BNpcs ); - private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.Data.ENpcs ); + private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Data.Mounts); + private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Data.Companions); + private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Data.Ornaments); + private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.Data.BNpcs); + private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.Data.ENpcs); private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; @@ -166,7 +166,7 @@ public partial class ConfigWindow } } - ImGui.Dummy( Vector2.Zero ); + ImGui.Dummy( _window._defaultSpace ); DrawNewIndividualCollection(); } @@ -243,11 +243,13 @@ public partial class ConfigWindow var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); - var combo = GetNpcCombo( _newKind ); - var change = DrawNewPlayerCollection( buttonWidth1, width ); + var change = DrawNewCurrentPlayerCollection(); + + change |= DrawNewPlayerCollection( buttonWidth1, width ); ImGui.SameLine(); change |= DrawNewRetainerCollection( buttonWidth2 ); + var combo = GetNpcCombo( _newKind ); change |= DrawNewNpcCollection( combo, buttonWidth1, width ); ImGui.SameLine(); change |= DrawNewOwnedCollection( buttonWidth2 ); @@ -258,6 +260,27 @@ public partial class ConfigWindow } } + private bool DrawNewCurrentPlayerCollection() + { + var player = Penumbra.Actors.GetCurrentPlayer(); + var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No logged-in character detected.", + _ => string.Empty, + }; + + if( ImGuiUtil.DrawDisabledButton( "Assign Currently Played Character", _window._inputTextWidth, tt, result != IndividualCollections.AddResult.Valid ) ) + { + Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); + return true; + } + + return false; + } + private void UpdateIdentifiers() { var combo = GetNpcCombo( _newKind ); @@ -306,7 +329,9 @@ public partial class ConfigWindow private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) { if( type == CollectionType.Individual ) + { UpdateIdentifiers(); + } } } } \ No newline at end of file From 7635c0c834ac6ea0800e3a1ca8cad6f4f4902ca6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 14:46:35 +0100 Subject: [PATCH 0607/2451] Update Changelog --- Penumbra/UI/ConfigWindow.Changelog.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 4a6ab8b1..741be600 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -42,19 +42,31 @@ public partial class ConfigWindow 1 ) .RegisterEntry( "You can also manually sort your Individual Collections by drag and drop now.", 1 ) .RegisterEntry( "This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1 ) - .RegisterEntry( "These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", 1 ) - .RegisterEntry( "General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", 1 ) - .RegisterEntry( "Added Dye Previews for in-game dyes and dyeing templates in Material Editing." ) - .RegisterEntry( "Added Export buttons to .mdl and .mtrl previews in Advanced Editing." ) + .RegisterEntry( "These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", 1 ) + .RegisterEntry( "General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", 1 ) + .RegisterEntry( "Added NPC Model Parsing, changes in NPC models should now display the names of the changed game objects for most NPCs." ) + .RegisterEntry( "Changed Items now also display variant or subtype in addition to the model set ID where applicable." ) .RegisterEntry( "Collection selectors can now be filtered by name." ) .RegisterEntry( "Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths." ) .RegisterEntry( "Improved interface for group settings (minimally)." ) - .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) - .RegisterEntry( "New Special or Individual Assignments now default to your current Base assignment instead of None." ) + .RegisterEntry( "New Special or Individual Assignments now default to your current Base assignment instead of None." ) .RegisterEntry( "Improved Support Info somewhat." ) + .RegisterEntry( "Added Dye Previews for in-game dyes and dyeing templates in Material Editing." ) + .RegisterEntry( "Colorset Editing now allows for negative values in all cases." ) + .RegisterEntry( "Added Export buttons to .mdl and .mtrl previews in Advanced Editing." ) + .RegisterEntry( "File Selection in the .mdl and .mtrl tabs now shows one associated game path by default and all on hover." ) + .RegisterEntry( + "Added the option to reduplicate and normalize a mod, restoring all duplicates and moving the files to appropriate folders. (Duplicates Tab in Advanced Editing)" ) + .RegisterEntry( "Added an option to re-export metadata changes to TexTools-typed .meta and .rgsp files. (Meta-Manipulations Tab in Advanced Editing)" ) + .RegisterEntry( "Fixed several bugs with the incorporation of meta changes when not done during TTMP import." ) + .RegisterEntry( "Fixed a bug with RSP changes on non-base collections not applying correctly in some cases." ) .RegisterEntry( "Fixed a bug when dragging options during mod edit." ) .RegisterEntry( "Fixed a bug where sometimes the valid folder check caused issues." ) + .RegisterEntry( "Fixed a bug where collections with inheritances were newly saved on every load." ) .RegisterEntry( "Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged)." ) + .RegisterEntry( "Mods without names or invalid mod folders are now warnings instead of errors." ) + .RegisterEntry( "Added IPC events for mod deletion, addition or moves, and resolving based on game objects." ) + .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) .RegisterEntry( "A lot of big backend changes." ); private static void Add5_11_1( Changelog log ) From 7db67fefa8ec5d99b39a1f53dcb2111cd23e6eef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 15:53:48 +0100 Subject: [PATCH 0608/2451] Update release action for build. --- .github/workflows/release.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf60c03a..565ec74f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,10 +75,13 @@ 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 master + git fetch origin master + git fetch origin test + git checkout master git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true - git push origin master || true - git branch -f test origin/master && git checkout test + git push origin master + git branch -f test origin/master + git checkout test git push origin test -f || true From 972187d8ed05f9fbf52cee3640e59528c1ba18ef Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Dec 2022 14:56:18 +0000 Subject: [PATCH 0609/2451] [CI] Updating repo.json for refs/tags/0.6.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2dd04e6d..7c988994 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.11.1", - "TestingAssemblyVersion": "0.5.11.1", + "AssemblyVersion": "0.6.0.0", + "TestingAssemblyVersion": "0.6.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5df00b0c7fe763c8d335ca6c8840ae4511d1c5f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 16:07:32 +0100 Subject: [PATCH 0610/2451] Fix the dumb --- Penumbra/Penumbra.cs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index dfb794ea..4bc7f694 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,7 +34,9 @@ namespace Penumbra; public class Penumbra : IDalamudPlugin { - public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public const string RepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/master/repo.json"; + public const string TestRepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/test/repo.json"; public string Name => "Penumbra"; @@ -344,7 +346,7 @@ public class Penumbra : IDalamudPlugin collectionName = split[ 0 ]; characterName = split[ 1 ]; - identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); + identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); if( !identifier.IsValid ) { Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); @@ -566,7 +568,7 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var dir = new DirectoryInfo( path ); try { @@ -587,9 +589,12 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; - if (!ret) - Log.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; + if( !ret ) + { + Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + } + return !ret; #else return false; @@ -602,10 +607,10 @@ public class Penumbra : IDalamudPlugin #if !DEBUG return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch { - null => false, - Repository => true, - "https://raw.githubusercontent.com/xivdev/Penumbra/test/repo.json" => true, - _ => false, + null => false, + RepositoryLower => true, + TestRepositoryLower => true, + _ => false, }; #else return true; From 2606632533cf955df6d616b406992535506bed1a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Dec 2022 15:10:07 +0000 Subject: [PATCH 0611/2451] [CI] Updating repo.json for refs/tags/0.6.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 7c988994..7984a3cf 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.0", - "TestingAssemblyVersion": "0.6.0.0", + "AssemblyVersion": "0.6.0.1", + "TestingAssemblyVersion": "0.6.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 114ed5954ed8e78794c04e771eb9460e321e11bc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 17:06:25 +0100 Subject: [PATCH 0612/2451] Check aesthetician for Yourself collection. --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 991027e4..868e08a8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -34,7 +34,7 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !Dalamud.ClientState.IsLoggedIn ) + if( !Dalamud.ClientState.IsLoggedIn || Dalamud.GameGui.GetAddonByName( "_CharaMakeTitle", 1 ) != IntPtr.Zero ) { var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) From bfddcdd7e2c10b788fd44c0f6a9c95c7b1ea244c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 17:07:01 +0100 Subject: [PATCH 0613/2451] Check Yourself assignment for special actors. --- Penumbra/Api/PenumbraApi.cs | 4 ++-- .../Collections/CollectionManager.Active.cs | 4 ++-- .../IndividualCollections.Access.cs | 19 ++++++++++--------- .../Resolver/PathResolver.Identification.cs | 12 ++++++------ 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 49ad61cd..d9e92eda 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -333,7 +333,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection( string characterName, ushort worldId ) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection ) + return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection, out _ ) ? ( collection.Name, true ) : ( Penumbra.CollectionManager.Default.Name, false ); } @@ -807,7 +807,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var identifier = NameToIdentifier( characterName, worldId ); - var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) + var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c, out _ ) ? c : Penumbra.CollectionManager.Individual( identifier ); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 355c9297..7f23eae0 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -42,7 +42,7 @@ public partial class ModCollection public readonly IndividualCollections Individuals = new(Penumbra.Actors); public ModCollection Individual( ActorIdentifier identifier ) - => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; + => Individuals.TryGetCollection( identifier, out var c, out _ ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; @@ -64,7 +64,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null, + CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c, out _ ) ? c : null : null, _ => null, }; } diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 01858010..142b9131 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -23,8 +23,9 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public (string DisplayName, ModCollection Collection) this[ int index ] => ( _assignments[ index ].DisplayName, _assignments[ index ].Collection ); - public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) + public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection, out ActorIdentifier specialIdentifier ) { + specialIdentifier = ActorIdentifier.Invalid; switch( identifier.Type ) { case IdentifierType.Player: return CheckWorlds( identifier, out collection ); @@ -73,12 +74,12 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: - return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); + return CheckWorlds( specialIdentifier = _actorManager.GetCurrentPlayer(), out collection ); case SpecialActor.ExamineScreen: { - return CheckWorlds( _actorManager.GetInspectPlayer(), out collection! ) - || CheckWorlds( _actorManager.GetCardPlayer(), out collection! ) - || CheckWorlds( _actorManager.GetGlamourPlayer(), out collection! ); + return CheckWorlds( specialIdentifier = _actorManager.GetInspectPlayer(), out collection! ) + || CheckWorlds( specialIdentifier = _actorManager.GetCardPlayer(), out collection! ) + || CheckWorlds( specialIdentifier = _actorManager.GetGlamourPlayer(), out collection! ); } } @@ -89,11 +90,11 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return false; } - public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); + public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection, out ActorIdentifier specialIdentifier ) + => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection, out specialIdentifier ); - public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); + public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection, out ActorIdentifier specialIdentifier ) + => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection, out specialIdentifier ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 868e08a8..f1d704f1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -44,8 +44,8 @@ public unsafe partial class PathResolver else { var identifier = Penumbra.Actors.FromObject( gameObject, false ); - var collection = CollectionByIdentifier( identifier ) - ?? CheckYourself( identifier, gameObject ) + var collection = CollectionByIdentifier( identifier, out var specialIdentifier ) + ?? CheckYourself( identifier.Type == IdentifierType.Special ? specialIdentifier : identifier, gameObject ) ?? CollectionByAttributes( gameObject ) ?? CheckOwnedCollection( identifier, gameObject ) ?? Penumbra.CollectionManager.Default; @@ -71,16 +71,16 @@ public unsafe partial class PathResolver } var player = Penumbra.Actors.GetCurrentPlayer(); - return CollectionByIdentifier( player ) + return CollectionByIdentifier( player, out _ ) ?? CheckYourself( player, gameObject ) ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; } // Check both temporary and permanent character collections. Temporary first. - private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) - => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) - || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) + private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier, out ActorIdentifier specialIdentifier ) + => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection, out specialIdentifier ) + || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection, out specialIdentifier ) ? collection : null; From c06eb1ad3da68f8ceb8ec5f04bf131db25a49e69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 18:25:43 +0100 Subject: [PATCH 0614/2451] Fix typo in imc path. --- Penumbra.GameData/Data/GamePaths.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index 21f2d66a..8e2076a4 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -91,7 +91,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId demiId, SetId equipId) - => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/b{equipId.Value:D4}.imc"; + => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; } public static partial class Mdl From a6b3aab61a4d54644c7b06528aa204a3c4688c69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 18:54:06 +0100 Subject: [PATCH 0615/2451] Add Mannequin-Handling for Retainer Individuals --- Penumbra.GameData/Actors/ActorManager.Data.cs | 20 ++++++++++++++++ .../Actors/ActorManager.Identifiers.cs | 23 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index c54fafe4..3d962d0e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -234,4 +234,24 @@ public sealed partial class ActorManager : IDisposable private static unsafe ushort InspectWorldId => *_inspectWorldId; + + public static readonly IReadOnlySet MannequinIds = new HashSet() + { + 1026228u, + 1026229u, + 1026986u, + 1026987u, + 1026988u, + 1026989u, + 1032291u, + 1032292u, + 1032293u, + 1032294u, + 1033046u, + 1033047u, + 1033658u, + 1033659u, + 1007137u, + // TODO: Female Hrothgar + }; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 8b7ea937..a84f0e8f 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -76,7 +76,7 @@ public partial class ActorManager IdentifierType.Retainer => id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id ? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})'s {Data.ToName(id.Kind, id.DataId)}" - : $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", + : $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", IdentifierType.Special => id.Special.ToName(), IdentifierType.Npc => id.Index == ushort.MaxValue @@ -150,6 +150,23 @@ public partial class ActorManager // Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF if (dataId == 0xf845d) dataId = actor->GetNpcID(); + if (MannequinIds.Contains(dataId)) + { + static ByteString Get(byte* ptr) + => ptr == null ? ByteString.Empty : new ByteString(ptr); + + var actualName = Get(actor->GetName()); + var retainerName = Get(actor->Name); + if (!actualName.Equals(retainerName)) + { + var ident = check + ? CreateRetainer(retainerName) + : CreateIndividualUnchecked(IdentifierType.Retainer, retainerName, actor->ObjectIndex, ObjectKind.EventNpc, dataId); + if (ident.IsValid) + return ident; + } + } + return check ? CreateNpc(ObjectKind.EventNpc, dataId, actor->ObjectIndex) : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, dataId); @@ -182,7 +199,7 @@ public partial class ActorManager { var name = new ByteString(actor->Name); var index = actor->ObjectIndex; - return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); + return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); } } } @@ -212,7 +229,7 @@ public partial class ActorManager IdentifierType.Retainer => CreateRetainer(name), IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), - IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), + IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0), _ => ActorIdentifier.Invalid, }; From 69ced1089c2e8cf8602055dedb62bcb578149af9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 18:54:15 +0100 Subject: [PATCH 0616/2451] Add bugfix changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 741be600..02f47480 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -25,10 +25,19 @@ public partial class ConfigWindow Add5_11_0( ret ); Add5_11_1( ret ); Add6_0_0( ret ); + Add6_0_2( ret ); return ret; } + private static void Add6_0_2( Changelog log ) + => log.NextVersion( "Version 0.6.0.2" ) + .RegisterEntry( "Let Bell Retainer collections apply to retainer-named mannequins." ) + .RegisterEntry( "Added a few informations to a help marker for new individual assignments." ) + .RegisterEntry( "Fix bug with Demi Human IMC paths." ) + .RegisterEntry( "Fix Yourself collection not applying to UI actors." ) + .RegisterEntry( "Fix Yourself collection not applying during aesthetician." ); + private static void Add6_0_0( Changelog log ) => log.NextVersion( "Version 0.6.0.0" ) .RegisterEntry( "Revamped Individual Collections:" ) From 2e272f8e3a9346f8928fe1f889217120290f0a62 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Dec 2022 17:56:56 +0000 Subject: [PATCH 0617/2451] [CI] Updating repo.json for refs/tags/0.6.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 7984a3cf..6950afa4 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.1", - "TestingAssemblyVersion": "0.6.0.1", + "AssemblyVersion": "0.6.0.2", + "TestingAssemblyVersion": "0.6.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e221c275a2c0b8921c675771b3669566617eb584 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Dec 2022 20:42:19 +0100 Subject: [PATCH 0618/2451] Names can have a hyphen as last character apparently. --- Penumbra.GameData/Actors/ActorManager.Identifiers.cs | 4 ++-- Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index a84f0e8f..75cb7043 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -345,7 +345,7 @@ public partial class ActorManager last = current; } - return part[^1] != '-'; + return true; } private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) @@ -375,7 +375,7 @@ public partial class ActorManager last = current; } - return part[^1] != (byte)'-'; + return true; } /// Checks if the world is a valid public world or ushort.MaxValue (any world). diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 275e358d..16ae8253 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -278,6 +278,11 @@ public partial class ConfigWindow return true; } + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" + + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" + + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned." ); + return false; } From 1887a785f58d78b6013cd9e185245783233d496e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Dec 2022 19:45:09 +0000 Subject: [PATCH 0619/2451] [CI] Updating repo.json for refs/tags/0.6.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6950afa4..66c37aef 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.2", - "TestingAssemblyVersion": "0.6.0.2", + "AssemblyVersion": "0.6.0.3", + "TestingAssemblyVersion": "0.6.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5f4351d4f151b00d6c4e91fdc48d74a3e06ead29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 00:10:49 +0100 Subject: [PATCH 0620/2451] Fix collection selectors not updating correctly. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.Misc.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index cbf21c63..441d5f9b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cbf21c639f91d39422b2d4b7244bd8d8c5d5d4d7 +Subproject commit 441d5f9b2943f8ab81501870314d9b1610cb3b51 diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 9cc18bf4..38a98b50 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -114,7 +115,7 @@ public partial class ConfigWindow private sealed class CollectionSelector : FilterComboCache< ModCollection > { - public CollectionSelector( IEnumerable< ModCollection > items ) + public CollectionSelector( Func> items ) : base( items ) { } @@ -140,8 +141,8 @@ public partial class ConfigWindow => obj.Name; } - private static readonly CollectionSelector CollectionsWithEmpty = new(Penumbra.CollectionManager.OrderBy( c => c.Name ).Prepend( ModCollection.Empty )); - private static readonly CollectionSelector Collections = new(Penumbra.CollectionManager.OrderBy( c => c.Name )); + private static readonly CollectionSelector CollectionsWithEmpty = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).Prepend( ModCollection.Empty ).ToList()); + private static readonly CollectionSelector Collections = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).ToList()); // Draw a collection selector of a certain width for a certain type. private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty ) From 882a59c1bf09c69beeb58f884bee3364fd230303 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 00:48:05 +0100 Subject: [PATCH 0621/2451] Handle Chocobos and GPose Ownership. --- .../Actors/ActorManager.Identifiers.cs | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 75cb7043..fbfe235e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -4,6 +4,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Newtonsoft.Json.Linq; using Penumbra.String; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.GameData.Actors; @@ -89,6 +90,22 @@ public partial class ActorManager }; } + private unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* HandleCutscene( + FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* main) + { + if (main == null) + return null; + + if (main->ObjectIndex is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd) + { + var parentIdx = _toParentIdx(main->ObjectIndex); + if (parentIdx >= 0) + return (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx); + } + + return main; + } + /// /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// @@ -97,24 +114,17 @@ public partial class ActorManager if (actor == null) return ActorIdentifier.Invalid; + actor = HandleCutscene(actor); var idx = actor->ObjectIndex; - if (idx is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd) - { - var parentIdx = _toParentIdx(idx); - if (parentIdx >= 0) - return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx), check); - } - else if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) - { + if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue); - } switch ((ObjectKind)actor->ObjectKind) { case ObjectKind.Player: { var name = new ByteString(actor->Name); - var homeWorld = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->HomeWorld; + var homeWorld = ((Character*)actor)->HomeWorld; return check ? CreatePlayer(name, homeWorld) : CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue); @@ -122,27 +132,25 @@ public partial class ActorManager case ObjectKind.BattleNpc: { var ownerId = actor->OwnerID; + // 952 -> 780 is a special case for chocobos because they have NameId == 0 otherwise. + var nameId = actor->DataID == 952 ? 780 : ((Character*)actor)->NameID; if (ownerId != 0xE0000000) { - var owner = - (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero); + var owner = (Character*)HandleCutscene( + (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero)); if (owner == null) return ActorIdentifier.Invalid; var name = new ByteString(owner->GameObject.Name); var homeWorld = owner->HomeWorld; return check - ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, - ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID) - : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, - ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); + ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) + : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); } return check - ? CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, - actor->ObjectIndex) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.BattleNpc, - ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); + ? CreateNpc(ObjectKind.BattleNpc, nameId, actor->ObjectIndex) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.BattleNpc, nameId); } case ObjectKind.EventNpc: { @@ -175,10 +183,8 @@ public partial class ActorManager case ObjectKind.Companion: case (ObjectKind)15: // TODO: CS Update { - if (actor->ObjectIndex % 2 == 0) - return ActorIdentifier.Invalid; - - var owner = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_objects.GetObjectAddress(actor->ObjectIndex - 1); + var owner = (Character*)HandleCutscene( + (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(actor->ObjectIndex - 1)); if (owner == null) return ActorIdentifier.Invalid; From 2b6275fe67115d0152c4370219382c4c61b858ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 01:35:11 +0100 Subject: [PATCH 0622/2451] Handle ownership in gpose / cutscenes better. --- .../Actors/ActorManager.Identifiers.cs | 28 ++++++++++++------- .../IndividualCollections.Access.cs | 8 +++++- .../Resolver/PathResolver.Identification.cs | 22 +++------------ 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index fbfe235e..49af09c1 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -109,8 +109,10 @@ public partial class ActorManager /// /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// - public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, bool check = true) + public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, + out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool check = true) { + owner = null; if (actor == null) return ActorIdentifier.Invalid; @@ -136,13 +138,13 @@ public partial class ActorManager var nameId = actor->DataID == 952 ? 780 : ((Character*)actor)->NameID; if (ownerId != 0xE0000000) { - var owner = (Character*)HandleCutscene( + owner = HandleCutscene( (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero)); if (owner == null) return ActorIdentifier.Invalid; - var name = new ByteString(owner->GameObject.Name); - var homeWorld = owner->HomeWorld; + var name = new ByteString(owner->Name); + var homeWorld = ((Character*)owner)->HomeWorld; return check ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); @@ -183,16 +185,18 @@ public partial class ActorManager case ObjectKind.Companion: case (ObjectKind)15: // TODO: CS Update { - var owner = (Character*)HandleCutscene( + owner = HandleCutscene( (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(actor->ObjectIndex - 1)); if (owner == null) return ActorIdentifier.Invalid; - var dataId = GetCompanionId(actor, &owner->GameObject); + var dataId = GetCompanionId(actor, owner); + var name = new ByteString(owner->Name); + var homeWorld = ((Character*)owner)->HomeWorld; + var kind = (ObjectKind)actor->ObjectKind; return check - ? CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId) - : CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld, - (ObjectKind)actor->ObjectKind, dataId); + ? CreateOwned(name, homeWorld, kind, dataId) + : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, kind, dataId); } case ObjectKind.Retainer: { @@ -225,8 +229,12 @@ public partial class ActorManager }; } + public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, + bool check = true) + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, check); + public unsafe ActorIdentifier FromObject(GameObject? actor, bool check = true) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), check); + => FromObject(actor, out _, check); public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 142b9131..79561243 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -26,6 +26,12 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection, out ActorIdentifier specialIdentifier ) { specialIdentifier = ActorIdentifier.Invalid; + if( Count == 0 ) + { + collection = null; + return false; + } + switch( identifier.Type ) { case IdentifierType.Player: return CheckWorlds( identifier, out collection ); @@ -94,7 +100,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection, out specialIdentifier ); public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection, out ActorIdentifier specialIdentifier ) - => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection, out specialIdentifier ); + => TryGetCollection( _actorManager.FromObject( gameObject, out _, false ), out collection, out specialIdentifier ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index f1d704f1..973fad13 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -43,11 +43,11 @@ public unsafe partial class PathResolver } else { - var identifier = Penumbra.Actors.FromObject( gameObject, false ); + var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, false ); var collection = CollectionByIdentifier( identifier, out var specialIdentifier ) ?? CheckYourself( identifier.Type == IdentifierType.Special ? specialIdentifier : identifier, gameObject ) ?? CollectionByAttributes( gameObject ) - ?? CheckOwnedCollection( identifier, gameObject ) + ?? CheckOwnedCollection( identifier, owner ) ?? Penumbra.CollectionManager.Default; return IdentifiedCache.Set( collection, identifier, gameObject ); } @@ -124,23 +124,9 @@ public unsafe partial class PathResolver } // Get the collection applying to the owner if it is available. - private static ModCollection? CheckOwnedCollection( ActorIdentifier identifier, GameObject* obj ) + private static ModCollection? CheckOwnedCollection( ActorIdentifier identifier, GameObject* owner ) { - if( identifier.Type != IdentifierType.Owned || !Penumbra.Config.UseOwnerNameForCharacterCollection ) - { - return null; - } - - var owner = identifier.Kind switch - { - ObjectKind.BattleNpc when obj->OwnerID != 0xE0000000 => ( GameObject* )( Dalamud.Objects.SearchById( obj->OwnerID )?.Address ?? IntPtr.Zero ), - ObjectKind.MountType when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), - ObjectKind.Companion when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), - ( ObjectKind )15 when obj->ObjectIndex % 2 == 1 => ( GameObject* )Dalamud.Objects.GetObjectAddress( obj->ObjectIndex - 1 ), // TODO: CS Update - _ => null, - }; - - if( owner == null ) + if( identifier.Type != IdentifierType.Owned || !Penumbra.Config.UseOwnerNameForCharacterCollection || owner == null ) { return null; } From 63a22198aa6000c5293842021638b80f9c5c7c63 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Dec 2022 00:53:48 +0000 Subject: [PATCH 0623/2451] [CI] Updating repo.json for refs/tags/0.6.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 66c37aef..27bcc7c1 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.3", - "TestingAssemblyVersion": "0.6.0.3", + "AssemblyVersion": "0.6.0.4", + "TestingAssemblyVersion": "0.6.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9af7e9d9480ecef742b12576889d0d3513097692 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 16:25:38 +0100 Subject: [PATCH 0624/2451] Rework special actor identification again. --- .../Actors/ActorManager.Identifiers.cs | 4 +- Penumbra/Api/PenumbraApi.cs | 4 +- .../Collections/CollectionManager.Active.cs | 4 +- .../IndividualCollections.Access.cs | 65 ++++++++++++------- .../Resolver/PathResolver.Identification.cs | 15 +++-- 5 files changed, 55 insertions(+), 37 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 49af09c1..ad3af449 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -121,7 +121,8 @@ public partial class ActorManager if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue); - switch ((ObjectKind)actor->ObjectKind) + var kind = (ObjectKind)actor->ObjectKind; + switch (kind) { case ObjectKind.Player: { @@ -193,7 +194,6 @@ public partial class ActorManager var dataId = GetCompanionId(actor, owner); var name = new ByteString(owner->Name); var homeWorld = ((Character*)owner)->HomeWorld; - var kind = (ObjectKind)actor->ObjectKind; return check ? CreateOwned(name, homeWorld, kind, dataId) : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, kind, dataId); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index d9e92eda..49ad61cd 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -333,7 +333,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection( string characterName, ushort worldId ) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection, out _ ) + return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection ) ? ( collection.Name, true ) : ( Penumbra.CollectionManager.Default.Name, false ); } @@ -807,7 +807,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var identifier = NameToIdentifier( characterName, worldId ); - var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c, out _ ) + var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) ? c : Penumbra.CollectionManager.Individual( identifier ); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 7f23eae0..355c9297 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -42,7 +42,7 @@ public partial class ModCollection public readonly IndividualCollections Individuals = new(Penumbra.Actors); public ModCollection Individual( ActorIdentifier identifier ) - => Individuals.TryGetCollection( identifier, out var c, out _ ) ? c : Default; + => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; @@ -64,7 +64,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c, out _ ) ? c : null : null, + CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null, _ => null, }; } diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 79561243..1c02c19c 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -23,9 +23,8 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public (string DisplayName, ModCollection Collection) this[ int index ] => ( _assignments[ index ].DisplayName, _assignments[ index ].Collection ); - public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection, out ActorIdentifier specialIdentifier ) + public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) { - specialIdentifier = ActorIdentifier.Invalid; if( Count == 0 ) { collection = null; @@ -72,35 +71,53 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return false; } - case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); - case IdentifierType.Special: - switch( identifier.Special ) - { - case SpecialActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: - case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: - case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: - case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: - return CheckWorlds( specialIdentifier = _actorManager.GetCurrentPlayer(), out collection ); - case SpecialActor.ExamineScreen: - { - return CheckWorlds( specialIdentifier = _actorManager.GetInspectPlayer(), out collection! ) - || CheckWorlds( specialIdentifier = _actorManager.GetCardPlayer(), out collection! ) - || CheckWorlds( specialIdentifier = _actorManager.GetGlamourPlayer(), out collection! ); - } - } - - break; + case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); + case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ), out collection ); } collection = null; return false; } - public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection, out ActorIdentifier specialIdentifier ) - => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection, out specialIdentifier ); + public ActorIdentifier ConvertSpecialIdentifier( ActorIdentifier identifier ) + { + if( identifier.Type != IdentifierType.Special ) + { + return identifier; + } - public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection, out ActorIdentifier specialIdentifier ) - => TryGetCollection( _actorManager.FromObject( gameObject, out _, false ), out collection, out specialIdentifier ); + switch( identifier.Special ) + { + case SpecialActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: + case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: + return _actorManager.GetCurrentPlayer(); + case SpecialActor.ExamineScreen: + { + identifier = _actorManager.GetInspectPlayer(); + if( identifier.IsValid ) + { + return identifier; + } + + identifier = _actorManager.GetCardPlayer(); + if( identifier.IsValid ) + { + return identifier; + } + + return _actorManager.GetGlamourPlayer(); + } + default: return identifier; + } + } + + public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) + => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); + + public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) + => TryGetCollection( _actorManager.FromObject( gameObject, out _, false ), out collection ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 973fad13..9b0e04b8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -44,8 +44,9 @@ public unsafe partial class PathResolver else { var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, false ); - var collection = CollectionByIdentifier( identifier, out var specialIdentifier ) - ?? CheckYourself( identifier.Type == IdentifierType.Special ? specialIdentifier : identifier, gameObject ) + identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); + var collection = CollectionByIdentifier( identifier ) + ?? CheckYourself( identifier, gameObject ) ?? CollectionByAttributes( gameObject ) ?? CheckOwnedCollection( identifier, owner ) ?? Penumbra.CollectionManager.Default; @@ -71,16 +72,16 @@ public unsafe partial class PathResolver } var player = Penumbra.Actors.GetCurrentPlayer(); - return CollectionByIdentifier( player, out _ ) + return CollectionByIdentifier( player ) ?? CheckYourself( player, gameObject ) ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; } // Check both temporary and permanent character collections. Temporary first. - private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier, out ActorIdentifier specialIdentifier ) - => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection, out specialIdentifier ) - || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection, out specialIdentifier ) + private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) + => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) + || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) ? collection : null; @@ -143,7 +144,7 @@ public unsafe partial class PathResolver { var sheet = gameData.GetExcelSheet< ModelChara >()!; var ret = new BitArray( ( int )sheet.RowCount, false ); - foreach( var (row, idx) in sheet.WithIndex().Where( p => p.Value.Type == ( byte )CharacterBase.ModelType.Human ) ) + foreach( var (_, idx) in sheet.WithIndex().Where( p => p.Value.Type == ( byte )CharacterBase.ModelType.Human ) ) { ret[ idx ] = true; } From 84b0fc3f697ab6b590fd1cdf29a278d95e0a6098 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 17:01:26 +0100 Subject: [PATCH 0625/2451] Change aesthetician identification. --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 9b0e04b8..c825444a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -34,7 +34,7 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !Dalamud.ClientState.IsLoggedIn || Dalamud.GameGui.GetAddonByName( "_CharaMakeTitle", 1 ) != IntPtr.Zero ) + if( !Dalamud.ClientState.IsLoggedIn || Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) { var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) From 075f8bafa03d2329abcbecb3805843fef7cd33ca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Dec 2022 17:55:48 +0100 Subject: [PATCH 0626/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 02f47480..4600e29f 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -26,10 +26,20 @@ public partial class ConfigWindow Add5_11_1( ret ); Add6_0_0( ret ); Add6_0_2( ret ); + Add6_0_5( ret ); return ret; } + private static void Add6_0_5( Changelog log ) + => log.NextVersion( "Version 0.6.0.5" ) + .RegisterEntry( "Allow hyphen as last character in player and retainer names." ) + .RegisterEntry( "Fix various bugs with ownership and GPose." ) + .RegisterEntry( "Fix collection selectors not updating for new or deleted collections in some cases." ) + .RegisterEntry( "Fix Chocobos not being recognized correctly." ) + .RegisterEntry( "Fix some problems with UI actors." ) + .RegisterEntry( "Fix problems with aesthetician again." ); + private static void Add6_0_2( Changelog log ) => log.NextVersion( "Version 0.6.0.2" ) .RegisterEntry( "Let Bell Retainer collections apply to retainer-named mannequins." ) From cac601739204185bfd0979458ce46ff8a5ae6650 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Dec 2022 16:58:15 +0000 Subject: [PATCH 0627/2451] [CI] Updating repo.json for refs/tags/0.6.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 27bcc7c1..0ab72c76 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.4", - "TestingAssemblyVersion": "0.6.0.4", + "AssemblyVersion": "0.6.0.5", + "TestingAssemblyVersion": "0.6.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c800f3191fd93c8e80b96b2b3ae26d81d5158269 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Dec 2022 19:48:41 +0100 Subject: [PATCH 0628/2451] Try to use player collection during aesthetician. --- .../Resolver/PathResolver.Identification.cs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index c825444a..28d9905a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -34,24 +34,33 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !Dalamud.ClientState.IsLoggedIn || Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) + if( !Dalamud.ClientState.IsLoggedIn ) { - var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; - return IdentifiedCache.Set( collection, ActorIdentifier.Invalid, gameObject ); + return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } - else + + // Aesthetician. The relevant actor is yourself, so use player collection when possible. + if( Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) { - var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, false ); - identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); - var collection = CollectionByIdentifier( identifier ) - ?? CheckYourself( identifier, gameObject ) + var player = Penumbra.Actors.GetCurrentPlayer(); + var collection2 = ( player.IsValid ? CollectionByIdentifier( player ) : null ) + ?? Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) - ?? CheckOwnedCollection( identifier, owner ) ?? Penumbra.CollectionManager.Default; - return IdentifiedCache.Set( collection, identifier, gameObject ); + return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } + + var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, false ); + identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); + var collection = CollectionByIdentifier( identifier ) + ?? CheckYourself( identifier, gameObject ) + ?? CollectionByAttributes( gameObject ) + ?? CheckOwnedCollection( identifier, owner ) + ?? Penumbra.CollectionManager.Default; + return IdentifiedCache.Set( collection, identifier, gameObject ); } catch( Exception e ) { From b65bef17b272cde60a8e11374194af18dbbde27b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 6 Dec 2022 15:59:57 +0100 Subject: [PATCH 0629/2451] Small fixes for backup, respect export directory on load. --- OtterGui | 2 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 20 ++++++++++++------- Penumbra/Mods/Manager/Mod.Manager.cs | 1 + .../UI/ConfigWindow.SettingsTab.General.cs | 4 ++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/OtterGui b/OtterGui index 441d5f9b..1b61ce89 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 441d5f9b2943f8ab81501870314d9b1610cb3b51 +Subproject commit 1b61ce894209ebabe6cf2d8b7c120f5a6edbe86a diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 38c2fae7..a82fb88e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,4 +1,3 @@ -using ImGuizmoNET; using System; using System.IO; @@ -110,7 +109,7 @@ public sealed partial class Mod } } - public void UpdateExportDirectory( string newDirectory ) + public void UpdateExportDirectory( string newDirectory, bool change ) { if( newDirectory.Length == 0 ) { @@ -144,14 +143,21 @@ public sealed partial class Mod } } - foreach( var mod in _mods ) + if( change ) { - new ModBackup( mod ).Move( dir.FullName ); + foreach( var mod in _mods ) + { + new ModBackup( mod ).Move( dir.FullName ); + } } - _exportDirectory = dir; - Penumbra.Config.ExportDirectory = dir.FullName; - Penumbra.Config.Save(); + _exportDirectory = dir; + + if( change ) + { + Penumbra.Config.ExportDirectory = dir.FullName; + Penumbra.Config.Save(); + } } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 11f878ca..71069dbe 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -39,6 +39,7 @@ public sealed partial class Mod { ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory( modDirectory, true ); + UpdateExportDirectory( Penumbra.Config.ExportDirectory, false ); ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index a1aee0e4..826629e8 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -236,7 +236,7 @@ public partial class ConfigWindow if( ImGui.IsItemDeactivatedAfterEdit() ) { - Penumbra.ModManager.UpdateExportDirectory( _tempExportDirectory ); + Penumbra.ModManager.UpdateExportDirectory( _tempExportDirectory, true ); } ImGui.SameLine(); @@ -258,7 +258,7 @@ public partial class ConfigWindow _dialogManager.OpenFolderDialog( "Choose Default Export Directory", ( b, s ) => { - Penumbra.ModManager.UpdateExportDirectory( b ? s : Penumbra.Config.ExportDirectory ); + Penumbra.ModManager.UpdateExportDirectory( b ? s : Penumbra.Config.ExportDirectory, true ); _dialogOpen = false; }, startDir ); _dialogOpen = true; From d0ed8abab80fff5461beead3c31355a27734ce72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Dec 2022 21:32:51 +0100 Subject: [PATCH 0630/2451] Add a small hack to interpret BattleNPC as Players in some cases for Anamnesis. --- .../Actors/ActorManager.Identifiers.cs | 23 +++++++++++++++---- Penumbra/Api/PenumbraApi.cs | 2 +- .../IndividualCollections.Access.cs | 4 ++-- .../Resolver/PathResolver.Identification.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 4 ---- Penumbra/UI/ConfigWindow.DebugTab.cs | 2 +- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index ad3af449..47c03b82 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -110,7 +110,7 @@ public partial class ActorManager /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. ///
public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool check = true) + out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check) { owner = null; if (actor == null) @@ -150,6 +150,19 @@ public partial class ActorManager ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); } + + // Hack to support Anamnesis changing ObjectKind for NPC faces. + if (nameId == 0 && allowPlayerNpc) + { + var name = new ByteString(actor->Name); + if (!name.IsEmpty) + { + var homeWorld = ((Character*)actor)->HomeWorld; + return check + ? CreatePlayer(name, homeWorld) + : CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue); + } + } return check ? CreateNpc(ObjectKind.BattleNpc, nameId, actor->ObjectIndex) @@ -230,11 +243,11 @@ public partial class ActorManager } public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, - bool check = true) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, check); + bool allowPlayerNpc, bool check) + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, check); - public unsafe ActorIdentifier FromObject(GameObject? actor, bool check = true) - => FromObject(actor, out _, check); + public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check) + => FromObject(actor, out _, allowPlayerNpc, check); public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 49ad61cd..ebb09ddb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -677,7 +677,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidArgument; } - var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ] ); + var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ], false, false ); if( !identifier.IsValid ) { return PenumbraApiEc.InvalidArgument; diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 1c02c19c..669dd086 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -114,10 +114,10 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, false ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, true, false ), out collection ); public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, out _, false ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false ), out collection ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 28d9905a..6cb37665 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -53,7 +53,7 @@ public unsafe partial class PathResolver return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } - var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, false ); + var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); var collection = CollectionByIdentifier( identifier ) ?? CheckYourself( identifier, gameObject ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index aa1f9055..439040c1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,14 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using Dalamud.Data; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Lumina.Excel.GeneratedSheets; -using OtterGui; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 8036863e..4a234656 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -192,7 +192,7 @@ public partial class ConfigWindow { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); - var identifier = Penumbra.Actors.FromObject( obj, true ); + var identifier = Penumbra.Actors.FromObject( obj, false, true ); ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn( id ); From 727cf7e31318b311a0358512b2ecd2b508610df8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 8 Dec 2022 23:18:06 +0000 Subject: [PATCH 0631/2451] [CI] Updating repo.json for refs/tags/0.6.0.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 0ab72c76..5ea7f55d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.5", - "TestingAssemblyVersion": "0.6.0.5", + "AssemblyVersion": "0.6.0.6", + "TestingAssemblyVersion": "0.6.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f63903e3e6a4a7e20de582d1e097a2cb9073d3a8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Dec 2022 16:30:20 +0100 Subject: [PATCH 0632/2451] Add slots to demihuman imc. --- .../Import/TexToolsMeta.Deserialization.cs | 35 +++-------- .../Meta/Manipulations/ImcManipulation.cs | 33 +++++----- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 62 +++++++++++++++---- 3 files changed, 77 insertions(+), 53 deletions(-) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 0142e444..1494d949 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -130,37 +130,18 @@ public partial class TexToolsMeta ushort i = 0; try { - if( metaFileInfo.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) + var manip = new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, + new ImcEntry() ); + var def = new ImcFile( manip ); + var partIdx = ImcFile.PartIndex( manip.EquipSlot ); // Gets turned to unknown for things without equip, and unknown turns to 0. + foreach( var value in values ) { - var def = new ImcFile( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, new ImcEntry() ) ); - var partIdx = ImcFile.PartIndex( metaFileInfo.EquipSlot ); - foreach( var value in values ) + if( !value.Equals( def.GetEntry( partIdx, i ) ) ) { - if( !value.Equals( def.GetEntry( partIdx, i ) ) ) - { - MetaManipulations.Add( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, value ) ); - } - - ++i; + MetaManipulations.Add( manip.Copy( value ) ); } - } - else - { - var def = new ImcFile( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, - metaFileInfo.SecondaryId, i, - new ImcEntry() ) ); - foreach( var value in values ) - { - if( !value.Equals( def.GetEntry( 0, i ) ) ) - { - MetaManipulations.Add( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, - metaFileInfo.PrimaryId, - metaFileInfo.SecondaryId, i, - value ) ); - } - ++i; - } + ++i; } } catch( Exception e ) diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index ed9822a8..f2b4e45d 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -16,8 +16,8 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { public ImcEntry Entry { get; private init; } public ushort PrimaryId { get; private init; } - public ushort Variant { get; private init; } public ushort SecondaryId { get; private init; } + public byte Variant { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] public ObjectType ObjectType { get; private init; } @@ -32,25 +32,13 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { Entry = entry; PrimaryId = primaryId; - Variant = variant; + Variant = ( byte )variant; SecondaryId = 0; ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; EquipSlot = equipSlot; BodySlot = BodySlot.Unknown; } - public ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, - ImcEntry entry ) - { - Entry = entry; - ObjectType = objectType; - BodySlot = bodySlot; - SecondaryId = secondaryId; - PrimaryId = primaryId; - Variant = variant; - EquipSlot = EquipSlot.Unknown; - } - [JsonConstructor] internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry ) @@ -58,13 +46,19 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > Entry = entry; ObjectType = objectType; PrimaryId = primaryId; - Variant = variant; + Variant = ( byte )variant; if( objectType is ObjectType.Accessory or ObjectType.Equipment ) { BodySlot = BodySlot.Unknown; SecondaryId = 0; EquipSlot = equipSlot; } + else if( objectType is ObjectType.DemiHuman ) + { + BodySlot = BodySlot.Unknown; + SecondaryId = secondaryId; + EquipSlot = equipSlot; + } else { BodySlot = bodySlot; @@ -115,6 +109,15 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return e != 0 ? e : Variant.CompareTo( other.Variant ); } + if( ObjectType is ObjectType.DemiHuman ) + { + var e = EquipSlot.CompareTo( other.EquipSlot ); + if( e != 0 ) + { + return e; + } + } + var s = SecondaryId.CompareTo( other.SecondaryId ); if( s != 0 ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 8c2e8340..d1a4159f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -79,7 +79,7 @@ public partial class ModEditWindow DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew ); DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 9, ImcRow.Draw, ImcRow.DrawNew ); + DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew ); DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew ); DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew ); DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew ); @@ -143,7 +143,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); - if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) + if( EqpEquipSlotCombo( "##eqpSlot", 100, _new.Slot, out var slot ) ) { _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId ); } @@ -358,8 +358,14 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) ) { - _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, - _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); + var equipSlot = type switch + { + ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, _new.Variant, equipSlot, _new.Entry ); } ImGuiUtil.HoverTooltip( ObjectTypeTooltip ); @@ -378,9 +384,19 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if( _new.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + if( _new.ObjectType is ObjectType.Equipment ) { - if( EqdpEquipSlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) + if( EqpEquipSlotCombo( "##imcSlot", 100, _new.EquipSlot, out var slot ) ) + { + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); + } + + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); + } + else if( _new.ObjectType is ObjectType.Accessory ) + { + if( AccessorySlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -406,6 +422,22 @@ public partial class ModEditWindow ?? new ImcEntry() ); } + ImGui.TableNextColumn(); + if( _new.ObjectType is ObjectType.DemiHuman ) + { + if( EqpEquipSlotCombo( "##imcSlot", 70, _new.EquipSlot, out var slot ) ) + { + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); + } + + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); + } + else + { + ImGui.Dummy( new Vector2( 70 * ImGuiHelpers.GlobalScale, 0 ) ); + } + ImGuiUtil.HoverTooltip( VariantIdTooltip ); // Values @@ -470,6 +502,13 @@ public partial class ModEditWindow ImGui.TextUnformatted( meta.Variant.ToString() ); ImGuiUtil.HoverTooltip( VariantIdTooltip ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + if( meta.ObjectType is ObjectType.DemiHuman ) + { + ImGui.TextUnformatted( meta.EquipSlot.ToName() ); + } + // Values using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); @@ -827,12 +866,13 @@ public partial class ModEditWindow => ImGuiUtil.GenericEnumCombo( label, 120 * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); private static bool EqdpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, - EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); - private static bool EqpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, - EquipSlotExtensions.ToName ); + private static bool EqpEquipSlotCombo( string label, float width, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); + + private static bool AccessorySlotCombo( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); private static bool SubRaceCombo( string label, SubRace current, out SubRace subRace ) => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); From dc493268f8f199eb8006f3711234efd24fbddfe7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 Dec 2022 15:18:29 +0100 Subject: [PATCH 0633/2451] Add CopyModSetting API. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 7 +++ Penumbra/Api/PenumbraApi.cs | 25 +++++++++++ Penumbra/Api/PenumbraIpcProviders.cs | 5 ++- Penumbra/Collections/ModCollection.cs | 62 ++++++++++++++++++++++++++- Penumbra/Mods/Manager/Mod.Manager.cs | 2 +- 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 891eb195..36a06e50 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 891eb195c29904824004c45f84b92a9e1dd98ddf +Subproject commit 36a06e509bf0a7023b4c9b148b06f71f8116cc81 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index fea10f95..1b4c1d1d 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1084,6 +1084,13 @@ public class IpcTester : IDisposable _lastSettingsError = Ipc.TrySetModPriority.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); } + DrawIntro( Ipc.CopyModSettings.Label, "Copy Mod Settings" ); + if( ImGui.Button( "Copy Settings" ) ) + { + _lastSettingsError = Ipc.CopyModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName ); + } + ImGuiUtil.HoverTooltip( "Copy settings from Mod Directory Name to Mod Name (as directory) in collection." ); + DrawIntro( Ipc.TrySetModSetting.Label, "Set Setting(s)" ); if( _availableSettings == null ) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index ebb09ddb..54be2b27 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -618,6 +618,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi } + public PenumbraApiEc CopyModSettings( string? collectionName, string modDirectoryFrom, string modDirectoryTo ) + { + CheckInitialized(); + + var sourceModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; + var targetModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; + if( string.IsNullOrEmpty( collectionName ) ) + { + foreach( var collection in Penumbra.CollectionManager ) + { + collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); + } + } + else if( Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); + } + else + { + return PenumbraApiEc.CollectionMissing; + } + + return PenumbraApiEc.Success; + } + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 69db9c99..352edfcf 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -91,6 +91,7 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, string, string, string, string, PenumbraApiEc > TrySetModSetting; internal readonly FuncProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > TrySetModSettings; internal readonly EventProvider< ModSettingChange, string, string, bool > ModSettingChanged; + internal readonly FuncProvider< string, string, string, PenumbraApiEc > CopyModSettings; // Temporary internal readonly FuncProvider< string, string, bool, (PenumbraApiEc, string) > CreateTemporaryCollection; @@ -188,9 +189,10 @@ public class PenumbraIpcProviders : IDisposable TrySetModPriority = Ipc.TrySetModPriority.Provider( pi, Api.TrySetModPriority ); TrySetModSetting = Ipc.TrySetModSetting.Provider( pi, Api.TrySetModSetting ); TrySetModSettings = Ipc.TrySetModSettings.Provider( pi, Api.TrySetModSettings ); - ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, + ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, () => Api.ModSettingChanged += ModSettingChangedEvent, () => Api.ModSettingChanged -= ModSettingChangedEvent ); + CopyModSettings = Ipc.CopyModSettings.Provider( pi, Api.CopyModSettings ); // Temporary CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider( pi, Api.CreateTemporaryCollection ); @@ -287,6 +289,7 @@ public class PenumbraIpcProviders : IDisposable TrySetModSetting.Dispose(); TrySetModSettings.Dispose(); ModSettingChanged.Dispose(); + CopyModSettings.Dispose(); // Temporary CreateTemporaryCollection.Dispose(); diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 0b165b1f..2b7a8605 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -3,6 +3,7 @@ using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; +using OtterGui; namespace Penumbra.Collections; @@ -101,7 +102,7 @@ public partial class ModCollection // Check if a name is valid to use for a collection. // Does not check for uniqueness. public static bool IsValidName( string name ) - => name.Length > 0 && name.All( c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath() ); + => name.Length > 0 && name.All( c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath() ); // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() @@ -135,7 +136,7 @@ public partial class ModCollection var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings( settings, mod ); + _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( settings, mod ); } _settings.RemoveAt( idx ); @@ -173,6 +174,63 @@ public partial class ModCollection } } + public bool CopyModSettings( int modIdx, string modName, int targetIdx, string targetName ) + { + if( targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0 ) + { + return false; + } + + // If the source mod exists, convert its settings to saved settings or null if its inheriting. + // If it does not exist, check unused settings. + // If it does not exist and has no unused settings, also use null. + ModSettings.SavedSettings? savedSettings = modIdx >= 0 + ? _settings[ modIdx ] != null + ? new ModSettings.SavedSettings( _settings[ modIdx ]!, Penumbra.ModManager[ modIdx ] ) + : null + : _unusedSettings.TryGetValue( modName, out var s ) + ? s + : null; + + if( targetIdx >= 0 ) + { + if( savedSettings != null ) + { + // The target mod exists and the source settings are not inheriting, convert and fix the settings and copy them. + // This triggers multiple events. + savedSettings.Value.ToSettings( Penumbra.ModManager[ targetIdx ], out var settings ); + SetModState( targetIdx, settings.Enabled ); + SetModPriority( targetIdx, settings.Priority ); + foreach( var (value, index) in settings.Settings.WithIndex() ) + { + SetModSetting( targetIdx, index, value ); + } + } + else + { + // The target mod exists, but the source is inheriting, set the target to inheriting. + // This triggers events. + SetModInheritance( targetIdx, true ); + } + } + else + { + // The target mod does not exist. + // Either copy the unused source settings directly if they are not inheriting, + // or remove any unused settings for the target if they are inheriting. + if( savedSettings != null ) + { + _unusedSettings[ targetName ] = savedSettings.Value; + } + else + { + _unusedSettings.Remove( targetName ); + } + } + + return true; + } + public override string ToString() => Name; } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 71069dbe..55ef005f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -52,7 +52,7 @@ public sealed partial class Mod mod = null; foreach( var m in _mods ) { - if( m.ModPath.Name == modDirectory ) + if( string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase) ) { mod = m; return true; From 1ae96c71a36350f19080938d2de87f9944a7e964 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 Dec 2022 23:53:35 +0100 Subject: [PATCH 0634/2451] Use CreateIndividualCollection instead of Add in interface to trigger events. --- Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 16ae8253..39ab6a6c 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -179,7 +179,7 @@ public partial class ConfigWindow ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( "Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0 ) ) { - Penumbra.CollectionManager.Individuals.Add( _newPlayerIdentifiers, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( _newPlayerIdentifiers ); change = true; } @@ -196,7 +196,7 @@ public partial class ConfigWindow ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( "Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0 ) ) { - Penumbra.CollectionManager.Individuals.Add( _newNpcIdentifiers, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( _newNpcIdentifiers ); change = true; } @@ -207,7 +207,7 @@ public partial class ConfigWindow { if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) { - Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( _newOwnedIdentifiers ); return true; } @@ -218,7 +218,7 @@ public partial class ConfigWindow { if( ImGuiUtil.DrawDisabledButton( "Assign Bell Retainer", buttonWidth, _newRetainerTooltip, _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0 ) ) { - Penumbra.CollectionManager.Individuals.Add( _newRetainerIdentifiers, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( _newRetainerIdentifiers ); return true; } From 6f356105cc8458c754f14a8d863c327a2cd370d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Dec 2022 20:17:18 +0100 Subject: [PATCH 0635/2451] Add better chat command handling, help, and option to set basic mod state. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 9 + .../Actors/ActorManager.Identifiers.cs | 143 +++++- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionType.cs | 46 +- Penumbra/Collections/IndividualCollections.cs | 10 +- Penumbra/CommandHandler.cs | 474 ++++++++++++++++++ Penumbra/Penumbra.cs | 262 ++-------- 7 files changed, 719 insertions(+), 227 deletions(-) create mode 100644 Penumbra/CommandHandler.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 3d962d0e..77bd13bf 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -60,6 +60,15 @@ public sealed partial class ActorManager : IDisposable public string ToWorldName(ushort worldId) => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; + /// + /// Return the world id corresponding to the given name. + /// + /// ushort.MaxValue if the name is empty, 0 if it is not a valid world, or the worlds id. + public ushort ToWorldId(string worldName) + => worldName.Length != 0 + ? Worlds.FirstOrDefault(kvp => string.Equals(kvp.Value, worldName, StringComparison.OrdinalIgnoreCase), default).Key + : ushort.MaxValue; + /// /// Convert a given ID for a certain ObjectKind to a name. /// diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 47c03b82..2483fc5e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -1,5 +1,6 @@ using System; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Newtonsoft.Json.Linq; @@ -106,6 +107,139 @@ public partial class ActorManager return main; } + public class IdentifierParseError : Exception + { + public IdentifierParseError(string reason) + : base(reason) + { } + } + + public ActorIdentifier FromUserString(string userString) + { + if (userString.Length == 0) + throw new IdentifierParseError("The identifier string was empty."); + + var split = userString.Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 2) + throw new IdentifierParseError($"The identifier string {userString} does not contain a type and a value."); + + var type = IdentifierType.Invalid; + var playerName = ByteString.Empty; + ushort worldId = 0; + var kind = ObjectKind.Player; + var objectId = 0u; + + (ByteString, ushort) ParsePlayer(string player) + { + var parts = player.Split('@', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (!VerifyPlayerName(parts[0])) + throw new IdentifierParseError($"{parts[0]} is not a valid player name."); + if (!ByteString.FromString(parts[0], out var p, false)) + throw new IdentifierParseError($"The player string {parts[0]} contains invalid symbols."); + + var world = parts.Length == 2 + ? Data.ToWorldId(parts[1]) + : ushort.MaxValue; + + if (!VerifyWorld(world)) + throw new IdentifierParseError($"{parts[1]} is not a valid world name."); + + return (p, world); + } + + (ObjectKind, uint) ParseNpc(string npc) + { + var split = npc.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 2) + throw new IdentifierParseError("NPCs need to be specified by '[Object Type]:[NPC Name]'."); + + static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + switch (split[0].ToLowerInvariant()) + { + case "m": + case "mount": + return FindDataId(split[1], Data.Mounts, out var id) + ? (ObjectKind.MountType, id) + : throw new IdentifierParseError($"Could not identify a Mount named {split[1]}."); + case "c": + case "companion": + case "minion": + case "mini": + return FindDataId(split[1], Data.Companions, out id) + ? (ObjectKind.Companion, id) + : throw new IdentifierParseError($"Could not identify a Minion named {split[1]}."); + case "a": + case "o": + case "accessory": + case "ornament": + // TODO: Objectkind ornament. + return FindDataId(split[1], Data.Ornaments, out id) + ? ((ObjectKind)15, id) + : throw new IdentifierParseError($"Could not identify an Accessory named {split[1]}."); + case "e": + case "enpc": + case "eventnpc": + case "event npc": + return FindDataId(split[1], Data.ENpcs, out id) + ? (ObjectKind.EventNpc, id) + : throw new IdentifierParseError($"Could not identify an Event NPC named {split[1]}."); + case "b": + case "bnpc": + case "battlenpc": + case "battle npc": + return FindDataId(split[1], Data.BNpcs, out id) + ? (ObjectKind.BattleNpc, id) + : throw new IdentifierParseError($"Could not identify a Battle NPC named {split[1]}."); + default: + throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type."); + } + } + + switch (split[0].ToLowerInvariant()) + { + case "p": + case "player": + type = IdentifierType.Player; + (playerName, worldId) = ParsePlayer(split[1]); + break; + case "r": + case "retainer": + type = IdentifierType.Retainer; + if (!VerifyRetainerName(split[1])) + throw new IdentifierParseError($"{split[1]} is not a valid player name."); + if (!ByteString.FromString(split[1], out playerName, false)) + throw new IdentifierParseError($"The retainer string {split[1]} contains invalid symbols."); + + break; + case "n": + case "npc": + type = IdentifierType.Npc; + (kind, objectId) = ParseNpc(split[1]); + break; + case "o": + case "owned": + if (split.Length < 3) + throw new IdentifierParseError( + "Owned NPCs need a NPC and a player, separated by '|', but only one was provided."); + type = IdentifierType.Owned; + (kind, objectId) = ParseNpc(split[1]); + (playerName, worldId) = ParsePlayer(split[2]); + break; + default: + throw new IdentifierParseError( + $"{split[0]} is not a valid identifier type. Valid types are [P]layer, [R]etainer, [N]PC, or [O]wned"); + } + + return CreateIndividualUnchecked(type, playerName, worldId, kind, objectId); + } + /// /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// @@ -150,11 +284,11 @@ public partial class ActorManager ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); } - + // Hack to support Anamnesis changing ObjectKind for NPC faces. if (nameId == 0 && allowPlayerNpc) { - var name = new ByteString(actor->Name); + var name = new ByteString(actor->Name); if (!name.IsEmpty) { var homeWorld = ((Character*)actor)->HomeWorld; @@ -244,7 +378,8 @@ public partial class ActorManager public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, check); + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, + check); public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check) => FromObject(actor, out _, allowPlayerNpc, check); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 355c9297..79c6e83a 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -64,7 +64,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : null, _ => null, }; } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 30da217d..eab5d6eb 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -213,6 +213,50 @@ public static class CollectionTypeExtensions }; } + public static bool TryParse( string text, out CollectionType type ) + { + if( Enum.TryParse( text, true, out type ) ) + return type is not CollectionType.Inactive and not CollectionType.Temporary; + + if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Individual; + return true; + } + + if( string.Equals( text, "base", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Default; + return true; + } + + if( string.Equals( text, "ui", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Interface; + return true; + } + + if( string.Equals( text, "selected", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Current; + return true; + } + + foreach( var t in Enum.GetValues< CollectionType >() ) + { + if( t is CollectionType.Inactive or CollectionType.Temporary ) + continue; + + if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) ) + { + type = t; + return true; + } + } + + return false; + } + public static string ToName( this CollectionType collectionType ) => collectionType switch { @@ -288,7 +332,7 @@ public static class CollectionTypeExtensions CollectionType.Inactive => "Collection", CollectionType.Default => "Default", CollectionType.Interface => "Interface", - CollectionType.Individual => "Character", + CollectionType.Individual => "Individual", CollectionType.Current => "Current", _ => string.Empty, }; diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index d2d8bb70..3d5f3fdd 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -177,10 +177,10 @@ public sealed partial class IndividualCollections } internal bool Delete( ActorIdentifier identifier ) - => Delete( DisplayString( identifier ) ); + => Delete( Index( identifier ) ); internal bool Delete( string displayName ) - => Delete( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) ); + => Delete( Index( displayName ) ); internal bool Delete( int displayIndex ) { @@ -202,6 +202,12 @@ public sealed partial class IndividualCollections internal bool Move( int from, int to ) => _assignments.Move( from, to ); + internal int Index( string displayName ) + => _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ); + + internal int Index( ActorIdentifier identifier ) + => identifier.IsValid ? Index( DisplayString( identifier ) ) : -1; + private string DisplayString( ActorIdentifier identifier ) { return identifier.Type switch diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs new file mode 100644 index 00000000..2669a9f5 --- /dev/null +++ b/Penumbra/CommandHandler.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using ImGuiNET; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Interop; +using Penumbra.Mods; +using Penumbra.UI; + +namespace Penumbra; + +public static class SeStringBuilderExtensions +{ + public const ushort Green = 504; + public const ushort Yellow = 31; + public const ushort Red = 534; + public const ushort Blue = 517; + public const ushort White = 1; + public const ushort Purple = 541; + + public static SeStringBuilder AddText( this SeStringBuilder sb, string text, int color, bool brackets = false ) + => sb.AddUiForeground( ( ushort )color ).AddText( brackets ? $"[{text}]" : text ).AddUiForegroundOff(); + + public static SeStringBuilder AddGreen( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Green, brackets ); + + public static SeStringBuilder AddYellow( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Yellow, brackets ); + + public static SeStringBuilder AddRed( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Red, brackets ); + + public static SeStringBuilder AddBlue( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Blue, brackets ); + + public static SeStringBuilder AddWhite( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, White, brackets ); + + public static SeStringBuilder AddPurple( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Purple, brackets ); + + public static SeStringBuilder AddCommand( this SeStringBuilder sb, string command, string description ) + => sb.AddText( " 》 " ) + .AddBlue( command ) + .AddText( $" - {description}" ); + + public static SeStringBuilder AddInitialPurple( this SeStringBuilder sb, string word, bool withComma = true ) + => sb.AddPurple( $"[{word[ 0 ]}]" ) + .AddText( withComma ? $"{word[ 1.. ]}, " : word[ 1.. ] ); +} + +public class CommandHandler : IDisposable +{ + private const string CommandName = "/penumbra"; + + private readonly CommandManager _commandManager; + private readonly ObjectReloader _objectReloader; + private readonly Configuration _config; + private readonly Penumbra _penumbra; + private readonly ConfigWindow _configWindow; + private readonly ActorManager _actors; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + + public CommandHandler( CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, Mod.Manager modManager, + ModCollection.Manager collectionManager, ActorManager actors ) + { + _commandManager = commandManager; + _objectReloader = objectReloader; + _config = config; + _penumbra = penumbra; + _configWindow = configWindow; + _modManager = modManager; + _collectionManager = collectionManager; + _actors = actors; + _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) ); + } + + public void Dispose() + { + _commandManager.RemoveHandler( CommandName ); + } + + private void OnCommand( string command, string arguments ) + { + if( arguments.Length == 0 ) + { + arguments = "window"; + } + + var argumentList = arguments.Split( ' ', 2 ); + arguments = argumentList.Length == 2 ? argumentList[ 1 ] : string.Empty; + + var _ = argumentList[ 0 ].ToLowerInvariant() switch + { + "window" => ToggleWindow( arguments ), + "enable" => SetPenumbraState( arguments, true ), + "disable" => SetPenumbraState( arguments, false ), + "toggle" => SetPenumbraState( arguments, null ), + "reload" => Reload( arguments ), + "redraw" => Redraw( arguments ), + "lockui" => SetUiLockState( arguments ), + "debug" => SetDebug( arguments ), + "collection" => SetCollection( arguments ), + "mod" => SetMod( arguments ), + _ => PrintHelp( argumentList[ 0 ] ), + }; + } + + private static bool PrintHelp( string arguments ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "window", + "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state." ) + .BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) + .BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); + return true; + } + + private bool ToggleWindow( string arguments ) + { + var value = ParseTrueFalseToggle( arguments ) ?? !_configWindow.IsOpen; + if( value == _configWindow.IsOpen ) + { + return false; + } + + _configWindow.Toggle(); + return true; + } + + private bool Reload( string _ ) + { + _modManager.DiscoverMods(); + Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); + return true; + } + + private bool Redraw( string arguments ) + { + if( arguments.Length > 0 ) + { + _objectReloader.RedrawObject( arguments, RedrawType.Redraw ); + } + else + { + _objectReloader.RedrawAll( RedrawType.Redraw ); + } + + return true; + } + + private bool SetDebug( string arguments ) + { + var value = ParseTrueFalseToggle( arguments ) ?? !_config.DebugMode; + if( value == _config.DebugMode ) + { + return false; + } + + Dalamud.Chat.Print( value + ? "Debug mode enabled." + : "Debug mode disabled." ); + + _config.DebugMode = value; + _config.Save(); + return true; + } + + private bool SetPenumbraState( string _, bool? newValue ) + { + var value = newValue ?? !_config.EnableMods; + + if( value == _config.EnableMods ) + { + Dalamud.Chat.Print( value + ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" + : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); + return false; + } + + Dalamud.Chat.Print( value + ? "Your mods have been enabled." + : "Your mods have been disabled." ); + return _penumbra.SetEnabled( value ); + } + + private bool SetUiLockState( string arguments ) + { + var value = ParseTrueFalseToggle( arguments ) ?? !_config.FixMainWindow; + if( value == _config.FixMainWindow ) + { + return false; + } + + if( value ) + { + Dalamud.Chat.Print( "Penumbra UI locked in place." ); + _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + } + else + { + Dalamud.Chat.Print( "Penumbra UI unlocked." ); + _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + } + + _config.FixMainWindow = value; + _config.Save(); + return true; + } + + private bool SetCollection( string arguments ) + { + if( arguments.Length == 0 ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) + .AddText( " | " ).AddGreen( "" ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Types are " ).AddBlue( "Base" ).AddText( ", " ).AddBlue( "Ui" ).AddText( ", " ) + .AddBlue( "Selected" ).AddText( ", " ) + .AddBlue( "Individual" ).AddText( ", and all those selectable in Character Groups." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Names are " ).AddYellow( "None" ) + .AddText( ", all collections you have created by their full names, and " ).AddYellow( "Delete" ).AddText( " to remove assignments (not valid for all types)." ) + .BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) + .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) + .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "n" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) + .AddRed( "[NPC Name]" ).AddText( ", where NPC Type can be " ).AddInitialPurple( "Mount" ).AddInitialPurple( "Companion" ).AddInitialPurple( "Accessory" ) + .AddInitialPurple( "Event NPC" ).AddText( "or " ) + .AddInitialPurple( "Battle NPC", false ).AddText( "." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "o" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) + .AddRed( "[NPC Name]" ).AddText( " | " ).AddWhite( "[Player Name]@" ).AddText( "." ).BuiltString ); + return true; + } + + var split = arguments.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); + var typeName = split[ 0 ]; + + if( !CollectionTypeExtensions.TryParse( typeName, out var type ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); + return false; + } + + if( split.Length == 1 ) + { + Dalamud.Chat.Print( "There was no collection name provided." ); + return false; + } + + if( !GetModCollection( split[ 1 ], out var collection ) ) + { + return false; + } + + var identifier = ActorIdentifier.Invalid; + if( type is CollectionType.Individual ) + { + if( split.Length == 2 ) + { + Dalamud.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); + return false; + } + + try + { + identifier = _actors.FromUserString( split[ 2 ] ); + } + catch( ActorManager.IdentifierParseError e ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( split[ 2 ], true ).AddText( $" could not be converted to an identifier. {e.Message}" ) + .BuiltString ); + return false; + } + } + + var oldCollection = _collectionManager.ByType( type, identifier ); + if( collection == oldCollection ) + { + Dalamud.Chat.Print( collection == null + ? $"The {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}" : string.Empty )} is already unassigned" + : $"{collection.Name} already is the {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return false; + } + + var individualIndex = _collectionManager.Individuals.Index( identifier ); + + if( oldCollection == null ) + { + if( type.IsSpecial() ) + { + _collectionManager.CreateSpecialCollection( type ); + } + else if( identifier.IsValid ) + { + var identifiers = _collectionManager.Individuals.GetGroup( identifier ); + individualIndex = _collectionManager.Individuals.Count; + _collectionManager.CreateIndividualCollection( identifiers ); + } + } + else if( collection == null ) + { + if( type.IsSpecial() ) + { + _collectionManager.RemoveSpecialCollection( type ); + } + else if( individualIndex >= 0 ) + { + _collectionManager.RemoveIndividualCollection( individualIndex ); + } + else + { + Dalamud.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return false; + } + + Dalamud.Chat.Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return true; + } + + _collectionManager.SetCollection( collection!, type, individualIndex ); + Dalamud.Chat.Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return true; + } + + private bool SetMod( string arguments ) + { + if( arguments.Length == 0 ) + { + var seString = new SeStringBuilder() + .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddYellow( "[Collection Name]" ).AddText( " | " ) + .AddPurple( "[Mod Name or Mod Directory Name]" ); + Dalamud.Chat.Print( seString.BuiltString ); + return true; + } + + var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + if( nameSplit.Length != 2 ) + { + Dalamud.Chat.Print( "Not enough arguments provided." ); + return false; + } + + var state = split[ 0 ].ToLowerInvariant() switch + { + "enable" => 0, + "enabled" => 0, + "disable" => 1, + "disabled" => 1, + "toggle" => 2, + "inherit" => 3, + "inherited" => 3, + _ => -1, + }; + if( state == -1 ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + return false; + } + + if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) + { + return false; + } + + if( !_modManager.TryGetMod( nameSplit[ 1 ], nameSplit[ 1 ], out var mod ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); + return false; + } + + var settings = collection.Settings[ mod.Index ]; + switch( state ) + { + case 0: + if( collection.SetModState( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 1: + if( collection.SetModState( mod.Index, false ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 2: + var setting = !( settings?.Enabled ?? false ); + if( collection.SetModState( mod.Index, setting ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 3: + if( collection.SetModInheritance( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( " to inherit." ).BuiltString ); + return true; + } + + break; + } + + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) + .AddYellow( collection.Name, true ).AddText( "." ).BuiltString ); + return false; + } + + private bool GetModCollection( string collectionName, out ModCollection? collection ) + { + var lowerName = collectionName.ToLowerInvariant(); + if( lowerName == "delete" ) + { + collection = null; + return true; + } + + collection = string.Equals( lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) + ? ModCollection.Empty + : _collectionManager[ lowerName ]; + if( collection == null ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); + return false; + } + + return true; + } + + private static bool? ParseTrueFalseToggle( string value ) + => value.ToLowerInvariant() switch + { + "0" => false, + "false" => false, + "off" => false, + "disable" => false, + "disabled" => false, + + "1" => true, + "true" => true, + "on" => true, + "enable" => true, + "enabled" => true, + + _ => null, + }; +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4bc7f694..25ad785c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; -using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using EmbedIO; @@ -26,7 +25,6 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; -using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -41,8 +39,6 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - private const string CommandName = "/penumbra"; - public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; public static readonly string CommitHash = @@ -80,6 +76,7 @@ public class Penumbra : IDalamudPlugin private readonly LaunchButton _launchButton; private readonly WindowSystem _windowSystem; private readonly Changelog _changelog; + private readonly CommandHandler _commandHandler; internal WebServer? WebServer; @@ -116,12 +113,8 @@ public class Penumbra : IDalamudPlugin ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) - { - HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", - } ); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem, out _changelog ); + _commandHandler = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, _configWindow, ModManager, CollectionManager, Actors ); if( Config.EnableMods ) { @@ -203,54 +196,42 @@ public class Penumbra : IDalamudPlugin public event Action< bool >? EnabledChange; - public bool Enable() - { - if( Config.EnableMods ) - { - return false; - } - - Config.EnableMods = true; - ResourceLoader.EnableReplacements(); - PathResolver.Enable(); - Config.Save(); - if( CharacterUtility.Ready ) - { - CollectionManager.Default.SetFiles(); - ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - EnabledChange?.Invoke( true ); - - return true; - } - - public bool Disable() - { - if( !Config.EnableMods ) - { - return false; - } - - Config.EnableMods = false; - ResourceLoader.DisableReplacements(); - PathResolver.Disable(); - Config.Save(); - if( CharacterUtility.Ready ) - { - CharacterUtility.ResetAll(); - ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - EnabledChange?.Invoke( false ); - - return true; - } - public bool SetEnabled( bool enabled ) - => enabled ? Enable() : Disable(); + { + if( enabled == Config.EnableMods ) + { + return false; + } + + Config.EnableMods = enabled; + if( enabled ) + { + ResourceLoader.EnableReplacements(); + PathResolver.Enable(); + if( CharacterUtility.Ready ) + { + CollectionManager.Default.SetFiles(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + else + { + ResourceLoader.DisableReplacements(); + PathResolver.Disable(); + if( CharacterUtility.Ready ) + { + CharacterUtility.ResetAll(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + + Config.Save(); + EnabledChange?.Invoke( enabled ); + + return true; + } public void ForceChangelogOpen() => _changelog.ForceOpen = true; @@ -303,181 +284,24 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + ShutdownWebServer(); + IpcProviders?.Dispose(); + Api?.Dispose(); + _commandHandler?.Dispose(); StainManager?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); - ShutdownWebServer(); DisposeInterface(); - IpcProviders?.Dispose(); - Api?.Dispose(); ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); - - Dalamud.Commands.RemoveHandler( CommandName ); - PathResolver?.Dispose(); ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); } - public static bool SetCollection( string typeName, string collectionName ) - { - if( !Enum.TryParse< CollectionType >( typeName, true, out var type ) || type == CollectionType.Inactive ) - { - Dalamud.Chat.Print( - "Second command argument is not a valid collection type, the correct command format is: /penumbra collection [| characterName]" ); - return false; - } - - string? characterName = null; - var identifier = ActorIdentifier.Invalid; - if( type is CollectionType.Individual ) - { - var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); - if( split.Length < 2 || split[ 0 ].Length == 0 || split[ 1 ].Length == 0 ) - { - Dalamud.Chat.Print( "You need to provide a collection and a character name in the form of 'collection | name' to set an individual collection." ); - return false; - } - - collectionName = split[ 0 ]; - characterName = split[ 1 ]; - - identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); - if( !identifier.IsValid ) - { - Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); - return false; - } - } - - collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) - ? ModCollection.Empty - : CollectionManager[ collectionName ]; - if( collection == null ) - { - Dalamud.Chat.Print( $"The collection {collection} does not exist." ); - return false; - } - - var oldCollection = CollectionManager.ByType( type, identifier ); - if( collection == oldCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); - return false; - } - - if( oldCollection == null ) - { - if( type.IsSpecial() ) - { - CollectionManager.CreateSpecialCollection( type ); - } - else if( type is CollectionType.Individual ) - { - CollectionManager.CreateIndividualCollection( identifier ); - } - } - - CollectionManager.SetCollection( collection, type, CollectionManager.Individuals.Count - 1 ); - Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); - return true; - } - - private void OnCommand( string command, string rawArgs ) - { - const string modsEnabled = "Your mods have now been enabled."; - const string modsDisabled = "Your mods have now been disabled."; - - var args = rawArgs.Split( new[] { ' ' }, 2 ); - if( args.Length > 0 && args[ 0 ].Length > 0 ) - { - switch( args[ 0 ] ) - { - case "reload": - { - ModManager.DiscoverMods(); - Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Count} mods." - ); - break; - } - case "redraw": - { - if( args.Length > 1 ) - { - ObjectReloader.RedrawObject( args[ 1 ], RedrawType.Redraw ); - } - else - { - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - break; - } - case "debug": - { - Config.DebugMode = true; - Config.Save(); - break; - } - case "enable": - { - Dalamud.Chat.Print( Enable() - ? modsEnabled - : "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" ); - break; - } - case "disable": - { - Dalamud.Chat.Print( Disable() - ? modsDisabled - : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); - break; - } - case "toggle": - { - SetEnabled( !Config.EnableMods ); - Dalamud.Chat.Print( Config.EnableMods - ? modsEnabled - : modsDisabled ); - break; - } - case "unfix": - { - Config.FixMainWindow = false; - _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); - break; - } - case "collection": - { - if( args.Length == 2 ) - { - args = args[ 1 ].Split( new[] { ' ' }, 2 ); - if( args.Length == 2 ) - { - SetCollection( args[ 0 ], args[ 1 ] ); - } - } - else - { - Dalamud.Chat.Print( "Missing arguments, the correct command format is:" - + " /penumbra collection {default} [|characterName]" ); - } - - break; - } - } - - return; - } - - _configWindow.Toggle(); - } - // Collect all relevant files for penumbra configuration. private static IReadOnlyList< FileInfo > PenumbraBackupFiles() { @@ -568,7 +392,7 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var dir = new DirectoryInfo( path ); try { @@ -589,7 +413,7 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; if( !ret ) { Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); From b40de0e125d0cd126e25be73f3663a7c401a318c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Dec 2022 21:09:24 +0100 Subject: [PATCH 0636/2451] Improve chat command help a bit. --- Penumbra/CommandHandler.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 2669a9f5..2498fa9d 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -77,7 +77,11 @@ public class CommandHandler : IDisposable _modManager = modManager; _collectionManager = collectionManager; _actors = actors; - _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) ); + _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) + { + HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", + ShowInHelp = true, + } ); } public void Dispose() @@ -113,7 +117,15 @@ public class CommandHandler : IDisposable private static bool PrintHelp( string arguments ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); + if( !string.Equals( arguments, "help", StringComparison.OrdinalIgnoreCase ) && arguments == "?" ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); + } + else + { + Dalamud.Chat.Print( "Valid arguments for /penumbra are:" ); + } + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "window", "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided." ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); @@ -343,7 +355,7 @@ public class CommandHandler : IDisposable if( arguments.Length == 0 ) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddYellow( "[Collection Name]" ).AddText( " | " ) + .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) .AddPurple( "[Mod Name or Mod Directory Name]" ); Dalamud.Chat.Print( seString.BuiltString ); return true; From 6a3d214e15830986eed8a0537de09ea414069aab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Dec 2022 21:09:36 +0100 Subject: [PATCH 0637/2451] Consider manipulations in changed items. --- Penumbra/Mods/Mod.ChangedItems.cs | 71 ++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index 549422a1..7c066681 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -1,5 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; @@ -16,7 +20,72 @@ public sealed partial class Mod Penumbra.Identifier.Identify( ChangedItems, gamePath.ToString() ); } - // TODO: manipulations + foreach( var manip in AllManipulations ) + { + ComputeChangedItems( ChangedItems, manip ); + } + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); } + + public static void ComputeChangedItems( SortedList< string, object? > changedItems, MetaManipulation manip ) + { + switch( manip.ManipulationType ) + { + case MetaManipulation.Type.Imc: + switch( manip.Imc.ObjectType ) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + Penumbra.Identifier.Identify( changedItems, + GamePaths.Equipment.Mtrl.Path( manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, "a" ) ); + break; + case ObjectType.Weapon: + Penumbra.Identifier.Identify( changedItems, GamePaths.Weapon.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a" ) ); + break; + case ObjectType.DemiHuman: + Penumbra.Identifier.Identify( changedItems, + GamePaths.DemiHuman.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, "a" ) ); + break; + case ObjectType.Monster: + Penumbra.Identifier.Identify( changedItems, GamePaths.Monster.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a" ) ); + break; + } + + break; + case MetaManipulation.Type.Eqdp: + Penumbra.Identifier.Identify( changedItems, + GamePaths.Equipment.Mdl.Path( manip.Eqdp.SetId, Names.CombinedRace( manip.Eqdp.Gender, manip.Eqdp.Race ), manip.Eqdp.Slot ) ); + break; + case MetaManipulation.Type.Eqp: + Penumbra.Identifier.Identify( changedItems, GamePaths.Equipment.Mdl.Path( manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot ) ); + break; + case MetaManipulation.Type.Est: + switch( manip.Est.Slot ) + { + case EstManipulation.EstType.Hair: + changedItems.TryAdd( $"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null ); + break; + case EstManipulation.EstType.Face: + changedItems.TryAdd( $"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null ); + break; + case EstManipulation.EstType.Body: + Penumbra.Identifier.Identify( changedItems, + GamePaths.Equipment.Mdl.Path( manip.Est.SetId, Names.CombinedRace( manip.Est.Gender, manip.Est.Race ), EquipSlot.Body ) ); + break; + case EstManipulation.EstType.Head: + Penumbra.Identifier.Identify( changedItems, + GamePaths.Equipment.Mdl.Path( manip.Est.SetId, Names.CombinedRace( manip.Est.Gender, manip.Est.Race ), EquipSlot.Head ) ); + break; + } + + break; + case MetaManipulation.Type.Gmp: + Penumbra.Identifier.Identify( changedItems, GamePaths.Equipment.Mdl.Path( manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head ) ); + break; + case MetaManipulation.Type.Rsp: + changedItems.TryAdd( $"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null ); + break; + } + } } \ No newline at end of file From 347e4b202372617f78bf9c0d68ff38ffb1f2b63c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Dec 2022 21:18:37 +0100 Subject: [PATCH 0638/2451] Add changed item for meta manipulations to effective changes. --- Penumbra/Collections/ModCollection.Cache.cs | 27 ++++++++++++++++----- Penumbra/CommandHandler.cs | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 055d9f43..46d049e8 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -474,25 +474,40 @@ public partial class ModCollection // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = Penumbra.Identifier; - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) + var items = new SortedList< string, object? >(512); + + void AddItems( IMod mod ) { - foreach( var (name, obj) in identifier.Identify( resolved.ToString() ) ) + foreach( var (name, obj) in items ) { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< IMod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); } - else if( !data.Item1.Contains( modPath.Mod ) ) + else if( !data.Item1.Contains( mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); } else if( obj is int x && data.Item2 is int y ) { _changedItems[ name ] = ( data.Item1, x + y ); } } + + items.Clear(); + } + + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) + { + identifier.Identify( items, resolved.ToString() ); + AddItems( modPath.Mod ); + } + + foreach( var (manip, mod) in MetaManipulations ) + { + Mod.ComputeChangedItems( items, manip ); + AddItems( mod ); } - // TODO: Meta Manipulations } catch( Exception e ) { diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 2498fa9d..3ab97d9c 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -397,7 +397,7 @@ public class CommandHandler : IDisposable return false; } - var settings = collection.Settings[ mod.Index ]; + var settings = collection!.Settings[ mod.Index ]; switch( state ) { case 0: From 506f7d5824132a6b7f580e499ccec06f8922c083 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 Dec 2022 14:25:53 +0100 Subject: [PATCH 0639/2451] Use correct variant on imc deserialization. --- Penumbra/Import/TexToolsMeta.Deserialization.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 1494d949..ce8a0bd0 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -138,7 +138,7 @@ public partial class TexToolsMeta { if( !value.Equals( def.GetEntry( partIdx, i ) ) ) { - MetaManipulations.Add( manip.Copy( value ) ); + MetaManipulations.Add( new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ) ); } ++i; From d5e2fc3b057c2ea40fe564df8f3249bb7d0d77b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Dec 2022 16:45:23 +0100 Subject: [PATCH 0640/2451] Try to associate battle voices to characters. --- .../Resolver/PathResolver.AnimationState.cs | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 3c842250..e862b82b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -14,8 +14,9 @@ public unsafe partial class PathResolver { private readonly DrawObjectState _drawObjectState; - private ResolveData _animationLoadData = ResolveData.Invalid; - private ResolveData _lastAvfxData = ResolveData.Invalid; + private ResolveData _animationLoadData = ResolveData.Invalid; + private ResolveData _lastAvfxData = ResolveData.Invalid; + private ResolveData _characterSoundData = ResolveData.Invalid; public AnimationState( DrawObjectState drawObjectState ) { @@ -27,9 +28,22 @@ public unsafe partial class PathResolver { switch( type ) { + case ResourceType.Scd: + if( _characterSoundData.Valid ) + { + resolveData = _characterSoundData; + return true; + } + + if( _animationLoadData.Valid ) + { + resolveData = _animationLoadData; + return true; + } + + break; case ResourceType.Tmb: case ResourceType.Pap: - case ResourceType.Scd: if( _animationLoadData.Valid ) { resolveData = _animationLoadData; @@ -76,6 +90,7 @@ public unsafe partial class PathResolver _loadSomePapHook.Enable(); _someActionLoadHook.Enable(); _someOtherAvfxHook.Enable(); + _loadCharacterSoundHook.Enable(); } public void Disable() @@ -86,6 +101,7 @@ public unsafe partial class PathResolver _loadSomePapHook.Disable(); _someActionLoadHook.Disable(); _someOtherAvfxHook.Disable(); + _loadCharacterSoundHook.Disable(); } public void Dispose() @@ -96,6 +112,22 @@ public unsafe partial class PathResolver _loadSomePapHook.Dispose(); _someActionLoadHook.Dispose(); _someOtherAvfxHook.Dispose(); + _loadCharacterSoundHook.Dispose(); + } + + // Characters load some of their voice lines or whatever with this function. + private delegate IntPtr LoadCharacterSound( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ); + + [Signature( "4C 89 4C 24 ?? 55 57 41 56", DetourName = nameof( LoadCharacterSoundDetour ) )] + private readonly Hook< LoadCharacterSound > _loadCharacterSoundHook = null!; + + private IntPtr LoadCharacterSoundDetour( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ) + { + var last = _characterSoundData; + _characterSoundData = IdentifyCollection( ( GameObject* )character, true ); + var ret = _loadCharacterSoundHook.Original( character, unk1, unk2, unk3, unk4, unk5, unk6, unk7 ); + _characterSoundData = last; + return ret; } // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. From 8bca3d82f53defdc3882368bc8b7249594826b11 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Dec 2022 14:04:29 +0100 Subject: [PATCH 0641/2451] Probably fix some atex/avfx problems. --- Penumbra/Dalamud.cs | 26 +++++++++---------- .../Resolver/PathResolver.AnimationState.cs | 23 ++++++++++++++++ Penumbra/Interop/Resolver/PathResolver.cs | 1 + 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index 9aa29806..ab2b924a 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -19,18 +19,18 @@ public class Dalamud public static void Initialize( DalamudPluginInterface pluginInterface ) => pluginInterface.Create< Dalamud >(); - // @formatter:off - [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; + // @formatter:off + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; // @formatter:on } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index e862b82b..3153800c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -24,6 +24,14 @@ public unsafe partial class PathResolver SignatureHelper.Initialise( this ); } + public void UpdateAvfx( ResourceType type, ResolveData data ) + { + if( type == ResourceType.Avfx ) + { + _lastAvfxData = data; + } + } + public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData ) { switch( type ) @@ -91,6 +99,7 @@ public unsafe partial class PathResolver _someActionLoadHook.Enable(); _someOtherAvfxHook.Enable(); _loadCharacterSoundHook.Enable(); + //_apricotResourceLoadHook.Enable(); } public void Disable() @@ -102,6 +111,7 @@ public unsafe partial class PathResolver _someActionLoadHook.Disable(); _someOtherAvfxHook.Disable(); _loadCharacterSoundHook.Disable(); + //_apricotResourceLoadHook.Disable(); } public void Dispose() @@ -113,6 +123,7 @@ public unsafe partial class PathResolver _someActionLoadHook.Dispose(); _someOtherAvfxHook.Dispose(); _loadCharacterSoundHook.Dispose(); + //_apricotResourceLoadHook.Dispose(); } // Characters load some of their voice lines or whatever with this function. @@ -250,5 +261,17 @@ public unsafe partial class PathResolver _someOtherAvfxHook.Original( unk ); _animationLoadData = last; } + + //private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); + // + //[Signature( "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9", DetourName = nameof( ApricotResourceLoadDetour ) )] + //private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; + // + // + //private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) + //{ + // Penumbra.Log.Information( $"{handle:X} {new ByteString( ( ( ResourceHandle* )handle )->FileName() )} {unk1:X} {unk2} {_lastAvfxData.ModCollection.Name}" ); + // return _apricotResourceLoadHook.Original( handle, unk1, unk2 ); + //} } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 439040c1..14f345d6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -71,6 +71,7 @@ public partial class PathResolver : IDisposable // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; MaterialState.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); + _animations.UpdateAvfx( type, data.Item2 ); return true; } From 3e26972a159653eeeff377a961acffb81ac90a48 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Dec 2022 14:40:32 +0100 Subject: [PATCH 0642/2451] Convert Unknown Equipslots to Head for DemiHuman IMC. --- Penumbra.GameData/Enums/EquipSlot.cs | 4 ++-- Penumbra/Meta/Manipulations/ImcManipulation.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 2ab5b002..2afe9939 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -86,7 +86,7 @@ public static class EquipSlotExtensions EquipSlot.RFinger => "rir", EquipSlot.LFinger => "ril", EquipSlot.Wrists => "wrs", - _ => throw new InvalidEnumArgumentException(), + _ => "unk", }; } @@ -116,7 +116,7 @@ public static class EquipSlotExtensions EquipSlot.BodyHands => EquipSlot.Body, EquipSlot.BodyLegsFeet => EquipSlot.Body, EquipSlot.ChestHands => EquipSlot.Body, - _ => throw new InvalidEnumArgumentException($"{value} ({(int)value}) is not valid."), + _ => EquipSlot.Unknown, }; } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index f2b4e45d..4c65fd6d 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -57,7 +57,7 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { BodySlot = BodySlot.Unknown; SecondaryId = secondaryId; - EquipSlot = equipSlot; + EquipSlot = equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot; } else { From 707ae090bffbddb9e43ae508f33373ccfabea273 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Dec 2022 18:22:52 +0100 Subject: [PATCH 0643/2451] Treat AVFX similar to MTRL, and ATEX similar to TEX. --- .../Resolver/PathResolver.AnimationState.cs | 42 ++------ ...r.Material.cs => PathResolver.Subfiles.cs} | 98 ++++++++++++------- Penumbra/Interop/Resolver/PathResolver.cs | 15 ++- 3 files changed, 78 insertions(+), 77 deletions(-) rename Penumbra/Interop/Resolver/{PathResolver.Material.cs => PathResolver.Subfiles.cs} (57%) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 3153800c..fcc04234 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; using Penumbra.String.Classes; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -14,9 +17,8 @@ public unsafe partial class PathResolver { private readonly DrawObjectState _drawObjectState; - private ResolveData _animationLoadData = ResolveData.Invalid; - private ResolveData _lastAvfxData = ResolveData.Invalid; - private ResolveData _characterSoundData = ResolveData.Invalid; + private ResolveData _animationLoadData = ResolveData.Invalid; + private ResolveData _characterSoundData = ResolveData.Invalid; public AnimationState( DrawObjectState drawObjectState ) { @@ -24,15 +26,7 @@ public unsafe partial class PathResolver SignatureHelper.Initialise( this ); } - public void UpdateAvfx( ResourceType type, ResolveData data ) - { - if( type == ResourceType.Avfx ) - { - _lastAvfxData = data; - } - } - - public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData ) + public bool HandleFiles( ResourceType type, Utf8GamePath path, out ResolveData resolveData ) { switch( type ) { @@ -60,9 +54,6 @@ public unsafe partial class PathResolver break; case ResourceType.Avfx: - _lastAvfxData = _animationLoadData.Valid - ? _animationLoadData - : Penumbra.CollectionManager.Default.ToResolveData(); if( _animationLoadData.Valid ) { resolveData = _animationLoadData; @@ -71,12 +62,6 @@ public unsafe partial class PathResolver break; case ResourceType.Atex: - if( _lastAvfxData.Valid ) - { - resolveData = _lastAvfxData; - return true; - } - if( _animationLoadData.Valid ) { resolveData = _animationLoadData; @@ -99,7 +84,6 @@ public unsafe partial class PathResolver _someActionLoadHook.Enable(); _someOtherAvfxHook.Enable(); _loadCharacterSoundHook.Enable(); - //_apricotResourceLoadHook.Enable(); } public void Disable() @@ -111,7 +95,6 @@ public unsafe partial class PathResolver _someActionLoadHook.Disable(); _someOtherAvfxHook.Disable(); _loadCharacterSoundHook.Disable(); - //_apricotResourceLoadHook.Disable(); } public void Dispose() @@ -123,7 +106,6 @@ public unsafe partial class PathResolver _someActionLoadHook.Dispose(); _someOtherAvfxHook.Dispose(); _loadCharacterSoundHook.Dispose(); - //_apricotResourceLoadHook.Dispose(); } // Characters load some of their voice lines or whatever with this function. @@ -261,17 +243,5 @@ public unsafe partial class PathResolver _someOtherAvfxHook.Original( unk ); _animationLoadData = last; } - - //private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); - // - //[Signature( "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9", DetourName = nameof( ApricotResourceLoadDetour ) )] - //private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; - // - // - //private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) - //{ - // Penumbra.Log.Information( $"{handle:X} {new ByteString( ( ( ResourceHandle* )handle )->FileName() )} {unk1:X} {unk2} {_lastAvfxData.ModCollection.Name}" ); - // return _apricotResourceLoadHook.Original( handle, unk1, unk2 ); - //} } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs similarity index 57% rename from Penumbra/Interop/Resolver/PathResolver.Material.cs rename to Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 90893c20..8eecf362 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -13,16 +12,17 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - // Materials do contain their own paths to textures and shader packages. + // Materials and avfx do contain their own paths to textures and shader packages or atex respectively. // Those are loaded synchronously. // Thus, we need to ensure the correct files are loaded when a material is loaded. - public class MaterialState : IDisposable + public class SubfileHelper : IDisposable { private readonly PathState _paths; private ResolveData _mtrlData = ResolveData.Invalid; + private ResolveData _avfxData = ResolveData.Invalid; - public MaterialState( PathState paths ) + public SubfileHelper( PathState paths ) { SignatureHelper.Initialise( this ); _paths = paths; @@ -31,10 +31,20 @@ public unsafe partial class PathResolver // Check specifically for shpk and tex files whether we are currently in a material load. public bool HandleSubFiles( ResourceType type, out ResolveData collection ) { - if( _mtrlData.Valid && type is ResourceType.Tex or ResourceType.Shpk ) + switch( type ) { - collection = _mtrlData; - return true; + case ResourceType.Tex: + case ResourceType.Shpk: + if( _mtrlData.Valid ) + { + collection = _mtrlData; + return true; + } + + break; + case ResourceType.Atex when _avfxData.Valid: + collection = _avfxData; + return true; } collection = ResolveData.Invalid; @@ -45,29 +55,35 @@ public unsafe partial class PathResolver public static void HandleCollection( ResolveData resolveData, string path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, ResolveData) data ) { - if( nonDefault && type == ResourceType.Mtrl ) + if( nonDefault ) { - var fullPath = new FullPath( $"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}" ); - data = ( fullPath, resolveData ); - } - else - { - data = ( resolved, resolveData ); + switch( type ) + { + case ResourceType.Mtrl: + case ResourceType.Avfx: + var fullPath = new FullPath( $"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}" ); + data = ( fullPath, resolveData ); + return; + } } + + data = ( resolved, resolveData ); } public void Enable() { _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); - Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; + _apricotResourceLoadHook.Enable(); + Penumbra.ResourceLoader.ResourceLoadCustomization += SubfileLoadHandler; } public void Disable() { _loadMtrlShpkHook.Disable(); _loadMtrlTexHook.Disable(); - Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler; + _apricotResourceLoadHook.Disable(); + Penumbra.ResourceLoader.ResourceLoadCustomization -= SubfileLoadHandler; } public void Dispose() @@ -75,17 +91,21 @@ public unsafe partial class PathResolver Disable(); _loadMtrlShpkHook.Dispose(); _loadMtrlTexHook.Dispose(); + _apricotResourceLoadHook.Dispose(); } // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. - public bool MtrlLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, + public bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { ret = 0; - if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl ) + switch( fileDescriptor->ResourceHandle->FileType ) { - return false; + case ResourceType.Mtrl: + case ResourceType.Avfx: + break; + default: return false; } var lastUnderscore = split.LastIndexOf( ( byte )'_' ); @@ -94,17 +114,14 @@ public unsafe partial class PathResolver || Penumbra.CollectionManager.ByName( name, out collection ) ) { #if DEBUG - Penumbra.Log.Verbose( $"Using MtrlLoadHandler with collection {name} for path {path}." ); + Penumbra.Log.Verbose( $"Using {nameof(SubfileLoadHandler)} with collection {name} for path {path}." ); #endif - - var objFromObjTable = Dalamud.Objects.FirstOrDefault( f => f.Name.TextValue == name ); - var gameObjAddr = objFromObjTable?.Address ?? IntPtr.Zero; - _paths.SetCollection( gameObjAddr, path, collection ); + _paths.SetCollection( IntPtr.Zero, path, collection ); } else { #if DEBUG - Penumbra.Log.Verbose( $"Using MtrlLoadHandler with no collection for path {path}." ); + Penumbra.Log.Verbose( $"Using {nameof( SubfileLoadHandler )} with no collection for path {path}." ); #endif } @@ -124,7 +141,7 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlHelper( mtrlResourceHandle ); + _mtrlData = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); _mtrlData = ResolveData.Invalid; return ret; @@ -136,22 +153,37 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlHelper( mtrlResourceHandle ); + _mtrlData = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); _mtrlData = ResolveData.Invalid; return ret; } - private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) + private ResolveData LoadFileHelper( IntPtr resourceHandle ) { - if( mtrlResourceHandle == IntPtr.Zero ) + if( resourceHandle == IntPtr.Zero ) { - return; + return ResolveData.Invalid; } - var mtrl = ( MtrlResource* )mtrlResourceHandle; - var mtrlPath = ByteString.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - _mtrlData = _paths.TryGetValue( mtrlPath, out var c ) ? c : ResolveData.Invalid; + var resource = ( ResourceHandle* )resourceHandle; + var filePath = ByteString.FromSpanUnsafe( resource->FileNameSpan(), true, null, true ); + return _paths.TryGetValue( filePath, out var c ) ? c : ResolveData.Invalid; + } + + + private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); + + [Signature( "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9", DetourName = nameof( ApricotResourceLoadDetour ) )] + private readonly Hook _apricotResourceLoadHook = null!; + + + private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) + { + _avfxData = LoadFileHelper( handle ); + var ret = _apricotResourceLoadHook.Original( handle, unk1, unk2 ); + _avfxData = ResolveData.Invalid; + return ret; } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 14f345d6..a8acce6d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -30,7 +30,7 @@ public partial class PathResolver : IDisposable private readonly AnimationState _animations; private readonly PathState _paths; private readonly MetaState _meta; - private readonly MaterialState _materials; + private readonly SubfileHelper _subFiles; static PathResolver() => ValidHumanModels = GetValidHumanModels( Dalamud.GameData ); @@ -42,7 +42,7 @@ public partial class PathResolver : IDisposable _animations = new AnimationState( DrawObjects ); _paths = new PathState( this ); _meta = new MetaState( _paths.HumanVTable ); - _materials = new MaterialState( _paths ); + _subFiles = new SubfileHelper( _paths ); } // The modified resolver that handles game path resolving. @@ -54,7 +54,7 @@ public partial class PathResolver : IDisposable // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = _materials.HandleSubFiles( type, out var resolveData ) + var nonDefault = _subFiles.HandleSubFiles( type, out var resolveData ) || _paths.Consume( gamePath.Path, out resolveData ) || _animations.HandleFiles( type, gamePath, out resolveData ) || DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); @@ -70,8 +70,7 @@ public partial class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; - MaterialState.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); - _animations.UpdateAvfx( type, data.Item2 ); + SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); return true; } @@ -89,7 +88,7 @@ public partial class PathResolver : IDisposable _animations.Enable(); _paths.Enable(); _meta.Enable(); - _materials.Enable(); + _subFiles.Enable(); _loader.ResolvePathCustomization += CharacterResolver; Penumbra.Log.Debug( "Character Path Resolver enabled." ); @@ -109,7 +108,7 @@ public partial class PathResolver : IDisposable IdentifiedCache.Disable(); _paths.Disable(); _meta.Disable(); - _materials.Disable(); + _subFiles.Disable(); _loader.ResolvePathCustomization -= CharacterResolver; Penumbra.Log.Debug( "Character Path Resolver disabled." ); @@ -124,7 +123,7 @@ public partial class PathResolver : IDisposable Cutscenes.Dispose(); IdentifiedCache.Dispose(); _meta.Dispose(); - _materials.Dispose(); + _subFiles.Dispose(); } public static unsafe (IntPtr, ResolveData) IdentifyDrawObject( IntPtr drawObject ) From ef19af481b16fc0bf74677183f3bdf4313e6ef17 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Dec 2022 18:36:52 +0100 Subject: [PATCH 0644/2451] Add a toggle to keep metadata changes to the default value when importing TTMPs. --- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsMeta.Deserialization.cs | 10 +++++----- Penumbra/Import/TexToolsMeta.Rgsp.cs | 4 ++-- Penumbra/Import/TexToolsMeta.cs | 10 ++++++---- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 8 +++++--- Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs | 7 +++++++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b51251a1..0e6b70d2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -69,6 +69,7 @@ public partial class Configuration : IPluginConfiguration public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; + public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; public Dictionary< ColorId, uint > Colors { get; set; } diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index ce8a0bd0..2ea01648 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -22,7 +22,7 @@ public partial class TexToolsMeta var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data ); var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); - if( def.Entry != manip.Entry ) + if( _keepDefault || def.Entry != manip.Entry ) { MetaManipulations.Add( manip ); } @@ -53,7 +53,7 @@ public partial class TexToolsMeta metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); var manip = new EqdpManipulation( value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); - if( def.Entry != manip.Entry ) + if( _keepDefault || def.Entry != manip.Entry ) { MetaManipulations.Add( manip ); } @@ -72,7 +72,7 @@ public partial class TexToolsMeta var value = ( GmpEntry )reader.ReadUInt32(); value.UnknownTotal = reader.ReadByte(); var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId ); - if( value != def ) + if( _keepDefault || value != def ) { MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) ); } @@ -107,7 +107,7 @@ public partial class TexToolsMeta } var def = EstFile.GetDefault( type, gr, id ); - if( def != value ) + if( _keepDefault || def != value ) { MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) ); } @@ -136,7 +136,7 @@ public partial class TexToolsMeta var partIdx = ImcFile.PartIndex( manip.EquipSlot ); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach( var value in values ) { - if( !value.Equals( def.GetEntry( partIdx, i ) ) ) + if( _keepDefault || !value.Equals( def.GetEntry( partIdx, i ) ) ) { MetaManipulations.Add( new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ) ); } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index cc4055ad..8eb0c49a 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -9,7 +9,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Parse a single rgsp file. - public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) + public static TexToolsMeta FromRgspFile( string filePath, byte[] data, bool keepDefault ) { if( data.Length != 45 && data.Length != 42 ) { @@ -47,7 +47,7 @@ public partial class TexToolsMeta void Add( RspAttribute attribute, float value ) { var def = CmpFile.GetDefault( subRace, attribute ); - if( value != def ) + if( keepDefault || value != def ) { ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) ); } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 2a2c98e5..8a8bb193 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -23,12 +23,14 @@ public partial class TexToolsMeta // The info class determines the files or table locations the changes need to apply to from the filename. - public readonly uint Version; - public readonly string FilePath; - public readonly List< MetaManipulation > MetaManipulations = new(); + public readonly uint Version; + public readonly string FilePath; + public readonly List< MetaManipulation > MetaManipulations = new(); + private readonly bool _keepDefault = false; - public TexToolsMeta( byte[] data ) + public TexToolsMeta( byte[] data, bool keepDefault ) { + _keepDefault = keepDefault; try { using var reader = new BinaryReader( new MemoryStream( data ) ); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 0cdb9aa8..b38307be 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -201,7 +201,7 @@ public partial class Mod continue; } - var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); deleteList.Add( file.FullName ); ManipulationData.UnionWith( meta.MetaManipulations ); @@ -214,7 +214,7 @@ public partial class Mod continue; } - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); deleteList.Add( file.FullName ); @@ -274,7 +274,9 @@ public partial class Mod { try { - var x = file.EndsWith( "rgsp" ) ? TexToolsMeta.FromRgspFile( file, data ) : new TexToolsMeta( data ); + var x = file.EndsWith( "rgsp" ) + ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) + : new TexToolsMeta( data, Penumbra.Config.KeepDefaultMetaChanges ); meta.UnionWith( x.MetaManipulations ); } catch diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 2d47e290..d0ed97a7 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -19,10 +19,17 @@ public partial class ConfigWindow OpenTutorial( BasicTutorialSteps.AdvancedSettings ); if( !header ) + { 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 ); + Checkbox( "Keep Default Metadata Changes on Import", + "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", + Penumbra.Config.KeepDefaultMetaChanges, v => Penumbra.Config.KeepDefaultMetaChanges = v ); DrawRequestedResourceLogging(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); From 5cd4b49fee0c76af5456daee2e3c8906d8b69c59 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 Dec 2022 18:09:59 +0100 Subject: [PATCH 0645/2451] Add a toggle to advanced settings that uses reflection to change the Synchronous Load option in Dalamud. --- Penumbra/Dalamud.cs | 118 ++++++++++++++++++ Penumbra/Penumbra.cs | 1 + .../UI/ConfigWindow.SettingsTab.Advanced.cs | 15 +++ 3 files changed, 134 insertions(+) diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index ab2b924a..d872e629 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -9,6 +10,8 @@ using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.Plugin; +using System.Linq; +using System.Reflection; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local @@ -33,4 +36,119 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; // @formatter:on + + private static readonly object? DalamudConfig; + private static readonly object? SettingsWindow; + private static readonly MethodInfo? SaveDalamudConfig; + public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; + + static Dalamud() + { + try + { + var serviceType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "Service`1" && t.IsGenericType ); + var configType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "DalamudConfiguration" ); + var interfaceType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "DalamudInterface" ); + if( serviceType == null || configType == null || interfaceType == null ) + { + return; + } + + var configService = serviceType.MakeGenericType( configType ); + var interfaceService = serviceType.MakeGenericType( interfaceType ); + var configGetter = configService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); + var interfaceGetter = interfaceService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); + if( configGetter == null || interfaceGetter == null ) + { + return; + } + + DalamudConfig = configGetter.Invoke( null, null ); + if( DalamudConfig != null ) + { + SaveDalamudConfig = DalamudConfig.GetType().GetMethod( "Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); + if( SaveDalamudConfig == null ) + { + DalamudConfig = null; + } + + var inter = interfaceGetter.Invoke( null, null ); + SettingsWindow = inter?.GetType().GetField( "settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.GetValue( inter ); + if( SettingsWindow == null ) + { + DalamudConfig = null; + SaveDalamudConfig = null; + } + } + } + catch + { + DalamudConfig = null; + SaveDalamudConfig = null; + SettingsWindow = null; + } + } + + public static bool GetDalamudConfig< T >( string fieldName, out T? value ) + { + value = default; + try + { + if( DalamudConfig == null ) + { + return false; + } + + var getter = DalamudConfig.GetType().GetProperty( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); + if( getter == null ) + { + return false; + } + + var result = getter.GetValue( DalamudConfig ); + if( result is not T v ) + { + return false; + } + + value = v; + return true; + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error while fetching Dalamud Config {fieldName}:\n{e}" ); + return false; + } + } + + public static bool SetDalamudConfig< T >( string fieldName, in T? value, string? windowFieldName = null ) + { + try + { + if( DalamudConfig == null ) + { + return false; + } + + var getter = DalamudConfig.GetType().GetProperty( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); + if( getter == null ) + { + return false; + } + + getter.SetValue( DalamudConfig, value ); + if( windowFieldName != null ) + { + SettingsWindow!.GetType().GetField( windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.SetValue( SettingsWindow, value ); + } + + SaveDalamudConfig!.Invoke( DalamudConfig, null ); + return true; + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error while fetching Dalamud Config {fieldName}:\n{e}" ); + return false; + } + } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 25ad785c..32538a81 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -330,6 +330,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); + sb.Append( $"> **`Synchronous Load (Dalamud): `** {(Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown")}\n" ); sb.Append( $"> **`Logging: `** Full: {Config.EnableFullResourceLogging}, Resource: {Config.EnableResourceLogging}\n" ); sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.AppendLine( "**Mods**" ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index d0ed97a7..b4bd7dc8 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -30,6 +30,7 @@ public partial class ConfigWindow "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", Penumbra.Config.KeepDefaultMetaChanges, v => Penumbra.Config.KeepDefaultMetaChanges = v ); + DrawWaitForPluginsReflection(); DrawRequestedResourceLogging(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); @@ -158,5 +159,19 @@ public partial class ConfigWindow FontReloader.Reload(); } } + + private static void DrawWaitForPluginsReflection() + { + if( !Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool value ) ) + { + using var disabled = ImRaii.Disabled(); + Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); + } + else + { + Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, + v => Dalamud.SetDalamudConfig( Dalamud.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); + } + } } } \ No newline at end of file From 743f449a49368d1ba502e52bdb4840d70e9d5b07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Dec 2022 12:56:47 +0100 Subject: [PATCH 0646/2451] Don't reflect the interface before it apparently exists. --- Penumbra/Dalamud.cs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index d872e629..5be53335 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -38,7 +38,7 @@ public class Dalamud // @formatter:on private static readonly object? DalamudConfig; - private static readonly object? SettingsWindow; + private static readonly MethodInfo? InterfaceGetter; private static readonly MethodInfo? SaveDalamudConfig; public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; @@ -56,9 +56,9 @@ public class Dalamud var configService = serviceType.MakeGenericType( configType ); var interfaceService = serviceType.MakeGenericType( interfaceType ); - var configGetter = configService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); - var interfaceGetter = interfaceService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); - if( configGetter == null || interfaceGetter == null ) + var configGetter = configService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); + InterfaceGetter = interfaceService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); + if( configGetter == null || InterfaceGetter == null ) { return; } @@ -69,15 +69,8 @@ public class Dalamud SaveDalamudConfig = DalamudConfig.GetType().GetMethod( "Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); if( SaveDalamudConfig == null ) { - DalamudConfig = null; - } - - var inter = interfaceGetter.Invoke( null, null ); - SettingsWindow = inter?.GetType().GetField( "settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.GetValue( inter ); - if( SettingsWindow == null ) - { - DalamudConfig = null; - SaveDalamudConfig = null; + DalamudConfig = null; + InterfaceGetter = null; } } } @@ -85,7 +78,7 @@ public class Dalamud { DalamudConfig = null; SaveDalamudConfig = null; - SettingsWindow = null; + InterfaceGetter = null; } } @@ -139,7 +132,9 @@ public class Dalamud getter.SetValue( DalamudConfig, value ); if( windowFieldName != null ) { - SettingsWindow!.GetType().GetField( windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.SetValue( SettingsWindow, value ); + var inter = InterfaceGetter!.Invoke( null, null ); + var settingsWindow = inter?.GetType().GetField( "settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.GetValue( inter ); + settingsWindow?.GetType().GetField( windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.SetValue( settingsWindow, value ); } SaveDalamudConfig!.Invoke( DalamudConfig, null ); From 4df9ac463275f73abb7f86528c5b9e27a4188c27 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Dec 2022 14:09:09 +0100 Subject: [PATCH 0647/2451] Possibly improve VFX association with character collections for ground effects and maybe some normal effects, too. --- .../Resolver/PathResolver.AnimationState.cs | 153 ++++++++++++++---- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index fcc04234..a34679b8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; +using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -17,8 +16,8 @@ public unsafe partial class PathResolver { private readonly DrawObjectState _drawObjectState; - private ResolveData _animationLoadData = ResolveData.Invalid; - private ResolveData _characterSoundData = ResolveData.Invalid; + private ResolveData _animationLoadData = ResolveData.Invalid; + private ResolveData _characterSoundData = ResolveData.Invalid; public AnimationState( DrawObjectState drawObjectState ) { @@ -26,7 +25,7 @@ public unsafe partial class PathResolver SignatureHelper.Initialise( this ); } - public bool HandleFiles( ResourceType type, Utf8GamePath path, out ResolveData resolveData ) + public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData ) { switch( type ) { @@ -79,33 +78,42 @@ public unsafe partial class PathResolver { _loadTimelineResourcesHook.Enable(); _characterBaseLoadAnimationHook.Enable(); - _loadSomeAvfxHook.Enable(); _loadSomePapHook.Enable(); _someActionLoadHook.Enable(); - _someOtherAvfxHook.Enable(); _loadCharacterSoundHook.Enable(); + _loadCharacterVfxHook.Enable(); + _loadAreaVfxHook.Enable(); + + //_loadSomeAvfxHook.Enable(); + //_someOtherAvfxHook.Enable(); } public void Disable() { _loadTimelineResourcesHook.Disable(); _characterBaseLoadAnimationHook.Disable(); - _loadSomeAvfxHook.Disable(); _loadSomePapHook.Disable(); _someActionLoadHook.Disable(); - _someOtherAvfxHook.Disable(); _loadCharacterSoundHook.Disable(); + _loadCharacterVfxHook.Disable(); + _loadAreaVfxHook.Disable(); + + //_loadSomeAvfxHook.Disable(); + //_someOtherAvfxHook.Disable(); } public void Dispose() { _loadTimelineResourcesHook.Dispose(); _characterBaseLoadAnimationHook.Dispose(); - _loadSomeAvfxHook.Dispose(); _loadSomePapHook.Dispose(); _someActionLoadHook.Dispose(); - _someOtherAvfxHook.Dispose(); _loadCharacterSoundHook.Dispose(); + _loadCharacterVfxHook.Dispose(); + _loadAreaVfxHook.Dispose(); + + //_loadSomeAvfxHook.Dispose(); + //_someOtherAvfxHook.Dispose(); } // Characters load some of their voice lines or whatever with this function. @@ -181,21 +189,6 @@ public unsafe partial class PathResolver _animationLoadData = last; } - - public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); - - [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] - private readonly Hook< LoadSomeAvfx > _loadSomeAvfxHook = null!; - - private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) - { - var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); - var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); - _animationLoadData = last; - return ret; - } - // Unknown what exactly this is but it seems to load a bunch of paps. private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); @@ -232,16 +225,106 @@ public unsafe partial class PathResolver _animationLoadData = last; } - [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )] - private readonly Hook< CharacterBaseNoArgumentDelegate > _someOtherAvfxHook = null!; - - private void SomeOtherAvfxDetour( IntPtr unk ) + [StructLayout( LayoutKind.Explicit )] + private struct VfxParams { - var last = _animationLoadData; - var gameObject = ( GameObject* )( unk - 0x8D0 ); - _animationLoadData = IdentifyCollection( gameObject, true ); - _someOtherAvfxHook.Original( unk ); - _animationLoadData = last; + [FieldOffset( 0x118 )] + public uint GameObjectId; + + [FieldOffset( 0xD0 )] + public ushort TargetCount; + + [FieldOffset( 0x120 )] + public fixed uint Target[16]; } + + private delegate IntPtr LoadCharacterVfxDelegate( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ); + + [Signature( "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93", DetourName = nameof( LoadCharacterVfxDetour ) )] + private readonly Hook< LoadCharacterVfxDelegate > _loadCharacterVfxHook = null!; + + private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) + { + var last = _animationLoadData; + if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) ) + { + var obj = Dalamud.Objects.SearchById( vfxParams->GameObjectId ); + if( obj != null ) + { + _animationLoadData = IdentifyCollection( ( GameObject* )obj.Address, true ); + } + else + { + _animationLoadData = ResolveData.Invalid; + } + } + else + { + _animationLoadData = ResolveData.Invalid; + } + + var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 ); +#if DEBUG + Penumbra.Log.Verbose( + $"Load Character VFX: {new ByteString( vfxPath )} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); +#endif + _animationLoadData = last; + return ret; + } + + private delegate IntPtr LoadAreaVfxDelegate( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ); + + [Signature( "48 8B C4 53 55 56 57 41 56 48 81 EC", DetourName = nameof( LoadAreaVfxDetour ) )] + private readonly Hook< LoadAreaVfxDelegate > _loadAreaVfxHook = null!; + + private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ) + { + var last = _animationLoadData; + if( caster != null ) + { + _animationLoadData = IdentifyCollection( caster, true ); + } + else + { + _animationLoadData = ResolveData.Invalid; + } + + var ret = _loadAreaVfxHook.Original( vfxId, pos, caster, unk1, unk2, unk3 ); +#if DEBUG + Penumbra.Log.Verbose( + $"Load Area VFX: {vfxId}, {pos[ 0 ]} {pos[ 1 ]} {pos[ 2 ]} {( caster != null ? new ByteString( caster->GetName() ).ToString() : "Unknown" )} {unk1} {unk2} {unk3} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); +#endif + _animationLoadData = last; + return ret; + } + + + // ========== Those hooks seem to be superseded by LoadCharacterVfx ========= + + // public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); + // + // [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] + // private readonly Hook _loadSomeAvfxHook = null!; + // + // private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) + // { + // var last = _animationLoadData; + // _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); + // var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); + // _animationLoadData = last; + // return ret; + // } + // + // [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )] + // private readonly Hook _someOtherAvfxHook = null!; + // + // private void SomeOtherAvfxDetour( IntPtr unk ) + // { + // var last = _animationLoadData; + // var gameObject = ( GameObject* )( unk - 0x8D0 ); + // _animationLoadData = IdentifyCollection( gameObject, true ); + // _someOtherAvfxHook.Original( unk ); + // _animationLoadData = last; + // } } } \ No newline at end of file From 87b6fe6aa6115148c1a863dd6486ab8e969f8360 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Dec 2022 00:36:35 +0100 Subject: [PATCH 0648/2451] Change subfile handling to maybe retain associated game object for Mare. --- .../Interop/Loader/ResourceLoader.Debug.cs | 2 +- .../Resolver/PathResolver.AnimationState.cs | 14 ----- .../Resolver/PathResolver.PathState.cs | 2 +- .../Interop/Resolver/PathResolver.Subfiles.cs | 55 +++++++++---------- Penumbra/Interop/Resolver/PathResolver.cs | 4 +- 5 files changed, 31 insertions(+), 46 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index ba2f2962..693c73ec 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -246,7 +246,7 @@ public unsafe partial class ResourceLoader private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); - Penumbra.Log.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); + Penumbra.Log.Information( $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); } private static void LogLoadedFile( ByteString path, bool success, bool custom ) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index a34679b8..c53196cc 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -45,21 +45,7 @@ public unsafe partial class PathResolver break; case ResourceType.Tmb: case ResourceType.Pap: - if( _animationLoadData.Valid ) - { - resolveData = _animationLoadData; - return true; - } - - break; case ResourceType.Avfx: - if( _animationLoadData.Valid ) - { - resolveData = _animationLoadData; - return true; - } - - break; case ResourceType.Atex: if( _animationLoadData.Valid ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 42c458c3..2cf9cb4b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -31,7 +31,7 @@ public unsafe partial class PathResolver private readonly ResolverHooks _monster; // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); + private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); public PathState( PathResolver parent ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 8eecf362..a62141d6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Concurrent; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; +using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; @@ -17,15 +19,18 @@ public unsafe partial class PathResolver // Thus, we need to ensure the correct files are loaded when a material is loaded. public class SubfileHelper : IDisposable { - private readonly PathState _paths; + private readonly ResourceLoader _loader; private ResolveData _mtrlData = ResolveData.Invalid; private ResolveData _avfxData = ResolveData.Invalid; - public SubfileHelper( PathState paths ) + private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); + + public SubfileHelper( ResourceLoader loader ) { SignatureHelper.Initialise( this ); - _paths = paths; + + _loader = loader; } // Check specifically for shpk and tex files whether we are currently in a material load. @@ -52,7 +57,7 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - public static void HandleCollection( ResolveData resolveData, string path, bool nonDefault, ResourceType type, FullPath? resolved, + public static void HandleCollection( ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, ResolveData) data ) { if( nonDefault ) @@ -75,7 +80,8 @@ public unsafe partial class PathResolver _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); _apricotResourceLoadHook.Enable(); - Penumbra.ResourceLoader.ResourceLoadCustomization += SubfileLoadHandler; + _loader.ResourceLoadCustomization += SubfileLoadHandler; + _loader.ResourceLoaded += SubfileContainerLoaded; } public void Disable() @@ -83,7 +89,8 @@ public unsafe partial class PathResolver _loadMtrlShpkHook.Disable(); _loadMtrlTexHook.Disable(); _apricotResourceLoadHook.Disable(); - Penumbra.ResourceLoader.ResourceLoadCustomization -= SubfileLoadHandler; + _loader.ResourceLoadCustomization -= SubfileLoadHandler; + _loader.ResourceLoaded -= SubfileContainerLoaded; } public void Dispose() @@ -94,6 +101,17 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Dispose(); } + private void SubfileContainerLoaded( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) + { + switch( handle->FileType ) + { + case ResourceType.Mtrl: + case ResourceType.Avfx: + _subFileCollection[ ( IntPtr )handle ] = resolveData; + break; + } + } + // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. public bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, @@ -108,29 +126,12 @@ public unsafe partial class PathResolver default: return false; } - var lastUnderscore = split.LastIndexOf( ( byte )'_' ); - var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( Penumbra.TempMods.CollectionByName( name, out var collection ) - || Penumbra.CollectionManager.ByName( name, out collection ) ) - { -#if DEBUG - Penumbra.Log.Verbose( $"Using {nameof(SubfileLoadHandler)} with collection {name} for path {path}." ); -#endif - _paths.SetCollection( IntPtr.Zero, path, collection ); - } - else - { -#if DEBUG - Penumbra.Log.Verbose( $"Using {nameof( SubfileLoadHandler )} with no collection for path {path}." ); -#endif - } - // Force isSync = true for this call. I don't really understand why, // or where the difference even comes from. // Was called with True on my client and with false on other peoples clients, // which caused problems. ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); - _paths.Consume( path, out _ ); + _subFileCollection.TryRemove( ( IntPtr )fileDescriptor->ResourceHandle, out _ ); return true; } @@ -166,16 +167,14 @@ public unsafe partial class PathResolver return ResolveData.Invalid; } - var resource = ( ResourceHandle* )resourceHandle; - var filePath = ByteString.FromSpanUnsafe( resource->FileNameSpan(), true, null, true ); - return _paths.TryGetValue( filePath, out var c ) ? c : ResolveData.Invalid; + return _subFileCollection.TryGetValue( resourceHandle, out var c ) ? c : ResolveData.Invalid; } private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); [Signature( "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9", DetourName = nameof( ApricotResourceLoadDetour ) )] - private readonly Hook _apricotResourceLoadHook = null!; + private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index a8acce6d..1bdaab54 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -42,7 +42,7 @@ public partial class PathResolver : IDisposable _animations = new AnimationState( DrawObjects ); _paths = new PathState( this ); _meta = new MetaState( _paths.HumanVTable ); - _subFiles = new SubfileHelper( _paths ); + _subFiles = new SubfileHelper( _loader ); } // The modified resolver that handles game path resolving. @@ -69,7 +69,7 @@ public partial class PathResolver : IDisposable // 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. // We also need to handle defaulted materials against a non-default collection. - var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; + var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); return true; } From e534ce37d58b49e0c88a4add7cf10ba0f6242f94 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Dec 2022 13:09:40 +0100 Subject: [PATCH 0649/2451] Add display for subfile resources and clean up better. --- .../Interop/Loader/ResourceLoader.Debug.cs | 6 +-- .../Loader/ResourceLoader.Replacement.cs | 4 +- Penumbra/Interop/Loader/ResourceLoader.cs | 2 +- .../Interop/Resolver/PathResolver.Subfiles.cs | 45 ++++++++++++++++--- Penumbra/Interop/Resolver/PathResolver.cs | 9 ++++ Penumbra/Interop/Structs/ResourceHandle.cs | 9 ++++ Penumbra/Penumbra.cs | 10 ++--- Penumbra/UI/ConfigWindow.DebugTab.cs | 29 +++++++++++- 8 files changed, 95 insertions(+), 19 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 693c73ec..682ead7e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -249,8 +249,6 @@ public unsafe partial class ResourceLoader Penumbra.Log.Information( $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); } - private static void LogLoadedFile( ByteString path, bool success, bool custom ) - => Penumbra.Log.Information( success - ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}" - : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); + private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom ) + => Penumbra.Log.Information( $"[ResourceLoader] Loading {path} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 8e28d2ce..28c8ead9 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -214,7 +214,7 @@ public unsafe partial class ResourceLoader SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( path, ret != 0, false ); + FileLoaded?.Invoke( fileDescriptor->ResourceHandle, path, ret != 0, false ); return ret; } @@ -242,7 +242,7 @@ public unsafe partial class ResourceLoader // Use the SE ReadFile function. var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath, ret != 0, true ); + FileLoaded?.Invoke( fileDescriptor->ResourceHandle, gamePath, ret != 0, true ); return ret; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index de91fb2a..ac00d62d 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -128,7 +128,7 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is newly loaded. // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) // custom is true if the file was loaded from local files instead of the default SqPacks. - public delegate void FileLoadedDelegate( ByteString path, bool success, bool custom ); + public delegate void FileLoadedDelegate( ResourceHandle* resource, ByteString path, bool success, bool custom ); public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index a62141d6..f94b3551 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -17,7 +19,7 @@ public unsafe partial class PathResolver // Materials and avfx do contain their own paths to textures and shader packages or atex respectively. // Those are loaded synchronously. // Thus, we need to ensure the correct files are loaded when a material is loaded. - public class SubfileHelper : IDisposable + public class SubfileHelper : IDisposable, IReadOnlyCollection> { private readonly ResourceLoader _loader; @@ -81,7 +83,8 @@ public unsafe partial class PathResolver _loadMtrlTexHook.Enable(); _apricotResourceLoadHook.Enable(); _loader.ResourceLoadCustomization += SubfileLoadHandler; - _loader.ResourceLoaded += SubfileContainerLoaded; + _loader.ResourceLoaded += SubfileContainerRequested; + _loader.FileLoaded += SubfileContainerLoaded; } public void Disable() @@ -90,7 +93,8 @@ public unsafe partial class PathResolver _loadMtrlTexHook.Disable(); _apricotResourceLoadHook.Disable(); _loader.ResourceLoadCustomization -= SubfileLoadHandler; - _loader.ResourceLoaded -= SubfileContainerLoaded; + _loader.ResourceLoaded -= SubfileContainerRequested; + _loader.FileLoaded -= SubfileContainerLoaded; } public void Dispose() @@ -101,13 +105,28 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Dispose(); } - private void SubfileContainerLoaded( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) + private void SubfileContainerRequested( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) { switch( handle->FileType ) { case ResourceType.Mtrl: case ResourceType.Avfx: - _subFileCollection[ ( IntPtr )handle ] = resolveData; + if( handle->FileSize == 0 ) + { + _subFileCollection[ ( IntPtr )handle ] = resolveData; + } + + break; + } + } + + private void SubfileContainerLoaded( ResourceHandle* handle, ByteString path, bool success, bool custom ) + { + switch( handle->FileType ) + { + case ResourceType.Mtrl: + case ResourceType.Avfx: + _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); break; } } @@ -131,7 +150,6 @@ public unsafe partial class PathResolver // Was called with True on my client and with false on other peoples clients, // which caused problems. ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); - _subFileCollection.TryRemove( ( IntPtr )fileDescriptor->ResourceHandle, out _ ); return true; } @@ -184,5 +202,20 @@ public unsafe partial class PathResolver _avfxData = ResolveData.Invalid; return ret; } + + public IEnumerator< KeyValuePair< IntPtr, ResolveData > > GetEnumerator() + => _subFileCollection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _subFileCollection.Count; + + internal ResolveData MtrlData + => _mtrlData; + + internal ResolveData AvfxData + => _avfxData; } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 1bdaab54..e09042f1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -169,4 +169,13 @@ public partial class PathResolver : IDisposable internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors => Cutscenes.Actors; + + internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections + => _subFiles; + + internal ResolveData CurrentMtrlData + => _subFiles.MtrlData; + + internal ResolveData CurrentAvfxData + => _subFiles.AvfxData; } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index c8b3522e..329dbc2b 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -67,6 +67,15 @@ public unsafe struct ResourceHandle [FieldOffset( 0x10 )] public uint Id; + [FieldOffset( 0x28 )] + public uint FileSize; + + [FieldOffset( 0x2C )] + public uint FileSize2; + + [FieldOffset( 0x34 )] + public uint FileSize3; + [FieldOffset( 0x48 )] public byte* FileNameData; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 32538a81..b2550a69 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -138,11 +138,6 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - if( CharacterUtility.Ready ) - { - ResidentResources.Reload(); - } - Api = new PenumbraApi( this ); IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); SubscribeItemLinks(); @@ -159,6 +154,11 @@ public class Penumbra : IDalamudPlugin OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); + + if( CharacterUtility.Ready ) + { + ResidentResources.Reload(); + } } catch { diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 4a234656..8d57358a 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -239,7 +239,7 @@ public partial class ConfigWindow { if( pathTree ) { - using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table( "###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); if( table ) { foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) @@ -248,6 +248,33 @@ public partial class ConfigWindow ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); ImGui.TableNextColumn(); ImGui.TextUnformatted( collection.ModCollection.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.AssociatedGameObject.ToString("X") ); + } + } + } + } + + using( var resourceTree = ImRaii.TreeNode( "Subfile Collections" ) ) + { + if( resourceTree ) + { + using var table = ImRaii.Table( "###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + ImGuiUtil.DrawTableColumn( "Current Mtrl Data" ); + ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentMtrlData.ModCollection.Name ); + ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentMtrlData.AssociatedGameObject:X}" ); + + ImGuiUtil.DrawTableColumn( "Current Avfx Data" ); + ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentAvfxData.ModCollection.Name ); + ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentAvfxData.AssociatedGameObject:X}" ); + + foreach( var (resource, resolve) in _window._penumbra.PathResolver.ResourceCollections ) + { + ImGuiUtil.DrawTableColumn( $"0x{resource:X}" ); + ImGuiUtil.DrawTableColumn( resolve.ModCollection.Name ); + ImGuiUtil.DrawTableColumn( $"0x{resolve.AssociatedGameObject:X}" ); } } } From 5b3d5d1e67bacf56d2c95626d5d9976211c791f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Dec 2022 23:14:50 +0100 Subject: [PATCH 0650/2451] Add basic version of item swap, seemingly working for hair, tail and ears. --- Penumbra.GameData/Data/GamePaths.cs | 118 ++++- Penumbra.GameData/Data/MaterialHandling.cs | 31 ++ Penumbra.GameData/Enums/BodySlot.cs | 11 + Penumbra.GameData/Enums/Race.cs | 4 +- Penumbra.GameData/Files/IWritable.cs | 3 +- Penumbra.GameData/Files/MdlFile.cs | 5 +- Penumbra.GameData/Files/MtrlFile.cs | 5 + Penumbra.GameData/Structs/EqpEntry.cs | 73 +-- Penumbra/Interop/Structs/MtrlResource.cs | 3 + .../Meta/Manipulations/MetaManipulation.cs | 23 +- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 214 ++++++++ .../Mods/ItemSwap/EquipmentDataContainer.cs | 230 ++++++++ Penumbra/Mods/ItemSwap/ItemSwap.cs | 112 ++++ Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 134 +++++ Penumbra/Mods/ItemSwap/Swaps.cs | 185 +++++++ Penumbra/Mods/Subclasses/ModSettings.cs | 77 ++- Penumbra/UI/Classes/Combos.cs | 45 ++ Penumbra/UI/Classes/ItemSwapWindow.cs | 490 ++++++++++++++++++ .../UI/Classes/ModEditWindow.FileEditor.cs | 5 - .../UI/Classes/ModEditWindow.Materials.cs | 11 + Penumbra/UI/Classes/ModEditWindow.Meta.cs | 56 +- Penumbra/UI/Classes/ModEditWindow.cs | 15 +- 22 files changed, 1730 insertions(+), 120 deletions(-) create mode 100644 Penumbra.GameData/Data/MaterialHandling.cs create mode 100644 Penumbra/Mods/ItemSwap/CustomizationSwap.cs create mode 100644 Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs create mode 100644 Penumbra/Mods/ItemSwap/ItemSwap.cs create mode 100644 Penumbra/Mods/ItemSwap/ItemSwapContainer.cs create mode 100644 Penumbra/Mods/ItemSwap/Swaps.cs create mode 100644 Penumbra/UI/Classes/Combos.cs create mode 100644 Penumbra/UI/Classes/ItemSwapWindow.cs diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index 8e2076a4..89f2f58e 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -1,3 +1,5 @@ +using System.Text.RegularExpressions; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -5,6 +7,23 @@ namespace Penumbra.GameData.Data; public static partial class GamePaths { + private static readonly Regex RaceCodeRegex = new(@"c(?'racecode'\d{4})", RegexOptions.Compiled); + + //[GeneratedRegex(@"c(?'racecode'\d{4})")] + public static partial Regex RaceCodeParser(); + + public static partial Regex RaceCodeParser() + => RaceCodeRegex; + + public static GenderRace ParseRaceCode(string path) + { + var match = RaceCodeParser().Match(path); + return match.Success + ? Names.GenderRaceFromCode(match.Groups["racecode"].Value) + : GenderRace.Unknown; + } + + public static partial class Monster { public static partial class Imc @@ -139,7 +158,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot) - => $"chara/equipment/e{equipId.Value:D4}/model/c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; + => $"chara/equipment/e{equipId.Value:D4}/model/c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; } public static partial class Mtrl @@ -148,7 +167,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) - => $"{FolderPath(equipId, variant)}/mt_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; + => $"{FolderPath(equipId, variant)}/mt_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; public static string FolderPath(SetId equipId, byte variant) => $"chara/equipment/e{equipId.Value:D4}/material/v{variant:D4}"; @@ -160,7 +179,25 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + => $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + } + + public static partial class Avfx + { + //[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")] + //public static partial Regex Regex(); + + public static string Path(SetId equipId, byte effectId) + => $"chara/equipment/e{equipId.Value:D4}/vfx/eff/ve{effectId:D4}.avfx"; + } + + public static partial class Decal + { + //[GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")] + //public static partial Regex Regex(); + + public static string Path(byte decalId) + => $"chara/common/texture/decal_equip/-decal_{decalId:D3}.tex"; } } @@ -181,7 +218,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot) - => $"chara/accessory/a{accessoryId.Value:D4}/model/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl"; + => $"chara/accessory/a{accessoryId.Value:D4}/model/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl"; } public static partial class Mtrl @@ -190,7 +227,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) - => $"{FolderPath(accessoryId, variant)}/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; + => $"{FolderPath(accessoryId, variant)}/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; public static string FolderPath(SetId accessoryId, byte variant) => $"chara/accessory/a{accessoryId.Value:D4}/material/v{variant:D4}"; @@ -202,7 +239,7 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + => $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; } } @@ -214,7 +251,19 @@ public static partial class GamePaths // public static partial Regex Regex(); public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type) - => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; + => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; + } + + public static partial class Phyb + { + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId) + => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.phyb"; + } + + public static partial class Sklb + { + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId) + => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.sklb"; } public static partial class Mtrl @@ -222,11 +271,52 @@ public static partial class GamePaths // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] // public static partial Regex Regex(); - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string suffix, - CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue) - => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material/" - + (variant != byte.MaxValue ? $"v{variant:D4}/" : string.Empty) - + $"mt_c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}_{suffix}.mtrl"; + public static string FolderPath(GenderRace raceCode, BodySlot slot, SetId slotId, byte variant = byte.MaxValue) + => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material{(variant != byte.MaxValue ? $"/v{variant:D4}" : string.Empty)}"; + + public static string HairPath(GenderRace raceCode, SetId slotId, string fileName, out GenderRace actualGr) + { + actualGr = MaterialHandling.GetGameGenderRace(raceCode, slotId); + var folder = FolderPath(actualGr, BodySlot.Hair, slotId, 1); + return actualGr == raceCode + ? $"{folder}{fileName}" + : $"{folder}/mt_c{actualGr.ToRaceCode()}{fileName[9..]}"; + } + + public static string TailPath(GenderRace raceCode, SetId slotId, string fileName, byte variant, out SetId actualSlotId) + { + switch (raceCode) + { + case GenderRace.HrothgarMale: + case GenderRace.HrothgarFemale: + case GenderRace.HrothgarMaleNpc: + case GenderRace.HrothgarFemaleNpc: + var folder = FolderPath(raceCode, BodySlot.Tail, 1, variant == byte.MaxValue ? (byte)1 : variant); + actualSlotId = 1; + return $"{folder}{fileName}"; + default: + actualSlotId = slotId; + return $"{FolderPath(raceCode, BodySlot.Tail, slotId, variant)}{fileName}"; + } + } + + public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string fileName, + out GenderRace actualGr, out SetId actualSlotId, byte variant = byte.MaxValue) + { + switch (slot) + { + case BodySlot.Hair: + actualSlotId = slotId; + return HairPath(raceCode, slotId, fileName, out actualGr); + case BodySlot.Tail: + actualGr = raceCode; + return TailPath(raceCode, slotId, fileName, variant, out actualSlotId); + default: + actualSlotId = slotId; + actualGr = raceCode; + return $"{FolderPath(raceCode, slot, slotId, variant)}{fileName}"; + } + } } public static partial class Tex @@ -236,10 +326,10 @@ public static partial class GamePaths public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false, CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0') - => $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/" + => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/" + (minus ? "--" : string.Empty) + (variant != byte.MaxValue ? $"v{variant:D2}_" : string.Empty) - + $"c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; + + $"c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; // [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")] diff --git a/Penumbra.GameData/Data/MaterialHandling.cs b/Penumbra.GameData/Data/MaterialHandling.cs new file mode 100644 index 00000000..09bbab51 --- /dev/null +++ b/Penumbra.GameData/Data/MaterialHandling.cs @@ -0,0 +1,31 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Data; + +public static class MaterialHandling +{ + public static GenderRace GetGameGenderRace(GenderRace actualGr, SetId hairId) + { + // Hrothgar do not share hairstyles. + if (actualGr is GenderRace.HrothgarFemale or GenderRace.HrothgarMale) + return actualGr; + + // Some hairstyles are miqo'te specific but otherwise shared. + if (hairId.Value is >= 101 and <= 115) + { + if (actualGr is GenderRace.MiqoteFemale or GenderRace.MiqoteMale) + return actualGr; + + return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale; + } + + // All hairstyles above 116 are shared except for Hrothgar + if (hairId.Value is >= 116 and <= 200) + { + return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale; + } + + return actualGr; + } +} diff --git a/Penumbra.GameData/Enums/BodySlot.cs b/Penumbra.GameData/Enums/BodySlot.cs index 8eb6513b..92b4c6ce 100644 --- a/Penumbra.GameData/Enums/BodySlot.cs +++ b/Penumbra.GameData/Enums/BodySlot.cs @@ -37,6 +37,17 @@ public static class BodySlotEnumExtension BodySlot.Zear => 'z', _ => throw new InvalidEnumArgumentException(), }; + + public static CustomizationType ToCustomizationType(this BodySlot value) + => value switch + { + BodySlot.Hair => CustomizationType.Hair, + BodySlot.Face => CustomizationType.Face, + BodySlot.Tail => CustomizationType.Tail, + BodySlot.Body => CustomizationType.Body, + BodySlot.Zear => CustomizationType.Zear, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; } public static partial class Names diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index 1cf4f1ff..7f86cb6c 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -328,7 +328,7 @@ public static class RaceEnumExtensions VieraFemaleNpc => "1804", UnknownMaleNpc => "9104", UnknownFemaleNpc => "9204", - _ => throw new InvalidEnumArgumentException(), + _ => string.Empty, }; } @@ -427,7 +427,7 @@ public static partial class Names "1804" => VieraFemaleNpc, "9104" => UnknownMaleNpc, "9204" => UnknownFemaleNpc, - _ => throw new KeyNotFoundException(), + _ => Unknown, }; } diff --git a/Penumbra.GameData/Files/IWritable.cs b/Penumbra.GameData/Files/IWritable.cs index afad2e94..0a170af9 100644 --- a/Penumbra.GameData/Files/IWritable.cs +++ b/Penumbra.GameData/Files/IWritable.cs @@ -1,6 +1,7 @@ namespace Penumbra.GameData.Files; public interface IWritable -{ +{ + public bool Valid { get; } public byte[] Write(); } \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index 09efb624..a7d65ee8 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -83,7 +83,9 @@ public partial class MdlFile : IWritable public Shape[] Shapes; // Raw, unparsed data. - public byte[] RemainingData; + public byte[] RemainingData; + + public bool Valid { get; } public MdlFile(byte[] data) { @@ -180,6 +182,7 @@ public partial class MdlFile : IWritable BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r); RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position)); + Valid = true; } private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index b9f46c1f..7508688f 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -231,6 +231,9 @@ public partial class MtrlFile : IWritable { public string Path; public ushort Flags; + + public bool DX11 + => (Flags & 0x8000) != 0; } public struct Constant @@ -251,6 +254,7 @@ public partial class MtrlFile : IWritable public readonly uint Version; + public bool Valid { get; } public Texture[] Textures; public UvSet[] UvSets; @@ -368,6 +372,7 @@ public partial class MtrlFile : IWritable ShaderPackage.Constants = r.ReadStructuresAsArray(constantCount); ShaderPackage.Samplers = r.ReadStructuresAsArray(samplerCount); ShaderPackage.ShaderValues = r.ReadStructuresAsArray(shaderValueListSize / 4); + Valid = true; } private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets) diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index b628aa63..49b2f66f 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -87,39 +87,42 @@ public enum EqpEntry : ulong public static class Eqp { // cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags - public const EqpEntry DefaultEntry = ( EqpEntry )0x3fe00070603f00; + public const EqpEntry DefaultEntry = (EqpEntry)0x3fe00070603f00; - public static (int, int) BytesAndOffset( EquipSlot slot ) + public static (int, int) BytesAndOffset(EquipSlot slot) { return slot switch { - EquipSlot.Body => ( 2, 0 ), - EquipSlot.Legs => ( 1, 2 ), - EquipSlot.Hands => ( 1, 3 ), - EquipSlot.Feet => ( 1, 4 ), - EquipSlot.Head => ( 3, 5 ), + EquipSlot.Body => (2, 0), + EquipSlot.Legs => (1, 2), + EquipSlot.Hands => (1, 3), + EquipSlot.Feet => (1, 4), + EquipSlot.Head => (3, 5), _ => throw new InvalidEnumArgumentException(), }; } - public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value ) + public static EqpEntry ShiftAndMask(this EqpEntry entry, EquipSlot slot) + { + var (_, offset) = BytesAndOffset(slot); + var mask = Mask(slot); + return (EqpEntry)((ulong)(entry & mask) >> (offset * 8)); + } + + public static EqpEntry FromSlotAndBytes(EquipSlot slot, byte[] value) { EqpEntry ret = 0; - var (bytes, offset) = BytesAndOffset( slot ); - if( bytes != value.Length ) - { + var (bytes, offset) = BytesAndOffset(slot); + if (bytes != value.Length) throw new ArgumentException(); - } - for( var i = 0; i < bytes; ++i ) - { - ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) ); - } + for (var i = 0; i < bytes; ++i) + ret |= (EqpEntry)((ulong)value[i] << ((offset + i) * 8)); return ret; } - public static EqpEntry Mask( EquipSlot slot ) + public static EqpEntry Mask(EquipSlot slot) { return slot switch { @@ -132,7 +135,7 @@ public static class Eqp }; } - public static EquipSlot ToEquipSlot( this EqpEntry entry ) + public static EquipSlot ToEquipSlot(this EqpEntry entry) { return entry switch { @@ -211,7 +214,7 @@ public static class Eqp }; } - public static string ToLocalName( this EqpEntry entry ) + public static string ToLocalName(this EqpEntry entry) { return entry switch { @@ -289,25 +292,25 @@ public static class Eqp }; } - private static EqpEntry[] GetEntriesForSlot( EquipSlot slot ) + private static EqpEntry[] GetEntriesForSlot(EquipSlot slot) { - return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) ) - .Where( e => e.ToEquipSlot() == slot ) - .ToArray(); + return ((EqpEntry[])Enum.GetValues(typeof(EqpEntry))) + .Where(e => e.ToEquipSlot() == slot) + .ToArray(); } - public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body ); - public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs ); - public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands ); - public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet ); - public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head ); + public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot(EquipSlot.Body); + public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot(EquipSlot.Legs); + public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot(EquipSlot.Hands); + public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot(EquipSlot.Feet); + public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot(EquipSlot.Head); - public static readonly IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() + public static readonly IReadOnlyDictionary EqpAttributes = new Dictionary() { - [ EquipSlot.Body ] = EqpAttributesBody, - [ EquipSlot.Legs ] = EqpAttributesLegs, - [ EquipSlot.Hands ] = EqpAttributesHands, - [ EquipSlot.Feet ] = EqpAttributesFeet, - [ EquipSlot.Head ] = EqpAttributesHead, + [EquipSlot.Body] = EqpAttributesBody, + [EquipSlot.Legs] = EqpAttributesLegs, + [EquipSlot.Hands] = EqpAttributesHands, + [EquipSlot.Feet] = EqpAttributesFeet, + [EquipSlot.Head] = EqpAttributesHead, }; -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index ff5b6abf..28756877 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -25,4 +25,7 @@ public unsafe struct MtrlResource public byte* TexString( int idx ) => StringList + *( TexSpace + 4 + idx * 8 ); + + public bool TexIsDX11( int idx ) + => *(TexSpace + 5 + idx * 8) >= 0x8000; } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f6ff2d94..5cc96f98 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -198,6 +198,25 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa }; } + public MetaManipulation WithEntryOf( MetaManipulation other ) + { + if( ManipulationType != other.ManipulationType ) + { + return this; + } + + return ManipulationType switch + { + Type.Eqp => Eqp.Copy( other.Eqp.Entry ), + Type.Gmp => Gmp.Copy( other.Gmp.Entry ), + Type.Eqdp => Eqdp.Copy( other.Eqdp.Entry ), + Type.Est => Est.Copy( other.Est.Entry ), + Type.Rsp => Rsp.Copy( other.Rsp.Entry ), + Type.Imc => Imc.Copy( other.Imc.Entry ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + public override bool Equals( object? obj ) => obj is MetaManipulation other && Equals( other ); @@ -237,8 +256,8 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa => ManipulationType switch { Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort) Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", + Type.Eqdp => $"{( ushort )Eqdp.Entry:X}", + Type.Eqp => $"{( ulong )Eqp.Entry:X}", Type.Est => $"{Est.Entry}", Type.Gmp => $"{Gmp.Entry.Value}", Type.Rsp => $"{Rsp.Entry}", diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs new file mode 100644 index 00000000..46b85f00 --- /dev/null +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.ItemSwap; + +public static class CustomizationSwap +{ + /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. + public static bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, out FileSwap mdl ) + { + if( idFrom.Value > byte.MaxValue ) + { + mdl = new FileSwap(); + return false; + } + + var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() ); + var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() ); + + if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) ) + { + return false; + } + + var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1; + + foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() ) + { + var name = materialFileName; + foreach( var variant in Enumerable.Range( 1, range ) ) + { + name = materialFileName; + if( !CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged, out var mtrl ) ) + { + return false; + } + + mdl.ChildSwaps.Add( mtrl ); + } + + materialFileName = name; + } + + return true; + } + + public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true ) + => condition + ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" ) + : path; + + public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true ) + => ReplaceAnyId( path, 'c', ( ushort )to, condition ); + + public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true ) + => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition ); + + public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true ) + => condition + ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) + : path; + + public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) + => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); + + public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true ) + => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition ); + + public static string AddSuffix( string path, string ext, string suffix, bool condition = true ) + => condition + ? path.Replace( ext, suffix + ext ) + : path; + + public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, + ref string fileName, ref bool dataWasChanged, out FileSwap mtrl ) + { + variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; + var mtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant ); + var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant ); + + var newFileName = fileName; + newFileName = ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); + newFileName = ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); + newFileName = AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race ); + newFileName = AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value ); + + var actualMtrlFromPath = mtrlFromPath; + if( newFileName != fileName ) + { + actualMtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, newFileName, out _, out _, variant ); + fileName = newFileName; + dataWasChanged = true; + } + + if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, out mtrl, actualMtrlFromPath ) ) + { + return false; + } + + if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shpk ) ) + { + return false; + } + + mtrl.ChildSwaps.Add( shpk ); + + foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) + { + if( !CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged, out var tex ) ) + { + return false; + } + + mtrl.ChildSwaps.Add( tex ); + } + + return true; + } + + public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, + ref bool dataWasChanged, out FileSwap tex ) + { + var path = texture.Path; + var addedDashes = false; + if( texture.DX11 ) + { + var fileName = Path.GetFileName( path ); + if( !fileName.StartsWith( "--" ) ) + { + path = path.Replace( fileName, $"--{fileName}" ); + addedDashes = true; + } + } + + var newPath = ReplaceAnyRace( path, race ); + newPath = ReplaceAnyBody( newPath, slot, idFrom ); + if( newPath != path ) + { + texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; + dataWasChanged = true; + } + + return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path ); + } + + + public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk ) + { + var path = $"shader/sm5/shpk/{shaderName}"; + return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk ); + } + + /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. + public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > metaChanges, BodySlot slot, GenderRace gr, SetId idFrom, + SetId idTo, out MetaSwap? est ) + { + var (gender, race) = gr.Split(); + var estSlot = slot switch + { + BodySlot.Hair => EstManipulation.EstType.Hair, + BodySlot.Body => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; + if( estSlot == 0 ) + { + est = null; + return true; + } + + var fromDefault = new EstManipulation( gender, race, estSlot, idFrom.Value, EstFile.GetDefault( estSlot, gr, idFrom.Value ) ); + var toDefault = new EstManipulation( gender, race, estSlot, idTo.Value, EstFile.GetDefault( estSlot, gr, idTo.Value ) ); + est = new MetaSwap( metaChanges, fromDefault, toDefault ); + + if( est.SwapApplied.Est.Entry >= 2 ) + { + if( !CreatePhyb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var phyb ) ) + { + return false; + } + + if( !CreateSklb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var sklb ) ) + { + return false; + } + + est.ChildSwaps.Add( phyb ); + est.ChildSwaps.Add( sklb ); + } + + return true; + } + + public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap phyb ) + { + var phybPath = GamePaths.Character.Phyb.Path( race, slot, estEntry ); + return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb ); + } + + public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap sklb ) + { + var sklbPath = GamePaths.Character.Sklb.Path( race, slot, estEntry ); + return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs b/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs new file mode 100644 index 00000000..70847af8 --- /dev/null +++ b/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods.ItemSwap; + +public class EquipmentDataContainer +{ + public Item Item; + public EquipSlot Slot; + public SetId ModelId; + public byte Variant; + + public ImcManipulation ImcData; + + public EqpManipulation EqpData; + public GmpManipulation GmpData; + + // Example: Abyssos Helm / Body + public string AvfxPath = string.Empty; + + // Example: Dodore Doublet, but unknown what it does? + public string SoundPath = string.Empty; + + // Example: Crimson Standard Bracelet + public string DecalPath = string.Empty; + + // Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. + public string AnimationPath = string.Empty; + + public Dictionary< GenderRace, GenderRaceContainer > Files = new(); + + public struct GenderRaceContainer + { + public EqdpManipulation Eqdp; + public GenderRace ModelRace; + public GenderRace MaterialRace; + public EstManipulation Est; + public string MdlPath; + public MtrlContainer[] MtrlPaths; + } + + public struct MtrlContainer + { + public string MtrlPath; + public string[] Textures; + public string Shader; + + public MtrlContainer( string mtrlPath ) + { + MtrlPath = mtrlPath; + var file = Dalamud.GameData.GetFile( mtrlPath ); + if( file != null ) + { + var mtrl = new MtrlFile( file.Data ); + Textures = mtrl.Textures.Select( t => t.Path ).ToArray(); + Shader = $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}"; + } + else + { + Textures = Array.Empty< string >(); + Shader = string.Empty; + } + } + } + + + private static EstManipulation GetEstEntry( GenderRace genderRace, SetId setId, EquipSlot slot ) + { + if( slot == EquipSlot.Head ) + { + var entry = EstFile.GetDefault( EstManipulation.EstType.Head, genderRace, setId.Value ); + return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Head, setId.Value, entry ); + } + + if( slot == EquipSlot.Body ) + { + var entry = EstFile.GetDefault( EstManipulation.EstType.Body, genderRace, setId.Value ); + return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Body, setId.Value, entry ); + } + + return default; + } + + private static GenderRaceContainer GetGenderRace( GenderRace genderRace, SetId modelId, EquipSlot slot, ushort materialId ) + { + var ret = new GenderRaceContainer() + { + Eqdp = GetEqdpEntry( genderRace, modelId, slot ), + Est = GetEstEntry( genderRace, modelId, slot ), + }; + ( ret.ModelRace, ret.MaterialRace ) = TraverseEqdpTree( genderRace, modelId, slot ); + ret.MdlPath = GamePaths.Equipment.Mdl.Path( modelId, ret.ModelRace, slot ); + ret.MtrlPaths = MtrlPaths( ret.MdlPath, ret.MaterialRace, modelId, materialId ); + return ret; + } + + private static EqdpManipulation GetEqdpEntry( GenderRace genderRace, SetId modelId, EquipSlot slot ) + { + var entry = ExpandedEqdpFile.GetDefault( genderRace, slot.IsAccessory(), modelId.Value ); + return new EqdpManipulation( entry, slot, genderRace.Split().Item1, genderRace.Split().Item2, modelId.Value ); + } + + private static MtrlContainer[] MtrlPaths( string mdlPath, GenderRace mtrlRace, SetId modelId, ushort materialId ) + { + var file = Dalamud.GameData.GetFile( mdlPath ); + if( file == null ) + { + return Array.Empty< MtrlContainer >(); + } + + var mdl = new MdlFile( Dalamud.GameData.GetFile( mdlPath )!.Data ); + var basePath = GamePaths.Equipment.Mtrl.FolderPath( modelId, ( byte )materialId ); + var equipPart = $"e{modelId.Value:D4}"; + var racePart = $"c{mtrlRace.ToRaceCode()}"; + + return mdl.Materials + .Where( m => m.Contains( equipPart ) ) + .Select( m => new MtrlContainer( $"{basePath}{m.Replace( "c0101", racePart )}" ) ) + .ToArray(); + } + + private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot ) + { + var model = GenderRace.Unknown; + var material = GenderRace.Unknown; + var accessory = slot.IsAccessory(); + foreach( var gr in genderRace.Dependencies() ) + { + var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value ); + var (b1, b2) = entry.ToBits( slot ); + if( b1 && material == GenderRace.Unknown ) + { + material = gr; + if( model != GenderRace.Unknown ) + { + return ( model, material ); + } + } + + if( b2 && model == GenderRace.Unknown ) + { + model = gr; + if( material != GenderRace.Unknown ) + { + return ( model, material ); + } + } + } + + return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale ); + } + + + public EquipmentDataContainer( Item i ) + { + Item = i; + LookupItem( i, out Slot, out ModelId, out Variant ); + LookupImc( ModelId, Variant, Slot ); + EqpData = new EqpManipulation( ExpandedEqpFile.GetDefault( ModelId.Value ), Slot, ModelId.Value ); + GmpData = Slot == EquipSlot.Head ? new GmpManipulation( ExpandedGmpFile.GetDefault( ModelId.Value ), ModelId.Value ) : default; + + + foreach( var genderRace in Enum.GetValues< GenderRace >() ) + { + if( CharacterUtility.EqdpIdx( genderRace, Slot.IsAccessory() ) < 0 ) + { + continue; + } + + Files[ genderRace ] = GetGenderRace( genderRace, ModelId, Slot, ImcData.Entry.MaterialId ); + } + } + + + private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) + { + slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); + if( !slot.IsEquipment() ) + { + throw new ItemSwap.InvalidItemTypeException(); + } + + modelId = ( ( Quad )i.ModelMain ).A; + variant = ( byte )( ( Quad )i.ModelMain ).B; + } + + + + private void LookupImc( SetId modelId, byte variant, EquipSlot slot ) + { + var imc = ImcFile.GetDefault( GamePaths.Equipment.Imc.Path( modelId ), slot, variant, out var exists ); + if( !exists ) + { + throw new ItemSwap.InvalidImcException(); + } + + ImcData = new ImcManipulation( slot, variant, modelId.Value, imc ); + if( imc.DecalId != 0 ) + { + DecalPath = GamePaths.Equipment.Decal.Path( imc.DecalId ); + } + + // TODO: Figure out how this works. + if( imc.SoundId != 0 ) + { + SoundPath = string.Empty; + } + + if( imc.VfxId != 0 ) + { + AvfxPath = GamePaths.Equipment.Avfx.Path( modelId, imc.VfxId ); + } + + // TODO: Figure out how this works. + if( imc.MaterialAnimationId != 0 ) + { + AnimationPath = string.Empty; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs new file mode 100644 index 00000000..2e6585c3 --- /dev/null +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.ItemSwap; + +public static class ItemSwap +{ + public class InvalidItemTypeException : Exception + { } + + public class InvalidImcException : Exception + { } + + public class IdUnavailableException : Exception + { } + + private static bool LoadFile( FullPath path, out byte[] data ) + { + if( path.FullName.Length > 0 ) + { + try + { + if( path.IsRooted ) + { + data = File.ReadAllBytes( path.FullName ); + return true; + } + + var file = Dalamud.GameData.GetFile( path.InternalName.ToString() ); + if( file != null ) + { + data = file.Data; + return true; + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not load file {path}:\n{e}" ); + } + } + + data = Array.Empty< byte >(); + return false; + } + + public class GenericFile : IWritable + { + public readonly byte[] Data; + public bool Valid { get; } + + public GenericFile( FullPath path ) + => Valid = LoadFile( path, out Data ); + + public byte[] Write() + => Data; + + public static readonly GenericFile Invalid = new(FullPath.Empty); + } + + public static bool LoadFile( FullPath path, [NotNullWhen( true )] out GenericFile? file ) + { + file = new GenericFile( path ); + if( file.Valid ) + { + return true; + } + + file = null; + return false; + } + + public static bool LoadMdl( FullPath path, [NotNullWhen( true )] out MdlFile? file ) + { + try + { + if( LoadFile( path, out byte[] data ) ) + { + file = new MdlFile( data ); + return true; + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse file {path} to Mdl:\n{e}" ); + } + + file = null; + return false; + } + + public static bool LoadMtrl( FullPath path, [NotNullWhen( true )] out MtrlFile? file ) + { + try + { + if( LoadFile( path, out byte[] data ) ) + { + file = new MtrlFile( data ); + return true; + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse file {path} to Mtrl:\n{e}" ); + } + + file = null; + return false; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs new file mode 100644 index 00000000..4fc07cd4 --- /dev/null +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.ItemSwap; + +public class ItemSwapContainer +{ + private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); + private HashSet< MetaManipulation > _modManipulations = new(); + + public IReadOnlyDictionary< Utf8GamePath, FullPath > ModRedirections + => _modRedirections; + + public IReadOnlySet< MetaManipulation > ModManipulations + => _modManipulations; + + public readonly List< Swap > Swaps = new(); + public bool Loaded { get; private set; } + + public void Clear() + { + Swaps.Clear(); + Loaded = false; + } + + public enum WriteType + { + UseSwaps, + NoSwaps, + } + + public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps ) + { + var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); + var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); + var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); + try + { + foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) ) + { + switch( swap ) + { + case FileSwap file: + // Skip, nothing to do + if( file.SwapToModdedEqualsOriginal ) + { + continue; + } + + + if( writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged ) + { + convertedSwaps.TryAdd( file.SwapFromRequestPath, file.SwapToModded ); + } + else + { + var path = file.GetNewPath( mod.ModPath.FullName ); + var bytes = file.FileData.Write(); + Directory.CreateDirectory( Path.GetDirectoryName( path )! ); + File.WriteAllBytes( path, bytes ); + convertedFiles.TryAdd( file.SwapFromRequestPath, new FullPath( path ) ); + } + + break; + case MetaSwap meta: + if( !meta.SwapAppliedIsDefault ) + { + convertedManips.Add( meta.SwapApplied ); + } + + break; + } + } + + Penumbra.ModManager.OptionSetFiles( mod, -1, 0, convertedFiles ); + Penumbra.ModManager.OptionSetFileSwaps( mod, -1, 0, convertedSwaps ); + Penumbra.ModManager.OptionSetManipulations( mod, -1, 0, convertedManips ); + return true; + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write FileSwapContainer to {mod.ModPath}:\n{e}" ); + return false; + } + } + + public void LoadMod( Mod? mod, ModSettings? settings ) + { + Clear(); + if( mod == null ) + { + _modRedirections = new Dictionary< Utf8GamePath, FullPath >(); + _modManipulations = new HashSet< MetaManipulation >(); + } + else + { + ( _modRedirections, _modManipulations ) = ModSettings.GetResolveData( mod, settings ); + } + } + + public ItemSwapContainer() + { + LoadMod( null, null ); + } + + + public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to ) + { + if( !CustomizationSwap.CreateMdl( ModRedirections, slot, race, from, to, out var mdl ) ) + { + return false; + } + + if( !CustomizationSwap.CreateEst( ModRedirections, _modManipulations, slot, race, from, to, out var est ) ) + { + return false; + } + + Swaps.Add( mdl ); + if( est != null ) + { + Swaps.Add( est ); + } + + Loaded = true; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs new file mode 100644 index 00000000..d425e476 --- /dev/null +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -0,0 +1,185 @@ +using System; +using Penumbra.GameData.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using Penumbra.GameData.Enums; + +namespace Penumbra.Mods.ItemSwap; + +public class Swap +{ + /// Any further swaps belonging specifically to this tree of changes. + public List< Swap > ChildSwaps = new(); + + public IEnumerable< Swap > WithChildren() + => ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this ); +} + +public sealed class MetaSwap : Swap +{ + /// The default value of a specific meta manipulation that needs to be redirected. + public MetaManipulation SwapFrom; + + /// The default value of the same Meta entry of the redirected item. + public MetaManipulation SwapToDefault; + + /// The modded value of the same Meta entry of the redirected item, or the same as SwapToDefault if unmodded. + public MetaManipulation SwapToModded; + + /// The modded value applied to the specific meta manipulation target before redirection. + public MetaManipulation SwapApplied; + + /// Whether SwapToModded equals SwapToDefault. + public bool SwapToIsDefault; + + /// Whether the applied meta manipulation does not change anything against the default. + public bool SwapAppliedIsDefault; + + /// + /// Create a new MetaSwap from the original meta identifier and the target meta identifier. + /// + /// A set of modded meta manipulations to consider. This is not manipulated, but can not be IReadOnly because TryGetValue is not available for that. + /// The original meta identifier with its default value. + /// The target meta identifier with its default value. + public MetaSwap( HashSet< MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo ) + { + SwapFrom = manipFrom; + SwapToDefault = manipTo; + + if( manipulations.TryGetValue( manipTo, out var actual ) ) + { + SwapToModded = actual; + SwapToIsDefault = false; + } + else + { + SwapToModded = manipTo; + SwapToIsDefault = true; + } + + SwapApplied = SwapFrom.WithEntryOf( SwapToModded ); + SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom ); + } +} + +public sealed class FileSwap : Swap +{ + /// The file type, used for bookkeeping. + public ResourceType Type; + + /// The binary or parsed data of the file at SwapToModded. + public IWritable FileData = ItemSwap.GenericFile.Invalid; + + /// The path that would be requested without manipulated parent files. + public string SwapFromPreChangePath = string.Empty; + + /// The Path that needs to be redirected. + public Utf8GamePath SwapFromRequestPath; + + /// The path that the game should request instead, if no mods are involved. + public Utf8GamePath SwapToRequestPath; + + /// The path to the actual file that should be loaded. This can be the same as SwapToRequestPath or a file on the drive. + public FullPath SwapToModded; + + /// Whether the target file is an actual game file. + public bool SwapToModdedExistsInGame; + + /// Whether the target file could be read either from the game or the drive. + public bool SwapToModdedExists + => FileData.Valid; + + /// Whether SwapToModded is a path to a game file that equals SwapFromGamePath. + public bool SwapToModdedEqualsOriginal; + + /// Whether the data in FileData was manipulated from the original file. + public bool DataWasChanged; + + /// Whether SwapFromPreChangePath equals SwapFromRequest. + public bool SwapFromChanged; + + public string GetNewPath( string newMod ) + => Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() ); + + public MdlFile? AsMdl() + => FileData as MdlFile; + + public MtrlFile? AsMtrl() + => FileData as MtrlFile; + + /// + /// Create a full swap container for a specific file type using a modded redirection set, the actually requested path and the game file it should load instead after the swap. + /// + /// The file type. Mdl and Mtrl have special file loading treatment. + /// The set of redirections that need to be considered. + /// The path the game is going to request when loading the file. + /// The unmodded path to the file the game is supposed to load instead. + /// A full swap container with the actual file in memory. + /// True if everything could be read correctly, false otherwise. + public static bool CreateSwap( ResourceType type, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, out FileSwap swap, + string? swapFromPreChange = null ) + { + swap = new FileSwap + { + Type = type, + FileData = ItemSwap.GenericFile.Invalid, + DataWasChanged = false, + SwapFromPreChangePath = swapFromPreChange ?? swapFromRequest, + SwapFromChanged = swapFromPreChange != swapFromRequest, + SwapFromRequestPath = Utf8GamePath.Empty, + SwapToRequestPath = Utf8GamePath.Empty, + SwapToModded = FullPath.Empty, + }; + + if( swapFromRequest.Length == 0 + || swapToRequest.Length == 0 + || !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath ) + || !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) ) + { + return false; + } + + swap.SwapToModded = redirections.TryGetValue( swap.SwapToRequestPath, out var p ) ? p : new FullPath( swap.SwapToRequestPath ); + swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && Dalamud.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); + swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); + + swap.FileData = type switch + { + ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, + ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, + _ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, + }; + + return swap.SwapToModdedExists; + } + + + /// + /// Convert a single file redirection to use the file name and extension given by type and the files SHA256 hash, if possible. + /// + /// The set of redirections that need to be considered. + /// The in- and output path for a file + /// Will be set to true if was changed. + /// Will be updated. + public static bool CreateShaRedirection( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap ) + { + var oldFilename = Path.GetFileName( path ); + var hash = SHA256.HashData( swap.FileData.Write() ); + var name = + $"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}"; + var newPath = path.Replace( oldFilename, name ); + if( !CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString(), out var newSwap ) ) + { + return false; + } + + path = newPath; + dataWasChanged = true; + swap = newSwap; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 7b2a23ab..845456ae 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -5,6 +5,8 @@ using System.Numerics; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -34,6 +36,56 @@ public class ModSettings Settings = mod.Groups.Select( g => g.DefaultSettings ).ToList(), }; + // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. + public static (Dictionary< Utf8GamePath, FullPath >, HashSet< MetaManipulation >) GetResolveData( Mod mod, ModSettings? settings ) + { + if( settings == null ) + { + settings = DefaultSettings( mod ); + } + else + { + settings.AddMissingSettings( mod ); + } + + var dict = new Dictionary< Utf8GamePath, FullPath >(); + var set = new HashSet< MetaManipulation >(); + + void AddOption( ISubMod option ) + { + foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) ) + { + dict.TryAdd( path, file ); + } + + foreach( var manip in option.Manipulations ) + { + set.Add( manip ); + } + } + + foreach( var (group, index) in mod.Groups.WithIndex().OrderByDescending( g => g.Value.Priority ) ) + { + if( group.Type is GroupType.Single ) + { + AddOption( group[ ( int )settings.Settings[ index ] ] ); + } + else + { + foreach( var (option, optionIdx) in group.WithIndex().OrderByDescending( o => group.OptionPriority( o.Index ) ) ) + { + if( ( ( settings.Settings[ index ] >> optionIdx ) & 1 ) == 1 ) + { + AddOption( option ); + } + } + } + } + + AddOption( mod.Default ); + return ( dict, set ); + } + // Automatically react to changes in a mods available options. public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { @@ -42,7 +94,7 @@ public class ModSettings case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert( groupIdx, mod.Groups[groupIdx].DefaultSettings ); + Settings.Insert( groupIdx, mod.Groups[ groupIdx ].DefaultSettings ); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. @@ -59,7 +111,7 @@ public class ModSettings { GroupType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), GroupType.Multi => 1u << ( int )config, - _ => config, + _ => config, }; return config != Settings[ groupIdx ]; } @@ -73,7 +125,7 @@ public class ModSettings { GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, GroupType.Multi => Functions.RemoveBit( config, optionIdx ), - _ => config, + _ => config, }; return config != Settings[ groupIdx ]; } @@ -90,7 +142,7 @@ public class ModSettings { GroupType.Single => config == optionIdx ? ( uint )movedToIdx : config, GroupType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), - _ => config, + _ => config, }; return config != Settings[ groupIdx ]; } @@ -104,27 +156,28 @@ public class ModSettings { GroupType.Single => ( uint )Math.Min( value, group.Count - 1 ), GroupType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), - _ => value, + _ => value, }; // Set a setting. Ensures that there are enough settings and fixes the setting beforehand. public void SetValue( Mod mod, int groupIdx, uint newValue ) { - AddMissingSettings( groupIdx + 1 ); + AddMissingSettings( mod ); var group = mod.Groups[ groupIdx ]; Settings[ groupIdx ] = FixSetting( group, newValue ); } // Add defaulted settings up to the required count. - private bool AddMissingSettings( int totalCount ) + private bool AddMissingSettings( Mod mod ) { - if( totalCount <= Settings.Count ) + var changes = false; + for( var i = Settings.Count; i < mod.Groups.Count; ++i ) { - return false; + Settings.Add( mod.Groups[ i ].DefaultSettings ); + changes = true; } - Settings.AddRange( Enumerable.Repeat( 0u, totalCount - Settings.Count ) ); - return true; + return changes; } // A simple struct conversion to easily save settings by name instead of value. @@ -147,7 +200,7 @@ public class ModSettings Priority = settings.Priority; Enabled = settings.Enabled; Settings = new Dictionary< string, long >( mod.Groups.Count ); - settings.AddMissingSettings( mod.Groups.Count ); + settings.AddMissingSettings( mod ); foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) { diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs new file mode 100644 index 00000000..0f56cd77 --- /dev/null +++ b/Penumbra/UI/Classes/Combos.cs @@ -0,0 +1,45 @@ +using Dalamud.Interface; +using OtterGui; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.UI.Classes; + +public static class Combos +{ + // Different combos to use with enums. + public static bool Race( string label, ModelRace current, out ModelRace race ) + => Race( label, 100, current, out race ); + + public static bool Race( string label, float unscaledWidth, ModelRace current, out ModelRace race ) + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); + + public static bool Gender( string label, Gender current, out Gender gender ) + => Gender( label, 120, current, out gender ); + + public static bool Gender( string label, float unscaledWidth, Gender current, out Gender gender ) + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); + + public static bool EqdpEquipSlot( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); + + public static bool EqpEquipSlot( string label, float width, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); + + public static bool AccessorySlot( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); + + public static bool SubRace( string label, SubRace current, out SubRace subRace ) + => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + + public static bool RspAttribute( string label, RspAttribute current, out RspAttribute attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, + RspAttributeExtensions.ToFullString, 0, 1 ); + + public static bool EstSlot( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); + + public static bool ImcType( string label, ObjectType current, out ObjectType type ) + => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, + ObjectTypeExtensions.ToName ); +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs new file mode 100644 index 00000000..76967dfe --- /dev/null +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -0,0 +1,490 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods; +using Penumbra.Mods.ItemSwap; + +namespace Penumbra.UI.Classes; + +public class ItemSwapWindow : IDisposable +{ + private class ItemSelector : FilterComboCache< Item > + { + public ItemSelector() + : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i + => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipmentPiece() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) + { } + + protected override string ToString( Item obj ) + => obj.Name.ToString(); + } + + public ItemSwapWindow() + { + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; + } + + public void Dispose() + { + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; + } + + private readonly ItemSelector _itemSelector = new(); + private readonly ItemSwapContainer _swapData = new(); + + private Mod? _mod; + private ModSettings? _modSettings; + private bool _dirty; + + private SwapType _lastTab = SwapType.Equipment; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId = 0; + private int _sourceId = 0; + private int _currentVariant = 1; + private Exception? _loadException = null; + + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private bool _useFileSwaps = false; + + + public void UpdateMod( Mod mod, ModSettings? settings ) + { + if( mod == _mod && settings == _modSettings ) + { + return; + } + + var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; + if( _newModName.Length == 0 || oldDefaultName == _newModName ) + { + _newModName = $"{mod.Name.Text} (Swapped)"; + } + + _mod = mod; + _modSettings = settings; + _swapData.LoadMod( _mod, _modSettings ); + _dirty = true; + } + + private void UpdateState() + { + if( !_dirty ) + { + return; + } + + _swapData.Clear(); + _loadException = null; + if( _targetId > 0 && _sourceId > 0 ) + { + try + { + switch( _lastTab ) + { + case SwapType.Equipment: break; + case SwapType.Accessory: break; + case SwapType.Hair: + + _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Face: + _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Ears: + _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Tail: + _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Weapon: break; + case SwapType.Minion: break; + case SwapType.Mount: break; + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); + _loadException = e; + } + } + } + + private static string SwapToString( Swap swap ) + { + return swap switch + { + MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", + FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}", + _ => string.Empty, + }; + } + + private string CreateDescription() + => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + + private void DrawHeaderLine( float width ) + { + var newModAvailable = _loadException == null && _swapData.Loaded; + + ImGui.SetNextItemWidth( width ); + if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) ) + { } + + ImGui.SameLine(); + var tt = "Create a new mod of the given name containing only the swap."; + if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) + { + var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); + Mod.CreateDefaultFiles( newDir ); + Penumbra.ModManager.AddMod( newDir ); + if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) + { + Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); + } + } + + + ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); + if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) + { } + + ImGui.SameLine(); + ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); + if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) + { } + + ImGui.SameLine(); + tt = "Create a new option inside this mod containing only the swap."; + if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt, + true || (!newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) )) ) + { } + + ImGui.SameLine(); + var newPos = new Vector2( ImGui.GetCursorPosX() + 10 * ImGuiHelpers.GlobalScale, ImGui.GetCursorPosY() - ( ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); + ImGui.SetCursorPos( newPos ); + ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); + ImGuiUtil.HoverTooltip( "Use File Swaps." ); + } + + private enum SwapType + { + Equipment, + Accessory, + Hair, + Face, + Ears, + Tail, + Weapon, + Minion, + Mount, + } + + private void DrawSwapBar() + { + using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None ); + + DrawHairSwap(); + DrawFaceSwap(); + DrawEarSwap(); + DrawTailSwap(); + DrawArmorSwap(); + DrawAccessorySwap(); + DrawWeaponSwap(); + DrawMinionSwap(); + DrawMountSwap(); + } + + private ImRaii.IEndObject DrawTab( SwapType newTab ) + { + using var tab = ImRaii.TabItem( newTab.ToString() ); + if( tab ) + { + _dirty = _lastTab != newTab; + _lastTab = newTab; + } + + UpdateState(); + + return tab; + } + + private void DrawArmorSwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Equipment ); + if( !tab ) + { + return; + } + } + + private void DrawAccessorySwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Accessory ); + if( !tab ) + { + return; + } + } + + private void DrawHairSwap() + { + using var tab = DrawTab( SwapType.Hair ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + DrawTargetIdInput( "Take this Hairstyle" ); + DrawSourceIdInput(); + DrawGenderInput(); + } + + private void DrawFaceSwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Face ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + DrawTargetIdInput( "Take this Face Type" ); + DrawSourceIdInput(); + DrawGenderInput(); + } + + private void DrawTailSwap() + { + using var tab = DrawTab( SwapType.Tail ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + DrawTargetIdInput( "Take this Tail Type" ); + DrawSourceIdInput(); + DrawGenderInput("for all", 2); + } + + + private void DrawEarSwap() + { + using var tab = DrawTab( SwapType.Ears ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + DrawTargetIdInput( "Take this Ear Type" ); + DrawSourceIdInput(); + DrawGenderInput( "for all Viera", 0 ); + } + + + private void DrawWeaponSwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Weapon ); + if( !tab ) + { + return; + } + } + + private void DrawMinionSwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Minion ); + if( !tab ) + { + return; + } + } + + private void DrawMountSwap() + { + using var disabled = ImRaii.Disabled(); + using var tab = DrawTab( SwapType.Mount ); + if( !tab ) + { + return; + } + } + + private const float InputWidth = 120; + + private void DrawTargetIdInput( string text = "Take this ID" ) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( text ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) ) + _targetId = Math.Clamp( _targetId, 0, byte.MaxValue ); + _dirty |= ImGui.IsItemDeactivatedAfterEdit(); + } + + private void DrawSourceIdInput( string text = "and put it on this one" ) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( text ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); + if (ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 )) + _sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue ); + _dirty |= ImGui.IsItemDeactivatedAfterEdit(); + } + + private void DrawVariantInput( string text ) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( text ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); + if( ImGui.InputInt( "##variantId", ref _currentVariant, 0, 0 ) ) + _currentVariant = Math.Clamp( _currentVariant, 0, byte.MaxValue ); + _dirty |= ImGui.IsItemDeactivatedAfterEdit(); + } + + private void DrawGenderInput( string text = "for all", int drawRace = 1 ) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( text ); + + ImGui.TableNextColumn(); + _dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender ); + if( drawRace == 1 ) + { + ImGui.SameLine(); + _dirty |= Combos.Race( "##Race", InputWidth, _currentRace, out _currentRace ); + } + else if( drawRace == 2 ) + { + ImGui.SameLine(); + if( _currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar ) + { + _currentRace = ModelRace.Miqote; + } + + _dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar }, + RaceEnumExtensions.ToName ); + } + } + + private string NonExistentText() + => _lastTab switch + { + SwapType.Equipment => "One of the selected pieces of equipment does not seem to exist.", + SwapType.Accessory => "One of the selected accessories does not seem to exist.", + SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", + SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", + SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", + SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.", + SwapType.Weapon => "One of the selected weapons does not seem to exist.", + SwapType.Minion => "One of the selected minions does not seem to exist.", + SwapType.Mount => "One of the selected mounts does not seem to exist.", + _ => string.Empty, + }; + + + public void DrawItemSwapPanel() + { + using var tab = ImRaii.TabItem( "Item Swap (WIP)" ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale ); + ImGui.NewLine(); + + DrawSwapBar(); + + using var table = ImRaii.ListBox( "##swaps", -Vector2.One ); + if( _loadException != null ) + { + ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" ); + } + else if( _swapData.Loaded ) + { + foreach( var swap in _swapData.Swaps ) + { + DrawSwap( swap ); + } + } + else + { + ImGui.TextUnformatted( NonExistentText() ); + } + } + + private static void DrawSwap( Swap swap ) + { + var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; + using var tree = ImRaii.TreeNode( SwapToString( swap ), flags ); + if( !tree ) + { + return; + } + + foreach( var child in swap.ChildSwaps ) + { + DrawSwap( child ); + } + } + + private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, + ModCollection? newCollection, string _ ) + { + if( collectionType != CollectionType.Current || _mod == null || newCollection == null ) + { + return; + } + + UpdateMod( _mod, newCollection.Settings[ _mod.Index ] ); + newCollection.ModSettingChanged += OnSettingChange; + if( oldCollection != null ) + { + oldCollection.ModSettingChanged -= OnSettingChange; + } + } + + private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) + { + if( modIdx == _mod?.Index ) + { + _swapData.LoadMod( _mod, _modSettings ); + _dirty = true; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 22cb1398..343ef0ce 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -51,11 +51,6 @@ public partial class ModEditWindow public void Draw() { _list = _getFiles(); - if( _list.Count == 0 ) - { - return; - } - using var tab = ImRaii.TabItem( _tabName ); if( !tab ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index ab63ac79..3cc27aac 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -147,6 +147,17 @@ public partial class ModEditWindow return false; } + using( var textures = ImRaii.TreeNode( "Textures", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( textures ) + { + foreach( var tex in file.Textures ) + { + ImRaii.TreeNode( $"{tex.Path} - {tex.Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) { if( sets ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index d1a4159f..9707fb6f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -143,7 +143,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); - if( EqpEquipSlotCombo( "##eqpSlot", 100, _new.Slot, out var slot ) ) + if( Combos.EqpEquipSlot( "##eqpSlot", 100, _new.Slot, out var slot ) ) { _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId ); } @@ -241,7 +241,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); - if( RaceCombo( "##eqdpRace", _new.Race, out var race ) ) + if( Combos.Race( "##eqdpRace", _new.Race, out var race ) ) { var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId ); _new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId ); @@ -250,7 +250,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); - if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) ) + if( Combos.Gender( "##eqdpGender", _new.Gender, out var gender ) ) { var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ); _new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId ); @@ -259,7 +259,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); - if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) ) + if( Combos.EqdpEquipSlot( "##eqdpSlot", _new.Slot, out var slot ) ) { var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId ); _new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId ); @@ -356,7 +356,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) ) + if( Combos.ImcType( "##imcType", _new.ObjectType, out var type ) ) { var equipSlot = type switch { @@ -386,7 +386,7 @@ public partial class ModEditWindow // Equipment and accessories are slightly different imcs than other types. if( _new.ObjectType is ObjectType.Equipment ) { - if( EqpEquipSlotCombo( "##imcSlot", 100, _new.EquipSlot, out var slot ) ) + if( Combos.EqpEquipSlot( "##imcSlot", 100, _new.EquipSlot, out var slot ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -396,7 +396,7 @@ public partial class ModEditWindow } else if( _new.ObjectType is ObjectType.Accessory ) { - if( AccessorySlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) + if( Combos.AccessorySlot( "##imcSlot", _new.EquipSlot, out var slot ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -425,7 +425,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( _new.ObjectType is ObjectType.DemiHuman ) { - if( EqpEquipSlotCombo( "##imcSlot", 70, _new.EquipSlot, out var slot ) ) + if( Combos.EqpEquipSlot( "##imcSlot", 70, _new.EquipSlot, out var slot ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -599,7 +599,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); - if( RaceCombo( "##estRace", _new.Race, out var race ) ) + if( Combos.Race( "##estRace", _new.Race, out var race ) ) { var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId ); _new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry ); @@ -608,7 +608,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); - if( GenderCombo( "##estGender", _new.Gender, out var gender ) ) + if( Combos.Gender( "##estGender", _new.Gender, out var gender ) ) { var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId ); _new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry ); @@ -617,7 +617,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); - if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) ) + if( Combos.EstSlot( "##estSlot", _new.Slot, out var slot ) ) { var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); _new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry ); @@ -805,7 +805,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if( SubRaceCombo( "##rspSubRace", _new.SubRace, out var subRace ) ) + if( Combos.SubRace( "##rspSubRace", _new.SubRace, out var subRace ) ) { _new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) ); } @@ -813,7 +813,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( RacialTribeTooltip ); ImGui.TableNextColumn(); - if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) ) + if( Combos.RspAttribute( "##rspAttribute", _new.Attribute, out var attribute ) ) { _new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) ); } @@ -858,36 +858,6 @@ public partial class ModEditWindow } } - // Different combos to use with enums. - private static bool RaceCombo( string label, ModelRace current, out ModelRace race ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); - - private static bool GenderCombo( string label, Gender current, out Gender gender ) - => ImGuiUtil.GenericEnumCombo( label, 120 * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); - - private static bool EqdpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); - - private static bool EqpEquipSlotCombo( string label, float width, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); - - private static bool AccessorySlotCombo( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); - - private static bool SubRaceCombo( string label, SubRace current, out SubRace subRace ) - => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); - - private static bool RspAttributeCombo( string label, RspAttribute current, out RspAttribute attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, - RspAttributeExtensions.ToFullString, 0, 1 ); - - private static bool EstSlotCombo( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); - - private static bool ImcTypeCombo( string label, ObjectType current, out ObjectType type ) - => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, - ObjectTypeExtensions.ToName ); - // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border ) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index f527c555..886a47d2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -20,11 +20,13 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - private Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; - private bool _allowReduplicate = false; + private const string WindowBaseLabel = "###SubModEdit"; + internal readonly ItemSwapWindow _swapWindow = new(); + + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; + private bool _allowReduplicate = false; public void ChangeMod( Mod mod ) { @@ -45,6 +47,7 @@ public partial class ModEditWindow : Window, IDisposable _selectedFiles.Clear(); _modelTab.Reset(); _materialTab.Reset(); + _swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings ); } public void ChangeOption( ISubMod? subMod ) @@ -148,6 +151,7 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Draw(); _materialTab.Draw(); DrawTextureTab(); + _swapWindow.DrawItemSwapPanel(); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. @@ -544,5 +548,6 @@ public partial class ModEditWindow : Window, IDisposable _left.Dispose(); _right.Dispose(); _center.Dispose(); + _swapWindow.Dispose(); } } \ No newline at end of file From cc55ebb28fe32183d675d60ace27e81f69b7a910 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Dec 2022 21:11:38 +0100 Subject: [PATCH 0651/2451] Add a suffix of a stable hash of the original filename to texture paths. --- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 1 + Penumbra/Mods/ItemSwap/ItemSwap.cs | 22 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index 46b85f00..e442ae8e 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -144,6 +144,7 @@ public static class CustomizationSwap var newPath = ReplaceAnyRace( path, race ); newPath = ReplaceAnyBody( newPath, slot, idFrom ); + newPath = AddSuffix( newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}", true ); if( newPath != path ) { texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 2e6585c3..3771cd6d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -109,4 +109,26 @@ public static class ItemSwap file = null; return false; } + + public static int GetStableHashCode( this string str ) + { + unchecked + { + var hash1 = 5381; + var hash2 = hash1; + + for( var i = 0; i < str.Length && str[ i ] != '\0'; i += 2 ) + { + hash1 = ( ( hash1 << 5 ) + hash1 ) ^ str[ i ]; + if( i == str.Length - 1 || str[ i + 1 ] == '\0' ) + { + break; + } + + hash2 = ( ( hash2 << 5 ) + hash2 ) ^ str[ i + 1 ]; + } + + return hash1 + hash2 * 1566083941; + } + } } \ No newline at end of file From 3eb35c479e47262f745126f613b735bd1102bbfd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Dec 2022 21:11:54 +0100 Subject: [PATCH 0652/2451] Fix dirty flag in Item Swap Window. --- Penumbra/UI/Classes/ItemSwapWindow.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 76967dfe..a1329a6d 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -122,6 +122,8 @@ public class ItemSwapWindow : IDisposable _loadException = e; } } + + _dirty = false; } private static string SwapToString( Swap swap ) @@ -215,7 +217,7 @@ public class ItemSwapWindow : IDisposable using var tab = ImRaii.TabItem( newTab.ToString() ); if( tab ) { - _dirty = _lastTab != newTab; + _dirty |= _lastTab != newTab; _lastTab = newTab; } From 6cd43aa304630ac0974abc81625d5800ffd89f06 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 01:18:58 +0100 Subject: [PATCH 0653/2451] Add AVFX parsing. --- Penumbra.GameData/Files/AvfxFile.cs | 282 +++++++++++++++++++++++++++ Penumbra.GameData/Files/AvfxMagic.cs | 137 +++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 Penumbra.GameData/Files/AvfxFile.cs create mode 100644 Penumbra.GameData/Files/AvfxMagic.cs diff --git a/Penumbra.GameData/Files/AvfxFile.cs b/Penumbra.GameData/Files/AvfxFile.cs new file mode 100644 index 00000000..3077d960 --- /dev/null +++ b/Penumbra.GameData/Files/AvfxFile.cs @@ -0,0 +1,282 @@ +using System; +using System.IO; +using System.Numerics; +using System.Text; + +namespace Penumbra.GameData.Files; + +public class AvfxFile : IWritable +{ + public struct Block + { + public uint Name; + public uint Size; + public byte[] Data; + + public Block(BinaryReader r) + { + Name = r.ReadUInt32(); + Size = r.ReadUInt32(); + Data = r.ReadBytes((int)Size.RoundTo4()); + } + + public bool ToBool() + => BitConverter.ToBoolean(Data); + + public uint ToUint() + => BitConverter.ToUInt32(Data); + + public float ToFloat() + => BitConverter.ToSingle(Data); + + public new string ToString() + { + var span = Data.AsSpan(0, (int)Size - 1); + return Encoding.UTF8.GetString(span); + } + } + + + public Vector3 ClipBox; + public Vector3 ClipBoxSize; + public Vector3 RevisedValuesPos; + public Vector3 RevisedValuesRot; + public Vector3 RevisedValuesScale; + public Vector3 RevisedValuesColor; + + public uint Version; + public uint DrawLayerType; + public uint DrawOrderType; + public uint DirectionalLightSourceType; + public uint PointLightsType1; + public uint PointLightsType2; + + public float BiasZmaxScale; + public float BiasZmaxDistance; + public float NearClipBegin; + public float NearClipEnd; + public float FadeInnerX; + public float FadeOuterX; + public float FadeInnerY; + public float FadeOuterY; + public float FadeInnerZ; + public float FadeOuterZ; + public float FarClipBegin; + public float FarClipEnd; + public float SoftParticleFadeRange; + public float SoftKeyOffset; + public float GlobalFogInfluence; + + public bool IsDelayFastParticle; + public bool IsFitGround; + public bool IsTransformSkip; + public bool IsAllStopOnHide; + public bool CanBeClippedOut; + public bool ClipBoxEnabled; + public bool IsCameraSpace; + public bool IsFullEnvLight; + public bool IsClipOwnSetting; + public bool FadeEnabledX; + public bool FadeEnabledY; + public bool FadeEnabledZ; + public bool GlobalFogEnabled; + public bool LtsEnabled; + + public Block[] Schedulers = Array.Empty(); + public Block[] Timelines = Array.Empty(); + public Block[] Emitters = Array.Empty(); + public Block[] Particles = Array.Empty(); + public Block[] Effectors = Array.Empty(); + public Block[] Binders = Array.Empty(); + public string[] Textures = Array.Empty(); + public Block[] Models = Array.Empty(); + + public bool Valid { get; } = true; + + public AvfxFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var r = new BinaryReader(stream); + + var name = r.ReadUInt32(); + var size = r.ReadUInt32(); + var schedulerCount = 0; + var timelineCount = 0; + var emitterCount = 0; + var particleCount = 0; + var effectorCount = 0; + var binderCount = 0; + var textureCount = 0; + var modelCount = 0; + while (r.BaseStream.Position < size) + { + var block = new Block(r); + switch (block.Name) + { + // @formatter:off + case AvfxMagic.Version: Version = block.ToUint(); break; + case AvfxMagic.IsDelayFastParticle: IsDelayFastParticle = block.ToBool(); break; + case AvfxMagic.IsFitGround: IsFitGround = block.ToBool(); break; + case AvfxMagic.IsTransformSkip: IsTransformSkip = block.ToBool(); break; + case AvfxMagic.IsAllStopOnHide: IsAllStopOnHide = block.ToBool(); break; + case AvfxMagic.CanBeClippedOut: CanBeClippedOut = block.ToBool(); break; + case AvfxMagic.ClipBoxEnabled: ClipBoxEnabled = block.ToBool(); break; + case AvfxMagic.ClipBoxX: ClipBox.X = block.ToFloat(); break; + case AvfxMagic.ClipBoxY: ClipBox.Y = block.ToFloat(); break; + case AvfxMagic.ClipBoxZ: ClipBox.Z = block.ToFloat(); break; + case AvfxMagic.ClipBoxSizeX: ClipBoxSize.X = block.ToFloat(); break; + case AvfxMagic.ClipBoxSizeY: ClipBoxSize.Y = block.ToFloat(); break; + case AvfxMagic.ClipBoxSizeZ: ClipBoxSize.Z = block.ToFloat(); break; + case AvfxMagic.BiasZmaxScale: BiasZmaxScale = block.ToFloat(); break; + case AvfxMagic.BiasZmaxDistance: BiasZmaxDistance = block.ToFloat(); break; + case AvfxMagic.IsCameraSpace: IsCameraSpace = block.ToBool(); break; + case AvfxMagic.IsFullEnvLight: IsFullEnvLight = block.ToBool(); break; + case AvfxMagic.IsClipOwnSetting: IsClipOwnSetting = block.ToBool(); break; + case AvfxMagic.NearClipBegin: NearClipBegin = block.ToFloat(); break; + case AvfxMagic.NearClipEnd: NearClipEnd = block.ToFloat(); break; + case AvfxMagic.FarClipBegin: FarClipBegin = block.ToFloat(); break; + case AvfxMagic.FarClipEnd: FarClipEnd = block.ToFloat(); break; + case AvfxMagic.SoftParticleFadeRange: SoftParticleFadeRange = block.ToFloat(); break; + case AvfxMagic.SoftKeyOffset: SoftKeyOffset = block.ToFloat(); break; + case AvfxMagic.DrawLayerType: DrawLayerType = block.ToUint(); break; + case AvfxMagic.DrawOrderType: DrawOrderType = block.ToUint(); break; + case AvfxMagic.DirectionalLightSourceType: DirectionalLightSourceType = block.ToUint(); break; + case AvfxMagic.PointLightsType1: PointLightsType1 = block.ToUint(); break; + case AvfxMagic.PointLightsType2: PointLightsType2 = block.ToUint(); break; + case AvfxMagic.RevisedValuesPosX: RevisedValuesPos.X = block.ToFloat(); break; + case AvfxMagic.RevisedValuesPosY: RevisedValuesPos.Y = block.ToFloat(); break; + case AvfxMagic.RevisedValuesPosZ: RevisedValuesPos.Z = block.ToFloat(); break; + case AvfxMagic.RevisedValuesRotX: RevisedValuesRot.X = block.ToFloat(); break; + case AvfxMagic.RevisedValuesRotY: RevisedValuesRot.Y = block.ToFloat(); break; + case AvfxMagic.RevisedValuesRotZ: RevisedValuesRot.Z = block.ToFloat(); break; + case AvfxMagic.RevisedValuesScaleX: RevisedValuesScale.X = block.ToFloat(); break; + case AvfxMagic.RevisedValuesScaleY: RevisedValuesScale.Y = block.ToFloat(); break; + case AvfxMagic.RevisedValuesScaleZ: RevisedValuesScale.Z = block.ToFloat(); break; + case AvfxMagic.RevisedValuesColorR: RevisedValuesColor.X = block.ToFloat(); break; + case AvfxMagic.RevisedValuesColorG: RevisedValuesColor.Y = block.ToFloat(); break; + case AvfxMagic.RevisedValuesColorB: RevisedValuesColor.Z = block.ToFloat(); break; + case AvfxMagic.FadeEnabledX: FadeEnabledX = block.ToBool(); break; + case AvfxMagic.FadeInnerX: FadeInnerX = block.ToFloat(); break; + case AvfxMagic.FadeOuterX: FadeOuterX = block.ToFloat(); break; + case AvfxMagic.FadeEnabledY: FadeEnabledY = block.ToBool(); break; + case AvfxMagic.FadeInnerY: FadeInnerY = block.ToFloat(); break; + case AvfxMagic.FadeOuterY: FadeOuterY = block.ToFloat(); break; + case AvfxMagic.FadeEnabledZ: FadeEnabledZ = block.ToBool(); break; + case AvfxMagic.FadeInnerZ: FadeInnerZ = block.ToFloat(); break; + case AvfxMagic.FadeOuterZ: FadeOuterZ = block.ToFloat(); break; + case AvfxMagic.GlobalFogEnabled: GlobalFogEnabled = block.ToBool(); break; + case AvfxMagic.GlobalFogInfluence: GlobalFogInfluence = block.ToFloat(); break; + case AvfxMagic.LtsEnabled: LtsEnabled = block.ToBool(); break; + case AvfxMagic.NumSchedulers: Schedulers = new Block[block.ToUint()]; break; + case AvfxMagic.NumTimelines: Timelines = new Block[block.ToUint()]; break; + case AvfxMagic.NumEmitters: Emitters = new Block[block.ToUint()]; break; + case AvfxMagic.NumParticles: Particles = new Block[block.ToUint()]; break; + case AvfxMagic.NumEffectors: Effectors = new Block[block.ToUint()]; break; + case AvfxMagic.NumBinders: Binders = new Block[block.ToUint()]; break; + case AvfxMagic.NumTextures: Textures = new string[block.ToUint()]; break; + case AvfxMagic.NumModels: Models = new Block[block.ToUint()]; break; + case AvfxMagic.Scheduler: Schedulers[schedulerCount++] = block; break; + case AvfxMagic.Timeline: Timelines[timelineCount++] = block; break; + case AvfxMagic.Emitter: Emitters[emitterCount++] = block; break; + case AvfxMagic.Particle: Particles[particleCount++] = block; break; + case AvfxMagic.Effector: Effectors[effectorCount++] = block; break; + case AvfxMagic.Binder: Binders[binderCount++] = block; break; + case AvfxMagic.Texture: Textures[textureCount++] = block.ToString(); break; + case AvfxMagic.Model: Models[modelCount++] = block; break; + // @formatter:on + } + } + } + + + public byte[] Write() + { + using var m = new MemoryStream(512 * 1024); + using var w = new BinaryWriter(m); + + w.Write(AvfxMagic.AvfxBase); + var sizePos = w.BaseStream.Position; + w.Write(0u); + w.WriteBlock(AvfxMagic.Version, Version) + .WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle) + .WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle) + .WriteBlock(AvfxMagic.IsFitGround, IsFitGround) + .WriteBlock(AvfxMagic.IsTransformSkip, IsTransformSkip) + .WriteBlock(AvfxMagic.IsAllStopOnHide, IsAllStopOnHide) + .WriteBlock(AvfxMagic.CanBeClippedOut, CanBeClippedOut) + .WriteBlock(AvfxMagic.ClipBoxEnabled, ClipBoxEnabled) + .WriteBlock(AvfxMagic.ClipBoxX, ClipBox.X) + .WriteBlock(AvfxMagic.ClipBoxY, ClipBox.Y) + .WriteBlock(AvfxMagic.ClipBoxZ, ClipBox.Z) + .WriteBlock(AvfxMagic.ClipBoxSizeX, ClipBoxSize.X) + .WriteBlock(AvfxMagic.ClipBoxSizeY, ClipBoxSize.Y) + .WriteBlock(AvfxMagic.ClipBoxSizeZ, ClipBoxSize.Z) + .WriteBlock(AvfxMagic.BiasZmaxScale, BiasZmaxScale) + .WriteBlock(AvfxMagic.BiasZmaxDistance, BiasZmaxDistance) + .WriteBlock(AvfxMagic.IsCameraSpace, IsCameraSpace) + .WriteBlock(AvfxMagic.IsFullEnvLight, IsFullEnvLight) + .WriteBlock(AvfxMagic.IsClipOwnSetting, IsClipOwnSetting) + .WriteBlock(AvfxMagic.NearClipBegin, NearClipBegin) + .WriteBlock(AvfxMagic.NearClipEnd, NearClipEnd) + .WriteBlock(AvfxMagic.FarClipBegin, FarClipBegin) + .WriteBlock(AvfxMagic.FarClipEnd, FarClipEnd) + .WriteBlock(AvfxMagic.SoftParticleFadeRange, SoftParticleFadeRange) + .WriteBlock(AvfxMagic.SoftKeyOffset, SoftKeyOffset) + .WriteBlock(AvfxMagic.DrawLayerType, DrawLayerType) + .WriteBlock(AvfxMagic.DrawOrderType, DrawOrderType) + .WriteBlock(AvfxMagic.DirectionalLightSourceType, DirectionalLightSourceType) + .WriteBlock(AvfxMagic.PointLightsType1, PointLightsType1) + .WriteBlock(AvfxMagic.PointLightsType2, PointLightsType2) + .WriteBlock(AvfxMagic.RevisedValuesPosX, RevisedValuesPos.X) + .WriteBlock(AvfxMagic.RevisedValuesPosY, RevisedValuesPos.Y) + .WriteBlock(AvfxMagic.RevisedValuesPosZ, RevisedValuesPos.Z) + .WriteBlock(AvfxMagic.RevisedValuesRotX, RevisedValuesRot.X) + .WriteBlock(AvfxMagic.RevisedValuesRotY, RevisedValuesRot.Y) + .WriteBlock(AvfxMagic.RevisedValuesRotZ, RevisedValuesRot.Z) + .WriteBlock(AvfxMagic.RevisedValuesScaleX, RevisedValuesScale.X) + .WriteBlock(AvfxMagic.RevisedValuesScaleY, RevisedValuesScale.Y) + .WriteBlock(AvfxMagic.RevisedValuesScaleZ, RevisedValuesScale.Z) + .WriteBlock(AvfxMagic.RevisedValuesColorR, RevisedValuesColor.X) + .WriteBlock(AvfxMagic.RevisedValuesColorG, RevisedValuesColor.Y) + .WriteBlock(AvfxMagic.RevisedValuesColorB, RevisedValuesColor.Z) + .WriteBlock(AvfxMagic.FadeEnabledX, FadeEnabledX) + .WriteBlock(AvfxMagic.FadeInnerX, FadeInnerX) + .WriteBlock(AvfxMagic.FadeOuterX, FadeOuterX) + .WriteBlock(AvfxMagic.FadeEnabledY, FadeEnabledY) + .WriteBlock(AvfxMagic.FadeInnerY, FadeInnerY) + .WriteBlock(AvfxMagic.FadeOuterY, FadeOuterY) + .WriteBlock(AvfxMagic.FadeEnabledZ, FadeEnabledZ) + .WriteBlock(AvfxMagic.FadeInnerZ, FadeInnerZ) + .WriteBlock(AvfxMagic.FadeOuterZ, FadeOuterZ) + .WriteBlock(AvfxMagic.GlobalFogEnabled, GlobalFogEnabled) + .WriteBlock(AvfxMagic.GlobalFogInfluence, GlobalFogInfluence) + .WriteBlock(AvfxMagic.LtsEnabled, LtsEnabled) + .WriteBlock(AvfxMagic.NumSchedulers, (uint)Schedulers.Length) + .WriteBlock(AvfxMagic.NumTimelines, (uint)Timelines.Length) + .WriteBlock(AvfxMagic.NumEmitters, (uint)Emitters.Length) + .WriteBlock(AvfxMagic.NumParticles, (uint)Particles.Length) + .WriteBlock(AvfxMagic.NumEffectors, (uint)Effectors.Length) + .WriteBlock(AvfxMagic.NumBinders, (uint)Binders.Length) + .WriteBlock(AvfxMagic.NumTextures, (uint)Textures.Length) + .WriteBlock(AvfxMagic.NumModels, (uint)Models.Length); + foreach (var block in Schedulers) + w.WriteBlock(block); + foreach (var block in Timelines) + w.WriteBlock(block); + foreach (var block in Emitters) + w.WriteBlock(block); + foreach (var block in Particles) + w.WriteBlock(block); + foreach (var block in Effectors) + w.WriteBlock(block); + foreach (var block in Binders) + w.WriteBlock(block); + foreach (var texture in Textures) + w.WriteTextureBlock(texture); + foreach (var block in Models) + w.WriteBlock(block); + w.Seek((int)sizePos, SeekOrigin.Begin); + w.Write((uint)w.BaseStream.Length); + return m.ToArray(); + } +} diff --git a/Penumbra.GameData/Files/AvfxMagic.cs b/Penumbra.GameData/Files/AvfxMagic.cs new file mode 100644 index 00000000..1c315711 --- /dev/null +++ b/Penumbra.GameData/Files/AvfxMagic.cs @@ -0,0 +1,137 @@ +using System.IO; +using System.Numerics; +using System.Text; + +// ReSharper disable ShiftExpressionZeroLeftOperand + +namespace Penumbra.GameData.Files; + +public static class AvfxMagic +{ + public const uint AvfxBase = ('A' << 24) | ('V' << 16) | ('F' << 8) | (uint)'X'; + public const uint Version = (000 << 24) | ('V' << 16) | ('e' << 8) | (uint)'r'; + public const uint IsDelayFastParticle = ('b' << 24) | ('D' << 16) | ('F' << 8) | (uint)'P'; + public const uint IsFitGround = (000 << 24) | ('b' << 16) | ('F' << 8) | (uint)'G'; + public const uint IsTransformSkip = (000 << 24) | ('b' << 16) | ('T' << 8) | (uint)'S'; + public const uint IsAllStopOnHide = ('b' << 24) | ('A' << 16) | ('S' << 8) | (uint)'H'; + public const uint CanBeClippedOut = ('b' << 24) | ('C' << 16) | ('B' << 8) | (uint)'C'; + public const uint ClipBoxEnabled = ('b' << 24) | ('C' << 16) | ('u' << 8) | (uint)'l'; + public const uint ClipBoxX = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'x'; + public const uint ClipBoxY = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'y'; + public const uint ClipBoxZ = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'z'; + public const uint ClipBoxSizeX = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'x'; + public const uint ClipBoxSizeY = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'y'; + public const uint ClipBoxSizeZ = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'z'; + public const uint BiasZmaxScale = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'s'; + public const uint BiasZmaxDistance = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'d'; + public const uint IsCameraSpace = ('b' << 24) | ('C' << 16) | ('m' << 8) | (uint)'S'; + public const uint IsFullEnvLight = ('b' << 24) | ('F' << 16) | ('E' << 8) | (uint)'L'; + public const uint IsClipOwnSetting = ('b' << 24) | ('O' << 16) | ('S' << 8) | (uint)'t'; + public const uint NearClipBegin = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'B'; + public const uint NearClipEnd = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'E'; + public const uint FarClipBegin = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'B'; + public const uint FarClipEnd = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'E'; + public const uint SoftParticleFadeRange = ('S' << 24) | ('P' << 16) | ('F' << 8) | (uint)'R'; + public const uint SoftKeyOffset = (000 << 24) | ('S' << 16) | ('K' << 8) | (uint)'O'; + public const uint DrawLayerType = ('D' << 24) | ('w' << 16) | ('L' << 8) | (uint)'y'; + public const uint DrawOrderType = ('D' << 24) | ('w' << 16) | ('O' << 8) | (uint)'T'; + public const uint DirectionalLightSourceType = ('D' << 24) | ('L' << 16) | ('S' << 8) | (uint)'T'; + public const uint PointLightsType1 = ('P' << 24) | ('L' << 16) | ('1' << 8) | (uint)'S'; + public const uint PointLightsType2 = ('P' << 24) | ('L' << 16) | ('2' << 8) | (uint)'S'; + public const uint RevisedValuesPosX = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'x'; + public const uint RevisedValuesPosY = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'y'; + public const uint RevisedValuesPosZ = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'z'; + public const uint RevisedValuesRotX = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'x'; + public const uint RevisedValuesRotY = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'y'; + public const uint RevisedValuesRotZ = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'z'; + public const uint RevisedValuesScaleX = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'x'; + public const uint RevisedValuesScaleY = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'y'; + public const uint RevisedValuesScaleZ = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'z'; + public const uint RevisedValuesColorR = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'R'; + public const uint RevisedValuesColorG = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'G'; + public const uint RevisedValuesColorB = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'B'; + public const uint FadeEnabledX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'e'; + public const uint FadeInnerX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'i'; + public const uint FadeOuterX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'o'; + public const uint FadeEnabledY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'e'; + public const uint FadeInnerY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'i'; + public const uint FadeOuterY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'o'; + public const uint FadeEnabledZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'e'; + public const uint FadeInnerZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'i'; + public const uint FadeOuterZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'o'; + public const uint GlobalFogEnabled = ('b' << 24) | ('G' << 16) | ('F' << 8) | (uint)'E'; + public const uint GlobalFogInfluence = ('G' << 24) | ('F' << 16) | ('I' << 8) | (uint)'M'; + public const uint LtsEnabled = ('b' << 24) | ('L' << 16) | ('T' << 8) | (uint)'S'; + public const uint NumSchedulers = ('S' << 24) | ('c' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumTimelines = ('T' << 24) | ('l' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumEmitters = ('E' << 24) | ('m' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumParticles = ('P' << 24) | ('r' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumEffectors = ('E' << 24) | ('f' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumBinders = ('B' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumTextures = ('T' << 24) | ('x' << 16) | ('C' << 8) | (uint)'n'; + public const uint NumModels = ('M' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n'; + public const uint Scheduler = ('S' << 24) | ('c' << 16) | ('h' << 8) | (uint)'d'; + public const uint Timeline = ('T' << 24) | ('m' << 16) | ('L' << 8) | (uint)'n'; + public const uint Emitter = ('E' << 24) | ('m' << 16) | ('i' << 8) | (uint)'t'; + public const uint Particle = ('P' << 24) | ('t' << 16) | ('c' << 8) | (uint)'l'; + public const uint Effector = ('E' << 24) | ('f' << 16) | ('c' << 8) | (uint)'t'; + public const uint Binder = ('B' << 24) | ('i' << 16) | ('n' << 8) | (uint)'d'; + public const uint Texture = (000 << 24) | ('T' << 16) | ('e' << 8) | (uint)'x'; + public const uint Model = ('M' << 24) | ('o' << 16) | ('d' << 8) | (uint)'l'; + + internal static uint RoundTo4(this uint size) + { + var rest = size & 0b11u; + return rest > 0 ? (size & ~0b11u) + 4u : size; + } + + internal static BinaryWriter WriteTextureBlock(this BinaryWriter bw, string texture) + { + bw.Write(Texture); + var bytes = Encoding.UTF8.GetBytes(texture); + var size = (uint)bytes.Length + 1u; + bw.Write(size); + bw.Write(bytes); + bw.Write((byte)0); + for (var end = size.RoundTo4(); size < end; ++size) + bw.Write((byte)0); + return bw; + } + + internal static BinaryWriter WriteBlock(this BinaryWriter bw, AvfxFile.Block block) + { + bw.Write(block.Name); + bw.Write(block.Size); + bw.Write(block.Data); + return bw; + } + + internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, uint value) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value); + return bw; + } + + internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, bool value) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value ? 1u : 0u); + return bw; + } + + internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, float value) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value); + return bw; + } + + internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magicX, uint magicY, uint magicZ, Vector3 value) + => bw.WriteBlock(magicX, value.X) + .WriteBlock(magicY, value.Y) + .WriteBlock(magicZ, value.Z); +} From a01f73cde4fbb4d8d6f04824fe3a2ea67ab1c972 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 12:19:03 +0100 Subject: [PATCH 0654/2451] Add equipment swapping. --- Penumbra.GameData/Data/GamePaths.cs | 45 +- .../Meta/Manipulations/EstManipulation.cs | 10 + Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 95 +--- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 406 ++++++++++++++++++ Penumbra/Mods/ItemSwap/ItemSwap.cs | 104 +++++ Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 26 +- Penumbra/Mods/ItemSwap/Swaps.cs | 7 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 195 +++++---- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.cs | 4 +- 10 files changed, 710 insertions(+), 184 deletions(-) create mode 100644 Penumbra/Mods/ItemSwap/EquipmentSwap.cs diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index 89f2f58e..3d70c13d 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -61,6 +61,24 @@ public static partial class GamePaths public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; } + + public static partial class Sklb + { + public static string Path(SetId monsterId) + => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.sklb"; + } + + public static partial class Skp + { + public static string Path(SetId monsterId) + => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.skp"; + } + + public static partial class Eid + { + public static string Path(SetId monsterId) + => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/eid_m{monsterId.Value:D4}b0001.eid"; + } } public static partial class Weapon @@ -243,6 +261,21 @@ public static partial class GamePaths } } + public static partial class Skeleton + { + public static partial class Phyb + { + public static string Path(GenderRace raceCode, string slot, SetId slotId) + => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.phyb"; + } + + public static partial class Sklb + { + public static string Path(GenderRace raceCode, string slot, SetId slotId) + => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.sklb"; + } + } + public static partial class Character { public static partial class Mdl @@ -254,18 +287,6 @@ public static partial class GamePaths => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; } - public static partial class Phyb - { - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId) - => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.phyb"; - } - - public static partial class Sklb - { - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId) - => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.sklb"; - } - public static partial class Mtrl { // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index b21148ef..3891cc6f 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,6 +19,16 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.Index.HeadEst, } + public static string ToName( EstType type ) + => type switch + { + EstType.Hair => "hair", + EstType.Face => "face", + EstType.Body => "top", + EstType.Head => "met", + _ => "unk", + }; + public ushort Entry { get; private init; } // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index e442ae8e..b9e9a94f 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -7,8 +7,6 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; namespace Penumbra.Mods.ItemSwap; @@ -54,33 +52,6 @@ public static class CustomizationSwap return true; } - public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true ) - => condition - ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" ) - : path; - - public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true ) - => ReplaceAnyId( path, 'c', ( ushort )to, condition ); - - public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true ) - => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition ); - - public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true ) - => condition - ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) - : path; - - public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) - => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); - - public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true ) - => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition ); - - public static string AddSuffix( string path, string ext, string suffix, bool condition = true ) - => condition - ? path.Replace( ext, suffix + ext ) - : path; - public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, ref string fileName, ref bool dataWasChanged, out FileSwap mtrl ) { @@ -89,10 +60,10 @@ public static class CustomizationSwap var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant ); var newFileName = fileName; - newFileName = ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); - newFileName = ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); - newFileName = AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race ); - newFileName = AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value ); + newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); + newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); + newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race ); + newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value ); var actualMtrlFromPath = mtrlFromPath; if( newFileName != fileName ) @@ -142,9 +113,9 @@ public static class CustomizationSwap } } - var newPath = ReplaceAnyRace( path, race ); - newPath = ReplaceAnyBody( newPath, slot, idFrom ); - newPath = AddSuffix( newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}", true ); + var newPath = ItemSwap.ReplaceAnyRace( path, race ); + newPath = ItemSwap.ReplaceAnyBody( newPath, slot, idFrom ); + newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true ); if( newPath != path ) { texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; @@ -160,56 +131,4 @@ public static class CustomizationSwap var path = $"shader/sm5/shpk/{shaderName}"; return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk ); } - - /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > metaChanges, BodySlot slot, GenderRace gr, SetId idFrom, - SetId idTo, out MetaSwap? est ) - { - var (gender, race) = gr.Split(); - var estSlot = slot switch - { - BodySlot.Hair => EstManipulation.EstType.Hair, - BodySlot.Body => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, - }; - if( estSlot == 0 ) - { - est = null; - return true; - } - - var fromDefault = new EstManipulation( gender, race, estSlot, idFrom.Value, EstFile.GetDefault( estSlot, gr, idFrom.Value ) ); - var toDefault = new EstManipulation( gender, race, estSlot, idTo.Value, EstFile.GetDefault( estSlot, gr, idTo.Value ) ); - est = new MetaSwap( metaChanges, fromDefault, toDefault ); - - if( est.SwapApplied.Est.Entry >= 2 ) - { - if( !CreatePhyb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var phyb ) ) - { - return false; - } - - if( !CreateSklb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var sklb ) ) - { - return false; - } - - est.ChildSwaps.Add( phyb ); - est.ChildSwaps.Add( sklb ); - } - - return true; - } - - public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap phyb ) - { - var phybPath = GamePaths.Character.Phyb.Path( race, slot, estEntry ); - return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb ); - } - - public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap sklb ) - { - var sklbPath = GamePaths.Character.Sklb.Path( race, slot, estEntry ); - return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb ); - } } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs new file mode 100644 index 00000000..f065d6be --- /dev/null +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.ItemSwap; + +public static class EquipmentSwap +{ + public static Item[] CreateItemSwap( List< Swap > swaps, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, Item itemFrom, + Item itemTo ) + { + // Check actual ids, variants and slots. We only support using the same slot. + LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom ); + LookupItem( itemTo, out var slotTo, out var idTo, out var variantTo ); + if( slotFrom != slotTo ) + { + throw new ItemSwap.InvalidItemTypeException(); + } + + if( !CreateEqp( manips, slotFrom, idFrom, idTo, out var eqp ) ) + { + throw new Exception( "Could not get Eqp Entry for Swap." ); + } + + if( eqp != null ) + { + swaps.Add( eqp ); + } + + if( !CreateGmp( manips, slotFrom, idFrom, idTo, out var gmp ) ) + { + throw new Exception( "Could not get Gmp Entry for Swap." ); + } + + if( gmp != null ) + { + swaps.Add( gmp ); + } + + + var (imcFileFrom, variants, affectedItems) = GetVariants( slotFrom, idFrom, idTo, variantFrom ); + var imcFileTo = new ImcFile( new ImcManipulation( slotFrom, variantTo, idTo.Value, default ) ); + + var isAccessory = slotFrom.IsAccessory(); + var estType = slotFrom switch + { + EquipSlot.Head => EstManipulation.EstType.Head, + EquipSlot.Body => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; + + var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotFrom ), variantTo ).MaterialId; + foreach( var gr in Enum.GetValues< GenderRace >() ) + { + if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 ) + { + continue; + } + + if( !ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, out var est ) ) + { + throw new Exception( "Could not get Est Entry for Swap." ); + } + + if( est != null ) + { + swaps.Add( est ); + } + + if( !CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo, out var eqdp ) ) + { + throw new Exception( "Could not get Eqdp Entry for Swap." ); + } + + if( eqdp != null ) + { + swaps.Add( eqdp ); + } + } + + foreach( var variant in variants ) + { + if( !CreateImc( redirections, manips, slotFrom, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo, out var imc ) ) + { + throw new Exception( "Could not get IMC Entry for Swap." ); + } + + swaps.Add( imc ); + } + + + return affectedItems; + } + + public static bool CreateEqdp( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo, out MetaSwap? meta ) + { + var (gender, race) = gr.Split(); + var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idFrom.Value ), slot, gender, race, idFrom.Value ); + var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idTo.Value ), slot, gender, race, idTo.Value ); + meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); + var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slot ); + if( ownMdl ) + { + if( !CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo, out var mdl ) ) + { + return false; + } + + meta.ChildSwaps.Add( mdl ); + } + else if( !ownMtrl && meta.SwapAppliedIsDefault ) + { + meta = null; + } + + return true; + } + + public static bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo, + out FileSwap mdl ) + { + var mdlPathFrom = GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); + var mdlPathTo = GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); + if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) ) + { + return false; + } + + foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) + { + if( !CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged, out var mtrl ) ) + { + return false; + } + + if( mtrl != null ) + { + mdl.ChildSwaps.Add( mtrl ); + } + } + + return true; + } + + private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot ) + { + var model = GenderRace.Unknown; + var material = GenderRace.Unknown; + var accessory = slot.IsAccessory(); + foreach( var gr in genderRace.Dependencies() ) + { + var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value ); + var (b1, b2) = entry.ToBits( slot ); + if( b1 && material == GenderRace.Unknown ) + { + material = gr; + if( model != GenderRace.Unknown ) + { + return ( model, material ); + } + } + + if( b2 && model == GenderRace.Unknown ) + { + model = gr; + if( material != GenderRace.Unknown ) + { + return ( model, material ); + } + } + } + + return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale ); + } + + private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) + { + slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); + if( !slot.IsEquipmentPiece() ) + { + throw new ItemSwap.InvalidItemTypeException(); + } + + modelId = ( ( Quad )i.ModelMain ).A; + variant = ( byte )( ( Quad )i.ModelMain ).B; + } + + private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom ) + { + var entry = new ImcManipulation( slot, variantFrom, idFrom.Value, default ); + var imc = new ImcFile( entry ); + Item[] items; + byte[] variants; + if( idFrom.Value == idTo.Value ) + { + items = Penumbra.Identifier.Identify( idFrom, variantFrom, slot ).ToArray(); + variants = new[] { variantFrom }; + } + else + { + items = Penumbra.Identifier.Identify( slot.IsEquipment() + ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) + : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); + variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); + } + + return ( imc, variants, items ); + } + + public static bool CreateGmp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? gmp ) + { + if( slot is not EquipSlot.Head ) + { + gmp = null; + return true; + } + + var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value ); + var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value ); + gmp = new MetaSwap( manips, manipFrom, manipTo ); + return true; + } + + public static bool CreateImc( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo, out MetaSwap imc ) + { + var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slot ), variantFrom ); + var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ); + var manipulationFrom = new ImcManipulation( slot, variantFrom, idFrom.Value, entryFrom ); + var manipulationTo = new ImcManipulation( slot, variantTo, idTo.Value, entryTo ); + imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); + + if( !AddDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId, imc ) ) + { + return false; + } + + if( !AddAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId, imc ) ) + { + return false; + } + + return true; + } + + public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc ) + { + if( decalId != 0 ) + { + var decalPath = GamePaths.Equipment.Decal.Path( decalId ); + if( !FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath, out var swap ) ) + { + return false; + } + + imc.ChildSwaps.Add( swap ); + } + + return true; + } + + public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc ) + { + if( vfxId != 0 ) + { + var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId ); + var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId ); + if( !FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo, out var swap ) ) + { + return false; + } + + foreach( ref var filePath in swap.AsAvfx()!.Textures.AsSpan() ) + { + if( !CreateAtex( redirections, ref filePath, ref swap.DataWasChanged, out var atex ) ) + { + return false; + } + + swap.ChildSwaps.Add( atex ); + } + + imc.ChildSwaps.Add( swap ); + } + + return true; + } + + public static bool CreateEqp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? eqp ) + { + if( slot.IsAccessory() ) + { + eqp = null; + return true; + } + + var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value ); + var eqpValueTo = ExpandedEqpFile.GetDefault( idTo.Value ); + var eqpFrom = new EqpManipulation( eqpValueFrom, slot, idFrom.Value ); + var eqpTo = new EqpManipulation( eqpValueTo, slot, idFrom.Value ); + eqp = new MetaSwap( manips, eqpFrom, eqpTo ); + return true; + } + + public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged, out FileSwap? mtrl ) + { + var prefix = slot.IsAccessory() ? 'a' : 'e'; + if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) + { + mtrl = null; + return true; + } + + var folderTo = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); + var pathTo = $"{folderTo}{fileName}"; + + var folderFrom = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); + var newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom ); + var pathFrom = $"{folderFrom}{newFileName}"; + + if( newFileName != fileName ) + { + fileName = newFileName; + dataWasChanged = true; + } + + if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo, out mtrl ) ) + { + return false; + } + + if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shader ) ) + { + return false; + } + + mtrl.ChildSwaps.Add( shader ); + + foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) + { + if( !CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged, out var swap ) ) + { + return false; + } + + mtrl.ChildSwaps.Add( swap ); + } + + return true; + } + + public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, + ref bool dataWasChanged, + out FileSwap tex ) + { + var path = texture.Path; + var addedDashes = false; + if( texture.DX11 ) + { + var fileName = Path.GetFileName( path ); + if( !fileName.StartsWith( "--" ) ) + { + path = path.Replace( fileName, $"--{fileName}" ); + addedDashes = true; + } + } + + var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); + newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true ); + if( newPath != path ) + { + texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; + dataWasChanged = true; + } + + return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path ); + } + + public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk ) + { + var path = $"shader/sm5/shpk/{shaderName}"; + return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk ); + } + + public static bool CreateAtex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged, out FileSwap atex ) + { + var oldPath = filePath; + filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}", true ); + dataWasChanged = true; + + return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, out atex, oldPath ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 3771cd6d..d32e2073 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,7 +1,14 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Text.RegularExpressions; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; namespace Penumbra.Mods.ItemSwap; @@ -110,6 +117,76 @@ public static class ItemSwap return false; } + public static bool LoadAvfx( FullPath path, [NotNullWhen( true )] out AvfxFile? file ) + { + try + { + if( LoadFile( path, out byte[] data ) ) + { + file = new AvfxFile( data ); + return true; + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse file {path} to Avfx:\n{e}" ); + } + + file = null; + return false; + } + + + public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap phyb ) + { + var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry ); + return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb ); + } + + public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap sklb ) + { + var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry ); + return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb ); + } + + /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. + public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EstManipulation.EstType type, + GenderRace genderRace, SetId idFrom, SetId idTo, out MetaSwap? est ) + { + if( type == 0 ) + { + est = null; + return true; + } + + var (gender, race) = genderRace.Split(); + var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) ); + var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) ); + est = new MetaSwap( manips, fromDefault, toDefault ); + + if( est.SwapApplied.Est.Entry >= 2 ) + { + if( !CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var phyb ) ) + { + return false; + } + + if( !CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var sklb ) ) + { + return false; + } + + est.ChildSwaps.Add( phyb ); + est.ChildSwaps.Add( sklb ); + } + else if( est.SwapAppliedIsDefault ) + { + est = null; + } + + return true; + } + public static int GetStableHashCode( this string str ) { unchecked @@ -131,4 +208,31 @@ public static class ItemSwap return hash1 + hash2 * 1566083941; } } + + public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true ) + => condition + ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" ) + : path; + + public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true ) + => ReplaceAnyId( path, 'c', ( ushort )to, condition ); + + public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true ) + => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition ); + + public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true ) + => condition + ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) + : path; + + public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) + => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); + + public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true ) + => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition ); + + public static string AddSuffix( string path, string ext, string suffix, bool condition = true ) + => condition + ? path.Replace( ext, suffix + ext ) + : path; } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 4fc07cd4..8027e7d1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -21,6 +22,7 @@ public class ItemSwapContainer => _modManipulations; public readonly List< Swap > Swaps = new(); + public bool Loaded { get; private set; } public void Clear() @@ -109,6 +111,22 @@ public class ItemSwapContainer LoadMod( null, null ); } + public Item[] LoadEquipment( Item from, Item to ) + { + try + { + Swaps.Clear(); + var ret = EquipmentSwap.CreateItemSwap( Swaps, ModRedirections, _modManipulations, from, to ); + Loaded = true; + return ret; + } + catch( Exception e ) + { + Swaps.Clear(); + Loaded = false; + return Array.Empty< Item >(); + } + } public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to ) { @@ -117,7 +135,13 @@ public class ItemSwapContainer return false; } - if( !CustomizationSwap.CreateEst( ModRedirections, _modManipulations, slot, race, from, to, out var est ) ) + var type = slot switch + { + BodySlot.Hair => EstManipulation.EstType.Hair, + BodySlot.Face => EstManipulation.EstType.Face, + _ => ( EstManipulation.EstType )0, + }; + if( !ItemSwap.CreateEst( ModRedirections, _modManipulations, type, race, from, to, out var est ) ) { return false; } diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index d425e476..5018f2a7 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,4 +1,3 @@ -using System; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -13,7 +12,7 @@ namespace Penumbra.Mods.ItemSwap; public class Swap { /// Any further swaps belonging specifically to this tree of changes. - public List< Swap > ChildSwaps = new(); + public readonly List< Swap > ChildSwaps = new(); public IEnumerable< Swap > WithChildren() => ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this ); @@ -111,6 +110,9 @@ public sealed class FileSwap : Swap public MtrlFile? AsMtrl() => FileData as MtrlFile; + public AvfxFile? AsAvfx() + => FileData as AvfxFile; + /// /// Create a full swap container for a specific file type using a modded redirection set, the actually requested path and the game file it should load instead after the swap. /// @@ -151,6 +153,7 @@ public sealed class FileSwap : Swap { ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, + ResourceType.Avfx => ItemSwap.LoadAvfx( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, _ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, }; diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index a1329a6d..2d23abc4 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -5,6 +5,7 @@ using Dalamud.Interface; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; @@ -18,17 +19,43 @@ namespace Penumbra.UI.Classes; public class ItemSwapWindow : IDisposable { - private class ItemSelector : FilterComboCache< Item > + private class EquipSelector : FilterComboCache< Item > { - public ItemSelector() + public EquipSelector() : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipmentPiece() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) + => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipment() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) { } protected override string ToString( Item obj ) => obj.Name.ToString(); } + private class AccessorySelector : FilterComboCache< Item > + { + public AccessorySelector() + : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i + => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsAccessory() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) + { } + + protected override string ToString( Item obj ) + => obj.Name.ToString(); + } + + private class SlotSelector : FilterComboCache< Item > + { + public readonly EquipSlot CurrentSlot; + + public SlotSelector( EquipSlot slot ) + : base( () => Dalamud.GameData.GetExcelSheet< Item >()!.Where( i + => ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot() == slot && i.ModelMain != 0 && i.Name.RawData.Length > 0 ).ToList() ) + { + CurrentSlot = slot; + } + + protected override string ToString( Item obj ) + => obj.Name.ToString(); + } + public ItemSwapWindow() { Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; @@ -41,8 +68,10 @@ public class ItemSwapWindow : IDisposable Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; } - private readonly ItemSelector _itemSelector = new(); - private readonly ItemSwapContainer _swapData = new(); + private readonly EquipSelector _equipSelector = new(); + private readonly AccessorySelector _accessorySelector = new(); + private SlotSelector? _slotSelector; + private readonly ItemSwapContainer _swapData = new(); private Mod? _mod; private ModSettings? _modSettings; @@ -53,13 +82,14 @@ public class ItemSwapWindow : IDisposable private ModelRace _currentRace = ModelRace.Midlander; private int _targetId = 0; private int _sourceId = 0; - private int _currentVariant = 1; private Exception? _loadException = null; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; private string _newOptionName = string.Empty; - private bool _useFileSwaps = false; + private bool _useFileSwaps = true; + + private Item[]? _affectedItems; public void UpdateMod( Mod mod, ModSettings? settings ) @@ -90,38 +120,38 @@ public class ItemSwapWindow : IDisposable _swapData.Clear(); _loadException = null; - if( _targetId > 0 && _sourceId > 0 ) + try { - try + switch( _lastTab ) { - switch( _lastTab ) - { - case SwapType.Equipment: break; - case SwapType.Accessory: break; - case SwapType.Hair: - - _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); - break; - case SwapType.Face: - _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); - break; - case SwapType.Ears: - _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId ); - break; - case SwapType.Tail: - _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); - break; - case SwapType.Weapon: break; - case SwapType.Minion: break; - case SwapType.Mount: break; - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); - _loadException = e; + case SwapType.Equipment when _slotSelector?.CurrentSelection != null && _equipSelector.CurrentSelection != null: + _affectedItems = _swapData.LoadEquipment( _equipSelector.CurrentSelection, _slotSelector.CurrentSelection ); + break; + case SwapType.Accessory when _slotSelector?.CurrentSelection != null && _accessorySelector.CurrentSelection != null: + _affectedItems = _swapData.LoadEquipment( _accessorySelector.CurrentSelection, _slotSelector.CurrentSelection ); + break; + case SwapType.Hair when _targetId > 0 && _sourceId > 0: + _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Face when _targetId > 0 && _sourceId > 0: + _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Ears when _targetId > 0 && _sourceId > 0: + _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Tail when _targetId > 0 && _sourceId > 0: + _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + break; + case SwapType.Weapon: break; + case SwapType.Minion: break; + case SwapType.Mount: break; } } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); + _loadException = e; + } _dirty = false; } @@ -174,7 +204,7 @@ public class ItemSwapWindow : IDisposable ImGui.SameLine(); tt = "Create a new option inside this mod containing only the swap."; if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt, - true || (!newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) )) ) + true || !newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) ) ) { } ImGui.SameLine(); @@ -201,15 +231,13 @@ public class ItemSwapWindow : IDisposable { using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None ); + DrawArmorSwap(); + DrawAccessorySwap(); DrawHairSwap(); DrawFaceSwap(); DrawEarSwap(); DrawTailSwap(); - DrawArmorSwap(); - DrawAccessorySwap(); DrawWeaponSwap(); - DrawMinionSwap(); - DrawMountSwap(); } private ImRaii.IEndObject DrawTab( SwapType newTab ) @@ -218,7 +246,7 @@ public class ItemSwapWindow : IDisposable if( tab ) { _dirty |= _lastTab != newTab; - _lastTab = newTab; + _lastTab = newTab; } UpdateState(); @@ -228,22 +256,60 @@ public class ItemSwapWindow : IDisposable private void DrawArmorSwap() { - using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Equipment ); + using var tab = DrawTab( SwapType.Equipment ); if( !tab ) { return; } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "Take this piece of equipment" ); + ImGui.TableNextColumn(); + if( _equipSelector.Draw( "##itemTarget", _equipSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + { + var slot = ( ( EquipSlot )( _equipSelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); + if( slot != _slotSelector?.CurrentSlot ) + _slotSelector = new SlotSelector( slot ); + _dirty = true; + } + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "And put it on this one" ); + ImGui.TableNextColumn(); + _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); + _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); } private void DrawAccessorySwap() { - using var disabled = ImRaii.Disabled(); using var tab = DrawTab( SwapType.Accessory ); if( !tab ) { return; } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "Take this accessory" ); + ImGui.TableNextColumn(); + if( _accessorySelector.Draw( "##itemTarget", _accessorySelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + { + var slot = ( ( EquipSlot )( _accessorySelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); + if( slot != _slotSelector?.CurrentSlot ) + _slotSelector = new SlotSelector( slot ); + _dirty = true; + } + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "And put it on this one" ); + ImGui.TableNextColumn(); + _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); + _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); } private void DrawHairSwap() @@ -286,7 +352,7 @@ public class ItemSwapWindow : IDisposable using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); DrawTargetIdInput( "Take this Tail Type" ); DrawSourceIdInput(); - DrawGenderInput("for all", 2); + DrawGenderInput( "for all", 2 ); } @@ -315,26 +381,6 @@ public class ItemSwapWindow : IDisposable } } - private void DrawMinionSwap() - { - using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Minion ); - if( !tab ) - { - return; - } - } - - private void DrawMountSwap() - { - using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Mount ); - if( !tab ) - { - return; - } - } - private const float InputWidth = 120; private void DrawTargetIdInput( string text = "Take this ID" ) @@ -346,7 +392,10 @@ public class ItemSwapWindow : IDisposable ImGui.TableNextColumn(); ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) ) + { _targetId = Math.Clamp( _targetId, 0, byte.MaxValue ); + } + _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } @@ -358,21 +407,11 @@ public class ItemSwapWindow : IDisposable ImGui.TableNextColumn(); ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if (ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 )) + if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) ) + { _sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue ); - _dirty |= ImGui.IsItemDeactivatedAfterEdit(); - } + } - private void DrawVariantInput( string text ) - { - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##variantId", ref _currentVariant, 0, 0 ) ) - _currentVariant = Math.Clamp( _currentVariant, 0, byte.MaxValue ); _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 03f984c0..c3ab2a14 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -184,7 +184,7 @@ public partial class ModEditWindow { _dialogManager.Draw(); - using var tab = ImRaii.TabItem( "Texture Import/Export (WIP)" ); + using var tab = ImRaii.TabItem( "Texture Import/Export" ); if( !tab ) { return; diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 886a47d2..f7a09c88 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -531,11 +531,11 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) { - _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", + _materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty ); - _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", + _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty ); From 33b4905ae28c251c19e2851b761d6462f0ecfd80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 12:26:05 +0100 Subject: [PATCH 0655/2451] Add some checks for valid variants in IMC Meta Edits. --- Penumbra/Api/PenumbraApi.cs | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 6 ++++- Penumbra/Meta/Manager/MetaManager.Imc.cs | 7 ++++- .../Meta/Manipulations/ImcManipulation.cs | 26 ++++++++++++++----- .../Meta/Manipulations/MetaManipulation.cs | 8 +++--- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 4 +-- 7 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 54be2b27..d9a198ec 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -963,7 +963,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } manips = new HashSet< MetaManipulation >( manipArray!.Length ); - foreach( var manip in manipArray ) + foreach( var manip in manipArray.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { if( !manips.Add( manip ) ) { diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 2ea01648..252a1720 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -138,7 +138,11 @@ public partial class TexToolsMeta { if( _keepDefault || !value.Equals( def.GetEntry( partIdx, i ) ) ) { - MetaManipulations.Add( new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ) ); + var imc = new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ); + if( imc.Valid ) + { + MetaManipulations.Add( imc ); + } } ++i; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index d61f158f..406b5671 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -53,6 +53,11 @@ public partial class MetaManager public bool ApplyMod( ImcManipulation manip ) { + if( !manip.Valid ) + { + return false; + } + _imcManipulations.AddOrReplace( manip ); var path = manip.GamePath(); try @@ -91,7 +96,7 @@ public partial class MetaManager public bool RevertMod( ImcManipulation m ) { - if( !_imcManipulations.Remove( m ) ) + if( !m.Valid || !_imcManipulations.Remove( m ) ) { return false; } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 4c65fd6d..f4656a79 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -32,13 +32,17 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { Entry = entry; PrimaryId = primaryId; - Variant = ( byte )variant; + Variant = ( byte )Math.Clamp( variant, ( ushort )0, byte.MaxValue ); SecondaryId = 0; ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; EquipSlot = equipSlot; - BodySlot = BodySlot.Unknown; + BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; } + // Variants were initially ushorts but got shortened to bytes. + // There are still some manipulations around that have values > 255 for variant, + // so we change the unused value to something nonsensical in that case, just so they do not compare equal, + // and clamp the variant to 255. [JsonConstructor] internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry ) @@ -46,16 +50,17 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > Entry = entry; ObjectType = objectType; PrimaryId = primaryId; - Variant = ( byte )variant; + Variant = ( byte )Math.Clamp( variant, ( ushort )0, byte.MaxValue ); + if( objectType is ObjectType.Accessory or ObjectType.Equipment ) { - BodySlot = BodySlot.Unknown; + BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; SecondaryId = 0; EquipSlot = equipSlot; } else if( objectType is ObjectType.DemiHuman ) { - BodySlot = BodySlot.Unknown; + BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; SecondaryId = secondaryId; EquipSlot = equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot; } @@ -63,10 +68,19 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { BodySlot = bodySlot; SecondaryId = secondaryId; - EquipSlot = EquipSlot.Unknown; + EquipSlot = variant > byte.MaxValue ? EquipSlot.All : EquipSlot.Unknown; } } + public bool Valid + => ObjectType switch + { + ObjectType.Accessory => BodySlot == BodySlot.Unknown, + ObjectType.Equipment => BodySlot == BodySlot.Unknown, + ObjectType.DemiHuman => BodySlot == BodySlot.Unknown, + _ => EquipSlot == EquipSlot.Unknown, + }; + public ImcManipulation Copy( ImcEntry entry ) => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant, EquipSlot, entry); diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 5cc96f98..d1ec5f3a 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -100,7 +100,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa return; case ImcManipulation m: Imc = m; - ManipulationType = Type.Imc; + ManipulationType = m.Valid ? Type.Imc : Type.Unknown; return; } } @@ -194,7 +194,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Type.Est => Est.Equals( other.Est ), Type.Rsp => Rsp.Equals( other.Rsp ), Type.Imc => Imc.Equals( other.Imc ), - _ => throw new ArgumentOutOfRangeException(), + _ => false, }; } @@ -229,7 +229,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Type.Est => Est.GetHashCode(), Type.Rsp => Rsp.GetHashCode(), Type.Imc => Imc.GetHashCode(), - _ => throw new ArgumentOutOfRangeException(), + _ => 0, }; public unsafe int CompareTo( MetaManipulation other ) @@ -249,7 +249,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Type.Est => Est.ToString(), Type.Rsp => Rsp.ToString(), Type.Imc => Imc.ToString(), - _ => throw new ArgumentOutOfRangeException(), + _ => "Invalid", }; public string EntryToString() diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index b38307be..a7b4981f 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -173,7 +173,7 @@ public partial class Mod var manips = json[ nameof( Manipulations ) ]; if( manips != null ) { - foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ) ) + foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { ManipulationData.Add( s ); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 9707fb6f..66ac4a87 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -930,7 +930,7 @@ public partial class ModEditWindow var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); if( version == MetaManipulation.CurrentVersion && manips != null ) { - foreach( var manip in manips ) + foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { _editor!.Meta.Set( manip ); } @@ -950,7 +950,7 @@ public partial class ModEditWindow if( version == MetaManipulation.CurrentVersion && manips != null ) { _editor!.Meta.Clear(); - foreach( var manip in manips ) + foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { _editor!.Meta.Set( manip ); } From ab53f17a7ee65bcd72f908ed9fe9689a4680e159 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 21:56:25 +0100 Subject: [PATCH 0656/2451] Add equipment swaps and writing to option. --- Penumbra.GameData/Data/ItemData.cs | 82 ++++ Penumbra.GameData/Enums/FullEquipType.cs | 358 ++++++++++++++++ Penumbra.GameData/Enums/WeaponCategory.cs | 156 ------- .../Mods/ItemSwap/EquipmentDataContainer.cs | 230 ----------- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 8 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 13 +- Penumbra/Mods/Mod.Creation.cs | 6 +- Penumbra/Penumbra.cs | 8 + Penumbra/UI/Classes/ItemSwapWindow.cs | 388 ++++++++++++------ 9 files changed, 723 insertions(+), 526 deletions(-) create mode 100644 Penumbra.GameData/Data/ItemData.cs create mode 100644 Penumbra.GameData/Enums/FullEquipType.cs delete mode 100644 Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs new file mode 100644 index 00000000..96e305ad --- /dev/null +++ b/Penumbra.GameData/Data/ItemData.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; + +namespace Penumbra.GameData.Data; + +public sealed class ItemData : DataSharer, IReadOnlyDictionary> +{ + private readonly IReadOnlyList> _items; + + private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) + { + var tmp = Enum.GetValues().Select(t => new List(1024)).ToArray(); + + var itemSheet = dataManager.GetExcelSheet(language)!; + foreach (var item in itemSheet) + { + var type = item.ToEquipType(); + if (type != FullEquipType.Unknown && item.Name.RawData.Length > 1) + tmp[(int)type].Add(item); + } + + var ret = new IReadOnlyList[tmp.Length]; + ret[0] = Array.Empty(); + for (var i = 1; i < tmp.Length; ++i) + ret[i] = tmp[i].OrderBy(item => item.Name.ToDalamudString().TextValue).ToArray(); + + return ret; + } + + public ItemData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, 1) + { + _items = TryCatchData("ItemList", () => CreateItems(dataManager, language)); + } + + protected override void DisposeInternal() + => DisposeTag("ItemList"); + + public IEnumerator>> GetEnumerator() + { + for (var i = 1; i < _items.Count; ++i) + yield return new KeyValuePair>((FullEquipType)i, _items[i]); + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _items.Count - 1; + + public bool ContainsKey(FullEquipType key) + => (int)key < _items.Count && key != FullEquipType.Unknown; + + public bool TryGetValue(FullEquipType key, out IReadOnlyList value) + { + if (ContainsKey(key)) + { + value = _items[(int)key]; + return true; + } + + value = _items[0]; + return false; + } + + public IReadOnlyList this[FullEquipType key] + => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); + + public IEnumerable Keys + => Enum.GetValues().Skip(1); + + public IEnumerable> Values + => _items.Skip(1); +} diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs new file mode 100644 index 00000000..27c046ea --- /dev/null +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.GameData.Enums; + +public enum FullEquipType : byte +{ + Unknown, + + Head, + Body, + Hands, + Legs, + Feet, + + Ears, + Neck, + Wrists, + Finger, + + Fists, // PGL, MNK + Sword, // GLA, PLD Main + Axe, // MRD, WAR + Bow, // ARC, BRD + Lance, // LNC, DRG, + Staff, // THM, BLM, CNJ, WHM + Wand, // THM, BLM, CNJ, WHM Main + Book, // ACN, SMN, SCH + Daggers, // ROG, NIN + Broadsword, // DRK, + Gun, // MCH, + Orrery, // AST, + Katana, // SAM + Rapier, // RDM + Cane, // BLU + Gunblade, // GNB, + Glaives, // DNC, + Scythe, // RPR, + Nouliths, // SGE + Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off + + Saw, // CRP + CrossPeinHammer, // BSM + RaisingHammer, // ARM + LapidaryHammer, // GSM + Knife, // LTW + Needle, // WVR + Alembic, // ALC + Frypan, // CUL + Pickaxe, // MIN + Hatchet, // BTN + FishingRod, // FSH + + ClawHammer, // CRP Off + File, // BSM Off + Pliers, // ARM Off + GrindingWheel, // GSM Off + Awl, // LTW Off + SpinningWheel, // WVR Off + Mortar, // ALC Off + CulinaryKnife, // CUL Off + Sledgehammer, // MIN Off + GardenScythe, // BTN Off + Gig, // FSH Off +} + +public static class FullEquipTypeExtensions +{ + public static FullEquipType ToEquipType(this Item item) + { + var slot = (EquipSlot)item.EquipSlotCategory.Row; + var weapon = (WeaponCategory)item.ItemUICategory.Row; + return slot.ToEquipType(weapon); + } + + public static bool IsWeapon(this FullEquipType type) + => type switch + { + FullEquipType.Fists => true, + FullEquipType.Sword => true, + FullEquipType.Axe => true, + FullEquipType.Bow => true, + FullEquipType.Lance => true, + FullEquipType.Staff => true, + FullEquipType.Wand => true, + FullEquipType.Book => true, + FullEquipType.Daggers => true, + FullEquipType.Broadsword => true, + FullEquipType.Gun => true, + FullEquipType.Orrery => true, + FullEquipType.Katana => true, + FullEquipType.Rapier => true, + FullEquipType.Cane => true, + FullEquipType.Gunblade => true, + FullEquipType.Glaives => true, + FullEquipType.Scythe => true, + FullEquipType.Nouliths => true, + FullEquipType.Shield => true, + _ => false, + }; + + public static bool IsTool(this FullEquipType type) + => type switch + { + FullEquipType.Saw => true, + FullEquipType.CrossPeinHammer => true, + FullEquipType.RaisingHammer => true, + FullEquipType.LapidaryHammer => true, + FullEquipType.Knife => true, + FullEquipType.Needle => true, + FullEquipType.Alembic => true, + FullEquipType.Frypan => true, + FullEquipType.Pickaxe => true, + FullEquipType.Hatchet => true, + FullEquipType.FishingRod => true, + _ => false, + }; + + public static bool IsEquipment(this FullEquipType type) + => type switch + { + FullEquipType.Head => true, + FullEquipType.Body => true, + FullEquipType.Hands => true, + FullEquipType.Legs => true, + FullEquipType.Feet => true, + _ => false, + }; + + public static bool IsAccessory(this FullEquipType type) + => type switch + { + FullEquipType.Ears => true, + FullEquipType.Neck => true, + FullEquipType.Wrists => true, + FullEquipType.Finger => true, + _ => false, + }; + + public static string ToName(this FullEquipType type) + => type switch + { + FullEquipType.Head => EquipSlot.Head.ToName(), + FullEquipType.Body => EquipSlot.Body.ToName(), + FullEquipType.Hands => EquipSlot.Hands.ToName(), + FullEquipType.Legs => EquipSlot.Legs.ToName(), + FullEquipType.Feet => EquipSlot.Feet.ToName(), + FullEquipType.Ears => EquipSlot.Ears.ToName(), + FullEquipType.Neck => EquipSlot.Neck.ToName(), + FullEquipType.Wrists => EquipSlot.Wrists.ToName(), + FullEquipType.Finger => "Ring", + FullEquipType.Fists => "Fist Weapon", + FullEquipType.Sword => "Sword", + FullEquipType.Axe => "Axe", + FullEquipType.Bow => "Bow", + FullEquipType.Lance => "Lance", + FullEquipType.Staff => "Staff", + FullEquipType.Wand => "Mace", + FullEquipType.Book => "Book", + FullEquipType.Daggers => "Dagger", + FullEquipType.Broadsword => "Broadsword", + FullEquipType.Gun => "Gun", + FullEquipType.Orrery => "Orrery", + FullEquipType.Katana => "Katana", + FullEquipType.Rapier => "Rapier", + FullEquipType.Cane => "Cane", + FullEquipType.Gunblade => "Gunblade", + FullEquipType.Glaives => "Glaive", + FullEquipType.Scythe => "Scythe", + FullEquipType.Nouliths => "Nouliths", + FullEquipType.Shield => "Shield", + FullEquipType.Saw => "Saw (Carpenter)", + FullEquipType.CrossPeinHammer => "Hammer (Blacksmith)", + FullEquipType.RaisingHammer => "Hammer (Armorsmith)", + FullEquipType.LapidaryHammer => "Hammer (Goldsmith)", + FullEquipType.Knife => "Knife (Leatherworker)", + FullEquipType.Needle => "Needle (Weaver)", + FullEquipType.Alembic => "Alembic (Alchemist)", + FullEquipType.Frypan => "Frypan (Culinarian)", + FullEquipType.Pickaxe => "Pickaxe (Miner)", + FullEquipType.Hatchet => "Hatchet (Botanist)", + FullEquipType.FishingRod => "Fishing Rod", + FullEquipType.ClawHammer => "Clawhammer (Carpenter)", + FullEquipType.File => "File (Blacksmith)", + FullEquipType.Pliers => "Pliers (Armorsmith)", + FullEquipType.GrindingWheel => "Grinding Wheel (Goldsmith)", + FullEquipType.Awl => "Awl (Leatherworker)", + FullEquipType.SpinningWheel => "Spinning Wheel (Weaver)", + FullEquipType.Mortar => "Mortar (Alchemist)", + FullEquipType.CulinaryKnife => "Knife (Culinarian)", + FullEquipType.Sledgehammer => "Sledgehammer (Miner)", + FullEquipType.GardenScythe => "Garden Scythe (Botanist)", + FullEquipType.Gig => "Gig (Fisher)", + _ => "Unknown", + }; + + public static EquipSlot ToSlot(this FullEquipType type) + => type switch + { + FullEquipType.Head => EquipSlot.Head, + FullEquipType.Body => EquipSlot.Body, + FullEquipType.Hands => EquipSlot.Hands, + FullEquipType.Legs => EquipSlot.Legs, + FullEquipType.Feet => EquipSlot.Feet, + FullEquipType.Ears => EquipSlot.Ears, + FullEquipType.Neck => EquipSlot.Neck, + FullEquipType.Wrists => EquipSlot.Wrists, + FullEquipType.Finger => EquipSlot.RFinger, + FullEquipType.Fists => EquipSlot.MainHand, + FullEquipType.Sword => EquipSlot.MainHand, + FullEquipType.Axe => EquipSlot.MainHand, + FullEquipType.Bow => EquipSlot.MainHand, + FullEquipType.Lance => EquipSlot.MainHand, + FullEquipType.Staff => EquipSlot.MainHand, + FullEquipType.Wand => EquipSlot.MainHand, + FullEquipType.Book => EquipSlot.MainHand, + FullEquipType.Daggers => EquipSlot.MainHand, + FullEquipType.Broadsword => EquipSlot.MainHand, + FullEquipType.Gun => EquipSlot.MainHand, + FullEquipType.Orrery => EquipSlot.MainHand, + FullEquipType.Katana => EquipSlot.MainHand, + FullEquipType.Rapier => EquipSlot.MainHand, + FullEquipType.Cane => EquipSlot.MainHand, + FullEquipType.Gunblade => EquipSlot.MainHand, + FullEquipType.Glaives => EquipSlot.MainHand, + FullEquipType.Scythe => EquipSlot.MainHand, + FullEquipType.Nouliths => EquipSlot.MainHand, + FullEquipType.Shield => EquipSlot.OffHand, + FullEquipType.Saw => EquipSlot.MainHand, + FullEquipType.CrossPeinHammer => EquipSlot.MainHand, + FullEquipType.RaisingHammer => EquipSlot.MainHand, + FullEquipType.LapidaryHammer => EquipSlot.MainHand, + FullEquipType.Knife => EquipSlot.MainHand, + FullEquipType.Needle => EquipSlot.MainHand, + FullEquipType.Alembic => EquipSlot.MainHand, + FullEquipType.Frypan => EquipSlot.MainHand, + FullEquipType.Pickaxe => EquipSlot.MainHand, + FullEquipType.Hatchet => EquipSlot.MainHand, + FullEquipType.FishingRod => EquipSlot.MainHand, + FullEquipType.ClawHammer => EquipSlot.OffHand, + FullEquipType.File => EquipSlot.OffHand, + FullEquipType.Pliers => EquipSlot.OffHand, + FullEquipType.GrindingWheel => EquipSlot.OffHand, + FullEquipType.Awl => EquipSlot.OffHand, + FullEquipType.SpinningWheel => EquipSlot.OffHand, + FullEquipType.Mortar => EquipSlot.OffHand, + FullEquipType.CulinaryKnife => EquipSlot.OffHand, + FullEquipType.Sledgehammer => EquipSlot.OffHand, + FullEquipType.GardenScythe => EquipSlot.OffHand, + FullEquipType.Gig => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; + + public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown) + => slot switch + { + EquipSlot.Head => FullEquipType.Head, + EquipSlot.Body => FullEquipType.Body, + EquipSlot.Hands => FullEquipType.Hands, + EquipSlot.Legs => FullEquipType.Legs, + EquipSlot.Feet => FullEquipType.Feet, + EquipSlot.Ears => FullEquipType.Ears, + EquipSlot.Neck => FullEquipType.Neck, + EquipSlot.Wrists => FullEquipType.Wrists, + EquipSlot.RFinger => FullEquipType.Finger, + EquipSlot.LFinger => FullEquipType.Finger, + EquipSlot.HeadBody => FullEquipType.Body, + EquipSlot.BodyHandsLegsFeet => FullEquipType.Body, + EquipSlot.LegsFeet => FullEquipType.Legs, + EquipSlot.FullBody => FullEquipType.Body, + EquipSlot.BodyHands => FullEquipType.Body, + EquipSlot.BodyLegsFeet => FullEquipType.Body, + EquipSlot.ChestHands => FullEquipType.Body, + EquipSlot.MainHand => category.ToEquipType(), + EquipSlot.OffHand => category.ToEquipType(), + EquipSlot.BothHand => category.ToEquipType(), + _ => FullEquipType.Unknown, + }; + + public static FullEquipType ToEquipType(this WeaponCategory category) + => category switch + { + WeaponCategory.Pugilist => FullEquipType.Fists, + WeaponCategory.Gladiator => FullEquipType.Sword, + WeaponCategory.Marauder => FullEquipType.Axe, + WeaponCategory.Archer => FullEquipType.Bow, + WeaponCategory.Lancer => FullEquipType.Lance, + WeaponCategory.Thaumaturge1 => FullEquipType.Wand, + WeaponCategory.Thaumaturge2 => FullEquipType.Staff, + WeaponCategory.Conjurer1 => FullEquipType.Wand, + WeaponCategory.Conjurer2 => FullEquipType.Staff, + WeaponCategory.Arcanist => FullEquipType.Book, + WeaponCategory.Shield => FullEquipType.Shield, + WeaponCategory.CarpenterMain => FullEquipType.Saw, + WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, + WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, + WeaponCategory.BlacksmithOff => FullEquipType.File, + WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, + WeaponCategory.ArmorerOff => FullEquipType.Pliers, + WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, + WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, + WeaponCategory.LeatherworkerMain => FullEquipType.Knife, + WeaponCategory.LeatherworkerOff => FullEquipType.Awl, + WeaponCategory.WeaverMain => FullEquipType.Needle, + WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, + WeaponCategory.AlchemistMain => FullEquipType.Alembic, + WeaponCategory.AlchemistOff => FullEquipType.Mortar, + WeaponCategory.CulinarianMain => FullEquipType.Frypan, + WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, + WeaponCategory.MinerMain => FullEquipType.Pickaxe, + WeaponCategory.MinerOff => FullEquipType.Sledgehammer, + WeaponCategory.BotanistMain => FullEquipType.Hatchet, + WeaponCategory.BotanistOff => FullEquipType.GardenScythe, + WeaponCategory.FisherMain => FullEquipType.FishingRod, + WeaponCategory.Rogue => FullEquipType.Gig, + WeaponCategory.DarkKnight => FullEquipType.Broadsword, + WeaponCategory.Machinist => FullEquipType.Gun, + WeaponCategory.Astrologian => FullEquipType.Orrery, + WeaponCategory.Samurai => FullEquipType.Katana, + WeaponCategory.RedMage => FullEquipType.Rapier, + WeaponCategory.Scholar => FullEquipType.Book, + WeaponCategory.FisherOff => FullEquipType.Gig, + WeaponCategory.BlueMage => FullEquipType.Cane, + WeaponCategory.Gunbreaker => FullEquipType.Gunblade, + WeaponCategory.Dancer => FullEquipType.Glaives, + WeaponCategory.Reaper => FullEquipType.Scythe, + WeaponCategory.Sage => FullEquipType.Nouliths, + _ => FullEquipType.Unknown, + }; + + public static FullEquipType Offhand(this FullEquipType type) + => type switch + { + FullEquipType.Fists => FullEquipType.Fists, + FullEquipType.Sword => FullEquipType.Shield, + FullEquipType.Wand => FullEquipType.Shield, + FullEquipType.Daggers => FullEquipType.Daggers, + FullEquipType.Gun => FullEquipType.Gun, + FullEquipType.Orrery => FullEquipType.Orrery, + FullEquipType.Rapier => FullEquipType.Rapier, + FullEquipType.Glaives => FullEquipType.Glaives, + _ => FullEquipType.Unknown, + }; + + public static readonly IReadOnlyList WeaponTypes + = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); + + public static readonly IReadOnlyList ToolTypes + = Enum.GetValues().Where(v => v.IsTool()).ToArray(); + + public static readonly IReadOnlyList EquipmentTypes + = Enum.GetValues().Where(v => v.IsEquipment()).ToArray(); + + public static readonly IReadOnlyList AccessoryTypes + = Enum.GetValues().Where(v => v.IsAccessory()).ToArray(); +} diff --git a/Penumbra.GameData/Enums/WeaponCategory.cs b/Penumbra.GameData/Enums/WeaponCategory.cs index f7b1fc91..b40fa48a 100644 --- a/Penumbra.GameData/Enums/WeaponCategory.cs +++ b/Penumbra.GameData/Enums/WeaponCategory.cs @@ -50,160 +50,4 @@ public enum WeaponCategory : byte Dancer = 107, Reaper = 108, Sage = 109, -} - -public static class WeaponCategoryExtensions -{ - public static WeaponCategory AllowsOffHand( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => WeaponCategory.Pugilist, - WeaponCategory.Gladiator => WeaponCategory.Shield, - WeaponCategory.Marauder => WeaponCategory.Unknown, - WeaponCategory.Archer => WeaponCategory.Unknown, - WeaponCategory.Lancer => WeaponCategory.Unknown, - WeaponCategory.Thaumaturge1 => WeaponCategory.Shield, - WeaponCategory.Thaumaturge2 => WeaponCategory.Unknown, - WeaponCategory.Conjurer1 => WeaponCategory.Shield, - WeaponCategory.Conjurer2 => WeaponCategory.Unknown, - WeaponCategory.Arcanist => WeaponCategory.Unknown, - WeaponCategory.Shield => WeaponCategory.Unknown, - WeaponCategory.CarpenterMain => WeaponCategory.CarpenterOff, - WeaponCategory.CarpenterOff => WeaponCategory.Unknown, - WeaponCategory.BlacksmithMain => WeaponCategory.BlacksmithOff, - WeaponCategory.BlacksmithOff => WeaponCategory.Unknown, - WeaponCategory.ArmorerMain => WeaponCategory.ArmorerOff, - WeaponCategory.ArmorerOff => WeaponCategory.Unknown, - WeaponCategory.GoldsmithMain => WeaponCategory.GoldsmithOff, - WeaponCategory.GoldsmithOff => WeaponCategory.Unknown, - WeaponCategory.LeatherworkerMain => WeaponCategory.LeatherworkerOff, - WeaponCategory.LeatherworkerOff => WeaponCategory.Unknown, - WeaponCategory.WeaverMain => WeaponCategory.WeaverOff, - WeaponCategory.WeaverOff => WeaponCategory.Unknown, - WeaponCategory.AlchemistMain => WeaponCategory.AlchemistOff, - WeaponCategory.AlchemistOff => WeaponCategory.Unknown, - WeaponCategory.CulinarianMain => WeaponCategory.CulinarianOff, - WeaponCategory.CulinarianOff => WeaponCategory.Unknown, - WeaponCategory.MinerMain => WeaponCategory.MinerOff, - WeaponCategory.MinerOff => WeaponCategory.Unknown, - WeaponCategory.BotanistMain => WeaponCategory.BotanistOff, - WeaponCategory.BotanistOff => WeaponCategory.Unknown, - WeaponCategory.FisherMain => WeaponCategory.FisherOff, - WeaponCategory.Rogue => WeaponCategory.Rogue, - WeaponCategory.DarkKnight => WeaponCategory.Unknown, - WeaponCategory.Machinist => WeaponCategory.Machinist, - WeaponCategory.Astrologian => WeaponCategory.Astrologian, - WeaponCategory.Samurai => WeaponCategory.Unknown, - WeaponCategory.RedMage => WeaponCategory.RedMage, - WeaponCategory.Scholar => WeaponCategory.Unknown, - WeaponCategory.FisherOff => WeaponCategory.Unknown, - WeaponCategory.BlueMage => WeaponCategory.Unknown, - WeaponCategory.Gunbreaker => WeaponCategory.Unknown, - WeaponCategory.Dancer => WeaponCategory.Dancer, - WeaponCategory.Reaper => WeaponCategory.Unknown, - WeaponCategory.Sage => WeaponCategory.Unknown, - _ => WeaponCategory.Unknown, - }; - - public static EquipSlot ToSlot( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => EquipSlot.MainHand, - WeaponCategory.Gladiator => EquipSlot.MainHand, - WeaponCategory.Marauder => EquipSlot.MainHand, - WeaponCategory.Archer => EquipSlot.MainHand, - WeaponCategory.Lancer => EquipSlot.MainHand, - WeaponCategory.Thaumaturge1 => EquipSlot.MainHand, - WeaponCategory.Thaumaturge2 => EquipSlot.MainHand, - WeaponCategory.Conjurer1 => EquipSlot.MainHand, - WeaponCategory.Conjurer2 => EquipSlot.MainHand, - WeaponCategory.Arcanist => EquipSlot.MainHand, - WeaponCategory.Shield => EquipSlot.OffHand, - WeaponCategory.CarpenterMain => EquipSlot.MainHand, - WeaponCategory.CarpenterOff => EquipSlot.OffHand, - WeaponCategory.BlacksmithMain => EquipSlot.MainHand, - WeaponCategory.BlacksmithOff => EquipSlot.OffHand, - WeaponCategory.ArmorerMain => EquipSlot.MainHand, - WeaponCategory.ArmorerOff => EquipSlot.OffHand, - WeaponCategory.GoldsmithMain => EquipSlot.MainHand, - WeaponCategory.GoldsmithOff => EquipSlot.OffHand, - WeaponCategory.LeatherworkerMain => EquipSlot.MainHand, - WeaponCategory.LeatherworkerOff => EquipSlot.OffHand, - WeaponCategory.WeaverMain => EquipSlot.MainHand, - WeaponCategory.WeaverOff => EquipSlot.OffHand, - WeaponCategory.AlchemistMain => EquipSlot.MainHand, - WeaponCategory.AlchemistOff => EquipSlot.OffHand, - WeaponCategory.CulinarianMain => EquipSlot.MainHand, - WeaponCategory.CulinarianOff => EquipSlot.OffHand, - WeaponCategory.MinerMain => EquipSlot.MainHand, - WeaponCategory.MinerOff => EquipSlot.OffHand, - WeaponCategory.BotanistMain => EquipSlot.MainHand, - WeaponCategory.BotanistOff => EquipSlot.OffHand, - WeaponCategory.FisherMain => EquipSlot.MainHand, - WeaponCategory.Rogue => EquipSlot.MainHand, - WeaponCategory.DarkKnight => EquipSlot.MainHand, - WeaponCategory.Machinist => EquipSlot.MainHand, - WeaponCategory.Astrologian => EquipSlot.MainHand, - WeaponCategory.Samurai => EquipSlot.MainHand, - WeaponCategory.RedMage => EquipSlot.MainHand, - WeaponCategory.Scholar => EquipSlot.MainHand, - WeaponCategory.FisherOff => EquipSlot.OffHand, - WeaponCategory.BlueMage => EquipSlot.MainHand, - WeaponCategory.Gunbreaker => EquipSlot.MainHand, - WeaponCategory.Dancer => EquipSlot.MainHand, - WeaponCategory.Reaper => EquipSlot.MainHand, - WeaponCategory.Sage => EquipSlot.MainHand, - _ => EquipSlot.Unknown, - }; - - public static int ToIndex( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => 0, - WeaponCategory.Gladiator => 1, - WeaponCategory.Marauder => 2, - WeaponCategory.Archer => 3, - WeaponCategory.Lancer => 4, - WeaponCategory.Thaumaturge1 => 5, - WeaponCategory.Thaumaturge2 => 6, - WeaponCategory.Conjurer1 => 7, - WeaponCategory.Conjurer2 => 8, - WeaponCategory.Arcanist => 9, - WeaponCategory.Shield => 10, - WeaponCategory.CarpenterMain => 11, - WeaponCategory.CarpenterOff => 12, - WeaponCategory.BlacksmithMain => 13, - WeaponCategory.BlacksmithOff => 14, - WeaponCategory.ArmorerMain => 15, - WeaponCategory.ArmorerOff => 16, - WeaponCategory.GoldsmithMain => 17, - WeaponCategory.GoldsmithOff => 18, - WeaponCategory.LeatherworkerMain => 19, - WeaponCategory.LeatherworkerOff => 20, - WeaponCategory.WeaverMain => 21, - WeaponCategory.WeaverOff => 22, - WeaponCategory.AlchemistMain => 23, - WeaponCategory.AlchemistOff => 24, - WeaponCategory.CulinarianMain => 25, - WeaponCategory.CulinarianOff => 26, - WeaponCategory.MinerMain => 27, - WeaponCategory.MinerOff => 28, - WeaponCategory.BotanistMain => 29, - WeaponCategory.BotanistOff => 30, - WeaponCategory.FisherMain => 31, - WeaponCategory.Rogue => 32, - WeaponCategory.DarkKnight => 33, - WeaponCategory.Machinist => 34, - WeaponCategory.Astrologian => 35, - WeaponCategory.Samurai => 36, - WeaponCategory.RedMage => 37, - WeaponCategory.Scholar => 38, - WeaponCategory.FisherOff => 39, - WeaponCategory.BlueMage => 40, - WeaponCategory.Gunbreaker => 41, - WeaponCategory.Dancer => 42, - WeaponCategory.Reaper => 43, - WeaponCategory.Sage => 44, - _ => -1, - }; } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs b/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs deleted file mode 100644 index 70847af8..00000000 --- a/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods.ItemSwap; - -public class EquipmentDataContainer -{ - public Item Item; - public EquipSlot Slot; - public SetId ModelId; - public byte Variant; - - public ImcManipulation ImcData; - - public EqpManipulation EqpData; - public GmpManipulation GmpData; - - // Example: Abyssos Helm / Body - public string AvfxPath = string.Empty; - - // Example: Dodore Doublet, but unknown what it does? - public string SoundPath = string.Empty; - - // Example: Crimson Standard Bracelet - public string DecalPath = string.Empty; - - // Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. - public string AnimationPath = string.Empty; - - public Dictionary< GenderRace, GenderRaceContainer > Files = new(); - - public struct GenderRaceContainer - { - public EqdpManipulation Eqdp; - public GenderRace ModelRace; - public GenderRace MaterialRace; - public EstManipulation Est; - public string MdlPath; - public MtrlContainer[] MtrlPaths; - } - - public struct MtrlContainer - { - public string MtrlPath; - public string[] Textures; - public string Shader; - - public MtrlContainer( string mtrlPath ) - { - MtrlPath = mtrlPath; - var file = Dalamud.GameData.GetFile( mtrlPath ); - if( file != null ) - { - var mtrl = new MtrlFile( file.Data ); - Textures = mtrl.Textures.Select( t => t.Path ).ToArray(); - Shader = $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}"; - } - else - { - Textures = Array.Empty< string >(); - Shader = string.Empty; - } - } - } - - - private static EstManipulation GetEstEntry( GenderRace genderRace, SetId setId, EquipSlot slot ) - { - if( slot == EquipSlot.Head ) - { - var entry = EstFile.GetDefault( EstManipulation.EstType.Head, genderRace, setId.Value ); - return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Head, setId.Value, entry ); - } - - if( slot == EquipSlot.Body ) - { - var entry = EstFile.GetDefault( EstManipulation.EstType.Body, genderRace, setId.Value ); - return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Body, setId.Value, entry ); - } - - return default; - } - - private static GenderRaceContainer GetGenderRace( GenderRace genderRace, SetId modelId, EquipSlot slot, ushort materialId ) - { - var ret = new GenderRaceContainer() - { - Eqdp = GetEqdpEntry( genderRace, modelId, slot ), - Est = GetEstEntry( genderRace, modelId, slot ), - }; - ( ret.ModelRace, ret.MaterialRace ) = TraverseEqdpTree( genderRace, modelId, slot ); - ret.MdlPath = GamePaths.Equipment.Mdl.Path( modelId, ret.ModelRace, slot ); - ret.MtrlPaths = MtrlPaths( ret.MdlPath, ret.MaterialRace, modelId, materialId ); - return ret; - } - - private static EqdpManipulation GetEqdpEntry( GenderRace genderRace, SetId modelId, EquipSlot slot ) - { - var entry = ExpandedEqdpFile.GetDefault( genderRace, slot.IsAccessory(), modelId.Value ); - return new EqdpManipulation( entry, slot, genderRace.Split().Item1, genderRace.Split().Item2, modelId.Value ); - } - - private static MtrlContainer[] MtrlPaths( string mdlPath, GenderRace mtrlRace, SetId modelId, ushort materialId ) - { - var file = Dalamud.GameData.GetFile( mdlPath ); - if( file == null ) - { - return Array.Empty< MtrlContainer >(); - } - - var mdl = new MdlFile( Dalamud.GameData.GetFile( mdlPath )!.Data ); - var basePath = GamePaths.Equipment.Mtrl.FolderPath( modelId, ( byte )materialId ); - var equipPart = $"e{modelId.Value:D4}"; - var racePart = $"c{mtrlRace.ToRaceCode()}"; - - return mdl.Materials - .Where( m => m.Contains( equipPart ) ) - .Select( m => new MtrlContainer( $"{basePath}{m.Replace( "c0101", racePart )}" ) ) - .ToArray(); - } - - private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot ) - { - var model = GenderRace.Unknown; - var material = GenderRace.Unknown; - var accessory = slot.IsAccessory(); - foreach( var gr in genderRace.Dependencies() ) - { - var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value ); - var (b1, b2) = entry.ToBits( slot ); - if( b1 && material == GenderRace.Unknown ) - { - material = gr; - if( model != GenderRace.Unknown ) - { - return ( model, material ); - } - } - - if( b2 && model == GenderRace.Unknown ) - { - model = gr; - if( material != GenderRace.Unknown ) - { - return ( model, material ); - } - } - } - - return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale ); - } - - - public EquipmentDataContainer( Item i ) - { - Item = i; - LookupItem( i, out Slot, out ModelId, out Variant ); - LookupImc( ModelId, Variant, Slot ); - EqpData = new EqpManipulation( ExpandedEqpFile.GetDefault( ModelId.Value ), Slot, ModelId.Value ); - GmpData = Slot == EquipSlot.Head ? new GmpManipulation( ExpandedGmpFile.GetDefault( ModelId.Value ), ModelId.Value ) : default; - - - foreach( var genderRace in Enum.GetValues< GenderRace >() ) - { - if( CharacterUtility.EqdpIdx( genderRace, Slot.IsAccessory() ) < 0 ) - { - continue; - } - - Files[ genderRace ] = GetGenderRace( genderRace, ModelId, Slot, ImcData.Entry.MaterialId ); - } - } - - - private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) - { - slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - if( !slot.IsEquipment() ) - { - throw new ItemSwap.InvalidItemTypeException(); - } - - modelId = ( ( Quad )i.ModelMain ).A; - variant = ( byte )( ( Quad )i.ModelMain ).B; - } - - - - private void LookupImc( SetId modelId, byte variant, EquipSlot slot ) - { - var imc = ImcFile.GetDefault( GamePaths.Equipment.Imc.Path( modelId ), slot, variant, out var exists ); - if( !exists ) - { - throw new ItemSwap.InvalidImcException(); - } - - ImcData = new ImcManipulation( slot, variant, modelId.Value, imc ); - if( imc.DecalId != 0 ) - { - DecalPath = GamePaths.Equipment.Decal.Path( imc.DecalId ); - } - - // TODO: Figure out how this works. - if( imc.SoundId != 0 ) - { - SoundPath = string.Empty; - } - - if( imc.VfxId != 0 ) - { - AvfxPath = GamePaths.Equipment.Avfx.Path( modelId, imc.VfxId ); - } - - // TODO: Figure out how this works. - if( imc.MaterialAnimationId != 0 ) - { - AnimationPath = string.Empty; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index f065d6be..301108f2 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -252,9 +252,13 @@ public static class EquipmentSwap return false; } + // IMC also controls sound, Example: Dodore Doublet, but unknown what it does? + // IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. + return true; } - + + // Example: Crimson Standard Bracelet public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc ) { if( decalId != 0 ) @@ -271,6 +275,8 @@ public static class EquipmentSwap return true; } + + // Example: Abyssos Helm / Body public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc ) { if( vfxId != 0 ) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 8027e7d1..eae8a60b 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -37,11 +37,12 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps ) + public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) { var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); + directory ??= mod.ModPath; try { foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) ) @@ -62,7 +63,7 @@ public class ItemSwapContainer } else { - var path = file.GetNewPath( mod.ModPath.FullName ); + var path = file.GetNewPath( directory.FullName ); var bytes = file.FileData.Write(); Directory.CreateDirectory( Path.GetDirectoryName( path )! ); File.WriteAllBytes( path, bytes ); @@ -80,9 +81,9 @@ public class ItemSwapContainer } } - Penumbra.ModManager.OptionSetFiles( mod, -1, 0, convertedFiles ); - Penumbra.ModManager.OptionSetFileSwaps( mod, -1, 0, convertedSwaps ); - Penumbra.ModManager.OptionSetManipulations( mod, -1, 0, convertedManips ); + Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); + Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); + Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); return true; } catch( Exception e ) @@ -120,7 +121,7 @@ public class ItemSwapContainer Loaded = true; return ret; } - catch( Exception e ) + catch { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index afb59c94..40cc9d2f 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -47,8 +47,10 @@ public partial class Mod return new DirectoryInfo( newModFolder ); } - // Create the name for a group or option subfolder based on its parent folder and given name. - // subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) { var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b2550a69..91c24705 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,13 @@ using Penumbra.Util; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; +using Penumbra.Meta.Files; using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -63,6 +68,7 @@ public class Penumbra : IDalamudPlugin public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainManager StainManager { get; private set; } = null!; + public static ItemData ItemData { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -92,6 +98,7 @@ public class Penumbra : IDalamudPlugin Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); + ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ); Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); Framework = new FrameworkManager(); @@ -289,6 +296,7 @@ public class Penumbra : IDalamudPlugin Api?.Dispose(); _commandHandler?.Dispose(); StainManager?.Dispose(); + ItemData?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 2d23abc4..2e59b540 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; @@ -14,46 +17,48 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; +using Penumbra.Util; namespace Penumbra.UI.Classes; public class ItemSwapWindow : IDisposable { - private class EquipSelector : FilterComboCache< Item > + private enum SwapType { - public EquipSelector() - : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipment() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) - { } - - protected override string ToString( Item obj ) - => obj.Name.ToString(); + Hat, + Top, + Gloves, + Pants, + Shoes, + Earrings, + Necklace, + Bracelet, + Ring, + Hair, + Face, + Ears, + Tail, + Weapon, } - private class AccessorySelector : FilterComboCache< Item > + private class ItemSelector : FilterComboCache< (string, Item) > { - public AccessorySelector() - : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsAccessory() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) + public ItemSelector( FullEquipType type ) + : base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() ) { } - protected override string ToString( Item obj ) - => obj.Name.ToString(); + protected override string ToString( (string, Item) obj ) + => obj.Item1; } - private class SlotSelector : FilterComboCache< Item > + private class WeaponSelector : FilterComboCache< FullEquipType > { - public readonly EquipSlot CurrentSlot; + public WeaponSelector() + : base( FullEquipTypeExtensions.WeaponTypes.Concat( FullEquipTypeExtensions.ToolTypes ) ) + { } - public SlotSelector( EquipSlot slot ) - : base( () => Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot() == slot && i.ModelMain != 0 && i.Name.RawData.Length > 0 ).ToList() ) - { - CurrentSlot = slot; - } - - protected override string ToString( Item obj ) - => obj.Name.ToString(); + protected override string ToString( FullEquipType type ) + => type.ToName(); } public ItemSwapWindow() @@ -68,30 +73,44 @@ public class ItemSwapWindow : IDisposable Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; } - private readonly EquipSelector _equipSelector = new(); - private readonly AccessorySelector _accessorySelector = new(); - private SlotSelector? _slotSelector; - private readonly ItemSwapContainer _swapData = new(); + private readonly Dictionary< SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo) > _selectors = new() + { + [ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), + [ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), + [ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), + [ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), + [ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), + [ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), + [ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), + [ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), + [ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), + }; + + private ItemSelector? _weaponSource = null; + private ItemSelector? _weaponTarget = null; + private readonly WeaponSelector _slotSelector = new(); + private readonly ItemSwapContainer _swapData = new(); private Mod? _mod; private ModSettings? _modSettings; private bool _dirty; - private SwapType _lastTab = SwapType.Equipment; - private Gender _currentGender = Gender.Male; - private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId = 0; - private int _sourceId = 0; - private Exception? _loadException = null; + private SwapType _lastTab = SwapType.Hair; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId = 0; + private int _sourceId = 0; + private Exception? _loadException = null; - private string _newModName = string.Empty; - private string _newGroupName = "Swaps"; - private string _newOptionName = string.Empty; - private bool _useFileSwaps = true; + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private IModGroup? _selectedGroup = null; + private bool _subModValid = false; + private bool _useFileSwaps = true; private Item[]? _affectedItems; - public void UpdateMod( Mod mod, ModSettings? settings ) { if( mod == _mod && settings == _modSettings ) @@ -108,6 +127,7 @@ public class ItemSwapWindow : IDisposable _mod = mod; _modSettings = settings; _swapData.LoadMod( _mod, _modSettings ); + UpdateOption(); _dirty = true; } @@ -120,15 +140,26 @@ public class ItemSwapWindow : IDisposable _swapData.Clear(); _loadException = null; + _affectedItems = null; try { switch( _lastTab ) { - case SwapType.Equipment when _slotSelector?.CurrentSelection != null && _equipSelector.CurrentSelection != null: - _affectedItems = _swapData.LoadEquipment( _equipSelector.CurrentSelection, _slotSelector.CurrentSelection ); - break; - case SwapType.Accessory when _slotSelector?.CurrentSelection != null && _accessorySelector.CurrentSelection != null: - _affectedItems = _swapData.LoadEquipment( _accessorySelector.CurrentSelection, _slotSelector.CurrentSelection ); + case SwapType.Hat: + case SwapType.Top: + case SwapType.Gloves: + case SwapType.Pants: + case SwapType.Shoes: + case SwapType.Earrings: + case SwapType.Necklace: + case SwapType.Bracelet: + case SwapType.Ring: + var values = _selectors[ _lastTab ]; + if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) + { + _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2 ); + } + break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); @@ -143,8 +174,6 @@ public class ItemSwapWindow : IDisposable _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); break; case SwapType.Weapon: break; - case SwapType.Minion: break; - case SwapType.Mount: break; } } catch( Exception e ) @@ -169,6 +198,93 @@ public class ItemSwapWindow : IDisposable private string CreateDescription() => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + private void UpdateOption() + { + _selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName ); + _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); + } + + private void CreateMod() + { + var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); + Mod.CreateDefaultFiles( newDir ); + Penumbra.ModManager.AddMod( newDir ); + if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) + { + Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); + } + } + + private void CreateOption() + { + if( _mod == null || !_subModValid ) + { + return; + } + + var groupCreated = false; + var dirCreated = false; + var optionCreated = false; + DirectoryInfo? optionFolderName = null; + try + { + optionFolderName = Mod.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); + if( optionFolderName?.Exists == true ) + { + throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); + } + + if( optionFolderName != null ) + { + if( _selectedGroup == null ) + { + Penumbra.ModManager.AddModGroup( _mod, GroupType.Multi, _newGroupName ); + _selectedGroup = _mod.Groups.Last(); + groupCreated = true; + } + + Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName ); + optionCreated = true; + optionFolderName = Directory.CreateDirectory( optionFolderName.FullName ); + dirCreated = true; + if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, + _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) ) + { + throw new Exception( "Failure writing files for mod swap." ); + } + } + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error ); + try + { + if( optionCreated && _selectedGroup != null ) + { + Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ); + } + + if( groupCreated ) + { + Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) ); + _selectedGroup = null; + } + + if( dirCreated && optionFolderName != null ) + { + Directory.Delete( optionFolderName.FullName, true ); + } + } + catch + { + // ignored + } + } + + UpdateOption(); + } + private void DrawHeaderLine( float width ) { var newModAvailable = _loadException == null && _swapData.Loaded; @@ -178,34 +294,40 @@ public class ItemSwapWindow : IDisposable { } ImGui.SameLine(); - var tt = "Create a new mod of the given name containing only the swap."; + var tt = !newModAvailable + ? "No swap is currently loaded." + : _newModName.Length == 0 + ? "Please enter a name for your mod." + : "Create a new mod of the given name containing only the swap."; if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) { - var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); - Mod.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); - if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) - { - Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); - } + CreateMod(); } ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) - { } + { + UpdateOption(); + } ImGui.SameLine(); ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) - { } + { + UpdateOption(); + } ImGui.SameLine(); - tt = "Create a new option inside this mod containing only the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt, - true || !newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) ) ) - { } + tt = !_subModValid + ? "An option with that name already exists in that group, or no name is specified." + : !newModAvailable + ? "Create a new option inside this mod containing only the swap." + : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; + if( ImGuiUtil.DrawDisabledButton( "Create New Option", new Vector2( width / 2, 0 ), tt, !newModAvailable || !_subModValid ) ) + { + CreateOption(); + } ImGui.SameLine(); var newPos = new Vector2( ImGui.GetCursorPosX() + 10 * ImGuiHelpers.GlobalScale, ImGui.GetCursorPosY() - ( ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); @@ -214,25 +336,19 @@ public class ItemSwapWindow : IDisposable ImGuiUtil.HoverTooltip( "Use File Swaps." ); } - private enum SwapType - { - Equipment, - Accessory, - Hair, - Face, - Ears, - Tail, - Weapon, - Minion, - Mount, - } - private void DrawSwapBar() { using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None ); - DrawArmorSwap(); - DrawAccessorySwap(); + DrawEquipmentSwap( SwapType.Hat ); + DrawEquipmentSwap( SwapType.Top ); + DrawEquipmentSwap( SwapType.Gloves ); + DrawEquipmentSwap( SwapType.Pants ); + DrawEquipmentSwap( SwapType.Shoes ); + DrawEquipmentSwap( SwapType.Earrings ); + DrawEquipmentSwap( SwapType.Necklace ); + DrawEquipmentSwap( SwapType.Bracelet ); + DrawEquipmentSwap( SwapType.Ring ); DrawHairSwap(); DrawFaceSwap(); DrawEarSwap(); @@ -254,64 +370,39 @@ public class ItemSwapWindow : IDisposable return tab; } - private void DrawArmorSwap() + private void DrawEquipmentSwap( SwapType type ) { - using var tab = DrawTab( SwapType.Equipment ); + using var tab = DrawTab( type ); if( !tab ) { return; } + var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ]; using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Take this piece of equipment" ); + ImGui.TextUnformatted( text1 ); ImGui.TableNextColumn(); - if( _equipSelector.Draw( "##itemTarget", _equipSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) - { - var slot = ( ( EquipSlot )( _equipSelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); - if( slot != _slotSelector?.CurrentSlot ) - _slotSelector = new SlotSelector( slot ); - _dirty = true; - } + _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "And put it on this one" ); + ImGui.TextUnformatted( text2 ); ImGui.TableNextColumn(); - _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); - _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + if( _affectedItems is { Length: > 0 } ) + { + ImGui.SameLine(); + ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( '\n', _affectedItems.Select( i => i.Name.ToDalamudString().TextValue ) ) ); + } + } } - - private void DrawAccessorySwap() - { - using var tab = DrawTab( SwapType.Accessory ); - if( !tab ) - { - return; - } - - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Take this accessory" ); - ImGui.TableNextColumn(); - if( _accessorySelector.Draw( "##itemTarget", _accessorySelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) - { - var slot = ( ( EquipSlot )( _accessorySelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); - if( slot != _slotSelector?.CurrentSlot ) - _slotSelector = new SlotSelector( slot ); - _dirty = true; - } - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "And put it on this one" ); - ImGui.TableNextColumn(); - _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); - _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - } - + private void DrawHairSwap() { using var tab = DrawTab( SwapType.Hair ); @@ -379,6 +470,36 @@ public class ItemSwapWindow : IDisposable { return; } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "Select the weapon or tool you want" ); + ImGui.TableNextColumn(); + if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + { + _dirty = true; + _weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); + _weaponTarget = new ItemSelector( _slotSelector.CurrentSelection ); + } + else + { + _dirty = _weaponSource == null || _weaponTarget == null; + _weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection ); + _weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection ); + } + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "and put this variant of it" ); + ImGui.TableNextColumn(); + _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "onto this one" ); + ImGui.TableNextColumn(); + _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); } private const float InputWidth = 120; @@ -444,16 +565,21 @@ public class ItemSwapWindow : IDisposable private string NonExistentText() => _lastTab switch { - SwapType.Equipment => "One of the selected pieces of equipment does not seem to exist.", - SwapType.Accessory => "One of the selected accessories does not seem to exist.", - SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", - SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", - SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", - SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.", - SwapType.Weapon => "One of the selected weapons does not seem to exist.", - SwapType.Minion => "One of the selected minions does not seem to exist.", - SwapType.Mount => "One of the selected mounts does not seem to exist.", - _ => string.Empty, + SwapType.Hat => "One of the selected hats does not seem to exist.", + SwapType.Top => "One of the selected tops does not seem to exist.", + SwapType.Gloves => "One of the selected pairs of gloves does not seem to exist.", + SwapType.Pants => "One of the selected pants does not seem to exist.", + SwapType.Shoes => "One of the selected pairs of shoes does not seem to exist.", + SwapType.Earrings => "One of the selected earrings does not seem to exist.", + SwapType.Necklace => "One of the selected necklaces does not seem to exist.", + SwapType.Bracelet => "One of the selected bracelets does not seem to exist.", + SwapType.Ring => "One of the selected rings does not seem to exist.", + SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", + SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", + SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", + SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.", + SwapType.Weapon => "One of the selected weapons or tools does not seem to exist.", + _ => string.Empty, }; From 5dd4701c4c6cd853b56597920c4d99e9ff585b2f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 21:56:39 +0100 Subject: [PATCH 0657/2451] Improve AVFX writing. --- Penumbra.GameData/Files/AvfxFile.cs | 93 ++++++++++++++-------------- Penumbra.GameData/Files/AvfxMagic.cs | 32 +++++++--- 2 files changed, 69 insertions(+), 56 deletions(-) diff --git a/Penumbra.GameData/Files/AvfxFile.cs b/Penumbra.GameData/Files/AvfxFile.cs index 3077d960..330a8416 100644 --- a/Penumbra.GameData/Files/AvfxFile.cs +++ b/Penumbra.GameData/Files/AvfxFile.cs @@ -20,8 +20,8 @@ public class AvfxFile : IWritable Data = r.ReadBytes((int)Size.RoundTo4()); } - public bool ToBool() - => BitConverter.ToBoolean(Data); + public byte ToBool() + => BitConverter.ToBoolean(Data) ? (byte)1 : (byte)0; public uint ToUint() => BitConverter.ToUInt32(Data); @@ -36,51 +36,52 @@ public class AvfxFile : IWritable } } + public static readonly Vector3 BadVector = new(float.NaN); - public Vector3 ClipBox; - public Vector3 ClipBoxSize; - public Vector3 RevisedValuesPos; - public Vector3 RevisedValuesRot; - public Vector3 RevisedValuesScale; - public Vector3 RevisedValuesColor; + public Vector3 ClipBox = BadVector; + public Vector3 ClipBoxSize = BadVector; + public Vector3 RevisedValuesPos = BadVector; + public Vector3 RevisedValuesRot = BadVector; + public Vector3 RevisedValuesScale = BadVector; + public Vector3 RevisedValuesColor = BadVector; - public uint Version; - public uint DrawLayerType; - public uint DrawOrderType; - public uint DirectionalLightSourceType; - public uint PointLightsType1; - public uint PointLightsType2; + public uint Version = uint.MaxValue; + public uint DrawLayerType = uint.MaxValue; + public uint DrawOrderType = uint.MaxValue; + public uint DirectionalLightSourceType = uint.MaxValue; + public uint PointLightsType1 = uint.MaxValue; + public uint PointLightsType2 = uint.MaxValue; - public float BiasZmaxScale; - public float BiasZmaxDistance; - public float NearClipBegin; - public float NearClipEnd; - public float FadeInnerX; - public float FadeOuterX; - public float FadeInnerY; - public float FadeOuterY; - public float FadeInnerZ; - public float FadeOuterZ; - public float FarClipBegin; - public float FarClipEnd; - public float SoftParticleFadeRange; - public float SoftKeyOffset; - public float GlobalFogInfluence; + public float BiasZmaxScale = float.NaN; + public float BiasZmaxDistance = float.NaN; + public float NearClipBegin = float.NaN; + public float NearClipEnd = float.NaN; + public float FadeInnerX = float.NaN; + public float FadeOuterX = float.NaN; + public float FadeInnerY = float.NaN; + public float FadeOuterY = float.NaN; + public float FadeInnerZ = float.NaN; + public float FadeOuterZ = float.NaN; + public float FarClipBegin = float.NaN; + public float FarClipEnd = float.NaN; + public float SoftParticleFadeRange = float.NaN; + public float SoftKeyOffset = float.NaN; + public float GlobalFogInfluence = float.NaN; - public bool IsDelayFastParticle; - public bool IsFitGround; - public bool IsTransformSkip; - public bool IsAllStopOnHide; - public bool CanBeClippedOut; - public bool ClipBoxEnabled; - public bool IsCameraSpace; - public bool IsFullEnvLight; - public bool IsClipOwnSetting; - public bool FadeEnabledX; - public bool FadeEnabledY; - public bool FadeEnabledZ; - public bool GlobalFogEnabled; - public bool LtsEnabled; + public byte IsDelayFastParticle = byte.MaxValue; + public byte IsFitGround = byte.MaxValue; + public byte IsTransformSkip = byte.MaxValue; + public byte IsAllStopOnHide = byte.MaxValue; + public byte CanBeClippedOut = byte.MaxValue; + public byte ClipBoxEnabled = byte.MaxValue; + public byte IsCameraSpace = byte.MaxValue; + public byte IsFullEnvLight = byte.MaxValue; + public byte IsClipOwnSetting = byte.MaxValue; + public byte FadeEnabledX = byte.MaxValue; + public byte FadeEnabledY = byte.MaxValue; + public byte FadeEnabledZ = byte.MaxValue; + public byte GlobalFogEnabled = byte.MaxValue; + public byte LtsEnabled = byte.MaxValue; public Block[] Schedulers = Array.Empty(); public Block[] Timelines = Array.Empty(); @@ -91,7 +92,8 @@ public class AvfxFile : IWritable public string[] Textures = Array.Empty(); public Block[] Models = Array.Empty(); - public bool Valid { get; } = true; + public bool Valid + => true; public AvfxFile(byte[] data) { @@ -198,7 +200,6 @@ public class AvfxFile : IWritable var sizePos = w.BaseStream.Position; w.Write(0u); w.WriteBlock(AvfxMagic.Version, Version) - .WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle) .WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle) .WriteBlock(AvfxMagic.IsFitGround, IsFitGround) .WriteBlock(AvfxMagic.IsTransformSkip, IsTransformSkip) @@ -276,7 +277,7 @@ public class AvfxFile : IWritable foreach (var block in Models) w.WriteBlock(block); w.Seek((int)sizePos, SeekOrigin.Begin); - w.Write((uint)w.BaseStream.Length); + w.Write((uint)w.BaseStream.Length - 8u); return m.ToArray(); } } diff --git a/Penumbra.GameData/Files/AvfxMagic.cs b/Penumbra.GameData/Files/AvfxMagic.cs index 1c315711..0a78b4fb 100644 --- a/Penumbra.GameData/Files/AvfxMagic.cs +++ b/Penumbra.GameData/Files/AvfxMagic.cs @@ -108,25 +108,37 @@ public static class AvfxMagic internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, uint value) { - bw.Write(magic); - bw.Write(4u); - bw.Write(value); + if (value != uint.MaxValue) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value); + } + return bw; } - internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, bool value) + internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, byte value) { - bw.Write(magic); - bw.Write(4u); - bw.Write(value ? 1u : 0u); + if (value != byte.MaxValue) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value == 1 ? 1u : 0u); + } + return bw; } internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, float value) { - bw.Write(magic); - bw.Write(4u); - bw.Write(value); + if (!float.IsNaN(value)) + { + bw.Write(magic); + bw.Write(4u); + bw.Write(value); + } + return bw; } From c590b7fb2458a5d6b93e928cd584ffef4f5a5157 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Dec 2022 22:22:35 +0100 Subject: [PATCH 0658/2451] Add Changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 4600e29f..38446319 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using OtterGui.Widgets; namespace Penumbra.UI; @@ -27,10 +28,35 @@ public partial class ConfigWindow Add6_0_0( ret ); Add6_0_2( ret ); Add6_0_5( ret ); + Add6_1_0( ret ); return ret; } + private static void Add6_1_0( Changelog log ) + => log.NextVersion( "Version 0.6.1.0 (Happy New Year! Edition)" ) + .RegisterEntry( "Added a prototype for Item Swapping." ) + .RegisterEntry( "A new tab in Advanced Editing.", 1 ) + .RegisterEntry( "Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1 ) + .RegisterEntry( "The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" + + " should be used when creating the swap, but you can also just swap unmodded things.", 1 ) + .RegisterEntry( "You can write a swap to a new mod, or to a new option in the currently selected mod.", 1 ) + .RegisterEntry( "The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1 ) + .RegisterEntry( "More detailed help or explanations will be added later.", 1 ) + .RegisterEntry( "Heavily improved Chat Commands. Use /penumbra help for more information." ) + .RegisterEntry( "Penumbra now considers meta manipulations for Changed Items." ) + .RegisterEntry( "Penumbra now tries to associate battle voices to specific actors, so that they work in collections." ) + .RegisterEntry( "Heavily improved .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects." ) + .RegisterEntry( "Improved some file handling for Mare-Interaction." ) + .RegisterEntry( "Added Equipment Slots to Demihuman IMC Edits." ) + .RegisterEntry( "Added a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files." ) + .RegisterEntry( "Added an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra." ) + .RegisterEntry( "Added API to copy mod settings from one mod to another." ) + .RegisterEntry( "Fixed a problem where creating individual collections did not trigger events." ) + .RegisterEntry( "Added a Hack to support Anamnesis Redrawing better. (0.6.0.6)" ) + .RegisterEntry( "Fixed another problem with the aesthetician. (0.6.0.6)" ) + .RegisterEntry( "Fixed a problem with the export directory not being respected. (0.6.0.6)" ); + private static void Add6_0_5( Changelog log ) => log.NextVersion( "Version 0.6.0.5" ) .RegisterEntry( "Allow hyphen as last character in player and retainer names." ) From 6493394256fc6c4d6e5d284a5fdbcbf079361a3a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 31 Dec 2022 23:01:00 +0000 Subject: [PATCH 0659/2451] [CI] Updating repo.json for refs/tags/0.6.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5ea7f55d..f628b801 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.0.6", - "TestingAssemblyVersion": "0.6.0.6", + "AssemblyVersion": "0.6.1.0", + "TestingAssemblyVersion": "0.6.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.0.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 29d01e698b1b42ae2231bc0a31239f601418c1e0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jan 2023 13:55:23 +0100 Subject: [PATCH 0660/2451] Fix swapping universal hairstyles for midlanders breaking them for others. --- Penumbra.GameData/Data/MaterialHandling.cs | 5 +++-- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData/Data/MaterialHandling.cs b/Penumbra.GameData/Data/MaterialHandling.cs index 09bbab51..ef336a9d 100644 --- a/Penumbra.GameData/Data/MaterialHandling.cs +++ b/Penumbra.GameData/Data/MaterialHandling.cs @@ -22,10 +22,11 @@ public static class MaterialHandling // All hairstyles above 116 are shared except for Hrothgar if (hairId.Value is >= 116 and <= 200) - { return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale; - } return actualGr; } + + public static bool IsSpecialCase(GenderRace gr, SetId hairId) + => gr is GenderRace.MidlanderMale or GenderRace.MidlanderFemale && hairId.Value is >= 101 and <= 200; } diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index b9e9a94f..f650818a 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -60,9 +60,9 @@ public static class CustomizationSwap var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant ); var newFileName = fileName; - newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); - newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); - newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race ); + newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); + newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); + newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race || MaterialHandling.IsSpecialCase( race, idFrom ) ); newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value ); var actualMtrlFromPath = mtrlFromPath; From 45ec212b781e53caf0617a97163516e8908c432c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jan 2023 22:06:01 +0100 Subject: [PATCH 0661/2451] Change Item Swaps to use exceptions for actual error messages. --- Penumbra/Meta/Manager/MetaManager.cs | 1 - Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 55 ++--- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 227 ++++++-------------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 34 ++- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 46 ++-- Penumbra/Mods/ItemSwap/Swaps.cs | 46 ++-- Penumbra/UI/Classes/ItemSwapWindow.cs | 25 ++- 7 files changed, 149 insertions(+), 285 deletions(-) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 0469dc09..a37af335 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using OtterGui; using Penumbra.Collections; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index f650818a..9b346c00 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -14,22 +12,17 @@ namespace Penumbra.Mods.ItemSwap; public static class CustomizationSwap { /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. - public static bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, out FileSwap mdl ) + public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) { if( idFrom.Value > byte.MaxValue ) { - mdl = new FileSwap(); - return false; + throw new Exception( $"The Customization ID {idFrom} is too large for {slot}." ); } var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() ); var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() ); - if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) ) - { - return false; - } - + var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1; foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() ) @@ -38,22 +31,18 @@ public static class CustomizationSwap foreach( var variant in Enumerable.Range( 1, range ) ) { name = materialFileName; - if( !CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged, out var mtrl ) ) - { - return false; - } - + var mtrl = CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged ); mdl.ChildSwaps.Add( mtrl ); } materialFileName = name; } - return true; + return mdl; } - public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, - ref string fileName, ref bool dataWasChanged, out FileSwap mtrl ) + public static FileSwap CreateMtrl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, + ref string fileName, ref bool dataWasChanged ) { variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; var mtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant ); @@ -73,33 +62,21 @@ public static class CustomizationSwap dataWasChanged = true; } - if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, out mtrl, actualMtrlFromPath ) ) - { - return false; - } - - if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shpk ) ) - { - return false; - } - + var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath ); + var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( shpk ); foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) { - if( !CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged, out var tex ) ) - { - return false; - } - + var tex = CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( tex ); } - return true; + return mtrl; } - public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, - ref bool dataWasChanged, out FileSwap tex ) + public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, + ref bool dataWasChanged ) { var path = texture.Path; var addedDashes = false; @@ -122,13 +99,13 @@ public static class CustomizationSwap dataWasChanged = true; } - return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path ); + return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); } - public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk ) + public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) { var path = $"shader/sm5/shpk/{shaderName}"; - return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk ); + return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); } } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 301108f2..3bdc99a9 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -17,7 +17,7 @@ namespace Penumbra.Mods.ItemSwap; public static class EquipmentSwap { - public static Item[] CreateItemSwap( List< Swap > swaps, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, Item itemFrom, + public static Item[] CreateItemSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, Item itemTo ) { // Check actual ids, variants and slots. We only support using the same slot. @@ -28,21 +28,13 @@ public static class EquipmentSwap throw new ItemSwap.InvalidItemTypeException(); } - if( !CreateEqp( manips, slotFrom, idFrom, idTo, out var eqp ) ) - { - throw new Exception( "Could not get Eqp Entry for Swap." ); - } - + var eqp = CreateEqp( manips, slotFrom, idFrom, idTo ); if( eqp != null ) { swaps.Add( eqp ); } - if( !CreateGmp( manips, slotFrom, idFrom, idTo, out var gmp ) ) - { - throw new Exception( "Could not get Gmp Entry for Swap." ); - } - + var gmp = CreateGmp( manips, slotFrom, idFrom, idTo); if( gmp != null ) { swaps.Add( gmp ); @@ -68,21 +60,13 @@ public static class EquipmentSwap continue; } - if( !ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, out var est ) ) - { - throw new Exception( "Could not get Est Entry for Swap." ); - } - + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); if( est != null ) { swaps.Add( est ); } - if( !CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo, out var eqdp ) ) - { - throw new Exception( "Could not get Eqdp Entry for Swap." ); - } - + var eqdp = CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo ); if( eqdp != null ) { swaps.Add( eqdp ); @@ -91,11 +75,7 @@ public static class EquipmentSwap foreach( var variant in variants ) { - if( !CreateImc( redirections, manips, slotFrom, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo, out var imc ) ) - { - throw new Exception( "Could not get IMC Entry for Swap." ); - } - + var imc = CreateImc( redirections, manips, slotFrom, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); swaps.Add( imc ); } @@ -103,21 +83,17 @@ public static class EquipmentSwap return affectedItems; } - public static bool CreateEqdp( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, - SetId idTo, byte mtrlTo, out MetaSwap? meta ) + public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo ) { var (gender, race) = gr.Split(); var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idFrom.Value ), slot, gender, race, idFrom.Value ); var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idTo.Value ), slot, gender, race, idTo.Value ); - meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); + var meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slot ); if( ownMdl ) { - if( !CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo, out var mdl ) ) - { - return false; - } - + var mdl = CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo ); meta.ChildSwaps.Add( mdl ); } else if( !ownMtrl && meta.SwapAppliedIsDefault ) @@ -125,64 +101,25 @@ public static class EquipmentSwap meta = null; } - return true; + return meta; } - public static bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo, - out FileSwap mdl ) + public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) { var mdlPathFrom = GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); var mdlPathTo = GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); - if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) ) - { - return false; - } + var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) { - if( !CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged, out var mtrl ) ) - { - return false; - } - + var mtrl = CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); if( mtrl != null ) { mdl.ChildSwaps.Add( mtrl ); } } - return true; - } - - private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot ) - { - var model = GenderRace.Unknown; - var material = GenderRace.Unknown; - var accessory = slot.IsAccessory(); - foreach( var gr in genderRace.Dependencies() ) - { - var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value ); - var (b1, b2) = entry.ToBits( slot ); - if( b1 && material == GenderRace.Unknown ) - { - material = gr; - if( model != GenderRace.Unknown ) - { - return ( model, material ); - } - } - - if( b2 && model == GenderRace.Unknown ) - { - model = gr; - if( material != GenderRace.Unknown ) - { - return ( model, material ); - } - } - } - - return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale ); + return mdl; } private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) @@ -219,115 +156,99 @@ public static class EquipmentSwap return ( imc, variants, items ); } - public static bool CreateGmp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? gmp ) + public static MetaSwap? CreateGmp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) { if( slot is not EquipSlot.Head ) { - gmp = null; - return true; + return null; } var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value ); var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value ); - gmp = new MetaSwap( manips, manipFrom, manipTo ); - return true; + return new MetaSwap( manips, manipFrom, manipTo ); } - public static bool CreateImc( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, - byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo, out MetaSwap imc ) + public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) { var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slot ), variantFrom ); var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ); var manipulationFrom = new ImcManipulation( slot, variantFrom, idFrom.Value, entryFrom ); var manipulationTo = new ImcManipulation( slot, variantTo, idTo.Value, entryTo ); - imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); + var imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); - if( !AddDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId, imc ) ) + var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId ); + if( decal != null ) { - return false; + imc.ChildSwaps.Add( decal ); } - if( !AddAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId, imc ) ) + var avfx = CreateAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId ); + if( avfx != null ) { - return false; + imc.ChildSwaps.Add( avfx ); } // IMC also controls sound, Example: Dodore Doublet, but unknown what it does? // IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. - - return true; + return imc; } - + // Example: Crimson Standard Bracelet - public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc ) + public static FileSwap? CreateDecal( Func< Utf8GamePath, FullPath > redirections, byte decalId ) { - if( decalId != 0 ) + if( decalId == 0 ) { - var decalPath = GamePaths.Equipment.Decal.Path( decalId ); - if( !FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath, out var swap ) ) - { - return false; - } - - imc.ChildSwaps.Add( swap ); + return null; } - return true; + var decalPath = GamePaths.Equipment.Decal.Path( decalId ); + return FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath ); } - + // Example: Abyssos Helm / Body - public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc ) + public static FileSwap? CreateAvfx( Func< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId ) { - if( vfxId != 0 ) + if( vfxId == 0 ) { - var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId ); - var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId ); - if( !FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo, out var swap ) ) - { - return false; - } - - foreach( ref var filePath in swap.AsAvfx()!.Textures.AsSpan() ) - { - if( !CreateAtex( redirections, ref filePath, ref swap.DataWasChanged, out var atex ) ) - { - return false; - } - - swap.ChildSwaps.Add( atex ); - } - - imc.ChildSwaps.Add( swap ); + return null; } - return true; + var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId ); + var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId ); + var avfx = FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo ); + + foreach( ref var filePath in avfx.AsAvfx()!.Textures.AsSpan() ) + { + var atex = CreateAtex( redirections, ref filePath, ref avfx.DataWasChanged ); + avfx.ChildSwaps.Add( atex ); + } + + return avfx; } - public static bool CreateEqp( HashSet< MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, out MetaSwap? eqp ) + public static MetaSwap? CreateEqp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) { if( slot.IsAccessory() ) { - eqp = null; - return true; + return null; } var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value ); var eqpValueTo = ExpandedEqpFile.GetDefault( idTo.Value ); var eqpFrom = new EqpManipulation( eqpValueFrom, slot, idFrom.Value ); var eqpTo = new EqpManipulation( eqpValueTo, slot, idFrom.Value ); - eqp = new MetaSwap( manips, eqpFrom, eqpTo ); - return true; + return new MetaSwap( manips, eqpFrom, eqpTo ); } - public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, - ref bool dataWasChanged, out FileSwap? mtrl ) + public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged ) { var prefix = slot.IsAccessory() ? 'a' : 'e'; if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) { - mtrl = null; - return true; + return null; } var folderTo = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); @@ -343,34 +264,20 @@ public static class EquipmentSwap dataWasChanged = true; } - if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo, out mtrl ) ) - { - return false; - } - - if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shader ) ) - { - return false; - } - - mtrl.ChildSwaps.Add( shader ); + var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo ); + var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); + mtrl.ChildSwaps.Add( shpk ); foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) { - if( !CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged, out var swap ) ) - { - return false; - } - - mtrl.ChildSwaps.Add( swap ); + var tex = CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); + mtrl.ChildSwaps.Add( tex ); } - return true; + return mtrl; } - public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, - ref bool dataWasChanged, - out FileSwap tex ) + public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) { var path = texture.Path; var addedDashes = false; @@ -392,21 +299,21 @@ public static class EquipmentSwap dataWasChanged = true; } - return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path ); + return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); } - public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk ) + public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) { var path = $"shader/sm5/shpk/{shaderName}"; - return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk ); + return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); } - public static bool CreateAtex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged, out FileSwap atex ) + public static FileSwap CreateAtex( Func< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged ) { var oldPath = filePath; filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}", true ); dataWasChanged = true; - return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, out atex, oldPath ); + return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, oldPath ); } } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index d32e2073..d8e8809a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.RegularExpressions; @@ -137,54 +136,45 @@ public static class ItemSwap } - public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap phyb ) + public static FileSwap CreatePhyb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) { var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb ); + return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath ); } - public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry, out FileSwap sklb ) + public static FileSwap CreateSklb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) { var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb ); + return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath ); } /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > manips, EstManipulation.EstType type, - GenderRace genderRace, SetId idFrom, SetId idTo, out MetaSwap? est ) + public static MetaSwap? CreateEst( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, + GenderRace genderRace, SetId idFrom, SetId idTo ) { if( type == 0 ) { - est = null; - return true; + return null; } var (gender, race) = genderRace.Split(); var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) ); var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) ); - est = new MetaSwap( manips, fromDefault, toDefault ); + var est = new MetaSwap( manips, fromDefault, toDefault ); if( est.SwapApplied.Est.Entry >= 2 ) { - if( !CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var phyb ) ) - { - return false; - } - - if( !CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry, out var sklb ) ) - { - return false; - } - + var phyb = CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); est.ChildSwaps.Add( phyb ); + var sklb = CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); est.ChildSwaps.Add( sklb ); } else if( est.SwapAppliedIsDefault ) { - est = null; + return null; } - return true; + return est; } public static int GetStableHashCode( this string str ) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index eae8a60b..66ceb930 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Lumina.Excel.GeneratedSheets; +using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -112,40 +113,39 @@ public class ItemSwapContainer LoadMod( null, null ); } - public Item[] LoadEquipment( Item from, Item to ) + private Func< Utf8GamePath, FullPath > PathResolver( ModCollection? collection ) + => collection != null + ? p => collection.ResolvePath( p ) ?? new FullPath( p ) + : p => ModRedirections.TryGetValue( p, out var path ) ? path : new FullPath( p ); + + private Func< MetaManipulation, MetaManipulation > MetaResolver( ModCollection? collection ) { - try - { - Swaps.Clear(); - var ret = EquipmentSwap.CreateItemSwap( Swaps, ModRedirections, _modManipulations, from, to ); - Loaded = true; - return ret; - } - catch - { - Swaps.Clear(); - Loaded = false; - return Array.Empty< Item >(); - } + var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _modManipulations; + return m => set.TryGetValue( m, out var a ) ? a : m; } - public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to ) + public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null ) { - if( !CustomizationSwap.CreateMdl( ModRedirections, slot, race, from, to, out var mdl ) ) - { - return false; - } + Swaps.Clear(); + Loaded = false; + var ret = EquipmentSwap.CreateItemSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), from, to ); + Loaded = true; + return ret; + } + public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) + { + var pathResolver = PathResolver( collection ); + var mdl = CustomizationSwap.CreateMdl( pathResolver, slot, race, from, to ); var type = slot switch { BodySlot.Hair => EstManipulation.EstType.Hair, BodySlot.Face => EstManipulation.EstType.Face, _ => ( EstManipulation.EstType )0, }; - if( !ItemSwap.CreateEst( ModRedirections, _modManipulations, type, race, from, to, out var est ) ) - { - return false; - } + + var metaResolver = MetaResolver( collection ); + var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to ); Swaps.Add( mdl ); if( est != null ) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 5018f2a7..5e40f9ac 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,3 +1,4 @@ +using System; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -41,25 +42,16 @@ public sealed class MetaSwap : Swap /// /// Create a new MetaSwap from the original meta identifier and the target meta identifier. /// - /// A set of modded meta manipulations to consider. This is not manipulated, but can not be IReadOnly because TryGetValue is not available for that. + /// A function that converts the given manipulation to the modded one. /// The original meta identifier with its default value. /// The target meta identifier with its default value. - public MetaSwap( HashSet< MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo ) + public MetaSwap( Func< MetaManipulation, MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo ) { SwapFrom = manipFrom; SwapToDefault = manipTo; - if( manipulations.TryGetValue( manipTo, out var actual ) ) - { - SwapToModded = actual; - SwapToIsDefault = false; - } - else - { - SwapToModded = manipTo; - SwapToIsDefault = true; - } - + SwapToModded = manipulations( manipTo ); + SwapToIsDefault = manipTo.EntryEquals( SwapToModded ); SwapApplied = SwapFrom.WithEntryOf( SwapToModded ); SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom ); } @@ -117,15 +109,14 @@ public sealed class FileSwap : Swap /// Create a full swap container for a specific file type using a modded redirection set, the actually requested path and the game file it should load instead after the swap. ///
/// The file type. Mdl and Mtrl have special file loading treatment. - /// The set of redirections that need to be considered. + /// A function either returning the path after mod application. /// The path the game is going to request when loading the file. /// The unmodded path to the file the game is supposed to load instead. /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. - public static bool CreateSwap( ResourceType type, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, out FileSwap swap, - string? swapFromPreChange = null ) + public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, string? swapFromPreChange = null ) { - swap = new FileSwap + var swap = new FileSwap { Type = type, FileData = ItemSwap.GenericFile.Invalid, @@ -142,22 +133,22 @@ public sealed class FileSwap : Swap || !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath ) || !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) ) { - return false; + throw new Exception( $"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"." ); } - swap.SwapToModded = redirections.TryGetValue( swap.SwapToRequestPath, out var p ) ? p : new FullPath( swap.SwapToRequestPath ); + swap.SwapToModded = redirections( swap.SwapToRequestPath ); swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && Dalamud.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); swap.FileData = type switch { - ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, - ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, - ResourceType.Avfx => ItemSwap.LoadAvfx( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, - _ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid, + ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), + ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), + ResourceType.Avfx => ItemSwap.LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), + _ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), }; - return swap.SwapToModdedExists; + return swap; } @@ -168,17 +159,14 @@ public sealed class FileSwap : Swap /// The in- and output path for a file /// Will be set to true if was changed. /// Will be updated. - public static bool CreateShaRedirection( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap ) + public static bool CreateShaRedirection( Func< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap ) { var oldFilename = Path.GetFileName( path ); var hash = SHA256.HashData( swap.FileData.Write() ); var name = $"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}"; var newPath = path.Replace( oldFilename, name ); - if( !CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString(), out var newSwap ) ) - { - return false; - } + var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString()); path = newPath; dataWasChanged = true; diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 2e59b540..124908f6 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -102,12 +102,13 @@ public class ItemSwapWindow : IDisposable private int _sourceId = 0; private Exception? _loadException = null; - private string _newModName = string.Empty; - private string _newGroupName = "Swaps"; - private string _newOptionName = string.Empty; - private IModGroup? _selectedGroup = null; - private bool _subModValid = false; - private bool _useFileSwaps = true; + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private IModGroup? _selectedGroup = null; + private bool _subModValid = false; + private bool _useFileSwaps = true; + private bool _useCurrentCollection = false; private Item[]? _affectedItems; @@ -157,21 +158,21 @@ public class ItemSwapWindow : IDisposable var values = _selectors[ _lastTab ]; if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) { - _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2 ); + _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId ); + _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); + _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Weapon: break; } @@ -180,6 +181,8 @@ public class ItemSwapWindow : IDisposable { Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); _loadException = e; + _affectedItems = null; + _swapData.Clear(); } _dirty = false; From 070d73a5a13a3b1a782789ff563061582a0d6f31 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jan 2023 23:30:22 +0100 Subject: [PATCH 0662/2451] Fix affected item warning appearing on single item. --- Penumbra/UI/Classes/ItemSwapWindow.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 124908f6..794243a4 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -158,21 +158,26 @@ public class ItemSwapWindow : IDisposable var values = _selectors[ _lastTab ]; if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) { - _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); break; case SwapType.Weapon: break; } @@ -395,17 +400,18 @@ public class ItemSwapWindow : IDisposable ImGui.TableNextColumn(); _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - if( _affectedItems is { Length: > 0 } ) + if( _affectedItems is { Length: > 1 } ) { ImGui.SameLine(); ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); if( ImGui.IsItemHovered() ) { - ImGui.SetTooltip( string.Join( '\n', _affectedItems.Select( i => i.Name.ToDalamudString().TextValue ) ) ); + ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) ) + .Select( i => i.Name.ToDalamudString().TextValue ) ) ); } } } - + private void DrawHairSwap() { using var tab = DrawTab( SwapType.Hair ); From bc3a55eded5fcbfe67f101904f3722486896e2b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jan 2023 23:32:54 +0100 Subject: [PATCH 0663/2451] Fix issues with accessories and with gender-locked gear. --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 44 ++++++++++++++++++++----- Penumbra/Mods/ItemSwap/ItemSwap.cs | 9 +++++ Penumbra/Mods/ItemSwap/Swaps.cs | 18 +++++----- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3bdc99a9..49e8ae66 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -34,7 +34,7 @@ public static class EquipmentSwap swaps.Add( eqp ); } - var gmp = CreateGmp( manips, slotFrom, idFrom, idTo); + var gmp = CreateGmp( manips, slotFrom, idFrom, idTo ); if( gmp != null ) { swaps.Add( gmp ); @@ -52,9 +52,19 @@ public static class EquipmentSwap _ => ( EstManipulation.EstType )0, }; + var skipFemale = false; + var skipMale = false; var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotFrom ), variantTo ).MaterialId; foreach( var gr in Enum.GetValues< GenderRace >() ) { + switch( gr.Split().Item1 ) + { + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; + case Gender.FemaleNpc when skipFemale: continue; + } + if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 ) { continue; @@ -66,10 +76,26 @@ public static class EquipmentSwap swaps.Add( est ); } - var eqdp = CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo ); - if( eqdp != null ) + try { - swaps.Add( eqdp ); + var eqdp = CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo ); + if( eqdp != null ) + { + swaps.Add( eqdp ); + } + } + catch( ItemSwap.MissingFileException e ) + { + switch( gr ) + { + case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: + skipMale = true; + continue; + case GenderRace.MidlanderFemale when e.Type == ResourceType.Mdl: + skipFemale = true; + continue; + default: throw; + } } } @@ -79,7 +105,6 @@ public static class EquipmentSwap swaps.Add( imc ); } - return affectedItems; } @@ -106,8 +131,9 @@ public static class EquipmentSwap public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) { - var mdlPathFrom = GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); - var mdlPathTo = GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); + var accessory = slot.IsAccessory(); + var mdlPathFrom = accessory ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slot ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); + var mdlPathTo = accessory ? GamePaths.Accessory.Mdl.Path( idTo, gr, slot ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) @@ -292,7 +318,7 @@ public static class EquipmentSwap } var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); - newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true ); + newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" ); if( newPath != path ) { texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; @@ -311,7 +337,7 @@ public static class EquipmentSwap public static FileSwap CreateAtex( Func< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged ) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}", true ); + filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}" ); dataWasChanged = true; return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, oldPath ); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index d8e8809a..68812674 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -23,6 +23,15 @@ public static class ItemSwap public class IdUnavailableException : Exception { } + public class MissingFileException : Exception + { + public readonly ResourceType Type; + + public MissingFileException( ResourceType type, object path ) + : base($"Could not load {type} File Data for \"{path}\".") + => Type = type; + } + private static bool LoadFile( FullPath path, out byte[] data ) { if( path.FullName.Length > 0 ) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 5e40f9ac..332aa6ac 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using Penumbra.GameData.Enums; +using static Penumbra.Mods.ItemSwap.ItemSwap; namespace Penumbra.Mods.ItemSwap; @@ -63,7 +64,7 @@ public sealed class FileSwap : Swap public ResourceType Type; /// The binary or parsed data of the file at SwapToModded. - public IWritable FileData = ItemSwap.GenericFile.Invalid; + public IWritable FileData = GenericFile.Invalid; /// The path that would be requested without manipulated parent files. public string SwapFromPreChangePath = string.Empty; @@ -114,12 +115,13 @@ public sealed class FileSwap : Swap /// The unmodded path to the file the game is supposed to load instead. /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. - public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, string? swapFromPreChange = null ) + public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, + string? swapFromPreChange = null ) { var swap = new FileSwap { Type = type, - FileData = ItemSwap.GenericFile.Invalid, + FileData = GenericFile.Invalid, DataWasChanged = false, SwapFromPreChangePath = swapFromPreChange ?? swapFromRequest, SwapFromChanged = swapFromPreChange != swapFromRequest, @@ -142,10 +144,10 @@ public sealed class FileSwap : Swap swap.FileData = type switch { - ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), - ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), - ResourceType.Avfx => ItemSwap.LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), - _ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : throw new Exception( $"Could not load file data for {swap.SwapToModded}." ), + ResourceType.Mdl => LoadMdl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), + ResourceType.Mtrl => LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), + ResourceType.Avfx => LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), + _ => LoadFile( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), }; return swap; @@ -166,7 +168,7 @@ public sealed class FileSwap : Swap var name = $"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}"; var newPath = path.Replace( oldFilename, name ); - var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString()); + var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString() ); path = newPath; dataWasChanged = true; From fbb8f48e49668d737361b5bb3140527b11ec9e16 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Jan 2023 00:15:34 +0100 Subject: [PATCH 0664/2451] Add toggle to use entire current collection for item swap. --- Penumbra/UI/Classes/ItemSwapWindow.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 794243a4..8dbab3e8 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -312,6 +312,11 @@ public class ItemSwapWindow : IDisposable CreateMod(); } + ImGui.SameLine(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); + ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); + ImGuiUtil.HoverTooltip( "Instead of writing every single non-default file to the newly created mod or option,\n" + + "even those available from game files, use File Swaps to default game files where possible." ); ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) @@ -338,10 +343,10 @@ public class ItemSwapWindow : IDisposable } ImGui.SameLine(); - var newPos = new Vector2( ImGui.GetCursorPosX() + 10 * ImGuiHelpers.GlobalScale, ImGui.GetCursorPosY() - ( ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); - ImGui.SetCursorPos( newPos ); - ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); - ImGuiUtil.HoverTooltip( "Use File Swaps." ); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); + _dirty |= ImGui.Checkbox( "Use Entire Collection", ref _useCurrentCollection ); + ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" + + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance." ); } private void DrawSwapBar() From 047f14a2883360ee256e82411766fbcfd309c057 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Jan 2023 00:20:39 +0100 Subject: [PATCH 0665/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 38446319..7eb71846 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -29,10 +29,20 @@ public partial class ConfigWindow Add6_0_2( ret ); Add6_0_5( ret ); Add6_1_0( ret ); + Add6_1_1( ret ); return ret; } + private static void Add6_1_1( Changelog log ) + => log.NextVersion( "Version 0.6.1.1" ) + .RegisterEntry( "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod." ) + .RegisterEntry( "Fix using equipment paths for accessory swaps and thus accessory swaps not working at all" ) + .RegisterEntry( "Fix issues with swaps with gender-locked gear where the models for the other gender do not exist." ) + .RegisterEntry( "Fix swapping universal hairstyles for midlanders breaking them for other races." ) + .RegisterEntry( "Add some actual error messages on failure to create item swaps." ) + .RegisterEntry( "Fix warnings about more than one affected item appearing for single items." ); + private static void Add6_1_0( Changelog log ) => log.NextVersion( "Version 0.6.1.0 (Happy New Year! Edition)" ) .RegisterEntry( "Added a prototype for Item Swapping." ) From 1268344d04dff8f9e74c073cda528624b609a7b1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 1 Jan 2023 23:22:55 +0000 Subject: [PATCH 0666/2451] [CI] Updating repo.json for refs/tags/0.6.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f628b801..5162aa8e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.1.0", - "TestingAssemblyVersion": "0.6.1.0", + "AssemblyVersion": "0.6.1.1", + "TestingAssemblyVersion": "0.6.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 20e6baee0b8a38340f342b4efb5008bc7ddf9d22 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Jan 2023 17:20:56 +0100 Subject: [PATCH 0667/2451] Fix a problem when incorporating deduplicated meta files simultaneously over multiple options. --- Penumbra/Mods/Mod.BasePath.cs | 13 +++++++++++-- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 851ed943..3a849a26 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Linq; @@ -80,12 +81,20 @@ public partial class Mod // Deletes the source files if delete is true. private void IncorporateAllMetaChanges( bool delete ) { - var changes = false; + var changes = false; + List< string > deleteList = new(); foreach( var subMod in AllSubMods.OfType< SubMod >() ) { - changes |= subMod.IncorporateMetaChanges( ModPath, delete ); + var (localChanges, localDeleteList) = subMod.IncorporateMetaChanges( ModPath, false ); + changes |= localChanges; + if( delete ) + { + deleteList.AddRange( localDeleteList ); + } } + SubMod.DeleteDeleteList( deleteList, delete ); + if( changes ) { SaveAllGroups(); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index a7b4981f..80c35d0f 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -182,7 +182,7 @@ public partial class Mod // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. // If delete is true, the files are deleted afterwards. - public bool IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) { var deleteList = new List< string >(); var oldSize = ManipulationData.Count; @@ -227,6 +227,17 @@ public partial class Mod } } + DeleteDeleteList( deleteList, delete ); + return ( oldSize < ManipulationData.Count, deleteList ); + } + + internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) + { + if( !delete ) + { + return; + } + foreach( var file in deleteList ) { try @@ -238,8 +249,6 @@ public partial class Mod Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); } } - - return oldSize < ManipulationData.Count; } public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) From 36c77034a4ee4e37a95488791c425456a1098944 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 2 Jan 2023 16:24:38 +0000 Subject: [PATCH 0668/2451] [CI] Updating repo.json for refs/tags/0.6.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5162aa8e..505d7ee4 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.1.1", - "TestingAssemblyVersion": "0.6.1.1", + "AssemblyVersion": "0.6.1.2", + "TestingAssemblyVersion": "0.6.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8df4bb0781b696e1dc2c4a36e4a1272ff49ef606 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 12:52:21 +0100 Subject: [PATCH 0669/2451] Add some Additional Information to Mdl display, discard some padding when reading mdl files. --- Penumbra.GameData/Files/MdlFile.Write.cs | 442 ++++++++---------- Penumbra.GameData/Files/MdlFile.cs | 7 +- .../UI/Classes/ModEditWindow.Materials.cs | 4 +- Penumbra/UI/Classes/ModEditWindow.Models.cs | 103 ++++ 4 files changed, 315 insertions(+), 241 deletions(-) diff --git a/Penumbra.GameData/Files/MdlFile.Write.cs b/Penumbra.GameData/Files/MdlFile.Write.cs index b4017c52..7db29954 100644 --- a/Penumbra.GameData/Files/MdlFile.Write.cs +++ b/Penumbra.GameData/Files/MdlFile.Write.cs @@ -9,309 +9,277 @@ namespace Penumbra.GameData.Files; public partial class MdlFile { - private static uint Write( BinaryWriter w, string s, long basePos ) + private static uint Write(BinaryWriter w, string s, long basePos) { var currentPos = w.BaseStream.Position; - w.Write( Encoding.UTF8.GetBytes( s ) ); - w.Write( ( byte )0 ); - return ( uint )( currentPos - basePos ); + w.Write(Encoding.UTF8.GetBytes(s)); + w.Write((byte)0); + return (uint)(currentPos - basePos); } - private List< uint > WriteStrings( BinaryWriter w ) + private List WriteStrings(BinaryWriter w) { - var startPos = ( int )w.BaseStream.Position; + var startPos = (int)w.BaseStream.Position; var basePos = startPos + 8; - var count = ( ushort )( Attributes.Length + Bones.Length + Materials.Length + Shapes.Length ); + var count = (ushort)(Attributes.Length + Bones.Length + Materials.Length + Shapes.Length); - w.Write( count ); - w.Seek( basePos, SeekOrigin.Begin ); - var ret = Attributes.Concat( Bones ) - .Concat( Materials ) - .Concat( Shapes.Select( s => s.ShapeName ) ) - .Select( attribute => Write( w, attribute, basePos ) ).ToList(); + w.Write(count); + w.Seek(basePos, SeekOrigin.Begin); + var ret = Attributes.Concat(Bones) + .Concat(Materials) + .Concat(Shapes.Select(s => s.ShapeName)) + .Select(attribute => Write(w, attribute, basePos)).ToList(); - w.Write( ( ushort )0 ); // Seems to always have two additional null-bytes, not padding. - var size = ( int )w.BaseStream.Position - basePos; - w.Seek( startPos + 4, SeekOrigin.Begin ); - w.Write( ( uint )size ); - w.Seek( basePos + size, SeekOrigin.Begin ); + var padding = (w.BaseStream.Position & 0b111) > 0 ? (w.BaseStream.Position & ~0b111) + 8 : w.BaseStream.Position; + for (var i = w.BaseStream.Position; i < padding; ++i) + w.Write((byte)0); + var size = (int)w.BaseStream.Position - basePos; + w.Seek(startPos + 4, SeekOrigin.Begin); + w.Write((uint)size); + w.Seek(basePos + size, SeekOrigin.Begin); return ret; } - private void WriteModelFileHeader( BinaryWriter w, uint runtimeSize ) + private void WriteModelFileHeader(BinaryWriter w, uint runtimeSize) { - w.Write( Version ); - w.Write( StackSize ); - w.Write( runtimeSize ); - w.Write( ( ushort )VertexDeclarations.Length ); - w.Write( ( ushort )Materials.Length ); - w.Write( VertexOffset[ 0 ] > 0 ? VertexOffset[ 0 ] + runtimeSize : 0u ); - w.Write( VertexOffset[ 1 ] > 0 ? VertexOffset[ 1 ] + runtimeSize : 0u ); - w.Write( VertexOffset[ 2 ] > 0 ? VertexOffset[ 2 ] + runtimeSize : 0u ); - w.Write( IndexOffset[ 0 ] > 0 ? IndexOffset[ 0 ] + runtimeSize : 0u ); - w.Write( IndexOffset[ 1 ] > 0 ? IndexOffset[ 1 ] + runtimeSize : 0u ); - w.Write( IndexOffset[ 2 ] > 0 ? IndexOffset[ 2 ] + runtimeSize : 0u ); - w.Write( VertexBufferSize[ 0 ] ); - w.Write( VertexBufferSize[ 1 ] ); - w.Write( VertexBufferSize[ 2 ] ); - w.Write( IndexBufferSize[ 0 ] ); - w.Write( IndexBufferSize[ 1 ] ); - w.Write( IndexBufferSize[ 2 ] ); - w.Write( LodCount ); - w.Write( EnableIndexBufferStreaming ); - w.Write( EnableEdgeGeometry ); - w.Write( ( byte )0 ); // Padding + w.Write(Version); + w.Write(StackSize); + w.Write(runtimeSize); + w.Write((ushort)VertexDeclarations.Length); + w.Write((ushort)Materials.Length); + w.Write(VertexOffset[0] > 0 ? VertexOffset[0] + runtimeSize : 0u); + w.Write(VertexOffset[1] > 0 ? VertexOffset[1] + runtimeSize : 0u); + w.Write(VertexOffset[2] > 0 ? VertexOffset[2] + runtimeSize : 0u); + w.Write(IndexOffset[0] > 0 ? IndexOffset[0] + runtimeSize : 0u); + w.Write(IndexOffset[1] > 0 ? IndexOffset[1] + runtimeSize : 0u); + w.Write(IndexOffset[2] > 0 ? IndexOffset[2] + runtimeSize : 0u); + w.Write(VertexBufferSize[0]); + w.Write(VertexBufferSize[1]); + w.Write(VertexBufferSize[2]); + w.Write(IndexBufferSize[0]); + w.Write(IndexBufferSize[1]); + w.Write(IndexBufferSize[2]); + w.Write(LodCount); + w.Write(EnableIndexBufferStreaming); + w.Write(EnableEdgeGeometry); + w.Write((byte)0); // Padding } - private void WriteModelHeader( BinaryWriter w ) + private void WriteModelHeader(BinaryWriter w) { - w.Write( Radius ); - w.Write( ( ushort )Meshes.Length ); - w.Write( ( ushort )Attributes.Length ); - w.Write( ( ushort )SubMeshes.Length ); - w.Write( ( ushort )Materials.Length ); - w.Write( ( ushort )Bones.Length ); - w.Write( ( ushort )BoneTables.Length ); - w.Write( ( ushort )Shapes.Length ); - w.Write( ( ushort )ShapeMeshes.Length ); - w.Write( ( ushort )ShapeValues.Length ); - w.Write( LodCount ); - w.Write( ( byte )Flags1 ); - w.Write( ( ushort )ElementIds.Length ); - w.Write( ( byte )TerrainShadowMeshes.Length ); - w.Write( ( byte )Flags2 ); - w.Write( ModelClipOutDistance ); - w.Write( ShadowClipOutDistance ); - w.Write( Unknown4 ); - w.Write( ( ushort )TerrainShadowSubMeshes.Length ); - w.Write( Unknown5 ); - w.Write( BgChangeMaterialIndex ); - w.Write( BgCrestChangeMaterialIndex ); - w.Write( Unknown6 ); - w.Write( Unknown7 ); - w.Write( Unknown8 ); - w.Write( Unknown9 ); - w.Write( ( uint )0 ); // 6 byte padding - w.Write( ( ushort )0 ); + w.Write(Radius); + w.Write((ushort)Meshes.Length); + w.Write((ushort)Attributes.Length); + w.Write((ushort)SubMeshes.Length); + w.Write((ushort)Materials.Length); + w.Write((ushort)Bones.Length); + w.Write((ushort)BoneTables.Length); + w.Write((ushort)Shapes.Length); + w.Write((ushort)ShapeMeshes.Length); + w.Write((ushort)ShapeValues.Length); + w.Write(LodCount); + w.Write((byte)Flags1); + w.Write((ushort)ElementIds.Length); + w.Write((byte)TerrainShadowMeshes.Length); + w.Write((byte)Flags2); + w.Write(ModelClipOutDistance); + w.Write(ShadowClipOutDistance); + w.Write(Unknown4); + w.Write((ushort)TerrainShadowSubMeshes.Length); + w.Write(Unknown5); + w.Write(BgChangeMaterialIndex); + w.Write(BgCrestChangeMaterialIndex); + w.Write(Unknown6); + w.Write(Unknown7); + w.Write(Unknown8); + w.Write(Unknown9); + w.Write((uint)0); // 6 byte padding + w.Write((ushort)0); } - private static void Write( BinaryWriter w, in MdlStructs.VertexElement vertex ) + private static void Write(BinaryWriter w, in MdlStructs.VertexElement vertex) { - w.Write( vertex.Stream ); - w.Write( vertex.Offset ); - w.Write( vertex.Type ); - w.Write( vertex.Usage ); - w.Write( vertex.UsageIndex ); - w.Write( ( ushort )0 ); // 3 byte padding - w.Write( ( byte )0 ); + w.Write(vertex.Stream); + w.Write(vertex.Offset); + w.Write(vertex.Type); + w.Write(vertex.Usage); + w.Write(vertex.UsageIndex); + w.Write((ushort)0); // 3 byte padding + w.Write((byte)0); } - private static void Write( BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl ) + private static void Write(BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl) { - foreach( var vertex in vertexDecl.VertexElements ) + foreach (var vertex in vertexDecl.VertexElements) + Write(w, vertex); + + Write(w, new MdlStructs.VertexElement() { Stream = 255 }); + w.Seek((int)(NumVertices - 1 - vertexDecl.VertexElements.Length) * 8, SeekOrigin.Current); + } + + private static void Write(BinaryWriter w, in MdlStructs.ElementIdStruct elementId) + { + w.Write(elementId.ElementId); + w.Write(elementId.ParentBoneName); + w.Write(elementId.Translate[0]); + w.Write(elementId.Translate[1]); + w.Write(elementId.Translate[2]); + w.Write(elementId.Rotate[0]); + w.Write(elementId.Rotate[1]); + w.Write(elementId.Rotate[2]); + } + + private static unsafe void Write(BinaryWriter w, in T data) where T : unmanaged + { + fixed (T* ptr = &data) { - Write( w, vertex ); - } - - Write( w, new MdlStructs.VertexElement() { Stream = 255 } ); - w.Seek( ( int )( NumVertices - 1 - vertexDecl.VertexElements.Length ) * 8, SeekOrigin.Current ); - } - - private static void Write( BinaryWriter w, in MdlStructs.ElementIdStruct elementId ) - { - w.Write( elementId.ElementId ); - w.Write( elementId.ParentBoneName ); - w.Write( elementId.Translate[ 0 ] ); - w.Write( elementId.Translate[ 1 ] ); - w.Write( elementId.Translate[ 2 ] ); - w.Write( elementId.Rotate[ 0 ] ); - w.Write( elementId.Rotate[ 1 ] ); - w.Write( elementId.Rotate[ 2 ] ); - } - - private static unsafe void Write< T >( BinaryWriter w, in T data ) where T : unmanaged - { - fixed( T* ptr = &data ) - { - var bytePtr = ( byte* )ptr; - var size = sizeof( T ); - var span = new ReadOnlySpan< byte >( bytePtr, size ); - w.Write( span ); + var bytePtr = (byte*)ptr; + var size = sizeof(T); + var span = new ReadOnlySpan(bytePtr, size); + w.Write(span); } } - private static void Write( BinaryWriter w, MdlStructs.MeshStruct mesh ) + private static void Write(BinaryWriter w, MdlStructs.MeshStruct mesh) { - w.Write( mesh.VertexCount ); - w.Write( ( ushort )0 ); // padding - w.Write( mesh.IndexCount ); - w.Write( mesh.MaterialIndex ); - w.Write( mesh.SubMeshIndex ); - w.Write( mesh.SubMeshCount ); - w.Write( mesh.BoneTableIndex ); - w.Write( mesh.StartIndex ); - w.Write( mesh.VertexBufferOffset[ 0 ] ); - w.Write( mesh.VertexBufferOffset[ 1 ] ); - w.Write( mesh.VertexBufferOffset[ 2 ] ); - w.Write( mesh.VertexBufferStride[ 0 ] ); - w.Write( mesh.VertexBufferStride[ 1 ] ); - w.Write( mesh.VertexBufferStride[ 2 ] ); - w.Write( mesh.VertexStreamCount ); + w.Write(mesh.VertexCount); + w.Write((ushort)0); // padding + w.Write(mesh.IndexCount); + w.Write(mesh.MaterialIndex); + w.Write(mesh.SubMeshIndex); + w.Write(mesh.SubMeshCount); + w.Write(mesh.BoneTableIndex); + w.Write(mesh.StartIndex); + w.Write(mesh.VertexBufferOffset[0]); + w.Write(mesh.VertexBufferOffset[1]); + w.Write(mesh.VertexBufferOffset[2]); + w.Write(mesh.VertexBufferStride[0]); + w.Write(mesh.VertexBufferStride[1]); + w.Write(mesh.VertexBufferStride[2]); + w.Write(mesh.VertexStreamCount); } - private static void Write( BinaryWriter w, MdlStructs.BoneTableStruct bone ) + private static void Write(BinaryWriter w, MdlStructs.BoneTableStruct bone) { - foreach( var index in bone.BoneIndex ) - { - w.Write( index ); - } + foreach (var index in bone.BoneIndex) + w.Write(index); - w.Write( bone.BoneCount ); - w.Write( ( ushort )0 ); // 3 bytes padding - w.Write( ( byte )0 ); + w.Write(bone.BoneCount); + w.Write((ushort)0); // 3 bytes padding + w.Write((byte)0); } - private void Write( BinaryWriter w, int shapeIdx, IReadOnlyList< uint > offsets ) + private void Write(BinaryWriter w, int shapeIdx, IReadOnlyList offsets) { - var shape = Shapes[ shapeIdx ]; - var offset = offsets[ Attributes.Length + Bones.Length + Materials.Length + shapeIdx ]; - w.Write( offset ); - w.Write( shape.ShapeMeshStartIndex[ 0 ] ); - w.Write( shape.ShapeMeshStartIndex[ 1 ] ); - w.Write( shape.ShapeMeshStartIndex[ 2 ] ); - w.Write( shape.ShapeMeshCount[ 0 ] ); - w.Write( shape.ShapeMeshCount[ 1 ] ); - w.Write( shape.ShapeMeshCount[ 2 ] ); + var shape = Shapes[shapeIdx]; + var offset = offsets[Attributes.Length + Bones.Length + Materials.Length + shapeIdx]; + w.Write(offset); + w.Write(shape.ShapeMeshStartIndex[0]); + w.Write(shape.ShapeMeshStartIndex[1]); + w.Write(shape.ShapeMeshStartIndex[2]); + w.Write(shape.ShapeMeshCount[0]); + w.Write(shape.ShapeMeshCount[1]); + w.Write(shape.ShapeMeshCount[2]); } - private static void Write( BinaryWriter w, MdlStructs.BoundingBoxStruct box ) + private static void Write(BinaryWriter w, MdlStructs.BoundingBoxStruct box) { - w.Write( box.Min[ 0 ] ); - w.Write( box.Min[ 1 ] ); - w.Write( box.Min[ 2 ] ); - w.Write( box.Min[ 3 ] ); - w.Write( box.Max[ 0 ] ); - w.Write( box.Max[ 1 ] ); - w.Write( box.Max[ 2 ] ); - w.Write( box.Max[ 3 ] ); + w.Write(box.Min[0]); + w.Write(box.Min[1]); + w.Write(box.Min[2]); + w.Write(box.Min[3]); + w.Write(box.Max[0]); + w.Write(box.Max[1]); + w.Write(box.Max[2]); + w.Write(box.Max[3]); } public byte[] Write() { using var stream = new MemoryStream(); - using( var w = new BinaryWriter( stream ) ) + using (var w = new BinaryWriter(stream)) { // Skip and write this later when we actually know it. - w.Seek( ( int )FileHeaderSize, SeekOrigin.Begin ); + w.Seek((int)FileHeaderSize, SeekOrigin.Begin); - foreach( var vertexDecl in VertexDeclarations ) - { - Write( w, vertexDecl ); - } + foreach (var vertexDecl in VertexDeclarations) + Write(w, vertexDecl); - var offsets = WriteStrings( w ); - WriteModelHeader( w ); + var offsets = WriteStrings(w); + WriteModelHeader(w); - foreach( var elementId in ElementIds ) - { - Write( w, elementId ); - } + foreach (var elementId in ElementIds) + Write(w, elementId); - foreach( var lod in Lods ) - { - Write( w, lod ); - } + foreach (var lod in Lods) + Write(w, lod); - if( Flags2.HasFlag( MdlStructs.ModelFlags2.ExtraLodEnabled ) ) - { - foreach( var extraLod in ExtraLods ) - { - Write( w, extraLod ); - } - } + if (Flags2.HasFlag(MdlStructs.ModelFlags2.ExtraLodEnabled)) + foreach (var extraLod in ExtraLods) + Write(w, extraLod); - foreach( var mesh in Meshes ) - { - Write( w, mesh ); - } + foreach (var mesh in Meshes) + Write(w, mesh); - for( var i = 0; i < Attributes.Length; ++i ) - { - w.Write( offsets[ i ] ); - } + for (var i = 0; i < Attributes.Length; ++i) + w.Write(offsets[i]); - foreach( var terrainShadowMesh in TerrainShadowMeshes ) - { - Write( w, terrainShadowMesh ); - } + foreach (var terrainShadowMesh in TerrainShadowMeshes) + Write(w, terrainShadowMesh); - foreach( var subMesh in SubMeshes ) - { - Write( w, subMesh ); - } + foreach (var subMesh in SubMeshes) + Write(w, subMesh); - foreach( var terrainShadowSubMesh in TerrainShadowSubMeshes ) - { - Write( w, terrainShadowSubMesh ); - } + foreach (var terrainShadowSubMesh in TerrainShadowSubMeshes) + Write(w, terrainShadowSubMesh); - for( var i = 0; i < Materials.Length; ++i ) - { - w.Write( offsets[ Attributes.Length + Bones.Length + i ] ); - } + for (var i = 0; i < Materials.Length; ++i) + w.Write(offsets[Attributes.Length + Bones.Length + i]); - for( var i = 0; i < Bones.Length; ++i ) - { - w.Write( offsets[ Attributes.Length + i ] ); - } + for (var i = 0; i < Bones.Length; ++i) + w.Write(offsets[Attributes.Length + i]); - foreach( var boneTable in BoneTables ) - { - Write( w, boneTable ); - } + foreach (var boneTable in BoneTables) + Write(w, boneTable); - for( var i = 0; i < Shapes.Length; ++i ) - { - Write( w, i, offsets ); - } + for (var i = 0; i < Shapes.Length; ++i) + Write(w, i, offsets); - foreach( var shapeMesh in ShapeMeshes ) - { - Write( w, shapeMesh ); - } + foreach (var shapeMesh in ShapeMeshes) + Write(w, shapeMesh); - foreach( var shapeValue in ShapeValues ) - { - Write( w, shapeValue ); - } + foreach (var shapeValue in ShapeValues) + Write(w, shapeValue); - w.Write( SubMeshBoneMap.Length * 2 ); - foreach( var bone in SubMeshBoneMap ) - { - w.Write( bone ); - } + w.Write(SubMeshBoneMap.Length * 2); + foreach (var bone in SubMeshBoneMap) + w.Write(bone); - w.Write( ( byte )0 ); // number of padding bytes, which is 0 for us. + var pos = w.BaseStream.Position + 1; + var padding = (byte) (pos & 0b111); + if (padding > 0) + padding = (byte) (8 - padding); + w.Write(padding); + for (var i = 0; i < padding; ++i) + w.Write((byte) (0xDEADBEEFF00DCAFEu >> (8 * (7 - i)))); - Write( w, BoundingBoxes ); - Write( w, ModelBoundingBoxes ); - Write( w, WaterBoundingBoxes ); - Write( w, VerticalFogBoundingBoxes ); - foreach( var box in BoneBoundingBoxes ) - { - Write( w, box ); - } - - var totalSize = w.BaseStream.Position; - var runtimeSize = ( uint )( totalSize - StackSize - FileHeaderSize ); - w.Write( RemainingData ); + Write(w, BoundingBoxes); + Write(w, ModelBoundingBoxes); + Write(w, WaterBoundingBoxes); + Write(w, VerticalFogBoundingBoxes); + foreach (var box in BoneBoundingBoxes) + Write(w, box); + + var totalSize = w.BaseStream.Position; + var runtimeSize = (uint)(totalSize - StackSize - FileHeaderSize); + w.Write(RemainingData); // Write header data. - w.Seek( 0, SeekOrigin.Begin ); - WriteModelFileHeader( w, runtimeSize ); + w.Seek(0, SeekOrigin.Begin); + WriteModelFileHeader(w, runtimeSize); } return stream.ToArray(); } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index a7d65ee8..6cde07f5 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -83,8 +83,8 @@ public partial class MdlFile : IWritable public Shape[] Shapes; // Raw, unparsed data. - public byte[] RemainingData; - + public byte[] RemainingData; + public bool Valid { get; } public MdlFile(byte[] data) @@ -181,6 +181,9 @@ public partial class MdlFile : IWritable for (var i = 0; i < modelHeader.BoneCount; i++) BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r); + var runtimePadding = header.RuntimeSize + FileHeaderSize + header.StackSize - r.BaseStream.Position; + if (runtimePadding > 0) + r.ReadBytes((int)runtimePadding); RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position)); Valid = true; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 3cc27aac..54330ecb 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -215,10 +215,10 @@ public partial class ModEditWindow if( file.AdditionalData.Length > 0 ) { - using var t = ImRaii.TreeNode( "Additional Data" ); + using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); if( t ) { - ImRaii.TreeNode( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ), ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Models.cs b/Penumbra/UI/Classes/ModEditWindow.Models.cs index 5b6ad685..73f2a7dc 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Models.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Models.cs @@ -1,7 +1,10 @@ using ImGuiNET; +using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; +using System.Globalization; +using System.Linq; namespace Penumbra.UI.Classes; @@ -26,6 +29,106 @@ public partial class ModEditWindow } } + ret |= DrawOtherModelDetails( file, disabled ); + return !disabled && ret; } + + private static bool DrawOtherModelDetails( MdlFile file, bool _ ) + { + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return false; + } + + using( var table = ImRaii.Table( "##data", 2, ImGuiTableFlags.SizingFixedFit ) ) + { + if( table ) + { + ImGuiUtil.DrawTableColumn( "Version" ); + ImGuiUtil.DrawTableColumn( file.Version.ToString() ); + ImGuiUtil.DrawTableColumn( "Radius" ); + ImGuiUtil.DrawTableColumn( file.Radius.ToString( CultureInfo.InvariantCulture ) ); + ImGuiUtil.DrawTableColumn( "Model Clip Out Distance" ); + ImGuiUtil.DrawTableColumn( file.ModelClipOutDistance.ToString( CultureInfo.InvariantCulture ) ); + ImGuiUtil.DrawTableColumn( "Shadow Clip Out Distance" ); + ImGuiUtil.DrawTableColumn( file.ShadowClipOutDistance.ToString( CultureInfo.InvariantCulture ) ); + ImGuiUtil.DrawTableColumn( "LOD Count" ); + ImGuiUtil.DrawTableColumn( file.LodCount.ToString() ); + ImGuiUtil.DrawTableColumn( "Enable Index Buffer Streaming" ); + ImGuiUtil.DrawTableColumn( file.EnableIndexBufferStreaming.ToString() ); + ImGuiUtil.DrawTableColumn( "Enable Edge Geometry" ); + ImGuiUtil.DrawTableColumn( file.EnableEdgeGeometry.ToString() ); + ImGuiUtil.DrawTableColumn( "Flags 1" ); + ImGuiUtil.DrawTableColumn( file.Flags1.ToString() ); + ImGuiUtil.DrawTableColumn( "Flags 2" ); + ImGuiUtil.DrawTableColumn( file.Flags2.ToString() ); + ImGuiUtil.DrawTableColumn( "Vertex Declarations" ); + ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Bone Bounding Boxes" ); + ImGuiUtil.DrawTableColumn( file.BoneBoundingBoxes.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Bone Tables" ); + ImGuiUtil.DrawTableColumn( file.BoneTables.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Element IDs" ); + ImGuiUtil.DrawTableColumn( file.ElementIds.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Extra LoDs" ); + ImGuiUtil.DrawTableColumn( file.ExtraLods.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Meshes" ); + ImGuiUtil.DrawTableColumn( file.Meshes.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Shape Meshes" ); + ImGuiUtil.DrawTableColumn( file.ShapeMeshes.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "LoDs" ); + ImGuiUtil.DrawTableColumn( file.Lods.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Vertex Declarations" ); + ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() ); + ImGuiUtil.DrawTableColumn( "Stack Size" ); + ImGuiUtil.DrawTableColumn( file.StackSize.ToString() ); + } + } + + using( var attributes = ImRaii.TreeNode( "Attributes", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( attributes ) + { + foreach( var attribute in file.Attributes ) + { + ImRaii.TreeNode( attribute, ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + using( var bones = ImRaii.TreeNode( "Bones", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( bones ) + { + foreach( var bone in file.Bones ) + { + ImRaii.TreeNode( bone, ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + using( var shapes = ImRaii.TreeNode( "Shapes", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( shapes ) + { + foreach( var shape in file.Shapes ) + { + ImRaii.TreeNode( shape.ShapeName, ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + if( file.RemainingData.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.RemainingData.Length})###AdditionalData" ); + if( t ) + { + ImGuiUtil.TextWrapped( string.Join( ' ', file.RemainingData.Select( c => $"{c:X2}" ) ) ); + } + } + + return false; + } + } \ No newline at end of file From 2dda954806d65cbd57371caabe12e3c753fc98d2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 17:35:34 +0100 Subject: [PATCH 0670/2451] Fix association of vfx game objects for ID-less objects. --- Penumbra.String | 2 +- .../Resolver/PathResolver.AnimationState.cs | 37 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Penumbra.String b/Penumbra.String index 944e712d..e8fad797 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 944e712d404c22ef4ce945ff35cf08af3f67ca12 +Subproject commit e8fad7976e3a156a9d3f0c7f9b2a03267bd3c772 diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index c53196cc..8c50afa7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -217,11 +217,14 @@ public unsafe partial class PathResolver [FieldOffset( 0x118 )] public uint GameObjectId; + [FieldOffset( 0x11C )] + public byte GameObjectType; + [FieldOffset( 0xD0 )] public ushort TargetCount; [FieldOffset( 0x120 )] - public fixed uint Target[16]; + public fixed ulong Target[16]; } private delegate IntPtr LoadCharacterVfxDelegate( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ); @@ -229,20 +232,33 @@ public unsafe partial class PathResolver [Signature( "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93", DetourName = nameof( LoadCharacterVfxDetour ) )] private readonly Hook< LoadCharacterVfxDelegate > _loadCharacterVfxHook = null!; + private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) + { + var owner = Dalamud.Objects.SearchById( id ); + if( owner == null ) + { + return null; + } + + var idx = ( ( GameObject* )owner.Address )->ObjectIndex; + return Dalamud.Objects[ idx + 1 ]; + } + private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) { var last = _animationLoadData; if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) ) { - var obj = Dalamud.Objects.SearchById( vfxParams->GameObjectId ); - if( obj != null ) + var obj = vfxParams->GameObjectType switch { - _animationLoadData = IdentifyCollection( ( GameObject* )obj.Address, true ); - } - else - { - _animationLoadData = ResolveData.Invalid; - } + 0 => Dalamud.Objects.SearchById( vfxParams->GameObjectId ), + 2 => Dalamud.Objects[ ( int )vfxParams->GameObjectId ], + 4 => GetOwnedObject( vfxParams->GameObjectId ), + _ => null, + }; + _animationLoadData = obj != null + ? IdentifyCollection( ( GameObject* )obj.Address, true ) + : ResolveData.Invalid; } else { @@ -251,8 +267,9 @@ public unsafe partial class PathResolver var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 ); #if DEBUG + var path = new ByteString( vfxPath ); Penumbra.Log.Verbose( - $"Load Character VFX: {new ByteString( vfxPath )} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); + $"Load Character VFX: {path} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); #endif _animationLoadData = last; return ret; From 0b7b63a3a9ae9a3a00801443adbe70f162f04d58 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 17:35:50 +0100 Subject: [PATCH 0671/2451] Do not force avfx files to load synchronously. --- .../Interop/Resolver/PathResolver.Subfiles.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index f94b3551..9e9e3c2d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -19,7 +19,7 @@ public unsafe partial class PathResolver // Materials and avfx do contain their own paths to textures and shader packages or atex respectively. // Those are loaded synchronously. // Thus, we need to ensure the correct files are loaded when a material is loaded. - public class SubfileHelper : IDisposable, IReadOnlyCollection> + public class SubfileHelper : IDisposable, IReadOnlyCollection< KeyValuePair< IntPtr, ResolveData > > { private readonly ResourceLoader _loader; @@ -133,24 +133,27 @@ public unsafe partial class PathResolver // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. - public bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, + public static bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { - ret = 0; switch( fileDescriptor->ResourceHandle->FileType ) { case ResourceType.Mtrl: + // Force isSync = true for this call. I don't really understand why, + // or where the difference even comes from. + // Was called with True on my client and with false on other peoples clients, + // which caused problems. + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); + return true; case ResourceType.Avfx: - break; - default: return false; - } + // Do nothing special right now. + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); + return true; - // Force isSync = true for this call. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. - ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); - return true; + default: + ret = 0; + return false; + } } private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); From 0158ff2074418c83e7b9fdc4619422f9a4a8c1c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 17:36:02 +0100 Subject: [PATCH 0672/2451] Fix typo in color explanations. --- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 7681d09c..61b46a36 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -484,7 +484,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "disabled in all inherited collections." ); + ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections." ); ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), From b3b552235c76af950800320c8f82673026025bdd Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 3 Jan 2023 16:38:57 +0000 Subject: [PATCH 0673/2451] [CI] Updating repo.json for refs/tags/0.6.1.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 505d7ee4..6b4f15ac 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.1.2", - "TestingAssemblyVersion": "0.6.1.2", + "AssemblyVersion": "0.6.1.3", + "TestingAssemblyVersion": "0.6.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6bc0b77ad36ac0e8fff230e16faf4e6cf47acadb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 21:55:04 +0100 Subject: [PATCH 0674/2451] Formatting. --- Penumbra/CommandHandler.cs | 2 -- Penumbra/Penumbra.cs | 4 ---- 2 files changed, 6 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 3ab97d9c..978181ed 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 91c24705..09d3d155 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,12 +23,8 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Meta.Files; using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; From f2997102c7ca3e7761d47e30c7da9deaf7aab676 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jan 2023 21:55:39 +0100 Subject: [PATCH 0675/2451] Timing test. --- Penumbra/Interop/CharacterUtility.List.cs | 4 + .../Interop/Loader/ResourceLoader.Debug.cs | 4 + .../Loader/ResourceLoader.Replacement.cs | 114 ++++++++++-------- .../Resolver/PathResolver.AnimationState.cs | 7 +- .../Resolver/PathResolver.DrawObjectState.cs | 2 + .../Resolver/PathResolver.Identification.cs | 27 +++-- .../Resolver/PathResolver.PathState.cs | 2 + .../Interop/Resolver/PathResolver.Subfiles.cs | 4 + Penumbra/Interop/Resolver/PathResolver.cs | 2 + Penumbra/Penumbra.cs | 5 + Penumbra/TimingManager.cs | 90 ++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 4 + .../UI/ConfigWindow.SettingsTab.General.cs | 3 + Penumbra/UI/ConfigWindow.SettingsTab.cs | 8 ++ Penumbra/UI/ConfigWindow.cs | 3 + 15 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 Penumbra/TimingManager.cs diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index 3cf137e8..38f6e804 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -77,11 +77,13 @@ public unsafe partial class CharacterUtility // Set the currently stored data of this resource to new values. private void SetResourceInternal( IntPtr data, int length ) { + TimingManager.StartTimer( TimingType.SetResource ); if( Ready ) { var resource = Penumbra.CharacterUtility.Address->Resource( GlobalIndex ); resource->SetData( data, length ); } + TimingManager.StopTimer( TimingType.SetResource ); } // Reset the currently stored data of this resource to its default values. @@ -133,6 +135,7 @@ public unsafe partial class CharacterUtility { if( !Disposed ) { + TimingManager.StartTimer( TimingType.SetResource ); var list = List._entries; var wasCurrent = ReferenceEquals( this, list.First?.Value ); list.Remove( this ); @@ -159,6 +162,7 @@ public unsafe partial class CharacterUtility } Disposed = true; + TimingManager.StopTimer( TimingType.SetResource ); } } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 682ead7e..a44ac58c 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -76,6 +76,7 @@ public unsafe partial class ResourceLoader return; } + TimingManager.StartTimer( TimingType.DebugTimes ); // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra. try { @@ -96,6 +97,7 @@ public unsafe partial class ResourceLoader { Penumbra.Log.Error( e.ToString() ); } + TimingManager.StopTimer( TimingType.DebugTimes ); } // Find a key in a StdMap. @@ -202,6 +204,7 @@ public unsafe partial class ResourceLoader // Only used when the Replaced Resources Tab in the Debug tab is open. public void UpdateDebugInfo() { + TimingManager.StartTimer( TimingType.DebugTimes ); for( var i = 0; i < _debugList.Count; ++i ) { var data = _debugList.Values[ i ]; @@ -220,6 +223,7 @@ public unsafe partial class ResourceLoader }; } } + TimingManager.StopTimer( TimingType.DebugTimes ); } // Prevent resource management weirdness. diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 28c8ead9..e2c83250 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -81,34 +81,44 @@ public unsafe partial class ResourceLoader internal ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { + TimingManager.StartTimer( TimingType.GetResourceHandler ); + ResourceHandle* ret = null; if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { Penumbra.Log.Error( "Could not create GamePath from resource path." ); - return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); } - - CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); - - ResourceRequested?.Invoke( gamePath, isSync ); - - // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); - if( resolvedPath == null ) + else { - var retUnmodified = - CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); - return retUnmodified; + + CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); + + ResourceRequested?.Invoke( gamePath, isSync ); + + // If no replacements are being made, we still want to be able to trigger the event. + var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); + PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); + if( resolvedPath == null ) + { + var retUnmodified = + CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); + ret = retUnmodified; + } + else + { + + // Replace the hash and path with the correct one for the replacement. + *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); + + path = resolvedPath.Value.InternalName.Path; + ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); + + } } - - // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); - - path = resolvedPath.Value.InternalName.Path; - var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retModified, gamePath, resolvedPath.Value, data ); - return retModified; + TimingManager.StopTimer( TimingType.GetResourceHandler ); + return ret; } @@ -164,48 +174,50 @@ public unsafe partial class ResourceLoader private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { + TimingManager.StartTimer( TimingType.ReadSqPack ); + byte ret = 0; if( !DoReplacements ) { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - - if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) + else if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) { Penumbra.Log.Error( "Failure to load file from SqPack: invalid File Descriptor." ); - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - - if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) + else if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + ret = ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); } - // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) + else if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) { - return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); + ret = DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); + } + else + { + // Split the path into the special-treatment part (between the first and second '|') + // and the actual path. + var split = gamePath.Path.Split( ( byte )'|', 3, false ); + fileDescriptor->ResourceHandle->FileNameData = split[2].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; + var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui + && ResourceLoadCustomization.GetInvocationList() + .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) + .Invoke( split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + + if( !funcFound ) + { + ret = DefaultLoadResource( split[2], resourceManager, fileDescriptor, priority, isSync ); + } + + // Return original resource handle path so that they can be loaded separately. + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; } - // Split the path into the special-treatment part (between the first and second '|') - // and the actual path. - byte ret = 0; - var split = gamePath.Path.Split( ( byte )'|', 3, false ); - fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; - var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui - && ResourceLoadCustomization.GetInvocationList() - .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); - - if( !funcFound ) - { - ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); - } - - // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + TimingManager.StopTimer( TimingType.ReadSqPack ); return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 8c50afa7..09e628fd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -126,6 +126,7 @@ public unsafe partial class PathResolver private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { + TimingManager.StartTimer( TimingType.TimelineResources ); ulong ret; var old = _animationLoadData; try @@ -152,6 +153,7 @@ public unsafe partial class PathResolver _animationLoadData = old; + TimingManager.StopTimer( TimingType.TimelineResources ); return ret; } @@ -246,6 +248,7 @@ public unsafe partial class PathResolver private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) { + TimingManager.StartTimer( TimingType.LoadCharacterVfx ); var last = _animationLoadData; if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) ) { @@ -264,7 +267,6 @@ public unsafe partial class PathResolver { _animationLoadData = ResolveData.Invalid; } - var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 ); #if DEBUG var path = new ByteString( vfxPath ); @@ -272,6 +274,7 @@ public unsafe partial class PathResolver $"Load Character VFX: {path} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); #endif _animationLoadData = last; + TimingManager.StopTimer( TimingType.LoadCharacterVfx ); return ret; } @@ -282,6 +285,7 @@ public unsafe partial class PathResolver private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ) { + TimingManager.StartTimer( TimingType.LoadAreaVfx ); var last = _animationLoadData; if( caster != null ) { @@ -298,6 +302,7 @@ public unsafe partial class PathResolver $"Load Area VFX: {vfxId}, {pos[ 0 ]} {pos[ 1 ]} {pos[ 2 ]} {( caster != null ? new ByteString( caster->GetName() ).ToString() : "Unknown" )} {unk1} {unk2} {unk3} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); #endif _animationLoadData = last; + TimingManager.StopTimer( TimingType.LoadAreaVfx ); return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 18e04041..be17af9b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -138,6 +138,7 @@ public unsafe partial class PathResolver private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { + TimingManager.StartTimer( TimingType.CharacterBaseCreate ); var meta = DisposableContainer.Empty; if( LastGameObject != null ) { @@ -170,6 +171,7 @@ public unsafe partial class PathResolver { meta.Dispose(); } + TimingManager.StopTimer( TimingType.CharacterBaseCreate ); return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 6cb37665..45322e14 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -24,6 +24,7 @@ public unsafe partial class PathResolver return new ResolveData( Penumbra.CollectionManager.Default ); } + TimingManager.StartTimer( TimingType.IdentifyCollection ); try { if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) ) @@ -60,6 +61,7 @@ public unsafe partial class PathResolver ?? CollectionByAttributes( gameObject ) ?? CheckOwnedCollection( identifier, owner ) ?? Penumbra.CollectionManager.Default; + return IdentifiedCache.Set( collection, identifier, gameObject ); } catch( Exception e ) @@ -67,24 +69,35 @@ public unsafe partial class PathResolver Penumbra.Log.Error( $"Error identifying collection:\n{e}" ); return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); } + finally + { + TimingManager.StopTimer( TimingType.IdentifyCollection ); + } } // Get the collection applying to the current player character // or the default collection if no player exists. public static ModCollection PlayerCollection() { - var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); + TimingManager.StartTimer( TimingType.IdentifyCollection ); + var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); + ModCollection ret; if( gameObject == null ) { - return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + ret = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? Penumbra.CollectionManager.Default; } + else + { - var player = Penumbra.Actors.GetCurrentPlayer(); - return CollectionByIdentifier( player ) - ?? CheckYourself( player, gameObject ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; + var player = Penumbra.Actors.GetCurrentPlayer(); + ret = CollectionByIdentifier( player ) + ?? CheckYourself( player, gameObject ) + ?? CollectionByAttributes( gameObject ) + ?? Penumbra.CollectionManager.Default; + } + TimingManager.StopTimer( TimingType.IdentifyCollection ); + return ret; } // Check both temporary and permanent character collections. Temporary first. diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 2cf9cb4b..2974dd03 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -95,6 +95,7 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection ) { + TimingManager.StartTimer( TimingType.SetPathCollection ); if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { _pathCollections[ path ] = collection.ToResolveData( gameObject ); @@ -103,6 +104,7 @@ public unsafe partial class PathResolver { _pathCollections[ path.Clone() ] = collection.ToResolveData( gameObject ); } + TimingManager.StopTimer( TimingType.SetPathCollection ); } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 9e9e3c2d..091f6f62 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -113,7 +113,9 @@ public unsafe partial class PathResolver case ResourceType.Avfx: if( handle->FileSize == 0 ) { + TimingManager.StartTimer( TimingType.AddSubfile ); _subFileCollection[ ( IntPtr )handle ] = resolveData; + TimingManager.StopTimer( TimingType.AddSubfile ); } break; @@ -126,7 +128,9 @@ public unsafe partial class PathResolver { case ResourceType.Mtrl: case ResourceType.Avfx: + TimingManager.StartTimer( TimingType.AddSubfile ); _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); + TimingManager.StopTimer( TimingType.AddSubfile ); break; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index e09042f1..b29432ba 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -48,6 +48,7 @@ public partial class PathResolver : IDisposable // The modified resolver that handles game path resolving. private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data ) { + TimingManager.StartTimer( TimingType.CharacterResolver ); // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, // or if it is a face decal path and the current mod collection is set. @@ -71,6 +72,7 @@ public partial class PathResolver : IDisposable // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); + TimingManager.StopTimer( TimingType.CharacterResolver ); return true; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 09d3d155..b9cb7e80 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -86,6 +86,9 @@ public class Penumbra : IDalamudPlugin { try { + TimingManager.StartTimer( TimingType.TotalTime ); + TimingManager.StartTimer( TimingType.LaunchTime ); + Dalamud.Initialize( pluginInterface ); Log = new Logger(); DevPenumbraExists = CheckDevPluginPenumbra(); @@ -162,6 +165,7 @@ public class Penumbra : IDalamudPlugin { ResidentResources.Reload(); } + TimingManager.StopTimer( TimingType.LaunchTime ); } catch { @@ -304,6 +308,7 @@ public class Penumbra : IDalamudPlugin ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); + TimingManager.StopAllTimers(); } // Collect all relevant files for penumbra configuration. diff --git a/Penumbra/TimingManager.cs b/Penumbra/TimingManager.cs new file mode 100644 index 00000000..fea4e00f --- /dev/null +++ b/Penumbra/TimingManager.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using ImGuiNET; + +namespace Penumbra; + +public enum TimingType +{ + TotalTime, + LaunchTime, + DebugTimes, + UiMainWindow, + UiAdvancedWindow, + GetResourceHandler, + ReadSqPack, + CharacterResolver, + IdentifyCollection, + CharacterBaseCreate, + TimelineResources, + LoadCharacterVfx, + LoadAreaVfx, + AddSubfile, + SetResource, + SetPathCollection, +} + +public static class TimingManager +{ + public static readonly IReadOnlyList< ThreadLocal< Stopwatch > > StopWatches = +#if DEBUG + Enum.GetValues< TimingType >().Select( e => new ThreadLocal< Stopwatch >( () => new Stopwatch(), true ) ).ToArray(); +#else + Array.Empty>(); +#endif + + [Conditional( "DEBUG" )] + public static void StartTimer( TimingType timingType ) + { + var stopWatch = StopWatches[ ( int )timingType ].Value; + stopWatch!.Start(); + } + + [Conditional( "DEBUG" )] + public static void StopTimer( TimingType timingType ) + { + var stopWatch = StopWatches[ ( int )timingType ].Value; + stopWatch!.Stop(); + } + + [Conditional( "DEBUG" )] + public static void StopAllTimers() + { + foreach( var threadWatch in StopWatches ) + { + foreach( var stopWatch in threadWatch.Values ) + { + stopWatch.Stop(); + } + } + } + + [Conditional( "DEBUG" )] + public static void CreateTimingReport() + { + try + { + var sb = new StringBuilder( 1024 ); + sb.AppendLine( "```" ); + foreach( var type in Enum.GetValues< TimingType >() ) + { + var watches = StopWatches[ ( int )type ]; + var timeSum = watches.Values.Sum( w => w.ElapsedMilliseconds ); + + sb.AppendLine( $"{type,-20} - {timeSum,8} ms over {watches.Values.Count,2} Thread(s)" ); + } + + sb.AppendLine( "```" ); + + ImGui.SetClipboardText( sb.ToString() ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Could not create timing report:\n{ex}" ); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index f7a09c88..3b8aac19 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -61,6 +61,7 @@ public partial class ModEditWindow : Window, IDisposable public override void PreDraw() { + TimingManager.StartTimer( TimingType.UiAdvancedWindow ); var sb = new StringBuilder( 256 ); var redirections = 0; @@ -125,6 +126,7 @@ public partial class ModEditWindow : Window, IDisposable _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; sb.Append( WindowBaseLabel ); WindowName = sb.ToString(); + TimingManager.StopTimer( TimingType.UiAdvancedWindow ); } public override void OnClose() @@ -135,6 +137,7 @@ public partial class ModEditWindow : Window, IDisposable public override void Draw() { + TimingManager.StartTimer( TimingType.UiAdvancedWindow ); using var tabBar = ImRaii.TabBar( "##tabs" ); if( !tabBar ) { @@ -152,6 +155,7 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Draw(); DrawTextureTab(); _swapWindow.DrawItemSwapPanel(); + TimingManager.StopTimer( TimingType.UiAdvancedWindow ); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 826629e8..379428d6 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -29,6 +29,9 @@ public partial class ConfigWindow private void DrawModSelectorSettings() { +#if DEBUG + ImGui.NewLine(); // Due to the timing button. +#endif if( !ImGui.CollapsingHeader( "General" ) ) { OpenTutorial( BasicTutorialSteps.GeneralSettings ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 3a251e23..ba639180 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -370,6 +370,14 @@ public partial class ConfigWindow { _window._penumbra.ForceChangelogOpen(); } + +#if DEBUG + ImGui.SetCursorPos( new Vector2( xPos, 5 * ImGui.GetFrameHeightWithSpacing() ) ); + if( ImGui.Button( "Copy Timings", new Vector2( width, 0 ) ) ) + { + TimingManager.CreateTimingReport(); + } +#endif } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f367a92b..55b3a6cd 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -54,6 +54,8 @@ public sealed partial class ConfigWindow : Window, IDisposable { try { + TimingManager.StartTimer( TimingType.UiMainWindow ); + if( Penumbra.ImcExceptions.Count > 0 ) { DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" @@ -101,6 +103,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); } + TimingManager.StopTimer( TimingType.UiMainWindow ); } private static void DrawProblemWindow( string text, bool withExceptions ) From 2f7b6e3d559d5be4207d847dc0328d552cd040b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Jan 2023 14:44:33 +0100 Subject: [PATCH 0676/2451] Add performance monitor in debug compilations. --- OtterGui | 2 +- Penumbra/Interop/CharacterUtility.List.cs | 4 - .../Interop/Loader/ResourceLoader.Debug.cs | 16 ++- .../Loader/ResourceLoader.Replacement.cs | 119 +++++++++--------- .../Resolver/PathResolver.AnimationState.cs | 34 ++--- .../Resolver/PathResolver.DrawObjectState.cs | 6 +- .../Resolver/PathResolver.Identification.cs | 30 ++--- .../Interop/Resolver/PathResolver.Meta.cs | 7 +- .../Resolver/PathResolver.PathState.cs | 2 - .../Interop/Resolver/PathResolver.Subfiles.cs | 8 +- Penumbra/Interop/Resolver/PathResolver.cs | 4 +- Penumbra/Penumbra.cs | 8 +- Penumbra/TimingManager.cs | 90 ------------- Penumbra/UI/Classes/ModEditWindow.cs | 16 +-- Penumbra/UI/ConfigWindow.DebugTab.cs | 17 ++- Penumbra/UI/ConfigWindow.SettingsTab.cs | 8 -- Penumbra/UI/ConfigWindow.cs | 6 +- Penumbra/Util/PerformanceType.cs | 60 +++++++++ 18 files changed, 204 insertions(+), 233 deletions(-) delete mode 100644 Penumbra/TimingManager.cs create mode 100644 Penumbra/Util/PerformanceType.cs diff --git a/OtterGui b/OtterGui index 1b61ce89..6a1e25a6 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1b61ce894209ebabe6cf2d8b7c120f5a6edbe86a +Subproject commit 6a1e25a6c6aea6165c0a38771953e35550a1f9cf diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index 38f6e804..3cf137e8 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -77,13 +77,11 @@ public unsafe partial class CharacterUtility // Set the currently stored data of this resource to new values. private void SetResourceInternal( IntPtr data, int length ) { - TimingManager.StartTimer( TimingType.SetResource ); if( Ready ) { var resource = Penumbra.CharacterUtility.Address->Resource( GlobalIndex ); resource->SetData( data, length ); } - TimingManager.StopTimer( TimingType.SetResource ); } // Reset the currently stored data of this resource to its default values. @@ -135,7 +133,6 @@ public unsafe partial class CharacterUtility { if( !Disposed ) { - TimingManager.StartTimer( TimingType.SetResource ); var list = List._entries; var wasCurrent = ReferenceEquals( this, list.First?.Value ); list.Remove( this ); @@ -162,7 +159,6 @@ public unsafe partial class CharacterUtility } Disposed = true; - TimingManager.StopTimer( TimingType.SetResource ); } } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index a44ac58c..76eba25d 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -10,6 +10,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Interop.Loader; @@ -71,12 +72,13 @@ public unsafe partial class ResourceLoader private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolverInfo ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes ); + if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 ) { return; } - TimingManager.StartTimer( TimingType.DebugTimes ); // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra. try { @@ -97,7 +99,6 @@ public unsafe partial class ResourceLoader { Penumbra.Log.Error( e.ToString() ); } - TimingManager.StopTimer( TimingType.DebugTimes ); } // Find a key in a StdMap. @@ -204,10 +205,16 @@ public unsafe partial class ResourceLoader // Only used when the Replaced Resources Tab in the Debug tab is open. public void UpdateDebugInfo() { - TimingManager.StartTimer( TimingType.DebugTimes ); + using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes ); for( var i = 0; i < _debugList.Count; ++i ) { - var data = _debugList.Values[ i ]; + var data = _debugList.Values[ i ]; + if( data.OriginalPath.Path == null ) + { + _debugList.RemoveAt( i-- ); + continue; + } + var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); if( modifiedResource == null ) @@ -223,7 +230,6 @@ public unsafe partial class ResourceLoader }; } } - TimingManager.StopTimer( TimingType.DebugTimes ); } // Prevent resource management weirdness. diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index e2c83250..b4505ce7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.Util; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -81,43 +82,36 @@ public unsafe partial class ResourceLoader internal ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { - TimingManager.StartTimer( TimingType.GetResourceHandler ); - ResourceHandle* ret = null; + using var performance = Penumbra.Performance.Measure( PerformanceType.GetResourceHandler ); + + ResourceHandle* ret; if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { Penumbra.Log.Error( "Could not create GamePath from resource path." ); - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); } - else + + CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); + + ResourceRequested?.Invoke( gamePath, isSync ); + + // If no replacements are being made, we still want to be able to trigger the event. + var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); + PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); + if( resolvedPath == null ) { - - CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); - - ResourceRequested?.Invoke( gamePath, isSync ); - - // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); - if( resolvedPath == null ) - { - var retUnmodified = - CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); - ret = retUnmodified; - } - else - { - - // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); - - path = resolvedPath.Value.InternalName.Path; - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); - - } + ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, null, data ); + return ret; } - TimingManager.StopTimer( TimingType.GetResourceHandler ); + + // Replace the hash and path with the correct one for the replacement. + *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); + + path = resolvedPath.Value.InternalName.Path; + ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); + return ret; } @@ -174,50 +168,51 @@ public unsafe partial class ResourceLoader private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { - TimingManager.StartTimer( TimingType.ReadSqPack ); - byte ret = 0; + using var performance = Penumbra.Performance.Measure( PerformanceType.ReadSqPack ); + if( !DoReplacements ) { - ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - else if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) + + if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) { Penumbra.Log.Error( "Failure to load file from SqPack: invalid File Descriptor." ); - ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - else if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) + + if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) { - ret = ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } + // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - else if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) + if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) { - ret = DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); - } - else - { - // Split the path into the special-treatment part (between the first and second '|') - // and the actual path. - var split = gamePath.Path.Split( ( byte )'|', 3, false ); - fileDescriptor->ResourceHandle->FileNameData = split[2].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; - var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui - && ResourceLoadCustomization.GetInvocationList() - .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret ) ); - - if( !funcFound ) - { - ret = DefaultLoadResource( split[2], resourceManager, fileDescriptor, priority, isSync ); - } - - // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); } - TimingManager.StopTimer( TimingType.ReadSqPack ); + // Split the path into the special-treatment part (between the first and second '|') + // and the actual path. + byte ret = 0; + var split = gamePath.Path.Split( ( byte )'|', 3, false ); + fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; + var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui + && ResourceLoadCustomization.GetInvocationList() + .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + + if( !funcFound ) + { + ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); + } + + // Return original resource handle path so that they can be loaded separately. + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 09e628fd..3a16cb30 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -6,6 +6,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.Util; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace Penumbra.Interop.Resolver; @@ -110,7 +111,8 @@ public unsafe partial class PathResolver private IntPtr LoadCharacterSoundDetour( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ) { - var last = _characterSoundData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadSound ); + var last = _characterSoundData; _characterSoundData = IdentifyCollection( ( GameObject* )character, true ); var ret = _loadCharacterSoundHook.Original( character, unk1, unk2, unk3, unk4, unk5, unk6, unk7 ); _characterSoundData = last; @@ -126,9 +128,9 @@ public unsafe partial class PathResolver private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { - TimingManager.StartTimer( TimingType.TimelineResources ); - ulong ret; - var old = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.TimelineResources ); + ulong ret; + var old = _animationLoadData; try { if( timeline != IntPtr.Zero ) @@ -152,8 +154,6 @@ public unsafe partial class PathResolver } _animationLoadData = old; - - TimingManager.StopTimer( TimingType.TimelineResources ); return ret; } @@ -167,7 +167,8 @@ public unsafe partial class PathResolver private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) { - var last = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterBaseAnimation ); + var last = _animationLoadData; _animationLoadData = _drawObjectState.LastCreatedCollection.Valid ? _drawObjectState.LastCreatedCollection : FindParent( drawObject, out var collection ) != null @@ -186,8 +187,9 @@ public unsafe partial class PathResolver private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) { - var timelinePtr = a1 + 0x50; - var last = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadPap ); + var timelinePtr = a1 + 0x50; + var last = _animationLoadData; if( timelinePtr != IntPtr.Zero ) { var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); @@ -207,7 +209,8 @@ public unsafe partial class PathResolver private void SomeActionLoadDetour( IntPtr gameObject ) { - var last = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAction ); + var last = _animationLoadData; _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); _someActionLoadHook.Original( gameObject ); _animationLoadData = last; @@ -248,8 +251,8 @@ public unsafe partial class PathResolver private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) { - TimingManager.StartTimer( TimingType.LoadCharacterVfx ); - var last = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterVfx ); + var last = _animationLoadData; if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) ) { var obj = vfxParams->GameObjectType switch @@ -267,6 +270,7 @@ public unsafe partial class PathResolver { _animationLoadData = ResolveData.Invalid; } + var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 ); #if DEBUG var path = new ByteString( vfxPath ); @@ -274,7 +278,6 @@ public unsafe partial class PathResolver $"Load Character VFX: {path} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); #endif _animationLoadData = last; - TimingManager.StopTimer( TimingType.LoadCharacterVfx ); return ret; } @@ -285,8 +288,8 @@ public unsafe partial class PathResolver private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ) { - TimingManager.StartTimer( TimingType.LoadAreaVfx ); - var last = _animationLoadData; + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAreaVfx ); + var last = _animationLoadData; if( caster != null ) { _animationLoadData = IdentifyCollection( caster, true ); @@ -302,7 +305,6 @@ public unsafe partial class PathResolver $"Load Area VFX: {vfxId}, {pos[ 0 ]} {pos[ 1 ]} {pos[ 2 ]} {( caster != null ? new ByteString( caster->GetName() ).ToString() : "Unknown" )} {unk1} {unk2} {unk3} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); #endif _animationLoadData = last; - TimingManager.StopTimer( TimingType.LoadAreaVfx ); return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index be17af9b..78a5ed0a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -10,6 +10,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Interop.Resolver; @@ -138,7 +139,8 @@ public unsafe partial class PathResolver private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { - TimingManager.StartTimer( TimingType.CharacterBaseCreate ); + using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterBaseCreate ); + var meta = DisposableContainer.Empty; if( LastGameObject != null ) { @@ -171,7 +173,7 @@ public unsafe partial class PathResolver { meta.Dispose(); } - TimingManager.StopTimer( TimingType.CharacterBaseCreate ); + return ret; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 45322e14..8ee75678 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -8,6 +8,7 @@ using OtterGui; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -19,12 +20,13 @@ public unsafe partial class PathResolver // Identify the correct collection for a GameObject by index and name. public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); + if( gameObject == null ) { return new ResolveData( Penumbra.CollectionManager.Default ); } - TimingManager.StartTimer( TimingType.IdentifyCollection ); try { if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) ) @@ -69,35 +71,25 @@ public unsafe partial class PathResolver Penumbra.Log.Error( $"Error identifying collection:\n{e}" ); return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); } - finally - { - TimingManager.StopTimer( TimingType.IdentifyCollection ); - } } // Get the collection applying to the current player character // or the default collection if no player exists. public static ModCollection PlayerCollection() { - TimingManager.StartTimer( TimingType.IdentifyCollection ); - var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); - ModCollection ret; + using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); + var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); if( gameObject == null ) { - ret = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? Penumbra.CollectionManager.Default; } - else - { - var player = Penumbra.Actors.GetCurrentPlayer(); - ret = CollectionByIdentifier( player ) - ?? CheckYourself( player, gameObject ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; - } - TimingManager.StopTimer( TimingType.IdentifyCollection ); - return ret; + var player = Penumbra.Actors.GetCurrentPlayer(); + return CollectionByIdentifier( player ) + ?? CheckYourself( player, gameObject ) + ?? CollectionByAttributes( gameObject ) + ?? Penumbra.CollectionManager.Default; } // Check both temporary and permanent character collections. Temporary first. diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 1faa3b76..a618e705 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; +using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using static Penumbra.GameData.Enums.GenderRace; @@ -97,6 +98,7 @@ public unsafe partial class PathResolver { return; } + using var performance = Penumbra.Performance.Measure( PerformanceType.UpdateModels ); var collection = GetResolveData( drawObject ); using var eqp = collection.ModCollection.TemporarilySetEqpFile(); @@ -134,7 +136,7 @@ public unsafe partial class PathResolver { return; } - + using var performance = Penumbra.Performance.Measure( PerformanceType.GetEqp ); var resolveData = GetResolveData( drawObject ); using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); _getEqpIndirectHook.Original( drawObject ); @@ -150,6 +152,7 @@ public unsafe partial class PathResolver private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.SetupVisor ); var resolveData = GetResolveData( drawObject ); using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(); return _setupVisorHook.Original( drawObject, modelId, visorState ); @@ -169,6 +172,7 @@ public unsafe partial class PathResolver } else { + using var performance = Penumbra.Performance.Measure( PerformanceType.SetupCharacter ); var resolveData = GetResolveData( drawObject ); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); @@ -184,6 +188,7 @@ public unsafe partial class PathResolver private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.ChangeCustomize ); _inChangeCustomize = true; var resolveData = GetResolveData( human ); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 2974dd03..2cf9cb4b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -95,7 +95,6 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection ) { - TimingManager.StartTimer( TimingType.SetPathCollection ); if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { _pathCollections[ path ] = collection.ToResolveData( gameObject ); @@ -104,7 +103,6 @@ public unsafe partial class PathResolver { _pathCollections[ path.Clone() ] = collection.ToResolveData( gameObject ); } - TimingManager.StopTimer( TimingType.SetPathCollection ); } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 091f6f62..cca76c70 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -11,6 +11,7 @@ using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Interop.Resolver; @@ -113,9 +114,7 @@ public unsafe partial class PathResolver case ResourceType.Avfx: if( handle->FileSize == 0 ) { - TimingManager.StartTimer( TimingType.AddSubfile ); _subFileCollection[ ( IntPtr )handle ] = resolveData; - TimingManager.StopTimer( TimingType.AddSubfile ); } break; @@ -128,9 +127,7 @@ public unsafe partial class PathResolver { case ResourceType.Mtrl: case ResourceType.Avfx: - TimingManager.StartTimer( TimingType.AddSubfile ); _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); - TimingManager.StopTimer( TimingType.AddSubfile ); break; } } @@ -167,6 +164,7 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadTextures ); _mtrlData = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); _mtrlData = ResolveData.Invalid; @@ -179,6 +177,7 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadShaders ); _mtrlData = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); _mtrlData = ResolveData.Invalid; @@ -204,6 +203,7 @@ public unsafe partial class PathResolver private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) { + using var performance = Penumbra.Performance.Measure( PerformanceType.LoadApricotResources ); _avfxData = LoadFileHelper( handle ); var ret = _apricotResourceLoadHook.Original( handle, unk1, unk2 ); _avfxData = ResolveData.Invalid; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index b29432ba..2ece65f6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Interop.Resolver; @@ -48,7 +49,7 @@ public partial class PathResolver : IDisposable // The modified resolver that handles game path resolving. private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data ) { - TimingManager.StartTimer( TimingType.CharacterResolver ); + using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterResolver ); // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, // or if it is a face decal path and the current mod collection is set. @@ -72,7 +73,6 @@ public partial class PathResolver : IDisposable // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); - TimingManager.StopTimer( TimingType.CharacterResolver ); return true; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b9cb7e80..79f6ba77 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -65,6 +65,7 @@ public class Penumbra : IDalamudPlugin public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainManager StainManager { get; private set; } = null!; public static ItemData ItemData { get; private set; } = null!; + public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -86,10 +87,8 @@ public class Penumbra : IDalamudPlugin { try { - TimingManager.StartTimer( TimingType.TotalTime ); - TimingManager.StartTimer( TimingType.LaunchTime ); - Dalamud.Initialize( pluginInterface ); + Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); Log = new Logger(); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); @@ -165,7 +164,6 @@ public class Penumbra : IDalamudPlugin { ResidentResources.Reload(); } - TimingManager.StopTimer( TimingType.LaunchTime ); } catch { @@ -308,7 +306,7 @@ public class Penumbra : IDalamudPlugin ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); - TimingManager.StopAllTimers(); + Performance?.Dispose(); } // Collect all relevant files for penumbra configuration. diff --git a/Penumbra/TimingManager.cs b/Penumbra/TimingManager.cs deleted file mode 100644 index fea4e00f..00000000 --- a/Penumbra/TimingManager.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; -using ImGuiNET; - -namespace Penumbra; - -public enum TimingType -{ - TotalTime, - LaunchTime, - DebugTimes, - UiMainWindow, - UiAdvancedWindow, - GetResourceHandler, - ReadSqPack, - CharacterResolver, - IdentifyCollection, - CharacterBaseCreate, - TimelineResources, - LoadCharacterVfx, - LoadAreaVfx, - AddSubfile, - SetResource, - SetPathCollection, -} - -public static class TimingManager -{ - public static readonly IReadOnlyList< ThreadLocal< Stopwatch > > StopWatches = -#if DEBUG - Enum.GetValues< TimingType >().Select( e => new ThreadLocal< Stopwatch >( () => new Stopwatch(), true ) ).ToArray(); -#else - Array.Empty>(); -#endif - - [Conditional( "DEBUG" )] - public static void StartTimer( TimingType timingType ) - { - var stopWatch = StopWatches[ ( int )timingType ].Value; - stopWatch!.Start(); - } - - [Conditional( "DEBUG" )] - public static void StopTimer( TimingType timingType ) - { - var stopWatch = StopWatches[ ( int )timingType ].Value; - stopWatch!.Stop(); - } - - [Conditional( "DEBUG" )] - public static void StopAllTimers() - { - foreach( var threadWatch in StopWatches ) - { - foreach( var stopWatch in threadWatch.Values ) - { - stopWatch.Stop(); - } - } - } - - [Conditional( "DEBUG" )] - public static void CreateTimingReport() - { - try - { - var sb = new StringBuilder( 1024 ); - sb.AppendLine( "```" ); - foreach( var type in Enum.GetValues< TimingType >() ) - { - var watches = StopWatches[ ( int )type ]; - var timeSum = watches.Values.Sum( w => w.ElapsedMilliseconds ); - - sb.AppendLine( $"{type,-20} - {timeSum,8} ms over {watches.Values.Count,2} Thread(s)" ); - } - - sb.AppendLine( "```" ); - - ImGui.SetClipboardText( sb.ToString() ); - } - catch( Exception ex ) - { - Penumbra.Log.Error( $"Could not create timing report:\n{ex}" ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 3b8aac19..fe6ba962 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -23,10 +23,10 @@ public partial class ModEditWindow : Window, IDisposable private const string WindowBaseLabel = "###SubModEdit"; internal readonly ItemSwapWindow _swapWindow = new(); - private Editor? _editor; - private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; - private bool _allowReduplicate = false; + private Editor? _editor; + private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; + private bool _allowReduplicate = false; public void ChangeMod( Mod mod ) { @@ -61,7 +61,8 @@ public partial class ModEditWindow : Window, IDisposable public override void PreDraw() { - TimingManager.StartTimer( TimingType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + var sb = new StringBuilder( 256 ); var redirections = 0; @@ -126,7 +127,6 @@ public partial class ModEditWindow : Window, IDisposable _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; sb.Append( WindowBaseLabel ); WindowName = sb.ToString(); - TimingManager.StopTimer( TimingType.UiAdvancedWindow ); } public override void OnClose() @@ -137,7 +137,8 @@ public partial class ModEditWindow : Window, IDisposable public override void Draw() { - TimingManager.StartTimer( TimingType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + using var tabBar = ImRaii.TabBar( "##tabs" ); if( !tabBar ) { @@ -155,7 +156,6 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Draw(); DrawTextureTab(); _swapWindow.DrawItemSwapPanel(); - TimingManager.StopTimer( TimingType.UiAdvancedWindow ); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 8d57358a..0948231e 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; @@ -15,6 +16,7 @@ using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.Util; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -55,6 +57,7 @@ public partial class ConfigWindow } DrawDebugTabGeneral(); + DrawPerformanceTab(); ImGui.NewLine(); DrawDebugTabReplacedResources(); ImGui.NewLine(); @@ -109,6 +112,18 @@ public partial class ConfigWindow PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); } + [Conditional("DEBUG")] + private static void DrawPerformanceTab() + { + ImGui.NewLine(); + if( !ImGui.CollapsingHeader( "Performance" ) ) + { + return; + } + + Penumbra.Performance.Draw( "##performance", "Enable Performance Tracking", PerformanceTypeExtensions.ToName ); + } + // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. // Resources are collected by iterating through the private static unsafe void DrawDebugTabReplacedResources() @@ -249,7 +264,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( collection.ModCollection.Name ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.AssociatedGameObject.ToString("X") ); + ImGui.TextUnformatted( collection.AssociatedGameObject.ToString( "X" ) ); } } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index ba639180..3a251e23 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -370,14 +370,6 @@ public partial class ConfigWindow { _window._penumbra.ForceChangelogOpen(); } - -#if DEBUG - ImGui.SetCursorPos( new Vector2( xPos, 5 * ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( "Copy Timings", new Vector2( width, 0 ) ) ) - { - TimingManager.CreateTimingReport(); - } -#endif } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 55b3a6cd..e861ccec 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; +using Penumbra.Util; namespace Penumbra.UI; @@ -52,10 +53,10 @@ public sealed partial class ConfigWindow : Window, IDisposable public override void Draw() { + using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow ); + try { - TimingManager.StartTimer( TimingType.UiMainWindow ); - if( Penumbra.ImcExceptions.Count > 0 ) { DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" @@ -103,7 +104,6 @@ public sealed partial class ConfigWindow : Window, IDisposable { Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); } - TimingManager.StopTimer( TimingType.UiMainWindow ); } private static void DrawProblemWindow( string text, bool withExceptions ) diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs new file mode 100644 index 00000000..471f113a --- /dev/null +++ b/Penumbra/Util/PerformanceType.cs @@ -0,0 +1,60 @@ +namespace Penumbra.Util; + +public enum PerformanceType +{ + UiMainWindow, + UiAdvancedWindow, + CharacterResolver, + IdentifyCollection, + GetResourceHandler, + ReadSqPack, + CharacterBaseCreate, + TimelineResources, + LoadCharacterVfx, + LoadAreaVfx, + LoadSound, + LoadAction, + LoadCharacterBaseAnimation, + LoadPap, + LoadTextures, + LoadShaders, + LoadApricotResources, + UpdateModels, + GetEqp, + SetupVisor, + SetupCharacter, + ChangeCustomize, + DebugTimes, +} + +public static class PerformanceTypeExtensions +{ + public static string ToName( this PerformanceType type ) + => type switch + { + PerformanceType.UiMainWindow => "Main Interface Drawing", + PerformanceType.UiAdvancedWindow => "Advanced Window Drawing", + PerformanceType.GetResourceHandler => "GetResource Hook", + PerformanceType.ReadSqPack => "ReadSqPack Hook", + PerformanceType.CharacterResolver => "Resolving Characters", + PerformanceType.IdentifyCollection => "Identifying Collections", + PerformanceType.CharacterBaseCreate => "CharacterBaseCreate Hook", + PerformanceType.TimelineResources => "LoadTimelineResources Hook", + PerformanceType.LoadCharacterVfx => "LoadCharacterVfx Hook", + PerformanceType.LoadAreaVfx => "LoadAreaVfx Hook", + PerformanceType.LoadTextures => "LoadTextures Hook", + PerformanceType.LoadShaders => "LoadShaders Hook", + PerformanceType.LoadApricotResources => "LoadApricotFiles Hook", + PerformanceType.UpdateModels => "UpdateModels Hook", + PerformanceType.GetEqp => "GetEqp Hook", + PerformanceType.SetupVisor => "SetupVisor Hook", + PerformanceType.SetupCharacter => "SetupCharacter Hook", + PerformanceType.ChangeCustomize => "ChangeCustomize Hook", + PerformanceType.LoadSound => "LoadSound Hook", + PerformanceType.LoadCharacterBaseAnimation => "LoadCharacterAnimation Hook", + PerformanceType.LoadPap => "LoadPap Hook", + PerformanceType.LoadAction => "LoadAction Hook", + PerformanceType.DebugTimes => "Debug Tracking", + _ => $"Unknown {( int )type}", + }; +} \ No newline at end of file From 2b8862e4a561e8c6b3dbe9fc277090d31a0a036e Mon Sep 17 00:00:00 2001 From: SoyaX Date: Thu, 5 Jan 2023 20:03:50 +1030 Subject: [PATCH 0677/2451] Add tag command to CommandHandler --- Penumbra/CommandHandler.cs | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 978181ed..92334f41 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; @@ -109,6 +110,7 @@ public class CommandHandler : IDisposable "debug" => SetDebug( arguments ), "collection" => SetCollection( arguments ), "mod" => SetMod( arguments ), + "tag" => SetTag( arguments ), _ => PrintHelp( argumentList[ 0 ] ), }; } @@ -137,6 +139,7 @@ public class CommandHandler : IDisposable Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) .BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "tag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ).BuiltString ); return true; } @@ -443,6 +446,101 @@ public class CommandHandler : IDisposable return false; } + private bool SetTag( string arguments ) + { + if( arguments.Length == 0 ) + { + var seString = new SeStringBuilder() + .AddText( "Use with /penumbra tag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) + .AddPurple( "[Local Tag]" ); + Dalamud.Chat.Print( seString.BuiltString ); + return true; + } + + var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + if( nameSplit.Length != 2 ) + { + Dalamud.Chat.Print( "Not enough arguments provided." ); + return false; + } + + var state = split[ 0 ].ToLowerInvariant() switch + { + "enable" => 0, + "enabled" => 0, + "disable" => 1, + "disabled" => 1, + "toggle" => 2, + "inherit" => 3, + "inherited" => 3, + _ => -1, + }; + + if( state == -1 ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + return false; + } + + if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) + { + return false; + } + + var mods = _modManager.Where( m => m.LocalTags.Contains( nameSplit[ 1 ], StringComparer.InvariantCultureIgnoreCase ) ).ToArray(); + + if( mods.Length == 0 ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); + return false; + } + + foreach( var mod in mods ) + { + var settings = collection!.Settings[ mod.Index ]; + switch( state ) + { + case 0: + if( collection.SetModState( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + } + + break; + case 1: + if( collection.SetModState( mod.Index, false ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + } + + break; + case 2: + var setting = !( settings?.Enabled ?? false ); + if( collection.SetModState( mod.Index, setting ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + } + + break; + case 3: + if( collection.SetModInheritance( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( " to inherit." ).BuiltString ); + } + + break; + } + } + + return true; + } + private bool GetModCollection( string collectionName, out ModCollection? collection ) { var lowerName = collectionName.ToLowerInvariant(); From c684db300082dd460fa7964d32a8d53a33ff989d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Jan 2023 15:57:18 +0100 Subject: [PATCH 0678/2451] Small security check. --- Penumbra/UI/Classes/ItemSwapWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 8dbab3e8..b086e148 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -652,7 +652,7 @@ public class ItemSwapWindow : IDisposable return; } - UpdateMod( _mod, newCollection.Settings[ _mod.Index ] ); + UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); newCollection.ModSettingChanged += OnSettingChange; if( oldCollection != null ) { From 6e983c873524e7614444db07154d86d45b57d846 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Jan 2023 16:52:01 +0100 Subject: [PATCH 0679/2451] Add placeholder options to collection chat command, slightly refactor tag -> bulktag command by SoyaX. --- Penumbra/CommandHandler.cs | 200 ++++++++++++++--------------- Penumbra/Interop/ObjectReloader.cs | 2 +- 2 files changed, 96 insertions(+), 106 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 92334f41..c61c09b2 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -110,7 +110,7 @@ public class CommandHandler : IDisposable "debug" => SetDebug( arguments ), "collection" => SetCollection( arguments ), "mod" => SetMod( arguments ), - "tag" => SetTag( arguments ), + "bulktag" => SetTag( arguments ), _ => PrintHelp( argumentList[ 0 ] ), }; } @@ -139,7 +139,8 @@ public class CommandHandler : IDisposable Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) .BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "tag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "tag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) + .BuiltString ); return true; } @@ -249,6 +250,8 @@ public class CommandHandler : IDisposable .BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) + .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); @@ -292,7 +295,20 @@ public class CommandHandler : IDisposable try { - identifier = _actors.FromUserString( split[ 2 ] ); + if( ObjectReloader.GetName( split[ 2 ].ToLowerInvariant(), out var obj ) ) + { + identifier = _actors.FromObject( obj, false, true ); + if( !identifier.IsValid ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) + .AddText( " did not resolve to a game object with a valid identifier." ).BuiltString ); + return false; + } + } + else + { + identifier = _actors.FromUserString( split[ 2 ] ); + } } catch( ActorManager.IdentifierParseError e ) { @@ -370,17 +386,7 @@ public class CommandHandler : IDisposable return false; } - var state = split[ 0 ].ToLowerInvariant() switch - { - "enable" => 0, - "enabled" => 0, - "disable" => 1, - "disabled" => 1, - "toggle" => 2, - "inherit" => 3, - "inherited" => 3, - _ => -1, - }; + var state = ConvertToSettingState( split[ 0 ] ); if( state == -1 ) { Dalamud.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); @@ -398,51 +404,13 @@ public class CommandHandler : IDisposable return false; } - var settings = collection!.Settings[ mod.Index ]; - switch( state ) + if( HandleModState( state, collection!, mod ) ) { - case 0: - if( collection.SetModState( mod.Index, true ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - return true; - } - - break; - case 1: - if( collection.SetModState( mod.Index, false ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - return true; - } - - break; - case 2: - var setting = !( settings?.Enabled ?? false ); - if( collection.SetModState( mod.Index, setting ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - return true; - } - - break; - case 3: - if( collection.SetModInheritance( mod.Index, true ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( " to inherit." ).BuiltString ); - return true; - } - - break; + return true; } Dalamud.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) - .AddYellow( collection.Name, true ).AddText( "." ).BuiltString ); + .AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); return false; } @@ -465,17 +433,7 @@ public class CommandHandler : IDisposable return false; } - var state = split[ 0 ].ToLowerInvariant() switch - { - "enable" => 0, - "enabled" => 0, - "disable" => 1, - "disabled" => 1, - "toggle" => 2, - "inherit" => 3, - "inherited" => 3, - _ => -1, - }; + var state = ConvertToSettingState( split[ 0 ] ); if( state == -1 ) { @@ -488,54 +446,23 @@ public class CommandHandler : IDisposable return false; } - var mods = _modManager.Where( m => m.LocalTags.Contains( nameSplit[ 1 ], StringComparer.InvariantCultureIgnoreCase ) ).ToArray(); + var mods = _modManager.Where( m => m.LocalTags.Contains( nameSplit[ 1 ], StringComparer.OrdinalIgnoreCase ) ).ToList(); - if( mods.Length == 0 ) + if( mods.Count == 0 ) { Dalamud.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); return false; } + var changes = false; foreach( var mod in mods ) { - var settings = collection!.Settings[ mod.Index ]; - switch( state ) - { - case 0: - if( collection.SetModState( mod.Index, true ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - } + changes |= HandleModState( state, collection!, mod ); + } - break; - case 1: - if( collection.SetModState( mod.Index, false ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - } - - break; - case 2: - var setting = !( settings?.Enabled ?? false ); - if( collection.SetModState( mod.Index, setting ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); - } - - break; - case 3: - if( collection.SetModInheritance( mod.Index, true ) ) - { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( " to inherit." ).BuiltString ); - } - - break; - } + if( !changes ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "No mod states were changed in collection " ).AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); } return true; @@ -579,4 +506,67 @@ public class CommandHandler : IDisposable _ => null, }; + + private static int ConvertToSettingState( string text ) + => text.ToLowerInvariant() switch + { + "enable" => 0, + "enabled" => 0, + "disable" => 1, + "disabled" => 1, + "toggle" => 2, + "inherit" => 3, + "inherited" => 3, + _ => -1, + }; + + private static bool HandleModState( int settingState, ModCollection collection, Mod mod ) + { + var settings = collection!.Settings[ mod.Index ]; + switch( settingState ) + { + case 0: + if( collection.SetModState( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + return false; + case 1: + if( collection.SetModState( mod.Index, false ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + return false; + case 2: + var setting = !( settings?.Enabled ?? false ); + if( collection.SetModState( mod.Index, setting ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + return false; + case 3: + if( collection.SetModInheritance( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( " to inherit." ).BuiltString ); + return true; + } + + return false; + } + + return false; + } } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 7a3e7915..31b2c6dc 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -316,7 +316,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable return gPosePlayer ?? Dalamud.Objects[ 0 ]; } - private static bool GetName( string lowerName, out GameObject? actor ) + public static bool GetName( string lowerName, out GameObject? actor ) { ( actor, var ret ) = lowerName switch { From baf3b06060944cc63122d53fba13cfaccc23231c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 6 Jan 2023 14:33:46 +0100 Subject: [PATCH 0680/2451] Remove gender-unlocked clothes --- Penumbra.GameData/Data/RestrictedGear.cs | 32 ++---------------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs index 21dff7cf..7aeac80a 100644 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Dalamud; @@ -8,12 +8,11 @@ using Dalamud.Plugin; using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Race = Penumbra.GameData.Enums.Race; -namespace Glamourer; +namespace Penumbra.GameData.Data; /// /// Handle gender- or race-locked gear in the draw model itself. @@ -248,8 +247,6 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga AddItem(m2f, f2m, 06102, 06104); // Black Summer Top <-> Black Summer Halter AddItem(m2f, f2m, 06103, 06105); // Black Summer Trunks <-> Black Summer Tanga - AddItem(m2f, f2m, 06972, 06973); // Valentione Apron <-> Valentione Apron Dress - AddItem(m2f, f2m, 06975, 06976); // Valentione Trousers <-> Valentione Skirt AddItem(m2f, f2m, 08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly) AddItem(m2f, f2m, 08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly) AddItem(m2f, f2m, 08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly) @@ -319,11 +316,6 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves AddItem(m2f, f2m, 17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt AddItem(m2f, f2m, 17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots - AddItem(m2f, f2m, 17481, 17476); // Royal Seneschal's Chapeau <-> Songbird Hat - AddItem(m2f, f2m, 17482, 17477); // Royal Seneschal's Coat <-> Songbird Jacket - AddItem(m2f, f2m, 17483, 17478); // Royal Seneschal's Fingerless Gloves <-> Songbird Gloves - AddItem(m2f, f2m, 17484, 17479); // Royal Seneschal's Breeches <-> Songbird Skirt - AddItem(m2f, f2m, 17485, 17480); // Royal Seneschal's Boots <-> Songbird Boots AddItem(m2f, f2m, 20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady AddItem(m2f, f2m, 20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi AddItem(m2f, f2m, 20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves @@ -337,31 +329,11 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon AddItem(m2f, f2m, 24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama AddItem(m2f, f2m, 24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots - AddItem(m2f, f2m, 28558, 28573); // Valentione Rose Hat <-> Valentione Rose Ribboned Hat - AddItem(m2f, f2m, 28559, 28574); // Valentione Rose Waistcoat <-> Valentione Rose Dress - AddItem(m2f, f2m, 28560, 28575); // Valentione Rose Gloves <-> Valentione Rose Ribboned Gloves - AddItem(m2f, f2m, 28561, 28576); // Valentione Rose Slacks <-> Valentione Rose Tights - AddItem(m2f, f2m, 28562, 28577); // Valentione Rose Shoes <-> Valentione Rose Heels - AddItem(m2f, f2m, 28563, 28578); // Valentione Forget-me-not Hat <-> Valentione Forget-me-not Ribboned Hat - AddItem(m2f, f2m, 28564, 28579); // Valentione Forget-me-not Waistcoat <-> Valentione Forget-me-not Dress - AddItem(m2f, f2m, 28565, 28580); // Valentione Forget-me-not Gloves <-> Valentione Forget-me-not Ribboned Gloves - AddItem(m2f, f2m, 28566, 28581); // Valentione Forget-me-not Slacks <-> Valentione Forget-me-not Tights - AddItem(m2f, f2m, 28567, 28582); // Valentione Forget-me-not Shoes <-> Valentione Forget-me-not Heels - AddItem(m2f, f2m, 28568, 28583); // Valentione Acacia Hat <-> Valentione Acacia Ribboned Hat - AddItem(m2f, f2m, 28569, 28584); // Valentione Acacia Waistcoat <-> Valentione Acacia Dress - AddItem(m2f, f2m, 28570, 28585); // Valentione Acacia Gloves <-> Valentione Acacia Ribboned Gloves - AddItem(m2f, f2m, 28571, 28586); // Valentione Acacia Slacks <-> Valentione Acacia Tights - AddItem(m2f, f2m, 28572, 28587); // Valentione Acacia Shoes <-> Valentione Acacia Heels AddItem(m2f, f2m, 28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat AddItem(m2f, f2m, 28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat AddItem(m2f, f2m, 28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves AddItem(m2f, f2m, 28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt AddItem(m2f, f2m, 28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots - AddItem(m2f, f2m, 31408, 31413); // Bergsteiger's Hat <-> Dirndl's Hat - AddItem(m2f, f2m, 31409, 31414); // Bergsteiger's Jacket <-> Dirndl's Bodice - AddItem(m2f, f2m, 31410, 31415); // Bergsteiger's Halfgloves <-> Dirndl's Wrist Torque - AddItem(m2f, f2m, 31411, 31416); // Bergsteiger's Halfslops <-> Dirndl's Long Skirt - AddItem(m2f, f2m, 31412, 31417); // Bergsteiger's Boots <-> Dirndl's Pumps AddItem(m2f, f2m, 36336, 36337); // Omega-M Attire <-> Omega-F Attire AddItem(m2f, f2m, 36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings AddItem(m2f, f2m, 37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon From 72408bf45c8c5bfbeae3a04407bd5b9231af24f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 6 Jan 2023 14:34:28 +0100 Subject: [PATCH 0681/2451] Add a hook for updating looped scds. --- OtterGui | 2 +- .../Resolver/PathResolver.AnimationState.cs | 79 +++++++++++++------ Penumbra/Util/PerformanceType.cs | 2 + 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/OtterGui b/OtterGui index 6a1e25a6..7c7b641f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6a1e25a6c6aea6165c0a38771953e35550a1f9cf +Subproject commit 7c7b641feb30c1a03c47a5653c7f7cdde529dfc7 diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 3a16cb30..17823782 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -70,6 +70,7 @@ public unsafe partial class PathResolver _loadCharacterSoundHook.Enable(); _loadCharacterVfxHook.Enable(); _loadAreaVfxHook.Enable(); + _scheduleClipUpdateHook.Enable(); //_loadSomeAvfxHook.Enable(); //_someOtherAvfxHook.Enable(); @@ -84,6 +85,7 @@ public unsafe partial class PathResolver _loadCharacterSoundHook.Disable(); _loadCharacterVfxHook.Disable(); _loadAreaVfxHook.Disable(); + _scheduleClipUpdateHook.Disable(); //_loadSomeAvfxHook.Disable(); //_someOtherAvfxHook.Disable(); @@ -98,6 +100,7 @@ public unsafe partial class PathResolver _loadCharacterSoundHook.Dispose(); _loadCharacterVfxHook.Dispose(); _loadAreaVfxHook.Dispose(); + _scheduleClipUpdateHook.Dispose(); //_loadSomeAvfxHook.Dispose(); //_someOtherAvfxHook.Dispose(); @@ -119,6 +122,29 @@ public unsafe partial class PathResolver return ret; } + private static ResolveData GetDataFromTimeline( IntPtr timeline ) + { + try + { + if( timeline != IntPtr.Zero ) + { + var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; + var idx = getGameObjectIdx( timeline ); + if( idx >= 0 && idx < Dalamud.Objects.Length ) + { + var obj = Dalamud.Objects[ idx ]; + return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; + } + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error getting timeline data for 0x{timeline:X}:\n{e}" ); + } + + return ResolveData.Invalid; + } + // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); @@ -129,30 +155,9 @@ public unsafe partial class PathResolver private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { using var performance = Penumbra.Performance.Measure( PerformanceType.TimelineResources ); - ulong ret; - var old = _animationLoadData; - try - { - if( timeline != IntPtr.Zero ) - { - var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; - var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < Dalamud.Objects.Length ) - { - var obj = Dalamud.Objects[ idx ]; - _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; - } - else - { - _animationLoadData = ResolveData.Invalid; - } - } - } - finally - { - ret = _loadTimelineResourcesHook.Original( timeline ); - } - + var old = _animationLoadData; + _animationLoadData = GetDataFromTimeline( timeline ); + var ret = _loadTimelineResourcesHook.Original( timeline ); _animationLoadData = old; return ret; } @@ -309,6 +314,32 @@ public unsafe partial class PathResolver } + + [StructLayout( LayoutKind.Explicit )] + private struct ClipScheduler + { + [FieldOffset( 0 )] + public IntPtr* VTable; + + [FieldOffset( 0x38 )] + public IntPtr SchedulerTimeline; + } + + private delegate void ScheduleClipUpdate( ClipScheduler* x ); + + [Signature( "40 53 55 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B F9", DetourName = nameof( ScheduleClipUpdateDetour ) )] + private readonly Hook< ScheduleClipUpdate > _scheduleClipUpdateHook = null!; + + private void ScheduleClipUpdateDetour( ClipScheduler* x ) + { + using var performance = Penumbra.Performance.Measure( PerformanceType.ScheduleClipUpdate ); + var old = _animationLoadData; + var timeline = x->SchedulerTimeline; + _animationLoadData = GetDataFromTimeline( timeline ); + _scheduleClipUpdateHook.Original( x ); + _animationLoadData = old; + } + // ========== Those hooks seem to be superseded by LoadCharacterVfx ========= // public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index 471f113a..9328d750 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -13,6 +13,7 @@ public enum PerformanceType LoadCharacterVfx, LoadAreaVfx, LoadSound, + ScheduleClipUpdate, LoadAction, LoadCharacterBaseAnimation, LoadPap, @@ -51,6 +52,7 @@ public static class PerformanceTypeExtensions PerformanceType.SetupCharacter => "SetupCharacter Hook", PerformanceType.ChangeCustomize => "ChangeCustomize Hook", PerformanceType.LoadSound => "LoadSound Hook", + PerformanceType.ScheduleClipUpdate => "ScheduleClipUpdate Hook", PerformanceType.LoadCharacterBaseAnimation => "LoadCharacterAnimation Hook", PerformanceType.LoadPap => "LoadPap Hook", PerformanceType.LoadAction => "LoadAction Hook", From 28ab12c21b6a61fdaec9fe0bb3bc8a3d10711336 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 6 Jan 2023 16:03:45 +0100 Subject: [PATCH 0682/2451] Add option to not use any mods when inspecting players. --- Penumbra/Configuration.cs | 1 + Penumbra/Interop/Resolver/PathResolver.Identification.cs | 9 +++++++-- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 0e6b70d2..167be4f4 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -37,6 +37,7 @@ public partial class Configuration : IPluginConfiguration public bool UseCharacterCollectionInInspect { get; set; } = true; public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; public bool HideRedrawBar { get; set; } = false; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 8ee75678..d3a2fb3f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -57,6 +57,11 @@ public unsafe partial class PathResolver } var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); + if( Penumbra.Config.UseNoModsInInspect && identifier.Type == IdentifierType.Special && identifier.Special == SpecialActor.ExamineScreen ) + { + return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); + } + identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); var collection = CollectionByIdentifier( identifier ) ?? CheckYourself( identifier, gameObject ) @@ -77,8 +82,8 @@ public unsafe partial class PathResolver // or the default collection if no player exists. public static ModCollection PlayerCollection() { - using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); + using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); + var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); if( gameObject == null ) { return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 379428d6..ea73e1b4 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -74,6 +74,8 @@ public partial class ConfigWindow Checkbox( $"Use {AssignedCollections} in Try-On Window", "Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); + Checkbox( "Use No Mods in Inspect Windows", "Use the empty collection for characters you are inspecting, regardless of the character.\n" + + "Takes precedence before the next option.", Penumbra.Config.UseNoModsInInspect, v => Penumbra.Config.UseNoModsInInspect = v ); Checkbox( $"Use {AssignedCollections} in Inspect Windows", "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); From 7b318c9ce47589415e870d7e60eeb86b79e51797 Mon Sep 17 00:00:00 2001 From: SoyaX Date: Sun, 8 Jan 2023 15:21:16 +1030 Subject: [PATCH 0683/2451] Update bulktag labels --- Penumbra/CommandHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index c61c09b2..3779e512 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -139,7 +139,7 @@ public class CommandHandler : IDisposable Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) .BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "tag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) .BuiltString ); return true; } @@ -419,7 +419,7 @@ public class CommandHandler : IDisposable if( arguments.Length == 0 ) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra tag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) + .AddText( "Use with /penumbra bulktag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) .AddPurple( "[Local Tag]" ); Dalamud.Chat.Print( seString.BuiltString ); return true; From d23eab053005e6e13aba3a00e996ed97ca3ee322 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jan 2023 13:33:27 +0100 Subject: [PATCH 0684/2451] Fix collection caches not resetting correctly. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 7c7b641f..fe73e790 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 7c7b641feb30c1a03c47a5653c7f7cdde529dfc7 +Subproject commit fe73e7901270a9d33afa374c9de930c53580fada From b7408f15fb7fe85a1f15585317e1134a82274d23 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jan 2023 13:33:48 +0100 Subject: [PATCH 0685/2451] Fix some screen actors not respecting settings. --- Penumbra/Collections/IndividualCollections.Access.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 669dd086..463907bb 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -98,16 +98,16 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ identifier = _actorManager.GetInspectPlayer(); if( identifier.IsValid ) { - return identifier; + return Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid; } identifier = _actorManager.GetCardPlayer(); if( identifier.IsValid ) { - return identifier; + return Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid; } - return _actorManager.GetGlamourPlayer(); + return Penumbra.Config.UseCharacterCollectionInTryOn ? _actorManager.GetGlamourPlayer() : ActorIdentifier.Invalid; } default: return identifier; } From f27d49f5d69e8df20950f939fd2571f158d40fcb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jan 2023 13:34:04 +0100 Subject: [PATCH 0686/2451] Try to handle Mahjong actors. --- .../Resolver/PathResolver.Identification.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index d3a2fb3f..d01e9962 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -17,6 +18,48 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { + private static ResolveData IdentifyMahjong( GameObject* gameObject ) + { + static bool SearchPlayer( Character* character, int idx, out ActorIdentifier id ) + { + var other = ( Character* )Dalamud.Objects.GetObjectAddress( idx ); + if( other == null || !CustomizeData.Equals( ( CustomizeData* )character->CustomizeData, ( CustomizeData* )other->CustomizeData ) ) + { + id = ActorIdentifier.Invalid; + return false; + } + + id = Penumbra.Actors.FromObject( &other->GameObject, out _, false, true ); + return true; + } + + static ActorIdentifier SearchPlayers( Character* gameObject, int idx1, int idx2, int idx3 ) + => SearchPlayer( gameObject, idx1, out var id ) || SearchPlayer( gameObject, idx2, out id ) || SearchPlayer( gameObject, idx3, out id ) + ? id + : ActorIdentifier.Invalid; + + var identifier = gameObject->ObjectIndex switch + { + 0 => Penumbra.Actors.GetCurrentPlayer(), + 2 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), + 4 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), + 6 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), + 240 => Penumbra.Actors.GetCurrentPlayer(), + 241 => SearchPlayers( ( Character* )gameObject, 2, 4, 6 ), + 242 => SearchPlayers( ( Character* )gameObject, 4, 2, 6 ), + 243 => SearchPlayers( ( Character* )gameObject, 6, 2, 4 ), + _ => ActorIdentifier.Invalid, + }; + + var collection = ( identifier.IsValid ? CollectionByIdentifier( identifier ) : null ) + ?? CheckYourself( identifier, gameObject ) + ?? CollectionByAttributes( gameObject ) + ?? Penumbra.CollectionManager.Default; + + return IdentifiedCache.Set( collection, identifier, gameObject ); + } + + // Identify the correct collection for a GameObject by index and name. public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { @@ -45,6 +88,12 @@ public unsafe partial class PathResolver return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } + // Mahjong special case. + if( Dalamud.ClientState.TerritoryType == 831 ) + { + return IdentifyMahjong( gameObject ); + } + // Aesthetician. The relevant actor is yourself, so use player collection when possible. if( Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) { From ea2a411a2e2d1dc5baf37805b1a8b0a5291cf4a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jan 2023 13:34:43 +0100 Subject: [PATCH 0687/2451] Improve File Swap hint texts, also reorders them. --- Penumbra/UI/Classes/ModEditWindow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index fe6ba962..e1fecd92 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -526,10 +526,10 @@ public partial class ModEditWindow : Window, IDisposable ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapKey", "New Swap Source...", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); + ImGui.InputTextWithHint( "##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapValue", "New Swap Target...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); + ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); } public ModEditWindow() From 40b7266c22756d7d2916152b5fa9413b7371ee5d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jan 2023 13:35:51 +0100 Subject: [PATCH 0688/2451] Add handling for left and right ring. --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 129 +++++++++++--------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 17 ++- 3 files changed, 91 insertions(+), 59 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 49e8ae66..4f49ef62 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -17,8 +17,24 @@ namespace Penumbra.Mods.ItemSwap; public static class EquipmentSwap { + private static EquipSlot[] ConvertSlots( EquipSlot slot, bool rFinger, bool lFinger ) + { + if( slot != EquipSlot.RFinger ) + { + return new[] { slot }; + } + + return rFinger + ? lFinger + ? new[] { EquipSlot.RFinger, EquipSlot.LFinger } + : new[] { EquipSlot.RFinger } + : lFinger + ? new[] { EquipSlot.LFinger } + : Array.Empty< EquipSlot >(); + } + public static Item[] CreateItemSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, - Item itemTo ) + Item itemTo, bool rFinger = true, bool lFinger = true ) { // Check actual ids, variants and slots. We only support using the same slot. LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom ); @@ -40,69 +56,72 @@ public static class EquipmentSwap swaps.Add( gmp ); } - - var (imcFileFrom, variants, affectedItems) = GetVariants( slotFrom, idFrom, idTo, variantFrom ); - var imcFileTo = new ImcFile( new ImcManipulation( slotFrom, variantTo, idTo.Value, default ) ); - - var isAccessory = slotFrom.IsAccessory(); - var estType = slotFrom switch + var affectedItems = Array.Empty< Item >(); + foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) { - EquipSlot.Head => EstManipulation.EstType.Head, - EquipSlot.Body => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, - }; + (var imcFileFrom, var variants, affectedItems) = GetVariants( slot, idFrom, idTo, variantFrom ); + var imcFileTo = new ImcFile( new ImcManipulation( slot, variantTo, idTo.Value, default ) ); - var skipFemale = false; - var skipMale = false; - var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotFrom ), variantTo ).MaterialId; - foreach( var gr in Enum.GetValues< GenderRace >() ) - { - switch( gr.Split().Item1 ) + var isAccessory = slot.IsAccessory(); + var estType = slot switch { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; - case Gender.FemaleNpc when skipFemale: continue; - } + EquipSlot.Head => EstManipulation.EstType.Head, + EquipSlot.Body => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; - if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 ) + var skipFemale = false; + var skipMale = false; + var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ).MaterialId; + foreach( var gr in Enum.GetValues< GenderRace >() ) { - continue; - } - - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); - if( est != null ) - { - swaps.Add( est ); - } - - try - { - var eqdp = CreateEqdp( redirections, manips, slotFrom, gr, idFrom, idTo, mtrlVariantTo ); - if( eqdp != null ) + switch( gr.Split().Item1 ) { - swaps.Add( eqdp ); + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; + case Gender.FemaleNpc when skipFemale: continue; + } + + if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 ) + { + continue; + } + + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); + if( est != null ) + { + swaps.Add( est ); + } + + try + { + var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); + if( eqdp != null ) + { + swaps.Add( eqdp ); + } + } + catch( ItemSwap.MissingFileException e ) + { + switch( gr ) + { + case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: + skipMale = true; + continue; + case GenderRace.MidlanderFemale when e.Type == ResourceType.Mdl: + skipFemale = true; + continue; + default: throw; + } } } - catch( ItemSwap.MissingFileException e ) - { - switch( gr ) - { - case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: - skipMale = true; - continue; - case GenderRace.MidlanderFemale when e.Type == ResourceType.Mdl: - skipFemale = true; - continue; - default: throw; - } - } - } - foreach( var variant in variants ) - { - var imc = CreateImc( redirections, manips, slotFrom, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); - swaps.Add( imc ); + foreach( var variant in variants ) + { + var imc = CreateImc( redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); + swaps.Add( imc ); + } } return affectedItems; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 66ceb930..deb15bee 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -124,11 +124,11 @@ public class ItemSwapContainer return m => set.TryGetValue( m, out var a ) ? a : m; } - public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null ) + public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), from, to ); + var ret = EquipmentSwap.CreateItemSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); Loaded = true; return ret; } diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index b086e148..69a4494c 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -109,6 +109,8 @@ public class ItemSwapWindow : IDisposable private bool _subModValid = false; private bool _useFileSwaps = true; private bool _useCurrentCollection = false; + private bool _useLeftRing = true; + private bool _useRightRing = true; private Item[]? _affectedItems; @@ -159,7 +161,7 @@ public class ItemSwapWindow : IDisposable if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) { _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing ); } break; @@ -399,11 +401,22 @@ public class ItemSwapWindow : IDisposable ImGui.TableNextColumn(); _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + if( type == SwapType.Ring ) + { + ImGui.SameLine(); + _dirty |= ImGui.Checkbox( "Swap Right Ring", ref _useRightRing ); + } + ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( text2 ); ImGui.TableNextColumn(); _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + if( type == SwapType.Ring ) + { + ImGui.SameLine(); + _dirty |= ImGui.Checkbox( "Swap Left Ring", ref _useLeftRing ); + } if( _affectedItems is { Length: > 1 } ) { @@ -652,7 +665,7 @@ public class ItemSwapWindow : IDisposable return; } - UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); + UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); newCollection.ModSettingChanged += OnSettingChange; if( oldCollection != null ) { From a061ab9b8b7db7113d79782b12c79ca4c4f33134 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Jan 2023 13:59:24 +0100 Subject: [PATCH 0689/2451] Extract all signatures to a single file. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 5 +- Penumbra.GameData/Signatures.cs | 91 +++++++++++++++++++ Penumbra/Interop/CharacterUtility.cs | 5 +- .../Interop/Loader/ResourceLoader.Debug.cs | 6 +- .../Loader/ResourceLoader.Replacement.cs | 19 ++-- .../Interop/Loader/ResourceLoader.TexMdl.cs | 21 +++-- Penumbra/Interop/MetaFileManager.cs | 3 +- Penumbra/Interop/ObjectReloader.cs | 1 - Penumbra/Interop/ResidentResourceManager.cs | 17 ++-- .../Interop/Resolver/CutsceneCharacters.cs | 3 +- .../Resolver/IdentifiedCollectionCache.cs | 4 +- .../Resolver/PathResolver.AnimationState.cs | 48 ++-------- .../Resolver/PathResolver.DrawObjectState.cs | 10 +- .../Interop/Resolver/PathResolver.Meta.cs | 14 +-- .../Resolver/PathResolver.PathState.cs | 10 +- .../Interop/Resolver/PathResolver.Subfiles.cs | 8 +- Penumbra/Interop/Structs/ClipScheduler.cs | 14 +++ Penumbra/Interop/Structs/VfxParams.cs | 19 ++++ 18 files changed, 202 insertions(+), 96 deletions(-) create mode 100644 Penumbra.GameData/Signatures.cs create mode 100644 Penumbra/Interop/Structs/ClipScheduler.cs create mode 100644 Penumbra/Interop/Structs/VfxParams.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 77bd13bf..5dd6d696 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Dalamud; using Dalamud.Data; +using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Enums; @@ -229,10 +230,10 @@ public sealed partial class ActorManager : IDisposable private readonly Func _toParentIdx; - [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] + [Signature(Sigs.InspectTitleId, ScanType = ScanType.StaticAddress)] private static unsafe ushort* _inspectTitleId = null!; - [Signature("0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0", ScanType = ScanType.StaticAddress)] + [Signature(Sigs.InspectWorldId, ScanType = ScanType.StaticAddress)] private static unsafe ushort* _inspectWorldId = null!; private static unsafe ushort InspectTitleId diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs new file mode 100644 index 00000000..9f13bdf1 --- /dev/null +++ b/Penumbra.GameData/Signatures.cs @@ -0,0 +1,91 @@ +namespace Penumbra.GameData; + +public static class Sigs +{ + // ResourceLoader.Debug + public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; + public const string ResourceManager = "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0"; + + // ResourceLoader.Replacement + public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00"; + public const string GetResourceAsync = "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00"; + public const string ReadFile = "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05"; + public const string ReadSqPack = "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3"; + + // ResourceLoader.TexMdl + public const string CheckFileState = "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24"; + public const string LoadTexFileLocal = "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20"; + public const string LoadMdlFileLocal = "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00"; + public const string LoadTexFileExtern = "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8"; + public const string LoadMdlFileExtern = "E8 ?? ?? ?? ?? EB 02 B0 F1"; + + // CutsceneCharacters + public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; + + // IdentifiedCollectionCache + public const string CharacterDestructor = + "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05"; + + // PathResolver.AnimationState + public const string LoadCharacterSound = "4C 89 4C 24 ?? 55 57 41 56"; + public const string LoadTimelineResources = "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87"; + public const string CharacterBaseLoadAnimation = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05"; + public const string LoadSomePap = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51"; + public const string LoadSomeAction = "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E"; + public const string LoadCharacterVfx = "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93"; + public const string LoadAreaVfx = "48 8B C4 53 55 56 57 41 56 48 81 EC"; + public const string ScheduleClipUpdate = "40 53 55 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B F9"; + + // PathResolver.DrawObjectState + public const string CharacterBaseCreate = "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40"; + + public const string CharacterBaseDestructor = + "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30"; + + public const string EnableDraw = "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0"; + public const string WeaponReload = "E8 ?? ?? ?? ?? 44 8B 9F"; + + // PathResolver.Meta + public const string UpdateModel = "48 8B ?? 56 48 83 ?? ?? ?? B9"; + public const string GetEqpIndirect = "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C"; + public const string SetupVisor = "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B"; + public const string RspSetupCharacter = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56"; + public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86"; + + // PathResolver.PathState + public const string HumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1"; + + public const string WeaponVTable = + "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B"; + + public const string DemiHumanVTable = "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA"; + public const string MonsterVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83"; + + // PathResolver.Subfiles + public const string LoadMtrlTex = "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55"; + + public const string LoadMtrlShpk = + "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89"; + + public const string ApricotResourceLoad = "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9"; + + // CharacterUtility + public const string CharacterUtility = "48 8B 05 ?? ?? ?? ?? 83 B9"; + public const string LoadCharacterResources = "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0"; + + // MetaFileManager + public const string GetFileSpace = "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0"; + + // ResidentResourceManager + public const string ResidentResourceManager = "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05"; + + public const string LoadPlayerResources = + "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A"; + + public const string UnloadPlayerResources = + "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08"; + + // ActorManager + public const string InspectTitleId = "0F B7 0D ?? ?? ?? ?? C7 85"; + public const string InspectWorldId = "0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0"; +} diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 63d08bc0..de02f091 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Utility.Signatures; +using Penumbra.GameData; namespace Penumbra.Interop; @@ -10,13 +11,13 @@ public unsafe partial class CharacterUtility : IDisposable public record struct InternalIndex( int Value ); // A static pointer to the CharacterUtility address. - [Signature( "48 8B 05 ?? ?? ?? ?? 83 B9", ScanType = ScanType.StaticAddress )] + [Signature( Sigs.CharacterUtility, ScanType = ScanType.StaticAddress )] private readonly Structs.CharacterUtility** _characterUtilityAddress = null; // Only required for migration anymore. public delegate void LoadResources( Structs.CharacterUtility* address ); - [Signature( "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0" )] + [Signature( Sigs.LoadCharacterResources )] public readonly LoadResources LoadCharacterResourcesFunc = null!; public void LoadCharacterResources() diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 76eba25d..64ae0026 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,6 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String; using Penumbra.String.Classes; @@ -22,8 +23,7 @@ public unsafe partial class ResourceLoader public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); - [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", - DetourName = nameof( ResourceHandleDestructorDetour ) )] + [Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )] public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) @@ -37,7 +37,7 @@ public unsafe partial class ResourceLoader } // A static pointer to the SE Resource Manager - [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0", ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] + [Signature( Sigs.ResourceManager, ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] public static ResourceManager** ResourceManager; // Gather some debugging data about penumbra-loaded objects. diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index b4505ce7..b02cfff6 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -7,6 +7,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; @@ -38,14 +39,14 @@ public unsafe partial class ResourceLoader public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams ); - [Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )] - public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; + [Signature( Sigs.GetResourceSync, DetourName = nameof( GetResourceSyncDetour ) )] + public readonly Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown ); - [Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )] - public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; + [Signature( Sigs.GetResourceAsync, DetourName = nameof( GetResourceAsyncDetour ) )] + public readonly Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams ) @@ -79,7 +80,7 @@ public unsafe partial class ResourceLoader return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); } - internal ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { using var performance = Penumbra.Performance.Measure( PerformanceType.GetResourceHandler ); @@ -157,14 +158,14 @@ public unsafe partial class ResourceLoader public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ); - [Signature( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" )] - public ReadFileDelegate ReadFile = null!; + [Signature( Sigs.ReadFile )] + public readonly ReadFileDelegate ReadFile = null!; // We hook ReadSqPack to redirect rooted files to ReadFile. public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = nameof( ReadSqPackDetour ) )] - public Hook< ReadSqPackPrototype > ReadSqPackHook = null!; + [Signature( Sigs.ReadSqPack, DetourName = nameof( ReadSqPackDetour ) )] + public readonly Hook< ReadSqPackPrototype > ReadSqPackHook = null!; private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 415b18f7..7baced91 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; @@ -26,8 +27,8 @@ public unsafe partial class ResourceLoader // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof( CheckFileStateDetour ) )] - public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; + [Signature( Sigs.CheckFileState, DetourName = nameof( CheckFileStateDetour ) )] + public readonly Hook< CheckFileStatePrototype > CheckFileStateHook = null!; private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) => _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 ); @@ -36,20 +37,20 @@ public unsafe partial class ResourceLoader // We use the local functions for our own files in the extern hook. public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 ); - [Signature( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" )] - public LoadTexFileLocalDelegate LoadTexFileLocal = null!; + [Signature( Sigs.LoadTexFileLocal )] + public readonly LoadTexFileLocalDelegate LoadTexFileLocal = null!; public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 ); - [Signature( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" )] - public LoadMdlFileLocalPrototype LoadMdlFileLocal = null!; + [Signature( Sigs.LoadMdlFileLocal )] + public readonly LoadMdlFileLocalPrototype LoadMdlFileLocal = null!; // We hook the extern functions to just return the local one if given the custom flag as last argument. public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof( LoadTexFileExternDetour ) )] - public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; + [Signature( Sigs.LoadTexFileExtern, DetourName = nameof( LoadTexFileExternDetour ) )] + public readonly Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) => ptr.Equals( CustomFileFlag ) @@ -59,8 +60,8 @@ public unsafe partial class ResourceLoader public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof( LoadMdlFileExternDetour ) )] - public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; + [Signature( Sigs.LoadMdlFileExtern, DetourName = nameof( LoadMdlFileExternDetour ) )] + public readonly Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) => ptr.Equals( CustomFileFlag ) diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs index 880a0ee8..c49efa1e 100644 --- a/Penumbra/Interop/MetaFileManager.cs +++ b/Penumbra/Interop/MetaFileManager.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; +using Penumbra.GameData; namespace Penumbra.Interop; @@ -13,7 +14,7 @@ public unsafe class MetaFileManager // Allocate in the games space for file storage. // We only need this if using any meta file. - [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] + [Signature( Sigs.GetFileSpace )] private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero; public IMemorySpace* GetFileSpace() diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 31b2c6dc..36415696 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -7,7 +7,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; namespace Penumbra.Interop; diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs index 37ed1bfe..84063fa6 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -1,21 +1,24 @@ using Dalamud.Utility.Signatures; +using Penumbra.GameData; namespace Penumbra.Interop; public unsafe class ResidentResourceManager { + // A static pointer to the resident resource manager address. + [Signature( Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress )] + private readonly Structs.ResidentResourceManager** _residentResourceManagerAddress = null; + // Some attach and physics files are stored in the resident resource manager, and we need to manually trigger a reload of them to get them to apply. public delegate void* ResidentResourceDelegate( void* residentResourceManager ); - [Signature( "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" )] - public ResidentResourceDelegate LoadPlayerResources = null!; + [Signature( Sigs.LoadPlayerResources )] + public readonly ResidentResourceDelegate LoadPlayerResources = null!; + + [Signature( Sigs.UnloadPlayerResources )] + public readonly ResidentResourceDelegate UnloadPlayerResources = null!; - [Signature( "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" )] - public ResidentResourceDelegate UnloadPlayerResources = null!; - // A static pointer to the resident resource manager address. - [Signature( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05", ScanType = ScanType.StaticAddress )] - private readonly Structs.ResidentResourceManager** _residentResourceManagerAddress = null; public Structs.ResidentResourceManager* Address => *_residentResourceManagerAddress; diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index b055320a..7a3b9cf7 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData; namespace Penumbra.Interop.Resolver; @@ -96,7 +97,7 @@ public class CutsceneCharacters : IDisposable private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F", DetourName = nameof( CopyCharacterDetour ) )] + [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index a6692839..12fd6749 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -6,6 +6,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Actors; namespace Penumbra.Interop.Resolver; @@ -117,8 +118,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt private delegate void CharacterDestructorDelegate( Character* character ); - [Signature( "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05", - DetourName = nameof( CharacterDestructorDetour ) )] + [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] private Hook< CharacterDestructorDelegate > _characterDtorHook = null!; private void CharacterDestructorDetour( Character* character ) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 17823782..022074af 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -1,9 +1,10 @@ using System; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -109,7 +110,7 @@ public unsafe partial class PathResolver // Characters load some of their voice lines or whatever with this function. private delegate IntPtr LoadCharacterSound( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ); - [Signature( "4C 89 4C 24 ?? 55 57 41 56", DetourName = nameof( LoadCharacterSoundDetour ) )] + [Signature( Sigs.LoadCharacterSound, DetourName = nameof( LoadCharacterSoundDetour ) )] private readonly Hook< LoadCharacterSound > _loadCharacterSoundHook = null!; private IntPtr LoadCharacterSoundDetour( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ) @@ -149,7 +150,7 @@ public unsafe partial class PathResolver // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); - [Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )] + [Signature( Sigs.LoadTimelineResources, DetourName = nameof( LoadTimelineResourcesDetour ) )] private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!; private ulong LoadTimelineResourcesDetour( IntPtr timeline ) @@ -166,8 +167,7 @@ public unsafe partial class PathResolver // Make it aware of the correct collection to load the correct pap files. private delegate void CharacterBaseNoArgumentDelegate( IntPtr drawBase ); - [Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", - DetourName = nameof( CharacterBaseLoadAnimationDetour ) )] + [Signature( Sigs.CharacterBaseLoadAnimation, DetourName = nameof( CharacterBaseLoadAnimationDetour ) )] private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!; private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) @@ -186,8 +186,7 @@ public unsafe partial class PathResolver // Unknown what exactly this is but it seems to load a bunch of paps. private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); - [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51", - DetourName = nameof( LoadSomePapDetour ) )] + [Signature( Sigs.LoadSomePap, DetourName = nameof( LoadSomePapDetour ) )] private readonly Hook< LoadSomePap > _loadSomePapHook = null!; private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) @@ -209,7 +208,7 @@ public unsafe partial class PathResolver } // Seems to load character actions when zoning or changing class, maybe. - [Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )] + [Signature( Sigs.LoadSomeAction, DetourName = nameof( SomeActionLoadDetour ) )] private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!; private void SomeActionLoadDetour( IntPtr gameObject ) @@ -221,25 +220,9 @@ public unsafe partial class PathResolver _animationLoadData = last; } - [StructLayout( LayoutKind.Explicit )] - private struct VfxParams - { - [FieldOffset( 0x118 )] - public uint GameObjectId; - - [FieldOffset( 0x11C )] - public byte GameObjectType; - - [FieldOffset( 0xD0 )] - public ushort TargetCount; - - [FieldOffset( 0x120 )] - public fixed ulong Target[16]; - } - private delegate IntPtr LoadCharacterVfxDelegate( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ); - [Signature( "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93", DetourName = nameof( LoadCharacterVfxDetour ) )] + [Signature( Sigs.LoadCharacterVfx, DetourName = nameof( LoadCharacterVfxDetour ) )] private readonly Hook< LoadCharacterVfxDelegate > _loadCharacterVfxHook = null!; private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) @@ -288,7 +271,7 @@ public unsafe partial class PathResolver private delegate IntPtr LoadAreaVfxDelegate( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ); - [Signature( "48 8B C4 53 55 56 57 41 56 48 81 EC", DetourName = nameof( LoadAreaVfxDetour ) )] + [Signature( Sigs.LoadAreaVfx, DetourName = nameof( LoadAreaVfxDetour ) )] private readonly Hook< LoadAreaVfxDelegate > _loadAreaVfxHook = null!; private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ) @@ -314,20 +297,9 @@ public unsafe partial class PathResolver } - - [StructLayout( LayoutKind.Explicit )] - private struct ClipScheduler - { - [FieldOffset( 0 )] - public IntPtr* VTable; - - [FieldOffset( 0x38 )] - public IntPtr SchedulerTimeline; - } - private delegate void ScheduleClipUpdate( ClipScheduler* x ); - [Signature( "40 53 55 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B F9", DetourName = nameof( ScheduleClipUpdateDetour ) )] + [Signature( Sigs.ScheduleClipUpdate, DetourName = nameof( ScheduleClipUpdateDetour ) )] private readonly Hook< ScheduleClipUpdate > _scheduleClipUpdateHook = null!; private void ScheduleClipUpdateDetour( ClipScheduler* x ) diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 78a5ed0a..16c321d4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; using Penumbra.Util; @@ -134,7 +135,7 @@ public unsafe partial class PathResolver // and use the last game object that called EnableDraw to link them. private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); - [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = nameof( CharacterBaseCreateDetour ) )] + [Signature( Sigs.CharacterBaseCreate, DetourName = nameof( CharacterBaseCreateDetour ) )] private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!; private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) @@ -186,8 +187,7 @@ public unsafe partial class PathResolver // Remove DrawObjects from the list when they are destroyed. private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); - [Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", - DetourName = nameof( CharacterBaseDestructorDetour ) )] + [Signature( Sigs.CharacterBaseDestructor, DetourName = nameof( CharacterBaseDestructorDetour ) )] private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!; private void CharacterBaseDestructorDetour( IntPtr drawBase ) @@ -201,7 +201,7 @@ public unsafe partial class PathResolver // so we always keep track of the current GameObject to be able to link it to the DrawObject. private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); - [Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0", DetourName = nameof( EnableDrawDetour ) )] + [Signature( Sigs.EnableDraw, DetourName = nameof( EnableDrawDetour ) )] private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) @@ -216,7 +216,7 @@ public unsafe partial class PathResolver // so we use that. private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); - [Signature( "E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof( WeaponReloadDetour ) )] + [Signature( Sigs.WeaponReload, DetourName = nameof( WeaponReloadDetour ) )] private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!; public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index a618e705..0970347c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -5,6 +5,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; @@ -87,7 +88,7 @@ public unsafe partial class PathResolver private delegate void UpdateModelDelegate( IntPtr drawObject ); - [Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = nameof( UpdateModelsDetour ) )] + [Signature( Sigs.UpdateModel, DetourName = nameof( UpdateModelsDetour ) )] private readonly Hook< UpdateModelDelegate > _updateModelsHook = null!; private void UpdateModelsDetour( IntPtr drawObject ) @@ -98,6 +99,7 @@ public unsafe partial class PathResolver { return; } + using var performance = Penumbra.Performance.Measure( PerformanceType.UpdateModels ); var collection = GetResolveData( drawObject ); @@ -124,8 +126,7 @@ public unsafe partial class PathResolver public static GenderRace GetHumanGenderRace( IntPtr human ) => ( GenderRace )( ( Human* )human )->RaceSexId; - [Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", - DetourName = nameof( GetEqpIndirectDetour ) )] + [Signature( Sigs.GetEqpIndirect, DetourName = nameof( GetEqpIndirectDetour ) )] private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!; private void GetEqpIndirectDetour( IntPtr drawObject ) @@ -136,6 +137,7 @@ public unsafe partial class PathResolver { return; } + using var performance = Penumbra.Performance.Measure( PerformanceType.GetEqp ); var resolveData = GetResolveData( drawObject ); using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); @@ -147,7 +149,7 @@ public unsafe partial class PathResolver // but it only applies a changed gmp file after a redraw for some reason. private delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState ); - [Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = nameof( SetupVisorDetour ) )] + [Signature( Sigs.SetupVisor, DetourName = nameof( SetupVisorDetour ) )] private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!; private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) @@ -161,7 +163,7 @@ public unsafe partial class PathResolver // RSP private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); - [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = nameof( RspSetupCharacterDetour ) )] + [Signature( Sigs.RspSetupCharacter, DetourName = nameof( RspSetupCharacterDetour ) )] private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!; private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) @@ -183,7 +185,7 @@ public unsafe partial class PathResolver private bool _inChangeCustomize; private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment ); - [Signature( "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86", DetourName = nameof( ChangeCustomizeDetour ) )] + [Signature( Sigs.ChangeCustomize, DetourName = nameof( ChangeCustomizeDetour ) )] private readonly Hook< ChangeCustomizeDelegate > _changeCustomize = null!; private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment ) diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 2cf9cb4b..2b4aceca 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Dalamud.Utility.Signatures; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.String; namespace Penumbra.Interop.Resolver; @@ -12,17 +13,16 @@ public unsafe partial class PathResolver { public class PathState : IDisposable { - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )] + [Signature( Sigs.HumanVTable, ScanType = ScanType.StaticAddress )] public readonly IntPtr* HumanVTable = null!; - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B", - ScanType = ScanType.StaticAddress )] + [Signature( Sigs.WeaponVTable, ScanType = ScanType.StaticAddress )] private readonly IntPtr* _weaponVTable = null!; - [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] + [Signature( Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress )] private readonly IntPtr* _demiHumanVTable = null!; - [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] + [Signature( Sigs.MonsterVTable, ScanType = ScanType.StaticAddress )] private readonly IntPtr* _monsterVTable = null!; private readonly ResolverHooks _human; diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index cca76c70..c2743398 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -6,6 +6,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; @@ -159,7 +160,7 @@ public unsafe partial class PathResolver private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); - [Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = nameof( LoadMtrlTexDetour ) )] + [Signature( Sigs.LoadMtrlTex, DetourName = nameof( LoadMtrlTexDetour ) )] private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!; private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) @@ -171,8 +172,7 @@ public unsafe partial class PathResolver return ret; } - [Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", - DetourName = nameof( LoadMtrlShpkDetour ) )] + [Signature( Sigs.LoadMtrlShpk, DetourName = nameof( LoadMtrlShpkDetour ) )] private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!; private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) @@ -197,7 +197,7 @@ public unsafe partial class PathResolver private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); - [Signature( "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9", DetourName = nameof( ApricotResourceLoadDetour ) )] + [Signature( Sigs.ApricotResourceLoad, DetourName = nameof( ApricotResourceLoadDetour ) )] private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs new file mode 100644 index 00000000..d968ffbe --- /dev/null +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct ClipScheduler +{ + [FieldOffset( 0 )] + public IntPtr* VTable; + + [FieldOffset( 0x38 )] + public IntPtr SchedulerTimeline; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/VfxParams.cs b/Penumbra/Interop/Structs/VfxParams.cs new file mode 100644 index 00000000..644d5a9a --- /dev/null +++ b/Penumbra/Interop/Structs/VfxParams.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct VfxParams +{ + [FieldOffset( 0x118 )] + public uint GameObjectId; + + [FieldOffset( 0x11C )] + public byte GameObjectType; + + [FieldOffset( 0xD0 )] + public ushort TargetCount; + + [FieldOffset( 0x120 )] + public fixed ulong Target[16]; +} \ No newline at end of file From aa15ff40e1666b07d712567d5be356918528388b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Jan 2023 14:01:37 +0100 Subject: [PATCH 0690/2451] .net7 --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData/Penumbra.GameData.csproj | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/packages.lock.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OtterGui b/OtterGui index fe73e790..774f1678 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fe73e7901270a9d33afa374c9de930c53580fada +Subproject commit 774f16781b350da5b14612ef4a1409082e6eda5b diff --git a/Penumbra.Api b/Penumbra.Api index 36a06e50..4409ad25 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 36a06e509bf0a7023b4c9b148b06f71f8116cc81 +Subproject commit 4409ad25e76c427692526f0e139d6811b0d138d4 diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index 0aaf9c89..68fcb147 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -1,6 +1,6 @@ - net6.0-windows + net7.0-windows preview x64 Penumbra.GameData diff --git a/Penumbra.String b/Penumbra.String index e8fad797..4f7a112e 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit e8fad7976e3a156a9d3f0c7f9b2a03267bd3c772 +Subproject commit 4f7a112ea9c9b377c265746dc572fa26e371de69 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b7ab0231..8006457b 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,6 +1,6 @@ - net6.0-windows + net7.0-windows preview x64 Penumbra diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index aea52c7e..d116395f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net6.0-windows7.0": { + "net7.0-windows7.0": { "EmbedIO": { "type": "Direct", "requested": "[3.4.3, )", @@ -67,8 +67,8 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.3, )", - "Penumbra.String": "[1.0.0, )" + "Penumbra.Api": "[1.0.5, )", + "Penumbra.String": "[1.0.1, )" } }, "penumbra.string": { From 58f86743ebd0994679b5e85b74eac0ff85d9f099 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Jan 2023 14:03:22 +0100 Subject: [PATCH 0691/2451] API 8. --- Penumbra/Penumbra.json | 2 +- base_repo.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 38d0c056..8c1f6680 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -7,7 +7,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 7, + "DalamudApiLevel": 8, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/base_repo.json b/base_repo.json index 5016b6e4..0b50da65 100644 --- a/base_repo.json +++ b/base_repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "1.0.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 7, + "DalamudApiLevel": 8, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 889fc101a88d39b330fd8ce4d8b4998ba2574741 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 10 Jan 2023 08:52:36 +0100 Subject: [PATCH 0692/2451] Add collection logging to resolve logging. --- Penumbra/Collections/ResolveData.cs | 28 +++++++++++++++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 5 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 722675fd..90c56144 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.String; namespace Penumbra.Collections; @@ -34,6 +37,31 @@ public readonly struct ResolveData public override string ToString() => ModCollection.Name; + + public unsafe string AssociatedName() + { + if( AssociatedGameObject == IntPtr.Zero ) + { + return "no associated object."; + } + + try + { + var id = Penumbra.Actors.FromObject( ( GameObject* )AssociatedGameObject, out _, false, true ); + if( id.IsValid ) + { + var name = id.ToString(); + var parts = name.Split( ' ', 3 ); + return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); + } + } + catch + { + // ignored + } + + return $"0x{AssociatedGameObject:X}"; + } } public static class ResolveDataExtensions diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 64ae0026..6791f5be 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -253,10 +253,11 @@ public unsafe partial class ResourceLoader private static void LogPath( Utf8GamePath path, bool synchronous ) => Penumbra.Log.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData _ ) + private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); - Penumbra.Log.Information( $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); + Penumbra.Log.Information( + $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()}. (Refcount {handle->RefCount}) " ); } private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom ) From 9ccbe1064245156d6ff7fa9d8982744da85a2d49 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 10 Jan 2023 09:42:59 +0100 Subject: [PATCH 0693/2451] Fix signature and CS changes. --- Penumbra.GameData/Signatures.cs | 2 +- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 1 + Penumbra/Interop/Loader/ResourceLoader.cs | 4 ++-- Penumbra/UI/ConfigWindow.ResourceTab.cs | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs index 9f13bdf1..aae686dc 100644 --- a/Penumbra.GameData/Signatures.cs +++ b/Penumbra.GameData/Signatures.cs @@ -43,7 +43,7 @@ public static class Sigs "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30"; public const string EnableDraw = "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0"; - public const string WeaponReload = "E8 ?? ?? ?? ?? 44 8B 9F"; + public const string WeaponReload = "E8 ?? ?? ?? ?? 33 DB BE"; // PathResolver.Meta public const string UpdateModel = "48 8B ?? 56 48 83 ?? ?? ?? B9"; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 6791f5be..a168299e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -5,6 +5,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; using Penumbra.Collections; using Penumbra.GameData; diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index ac00d62d..9a929eaf 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -106,10 +106,10 @@ public unsafe partial class ResourceLoader : IDisposable { SignatureHelper.Initialise( this ); _decRefHook = Hook< ResourceHandleDecRef >.FromAddress( - ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpDecRef, + ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour ); _incRefHook = Hook< ResourceHandleDestructor >.FromAddress( - ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.fpIncRef, ResourceHandleIncRefDetour ); + ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour ); } // Event fired whenever a resource is requested. diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index ad128ee9..ce6bf442 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -4,6 +4,7 @@ using System.Numerics; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; From e00cb6cc6a5752754ebd34cef518208395822109 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 Jan 2023 12:38:59 +0100 Subject: [PATCH 0694/2451] Fix renderflags offset for redrawing. --- Penumbra/Interop/ObjectReloader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 36415696..da96af5b 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -117,7 +117,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable => Dalamud.Framework.Update -= OnUpdateEvent; public static DrawState* ActorDrawState( GameObject actor ) - => ( DrawState* )( actor.Address + 0x0104 ); + => ( DrawState* )( &( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->RenderFlags ); private static int ObjectTableIndex( GameObject actor ) => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; From 9555b4eecb9cf8a9feeca7722c0bf6d85484ba30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 Jan 2023 13:20:36 +0100 Subject: [PATCH 0695/2451] Fix RenderModel offsets. --- Penumbra/Interop/Structs/RenderModel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs index b9e04908..9c8581b0 100644 --- a/Penumbra/Interop/Structs/RenderModel.cs +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -24,18 +24,18 @@ public unsafe struct RenderModel [FieldOffset( 0x60 )] public int BoneListCount; - [FieldOffset( 0x68 )] + [FieldOffset( 0x70 )] private void* UnkDXBuffer1; - [FieldOffset( 0x70 )] + [FieldOffset( 0x78 )] private void* UnkDXBuffer2; - [FieldOffset( 0x78 )] + [FieldOffset( 0x80 )] private void* UnkDXBuffer3; - [FieldOffset( 0x90 )] + [FieldOffset( 0x98 )] public void** Materials; - [FieldOffset( 0x98 )] + [FieldOffset( 0xA0 )] public int MaterialCount; } \ No newline at end of file From c7cb771992beda75bd4e751377a2432b22837f9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 11 Jan 2023 15:12:39 +0100 Subject: [PATCH 0696/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 7eb71846..89892243 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -30,10 +30,31 @@ public partial class ConfigWindow Add6_0_5( ret ); Add6_1_0( ret ); Add6_1_1( ret ); + Add6_2_0( ret ); return ret; } + private static void Add6_2_0( Changelog log ) + => log.NextVersion( "Version 0.6.2.0" ) + .RegisterEntry( "Updated Penumbra for .net7, Dalamud API 8 and patch 6.3." ) + .RegisterEntry( "Added a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)" ) + .RegisterEntry( "Added placeholder options for setting individual collections via chat command." ) + .RegisterEntry( "Added toggles to swap left and/or right rings separately for ring item swap." ) + .RegisterEntry( "Added handling for looping sound effects caused by animations in non-base collections." ) + .RegisterEntry( "Added an option to not use any mods at all in the Inspect/Try-On window." ) + .RegisterEntry( "Added handling for Mahjong actors." ) + .RegisterEntry( "Improved hint text for File Swaps in Advanced Editing, also inverted file swap display order." ) + .RegisterEntry( "Fixed a problem where the collection selectors could get desynchronized after adding or deleting collections." ) + .RegisterEntry( "Fixed a problem that could cause setting state to get desynchronized." ) + .RegisterEntry( "Fixed an oversight where some special screen actors did not actually respect the settings made for them." ) + .RegisterEntry( "Added collection and associated game object to Full Resource Logging." ) + .RegisterEntry( "Added performance tracking for DEBUG-compiled versions (i.e. testing only)." ) + .RegisterEntry( "Added some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)" ) + .RegisterEntry( "Fixed association of some vfx game objects. (0.6.1.3)" ) + .RegisterEntry( "Stopped forcing AVFX files to load synchronously. (0.6.1.3)" ) + .RegisterEntry( "Fixed an issue when incorporating deduplicated meta files. (0.6.1.2)" ); + private static void Add6_1_1( Changelog log ) => log.NextVersion( "Version 0.6.1.1" ) .RegisterEntry( "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod." ) From 79b4415a44968956b5e2193c812a1b1e5f9077b6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 11 Jan 2023 14:15:54 +0000 Subject: [PATCH 0697/2451] [CI] Updating repo.json for refs/tags/0.6.2.0 --- repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/repo.json b/repo.json index 6b4f15ac..8a73e0c7 100644 --- a/repo.json +++ b/repo.json @@ -4,11 +4,11 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.1.3", - "TestingAssemblyVersion": "0.6.1.3", + "AssemblyVersion": "0.6.2.0", + "TestingAssemblyVersion": "0.6.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 7, + "DalamudApiLevel": 8, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.1.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From efdece613a4b3df5e03a2ecb134564495c2892e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 14 Jan 2023 19:57:42 +0100 Subject: [PATCH 0698/2451] Add names, maybe fix combos. --- OtterGui | 2 +- Penumbra/Penumbra.json | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- base_repo.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index 774f1678..d018a822 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 774f16781b350da5b14612ef4a1409082e6eda5b +Subproject commit d018a822212a68492771db1ee6a8921df36f677d diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 8c1f6680..cf7170d8 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -1,5 +1,5 @@ { - "Author": "Adam", + "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 39ab6a6c..83e7d79e 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -130,7 +130,7 @@ public partial class ConfigWindow { var (name, _) = Penumbra.CollectionManager.Individuals[ i ]; using var id = ImRaii.PushId( i ); - CollectionsWithEmpty.Draw( string.Empty, _window._inputTextWidth.X, i ); + CollectionsWithEmpty.Draw( "##IndividualCombo", _window._inputTextWidth.X, i ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index d1769cd3..979ada02 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -225,7 +225,7 @@ public partial class ConfigWindow if( collection != null ) { using var id = ImRaii.PushId( ( int )type ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true ); + DrawCollectionSelector( "##SpecialCombo", _window._inputTextWidth.X, type, true ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) diff --git a/base_repo.json b/base_repo.json index 0b50da65..202d59de 100644 --- a/base_repo.json +++ b/base_repo.json @@ -1,6 +1,6 @@ [ { - "Author": "Adam", + "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", From ff2b9de93e97616a9b9500213a58f8f9ada7db02 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 14 Jan 2023 19:58:55 +0100 Subject: [PATCH 0699/2451] Fix deleting active collections. --- Penumbra/Collections/CollectionManager.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 261a91be..2610c455 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -148,13 +147,22 @@ public partial class ModCollection SetCollection( Empty.Index, CollectionType.Default ); } + for( var i = 0; i < _specialCollections.Length; ++i ) + { + if( idx == _specialCollections[ i ]?.Index ) + { + SetCollection( Empty, ( CollectionType )i ); + } + } + for( var i = 0; i < Individuals.Count; ++i ) { if( Individuals[ i ].Collection.Index == idx ) { - Individuals.ChangeCollection( i, Empty ); + SetCollection( Empty, CollectionType.Individual, i ); } } + var collection = _collections[ idx ]; // Clear own inheritances. From 27fed7860d12af40b5a7b752ea50339cb6ecc341 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 14 Jan 2023 19:59:13 +0100 Subject: [PATCH 0700/2451] Make SubFiles threadlocal. --- .../Interop/Resolver/PathResolver.Subfiles.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index c2743398..3359270f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -25,8 +26,8 @@ public unsafe partial class PathResolver { private readonly ResourceLoader _loader; - private ResolveData _mtrlData = ResolveData.Invalid; - private ResolveData _avfxData = ResolveData.Invalid; + private readonly ThreadLocal< ResolveData > _mtrlData = new(() => ResolveData.Invalid); + private readonly ThreadLocal< ResolveData > _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); @@ -44,15 +45,15 @@ public unsafe partial class PathResolver { case ResourceType.Tex: case ResourceType.Shpk: - if( _mtrlData.Valid ) + if( _mtrlData.Value.Valid ) { - collection = _mtrlData; + collection = _mtrlData.Value; return true; } break; - case ResourceType.Atex when _avfxData.Valid: - collection = _avfxData; + case ResourceType.Atex when _avfxData.Value.Valid: + collection = _avfxData.Value; return true; } @@ -166,9 +167,10 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { using var performance = Penumbra.Performance.Measure( PerformanceType.LoadTextures ); - _mtrlData = LoadFileHelper( mtrlResourceHandle ); + var old = _mtrlData.Value; + _mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); - _mtrlData = ResolveData.Invalid; + _mtrlData.Value = old; return ret; } @@ -178,9 +180,10 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { using var performance = Penumbra.Performance.Measure( PerformanceType.LoadShaders ); - _mtrlData = LoadFileHelper( mtrlResourceHandle ); + var old = _mtrlData.Value; + _mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); - _mtrlData = ResolveData.Invalid; + _mtrlData.Value = old; return ret; } @@ -204,9 +207,10 @@ public unsafe partial class PathResolver private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) { using var performance = Penumbra.Performance.Measure( PerformanceType.LoadApricotResources ); - _avfxData = LoadFileHelper( handle ); + var old = _avfxData.Value; + _avfxData.Value = LoadFileHelper( handle ); var ret = _apricotResourceLoadHook.Original( handle, unk1, unk2 ); - _avfxData = ResolveData.Invalid; + _avfxData.Value = old; return ret; } @@ -220,9 +224,9 @@ public unsafe partial class PathResolver => _subFileCollection.Count; internal ResolveData MtrlData - => _mtrlData; + => _mtrlData.Value; internal ResolveData AvfxData - => _avfxData; + => _avfxData.Value; } } \ No newline at end of file From 79eee0e2c7e961fc9443dc67d025c762f80c8ac7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 14 Jan 2023 20:00:48 +0100 Subject: [PATCH 0701/2451] Rename SpecialActor -> ScreenActor, add new ScreenActors. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 16 ++++++------ .../Actors/ActorManager.Identifiers.cs | 26 +++++++++---------- .../{SpecialActor.cs => ScreenActor.cs} | 7 +++-- .../IndividualCollections.Access.cs | 10 +++---- .../Resolver/PathResolver.Identification.cs | 2 +- 5 files changed, 32 insertions(+), 29 deletions(-) rename Penumbra.GameData/Actors/{SpecialActor.cs => ScreenActor.cs} (66%) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 376a5314..f00ad8f6 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -19,7 +19,7 @@ public readonly struct ActorIdentifier : IEquatable [FieldOffset( 1 )] public readonly ObjectKind Kind; // Npc, Owned [FieldOffset( 2 )] public readonly ushort HomeWorld; // Player, Owned [FieldOffset( 2 )] public readonly ushort Index; // NPC - [FieldOffset( 2 )] public readonly SpecialActor Special; // Special + [FieldOffset( 2 )] public readonly ScreenActor Special; // Special [FieldOffset( 4 )] public readonly uint DataId; // Owned, NPC [FieldOffset( 8 )] public readonly ByteString PlayerName; // Player, Owned // @formatter:on @@ -91,7 +91,7 @@ public readonly struct ActorIdentifier : IEquatable { Type = type; Kind = kind; - Special = (SpecialActor)index; + Special = (ScreenActor)index; HomeWorld = Index = index; DataId = data; PlayerName = playerName; @@ -189,14 +189,14 @@ public static class ActorManagerExtensions /// /// Fixed names for special actors. /// - public static string ToName(this SpecialActor actor) + public static string ToName(this ScreenActor actor) => actor switch { - SpecialActor.CharacterScreen => "Character Screen Actor", - SpecialActor.ExamineScreen => "Examine Screen Actor", - SpecialActor.FittingRoom => "Fitting Room Actor", - SpecialActor.DyePreview => "Dye Preview Actor", - SpecialActor.Portrait => "Portrait Actor", + ScreenActor.CharacterScreen => "Character Screen Actor", + ScreenActor.ExamineScreen => "Examine Screen Actor", + ScreenActor.FittingRoom => "Fitting Room Actor", + ScreenActor.DyePreview => "Dye Preview Actor", + ScreenActor.Portrait => "Portrait Actor", _ => "Invalid", }; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 2483fc5e..59d6c134 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -45,7 +45,7 @@ public partial class ActorManager } case IdentifierType.Special: { - var special = data[nameof(ActorIdentifier.Special)]?.ToObject() ?? 0; + var special = data[nameof(ActorIdentifier.Special)]?.ToObject() ?? 0; return CreateSpecial(special); } case IdentifierType.Npc: @@ -97,7 +97,7 @@ public partial class ActorManager if (main == null) return null; - if (main->ObjectIndex is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd) + if (main->ObjectIndex is >= (ushort)ScreenActor.CutsceneStart and < (ushort)ScreenActor.CutsceneEnd) { var parentIdx = _toParentIdx(main->ObjectIndex); if (parentIdx >= 0) @@ -197,8 +197,7 @@ public partial class ActorManager return FindDataId(split[1], Data.BNpcs, out id) ? (ObjectKind.BattleNpc, id) : throw new IdentifierParseError($"Could not identify a Battle NPC named {split[1]}."); - default: - throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type."); + default: throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type."); } } @@ -228,6 +227,7 @@ public partial class ActorManager if (split.Length < 3) throw new IdentifierParseError( "Owned NPCs need a NPC and a player, separated by '|', but only one was provided."); + type = IdentifierType.Owned; (kind, objectId) = ParseNpc(split[1]); (playerName, worldId) = ParsePlayer(split[2]); @@ -252,7 +252,7 @@ public partial class ActorManager actor = HandleCutscene(actor); var idx = actor->ObjectIndex; - if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) + if (idx is >= (ushort)ScreenActor.CharacterScreen and <= (ushort)ScreenActor.Card8) return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue); var kind = (ObjectKind)actor->ObjectKind; @@ -390,7 +390,7 @@ public partial class ActorManager IdentifierType.Player => CreatePlayer(name, homeWorld), IdentifierType.Retainer => CreateRetainer(name), IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), - IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), + IdentifierType.Special => CreateSpecial((ScreenActor)homeWorld), IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0), _ => ActorIdentifier.Invalid, @@ -418,7 +418,7 @@ public partial class ActorManager return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, 0, 0, name); } - public ActorIdentifier CreateSpecial(SpecialActor actor) + public ActorIdentifier CreateSpecial(ScreenActor actor) { if (!VerifySpecial(actor)) return ActorIdentifier.Invalid; @@ -545,18 +545,18 @@ public partial class ActorManager => worldId == ushort.MaxValue || Data.Worlds.ContainsKey(worldId); /// Verify that the enum value is a specific actor and return the name if it is. - public static bool VerifySpecial(SpecialActor actor) - => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; + public static bool VerifySpecial(ScreenActor actor) + => actor is >= ScreenActor.CharacterScreen and <= ScreenActor.Card8; /// Verify that the object index is a valid index for an NPC. public static bool VerifyIndex(ushort index) { return index switch { - ushort.MaxValue => true, - < 200 => index % 2 == 0, - > (ushort)SpecialActor.Portrait => index < 426, - _ => false, + ushort.MaxValue => true, + < 200 => index % 2 == 0, + > (ushort)ScreenActor.Card8 => index < 426, + _ => false, }; } diff --git a/Penumbra.GameData/Actors/SpecialActor.cs b/Penumbra.GameData/Actors/ScreenActor.cs similarity index 66% rename from Penumbra.GameData/Actors/SpecialActor.cs rename to Penumbra.GameData/Actors/ScreenActor.cs index 5319c747..bc046407 100644 --- a/Penumbra.GameData/Actors/SpecialActor.cs +++ b/Penumbra.GameData/Actors/ScreenActor.cs @@ -1,6 +1,6 @@ namespace Penumbra.GameData.Actors; -public enum SpecialActor : ushort +public enum ScreenActor : ushort { CutsceneStart = 200, CutsceneEnd = 240, @@ -9,4 +9,7 @@ public enum SpecialActor : ushort FittingRoom = 242, DyePreview = 243, Portrait = 244, -} \ No newline at end of file + Card6 = 245, + Card7 = 246, + Card8 = 247, +} diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 463907bb..0d96fc62 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -88,12 +88,12 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ switch( identifier.Special ) { - case SpecialActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: - case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: - case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: - case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: + case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: + case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: + case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: + case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: return _actorManager.GetCurrentPlayer(); - case SpecialActor.ExamineScreen: + case ScreenActor.ExamineScreen: { identifier = _actorManager.GetInspectPlayer(); if( identifier.IsValid ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index d01e9962..404f6c90 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -106,7 +106,7 @@ public unsafe partial class PathResolver } var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); - if( Penumbra.Config.UseNoModsInInspect && identifier.Type == IdentifierType.Special && identifier.Special == SpecialActor.ExamineScreen ) + if( Penumbra.Config.UseNoModsInInspect && identifier.Type == IdentifierType.Special && identifier.Special == ScreenActor.ExamineScreen ) { return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); } From 4059e0630a267d0b1f2609081f02a61adf4bd129 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Jan 2023 13:02:22 +0100 Subject: [PATCH 0702/2451] Fix companion identification, extract offsets and vtable indices to separate file. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 2 +- .../Actors/ActorManager.Identifiers.cs | 12 +++---- .../Data/ObjectIdentification.cs | 12 +++---- Penumbra.GameData/Offsets.cs | 35 +++++++++++++++++++ Penumbra/Interop/FontReloader.cs | 3 +- Penumbra/Interop/ObjectReloader.cs | 5 +-- .../Resolver/PathResolver.AnimationState.cs | 4 +-- .../Interop/Resolver/PathResolver.Meta.cs | 4 +-- Penumbra/Interop/Structs/ResourceHandle.cs | 5 +-- 9 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 Penumbra.GameData/Offsets.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 5dd6d696..9828fe5f 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -204,7 +204,7 @@ public sealed partial class ActorManager : IDisposable if (agent == null || agent->Data == null) return ActorIdentifier.Invalid; - var worldId = *(ushort*)((byte*)agent->Data + 0xC0); + var worldId = *(ushort*)((byte*)agent->Data + Offsets.AgentCharaCardDataWorldId); return CreatePlayer(new ByteString(agent->Data->Name.StringPtr), worldId); } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 59d6c134..d5e0c30e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -306,7 +306,7 @@ public partial class ActorManager { var dataId = actor->DataID; // Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF - if (dataId == 0xf845d) + if (dataId == 0xF845D) dataId = actor->GetNpcID(); if (MannequinIds.Contains(dataId)) { @@ -338,7 +338,7 @@ public partial class ActorManager if (owner == null) return ActorIdentifier.Invalid; - var dataId = GetCompanionId(actor, owner); + var dataId = GetCompanionId(actor, (Character*) owner); var name = new ByteString(owner->Name); var homeWorld = ((Character*)owner)->HomeWorld; return check @@ -365,13 +365,13 @@ public partial class ActorManager /// Obtain the current companion ID for an object by its actor and owner. /// private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) // TODO: CS Update + Character* owner) // TODO: CS Update { return (ObjectKind)actor->ObjectKind switch { - ObjectKind.MountType => *(ushort*)((byte*)owner + 0x650 + 0x18), - (ObjectKind)15 => *(ushort*)((byte*)owner + 0x860 + 0x18), - ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC), + ObjectKind.MountType => owner->Mount.MountId, + (ObjectKind)15 => owner->Ornament.OrnamentId, + ObjectKind.Companion => actor->DataID, _ => actor->DataID, }; } diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 0f5ec4aa..4dfe21f2 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -400,17 +400,17 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) { - var type = (*(delegate* unmanaged**)drawObject)[50](drawObject); - var unk = (ulong)*((byte*)drawObject + 0x8E8) << 8; + var type = (*(delegate* unmanaged**)drawObject)[Offsets.DrawObjectGetModelTypeVfunc](drawObject); + var unk = (ulong)*((byte*)drawObject + Offsets.DrawObjectModelUnk1) << 8; return type switch { 1 => type | unk, - 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x908) << 16), + 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 16), 3 => type | unk - | ((ulong)*(ushort*)((byte*)drawObject + 0x8F0) << 16) - | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 32) - | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 40), + | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk2) << 16) + | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk4) << 32) + | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 40), _ => 0u, }; } diff --git a/Penumbra.GameData/Offsets.cs b/Penumbra.GameData/Offsets.cs new file mode 100644 index 00000000..00c1c8e2 --- /dev/null +++ b/Penumbra.GameData/Offsets.cs @@ -0,0 +1,35 @@ +namespace Penumbra.GameData; + +public static class Offsets +{ + // ActorManager.Data + public const int AgentCharaCardDataWorldId = 0xC0; + + // ObjectIdentification + public const int DrawObjectGetModelTypeVfunc = 50; + private const int DrawObjectModelBase = 0x8E8; + public const int DrawObjectModelUnk1 = DrawObjectModelBase; + public const int DrawObjectModelUnk2 = DrawObjectModelBase + 0x08; + public const int DrawObjectModelUnk3 = DrawObjectModelBase + 0x20; + public const int DrawObjectModelUnk4 = DrawObjectModelBase + 0x28; + + // PathResolver.AnimationState + public const int GetGameObjectIdxVfunc = 28; + public const int TimeLinePtr = 0x50; + + // PathResolver.Meta + public const int UpdateModelSkip = 0x90c; + public const int GetEqpIndirectSkip1 = 0xA30; + public const int GetEqpIndirectSkip2 = 0xA28; + + // FontReloader + public const int ReloadFontsVfunc = 43; + + // ObjectReloader + public const int EnableDrawVfunc = 16; + public const int DisableDrawVfunc = 17; + + // ResourceHandle + public const int ResourceHandleGetDataVfunc = 23; + public const int ResourceHandleGetLengthVfunc = 17; +} diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/FontReloader.cs index e084f70c..64e5db96 100644 --- a/Penumbra/Interop/FontReloader.cs +++ b/Penumbra/Interop/FontReloader.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; +using Penumbra.GameData; namespace Penumbra.Interop; @@ -51,6 +52,6 @@ public static unsafe class FontReloader } AtkModule = &atkModule->AtkModule; - ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ 43 ]; + ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ Offsets.ReloadFontsVfunc ]; } } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index da96af5b..3f7adaf1 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -7,6 +7,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.GameData; using Penumbra.Interop.Structs; namespace Penumbra.Interop; @@ -23,10 +24,10 @@ public unsafe partial class ObjectReloader // 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 ); + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ Offsets.DisableDrawVfunc ]( actor.Address ); private static void EnableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ Offsets.EnableDrawVfunc ]( actor.Address ); // Check whether we currently are in GPose. // Also clear the name list. diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 022074af..fcd5c2a0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -129,7 +129,7 @@ public unsafe partial class PathResolver { if( timeline != IntPtr.Zero ) { - var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; + var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ]; var idx = getGameObjectIdx( timeline ); if( idx >= 0 && idx < Dalamud.Objects.Length ) { @@ -192,7 +192,7 @@ public unsafe partial class PathResolver private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) { using var performance = Penumbra.Performance.Measure( PerformanceType.LoadPap ); - var timelinePtr = a1 + 0x50; + var timelinePtr = a1 + Offsets.TimeLinePtr; var last = _animationLoadData; if( timelinePtr != IntPtr.Zero ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 0970347c..c3678af5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -95,7 +95,7 @@ public unsafe partial class PathResolver { // Shortcut because this is called all the time. // Same thing is checked at the beginning of the original function. - if( *( int* )( drawObject + 0x90c ) == 0 ) + if( *( int* )( drawObject + Offsets.UpdateModelSkip ) == 0 ) { return; } @@ -133,7 +133,7 @@ public unsafe partial class PathResolver { // Shortcut because this is also called all the time. // Same thing is checked at the beginning of the original function. - if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) + if( ( *( byte* )( drawObject + Offsets.GetEqpIndirectSkip1 ) & 1 ) == 0 || *( ulong* )( drawObject + Offsets.GetEqpIndirectSkip2 ) == 0 ) { return; } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 329dbc2b..59b4b942 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData; using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; @@ -87,10 +88,10 @@ public unsafe struct ResourceHandle // May return null. public static byte* GetData( ResourceHandle* handle ) - => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ Offsets.ResourceHandleGetDataVfunc ] )( handle ); public static ulong GetLength( ResourceHandle* handle ) - => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ Offsets.ResourceHandleGetLengthVfunc ] )( handle ); // Only use these if you know what you are doing. From 123ed256b129cf22b001f0920dbaccbddaaea2c1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Jan 2023 13:03:26 +0100 Subject: [PATCH 0703/2451] Try to resolve banner players better. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 107 +++++++++++++++++- .../IndividualCollections.Access.cs | 7 ++ .../Resolver/PathResolver.Identification.cs | 16 ++- 3 files changed, 119 insertions(+), 11 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 9828fe5f..97655e16 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -5,20 +5,22 @@ using System.Linq; using System.Text; using Dalamud; using Dalamud.Data; -using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Gui; using Dalamud.Plugin; using Dalamud.Utility; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Group; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.GeneratedSheets; using Lumina.Text; using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.String; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.GameData.Actors; @@ -198,6 +200,107 @@ public sealed partial class ActorManager : IDisposable return CreatePlayer(InspectName, InspectWorldId); } + public unsafe bool ResolvePartyBannerPlayer(ScreenActor type, out ActorIdentifier id) + { + id = ActorIdentifier.Invalid; + var addon = _gameGui.GetAddonByName("BannerParty"); + if (addon == IntPtr.Zero) + return false; + + var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen; + if (idx is < 0 or > 7) + return true; + + if (idx == 0) + { + id = GetCurrentPlayer(); + return true; + } + + var obj = GroupManager.Instance()->GetPartyMemberByIndex(idx - 1); + if (obj != null) + id = CreatePlayer(new ByteString(obj->Name), obj->HomeWorld); + + return true; + } + + private unsafe bool SearchPlayerCustomize(Character* character, int idx, out ActorIdentifier id) + { + var other = (Character*)_objects.GetObjectAddress(idx); + if (other == null || !CustomizeData.Equals((CustomizeData*)character->CustomizeData, (CustomizeData*)other->CustomizeData)) + { + id = ActorIdentifier.Invalid; + return false; + } + + id = FromObject(&other->GameObject, out _, false, true); + return true; + } + + private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject, int idx1, int idx2, int idx3) + => SearchPlayerCustomize(gameObject, idx1, out var ret) + || SearchPlayerCustomize(gameObject, idx2, out ret) + || SearchPlayerCustomize(gameObject, idx3, out ret) + ? ret + : ActorIdentifier.Invalid; + + private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject) + { + for (var i = 0; i < (int)ScreenActor.CutsceneStart; i += 2) + { + var obj = (GameObject*)_objects.GetObjectAddress(i); + if (obj != null + && obj->ObjectKind is (byte)ObjectKind.Player + && !CustomizeData.Equals((CustomizeData*)gameObject->CustomizeData, (CustomizeData*)((Character*)obj)->CustomizeData)) + return FromObject(obj, out _, false, true); + } + return ActorIdentifier.Invalid; + } + + public unsafe bool ResolveMahjongPlayer(ScreenActor type, out ActorIdentifier id) + { + id = ActorIdentifier.Invalid; + if (_clientState.TerritoryType != 831 && _gameGui.GetAddonByName("EmjIntro") == IntPtr.Zero) + return false; + + var obj = (Character*)_objects.GetObjectAddress((int)type); + if (obj == null) + return false; + + id = type switch + { + ScreenActor.CharacterScreen => GetCurrentPlayer(), + ScreenActor.ExamineScreen => SearchPlayersCustomize(obj, 2, 4, 6), + ScreenActor.FittingRoom => SearchPlayersCustomize(obj, 4, 2, 6), + ScreenActor.DyePreview => SearchPlayersCustomize(obj, 6, 2, 4), + _ => ActorIdentifier.Invalid, + }; + return true; + } + + public unsafe bool ResolvePvPBannerPlayer(ScreenActor type, out ActorIdentifier id) + { + id = ActorIdentifier.Invalid; + var addon = _gameGui.GetAddonByName("PvPMKSIntroduction"); + if (addon == IntPtr.Zero) + return false; + + var obj = (Character*)_objects.GetObjectAddress((int)type); + if (obj == null) + return false; + + var identifier = type switch + { + ScreenActor.CharacterScreen => SearchPlayersCustomize(obj), + ScreenActor.ExamineScreen => SearchPlayersCustomize(obj), + ScreenActor.FittingRoom => SearchPlayersCustomize(obj), + ScreenActor.DyePreview => SearchPlayersCustomize(obj), + ScreenActor.Portrait => SearchPlayersCustomize(obj), + _ => ActorIdentifier.Invalid, + }; + return true; + } + public unsafe ActorIdentifier GetCardPlayer() { var agent = AgentCharaCard.Instance(); diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 0d96fc62..73e06fef 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -86,6 +86,13 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return identifier; } + if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) + || _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) + || _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) ) + { + return identifier; + } + switch( identifier.Special ) { case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 404f6c90..abf85749 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -88,12 +88,6 @@ public unsafe partial class PathResolver return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } - // Mahjong special case. - if( Dalamud.ClientState.TerritoryType == 831 ) - { - return IdentifyMahjong( gameObject ); - } - // Aesthetician. The relevant actor is yourself, so use player collection when possible. if( Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) { @@ -106,12 +100,16 @@ public unsafe partial class PathResolver } var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); - if( Penumbra.Config.UseNoModsInInspect && identifier.Type == IdentifierType.Special && identifier.Special == ScreenActor.ExamineScreen ) + if( identifier.Type is IdentifierType.Special ) { - return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); + if( Penumbra.Config.UseNoModsInInspect && identifier.Special == ScreenActor.ExamineScreen ) + { + return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); + } + + identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); } - identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); var collection = CollectionByIdentifier( identifier ) ?? CheckYourself( identifier, gameObject ) ?? CollectionByAttributes( gameObject ) From 80f02e537762535931ca07f5e332a9bcfa4b2cca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Jan 2023 17:42:26 +0100 Subject: [PATCH 0704/2451] Add API/IPC for collection handling. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 45 +++++ Penumbra/Api/PenumbraApi.cs | 140 +++++++++++++++- Penumbra/Api/PenumbraIpcProviders.cs | 26 ++- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionType.cs | 156 +++++++++--------- 6 files changed, 284 insertions(+), 87 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 4409ad25..866f4c45 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 4409ad25e76c427692526f0e139d6811b0d138d4 +Subproject commit 866f4c45bd21219a31d044c5eb55b162ee2bc0e2 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 1b4c1d1d..2897c380 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -9,8 +9,10 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; +using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; @@ -683,10 +685,18 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; + private int _objectIdx = 0; + private string _collectionName = string.Empty; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Current; + private string _characterCollectionName = string.Empty; private IList< string > _collections = new List< string >(); private string _changedItemCollection = string.Empty; private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >(); + private PenumbraApiEc _returnCode = PenumbraApiEc.Success; + private string? _oldCollection = null; public Collections( DalamudPluginInterface pi ) => _pi = pi; @@ -699,12 +709,25 @@ public class IpcTester : IDisposable return; } + ImGuiUtil.GenericEnumCombo( "Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName() ); + ImGui.InputInt( "Object Index##Collections", ref _objectIdx, 0, 0 ); + ImGui.InputText( "Collection Name##Collections", ref _collectionName, 64 ); + ImGui.Checkbox( "Allow Assignment Creation", ref _allowCreation ); + ImGui.SameLine(); + ImGui.Checkbox( "Allow Assignment Deletion", ref _allowDeletion ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; } + DrawIntro( "Last Return Code", _returnCode.ToString() ); + if( _oldCollection != null ) + { + ImGui.TextUnformatted( _oldCollection.Length == 0 ? "Created" : _oldCollection ); + } + DrawIntro( Ipc.GetCurrentCollectionName.Label, "Current Collection" ); ImGui.TextUnformatted( Ipc.GetCurrentCollectionName.Subscriber( _pi ).Invoke() ); DrawIntro( Ipc.GetDefaultCollectionName.Label, "Default Collection" ); @@ -725,6 +748,27 @@ public class IpcTester : IDisposable ImGui.OpenPopup( "Collections" ); } + DrawIntro( Ipc.GetCollectionForType.Label, "Get Special Collection" ); + var name = Ipc.GetCollectionForType.Subscriber( _pi ).Invoke( _type ); + ImGui.TextUnformatted( name.Length == 0 ? "Unassigned" : name ); + DrawIntro( Ipc.SetCollectionForType.Label, "Set Special Collection" ); + if( ImGui.Button( "Set##TypeCollection" ) ) + { + ( _returnCode, _oldCollection ) = Ipc.SetCollectionForType.Subscriber( _pi ).Invoke( _type, _collectionName, _allowCreation, _allowDeletion ); + } + + DrawIntro( Ipc.GetCollectionForObject.Label, "Get Object Collection" ); + ( var valid, var individual, name ) = Ipc.GetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx ); + ImGui.TextUnformatted( + $"{( valid ? "Valid" : "Invalid" )} Object, {( name.Length == 0 ? "Unassigned" : name )}{( individual ? " (Individual Assignment)" : string.Empty )}" ); + DrawIntro( Ipc.SetCollectionForObject.Label, "Set Object Collection" ); + if( ImGui.Button( "Set##ObjectCollection" ) ) + { + ( _returnCode, _oldCollection ) = Ipc.SetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx, _collectionName, _allowCreation, _allowDeletion ); + } + if( _returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty() ) + _oldCollection = null; + DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); @@ -1089,6 +1133,7 @@ public class IpcTester : IDisposable { _lastSettingsError = Ipc.CopyModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName ); } + ImGuiUtil.HoverTooltip( "Copy settings from Mod Directory Name to Mod Name (as directory) in collection." ); DrawIntro( Ipc.TrySetModSetting.Label, "Set Setting(s)" ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index d9a198ec..e303e841 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,7 +23,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 17 ); + => ( 4, 18 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -302,6 +302,132 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } + public string GetCollectionForType( Enums.ApiCollectionType type ) + { + CheckInitialized(); + if( !Enum.IsDefined( type ) ) + return string.Empty; + + var collection = Penumbra.CollectionManager.ByType( ( CollectionType )type ); + return collection?.Name ?? string.Empty; + } + + public (PenumbraApiEc, string OldCollection) SetCollectionForType( Enums.ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) + { + CheckInitialized(); + if( !Enum.IsDefined( type ) ) + return ( PenumbraApiEc.InvalidArgument, string.Empty ); + + var oldCollection = Penumbra.CollectionManager.ByType( ( CollectionType )type )?.Name ?? string.Empty; + + if( collectionName.Length == 0 ) + { + if( oldCollection.Length == 0 ) + { + return ( PenumbraApiEc.NothingChanged, oldCollection ); + } + + if( !allowDelete || type is Enums.ApiCollectionType.Current or Enums.ApiCollectionType.Default or Enums.ApiCollectionType.Interface ) + { + return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); + } + + Penumbra.CollectionManager.RemoveSpecialCollection( (CollectionType) type ); + return ( PenumbraApiEc.Success, oldCollection ); + } + + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return (PenumbraApiEc.CollectionMissing, oldCollection); + } + + if( oldCollection.Length == 0 ) + { + if( !allowCreateNew ) + { + return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); + } + + Penumbra.CollectionManager.CreateSpecialCollection( ( CollectionType )type ); + } + else if( oldCollection == collection.Name ) + { + return ( PenumbraApiEc.NothingChanged, oldCollection ); + } + + Penumbra.CollectionManager.SetCollection( collection, (CollectionType) type ); + return ( PenumbraApiEc.Success, oldCollection ); + } + + public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject( int gameObjectIdx ) + { + CheckInitialized(); + var id = AssociatedIdentifier( gameObjectIdx ); + if( !id.IsValid ) + { + return ( false, false, Penumbra.CollectionManager.Default.Name ); + } + + if( Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var collection ) ) + { + return ( true, true, collection.Name ); + } + + AssociatedCollection( gameObjectIdx, out collection ); + return ( true, false, collection.Name ); + } + + public (PenumbraApiEc, string OldCollection) SetCollectionForObject( int gameObjectIdx, string collectionName, bool allowCreateNew, bool allowDelete ) + { + CheckInitialized(); + var id = AssociatedIdentifier( gameObjectIdx ); + if( !id.IsValid ) + { + return ( PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name ); + } + + var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var c ) ? c.Name : string.Empty; + + if( collectionName.Length == 0 ) + { + if( oldCollection.Length == 0 ) + { + return (PenumbraApiEc.NothingChanged, oldCollection); + } + + if( !allowDelete ) + { + return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + } + var idx = Penumbra.CollectionManager.Individuals.Index( id ); + Penumbra.CollectionManager.RemoveIndividualCollection(idx ); + return (PenumbraApiEc.Success, oldCollection); + } + + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return (PenumbraApiEc.CollectionMissing, oldCollection); + } + + if( oldCollection.Length == 0 ) + { + if( !allowCreateNew ) + { + return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); + } + + var ids = Penumbra.CollectionManager.Individuals.GetGroup( id ); + Penumbra.CollectionManager.CreateIndividualCollection( ids ); + } + else if( oldCollection == collection.Name ) + { + return (PenumbraApiEc.NothingChanged, oldCollection); + } + + Penumbra.CollectionManager.SetCollection( collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index( id ) ); + return (PenumbraApiEc.Success, oldCollection); + } + public IList< string > GetCollections() { CheckInitialized(); @@ -867,7 +993,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. - private unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) + private static unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) { collection = Penumbra.CollectionManager.Default; if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) @@ -885,6 +1011,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } + private static unsafe ActorIdentifier AssociatedIdentifier( int gameObjectIdx ) + { + if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) + { + return ActorIdentifier.Invalid; + } + var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + return Penumbra.Actors.FromObject( ptr, out _, false, true ); + } + // Resolve a path given by string for a specific collection. private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) { diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 352edfcf..5e23624f 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -59,12 +59,16 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, string[] > ReverseResolvePlayerPath; // Collections - internal readonly FuncProvider< IList< string > > GetCollections; - internal readonly FuncProvider< string > GetCurrentCollectionName; - internal readonly FuncProvider< string > GetDefaultCollectionName; - internal readonly FuncProvider< string > GetInterfaceCollectionName; - internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; - internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; + internal readonly FuncProvider< IList< string > > GetCollections; + internal readonly FuncProvider< string > GetCurrentCollectionName; + internal readonly FuncProvider< string > GetDefaultCollectionName; + internal readonly FuncProvider< string > GetInterfaceCollectionName; + internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; + internal readonly FuncProvider< ApiCollectionType, string > GetCollectionForType; + internal readonly FuncProvider< ApiCollectionType, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForType; + internal readonly FuncProvider< int, (bool, bool, string) > GetCollectionForObject; + internal readonly FuncProvider< int, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForObject; + internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; // Meta internal readonly FuncProvider< string > GetPlayerMetaManipulations; @@ -163,6 +167,10 @@ public class PenumbraIpcProviders : IDisposable GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider( pi, Api.GetDefaultCollection ); GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider( pi, Api.GetInterfaceCollection ); GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider( pi, Api.GetCharacterCollection ); + GetCollectionForType = Ipc.GetCollectionForType.Provider( pi, Api.GetCollectionForType ); + SetCollectionForType = Ipc.SetCollectionForType.Provider( pi, Api.SetCollectionForType ); + GetCollectionForObject = Ipc.GetCollectionForObject.Provider( pi, Api.GetCollectionForObject ); + SetCollectionForObject = Ipc.SetCollectionForObject.Provider( pi, Api.SetCollectionForObject ); GetChangedItems = Ipc.GetChangedItems.Provider( pi, Api.GetChangedItemsForCollection ); // Meta @@ -189,7 +197,7 @@ public class PenumbraIpcProviders : IDisposable TrySetModPriority = Ipc.TrySetModPriority.Provider( pi, Api.TrySetModPriority ); TrySetModSetting = Ipc.TrySetModSetting.Provider( pi, Api.TrySetModSetting ); TrySetModSettings = Ipc.TrySetModSettings.Provider( pi, Api.TrySetModSettings ); - ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, + ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, () => Api.ModSettingChanged += ModSettingChangedEvent, () => Api.ModSettingChanged -= ModSettingChangedEvent ); CopyModSettings = Ipc.CopyModSettings.Provider( pi, Api.CopyModSettings ); @@ -262,6 +270,10 @@ public class PenumbraIpcProviders : IDisposable GetDefaultCollectionName.Dispose(); GetInterfaceCollectionName.Dispose(); GetCharacterCollectionName.Dispose(); + GetCollectionForType.Dispose(); + SetCollectionForType.Dispose(); + GetCollectionForObject.Dispose(); + SetCollectionForObject.Dispose(); GetChangedItems.Dispose(); // Meta diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 79c6e83a..3726d9dc 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -45,7 +45,7 @@ public partial class ModCollection => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< Api.Enums.ApiCollectionType >().Length - 3]; // Return the configured collection for the given type or null. // Does not handle Inactive, use ByName instead. diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index eab5d6eb..6b7f7638 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -7,105 +7,105 @@ namespace Penumbra.Collections; public enum CollectionType : byte { // Special Collections - Yourself = 0, + Yourself = Api.Enums.ApiCollectionType.Yourself, - MalePlayerCharacter, - FemalePlayerCharacter, - MaleNonPlayerCharacter, - FemaleNonPlayerCharacter, + MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, + FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, + MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, + FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, - MaleMidlander, - FemaleMidlander, - MaleHighlander, - FemaleHighlander, + MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, + FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, + MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, + FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, - MaleWildwood, - FemaleWildwood, - MaleDuskwight, - FemaleDuskwight, + MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, + FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, + MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, + FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, - MalePlainsfolk, - FemalePlainsfolk, - MaleDunesfolk, - FemaleDunesfolk, + MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, + FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, + MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, + FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, - MaleSeekerOfTheSun, - FemaleSeekerOfTheSun, - MaleKeeperOfTheMoon, - FemaleKeeperOfTheMoon, + MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, + FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, + FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, - MaleSeawolf, - FemaleSeawolf, - MaleHellsguard, - FemaleHellsguard, + MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, + FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, + MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, + FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, - MaleRaen, - FemaleRaen, - MaleXaela, - FemaleXaela, + MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, + FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, + MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, + FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, - MaleHelion, - FemaleHelion, - MaleLost, - FemaleLost, + MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, + FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, + MaleLost = Api.Enums.ApiCollectionType.MaleLost, + FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, - MaleRava, - FemaleRava, - MaleVeena, - FemaleVeena, + MaleRava = Api.Enums.ApiCollectionType.MaleRava, + FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, + MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, + FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, - MaleMidlanderNpc, - FemaleMidlanderNpc, - MaleHighlanderNpc, - FemaleHighlanderNpc, + MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, + FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, + MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, + FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, - MaleWildwoodNpc, - FemaleWildwoodNpc, - MaleDuskwightNpc, - FemaleDuskwightNpc, + MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, + FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, + MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, + FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, - MalePlainsfolkNpc, - FemalePlainsfolkNpc, - MaleDunesfolkNpc, - FemaleDunesfolkNpc, + MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, + FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, + MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, + FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, - MaleSeekerOfTheSunNpc, - FemaleSeekerOfTheSunNpc, - MaleKeeperOfTheMoonNpc, - FemaleKeeperOfTheMoonNpc, + MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, + FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, - MaleSeawolfNpc, - FemaleSeawolfNpc, - MaleHellsguardNpc, - FemaleHellsguardNpc, + MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, + FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, + MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, + FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, - MaleRaenNpc, - FemaleRaenNpc, - MaleXaelaNpc, - FemaleXaelaNpc, + MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, + FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, + MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, + FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, - MaleHelionNpc, - FemaleHelionNpc, - MaleLostNpc, - FemaleLostNpc, + MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, + FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, + MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, + FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, - MaleRavaNpc, - FemaleRavaNpc, - MaleVeenaNpc, - FemaleVeenaNpc, + MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, + FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, + MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, + FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, - Inactive, // A collection was added or removed - Default, // The default collection was changed - Interface, // The ui collection was changed - Individual, // An individual collection was changed - Current, // The current collection was changed - Temporary, // A temporary collections was set or deleted via IPC + Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed + Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed + Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed + Individual, // An individual collection was changed + Inactive, // A collection was added or removed + Temporary, // A temporary collections was set or deleted via IPC } public static class CollectionTypeExtensions { public static bool IsSpecial( this CollectionType collectionType ) - => collectionType is >= CollectionType.Yourself and < CollectionType.Inactive; + => collectionType is >= CollectionType.Yourself and < CollectionType.Default; public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >() .Where( IsSpecial ) @@ -216,7 +216,9 @@ public static class CollectionTypeExtensions public static bool TryParse( string text, out CollectionType type ) { if( Enum.TryParse( text, true, out type ) ) + { return type is not CollectionType.Inactive and not CollectionType.Temporary; + } if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) ) { @@ -245,7 +247,9 @@ public static class CollectionTypeExtensions foreach( var t in Enum.GetValues< CollectionType >() ) { if( t is CollectionType.Inactive or CollectionType.Temporary ) + { continue; + } if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) ) { From e55ff791fe83108b4d07979543616aac37f6a1fe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Jan 2023 17:54:40 +0100 Subject: [PATCH 0705/2451] Fix pvp actor maybe. --- Penumbra.Api | 2 +- Penumbra.GameData/Actors/ActorManager.Data.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 866f4c45..b65f0a4e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 866f4c45bd21219a31d044c5eb55b162ee2bc0e2 +Subproject commit b65f0a4e2a761a3142a07587a6c0f8657f1361ee diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 97655e16..235ba0a3 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -251,7 +251,7 @@ public sealed partial class ActorManager : IDisposable var obj = (GameObject*)_objects.GetObjectAddress(i); if (obj != null && obj->ObjectKind is (byte)ObjectKind.Player - && !CustomizeData.Equals((CustomizeData*)gameObject->CustomizeData, (CustomizeData*)((Character*)obj)->CustomizeData)) + && CustomizeData.Equals((CustomizeData*)gameObject->CustomizeData, (CustomizeData*)((Character*)obj)->CustomizeData)) return FromObject(obj, out _, false, true); } return ActorIdentifier.Invalid; From 7bb5a1ebe32afbd94a7131ee35aa71acac3516e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 13:16:53 +0100 Subject: [PATCH 0706/2451] Update BNPCs --- Penumbra.GameData/Data/BNpcNames.cs | 480 +++++++++++++++++++++++++--- 1 file changed, 440 insertions(+), 40 deletions(-) diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs index d8a773a5..6c044e25 100644 --- a/Penumbra.GameData/Data/BNpcNames.cs +++ b/Penumbra.GameData/Data/BNpcNames.cs @@ -5,11 +5,11 @@ namespace Penumbra.GameData.Data; public static class NpcNames { - /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2022-11-24. + /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2023-01-17. public static IReadOnlyList> CreateNames() => new IReadOnlyList[] { - new uint[]{0}, + Array.Empty(), new uint[]{6373}, new uint[]{411, 412, 965, 1064, 1863, 2012}, new uint[]{3, 176}, @@ -115,7 +115,7 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), - new uint[]{0}, + Array.Empty(), new uint[]{548}, Array.Empty(), new uint[]{310}, @@ -195,7 +195,7 @@ public static class NpcNames new uint[]{242, 1739, 1759}, Array.Empty(), Array.Empty(), - new uint[]{0}, + Array.Empty(), new uint[]{276, 277, 278, 575, 901, 1041}, new uint[]{275, 1741, 2008}, new uint[]{1671, 1836}, @@ -573,7 +573,7 @@ public static class NpcNames Array.Empty(), new uint[]{108}, Array.Empty(), - new uint[]{0}, + Array.Empty(), Array.Empty(), new uint[]{613}, new uint[]{455}, @@ -609,7 +609,7 @@ public static class NpcNames Array.Empty(), new uint[]{627}, new uint[]{439}, - new uint[]{0}, + Array.Empty(), new uint[]{448}, new uint[]{448}, new uint[]{521, 902, 905, 1859, 4396}, @@ -779,7 +779,7 @@ public static class NpcNames new uint[]{389, 1829, 2523, 2537, 2721}, new uint[]{565, 1830, 2526, 2541}, new uint[]{225, 283, 367, 1360, 3099}, - new uint[]{0}, + Array.Empty(), new uint[]{218, 266, 350}, Array.Empty(), new uint[]{347, 1168, 1705, 2539}, @@ -790,7 +790,7 @@ public static class NpcNames new uint[]{914}, new uint[]{914}, new uint[]{914}, - new uint[]{0}, + Array.Empty(), new uint[]{1329}, new uint[]{1272}, new uint[]{1272}, @@ -2707,7 +2707,7 @@ public static class NpcNames new uint[]{2550}, new uint[]{2550}, Array.Empty(), - new uint[]{0}, + Array.Empty(), new uint[]{2567}, Array.Empty(), new uint[]{2560}, @@ -8071,7 +8071,7 @@ public static class NpcNames new uint[]{6726}, new uint[]{6742}, Array.Empty(), - Array.Empty(), + new uint[]{12317}, new uint[]{6857, 6858}, new uint[]{6859, 6860, 6861, 6862}, new uint[]{541}, @@ -8643,7 +8643,7 @@ public static class NpcNames new uint[]{6203}, new uint[]{7036}, new uint[]{7136}, - Array.Empty(), + new uint[]{7126}, Array.Empty(), new uint[]{6984}, new uint[]{7037}, @@ -11585,8 +11585,8 @@ public static class NpcNames new uint[]{9301}, new uint[]{9301}, Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{9302}, + new uint[]{9303}, new uint[]{108}, new uint[]{108}, new uint[]{9296}, @@ -13123,9 +13123,9 @@ public static class NpcNames Array.Empty(), new uint[]{9378}, new uint[]{10251}, - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{9344}, + new uint[]{9344}, + new uint[]{9344}, new uint[]{1455}, new uint[]{11313}, new uint[]{11312}, @@ -13294,12 +13294,12 @@ public static class NpcNames new uint[]{10207}, new uint[]{10250}, new uint[]{10249}, - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{9344}, + new uint[]{9344}, + new uint[]{9344}, new uint[]{10717}, new uint[]{10718}, - new uint[]{0}, + Array.Empty(), new uint[]{10719}, Array.Empty(), new uint[]{9425}, @@ -14714,7 +14714,7 @@ public static class NpcNames Array.Empty(), new uint[]{11355}, new uint[]{3455}, - Array.Empty(), + new uint[]{10308}, new uint[]{11386}, new uint[]{108}, Array.Empty(), @@ -14944,7 +14944,7 @@ public static class NpcNames new uint[]{3499}, new uint[]{11460}, new uint[]{353}, - Array.Empty(), + new uint[]{12313}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -15068,7 +15068,7 @@ public static class NpcNames new uint[]{11404}, new uint[]{11513}, new uint[]{11481}, - new uint[]{0, 11511}, + new uint[]{11511}, new uint[]{11512}, new uint[]{11514}, new uint[]{11510}, @@ -15083,8 +15083,8 @@ public static class NpcNames new uint[]{11510}, new uint[]{11506}, new uint[]{11515}, - Array.Empty(), - Array.Empty(), + new uint[]{11995}, + new uint[]{11996}, new uint[]{108}, new uint[]{11402}, new uint[]{11406}, @@ -15095,7 +15095,7 @@ public static class NpcNames new uint[]{11462}, new uint[]{11391}, new uint[]{11468}, - Array.Empty(), + new uint[]{12054}, new uint[]{11405}, Array.Empty(), Array.Empty(), @@ -15563,7 +15563,7 @@ public static class NpcNames new uint[]{11982}, new uint[]{11983}, new uint[]{11984}, - Array.Empty(), + new uint[]{11985}, new uint[]{11986}, Array.Empty(), Array.Empty(), @@ -15592,23 +15592,23 @@ public static class NpcNames new uint[]{11467}, new uint[]{11519}, new uint[]{11520}, + new uint[]{12062}, + new uint[]{12061}, + new uint[]{12094}, + new uint[]{12081}, + Array.Empty(), + new uint[]{12095}, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12082}, + new uint[]{12088}, + new uint[]{12087}, + new uint[]{12086}, + new uint[]{12084}, + new uint[]{12085}, + new uint[]{12083}, new uint[]{3329}, new uint[]{3329}, new uint[]{11431}, @@ -15623,6 +15623,15 @@ public static class NpcNames new uint[]{11431}, Array.Empty(), Array.Empty(), + new uint[]{11992}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{12060}, + new uint[]{108}, + new uint[]{9363}, + new uint[]{108}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -15633,6 +15642,13 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12250}, + new uint[]{12251}, + new uint[]{12252}, + new uint[]{12253}, + new uint[]{12254}, + new uint[]{12255}, + new uint[]{4954}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -15640,13 +15656,62 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12079}, + new uint[]{12080}, + new uint[]{108}, + new uint[]{108}, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{11997}, + new uint[]{11998}, + new uint[]{11999}, + new uint[]{12000}, + new uint[]{12001}, + new uint[]{12002}, + new uint[]{12003}, + new uint[]{12004}, + new uint[]{12005}, + new uint[]{12006}, + new uint[]{12007}, + new uint[]{12008}, + new uint[]{12009}, Array.Empty(), + new uint[]{12010}, + new uint[]{12011}, + new uint[]{12012}, + new uint[]{12013}, + new uint[]{12014}, + new uint[]{12015}, + new uint[]{12016}, + new uint[]{12017}, + new uint[]{12018}, + new uint[]{12019}, + new uint[]{12020}, + new uint[]{12021}, + new uint[]{12022}, + new uint[]{12023}, + new uint[]{12024}, + new uint[]{12025}, + new uint[]{12026}, + new uint[]{12027}, + new uint[]{12028}, + new uint[]{12029}, + new uint[]{12030}, + new uint[]{12031}, + new uint[]{12032}, + new uint[]{12033}, + new uint[]{12034}, + new uint[]{12035}, + new uint[]{12036}, + new uint[]{12037}, + new uint[]{12038}, + new uint[]{12039}, + new uint[]{12040}, Array.Empty(), Array.Empty(), + new uint[]{12078}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -15672,23 +15737,89 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12063}, + new uint[]{12066}, + new uint[]{12067}, Array.Empty(), + new uint[]{12069}, + new uint[]{12070}, + new uint[]{12071}, + new uint[]{12067}, + new uint[]{12067}, + new uint[]{12072}, Array.Empty(), Array.Empty(), + new uint[]{12073}, + new uint[]{12074}, + new uint[]{12075}, + new uint[]{12076}, + new uint[]{12077}, + new uint[]{12065}, + new uint[]{4808}, + new uint[]{4810}, Array.Empty(), + new uint[]{12057}, + new uint[]{12059}, Array.Empty(), + new uint[]{12280}, + new uint[]{12056}, + new uint[]{12056}, + new uint[]{12056}, + new uint[]{12054}, Array.Empty(), + new uint[]{12057}, + new uint[]{12059}, + new uint[]{12059}, + new uint[]{12280}, + new uint[]{12056}, + new uint[]{12056}, + new uint[]{12056}, + new uint[]{12058}, + new uint[]{12058}, + new uint[]{12058}, + new uint[]{108}, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{6091}, + new uint[]{12278}, + new uint[]{12279}, Array.Empty(), Array.Empty(), + new uint[]{3822}, + new uint[]{2143}, + new uint[]{12292}, + new uint[]{12293}, + new uint[]{3823}, + new uint[]{12292}, + new uint[]{12293}, + new uint[]{12296}, + new uint[]{12295}, + new uint[]{12294}, + new uint[]{12064}, + new uint[]{12297}, + new uint[]{12297}, + new uint[]{12312}, + new uint[]{12312}, + new uint[]{8378}, + new uint[]{4392}, + new uint[]{4392}, + new uint[]{4130}, + new uint[]{4130}, + new uint[]{11330}, + new uint[]{11331}, + new uint[]{11332}, + new uint[]{11333}, + new uint[]{713}, + new uint[]{713}, + new uint[]{11262}, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{11418, 12053}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -15770,6 +15901,275 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{5199}, + new uint[]{5200}, + new uint[]{5201}, + new uint[]{5202}, + new uint[]{5203}, + new uint[]{5204}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12244}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{11296}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{11295}, + new uint[]{5199}, + new uint[]{5199, 5204}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12031}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + new uint[]{750}, + new uint[]{750}, + new uint[]{750}, + new uint[]{750}, + new uint[]{12276}, + new uint[]{12276}, + new uint[]{12277}, + new uint[]{12245}, + Array.Empty(), + Array.Empty(), + new uint[]{12308}, + new uint[]{12309}, + Array.Empty(), + new uint[]{12308}, + new uint[]{12072}, + new uint[]{12311}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12281}, + new uint[]{12282}, + Array.Empty(), + new uint[]{12284}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12288}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12298}, + Array.Empty(), + new uint[]{12300}, + Array.Empty(), + new uint[]{12302}, + new uint[]{12303}, + new uint[]{12304}, + new uint[]{12305}, + new uint[]{12306}, + new uint[]{12307}, + new uint[]{12314}, + new uint[]{12315}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{11297}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12311}, + new uint[]{12308}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), From 23919d80833fc62ec25711d3b051dd1448e6f8fa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 13:19:05 +0100 Subject: [PATCH 0707/2451] Ensure permanent identifiers. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 2 +- Penumbra/Collections/IndividualCollections.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index f00ad8f6..26565dea 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -25,7 +25,7 @@ public readonly struct ActorIdentifier : IEquatable // @formatter:on public ActorIdentifier CreatePermanent() - => new(Type, Kind, Index, DataId, PlayerName.IsEmpty ? PlayerName : PlayerName.Clone()); + => new(Type, Kind, Index, DataId, PlayerName.IsEmpty || PlayerName.IsOwned ? PlayerName : PlayerName.Clone()); public bool Equals(ActorIdentifier other) { diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 3d5f3fdd..bca5b4a2 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -145,12 +145,14 @@ public sealed partial class IndividualCollections return false; } - _assignments.Add( ( displayName, identifiers, collection ) ); - foreach( var identifier in identifiers ) + for( var i = 0; i < identifiers.Length; ++i ) { - _individuals.Add( identifier, collection ); + identifiers[ i ] = identifiers[ i ].CreatePermanent(); + _individuals.Add( identifiers[ i ], collection ); } + _assignments.Add( ( displayName, identifiers, collection ) ); + return true; } From 6b558c594012f8ab444065d50aba91c22efa8118 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 15:03:59 +0100 Subject: [PATCH 0708/2451] Try to identify actors in banners correctly. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 23 ++++++-- Penumbra.GameData/Structs/CustomizeData.cs | 28 ++++++++-- .../IndividualCollections.Access.cs | 54 +++++++++++++------ .../Resolver/PathResolver.Identification.cs | 50 ++--------------- 4 files changed, 83 insertions(+), 72 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 235ba0a3..0a030b28 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -14,6 +14,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Lumina.Text; using Penumbra.GameData.Data; @@ -227,7 +228,7 @@ public sealed partial class ActorManager : IDisposable private unsafe bool SearchPlayerCustomize(Character* character, int idx, out ActorIdentifier id) { var other = (Character*)_objects.GetObjectAddress(idx); - if (other == null || !CustomizeData.Equals((CustomizeData*)character->CustomizeData, (CustomizeData*)other->CustomizeData)) + if (other == null || !CustomizeData.ScreenActorEquals((CustomizeData*)character->CustomizeData, (CustomizeData*)other->CustomizeData)) { id = ActorIdentifier.Invalid; return false; @@ -246,14 +247,23 @@ public sealed partial class ActorManager : IDisposable private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject) { + static bool Compare(Character* a, Character* b) + { + var data1 = (CustomizeData*)a->CustomizeData; + var data2 = (CustomizeData*)b->CustomizeData; + var equals = CustomizeData.ScreenActorEquals(data1, data2); + return equals; + } + for (var i = 0; i < (int)ScreenActor.CutsceneStart; i += 2) { var obj = (GameObject*)_objects.GetObjectAddress(i); if (obj != null && obj->ObjectKind is (byte)ObjectKind.Player - && CustomizeData.Equals((CustomizeData*)gameObject->CustomizeData, (CustomizeData*)((Character*)obj)->CustomizeData)) + && Compare(gameObject, (Character*)obj)) return FromObject(obj, out _, false, true); } + return ActorIdentifier.Invalid; } @@ -281,15 +291,18 @@ public sealed partial class ActorManager : IDisposable public unsafe bool ResolvePvPBannerPlayer(ScreenActor type, out ActorIdentifier id) { id = ActorIdentifier.Invalid; - var addon = _gameGui.GetAddonByName("PvPMKSIntroduction"); - if (addon == IntPtr.Zero) + if (!_clientState.IsPvPExcludingDen) + return false; + + var addon = (AtkUnitBase*)_gameGui.GetAddonByName("PvPMap"); + if (addon == null || addon->IsVisible) return false; var obj = (Character*)_objects.GetObjectAddress((int)type); if (obj == null) return false; - var identifier = type switch + id = type switch { ScreenActor.CharacterScreen => SearchPlayersCustomize(obj), ScreenActor.ExamineScreen => SearchPlayersCustomize(obj), diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 1524ae11..c60ee746 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -1,8 +1,11 @@ using System; +using System.Runtime.InteropServices; +using System.Text; using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; - + +[StructLayout(LayoutKind.Sequential, Size = Size)] public unsafe struct CustomizeData : IEquatable< CustomizeData > { public const int Size = 26; @@ -40,12 +43,18 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } - public static bool Equals( CustomizeData* lhs, CustomizeData* rhs ) - => MemoryUtility.MemCmpUnchecked( lhs, rhs, Size ) == 0; - public override bool Equals( object? obj ) => obj is CustomizeData other && Equals( other ); + public static bool Equals(CustomizeData* lhs, CustomizeData* rhs) + => MemoryUtility.MemCmpUnchecked(lhs, rhs, Size) == 0; + + /// Compare Gender and then only from Height onwards, because all screen actors are set to Height 50, + /// the Race is implicitly included in the subrace (after height), + /// and the body type is irrelevant for players.> + public static bool ScreenActorEquals(CustomizeData* lhs, CustomizeData* rhs) + => lhs->Data[1] == rhs->Data[1] && MemoryUtility.MemCmpUnchecked(lhs->Data + 4, rhs->Data + 4, Size - 4) == 0; + public override int GetHashCode() { fixed( byte* ptr = Data ) @@ -65,6 +74,17 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } + public string WriteBytes() + { + var sb = new StringBuilder(Size * 3); + for (var i = 0; i < Size - 1; ++i) + { + sb.Append($"{Data[i]:X2} "); + } + sb.Append($"{Data[Size - 1]:X2}"); + return sb.ToString(); + } + public bool LoadBase64( string base64 ) { var buffer = stackalloc byte[Size]; diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 73e06fef..2e421807 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -72,51 +72,73 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return false; } case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); - case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ), out collection ); + case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ).Item1, out collection ); } collection = null; return false; } - public ActorIdentifier ConvertSpecialIdentifier( ActorIdentifier identifier ) + public enum SpecialResult + { + PartyBanner, + PvPBanner, + Mahjong, + CharacterScreen, + FittingRoom, + DyePreview, + Portrait, + Inspect, + Card, + Glamour, + Invalid, + } + + public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier( ActorIdentifier identifier ) { if( identifier.Type != IdentifierType.Special ) { - return identifier; + return ( identifier, SpecialResult.Invalid ); } - if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) - || _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) - || _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) ) + if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) ) { - return identifier; + return ( id, SpecialResult.PartyBanner ); + } + + if( _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) ) + { + return ( id, SpecialResult.PvPBanner ); + } + + if( _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) ) + { + return ( id, SpecialResult.Mahjong ); } switch( identifier.Special ) { - case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: - case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: - case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: - case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: - return _actorManager.GetCurrentPlayer(); + case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: return ( _actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen ); + case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom ); + case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.DyePreview ); + case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: return ( _actorManager.GetCurrentPlayer(), SpecialResult.Portrait ); case ScreenActor.ExamineScreen: { identifier = _actorManager.GetInspectPlayer(); if( identifier.IsValid ) { - return Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid; + return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect ); } identifier = _actorManager.GetCardPlayer(); if( identifier.IsValid ) { - return Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid; + return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card ); } - return Penumbra.Config.UseCharacterCollectionInTryOn ? _actorManager.GetGlamourPlayer() : ActorIdentifier.Invalid; + return ( Penumbra.Config.UseCharacterCollectionInTryOn ? _actorManager.GetGlamourPlayer() : ActorIdentifier.Invalid, SpecialResult.Glamour ); } - default: return identifier; + default: return ( identifier, SpecialResult.Invalid ); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index abf85749..9ddb9941 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -18,48 +17,6 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { - private static ResolveData IdentifyMahjong( GameObject* gameObject ) - { - static bool SearchPlayer( Character* character, int idx, out ActorIdentifier id ) - { - var other = ( Character* )Dalamud.Objects.GetObjectAddress( idx ); - if( other == null || !CustomizeData.Equals( ( CustomizeData* )character->CustomizeData, ( CustomizeData* )other->CustomizeData ) ) - { - id = ActorIdentifier.Invalid; - return false; - } - - id = Penumbra.Actors.FromObject( &other->GameObject, out _, false, true ); - return true; - } - - static ActorIdentifier SearchPlayers( Character* gameObject, int idx1, int idx2, int idx3 ) - => SearchPlayer( gameObject, idx1, out var id ) || SearchPlayer( gameObject, idx2, out id ) || SearchPlayer( gameObject, idx3, out id ) - ? id - : ActorIdentifier.Invalid; - - var identifier = gameObject->ObjectIndex switch - { - 0 => Penumbra.Actors.GetCurrentPlayer(), - 2 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), - 4 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), - 6 => Penumbra.Actors.FromObject( gameObject, out _, false, true ), - 240 => Penumbra.Actors.GetCurrentPlayer(), - 241 => SearchPlayers( ( Character* )gameObject, 2, 4, 6 ), - 242 => SearchPlayers( ( Character* )gameObject, 4, 2, 6 ), - 243 => SearchPlayers( ( Character* )gameObject, 6, 2, 4 ), - _ => ActorIdentifier.Invalid, - }; - - var collection = ( identifier.IsValid ? CollectionByIdentifier( identifier ) : null ) - ?? CheckYourself( identifier, gameObject ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; - - return IdentifiedCache.Set( collection, identifier, gameObject ); - } - - // Identify the correct collection for a GameObject by index and name. public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { @@ -89,7 +46,7 @@ public unsafe partial class PathResolver } // Aesthetician. The relevant actor is yourself, so use player collection when possible. - if( Dalamud.GameGui.GetAddonByName( "ScreenLog", 1 ) == IntPtr.Zero ) + if( Dalamud.GameGui.GetAddonByName( "ScreenLog" ) == IntPtr.Zero ) { var player = Penumbra.Actors.GetCurrentPlayer(); var collection2 = ( player.IsValid ? CollectionByIdentifier( player ) : null ) @@ -102,12 +59,11 @@ public unsafe partial class PathResolver var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); if( identifier.Type is IdentifierType.Special ) { - if( Penumbra.Config.UseNoModsInInspect && identifier.Special == ScreenActor.ExamineScreen ) + ( identifier, var type ) = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); + if( Penumbra.Config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect ) { return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); } - - identifier = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); } var collection = CollectionByIdentifier( identifier ) From b64a9a51f8b969af09f968292cad3af0bfa6445d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 16:41:25 +0100 Subject: [PATCH 0709/2451] Add Target button to collection assignments. --- .../ConfigWindow.CollectionsTab.Individual.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 83e7d79e..5118352d 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -244,6 +244,7 @@ public partial class ConfigWindow var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); var change = DrawNewCurrentPlayerCollection(); + change |= DrawNewTargetCollection(); change |= DrawNewPlayerCollection( buttonWidth1, width ); ImGui.SameLine(); @@ -278,6 +279,28 @@ public partial class ConfigWindow return true; } + return false; + } + + private bool DrawNewTargetCollection() + { + var target = Dalamud.Targets.Target; + var player = Penumbra.Actors.FromObject( target, false, true ); + var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No valid character in target detected.", + _ => string.Empty, + }; + + if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", _window._inputTextWidth, tt, result != IndividualCollections.AddResult.Valid ) ) + { + Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); + return true; + } + ImGui.SameLine(); ImGuiComponents.HelpMarker( "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" From bff99da585e16c148da1d6e166f5f466b0968f83 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 16:46:09 +0100 Subject: [PATCH 0710/2451] Reformat buttons. --- .../UI/ConfigWindow.CollectionsTab.Individual.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 5118352d..992a0d80 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -243,8 +243,10 @@ public partial class ConfigWindow var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); - var change = DrawNewCurrentPlayerCollection(); - change |= DrawNewTargetCollection(); + var assignWidth = new Vector2((_window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var change = DrawNewCurrentPlayerCollection(assignWidth); + ImGui.SameLine(); + change |= DrawNewTargetCollection(assignWidth); change |= DrawNewPlayerCollection( buttonWidth1, width ); ImGui.SameLine(); @@ -261,7 +263,7 @@ public partial class ConfigWindow } } - private bool DrawNewCurrentPlayerCollection() + private static bool DrawNewCurrentPlayerCollection(Vector2 width) { var player = Penumbra.Actors.GetCurrentPlayer(); var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); @@ -273,7 +275,8 @@ public partial class ConfigWindow _ => string.Empty, }; - if( ImGuiUtil.DrawDisabledButton( "Assign Currently Played Character", _window._inputTextWidth, tt, result != IndividualCollections.AddResult.Valid ) ) + + if( ImGuiUtil.DrawDisabledButton( "Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid ) ) { Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); return true; @@ -282,7 +285,7 @@ public partial class ConfigWindow return false; } - private bool DrawNewTargetCollection() + private static bool DrawNewTargetCollection(Vector2 width) { var target = Dalamud.Targets.Target; var player = Penumbra.Actors.FromObject( target, false, true ); @@ -294,8 +297,7 @@ public partial class ConfigWindow IndividualCollections.AddResult.Invalid => "No valid character in target detected.", _ => string.Empty, }; - - if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", _window._inputTextWidth, tt, result != IndividualCollections.AddResult.Valid ) ) + if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid ) ) { Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); return true; From f9b87175827ee1a7514db170b44e9e93f554682c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Jan 2023 16:46:17 +0100 Subject: [PATCH 0711/2451] Add changelog. --- Penumbra.sln | 1 + Penumbra/UI/ConfigWindow.Changelog.cs | 74 ++++++++++++++++----------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 5c11aaea..bccc56d8 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + base_repo.json = base_repo.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 89892243..279b4b02 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -31,29 +31,45 @@ public partial class ConfigWindow Add6_1_0( ret ); Add6_1_1( ret ); Add6_2_0( ret ); + Add6_3_0( ret ); return ret; } + private static void Add6_3_0( Changelog log ) + => log.NextVersion( "Version 0.6.3.0" ) + .RegisterEntry( "Add an Assign Current Target button for individual assignments" ) + .RegisterEntry( "Try identifying all banner actors correctly for PvE duties, Crystalline Conflict and Mahjong." ) + .RegisterEntry( "Please let me know if this does not work for anything except identical twins.", 1 ) + .RegisterEntry( "Add handling for the 3 new screen actors (now 8 total, for PvE dutie portraits)." ) + .RegisterEntry( "Update the Battle NPC name database for 6.3." ) + .RegisterEntry( "Added API/IPC functions to obtain or set group or individual collections." ) + .RegisterEntry( "Maybe fix a problem with textures sometimes not loading from their corresponding collection." ) + .RegisterEntry( "Another try to fix a problem with the collection selectors breaking state." ) + .RegisterEntry( "Fix a problem identifying companions." ) + .RegisterEntry( "Fix a problem when deleting collections assigned to Groups." ) + .RegisterEntry( "Fix a problem when using the Assign Currently Played Character button and then logging onto a different character without restarting in between." ) + .RegisterEntry( "Some miscellaneous backend changes." ); + private static void Add6_2_0( Changelog log ) => log.NextVersion( "Version 0.6.2.0" ) - .RegisterEntry( "Updated Penumbra for .net7, Dalamud API 8 and patch 6.3." ) - .RegisterEntry( "Added a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)" ) - .RegisterEntry( "Added placeholder options for setting individual collections via chat command." ) - .RegisterEntry( "Added toggles to swap left and/or right rings separately for ring item swap." ) - .RegisterEntry( "Added handling for looping sound effects caused by animations in non-base collections." ) - .RegisterEntry( "Added an option to not use any mods at all in the Inspect/Try-On window." ) - .RegisterEntry( "Added handling for Mahjong actors." ) - .RegisterEntry( "Improved hint text for File Swaps in Advanced Editing, also inverted file swap display order." ) - .RegisterEntry( "Fixed a problem where the collection selectors could get desynchronized after adding or deleting collections." ) - .RegisterEntry( "Fixed a problem that could cause setting state to get desynchronized." ) - .RegisterEntry( "Fixed an oversight where some special screen actors did not actually respect the settings made for them." ) - .RegisterEntry( "Added collection and associated game object to Full Resource Logging." ) - .RegisterEntry( "Added performance tracking for DEBUG-compiled versions (i.e. testing only)." ) - .RegisterEntry( "Added some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)" ) - .RegisterEntry( "Fixed association of some vfx game objects. (0.6.1.3)" ) - .RegisterEntry( "Stopped forcing AVFX files to load synchronously. (0.6.1.3)" ) - .RegisterEntry( "Fixed an issue when incorporating deduplicated meta files. (0.6.1.2)" ); + .RegisterEntry( "Update Penumbra for .net7, Dalamud API 8 and patch 6.3." ) + .RegisterEntry( "Add a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)" ) + .RegisterEntry( "Add placeholder options for setting individual collections via chat command." ) + .RegisterEntry( "Add toggles to swap left and/or right rings separately for ring item swap." ) + .RegisterEntry( "Add handling for looping sound effects caused by animations in non-base collections." ) + .RegisterEntry( "Add an option to not use any mods at all in the Inspect/Try-On window." ) + .RegisterEntry( "Add handling for Mahjong actors." ) + .RegisterEntry( "Improve hint text for File Swaps in Advanced Editing, also inverted file swap display order." ) + .RegisterEntry( "Fix a problem where the collection selectors could get desynchronized after adding or deleting collections." ) + .RegisterEntry( "Fix a problem that could cause setting state to get desynchronized." ) + .RegisterEntry( "Fix an oversight where some special screen actors did not actually respect the settings made for them." ) + .RegisterEntry( "Add collection and associated game object to Full Resource Logging." ) + .RegisterEntry( "Add performance tracking for DEBUG-compiled versions (i.e. testing only)." ) + .RegisterEntry( "Add some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)" ) + .RegisterEntry( "Fix association of some vfx game objects. (0.6.1.3)" ) + .RegisterEntry( "Stop forcing AVFX files to load synchronously. (0.6.1.3)" ) + .RegisterEntry( "Fix an issue when incorporating deduplicated meta files. (0.6.1.2)" ); private static void Add6_1_1( Changelog log ) => log.NextVersion( "Version 0.6.1.1" ) @@ -66,7 +82,7 @@ public partial class ConfigWindow private static void Add6_1_0( Changelog log ) => log.NextVersion( "Version 0.6.1.0 (Happy New Year! Edition)" ) - .RegisterEntry( "Added a prototype for Item Swapping." ) + .RegisterEntry( "Add a prototype for Item Swapping." ) .RegisterEntry( "A new tab in Advanced Editing.", 1 ) .RegisterEntry( "Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1 ) .RegisterEntry( "The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" @@ -74,19 +90,19 @@ public partial class ConfigWindow .RegisterEntry( "You can write a swap to a new mod, or to a new option in the currently selected mod.", 1 ) .RegisterEntry( "The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1 ) .RegisterEntry( "More detailed help or explanations will be added later.", 1 ) - .RegisterEntry( "Heavily improved Chat Commands. Use /penumbra help for more information." ) + .RegisterEntry( "Heavily improve Chat Commands. Use /penumbra help for more information." ) .RegisterEntry( "Penumbra now considers meta manipulations for Changed Items." ) .RegisterEntry( "Penumbra now tries to associate battle voices to specific actors, so that they work in collections." ) - .RegisterEntry( "Heavily improved .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects." ) - .RegisterEntry( "Improved some file handling for Mare-Interaction." ) - .RegisterEntry( "Added Equipment Slots to Demihuman IMC Edits." ) - .RegisterEntry( "Added a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files." ) - .RegisterEntry( "Added an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra." ) - .RegisterEntry( "Added API to copy mod settings from one mod to another." ) - .RegisterEntry( "Fixed a problem where creating individual collections did not trigger events." ) - .RegisterEntry( "Added a Hack to support Anamnesis Redrawing better. (0.6.0.6)" ) - .RegisterEntry( "Fixed another problem with the aesthetician. (0.6.0.6)" ) - .RegisterEntry( "Fixed a problem with the export directory not being respected. (0.6.0.6)" ); + .RegisterEntry( "Heavily improve .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects." ) + .RegisterEntry( "Improve some file handling for Mare-Interaction." ) + .RegisterEntry( "Add Equipment Slots to Demihuman IMC Edits." ) + .RegisterEntry( "Add a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files." ) + .RegisterEntry( "Add an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra." ) + .RegisterEntry( "Add API to copy mod settings from one mod to another." ) + .RegisterEntry( "Fix a problem where creating individual collections did not trigger events." ) + .RegisterEntry( "Add a Hack to support Anamnesis Redrawing better. (0.6.0.6)" ) + .RegisterEntry( "Fix another problem with the aesthetician. (0.6.0.6)" ) + .RegisterEntry( "Fix a problem with the export directory not being respected. (0.6.0.6)" ); private static void Add6_0_5( Changelog log ) => log.NextVersion( "Version 0.6.0.5" ) From b0370139ec0a9e8a2d6aaa42740b436e37d6c002 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 17 Jan 2023 15:49:07 +0000 Subject: [PATCH 0712/2451] [CI] Updating repo.json for refs/tags/0.6.3.0 --- repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/repo.json b/repo.json index 8a73e0c7..02bdd07e 100644 --- a/repo.json +++ b/repo.json @@ -1,11 +1,11 @@ [ { - "Author": "Adam", + "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.2.0", - "TestingAssemblyVersion": "0.6.2.0", + "AssemblyVersion": "0.6.3.0", + "TestingAssemblyVersion": "0.6.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.2.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From fe0e01b8fe610c0037b17aefc91ad143706ccd99 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 28 Nov 2022 15:30:37 +0100 Subject: [PATCH 0713/2451] Use compiled regex. --- Penumbra.GameData/Data/GamePathParser.cs | 118 ++++++++++------------- 1 file changed, 49 insertions(+), 69 deletions(-) diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index 58817c28..d784463a 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Dalamud.Logging; @@ -80,62 +78,6 @@ internal class GamePathParser : IGamePathParser private const string WorldFolder1 = "bgcommon"; private const string WorldFolder2 = "bg"; - // @formatter:off - // language=regex - private readonly IReadOnlyDictionary>> _regexes = new Dictionary>>() - { - [FileType.Font] = new Dictionary> - { - [ObjectType.Font] = CreateRegexes(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), - }, - [FileType.Texture] = new Dictionary> - { - [ObjectType.Icon] = CreateRegexes(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex"), - [ObjectType.Map] = CreateRegexes(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex"), - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex"), - [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), - [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex"), - [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex"), - [ObjectType.Character] = CreateRegexes( @"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" - , @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture" - , @"chara/common/texture/skin(?'skin'.*)\.tex" - , @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex" - , @"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex"), - }, - [FileType.Model] = new Dictionary> - { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl"), - [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl"), - [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl"), - [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl"), - [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl"), - }, - [FileType.Material] = new Dictionary> - { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl"), - [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), - [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), - [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl"), - [ObjectType.Character] = CreateRegexes(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl"), - }, - [FileType.Imc] = new Dictionary> - { - [ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc"), - [ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc"), - [ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc"), - [ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc"), - [ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc"), - }, - }; - - private static IReadOnlyList CreateRegexes(params string[] regexes) - => regexes.Select(s => new Regex(s, RegexOptions.Compiled)).ToArray(); - // @formatter:on - - public ObjectType PathToObjectType(string path) { if (path.Length == 0) @@ -190,20 +132,58 @@ internal class GamePathParser : IGamePathParser var objectType = PathToObjectType(path); - if (!_regexes.TryGetValue(fileType, out var objectDict)) - return (fileType, objectType, null); - - if (!objectDict.TryGetValue(objectType, out var regexes)) - return (fileType, objectType, null); - - foreach (var regex in regexes) + static Match TestCharacterTextures(string path) { - var match = regex.Match(path); - if (match.Success) - return (fileType, objectType, match); + var regexes = new Regex[] + { + GamePathManager.Character.Tex.Regex(), + GamePathManager.Character.Tex.FolderRegex(), + GamePathManager.Character.Tex.SkinRegex(), + GamePathManager.Character.Tex.CatchlightRegex(), + GamePathManager.Character.Tex.DecalRegex(), + }; + foreach (var regex in regexes) + { + var match = regex.Match(path); + if (match.Success) + return match; + } + + return Match.Empty; } - return (fileType, objectType, null); + var match = (fileType, objectType) switch + { + (FileType.Font, ObjectType.Font) => GamePathManager.Font.Regex().Match(path), + (FileType.Imc, ObjectType.Weapon) => GamePathManager.Weapon.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Monster) => GamePathManager.Monster.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Equipment) => GamePathManager.Equipment.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Accessory) => GamePathManager.Accessory.Imc.Regex().Match(path), + (FileType.Model, ObjectType.Weapon) => GamePathManager.Weapon.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Monster) => GamePathManager.Monster.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Equipment) => GamePathManager.Equipment.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Accessory) => GamePathManager.Accessory.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Character) => GamePathManager.Character.Mdl.Regex().Match(path), + (FileType.Material, ObjectType.Weapon) => GamePathManager.Weapon.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Monster) => GamePathManager.Monster.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Equipment) => GamePathManager.Equipment.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Accessory) => GamePathManager.Accessory.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Character) => GamePathManager.Character.Mtrl.Regex().Match(path), + (FileType.Texture, ObjectType.Weapon) => GamePathManager.Weapon.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Monster) => GamePathManager.Monster.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Equipment) => GamePathManager.Equipment.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Accessory) => GamePathManager.Accessory.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Character) => TestCharacterTextures(path), + (FileType.Texture, ObjectType.Icon) => GamePathManager.Icon.Regex().Match(path), + (FileType.Texture, ObjectType.Map) => GamePathManager.Map.Regex().Match(path), + _ => Match.Empty, + }; + + return (fileType, objectType, match.Success ? match : null); } private static GameObjectInfo HandleEquipment(FileType fileType, GroupCollection groups) From 93840e30f057f2e25d2a45eea07064dec6e94abc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Jan 2023 17:39:51 +0100 Subject: [PATCH 0714/2451] Update gamepaths stuff. --- Penumbra.GameData/Data/GamePathParser.cs | 70 +++++----- Penumbra.GameData/Data/GamePaths.cs | 157 +++++++++++++---------- 2 files changed, 119 insertions(+), 108 deletions(-) diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index d784463a..2f267d0a 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -51,11 +51,11 @@ internal class GamePathParser : IGamePathParser public string VfxToKey(string path) { - var match = _vfxRegexTmb.Match(path); + var match = GamePaths.Vfx.Tmb().Match(path); if (match.Success) return match.Groups["key"].Value.ToLowerInvariant(); - match = _vfxRegexPap.Match(path); + match = GamePaths.Vfx.Pap().Match(path); return match.Success ? match.Groups["key"].Value.ToLowerInvariant() : string.Empty; } @@ -136,11 +136,11 @@ internal class GamePathParser : IGamePathParser { var regexes = new Regex[] { - GamePathManager.Character.Tex.Regex(), - GamePathManager.Character.Tex.FolderRegex(), - GamePathManager.Character.Tex.SkinRegex(), - GamePathManager.Character.Tex.CatchlightRegex(), - GamePathManager.Character.Tex.DecalRegex(), + GamePaths.Character.Tex.Regex(), + GamePaths.Character.Tex.FolderRegex(), + GamePaths.Character.Tex.SkinRegex(), + GamePaths.Character.Tex.CatchlightRegex(), + GamePaths.Character.Tex.DecalRegex(), }; foreach (var regex in regexes) { @@ -154,32 +154,32 @@ internal class GamePathParser : IGamePathParser var match = (fileType, objectType) switch { - (FileType.Font, ObjectType.Font) => GamePathManager.Font.Regex().Match(path), - (FileType.Imc, ObjectType.Weapon) => GamePathManager.Weapon.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Monster) => GamePathManager.Monster.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Equipment) => GamePathManager.Equipment.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Accessory) => GamePathManager.Accessory.Imc.Regex().Match(path), - (FileType.Model, ObjectType.Weapon) => GamePathManager.Weapon.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Monster) => GamePathManager.Monster.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Equipment) => GamePathManager.Equipment.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Accessory) => GamePathManager.Accessory.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Character) => GamePathManager.Character.Mdl.Regex().Match(path), - (FileType.Material, ObjectType.Weapon) => GamePathManager.Weapon.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Monster) => GamePathManager.Monster.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Equipment) => GamePathManager.Equipment.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Accessory) => GamePathManager.Accessory.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Character) => GamePathManager.Character.Mtrl.Regex().Match(path), - (FileType.Texture, ObjectType.Weapon) => GamePathManager.Weapon.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Monster) => GamePathManager.Monster.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.DemiHuman) => GamePathManager.DemiHuman.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Equipment) => GamePathManager.Equipment.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Accessory) => GamePathManager.Accessory.Tex.Regex().Match(path), + (FileType.Font, ObjectType.Font) => GamePaths.Font.Regex().Match(path), + (FileType.Imc, ObjectType.Weapon) => GamePaths.Weapon.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Monster) => GamePaths.Monster.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.DemiHuman) => GamePaths.DemiHuman.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Equipment) => GamePaths.Equipment.Imc.Regex().Match(path), + (FileType.Imc, ObjectType.Accessory) => GamePaths.Accessory.Imc.Regex().Match(path), + (FileType.Model, ObjectType.Weapon) => GamePaths.Weapon.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Monster) => GamePaths.Monster.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Equipment) => GamePaths.Equipment.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Accessory) => GamePaths.Accessory.Mdl.Regex().Match(path), + (FileType.Model, ObjectType.Character) => GamePaths.Character.Mdl.Regex().Match(path), + (FileType.Material, ObjectType.Weapon) => GamePaths.Weapon.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Monster) => GamePaths.Monster.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Equipment) => GamePaths.Equipment.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Accessory) => GamePaths.Accessory.Mtrl.Regex().Match(path), + (FileType.Material, ObjectType.Character) => GamePaths.Character.Mtrl.Regex().Match(path), + (FileType.Texture, ObjectType.Weapon) => GamePaths.Weapon.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Monster) => GamePaths.Monster.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.DemiHuman) => GamePaths.DemiHuman.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Equipment) => GamePaths.Equipment.Tex.Regex().Match(path), + (FileType.Texture, ObjectType.Accessory) => GamePaths.Accessory.Tex.Regex().Match(path), (FileType.Texture, ObjectType.Character) => TestCharacterTextures(path), - (FileType.Texture, ObjectType.Icon) => GamePathManager.Icon.Regex().Match(path), - (FileType.Texture, ObjectType.Map) => GamePathManager.Map.Regex().Match(path), + (FileType.Texture, ObjectType.Icon) => GamePaths.Icon.Regex().Match(path), + (FileType.Texture, ObjectType.Map) => GamePaths.Map.Regex().Match(path), _ => Match.Empty, }; @@ -299,10 +299,4 @@ internal class GamePathParser : IGamePathParser return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant); } - - - private readonly Regex _vfxRegexTmb = new(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", - RegexOptions.Compiled | RegexOptions.IgnoreCase); } diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index 3d70c13d..22d5ac69 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -7,14 +7,9 @@ namespace Penumbra.GameData.Data; public static partial class GamePaths { - private static readonly Regex RaceCodeRegex = new(@"c(?'racecode'\d{4})", RegexOptions.Compiled); - - //[GeneratedRegex(@"c(?'racecode'\d{4})")] + [GeneratedRegex(@"c(?'racecode'\d{4})")] public static partial Regex RaceCodeParser(); - public static partial Regex RaceCodeParser() - => RaceCodeRegex; - public static GenderRace ParseRaceCode(string path) { var match = RaceCodeParser().Match(path); @@ -28,8 +23,8 @@ public static partial class GamePaths { public static partial class Imc { - // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc")] + public static partial Regex Regex(); public static string Path(SetId monsterId, SetId bodyId) => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; @@ -37,8 +32,8 @@ public static partial class GamePaths public static partial class Mdl { - // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl")] + public static partial Regex Regex(); public static string Path(SetId monsterId, SetId bodyId) => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/m{monsterId.Value:D4}b{bodyId.Value:D4}.mdl"; @@ -46,8 +41,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string Path(SetId monsterId, SetId bodyId, byte variant, string suffix) => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_m{monsterId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; @@ -55,8 +51,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex")] + public static partial Regex Regex(); public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; @@ -85,8 +82,8 @@ public static partial class GamePaths { public static partial class Imc { - // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc")] + public static partial Regex Regex(); public static string Path(SetId weaponId, SetId bodyId) => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; @@ -94,8 +91,8 @@ public static partial class GamePaths public static partial class Mdl { - // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl")] + public static partial Regex Regex(); public static string Path(SetId weaponId, SetId bodyId) => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/w{weaponId.Value:D4}b{bodyId.Value:D4}.mdl"; @@ -103,8 +100,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string Path(SetId weaponId, SetId bodyId, byte variant, string suffix) => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_w{weaponId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; @@ -112,8 +110,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex")] + public static partial Regex Regex(); public static string Path(SetId weaponId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_w{weaponId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; @@ -124,8 +123,8 @@ public static partial class GamePaths { public static partial class Imc { - // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc")] + public static partial Regex Regex(); public static string Path(SetId demiId, SetId equipId) => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; @@ -133,8 +132,8 @@ public static partial class GamePaths public static partial class Mdl { - // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl")] + public static partial Regex Regex(); public static string Path(SetId demiId, SetId equipId, EquipSlot slot) => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/model/d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; @@ -142,8 +141,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, string suffix) => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/material/v{variant:D4}/mt_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; @@ -151,8 +151,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + public static partial Regex Regex(); public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; @@ -163,8 +164,8 @@ public static partial class GamePaths { public static partial class Imc { - // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc")] + public static partial Regex Regex(); public static string Path(SetId equipId) => $"chara/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; @@ -172,8 +173,8 @@ public static partial class GamePaths public static partial class Mdl { - // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl")] + public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot) => $"chara/equipment/e{equipId.Value:D4}/model/c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; @@ -181,8 +182,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) => $"{FolderPath(equipId, variant)}/mt_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; @@ -193,8 +195,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + public static partial Regex Regex(); public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') => $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; @@ -202,8 +205,8 @@ public static partial class GamePaths public static partial class Avfx { - //[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")] - //public static partial Regex Regex(); + [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")] + public static partial Regex Regex(); public static string Path(SetId equipId, byte effectId) => $"chara/equipment/e{equipId.Value:D4}/vfx/eff/ve{effectId:D4}.avfx"; @@ -211,8 +214,8 @@ public static partial class GamePaths public static partial class Decal { - //[GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")] - //public static partial Regex Regex(); + [GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")] + public static partial Regex Regex(); public static string Path(byte decalId) => $"chara/common/texture/decal_equip/-decal_{decalId:D3}.tex"; @@ -223,8 +226,8 @@ public static partial class GamePaths { public static partial class Imc { - // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc")] + public static partial Regex Regex(); public static string Path(SetId accessoryId) => $"chara/accessory/a{accessoryId.Value:D4}/a{accessoryId.Value:D4}.imc"; @@ -232,8 +235,8 @@ public static partial class GamePaths public static partial class Mdl { - // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl")] + public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot) => $"chara/accessory/a{accessoryId.Value:D4}/model/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl"; @@ -241,8 +244,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) => $"{FolderPath(accessoryId, variant)}/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; @@ -253,8 +257,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] + public static partial Regex Regex(); public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') => $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; @@ -280,8 +285,9 @@ public static partial class GamePaths { public static partial class Mdl { - // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl")] + public static partial Regex Regex(); public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type) => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; @@ -289,8 +295,9 @@ public static partial class GamePaths public static partial class Mtrl { - // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] + public static partial Regex Regex(); public static string FolderPath(GenderRace raceCode, BodySlot slot, SetId slotId, byte variant = byte.MaxValue) => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material{(variant != byte.MaxValue ? $"/v{variant:D4}" : string.Empty)}"; @@ -342,8 +349,9 @@ public static partial class GamePaths public static partial class Tex { - // [GeneratedRegex(@"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")] - // public static partial Regex Regex(); + [GeneratedRegex( + @"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")] + public static partial Regex Regex(); public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false, CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0') @@ -353,35 +361,44 @@ public static partial class GamePaths + $"c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - // [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")] - // public static partial Regex CatchlightRegex(); + [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")] + public static partial Regex CatchlightRegex(); - // [GeneratedRegex(@"chara/common/texture/skin(?'skin'.*)\.tex")] - // public static partial Regex SkinRegex(); + [GeneratedRegex(@"chara/common/texture/skin(?'skin'.*)\.tex")] + public static partial Regex SkinRegex(); - // [GeneratedRegex(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex")] - // public static partial Regex DecalRegex(); + [GeneratedRegex(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex")] + public static partial Regex DecalRegex(); - // [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")] - // public static partial Regex FolderRegex(); + [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")] + public static partial Regex FolderRegex(); } } public static partial class Icon { - // [GeneratedRegex(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex")] + public static partial Regex Regex(); } public static partial class Map { - // [GeneratedRegex(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex")] - // public static partial Regex Regex(); + [GeneratedRegex(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex")] + public static partial Regex Regex(); } public static partial class Font { - // [GeneratedRegex(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt")] - // public static partial Regex Regex(); + [GeneratedRegex(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt")] + public static partial Regex Regex(); + } + + public static partial class Vfx + { + [GeneratedRegex(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.IgnoreCase)] + public static partial Regex Tmb(); + + [GeneratedRegex(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.IgnoreCase)] + public static partial Regex Pap(); } } From 123dd3aaccae6ab7c0f39dd547934edc6cc90bd9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Jan 2023 17:40:16 +0100 Subject: [PATCH 0715/2451] Update to new resourcehandler handling. --- Penumbra.GameData/Enums/WeaponCategory.cs | 2 -- Penumbra.GameData/Signatures.cs | 2 +- Penumbra/Dalamud.cs | 1 + Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 8 ++++---- Penumbra/UI/ConfigWindow.ResourceTab.cs | 9 +++++++++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Penumbra.GameData/Enums/WeaponCategory.cs b/Penumbra.GameData/Enums/WeaponCategory.cs index b40fa48a..4128361f 100644 --- a/Penumbra.GameData/Enums/WeaponCategory.cs +++ b/Penumbra.GameData/Enums/WeaponCategory.cs @@ -1,5 +1,3 @@ -using System; - namespace Penumbra.GameData.Enums; public enum WeaponCategory : byte diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs index aae686dc..fd6bdcfd 100644 --- a/Penumbra.GameData/Signatures.cs +++ b/Penumbra.GameData/Signatures.cs @@ -4,7 +4,7 @@ public static class Sigs { // ResourceLoader.Debug public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; - public const string ResourceManager = "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0"; + public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; // ResourceLoader.Replacement public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00"; diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index 5be53335..d0784fa6 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -35,6 +35,7 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on private static readonly object? DalamudConfig; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index a168299e..02f1882f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -38,7 +38,7 @@ public unsafe partial class ResourceLoader } // A static pointer to the SE Resource Manager - [Signature( Sigs.ResourceManager, ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] + [Signature( Sigs.ResourceManager, ScanType = ScanType.StaticAddress)] public static ResourceManager** ResourceManager; // Gather some debugging data about penumbra-loaded objects. @@ -177,11 +177,11 @@ public unsafe partial class ResourceLoader ref var manager = ref *ResourceManager; foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) ) { - var graph = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )resourceType; + ref var graph = ref manager->ResourceGraph->ContainerArraySpan[(int) resourceType]; for( var i = 0; i < 20; ++i ) { - var map = ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )graph->CategoryMaps[ i ]; - if( map != null ) + var map = graph.CategoryMapsSpan[i]; + if( map.Value != null ) { action( resourceType, map, i ); } diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index ce6bf442..4af5b271 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -9,6 +10,7 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData; using Penumbra.Interop.Loader; using Penumbra.String.Classes; @@ -42,6 +44,13 @@ public partial class ConfigWindow return; } + unsafe + { + Dalamud.SigScanner.TryGetStaticAddressFromSig( Sigs.ResourceManager, out var x ); + ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{(ulong) ResourceLoader.ResourceManager - (ulong) Dalamud.SigScanner.Module.BaseAddress:X})" ); + ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); + } + // Filter for resources containing the input string. ImGui.SetNextItemWidth( -1 ); ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); From 832b1163e0932e1290b89a6111779cd80579e892 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Jan 2023 17:56:26 +0100 Subject: [PATCH 0716/2451] More resourcehandler fixes. --- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 5 ++--- Penumbra/UI/ConfigWindow.ResourceTab.cs | 13 ++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 02f1882f..ad1d69d7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -153,9 +153,8 @@ public unsafe partial class ResourceLoader ref var manager = ref *ResourceManager; var catIdx = ( uint )cat >> 0x18; cat = ( ResourceCategory )( ushort )cat; - var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; - var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ], - ( uint )ext ); + ref var category = ref manager->ResourceGraph->ContainerArraySpan[(int) cat]; + var extMap = FindInMap( category.CategoryMapsSpan[ (int) catIdx ].Value, ( uint )ext ); if( extMap == null ) { return null; diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 4af5b271..51c64252 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -44,13 +44,6 @@ public partial class ConfigWindow return; } - unsafe - { - Dalamud.SigScanner.TryGetStaticAddressFromSig( Sigs.ResourceManager, out var x ); - ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{(ulong) ResourceLoader.ResourceManager - (ulong) Dalamud.SigScanner.Module.BaseAddress:X})" ); - ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); - } - // Filter for resources containing the input string. ImGui.SetNextItemWidth( -1 ); ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); @@ -65,6 +58,12 @@ public partial class ConfigWindow { ResourceLoader.IterateGraphs( DrawCategoryContainer ); } + ImGui.NewLine(); + unsafe + { + ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{( ulong )ResourceLoader.ResourceManager - ( ulong )Dalamud.SigScanner.Module.BaseAddress:X})" ); + ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); + } } private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) From a11e1d464b34cfe9594b9e93ef158844f358ab51 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Jan 2023 18:03:37 +0100 Subject: [PATCH 0717/2451] Test higher dotnet version. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57401edb..a317f236 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.100-preview.7.22377.5' + dotnet-version: '7.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 565ec74f..490a171f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.100-preview.7.22377.5' + dotnet-version: '7.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 10ff26d9..33afd0ee 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.100-preview.7.22377.5' + dotnet-version: '7.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 513df2beac41d4c09ff3bd4f78a893408556e1e8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 20 Jan 2023 09:42:37 +0000 Subject: [PATCH 0718/2451] [CI] Updating repo.json for refs/tags/0.6.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 02bdd07e..5047a31e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.3.0", - "TestingAssemblyVersion": "0.6.3.0", + "AssemblyVersion": "0.6.3.1", + "TestingAssemblyVersion": "0.6.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From deb630795df26f671a1053729b4595b3491e355f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 12:56:36 +0100 Subject: [PATCH 0719/2451] Move Frameworkmanager to OtterGui. --- OtterGui | 2 +- .../Interop/Resolver/PathResolver.Subfiles.cs | 16 ++- Penumbra/Penumbra.cs | 2 +- Penumbra/Util/FrameworkManager.cs | 99 ------------------- 4 files changed, 9 insertions(+), 110 deletions(-) delete mode 100644 Penumbra/Util/FrameworkManager.cs diff --git a/OtterGui b/OtterGui index d018a822..43b78fd8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d018a822212a68492771db1ee6a8921df36f677d +Subproject commit 43b78fd8dcdc12e0c2ba831bd037ad8e8a415e0e diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 3359270f..a705fd6d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -43,15 +43,13 @@ public unsafe partial class PathResolver { switch( type ) { - case ResourceType.Tex: - case ResourceType.Shpk: - if( _mtrlData.Value.Valid ) - { - collection = _mtrlData.Value; - return true; - } - - break; + case ResourceType.Tex when _mtrlData.Value.Valid: + case ResourceType.Shpk when _mtrlData.Value.Valid: + collection = _mtrlData.Value; + return true; + case ResourceType.Scd when _avfxData.Value.Valid: + collection = _avfxData.Value; + return true; case ResourceType.Atex when _avfxData.Value.Valid: collection = _avfxData.Value; return true; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 79f6ba77..2c90c01d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -99,7 +99,7 @@ public class Penumbra : IDalamudPlugin ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ); Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); - Framework = new FrameworkManager(); + Framework = new FrameworkManager(Dalamud.Framework, Log); CharacterUtility = new CharacterUtility(); Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); Config = Configuration.Load(); diff --git a/Penumbra/Util/FrameworkManager.cs b/Penumbra/Util/FrameworkManager.cs deleted file mode 100644 index 5a52d340..00000000 --- a/Penumbra/Util/FrameworkManager.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Game; - -namespace Penumbra.Util; - -// Manage certain actions to only occur on framework updates. -public class FrameworkManager : IDisposable -{ - private readonly Dictionary< string, Action > _important = new(); - private readonly Dictionary< string, Action > _delayed = new(); - - public FrameworkManager() - => Dalamud.Framework.Update += OnUpdate; - - // Register an action that is not time critical. - // One action per frame will be executed. - // On dispose, any remaining actions will be executed. - public void RegisterDelayed( string tag, Action action ) - { - lock( _delayed ) - { - _delayed[ tag ] = action; - } - } - - // Register an action that should be executed on the next frame. - // All of those actions will be executed in the next frame. - // If there are more than one, they will be launched in separated tasks, but waited for. - public void RegisterImportant( string tag, Action action ) - { - lock( _important ) - { - _important[ tag ] = action; - } - } - - public void Dispose() - { - Dalamud.Framework.Update -= OnUpdate; - foreach( var (_, action) in _delayed ) - { - action(); - } - - _delayed.Clear(); - } - - private void OnUpdate( Framework _ ) - { - try - { - HandleOne( _delayed ); - HandleAllTasks( _important ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Problem saving data:\n{e}" ); - } - } - - private static void HandleOne( IDictionary< string, Action > dict ) - { - if( dict.Count == 0 ) - { - return; - } - - Action action; - lock( dict ) - { - ( var key, action ) = dict.First(); - dict.Remove( key ); - } - - action(); - } - - private static void HandleAllTasks( IDictionary< string, Action > dict ) - { - if( dict.Count < 2 ) - { - HandleOne( dict ); - } - else - { - Task[] tasks; - lock( dict ) - { - tasks = dict.Values.Select( Task.Run ).ToArray(); - dict.Clear(); - } - - Task.WaitAll( tasks ); - } - } -} \ No newline at end of file From 471005b5b1f28bc2f7abc7965a8075b3d0683138 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 15:15:49 +0100 Subject: [PATCH 0720/2451] Fix group banner identification. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 0a030b28..20fbe679 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -13,6 +13,7 @@ using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; @@ -204,21 +205,15 @@ public sealed partial class ActorManager : IDisposable public unsafe bool ResolvePartyBannerPlayer(ScreenActor type, out ActorIdentifier id) { id = ActorIdentifier.Invalid; - var addon = _gameGui.GetAddonByName("BannerParty"); - if (addon == IntPtr.Zero) + var addon = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.BannerParty); + if (addon == null || !addon->IsAgentActive()) return false; var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen; if (idx is < 0 or > 7) return true; - if (idx == 0) - { - id = GetCurrentPlayer(); - return true; - } - - var obj = GroupManager.Instance()->GetPartyMemberByIndex(idx - 1); + var obj = GroupManager.Instance()->GetPartyMemberByIndex(idx); if (obj != null) id = CreatePlayer(new ByteString(obj->Name), obj->HomeWorld); From 7ab1426a2c1f386cb2d51ec631b92cf9beafbb2e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 15:16:19 +0100 Subject: [PATCH 0721/2451] Change ResourceHandle strings a bit. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 2 +- Penumbra/Interop/Structs/ResourceHandle.cs | 11 ++++++++--- Penumbra/UI/ConfigWindow.Misc.cs | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index b02cfff6..89237ff7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -182,7 +182,7 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) || gamePath.Length == 0 ) + if( !fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0 ) { return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 59b4b942..77c212ef 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -3,6 +3,8 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Structs; @@ -40,7 +42,7 @@ public unsafe struct ResourceHandle public const int SsoSize = 15; - public byte* FileName() + public byte* FileNamePtr() { if( FileNameLength > SsoSize ) { @@ -53,8 +55,11 @@ public unsafe struct ResourceHandle } } - public ReadOnlySpan< byte > FileNameSpan() - => new(FileName(), FileNameLength); + public ByteString FileName() + => ByteString.FromByteStringUnsafe( FileNamePtr(), FileNameLength, true ); + + public bool GamePath( out Utf8GamePath path ) + => Utf8GamePath.FromSpan( new ReadOnlySpan< byte >( FileNamePtr(), FileNameLength ), out path ); [FieldOffset( 0x00 )] public void** VTable; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 38a98b50..ed180fd2 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -33,7 +33,7 @@ public partial class ConfigWindow // Draw the name of a resource file. private static unsafe void Text( ResourceHandle* resource ) - => Text( resource->FileName(), resource->FileNameLength ); + => Text( resource->FileName().Path, resource->FileNameLength ); // Draw a ByteString as a selectable. internal static unsafe bool Selectable( ByteString s, bool selected ) From 24fda725a2992c65765c57a63e77db155d16f468 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 15:16:53 +0100 Subject: [PATCH 0722/2451] Add GameEventManager, change cutscene character and subfile container resets. --- Penumbra.GameData/Signatures.cs | 9 +- Penumbra/Interop/GameEventManager.cs | 126 ++++++++++++++++++ .../Interop/Resolver/CutsceneCharacters.cs | 90 ++++--------- .../Resolver/IdentifiedCollectionCache.cs | 29 ++-- .../Interop/Resolver/PathResolver.Subfiles.cs | 22 ++- Penumbra/Interop/Resolver/PathResolver.cs | 9 +- Penumbra/Penumbra.cs | 5 +- 7 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 Penumbra/Interop/GameEventManager.cs diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs index fd6bdcfd..5b7f139b 100644 --- a/Penumbra.GameData/Signatures.cs +++ b/Penumbra.GameData/Signatures.cs @@ -3,8 +3,7 @@ namespace Penumbra.GameData; public static class Sigs { // ResourceLoader.Debug - public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; - public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; + public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; // ResourceLoader.Replacement public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00"; @@ -19,10 +18,10 @@ public static class Sigs public const string LoadTexFileExtern = "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8"; public const string LoadMdlFileExtern = "E8 ?? ?? ?? ?? EB 02 B0 F1"; - // CutsceneCharacters - public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; + // GameEventManager + public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; + public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; - // IdentifiedCollectionCache public const string CharacterDestructor = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05"; diff --git a/Penumbra/Interop/GameEventManager.cs b/Penumbra/Interop/GameEventManager.cs new file mode 100644 index 00000000..1549f888 --- /dev/null +++ b/Penumbra/Interop/GameEventManager.cs @@ -0,0 +1,126 @@ +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.GameData; +using System; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop; + +public unsafe class GameEventManager : IDisposable +{ + public GameEventManager() + { + SignatureHelper.Initialise( this ); + _characterDtorHook.Enable(); + _copyCharacterHook.Enable(); + _resourceHandleDestructorHook.Enable(); + } + + public void Dispose() + { + _characterDtorHook.Dispose(); + _copyCharacterHook.Dispose(); + _resourceHandleDestructorHook.Dispose(); + } + + #region Character Destructor + + private delegate void CharacterDestructorDelegate( Character* character ); + + [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] + private readonly Hook< CharacterDestructorDelegate > _characterDtorHook = null!; + + private void CharacterDestructorDetour( Character* character ) + { + if( CharacterDestructor != null ) + { + foreach( var subscriber in CharacterDestructor.GetInvocationList() ) + { + try + { + ( ( CharacterDestructorEvent )subscriber ).Invoke( character ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( CharacterDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( CharacterDestructor )} triggered with 0x{( nint )character:X}." ); + _characterDtorHook.Original( character ); + } + + public delegate void CharacterDestructorEvent( Character* character ); + public event CharacterDestructorEvent? CharacterDestructor; + + #endregion + + #region Copy Character + + private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); + + [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] + private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; + + private ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + { + if( CopyCharacter != null ) + { + foreach( var subscriber in CopyCharacter.GetInvocationList() ) + { + try + { + ( ( CopyCharacterEvent )subscriber ).Invoke( ( Character* )target, ( Character* )source ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( CopyCharacter )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( CopyCharacter )} triggered with target 0x{( nint )target:X} and source 0x{( nint )source:X}." ); + return _copyCharacterHook.Original( target, source, unk ); + } + + public delegate void CopyCharacterEvent( Character* target, Character* source ); + public event CopyCharacterEvent? CopyCharacter; + + #endregion + + #region ResourceHandle Destructor + + private delegate IntPtr ResourceHandleDestructorDelegate( ResourceHandle* handle ); + + [Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )] + private readonly Hook< ResourceHandleDestructorDelegate > _resourceHandleDestructorHook = null!; + + private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) + { + if( ResourceHandleDestructor != null ) + { + foreach( var subscriber in ResourceHandleDestructor.GetInvocationList() ) + { + try + { + ( ( ResourceHandleDestructorEvent )subscriber ).Invoke( handle ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( ResourceHandleDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( ResourceHandleDestructor )} triggered with 0x{( nint )handle:X}." ); + return _resourceHandleDestructorHook!.Original( handle ); + } + + public delegate void ResourceHandleDestructorEvent( ResourceHandle* handle ); + public event ResourceHandleDestructorEvent? ResourceHandleDestructor; + + #endregion +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index 7a3b9cf7..a1e2961b 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -2,11 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; +using FFXIVClientStructs.FFXIV.Client.Game.Character; namespace Penumbra.Interop.Resolver; @@ -16,17 +12,18 @@ public class CutsceneCharacters : IDisposable public const int CutsceneSlots = 40; public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; - private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); + private readonly GameEventManager _events; + private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) .Where( i => Dalamud.Objects[ i ] != null ) .Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) ); - public CutsceneCharacters() + public CutsceneCharacters(GameEventManager events) { - SignatureHelper.Initialise( this ); - Dalamud.Conditions.ConditionChange += Reset; + _events = events; + Enable(); } // Get the related actor to a cutscene actor. @@ -53,71 +50,36 @@ public class CutsceneCharacters : IDisposable return -1; } - public void Reset( ConditionFlag flag, bool value ) + public unsafe void Enable() { - switch( flag ) - { - case ConditionFlag.BetweenAreas: - case ConditionFlag.BetweenAreas51: - if( !value ) - { - return; - } - - break; - case ConditionFlag.OccupiedInCutSceneEvent: - case ConditionFlag.WatchingCutscene: - case ConditionFlag.WatchingCutscene78: - if( value ) - { - return; - } - - break; - default: return; - } - - for( var i = 0; i < _copiedCharacters.Length; ++i ) - { - _copiedCharacters[ i ] = -1; - } + _events.CopyCharacter += OnCharacterCopy; + _events.CharacterDestructor += OnCharacterDestructor; } - public void Enable() - => _copyCharacterHook.Enable(); - - public void Disable() - => _copyCharacterHook.Disable(); + public unsafe void Disable() + { + _events.CopyCharacter -= OnCharacterCopy; + _events.CharacterDestructor -= OnCharacterDestructor; + } public void Dispose() + => Disable(); + + private unsafe void OnCharacterDestructor( Character* character ) { - _copyCharacterHook.Dispose(); - Dalamud.Conditions.ConditionChange -= Reset; + if( character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + { + var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[ idx ] = -1; + } } - private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); - - [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] - private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; - - private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + private unsafe void OnCharacterCopy( Character* target, Character* source ) { - try + if( target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) { - if( target != null && target->ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) - { - var parent = source == null || source->ObjectIndex is < 0 or >= CutsceneStartIdx - ? -1 - : source->ObjectIndex; - _copiedCharacters[ target->ObjectIndex - CutsceneStartIdx ] = ( short )parent; - Penumbra.Log.Debug( $"Set cutscene character {target->ObjectIndex} to {parent}." ); - } + var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = (short) (source != null ? source->GameObject.ObjectIndex : -1); } - catch - { - // ignored - } - - return _copyCharacterHook.Original( target, source, unk ); } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index 12fd6749..b3ffc5d3 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -1,25 +1,23 @@ using System; using System.Collections; using System.Collections.Generic; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; -using Penumbra.GameData; using Penumbra.GameData.Actors; namespace Penumbra.Interop.Resolver; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > { + private readonly GameEventManager _events; private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); private bool _dirty = false; private bool _enabled = false; - public IdentifiedCollectionCache() + public IdentifiedCollectionCache(GameEventManager events) { - SignatureHelper.Initialise( this ); + _events = events; } public void Enable() @@ -32,8 +30,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; Penumbra.TempMods.CollectionChanged += CollectionChangeClear; Dalamud.ClientState.TerritoryChanged += TerritoryClear; - _characterDtorHook.Enable(); - _enabled = true; + _events.CharacterDestructor += OnCharacterDestruct; + _enabled = true; } public void Disable() @@ -46,8 +44,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; Dalamud.ClientState.TerritoryChanged -= TerritoryClear; - _characterDtorHook.Disable(); - _enabled = false; + _events.CharacterDestructor -= OnCharacterDestruct; + _enabled = false; } public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) @@ -82,7 +80,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt public void Dispose() { Disable(); - _characterDtorHook.Dispose(); GC.SuppressFinalize( this ); } @@ -116,14 +113,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt private void TerritoryClear( object? _1, ushort _2 ) => _dirty = _cache.Count > 0; - private delegate void CharacterDestructorDelegate( Character* character ); - - [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] - private Hook< CharacterDestructorDelegate > _characterDtorHook = null!; - - private void CharacterDestructorDetour( Character* character ) - { - _cache.Remove( ( IntPtr )character ); - _characterDtorHook.Original( character ); - } + private void OnCharacterDestruct( Character* character ) + => _cache.Remove( ( IntPtr )character ); } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index a705fd6d..016ca38d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -24,18 +24,20 @@ public unsafe partial class PathResolver // Thus, we need to ensure the correct files are loaded when a material is loaded. public class SubfileHelper : IDisposable, IReadOnlyCollection< KeyValuePair< IntPtr, ResolveData > > { - private readonly ResourceLoader _loader; + private readonly ResourceLoader _loader; + private readonly GameEventManager _events; private readonly ThreadLocal< ResolveData > _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal< ResolveData > _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); - public SubfileHelper( ResourceLoader loader ) + public SubfileHelper( ResourceLoader loader, GameEventManager events ) { SignatureHelper.Initialise( this ); _loader = loader; + _events = events; } // Check specifically for shpk and tex files whether we are currently in a material load. @@ -85,7 +87,7 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Enable(); _loader.ResourceLoadCustomization += SubfileLoadHandler; _loader.ResourceLoaded += SubfileContainerRequested; - _loader.FileLoaded += SubfileContainerLoaded; + _events.ResourceHandleDestructor += ResourceDestroyed; } public void Disable() @@ -95,7 +97,7 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Disable(); _loader.ResourceLoadCustomization -= SubfileLoadHandler; _loader.ResourceLoaded -= SubfileContainerRequested; - _loader.FileLoaded -= SubfileContainerLoaded; + _events.ResourceHandleDestructor -= ResourceDestroyed; } public void Dispose() @@ -121,16 +123,8 @@ public unsafe partial class PathResolver } } - private void SubfileContainerLoaded( ResourceHandle* handle, ByteString path, bool success, bool custom ) - { - switch( handle->FileType ) - { - case ResourceType.Mtrl: - case ResourceType.Avfx: - _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); - break; - } - } + private void ResourceDestroyed( ResourceHandle* handle ) + => _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 2ece65f6..e00e9c45 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -24,10 +24,10 @@ public partial class PathResolver : IDisposable public bool Enabled { get; private set; } private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(); + private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); private static readonly DrawObjectState DrawObjects = new(); private static readonly BitArray ValidHumanModels; - internal static readonly IdentifiedCollectionCache IdentifiedCache = new(); + internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); private readonly AnimationState _animations; private readonly PathState _paths; private readonly MetaState _meta; @@ -43,7 +43,7 @@ public partial class PathResolver : IDisposable _animations = new AnimationState( DrawObjects ); _paths = new PathState( this ); _meta = new MetaState( _paths.HumanVTable ); - _subFiles = new SubfileHelper( _loader ); + _subFiles = new SubfileHelper( _loader, Penumbra.GameEvents ); } // The modified resolver that handles game path resolving. @@ -175,6 +175,9 @@ public partial class PathResolver : IDisposable internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections => _subFiles; + internal int SubfileCount + => _subFiles.Count; + internal ResolveData CurrentMtrlData => _subFiles.MtrlData; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2c90c01d..9d4d31e1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -54,6 +54,7 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; @@ -88,11 +89,12 @@ public class Penumbra : IDalamudPlugin try { Dalamud.Initialize( pluginInterface ); - Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); + Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); Log = new Logger(); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); IsValidSourceRepo = CheckSourceRepo(); + GameEvents = new GameEventManager(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); @@ -305,6 +307,7 @@ public class Penumbra : IDalamudPlugin PathResolver?.Dispose(); ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); + GameEvents?.Dispose(); CharacterUtility?.Dispose(); Performance?.Dispose(); } From 853fe8644cae26f16cde4370bebf26c8dd32b8c7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 15:17:08 +0100 Subject: [PATCH 0723/2451] Add number of subfiles to debug tab. --- Penumbra/UI/ConfigWindow.DebugTab.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 0948231e..3dc61dde 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -112,7 +112,7 @@ public partial class ConfigWindow PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); } - [Conditional("DEBUG")] + [Conditional( "DEBUG" )] private static void DrawPerformanceTab() { ImGui.NewLine(); @@ -285,6 +285,10 @@ public partial class ConfigWindow ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentAvfxData.ModCollection.Name ); ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentAvfxData.AssociatedGameObject:X}" ); + ImGuiUtil.DrawTableColumn( "Current Resources" ); + ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.SubfileCount.ToString() ); + ImGui.TableNextColumn(); + foreach( var (resource, resolve) in _window._penumbra.PathResolver.ResourceCollections ) { ImGuiUtil.DrawTableColumn( $"0x{resource:X}" ); From a9a5f91c907cb105fe9bf9fc2ab3457c703a444e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 16:09:46 +0100 Subject: [PATCH 0724/2451] Maybe fix animation handling after redraws (esp. for PLD with shield), maybe break everything else. --- Penumbra/Interop/Resolver/PathResolver.AnimationState.cs | 6 ++++++ Penumbra/Interop/Resolver/PathResolver.cs | 6 ++++++ Penumbra/UI/ConfigWindow.DebugTab.cs | 1 + 3 files changed, 13 insertions(+) diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index fcd5c2a0..60822ad0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -58,6 +58,12 @@ public unsafe partial class PathResolver break; } + if( _drawObjectState.LastGameObject != null ) + { + resolveData = _drawObjectState.LastCreatedCollection; + return true; + } + resolveData = ResolveData.Invalid; return false; } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index e00e9c45..46e075c7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -183,4 +183,10 @@ public partial class PathResolver : IDisposable internal ResolveData CurrentAvfxData => _subFiles.AvfxData; + + internal ResolveData LastGameObjectData + => DrawObjects.LastCreatedCollection; + + internal unsafe nint LastGameObject + => (nint) DrawObjects.LastGameObject; } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 3dc61dde..f25ed1d4 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -223,6 +223,7 @@ public partial class ConfigWindow return; } + ImGui.TextUnformatted( $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) ) { if( drawTree ) From e6d73971e99def23848a826e5c6a9c57cb303f5c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 16:39:15 +0100 Subject: [PATCH 0725/2451] Change resolving to consider every resource category to fix music resolving. --- .../Loader/ResourceLoader.Replacement.cs | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 89237ff7..437a51b6 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -133,23 +133,46 @@ public unsafe partial class ResourceLoader } path = path.ToLower(); - if( category == ResourceCategory.Ui ) + switch( category ) { - var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); - return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); - } - - if( ResolvePathCustomization != null ) - { - foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + // Only Interface collection. + case ResourceCategory.Ui: { - if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) - { - return ret; - } + var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); + return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); } - } + // Never allow changing scripts. + case ResourceCategory.UiScript: + case ResourceCategory.GameScript: + return ( null, ResolveData.Invalid ); + // Use actual resolving. + case ResourceCategory.Chara: + case ResourceCategory.Shader: + case ResourceCategory.Vfx: + if( ResolvePathCustomization != null ) + { + foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + { + if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) + { + return ret; + } + } + } + break; + // None of these files are ever associated with specific characters, + // always use the default resolver for now. + case ResourceCategory.Common: + case ResourceCategory.BgCommon: + case ResourceCategory.Bg: + case ResourceCategory.Cut: + case ResourceCategory.Exd: + case ResourceCategory.Music: + case ResourceCategory.Sound: + default: + break; + } return DefaultResolver( path ); } @@ -182,7 +205,7 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - if( !fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0 ) + if( !fileDescriptor->ResourceHandle->GamePath( out var gamePath ) || gamePath.Length == 0 ) { return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } From 5f63d4de382d680656d9381766a07d02d306371e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 23 Jan 2023 16:26:54 +0100 Subject: [PATCH 0726/2451] Change sounds to be able to be resolved. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 437a51b6..7b19c62a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -149,6 +149,7 @@ public unsafe partial class ResourceLoader case ResourceCategory.Chara: case ResourceCategory.Shader: case ResourceCategory.Vfx: + case ResourceCategory.Sound: if( ResolvePathCustomization != null ) { foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) @@ -169,7 +170,6 @@ public unsafe partial class ResourceLoader case ResourceCategory.Cut: case ResourceCategory.Exd: case ResourceCategory.Music: - case ResourceCategory.Sound: default: break; } From 0239c2f60b2bce7202e939e27493a00a7b0d07ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 Jan 2023 09:57:24 +0100 Subject: [PATCH 0727/2451] Glamourer changes --- Penumbra.GameData/Enums/EquipSlot.cs | 1 - Penumbra.GameData/Structs/CharacterArmor.cs | 10 ++--- Penumbra.GameData/Structs/CharacterWeapon.cs | 46 ++++++++++---------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 2afe9939..b23ae22d 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; namespace Penumbra.GameData.Enums; diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index ae0f9494..f8239c0e 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -4,19 +4,19 @@ using System.Runtime.InteropServices; namespace Penumbra.GameData.Structs; [StructLayout( LayoutKind.Explicit, Pack = 1 )] -public readonly struct CharacterArmor : IEquatable< CharacterArmor > +public struct CharacterArmor : IEquatable< CharacterArmor > { [FieldOffset( 0 )] - public readonly uint Value; + public uint Value; [FieldOffset( 0 )] - public readonly SetId Set; + public SetId Set; [FieldOffset( 2 )] - public readonly byte Variant; + public byte Variant; [FieldOffset( 3 )] - public readonly StainId Stain; + public StainId Stain; public CharacterArmor( SetId set, byte variant, StainId stain ) { diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index 0c25d66f..b94e0b05 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -3,28 +3,28 @@ using System.Runtime.InteropServices; namespace Penumbra.GameData.Structs; -[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 7 )] -public readonly struct CharacterWeapon : IEquatable< CharacterWeapon > +[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 7)] +public struct CharacterWeapon : IEquatable { - [FieldOffset( 0 )] - public readonly SetId Set; + [FieldOffset(0)] + public SetId Set; - [FieldOffset( 2 )] - public readonly WeaponType Type; + [FieldOffset(2)] + public WeaponType Type; - [FieldOffset( 4 )] - public readonly ushort Variant; + [FieldOffset(4)] + public ushort Variant; - [FieldOffset( 6 )] - public readonly StainId Stain; + [FieldOffset(6)] + public StainId Stain; public ulong Value - => ( ulong )Set | ( ( ulong )Type << 16 ) | ( ( ulong )Variant << 32 ) | ( ( ulong )Stain << 48 ); + => (ulong)Set | ((ulong)Type << 16) | ((ulong)Variant << 32) | ((ulong)Stain << 48); public override string ToString() => $"{Set},{Type},{Variant},{Stain}"; - public CharacterWeapon( SetId set, WeaponType type, ushort variant, StainId stain ) + public CharacterWeapon(SetId set, WeaponType type, ushort variant, StainId stain) { Set = set; Type = type; @@ -32,28 +32,28 @@ public readonly struct CharacterWeapon : IEquatable< CharacterWeapon > Stain = stain; } - public CharacterWeapon( ulong value ) + public CharacterWeapon(ulong value) { - Set = ( SetId )value; - Type = ( WeaponType )( value >> 16 ); - Variant = ( ushort )( value >> 32 ); - Stain = ( StainId )( value >> 48 ); + Set = (SetId)value; + Type = (WeaponType)(value >> 16); + Variant = (ushort)(value >> 32); + Stain = (StainId)(value >> 48); } public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); - public bool Equals( CharacterWeapon other ) + public bool Equals(CharacterWeapon other) => Value == other.Value; - public override bool Equals( object? obj ) - => obj is CharacterWeapon other && Equals( other ); + public override bool Equals(object? obj) + => obj is CharacterWeapon other && Equals(other); public override int GetHashCode() => Value.GetHashCode(); - public static bool operator ==( CharacterWeapon left, CharacterWeapon right ) + public static bool operator ==(CharacterWeapon left, CharacterWeapon right) => left.Value == right.Value; - public static bool operator !=( CharacterWeapon left, CharacterWeapon right ) + public static bool operator !=(CharacterWeapon left, CharacterWeapon right) => left.Value != right.Value; -} \ No newline at end of file +} From e716bbbc01ac69ff020f5e5b4ab0c633a2bca381 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 Jan 2023 09:57:36 +0100 Subject: [PATCH 0728/2451] Remove doubled skip from collection listing. --- Penumbra/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index e303e841..987f29c9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -431,7 +431,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IList< string > GetCollections() { CheckInitialized(); - return Penumbra.CollectionManager.Skip( 1 ).Select( c => c.Name ).ToArray(); + return Penumbra.CollectionManager.Select( c => c.Name ).ToArray(); } public string GetCurrentCollection() From 21e6a17d1c39104899adbc2c410298a2e6b964c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 Jan 2023 16:45:23 +0100 Subject: [PATCH 0729/2451] Some cleanup. --- .../Data/ObjectIdentification.cs | 72 +------------------ Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Util/ChatUtil.cs | 2 - 3 files changed, 2 insertions(+), 74 deletions(-) diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 4dfe21f2..9a45dcfc 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -13,8 +13,7 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Actors; using Action = Lumina.Excel.GeneratedSheets.Action; -using ObjectType = Penumbra.GameData.Enums.ObjectType; - +using ObjectType = Penumbra.GameData.Enums.ObjectType; namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier @@ -96,75 +95,6 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return true; } - private static ulong EquipmentKey(Item i) - { - var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; - var variant = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).B; - var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - return (model << 32) | (slot << 16) | variant; - } - - private static ulong WeaponKey(Item i, bool offhand) - { - var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; - var model = (ulong)quad.A; - var type = (ulong)quad.B; - var variant = (ulong)quad.C; - - return (model << 32) | (type << 16) | variant; - } - - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList(DataManager gameData) - { - var items = gameData.GetExcelSheet(Language)!; - var storage = new SortedList>(); - foreach (var item in items.Where(i - => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) - { - if (item.ModelMain != 0) - Add(storage, WeaponKey(item, false), item); - - if (item.ModelSub != 0) - Add(storage, WeaponKey(item, true), item); - } - - return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); - } - - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList(DataManager gameData) - { - var items = gameData.GetExcelSheet(Language)!; - var storage = new SortedList>(); - foreach (var item in items) - { - switch ((EquipSlot)item.EquipSlotCategory.Row) - { - // Accessories - case EquipSlot.RFinger: - case EquipSlot.Wrists: - case EquipSlot.Ears: - case EquipSlot.Neck: - // Equipment - case EquipSlot.Head: - case EquipSlot.Body: - case EquipSlot.Hands: - case EquipSlot.Legs: - case EquipSlot.Feet: - case EquipSlot.BodyHands: - case EquipSlot.BodyHandsLegsFeet: - case EquipSlot.BodyLegsFeet: - case EquipSlot.FullBody: - case EquipSlot.HeadBody: - case EquipSlot.LegsFeet: - case EquipSlot.ChestHands: - Add(storage, EquipmentKey(item), item); - break; - } - } - - return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); - } - private IReadOnlyDictionary> CreateActionList(DataManager gameData) { var sheet = gameData.GetExcelSheet(Language)!; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 987f29c9..7997f8e8 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -302,7 +302,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public string GetCollectionForType( Enums.ApiCollectionType type ) + public string GetCollectionForType( ApiCollectionType type ) { CheckInitialized(); if( !Enum.IsDefined( type ) ) diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs index 220da9bf..ecef1b57 100644 --- a/Penumbra/Util/ChatUtil.cs +++ b/Penumbra/Util/ChatUtil.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; From 2ef9d3d56ed2a730ca53471aa500b1d26754dee0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 29 Jan 2023 21:08:43 +0100 Subject: [PATCH 0730/2451] Some Glamourer stuff. --- OtterGui | 2 +- Penumbra.GameData/Data/DataSharer.cs | 2 +- Penumbra.GameData/Data/RestrictedGear.cs | 9 +-- Penumbra.GameData/Enums/FileType.cs | 79 ++++++++++++------------ Penumbra.GameData/Structs/SetId.cs | 8 ++- Penumbra/Dalamud.cs | 2 +- Penumbra/Penumbra.cs | 1 + 7 files changed, 55 insertions(+), 48 deletions(-) diff --git a/OtterGui b/OtterGui index 43b78fd8..2c73fd57 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 43b78fd8dcdc12e0c2ba831bd037ad8e8a415e0e +Subproject commit 2c73fd57bf53fc3c9f390db0acc9e176607c2dbc diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs index e1d118c8..57b11bea 100644 --- a/Penumbra.GameData/Data/DataSharer.cs +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -50,7 +50,7 @@ public abstract class DataSharer : IDisposable } catch (Exception ex) { - PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); + PluginLog.Error($"Error creating shared data for {tag}:\n{ex}"); return func(); } } diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs index 7aeac80a..6f59ae2d 100644 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -29,7 +29,7 @@ public sealed class RestrictedGear : DataSharer public readonly IReadOnlyDictionary MaleToFemale; public readonly IReadOnlyDictionary FemaleToMale; - internal RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) + public RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) : base(pi, language, 1) { _items = gameData.GetExcelSheet()!; @@ -78,14 +78,14 @@ public sealed class RestrictedGear : DataSharer var f2m = new Dictionary(); var rg = RaceGenderGroup.Where(c => c is not 0 and not uint.MaxValue).ToHashSet(); AddKnown(m2f, f2m); - UnhandledRestrictedGear(m2f, f2m, false); // Set this to true to create a print of unassigned gear on launch. + UnhandledRestrictedGear(rg, m2f, f2m, false); // Set this to true to create a print of unassigned gear on launch. return new Tuple, IReadOnlyDictionary, IReadOnlyDictionary>(rg, m2f, f2m); } // Add all unknown restricted gear and pair it with emperor's new gear on start up. // Can also print unhandled items. - private void UnhandledRestrictedGear(Dictionary m2f, Dictionary f2m, bool print) + private void UnhandledRestrictedGear(IReadOnlySet rg, Dictionary m2f, Dictionary f2m, bool print) { if (print) PluginLog.Information("#### MALE ONLY ######"); @@ -148,7 +148,7 @@ public sealed class RestrictedGear : DataSharer foreach (var item in _items.Where(i => i.EquipRestriction > 3)) { - if (RaceGenderSet.Contains((uint)item.ModelMain)) + if (rg.Contains((uint)item.ModelMain)) continue; ++unhandled; @@ -409,6 +409,7 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 38254, 38258); // Valentione Emissary's Jacket <-> Valentione Emissary's Ruffled Dress AddItem(m2f, f2m, 38255, 38259); // Valentione Emissary's Bottoms <-> Valentione Emissary's Culottes AddItem(m2f, f2m, 38256, 38260); // Valentione Emissary's Boots <-> Valentione Emissary's Boots + AddItem(m2f, f2m, 32393, 39302, false); // Edenmete Gown of Casting <- Gaia's Attire } // The racial starter sets are available for all 4 slots each, diff --git a/Penumbra.GameData/Enums/FileType.cs b/Penumbra.GameData/Enums/FileType.cs index e326ef17..14c077b8 100644 --- a/Penumbra.GameData/Enums/FileType.cs +++ b/Penumbra.GameData/Enums/FileType.cs @@ -1,45 +1,44 @@ using System.Collections.Generic; -namespace Penumbra.GameData.Enums -{ - public enum FileType : byte - { - Unknown, - Sound, - Imc, - Vfx, - Animation, - Pap, - MetaInfo, - Material, - Texture, - Model, - Shader, - Font, - Environment, - } +namespace Penumbra.GameData.Enums; - public static partial class Names +public enum FileType : byte +{ + Unknown, + Sound, + Imc, + Vfx, + Animation, + Pap, + MetaInfo, + Material, + Texture, + Model, + Shader, + Font, + Environment, +} + +public static partial class Names +{ + public static readonly Dictionary< string, FileType > ExtensionToFileType = new() { - public static readonly Dictionary< string, FileType > ExtensionToFileType = new() - { - { ".mdl", FileType.Model }, - { ".tex", FileType.Texture }, - { ".mtrl", FileType.Material }, - { ".atex", FileType.Animation }, - { ".avfx", FileType.Vfx }, - { ".scd", FileType.Sound }, - { ".imc", FileType.Imc }, - { ".pap", FileType.Pap }, - { ".eqp", FileType.MetaInfo }, - { ".eqdp", FileType.MetaInfo }, - { ".est", FileType.MetaInfo }, - { ".exd", FileType.MetaInfo }, - { ".exh", FileType.MetaInfo }, - { ".shpk", FileType.Shader }, - { ".shcd", FileType.Shader }, - { ".fdt", FileType.Font }, - { ".envb", FileType.Environment }, - }; - } + { ".mdl", FileType.Model }, + { ".tex", FileType.Texture }, + { ".mtrl", FileType.Material }, + { ".atex", FileType.Animation }, + { ".avfx", FileType.Vfx }, + { ".scd", FileType.Sound }, + { ".imc", FileType.Imc }, + { ".pap", FileType.Pap }, + { ".eqp", FileType.MetaInfo }, + { ".eqdp", FileType.MetaInfo }, + { ".est", FileType.MetaInfo }, + { ".exd", FileType.MetaInfo }, + { ".exh", FileType.MetaInfo }, + { ".shpk", FileType.Shader }, + { ".shcd", FileType.Shader }, + { ".fdt", FileType.Font }, + { ".envb", FileType.Environment }, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/SetId.cs b/Penumbra.GameData/Structs/SetId.cs index 79674fac..5de82c68 100644 --- a/Penumbra.GameData/Structs/SetId.cs +++ b/Penumbra.GameData/Structs/SetId.cs @@ -2,7 +2,7 @@ using System; namespace Penumbra.GameData.Structs; -public readonly struct SetId : IComparable< SetId > +public readonly struct SetId : IComparable< SetId >, IEquatable, IEquatable { public readonly ushort Value; @@ -15,6 +15,12 @@ public readonly struct SetId : IComparable< SetId > public static explicit operator ushort( SetId id ) => id.Value; + public bool Equals(SetId other) + => Value == other.Value; + + public bool Equals(ushort other) + => Value == other; + public override string ToString() => Value.ToString(); diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs index d0784fa6..e9366d9a 100644 --- a/Penumbra/Dalamud.cs +++ b/Penumbra/Dalamud.cs @@ -35,7 +35,7 @@ public class Dalamud [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on private static readonly object? DalamudConfig; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9d4d31e1..739975e5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -103,6 +103,7 @@ public class Penumbra : IDalamudPlugin Framework = new FrameworkManager(Dalamud.Framework, Log); CharacterUtility = new CharacterUtility(); + Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); Config = Configuration.Load(); From d5efe3f7488aaecac1f426fbbb1b8a84aa38b4a7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 Jan 2023 18:04:48 +0100 Subject: [PATCH 0731/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 279b4b02..6774d577 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -32,10 +32,21 @@ public partial class ConfigWindow Add6_1_1( ret ); Add6_2_0( ret ); Add6_3_0( ret ); + Add6_4_0( ret ); return ret; } + private static void Add6_4_0( Changelog log ) + => log.NextVersion( "Version 0.6.4.0" ) + .RegisterEntry( "Fixed an issue with the identification of actors in the duty group portrait." ) + .RegisterEntry( "Fixed some issues with wrongly cached actors and resources." ) + .RegisterEntry( "Fixed animation handling after redraws (notably for PLD idle animations with a shield equipped)." ) + .RegisterEntry( "Fixed an issue with collection listing API skipping one collection." ) + .RegisterEntry( "Fixed an issue with BGM files being sometimes loaded from other collections than the base collection, causing crashes." ) + .RegisterEntry( "Also distinguished file resolving for different file categories (improving performance) and disabled resolving for script files entirely.", 1 ) + .RegisterEntry( "Some miscellaneous backend changes due to the Glamourer rework." ); + private static void Add6_3_0( Changelog log ) => log.NextVersion( "Version 0.6.3.0" ) .RegisterEntry( "Add an Assign Current Target button for individual assignments" ) From 20303a9416220a6e5fd000462f3254af7e2baf19 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 30 Jan 2023 17:08:06 +0000 Subject: [PATCH 0732/2451] [CI] Updating repo.json for refs/tags/0.6.4.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5047a31e..eef91843 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.3.1", - "TestingAssemblyVersion": "0.6.3.1", + "AssemblyVersion": "0.6.4.0", + "TestingAssemblyVersion": "0.6.4.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 960c936f8de50e515d9ce6b3afa9df364dd343f1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 Jan 2023 22:17:00 +0100 Subject: [PATCH 0733/2451] Fix ItemSwap with changed target IMC material variant. --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 5 +++-- Penumbra/UI/Classes/ItemSwapWindow.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 4f49ef62..a1de7b7a 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -60,7 +60,8 @@ public static class EquipmentSwap foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) { (var imcFileFrom, var variants, affectedItems) = GetVariants( slot, idFrom, idTo, variantFrom ); - var imcFileTo = new ImcFile( new ImcManipulation( slot, variantTo, idTo.Value, default ) ); + var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); + var imcFileTo = new ImcFile( imcManip); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -72,7 +73,7 @@ public static class EquipmentSwap var skipFemale = false; var skipMale = false; - var mtrlVariantTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ).MaterialId; + var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ) ) ).Imc.Entry.MaterialId; foreach( var gr in Enum.GetValues< GenderRace >() ) { switch( gr.Split().Item1 ) diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 69a4494c..cbef6c19 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -421,7 +421,7 @@ public class ItemSwapWindow : IDisposable if( _affectedItems is { Length: > 1 } ) { ImGui.SameLine(); - ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); + ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); if( ImGui.IsItemHovered() ) { ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) ) From 58c74e839c2193ad2119d4e5b7337f16826b56f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 Jan 2023 22:22:39 +0100 Subject: [PATCH 0734/2451] Maybe improve error handling when unable to create mipmaps. --- Penumbra/Import/Textures/CombinedTexture.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c3065186..3a8762d4 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -130,9 +130,21 @@ public partial class CombinedTexture : IDisposable } private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) - => mipMaps - ? input.GenerateMipMaps( Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ), FilterFlags.SeparateAlpha ) - : input; + { + if( !mipMaps ) + { + return input; + } + + var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ); + var ec = input.GenerateMipMaps( out var ret, numMips, FilterFlags.SeparateAlpha ); + if (ec != ErrorCode.Ok) + { + throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" ); + } + + return ret; + } private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) { From fe561f39c249042c60bf81f01530a84b3ed1387f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Jan 2023 16:12:57 +0100 Subject: [PATCH 0735/2451] Add ResolvePlayerPaths. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 39 ++++++++++++-- Penumbra/Api/PenumbraApi.cs | 53 ++++++++++++++----- Penumbra/Api/PenumbraIpcProviders.cs | 35 ++++++------ .../Collections/ModCollection.Cache.Access.cs | 4 ++ Penumbra/Collections/ModCollection.Cache.cs | 30 ++++++++++- 6 files changed, 126 insertions(+), 37 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index b65f0a4e..97dc16ba 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b65f0a4e2a761a3142a07587a6c0f8657f1361ee +Subproject commit 97dc16ba32bf78c4d3a4d210a08010cd6d4eec3c diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 2897c380..90a17687 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -678,6 +678,32 @@ public class IpcTester : IDisposable } } } + + DrawIntro( Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)" ); + if( _currentResolvePath.Length > 0 || _currentReversePath.Length > 0 ) + { + var forwardArray = _currentResolvePath.Length > 0 ? new[] { _currentResolvePath } : Array.Empty< string >(); + var reverseArray = _currentReversePath.Length > 0 ? new[] { _currentReversePath } : Array.Empty< string >(); + var ret = Ipc.ResolvePlayerPaths.Subscriber( _pi ).Invoke( forwardArray, reverseArray ); + var text = string.Empty; + if( ret.Item1.Length > 0 ) + { + if( ret.Item2.Length > 0 ) + { + text = $"Forward: {ret.Item1[ 0 ]} | Reverse: {string.Join( "; ", ret.Item2[ 0 ] )}."; + } + else + { + text = $"Forward: {ret.Item1[ 0 ]}."; + } + } + else if( ret.Item2.Length > 0 ) + { + text = $"Reverse: {string.Join( "; ", ret.Item2[ 0 ] )}."; + } + + ImGui.TextUnformatted( text ); + } } } @@ -685,10 +711,10 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private int _objectIdx = 0; - private string _collectionName = string.Empty; - private bool _allowCreation = true; - private bool _allowDeletion = true; + private int _objectIdx = 0; + private string _collectionName = string.Empty; + private bool _allowCreation = true; + private bool _allowDeletion = true; private ApiCollectionType _type = ApiCollectionType.Current; private string _characterCollectionName = string.Empty; @@ -709,7 +735,7 @@ public class IpcTester : IDisposable return; } - ImGuiUtil.GenericEnumCombo( "Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName() ); + ImGuiUtil.GenericEnumCombo( "Collection Type", 200, _type, out _type, t => ( ( CollectionType )t ).ToName() ); ImGui.InputInt( "Object Index##Collections", ref _objectIdx, 0, 0 ); ImGui.InputText( "Collection Name##Collections", ref _collectionName, 64 ); ImGui.Checkbox( "Allow Assignment Creation", ref _allowCreation ); @@ -766,8 +792,11 @@ public class IpcTester : IDisposable { ( _returnCode, _oldCollection ) = Ipc.SetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx, _collectionName, _allowCreation, _allowDeletion ); } + if( _returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty() ) + { _oldCollection = null; + } DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7997f8e8..8e609cb7 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -13,6 +13,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.String; @@ -23,7 +24,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 18 ); + => ( 4, 19 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -271,6 +272,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ret.Select( r => r.ToString() ).ToArray(); } + public (string[], string[][]) ResolvePlayerPaths( string[] forward, string[] reverse ) + { + CheckInitialized(); + if( !Penumbra.Config.EnableMods ) + { + return ( forward, reverse.Select( p => new[] { p } ).ToArray() ); + } + + var playerCollection = PathResolver.PlayerCollection(); + var resolved = forward.Select( p => ResolvePath( p, Penumbra.ModManager, playerCollection ) ).ToArray(); + var reverseResolved = playerCollection.ReverseResolvePaths( reverse ); + return ( resolved, reverseResolved.Select( a => a.Select( p => p.ToString() ).ToArray() ).ToArray() ); + } + public T? GetFile< T >( string gamePath ) where T : FileResource => GetFileIntern< T >( ResolveDefaultPath( gamePath ) ); @@ -306,17 +321,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Enum.IsDefined( type ) ) + { return string.Empty; + } var collection = Penumbra.CollectionManager.ByType( ( CollectionType )type ); return collection?.Name ?? string.Empty; } - public (PenumbraApiEc, string OldCollection) SetCollectionForType( Enums.ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) + public (PenumbraApiEc, string OldCollection) SetCollectionForType( ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) { CheckInitialized(); if( !Enum.IsDefined( type ) ) + { return ( PenumbraApiEc.InvalidArgument, string.Empty ); + } var oldCollection = Penumbra.CollectionManager.ByType( ( CollectionType )type )?.Name ?? string.Empty; @@ -327,18 +346,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.NothingChanged, oldCollection ); } - if( !allowDelete || type is Enums.ApiCollectionType.Current or Enums.ApiCollectionType.Default or Enums.ApiCollectionType.Interface ) + if( !allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface ) { return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); } - Penumbra.CollectionManager.RemoveSpecialCollection( (CollectionType) type ); + Penumbra.CollectionManager.RemoveSpecialCollection( ( CollectionType )type ); return ( PenumbraApiEc.Success, oldCollection ); } if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { - return (PenumbraApiEc.CollectionMissing, oldCollection); + return ( PenumbraApiEc.CollectionMissing, oldCollection ); } if( oldCollection.Length == 0 ) @@ -355,7 +374,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( PenumbraApiEc.NothingChanged, oldCollection ); } - Penumbra.CollectionManager.SetCollection( collection, (CollectionType) type ); + Penumbra.CollectionManager.SetCollection( collection, ( CollectionType )type ); return ( PenumbraApiEc.Success, oldCollection ); } @@ -392,28 +411,29 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if( oldCollection.Length == 0 ) { - return (PenumbraApiEc.NothingChanged, oldCollection); + return ( PenumbraApiEc.NothingChanged, oldCollection ); } if( !allowDelete ) { - return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); } + var idx = Penumbra.CollectionManager.Individuals.Index( id ); - Penumbra.CollectionManager.RemoveIndividualCollection(idx ); - return (PenumbraApiEc.Success, oldCollection); + Penumbra.CollectionManager.RemoveIndividualCollection( idx ); + return ( PenumbraApiEc.Success, oldCollection ); } if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { - return (PenumbraApiEc.CollectionMissing, oldCollection); + return ( PenumbraApiEc.CollectionMissing, oldCollection ); } if( oldCollection.Length == 0 ) { if( !allowCreateNew ) { - return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); + return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); } var ids = Penumbra.CollectionManager.Individuals.GetGroup( id ); @@ -421,11 +441,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi } else if( oldCollection == collection.Name ) { - return (PenumbraApiEc.NothingChanged, oldCollection); + return ( PenumbraApiEc.NothingChanged, oldCollection ); } Penumbra.CollectionManager.SetCollection( collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index( id ) ); - return (PenumbraApiEc.Success, oldCollection); + return ( PenumbraApiEc.Success, oldCollection ); } public IList< string > GetCollections() @@ -983,6 +1003,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi => ChangedItemClicked?.Invoke( button, it ); + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private void CheckInitialized() { if( !Valid ) @@ -993,6 +1014,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) { collection = Penumbra.CollectionManager.Default; @@ -1011,17 +1033,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static unsafe ActorIdentifier AssociatedIdentifier( int gameObjectIdx ) { if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) { return ActorIdentifier.Invalid; } + var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); return Penumbra.Actors.FromObject( ptr, out _, false, true ); } // Resolve a path given by string for a specific collection. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 5e23624f..0425d721 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -49,26 +49,27 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider< nint, string, string > GameObjectResourcePathResolved; // Resolve - internal readonly FuncProvider< string, string > ResolveDefaultPath; - internal readonly FuncProvider< string, string > ResolveInterfacePath; - internal readonly FuncProvider< string, string > ResolvePlayerPath; - internal readonly FuncProvider< string, int, string > ResolveGameObjectPath; - internal readonly FuncProvider< string, string, string > ResolveCharacterPath; - internal readonly FuncProvider< string, string, string[] > ReverseResolvePath; - internal readonly FuncProvider< string, int, string[] > ReverseResolveGameObjectPath; - internal readonly FuncProvider< string, string[] > ReverseResolvePlayerPath; + internal readonly FuncProvider< string, string > ResolveDefaultPath; + internal readonly FuncProvider< string, string > ResolveInterfacePath; + internal readonly FuncProvider< string, string > ResolvePlayerPath; + internal readonly FuncProvider< string, int, string > ResolveGameObjectPath; + internal readonly FuncProvider< string, string, string > ResolveCharacterPath; + internal readonly FuncProvider< string, string, string[] > ReverseResolvePath; + internal readonly FuncProvider< string, int, string[] > ReverseResolveGameObjectPath; + internal readonly FuncProvider< string, string[] > ReverseResolvePlayerPath; + internal readonly FuncProvider< string[], string[], (string[], string[][]) > ResolvePlayerPaths; // Collections - internal readonly FuncProvider< IList< string > > GetCollections; - internal readonly FuncProvider< string > GetCurrentCollectionName; - internal readonly FuncProvider< string > GetDefaultCollectionName; - internal readonly FuncProvider< string > GetInterfaceCollectionName; - internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; + internal readonly FuncProvider< IList< string > > GetCollections; + internal readonly FuncProvider< string > GetCurrentCollectionName; + internal readonly FuncProvider< string > GetDefaultCollectionName; + internal readonly FuncProvider< string > GetInterfaceCollectionName; + internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; internal readonly FuncProvider< ApiCollectionType, string > GetCollectionForType; internal readonly FuncProvider< ApiCollectionType, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForType; - internal readonly FuncProvider< int, (bool, bool, string) > GetCollectionForObject; - internal readonly FuncProvider< int, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForObject; - internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; + internal readonly FuncProvider< int, (bool, bool, string) > GetCollectionForObject; + internal readonly FuncProvider< int, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForObject; + internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; // Meta internal readonly FuncProvider< string > GetPlayerMetaManipulations; @@ -160,6 +161,7 @@ public class PenumbraIpcProviders : IDisposable ReverseResolvePath = Ipc.ReverseResolvePath.Provider( pi, Api.ReverseResolvePath ); ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider( pi, Api.ReverseResolveGameObjectPath ); ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider( pi, Api.ReverseResolvePlayerPath ); + ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider( pi, Api.ResolvePlayerPaths ); // Collections GetCollections = Ipc.GetCollections.Provider( pi, Api.GetCollections ); @@ -263,6 +265,7 @@ public class PenumbraIpcProviders : IDisposable ReverseResolvePath.Dispose(); ReverseResolveGameObjectPath.Dispose(); ReverseResolvePlayerPath.Dispose(); + ResolvePlayerPaths.Dispose(); // Collections GetCollections.Dispose(); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index a4d1619c..d425a031 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -4,6 +4,7 @@ using Penumbra.Meta.Manager; using Penumbra.Mods; using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; @@ -68,6 +69,9 @@ public partial class ModCollection public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath path ) => _cache?.ReverseResolvePath( path ) ?? Array.Empty< Utf8GamePath >(); + public HashSet< Utf8GamePath >[] ReverseResolvePaths( string[] paths ) + => _cache?.ReverseResolvePaths( paths ) ?? paths.Select( _ => new HashSet< Utf8GamePath >() ).ToArray(); + public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 46d049e8..1b60561d 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -5,6 +5,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Penumbra.Api.Enums; using Penumbra.String.Classes; @@ -104,6 +105,33 @@ public partial class ModCollection return iterator; } + // Reverse resolve multiple paths at once for efficiency. + public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) + { + if( fullPaths.Count == 0 ) + return Array.Empty< HashSet< Utf8GamePath > >(); + + var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; + var dict = new Dictionary< FullPath, int >( fullPaths.Count ); + foreach( var (path, idx) in fullPaths.WithIndex() ) + { + dict[ new FullPath(path) ] = idx; + ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) + ? new HashSet< Utf8GamePath > { utf8 } + : new HashSet< Utf8GamePath >(); + } + + foreach( var (game, full) in ResolvedFiles ) + { + if( dict.TryGetValue( full.Path, out var idx ) ) + { + ret[ idx ].Add( game ); + } + } + + return ret; + } + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { switch( type ) @@ -474,7 +502,7 @@ public partial class ModCollection // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = Penumbra.Identifier; - var items = new SortedList< string, object? >(512); + var items = new SortedList< string, object? >( 512 ); void AddItems( IMod mod ) { From 5997ddca02b321df487031f1c7e3f3e47bbd97c1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Feb 2023 11:35:59 +0100 Subject: [PATCH 0736/2451] Add even more handling for stupid banners and some debug info for them. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 21 +++-- Penumbra.GameData/Actors/AgentBannerParty.cs | 91 +++++++++++++++++++ Penumbra/UI/ConfigWindow.DebugTab.cs | 71 +++++++++++++-- 3 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 Penumbra.GameData/Actors/AgentBannerParty.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 20fbe679..fb161304 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -205,18 +205,23 @@ public sealed partial class ActorManager : IDisposable public unsafe bool ResolvePartyBannerPlayer(ScreenActor type, out ActorIdentifier id) { id = ActorIdentifier.Invalid; - var addon = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.BannerParty); - if (addon == null || !addon->IsAgentActive()) + var module = Framework.Instance()->GetUiModule()->GetAgentModule(); + if (module == null) return false; - var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen; - if (idx is < 0 or > 7) + var agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerParty); + if (agent == null || !agent->AgentInterface.IsAgentActive()) + agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerMIP); + if (agent == null || !agent->AgentInterface.IsAgentActive()) + return false; + + var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen; + var character = agent->Character(idx); + if (character == null) return true; - var obj = GroupManager.Instance()->GetPartyMemberByIndex(idx); - if (obj != null) - id = CreatePlayer(new ByteString(obj->Name), obj->HomeWorld); - + var name = new ByteString(character->Name1.StringPtr); + id = CreatePlayer(name, (ushort)character->WorldId); return true; } diff --git a/Penumbra.GameData/Actors/AgentBannerParty.cs b/Penumbra.GameData/Actors/AgentBannerParty.cs new file mode 100644 index 00000000..9756a289 --- /dev/null +++ b/Penumbra.GameData/Actors/AgentBannerParty.cs @@ -0,0 +1,91 @@ +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.Framework; + +namespace Penumbra; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct AgentBannerInterface +{ + [FieldOffset( 0x0 )] public AgentInterface AgentInterface; + [FieldOffset( 0x28 )] public BannerInterfaceStorage* Data; + + public BannerInterfaceStorage.CharacterData* Character( int idx ) + => idx switch + { + _ when Data == null => null, + 0 => &Data->Character1, + 1 => &Data->Character2, + 2 => &Data->Character3, + 3 => &Data->Character4, + 4 => &Data->Character5, + 5 => &Data->Character6, + 6 => &Data->Character7, + 7 => &Data->Character8, + _ => null, + }; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct AgentBannerParty +{ + public static AgentBannerParty* Instance() => ( AgentBannerParty* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerParty ); + + [FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface; +} + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct AgentBannerMIP +{ + public static AgentBannerMIP* Instance() => ( AgentBannerMIP* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerMIP ); + [FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface; +} + +// Client::UI::Agent::AgentBannerInterface::Storage +// destructed in Client::UI::Agent::AgentBannerInterface::dtor +[StructLayout( LayoutKind.Explicit, Size = 0x3BB0 )] +public unsafe struct BannerInterfaceStorage +{ + // vtable: 48 8D 05 ?? ?? ?? ?? 48 89 01 48 8B F9 7E + // dtor: E8 ?? ?? ?? ?? 48 83 EF ?? 75 ?? BA ?? ?? ?? ?? 48 8B CE E8 ?? ?? ?? ?? 48 89 7D + [StructLayout( LayoutKind.Explicit, Size = 0x770 )] + public struct CharacterData + { + [FieldOffset( 0x000 )] public void** VTable; + + [FieldOffset( 0x018 )] public Utf8String Name1; + [FieldOffset( 0x080 )] public Utf8String Name2; + [FieldOffset( 0x0E8 )] public Utf8String UnkString1; + [FieldOffset( 0x150 )] public Utf8String UnkString2; + [FieldOffset( 0x1C0 )] public Utf8String Job; + [FieldOffset( 0x238 )] public uint WorldId; + [FieldOffset( 0x240 )] public Utf8String UnkString3; + + [FieldOffset( 0x2B0 )] public void* CharaView; + [FieldOffset( 0x5D0 )] public AtkTexture AtkTexture; + + [FieldOffset( 0x6F8 )] public Utf8String Title; + [FieldOffset( 0x768 )] public void* SomePointer; + + } + + [FieldOffset( 0x0000 )] public void* Agent; // AgentBannerParty, maybe other Banner agents + [FieldOffset( 0x0008 )] public UIModule* UiModule; + [FieldOffset( 0x0010 )] public uint Unk1; // Maybe count or bitfield, but probably not + [FieldOffset( 0x0014 )] public uint Unk2; + + [FieldOffset( 0x0020 )] public CharacterData Character1; + [FieldOffset( 0x0790 )] public CharacterData Character2; + [FieldOffset( 0x0F00 )] public CharacterData Character3; + [FieldOffset( 0x1670 )] public CharacterData Character4; + [FieldOffset( 0x1DE0 )] public CharacterData Character5; + [FieldOffset( 0x2550 )] public CharacterData Character6; + [FieldOffset( 0x2CC0 )] public CharacterData Character7; + [FieldOffset( 0x3430 )] public CharacterData Character8; + + [FieldOffset( 0x3BA0 )] public long Unk3; + [FieldOffset( 0x3BA8 )] public long Unk4; +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index f25ed1d4..52a1f636 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -4,9 +4,11 @@ using System.IO; using System.Linq; using System.Numerics; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -17,6 +19,7 @@ using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.Util; +using static OtterGui.Raii.ImRaii; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -223,7 +226,8 @@ public partial class ConfigWindow return; } - ImGui.TextUnformatted( $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); + ImGui.TextUnformatted( + $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) ) { if( drawTree ) @@ -317,18 +321,65 @@ public partial class ConfigWindow } } - using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ); - if( cutsceneTree ) + using( var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ) ) { - using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) + if( cutsceneTree ) { - foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) + using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"Cutscene Actor {idx}" ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( actor.Name.ToString() ); + foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) + { + ImGuiUtil.DrawTableColumn( $"Cutscene Actor {idx}" ); + ImGuiUtil.DrawTableColumn( actor.Name.ToString() ); + } + } + } + } + + using( var groupTree = ImRaii.TreeNode( "Group" ) ) + { + if( groupTree ) + { + using var table = ImRaii.Table( "###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + ImGuiUtil.DrawTableColumn( "Group Members" ); + ImGuiUtil.DrawTableColumn( GroupManager.Instance()->MemberCount.ToString() ); + for( var i = 0; i < 8; ++i ) + { + ImGuiUtil.DrawTableColumn( $"Member #{i}" ); + var member = GroupManager.Instance()->GetPartyMemberByIndex( i ); + ImGuiUtil.DrawTableColumn( member == null ? "NULL" : new ByteString( member->Name ).ToString() ); + } + } + } + } + + using( var bannerTree = ImRaii.TreeNode( "Party Banner" ) ) + { + if( bannerTree ) + { + var agent = &AgentBannerParty.Instance()->AgentBannerInterface; + if( agent->Data == null ) + agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + if( agent->Data != null ) + { + using var table = ImRaii.Table( "###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + for( var i = 0; i < 8; ++i ) + { + var c = agent->Character( i ); + ImGuiUtil.DrawTableColumn( $"Character {i}" ); + var name = c->Name1.ToString(); + ImGuiUtil.DrawTableColumn( name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})" ); + } + } + } + else + { + ImGui.TextUnformatted( "INACTIVE" ); } } } From c2b3e4dbaf01cf0160a908bae810106c5d50bc51 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Feb 2023 12:00:03 +0100 Subject: [PATCH 0737/2451] Apply OtterGui changes. --- OtterGui | 2 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 10 +++++----- Penumbra/UI/Classes/ModEditWindow.Materials.cs | 4 ++-- Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs | 4 ++-- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 2 +- Penumbra/UI/ConfigWindow.Misc.cs | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/OtterGui b/OtterGui index 2c73fd57..4e730a0d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2c73fd57bf53fc3c9f390db0acc9e176607c2dbc +Subproject commit 4e730a0d5a86a9819bcea0b766134c02f35ac27e diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index cbef6c19..8215bcc7 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -399,7 +399,7 @@ public class ItemSwapWindow : IDisposable ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( text1 ); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); if( type == SwapType.Ring ) { @@ -411,7 +411,7 @@ public class ItemSwapWindow : IDisposable ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( text2 ); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); if( type == SwapType.Ring ) { ImGui.SameLine(); @@ -503,7 +503,7 @@ public class ItemSwapWindow : IDisposable ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( "Select the weapon or tool you want" ); ImGui.TableNextColumn(); - if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) { _dirty = true; _weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); @@ -520,13 +520,13 @@ public class ItemSwapWindow : IDisposable ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( "and put this variant of it" ); ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( "onto this one" ); ImGui.TableNextColumn(); - _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); } private const float InputWidth = 120; diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 54330ecb..400a7f47 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -268,7 +268,7 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainManager.StainCombo.Draw( label, dyeColor, true ); + Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); return false; } @@ -508,7 +508,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( hasDye ) { - if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), intSize + if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 992a0d80..2e3a7194 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -33,7 +33,7 @@ public partial class ConfigWindow => obj.Value; public bool Draw( float width ) - => Draw( "##worldCombo", CurrentSelection.Value, width, ImGui.GetTextLineHeightWithSpacing() ); + => Draw( "##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); } private sealed class NpcCombo : FilterComboCache< (string Name, uint[] Ids) > @@ -60,7 +60,7 @@ public partial class ConfigWindow } public bool Draw( float width ) - => Draw( _label, CurrentSelection.Name, width, ImGui.GetTextLineHeightWithSpacing() ); + => Draw( _label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 979ada02..f356f983 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -167,7 +167,7 @@ public partial class ConfigWindow public void Draw() { var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; - Draw( _label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); + Draw( _label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); } protected override string ToString( (CollectionType, string, string) obj ) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index ed180fd2..b683b75e 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -122,7 +122,7 @@ public partial class ConfigWindow public void Draw( string label, float width, int individualIdx ) { var (_, collection) = Penumbra.CollectionManager.Individuals[ individualIdx ]; - if( Draw( label, collection.Name, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) + if( Draw( label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) { Penumbra.CollectionManager.SetCollection( CurrentSelection, CollectionType.Individual, individualIdx ); } @@ -131,7 +131,7 @@ public partial class ConfigWindow public void Draw( string label, float width, CollectionType type ) { var current = Penumbra.CollectionManager.ByType( type, ActorIdentifier.Invalid ); - if( Draw( label, current?.Name ?? string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) + if( Draw( label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) { Penumbra.CollectionManager.SetCollection( CurrentSelection, type ); } From 41ddc451deb3f3f1c40bee934ac514d55938690e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 2 Feb 2023 12:04:11 +0100 Subject: [PATCH 0738/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 6774d577..e595fb91 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -33,10 +33,18 @@ public partial class ConfigWindow Add6_2_0( ret ); Add6_3_0( ret ); Add6_4_0( ret ); + Add6_5_0( ret ); return ret; } + private static void Add6_5_0( Changelog log ) + => log.NextVersion( "Version 0.6.5.0" ) + .RegisterEntry( "Fixed an issue with Item Swaps not using applied IMC changes in some cases." ) + .RegisterEntry( "Improved error message on texture import when failing to create mip maps (slightly)." ) + .RegisterEntry( "Tried to fix duty party banner identification again, also for the recommendation window this time." ) + .RegisterEntry( "Added batched IPC to improve Mare performance." ); + private static void Add6_4_0( Changelog log ) => log.NextVersion( "Version 0.6.4.0" ) .RegisterEntry( "Fixed an issue with the identification of actors in the duty group portrait." ) From 60f54fa047f2145da69610b1d329e41734164e24 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 2 Feb 2023 11:08:35 +0000 Subject: [PATCH 0739/2451] [CI] Updating repo.json for refs/tags/0.6.5.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index eef91843..574c4fd2 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.4.0", - "TestingAssemblyVersion": "0.6.4.0", + "AssemblyVersion": "0.6.5.0", + "TestingAssemblyVersion": "0.6.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.4.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e34aca68aac12dda736fbb677dc05138ba1a8ec5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Feb 2023 21:23:38 +0100 Subject: [PATCH 0740/2451] Fix crash on mannequins with resource logging on, fix problem with temp collections not keeping count. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 14 ++++++++------ .../Actors/ActorManager.Identifiers.cs | 11 ++++++----- Penumbra.String | 2 +- Penumbra/Api/TempModManager.cs | 7 ++++++- Penumbra/Collections/ModCollection.cs | 3 ++- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 2 +- Penumbra/Interop/ObjectReloader.cs | 3 ++- Penumbra/Penumbra.cs | 4 ++-- Penumbra/UI/ConfigWindow.DebugTab.cs | 14 ++++++++++++++ 9 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index fb161304..0bcaa792 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -165,14 +165,15 @@ public sealed partial class ActorManager : IDisposable public readonly ActorManagerData Data; - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, DataManager gameData, GameGui gameGui, Func toParentIdx) - : this(pluginInterface, objects, state, gameData, gameGui, gameData.Language, toParentIdx) + : this(pluginInterface, objects, state, framework, gameData, gameGui, gameData.Language, toParentIdx) { } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, DataManager gameData, GameGui gameGui, ClientLanguage language, Func toParentIdx) { + _framework = framework; _objects = objects; _gameGui = gameGui; _clientState = state; @@ -340,9 +341,10 @@ public sealed partial class ActorManager : IDisposable ~ActorManager() => Dispose(); - private readonly ObjectTable _objects; - private readonly ClientState _clientState; - private readonly GameGui _gameGui; + private readonly Dalamud.Game.Framework _framework; + private readonly ObjectTable _objects; + private readonly ClientState _clientState; + private readonly GameGui _gameGui; private readonly Func _toParentIdx; diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index d5e0c30e..500bbc98 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.String; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -313,8 +314,8 @@ public partial class ActorManager static ByteString Get(byte* ptr) => ptr == null ? ByteString.Empty : new ByteString(ptr); - var actualName = Get(actor->GetName()); var retainerName = Get(actor->Name); + var actualName = _framework.IsInFrameworkUpdateThread ? Get(actor->GetName()) : ByteString.Empty; if (!actualName.Equals(retainerName)) { var ident = check @@ -338,7 +339,7 @@ public partial class ActorManager if (owner == null) return ActorIdentifier.Invalid; - var dataId = GetCompanionId(actor, (Character*) owner); + var dataId = GetCompanionId(actor, (Character*)owner); var name = new ByteString(owner->Name); var homeWorld = ((Character*)owner)->HomeWorld; return check @@ -553,10 +554,10 @@ public partial class ActorManager { return index switch { - ushort.MaxValue => true, - < 200 => index % 2 == 0, + ushort.MaxValue => true, + < 200 => index % 2 == 0, > (ushort)ScreenActor.Card8 => index < 426, - _ => false, + _ => false, }; } diff --git a/Penumbra.String b/Penumbra.String index 4f7a112e..5ae32fd5 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 4f7a112ea9c9b377c265746dc572fa26e371de69 +Subproject commit 5ae32fd5f19fcc641d5f2d777de2276186900c0b diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 511cb861..08393ebd 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,3 +1,4 @@ +using System; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -20,6 +21,7 @@ public enum RedirectResult public class TempModManager { + public int GlobalChangeCounter { get; private set; } = 0; private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); private readonly Dictionary< string, ModCollection > _customCollections = new(); @@ -158,7 +160,9 @@ public class TempModManager return string.Empty; } - var collection = ModCollection.CreateNewTemporary( name ); + if( GlobalChangeCounter == int.MaxValue ) + GlobalChangeCounter = 0; + var collection = ModCollection.CreateNewTemporary( name, GlobalChangeCounter++ ); if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) ) { return collection.Name; @@ -175,6 +179,7 @@ public class TempModManager return false; } + GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); _mods.Remove( collection ); collection.ClearCache(); for( var i = 0; i < Collections.Count; ++i ) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 2b7a8605..f5ce009d 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -85,12 +85,13 @@ public partial class ModCollection => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary( string name ) + public static ModCollection CreateNewTemporary( string name, int changeCounter ) { var collection = new ModCollection( name, Empty ); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; collection.Index = ~Penumbra.TempMods.Collections.Count; + collection.ChangeCounter = changeCounter; collection.CreateCache(); return collection; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index ad1d69d7..438e6fa5 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -257,7 +257,7 @@ public unsafe partial class ResourceLoader { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); Penumbra.Log.Information( - $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()}. (Refcount {handle->RefCount}) " ); + $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); } private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom ) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 3f7adaf1..5f466b95 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -8,6 +8,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; namespace Penumbra.Interop; @@ -99,7 +100,7 @@ public unsafe partial class ObjectReloader } tableIndex = ObjectTableIndex( actor ); - return tableIndex is >= 240 and < 245; + return tableIndex is >= (int) ScreenActor.CharacterScreen and <= ( int) ScreenActor.Card8; } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 739975e5..92ac8385 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -99,14 +99,14 @@ public class Penumbra : IDalamudPlugin GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ); - Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); + Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.Framework, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); Framework = new FrameworkManager(Dalamud.Framework, Log); CharacterUtility = new CharacterUtility(); Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); Config = Configuration.Load(); - + TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 52a1f636..10eded81 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -20,6 +20,7 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.Util; using static OtterGui.Raii.ImRaii; +using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -550,6 +551,19 @@ public partial class ConfigWindow return; } + using( var t1 = ImRaii.Table( "##table", 2, ImGuiTableFlags.SizingFixedFit ) ) + { + if( t1 ) + { + ImGuiUtil.DrawTableColumn( "Flags" ); + ImGuiUtil.DrawTableColumn( $"{model->UnkFlags_01:X2}" ); + ImGuiUtil.DrawTableColumn( "Has Model In Slot Loaded" ); + ImGuiUtil.DrawTableColumn( $"{model->HasModelInSlotLoaded:X8}" ); + ImGuiUtil.DrawTableColumn( "Has Model Files In Slot Loaded" ); + ImGuiUtil.DrawTableColumn( $"{model->HasModelFilesInSlotLoaded:X8}" ); + } + } + using var table = ImRaii.Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); if( !table ) { From 9f6a45041def635c9abdacfb272490d50a18574f Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sat, 4 Feb 2023 17:26:13 +1030 Subject: [PATCH 0741/2451] Add descriptions for SubMods Adjusted importing of TexTools mods to give each option its own description instead of combining all option descriptions into the group description. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 9 +---- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 17 ++++++++++ Penumbra/Mods/Mod.Creation.cs | 1 + Penumbra/Mods/Subclasses/ISubMod.cs | 3 ++ Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 7 ++-- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 33 ++++++++++++++----- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 13 ++++++++ 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index ca8eb492..71760187 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -173,7 +173,6 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var description = new StringBuilder(); var groupFolder = Mod.NewSubFolderName( _currentModDirectory, name ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); @@ -188,12 +187,6 @@ public partial class TexToolsImporter ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); ExtractSimpleModList( optionFolder, option.ModsJsons ); options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) ); - description.Append( option.Description ); - if( !string.IsNullOrEmpty( option.Description ) ) - { - description.Append( '\n' ); - } - if( option.IsChecked ) { defaultSettings = group.SelectionType == GroupType.Multi @@ -220,7 +213,7 @@ public partial class TexToolsImporter } Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - defaultSettings ?? 0, description.ToString(), options ); + defaultSettings ?? 0, string.Empty, options ); ++groupPriority; } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 82e171eb..1887c490 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -124,6 +124,23 @@ public sealed partial class Mod ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1 ); } + public void ChangeOptionDescription( Mod mod, int groupIdx, int optionIdx, string newDescription ) + { + var group = mod._groups[ groupIdx ]; + var option = group[ optionIdx ]; + if( option.Description == newDescription ) + { + return; + } + + var _ = option switch + { + SubMod s => s.Description = newDescription, + }; + + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); + } + public void ChangeGroupPriority( Mod mod, int groupIdx, int newPriority ) { var group = mod._groups[ groupIdx ]; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 40cc9d2f..69adbeef 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -116,6 +116,7 @@ public partial class Mod var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. { Name = option.Name, + Description = option.Description, }; foreach( var (_, gamePath, file) in list ) { diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 89bf2053..2693fcad 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -10,6 +10,7 @@ public interface ISubMod { public string Name { get; } public string FullName { get; } + public string Description { get; } public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } @@ -22,6 +23,8 @@ public interface ISubMod j.WriteStartObject(); j.WritePropertyName( nameof( Name ) ); j.WriteValue( mod.Name ); + j.WritePropertyName( nameof(Description) ); + j.WriteValue( mod.Description ); if( priority != null ) { j.WritePropertyName( nameof( IModGroup.Priority ) ); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 80c35d0f..ed0fd1fb 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -107,6 +107,8 @@ public partial class Mod public string FullName => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + public string Description { get; set; } = string.Empty; + internal IMod ParentMod { get; private init; } internal int GroupIdx { get; private set; } internal int OptionIdx { get; private set; } @@ -143,8 +145,9 @@ public partial class Mod ManipulationData.Clear(); // Every option has a name, but priorities are only relevant for multi group options. - Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; - priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; + Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; + Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; + priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; var files = ( JObject? )json[ nameof( Files ) ]; if( files != null ) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index d05d1ab2..951b0200 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -301,16 +301,18 @@ public partial class ConfigWindow // Open a popup to edit a multi-line mod or option description. private static class DescriptionEdit { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDesriptionOptionIdx = -1; private static Mod? _mod; - public static void OpenPopup( Mod mod, int groupIdx ) + public static void OpenPopup( Mod mod, int groupIdx, int optionIdx = -1 ) { - _newDescriptionIdx = groupIdx; - _newDescription = groupIdx < 0 ? mod.Description : mod.Groups[ groupIdx ].Description; - _mod = mod; + _newDescriptionIdx = groupIdx; + _newDesriptionOptionIdx = optionIdx; + _newDescription = groupIdx < 0 ? mod.Description : optionIdx < 0 ? mod.Groups[ groupIdx ].Description : mod.Groups[ groupIdx ][ optionIdx ].Description; + _mod = mod; ImGui.OpenPopup( PopupName ); } @@ -355,7 +357,14 @@ public partial class ConfigWindow Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); break; case >= 0: - Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); + if( _newDesriptionOptionIdx < 0 ) + { + Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); + } + else + { + Penumbra.ModManager.ChangeOptionDescription( _mod, _newDescriptionIdx, _newDesriptionOptionIdx, _newDescription ); + } break; } @@ -468,7 +477,7 @@ public partial class ConfigWindow public static void Draw( ModPanel panel, int groupIdx ) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table( string.Empty, 6, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; @@ -478,6 +487,7 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, panel._window._inputTextWidth.X - 68 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); + ImGui.TableSetupColumn( "description", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); @@ -532,6 +542,11 @@ public partial class ConfigWindow Penumbra.ModManager.RenameOption( panel._mod, groupIdx, optionIdx, newOptionName ); } + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, "Edit option description.", false, true ) ) + { + panel._delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( panel._mod, groupIdx, optionIdx ) ); + } ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 7b19243d..32735bc0 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -179,6 +180,12 @@ public partial class ConfigWindow Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); } + if( !string.IsNullOrEmpty( group[ idx2 ].Description ) ) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(group[idx2].Description); + } + id.Pop(); } } @@ -213,6 +220,12 @@ public partial class ConfigWindow Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); } + if( !string.IsNullOrEmpty( group[ idx2 ].Description ) ) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(group[idx2].Description); + } + id.Pop(); } From 98bc14882b81293d553989a90582bbb9ad65ab31 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 14:57:34 +0100 Subject: [PATCH 0742/2451] Add some startup information in debug mode. --- OtterGui | 2 +- Penumbra/Penumbra.cs | 64 ++++++++++++++++++++-------- Penumbra/UI/ConfigWindow.DebugTab.cs | 61 ++++++++++++++------------ Penumbra/Util/PerformanceType.cs | 38 ++++++++++++++++- 4 files changed, 118 insertions(+), 47 deletions(-) diff --git a/OtterGui b/OtterGui index 4e730a0d..aee8a3dc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4e730a0d5a86a9819bcea0b766134c02f35ac27e +Subproject commit aee8a3dc8e7eb1145c328a7c50f7e5bbcdd234f8 diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 92ac8385..85f97f9e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -26,6 +26,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; +using Action = System.Action; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -66,8 +67,12 @@ public class Penumbra : IDalamudPlugin public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainManager StainManager { get; private set; } = null!; public static ItemData ItemData { get; private set; } = null!; + + public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; + public static StartTimeTracker< StartTimeType > StartTimer = new(); + public static readonly List< Exception > ImcExceptions = new(); public readonly ResourceLogger ResourceLogger; @@ -84,39 +89,59 @@ public class Penumbra : IDalamudPlugin internal WebServer? WebServer; + private static void Timed( StartTimeType type, Action action ) + { + using var t = StartTimer.Measure( type ); + action(); + } + public Penumbra( DalamudPluginInterface pluginInterface ) { + using var time = StartTimer.Measure( StartTimeType.Total ); + try { Dalamud.Initialize( pluginInterface ); + Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); Log = new Logger(); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); IsValidSourceRepo = CheckSourceRepo(); - GameEvents = new GameEventManager(); - Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); - GamePathParser = GameData.GameData.GetGamePathParser(); - StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); - ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ); - Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.Framework, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); - Framework = new FrameworkManager(Dalamud.Framework, Log); + GameEvents = new GameEventManager(); + StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ) ); + StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() ); + StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ) ); + StartTimer.Measure( StartTimeType.Items, () => ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ) ); + StartTimer.Measure( StartTimeType.Actors, + () => Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.Framework, Dalamud.GameData, Dalamud.GameGui, + ResolveCutscene ) ); + + Framework = new FrameworkManager( Dalamud.Framework, Log ); CharacterUtility = new CharacterUtility(); - Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ); + StartTimer.Measure( StartTimeType.Backup, () => Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ) ); Config = Configuration.Load(); - + TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLoader.EnableHooks(); ResourceLogger = new ResourceLogger( ResourceLoader ); ResidentResources = new ResidentResourceManager(); - ModManager = new Mod.Manager( Config.ModDirectory ); - ModManager.DiscoverMods(); - CollectionManager = new ModCollection.Manager( ModManager ); - CollectionManager.CreateNecessaryCaches(); + StartTimer.Measure( StartTimeType.Mods, () => + { + ModManager = new Mod.Manager( Config.ModDirectory ); + ModManager.DiscoverMods(); + } ); + + StartTimer.Measure( StartTimeType.Collections, () => + { + CollectionManager = new ModCollection.Manager( ModManager ); + CollectionManager.CreateNecessaryCaches(); + } ); + ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -146,9 +171,13 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - Api = new PenumbraApi( this ); - IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); - SubscribeItemLinks(); + using( var tAPI = StartTimer.Measure( StartTimeType.Api ) ) + { + Api = new PenumbraApi( this ); + IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); + SubscribeItemLinks(); + } + if( ImcExceptions.Count > 0 ) { Log.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); @@ -177,6 +206,7 @@ public class Penumbra : IDalamudPlugin private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system, out Changelog changelog ) { + using var tInterface = StartTimer.Measure( StartTimeType.Interface ); cfg = new ConfigWindow( this ); btn = new LaunchButton( _configWindow ); system = new WindowSystem( Name ); @@ -341,7 +371,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); - sb.Append( $"> **`Synchronous Load (Dalamud): `** {(Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown")}\n" ); + sb.Append( $"> **`Synchronous Load (Dalamud): `** {( Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); sb.Append( $"> **`Logging: `** Full: {Config.EnableFullResourceLogging}, Resource: {Config.EnableResourceLogging}\n" ); sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.AppendLine( "**Mods**" ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 10eded81..19ada144 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -48,13 +48,13 @@ public partial class ConfigWindow return; } - using var tab = ImRaii.TabItem( "Debug" ); + using var tab = TabItem( "Debug" ); if( !tab ) { return; } - using var child = ImRaii.Child( "##DebugTab", -Vector2.One ); + using var child = Child( "##DebugTab", -Vector2.One ); if( !child ) { return; @@ -93,7 +93,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + using var table = Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); if( !table ) { @@ -125,7 +125,9 @@ public partial class ConfigWindow return; } - Penumbra.Performance.Draw( "##performance", "Enable Performance Tracking", PerformanceTypeExtensions.ToName ); + Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); + ImGui.NewLine(); + Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); } // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. @@ -144,7 +146,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ); if( !table ) { @@ -182,7 +184,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ); if( !table ) { @@ -229,11 +231,11 @@ public partial class ConfigWindow ImGui.TextUnformatted( $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); - using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) ) + using( var drawTree = TreeNode( "Draw Object to Object" ) ) { if( drawTree ) { - using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); if( table ) { foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap ) @@ -256,11 +258,11 @@ public partial class ConfigWindow } } - using( var pathTree = ImRaii.TreeNode( "Path Collections" ) ) + using( var pathTree = TreeNode( "Path Collections" ) ) { if( pathTree ) { - using var table = ImRaii.Table( "###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); if( table ) { foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) @@ -276,11 +278,11 @@ public partial class ConfigWindow } } - using( var resourceTree = ImRaii.TreeNode( "Subfile Collections" ) ) + using( var resourceTree = TreeNode( "Subfile Collections" ) ) { if( resourceTree ) { - using var table = ImRaii.Table( "###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); if( table ) { ImGuiUtil.DrawTableColumn( "Current Mtrl Data" ); @@ -305,11 +307,11 @@ public partial class ConfigWindow } } - using( var identifiedTree = ImRaii.TreeNode( "Identified Collections" ) ) + using( var identifiedTree = TreeNode( "Identified Collections" ) ) { if( identifiedTree ) { - using var table = ImRaii.Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); if( table ) { foreach( var (address, identifier, collection) in PathResolver.IdentifiedCache ) @@ -322,11 +324,11 @@ public partial class ConfigWindow } } - using( var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ) ) + using( var cutsceneTree = TreeNode( "Cutscene Actors" ) ) { if( cutsceneTree ) { - using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); if( table ) { foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) @@ -338,11 +340,11 @@ public partial class ConfigWindow } } - using( var groupTree = ImRaii.TreeNode( "Group" ) ) + using( var groupTree = TreeNode( "Group" ) ) { if( groupTree ) { - using var table = ImRaii.Table( "###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit ); if( table ) { ImGuiUtil.DrawTableColumn( "Group Members" ); @@ -357,16 +359,19 @@ public partial class ConfigWindow } } - using( var bannerTree = ImRaii.TreeNode( "Party Banner" ) ) + using( var bannerTree = TreeNode( "Party Banner" ) ) { if( bannerTree ) { var agent = &AgentBannerParty.Instance()->AgentBannerInterface; if( agent->Data == null ) + { agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + } + if( agent->Data != null ) { - using var table = ImRaii.Table( "###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit ); if( table ) { for( var i = 0; i < 8; ++i ) @@ -395,13 +400,13 @@ public partial class ConfigWindow foreach( var (key, data) in Penumbra.StainManager.StmFile.Entries ) { - using var tree = ImRaii.TreeNode( $"Template {key}" ); + using var tree = TreeNode( $"Template {key}" ); if( !tree ) { continue; } - using var table = ImRaii.Table( "##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + using var table = Table( "##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); if( !table ) { continue; @@ -436,7 +441,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ); if( !table ) { @@ -491,7 +496,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; @@ -518,7 +523,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ); if( !table ) { @@ -551,7 +556,7 @@ public partial class ConfigWindow return; } - using( var t1 = ImRaii.Table( "##table", 2, ImGuiTableFlags.SizingFixedFit ) ) + using( var t1 = Table( "##table", 2, ImGuiTableFlags.SizingFixedFit ) ) { if( t1 ) { @@ -564,7 +569,7 @@ public partial class ConfigWindow } } - using var table = ImRaii.Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + using var table = Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; @@ -620,7 +625,7 @@ public partial class ConfigWindow return; } - using var table = ImRaii.Table( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); + using var table = Table( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index 9328d750..80205f7c 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,5 +1,24 @@ +using Lumina.Excel.GeneratedSheets; +using OtterGui.Classes; +using Penumbra.GameData; + namespace Penumbra.Util; +public enum StartTimeType +{ + Total, + Identifier, + GamePathParser, + Stains, + Items, + Actors, + Backup, + Mods, + Collections, + Api, + Interface, +} + public enum PerformanceType { UiMainWindow, @@ -28,8 +47,25 @@ public enum PerformanceType DebugTimes, } -public static class PerformanceTypeExtensions +public static class TimingExtensions { + public static string ToName( this StartTimeType type ) + => type switch + { + StartTimeType.Total => "Total Construction", + StartTimeType.Identifier => "Identification Data", + StartTimeType.GamePathParser => "Game Path Data", + StartTimeType.Stains => "Stain Data", + StartTimeType.Items => "Item Data", + StartTimeType.Actors => "Actor Data", + StartTimeType.Backup => "Checking Backups", + StartTimeType.Mods => "Loading Mods", + StartTimeType.Collections => "Loading Collections", + StartTimeType.Api => "Setting Up API", + StartTimeType.Interface => "Setting Up Interface", + _ => $"Unknown {( int )type}", + }; + public static string ToName( this PerformanceType type ) => type switch { From f29bdee010e828ca714a41917f8d49b4410fc0bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 14:58:07 +0100 Subject: [PATCH 0743/2451] Try to improve launch times somewhat. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 91 ++++++++----- .../Actors/ActorManager.Identifiers.cs | 15 +-- Penumbra.GameData/Data/DataSharer.cs | 16 +++ .../Data/ObjectIdentification.cs | 123 ++++++++---------- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 19 ++- 5 files changed, 147 insertions(+), 117 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 0bcaa792..7889f679 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -11,7 +11,6 @@ using Dalamud.Game.Gui; using Dalamud.Plugin; using Dalamud.Utility; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -51,12 +50,19 @@ public sealed partial class ActorManager : IDisposable public ActorManagerData(DalamudPluginInterface pluginInterface, DataManager gameData, ClientLanguage language) : base(pluginInterface, language, 1) { - Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); - Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); - Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); - Ornaments = TryCatchData("Ornaments", () => CreateOrnamentData(gameData)); - BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); - ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); + var worldTask = TryCatchDataAsync("Worlds", CreateWorldData(gameData)); + var mountsTask = TryCatchDataAsync("Mounts", CreateMountData(gameData)); + var companionsTask = TryCatchDataAsync("Companions", CreateCompanionData(gameData)); + var ornamentsTask = TryCatchDataAsync("Ornaments", CreateOrnamentData(gameData)); + var bNpcsTask = TryCatchDataAsync("BNpcs", CreateBNpcData(gameData)); + var eNpcsTask = TryCatchDataAsync("ENpcs", CreateENpcData(gameData)); + + Worlds = worldTask.Result; + Mounts = mountsTask.Result; + Companions = companionsTask.Result; + Ornaments = ornamentsTask.Result; + BNpcs = bNpcsTask.Result; + ENpcs = eNpcsTask.Result; } /// @@ -109,40 +115,53 @@ public sealed partial class ActorManager : IDisposable DisposeTag("ENpcs"); } - private IReadOnlyDictionary CreateWorldData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) - .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + private Action> CreateWorldData(DataManager gameData) + => d => + { + foreach (var w in gameData.GetExcelSheet(Language)!.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)) + d.TryAdd((ushort)w.RowId, string.Intern(w.Name.ToDalamudString().TextValue)); + }; - private IReadOnlyDictionary CreateMountData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) - .ToDictionary(m => m.RowId, m => ToTitleCaseExtended(m.Singular, m.Article)); + private Action> CreateMountData(DataManager gameData) + => d => + { + foreach (var m in gameData.GetExcelSheet(Language)!.Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0)) + d.TryAdd(m.RowId, ToTitleCaseExtended(m.Singular, m.Article)); + }; - private IReadOnlyDictionary CreateCompanionData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) - .ToDictionary(c => c.RowId, c => ToTitleCaseExtended(c.Singular, c.Article)); + private Action> CreateCompanionData(DataManager gameData) + => d => + { + foreach (var c in gameData.GetExcelSheet(Language)!.Where(c + => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue)) + d.TryAdd(c.RowId, ToTitleCaseExtended(c.Singular, c.Article)); + }; - private IReadOnlyDictionary CreateOrnamentData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(o => o.Singular.RawData.Length > 0) - .ToDictionary(o => o.RowId, o => ToTitleCaseExtended(o.Singular, o.Article)); + private Action> CreateOrnamentData(DataManager gameData) + => d => + { + foreach (var o in gameData.GetExcelSheet(Language)!.Where(o => o.Singular.RawData.Length > 0)) + d.TryAdd(o.RowId, ToTitleCaseExtended(o.Singular, o.Article)); + }; - private IReadOnlyDictionary CreateBNpcData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(n => n.Singular.RawData.Length > 0) - .ToDictionary(n => n.RowId, n => ToTitleCaseExtended(n.Singular, n.Article)); + private Action> CreateBNpcData(DataManager gameData) + => d => + { + foreach (var n in gameData.GetExcelSheet(Language)!.Where(n => n.Singular.RawData.Length > 0)) + d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article)); + }; - private IReadOnlyDictionary CreateENpcData(DataManager gameData) - => gameData.GetExcelSheet(Language)! - .Where(e => e.Singular.RawData.Length > 0) - .ToDictionary(e => e.RowId, e => ToTitleCaseExtended(e.Singular, e.Article)); + private Action> CreateENpcData(DataManager gameData) + => d => + { + foreach (var n in gameData.GetExcelSheet(Language)!.Where(e => e.Singular.RawData.Length > 0)) + d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article)); + }; private static string ToTitleCaseExtended(SeString s, sbyte article) { if (article == 1) - return s.ToDalamudString().ToString(); + return string.Intern(s.ToDalamudString().ToString()); var sb = new StringBuilder(s.ToDalamudString().ToString()); var lastSpace = true; @@ -159,18 +178,20 @@ public sealed partial class ActorManager : IDisposable } } - return sb.ToString(); + return string.Intern(sb.ToString()); } } public readonly ActorManagerData Data; - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, DataManager gameData, GameGui gameGui, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, + DataManager gameData, GameGui gameGui, Func toParentIdx) : this(pluginInterface, objects, state, framework, gameData, gameGui, gameData.Language, toParentIdx) { } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, DataManager gameData, GameGui gameGui, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, + DataManager gameData, GameGui gameGui, ClientLanguage language, Func toParentIdx) { _framework = framework; diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 500bbc98..15c12714 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.String; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -124,18 +123,18 @@ public partial class ActorManager if (split.Length < 2) throw new IdentifierParseError($"The identifier string {userString} does not contain a type and a value."); - var type = IdentifierType.Invalid; - var playerName = ByteString.Empty; - ushort worldId = 0; - var kind = ObjectKind.Player; - var objectId = 0u; + IdentifierType type; + var playerName = ByteString.Empty; + ushort worldId = 0; + var kind = ObjectKind.Player; + var objectId = 0u; (ByteString, ushort) ParsePlayer(string player) { var parts = player.Split('@', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (!VerifyPlayerName(parts[0])) throw new IdentifierParseError($"{parts[0]} is not a valid player name."); - if (!ByteString.FromString(parts[0], out var p, false)) + if (!ByteString.FromString(parts[0], out var p)) throw new IdentifierParseError($"The player string {parts[0]} contains invalid symbols."); var world = parts.Length == 2 @@ -214,7 +213,7 @@ public partial class ActorManager type = IdentifierType.Retainer; if (!VerifyRetainerName(split[1])) throw new IdentifierParseError($"{split[1]} is not a valid player name."); - if (!ByteString.FromString(split[1], out playerName, false)) + if (!ByteString.FromString(split[1], out playerName)) throw new IdentifierParseError($"The retainer string {split[1]} contains invalid symbols."); break; diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs index 57b11bea..ce5fc0c3 100644 --- a/Penumbra.GameData/Data/DataSharer.cs +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Dalamud; using Dalamud.Logging; using Dalamud.Plugin; @@ -55,6 +57,20 @@ public abstract class DataSharer : IDisposable } } + protected Task TryCatchDataAsync(string tag, Action fill) where T : class, new() + { + tag = GetVersionedTag(tag, Language, Version); + if (PluginInterface.TryGetData(tag, out var data)) + return Task.FromResult(data); + + T ret = new(); + return Task.Run(() => + { + fill(ret); + return ret; + }); + } + public static void DisposeTag(DalamudPluginInterface pi, string tag, ClientLanguage language, int version) => pi.RelinquishData(GetVersionedTag(tag, language, version)); diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 9a45dcfc..53c71730 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; @@ -7,13 +8,15 @@ using Penumbra.GameData.Structs; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Plugin; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Actors; using Action = Lumina.Excel.GeneratedSheets.Action; -using ObjectType = Penumbra.GameData.Enums.ObjectType; +using ObjectType = Penumbra.GameData.Enums.ObjectType; + namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier @@ -38,7 +41,6 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier _equipment = new EquipmentIdentificationList(pluginInterface, language, dataManager); _weapons = new WeaponIdentificationList(pluginInterface, language, dataManager); Actions = TryCatchData("Actions", () => CreateActionList(dataManager)); - _equipment = new EquipmentIdentificationList(pluginInterface, language, dataManager); _modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager); BnpcNames = TryCatchData("BNpcNames", NpcNames.CreateNames); @@ -86,19 +88,10 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier DisposeTag("ModelObjects"); } - private static bool Add(IDictionary> dict, ulong key, Item item) - { - if (dict.TryGetValue(key, out var list)) - return list.Add(item); - - dict[key] = new HashSet { item }; - return true; - } - private IReadOnlyDictionary> CreateActionList(DataManager gameData) { var sheet = gameData.GetExcelSheet(Language)!; - var storage = new Dictionary>((int)sheet.RowCount); + var storage = new ConcurrentDictionary>(); void AddAction(string? key, Action action) { @@ -109,10 +102,15 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier if (storage.TryGetValue(key, out var actions)) actions.Add(action); else - storage[key] = new HashSet { action }; + storage[key] = new ConcurrentBag { action }; } - foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty)) + var options = new ParallelOptions + { + MaxDegreeOfParallelism = Environment.ProcessorCount, + }; + + Parallel.ForEach(sheet.Where(a => !a.Name.RawData.IsEmpty), options, action => { var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); @@ -120,39 +118,11 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier AddAction(startKey, action); AddAction(endKey, action); AddAction(hitKey, action); - } + }); return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } - private class Comparer : IComparer<(ulong, IReadOnlyList)> - { - public int Compare((ulong, IReadOnlyList) x, (ulong, IReadOnlyList) y) - => x.Item1.CompareTo(y.Item1); - } - - private static readonly Comparer _arrayComparer = new(); - - - private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList)> list, ulong key, ulong mask) - { - var maskedKey = key & mask; - var idx = list.BinarySearch(0, list.Count, (key, null!), _arrayComparer); - if (idx < 0) - { - if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask)) - return (-1, -1); - - idx = ~idx; - } - - var endIdx = idx + 1; - while (endIdx < list.Count && maskedKey == (list[endIdx].Item1 & mask)) - ++endIdx; - - return (idx, endIdx); - } - private void FindEquipment(IDictionary set, GameObjectInfo info) { var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant); @@ -282,21 +252,15 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier } private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, - DataManager gameData, - ClientLanguage language) + DataManager gameData, ClientLanguage language) { - var modelSheet = gameData.GetExcelSheet(language)!; - var bnpcSheet = gameData.GetExcelSheet(language)!; - var enpcSheet = gameData.GetExcelSheet(language)!; - var ornamentSheet = gameData.GetExcelSheet(language)!; - var mountSheet = gameData.GetExcelSheet(language)!; - var companionSheet = gameData.GetExcelSheet(language)!; - var ret = new List>((int)modelSheet.RowCount); + var modelSheet = gameData.GetExcelSheet(language)!; + var ret = new List>((int)modelSheet.RowCount); for (var i = -1; i < modelSheet.Last().RowId; ++i) - ret.Add(new HashSet<(string Name, ObjectKind Kind)>()); + ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind)>()); - void Add(int modelChara, ObjectKind kind, uint dataId) + void AddChara(int modelChara, ObjectKind kind, uint dataId) { if (modelChara == 0 || modelChara >= ret.Count) return; @@ -305,23 +269,42 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier ret[modelChara].Add((name, kind)); } - foreach (var ornament in ornamentSheet) - Add(ornament.Model, (ObjectKind)15, ornament.RowId); - - foreach (var mount in mountSheet) - Add((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId); - - foreach (var companion in companionSheet) - Add((int)companion.Model.Row, ObjectKind.Companion, companion.RowId); - - foreach (var enpc in enpcSheet) - Add((int)enpc.ModelChara.Row, ObjectKind.EventNpc, enpc.RowId); - - foreach (var bnpc in bnpcSheet.Where(b => b.RowId < BnpcNames.Count)) + var oTask = Task.Run(() => { - foreach (var name in BnpcNames[(int)bnpc.RowId]) - Add((int)bnpc.ModelChara.Row, ObjectKind.BattleNpc, name); - } + foreach (var ornament in gameData.GetExcelSheet(language)!) + AddChara(ornament.Model, (ObjectKind)15, ornament.RowId); + }); + + var mTask = Task.Run(() => + { + foreach (var mount in gameData.GetExcelSheet(language)!) + AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId); + }); + + var cTask = Task.Run(() => + { + foreach (var companion in gameData.GetExcelSheet(language)!) + AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId); + }); + + var eTask = Task.Run(() => + { + foreach (var eNpc in gameData.GetExcelSheet(language)!) + AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId); + }); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + + Parallel.ForEach(gameData.GetExcelSheet(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc => + { + foreach (var name in BnpcNames[(int)bNpc.RowId]) + AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name); + }); + + Task.WaitAll(oTask, mTask, cTask, eTask); return ret.Select(s => s.Count > 0 ? s.ToArray() diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index a82fb88e..dace9f57 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,5 +1,8 @@ +using Penumbra.UI.Classes; using System; +using System.Collections.Concurrent; using System.IO; +using System.Threading.Tasks; namespace Penumbra.Mods; @@ -87,14 +90,22 @@ public sealed partial class Mod if( Valid && BasePath.Exists ) { - foreach( var modFolder in BasePath.EnumerateDirectories() ) + var options = new ParallelOptions() { - var mod = LoadMod( modFolder, false ); - if( mod == null ) + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + var queue = new ConcurrentQueue< Mod >(); + Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir => + { + var mod = LoadMod( dir, false ); + if( mod != null ) { - continue; + queue.Enqueue( mod ); } + } ); + foreach( var mod in queue ) + { mod.Index = _mods.Count; _mods.Add( mod ); } From ea66bd2e67472290946ff05374cc705e0f1b7d89 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 15:38:57 +0100 Subject: [PATCH 0744/2451] Allow Profiling in Release for now. --- OtterGui | 2 +- Penumbra/Penumbra.csproj | 1 + Penumbra/UI/ConfigWindow.DebugTab.cs | 11 +++++++---- Penumbra/UI/ConfigWindow.cs | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index aee8a3dc..407aa485 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit aee8a3dc8e7eb1145c328a7c50f7e5bbcdd234f8 +Subproject commit 407aa4857ead69a03793a62b889452529c9bc572 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 8006457b..a97d1d90 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -20,6 +20,7 @@ $(MSBuildWarningsAsMessages);MSB3277 + PROFILING; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 19ada144..b800d3c8 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -116,17 +116,20 @@ public partial class ConfigWindow PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); } - [Conditional( "DEBUG" )] private static void DrawPerformanceTab() { ImGui.NewLine(); - if( !ImGui.CollapsingHeader( "Performance" ) ) + if( ImGui.CollapsingHeader( "Performance" ) ) { return; } - Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); - ImGui.NewLine(); + using( var start = TreeNode( "Startup Performance", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); + ImGui.NewLine(); + } + Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index e861ccec..99646b21 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -27,6 +27,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _penumbra = penumbra; _settingsTab = new SettingsTab( this ); + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _modPanel = new ModPanel( this ); _selector.SelectionChanged += _modPanel.OnSelectionChange; From b26923e5046eea59c66cc23a6d1cca0dbce3d42d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 15:40:35 +0100 Subject: [PATCH 0745/2451] Derp. --- Penumbra/UI/ConfigWindow.DebugTab.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index b800d3c8..9a1e0940 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -126,8 +126,11 @@ public partial class ConfigWindow using( var start = TreeNode( "Startup Performance", ImGuiTreeNodeFlags.DefaultOpen ) ) { - Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); - ImGui.NewLine(); + if( start ) + { + Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); + ImGui.NewLine(); + } } Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); From 41b88b036ee08b46cd95697140e708e1968f8b47 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 19:15:48 +0100 Subject: [PATCH 0746/2451] Misc. --- Penumbra/UI/ConfigWindow.DebugTab.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 9a1e0940..bc8499e1 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,17 +1,13 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using OtterGui; -using OtterGui.Raii; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; From bb805345b160c3a56ffa6fd632da79bd9abcbec5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Feb 2023 20:23:52 +0100 Subject: [PATCH 0747/2451] Minimal cleanup on option descriptions. --- .../IndividualCollections.Access.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Import/TexToolsStructs.cs | 5 ++-- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 8 ++---- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 13 ++++++--- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 27 ++++++++++++++----- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 2e421807..83f32b18 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -136,7 +136,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card ); } - return ( Penumbra.Config.UseCharacterCollectionInTryOn ? _actorManager.GetGlamourPlayer() : ActorIdentifier.Invalid, SpecialResult.Glamour ); + return Penumbra.Config.UseCharacterCollectionInTryOn ? ( _actorManager.GetGlamourPlayer(), SpecialResult.Glamour ) : ( identifier, SpecialResult.Invalid ); } default: return ( identifier, SpecialResult.Invalid ); } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 71760187..e2edfe92 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -213,7 +213,7 @@ public partial class TexToolsImporter } Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - defaultSettings ?? 0, string.Empty, options ); + defaultSettings ?? 0, group.Description, options ); ++groupPriority; } } diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/TexToolsStructs.cs index 1e92d1a4..da01dda2 100644 --- a/Penumbra/Import/TexToolsStructs.cs +++ b/Penumbra/Import/TexToolsStructs.cs @@ -34,8 +34,9 @@ internal class ModPackPage internal class ModGroup { public string GroupName = string.Empty; - public GroupType SelectionType = GroupType.Single; + public GroupType SelectionType = GroupType.Single; public OptionList[] OptionList = Array.Empty< OptionList >(); + public string Description = string.Empty; } [Serializable] @@ -46,7 +47,7 @@ internal class OptionList public string ImagePath = string.Empty; public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >(); public string GroupName = string.Empty; - public GroupType SelectionType = GroupType.Single; + public GroupType SelectionType = GroupType.Single; public bool IsChecked = false; } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 1887c490..4e482612 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -128,16 +128,12 @@ public sealed partial class Mod { var group = mod._groups[ groupIdx ]; var option = group[ optionIdx ]; - if( option.Description == newDescription ) + if( option.Description == newDescription || option is not SubMod s ) { return; } - var _ = option switch - { - SubMod s => s.Description = newDescription, - }; - + s.Description = newDescription; ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 951b0200..45f8f25c 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -311,8 +311,13 @@ public partial class ConfigWindow { _newDescriptionIdx = groupIdx; _newDesriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 ? mod.Description : optionIdx < 0 ? mod.Groups[ groupIdx ].Description : mod.Groups[ groupIdx ][ optionIdx ].Description; - _mod = mod; + _newDescription = groupIdx < 0 + ? mod.Description + : optionIdx < 0 + ? mod.Groups[ groupIdx ].Description + : mod.Groups[ groupIdx ][ optionIdx ].Description; + + _mod = mod; ImGui.OpenPopup( PopupName ); } @@ -365,6 +370,7 @@ public partial class ConfigWindow { Penumbra.ModManager.ChangeOptionDescription( _mod, _newDescriptionIdx, _newDesriptionOptionIdx, _newDescription ); } + break; } @@ -486,7 +492,7 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, - panel._window._inputTextWidth.X - 68 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); + panel._window._inputTextWidth.X - 72 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() - panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "description", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); @@ -547,6 +553,7 @@ public partial class ConfigWindow { panel._delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( panel._mod, groupIdx, optionIdx ) ); } + ImGui.TableNextColumn(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 32735bc0..8f9b8c36 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -64,7 +64,7 @@ public partial class ConfigWindow if( _mod.Groups.Count > 0 ) { var useDummy = true; - foreach(var (group, idx) in _mod.Groups.WithIndex().Where(g => g.Value.Type == GroupType.Single && g.Value.IsOption )) + foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Single && g.Value.IsOption ) ) { ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); useDummy = false; @@ -175,15 +175,27 @@ public partial class ConfigWindow for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { id.Push( idx2 ); - if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) ) + var option = group[ idx2 ]; + if( ImGui.Selectable( option.Name, idx2 == selectedOption ) ) { Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); } - if( !string.IsNullOrEmpty( group[ idx2 ].Description ) ) + if( option.Description.Length > 0 ) { + var hovered = ImGui.IsItemHovered(); ImGui.SameLine(); - ImGuiComponents.HelpMarker(group[idx2].Description); + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetColorU32( ImGuiCol.TextDisabled ) ); + ImGuiUtil.RightAlign( FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X ); + } + + if( hovered ) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted( option.Description ); + } } id.Pop(); @@ -211,19 +223,20 @@ public partial class ConfigWindow Widget.BeginFramedGroup( group.Name, group.Description ); for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { + var option = group[ idx2 ]; id.Push( idx2 ); var flag = 1u << idx2; var setting = ( flags & flag ) != 0; - if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) ) + if( ImGui.Checkbox( option.Name, ref setting ) ) { flags = setting ? flags | flag : flags & ~flag; Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); } - if( !string.IsNullOrEmpty( group[ idx2 ].Description ) ) + if( option.Description.Length > 0 ) { ImGui.SameLine(); - ImGuiComponents.HelpMarker(group[idx2].Description); + ImGuiComponents.HelpMarker( option.Description ); } id.Pop(); From bdef7a51182fa9c489d1ede346673fdd6a4ab2f2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 7 Feb 2023 21:22:15 +0100 Subject: [PATCH 0748/2451] Fix mipmap generation on Wine (Mac/Linux) --- Penumbra/Import/Textures/CombinedTexture.cs | 3 ++- Penumbra/Penumbra.cs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 3a8762d4..4b82da4b 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -6,6 +6,7 @@ using OtterTex; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; +using DalamudUtil = Dalamud.Utility.Util; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; @@ -137,7 +138,7 @@ public partial class CombinedTexture : IDisposable } var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ); - var ec = input.GenerateMipMaps( out var ret, numMips, FilterFlags.SeparateAlpha ); + var ec = input.GenerateMipMaps( out var ret, numMips, ( DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0 ) | FilterFlags.SeparateAlpha ); if (ec != ErrorCode.Ok) { throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 85f97f9e..fd988889 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -28,6 +28,7 @@ using Penumbra.Interop.Resolver; using Penumbra.Mods; using Action = System.Action; using CharacterUtility = Penumbra.Interop.CharacterUtility; +using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; namespace Penumbra; @@ -367,6 +368,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Commit Hash: `** {CommitHash}\n" ); sb.Append( $"> **`Enable Mods: `** {Config.EnableMods}\n" ); sb.Append( $"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n" ); + sb.Append( $"> **`Operating System: `** {( DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows" )}\n" ); sb.Append( $"> **`Root Directory: `** `{Config.ModDirectory}`, {( exists ? "Exists" : "Not Existing" )}\n" ); sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); From c3a71ab95e65c5f97038d8fcb06635803378122b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Feb 2023 20:46:35 +0100 Subject: [PATCH 0749/2451] Update BannerInterfaceStorage for 6.31h --- OtterGui | 2 +- Penumbra.GameData/Actors/AgentBannerParty.cs | 26 ++++++++++---------- Penumbra.GameData/Actors/ScreenActor.cs | 6 +++-- Penumbra.String | 2 +- Penumbra/UI/ConfigWindow.Changelog.cs | 12 +++++++++ 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/OtterGui b/OtterGui index 407aa485..9a574c4a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 407aa4857ead69a03793a62b889452529c9bc572 +Subproject commit 9a574c4a50b86a5ff84544d989608d2339b713b2 diff --git a/Penumbra.GameData/Actors/AgentBannerParty.cs b/Penumbra.GameData/Actors/AgentBannerParty.cs index 9756a289..2de95c8c 100644 --- a/Penumbra.GameData/Actors/AgentBannerParty.cs +++ b/Penumbra.GameData/Actors/AgentBannerParty.cs @@ -46,12 +46,12 @@ public unsafe struct AgentBannerMIP // Client::UI::Agent::AgentBannerInterface::Storage // destructed in Client::UI::Agent::AgentBannerInterface::dtor -[StructLayout( LayoutKind.Explicit, Size = 0x3BB0 )] +[StructLayout( LayoutKind.Explicit, Size = 0x3B30 )] public unsafe struct BannerInterfaceStorage { // vtable: 48 8D 05 ?? ?? ?? ?? 48 89 01 48 8B F9 7E // dtor: E8 ?? ?? ?? ?? 48 83 EF ?? 75 ?? BA ?? ?? ?? ?? 48 8B CE E8 ?? ?? ?? ?? 48 89 7D - [StructLayout( LayoutKind.Explicit, Size = 0x770 )] + [StructLayout( LayoutKind.Explicit, Size = 0x760 )] public struct CharacterData { [FieldOffset( 0x000 )] public void** VTable; @@ -67,8 +67,8 @@ public unsafe struct BannerInterfaceStorage [FieldOffset( 0x2B0 )] public void* CharaView; [FieldOffset( 0x5D0 )] public AtkTexture AtkTexture; - [FieldOffset( 0x6F8 )] public Utf8String Title; - [FieldOffset( 0x768 )] public void* SomePointer; + [FieldOffset( 0x6E0 )] public Utf8String Title; + [FieldOffset( 0x750 )] public void* SomePointer; } @@ -78,14 +78,14 @@ public unsafe struct BannerInterfaceStorage [FieldOffset( 0x0014 )] public uint Unk2; [FieldOffset( 0x0020 )] public CharacterData Character1; - [FieldOffset( 0x0790 )] public CharacterData Character2; - [FieldOffset( 0x0F00 )] public CharacterData Character3; - [FieldOffset( 0x1670 )] public CharacterData Character4; - [FieldOffset( 0x1DE0 )] public CharacterData Character5; - [FieldOffset( 0x2550 )] public CharacterData Character6; - [FieldOffset( 0x2CC0 )] public CharacterData Character7; - [FieldOffset( 0x3430 )] public CharacterData Character8; + [FieldOffset( 0x0780 )] public CharacterData Character2; + [FieldOffset( 0x0EE0 )] public CharacterData Character3; + [FieldOffset( 0x1640 )] public CharacterData Character4; + [FieldOffset( 0x1DA0 )] public CharacterData Character5; + [FieldOffset( 0x2500 )] public CharacterData Character6; + [FieldOffset( 0x2C60 )] public CharacterData Character7; + [FieldOffset( 0x33C0 )] public CharacterData Character8; - [FieldOffset( 0x3BA0 )] public long Unk3; - [FieldOffset( 0x3BA8 )] public long Unk4; + [FieldOffset( 0x3B20 )] public long Unk3; + [FieldOffset( 0x3B28 )] public long Unk4; } \ No newline at end of file diff --git a/Penumbra.GameData/Actors/ScreenActor.cs b/Penumbra.GameData/Actors/ScreenActor.cs index bc046407..00cf66fc 100644 --- a/Penumbra.GameData/Actors/ScreenActor.cs +++ b/Penumbra.GameData/Actors/ScreenActor.cs @@ -2,9 +2,10 @@ namespace Penumbra.GameData.Actors; public enum ScreenActor : ushort { - CutsceneStart = 200, + CutsceneStart = 200, + GPosePlayer = 201, CutsceneEnd = 240, - CharacterScreen = 240, + CharacterScreen = CutsceneEnd, ExamineScreen = 241, FittingRoom = 242, DyePreview = 243, @@ -12,4 +13,5 @@ public enum ScreenActor : ushort Card6 = 245, Card7 = 246, Card8 = 247, + ScreenEnd = Card8 + 1, } diff --git a/Penumbra.String b/Penumbra.String index 5ae32fd5..2f396444 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 5ae32fd5f19fcc641d5f2d777de2276186900c0b +Subproject commit 2f396444c80ef2d2dca30b32c8f0ef787a928534 diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index e595fb91..50c80eea 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -34,10 +34,22 @@ public partial class ConfigWindow Add6_3_0( ret ); Add6_4_0( ret ); Add6_5_0( ret ); + Add6_5_2( ret ); return ret; } + private static void Add6_5_2( Changelog log ) + => log.NextVersion( "Version 0.6.5.2" ) + .RegisterEntry( "Updated for game version 6.31 Hotfix." ) + .RegisterEntry( "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)" ) + .RegisterEntry( "Those are now accurately parsed from TTMPs, too.", 1 ) + .RegisterEntry( "Improved launch times somewhat through parallelization of some tasks." ) + .RegisterEntry( "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled." ) + .RegisterEntry( "Fixed an issue with IMC changes and Mare Synchronos interoperability." ) + .RegisterEntry( "Fixed an issue with housing mannequins crashing the game when resource logging was enabled." ) + .RegisterEntry( "Fixed an issue generating Mip Maps for texture import on Wine." ); + private static void Add6_5_0( Changelog log ) => log.NextVersion( "Version 0.6.5.0" ) .RegisterEntry( "Fixed an issue with Item Swaps not using applied IMC changes in some cases." ) From 68a787d125ce6a4a2ef150d6c23d73b742dc262c Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 9 Feb 2023 20:12:21 +0000 Subject: [PATCH 0750/2451] [CI] Updating repo.json for refs/tags/0.6.5.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 574c4fd2..4fc8d718 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.5.0", - "TestingAssemblyVersion": "0.6.5.0", + "AssemblyVersion": "0.6.5.2", + "TestingAssemblyVersion": "0.6.5.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 08519396a0bf0c39edeec3d2a473d5f978d48c28 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Feb 2023 13:19:18 +0100 Subject: [PATCH 0751/2451] Allow Penumbra to use long and arbitrary UTF8 paths. --- Penumbra.String | 2 +- Penumbra/Interop/Loader/CreateFileWHook.cs | 172 ++++++++++++++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 1 + .../Loader/ResourceLoader.Replacement.cs | 37 ++-- Penumbra/Interop/Loader/ResourceLoader.cs | 2 + Penumbra/Mods/Mod.Creation.cs | 6 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 6 - 7 files changed, 195 insertions(+), 31 deletions(-) create mode 100644 Penumbra/Interop/Loader/CreateFileWHook.cs diff --git a/Penumbra.String b/Penumbra.String index 2f396444..3276f379 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 2f396444c80ef2d2dca30b32c8f0ef787a928534 +Subproject commit 3276f37974e668b1e4769c176c114da17611b734 diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs new file mode 100644 index 00000000..21f40e6b --- /dev/null +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -0,0 +1,172 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Dalamud.Hooking; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.String.Functions; + +namespace Penumbra.Interop.Loader; + +/// +/// To allow XIV to load files of arbitrary path length, +/// we use the fixed size buffers of their formats to only store pointers to the actual path instead. +/// Then we translate the stored pointer to the path in CreateFileW, if the prefix matches. +/// +public unsafe class CreateFileWHook : IDisposable +{ + public const int RequiredSize = 28; + + // The prefix is not valid for any actual path, so should never run into false-positives. + private const char Prefix = ( char )( ( byte )'P' | ( ( '?' & 0x00FF ) << 8 ) ); + private const int BufferSize = Utf8GamePath.MaxGamePathLength; + + [DllImport( "kernel32.dll" )] + private static extern nint LoadLibrary( string dllName ); + + [DllImport( "kernel32.dll" )] + private static extern nint GetProcAddress( nint hModule, string procName ); + + private delegate nint CreateFileWDelegate( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ); + + private readonly Hook< CreateFileWDelegate > _createFileWHook; + + /// Some storage to skip repeated allocations. + private readonly ThreadLocal< nint > _fileNameStorage = new(SetupStorage, true); + + public CreateFileWHook() + { + var userApi = LoadLibrary( "kernel32.dll" ); + var createFileAddress = GetProcAddress( userApi, "CreateFileW" ); + _createFileWHook = Hook< CreateFileWDelegate >.FromAddress( createFileAddress, CreateFileWDetour ); + } + + /// Long paths in windows need to start with "\\?\", so we keep this static in the pointers. + private static nint SetupStorage() + { + var ptr = ( char* )Marshal.AllocHGlobal( 2 * BufferSize ); + ptr[ 0 ] = '\\'; + ptr[ 1 ] = '\\'; + ptr[ 2 ] = '?'; + ptr[ 3 ] = '\\'; + ptr[ 4 ] = '\0'; + return ( nint )ptr; + } + + public void Enable() + => _createFileWHook.Enable(); + + public void Disable() + => _createFileWHook.Disable(); + + public void Dispose() + { + _createFileWHook.Dispose(); + foreach( var ptr in _fileNameStorage.Values ) + { + Marshal.FreeHGlobal( ptr ); + } + } + + private nint CreateFileWDetour( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ) + { + // Translate data if prefix fits. + if( CheckPtr( fileName, out var name ) ) + { + // Use static storage. + var ptr = WriteFileName( name ); + Penumbra.Log.Verbose( $"Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); + return _createFileWHook.Original( ptr, access, shareMode, security, creation, flags, template ); + } + + return _createFileWHook.Original( fileName, access, shareMode, security, creation, flags, template ); + } + + + /// Write the UTF8-encoded byte string as UTF16 into the static buffers, + /// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t. + private char* WriteFileName( ReadOnlySpan< byte > actualName ) + { + var span = new Span< char >( ( char* )_fileNameStorage.Value + 4, BufferSize - 4 ); + var written = Encoding.UTF8.GetChars( actualName, span ); + for( var i = 0; i < written; ++i ) + { + if( span[ i ] == '/' ) + { + span[ i ] = '\\'; + } + } + + span[ written ] = '\0'; + + return ( char* )_fileNameStorage.Value; + } + + + public static void WritePtr( char* buffer, byte* address, int length ) + { + // Set the prefix, which is not valid for any actual path. + buffer[ 0 ] = Prefix; + + var ptr = ( byte* )buffer; + var v = ( ulong )address; + var l = ( uint )length; + + // Since the game calls wstrcpy without a length, we need to ensure + // that there is no wchar_t (i.e. 2 bytes) of 0-values before the end. + // Fill everything with 0xFF and use every second byte. + MemoryUtility.MemSet( ptr + 2, 0xFF, 23 ); + + // Write the byte pointer. + ptr[ 2 ] = ( byte )( v >> 0 ); + ptr[ 4 ] = ( byte )( v >> 8 ); + ptr[ 6 ] = ( byte )( v >> 16 ); + ptr[ 8 ] = ( byte )( v >> 24 ); + ptr[ 10 ] = ( byte )( v >> 32 ); + ptr[ 12 ] = ( byte )( v >> 40 ); + ptr[ 14 ] = ( byte )( v >> 48 ); + ptr[ 16 ] = ( byte )( v >> 56 ); + + // Write the length. + ptr[ 18 ] = ( byte )( l >> 0 ); + ptr[ 20 ] = ( byte )( l >> 8 ); + ptr[ 22 ] = ( byte )( l >> 16 ); + ptr[ 24 ] = ( byte )( l >> 24 ); + + ptr[ RequiredSize - 2 ] = 0; + ptr[ RequiredSize - 1 ] = 0; + } + + private static bool CheckPtr( char* buffer, out ReadOnlySpan< byte > fileName ) + { + if( buffer[ 0 ] is not Prefix ) + { + fileName = ReadOnlySpan< byte >.Empty; + return false; + } + + var ptr = ( byte* )buffer; + + // Read the byte pointer. + var address = 0ul; + address |= ( ulong )ptr[ 2 ] << 0; + address |= ( ulong )ptr[ 4 ] << 8; + address |= ( ulong )ptr[ 6 ] << 16; + address |= ( ulong )ptr[ 8 ] << 24; + address |= ( ulong )ptr[ 10 ] << 32; + address |= ( ulong )ptr[ 12 ] << 40; + address |= ( ulong )ptr[ 14 ] << 48; + address |= ( ulong )ptr[ 16 ] << 56; + + // Read the length. + var length = 0u; + length |= ( uint )ptr[ 18 ] << 0; + length |= ( uint )ptr[ 20 ] << 8; + length |= ( uint )ptr[ 22 ] << 16; + length |= ( uint )ptr[ 24 ] << 24; + + fileName = new ReadOnlySpan< byte >( ( void* )address, ( int )length ); + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 438e6fa5..d921e7f3 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -209,6 +209,7 @@ public unsafe partial class ResourceLoader for( var i = 0; i < _debugList.Count; ++i ) { var data = _debugList.Values[ i ]; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if( data.OriginalPath.Path == null ) { _debugList.RemoveAt( i-- ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 7b19c62a..27dac9b3 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,8 +1,3 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -13,6 +8,11 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -20,6 +20,8 @@ namespace Penumbra.Interop.Loader; public unsafe partial class ResourceLoader { + private readonly CreateFileWHook _createFileWHook = new(); + // Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. // Both work basically the same, so we can reduce the main work to one function used by both hooks. @@ -173,6 +175,7 @@ public unsafe partial class ResourceLoader default: break; } + return DefaultResolver( path ); } @@ -249,27 +252,20 @@ public unsafe partial class ResourceLoader return ret; } - // Load the resource from a path on the users hard drives. + /// Load the resource from a path on the users hard drives. + /// private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { // Specify that we are loading unpacked files from the drive. - // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, - // but since we only allow ASCII in the game paths, this is just a matter of upcasting. + // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations. fileDescriptor->FileMode = FileMode.LoadUnpackedResource; - var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; - fileDescriptor->FileDescriptor = fd; - var fdPtr = ( char* )( fd + 0x21 ); - for( var i = 0; i < gamePath.Length; ++i ) - { - var c = ( char )gamePath.Path[ i ]; - ( &fileDescriptor->Utf16FileName )[ i ] = c; - fdPtr[ i ] = c; - } - - ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; - fdPtr[ gamePath.Length ] = '\0'; + // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. + var fd = stackalloc char[0x11 + 0x0B + 14]; + fileDescriptor->FileDescriptor = (byte*) fd + 1; + CreateFileWHook.WritePtr( fd + 0x11, gamePath.Path, gamePath.Length ); + CreateFileWHook.WritePtr( &fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length ); // Use the SE ReadFile function. var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); @@ -287,6 +283,7 @@ public unsafe partial class ResourceLoader private void DisposeHooks() { DisableHooks(); + _createFileWHook.Dispose(); ReadSqPackHook.Dispose(); GetResourceSyncHook.Dispose(); GetResourceAsyncHook.Dispose(); diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 9a929eaf..2eb0e010 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -82,6 +82,7 @@ public unsafe partial class ResourceLoader : IDisposable } HooksEnabled = true; + _createFileWHook.Enable(); ReadSqPackHook.Enable(); GetResourceSyncHook.Enable(); GetResourceAsyncHook.Enable(); @@ -96,6 +97,7 @@ public unsafe partial class ResourceLoader : IDisposable } HooksEnabled = false; + _createFileWHook.Disable(); ReadSqPackHook.Disable(); GetResourceSyncHook.Disable(); GetResourceAsyncHook.Disable(); diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 69adbeef..13cd4ab7 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -156,9 +156,7 @@ public partial class Mod private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); - - // XIV can not deal with non-ascii symbols in a path, - // and the path must obviously be valid itself. + // Normalize for nicer names, and remove invalid symbols or invalid paths. public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) { if( s == "." ) @@ -174,7 +172,7 @@ public partial class Mod StringBuilder sb = new(s.Length); foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) { - if( c.IsInvalidAscii() || c.IsInvalidInPath() ) + if( c.IsInvalidInPath() ) { sb.Append( replacement ); } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 3a251e23..8857a21b 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -104,12 +104,6 @@ public partial class ConfigWindow return ( "Path is not allowed to be a drive root. Please add a directory.", false ); } - var symbol = '\0'; - if( newName.Any( c => ( symbol = c ) > ( char )0x7F ) ) - { - return ( $"Path contains invalid symbol {symbol}. Only ASCII is allowed.", false ); - } - var desktop = Environment.GetFolderPath( Environment.SpecialFolder.Desktop ); if( IsSubPathOf( desktop, newName ) ) { From 9098b5b3b36ca532b56c1baeba381204ec6b0f43 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Feb 2023 16:53:31 +0100 Subject: [PATCH 0752/2451] Revamp resource logging. --- OtterGui | 2 +- Penumbra.GameData/Enums/ResourceType.cs | 368 ++++++++++++++---- Penumbra/Configuration.cs | 9 +- Penumbra/Penumbra.cs | 12 +- Penumbra/Penumbra.csproj.DotSettings | 3 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 55 --- Penumbra/UI/ConfigWindow.cs | 18 +- .../ResourceWatcher/ResourceWatcher.Record.cs | 118 ++++++ .../ResourceWatcher.RecordType.cs | 17 + .../ResourceWatcher/ResourceWatcher.Table.cs | 352 +++++++++++++++++ .../UI/ResourceWatcher/ResourceWatcher.cs | 245 ++++++++++++ 11 files changed, 1051 insertions(+), 148 deletions(-) create mode 100644 Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs create mode 100644 Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs create mode 100644 Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs create mode 100644 Penumbra/UI/ResourceWatcher/ResourceWatcher.cs diff --git a/OtterGui b/OtterGui index 9a574c4a..fb6526d0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9a574c4a50b86a5ff84544d989608d2339b713b2 +Subproject commit fb6526d0c034e97ee079ee88ca931e536f99c5a7 diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs index 42783c97..80ba03e9 100644 --- a/Penumbra.GameData/Enums/ResourceType.cs +++ b/Penumbra.GameData/Enums/ResourceType.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.String; using Penumbra.String.Functions; @@ -7,105 +9,311 @@ namespace Penumbra.GameData.Enums; public enum ResourceType : uint { - Aet = 0x00616574, - Amb = 0x00616D62, - Atch = 0x61746368, - Atex = 0x61746578, - Avfx = 0x61766678, - Awt = 0x00617774, - Cmp = 0x00636D70, - Dic = 0x00646963, - Eid = 0x00656964, - Envb = 0x656E7662, - Eqdp = 0x65716470, - Eqp = 0x00657170, - Essb = 0x65737362, - Est = 0x00657374, - Exd = 0x00657864, - Exh = 0x00657868, - Exl = 0x0065786C, - Fdt = 0x00666474, - Gfd = 0x00676664, - Ggd = 0x00676764, - Gmp = 0x00676D70, - Gzd = 0x00677A64, - Imc = 0x00696D63, - Lcb = 0x006C6362, - Lgb = 0x006C6762, - Luab = 0x6C756162, - Lvb = 0x006C7662, - Mdl = 0x006D646C, - Mlt = 0x006D6C74, - Mtrl = 0x6D74726C, - Obsb = 0x6F627362, - Pap = 0x00706170, - Pbd = 0x00706264, - Pcb = 0x00706362, - Phyb = 0x70687962, - Plt = 0x00706C74, - Scd = 0x00736364, - Sgb = 0x00736762, - Shcd = 0x73686364, - Shpk = 0x7368706B, - Sklb = 0x736B6C62, - Skp = 0x00736B70, - Stm = 0x0073746D, - Svb = 0x00737662, - Tera = 0x74657261, - Tex = 0x00746578, - Tmb = 0x00746D62, - Ugd = 0x00756764, - Uld = 0x00756C64, - Waoe = 0x77616F65, - Wtd = 0x00777464, + Unknown = 0, + Aet = 0x00616574, + Amb = 0x00616D62, + Atch = 0x61746368, + Atex = 0x61746578, + Avfx = 0x61766678, + Awt = 0x00617774, + Cmp = 0x00636D70, + Dic = 0x00646963, + Eid = 0x00656964, + Envb = 0x656E7662, + Eqdp = 0x65716470, + Eqp = 0x00657170, + Essb = 0x65737362, + Est = 0x00657374, + Evp = 0x00657670, + Exd = 0x00657864, + Exh = 0x00657868, + Exl = 0x0065786C, + Fdt = 0x00666474, + Gfd = 0x00676664, + Ggd = 0x00676764, + Gmp = 0x00676D70, + Gzd = 0x00677A64, + Imc = 0x00696D63, + Lcb = 0x006C6362, + Lgb = 0x006C6762, + Luab = 0x6C756162, + Lvb = 0x006C7662, + Mdl = 0x006D646C, + Mlt = 0x006D6C74, + Mtrl = 0x6D74726C, + Obsb = 0x6F627362, + Pap = 0x00706170, + Pbd = 0x00706264, + Pcb = 0x00706362, + Phyb = 0x70687962, + Plt = 0x00706C74, + Scd = 0x00736364, + Sgb = 0x00736762, + Shcd = 0x73686364, + Shpk = 0x7368706B, + Sklb = 0x736B6C62, + Skp = 0x00736B70, + Stm = 0x0073746D, + Svb = 0x00737662, + Tera = 0x74657261, + Tex = 0x00746578, + Tmb = 0x00746D62, + Ugd = 0x00756764, + Uld = 0x00756C64, + Waoe = 0x77616F65, + Wtd = 0x00777464, } -public static class ResourceTypeExtensions +[Flags] +public enum ResourceTypeFlag : ulong { - public static ResourceType FromBytes( byte a1, byte a2, byte a3 ) - => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 16 ) - | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 8 ) - | ByteStringFunctions.AsciiToLower( a3 ) ); + Aet = 0x0000_0000_0000_0001, + Amb = 0x0000_0000_0000_0002, + Atch = 0x0000_0000_0000_0004, + Atex = 0x0000_0000_0000_0008, + Avfx = 0x0000_0000_0000_0010, + Awt = 0x0000_0000_0000_0020, + Cmp = 0x0000_0000_0000_0040, + Dic = 0x0000_0000_0000_0080, + Eid = 0x0000_0000_0000_0100, + Envb = 0x0000_0000_0000_0200, + Eqdp = 0x0000_0000_0000_0400, + Eqp = 0x0000_0000_0000_0800, + Essb = 0x0000_0000_0000_1000, + Est = 0x0000_0000_0000_2000, + Evp = 0x0000_0000_0000_4000, + Exd = 0x0000_0000_0000_8000, + Exh = 0x0000_0000_0001_0000, + Exl = 0x0000_0000_0002_0000, + Fdt = 0x0000_0000_0004_0000, + Gfd = 0x0000_0000_0008_0000, + Ggd = 0x0000_0000_0010_0000, + Gmp = 0x0000_0000_0020_0000, + Gzd = 0x0000_0000_0040_0000, + Imc = 0x0000_0000_0080_0000, + Lcb = 0x0000_0000_0100_0000, + Lgb = 0x0000_0000_0200_0000, + Luab = 0x0000_0000_0400_0000, + Lvb = 0x0000_0000_0800_0000, + Mdl = 0x0000_0000_1000_0000, + Mlt = 0x0000_0000_2000_0000, + Mtrl = 0x0000_0000_4000_0000, + Obsb = 0x0000_0000_8000_0000, + Pap = 0x0000_0001_0000_0000, + Pbd = 0x0000_0002_0000_0000, + Pcb = 0x0000_0004_0000_0000, + Phyb = 0x0000_0008_0000_0000, + Plt = 0x0000_0010_0000_0000, + Scd = 0x0000_0020_0000_0000, + Sgb = 0x0000_0040_0000_0000, + Shcd = 0x0000_0080_0000_0000, + Shpk = 0x0000_0100_0000_0000, + Sklb = 0x0000_0200_0000_0000, + Skp = 0x0000_0400_0000_0000, + Stm = 0x0000_0800_0000_0000, + Svb = 0x0000_1000_0000_0000, + Tera = 0x0000_2000_0000_0000, + Tex = 0x0000_4000_0000_0000, + Tmb = 0x0000_8000_0000_0000, + Ugd = 0x0001_0000_0000_0000, + Uld = 0x0002_0000_0000_0000, + Waoe = 0x0004_0000_0000_0000, + Wtd = 0x0008_0000_0000_0000, +} - public static ResourceType FromBytes( byte a1, byte a2, byte a3, byte a4 ) - => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 24 ) - | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 16 ) - | ( ( uint )ByteStringFunctions.AsciiToLower( a3 ) << 8 ) - | ByteStringFunctions.AsciiToLower( a4 ) ); +[Flags] +public enum ResourceCategoryFlag : ushort +{ + Common = 0x0001, + BgCommon = 0x0002, + Bg = 0x0004, + Cut = 0x0008, + Chara = 0x0010, + Shader = 0x0020, + Ui = 0x0040, + Sound = 0x0080, + Vfx = 0x0100, + UiScript = 0x0200, + Exd = 0x0400, + GameScript = 0x0800, + Music = 0x1000, + SqpackTest = 0x2000, +} - public static ResourceType FromBytes( char a1, char a2, char a3 ) - => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3 ); +public static class ResourceExtensions +{ + public static readonly ResourceTypeFlag AllResourceTypes = Enum.GetValues().Aggregate((v, f) => v | f); + public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues().Aggregate((v, f) => v | f); - public static ResourceType FromBytes( char a1, char a2, char a3, char a4 ) - => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3, ( byte )a4 ); + public static ResourceTypeFlag ToFlag(this ResourceType type) + => type switch + { + ResourceType.Aet => ResourceTypeFlag.Aet, + ResourceType.Amb => ResourceTypeFlag.Amb, + ResourceType.Atch => ResourceTypeFlag.Atch, + ResourceType.Atex => ResourceTypeFlag.Atex, + ResourceType.Avfx => ResourceTypeFlag.Avfx, + ResourceType.Awt => ResourceTypeFlag.Awt, + ResourceType.Cmp => ResourceTypeFlag.Cmp, + ResourceType.Dic => ResourceTypeFlag.Dic, + ResourceType.Eid => ResourceTypeFlag.Eid, + ResourceType.Envb => ResourceTypeFlag.Envb, + ResourceType.Eqdp => ResourceTypeFlag.Eqdp, + ResourceType.Eqp => ResourceTypeFlag.Eqp, + ResourceType.Essb => ResourceTypeFlag.Essb, + ResourceType.Est => ResourceTypeFlag.Est, + ResourceType.Evp => ResourceTypeFlag.Evp, + ResourceType.Exd => ResourceTypeFlag.Exd, + ResourceType.Exh => ResourceTypeFlag.Exh, + ResourceType.Exl => ResourceTypeFlag.Exl, + ResourceType.Fdt => ResourceTypeFlag.Fdt, + ResourceType.Gfd => ResourceTypeFlag.Gfd, + ResourceType.Ggd => ResourceTypeFlag.Ggd, + ResourceType.Gmp => ResourceTypeFlag.Gmp, + ResourceType.Gzd => ResourceTypeFlag.Gzd, + ResourceType.Imc => ResourceTypeFlag.Imc, + ResourceType.Lcb => ResourceTypeFlag.Lcb, + ResourceType.Lgb => ResourceTypeFlag.Lgb, + ResourceType.Luab => ResourceTypeFlag.Luab, + ResourceType.Lvb => ResourceTypeFlag.Lvb, + ResourceType.Mdl => ResourceTypeFlag.Mdl, + ResourceType.Mlt => ResourceTypeFlag.Mlt, + ResourceType.Mtrl => ResourceTypeFlag.Mtrl, + ResourceType.Obsb => ResourceTypeFlag.Obsb, + ResourceType.Pap => ResourceTypeFlag.Pap, + ResourceType.Pbd => ResourceTypeFlag.Pbd, + ResourceType.Pcb => ResourceTypeFlag.Pcb, + ResourceType.Phyb => ResourceTypeFlag.Phyb, + ResourceType.Plt => ResourceTypeFlag.Plt, + ResourceType.Scd => ResourceTypeFlag.Scd, + ResourceType.Sgb => ResourceTypeFlag.Sgb, + ResourceType.Shcd => ResourceTypeFlag.Shcd, + ResourceType.Shpk => ResourceTypeFlag.Shpk, + ResourceType.Sklb => ResourceTypeFlag.Sklb, + ResourceType.Skp => ResourceTypeFlag.Skp, + ResourceType.Stm => ResourceTypeFlag.Stm, + ResourceType.Svb => ResourceTypeFlag.Svb, + ResourceType.Tera => ResourceTypeFlag.Tera, + ResourceType.Tex => ResourceTypeFlag.Tex, + ResourceType.Tmb => ResourceTypeFlag.Tmb, + ResourceType.Ugd => ResourceTypeFlag.Ugd, + ResourceType.Uld => ResourceTypeFlag.Uld, + ResourceType.Waoe => ResourceTypeFlag.Waoe, + ResourceType.Wtd => ResourceTypeFlag.Wtd, + _ => 0, + }; - public static ResourceType FromString( string path ) + public static bool FitsFlag(this ResourceType type, ResourceTypeFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceCategoryFlag ToFlag(this ResourceCategory type) + => type switch + { + ResourceCategory.Common => ResourceCategoryFlag.Common, + ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, + ResourceCategory.Bg => ResourceCategoryFlag.Bg, + ResourceCategory.Cut => ResourceCategoryFlag.Cut, + ResourceCategory.Chara => ResourceCategoryFlag.Chara, + ResourceCategory.Shader => ResourceCategoryFlag.Shader, + ResourceCategory.Ui => ResourceCategoryFlag.Ui, + ResourceCategory.Sound => ResourceCategoryFlag.Sound, + ResourceCategory.Vfx => ResourceCategoryFlag.Vfx, + ResourceCategory.UiScript => ResourceCategoryFlag.UiScript, + ResourceCategory.Exd => ResourceCategoryFlag.Exd, + ResourceCategory.GameScript => ResourceCategoryFlag.GameScript, + ResourceCategory.Music => ResourceCategoryFlag.Music, + ResourceCategory.SqpackTest => ResourceCategoryFlag.SqpackTest, + _ => 0, + }; + + public static bool FitsFlag(this ResourceCategory type, ResourceCategoryFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceType FromBytes(byte a1, byte a2, byte a3) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 8) + | ByteStringFunctions.AsciiToLower(a3)); + + public static ResourceType FromBytes(byte a1, byte a2, byte a3, byte a4) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 24) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a3) << 8) + | ByteStringFunctions.AsciiToLower(a4)); + + public static ResourceType FromBytes(char a1, char a2, char a3) + => FromBytes((byte)a1, (byte)a2, (byte)a3); + + public static ResourceType FromBytes(char a1, char a2, char a3, char a4) + => FromBytes((byte)a1, (byte)a2, (byte)a3, (byte)a4); + + public static ResourceType Type(string path) { - var ext = Path.GetExtension( path.AsSpan() ); - ext = ext.Length == 0 ? path.AsSpan() : ext[ 1.. ]; + var ext = Path.GetExtension(path.AsSpan()); + ext = ext.Length == 0 ? path.AsSpan() : ext[1..]; return ext.Length switch { 0 => 0, - 1 => ( ResourceType )ext[ ^1 ], - 2 => FromBytes( '\0', ext[ ^2 ], ext[ ^1 ] ), - 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), - _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + 1 => (ResourceType)ext[^1], + 2 => FromBytes('\0', ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), }; } - public static ResourceType FromString( ByteString path ) + public static ResourceType Type(ByteString path) { - var extIdx = path.LastIndexOf( ( byte )'.' ); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring( extIdx + 1 ); + var extIdx = path.LastIndexOf((byte)'.'); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); return ext.Length switch { 0 => 0, - 1 => ( ResourceType )ext[ ^1 ], - 2 => FromBytes( 0, ext[ ^2 ], ext[ ^1 ] ), - 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), - _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + 1 => (ResourceType)ext[^1], + 2 => FromBytes(0, ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), }; } -} \ No newline at end of file + + public static ResourceCategory Category(ByteString path) + { + if (path.Length < 3) + return ResourceCategory.Debug; + + return ByteStringFunctions.AsciiToUpper(path[0]) switch + { + (byte)'C' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'O' => ResourceCategory.Common, + (byte)'U' => ResourceCategory.Cut, + (byte)'H' => ResourceCategory.Chara, + _ => ResourceCategory.Debug, + }, + (byte)'B' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'C' => ResourceCategory.BgCommon, + (byte)'/' => ResourceCategory.Bg, + _ => ResourceCategory.Debug, + }, + (byte)'S' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'H' => ResourceCategory.Shader, + (byte)'O' => ResourceCategory.Sound, + (byte)'Q' => ResourceCategory.SqpackTest, + _ => ResourceCategory.Debug, + }, + (byte)'U' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'/' => ResourceCategory.Ui, + (byte)'S' => ResourceCategory.UiScript, + _ => ResourceCategory.Debug, + }, + (byte)'V' => ResourceCategory.Vfx, + (byte)'E' => ResourceCategory.Exd, + (byte)'G' => ResourceCategory.GameScript, + (byte)'M' => ResourceCategory.Music, + _ => ResourceCategory.Debug, + }; + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 167be4f4..9b7e4c5e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -8,6 +8,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; +using Penumbra.GameData.Enums; using Penumbra.Import; using Penumbra.Mods; using Penumbra.UI; @@ -49,9 +50,15 @@ public partial class Configuration : IPluginConfiguration public int TutorialStep { get; set; } = 0; - public bool EnableFullResourceLogging { get; set; } = false; public bool EnableResourceLogging { get; set; } = false; public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; + + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + [JsonConverter( typeof( SortModeConverter ) )] [JsonProperty( Order = int.MaxValue )] diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fd988889..8c6b153e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -87,6 +87,7 @@ public class Penumbra : IDalamudPlugin private readonly WindowSystem _windowSystem; private readonly Changelog _changelog; private readonly CommandHandler _commandHandler; + private readonly ResourceWatcher _resourceWatcher; internal WebServer? WebServer; @@ -129,6 +130,7 @@ public class Penumbra : IDalamudPlugin MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLoader.EnableHooks(); + _resourceWatcher = new ResourceWatcher( ResourceLoader ); ResourceLogger = new ResourceLogger( ResourceLoader ); ResidentResources = new ResidentResourceManager(); StartTimer.Measure( StartTimeType.Mods, () => @@ -167,11 +169,6 @@ public class Penumbra : IDalamudPlugin _configWindow.IsOpen = true; } - if( Config.EnableFullResourceLogging ) - { - ResourceLoader.EnableFullLogging(); - } - using( var tAPI = StartTimer.Measure( StartTimeType.Api ) ) { Api = new PenumbraApi( this ); @@ -208,7 +205,7 @@ public class Penumbra : IDalamudPlugin private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system, out Changelog changelog ) { using var tInterface = StartTimer.Measure( StartTimeType.Interface ); - cfg = new ConfigWindow( this ); + cfg = new ConfigWindow( this, _resourceWatcher ); btn = new LaunchButton( _configWindow ); system = new WindowSystem( Name ); changelog = ConfigWindow.CreateChangelog(); @@ -338,6 +335,7 @@ public class Penumbra : IDalamudPlugin CollectionManager?.Dispose(); PathResolver?.Dispose(); ResourceLogger?.Dispose(); + _resourceWatcher?.Dispose(); ResourceLoader?.Dispose(); GameEvents?.Dispose(); CharacterUtility?.Dispose(); @@ -374,7 +372,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); sb.Append( $"> **`Synchronous Load (Dalamud): `** {( Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); - sb.Append( $"> **`Logging: `** Full: {Config.EnableFullResourceLogging}, Resource: {Config.EnableResourceLogging}\n" ); + sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.AppendLine( "**Mods**" ); sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\n" ); diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings index 4e906820..d89860c0 100644 --- a/Penumbra/Penumbra.csproj.DotSettings +++ b/Penumbra/Penumbra.csproj.DotSettings @@ -1,4 +1,5 @@  True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index b4bd7dc8..810140a5 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -31,44 +31,13 @@ public partial class ConfigWindow + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", Penumbra.Config.KeepDefaultMetaChanges, v => Penumbra.Config.KeepDefaultMetaChanges = v ); DrawWaitForPluginsReflection(); - DrawRequestedResourceLogging(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); - DrawEnableFullResourceLoggingBox(); DrawReloadResourceButton(); DrawReloadFontsButton(); ImGui.NewLine(); } - // Sets the resource logger state when toggled, - // and the filter when entered. - private void DrawRequestedResourceLogging() - { - var tmp = Penumbra.Config.EnableResourceLogging; - if( ImGui.Checkbox( "##resourceLogging", ref tmp ) ) - { - _window._penumbra.ResourceLogger.SetState( tmp ); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Requested Resource Logging", "Log all game paths FFXIV requests to the plugin log.\n" - + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" - + "Red boundary indicates invalid regex." ); - - ImGui.SameLine(); - - // Red borders if the string is not a valid regex. - var tmpString = Penumbra.Config.ResourceLoggingFilter; - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, !_window._penumbra.ResourceLogger.ValidRegex ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, - !_window._penumbra.ResourceLogger.ValidRegex ); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) - { - _window._penumbra.ResourceLogger.SetFilter( tmpString ); - } - } - // Creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { @@ -93,30 +62,6 @@ public partial class ConfigWindow "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); } - // Should only be used for debugging. - private static void DrawEnableFullResourceLoggingBox() - { - var tmp = Penumbra.Config.EnableFullResourceLogging; - if( ImGui.Checkbox( "##fullLogging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) - { - if( tmp ) - { - Penumbra.ResourceLoader.EnableFullLogging(); - } - else - { - Penumbra.ResourceLoader.DisableFullLogging(); - } - - Penumbra.Config.EnableFullResourceLogging = tmp; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Full Resource Logging", - "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); - } - // Should only be used for debugging. private static void DrawEnableDebugModeBox() { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 99646b21..6aab4f88 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -20,14 +20,16 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly EffectiveTab _effectiveTab; private readonly DebugTab _debugTab; private readonly ResourceTab _resourceTab; + private readonly ResourceWatcher _resourceWatcher; public readonly ModEditWindow ModEditPopup = new(); - public ConfigWindow( Penumbra penumbra ) + public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) : base( GetLabel() ) { - _penumbra = penumbra; + _penumbra = penumbra; + _resourceWatcher = watcher; + _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _modPanel = new ModPanel( this ); _selector.SelectionChanged += _modPanel.OnSelectionChange; @@ -99,6 +101,7 @@ public sealed partial class ConfigWindow : Window, IDisposable _effectiveTab.Draw(); _debugTab.Draw(); _resourceTab.Draw(); + DrawResourceWatcher(); } } catch( Exception e ) @@ -160,4 +163,13 @@ public sealed partial class ConfigWindow : Window, IDisposable _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); _iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); } + + private void DrawResourceWatcher() + { + using var tab = ImRaii.TabItem( "Resource Logger" ); + if (tab) + { + _resourceWatcher.Draw(); + } + } } \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs new file mode 100644 index 00000000..3cc82173 --- /dev/null +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs @@ -0,0 +1,118 @@ +using System; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.UI; + +public partial class ResourceWatcher +{ + private unsafe struct Record + { + public DateTime Time; + public ByteString Path; + public ByteString OriginalPath; + public ModCollection? Collection; + public ResourceHandle* Handle; + public ResourceTypeFlag ResourceType; + public ResourceCategoryFlag Category; + public uint RefCount; + public RecordType RecordType; + public OptionalBool Synchronously; + public OptionalBool ReturnValue; + public OptionalBool CustomLoad; + + public static Record CreateRequest( ByteString path, bool sync ) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = null, + ResourceType = ResourceExtensions.Type( path ).ToFlag(), + Category = ResourceExtensions.Category( path ).ToFlag(), + RefCount = 0, + RecordType = RecordType.Request, + Synchronously = sync, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + }; + + public static Record CreateDefaultLoad( ByteString path, ResourceHandle* handle, ModCollection collection ) + { + path = path.IsOwned ? path : path.Clone(); + return new Record + { + Time = DateTime.UtcNow, + Path = path, + OriginalPath = path, + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = false, + }; + } + + public static Record CreateLoad( ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection ) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = true, + }; + + public static Record CreateDestruction( ResourceHandle* handle ) + { + var path = handle->FileName().Clone(); + return new Record + { + Time = DateTime.UtcNow, + Path = path, + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.Destruction, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + }; + } + + public static Record CreateFileLoad( ByteString path, ResourceHandle* handle, bool ret, bool custom ) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.FileLoad, + Synchronously = OptionalBool.Null, + ReturnValue = ret, + CustomLoad = custom, + }; + } +} \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs new file mode 100644 index 00000000..3b3fed73 --- /dev/null +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs @@ -0,0 +1,17 @@ +using System; + +namespace Penumbra.UI; + +public partial class ResourceWatcher +{ + [Flags] + public enum RecordType : byte + { + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, + } + + public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; +} \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs new file mode 100644 index 00000000..0f97d883 --- /dev/null +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Table; +using Penumbra.GameData.Enums; +using Penumbra.String; + +namespace Penumbra.UI; + +public partial class ResourceWatcher +{ + private sealed class Table : Table< Record > + { + private static readonly PathColumn Path = new() { Label = "Path" }; + private static readonly RecordTypeColumn RecordType = new() { Label = "Record" }; + private static readonly DateColumn Date = new() { Label = "Time" }; + + private static readonly CollectionColumn Coll = new() { Label = "Collection" }; + private static readonly CustomLoadColumn Custom = new() { Label = "Custom" }; + private static readonly SynchronousLoadColumn Sync = new() { Label = "Sync" }; + + private static readonly OriginalPathColumn Orig = new() { Label = "Original Path" }; + private static readonly ResourceCategoryColumn Cat = new() { Label = "Category" }; + private static readonly ResourceTypeColumn Type = new() { Label = "Type" }; + + private static readonly HandleColumn Handle = new() { Label = "Resource" }; + private static readonly RefCountColumn Ref = new() { Label = "#Ref" }; + + public Table( ICollection< Record > records ) + : base( "##records", records, Path, RecordType, Coll, Custom, Sync, Orig, Cat, Type, Handle, Ref, Date ) + { } + + public void Reset() + => FilterDirty = true; + + private sealed class PathColumn : ColumnString< Record > + { + public override float Width + => 300 * ImGuiHelpers.GlobalScale; + + public override string ToName( Record item ) + => item.Path.ToString(); + + public override int Compare( Record lhs, Record rhs ) + => lhs.Path.CompareTo( rhs.Path ); + + public override void DrawColumn( Record item, int _ ) + => DrawByteString( item.Path, 290 * ImGuiHelpers.GlobalScale ); + } + + private static unsafe void DrawByteString( ByteString path, float length ) + { + Vector2 vec; + ImGuiNative.igCalcTextSize( &vec, path.Path, path.Path + path.Length, 0, 0 ); + if( vec.X <= length ) + { + ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + } + else + { + var fileName = path.LastIndexOf( ( byte )'/' ); + ByteString shortPath; + if( fileName != -1 ) + { + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * ImGuiHelpers.GlobalScale ) ); + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + ImGui.TextUnformatted( FontAwesomeIcon.EllipsisH.ToIconString() ); + ImGui.SameLine(); + shortPath = path.Substring( fileName, path.Length - fileName ); + } + else + { + shortPath = path; + } + + ImGuiNative.igTextUnformatted( shortPath.Path, shortPath.Path + shortPath.Length ); + if( ImGui.IsItemClicked() ) + { + ImGuiNative.igSetClipboardText( path.Path ); + } + + if( ImGui.IsItemHovered() ) + { + ImGuiNative.igSetTooltip( path.Path ); + } + } + } + + private sealed class RecordTypeColumn : ColumnFlags< RecordType, Record > + { + public RecordTypeColumn() + => AllFlags = AllRecords; + + public override float Width + => 80 * ImGuiHelpers.GlobalScale; + + public override bool FilterFunc( Record item ) + => FilterValue.HasFlag( item.RecordType ); + + public override RecordType FilterValue + => Penumbra.Config.ResourceWatcherRecordTypes; + + protected override void SetValue( RecordType value, bool enable ) + { + if( enable ) + { + Penumbra.Config.ResourceWatcherRecordTypes |= value; + } + else + { + Penumbra.Config.ResourceWatcherRecordTypes &= ~value; + } + + Penumbra.Config.Save(); + } + + public override void DrawColumn( Record item, int idx ) + { + ImGui.TextUnformatted( item.RecordType switch + { + ResourceWatcher.RecordType.Request => "REQ", + ResourceWatcher.RecordType.ResourceLoad => "LOAD", + ResourceWatcher.RecordType.FileLoad => "FILE", + ResourceWatcher.RecordType.Destruction => "DEST", + _ => string.Empty, + } ); + } + } + + private sealed class DateColumn : Column< Record > + { + public override float Width + => 80 * ImGuiHelpers.GlobalScale; + + public override int Compare( Record lhs, Record rhs ) + => lhs.Time.CompareTo( rhs.Time ); + + public override void DrawColumn( Record item, int _ ) + => ImGui.TextUnformatted( $"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}" ); + } + + + private sealed class CollectionColumn : ColumnString< Record > + { + public override float Width + => 80 * ImGuiHelpers.GlobalScale; + + public override string ToName( Record item ) + => item.Collection?.Name ?? string.Empty; + } + + private sealed class OriginalPathColumn : ColumnString< Record > + { + public override float Width + => 200 * ImGuiHelpers.GlobalScale; + + public override string ToName( Record item ) + => item.OriginalPath.ToString(); + + public override int Compare( Record lhs, Record rhs ) + => lhs.OriginalPath.CompareTo( rhs.OriginalPath ); + + public override void DrawColumn( Record item, int _ ) + => DrawByteString( item.OriginalPath, 190 * ImGuiHelpers.GlobalScale ); + } + + private sealed class ResourceCategoryColumn : ColumnFlags< ResourceCategoryFlag, Record > + { + public ResourceCategoryColumn() + => AllFlags = ResourceExtensions.AllResourceCategories; + + public override float Width + => 80 * ImGuiHelpers.GlobalScale; + + public override bool FilterFunc( Record item ) + => FilterValue.HasFlag( item.Category ); + + public override ResourceCategoryFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceCategories; + + protected override void SetValue( ResourceCategoryFlag value, bool enable ) + { + if( enable ) + { + Penumbra.Config.ResourceWatcherResourceCategories |= value; + } + else + { + Penumbra.Config.ResourceWatcherResourceCategories &= ~value; + } + + Penumbra.Config.Save(); + } + + public override void DrawColumn( Record item, int idx ) + { + ImGui.TextUnformatted( item.Category.ToString() ); + } + } + + private sealed class ResourceTypeColumn : ColumnFlags< ResourceTypeFlag, Record > + { + public ResourceTypeColumn() + { + AllFlags = Enum.GetValues< ResourceTypeFlag >().Aggregate( ( v, f ) => v | f ); + for( var i = 0; i < Names.Length; ++i ) + { + Names[ i ] = Names[ i ].ToLowerInvariant(); + } + } + + public override float Width + => 50 * ImGuiHelpers.GlobalScale; + + public override bool FilterFunc( Record item ) + => FilterValue.HasFlag( item.ResourceType ); + + public override ResourceTypeFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceTypes; + + protected override void SetValue( ResourceTypeFlag value, bool enable ) + { + if( enable ) + { + Penumbra.Config.ResourceWatcherResourceTypes |= value; + } + else + { + Penumbra.Config.ResourceWatcherResourceTypes &= ~value; + } + + Penumbra.Config.Save(); + } + + public override void DrawColumn( Record item, int idx ) + { + ImGui.TextUnformatted( item.ResourceType.ToString().ToLowerInvariant() ); + } + } + + private sealed class HandleColumn : ColumnString< Record > + { + public override float Width + => 120 * ImGuiHelpers.GlobalScale; + + public override unsafe string ToName( Record item ) + => item.Handle == null ? string.Empty : $"0x{( ulong )item.Handle:X}"; + + public override unsafe void DrawColumn( Record item, int _ ) + { + using var font = ImRaii.PushFont( UiBuilder.MonoFont, item.Handle != null ); + ImGuiUtil.RightAlign( ToName( item ) ); + } + } + + [Flags] + private enum BoolEnum : byte + { + True = 0x01, + False = 0x02, + Unknown = 0x04, + } + + private class OptBoolColumn : ColumnFlags< BoolEnum, Record > + { + private BoolEnum _filter; + + public OptBoolColumn() + { + AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; + _filter = AllFlags; + Flags &= ~ImGuiTableColumnFlags.NoSort; + } + + protected bool FilterFunc( OptionalBool b ) + => b.Value switch + { + null => _filter.HasFlag( BoolEnum.Unknown ), + true => _filter.HasFlag( BoolEnum.True ), + false => _filter.HasFlag( BoolEnum.False ), + }; + + public override BoolEnum FilterValue + => _filter; + + protected override void SetValue( BoolEnum value, bool enable ) + { + if( enable ) + { + _filter |= value; + } + else + { + _filter &= ~value; + } + } + + protected static void DrawColumn( OptionalBool b ) + { + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + ImGui.TextUnformatted( b.Value switch + { + null => string.Empty, + true => FontAwesomeIcon.Check.ToIconString(), + false => FontAwesomeIcon.Times.ToIconString(), + } ); + } + } + + private sealed class CustomLoadColumn : OptBoolColumn + { + public override float Width + => 60 * ImGuiHelpers.GlobalScale; + + public override bool FilterFunc( Record item ) + => FilterFunc( item.CustomLoad ); + + public override void DrawColumn( Record item, int idx ) + => DrawColumn( item.CustomLoad ); + } + + private sealed class SynchronousLoadColumn : OptBoolColumn + { + public override float Width + => 45 * ImGuiHelpers.GlobalScale; + + public override bool FilterFunc( Record item ) + => FilterFunc( item.Synchronously ); + + public override void DrawColumn( Record item, int idx ) + => DrawColumn( item.Synchronously ); + } + + private sealed class RefCountColumn : Column< Record > + { + public override float Width + => 30 * ImGuiHelpers.GlobalScale; + + public override void DrawColumn( Record item, int _ ) + => ImGuiUtil.RightAlign( item.RefCount.ToString() ); + + public override int Compare( Record lhs, Record rhs ) + => lhs.RefCount.CompareTo( rhs.RefCount ); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs new file mode 100644 index 00000000..e180082f --- /dev/null +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ResourceWatcher : IDisposable +{ + public const int DefaultMaxEntries = 1024 * 1024; + + private readonly ResourceLoader _loader; + private readonly List< Record > _records = new(); + private readonly ConcurrentQueue< Record > _newRecords = new(); + private readonly Table _table; + private bool _writeToLog; + private bool _isEnabled; + private string _logFilter = string.Empty; + private Regex? _logRegex; + private int _maxEntries; + private int _newMaxEntries; + + public unsafe ResourceWatcher( ResourceLoader loader ) + { + _loader = loader; + _table = new Table( _records ); + _loader.ResourceRequested += OnResourceRequested; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.FileLoaded += OnFileLoaded; + UpdateFilter( Penumbra.Config.ResourceLoggingFilter, false ); + _writeToLog = Penumbra.Config.EnableResourceLogging; + _isEnabled = Penumbra.Config.EnableResourceWatcher; + _maxEntries = Penumbra.Config.MaxResourceWatcherRecords; + _newMaxEntries = _maxEntries; + } + + public unsafe void Dispose() + { + Clear(); + _records.TrimExcess(); + _loader.ResourceRequested -= OnResourceRequested; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.FileLoaded -= OnFileLoaded; + } + + private void Clear() + { + _records.Clear(); + _newRecords.Clear(); + _table.Reset(); + } + + public void Draw() + { + UpdateRecords(); + + ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); + if( ImGui.Checkbox( "Enable", ref _isEnabled ) ) + { + Penumbra.Config.EnableResourceWatcher = _isEnabled; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + DrawMaxEntries(); + ImGui.SameLine(); + if( ImGui.Button( "Clear" ) ) + { + Clear(); + } + + ImGui.SameLine(); + if( ImGui.Checkbox( "Write to Log", ref _writeToLog ) ) + { + Penumbra.Config.EnableResourceLogging = _writeToLog; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + DrawFilterInput(); + + ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); + + _table.Draw( ImGui.GetTextLineHeightWithSpacing() ); + } + + private void DrawFilterInput() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + var tmp = _logFilter; + var invalidRegex = _logRegex == null && _logFilter.Length > 0; + using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex ); + if( ImGui.InputTextWithHint( "##logFilter", "If path matches this Regex...", ref tmp, 256 ) ) + { + UpdateFilter( tmp, true ); + } + } + + private void UpdateFilter( string newString, bool config ) + { + if( newString == _logFilter ) + { + return; + } + + _logFilter = newString; + try + { + _logRegex = new Regex( _logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase ); + } + catch + { + _logRegex = null; + } + + if( config ) + { + Penumbra.Config.ResourceLoggingFilter = newString; + Penumbra.Config.Save(); + } + } + + private bool FilterMatch( ByteString path, out string match ) + { + match = path.ToString(); + return _logFilter.Length == 0 || ( _logRegex?.IsMatch( match ) ?? false ) || match.Contains( _logFilter, StringComparison.OrdinalIgnoreCase ); + } + + + private void DrawMaxEntries() + { + ImGui.SetNextItemWidth( 80 * ImGuiHelpers.GlobalScale ); + ImGui.InputInt( "Max. Entries", ref _newMaxEntries, 0, 0 ); + var change = ImGui.IsItemDeactivatedAfterEdit(); + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) + { + change = true; + _newMaxEntries = DefaultMaxEntries; + } + + if( _maxEntries != DefaultMaxEntries && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( $"CTRL + Right-Click to reset to default {DefaultMaxEntries}." ); + } + + if( !change ) + { + return; + } + + _newMaxEntries = Math.Max( 16, _newMaxEntries ); + if( _newMaxEntries != _maxEntries ) + { + _maxEntries = _newMaxEntries; + Penumbra.Config.MaxResourceWatcherRecords = _maxEntries; + Penumbra.Config.Save(); + _records.RemoveRange( 0, _records.Count - _maxEntries ); + } + } + + private void UpdateRecords() + { + var count = _newRecords.Count; + if( count > 0 ) + { + while( _newRecords.TryDequeue( out var rec ) && count-- > 0 ) + { + _records.Add( rec ); + } + + if( _records.Count > _maxEntries ) + { + _records.RemoveRange( 0, _records.Count - _maxEntries ); + } + + _table.Reset(); + } + } + + + private void OnResourceRequested( Utf8GamePath data, bool synchronous ) + { + if( _writeToLog && FilterMatch( data.Path, out var match ) ) + { + Penumbra.Log.Information( $"[ResourceLoader] [REQ] {match} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); + } + + if( _isEnabled ) + { + _newRecords.Enqueue( Record.CreateRequest( data.Path, synchronous ) ); + } + } + + private unsafe void OnResourceLoaded( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) + { + if( _writeToLog ) + { + var log = FilterMatch( path.Path, out var name ); + var name2 = string.Empty; + if( manipulatedPath != null ) + { + log |= FilterMatch( manipulatedPath.Value.InternalName, out name2 ); + } + + if( log ) + { + var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; + Penumbra.Log.Information( + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); + } + } + + if( _isEnabled ) + { + var record = manipulatedPath == null + ? Record.CreateDefaultLoad( path.Path, handle, data.ModCollection ) + : Record.CreateLoad( path.Path, manipulatedPath.Value.InternalName, handle, data.ModCollection ); + _newRecords.Enqueue( record ); + } + } + + private unsafe void OnFileLoaded( ResourceHandle* resource, ByteString path, bool success, bool custom ) + { + if( _writeToLog && FilterMatch( path, out var match ) ) + { + Penumbra.Log.Information( + $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); + } + + if( _isEnabled ) + { + _newRecords.Enqueue( Record.CreateFileLoad( path, resource, success, custom ) ); + } + } +} \ No newline at end of file From 579a9edca157b6bdb692bd9d9cf231d07947d05b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Feb 2023 17:57:33 +0100 Subject: [PATCH 0753/2451] Fix culture. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index fb6526d0..f033fc9c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fb6526d0c034e97ee079ee88ca931e536f99c5a7 +Subproject commit f033fc9c103b8a07398481cbff00b0ad3ea749e2 From 738d62757c49f0f0270fc71dc94d7fc89b797db5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Feb 2023 00:14:09 +0100 Subject: [PATCH 0754/2451] Fix ascii check in RelPath. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 3276f379..574fd9f8 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 3276f37974e668b1e4769c176c114da17611b734 +Subproject commit 574fd9f8bb7d957457775a698f5e29a246fab8bd From 30fba90e9fbc85f3f794feb645021d6b85f14301 Mon Sep 17 00:00:00 2001 From: Sebastian Lawe Date: Fri, 17 Feb 2023 17:33:41 -0600 Subject: [PATCH 0755/2451] Add ReloadController.cs allows the discovery and reloading of a specified mod via HTTP API --- Penumbra/Api/ReloadController.cs | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Penumbra/Api/ReloadController.cs diff --git a/Penumbra/Api/ReloadController.cs b/Penumbra/Api/ReloadController.cs new file mode 100644 index 00000000..0f8fe19e --- /dev/null +++ b/Penumbra/Api/ReloadController.cs @@ -0,0 +1,34 @@ +using EmbedIO.Routing; +using EmbedIO; +using EmbedIO.WebApi; +using Penumbra.Api.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace Penumbra.Api { + public class ReloadController : WebApiController { + private readonly Penumbra _penumbra; + public ReloadController( Penumbra penumbra ) + => _penumbra = penumbra; + + [Route( HttpVerbs.Post, "/reload" )] + public async Task Reload() { + var data = await HttpContext.GetRequestDataAsync(); + if( !string.IsNullOrEmpty( data.ModPath ) && !string.IsNullOrEmpty( data.ModName ) ) { + if(Directory.Exists( data.ModPath ) ) { + _penumbra.Api.AddMod( data.ModPath ); + } + _penumbra.Api.ReloadMod( data.ModPath, data.ModName ); + } + } + + public class ReloadData { + public string ModPath { get; set; } = string.Empty; + public string ModName { get; set; } = string.Empty; + } + } +} From 0a47ae5b188d49f7757da65934490cd8686b428f Mon Sep 17 00:00:00 2001 From: Sebastian Lawe Date: Fri, 17 Feb 2023 19:36:24 -0600 Subject: [PATCH 0756/2451] Update Penumbra.cs add ReloadController.cs to HTTP API --- Penumbra/Penumbra.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8c6b153e..ad7c323e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -302,7 +302,8 @@ public class Penumbra : IDalamudPlugin .WithCors( prefix ) .WithWebApi( "/api", m => m .WithController( () => new ModsController( this ) ) - .WithController( () => new RedrawController( this ) ) ); + .WithController( () => new RedrawController( this ) ) + .WithController( () => new ReloadController( this ) ) ); WebServer.StateChanged += ( _, e ) => Log.Information( $"WebServer New State - {e.NewState}" ); From 7ee80c7d487bf35e94bde8ef6a23f5afd4d8739f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 Feb 2023 16:30:09 +0100 Subject: [PATCH 0757/2451] Cleanup HTTP API, remove unused options. --- Penumbra/Api/HttpApi.cs | 128 ++++++++++++++++++ Penumbra/Api/ModsController.cs | 41 ------ Penumbra/Api/RedrawController.cs | 46 ------- Penumbra/Api/ReloadController.cs | 34 ----- Penumbra/Penumbra.cs | 52 ++----- Penumbra/UI/ConfigWindow.DebugTab.cs | 2 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 7 +- 7 files changed, 140 insertions(+), 170 deletions(-) create mode 100644 Penumbra/Api/HttpApi.cs delete mode 100644 Penumbra/Api/ModsController.cs delete mode 100644 Penumbra/Api/RedrawController.cs delete mode 100644 Penumbra/Api/ReloadController.cs diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs new file mode 100644 index 00000000..ee87d026 --- /dev/null +++ b/Penumbra/Api/HttpApi.cs @@ -0,0 +1,128 @@ +using System; +using System.Threading.Tasks; +using EmbedIO; +using EmbedIO.Routing; +using EmbedIO.WebApi; +using Penumbra.Api.Enums; + +namespace Penumbra.Api; + +public class HttpApi : IDisposable +{ + private partial class Controller : WebApiController + { + // @formatter:off + [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); + [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); + [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); + [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); + // @formatter:on + } + + public const string Prefix = "http://localhost:42069/"; + + private readonly IPenumbraApi _api; + private WebServer? _server; + + public HttpApi( IPenumbraApi api ) + { + _api = api; + if( Penumbra.Config.EnableHttpApi ) + { + CreateWebServer(); + } + } + + public bool Enabled + => _server != null; + + public void CreateWebServer() + { + ShutdownWebServer(); + + _server = new WebServer( o => o + .WithUrlPrefix( Prefix ) + .WithMode( HttpListenerMode.EmbedIO ) ) + .WithCors( Prefix ) + .WithWebApi( "/api", m => m.WithController( () => new Controller( _api ) ) ); + + _server.StateChanged += ( _, e ) => Penumbra.Log.Information( $"WebServer New State - {e.NewState}" ); + _server.RunAsync(); + } + + public void ShutdownWebServer() + { + _server?.Dispose(); + _server = null; + } + + public void Dispose() + => ShutdownWebServer(); + + private partial class Controller + { + private readonly IPenumbraApi _api; + + public Controller( IPenumbraApi api ) + => _api = api; + + public partial object? GetMods() + { + Penumbra.Log.Debug( $"[HTTP] {nameof( GetMods )} triggered." ); + return _api.GetModList(); + } + + public async partial Task Redraw() + { + var data = await HttpContext.GetRequestDataAsync< RedrawData >(); + Penumbra.Log.Debug( $"[HTTP] {nameof( Redraw )} triggered with {data}." ); + if( data.ObjectTableIndex >= 0 ) + { + _api.RedrawObject( data.ObjectTableIndex, data.Type ); + } + else if( data.Name.Length > 0 ) + { + _api.RedrawObject( data.Name, data.Type ); + } + else + { + _api.RedrawAll( data.Type ); + } + } + + public partial void RedrawAll() + { + Penumbra.Log.Debug( $"[HTTP] {nameof( RedrawAll )} triggered." ); + _api.RedrawAll( RedrawType.Redraw ); + } + + public async partial Task ReloadMod() + { + var data = await HttpContext.GetRequestDataAsync< ModReloadData >(); + Penumbra.Log.Debug( $"[HTTP] {nameof( ReloadMod )} triggered with {data}." ); + // Add the mod if it is not already loaded and if the directory name is given. + // AddMod returns Success if the mod is already loaded. + if( data.Path.Length != 0 ) + { + _api.AddMod( data.Path ); + } + + // Reload the mod by path or name, which will also remove no-longer existing mods. + _api.ReloadMod( data.Path, data.Name ); + } + + private record ModReloadData( string Path, string Name ) + { + public ModReloadData() + : this( string.Empty, string.Empty ) + { } + } + + private record RedrawData( string Name, RedrawType Type, int ObjectTableIndex ) + { + public RedrawData() + : this( string.Empty, RedrawType.Redraw, -1 ) + { } + } + } +} \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs deleted file mode 100644 index 3432fb86..00000000 --- a/Penumbra/Api/ModsController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using EmbedIO; -using EmbedIO.Routing; -using EmbedIO.WebApi; -using System.Linq; - -namespace Penumbra.Api; - -public class ModsController : WebApiController -{ - private readonly Penumbra _penumbra; - - public ModsController( Penumbra penumbra ) - => _penumbra = penumbra; - - [Route( HttpVerbs.Get, "/mods" )] - public object? GetMods() - { - return Penumbra.ModManager.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new - { - x.Second?.Enabled, - x.Second?.Priority, - FolderName = x.First.ModPath.Name, - x.First.Name, - BasePath = x.First.ModPath.FullName, - Files = x.First.AllFiles, - } ); - } - - [Route( HttpVerbs.Post, "/mods" )] - public object CreateMod() - => new { }; - - [Route( HttpVerbs.Get, "/files" )] - public object GetFiles() - { - return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( - o => o.Key.ToString(), - o => o.Value.Path.FullName - ); - } -} \ No newline at end of file diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs deleted file mode 100644 index ad37c587..00000000 --- a/Penumbra/Api/RedrawController.cs +++ /dev/null @@ -1,46 +0,0 @@ -using EmbedIO; -using EmbedIO.Routing; -using EmbedIO.WebApi; -using System.Threading.Tasks; -using Penumbra.Api.Enums; - -namespace Penumbra.Api; - -public class RedrawController : WebApiController -{ - private readonly Penumbra _penumbra; - - public RedrawController( Penumbra penumbra ) - => _penumbra = penumbra; - - [Route( HttpVerbs.Post, "/redraw" )] - public async Task Redraw() - { - var data = await HttpContext.GetRequestDataAsync< RedrawData >(); - 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" )] - public void RedrawAll() - { - _penumbra.Api.RedrawAll( RedrawType.Redraw ); - } - - public class RedrawData - { - 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/Api/ReloadController.cs b/Penumbra/Api/ReloadController.cs deleted file mode 100644 index 0f8fe19e..00000000 --- a/Penumbra/Api/ReloadController.cs +++ /dev/null @@ -1,34 +0,0 @@ -using EmbedIO.Routing; -using EmbedIO; -using EmbedIO.WebApi; -using Penumbra.Api.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.IO; - -namespace Penumbra.Api { - public class ReloadController : WebApiController { - private readonly Penumbra _penumbra; - public ReloadController( Penumbra penumbra ) - => _penumbra = penumbra; - - [Route( HttpVerbs.Post, "/reload" )] - public async Task Reload() { - var data = await HttpContext.GetRequestDataAsync(); - if( !string.IsNullOrEmpty( data.ModPath ) && !string.IsNullOrEmpty( data.ModName ) ) { - if(Directory.Exists( data.ModPath ) ) { - _penumbra.Api.AddMod( data.ModPath ); - } - _penumbra.Api.ReloadMod( data.ModPath, data.ModName ); - } - } - - public class ReloadData { - public string ModPath { get; set; } = string.Empty; - public string ModName { get; set; } = string.Empty; - } - } -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ad7c323e..6a3c2ae4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -6,8 +6,6 @@ using System.Reflection; using System.Text; using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using EmbedIO; -using EmbedIO.WebApi; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; @@ -81,6 +79,7 @@ public class Penumbra : IDalamudPlugin public readonly ObjectReloader ObjectReloader; public readonly ModFileSystem ModFileSystem; public readonly PenumbraApi Api; + public readonly HttpApi HttpApi; public readonly PenumbraIpcProviders IpcProviders; private readonly ConfigWindow _configWindow; private readonly LaunchButton _launchButton; @@ -89,14 +88,6 @@ public class Penumbra : IDalamudPlugin private readonly CommandHandler _commandHandler; private readonly ResourceWatcher _resourceWatcher; - internal WebServer? WebServer; - - private static void Timed( StartTimeType type, Action action ) - { - using var t = StartTimer.Measure( type ); - action(); - } - public Penumbra( DalamudPluginInterface pluginInterface ) { using var time = StartTimer.Measure( StartTimeType.Total ); @@ -158,21 +149,22 @@ public class Penumbra : IDalamudPlugin PathResolver.Enable(); } - if( Config.EnableHttpApi ) - { - CreateWebServer(); - } - if( Config.DebugMode ) { ResourceLoader.EnableDebug(); _configWindow.IsOpen = true; } - using( var tAPI = StartTimer.Measure( StartTimeType.Api ) ) + using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) { Api = new PenumbraApi( this ); IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); + HttpApi = new HttpApi( Api ); + if( Config.EnableHttpApi ) + { + HttpApi.CreateWebServer(); + } + SubscribeItemLinks(); } @@ -290,38 +282,12 @@ public class Penumbra : IDalamudPlugin }; } - public void CreateWebServer() - { - const string prefix = "http://localhost:42069/"; - - ShutdownWebServer(); - - WebServer = new WebServer( o => o - .WithUrlPrefix( prefix ) - .WithMode( HttpListenerMode.EmbedIO ) ) - .WithCors( prefix ) - .WithWebApi( "/api", m => m - .WithController( () => new ModsController( this ) ) - .WithController( () => new RedrawController( this ) ) - .WithController( () => new ReloadController( this ) ) ); - - WebServer.StateChanged += ( _, e ) => Log.Information( $"WebServer New State - {e.NewState}" ); - - WebServer.RunAsync(); - } - - public void ShutdownWebServer() - { - WebServer?.Dispose(); - WebServer = null; - } - private short ResolveCutscene( ushort index ) => ( short )PathResolver.CutsceneActor( index ); public void Dispose() { - ShutdownWebServer(); + HttpApi?.Dispose(); IpcProviders?.Dispose(); Api?.Dispose(); _commandHandler?.Dispose(); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index bc8499e1..39df2f3e 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -109,7 +109,7 @@ public partial class ConfigWindow PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); PrintValue( "Path Resolver Enabled", _window._penumbra.PathResolver.Enabled.ToString() ); - PrintValue( "Web Server Enabled", ( _window._penumbra.WebServer != null ).ToString() ); + PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); } private static void DrawPerformanceTab() diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 810140a5..f05c52cb 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -1,11 +1,8 @@ using System.Numerics; -using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Interop; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -46,11 +43,11 @@ public partial class ConfigWindow { if( http ) { - _window._penumbra.CreateWebServer(); + _window._penumbra.HttpApi.CreateWebServer(); } else { - _window._penumbra.ShutdownWebServer(); + _window._penumbra.HttpApi.ShutdownWebServer(); } Penumbra.Config.EnableHttpApi = http; From 0c17892f03f5b59071763a9ba52bd09111c96c0b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 15 Feb 2023 02:07:10 +0100 Subject: [PATCH 0758/2451] Mtrl shader resource editing, ShPk editing --- Penumbra.GameData/Data/DisassembledShader.cs | 481 +++++++++++++ Penumbra.GameData/Files/MtrlFile.Write.cs | 3 +- Penumbra.GameData/Files/MtrlFile.cs | 57 +- Penumbra.GameData/Files/ShpkFile.Write.cs | 123 ++++ Penumbra.GameData/Files/ShpkFile.cs | 644 ++++++++++++++++++ Penumbra.GameData/Interop/D3DCompiler.cs | 65 ++ Penumbra/Mods/Editor/Mod.Editor.Files.cs | 5 + .../UI/Classes/ModEditWindow.FileEditor.cs | 14 +- .../UI/Classes/ModEditWindow.Materials.cs | 484 +++++++++++-- .../Classes/ModEditWindow.ShaderPackages.cs | 557 +++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 55 +- Penumbra/Util/IndexSet.cs | 110 +++ 12 files changed, 2535 insertions(+), 63 deletions(-) create mode 100644 Penumbra.GameData/Data/DisassembledShader.cs create mode 100644 Penumbra.GameData/Files/ShpkFile.Write.cs create mode 100644 Penumbra.GameData/Files/ShpkFile.cs create mode 100644 Penumbra.GameData/Interop/D3DCompiler.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs create mode 100644 Penumbra/Util/IndexSet.cs diff --git a/Penumbra.GameData/Data/DisassembledShader.cs b/Penumbra.GameData/Data/DisassembledShader.cs new file mode 100644 index 00000000..b782bf74 --- /dev/null +++ b/Penumbra.GameData/Data/DisassembledShader.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Penumbra.GameData.Interop; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.GameData.Data; + +public class DisassembledShader +{ + public struct ResourceBinding + { + public string Name; + public ResourceType Type; + public Format Format; + public ResourceDimension Dimension; + public uint Slot; + public uint Elements; + public uint RegisterCount; + public VectorComponents[] Used; + public VectorComponents UsedDynamically; + } + + // Abbreviated using the uppercased first char of their name + public enum ResourceType : byte + { + Unspecified = 0, + ConstantBuffer = 0x43, // 'C' + Sampler = 0x53, // 'S' + Texture = 0x54, // 'T' + UAV = 0x55, // 'U' + } + + // Abbreviated using the uppercased first and last char of their name + public enum Format : ushort + { + Unspecified = 0, + NotApplicable = 0x4E41, // 'NA' + Int = 0x4954, // 'IT' + Int4 = 0x4934, // 'I4' + Float = 0x4654, // 'FT' + Float4 = 0x4634, // 'F4' + } + + // Abbreviated using the uppercased first and last char of their name + public enum ResourceDimension : ushort + { + Unspecified = 0, + NotApplicable = 0x4E41, // 'NA' + TwoD = 0x3244, // '2D' + ThreeD = 0x3344, // '3D' + Cube = 0x4345, // 'CE' + } + + public struct InputOutput + { + public string Name; + public uint Index; + public VectorComponents Mask; + public uint Register; + public string SystemValue; + public Format Format; + public VectorComponents Used; + } + + [Flags] + public enum VectorComponents : byte + { + X = 1, + Y = 2, + Z = 4, + W = 8, + All = 15, + } + + public enum ShaderStage : byte + { + Unspecified = 0, + Pixel = 0x50, // 'P' + Vertex = 0x56, // 'V' + } + + private static readonly Regex ResourceBindingSizeRegex = new(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking); + private static readonly Regex SM3ConstantBufferUsageRegex = new(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking); + private static readonly Regex SM3TextureUsageRegex = new(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking); + private static readonly Regex SM5ConstantBufferUsageRegex = new(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking); + private static readonly Regex SM5TextureUsageRegex = new(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking); + private static readonly char[] Digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + + public readonly string RawDisassembly; + public readonly uint ShaderModel; + public readonly ShaderStage Stage; + public readonly string BufferDefinitions; + public readonly ResourceBinding[] ResourceBindings; + public readonly InputOutput[] InputSignature; + public readonly InputOutput[] OutputSignature; + public readonly string[] Instructions; + + public DisassembledShader(string rawDisassembly) + { + RawDisassembly = rawDisassembly; + var lines = rawDisassembly.Split('\n'); + Instructions = Array.FindAll(lines, ln => !ln.StartsWith("//") && ln.Length > 0); + var shaderModel = Instructions[0].Trim().Split('_'); + Stage = (ShaderStage)(byte)char.ToUpper(shaderModel[0][0]); + ShaderModel = (uint.Parse(shaderModel[1]) << 8) | uint.Parse(shaderModel[2]); + var header = PreParseHeader(lines.AsSpan()[..Array.IndexOf(lines, Instructions[0])]); + switch (ShaderModel >> 8) + { + case 3: + ParseSM3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); + ParseSM3ResourceUsage(Instructions, ResourceBindings); + break; + case 5: + ParseSM5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); + ParseSM5ResourceUsage(Instructions, ResourceBindings); + break; + default: + throw new NotImplementedException(); + } + } + + public ResourceBinding? GetResourceBindingByName(ResourceType type, string name) + { + return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Name == name); + } + + public ResourceBinding? GetResourceBindingBySlot(ResourceType type, uint slot) + { + return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Slot == slot); + } + + public static DisassembledShader Disassemble(ReadOnlySpan shaderBlob) + { + return new DisassembledShader(D3DCompiler.Disassemble(shaderBlob)); + } + + private static void ParseSM3Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) + { + if (header.TryGetValue("Parameters", out var rawParameters)) + { + bufferDefinitions = string.Join('\n', rawParameters); + } + else + { + bufferDefinitions = string.Empty; + } + if (header.TryGetValue("Registers", out var rawRegisters)) + { + var (_, registers) = ParseTable(rawRegisters); + resourceBindings = Array.ConvertAll(registers, register => + { + var type = (ResourceType)(byte)char.ToUpper(register[1][0]); + if (type == ResourceType.Sampler) + { + type = ResourceType.Texture; + } + uint size = uint.Parse(register[2]); + return new ResourceBinding + { + Name = register[0], + Type = type, + Format = Format.Unspecified, + Dimension = ResourceDimension.Unspecified, + Slot = uint.Parse(register[1][1..]), + Elements = 1, + RegisterCount = size, + Used = new VectorComponents[size], + }; + }); + } + else + { + resourceBindings = Array.Empty(); + } + inputSignature = Array.Empty(); + outputSignature = Array.Empty(); + } + + private static void ParseSM3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + { + var cbIndices = new Dictionary(); + var tIndices = new Dictionary(); + { + var i = 0; + foreach (var binding in resourceBindings) + { + switch (binding.Type) + { + case ResourceType.ConstantBuffer: + for (var j = 0u; j < binding.RegisterCount; j++) + { + cbIndices[binding.Slot + j] = i; + } + break; + case ResourceType.Texture: + tIndices[binding.Slot] = i; + break; + } + ++i; + } + } + foreach (var instruction in instructions) + { + var trimmed = instruction.Trim(); + if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + { + continue; + } + foreach (Match cbMatch in SM3ConstantBufferUsageRegex.Matches(instruction)) + { + var buffer = uint.Parse(cbMatch.Groups[1].Value); + if (cbIndices.TryGetValue(buffer, out var i)) + { + var swizzle = cbMatch.Groups[3].Success ? ParseVectorComponents(cbMatch.Groups[3].Value) : VectorComponents.All; + if (cbMatch.Groups[2].Success) + { + resourceBindings[i].UsedDynamically |= swizzle; + } + else + { + resourceBindings[i].Used[buffer - resourceBindings[i].Slot] |= swizzle; + } + } + } + var tMatch = SM3TextureUsageRegex.Match(instruction); + if (tMatch.Success) + { + var texture = uint.Parse(tMatch.Groups[1].Value); + if (tIndices.TryGetValue(texture, out var i)) + { + resourceBindings[i].Used[0] = VectorComponents.All; + } + } + } + } + + private static void ParseSM5Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) + { + if (header.TryGetValue("Resource Bindings", out var rawResBindings)) + { + var (head, resBindings) = ParseTable(rawResBindings); + resourceBindings = Array.ConvertAll(resBindings, binding => { + var type = (ResourceType)(byte)char.ToUpper(binding[1][0]); + return new ResourceBinding + { + Name = binding[0], + Type = type, + Format = (Format)(((byte)char.ToUpper(binding[2][0]) << 8) | (byte)char.ToUpper(binding[2][^1])), + Dimension = (ResourceDimension)(((byte)char.ToUpper(binding[3][0]) << 8) | (byte)char.ToUpper(binding[3][^1])), + Slot = uint.Parse(binding[4][binding[4].IndexOfAny(Digits)..]), + Elements = uint.Parse(binding[5]), + RegisterCount = type == ResourceType.Texture ? 1u : 0u, + Used = type == ResourceType.Texture ? new VectorComponents[1] : Array.Empty(), + }; + }); + } + else + { + resourceBindings = Array.Empty(); + } + if (header.TryGetValue("Buffer Definitions", out var rawBufferDefs)) + { + bufferDefinitions = string.Join('\n', rawBufferDefs); + foreach (Match match in ResourceBindingSizeRegex.Matches(bufferDefinitions)) + { + var name = match.Groups[1].Value; + var bytesSize = uint.Parse(match.Groups[2].Value); + var pos = Array.FindIndex(resourceBindings, binding => binding.Type == ResourceType.ConstantBuffer && binding.Name == name); + if (pos >= 0) + { + resourceBindings[pos].RegisterCount = (bytesSize + 0xF) >> 4; + resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount]; + } + } + } + else + { + bufferDefinitions = string.Empty; + } + + static InputOutput ParseInputOutput(string[] inOut) => new() + { + Name = inOut[0], + Index = uint.Parse(inOut[1]), + Mask = ParseVectorComponents(inOut[2]), + Register = uint.Parse(inOut[3]), + SystemValue = string.Intern(inOut[4]), + Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])), + Used = ParseVectorComponents(inOut[6]), + }; + + if (header.TryGetValue("Input signature", out var rawInputSig)) + { + var (_, inputSig) = ParseTable(rawInputSig); + inputSignature = Array.ConvertAll(inputSig, ParseInputOutput); + } + else + { + inputSignature = Array.Empty(); + } + if (header.TryGetValue("Output signature", out var rawOutputSig)) + { + var (_, outputSig) = ParseTable(rawOutputSig); + outputSignature = Array.ConvertAll(outputSig, ParseInputOutput); + } + else + { + outputSignature = Array.Empty(); + } + } + + private static void ParseSM5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + { + var cbIndices = new Dictionary(); + var tIndices = new Dictionary(); + { + var i = 0; + foreach (var binding in resourceBindings) + { + switch (binding.Type) + { + case ResourceType.ConstantBuffer: + cbIndices[binding.Slot] = i; + break; + case ResourceType.Texture: + tIndices[binding.Slot] = i; + break; + } + ++i; + } + } + foreach (var instruction in instructions) + { + var trimmed = instruction.Trim(); + if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + { + continue; + } + foreach (Match cbMatch in SM5ConstantBufferUsageRegex.Matches(instruction)) + { + var buffer = uint.Parse(cbMatch.Groups[1].Value); + if (cbIndices.TryGetValue(buffer, out var i)) + { + var swizzle = ParseVectorComponents(cbMatch.Groups[3].Value); + if (int.TryParse(cbMatch.Groups[2].Value, out var vector)) + { + if (vector < resourceBindings[i].Used.Length) + { + resourceBindings[i].Used[vector] |= swizzle; + } + } + else + { + resourceBindings[i].UsedDynamically |= swizzle; + } + } + } + var tMatch = SM5TextureUsageRegex.Match(instruction); + if (tMatch.Success) + { + var texture = uint.Parse(tMatch.Groups[2].Value); + if (tIndices.TryGetValue(texture, out var i)) + { + var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value); + var rawInSwizzle = tMatch.Groups[3].Value; + var inSwizzle = new StringBuilder(4); + if ((outSwizzle & VectorComponents.X) != 0) + { + inSwizzle.Append(rawInSwizzle[0]); + } + if ((outSwizzle & VectorComponents.Y) != 0) + { + inSwizzle.Append(rawInSwizzle[1]); + } + if ((outSwizzle & VectorComponents.Z) != 0) + { + inSwizzle.Append(rawInSwizzle[2]); + } + if ((outSwizzle & VectorComponents.W) != 0) + { + inSwizzle.Append(rawInSwizzle[3]); + } + resourceBindings[i].Used[0] |= ParseVectorComponents(inSwizzle.ToString()); + } + } + } + } + + private static VectorComponents ParseVectorComponents(string components) + { + components = components.ToUpperInvariant(); + return (components.Contains('X') ? VectorComponents.X : 0) + | (components.Contains('Y') ? VectorComponents.Y : 0) + | (components.Contains('Z') ? VectorComponents.Z : 0) + | (components.Contains('W') ? VectorComponents.W : 0); + } + + private static Dictionary PreParseHeader(ReadOnlySpan header) + { + var sections = new Dictionary(); + + void AddSection(string name, ReadOnlySpan section) + { + while (section.Length > 0 && section[0].Length <= 3) + { + section = section[1..]; + } + while (section.Length > 0 && section[^1].Length <= 3) + { + section = section[..^1]; + } + sections.Add(name, Array.ConvertAll(section.ToArray(), ln => ln.Length <= 3 ? string.Empty : ln[3..])); + } + + var lastSectionName = ""; + var lastSectionStart = 0; + for (var i = 1; i < header.Length - 1; ++i) + { + string current; + if (header[i - 1].Length <= 3 && header[i + 1].Length <= 3 && (current = header[i].TrimEnd()).EndsWith(':')) + { + AddSection(lastSectionName, header[lastSectionStart..(i - 1)]); + lastSectionName = current[3..^1]; + lastSectionStart = i + 2; + ++i; // The next line cannot match + } + } + AddSection(lastSectionName, header[lastSectionStart..]); + + return sections; + } + + private static (string[], string[][]) ParseTable(ReadOnlySpan lines) + { + var columns = new List(); + { + var dashLine = lines[1]; + for (var i = 0; true; /* this part intentionally left blank */) + { + var start = dashLine.IndexOf('-', i); + if (start < 0) + { + break; + } + var end = dashLine.IndexOf(' ', start + 1); + if (end < 0) + { + columns.Add(start..dashLine.Length); + break; + } + else + { + columns.Add(start..end); + i = end + 1; + } + } + } + var headers = new string[columns.Count]; + { + var headerLine = lines[0]; + for (var i = 0; i < columns.Count; ++i) + { + headers[i] = headerLine[columns[i]].Trim(); + } + } + var data = new List(); + foreach (var line in lines[2..]) + { + var row = new string[columns.Count]; + for (var i = 0; i < columns.Count; ++i) + { + row[i] = line[columns[i]].Trim(); + } + data.Add(row); + } + return (headers, data.ToArray()); + } +} diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 1c2b9e6b..7faabc68 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -77,7 +77,8 @@ public partial class MtrlFile foreach( var constant in ShaderPackage.Constants ) { w.Write( constant.Id ); - w.Write( constant.Value ); + w.Write( constant.ByteOffset ); + w.Write( constant.ByteSize ); } foreach( var sampler in ShaderPackage.Samplers ) diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 7508688f..910e4adb 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -239,7 +239,8 @@ public partial class MtrlFile : IWritable public struct Constant { public uint Id; - public uint Value; + public ushort ByteOffset; + public ushort ByteSize; } public struct ShaderPackageData @@ -254,7 +255,20 @@ public partial class MtrlFile : IWritable public readonly uint Version; - public bool Valid { get; } + public bool Valid + { + get + { + foreach (var texture in Textures) + { + if (!texture.Path.Contains('/')) + { + return false; + } + } + return true; + } + } public Texture[] Textures; public UvSet[] UvSets; @@ -263,6 +277,8 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; + public ShpkFile? AssociatedShpk; + public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) @@ -306,7 +322,41 @@ public partial class MtrlFile : IWritable return ret; } + public Span GetConstantValues(Constant constant) + { + if ((constant.ByteOffset & 0x3) == 0 && (constant.ByteSize & 0x3) == 0 + && ((constant.ByteOffset + constant.ByteSize) >> 2) <= ShaderPackage.ShaderValues.Length) + { + return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2); + } + else + { + return null; + } + } + + public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture() + { + var samplers = new List<(Sampler?, ShpkFile.Resource?)>(); + for (var i = 0; i < Textures.Length; ++i) + { + samplers.Add((null, null)); + } + foreach (var sampler in ShaderPackage.Samplers) + { + samplers[sampler.TextureIndex] = (sampler, AssociatedShpk?.GetSamplerById(sampler.SamplerId)); + } + + return samplers; + } + + // Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit public MtrlFile(byte[] data) + : this(data, null) + { + } + + public MtrlFile(byte[] data, Func? loadAssociatedShpk = null) { using var stream = new MemoryStream(data); using var r = new BinaryReader(stream); @@ -345,6 +395,8 @@ public partial class MtrlFile : IWritable ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset); + AssociatedShpk = loadAssociatedShpk?.Invoke(ShaderPackage.Name); + AdditionalData = r.ReadBytes(additionalDataSize); for (var i = 0; i < ColorSets.Length; ++i) { @@ -372,7 +424,6 @@ public partial class MtrlFile : IWritable ShaderPackage.Constants = r.ReadStructuresAsArray(constantCount); ShaderPackage.Samplers = r.ReadStructuresAsArray(samplerCount); ShaderPackage.ShaderValues = r.ReadStructuresAsArray(shaderValueListSize / 4); - Valid = true; } private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets) diff --git a/Penumbra.GameData/Files/ShpkFile.Write.cs b/Penumbra.GameData/Files/ShpkFile.Write.cs new file mode 100644 index 00000000..89effe84 --- /dev/null +++ b/Penumbra.GameData/Files/ShpkFile.Write.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; + +namespace Penumbra.GameData.Files; + +public partial class ShpkFile +{ + public byte[] Write() + { + using var stream = new MemoryStream(); + using var blobs = new MemoryStream(); + using (var w = new BinaryWriter(stream)) + { + w.Write(ShPkMagic); + w.Write(Unknown1); + w.Write(DirectXVersion switch + { + DXVersion.DirectX9 => DX9Magic, + DXVersion.DirectX11 => DX11Magic, + _ => throw new NotImplementedException(), + }); + long offsetsPosition = stream.Position; + w.Write(0u); // Placeholder for file size + w.Write(0u); // Placeholder for blobs offset + w.Write(0u); // Placeholder for strings offset + w.Write((uint)VertexShaders.Length); + w.Write((uint)PixelShaders.Length); + w.Write(MaterialParamsSize); + w.Write((uint)MaterialParams.Length); + w.Write((uint)Constants.Length); + w.Write((uint)Samplers.Length); + w.Write((uint)UnknownA.Length); + w.Write((uint)UnknownB.Length); + w.Write((uint)UnknownC.Length); + w.Write(Unknown2); + w.Write(Unknown3); + w.Write(Unknown4); + + WriteShaderArray(w, VertexShaders, blobs, Strings); + WriteShaderArray(w, PixelShaders, blobs, Strings); + + foreach (var materialParam in MaterialParams) + { + w.Write(materialParam.Id); + w.Write(materialParam.ByteOffset); + w.Write(materialParam.ByteSize); + } + + WriteResourceArray(w, Constants, Strings); + WriteResourceArray(w, Samplers, Strings); + + w.Write(Unknowns.Item1); + w.Write(Unknowns.Item2); + w.Write(Unknowns.Item3); + + WriteUInt32PairArray(w, UnknownA); + WriteUInt32PairArray(w, UnknownB); + WriteUInt32PairArray(w, UnknownC); + + w.Write(AdditionalData); + + var blobsOffset = (int)stream.Position; + blobs.WriteTo(stream); + + var stringsOffset = (int)stream.Position; + Strings.Data.WriteTo(stream); + + var fileSize = (int)stream.Position; + + stream.Seek(offsetsPosition, SeekOrigin.Begin); + w.Write(fileSize); + w.Write(blobsOffset); + w.Write(stringsOffset); + } + + return stream.ToArray(); + } + + private static void WriteResourceArray(BinaryWriter w, Resource[] array, StringPool strings) + { + foreach (var buf in array) + { + var (strOffset, strSize) = strings.FindOrAddString(buf.Name); + w.Write(buf.Id); + w.Write(strOffset); + w.Write(strSize); + w.Write(buf.Slot); + w.Write(buf.Size); + } + } + + private static void WriteShaderArray(BinaryWriter w, Shader[] array, MemoryStream blobs, StringPool strings) + { + foreach (var shader in array) + { + var blobOffset = (int)blobs.Position; + blobs.Write(shader.AdditionalHeader); + blobs.Write(shader.Blob); + var blobSize = (int)blobs.Position - blobOffset; + + w.Write(blobOffset); + w.Write(blobSize); + w.Write((ushort)shader.Constants.Length); + w.Write((ushort)shader.Samplers.Length); + w.Write((ushort)shader.UnknownX.Length); + w.Write((ushort)shader.UnknownY.Length); + + WriteResourceArray(w, shader.Constants, strings); + WriteResourceArray(w, shader.Samplers, strings); + WriteResourceArray(w, shader.UnknownX, strings); + WriteResourceArray(w, shader.UnknownY, strings); + } + } + + private static void WriteUInt32PairArray(BinaryWriter w, (uint, uint)[] array) + { + foreach (var (first, second) in array) + { + w.Write(first); + w.Write(second); + } + } +} diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs new file mode 100644 index 00000000..ae25a7c5 --- /dev/null +++ b/Penumbra.GameData/Files/ShpkFile.cs @@ -0,0 +1,644 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Lumina.Extensions; +using Lumina.Misc; +using Penumbra.GameData.Data; + +namespace Penumbra.GameData.Files; + +public partial class ShpkFile : IWritable +{ + public enum DXVersion : uint + { + DirectX9 = 9, + DirectX11 = 11, + } + + public struct Resource + { + public uint Id; + public string Name; + public ushort Slot; + public ushort Size; + public DisassembledShader.VectorComponents[]? Used; + public DisassembledShader.VectorComponents? UsedDynamically; + } + + public struct Shader + { + public DisassembledShader.ShaderStage Stage; + public DXVersion DirectXVersion; + public Resource[] Constants; + public Resource[] Samplers; + public Resource[] UnknownX; + public Resource[] UnknownY; + public byte[] AdditionalHeader; + private byte[] _blob; + private DisassembledShader? _disassembly; + + public byte[] Blob + { + get => _blob; + set + { + if (_blob == value) + { + return; + } + if (Stage != DisassembledShader.ShaderStage.Unspecified) + { + // Reject the blob entirely if we can't disassemble it or if we find inconsistencies. + var disasm = DisassembledShader.Disassemble(value); + if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion) + { + throw new ArgumentException($"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.", nameof(value)); + } + if (disasm.ShaderModel >= 0x0500) + { + var samplers = new Dictionary(); + var textures = new Dictionary(); + foreach (var binding in disasm.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.Texture: + textures[binding.Slot] = NormalizeResourceName(binding.Name); + break; + case DisassembledShader.ResourceType.Sampler: + samplers[binding.Slot] = NormalizeResourceName(binding.Name); + break; + } + } + if (samplers.Count != textures.Count || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName)) + { + throw new ArgumentException($"The supplied blob has inconsistent shader and texture allocation."); + } + } + _blob = value; + _disassembly = disasm; + } + else + { + _blob = value; + _disassembly = null; + } + UpdateUsed(); + } + } + + public DisassembledShader? Disassembly => _disassembly; + + public Resource? GetConstantById(uint id) + { + return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetConstantByName(string name) + { + return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + + public Resource? GetSamplerById(uint id) + { + return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetSamplerByName(string name) + { + return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + + public void UpdateResources(ShpkFile file) + { + if (_disassembly == null) + { + throw new InvalidOperationException(); + } + var constants = new List(); + var samplers = new List(); + foreach (var binding in _disassembly.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.ConstantBuffer: + var name = NormalizeResourceName(binding.Name); + // We want to preserve IDs as much as possible, and to deterministically generate new ones, to maximize compatibility. + var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name); + constants.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.RegisterCount, + Used = binding.Used, + }); + break; + case DisassembledShader.ResourceType.Texture: + name = NormalizeResourceName(binding.Name); + id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name); + samplers.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.Slot, + Used = binding.Used, + }); + break; + } + } + Constants = constants.ToArray(); + Samplers = samplers.ToArray(); + } + + private void UpdateUsed() + { + if (_disassembly != null) + { + var cbUsage = new Dictionary(); + var tUsage = new Dictionary(); + foreach (var binding in _disassembly.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.ConstantBuffer: + cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; + case DisassembledShader.ResourceType.Texture: + tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; + } + } + for (var i = 0; i < Constants.Length; ++i) + { + if (cbUsage.TryGetValue(Constants[i].Name, out var usage)) + { + Constants[i].Used = usage.Item1; + Constants[i].UsedDynamically = usage.Item2; + } + else + { + Constants[i].Used = null; + Constants[i].UsedDynamically = null; + } + } + for (var i = 0; i < Samplers.Length; ++i) + { + if (tUsage.TryGetValue(Samplers[i].Name, out var usage)) + { + Samplers[i].Used = usage.Item1; + Samplers[i].UsedDynamically = usage.Item2; + } + else + { + Samplers[i].Used = null; + Samplers[i].UsedDynamically = null; + } + } + } + else + { + ClearUsed(Constants); + ClearUsed(Samplers); + } + } + + private static string NormalizeResourceName(string resourceName) + { + var dot = resourceName.IndexOf('.'); + if (dot >= 0) + { + return resourceName[..dot]; + } + else if (resourceName.EndsWith("_S") || resourceName.EndsWith("_T")) + { + return resourceName[..^2]; + } + else + { + return resourceName; + } + } + } + + public struct MaterialParam + { + public uint Id; + public ushort ByteOffset; + public ushort ByteSize; + } + + public class StringPool + { + public MemoryStream Data; + public List StartingOffsets; + + public StringPool(ReadOnlySpan bytes) + { + Data = new MemoryStream(); + Data.Write(bytes); + StartingOffsets = new List + { + 0, + }; + for (var i = 0; i < bytes.Length; ++i) + { + if (bytes[i] == 0) + { + StartingOffsets.Add(i + 1); + } + } + if (StartingOffsets[^1] == bytes.Length) + { + StartingOffsets.RemoveAt(StartingOffsets.Count - 1); + } + else + { + Data.WriteByte(0); + } + } + + public string GetString(int offset, int size) + { + return Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size)); + } + + public string GetNullTerminatedString(int offset) + { + var str = Data.GetBuffer().AsSpan()[offset..]; + var size = str.IndexOf((byte)0); + if (size >= 0) + { + str = str[..size]; + } + return Encoding.UTF8.GetString(str); + } + + public (int, int) FindOrAddString(string str) + { + var dataSpan = Data.GetBuffer().AsSpan(); + var bytes = Encoding.UTF8.GetBytes(str); + foreach (var offset in StartingOffsets) + { + if (offset + bytes.Length > Data.Length) + { + break; + } + var strSpan = dataSpan[offset..]; + var match = true; + for (var i = 0; i < bytes.Length; ++i) + { + if (strSpan[i] != bytes[i]) + { + match = false; + break; + } + } + if (match && strSpan[bytes.Length] == 0) + { + return (offset, bytes.Length); + } + } + Data.Seek(0L, SeekOrigin.End); + var newOffset = (int)Data.Position; + StartingOffsets.Add(newOffset); + Data.Write(bytes); + Data.WriteByte(0); + return (newOffset, bytes.Length); + } + } + + private const uint ShPkMagic = 0x6B506853u; // bytes of ShPk + private const uint DX9Magic = 0x00395844u; // bytes of DX9\0 + private const uint DX11Magic = 0x31315844u; // bytes of DX11 + + public const uint MaterialParamsConstantId = 0x64D12851u; + + public uint Unknown1; + public DXVersion DirectXVersion; + public Shader[] VertexShaders; + public Shader[] PixelShaders; + public uint MaterialParamsSize; + public MaterialParam[] MaterialParams; + public Resource[] Constants; + public Resource[] Samplers; + public (uint, uint)[] UnknownA; + public (uint, uint)[] UnknownB; + public (uint, uint)[] UnknownC; + public uint Unknown2; + public uint Unknown3; + public uint Unknown4; + public (uint, uint, uint) Unknowns; + public byte[] AdditionalData; + public StringPool Strings; // Cannot be safely discarded yet, we don't know if AdditionalData references it + + public bool Valid { get; private set; } + private bool _changed; + + public MaterialParam? GetMaterialParamById(uint id) + { + return MaterialParams.Select(param => new MaterialParam?(param)).FirstOrDefault(param => param!.Value.Id == id); + } + + public Resource? GetConstantById(uint id) + { + return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetConstantByName(string name) + { + return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + + public Resource? GetSamplerById(uint id) + { + return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetSamplerByName(string name) + { + return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + + // Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit + public ShpkFile(byte[] data) + : this(data, false) + { + } + + public ShpkFile(byte[] data, bool disassemble = false) + { + using var stream = new MemoryStream(data); + using var r = new BinaryReader(stream); + + if (r.ReadUInt32() != ShPkMagic) + { + throw new InvalidDataException(); + } + Unknown1 = r.ReadUInt32(); + DirectXVersion = r.ReadUInt32() switch + { + DX9Magic => DXVersion.DirectX9, + DX11Magic => DXVersion.DirectX11, + _ => throw new InvalidDataException(), + }; + if (r.ReadUInt32() != data.Length) + { + throw new InvalidDataException(); + } + var blobsOffset = r.ReadUInt32(); + var stringsOffset = r.ReadUInt32(); + var vertexShaderCount = r.ReadUInt32(); + var pixelShaderCount = r.ReadUInt32(); + MaterialParamsSize = r.ReadUInt32(); + var materialParamCount = r.ReadUInt32(); + var constantCount = r.ReadUInt32(); + var samplerCount = r.ReadUInt32(); + var unknownACount = r.ReadUInt32(); + var unknownBCount = r.ReadUInt32(); + var unknownCCount = r.ReadUInt32(); + Unknown2 = r.ReadUInt32(); + Unknown3 = r.ReadUInt32(); + Unknown4 = r.ReadUInt32(); + + var blobs = new ReadOnlySpan(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset)); + Strings = new StringPool(new ReadOnlySpan(data, (int)stringsOffset, (int)(data.Length - stringsOffset))); + + VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, Strings); + PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, Strings); + + MaterialParams = r.ReadStructuresAsArray((int)materialParamCount); + + Constants = ReadResourceArray(r, (int)constantCount, Strings); + Samplers = ReadResourceArray(r, (int)samplerCount, Strings); + + var unk1 = r.ReadUInt32(); + var unk2 = r.ReadUInt32(); + var unk3 = r.ReadUInt32(); + Unknowns = (unk1, unk2, unk3); + + UnknownA = ReadUInt32PairArray(r, (int)unknownACount); + UnknownB = ReadUInt32PairArray(r, (int)unknownBCount); + UnknownC = ReadUInt32PairArray(r, (int)unknownCCount); + + AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); + + if (disassemble) + { + UpdateUsed(); + } + + Valid = true; + _changed = false; + } + + public void UpdateResources() + { + var constants = new Dictionary(); + var samplers = new Dictionary(); + static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, bool isSamplers) + { + foreach (var resource in shaderResources) + { + if (resources.TryGetValue(resource.Id, out var carry) && isSamplers) + { + continue; + } + var existing = getExistingById(resource.Id); + resources[resource.Id] = new Resource + { + Id = resource.Id, + Name = resource.Name, + Slot = existing?.Slot ?? (isSamplers ? (ushort)2 : (ushort)65535), + Size = isSamplers ? (existing?.Size ?? 0) : Math.Max(carry.Size, resource.Size), + Used = null, + UsedDynamically = null, + }; + } + } + foreach (var shader in VertexShaders) + { + CollectResources(constants, shader.Constants, GetConstantById, false); + CollectResources(samplers, shader.Samplers, GetSamplerById, true); + } + foreach (var shader in PixelShaders) + { + CollectResources(constants, shader.Constants, GetConstantById, false); + CollectResources(samplers, shader.Samplers, GetSamplerById, true); + } + Constants = constants.Values.ToArray(); + Samplers = samplers.Values.ToArray(); + UpdateUsed(); + MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4; + foreach (var param in MaterialParams) + { + MaterialParamsSize = Math.Max(MaterialParamsSize, (uint)param.ByteOffset + param.ByteSize); + } + MaterialParamsSize = (MaterialParamsSize + 0xFu) & ~0xFu; + } + + private void UpdateUsed() + { + var cUsage = new Dictionary(); + var sUsage = new Dictionary(); + static void CollectUsage(Dictionary usage, Resource[] resources) + { + foreach (var resource in resources) + { + if (resource.Used == null) + { + continue; + } + usage.TryGetValue(resource.Id, out var carry); + carry.Item1 ??= Array.Empty(); + var combined = new DisassembledShader.VectorComponents[Math.Max(carry.Item1.Length, resource.Used.Length)]; + for (var i = 0; i < combined.Length; ++i) + { + combined[i] = (i < carry.Item1.Length ? carry.Item1[i] : 0) | (i < resource.Used.Length ? resource.Used[i] : 0); + } + usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0)); + } + } + foreach (var shader in VertexShaders) + { + CollectUsage(cUsage, shader.Constants); + CollectUsage(sUsage, shader.Samplers); + } + foreach (var shader in PixelShaders) + { + CollectUsage(cUsage, shader.Constants); + CollectUsage(sUsage, shader.Samplers); + } + for (var i = 0; i < Constants.Length; ++i) + { + if (cUsage.TryGetValue(Constants[i].Id, out var usage)) + { + Constants[i].Used = usage.Item1; + Constants[i].UsedDynamically = usage.Item2; + } + else + { + Constants[i].Used = null; + Constants[i].UsedDynamically = null; + } + } + for (var i = 0; i < Samplers.Length; ++i) + { + if (sUsage.TryGetValue(Samplers[i].Id, out var usage)) + { + Samplers[i].Used = usage.Item1; + Samplers[i].UsedDynamically = usage.Item2; + } + else + { + Samplers[i].Used = null; + Samplers[i].UsedDynamically = null; + } + } + } + + public void SetInvalid() + { + Valid = false; + } + + public void SetChanged() + { + _changed = true; + } + + public bool IsChanged() + { + var changed = _changed; + _changed = false; + return changed; + } + + private static void ClearUsed(Resource[] resources) + { + for (var i = 0; i < resources.Length; ++i) + { + resources[i].Used = null; + resources[i].UsedDynamically = null; + } + } + + private static Resource[] ReadResourceArray(BinaryReader r, int count, StringPool strings) + { + var ret = new Resource[count]; + for (var i = 0; i < count; ++i) + { + var buf = new Resource(); + + buf.Id = r.ReadUInt32(); + var strOffset = r.ReadUInt32(); + var strSize = r.ReadUInt32(); + buf.Name = strings.GetString((int)strOffset, (int)strSize); + buf.Slot = r.ReadUInt16(); + buf.Size = r.ReadUInt16(); + + ret[i] = buf; + } + + return ret; + } + + private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DXVersion directX, bool disassemble, ReadOnlySpan blobs, StringPool strings) + { + var extraHeaderSize = stage switch + { + DisassembledShader.ShaderStage.Vertex => directX switch + { + DXVersion.DirectX9 => 4, + DXVersion.DirectX11 => 8, + _ => throw new NotImplementedException(), + }, + _ => 0, + }; + + var ret = new Shader[count]; + for (var i = 0; i < count; ++i) + { + var blobOffset = r.ReadUInt32(); + var blobSize = r.ReadUInt32(); + var constantCount = r.ReadUInt16(); + var samplerCount = r.ReadUInt16(); + var unknownXCount = r.ReadUInt16(); + var unknownYCount = r.ReadUInt16(); + + var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize); + + var shader = new Shader(); + + shader.Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified; + shader.DirectXVersion = directX; + shader.Constants = ReadResourceArray(r, constantCount, strings); + shader.Samplers = ReadResourceArray(r, samplerCount, strings); + shader.UnknownX = ReadResourceArray(r, unknownXCount, strings); + shader.UnknownY = ReadResourceArray(r, unknownYCount, strings); + shader.AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(); + shader.Blob = rawBlob[extraHeaderSize..].ToArray(); + + ret[i] = shader; + } + + return ret; + } + + private static (uint, uint)[] ReadUInt32PairArray(BinaryReader r, int count) + { + var ret = new (uint, uint)[count]; + for (var i = 0; i < count; ++i) + { + var first = r.ReadUInt32(); + var second = r.ReadUInt32(); + + ret[i] = (first, second); + } + + return ret; + } +} diff --git a/Penumbra.GameData/Interop/D3DCompiler.cs b/Penumbra.GameData/Interop/D3DCompiler.cs new file mode 100644 index 00000000..ed6366c2 --- /dev/null +++ b/Penumbra.GameData/Interop/D3DCompiler.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Penumbra.GameData.Interop; + +internal static class D3DCompiler +{ + [Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface ID3DBlob + { + [PreserveSig] + public unsafe void* GetBufferPointer(); + + [PreserveSig] + public UIntPtr GetBufferSize(); + } + + [Flags] + public enum DisassembleFlags : uint + { + EnableColorCode = 1, + EnableDefaultValuePrints = 2, + EnableInstructionNumbering = 4, + EnableInstructionCycle = 8, + DisableDebugInfo = 16, + EnableInstructionOffset = 32, + InstructionOnly = 64, + PrintHexLiterals = 128, + } + + public static unsafe string Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") + { + ID3DBlob? disassembly; + int hr; + fixed (byte* pSrcData = blob) + { + hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); + } + Marshal.ThrowExceptionForHR(hr); + var ret = Encoding.UTF8.GetString(BlobContents(disassembly)); + GC.KeepAlive(disassembly); + return ret; + } + + private static unsafe ReadOnlySpan BlobContents(ID3DBlob? blob) + { + if (blob == null) + { + return ReadOnlySpan.Empty; + } + + return new ReadOnlySpan(blob.GetBufferPointer(), (int)blob.GetBufferSize().ToUInt32()); + } + + [PreserveSig] + [DllImport("D3DCompiler_47.dll")] + private extern static unsafe int D3DDisassemble( + [In] byte* pSrcData, + [In] UIntPtr srcDataSize, + uint flags, + [MarshalAs(UnmanagedType.LPStr)] string szComments, + out ID3DBlob? ppDisassembly); +} diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 583442fd..ea149216 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -76,6 +76,7 @@ public partial class Mod private List< FileRegistry > _mtrlFiles = null!; private List< FileRegistry > _mdlFiles = null!; private List< FileRegistry > _texFiles = null!; + private List< FileRegistry > _shpkFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -93,6 +94,9 @@ public partial class Mod public IReadOnlyList< FileRegistry > TexFiles => _texFiles; + public IReadOnlyList< FileRegistry > ShpkFiles + => _shpkFiles; + // Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths() { @@ -140,6 +144,7 @@ public partial class Mod _mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList(); _mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList(); _texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList(); + _shpkFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".shpk", StringComparison.OrdinalIgnoreCase ) ).ToList(); FileChanges = false; foreach( var subMod in _mod.AllSubMods ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 343ef0ce..9f4b4562 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Mods; using Penumbra.String.Classes; +using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.UI.Classes; @@ -23,6 +24,7 @@ public partial class ModEditWindow private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; private readonly Func< T, bool, bool > _drawEdit; private readonly Func< string > _getInitialPath; + private readonly Func< byte[], T? > _parseFile; private Mod.Editor.FileRegistry? _currentPath; private T? _currentFile; @@ -39,13 +41,14 @@ public partial class ModEditWindow private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) + Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile ) { _tabName = tabName; _fileType = fileType; _getFiles = getFiles; _drawEdit = drawEdit; _getInitialPath = getInitialPath; + _parseFile = parseFile ?? DefaultParseFile; } public void Draw() @@ -84,7 +87,7 @@ public partial class ModEditWindow if( file != null ) { _defaultException = null; - _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + _defaultFile = _parseFile( file.Data ); } else { @@ -172,6 +175,11 @@ public partial class ModEditWindow } } + private static T? DefaultParseFile( byte[] bytes ) + { + return Activator.CreateInstance( typeof( T ), bytes ) as T; + } + private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) { if( ReferenceEquals( _currentPath, path ) ) @@ -185,7 +193,7 @@ public partial class ModEditWindow try { var bytes = File.ReadAllBytes( _currentPath.File.FullName ); - _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; + _currentFile = _parseFile( bytes ); } catch( Exception e ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 400a7f47..d0a0c4bb 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,15 +1,23 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; using ImGuiNET; +using Lumina.Data.Parsing; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.String.Functions; +using Penumbra.Util; +using static OtterGui.Raii.ImRaii; namespace Penumbra.UI.Classes; @@ -17,7 +25,12 @@ public partial class ModEditWindow { private readonly FileEditor< MtrlFile > _materialTab; - private static bool DrawMaterialPanel( MtrlFile file, bool disabled ) + private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + + private uint _materialNewConstantId = 0; + private uint _materialNewSamplerId = 0; + + private bool DrawMaterialPanel( MtrlFile file, bool disabled ) { var ret = DrawMaterialTextureChange( file, disabled ); @@ -27,22 +40,38 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawMaterialColorSetChange( file, disabled ); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawMaterialShaderResources( file, disabled ); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawOtherMaterialDetails( file, disabled ); + _materialFileDialog.Draw(); + return !disabled && ret; } private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) { + var samplers = file.GetSamplersByTexture(); + var names = new List(); + var maxWidth = 0.0f; + for( var i = 0; i < file.Textures.Length; ++i ) + { + var (sampler, shpkSampler) = samplers[i]; + var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; + names.Add( name ); + maxWidth = Math.Max( maxWidth, ImGui.CalcTextSize( name ).X ); + } + using var id = ImRaii.PushId( "Textures" ); var ret = false; for( var i = 0; i < file.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); var tmp = file.Textures[ i ].Path; - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - maxWidth ); + if( ImGui.InputText( names[i], ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 && tmp != file.Textures[ i ].Path ) @@ -140,24 +169,350 @@ public partial class ModEditWindow return ret; } - private static bool DrawOtherMaterialDetails( MtrlFile file, bool _ ) + private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) + var ret = false; + + if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) { return false; } - using( var textures = ImRaii.TreeNode( "Textures", ImGuiTreeNodeFlags.DefaultOpen ) ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - if( textures ) + ret = true; + } + var shpkFlags = ( int )file.ShaderPackage.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Flags = ( uint )shpkFlags; + ret = true; + } + ImRaii.TreeNode( $"Has associated ShPk file (for advanced editing): {( file.AssociatedShpk != null ? "Yes" : "No" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + if( !disabled && ImGui.Button( "Associate modded ShPk file" ) ) + { + _materialFileDialog.OpenFileDialog( $"Associate modded ShPk file...", ".shpk", ( success, name ) => { - foreach( var tex in file.Textures ) + if( !success ) { - ImRaii.TreeNode( $"{tex.Path} - {tex.Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + return; + } + + try + { + file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + + if( file.ShaderPackage.ShaderKeys.Length > 0 ) + { + using var t = ImRaii.TreeNode( "Shader Keys" ); + if( t ) + { + foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + { + using var t2 = ImRaii.TreeNode( $"Shader Key #{idx}", file.ShaderPackage.ShaderKeys.Length == 1 ? ImGuiTreeNodeFlags.DefaultOpen : 0 ); + if( t2 ) + { + ImRaii.TreeNode( $"Category: 0x{key.Category:X8} ({key.Category})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Value: 0x{key.Value:X8} ({key.Value})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } } } } + if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0 + || file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) + { + var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); + if( t ) + { + var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet< uint >(); + var hasMalformedConstants = false; + + foreach( var constant in file.ShaderPackage.Constants ) + { + definedConstants.Add( constant.Id ); + var values = file.GetConstantValues( constant ); + if( file.GetConstantValues( constant ).Length > 0 ) + { + var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + aliasedValueCount += values.Length - unique; + } + else + { + hasMalformedConstants = true; + } + } + + foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + { + var values = file.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) + { + var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; + if( ( paramByteOffset & 0x3 ) == 0 ) + { + paramValueOffset = paramByteOffset >> 2; + } + } + var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); + + using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ( ": " + constantName ) : "" )} (ID: 0x{constant.Id:X8})" ); + if( t2 ) + { + if( values.Length > 0 ) + { + var valueOffset = constant.ByteOffset >> 2; + + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( ( valueOffset + valueIdx ) << 2 ):X4})", + ref values[valueIdx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + else + { + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( !disabled && !hasMalformedConstants && orphanValues.Count == 0 && aliasedValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + ArrayRemove( ref file.ShaderPackage.ShaderValues, constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + ArrayRemove( ref file.ShaderPackage.Constants, idx ); + for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) + { + if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) + { + file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; + } + } + ret = true; + } + } + } + + if( orphanValues.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); + if( t2 ) + { + foreach( var idx in orphanValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{( idx << 2 ):X4})", + ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + } + else if ( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) + { + var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); + if( missingConstants.Length > 0 ) + { + var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _materialNewConstantId ); + if( selectedConstant.ByteSize == 0 ) + { + selectedConstant = missingConstants[0]; + _materialNewConstantId = selectedConstant.Id; + } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); + using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + { + if( c ) + { + foreach( var constant in missingConstants ) + { + var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})" ) ) + { + selectedConstant = constant; + _materialNewConstantId = constant.Id; + } + } + } + } + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + var valueOffset = ArrayAdd( ref file.ShaderPackage.ShaderValues, 0.0f, selectedConstant.ByteSize >> 2 ); + ArrayAdd( ref file.ShaderPackage.Constants, new MtrlFile.Constant + { + Id = _materialNewConstantId, + ByteOffset = ( ushort )( valueOffset << 2 ), + ByteSize = selectedConstant.ByteSize, + } ); + ret = true; + } + } + } + } + } + + if( file.ShaderPackage.Samplers.Length > 0 || file.Textures.Length > 0 + || file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + { + using var t = ImRaii.TreeNode( "Samplers" ); + if( t ) + { + var orphanTextures = new IndexSet( file.Textures.Length, true ); + var aliasedTextureCount = 0; + var definedSamplers = new HashSet< uint >(); + + foreach( var sampler in file.ShaderPackage.Samplers ) + { + if( !orphanTextures.Remove( sampler.TextureIndex ) ) + { + ++aliasedTextureCount; + } + definedSamplers.Add( sampler.SamplerId ); + } + + foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + { + var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ( ": " + shpkSampler.Value.Name ) : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + if( t2 ) + { + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + + // FIXME this probably doesn't belong here + static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + { + fixed( ushort* v2 = &v ) + { + return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%04X", flags ); + } + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + var sampFlags = ( int )sampler.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; + ret = true; + } + + if( !disabled && orphanTextures.Count == 0 && aliasedTextureCount == 0 + && ImGui.Button( "Remove Sampler" ) ) + { + ArrayRemove( ref file.Textures, sampler.TextureIndex ); + ArrayRemove( ref file.ShaderPackage.Samplers, idx ); + for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) + { + if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) + { + --file.ShaderPackage.Samplers[i].TextureIndex; + } + } + ret = true; + } + } + } + + if( orphanTextures.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); + if( t2 ) + { + foreach( var idx in orphanTextures ) + { + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) + { + var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); + if( missingSamplers.Length > 0 ) + { + var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _materialNewSamplerId ); + if( selectedSampler.Name == null ) + { + selectedSampler = missingSamplers[0]; + _materialNewSamplerId = selectedSampler.Id; + } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) + { + if( c ) + { + foreach( var sampler in missingSamplers ) + { + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})" ) ) + { + selectedSampler = sampler; + _materialNewSamplerId = sampler.Id; + } + } + } + } + ImGui.SameLine(); + if( ImGui.Button( "Add Sampler" ) ) + { + var texIndex = ArrayAdd( ref file.Textures, new MtrlFile.Texture + { + Path = string.Empty, + Flags = 0, + } ); + ArrayAdd( ref file.ShaderPackage.Samplers, new Sampler + { + SamplerId = _materialNewSamplerId, + TextureIndex = ( byte )texIndex, + Flags = 0, + } ); + ret = true; + } + } + } + } + } + + return ret; + } + + private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled ) + { + var ret = false; + + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return false; + } + using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) { if( sets ) @@ -169,50 +524,6 @@ public partial class ModEditWindow } } - using( var shaders = ImRaii.TreeNode( "Shaders", ImGuiTreeNodeFlags.DefaultOpen ) ) - { - if( shaders ) - { - ImRaii.TreeNode( $"Name: {file.ShaderPackage.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Flags: {file.ShaderPackage.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) - { - using var t = ImRaii.TreeNode( $"Shader Key #{idx}" ); - if( t ) - { - ImRaii.TreeNode( $"Category: {key.Category}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Value: {key.Value}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) - { - using var t = ImRaii.TreeNode( $"Constant #{idx}" ); - if( t ) - { - ImRaii.TreeNode( $"Category: {constant.Id}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Value: 0x{constant.Value:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) - { - using var t = ImRaii.TreeNode( $"Sampler #{idx}" ); - if( t ) - { - ImRaii.TreeNode( $"ID: {sampler.SamplerId}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Texture Index: {sampler.TextureIndex}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Flags: 0x{sampler.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - foreach( var (value, idx) in file.ShaderPackage.ShaderValues.WithIndex() ) - { - ImRaii.TreeNode( $"Value #{idx}: {value.ToString( CultureInfo.InvariantCulture )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - if( file.AdditionalData.Length > 0 ) { using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); @@ -222,7 +533,7 @@ public partial class ModEditWindow } } - return false; + return ret; } private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) @@ -659,4 +970,69 @@ public partial class ModEditWindow } } } + + // FIXME this probably doesn't belong here + // Also used in ShaderPackages + private static int ArrayAdd( ref T[] array, T element, int count = 1 ) + { + var length = array.Length; + var newArray = new T[array.Length + count]; + Array.Copy( array, newArray, length ); + for( var i = 0; i < count; ++i ) + { + newArray[length + i] = element; + } + array = newArray; + return length; + } + + private static void ArrayRemove( ref T[] array, int offset, int count = 1 ) + { + var newArray = new T[array.Length - count]; + Array.Copy( array, newArray, offset ); + Array.Copy( array, offset + count, newArray, offset, newArray.Length - offset ); + array = newArray; + } + + private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + { + if( valueLength == 0 || valueOffset < 0 ) + { + return (null, false); + } + + var firstVector = valueOffset >> 2; + var lastVector = ( valueOffset + valueLength - 1 ) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; + + static string VectorSwizzle( int firstComponent, int numComponents ) + => ( numComponents == 4 ) ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); + + if( firstVector == lastVector ) + { + return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + } + + var parts = new string[lastVector + 1 - firstVector]; + parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + for( var i = firstVector + 1; i < lastVector; ++i ) + { + parts[i - firstVector] = $"[{i}]"; + } + + return (string.Join( ", ", parts ), false); + } + + private static string? MaterialParamName( bool componentOnly, int offset ) + { + if( offset < 0 ) + { + return null; + } + var component = "xyzw"[offset & 0x3]; + + return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs new file mode 100644 index 00000000..4facc34f --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -0,0 +1,557 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.Util; +using Lumina.Data.Parsing; +using static OtterGui.Raii.ImRaii; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileEditor _shaderPackageTab; + + private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); + + private uint _shaderPackageNewMaterialParamId = 0; + private ushort _shaderPackageNewMaterialParamStart = 0; + private ushort _shaderPackageNewMaterialParamEnd = 0; + + private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) + { + var ret = DrawShaderPackageSummary( file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawShaderPackageShaderArray( "Vertex Shader", file.VertexShaders, file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawShaderPackageShaderArray( "Pixel Shader", file.PixelShaders, file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawOtherShaderPackageDetails( file, disabled ); + + _shaderPackageFileDialog.Draw(); + + ret |= file.IsChanged(); + + return !disabled && ret; + } + + private static bool DrawShaderPackageSummary( ShpkFile file, bool _ ) + { + ImGui.Text( $"Shader Package for DirectX {( int )file.DirectXVersion}" ); + + return false; + } + + private bool DrawShaderPackageShaderArray( string objectName, ShpkFile.Shader[] shaders, ShpkFile file, bool disabled ) + { + if( shaders.Length == 0 ) + { + return false; + } + + if( !ImGui.CollapsingHeader( $"{objectName}s" ) ) + { + return false; + } + + var ret = false; + + foreach( var (shader, idx) in shaders.WithIndex() ) + { + using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); + if( t ) + { + if( ImGui.Button( $"Export Shader Blob ({shader.Blob.Length} bytes)" ) ) + { + var extension = file.DirectXVersion switch + { + ShpkFile.DXVersion.DirectX9 => ".cso", + ShpkFile.DXVersion.DirectX11 => ".dxbc", + _ => throw new NotImplementedException(), + }; + var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); + var blob = shader.Blob; + _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Blob to...", extension, defaultName, extension, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, blob ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + ChatUtil.NotificationMessage( $"Shader Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + if( !disabled ) + { + ImGui.SameLine(); + if( ImGui.Button( "Replace Shader Blob" ) ) + { + _shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Blob...", "Shader Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + shaders[idx].Blob = File.ReadAllBytes( name ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not import Shader Blob {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + try + { + shaders[idx].UpdateResources( file ); + file.UpdateResources(); + } + catch( Exception e ) + { + file.SetInvalid(); + Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + file.SetChanged(); + ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unknown Type X Resources", "slot", true, shader.UnknownX, disabled ); + ret |= DrawShaderPackageResourceArray( "Unknown Type Y Resources", "slot", true, shader.UnknownY, disabled ); + + if( shader.AdditionalHeader.Length > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); + if( t2 ) + { + ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); + } + } + + using( var t2 = ImRaii.TreeNode( "Raw Disassembly" ) ) + { + if( t2 ) + { + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.TextUnformatted( shader.Disassembly!.RawDisassembly ); + } + } + } + } + } + + return ret; + } + + private bool DrawShaderPackageMaterialParamLayout( ShpkFile file, bool disabled ) + { + var ret = false; + + var materialParams = file.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + if( !ImGui.CollapsingHeader( $"{materialParams?.Name ?? "Material Parameter"} Layout" ) ) + { + return false; + } + + var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == ( materialParams.Value.Size << 4 ) ); + + if( !isSizeWellDefined ) + { + if( materialParams.HasValue ) + { + ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" ); + } + else + { + ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + } + } + + var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu) >> 2]; + var orphanParameters = new IndexSet( parameters.Length, true ); + var definedParameters = new HashSet< uint >(); + var hasMalformedParameters = false; + + foreach( var param in file.MaterialParams ) + { + definedParameters.Add( param.Id ); + if( ( param.ByteOffset & 0x3 ) == 0 && ( param.ByteSize & 0x3 ) == 0 + && ( param.ByteOffset + param.ByteSize ) <= file.MaterialParamsSize ) + { + var valueOffset = param.ByteOffset >> 2; + var valueCount = param.ByteSize >> 2; + orphanParameters.RemoveRange( valueOffset, valueCount ); + + parameters[valueOffset] = (param.Id, true); + + for( var i = 1; i < valueCount; ++i ) + { + parameters[valueOffset + i] = (param.Id, false); + } + } + else + { + hasMalformedParameters = true; + } + } + + ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); + + using( var table = ImRaii.Table( "##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + { + if( table ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader( string.Empty ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "x" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "y" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "z" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "w" ); + + var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity + var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red + var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + + for( var idx = 0; idx < parameters.Length; idx += 4 ) + { + var usedComponents = materialParams?.Used?[idx >> 2] ?? DisassembledShader.VectorComponents.All; + ImGui.TableNextColumn(); + ImGui.Text( $"[{idx >> 2}]" ); + for( var col = 0; col < 4; ++col ) + { + var cell = parameters[idx + col]; + ImGui.TableNextColumn(); + var start = cell.HasValue && cell.Value.Item2; + var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; + using var c = ImRaii.PushColor( ImGuiCol.Text, used ? ( start ? textColorStart : textColorCont ) : ( start ? textColorUnusedStart : textColorUnusedCont ) ); + ImGui.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" ); + } + ImGui.TableNextRow(); + } + } + } + + if( hasMalformedParameters ) + { + using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); + if( t ) + { + foreach( var param in file.MaterialParams ) + { + if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) + { + ImRaii.TreeNode( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + else if( ( param.ByteOffset + param.ByteSize ) > file.MaterialParamsSize ) + { + ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + } + else if( !disabled && isSizeWellDefined ) + { + using var t = ImRaii.TreeNode( "Add / Remove Parameters" ); + if( t ) + { + for( var i = 0; i < file.MaterialParams.Length; ++i ) + { + var param = file.MaterialParams[i]; + using var t2 = ImRaii.TreeNode( $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})" ); + if( t2 ) + { + if( ImGui.Button( "Remove" ) ) + { + ArrayRemove( ref file.MaterialParams, i ); + ret = true; + } + } + } + if( orphanParameters.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( "New Parameter" ); + if( t2 ) + { + var starts = orphanParameters.ToArray(); + if( !orphanParameters[_shaderPackageNewMaterialParamStart] ) + { + _shaderPackageNewMaterialParamStart = ( ushort )starts[0]; + } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); + var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; + using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) + { + if( c ) + { + foreach( var start in starts ) + { + var name = MaterialParamName( false, start )!; + if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}" ) ) + { + _shaderPackageNewMaterialParamStart = ( ushort )start; + } + } + } + } + var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; + var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => { + var ret = i <= lastEndCandidate + 1; + lastEndCandidate = i; + return ret; + } ).ToArray(); + if( Array.IndexOf(ends, _shaderPackageNewMaterialParamEnd) < 0 ) + { + _shaderPackageNewMaterialParamEnd = ( ushort )ends[0]; + } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); + var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; + using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) + { + if( c ) + { + foreach( var end in ends ) + { + var name = MaterialParamName( false, end )!; + if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}" ) ) + { + _shaderPackageNewMaterialParamEnd = ( ushort )end; + } + } + } + } + var id = ( int )_shaderPackageNewMaterialParamId; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "ID", ref id, 0, 0, ImGuiInputTextFlags.CharsHexadecimal ) ) + { + _shaderPackageNewMaterialParamId = ( uint )id; + } + if( ImGui.Button( "Add" ) ) + { + if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) + { + ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", NotificationType.Error ); + } + else + { + ArrayAdd( ref file.MaterialParams, new ShpkFile.MaterialParam + { + Id = _shaderPackageNewMaterialParamId, + ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), + ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), + } ); + ret = true; + } + } + } + } + } + } + + return ret; + } + + private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool _ ) + { + if( resources.Length == 0 ) + { + return false; + } + + using var t = ImRaii.TreeNode( arrayName ); + if( !t ) + { + return false; + } + + var ret = false; + + foreach( var (buf, idx) in resources.WithIndex() ) + { + using var t2 = ImRaii.TreeNode( $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + ( withSize ? $", size: {buf.Size} registers" : string.Empty ), ( buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf ); + if( t2 ) + { + var used = new List< string >(); + if( withSize ) + { + foreach( var (components, i) in ( buf.Used ?? Array.Empty() ).WithIndex() ) + { + switch( components ) + { + case 0: + break; + case DisassembledShader.VectorComponents.All: + used.Add( $"[{i}]" ); + break; + default: + used.Add( $"[{i}].{new string( components.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); + break; + } + } + switch( buf.UsedDynamically ?? 0 ) + { + case 0: + break; + case DisassembledShader.VectorComponents.All: + used.Add( "[*]" ); + break; + default: + used.Add( $"[*].{new string( buf.UsedDynamically!.Value.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); + break; + } + } + else + { + var components = ( ( buf.Used != null && buf.Used.Length > 0 ) ? buf.Used[0] : 0 ) | (buf.UsedDynamically ?? 0); + if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) + { + used.Add( "Red" ); + } + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) + { + used.Add( "Green" ); + } + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) + { + used.Add( "Blue" ); + } + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) + { + used.Add( "Alpha" ); + } + } + if( used.Count > 0 ) + { + ImRaii.TreeNode( $"Used: {string.Join(", ", used)}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + else + { + ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + return ret; + } + + private static bool DrawOtherShaderPackageDetails( ShpkFile file, bool disabled ) + { + var ret = false; + + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return false; + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled ); + + if( file.UnknownA.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Unknown Type A Structures ({file.UnknownA.Length})" ); + if( t ) + { + foreach( var (unk, idx) in file.UnknownA.WithIndex() ) + { + ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + if( file.UnknownB.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Unknown Type B Structures ({file.UnknownB.Length})" ); + if( t ) + { + foreach( var (unk, idx) in file.UnknownB.WithIndex() ) + { + ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + if( file.UnknownC.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Unknown Type C Structures ({file.UnknownC.Length})" ); + if( t ) + { + foreach( var (unk, idx) in file.UnknownC.WithIndex() ) + { + ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + using( var t = ImRaii.TreeNode( $"Misc. Unknown Fields" ) ) + { + if( t ) + { + ImRaii.TreeNode( $"#1 (at 0x0004): 0x{file.Unknown1:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#2 (at 0x003C): 0x{file.Unknown2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#3 (at 0x0040): 0x{file.Unknown3:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#4 (at 0x0044): 0x{file.Unknown4:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + if( file.AdditionalData.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); + if( t ) + { + ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); + } + } + + using( var t = ImRaii.TreeNode( $"String Pool" ) ) + { + if( t ) + { + foreach( var offset in file.Strings.StartingOffsets ) + { + ImGui.Text( file.Strings.GetNullTerminatedString( offset ) ); + } + } + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index e1fecd92..5e5c514e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Numerics; using System.Text; @@ -47,6 +48,7 @@ public partial class ModEditWindow : Window, IDisposable _selectedFiles.Clear(); _modelTab.Reset(); _materialTab.Reset(); + _shaderPackageTab.Reset(); _swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings ); } @@ -155,6 +157,7 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Draw(); _materialTab.Draw(); DrawTextureTab(); + _shaderPackageTab.Draw(); _swapWindow.DrawItemSwapPanel(); } @@ -532,17 +535,65 @@ public partial class ModEditWindow : Window, IDisposable ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); } + // FIXME this probably doesn't belong here + private T? LoadAssociatedFile( string gamePath, Func< byte[], T? > parse ) + { + var defaultFiles = _mod?.Default?.Files; + if( defaultFiles != null ) + { + if( Utf8GamePath.FromString( gamePath, out var utf8Path, true ) ) + { + try + { + if (defaultFiles.TryGetValue( utf8Path, out var fsPath )) + { + return parse( File.ReadAllBytes( fsPath.FullName ) ); + } + } + finally + { + utf8Path.Dispose(); + } + } + } + + var file = Dalamud.GameData.GetFile( gamePath )?.Data; + return file == null ? default : parse( file ); + } + + // FIXME neither does this + private ShpkFile? LoadAssociatedShpk( string shaderName ) + { + var path = $"shader/sm5/shpk/{shaderName}"; + try + { + return LoadAssociatedFile( path, file => new ShpkFile( file ) ); + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse associated file {path} to Shpk:\n{e}" ); + return null; + } + } + public ModEditWindow() : base( WindowBaseLabel ) { _materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, - () => _mod?.ModPath.FullName ?? string.Empty ); + () => _mod?.ModPath.FullName ?? string.Empty, + bytes => new MtrlFile( bytes, LoadAssociatedShpk ) ); _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel, - () => _mod?.ModPath.FullName ?? string.Empty ); + () => _mod?.ModPath.FullName ?? string.Empty, + null ); + _shaderPackageTab = new FileEditor< ShpkFile >( "Shader Packages", ".shpk", + () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawShaderPackagePanel, + () => _mod?.ModPath.FullName ?? string.Empty, + bytes => new ShpkFile( bytes, true ) ); _center = new CombinedTexture( _left, _right ); } diff --git a/Penumbra/Util/IndexSet.cs b/Penumbra/Util/IndexSet.cs new file mode 100644 index 00000000..e0ed7921 --- /dev/null +++ b/Penumbra/Util/IndexSet.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.Util; + +public class IndexSet : IEnumerable +{ + private readonly BitArray _set; + private int _count; + + public int Capacity => _set.Count; + + public int Count => _count; + + public bool this[Index index] + { + get => _set[index]; + set + { + if( value ) + { + Add( index ); + } + else + { + Remove( index ); + } + } + } + + public IndexSet( int capacity, bool initiallyFull ) + { + _set = new BitArray( capacity, initiallyFull ); + _count = initiallyFull ? capacity : 0; + } + + public bool Add( Index index ) + { + var ret = !_set[index]; + if( ret ) + { + ++_count; + _set[index] = true; + } + return ret; + } + + public bool Remove( Index index ) + { + var ret = _set[index]; + if( ret ) + { + --_count; + _set[index] = false; + } + return ret; + } + + public int AddRange( int offset, int length ) + { + var ret = 0; + for( var idx = 0; idx < length; ++idx ) + { + if( Add( offset + idx ) ) + { + ++ret; + } + } + return ret; + } + + public int RemoveRange( int offset, int length ) + { + var ret = 0; + for( var idx = 0; idx < length; ++idx ) + { + if( Remove( offset + idx ) ) + { + ++ret; + } + } + return ret; + } + + public IEnumerator GetEnumerator() + { + if( _count > 0 ) + { + var capacity = _set.Count; + var remaining = _count; + for( var i = 0; i < capacity; ++i ) + { + if( _set[i] ) + { + yield return i; + if( --remaining == 0 ) + { + yield break; + } + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} From 33231959b2c697d3a84e2aae0b47d39a4b15dabf Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 15 Feb 2023 22:20:38 +0100 Subject: [PATCH 0759/2451] More Mtrl and ShPk editing (thanks @aers) --- Penumbra.GameData/Files/ShpkFile.Write.cs | 103 ++++- Penumbra.GameData/Files/ShpkFile.cs | 395 +++++++++++++----- .../UI/Classes/ModEditWindow.Materials.cs | 163 +++++++- .../Classes/ModEditWindow.ShaderPackages.cs | 234 ++++++----- 4 files changed, 648 insertions(+), 247 deletions(-) diff --git a/Penumbra.GameData/Files/ShpkFile.Write.cs b/Penumbra.GameData/Files/ShpkFile.Write.cs index 89effe84..dd8b70ac 100644 --- a/Penumbra.GameData/Files/ShpkFile.Write.cs +++ b/Penumbra.GameData/Files/ShpkFile.Write.cs @@ -7,19 +7,25 @@ public partial class ShpkFile { public byte[] Write() { + if (SubViewKeys.Length != 2) + { + throw new InvalidDataException(); + } + using var stream = new MemoryStream(); using var blobs = new MemoryStream(); + var strings = new StringPool(ReadOnlySpan.Empty); using (var w = new BinaryWriter(stream)) { w.Write(ShPkMagic); - w.Write(Unknown1); + w.Write(Version); w.Write(DirectXVersion switch { DXVersion.DirectX9 => DX9Magic, DXVersion.DirectX11 => DX11Magic, _ => throw new NotImplementedException(), }); - long offsetsPosition = stream.Position; + var offsetsPosition = stream.Position; w.Write(0u); // Placeholder for file size w.Write(0u); // Placeholder for blobs offset w.Write(0u); // Placeholder for strings offset @@ -29,15 +35,15 @@ public partial class ShpkFile w.Write((uint)MaterialParams.Length); w.Write((uint)Constants.Length); w.Write((uint)Samplers.Length); - w.Write((uint)UnknownA.Length); - w.Write((uint)UnknownB.Length); - w.Write((uint)UnknownC.Length); - w.Write(Unknown2); - w.Write(Unknown3); - w.Write(Unknown4); + w.Write((uint)UAVs.Length); + w.Write((uint)SystemKeys.Length); + w.Write((uint)SceneKeys.Length); + w.Write((uint)MaterialKeys.Length); + w.Write((uint)Nodes.Length); + w.Write((uint)Items.Length); - WriteShaderArray(w, VertexShaders, blobs, Strings); - WriteShaderArray(w, PixelShaders, blobs, Strings); + WriteShaderArray(w, VertexShaders, blobs, strings); + WriteShaderArray(w, PixelShaders, blobs, strings); foreach (var materialParam in MaterialParams) { @@ -46,16 +52,68 @@ public partial class ShpkFile w.Write(materialParam.ByteSize); } - WriteResourceArray(w, Constants, Strings); - WriteResourceArray(w, Samplers, Strings); + WriteResourceArray(w, Constants, strings); + WriteResourceArray(w, Samplers, strings); + WriteResourceArray(w, UAVs, strings); - w.Write(Unknowns.Item1); - w.Write(Unknowns.Item2); - w.Write(Unknowns.Item3); + foreach (var key in SystemKeys) + { + w.Write(key.Id); + w.Write(key.DefaultValue); + } + foreach (var key in SceneKeys) + { + w.Write(key.Id); + w.Write(key.DefaultValue); + } + foreach (var key in MaterialKeys) + { + w.Write(key.Id); + w.Write(key.DefaultValue); + } + foreach (var key in SubViewKeys) + { + w.Write(key.DefaultValue); + } - WriteUInt32PairArray(w, UnknownA); - WriteUInt32PairArray(w, UnknownB); - WriteUInt32PairArray(w, UnknownC); + foreach (var node in Nodes) + { + if (node.PassIndices.Length != 16 || node.SystemKeys.Length != SystemKeys.Length || node.SceneKeys.Length != SceneKeys.Length || node.MaterialKeys.Length != MaterialKeys.Length || node.SubViewKeys.Length != SubViewKeys.Length) + { + throw new InvalidDataException(); + } + w.Write(node.Id); + w.Write(node.Passes.Length); + w.Write(node.PassIndices); + foreach (var key in node.SystemKeys) + { + w.Write(key); + } + foreach (var key in node.SceneKeys) + { + w.Write(key); + } + foreach (var key in node.MaterialKeys) + { + w.Write(key); + } + foreach (var key in node.SubViewKeys) + { + w.Write(key); + } + foreach (var pass in node.Passes) + { + w.Write(pass.Id); + w.Write(pass.VertexShader); + w.Write(pass.PixelShader); + } + } + + foreach (var item in Items) + { + w.Write(item.Id); + w.Write(item.Node); + } w.Write(AdditionalData); @@ -63,7 +121,7 @@ public partial class ShpkFile blobs.WriteTo(stream); var stringsOffset = (int)stream.Position; - Strings.Data.WriteTo(stream); + strings.Data.WriteTo(stream); var fileSize = (int)stream.Position; @@ -102,13 +160,12 @@ public partial class ShpkFile w.Write(blobSize); w.Write((ushort)shader.Constants.Length); w.Write((ushort)shader.Samplers.Length); - w.Write((ushort)shader.UnknownX.Length); - w.Write((ushort)shader.UnknownY.Length); + w.Write((ushort)shader.UAVs.Length); + w.Write((ushort)0); WriteResourceArray(w, shader.Constants, strings); WriteResourceArray(w, shader.Samplers, strings); - WriteResourceArray(w, shader.UnknownX, strings); - WriteResourceArray(w, shader.UnknownY, strings); + WriteResourceArray(w, shader.UAVs, strings); } } diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs index ae25a7c5..12192a43 100644 --- a/Penumbra.GameData/Files/ShpkFile.cs +++ b/Penumbra.GameData/Files/ShpkFile.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Lumina.Data.Parsing; using Lumina.Extensions; using Lumina.Misc; using Penumbra.GameData.Data; @@ -33,8 +34,7 @@ public partial class ShpkFile : IWritable public DXVersion DirectXVersion; public Resource[] Constants; public Resource[] Samplers; - public Resource[] UnknownX; - public Resource[] UnknownY; + public Resource[] UAVs; public byte[] AdditionalHeader; private byte[] _blob; private DisassembledShader? _disassembly; @@ -111,6 +111,16 @@ public partial class ShpkFile : IWritable return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); } + public Resource? GetUAVById(uint id) + { + return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetUAVByName(string name) + { + return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + public void UpdateResources(ShpkFile file) { if (_disassembly == null) @@ -119,39 +129,56 @@ public partial class ShpkFile : IWritable } var constants = new List(); var samplers = new List(); + var uavs = new List(); foreach (var binding in _disassembly.ResourceBindings) { switch (binding.Type) { case DisassembledShader.ResourceType.ConstantBuffer: var name = NormalizeResourceName(binding.Name); - // We want to preserve IDs as much as possible, and to deterministically generate new ones, to maximize compatibility. - var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name); + // We want to preserve IDs as much as possible, and to deterministically generate new ones in a way that's most compliant with the native ones, to maximize compatibility. + var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); constants.Add(new Resource { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.RegisterCount, - Used = binding.Used, + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.RegisterCount, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, }); break; case DisassembledShader.ResourceType.Texture: name = NormalizeResourceName(binding.Name); - id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name); + id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); samplers.Add(new Resource { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.Slot, - Used = binding.Used, + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.Slot, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, + }); + break; + case DisassembledShader.ResourceType.UAV: + name = NormalizeResourceName(binding.Name); + id = GetUAVByName(name)?.Id ?? file.GetUAVByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); + uavs.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.Slot, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, }); break; } } Constants = constants.ToArray(); Samplers = samplers.ToArray(); + UAVs = uavs.ToArray(); } private void UpdateUsed() @@ -160,6 +187,7 @@ public partial class ShpkFile : IWritable { var cbUsage = new Dictionary(); var tUsage = new Dictionary(); + var uUsage = new Dictionary(); foreach (var binding in _disassembly.ResourceBindings) { switch (binding.Type) @@ -170,39 +198,36 @@ public partial class ShpkFile : IWritable case DisassembledShader.ResourceType.Texture: tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); break; + case DisassembledShader.ResourceType.UAV: + uUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; } } - for (var i = 0; i < Constants.Length; ++i) + static void CopyUsed(Resource[] resources, Dictionary used) { - if (cbUsage.TryGetValue(Constants[i].Name, out var usage)) + for (var i = 0; i < resources.Length; ++i) { - Constants[i].Used = usage.Item1; - Constants[i].UsedDynamically = usage.Item2; - } - else - { - Constants[i].Used = null; - Constants[i].UsedDynamically = null; - } - } - for (var i = 0; i < Samplers.Length; ++i) - { - if (tUsage.TryGetValue(Samplers[i].Name, out var usage)) - { - Samplers[i].Used = usage.Item1; - Samplers[i].UsedDynamically = usage.Item2; - } - else - { - Samplers[i].Used = null; - Samplers[i].UsedDynamically = null; + if (used.TryGetValue(resources[i].Name, out var usage)) + { + resources[i].Used = usage.Item1; + resources[i].UsedDynamically = usage.Item2; + } + else + { + resources[i].Used = null; + resources[i].UsedDynamically = null; + } } } + CopyUsed(Constants, cbUsage); + CopyUsed(Samplers, tUsage); + CopyUsed(UAVs, uUsage); } else { ClearUsed(Constants); ClearUsed(Samplers); + ClearUsed(UAVs); } } @@ -231,6 +256,37 @@ public partial class ShpkFile : IWritable public ushort ByteSize; } + public struct Pass + { + public uint Id; + public uint VertexShader; + public uint PixelShader; + } + + public struct Key + { + public uint Id; + public uint DefaultValue; + public uint[] Values; + } + + public struct Node + { + public uint Id; + public byte[] PassIndices; + public uint[] SystemKeys; + public uint[] SceneKeys; + public uint[] MaterialKeys; + public uint[] SubViewKeys; + public Pass[] Passes; + } + + public struct Item + { + public uint Id; + public uint Node; + } + public class StringPool { public MemoryStream Data; @@ -317,7 +373,7 @@ public partial class ShpkFile : IWritable public const uint MaterialParamsConstantId = 0x64D12851u; - public uint Unknown1; + public uint Version; public DXVersion DirectXVersion; public Shader[] VertexShaders; public Shader[] PixelShaders; @@ -325,15 +381,14 @@ public partial class ShpkFile : IWritable public MaterialParam[] MaterialParams; public Resource[] Constants; public Resource[] Samplers; - public (uint, uint)[] UnknownA; - public (uint, uint)[] UnknownB; - public (uint, uint)[] UnknownC; - public uint Unknown2; - public uint Unknown3; - public uint Unknown4; - public (uint, uint, uint) Unknowns; + public Resource[] UAVs; + public Key[] SystemKeys; + public Key[] SceneKeys; + public Key[] MaterialKeys; + public Key[] SubViewKeys; + public Node[] Nodes; + public Item[] Items; public byte[] AdditionalData; - public StringPool Strings; // Cannot be safely discarded yet, we don't know if AdditionalData references it public bool Valid { get; private set; } private bool _changed; @@ -363,6 +418,41 @@ public partial class ShpkFile : IWritable return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); } + public Resource? GetUAVById(uint id) + { + return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); + } + + public Resource? GetUAVByName(string name) + { + return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); + } + + public Key? GetSystemKeyById(uint id) + { + return SystemKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); + } + + public Key? GetSceneKeyById(uint id) + { + return SceneKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); + } + + public Key? GetMaterialKeyById(uint id) + { + return MaterialKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); + } + + public Node? GetNodeById(uint id) + { + return Nodes.Select(node => new Node?(node)).FirstOrDefault(node => node!.Value.Id == id); + } + + public Item? GetItemById(uint id) + { + return Items.Select(item => new Item?(item)).FirstOrDefault(item => item!.Value.Id == id); + } + // Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit public ShpkFile(byte[] data) : this(data, false) @@ -378,7 +468,7 @@ public partial class ShpkFile : IWritable { throw new InvalidDataException(); } - Unknown1 = r.ReadUInt32(); + Version = r.ReadUInt32(); DirectXVersion = r.ReadUInt32() switch { DX9Magic => DXVersion.DirectX9, @@ -397,40 +487,59 @@ public partial class ShpkFile : IWritable var materialParamCount = r.ReadUInt32(); var constantCount = r.ReadUInt32(); var samplerCount = r.ReadUInt32(); - var unknownACount = r.ReadUInt32(); - var unknownBCount = r.ReadUInt32(); - var unknownCCount = r.ReadUInt32(); - Unknown2 = r.ReadUInt32(); - Unknown3 = r.ReadUInt32(); - Unknown4 = r.ReadUInt32(); + var uavCount = r.ReadUInt32(); + var systemKeyCount = r.ReadUInt32(); + var sceneKeyCount = r.ReadUInt32(); + var materialKeyCount = r.ReadUInt32(); + var nodeCount = r.ReadUInt32(); + var itemCount = r.ReadUInt32(); var blobs = new ReadOnlySpan(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset)); - Strings = new StringPool(new ReadOnlySpan(data, (int)stringsOffset, (int)(data.Length - stringsOffset))); + var strings = new StringPool(new ReadOnlySpan(data, (int)stringsOffset, (int)(data.Length - stringsOffset))); - VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, Strings); - PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, Strings); + VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, strings); + PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, strings); MaterialParams = r.ReadStructuresAsArray((int)materialParamCount); - Constants = ReadResourceArray(r, (int)constantCount, Strings); - Samplers = ReadResourceArray(r, (int)samplerCount, Strings); + Constants = ReadResourceArray(r, (int)constantCount, strings); + Samplers = ReadResourceArray(r, (int)samplerCount, strings); + UAVs = ReadResourceArray(r, (int)uavCount, strings); - var unk1 = r.ReadUInt32(); - var unk2 = r.ReadUInt32(); - var unk3 = r.ReadUInt32(); - Unknowns = (unk1, unk2, unk3); + SystemKeys = ReadKeyArray(r, (int)systemKeyCount); + SceneKeys = ReadKeyArray(r, (int)sceneKeyCount); + MaterialKeys = ReadKeyArray(r, (int)materialKeyCount); - UnknownA = ReadUInt32PairArray(r, (int)unknownACount); - UnknownB = ReadUInt32PairArray(r, (int)unknownBCount); - UnknownC = ReadUInt32PairArray(r, (int)unknownCCount); + var subViewKey1Default = r.ReadUInt32(); + var subViewKey2Default = r.ReadUInt32(); - AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); + SubViewKeys = new Key[] { + new Key + { + Id = 1, + DefaultValue = subViewKey1Default, + Values = Array.Empty(), + }, + new Key + { + Id = 2, + DefaultValue = subViewKey2Default, + Values = Array.Empty(), + }, + }; + + Nodes = ReadNodeArray(r, (int)nodeCount, SystemKeys.Length, SceneKeys.Length, MaterialKeys.Length, SubViewKeys.Length); + Items = r.ReadStructuresAsArray((int)itemCount); + + AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); // This should be empty, but just in case. if (disassemble) { UpdateUsed(); } + UpdateKeyValues(); + Valid = true; _changed = false; } @@ -439,11 +548,12 @@ public partial class ShpkFile : IWritable { var constants = new Dictionary(); var samplers = new Dictionary(); - static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, bool isSamplers) + var uavs = new Dictionary(); + static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, DisassembledShader.ResourceType type) { foreach (var resource in shaderResources) { - if (resources.TryGetValue(resource.Id, out var carry) && isSamplers) + if (resources.TryGetValue(resource.Id, out var carry) && type != DisassembledShader.ResourceType.ConstantBuffer) { continue; } @@ -452,8 +562,8 @@ public partial class ShpkFile : IWritable { Id = resource.Id, Name = resource.Name, - Slot = existing?.Slot ?? (isSamplers ? (ushort)2 : (ushort)65535), - Size = isSamplers ? (existing?.Size ?? 0) : Math.Max(carry.Size, resource.Size), + Slot = existing?.Slot ?? (type == DisassembledShader.ResourceType.ConstantBuffer ? (ushort)65535 : (ushort)2), + Size = type == DisassembledShader.ResourceType.ConstantBuffer ? Math.Max(carry.Size, resource.Size) : (existing?.Size ?? 0), Used = null, UsedDynamically = null, }; @@ -461,16 +571,19 @@ public partial class ShpkFile : IWritable } foreach (var shader in VertexShaders) { - CollectResources(constants, shader.Constants, GetConstantById, false); - CollectResources(samplers, shader.Samplers, GetSamplerById, true); + CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); + CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); + CollectResources(uavs, shader.UAVs, GetUAVById, DisassembledShader.ResourceType.UAV); } foreach (var shader in PixelShaders) { - CollectResources(constants, shader.Constants, GetConstantById, false); - CollectResources(samplers, shader.Samplers, GetSamplerById, true); + CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); + CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); + CollectResources(uavs, shader.UAVs, GetUAVById, DisassembledShader.ResourceType.UAV); } Constants = constants.Values.ToArray(); Samplers = samplers.Values.ToArray(); + UAVs = uavs.Values.ToArray(); UpdateUsed(); MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4; foreach (var param in MaterialParams) @@ -484,7 +597,8 @@ public partial class ShpkFile : IWritable { var cUsage = new Dictionary(); var sUsage = new Dictionary(); - static void CollectUsage(Dictionary usage, Resource[] resources) + var uUsage = new Dictionary(); + static void CollectUsed(Dictionary usage, Resource[] resources) { foreach (var resource in resources) { @@ -502,42 +616,75 @@ public partial class ShpkFile : IWritable usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0)); } } + static void CopyUsed(Resource[] resources, Dictionary used) + { + for (var i = 0; i < resources.Length; ++i) + { + if (used.TryGetValue(resources[i].Id, out var usage)) + { + resources[i].Used = usage.Item1; + resources[i].UsedDynamically = usage.Item2; + } + else + { + resources[i].Used = null; + resources[i].UsedDynamically = null; + } + } + } foreach (var shader in VertexShaders) { - CollectUsage(cUsage, shader.Constants); - CollectUsage(sUsage, shader.Samplers); + CollectUsed(cUsage, shader.Constants); + CollectUsed(sUsage, shader.Samplers); + CollectUsed(uUsage, shader.UAVs); } foreach (var shader in PixelShaders) { - CollectUsage(cUsage, shader.Constants); - CollectUsage(sUsage, shader.Samplers); + CollectUsed(cUsage, shader.Constants); + CollectUsed(sUsage, shader.Samplers); + CollectUsed(uUsage, shader.UAVs); } - for (var i = 0; i < Constants.Length; ++i) + CopyUsed(Constants, cUsage); + CopyUsed(Samplers, sUsage); + CopyUsed(UAVs, uUsage); + } + + public void UpdateKeyValues() + { + static HashSet[] InitializeValueSet(Key[] keys) + => Array.ConvertAll(keys, key => new HashSet() + { + key.DefaultValue, + }); + static void CollectValues(HashSet[] valueSets, uint[] values) { - if (cUsage.TryGetValue(Constants[i].Id, out var usage)) + for (var i = 0; i < valueSets.Length; ++i) { - Constants[i].Used = usage.Item1; - Constants[i].UsedDynamically = usage.Item2; - } - else - { - Constants[i].Used = null; - Constants[i].UsedDynamically = null; + valueSets[i].Add(values[i]); } } - for (var i = 0; i < Samplers.Length; ++i) + static void CopyValues(Key[] keys, HashSet[] valueSets) { - if (sUsage.TryGetValue(Samplers[i].Id, out var usage)) + for (var i = 0; i < keys.Length; ++i) { - Samplers[i].Used = usage.Item1; - Samplers[i].UsedDynamically = usage.Item2; - } - else - { - Samplers[i].Used = null; - Samplers[i].UsedDynamically = null; + keys[i].Values = valueSets[i].ToArray(); } } + var systemKeyValues = InitializeValueSet(SystemKeys); + var sceneKeyValues = InitializeValueSet(SceneKeys); + var materialKeyValues = InitializeValueSet(MaterialKeys); + var subViewKeyValues = InitializeValueSet(SubViewKeys); + foreach (var node in Nodes) + { + CollectValues(systemKeyValues, node.SystemKeys); + CollectValues(sceneKeyValues, node.SceneKeys); + CollectValues(materialKeyValues, node.MaterialKeys); + CollectValues(subViewKeyValues, node.SubViewKeys); + } + CopyValues(SystemKeys, systemKeyValues); + CopyValues(SceneKeys, sceneKeyValues); + CopyValues(MaterialKeys, materialKeyValues); + CopyValues(SubViewKeys, subViewKeyValues); } public void SetInvalid() @@ -606,8 +753,11 @@ public partial class ShpkFile : IWritable var blobSize = r.ReadUInt32(); var constantCount = r.ReadUInt16(); var samplerCount = r.ReadUInt16(); - var unknownXCount = r.ReadUInt16(); - var unknownYCount = r.ReadUInt16(); + var uavCount = r.ReadUInt16(); + if (r.ReadUInt16() != 0) + { + throw new NotImplementedException(); + } var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize); @@ -617,8 +767,7 @@ public partial class ShpkFile : IWritable shader.DirectXVersion = directX; shader.Constants = ReadResourceArray(r, constantCount, strings); shader.Samplers = ReadResourceArray(r, samplerCount, strings); - shader.UnknownX = ReadResourceArray(r, unknownXCount, strings); - shader.UnknownY = ReadResourceArray(r, unknownYCount, strings); + shader.UAVs = ReadResourceArray(r, uavCount, strings); shader.AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(); shader.Blob = rawBlob[extraHeaderSize..].ToArray(); @@ -628,15 +777,49 @@ public partial class ShpkFile : IWritable return ret; } - private static (uint, uint)[] ReadUInt32PairArray(BinaryReader r, int count) + private static Key[] ReadKeyArray(BinaryReader r, int count) { - var ret = new (uint, uint)[count]; + var ret = new Key[count]; for (var i = 0; i < count; ++i) { - var first = r.ReadUInt32(); - var second = r.ReadUInt32(); + var id = r.ReadUInt32(); + var defaultValue = r.ReadUInt32(); - ret[i] = (first, second); + ret[i] = new Key + { + Id = id, + DefaultValue = defaultValue, + Values = Array.Empty(), + }; + } + + return ret; + } + + private static Node[] ReadNodeArray(BinaryReader r, int count, int systemKeyCount, int sceneKeyCount, int materialKeyCount, int subViewKeyCount) + { + var ret = new Node[count]; + for (var i = 0; i < count; ++i) + { + var id = r.ReadUInt32(); + var passCount = r.ReadUInt32(); + var passIndices = r.ReadBytes(16); + var systemKeys = r.ReadStructuresAsArray(systemKeyCount); + var sceneKeys = r.ReadStructuresAsArray(sceneKeyCount); + var materialKeys = r.ReadStructuresAsArray(materialKeyCount); + var subViewKeys = r.ReadStructuresAsArray(subViewKeyCount); + var passes = r.ReadStructuresAsArray((int)passCount); + + ret[i] = new Node + { + Id = id, + PassIndices = passIndices, + SystemKeys = systemKeys, + SceneKeys = sceneKeys, + MaterialKeys = materialKeys, + SubViewKeys = subViewKeys, + Passes = passes, + }; } return ret; diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index d0a0c4bb..41e513d6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -10,14 +10,13 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; -using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.String.Functions; using Penumbra.Util; -using static OtterGui.Raii.ImRaii; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; @@ -27,6 +26,7 @@ public partial class ModEditWindow private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + private uint _materialNewKeyId = 0; private uint _materialNewConstantId = 0; private uint _materialNewSamplerId = 0; @@ -191,48 +191,165 @@ public partial class ModEditWindow ret = true; } ImRaii.TreeNode( $"Has associated ShPk file (for advanced editing): {( file.AssociatedShpk != null ? "Yes" : "No" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - if( !disabled && ImGui.Button( "Associate modded ShPk file" ) ) + if( !disabled ) { - _materialFileDialog.OpenFileDialog( $"Associate modded ShPk file...", ".shpk", ( success, name ) => + if( ImGui.Button( "Associate custom ShPk file" ) ) { - if( !success ) + _materialFileDialog.OpenFileDialog( $"Associate custom ShPk file...", ".shpk", ( success, name ) => { - return; - } + if( !success ) + { + return; + } - try + try + { + file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + ImGui.SameLine(); + if( ImGui.Button( "Associate default ShPk file" ) ) + { + var shpk = LoadAssociatedShpk( file.ShaderPackage.Name ); + if( null != shpk ) { - file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); + file.AssociatedShpk = shpk; + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Success ); } - catch( Exception e ) + else { - Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; + ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); } - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - } ); + } } - if( file.ShaderPackage.ShaderKeys.Length > 0 ) + if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) { using var t = ImRaii.TreeNode( "Shader Keys" ); if( t ) { + var definedKeys = new HashSet< uint >(); + foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"Shader Key #{idx}", file.ShaderPackage.ShaderKeys.Length == 1 ? ImGuiTreeNodeFlags.DefaultOpen : 0 ); + definedKeys.Add( key.Category ); + using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); if( t2 ) { - ImRaii.TreeNode( $"Category: 0x{key.Category:X8} ({key.Category})", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Value: 0x{key.Value:X8} ({key.Value})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + if( !disabled ) + { + var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); + if( shpkKey.HasValue ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); + if( c ) + { + foreach( var value in shpkKey.Value.Values ) + { + if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) + { + file.ShaderPackage.ShaderKeys[idx].Value = value; + ret = true; + } + } + } + } + if( ImGui.Button( "Remove Key" ) ) + { + ArrayRemove( ref file.ShaderPackage.ShaderKeys, idx ); + ret = true; + } + } + } + } + + if( !disabled && file.AssociatedShpk != null ) + { + var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); + if( missingKeys.Length > 0 ) + { + var selectedKey = Array.Find( missingKeys, key => key.Id == _materialNewKeyId ); + if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) + { + selectedKey = missingKeys[0]; + _materialNewKeyId = selectedKey.Id; + } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) + { + if( c ) + { + foreach( var key in missingKeys ) + { + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _materialNewKeyId ) ) + { + selectedKey = key; + _materialNewKeyId = key.Id; + } + } + } + } + ImGui.SameLine(); + if( ImGui.Button( "Add Key" ) ) + { + ArrayAdd( ref file.ShaderPackage.ShaderKeys, new ShaderKey + { + Category = selectedKey.Id, + Value = selectedKey.DefaultValue, + } ); + ret = true; + } } } } } + if( file.AssociatedShpk != null ) + { + var definedKeys = new Dictionary< uint, uint >(); + foreach( var key in file.ShaderPackage.ShaderKeys ) + { + definedKeys[key.Category] = key.Value; + } + var materialKeys = Array.ConvertAll(file.AssociatedShpk.MaterialKeys, key => + { + if( definedKeys.TryGetValue( key.Id, out var value ) ) + { + return value; + } + else + { + return key.DefaultValue; + } + } ); + var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); + var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); + foreach( var node in file.AssociatedShpk.Nodes ) + { + if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) + { + foreach( var pass in node.Passes ) + { + vertexShaders.Add( ( int )pass.VertexShader ); + pixelShaders.Add( ( int )pass.PixelShader ); + } + } + } + ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0 - || file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) { var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); @@ -352,7 +469,7 @@ public partial class ModEditWindow foreach( var constant in missingConstants ) { var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})" ) ) + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _materialNewConstantId ) ) { selectedConstant = constant; _materialNewConstantId = constant.Id; @@ -378,7 +495,7 @@ public partial class ModEditWindow } if( file.ShaderPackage.Samplers.Length > 0 || file.Textures.Length > 0 - || file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) { using var t = ImRaii.TreeNode( "Samplers" ); if( t ) @@ -472,7 +589,7 @@ public partial class ModEditWindow { foreach( var sampler in missingSamplers ) { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})" ) ) + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _materialNewSamplerId ) ) { selectedSampler = sampler; _materialNewSamplerId = sampler.Id; diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 4facc34f..cb3aaf3c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -14,6 +14,7 @@ using Penumbra.GameData.Files; using Penumbra.Util; using Lumina.Data.Parsing; using static OtterGui.Raii.ImRaii; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; @@ -76,7 +77,7 @@ public partial class ModEditWindow using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); if( t ) { - if( ImGui.Button( $"Export Shader Blob ({shader.Blob.Length} bytes)" ) ) + if( ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) { var extension = file.DirectXVersion switch { @@ -86,7 +87,7 @@ public partial class ModEditWindow }; var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); var blob = shader.Blob; - _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Blob to...", extension, defaultName, extension, ( success, name ) => + _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) => { if( !success ) { @@ -103,15 +104,15 @@ public partial class ModEditWindow ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } - ChatUtil.NotificationMessage( $"Shader Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); } ); } if( !disabled ) { ImGui.SameLine(); - if( ImGui.Button( "Replace Shader Blob" ) ) + if( ImGui.Button( "Replace Shader Program Blob" ) ) { - _shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Blob...", "Shader Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + _shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => { if( !success ) { @@ -146,10 +147,9 @@ public partial class ModEditWindow } } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unknown Type X Resources", "slot", true, shader.UnknownX, disabled ); - ret |= DrawShaderPackageResourceArray( "Unknown Type Y Resources", "slot", true, shader.UnknownY, disabled ); + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); + ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.UAVs, true ); if( shader.AdditionalHeader.Length > 0 ) { @@ -160,7 +160,7 @@ public partial class ModEditWindow } } - using( var t2 = ImRaii.TreeNode( "Raw Disassembly" ) ) + using( var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ) ) { if( t2 ) { @@ -326,7 +326,7 @@ public partial class ModEditWindow foreach( var start in starts ) { var name = MaterialParamName( false, start )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}" ) ) + if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", start == _shaderPackageNewMaterialParamStart ) ) { _shaderPackageNewMaterialParamStart = ( ushort )start; } @@ -352,7 +352,7 @@ public partial class ModEditWindow foreach( var end in ends ) { var name = MaterialParamName( false, end )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}" ) ) + if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", end == _shaderPackageNewMaterialParamEnd ) ) { _shaderPackageNewMaterialParamEnd = ( ushort )end; } @@ -390,7 +390,7 @@ public partial class ModEditWindow return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool _ ) + private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool disabled ) { if( resources.Length == 0 ) { @@ -407,65 +407,85 @@ public partial class ModEditWindow foreach( var (buf, idx) in resources.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + ( withSize ? $", size: {buf.Size} registers" : string.Empty ), ( buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf ); + using var t2 = ImRaii.TreeNode( $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ), ( !disabled || buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf ); if( t2 ) { - var used = new List< string >(); - if( withSize ) + if( !disabled ) { - foreach( var (components, i) in ( buf.Used ?? Array.Empty() ).WithIndex() ) + // FIXME this probably doesn't belong here + static unsafe bool InputUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) { - switch( components ) + fixed( ushort* v2 = &v ) + { + return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%hu", flags ); + } + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( InputUInt16( $"{char.ToUpper( slotLabel[0] )}{slotLabel[1..].ToLower()}", ref resources[idx].Slot, ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + if( buf.Used != null ) + { + var used = new List(); + if( withSize ) + { + foreach( var (components, i) in ( buf.Used ?? Array.Empty() ).WithIndex() ) + { + switch( components ) + { + case 0: + break; + case DisassembledShader.VectorComponents.All: + used.Add( $"[{i}]" ); + break; + default: + used.Add( $"[{i}].{new string( components.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); + break; + } + } + switch( buf.UsedDynamically ?? 0 ) { case 0: break; case DisassembledShader.VectorComponents.All: - used.Add( $"[{i}]" ); + used.Add( "[*]" ); break; default: - used.Add( $"[{i}].{new string( components.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); + used.Add( $"[*].{new string( buf.UsedDynamically!.Value.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); break; } } - switch( buf.UsedDynamically ?? 0 ) + else { - case 0: - break; - case DisassembledShader.VectorComponents.All: - used.Add( "[*]" ); - break; - default: - used.Add( $"[*].{new string( buf.UsedDynamically!.Value.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); - break; + var components = ( ( buf.Used != null && buf.Used.Length > 0 ) ? buf.Used[0] : 0 ) | ( buf.UsedDynamically ?? 0 ); + if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) + { + used.Add( "Red" ); + } + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) + { + used.Add( "Green" ); + } + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) + { + used.Add( "Blue" ); + } + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) + { + used.Add( "Alpha" ); + } } - } - else - { - var components = ( ( buf.Used != null && buf.Used.Length > 0 ) ? buf.Used[0] : 0 ) | (buf.UsedDynamically ?? 0); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) + if( used.Count > 0 ) { - used.Add( "Red" ); + ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) + else { - used.Add( "Green" ); + ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf ).Dispose(); } - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - used.Add( "Blue" ); - } - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - used.Add( "Alpha" ); - } - } - if( used.Count > 0 ) - { - ImRaii.TreeNode( $"Used: {string.Join(", ", used)}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf ).Dispose(); } } } @@ -482,56 +502,91 @@ public partial class ModEditWindow return false; } + ImRaii.TreeNode( $"Version: 0x{file.Version:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled ); ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, file.UAVs, disabled ); - if( file.UnknownA.Length > 0 ) + static bool DrawKeyArray( string arrayName, bool withId, ShpkFile.Key[] keys, bool _ ) { - using var t = ImRaii.TreeNode( $"Unknown Type A Structures ({file.UnknownA.Length})" ); + if( keys.Length == 0 ) + { + return false; + } + + using var t = ImRaii.TreeNode( arrayName ); + if( !t ) + { + return false; + } + + foreach( var (key, idx) in keys.WithIndex() ) + { + using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); + if( t2 ) + { + ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + return false; + } + + ret |= DrawKeyArray( "System Keys", true, file.SystemKeys, disabled ); + ret |= DrawKeyArray( "Scene Keys", true, file.SceneKeys, disabled ); + ret |= DrawKeyArray( "Material Keys", true, file.MaterialKeys, disabled ); + ret |= DrawKeyArray( "Sub-View Keys", false, file.SubViewKeys, disabled ); + + if( file.Nodes.Length > 0 ) + { + using var t = ImRaii.TreeNode( $"Nodes ({file.Nodes.Length})" ); if( t ) { - foreach( var (unk, idx) in file.UnknownA.WithIndex() ) + foreach( var (node, idx) in file.Nodes.WithIndex() ) { - ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + using var t2 = ImRaii.TreeNode( $"#{idx}: ID: 0x{node.Id:X8}" ); + if( t2 ) + { + foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) + { + ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + { + ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } } } } - if( file.UnknownB.Length > 0 ) + if( file.Items.Length > 0 ) { - using var t = ImRaii.TreeNode( $"Unknown Type B Structures ({file.UnknownB.Length})" ); + using var t = ImRaii.TreeNode( $"Items ({file.Items.Length})" ); if( t ) { - foreach( var (unk, idx) in file.UnknownB.WithIndex() ) + foreach( var (item, idx) in file.Items.WithIndex() ) { - ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#{idx}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } } } - if( file.UnknownC.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Unknown Type C Structures ({file.UnknownC.Length})" ); - if( t ) - { - foreach( var (unk, idx) in file.UnknownC.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - - using( var t = ImRaii.TreeNode( $"Misc. Unknown Fields" ) ) - { - if( t ) - { - ImRaii.TreeNode( $"#1 (at 0x0004): 0x{file.Unknown1:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"#2 (at 0x003C): 0x{file.Unknown2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"#3 (at 0x0040): 0x{file.Unknown3:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"#4 (at 0x0044): 0x{file.Unknown4:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - if( file.AdditionalData.Length > 0 ) { using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); @@ -541,17 +596,6 @@ public partial class ModEditWindow } } - using( var t = ImRaii.TreeNode( $"String Pool" ) ) - { - if( t ) - { - foreach( var offset in file.Strings.StartingOffsets ) - { - ImGui.Text( file.Strings.GetNullTerminatedString( offset ) ); - } - } - } - return ret; } } \ No newline at end of file From 86de28245d8ffbcad5f5ac55fe8e48abf7cc7f32 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 16 Feb 2023 19:15:40 +0100 Subject: [PATCH 0760/2451] Auto-generate ID from a name through CRC when adding a param --- .../Classes/ModEditWindow.ShaderPackages.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index cb3aaf3c..c117c0e6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -7,14 +7,12 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; using ImGuiNET; +using Lumina.Misc; using OtterGui.Raii; using OtterGui; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Util; -using Lumina.Data.Parsing; -using static OtterGui.Raii.ImRaii; -using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; @@ -24,7 +22,8 @@ public partial class ModEditWindow private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); - private uint _shaderPackageNewMaterialParamId = 0; + private string _shaderPackageNewMaterialParamName = string.Empty; + private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); private ushort _shaderPackageNewMaterialParamStart = 0; private ushort _shaderPackageNewMaterialParamEnd = 0; @@ -254,7 +253,7 @@ public partial class ModEditWindow for( var idx = 0; idx < parameters.Length; idx += 4 ) { - var usedComponents = materialParams?.Used?[idx >> 2] ?? DisassembledShader.VectorComponents.All; + var usedComponents = ( materialParams?.Used?[idx >> 2] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); ImGui.TableNextColumn(); ImGui.Text( $"[{idx >> 2}]" ); for( var col = 0; col < 4; ++col ) @@ -297,7 +296,7 @@ public partial class ModEditWindow for( var i = 0; i < file.MaterialParams.Length; ++i ) { var param = file.MaterialParams[i]; - using var t2 = ImRaii.TreeNode( $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})" ); + using var t2 = ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); if( t2 ) { if( ImGui.Button( "Remove" ) ) @@ -359,12 +358,13 @@ public partial class ModEditWindow } } } - var id = ( int )_shaderPackageNewMaterialParamId; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "ID", ref id, 0, 0, ImGuiInputTextFlags.CharsHexadecimal ) ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); + if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) { - _shaderPackageNewMaterialParamId = ( uint )id; + _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); } + ImGui.SameLine(); + ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); if( ImGui.Button( "Add" ) ) { if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) From e058b6e32b3777595e3332b39984aef2cdf2a45a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 19 Feb 2023 05:03:34 +0100 Subject: [PATCH 0761/2451] Fix wrong error message --- Penumbra.GameData/Files/ShpkFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs index 12192a43..ebcd2e58 100644 --- a/Penumbra.GameData/Files/ShpkFile.cs +++ b/Penumbra.GameData/Files/ShpkFile.cs @@ -74,7 +74,7 @@ public partial class ShpkFile : IWritable } if (samplers.Count != textures.Count || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName)) { - throw new ArgumentException($"The supplied blob has inconsistent shader and texture allocation."); + throw new ArgumentException($"The supplied blob has inconsistent sampler and texture allocation."); } } _blob = value; From 1e471551d43ed70c0273f0d445a8c87384c51c27 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 14:44:37 +0100 Subject: [PATCH 0762/2451] Move IndexSet to OtterGui --- OtterGui | 2 +- Penumbra/Util/IndexSet.cs | 110 -------------------------------------- 2 files changed, 1 insertion(+), 111 deletions(-) delete mode 100644 Penumbra/Util/IndexSet.cs diff --git a/OtterGui b/OtterGui index f033fc9c..ebaedd64 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f033fc9c103b8a07398481cbff00b0ad3ea749e2 +Subproject commit ebaedd64ed28032e4c9bc34c0c1ec3488b2f0937 diff --git a/Penumbra/Util/IndexSet.cs b/Penumbra/Util/IndexSet.cs deleted file mode 100644 index e0ed7921..00000000 --- a/Penumbra/Util/IndexSet.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Penumbra.Util; - -public class IndexSet : IEnumerable -{ - private readonly BitArray _set; - private int _count; - - public int Capacity => _set.Count; - - public int Count => _count; - - public bool this[Index index] - { - get => _set[index]; - set - { - if( value ) - { - Add( index ); - } - else - { - Remove( index ); - } - } - } - - public IndexSet( int capacity, bool initiallyFull ) - { - _set = new BitArray( capacity, initiallyFull ); - _count = initiallyFull ? capacity : 0; - } - - public bool Add( Index index ) - { - var ret = !_set[index]; - if( ret ) - { - ++_count; - _set[index] = true; - } - return ret; - } - - public bool Remove( Index index ) - { - var ret = _set[index]; - if( ret ) - { - --_count; - _set[index] = false; - } - return ret; - } - - public int AddRange( int offset, int length ) - { - var ret = 0; - for( var idx = 0; idx < length; ++idx ) - { - if( Add( offset + idx ) ) - { - ++ret; - } - } - return ret; - } - - public int RemoveRange( int offset, int length ) - { - var ret = 0; - for( var idx = 0; idx < length; ++idx ) - { - if( Remove( offset + idx ) ) - { - ++ret; - } - } - return ret; - } - - public IEnumerator GetEnumerator() - { - if( _count > 0 ) - { - var capacity = _set.Count; - var remaining = _count; - for( var i = 0; i < capacity; ++i ) - { - if( _set[i] ) - { - yield return i; - if( --remaining == 0 ) - { - yield break; - } - } - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } -} From a2b62a8b6a65e92424af78afe9baac752c436635 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 14:48:46 +0100 Subject: [PATCH 0763/2451] Some formatting and naming changes, splitting files and some minor improvements. --- Penumbra.GameData/Data/DisassembledShader.cs | 229 +++--- .../Files/MtrlFile.ColorDyeSet.cs | 90 +++ Penumbra.GameData/Files/MtrlFile.ColorSet.cs | 135 ++++ Penumbra.GameData/Files/MtrlFile.Write.cs | 1 - Penumbra.GameData/Files/MtrlFile.cs | 321 ++------ Penumbra.GameData/Files/ShpkFile.Shader.cs | 220 +++++ .../Files/ShpkFile.StringPool.cs | 79 ++ Penumbra.GameData/Files/ShpkFile.Write.cs | 57 +- Penumbra.GameData/Files/ShpkFile.cs | 763 +++++------------- Penumbra.GameData/UtilityFunctions.cs | 13 + .../UI/Classes/ModEditWindow.FileEditor.cs | 7 +- .../UI/Classes/ModEditWindow.Materials.cs | 1 + .../Classes/ModEditWindow.ShaderPackages.cs | 9 +- 13 files changed, 928 insertions(+), 997 deletions(-) create mode 100644 Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs create mode 100644 Penumbra.GameData/Files/MtrlFile.ColorSet.cs create mode 100644 Penumbra.GameData/Files/ShpkFile.Shader.cs create mode 100644 Penumbra.GameData/Files/ShpkFile.StringPool.cs create mode 100644 Penumbra.GameData/UtilityFunctions.cs diff --git a/Penumbra.GameData/Data/DisassembledShader.cs b/Penumbra.GameData/Data/DisassembledShader.cs index b782bf74..bfae7a74 100644 --- a/Penumbra.GameData/Data/DisassembledShader.cs +++ b/Penumbra.GameData/Data/DisassembledShader.cs @@ -4,23 +4,22 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Penumbra.GameData.Interop; -using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.GameData.Data; -public class DisassembledShader +public partial class DisassembledShader { public struct ResourceBinding { - public string Name; - public ResourceType Type; - public Format Format; - public ResourceDimension Dimension; - public uint Slot; - public uint Elements; - public uint RegisterCount; + public string Name; + public ResourceType Type; + public Format Format; + public ResourceDimension Dimension; + public uint Slot; + public uint Elements; + public uint RegisterCount; public VectorComponents[] Used; - public VectorComponents UsedDynamically; + public VectorComponents UsedDynamically; } // Abbreviated using the uppercased first char of their name @@ -30,7 +29,7 @@ public class DisassembledShader ConstantBuffer = 0x43, // 'C' Sampler = 0x53, // 'S' Texture = 0x54, // 'T' - UAV = 0x55, // 'U' + Uav = 0x55, // 'U' } // Abbreviated using the uppercased first and last char of their name @@ -56,22 +55,22 @@ public class DisassembledShader public struct InputOutput { - public string Name; - public uint Index; + public string Name; + public uint Index; public VectorComponents Mask; - public uint Register; - public string SystemValue; - public Format Format; + public uint Register; + public string SystemValue; + public Format Format; public VectorComponents Used; } [Flags] public enum VectorComponents : byte { - X = 1, - Y = 2, - Z = 4, - W = 8, + X = 1, + Y = 2, + Z = 4, + W = 8, All = 15, } @@ -82,21 +81,31 @@ public class DisassembledShader Vertex = 0x56, // 'V' } - private static readonly Regex ResourceBindingSizeRegex = new(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking); - private static readonly Regex SM3ConstantBufferUsageRegex = new(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking); - private static readonly Regex SM3TextureUsageRegex = new(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking); - private static readonly Regex SM5ConstantBufferUsageRegex = new(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking); - private static readonly Regex SM5TextureUsageRegex = new(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking); - private static readonly char[] Digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + [GeneratedRegex(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking)] + private static partial Regex ResourceBindingSizeRegex(); - public readonly string RawDisassembly; - public readonly uint ShaderModel; - public readonly ShaderStage Stage; - public readonly string BufferDefinitions; + [GeneratedRegex(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking)] + private static partial Regex Sm3ConstantBufferUsageRegex(); + + [GeneratedRegex(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking)] + private static partial Regex Sm3TextureUsageRegex(); + + [GeneratedRegex(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking)] + private static partial Regex Sm5ConstantBufferUsageRegex(); + + [GeneratedRegex(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking)] + private static partial Regex Sm5TextureUsageRegex(); + + private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char) ('0' + c)).ToArray(); + + public readonly string RawDisassembly; + public readonly uint ShaderModel; + public readonly ShaderStage Stage; + public readonly string BufferDefinitions; public readonly ResourceBinding[] ResourceBindings; - public readonly InputOutput[] InputSignature; - public readonly InputOutput[] OutputSignature; - public readonly string[] Instructions; + public readonly InputOutput[] InputSignature; + public readonly InputOutput[] OutputSignature; + public readonly string[] Instructions; public DisassembledShader(string rawDisassembly) { @@ -104,49 +113,38 @@ public class DisassembledShader var lines = rawDisassembly.Split('\n'); Instructions = Array.FindAll(lines, ln => !ln.StartsWith("//") && ln.Length > 0); var shaderModel = Instructions[0].Trim().Split('_'); - Stage = (ShaderStage)(byte)char.ToUpper(shaderModel[0][0]); + Stage = (ShaderStage)(byte)char.ToUpper(shaderModel[0][0]); ShaderModel = (uint.Parse(shaderModel[1]) << 8) | uint.Parse(shaderModel[2]); var header = PreParseHeader(lines.AsSpan()[..Array.IndexOf(lines, Instructions[0])]); switch (ShaderModel >> 8) { case 3: - ParseSM3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); - ParseSM3ResourceUsage(Instructions, ResourceBindings); + ParseSm3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); + ParseSm3ResourceUsage(Instructions, ResourceBindings); break; case 5: - ParseSM5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); - ParseSM5ResourceUsage(Instructions, ResourceBindings); + ParseSm5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); + ParseSm5ResourceUsage(Instructions, ResourceBindings); break; - default: - throw new NotImplementedException(); + default: throw new NotImplementedException(); } } public ResourceBinding? GetResourceBindingByName(ResourceType type, string name) - { - return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Name == name); - } + => ResourceBindings.FirstOrNull(b => b.Type == type && b.Name == name); public ResourceBinding? GetResourceBindingBySlot(ResourceType type, uint slot) - { - return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Slot == slot); - } + => ResourceBindings.FirstOrNull(b => b.Type == type && b.Slot == slot); public static DisassembledShader Disassemble(ReadOnlySpan shaderBlob) - { - return new DisassembledShader(D3DCompiler.Disassemble(shaderBlob)); - } + => new(D3DCompiler.Disassemble(shaderBlob)); - private static void ParseSM3Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) + private static void ParseSm3Header(Dictionary header, out string bufferDefinitions, + out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) { - if (header.TryGetValue("Parameters", out var rawParameters)) - { - bufferDefinitions = string.Join('\n', rawParameters); - } - else - { - bufferDefinitions = string.Empty; - } + bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) + ? string.Join('\n', rawParameters) + : string.Empty; if (header.TryGetValue("Registers", out var rawRegisters)) { var (_, registers) = ParseTable(rawRegisters); @@ -154,10 +152,8 @@ public class DisassembledShader { var type = (ResourceType)(byte)char.ToUpper(register[1][0]); if (type == ResourceType.Sampler) - { type = ResourceType.Texture; - } - uint size = uint.Parse(register[2]); + var size = uint.Parse(register[2]); return new ResourceBinding { Name = register[0], @@ -175,14 +171,15 @@ public class DisassembledShader { resourceBindings = Array.Empty(); } - inputSignature = Array.Empty(); + + inputSignature = Array.Empty(); outputSignature = Array.Empty(); } - private static void ParseSM3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); - var tIndices = new Dictionary(); + var tIndices = new Dictionary(); { var i = 0; foreach (var binding in resourceBindings) @@ -191,14 +188,13 @@ public class DisassembledShader { case ResourceType.ConstantBuffer: for (var j = 0u; j < binding.RegisterCount; j++) - { cbIndices[binding.Slot + j] = i; - } break; case ResourceType.Texture: tIndices[binding.Slot] = i; break; } + ++i; } } @@ -206,43 +202,39 @@ public class DisassembledShader { var trimmed = instruction.Trim(); if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) - { continue; - } - foreach (Match cbMatch in SM3ConstantBufferUsageRegex.Matches(instruction)) + + foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instruction)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) { var swizzle = cbMatch.Groups[3].Success ? ParseVectorComponents(cbMatch.Groups[3].Value) : VectorComponents.All; if (cbMatch.Groups[2].Success) - { resourceBindings[i].UsedDynamically |= swizzle; - } else - { resourceBindings[i].Used[buffer - resourceBindings[i].Slot] |= swizzle; - } } } - var tMatch = SM3TextureUsageRegex.Match(instruction); + + var tMatch = Sm3TextureUsageRegex().Match(instruction); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[1].Value); if (tIndices.TryGetValue(texture, out var i)) - { resourceBindings[i].Used[0] = VectorComponents.All; - } } } } - private static void ParseSM5Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) + private static void ParseSm5Header(Dictionary header, out string bufferDefinitions, + out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) { if (header.TryGetValue("Resource Bindings", out var rawResBindings)) { var (head, resBindings) = ParseTable(rawResBindings); - resourceBindings = Array.ConvertAll(resBindings, binding => { + resourceBindings = Array.ConvertAll(resBindings, binding => + { var type = (ResourceType)(byte)char.ToUpper(binding[1][0]); return new ResourceBinding { @@ -261,10 +253,11 @@ public class DisassembledShader { resourceBindings = Array.Empty(); } + if (header.TryGetValue("Buffer Definitions", out var rawBufferDefs)) { bufferDefinitions = string.Join('\n', rawBufferDefs); - foreach (Match match in ResourceBindingSizeRegex.Matches(bufferDefinitions)) + foreach (Match match in ResourceBindingSizeRegex().Matches(bufferDefinitions)) { var name = match.Groups[1].Value; var bytesSize = uint.Parse(match.Groups[2].Value); @@ -272,7 +265,7 @@ public class DisassembledShader if (pos >= 0) { resourceBindings[pos].RegisterCount = (bytesSize + 0xF) >> 4; - resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount]; + resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount]; } } } @@ -281,30 +274,32 @@ public class DisassembledShader bufferDefinitions = string.Empty; } - static InputOutput ParseInputOutput(string[] inOut) => new() - { - Name = inOut[0], - Index = uint.Parse(inOut[1]), - Mask = ParseVectorComponents(inOut[2]), - Register = uint.Parse(inOut[3]), - SystemValue = string.Intern(inOut[4]), - Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])), - Used = ParseVectorComponents(inOut[6]), - }; + static InputOutput ParseInputOutput(string[] inOut) + => new() + { + Name = inOut[0], + Index = uint.Parse(inOut[1]), + Mask = ParseVectorComponents(inOut[2]), + Register = uint.Parse(inOut[3]), + SystemValue = string.Intern(inOut[4]), + Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])), + Used = ParseVectorComponents(inOut[6]), + }; if (header.TryGetValue("Input signature", out var rawInputSig)) { var (_, inputSig) = ParseTable(rawInputSig); - inputSignature = Array.ConvertAll(inputSig, ParseInputOutput); + inputSignature = Array.ConvertAll(inputSig, ParseInputOutput); } else { inputSignature = Array.Empty(); } + if (header.TryGetValue("Output signature", out var rawOutputSig)) { var (_, outputSig) = ParseTable(rawOutputSig); - outputSignature = Array.ConvertAll(outputSig, ParseInputOutput); + outputSignature = Array.ConvertAll(outputSig, ParseInputOutput); } else { @@ -312,10 +307,10 @@ public class DisassembledShader } } - private static void ParseSM5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); - var tIndices = new Dictionary(); + var tIndices = new Dictionary(); { var i = 0; foreach (var binding in resourceBindings) @@ -329,6 +324,7 @@ public class DisassembledShader tIndices[binding.Slot] = i; break; } + ++i; } } @@ -336,10 +332,9 @@ public class DisassembledShader { var trimmed = instruction.Trim(); if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) - { continue; - } - foreach (Match cbMatch in SM5ConstantBufferUsageRegex.Matches(instruction)) + + foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instruction)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) @@ -348,9 +343,7 @@ public class DisassembledShader if (int.TryParse(cbMatch.Groups[2].Value, out var vector)) { if (vector < resourceBindings[i].Used.Length) - { resourceBindings[i].Used[vector] |= swizzle; - } } else { @@ -358,31 +351,24 @@ public class DisassembledShader } } } - var tMatch = SM5TextureUsageRegex.Match(instruction); + + var tMatch = Sm5TextureUsageRegex().Match(instruction); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[2].Value); if (tIndices.TryGetValue(texture, out var i)) { - var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value); + var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value); var rawInSwizzle = tMatch.Groups[3].Value; - var inSwizzle = new StringBuilder(4); + var inSwizzle = new StringBuilder(4); if ((outSwizzle & VectorComponents.X) != 0) - { inSwizzle.Append(rawInSwizzle[0]); - } if ((outSwizzle & VectorComponents.Y) != 0) - { inSwizzle.Append(rawInSwizzle[1]); - } if ((outSwizzle & VectorComponents.Z) != 0) - { inSwizzle.Append(rawInSwizzle[2]); - } if ((outSwizzle & VectorComponents.W) != 0) - { inSwizzle.Append(rawInSwizzle[3]); - } resourceBindings[i].Used[0] |= ParseVectorComponents(inSwizzle.ToString()); } } @@ -393,9 +379,9 @@ public class DisassembledShader { components = components.ToUpperInvariant(); return (components.Contains('X') ? VectorComponents.X : 0) - | (components.Contains('Y') ? VectorComponents.Y : 0) - | (components.Contains('Z') ? VectorComponents.Z : 0) - | (components.Contains('W') ? VectorComponents.W : 0); + | (components.Contains('Y') ? VectorComponents.Y : 0) + | (components.Contains('Z') ? VectorComponents.Z : 0) + | (components.Contains('W') ? VectorComponents.W : 0); } private static Dictionary PreParseHeader(ReadOnlySpan header) @@ -405,17 +391,13 @@ public class DisassembledShader void AddSection(string name, ReadOnlySpan section) { while (section.Length > 0 && section[0].Length <= 3) - { section = section[1..]; - } while (section.Length > 0 && section[^1].Length <= 3) - { section = section[..^1]; - } sections.Add(name, Array.ConvertAll(section.ToArray(), ln => ln.Length <= 3 ? string.Empty : ln[3..])); } - var lastSectionName = ""; + var lastSectionName = ""; var lastSectionStart = 0; for (var i = 1; i < header.Length - 1; ++i) { @@ -423,11 +405,12 @@ public class DisassembledShader if (header[i - 1].Length <= 3 && header[i + 1].Length <= 3 && (current = header[i].TrimEnd()).EndsWith(':')) { AddSection(lastSectionName, header[lastSectionStart..(i - 1)]); - lastSectionName = current[3..^1]; + lastSectionName = current[3..^1]; lastSectionStart = i + 2; ++i; // The next line cannot match } } + AddSection(lastSectionName, header[lastSectionStart..]); return sections; @@ -442,9 +425,8 @@ public class DisassembledShader { var start = dashLine.IndexOf('-', i); if (start < 0) - { break; - } + var end = dashLine.IndexOf(' ', start + 1); if (end < 0) { @@ -462,20 +444,17 @@ public class DisassembledShader { var headerLine = lines[0]; for (var i = 0; i < columns.Count; ++i) - { headers[i] = headerLine[columns[i]].Trim(); - } } var data = new List(); foreach (var line in lines[2..]) { var row = new string[columns.Count]; for (var i = 0; i < columns.Count; ++i) - { row[i] = line[columns[i]].Trim(); - } data.Add(row); } + return (headers, data.ToArray()); } } diff --git a/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs b/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs new file mode 100644 index 00000000..4cd2ff28 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public unsafe struct ColorDyeSet + { + public struct Row + { + private ushort _data; + + public ushort Template + { + get => (ushort)(_data >> 5); + set => _data = (ushort)((_data & 0x1F) | (value << 5)); + } + + public bool Diffuse + { + get => (_data & 0x01) != 0; + set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); + } + + public bool Specular + { + get => (_data & 0x02) != 0; + set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); + } + + public bool Emissive + { + get => (_data & 0x04) != 0; + set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); + } + + public bool Gloss + { + get => (_data & 0x08) != 0; + set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); + } + + public bool SpecularStrength + { + get => (_data & 0x10) != 0; + set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); + } + } + + public struct RowArray : IEnumerable + { + public const int NumRows = 16; + private fixed ushort _rowData[NumRows]; + + public ref Row this[int i] + { + get + { + fixed (ushort* ptr = _rowData) + { + return ref ((Row*)ptr)[i]; + } + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < NumRows; ++i) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan AsBytes() + { + fixed (ushort* ptr = _rowData) + { + return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + } +} diff --git a/Penumbra.GameData/Files/MtrlFile.ColorSet.cs b/Penumbra.GameData/Files/MtrlFile.ColorSet.cs new file mode 100644 index 00000000..61647d79 --- /dev/null +++ b/Penumbra.GameData/Files/MtrlFile.ColorSet.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; + +namespace Penumbra.GameData.Files; + +public partial class MtrlFile +{ + public unsafe struct ColorSet + { + public struct Row + { + public const int Size = 32; + + private fixed ushort _data[16]; + + public Vector3 Diffuse + { + get => new(ToFloat(0), ToFloat(1), ToFloat(2)); + set + { + _data[0] = FromFloat(value.X); + _data[1] = FromFloat(value.Y); + _data[2] = FromFloat(value.Z); + } + } + + public Vector3 Specular + { + get => new(ToFloat(4), ToFloat(5), ToFloat(6)); + set + { + _data[4] = FromFloat(value.X); + _data[5] = FromFloat(value.Y); + _data[6] = FromFloat(value.Z); + } + } + + public Vector3 Emissive + { + get => new(ToFloat(8), ToFloat(9), ToFloat(10)); + set + { + _data[8] = FromFloat(value.X); + _data[9] = FromFloat(value.Y); + _data[10] = FromFloat(value.Z); + } + } + + public Vector2 MaterialRepeat + { + get => new(ToFloat(12), ToFloat(15)); + set + { + _data[12] = FromFloat(value.X); + _data[15] = FromFloat(value.Y); + } + } + + public Vector2 MaterialSkew + { + get => new(ToFloat(13), ToFloat(14)); + set + { + _data[13] = FromFloat(value.X); + _data[14] = FromFloat(value.Y); + } + } + + public float SpecularStrength + { + get => ToFloat(3); + set => _data[3] = FromFloat(value); + } + + public float GlossStrength + { + get => ToFloat(7); + set => _data[7] = FromFloat(value); + } + + public ushort TileSet + { + get => (ushort)(ToFloat(11) * 64f); + set => _data[11] = FromFloat(value / 64f); + } + + private float ToFloat(int idx) + => (float)BitConverter.UInt16BitsToHalf(_data[idx]); + + private static ushort FromFloat(float x) + => BitConverter.HalfToUInt16Bits((Half)x); + } + + public struct RowArray : IEnumerable + { + public const int NumRows = 16; + private fixed byte _rowData[NumRows * Row.Size]; + + public ref Row this[int i] + { + get + { + fixed (byte* ptr = _rowData) + { + return ref ((Row*)ptr)[i]; + } + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < NumRows; ++i) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan AsBytes() + { + fixed (byte* ptr = _rowData) + { + return new ReadOnlySpan(ptr, NumRows * Row.Size); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + public bool HasRows; + } +} diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 7faabc68..9bc5a2ce 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using System.Text; diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 910e4adb..6a51a39b 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -1,9 +1,7 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Numerics; using System.Text; using Lumina.Data.Parsing; using Lumina.Extensions; @@ -13,262 +11,10 @@ namespace Penumbra.GameData.Files; public partial class MtrlFile : IWritable { - public struct UvSet - { - public string Name; - public ushort Index; - } - - public unsafe struct ColorSet - { - public struct Row - { - public const int Size = 32; - - private fixed ushort _data[16]; - - public Vector3 Diffuse - { - get => new(ToFloat(0), ToFloat(1), ToFloat(2)); - set - { - _data[0] = FromFloat(value.X); - _data[1] = FromFloat(value.Y); - _data[2] = FromFloat(value.Z); - } - } - - public Vector3 Specular - { - get => new(ToFloat(4), ToFloat(5), ToFloat(6)); - set - { - _data[4] = FromFloat(value.X); - _data[5] = FromFloat(value.Y); - _data[6] = FromFloat(value.Z); - } - } - - public Vector3 Emissive - { - get => new(ToFloat(8), ToFloat(9), ToFloat(10)); - set - { - _data[8] = FromFloat(value.X); - _data[9] = FromFloat(value.Y); - _data[10] = FromFloat(value.Z); - } - } - - public Vector2 MaterialRepeat - { - get => new(ToFloat(12), ToFloat(15)); - set - { - _data[12] = FromFloat(value.X); - _data[15] = FromFloat(value.Y); - } - } - - public Vector2 MaterialSkew - { - get => new(ToFloat(13), ToFloat(14)); - set - { - _data[13] = FromFloat(value.X); - _data[14] = FromFloat(value.Y); - } - } - - public float SpecularStrength - { - get => ToFloat(3); - set => _data[3] = FromFloat(value); - } - - public float GlossStrength - { - get => ToFloat(7); - set => _data[7] = FromFloat(value); - } - - public ushort TileSet - { - get => (ushort)(ToFloat(11) * 64f); - set => _data[11] = FromFloat(value / 64f); - } - - private float ToFloat(int idx) - => (float)BitConverter.UInt16BitsToHalf(_data[idx]); - - private static ushort FromFloat(float x) - => BitConverter.HalfToUInt16Bits((Half)x); - } - - public struct RowArray : IEnumerable - { - public const int NumRows = 16; - private fixed byte _rowData[NumRows * Row.Size]; - - public ref Row this[int i] - { - get - { - fixed (byte* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ReadOnlySpan AsBytes() - { - fixed (byte* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * Row.Size); - } - } - } - - public RowArray Rows; - public string Name; - public ushort Index; - public bool HasRows; - } - - public unsafe struct ColorDyeSet - { - public struct Row - { - private ushort _data; - - public ushort Template - { - get => (ushort)(_data >> 5); - set => _data = (ushort)((_data & 0x1F) | (value << 5)); - } - - public bool Diffuse - { - get => (_data & 0x01) != 0; - set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); - } - - public bool Specular - { - get => (_data & 0x02) != 0; - set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); - } - - public bool Emissive - { - get => (_data & 0x04) != 0; - set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); - } - - public bool Gloss - { - get => (_data & 0x08) != 0; - set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); - } - - public bool SpecularStrength - { - get => (_data & 0x10) != 0; - set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); - } - } - - public struct RowArray : IEnumerable - { - public const int NumRows = 16; - private fixed ushort _rowData[NumRows]; - - public ref Row this[int i] - { - get - { - fixed (ushort* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ReadOnlySpan AsBytes() - { - fixed (ushort* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); - } - } - } - - public RowArray Rows; - public string Name; - public ushort Index; - } - - public struct Texture - { - public string Path; - public ushort Flags; - - public bool DX11 - => (Flags & 0x8000) != 0; - } - - public struct Constant - { - public uint Id; - public ushort ByteOffset; - public ushort ByteSize; - } - - public struct ShaderPackageData - { - public string Name; - public ShaderKey[] ShaderKeys; - public Constant[] Constants; - public Sampler[] Samplers; - public float[] ShaderValues; - public uint Flags; - } - - public readonly uint Version; - public bool Valid - { - get - { - foreach (var texture in Textures) - { - if (!texture.Path.Contains('/')) - { - return false; - } - } - return true; - } - } + + public bool Valid + => CheckTextures(); public Texture[] Textures; public UvSet[] UvSets; @@ -277,7 +23,7 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; - public ShpkFile? AssociatedShpk; + public ShpkFile? AssociatedShpk; public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { @@ -324,15 +70,13 @@ public partial class MtrlFile : IWritable public Span GetConstantValues(Constant constant) { - if ((constant.ByteOffset & 0x3) == 0 && (constant.ByteSize & 0x3) == 0 - && ((constant.ByteOffset + constant.ByteSize) >> 2) <= ShaderPackage.ShaderValues.Length) - { - return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2); - } - else - { + if ((constant.ByteOffset & 0x3) != 0 + || (constant.ByteSize & 0x3) != 0 + || (constant.ByteOffset + constant.ByteSize) >> 2 > ShaderPackage.ShaderValues.Length) return null; - } + + return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2); + } public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture() @@ -348,13 +92,7 @@ public partial class MtrlFile : IWritable } return samplers; - } - - // Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit - public MtrlFile(byte[] data) - : this(data, null) - { - } + } public MtrlFile(byte[] data, Func? loadAssociatedShpk = null) { @@ -469,6 +207,41 @@ public partial class MtrlFile : IWritable { strings = strings[offset..]; var end = strings.IndexOf((byte)'\0'); - return Encoding.UTF8.GetString(strings[..end]); + return Encoding.UTF8.GetString(end == -1 ? strings : strings[..end]); + } + + private bool CheckTextures() + => Textures.All(texture => texture.Path.Contains('/')); + + public struct UvSet + { + public string Name; + public ushort Index; + } + + public struct Texture + { + public string Path; + public ushort Flags; + + public bool DX11 + => (Flags & 0x8000) != 0; + } + + public struct Constant + { + public uint Id; + public ushort ByteOffset; + public ushort ByteSize; + } + + public struct ShaderPackageData + { + public string Name; + public ShaderKey[] ShaderKeys; + public Constant[] Constants; + public Sampler[] Samplers; + public float[] ShaderValues; + public uint Flags; } } diff --git a/Penumbra.GameData/Files/ShpkFile.Shader.cs b/Penumbra.GameData/Files/ShpkFile.Shader.cs new file mode 100644 index 00000000..3d94dbb4 --- /dev/null +++ b/Penumbra.GameData/Files/ShpkFile.Shader.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Lumina.Misc; +using Penumbra.GameData.Data; + +namespace Penumbra.GameData.Files; + +public partial class ShpkFile +{ + public struct Shader + { + public DisassembledShader.ShaderStage Stage; + public DxVersion DirectXVersion; + public Resource[] Constants; + public Resource[] Samplers; + public Resource[] Uavs; + public byte[] AdditionalHeader; + private byte[] _byteData; + private DisassembledShader? _disassembly; + + public byte[] Blob + { + get => _byteData; + set + { + if (_byteData == value) + return; + + if (Stage != DisassembledShader.ShaderStage.Unspecified) + { + // Reject the blob entirely if we can't disassemble it or if we find inconsistencies. + var disasm = DisassembledShader.Disassemble(value); + if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion) + throw new ArgumentException( + $"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.", + nameof(value)); + + if (disasm.ShaderModel >= 0x0500) + { + var samplers = new Dictionary(); + var textures = new Dictionary(); + foreach (var binding in disasm.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.Texture: + textures[binding.Slot] = NormalizeResourceName(binding.Name); + break; + case DisassembledShader.ResourceType.Sampler: + samplers[binding.Slot] = NormalizeResourceName(binding.Name); + break; + } + } + + if (samplers.Count != textures.Count + || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName)) + throw new ArgumentException($"The supplied blob has inconsistent sampler and texture allocation."); + } + + _byteData = value; + _disassembly = disasm; + } + else + { + _byteData = value; + _disassembly = null; + } + + UpdateUsed(); + } + } + + public DisassembledShader? Disassembly + => _disassembly; + + public Resource? GetConstantById(uint id) + => Constants.FirstOrNull(res => res.Id == id); + + public Resource? GetConstantByName(string name) + => Constants.FirstOrNull(res => res.Name == name); + + public Resource? GetSamplerById(uint id) + => Samplers.FirstOrNull(s => s.Id == id); + + public Resource? GetSamplerByName(string name) + => Samplers.FirstOrNull(s => s.Name == name); + + public Resource? GetUavById(uint id) + => Uavs.FirstOrNull(u => u.Id == id); + + public Resource? GetUavByName(string name) + => Uavs.FirstOrNull(u => u.Name == name); + + public void UpdateResources(ShpkFile file) + { + if (_disassembly == null) + throw new InvalidOperationException(); + + var constants = new List(); + var samplers = new List(); + var uavs = new List(); + foreach (var binding in _disassembly.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.ConstantBuffer: + var name = NormalizeResourceName(binding.Name); + // We want to preserve IDs as much as possible, and to deterministically generate new ones in a way that's most compliant with the native ones, to maximize compatibility. + var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); + constants.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.RegisterCount, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, + }); + break; + case DisassembledShader.ResourceType.Texture: + name = NormalizeResourceName(binding.Name); + id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); + samplers.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.Slot, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, + }); + break; + case DisassembledShader.ResourceType.Uav: + name = NormalizeResourceName(binding.Name); + id = GetUavByName(name)?.Id ?? file.GetUavByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); + uavs.Add(new Resource + { + Id = id, + Name = name, + Slot = (ushort)binding.Slot, + Size = (ushort)binding.Slot, + Used = binding.Used, + UsedDynamically = binding.UsedDynamically, + }); + break; + } + } + + Constants = constants.ToArray(); + Samplers = samplers.ToArray(); + Uavs = uavs.ToArray(); + } + + private void UpdateUsed() + { + if (_disassembly != null) + { + var cbUsage = new Dictionary(); + var tUsage = new Dictionary(); + var uUsage = new Dictionary(); + foreach (var binding in _disassembly.ResourceBindings) + { + switch (binding.Type) + { + case DisassembledShader.ResourceType.ConstantBuffer: + cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; + case DisassembledShader.ResourceType.Texture: + tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; + case DisassembledShader.ResourceType.Uav: + uUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); + break; + } + } + + static void CopyUsed(Resource[] resources, + Dictionary used) + { + for (var i = 0; i < resources.Length; ++i) + { + if (used.TryGetValue(resources[i].Name, out var usage)) + { + resources[i].Used = usage.Item1; + resources[i].UsedDynamically = usage.Item2; + } + else + { + resources[i].Used = null; + resources[i].UsedDynamically = null; + } + } + } + + CopyUsed(Constants, cbUsage); + CopyUsed(Samplers, tUsage); + CopyUsed(Uavs, uUsage); + } + else + { + ClearUsed(Constants); + ClearUsed(Samplers); + ClearUsed(Uavs); + } + } + + private static string NormalizeResourceName(string resourceName) + { + var dot = resourceName.IndexOf('.'); + if (dot >= 0) + return resourceName[..dot]; + if (resourceName.Length > 1 && resourceName[^2] is '_' && resourceName[^1] is 'S' or 'T') + return resourceName[..^2]; + + return resourceName; + } + } +} diff --git a/Penumbra.GameData/Files/ShpkFile.StringPool.cs b/Penumbra.GameData/Files/ShpkFile.StringPool.cs new file mode 100644 index 00000000..bad56d20 --- /dev/null +++ b/Penumbra.GameData/Files/ShpkFile.StringPool.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Penumbra.GameData.Files; + +public partial class ShpkFile +{ + public class StringPool + { + public MemoryStream Data; + public List StartingOffsets; + + public StringPool(ReadOnlySpan bytes) + { + Data = new MemoryStream(); + Data.Write(bytes); + StartingOffsets = new List + { + 0, + }; + for (var i = 0; i < bytes.Length; ++i) + { + if (bytes[i] == 0) + StartingOffsets.Add(i + 1); + } + + if (StartingOffsets[^1] == bytes.Length) + StartingOffsets.RemoveAt(StartingOffsets.Count - 1); + else + Data.WriteByte(0); + } + + public string GetString(int offset, int size) + => Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size)); + + public string GetNullTerminatedString(int offset) + { + var str = Data.GetBuffer().AsSpan()[offset..]; + var size = str.IndexOf((byte)0); + if (size >= 0) + str = str[..size]; + return Encoding.UTF8.GetString(str); + } + + public (int, int) FindOrAddString(string str) + { + var dataSpan = Data.GetBuffer().AsSpan(); + var bytes = Encoding.UTF8.GetBytes(str); + foreach (var offset in StartingOffsets) + { + if (offset + bytes.Length > Data.Length) + break; + + var strSpan = dataSpan[offset..]; + var match = true; + for (var i = 0; i < bytes.Length; ++i) + { + if (strSpan[i] != bytes[i]) + { + match = false; + break; + } + } + + if (match && strSpan[bytes.Length] == 0) + return (offset, bytes.Length); + } + + Data.Seek(0L, SeekOrigin.End); + var newOffset = (int)Data.Position; + StartingOffsets.Add(newOffset); + Data.Write(bytes); + Data.WriteByte(0); + return (newOffset, bytes.Length); + } + } +} diff --git a/Penumbra.GameData/Files/ShpkFile.Write.cs b/Penumbra.GameData/Files/ShpkFile.Write.cs index dd8b70ac..117ea5e5 100644 --- a/Penumbra.GameData/Files/ShpkFile.Write.cs +++ b/Penumbra.GameData/Files/ShpkFile.Write.cs @@ -8,21 +8,19 @@ public partial class ShpkFile public byte[] Write() { if (SubViewKeys.Length != 2) - { throw new InvalidDataException(); - } - using var stream = new MemoryStream(); - using var blobs = new MemoryStream(); - var strings = new StringPool(ReadOnlySpan.Empty); + using var stream = new MemoryStream(); + using var blobs = new MemoryStream(); + var strings = new StringPool(ReadOnlySpan.Empty); using (var w = new BinaryWriter(stream)) { w.Write(ShPkMagic); w.Write(Version); w.Write(DirectXVersion switch { - DXVersion.DirectX9 => DX9Magic, - DXVersion.DirectX11 => DX11Magic, + DxVersion.DirectX9 => Dx9Magic, + DxVersion.DirectX11 => Dx11Magic, _ => throw new NotImplementedException(), }); var offsetsPosition = stream.Position; @@ -35,7 +33,7 @@ public partial class ShpkFile w.Write((uint)MaterialParams.Length); w.Write((uint)Constants.Length); w.Write((uint)Samplers.Length); - w.Write((uint)UAVs.Length); + w.Write((uint)Uavs.Length); w.Write((uint)SystemKeys.Length); w.Write((uint)SceneKeys.Length); w.Write((uint)MaterialKeys.Length); @@ -43,7 +41,7 @@ public partial class ShpkFile w.Write((uint)Items.Length); WriteShaderArray(w, VertexShaders, blobs, strings); - WriteShaderArray(w, PixelShaders, blobs, strings); + WriteShaderArray(w, PixelShaders, blobs, strings); foreach (var materialParam in MaterialParams) { @@ -53,54 +51,50 @@ public partial class ShpkFile } WriteResourceArray(w, Constants, strings); - WriteResourceArray(w, Samplers, strings); - WriteResourceArray(w, UAVs, strings); + WriteResourceArray(w, Samplers, strings); + WriteResourceArray(w, Uavs, strings); foreach (var key in SystemKeys) { w.Write(key.Id); w.Write(key.DefaultValue); } + foreach (var key in SceneKeys) { w.Write(key.Id); w.Write(key.DefaultValue); } + foreach (var key in MaterialKeys) { w.Write(key.Id); w.Write(key.DefaultValue); } + foreach (var key in SubViewKeys) - { w.Write(key.DefaultValue); - } foreach (var node in Nodes) { - if (node.PassIndices.Length != 16 || node.SystemKeys.Length != SystemKeys.Length || node.SceneKeys.Length != SceneKeys.Length || node.MaterialKeys.Length != MaterialKeys.Length || node.SubViewKeys.Length != SubViewKeys.Length) - { + if (node.PassIndices.Length != 16 + || node.SystemKeys.Length != SystemKeys.Length + || node.SceneKeys.Length != SceneKeys.Length + || node.MaterialKeys.Length != MaterialKeys.Length + || node.SubViewKeys.Length != SubViewKeys.Length) throw new InvalidDataException(); - } + w.Write(node.Id); w.Write(node.Passes.Length); w.Write(node.PassIndices); foreach (var key in node.SystemKeys) - { w.Write(key); - } foreach (var key in node.SceneKeys) - { w.Write(key); - } foreach (var key in node.MaterialKeys) - { w.Write(key); - } foreach (var key in node.SubViewKeys) - { w.Write(key); - } foreach (var pass in node.Passes) { w.Write(pass.Id); @@ -160,21 +154,12 @@ public partial class ShpkFile w.Write(blobSize); w.Write((ushort)shader.Constants.Length); w.Write((ushort)shader.Samplers.Length); - w.Write((ushort)shader.UAVs.Length); + w.Write((ushort)shader.Uavs.Length); w.Write((ushort)0); WriteResourceArray(w, shader.Constants, strings); - WriteResourceArray(w, shader.Samplers, strings); - WriteResourceArray(w, shader.UAVs, strings); - } - } - - private static void WriteUInt32PairArray(BinaryWriter w, (uint, uint)[] array) - { - foreach (var (first, second) in array) - { - w.Write(first); - w.Write(second); + WriteResourceArray(w, shader.Samplers, strings); + WriteResourceArray(w, shader.Uavs, strings); } } } diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs index ebcd2e58..75651ef9 100644 --- a/Penumbra.GameData/Files/ShpkFile.cs +++ b/Penumbra.GameData/Files/ShpkFile.cs @@ -2,462 +2,74 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using Lumina.Data.Parsing; using Lumina.Extensions; -using Lumina.Misc; using Penumbra.GameData.Data; namespace Penumbra.GameData.Files; public partial class ShpkFile : IWritable { - public enum DXVersion : uint - { - DirectX9 = 9, - DirectX11 = 11, - } - - public struct Resource - { - public uint Id; - public string Name; - public ushort Slot; - public ushort Size; - public DisassembledShader.VectorComponents[]? Used; - public DisassembledShader.VectorComponents? UsedDynamically; - } - - public struct Shader - { - public DisassembledShader.ShaderStage Stage; - public DXVersion DirectXVersion; - public Resource[] Constants; - public Resource[] Samplers; - public Resource[] UAVs; - public byte[] AdditionalHeader; - private byte[] _blob; - private DisassembledShader? _disassembly; - - public byte[] Blob - { - get => _blob; - set - { - if (_blob == value) - { - return; - } - if (Stage != DisassembledShader.ShaderStage.Unspecified) - { - // Reject the blob entirely if we can't disassemble it or if we find inconsistencies. - var disasm = DisassembledShader.Disassemble(value); - if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion) - { - throw new ArgumentException($"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.", nameof(value)); - } - if (disasm.ShaderModel >= 0x0500) - { - var samplers = new Dictionary(); - var textures = new Dictionary(); - foreach (var binding in disasm.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.Texture: - textures[binding.Slot] = NormalizeResourceName(binding.Name); - break; - case DisassembledShader.ResourceType.Sampler: - samplers[binding.Slot] = NormalizeResourceName(binding.Name); - break; - } - } - if (samplers.Count != textures.Count || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName)) - { - throw new ArgumentException($"The supplied blob has inconsistent sampler and texture allocation."); - } - } - _blob = value; - _disassembly = disasm; - } - else - { - _blob = value; - _disassembly = null; - } - UpdateUsed(); - } - } - - public DisassembledShader? Disassembly => _disassembly; - - public Resource? GetConstantById(uint id) - { - return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } - - public Resource? GetConstantByName(string name) - { - return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } - - public Resource? GetSamplerById(uint id) - { - return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } - - public Resource? GetSamplerByName(string name) - { - return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } - - public Resource? GetUAVById(uint id) - { - return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } - - public Resource? GetUAVByName(string name) - { - return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } - - public void UpdateResources(ShpkFile file) - { - if (_disassembly == null) - { - throw new InvalidOperationException(); - } - var constants = new List(); - var samplers = new List(); - var uavs = new List(); - foreach (var binding in _disassembly.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.ConstantBuffer: - var name = NormalizeResourceName(binding.Name); - // We want to preserve IDs as much as possible, and to deterministically generate new ones in a way that's most compliant with the native ones, to maximize compatibility. - var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - constants.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.RegisterCount, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - case DisassembledShader.ResourceType.Texture: - name = NormalizeResourceName(binding.Name); - id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - samplers.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.Slot, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - case DisassembledShader.ResourceType.UAV: - name = NormalizeResourceName(binding.Name); - id = GetUAVByName(name)?.Id ?? file.GetUAVByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - uavs.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.Slot, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - } - } - Constants = constants.ToArray(); - Samplers = samplers.ToArray(); - UAVs = uavs.ToArray(); - } - - private void UpdateUsed() - { - if (_disassembly != null) - { - var cbUsage = new Dictionary(); - var tUsage = new Dictionary(); - var uUsage = new Dictionary(); - foreach (var binding in _disassembly.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.ConstantBuffer: - cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - case DisassembledShader.ResourceType.Texture: - tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - case DisassembledShader.ResourceType.UAV: - uUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - } - } - static void CopyUsed(Resource[] resources, Dictionary used) - { - for (var i = 0; i < resources.Length; ++i) - { - if (used.TryGetValue(resources[i].Name, out var usage)) - { - resources[i].Used = usage.Item1; - resources[i].UsedDynamically = usage.Item2; - } - else - { - resources[i].Used = null; - resources[i].UsedDynamically = null; - } - } - } - CopyUsed(Constants, cbUsage); - CopyUsed(Samplers, tUsage); - CopyUsed(UAVs, uUsage); - } - else - { - ClearUsed(Constants); - ClearUsed(Samplers); - ClearUsed(UAVs); - } - } - - private static string NormalizeResourceName(string resourceName) - { - var dot = resourceName.IndexOf('.'); - if (dot >= 0) - { - return resourceName[..dot]; - } - else if (resourceName.EndsWith("_S") || resourceName.EndsWith("_T")) - { - return resourceName[..^2]; - } - else - { - return resourceName; - } - } - } - - public struct MaterialParam - { - public uint Id; - public ushort ByteOffset; - public ushort ByteSize; - } - - public struct Pass - { - public uint Id; - public uint VertexShader; - public uint PixelShader; - } - - public struct Key - { - public uint Id; - public uint DefaultValue; - public uint[] Values; - } - - public struct Node - { - public uint Id; - public byte[] PassIndices; - public uint[] SystemKeys; - public uint[] SceneKeys; - public uint[] MaterialKeys; - public uint[] SubViewKeys; - public Pass[] Passes; - } - - public struct Item - { - public uint Id; - public uint Node; - } - - public class StringPool - { - public MemoryStream Data; - public List StartingOffsets; - - public StringPool(ReadOnlySpan bytes) - { - Data = new MemoryStream(); - Data.Write(bytes); - StartingOffsets = new List - { - 0, - }; - for (var i = 0; i < bytes.Length; ++i) - { - if (bytes[i] == 0) - { - StartingOffsets.Add(i + 1); - } - } - if (StartingOffsets[^1] == bytes.Length) - { - StartingOffsets.RemoveAt(StartingOffsets.Count - 1); - } - else - { - Data.WriteByte(0); - } - } - - public string GetString(int offset, int size) - { - return Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size)); - } - - public string GetNullTerminatedString(int offset) - { - var str = Data.GetBuffer().AsSpan()[offset..]; - var size = str.IndexOf((byte)0); - if (size >= 0) - { - str = str[..size]; - } - return Encoding.UTF8.GetString(str); - } - - public (int, int) FindOrAddString(string str) - { - var dataSpan = Data.GetBuffer().AsSpan(); - var bytes = Encoding.UTF8.GetBytes(str); - foreach (var offset in StartingOffsets) - { - if (offset + bytes.Length > Data.Length) - { - break; - } - var strSpan = dataSpan[offset..]; - var match = true; - for (var i = 0; i < bytes.Length; ++i) - { - if (strSpan[i] != bytes[i]) - { - match = false; - break; - } - } - if (match && strSpan[bytes.Length] == 0) - { - return (offset, bytes.Length); - } - } - Data.Seek(0L, SeekOrigin.End); - var newOffset = (int)Data.Position; - StartingOffsets.Add(newOffset); - Data.Write(bytes); - Data.WriteByte(0); - return (newOffset, bytes.Length); - } - } - private const uint ShPkMagic = 0x6B506853u; // bytes of ShPk - private const uint DX9Magic = 0x00395844u; // bytes of DX9\0 - private const uint DX11Magic = 0x31315844u; // bytes of DX11 + private const uint Dx9Magic = 0x00395844u; // bytes of DX9\0 + private const uint Dx11Magic = 0x31315844u; // bytes of DX11 public const uint MaterialParamsConstantId = 0x64D12851u; - public uint Version; - public DXVersion DirectXVersion; - public Shader[] VertexShaders; - public Shader[] PixelShaders; - public uint MaterialParamsSize; + public uint Version; + public DxVersion DirectXVersion; + public Shader[] VertexShaders; + public Shader[] PixelShaders; + public uint MaterialParamsSize; public MaterialParam[] MaterialParams; - public Resource[] Constants; - public Resource[] Samplers; - public Resource[] UAVs; - public Key[] SystemKeys; - public Key[] SceneKeys; - public Key[] MaterialKeys; - public Key[] SubViewKeys; - public Node[] Nodes; - public Item[] Items; - public byte[] AdditionalData; + public Resource[] Constants; + public Resource[] Samplers; + public Resource[] Uavs; + public Key[] SystemKeys; + public Key[] SceneKeys; + public Key[] MaterialKeys; + public Key[] SubViewKeys; + public Node[] Nodes; + public Item[] Items; + public byte[] AdditionalData; - public bool Valid { get; private set; } + public bool Valid { get; private set; } private bool _changed; public MaterialParam? GetMaterialParamById(uint id) - { - return MaterialParams.Select(param => new MaterialParam?(param)).FirstOrDefault(param => param!.Value.Id == id); - } + => MaterialParams.FirstOrNull(m => m.Id == id); public Resource? GetConstantById(uint id) - { - return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } + => Constants.FirstOrNull(c => c.Id == id); public Resource? GetConstantByName(string name) - { - return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } + => Constants.FirstOrNull(c => c.Name == name); public Resource? GetSamplerById(uint id) - { - return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } + => Samplers.FirstOrNull(s => s.Id == id); public Resource? GetSamplerByName(string name) - { - return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } + => Samplers.FirstOrNull(s => s.Name == name); - public Resource? GetUAVById(uint id) - { - return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id); - } + public Resource? GetUavById(uint id) + => Uavs.FirstOrNull(u => u.Id == id); - public Resource? GetUAVByName(string name) - { - return UAVs.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name); - } + public Resource? GetUavByName(string name) + => Uavs.FirstOrNull(u => u.Name == name); public Key? GetSystemKeyById(uint id) - { - return SystemKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); - } + => SystemKeys.FirstOrNull(k => k.Id == id); public Key? GetSceneKeyById(uint id) - { - return SceneKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); - } + => SceneKeys.FirstOrNull(k => k.Id == id); public Key? GetMaterialKeyById(uint id) - { - return MaterialKeys.Select(key => new Key?(key)).FirstOrDefault(key => key!.Value.Id == id); - } + => MaterialKeys.FirstOrNull(k => k.Id == id); public Node? GetNodeById(uint id) - { - return Nodes.Select(node => new Node?(node)).FirstOrDefault(node => node!.Value.Id == id); - } + => Nodes.FirstOrNull(n => n.Id == id); public Item? GetItemById(uint id) - { - return Items.Select(item => new Item?(item)).FirstOrDefault(item => item!.Value.Id == id); - } - - // Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit - public ShpkFile(byte[] data) - : this(data, false) - { - } + => Items.FirstOrNull(i => i.Id == id); public ShpkFile(byte[] data, bool disassemble = false) { @@ -465,25 +77,23 @@ public partial class ShpkFile : IWritable using var r = new BinaryReader(stream); if (r.ReadUInt32() != ShPkMagic) - { throw new InvalidDataException(); - } + Version = r.ReadUInt32(); DirectXVersion = r.ReadUInt32() switch { - DX9Magic => DXVersion.DirectX9, - DX11Magic => DXVersion.DirectX11, + Dx9Magic => DxVersion.DirectX9, + Dx11Magic => DxVersion.DirectX11, _ => throw new InvalidDataException(), }; if (r.ReadUInt32() != data.Length) - { throw new InvalidDataException(); - } - var blobsOffset = r.ReadUInt32(); - var stringsOffset = r.ReadUInt32(); - var vertexShaderCount = r.ReadUInt32(); - var pixelShaderCount = r.ReadUInt32(); - MaterialParamsSize = r.ReadUInt32(); + + var blobsOffset = r.ReadUInt32(); + var stringsOffset = r.ReadUInt32(); + var vertexShaderCount = r.ReadUInt32(); + var pixelShaderCount = r.ReadUInt32(); + MaterialParamsSize = r.ReadUInt32(); var materialParamCount = r.ReadUInt32(); var constantCount = r.ReadUInt32(); var samplerCount = r.ReadUInt32(); @@ -497,99 +107,105 @@ public partial class ShpkFile : IWritable var blobs = new ReadOnlySpan(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset)); var strings = new StringPool(new ReadOnlySpan(data, (int)stringsOffset, (int)(data.Length - stringsOffset))); - VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, strings); - PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, strings); + VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, + strings); + PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, + strings); MaterialParams = r.ReadStructuresAsArray((int)materialParamCount); - Constants = ReadResourceArray(r, (int)constantCount, strings); - Samplers = ReadResourceArray(r, (int)samplerCount, strings); - UAVs = ReadResourceArray(r, (int)uavCount, strings); + Constants = ReadResourceArray(r, (int)constantCount, strings); + Samplers = ReadResourceArray(r, (int)samplerCount, strings); + Uavs = ReadResourceArray(r, (int)uavCount, strings); - SystemKeys = ReadKeyArray(r, (int)systemKeyCount); - SceneKeys = ReadKeyArray(r, (int)sceneKeyCount); - MaterialKeys = ReadKeyArray(r, (int)materialKeyCount); + SystemKeys = ReadKeyArray(r, (int)systemKeyCount); + SceneKeys = ReadKeyArray(r, (int)sceneKeyCount); + MaterialKeys = ReadKeyArray(r, (int)materialKeyCount); - var subViewKey1Default = r.ReadUInt32(); - var subViewKey2Default = r.ReadUInt32(); + var subViewKey1Null = r.ReadUInt32(); + var subViewKey2Null = r.ReadUInt32(); - SubViewKeys = new Key[] { - new Key + SubViewKeys = new Key[] + { + new() { - Id = 1, - DefaultValue = subViewKey1Default, - Values = Array.Empty(), + Id = 1, + DefaultValue = subViewKey1Null, + Values = Array.Empty(), }, - new Key + new() { - Id = 2, - DefaultValue = subViewKey2Default, - Values = Array.Empty(), + Id = 2, + DefaultValue = subViewKey2Null, + Values = Array.Empty(), }, }; - Nodes = ReadNodeArray(r, (int)nodeCount, SystemKeys.Length, SceneKeys.Length, MaterialKeys.Length, SubViewKeys.Length); - Items = r.ReadStructuresAsArray((int)itemCount); + Nodes = ReadNodeArray(r, (int)nodeCount, SystemKeys.Length, SceneKeys.Length, MaterialKeys.Length, SubViewKeys.Length); + Items = r.ReadStructuresAsArray((int)itemCount); AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); // This should be empty, but just in case. if (disassemble) - { UpdateUsed(); - } UpdateKeyValues(); - Valid = true; + Valid = true; _changed = false; } public void UpdateResources() { var constants = new Dictionary(); - var samplers = new Dictionary(); - var uavs = new Dictionary(); - static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, DisassembledShader.ResourceType type) + var samplers = new Dictionary(); + var uavs = new Dictionary(); + + static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, + DisassembledShader.ResourceType type) { foreach (var resource in shaderResources) { if (resources.TryGetValue(resource.Id, out var carry) && type != DisassembledShader.ResourceType.ConstantBuffer) - { continue; - } + var existing = getExistingById(resource.Id); resources[resource.Id] = new Resource { - Id = resource.Id, - Name = resource.Name, - Slot = existing?.Slot ?? (type == DisassembledShader.ResourceType.ConstantBuffer ? (ushort)65535 : (ushort)2), - Size = type == DisassembledShader.ResourceType.ConstantBuffer ? Math.Max(carry.Size, resource.Size) : (existing?.Size ?? 0), - Used = null, + Id = resource.Id, + Name = resource.Name, + Slot = existing?.Slot ?? (type == DisassembledShader.ResourceType.ConstantBuffer ? (ushort)65535 : (ushort)2), + Size = type == DisassembledShader.ResourceType.ConstantBuffer ? Math.Max(carry.Size, resource.Size) : existing?.Size ?? 0, + Used = null, UsedDynamically = null, }; } } + foreach (var shader in VertexShaders) { CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); - CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); - CollectResources(uavs, shader.UAVs, GetUAVById, DisassembledShader.ResourceType.UAV); + CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); + CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav); } + foreach (var shader in PixelShaders) { CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); - CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); - CollectResources(uavs, shader.UAVs, GetUAVById, DisassembledShader.ResourceType.UAV); + CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); + CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav); } + Constants = constants.Values.ToArray(); - Samplers = samplers.Values.ToArray(); - UAVs = uavs.Values.ToArray(); - UpdateUsed(); + Samplers = samplers.Values.ToArray(); + Uavs = uavs.Values.ToArray(); + UpdateUsed(); + + // Ceil required size to a multiple of 16 bytes. + // Offsets can be skipped, MaterialParamsConstantId's size is the count. MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4; foreach (var param in MaterialParams) - { MaterialParamsSize = Math.Max(MaterialParamsSize, (uint)param.ByteOffset + param.ByteSize); - } MaterialParamsSize = (MaterialParamsSize + 0xFu) & ~0xFu; } @@ -598,55 +214,59 @@ public partial class ShpkFile : IWritable var cUsage = new Dictionary(); var sUsage = new Dictionary(); var uUsage = new Dictionary(); - static void CollectUsed(Dictionary usage, Resource[] resources) + + static void CollectUsed(Dictionary usage, + Resource[] resources) { foreach (var resource in resources) { if (resource.Used == null) - { continue; - } + usage.TryGetValue(resource.Id, out var carry); carry.Item1 ??= Array.Empty(); var combined = new DisassembledShader.VectorComponents[Math.Max(carry.Item1.Length, resource.Used.Length)]; for (var i = 0; i < combined.Length; ++i) - { combined[i] = (i < carry.Item1.Length ? carry.Item1[i] : 0) | (i < resource.Used.Length ? resource.Used[i] : 0); - } usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0)); } } - static void CopyUsed(Resource[] resources, Dictionary used) + + static void CopyUsed(Resource[] resources, + Dictionary used) { for (var i = 0; i < resources.Length; ++i) { if (used.TryGetValue(resources[i].Id, out var usage)) { - resources[i].Used = usage.Item1; + resources[i].Used = usage.Item1; resources[i].UsedDynamically = usage.Item2; } else { - resources[i].Used = null; + resources[i].Used = null; resources[i].UsedDynamically = null; } } } + foreach (var shader in VertexShaders) { CollectUsed(cUsage, shader.Constants); CollectUsed(sUsage, shader.Samplers); - CollectUsed(uUsage, shader.UAVs); + CollectUsed(uUsage, shader.Uavs); } + foreach (var shader in PixelShaders) { CollectUsed(cUsage, shader.Constants); CollectUsed(sUsage, shader.Samplers); - CollectUsed(uUsage, shader.UAVs); + CollectUsed(uUsage, shader.Uavs); } + CopyUsed(Constants, cUsage); - CopyUsed(Samplers, sUsage); - CopyUsed(UAVs, uUsage); + CopyUsed(Samplers, sUsage); + CopyUsed(Uavs, uUsage); } public void UpdateKeyValues() @@ -656,46 +276,42 @@ public partial class ShpkFile : IWritable { key.DefaultValue, }); + static void CollectValues(HashSet[] valueSets, uint[] values) { for (var i = 0; i < valueSets.Length; ++i) - { valueSets[i].Add(values[i]); - } } + static void CopyValues(Key[] keys, HashSet[] valueSets) { for (var i = 0; i < keys.Length; ++i) - { keys[i].Values = valueSets[i].ToArray(); - } } - var systemKeyValues = InitializeValueSet(SystemKeys); - var sceneKeyValues = InitializeValueSet(SceneKeys); + + var systemKeyValues = InitializeValueSet(SystemKeys); + var sceneKeyValues = InitializeValueSet(SceneKeys); var materialKeyValues = InitializeValueSet(MaterialKeys); - var subViewKeyValues = InitializeValueSet(SubViewKeys); + var subViewKeyValues = InitializeValueSet(SubViewKeys); foreach (var node in Nodes) { - CollectValues(systemKeyValues, node.SystemKeys); - CollectValues(sceneKeyValues, node.SceneKeys); + CollectValues(systemKeyValues, node.SystemKeys); + CollectValues(sceneKeyValues, node.SceneKeys); CollectValues(materialKeyValues, node.MaterialKeys); - CollectValues(subViewKeyValues, node.SubViewKeys); + CollectValues(subViewKeyValues, node.SubViewKeys); } - CopyValues(SystemKeys, systemKeyValues); - CopyValues(SceneKeys, sceneKeyValues); + + CopyValues(SystemKeys, systemKeyValues); + CopyValues(SceneKeys, sceneKeyValues); CopyValues(MaterialKeys, materialKeyValues); - CopyValues(SubViewKeys, subViewKeyValues); + CopyValues(SubViewKeys, subViewKeyValues); } public void SetInvalid() - { - Valid = false; - } + => Valid = false; public void SetChanged() - { - _changed = true; - } + => _changed = true; public bool IsChanged() { @@ -708,7 +324,7 @@ public partial class ShpkFile : IWritable { for (var i = 0; i < resources.Length; ++i) { - resources[i].Used = null; + resources[i].Used = null; resources[i].UsedDynamically = null; } } @@ -718,29 +334,30 @@ public partial class ShpkFile : IWritable var ret = new Resource[count]; for (var i = 0; i < count; ++i) { - var buf = new Resource(); - - buf.Id = r.ReadUInt32(); + var id = r.ReadUInt32(); var strOffset = r.ReadUInt32(); var strSize = r.ReadUInt32(); - buf.Name = strings.GetString((int)strOffset, (int)strSize); - buf.Slot = r.ReadUInt16(); - buf.Size = r.ReadUInt16(); - - ret[i] = buf; + ret[i] = new Resource + { + Id = id, + Name = strings.GetString((int)strOffset, (int)strSize), + Slot = r.ReadUInt16(), + Size = r.ReadUInt16(), + }; } return ret; } - private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DXVersion directX, bool disassemble, ReadOnlySpan blobs, StringPool strings) + private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DxVersion directX, + bool disassemble, ReadOnlySpan blobs, StringPool strings) { var extraHeaderSize = stage switch { DisassembledShader.ShaderStage.Vertex => directX switch { - DXVersion.DirectX9 => 4, - DXVersion.DirectX11 => 8, + DxVersion.DirectX9 => 4, + DxVersion.DirectX11 => 8, _ => throw new NotImplementedException(), }, _ => 0, @@ -755,23 +372,20 @@ public partial class ShpkFile : IWritable var samplerCount = r.ReadUInt16(); var uavCount = r.ReadUInt16(); if (r.ReadUInt16() != 0) - { throw new NotImplementedException(); - } var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize); - var shader = new Shader(); - - shader.Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified; - shader.DirectXVersion = directX; - shader.Constants = ReadResourceArray(r, constantCount, strings); - shader.Samplers = ReadResourceArray(r, samplerCount, strings); - shader.UAVs = ReadResourceArray(r, uavCount, strings); - shader.AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(); - shader.Blob = rawBlob[extraHeaderSize..].ToArray(); - - ret[i] = shader; + ret[i] = new Shader + { + Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified, + DirectXVersion = directX, + Constants = ReadResourceArray(r, constantCount, strings), + Samplers = ReadResourceArray(r, samplerCount, strings), + Uavs = ReadResourceArray(r, uavCount, strings), + AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(), + Blob = rawBlob[extraHeaderSize..].ToArray(), + }; } return ret; @@ -782,46 +396,91 @@ public partial class ShpkFile : IWritable var ret = new Key[count]; for (var i = 0; i < count; ++i) { - var id = r.ReadUInt32(); - var defaultValue = r.ReadUInt32(); - ret[i] = new Key { - Id = id, - DefaultValue = defaultValue, - Values = Array.Empty(), + Id = r.ReadUInt32(), + DefaultValue = r.ReadUInt32(), + Values = Array.Empty(), }; } return ret; } - private static Node[] ReadNodeArray(BinaryReader r, int count, int systemKeyCount, int sceneKeyCount, int materialKeyCount, int subViewKeyCount) + private static Node[] ReadNodeArray(BinaryReader r, int count, int systemKeyCount, int sceneKeyCount, int materialKeyCount, + int subViewKeyCount) { var ret = new Node[count]; for (var i = 0; i < count; ++i) { - var id = r.ReadUInt32(); - var passCount = r.ReadUInt32(); - var passIndices = r.ReadBytes(16); - var systemKeys = r.ReadStructuresAsArray(systemKeyCount); - var sceneKeys = r.ReadStructuresAsArray(sceneKeyCount); - var materialKeys = r.ReadStructuresAsArray(materialKeyCount); - var subViewKeys = r.ReadStructuresAsArray(subViewKeyCount); - var passes = r.ReadStructuresAsArray((int)passCount); - + var id = r.ReadUInt32(); + var passCount = r.ReadUInt32(); ret[i] = new Node { Id = id, - PassIndices = passIndices, - SystemKeys = systemKeys, - SceneKeys = sceneKeys, - MaterialKeys = materialKeys, - SubViewKeys = subViewKeys, - Passes = passes, + PassIndices = r.ReadBytes(16), + SystemKeys = r.ReadStructuresAsArray(systemKeyCount), + SceneKeys = r.ReadStructuresAsArray(sceneKeyCount), + MaterialKeys = r.ReadStructuresAsArray(materialKeyCount), + SubViewKeys = r.ReadStructuresAsArray(subViewKeyCount), + Passes = r.ReadStructuresAsArray((int)passCount), }; } return ret; } + + public enum DxVersion : uint + { + DirectX9 = 9, + DirectX11 = 11, + } + + public struct Resource + { + public uint Id; + public string Name; + public ushort Slot; + public ushort Size; + public DisassembledShader.VectorComponents[]? Used; + public DisassembledShader.VectorComponents? UsedDynamically; + } + + public struct MaterialParam + { + public uint Id; + public ushort ByteOffset; + public ushort ByteSize; + } + + public struct Pass + { + public uint Id; + public uint VertexShader; + public uint PixelShader; + } + + public struct Key + { + public uint Id; + public uint DefaultValue; + public uint[] Values; + } + + public struct Node + { + public uint Id; + public byte[] PassIndices; + public uint[] SystemKeys; + public uint[] SceneKeys; + public uint[] MaterialKeys; + public uint[] SubViewKeys; + public Pass[] Passes; + } + + public struct Item + { + public uint Id; + public uint Node; + } } diff --git a/Penumbra.GameData/UtilityFunctions.cs b/Penumbra.GameData/UtilityFunctions.cs new file mode 100644 index 00000000..71365c09 --- /dev/null +++ b/Penumbra.GameData/UtilityFunctions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Penumbra.GameData; + +public static class UtilityFunctions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T? FirstOrNull(this IEnumerable values, Func predicate) where T : struct + => values.Cast().FirstOrDefault(v => predicate(v!.Value)); +} diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 9f4b4562..d3a91f86 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Numerics; +using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; @@ -11,7 +11,6 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Mods; using Penumbra.String.Classes; -using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.UI.Classes; @@ -176,9 +175,7 @@ public partial class ModEditWindow } private static T? DefaultParseFile( byte[] bytes ) - { - return Activator.CreateInstance( typeof( T ), bytes ) as T; - } + => Activator.CreateInstance( typeof( T ), BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding, bytes ) as T; private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 41e513d6..d9354807 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index c117c0e6..4f5e72ba 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -10,6 +10,7 @@ using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; using OtterGui; +using OtterGui.Classes; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Util; @@ -80,8 +81,8 @@ public partial class ModEditWindow { var extension = file.DirectXVersion switch { - ShpkFile.DXVersion.DirectX9 => ".cso", - ShpkFile.DXVersion.DirectX11 => ".dxbc", + ShpkFile.DxVersion.DirectX9 => ".cso", + ShpkFile.DxVersion.DirectX11 => ".dxbc", _ => throw new NotImplementedException(), }; var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); @@ -148,7 +149,7 @@ public partial class ModEditWindow ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.UAVs, true ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); if( shader.AdditionalHeader.Length > 0 ) { @@ -506,7 +507,7 @@ public partial class ModEditWindow ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled ); ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, file.UAVs, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, file.Uavs, disabled ); static bool DrawKeyArray( string arrayName, bool withId, ShpkFile.Key[] keys, bool _ ) { From 7e56858bc6439a0a8499e71433c4416454da3a8c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 15:20:51 +0100 Subject: [PATCH 0764/2451] Change handling of associated shpk for material. --- Penumbra.GameData/Files/MtrlFile.cs | 4 +- .../UI/Classes/ModEditWindow.Materials.cs | 31 ++++++++++- Penumbra/UI/Classes/ModEditWindow.cs | 55 ++++++++----------- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 6a51a39b..f890f539 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -94,7 +94,7 @@ public partial class MtrlFile : IWritable return samplers; } - public MtrlFile(byte[] data, Func? loadAssociatedShpk = null) + public MtrlFile(byte[] data) { using var stream = new MemoryStream(data); using var r = new BinaryReader(stream); @@ -133,8 +133,6 @@ public partial class MtrlFile : IWritable ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset); - AssociatedShpk = loadAssociatedShpk?.Invoke(ShaderPackage.Name); - AdditionalData = r.ReadBytes(additionalDataSize); for (var i = 0; i < ColorSets.Length; ++i) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index d9354807..f88fa5cf 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Numerics; @@ -17,7 +16,6 @@ using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.String.Functions; using Penumbra.Util; -using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; @@ -31,6 +29,35 @@ public partial class ModEditWindow private uint _materialNewConstantId = 0; private uint _materialNewSamplerId = 0; + /// Load the material with an associated shader package if it can be found. See . + private MtrlFile LoadMtrl( byte[] bytes ) + { + var mtrl = new MtrlFile( bytes ); + if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) + { + return mtrl; + } + + try + { + var shpkFilePath = FindBestMatch( shpkPath ); + var data = shpkFilePath.IsRooted + ? File.ReadAllBytes( shpkFilePath.FullName ) + : Dalamud.GameData.GetFile( shpkFilePath.FullName )?.Data; + if( data?.Length > 0 ) + { + mtrl.AssociatedShpk = new ShpkFile( data ); + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse associated file {shpkPath} to Shpk:\n{e}" ); + mtrl.AssociatedShpk = null; + } + + return mtrl; + } + private bool DrawMaterialPanel( MtrlFile file, bool disabled ) { var ret = DrawMaterialTextureChange( file, disabled ); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 5e5c514e..a3de3c34 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -535,45 +535,36 @@ public partial class ModEditWindow : Window, IDisposable ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); } - // FIXME this probably doesn't belong here - private T? LoadAssociatedFile( string gamePath, Func< byte[], T? > parse ) + /// + /// Find the best matching associated file for a given path. + /// + /// + /// Tries to resolve from the current collection first and chooses the currently resolved file if any exists. + /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. + /// If no redirection is found in either of those options, returns the original path. + /// + private FullPath FindBestMatch( Utf8GamePath path ) { - var defaultFiles = _mod?.Default?.Files; - if( defaultFiles != null ) + var currentFile = Penumbra.CollectionManager.Current.ResolvePath( path ); + if( currentFile != null ) { - if( Utf8GamePath.FromString( gamePath, out var utf8Path, true ) ) + return currentFile.Value; + } + + if( _mod != null ) + { + foreach( var option in _mod.Groups.OrderByDescending( g => g.Priority ) + .SelectMany( g => g.WithIndex().OrderByDescending( o => g.OptionPriority( o.Index ) ).Select( g => g.Value ) ) + .Append( _mod.Default ) ) { - try + if( option.Files.TryGetValue( path, out var value ) || option.FileSwaps.TryGetValue( path, out value ) ) { - if (defaultFiles.TryGetValue( utf8Path, out var fsPath )) - { - return parse( File.ReadAllBytes( fsPath.FullName ) ); - } - } - finally - { - utf8Path.Dispose(); + return value; } } } - var file = Dalamud.GameData.GetFile( gamePath )?.Data; - return file == null ? default : parse( file ); - } - - // FIXME neither does this - private ShpkFile? LoadAssociatedShpk( string shaderName ) - { - var path = $"shader/sm5/shpk/{shaderName}"; - try - { - return LoadAssociatedFile( path, file => new ShpkFile( file ) ); - } - catch( Exception e ) - { - Penumbra.Log.Debug( $"Could not parse associated file {path} to Shpk:\n{e}" ); - return null; - } + return new FullPath( path ); } public ModEditWindow() @@ -583,7 +574,7 @@ public partial class ModEditWindow : Window, IDisposable () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlFile( bytes, LoadAssociatedShpk ) ); + LoadMtrl ); _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel, From ebbc3fed8642e05860b06e47e7a180fe3064c597 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 17:47:59 +0100 Subject: [PATCH 0765/2451] Some material shpk refactoring. --- Penumbra.GameData/UtilityFunctions.cs | 22 + Penumbra.String | 2 +- .../Classes/ModEditWindow.Materials.Shpk.cs | 596 ++++++++++++++++++ .../UI/Classes/ModEditWindow.Materials.cs | 580 ++--------------- .../Classes/ModEditWindow.ShaderPackages.cs | 148 +++-- 5 files changed, 754 insertions(+), 594 deletions(-) create mode 100644 Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs diff --git a/Penumbra.GameData/UtilityFunctions.cs b/Penumbra.GameData/UtilityFunctions.cs index 71365c09..704d7a0c 100644 --- a/Penumbra.GameData/UtilityFunctions.cs +++ b/Penumbra.GameData/UtilityFunctions.cs @@ -10,4 +10,26 @@ public static class UtilityFunctions [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static T? FirstOrNull(this IEnumerable values, Func predicate) where T : struct => values.Cast().FirstOrDefault(v => predicate(v!.Value)); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T[] AddItem(this T[] array, T element, int count = 1) + { + var length = array.Length; + var newArray = new T[array.Length + count]; + Array.Copy(array, newArray, length); + for (var i = length; i < newArray.Length; ++i) + newArray[i] = element; + + return newArray; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T[] RemoveItems(this T[] array, int offset, int count = 1) + { + var newArray = new T[array.Length - count]; + Array.Copy(array, newArray, offset); + Array.Copy(array, offset + count, newArray, offset, newArray.Length - offset); + return newArray; + } } diff --git a/Penumbra.String b/Penumbra.String index 574fd9f8..84f9ec42 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 574fd9f8bb7d957457775a698f5e29a246fab8bd +Subproject commit 84f9ec42cc7039d0731f538e11b0c5be3f766f29 diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs new file mode 100644 index 00000000..1e3e78e3 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -0,0 +1,596 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using Lumina.Data.Parsing; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + + private FullPath FindAssociatedShpk( MtrlFile mtrl ) + { + if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) + { + return FullPath.Empty; + } + + return FindBestMatch( shpkPath ); + } + + private void LoadAssociatedShpk( MtrlFile mtrl ) + { + try + { + _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl ); + var data = _mtrlTabState.LoadedShpkPath.IsRooted + ? File.ReadAllBytes( _mtrlTabState.LoadedShpkPath.FullName ) + : Dalamud.GameData.GetFile( _mtrlTabState.LoadedShpkPath.InternalName.ToString() )?.Data; + if( data?.Length > 0 ) + { + mtrl.AssociatedShpk = new ShpkFile( data ); + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse associated file {_mtrlTabState.LoadedShpkPath} to Shpk:\n{e}" ); + _mtrlTabState.LoadedShpkPath = FullPath.Empty; + mtrl.AssociatedShpk = null; + } + + UpdateTextureLabels( mtrl ); + } + + private void UpdateTextureLabels( MtrlFile file ) + { + var samplers = file.GetSamplersByTexture(); + _mtrlTabState.TextureLabels.Clear(); + _mtrlTabState.TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + for( var i = 0; i < file.Textures.Length; ++i ) + { + var (sampler, shpkSampler) = samplers[ i ]; + var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; + _mtrlTabState.TextureLabels.Add( name ); + _mtrlTabState.TextureLabelWidth = Math.Max( _mtrlTabState.TextureLabelWidth, ImGui.CalcTextSize( name ).X ); + } + } + + _mtrlTabState.TextureLabelWidth = _mtrlTabState.TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + } + + private bool DrawPackageNameInput( MtrlFile file, bool disabled ) + { + var ret = false; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + file.AssociatedShpk = null; + _mtrlTabState.LoadedShpkPath = FullPath.Empty; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + LoadAssociatedShpk( file ); + } + + return ret; + } + + private static bool DrawShaderFlagsInput( MtrlFile file, bool disabled ) + { + var ret = false; + var shpkFlags = ( int )file.ShaderPackage.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Flags = ( uint )shpkFlags; + ret = true; + } + + return ret; + } + + private void DrawCustomAssociations( MtrlFile file, bool disabled ) + { + var text = file.AssociatedShpk == null + ? "Associated .shpk file: None" + : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath}"; + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Selectable( text ); + + if( disabled ) + { + return; + } + + if( ImGui.Button( "Associate custom ShPk file" ) ) + { + _materialFileDialog.OpenFileDialog( "Associate custom .shpk file...", ".shpk", ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); + _mtrlTabState.LoadedShpkPath = new FullPath( name ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load .shpk file {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + } + + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); + }, 1 ); + } + + var defaultFile = FindAssociatedShpk( file ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Associate default ShPk file", Vector2.Zero, defaultFile.FullName, defaultFile.Equals( _mtrlTabState.LoadedShpkPath ) ) ) + { + LoadAssociatedShpk( file ); + if( file.AssociatedShpk != null ) + { + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", + "Penumbra Advanced Editing", NotificationType.Success ); + } + else + { + ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); + } + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + } + + private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + { + var ret = false; + if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) + { + return ret; + } + + ret |= DrawPackageNameInput( file, disabled ); + ret |= DrawShaderFlagsInput( file, disabled ); + DrawCustomAssociations( file, disabled ); + + if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) + { + using var t = ImRaii.TreeNode( "Shader Keys" ); + if( t ) + { + var definedKeys = new HashSet< uint >(); + + foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + { + definedKeys.Add( key.Category ); + using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); + if( t2 ) + { + if( !disabled ) + { + var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); + if( shpkKey.HasValue ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); + if( c ) + { + foreach( var value in shpkKey.Value.Values ) + { + if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) + { + file.ShaderPackage.ShaderKeys[ idx ].Value = value; + ret = true; + } + } + } + } + + if( ImGui.Button( "Remove Key" ) ) + { + file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.RemoveItems( idx ); + ret = true; + } + } + } + } + + if( !disabled && file.AssociatedShpk != null ) + { + var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); + if( missingKeys.Length > 0 ) + { + var selectedKey = Array.Find( missingKeys, key => key.Id == _mtrlTabState.MaterialNewKeyId ); + if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) + { + selectedKey = missingKeys[ 0 ]; + _mtrlTabState.MaterialNewKeyId = selectedKey.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) + { + if( c ) + { + foreach( var key in missingKeys ) + { + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _mtrlTabState.MaterialNewKeyId ) ) + { + selectedKey = key; + _mtrlTabState.MaterialNewKeyId = key.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Key" ) ) + { + file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + { + Category = selectedKey.Id, + Value = selectedKey.DefaultValue, + } ); + ret = true; + } + } + } + } + } + + if( file.AssociatedShpk != null ) + { + var definedKeys = new Dictionary< uint, uint >(); + foreach( var key in file.ShaderPackage.ShaderKeys ) + { + definedKeys[ key.Category ] = key.Value; + } + + var materialKeys = Array.ConvertAll( file.AssociatedShpk.MaterialKeys, key => + { + if( definedKeys.TryGetValue( key.Id, out var value ) ) + { + return value; + } + else + { + return key.DefaultValue; + } + } ); + var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); + var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); + foreach( var node in file.AssociatedShpk.Nodes ) + { + if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[ key.Index ] ) ) + { + foreach( var pass in node.Passes ) + { + vertexShaders.Add( ( int )pass.VertexShader ); + pixelShaders.Add( ( int )pass.PixelShader ); + } + } + } + + ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); + ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( file.ShaderPackage.Constants.Length > 0 + || file.ShaderPackage.ShaderValues.Length > 0 + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) + { + var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); + if( t ) + { + var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet< uint >(); + var hasMalformedConstants = false; + + foreach( var constant in file.ShaderPackage.Constants ) + { + definedConstants.Add( constant.Id ); + var values = file.GetConstantValues( constant ); + if( file.GetConstantValues( constant ).Length > 0 ) + { + var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + aliasedValueCount += values.Length - unique; + } + else + { + hasMalformedConstants = true; + } + } + + foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + { + var values = file.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) + { + var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; + if( ( paramByteOffset & 0x3 ) == 0 ) + { + paramValueOffset = paramByteOffset >> 2; + } + } + + var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); + + using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); + if( t2 ) + { + if( values.Length > 0 ) + { + var valueOffset = constant.ByteOffset >> 2; + + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( + $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", + ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + else + { + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( !disabled + && !hasMalformedConstants + && orphanValues.Count == 0 + && aliasedValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); + for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) + { + if( file.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) + { + file.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; + } + } + + ret = true; + } + } + } + + if( orphanValues.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); + if( t2 ) + { + foreach( var idx in orphanValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", + ref file.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + } + else if( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) + { + var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant + => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); + if( missingConstants.Length > 0 ) + { + var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _mtrlTabState.MaterialNewConstantId ); + if( selectedConstant.ByteSize == 0 ) + { + selectedConstant = missingConstants[ 0 ]; + _mtrlTabState.MaterialNewConstantId = selectedConstant.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); + using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + { + if( c ) + { + foreach( var constant in missingConstants ) + { + var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _mtrlTabState.MaterialNewConstantId ) ) + { + selectedConstant = constant; + _mtrlTabState.MaterialNewConstantId = constant.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); + file.ShaderPackage.Constants = file.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + { + Id = _mtrlTabState.MaterialNewConstantId, + ByteOffset = ( ushort )( file.ShaderPackage.ShaderValues.Length << 2 ), + ByteSize = selectedConstant.ByteSize, + } ); + ret = true; + } + } + } + } + } + + if( file.ShaderPackage.Samplers.Length > 0 + || file.Textures.Length > 0 + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + { + using var t = ImRaii.TreeNode( "Samplers" ); + if( t ) + { + var orphanTextures = new IndexSet( file.Textures.Length, true ); + var aliasedTextureCount = 0; + var definedSamplers = new HashSet< uint >(); + + foreach( var sampler in file.ShaderPackage.Samplers ) + { + if( !orphanTextures.Remove( sampler.TextureIndex ) ) + { + ++aliasedTextureCount; + } + + definedSamplers.Add( sampler.SamplerId ); + } + + foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + { + var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + if( t2 ) + { + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); + + // FIXME this probably doesn't belong here + static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + { + fixed( ushort* v2 = &v ) + { + return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, nint.Zero, nint.Zero, "%04X", flags ); + } + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( InputHexUInt16( "Texture Flags", ref file.Textures[ sampler.TextureIndex ].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + + var sampFlags = ( int )sampler.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; + ret = true; + } + + if( !disabled + && orphanTextures.Count == 0 + && aliasedTextureCount == 0 + && ImGui.Button( "Remove Sampler" ) ) + { + file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); + file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.RemoveItems( idx ); + for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) + { + if( file.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) + { + --file.ShaderPackage.Samplers[ i ].TextureIndex; + } + } + + ret = true; + } + } + } + + if( orphanTextures.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); + if( t2 ) + { + foreach( var idx in orphanTextures ) + { + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[ idx ].Path )} - {file.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) + { + var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); + if( missingSamplers.Length > 0 ) + { + var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _mtrlTabState.MaterialNewSamplerId ); + if( selectedSampler.Name == null ) + { + selectedSampler = missingSamplers[ 0 ]; + _mtrlTabState.MaterialNewSamplerId = selectedSampler.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) + { + if( c ) + { + foreach( var sampler in missingSamplers ) + { + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _mtrlTabState.MaterialNewSamplerId ) ) + { + selectedSampler = sampler; + _mtrlTabState.MaterialNewSamplerId = sampler.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Sampler" ) ) + { + file.Textures = file.Textures.AddItem( new MtrlFile.Texture + { + Path = string.Empty, + Flags = 0, + } ); + file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.AddItem( new Sampler + { + SamplerId = _mtrlTabState.MaterialNewSamplerId, + TextureIndex = ( byte )file.Textures.Length, + Flags = 0, + } ); + ret = true; + } + } + } + } + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index f88fa5cf..ef9e463c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,21 +1,15 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; using ImGuiNET; -using Lumina.Data.Parsing; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.String.Functions; -using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -23,41 +17,32 @@ public partial class ModEditWindow { private readonly FileEditor< MtrlFile > _materialTab; - private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + private struct MtrlTabState + { + public uint MaterialNewKeyId = 0; + public uint MaterialNewConstantId = 0; + public uint MaterialNewSamplerId = 0; + + public readonly List< string > TextureLabels = new(4); + public FullPath LoadedShpkPath = FullPath.Empty; + public float TextureLabelWidth = 0f; + + public MtrlTabState() + { } + } + + private MtrlTabState _mtrlTabState = new(); - private uint _materialNewKeyId = 0; - private uint _materialNewConstantId = 0; - private uint _materialNewSamplerId = 0; /// Load the material with an associated shader package if it can be found. See . private MtrlFile LoadMtrl( byte[] bytes ) { var mtrl = new MtrlFile( bytes ); - if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) - { - return mtrl; - } - - try - { - var shpkFilePath = FindBestMatch( shpkPath ); - var data = shpkFilePath.IsRooted - ? File.ReadAllBytes( shpkFilePath.FullName ) - : Dalamud.GameData.GetFile( shpkFilePath.FullName )?.Data; - if( data?.Length > 0 ) - { - mtrl.AssociatedShpk = new ShpkFile( data ); - } - } - catch( Exception e ) - { - Penumbra.Log.Debug( $"Could not parse associated file {shpkPath} to Shpk:\n{e}" ); - mtrl.AssociatedShpk = null; - } - + LoadAssociatedShpk( mtrl ); return mtrl; } + private bool DrawMaterialPanel( MtrlFile file, bool disabled ) { var ret = DrawMaterialTextureChange( file, disabled ); @@ -79,27 +64,26 @@ public partial class ModEditWindow return !disabled && ret; } - private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + private bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) { - var samplers = file.GetSamplersByTexture(); - var names = new List(); - var maxWidth = 0.0f; - for( var i = 0; i < file.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[i]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - names.Add( name ); - maxWidth = Math.Max( maxWidth, ImGui.CalcTextSize( name ).X ); - } - - using var id = ImRaii.PushId( "Textures" ); - var ret = false; + var ret = false; + using var table = ImRaii.Table( "##Textures", 2 ); + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); for( var i = 0; i < file.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); var tmp = file.Textures[ i ].Path; - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - maxWidth ); - if( ImGui.InputText( names[i], ref tmp, Utf8GamePath.MaxGamePathLength, + ImGui.TableNextColumn(); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( _mtrlTabState.TextureLabels[ i ] ); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 && tmp != file.Textures[ i ].Path ) @@ -197,458 +181,6 @@ public partial class ModEditWindow return ret; } - private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) - { - var ret = false; - - if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) - { - return false; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - var shpkFlags = ( int )file.ShaderPackage.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) - { - file.ShaderPackage.Flags = ( uint )shpkFlags; - ret = true; - } - ImRaii.TreeNode( $"Has associated ShPk file (for advanced editing): {( file.AssociatedShpk != null ? "Yes" : "No" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - if( !disabled ) - { - if( ImGui.Button( "Associate custom ShPk file" ) ) - { - _materialFileDialog.OpenFileDialog( $"Associate custom ShPk file...", ".shpk", ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - ImGui.SameLine(); - if( ImGui.Button( "Associate default ShPk file" ) ) - { - var shpk = LoadAssociatedShpk( file.ShaderPackage.Name ); - if( null != shpk ) - { - file.AssociatedShpk = shpk; - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Success ); - } - else - { - ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); - } - } - } - - if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) - { - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( t ) - { - var definedKeys = new HashSet< uint >(); - - foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) - { - definedKeys.Add( key.Category ); - using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( t2 ) - { - if( !disabled ) - { - var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); - if( c ) - { - foreach( var value in shpkKey.Value.Values ) - { - if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) - { - file.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - } - } - } - } - if( ImGui.Button( "Remove Key" ) ) - { - ArrayRemove( ref file.ShaderPackage.ShaderKeys, idx ); - ret = true; - } - } - } - } - - if( !disabled && file.AssociatedShpk != null ) - { - var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); - if( missingKeys.Length > 0 ) - { - var selectedKey = Array.Find( missingKeys, key => key.Id == _materialNewKeyId ); - if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) - { - selectedKey = missingKeys[0]; - _materialNewKeyId = selectedKey.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) - { - if( c ) - { - foreach( var key in missingKeys ) - { - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _materialNewKeyId ) ) - { - selectedKey = key; - _materialNewKeyId = key.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) - { - ArrayAdd( ref file.ShaderPackage.ShaderKeys, new ShaderKey - { - Category = selectedKey.Id, - Value = selectedKey.DefaultValue, - } ); - ret = true; - } - } - } - } - } - - if( file.AssociatedShpk != null ) - { - var definedKeys = new Dictionary< uint, uint >(); - foreach( var key in file.ShaderPackage.ShaderKeys ) - { - definedKeys[key.Category] = key.Value; - } - var materialKeys = Array.ConvertAll(file.AssociatedShpk.MaterialKeys, key => - { - if( definedKeys.TryGetValue( key.Id, out var value ) ) - { - return value; - } - else - { - return key.DefaultValue; - } - } ); - var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); - var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); - foreach( var node in file.AssociatedShpk.Nodes ) - { - if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) - { - foreach( var pass in node.Passes ) - { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); - } - } - } - ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) - { - var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); - if( t ) - { - var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet< uint >(); - var hasMalformedConstants = false; - - foreach( var constant in file.ShaderPackage.Constants ) - { - definedConstants.Add( constant.Id ); - var values = file.GetConstantValues( constant ); - if( file.GetConstantValues( constant ).Length > 0 ) - { - var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - aliasedValueCount += values.Length - unique; - } - else - { - hasMalformedConstants = true; - } - } - - foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) - { - var values = file.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) - { - var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - } - var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); - - using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ( ": " + constantName ) : "" )} (ID: 0x{constant.Id:X8})" ); - if( t2 ) - { - if( values.Length > 0 ) - { - var valueOffset = constant.ByteOffset >> 2; - - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( ( valueOffset + valueIdx ) << 2 ):X4})", - ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - else - { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( !disabled && !hasMalformedConstants && orphanValues.Count == 0 && aliasedValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) - { - ArrayRemove( ref file.ShaderPackage.ShaderValues, constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - ArrayRemove( ref file.ShaderPackage.Constants, idx ); - for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) - { - if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) - { - file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - } - ret = true; - } - } - } - - if( orphanValues.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); - if( t2 ) - { - foreach( var idx in orphanValues ) - { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{( idx << 2 ):X4})", - ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - } - else if ( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) - { - var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); - if( missingConstants.Length > 0 ) - { - var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _materialNewConstantId ); - if( selectedConstant.ByteSize == 0 ) - { - selectedConstant = missingConstants[0]; - _materialNewConstantId = selectedConstant.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); - using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) - { - if( c ) - { - foreach( var constant in missingConstants ) - { - var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _materialNewConstantId ) ) - { - selectedConstant = constant; - _materialNewConstantId = constant.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) - { - var valueOffset = ArrayAdd( ref file.ShaderPackage.ShaderValues, 0.0f, selectedConstant.ByteSize >> 2 ); - ArrayAdd( ref file.ShaderPackage.Constants, new MtrlFile.Constant - { - Id = _materialNewConstantId, - ByteOffset = ( ushort )( valueOffset << 2 ), - ByteSize = selectedConstant.ByteSize, - } ); - ret = true; - } - } - } - } - } - - if( file.ShaderPackage.Samplers.Length > 0 || file.Textures.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) - { - using var t = ImRaii.TreeNode( "Samplers" ); - if( t ) - { - var orphanTextures = new IndexSet( file.Textures.Length, true ); - var aliasedTextureCount = 0; - var definedSamplers = new HashSet< uint >(); - - foreach( var sampler in file.ShaderPackage.Samplers ) - { - if( !orphanTextures.Remove( sampler.TextureIndex ) ) - { - ++aliasedTextureCount; - } - definedSamplers.Add( sampler.SamplerId ); - } - - foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) - { - var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ( ": " + shpkSampler.Value.Name ) : "" )} (ID: 0x{sampler.SamplerId:X8})" ); - if( t2 ) - { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) - { - fixed( ushort* v2 = &v ) - { - return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%04X", flags ); - } - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - var sampFlags = ( int )sampler.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) - { - file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; - ret = true; - } - - if( !disabled && orphanTextures.Count == 0 && aliasedTextureCount == 0 - && ImGui.Button( "Remove Sampler" ) ) - { - ArrayRemove( ref file.Textures, sampler.TextureIndex ); - ArrayRemove( ref file.ShaderPackage.Samplers, idx ); - for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) - { - if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) - { - --file.ShaderPackage.Samplers[i].TextureIndex; - } - } - ret = true; - } - } - } - - if( orphanTextures.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); - if( t2 ) - { - foreach( var idx in orphanTextures ) - { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) - { - var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); - if( missingSamplers.Length > 0 ) - { - var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _materialNewSamplerId ); - if( selectedSampler.Name == null ) - { - selectedSampler = missingSamplers[0]; - _materialNewSamplerId = selectedSampler.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) - { - if( c ) - { - foreach( var sampler in missingSamplers ) - { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _materialNewSamplerId ) ) - { - selectedSampler = sampler; - _materialNewSamplerId = sampler.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Sampler" ) ) - { - var texIndex = ArrayAdd( ref file.Textures, new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - } ); - ArrayAdd( ref file.ShaderPackage.Samplers, new Sampler - { - SamplerId = _materialNewSamplerId, - TextureIndex = ( byte )texIndex, - Flags = 0, - } ); - ret = true; - } - } - } - } - } - - return ret; - } - private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled ) { var ret = false; @@ -1116,58 +648,35 @@ public partial class ModEditWindow } } - // FIXME this probably doesn't belong here - // Also used in ShaderPackages - private static int ArrayAdd( ref T[] array, T element, int count = 1 ) - { - var length = array.Length; - var newArray = new T[array.Length + count]; - Array.Copy( array, newArray, length ); - for( var i = 0; i < count; ++i ) - { - newArray[length + i] = element; - } - array = newArray; - return length; - } - - private static void ArrayRemove( ref T[] array, int offset, int count = 1 ) - { - var newArray = new T[array.Length - count]; - Array.Copy( array, newArray, offset ); - Array.Copy( array, offset + count, newArray, offset, newArray.Length - offset ); - array = newArray; - } - private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) { if( valueLength == 0 || valueOffset < 0 ) { - return (null, false); + return ( null, false ); } - var firstVector = valueOffset >> 2; - var lastVector = ( valueOffset + valueLength - 1 ) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; + var firstVector = valueOffset >> 2; + var lastVector = ( valueOffset + valueLength - 1 ) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; static string VectorSwizzle( int firstComponent, int numComponents ) - => ( numComponents == 4 ) ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); + => numComponents == 4 ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); if( firstVector == lastVector ) { - return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); } var parts = new string[lastVector + 1 - firstVector]; - parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; for( var i = firstVector + 1; i < lastVector; ++i ) { - parts[i - firstVector] = $"[{i}]"; + parts[ i - firstVector ] = $"[{i}]"; } - return (string.Join( ", ", parts ), false); + return ( string.Join( ", ", parts ), false ); } private static string? MaterialParamName( bool componentOnly, int offset ) @@ -1176,7 +685,8 @@ public partial class ModEditWindow { return null; } - var component = "xyzw"[offset & 0x3]; + + var component = "xyzw"[ offset & 0x3 ]; return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; } diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 4f5e72ba..9809874b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -11,6 +11,7 @@ using Lumina.Misc; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Util; @@ -19,14 +20,14 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor _shaderPackageTab; + private readonly FileEditor< ShpkFile > _shaderPackageTab; private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); - private string _shaderPackageNewMaterialParamName = string.Empty; - private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + private string _shaderPackageNewMaterialParamName = string.Empty; + private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); private ushort _shaderPackageNewMaterialParamStart = 0; - private ushort _shaderPackageNewMaterialParamEnd = 0; + private ushort _shaderPackageNewMaterialParamEnd = 0; private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) { @@ -86,7 +87,7 @@ public partial class ModEditWindow _ => throw new NotImplementedException(), }; var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); - var blob = shader.Blob; + var blob = shader.Blob; _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) => { if( !success ) @@ -101,12 +102,16 @@ public partial class ModEditWindow catch( Exception e ) { Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); return; } - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + + ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); } ); } + if( !disabled ) { ImGui.SameLine(); @@ -121,7 +126,7 @@ public partial class ModEditWindow try { - shaders[idx].Blob = File.ReadAllBytes( name ); + shaders[ idx ].Blob = File.ReadAllBytes( name ); } catch( Exception e ) { @@ -129,18 +134,21 @@ public partial class ModEditWindow ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } + try { - shaders[idx].UpdateResources( file ); + shaders[ idx ].UpdateResources( file ); file.UpdateResources(); } catch( Exception e ) { file.SetInvalid(); Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); return; } + file.SetChanged(); ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); } ); @@ -187,7 +195,7 @@ public partial class ModEditWindow return false; } - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == ( materialParams.Value.Size << 4 ) ); + var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); if( !isSizeWellDefined ) { @@ -201,26 +209,27 @@ public partial class ModEditWindow } } - var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu) >> 2]; - var orphanParameters = new IndexSet( parameters.Length, true ); - var definedParameters = new HashSet< uint >(); + var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 2]; + var orphanParameters = new IndexSet( parameters.Length, true ); + var definedParameters = new HashSet< uint >(); var hasMalformedParameters = false; foreach( var param in file.MaterialParams ) { definedParameters.Add( param.Id ); - if( ( param.ByteOffset & 0x3 ) == 0 && ( param.ByteSize & 0x3 ) == 0 - && ( param.ByteOffset + param.ByteSize ) <= file.MaterialParamsSize ) + if( ( param.ByteOffset & 0x3 ) == 0 + && ( param.ByteSize & 0x3 ) == 0 + && param.ByteOffset + param.ByteSize <= file.MaterialParamsSize ) { var valueOffset = param.ByteOffset >> 2; - var valueCount = param.ByteSize >> 2; + var valueCount = param.ByteSize >> 2; orphanParameters.RemoveRange( valueOffset, valueCount ); - parameters[valueOffset] = (param.Id, true); + parameters[ valueOffset ] = ( param.Id, true ); for( var i = 1; i < valueCount; ++i ) { - parameters[valueOffset + i] = (param.Id, false); + parameters[ valueOffset + i ] = ( param.Id, false ); } } else @@ -232,7 +241,7 @@ public partial class ModEditWindow ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); using( var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) { if( table ) { @@ -247,25 +256,26 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableHeader( "w" ); - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity + var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red + var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); for( var idx = 0; idx < parameters.Length; idx += 4 ) { - var usedComponents = ( materialParams?.Used?[idx >> 2] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); + var usedComponents = ( materialParams?.Used?[ idx >> 2 ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); ImGui.TableNextColumn(); ImGui.Text( $"[{idx >> 2}]" ); for( var col = 0; col < 4; ++col ) { - var cell = parameters[idx + col]; + var cell = parameters[ idx + col ]; ImGui.TableNextColumn(); - var start = cell.HasValue && cell.Value.Item2; - var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; - using var c = ImRaii.PushColor( ImGuiCol.Text, used ? ( start ? textColorStart : textColorCont ) : ( start ? textColorUnusedStart : textColorUnusedCont ) ); + var start = cell.HasValue && cell.Value.Item2; + var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; + using var c = ImRaii.PushColor( ImGuiCol.Text, used ? start ? textColorStart : textColorCont : start ? textColorUnusedStart : textColorUnusedCont ); ImGui.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" ); } + ImGui.TableNextRow(); } } @@ -282,9 +292,10 @@ public partial class ModEditWindow { ImRaii.TreeNode( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } - else if( ( param.ByteOffset + param.ByteSize ) > file.MaterialParamsSize ) + else if( param.ByteOffset + param.ByteSize > file.MaterialParamsSize ) { - ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", + ImGuiTreeNodeFlags.Leaf ).Dispose(); } } } @@ -296,27 +307,30 @@ public partial class ModEditWindow { for( var i = 0; i < file.MaterialParams.Length; ++i ) { - var param = file.MaterialParams[i]; - using var t2 = ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); + var param = file.MaterialParams[ i ]; + using var t2 = ImRaii.TreeNode( + $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); if( t2 ) { if( ImGui.Button( "Remove" ) ) { - ArrayRemove( ref file.MaterialParams, i ); - ret = true; + file.MaterialParams = file.MaterialParams.RemoveItems( i ); + ret = true; } } } + if( orphanParameters.Count > 0 ) { using var t2 = ImRaii.TreeNode( "New Parameter" ); if( t2 ) { var starts = orphanParameters.ToArray(); - if( !orphanParameters[_shaderPackageNewMaterialParamStart] ) + if( !orphanParameters[ _shaderPackageNewMaterialParamStart ] ) { - _shaderPackageNewMaterialParamStart = ( ushort )starts[0]; + _shaderPackageNewMaterialParamStart = ( ushort )starts[ 0 ]; } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) @@ -333,16 +347,19 @@ public partial class ModEditWindow } } } + var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; - var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => { + var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => + { var ret = i <= lastEndCandidate + 1; lastEndCandidate = i; return ret; } ).ToArray(); - if( Array.IndexOf(ends, _shaderPackageNewMaterialParamEnd) < 0 ) + if( Array.IndexOf( ends, _shaderPackageNewMaterialParamEnd ) < 0 ) { - _shaderPackageNewMaterialParamEnd = ( ushort )ends[0]; + _shaderPackageNewMaterialParamEnd = ( ushort )ends[ 0 ]; } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) @@ -359,26 +376,29 @@ public partial class ModEditWindow } } } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) { _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); } + ImGui.SameLine(); ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); if( ImGui.Button( "Add" ) ) { if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) { - ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", + NotificationType.Error ); } else { - ArrayAdd( ref file.MaterialParams, new ShpkFile.MaterialParam + file.MaterialParams = file.MaterialParams.AddItem( new ShpkFile.MaterialParam { - Id = _shaderPackageNewMaterialParamId, - ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), - ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), + Id = _shaderPackageNewMaterialParamId, + ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), + ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), } ); ret = true; } @@ -408,7 +428,10 @@ public partial class ModEditWindow foreach( var (buf, idx) in resources.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ), ( !disabled || buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf ); + using var t2 = ImRaii.TreeNode( + $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ), + !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf ); if( t2 ) { if( !disabled ) @@ -423,22 +446,22 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputUInt16( $"{char.ToUpper( slotLabel[0] )}{slotLabel[1..].ToLower()}", ref resources[idx].Slot, ImGuiInputTextFlags.None ) ) + if( InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resources[ idx ].Slot, ImGuiInputTextFlags.None ) ) { ret = true; } } + if( buf.Used != null ) { - var used = new List(); + var used = new List< string >(); if( withSize ) { - foreach( var (components, i) in ( buf.Used ?? Array.Empty() ).WithIndex() ) + foreach( var (components, i) in ( buf.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) { switch( components ) { - case 0: - break; + case 0: break; case DisassembledShader.VectorComponents.All: used.Add( $"[{i}]" ); break; @@ -447,10 +470,10 @@ public partial class ModEditWindow break; } } + switch( buf.UsedDynamically ?? 0 ) { - case 0: - break; + case 0: break; case DisassembledShader.VectorComponents.All: used.Add( "[*]" ); break; @@ -461,24 +484,28 @@ public partial class ModEditWindow } else { - var components = ( ( buf.Used != null && buf.Used.Length > 0 ) ? buf.Used[0] : 0 ) | ( buf.UsedDynamically ?? 0 ); + var components = ( buf.Used != null && buf.Used.Length > 0 ? buf.Used[ 0 ] : 0 ) | ( buf.UsedDynamically ?? 0 ); if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) { used.Add( "Red" ); } + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) { used.Add( "Green" ); } + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) { used.Add( "Blue" ); } + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) { used.Add( "Alpha" ); } } + if( used.Count > 0 ) { ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); @@ -552,24 +579,29 @@ public partial class ModEditWindow { foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) { - ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) { - ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) { - ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) { ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); foreach( var (pass, passIdx) in node.Passes.WithIndex() ) { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); } } } From 47ddca05065b159058139298a4fc8310e8802b90 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 18:29:54 +0100 Subject: [PATCH 0766/2451] Add option to display single select groups as radio buttons. --- Penumbra/Configuration.cs | 1 + Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 102 ++++++++++++------ .../UI/ConfigWindow.SettingsTab.General.cs | 28 +++++ 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9b7e4c5e..6fd70b03 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -68,6 +68,7 @@ public partial class Configuration : IPluginConfiguration public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; + public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 8f9b8c36..9d482c35 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -64,19 +64,27 @@ public partial class ConfigWindow if( _mod.Groups.Count > 0 ) { var useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Single && g.Value.IsOption ) ) + foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Single && g.Value.Count > Penumbra.Config.SingleGroupRadioMax ) ) { ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); useDummy = false; - DrawSingleGroup( group, idx ); + DrawSingleGroupCombo( group, idx ); } useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Multi && g.Value.IsOption ) ) + foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.IsOption ) ) { ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); useDummy = false; - DrawMultiGroup( group, idx ); + switch( group.Type ) + { + case GroupType.Multi: + DrawMultiGroup( group, idx ); + break; + case GroupType.Single when group.Count <= Penumbra.Config.SingleGroupRadioMax: + DrawSingleGroupRadio( group, idx ); + break; + } } } @@ -162,47 +170,49 @@ public partial class ConfigWindow + "If no inherited collection has settings for this mod, it will be disabled." ); } + // Draw a single group selector as a combo box. // If a description is provided, add a help marker besides it. - private void DrawSingleGroup( IModGroup group, int groupIdx ) + private void DrawSingleGroupCombo( IModGroup group, int groupIdx ) { using var id = ImRaii.PushId( groupIdx ); var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); - using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); - if( combo ) + using( var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ) ) { - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + if( combo ) { - id.Push( idx2 ); - var option = group[ idx2 ]; - if( ImGui.Selectable( option.Name, idx2 == selectedOption ) ) + for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); - } - - if( option.Description.Length > 0 ) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + id.Push( idx2 ); + var option = group[ idx2 ]; + if( ImGui.Selectable( option.Name, idx2 == selectedOption ) ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetColorU32( ImGuiCol.TextDisabled ) ); - ImGuiUtil.RightAlign( FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X ); + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); } - if( hovered ) + if( option.Description.Length > 0 ) { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted( option.Description ); - } - } + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetColorU32( ImGuiCol.TextDisabled ) ); + ImGuiUtil.RightAlign( FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X ); + } - id.Pop(); + if( hovered ) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted( option.Description ); + } + } + + id.Pop(); + } } } - combo.Dispose(); ImGui.SameLine(); if( group.Description.Length > 0 ) { @@ -214,6 +224,34 @@ public partial class ConfigWindow } } + // Draw a single group selector as a set of radio buttons. + // If a description is provided, add a help marker besides it. + private void DrawSingleGroupRadio( IModGroup group, int groupIdx ) + { + using var id = ImRaii.PushId( groupIdx ); + var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; + Widget.BeginFramedGroup( group.Name, group.Description ); + for( var idx = 0; idx < group.Count; ++idx ) + { + id.Push( idx ); + var option = group[ idx ]; + if( ImGui.RadioButton( option.Name, selectedOption == idx ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx ); + } + + if( option.Description.Length > 0 ) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker( option.Description ); + } + + id.Pop( idx ); + } + + Widget.EndFramedGroup(); + } + // Draw a multi group selector as a bordered set of checkboxes. // If a description is provided, add a help marker in the title. private void DrawMultiGroup( IModGroup group, int groupIdx ) @@ -221,11 +259,11 @@ public partial class ConfigWindow using var id = ImRaii.PushId( groupIdx ); var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; Widget.BeginFramedGroup( group.Name, group.Description ); - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) + for( var idx = 0; idx < group.Count; ++idx ) { - var option = group[ idx2 ]; - id.Push( idx2 ); - var flag = 1u << idx2; + var option = group[ idx ]; + id.Push( idx ); + var flag = 1u << idx; var setting = ( flags & flag ) != 0; if( ImGui.Checkbox( option.Name, ref setting ) ) { diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index ea73e1b4..62acbba7 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -27,6 +27,32 @@ public partial class ConfigWindow ImGuiUtil.LabeledHelpMarker( label, tooltip ); } + private static int _singleGroupRadioMax = int.MaxValue; + private void DrawSingleSelectRadioMax() + { + if ( _singleGroupRadioMax == int.MaxValue) + _singleGroupRadioMax = Penumbra.Config.SingleGroupRadioMax; + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); + if( ImGui.DragInt( "##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1 ) ) + { + _singleGroupRadioMax = Math.Max( 1, _singleGroupRadioMax ); + } + + if (ImGui.IsItemDeactivated()) + { + if( _singleGroupRadioMax != Penumbra.Config.SingleGroupRadioMax ) + { + Penumbra.Config.SingleGroupRadioMax = _singleGroupRadioMax; + Penumbra.Config.Save(); + } + + _singleGroupRadioMax = int.MaxValue; + } + ImGuiUtil.LabeledHelpMarker( "Upper Limit for Single-Selection Group Radio Buttons", + "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" + + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons." ); + } + private void DrawModSelectorSettings() { #if DEBUG @@ -62,6 +88,7 @@ public partial class ConfigWindow Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); + Checkbox( "Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); ImGui.Dummy( _window._defaultSpace ); @@ -83,6 +110,7 @@ public partial class ConfigWindow "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); ImGui.Dummy( _window._defaultSpace ); + DrawSingleSelectRadioMax(); DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); From b31c5fdd1fb042370f75db96060e3367b087b7a6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 18:33:12 +0100 Subject: [PATCH 0767/2451] Fix accidentally reverted change. --- Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 1e3e78e3..1624cd84 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -144,7 +144,7 @@ public partial class ModEditWindow ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - }, 1 ); + }); } var defaultFile = FindAssociatedShpk( file ); From 9cf69def7bf36d1befaa25f45c3b18a74fdbc0e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Feb 2023 20:44:06 +0100 Subject: [PATCH 0768/2451] Fix mistaken rename. --- Penumbra.GameData/Files/ShpkFile.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs index 75651ef9..5b88bb00 100644 --- a/Penumbra.GameData/Files/ShpkFile.cs +++ b/Penumbra.GameData/Files/ShpkFile.cs @@ -122,21 +122,21 @@ public partial class ShpkFile : IWritable SceneKeys = ReadKeyArray(r, (int)sceneKeyCount); MaterialKeys = ReadKeyArray(r, (int)materialKeyCount); - var subViewKey1Null = r.ReadUInt32(); - var subViewKey2Null = r.ReadUInt32(); + var subViewKey1Default = r.ReadUInt32(); + var subViewKey2Default = r.ReadUInt32(); SubViewKeys = new Key[] { new() { Id = 1, - DefaultValue = subViewKey1Null, + DefaultValue = subViewKey1Default, Values = Array.Empty(), }, new() { Id = 2, - DefaultValue = subViewKey2Null, + DefaultValue = subViewKey2Default, Values = Array.Empty(), }, }; From d4f1097ebacd51782fe18cd76ca84accf8df7584 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Feb 2023 00:09:42 +0100 Subject: [PATCH 0769/2451] Fix issue when extracting some textures. --- Penumbra/Util/PenumbraSqPackStream.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 7bb0687d..0109ff35 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -305,6 +305,9 @@ public class PenumbraSqPackStream : IDisposable // i is for texture blocks, j is 'data blocks'... for( byte i = 0; i < blocks.Count; i++ ) { + if( blocks[ i ].CompressedSize == 0 ) + continue; + // start from comp_offset var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; ReadFileBlock( runningBlockTotal, ms, true ); From 7619503a2b1a0758a3c7b7ee3a8097088cf71a87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Feb 2023 15:34:20 +0100 Subject: [PATCH 0770/2451] Split huge material shpk gui function. --- .../Classes/ModEditWindow.Materials.Shpk.cs | 181 ++++++++++++------ .../UI/Classes/ModEditWindow.Materials.cs | 43 ----- 2 files changed, 125 insertions(+), 99 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 1624cd84..8b7a0a28 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -60,7 +61,7 @@ public partial class ModEditWindow var samplers = file.GetSamplersByTexture(); _mtrlTabState.TextureLabels.Clear(); _mtrlTabState.TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { for( var i = 0; i < file.Textures.Length; ++i ) { @@ -166,24 +167,15 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); } - private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + private bool DrawMaterialShaderKeys( MtrlFile file, bool disabled ) { var ret = false; - if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) - { - return ret; - } - - ret |= DrawPackageNameInput( file, disabled ); - ret |= DrawShaderFlagsInput( file, disabled ); - DrawCustomAssociations( file, disabled ); - if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) { using var t = ImRaii.TreeNode( "Shader Keys" ); if( t ) { - var definedKeys = new HashSet< uint >(); + var definedKeys = new HashSet(); foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) { @@ -204,8 +196,8 @@ public partial class ModEditWindow { if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) { - file.ShaderPackage.ShaderKeys[ idx ].Value = value; - ret = true; + file.ShaderPackage.ShaderKeys[idx].Value = value; + ret = true; } } } @@ -214,7 +206,7 @@ public partial class ModEditWindow if( ImGui.Button( "Remove Key" ) ) { file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.RemoveItems( idx ); - ret = true; + ret = true; } } } @@ -228,7 +220,7 @@ public partial class ModEditWindow var selectedKey = Array.Find( missingKeys, key => key.Id == _mtrlTabState.MaterialNewKeyId ); if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) { - selectedKey = missingKeys[ 0 ]; + selectedKey = missingKeys[0]; _mtrlTabState.MaterialNewKeyId = selectedKey.Id; } @@ -241,7 +233,7 @@ public partial class ModEditWindow { if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _mtrlTabState.MaterialNewKeyId ) ) { - selectedKey = key; + selectedKey = key; _mtrlTabState.MaterialNewKeyId = key.Id; } } @@ -254,7 +246,7 @@ public partial class ModEditWindow file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.AddItem( new ShaderKey { Category = selectedKey.Id, - Value = selectedKey.DefaultValue, + Value = selectedKey.DefaultValue, } ); ret = true; } @@ -263,12 +255,17 @@ public partial class ModEditWindow } } + return ret; + } + + private void DrawMaterialShaders( MtrlFile file ) + { if( file.AssociatedShpk != null ) { - var definedKeys = new Dictionary< uint, uint >(); + var definedKeys = new Dictionary(); foreach( var key in file.ShaderPackage.ShaderKeys ) { - definedKeys[ key.Category ] = key.Value; + definedKeys[key.Category] = key.Value; } var materialKeys = Array.ConvertAll( file.AssociatedShpk.MaterialKeys, key => @@ -286,7 +283,7 @@ public partial class ModEditWindow var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); foreach( var node in file.AssociatedShpk.Nodes ) { - if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[ key.Index ] ) ) + if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) { foreach( var pass in node.Passes ) { @@ -300,8 +297,12 @@ public partial class ModEditWindow .Dispose(); ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + } - if( file.ShaderPackage.Constants.Length > 0 + private bool DrawMaterialConstants( MtrlFile file, bool disabled ) + { + var ret = false; + if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) { @@ -310,9 +311,9 @@ public partial class ModEditWindow using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); if( t ) { - var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet< uint >(); + var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet(); var hasMalformedConstants = false; foreach( var constant in file.ShaderPackage.Constants ) @@ -332,11 +333,11 @@ public partial class ModEditWindow foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) { - var values = file.GetConstantValues( constant ); + var values = file.GetConstantValues( constant ); var paramValueOffset = -values.Length; if( values.Length > 0 ) { - var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; if( ( paramByteOffset & 0x3 ) == 0 ) { @@ -358,7 +359,7 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); if( ImGui.InputFloat( $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", - ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", + ref values[valueIdx], 0.0f, 0.0f, "%.3f", disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; @@ -374,16 +375,16 @@ public partial class ModEditWindow if( !disabled && !hasMalformedConstants && orphanValues.Count == 0 - && aliasedValueCount == 0 + && aliasedValueCount == 0 && ImGui.Button( "Remove Constant" ) ) { file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); + file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) { - if( file.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) + if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) { - file.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; + file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; } } @@ -401,7 +402,7 @@ public partial class ModEditWindow { ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref file.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; @@ -418,7 +419,7 @@ public partial class ModEditWindow var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _mtrlTabState.MaterialNewConstantId ); if( selectedConstant.ByteSize == 0 ) { - selectedConstant = missingConstants[ 0 ]; + selectedConstant = missingConstants[0]; _mtrlTabState.MaterialNewConstantId = selectedConstant.Id; } @@ -433,7 +434,7 @@ public partial class ModEditWindow var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _mtrlTabState.MaterialNewConstantId ) ) { - selectedConstant = constant; + selectedConstant = constant; _mtrlTabState.MaterialNewConstantId = constant.Id; } } @@ -446,9 +447,9 @@ public partial class ModEditWindow file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); file.ShaderPackage.Constants = file.ShaderPackage.Constants.AddItem( new MtrlFile.Constant { - Id = _mtrlTabState.MaterialNewConstantId, + Id = _mtrlTabState.MaterialNewConstantId, ByteOffset = ( ushort )( file.ShaderPackage.ShaderValues.Length << 2 ), - ByteSize = selectedConstant.ByteSize, + ByteSize = selectedConstant.ByteSize, } ); ret = true; } @@ -457,16 +458,22 @@ public partial class ModEditWindow } } + return ret; + } + + private bool DrawMaterialSamplers( MtrlFile file, bool disabled ) + { + var ret = false; if( file.ShaderPackage.Samplers.Length > 0 - || file.Textures.Length > 0 + || file.Textures.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) { using var t = ImRaii.TreeNode( "Samplers" ); if( t ) { - var orphanTextures = new IndexSet( file.Textures.Length, true ); + var orphanTextures = new IndexSet( file.Textures.Length, true ); var aliasedTextureCount = 0; - var definedSamplers = new HashSet< uint >(); + var definedSamplers = new HashSet(); foreach( var sampler in file.ShaderPackage.Samplers ) { @@ -480,11 +487,11 @@ public partial class ModEditWindow foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) { - var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); if( t2 ) { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ) .Dispose(); // FIXME this probably doesn't belong here @@ -497,7 +504,7 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref file.Textures[ sampler.TextureIndex ].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; } @@ -507,22 +514,22 @@ public partial class ModEditWindow if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) { - file.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; - ret = true; + file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; + ret = true; } if( !disabled && orphanTextures.Count == 0 - && aliasedTextureCount == 0 + && aliasedTextureCount == 0 && ImGui.Button( "Remove Sampler" ) ) { - file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); + file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.RemoveItems( idx ); for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) { - if( file.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) + if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) { - --file.ShaderPackage.Samplers[ i ].TextureIndex; + --file.ShaderPackage.Samplers[i].TextureIndex; } } @@ -538,7 +545,7 @@ public partial class ModEditWindow { foreach( var idx in orphanTextures ) { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[ idx ].Path )} - {file.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } } } @@ -550,7 +557,7 @@ public partial class ModEditWindow var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _mtrlTabState.MaterialNewSamplerId ); if( selectedSampler.Name == null ) { - selectedSampler = missingSamplers[ 0 ]; + selectedSampler = missingSamplers[0]; _mtrlTabState.MaterialNewSamplerId = selectedSampler.Id; } @@ -563,7 +570,7 @@ public partial class ModEditWindow { if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _mtrlTabState.MaterialNewSamplerId ) ) { - selectedSampler = sampler; + selectedSampler = sampler; _mtrlTabState.MaterialNewSamplerId = sampler.Id; } } @@ -575,14 +582,14 @@ public partial class ModEditWindow { file.Textures = file.Textures.AddItem( new MtrlFile.Texture { - Path = string.Empty, + Path = string.Empty, Flags = 0, } ); file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.AddItem( new Sampler { - SamplerId = _mtrlTabState.MaterialNewSamplerId, + SamplerId = _mtrlTabState.MaterialNewSamplerId, TextureIndex = ( byte )file.Textures.Length, - Flags = 0, + Flags = 0, } ); ret = true; } @@ -593,4 +600,66 @@ public partial class ModEditWindow return ret; } + + private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + { + var ret = false; + if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) + { + return ret; + } + + ret |= DrawPackageNameInput( file, disabled ); + ret |= DrawShaderFlagsInput( file, disabled ); + DrawCustomAssociations( file, disabled ); + ret |= DrawMaterialShaderKeys( file, disabled ); + DrawMaterialShaders( file ); + ret |= DrawMaterialConstants( file, disabled ); + ret |= DrawMaterialSamplers( file, disabled ); + return ret; + } + + + private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + { + if( valueLength == 0 || valueOffset < 0 ) + { + return (null, false); + } + + var firstVector = valueOffset >> 2; + var lastVector = ( valueOffset + valueLength - 1 ) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; + + static string VectorSwizzle( int firstComponent, int numComponents ) + => numComponents == 4 ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); + + if( firstVector == lastVector ) + { + return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + } + + var parts = new string[lastVector + 1 - firstVector]; + parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + for( var i = firstVector + 1; i < lastVector; ++i ) + { + parts[i - firstVector] = $"[{i}]"; + } + + return (string.Join( ", ", parts ), false); + } + + private static string? MaterialParamName( bool componentOnly, int offset ) + { + if( offset < 0 ) + { + return null; + } + + var component = "xyzw"[offset & 0x3]; + + return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index ef9e463c..062fffd6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -647,47 +647,4 @@ public partial class ModEditWindow } } } - - private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) - { - if( valueLength == 0 || valueOffset < 0 ) - { - return ( null, false ); - } - - var firstVector = valueOffset >> 2; - var lastVector = ( valueOffset + valueLength - 1 ) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; - - static string VectorSwizzle( int firstComponent, int numComponents ) - => numComponents == 4 ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); - - if( firstVector == lastVector ) - { - return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); - } - - var parts = new string[lastVector + 1 - firstVector]; - parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; - for( var i = firstVector + 1; i < lastVector; ++i ) - { - parts[ i - firstVector ] = $"[{i}]"; - } - - return ( string.Join( ", ", parts ), false ); - } - - private static string? MaterialParamName( bool componentOnly, int offset ) - { - if( offset < 0 ) - { - return null; - } - - var component = "xyzw"[ offset & 0x3 ]; - - return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; - } } \ No newline at end of file From c4a4aec221c513f81719decc868b31ff67333ace Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Feb 2023 16:12:41 +0100 Subject: [PATCH 0771/2451] Some more material shpk restructuring --- Penumbra.GameData/Data/GamePaths.cs | 6 ++ Penumbra.String | 2 +- .../Classes/ModEditWindow.Materials.Shpk.cs | 64 +++++++++++-------- .../UI/Classes/ModEditWindow.Materials.cs | 14 ++-- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index 22d5ac69..ed6078c7 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -401,4 +401,10 @@ public static partial class GamePaths [GeneratedRegex(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.IgnoreCase)] public static partial Regex Pap(); } + + public static partial class Shader + { + public static string ShpkPath(string name) + => $"shader/sm5/shpk/{name}"; + } } diff --git a/Penumbra.String b/Penumbra.String index 84f9ec42..3447fe0d 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 84f9ec42cc7039d0731f538e11b0c5be3f766f29 +Subproject commit 3447fe0dc9cfc5f056e1595c6c2413de37db924a diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 8b7a0a28..67d9996d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -8,11 +8,11 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData; +using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.Util; @@ -23,21 +23,22 @@ public partial class ModEditWindow { private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); - private FullPath FindAssociatedShpk( MtrlFile mtrl ) + private FullPath FindAssociatedShpk( MtrlFile mtrl, out string defaultPath, out Utf8GamePath defaultGamePath ) { - if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) + defaultPath = GamePaths.Shader.ShpkPath( mtrl.ShaderPackage.Name ); + if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) { return FullPath.Empty; } - return FindBestMatch( shpkPath ); + return FindBestMatch( defaultGamePath ); } private void LoadAssociatedShpk( MtrlFile mtrl ) { try { - _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl ); + _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl, out _, out _ ); var data = _mtrlTabState.LoadedShpkPath.IsRooted ? File.ReadAllBytes( _mtrlTabState.LoadedShpkPath.FullName ) : Dalamud.GameData.GetFile( _mtrlTabState.LoadedShpkPath.InternalName.ToString() )?.Data; @@ -109,23 +110,28 @@ public partial class ModEditWindow return ret; } - private void DrawCustomAssociations( MtrlFile file, bool disabled ) + /// + /// Show the currently associated shpk file, if any, and the buttons to associate + /// a specific shpk from your drive, the modded shpk by path or the default shpk. + /// + private void DrawCustomAssociations( MtrlFile file ) { var text = file.AssociatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath}"; + : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath.ToPath()}"; ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ImGui.Selectable( text ); - if( disabled ) + if( ImGui.Selectable( text ) ) { - return; + ImGui.SetClipboardText( _mtrlTabState.LoadedShpkPath.IsRooted ? _mtrlTabState.LoadedShpkPath.FullName.Replace( "\\", "/" ) ); } - if( ImGui.Button( "Associate custom ShPk file" ) ) + ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); + + if( ImGui.Button( "Associate Custom .shpk File" ) ) { - _materialFileDialog.OpenFileDialog( "Associate custom .shpk file...", ".shpk", ( success, name ) => + _materialFileDialog.OpenFileDialog( "Associate Custom .shpk File...", ".shpk", ( success, name ) => { if( !success ) { @@ -139,28 +145,36 @@ public partial class ModEditWindow } catch( Exception e ) { - Penumbra.Log.Error( $"Could not load .shpk file {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Could not load {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); } - - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); }); } - var defaultFile = FindAssociatedShpk( file ); + var moddedPath = FindAssociatedShpk( file, out var defaultPath, out var gamePath ); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate default ShPk file", Vector2.Zero, defaultFile.FullName, defaultFile.Equals( _mtrlTabState.LoadedShpkPath ) ) ) + if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( _mtrlTabState.LoadedShpkPath ) ) ) { LoadAssociatedShpk( file ); - if( file.AssociatedShpk != null ) + if( file.AssociatedShpk == null ) { - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", - "Penumbra Advanced Editing", NotificationType.Success ); + ChatUtil.NotificationMessage( $"Could not load {moddedPath}.", "Penumbra Advanced Editing", NotificationType.Error ); } - else + } + + if( !gamePath.Path.Equals( moddedPath.InternalName ) ) + { + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( _mtrlTabState.LoadedShpkPath.InternalName ) ) ) { - ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); + try + { + file.AssociatedShpk = new ShpkFile( Dalamud.GameData.GetFile( defaultPath )?.Data ?? Array.Empty< byte >() ); + _mtrlTabState.LoadedShpkPath = new FullPath( defaultPath ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not load {defaultPath}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + } } } @@ -611,7 +625,7 @@ public partial class ModEditWindow ret |= DrawPackageNameInput( file, disabled ); ret |= DrawShaderFlagsInput( file, disabled ); - DrawCustomAssociations( file, disabled ); + DrawCustomAssociations( file ); ret |= DrawMaterialShaderKeys( file, disabled ); DrawMaterialShaders( file ); ret |= DrawMaterialConstants( file, disabled ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 062fffd6..3fac894b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -68,19 +68,12 @@ public partial class ModEditWindow { var ret = false; using var table = ImRaii.Table( "##Textures", 2 ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); for( var i = 0; i < file.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); var tmp = file.Textures[ i ].Path; - ImGui.TableNextColumn(); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( _mtrlTabState.TextureLabels[ i ] ); - } - ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, @@ -91,6 +84,11 @@ public partial class ModEditWindow ret = true; file.Textures[ i ].Path = tmp; } + + ImGui.TableNextColumn(); + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( _mtrlTabState.TextureLabels[i] ); } return ret; From 397362caa54b3e7537f1b362b9dc8b43159e08aa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Feb 2023 18:45:04 +0100 Subject: [PATCH 0772/2451] More restructuring. --- OtterGui | 2 +- Penumbra.GameData/Files/MtrlFile.cs | 6 +- .../ModEditWindow.Materials.ColorSet.cs | 436 ++++++++++++ .../ModEditWindow.Materials.MtrlTab.cs | 159 +++++ .../Classes/ModEditWindow.Materials.Shpk.cs | 673 ++++++++---------- .../UI/Classes/ModEditWindow.Materials.cs | 492 +------------ Penumbra/UI/Classes/ModEditWindow.cs | 4 +- 7 files changed, 912 insertions(+), 860 deletions(-) create mode 100644 Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs diff --git a/OtterGui b/OtterGui index ebaedd64..2feb762d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ebaedd64ed28032e4c9bc34c0c1ec3488b2f0937 +Subproject commit 2feb762d72c8717d8ece6a0bf72298776312b2c1 diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index f890f539..c1c683e7 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -23,8 +23,6 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; - public ShpkFile? AssociatedShpk; - public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) @@ -79,7 +77,7 @@ public partial class MtrlFile : IWritable } - public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture() + public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture(ShpkFile? shpk) { var samplers = new List<(Sampler?, ShpkFile.Resource?)>(); for (var i = 0; i < Textures.Length; ++i) @@ -88,7 +86,7 @@ public partial class MtrlFile : IWritable } foreach (var sampler in ShaderPackage.Samplers) { - samplers[sampler.TextureIndex] = (sampler, AssociatedShpk?.GetSamplerById(sampler.SamplerId)); + samplers[sampler.TextureIndex] = (sampler, shpk?.GetSamplerById(sampler.SamplerId)); } return samplers; diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs new file mode 100644 index 00000000..d5cd5a44 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -0,0 +1,436 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.String.Functions; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + { + if( !file.ColorSets.Any( c => c.HasRows ) ) + { + return false; + } + + ColorSetCopyAllClipboardButton( file, 0 ); + ImGui.SameLine(); + var ret = ColorSetPasteAllClipboardButton( file, 0 ); + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= DrawPreviewDye( file, disabled ); + + using var table = ImRaii.Table( "##ColorSets", 11, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); + if( !table ) + { + return false; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( string.Empty ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Row" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Diffuse" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Specular" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Emissive" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Gloss" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Tile" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Repeat" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Skew" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye Preview" ); + + for( var j = 0; j < file.ColorSets.Length; ++j ) + { + using var _ = ImRaii.PushId( j ); + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= DrawColorSetRow( file, j, i, disabled ); + ImGui.TableNextRow(); + } + } + + return ret; + } + + + private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) + { + return; + } + + try + { + var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo( array ); + data2.TryCopyTo( array.AsSpan( data1.Length ) ); + var text = Convert.ToBase64String( array ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + + private static bool DrawPreviewDye( MtrlFile file, bool disabled ) + { + var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + { + var ret = false; + for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + { + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + } + } + + return ret; + } + + ImGui.SameLine(); + var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; + Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); + return false; + } + + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) + { + return false; + } + + ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; + fixed( void* ptr = data, output = &rows ) + { + MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); + if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() + && file.ColorDyeSets.Length > colorSetIdx ) + { + ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; + fixed( void* output2 = &dyeRows ) + { + MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + } + } + } + + return true; + } + catch + { + return false; + } + } + + private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true ) ) + { + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed( byte* ptr = data ) + { + MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + } + + var text = Convert.ToBase64String( data ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + } + + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true ) ) + { + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length != MtrlFile.ColorSet.Row.Size + 2 + || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + fixed( byte* ptr = data ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( colorSetIdx < file.ColorDyeSets.Length ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + } + } + + return true; + } + catch + { + // ignored + } + } + + return false; + } + + private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + static bool FixFloat( ref float val, float current ) + { + val = ( float )( Half )val; + return val != current; + } + + using var id = ImRaii.PushId( rowIdx ); + var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = file.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var floatSize = 70 * ImGuiHelpers.GlobalScale; + var intSize = 45 * ImGuiHelpers.GlobalScale; + ImGui.TableNextColumn(); + ColorSetCopyClipboardButton( row, dye ); + ImGui.SameLine(); + var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled( disabled ); + ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ImGui.SameLine(); + var tmpFloat = row.SpecularStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + tmpFloat = row.GlossStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + int tmpInt = row.TileSet; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialRepeat.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + tmpFloat = row.MaterialRepeat.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialSkew.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.SameLine(); + tmpFloat = row.MaterialSkew.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + if( hasDye ) + { + if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); + } + else + { + ImGui.TableNextColumn(); + } + + + return ret; + } + + private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + { + var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + { + return false; + } + + var values = entry[ ( int )stain ]; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + + var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), + "Apply the selected dye to this row.", disabled, true ); + + ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + + ImGui.SameLine(); + ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ImGui.SameLine(); + ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ImGui.SameLine(); + ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ImGui.SameLine(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) + { + var ret = false; + var tmp = input; + if( ImGui.ColorEdit3( label, ref tmp, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + && tmp != input ) + { + setter( tmp ); + ret = true; + } + + if( letter.Length > 0 && ImGui.IsItemVisible() ) + { + var textSize = ImGui.CalcTextSize( letter ); + var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + } + + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs new file mode 100644 index 00000000..2280d316 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private sealed class MtrlTab : IWritable + { + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + + public uint MaterialNewKeyId = 0; + public uint MaterialNewKeyDefault = 0; + public uint MaterialNewConstantId = 0; + public uint MaterialNewSamplerId = 0; + + + public ShpkFile? AssociatedShpk; + public readonly List< string > TextureLabels = new(4); + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public float TextureLabelWidth = 0f; + + // Shader Key State + public readonly List< string > ShaderKeyLabels = new(16); + public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); + public readonly List< int > MissingShaderKeyIndices = new(16); + public readonly List< uint > AvailableKeyValues = new(16); + public string VertexShaders = "Vertex Shaders: ???"; + public string PixelShaders = "Pixel Shaders: ???"; + + public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) + { + defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); + if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) + { + return FullPath.Empty; + } + + return _edit.FindBestMatch( defaultGamePath ); + } + + public void LoadShpk( FullPath path ) + { + try + { + LoadedShpkPath = path; + var data = LoadedShpkPath.IsRooted + ? File.ReadAllBytes( LoadedShpkPath.FullName ) + : Dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); + LoadedShpkPathName = path.ToPath(); + } + catch( Exception e ) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + ChatUtil.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + } + + Update(); + } + + public void UpdateTextureLabels() + { + var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); + TextureLabels.Clear(); + TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + for( var i = 0; i < Mtrl.Textures.Length; ++i ) + { + var (sampler, shpkSampler) = samplers[ i ]; + var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; + TextureLabels.Add( name ); + TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); + } + } + + TextureLabelWidth = TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + } + + public void UpdateShaderKeyLabels() + { + ShaderKeyLabels.Clear(); + DefinedShaderKeys.Clear(); + foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + { + ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); + DefinedShaderKeys.Add( key.Category, key.Value ); + } + + MissingShaderKeyIndices.Clear(); + AvailableKeyValues.Clear(); + var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); + var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); + if( AssociatedShpk != null ) + { + MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); + + if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != MaterialNewKeyId ) ) + { + var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; + MaterialNewKeyId = key.Id; + MaterialNewKeyDefault = key.DefaultValue; + } + + AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); + foreach( var node in AssociatedShpk.Nodes ) + { + if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + { + foreach( var pass in node.Passes ) + { + vertexShaders.Add( ( int )pass.VertexShader ); + pixelShaders.Add( ( int )pass.PixelShader ); + } + } + } + } + + VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; + PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; + } + + public void Update() + { + UpdateTextureLabels(); + UpdateShaderKeyLabels(); + } + + public MtrlTab( ModEditWindow edit, MtrlFile file ) + { + _edit = edit; + Mtrl = file; + LoadShpk( FindAssociatedShpk( out _, out _ ) ); + } + + public bool Valid + => Mtrl.Valid; + + public byte[] Write() + => Mtrl.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 67d9996d..8f3dfb1e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -5,17 +5,14 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData; -using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -23,73 +20,20 @@ public partial class ModEditWindow { private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); - private FullPath FindAssociatedShpk( MtrlFile mtrl, out string defaultPath, out Utf8GamePath defaultGamePath ) - { - defaultPath = GamePaths.Shader.ShpkPath( mtrl.ShaderPackage.Name ); - if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) - { - return FullPath.Empty; - } - - return FindBestMatch( defaultGamePath ); - } - - private void LoadAssociatedShpk( MtrlFile mtrl ) - { - try - { - _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl, out _, out _ ); - var data = _mtrlTabState.LoadedShpkPath.IsRooted - ? File.ReadAllBytes( _mtrlTabState.LoadedShpkPath.FullName ) - : Dalamud.GameData.GetFile( _mtrlTabState.LoadedShpkPath.InternalName.ToString() )?.Data; - if( data?.Length > 0 ) - { - mtrl.AssociatedShpk = new ShpkFile( data ); - } - } - catch( Exception e ) - { - Penumbra.Log.Debug( $"Could not parse associated file {_mtrlTabState.LoadedShpkPath} to Shpk:\n{e}" ); - _mtrlTabState.LoadedShpkPath = FullPath.Empty; - mtrl.AssociatedShpk = null; - } - - UpdateTextureLabels( mtrl ); - } - - private void UpdateTextureLabels( MtrlFile file ) - { - var samplers = file.GetSamplersByTexture(); - _mtrlTabState.TextureLabels.Clear(); - _mtrlTabState.TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) - { - for( var i = 0; i < file.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - _mtrlTabState.TextureLabels.Add( name ); - _mtrlTabState.TextureLabelWidth = Math.Max( _mtrlTabState.TextureLabelWidth, ImGui.CalcTextSize( name ).X ); - } - } - - _mtrlTabState.TextureLabelWidth = _mtrlTabState.TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; - } - - private bool DrawPackageNameInput( MtrlFile file, bool disabled ) + private bool DrawPackageNameInput( MtrlTab tab, bool disabled ) { var ret = false; ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + if( ImGui.InputText( "Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - ret = true; - file.AssociatedShpk = null; - _mtrlTabState.LoadedShpkPath = FullPath.Empty; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; } if( ImGui.IsItemDeactivatedAfterEdit() ) { - LoadAssociatedShpk( file ); + tab.LoadShpk( tab.FindAssociatedShpk( out _, out _ ) ); } return ret; @@ -114,20 +58,20 @@ public partial class ModEditWindow /// Show the currently associated shpk file, if any, and the buttons to associate /// a specific shpk from your drive, the modded shpk by path or the default shpk. /// - private void DrawCustomAssociations( MtrlFile file ) + private void DrawCustomAssociations( MtrlTab tab ) { - var text = file.AssociatedShpk == null + var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath.ToPath()}"; + : $"Associated .shpk file: {tab.LoadedShpkPathName}"; ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); if( ImGui.Selectable( text ) ) { - ImGui.SetClipboardText( _mtrlTabState.LoadedShpkPath.IsRooted ? _mtrlTabState.LoadedShpkPath.FullName.Replace( "\\", "/" ) ); + ImGui.SetClipboardText( tab.LoadedShpkPathName ); } - ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); + ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); if( ImGui.Button( "Associate Custom .shpk File" ) ) { @@ -138,358 +82,323 @@ public partial class ModEditWindow return; } - try - { - file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); - _mtrlTabState.LoadedShpkPath = new FullPath( name ); - } - catch( Exception e ) - { - ChatUtil.NotificationMessage( $"Could not load {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - } - }); + tab.LoadShpk( new FullPath( name ) ); + } ); } - var moddedPath = FindAssociatedShpk( file, out var defaultPath, out var gamePath ); + var moddedPath = tab.FindAssociatedShpk( out var defaultPath, out var gamePath ); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( _mtrlTabState.LoadedShpkPath ) ) ) + if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( tab.LoadedShpkPath ) ) ) { - LoadAssociatedShpk( file ); - if( file.AssociatedShpk == null ) - { - ChatUtil.NotificationMessage( $"Could not load {moddedPath}.", "Penumbra Advanced Editing", NotificationType.Error ); - } + tab.LoadShpk( moddedPath ); } if( !gamePath.Path.Equals( moddedPath.InternalName ) ) { ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( _mtrlTabState.LoadedShpkPath.InternalName ) ) ) + if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( tab.LoadedShpkPath.InternalName ) ) ) { - try - { - file.AssociatedShpk = new ShpkFile( Dalamud.GameData.GetFile( defaultPath )?.Data ?? Array.Empty< byte >() ); - _mtrlTabState.LoadedShpkPath = new FullPath( defaultPath ); - } - catch( Exception e ) - { - ChatUtil.NotificationMessage( $"Could not load {defaultPath}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); - } + tab.LoadShpk( new FullPath( gamePath ) ); } } ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); } - private bool DrawMaterialShaderKeys( MtrlFile file, bool disabled ) + + private static bool DrawShaderKey( MtrlTab tab, bool disabled, ShaderKey key, int idx ) { - var ret = false; - if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) + var ret = false; + using var t2 = ImRaii.TreeNode( tab.ShaderKeyLabels[ idx ], disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); + if( !t2 || disabled ) { - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( t ) + return ret; + } + + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById( key.Category ); + if( shpkKey.HasValue ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); + if( c ) { - var definedKeys = new HashSet(); - - foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + foreach( var value in shpkKey.Value.Values ) { - definedKeys.Add( key.Category ); - using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( t2 ) + if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) { - if( !disabled ) - { - var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); - if( c ) - { - foreach( var value in shpkKey.Value.Values ) - { - if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) - { - file.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - } - } - } - } - - if( ImGui.Button( "Remove Key" ) ) - { - file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.RemoveItems( idx ); - ret = true; - } - } - } - } - - if( !disabled && file.AssociatedShpk != null ) - { - var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); - if( missingKeys.Length > 0 ) - { - var selectedKey = Array.Find( missingKeys, key => key.Id == _mtrlTabState.MaterialNewKeyId ); - if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) - { - selectedKey = missingKeys[0]; - _mtrlTabState.MaterialNewKeyId = selectedKey.Id; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) - { - if( c ) - { - foreach( var key in missingKeys ) - { - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _mtrlTabState.MaterialNewKeyId ) ) - { - selectedKey = key; - _mtrlTabState.MaterialNewKeyId = key.Id; - } - } - } - } - - ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) - { - file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.AddItem( new ShaderKey - { - Category = selectedKey.Id, - Value = selectedKey.DefaultValue, - } ); - ret = true; - } + tab.Mtrl.ShaderPackage.ShaderKeys[ idx ].Value = value; + ret = true; } } } } + if( ImGui.Button( "Remove Key" ) ) + { + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx ); + ret = true; + } + return ret; } - private void DrawMaterialShaders( MtrlFile file ) + private static bool DrawNewShaderKey( MtrlTab tab ) { - if( file.AssociatedShpk != null ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.MaterialNewKeyId:X8}" ) ) { - var definedKeys = new Dictionary(); - foreach( var key in file.ShaderPackage.ShaderKeys ) + if( c ) { - definedKeys[key.Category] = key.Value; - } - - var materialKeys = Array.ConvertAll( file.AssociatedShpk.MaterialKeys, key => - { - if( definedKeys.TryGetValue( key.Id, out var value ) ) + foreach( var idx in tab.MissingShaderKeyIndices ) { - return value; + var key = tab.AssociatedShpk!.MaterialKeys[ idx ]; + + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.MaterialNewKeyId ) ) + { + tab.MaterialNewKeyDefault = key.DefaultValue; + tab.MaterialNewKeyId = key.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Key" ) ) + { + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + { + Category = tab.MaterialNewKeyId, + Value = tab.MaterialNewKeyDefault, + } ); + return true; + } + + return false; + } + + private static bool DrawMaterialShaderKeys( MtrlTab tab, bool disabled ) + { + if( tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0 ) ) + { + return false; + } + + using var t = ImRaii.TreeNode( "Shader Keys" ); + if( !t ) + { + return false; + } + + var ret = false; + foreach( var (key, idx) in tab.Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + { + ret |= DrawShaderKey( tab, disabled, key, idx ); + } + + if( !disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0 ) + { + ret |= DrawNewShaderKey( tab ); + } + + if( ret ) + { + tab.UpdateShaderKeyLabels(); + } + + return ret; + } + + + private static void DrawMaterialShaders( MtrlTab tab ) + { + if( tab.AssociatedShpk == null ) + { + return; + } + + ImRaii.TreeNode( tab.VertexShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( tab.PixelShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + private bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + { + var ret = false; + if( tab.Mtrl.ShaderPackage.Constants.Length <= 0 + && tab.Mtrl.ShaderPackage.ShaderValues.Length <= 0 + && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.Constants.Length <= 0 ) ) + { + return ret; + } + + var materialParams = tab.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); + if( t ) + { + var orphanValues = new IndexSet( tab.Mtrl.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet< uint >(); + var hasMalformedConstants = false; + + foreach( var constant in tab.Mtrl.ShaderPackage.Constants ) + { + definedConstants.Add( constant.Id ); + var values = tab.Mtrl.GetConstantValues( constant ); + if( tab.Mtrl.GetConstantValues( constant ).Length > 0 ) + { + var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + aliasedValueCount += values.Length - unique; } else { - return key.DefaultValue; - } - } ); - var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); - var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); - foreach( var node in file.AssociatedShpk.Nodes ) - { - if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) - { - foreach( var pass in node.Passes ) - { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); - } + hasMalformedConstants = true; } } - ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - private bool DrawMaterialConstants( MtrlFile file, bool disabled ) - { - var ret = false; - if( file.ShaderPackage.Constants.Length > 0 - || file.ShaderPackage.ShaderValues.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) - { - var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); - if( t ) + foreach( var (constant, idx) in tab.Mtrl.ShaderPackage.Constants.WithIndex() ) { - var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet(); - var hasMalformedConstants = false; - - foreach( var constant in file.ShaderPackage.Constants ) + var values = tab.Mtrl.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) { - definedConstants.Add( constant.Id ); - var values = file.GetConstantValues( constant ); - if( file.GetConstantValues( constant ).Length > 0 ) + var shpkParam = tab.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; + if( ( paramByteOffset & 0x3 ) == 0 ) { - var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - aliasedValueCount += values.Length - unique; - } - else - { - hasMalformedConstants = true; + paramValueOffset = paramByteOffset >> 2; } } - foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); + + using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); + if( t2 ) { - var values = file.GetConstantValues( constant ); - var paramValueOffset = -values.Length; if( values.Length > 0 ) { - var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; - if( ( paramByteOffset & 0x3 ) == 0 ) + var valueOffset = constant.ByteOffset >> 2; + + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) { - paramValueOffset = paramByteOffset >> 2; - } - } - - var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); - - using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); - if( t2 ) - { - if( values.Length > 0 ) - { - var valueOffset = constant.ByteOffset >> 2; - - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( - $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", - ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - else - { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( !disabled - && !hasMalformedConstants - && orphanValues.Count == 0 - && aliasedValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) - { - file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); - for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) - { - if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) - { - file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - } - - ret = true; - } - } - } - - if( orphanValues.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); - if( t2 ) - { - foreach( var idx in orphanValues ) - { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( + $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", + ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; } } } - } - else if( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) - { - var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant - => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); - if( missingConstants.Length > 0 ) + else { - var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _mtrlTabState.MaterialNewConstantId ); - if( selectedConstant.ByteSize == 0 ) - { - selectedConstant = missingConstants[0]; - _mtrlTabState.MaterialNewConstantId = selectedConstant.Id; - } + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); - using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + if( !disabled + && !hasMalformedConstants + && orphanValues.Count == 0 + && aliasedValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) { - if( c ) + if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) { - foreach( var constant in missingConstants ) - { - var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _mtrlTabState.MaterialNewConstantId ) ) - { - selectedConstant = constant; - _mtrlTabState.MaterialNewConstantId = constant.Id; - } - } + tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; } } - ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) + ret = true; + } + } + } + + if( orphanValues.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); + if( t2 ) + { + foreach( var idx in orphanValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", + ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); - file.ShaderPackage.Constants = file.ShaderPackage.Constants.AddItem( new MtrlFile.Constant - { - Id = _mtrlTabState.MaterialNewConstantId, - ByteOffset = ( ushort )( file.ShaderPackage.ShaderValues.Length << 2 ), - ByteSize = selectedConstant.ByteSize, - } ); ret = true; } } } } + else if( !disabled && !hasMalformedConstants && tab.AssociatedShpk != null ) + { + var missingConstants = tab.AssociatedShpk.MaterialParams.Where( constant + => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); + if( missingConstants.Length > 0 ) + { + var selectedConstant = Array.Find( missingConstants, constant => constant.Id == tab.MaterialNewConstantId ); + if( selectedConstant.ByteSize == 0 ) + { + selectedConstant = missingConstants[ 0 ]; + tab.MaterialNewConstantId = selectedConstant.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); + using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + { + if( c ) + { + foreach( var constant in missingConstants ) + { + var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == tab.MaterialNewConstantId ) ) + { + selectedConstant = constant; + tab.MaterialNewConstantId = constant.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + { + Id = tab.MaterialNewConstantId, + ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), + ByteSize = selectedConstant.ByteSize, + } ); + ret = true; + } + } + } } return ret; } - private bool DrawMaterialSamplers( MtrlFile file, bool disabled ) + private bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) { var ret = false; - if( file.ShaderPackage.Samplers.Length > 0 - || file.Textures.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + if( tab.Mtrl.ShaderPackage.Samplers.Length > 0 + || tab.Mtrl.Textures.Length > 0 + || !disabled && tab.AssociatedShpk != null && tab.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) { using var t = ImRaii.TreeNode( "Samplers" ); if( t ) { - var orphanTextures = new IndexSet( file.Textures.Length, true ); + var orphanTextures = new IndexSet( tab.Mtrl.Textures.Length, true ); var aliasedTextureCount = 0; - var definedSamplers = new HashSet(); + var definedSamplers = new HashSet< uint >(); - foreach( var sampler in file.ShaderPackage.Samplers ) + foreach( var sampler in tab.Mtrl.ShaderPackage.Samplers ) { if( !orphanTextures.Remove( sampler.TextureIndex ) ) { @@ -499,13 +408,13 @@ public partial class ModEditWindow definedSamplers.Add( sampler.SamplerId ); } - foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + foreach( var (sampler, idx) in tab.Mtrl.ShaderPackage.Samplers.WithIndex() ) { - var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + var shpkSampler = tab.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); if( t2 ) { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ) + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( tab.Mtrl.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) .Dispose(); // FIXME this probably doesn't belong here @@ -518,7 +427,8 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; } @@ -528,22 +438,22 @@ public partial class ModEditWindow if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) { - file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; - ret = true; + tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; + ret = true; } if( !disabled && orphanTextures.Count == 0 - && aliasedTextureCount == 0 + && aliasedTextureCount == 0 && ImGui.Button( "Remove Sampler" ) ) { - file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); - file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.RemoveItems( idx ); - for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) { - if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) + if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) { - --file.ShaderPackage.Samplers[i].TextureIndex; + --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; } } @@ -559,20 +469,21 @@ public partial class ModEditWindow { foreach( var idx in orphanTextures ) { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); } } } - else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) + else if( !disabled && tab.AssociatedShpk != null && aliasedTextureCount == 0 && tab.Mtrl.Textures.Length < 255 ) { - var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); + var missingSamplers = tab.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); if( missingSamplers.Length > 0 ) { - var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _mtrlTabState.MaterialNewSamplerId ); + var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == tab.MaterialNewSamplerId ); if( selectedSampler.Name == null ) { - selectedSampler = missingSamplers[0]; - _mtrlTabState.MaterialNewSamplerId = selectedSampler.Id; + selectedSampler = missingSamplers[ 0 ]; + tab.MaterialNewSamplerId = selectedSampler.Id; } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); @@ -582,10 +493,10 @@ public partial class ModEditWindow { foreach( var sampler in missingSamplers ) { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _mtrlTabState.MaterialNewSamplerId ) ) + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.MaterialNewSamplerId ) ) { - selectedSampler = sampler; - _mtrlTabState.MaterialNewSamplerId = sampler.Id; + selectedSampler = sampler; + tab.MaterialNewSamplerId = sampler.Id; } } } @@ -594,16 +505,16 @@ public partial class ModEditWindow ImGui.SameLine(); if( ImGui.Button( "Add Sampler" ) ) { - file.Textures = file.Textures.AddItem( new MtrlFile.Texture + tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture { - Path = string.Empty, + Path = string.Empty, Flags = 0, } ); - file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.AddItem( new Sampler + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler { - SamplerId = _mtrlTabState.MaterialNewSamplerId, - TextureIndex = ( byte )file.Textures.Length, - Flags = 0, + SamplerId = tab.MaterialNewSamplerId, + TextureIndex = ( byte )tab.Mtrl.Textures.Length, + Flags = 0, } ); ret = true; } @@ -615,7 +526,7 @@ public partial class ModEditWindow return ret; } - private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + private bool DrawMaterialShaderResources( MtrlTab tab, bool disabled ) { var ret = false; if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) @@ -623,13 +534,13 @@ public partial class ModEditWindow return ret; } - ret |= DrawPackageNameInput( file, disabled ); - ret |= DrawShaderFlagsInput( file, disabled ); - DrawCustomAssociations( file ); - ret |= DrawMaterialShaderKeys( file, disabled ); - DrawMaterialShaders( file ); - ret |= DrawMaterialConstants( file, disabled ); - ret |= DrawMaterialSamplers( file, disabled ); + ret |= DrawPackageNameInput( tab, disabled ); + ret |= DrawShaderFlagsInput( tab.Mtrl, disabled ); + DrawCustomAssociations( tab ); + ret |= DrawMaterialShaderKeys( tab, disabled ); + DrawMaterialShaders( tab ); + ret |= DrawMaterialConstants( tab, disabled ); + ret |= DrawMaterialSamplers( tab, disabled ); return ret; } @@ -638,7 +549,7 @@ public partial class ModEditWindow { if( valueLength == 0 || valueOffset < 0 ) { - return (null, false); + return ( null, false ); } var firstVector = valueOffset >> 2; @@ -651,18 +562,18 @@ public partial class ModEditWindow if( firstVector == lastVector ) { - return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); } var parts = new string[lastVector + 1 - firstVector]; - parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; for( var i = firstVector + 1; i < lastVector; ++i ) { - parts[i - firstVector] = $"[{i}]"; + parts[ i - firstVector ] = $"[{i}]"; } - return (string.Join( ", ", parts ), false); + return ( string.Join( ", ", parts ), false ); } private static string? MaterialParamName( bool componentOnly, int offset ) @@ -672,7 +583,7 @@ public partial class ModEditWindow return null; } - var component = "xyzw"[offset & 0x3]; + var component = "xyzw"[ offset & 0x3 ]; return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 3fac894b..2cf5cd26 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,152 +1,64 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.String.Functions; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor< MtrlFile > _materialTab; + private readonly FileEditor< MtrlTab > _materialTab; - private struct MtrlTabState + private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) { - public uint MaterialNewKeyId = 0; - public uint MaterialNewConstantId = 0; - public uint MaterialNewSamplerId = 0; - - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public float TextureLabelWidth = 0f; - - public MtrlTabState() - { } - } - - private MtrlTabState _mtrlTabState = new(); - - - /// Load the material with an associated shader package if it can be found. See . - private MtrlFile LoadMtrl( byte[] bytes ) - { - var mtrl = new MtrlFile( bytes ); - LoadAssociatedShpk( mtrl ); - return mtrl; - } - - - private bool DrawMaterialPanel( MtrlFile file, bool disabled ) - { - var ret = DrawMaterialTextureChange( file, disabled ); + var ret = DrawMaterialTextureChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( file, disabled ); + ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialColorSetChange( file, disabled ); + ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( file, disabled ); + ret |= DrawMaterialShaderResources( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherMaterialDetails( file, disabled ); + DrawOtherMaterialDetails( tab.Mtrl, disabled ); _materialFileDialog.Draw(); return !disabled && ret; } - private bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { var ret = false; using var table = ImRaii.Table( "##Textures", 2 ); ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); - for( var i = 0; i < file.Textures.Length; ++i ) + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); - var tmp = file.Textures[ i ].Path; + var tmp = tab.Mtrl.Textures[ i ].Path; ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 - && tmp != file.Textures[ i ].Path ) + && tmp != tab.Mtrl.Textures[ i ].Path ) { ret = true; - file.Textures[ i ].Path = tmp; + tab.Mtrl.Textures[ i ].Path = tmp; } ImGui.TableNextColumn(); using var font = ImRaii.PushFont( UiBuilder.MonoFont ); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( _mtrlTabState.TextureLabels[i] ); - } - - return ret; - } - - private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) - { - if( !file.ColorSets.Any( c => c.HasRows ) ) - { - return false; - } - - ColorSetCopyAllClipboardButton( file, 0 ); - ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( file, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( file, disabled ); - - using var table = ImRaii.Table( "##ColorSets", 11, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); - if( !table ) - { - return false; - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Row" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Diffuse" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Specular" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Emissive" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Gloss" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Tile" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Repeat" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); - - for( var j = 0; j < file.ColorSets.Length; ++j ) - { - using var _ = ImRaii.PushId( j ); - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= DrawColorSetRow( file, j, i, disabled ); - ImGui.TableNextRow(); - } + ImGui.TextUnformatted( tab.TextureLabels[ i ] ); } return ret; @@ -179,13 +91,11 @@ public partial class ModEditWindow return ret; } - private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled ) + private static void DrawOtherMaterialDetails( MtrlFile file, bool _ ) { - var ret = false; - if( !ImGui.CollapsingHeader( "Further Content" ) ) { - return false; + return; } using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) @@ -199,378 +109,16 @@ public partial class ModEditWindow } } - if( file.AdditionalData.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } - } - - return ret; - } - - private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) - { - if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) + if( file.AdditionalData.Length <= 0 ) { return; } - try + using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); + if( t ) { - var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo( array ); - data2.TryCopyTo( array.AsSpan( data1.Length ) ); - var text = Convert.ToBase64String( array ); - ImGui.SetClipboardText( text ); + ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); } - catch - { - // ignored - } - } - - private static bool DrawPreviewDye( MtrlFile file, bool disabled ) - { - var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; - var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) - { - var ret = false; - for( var j = 0; j < file.ColorDyeSets.Length; ++j ) - { - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); - } - } - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); - return false; - } - - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) - { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) - { - return false; - } - - ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; - fixed( void* ptr = data, output = &rows ) - { - MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); - if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx ) - { - ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; - fixed( void* output2 = &dyeRows ) - { - MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); - } - } - } - - return true; - } - catch - { - return false; - } - } - - private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true ) ) - { - try - { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; - fixed( byte* ptr = data ) - { - MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); - } - - var text = Convert.ToBase64String( data ); - ImGui.SetClipboardText( text ); - } - catch - { - // ignored - } - } - } - - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true ) ) - { - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || file.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - fixed( byte* ptr = data ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < file.ColorDyeSets.Length ) - { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); - } - } - - return true; - } - catch - { - // ignored - } - } - - return false; - } - - private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) - { - static bool FixFloat( ref float val, float current ) - { - val = ( float )( Half )val; - return val != current; - } - - using var id = ImRaii.PushId( rowIdx ); - var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = file.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); - var floatSize = 70 * ImGuiHelpers.GlobalScale; - var intSize = 45 * ImGuiHelpers.GlobalScale; - ImGui.TableNextColumn(); - ColorSetCopyClipboardButton( row, dye ); - ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - if( hasDye ) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - if( hasDye ) - { - if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) - { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; - ret = true; - } - - ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); - } - else - { - ImGui.TableNextColumn(); - } - - - return ret; - } - - private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) - { - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { - return false; - } - - var values = entry[ ( int )stain ]; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); - - var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Apply the selected dye to this row.", disabled, true ); - - ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); - - ImGui.SameLine(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); - ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); - ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); - - return ret; - } - - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) - { - var ret = false; - var tmp = input; - if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) - && tmp != input ) - { - setter( tmp ); - ret = true; - } - - if( letter.Length > 0 && ImGui.IsItemVisible() ) - { - var textSize = ImGui.CalcTextSize( letter ); - var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText( center, textColor, letter ); - } - - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); - - return ret; } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index a3de3c34..9b0aca95 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -570,11 +570,11 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) { - _materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl", + _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - LoadMtrl ); + bytes => new MtrlTab( this, new MtrlFile( bytes ) ) ); _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel, From d175802beceffdc848109a98b029efce9ff9bb79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Feb 2023 15:10:38 +0100 Subject: [PATCH 0773/2451] Rework Material Constants --- .../ModEditWindow.Materials.MtrlTab.cs | 87 +++- .../Classes/ModEditWindow.Materials.Shpk.cs | 375 +++++++++--------- Penumbra/UI/ConfigWindow.ResourceTab.cs | 2 - 3 files changed, 279 insertions(+), 185 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 2280d316..5f0c50f7 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -12,6 +12,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.Util; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; @@ -22,10 +23,11 @@ public partial class ModEditWindow private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; - public uint MaterialNewKeyId = 0; - public uint MaterialNewKeyDefault = 0; - public uint MaterialNewConstantId = 0; - public uint MaterialNewSamplerId = 0; + public uint MaterialNewKeyId = 0; + public uint MaterialNewKeyDefault = 0; + public uint MaterialNewConstantId = 0; + public int MaterialNewConstantIdx = 0; + public uint MaterialNewSamplerId = 0; public ShpkFile? AssociatedShpk; @@ -42,6 +44,16 @@ public partial class ModEditWindow public string VertexShaders = "Vertex Shaders: ???"; public string PixelShaders = "Pixel Shaders: ???"; + // Material Constants + public List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); + public List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); + + public string MaterialConstantLabel = "Constants###Constants"; + public bool HasMalformedMaterialConstants = false; + public IndexSet OrphanedMaterialValues = new(0, false); + public HashSet< uint > DefinedMaterialConstants = new(16); + public int AliasedMaterialValueCount = 0; + public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); @@ -137,10 +149,77 @@ public partial class ModEditWindow PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; } + public void UpdateConstantLabels() + { + var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; + MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; + + DefinedMaterialConstants.Clear(); + MaterialConstants.Clear(); + HasMalformedMaterialConstants = false; + AliasedMaterialValueCount = 0; + OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true ); + foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() ) + { + DefinedMaterialConstants.Add( constant.Id ); + var values = Mtrl.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) + { + var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam?.ByteOffset ?? -1; + if( ( paramByteOffset & 0x3 ) == 0 ) + { + paramValueOffset = paramByteOffset >> 2; + } + + var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + AliasedMaterialValueCount += values.Length - unique; + } + else + { + HasMalformedMaterialConstants = true; + } + + var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); + var label = name == null + ? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}" + : $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}"; + + MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) ); + } + + MissingMaterialConstants.Clear(); + if( AssociatedShpk != null ) + { + var setIdx = false; + foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains(m.Id)) ) + { + var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); + var label = name == null + ? $"(ID: 0x{param.Id:X8})" + : $"{name} (ID: 0x{param.Id:X8})"; + if( MaterialNewConstantId == param.Id ) + { + setIdx = true; + MaterialNewConstantIdx = MissingMaterialConstants.Count; + } + MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); + } + + if (!setIdx && MissingMaterialConstants.Count > 0) + { + MaterialNewConstantIdx = 0; + MaterialNewConstantId = MissingMaterialConstants[ 0 ].Id; + } + } + } + public void Update() { UpdateTextureLabels(); UpdateShaderKeyLabels(); + UpdateConstantLabels(); } public MtrlTab( ModEditWindow edit, MtrlFile file ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 8f3dfb1e..12780a09 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Text; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; @@ -106,7 +107,7 @@ public partial class ModEditWindow } - private static bool DrawShaderKey( MtrlTab tab, bool disabled, ShaderKey key, int idx ) + private static bool DrawShaderKey( MtrlTab tab, bool disabled, ref int idx ) { var ret = false; using var t2 = ImRaii.TreeNode( tab.ShaderKeyLabels[ idx ], disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); @@ -115,6 +116,7 @@ public partial class ModEditWindow return ret; } + var key = tab.Mtrl.ShaderPackage.ShaderKeys[ idx ]; var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById( key.Category ); if( shpkKey.HasValue ) { @@ -128,6 +130,7 @@ public partial class ModEditWindow { tab.Mtrl.ShaderPackage.ShaderKeys[ idx ].Value = value; ret = true; + tab.UpdateShaderKeyLabels(); } } } @@ -135,8 +138,9 @@ public partial class ModEditWindow if( ImGui.Button( "Remove Key" ) ) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx ); + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx-- ); ret = true; + tab.UpdateShaderKeyLabels(); } return ret; @@ -145,6 +149,7 @@ public partial class ModEditWindow private static bool DrawNewShaderKey( MtrlTab tab ) { ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + var ret = false; using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.MaterialNewKeyId:X8}" ) ) { if( c ) @@ -157,6 +162,8 @@ public partial class ModEditWindow { tab.MaterialNewKeyDefault = key.DefaultValue; tab.MaterialNewKeyId = key.Id; + ret = true; + tab.UpdateShaderKeyLabels(); } } } @@ -170,10 +177,11 @@ public partial class ModEditWindow Category = tab.MaterialNewKeyId, Value = tab.MaterialNewKeyDefault, } ); - return true; + ret = true; + tab.UpdateShaderKeyLabels(); } - return false; + return ret; } private static bool DrawMaterialShaderKeys( MtrlTab tab, bool disabled ) @@ -190,9 +198,9 @@ public partial class ModEditWindow } var ret = false; - foreach( var (key, idx) in tab.Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + for( var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx ) { - ret |= DrawShaderKey( tab, disabled, key, idx ); + ret |= DrawShaderKey( tab, disabled, ref idx ); } if( !disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0 ) @@ -200,15 +208,9 @@ public partial class ModEditWindow ret |= DrawNewShaderKey( tab ); } - if( ret ) - { - tab.UpdateShaderKeyLabels(); - } - return ret; } - private static void DrawMaterialShaders( MtrlTab tab ) { if( tab.AssociatedShpk == null ) @@ -220,170 +222,164 @@ public partial class ModEditWindow ImRaii.TreeNode( tab.PixelShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); } - private bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + + private static bool DrawMaterialConstantValues( MtrlTab tab, bool disabled, ref int idx ) { - var ret = false; - if( tab.Mtrl.ShaderPackage.Constants.Length <= 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length <= 0 - && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.Constants.Length <= 0 ) ) + var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[ idx ]; + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t2 = ImRaii.TreeNode( name ); + if( !t2 ) { - return ret; + return false; } - var materialParams = tab.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + font.Dispose(); - using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); - if( t ) + var constant = tab.Mtrl.ShaderPackage.Constants[ idx ]; + var ret = false; + var values = tab.Mtrl.GetConstantValues( constant ); + if( values.Length > 0 ) { - var orphanValues = new IndexSet( tab.Mtrl.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet< uint >(); - var hasMalformedConstants = false; + var valueOffset = constant.ByteOffset >> 2; - foreach( var constant in tab.Mtrl.ShaderPackage.Constants ) + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) { - definedConstants.Add( constant.Id ); - var values = tab.Mtrl.GetConstantValues( constant ); - if( tab.Mtrl.GetConstantValues( constant ).Length > 0 ) + var paramName = MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( $"{paramName} (at 0x{( valueOffset + valueIdx ) << 2:X4})", ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - aliasedValueCount += values.Length - unique; + ret = true; + tab.UpdateConstantLabels(); } - else + } + } + else + { + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( !disabled + && !tab.HasMalformedMaterialConstants + && tab.OrphanedMaterialValues.Count == 0 + && tab.AliasedMaterialValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx-- ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) + { + if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) { - hasMalformedConstants = true; + tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; } } - foreach( var (constant, idx) in tab.Mtrl.ShaderPackage.Constants.WithIndex() ) + ret = true; + tab.UpdateConstantLabels(); + } + + return ret; + } + + private static bool DrawMaterialOrphans( MtrlTab tab, bool disabled ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({tab.OrphanedMaterialValues.Count})" ); + if( !t2 ) + { + return false; + } + + var ret = false; + foreach( var idx in tab.OrphanedMaterialValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", + ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { - var values = tab.Mtrl.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) - { - var shpkParam = tab.AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - } - - var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); - - using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); - if( t2 ) - { - if( values.Length > 0 ) - { - var valueOffset = constant.ByteOffset >> 2; - - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( - $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", - ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - else - { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( !disabled - && !hasMalformedConstants - && orphanValues.Count == 0 - && aliasedValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) - { - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) - { - if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) - { - tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; - } - } - - ret = true; - } - } - } - - if( orphanValues.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); - if( t2 ) - { - foreach( var idx in orphanValues ) - { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - } - else if( !disabled && !hasMalformedConstants && tab.AssociatedShpk != null ) - { - var missingConstants = tab.AssociatedShpk.MaterialParams.Where( constant - => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); - if( missingConstants.Length > 0 ) - { - var selectedConstant = Array.Find( missingConstants, constant => constant.Id == tab.MaterialNewConstantId ); - if( selectedConstant.ByteSize == 0 ) - { - selectedConstant = missingConstants[ 0 ]; - tab.MaterialNewConstantId = selectedConstant.Id; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); - using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) - { - if( c ) - { - foreach( var constant in missingConstants ) - { - var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == tab.MaterialNewConstantId ) ) - { - selectedConstant = constant; - tab.MaterialNewConstantId = constant.Id; - } - } - } - } - - ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) - { - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant - { - Id = tab.MaterialNewConstantId, - ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), - ByteSize = selectedConstant.ByteSize, - } ); - ret = true; - } - } + ret = true; + tab.UpdateConstantLabels(); } } return ret; } + private static bool DrawNewMaterialParam( MtrlTab tab ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + using var c = ImRaii.Combo( "##NewConstantId", tab.MissingMaterialConstants[ tab.MaterialNewConstantIdx ].Name ); + if( c ) + { + foreach( var (constant, idx) in tab.MissingMaterialConstants.WithIndex() ) + { + if( ImGui.Selectable( constant.Name, constant.Id == tab.MaterialNewConstantId ) ) + { + tab.MaterialNewConstantIdx = idx; + tab.MaterialNewConstantId = constant.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + var (_, _, byteSize) = tab.MissingMaterialConstants[ tab.MaterialNewConstantIdx ]; + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + { + Id = tab.MaterialNewConstantId, + ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), + ByteSize = byteSize, + } ); + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, byteSize >> 2 ); + tab.UpdateConstantLabels(); + return true; + } + + return false; + } + + private static bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + { + if( tab.Mtrl.ShaderPackage.Constants.Length == 0 + && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 + && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0 ) ) + { + return false; + } + + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t = ImRaii.TreeNode( tab.MaterialConstantLabel ); + if( !t ) + { + return false; + } + + font.Dispose(); + var ret = false; + for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx ) + { + ret |= DrawMaterialConstantValues( tab, disabled, ref idx ); + } + + if( tab.OrphanedMaterialValues.Count > 0 ) + { + ret |= DrawMaterialOrphans( tab, disabled ); + } + else if( !disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0 ) + { + ret |= DrawNewMaterialParam( tab ); + } + + return ret; + } + + private bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) { var ret = false; @@ -544,9 +540,46 @@ public partial class ModEditWindow return ret; } - - private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + private static string? MaterialParamName( bool componentOnly, int offset ) { + if( offset < 0 ) + { + return null; + } + + return ( componentOnly, offset & 0x3 ) switch + { + (true, 0) => "x", + (true, 1) => "y", + (true, 2) => "z", + (true, 3) => "w", + (false, 0) => $"[{offset >> 2:D2}].x", + (false, 1) => $"[{offset >> 2:D2}].y", + (false, 2) => $"[{offset >> 2:D2}].z", + (false, 3) => $"[{offset >> 2:D2}].w", + _ => null, + }; + } + + private static (string? Name, bool ComponentOnly) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + { + static string VectorSwizzle( int firstComponent, int lastComponent ) + => ( firstComponent, lastComponent ) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz ", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw ", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; + if( valueLength == 0 || valueOffset < 0 ) { return ( null, false ); @@ -556,35 +589,19 @@ public partial class ModEditWindow var lastVector = ( valueOffset + valueLength - 1 ) >> 2; var firstComponent = valueOffset & 0x3; var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; - - static string VectorSwizzle( int firstComponent, int numComponents ) - => numComponents == 4 ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); - if( firstVector == lastVector ) { - return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); + return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent )}", true ); } - var parts = new string[lastVector + 1 - firstVector]; - parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + var sb = new StringBuilder( 128 ); + sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 )}" ); for( var i = firstVector + 1; i < lastVector; ++i ) { - parts[ i - firstVector ] = $"[{i}]"; + sb.Append( $", [{i}]" ); } - return ( string.Join( ", ", parts ), false ); - } - - private static string? MaterialParamName( bool componentOnly, int offset ) - { - if( offset < 0 ) - { - return null; - } - - var component = "xyzw"[ offset & 0x3 ]; - - return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; + sb.Append( $", [{lastVector}]{VectorSwizzle( 0, lastComponent )}" ); + return ( sb.ToString(), false ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 51c64252..56eb19e8 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -10,7 +9,6 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData; using Penumbra.Interop.Loader; using Penumbra.String.Classes; From 2e6cc736669a043ebe8868e1dd1901ca2ef97660 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Feb 2023 18:11:21 +0100 Subject: [PATCH 0774/2451] Rework Samplers. --- .../ModEditWindow.Materials.MtrlTab.cs | 100 +++++-- .../Classes/ModEditWindow.Materials.Shpk.cs | 277 +++++++++--------- 2 files changed, 214 insertions(+), 163 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 5f0c50f7..8c8cc508 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -23,18 +23,19 @@ public partial class ModEditWindow private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; - public uint MaterialNewKeyId = 0; - public uint MaterialNewKeyDefault = 0; - public uint MaterialNewConstantId = 0; - public int MaterialNewConstantIdx = 0; - public uint MaterialNewSamplerId = 0; + public uint NewKeyId; + public uint NewKeyDefault; + public uint NewConstantId; + public int NewConstantIdx; + public uint NewSamplerId; + public int NewSamplerIdx; public ShpkFile? AssociatedShpk; public readonly List< string > TextureLabels = new(4); public FullPath LoadedShpkPath = FullPath.Empty; public string LoadedShpkPathName = string.Empty; - public float TextureLabelWidth = 0f; + public float TextureLabelWidth; // Shader Key State public readonly List< string > ShaderKeyLabels = new(16); @@ -45,14 +46,21 @@ public partial class ModEditWindow public string PixelShaders = "Pixel Shaders: ???"; // Material Constants - public List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); - public List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); + public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); + public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); + public readonly HashSet< uint > DefinedMaterialConstants = new(16); - public string MaterialConstantLabel = "Constants###Constants"; - public bool HasMalformedMaterialConstants = false; - public IndexSet OrphanedMaterialValues = new(0, false); - public HashSet< uint > DefinedMaterialConstants = new(16); - public int AliasedMaterialValueCount = 0; + public string MaterialConstantLabel = "Constants###Constants"; + public IndexSet OrphanedMaterialValues = new(0, false); + public int AliasedMaterialValueCount; + public bool HasMalformedMaterialConstants; + + // Samplers + public readonly List< (string Label, string FileName) > Samplers = new(4); + public readonly List< (string Name, uint Id) > MissingSamplers = new(4); + public readonly HashSet< uint > DefinedSamplers = new(4); + public IndexSet OrphanedSamplers = new(0, false); + public int AliasedSamplerCount; public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { @@ -124,11 +132,11 @@ public partial class ModEditWindow { MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); - if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != MaterialNewKeyId ) ) + if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) ) { var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; - MaterialNewKeyId = key.Id; - MaterialNewKeyDefault = key.DefaultValue; + NewKeyId = key.Id; + NewKeyDefault = key.DefaultValue; } AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); @@ -193,24 +201,69 @@ public partial class ModEditWindow if( AssociatedShpk != null ) { var setIdx = false; - foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains(m.Id)) ) + foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) ) { var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); var label = name == null ? $"(ID: 0x{param.Id:X8})" : $"{name} (ID: 0x{param.Id:X8})"; - if( MaterialNewConstantId == param.Id ) + if( NewConstantId == param.Id ) { - setIdx = true; - MaterialNewConstantIdx = MissingMaterialConstants.Count; + setIdx = true; + NewConstantIdx = MissingMaterialConstants.Count; } + MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); } - if (!setIdx && MissingMaterialConstants.Count > 0) + if( !setIdx && MissingMaterialConstants.Count > 0 ) { - MaterialNewConstantIdx = 0; - MaterialNewConstantId = MissingMaterialConstants[ 0 ].Id; + NewConstantIdx = 0; + NewConstantId = MissingMaterialConstants[ 0 ].Id; + } + } + } + + public void UpdateSamplers() + { + Samplers.Clear(); + DefinedSamplers.Clear(); + OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true ); + foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() ) + { + DefinedSamplers.Add( sampler.SamplerId ); + if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) + { + ++AliasedSamplerCount; + } + + var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + var label = shpk.HasValue + ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" + : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; + var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; + Samplers.Add( ( label, fileName ) ); + } + + MissingSamplers.Clear(); + if( AssociatedShpk != null ) + { + var setSampler = false; + foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) + { + if( sampler.Id == NewSamplerId ) + { + setSampler = true; + NewSamplerIdx = MissingSamplers.Count; + } + + MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); + } + + if( !setSampler && MissingSamplers.Count > 0 ) + { + NewSamplerIdx = 0; + NewSamplerId = MissingSamplers[ 0 ].Id; } } } @@ -220,6 +273,7 @@ public partial class ModEditWindow UpdateTextureLabels(); UpdateShaderKeyLabels(); UpdateConstantLabels(); + UpdateSamplers(); } public MtrlTab( ModEditWindow edit, MtrlFile file ) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 12780a09..14719f88 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -9,7 +8,6 @@ using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -150,7 +148,7 @@ public partial class ModEditWindow { ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); var ret = false; - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.MaterialNewKeyId:X8}" ) ) + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}" ) ) { if( c ) { @@ -158,10 +156,10 @@ public partial class ModEditWindow { var key = tab.AssociatedShpk!.MaterialKeys[ idx ]; - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.MaterialNewKeyId ) ) + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId ) ) { - tab.MaterialNewKeyDefault = key.DefaultValue; - tab.MaterialNewKeyId = key.Id; + tab.NewKeyDefault = key.DefaultValue; + tab.NewKeyId = key.Id; ret = true; tab.UpdateShaderKeyLabels(); } @@ -174,8 +172,8 @@ public partial class ModEditWindow { tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem( new ShaderKey { - Category = tab.MaterialNewKeyId, - Value = tab.MaterialNewKeyDefault, + Category = tab.NewKeyId, + Value = tab.NewKeyDefault, } ); ret = true; tab.UpdateShaderKeyLabels(); @@ -312,15 +310,15 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) { - using var c = ImRaii.Combo( "##NewConstantId", tab.MissingMaterialConstants[ tab.MaterialNewConstantIdx ].Name ); + using var c = ImRaii.Combo( "##NewConstantId", tab.MissingMaterialConstants[ tab.NewConstantIdx ].Name ); if( c ) { foreach( var (constant, idx) in tab.MissingMaterialConstants.WithIndex() ) { - if( ImGui.Selectable( constant.Name, constant.Id == tab.MaterialNewConstantId ) ) + if( ImGui.Selectable( constant.Name, constant.Id == tab.NewConstantId ) ) { - tab.MaterialNewConstantIdx = idx; - tab.MaterialNewConstantId = constant.Id; + tab.NewConstantIdx = idx; + tab.NewConstantId = constant.Id; } } } @@ -329,10 +327,10 @@ public partial class ModEditWindow ImGui.SameLine(); if( ImGui.Button( "Add Constant" ) ) { - var (_, _, byteSize) = tab.MissingMaterialConstants[ tab.MaterialNewConstantIdx ]; + var (_, _, byteSize) = tab.MissingMaterialConstants[ tab.NewConstantIdx ]; tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant { - Id = tab.MaterialNewConstantId, + Id = tab.NewConstantId, ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), ByteSize = byteSize, } ); @@ -379,146 +377,145 @@ public partial class ModEditWindow return ret; } - - private bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) + private static bool DrawMaterialSampler( MtrlTab tab, bool disabled, ref int idx ) { - var ret = false; - if( tab.Mtrl.ShaderPackage.Samplers.Length > 0 - || tab.Mtrl.Textures.Length > 0 - || !disabled && tab.AssociatedShpk != null && tab.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + var (label, filename) = tab.Samplers[ idx ]; + using var tree = ImRaii.TreeNode( label ); + if( !tree ) + return false; + + ImRaii.TreeNode( filename, ImGuiTreeNodeFlags.Leaf ).Dispose(); + var ret = false; + var sampler = tab.Mtrl.ShaderPackage.Samplers[ idx ]; + + // FIXME this probably doesn't belong here + static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) { - using var t = ImRaii.TreeNode( "Samplers" ); - if( t ) + fixed( ushort* v2 = &v ) { - var orphanTextures = new IndexSet( tab.Mtrl.Textures.Length, true ); - var aliasedTextureCount = 0; - var definedSamplers = new HashSet< uint >(); + return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags ); + } + } - foreach( var sampler in tab.Mtrl.ShaderPackage.Samplers ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + + var samplerFlags = ( int )sampler.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Sampler Flags", ref samplerFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + tab.Mtrl.ShaderPackage.Samplers[idx].Flags = ( uint )samplerFlags; + ret = true; + } + + if( !disabled + && tab.OrphanedSamplers.Count == 0 + && tab.AliasedSamplerCount == 0 + && ImGui.Button( "Remove Sampler" ) ) + { + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx-- ); + for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) + { + if( tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) { - if( !orphanTextures.Remove( sampler.TextureIndex ) ) - { - ++aliasedTextureCount; - } - - definedSamplers.Add( sampler.SamplerId ); + --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; } + } - foreach( var (sampler, idx) in tab.Mtrl.ShaderPackage.Samplers.WithIndex() ) + ret = true; + tab.UpdateSamplers(); + tab.UpdateTextureLabels(); + } + + return ret; + } + + private static bool DrawMaterialNewSampler( MtrlTab tab ) + { + var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + using( var c = ImRaii.Combo( "##NewSamplerId", $"{name} (ID: 0x{id:X8})" ) ) + { + if( c ) + { + foreach( var (sampler, idx) in tab.MissingSamplers.WithIndex() ) { - var shpkSampler = tab.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); - if( t2 ) + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId ) ) { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( tab.Mtrl.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) - { - fixed( ushort* v2 = &v ) - { - return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, nint.Zero, nint.Zero, "%04X", flags ); - } - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - - var sampFlags = ( int )sampler.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) - { - tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; - ret = true; - } - - if( !disabled - && orphanTextures.Count == 0 - && aliasedTextureCount == 0 - && ImGui.Button( "Remove Sampler" ) ) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) - { - if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) - { - --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; - } - } - - ret = true; - } - } - } - - if( orphanTextures.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); - if( t2 ) - { - foreach( var idx in orphanTextures ) - { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - } - } - } - else if( !disabled && tab.AssociatedShpk != null && aliasedTextureCount == 0 && tab.Mtrl.Textures.Length < 255 ) - { - var missingSamplers = tab.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); - if( missingSamplers.Length > 0 ) - { - var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == tab.MaterialNewSamplerId ); - if( selectedSampler.Name == null ) - { - selectedSampler = missingSamplers[ 0 ]; - tab.MaterialNewSamplerId = selectedSampler.Id; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) - { - if( c ) - { - foreach( var sampler in missingSamplers ) - { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.MaterialNewSamplerId ) ) - { - selectedSampler = sampler; - tab.MaterialNewSamplerId = sampler.Id; - } - } - } - } - - ImGui.SameLine(); - if( ImGui.Button( "Add Sampler" ) ) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - } ); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler - { - SamplerId = tab.MaterialNewSamplerId, - TextureIndex = ( byte )tab.Mtrl.Textures.Length, - Flags = 0, - } ); - ret = true; - } + tab.NewSamplerIdx = idx; + tab.NewSamplerId = sampler.Id; } } } } + ImGui.SameLine(); + if( !ImGui.Button( "Add Sampler" ) ) + { + return false; + } + + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler + { + SamplerId = tab.NewSamplerId, + TextureIndex = ( byte )tab.Mtrl.Textures.Length, + Flags = 0, + } ); + tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture + { + Path = string.Empty, + Flags = 0, + } ); + tab.UpdateSamplers(); + tab.UpdateTextureLabels(); + return true; + + } + + private static bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) + { + if( tab.Mtrl.ShaderPackage.Samplers.Length == 0 + && tab.Mtrl.Textures.Length == 0 + && ( disabled || (tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) )) + { + return false; + } + + using var t = ImRaii.TreeNode( "Samplers" ); + if( !t ) + { + return false; + } + + var ret = false; + for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx ) + { + ret |= DrawMaterialSampler( tab, disabled, ref idx ); + } + + if( tab.OrphanedSamplers.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Textures ({tab.OrphanedSamplers.Count})" ); + if( t2 ) + { + foreach( var idx in tab.OrphanedSamplers ) + { + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); + } + } + } + else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) + { + ret |= DrawMaterialNewSampler( tab ); + } + return ret; } From 7ae6d0a348dab5d31839a2a4206e4beedb9ecd56 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 Feb 2023 15:48:45 +0100 Subject: [PATCH 0775/2451] Add collection groups for Children and Elderly. --- Penumbra.Api | 2 +- Penumbra/Collections/CollectionType.cs | 20 ++++++++++++++----- .../Resolver/PathResolver.Identification.cs | 18 +++++++++++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 97dc16ba..1f62ae97 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 97dc16ba32bf78c4d3a4d210a08010cd6d4eec3c +Subproject commit 1f62ae970e02e48f686a41a2cecdb79e0af87994 diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 6b7f7638..de9d80c4 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -13,6 +13,8 @@ public enum CollectionType : byte FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, + NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, + NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, @@ -97,15 +99,15 @@ public enum CollectionType : byte Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed - Individual, // An individual collection was changed - Inactive, // A collection was added or removed - Temporary, // A temporary collections was set or deleted via IPC + Individual, // An individual collection was changed + Inactive, // A collection was added or removed + Temporary, // A temporary collections was set or deleted via IPC } public static class CollectionTypeExtensions { public static bool IsSpecial( this CollectionType collectionType ) - => collectionType is >= CollectionType.Yourself and < CollectionType.Default; + => collectionType < CollectionType.Default; public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >() .Where( IsSpecial ) @@ -265,6 +267,8 @@ public static class CollectionTypeExtensions => collectionType switch { CollectionType.Yourself => "Your Character", + CollectionType.NonPlayerChild => "Non-Player Children", + CollectionType.NonPlayerElderly => "Non-Player Elderly", CollectionType.MalePlayerCharacter => "Male Player Characters", CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", @@ -345,7 +349,13 @@ public static class CollectionTypeExtensions => collectionType switch { CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" - + "It takes precedence before all other collections except for explicitly named character collections.", + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerChild => + "This collection applies to all non-player characters with a child body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerElderly => + "This collection applies to all non-player characters with an elderly body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", CollectionType.MalePlayerCharacter => "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", CollectionType.MaleNonPlayerCharacter => diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 9ddb9941..cbd91b4f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -133,12 +133,22 @@ public unsafe partial class PathResolver var character = ( Character* )actor; if( character->ModelCharaId >= 0 && character->ModelCharaId < ValidHumanModels.Count && ValidHumanModels[ character->ModelCharaId ] ) { - var race = ( SubRace )character->CustomizeData[ 4 ]; - var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); - var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; + var bodyType = character->CustomizeData[2]; + var collection = bodyType switch + { + 3 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerElderly ), + 4 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerChild ), + _ => null, + }; + if( collection != null ) + return collection; + + var race = ( SubRace )character->CustomizeData[ 4 ]; + var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); + var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; var type = CollectionTypeExtensions.FromParts( race, gender, isNpc ); - var collection = Penumbra.CollectionManager.ByType( type ); + collection = Penumbra.CollectionManager.ByType( type ); collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) ); return collection; } From e62b0155d472e35dddcb1e0f18dac40fffb58090 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 1 Mar 2023 18:03:59 +0100 Subject: [PATCH 0776/2451] Redesign Shpk tab. --- OtterGui | 2 +- Penumbra.GameData/Data/DisassembledShader.cs | 55 +- Penumbra.GameData/Interop/D3DCompiler.cs | 49 +- Penumbra.String | 2 +- .../Classes/ModEditWindow.Materials.Shpk.cs | 29 +- .../Classes/ModEditWindow.ShaderPackages.cs | 1032 +++++++++-------- Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs | 188 +++ Penumbra/UI/Classes/ModEditWindow.cs | 4 +- 8 files changed, 792 insertions(+), 569 deletions(-) create mode 100644 Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs diff --git a/OtterGui b/OtterGui index 2feb762d..9e98cb97 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2feb762d72c8717d8ece6a0bf72298776312b2c1 +Subproject commit 9e98cb9722bed3129134c7bc2fbe51268b2d6acd diff --git a/Penumbra.GameData/Data/DisassembledShader.cs b/Penumbra.GameData/Data/DisassembledShader.cs index bfae7a74..f611982c 100644 --- a/Penumbra.GameData/Data/DisassembledShader.cs +++ b/Penumbra.GameData/Data/DisassembledShader.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Penumbra.GameData.Interop; +using Penumbra.String; namespace Penumbra.GameData.Data; @@ -96,26 +97,26 @@ public partial class DisassembledShader [GeneratedRegex(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking)] private static partial Regex Sm5TextureUsageRegex(); - private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char) ('0' + c)).ToArray(); + private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char)('0' + c)).ToArray(); - public readonly string RawDisassembly; - public readonly uint ShaderModel; - public readonly ShaderStage Stage; - public readonly string BufferDefinitions; - public readonly ResourceBinding[] ResourceBindings; - public readonly InputOutput[] InputSignature; - public readonly InputOutput[] OutputSignature; - public readonly string[] Instructions; + public readonly ByteString RawDisassembly; + public readonly uint ShaderModel; + public readonly ShaderStage Stage; + public readonly string BufferDefinitions; + public readonly ResourceBinding[] ResourceBindings; + public readonly InputOutput[] InputSignature; + public readonly InputOutput[] OutputSignature; + public readonly IReadOnlyList Instructions; - public DisassembledShader(string rawDisassembly) + public DisassembledShader(ByteString rawDisassembly) { RawDisassembly = rawDisassembly; - var lines = rawDisassembly.Split('\n'); - Instructions = Array.FindAll(lines, ln => !ln.StartsWith("//") && ln.Length > 0); - var shaderModel = Instructions[0].Trim().Split('_'); - Stage = (ShaderStage)(byte)char.ToUpper(shaderModel[0][0]); - ShaderModel = (uint.Parse(shaderModel[1]) << 8) | uint.Parse(shaderModel[2]); - var header = PreParseHeader(lines.AsSpan()[..Array.IndexOf(lines, Instructions[0])]); + var lines = rawDisassembly.Split((byte) '\n'); + Instructions = lines.FindAll(ln => !ln.StartsWith("//"u8) && ln.Length > 0); + var shaderModel = Instructions[0].Trim().Split((byte) '_'); + Stage = (ShaderStage)(byte)char.ToUpper((char) shaderModel[0][0]); + ShaderModel = (uint.Parse(shaderModel[1].ToString()) << 8) | uint.Parse(shaderModel[2].ToString()); + var header = PreParseHeader(lines.Take(lines.IndexOf(Instructions[0])).Select(l => l.ToString()).ToArray()); switch (ShaderModel >> 8) { case 3: @@ -142,8 +143,8 @@ public partial class DisassembledShader private static void ParseSm3Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) { - bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) - ? string.Join('\n', rawParameters) + bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) + ? string.Join('\n', rawParameters) : string.Empty; if (header.TryGetValue("Registers", out var rawRegisters)) { @@ -176,7 +177,7 @@ public partial class DisassembledShader outputSignature = Array.Empty(); } - private static void ParseSm3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm3ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); var tIndices = new Dictionary(); @@ -201,10 +202,11 @@ public partial class DisassembledShader foreach (var instruction in instructions) { var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) continue; - foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instruction)) + var instructionString = instruction.ToString(); + foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instructionString)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) @@ -217,7 +219,7 @@ public partial class DisassembledShader } } - var tMatch = Sm3TextureUsageRegex().Match(instruction); + var tMatch = Sm3TextureUsageRegex().Match(instructionString); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[1].Value); @@ -307,7 +309,7 @@ public partial class DisassembledShader } } - private static void ParseSm5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm5ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); var tIndices = new Dictionary(); @@ -331,10 +333,11 @@ public partial class DisassembledShader foreach (var instruction in instructions) { var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) continue; - foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instruction)) + var instructionString = instruction.ToString(); + foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instructionString)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) @@ -352,7 +355,7 @@ public partial class DisassembledShader } } - var tMatch = Sm5TextureUsageRegex().Match(instruction); + var tMatch = Sm5TextureUsageRegex().Match(instructionString); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[2].Value); diff --git a/Penumbra.GameData/Interop/D3DCompiler.cs b/Penumbra.GameData/Interop/D3DCompiler.cs index ed6366c2..04bf1ba7 100644 --- a/Penumbra.GameData/Interop/D3DCompiler.cs +++ b/Penumbra.GameData/Interop/D3DCompiler.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Penumbra.String; namespace Penumbra.GameData.Interop; @@ -20,43 +21,41 @@ internal static class D3DCompiler [Flags] public enum DisassembleFlags : uint { - EnableColorCode = 1, - EnableDefaultValuePrints = 2, + EnableColorCode = 1, + EnableDefaultValuePrints = 2, EnableInstructionNumbering = 4, - EnableInstructionCycle = 8, - DisableDebugInfo = 16, - EnableInstructionOffset = 32, - InstructionOnly = 64, - PrintHexLiterals = 128, + EnableInstructionCycle = 8, + DisableDebugInfo = 16, + EnableInstructionOffset = 32, + InstructionOnly = 64, + PrintHexLiterals = 128, } - public static unsafe string Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") + public static unsafe ByteString Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") { - ID3DBlob? disassembly; - int hr; - fixed (byte* pSrcData = blob) + ID3DBlob? disassembly = null; + try { - hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); + fixed (byte* pSrcData = blob) + { + var hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); + Marshal.ThrowExceptionForHR(hr); + } + + return disassembly == null + ? ByteString.Empty + : new ByteString((byte*)disassembly.GetBufferPointer()).Clone(); } - Marshal.ThrowExceptionForHR(hr); - var ret = Encoding.UTF8.GetString(BlobContents(disassembly)); - GC.KeepAlive(disassembly); - return ret; - } - - private static unsafe ReadOnlySpan BlobContents(ID3DBlob? blob) - { - if (blob == null) + finally { - return ReadOnlySpan.Empty; + if (disassembly != null) + Marshal.FinalReleaseComObject(disassembly); } - - return new ReadOnlySpan(blob.GetBufferPointer(), (int)blob.GetBufferSize().ToUInt32()); } [PreserveSig] [DllImport("D3DCompiler_47.dll")] - private extern static unsafe int D3DDisassemble( + private static extern unsafe int D3DDisassemble( [In] byte* pSrcData, [In] UIntPtr srcDataSize, uint flags, diff --git a/Penumbra.String b/Penumbra.String index 3447fe0d..ce41e2b7 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 3447fe0dc9cfc5f056e1595c6c2413de37db924a +Subproject commit ce41e2b7da65edb25b5308ae41e4ca7d74d75e38 diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 14719f88..5238d9a0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -160,7 +160,7 @@ public partial class ModEditWindow { tab.NewKeyDefault = key.DefaultValue; tab.NewKeyId = key.Id; - ret = true; + ret = true; tab.UpdateShaderKeyLabels(); } } @@ -382,7 +382,9 @@ public partial class ModEditWindow var (label, filename) = tab.Samplers[ idx ]; using var tree = ImRaii.TreeNode( label ); if( !tree ) + { return false; + } ImRaii.TreeNode( filename, ImGuiTreeNodeFlags.Leaf ).Dispose(); var ret = false; @@ -398,7 +400,7 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; @@ -409,22 +411,22 @@ public partial class ModEditWindow if( ImGui.InputInt( "Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = ( uint )samplerFlags; - ret = true; + tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )samplerFlags; + ret = true; } if( !disabled && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 + && tab.AliasedSamplerCount == 0 && ImGui.Button( "Remove Sampler" ) ) { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx-- ); for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) { - if( tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) + if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) { - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; + --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; } } @@ -438,7 +440,7 @@ public partial class ModEditWindow private static bool DrawMaterialNewSampler( MtrlTab tab ) { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; + var (name, id) = tab.MissingSamplers[ tab.NewSamplerIdx ]; ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); using( var c = ImRaii.Combo( "##NewSamplerId", $"{name} (ID: 0x{id:X8})" ) ) { @@ -475,14 +477,13 @@ public partial class ModEditWindow tab.UpdateSamplers(); tab.UpdateTextureLabels(); return true; - } private static bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) { if( tab.Mtrl.ShaderPackage.Samplers.Length == 0 && tab.Mtrl.Textures.Length == 0 - && ( disabled || (tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) )) + && ( disabled || ( tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) ) ) { return false; } @@ -493,7 +494,7 @@ public partial class ModEditWindow return false; } - var ret = false; + var ret = false; for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx ) { ret |= DrawMaterialSampler( tab, disabled, ref idx ); @@ -511,7 +512,7 @@ public partial class ModEditWindow } } } - else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) + else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) { ret |= DrawMaterialNewSampler( tab ); } @@ -592,7 +593,7 @@ public partial class ModEditWindow } var sb = new StringBuilder( 128 ); - sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 )}" ); + sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 ).TrimEnd()}" ); for( var i = firstVector + 1; i < lastVector; ++i ) { sb.Append( $", [{i}]" ); diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 9809874b..784c9e24 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -3,41 +3,38 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Interface.ImGuiFileDialog; +using System.Text; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; using OtterGui; -using OtterGui.Classes; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.String; using Penumbra.Util; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor< ShpkFile > _shaderPackageTab; + private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true ); - private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); + private readonly FileEditor< ShpkTab > _shaderPackageTab; - private string _shaderPackageNewMaterialParamName = string.Empty; - private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); - private ushort _shaderPackageNewMaterialParamStart = 0; - private ushort _shaderPackageNewMaterialParamEnd = 0; - - private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) + private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) { - var ret = DrawShaderPackageSummary( file, disabled ); + DrawShaderPackageSummary( file ); + + var ret = false; + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( "Vertex Shader", file.VertexShaders, file, disabled ); - - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( "Pixel Shader", file.PixelShaders, file, disabled ); + ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); @@ -45,373 +42,183 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawOtherShaderPackageDetails( file, disabled ); - _shaderPackageFileDialog.Draw(); + file.FileDialog.Draw(); - ret |= file.IsChanged(); + ret |= file.Shpk.IsChanged(); return !disabled && ret; } - private static bool DrawShaderPackageSummary( ShpkFile file, bool _ ) - { - ImGui.Text( $"Shader Package for DirectX {( int )file.DirectXVersion}" ); + private static void DrawShaderPackageSummary( ShpkTab tab ) + => ImGui.TextUnformatted( tab.Header ); - return false; - } - - private bool DrawShaderPackageShaderArray( string objectName, ShpkFile.Shader[] shaders, ShpkFile file, bool disabled ) + private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) { - if( shaders.Length == 0 ) + if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) { - return false; + return; } - if( !ImGui.CollapsingHeader( $"{objectName}s" ) ) + var defaultName = objectName[ 0 ] switch + { + 'V' => $"vs{idx}", + 'P' => $"ps{idx}", + _ => throw new NotImplementedException(), + }; + + var blob = shader.Blob; + tab.FileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, blob ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); + return; + } + + ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + + private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) + { + if( !ImGui.Button( "Replace Shader Program Blob" ) ) + { + return; + } + + tab.FileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + shaders[ idx ].Blob = File.ReadAllBytes( name ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + + try + { + shaders[ idx ].UpdateResources( tab.Shpk ); + tab.Shpk.UpdateResources(); + } + catch( Exception e ) + { + tab.Shpk.SetInvalid(); + ChatUtil.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); + return; + } + + tab.Shpk.SetChanged(); + } ); + } + + private static unsafe void DrawRawDisassembly( Shader shader ) + { + using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ); + if( !t2 ) + { + return; + } + + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 ); + ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size, + ImGuiInputTextFlags.ReadOnly, null, null ); + } + + private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled ) + { + if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) ) { return false; } var ret = false; - - foreach( var (shader, idx) in shaders.WithIndex() ) + for( var idx = 0; idx < shaders.Length; ++idx ) { - using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); - if( t ) + var shader = shaders[ idx ]; + using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); + if( !t ) { - if( ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) + continue; + } + + DrawShaderExportButton( tab, objectName, shader, idx ); + if( !disabled ) + { + ImGui.SameLine(); + DrawShaderImportButton( tab, objectName, shaders, idx ); + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); + ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); + + if( shader.AdditionalHeader.Length > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); + if( t2 ) { - var extension = file.DirectXVersion switch - { - ShpkFile.DxVersion.DirectX9 => ".cso", - ShpkFile.DxVersion.DirectX11 => ".dxbc", - _ => throw new NotImplementedException(), - }; - var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); - var blob = shader.Blob; - _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, blob ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } - - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - - if( !disabled ) - { - ImGui.SameLine(); - if( ImGui.Button( "Replace Shader Program Blob" ) ) - { - _shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - shaders[ idx ].Blob = File.ReadAllBytes( name ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not import Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } - - try - { - shaders[ idx ].UpdateResources( file ); - file.UpdateResources(); - } - catch( Exception e ) - { - file.SetInvalid(); - Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } - - file.SetChanged(); - ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - } - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); - - if( shader.AdditionalHeader.Length > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); - if( t2 ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); - } - } - - using( var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ) ) - { - if( t2 ) - { - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) - { - ImGui.TextUnformatted( shader.Disassembly!.RawDisassembly ); - } - } + ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); } } + + DrawRawDisassembly( shader ); } return ret; } - private bool DrawShaderPackageMaterialParamLayout( ShpkFile file, bool disabled ) + private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled ) { var ret = false; - - var materialParams = file.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - if( !ImGui.CollapsingHeader( $"{materialParams?.Name ?? "Material Parameter"} Layout" ) ) + if( !disabled ) { - return false; - } - - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); - - if( !isSizeWellDefined ) - { - if( materialParams.HasValue ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) { - ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" ); - } - else - { - ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + ret = true; } } - var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 2]; - var orphanParameters = new IndexSet( parameters.Length, true ); - var definedParameters = new HashSet< uint >(); - var hasMalformedParameters = false; - - foreach( var param in file.MaterialParams ) + if( resource.Used == null ) { - definedParameters.Add( param.Id ); - if( ( param.ByteOffset & 0x3 ) == 0 - && ( param.ByteSize & 0x3 ) == 0 - && param.ByteOffset + param.ByteSize <= file.MaterialParamsSize ) - { - var valueOffset = param.ByteOffset >> 2; - var valueCount = param.ByteSize >> 2; - orphanParameters.RemoveRange( valueOffset, valueCount ); - - parameters[ valueOffset ] = ( param.Id, true ); - - for( var i = 1; i < valueCount; ++i ) - { - parameters[ valueOffset + i ] = ( param.Id, false ); - } - } - else - { - hasMalformedParameters = true; - } + return ret; } - ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); - - using( var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + var usedString = UsedComponentString( withSize, resource ); + if( usedString.Length > 0 ) { - if( table ) - { - ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "x" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "y" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "z" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "w" ); - - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); - - for( var idx = 0; idx < parameters.Length; idx += 4 ) - { - var usedComponents = ( materialParams?.Used?[ idx >> 2 ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); - ImGui.TableNextColumn(); - ImGui.Text( $"[{idx >> 2}]" ); - for( var col = 0; col < 4; ++col ) - { - var cell = parameters[ idx + col ]; - ImGui.TableNextColumn(); - var start = cell.HasValue && cell.Value.Item2; - var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; - using var c = ImRaii.PushColor( ImGuiCol.Text, used ? start ? textColorStart : textColorCont : start ? textColorUnusedStart : textColorUnusedCont ); - ImGui.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" ); - } - - ImGui.TableNextRow(); - } - } + ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } - - if( hasMalformedParameters ) + else { - using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); - if( t ) - { - foreach( var param in file.MaterialParams ) - { - if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) - { - ImRaii.TreeNode( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - else if( param.ByteOffset + param.ByteSize > file.MaterialParamsSize ) - { - ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", - ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - } - else if( !disabled && isSizeWellDefined ) - { - using var t = ImRaii.TreeNode( "Add / Remove Parameters" ); - if( t ) - { - for( var i = 0; i < file.MaterialParams.Length; ++i ) - { - var param = file.MaterialParams[ i ]; - using var t2 = ImRaii.TreeNode( - $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); - if( t2 ) - { - if( ImGui.Button( "Remove" ) ) - { - file.MaterialParams = file.MaterialParams.RemoveItems( i ); - ret = true; - } - } - } - - if( orphanParameters.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( "New Parameter" ); - if( t2 ) - { - var starts = orphanParameters.ToArray(); - if( !orphanParameters[ _shaderPackageNewMaterialParamStart ] ) - { - _shaderPackageNewMaterialParamStart = ( ushort )starts[ 0 ]; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; - using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) - { - if( c ) - { - foreach( var start in starts ) - { - var name = MaterialParamName( false, start )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", start == _shaderPackageNewMaterialParamStart ) ) - { - _shaderPackageNewMaterialParamStart = ( ushort )start; - } - } - } - } - - var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; - var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => - { - var ret = i <= lastEndCandidate + 1; - lastEndCandidate = i; - return ret; - } ).ToArray(); - if( Array.IndexOf( ends, _shaderPackageNewMaterialParamEnd ) < 0 ) - { - _shaderPackageNewMaterialParamEnd = ( ushort )ends[ 0 ]; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; - using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) - { - if( c ) - { - foreach( var end in ends ) - { - var name = MaterialParamName( false, end )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", end == _shaderPackageNewMaterialParamEnd ) ) - { - _shaderPackageNewMaterialParamEnd = ( ushort )end; - } - } - } - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) - { - _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); - } - - ImGui.SameLine(); - ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); - if( ImGui.Button( "Add" ) ) - { - if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) - { - ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", - NotificationType.Error ); - } - else - { - file.MaterialParams = file.MaterialParams.AddItem( new ShpkFile.MaterialParam - { - Id = _shaderPackageNewMaterialParamId, - ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), - ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), - } ); - ret = true; - } - } - } - } - } + ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool disabled ) + private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled ) { if( resources.Length == 0 ) { @@ -425,95 +232,109 @@ public partial class ModEditWindow } var ret = false; - - foreach( var (buf, idx) in resources.WithIndex() ) + for( var idx = 0; idx < resources.Length; ++idx ) { - using var t2 = ImRaii.TreeNode( - $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" - + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ), - !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf ); + ref var buf = ref resources[ idx ]; + var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ); + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + font.Dispose(); if( t2 ) { - if( !disabled ) + ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled ); + } + } + + return ret; + } + + private static bool DrawMaterialParamLayoutHeader( string label ) + { + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + var pos = ImGui.GetCursorScreenPos() + + new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y ); + + var ret = ImGui.CollapsingHeader( label ); + ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" ); + return ret; + } + + private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams ) + { + var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); + if( isSizeWellDefined ) + { + return true; + } + + ImGui.TextUnformatted( materialParams.HasValue + ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" + : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + return false; + } + + private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) + { + ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + + using var table = ImRaii.Table( "##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + return false; + } + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableHeadersRow(); + + var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity + var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + + var ret = false; + for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader( $" [{i}]" ); + for( var j = 0; j < 4; ++j ) + { + var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ]; + var color = colorType switch { - // FIXME this probably doesn't belong here - static unsafe bool InputUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + ShpkTab.ColorType.Unused => textColorUnusedStart, + ShpkTab.ColorType.Used => textColorStart, + ShpkTab.ColorType.Continuation => textColorUnusedCont, + ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, + _ => textColorStart, + }; + using var _ = ImRaii.PushId( i * 4 + j ); + var deletable = !disabled && idx >= 0; + using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) ) + { + using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) ) { - fixed( ushort* v2 = &v ) + ImGui.TableNextColumn(); + ImGui.Selectable( name ); + if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) { - return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%hu", flags ); + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx ); + ret = true; + tab.Update(); } } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resources[ idx ].Slot, ImGuiInputTextFlags.None ) ) - { - ret = true; - } + ImGuiUtil.HoverTooltip( tooltip ); } - if( buf.Used != null ) + if( deletable ) { - var used = new List< string >(); - if( withSize ) - { - foreach( var (components, i) in ( buf.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) - { - switch( components ) - { - case 0: break; - case DisassembledShader.VectorComponents.All: - used.Add( $"[{i}]" ); - break; - default: - used.Add( $"[{i}].{new string( components.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); - break; - } - } - - switch( buf.UsedDynamically ?? 0 ) - { - case 0: break; - case DisassembledShader.VectorComponents.All: - used.Add( "[*]" ); - break; - default: - used.Add( $"[*].{new string( buf.UsedDynamically!.Value.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); - break; - } - } - else - { - var components = ( buf.Used != null && buf.Used.Length > 0 ? buf.Used[ 0 ] : 0 ) | ( buf.UsedDynamically ?? 0 ); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) - { - used.Add( "Red" ); - } - - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) - { - used.Add( "Green" ); - } - - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - used.Add( "Blue" ); - } - - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - used.Add( "Alpha" ); - } - } - - if( used.Count > 0 ) - { - ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } + ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." ); } } } @@ -521,7 +342,214 @@ public partial class ModEditWindow return ret; } - private static bool DrawOtherShaderPackageDetails( ShpkFile file, bool disabled ) + private static void DrawShaderPackageMisalignedParameters( ShpkTab tab ) + { + using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); + if( !t ) + { + return; + } + + using var _ = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var name in tab.MalformedParameters ) + { + ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + } + + private static void DrawShaderPackageStartCombo( ShpkTab tab ) + { + using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); + if( c ) + { + foreach( var (start, idx) in tab.Orphans.WithIndex() ) + { + if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) ) + { + tab.UpdateOrphanStart( idx ); + } + } + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( "Start" ); + } + + private static void DrawShaderPackageEndCombo( ShpkTab tab ) + { + using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); + if( c ) + { + var current = tab.Orphans[ tab.NewMaterialParamStart ].Index; + for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i ) + { + var next = tab.Orphans[ i ]; + if( current++ != next.Index ) + { + break; + } + + if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) ) + { + tab.NewMaterialParamEnd = i; + } + } + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( "End" ); + } + + private static bool DrawShaderPackageNewParameter( ShpkTab tab ) + { + if( tab.Orphans.Count == 0 ) + { + return false; + } + + DrawShaderPackageStartCombo( tab ); + DrawShaderPackageEndCombo( tab ); + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) + { + tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); + } + + var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) + ? "The ID is already in use. Please choose a different name." + : string.Empty; + if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * ImGuiHelpers.GlobalScale, ImGui.GetFrameHeight() ), tooltip, + tooltip.Length > 0 ) ) + { + return false; + } + + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam + { + Id = tab.NewMaterialParamId, + ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ), + ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ), + } ); + tab.Update(); + return true; + } + + private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled ) + { + var ret = false; + + var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId ); + if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) ) + { + return false; + } + + var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams ); + + ret |= DrawShaderPackageMaterialMatrix( tab, disabled ); + + if( tab.MalformedParameters.Count > 0 ) + { + DrawShaderPackageMisalignedParameters( tab ); + } + else if( !disabled && sizeWellDefined ) + { + ret |= DrawShaderPackageNewParameter( tab ); + } + + return ret; + } + + private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) + { + if( keys.Count == 0 ) + { + return; + } + + using var t = ImRaii.TreeNode( arrayName ); + if( !t ) + { + return; + } + + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var (key, idx) in keys.WithIndex() ) + { + using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); + if( t2 ) + { + ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + } + } + + private static void DrawShaderPackageNodes( ShpkTab tab ) + { + if( tab.Shpk.Nodes.Length <= 0 ) + { + return; + } + + using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" ); + if( !t ) + { + return; + } + + foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) + { + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); + if( !t2 ) + { + continue; + } + + foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) + { + ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + { + ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ) + .Dispose(); + } + } + } + + private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) { var ret = false; @@ -530,105 +558,109 @@ public partial class ModEditWindow return false; } - ImRaii.TreeNode( $"Version: 0x{file.Version:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, file.Uavs, disabled ); + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - static bool DrawKeyArray( string arrayName, bool withId, ShpkFile.Key[] keys, bool _ ) + DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); + DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); + DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); + DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); + + DrawShaderPackageNodes( tab ); + if( tab.Shpk.Items.Length > 0 ) { - if( keys.Length == 0 ) - { - return false; - } - - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { - return false; - } - - foreach( var (key, idx) in keys.WithIndex() ) - { - using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); - if( t2 ) - { - ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - return false; - } - - ret |= DrawKeyArray( "System Keys", true, file.SystemKeys, disabled ); - ret |= DrawKeyArray( "Scene Keys", true, file.SceneKeys, disabled ); - ret |= DrawKeyArray( "Material Keys", true, file.MaterialKeys, disabled ); - ret |= DrawKeyArray( "Sub-View Keys", false, file.SubViewKeys, disabled ); - - if( file.Nodes.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Nodes ({file.Nodes.Length})" ); + using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); if( t ) { - foreach( var (node, idx) in file.Nodes.WithIndex() ) + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"#{idx}: ID: 0x{node.Id:X8}" ); - if( t2 ) - { - foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) - { - ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - foreach( var (pass, passIdx) in node.Passes.WithIndex() ) - { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - } - } + ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } } } - if( file.Items.Length > 0 ) + if( tab.Shpk.AdditionalData.Length > 0 ) { - using var t = ImRaii.TreeNode( $"Items ({file.Items.Length})" ); + using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" ); if( t ) { - foreach( var (item, idx) in file.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - - if( file.AdditionalData.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); + ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } return ret; } + + private static string UsedComponentString( bool withSize, in Resource resource ) + { + var sb = new StringBuilder( 256 ); + if( withSize ) + { + foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) + { + switch( components ) + { + case 0: break; + case DisassembledShader.VectorComponents.All: + sb.Append( $"[{i}], " ); + break; + default: + sb.Append( $"[{i}]." ); + foreach( var c in components.ToString().Where( char.IsUpper ) ) + { + sb.Append( char.ToLower( c ) ); + } + + sb.Append( ", " ); + break; + } + } + + switch( resource.UsedDynamically ?? 0 ) + { + case 0: break; + case DisassembledShader.VectorComponents.All: + sb.Append( "[*], " ); + break; + default: + sb.Append( "[*]." ); + foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) ) + { + sb.Append( char.ToLower( c ) ); + } + + sb.Append( ", " ); + break; + } + } + else + { + var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 ); + if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) + { + sb.Append( "Red, " ); + } + + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) + { + sb.Append( "Green, " ); + } + + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) + { + sb.Append( "Blue, " ); + } + + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) + { + sb.Append( "Alpha, " ); + } + } + + return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 ); + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs new file mode 100644 index 00000000..448d8e35 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using Lumina.Misc; +using OtterGui; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private class ShpkTab : IWritable + { + public readonly ShpkFile Shpk; + + public string NewMaterialParamName = string.Empty; + public uint NewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + public short NewMaterialParamStart; + public short NewMaterialParamEnd; + + public readonly FileDialogManager FileDialog = ConfigWindow.SetupFileManager(); + + public readonly string Header; + public readonly string Extension; + + public ShpkTab( byte[] bytes ) + { + Shpk = new ShpkFile( bytes, true ); + Header = $"Shader Package for DirectX {( int )Shpk.DirectXVersion}"; + Extension = Shpk.DirectXVersion switch + { + ShpkFile.DxVersion.DirectX9 => ".cso", + ShpkFile.DxVersion.DirectX11 => ".dxbc", + _ => throw new NotImplementedException(), + }; + Update(); + } + + [Flags] + public enum ColorType : byte + { + Unused = 0, + Used = 1, + Continuation = 2, + } + + public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; + public readonly List< string > MalformedParameters = new(); + public readonly HashSet< uint > UsedIds = new(16); + public readonly List< (string Name, short Index) > Orphans = new(16); + + public void Update() + { + var materialParams = Shpk.GetConstantById( ShpkFile.MaterialParamsConstantId ); + var numParameters = ( ( Shpk.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 4; + Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; + + MalformedParameters.Clear(); + UsedIds.Clear(); + foreach( var (param, idx) in Shpk.MaterialParams.WithIndex() ) + { + UsedIds.Add( param.Id ); + var iStart = param.ByteOffset >> 4; + var jStart = ( param.ByteOffset >> 2 ) & 3; + var iEnd = ( param.ByteOffset + param.ByteSize - 1 ) >> 4; + var jEnd = ( ( param.ByteOffset + param.ByteSize - 1 ) >> 2 ) & 3; + if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) + { + MalformedParameters.Add( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}" ); + continue; + } + + if( iEnd >= numParameters ) + { + MalformedParameters.Add( + $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})" ); + continue; + } + + for( var i = iStart; i <= iEnd; ++i ) + { + var end = i == iEnd ? jEnd : 3; + for( var j = i == iStart ? jStart : 0; j <= end; ++j ) + { + var tt = $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})"; + Matrix[ i, j ] = ( $"0x{param.Id:X8}", tt, ( short )idx, 0 ); + } + } + } + + UpdateOrphans( materialParams ); + UpdateColors( materialParams ); + } + + public void UpdateOrphanStart( int orphanStart ) + { + var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + UpdateOrphanStart( orphanStart, oldEnd ); + } + + private void UpdateOrphanStart( int orphanStart, int oldEnd ) + { + var count = Math.Min( NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count ); + NewMaterialParamStart = ( short )orphanStart; + var current = Orphans[ NewMaterialParamStart ].Index; + for( var i = NewMaterialParamStart; i < count; ++i ) + { + var next = Orphans[ i ].Index; + if( current++ != next ) + { + NewMaterialParamEnd = ( short )( i - 1 ); + return; + } + + if( next == oldEnd ) + { + NewMaterialParamEnd = i; + return; + } + } + + NewMaterialParamEnd = ( short )( count - 1 ); + } + + private void UpdateOrphans( ShpkFile.Resource? materialParams ) + { + var oldStart = Orphans.Count > 0 ? Orphans[ NewMaterialParamStart ].Index : -1; + var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + + Orphans.Clear(); + short newMaterialParamStart = 0; + for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + for( var j = 0; j < 4; ++j ) + { + if( !Matrix[ i, j ].Name.IsNullOrEmpty() ) + { + continue; + } + + Matrix[ i, j ] = ( "(none)", string.Empty, -1, 0 ); + var linear = ( short )( 4 * i + j ); + if( oldStart == linear ) + { + newMaterialParamStart = ( short )Orphans.Count; + } + + Orphans.Add( ( $"{materialParams?.Name ?? string.Empty}{MaterialParamName( false, linear )}", linear ) ); + } + + if( Orphans.Count == 0 ) + { + return; + } + + UpdateOrphanStart( newMaterialParamStart, oldEnd ); + } + + private void UpdateColors( ShpkFile.Resource? materialParams ) + { + var lastIndex = -1; + for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + { + var usedComponents = ( materialParams?.Used?[ i ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); + for( var j = 0; j < 4; ++j ) + { + var color = ( ( byte )usedComponents & ( 1 << j ) ) != 0 + ? ColorType.Used + : 0; + if( Matrix[ i, j ].Index == lastIndex || Matrix[ i, j ].Index < 0 ) + { + color |= ColorType.Continuation; + } + + lastIndex = Matrix[ i, j ].Index; + Matrix[ i, j ].Color = color; + } + } + } + + public bool Valid + => Shpk.Valid; + + public byte[] Write() + => Shpk.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 9b0aca95..2c43cca0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -580,11 +580,11 @@ public partial class ModEditWindow : Window, IDisposable DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null ); - _shaderPackageTab = new FileEditor< ShpkFile >( "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk", () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkFile( bytes, true ) ); + bytes => new ShpkTab( bytes ) ); _center = new CombinedTexture( _left, _right ); } From c2ac745d72a79d721d2eb42d8476f5a97960be45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 1 Mar 2023 23:11:05 +0100 Subject: [PATCH 0777/2451] Fix an issue with retainer assignments using ownership wrongly for mannequins. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 8 ++++++++ .../Actors/ActorManager.Identifiers.cs | 18 ++++++++++-------- .../IndividualCollections.Access.cs | 2 +- Penumbra/Collections/IndividualCollections.cs | 2 +- .../UI/Classes/ModEditWindow.FileEditor.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.cs | 2 +- .../ConfigWindow.CollectionsTab.Individual.cs | 2 +- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 26565dea..81199fc6 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -14,11 +14,19 @@ public readonly struct ActorIdentifier : IEquatable public static readonly ActorIdentifier Invalid = new(IdentifierType.Invalid, 0, 0, 0, ByteString.Empty); + public enum RetainerType : ushort + { + Both = 0, + Bell = 1, + Mannequin = 2, + } + // @formatter:off [FieldOffset( 0 )] public readonly IdentifierType Type; // All [FieldOffset( 1 )] public readonly ObjectKind Kind; // Npc, Owned [FieldOffset( 2 )] public readonly ushort HomeWorld; // Player, Owned [FieldOffset( 2 )] public readonly ushort Index; // NPC + [FieldOffset( 2 )] public readonly RetainerType Retainer; // Retainer [FieldOffset( 2 )] public readonly ScreenActor Special; // Special [FieldOffset( 4 )] public readonly uint DataId; // Owned, NPC [FieldOffset( 8 )] public readonly ByteString PlayerName; // Player, Owned diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 15c12714..daa07168 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -33,7 +33,7 @@ public partial class ActorManager case IdentifierType.Retainer: { var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); - return CreateRetainer(name); + return CreateRetainer(name, 0); } case IdentifierType.Owned: { @@ -318,8 +318,9 @@ public partial class ActorManager if (!actualName.Equals(retainerName)) { var ident = check - ? CreateRetainer(retainerName) - : CreateIndividualUnchecked(IdentifierType.Retainer, retainerName, actor->ObjectIndex, ObjectKind.EventNpc, dataId); + ? CreateRetainer(retainerName, ActorIdentifier.RetainerType.Mannequin) + : CreateIndividualUnchecked(IdentifierType.Retainer, retainerName, (ushort)ActorIdentifier.RetainerType.Mannequin, + ObjectKind.EventNpc, dataId); if (ident.IsValid) return ident; } @@ -349,8 +350,9 @@ public partial class ActorManager { var name = new ByteString(actor->Name); return check - ? CreateRetainer(name) - : CreateIndividualUnchecked(IdentifierType.Retainer, name, 0, ObjectKind.None, uint.MaxValue); + ? CreateRetainer(name, ActorIdentifier.RetainerType.Bell) + : CreateIndividualUnchecked(IdentifierType.Retainer, name, (ushort)ActorIdentifier.RetainerType.Bell, ObjectKind.None, + uint.MaxValue); } default: { @@ -388,7 +390,7 @@ public partial class ActorManager => type switch { IdentifierType.Player => CreatePlayer(name, homeWorld), - IdentifierType.Retainer => CreateRetainer(name), + IdentifierType.Retainer => CreateRetainer(name, (ActorIdentifier.RetainerType)homeWorld), IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), IdentifierType.Special => CreateSpecial((ScreenActor)homeWorld), IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), @@ -410,12 +412,12 @@ public partial class ActorManager return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); } - public ActorIdentifier CreateRetainer(ByteString name) + public ActorIdentifier CreateRetainer(ByteString name, ActorIdentifier.RetainerType type) { if (!VerifyRetainerName(name.Span)) return ActorIdentifier.Invalid; - return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, 0, 0, name); + return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, (ushort)type, 0, name); } public ActorIdentifier CreateSpecial(ScreenActor actor) diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 83f32b18..713f8524 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -41,7 +41,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - if( Penumbra.Config.UseOwnerNameForCharacterCollection ) + if( identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && Penumbra.Config.UseOwnerNameForCharacterCollection ) { return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); } diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index bca5b4a2..61ea2c0e 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -70,7 +70,7 @@ public sealed partial class IndividualCollections return AddResult.Invalid; } - identifiers = new[] { _actorManager.CreateRetainer( retainerName ) }; + identifiers = new[] { _actorManager.CreateRetainer( retainerName, 0 ) }; break; case IdentifierType.Owned: if( !ByteString.FromString( name, out var ownerName ) ) diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index d3a91f86..19630beb 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -175,7 +175,7 @@ public partial class ModEditWindow } private static T? DefaultParseFile( byte[] bytes ) - => Activator.CreateInstance( typeof( T ), BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding, bytes ) as T; + => Activator.CreateInstance( typeof( T ), bytes ) as T; private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 2c43cca0..3fe48126 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -584,7 +584,7 @@ public partial class ModEditWindow : Window, IDisposable () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkTab( bytes ) ); + null ); _center = new CombinedTexture( _left, _right ); } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 2e3a7194..660bb1d5 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -322,7 +322,7 @@ public partial class ConfigWindow IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, + _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, Array.Empty< uint >(), out _newRetainerIdentifiers ) switch { _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, From 1f942491ac2f0d711a417441f2e476b0dfa86a2b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Mar 2023 13:43:00 +0100 Subject: [PATCH 0778/2451] Move mod creation functions to own subclass. --- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Import/TexToolsImporter.Archives.cs | 7 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 28 +-- Penumbra/Mods/Editor/Mod.Normalization.cs | 4 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 25 +-- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 2 +- Penumbra/Mods/Mod.Creation.cs | 187 ----------------- Penumbra/Mods/Mod.Creator.cs | 188 ++++++++++++++++++ Penumbra/Mods/Mod.TemporaryMod.cs | 4 +- .../Subclasses/Mod.Files.MultiModGroup.cs | 5 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 4 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 8 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 6 +- 13 files changed, 237 insertions(+), 233 deletions(-) delete mode 100644 Penumbra/Mods/Mod.Creation.cs create mode 100644 Penumbra/Mods/Mod.Creator.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8e609cb7..be3a01ac 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -829,7 +829,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection( string name ) { CheckInitialized(); - if( name.Length == 0 || Mod.ReplaceBadXivSymbols( name ) != name ) + if( name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols( name ) != name ) { return PenumbraApiEc.InvalidArgument; } diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index a6403027..a00d042c 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -44,7 +44,7 @@ public partial class TexToolsImporter }; Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." ); - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -99,13 +99,13 @@ public partial class TexToolsImporter // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if( leadDir ) { - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName, false ); + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, baseName, false ); Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); Directory.Delete( oldName ); } else { - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, name, false ); + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, name, false ); Directory.Move( oldName, _currentModDirectory.FullName ); } @@ -114,6 +114,7 @@ public partial class TexToolsImporter return _currentModDirectory; } + // Search the archive for the meta.json file which needs to exist. private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) { diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index e2edfe92..2fb79390 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -34,14 +34,14 @@ public partial class TexToolsImporter var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); // Create a new ModMeta from the TTMP mod list info - Mod.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); + Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList ); - Mod.CreateDefaultFiles( _currentModDirectory ); + Mod.Creator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -90,15 +90,15 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); - Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); - Mod.CreateDefaultFiles( _currentModDirectory ); + Mod.Creator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -135,8 +135,8 @@ public partial class TexToolsImporter _currentNumOptions = GetOptionCount( modList ); _currentModName = modList.Name; - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); - Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); + _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); if( _currentNumOptions == 0 ) { @@ -173,7 +173,7 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = Mod.NewSubFolderName( _currentModDirectory, name ) + var groupFolder = Mod.Creator.NewSubFolderName( _currentModDirectory, name ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); @@ -183,10 +183,10 @@ public partial class TexToolsImporter var option = allOptions[ i + optionIdx ]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name ) + var optionFolder = Mod.Creator.NewSubFolderName( groupFolder, option.Name ) ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( Mod.CreateSubMod( _currentModDirectory, optionFolder, option ) ); + options.Add( Mod.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); if( option.IsChecked ) { defaultSettings = group.SelectionType == GroupType.Multi @@ -207,12 +207,12 @@ public partial class TexToolsImporter if( empty != null ) { _currentOptionName = empty.Name; - options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + options.Insert( 0, Mod.Creator.CreateEmptySubMod( empty.Name ) ); defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; } } - Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + Mod.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, defaultSettings ?? 0, group.Description, options ); ++groupPriority; } @@ -220,7 +220,7 @@ public partial class TexToolsImporter } ResetStreamDisposer(); - Mod.CreateDefaultFiles( _currentModDirectory ); + Mod.Creator.CreateDefaultFiles( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs index 73a92a09..0696271d 100644 --- a/Penumbra/Mods/Editor/Mod.Normalization.cs +++ b/Penumbra/Mods/Editor/Mod.Normalization.cs @@ -150,11 +150,11 @@ public partial class Mod foreach( var (group, groupIdx) in _mod.Groups.WithIndex() ) { _redirections[ groupIdx + 1 ] = new Dictionary< Utf8GamePath, FullPath >[group.Count]; - var groupDir = CreateModFolder( directory, group.Name ); + var groupDir = Creator.CreateModFolder( directory, group.Name ); foreach( var option in group.OfType< SubMod >() ) { - var optionDir = CreateModFolder( groupDir, option.Name ); + var optionDir = Creator.CreateModFolder( groupDir, option.Name ); newDict = new Dictionary< Utf8GamePath, FullPath >( option.FileData.Count ); _redirections[ groupIdx + 1 ][ option.OptionIdx ] = newDict; foreach( var (gamePath, fullPath) in option.FileData ) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index a1de7b7a..51b3dea0 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -64,12 +64,6 @@ public static class EquipmentSwap var imcFileTo = new ImcFile( imcManip); var isAccessory = slot.IsAccessory(); - var estType = slot switch - { - EquipSlot.Head => EstManipulation.EstType.Head, - EquipSlot.Body => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, - }; var skipFemale = false; var skipMale = false; @@ -89,12 +83,6 @@ public static class EquipmentSwap continue; } - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); - if( est != null ) - { - swaps.Add( est ); - } - try { var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); @@ -140,6 +128,19 @@ public static class EquipmentSwap { var mdl = CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo ); meta.ChildSwaps.Add( mdl ); + + var estType = slot switch + { + EquipSlot.Head => EstManipulation.EstType.Head, + EquipSlot.Body => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; + + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); + if( est != null ) + { + meta.ChildSwaps.Add( est ); + } } else if( !ownMtrl && meta.SwapAppliedIsDefault ) { diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 8f092ed4..3712831f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -175,7 +175,7 @@ public partial class Mod return NewDirectoryState.Identical; } - var fixedNewName = ReplaceBadXivSymbols( newName ); + var fixedNewName = Creator.ReplaceBadXivSymbols( newName ); if( fixedNewName != newName ) { return NewDirectoryState.ContainsInvalidSymbols; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs deleted file mode 100644 index 13cd4ab7..00000000 --- a/Penumbra/Mods/Mod.Creation.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Dalamud.Utility; -using OtterGui.Classes; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Import; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public partial class Mod -{ - /// - /// Create and return a new directory based on the given directory and name, that is
- /// - Not Empty.
- /// - Unique, by appending (digit) for duplicates.
- /// - Containing no symbols invalid for FFXIV or windows paths.
- ///
- /// - /// - /// - /// - /// - internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) - { - var name = modListName; - if( name.Length == 0 ) - { - name = "_"; - } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - if( newModFolder.Length == 0 ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - if( create ) - { - Directory.CreateDirectory( newModFolder ); - } - - return new DirectoryInfo( newModFolder ); - } - - /// - /// Create the name for a group or option subfolder based on its parent folder and given name. - /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. - /// - internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) - { - var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); - } - - // Create the file containing the meta information about a mod from scratch. - internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, - string? website ) - { - var mod = new Mod( directory ); - mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! ); - mod.Author = author != null ? new LowerString( author ) : mod.Author; - mod.Description = description ?? mod.Description; - mod.Version = version ?? mod.Version; - mod.Website = website ?? mod.Website; - mod.SaveMetaFile(); // Not delayed. - } - - // Create a file for an option group from given data. - internal static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) - { - switch( type ) - { - case GroupType.Multi: - { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.Save( group, baseFolder, index ); - break; - } - case GroupType.Single: - { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.Save( group, baseFolder, index ); - break; - } - } - } - - // Create the data for a given sub mod from its data and the folder it is based on. - internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) - { - var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) - .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) - .Where( t => t.Item1 ); - - var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; - foreach( var (_, gamePath, file) in list ) - { - mod.FileData.TryAdd( gamePath, file ); - } - - mod.IncorporateMetaChanges( baseFolder, true ); - return mod; - } - - // Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - - // Create the default data file from all unused files that were not handled before - // and are used in sub mods. - internal static void CreateDefaultFiles( DirectoryInfo directory ) - { - var mod = new Mod( directory ); - mod.Reload( false, out _ ); - foreach( var file in mod.FindUnusedFiles() ) - { - if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - { - mod._default.FileData.TryAdd( gamePath, file ); - } - } - - mod._default.IncorporateMetaChanges( directory, true ); - mod.SaveDefaultMod(); - } - - // Return the name of a new valid directory based on the base directory and the given name. - private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); - - // Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) - { - if( s == "." ) - { - return replacement; - } - - if( s == ".." ) - { - return replacement + replacement; - } - - StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) - { - if( c.IsInvalidInPath() ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs new file mode 100644 index 00000000..85fbfa50 --- /dev/null +++ b/Penumbra/Mods/Mod.Creator.cs @@ -0,0 +1,188 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Utility; +using OtterGui.Classes; +using OtterGui.Filesystem; +using Penumbra.Api.Enums; +using Penumbra.Import; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public partial class Mod +{ + internal static class Creator + { + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ /// + /// + /// + /// + /// + public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) + { + var name = modListName; + if( name.Length == 0 ) + { + name = "_"; + } + + var newModFolderBase = NewOptionDirectory( outDirectory, name ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + if( newModFolder.Length == 0 ) + { + throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); + } + + if( create ) + { + Directory.CreateDirectory( newModFolder ); + } + + return new DirectoryInfo( newModFolder ); + } + + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + { + var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + } + + /// Create the file containing the meta information about a mod from scratch. + public static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, + string? website ) + { + var mod = new Mod( directory ); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! ); + mod.Author = author != null ? new LowerString( author ) : mod.Author; + mod.Description = description ?? mod.Description; + mod.Version = version ?? mod.Version; + mod.Website = website ?? mod.Website; + mod.SaveMetaFile(); // Not delayed. + } + + /// Create a file for an option group from given data. + public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, + int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) + { + switch( type ) + { + case GroupType.Multi: + { + var group = new MultiModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + IModGroup.Save( group, baseFolder, index ); + break; + } + case GroupType.Single: + { + var group = new SingleModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + IModGroup.Save( group, baseFolder, index ); + break; + } + } + } + + /// Create the data for a given sub mod from its data and the folder it is based on. + public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = option.Name, + Description = option.Description, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + /// Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod( string name ) + => new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = name, + }; + + /// + /// Create the default data file from all unused files that were not handled before + /// and are used in sub mods. + /// + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod( directory ); + mod.Reload( false, out _ ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + { + mod._default.FileData.TryAdd( gamePath, file ); + } + } + + mod._default.IncorporateMetaChanges( directory, true ); + mod.SaveDefaultMod(); + } + + /// Return the name of a new valid directory based on the base directory and the given name. + public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); + + /// Normalize for nicer names, and remove invalid symbols or invalid paths. + public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) + { + switch( s ) + { + case ".": return replacement; + case "..": return replacement + replacement; + } + + StringBuilder sb = new(s.Length); + foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) + { + if( c.IsInvalidInPath() ) + { + sb.Append( replacement ); + } + else + { + sb.Append( c ); + } + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 802852e9..6de0a682 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -51,9 +51,9 @@ public sealed partial class Mod DirectoryInfo? dir = null; try { - dir = CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); - CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, + Creator.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); var defaultMod = mod._default; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index fe7b1173..6b6259f6 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -3,11 +3,13 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Numerics; +using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Util; namespace Penumbra.Mods; @@ -63,8 +65,7 @@ public partial class Mod { if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) { - Penumbra.Log.Warning( - $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options." ); + ChatUtil.NotificationMessage( $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", NotificationType.Warning ); break; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index ed0fd1fb..981b336b 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -67,7 +67,7 @@ public partial class Mod _default.WriteTexToolsMeta( ModPath ); foreach( var group in Groups ) { - var dir = NewOptionDirectory( ModPath, group.Name ); + var dir = Creator.NewOptionDirectory( ModPath, group.Name ); if( !dir.Exists ) { dir.Create(); @@ -75,7 +75,7 @@ public partial class Mod foreach( var option in group.OfType< SubMod >() ) { - var optionDir = NewOptionDirectory( dir, option.Name ); + var optionDir = Creator.NewOptionDirectory( dir, option.Name ); if( !optionDir.Exists ) { optionDir.Create(); diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 8215bcc7..cd551088 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -216,9 +216,9 @@ public class ItemSwapWindow : IDisposable private void CreateMod() { - var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); - Mod.CreateDefaultFiles( newDir ); + var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); + Mod.Creator.CreateDefaultFiles( newDir ); Penumbra.ModManager.AddMod( newDir ); if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) { @@ -239,7 +239,7 @@ public class ItemSwapWindow : IDisposable DirectoryInfo? optionFolderName = null; try { - optionFolderName = Mod.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); + optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); if( optionFolderName?.Exists == true ) { throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 61b46a36..497a2f0e 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -92,9 +92,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { try { - var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); - Mod.CreateDefaultFiles( newDir ); + var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); + Mod.Creator.CreateDefaultFiles( newDir ); Penumbra.ModManager.AddMod( newDir ); _newModName = string.Empty; } From cdc4ee699132f3755d11cae2bf5577bc7613fb15 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Mar 2023 13:43:20 +0100 Subject: [PATCH 0779/2451] Change phyb and sklb resolving for item swap. --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 27 +++++++++++---------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 4 +-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 51b3dea0..231ce02d 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -64,6 +64,12 @@ public static class EquipmentSwap var imcFileTo = new ImcFile( imcManip); var isAccessory = slot.IsAccessory(); + var estType = slot switch + { + EquipSlot.Head => EstManipulation.EstType.Head, + EquipSlot.Body => EstManipulation.EstType.Body, + _ => ( EstManipulation.EstType )0, + }; var skipFemale = false; var skipMale = false; @@ -83,6 +89,7 @@ public static class EquipmentSwap continue; } + try { var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); @@ -90,6 +97,13 @@ public static class EquipmentSwap { swaps.Add( eqdp ); } + + var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false; + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); + if( est != null ) + { + swaps.Add( est ); + } } catch( ItemSwap.MissingFileException e ) { @@ -128,19 +142,6 @@ public static class EquipmentSwap { var mdl = CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo ); meta.ChildSwaps.Add( mdl ); - - var estType = slot switch - { - EquipSlot.Head => EstManipulation.EstType.Head, - EquipSlot.Body => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, - }; - - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo ); - if( est != null ) - { - meta.ChildSwaps.Add( est ); - } } else if( !ownMtrl && meta.SwapAppliedIsDefault ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 68812674..2369a92d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -159,7 +159,7 @@ public static class ItemSwap /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. public static MetaSwap? CreateEst( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, - GenderRace genderRace, SetId idFrom, SetId idTo ) + GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl ) { if( type == 0 ) { @@ -171,7 +171,7 @@ public static class ItemSwap var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) ); var est = new MetaSwap( manips, fromDefault, toDefault ); - if( est.SwapApplied.Est.Entry >= 2 ) + if( ownMdl && est.SwapApplied.Est.Entry >= 2 ) { var phyb = CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); est.ChildSwaps.Add( phyb ); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index deb15bee..66d4851e 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -145,7 +145,7 @@ public class ItemSwapContainer }; var metaResolver = MetaResolver( collection ); - var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to ); + var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to, true ); Swaps.Add( mdl ); if( est != null ) From 522fc832dba009b5bde423d4e302d81860f887d9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Mar 2023 18:06:30 +0100 Subject: [PATCH 0780/2451] Add handling of too large multi groups on import of pmp or adding mods via IPC. --- Penumbra/Import/TexToolsImporter.Archives.cs | 1 + Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 1 + Penumbra/Mods/Mod.Creator.cs | 116 +++++++++++++++++- .../Subclasses/Mod.Files.MultiModGroup.cs | 2 +- 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index a00d042c..9d9f5a69 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -110,6 +110,7 @@ public partial class TexToolsImporter } _currentModDirectory.Refresh(); + Mod.Creator.SplitMultiGroups( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 3712831f..24d3a0e6 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -138,6 +138,7 @@ public partial class Mod return; } + Creator.SplitMultiGroups( modFolder ); var mod = LoadMod( modFolder, true ); if( mod == null ) { diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 85fbfa50..3a5368ac 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -1,8 +1,12 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; @@ -13,7 +17,7 @@ namespace Penumbra.Mods; public partial class Mod { - internal static class Creator + internal static partial class Creator { /// /// Create and return a new directory based on the given directory and name, that is
@@ -184,5 +188,115 @@ public partial class Mod return sb.ToString(); } + + public static void SplitMultiGroups( DirectoryInfo baseDir ) + { + var mod = new Mod( baseDir ); + + var files = mod.GroupFiles.ToList(); + var idx = 0; + var reorder = false; + foreach( var groupFile in files ) + { + ++idx; + try + { + if( reorder ) + { + var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; + Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); + groupFile.MoveTo( newName, false ); + } + } + catch( Exception ex ) + { + throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); + } + + try + { + var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); + if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) + { + continue; + } + + var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; + if( name.Length == 0 ) + { + continue; + } + + + var options = json[ "Options" ]?.Children().ToList(); + if( options == null ) + { + continue; + } + + if( options.Count <= IModGroup.MaxMultiOptions ) + { + continue; + } + + Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); + var clone = json.DeepClone(); + reorder = true; + foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var newOptions = clone[ "Options" ]!.Children().ToList(); + foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var match = DuplicateNumber().Match( name ); + var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; + name = match.Success ? name[ ..4 ] : name; + var oldName = $"{name}, Part {startNumber}"; + var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + var newName = $"{name}, Part {startNumber + 1}"; + var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + json[ nameof( IModGroup.Name ) ] = oldName; + clone[ nameof( IModGroup.Name ) ] = newName; + + clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; + + Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); + using( var oldFile = File.CreateText( oldPath ) ) + { + using var j = new JsonTextWriter( oldFile ) + { + Formatting = Formatting.Indented, + }; + json.WriteTo( j ); + } + + Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); + using( var newFile = File.CreateText( newPath ) ) + { + using var j = new JsonTextWriter( newFile ) + { + Formatting = Formatting.Indented, + }; + clone.WriteTo( j ); + } + + Penumbra.Log.Debug( + $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); + groupFile.Delete(); + } + catch( Exception ex ) + { + throw new Exception( "Could not split multi group file on .pmp import.", ex ); + } + } + } + + [GeneratedRegex( @", Part (\d+)$", RegexOptions.NonBacktracking )] + private static partial Regex DuplicateNumber(); } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 6b6259f6..90bc687a 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -46,7 +46,6 @@ public partial class Mod public static MultiModGroup? Load( Mod mod, JObject json, int groupIdx ) { - var options = json[ "Options" ]; var ret = new MultiModGroup() { Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, @@ -59,6 +58,7 @@ public partial class Mod return null; } + var options = json["Options"]; if( options != null ) { foreach( var child in options.Children() ) From 21be245c5c5563067acd428456a28df60c350765 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Mar 2023 18:43:37 +0100 Subject: [PATCH 0781/2451] Add collapsing to big option groups. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 104 +++++++++++++----- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/OtterGui b/OtterGui index 9e98cb97..95a26e94 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9e98cb9722bed3129134c7bc2fbe51268b2d6acd +Subproject commit 95a26e944d550a3e77150667af00c23ef307b672 diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 9d482c35..10cac9ba 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -231,27 +232,71 @@ public partial class ConfigWindow using var id = ImRaii.PushId( groupIdx ); var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; Widget.BeginFramedGroup( group.Name, group.Description ); - for( var idx = 0; idx < group.Count; ++idx ) + + void DrawOptions() { - id.Push( idx ); - var option = group[ idx ]; - if( ImGui.RadioButton( option.Name, selectedOption == idx ) ) + for( var idx = 0; idx < group.Count; ++idx ) { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx ); - } + using var i = ImRaii.PushId( idx ); + var option = group[ idx ]; + if( ImGui.RadioButton( option.Name, selectedOption == idx ) ) + { + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx ); + } - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); + if( option.Description.Length > 0 ) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker( option.Description ); + } } - - id.Pop( idx ); } + DrawCollapseHandling( group, DrawOptions ); + Widget.EndFramedGroup(); } + private static void DrawCollapseHandling( IModGroup group, Action draw ) + { + if( group.Count <= 5 ) + { + draw(); + } + else + { + var collapseId = ImGui.GetID( "Collapse" ); + var shown = ImGui.GetStateStorage().GetBool( collapseId, true ); + if( shown ) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy( new Vector2( ImGui.GetFrameHeight() ) ); + using( var _ = ImRaii.Group() ) + { + draw(); + } + + var width = ImGui.GetItemRectSize().X; + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos( pos ); + if( ImGui.Button( $"Hide {group.Count} Options", new Vector2( width, 0 ) ) ) + { + ImGui.GetStateStorage().SetBool( collapseId, !shown ); + } + + ImGui.SetCursorPos( endPos ); + } + else + { + var max = group.Max( o => ImGui.CalcTextSize( o.Name ).X ) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; + if( ImGui.Button( $"Show {group.Count} Options", new Vector2( max, 0 ) ) ) + { + ImGui.GetStateStorage().SetBool( collapseId, !shown ); + } + } + } + } + // Draw a multi group selector as a bordered set of checkboxes. // If a description is provided, add a help marker in the title. private void DrawMultiGroup( IModGroup group, int groupIdx ) @@ -259,27 +304,32 @@ public partial class ConfigWindow using var id = ImRaii.PushId( groupIdx ); var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; Widget.BeginFramedGroup( group.Name, group.Description ); - for( var idx = 0; idx < group.Count; ++idx ) + + void DrawOptions() { - var option = group[ idx ]; - id.Push( idx ); - var flag = 1u << idx; - var setting = ( flags & flag ) != 0; - if( ImGui.Checkbox( option.Name, ref setting ) ) + for( var idx = 0; idx < group.Count; ++idx ) { - flags = setting ? flags | flag : flags & ~flag; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); - } + using var i = ImRaii.PushId( idx ); + var option = group[ idx ]; + var flag = 1u << idx; + var setting = ( flags & flag ) != 0; - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); - } + if( ImGui.Checkbox( option.Name, ref setting ) ) + { + flags = setting ? flags | flag : flags & ~flag; + Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); + } - id.Pop(); + if( option.Description.Length > 0 ) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker( option.Description ); + } + } } + DrawCollapseHandling( group, DrawOptions ); + Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) From 3c78d6b695fcff54e097cfcd68fdc39a2d9c4f98 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 Mar 2023 19:55:02 +0100 Subject: [PATCH 0782/2451] Add Changelog. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 -- Penumbra/UI/ConfigWindow.Changelog.cs | 25 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 2fb79390..33e3c918 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -2,9 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using Newtonsoft.Json; -using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Util; diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 50c80eea..32bf5da7 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -35,17 +35,38 @@ public partial class ConfigWindow Add6_4_0( ret ); Add6_5_0( ret ); Add6_5_2( ret ); - + Add6_6_0( ret ); return ret; } + private static void Add6_6_0( Changelog log ) + => log.NextVersion( "Version 0.6.6.0" ) + .RegisterEntry( "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively." ) + .RegisterEntry( "Added an option to display Single Selection Groups as a group of radio buttons similar to Multi Selection Groups, when the number of available options is below the specified value. Default value is 2." ) + .RegisterEntry( "Added a button in option groups to collapse the option list if it has more than 5 available options." ) + .RegisterEntry( + "Penumbra now circumvents the games inability to read files at paths longer than 260 UTF16 characters and can also deal with generic unicode symbols in paths." ) + .RegisterEntry( "This means that Penumbra should no longer cause issues when files become too long or when there is a non-ASCII character in them.", 1 ) + .RegisterEntry( + "Shorter paths are still better, so restrictions on the root directory have not been relaxed. Mod names should no longer replace non-ASCII symbols on import though.", 1 ) + .RegisterEntry( + "Resource logging has been relegated to its own tab with better filtering. Please do not keep resource logging on arbitrarily or set a low record limit if you do, otherwise this eats a lot of performance and memory after a while." ) + .RegisterEntry( "Added a lot of facilities to edit the shader part of .mtrl files and .shpk files themselves in the Advanced Editing Tab (Thanks Ny and aers)." ) + .RegisterEntry( "Added splitting of Multi Selection Groups with too many options when importing .pmp files or adding mods via IPC." ) + .RegisterEntry( "Discovery, Reloading and Unloading of a specified mod is now possible via HTTP API (Thanks Sebastina)." ) + .RegisterEntry( "Cleaned up the HTTP API somewhat, removed currently useless options." ) + .RegisterEntry( "Fixed an issue when extracting some textures." ) + .RegisterEntry( "Fixed an issue with mannequins inheriting individual assignments for the current player when using ownership." ) + .RegisterEntry( "Fixed an issue with the resolving of .phyb and .sklb files for Item Swaps of head or body items with an EST entry but no unique racial model." ); + private static void Add6_5_2( Changelog log ) => log.NextVersion( "Version 0.6.5.2" ) .RegisterEntry( "Updated for game version 6.31 Hotfix." ) .RegisterEntry( "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)" ) .RegisterEntry( "Those are now accurately parsed from TTMPs, too.", 1 ) .RegisterEntry( "Improved launch times somewhat through parallelization of some tasks." ) - .RegisterEntry( "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled." ) + .RegisterEntry( + "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled." ) .RegisterEntry( "Fixed an issue with IMC changes and Mare Synchronos interoperability." ) .RegisterEntry( "Fixed an issue with housing mannequins crashing the game when resource logging was enabled." ) .RegisterEntry( "Fixed an issue generating Mip Maps for texture import on Wine." ); From 009499cdf67371a32f9313c971773175c5965820 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 4 Mar 2023 18:57:48 +0000 Subject: [PATCH 0783/2451] [CI] Updating repo.json for refs/tags/0.6.6.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4fc8d718..addc299a 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.5.2", - "TestingAssemblyVersion": "0.6.5.2", + "AssemblyVersion": "0.6.6.0", + "TestingAssemblyVersion": "0.6.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.5.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 64c8f29c47e052ac3ba6d733933e374ae5e579ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Mar 2023 14:52:28 +0100 Subject: [PATCH 0784/2451] Fix issue with assigning indexed npcs. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 4 +-- .../Actors/ActorManager.Identifiers.cs | 26 ++++++++++--------- Penumbra/Api/PenumbraApi.cs | 4 +-- .../IndividualCollections.Access.cs | 4 +-- Penumbra/Collections/ResolveData.cs | 2 +- Penumbra/CommandHandler.cs | 2 +- .../Resolver/PathResolver.Identification.cs | 2 +- .../ConfigWindow.CollectionsTab.Individual.cs | 14 +++++----- Penumbra/UI/ConfigWindow.DebugTab.cs | 2 +- 9 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 7889f679..51e1811a 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -256,7 +256,7 @@ public sealed partial class ActorManager : IDisposable return false; } - id = FromObject(&other->GameObject, out _, false, true); + id = FromObject(&other->GameObject, out _, false, true, false); return true; } @@ -283,7 +283,7 @@ public sealed partial class ActorManager : IDisposable if (obj != null && obj->ObjectKind is (byte)ObjectKind.Player && Compare(gameObject, (Character*)obj)) - return FromObject(obj, out _, false, true); + return FromObject(obj, out _, false, true, false); } return ActorIdentifier.Invalid; diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index daa07168..9f5f246f 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -244,7 +244,7 @@ public partial class ActorManager /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. ///
public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check) + out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check, bool withoutIndex) { owner = null; if (actor == null) @@ -298,9 +298,10 @@ public partial class ActorManager } } + var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; return check - ? CreateNpc(ObjectKind.BattleNpc, nameId, actor->ObjectIndex) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.BattleNpc, nameId); + ? CreateNpc(ObjectKind.BattleNpc, nameId, index) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.BattleNpc, nameId); } case ObjectKind.EventNpc: { @@ -326,9 +327,10 @@ public partial class ActorManager } } + var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; return check - ? CreateNpc(ObjectKind.EventNpc, dataId, actor->ObjectIndex) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, dataId); + ? CreateNpc(ObjectKind.EventNpc, dataId, index) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.EventNpc, dataId); } case ObjectKind.MountType: case ObjectKind.Companion: @@ -357,7 +359,7 @@ public partial class ActorManager default: { var name = new ByteString(actor->Name); - var index = actor->ObjectIndex; + var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); } } @@ -379,12 +381,12 @@ public partial class ActorManager } public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, - bool allowPlayerNpc, bool check) + bool allowPlayerNpc, bool check, bool withoutIndex) => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, - check); + check, withoutIndex); - public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check) - => FromObject(actor, out _, allowPlayerNpc, check); + public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check, bool withoutIndex) + => FromObject(actor, out _, allowPlayerNpc, check, withoutIndex); public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch @@ -551,13 +553,13 @@ public partial class ActorManager => actor is >= ScreenActor.CharacterScreen and <= ScreenActor.Card8; /// Verify that the object index is a valid index for an NPC. - public static bool VerifyIndex(ushort index) + public bool VerifyIndex(ushort index) { return index switch { ushort.MaxValue => true, < 200 => index % 2 == 0, - > (ushort)ScreenActor.Card8 => index < 426, + > (ushort)ScreenActor.Card8 => index < _objects.Length, _ => false, }; } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index be3a01ac..0df89f55 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -848,7 +848,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidArgument; } - var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ], false, false ); + var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ], false, false, true ); if( !identifier.IsValid ) { return PenumbraApiEc.InvalidArgument; @@ -1042,7 +1042,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); - return Penumbra.Actors.FromObject( ptr, out _, false, true ); + return Penumbra.Actors.FromObject( ptr, out _, false, true, true ); } // Resolve a path given by string for a specific collection. diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 713f8524..0b43baf3 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -143,10 +143,10 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ } public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, true, false ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, true, false, false ), out collection ); public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false ), out collection ); + => TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false, false ), out collection ); private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) { diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 90c56144..485f2c08 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -47,7 +47,7 @@ public readonly struct ResolveData try { - var id = Penumbra.Actors.FromObject( ( GameObject* )AssociatedGameObject, out _, false, true ); + var id = Penumbra.Actors.FromObject( ( GameObject* )AssociatedGameObject, out _, false, true, true ); if( id.IsValid ) { var name = id.ToString(); diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 3779e512..0af75477 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -297,7 +297,7 @@ public class CommandHandler : IDisposable { if( ObjectReloader.GetName( split[ 2 ].ToLowerInvariant(), out var obj ) ) { - identifier = _actors.FromObject( obj, false, true ); + identifier = _actors.FromObject( obj, false, true, true ); if( !identifier.IsValid ) { Dalamud.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index cbd91b4f..85839f24 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -56,7 +56,7 @@ public unsafe partial class PathResolver return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); } - var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false ); + var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false, false ); if( identifier.Type is IdentifierType.Special ) { ( identifier, var type ) = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 660bb1d5..7f8e0f9a 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -243,10 +243,10 @@ public partial class ConfigWindow var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); - var assignWidth = new Vector2((_window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); - var change = DrawNewCurrentPlayerCollection(assignWidth); + var assignWidth = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); + var change = DrawNewCurrentPlayerCollection( assignWidth ); ImGui.SameLine(); - change |= DrawNewTargetCollection(assignWidth); + change |= DrawNewTargetCollection( assignWidth ); change |= DrawNewPlayerCollection( buttonWidth1, width ); ImGui.SameLine(); @@ -263,7 +263,7 @@ public partial class ConfigWindow } } - private static bool DrawNewCurrentPlayerCollection(Vector2 width) + private static bool DrawNewCurrentPlayerCollection( Vector2 width ) { var player = Penumbra.Actors.GetCurrentPlayer(); var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); @@ -285,10 +285,10 @@ public partial class ConfigWindow return false; } - private static bool DrawNewTargetCollection(Vector2 width) + private static bool DrawNewTargetCollection( Vector2 width ) { var target = Dalamud.Targets.Target; - var player = Penumbra.Actors.FromObject( target, false, true ); + var player = Penumbra.Actors.FromObject( target, false, true, true ); var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); var tt = result switch { @@ -299,7 +299,7 @@ public partial class ConfigWindow }; if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid ) ) { - Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.Individuals.Add( Penumbra.CollectionManager.Individuals.GetGroup( player ), Penumbra.CollectionManager.Default ); return true; } diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 39df2f3e..47abe7ec 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -215,7 +215,7 @@ public partial class ConfigWindow { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); - var identifier = Penumbra.Actors.FromObject( obj, false, true ); + var identifier = Penumbra.Actors.FromObject( obj, false, true, false ); ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn( id ); From 45075d5b2763953f6706f52b31b95498382d4214 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Mar 2023 14:53:12 +0100 Subject: [PATCH 0785/2451] Fix migration of old mods not working anymore --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index ce41e2b7..a5f5fd3e 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit ce41e2b7da65edb25b5308ae41e4ca7d74d75e38 +Subproject commit a5f5fd3e84be2f829c0e7f779fde1ed34ece3042 From 6ee6e4a4ba8bb77f2462a7c980333dddd12ca820 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Mar 2023 15:16:14 +0100 Subject: [PATCH 0786/2451] Fix assign current player and assign current target buttons not triggering events. --- Penumbra.String | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Penumbra.String b/Penumbra.String index a5f5fd3e..5ec8eba3 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit a5f5fd3e84be2f829c0e7f779fde1ed34ece3042 +Subproject commit 5ec8eba3277872682b027796fb7e6d534330571e diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 7f8e0f9a..528329cd 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -278,7 +278,7 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( "Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid ) ) { - Penumbra.CollectionManager.Individuals.Add( new[] { player }, Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( player ); return true; } @@ -287,19 +287,18 @@ public partial class ConfigWindow private static bool DrawNewTargetCollection( Vector2 width ) { - var target = Dalamud.Targets.Target; - var player = Penumbra.Actors.FromObject( target, false, true, true ); - var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); + var target = Penumbra.Actors.FromObject( Dalamud.Targets.Target, false, true, true ); + var result = Penumbra.CollectionManager.Individuals.CanAdd( target ); var tt = result switch { - IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", + IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, IndividualCollections.AddResult.Invalid => "No valid character in target detected.", _ => string.Empty, }; if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid ) ) { - Penumbra.CollectionManager.Individuals.Add( Penumbra.CollectionManager.Individuals.GetGroup( player ), Penumbra.CollectionManager.Default ); + Penumbra.CollectionManager.CreateIndividualCollection( Penumbra.CollectionManager.Individuals.GetGroup( target ) ); return true; } From c2bb1407a9fbbea62026c90cb58787eb4e1f8d8f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Mar 2023 15:16:45 +0100 Subject: [PATCH 0787/2451] Add option to make successful chat commands silent. --- Penumbra/CommandHandler.cs | 58 +++++++++++++------ Penumbra/Configuration.cs | 1 + .../UI/ConfigWindow.SettingsTab.General.cs | 12 +++- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 0af75477..4f53a7b6 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; @@ -139,7 +140,8 @@ public class CommandHandler : IDisposable Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) .BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) + Dalamud.Chat.Print( new SeStringBuilder() + .AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) .BuiltString ); return true; } @@ -159,7 +161,7 @@ public class CommandHandler : IDisposable private bool Reload( string _ ) { _modManager.DiscoverMods(); - Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); + Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); return true; } @@ -185,9 +187,7 @@ public class CommandHandler : IDisposable return false; } - Dalamud.Chat.Print( value - ? "Debug mode enabled." - : "Debug mode disabled." ); + Print( value ? "Debug mode enabled." : "Debug mode disabled." ); _config.DebugMode = value; _config.Save(); @@ -200,13 +200,13 @@ public class CommandHandler : IDisposable if( value == _config.EnableMods ) { - Dalamud.Chat.Print( value + Print( value ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); return false; } - Dalamud.Chat.Print( value + Print( value ? "Your mods have been enabled." : "Your mods have been disabled." ); return _penumbra.SetEnabled( value ); @@ -222,12 +222,12 @@ public class CommandHandler : IDisposable if( value ) { - Dalamud.Chat.Print( "Penumbra UI locked in place." ); + Print( "Penumbra UI locked in place." ); _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; } else { - Dalamud.Chat.Print( "Penumbra UI unlocked." ); + Print( "Penumbra UI unlocked." ); _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); } @@ -251,7 +251,7 @@ public class CommandHandler : IDisposable Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) - .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); + .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); @@ -358,12 +358,12 @@ public class CommandHandler : IDisposable return false; } - Dalamud.Chat.Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); return true; } _collectionManager.SetCollection( collection!, type, individualIndex ); - Dalamud.Chat.Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); return true; } @@ -462,7 +462,7 @@ public class CommandHandler : IDisposable if( !changes ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "No mod states were changed in collection " ).AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); + Print( () => new SeStringBuilder().AddText( "No mod states were changed in collection " ).AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); } return true; @@ -528,7 +528,7 @@ public class CommandHandler : IDisposable case 0: if( collection.SetModState( mod.Index, true ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + Print( () => new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) .AddYellow( collection.Name, true ) .AddText( "." ).BuiltString ); return true; @@ -538,7 +538,7 @@ public class CommandHandler : IDisposable case 1: if( collection.SetModState( mod.Index, false ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + Print( () => new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) .AddYellow( collection.Name, true ) .AddText( "." ).BuiltString ); return true; @@ -549,7 +549,7 @@ public class CommandHandler : IDisposable var setting = !( settings?.Enabled ?? false ); if( collection.SetModState( mod.Index, setting ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + Print( () => new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) .AddYellow( collection.Name, true ) .AddText( "." ).BuiltString ); return true; @@ -559,7 +559,7 @@ public class CommandHandler : IDisposable case 3: if( collection.SetModInheritance( mod.Index, true ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + Print( () => new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) .AddText( " to inherit." ).BuiltString ); return true; } @@ -569,4 +569,28 @@ public class CommandHandler : IDisposable return false; } + + private static void Print( string text ) + { + if( Penumbra.Config.PrintSuccessfulCommandsToChat ) + { + Dalamud.Chat.Print( text ); + } + } + + private static void Print( DefaultInterpolatedStringHandler text ) + { + if( Penumbra.Config.PrintSuccessfulCommandsToChat ) + { + Dalamud.Chat.Print( text.ToStringAndClear() ); + } + } + + private static void Print( Func text ) + { + if( Penumbra.Config.PrintSuccessfulCommandsToChat ) + { + Dalamud.Chat.Print( text() ); + } + } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 6fd70b03..878b4a3b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -72,6 +72,7 @@ public partial class Configuration : IPluginConfiguration public string DefaultImportFolder { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; public bool AutoDeduplicateOnImport { get; set; } = true; public bool EnableHttpApi { get; set; } = true; diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 62acbba7..d8f5808d 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -28,17 +28,21 @@ public partial class ConfigWindow } private static int _singleGroupRadioMax = int.MaxValue; + private void DrawSingleSelectRadioMax() { - if ( _singleGroupRadioMax == int.MaxValue) + if( _singleGroupRadioMax == int.MaxValue ) + { _singleGroupRadioMax = Penumbra.Config.SingleGroupRadioMax; + } + ImGui.SetNextItemWidth( _window._inputTextWidth.X ); if( ImGui.DragInt( "##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1 ) ) { _singleGroupRadioMax = Math.Max( 1, _singleGroupRadioMax ); } - if (ImGui.IsItemDeactivated()) + if( ImGui.IsItemDeactivated() ) { if( _singleGroupRadioMax != Penumbra.Config.SingleGroupRadioMax ) { @@ -48,6 +52,7 @@ public partial class ConfigWindow _singleGroupRadioMax = int.MaxValue; } + ImGuiUtil.LabeledHelpMarker( "Upper Limit for Single-Selection Group Radio Buttons", "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons." ); @@ -89,6 +94,9 @@ public partial class ConfigWindow } ); ImGui.Dummy( _window._defaultSpace ); + Checkbox( "Print Chat Command Success Messages to Chat", + "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", + Penumbra.Config.PrintSuccessfulCommandsToChat, v => Penumbra.Config.PrintSuccessfulCommandsToChat = v ); Checkbox( "Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); ImGui.Dummy( _window._defaultSpace ); From 6159f1e9989698a76078d1d5ebd7143f063644f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 5 Mar 2023 15:19:01 +0100 Subject: [PATCH 0788/2451] Add changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 32bf5da7..bbe0e9ad 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -36,9 +36,16 @@ public partial class ConfigWindow Add6_5_0( ret ); Add6_5_2( ret ); Add6_6_0( ret ); + Add6_6_1( ret ); return ret; } + private static void Add6_6_1( Changelog log ) + => log.NextVersion( "Version 0.6.6.1" ) + .RegisterEntry( "Added an option to make successful chat commands not print their success confirmations to chat." ) + .RegisterEntry( "Fixed an issue with migration of old mods not working anymore (fixes Material UI problems)." ) + .RegisterEntry( "Fixed some issues with using the Assign Current Player and Assign Current Target buttons." ); + private static void Add6_6_0( Changelog log ) => log.NextVersion( "Version 0.6.6.0" ) .RegisterEntry( "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively." ) From 4a8f0aac6130eb76fc03c3e083f12293dfff5201 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 5 Mar 2023 14:22:23 +0000 Subject: [PATCH 0789/2451] [CI] Updating repo.json for refs/tags/0.6.6.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index addc299a..1ef1c4e6 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.0", - "TestingAssemblyVersion": "0.6.6.0", + "AssemblyVersion": "0.6.6.1", + "TestingAssemblyVersion": "0.6.6.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6a54d246349eca8e9902637db05af54dd40137d9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 8 Mar 2023 12:45:20 +0100 Subject: [PATCH 0790/2451] Maybe fix UTF8 issues. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 5ec8eba3..2f999713 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 5ec8eba3277872682b027796fb7e6d534330571e +Subproject commit 2f999713c5b692fead3fb28c39002d1cd82c9261 From 8d38f73f52b36fd1128a3f5ab4453f2fcce477af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 8 Mar 2023 12:48:59 +0100 Subject: [PATCH 0791/2451] Use TabBar, add OpenMainWindow and CloseMainWindow to API --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 32 ++ Penumbra/Api/PenumbraApi.cs | 32 ++ Penumbra/Api/PenumbraIpcProviders.cs | 14 +- Penumbra/Penumbra.cs | 44 +-- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 109 +++--- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 17 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 22 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 16 +- Penumbra/UI/ConfigWindow.ModPanel.cs | 79 ++++ Penumbra/UI/ConfigWindow.ModsTab.cs | 353 ++++++++---------- Penumbra/UI/ConfigWindow.ResourceTab.cs | 23 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 14 +- Penumbra/UI/ConfigWindow.cs | 63 ++-- .../UI/ResourceWatcher/ResourceWatcher.cs | 10 +- 16 files changed, 463 insertions(+), 369 deletions(-) create mode 100644 Penumbra/UI/ConfigWindow.ModPanel.cs diff --git a/OtterGui b/OtterGui index 95a26e94..9ee5721e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 95a26e944d550a3e77150667af00c23ef307b672 +Subproject commit 9ee5721e317457e98f2b8a4500776770f57d204e diff --git a/Penumbra.Api b/Penumbra.Api index 1f62ae97..a2b680a5 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1f62ae970e02e48f686a41a2cecdb79e0af87994 +Subproject commit a2b680a5991d9287c2dcda7cfa54183c37384fd0 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 90a17687..aa0392a0 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -312,6 +312,9 @@ public class IpcTester : IDisposable private bool _subscribedToClick = false; private string _lastClicked = string.Empty; private string _lastHovered = string.Empty; + private TabType _selectTab = TabType.None; + private string _modName = string.Empty; + private PenumbraApiEc _ec = PenumbraApiEc.Success; public Ui( DalamudPluginInterface pi ) { @@ -330,6 +333,21 @@ public class IpcTester : IDisposable return; } + using( var combo = ImRaii.Combo( "Tab to Open at", _selectTab.ToString() ) ) + { + if( combo ) + { + foreach( var val in Enum.GetValues< TabType >() ) + { + if( ImGui.Selectable( val.ToString(), _selectTab == val ) ) + { + _selectTab = val; + } + } + } + } + + ImGui.InputTextWithHint( "##openMod", "Mod to Open at...", ref _modName, 256 ); using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); if( !table ) { @@ -370,6 +388,20 @@ public class IpcTester : IDisposable ImGui.SameLine(); ImGui.TextUnformatted( _lastClicked ); + DrawIntro( Ipc.OpenMainWindow.Label, "Open Mod Window" ); + if( ImGui.Button( "Open##window" ) ) + { + _ec = Ipc.OpenMainWindow.Subscriber( _pi ).Invoke( _selectTab, _modName, _modName ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _ec.ToString() ); + + DrawIntro( Ipc.CloseMainWindow.Label, "Close Mod Window" ); + if( ImGui.Button( "Close##window" ) ) + { + Ipc.CloseMainWindow.Subscriber( _pi ).Invoke(); + } } private void UpdateLastDrawnMod( string name ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0df89f55..fc5b2006 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -171,6 +171,38 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemHover? ChangedItemTooltip; public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; + public PenumbraApiEc OpenMainWindow( TabType tab, string modDirectory, string modName ) + { + CheckInitialized(); + + _penumbra!.ConfigWindow.IsOpen = true; + + if( !Enum.IsDefined( tab ) ) + return PenumbraApiEc.InvalidArgument; + + if( tab != TabType.None ) + _penumbra!.ConfigWindow.SelectTab = tab; + + if( tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0) ) + { + if( Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + _penumbra!.ConfigWindow.SelectMod( mod ); + } + else + { + return PenumbraApiEc.ModMissing; + } + } + return PenumbraApiEc.Success; + } + + public void CloseMainWindow() + { + CheckInitialized(); + _penumbra!.ConfigWindow.IsOpen = false; + } + public void RedrawObject( int tableIndex, RedrawType setting ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 0425d721..90d316d0 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -29,10 +29,12 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider< string, bool > ModDirectoryChanged; // UI - internal readonly EventProvider< string > PreSettingsDraw; - internal readonly EventProvider< string > PostSettingsDraw; - internal readonly EventProvider< ChangedItemType, uint > ChangedItemTooltip; - internal readonly EventProvider< MouseButton, ChangedItemType, uint > ChangedItemClick; + internal readonly EventProvider< string > PreSettingsDraw; + internal readonly EventProvider< string > PostSettingsDraw; + internal readonly EventProvider< ChangedItemType, uint > ChangedItemTooltip; + internal readonly EventProvider< MouseButton, ChangedItemType, uint > ChangedItemClick; + internal readonly FuncProvider< TabType, string, string, PenumbraApiEc > OpenMainWindow; + internal readonly ActionProvider CloseMainWindow; // Redrawing internal readonly ActionProvider< RedrawType > RedrawAll; @@ -131,6 +133,8 @@ public class PenumbraIpcProviders : IDisposable PostSettingsDraw = Ipc.PostSettingsDraw.Provider( pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a ); ChangedItemTooltip = Ipc.ChangedItemTooltip.Provider( pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip ); ChangedItemClick = Ipc.ChangedItemClick.Provider( pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick ); + OpenMainWindow = Ipc.OpenMainWindow.Provider( pi, Api.OpenMainWindow ); + CloseMainWindow = Ipc.CloseMainWindow.Provider( pi, Api.CloseMainWindow ); // Redrawing RedrawAll = Ipc.RedrawAll.Provider( pi, Api.RedrawAll ); @@ -241,6 +245,8 @@ public class PenumbraIpcProviders : IDisposable PostSettingsDraw.Dispose(); ChangedItemTooltip.Dispose(); ChangedItemClick.Dispose(); + OpenMainWindow.Dispose(); + CloseMainWindow.Dispose(); // Redrawing RedrawAll.Dispose(); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6a3c2ae4..e440d9e4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -70,23 +70,23 @@ public class Penumbra : IDalamudPlugin public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; - public static StartTimeTracker< StartTimeType > StartTimer = new(); + public static readonly StartTimeTracker< StartTimeType > StartTimer = new(); public static readonly List< Exception > ImcExceptions = new(); - public readonly ResourceLogger ResourceLogger; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; - private readonly ConfigWindow _configWindow; - private readonly LaunchButton _launchButton; - private readonly WindowSystem _windowSystem; - private readonly Changelog _changelog; - private readonly CommandHandler _commandHandler; - private readonly ResourceWatcher _resourceWatcher; + public readonly ResourceLogger ResourceLogger; + public readonly PathResolver PathResolver; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystem ModFileSystem; + public readonly PenumbraApi Api; + public readonly HttpApi HttpApi; + public readonly PenumbraIpcProviders IpcProviders; + internal readonly ConfigWindow ConfigWindow; + private readonly LaunchButton _launchButton; + private readonly WindowSystem _windowSystem; + private readonly Changelog _changelog; + private readonly CommandHandler _commandHandler; + private readonly ResourceWatcher _resourceWatcher; public Penumbra( DalamudPluginInterface pluginInterface ) { @@ -140,8 +140,8 @@ public class Penumbra : IDalamudPlugin ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem, out _changelog ); - _commandHandler = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, _configWindow, ModManager, CollectionManager, Actors ); + SetupInterface( out ConfigWindow, out _launchButton, out _windowSystem, out _changelog ); + _commandHandler = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, ConfigWindow, ModManager, CollectionManager, Actors ); if( Config.EnableMods ) { @@ -152,7 +152,7 @@ public class Penumbra : IDalamudPlugin if( Config.DebugMode ) { ResourceLoader.EnableDebug(); - _configWindow.IsOpen = true; + ConfigWindow.IsOpen = true; } using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) @@ -198,10 +198,10 @@ public class Penumbra : IDalamudPlugin { using var tInterface = StartTimer.Measure( StartTimeType.Interface ); cfg = new ConfigWindow( this, _resourceWatcher ); - btn = new LaunchButton( _configWindow ); + btn = new LaunchButton( ConfigWindow ); system = new WindowSystem( Name ); changelog = ConfigWindow.CreateChangelog(); - system.AddWindow( _configWindow ); + system.AddWindow( ConfigWindow ); system.AddWindow( cfg.ModEditPopup ); system.AddWindow( changelog ); Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; @@ -215,10 +215,10 @@ public class Penumbra : IDalamudPlugin } _launchButton?.Dispose(); - if( _configWindow != null ) + if( ConfigWindow != null ) { - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; - _configWindow.Dispose(); + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; + ConfigWindow.Dispose(); } } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 245dc63b..c55fd6d4 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -3,14 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.GameData.Structs; +using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -18,23 +15,71 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private LowerString _changedItemFilter = LowerString.Empty; - private LowerString _changedItemModFilter = LowerString.Empty; - // Draw a simple clipped table containing all changed items. - private void DrawChangedItemTab() + public class ChangedItemsTab : ITab { + private readonly ConfigWindow _config; + + public ChangedItemsTab( ConfigWindow config ) + => _config = config; + + public ReadOnlySpan Label + => "Changed Items"u8; + + private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemModFilter = LowerString.Empty; + + public void DrawContent() + { + // Draw filters. + var varWidth = ImGui.GetContentRegionAvail().X + - 400 * ImGuiHelpers.GlobalScale + - ImGui.GetStyle().ItemSpacing.X; + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( varWidth ); + LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); + + using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); + if( !child ) + { + return; + } + + // Draw table of changed items. + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips( height ); + using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !list ) + { + return; + } + + const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; + ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "mods", flags, varWidth - 120 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "id", flags, 120 * ImGuiHelpers.GlobalScale ); + + var items = Penumbra.CollectionManager.Current.ChangedItems; + var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty + ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) + : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); + ImGuiClip.DrawEndDummy( rest, height ); + } + + // Functions in here for less pollution. - bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) + private bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) => ( _changedItemFilter.IsEmpty || ChangedItemName( item.Key, item.Value.Item2 ) .Contains( _changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) + private void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) { ImGui.TableNextColumn(); - DrawChangedItem( item.Key, item.Value.Item2, false ); + _config.DrawChangedItem( item.Key, item.Value.Item2, false ); ImGui.TableNextColumn(); if( item.Value.Item1.Count > 0 ) { @@ -52,47 +97,5 @@ public partial class ConfigWindow ImGuiUtil.RightAlign( text ); } } - - using var tab = ImRaii.TabItem( "Changed Items" ); - if( !tab ) - { - return; - } - - // Draw filters. - var varWidth = ImGui.GetContentRegionAvail().X - - 400 * ImGuiHelpers.GlobalScale - - ImGui.GetStyle().ItemSpacing.X; - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( varWidth ); - LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); - - using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); - if( !child ) - { - return; - } - - // Draw table of changed items. - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !list ) - { - return; - } - - const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "mods", flags, varWidth - 120 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "id", flags, 120 * ImGuiHelpers.GlobalScale ); - - var items = Penumbra.CollectionManager.Current.ChangedItems; - var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty - ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) - : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); - ImGuiClip.DrawEndDummy( rest, height ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index f356f983..42417134 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -14,7 +14,7 @@ namespace Penumbra.UI; public partial class ConfigWindow { // Encapsulate for less pollution. - private partial class CollectionsTab : IDisposable + private partial class CollectionsTab : IDisposable, ITab { private readonly ConfigWindow _window; @@ -25,18 +25,17 @@ public partial class ConfigWindow Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; } + public ReadOnlySpan Label + => "Collections"u8; + public void Dispose() => Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; - public void Draw() - { - using var tab = ImRaii.TabItem( "Collections" ); - OpenTutorial( BasicTutorialSteps.Collections ); - if( !tab ) - { - return; - } + public void DrawHeader() + => OpenTutorial( BasicTutorialSteps.Collections ); + public void DrawContent() + { using var child = ImRaii.Child( "##collections", -Vector2.One ); if( child ) { diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 47abe7ec..6ca18204 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; +using OtterGui.Widgets; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; @@ -24,32 +25,27 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private class DebugTab + private class DebugTab : ITab { private readonly ConfigWindow _window; public DebugTab( ConfigWindow window ) => _window = window; + public ReadOnlySpan Label + => "Debug"u8; + + public bool IsVisible + => Penumbra.Config.DebugMode; + #if DEBUG private const string DebugVersionString = "(Debug)"; #else private const string DebugVersionString = "(Release)"; #endif - public void Draw() + public void DrawContent() { - if( !Penumbra.Config.DebugMode ) - { - return; - } - - using var tab = TabItem( "Debug" ); - if( !tab ) - { - return; - } - using var child = Child( "##DebugTab", -Vector2.One ); if( !child ) { diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index a27307d9..a90df004 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -6,6 +7,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -15,17 +17,13 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private class EffectiveTab + private class EffectiveTab : ITab { - // Draw the effective tab if ShowAdvanced is on. - public void Draw() - { - using var tab = ImRaii.TabItem( "Effective Changes" ); - if( !tab ) - { - return; - } + public ReadOnlySpan Label + => "Effective Changes"u8; + public void DrawContent() + { SetupEffectiveSizes(); DrawFilters(); using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.cs b/Penumbra/UI/ConfigWindow.ModPanel.cs new file mode 100644 index 00000000..d9b1f7ac --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModPanel.cs @@ -0,0 +1,79 @@ +using System; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + + // The basic setup for the mod panel. + // Details are in other files. + private partial class ModPanel : IDisposable + { + private readonly ConfigWindow _window; + + private bool _valid; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + private readonly TagButtons _localTags = new(); + + public ModPanel( ConfigWindow window ) + => _window = window; + + public void Dispose() + { + _nameFont.Dispose(); + } + + public void Draw( ModFileSystemSelector selector ) + { + Init( selector ); + if( !_valid ) + { + return; + } + + DrawModHeader(); + DrawTabBar(); + } + + private void Init( ModFileSystemSelector selector ) + { + _valid = selector.Selected != null; + if( !_valid ) + { + return; + } + + _leaf = selector.SelectedLeaf!; + _mod = selector.Selected!; + UpdateSettingsData( selector ); + UpdateModData(); + } + + public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) + { + if( old == mod ) + { + return; + } + + if( mod == null ) + { + _window.ModEditPopup.IsOpen = false; + } + else if( _window.ModEditPopup.IsOpen ) + { + _window.ModEditPopup.ChangeMod( mod ); + } + + _currentPriority = null; + MoveDirectory.Reset(); + OptionTable.Reset(); + Input.Reset(); + AddOptionGroup.Reset(); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index b520dd91..079db72d 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -2,7 +2,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; -using Penumbra.Mods; using Penumbra.UI.Classes; using System; using System.Linq; @@ -15,249 +14,191 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private void DrawModsTab() + private class ModsTab : ITab { - if( !Penumbra.ModManager.Valid ) + private readonly ModFileSystemSelector _selector; + private readonly ModPanel _panel; + private readonly Penumbra _penumbra; + + public ModsTab(ModFileSystemSelector selector, ModPanel panel, Penumbra penumbra) { - return; + _selector = selector; + _panel = panel; + _penumbra = penumbra; } - try + public bool IsVisible + => Penumbra.ModManager.Valid; + + public ReadOnlySpan Label + => "Mods"u8; + + public void DrawHeader() + => OpenTutorial( BasicTutorialSteps.Mods ); + + public void DrawContent() { - using var tab = ImRaii.TabItem( "Mods" ); - OpenTutorial( BasicTutorialSteps.Mods ); - if( !tab ) + try { - return; - } - - _selector.Draw( GetModSelectorSize() ); - ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawHeaderLine(); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - - using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight() ), - true, ImGuiWindowFlags.HorizontalScrollbar ) ) - { - style.Pop(); - if( child ) - { - _modPanel.Draw( _selector ); - } - - style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - } - - style.Push( ImGuiStyleVar.FrameRounding, 0 ); - DrawRedrawLine(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Exception thrown during ModPanel Render:\n{e}" ); - Penumbra.Log.Error( $"{Penumbra.ModManager.Count} Mods\n" - + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" - + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" - + $"{_selector.SortMode.Name} Sort Mode\n" - + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" - + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); - } - } - - private void DrawRedrawLine() - { - if( Penumbra.Config.HideRedrawBar ) - { - SkipTutorial( BasicTutorialSteps.Redrawing ); - return; - } - - var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); - var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); - using( var _ = ImRaii.Group() ) - { - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); + _selector.Draw( GetModSelectorSize() ); ImGui.SameLine(); - } + using var group = ImRaii.Group(); + DrawHeaderLine(); - ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); - } + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - var hovered = ImGui.IsItemHovered(); - OpenTutorial( BasicTutorialSteps.Redrawing ); - if( hovered ) - { - ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); - } - - void DrawButton( Vector2 size, string label, string lower ) - { - if( ImGui.Button( label, size ) ) - { - if( lower.Length > 0 ) + using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight() ), + true, ImGuiWindowFlags.HorizontalScrollbar ) ) { - _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + style.Pop(); + if( child ) + { + _panel.Draw( _selector ); + } + + style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); } - else + + style.Push( ImGuiStyleVar.FrameRounding, 0 ); + DrawRedrawLine(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Exception thrown during ModPanel Render:\n{e}" ); + Penumbra.Log.Error( $"{Penumbra.ModManager.Count} Mods\n" + + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" + + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + + $"{_selector.SortMode.Name} Sort Mode\n" + + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" + + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); + } + } + + private void DrawRedrawLine() + { + if( Penumbra.Config.HideRedrawBar ) + { + SkipTutorial( BasicTutorialSteps.Redrawing ); + return; + } + + var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); + var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); + using( var _ = ImRaii.Group() ) + { + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) { - _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); + ImGui.SameLine(); } + + ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); } - ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); - } + var hovered = ImGui.IsItemHovered(); + OpenTutorial( BasicTutorialSteps.Redrawing ); + if( hovered ) + { + ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); + } - using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); - ImGui.SameLine(); - var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; - DrawButton( buttonWidth, "All", string.Empty ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Self", "self" ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Target", "target" ); - ImGui.SameLine(); - DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); - } + void DrawButton( Vector2 size, string label, string lower ) + { + if( ImGui.Button( label, size ) ) + { + if( lower.Length > 0 ) + { + _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + } + else + { + _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } - // Draw the header line that can quick switch between collections. - private void DrawHeaderLine() - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); + ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); + } - using( var _ = ImRaii.Group() ) - { - DrawDefaultCollectionButton( 3 * buttonSize ); + using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); ImGui.SameLine(); - DrawInheritedCollectionButton( 3 * buttonSize ); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; + DrawButton( buttonWidth, "All", string.Empty ); ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false ); + DrawButton( buttonWidth, "Self", "self" ); + ImGui.SameLine(); + DrawButton( buttonWidth, "Target", "target" ); + ImGui.SameLine(); + DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); } - OpenTutorial( BasicTutorialSteps.CollectionSelectors ); - - if( !Penumbra.CollectionManager.CurrentCollectionInUse ) + // Draw the header line that can quick switch between collections. + private void DrawHeaderLine() { - ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); - } - } + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - private static void DrawDefaultCollectionButton( Vector2 width ) - { - var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})"; - var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; - var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; - var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}." - : isEmpty ? $"The {DefaultCollection} is configured to be empty." - : $"Set the {SelectedCollection} to the configured {DefaultCollection}."; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) - { - Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); - } - } - - private void DrawInheritedCollectionButton( Vector2 width ) - { - var noModSelected = _selector.Selected == null; - var collection = _selector.SelectedSettingCollection; - var modInherited = collection != Penumbra.CollectionManager.Current; - var (name, tt) = ( noModSelected, modInherited ) switch - { - (true, _) => ( "Inherited Collection", "No mod selected." ), - (false, true) => ( $"Inherited Collection ({collection.Name})", - "Set the current collection to the collection the selected mod inherits its settings from." ), - (false, false) => ( "Not Inherited", "The selected mod does not inherit its settings." ), - }; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) - { - Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); - } - } - - // Get the correct size for the mod selector based on current config. - private static float GetModSelectorSize() - { - var absoluteSize = Math.Clamp( Penumbra.Config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min( Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100 ) ); - var relativeSize = Penumbra.Config.ScaleModSelector - ? Math.Clamp( Penumbra.Config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize ) - : 0; - return !Penumbra.Config.ScaleModSelector - ? absoluteSize - : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); - } - - // The basic setup for the mod panel. - // Details are in other files. - private partial class ModPanel : IDisposable - { - private readonly ConfigWindow _window; - - private bool _valid; - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; - private readonly TagButtons _localTags = new(); - - public ModPanel( ConfigWindow window ) - => _window = window; - - public void Dispose() - { - _nameFont.Dispose(); - } - - public void Draw( ModFileSystemSelector selector ) - { - Init( selector ); - if( !_valid ) + using( var _ = ImRaii.Group() ) { - return; + DrawDefaultCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawInheritedCollectionButton( 3 * buttonSize ); + ImGui.SameLine(); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false ); } - DrawModHeader(); - DrawTabBar(); + OpenTutorial( BasicTutorialSteps.CollectionSelectors ); + + if( !Penumbra.CollectionManager.CurrentCollectionInUse ) + { + ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); + } } - private void Init( ModFileSystemSelector selector ) + private static void DrawDefaultCollectionButton( Vector2 width ) { - _valid = selector.Selected != null; - if( !_valid ) + var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})"; + var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; + var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; + var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}." + : isEmpty ? $"The {DefaultCollection} is configured to be empty." + : $"Set the {SelectedCollection} to the configured {DefaultCollection}."; + if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) { - return; + Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); } - - _leaf = selector.SelectedLeaf!; - _mod = selector.Selected!; - UpdateSettingsData( selector ); - UpdateModData(); } - public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) + private void DrawInheritedCollectionButton( Vector2 width ) { - if( old == mod ) + var noModSelected = _selector.Selected == null; + var collection = _selector.SelectedSettingCollection; + var modInherited = collection != Penumbra.CollectionManager.Current; + var (name, tt) = (noModSelected, modInherited) switch { - return; + (true, _ ) => ("Inherited Collection", "No mod selected."), + (false, true ) => ($"Inherited Collection ({collection.Name})", + "Set the current collection to the collection the selected mod inherits its settings from."), + (false, false ) => ("Not Inherited", "The selected mod does not inherit its settings."), + }; + if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) + { + Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); } + } - if( mod == null ) - { - _window.ModEditPopup.IsOpen = false; - } - else if( _window.ModEditPopup.IsOpen ) - { - _window.ModEditPopup.ChangeMod( mod ); - } - - _currentPriority = null; - MoveDirectory.Reset(); - OptionTable.Reset(); - Input.Reset(); - AddOptionGroup.Reset(); + // Get the correct size for the mod selector based on current config. + private static float GetModSelectorSize() + { + var absoluteSize = Math.Clamp( Penumbra.Config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, + Math.Min( Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100 ) ); + var relativeSize = Penumbra.Config.ScaleModSelector + ? Math.Clamp( Penumbra.Config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize ) + : 0; + return !Penumbra.Config.ScaleModSelector + ? absoluteSize + : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 56eb19e8..46a322b5 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Interop.Loader; using Penumbra.String.Classes; @@ -16,12 +17,13 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private class ResourceTab + private class ResourceTab : ITab { - private readonly ConfigWindow _window; + public ReadOnlySpan Label + => "Resource Manager"u8; - public ResourceTab( ConfigWindow window ) - => _window = window; + public bool IsVisible + => Penumbra.Config.DebugMode; private float _hashColumnWidth; private float _pathColumnWidth; @@ -29,19 +31,8 @@ public partial class ConfigWindow private string _resourceManagerFilter = string.Empty; // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. - public void Draw() + public void DrawContent() { - if( !Penumbra.Config.DebugMode ) - { - return; - } - - using var tab = ImRaii.TabItem( "Resource Manager" ); - if( !tab ) - { - return; - } - // Filter for resources containing the input string. ImGui.SetNextItemWidth( -1 ); ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 8857a21b..9ac59622 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -17,26 +17,26 @@ namespace Penumbra.UI; public partial class ConfigWindow { - private partial class SettingsTab + private partial class SettingsTab : ITab { public const int RootDirectoryMaxLength = 64; private readonly ConfigWindow _window; + public ReadOnlySpan Label + => "Settings"u8; public SettingsTab( ConfigWindow window ) => _window = window; - public void Draw() + public void DrawHeader() { - using var tab = ImRaii.TabItem( "Settings" ); OpenTutorial( BasicTutorialSteps.Fin ); OpenTutorial( BasicTutorialSteps.Faq1 ); OpenTutorial( BasicTutorialSteps.Faq2 ); OpenTutorial( BasicTutorialSteps.Faq3 ); - if( !tab ) - { - return; - } + } + public void DrawContent() + { using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); if( !child ) { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 6aab4f88..b16252c5 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -5,6 +5,9 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Mods; using Penumbra.UI.Classes; using Penumbra.Util; @@ -13,16 +16,23 @@ namespace Penumbra.UI; public sealed partial class ConfigWindow : Window, IDisposable { private readonly Penumbra _penumbra; - private readonly SettingsTab _settingsTab; private readonly ModFileSystemSelector _selector; private readonly ModPanel _modPanel; - private readonly CollectionsTab _collectionsTab; - private readonly EffectiveTab _effectiveTab; - private readonly DebugTab _debugTab; - private readonly ResourceTab _resourceTab; - private readonly ResourceWatcher _resourceWatcher; public readonly ModEditWindow ModEditPopup = new(); + private readonly SettingsTab _settingsTab; + private readonly CollectionsTab _collectionsTab; + private readonly ModsTab _modsTab; + private readonly ChangedItemsTab _changedItemsTab; + private readonly EffectiveTab _effectiveTab; + private readonly DebugTab _debugTab; + private readonly ResourceTab _resourceTab; + private readonly ResourceWatcher _resourceWatcher; + + public TabType SelectTab = TabType.None; + public void SelectMod( Mod mod ) + => _selector.SelectByValue( mod ); + public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) : base( GetLabel() ) { @@ -32,11 +42,13 @@ public sealed partial class ConfigWindow : Window, IDisposable _settingsTab = new SettingsTab( this ); _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _modPanel = new ModPanel( this ); + _modsTab = new ModsTab( _selector, _modPanel, _penumbra ); _selector.SelectionChanged += _modPanel.OnSelectionChange; _collectionsTab = new CollectionsTab( this ); + _changedItemsTab = new ChangedItemsTab( this ); _effectiveTab = new EffectiveTab(); _debugTab = new DebugTab( this ); - _resourceTab = new ResourceTab( this ); + _resourceTab = new ResourceTab(); if( Penumbra.Config.FixMainWindow ) { Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; @@ -54,6 +66,20 @@ public sealed partial class ConfigWindow : Window, IDisposable UpdateTutorialStep(); } + private ReadOnlySpan< byte > ToLabel( TabType type ) + => type switch + { + TabType.Settings => _settingsTab.Label, + TabType.Mods => _modsTab.Label, + TabType.Collections => _collectionsTab.Label, + TabType.ChangedItems => _changedItemsTab.Label, + TabType.EffectiveChanges => _effectiveTab.Label, + TabType.ResourceWatcher => _resourceWatcher.Label, + TabType.Debug => _debugTab.Label, + TabType.ResourceManager => _resourceTab.Label, + _ => ReadOnlySpan< byte >.Empty, + }; + public override void Draw() { using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow ); @@ -92,16 +118,12 @@ public sealed partial class ConfigWindow : Window, IDisposable } else { - using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); SetupSizes(); - _settingsTab.Draw(); - DrawModsTab(); - _collectionsTab.Draw(); - DrawChangedItemTab(); - _effectiveTab.Draw(); - _debugTab.Draw(); - _resourceTab.Draw(); - DrawResourceWatcher(); + if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab, + _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) ) + { + SelectTab = TabType.None; + } } } catch( Exception e ) @@ -163,13 +185,4 @@ public sealed partial class ConfigWindow : Window, IDisposable _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); _iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); } - - private void DrawResourceWatcher() - { - using var tab = ImRaii.TabItem( "Resource Logger" ); - if (tab) - { - _resourceWatcher.Draw(); - } - } } \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index e180082f..0d344e63 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; @@ -14,9 +15,9 @@ using Penumbra.UI.Classes; namespace Penumbra.UI; -public partial class ResourceWatcher : IDisposable +public partial class ResourceWatcher : IDisposable, ITab { - public const int DefaultMaxEntries = 1024 * 1024; + public const int DefaultMaxEntries = 1024; private readonly ResourceLoader _loader; private readonly List< Record > _records = new(); @@ -59,7 +60,10 @@ public partial class ResourceWatcher : IDisposable _table.Reset(); } - public void Draw() + public ReadOnlySpan Label + => "Resource Logger"u8; + + public void DrawContent() { UpdateRecords(); From 9c6bcb2409561d4e7fb0ed5f3d4a99d47cd4a1ae Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 8 Mar 2023 12:28:29 +0000 Subject: [PATCH 0792/2451] [CI] Updating repo.json for refs/tags/0.6.6.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1ef1c4e6..562c6c31 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.1", - "TestingAssemblyVersion": "0.6.6.1", + "AssemblyVersion": "0.6.6.2", + "TestingAssemblyVersion": "0.6.6.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5c6c96b6c024b9f9dd8906a964aeb5e8fb47b189 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Mar 2023 15:37:25 +0100 Subject: [PATCH 0793/2451] Improve startup performance tracking --- OtterGui | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 1 + Penumbra/Util/PerformanceType.cs | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 9ee5721e..d7867dfa 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9ee5721e317457e98f2b8a4500776770f57d204e +Subproject commit d7867dfa6579d4e69876753e9cde72e13d3372ce diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 46e075c7..8e60cf91 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -38,6 +38,7 @@ public partial class PathResolver : IDisposable public unsafe PathResolver( ResourceLoader loader ) { + using var tApi = Penumbra.StartTimer.Measure( StartTimeType.PathResolver ); SignatureHelper.Initialise( this ); _loader = loader; _animations = new AnimationState( DrawObjects ); diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index 80205f7c..e5941ea8 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,6 +1,4 @@ -using Lumina.Excel.GeneratedSheets; -using OtterGui.Classes; -using Penumbra.GameData; +using System; namespace Penumbra.Util; @@ -15,8 +13,9 @@ public enum StartTimeType Backup, Mods, Collections, - Api, + PathResolver, Interface, + Api, } public enum PerformanceType @@ -63,7 +62,8 @@ public static class TimingExtensions StartTimeType.Collections => "Loading Collections", StartTimeType.Api => "Setting Up API", StartTimeType.Interface => "Setting Up Interface", - _ => $"Unknown {( int )type}", + StartTimeType.PathResolver => "Setting Up Path Resolver", + _ => $"Unknown {(int) type}", }; public static string ToName( this PerformanceType type ) From cd894e415dc1e010d8d027a5ee039a5653f4e2e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 Mar 2023 15:37:43 +0100 Subject: [PATCH 0794/2451] Switch CreateFileW hook to hooking from import table. --- Penumbra/Interop/Loader/CreateFileWHook.cs | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs index 21f40e6b..f55272e8 100644 --- a/Penumbra/Interop/Loader/CreateFileWHook.cs +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -22,12 +22,6 @@ public unsafe class CreateFileWHook : IDisposable private const char Prefix = ( char )( ( byte )'P' | ( ( '?' & 0x00FF ) << 8 ) ); private const int BufferSize = Utf8GamePath.MaxGamePathLength; - [DllImport( "kernel32.dll" )] - private static extern nint LoadLibrary( string dllName ); - - [DllImport( "kernel32.dll" )] - private static extern nint GetProcAddress( nint hModule, string procName ); - private delegate nint CreateFileWDelegate( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ); private readonly Hook< CreateFileWDelegate > _createFileWHook; @@ -36,11 +30,7 @@ public unsafe class CreateFileWHook : IDisposable private readonly ThreadLocal< nint > _fileNameStorage = new(SetupStorage, true); public CreateFileWHook() - { - var userApi = LoadLibrary( "kernel32.dll" ); - var createFileAddress = GetProcAddress( userApi, "CreateFileW" ); - _createFileWHook = Hook< CreateFileWDelegate >.FromAddress( createFileAddress, CreateFileWDetour ); - } + => _createFileWHook = Hook< CreateFileWDelegate >.FromImport( null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour ); /// Long paths in windows need to start with "\\?\", so we keep this static in the pointers. private static nint SetupStorage() @@ -169,4 +159,19 @@ public unsafe class CreateFileWHook : IDisposable fileName = new ReadOnlySpan< byte >( ( void* )address, ( int )length ); return true; } + + // ***** Old method ***** + + //[DllImport( "kernel32.dll" )] + //private static extern nint LoadLibrary( string dllName ); + // + //[DllImport( "kernel32.dll" )] + //private static extern nint GetProcAddress( nint hModule, string procName ); + // + //public CreateFileWHookOld() + //{ + // var userApi = LoadLibrary( "kernel32.dll" ); + // var createFileAddress = GetProcAddress( userApi, "CreateFileW" ); + // _createFileWHook = Hook.FromAddress( createFileAddress, CreateFileWDetour ); + //} } \ No newline at end of file From f03584a057bc1e77a873bc83410f98d5430e1f3c Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 9 Mar 2023 14:41:48 +0000 Subject: [PATCH 0795/2451] [CI] Updating repo.json for refs/tags/0.6.6.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 562c6c31..599faae9 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.2", - "TestingAssemblyVersion": "0.6.6.2", + "AssemblyVersion": "0.6.6.3", + "TestingAssemblyVersion": "0.6.6.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8ce52b7028499f7675bfef295a5eddad75ac1362 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Mar 2023 15:11:55 +0100 Subject: [PATCH 0796/2451] Use OriginalDisposeSafe. --- Penumbra/Interop/Loader/CreateFileWHook.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs index f55272e8..7ba4ba22 100644 --- a/Penumbra/Interop/Loader/CreateFileWHook.cs +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -52,6 +52,7 @@ public unsafe class CreateFileWHook : IDisposable public void Dispose() { + Disable(); _createFileWHook.Dispose(); foreach( var ptr in _fileNameStorage.Values ) { @@ -67,10 +68,10 @@ public unsafe class CreateFileWHook : IDisposable // Use static storage. var ptr = WriteFileName( name ); Penumbra.Log.Verbose( $"Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); - return _createFileWHook.Original( ptr, access, shareMode, security, creation, flags, template ); + return _createFileWHook.OriginalDisposeSafe( ptr, access, shareMode, security, creation, flags, template ); } - - return _createFileWHook.Original( fileName, access, shareMode, security, creation, flags, template ); + + return _createFileWHook.OriginalDisposeSafe( fileName, access, shareMode, security, creation, flags, template ); } From 23c1ee9dc6fd64914b691582ac479d77373c5ae4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Mar 2023 15:16:38 +0100 Subject: [PATCH 0797/2451] Move Validating outside of main class. --- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- Penumbra/Penumbra.cs | 113 ++++------------------- Penumbra/UI/ConfigWindow.cs | 14 +-- Penumbra/ValidityChecker.cs | 87 +++++++++++++++++ 4 files changed, 115 insertions(+), 101 deletions(-) create mode 100644 Penumbra/ValidityChecker.cs diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 406b5671..661a692c 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -83,7 +83,7 @@ public partial class MetaManager } catch( ImcException e ) { - Penumbra.ImcExceptions.Add( e ); + Penumbra.ValidityChecker.ImcExceptions.Add( e ); Penumbra.Log.Error( e.ToString() ); } catch( Exception e ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e440d9e4..524bd68c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -24,7 +24,6 @@ using Penumbra.GameData.Data; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; -using Action = System.Action; using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -33,10 +32,6 @@ namespace Penumbra; public class Penumbra : IDalamudPlugin { - public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; - public const string RepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/master/repo.json"; - public const string TestRepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/test/repo.json"; - public string Name => "Penumbra"; @@ -45,10 +40,6 @@ public class Penumbra : IDalamudPlugin public static readonly string CommitHash = Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; - public static bool DevPenumbraExists; - public static bool IsNotInstalledPenumbra; - public static bool IsValidSourceRepo; - public static Logger Log { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; @@ -67,26 +58,25 @@ public class Penumbra : IDalamudPlugin public static StainManager StainManager { get; private set; } = null!; public static ItemData ItemData { get; private set; } = null!; + public static ValidityChecker ValidityChecker { get; private set; } = null!; public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; public static readonly StartTimeTracker< StartTimeType > StartTimer = new(); - public static readonly List< Exception > ImcExceptions = new(); - - public readonly ResourceLogger ResourceLogger; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; + public readonly ResourceLogger ResourceLogger; + public readonly PathResolver PathResolver; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystem ModFileSystem; + public readonly PenumbraApi Api; + public readonly HttpApi HttpApi; + public readonly PenumbraIpcProviders IpcProviders; internal readonly ConfigWindow ConfigWindow; private readonly LaunchButton _launchButton; private readonly WindowSystem _windowSystem; private readonly Changelog _changelog; private readonly CommandHandler _commandHandler; - private readonly ResourceWatcher _resourceWatcher; + private readonly ResourceWatcher _resourceWatcher; public Penumbra( DalamudPluginInterface pluginInterface ) { @@ -96,11 +86,9 @@ public class Penumbra : IDalamudPlugin { Dalamud.Initialize( pluginInterface ); - Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); - Log = new Logger(); - DevPenumbraExists = CheckDevPluginPenumbra(); - IsNotInstalledPenumbra = CheckIsNotInstalled(); - IsValidSourceRepo = CheckSourceRepo(); + Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); + Log = new Logger(); + ValidityChecker = new ValidityChecker( Dalamud.PluginInterface ); GameEvents = new GameEventManager(); StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ) ); @@ -168,17 +156,10 @@ public class Penumbra : IDalamudPlugin SubscribeItemLinks(); } - if( ImcExceptions.Count > 0 ) - { - Log.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); - } - else - { - Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); - } - Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + ValidityChecker.LogExceptions(); + Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); @@ -195,16 +176,16 @@ public class Penumbra : IDalamudPlugin } private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system, out Changelog changelog ) - { - using var tInterface = StartTimer.Measure( StartTimeType.Interface ); + { + using var tInterface = StartTimer.Measure( StartTimeType.Interface ); cfg = new ConfigWindow( this, _resourceWatcher ); btn = new LaunchButton( ConfigWindow ); system = new WindowSystem( Name ); changelog = ConfigWindow.CreateChangelog(); system.AddWindow( ConfigWindow ); - system.AddWindow( cfg.ModEditPopup ); - system.AddWindow( changelog ); - Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; + system.AddWindow( cfg.ModEditPopup ); + system.AddWindow( changelog ); + Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } private void DisposeInterface() @@ -347,7 +328,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); - sb.Append( $"> **`IMC Exceptions Thrown: `** {ImcExceptions.Count}\n" ); + sb.Append( $"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n" ); sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" ); string CharacterName( ActorIdentifier id, string name ) @@ -395,58 +376,4 @@ public class Penumbra : IDalamudPlugin return sb.ToString(); } - - // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. - private static bool CheckDevPluginPenumbra() - { -#if !DEBUG - var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); - - try - { - return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); - } - catch( Exception e ) - { - Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); - return true; - } -#else - return false; -#endif - } - - // Check if the loaded version of Penumbra itself is in devPlugins. - private static bool CheckIsNotInstalled() - { -#if !DEBUG - var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; - if( !ret ) - { - Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); - } - - return !ret; -#else - return false; -#endif - } - - // Check if the loaded version of Penumbra is installed from a valid source repo. - private static bool CheckSourceRepo() - { -#if !DEBUG - return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch - { - null => false, - RepositoryLower => true, - TestRepositoryLower => true, - _ => false, - }; -#else - return true; -#endif - } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index b16252c5..f836d7e5 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -86,21 +86,21 @@ public sealed partial class ConfigWindow : Window, IDisposable try { - if( Penumbra.ImcExceptions.Count > 0 ) + if( Penumbra.ValidityChecker.ImcExceptions.Count > 0 ) { - DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "Please use the Launcher's Repair Game Files function to repair your client installation.", true ); } - else if( !Penumbra.IsValidSourceRepo ) + else if( !Penumbra.ValidityChecker.IsValidSourceRepo ) { DrawProblemWindow( $"You are loading a release version of Penumbra from the repository \"{Dalamud.PluginInterface.SourceRepository}\" instead of the official repository.\n" - + $"Please use the official repository at {Penumbra.Repository}.\n\n" + + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); } - else if( Penumbra.IsNotInstalledPenumbra ) + else if( Penumbra.ValidityChecker.IsNotInstalledPenumbra ) { DrawProblemWindow( $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" @@ -108,7 +108,7 @@ public sealed partial class ConfigWindow : Window, IDisposable + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); } - else if( Penumbra.DevPenumbraExists ) + else if( Penumbra.ValidityChecker.DevPenumbraExists ) { DrawProblemWindow( $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " @@ -153,7 +153,7 @@ public sealed partial class ConfigWindow : Window, IDisposable ImGui.TextUnformatted( "Exceptions" ); ImGui.Separator(); using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) ); - foreach( var exception in Penumbra.ImcExceptions ) + foreach( var exception in Penumbra.ValidityChecker.ImcExceptions ) { ImGuiUtil.TextWrapped( exception.ToString() ); ImGui.Separator(); diff --git a/Penumbra/ValidityChecker.cs b/Penumbra/ValidityChecker.cs new file mode 100644 index 00000000..6b55fc16 --- /dev/null +++ b/Penumbra/ValidityChecker.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; +using Penumbra.Util; + +namespace Penumbra; + +public class ValidityChecker +{ + public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public const string RepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/master/repo.json"; + public const string TestRepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/test/repo.json"; + + public readonly bool DevPenumbraExists; + public readonly bool IsNotInstalledPenumbra; + public readonly bool IsValidSourceRepo; + + public readonly List ImcExceptions = new(); + + public ValidityChecker(DalamudPluginInterface pi) + { + DevPenumbraExists = CheckDevPluginPenumbra(pi); + IsNotInstalledPenumbra = CheckIsNotInstalled(pi); + IsValidSourceRepo = CheckSourceRepo(pi); + } + + public void LogExceptions() + { + if( ImcExceptions.Count > 0 ) + ChatUtil.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); + } + + // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. + private static bool CheckDevPluginPenumbra( DalamudPluginInterface pi ) + { +#if !DEBUG + var path = Path.Combine( pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); + var dir = new DirectoryInfo( path ); + + try + { + return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); + } + catch( Exception e ) + { + Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + return true; + } +#else + return false; +#endif + } + + // Check if the loaded version of Penumbra itself is in devPlugins. + private static bool CheckIsNotInstalled( DalamudPluginInterface pi ) + { +#if !DEBUG + var checkedDirectory = pi.AssemblyLocation.Directory?.Parent?.Parent?.Name; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; + if( !ret ) + { + Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{pi.AssemblyLocation.Directory!.FullName}\"." ); + } + + return !ret; +#else + return false; +#endif + } + + // Check if the loaded version of Penumbra is installed from a valid source repo. + private static bool CheckSourceRepo( DalamudPluginInterface pi ) + { +#if !DEBUG + return pi.SourceRepository.Trim().ToLowerInvariant() switch + { + null => false, + RepositoryLower => true, + TestRepositoryLower => true, + _ => false, + }; +#else + return true; +#endif + } +} \ No newline at end of file From d8e2a5ba28bf1432a9058390b22304a883b01ec8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Mar 2023 15:17:45 +0100 Subject: [PATCH 0798/2451] Move UI Building to thread. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 16 ++++++++- Penumbra/Penumbra.cs | 71 +++++++++++++++++++++++++++---------- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index a2b680a5..f66e49bd 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a2b680a5991d9287c2dcda7cfa54183c37384fd0 +Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index fc5b2006..da560ca9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -174,16 +174,24 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc OpenMainWindow( TabType tab, string modDirectory, string modName ) { CheckInitialized(); + if( _penumbra!.ConfigWindow == null ) + { + return PenumbraApiEc.SystemDisposed; + } _penumbra!.ConfigWindow.IsOpen = true; if( !Enum.IsDefined( tab ) ) + { return PenumbraApiEc.InvalidArgument; + } if( tab != TabType.None ) + { _penumbra!.ConfigWindow.SelectTab = tab; + } - if( tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0) ) + if( tab == TabType.Mods && ( modDirectory.Length > 0 || modName.Length > 0 ) ) { if( Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) { @@ -194,12 +202,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; } } + return PenumbraApiEc.Success; } public void CloseMainWindow() { CheckInitialized(); + if( _penumbra!.ConfigWindow == null ) + { + return; + } + _penumbra!.ConfigWindow.IsOpen = false; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 524bd68c..8d8c70d2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading.Tasks; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; @@ -56,7 +57,6 @@ public class Penumbra : IDalamudPlugin public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainManager StainManager { get; private set; } = null!; - public static ItemData ItemData { get; private set; } = null!; public static ValidityChecker ValidityChecker { get; private set; } = null!; @@ -71,12 +71,15 @@ public class Penumbra : IDalamudPlugin public readonly PenumbraApi Api; public readonly HttpApi HttpApi; public readonly PenumbraIpcProviders IpcProviders; - internal readonly ConfigWindow ConfigWindow; - private readonly LaunchButton _launchButton; - private readonly WindowSystem _windowSystem; - private readonly Changelog _changelog; - private readonly CommandHandler _commandHandler; + internal ConfigWindow? ConfigWindow { get; private set; } + private LaunchButton? _launchButton; + private WindowSystem? _windowSystem; + private Changelog? _changelog; + private CommandHandler? _commandHandler; private readonly ResourceWatcher _resourceWatcher; + private bool _disposed; + + public static ItemData ItemData { get; private set; } = null!; public Penumbra( DalamudPluginInterface pluginInterface ) { @@ -94,7 +97,7 @@ public class Penumbra : IDalamudPlugin StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ) ); StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() ); StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ) ); - StartTimer.Measure( StartTimeType.Items, () => ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ) ); + ItemData = StartTimer.Measure( StartTimeType.Items, () => new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ) ); StartTimer.Measure( StartTimeType.Actors, () => Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.Framework, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ) ); @@ -128,8 +131,7 @@ public class Penumbra : IDalamudPlugin ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - SetupInterface( out ConfigWindow, out _launchButton, out _windowSystem, out _changelog ); - _commandHandler = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, ConfigWindow, ModManager, CollectionManager, Actors ); + SetupInterface(); if( Config.EnableMods ) { @@ -140,7 +142,6 @@ public class Penumbra : IDalamudPlugin if( Config.DebugMode ) { ResourceLoader.EnableDebug(); - ConfigWindow.IsOpen = true; } using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) @@ -156,8 +157,6 @@ public class Penumbra : IDalamudPlugin SubscribeItemLinks(); } - Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; - ValidityChecker.LogExceptions(); Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); @@ -175,17 +174,40 @@ public class Penumbra : IDalamudPlugin } } - private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system, out Changelog changelog ) + private void SetupInterface() + { + Task.Run( () => { using var tInterface = StartTimer.Measure( StartTimeType.Interface ); - cfg = new ConfigWindow( this, _resourceWatcher ); - btn = new LaunchButton( ConfigWindow ); - system = new WindowSystem( Name ); - changelog = ConfigWindow.CreateChangelog(); - system.AddWindow( ConfigWindow ); + var changelog = ConfigWindow.CreateChangelog(); + var cfg = new ConfigWindow( this, _resourceWatcher ) + { + IsOpen = Config.DebugMode, + }; + var btn = new LaunchButton( cfg ); + var system = new WindowSystem( Name ); + var cmd = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); + system.AddWindow( cfg ); system.AddWindow( cfg.ModEditPopup ); system.AddWindow( changelog ); + if( !_disposed ) + { + _changelog = changelog; + ConfigWindow = cfg; + _windowSystem = system; + _launchButton = btn; + _commandHandler = cmd; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; + Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + } + else + { + cfg.Dispose(); + btn.Dispose(); + cmd.Dispose(); + } + } + ); } private void DisposeInterface() @@ -243,7 +265,12 @@ public class Penumbra : IDalamudPlugin } public void ForceChangelogOpen() - => _changelog.ForceOpen = true; + { + if( _changelog != null ) + { + _changelog.ForceOpen = true; + } + } private void SubscribeItemLinks() { @@ -268,6 +295,12 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + if( _disposed ) + { + return; + } + + _disposed = true; HttpApi?.Dispose(); IpcProviders?.Dispose(); Api?.Dispose(); From 19dde3cbc4298ce50177b41272cf4eb3ff01e260 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Mar 2023 21:19:53 +0100 Subject: [PATCH 0799/2451] Add file name to exception. --- Penumbra/Mods/Mod.Creator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 3a5368ac..0fb2e5a1 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -291,7 +291,7 @@ public partial class Mod } catch( Exception ex ) { - throw new Exception( "Could not split multi group file on .pmp import.", ex ); + throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); } } } From 1b7360f8be78edb2dfa2adcee7283b7eb192d3bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Mar 2023 02:27:08 +0100 Subject: [PATCH 0800/2451] Allow item swapping between from accessories and hats to other accessory types. --- .../Meta/Manipulations/EqdpManipulation.cs | 10 ++ .../Meta/Manipulations/MetaManipulation.cs | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 133 ++++++++++++++---- Penumbra/Mods/ItemSwap/ItemSwap.cs | 11 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 17 ++- Penumbra/UI/Classes/ItemSwapWindow.cs | 108 +++++++++++++- 6 files changed, 243 insertions(+), 38 deletions(-) diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index d746295e..92ebbb2c 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -35,6 +35,16 @@ public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > Entry = Eqdp.Mask( Slot ) & entry; } + public EqdpManipulation Copy( EqdpManipulation entry ) + { + if( entry.Slot != Slot ) + { + var (bit1, bit2) = entry.Entry.ToBits( entry.Slot ); + return new EqdpManipulation(Eqdp.FromSlotAndBits( Slot, bit1, bit2 ), Slot, Gender, Race, SetId); + } + return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); + } + public EqdpManipulation Copy( EqdpEntry entry ) => new(entry, Slot, Gender, Race, SetId); diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index d1ec5f3a..1d4b370b 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -209,7 +209,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa { Type.Eqp => Eqp.Copy( other.Eqp.Entry ), Type.Gmp => Gmp.Copy( other.Gmp.Entry ), - Type.Eqdp => Eqdp.Copy( other.Eqdp.Entry ), + Type.Eqdp => Eqdp.Copy( other.Eqdp ), Type.Est => Est.Copy( other.Est.Entry ), Type.Rsp => Rsp.Copy( other.Rsp.Entry ), Type.Imc => Imc.Copy( other.Imc.Entry ), diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 231ce02d..22e37eba 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -33,6 +33,69 @@ public static class EquipmentSwap : Array.Empty< EquipSlot >(); } + public static Item[] CreateTypeSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, + EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo ) + { + LookupItem( itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom ); + LookupItem( itemTo, out var actualSlotTo, out var idTo, out var variantTo ); + if( actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot() ) + { + throw new ItemSwap.InvalidItemTypeException(); + } + + var ( imcFileFrom, variants, affectedItems ) = GetVariants( slotFrom, idFrom, idTo, variantFrom ); + var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default ); + var imcFileTo = new ImcFile( imcManip ); + var skipFemale = false; + var skipMale = false; + var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ) ) ).Imc.Entry.MaterialId; + foreach( var gr in Enum.GetValues< GenderRace >() ) + { + switch( gr.Split().Item1 ) + { + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; + case Gender.FemaleNpc when skipFemale: continue; + } + + if( CharacterUtility.EqdpIdx( gr, true ) < 0 ) + { + continue; + } + + try + { + var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo ); + if( eqdp != null ) + { + swaps.Add( eqdp ); + } + } + catch( ItemSwap.MissingFileException e ) + { + switch( gr ) + { + case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: + skipMale = true; + continue; + case GenderRace.MidlanderFemale when e.Type == ResourceType.Mdl: + skipFemale = true; + continue; + default: throw; + } + } + } + + foreach( var variant in variants ) + { + var imc = CreateImc( redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); + swaps.Add( imc ); + } + + return affectedItems; + } + public static Item[] CreateItemSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, Item itemTo, bool rFinger = true, bool lFinger = true ) { @@ -59,9 +122,9 @@ public static class EquipmentSwap var affectedItems = Array.Empty< Item >(); foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) { - (var imcFileFrom, var variants, affectedItems) = GetVariants( slot, idFrom, idTo, variantFrom ); + ( var imcFileFrom, var variants, affectedItems ) = GetVariants( slot, idFrom, idTo, variantFrom ); var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); - var imcFileTo = new ImcFile( imcManip); + var imcFileTo = new ImcFile( imcManip ); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -89,7 +152,7 @@ public static class EquipmentSwap continue; } - + try { var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); @@ -99,7 +162,7 @@ public static class EquipmentSwap } var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false; - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); + var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); if( est != null ) { swaps.Add( est ); @@ -132,15 +195,18 @@ public static class EquipmentSwap public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) + => CreateEqdp( redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo ); + public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo ) { var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idFrom.Value ), slot, gender, race, idFrom.Value ); - var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slot.IsAccessory(), idTo.Value ), slot, gender, race, idTo.Value ); + var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotFrom.IsAccessory(), idFrom.Value ), slotFrom, gender, race, idFrom.Value ); + var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotTo.IsAccessory(), idTo.Value ), slotTo, gender, race, idTo.Value ); var meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slot ); + var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slotFrom ); if( ownMdl ) { - var mdl = CreateMdl( redirections, slot, gr, idFrom, idTo, mtrlTo ); + var mdl = CreateMdl( redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo ); meta.ChildSwaps.Add( mdl ); } else if( !ownMtrl && meta.SwapAppliedIsDefault ) @@ -152,15 +218,17 @@ public static class EquipmentSwap } public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) + => CreateMdl( redirections, slot, slot, gr, idFrom, idTo, mtrlTo ); + + public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) { - var accessory = slot.IsAccessory(); - var mdlPathFrom = accessory ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slot ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slot ); - var mdlPathTo = accessory ? GamePaths.Accessory.Mdl.Path( idTo, gr, slot ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slot ); + var mdlPathFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slotFrom ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slotFrom ); + var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idTo, gr, slotTo ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slotTo ); var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) { - var mtrl = CreateMtrl( redirections, slot, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); + var mtrl = CreateMtrl( redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); if( mtrl != null ) { mdl.ChildSwaps.Add( mtrl ); @@ -182,22 +250,22 @@ public static class EquipmentSwap variant = ( byte )( ( Quad )i.ModelMain ).B; } - private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom ) + private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom ) { - var entry = new ImcManipulation( slot, variantFrom, idFrom.Value, default ); + var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default ); var imc = new ImcFile( entry ); Item[] items; byte[] variants; if( idFrom.Value == idTo.Value ) { - items = Penumbra.Identifier.Identify( idFrom, variantFrom, slot ).ToArray(); + items = Penumbra.Identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); variants = new[] { variantFrom }; } else { - items = Penumbra.Identifier.Identify( slot.IsEquipment() - ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) - : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slot ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); + items = Penumbra.Identifier.Identify( slotFrom.IsEquipment() + ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) + : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); } @@ -218,11 +286,15 @@ public static class EquipmentSwap public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) + => CreateImc( redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo ); + + public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) { - var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slot ), variantFrom ); - var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ); - var manipulationFrom = new ImcManipulation( slot, variantFrom, idFrom.Value, entryFrom ); - var manipulationTo = new ImcManipulation( slot, variantTo, idTo.Value, entryTo ); + var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slotFrom ), variantFrom ); + var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ); + var manipulationFrom = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, entryFrom ); + var manipulationTo = new ImcManipulation( slotTo, variantTo, idTo.Value, entryTo ); var imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId ); @@ -292,18 +364,23 @@ public static class EquipmentSwap public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, ref bool dataWasChanged ) + => CreateMtrl( redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged ); + + public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged ) { - var prefix = slot.IsAccessory() ? 'a' : 'e'; + var prefix = slotTo.IsAccessory() ? 'a' : 'e'; if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) { return null; } - var folderTo = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); + var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); var pathTo = $"{folderTo}{fileName}"; - var folderFrom = slot.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); + var folderFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); var newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom ); + newFileName = ItemSwap.ReplaceSlot( newFileName, slotTo, slotFrom, slotTo != slotFrom ); var pathFrom = $"{folderFrom}{newFileName}"; if( newFileName != fileName ) @@ -318,7 +395,7 @@ public static class EquipmentSwap foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) { - var tex = CreateTex( redirections, prefix, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); + var tex = CreateTex( redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( tex ); } @@ -326,6 +403,9 @@ public static class EquipmentSwap } public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) + => CreateTex( redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged ); + + public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) { var path = texture.Path; var addedDashes = false; @@ -340,6 +420,7 @@ public static class EquipmentSwap } var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); + newPath = ItemSwap.ReplaceSlot( newPath, slotTo, slotFrom, slotTo != slotFrom ); newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" ); if( newPath != path ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 2369a92d..e479afc1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -17,12 +17,6 @@ public static class ItemSwap public class InvalidItemTypeException : Exception { } - public class InvalidImcException : Exception - { } - - public class IdUnavailableException : Exception - { } - public class MissingFileException : Exception { public readonly ResourceType Type; @@ -224,6 +218,11 @@ public static class ItemSwap ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) : path; + public static string ReplaceSlot( string path, EquipSlot from, EquipSlot to, bool condition = true ) + => condition + ? path.Replace( $"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_" ) + : path; + public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 66d4851e..6b6e3111 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Penumbra.Mods.ItemSwap; @@ -133,6 +133,15 @@ public class ItemSwapContainer return ret; } + public Item[] LoadTypeSwap( EquipSlot slotFrom, Item from, EquipSlot slotTo, Item to, ModCollection? collection = null ) + { + Swaps.Clear(); + Loaded = false; + var ret = EquipmentSwap.CreateTypeSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + Loaded = true; + return ret; + } + public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) { var pathResolver = PathResolver( collection ); diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index cd551088..6c7ae4c6 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -34,6 +34,7 @@ public class ItemSwapWindow : IDisposable Necklace, Bracelet, Ring, + BetweenSlots, Hair, Face, Ears, @@ -101,6 +102,8 @@ public class ItemSwapWindow : IDisposable private int _targetId = 0; private int _sourceId = 0; private Exception? _loadException = null; + private EquipSlot _slotFrom = EquipSlot.Head; + private EquipSlot _slotTo = EquipSlot.Ears; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; @@ -164,6 +167,15 @@ public class ItemSwapWindow : IDisposable _useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing ); } + break; + case SwapType.BetweenSlots: + var (_, _, selectorFrom) = GetAccessorySelector( _slotFrom, true ); + var (_, _, selectorTo) = GetAccessorySelector( _slotTo, false ); + if( selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null ) + { + _affectedItems = _swapData.LoadTypeSwap( _slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, @@ -364,6 +376,7 @@ public class ItemSwapWindow : IDisposable DrawEquipmentSwap( SwapType.Necklace ); DrawEquipmentSwap( SwapType.Bracelet ); DrawEquipmentSwap( SwapType.Ring ); + DrawAccessorySwap(); DrawHairSwap(); DrawFaceSwap(); DrawEarSwap(); @@ -373,7 +386,7 @@ public class ItemSwapWindow : IDisposable private ImRaii.IEndObject DrawTab( SwapType newTab ) { - using var tab = ImRaii.TabItem( newTab.ToString() ); + using var tab = ImRaii.TabItem( newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString() ); if( tab ) { _dirty |= _lastTab != newTab; @@ -385,6 +398,99 @@ public class ItemSwapWindow : IDisposable return tab; } + private void DrawAccessorySwap() + { + using var tab = DrawTab( SwapType.BetweenSlots ); + if( !tab ) + { + return; + } + + using var table = ImRaii.Table( "##settings", 3, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableSetupColumn( "##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "and put them on these" ).X ); + + var (article1, article2, selector) = GetAccessorySelector( _slotFrom, true ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( $"Take {article1}" ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) ) + { + if( combo ) + { + foreach( var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head) ) + { + if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom ) + { + _dirty = true; + _slotFrom = slot; + if( slot == _slotTo ) + { + _slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s ); + } + } + } + } + } + + ImGui.TableNextColumn(); + _dirty |= selector.Draw( "##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + (article1, _, selector) = GetAccessorySelector( _slotTo, false ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( $"and put {article2} on {article1}" ); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) ) + { + if( combo ) + { + foreach( var slot in EquipSlotExtensions.AccessorySlots.Where( s => s != _slotFrom ) ) + { + if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo ) + { + _dirty = true; + _slotTo = slot; + } + } + } + } + + ImGui.TableNextColumn(); + + _dirty |= selector.Draw( "##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + if( _affectedItems is { Length: > 1 } ) + { + ImGui.SameLine(); + ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, selector.CurrentSelection.Item2 ) ) + .Select( i => i.Name.ToDalamudString().TextValue ) ) ); + } + } + } + + private (string, string, ItemSelector) GetAccessorySelector( EquipSlot slot, bool source ) + { + var (type, article1, article2) = slot switch + { + EquipSlot.Head => (SwapType.Hat, "this", "it"), + EquipSlot.Ears => (SwapType.Earrings, "these", "them"), + EquipSlot.Neck => (SwapType.Necklace, "this", "it"), + EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"), + EquipSlot.RFinger => (SwapType.Ring, "this", "it"), + EquipSlot.LFinger => (SwapType.Ring, "this", "it"), + _ => (SwapType.Ring, "this", "it"), + }; + var tuple = _selectors[ type ]; + return (article1, article2, source ? tuple.Source : tuple.Target); + } + private void DrawEquipmentSwap( SwapType type ) { using var tab = DrawTab( type ); From 3c564add0e900a53ffd5e60977719fe047496a44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Mar 2023 02:42:07 +0100 Subject: [PATCH 0801/2451] Fix missing stuff from defines. --- Penumbra/ValidityChecker.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Penumbra/ValidityChecker.cs b/Penumbra/ValidityChecker.cs index 6b55fc16..5e71d998 100644 --- a/Penumbra/ValidityChecker.cs +++ b/Penumbra/ValidityChecker.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; using Penumbra.Util; @@ -44,7 +46,7 @@ public class ValidityChecker } catch( Exception e ) { - Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + Penumbra.Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); return true; } #else @@ -60,7 +62,7 @@ public class ValidityChecker var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; if( !ret ) { - Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{pi.AssemblyLocation.Directory!.FullName}\"." ); + Penumbra.Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{pi.AssemblyLocation.Directory!.FullName}\"." ); } return !ret; From 73e2793da6883424cf20724d97a512e8967aecd2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Mar 2023 18:29:58 +0100 Subject: [PATCH 0802/2451] tmp --- .editorconfig | 3606 ++++++++++++++++- Penumbra.GameData/Data/GamePathParser.cs | 46 +- Penumbra/Api/IpcTester.cs | 15 +- Penumbra/Api/PenumbraApi.cs | 21 +- .../Collections/CollectionManager.Active.cs | 5 +- Penumbra/Collections/ModCollection.File.cs | 3 +- Penumbra/CommandHandler.cs | 85 +- Penumbra/Configuration.Migration.cs | 9 +- Penumbra/Configuration.cs | 7 +- Penumbra/Dalamud.cs | 150 - Penumbra/Import/Textures/CombinedTexture.cs | 3 +- Penumbra/Import/Textures/Texture.cs | 7 +- Penumbra/Interop/CharacterUtility.cs | 7 +- Penumbra/Interop/GameEventManager.cs | 90 +- Penumbra/Interop/ObjectReloader.cs | 65 +- .../Interop/Resolver/CutsceneCharacters.cs | 9 +- .../Resolver/IdentifiedCollectionCache.cs | 7 +- .../Resolver/PathResolver.AnimationState.cs | 17 +- .../Resolver/PathResolver.DrawObjectState.cs | 9 +- .../Resolver/PathResolver.Identification.cs | 7 +- Penumbra/Interop/Resolver/PathResolver.cs | 3 +- Penumbra/Meta/Files/ImcFile.cs | 7 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 3 +- Penumbra/Mods/ItemSwap/Swaps.cs | 5 +- Penumbra/Mods/Mod.LocalData.cs | 11 +- Penumbra/Mods/ModFileSystem.cs | 5 +- Penumbra/Penumbra.cs | 84 +- Penumbra/Penumbra.csproj | 1 + Penumbra/PenumbraNew.cs | 50 + Penumbra/Services/DalamudServices.cs | 168 + Penumbra/Services/ObjectIdentifier.cs | 66 + Penumbra/{ => Services}/ValidityChecker.cs | 8 + .../UI/Classes/ModEditWindow.FileEditor.cs | 3 +- .../ModEditWindow.Materials.MtrlTab.cs | 3 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +- .../ConfigWindow.CollectionsTab.Individual.cs | 5 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 7 +- Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 3 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 5 +- Penumbra/UI/ConfigWindow.ResourceTab.cs | 3 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 7 +- .../UI/ConfigWindow.SettingsTab.General.cs | 9 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 5 +- Penumbra/UI/ConfigWindow.cs | 13 +- Penumbra/UI/LaunchButton.cs | 13 +- Penumbra/Util/ChatUtil.cs | 7 +- Penumbra/Util/PerformanceType.cs | 2 - Penumbra/packages.lock.json | 18 +- 48 files changed, 4231 insertions(+), 456 deletions(-) delete mode 100644 Penumbra/Dalamud.cs create mode 100644 Penumbra/PenumbraNew.cs create mode 100644 Penumbra/Services/DalamudServices.cs create mode 100644 Penumbra/Services/ObjectIdentifier.cs rename Penumbra/{ => Services}/ValidityChecker.cs (89%) diff --git a/.editorconfig b/.editorconfig index 238bb1dc..0bbaa114 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,27 +1,346 @@ -[*] -charset=utf-8 -end_of_line=lf -trim_trailing_whitespace=true -insert_final_newline=false +[*.proto] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] indent_style=space indent_size=4 +tab_width=4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + +[*] # Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers=false +csharp_indent_braces=false +csharp_indent_switch_labels=true +csharp_new_line_before_catch=true +csharp_new_line_before_else=true +csharp_new_line_before_finally=true +csharp_new_line_before_members_in_object_initializers=true +csharp_new_line_before_open_brace=all +csharp_new_line_between_query_expression_clauses=true csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_prefer_braces=true:none +csharp_preserve_single_line_blocks=true csharp_space_after_cast=false -csharp_space_after_keywords_in_control_flow_statements=false -csharp_space_between_method_call_parameter_list_parentheses=true -csharp_space_between_method_declaration_parameter_list_parentheses=true -csharp_space_between_parentheses=control_flow_statements,expressions,type_casts +csharp_space_after_colon_in_inheritance_clause=true +csharp_space_after_comma=true +csharp_space_after_dot=false +csharp_space_after_keywords_in_control_flow_statements=true +csharp_space_after_semicolon_in_for_statement=true +csharp_space_around_binary_operators=before_and_after +csharp_space_before_colon_in_inheritance_clause=true +csharp_space_before_comma=false +csharp_space_before_dot=false +csharp_space_before_open_square_brackets=false +csharp_space_before_semicolon_in_for_statement=false +csharp_space_between_empty_square_brackets=false +csharp_space_between_method_call_empty_parameter_list_parentheses=false +csharp_space_between_method_call_name_and_opening_parenthesis=false +csharp_space_between_method_call_parameter_list_parentheses=false +csharp_space_between_method_declaration_empty_parameter_list_parentheses=false +csharp_space_between_method_declaration_name_and_open_parenthesis=false +csharp_space_between_method_declaration_parameter_list_parentheses=false +csharp_space_between_parentheses=false +csharp_space_between_square_brackets=false +csharp_style_namespace_declarations= file_scoped:suggestion csharp_style_var_elsewhere=true:suggestion csharp_style_var_for_built_in_types=true:suggestion csharp_style_var_when_type_is_apparent=true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none -dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none +csharp_using_directive_placement= outside_namespace:silent +dotnet_diagnostic.bc40000.severity=warning +dotnet_diagnostic.bc400005.severity=warning +dotnet_diagnostic.bc40008.severity=warning +dotnet_diagnostic.bc40056.severity=warning +dotnet_diagnostic.bc42016.severity=warning +dotnet_diagnostic.bc42024.severity=warning +dotnet_diagnostic.bc42025.severity=warning +dotnet_diagnostic.bc42104.severity=warning +dotnet_diagnostic.bc42105.severity=warning +dotnet_diagnostic.bc42106.severity=warning +dotnet_diagnostic.bc42107.severity=warning +dotnet_diagnostic.bc42304.severity=warning +dotnet_diagnostic.bc42309.severity=warning +dotnet_diagnostic.bc42322.severity=warning +dotnet_diagnostic.bc42349.severity=warning +dotnet_diagnostic.bc42353.severity=warning +dotnet_diagnostic.bc42354.severity=warning +dotnet_diagnostic.bc42355.severity=warning +dotnet_diagnostic.bc42356.severity=warning +dotnet_diagnostic.bc42358.severity=warning +dotnet_diagnostic.bc42504.severity=warning +dotnet_diagnostic.bc42505.severity=warning +dotnet_diagnostic.cs0067.severity=warning +dotnet_diagnostic.cs0078.severity=warning +dotnet_diagnostic.cs0108.severity=warning +dotnet_diagnostic.cs0109.severity=warning +dotnet_diagnostic.cs0114.severity=warning +dotnet_diagnostic.cs0162.severity=warning +dotnet_diagnostic.cs0164.severity=warning +dotnet_diagnostic.cs0168.severity=warning +dotnet_diagnostic.cs0169.severity=warning +dotnet_diagnostic.cs0183.severity=warning +dotnet_diagnostic.cs0184.severity=warning +dotnet_diagnostic.cs0197.severity=warning +dotnet_diagnostic.cs0219.severity=warning +dotnet_diagnostic.cs0252.severity=warning +dotnet_diagnostic.cs0253.severity=warning +dotnet_diagnostic.cs0414.severity=warning +dotnet_diagnostic.cs0420.severity=warning +dotnet_diagnostic.cs0465.severity=warning +dotnet_diagnostic.cs0469.severity=warning +dotnet_diagnostic.cs0612.severity=warning +dotnet_diagnostic.cs0618.severity=warning +dotnet_diagnostic.cs0628.severity=warning +dotnet_diagnostic.cs0642.severity=warning +dotnet_diagnostic.cs0649.severity=warning +dotnet_diagnostic.cs0652.severity=warning +dotnet_diagnostic.cs0657.severity=warning +dotnet_diagnostic.cs0658.severity=warning +dotnet_diagnostic.cs0659.severity=warning +dotnet_diagnostic.cs0660.severity=warning +dotnet_diagnostic.cs0661.severity=warning +dotnet_diagnostic.cs0665.severity=warning +dotnet_diagnostic.cs0672.severity=warning +dotnet_diagnostic.cs0675.severity=warning +dotnet_diagnostic.cs0693.severity=warning +dotnet_diagnostic.cs1030.severity=warning +dotnet_diagnostic.cs1058.severity=warning +dotnet_diagnostic.cs1066.severity=warning +dotnet_diagnostic.cs1522.severity=warning +dotnet_diagnostic.cs1570.severity=warning +dotnet_diagnostic.cs1571.severity=warning +dotnet_diagnostic.cs1572.severity=warning +dotnet_diagnostic.cs1573.severity=warning +dotnet_diagnostic.cs1574.severity=warning +dotnet_diagnostic.cs1580.severity=warning +dotnet_diagnostic.cs1581.severity=warning +dotnet_diagnostic.cs1584.severity=warning +dotnet_diagnostic.cs1587.severity=warning +dotnet_diagnostic.cs1589.severity=warning +dotnet_diagnostic.cs1590.severity=warning +dotnet_diagnostic.cs1591.severity=warning +dotnet_diagnostic.cs1592.severity=warning +dotnet_diagnostic.cs1710.severity=warning +dotnet_diagnostic.cs1711.severity=warning +dotnet_diagnostic.cs1712.severity=warning +dotnet_diagnostic.cs1717.severity=warning +dotnet_diagnostic.cs1723.severity=warning +dotnet_diagnostic.cs1911.severity=warning +dotnet_diagnostic.cs1957.severity=warning +dotnet_diagnostic.cs1981.severity=warning +dotnet_diagnostic.cs1998.severity=warning +dotnet_diagnostic.cs4014.severity=warning +dotnet_diagnostic.cs7022.severity=warning +dotnet_diagnostic.cs7023.severity=warning +dotnet_diagnostic.cs7095.severity=warning +dotnet_diagnostic.cs8094.severity=warning +dotnet_diagnostic.cs8123.severity=warning +dotnet_diagnostic.cs8321.severity=warning +dotnet_diagnostic.cs8383.severity=warning +dotnet_diagnostic.cs8416.severity=warning +dotnet_diagnostic.cs8417.severity=warning +dotnet_diagnostic.cs8424.severity=warning +dotnet_diagnostic.cs8425.severity=warning +dotnet_diagnostic.cs8509.severity=warning +dotnet_diagnostic.cs8524.severity=warning +dotnet_diagnostic.cs8597.severity=warning +dotnet_diagnostic.cs8600.severity=warning +dotnet_diagnostic.cs8601.severity=warning +dotnet_diagnostic.cs8602.severity=warning +dotnet_diagnostic.cs8603.severity=warning +dotnet_diagnostic.cs8604.severity=warning +dotnet_diagnostic.cs8605.severity=warning +dotnet_diagnostic.cs8607.severity=warning +dotnet_diagnostic.cs8608.severity=warning +dotnet_diagnostic.cs8609.severity=warning +dotnet_diagnostic.cs8610.severity=warning +dotnet_diagnostic.cs8611.severity=warning +dotnet_diagnostic.cs8612.severity=warning +dotnet_diagnostic.cs8613.severity=warning +dotnet_diagnostic.cs8614.severity=warning +dotnet_diagnostic.cs8615.severity=warning +dotnet_diagnostic.cs8616.severity=warning +dotnet_diagnostic.cs8617.severity=warning +dotnet_diagnostic.cs8618.severity=warning +dotnet_diagnostic.cs8619.severity=warning +dotnet_diagnostic.cs8620.severity=warning +dotnet_diagnostic.cs8621.severity=warning +dotnet_diagnostic.cs8622.severity=warning +dotnet_diagnostic.cs8624.severity=warning +dotnet_diagnostic.cs8625.severity=warning +dotnet_diagnostic.cs8629.severity=warning +dotnet_diagnostic.cs8631.severity=warning +dotnet_diagnostic.cs8632.severity=none +dotnet_diagnostic.cs8633.severity=warning +dotnet_diagnostic.cs8634.severity=warning +dotnet_diagnostic.cs8643.severity=warning +dotnet_diagnostic.cs8644.severity=warning +dotnet_diagnostic.cs8645.severity=warning +dotnet_diagnostic.cs8655.severity=warning +dotnet_diagnostic.cs8656.severity=warning +dotnet_diagnostic.cs8667.severity=warning +dotnet_diagnostic.cs8669.severity=none +dotnet_diagnostic.cs8670.severity=warning +dotnet_diagnostic.cs8714.severity=warning +dotnet_diagnostic.cs8762.severity=warning +dotnet_diagnostic.cs8763.severity=warning +dotnet_diagnostic.cs8764.severity=warning +dotnet_diagnostic.cs8765.severity=warning +dotnet_diagnostic.cs8766.severity=warning +dotnet_diagnostic.cs8767.severity=warning +dotnet_diagnostic.cs8768.severity=warning +dotnet_diagnostic.cs8769.severity=warning +dotnet_diagnostic.cs8770.severity=warning +dotnet_diagnostic.cs8774.severity=warning +dotnet_diagnostic.cs8775.severity=warning +dotnet_diagnostic.cs8776.severity=warning +dotnet_diagnostic.cs8777.severity=warning +dotnet_diagnostic.cs8794.severity=warning +dotnet_diagnostic.cs8819.severity=warning +dotnet_diagnostic.cs8824.severity=warning +dotnet_diagnostic.cs8825.severity=warning +dotnet_diagnostic.cs8846.severity=warning +dotnet_diagnostic.cs8847.severity=warning +dotnet_diagnostic.cs8851.severity=warning +dotnet_diagnostic.cs8860.severity=warning +dotnet_diagnostic.cs8892.severity=warning +dotnet_diagnostic.cs8907.severity=warning +dotnet_diagnostic.cs8947.severity=warning +dotnet_diagnostic.cs8960.severity=warning +dotnet_diagnostic.cs8961.severity=warning +dotnet_diagnostic.cs8962.severity=warning +dotnet_diagnostic.cs8963.severity=warning +dotnet_diagnostic.cs8965.severity=warning +dotnet_diagnostic.cs8966.severity=warning +dotnet_diagnostic.cs8971.severity=warning +dotnet_diagnostic.wme006.severity=warning +dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols=constants_symbols +dotnet_naming_rule.event_rule.import_to_resharper=as_predefined +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols=event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols +dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined +dotnet_naming_rule.locals_rule.severity = warning +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols=locals_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols +dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_functions_rule.severity = warning +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols +dotnet_naming_rule.method_rule.import_to_resharper=as_predefined +dotnet_naming_rule.method_rule.severity = warning +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols=method_symbols +dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.parameters_rule.symbols=parameters_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols +dotnet_naming_rule.property_rule.import_to_resharper=as_predefined +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols=property_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = warning +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix=I +dotnet_naming_style.lower_camel_case_style.capitalization=camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix=_ +dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix=T +dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds=field +dotnet_naming_symbols.constants_symbols.required_modifiers=const +dotnet_naming_symbols.event_symbols.applicable_accessibilities=* +dotnet_naming_symbols.event_symbols.applicable_kinds=event +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface +dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* +dotnet_naming_symbols.locals_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.required_modifiers=const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function +dotnet_naming_symbols.method_symbols.applicable_accessibilities=* +dotnet_naming_symbols.method_symbols.applicable_kinds=method +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field +dotnet_naming_symbols.private_constants_symbols.required_modifiers=const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.property_symbols.applicable_accessibilities=* +dotnet_naming_symbols.property_symbols.applicable_kinds=property +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter +dotnet_separate_import_directive_groups=false +dotnet_sort_system_directives_first=true +dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion dotnet_style_predefined_type_for_member_access=true:suggestion dotnet_style_qualification_for_event=false:suggestion @@ -29,57 +348,3276 @@ dotnet_style_qualification_for_field=false:suggestion dotnet_style_qualification_for_method=false:suggestion dotnet_style_qualification_for_property=false:suggestion dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +file_header_template= # ReSharper properties +resharper_accessor_owner_body=expression_body +resharper_alignment_tab_fill_style=use_spaces +resharper_align_first_arg_by_paren=false +resharper_align_linq_query=false +resharper_align_multiline_argument=true +resharper_align_multiline_array_and_object_initializer=false +resharper_align_multiline_array_initializer=true resharper_align_multiline_binary_expressions_chain=false -resharper_align_multiline_calls_chain=false -resharper_autodetect_indent_settings=true +resharper_align_multiline_binary_patterns=false +resharper_align_multiline_ctor_init=true +resharper_align_multiline_expression_braces=false +resharper_align_multiline_implements_list=true +resharper_align_multiline_property_pattern=false +resharper_align_multiline_statement_conditions=true +resharper_align_multiline_switch_expression=false +resharper_align_multiline_type_argument=true +resharper_align_multiline_type_parameter=true +resharper_align_multline_type_parameter_constrains=true +resharper_align_multline_type_parameter_list=false +resharper_align_tuple_components=false +resharper_align_union_type_usage=true +resharper_allow_alias=true +resharper_allow_comment_after_lbrace=false +resharper_allow_far_alignment=false +resharper_always_use_end_of_line_brace_style=false +resharper_apply_auto_detected_rules=false +resharper_apply_on_completion=false +resharper_arguments_anonymous_function=positional +resharper_arguments_literal=positional +resharper_arguments_named=positional +resharper_arguments_other=positional +resharper_arguments_skip_single=false +resharper_arguments_string_literal=positional +resharper_attribute_style=do_not_touch +resharper_autodetect_indent_settings=false +resharper_blank_lines_after_block_statements=1 +resharper_blank_lines_after_case=0 +resharper_blank_lines_after_control_transfer_statements=1 +resharper_blank_lines_after_file_scoped_namespace_directive=1 +resharper_blank_lines_after_imports=1 +resharper_blank_lines_after_multiline_statements=0 +resharper_blank_lines_after_options=1 +resharper_blank_lines_after_start_comment=1 +resharper_blank_lines_after_using_list=1 +resharper_blank_lines_around_accessor=0 +resharper_blank_lines_around_auto_property=1 +resharper_blank_lines_around_block_case_section=0 +resharper_blank_lines_around_class_definition=1 +resharper_blank_lines_around_field=1 +resharper_blank_lines_around_function_declaration=0 +resharper_blank_lines_around_function_definition=1 +resharper_blank_lines_around_global_attribute=0 +resharper_blank_lines_around_invocable=1 +resharper_blank_lines_around_local_method=1 +resharper_blank_lines_around_multiline_case_section=0 +resharper_blank_lines_around_namespace=1 +resharper_blank_lines_around_other_declaration=0 +resharper_blank_lines_around_property=1 +resharper_blank_lines_around_razor_functions=1 +resharper_blank_lines_around_razor_helpers=1 +resharper_blank_lines_around_razor_sections=1 +resharper_blank_lines_around_region=1 +resharper_blank_lines_around_single_line_accessor=0 +resharper_blank_lines_around_single_line_auto_property=0 +resharper_blank_lines_around_single_line_field=0 +resharper_blank_lines_around_single_line_function_definition=0 +resharper_blank_lines_around_single_line_invocable=0 +resharper_blank_lines_around_single_line_local_method=0 +resharper_blank_lines_around_single_line_property=0 +resharper_blank_lines_around_single_line_type=0 +resharper_blank_lines_around_type=1 +resharper_blank_lines_before_block_statements=0 +resharper_blank_lines_before_case=0 +resharper_blank_lines_before_control_transfer_statements=0 +resharper_blank_lines_before_multiline_statements=0 +resharper_blank_lines_before_single_line_comment=0 +resharper_blank_lines_inside_namespace=0 +resharper_blank_lines_inside_region=1 +resharper_blank_lines_inside_type=0 +resharper_blank_line_after_pi=true +resharper_braces_for_dowhile=required +resharper_braces_for_fixed=required +resharper_braces_for_for=required_for_multiline +resharper_braces_for_foreach=required_for_multiline +resharper_braces_for_ifelse=not_required_for_both +resharper_braces_for_lock=required +resharper_braces_for_using=required +resharper_braces_for_while=required_for_multiline resharper_braces_redundant=true +resharper_break_template_declaration=line_break +resharper_can_use_global_alias=true +resharper_configure_await_analysis_mode=disabled resharper_constructor_or_destructor_body=expression_body +resharper_continuous_indent_multiplier=1 +resharper_continuous_line_indent=single +resharper_cpp_align_multiline_argument=true +resharper_cpp_align_multiline_calls_chain=true +resharper_cpp_align_multiline_extends_list=true +resharper_cpp_align_multiline_for_stmt=true +resharper_cpp_align_multiline_parameter=true +resharper_cpp_align_multiple_declaration=true +resharper_cpp_align_ternary=align_not_nested +resharper_cpp_anonymous_method_declaration_braces=next_line +resharper_cpp_case_block_braces=next_line_shifted_2 +resharper_cpp_empty_block_style=multiline +resharper_cpp_indent_switch_labels=false +resharper_cpp_insert_final_newline=false +resharper_cpp_int_align_comments=false +resharper_cpp_invocable_declaration_braces=next_line +resharper_cpp_max_line_length=120 +resharper_cpp_new_line_before_catch=true +resharper_cpp_new_line_before_else=true +resharper_cpp_new_line_before_while=true +resharper_cpp_other_braces=next_line +resharper_cpp_space_around_binary_operator=true +resharper_cpp_type_declaration_braces=next_line +resharper_cpp_wrap_arguments_style=wrap_if_long +resharper_cpp_wrap_lines=true +resharper_cpp_wrap_parameters_style=wrap_if_long +resharper_csharp_align_multiline_argument=false +resharper_csharp_align_multiline_calls_chain=false +resharper_csharp_align_multiline_expression=false +resharper_csharp_align_multiline_extends_list=false +resharper_csharp_align_multiline_for_stmt=false +resharper_csharp_align_multiline_parameter=false +resharper_csharp_align_multiple_declaration=true resharper_csharp_empty_block_style=together -resharper_csharp_max_line_length=180 -resharper_csharp_space_within_array_access_brackets=true -resharper_enforce_line_ending_style=true +resharper_csharp_insert_final_newline=true +resharper_csharp_int_align_comments=true +resharper_csharp_max_line_length=144 +resharper_csharp_naming_rule.enum_member=AaBb +resharper_csharp_naming_rule.method_property_event=AaBb +resharper_csharp_naming_rule.other=AaBb +resharper_csharp_new_line_before_while=false +resharper_csharp_prefer_qualified_reference=false +resharper_csharp_space_after_unary_operator=false +resharper_csharp_wrap_arguments_style=wrap_if_long +resharper_csharp_wrap_before_binary_opsign=true +resharper_csharp_wrap_for_stmt_header_style=wrap_if_long +resharper_csharp_wrap_lines=true +resharper_csharp_wrap_parameters_style=wrap_if_long +resharper_css_brace_style=end_of_line +resharper_css_insert_final_newline=false +resharper_css_keep_blank_lines_between_declarations=1 +resharper_css_max_line_length=120 +resharper_css_wrap_lines=true +resharper_cxxcli_property_declaration_braces=next_line +resharper_declarations_style=separate_lines +resharper_default_exception_variable_name=e +resharper_default_value_when_type_evident=default_literal +resharper_default_value_when_type_not_evident=default_literal +resharper_delete_quotes_from_solid_values=false +resharper_disable_blank_line_changes=false +resharper_disable_formatter=false +resharper_disable_indenter=false +resharper_disable_int_align=false +resharper_disable_line_break_changes=false +resharper_disable_line_break_removal=false +resharper_disable_space_changes=false +resharper_disable_space_changes_before_trailing_comment=false +resharper_dont_remove_extra_blank_lines=false +resharper_enable_wrapping=false +resharper_enforce_line_ending_style=false +resharper_event_handler_pattern_long=$object$On$event$ +resharper_event_handler_pattern_short=On$event$ +resharper_expression_braces=inside +resharper_expression_pars=inside +resharper_extra_spaces=remove_all +resharper_force_attribute_style=separate +resharper_force_chop_compound_do_expression=false +resharper_force_chop_compound_if_expression=false +resharper_force_chop_compound_while_expression=false +resharper_force_control_statements_braces=do_not_change +resharper_force_linebreaks_inside_complex_literals=true +resharper_force_variable_declarations_on_new_line=false +resharper_format_leading_spaces_decl=false +resharper_free_block_braces=next_line +resharper_function_declaration_return_type_style=do_not_change +resharper_function_definition_return_type_style=do_not_change +resharper_generator_mode=false +resharper_html_attribute_indent=align_by_first_attribute +resharper_html_insert_final_newline=false +resharper_html_linebreak_before_elements=body,div,p,form,h1,h2,h3 +resharper_html_max_blank_lines_between_tags=2 +resharper_html_max_line_length=120 +resharper_html_pi_attribute_style=on_single_line +resharper_html_space_before_self_closing=false +resharper_html_wrap_lines=true +resharper_ignore_space_preservation=false +resharper_include_prefix_comment_in_indent=false +resharper_indent_access_specifiers_from_class=false +resharper_indent_aligned_ternary=true +resharper_indent_anonymous_method_block=false +resharper_indent_braces_inside_statement_conditions=true +resharper_indent_case_from_select=true +resharper_indent_child_elements=OneIndent +resharper_indent_class_members_from_access_specifiers=false +resharper_indent_comment=true +resharper_indent_inside_namespace=true +resharper_indent_invocation_pars=inside +resharper_indent_left_par_inside_expression=false +resharper_indent_method_decl_pars=inside +resharper_indent_nested_fixed_stmt=false +resharper_indent_nested_foreach_stmt=true +resharper_indent_nested_for_stmt=true +resharper_indent_nested_lock_stmt=false +resharper_indent_nested_usings_stmt=false +resharper_indent_nested_while_stmt=true +resharper_indent_pars=inside +resharper_indent_preprocessor_directives=none +resharper_indent_preprocessor_if=no_indent +resharper_indent_preprocessor_other=no_indent +resharper_indent_preprocessor_region=usual_indent +resharper_indent_statement_pars=inside +resharper_indent_text=OneIndent +resharper_indent_typearg_angles=inside +resharper_indent_typeparam_angles=inside +resharper_indent_type_constraints=true +resharper_indent_wrapped_function_names=false +resharper_instance_members_qualify_declared_in=this_class, base_class +resharper_int_align=true resharper_int_align_assignments=true -resharper_int_align_comments=true +resharper_int_align_binary_expressions=false +resharper_int_align_declaration_names=false +resharper_int_align_eq=false resharper_int_align_fields=true -resharper_int_align_invocations=false +resharper_int_align_fix_in_adjacent=true +resharper_int_align_invocations=true +resharper_int_align_methods=true resharper_int_align_nested_ternary=true -resharper_int_align_properties=false +resharper_int_align_parameters=false +resharper_int_align_properties=true +resharper_int_align_property_patterns=true resharper_int_align_switch_expressions=true resharper_int_align_switch_sections=true resharper_int_align_variables=true +resharper_js_align_multiline_parameter=false +resharper_js_align_multiple_declaration=false +resharper_js_align_ternary=none +resharper_js_brace_style=end_of_line +resharper_js_empty_block_style=multiline +resharper_js_indent_switch_labels=false +resharper_js_insert_final_newline=false +resharper_js_keep_blank_lines_between_declarations=2 +resharper_js_max_line_length=120 +resharper_js_new_line_before_catch=false +resharper_js_new_line_before_else=false +resharper_js_new_line_before_finally=false +resharper_js_new_line_before_while=false +resharper_js_space_around_binary_operator=true +resharper_js_wrap_arguments_style=chop_if_long +resharper_js_wrap_before_binary_opsign=false +resharper_js_wrap_for_stmt_header_style=chop_if_long +resharper_js_wrap_lines=true +resharper_js_wrap_parameters_style=chop_if_long +resharper_keep_blank_lines_in_code=2 +resharper_keep_blank_lines_in_declarations=2 +resharper_keep_existing_attribute_arrangement=false +resharper_keep_existing_declaration_block_arrangement=false +resharper_keep_existing_declaration_parens_arrangement=true +resharper_keep_existing_embedded_arrangement=false +resharper_keep_existing_embedded_block_arrangement=false +resharper_keep_existing_enum_arrangement=false +resharper_keep_existing_expr_member_arrangement=false +resharper_keep_existing_initializer_arrangement=false +resharper_keep_existing_invocation_parens_arrangement=true +resharper_keep_existing_property_patterns_arrangement=true +resharper_keep_existing_switch_expression_arrangement=false +resharper_keep_nontrivial_alias=true +resharper_keep_user_linebreaks=true +resharper_keep_user_wrapping=true +resharper_linebreaks_around_razor_statements=true +resharper_linebreaks_inside_tags_for_elements_longer_than=2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements=true +resharper_linebreaks_inside_tags_for_multiline_elements=true +resharper_linebreak_before_all_elements=false +resharper_linebreak_before_multiline_elements=true +resharper_linebreak_before_singleline_elements=false +resharper_line_break_after_colon_in_member_initializer_lists=do_not_change +resharper_line_break_after_comma_in_member_initializer_lists=false +resharper_line_break_before_comma_in_member_initializer_lists=false +resharper_line_break_before_requires_clause=do_not_change +resharper_linkage_specification_braces=end_of_line +resharper_linkage_specification_indentation=none resharper_local_function_body=expression_body +resharper_macro_block_begin= +resharper_macro_block_end= +resharper_max_array_initializer_elements_on_line=10000 +resharper_max_attribute_length_for_same_line=38 +resharper_max_enum_members_on_line=1 +resharper_max_formal_parameters_on_line=10000 +resharper_max_initializer_elements_on_line=1 +resharper_max_invocation_arguments_on_line=10000 +resharper_media_query_style=same_line +resharper_member_initializer_list_style=do_not_change resharper_method_or_operator_body=expression_body +resharper_min_blank_lines_after_imports=0 +resharper_min_blank_lines_around_fields=0 +resharper_min_blank_lines_around_functions=1 +resharper_min_blank_lines_around_types=1 +resharper_min_blank_lines_between_declarations=1 +resharper_namespace_declaration_braces=next_line +resharper_namespace_indentation=all +resharper_nested_ternary_style=autodetect +resharper_new_line_before_enumerators=true +resharper_normalize_tag_names=false +resharper_no_indent_inside_elements=html,body,thead,tbody,tfoot +resharper_no_indent_inside_if_element_longer_than=200 +resharper_object_creation_when_type_evident=target_typed +resharper_object_creation_when_type_not_evident=explicitly_typed +resharper_old_engine=false +resharper_options_braces_pointy=false +resharper_outdent_binary_ops=true +resharper_outdent_binary_pattern_ops=false +resharper_outdent_commas=false +resharper_outdent_dots=false +resharper_outdent_namespace_member=false +resharper_outdent_statement_labels=false +resharper_outdent_ternary_ops=false +resharper_parentheses_non_obvious_operations=none, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, bitwise_and +resharper_parentheses_redundancy_style=remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations=false +resharper_pi_attributes_indent=align_by_first_attribute resharper_place_attribute_on_same_line=false +resharper_place_class_decorator_on_the_same_line=false +resharper_place_comments_at_first_column=false +resharper_place_constructor_initializer_on_same_line=false +resharper_place_each_decorator_on_new_line=false +resharper_place_event_attribute_on_same_line=false +resharper_place_expr_accessor_on_single_line=true +resharper_place_expr_method_on_single_line=false +resharper_place_expr_property_on_single_line=false +resharper_place_field_decorator_on_the_same_line=false +resharper_place_linq_into_on_new_line=true +resharper_place_method_decorator_on_the_same_line=false +resharper_place_namespace_definitions_on_same_line=false +resharper_place_property_attribute_on_same_line=false +resharper_place_property_decorator_on_the_same_line=false +resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line +resharper_place_simple_embedded_statement_on_same_line=false +resharper_place_simple_enum_on_single_line=true +resharper_place_simple_initializer_on_single_line=true +resharper_place_simple_property_pattern_on_single_line=true +resharper_place_simple_switch_expression_on_single_line=true +resharper_place_template_args_on_new_line=false +resharper_place_type_constraints_on_same_line=true +resharper_prefer_explicit_discard_declaration=false +resharper_prefer_separate_deconstructed_variables_declaration=false +resharper_preserve_spaces_inside_tags=pre,textarea +resharper_properties_style=separate_lines_for_nonsingle +resharper_protobuf_brace_style=end_of_line +resharper_protobuf_empty_block_style=together_same_line +resharper_protobuf_insert_final_newline=false +resharper_protobuf_max_line_length=120 +resharper_protobuf_wrap_lines=true +resharper_qualified_using_at_nested_scope=false +resharper_quote_style=doublequoted +resharper_razor_prefer_qualified_reference=true +resharper_remove_blank_lines_near_braces=false +resharper_remove_blank_lines_near_braces_in_code=true +resharper_remove_blank_lines_near_braces_in_declarations=true +resharper_remove_this_qualifier=true +resharper_requires_expression_braces=next_line +resharper_resx_attribute_indent=single_indent +resharper_resx_insert_final_newline=false +resharper_resx_linebreak_before_elements= +resharper_resx_max_blank_lines_between_tags=0 +resharper_resx_max_line_length=2147483647 +resharper_resx_pi_attribute_style=do_not_touch +resharper_resx_space_before_self_closing=false +resharper_resx_wrap_lines=false +resharper_resx_wrap_tags_and_pi=false +resharper_resx_wrap_text=false +resharper_selector_style=same_line +resharper_show_autodetect_configure_formatting_tip=true +resharper_simple_blocks=do_not_change +resharper_simple_block_style=do_not_change +resharper_simple_case_statement_style=do_not_change +resharper_simple_embedded_statement_style=do_not_change +resharper_single_statement_function_style=do_not_change +resharper_sort_attributes=false +resharper_sort_class_selectors=false +resharper_sort_usings=true +resharper_sort_usings_lowercase_first=false +resharper_spaces_around_eq_in_attribute=false +resharper_spaces_around_eq_in_pi_attribute=false +resharper_spaces_inside_tags=false +resharper_space_after_arrow=true +resharper_space_after_attributes=true +resharper_space_after_attribute_target_colon=true resharper_space_after_cast=false -resharper_space_within_checked_parentheses=true -resharper_space_within_default_parentheses=true -resharper_space_within_nameof_parentheses=true +resharper_space_after_colon=true +resharper_space_after_colon_in_case=true +resharper_space_after_colon_in_inheritance_clause=true +resharper_space_after_colon_in_type_annotation=true +resharper_space_after_comma=true +resharper_space_after_for_colon=true +resharper_space_after_function_comma=true +resharper_space_after_keywords_in_control_flow_statements=true +resharper_space_after_last_attribute=false +resharper_space_after_last_pi_attribute=false +resharper_space_after_media_colon=true +resharper_space_after_media_comma=true +resharper_space_after_operator_keyword=true +resharper_space_after_property_colon=true +resharper_space_after_property_semicolon=true +resharper_space_after_ptr_in_data_member=true +resharper_space_after_ptr_in_data_members=false +resharper_space_after_ptr_in_method=true +resharper_space_after_ref_in_data_member=true +resharper_space_after_ref_in_data_members=false +resharper_space_after_ref_in_method=true +resharper_space_after_selector_comma=true +resharper_space_after_semicolon_in_for_statement=true +resharper_space_after_separator=false +resharper_space_after_ternary_colon=true +resharper_space_after_ternary_quest=true +resharper_space_after_triple_slash=true +resharper_space_after_type_parameter_constraint_colon=true +resharper_space_around_additive_op=true +resharper_space_around_alias_eq=true +resharper_space_around_assignment_op=true +resharper_space_around_assignment_operator=true +resharper_space_around_attribute_match_operator=false +resharper_space_around_deref_in_trailing_return_type=true +resharper_space_around_lambda_arrow=true +resharper_space_around_member_access_operator=false +resharper_space_around_operator=true +resharper_space_around_pipe_or_amper_in_type_usage=true +resharper_space_around_relational_op=true +resharper_space_around_selector_operator=true +resharper_space_around_shift_op=true +resharper_space_around_stmt_colon=true +resharper_space_around_ternary_operator=true +resharper_space_before_array_rank_parentheses=false +resharper_space_before_arrow=true +resharper_space_before_attribute_target_colon=false +resharper_space_before_checked_parentheses=false +resharper_space_before_colon=false +resharper_space_before_colon_in_case=false +resharper_space_before_colon_in_inheritance_clause=true +resharper_space_before_colon_in_type_annotation=false +resharper_space_before_comma=false +resharper_space_before_default_parentheses=false +resharper_space_before_empty_invocation_parentheses=false +resharper_space_before_empty_method_parentheses=false +resharper_space_before_for_colon=true +resharper_space_before_function_comma=false +resharper_space_before_initializer_braces=false +resharper_space_before_invocation_parentheses=false +resharper_space_before_label_colon=false +resharper_space_before_lambda_parentheses=false +resharper_space_before_media_colon=false +resharper_space_before_media_comma=false +resharper_space_before_method_parentheses=false +resharper_space_before_nameof_parentheses=false +resharper_space_before_new_parentheses=false +resharper_space_before_nullable_mark=false +resharper_space_before_open_square_brackets=false +resharper_space_before_pointer_asterik_declaration=false +resharper_space_before_property_colon=false +resharper_space_before_property_semicolon=false +resharper_space_before_ptr_in_abstract_decl=false +resharper_space_before_ptr_in_data_member=false +resharper_space_before_ptr_in_data_members=true +resharper_space_before_ptr_in_method=false +resharper_space_before_ref_in_abstract_decl=false +resharper_space_before_ref_in_data_member=false +resharper_space_before_ref_in_data_members=true +resharper_space_before_ref_in_method=false +resharper_space_before_selector_comma=false +resharper_space_before_semicolon=false +resharper_space_before_semicolon_in_for_statement=false +resharper_space_before_separator=false +resharper_space_before_singleline_accessorholder=true +resharper_space_before_sizeof_parentheses=false +resharper_space_before_template_args=false +resharper_space_before_template_params=true +resharper_space_before_ternary_colon=true +resharper_space_before_ternary_quest=true +resharper_space_before_trailing_comment=true +resharper_space_before_typeof_parentheses=false +resharper_space_before_type_argument_angle=false +resharper_space_before_type_parameters_brackets=false +resharper_space_before_type_parameter_angle=false +resharper_space_before_type_parameter_constraint_colon=true +resharper_space_before_type_parameter_parentheses=true +resharper_space_between_accessors_in_singleline_property=true +resharper_space_between_attribute_sections=true +resharper_space_between_closing_angle_brackets_in_template_args=false +resharper_space_between_empty_square_brackets=false +resharper_space_between_keyword_and_expression=true +resharper_space_between_keyword_and_type=true +resharper_space_between_method_call_empty_parameter_list_parentheses=false +resharper_space_between_method_call_name_and_opening_parenthesis=false +resharper_space_between_method_call_parameter_list_parentheses=false +resharper_space_between_method_declaration_empty_parameter_list_parentheses=false +resharper_space_between_method_declaration_name_and_open_parenthesis=false +resharper_space_between_method_declaration_parameter_list_parentheses=false +resharper_space_between_parentheses_of_control_flow_statements=false +resharper_space_between_square_brackets=false +resharper_space_between_typecast_parentheses=false +resharper_space_colon_after=true +resharper_space_colon_before=false +resharper_space_comma=true +resharper_space_equals=true +resharper_space_inside_braces=true +resharper_space_in_singleline_accessorholder=true +resharper_space_in_singleline_anonymous_method=true +resharper_space_in_singleline_method=true +resharper_space_near_postfix_and_prefix_op=false +resharper_space_within_array_initialization_braces=false +resharper_space_within_array_rank_empty_parentheses=false +resharper_space_within_array_rank_parentheses=false +resharper_space_within_attribute_angles=false +resharper_space_within_attribute_match_brackets=false +resharper_space_within_checked_parentheses=false +resharper_space_within_default_parentheses=false +resharper_space_within_empty_braces=true +resharper_space_within_empty_initializer_braces=false +resharper_space_within_empty_invocation_parentheses=false +resharper_space_within_empty_method_parentheses=false +resharper_space_within_empty_object_literal_braces=false +resharper_space_within_empty_template_params=false +resharper_space_within_expression_parentheses=false +resharper_space_within_function_parentheses=false +resharper_space_within_import_braces=true +resharper_space_within_initializer_braces=false +resharper_space_within_invocation_parentheses=false +resharper_space_within_media_block=true +resharper_space_within_media_parentheses=false +resharper_space_within_method_parentheses=false +resharper_space_within_nameof_parentheses=false +resharper_space_within_new_parentheses=false +resharper_space_within_object_literal_braces=true +resharper_space_within_parentheses=false +resharper_space_within_property_block=true resharper_space_within_single_line_array_initializer_braces=true -resharper_space_within_sizeof_parentheses=true -resharper_space_within_typeof_parentheses=true -resharper_space_within_type_argument_angles=true -resharper_space_within_type_parameter_angles=true +resharper_space_within_sizeof_parentheses=false +resharper_space_within_template_args=false +resharper_space_within_template_argument=false +resharper_space_within_template_params=false +resharper_space_within_tuple_parentheses=false +resharper_space_within_typeof_parentheses=false +resharper_space_within_type_argument_angles=false +resharper_space_within_type_parameters_brackets=false +resharper_space_within_type_parameter_angles=false +resharper_space_within_type_parameter_parentheses=false +resharper_special_else_if_treatment=true +resharper_static_members_qualify_members=none +resharper_static_members_qualify_with=declared_type +resharper_stick_comment=true +resharper_support_vs_event_naming_pattern=true +resharper_termination_style=ensure_semicolon +resharper_toplevel_function_declaration_return_type_style=do_not_change +resharper_toplevel_function_definition_return_type_style=do_not_change +resharper_trailing_comma_in_multiline_lists=true +resharper_trailing_comma_in_singleline_lists=false +resharper_types_braces=end_of_line +resharper_use_continuous_indent_inside_initializer_braces=true +resharper_use_continuous_indent_inside_parens=true +resharper_use_continuous_line_indent_in_expression_braces=false +resharper_use_continuous_line_indent_in_method_pars=false +resharper_use_heuristics_for_body_style=true +resharper_use_indents_from_main_language_in_file=true +resharper_use_indent_from_previous_element=true resharper_use_indent_from_vs=false -resharper_wrap_lines=true +resharper_use_roslyn_logic_for_evident_types=false +resharper_vb_align_multiline_argument=true +resharper_vb_align_multiline_expression=true +resharper_vb_align_multiline_parameter=true +resharper_vb_align_multiple_declaration=true +resharper_vb_insert_final_newline=false +resharper_vb_max_line_length=120 +resharper_vb_place_field_attribute_on_same_line=true +resharper_vb_place_method_attribute_on_same_line=false +resharper_vb_place_type_attribute_on_same_line=false +resharper_vb_prefer_qualified_reference=false +resharper_vb_space_after_unary_operator=true +resharper_vb_space_around_multiplicative_op=false +resharper_vb_wrap_arguments_style=wrap_if_long +resharper_vb_wrap_before_binary_opsign=false +resharper_vb_wrap_lines=true +resharper_vb_wrap_parameters_style=wrap_if_long +resharper_wrap_after_binary_opsign=true +resharper_wrap_after_declaration_lpar=false +resharper_wrap_after_dot=false +resharper_wrap_after_dot_in_method_calls=false +resharper_wrap_after_expression_lbrace=true +resharper_wrap_after_invocation_lpar=false +resharper_wrap_around_elements=true +resharper_wrap_array_initializer_style=chop_always +resharper_wrap_array_literals=chop_if_long +resharper_wrap_base_clause_style=wrap_if_long +resharper_wrap_before_arrow_with_expressions=true +resharper_wrap_before_binary_pattern_op=true +resharper_wrap_before_colon=false +resharper_wrap_before_comma=false +resharper_wrap_before_comma_in_base_clause=false +resharper_wrap_before_declaration_lpar=false +resharper_wrap_before_declaration_rpar=false +resharper_wrap_before_dot=true +resharper_wrap_before_eq=false +resharper_wrap_before_expression_rbrace=true +resharper_wrap_before_extends_colon=false +resharper_wrap_before_first_type_parameter_constraint=false +resharper_wrap_before_invocation_lpar=false +resharper_wrap_before_invocation_rpar=false +resharper_wrap_before_linq_expression=false +resharper_wrap_before_ternary_opsigns=true +resharper_wrap_before_type_parameter_langle=false +resharper_wrap_braced_init_list_style=wrap_if_long +resharper_wrap_chained_binary_expressions=chop_if_long +resharper_wrap_chained_binary_patterns=wrap_if_long +resharper_wrap_chained_method_calls=wrap_if_long +resharper_wrap_ctor_initializer_style=wrap_if_long +resharper_wrap_enumeration_style=chop_if_long +resharper_wrap_enum_declaration=chop_always +resharper_wrap_enum_style=do_not_change +resharper_wrap_extends_list_style=wrap_if_long +resharper_wrap_imports=chop_if_long +resharper_wrap_multiple_declaration_style=chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style=chop_if_long +resharper_wrap_object_literals=chop_if_long +resharper_wrap_property_pattern=chop_if_long +resharper_wrap_switch_expression=chop_always +resharper_wrap_ternary_expr_style=chop_if_long +resharper_wrap_union_type_usage=chop_if_long +resharper_wrap_verbatim_interpolated_strings=no_wrap +resharper_xmldoc_attribute_indent=single_indent +resharper_xmldoc_insert_final_newline=false +resharper_xmldoc_linebreak_before_elements=summary,remarks,example,returns,param,typeparam,value,para +resharper_xmldoc_max_blank_lines_between_tags=0 +resharper_xmldoc_max_line_length=120 +resharper_xmldoc_pi_attribute_style=do_not_touch +resharper_xmldoc_space_before_self_closing=true +resharper_xmldoc_wrap_lines=true +resharper_xmldoc_wrap_tags_and_pi=true +resharper_xmldoc_wrap_text=true +resharper_xml_attribute_indent=align_by_first_attribute +resharper_xml_insert_final_newline=false +resharper_xml_linebreak_before_elements= +resharper_xml_max_blank_lines_between_tags=2 +resharper_xml_max_line_length=120 +resharper_xml_pi_attribute_style=do_not_touch +resharper_xml_space_before_self_closing=true +resharper_xml_wrap_lines=true +resharper_xml_wrap_tags_and_pi=true +resharper_xml_wrap_text=false # ReSharper inspection severities +resharper_abstract_class_constructor_can_be_made_protected_highlighting=hint +resharper_access_rights_in_text_highlighting=warning +resharper_access_to_disposed_closure_highlighting=warning +resharper_access_to_for_each_variable_in_closure_highlighting=warning +resharper_access_to_modified_closure_highlighting=warning +resharper_access_to_static_member_via_derived_type_highlighting=warning +resharper_address_of_marshal_by_ref_object_highlighting=warning +resharper_amd_dependency_path_problem_highlighting=none +resharper_amd_external_module_highlighting=suggestion +resharper_angular_html_banana_highlighting=warning +resharper_annotate_can_be_null_parameter_highlighting=none +resharper_annotate_can_be_null_type_member_highlighting=none +resharper_annotate_not_null_parameter_highlighting=none +resharper_annotate_not_null_type_member_highlighting=none +resharper_annotation_conflict_in_hierarchy_highlighting=warning +resharper_annotation_redundancy_at_value_type_highlighting=warning +resharper_annotation_redundancy_in_hierarchy_highlighting=warning +resharper_arguments_style_anonymous_function_highlighting=hint +resharper_arguments_style_literal_highlighting=hint +resharper_arguments_style_named_expression_highlighting=hint +resharper_arguments_style_other_highlighting=hint +resharper_arguments_style_string_literal_highlighting=hint +resharper_arrange_accessor_owner_body_highlighting=suggestion +resharper_arrange_attributes_highlighting=none +resharper_arrange_constructor_or_destructor_body_highlighting=hint +resharper_arrange_default_value_when_type_evident_highlighting=suggestion +resharper_arrange_default_value_when_type_not_evident_highlighting=hint +resharper_arrange_local_function_body_highlighting=hint +resharper_arrange_method_or_operator_body_highlighting=hint +resharper_arrange_missing_parentheses_highlighting=hint +resharper_arrange_namespace_body_highlighting=hint +resharper_arrange_object_creation_when_type_evident_highlighting=suggestion +resharper_arrange_object_creation_when_type_not_evident_highlighting=hint resharper_arrange_redundant_parentheses_highlighting=hint +resharper_arrange_static_member_qualifier_highlighting=hint resharper_arrange_this_qualifier_highlighting=hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting=hint +resharper_arrange_trailing_comma_in_singleline_lists_highlighting=hint resharper_arrange_type_member_modifiers_highlighting=hint resharper_arrange_type_modifiers_highlighting=hint +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting=suggestion +resharper_asp_content_placeholder_not_resolved_highlighting=error +resharper_asp_custom_page_parser_filter_type_highlighting=warning +resharper_asp_dead_code_highlighting=warning +resharper_asp_entity_highlighting=warning +resharper_asp_image_highlighting=warning +resharper_asp_invalid_control_type_highlighting=error +resharper_asp_not_resolved_highlighting=error +resharper_asp_ods_method_reference_resolve_error_highlighting=error +resharper_asp_resolve_warning_highlighting=warning +resharper_asp_skin_not_resolved_highlighting=error +resharper_asp_tag_attribute_with_optional_value_highlighting=warning +resharper_asp_theme_not_resolved_highlighting=error +resharper_asp_unused_register_directive_highlighting_highlighting=warning +resharper_asp_warning_highlighting=warning +resharper_assigned_value_is_never_used_highlighting=warning +resharper_assigned_value_wont_be_assigned_to_corresponding_field_highlighting=warning +resharper_assignment_in_conditional_expression_highlighting=warning +resharper_assignment_in_condition_expression_highlighting=warning +resharper_assignment_is_fully_discarded_highlighting=warning +resharper_assign_null_to_not_null_attribute_highlighting=warning +resharper_assign_to_constant_highlighting=error +resharper_assign_to_implicit_global_in_function_scope_highlighting=warning +resharper_asxx_path_error_highlighting=warning +resharper_async_iterator_invocation_without_await_foreach_highlighting=warning +resharper_async_void_lambda_highlighting=warning +resharper_async_void_method_highlighting=none +resharper_auto_property_can_be_made_get_only_global_highlighting=suggestion +resharper_auto_property_can_be_made_get_only_local_highlighting=suggestion +resharper_bad_attribute_brackets_spaces_highlighting=none +resharper_bad_braces_spaces_highlighting=none +resharper_bad_child_statement_indent_highlighting=warning +resharper_bad_colon_spaces_highlighting=none +resharper_bad_comma_spaces_highlighting=none +resharper_bad_control_braces_indent_highlighting=suggestion +resharper_bad_control_braces_line_breaks_highlighting=none +resharper_bad_declaration_braces_indent_highlighting=none +resharper_bad_declaration_braces_line_breaks_highlighting=none +resharper_bad_empty_braces_line_breaks_highlighting=none +resharper_bad_expression_braces_indent_highlighting=none +resharper_bad_expression_braces_line_breaks_highlighting=none +resharper_bad_generic_brackets_spaces_highlighting=none +resharper_bad_indent_highlighting=none +resharper_bad_linq_line_breaks_highlighting=none +resharper_bad_list_line_breaks_highlighting=none +resharper_bad_member_access_spaces_highlighting=none +resharper_bad_namespace_braces_indent_highlighting=none +resharper_bad_parens_line_breaks_highlighting=none +resharper_bad_parens_spaces_highlighting=none +resharper_bad_preprocessor_indent_highlighting=none +resharper_bad_semicolon_spaces_highlighting=none +resharper_bad_spaces_after_keyword_highlighting=none +resharper_bad_square_brackets_spaces_highlighting=none +resharper_bad_switch_braces_indent_highlighting=none +resharper_bad_symbol_spaces_highlighting=none +resharper_base_member_has_params_highlighting=warning +resharper_base_method_call_with_default_parameter_highlighting=warning +resharper_base_object_equals_is_object_equals_highlighting=warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting=warning +resharper_bitwise_operator_on_enum_without_flags_highlighting=warning +resharper_block_scope_redeclaration_highlighting=error resharper_built_in_type_reference_style_for_member_access_highlighting=hint resharper_built_in_type_reference_style_highlighting=hint +resharper_by_ref_argument_is_volatile_field_highlighting=warning +resharper_caller_callee_using_error_highlighting=error +resharper_caller_callee_using_highlighting=warning +resharper_cannot_apply_equality_operator_to_type_highlighting=warning +resharper_center_tag_is_obsolete_highlighting=warning +resharper_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_check_for_reference_equality_instead_3_highlighting=suggestion +resharper_check_for_reference_equality_instead_4_highlighting=suggestion +resharper_check_namespace_highlighting=warning +resharper_class_cannot_be_instantiated_highlighting=warning +resharper_class_can_be_sealed_global_highlighting=none +resharper_class_can_be_sealed_local_highlighting=none +resharper_class_highlighting=suggestion +resharper_class_never_instantiated_global_highlighting=suggestion +resharper_class_never_instantiated_local_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_global_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_local_highlighting=suggestion +resharper_clear_attribute_is_obsolete_all_highlighting=warning +resharper_clear_attribute_is_obsolete_highlighting=warning +resharper_closure_on_modified_variable_highlighting=warning +resharper_coerced_equals_using_highlighting=warning +resharper_coerced_equals_using_with_null_undefined_highlighting=none +resharper_collection_never_queried_global_highlighting=warning +resharper_collection_never_queried_local_highlighting=warning +resharper_collection_never_updated_global_highlighting=warning +resharper_collection_never_updated_local_highlighting=warning +resharper_comma_not_valid_here_highlighting=error +resharper_comment_typo_highlighting=suggestion +resharper_common_js_external_module_highlighting=suggestion +resharper_compare_non_constrained_generic_with_null_highlighting=none +resharper_compare_of_floats_by_equality_operator_highlighting=none +resharper_conditional_ternary_equal_branch_highlighting=warning +resharper_condition_is_always_const_highlighting=warning +resharper_condition_is_always_true_or_false_highlighting=warning +resharper_confusing_char_as_integer_in_constructor_highlighting=warning +resharper_constant_conditional_access_qualifier_highlighting=warning +resharper_constant_null_coalescing_condition_highlighting=warning +resharper_constructor_call_not_used_highlighting=warning +resharper_constructor_initializer_loop_highlighting=warning +resharper_container_annotation_redundancy_highlighting=warning +resharper_context_value_is_provided_highlighting=none +resharper_contract_annotation_not_parsed_highlighting=warning +resharper_convert_closure_to_method_group_highlighting=suggestion +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting=hint +resharper_convert_if_do_to_while_highlighting=suggestion +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_expression_highlighting=suggestion +resharper_convert_if_statement_to_return_statement_highlighting=hint +resharper_convert_if_statement_to_switch_expression_highlighting=hint +resharper_convert_if_statement_to_switch_statement_highlighting=hint +resharper_convert_if_to_or_expression_highlighting=suggestion +resharper_convert_nullable_to_short_form_highlighting=suggestion +resharper_convert_switch_statement_to_switch_expression_highlighting=hint +resharper_convert_to_auto_property_highlighting=suggestion +resharper_convert_to_auto_property_when_possible_highlighting=hint +resharper_convert_to_auto_property_with_private_setter_highlighting=hint +resharper_convert_to_compound_assignment_highlighting=hint +resharper_convert_to_constant_global_highlighting=hint +resharper_convert_to_constant_local_highlighting=hint +resharper_convert_to_lambda_expression_highlighting=suggestion +resharper_convert_to_lambda_expression_when_possible_highlighting=none +resharper_convert_to_local_function_highlighting=suggestion +resharper_convert_to_null_coalescing_compound_assignment_highlighting=suggestion +resharper_convert_to_primary_constructor_highlighting=suggestion +resharper_convert_to_static_class_highlighting=suggestion +resharper_convert_to_using_declaration_highlighting=suggestion +resharper_convert_to_vb_auto_property_highlighting=suggestion +resharper_convert_to_vb_auto_property_when_possible_highlighting=hint +resharper_convert_to_vb_auto_property_with_private_setter_highlighting=hint +resharper_convert_type_check_pattern_to_null_check_highlighting=warning +resharper_convert_type_check_to_null_check_highlighting=warning +resharper_co_variant_array_conversion_highlighting=warning +resharper_cpp_abstract_class_without_specifier_highlighting=warning +resharper_cpp_abstract_final_class_highlighting=warning +resharper_cpp_abstract_virtual_function_call_in_ctor_highlighting=error +resharper_cpp_access_specifier_with_no_declarations_highlighting=suggestion +resharper_cpp_assigned_value_is_never_used_highlighting=warning +resharper_cpp_awaiter_type_is_not_class_highlighting=warning +resharper_cpp_bad_angle_brackets_spaces_highlighting=none +resharper_cpp_bad_braces_spaces_highlighting=none +resharper_cpp_bad_child_statement_indent_highlighting=none +resharper_cpp_bad_colon_spaces_highlighting=none +resharper_cpp_bad_comma_spaces_highlighting=none +resharper_cpp_bad_control_braces_indent_highlighting=none +resharper_cpp_bad_control_braces_line_breaks_highlighting=none +resharper_cpp_bad_declaration_braces_indent_highlighting=none +resharper_cpp_bad_declaration_braces_line_breaks_highlighting=none +resharper_cpp_bad_empty_braces_line_breaks_highlighting=none +resharper_cpp_bad_expression_braces_indent_highlighting=none +resharper_cpp_bad_expression_braces_line_breaks_highlighting=none +resharper_cpp_bad_indent_highlighting=none +resharper_cpp_bad_list_line_breaks_highlighting=none +resharper_cpp_bad_member_access_spaces_highlighting=none +resharper_cpp_bad_namespace_braces_indent_highlighting=none +resharper_cpp_bad_parens_line_breaks_highlighting=none +resharper_cpp_bad_parens_spaces_highlighting=none +resharper_cpp_bad_semicolon_spaces_highlighting=none +resharper_cpp_bad_spaces_after_keyword_highlighting=none +resharper_cpp_bad_square_brackets_spaces_highlighting=none +resharper_cpp_bad_switch_braces_indent_highlighting=none +resharper_cpp_bad_symbol_spaces_highlighting=none +resharper_cpp_boolean_increment_expression_highlighting=warning +resharper_cpp_boost_format_bad_code_highlighting=warning +resharper_cpp_boost_format_legacy_code_highlighting=suggestion +resharper_cpp_boost_format_mixed_args_highlighting=error +resharper_cpp_boost_format_too_few_args_highlighting=error +resharper_cpp_boost_format_too_many_args_highlighting=warning +resharper_cpp_clang_tidy_abseil_duration_addition_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_conversion_cast_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_division_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_float_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_scale_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_unnecessary_conversion_highlighting=none +resharper_cpp_clang_tidy_abseil_faster_strsplit_delimiter_highlighting=none +resharper_cpp_clang_tidy_abseil_no_internal_dependencies_highlighting=none +resharper_cpp_clang_tidy_abseil_no_namespace_highlighting=none +resharper_cpp_clang_tidy_abseil_redundant_strcat_calls_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_startswith_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_str_contains_highlighting=none +resharper_cpp_clang_tidy_abseil_str_cat_append_highlighting=none +resharper_cpp_clang_tidy_abseil_time_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_time_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_upgrade_duration_conversions_highlighting=none +resharper_cpp_clang_tidy_altera_id_dependent_backward_branch_highlighting=none +resharper_cpp_clang_tidy_altera_kernel_name_restriction_highlighting=none +resharper_cpp_clang_tidy_altera_single_work_item_barrier_highlighting=none +resharper_cpp_clang_tidy_altera_struct_pack_align_highlighting=none +resharper_cpp_clang_tidy_altera_unroll_loops_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept4_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_creat_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_dup_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_fopen_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_memfd_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_open_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe2_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_socket_highlighting=none +resharper_cpp_clang_tidy_android_comparison_in_temp_failure_retry_highlighting=none +resharper_cpp_clang_tidy_boost_use_to_string_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_argument_comment_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_assert_side_effect_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bad_signal_to_kill_thread_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bool_pointer_implicit_conversion_highlighting=none +resharper_cpp_clang_tidy_bugprone_branch_clone_highlighting=warning +resharper_cpp_clang_tidy_bugprone_copy_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dangling_handle_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dynamic_static_initializers_highlighting=warning +resharper_cpp_clang_tidy_bugprone_easily_swappable_parameters_highlighting=none +resharper_cpp_clang_tidy_bugprone_exception_escape_highlighting=none +resharper_cpp_clang_tidy_bugprone_fold_init_type_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forwarding_reference_overload_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forward_declaration_namespace_highlighting=warning +resharper_cpp_clang_tidy_bugprone_implicit_widening_of_multiplication_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_inaccurate_erase_highlighting=warning +resharper_cpp_clang_tidy_bugprone_incorrect_roundings_highlighting=warning +resharper_cpp_clang_tidy_bugprone_infinite_loop_highlighting=warning +resharper_cpp_clang_tidy_bugprone_integer_division_highlighting=warning +resharper_cpp_clang_tidy_bugprone_lambda_function_name_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_parentheses_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_repeated_side_effects_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_operator_in_strlen_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_pointer_arithmetic_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_widening_cast_highlighting=warning +resharper_cpp_clang_tidy_bugprone_move_forwarding_reference_highlighting=warning +resharper_cpp_clang_tidy_bugprone_multiple_statement_macro_highlighting=warning +resharper_cpp_clang_tidy_bugprone_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_not_null_terminated_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_no_escape_highlighting=warning +resharper_cpp_clang_tidy_bugprone_parent_virtual_call_highlighting=warning +resharper_cpp_clang_tidy_bugprone_posix_return_highlighting=warning +resharper_cpp_clang_tidy_bugprone_redundant_branch_condition_highlighting=warning +resharper_cpp_clang_tidy_bugprone_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signal_handler_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signed_char_misuse_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_container_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_expression_highlighting=warning +resharper_cpp_clang_tidy_bugprone_spuriously_wake_up_functions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_integer_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_literal_with_embedded_nul_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_enum_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_memset_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_missing_comma_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_semicolon_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_string_compare_highlighting=warning +resharper_cpp_clang_tidy_bugprone_swapped_arguments_highlighting=warning +resharper_cpp_clang_tidy_bugprone_terminating_continue_highlighting=warning +resharper_cpp_clang_tidy_bugprone_throw_keyword_missing_highlighting=warning +resharper_cpp_clang_tidy_bugprone_too_small_loop_variable_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undefined_memory_manipulation_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undelegated_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unhandled_exception_at_new_highlighting=none +resharper_cpp_clang_tidy_bugprone_unhandled_self_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_raii_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_return_value_highlighting=warning +resharper_cpp_clang_tidy_bugprone_use_after_move_highlighting=warning +resharper_cpp_clang_tidy_bugprone_virtual_near_miss_highlighting=suggestion +resharper_cpp_clang_tidy_cert_con36_c_highlighting=none +resharper_cpp_clang_tidy_cert_con54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl03_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl16_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl21_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl37_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl51_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_dcl59_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_env33_c_highlighting=none +resharper_cpp_clang_tidy_cert_err09_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err34_c_highlighting=suggestion +resharper_cpp_clang_tidy_cert_err52_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err58_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err60_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_err61_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_fio38_c_highlighting=none +resharper_cpp_clang_tidy_cert_flp30_c_highlighting=warning +resharper_cpp_clang_tidy_cert_mem57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_msc30_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc32_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_msc51_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop11_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_pos44_c_highlighting=none +resharper_cpp_clang_tidy_cert_pos47_c_highlighting=none +resharper_cpp_clang_tidy_cert_sig30_c_highlighting=none +resharper_cpp_clang_tidy_cert_str34_c_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_google_g_test_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_cast_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_std_c_library_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_trust_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_builtin_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_no_return_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_divide_zero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_dynamic_type_propagation_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_nonnil_string_constants_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_non_null_param_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_null_dereference_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_address_escape_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_addr_escape_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_undefined_binary_operator_result_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_array_subscript_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_assign_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_branch_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_captured_block_variable_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_undef_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_vla_size_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_inner_pointer_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_move_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_leaks_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_placement_new_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_pure_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_self_assignment_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_smart_ptr_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_virtual_call_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_deadcode_dead_stores_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_fuchsia_handle_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullability_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_dereferenced_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_uninitialized_object_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_mpi_mpi_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_empty_localization_context_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_non_localized_string_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_os_object_c_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_gcd_antipattern_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_padding_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_portability_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_at_sync_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_autorelease_write_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_class_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_incompatible_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_loops_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_missing_super_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_nil_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_non_nil_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_autorelease_pool_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_obj_c_generics_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_run_loop_autorelease_leak_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_self_init_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_super_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_unused_ivars_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_variadic_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_number_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_retain_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_out_of_bounds_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_pointer_sized_values_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_mig_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_ns_or_cf_error_deref_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_number_object_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_obj_c_property_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_os_object_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_sec_keychain_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_float_loop_counter_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcmp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcopy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bzero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_decode_value_of_obj_c_type_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_deprecated_or_unsafe_buffer_handling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_getpw_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_gets_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mkstemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mktemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_rand_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_security_syntax_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_strcpy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_unchecked_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_bad_size_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_c_string_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_null_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_dynamic_memory_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_sizeof_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_mismatched_deallocator_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_copy_to_self_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_uninitialized_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_unterminated_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_valist_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_no_uncounted_member_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_ref_cntbl_base_virtual_dtor_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_uncounted_lambda_captures_checker_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_absolute_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_abstract_final_class_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_abstract_vbase_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_packed_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_temporary_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_aix_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_align_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_with_align_alignof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_ellipsis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_member_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_reversed_operator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_analyzer_incompatible_plugin_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anonymous_pack_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_bridge_casts_disallowed_in_nonarc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_maybe_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_non_pod_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_perform_selector_leaks_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_retain_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_unsafe_retained_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_argument_outside_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_asm_operand_widths_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assign_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assume_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atimport_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_implicit_seq_cst_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_atomic_memory_ordering_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_property_with_user_defined_accessor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_attribute_packed_for_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_at_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_disable_vptr_sanitizer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_import_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_storage_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_var_id_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_avr_rtlib_linking_quirks_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_backslash_newline_escape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bad_function_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_binding_in_condition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bind_to_temporary_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_conditional_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_block_capture_autoreleasing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_operation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_braced_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bridge_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_assume_aligned_alignment_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_builtin_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_memcpy_chk_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_requires_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c2x_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_c99_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_called_once_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_call_to_pure_virtual_from_ctor_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_align_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_calling_convention_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_function_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_of_sel_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_unrelated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cf_string_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_char_subscripts_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_clang_cl_pch_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_class_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_class_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cmse_union_leak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comma_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compare_distinct_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_completion_handler_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_complex_component_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_space_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_concepts_ts_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_conditional_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conditional_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_config_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_evaluated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_logical_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constexpr_not_const_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_consumed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_coroutine_missing_unhandled_exception_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_covered_switch_default_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_deprecated_writable_strings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_reserved_user_defined_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_inline_namespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_narrowing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_mangling_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2b_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_bind_to_temporary_copy_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_extra_semi_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_local_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_unnamed_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_binary_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cstring_format_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ctad_maybe_unsupported_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ctu_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cuda_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_custom_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cxx_attribute_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_else_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_gsl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_initializer_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_darwin_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_debug_compression_unavailable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_declaration_after_statement_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_defaulted_function_deleted_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_delegating_ctor_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_incomplete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_altivec_src_compat_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_array_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_comma_subscript_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_implementations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_isa_usage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_perform_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_this_capture_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_volatile_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_direct_ivar_access_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_disabled_macro_expansion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_distributed_object_modifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_division_by_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllexport_explicit_instantiation_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllimport_static_field_def_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dll_attribute_on_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_deprecated_sync_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_html_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_unknown_command_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_dollar_in_identifier_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_double_promotion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_decl_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_arg_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_class_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_embedded_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_decomposition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_init_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_translation_unit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_encode_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_too_large_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_error_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_exceptions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_excess_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_exit_time_destructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_expansion_to_defined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_initialize_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_ownership_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_unnamed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_using_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extern_c_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_extern_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_tokens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_final_dtor_non_final_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fixed_enum_extension_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_fixed_point_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flag_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flexible_array_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_equal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_overflow_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_zero_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_extra_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_insufficient_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_invalid_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_nonliteral_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_non_iso_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_security_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_type_confusion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_zero_length_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fortify_source_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_for_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_four_char_constants_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_framework_include_private_from_public_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_larger_than_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_free_nonheap_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_def_in_objc_container_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_multiversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gcc_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_constructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_isel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_alignof_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_anonymous_struct_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_gnu_array_member_paren_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_auto_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_case_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_complex_integer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_compound_literal_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_conditional_omitted_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_union_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_folding_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_imaginary_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_include_next_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_inline_cpp_without_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_label_as_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_redeclared_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_statement_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_string_literal_operator_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_union_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_variable_sized_type_not_at_end_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_zero_variadic_macro_arguments_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_hygiene_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_hip_only_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_idiomatic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_availability_without_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_optimization_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_intrinsic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_optimize_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_qualifiers_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_implicitly_unsigned_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_const_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_conversion_floating_point_to_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_exception_spec_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_per_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fixed_point_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_function_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_retain_self_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_import_preprocessor_directive_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_absolute_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_function_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_library_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_ms_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_discards_qualifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_property_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_framework_module_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_implementation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_setjmp_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_umbrella_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_dllimport_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_destructor_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_independent_class_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_infinite_recursion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_initializer_overrides_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_injected_class_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_asm_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_namespace_reopened_noninline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_instantiation_after_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_integer_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_interrupt_service_routine_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_in_bool_context_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_void_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_constexpr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_iboutlet_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_initializer_from_system_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_ios_deployment_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_no_builtin_names_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_offsetof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_or_nonexistent_directory_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_pp_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_source_encoding_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_token_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_jump_seh_finally_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_keyword_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_keyword_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_knr_promoted_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_language_extension_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_large_by_value_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_local_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_not_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_op_parentheses_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_malformed_warning_check_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_many_braces_around_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_max_tokens_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_max_unsigned_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memset_transposed_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memsize_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_method_signatures_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_abstract_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_anon_tag_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_charize_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_comment_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_const_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cpp_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_default_arg_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_drectve_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_end_of_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_forward_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exists_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_explicit_constructor_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_fixed_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_flexible_array_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_goto_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_mutable_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_pure_definition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_redeclare_static_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_sealed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_static_assert_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_union_member_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_unqualified_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_using_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_void_pseudo_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_misleading_indentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_parameter_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_return_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_tags_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_braces_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_constinit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_field_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_method_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noescape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototypes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototype_for_cc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_selector_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_variable_declarations_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_misspelled_assumption_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_ambiguous_internal_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_import_nested_redundant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_conflict_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_config_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_import_in_extern_c_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_msvc_not_found_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multichar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multiple_move_vbase_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nested_anon_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_newline_eof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_new_returns_null_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_noderef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonnull_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_system_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_vector_initialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nontrivial_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_c_typedef_for_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_literal_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_framework_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_pod_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_power_of_two_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsconsumed_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsreturns_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ns_object_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_on_arrays_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_declspec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_inferred_on_nested_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullable_to_nonnull_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_character_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_subtraction_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_odr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_old_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_opencl_unsupported_rgba_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp51_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_clauses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_loop_form_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_mapping_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_option_ignored_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ordered_compare_function_pointers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_line_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_scope_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overlength_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_virtual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_t_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_over_aligned_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_packed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_padded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_equality_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pass_failed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pch_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_core_features_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pessimizing_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_arith_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_integer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_sign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_poison_system_directories_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_potentially_evaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_clang_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_messages_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_once_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_system_header_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_predefined_identifier_outside_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_openmp51_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_private_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_out_of_date_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_unprofiled_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_access_dot_syntax_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_attribute_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_property_synthesis_ambiguity_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_psabi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_qualified_void_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_quoted_include_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_bind_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_construct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_readonly_iboutlet_property_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_expr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_forward_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redeclared_class_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reinterpret_base_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_ctor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_init_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_super_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_id_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_macro_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_user_defined_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_retained_language_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_stack_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_std_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_c_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_rewrite_not_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_overloaded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_semicolon_before_method_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sentinel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_serialized_diagnostics_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_modified_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_uncaptured_local_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_negative_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_sign_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shorten64_to32_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_enum_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_unsigned_wchar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_decay_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slash_u_filename_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slh_asm_goto_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sometimes_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_source_uses_openmp_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_spir_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_inline_explicit_instantiation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_local_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_self_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_stdlibcxx_not_found_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_strict_prototypes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strict_selector_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_concatenation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_char_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strlcpy_strlcat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strncat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suggest_destructor_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_suggest_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_super_class_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suspicious_bzero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sync_fetch_and_nand_semantics_changed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_bitwise_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_in_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_out_of_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_objc_bool_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_overlap_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_type_limit_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_undefined_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_char_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_enum_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_zero_compare_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_tautological_value_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tentative_definition_incomplete_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_beta_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_precise_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_verbose_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_trigraphs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typedef_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typename_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_type_safety_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unable_to_open_stats_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unavailable_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undeclared_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_func_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_reinterpret_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_var_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_prefix_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_underaligned_exception_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unevaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_new_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_homoglyph_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_whitespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_zero_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_const_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_cuda_version_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_escape_sequence_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_pragmas_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_sanitizers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_warning_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unnamed_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_internal_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_break_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_loop_increment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_return_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsequenced_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_abs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_availability_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_cb_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_dll_base_class_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_gpopt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_nan_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_target_opt_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unusable_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_const_variable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_exception_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_getter_return_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_label_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_lambda_capture_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_local_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_private_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_property_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_result_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_volatile_lvalue_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_used_but_marked_unused_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_literals_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_variadic_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vector_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vec_elem_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vexing_parse_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_ptr_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_wasm_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_template_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_writable_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_xor_used_as_pow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_zero_as_null_pointer_constant_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_zero_length_array_highlighting=warning +resharper_cpp_clang_tidy_concurrency_mt_unsafe_highlighting=warning +resharper_cpp_clang_tidy_concurrency_thread_canceltype_asynchronous_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_non_const_global_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_c_copy_assignment_signature_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_explicit_virtual_functions_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_init_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_interfaces_global_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_macro_usage_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_no_malloc_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_owning_memory_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_prefer_member_initializer_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_array_to_pointer_decay_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_constant_array_index_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_pointer_arithmetic_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_const_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_cstyle_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_member_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_reinterpret_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_static_cast_downcast_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_union_access_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_vararg_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_slicing_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_special_member_functions_highlighting=suggestion +resharper_cpp_clang_tidy_darwin_avoid_spinlock_highlighting=none +resharper_cpp_clang_tidy_darwin_dispatch_once_nonstatic_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_calls_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_declarations_highlighting=none +resharper_cpp_clang_tidy_fuchsia_header_anon_namespaces_highlighting=none +resharper_cpp_clang_tidy_fuchsia_multiple_inheritance_highlighting=none +resharper_cpp_clang_tidy_fuchsia_overloaded_operator_highlighting=none +resharper_cpp_clang_tidy_fuchsia_statically_constructed_objects_highlighting=none +resharper_cpp_clang_tidy_fuchsia_trailing_return_highlighting=none +resharper_cpp_clang_tidy_fuchsia_virtual_inheritance_highlighting=none +resharper_cpp_clang_tidy_google_build_explicit_make_pair_highlighting=none +resharper_cpp_clang_tidy_google_build_namespaces_highlighting=none +resharper_cpp_clang_tidy_google_build_using_namespace_highlighting=none +resharper_cpp_clang_tidy_google_default_arguments_highlighting=none +resharper_cpp_clang_tidy_google_explicit_constructor_highlighting=none +resharper_cpp_clang_tidy_google_global_names_in_headers_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_nsobject_new_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_throwing_exception_highlighting=none +resharper_cpp_clang_tidy_google_objc_function_naming_highlighting=none +resharper_cpp_clang_tidy_google_objc_global_variable_declaration_highlighting=none +resharper_cpp_clang_tidy_google_readability_avoid_underscore_in_googletest_name_highlighting=none +resharper_cpp_clang_tidy_google_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_google_readability_casting_highlighting=none +resharper_cpp_clang_tidy_google_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_google_readability_namespace_comments_highlighting=none +resharper_cpp_clang_tidy_google_readability_todo_highlighting=none +resharper_cpp_clang_tidy_google_runtime_int_highlighting=none +resharper_cpp_clang_tidy_google_runtime_operator_highlighting=warning +resharper_cpp_clang_tidy_google_upgrade_googletest_case_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_hicpp_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_hicpp_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_hicpp_deprecated_headers_highlighting=none +resharper_cpp_clang_tidy_hicpp_exception_baseclass_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_explicit_conversions_highlighting=none +resharper_cpp_clang_tidy_hicpp_function_size_highlighting=none +resharper_cpp_clang_tidy_hicpp_invalid_access_moved_highlighting=none +resharper_cpp_clang_tidy_hicpp_member_init_highlighting=none +resharper_cpp_clang_tidy_hicpp_move_const_arg_highlighting=none +resharper_cpp_clang_tidy_hicpp_multiway_paths_covered_highlighting=warning +resharper_cpp_clang_tidy_hicpp_named_parameter_highlighting=none +resharper_cpp_clang_tidy_hicpp_new_delete_operators_highlighting=none +resharper_cpp_clang_tidy_hicpp_noexcept_move_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_array_decay_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_assembler_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_malloc_highlighting=none +resharper_cpp_clang_tidy_hicpp_signed_bitwise_highlighting=none +resharper_cpp_clang_tidy_hicpp_special_member_functions_highlighting=none +resharper_cpp_clang_tidy_hicpp_static_assert_highlighting=none +resharper_cpp_clang_tidy_hicpp_undelegated_constructor_highlighting=none +resharper_cpp_clang_tidy_hicpp_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_auto_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_emplace_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_default_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_delete_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_noexcept_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_override_highlighting=none +resharper_cpp_clang_tidy_hicpp_vararg_highlighting=none +resharper_cpp_clang_tidy_highlighting_highlighting=suggestion +resharper_cpp_clang_tidy_linuxkernel_must_check_errs_highlighting=warning +resharper_cpp_clang_tidy_llvmlibc_callee_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_implementation_in_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_restrict_system_libc_headers_highlighting=none +resharper_cpp_clang_tidy_llvm_else_after_return_highlighting=none +resharper_cpp_clang_tidy_llvm_header_guard_highlighting=none +resharper_cpp_clang_tidy_llvm_include_order_highlighting=none +resharper_cpp_clang_tidy_llvm_namespace_comment_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_isa_or_dyn_cast_in_conditionals_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_register_over_unsigned_highlighting=suggestion +resharper_cpp_clang_tidy_llvm_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_llvm_twine_local_highlighting=none +resharper_cpp_clang_tidy_misc_definitions_in_headers_highlighting=none +resharper_cpp_clang_tidy_misc_misplaced_const_highlighting=warning +resharper_cpp_clang_tidy_misc_new_delete_overloads_highlighting=warning +resharper_cpp_clang_tidy_misc_non_copyable_objects_highlighting=warning +resharper_cpp_clang_tidy_misc_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_misc_no_recursion_highlighting=none +resharper_cpp_clang_tidy_misc_redundant_expression_highlighting=warning +resharper_cpp_clang_tidy_misc_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_misc_throw_by_value_catch_by_reference_highlighting=warning +resharper_cpp_clang_tidy_misc_unconventional_assign_operator_highlighting=warning +resharper_cpp_clang_tidy_misc_uniqueptr_reset_release_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_alias_decls_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_parameters_highlighting=none +resharper_cpp_clang_tidy_misc_unused_using_decls_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_bind_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_modernize_concat_nested_namespaces_highlighting=none +resharper_cpp_clang_tidy_modernize_deprecated_headers_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_deprecated_ios_base_aliases_highlighting=warning +resharper_cpp_clang_tidy_modernize_loop_convert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_make_shared_highlighting=none +resharper_cpp_clang_tidy_modernize_make_unique_highlighting=none +resharper_cpp_clang_tidy_modernize_pass_by_value_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_raw_string_literal_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_redundant_void_arg_highlighting=none +resharper_cpp_clang_tidy_modernize_replace_auto_ptr_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_disallow_copy_and_assign_macro_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_random_shuffle_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_return_braced_init_list_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_shrink_to_fit_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_unary_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_auto_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_bool_literals_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_default_member_init_highlighting=none +resharper_cpp_clang_tidy_modernize_use_emplace_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_default_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_delete_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nodiscard_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_noexcept_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_modernize_use_override_highlighting=none +resharper_cpp_clang_tidy_modernize_use_trailing_return_type_highlighting=none +resharper_cpp_clang_tidy_modernize_use_transparent_functors_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_uncaught_exceptions_highlighting=warning +resharper_cpp_clang_tidy_modernize_use_using_highlighting=none +resharper_cpp_clang_tidy_mpi_buffer_deref_highlighting=warning +resharper_cpp_clang_tidy_mpi_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_objc_avoid_nserror_init_highlighting=warning +resharper_cpp_clang_tidy_objc_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_objc_forbidden_subclassing_highlighting=warning +resharper_cpp_clang_tidy_objc_missing_hash_highlighting=warning +resharper_cpp_clang_tidy_objc_nsinvocation_argument_lifetime_highlighting=warning +resharper_cpp_clang_tidy_objc_property_declaration_highlighting=warning +resharper_cpp_clang_tidy_objc_super_self_highlighting=warning +resharper_cpp_clang_tidy_openmp_exception_escape_highlighting=warning +resharper_cpp_clang_tidy_openmp_use_default_none_highlighting=warning +resharper_cpp_clang_tidy_performance_faster_string_find_highlighting=suggestion +resharper_cpp_clang_tidy_performance_for_range_copy_highlighting=suggestion +resharper_cpp_clang_tidy_performance_implicit_conversion_in_loop_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_algorithm_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_string_concatenation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_vector_operation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_move_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_performance_move_const_arg_highlighting=suggestion +resharper_cpp_clang_tidy_performance_noexcept_move_constructor_highlighting=none +resharper_cpp_clang_tidy_performance_no_automatic_move_highlighting=warning +resharper_cpp_clang_tidy_performance_no_int_to_ptr_highlighting=warning +resharper_cpp_clang_tidy_performance_trivially_destructible_highlighting=suggestion +resharper_cpp_clang_tidy_performance_type_promotion_in_math_fn_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_copy_initialization_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_value_param_highlighting=suggestion +resharper_cpp_clang_tidy_portability_restrict_system_includes_highlighting=none +resharper_cpp_clang_tidy_portability_simd_intrinsics_highlighting=none +resharper_cpp_clang_tidy_readability_avoid_const_params_in_decls_highlighting=none +resharper_cpp_clang_tidy_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_readability_const_return_type_highlighting=none +resharper_cpp_clang_tidy_readability_container_size_empty_highlighting=suggestion +resharper_cpp_clang_tidy_readability_convert_member_functions_to_static_highlighting=none +resharper_cpp_clang_tidy_readability_delete_null_pointer_highlighting=suggestion +resharper_cpp_clang_tidy_readability_else_after_return_highlighting=none +resharper_cpp_clang_tidy_readability_function_cognitive_complexity_highlighting=none +resharper_cpp_clang_tidy_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_readability_identifier_naming_highlighting=none +resharper_cpp_clang_tidy_readability_implicit_bool_conversion_highlighting=none +resharper_cpp_clang_tidy_readability_inconsistent_declaration_parameter_name_highlighting=suggestion +resharper_cpp_clang_tidy_readability_isolate_declaration_highlighting=none +resharper_cpp_clang_tidy_readability_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_readability_make_member_function_const_highlighting=none +resharper_cpp_clang_tidy_readability_misleading_indentation_highlighting=none +resharper_cpp_clang_tidy_readability_misplaced_array_index_highlighting=suggestion +resharper_cpp_clang_tidy_readability_named_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_non_const_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_access_specifiers_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_control_flow_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_declaration_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_function_ptr_dereference_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_member_init_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_preprocessor_highlighting=warning +resharper_cpp_clang_tidy_readability_redundant_smartptr_get_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_cstr_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_init_highlighting=suggestion +resharper_cpp_clang_tidy_readability_simplify_boolean_expr_highlighting=none +resharper_cpp_clang_tidy_readability_simplify_subscript_expr_highlighting=warning +resharper_cpp_clang_tidy_readability_static_accessed_through_instance_highlighting=suggestion +resharper_cpp_clang_tidy_readability_static_definition_in_anonymous_namespace_highlighting=none +resharper_cpp_clang_tidy_readability_string_compare_highlighting=warning +resharper_cpp_clang_tidy_readability_suspicious_call_argument_highlighting=warning +resharper_cpp_clang_tidy_readability_uniqueptr_delete_release_highlighting=suggestion +resharper_cpp_clang_tidy_readability_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_readability_use_anyofallof_highlighting=suggestion +resharper_cpp_clang_tidy_zircon_temporary_objects_highlighting=none +resharper_cpp_class_can_be_final_highlighting=hint +resharper_cpp_class_disallow_lazy_merging_highlighting=warning +resharper_cpp_class_is_incomplete_highlighting=warning +resharper_cpp_class_needs_constructor_because_of_uninitialized_member_highlighting=warning +resharper_cpp_class_never_used_highlighting=warning +resharper_cpp_compile_time_constant_can_be_replaced_with_boolean_constant_highlighting=suggestion +resharper_cpp_const_parameter_in_declaration_highlighting=suggestion +resharper_cpp_const_value_function_return_type_highlighting=suggestion +resharper_cpp_coroutine_call_resolve_error_highlighting=warning +resharper_cpp_cv_qualifier_can_not_be_applied_to_reference_highlighting=warning +resharper_cpp_c_style_cast_highlighting=suggestion +resharper_cpp_declaration_hides_local_highlighting=warning +resharper_cpp_declaration_hides_uncaptured_local_highlighting=hint +resharper_cpp_declaration_specifier_without_declarators_highlighting=warning +resharper_cpp_declarator_disambiguated_as_function_highlighting=warning +resharper_cpp_declarator_never_used_highlighting=warning +resharper_cpp_declarator_used_before_initialization_highlighting=error +resharper_cpp_defaulted_special_member_function_is_implicitly_deleted_highlighting=warning +resharper_cpp_default_case_not_handled_in_switch_statement_highlighting=warning +resharper_cpp_default_initialization_with_no_user_constructor_highlighting=warning +resharper_cpp_default_is_used_as_identifier_highlighting=warning +resharper_cpp_deleting_void_pointer_highlighting=warning +resharper_cpp_dependent_template_without_template_keyword_highlighting=warning +resharper_cpp_dependent_type_without_typename_keyword_highlighting=warning +resharper_cpp_deprecated_entity_highlighting=warning +resharper_cpp_deprecated_register_storage_class_specifier_highlighting=warning +resharper_cpp_dereference_operator_limit_exceeded_highlighting=warning +resharper_cpp_discarded_postfix_operator_result_highlighting=suggestion +resharper_cpp_doxygen_syntax_error_highlighting=warning +resharper_cpp_doxygen_undocumented_parameter_highlighting=suggestion +resharper_cpp_doxygen_unresolved_reference_highlighting=warning +resharper_cpp_empty_declaration_highlighting=warning +resharper_cpp_enforce_cv_qualifiers_order_highlighting=none +resharper_cpp_enforce_cv_qualifiers_placement_highlighting=none +resharper_cpp_enforce_do_statement_braces_highlighting=none +resharper_cpp_enforce_for_statement_braces_highlighting=none +resharper_cpp_enforce_function_declaration_style_highlighting=none +resharper_cpp_enforce_if_statement_braces_highlighting=none +resharper_cpp_enforce_nested_namespaces_style_highlighting=hint +resharper_cpp_enforce_overriding_destructor_style_highlighting=suggestion +resharper_cpp_enforce_overriding_function_style_highlighting=suggestion +resharper_cpp_enforce_type_alias_code_style_highlighting=none +resharper_cpp_enforce_while_statement_braces_highlighting=none +resharper_cpp_entity_assigned_but_no_read_highlighting=warning +resharper_cpp_entity_used_only_in_unevaluated_context_highlighting=warning +resharper_cpp_enumerator_never_used_highlighting=warning +resharper_cpp_equal_operands_in_binary_expression_highlighting=warning +resharper_cpp_explicit_specialization_in_non_namespace_scope_highlighting=warning +resharper_cpp_expression_without_side_effects_highlighting=warning +resharper_cpp_final_function_in_final_class_highlighting=suggestion +resharper_cpp_final_non_overriding_virtual_function_highlighting=suggestion +resharper_cpp_for_loop_can_be_replaced_with_while_highlighting=suggestion +resharper_cpp_functional_style_cast_highlighting=suggestion +resharper_cpp_function_doesnt_return_value_highlighting=warning +resharper_cpp_function_is_not_implemented_highlighting=warning +resharper_cpp_header_has_been_already_included_highlighting=hint +resharper_cpp_hidden_function_highlighting=warning +resharper_cpp_hiding_function_highlighting=warning +resharper_cpp_identical_operands_in_binary_expression_highlighting=warning +resharper_cpp_if_can_be_replaced_by_constexpr_if_highlighting=suggestion +resharper_cpp_implicit_default_constructor_not_available_highlighting=warning +resharper_cpp_incompatible_pointer_conversion_highlighting=warning +resharper_cpp_incomplete_switch_statement_highlighting=warning +resharper_cpp_inconsistent_naming_highlighting=hint +resharper_cpp_incorrect_blank_lines_near_braces_highlighting=none +resharper_cpp_initialized_value_is_always_rewritten_highlighting=warning +resharper_cpp_integral_to_pointer_conversion_highlighting=warning +resharper_cpp_invalid_line_continuation_highlighting=warning +resharper_cpp_join_declaration_and_assignment_highlighting=suggestion +resharper_cpp_lambda_capture_never_used_highlighting=warning +resharper_cpp_local_variable_may_be_const_highlighting=suggestion +resharper_cpp_local_variable_might_not_be_initialized_highlighting=warning +resharper_cpp_local_variable_with_non_trivial_dtor_is_never_used_highlighting=none +resharper_cpp_long_float_highlighting=warning +resharper_cpp_member_function_may_be_const_highlighting=suggestion +resharper_cpp_member_function_may_be_static_highlighting=suggestion +resharper_cpp_member_initializers_order_highlighting=suggestion +resharper_cpp_mismatched_class_tags_highlighting=warning +resharper_cpp_missing_blank_lines_highlighting=none +resharper_cpp_missing_include_guard_highlighting=warning +resharper_cpp_missing_indent_highlighting=none +resharper_cpp_missing_keyword_throw_highlighting=warning +resharper_cpp_missing_linebreak_highlighting=none +resharper_cpp_missing_space_highlighting=none +resharper_cpp_ms_ext_address_of_class_r_value_highlighting=warning +resharper_cpp_ms_ext_binding_r_value_to_lvalue_reference_highlighting=warning +resharper_cpp_ms_ext_copy_elision_in_copy_init_declarator_highlighting=warning +resharper_cpp_ms_ext_double_user_conversion_in_copy_init_highlighting=warning +resharper_cpp_ms_ext_not_initialized_static_const_local_var_highlighting=warning +resharper_cpp_ms_ext_reinterpret_cast_from_nullptr_highlighting=warning +resharper_cpp_multiple_spaces_highlighting=none +resharper_cpp_must_be_public_virtual_to_implement_interface_highlighting=warning +resharper_cpp_mutable_specifier_on_reference_member_highlighting=warning +resharper_cpp_nodiscard_function_without_return_value_highlighting=warning +resharper_cpp_non_exception_safe_resource_acquisition_highlighting=hint +resharper_cpp_non_explicit_conversion_operator_highlighting=hint +resharper_cpp_non_explicit_converting_constructor_highlighting=hint +resharper_cpp_non_inline_function_definition_in_header_file_highlighting=warning +resharper_cpp_non_inline_variable_definition_in_header_file_highlighting=warning +resharper_cpp_not_all_paths_return_value_highlighting=warning +resharper_cpp_no_discard_expression_highlighting=warning +resharper_cpp_object_member_might_not_be_initialized_highlighting=warning +resharper_cpp_outdent_is_off_prev_level_highlighting=none +resharper_cpp_out_parameter_must_be_written_highlighting=warning +resharper_cpp_parameter_may_be_const_highlighting=hint +resharper_cpp_parameter_may_be_const_ptr_or_ref_highlighting=suggestion +resharper_cpp_parameter_names_mismatch_highlighting=hint +resharper_cpp_parameter_never_used_highlighting=hint +resharper_cpp_parameter_value_is_reassigned_highlighting=warning +resharper_cpp_pointer_conversion_drops_qualifiers_highlighting=warning +resharper_cpp_pointer_to_integral_conversion_highlighting=warning +resharper_cpp_polymorphic_class_with_non_virtual_public_destructor_highlighting=warning +resharper_cpp_possibly_erroneous_empty_statements_highlighting=warning +resharper_cpp_possibly_uninitialized_member_highlighting=warning +resharper_cpp_possibly_unintended_object_slicing_highlighting=warning +resharper_cpp_precompiled_header_is_not_included_highlighting=error +resharper_cpp_precompiled_header_not_found_highlighting=error +resharper_cpp_printf_bad_format_highlighting=warning +resharper_cpp_printf_extra_arg_highlighting=warning +resharper_cpp_printf_missed_arg_highlighting=error +resharper_cpp_printf_risky_format_highlighting=warning +resharper_cpp_private_special_member_function_is_not_implemented_highlighting=warning +resharper_cpp_range_based_for_incompatible_reference_highlighting=warning +resharper_cpp_redefinition_of_default_argument_in_override_function_highlighting=warning +resharper_cpp_redundant_access_specifier_highlighting=hint +resharper_cpp_redundant_base_class_access_specifier_highlighting=hint +resharper_cpp_redundant_blank_lines_highlighting=none +resharper_cpp_redundant_boolean_expression_argument_highlighting=warning +resharper_cpp_redundant_cast_expression_highlighting=hint +resharper_cpp_redundant_const_specifier_highlighting=hint +resharper_cpp_redundant_control_flow_jump_highlighting=hint +resharper_cpp_redundant_elaborated_type_specifier_highlighting=hint +resharper_cpp_redundant_else_keyword_highlighting=hint +resharper_cpp_redundant_else_keyword_inside_compound_statement_highlighting=hint +resharper_cpp_redundant_empty_declaration_highlighting=hint +resharper_cpp_redundant_empty_statement_highlighting=hint +resharper_cpp_redundant_explicit_template_arguments_highlighting=hint +resharper_cpp_redundant_inline_specifier_highlighting=hint +resharper_cpp_redundant_lambda_parameter_list_highlighting=hint +resharper_cpp_redundant_linebreak_highlighting=none +resharper_cpp_redundant_member_initializer_highlighting=suggestion +resharper_cpp_redundant_namespace_definition_highlighting=suggestion +resharper_cpp_redundant_parentheses_highlighting=hint +resharper_cpp_redundant_qualifier_highlighting=hint +resharper_cpp_redundant_space_highlighting=none +resharper_cpp_redundant_static_specifier_on_member_allocation_function_highlighting=hint +resharper_cpp_redundant_template_keyword_highlighting=warning +resharper_cpp_redundant_typename_keyword_highlighting=warning +resharper_cpp_redundant_void_argument_list_highlighting=suggestion +resharper_cpp_reinterpret_cast_from_void_ptr_highlighting=suggestion +resharper_cpp_remove_redundant_braces_highlighting=none +resharper_cpp_replace_memset_with_zero_initialization_highlighting=suggestion +resharper_cpp_replace_tie_with_structured_binding_highlighting=suggestion +resharper_cpp_return_no_value_in_non_void_function_highlighting=warning +resharper_cpp_smart_pointer_vs_make_function_highlighting=suggestion +resharper_cpp_some_object_members_might_not_be_initialized_highlighting=warning +resharper_cpp_special_function_without_noexcept_specification_highlighting=warning +resharper_cpp_static_data_member_in_unnamed_struct_highlighting=warning +resharper_cpp_static_specifier_on_anonymous_namespace_member_highlighting=suggestion +resharper_cpp_string_literal_to_char_pointer_conversion_highlighting=warning +resharper_cpp_syntax_warning_highlighting=warning +resharper_cpp_tabs_and_spaces_mismatch_highlighting=none +resharper_cpp_tabs_are_disallowed_highlighting=none +resharper_cpp_tabs_outside_indent_highlighting=none +resharper_cpp_template_parameter_shadowing_highlighting=warning +resharper_cpp_this_arg_member_func_delegate_ctor_is_unsuported_by_dot_net_core_highlighting=none +resharper_cpp_throw_expression_can_be_replaced_with_rethrow_highlighting=warning +resharper_cpp_too_wide_scope_highlighting=suggestion +resharper_cpp_too_wide_scope_init_statement_highlighting=hint +resharper_cpp_type_alias_never_used_highlighting=warning +resharper_cpp_ue4_blueprint_callable_function_may_be_const_highlighting=hint +resharper_cpp_ue4_blueprint_callable_function_may_be_static_highlighting=hint +resharper_cpp_ue4_coding_standard_naming_violation_warning_highlighting=hint +resharper_cpp_ue4_coding_standard_u_class_naming_violation_error_highlighting=error +resharper_cpp_ue4_probable_memory_issues_with_u_objects_in_container_highlighting=warning +resharper_cpp_ue4_probable_memory_issues_with_u_object_highlighting=warning +resharper_cpp_ue_blueprint_callable_function_unused_highlighting=warning +resharper_cpp_ue_blueprint_implementable_event_not_implemented_highlighting=warning +resharper_cpp_ue_incorrect_engine_directory_highlighting=error +resharper_cpp_ue_non_existent_input_action_highlighting=warning +resharper_cpp_ue_non_existent_input_axis_highlighting=warning +resharper_cpp_ue_source_file_without_predefined_macros_highlighting=warning +resharper_cpp_ue_source_file_without_standard_library_highlighting=error +resharper_cpp_ue_version_file_doesnt_exist_highlighting=error +resharper_cpp_uninitialized_dependent_base_class_highlighting=warning +resharper_cpp_uninitialized_non_static_data_member_highlighting=warning +resharper_cpp_union_member_of_reference_type_highlighting=warning +resharper_cpp_unnamed_namespace_in_header_file_highlighting=warning +resharper_cpp_unnecessary_whitespace_highlighting=none +resharper_cpp_unreachable_code_highlighting=warning +resharper_cpp_unsigned_zero_comparison_highlighting=warning +resharper_cpp_unused_include_directive_highlighting=warning +resharper_cpp_user_defined_literal_suffix_does_not_start_with_underscore_highlighting=warning +resharper_cpp_use_algorithm_with_count_highlighting=suggestion +resharper_cpp_use_associative_contains_highlighting=suggestion +resharper_cpp_use_auto_for_numeric_highlighting=hint +resharper_cpp_use_auto_highlighting=hint +resharper_cpp_use_elements_view_highlighting=suggestion +resharper_cpp_use_erase_algorithm_highlighting=suggestion +resharper_cpp_use_familiar_template_syntax_for_generic_lambdas_highlighting=suggestion +resharper_cpp_use_range_algorithm_highlighting=suggestion +resharper_cpp_use_std_size_highlighting=suggestion +resharper_cpp_use_structured_binding_highlighting=hint +resharper_cpp_use_type_trait_alias_highlighting=suggestion +resharper_cpp_using_result_of_assignment_as_condition_highlighting=warning +resharper_cpp_u_function_macro_call_has_no_effect_highlighting=warning +resharper_cpp_u_property_macro_call_has_no_effect_highlighting=warning +resharper_cpp_variable_can_be_made_constexpr_highlighting=suggestion +resharper_cpp_virtual_function_call_inside_ctor_highlighting=warning +resharper_cpp_virtual_function_in_final_class_highlighting=warning +resharper_cpp_volatile_parameter_in_declaration_highlighting=suggestion +resharper_cpp_wrong_includes_order_highlighting=hint +resharper_cpp_wrong_indent_size_highlighting=none +resharper_cpp_wrong_slashes_in_include_directive_highlighting=hint +resharper_cpp_zero_constant_can_be_replaced_with_nullptr_highlighting=suggestion +resharper_cpp_zero_valued_expression_used_as_null_pointer_highlighting=warning +resharper_create_specialized_overload_highlighting=hint +resharper_css_browser_compatibility_highlighting=warning +resharper_css_caniuse_feature_requires_prefix_highlighting=hint +resharper_css_caniuse_unsupported_feature_highlighting=hint +resharper_css_not_resolved_highlighting=error +resharper_css_obsolete_highlighting=hint +resharper_css_property_does_not_override_vendor_property_highlighting=warning +resharper_cyclic_reference_comment_highlighting=none +resharper_c_declaration_with_implicit_int_type_highlighting=warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting=warning +resharper_c_sharp_missing_plugin_dependency_highlighting=warning +resharper_declaration_hides_highlighting=hint +resharper_declaration_is_empty_highlighting=warning +resharper_declaration_visibility_error_highlighting=error +resharper_default_value_attribute_for_optional_parameter_highlighting=warning +resharper_deleting_non_qualified_reference_highlighting=error +resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting=hint +resharper_double_colons_expected_highlighting=error +resharper_double_colons_preferred_highlighting=suggestion +resharper_double_negation_in_pattern_highlighting=suggestion +resharper_double_negation_of_boolean_highlighting=warning +resharper_double_negation_operator_highlighting=suggestion +resharper_duplicate_identifier_error_highlighting=error +resharper_duplicate_reference_comment_highlighting=warning +resharper_duplicate_resource_highlighting=warning +resharper_duplicating_local_declaration_highlighting=warning +resharper_duplicating_parameter_declaration_error_highlighting=error +resharper_duplicating_property_declaration_error_highlighting=error +resharper_duplicating_property_declaration_highlighting=warning +resharper_duplicating_switch_label_highlighting=warning +resharper_dynamic_shift_right_op_is_not_int_highlighting=warning +resharper_elided_trailing_element_highlighting=warning +resharper_empty_constructor_highlighting=warning +resharper_empty_destructor_highlighting=warning +resharper_empty_embedded_statement_highlighting=warning +resharper_empty_for_statement_highlighting=warning +resharper_empty_general_catch_clause_highlighting=warning +resharper_empty_namespace_highlighting=warning +resharper_empty_object_property_declaration_highlighting=error +resharper_empty_return_value_for_type_annotated_function_highlighting=warning +resharper_empty_statement_highlighting=warning +resharper_empty_title_tag_highlighting=hint +resharper_enforce_do_while_statement_braces_highlighting=none +resharper_enforce_fixed_statement_braces_highlighting=none +resharper_enforce_foreach_statement_braces_highlighting=none +resharper_enforce_for_statement_braces_highlighting=none +resharper_enforce_if_statement_braces_highlighting=none +resharper_enforce_lock_statement_braces_highlighting=none +resharper_enforce_using_statement_braces_highlighting=none +resharper_enforce_while_statement_braces_highlighting=none +resharper_entity_name_captured_only_global_highlighting=warning +resharper_entity_name_captured_only_local_highlighting=warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting=warning +resharper_enum_underlying_type_is_int_highlighting=warning +resharper_equal_expression_comparison_highlighting=warning +resharper_error_in_xml_doc_reference_highlighting=error +resharper_es6_feature_highlighting=error +resharper_es7_feature_highlighting=error +resharper_eval_arguments_name_error_highlighting=error +resharper_event_never_invoked_global_highlighting=suggestion +resharper_event_never_subscribed_to_global_highlighting=suggestion +resharper_event_never_subscribed_to_local_highlighting=suggestion +resharper_event_unsubscription_via_anonymous_delegate_highlighting=warning +resharper_experimental_feature_highlighting=error +resharper_explicit_caller_info_argument_highlighting=warning +resharper_expression_is_always_const_highlighting=warning +resharper_expression_is_always_null_highlighting=warning +resharper_field_can_be_made_read_only_global_highlighting=suggestion +resharper_field_can_be_made_read_only_local_highlighting=suggestion +resharper_field_hides_interface_property_with_default_implementation_highlighting=warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_format_string_placeholders_mismatch_highlighting=warning +resharper_format_string_problem_highlighting=warning +resharper_for_can_be_converted_to_foreach_highlighting=suggestion +resharper_for_statement_condition_is_true_highlighting=warning +resharper_functions_used_before_declared_highlighting=none +resharper_function_complexity_overflow_highlighting=none +resharper_function_never_returns_highlighting=warning +resharper_function_parameter_named_arguments_highlighting=warning +resharper_function_recursive_on_all_paths_highlighting=warning +resharper_function_used_out_of_scope_highlighting=warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting=warning +resharper_generic_enumerator_not_disposed_highlighting=warning +resharper_heuristically_unreachable_code_highlighting=warning +resharper_heuristic_unreachable_code_highlighting=warning +resharper_hex_color_value_with_alpha_highlighting=error +resharper_html_attributes_quotes_highlighting=hint +resharper_html_attribute_not_resolved_highlighting=warning +resharper_html_attribute_value_not_resolved_highlighting=warning +resharper_html_dead_code_highlighting=warning +resharper_html_event_not_resolved_highlighting=warning +resharper_html_id_duplication_highlighting=warning +resharper_html_id_not_resolved_highlighting=warning +resharper_html_obsolete_highlighting=warning +resharper_html_path_error_highlighting=warning +resharper_html_tag_not_closed_highlighting=error +resharper_html_tag_not_resolved_highlighting=warning +resharper_html_tag_should_be_self_closed_highlighting=warning +resharper_html_tag_should_not_be_self_closed_highlighting=warning +resharper_html_warning_highlighting=warning +resharper_identifier_typo_highlighting=suggestion +resharper_implicit_any_error_highlighting=error +resharper_implicit_any_type_warning_highlighting=warning +resharper_import_keyword_not_with_invocation_highlighting=error +resharper_inactive_preprocessor_branch_highlighting=warning +resharper_inconsistently_synchronized_field_highlighting=warning +resharper_inconsistent_function_returns_highlighting=warning +resharper_inconsistent_naming_highlighting=warning +resharper_inconsistent_order_of_locks_highlighting=warning +resharper_incorrect_blank_lines_near_braces_highlighting=none +resharper_incorrect_operand_in_type_of_comparison_highlighting=warning +resharper_incorrect_triple_slash_location_highlighting=warning +resharper_indexing_by_invalid_range_highlighting=warning +resharper_inheritdoc_consider_usage_highlighting=none +resharper_inheritdoc_invalid_usage_highlighting=warning +resharper_inline_out_variable_declaration_highlighting=suggestion +resharper_inline_temporary_variable_highlighting=hint +resharper_internal_module_highlighting=suggestion +resharper_internal_or_private_member_not_documented_highlighting=none +resharper_interpolated_string_expression_is_not_i_formattable_highlighting=warning +resharper_introduce_optional_parameters_global_highlighting=suggestion +resharper_introduce_optional_parameters_local_highlighting=suggestion +resharper_introduce_variable_to_apply_guard_highlighting=hint +resharper_int_division_by_zero_highlighting=warning +resharper_int_variable_overflow_highlighting=warning +resharper_int_variable_overflow_in_checked_context_highlighting=warning +resharper_int_variable_overflow_in_unchecked_context_highlighting=warning +resharper_invalid_attribute_value_highlighting=warning +resharper_invalid_json_syntax_highlighting=error +resharper_invalid_task_element_highlighting=none +resharper_invalid_value_highlighting=error +resharper_invalid_value_type_highlighting=warning +resharper_invalid_xml_doc_comment_highlighting=warning +resharper_invert_condition_1_highlighting=hint +resharper_invert_if_highlighting=hint +resharper_invocation_is_skipped_highlighting=hint +resharper_invocation_of_non_function_highlighting=warning +resharper_invoked_expression_maybe_non_function_highlighting=warning +resharper_invoke_as_extension_method_highlighting=suggestion +resharper_is_expression_always_false_highlighting=warning +resharper_is_expression_always_true_highlighting=warning +resharper_iterator_method_result_is_ignored_highlighting=warning +resharper_iterator_never_returns_highlighting=warning +resharper_join_declaration_and_initializer_highlighting=suggestion +resharper_join_declaration_and_initializer_js_highlighting=suggestion +resharper_join_null_check_with_usage_highlighting=suggestion +resharper_join_null_check_with_usage_when_possible_highlighting=none +resharper_json_validation_failed_highlighting=error +resharper_js_path_not_found_highlighting=error +resharper_js_unreachable_code_highlighting=warning +resharper_jump_must_be_in_loop_highlighting=warning +resharper_label_or_semicolon_expected_highlighting=error +resharper_lambda_expression_can_be_made_static_highlighting=none +resharper_lambda_expression_must_be_static_highlighting=suggestion +resharper_lambda_highlighting=suggestion +resharper_lambda_should_not_capture_context_highlighting=warning +resharper_less_specific_overload_than_main_signature_highlighting=warning +resharper_lexical_declaration_needs_block_highlighting=error +resharper_localizable_element_highlighting=warning +resharper_local_function_can_be_made_static_highlighting=none +resharper_local_function_hides_method_highlighting=warning +resharper_local_function_redefined_later_highlighting=warning +resharper_local_variable_hides_member_highlighting=warning +resharper_long_literal_ending_lower_l_highlighting=warning +resharper_loop_can_be_converted_to_query_highlighting=hint +resharper_loop_can_be_partly_converted_to_query_highlighting=none +resharper_loop_variable_is_never_changed_inside_loop_highlighting=warning +resharper_l_value_is_expected_highlighting=error +resharper_markup_attribute_typo_highlighting=suggestion +resharper_markup_text_typo_highlighting=suggestion +resharper_math_abs_method_is_redundant_highlighting=warning +resharper_math_clamp_min_greater_than_max_highlighting=warning +resharper_meaningless_default_parameter_value_highlighting=warning +resharper_member_can_be_internal_highlighting=none +resharper_member_can_be_made_static_global_highlighting=hint +resharper_member_can_be_made_static_local_highlighting=hint +resharper_member_can_be_private_global_highlighting=suggestion +resharper_member_can_be_private_local_highlighting=suggestion +resharper_member_can_be_protected_global_highlighting=suggestion +resharper_member_can_be_protected_local_highlighting=suggestion +resharper_member_hides_interface_member_with_default_implementation_highlighting=warning +resharper_member_hides_static_from_outer_class_highlighting=warning +resharper_member_initializer_value_ignored_highlighting=warning +resharper_merge_and_pattern_highlighting=suggestion +resharper_merge_cast_with_type_check_highlighting=suggestion +resharper_merge_conditional_expression_highlighting=suggestion +resharper_merge_conditional_expression_when_possible_highlighting=none +resharper_merge_into_logical_pattern_highlighting=hint +resharper_merge_into_negated_pattern_highlighting=hint +resharper_merge_into_pattern_highlighting=suggestion +resharper_merge_nested_property_patterns_highlighting=suggestion +resharper_merge_sequential_checks_highlighting=hint +resharper_merge_sequential_checks_when_possible_highlighting=none +resharper_method_has_async_overload_highlighting=suggestion +resharper_method_has_async_overload_with_cancellation_highlighting=suggestion +resharper_method_overload_with_optional_parameter_highlighting=warning +resharper_method_safe_this_highlighting=suggestion +resharper_method_supports_cancellation_highlighting=suggestion +resharper_missing_alt_attribute_in_img_tag_highlighting=hint +resharper_missing_attribute_highlighting=warning +resharper_missing_blank_lines_highlighting=none +resharper_missing_body_tag_highlighting=warning +resharper_missing_has_own_property_in_foreach_highlighting=warning +resharper_missing_head_and_body_tags_highlighting=warning +resharper_missing_head_tag_highlighting=warning +resharper_missing_indent_highlighting=none +resharper_missing_linebreak_highlighting=none +resharper_missing_space_highlighting=none +resharper_missing_title_tag_highlighting=hint +resharper_misuse_of_owner_function_this_highlighting=warning +resharper_more_specific_foreach_variable_type_available_highlighting=suggestion +resharper_more_specific_signature_after_less_specific_highlighting=warning +resharper_move_to_existing_positional_deconstruction_pattern_highlighting=hint +resharper_multiple_declarations_in_foreach_highlighting=error +resharper_multiple_nullable_attributes_usage_highlighting=warning +resharper_multiple_order_by_highlighting=warning +resharper_multiple_output_tags_highlighting=warning +resharper_multiple_resolve_candidates_in_text_highlighting=warning +resharper_multiple_spaces_highlighting=none +resharper_multiple_statements_on_one_line_highlighting=none +resharper_multiple_type_members_on_one_line_highlighting=none +resharper_must_use_return_value_highlighting=warning +resharper_mvc_action_not_resolved_highlighting=error +resharper_mvc_area_not_resolved_highlighting=error +resharper_mvc_controller_not_resolved_highlighting=error +resharper_mvc_invalid_model_type_highlighting=error +resharper_mvc_masterpage_not_resolved_highlighting=error +resharper_mvc_partial_view_not_resolved_highlighting=error +resharper_mvc_template_not_resolved_highlighting=error +resharper_mvc_view_component_not_resolved_highlighting=error +resharper_mvc_view_component_view_not_resolved_highlighting=error +resharper_mvc_view_not_resolved_highlighting=error +resharper_native_type_prototype_extending_highlighting=warning +resharper_native_type_prototype_overwriting_highlighting=warning +resharper_negation_of_relational_pattern_highlighting=suggestion +resharper_negative_equality_expression_highlighting=suggestion +resharper_negative_index_highlighting=warning +resharper_nested_string_interpolation_highlighting=suggestion +resharper_non_assigned_constant_highlighting=error +resharper_non_atomic_compound_operator_highlighting=warning +resharper_non_constant_equality_expression_has_constant_result_highlighting=warning +resharper_non_parsable_element_highlighting=warning +resharper_non_readonly_member_in_get_hash_code_highlighting=warning +resharper_non_volatile_field_in_double_check_locking_highlighting=warning +resharper_not_accessed_field_global_highlighting=suggestion +resharper_not_accessed_field_local_highlighting=warning +resharper_not_accessed_positional_property_global_highlighting=warning +resharper_not_accessed_positional_property_local_highlighting=warning +resharper_not_accessed_variable_highlighting=warning +resharper_not_all_paths_return_value_highlighting=warning +resharper_not_assigned_out_parameter_highlighting=warning +resharper_not_declared_in_parent_culture_highlighting=warning +resharper_not_null_member_is_not_initialized_highlighting=warning +resharper_not_observable_annotation_redundancy_highlighting=warning +resharper_not_overridden_in_specific_culture_highlighting=warning +resharper_not_resolved_highlighting=warning +resharper_not_resolved_in_text_highlighting=warning +resharper_nullable_warning_suppression_is_used_highlighting=none +resharper_n_unit_async_method_must_be_task_highlighting=warning +resharper_n_unit_attribute_produces_too_many_tests_highlighting=none +resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_attribute_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting=warning +resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting=warning +resharper_n_unit_duplicate_values_highlighting=warning +resharper_n_unit_ignored_parameter_attribute_highlighting=warning +resharper_n_unit_implicit_unspecified_null_values_highlighting=warning +resharper_n_unit_incorrect_argument_type_highlighting=warning +resharper_n_unit_incorrect_expected_result_type_highlighting=warning +resharper_n_unit_incorrect_range_bounds_highlighting=warning +resharper_n_unit_method_with_parameters_and_test_attribute_highlighting=warning +resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting=warning +resharper_n_unit_non_public_method_with_test_attribute_highlighting=warning +resharper_n_unit_no_values_provided_highlighting=warning +resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting=warning +resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting=warning +resharper_n_unit_range_step_sign_mismatch_highlighting=warning +resharper_n_unit_range_step_value_must_not_be_zero_highlighting=warning +resharper_n_unit_range_to_value_is_not_reachable_highlighting=warning +resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting=warning +resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting=warning +resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting=warning +resharper_n_unit_test_case_attribute_requires_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_is_obsolete_highlighting=warning +resharper_n_unit_test_case_source_cannot_be_resolved_highlighting=warning +resharper_n_unit_test_case_source_must_be_field_property_method_highlighting=warning +resharper_n_unit_test_case_source_must_be_static_highlighting=warning +resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting=warning +resharper_object_creation_as_statement_highlighting=warning +resharper_object_destructuring_without_parentheses_highlighting=error +resharper_object_literals_are_not_comma_free_highlighting=error +resharper_obsolete_element_error_highlighting=error +resharper_obsolete_element_highlighting=warning +resharper_octal_literals_not_allowed_error_highlighting=error +resharper_ol_tag_contains_non_li_elements_highlighting=hint +resharper_one_way_operation_contract_with_return_type_highlighting=warning +resharper_operation_contract_without_service_contract_highlighting=warning +resharper_operator_is_can_be_used_highlighting=warning +resharper_optional_parameter_hierarchy_mismatch_highlighting=warning +resharper_optional_parameter_ref_out_highlighting=warning +resharper_other_tags_inside_script1_highlighting=error +resharper_other_tags_inside_script2_highlighting=error +resharper_other_tags_inside_unclosed_script_highlighting=error +resharper_outdent_is_off_prev_level_highlighting=none +resharper_output_tag_required_highlighting=warning +resharper_out_parameter_value_is_always_discarded_global_highlighting=suggestion +resharper_out_parameter_value_is_always_discarded_local_highlighting=warning +resharper_overload_signature_inferring_highlighting=hint +resharper_overridden_with_empty_value_highlighting=warning +resharper_overridden_with_same_value_highlighting=suggestion +resharper_parameter_doesnt_make_any_sense_highlighting=warning +resharper_parameter_hides_member_highlighting=warning +resharper_parameter_only_used_for_precondition_check_global_highlighting=suggestion +resharper_parameter_only_used_for_precondition_check_local_highlighting=warning +resharper_parameter_type_can_be_enumerable_global_highlighting=hint +resharper_parameter_type_can_be_enumerable_local_highlighting=hint +resharper_parameter_value_is_not_used_highlighting=warning +resharper_partial_method_parameter_name_mismatch_highlighting=warning +resharper_partial_method_with_single_part_highlighting=warning +resharper_partial_type_with_single_part_highlighting=warning +resharper_pass_string_interpolation_highlighting=hint +resharper_path_not_resolved_highlighting=error +resharper_pattern_always_matches_highlighting=warning +resharper_pattern_is_always_true_or_false_highlighting=warning +resharper_pattern_never_matches_highlighting=warning +resharper_polymorphic_field_like_event_invocation_highlighting=warning +resharper_possible_infinite_inheritance_highlighting=warning +resharper_possible_intended_rethrow_highlighting=warning +resharper_possible_interface_member_ambiguity_highlighting=warning +resharper_possible_invalid_cast_exception_highlighting=warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting=warning +resharper_possible_invalid_operation_exception_highlighting=warning +resharper_possible_loss_of_fraction_highlighting=warning +resharper_possible_mistaken_argument_highlighting=warning +resharper_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_possible_multiple_enumeration_highlighting=warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting=warning +resharper_possible_null_reference_exception_highlighting=warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting=warning +resharper_possible_unintended_linear_search_in_set_highlighting=warning +resharper_possible_unintended_queryable_as_enumerable_highlighting=suggestion +resharper_possible_unintended_reference_comparison_highlighting=warning +resharper_possible_write_to_me_highlighting=warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting=warning +resharper_possibly_incorrectly_broken_statement_highlighting=warning +resharper_possibly_missing_indexer_initializer_comma_highlighting=warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting=warning +resharper_possibly_mistaken_use_of_params_method_highlighting=warning +resharper_possibly_unassigned_property_highlighting=hint +resharper_private_field_can_be_converted_to_local_variable_highlighting=warning +resharper_private_variable_can_be_made_readonly_highlighting=hint +resharper_property_can_be_made_init_only_global_highlighting=suggestion +resharper_property_can_be_made_init_only_local_highlighting=suggestion +resharper_property_getter_cannot_have_parameters_highlighting=error +resharper_property_not_resolved_highlighting=error +resharper_property_setter_must_have_single_parameter_highlighting=error +resharper_public_constructor_in_abstract_class_highlighting=suggestion +resharper_pure_attribute_on_void_method_highlighting=warning +resharper_qualified_expression_is_null_highlighting=warning +resharper_qualified_expression_maybe_null_highlighting=warning +resharper_razor_layout_not_resolved_highlighting=error +resharper_razor_section_not_resolved_highlighting=error +resharper_read_access_in_double_check_locking_highlighting=warning +resharper_redundant_abstract_modifier_highlighting=warning +resharper_redundant_always_match_subpattern_highlighting=suggestion +resharper_redundant_anonymous_type_property_name_highlighting=warning +resharper_redundant_argument_default_value_highlighting=warning +resharper_redundant_array_creation_expression_highlighting=hint +resharper_redundant_array_lower_bound_specification_highlighting=warning +resharper_redundant_assignment_highlighting=warning +resharper_redundant_attribute_parentheses_highlighting=hint +resharper_redundant_attribute_usage_property_highlighting=suggestion +resharper_redundant_base_constructor_call_highlighting=warning resharper_redundant_base_qualifier_highlighting=warning +resharper_redundant_blank_lines_highlighting=none +resharper_redundant_block_highlighting=warning +resharper_redundant_bool_compare_highlighting=warning +resharper_redundant_case_label_highlighting=warning +resharper_redundant_cast_highlighting=warning +resharper_redundant_catch_clause_highlighting=warning +resharper_redundant_check_before_assignment_highlighting=warning +resharper_redundant_collection_initializer_element_braces_highlighting=hint +resharper_redundant_comparison_with_boolean_highlighting=warning +resharper_redundant_configure_await_highlighting=suggestion +resharper_redundant_css_hack_highlighting=warning +resharper_redundant_declaration_semicolon_highlighting=hint +resharper_redundant_default_member_initializer_highlighting=warning +resharper_redundant_delegate_creation_highlighting=warning +resharper_redundant_disable_warning_comment_highlighting=warning +resharper_redundant_discard_designation_highlighting=suggestion +resharper_redundant_else_block_highlighting=warning +resharper_redundant_empty_case_else_highlighting=warning +resharper_redundant_empty_constructor_highlighting=warning +resharper_redundant_empty_finally_block_highlighting=warning +resharper_redundant_empty_object_creation_argument_list_highlighting=hint +resharper_redundant_empty_object_or_collection_initializer_highlighting=warning +resharper_redundant_empty_switch_section_highlighting=warning +resharper_redundant_enumerable_cast_call_highlighting=warning +resharper_redundant_enum_case_label_for_default_section_highlighting=none +resharper_redundant_explicit_array_creation_highlighting=warning +resharper_redundant_explicit_array_size_highlighting=warning +resharper_redundant_explicit_nullable_creation_highlighting=warning +resharper_redundant_explicit_params_array_creation_highlighting=suggestion +resharper_redundant_explicit_positional_property_declaration_highlighting=warning +resharper_redundant_explicit_tuple_component_name_highlighting=warning +resharper_redundant_extends_list_entry_highlighting=warning +resharper_redundant_fixed_pointer_declaration_highlighting=suggestion +resharper_redundant_highlighting=warning +resharper_redundant_if_else_block_highlighting=hint +resharper_redundant_if_statement_then_keyword_highlighting=none +resharper_redundant_immediate_delegate_invocation_highlighting=suggestion +resharper_redundant_intermediate_variable_highlighting=hint +resharper_redundant_is_before_relational_pattern_highlighting=suggestion +resharper_redundant_iterator_keyword_highlighting=warning +resharper_redundant_jump_statement_highlighting=warning +resharper_redundant_lambda_parameter_type_highlighting=warning +resharper_redundant_lambda_signature_parentheses_highlighting=hint +resharper_redundant_linebreak_highlighting=none +resharper_redundant_local_class_name_highlighting=hint +resharper_redundant_local_function_name_highlighting=hint +resharper_redundant_logical_conditional_expression_operand_highlighting=warning +resharper_redundant_me_qualifier_highlighting=warning +resharper_redundant_my_base_qualifier_highlighting=warning +resharper_redundant_my_class_qualifier_highlighting=warning +resharper_redundant_name_qualifier_highlighting=warning +resharper_redundant_not_null_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting=warning +resharper_redundant_nullable_flow_attribute_highlighting=warning +resharper_redundant_nullable_type_mark_highlighting=warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting=warning +resharper_redundant_overflow_checking_context_highlighting=warning +resharper_redundant_overload_global_highlighting=suggestion +resharper_redundant_overload_local_highlighting=suggestion +resharper_redundant_overridden_member_highlighting=warning +resharper_redundant_params_highlighting=warning +resharper_redundant_parentheses_highlighting=none +resharper_redundant_parent_type_declaration_highlighting=warning +resharper_redundant_pattern_parentheses_highlighting=hint +resharper_redundant_property_parentheses_highlighting=hint +resharper_redundant_property_pattern_clause_highlighting=suggestion +resharper_redundant_qualifier_highlighting=warning +resharper_redundant_query_order_by_ascending_keyword_highlighting=hint +resharper_redundant_range_bound_highlighting=suggestion +resharper_redundant_readonly_modifier_highlighting=suggestion +resharper_redundant_record_body_highlighting=warning +resharper_redundant_record_class_keyword_highlighting=warning +resharper_redundant_setter_value_parameter_declaration_highlighting=hint +resharper_redundant_space_highlighting=none +resharper_redundant_string_format_call_highlighting=warning +resharper_redundant_string_interpolation_highlighting=suggestion +resharper_redundant_string_to_char_array_call_highlighting=warning +resharper_redundant_string_type_highlighting=suggestion +resharper_redundant_suppress_nullable_warning_expression_highlighting=warning +resharper_redundant_ternary_expression_highlighting=warning +resharper_redundant_to_string_call_for_value_type_highlighting=hint +resharper_redundant_to_string_call_highlighting=warning +resharper_redundant_type_arguments_of_method_highlighting=warning +resharper_redundant_type_cast_highlighting=warning +resharper_redundant_type_cast_structural_highlighting=warning +resharper_redundant_type_check_in_pattern_highlighting=warning +resharper_redundant_units_highlighting=warning +resharper_redundant_unsafe_context_highlighting=warning +resharper_redundant_using_directive_global_highlighting=warning +resharper_redundant_using_directive_highlighting=warning +resharper_redundant_variable_type_specification_highlighting=hint +resharper_redundant_verbatim_prefix_highlighting=suggestion +resharper_redundant_verbatim_string_prefix_highlighting=suggestion +resharper_redundant_with_expression_highlighting=suggestion +resharper_reference_equals_with_value_type_highlighting=warning +resharper_reg_exp_inspections_highlighting=warning +resharper_remove_constructor_invocation_highlighting=none +resharper_remove_redundant_braces_highlighting=none +resharper_remove_redundant_or_statement_false_highlighting=suggestion +resharper_remove_redundant_or_statement_true_highlighting=suggestion +resharper_remove_to_list_1_highlighting=suggestion +resharper_remove_to_list_2_highlighting=suggestion +resharper_replace_auto_property_with_computed_property_highlighting=hint +resharper_replace_indicing_with_array_destructuring_highlighting=hint +resharper_replace_indicing_with_short_hand_properties_after_destructuring_highlighting=hint +resharper_replace_object_pattern_with_var_pattern_highlighting=suggestion +resharper_replace_slice_with_range_indexer_highlighting=hint +resharper_replace_substring_with_range_indexer_highlighting=hint +resharper_replace_undefined_checking_series_with_object_destructuring_highlighting=hint +resharper_replace_with_destructuring_swap_highlighting=hint +resharper_replace_with_first_or_default_1_highlighting=suggestion +resharper_replace_with_first_or_default_2_highlighting=suggestion +resharper_replace_with_first_or_default_3_highlighting=suggestion +resharper_replace_with_first_or_default_4_highlighting=suggestion +resharper_replace_with_last_or_default_1_highlighting=suggestion +resharper_replace_with_last_or_default_2_highlighting=suggestion +resharper_replace_with_last_or_default_3_highlighting=suggestion +resharper_replace_with_last_or_default_4_highlighting=suggestion +resharper_replace_with_of_type_1_highlighting=suggestion +resharper_replace_with_of_type_2_highlighting=suggestion +resharper_replace_with_of_type_3_highlighting=suggestion +resharper_replace_with_of_type_any_1_highlighting=suggestion +resharper_replace_with_of_type_any_2_highlighting=suggestion +resharper_replace_with_of_type_count_1_highlighting=suggestion +resharper_replace_with_of_type_count_2_highlighting=suggestion +resharper_replace_with_of_type_first_1_highlighting=suggestion +resharper_replace_with_of_type_first_2_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_last_1_highlighting=suggestion +resharper_replace_with_of_type_last_2_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_long_count_highlighting=suggestion +resharper_replace_with_of_type_single_1_highlighting=suggestion +resharper_replace_with_of_type_single_2_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_where_highlighting=suggestion +resharper_replace_with_simple_assignment_false_highlighting=suggestion +resharper_replace_with_simple_assignment_true_highlighting=suggestion +resharper_replace_with_single_assignment_false_highlighting=suggestion +resharper_replace_with_single_assignment_true_highlighting=suggestion +resharper_replace_with_single_call_to_any_highlighting=suggestion +resharper_replace_with_single_call_to_count_highlighting=suggestion +resharper_replace_with_single_call_to_first_highlighting=suggestion +resharper_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_last_highlighting=suggestion +resharper_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_single_highlighting=suggestion +resharper_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_replace_with_single_or_default_1_highlighting=suggestion +resharper_replace_with_single_or_default_2_highlighting=suggestion +resharper_replace_with_single_or_default_3_highlighting=suggestion +resharper_replace_with_single_or_default_4_highlighting=suggestion +resharper_replace_with_string_is_null_or_empty_highlighting=suggestion +resharper_required_base_types_conflict_highlighting=warning +resharper_required_base_types_direct_conflict_highlighting=warning +resharper_required_base_types_is_not_inherited_highlighting=warning +resharper_requires_fallback_color_highlighting=warning +resharper_resource_item_not_resolved_highlighting=error +resharper_resource_not_resolved_highlighting=error +resharper_resx_not_resolved_highlighting=warning +resharper_return_from_global_scopet_with_value_highlighting=warning +resharper_return_type_can_be_enumerable_global_highlighting=hint +resharper_return_type_can_be_enumerable_local_highlighting=hint +resharper_return_type_can_be_not_nullable_highlighting=warning +resharper_return_value_of_pure_method_is_not_used_highlighting=warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting=hint +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting=warning +resharper_route_templates_ambiguous_route_match_highlighting=warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting=warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting=hint +resharper_route_templates_duplicated_parameter_highlighting=warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting=warning +resharper_route_templates_method_missing_route_parameters_highlighting=hint +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting=warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting=warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting=hint +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting=warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting=suggestion +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting=warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting=hint +resharper_route_templates_route_token_not_resolved_highlighting=warning +resharper_route_templates_symbol_not_resolved_highlighting=warning +resharper_route_templates_syntax_error_highlighting=warning +resharper_safe_cast_is_used_as_type_check_highlighting=suggestion +resharper_same_imports_with_different_name_highlighting=warning +resharper_same_variable_assignment_highlighting=warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting=error +resharper_script_tag_with_content_before_includes_highlighting=hint +resharper_sealed_member_in_sealed_class_highlighting=warning +resharper_separate_control_transfer_statement_highlighting=none +resharper_service_contract_without_operations_highlighting=warning +resharper_shift_expression_real_shift_count_is_zero_highlighting=warning +resharper_shift_expression_result_equals_zero_highlighting=warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting=warning +resharper_shift_expression_zero_left_operand_highlighting=warning +resharper_similar_anonymous_type_nearby_highlighting=hint +resharper_similar_expressions_comparison_highlighting=warning +resharper_simplify_conditional_operator_highlighting=suggestion +resharper_simplify_conditional_ternary_expression_highlighting=suggestion +resharper_simplify_i_if_highlighting=suggestion +resharper_simplify_linq_expression_use_all_highlighting=suggestion +resharper_simplify_linq_expression_use_any_highlighting=suggestion +resharper_simplify_string_interpolation_highlighting=suggestion +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting=warning +resharper_specify_string_comparison_highlighting=hint +resharper_specify_variable_type_explicitly_highlighting=hint +resharper_spin_lock_in_readonly_field_highlighting=warning +resharper_stack_alloc_inside_loop_highlighting=warning +resharper_statement_termination_highlighting=warning +resharper_static_member_initializer_referes_to_member_below_highlighting=warning +resharper_static_member_in_generic_type_highlighting=none +resharper_static_problem_in_text_highlighting=warning +resharper_string_compare_is_culture_specific_1_highlighting=warning +resharper_string_compare_is_culture_specific_2_highlighting=warning +resharper_string_compare_is_culture_specific_3_highlighting=warning +resharper_string_compare_is_culture_specific_4_highlighting=warning +resharper_string_compare_is_culture_specific_5_highlighting=warning +resharper_string_compare_is_culture_specific_6_highlighting=warning +resharper_string_compare_to_is_culture_specific_highlighting=warning +resharper_string_concatenation_to_template_string_highlighting=hint +resharper_string_ends_with_is_culture_specific_highlighting=none +resharper_string_index_of_is_culture_specific_1_highlighting=warning +resharper_string_index_of_is_culture_specific_2_highlighting=warning +resharper_string_index_of_is_culture_specific_3_highlighting=warning +resharper_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_string_literal_as_interpolation_argument_highlighting=suggestion +resharper_string_literal_typo_highlighting=suggestion +resharper_string_literal_wrong_quotes_highlighting=hint +resharper_string_starts_with_is_culture_specific_highlighting=none +resharper_structured_message_template_problem_highlighting=warning +resharper_struct_can_be_made_read_only_highlighting=suggestion +resharper_struct_member_can_be_made_read_only_highlighting=none +resharper_suggest_base_type_for_parameter_highlighting=hint +resharper_suggest_base_type_for_parameter_in_constructor_highlighting=hint +resharper_suggest_discard_declaration_var_style_highlighting=hint resharper_suggest_var_or_type_built_in_types_highlighting=hint +resharper_suggest_var_or_type_deconstruction_declarations_highlighting=hint resharper_suggest_var_or_type_elsewhere_highlighting=hint resharper_suggest_var_or_type_simple_types_highlighting=hint -resharper_web_config_module_not_resolved_highlighting=warning -resharper_web_config_type_not_resolved_highlighting=warning -resharper_web_config_wrong_module_highlighting=warning +resharper_super_call_highlighting=suggestion +resharper_super_call_prohibits_this_highlighting=error +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting=warning +resharper_suspicious_instanceof_check_highlighting=warning +resharper_suspicious_lambda_block_highlighting=warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting=warning +resharper_suspicious_math_sign_method_highlighting=warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting=warning +resharper_suspicious_this_usage_highlighting=warning +resharper_suspicious_typeof_check_highlighting=warning +resharper_suspicious_type_conversion_global_highlighting=warning +resharper_swap_via_deconstruction_highlighting=suggestion +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting=hint +resharper_switch_statement_for_enum_misses_default_section_highlighting=hint +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting=hint +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting=none +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting=warning +resharper_syntax_is_not_allowed_highlighting=warning +resharper_tabs_and_spaces_mismatch_highlighting=none +resharper_tabs_are_disallowed_highlighting=none +resharper_tabs_outside_indent_highlighting=none +resharper_tail_recursive_call_highlighting=hint +resharper_tasks_not_loaded_highlighting=warning +resharper_ternary_can_be_replaced_by_its_condition_highlighting=warning +resharper_this_in_global_context_highlighting=warning +resharper_thread_static_at_instance_field_highlighting=warning +resharper_thread_static_field_has_initializer_highlighting=warning +resharper_throw_must_be_followed_by_expression_highlighting=error +resharper_too_wide_local_variable_scope_highlighting=suggestion +resharper_try_cast_always_succeeds_highlighting=suggestion +resharper_try_statements_can_be_merged_highlighting=hint +resharper_ts_not_resolved_highlighting=error +resharper_ts_resolved_from_inaccessible_module_highlighting=error +resharper_type_guard_doesnt_affect_anything_highlighting=warning +resharper_type_guard_produces_never_type_highlighting=warning +resharper_type_parameter_can_be_variant_highlighting=suggestion +resharper_type_parameter_hides_type_param_from_outer_scope_highlighting=warning +resharper_ul_tag_contains_non_li_elements_highlighting=hint +resharper_unassigned_field_global_highlighting=suggestion +resharper_unassigned_field_local_highlighting=warning +resharper_unassigned_get_only_auto_property_highlighting=warning +resharper_unassigned_readonly_field_highlighting=warning +resharper_unclosed_script_highlighting=error +resharper_undeclared_global_variable_using_highlighting=warning +resharper_unexpected_value_highlighting=error +resharper_unknown_css_class_highlighting=warning +resharper_unknown_css_variable_highlighting=warning +resharper_unknown_css_vendor_extension_highlighting=hint +resharper_unknown_item_group_highlighting=warning +resharper_unknown_metadata_highlighting=warning +resharper_unknown_output_parameter_highlighting=warning +resharper_unknown_property_highlighting=warning +resharper_unknown_target_highlighting=warning +resharper_unknown_task_attribute_highlighting=warning +resharper_unknown_task_highlighting=warning +resharper_unnecessary_whitespace_highlighting=none +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting=warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting=warning +resharper_unreal_header_tool_error_highlighting=error +resharper_unreal_header_tool_parser_error_highlighting=error +resharper_unreal_header_tool_warning_highlighting=warning +resharper_unsafe_comma_in_object_properties_list_highlighting=warning +resharper_unsupported_required_base_type_highlighting=warning +resharper_unused_anonymous_method_signature_highlighting=warning +resharper_unused_auto_property_accessor_global_highlighting=warning +resharper_unused_auto_property_accessor_local_highlighting=warning +resharper_unused_import_clause_highlighting=warning +resharper_unused_inherited_parameter_highlighting=hint +resharper_unused_locals_highlighting=warning +resharper_unused_local_function_highlighting=warning +resharper_unused_local_function_parameter_highlighting=warning +resharper_unused_local_function_return_value_highlighting=warning +resharper_unused_local_import_highlighting=warning +resharper_unused_member_global_highlighting=suggestion +resharper_unused_member_hierarchy_global_highlighting=suggestion +resharper_unused_member_hierarchy_local_highlighting=warning +resharper_unused_member_in_super_global_highlighting=suggestion +resharper_unused_member_in_super_local_highlighting=warning +resharper_unused_member_local_highlighting=warning +resharper_unused_method_return_value_global_highlighting=suggestion +resharper_unused_method_return_value_local_highlighting=warning +resharper_unused_parameter_global_highlighting=suggestion +resharper_unused_parameter_highlighting=warning +resharper_unused_parameter_in_partial_method_highlighting=warning +resharper_unused_parameter_local_highlighting=warning +resharper_unused_property_highlighting=warning +resharper_unused_tuple_component_in_return_value_highlighting=warning +resharper_unused_type_global_highlighting=suggestion +resharper_unused_type_local_highlighting=warning +resharper_unused_type_parameter_highlighting=warning +resharper_unused_variable_highlighting=warning +resharper_usage_of_definitely_unassigned_value_highlighting=warning +resharper_usage_of_possibly_unassigned_value_highlighting=warning +resharper_useless_binary_operation_highlighting=warning +resharper_useless_comparison_to_integral_constant_highlighting=warning +resharper_use_array_creation_expression_1_highlighting=suggestion +resharper_use_array_creation_expression_2_highlighting=suggestion +resharper_use_array_empty_method_highlighting=suggestion +resharper_use_as_instead_of_type_cast_highlighting=hint +resharper_use_await_using_highlighting=suggestion +resharper_use_cancellation_token_for_i_async_enumerable_highlighting=suggestion +resharper_use_collection_count_property_highlighting=suggestion +resharper_use_configure_await_false_for_async_disposable_highlighting=none +resharper_use_configure_await_false_highlighting=suggestion +resharper_use_deconstruction_highlighting=hint +resharper_use_deconstruction_on_parameter_highlighting=hint +resharper_use_empty_types_field_highlighting=suggestion +resharper_use_event_args_empty_field_highlighting=suggestion +resharper_use_format_specifier_in_format_string_highlighting=suggestion +resharper_use_implicitly_typed_variable_evident_highlighting=hint +resharper_use_implicitly_typed_variable_highlighting=none +resharper_use_implicit_by_val_modifier_highlighting=hint +resharper_use_indexed_property_highlighting=suggestion +resharper_use_index_from_end_expression_highlighting=suggestion +resharper_use_is_operator_1_highlighting=suggestion +resharper_use_is_operator_2_highlighting=suggestion +resharper_use_method_any_0_highlighting=suggestion +resharper_use_method_any_1_highlighting=suggestion +resharper_use_method_any_2_highlighting=suggestion +resharper_use_method_any_3_highlighting=suggestion +resharper_use_method_any_4_highlighting=suggestion +resharper_use_method_is_instance_of_type_highlighting=suggestion +resharper_use_nameof_expression_for_part_of_the_string_highlighting=none +resharper_use_nameof_expression_highlighting=suggestion +resharper_use_name_of_instead_of_type_of_highlighting=suggestion +resharper_use_negated_pattern_in_is_expression_highlighting=hint +resharper_use_negated_pattern_matching_highlighting=hint +resharper_use_nullable_annotation_instead_of_attribute_highlighting=suggestion +resharper_use_nullable_attributes_supported_by_compiler_highlighting=suggestion +resharper_use_nullable_reference_types_annotation_syntax_highlighting=warning +resharper_use_null_propagation_highlighting=hint +resharper_use_null_propagation_when_possible_highlighting=none +resharper_use_object_or_collection_initializer_highlighting=suggestion +resharper_use_of_implicit_global_in_function_scope_highlighting=warning +resharper_use_of_possibly_unassigned_property_highlighting=warning +resharper_use_pattern_matching_highlighting=suggestion +resharper_use_positional_deconstruction_pattern_highlighting=none +resharper_use_string_interpolation_highlighting=suggestion +resharper_use_switch_case_pattern_variable_highlighting=suggestion +resharper_use_throw_if_null_method_highlighting=none +resharper_use_verbatim_string_highlighting=hint +resharper_using_of_reserved_word_error_highlighting=error +resharper_using_of_reserved_word_highlighting=warning +resharper_value_parameter_not_used_highlighting=warning +resharper_value_range_attribute_violation_highlighting=warning +resharper_value_should_have_units_highlighting=error +resharper_variable_can_be_made_const_highlighting=hint +resharper_variable_can_be_made_let_highlighting=hint +resharper_variable_can_be_moved_to_inner_block_highlighting=hint +resharper_variable_can_be_not_nullable_highlighting=warning +resharper_variable_hides_outer_variable_highlighting=warning +resharper_variable_used_before_declared_highlighting=warning +resharper_variable_used_in_inner_scope_before_declared_highlighting=warning +resharper_variable_used_out_of_scope_highlighting=warning +resharper_vb_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_vb_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_vb_possible_mistaken_argument_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_vb_remove_to_list_1_highlighting=suggestion +resharper_vb_remove_to_list_2_highlighting=suggestion +resharper_vb_replace_with_first_or_default_highlighting=suggestion +resharper_vb_replace_with_last_or_default_highlighting=suggestion +resharper_vb_replace_with_of_type_1_highlighting=suggestion +resharper_vb_replace_with_of_type_2_highlighting=suggestion +resharper_vb_replace_with_of_type_any_1_highlighting=suggestion +resharper_vb_replace_with_of_type_any_2_highlighting=suggestion +resharper_vb_replace_with_of_type_count_1_highlighting=suggestion +resharper_vb_replace_with_of_type_count_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_where_highlighting=suggestion +resharper_vb_replace_with_single_assignment_1_highlighting=suggestion +resharper_vb_replace_with_single_assignment_2_highlighting=suggestion +resharper_vb_replace_with_single_call_to_any_highlighting=suggestion +resharper_vb_replace_with_single_call_to_count_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_vb_replace_with_single_or_default_highlighting=suggestion +resharper_vb_simplify_linq_expression_10_highlighting=hint +resharper_vb_simplify_linq_expression_1_highlighting=suggestion +resharper_vb_simplify_linq_expression_2_highlighting=suggestion +resharper_vb_simplify_linq_expression_3_highlighting=suggestion +resharper_vb_simplify_linq_expression_4_highlighting=suggestion +resharper_vb_simplify_linq_expression_5_highlighting=suggestion +resharper_vb_simplify_linq_expression_6_highlighting=suggestion +resharper_vb_simplify_linq_expression_7_highlighting=hint +resharper_vb_simplify_linq_expression_8_highlighting=hint +resharper_vb_simplify_linq_expression_9_highlighting=hint +resharper_vb_string_compare_is_culture_specific_1_highlighting=warning +resharper_vb_string_compare_is_culture_specific_2_highlighting=warning +resharper_vb_string_compare_is_culture_specific_3_highlighting=warning +resharper_vb_string_compare_is_culture_specific_4_highlighting=warning +resharper_vb_string_compare_is_culture_specific_5_highlighting=warning +resharper_vb_string_compare_is_culture_specific_6_highlighting=warning +resharper_vb_string_compare_to_is_culture_specific_highlighting=warning +resharper_vb_string_ends_with_is_culture_specific_highlighting=none +resharper_vb_string_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_starts_with_is_culture_specific_highlighting=none +resharper_vb_unreachable_code_highlighting=warning +resharper_vb_use_array_creation_expression_1_highlighting=suggestion +resharper_vb_use_array_creation_expression_2_highlighting=suggestion +resharper_vb_use_first_instead_highlighting=warning +resharper_vb_use_method_any_1_highlighting=suggestion +resharper_vb_use_method_any_2_highlighting=suggestion +resharper_vb_use_method_any_3_highlighting=suggestion +resharper_vb_use_method_any_4_highlighting=suggestion +resharper_vb_use_method_any_5_highlighting=suggestion +resharper_vb_use_method_is_instance_of_type_highlighting=suggestion +resharper_vb_use_type_of_is_operator_1_highlighting=suggestion +resharper_vb_use_type_of_is_operator_2_highlighting=suggestion +resharper_virtual_member_call_in_constructor_highlighting=warning +resharper_virtual_member_never_overridden_global_highlighting=suggestion +resharper_virtual_member_never_overridden_local_highlighting=suggestion +resharper_void_method_with_must_use_return_value_attribute_highlighting=warning +resharper_web_config_module_not_resolved_highlighting=error +resharper_web_config_module_qualification_resolve_highlighting=warning +resharper_web_config_redundant_add_namespace_tag_highlighting=warning +resharper_web_config_redundant_location_tag_highlighting=warning +resharper_web_config_tag_prefix_redundand_highlighting=warning +resharper_web_config_type_not_resolved_highlighting=error +resharper_web_config_unused_add_tag_highlighting=warning +resharper_web_config_unused_element_due_to_config_source_attribute_highlighting=warning +resharper_web_config_unused_remove_or_clear_tag_highlighting=warning +resharper_web_config_web_config_path_warning_highlighting=warning +resharper_web_config_wrong_module_highlighting=error +resharper_web_ignored_path_highlighting=none +resharper_web_mapped_path_highlighting=hint +resharper_with_expression_instead_of_initializer_highlighting=suggestion +resharper_with_statement_using_error_highlighting=error +resharper_wrong_expression_statement_highlighting=warning +resharper_wrong_indent_size_highlighting=none +resharper_wrong_metadata_use_highlighting=none +resharper_wrong_public_modifier_specification_highlighting=hint +resharper_wrong_require_relative_path_highlighting=hint +resharper_xaml_assign_null_to_not_null_attribute_highlighting=warning +resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting=warning +resharper_xaml_binding_without_context_not_resolved_highlighting=hint +resharper_xaml_binding_with_context_not_resolved_highlighting=warning +resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting=error +resharper_xaml_constructor_warning_highlighting=warning +resharper_xaml_decimal_parsing_is_culture_dependent_highlighting=warning +resharper_xaml_dependency_property_resolve_error_highlighting=warning +resharper_xaml_duplicate_style_setter_highlighting=warning +resharper_xaml_dynamic_resource_error_highlighting=error +resharper_xaml_element_name_reference_not_resolved_highlighting=error +resharper_xaml_empty_grid_length_definition_highlighting=error +resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting=hint +resharper_xaml_ignored_path_highlighting_highlighting=none +resharper_xaml_index_out_of_grid_definition_highlighting=warning +resharper_xaml_invalid_member_type_highlighting=error +resharper_xaml_invalid_resource_target_type_highlighting=error +resharper_xaml_invalid_resource_type_highlighting=error +resharper_xaml_invalid_type_highlighting=error +resharper_xaml_language_level_highlighting=error +resharper_xaml_mapped_path_highlighting_highlighting=hint +resharper_xaml_method_arguments_will_be_ignored_highlighting=warning +resharper_xaml_missing_grid_index_highlighting=warning +resharper_xaml_overloads_collision_highlighting=warning +resharper_xaml_parent_is_out_of_current_component_tree_highlighting=warning +resharper_xaml_path_error_highlighting=warning +resharper_xaml_possible_null_reference_exception_highlighting=suggestion +resharper_xaml_redundant_attached_property_highlighting=warning +resharper_xaml_redundant_binding_mode_attribute_highlighting=warning +resharper_xaml_redundant_collection_property_highlighting=warning +resharper_xaml_redundant_freeze_attribute_highlighting=warning +resharper_xaml_redundant_grid_definitions_highlighting=warning +resharper_xaml_redundant_grid_span_highlighting=warning +resharper_xaml_redundant_modifiers_attribute_highlighting=warning +resharper_xaml_redundant_namespace_alias_highlighting=warning +resharper_xaml_redundant_name_attribute_highlighting=warning +resharper_xaml_redundant_property_type_qualifier_highlighting=warning +resharper_xaml_redundant_resource_highlighting=warning +resharper_xaml_redundant_styled_value_highlighting=warning +resharper_xaml_redundant_update_source_trigger_attribute_highlighting=warning +resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting=warning +resharper_xaml_resource_file_path_case_mismatch_highlighting=warning +resharper_xaml_routed_event_resolve_error_highlighting=warning +resharper_xaml_static_resource_not_resolved_highlighting=warning +resharper_xaml_style_class_not_found_highlighting=warning +resharper_xaml_style_invalid_target_type_highlighting=error +resharper_xaml_unexpected_text_token_highlighting=error +resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting=error +resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting=warning +resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting=warning +resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting=warning +resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting=warning +resharper_xaml_x_key_attribute_disallowed_highlighting=error +resharper_xml_doc_comment_syntax_problem_highlighting=warning +resharper_xunit_xunit_test_with_console_output_highlighting=warning -[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +# Standard properties +end_of_line= crlf +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +[*.{cshtml,htm,html,proto,razor}] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,c,c++,cc,cginc,compute,cp,cpp,cs,css,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,js,jsx,master,mpp,mq4,mq5,mqh,paml,skin,tpp,ts,tsx,usf,ush,vb,xaml,xamlx,xoml}] indent_style=space indent_size=4 tab_width=4 + +[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] +indent_style=space +indent_size= 4 +tab_width= 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index 2f267d0a..3f2f54a1 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -8,8 +8,9 @@ using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -internal class GamePathParser : IGamePathParser -{ +public class GamePathParser : IGamePathParser +{ + /// Obtain basic information about a file path. public GameObjectInfo GetFileInfo(string path) { path = path.ToLowerInvariant().Replace('\\', '/'); @@ -49,6 +50,8 @@ internal class GamePathParser : IGamePathParser }; } + /// Get the key of a VFX symbol. + /// The lower-case key or an empty string if no match is found. public string VfxToKey(string path) { var match = GamePaths.Vfx.Tmb().Match(path); @@ -59,25 +62,7 @@ internal class GamePathParser : IGamePathParser return match.Success ? match.Groups["key"].Value.ToLowerInvariant() : string.Empty; } - private const string CharacterFolder = "chara"; - private const string EquipmentFolder = "equipment"; - private const string PlayerFolder = "human"; - private const string WeaponFolder = "weapon"; - private const string AccessoryFolder = "accessory"; - private const string DemiHumanFolder = "demihuman"; - private const string MonsterFolder = "monster"; - private const string CommonFolder = "common"; - private const string UiFolder = "ui"; - private const string IconFolder = "icon"; - private const string LoadingFolder = "loadingimage"; - private const string MapFolder = "map"; - private const string InterfaceFolder = "uld"; - private const string FontFolder = "font"; - private const string HousingFolder = "hou"; - private const string VfxFolder = "vfx"; - private const string WorldFolder1 = "bgcommon"; - private const string WorldFolder2 = "bg"; - + /// Obtain the ObjectType from a given path. public ObjectType PathToObjectType(string path) { if (path.Length == 0) @@ -125,6 +110,25 @@ internal class GamePathParser : IGamePathParser }; } + private const string CharacterFolder = "chara"; + private const string EquipmentFolder = "equipment"; + private const string PlayerFolder = "human"; + private const string WeaponFolder = "weapon"; + private const string AccessoryFolder = "accessory"; + private const string DemiHumanFolder = "demihuman"; + private const string MonsterFolder = "monster"; + private const string CommonFolder = "common"; + private const string UiFolder = "ui"; + private const string IconFolder = "icon"; + private const string LoadingFolder = "loadingimage"; + private const string MapFolder = "map"; + private const string InterfaceFolder = "uld"; + private const string FontFolder = "font"; + private const string HousingFolder = "hou"; + private const string VfxFolder = "vfx"; + private const string WorldFolder1 = "bgcommon"; + private const string WorldFolder2 = "bg"; + private (FileType, ObjectType, Match?) ParseGamePath(string path) { if (!Names.ExtensionToFileType.TryGetValue(Path.GetExtension(path), out var fileType)) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index aa0392a0..6c097704 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,7 +16,8 @@ using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; - +using Penumbra.Services; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -458,17 +459,17 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.RedrawObject.Label, "Redraw Player Character" ); - if( ImGui.Button( "Redraw##pc" ) && Dalamud.ClientState.LocalPlayer != null ) + if( ImGui.Button( "Redraw##pc" ) && DalamudServices.ClientState.LocalPlayer != null ) { - Ipc.RedrawObject.Subscriber( _pi ).Invoke( Dalamud.ClientState.LocalPlayer, RedrawType.Redraw ); + Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.ClientState.LocalPlayer, RedrawType.Redraw ); } DrawIntro( Ipc.RedrawObjectByIndex.Label, "Redraw by Index" ); var tmp = _redrawIndex; ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, Dalamud.Objects.Length ) ) + if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.Objects.Length ) ) { - _redrawIndex = Math.Clamp( tmp, 0, Dalamud.Objects.Length ); + _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.Objects.Length ); } ImGui.SameLine(); @@ -489,12 +490,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn( IntPtr address, int index ) { - if( index < 0 || index > Dalamud.Objects.Length || address == IntPtr.Zero || Dalamud.Objects[ index ]?.Address != address ) + if( index < 0 || index > DalamudServices.Objects.Length || address == IntPtr.Zero || DalamudServices.Objects[ index ]?.Address != address ) { _lastRedrawnString = "Invalid"; } - _lastRedrawnString = $"{Dalamud.Objects[ index ]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{DalamudServices.Objects[ index ]!.Name} (0x{address:X}, {index})"; } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index da560ca9..88181927 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,7 +18,8 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.String; using Penumbra.String.Classes; - +using Penumbra.Services; + namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi @@ -84,9 +85,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi public unsafe PenumbraApi( Penumbra penumbra ) { _penumbra = penumbra; - _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() + _lumina = ( Lumina.GameData? )DalamudServices.GameData.GetType() .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + ?.GetValue( DalamudServices.GameData ); foreach( var collection in Penumbra.CollectionManager ) { SubscribeToCollection( collection ); @@ -889,12 +890,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if( actorIndex < 0 || actorIndex >= Dalamud.Objects.Length ) + if( actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length ) { return PenumbraApiEc.InvalidArgument; } - var identifier = Penumbra.Actors.FromObject( Dalamud.Objects[ actorIndex ], false, false, true ); + var identifier = Penumbra.Actors.FromObject( DalamudServices.Objects[ actorIndex ], false, false, true ); if( !identifier.IsValid ) { return PenumbraApiEc.InvalidArgument; @@ -1064,12 +1065,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi private static unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) { collection = Penumbra.CollectionManager.Default; - if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) + if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) { return false; } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); var data = PathResolver.IdentifyCollection( ptr, false ); if( data.Valid ) { @@ -1082,12 +1083,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static unsafe ActorIdentifier AssociatedIdentifier( int gameObjectIdx ) { - if( gameObjectIdx < 0 || gameObjectIdx >= Dalamud.Objects.Length ) + if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) { return ActorIdentifier.Invalid; } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); return Penumbra.Actors.FromObject( ptr, out _, false, true, true ); } @@ -1116,7 +1117,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - return Dalamud.GameData.GetFile< T >( resolvedPath ); + return DalamudServices.GameData.GetFile< T >( resolvedPath ); } catch( Exception e ) { diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 3726d9dc..81750870 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -11,7 +11,8 @@ using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; using Penumbra.GameData.Actors; using Penumbra.Util; - +using Penumbra.Services; + namespace Penumbra.Collections; public partial class ModCollection @@ -205,7 +206,7 @@ public partial class ModCollection => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); public static string ActiveCollectionFile - => Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); + => Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 150382ad..60ab44e0 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Mods; +using Penumbra.Services; using System; using System.Collections.Generic; using System.IO; @@ -14,7 +15,7 @@ namespace Penumbra.Collections; public partial class ModCollection { public static string CollectionDirectory - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); + => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "collections" ); // We need to remove all invalid path symbols from the collection name to be able to save it to file. public FileInfo FileName diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4f53a7b6..63922566 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI; namespace Penumbra; @@ -120,27 +121,27 @@ public class CommandHandler : IDisposable { if( !string.Equals( arguments, "help", StringComparison.OrdinalIgnoreCase ) && arguments == "?" ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); } else { - Dalamud.Chat.Print( "Valid arguments for /penumbra are:" ); + DalamudServices.Chat.Print( "Valid arguments for /penumbra are:" ); } - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "window", + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "window", "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state." ) + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state." ) .BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) .BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder() + DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder() .AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) .BuiltString ); return true; @@ -240,26 +241,26 @@ public class CommandHandler : IDisposable { if( arguments.Length == 0 ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) .AddText( " | " ).AddGreen( "" ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Types are " ).AddBlue( "Base" ).AddText( ", " ).AddBlue( "Ui" ).AddText( ", " ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Types are " ).AddBlue( "Base" ).AddText( ", " ).AddBlue( "Ui" ).AddText( ", " ) .AddBlue( "Selected" ).AddText( ", " ) .AddBlue( "Individual" ).AddText( ", and all those selectable in Character Groups." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Names are " ).AddYellow( "None" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Names are " ).AddYellow( "None" ) .AddText( ", all collections you have created by their full names, and " ).AddYellow( "Delete" ).AddText( " to remove assignments (not valid for all types)." ) .BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "n" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "n" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) .AddRed( "[NPC Name]" ).AddText( ", where NPC Type can be " ).AddInitialPurple( "Mount" ).AddInitialPurple( "Companion" ).AddInitialPurple( "Accessory" ) .AddInitialPurple( "Event NPC" ).AddText( "or " ) .AddInitialPurple( "Battle NPC", false ).AddText( "." ).BuiltString ); - Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "o" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "o" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) .AddRed( "[NPC Name]" ).AddText( " | " ).AddWhite( "[Player Name]@" ).AddText( "." ).BuiltString ); return true; } @@ -269,13 +270,13 @@ public class CommandHandler : IDisposable if( !CollectionTypeExtensions.TryParse( typeName, out var type ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); return false; } if( split.Length == 1 ) { - Dalamud.Chat.Print( "There was no collection name provided." ); + DalamudServices.Chat.Print( "There was no collection name provided." ); return false; } @@ -289,7 +290,7 @@ public class CommandHandler : IDisposable { if( split.Length == 2 ) { - Dalamud.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); + DalamudServices.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); return false; } @@ -300,7 +301,7 @@ public class CommandHandler : IDisposable identifier = _actors.FromObject( obj, false, true, true ); if( !identifier.IsValid ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) .AddText( " did not resolve to a game object with a valid identifier." ).BuiltString ); return false; } @@ -312,7 +313,7 @@ public class CommandHandler : IDisposable } catch( ActorManager.IdentifierParseError e ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( split[ 2 ], true ).AddText( $" could not be converted to an identifier. {e.Message}" ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( split[ 2 ], true ).AddText( $" could not be converted to an identifier. {e.Message}" ) .BuiltString ); return false; } @@ -321,7 +322,7 @@ public class CommandHandler : IDisposable var oldCollection = _collectionManager.ByType( type, identifier ); if( collection == oldCollection ) { - Dalamud.Chat.Print( collection == null + DalamudServices.Chat.Print( collection == null ? $"The {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}" : string.Empty )} is already unassigned" : $"{collection.Name} already is the {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); return false; @@ -354,7 +355,7 @@ public class CommandHandler : IDisposable } else { - Dalamud.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + DalamudServices.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); return false; } @@ -374,7 +375,7 @@ public class CommandHandler : IDisposable var seString = new SeStringBuilder() .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) .AddPurple( "[Mod Name or Mod Directory Name]" ); - Dalamud.Chat.Print( seString.BuiltString ); + DalamudServices.Chat.Print( seString.BuiltString ); return true; } @@ -382,14 +383,14 @@ public class CommandHandler : IDisposable var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); if( nameSplit.Length != 2 ) { - Dalamud.Chat.Print( "Not enough arguments provided." ); + DalamudServices.Chat.Print( "Not enough arguments provided." ); return false; } var state = ConvertToSettingState( split[ 0 ] ); if( state == -1 ) { - Dalamud.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); return false; } @@ -400,7 +401,7 @@ public class CommandHandler : IDisposable if( !_modManager.TryGetMod( nameSplit[ 1 ], nameSplit[ 1 ], out var mod ) ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); return false; } @@ -409,7 +410,7 @@ public class CommandHandler : IDisposable return true; } - Dalamud.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) .AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); return false; } @@ -421,7 +422,7 @@ public class CommandHandler : IDisposable var seString = new SeStringBuilder() .AddText( "Use with /penumbra bulktag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) .AddPurple( "[Local Tag]" ); - Dalamud.Chat.Print( seString.BuiltString ); + DalamudServices.Chat.Print( seString.BuiltString ); return true; } @@ -429,7 +430,7 @@ public class CommandHandler : IDisposable var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); if( nameSplit.Length != 2 ) { - Dalamud.Chat.Print( "Not enough arguments provided." ); + DalamudServices.Chat.Print( "Not enough arguments provided." ); return false; } @@ -437,7 +438,7 @@ public class CommandHandler : IDisposable if( state == -1 ) { - Dalamud.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); return false; } @@ -450,7 +451,7 @@ public class CommandHandler : IDisposable if( mods.Count == 0 ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); return false; } @@ -482,7 +483,7 @@ public class CommandHandler : IDisposable : _collectionManager[ lowerName ]; if( collection == null ) { - Dalamud.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); + DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); return false; } @@ -574,7 +575,7 @@ public class CommandHandler : IDisposable { if( Penumbra.Config.PrintSuccessfulCommandsToChat ) { - Dalamud.Chat.Print( text ); + DalamudServices.Chat.Print( text ); } } @@ -582,7 +583,7 @@ public class CommandHandler : IDisposable { if( Penumbra.Config.PrintSuccessfulCommandsToChat ) { - Dalamud.Chat.Print( text.ToStringAndClear() ); + DalamudServices.Chat.Print( text.ToStringAndClear() ); } } @@ -590,7 +591,7 @@ public class CommandHandler : IDisposable { if( Penumbra.Config.PrintSuccessfulCommandsToChat ) { - Dalamud.Chat.Print( text() ); + DalamudServices.Chat.Print( text() ); } } } \ No newline at end of file diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index ee1ee6ac..a4b368dd 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -7,7 +7,8 @@ using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Mods; - +using Penumbra.Services; + namespace Penumbra; public partial class Configuration @@ -30,7 +31,7 @@ public partial class Configuration public static void Migrate( Configuration config ) { - if( !File.Exists( Dalamud.PluginInterface.ConfigFile.FullName ) ) + if( !File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) { return; } @@ -38,7 +39,7 @@ public partial class Configuration var m = new Migration { _config = config, - _data = JObject.Parse( File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ) ), + _data = JObject.Parse( File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ) ), }; CreateBackup(); @@ -342,7 +343,7 @@ public partial class Configuration // Create a backup of the configuration file specifically. private static void CreateBackup() { - var name = Dalamud.PluginInterface.ConfigFile.FullName; + var name = DalamudServices.PluginInterface.ConfigFile.FullName; var bakName = name + ".bak"; try { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 878b4a3b..1821ca1f 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,6 +11,7 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -97,9 +98,9 @@ public partial class Configuration : IPluginConfiguration } Configuration? configuration = null; - if( File.Exists( Dalamud.PluginInterface.ConfigFile.FullName ) ) + if( File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) { - var text = File.ReadAllText( Dalamud.PluginInterface.ConfigFile.FullName ); + var text = File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ); configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings { Error = HandleDeserializationError, @@ -125,7 +126,7 @@ public partial class Configuration : IPluginConfiguration try { var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( Dalamud.PluginInterface.ConfigFile.FullName, text ); + File.WriteAllText( DalamudServices.PluginInterface.ConfigFile.FullName, text ); } catch( Exception e ) { diff --git a/Penumbra/Dalamud.cs b/Penumbra/Dalamud.cs deleted file mode 100644 index e9366d9a..00000000 --- a/Penumbra/Dalamud.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Interface; -using Dalamud.IoC; -using Dalamud.Plugin; -using System.Linq; -using System.Reflection; - -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - -namespace Penumbra; - -public class Dalamud -{ - public static void Initialize( DalamudPluginInterface pluginInterface ) - => pluginInterface.Create< Dalamud >(); - - // @formatter:off - [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; - // @formatter:on - - private static readonly object? DalamudConfig; - private static readonly MethodInfo? InterfaceGetter; - private static readonly MethodInfo? SaveDalamudConfig; - public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; - - static Dalamud() - { - try - { - var serviceType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "Service`1" && t.IsGenericType ); - var configType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "DalamudConfiguration" ); - var interfaceType = typeof( DalamudPluginInterface ).Assembly.DefinedTypes.FirstOrDefault( t => t.Name == "DalamudInterface" ); - if( serviceType == null || configType == null || interfaceType == null ) - { - return; - } - - var configService = serviceType.MakeGenericType( configType ); - var interfaceService = serviceType.MakeGenericType( interfaceType ); - var configGetter = configService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); - InterfaceGetter = interfaceService.GetMethod( "Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ); - if( configGetter == null || InterfaceGetter == null ) - { - return; - } - - DalamudConfig = configGetter.Invoke( null, null ); - if( DalamudConfig != null ) - { - SaveDalamudConfig = DalamudConfig.GetType().GetMethod( "Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); - if( SaveDalamudConfig == null ) - { - DalamudConfig = null; - InterfaceGetter = null; - } - } - } - catch - { - DalamudConfig = null; - SaveDalamudConfig = null; - InterfaceGetter = null; - } - } - - public static bool GetDalamudConfig< T >( string fieldName, out T? value ) - { - value = default; - try - { - if( DalamudConfig == null ) - { - return false; - } - - var getter = DalamudConfig.GetType().GetProperty( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); - if( getter == null ) - { - return false; - } - - var result = getter.GetValue( DalamudConfig ); - if( result is not T v ) - { - return false; - } - - value = v; - return true; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error while fetching Dalamud Config {fieldName}:\n{e}" ); - return false; - } - } - - public static bool SetDalamudConfig< T >( string fieldName, in T? value, string? windowFieldName = null ) - { - try - { - if( DalamudConfig == null ) - { - return false; - } - - var getter = DalamudConfig.GetType().GetProperty( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); - if( getter == null ) - { - return false; - } - - getter.SetValue( DalamudConfig, value ); - if( windowFieldName != null ) - { - var inter = InterfaceGetter!.Invoke( null, null ); - var settingsWindow = inter?.GetType().GetField( "settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.GetValue( inter ); - settingsWindow?.GetType().GetField( windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic )?.SetValue( settingsWindow, value ); - } - - SaveDalamudConfig!.Invoke( DalamudConfig, null ); - return true; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error while fetching Dalamud Config {fieldName}:\n{e}" ); - return false; - } - } -} \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 4b82da4b..bd9154a0 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -3,6 +3,7 @@ using System.IO; using System.Numerics; using Lumina.Data.Files; using OtterTex; +using Penumbra.Services; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; @@ -48,7 +49,7 @@ public partial class CombinedTexture : IDisposable { var (width, height) = CombineImage(); _centerStorage.TextureWrap = - Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + DalamudServices.PluginInterface.UiBuilder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); } _current?.Draw( size ); diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c54218d5..f6bb9866 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -10,6 +10,7 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; using SixLabors.ImageSharp.PixelFormats; @@ -195,12 +196,12 @@ public sealed class Texture : IDisposable return File.OpenRead( Path ); } - var file = Dalamud.GameData.GetFile( Path ); + var file = DalamudServices.GameData.GetFile( Path ); return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); } private void CreateTextureWrap( int width, int height ) - => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); + => TextureWrap = DalamudServices.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); private string? _tmpPath; @@ -215,7 +216,7 @@ public sealed class Texture : IDisposable { if( game ) { - if( !Dalamud.GameData.FileExists( path ) ) + if( !DalamudServices.GameData.FileExists( path ) ) { continue; } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index de02f091..307e55c3 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Utility.Signatures; using Penumbra.GameData; - +using Penumbra.Services; + namespace Penumbra.Interop; public unsafe partial class CharacterUtility : IDisposable @@ -58,7 +59,7 @@ public unsafe partial class CharacterUtility : IDisposable LoadDefaultResources( null! ); if( !Ready ) { - Dalamud.Framework.Update += LoadDefaultResources; + DalamudServices.Framework.Update += LoadDefaultResources; } } @@ -98,7 +99,7 @@ public unsafe partial class CharacterUtility : IDisposable if( !anyMissing ) { Ready = true; - Dalamud.Framework.Update -= LoadDefaultResources; + DalamudServices.Framework.Update -= LoadDefaultResources; LoadingFinished.Invoke(); } } diff --git a/Penumbra/Interop/GameEventManager.cs b/Penumbra/Interop/GameEventManager.cs index 1549f888..1080e7b8 100644 --- a/Penumbra/Interop/GameEventManager.cs +++ b/Penumbra/Interop/GameEventManager.cs @@ -4,18 +4,22 @@ using Penumbra.GameData; using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Log; using Penumbra.Interop.Structs; namespace Penumbra.Interop; public unsafe class GameEventManager : IDisposable { + private const string Prefix = $"[{nameof(GameEventManager)}]"; + public GameEventManager() { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); _characterDtorHook.Enable(); _copyCharacterHook.Enable(); _resourceHandleDestructorHook.Enable(); + Penumbra.Log.Verbose($"{Prefix} Created."); } public void Dispose() @@ -23,104 +27,102 @@ public unsafe class GameEventManager : IDisposable _characterDtorHook.Dispose(); _copyCharacterHook.Dispose(); _resourceHandleDestructorHook.Dispose(); + Penumbra.Log.Verbose($"{Prefix} Disposed."); } #region Character Destructor - private delegate void CharacterDestructorDelegate( Character* character ); + private delegate void CharacterDestructorDelegate(Character* character); - [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] - private readonly Hook< CharacterDestructorDelegate > _characterDtorHook = null!; + [Signature(Sigs.CharacterDestructor, DetourName = nameof(CharacterDestructorDetour))] + private readonly Hook _characterDtorHook = null!; - private void CharacterDestructorDetour( Character* character ) + private void CharacterDestructorDetour(Character* character) { - if( CharacterDestructor != null ) - { - foreach( var subscriber in CharacterDestructor.GetInvocationList() ) + if (CharacterDestructor != null) + foreach (var subscriber in CharacterDestructor.GetInvocationList()) { try { - ( ( CharacterDestructorEvent )subscriber ).Invoke( character ); + ((CharacterDestructorEvent)subscriber).Invoke(character); } - catch( Exception ex ) + catch (Exception ex) { - Penumbra.Log.Error( $"Error in {nameof( CharacterDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + Penumbra.Log.Error($"{Prefix} Error in {nameof(CharacterDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); } } - } - Penumbra.Log.Verbose( $"{nameof( CharacterDestructor )} triggered with 0x{( nint )character:X}." ); - _characterDtorHook.Original( character ); + Penumbra.Log.Verbose($"{Prefix} {nameof(CharacterDestructor)} triggered with 0x{(nint)character:X}."); + _characterDtorHook.Original(character); } - public delegate void CharacterDestructorEvent( Character* character ); + public delegate void CharacterDestructorEvent(Character* character); public event CharacterDestructorEvent? CharacterDestructor; #endregion #region Copy Character - private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); + private unsafe delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk); - [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] - private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; + [Signature(Sigs.CopyCharacter, DetourName = nameof(CopyCharacterDetour))] + private readonly Hook _copyCharacterHook = null!; - private ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + private ulong CopyCharacterDetour(GameObject* target, GameObject* source, uint unk) { - if( CopyCharacter != null ) - { - foreach( var subscriber in CopyCharacter.GetInvocationList() ) + if (CopyCharacter != null) + foreach (var subscriber in CopyCharacter.GetInvocationList()) { try { - ( ( CopyCharacterEvent )subscriber ).Invoke( ( Character* )target, ( Character* )source ); + ((CopyCharacterEvent)subscriber).Invoke((Character*)target, (Character*)source); } - catch( Exception ex ) + catch (Exception ex) { - Penumbra.Log.Error( $"Error in {nameof( CopyCharacter )} event when executing {subscriber.Method.Name}:\n{ex}" ); + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(CopyCharacter)} event when executing {subscriber.Method.Name}:\n{ex}"); } } - } - Penumbra.Log.Verbose( $"{nameof( CopyCharacter )} triggered with target 0x{( nint )target:X} and source 0x{( nint )source:X}." ); - return _copyCharacterHook.Original( target, source, unk ); + Penumbra.Log.Verbose( + $"{Prefix} {nameof(CopyCharacter)} triggered with target 0x{(nint)target:X} and source 0x{(nint)source:X}."); + return _copyCharacterHook.Original(target, source, unk); } - public delegate void CopyCharacterEvent( Character* target, Character* source ); + public delegate void CopyCharacterEvent(Character* target, Character* source); public event CopyCharacterEvent? CopyCharacter; #endregion #region ResourceHandle Destructor - private delegate IntPtr ResourceHandleDestructorDelegate( ResourceHandle* handle ); + private delegate IntPtr ResourceHandleDestructorDelegate(ResourceHandle* handle); - [Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )] - private readonly Hook< ResourceHandleDestructorDelegate > _resourceHandleDestructorHook = null!; + [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] + private readonly Hook _resourceHandleDestructorHook = null!; - private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) + private IntPtr ResourceHandleDestructorDetour(ResourceHandle* handle) { - if( ResourceHandleDestructor != null ) - { - foreach( var subscriber in ResourceHandleDestructor.GetInvocationList() ) + if (ResourceHandleDestructor != null) + foreach (var subscriber in ResourceHandleDestructor.GetInvocationList()) { try { - ( ( ResourceHandleDestructorEvent )subscriber ).Invoke( handle ); + ((ResourceHandleDestructorEvent)subscriber).Invoke(handle); } - catch( Exception ex ) + catch (Exception ex) { - Penumbra.Log.Error( $"Error in {nameof( ResourceHandleDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(ResourceHandleDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); } } - } - Penumbra.Log.Verbose( $"{nameof( ResourceHandleDestructor )} triggered with 0x{( nint )handle:X}." ); - return _resourceHandleDestructorHook!.Original( handle ); + Penumbra.Log.Verbose($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}."); + return _resourceHandleDestructorHook!.Original(handle); } - public delegate void ResourceHandleDestructorEvent( ResourceHandle* handle ); + public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); public event ResourceHandleDestructorEvent? ResourceHandleDestructor; #endregion -} \ No newline at end of file +} diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 5f466b95..ba24f155 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -10,7 +10,8 @@ using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; - +using Penumbra.Services; + namespace Penumbra.Interop; public unsafe partial class ObjectReloader @@ -34,7 +35,7 @@ public unsafe partial class ObjectReloader // Also clear the name list. private void SetGPose() { - _inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null; + _inGPose = DalamudServices.Objects[ GPosePlayerIdx ] != null; _gPoseNameCounter = 0; } @@ -49,7 +50,7 @@ public unsafe partial class ObjectReloader // this will be in obj and true will be returned. private bool FindCorrectActor( int idx, out GameObject? obj ) { - obj = Dalamud.Objects[ idx ]; + obj = DalamudServices.Objects[ idx ]; if( !_inGPose || obj == null || IsGPoseActor( idx ) ) { return false; @@ -66,14 +67,14 @@ public unsafe partial class ObjectReloader if( name == gPoseName ) { - obj = Dalamud.Objects[ GPosePlayerIdx + i ]; + obj = DalamudServices.Objects[ GPosePlayerIdx + i ]; return true; } } for( ; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter ) { - var gPoseName = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString(); + var gPoseName = DalamudServices.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString(); _gPoseNames[ _gPoseNameCounter ] = gPoseName; if( gPoseName == null ) { @@ -82,7 +83,7 @@ public unsafe partial class ObjectReloader if( name == gPoseName ) { - obj = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ]; + obj = DalamudServices.Objects[ GPosePlayerIdx + _gPoseNameCounter ]; return true; } } @@ -113,10 +114,10 @@ public sealed unsafe partial class ObjectReloader : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; public ObjectReloader() - => Dalamud.Framework.Update += OnUpdateEvent; + => DalamudServices.Framework.Update += OnUpdateEvent; public void Dispose() - => Dalamud.Framework.Update -= OnUpdateEvent; + => DalamudServices.Framework.Update -= OnUpdateEvent; public static DrawState* ActorDrawState( GameObject actor ) => ( DrawState* )( &( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->RenderFlags ); @@ -139,7 +140,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable DisableDraw( actor! ); } - if( actor is PlayerCharacter && Dalamud.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) + if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) { *ActorDrawState( mount ) |= DrawState.Invisibility; if( gPose ) @@ -164,7 +165,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable EnableDraw( actor! ); } - if( actor is PlayerCharacter && Dalamud.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) + if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) { *ActorDrawState( mount ) &= ~DrawState.Invisibility; if( gPose ) @@ -183,7 +184,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable return; } - if( actor!.Address == Dalamud.Targets.Target?.Address ) + if( actor!.Address == DalamudServices.Targets.Target?.Address ) { _target = tableIndex; } @@ -193,7 +194,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable private void ReloadActorAfterGPose( GameObject? actor ) { - if( Dalamud.Objects[ GPosePlayerIdx ] != null ) + if( DalamudServices.Objects[ GPosePlayerIdx ] != null ) { ReloadActor( actor ); return; @@ -213,13 +214,13 @@ public sealed unsafe partial class ObjectReloader : IDisposable return; } - var actor = Dalamud.Objects[ _target ]; - if( actor == null || Dalamud.Targets.Target != null ) + var actor = DalamudServices.Objects[ _target ]; + if( actor == null || DalamudServices.Targets.Target != null ) { return; } - Dalamud.Targets.SetTarget( actor ); + DalamudServices.Targets.SetTarget( actor ); _target = -1; } @@ -270,12 +271,12 @@ public sealed unsafe partial class ObjectReloader : IDisposable if( idx < 0 ) { var newIdx = ~idx; - WriteInvisible( Dalamud.Objects[ newIdx ] ); + WriteInvisible( DalamudServices.Objects[ newIdx ] ); _afterGPoseQueue[ numKept++ ] = newIdx; } else { - WriteVisible( Dalamud.Objects[ idx ] ); + WriteVisible( DalamudServices.Objects[ idx ] ); } } @@ -284,9 +285,9 @@ public sealed unsafe partial class ObjectReloader : IDisposable private void OnUpdateEvent( object framework ) { - if( Dalamud.Conditions[ ConditionFlag.BetweenAreas51 ] - || Dalamud.Conditions[ ConditionFlag.BetweenAreas ] - || Dalamud.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) + if( DalamudServices.Conditions[ ConditionFlag.BetweenAreas51 ] + || DalamudServices.Conditions[ ConditionFlag.BetweenAreas ] + || DalamudServices.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) { return; } @@ -313,8 +314,8 @@ public sealed unsafe partial class ObjectReloader : IDisposable private static GameObject? GetLocalPlayer() { - var gPosePlayer = Dalamud.Objects[ GPosePlayerIdx ]; - return gPosePlayer ?? Dalamud.Objects[ 0 ]; + var gPosePlayer = DalamudServices.Objects[ GPosePlayerIdx ]; + return gPosePlayer ?? DalamudServices.Objects[ 0 ]; } public static bool GetName( string lowerName, out GameObject? actor ) @@ -324,12 +325,12 @@ public sealed unsafe partial class ObjectReloader : IDisposable "" => ( null, true ), "" => ( GetLocalPlayer(), true ), "self" => ( GetLocalPlayer(), true ), - "" => ( Dalamud.Targets.Target, true ), - "target" => ( Dalamud.Targets.Target, true ), - "" => ( Dalamud.Targets.FocusTarget, true ), - "focus" => ( Dalamud.Targets.FocusTarget, true ), - "" => ( Dalamud.Targets.MouseOverTarget, true ), - "mouseover" => ( Dalamud.Targets.MouseOverTarget, true ), + "" => ( DalamudServices.Targets.Target, true ), + "target" => ( DalamudServices.Targets.Target, true ), + "" => ( DalamudServices.Targets.FocusTarget, true ), + "focus" => ( DalamudServices.Targets.FocusTarget, true ), + "" => ( DalamudServices.Targets.MouseOverTarget, true ), + "mouseover" => ( DalamudServices.Targets.MouseOverTarget, true ), _ => ( null, false ), }; return ret; @@ -337,9 +338,9 @@ public sealed unsafe partial class ObjectReloader : IDisposable public void RedrawObject( int tableIndex, RedrawType settings ) { - if( tableIndex >= 0 && tableIndex < Dalamud.Objects.Length ) + if( tableIndex >= 0 && tableIndex < DalamudServices.Objects.Length ) { - RedrawObject( Dalamud.Objects[ tableIndex ], settings ); + RedrawObject( DalamudServices.Objects[ tableIndex ], settings ); } } @@ -352,7 +353,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable } else { - foreach( var actor in Dalamud.Objects.Where( a => a.Name.ToString().ToLowerInvariant() == lowerName ) ) + foreach( var actor in DalamudServices.Objects.Where( a => a.Name.ToString().ToLowerInvariant() == lowerName ) ) { RedrawObject( actor, settings ); } @@ -361,7 +362,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable public void RedrawAll( RedrawType settings ) { - foreach( var actor in Dalamud.Objects ) + foreach( var actor in DalamudServices.Objects ) { RedrawObject( actor, settings ); } diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index a1e2961b..71e54d07 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Character; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; public class CutsceneCharacters : IDisposable @@ -17,8 +18,8 @@ public class CutsceneCharacters : IDisposable public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) - .Where( i => Dalamud.Objects[ i ] != null ) - .Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) ); + .Where( i => DalamudServices.Objects[ i ] != null ) + .Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) ); public CutsceneCharacters(GameEventManager events) { @@ -35,7 +36,7 @@ public class CutsceneCharacters : IDisposable { Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx ); idx = _copiedCharacters[ idx - CutsceneStartIdx ]; - return idx < 0 ? null : Dalamud.Objects[ idx ]; + return idx < 0 ? null : DalamudServices.Objects[ idx ]; } } diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index b3ffc5d3..efdb67c5 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -5,7 +5,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using Penumbra.GameData.Actors; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > @@ -29,7 +30,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; Penumbra.TempMods.CollectionChanged += CollectionChangeClear; - Dalamud.ClientState.TerritoryChanged += TerritoryClear; + DalamudServices.ClientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; _enabled = true; } @@ -43,7 +44,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; - Dalamud.ClientState.TerritoryChanged -= TerritoryClear; + DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; _enabled = false; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 60822ad0..65d032de 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -5,6 +5,7 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -137,9 +138,9 @@ public unsafe partial class PathResolver { var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ]; var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < Dalamud.Objects.Length ) + if( idx >= 0 && idx < DalamudServices.Objects.Length ) { - var obj = Dalamud.Objects[ idx ]; + var obj = DalamudServices.Objects[ idx ]; return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; } } @@ -203,9 +204,9 @@ public unsafe partial class PathResolver if( timelinePtr != IntPtr.Zero ) { var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); - if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) + if( actorIdx >= 0 && actorIdx < DalamudServices.Objects.Length ) { - _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); + _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); } } @@ -233,14 +234,14 @@ public unsafe partial class PathResolver private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) { - var owner = Dalamud.Objects.SearchById( id ); + var owner = DalamudServices.Objects.SearchById( id ); if( owner == null ) { return null; } var idx = ( ( GameObject* )owner.Address )->ObjectIndex; - return Dalamud.Objects[ idx + 1 ]; + return DalamudServices.Objects[ idx + 1 ]; } private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) @@ -251,8 +252,8 @@ public unsafe partial class PathResolver { var obj = vfxParams->GameObjectType switch { - 0 => Dalamud.Objects.SearchById( vfxParams->GameObjectId ), - 2 => Dalamud.Objects[ ( int )vfxParams->GameObjectId ], + 0 => DalamudServices.Objects.SearchById( vfxParams->GameObjectId ), + 2 => DalamudServices.Objects[ ( int )vfxParams->GameObjectId ], 4 => GetOwnedObject( vfxParams->GameObjectId ), _ => null, }; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 16c321d4..efb7e61c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -12,7 +12,8 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; using Penumbra.Util; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver @@ -113,7 +114,7 @@ public unsafe partial class PathResolver // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) { - gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx ); + gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); var draw = ( DrawObject* )drawObject; if( gameObject != null && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) @@ -251,9 +252,9 @@ public unsafe partial class PathResolver // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - for( var i = 0; i < Dalamud.Objects.Length; ++i ) + for( var i = 0; i < DalamudServices.Objects.Length; ++i ) { - var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); + var ptr = ( GameObject* )DalamudServices.Objects.GetObjectAddress( i ); if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) { _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 85839f24..cacd3b20 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -8,6 +8,7 @@ using OtterGui; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Services; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -37,7 +38,7 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !Dalamud.ClientState.IsLoggedIn ) + if( !DalamudServices.ClientState.IsLoggedIn ) { var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) @@ -46,7 +47,7 @@ public unsafe partial class PathResolver } // Aesthetician. The relevant actor is yourself, so use player collection when possible. - if( Dalamud.GameGui.GetAddonByName( "ScreenLog" ) == IntPtr.Zero ) + if( DalamudServices.GameGui.GetAddonByName( "ScreenLog" ) == IntPtr.Zero ) { var player = Penumbra.Actors.GetCurrentPlayer(); var collection2 = ( player.IsValid ? CollectionByIdentifier( player ) : null ) @@ -86,7 +87,7 @@ public unsafe partial class PathResolver public static ModCollection PlayerCollection() { using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - var gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( 0 ); + var gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( 0 ); if( gameObject == null ) { return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 8e60cf91..b64323a2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -34,7 +35,7 @@ public partial class PathResolver : IDisposable private readonly SubfileHelper _subFiles; static PathResolver() - => ValidHumanModels = GetValidHumanModels( Dalamud.GameData ); + => ValidHumanModels = GetValidHumanModels( DalamudServices.GameData ); public unsafe PathResolver( ResourceLoader loader ) { diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 07dc0aff..cc3add2d 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -140,7 +141,7 @@ public unsafe class ImcFile : MetaBaseFile public override void Reset() { - var file = Dalamud.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.GameData.GetFile( Path.ToString() ); fixed( byte* ptr = file!.Data ) { MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); @@ -152,7 +153,7 @@ public unsafe class ImcFile : MetaBaseFile : base( 0 ) { Path = manip.GamePath(); - var file = Dalamud.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.GameData.GetFile( Path.ToString() ); if( file == null ) { throw new ImcException( manip, Path ); @@ -171,7 +172,7 @@ public unsafe class ImcFile : MetaBaseFile public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) { - var file = Dalamud.GameData.GetFile( path ); + var file = DalamudServices.GameData.GetFile( path ); exists = false; if( file == null ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index e479afc1..8358b97d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -8,6 +8,7 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.ItemSwap; @@ -38,7 +39,7 @@ public static class ItemSwap return true; } - var file = Dalamud.GameData.GetFile( path.InternalName.ToString() ); + var file = DalamudServices.GameData.GetFile( path.InternalName.ToString() ); if( file != null ) { data = file.Data; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 332aa6ac..6cca0356 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -8,7 +8,8 @@ using System.Linq; using System.Security.Cryptography; using Penumbra.GameData.Enums; using static Penumbra.Mods.ItemSwap.ItemSwap; - +using Penumbra.Services; + namespace Penumbra.Mods.ItemSwap; public class Swap @@ -139,7 +140,7 @@ public sealed class FileSwap : Swap } swap.SwapToModded = redirections( swap.SwapToRequestPath ); - swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && Dalamud.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); + swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); swap.FileData = type switch diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index 21841d3a..ad321b5e 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -4,13 +4,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; - +using Penumbra.Services; + namespace Penumbra.Mods; public sealed partial class Mod { public static DirectoryInfo LocalDataDirectory - => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data" )); + => new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data" )); public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); @@ -21,7 +22,7 @@ public sealed partial class Mod public bool Favorite { get; private set; } = false; private FileInfo LocalDataFile - => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" )); + => new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" )); private ModDataChangeType LoadLocalData() { @@ -113,8 +114,8 @@ public sealed partial class Mod private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod ) { - var oldFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" ); - var newFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" ); + var oldFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" ); + var newFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" ); if( File.Exists( oldFile ) ) { try diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 9b75dbc4..977db6f3 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -5,13 +5,14 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; - +using Penumbra.Services; + namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { public static string ModFileSystemFile - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); + => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); // Save the current sort order. // Does not save or copy the backup in the current mod directory, diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8d8c70d2..d14f94e3 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; using Lumina.Excel.GeneratedSheets; +using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Log; @@ -31,6 +32,42 @@ using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; namespace Penumbra; +public class PenumbraNew +{ + public string Name + => "Penumbra"; + + public static readonly Logger Log = new(); + public readonly StartTimeTracker< StartTimeType > StartTimer = new(); + + public readonly IServiceCollection Services = new ServiceCollection(); + + + public PenumbraNew( DalamudPluginInterface pi ) + { + using var time = StartTimer.Measure( StartTimeType.Total ); + + // Add meta services. + Services.AddSingleton( Log ); + Services.AddSingleton( StartTimer ); + Services.AddSingleton< ValidityChecker >(); + Services.AddSingleton< PerformanceTracker< PerformanceType > >(); + + // Add Dalamud services + var dalamud = new DalamudServices( pi ); + dalamud.AddServices( Services ); + + // Add Game Data + + + // Add Configuration + Services.AddSingleton< Configuration >(); + } + + public void Dispose() + { } +} + public class Penumbra : IDalamudPlugin { public string Name @@ -87,22 +124,24 @@ public class Penumbra : IDalamudPlugin try { - Dalamud.Initialize( pluginInterface ); + DalamudServices.Initialize( pluginInterface ); - Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); + Performance = new PerformanceTracker< PerformanceType >( DalamudServices.Framework ); Log = new Logger(); - ValidityChecker = new ValidityChecker( Dalamud.PluginInterface ); + ValidityChecker = new ValidityChecker( DalamudServices.PluginInterface ); GameEvents = new GameEventManager(); - StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ) ); + StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( DalamudServices.PluginInterface, DalamudServices.GameData ) ); StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() ); - StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ) ); - ItemData = StartTimer.Measure( StartTimeType.Items, () => new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ) ); + StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( DalamudServices.PluginInterface, DalamudServices.GameData ) ); + ItemData = StartTimer.Measure( StartTimeType.Items, + () => new ItemData( DalamudServices.PluginInterface, DalamudServices.GameData, DalamudServices.GameData.Language ) ); StartTimer.Measure( StartTimeType.Actors, - () => Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.Framework, Dalamud.GameData, Dalamud.GameGui, + () => Actors = new ActorManager( DalamudServices.PluginInterface, DalamudServices.Objects, DalamudServices.ClientState, DalamudServices.Framework, + DalamudServices.GameData, DalamudServices.GameGui, ResolveCutscene ) ); - Framework = new FrameworkManager( Dalamud.Framework, Log ); + Framework = new FrameworkManager( DalamudServices.Framework, Log ); CharacterUtility = new CharacterUtility(); StartTimer.Measure( StartTimeType.Backup, () => Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ) ); @@ -147,7 +186,7 @@ public class Penumbra : IDalamudPlugin using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) { Api = new PenumbraApi( this ); - IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); + IpcProviders = new PenumbraIpcProviders( DalamudServices.PluginInterface, Api ); HttpApi = new HttpApi( Api ); if( Config.EnableHttpApi ) { @@ -159,7 +198,7 @@ public class Penumbra : IDalamudPlugin ValidityChecker.LogExceptions(); Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); - OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); + OtterTex.NativeDll.Initialize( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName ); Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); if( CharacterUtility.Ready ) @@ -186,19 +225,19 @@ public class Penumbra : IDalamudPlugin }; var btn = new LaunchButton( cfg ); var system = new WindowSystem( Name ); - var cmd = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); + var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); system.AddWindow( cfg ); system.AddWindow( cfg.ModEditPopup ); system.AddWindow( changelog ); if( !_disposed ) { - _changelog = changelog; - ConfigWindow = cfg; - _windowSystem = system; - _launchButton = btn; - _commandHandler = cmd; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; - Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + _changelog = changelog; + ConfigWindow = cfg; + _windowSystem = system; + _launchButton = btn; + _commandHandler = cmd; + DalamudServices.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; + DalamudServices.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; } else { @@ -214,13 +253,13 @@ public class Penumbra : IDalamudPlugin { if( _windowSystem != null ) { - Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; } _launchButton?.Dispose(); if( ConfigWindow != null ) { - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; + DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; ConfigWindow.Dispose(); } } @@ -331,7 +370,7 @@ public class Penumbra : IDalamudPlugin ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() : new List< FileInfo >(); list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); - list.Add( Dalamud.PluginInterface.ConfigFile ); + list.Add( DalamudServices.PluginInterface.ConfigFile ); list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); return list; @@ -352,7 +391,8 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); - sb.Append( $"> **`Synchronous Load (Dalamud): `** {( Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); + sb.Append( + $"> **`Synchronous Load (Dalamud): `** {( DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.AppendLine( "**Mods**" ); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index a97d1d90..cddc5812 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -71,6 +71,7 @@ +
diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs new file mode 100644 index 00000000..b08055d7 --- /dev/null +++ b/Penumbra/PenumbraNew.cs @@ -0,0 +1,50 @@ +using System.IO; +using Dalamud.Plugin; +using Microsoft.Extensions.DependencyInjection; +using OtterGui.Classes; +using OtterGui.Log; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.Interop; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra; + +public class PenumbraNew +{ + public string Name + => "Penumbra"; + + public static readonly Logger Log = new(); + public readonly StartTimeTracker StartTimer = new(); + + public readonly IServiceCollection Services = new ServiceCollection(); + + + public PenumbraNew(DalamudPluginInterface pi) + { + using var time = StartTimer.Measure(StartTimeType.Total); + + // Add meta services. + Services.AddSingleton(Log); + Services.AddSingleton(StartTimer); + Services.AddSingleton(); + Services.AddSingleton>(); + + // Add Dalamud services + var dalamud = new DalamudServices(pi); + dalamud.AddServices(Services); + + // Add Game Data + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); + + // Add Configuration + Services.AddSingleton(); + } + + public void Dispose() + { } +} \ No newline at end of file diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs new file mode 100644 index 00000000..42d52067 --- /dev/null +++ b/Penumbra/Services/DalamudServices.cs @@ -0,0 +1,168 @@ +using System; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Interface; +using Dalamud.IoC; +using Dalamud.Plugin; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Penumbra.Services; + +public class DalamudServices +{ + public DalamudServices(DalamudPluginInterface pluginInterface) + { + pluginInterface.Inject(this); + try + { + var serviceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); + var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); + var interfaceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); + if (serviceType == null || configType == null || interfaceType == null) + { + return; + } + + var configService = serviceType.MakeGenericType(configType); + var interfaceService = serviceType.MakeGenericType(interfaceType); + var configGetter = configService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + _interfaceGetter = interfaceService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (configGetter == null || _interfaceGetter == null) + { + return; + } + + _dalamudConfig = configGetter.Invoke(null, null); + if (_dalamudConfig != null) + { + _saveDalamudConfig = _dalamudConfig.GetType().GetMethod("Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (_saveDalamudConfig == null) + { + _dalamudConfig = null; + _interfaceGetter = null; + } + } + } + catch + { + _dalamudConfig = null; + _saveDalamudConfig = null; + _interfaceGetter = null; + } + } + + public void AddServices(IServiceCollection services) + { + services.AddSingleton(PluginInterface); + services.AddSingleton(Commands); + services.AddSingleton(GameData); + services.AddSingleton(ClientState); + services.AddSingleton(Chat); + services.AddSingleton(Framework); + services.AddSingleton(Conditions); + services.AddSingleton(Targets); + services.AddSingleton(Objects); + services.AddSingleton(TitleScreenMenu); + services.AddSingleton(GameGui); + services.AddSingleton(KeyState); + services.AddSingleton(SigScanner); + services.AddSingleton(this); + } + + // @formatter:off + [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; + // @formatter:on + + public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; + + private readonly object? _dalamudConfig; + private readonly MethodInfo? _interfaceGetter; + private readonly MethodInfo? _saveDalamudConfig; + + public bool GetDalamudConfig(string fieldName, out T? value) + { + value = default; + try + { + if (_dalamudConfig == null) + { + return false; + } + + var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (getter == null) + { + return false; + } + + var result = getter.GetValue(_dalamudConfig); + if (result is not T v) + { + return false; + } + + value = v; + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while fetching Dalamud Config {fieldName}:\n{e}"); + return false; + } + } + + public bool SetDalamudConfig(string fieldName, in T? value, string? windowFieldName = null) + { + try + { + if (_dalamudConfig == null) + { + return false; + } + + var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (getter == null) + { + return false; + } + + getter.SetValue(_dalamudConfig, value); + if (windowFieldName != null) + { + var inter = _interfaceGetter!.Invoke(null, null); + var settingsWindow = inter?.GetType().GetField("settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(inter); + settingsWindow?.GetType().GetField(windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(settingsWindow, value); + } + + _saveDalamudConfig!.Invoke(_dalamudConfig, null); + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while fetching Dalamud Config {fieldName}:\n{e}"); + return false; + } + } +} \ No newline at end of file diff --git a/Penumbra/Services/ObjectIdentifier.cs b/Penumbra/Services/ObjectIdentifier.cs new file mode 100644 index 00000000..7223017b --- /dev/null +++ b/Penumbra/Services/ObjectIdentifier.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Dalamud.Data; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Classes; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Util; +using Action = System.Action; + +namespace Penumbra.Services; + +public sealed class ObjectIdentifier : IObjectIdentifier +{ + private const string Prefix = $"[{nameof(ObjectIdentifier)}]"; + + public IObjectIdentifier? Identifier { get; private set; } + + public bool IsDisposed { get; private set; } + + public bool Ready + => Identifier != null && !IsDisposed; + + public event Action? FinishedCreation; + + public ObjectIdentifier(StartTimeTracker tracker, DalamudPluginInterface pi, DataManager data) + { + Task.Run(() => + { + using var timer = tracker.Measure(StartTimeType.Identifier); + var identifier = GameData.GameData.GetIdentifier(pi, data); + if (IsDisposed) + { + identifier.Dispose(); + } + else + { + Identifier = identifier; + Penumbra.Log.Verbose($"{Prefix} Created."); + FinishedCreation?.Invoke(); + } + }); + } + + public void Dispose() + { + Identifier?.Dispose(); + IsDisposed = true; + Penumbra.Log.Verbose($"{Prefix} Disposed."); + } + + public IGamePathParser GamePathParser + => Identifier?.GamePathParser ?? throw new Exception($"{Prefix} Not yet ready."); + + public void Identify(IDictionary set, string path) + => Identifier?.Identify(set, path); + + public Dictionary Identify(string path) + => Identifier?.Identify(path) ?? new Dictionary(); + + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + => Identifier?.Identify(setId, weaponType, variant, slot) ?? Array.Empty(); +} diff --git a/Penumbra/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs similarity index 89% rename from Penumbra/ValidityChecker.cs rename to Penumbra/Services/ValidityChecker.cs index 5e71d998..d86167bc 100644 --- a/Penumbra/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; using Penumbra.Util; @@ -20,11 +21,18 @@ public class ValidityChecker public readonly List ImcExceptions = new(); + public readonly string Version; + public readonly string CommitHash; + public ValidityChecker(DalamudPluginInterface pi) { DevPenumbraExists = CheckDevPluginPenumbra(pi); IsNotInstalledPenumbra = CheckIsNotInstalled(pi); IsValidSourceRepo = CheckSourceRepo(pi); + + var assembly = Assembly.GetExecutingAssembly(); + Version = assembly.GetName().Version?.ToString() ?? string.Empty; + CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; } public void LogExceptions() diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 19630beb..1e0b609b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -10,6 +10,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.UI.Classes; @@ -82,7 +83,7 @@ public partial class ModEditWindow _fileDialog.Reset(); try { - var file = Dalamud.GameData.GetFile( _defaultPath ); + var file = DalamudServices.GameData.GetFile( _defaultPath ); if( file != null ) { _defaultException = null; diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 8c8cc508..5bb82fb3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -10,6 +10,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -80,7 +81,7 @@ public partial class ModEditWindow LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : Dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + : DalamudServices.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); LoadedShpkPathName = path.ToPath(); } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 497a2f0e..f1b9dbe3 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,7 +14,8 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; - +using Penumbra.Services; + namespace Penumbra.UI.Classes; public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > @@ -25,7 +26,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModFileSystemSelector( ModFileSystem fileSystem ) - : base( fileSystem, Dalamud.KeyState ) + : base( fileSystem, DalamudServices.KeyState ) { SubscribeRightClickFolder( EnableDescendants, 10 ); SubscribeRightClickFolder( DisableDescendants, 10 ); diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 528329cd..d89d554e 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -11,7 +11,8 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Components; using OtterGui.Widgets; using Penumbra.GameData.Actors; - +using Penumbra.Services; + namespace Penumbra.UI; public partial class ConfigWindow @@ -287,7 +288,7 @@ public partial class ConfigWindow private static bool DrawNewTargetCollection( Vector2 width ) { - var target = Penumbra.Actors.FromObject( Dalamud.Targets.Target, false, true, true ); + var target = Penumbra.Actors.FromObject( DalamudServices.Targets.Target, false, true, true ); var result = Penumbra.CollectionManager.Individuals.CanAdd( target ); var tt = result switch { diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 6ca18204..b59ce96a 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -14,6 +14,7 @@ using Penumbra.GameData.Files; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.Util; using static OtterGui.Raii.ImRaii; @@ -207,7 +208,7 @@ public partial class ConfigWindow DrawSpecial( "Current Card", Penumbra.Actors.GetCardPlayer() ); DrawSpecial( "Current Glamour", Penumbra.Actors.GetGlamourPlayer() ); - foreach( var obj in Dalamud.Objects ) + foreach( var obj in DalamudServices.Objects ) { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); @@ -243,7 +244,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( idx.ToString() ); ImGui.TableNextColumn(); - var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); + var obj = ( GameObject* )DalamudServices.Objects.GetObjectAddress( idx ); var (address, name) = obj != null ? ( $"0x{( ulong )obj:X}", new ByteString( obj->Name ).ToString() ) : ( "NULL", "NULL" ); ImGui.TextUnformatted( address ); @@ -541,7 +542,7 @@ public partial class ConfigWindow // Draw information about the models, materials and resources currently loaded by the local player. private static unsafe void DrawPlayerModelInfo() { - var player = Dalamud.ClientState.LocalPlayer; + var player = DalamudServices.ClientState.LocalPlayer; var name = player?.Name.ToString() ?? "NULL"; if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs index 4893c6da..6297e807 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.GameFonts; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -16,7 +17,7 @@ public partial class ConfigWindow { // We use a big, nice game font for the title. private readonly GameFontHandle _nameFont = - Dalamud.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); + DalamudServices.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); // Header data. private string _modName = string.Empty; diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 079db72d..d08a50c6 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -9,7 +9,8 @@ using System.Numerics; using Dalamud.Interface; using OtterGui.Widgets; using Penumbra.Api.Enums; - +using Penumbra.Services; + namespace Penumbra.UI; public partial class ConfigWindow @@ -121,7 +122,7 @@ public partial class ConfigWindow ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); } - using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); + using var disabled = ImRaii.Disabled( DalamudServices.ClientState.LocalPlayer == null ); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; DrawButton( buttonWidth, "All", string.Empty ); diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index 46a322b5..ecb294b5 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -11,6 +11,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Interop.Loader; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.UI; @@ -50,7 +51,7 @@ public partial class ConfigWindow ImGui.NewLine(); unsafe { - ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{( ulong )ResourceLoader.ResourceManager - ( ulong )Dalamud.SigScanner.Module.BaseAddress:X})" ); + ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{( ulong )ResourceLoader.ResourceManager - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index f05c52cb..4bc6454e 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -3,7 +3,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Interop; - +using Penumbra.Services; + namespace Penumbra.UI; public partial class ConfigWindow @@ -104,7 +105,7 @@ public partial class ConfigWindow private static void DrawWaitForPluginsReflection() { - if( !Dalamud.GetDalamudConfig( Dalamud.WaitingForPluginsOption, out bool value ) ) + if( !DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) { using var disabled = ImRaii.Disabled(); Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); @@ -112,7 +113,7 @@ public partial class ConfigWindow else { Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => Dalamud.SetDalamudConfig( Dalamud.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); + v => DalamudServices.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); } } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index d8f5808d..6995d7a3 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -6,7 +6,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; - +using Penumbra.Services; + namespace Penumbra.UI; public partial class ConfigWindow @@ -76,21 +77,21 @@ public partial class ConfigWindow v => { Penumbra.Config.HideUiWhenUiHidden = v; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !v; + DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !v; } ); Checkbox( "Hide Config Window when in Cutscenes", "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, v => { Penumbra.Config.HideUiInCutscenes = v; - Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !v; + DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !v; } ); Checkbox( "Hide Config Window when in GPose", "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, v => { Penumbra.Config.HideUiInGPose = v; - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; + DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 9ac59622..8300e83e 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,6 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -117,7 +118,7 @@ public partial class ConfigWindow return ( "Path is not allowed to be in ProgramFiles.", false ); } - var dalamud = Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; + var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; if( IsSubPathOf( dalamud.FullName, newName ) ) { return ( "Path is not allowed to be inside your Dalamud directories.", false ); @@ -128,7 +129,7 @@ public partial class ConfigWindow return ( "Path is not allowed to be inside your Downloads folder.", false ); } - var gameDir = Dalamud.GameData.GameData.DataPath.Parent!.Parent!.FullName; + var gameDir = DalamudServices.GameData.GameData.DataPath.Parent!.Parent!.FullName; if( IsSubPathOf( gameDir, newName ) ) { return ( "Path is not allowed to be inside your game folder.", false ); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f836d7e5..7342037c 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -8,6 +8,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; @@ -54,9 +55,9 @@ public sealed partial class ConfigWindow : Window, IDisposable Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; } - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; - Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; - Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; + DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; + DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; + DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { @@ -96,14 +97,14 @@ public sealed partial class ConfigWindow : Window, IDisposable else if( !Penumbra.ValidityChecker.IsValidSourceRepo ) { DrawProblemWindow( - $"You are loading a release version of Penumbra from the repository \"{Dalamud.PluginInterface.SourceRepository}\" instead of the official repository.\n" + $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); } else if( Penumbra.ValidityChecker.IsNotInstalledPenumbra ) { DrawProblemWindow( - $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); @@ -111,7 +112,7 @@ public sealed partial class ConfigWindow : Window, IDisposable else if( Penumbra.ValidityChecker.DevPenumbraExists ) { DrawProblemWindow( - $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index b25e4646..39f9cdd1 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -2,7 +2,8 @@ using System; using System.IO; using Dalamud.Interface; using ImGuiScene; - +using Penumbra.Services; + namespace Penumbra.UI; // A Launch Button used in the title screen of the game, @@ -21,17 +22,17 @@ public class LaunchButton : IDisposable void CreateEntry() { - _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + _icon = DalamudServices.PluginInterface.UiBuilder.LoadImage( Path.Combine( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName!, "tsmLogo.png" ) ); if( _icon != null ) { - _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); + _entry = DalamudServices.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); } - Dalamud.PluginInterface.UiBuilder.Draw -= CreateEntry; + DalamudServices.PluginInterface.UiBuilder.Draw -= CreateEntry; } - Dalamud.PluginInterface.UiBuilder.Draw += CreateEntry; + DalamudServices.PluginInterface.UiBuilder.Draw += CreateEntry; } private void OnTriggered() @@ -42,7 +43,7 @@ public class LaunchButton : IDisposable _icon?.Dispose(); if( _entry != null ) { - Dalamud.TitleScreenMenu.RemoveEntry( _entry ); + DalamudServices.TitleScreenMenu.RemoveEntry( _entry ); } } } \ No newline at end of file diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs index ecef1b57..aff2e67b 100644 --- a/Penumbra/Util/ChatUtil.cs +++ b/Penumbra/Util/ChatUtil.cs @@ -6,7 +6,8 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; - +using Penumbra.Services; + namespace Penumbra.Util; public static class ChatUtil @@ -30,7 +31,7 @@ public static class ChatUtil var payload = new SeString( payloadList ); - Dalamud.Chat.PrintChat( new XivChatEntry + DalamudServices.Chat.PrintChat( new XivChatEntry { Message = payload, } ); @@ -47,7 +48,7 @@ public static class ChatUtil NotificationType.Info => Logger.LogLevel.Information, _ => Logger.LogLevel.Debug, }; - Dalamud.PluginInterface.UiBuilder.AddNotification( content, title, type ); + DalamudServices.PluginInterface.UiBuilder.AddNotification( content, title, type ); Penumbra.Log.Message( logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}" ); } } \ No newline at end of file diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index e5941ea8..de692ad8 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -6,7 +6,6 @@ public enum StartTimeType { Total, Identifier, - GamePathParser, Stains, Items, Actors, @@ -53,7 +52,6 @@ public static class TimingExtensions { StartTimeType.Total => "Total Construction", StartTimeType.Identifier => "Identification Data", - StartTimeType.GamePathParser => "Game Path Data", StartTimeType.Stains => "Stain Data", StartTimeType.Items => "Item Data", StartTimeType.Actors => "Actor Data", diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index d116395f..26a67367 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,6 +11,15 @@ "Unosquare.Swan.Lite": "3.0.0" } }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0" + } + }, "SharpCompress": { "type": "Direct", "requested": "[0.32.1, )", @@ -27,6 +36,11 @@ "System.Text.Encoding.CodePages": "5.0.0" } }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" + }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "5.0.0", @@ -67,8 +81,8 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.5, )", - "Penumbra.String": "[1.0.1, )" + "Penumbra.Api": "[1.0.7, )", + "Penumbra.String": "[1.0.3, )" } }, "penumbra.string": { From bdaff7b781fe201ffb831f05c129742804d4b5d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Mar 2023 17:50:32 +0100 Subject: [PATCH 0803/2451] This is going rather well. --- OtterGui | 2 +- Penumbra/Api/IpcTester.cs | 6 +- Penumbra/Api/PenumbraApi.cs | 976 ++++++++---------- Penumbra/Api/TempCollectionManager.cs | 119 +++ Penumbra/Api/TempModManager.cs | 333 ++---- .../Collections/CollectionManager.Active.cs | 383 ++++--- Penumbra/Collections/CollectionManager.cs | 312 +++--- Penumbra/Collections/IndividualCollections.cs | 7 +- Penumbra/Collections/ModCollection.File.cs | 12 +- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/Configuration.Migration.cs | 370 ------- Penumbra/Configuration.cs | 73 +- Penumbra/Interop/CharacterUtility.cs | 11 +- .../Interop/Loader/ResourceLoader.Debug.cs | 15 - Penumbra/Interop/Loader/ResourceLoader.cs | 34 - Penumbra/Interop/Loader/ResourceLogger.cs | 98 -- .../Interop/Resolver/CutsceneCharacters.cs | 50 +- .../Resolver/IdentifiedCollectionCache.cs | 80 +- .../Resolver/PathResolver.DrawObjectState.cs | 164 ++- .../Resolver/PathResolver.Identification.cs | 2 +- .../Resolver/PathResolver.ResolverHooks.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 123 ++- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- Penumbra/Mods/Mod.LocalData.cs | 5 +- Penumbra/Mods/ModFileSystem.cs | 15 +- Penumbra/Penumbra.cs | 410 +++----- Penumbra/PenumbraNew.cs | 65 +- Penumbra/Services/BackupService.cs | 29 + Penumbra/Services/CommunicatorService.cs | 30 + Penumbra/Services/ConfigMigrationService.cs | 377 +++++++ Penumbra/Services/DalamudServices.cs | 29 +- Penumbra/Services/FilenameService.cs | 51 + Penumbra/Services/ObjectIdentifier.cs | 66 -- Penumbra/Services/ServiceWrapper.cs | 117 +++ Penumbra/Services/StainService.cs | 43 + Penumbra/Services/Wrappers.cs | 37 + Penumbra/UI/Classes/ItemSwapWindow.cs | 451 ++++---- .../ModEditWindow.Materials.ColorSet.cs | 16 +- Penumbra/UI/Classes/ModEditWindow.cs | 8 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 425 ++++---- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 13 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 15 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 4 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 6 +- Penumbra/UI/ConfigWindow.cs | 108 +- Penumbra/Util/EventWrapper.cs | 329 ++++++ Penumbra/Util/PerformanceType.cs | 35 +- Penumbra/Util/StainManager.cs | 36 - 48 files changed, 2944 insertions(+), 2952 deletions(-) create mode 100644 Penumbra/Api/TempCollectionManager.cs delete mode 100644 Penumbra/Configuration.Migration.cs delete mode 100644 Penumbra/Interop/Loader/ResourceLogger.cs create mode 100644 Penumbra/Services/BackupService.cs create mode 100644 Penumbra/Services/CommunicatorService.cs create mode 100644 Penumbra/Services/ConfigMigrationService.cs create mode 100644 Penumbra/Services/FilenameService.cs delete mode 100644 Penumbra/Services/ObjectIdentifier.cs create mode 100644 Penumbra/Services/ServiceWrapper.cs create mode 100644 Penumbra/Services/StainService.cs create mode 100644 Penumbra/Services/Wrappers.cs create mode 100644 Penumbra/Util/EventWrapper.cs delete mode 100644 Penumbra/Util/StainManager.cs diff --git a/OtterGui b/OtterGui index d7867dfa..3d346700 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d7867dfa6579d4e69876753e9cde72e13d3372ce +Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 6c097704..7e7fac3b 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1404,10 +1404,10 @@ public class IpcTester : IDisposable return; } - foreach( var collection in Penumbra.TempMods.CustomCollections.Values ) + foreach( var collection in Penumbra.TempCollections.Values ) { ImGui.TableNextColumn(); - var character = Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; + var character = Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; if( ImGui.Button( $"Save##{collection.Name}" ) ) { Mod.TemporaryMod.SaveTempCollection( collection, character ); @@ -1416,7 +1416,7 @@ public class IpcTester : IDisposable ImGuiUtil.DrawTableColumn( collection.Name ); ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() ); ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" ); - ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); + ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 88181927..7105ec72 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,22 +18,23 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 19 ); + => (4, 19); - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; + private CommunicatorService? _communicator; + private Penumbra? _penumbra; + private Lumina.GameData? _lumina; - private readonly Dictionary< ModCollection, ModCollection.ModSettingChangeDelegate > _delegates = new(); + private readonly Dictionary _delegates = new(); - public event Action< string >? PreSettingsPanelDraw; - public event Action< string >? PostSettingsPanelDraw; + public event Action? PreSettingsPanelDraw; + public event Action? PostSettingsPanelDraw; public event GameObjectRedrawnDelegate? GameObjectRedrawn { @@ -82,35 +83,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool Valid => _penumbra != null; - public unsafe PenumbraApi( Penumbra penumbra ) + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra) { - _penumbra = penumbra; - _lumina = ( Lumina.GameData? )DalamudServices.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( DalamudServices.GameData ); - foreach( var collection in Penumbra.CollectionManager ) - { - SubscribeToCollection( collection ); - } + _communicator = communicator; + _penumbra = penumbra; + _lumina = (Lumina.GameData?)DalamudServices.GameData.GetType() + .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(DalamudServices.GameData); + foreach (var collection in Penumbra.CollectionManager) + SubscribeToCollection(collection); - Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; - Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; - Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; + _communicator.CollectionChange.Event += SubscribeToNewCollections; + Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; + Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; } public unsafe void Dispose() { - Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; - Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; - Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; - _penumbra = null; - _lumina = null; - foreach( var collection in Penumbra.CollectionManager ) + Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator!.CollectionChange.Event -= SubscribeToNewCollections; + Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; + _penumbra = null; + _lumina = null; + _communicator = null; + foreach (var collection in Penumbra.CollectionManager) { - if( _delegates.TryGetValue( collection, out var del ) ) - { + if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; - } } } @@ -122,17 +121,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } - private unsafe void OnResourceLoaded( ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData ) + private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData) { - if( resolveData.AssociatedGameObject != IntPtr.Zero ) - { - GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString() ); - } + if (resolveData.AssociatedGameObject != IntPtr.Zero) + GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString()); } - public event Action< string, bool >? ModDirectoryChanged + public event Action? ModDirectoryChanged { add { @@ -149,7 +146,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool GetEnabledState() => Penumbra.Config.EnableMods; - public event Action< bool >? EnabledChange + public event Action? EnabledChange { add { @@ -166,42 +163,32 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetConfiguration() { CheckInitialized(); - return JsonConvert.SerializeObject( Penumbra.Config, Formatting.Indented ); + return JsonConvert.SerializeObject(Penumbra.Config, Formatting.Indented); } - public event ChangedItemHover? ChangedItemTooltip; + public event ChangedItemHover? ChangedItemTooltip; public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - public PenumbraApiEc OpenMainWindow( TabType tab, string modDirectory, string modName ) + public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) { CheckInitialized(); - if( _penumbra!.ConfigWindow == null ) - { + if (_penumbra!.ConfigWindow == null) return PenumbraApiEc.SystemDisposed; - } _penumbra!.ConfigWindow.IsOpen = true; - if( !Enum.IsDefined( tab ) ) - { + if (!Enum.IsDefined(tab)) return PenumbraApiEc.InvalidArgument; - } - if( tab != TabType.None ) - { + if (tab != TabType.None) _penumbra!.ConfigWindow.SelectTab = tab; - } - if( tab == TabType.Mods && ( modDirectory.Length > 0 || modName.Length > 0 ) ) + if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { - if( Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { - _penumbra!.ConfigWindow.SelectMod( mod ); - } + if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + _penumbra!.ConfigWindow.SelectMod(mod); else - { return PenumbraApiEc.ModMissing; - } } return PenumbraApiEc.Success; @@ -210,295 +197,269 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void CloseMainWindow() { CheckInitialized(); - if( _penumbra!.ConfigWindow == null ) - { + if (_penumbra!.ConfigWindow == null) return; - } _penumbra!.ConfigWindow.IsOpen = false; } - public void RedrawObject( int tableIndex, RedrawType setting ) + public void RedrawObject(int tableIndex, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( tableIndex, setting ); + _penumbra!.ObjectReloader.RedrawObject(tableIndex, setting); } - public void RedrawObject( string name, RedrawType setting ) + public void RedrawObject(string name, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( name, setting ); + _penumbra!.ObjectReloader.RedrawObject(name, setting); } - public void RedrawObject( GameObject? gameObject, RedrawType setting ) + public void RedrawObject(GameObject? gameObject, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); + _penumbra!.ObjectReloader.RedrawObject(gameObject, setting); } - public void RedrawAll( RedrawType setting ) + public void RedrawAll(RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawAll( setting ); + _penumbra!.ObjectReloader.RedrawAll(setting); } - public string ResolveDefaultPath( string path ) + public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); + return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Default); } - public string ResolveInterfacePath( string path ) + public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Interface ); + return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Interface); } - public string ResolvePlayerPath( string path ) + public string ResolvePlayerPath(string path) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, PathResolver.PlayerCollection() ); + return ResolvePath(path, Penumbra.ModManager, PathResolver.PlayerCollection()); } // TODO: cleanup when incrementing API level - public string ResolvePath( string path, string characterName ) - => ResolvePath( path, characterName, ushort.MaxValue ); + public string ResolvePath(string path, string characterName) + => ResolvePath(path, characterName, ushort.MaxValue); - public string ResolveGameObjectPath( string path, int gameObjectIdx ) + public string ResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - AssociatedCollection( gameObjectIdx, out var collection ); - return ResolvePath( path, Penumbra.ModManager, collection ); + AssociatedCollection(gameObjectIdx, out var collection); + return ResolvePath(path, Penumbra.ModManager, collection); } - public string ResolvePath( string path, string characterName, ushort worldId ) + public string ResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ) ); + return ResolvePath(path, Penumbra.ModManager, + Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level - public string[] ReverseResolvePath( string path, string characterName ) - => ReverseResolvePath( path, characterName, ushort.MaxValue ); + public string[] ReverseResolvePath(string path, string characterName) + => ReverseResolvePath(path, characterName, ushort.MaxValue); - public string[] ReverseResolvePath( string path, string characterName, ushort worldId ) + public string[] ReverseResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName, worldId ) ).ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public string[] ReverseResolveGameObjectPath( string path, int gameObjectIdx ) + public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - AssociatedCollection( gameObjectIdx, out var collection ); - var ret = collection.ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + AssociatedCollection(gameObjectIdx, out var collection); + var ret = collection.ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public string[] ReverseResolvePlayerPath( string path ) + public string[] ReverseResolvePlayerPath(string path) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return new[] { path }; - } + if (!Penumbra.Config.EnableMods) + return new[] + { + path, + }; - var ret = PathResolver.PlayerCollection().ReverseResolvePath( new FullPath( path ) ); - return ret.Select( r => r.ToString() ).ToArray(); + var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); + return ret.Select(r => r.ToString()).ToArray(); } - public (string[], string[][]) ResolvePlayerPaths( string[] forward, string[] reverse ) + public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) { CheckInitialized(); - if( !Penumbra.Config.EnableMods ) - { - return ( forward, reverse.Select( p => new[] { p } ).ToArray() ); - } + if (!Penumbra.Config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); var playerCollection = PathResolver.PlayerCollection(); - var resolved = forward.Select( p => ResolvePath( p, Penumbra.ModManager, playerCollection ) ).ToArray(); - var reverseResolved = playerCollection.ReverseResolvePaths( reverse ); - return ( resolved, reverseResolved.Select( a => a.Select( p => p.ToString() ).ToArray() ).ToArray() ); + var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray(); + var reverseResolved = playerCollection.ReverseResolvePaths(reverse); + return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolveDefaultPath( gamePath ) ); + public T? GetFile(string gamePath) where T : FileResource + => GetFileIntern(ResolveDefaultPath(gamePath)); - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + public T? GetFile(string gamePath, string characterName) where T : FileResource + => GetFileIntern(ResolvePath(gamePath, characterName)); - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + public IReadOnlyDictionary GetChangedItemsForCollection(string collectionName) { CheckInitialized(); try { - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) collection = ModCollection.Empty; - } - if( collection.HasCache ) - { - return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 ); - } + if (collection.HasCache) + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); - Penumbra.Log.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); + Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded."); + return new Dictionary(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + Penumbra.Log.Error($"Could not obtain Changed Items for {collectionName}:\n{e}"); throw; } } - public string GetCollectionForType( ApiCollectionType type ) + public string GetCollectionForType(ApiCollectionType type) { CheckInitialized(); - if( !Enum.IsDefined( type ) ) - { + if (!Enum.IsDefined(type)) return string.Empty; - } - var collection = Penumbra.CollectionManager.ByType( ( CollectionType )type ); + var collection = Penumbra.CollectionManager.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } - public (PenumbraApiEc, string OldCollection) SetCollectionForType( ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) + public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew, + bool allowDelete) { CheckInitialized(); - if( !Enum.IsDefined( type ) ) + if (!Enum.IsDefined(type)) + return (PenumbraApiEc.InvalidArgument, string.Empty); + + var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + + if (collectionName.Length == 0) { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); + if (oldCollection.Length == 0) + return (PenumbraApiEc.NothingChanged, oldCollection); + + if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) + return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + + Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type); + return (PenumbraApiEc.Success, oldCollection); } - var oldCollection = Penumbra.CollectionManager.ByType( ( CollectionType )type )?.Name ?? string.Empty; + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, oldCollection); - if( collectionName.Length == 0 ) + if (oldCollection.Length == 0) { - if( oldCollection.Length == 0 ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - if( !allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface ) - { - return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); - } - - Penumbra.CollectionManager.RemoveSpecialCollection( ( CollectionType )type ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type); + } + else if (oldCollection == collection.Name) + { + return (PenumbraApiEc.NothingChanged, oldCollection); } - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, oldCollection ); - } - - if( oldCollection.Length == 0 ) - { - if( !allowCreateNew ) - { - return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); - } - - Penumbra.CollectionManager.CreateSpecialCollection( ( CollectionType )type ); - } - else if( oldCollection == collection.Name ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } - - Penumbra.CollectionManager.SetCollection( collection, ( CollectionType )type ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.SetCollection(collection, (CollectionType)type); + return (PenumbraApiEc.Success, oldCollection); } - public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject( int gameObjectIdx ) + public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject(int gameObjectIdx) { CheckInitialized(); - var id = AssociatedIdentifier( gameObjectIdx ); - if( !id.IsValid ) - { - return ( false, false, Penumbra.CollectionManager.Default.Name ); - } + var id = AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (false, false, Penumbra.CollectionManager.Default.Name); - if( Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var collection ) ) - { - return ( true, true, collection.Name ); - } + if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) + return (true, true, collection.Name); - AssociatedCollection( gameObjectIdx, out collection ); - return ( true, false, collection.Name ); + AssociatedCollection(gameObjectIdx, out collection); + return (true, false, collection.Name); } - public (PenumbraApiEc, string OldCollection) SetCollectionForObject( int gameObjectIdx, string collectionName, bool allowCreateNew, bool allowDelete ) + public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew, + bool allowDelete) { CheckInitialized(); - var id = AssociatedIdentifier( gameObjectIdx ); - if( !id.IsValid ) + var id = AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name); + + var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + + if (collectionName.Length == 0) { - return ( PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name ); + if (oldCollection.Length == 0) + return (PenumbraApiEc.NothingChanged, oldCollection); + + if (!allowDelete) + return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); + + var idx = Penumbra.CollectionManager.Individuals.Index(id); + Penumbra.CollectionManager.RemoveIndividualCollection(idx); + return (PenumbraApiEc.Success, oldCollection); } - var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue( id, out var c ) ? c.Name : string.Empty; + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, oldCollection); - if( collectionName.Length == 0 ) + if (oldCollection.Length == 0) { - if( oldCollection.Length == 0 ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - if( !allowDelete ) - { - return ( PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection ); - } - - var idx = Penumbra.CollectionManager.Individuals.Index( id ); - Penumbra.CollectionManager.RemoveIndividualCollection( idx ); - return ( PenumbraApiEc.Success, oldCollection ); + var ids = Penumbra.CollectionManager.Individuals.GetGroup(id); + Penumbra.CollectionManager.CreateIndividualCollection(ids); + } + else if (oldCollection == collection.Name) + { + return (PenumbraApiEc.NothingChanged, oldCollection); } - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, oldCollection ); - } - - if( oldCollection.Length == 0 ) - { - if( !allowCreateNew ) - { - return ( PenumbraApiEc.AssignmentCreationDisallowed, oldCollection ); - } - - var ids = Penumbra.CollectionManager.Individuals.GetGroup( id ); - Penumbra.CollectionManager.CreateIndividualCollection( ids ); - } - else if( oldCollection == collection.Name ) - { - return ( PenumbraApiEc.NothingChanged, oldCollection ); - } - - Penumbra.CollectionManager.SetCollection( collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index( id ) ); - return ( PenumbraApiEc.Success, oldCollection ); + Penumbra.CollectionManager.SetCollection(collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index(id)); + return (PenumbraApiEc.Success, oldCollection); } - public IList< string > GetCollections() + public IList GetCollections() { CheckInitialized(); - return Penumbra.CollectionManager.Select( c => c.Name ).ToArray(); + return Penumbra.CollectionManager.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() @@ -520,158 +481,140 @@ public class PenumbraApi : IDisposable, IPenumbraApi } // TODO: cleanup when incrementing API level - public (string, bool) GetCharacterCollection( string characterName ) - => GetCharacterCollection( characterName, ushort.MaxValue ); + public (string, bool) GetCharacterCollection(string characterName) + => GetCharacterCollection(characterName, ushort.MaxValue); - public (string, bool) GetCharacterCollection( string characterName, ushort worldId ) + public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName, worldId ), out var collection ) - ? ( collection.Name, true ) - : ( Penumbra.CollectionManager.Default.Name, false ); + return Penumbra.CollectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + ? (collection.Name, true) + : (Penumbra.CollectionManager.Default.Name, false); } - public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) + public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) { CheckInitialized(); - var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.ModCollection.Name ); + var (obj, collection) = PathResolver.IdentifyDrawObject(drawObject); + return (obj, collection.ModCollection.Name); } - public int GetCutsceneParentIndex( int actorIdx ) + public int GetCutsceneParentIndex(int actorIdx) { CheckInitialized(); - return _penumbra!.PathResolver.CutsceneActor( actorIdx ); + return _penumbra!.PathResolver.CutsceneActor(actorIdx); } - public IList< (string, string) > GetModList() + public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); + return Penumbra.ModManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); } - public IDictionary< string, (IList< string >, GroupType) >? GetAvailableModSettings( string modDirectory, string modName ) + public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) { CheckInitialized(); - return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - ? mod.Groups.ToDictionary( g => g.Name, g => ( ( IList< string > )g.Select( o => o.Name ).ToList(), g.Type ) ) + return Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) : null; } - public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, - string modDirectory, string modName, bool allowInheritance ) + public (PenumbraApiEc, (bool, int, IDictionary>, bool)?) GetCurrentModSettings(string collectionName, + string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - return ( PenumbraApiEc.CollectionMissing, null ); - } + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { - return ( PenumbraApiEc.ModMissing, null ); - } + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + return (PenumbraApiEc.ModMissing, null); - var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; - if( settings == null ) - { - return ( PenumbraApiEc.Success, null ); - } + var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; + if (settings == null) + return (PenumbraApiEc.Success, null); - var shareSettings = settings.ConvertToShareable( mod ); - return ( PenumbraApiEc.Success, - ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); + var shareSettings = settings.ConvertToShareable(mod); + return (PenumbraApiEc.Success, + (shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[mod.Index] != null)); } - public PenumbraApiEc ReloadMod( string modDirectory, string modName ) + public PenumbraApiEc ReloadMod(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - Penumbra.ModManager.ReloadMod( mod.Index ); + Penumbra.ModManager.ReloadMod(mod.Index); return PenumbraApiEc.Success; } - public PenumbraApiEc AddMod( string modDirectory ) + public PenumbraApiEc AddMod(string modDirectory) { CheckInitialized(); - var dir = new DirectoryInfo( Path.Join( Penumbra.ModManager.BasePath.FullName, Path.GetFileName( modDirectory ) ) ); - if( !dir.Exists ) - { + var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory))); + if (!dir.Exists) return PenumbraApiEc.FileMissing; - } - Penumbra.ModManager.AddMod( dir ); + Penumbra.ModManager.AddMod(dir); return PenumbraApiEc.Success; } - public PenumbraApiEc DeleteMod( string modDirectory, string modName ) + public PenumbraApiEc DeleteMod(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.NothingChanged; - } - Penumbra.ModManager.DeleteMod( mod.Index ); + Penumbra.ModManager.DeleteMod(mod.Index); return PenumbraApiEc.Success; } - public event Action< string >? ModDeleted; - public event Action< string >? ModAdded; - public event Action< string, string >? ModMoved; + public event Action? ModDeleted; + public event Action? ModAdded; + public event Action? ModMoved; - private void ModPathChangeSubscriber( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ) + private void ModPathChangeSubscriber(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) { - switch( type ) + switch (type) { case ModPathChangeType.Deleted when oldDirectory != null: - ModDeleted?.Invoke( oldDirectory.Name ); + ModDeleted?.Invoke(oldDirectory.Name); break; case ModPathChangeType.Added when newDirectory != null: - ModAdded?.Invoke( newDirectory.Name ); + ModAdded?.Invoke(newDirectory.Name); break; case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: - ModMoved?.Invoke( oldDirectory.Name, newDirectory.Name ); + ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); break; } } - public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ) + public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) { CheckInitialized(); - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) - { - return ( PenumbraApiEc.ModMissing, string.Empty, false ); - } + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) + return (PenumbraApiEc.ModMissing, string.Empty, false); var fullPath = leaf.FullName(); - return ( PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath( mod, fullPath ) ); + return (PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath(mod, fullPath)); } - public PenumbraApiEc SetModPath( string modDirectory, string modName, string newPath ) + public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) { CheckInitialized(); - if( newPath.Length == 0 ) - { + if (newPath.Length == 0) return PenumbraApiEc.InvalidArgument; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) - || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return PenumbraApiEc.ModMissing; - } try { - _penumbra.ModFileSystem.RenameAndMove( leaf, newPath ); + _penumbra.ModFileSystem.RenameAndMove(leaf, newPath); return PenumbraApiEc.Success; } catch @@ -680,311 +623,248 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) + public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) + public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) + public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - string optionName ) + public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, + string optionName) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); - if( groupIdx < 0 ) - { + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) return PenumbraApiEc.OptionGroupMissing; - } - var optionIdx = mod.Groups[ groupIdx ].IndexOf( o => o.Name == optionName ); - if( optionIdx < 0 ) - { + var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } - var setting = mod.Groups[ groupIdx ].Type == GroupType.Multi ? 1u << optionIdx : ( uint )optionIdx; + var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList< string > optionNames ) + public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, + IReadOnlyList optionNames) { CheckInitialized(); - if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { + if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) - { + if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - } - var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); - if( groupIdx < 0 ) - { + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) return PenumbraApiEc.OptionGroupMissing; - } - var group = mod.Groups[ groupIdx ]; + var group = mod.Groups[groupIdx]; uint setting = 0; - if( group.Type == GroupType.Single ) + if (group.Type == GroupType.Single) { - var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf( o => o.Name == optionNames[ ^1 ] ); - if( optionIdx < 0 ) - { + var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } - setting = ( uint )optionIdx; + setting = (uint)optionIdx; } else { - foreach( var name in optionNames ) + foreach (var name in optionNames) { - var optionIdx = group.IndexOf( o => o.Name == name ); - if( optionIdx < 0 ) - { + var optionIdx = group.IndexOf(o => o.Name == name); + if (optionIdx < 0) return PenumbraApiEc.OptionMissing; - } setting |= 1u << optionIdx; } } - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc CopyModSettings( string? collectionName, string modDirectoryFrom, string modDirectoryTo ) + public PenumbraApiEc CopyModSettings(string? collectionName, string modDirectoryFrom, string modDirectoryTo) { CheckInitialized(); - var sourceModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; - var targetModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; - if( string.IsNullOrEmpty( collectionName ) ) - { - foreach( var collection in Penumbra.CollectionManager ) - { - collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); - } - } - else if( Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) - { - collection.CopyModSettings( sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo ); - } + var sourceModIdx = Penumbra.ModManager + .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index + ?? -1; + var targetModIdx = Penumbra.ModManager + .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index + ?? -1; + if (string.IsNullOrEmpty(collectionName)) + foreach (var collection in Penumbra.CollectionManager) + collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); + else if (Penumbra.CollectionManager.ByName(collectionName, out var collection)) + collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else - { return PenumbraApiEc.CollectionMissing; - } return PenumbraApiEc.Success; } - public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) + public (PenumbraApiEc, string) CreateTemporaryCollection(string tag, string character, bool forceOverwriteCharacter) { CheckInitialized(); - if( !ActorManager.VerifyPlayerName( character.AsSpan() ) || tag.Length == 0 ) - { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); - } + if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) + return (PenumbraApiEc.InvalidArgument, string.Empty); - var identifier = NameToIdentifier( character, ushort.MaxValue ); - if( !identifier.IsValid ) - { - return ( PenumbraApiEc.InvalidArgument, string.Empty ); - } + var identifier = NameToIdentifier(character, ushort.MaxValue); + if (!identifier.IsValid) + return (PenumbraApiEc.InvalidArgument, string.Empty); - if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) - || Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) ) - { - return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); - } + if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier) + || Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier)) + return (PenumbraApiEc.CharacterCollectionExists, string.Empty); var name = $"{tag}_{character}"; - var ret = CreateNamedTemporaryCollection( name ); - if( ret != PenumbraApiEc.Success ) - { - return ( ret, name ); - } + var ret = CreateNamedTemporaryCollection(name); + if (ret != PenumbraApiEc.Success) + return (ret, name); - if( Penumbra.TempMods.AddIdentifier( name, identifier ) ) - { - return ( PenumbraApiEc.Success, name ); - } + if (Penumbra.TempCollections.AddIdentifier(name, identifier)) + return (PenumbraApiEc.Success, name); - Penumbra.TempMods.RemoveTemporaryCollection( name ); - return ( PenumbraApiEc.UnknownError, string.Empty ); + Penumbra.TempCollections.RemoveTemporaryCollection(name); + return (PenumbraApiEc.UnknownError, string.Empty); } - public PenumbraApiEc CreateNamedTemporaryCollection( string name ) + public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if( name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols( name ) != name ) - { + if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; - } - return Penumbra.TempMods.CreateTemporaryCollection( name ).Length > 0 + return Penumbra.TempCollections.CreateTemporaryCollection(name).Length > 0 ? PenumbraApiEc.Success : PenumbraApiEc.CollectionExists; } - public PenumbraApiEc AssignTemporaryCollection( string collectionName, int actorIndex, bool forceAssignment ) + public PenumbraApiEc AssignTemporaryCollection(string collectionName, int actorIndex, bool forceAssignment) { CheckInitialized(); - if( actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length ) - { + if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length) return PenumbraApiEc.InvalidArgument; - } - var identifier = Penumbra.Actors.FromObject( DalamudServices.Objects[ actorIndex ], false, false, true ); - if( !identifier.IsValid ) - { + var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true); + if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; - } - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - } - if( !forceAssignment - && ( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) ) - { + if (!forceAssignment + && (Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier) + || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier))) return PenumbraApiEc.CharacterCollectionExists; - } - var group = Penumbra.TempMods.Collections.GetGroup( identifier ); - return Penumbra.TempMods.AddIdentifier( collection, group ) + var group = Penumbra.TempCollections.Collections.GetGroup(identifier); + return Penumbra.TempCollections.AddIdentifier(collection, group) ? PenumbraApiEc.Success : PenumbraApiEc.UnknownError; } - public PenumbraApiEc RemoveTemporaryCollection( string character ) + public PenumbraApiEc RemoveTemporaryCollection(string character) { CheckInitialized(); - return Penumbra.TempMods.RemoveByCharacterName( character ) + return Penumbra.TempCollections.RemoveByCharacterName(character) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc RemoveTemporaryCollectionByName( string name ) + public PenumbraApiEc RemoveTemporaryCollectionByName(string name) { CheckInitialized(); - return Penumbra.TempMods.RemoveTemporaryCollection( name ) + return Penumbra.TempCollections.RemoveTemporaryCollection(name) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc AddTemporaryModAll( string tag, Dictionary< string, string > paths, string manipString, int priority ) + public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) { CheckInitialized(); - if( !ConvertPaths( paths, out var p ) ) - { + if (!ConvertPaths(paths, out var p)) return PenumbraApiEc.InvalidGamePath; - } - if( !ConvertManips( manipString, out var m ) ) - { + if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - } - return Penumbra.TempMods.Register( tag, null, p, m, priority ) switch + return Penumbra.TempMods.Register(tag, null, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, }; } - public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, Dictionary< string, string > paths, string manipString, - int priority ) + public PenumbraApiEc AddTemporaryMod(string tag, string collectionName, Dictionary paths, string manipString, + int priority) { CheckInitialized(); - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) + && !Penumbra.CollectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - } - if( !ConvertPaths( paths, out var p ) ) - { + if (!ConvertPaths(paths, out var p)) return PenumbraApiEc.InvalidGamePath; - } - if( !ConvertManips( manipString, out var m ) ) - { + if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - } - return Penumbra.TempMods.Register( tag, collection, p, m, priority ) switch + return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, }; } - public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ) + public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return Penumbra.TempMods.Unregister( tag, null, priority ) switch + return Penumbra.TempMods.Unregister(tag, null, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -992,16 +872,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; } - public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) + public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) { CheckInitialized(); - if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) - && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) - { + if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) + && !Penumbra.CollectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - } - return Penumbra.TempMods.Unregister( tag, collection, priority ) switch + return Penumbra.TempMods.Unregister(tag, collection, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1013,115 +891,103 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var collection = PathResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } // TODO: cleanup when incrementing API - public string GetMetaManipulations( string characterName ) - => GetMetaManipulations( characterName, ushort.MaxValue ); + public string GetMetaManipulations(string characterName) + => GetMetaManipulations(characterName, ushort.MaxValue); - public string GetMetaManipulations( string characterName, ushort worldId ) + public string GetMetaManipulations(string characterName, ushort worldId) { CheckInitialized(); - var identifier = NameToIdentifier( characterName, worldId ); - var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) + var identifier = NameToIdentifier(characterName, worldId); + var collection = Penumbra.TempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : Penumbra.CollectionManager.Individual( identifier ); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + : Penumbra.CollectionManager.Individual(identifier); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } - public string GetGameObjectMetaManipulations( int gameObjectIdx ) + public string GetGameObjectMetaManipulations(int gameObjectIdx) { CheckInitialized(); - AssociatedCollection( gameObjectIdx, out var collection ); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); - return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); + AssociatedCollection(gameObjectIdx, out var collection); + var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } internal bool HasTooltip => ChangedItemTooltip != null; - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); + internal void InvokeTooltip(object? it) + => ChangedItemTooltip?.Invoke(it); - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); + internal void InvokeClick(MouseButton button, object? it) + => ChangedItemClicked?.Invoke(button, it); - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void CheckInitialized() { - if( !Valid ) - { - throw new Exception( "PluginShare is not initialized." ); - } + if (!Valid) + throw new Exception("PluginShare is not initialized."); } // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe bool AssociatedCollection( int gameObjectIdx, out ModCollection collection ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = Penumbra.CollectionManager.Default; - if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) - { + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) return false; - } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - var data = PathResolver.IdentifyCollection( ptr, false ); - if( data.Valid ) - { + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var data = PathResolver.IdentifyCollection(ptr, false); + if (data.Valid) collection = data.ModCollection; - } return true; } - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe ActorIdentifier AssociatedIdentifier( int gameObjectIdx ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if( gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length ) - { + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) return ActorIdentifier.Invalid; - } - var ptr = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - return Penumbra.Actors.FromObject( ptr, out _, false, true, true ); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + return Penumbra.Actors.FromObject(ptr, out _, false, true, true); } // Resolve a path given by string for a specific collection. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static string ResolvePath(string path, Mod.Manager _, ModCollection collection) { - if( !Penumbra.Config.EnableMods ) - { + if (!Penumbra.Config.EnableMods) return path; - } - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); + var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath(gamePath); return ret?.ToString() ?? path; } // Get a file for a resolved path. - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + private T? GetFileIntern(string resolvedPath) where T : FileResource { CheckInitialized(); try { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } + if (Path.IsPathRooted(resolvedPath)) + return _lumina?.GetFileFromDisk(resolvedPath); - return DalamudServices.GameData.GetFile< T >( resolvedPath ); + return DalamudServices.GameData.GetFile(resolvedPath); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + Penumbra.Log.Warning($"Could not load file {resolvedPath}:\n{e}"); return null; } } @@ -1129,20 +995,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Convert a dictionary of strings to a dictionary of gamepaths to full paths. // Only returns true if all paths can successfully be converted and added. - private static bool ConvertPaths( IReadOnlyDictionary< string, string > redirections, - [NotNullWhen( true )] out Dictionary< Utf8GamePath, FullPath >? paths ) + private static bool ConvertPaths(IReadOnlyDictionary redirections, + [NotNullWhen(true)] out Dictionary? paths) { - paths = new Dictionary< Utf8GamePath, FullPath >( redirections.Count ); - foreach( var (gString, fString) in redirections ) + paths = new Dictionary(redirections.Count); + foreach (var (gString, fString) in redirections) { - if( !Utf8GamePath.FromString( gString, out var path, false ) ) + if (!Utf8GamePath.FromString(gString, out var path, false)) { paths = null; return false; } - var fullPath = new FullPath( fString ); - if( !paths.TryAdd( path, fullPath ) ) + var fullPath = new FullPath(fString); + if (!paths.TryAdd(path, fullPath)) { paths = null; return false; @@ -1155,25 +1021,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Convert manipulations from a transmitted base64 string to actual manipulations. // The empty string is treated as an empty set. // Only returns true if all conversions are successful and distinct. - private static bool ConvertManips( string manipString, - [NotNullWhen( true )] out HashSet< MetaManipulation >? manips ) + private static bool ConvertManips(string manipString, + [NotNullWhen(true)] out HashSet? manips) { - if( manipString.Length == 0 ) + if (manipString.Length == 0) { - manips = new HashSet< MetaManipulation >(); + manips = new HashSet(); return true; } - if( Functions.FromCompressedBase64< MetaManipulation[] >( manipString, out var manipArray ) != MetaManipulation.CurrentVersion ) + if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) { manips = null; return false; } - manips = new HashSet< MetaManipulation >( manipArray!.Length ); - foreach( var manip in manipArray.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) + manips = new HashSet(manipArray!.Length); + foreach (var manip in manipArray.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) { - if( !manips.Add( manip ) ) + if (!manips.Add(manip)) { manips = null; return false; @@ -1183,46 +1049,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } - private void SubscribeToCollection( ModCollection c ) + private void SubscribeToCollection(ModCollection c) { var name = c.Name; - void Del( ModSettingChange type, int idx, int _, int _2, bool inherited ) - => ModSettingChanged?.Invoke( type, name, idx >= 0 ? Penumbra.ModManager[ idx ].ModPath.Name : string.Empty, inherited ); + void Del(ModSettingChange type, int idx, int _, int _2, bool inherited) + => ModSettingChanged?.Invoke(type, name, idx >= 0 ? Penumbra.ModManager[idx].ModPath.Name : string.Empty, inherited); - _delegates[ c ] = Del; + _delegates[c] = Del; c.ModSettingChanged += Del; } - private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _ ) + private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) { - if( type != CollectionType.Inactive ) - { + if (type != CollectionType.Inactive) return; - } - if( oldCollection != null && _delegates.TryGetValue( oldCollection, out var del ) ) - { + if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del)) oldCollection.ModSettingChanged -= del; - } - if( newCollection != null ) - { - SubscribeToCollection( newCollection ); - } + if (newCollection != null) + SubscribeToCollection(newCollection); } - public void InvokePreSettingsPanel( string modDirectory ) - => PreSettingsPanelDraw?.Invoke( modDirectory ); + public void InvokePreSettingsPanel(string modDirectory) + => PreSettingsPanelDraw?.Invoke(modDirectory); - public void InvokePostSettingsPanel( string modDirectory ) - => PostSettingsPanelDraw?.Invoke( modDirectory ); + public void InvokePostSettingsPanel(string modDirectory) + => PostSettingsPanelDraw?.Invoke(modDirectory); // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private static ActorIdentifier NameToIdentifier( string name, ushort worldId ) + private static ActorIdentifier NameToIdentifier(string name, ushort worldId) { // Verified to be valid name beforehand. - var b = ByteString.FromStringUnsafe( name, false ); - return Penumbra.Actors.CreatePlayer( b, worldId ); + var b = ByteString.FromStringUnsafe(name, false); + return Penumbra.Actors.CreatePlayer(b, worldId); } -} \ No newline at end of file +} diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Api/TempCollectionManager.cs new file mode 100644 index 00000000..fab7eace --- /dev/null +++ b/Penumbra/Api/TempCollectionManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.String; + +namespace Penumbra.Api; + +public class TempCollectionManager : IDisposable +{ + public int GlobalChangeCounter { get; private set; } = 0; + public readonly IndividualCollections Collections; + + private readonly CommunicatorService _communicator; + private readonly Dictionary _customCollections = new(); + + public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections) + { + _communicator = communicator; + Collections = collections; + + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + } + + public void Dispose() + { + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + } + + private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_customCollections.Values, mod, created, removed); + + public int Count + => _customCollections.Count; + + public IEnumerable Values + => _customCollections.Values; + + public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _customCollections.TryGetValue(name.ToLowerInvariant(), out collection); + + public string CreateTemporaryCollection(string name) + { + if (Penumbra.CollectionManager.ByName(name, out _)) + return string.Empty; + + if (GlobalChangeCounter == int.MaxValue) + GlobalChangeCounter = 0; + var collection = ModCollection.CreateNewTemporary(name, GlobalChangeCounter++); + if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) + return collection.Name; + + collection.ClearCache(); + return string.Empty; + } + + public bool RemoveTemporaryCollection(string collectionName) + { + if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection)) + return false; + + GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); + collection.ClearCache(); + for (var i = 0; i < Collections.Count; ++i) + { + if (Collections[i].Collection == collection) + { + _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); + Collections.Delete(i); + } + } + + return true; + } + + public bool AddIdentifier(ModCollection collection, params ActorIdentifier[] identifiers) + { + if (Collections.Add(identifiers, collection)) + { + _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); + return true; + } + + return false; + } + + public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) + { + if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection)) + return false; + + return AddIdentifier(collection, identifiers); + } + + public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue) + { + if (!ByteString.FromString(characterName, out var byteString, false)) + return false; + + var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + if (!identifier.IsValid) + return false; + + return AddIdentifier(collectionName, identifier); + } + + internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue) + { + if (!ByteString.FromString(characterName, out var byteString, false)) + return false; + + var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + return Collections.Individuals.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); + } +} diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 08393ebd..3064d326 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -3,10 +3,7 @@ using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Penumbra.GameData.Actors; -using Penumbra.String; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Api; @@ -19,319 +16,120 @@ public enum RedirectResult FilteredGamePath = 3, } -public class TempModManager +public class TempModManager : IDisposable { - public int GlobalChangeCounter { get; private set; } = 0; - private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); - private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); - private readonly Dictionary< string, ModCollection > _customCollections = new(); - public readonly IndividualCollections Collections = new(Penumbra.Actors); + private readonly CommunicatorService _communicator; - public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; + private readonly Dictionary> _mods = new(); + private readonly List _modsForAllCollections = new(); - public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods + public TempModManager(CommunicatorService communicator) + { + _communicator = communicator; + _communicator.CollectionChange.Event += OnCollectionChange; + } + + public void Dispose() + { + _communicator.CollectionChange.Event -= OnCollectionChange; + } + + public IReadOnlyDictionary> Mods => _mods; - public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections + public IReadOnlyList ModsForAllCollections => _modsForAllCollections; - public IReadOnlyDictionary< string, ModCollection > CustomCollections - => _customCollections; - - public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _customCollections.TryGetValue( name.ToLowerInvariant(), out collection ); - - // These functions to check specific redirections or meta manipulations for existence are currently unused. - //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) - //{ - // var mod = GetExistingMod( tag, collection, null ); - // if( mod == null ) - // { - // priority = 0; - // fullPath = null; - // return false; - // } - // - // priority = mod.Priority; - // if( mod.Default.Files.TryGetValue( gamePath, out var f ) ) - // { - // fullPath = f; - // return true; - // } - // - // fullPath = null; - // return false; - //} - // - //public bool IsRegistered( string tag, ModCollection? collection, MetaManipulation meta, out MetaManipulation? manipulation, - // out int priority ) - //{ - // var mod = GetExistingMod( tag, collection, null ); - // if( mod == null ) - // { - // priority = 0; - // manipulation = null; - // return false; - // } - // - // priority = mod.Priority; - // // IReadOnlySet has no TryGetValue for some reason. - // if( ( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var manip ) ) - // { - // manipulation = manip; - // return true; - // } - // - // manipulation = null; - // return false; - //} - - // These functions for setting single redirections or manips are currently unused. - //public RedirectResult Register( string tag, ModCollection? collection, Utf8GamePath path, FullPath file, int priority ) - //{ - // if( Mod.FilterFile( path ) ) - // { - // return RedirectResult.FilteredGamePath; - // } - // - // var mod = GetOrCreateMod( tag, collection, priority, out var created ); - // - // var changes = !mod.Default.Files.TryGetValue( path, out var oldFile ) || !oldFile.Equals( file ); - // mod.SetFile( path, file ); - // ApplyModChange( mod, collection, created, false ); - // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; - //} - // - //public RedirectResult Register( string tag, ModCollection? collection, MetaManipulation meta, int priority ) - //{ - // var mod = GetOrCreateMod( tag, collection, priority, out var created ); - // var changes = !( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var oldMeta ) - // || !oldMeta.Equals( meta ); - // mod.SetManipulation( meta ); - // ApplyModChange( mod, collection, created, false ); - // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; - //} - - public RedirectResult Register( string tag, ModCollection? collection, Dictionary< Utf8GamePath, FullPath > dict, - HashSet< MetaManipulation > manips, int priority ) + public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, + HashSet manips, int priority) { - var mod = GetOrCreateMod( tag, collection, priority, out var created ); - mod.SetAll( dict, manips ); - ApplyModChange( mod, collection, created, false ); + var mod = GetOrCreateMod(tag, collection, priority, out var created); + mod.SetAll(dict, manips); + ApplyModChange(mod, collection, created, false); return RedirectResult.Success; } - public RedirectResult Unregister( string tag, ModCollection? collection, int? priority ) + public RedirectResult Unregister(string tag, ModCollection? collection, int? priority) { - var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; - if( list == null ) - { + var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; + if (list == null) return RedirectResult.NotRegistered; - } - var removed = list.RemoveAll( m => + var removed = list.RemoveAll(m => { - if( m.Name != tag || priority != null && m.Priority != priority.Value ) - { + if (m.Name != tag || priority != null && m.Priority != priority.Value) return false; - } - ApplyModChange( m, collection, false, true ); + ApplyModChange(m, collection, false, true); return true; - } ); + }); - if( removed == 0 ) - { + if (removed == 0) return RedirectResult.NotRegistered; - } - if( list.Count == 0 && collection != null ) - { - _mods.Remove( collection ); - } + if (list.Count == 0 && collection != null) + _mods.Remove(collection); return RedirectResult.Success; } - public string CreateTemporaryCollection( string name ) - { - if( Penumbra.CollectionManager.ByName( name, out _ ) ) - { - return string.Empty; - } - - if( GlobalChangeCounter == int.MaxValue ) - GlobalChangeCounter = 0; - var collection = ModCollection.CreateNewTemporary( name, GlobalChangeCounter++ ); - if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) ) - { - return collection.Name; - } - - collection.ClearCache(); - return string.Empty; - } - - public bool RemoveTemporaryCollection( string collectionName ) - { - if( !_customCollections.Remove( collectionName.ToLowerInvariant(), out var collection ) ) - { - return false; - } - - GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); - _mods.Remove( collection ); - collection.ClearCache(); - for( var i = 0; i < Collections.Count; ++i ) - { - if( Collections[ i ].Collection == collection ) - { - CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName ); - Collections.Delete( i ); - } - } - - return true; - } - - public bool AddIdentifier( ModCollection collection, params ActorIdentifier[] identifiers ) - { - if( Collections.Add( identifiers, collection ) ) - { - CollectionChanged?.Invoke( CollectionType.Temporary, null, collection, Collections.Last().DisplayName ); - return true; - } - - return false; - } - - public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers ) - { - if( !_customCollections.TryGetValue( collectionName.ToLowerInvariant(), out var collection ) ) - { - return false; - } - - return AddIdentifier( collection, identifiers ); - } - - public bool AddIdentifier( string collectionName, string characterName, ushort worldId = ushort.MaxValue ) - { - if( !ByteString.FromString( characterName, out var byteString, false ) ) - { - return false; - } - - var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); - if( !identifier.IsValid ) - { - return false; - } - - return AddIdentifier( collectionName, identifier ); - } - - internal bool RemoveByCharacterName( string characterName, ushort worldId = ushort.MaxValue ) - { - if( !ByteString.FromString( characterName, out var byteString, false ) ) - { - return false; - } - - var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId ); - return Collections.Individuals.TryGetValue( identifier, out var collection ) && RemoveTemporaryCollection( collection.Name ); - } - - // Apply any new changes to the temporary mod. - private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) + private void ApplyModChange(Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed) { - if( collection == null ) + if (collection != null) { - if( removed ) - { - foreach( var c in Penumbra.CollectionManager ) - { - c.Remove( mod ); - } - } + if (removed) + collection.Remove(mod); else - { - foreach( var c in Penumbra.CollectionManager ) - { - c.Apply( mod, created ); - } - } + collection.Apply(mod, created); } else { - if( removed ) - { - collection.Remove( mod ); - } - else - { - collection.Apply( mod, created ); - } + _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); } } - - // Only find already existing mods, currently unused. - //private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) - //{ - // var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; - // if( list == null ) - // { - // return null; - // } - // - // if( priority != null ) - // { - // return list.Find( m => m.Priority == priority.Value && m.Name == tag ); - // } - // - // Mod.TemporaryMod? highestMod = null; - // var highestPriority = int.MinValue; - // foreach( var m in list ) - // { - // if( highestPriority < m.Priority && m.Name == tag ) - // { - // highestPriority = m.Priority; - // highestMod = m; - // } - // } - // - // return highestMod; - //} + + /// + /// Apply a mod change to a set of collections. + /// + public static void OnGlobalModChange(IEnumerable collections, Mod.TemporaryMod mod, bool created, bool removed) + { + if (removed) + foreach (var c in collections) + c.Remove(mod); + else + foreach (var c in collections) + c.Apply(mod, created); + } // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Returns the found or created mod and whether it was newly created. - private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created ) + private Mod.TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) { - List< Mod.TemporaryMod > list; - if( collection == null ) + List list; + if (collection == null) { list = _modsForAllCollections; } - else if( _mods.TryGetValue( collection, out var l ) ) + else if (_mods.TryGetValue(collection, out var l)) { list = l; } else { - list = new List< Mod.TemporaryMod >(); - _mods.Add( collection, list ); + list = new List(); + _mods.Add(collection, list); } - var mod = list.Find( m => m.Priority == priority && m.Name == tag ); - if( mod == null ) + var mod = list.Find(m => m.Priority == priority && m.Name == tag); + if (mod == null) { mod = new Mod.TemporaryMod() { Name = tag, Priority = priority, }; - list.Add( mod ); + list.Add(mod); created = true; } else @@ -341,4 +139,11 @@ public class TempModManager return mod; } -} \ No newline at end of file + + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, + string _) + { + if (collectionType is CollectionType.Temporary or CollectionType.Inactive && newCollection == null && oldCollection != null) + _mods.Remove(oldCollection); + } +} diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 81750870..fd6da3ad 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -9,10 +9,11 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; using Penumbra.GameData.Actors; using Penumbra.Util; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Collections; public partial class ModCollection @@ -21,9 +22,6 @@ public partial class ModCollection { public const int Version = 1; - // Is invoked after the collections actually changed. - public event CollectionChangeDelegate CollectionChanged; - // The collection currently selected for changing settings. public ModCollection Current { get; private set; } = Empty; @@ -40,65 +38,62 @@ public partial class ModCollection private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. + // TODO public readonly IndividualCollections Individuals = new(Penumbra.Actors); - public ModCollection Individual( ActorIdentifier identifier ) - => Individuals.TryGetCollection( identifier, out var c ) ? c : Default; + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< Api.Enums.ApiCollectionType >().Length - 3]; + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; // Return the configured collection for the given type or null. // Does not handle Inactive, use ByName instead. - public ModCollection? ByType( CollectionType type ) - => ByType( type, ActorIdentifier.Invalid ); + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); - public ModCollection? ByType( CollectionType type, ActorIdentifier identifier ) + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) { - if( type.IsSpecial() ) - { - return _specialCollections[ ( int )type ]; - } + if (type.IsSpecial()) + return _specialCollections[(int)type]; return type switch { CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : null, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, _ => null, }; } // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection( int newIdx, CollectionType collectionType, int individualIndex = -1 ) + private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) { var oldCollectionIdx = collectionType switch { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, + CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count + ? -1 + : Individuals[individualIndex].Collection.Index, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, _ => -1, }; - if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) - { + if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) return; - } - var newCollection = this[ newIdx ]; - if( newIdx > Empty.Index ) - { + var newCollection = this[newIdx]; + if (newIdx > Empty.Index) newCollection.CreateCache(); - } - switch( collectionType ) + switch (collectionType) { case CollectionType.Default: Default = newCollection; - if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) { Penumbra.ResidentResources.Reload(); Default.SetFiles(); @@ -112,362 +107,336 @@ public partial class ModCollection Current = newCollection; break; case CollectionType.Individual: - if( !Individuals.ChangeCollection( individualIndex, newCollection ) ) + if (!Individuals.ChangeCollection(individualIndex, newCollection)) { - RemoveCache( newIdx ); + RemoveCache(newIdx); return; } break; default: - _specialCollections[ ( int )collectionType ] = newCollection; + _specialCollections[(int)collectionType] = newCollection; break; } - RemoveCache( oldCollectionIdx ); + RemoveCache(oldCollectionIdx); UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); + _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); } private void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = _specialCollections - .OfType< ModCollection >() - .Prepend( Interface ) - .Prepend( Default ) - .Concat( Individuals.Assignments.Select( kvp => kvp.Collection ) ) - .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 ) - => SetCollection( collection.Index, collectionType, individualIndex ); + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + => SetCollection(collection.Index, collectionType, individualIndex); // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection( CollectionType collectionType ) + public bool CreateSpecialCollection(CollectionType collectionType) { - if( !collectionType.IsSpecial() || _specialCollections[ ( int )collectionType ] != null ) - { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) return false; - } - _specialCollections[ ( int )collectionType ] = Default; - CollectionChanged.Invoke( collectionType, null, Default ); + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); return true; } // Remove a special collection if it exists - public void RemoveSpecialCollection( CollectionType collectionType ) + public void RemoveSpecialCollection(CollectionType collectionType) { - if( !collectionType.IsSpecial() ) - { + if (!collectionType.IsSpecial()) return; - } - var old = _specialCollections[ ( int )collectionType ]; - if( old != null ) + var old = _specialCollections[(int)collectionType]; + if (old != null) { - _specialCollections[ ( int )collectionType ] = null; - CollectionChanged.Invoke( collectionType, old, null ); + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); } } // Wrappers around Individual Collection handling. - public void CreateIndividualCollection( params ActorIdentifier[] identifiers ) + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) { - if( Individuals.Add( identifiers, Default ) ) - { - CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName ); - } + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); } - public void RemoveIndividualCollection( int individualIndex ) + public void RemoveIndividualCollection(int individualIndex) { - if( individualIndex < 0 || individualIndex >= Individuals.Count ) - { + if (individualIndex < 0 || individualIndex >= Individuals.Count) return; - } - var (name, old) = Individuals[ individualIndex ]; - if( Individuals.Delete( individualIndex ) ) - { - CollectionChanged.Invoke( CollectionType.Individual, old, null, name ); - } + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); } - public void MoveIndividualCollection( int from, int to ) + public void MoveIndividualCollection(int from, int to) { - if( Individuals.Move( from, to ) ) - { + if (Individuals.Move(from, to)) SaveActiveCollections(); - } } // Obtain the index of a collection by name. - private int GetIndexForCollectionName( string name ) - => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); + private int GetIndexForCollectionName(string name) + => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - public static string ActiveCollectionFile - => Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); + public static string ActiveCollectionFile(DalamudPluginInterface pi) + => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. private void LoadCollections() { - var configChanged = !ReadActiveCollections( out var jObject ); + var configChanged = !ReadActiveCollections(out var jObject); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); - var defaultIdx = GetIndexForCollectionName( defaultName ); - if( defaultIdx < 0 ) + var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); + var defaultIdx = GetIndexForCollectionName(defaultName); + if (defaultIdx < 0) { - ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning ); + ChatUtil.NotificationMessage( + $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning); Default = Empty; configChanged = true; } else { - Default = this[ defaultIdx ]; + Default = this[defaultIdx]; } // Load the interface collection. - var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName( interfaceName ); - if( interfaceIdx < 0 ) + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceIdx = GetIndexForCollectionName(interfaceName); + if (interfaceIdx < 0) { ChatUtil.NotificationMessage( - $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning ); + $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", + "Load Failure", NotificationType.Warning); Interface = Empty; configChanged = true; } else { - Interface = this[ interfaceIdx ]; + Interface = this[interfaceIdx]; } // Load the current collection. - var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; - var currentIdx = GetIndexForCollectionName( currentName ); - if( currentIdx < 0 ) + var currentName = jObject[nameof(Current)]?.ToObject() ?? DefaultCollection; + var currentIdx = GetIndexForCollectionName(currentName); + if (currentIdx < 0) { ChatUtil.NotificationMessage( - $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning ); + $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", + "Load Failure", NotificationType.Warning); Current = DefaultName; configChanged = true; } else { - Current = this[ currentIdx ]; + Current = this[currentIdx]; } // Load special collections. - foreach( var (type, name, _) in CollectionTypeExtensions.Special ) + foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var typeName = jObject[ type.ToString() ]?.ToObject< string >(); - if( typeName != null ) + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) { - var idx = GetIndexForCollectionName( typeName ); - if( idx < 0 ) + var idx = GetIndexForCollectionName(typeName); + if (idx < 0) { - ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning ); + ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", + NotificationType.Warning); configChanged = true; } else { - _specialCollections[ ( int )type ] = this[ idx ]; + _specialCollections[(int)type] = this[idx]; } } } - configChanged |= MigrateIndividualCollections( jObject ); - configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this ); + configChanged |= MigrateIndividualCollections(jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); // Save any changes and create all required caches. - if( configChanged ) - { + if (configChanged) SaveActiveCollections(); - } } // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections() + public static void MigrateUngenderedCollections(FilenameService fileNames) { - if( !ReadActiveCollections( out var jObject ) ) - { + if (!ReadActiveCollections(out var jObject)) return; - } - foreach( var (type, _, _) in CollectionTypeExtensions.Special.Where( t => t.Item2.StartsWith( "Male " ) ) ) + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) { - var oldName = type.ToString()[ 4.. ]; - var value = jObject[ oldName ]; - if( value == null ) - { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) continue; - } - jObject.Remove( oldName ); - jObject.Add( "Male" + oldName, value ); - jObject.Add( "Female" + oldName, value ); + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); } - using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; - jObject.WriteTo( j ); + jObject.WriteTo(j); } // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections( JObject jObject ) + private bool MigrateIndividualCollections(JObject jObject) { - var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; - if( version > 0 ) - { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) return false; - } // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject[ "Characters" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); - var dict = new Dictionary< string, ModCollection >( characters.Count ); - foreach( var (player, collectionName) in characters ) + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) { - var idx = GetIndexForCollectionName( collectionName ); - if( idx < 0 ) + var idx = GetIndexForCollectionName(collectionName); + if (idx < 0) { - ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning ); - dict.Add( player, Empty ); + ChatUtil.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, Empty); } else { - dict.Add( player, this[ idx ] ); + dict.Add(player, this[idx]); } } - Individuals.Migrate0To1( dict ); + Individuals.Migrate0To1(dict); return true; } public void SaveActiveCollections() { - Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - SaveActiveCollectionsInternal ); + Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections), + SaveActiveCollectionsInternal); } internal void SaveActiveCollectionsInternal() { - var file = ActiveCollectionFile; + // TODO + var file = ActiveCollectionFile(DalamudServices.PluginInterface); try { var jObj = new JObject { - { nameof( Version ), Version }, - { nameof( Default ), Default.Name }, - { nameof( Interface ), Interface.Name }, - { nameof( Current ), Current.Name }, + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, }; - foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) ) - { - jObj.Add( type.ToString(), collection.Name ); - } + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); - jObj.Add( nameof( Individuals ), Individuals.ToJObject() ); - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ) - { Formatting = Formatting.Indented }; - jObj.WriteTo( j ); - Penumbra.Log.Verbose( "Active Collections saved." ); + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + Penumbra.Log.Verbose("Active Collections saved."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); + Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); } } // Read the active collection file into a jObject. // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections( out JObject ret ) + private static bool ReadActiveCollections(out JObject ret) { - var file = ActiveCollectionFile; - if( File.Exists( file ) ) - { + // TODO + var file = ActiveCollectionFile(DalamudServices.PluginInterface); + if (File.Exists(file)) try { - ret = JObject.Parse( File.ReadAllText( file ) ); + ret = JObject.Parse(File.ReadAllText(file)); return true; } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not read active collections from file {file}:\n{e}" ); + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); } - } ret = new JObject(); return false; } // Save if any of the active collections is changed. - private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3 ) + private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) { - if( collectionType != CollectionType.Inactive ) - { + if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) SaveActiveCollections(); - } } // Cache handling. Usually recreate caches on the next framework tick, // but at launch create all of them at once. public void CreateNecessaryCaches() { - var tasks = _specialCollections.OfType< ModCollection >() - .Concat( Individuals.Select( p => p.Collection ) ) - .Prepend( Current ) - .Prepend( Default ) - .Prepend( Interface ) - .Distinct() - .Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) ) - .ToArray(); + var tasks = _specialCollections.OfType() + .Concat(Individuals.Select(p => p.Collection)) + .Prepend(Current) + .Prepend(Default) + .Prepend(Interface) + .Distinct() + .Select(c => Task.Run(c.CalculateEffectiveFileListInternal)) + .ToArray(); - Task.WaitAll( tasks ); + Task.WaitAll(tasks); } - private void RemoveCache( int idx ) + private void RemoveCache(int idx) { - if( idx != Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All( c => c == null || c.Index != idx ) - && Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) ) - { - _collections[ idx ].ClearCache(); - } + if (idx != Empty.Index + && idx != Default.Index + && idx != Interface.Index + && idx != Current.Index + && _specialCollections.All(c => c == null || c.Index != idx) + && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) + _collections[idx].ClearCache(); } // Recalculate effective files for active collections on events. - private void OnModAddedActive( Mod mod ) + private void OnModAddedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.AddMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); } - private void OnModRemovedActive( Mod mod ) + private void OnModRemovedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.RemoveMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); } - private void OnModMovedActive( Mod mod ) + private void OnModMovedActive(Mod mod) { - foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) - { - collection._cache!.ReloadMod( mod, true ); - } + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 2610c455..a07bc5af 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -7,60 +7,60 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Penumbra.Api; +using Penumbra.Services; namespace Penumbra.Collections; public partial class ModCollection { - public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > + public sealed partial class Manager : IDisposable, IEnumerable { - // On addition, oldCollection is null. On deletion, newCollection is null. - // displayName is only set for type == Individual. - public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, - ModCollection? newCollection, string displayName = "" ); - - private readonly Mod.Manager _modManager; + private readonly Mod.Manager _modManager; + private readonly CommunicatorService _communicator; // The empty collection is always available and always has index 0. // It can not be deleted or moved. - private readonly List< ModCollection > _collections = new() + private readonly List _collections = new() { Empty, }; - public ModCollection this[ Index idx ] - => _collections[ idx ]; + public ModCollection this[Index idx] + => _collections[idx]; - public ModCollection? this[ string name ] - => ByName( name, out var c ) ? c : null; + public ModCollection? this[string name] + => ByName(name, out var c) ? c : null; public int Count => _collections.Count; // Obtain a collection case-independently by name. - public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); // Default enumeration skips the empty collection. - public IEnumerator< ModCollection > GetEnumerator() - => _collections.Skip( 1 ).GetEnumerator(); + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerable< ModCollection > GetEnumeratorWithEmpty() + public IEnumerable GetEnumeratorWithEmpty() => _collections; - public Manager( Mod.Manager manager ) + public Manager(CommunicatorService communicator, Mod.Manager manager) { - _modManager = manager; + _communicator = communicator; + _modManager = manager; // The collection manager reacts to changes in mods by itself. - _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModOptionChanged += OnModOptionsChanged; - _modManager.ModPathChanged += OnModPathChange; - CollectionChanged += SaveOnChange; + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChange; + _communicator.CollectionChange.Event += SaveOnChange; + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; ReadCollections(); LoadCollections(); UpdateCurrentCollectionInUse(); @@ -68,26 +68,31 @@ public partial class ModCollection public void Dispose() { - _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModOptionChanged -= OnModOptionsChanged; - _modManager.ModPathChanged -= OnModPathChange; + _communicator.CollectionChange.Event -= SaveOnChange; + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChange; } + private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + // Returns true if the name is not empty, it is not the name of the empty collection // and no existing collection results in the same filename as name. - public bool CanAddCollection( string name, out string fixedName ) + public bool CanAddCollection(string name, out string fixedName) { - if( !IsValidName( name ) ) + if (!IsValidName(name)) { fixedName = string.Empty; return false; } name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( name.Length == 0 - || name == Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) ) + if (name.Length == 0 + || name == Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) { fixedName = string.Empty; return false; @@ -102,217 +107,179 @@ public partial class ModCollection // If the name of the collection would result in an already existing filename, skip it. // Returns true if the collection was successfully created and fires a Inactive event. // Also sets the current collection to the new collection afterwards. - public bool AddCollection( string name, ModCollection? duplicate ) + public bool AddCollection(string name, ModCollection? duplicate) { - if( !CanAddCollection( name, out var fixedName ) ) + if (!CanAddCollection(name, out var fixedName)) { - Penumbra.Log.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." ); + Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); return false; } - var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); + var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add( newCollection ); + _collections.Add(newCollection); newCollection.Save(); - Penumbra.Log.Debug( $"Added collection {newCollection.AnonymizedName}." ); - CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); - SetCollection( newCollection.Index, CollectionType.Current ); + Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + SetCollection(newCollection.Index, CollectionType.Current); return true; } // Remove the given collection if it exists and is neither the empty nor the default-named collection. // If the removed collection was active, it also sets the corresponding collection to the appropriate default. // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection( int idx ) + public bool RemoveCollection(int idx) { - if( idx <= Empty.Index || idx >= _collections.Count ) + if (idx <= Empty.Index || idx >= _collections.Count) { - Penumbra.Log.Error( "Can not remove the empty collection." ); + Penumbra.Log.Error("Can not remove the empty collection."); return false; } - if( idx == DefaultName.Index ) + if (idx == DefaultName.Index) { - Penumbra.Log.Error( "Can not remove the default collection." ); + Penumbra.Log.Error("Can not remove the default collection."); return false; } - if( idx == Current.Index ) + if (idx == Current.Index) + SetCollection(DefaultName.Index, CollectionType.Current); + + if (idx == Default.Index) + SetCollection(Empty.Index, CollectionType.Default); + + for (var i = 0; i < _specialCollections.Length; ++i) { - SetCollection( DefaultName.Index, CollectionType.Current ); + if (idx == _specialCollections[i]?.Index) + SetCollection(Empty, (CollectionType)i); } - if( idx == Default.Index ) + for (var i = 0; i < Individuals.Count; ++i) { - SetCollection( Empty.Index, CollectionType.Default ); + if (Individuals[i].Collection.Index == idx) + SetCollection(Empty, CollectionType.Individual, i); } - for( var i = 0; i < _specialCollections.Length; ++i ) - { - if( idx == _specialCollections[ i ]?.Index ) - { - SetCollection( Empty, ( CollectionType )i ); - } - } - - for( var i = 0; i < Individuals.Count; ++i ) - { - if( Individuals[ i ].Collection.Index == idx ) - { - SetCollection( Empty, CollectionType.Individual, i ); - } - } - - var collection = _collections[ idx ]; + var collection = _collections[idx]; // Clear own inheritances. - foreach( var inheritance in collection.Inheritance ) - { - collection.ClearSubscriptions( inheritance ); - } + foreach (var inheritance in collection.Inheritance) + collection.ClearSubscriptions(inheritance); collection.Delete(); - _collections.RemoveAt( idx ); + _collections.RemoveAt(idx); // Clear external inheritances. - foreach( var c in _collections ) + foreach (var c in _collections) { - var inheritedIdx = c._inheritance.IndexOf( collection ); - if( inheritedIdx >= 0 ) - { - c.RemoveInheritance( inheritedIdx ); - } + var inheritedIdx = c._inheritance.IndexOf(collection); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); - if( c.Index > idx ) - { + if (c.Index > idx) --c.Index; - } } - Penumbra.Log.Debug( $"Removed collection {collection.AnonymizedName}." ); - CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); + Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } - public bool RemoveCollection( ModCollection collection ) - => RemoveCollection( collection.Index ); + public bool RemoveCollection(ModCollection collection) + => RemoveCollection(collection.Index); private void OnModDiscoveryStarted() { - foreach( var collection in this ) - { + foreach (var collection in this) collection.PrepareModDiscovery(); - } } private void OnModDiscoveryFinished() { // First, re-apply all mod settings. - foreach( var collection in this ) - { + foreach (var collection in this) collection.ApplyModSettings(); - } // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach( var collection in this.Where( c => c.HasCache ) ) - { + foreach (var collection in this.Where(c => c.HasCache)) collection.ForceCacheUpdate(); - } } // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) { - switch( type ) + switch (type) { case ModPathChangeType.Added: - foreach( var collection in this ) - { - collection.AddMod( mod ); - } + foreach (var collection in this) + collection.AddMod(mod); - OnModAddedActive( mod ); + OnModAddedActive(mod); break; case ModPathChangeType.Deleted: - OnModRemovedActive( mod ); - foreach( var collection in this ) - { - collection.RemoveMod( mod, mod.Index ); - } + OnModRemovedActive(mod); + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); break; case ModPathChangeType.Moved: - OnModMovedActive( mod ); - foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) - { + OnModMovedActive(mod); + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) collection.Save(); - } break; case ModPathChangeType.StartingReload: - OnModRemovedActive( mod ); + OnModRemovedActive(mod); break; case ModPathChangeType.Reloaded: - OnModAddedActive( mod ); + OnModAddedActive(mod); break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } // Automatically update all relevant collections when a mod is changed. // This means saving if options change in a way where the settings may change and the collection has settings for this mod. // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) + private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) { // Handle changes that break revertability. - if( type == ModOptionChangeType.PrepareChange ) + if (type == ModOptionChangeType.PrepareChange) { - foreach( var collection in this.Where( c => c.HasCache ) ) + foreach (var collection in this.Where(c => c.HasCache)) { - if( collection[ mod.Index ].Settings is { Enabled: true } ) - { - collection._cache!.RemoveMod( mod, false ); - } + if (collection[mod.Index].Settings is { Enabled: true }) + collection._cache!.RemoveMod(mod, false); } return; } - type.HandlingInfo( out var requiresSaving, out var recomputeList, out var reload ); + type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); // Handle changes that require overwriting the collection. - if( requiresSaving ) - { - foreach( var collection in this ) + if (requiresSaving) + foreach (var collection in this) { - if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false ) - { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) collection.Save(); - } } - } // Handle changes that reload the mod if the changes did not need to be prepared, // or re-add the mod if they were prepared. - if( recomputeList ) - { - foreach( var collection in this.Where( c => c.HasCache ) ) + if (recomputeList) + foreach (var collection in this.Where(c => c.HasCache)) { - if( collection[ mod.Index ].Settings is { Enabled: true } ) + if (collection[mod.Index].Settings is { Enabled: true }) { - if( reload ) - { - collection._cache!.ReloadMod( mod, true ); - } + if (reload) + collection._cache!.ReloadMod(mod, true); else - { - collection._cache!.AddMod( mod, true ); - } + collection._cache!.AddMod(mod, true); } } - } } // Add the collection with the default name if it does not exist. @@ -320,44 +287,42 @@ public partial class ModCollection // This can also not be deleted, so there are always at least the empty and a collection with default name. private void AddDefaultCollection() { - var idx = GetIndexForCollectionName( DefaultCollection ); - if( idx >= 0 ) + var idx = GetIndexForCollectionName(DefaultCollection); + if (idx >= 0) { - DefaultName = this[ idx ]; + DefaultName = this[idx]; return; } - var defaultCollection = CreateNewEmpty( DefaultCollection ); + var defaultCollection = CreateNewEmpty(DefaultCollection); defaultCollection.Save(); defaultCollection.Index = _collections.Count; - _collections.Add( defaultCollection ); + _collections.Add(defaultCollection); } // Inheritances can not be setup before all collections are read, // so this happens after reading the collections. - private void ApplyInheritances( IEnumerable< IReadOnlyList< string > > inheritances ) + private void ApplyInheritances(IEnumerable> inheritances) { - foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + foreach (var (collection, inheritance) in this.Zip(inheritances)) { var changes = false; - foreach( var subCollectionName in inheritance ) + foreach (var subCollectionName in inheritance) { - if( !ByName( subCollectionName, out var subCollection ) ) + if (!ByName(subCollectionName, out var subCollection)) { changes = true; - Penumbra.Log.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); } - else if( !collection.AddInheritance( subCollection, false ) ) + else if (!collection.AddInheritance(subCollection, false)) { changes = true; - Penumbra.Log.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); } } - if( changes ) - { + if (changes) collection.Save(); - } } } @@ -366,38 +331,33 @@ public partial class ModCollection // Duplicate collection files are not deleted, just not added here. private void ReadCollections() { - var collectionDir = new DirectoryInfo( CollectionDirectory ); - var inheritances = new List< IReadOnlyList< string > >(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + // TODO + var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface)); + var inheritances = new List>(); + if (collectionDir.Exists) + foreach (var file in collectionDir.EnumerateFiles("*.json")) { - var collection = LoadFromFile( file, out var inheritance ); - if( collection == null || collection.Name.Length == 0 ) - { + var collection = LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) continue; - } - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - Penumbra.Log.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - if( this[ collection.Name ] != null ) + if (this[collection.Name] != null) { - Penumbra.Log.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); } else { - inheritances.Add( inheritance ); + inheritances.Add(inheritance); collection.Index = _collections.Count; - _collections.Add( collection ); + _collections.Add(collection); } } - } AddDefaultCollection(); - ApplyInheritances( inheritances ); + ApplyInheritances(inheritances); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 61ea2c0e..d52fd69f 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; using Penumbra.GameData.Actors; +using Penumbra.Services; using Penumbra.String; namespace Penumbra.Collections; @@ -19,8 +20,12 @@ public sealed partial class IndividualCollections public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals => _individuals; + + // TODO + public IndividualCollections( ActorService actorManager ) + => _actorManager = actorManager.AwaitedService; - public IndividualCollections( ActorManager actorManager ) + public IndividualCollections(ActorManager actorManager) => _actorManager = actorManager; public enum AddResult diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 60ab44e0..6474cbf5 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -2,24 +2,26 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Dalamud.Plugin; namespace Penumbra.Collections; // File operations like saving, loading and deleting for a collection. public partial class ModCollection { - public static string CollectionDirectory - => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "collections" ); + public static string CollectionDirectory(DalamudPluginInterface pi) + => Path.Combine( pi.GetPluginConfigDirectory(), "collections" ); - // We need to remove all invalid path symbols from the collection name to be able to save it to file. + // We need to remove all invalid path symbols from the collection name to be able to save it to file. + // TODO public FileInfo FileName - => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + => new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" )); // Custom serialization due to shared mod information across managers. private void SaveCollection() diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index f5ce009d..f1060880 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -90,7 +90,7 @@ public partial class ModCollection var collection = new ModCollection( name, Empty ); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; - collection.Index = ~Penumbra.TempMods.Collections.Count; + collection.Index = ~Penumbra.TempCollections.Count; collection.ChangeCounter = changeCounter; collection.CreateCache(); return collection; diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs deleted file mode 100644 index a4b368dd..00000000 --- a/Penumbra/Configuration.Migration.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; -using Penumbra.Collections; -using Penumbra.Mods; -using Penumbra.Services; - -namespace Penumbra; - -public partial class Configuration -{ - // Contains everything to migrate from older versions of the config to the current, - // including deprecated fields. - private class Migration - { - private Configuration _config = null!; - private JObject _data = null!; - - public string CurrentCollection = ModCollection.DefaultCollection; - public string DefaultCollection = ModCollection.DefaultCollection; - public string ForcedCollection = string.Empty; - public Dictionary< string, string > CharacterCollections = new(); - public Dictionary< string, string > ModSortOrder = new(); - public bool InvertModListOrder; - public bool SortFoldersFirst; - public SortModeV3 SortMode = SortModeV3.FoldersFirst; - - public static void Migrate( Configuration config ) - { - if( !File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) - { - return; - } - - var m = new Migration - { - _config = config, - _data = JObject.Parse( File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ) ), - }; - - CreateBackup(); - m.Version0To1(); - m.Version1To2(); - m.Version2To3(); - m.Version3To4(); - m.Version4To5(); - m.Version5To6(); - m.Version6To7(); - } - - // Gendered special collections were added. - private void Version6To7() - { - if( _config.Version != 6 ) - return; - - ModCollection.Manager.MigrateUngenderedCollections(); - _config.Version = 7; - } - - - // A new tutorial step was inserted in the middle. - // The UI collection and a new tutorial for it was added. - // The migration for the UI collection itself happens in the ActiveCollections file. - private void Version5To6() - { - if( _config.Version != 5 ) - { - return; - } - if( _config.TutorialStep == 25 ) - { - _config.TutorialStep = 27; - } - - _config.Version = 6; - } - - // Mod backup extension was changed from .zip to .pmp. - // Actual migration takes place in ModManager. - private void Version4To5() - { - if( _config.Version != 4 ) - { - return; - } - - Mod.Manager.MigrateModBackups = true; - _config.Version = 5; - } - - // SortMode was changed from an enum to a type. - private void Version3To4() - { - if( _config.Version != 3 ) - { - return; - } - - SortMode = _data[ nameof( SortMode ) ]?.ToObject< SortModeV3 >() ?? SortMode; - _config.SortMode = SortMode switch - { - SortModeV3.FoldersFirst => ISortMode< Mod >.FoldersFirst, - SortModeV3.Lexicographical => ISortMode< Mod >.Lexicographical, - SortModeV3.InverseFoldersFirst => ISortMode< Mod >.InverseFoldersFirst, - SortModeV3.InverseLexicographical => ISortMode< Mod >.InverseLexicographical, - SortModeV3.FoldersLast => ISortMode< Mod >.FoldersLast, - SortModeV3.InverseFoldersLast => ISortMode< Mod >.InverseFoldersLast, - SortModeV3.InternalOrder => ISortMode< Mod >.InternalOrder, - SortModeV3.InternalOrderInverse => ISortMode< Mod >.InverseInternalOrder, - _ => ISortMode< Mod >.FoldersFirst, - }; - _config.Version = 4; - } - - // SortFoldersFirst was changed from a bool to the enum SortMode. - private void Version2To3() - { - if( _config.Version != 2 ) - { - return; - } - - SortFoldersFirst = _data[ nameof( SortFoldersFirst ) ]?.ToObject< bool >() ?? false; - SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; - _config.Version = 3; - } - - // The forced collection was removed due to general inheritance. - // Sort Order was moved to a separate file and may contain empty folders. - // Active collections in general were moved to their own file. - // Delete the penumbrametatmp folder if it exists. - private void Version1To2() - { - if( _config.Version != 1 ) - { - return; - } - - // Ensure the right meta files are loaded. - DeleteMetaTmp(); - Penumbra.CharacterUtility.LoadCharacterResources(); - ResettleSortOrder(); - ResettleCollectionSettings(); - ResettleForcedCollection(); - _config.Version = 2; - } - - private void DeleteMetaTmp() - { - var path = Path.Combine( _config.ModDirectory, "penumbrametatmp" ); - if( Directory.Exists( path ) ) - { - try - { - Directory.Delete( path, true ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" ); - } - } - } - - private void ResettleForcedCollection() - { - ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection; - if( ForcedCollection.Length <= 0 ) - { - return; - } - - // Add the previous forced collection to all current collections except itself as an inheritance. - foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) ) - { - try - { - var jObject = JObject.Parse( File.ReadAllText( collection ) ); - if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection ) - { - jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } ); - File.WriteAllText( collection, jObject.ToString() ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( - $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); - } - } - } - - // Move the current sort order to its own file. - private void ResettleSortOrder() - { - ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder; - var file = ModFileSystem.ModFileSystemFile; - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( "Data" ); - j.WriteStartObject(); - foreach( var (mod, path) in ModSortOrder.Where( kvp => Directory.Exists( Path.Combine( _config.ModDirectory, kvp.Key ) ) ) ) - { - j.WritePropertyName( mod, true ); - j.WriteValue( path ); - } - - j.WriteEndObject(); - j.WritePropertyName( "EmptyFolders" ); - j.WriteStartArray(); - j.WriteEndArray(); - j.WriteEndObject(); - } - - // Move the active collections to their own file. - private void ResettleCollectionSettings() - { - CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; - DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; - CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - SaveActiveCollectionsV0( DefaultCollection, CurrentCollection, DefaultCollection, - CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); - } - - // Outdated saving using the Characters list. - private static void SaveActiveCollectionsV0( string def, string ui, string current, IEnumerable<(string, string)> characters, - IEnumerable<(CollectionType, string)> special ) - { - var file = ModCollection.Manager.ActiveCollectionFile; - try - { - using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); - using var writer = new StreamWriter( stream ); - using var j = new JsonTextWriter( writer ); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName( nameof( ModCollection.Manager.Default ) ); - j.WriteValue( def ); - j.WritePropertyName( nameof( ModCollection.Manager.Interface ) ); - j.WriteValue( ui ); - j.WritePropertyName( nameof( ModCollection.Manager.Current ) ); - j.WriteValue( current ); - foreach( var (type, collection) in special ) - { - j.WritePropertyName( type.ToString() ); - j.WriteValue( collection ); - } - - j.WritePropertyName( "Characters" ); - j.WriteStartObject(); - foreach( var (character, collection) in characters ) - { - j.WritePropertyName( character, true ); - j.WriteValue( collection ); - } - - j.WriteEndObject(); - j.WriteEndObject(); - Penumbra.Log.Verbose( "Active Collections saved." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); - } - } - - // Collections were introduced and the previous CurrentCollection got put into ModDirectory. - private void Version0To1() - { - if( _config.Version != 0 ) - { - return; - } - - _config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty; - _config.Version = 1; - ResettleCollectionJson(); - } - - // Move the previous mod configurations to a new default collection file. - private void ResettleCollectionJson() - { - var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) ); - if( !collectionJson.Exists ) - { - return; - } - - var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection ); - var defaultCollectionFile = defaultCollection.FileName; - if( defaultCollectionFile.Exists ) - { - return; - } - - try - { - var text = File.ReadAllText( collectionJson.FullName ); - var data = JArray.Parse( text ); - - var maxPriority = 0; - var dict = new Dictionary< string, ModSettings.SavedSettings >(); - foreach( var setting in data.Cast< JObject >() ) - { - var modName = ( string )setting[ "FolderName" ]!; - var enabled = ( bool )setting[ "Enabled" ]!; - var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, long > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, long > >(); - - dict[ modName ] = new ModSettings.SavedSettings() - { - Enabled = enabled, - Priority = priority, - Settings = settings!, - }; - maxPriority = Math.Max( maxPriority, priority ); - } - - InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; - if( !InvertModListOrder ) - { - dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); - } - - defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); - defaultCollection.Save(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); - throw; - } - } - - // Create a backup of the configuration file specifically. - private static void CreateBackup() - { - var name = DalamudServices.PluginInterface.ConfigFile.FullName; - var bakName = name + ".bak"; - try - { - File.Copy( name, bakName, true ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); - } - } - - public enum SortModeV3 : byte - { - FoldersFirst = 0x00, - Lexicographical = 0x01, - InverseFoldersFirst = 0x02, - InverseLexicographical = 0x03, - FoldersLast = 0x04, - InverseFoldersLast = 0x05, - InternalOrder = 0x06, - InternalOrderInverse = 0x07, - } - } -} \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 1821ca1f..4d98a4a1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -19,8 +19,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public partial class Configuration : IPluginConfiguration +public class Configuration : IPluginConfiguration { + [JsonIgnore] + private readonly string _fileName; + + [JsonIgnore] + private readonly FrameworkManager _framework; + public int Version { get; set; } = Constants.CurrentVersion; public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; @@ -86,47 +92,44 @@ public partial class Configuration : IPluginConfiguration public Dictionary< ColorId, uint > Colors { get; set; } = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); - // Load the current configuration. - // Includes adding new colors and migrating from old versions. - public static Configuration Load() + /// + /// Load the current configuration. + /// Includes adding new colors and migrating from old versions. + /// + public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework) { - void HandleDeserializationError( object? sender, ErrorEventArgs errorArgs ) + _fileName = fileNames.ConfigFile; + _framework = framework; + Load(migrator); + } + + public void Load(ConfigMigrationService migrator) + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { Penumbra.Log.Error( - $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}" ); + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); errorArgs.ErrorContext.Handled = true; } - Configuration? configuration = null; - if( File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) ) + if (File.Exists(_fileName)) { - var text = File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ); - configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings + var text = File.ReadAllText(_fileName); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, - } ); + }); } - - configuration ??= new Configuration(); - if( configuration.Version == Constants.CurrentVersion ) - { - configuration.AddColors( false ); - return configuration; - } - - Migration.Migrate( configuration ); - configuration.AddColors( true ); - - return configuration; + migrator.Migrate(this); } - // Save the current configuration. + /// Save the current configuration. private void SaveConfiguration() { try { var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( DalamudServices.PluginInterface.ConfigFile.FullName, text ); + File.WriteAllText( _fileName, text ); } catch( Exception e ) { @@ -135,24 +138,9 @@ public partial class Configuration : IPluginConfiguration } public void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); + => _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); - // Add missing colors to the dictionary if necessary. - private void AddColors( bool forceSave ) - { - var save = false; - foreach( var color in Enum.GetValues< ColorId >() ) - { - save |= Colors.TryAdd( color, color.Data().DefaultColor ); - } - - if( save || forceSave ) - { - Save(); - } - } - - // Contains some default values or boundaries for config values. + /// Contains some default values or boundaries for config values. public static class Constants { public const int CurrentVersion = 7; @@ -178,6 +166,7 @@ public partial class Configuration : IPluginConfiguration }; } + /// Convert SortMode Types to their name. private class SortModeConverter : JsonConverter< ISortMode< Mod > > { public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 307e55c3..0f3a46d5 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game; using Dalamud.Utility.Signatures; using Penumbra.GameData; -using Penumbra.Services; namespace Penumbra.Interop; @@ -52,14 +52,17 @@ public unsafe partial class CharacterUtility : IDisposable public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) => _lists[ idx.Value ].DefaultResource; - public CharacterUtility() + private readonly Framework _framework; + + public CharacterUtility(Framework framework) { SignatureHelper.Initialise( this ); + _framework = framework; LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." ); LoadDefaultResources( null! ); if( !Ready ) { - DalamudServices.Framework.Update += LoadDefaultResources; + _framework.Update += LoadDefaultResources; } } @@ -99,7 +102,7 @@ public unsafe partial class CharacterUtility : IDisposable if( !anyMissing ) { Ready = true; - DalamudServices.Framework.Update -= LoadDefaultResources; + _framework.Update -= LoadDefaultResources; LoadingFinished.Invoke(); } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index d921e7f3..8e60d26f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -10,7 +10,6 @@ using FFXIVClientStructs.STD; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -249,18 +248,4 @@ public unsafe partial class ResourceLoader Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); return 1; } - - // Logging functions for EnableFullLogging. - private static void LogPath( Utf8GamePath path, bool synchronous ) - => Penumbra.Log.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - - private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) - { - var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); - Penumbra.Log.Information( - $"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); - } - - private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom ) - => Penumbra.Log.Information( $"[ResourceLoader] Loading {path} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 2eb0e010..5ad06e93 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -18,39 +18,6 @@ public unsafe partial class ResourceLoader : IDisposable // Hooks are required for everything, even events firing. public bool HooksEnabled { get; private set; } - // This Logging just logs all file requests, returns and loads to the Dalamud log. - // Events can be used to make smarter logging. - public bool IsLoggingEnabled { get; private set; } - - public void EnableFullLogging() - { - if( IsLoggingEnabled ) - { - return; - } - - IsLoggingEnabled = true; - ResourceRequested += LogPath; - ResourceLoaded += LogResource; - FileLoaded += LogLoadedFile; - ResourceHandleDestructorHook?.Enable(); - EnableHooks(); - } - - public void DisableFullLogging() - { - if( !IsLoggingEnabled ) - { - return; - } - - IsLoggingEnabled = false; - ResourceRequested -= LogPath; - ResourceLoaded -= LogResource; - FileLoaded -= LogLoadedFile; - ResourceHandleDestructorHook?.Disable(); - } - public void EnableReplacements() { if( DoReplacements ) @@ -150,7 +117,6 @@ public unsafe partial class ResourceLoader : IDisposable public void Dispose() { - DisableFullLogging(); DisposeHooks(); DisposeTexMdlTreatment(); } diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs deleted file mode 100644 index dceaad36..00000000 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Penumbra.String; -using Penumbra.String.Classes; - -namespace Penumbra.Interop.Loader; - -// A logger class that contains the relevant data to log requested files via regex. -// Filters are case-insensitive. -public class ResourceLogger : IDisposable -{ - // Enable or disable the logging of resources subject to the current filter. - public void SetState( bool value ) - { - if( value == Penumbra.Config.EnableResourceLogging ) - { - return; - } - - Penumbra.Config.EnableResourceLogging = value; - Penumbra.Config.Save(); - if( value ) - { - _resourceLoader.ResourceRequested += OnResourceRequested; - } - else - { - _resourceLoader.ResourceRequested -= OnResourceRequested; - } - } - - // Set the current filter to a new string, doing all other necessary work. - public void SetFilter( string newFilter ) - { - if( newFilter == Filter ) - { - return; - } - - Penumbra.Config.ResourceLoggingFilter = newFilter; - Penumbra.Config.Save(); - SetupRegex(); - } - - // Returns whether the current filter is a valid regular expression. - public bool ValidRegex - => _filterRegex != null; - - private readonly ResourceLoader _resourceLoader; - private Regex? _filterRegex; - - private static string Filter - => Penumbra.Config.ResourceLoggingFilter; - - private void SetupRegex() - { - try - { - _filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant ); - } - catch - { - _filterRegex = null; - } - } - - public ResourceLogger( ResourceLoader loader ) - { - _resourceLoader = loader; - SetupRegex(); - if( Penumbra.Config.EnableResourceLogging ) - { - _resourceLoader.ResourceRequested += OnResourceRequested; - } - } - - private void OnResourceRequested( Utf8GamePath data, bool synchronous ) - { - var path = Match( data.Path ); - if( path != null ) - { - Penumbra.Log.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); - } - } - - // Returns the converted string if the filter matches, and null otherwise. - // The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it. - private string? Match( ByteString data ) - { - var s = data.ToString(); - return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) ) - ? s - : null; - } - - public void Dispose() - => _resourceLoader.ResourceRequested -= OnResourceRequested; -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index 71e54d07..9cf765d0 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.Services; - + namespace Penumbra.Interop.Resolver; public class CutsceneCharacters : IDisposable @@ -14,39 +14,39 @@ public class CutsceneCharacters : IDisposable public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; private readonly GameEventManager _events; - private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); + private readonly ObjectTable _objects; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); - public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors - => Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) - .Where( i => DalamudServices.Objects[ i ] != null ) - .Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) ); + public IEnumerable> Actors + => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) + .Where(i => _objects[i] != null) + .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public CutsceneCharacters(GameEventManager events) + public CutsceneCharacters(ObjectTable objects, GameEventManager events) { - _events = events; + _objects = objects; + _events = events; Enable(); } // Get the related actor to a cutscene actor. // Does not check for valid input index. // Returns null if no connected actor is set or the actor does not exist anymore. - public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ] + public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] { get { - Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx ); - idx = _copiedCharacters[ idx - CutsceneStartIdx ]; - return idx < 0 ? null : DalamudServices.Objects[ idx ]; + Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx); + idx = _copiedCharacters[idx - CutsceneStartIdx]; + return idx < 0 ? null : _objects[idx]; } } // Return the currently set index of a parent or -1 if none is set or the index is invalid. - public int GetParentIndex( int idx ) + public int GetParentIndex(int idx) { - if( idx is >= CutsceneStartIdx and < CutsceneEndIdx ) - { - return _copiedCharacters[ idx - CutsceneStartIdx ]; - } + if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) + return _copiedCharacters[idx - CutsceneStartIdx]; return -1; } @@ -66,21 +66,21 @@ public class CutsceneCharacters : IDisposable public void Dispose() => Disable(); - private unsafe void OnCharacterDestructor( Character* character ) + private unsafe void OnCharacterDestructor(Character* character) { - if( character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + if (character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) { var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[ idx ] = -1; + _copiedCharacters[idx] = -1; } } - private unsafe void OnCharacterCopy( Character* target, Character* source ) + private unsafe void OnCharacterCopy(Character* target, Character* source) { - if( target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + if (target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) { var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[idx] = (short) (source != null ? source->GameObject.ObjectIndex : -1); + _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index efdb67c5..cad29dc0 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -5,72 +5,68 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using Penumbra.GameData.Actors; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; -public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> { - private readonly GameEventManager _events; - private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); - private bool _dirty = false; - private bool _enabled = false; + private readonly CommunicatorService _communicator; + private readonly GameEventManager _events; + private readonly Dictionary _cache = new(317); + private bool _dirty = false; + private bool _enabled = false; - public IdentifiedCollectionCache(GameEventManager events) + public IdentifiedCollectionCache(CommunicatorService communicator, GameEventManager events) { - _events = events; + _communicator = communicator; + _events = events; } public void Enable() { - if( _enabled ) - { + if (_enabled) return; - } - Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; - Penumbra.TempMods.CollectionChanged += CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged += TerritoryClear; + _communicator.CollectionChange.Event += CollectionChangeClear; + DalamudServices.ClientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; _enabled = true; } public void Disable() { - if( !_enabled ) - { + if (!_enabled) return; - } - Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; - Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; + _communicator.CollectionChange.Event -= CollectionChangeClear; + DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; _enabled = false; } - public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) + public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) { - if( _dirty ) + if (_dirty) { _dirty = false; _cache.Clear(); } - _cache[ ( IntPtr )data ] = ( identifier, collection ); - return collection.ToResolveData( data ); + _cache[(IntPtr)data] = (identifier, collection); + return collection.ToResolveData(data); } - public bool TryGetValue( GameObject* gameObject, out ResolveData resolve ) + public bool TryGetValue(GameObject* gameObject, out ResolveData resolve) { - if( _dirty ) + if (_dirty) { _dirty = false; _cache.Clear(); } - else if( _cache.TryGetValue( ( IntPtr )gameObject, out var p ) ) + else if (_cache.TryGetValue((IntPtr)gameObject, out var p)) { - resolve = p.Item2.ToResolveData( gameObject ); + resolve = p.Item2.ToResolveData(gameObject); return true; } @@ -81,19 +77,17 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt public void Dispose() { Disable(); - GC.SuppressFinalize( this ); + GC.SuppressFinalize(this); } - public IEnumerator< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > GetEnumerator() + public IEnumerator<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() { - foreach( var (address, (identifier, collection)) in _cache ) + foreach (var (address, (identifier, collection)) in _cache) { - if( _dirty ) - { + if (_dirty) yield break; - } - yield return ( address, identifier, collection ); + yield return (address, identifier, collection); } } @@ -103,17 +97,15 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private void CollectionChangeClear( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + private void CollectionChangeClear(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) { - if( type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive) ) - { + if (type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive)) _dirty = _cache.Count > 0; - } } - private void TerritoryClear( object? _1, ushort _2 ) + private void TerritoryClear(object? _1, ushort _2) => _dirty = _cache.Count > 0; - private void OnCharacterDestruct( Character* character ) - => _cache.Remove( ( IntPtr )character ); -} \ No newline at end of file + private void OnCharacterDestruct(Character* character) + => _cache.Remove((IntPtr)character); +} diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index efb7e61c..7f0ee266 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -12,43 +12,42 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; using Penumbra.Util; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { public class DrawObjectState { + private readonly CommunicatorService _communicator; public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; - public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; + public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; - public IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjects + public IEnumerable> DrawObjects => _drawObjectToObject; public int Count => _drawObjectToObject.Count; - public bool TryGetValue( IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject ) + public bool TryGetValue(IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject) { gameObject = null; - if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) - { + if (!_drawObjectToObject.TryGetValue(drawObject, out value)) return false; - } var gameObjectIdx = value.Item2; - return VerifyEntry( drawObject, gameObjectIdx, out gameObject ); + return VerifyEntry(drawObject, gameObjectIdx, out gameObject); } // Set and update a parent object if it exists and a last game object is set. - public ResolveData CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) + public ResolveData CheckParentDrawObject(IntPtr drawObject, IntPtr parentObject) { - if( parentObject == IntPtr.Zero && LastGameObject != null ) + if (parentObject == IntPtr.Zero && LastGameObject != null) { - var collection = IdentifyCollection( LastGameObject, true ); - _drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); + var collection = IdentifyCollection(LastGameObject, true); + _drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex); return collection; } @@ -56,11 +55,11 @@ public unsafe partial class PathResolver } - public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData ) + public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) { - if( type == ResourceType.Tex - && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) ) + if (type == ResourceType.Tex + && LastCreatedCollection.Valid + && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) { resolveData = LastCreatedCollection; return true; @@ -76,9 +75,10 @@ public unsafe partial class PathResolver public GameObject* LastGameObject { get; private set; } - public DrawObjectState() + public DrawObjectState(CommunicatorService communicator) { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); + _communicator = communicator; } public void Enable() @@ -88,8 +88,7 @@ public unsafe partial class PathResolver _enableDrawHook.Enable(); _weaponReloadHook.Enable(); InitializeDrawObjects(); - Penumbra.CollectionManager.CollectionChanged += CheckCollections; - Penumbra.TempMods.CollectionChanged += CheckCollections; + _communicator.CollectionChange.Event += CheckCollections; } public void Disable() @@ -98,8 +97,7 @@ public unsafe partial class PathResolver _characterBaseDestructorHook.Disable(); _enableDrawHook.Disable(); _weaponReloadHook.Disable(); - Penumbra.CollectionManager.CollectionChanged -= CheckCollections; - Penumbra.TempMods.CollectionChanged -= CheckCollections; + _communicator.CollectionChange.Event -= CheckCollections; } public void Dispose() @@ -112,63 +110,61 @@ public unsafe partial class PathResolver } // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. - private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject) { - gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx ); - var draw = ( DrawObject* )drawObject; - if( gameObject != null - && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) - { + gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var draw = (DrawObject*)drawObject; + if (gameObject != null + && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) return true; - } gameObject = null; - _drawObjectToObject.Remove( drawObject ); + _drawObjectToObject.Remove(drawObject); return false; } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); - private ResolveData _lastCreatedCollection = ResolveData.Invalid; + private readonly Dictionary _drawObjectToObject = new(); + private ResolveData _lastCreatedCollection = ResolveData.Invalid; // Keep track of created DrawObjects that are CharacterBase, // and use the last game object that called EnableDraw to link them. - private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + private delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d); - [Signature( Sigs.CharacterBaseCreate, DetourName = nameof( CharacterBaseCreateDetour ) )] - private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!; + [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))] + private readonly Hook _characterBaseCreateHook = null!; - private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) { - using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterBaseCreate ); + using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterBaseCreate); var meta = DisposableContainer.Empty; - if( LastGameObject != null ) + if (LastGameObject != null) { - _lastCreatedCollection = IdentifyCollection( LastGameObject, false ); + _lastCreatedCollection = IdentifyCollection(LastGameObject, false); // Change the transparent or 1.0 Decal if necessary. - var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); + var decal = new CharacterUtility.DecalReverter(_lastCreatedCollection.ModCollection, UsesDecal(a, c)); // Change the rsp parameters. - meta = new DisposableContainer( _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal ); + meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal); try { var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ( IntPtr )modelPtr, b, c ); + CreatingCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, (IntPtr)modelPtr, b, c); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Unknown Error during CreatingCharacterBase:\n{e}" ); + Penumbra.Log.Error($"Unknown Error during CreatingCharacterBase:\n{e}"); } } - var ret = _characterBaseCreateHook.Original( a, b, c, d ); + var ret = _characterBaseCreateHook.Original(a, b, c, d); try { - if( LastGameObject != null && ret != IntPtr.Zero ) + if (LastGameObject != null && ret != IntPtr.Zero) { - _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret ); + _drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex); + CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret); } } finally @@ -181,70 +177,66 @@ public unsafe partial class PathResolver // Check the customize array for the FaceCustomization byte and the last bit of that. // Also check for humans. - public static bool UsesDecal( uint modelId, IntPtr customizeData ) - => modelId == 0 && ( ( byte* )customizeData )[ 12 ] > 0x7F; + public static bool UsesDecal(uint modelId, IntPtr customizeData) + => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; // Remove DrawObjects from the list when they are destroyed. - private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + private delegate void CharacterBaseDestructorDelegate(IntPtr drawBase); - [Signature( Sigs.CharacterBaseDestructor, DetourName = nameof( CharacterBaseDestructorDetour ) )] - private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!; + [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))] + private readonly Hook _characterBaseDestructorHook = null!; - private void CharacterBaseDestructorDetour( IntPtr drawBase ) + private void CharacterBaseDestructorDetour(IntPtr drawBase) { - _drawObjectToObject.Remove( drawBase ); - _characterBaseDestructorHook!.Original.Invoke( drawBase ); + _drawObjectToObject.Remove(drawBase); + _characterBaseDestructorHook!.Original.Invoke(drawBase); } // EnableDraw is what creates DrawObjects for gameObjects, // so we always keep track of the current GameObject to be able to link it to the DrawObject. - private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + private delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d); - [Signature( Sigs.EnableDraw, DetourName = nameof( EnableDrawDetour ) )] - private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; + [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] + private readonly Hook _enableDrawHook = null!; - private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d) { var oldObject = LastGameObject; - LastGameObject = ( GameObject* )gameObject; - _enableDrawHook!.Original.Invoke( gameObject, b, c, d ); + LastGameObject = (GameObject*)gameObject; + _enableDrawHook!.Original.Invoke(gameObject, b, c, d); LastGameObject = oldObject; } // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, // so we use that. - private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ); + private delegate void WeaponReloadFunc(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7); - [Signature( Sigs.WeaponReload, DetourName = nameof( WeaponReloadDetour ) )] - private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!; + [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))] + private readonly Hook _weaponReloadHook = null!; - public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 ) + public void WeaponReloadDetour(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7) { var oldGame = LastGameObject; - LastGameObject = *( GameObject** )( a1 + 8 ); - _weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); + LastGameObject = *(GameObject**)(a1 + 8); + _weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7); LastGameObject = oldGame; } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) { - if( type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface ) - { + if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface) return; - } - foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() ) + foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray()) { - if( !VerifyEntry( key, idx, out var obj ) ) - { - _drawObjectToObject.Remove( key ); - } + if (!VerifyEntry(key, idx, out var obj)) + _drawObjectToObject.Remove(key); - var newCollection = IdentifyCollection( obj, false ); - _drawObjectToObject[ key ] = ( newCollection, idx ); + var newCollection = IdentifyCollection(obj, false); + _drawObjectToObject[key] = (newCollection, idx); } } @@ -252,14 +244,12 @@ public unsafe partial class PathResolver // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - for( var i = 0; i < DalamudServices.Objects.Length; ++i ) + for (var i = 0; i < DalamudServices.Objects.Length; ++i) { - var ptr = ( GameObject* )DalamudServices.Objects.GetObjectAddress( i ); - if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) - { - _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex ); - } + var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i); + if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) + _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); } } } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index cacd3b20..516d2a2b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -103,7 +103,7 @@ public unsafe partial class PathResolver // Check both temporary and permanent character collections. Temporary first. private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) - => Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) + => Penumbra.TempCollections.Collections.TryGetCollection( identifier, out var collection ) || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) ? collection : null; diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index e3fb47e0..1568b363 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -265,7 +265,7 @@ public partial class PathResolver } var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; - var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); + var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject ); if( parentCollection.Valid ) { return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path ); diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index b64323a2..140804ce 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,10 +5,11 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -24,70 +25,70 @@ public partial class PathResolver : IDisposable { public bool Enabled { get; private set; } - private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); - private static readonly DrawObjectState DrawObjects = new(); - private static readonly BitArray ValidHumanModels; - internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); - private readonly AnimationState _animations; - private readonly PathState _paths; - private readonly MetaState _meta; - private readonly SubfileHelper _subFiles; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _loader; + private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO + private static DrawObjectState _drawObjects = null!; // TODO + private static readonly BitArray ValidHumanModels; + internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO + private readonly AnimationState _animations; + private readonly PathState _paths; + private readonly MetaState _meta; + private readonly SubfileHelper _subFiles; static PathResolver() - => ValidHumanModels = GetValidHumanModels( DalamudServices.GameData ); + => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData); - public unsafe PathResolver( ResourceLoader loader ) + public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) { - using var tApi = Penumbra.StartTimer.Measure( StartTimeType.PathResolver ); - SignatureHelper.Initialise( this ); + using var tApi = timer.Measure(StartTimeType.PathResolver); + _communicator = communicator; + IdentifiedCache = new IdentifiedCollectionCache(communicator, events); + SignatureHelper.Initialise(this); + _drawObjects = new DrawObjectState(_communicator); _loader = loader; - _animations = new AnimationState( DrawObjects ); - _paths = new PathState( this ); - _meta = new MetaState( _paths.HumanVTable ); - _subFiles = new SubfileHelper( _loader, Penumbra.GameEvents ); + _animations = new AnimationState(_drawObjects); + _paths = new PathState(this); + _meta = new MetaState(_paths.HumanVTable); + _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); } // The modified resolver that handles game path resolving. - private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data ) + private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data) { - using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterResolver ); + using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver); // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, // or if it is a face decal path and the current mod collection is set. // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = _subFiles.HandleSubFiles( type, out var resolveData ) - || _paths.Consume( gamePath.Path, out resolveData ) - || _animations.HandleFiles( type, gamePath, out resolveData ) - || DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); - if( !nonDefault || !resolveData.Valid ) - { + var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData) + || _paths.Consume(gamePath.Path, out resolveData) + || _animations.HandleFiles(type, gamePath, out resolveData) + || _drawObjects.HandleDecalFile(type, gamePath, out resolveData); + if (!nonDefault || !resolveData.Valid) resolveData = Penumbra.CollectionManager.Default.ToResolveData(); - } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = resolveData.ModCollection.ResolvePath( gamePath ); + var resolved = resolveData.ModCollection.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. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out data); return true; } public void Enable() { - if( Enabled ) - { + if (Enabled) return; - } Enabled = true; Cutscenes.Enable(); - DrawObjects.Enable(); + _drawObjects.Enable(); IdentifiedCache.Enable(); _animations.Enable(); _paths.Enable(); @@ -95,19 +96,17 @@ public partial class PathResolver : IDisposable _subFiles.Enable(); _loader.ResolvePathCustomization += CharacterResolver; - Penumbra.Log.Debug( "Character Path Resolver enabled." ); + Penumbra.Log.Debug("Character Path Resolver enabled."); } public void Disable() { - if( !Enabled ) - { + if (!Enabled) return; - } Enabled = false; _animations.Disable(); - DrawObjects.Disable(); + _drawObjects.Disable(); Cutscenes.Disable(); IdentifiedCache.Disable(); _paths.Disable(); @@ -115,7 +114,7 @@ public partial class PathResolver : IDisposable _subFiles.Disable(); _loader.ResolvePathCustomization -= CharacterResolver; - Penumbra.Log.Debug( "Character Path Resolver disabled." ); + Penumbra.Log.Debug("Character Path Resolver disabled."); } public void Dispose() @@ -123,58 +122,58 @@ public partial class PathResolver : IDisposable Disable(); _paths.Dispose(); _animations.Dispose(); - DrawObjects.Dispose(); + _drawObjects.Dispose(); Cutscenes.Dispose(); IdentifiedCache.Dispose(); _meta.Dispose(); _subFiles.Dispose(); } - public static unsafe (IntPtr, ResolveData) IdentifyDrawObject( IntPtr drawObject ) + public static unsafe (IntPtr, ResolveData) IdentifyDrawObject(IntPtr drawObject) { - var parent = FindParent( drawObject, out var resolveData ); - return ( ( IntPtr )parent, resolveData ); + var parent = FindParent(drawObject, out var resolveData); + return ((IntPtr)parent, resolveData); } - public int CutsceneActor( int idx ) - => Cutscenes.GetParentIndex( idx ); + public int CutsceneActor(int idx) + => Cutscenes.GetParentIndex(idx); // Use the stored information to find the GameObject and Collection linked to a DrawObject. - public static unsafe GameObject* FindParent( IntPtr drawObject, out ResolveData resolveData ) + public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData) { - if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) + if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject)) { resolveData = data.Item1; return gameObject; } - if( DrawObjects.LastGameObject != null - && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + if (_drawObjects.LastGameObject != null + && (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject)) { - resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); - return DrawObjects.LastGameObject; + resolveData = IdentifyCollection(_drawObjects.LastGameObject, true); + return _drawObjects.LastGameObject; } - resolveData = IdentifyCollection( null, true ); + resolveData = IdentifyCollection(null, true); return null; } - private static unsafe ResolveData GetResolveData( IntPtr drawObject ) + private static unsafe ResolveData GetResolveData(IntPtr drawObject) { - var _ = FindParent( drawObject, out var resolveData ); + var _ = FindParent(drawObject, out var resolveData); return resolveData; } - internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections + internal IEnumerable> PathCollections => _paths.Paths; - internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap - => DrawObjects.DrawObjects; + internal IEnumerable> DrawObjectMap + => _drawObjects.DrawObjects; - internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors + internal IEnumerable> CutsceneActors => Cutscenes.Actors; - internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections + internal IEnumerable> ResourceCollections => _subFiles; internal int SubfileCount @@ -187,8 +186,8 @@ public partial class PathResolver : IDisposable => _subFiles.AvfxData; internal ResolveData LastGameObjectData - => DrawObjects.LastCreatedCollection; + => _drawObjects.LastCreatedCollection; internal unsafe nint LastGameObject - => (nint) DrawObjects.LastGameObject; -} \ No newline at end of file + => (nint)_drawObjects.LastGameObject; +} diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 661a692c..3ee984b6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -169,7 +169,7 @@ public partial class MetaManager var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( ( Penumbra.TempMods.CollectionByName( name, out var collection ) + if( ( Penumbra.TempCollections.CollectionByName( name, out var collection ) || Penumbra.CollectionManager.ByName( name, out collection ) ) && collection.HasCache && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index ad321b5e..57699fcb 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Plugin; using Newtonsoft.Json; using Penumbra.Services; @@ -10,8 +11,8 @@ namespace Penumbra.Mods; public sealed partial class Mod { - public static DirectoryInfo LocalDataDirectory - => new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data" )); + public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi) + => new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" )); public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 977db6f3..fa9fec1d 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Dalamud.Plugin; using OtterGui.Filesystem; using Penumbra.Services; @@ -11,15 +12,16 @@ namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { - public static string ModFileSystemFile - => Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); + public static string ModFileSystemFile(DalamudPluginInterface pi) + => Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" ); // Save the current sort order. // Does not save or copy the backup in the current mod directory, - // as this is done on mod directory changes only. + // as this is done on mod directory changes only. + // TODO private void SaveFilesystem() { - SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); + SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true ); Penumbra.Log.Verbose( "Saved mod filesystem." ); } @@ -74,8 +76,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() - { - if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) + { + // TODO + if( Load( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) { Save(); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d14f94e3..0a85e113 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -29,45 +28,10 @@ using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; +using Penumbra.Services; namespace Penumbra; -public class PenumbraNew -{ - public string Name - => "Penumbra"; - - public static readonly Logger Log = new(); - public readonly StartTimeTracker< StartTimeType > StartTimer = new(); - - public readonly IServiceCollection Services = new ServiceCollection(); - - - public PenumbraNew( DalamudPluginInterface pi ) - { - using var time = StartTimer.Measure( StartTimeType.Total ); - - // Add meta services. - Services.AddSingleton( Log ); - Services.AddSingleton( StartTimer ); - Services.AddSingleton< ValidityChecker >(); - Services.AddSingleton< PerformanceTracker< PerformanceType > >(); - - // Add Dalamud services - var dalamud = new DalamudServices( pi ); - dalamud.AddServices( Services ); - - // Add Game Data - - - // Add Configuration - Services.AddSingleton< Configuration >(); - } - - public void Dispose() - { } -} - public class Penumbra : IDalamudPlugin { public string Name @@ -76,135 +40,123 @@ public class Penumbra : IDalamudPlugin public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; public static readonly string CommitHash = - Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown"; + Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static Logger Log { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static GameEventManager GameEvents { get; private set; } = null!; - public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Manager ModManager { get; private set; } = null!; - public static ModCollection.Manager CollectionManager { get; private set; } = null!; - public static TempModManager TempMods { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; private set; } = null!; - public static FrameworkManager Framework { get; private set; } = null!; - public static ActorManager Actors { get; private set; } = null!; - public static IObjectIdentifier Identifier { get; private set; } = null!; - public static IGamePathParser GamePathParser { get; private set; } = null!; - public static StainManager StainManager { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static GameEventManager GameEvents { get; private set; } = null!; + public static MetaFileManager MetaFileManager { get; private set; } = null!; + public static Mod.Manager ModManager { get; private set; } = null!; + public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static TempCollectionManager TempCollections { get; private set; } = null!; + public static TempModManager TempMods { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; + public static FrameworkManager Framework { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; + public static IObjectIdentifier Identifier { get; private set; } = null!; + public static IGamePathParser GamePathParser { get; private set; } = null!; + public static StainService StainService { get; private set; } = null!; + + // TODO + public static DalamudServices Dalamud { get; private set; } = null!; public static ValidityChecker ValidityChecker { get; private set; } = null!; - public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; + public static PerformanceTracker Performance { get; private set; } = null!; - public static readonly StartTimeTracker< StartTimeType > StartTimer = new(); + public readonly PathResolver PathResolver; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystem ModFileSystem; + public readonly PenumbraApi Api; + public readonly HttpApi HttpApi; + public readonly PenumbraIpcProviders IpcProviders; + internal ConfigWindow? ConfigWindow { get; private set; } + private LaunchButton? _launchButton; + private WindowSystem? _windowSystem; + private Changelog? _changelog; + private CommandHandler? _commandHandler; + private readonly ResourceWatcher _resourceWatcher; + private bool _disposed; - public readonly ResourceLogger ResourceLogger; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; - internal ConfigWindow? ConfigWindow { get; private set; } - private LaunchButton? _launchButton; - private WindowSystem? _windowSystem; - private Changelog? _changelog; - private CommandHandler? _commandHandler; - private readonly ResourceWatcher _resourceWatcher; - private bool _disposed; + private readonly PenumbraNew _tmp; + public static ItemData ItemData { get; private set; } = null!; - public static ItemData ItemData { get; private set; } = null!; - - public Penumbra( DalamudPluginInterface pluginInterface ) + public Penumbra(DalamudPluginInterface pluginInterface) { - using var time = StartTimer.Measure( StartTimeType.Total ); - + Log = PenumbraNew.Log; + _tmp = new PenumbraNew(pluginInterface); + Performance = _tmp.Services.GetRequiredService(); + ValidityChecker = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + ItemData = _tmp.Services.GetRequiredService().AwaitedService; + Dalamud = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); try { - DalamudServices.Initialize( pluginInterface ); - - Performance = new PerformanceTracker< PerformanceType >( DalamudServices.Framework ); - Log = new Logger(); - ValidityChecker = new ValidityChecker( DalamudServices.PluginInterface ); - - GameEvents = new GameEventManager(); - StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( DalamudServices.PluginInterface, DalamudServices.GameData ) ); - StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() ); - StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( DalamudServices.PluginInterface, DalamudServices.GameData ) ); - ItemData = StartTimer.Measure( StartTimeType.Items, - () => new ItemData( DalamudServices.PluginInterface, DalamudServices.GameData, DalamudServices.GameData.Language ) ); - StartTimer.Measure( StartTimeType.Actors, - () => Actors = new ActorManager( DalamudServices.PluginInterface, DalamudServices.Objects, DalamudServices.ClientState, DalamudServices.Framework, - DalamudServices.GameData, DalamudServices.GameGui, - ResolveCutscene ) ); - - Framework = new FrameworkManager( DalamudServices.Framework, Log ); - CharacterUtility = new CharacterUtility(); - - StartTimer.Measure( StartTimeType.Backup, () => Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ) ); - Config = Configuration.Load(); - - TempMods = new TempModManager(); - MetaFileManager = new MetaFileManager(); - ResourceLoader = new ResourceLoader( this ); + ResourceLoader = new ResourceLoader(this); ResourceLoader.EnableHooks(); - _resourceWatcher = new ResourceWatcher( ResourceLoader ); - ResourceLogger = new ResourceLogger( ResourceLoader ); + _resourceWatcher = new ResourceWatcher(ResourceLoader); ResidentResources = new ResidentResourceManager(); - StartTimer.Measure( StartTimeType.Mods, () => + _tmp.Services.GetRequiredService().Measure(StartTimeType.Mods, () => { - ModManager = new Mod.Manager( Config.ModDirectory ); + ModManager = new Mod.Manager(Config.ModDirectory); ModManager.DiscoverMods(); - } ); + }); - StartTimer.Measure( StartTimeType.Collections, () => + _tmp.Services.GetRequiredService().Measure(StartTimeType.Collections, () => { - CollectionManager = new ModCollection.Manager( ModManager ); + CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService(), ModManager); CollectionManager.CreateNecessaryCaches(); - } ); + }); + + + TempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); - PathResolver = new PathResolver( ResourceLoader ); + PathResolver = new PathResolver(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), ResourceLoader); SetupInterface(); - if( Config.EnableMods ) + if (Config.EnableMods) { ResourceLoader.EnableReplacements(); PathResolver.Enable(); } - if( Config.DebugMode ) - { + if (Config.DebugMode) ResourceLoader.EnableDebug(); - } - using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) + using (var tApi = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api)) { - Api = new PenumbraApi( this ); - IpcProviders = new PenumbraIpcProviders( DalamudServices.PluginInterface, Api ); - HttpApi = new HttpApi( Api ); - if( Config.EnableHttpApi ) - { + Api = new PenumbraApi(_tmp.Services.GetRequiredService(), this); + IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api); + HttpApi = new HttpApi(Api); + if (Config.EnableHttpApi) HttpApi.CreateWebServer(); - } SubscribeItemLinks(); } ValidityChecker.LogExceptions(); - Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); - OtterTex.NativeDll.Initialize( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName ); - Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); + Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + OtterTex.NativeDll.Initialize(DalamudServices.PluginInterface.AssemblyLocation.DirectoryName); + Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); - if( CharacterUtility.Ready ) - { + if (CharacterUtility.Ready) ResidentResources.Reload(); - } } catch { @@ -215,21 +167,22 @@ public class Penumbra : IDalamudPlugin private void SetupInterface() { - Task.Run( () => + Task.Run(() => { - using var tInterface = StartTimer.Measure( StartTimeType.Interface ); + using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); var changelog = ConfigWindow.CreateChangelog(); - var cfg = new ConfigWindow( this, _resourceWatcher ) + var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) { IsOpen = Config.DebugMode, }; - var btn = new LaunchButton( cfg ); - var system = new WindowSystem( Name ); - var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); - system.AddWindow( cfg ); - system.AddWindow( cfg.ModEditPopup ); - system.AddWindow( changelog ); - if( !_disposed ) + var btn = new LaunchButton(cfg); + var system = new WindowSystem(Name); + var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, + Actors); + system.AddWindow(cfg); + system.AddWindow(cfg.ModEditPopup); + system.AddWindow(changelog); + if (!_disposed) { _changelog = changelog; ConfigWindow = cfg; @@ -251,100 +204,87 @@ public class Penumbra : IDalamudPlugin private void DisposeInterface() { - if( _windowSystem != null ) - { + if (_windowSystem != null) DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - } _launchButton?.Dispose(); - if( ConfigWindow != null ) + if (ConfigWindow != null) { DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; ConfigWindow.Dispose(); } } - public event Action< bool >? EnabledChange; + public event Action? EnabledChange; - public bool SetEnabled( bool enabled ) + public bool SetEnabled(bool enabled) { - if( enabled == Config.EnableMods ) - { + if (enabled == Config.EnableMods) return false; - } Config.EnableMods = enabled; - if( enabled ) + if (enabled) { ResourceLoader.EnableReplacements(); PathResolver.Enable(); - if( CharacterUtility.Ready ) + if (CharacterUtility.Ready) { CollectionManager.Default.SetFiles(); ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + ObjectReloader.RedrawAll(RedrawType.Redraw); } } else { ResourceLoader.DisableReplacements(); PathResolver.Disable(); - if( CharacterUtility.Ready ) + if (CharacterUtility.Ready) { CharacterUtility.ResetAll(); ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); + ObjectReloader.RedrawAll(RedrawType.Redraw); } } Config.Save(); - EnabledChange?.Invoke( enabled ); + EnabledChange?.Invoke(enabled); return true; } public void ForceChangelogOpen() { - if( _changelog != null ) - { + if (_changelog != null) _changelog.ForceOpen = true; - } } private void SubscribeItemLinks() { Api.ChangedItemTooltip += it => { - if( it is Item ) - { - ImGui.TextUnformatted( "Left Click to create an item link in chat." ); - } + if (it is Item) + ImGui.TextUnformatted("Left Click to create an item link in chat."); }; - Api.ChangedItemClicked += ( button, it ) => + Api.ChangedItemClicked += (button, it) => { - if( button == MouseButton.Left && it is Item item ) - { - ChatUtil.LinkItem( item ); - } + if (button == MouseButton.Left && it is Item item) + ChatUtil.LinkItem(item); }; - } - - private short ResolveCutscene( ushort index ) - => ( short )PathResolver.CutsceneActor( index ); - + } + public void Dispose() { - if( _disposed ) - { + if (_disposed) return; - } - + + // TODO + _tmp?.Dispose(); _disposed = true; HttpApi?.Dispose(); IpcProviders?.Dispose(); Api?.Dispose(); _commandHandler?.Dispose(); - StainManager?.Dispose(); + StainService?.Dispose(); ItemData?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); @@ -354,99 +294,85 @@ public class Penumbra : IDalamudPlugin ModFileSystem?.Dispose(); CollectionManager?.Dispose(); PathResolver?.Dispose(); - ResourceLogger?.Dispose(); _resourceWatcher?.Dispose(); ResourceLoader?.Dispose(); GameEvents?.Dispose(); CharacterUtility?.Dispose(); - Performance?.Dispose(); + Performance?.Dispose(); } - // Collect all relevant files for penumbra configuration. - private static IReadOnlyList< FileInfo > PenumbraBackupFiles() + public string GatherSupportInformation() { - var collectionDir = ModCollection.CollectionDirectory; - var list = Directory.Exists( collectionDir ) - ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() - : new List< FileInfo >(); - list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); - list.Add( DalamudServices.PluginInterface.ConfigFile ); - list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); - list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); - return list; - } - - public static string GatherSupportInformation() - { - var sb = new StringBuilder( 10240 ); - var exists = Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory ); - var drive = exists ? new DriveInfo( new DirectoryInfo( Config.ModDirectory ).Root.FullName ) : null; - sb.AppendLine( "**Settings**" ); - sb.Append( $"> **`Plugin Version: `** {Version}\n" ); - sb.Append( $"> **`Commit Hash: `** {CommitHash}\n" ); - sb.Append( $"> **`Enable Mods: `** {Config.EnableMods}\n" ); - sb.Append( $"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n" ); - sb.Append( $"> **`Operating System: `** {( DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows" )}\n" ); - sb.Append( $"> **`Root Directory: `** `{Config.ModDirectory}`, {( exists ? "Exists" : "Not Existing" )}\n" ); - sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); - sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" ); - sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" ); + var sb = new StringBuilder(10240); + var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); + var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null; + sb.AppendLine("**Settings**"); + sb.Append($"> **`Plugin Version: `** {Version}\n"); + sb.Append($"> **`Commit Hash: `** {CommitHash}\n"); + sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n"); + sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); + sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); + sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( - $"> **`Synchronous Load (Dalamud): `** {( DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); - sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); - sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); - sb.AppendLine( "**Mods**" ); - sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\n" ); - sb.Append( $"> **`Mods with Config: `** {ModManager.Count( m => m.HasOptions )}\n" ); - sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); - sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); - sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); - sb.Append( $"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n" ); - sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" ); + $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); + sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n"); + sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n"); + sb.Append( + $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); + sb.Append( + $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n"); + sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); + sb.AppendLine("**Mods**"); + sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n"); + sb.Append($"> **`Mods with Config: `** {ModManager.Count(m => m.HasOptions)}\n"); + sb.Append( + $"> **`Mods with File Redirections: `** {ModManager.Count(m => m.TotalFileCount > 0)}, Total: {ModManager.Sum(m => m.TotalFileCount)}\n"); + sb.Append( + $"> **`Mods with FileSwaps: `** {ModManager.Count(m => m.TotalSwapCount > 0)}, Total: {ModManager.Sum(m => m.TotalSwapCount)}\n"); + sb.Append( + $"> **`Mods with Meta Manipulations:`** {ModManager.Count(m => m.TotalManipulations > 0)}, Total {ModManager.Sum(m => m.TotalManipulations)}\n"); + sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n"); + sb.Append( + $"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n"); - string CharacterName( ActorIdentifier id, string name ) + string CharacterName(ActorIdentifier id, string name) { - if( id.Type is IdentifierType.Player or IdentifierType.Owned ) + if (id.Type is IdentifierType.Player or IdentifierType.Owned) { - var parts = name.Split( ' ', 3 ); - return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); + var parts = name.Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); } return name + ':'; } - void PrintCollection( ModCollection c ) - => sb.Append( $"**Collection {c.AnonymizedName}**\n" + void PrintCollection(ModCollection c) + => sb.Append($"**Collection {c.AnonymizedName}**\n" + $"> **`Inheritances: `** {c.Inheritance.Count}\n" - + $"> **`Enabled Mods: `** {c.ActualSettings.Count( s => s is { Enabled: true } )}\n" - + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count )}/{c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count )}\n" ); + + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" + + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? 0 : x.Conflicts.Count)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count)}\n"); - sb.AppendLine( "**Collections**" ); - sb.Append( $"> **`#Collections: `** {CollectionManager.Count - 1}\n" ); - sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" ); - sb.Append( $"> **`Active Collections: `** {CollectionManager.Count( c => c.HasCache )}\n" ); - sb.Append( $"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n" ); - sb.Append( $"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n" ); - sb.Append( $"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n" ); - foreach( var (type, name, _) in CollectionTypeExtensions.Special ) + sb.AppendLine("**Collections**"); + sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); + sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n"); + sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n"); + foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var collection = CollectionManager.ByType( type ); - if( collection != null ) - { - sb.Append( $"> **`{name,-30}`** {collection.AnonymizedName}\n" ); - } + var collection = CollectionManager.ByType(type); + if (collection != null) + sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); } - foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments ) - { - sb.Append( $"> **`{CharacterName( id[ 0 ], name ),-30}`** {collection.AnonymizedName}\n" ); - } + foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments) + sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); - foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) - { - PrintCollection( collection ); - } + foreach (var collection in CollectionManager.Where(c => c.HasCache)) + PrintCollection(collection); return sb.ToString(); } -} \ No newline at end of file +} diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index b08055d7..18da6c62 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -1,11 +1,14 @@ -using System.IO; +using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using Penumbra.Api; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop; +using Penumbra.Interop.Resolver; using Penumbra.Services; using Penumbra.Util; @@ -16,35 +19,61 @@ public class PenumbraNew public string Name => "Penumbra"; - public static readonly Logger Log = new(); - public readonly StartTimeTracker StartTimer = new(); - - public readonly IServiceCollection Services = new ServiceCollection(); - + public static readonly Logger Log = new(); + public readonly ServiceProvider Services; public PenumbraNew(DalamudPluginInterface pi) { - using var time = StartTimer.Measure(StartTimeType.Total); + var startTimer = new StartTracker(); + using var time = startTimer.Measure(StartTimeType.Total); + var services = new ServiceCollection(); // Add meta services. - Services.AddSingleton(Log); - Services.AddSingleton(StartTimer); - Services.AddSingleton(); - Services.AddSingleton>(); + services.AddSingleton(Log) + .AddSingleton(startTimer) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Dalamud services var dalamud = new DalamudServices(pi); - dalamud.AddServices(Services); + dalamud.AddServices(services); // Add Game Data - Services.AddSingleton(); - Services.AddSingleton(); - Services.AddSingleton(); + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add Game Services + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + // Add Configuration - Services.AddSingleton(); + services.AddTransient() + .AddSingleton(); + + // Add Collection Services + services.AddTransient() + .AddSingleton(); + + // Add Mod Services + // TODO + services.AddSingleton(); + + // Add Interface + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() - { } -} \ No newline at end of file + { + Services.Dispose(); + } +} diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs new file mode 100644 index 00000000..23fb20cc --- /dev/null +++ b/Penumbra/Services/BackupService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OtterGui.Classes; +using OtterGui.Log; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class BackupService +{ + public BackupService(Logger logger, StartTracker timer, FilenameService fileNames) + { + using var t = timer.Measure(StartTimeType.Backup); + var files = PenumbraFiles(fileNames); + Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + } + + // Collect all relevant files for penumbra configuration. + private static IReadOnlyList PenumbraFiles(FilenameService fileNames) + { + var list = fileNames.CollectionFiles.ToList(); + list.AddRange(fileNames.LocalDataFiles); + list.Add(new FileInfo(fileNames.ConfigFile)); + list.Add(new FileInfo(fileNames.FilesystemFile)); + list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); + return list; + } +} diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs new file mode 100644 index 00000000..47d215c6 --- /dev/null +++ b/Penumbra/Services/CommunicatorService.cs @@ -0,0 +1,30 @@ +using System; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class CommunicatorService : IDisposable +{ + /// + /// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) + /// Parameter is the old collection, or null on additions. + /// Parameter is the new collection, or null on deletions. + /// Parameter is the display name for Individual collections or an empty string otherwise. + /// + public readonly EventWrapper CollectionChange = new(nameof(CollectionChange)); + + /// + /// Parameter added, deleted or edited temporary mod. + /// Parameter is whether the mod was newly created. + /// Parameter is whether the mod was deleted. + /// + public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + + public void Dispose() + { + CollectionChange.Dispose(); + TemporaryGlobalModChange.Dispose(); + } +} diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs new file mode 100644 index 00000000..4d37f693 --- /dev/null +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp; + +namespace Penumbra.Services; + +/// +/// Contains everything to migrate from older versions of the config to the current, +/// including deprecated fields. +/// +public class ConfigMigrationService +{ + private readonly FilenameService _fileNames; + private readonly DalamudPluginInterface _pluginInterface; + + private Configuration _config = null!; + private JObject _data = null!; + + public string CurrentCollection = ModCollection.DefaultCollection; + public string DefaultCollection = ModCollection.DefaultCollection; + public string ForcedCollection = string.Empty; + public Dictionary CharacterCollections = new(); + public Dictionary ModSortOrder = new(); + public bool InvertModListOrder; + public bool SortFoldersFirst; + public SortModeV3 SortMode = SortModeV3.FoldersFirst; + + public ConfigMigrationService(FilenameService fileNames, DalamudPluginInterface pi) + { + _fileNames = fileNames; + _pluginInterface = pi; + } + + /// Add missing colors to the dictionary if necessary. + private static void AddColors(Configuration config, bool forceSave) + { + var save = false; + foreach (var color in Enum.GetValues()) + { + save |= config.Colors.TryAdd(color, color.Data().DefaultColor); + } + + if (save || forceSave) + { + config.Save(); + } + } + + public void Migrate(Configuration config) + { + _config = config; + // Do this on every migration from now on for a while + // because it stayed alive for a bunch of people for some reason. + DeleteMetaTmp(); + + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_fileNames.ConfigFile)) + { + AddColors(config, false); + return; + } + + _data = JObject.Parse(File.ReadAllText(_fileNames.ConfigFile)); + CreateBackup(); + + Version0To1(); + Version1To2(); + Version2To3(); + Version3To4(); + Version4To5(); + Version5To6(); + Version6To7(); + AddColors(config, true); + } + + // Gendered special collections were added. + private void Version6To7() + { + if (_config.Version != 6) + return; + + ModCollection.Manager.MigrateUngenderedCollections(_fileNames); + _config.Version = 7; + } + + + // A new tutorial step was inserted in the middle. + // The UI collection and a new tutorial for it was added. + // The migration for the UI collection itself happens in the ActiveCollections file. + private void Version5To6() + { + if (_config.Version != 5) + return; + + if (_config.TutorialStep == 25) + _config.TutorialStep = 27; + + _config.Version = 6; + } + + // Mod backup extension was changed from .zip to .pmp. + // Actual migration takes place in ModManager. + private void Version4To5() + { + if (_config.Version != 4) + return; + + Mod.Manager.MigrateModBackups = true; + _config.Version = 5; + } + + // SortMode was changed from an enum to a type. + private void Version3To4() + { + if (_config.Version != 3) + return; + + SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; + _config.SortMode = SortMode switch + { + SortModeV3.FoldersFirst => ISortMode.FoldersFirst, + SortModeV3.Lexicographical => ISortMode.Lexicographical, + SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, + SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, + SortModeV3.FoldersLast => ISortMode.FoldersLast, + SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, + SortModeV3.InternalOrder => ISortMode.InternalOrder, + SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, + _ => ISortMode.FoldersFirst, + }; + _config.Version = 4; + } + + // SortFoldersFirst was changed from a bool to the enum SortMode. + private void Version2To3() + { + if (_config.Version != 2) + return; + + SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; + SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; + _config.Version = 3; + } + + // The forced collection was removed due to general inheritance. + // Sort Order was moved to a separate file and may contain empty folders. + // Active collections in general were moved to their own file. + // Delete the penumbrametatmp folder if it exists. + private void Version1To2() + { + if (_config.Version != 1) + return; + + // Ensure the right meta files are loaded. + DeleteMetaTmp(); + Penumbra.CharacterUtility.LoadCharacterResources(); + ResettleSortOrder(); + ResettleCollectionSettings(); + ResettleForcedCollection(); + _config.Version = 2; + } + + private void DeleteMetaTmp() + { + var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); + if (!Directory.Exists(path)) + return; + + try + { + Directory.Delete(path, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); + } + } + + private void ResettleForcedCollection() + { + ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection; + if (ForcedCollection.Length <= 0) + return; + + // Add the previous forced collection to all current collections except itself as an inheritance. + foreach (var collection in _fileNames.CollectionFiles) + { + try + { + var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); + if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) + continue; + + jObject[nameof(ModCollection.Inheritance)] = JToken.FromObject(new List { ForcedCollection }); + File.WriteAllText(collection.FullName, jObject.ToString()); + } + catch (Exception e) + { + Penumbra.Log.Error( + $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); + } + } + } + + // Move the current sort order to its own file. + private void ResettleSortOrder() + { + ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; + var file = _fileNames.FilesystemFile; + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName("Data"); + j.WriteStartObject(); + foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) + { + j.WritePropertyName(mod, true); + j.WriteValue(path); + } + + j.WriteEndObject(); + j.WritePropertyName("EmptyFolders"); + j.WriteStartArray(); + j.WriteEndArray(); + j.WriteEndObject(); + } + + // Move the active collections to their own file. + private void ResettleCollectionSettings() + { + CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; + DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; + CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; + SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, + CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); + } + + // Outdated saving using the Characters list. + private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, + IEnumerable<(CollectionType, string)> special) + { + var file = _fileNames.ActiveCollectionsFile; + try + { + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName(nameof(ModCollection.Manager.Default)); + j.WriteValue(def); + j.WritePropertyName(nameof(ModCollection.Manager.Interface)); + j.WriteValue(ui); + j.WritePropertyName(nameof(ModCollection.Manager.Current)); + j.WriteValue(current); + foreach (var (type, collection) in special) + { + j.WritePropertyName(type.ToString()); + j.WriteValue(collection); + } + + j.WritePropertyName("Characters"); + j.WriteStartObject(); + foreach (var (character, collection) in characters) + { + j.WritePropertyName(character, true); + j.WriteValue(collection); + } + + j.WriteEndObject(); + j.WriteEndObject(); + Penumbra.Log.Verbose("Active Collections saved."); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); + } + } + + // Collections were introduced and the previous CurrentCollection got put into ModDirectory. + private void Version0To1() + { + if (_config.Version != 0) + return; + + _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; + _config.Version = 1; + ResettleCollectionJson(); + } + + // Move the previous mod configurations to a new default collection file. + private void ResettleCollectionJson() + { + var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); + if (!collectionJson.Exists) + return; + + var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); + var defaultCollectionFile = defaultCollection.FileName; + if (defaultCollectionFile.Exists) + return; + + try + { + var text = File.ReadAllText(collectionJson.FullName); + var data = JArray.Parse(text); + + var maxPriority = 0; + var dict = new Dictionary(); + foreach (var setting in data.Cast()) + { + var modName = (string)setting["FolderName"]!; + var enabled = (bool)setting["Enabled"]!; + var priority = (int)setting["Priority"]!; + var settings = setting["Settings"]!.ToObject>() + ?? setting["Conf"]!.ToObject>(); + + dict[modName] = new ModSettings.SavedSettings() + { + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + maxPriority = Math.Max(maxPriority, priority); + } + + InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; + if (!InvertModListOrder) + dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); + + defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); + defaultCollection.Save(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); + throw; + } + } + + // Create a backup of the configuration file specifically. + private void CreateBackup() + { + var name = _fileNames.ConfigFile; + var bakName = name + ".bak"; + try + { + File.Copy(name, bakName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); + } + } + + public enum SortModeV3 : byte + { + FoldersFirst = 0x00, + Lexicographical = 0x01, + InverseFoldersFirst = 0x02, + InverseLexicographical = 0x03, + FoldersLast = 0x04, + InverseFoldersLast = 0x05, + InternalOrder = 0x06, + InternalOrderInverse = 0x07, + } +} diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 42d52067..a3227b92 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -78,21 +78,22 @@ public class DalamudServices services.AddSingleton(SigScanner); services.AddSingleton(this); } - + + // TODO remove static // @formatter:off - [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs new file mode 100644 index 00000000..27934da8 --- /dev/null +++ b/Penumbra/Services/FilenameService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Plugin; +using OtterGui.Filesystem; + +namespace Penumbra.Services; + +public class FilenameService +{ + public readonly string ConfigDirectory; + public readonly string CollectionDirectory; + public readonly string LocalDataDirectory; + public readonly string ConfigFile; + public readonly string FilesystemFile; + public readonly string ActiveCollectionsFile; + + public FilenameService(DalamudPluginInterface pi) + { + ConfigDirectory = pi.ConfigDirectory.FullName; + CollectionDirectory = Path.Combine(pi.GetPluginConfigDirectory(), "collections"); + LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); + ConfigFile = pi.ConfigFile.FullName; + FilesystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json"); + ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + } + + public string CollectionFile(string collectionName) + => Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json"); + + public string LocalDataFile(string modPath) + => Path.Combine(LocalDataDirectory, $"{modPath}.json"); + + public IEnumerable CollectionFiles + { + get + { + var directory = new DirectoryInfo(CollectionDirectory); + return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + } + } + + public IEnumerable LocalDataFiles + { + get + { + var directory = new DirectoryInfo(LocalDataDirectory); + return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + } + } +} diff --git a/Penumbra/Services/ObjectIdentifier.cs b/Penumbra/Services/ObjectIdentifier.cs deleted file mode 100644 index 7223017b..00000000 --- a/Penumbra/Services/ObjectIdentifier.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Dalamud.Data; -using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Classes; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Util; -using Action = System.Action; - -namespace Penumbra.Services; - -public sealed class ObjectIdentifier : IObjectIdentifier -{ - private const string Prefix = $"[{nameof(ObjectIdentifier)}]"; - - public IObjectIdentifier? Identifier { get; private set; } - - public bool IsDisposed { get; private set; } - - public bool Ready - => Identifier != null && !IsDisposed; - - public event Action? FinishedCreation; - - public ObjectIdentifier(StartTimeTracker tracker, DalamudPluginInterface pi, DataManager data) - { - Task.Run(() => - { - using var timer = tracker.Measure(StartTimeType.Identifier); - var identifier = GameData.GameData.GetIdentifier(pi, data); - if (IsDisposed) - { - identifier.Dispose(); - } - else - { - Identifier = identifier; - Penumbra.Log.Verbose($"{Prefix} Created."); - FinishedCreation?.Invoke(); - } - }); - } - - public void Dispose() - { - Identifier?.Dispose(); - IsDisposed = true; - Penumbra.Log.Verbose($"{Prefix} Disposed."); - } - - public IGamePathParser GamePathParser - => Identifier?.GamePathParser ?? throw new Exception($"{Prefix} Not yet ready."); - - public void Identify(IDictionary set, string path) - => Identifier?.Identify(set, path); - - public Dictionary Identify(string path) - => Identifier?.Identify(path) ?? new Dictionary(); - - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) - => Identifier?.Identify(setId, weaponType, variant, slot) ?? Array.Empty(); -} diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs new file mode 100644 index 00000000..403626b6 --- /dev/null +++ b/Penumbra/Services/ServiceWrapper.cs @@ -0,0 +1,117 @@ +using System; +using System.Threading.Tasks; +using OtterGui.Classes; +using Penumbra.Util; + +namespace Penumbra.Services; + +public interface IServiceWrapper : IDisposable +{ + public string Name { get; } + public T? Service { get; } + public bool Valid { get; } +} + +public abstract class SyncServiceWrapper : IServiceWrapper +{ + public string Name { get; } + public T Service { get; } + private bool _isDisposed; + + public bool Valid + => !_isDisposed; + + protected SyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) + { + Name = name; + using var timer = tracker.Measure(type); + Service = factory(); + Penumbra.Log.Verbose($"[{Name}] Created."); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + if (Service is IDisposable d) + d.Dispose(); + Penumbra.Log.Verbose($"[{Name}] Disposed."); + } +} + +public abstract class AsyncServiceWrapper : IServiceWrapper +{ + public string Name { get; } + public T? Service { get; private set; } + + public T AwaitedService + { + get + { + _task.Wait(); + return Service!; + } + } + + public bool Valid + => Service != null && !_isDisposed; + + public event Action? FinishedCreation; + private readonly Task _task; + + private bool _isDisposed; + + protected AsyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) + { + Name = name; + _task = Task.Run(() => + { + using var timer = tracker.Measure(type); + var service = factory(); + if (_isDisposed) + { + if (service is IDisposable d) + d.Dispose(); + } + else + { + Service = service; + Penumbra.Log.Verbose($"[{Name}] Created."); + FinishedCreation?.Invoke(); + } + }); + } + + protected AsyncServiceWrapper(string name, Func factory) + { + Name = name; + _task = Task.Run(() => + { + var service = factory(); + if (_isDisposed) + { + if (service is IDisposable d) + d.Dispose(); + } + else + { + Service = service; + Penumbra.Log.Verbose($"[{Name}] Created."); + FinishedCreation?.Invoke(); + } + }); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + if (Service is IDisposable d) + d.Dispose(); + Penumbra.Log.Verbose($"[{Name}] Disposed."); + } +} diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs new file mode 100644 index 00000000..cf493716 --- /dev/null +++ b/Penumbra/Services/StainService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Dalamud.Plugin; +using OtterGui.Classes; +using OtterGui.Widgets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.Util; + +namespace Penumbra.Services; + +public class StainService : IDisposable +{ + public sealed class StainTemplateCombo : FilterComboCache + { + public StainTemplateCombo(IEnumerable items) + : base(items) + { } + } + + public readonly StainData StainData; + public readonly FilterComboColors StainCombo; + public readonly StmFile StmFile; + public readonly StainTemplateCombo TemplateCombo; + + public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, DataManager dataManager) + { + using var t = timer.Measure(StartTimeType.Stains); + StainData = new StainData(pluginInterface, dataManager, dataManager.Language); + StainCombo = new FilterComboColors(140, StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + StmFile = new StmFile(dataManager); + TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); + Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); + } + + public void Dispose() + { + StainData.Dispose(); + Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed."); + } +} \ No newline at end of file diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs new file mode 100644 index 00000000..90a3cdd3 --- /dev/null +++ b/Penumbra/Services/Wrappers.cs @@ -0,0 +1,37 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using OtterGui.Classes; +using Penumbra.GameData; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; +using Penumbra.Interop.Resolver; +using Penumbra.Util; + +namespace Penumbra.Services; + +public sealed class IdentifierService : AsyncServiceWrapper +{ + public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data) + : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data)) + { } +} + +public sealed class ItemService : AsyncServiceWrapper +{ + public ItemService(StartTracker tracker, DalamudPluginInterface pi, DataManager gameData) + : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language)) + { } +} + +public sealed class ActorService : AsyncServiceWrapper +{ + public ActorService(StartTracker tracker, DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, + Framework framework, DataManager gameData, GameGui gui, CutsceneCharacters cutscene) + : base(nameof(ActorService), tracker, StartTimeType.Actors, + () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) + { } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 6c7ae4c6..c79f46d9 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -17,6 +17,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; +using Penumbra.Services; using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -42,49 +43,61 @@ public class ItemSwapWindow : IDisposable Weapon, } - private class ItemSelector : FilterComboCache< (string, Item) > + private class ItemSelector : FilterComboCache<(string, Item)> { - public ItemSelector( FullEquipType type ) - : base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() ) + public ItemSelector(FullEquipType type) + : base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) { } - protected override string ToString( (string, Item) obj ) + protected override string ToString((string, Item) obj) => obj.Item1; } - private class WeaponSelector : FilterComboCache< FullEquipType > + private class WeaponSelector : FilterComboCache { public WeaponSelector() - : base( FullEquipTypeExtensions.WeaponTypes.Concat( FullEquipTypeExtensions.ToolTypes ) ) + : base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes)) { } - protected override string ToString( FullEquipType type ) + protected override string ToString(FullEquipType type) => type.ToName(); } - public ItemSwapWindow() + private readonly CommunicatorService _communicator; + + public ItemSwapWindow(CommunicatorService communicator) { - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator = communicator; + _communicator.CollectionChange.Event += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; } public void Dispose() { - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator.CollectionChange.Event -= OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; } - private readonly Dictionary< SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo) > _selectors = new() + private readonly Dictionary _selectors = new() { - [ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), - [ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), - [ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), - [ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), - [ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), - [ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), - [ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), - [ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), - [ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = + (new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"), + [SwapType.Top] = + (new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"), + [SwapType.Gloves] = + (new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"), + [SwapType.Pants] = + (new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"), + [SwapType.Shoes] = + (new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"), + [SwapType.Earrings] = + (new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"), + [SwapType.Necklace] = + (new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"), + [SwapType.Bracelet] = + (new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"), + [SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring", + "and put it on this one"), }; private ItemSelector? _weaponSource = null; @@ -117,39 +130,33 @@ public class ItemSwapWindow : IDisposable private Item[]? _affectedItems; - public void UpdateMod( Mod mod, ModSettings? settings ) + public void UpdateMod(Mod mod, ModSettings? settings) { - if( mod == _mod && settings == _modSettings ) - { + if (mod == _mod && settings == _modSettings) return; - } var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; - if( _newModName.Length == 0 || oldDefaultName == _newModName ) - { + if (_newModName.Length == 0 || oldDefaultName == _newModName) _newModName = $"{mod.Name.Text} (Swapped)"; - } _mod = mod; _modSettings = settings; - _swapData.LoadMod( _mod, _modSettings ); + _swapData.LoadMod(_mod, _modSettings); UpdateOption(); _dirty = true; } private void UpdateState() { - if( !_dirty ) - { + if (!_dirty) return; - } _swapData.Clear(); _loadException = null; _affectedItems = null; try { - switch( _lastTab ) + switch (_lastTab) { case SwapType.Hat: case SwapType.Top: @@ -178,27 +185,31 @@ public class ItemSwapWindow : IDisposable } break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null ); + _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + (SetId)_targetId, + _useCurrentCollection ? Penumbra.CollectionManager.Current : null); break; case SwapType.Weapon: break; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" ); + Penumbra.Log.Error($"Could not get Customization Data container for {_lastTab}:\n{e}"); _loadException = e; _affectedItems = null; _swapData.Clear(); @@ -207,13 +218,14 @@ public class ItemSwapWindow : IDisposable _dirty = false; } - private static string SwapToString( Swap swap ) + private static string SwapToString(Swap swap) { return swap switch { MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", - FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}", - _ => string.Empty, + FileSwap file => + $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}", + _ => string.Empty, }; } @@ -222,28 +234,28 @@ public class ItemSwapWindow : IDisposable private void UpdateOption() { - _selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName ); - _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); + _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName); + _subModValid = _mod != null + && _newGroupName.Length > 0 + && _newOptionName.Length > 0 + && (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); } private void CreateMod() { - var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); - Mod.Creator.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); - if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) - { - Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); - } + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); + if (!_swapData.WriteMod(Penumbra.ModManager.Last(), + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) + Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1); } private void CreateOption() { - if( _mod == null || !_subModValid ) - { + if (_mod == null || !_subModValid) return; - } var groupCreated = false; var dirCreated = false; @@ -251,52 +263,47 @@ public class ItemSwapWindow : IDisposable DirectoryInfo? optionFolderName = null; try { - optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); - if( optionFolderName?.Exists == true ) - { - throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); - } + optionFolderName = + Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), + _newOptionName); + if (optionFolderName?.Exists == true) + throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); - if( optionFolderName != null ) + if (optionFolderName != null) { - if( _selectedGroup == null ) + if (_selectedGroup == null) { - Penumbra.ModManager.AddModGroup( _mod, GroupType.Multi, _newGroupName ); + Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); _selectedGroup = _mod.Groups.Last(); groupCreated = true; } - Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName ); + Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); optionCreated = true; - optionFolderName = Directory.CreateDirectory( optionFolderName.FullName ); + optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, - _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) ) - { - throw new Exception( "Failure writing files for mod swap." ); - } + if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, + optionFolderName, + _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) + throw new Exception("Failure writing files for mod swap."); } } - catch( Exception e ) + catch (Exception e) { - ChatUtil.NotificationMessage( $"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error ); + ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); try { - if( optionCreated && _selectedGroup != null ) - { - Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ); - } + if (optionCreated && _selectedGroup != null) + Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); - if( groupCreated ) + if (groupCreated) { - Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) ); + Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _selectedGroup = null; } - if( dirCreated && optionFolderName != null ) - { - Directory.Delete( optionFolderName.FullName, true ); - } + if (dirCreated && optionFolderName != null) + Directory.Delete(optionFolderName.FullName, true); } catch { @@ -307,12 +314,12 @@ public class ItemSwapWindow : IDisposable UpdateOption(); } - private void DrawHeaderLine( float width ) + private void DrawHeaderLine(float width) { var newModAvailable = _loadException == null && _swapData.Loaded; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) ) + ImGui.SetNextItemWidth(width); + if (ImGui.InputTextWithHint("##newModName", "New Mod Name...", ref _newModName, 64)) { } ImGui.SameLine(); @@ -321,29 +328,23 @@ public class ItemSwapWindow : IDisposable : _newModName.Length == 0 ? "Please enter a name for your mod." : "Create a new mod of the given name containing only the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) - { + if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0)) CreateMod(); - } ImGui.SameLine(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); - ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); - ImGuiUtil.HoverTooltip( "Instead of writing every single non-default file to the newly created mod or option,\n" - + "even those available from game files, use File Swaps to default game files where possible." ); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.Checkbox("Use File Swaps", ref _useFileSwaps); + ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n" + + "even those available from game files, use File Swaps to default game files where possible."); - ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); - if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) - { + ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); + if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32)) UpdateOption(); - } ImGui.SameLine(); - ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); - if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) - { + ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); + if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32)) UpdateOption(); - } ImGui.SameLine(); tt = !_subModValid @@ -351,16 +352,15 @@ public class ItemSwapWindow : IDisposable : !newModAvailable ? "Create a new option inside this mod containing only the swap." : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Option", new Vector2( width / 2, 0 ), tt, !newModAvailable || !_subModValid ) ) - { + if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid)) CreateOption(); - } ImGui.SameLine(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); - _dirty |= ImGui.Checkbox( "Use Entire Collection", ref _useCurrentCollection ); - ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" - + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance." ); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection); + ImGuiUtil.HoverTooltip( + "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" + + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance."); } private void DrawSwapBar() @@ -491,61 +491,58 @@ public class ItemSwapWindow : IDisposable return (article1, article2, source ? tuple.Source : tuple.Target); } - private void DrawEquipmentSwap( SwapType type ) + private void DrawEquipmentSwap(SwapType type) { - using var tab = DrawTab( type ); - if( !tab ) - { + using var tab = DrawTab(type); + if (!tab) return; - } - var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ]; - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + var (sourceSelector, targetSelector, text1, text2) = _selectors[type]; + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text1 ); + ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); - if( type == SwapType.Ring ) + if (type == SwapType.Ring) { ImGui.SameLine(); - _dirty |= ImGui.Checkbox( "Swap Right Ring", ref _useRightRing ); + _dirty |= ImGui.Checkbox("Swap Right Ring", ref _useRightRing); } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text2 ); + ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - if( type == SwapType.Ring ) + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); + if (type == SwapType.Ring) { ImGui.SameLine(); - _dirty |= ImGui.Checkbox( "Swap Left Ring", ref _useLeftRing ); + _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if( _affectedItems is { Length: > 1 } ) + if (_affectedItems is { Length: > 1 }) { ImGui.SameLine(); - ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) ) - .Select( i => i.Name.ToDalamudString().TextValue ) ) ); - } + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + Colors.PressEnterWarningBg); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) + .Select(i => i.Name.ToDalamudString().TextValue))); } } private void DrawHairSwap() { - using var tab = DrawTab( SwapType.Hair ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Hair); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Hairstyle" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Hairstyle"); DrawSourceIdInput(); DrawGenderInput(); } @@ -553,145 +550,139 @@ public class ItemSwapWindow : IDisposable private void DrawFaceSwap() { using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Face ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Face); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Face Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Face Type"); DrawSourceIdInput(); DrawGenderInput(); } private void DrawTailSwap() { - using var tab = DrawTab( SwapType.Tail ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Tail); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Tail Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Tail Type"); DrawSourceIdInput(); - DrawGenderInput( "for all", 2 ); + DrawGenderInput("for all", 2); } private void DrawEarSwap() { - using var tab = DrawTab( SwapType.Ears ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Ears); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - DrawTargetIdInput( "Take this Ear Type" ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); + DrawTargetIdInput("Take this Ear Type"); DrawSourceIdInput(); - DrawGenderInput( "for all Viera", 0 ); + DrawGenderInput("for all Viera", 0); } private void DrawWeaponSwap() { using var disabled = ImRaii.Disabled(); - using var tab = DrawTab( SwapType.Weapon ); - if( !tab ) - { + using var tab = DrawTab(SwapType.Weapon); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Select the weapon or tool you want" ); + ImGui.TextUnformatted("Select the weapon or tool you want"); ImGui.TableNextColumn(); - if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing())) { _dirty = true; - _weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); - _weaponTarget = new ItemSelector( _slotSelector.CurrentSelection ); + _weaponSource = new ItemSelector(_slotSelector.CurrentSelection); + _weaponTarget = new ItemSelector(_slotSelector.CurrentSelection); } else { _dirty = _weaponSource == null || _weaponTarget == null; - _weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection ); - _weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection ); + _weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection); + _weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection); } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "and put this variant of it" ); + ImGui.TextUnformatted("and put this variant of it"); ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "onto this one" ); + ImGui.TextUnformatted("onto this one"); ImGui.TableNextColumn(); - _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); } private const float InputWidth = 120; - private void DrawTargetIdInput( string text = "Take this ID" ) + private void DrawTargetIdInput(string text = "Take this ID") { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) ) - { - _targetId = Math.Clamp( _targetId, 0, byte.MaxValue ); - } + ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##targetId", ref _targetId, 0, 0)) + _targetId = Math.Clamp(_targetId, 0, byte.MaxValue); _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } - private void DrawSourceIdInput( string text = "and put it on this one" ) + private void DrawSourceIdInput(string text = "and put it on this one") { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) ) - { - _sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue ); - } + ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0)) + _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue); _dirty |= ImGui.IsItemDeactivatedAfterEdit(); } - private void DrawGenderInput( string text = "for all", int drawRace = 1 ) + private void DrawGenderInput(string text = "for all", int drawRace = 1) { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - _dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender ); - if( drawRace == 1 ) + _dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender); + if (drawRace == 1) { ImGui.SameLine(); - _dirty |= Combos.Race( "##Race", InputWidth, _currentRace, out _currentRace ); + _dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace); } - else if( drawRace == 2 ) + else if (drawRace == 2) { ImGui.SameLine(); - if( _currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar ) - { + if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar) _currentRace = ModelRace.Miqote; - } - _dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar }, - RaceEnumExtensions.ToName ); + _dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[] + { + ModelRace.Miqote, + ModelRace.AuRa, + ModelRace.Hrothgar, + }, + RaceEnumExtensions.ToName); } } @@ -718,72 +709,54 @@ public class ItemSwapWindow : IDisposable public void DrawItemSwapPanel() { - using var tab = ImRaii.TabItem( "Item Swap (WIP)" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Item Swap (WIP)"); + if (!tab) return; - } ImGui.NewLine(); - DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale ); + DrawHeaderLine(300 * ImGuiHelpers.GlobalScale); ImGui.NewLine(); DrawSwapBar(); - using var table = ImRaii.ListBox( "##swaps", -Vector2.One ); - if( _loadException != null ) - { - ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" ); - } - else if( _swapData.Loaded ) - { - foreach( var swap in _swapData.Swaps ) - { - DrawSwap( swap ); - } - } + using var table = ImRaii.ListBox("##swaps", -Vector2.One); + if (_loadException != null) + ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}"); + else if (_swapData.Loaded) + foreach (var swap in _swapData.Swaps) + DrawSwap(swap); else - { - ImGui.TextUnformatted( NonExistentText() ); - } + ImGui.TextUnformatted(NonExistentText()); } - private static void DrawSwap( Swap swap ) + private static void DrawSwap(Swap swap) { var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; - using var tree = ImRaii.TreeNode( SwapToString( swap ), flags ); - if( !tree ) - { + using var tree = ImRaii.TreeNode(SwapToString(swap), flags); + if (!tree) return; - } - foreach( var child in swap.ChildSwaps ) - { - DrawSwap( child ); - } + foreach (var child in swap.ChildSwaps) + DrawSwap(child); } - private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, - ModCollection? newCollection, string _ ) + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, + ModCollection? newCollection, string _) { - if( collectionType != CollectionType.Current || _mod == null || newCollection == null ) - { + if (collectionType != CollectionType.Current || _mod == null || newCollection == null) return; - } - UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null); newCollection.ModSettingChanged += OnSettingChange; - if( oldCollection != null ) - { + if (oldCollection != null) oldCollection.ModSettingChanged -= OnSettingChange; - } } - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) { - if( modIdx == _mod?.Index ) + if (modIdx == _mod?.Index) { - _swapData.LoadMod( _mod, _modSettings ); + _swapData.LoadMod(_mod, _modSettings); _dirty = true; } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs index d5cd5a44..bdfae0b8 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -97,7 +97,7 @@ public partial class ModEditWindow private static bool DrawPreviewDye( MtrlFile file, bool disabled ) { - var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { @@ -106,7 +106,7 @@ public partial class ModEditWindow { for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId ); } } @@ -115,7 +115,7 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); + Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true ); return false; } @@ -355,10 +355,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( hasDye ) { - if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection; ret = true; } @@ -378,8 +378,8 @@ public partial class ModEditWindow private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) { - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) { return false; } @@ -390,7 +390,7 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Apply the selected dye to this row.", disabled, true ); - ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain ); ImGui.SameLine(); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 3fe48126..b1eea353 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -22,7 +23,7 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - internal readonly ItemSwapWindow _swapWindow = new(); + internal readonly ItemSwapWindow _swapWindow; private Editor? _editor; private Mod? _mod; @@ -567,9 +568,10 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath( path ); } - public ModEditWindow() + public ModEditWindow(CommunicatorService communicator) : base( WindowBaseLabel ) - { + { + _swapWindow = new ItemSwapWindow( communicator ); _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index f1b9dbe3..c9ba7b13 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,41 +14,43 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.UI.Classes; -public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > +public sealed partial class ModFileSystemSelector : FileSystemSelector { - private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); - private TexToolsImporter? _import; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + private readonly CommunicatorService _communicator; + private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); + private TexToolsImporter? _import; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystem fileSystem ) - : base( fileSystem, DalamudServices.KeyState ) + public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem) + : base(fileSystem, DalamudServices.KeyState) { - SubscribeRightClickFolder( EnableDescendants, 10 ); - SubscribeRightClickFolder( DisableDescendants, 10 ); - SubscribeRightClickFolder( InheritDescendants, 15 ); - SubscribeRightClickFolder( OwnDescendants, 15 ); - SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); - SubscribeRightClickLeaf( ToggleLeafFavorite, 0 ); - SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); - AddButton( AddNewModButton, 0 ); - AddButton( AddImportModButton, 1 ); - AddButton( AddHelpButton, 2 ); - AddButton( DeleteModButton, 1000 ); + _communicator = communicator; + SubscribeRightClickFolder(EnableDescendants, 10); + SubscribeRightClickFolder(DisableDescendants, 10); + SubscribeRightClickFolder(InheritDescendants, 15); + SubscribeRightClickFolder(OwnDescendants, 15); + SubscribeRightClickFolder(SetDefaultImportFolder, 100); + SubscribeRightClickLeaf(ToggleLeafFavorite, 0); + SubscribeRightClickMain(ClearDefaultImportFolder, 100); + AddButton(AddNewModButton, 0); + AddButton(AddImportModButton, 1); + AddButton(AddHelpButton, 2); + AddButton(DeleteModButton, 1000); SetFilterTooltip(); SelectionChanged += OnSelectionChange; - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + _communicator.CollectionChange.Event += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" ); + OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, ""); } public override void Dispose() @@ -59,7 +61,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; - Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; + _communicator.CollectionChange.Event -= OnCollectionChange; _import?.Dispose(); _import = null; } @@ -68,7 +70,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod => base.SelectedLeaf; // Customization points. - public override ISortMode< Mod > SortMode + public override ISortMode SortMode => Penumbra.Config.SortMode; protected override uint ExpandedFolderColor @@ -89,91 +91,79 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod DrawHelpPopup(); DrawInfoPopup(); - if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) - { + if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) try { - var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); - Mod.Creator.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); _newModName = string.Empty; } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); + Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); } - } - while( _modsToAdd.TryDequeue( out var dir ) ) + while (_modsToAdd.TryDequeue(out var dir)) { - Penumbra.ModManager.AddMod( dir ); + Penumbra.ModManager.AddMod(dir); var mod = Penumbra.ModManager.LastOrDefault(); - if( mod != null ) + if (mod != null) { - MoveModToDefaultDirectory( mod ); - SelectByValue( mod ); + MoveModToDefaultDirectory(mod); + SelectByValue(mod); } } } - protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ) - .Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite ); - using var id = ImRaii.PushId( leaf.Value.Index ); - ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) + .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); + using var id = ImRaii.PushId(leaf.Value.Index); + ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); } // Add custom context menu items. - private static void EnableDescendants( ModFileSystem.Folder folder ) + private static void EnableDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Enable Descendants" ) ) - { - SetDescendants( folder, true ); - } + if (ImGui.MenuItem("Enable Descendants")) + SetDescendants(folder, true); } - private static void DisableDescendants( ModFileSystem.Folder folder ) + private static void DisableDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Disable Descendants" ) ) - { - SetDescendants( folder, false ); - } + if (ImGui.MenuItem("Disable Descendants")) + SetDescendants(folder, false); } - private static void InheritDescendants( ModFileSystem.Folder folder ) + private static void InheritDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Inherit Descendants" ) ) - { - SetDescendants( folder, true, true ); - } + if (ImGui.MenuItem("Inherit Descendants")) + SetDescendants(folder, true, true); } - private static void OwnDescendants( ModFileSystem.Folder folder ) + private static void OwnDescendants(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) - { - SetDescendants( folder, false, true ); - } + if (ImGui.MenuItem("Stop Inheriting Descendants")) + SetDescendants(folder, false, true); } - private static void ToggleLeafFavorite( FileSystem< Mod >.Leaf mod ) + private static void ToggleLeafFavorite(FileSystem.Leaf mod) { - if( ImGui.MenuItem( mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite" ) ) - { - Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite ); - } + if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); } - private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) + private static void SetDefaultImportFolder(ModFileSystem.Folder folder) { - if( ImGui.MenuItem( "Set As Default Import Folder" ) ) + if (ImGui.MenuItem("Set As Default Import Folder")) { var newName = folder.FullName(); - if( newName != Penumbra.Config.DefaultImportFolder ) + if (newName != Penumbra.Config.DefaultImportFolder) { Penumbra.Config.DefaultImportFolder = newName; Penumbra.Config.Save(); @@ -183,7 +173,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private static void ClearDefaultImportFolder() { - if( ImGui.MenuItem( "Clear Default Import Folder" ) && Penumbra.Config.DefaultImportFolder.Length > 0 ) + if (ImGui.MenuItem("Clear Default Import Folder") && Penumbra.Config.DefaultImportFolder.Length > 0) { Penumbra.Config.DefaultImportFolder = string.Empty; Penumbra.Config.Save(); @@ -194,71 +184,63 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod // Add custom buttons. private string _newModName = string.Empty; - private static void AddNewModButton( Vector2 size ) + private static void AddNewModButton(Vector2 size) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", - !Penumbra.ModManager.Valid, true ) ) - { - ImGui.OpenPopup( "Create New Mod" ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", + !Penumbra.ModManager.Valid, true)) + ImGui.OpenPopup("Create New Mod"); } // Add an import mods button that opens a file selector. // Only set the initial directory once. private bool _hasSetFolder; - private void AddImportModButton( Vector2 size ) + private void AddImportModButton(Vector2 size) { - var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true ); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); - if( !button ) - { + var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); + ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport); + if (!button) return; - } var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; + : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", - "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", ( s, f ) => + _fileManager.OpenFileDialog("Import Mod Pack", + "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => { - if( s ) + if (s) { - _import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ), - AddNewMod ); - ImGui.OpenPopup( "Import Status" ); + _import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), + AddNewMod); + ImGui.OpenPopup("Import Status"); } - }, 0, modPath ); + }, 0, modPath); } // Draw the progress information for import. private void DrawInfoPopup() { var display = ImGui.GetIO().DisplaySize; - var height = Math.Max( display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing() ); + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); var width = display.X / 8; - var size = new Vector2( width * 2, height ); - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); - ImGui.SetNextWindowSize( size ); - using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); - if( _import == null || !popup.Success ) - { + var size = new Vector2(width * 2, height); + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); + if (_import == null || !popup.Success) return; - } - using( var child = ImRaii.Child( "##import", new Vector2( -1, size.Y - ImGui.GetFrameHeight() * 2 ) ) ) + using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { - if( child ) - { - _import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) ); - } + if (child) + _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); } - if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX ) - || _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) ) + if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) + || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) { _import?.Dispose(); _import = null; @@ -267,100 +249,84 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } // Mods need to be added thread-safely outside of iteration. - private readonly ConcurrentQueue< DirectoryInfo > _modsToAdd = new(); + private readonly ConcurrentQueue _modsToAdd = new(); // Clean up invalid directory if necessary. // Add successfully extracted mods. - private void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) + private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) { - if( error != null ) + if (error != null) { - if( dir != null && Directory.Exists( dir.FullName ) ) - { + if (dir != null && Directory.Exists(dir.FullName)) try { - Directory.Delete( dir.FullName, true ); + Directory.Delete(dir.FullName, true); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); + Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); } - } - if( error is not OperationCanceledException ) - { - Penumbra.Log.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); - } + if (error is not OperationCanceledException) + Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); } - else if( dir != null ) + else if (dir != null) { - _modsToAdd.Enqueue( dir ); + _modsToAdd.Enqueue(dir); } } - private void DeleteModButton( Vector2 size ) + private void DeleteModButton(Vector2 size) { var keys = Penumbra.Config.DeleteModModifier.IsActive(); var tt = SelectedLeaf == null ? "No mod selected." : "Delete the currently selected mod entirely from your drive.\n" + "This can not be undone."; - if( !keys ) - { + if (!keys) tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) - && Selected != null ) - { - Penumbra.ModManager.DeleteMod( Selected.Index ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) + && Selected != null) + Penumbra.ModManager.DeleteMod(Selected.Index); } - private static void AddHelpButton( Vector2 size ) + private static void AddHelpButton(Vector2 size) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true ) ) - { - ImGui.OpenPopup( "ExtendedHelp" ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) + ImGui.OpenPopup("ExtendedHelp"); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp ); + ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp); } // Helpers. - private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false ) + private static void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) { - var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => + var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => { // Any mod handled here should not stay new. - Penumbra.ModManager.NewMods.Remove( l.Value ); + Penumbra.ModManager.NewMods.Remove(l.Value); return l.Value; - } ); + }); - if( inherit ) - { - Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled ); - } + if (inherit) + Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled); else - { - Penumbra.CollectionManager.Current.SetMultipleModStates( mods, enabled ); - } + Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled); } // Automatic cache update functions. - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) { // TODO: maybe make more efficient SetFilterDirty(); - if( modIdx == Selected?.Index ) - { - OnSelectionChange( Selected, Selected, default ); - } + if (modIdx == Selected?.Index) + OnSelectionChange(Selected, Selected, default); } - private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName ) + private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) { - switch( type ) + switch (type) { case ModDataChangeType.Name: case ModDataChangeType.Author: @@ -372,46 +338,44 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } - private void OnInheritanceChange( bool _ ) + private void OnInheritanceChange(bool _) { SetFilterDirty(); - OnSelectionChange( Selected, Selected, default ); + OnSelectionChange(Selected, Selected, default); } - private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _ ) + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) { - if( collectionType != CollectionType.Current || oldCollection == newCollection ) - { + if (collectionType != CollectionType.Current || oldCollection == newCollection) return; - } - if( oldCollection != null ) + if (oldCollection != null) { oldCollection.ModSettingChanged -= OnSettingChange; oldCollection.InheritanceChanged -= OnInheritanceChange; } - if( newCollection != null ) + if (newCollection != null) { newCollection.ModSettingChanged += OnSettingChange; newCollection.InheritanceChanged += OnInheritanceChange; } SetFilterDirty(); - OnSelectionChange( Selected, Selected, default ); + OnSelectionChange(Selected, Selected, default); } - private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 ) + private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) { - if( newSelection == null ) + if (newSelection == null) { SelectedSettings = ModSettings.Empty; SelectedSettingCollection = ModCollection.Empty; } else { - ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ]; - SelectedSettings = settings ?? ModSettings.Empty; + (var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index]; + SelectedSettings = settings ?? ModSettings.Empty; } } @@ -426,92 +390,89 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void RestoreLastSelection() { - if( _lastSelectedDirectory.Length > 0 ) + if (_lastSelectedDirectory.Length > 0) { - var leaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) - .FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); - Select( leaf ); + var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) + .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); + Select(leaf); _lastSelectedDirectory = string.Empty; } } // If a default import folder is setup, try to move the given mod in there. // If the folder does not exist, create it if possible. - private void MoveModToDefaultDirectory( Mod mod ) + private void MoveModToDefaultDirectory(Mod mod) { - if( Penumbra.Config.DefaultImportFolder.Length == 0 ) - { + if (Penumbra.Config.DefaultImportFolder.Length == 0) return; - } try { - var leaf = FileSystem.Root.GetChildren( ISortMode< Mod >.Lexicographical ) - .FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod ); - if( leaf == null ) - { - throw new Exception( "Mod was not found at root." ); - } + var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) + .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); + if (leaf == null) + throw new Exception("Mod was not found at root."); - var folder = FileSystem.FindOrCreateAllFolders( Penumbra.Config.DefaultImportFolder ); - FileSystem.Move( leaf, folder ); + var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); + FileSystem.Move(leaf, folder); } - catch( Exception e ) + catch (Exception e) { Penumbra.Log.Warning( - $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}" ); + $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}"); } } private static void DrawHelpPopup() { - ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing() ), () => + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => { - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.TextUnformatted( "Mod Management" ); - ImGui.BulletText( "You can create empty mods or import mods with the buttons in this row." ); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Management"); + ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); using var indent = ImRaii.PushIndent(); - ImGui.BulletText( "Supported formats for import are: .ttmp, .ttmp2, .pmp." ); - ImGui.BulletText( "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata." ); - indent.Pop( 1 ); - ImGui.BulletText( "You can also create empty mod folders and delete mods." ); - ImGui.BulletText( "For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.TextUnformatted( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information or change settings." ); - ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); - indent.Push(); - ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." ); - ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections." ); - ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(), - "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." ); - ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority." ); - ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." ); - ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" ); - indent.Pop( 1 ); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." ); - indent.Push(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); - indent.Pop( 1 ); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); - ImGui.BulletText( "Right-clicking a folder opens a context menu." ); - ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + indent.Pop(1); + ImGui.BulletText("You can also create empty mod folders and delete mods."); + ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Selector"); + ImGui.BulletText("Select a mod to obtain more information or change settings."); + ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); indent.Push(); - ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - indent.Pop( 1 ); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - } ); + ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); + ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); + ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); + ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), + "enabled and conflicting with another enabled Mod on the same priority."); + ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); + ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); + indent.Pop(1); + ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + indent.Push(); + ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + indent.Pop(1); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImGui.BulletText("Right-clicking a folder opens a context menu."); + ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); + ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + indent.Push(); + ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); + ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); + ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + indent.Pop(1); + ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + }); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 42417134..bac726a2 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Services; namespace Penumbra.UI; @@ -16,20 +17,22 @@ public partial class ConfigWindow // Encapsulate for less pollution. private partial class CollectionsTab : IDisposable, ITab { - private readonly ConfigWindow _window; + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _window; - public CollectionsTab( ConfigWindow window ) + public CollectionsTab( CommunicatorService communicator, ConfigWindow window ) { - _window = window; + _window = window; + _communicator = communicator; - Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; + _communicator.CollectionChange.Event += UpdateIdentifiers; } public ReadOnlySpan Label => "Collections"u8; public void Dispose() - => Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; + => _communicator.CollectionChange.Event -= UpdateIdentifiers; public void DrawHeader() => OpenTutorial( BasicTutorialSteps.Collections ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index b59ce96a..e5b0fe84 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; @@ -28,10 +29,14 @@ public partial class ConfigWindow { private class DebugTab : ITab { + private readonly StartTracker _timer; private readonly ConfigWindow _window; - public DebugTab( ConfigWindow window ) - => _window = window; + public DebugTab( ConfigWindow window, StartTracker timer) + { + _window = window; + _timer = timer; + } public ReadOnlySpan Label => "Debug"u8; @@ -109,7 +114,7 @@ public partial class ConfigWindow PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); } - private static void DrawPerformanceTab() + private void DrawPerformanceTab() { ImGui.NewLine(); if( ImGui.CollapsingHeader( "Performance" ) ) @@ -121,7 +126,7 @@ public partial class ConfigWindow { if( start ) { - Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); + _timer.Draw( "##startTimer", TimingExtensions.ToName ); ImGui.NewLine(); } } @@ -397,7 +402,7 @@ public partial class ConfigWindow return; } - foreach( var (key, data) in Penumbra.StainManager.StmFile.Entries ) + foreach( var (key, data) in Penumbra.StainService.StmFile.Entries ) { using var tree = TreeNode( $"Template {key}" ); if( !tree ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 4bc6454e..8a539b4f 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -105,7 +105,7 @@ public partial class ConfigWindow private static void DrawWaitForPluginsReflection() { - if( !DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) + if( !Penumbra.Dalamud.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) { using var disabled = ImRaii.Disabled(); Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); @@ -113,7 +113,7 @@ public partial class ConfigWindow else { Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => DalamudServices.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); + v => Penumbra.Dalamud.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); } } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 8300e83e..e3037d68 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -299,11 +299,11 @@ public partial class ConfigWindow private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; - public static void DrawSupportButton() + public static void DrawSupportButton(Penumbra penumbra) { if( ImGui.Button( SupportInfoButtonText ) ) { - var text = Penumbra.GatherSupportInformation(); + var text = penumbra.GatherSupportInformation(); ImGui.SetClipboardText( text ); } } @@ -345,7 +345,7 @@ public partial class ConfigWindow } ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); - DrawSupportButton(); + DrawSupportButton(_window._penumbra); ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); DrawDiscordButton( width ); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 7342037c..c925ce16 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -8,7 +8,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; @@ -19,7 +19,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly Penumbra _penumbra; private readonly ModFileSystemSelector _selector; private readonly ModPanel _modPanel; - public readonly ModEditWindow ModEditPopup = new(); + public readonly ModEditWindow ModEditPopup; private readonly SettingsTab _settingsTab; private readonly CollectionsTab _collectionsTab; @@ -31,43 +31,43 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly ResourceWatcher _resourceWatcher; public TabType SelectTab = TabType.None; - public void SelectMod( Mod mod ) - => _selector.SelectByValue( mod ); - public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) - : base( GetLabel() ) + public void SelectMod(Mod mod) + => _selector.SelectByValue(mod); + + public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher) + : base(GetLabel()) { _penumbra = penumbra; _resourceWatcher = watcher; - _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); - _modPanel = new ModPanel( this ); - _modsTab = new ModsTab( _selector, _modPanel, _penumbra ); + ModEditPopup = new ModEditWindow(communicator); + _settingsTab = new SettingsTab(this); + _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); + _modPanel = new ModPanel(this); + _modsTab = new ModsTab(_selector, _modPanel, _penumbra); _selector.SelectionChanged += _modPanel.OnSelectionChange; - _collectionsTab = new CollectionsTab( this ); - _changedItemsTab = new ChangedItemsTab( this ); + _collectionsTab = new CollectionsTab(communicator, this); + _changedItemsTab = new ChangedItemsTab(this); _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab( this ); + _debugTab = new DebugTab(this, timer); _resourceTab = new ResourceTab(); - if( Penumbra.Config.FixMainWindow ) - { + if (Penumbra.Config.FixMainWindow) Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; - } DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2( 800, 600 ), - MaximumSize = new Vector2( 4096, 2160 ), + MinimumSize = new Vector2(800, 600), + MaximumSize = new Vector2(4096, 2160), }; UpdateTutorialStep(); } - private ReadOnlySpan< byte > ToLabel( TabType type ) + private ReadOnlySpan ToLabel(TabType type) => type switch { TabType.Settings => _settingsTab.Label, @@ -78,85 +78,85 @@ public sealed partial class ConfigWindow : Window, IDisposable TabType.ResourceWatcher => _resourceWatcher.Label, TabType.Debug => _debugTab.Label, TabType.ResourceManager => _resourceTab.Label, - _ => ReadOnlySpan< byte >.Empty, + _ => ReadOnlySpan.Empty, }; public override void Draw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow); try { - if( Penumbra.ValidityChecker.ImcExceptions.Count > 0 ) + if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) { - DrawProblemWindow( $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow(_penumbra, + $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" - + "Please use the Launcher's Repair Game Files function to repair your client installation.", true ); + + "Please use the Launcher's Repair Game Files function to repair your client installation.", true); } - else if( !Penumbra.ValidityChecker.IsValidSourceRepo ) + else if (!Penumbra.ValidityChecker.IsValidSourceRepo) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); } - else if( Penumbra.ValidityChecker.IsNotInstalledPenumbra ) + else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); } - else if( Penumbra.ValidityChecker.DevPenumbraExists ) + else if (Penumbra.ValidityChecker.DevPenumbraExists) { - DrawProblemWindow( + DrawProblemWindow(_penumbra, $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" - + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", + false); } else { SetupSizes(); - if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab, - _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) ) - { + if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab, + _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab)) SelectTab = TabType.None; - } } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); + Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}"); } } - private static void DrawProblemWindow( string text, bool withExceptions ) + private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions) { - using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); ImGui.NewLine(); - ImGuiUtil.TextWrapped( text ); + ImGuiUtil.TextWrapped(text); color.Pop(); ImGui.NewLine(); ImGui.NewLine(); - SettingsTab.DrawDiscordButton( 0 ); + SettingsTab.DrawDiscordButton(0); ImGui.SameLine(); - SettingsTab.DrawSupportButton(); + SettingsTab.DrawSupportButton(penumbra); ImGui.NewLine(); ImGui.NewLine(); - if( withExceptions ) + if (withExceptions) { - ImGui.TextUnformatted( "Exceptions" ); + ImGui.TextUnformatted("Exceptions"); ImGui.Separator(); - using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) ); - foreach( var exception in Penumbra.ValidityChecker.ImcExceptions ) + using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); + foreach (var exception in Penumbra.ValidityChecker.ImcExceptions) { - ImGuiUtil.TextWrapped( exception.ToString() ); + ImGuiUtil.TextWrapped(exception.ToString()); ImGui.Separator(); ImGui.NewLine(); } @@ -182,8 +182,8 @@ public sealed partial class ConfigWindow : Window, IDisposable private void SetupSizes() { - _defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale ); - _inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); - _iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); + _defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale); + _inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0); + _iconButtonSize = new Vector2(ImGui.GetFrameHeight()); } -} \ No newline at end of file +} diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs new file mode 100644 index 00000000..e25cc99c --- /dev/null +++ b/Penumbra/Util/EventWrapper.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Util; + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke() + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4, arg5); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index de692ad8..c0ad766d 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,4 +1,5 @@ -using System; +global using StartTracker = OtterGui.Classes.StartTimeTracker; +global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; namespace Penumbra.Util; @@ -47,24 +48,24 @@ public enum PerformanceType public static class TimingExtensions { - public static string ToName( this StartTimeType type ) + public static string ToName(this StartTimeType type) => type switch { - StartTimeType.Total => "Total Construction", - StartTimeType.Identifier => "Identification Data", - StartTimeType.Stains => "Stain Data", - StartTimeType.Items => "Item Data", - StartTimeType.Actors => "Actor Data", - StartTimeType.Backup => "Checking Backups", - StartTimeType.Mods => "Loading Mods", - StartTimeType.Collections => "Loading Collections", - StartTimeType.Api => "Setting Up API", - StartTimeType.Interface => "Setting Up Interface", - StartTimeType.PathResolver => "Setting Up Path Resolver", - _ => $"Unknown {(int) type}", + StartTimeType.Total => "Total Construction", + StartTimeType.Identifier => "Identification Data", + StartTimeType.Stains => "Stain Data", + StartTimeType.Items => "Item Data", + StartTimeType.Actors => "Actor Data", + StartTimeType.Backup => "Checking Backups", + StartTimeType.Mods => "Loading Mods", + StartTimeType.Collections => "Loading Collections", + StartTimeType.Api => "Setting Up API", + StartTimeType.Interface => "Setting Up Interface", + StartTimeType.PathResolver => "Setting Up Path Resolver", + _ => $"Unknown {(int)type}", }; - public static string ToName( this PerformanceType type ) + public static string ToName(this PerformanceType type) => type switch { PerformanceType.UiMainWindow => "Main Interface Drawing", @@ -91,6 +92,6 @@ public static class TimingExtensions PerformanceType.LoadPap => "LoadPap Hook", PerformanceType.LoadAction => "LoadAction Hook", PerformanceType.DebugTimes => "Debug Tracking", - _ => $"Unknown {( int )type}", + _ => $"Unknown {(int)type}", }; -} \ No newline at end of file +} diff --git a/Penumbra/Util/StainManager.cs b/Penumbra/Util/StainManager.cs deleted file mode 100644 index 91c9a731..00000000 --- a/Penumbra/Util/StainManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Data; -using Dalamud.Plugin; -using OtterGui.Widgets; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; - -namespace Penumbra.Util; - -public class StainManager : IDisposable -{ - public sealed class StainTemplateCombo : FilterComboCache< ushort > - { - public StainTemplateCombo( IEnumerable< ushort > items ) - : base( items ) - { } - } - - public readonly StainData StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; - - public StainManager( DalamudPluginInterface pluginInterface, DataManager dataManager ) - { - StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); - StainCombo = new FilterComboColors( 140, StainData.Data.Prepend( new KeyValuePair< byte, (string Name, uint Dye, bool Gloss) >( 0, ( "None", 0, false ) ) ) ); - StmFile = new StmFile( dataManager ); - TemplateCombo = new StainTemplateCombo( StmFile.Entries.Keys.Prepend( ( ushort )0 ) ); - } - - public void Dispose() - => StainData.Dispose(); -} \ No newline at end of file From 99fd4b7806af548b5015a54524d50f4795d7eef0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Mar 2023 21:12:01 +0100 Subject: [PATCH 0804/2451] tmp --- Penumbra/Api/TempModManager.cs | 8 +- Penumbra/Interop/Loader/CreateFileWHook.cs | 2 +- Penumbra/Interop/Loader/FileReadHooks.cs | 50 ++++ Penumbra/Interop/Loader/ResourceHook.cs | 182 ++++++++++++ .../Loader/ResourceLoader.Replacement.cs | 279 ++++++++---------- .../Interop/Structs/GetResourceParameters.cs | 16 + 6 files changed, 377 insertions(+), 160 deletions(-) create mode 100644 Penumbra/Interop/Loader/FileReadHooks.cs create mode 100644 Penumbra/Interop/Loader/ResourceHook.cs create mode 100644 Penumbra/Interop/Structs/GetResourceParameters.cs diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 3064d326..09625a73 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -88,10 +88,10 @@ public class TempModManager : IDisposable _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); } } - - /// - /// Apply a mod change to a set of collections. - /// + + /// + /// Apply a mod change to a set of collections. + /// public static void OnGlobalModChange(IEnumerable collections, Mod.TemporaryMod mod, bool created, bool removed) { if (removed) diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs index 7ba4ba22..5d927b95 100644 --- a/Penumbra/Interop/Loader/CreateFileWHook.cs +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -67,7 +67,7 @@ public unsafe class CreateFileWHook : IDisposable { // Use static storage. var ptr = WriteFileName( name ); - Penumbra.Log.Verbose( $"Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); + Penumbra.Log.Verbose( $"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); return _createFileWHook.OriginalDisposeSafe( ptr, access, shareMode, security, creation, flags, template ); } diff --git a/Penumbra/Interop/Loader/FileReadHooks.cs b/Penumbra/Interop/Loader/FileReadHooks.cs new file mode 100644 index 00000000..03bc7d24 --- /dev/null +++ b/Penumbra/Interop/Loader/FileReadHooks.cs @@ -0,0 +1,50 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Loader; + +public unsafe class FileReadHooks : IDisposable +{ + private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); + + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + private readonly Hook _readSqPackHook = null!; + + public FileReadHooks() + { + SignatureHelper.Initialise(this); + _readSqPackHook.Enable(); + } + + /// Invoked when a file is supposed to be read from SqPack. + /// The file descriptor containing what file to read. + /// The games priority. Should not generally be changed. + /// Whether the file needs to be loaded synchronously. Should not generally be changed. + /// Whether to call the original function after the event is finished. + public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ReadSqPackDelegate? ReadSqPack; + + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) + { + var callOriginal = true; + ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); + return callOriginal + ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) + : (byte)1; + } + + public void Dispose() + { + _readSqPackHook.Disable(); + _readSqPackHook.Dispose(); + } +} diff --git a/Penumbra/Interop/Loader/ResourceHook.cs b/Penumbra/Interop/Loader/ResourceHook.cs new file mode 100644 index 00000000..475ba3ed --- /dev/null +++ b/Penumbra/Interop/Loader/ResourceHook.cs @@ -0,0 +1,182 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; + +namespace Penumbra.Interop.Loader; + +public unsafe class ResourceHook : IDisposable +{ + public ResourceHook() + { + SignatureHelper.Initialise(this); + _getResourceSyncHook.Enable(); + _getResourceAsyncHook.Enable(); + _resourceHandleDestructorHook.Enable(); + } + + public void Dispose() + { + _getResourceSyncHook.Dispose(); + _getResourceAsyncHook.Dispose(); + } + + #region GetResource + + /// Called before a resource is requested. + /// The resource category. Should not generally be changed. + /// The resource type. Should not generally be changed. + /// The resource hash. Should generally fit to the path. + /// The path of the requested resource. + /// Mainly used for SCD streaming. + /// Whether to request the resource synchronously or asynchronously. + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref ByteString path, + ref GetResourceParameters parameters, ref bool sync); + + /// + /// Subscribers should be exception-safe. + public event GetResourcePreDelegate? GetResourcePre; + + /// + /// The returned resource handle obtained from a resource request. Contains all the other information from the request. + /// + public delegate void GetResourcePostDelegate(ref ResourceHandle handle); + + /// + /// Subscribers should be exception-safe. + public event GetResourcePostDelegate? GetResourcePost; + + + private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); + + private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown); + + [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] + private readonly Hook _getResourceSyncHook = null!; + + [Signature(Sigs.GetResourceAsync, DetourName = nameof(GetResourceAsyncDetour))] + private readonly Hook _getResourceAsyncHook = null!; + + private ResourceHandle* GetResourceSyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, + int* resourceHash, byte* path, GetResourceParameters* pGetResParams) + => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false); + + private ResourceHandle* GetResourceAsyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + + /// + /// Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. + /// Both work basically the same, so we can reduce the main work to one function used by both hooks. + /// + private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + { + var byteString = new ByteString(path); + GetResourcePre?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref byteString, ref *pGetResParams, ref isSync); + var ret = isSync + ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams) + : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams, isUnk); + GetResourcePost?.Invoke(ref *ret); + return ret; + } + + #endregion + + private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); + + #region IncRef + + /// Invoked before a resource handle reference count is incremented. + /// The resource handle. + /// Whether to call original after the event has run. + /// The return value to use if not calling original. + public delegate void ResourceHandleIncRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref nint returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleIncRefDelegate? ResourceHandleIncRef; + + public nint IncRef(ref ResourceHandle handle) + { + fixed (ResourceHandle* ptr = &handle) + { + return _incRefHook.Original(ptr); + } + } + + private readonly Hook _incRefHook; + private nint ResourceHandleIncRefDetour(ResourceHandle* handle) + { + var callOriginal = true; + var ret = IntPtr.Zero; + ResourceHandleIncRef?.Invoke(ref *handle, ref callOriginal, ref ret); + return callOriginal ? _incRefHook.Original(handle) : ret; + } + + #endregion + + #region DecRef + + /// Invoked before a resource handle reference count is decremented. + /// The resource handle. + /// Whether to call original after the event has run. + /// The return value to use if not calling original. + public delegate void ResourceHandleDecRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref byte returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleDecRefDelegate? ResourceHandleDecRef; + + public byte DecRef(ref ResourceHandle handle) + { + fixed (ResourceHandle* ptr = &handle) + { + return _incRefHook.Original(ptr); + } + } + + private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle); + private readonly Hook _decRefHook; + private byte ResourceHandleDecRefDetour(ResourceHandle* handle) + { + var callOriginal = true; + var ret = byte.MinValue; + ResourceHandleDecRef?.Invoke(ref *handle, ref callOriginal, ref ret); + return callOriginal ? _decRefHook!.Original(handle) : ret; + } + + #endregion + + /// Invoked before a resource handle is destructed. + /// The resource handle. + public delegate void ResourceHandleDtorDelegate(ref ResourceHandle handle); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleDtorDelegate? ResourceHandleDestructor; + + [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] + private readonly Hook _resourceHandleDestructorHook = null!; + + private nint ResourceHandleDestructorDetour(ResourceHandle* handle) + { + ResourceHandleDestructor?.Invoke(ref *handle); + return _resourceHandleDestructorHook!.Original(handle); + } + + #endregion +} diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 27dac9b3..ca1612f1 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -11,157 +11,138 @@ using Penumbra.Util; using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; +using static Penumbra.Interop.Loader.ResourceLoader; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.Loader; +public unsafe class FileReadHooks : IDisposable +{ + private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); + + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + private readonly Hook _readSqPackHook = null!; + + public FileReadHooks() + { + SignatureHelper.Initialise(this); + _readSqPackHook.Enable(); + } + + public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); + + public event ReadSqPackDelegate? ReadSqPack; + + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) + { + var callOriginal = true; + ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); + return callOriginal + ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) + : (byte)1; + } + + public void Dispose() + { + _readSqPackHook.Disable(); + _readSqPackHook.Dispose(); + } +} + public unsafe partial class ResourceLoader { - private readonly CreateFileWHook _createFileWHook = new(); - // Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. - // Both work basically the same, so we can reduce the main work to one function used by both hooks. - - [StructLayout( LayoutKind.Explicit )] - public struct GetResourceParameters + [Conditional("DEBUG")] + private static void CompareHash(int local, int game, Utf8GamePath path) { - [FieldOffset( 16 )] - public uint SegmentOffset; - - [FieldOffset( 20 )] - public uint SegmentLength; - - public bool IsPartialRead - => SegmentLength != 0; + if (local != game) + Penumbra.Log.Warning($"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}."); } - public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams ); + private event Action? PathResolved; - [Signature( Sigs.GetResourceSync, DetourName = nameof( GetResourceSyncDetour ) )] - public readonly Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; - - public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown ); - - [Signature( Sigs.GetResourceAsync, DetourName = nameof( GetResourceAsyncDetour ) )] - public readonly Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; - - private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams ) - => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false ); - - private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) - => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - - private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) - => isSync - ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams ) - : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - - - [Conditional( "DEBUG" )] - private static void CompareHash( int local, int game, Utf8GamePath path ) - { - if( local != game ) - { - Penumbra.Log.Warning( $"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}." ); - } - } - - private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - - public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, ByteString path ) + public ResourceHandle* ResolvePathSync(ResourceCategory category, ResourceType type, ByteString path) { var hash = path.Crc32; - return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); + return GetResourceHandler(true, *ResourceManager, &category, &type, &hash, path.Path, null, false); } - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) + private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) { - using var performance = Penumbra.Performance.Measure( PerformanceType.GetResourceHandler ); + using var performance = Penumbra.Performance.Measure(PerformanceType.GetResourceHandler); ResourceHandle* ret; - if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) + if (!Utf8GamePath.FromPointer(path, out var gamePath)) { - Penumbra.Log.Error( "Could not create GamePath from resource path." ); - return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + Penumbra.Log.Error("Could not create GamePath from resource path."); + return CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); } - CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); + CompareHash(ComputeHash(gamePath.Path, pGetResParams), *resourceHash, gamePath); - ResourceRequested?.Invoke( gamePath, isSync ); + ResourceRequested?.Invoke(gamePath, isSync); // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); - if( resolvedPath == null ) + var (resolvedPath, data) = ResolvePath(gamePath, *categoryId, *resourceType, *resourceHash); + PathResolved?.Invoke(gamePath, *resourceType, resolvedPath ?? (gamePath.IsRooted() ? new FullPath(gamePath) : null), data); + if (resolvedPath == null) { - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, null, data ); + ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, null, data); return ret; } // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); + *resourceHash = ComputeHash(resolvedPath.Value.InternalName, pGetResParams); path = resolvedPath.Value.InternalName.Path; - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); + ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, resolvedPath.Value, data); return ret; } // Use the default method of path replacement. - public static (FullPath?, ResolveData) DefaultResolver( Utf8GamePath path ) + public static (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) { - var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); - return ( resolved, Penumbra.CollectionManager.Default.ToResolveData() ); + var resolved = Penumbra.CollectionManager.Default.ResolvePath(path); + return (resolved, Penumbra.CollectionManager.Default.ToResolveData()); } // Try all resolve path subscribers or use the default replacer. - private (FullPath?, ResolveData) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + private (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash) { - if( !DoReplacements || _incMode.Value ) - { - return ( null, ResolveData.Invalid ); - } + if (!DoReplacements || _incMode.Value) + return (null, ResolveData.Invalid); path = path.ToLower(); - switch( category ) + switch (category) { // Only Interface collection. case ResourceCategory.Ui: { - var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); - return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); + var resolved = Penumbra.CollectionManager.Interface.ResolvePath(path); + return (resolved, Penumbra.CollectionManager.Interface.ToResolveData()); } // Never allow changing scripts. case ResourceCategory.UiScript: case ResourceCategory.GameScript: - return ( null, ResolveData.Invalid ); + return (null, ResolveData.Invalid); // Use actual resolving. case ResourceCategory.Chara: case ResourceCategory.Shader: case ResourceCategory.Vfx: case ResourceCategory.Sound: - if( ResolvePathCustomization != null ) - { - foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + if (ResolvePathCustomization != null) + foreach (var resolver in ResolvePathCustomization.GetInvocationList()) { - if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) - { + if (((ResolvePathDelegate)resolver).Invoke(path, category, resourceType, resourceHash, out var ret)) return ret; - } } - } break; // None of these files are ever associated with specific characters, @@ -176,65 +157,57 @@ public unsafe partial class ResourceLoader break; } - return DefaultResolver( path ); + return DefaultResolver(path); } // We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. - public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, - bool isSync ); + public delegate byte ReadFileDelegate(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync); - [Signature( Sigs.ReadFile )] + [Signature(Sigs.ReadFile)] public readonly ReadFileDelegate ReadFile = null!; // We hook ReadSqPack to redirect rooted files to ReadFile. - public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + public delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); - [Signature( Sigs.ReadSqPack, DetourName = nameof( ReadSqPackDetour ) )] - public readonly Hook< ReadSqPackPrototype > ReadSqPackHook = null!; + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + public readonly Hook ReadSqPackHook = null!; - private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) { - using var performance = Penumbra.Performance.Measure( PerformanceType.ReadSqPack ); + using var performance = Penumbra.Performance.Measure(PerformanceType.ReadSqPack); - if( !DoReplacements ) + if (!DoReplacements) + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + + if (fileDescriptor == null || fileDescriptor->ResourceHandle == null) { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + Penumbra.Log.Error("Failure to load file from SqPack: invalid File Descriptor."); + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); } - if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) - { - Penumbra.Log.Error( "Failure to load file from SqPack: invalid File Descriptor." ); - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - } - - if( !fileDescriptor->ResourceHandle->GamePath( out var gamePath ) || gamePath.Length == 0 ) - { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - } + if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0) + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) - { - return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); - } + if (ResourceLoadCustomization == null || gamePath.Path[0] != (byte)'|') + return DefaultLoadResource(gamePath.Path, resourceManager, fileDescriptor, priority, isSync); // Split the path into the special-treatment part (between the first and second '|') // and the actual path. byte ret = 0; - var split = gamePath.Path.Split( ( byte )'|', 3, false ); - fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; + var split = gamePath.Path.Split((byte)'|', 3, false); + fileDescriptor->ResourceHandle->FileNameData = split[2].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui && ResourceLoadCustomization.GetInvocationList() - .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + .Any(f => ((ResourceLoadCustomizationDelegate)f) + .Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret)); - if( !funcFound ) - { - ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); - } + if (!funcFound) + ret = DefaultLoadResource(split[2], resourceManager, fileDescriptor, priority, isSync); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -244,18 +217,18 @@ public unsafe partial class ResourceLoader } // Load the resource from an SqPack and trigger the FileLoaded event. - private byte DefaultResourceLoad( ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte DefaultResourceLoad(ByteString path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync) { - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( fileDescriptor->ResourceHandle, path, ret != 0, false ); + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, path, ret != 0, false); return ret; } /// Load the resource from a path on the users hard drives. /// - private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte DefaultRootedResourceLoad(ByteString gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync) { // Specify that we are loading unpacked files from the drive. // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations. @@ -263,22 +236,22 @@ public unsafe partial class ResourceLoader // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; - fileDescriptor->FileDescriptor = (byte*) fd + 1; - CreateFileWHook.WritePtr( fd + 0x11, gamePath.Path, gamePath.Length ); - CreateFileWHook.WritePtr( &fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length ); + fileDescriptor->FileDescriptor = (byte*)fd + 1; + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. - var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( fileDescriptor->ResourceHandle, gamePath, ret != 0, true ); + var ret = ReadFile(resourceManager, fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true); return ret; } // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - internal byte DefaultLoadResource( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, - bool isSync ) - => Utf8GamePath.IsRooted( gamePath ) - ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) - : DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ); + internal byte DefaultLoadResource(ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync) + => Utf8GamePath.IsRooted(gamePath) + ? DefaultRootedResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync) + : DefaultResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync); private void DisposeHooks() { @@ -291,21 +264,19 @@ public unsafe partial class ResourceLoader _incRefHook.Dispose(); } - private static int ComputeHash( ByteString path, GetResourceParameters* pGetResParams ) + private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) { - if( pGetResParams == null || !pGetResParams->IsPartialRead ) - { + if (pGetResParams == null || !pGetResParams->IsPartialRead) return path.Crc32; - } // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 return ByteString.Join( - ( byte )'.', + (byte)'.', path, - ByteString.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), - ByteString.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) + ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), + ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) ).Crc32; } @@ -314,19 +285,17 @@ public unsafe partial class ResourceLoader // This means, that if the path determined from that is different than the resources path, // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. // This causes some problems and is hopefully prevented with this. - private readonly ThreadLocal< bool > _incMode = new(); - private readonly Hook< ResourceHandleDestructor > _incRefHook; + private readonly ThreadLocal _incMode = new(); + private readonly Hook _incRefHook; - private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) + private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle) { - if( handle->RefCount > 0 ) - { - return _incRefHook.Original( handle ); - } + if (handle->RefCount > 0) + return _incRefHook.Original(handle); _incMode.Value = true; - var ret = _incRefHook.Original( handle ); + var ret = _incRefHook.Original(handle); _incMode.Value = false; return ret; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/GetResourceParameters.cs b/Penumbra/Interop/Structs/GetResourceParameters.cs new file mode 100644 index 00000000..eb413ead --- /dev/null +++ b/Penumbra/Interop/Structs/GetResourceParameters.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit)] +public struct GetResourceParameters +{ + [FieldOffset(16)] + public uint SegmentOffset; + + [FieldOffset(20)] + public uint SegmentLength; + + public bool IsPartialRead + => SegmentLength != 0; +} From 0df12a34cb7f48a3b8ff86308f9dd591dd5d8722 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Mar 2023 15:15:42 +0100 Subject: [PATCH 0805/2451] Rework Interop/Loader Services. --- OtterGui | 2 +- Penumbra.String | 2 +- .../Collections/ModCollection.Cache.Access.cs | 133 ++++---- Penumbra/Configuration.cs | 9 +- .../Interop/CharacterUtility.DecalReverter.cs | 13 +- Penumbra/Interop/FontReloader.cs | 55 ++-- Penumbra/Interop/Loader/CharacterResolver.cs | 106 ++++++ Penumbra/Interop/Loader/CreateFileWHook.cs | 203 ++++++------ Penumbra/Interop/Loader/FileReadHooks.cs | 50 --- Penumbra/Interop/Loader/FileReadService.cs | 90 ++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 251 --------------- .../Loader/ResourceLoader.Replacement.cs | 301 ------------------ .../Interop/Loader/ResourceLoader.TexMdl.cs | 104 ------ Penumbra/Interop/Loader/ResourceLoader.cs | 297 +++++++++++------ .../Interop/Loader/ResourceManagerService.cs | 114 +++++++ .../{ResourceHook.cs => ResourceService.cs} | 140 ++++---- Penumbra/Interop/Loader/TexMdlService.cs | 100 ++++++ Penumbra/Interop/ResidentResourceManager.cs | 2 - .../Resolver/PathResolver.DrawObjectState.cs | 2 +- .../Interop/Resolver/PathResolver.Meta.cs | 2 +- .../Interop/Resolver/PathResolver.Subfiles.cs | 139 ++++---- Penumbra/Interop/Resolver/PathResolver.cs | 9 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 51 +-- Penumbra/Meta/Manager/MetaManager.cs | 1 - Penumbra/Penumbra.cs | 60 ++-- Penumbra/PenumbraNew.cs | 14 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 51 +-- Penumbra/UI/ConfigWindow.ResourceTab.cs | 11 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 15 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 10 +- Penumbra/UI/ConfigWindow.cs | 5 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 216 +++++++------ 32 files changed, 1137 insertions(+), 1421 deletions(-) create mode 100644 Penumbra/Interop/Loader/CharacterResolver.cs delete mode 100644 Penumbra/Interop/Loader/FileReadHooks.cs create mode 100644 Penumbra/Interop/Loader/FileReadService.cs delete mode 100644 Penumbra/Interop/Loader/ResourceLoader.Debug.cs delete mode 100644 Penumbra/Interop/Loader/ResourceLoader.Replacement.cs delete mode 100644 Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs create mode 100644 Penumbra/Interop/Loader/ResourceManagerService.cs rename Penumbra/Interop/Loader/{ResourceHook.cs => ResourceService.cs} (52%) create mode 100644 Penumbra/Interop/Loader/TexMdlService.cs diff --git a/OtterGui b/OtterGui index 3d346700..df1cd8b0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee +Subproject commit df1cd8b02d729b2e7f585c301105b37c70d81c3e diff --git a/Penumbra.String b/Penumbra.String index 2f999713..81f384cf 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 2f999713c5b692fead3fb28c39002d1cd82c9261 +Subproject commit 81f384cf96a9257b1ee2c7019772f30df78ba417 diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index d425a031..fc72d3dc 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -4,10 +4,12 @@ using Penumbra.Meta.Manager; using Penumbra.Mods; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -28,10 +30,10 @@ public partial class ModCollection // Only create, do not update. private void CreateCache() { - if( _cache == null ) + if (_cache == null) { CalculateEffectiveFileList(); - Penumbra.Log.Verbose( $"Created new cache for collection {Name}." ); + Penumbra.Log.Verbose($"Created new cache for collection {Name}."); } } @@ -40,21 +42,17 @@ public partial class ModCollection => CalculateEffectiveFileList(); // Handle temporary mods for this collection. - public void Apply( Mod.TemporaryMod tempMod, bool created ) + public void Apply(Mod.TemporaryMod tempMod, bool created) { - if( created ) - { - _cache?.AddMod( tempMod, tempMod.TotalManipulations > 0 ); - } + if (created) + _cache?.AddMod(tempMod, tempMod.TotalManipulations > 0); else - { - _cache?.ReloadMod( tempMod, tempMod.TotalManipulations > 0 ); - } + _cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0); } - public void Remove( Mod.TemporaryMod tempMod ) + public void Remove(Mod.TemporaryMod tempMod) { - _cache?.RemoveMod( tempMod, tempMod.TotalManipulations > 0 ); + _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); } @@ -63,123 +61,122 @@ public partial class ModCollection { _cache?.Dispose(); _cache = null; - Penumbra.Log.Verbose( $"Cleared cache of collection {Name}." ); + Penumbra.Log.Verbose($"Cleared cache of collection {Name}."); } - public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath path ) - => _cache?.ReverseResolvePath( path ) ?? Array.Empty< Utf8GamePath >(); + public IEnumerable ReverseResolvePath(FullPath path) + => _cache?.ReverseResolvePath(path) ?? Array.Empty(); - public HashSet< Utf8GamePath >[] ReverseResolvePaths( string[] paths ) - => _cache?.ReverseResolvePaths( paths ) ?? paths.Select( _ => new HashSet< Utf8GamePath >() ).ToArray(); + public HashSet[] ReverseResolvePaths(string[] paths) + => _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray(); - public FullPath? ResolvePath( Utf8GamePath path ) - => _cache?.ResolvePath( path ); + public FullPath? ResolvePath(Utf8GamePath path) + => _cache?.ResolvePath(path); // Force a file to be resolved to a specific path regardless of conflicts. - internal void ForceFile( Utf8GamePath path, FullPath fullPath ) + internal void ForceFile(Utf8GamePath path, FullPath fullPath) { - if( CheckFullPath( path, fullPath ) ) - { - _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath ); - } + if (CheckFullPath(path, fullPath)) + _cache!.ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath); } - [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static bool CheckFullPath( Utf8GamePath path, FullPath fullPath ) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) { - if( fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength ) - { + if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) return true; - } - Penumbra.Log.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" ); + Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}"); return false; } // Force a file resolve to be removed. - internal void RemoveFile( Utf8GamePath path ) - => _cache!.ResolvedFiles.Remove( path ); + internal void RemoveFile(Utf8GamePath path) + => _cache!.ResolvedFiles.Remove(path); // Obtain data from the cache. internal MetaManager? MetaCache => _cache?.MetaManipulations; - internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); + public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + { + if (_cache != null) + return _cache.MetaManipulations.GetImcFile(path, out file); - internal IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< IMod >, object?) >(); + file = null; + return false; + } - internal IEnumerable< SingleArray< ModConflicts > > AllConflicts - => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); + internal IReadOnlyDictionary ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary(); - internal SingleArray< ModConflicts > Conflicts( Mod mod ) - => _cache?.Conflicts( mod ) ?? new SingleArray< ModConflicts >(); + internal IReadOnlyDictionary, object?)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, object?)>(); + + internal IEnumerable> AllConflicts + => _cache?.AllConflicts ?? Array.Empty>(); + + internal SingleArray Conflicts(Mod mod) + => _cache?.Conflicts(mod) ?? new SingleArray(); // Update the effective file list for the given cache. // Creates a cache if necessary. public void CalculateEffectiveFileList() - => Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name, - CalculateEffectiveFileListInternal ); + => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, + CalculateEffectiveFileListInternal); private void CalculateEffectiveFileListInternal() { // Skip the empty collection. - if( Index == 0 ) - { + if (Index == 0) return; - } - Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}" ); - _cache ??= new Cache( this ); + Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); + _cache ??= new Cache(this); _cache.FullRecalculation(); - Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished." ); + Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); } public void SetFiles() { - if( _cache == null ) + if (_cache == null) { Penumbra.CharacterUtility.ResetAll(); } else { _cache.MetaManipulations.SetFiles(); - Penumbra.Log.Debug( $"Set CharacterUtility resources for collection {Name}." ); + Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}."); } } - public void SetMetaFile( Interop.Structs.CharacterUtility.Index idx ) + public void SetMetaFile(Interop.Structs.CharacterUtility.Index idx) { - if( _cache == null ) - { - Penumbra.CharacterUtility.ResetResource( idx ); - } + if (_cache == null) + Penumbra.CharacterUtility.ResetResource(idx); else - { - _cache.MetaManipulations.SetFile( idx ); - } + _cache.MetaManipulations.SetFile(idx); } // Used for short periods of changed files. - public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) - => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ) - ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.EqdpIdx( genderRace, accessory ) ); + public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + => _cache?.MetaManipulations.TemporarilySetEqdpFile(genderRace, accessory) + ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.EqdpIdx(genderRace, accessory)); public CharacterUtility.List.MetaReverter TemporarilySetEqpFile() => _cache?.MetaManipulations.TemporarilySetEqpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Eqp ); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Eqp); public CharacterUtility.List.MetaReverter TemporarilySetGmpFile() => _cache?.MetaManipulations.TemporarilySetGmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Gmp ); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Gmp); public CharacterUtility.List.MetaReverter TemporarilySetCmpFile() => _cache?.MetaManipulations.TemporarilySetCmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.HumanCmp ); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.HumanCmp); - public CharacterUtility.List.MetaReverter TemporarilySetEstFile( EstManipulation.EstType type ) - => _cache?.MetaManipulations.TemporarilySetEstFile( type ) - ?? Penumbra.CharacterUtility.TemporarilyResetResource( ( Interop.Structs.CharacterUtility.Index )type ); -} \ No newline at end of file + public CharacterUtility.List.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + => _cache?.MetaManipulations.TemporarilySetEstFile(type) + ?? Penumbra.CharacterUtility.TemporarilyResetResource((Interop.Structs.CharacterUtility.Index)type); +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 4d98a4a1..55eee054 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -57,10 +57,11 @@ public class Configuration : IPluginConfiguration public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; diff --git a/Penumbra/Interop/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs index 5aee657a..b49e0795 100644 --- a/Penumbra/Interop/CharacterUtility.DecalReverter.cs +++ b/Penumbra/Interop/CharacterUtility.DecalReverter.cs @@ -2,6 +2,7 @@ using System; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; +using Penumbra.Interop.Loader; using Penumbra.String.Classes; namespace Penumbra.Interop; @@ -11,15 +12,15 @@ public unsafe partial class CharacterUtility public sealed class DecalReverter : IDisposable { public static readonly Utf8GamePath DecalPath = - Utf8GamePath.FromString( "chara/common/texture/decal_equip/_stigma.tex", out var p ) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty; public static readonly Utf8GamePath TransparentPath = - Utf8GamePath.FromString( "chara/common/texture/transparent.tex", out var p ) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; private readonly Structs.TextureResourceHandle* _decal; private readonly Structs.TextureResourceHandle* _transparent; - public DecalReverter( ModCollection? collection, bool doDecal ) + public DecalReverter( ResourceService resources, ModCollection? collection, bool doDecal ) { var ptr = Penumbra.CharacterUtility.Address; _decal = null; @@ -27,7 +28,7 @@ public unsafe partial class CharacterUtility if( doDecal ) { var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path; - var decalHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, decalPath ); + var decalHandle = resources.GetResource( ResourceCategory.Chara, ResourceType.Tex, decalPath ); _decal = ( Structs.TextureResourceHandle* )decalHandle; if( _decal != null ) { @@ -37,7 +38,7 @@ public unsafe partial class CharacterUtility else { var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path; - var transparentHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, transparentPath ); + var transparentHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, transparentPath); _transparent = ( Structs.TextureResourceHandle* )transparentHandle; if( _transparent != null ) { @@ -54,7 +55,7 @@ public unsafe partial class CharacterUtility ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource; --_decal->Handle.RefCount; } - + if( _transparent != null ) { ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource; diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/FontReloader.cs index 64e5db96..f7e8af27 100644 --- a/Penumbra/Interop/FontReloader.cs +++ b/Penumbra/Interop/FontReloader.cs @@ -6,52 +6,37 @@ namespace Penumbra.Interop; // Handle font reloading via game functions. // May cause a interface flicker while reloading. -public static unsafe class FontReloader +public unsafe class FontReloader { - private static readonly AtkModule* AtkModule = null; - private static readonly delegate* unmanaged ReloadFontsFunc = null; + public bool Valid + => _reloadFontsFunc != null; - public static bool Valid - => ReloadFontsFunc != null; - - public static void Reload() + public void Reload() { - if( Valid ) - { - ReloadFontsFunc( AtkModule, false, true ); - } + if (Valid) + _reloadFontsFunc(_atkModule, false, true); else - { - Penumbra.Log.Error( "Could not reload fonts, function could not be found." ); - } + Penumbra.Log.Error("Could not reload fonts, function could not be found."); } - static FontReloader() - { - if( ReloadFontsFunc != null ) - { - return; - } + private readonly AtkModule* _atkModule = null!; + private readonly delegate* unmanaged _reloadFontsFunc = null!; + public FontReloader() + { var framework = Framework.Instance(); - if( framework == null ) - { + if (framework == null) return; - } var uiModule = framework->GetUiModule(); - if( uiModule == null ) - { + if (uiModule == null) return; - } - - var atkModule = uiModule->GetRaptureAtkModule(); - if( atkModule == null ) - { - return; - } - AtkModule = &atkModule->AtkModule; - ReloadFontsFunc = ( ( delegate* unmanaged< AtkModule*, bool, bool, void >* )AtkModule->vtbl )[ Offsets.ReloadFontsVfunc ]; + var atkModule = uiModule->GetRaptureAtkModule(); + if (atkModule == null) + return; + + _atkModule = &atkModule->AtkModule; + _reloadFontsFunc = ((delegate* unmanaged< AtkModule*, bool, bool, void >*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Loader/CharacterResolver.cs b/Penumbra/Interop/Loader/CharacterResolver.cs new file mode 100644 index 00000000..0231a1e8 --- /dev/null +++ b/Penumbra/Interop/Loader/CharacterResolver.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Loader; + +public class CharacterResolver : IDisposable +{ + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly TempCollectionManager _tempCollections; + private readonly ResourceLoader _loader; + private readonly PathResolver _pathResolver; + + public unsafe CharacterResolver(Configuration config, ModCollection.Manager collectionManager, TempCollectionManager tempCollections, + ResourceLoader loader, PathResolver pathResolver) + { + _config = config; + _collectionManager = collectionManager; + _tempCollections = tempCollections; + _loader = loader; + _pathResolver = pathResolver; + + _loader.ResolvePath = ResolvePath; + _loader.FileLoaded += ImcLoadResource; + } + + /// Obtain a temporary or permanent collection by name. + public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection); + + /// Try to resolve the given game path to the replaced path. + public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) + { + // Check if mods are enabled or if we are in a inc-ref at 0 reference count situation. + if (!_config.EnableMods) + return (null, ResolveData.Invalid); + + path = path.ToLower(); + return category switch + { + // Only Interface collection. + ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path), + _collectionManager.Interface.ToResolveData()), + // Never allow changing scripts. + ResourceCategory.UiScript => (null, ResolveData.Invalid), + ResourceCategory.GameScript => (null, ResolveData.Invalid), + // Use actual resolving. + ResourceCategory.Chara => _pathResolver.CharacterResolver(path, resourceType), + ResourceCategory.Shader => _pathResolver.CharacterResolver(path, resourceType), + ResourceCategory.Vfx => _pathResolver.CharacterResolver(path, resourceType), + ResourceCategory.Sound => _pathResolver.CharacterResolver(path, resourceType), + // None of these files are ever associated with specific characters, + // always use the default resolver for now. + ResourceCategory.Common => DefaultResolver(path), + ResourceCategory.BgCommon => DefaultResolver(path), + ResourceCategory.Bg => DefaultResolver(path), + ResourceCategory.Cut => DefaultResolver(path), + ResourceCategory.Exd => DefaultResolver(path), + ResourceCategory.Music => DefaultResolver(path), + _ => DefaultResolver(path), + }; + } + + // TODO + public unsafe void Dispose() + { + _loader.ResetResolvePath(); + _loader.FileLoaded -= ImcLoadResource; + _pathResolver.Dispose(); + } + + // Use the default method of path replacement. + private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) + { + var resolved = _collectionManager.Default.ResolvePath(path); + return (resolved, _collectionManager.Default.ToResolveData()); + } + + /// After loading an IMC file, replace its contents with the modded IMC file. + private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData) + { + if (resource->FileType != ResourceType.Imc) + return; + + var lastUnderscore = additionalData.LastIndexOf((byte)'_'); + var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString(); + if (Utf8GamePath.FromByteString(path, out var gamePath) + && CollectionByName(name, out var collection) + && collection.HasCache + && collection.GetImcFile(gamePath, out var file)) + { + file.Replace(resource); + Penumbra.Log.Verbose( + $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } + } +} diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs index 5d927b95..a16d5db7 100644 --- a/Penumbra/Interop/Loader/CreateFileWHook.cs +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -18,146 +18,145 @@ public unsafe class CreateFileWHook : IDisposable { public const int RequiredSize = 28; - // The prefix is not valid for any actual path, so should never run into false-positives. - private const char Prefix = ( char )( ( byte )'P' | ( ( '?' & 0x00FF ) << 8 ) ); - private const int BufferSize = Utf8GamePath.MaxGamePathLength; - - private delegate nint CreateFileWDelegate( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ); - - private readonly Hook< CreateFileWDelegate > _createFileWHook; - - /// Some storage to skip repeated allocations. - private readonly ThreadLocal< nint > _fileNameStorage = new(SetupStorage, true); - public CreateFileWHook() - => _createFileWHook = Hook< CreateFileWDelegate >.FromImport( null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour ); + { + _createFileWHook = Hook.FromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); + _createFileWHook.Enable(); + } + + /// + /// Write the data read specifically in the CreateFileW hook to a buffer array. + /// + /// The buffer the data is written to. + /// The pointer to the UTF8 string containing the path. + /// The length of the path in bytes. + public static void WritePtr(char* buffer, byte* address, int length) + { + // Set the prefix, which is not valid for any actual path. + buffer[0] = Prefix; + + var ptr = (byte*)buffer; + var v = (ulong)address; + var l = (uint)length; + + // Since the game calls wstrcpy without a length, we need to ensure + // that there is no wchar_t (i.e. 2 bytes) of 0-values before the end. + // Fill everything with 0xFF and use every second byte. + MemoryUtility.MemSet(ptr + 2, 0xFF, 23); + + // Write the byte pointer. + ptr[2] = (byte)(v >> 0); + ptr[4] = (byte)(v >> 8); + ptr[6] = (byte)(v >> 16); + ptr[8] = (byte)(v >> 24); + ptr[10] = (byte)(v >> 32); + ptr[12] = (byte)(v >> 40); + ptr[14] = (byte)(v >> 48); + ptr[16] = (byte)(v >> 56); + + // Write the length. + ptr[18] = (byte)(l >> 0); + ptr[20] = (byte)(l >> 8); + ptr[22] = (byte)(l >> 16); + ptr[24] = (byte)(l >> 24); + + ptr[RequiredSize - 2] = 0; + ptr[RequiredSize - 1] = 0; + } + + public void Dispose() + { + _createFileWHook.Disable(); + _createFileWHook.Dispose(); + foreach (var ptr in _fileNameStorage.Values) + Marshal.FreeHGlobal(ptr); + } /// Long paths in windows need to start with "\\?\", so we keep this static in the pointers. private static nint SetupStorage() { - var ptr = ( char* )Marshal.AllocHGlobal( 2 * BufferSize ); - ptr[ 0 ] = '\\'; - ptr[ 1 ] = '\\'; - ptr[ 2 ] = '?'; - ptr[ 3 ] = '\\'; - ptr[ 4 ] = '\0'; - return ( nint )ptr; + var ptr = (char*)Marshal.AllocHGlobal(2 * BufferSize); + ptr[0] = '\\'; + ptr[1] = '\\'; + ptr[2] = '?'; + ptr[3] = '\\'; + ptr[4] = '\0'; + return (nint)ptr; } - public void Enable() - => _createFileWHook.Enable(); + // The prefix is not valid for any actual path, so should never run into false-positives. + private const char Prefix = (char)((byte)'P' | (('?' & 0x00FF) << 8)); + private const int BufferSize = Utf8GamePath.MaxGamePathLength; - public void Disable() - => _createFileWHook.Disable(); + private delegate nint CreateFileWDelegate(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, + nint template); - public void Dispose() - { - Disable(); - _createFileWHook.Dispose(); - foreach( var ptr in _fileNameStorage.Values ) - { - Marshal.FreeHGlobal( ptr ); - } - } + private readonly Hook _createFileWHook; - private nint CreateFileWDetour( char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template ) + /// Some storage to skip repeated allocations. + private readonly ThreadLocal _fileNameStorage = new(SetupStorage, true); + + private nint CreateFileWDetour(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template) { // Translate data if prefix fits. - if( CheckPtr( fileName, out var name ) ) + if (CheckPtr(fileName, out var name)) { // Use static storage. - var ptr = WriteFileName( name ); - Penumbra.Log.Verbose( $"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); - return _createFileWHook.OriginalDisposeSafe( ptr, access, shareMode, security, creation, flags, template ); + var ptr = WriteFileName(name); + Penumbra.Log.Verbose($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}."); + return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template); } - - return _createFileWHook.OriginalDisposeSafe( fileName, access, shareMode, security, creation, flags, template ); + + return _createFileWHook.OriginalDisposeSafe(fileName, access, shareMode, security, creation, flags, template); } /// Write the UTF8-encoded byte string as UTF16 into the static buffers, /// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t. - private char* WriteFileName( ReadOnlySpan< byte > actualName ) + private char* WriteFileName(ReadOnlySpan actualName) { - var span = new Span< char >( ( char* )_fileNameStorage.Value + 4, BufferSize - 4 ); - var written = Encoding.UTF8.GetChars( actualName, span ); - for( var i = 0; i < written; ++i ) + var span = new Span((char*)_fileNameStorage.Value + 4, BufferSize - 4); + var written = Encoding.UTF8.GetChars(actualName, span); + for (var i = 0; i < written; ++i) { - if( span[ i ] == '/' ) - { - span[ i ] = '\\'; - } + if (span[i] == '/') + span[i] = '\\'; } - span[ written ] = '\0'; + span[written] = '\0'; - return ( char* )_fileNameStorage.Value; + return (char*)_fileNameStorage.Value; } - - public static void WritePtr( char* buffer, byte* address, int length ) + private static bool CheckPtr(char* buffer, out ReadOnlySpan fileName) { - // Set the prefix, which is not valid for any actual path. - buffer[ 0 ] = Prefix; - - var ptr = ( byte* )buffer; - var v = ( ulong )address; - var l = ( uint )length; - - // Since the game calls wstrcpy without a length, we need to ensure - // that there is no wchar_t (i.e. 2 bytes) of 0-values before the end. - // Fill everything with 0xFF and use every second byte. - MemoryUtility.MemSet( ptr + 2, 0xFF, 23 ); - - // Write the byte pointer. - ptr[ 2 ] = ( byte )( v >> 0 ); - ptr[ 4 ] = ( byte )( v >> 8 ); - ptr[ 6 ] = ( byte )( v >> 16 ); - ptr[ 8 ] = ( byte )( v >> 24 ); - ptr[ 10 ] = ( byte )( v >> 32 ); - ptr[ 12 ] = ( byte )( v >> 40 ); - ptr[ 14 ] = ( byte )( v >> 48 ); - ptr[ 16 ] = ( byte )( v >> 56 ); - - // Write the length. - ptr[ 18 ] = ( byte )( l >> 0 ); - ptr[ 20 ] = ( byte )( l >> 8 ); - ptr[ 22 ] = ( byte )( l >> 16 ); - ptr[ 24 ] = ( byte )( l >> 24 ); - - ptr[ RequiredSize - 2 ] = 0; - ptr[ RequiredSize - 1 ] = 0; - } - - private static bool CheckPtr( char* buffer, out ReadOnlySpan< byte > fileName ) - { - if( buffer[ 0 ] is not Prefix ) + if (buffer[0] is not Prefix) { - fileName = ReadOnlySpan< byte >.Empty; + fileName = ReadOnlySpan.Empty; return false; } - var ptr = ( byte* )buffer; + var ptr = (byte*)buffer; // Read the byte pointer. var address = 0ul; - address |= ( ulong )ptr[ 2 ] << 0; - address |= ( ulong )ptr[ 4 ] << 8; - address |= ( ulong )ptr[ 6 ] << 16; - address |= ( ulong )ptr[ 8 ] << 24; - address |= ( ulong )ptr[ 10 ] << 32; - address |= ( ulong )ptr[ 12 ] << 40; - address |= ( ulong )ptr[ 14 ] << 48; - address |= ( ulong )ptr[ 16 ] << 56; + address |= (ulong)ptr[2] << 0; + address |= (ulong)ptr[4] << 8; + address |= (ulong)ptr[6] << 16; + address |= (ulong)ptr[8] << 24; + address |= (ulong)ptr[10] << 32; + address |= (ulong)ptr[12] << 40; + address |= (ulong)ptr[14] << 48; + address |= (ulong)ptr[16] << 56; // Read the length. var length = 0u; - length |= ( uint )ptr[ 18 ] << 0; - length |= ( uint )ptr[ 20 ] << 8; - length |= ( uint )ptr[ 22 ] << 16; - length |= ( uint )ptr[ 24 ] << 24; + length |= (uint)ptr[18] << 0; + length |= (uint)ptr[20] << 8; + length |= (uint)ptr[22] << 16; + length |= (uint)ptr[24] << 24; - fileName = new ReadOnlySpan< byte >( ( void* )address, ( int )length ); + fileName = new ReadOnlySpan((void*)address, (int)length); return true; } @@ -175,4 +174,4 @@ public unsafe class CreateFileWHook : IDisposable // var createFileAddress = GetProcAddress( userApi, "CreateFileW" ); // _createFileWHook = Hook.FromAddress( createFileAddress, CreateFileWDetour ); //} -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Loader/FileReadHooks.cs b/Penumbra/Interop/Loader/FileReadHooks.cs deleted file mode 100644 index 03bc7d24..00000000 --- a/Penumbra/Interop/Loader/FileReadHooks.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.GameData; -using Penumbra.Interop.Structs; - -namespace Penumbra.Interop.Loader; - -public unsafe class FileReadHooks : IDisposable -{ - private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); - - [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] - private readonly Hook _readSqPackHook = null!; - - public FileReadHooks() - { - SignatureHelper.Initialise(this); - _readSqPackHook.Enable(); - } - - /// Invoked when a file is supposed to be read from SqPack. - /// The file descriptor containing what file to read. - /// The games priority. Should not generally be changed. - /// Whether the file needs to be loaded synchronously. Should not generally be changed. - /// Whether to call the original function after the event is finished. - public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); - - /// - /// - /// Subscribers should be exception-safe. - /// - public event ReadSqPackDelegate? ReadSqPack; - - private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) - { - var callOriginal = true; - ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); - return callOriginal - ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) - : (byte)1; - } - - public void Dispose() - { - _readSqPackHook.Disable(); - _readSqPackHook.Dispose(); - } -} diff --git a/Penumbra/Interop/Loader/FileReadService.cs b/Penumbra/Interop/Loader/FileReadService.cs new file mode 100644 index 00000000..6b89b576 --- /dev/null +++ b/Penumbra/Interop/Loader/FileReadService.cs @@ -0,0 +1,90 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.GameData; +using Penumbra.Interop.Structs; +using Penumbra.Util; + +namespace Penumbra.Interop.Loader; + +public unsafe class FileReadService : IDisposable +{ + public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager) + { + _resourceManager = resourceManager; + _performance = performance; + SignatureHelper.Initialise(this); + _readSqPackHook.Enable(); + } + + /// Invoked when a file is supposed to be read from SqPack. + /// The file descriptor containing what file to read. + /// The games priority. Should not generally be changed. + /// Whether the file needs to be loaded synchronously. Should not generally be changed. + /// The return value. If this is set, original will not be called. + public delegate void ReadSqPackDelegate(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ReadSqPackDelegate? ReadSqPack; + + /// + /// Use the games ReadFile function to read a file from the hard drive instead of an SqPack. + /// + /// The file to load. + /// The games priority. + /// Whether the file needs to be loaded synchronously. + /// Unknown, not directly success/failure. + public byte ReadFile(SeFileDescriptor* fileDescriptor, int priority, bool isSync) + => _readFile.Invoke(GetResourceManager(), fileDescriptor, priority, isSync); + + public byte ReadDefaultSqPack(SeFileDescriptor* fileDescriptor, int priority, bool isSync) + => _readSqPackHook.Original(GetResourceManager(), fileDescriptor, priority, isSync); + + public void Dispose() + { + _readSqPackHook.Dispose(); + } + + private readonly PerformanceTracker _performance; + private readonly ResourceManagerService _resourceManager; + + private delegate byte ReadSqPackPrototype(nint resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); + + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + private readonly Hook _readSqPackHook = null!; + + private byte ReadSqPackDetour(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) + { + using var performance = _performance.Measure(PerformanceType.ReadSqPack); + byte? ret = null; + _lastFileThreadResourceManager.Value = resourceManager; + ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret); + _lastFileThreadResourceManager.Value = IntPtr.Zero; + return ret ?? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + } + + + private delegate byte ReadFileDelegate(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync); + + /// We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. + [Signature(Sigs.ReadFile)] + private readonly ReadFileDelegate _readFile = null!; + + private readonly ThreadLocal _lastFileThreadResourceManager = new(true); + + /// + /// Usually files are loaded using the resource manager as a first pointer, but it seems some rare cases are using something else. + /// So we keep track of them per thread and use them. + /// + private nint GetResourceManager() + => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero + ? (nint) _resourceManager.ResourceManager + : _lastFileThreadResourceManager.Value; +} diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs deleted file mode 100644 index 8e60d26f..00000000 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using FFXIVClientStructs.Interop; -using FFXIVClientStructs.STD; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Interop.Loader; - -public unsafe partial class ResourceLoader -{ - // If in debug mode, this logs any resource at refcount 0 that gets decremented again, and skips the decrement instead. - private delegate byte ResourceHandleDecRef( ResourceHandle* handle ); - private readonly Hook< ResourceHandleDecRef > _decRefHook; - - public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); - - [Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )] - public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; - - private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) - { - if( handle != null ) - { - Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})." ); - } - - return ResourceHandleDestructorHook!.Original( handle ); - } - - // A static pointer to the SE Resource Manager - [Signature( Sigs.ResourceManager, ScanType = ScanType.StaticAddress)] - public static ResourceManager** ResourceManager; - - // Gather some debugging data about penumbra-loaded objects. - public struct DebugData - { - public Structs.ResourceHandle* OriginalResource; - public Structs.ResourceHandle* ManipulatedResource; - public Utf8GamePath OriginalPath; - public FullPath ManipulatedPath; - public ResourceCategory Category; - public ResolveData ResolverInfo; - public ResourceType Extension; - } - - private readonly SortedList< FullPath, DebugData > _debugList = new(); - - public IReadOnlyDictionary< FullPath, DebugData > DebugList - => _debugList; - - public void EnableDebug() - { - _decRefHook.Enable(); - ResourceLoaded += AddModifiedDebugInfo; - } - - public void DisableDebug() - { - _decRefHook.Disable(); - ResourceLoaded -= AddModifiedDebugInfo; - } - - private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolverInfo ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes ); - - if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 ) - { - return; - } - - // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra. - try - { - var crc = ( uint )originalPath.Path.Crc32; - var originalResource = FindResource( handle->Category, handle->FileType, crc ); - _debugList[ manipulatedPath.Value ] = new DebugData() - { - OriginalResource = ( Structs.ResourceHandle* )originalResource, - ManipulatedResource = handle, - Category = handle->Category, - Extension = handle->FileType, - OriginalPath = originalPath.Clone(), - ManipulatedPath = manipulatedPath.Value, - ResolverInfo = resolverInfo, - }; - } - catch( Exception e ) - { - Penumbra.Log.Error( e.ToString() ); - } - } - - // Find a key in a StdMap. - private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key ) - where TKey : unmanaged, IComparable< TKey > - where TValue : unmanaged - { - if( map == null || map->Count == 0 ) - { - return null; - } - - var node = map->Head->Parent; - while( !node->IsNil ) - { - switch( key.CompareTo( node->KeyValuePair.Item1 ) ) - { - case 0: return &node->KeyValuePair.Item2; - case < 0: - node = node->Left; - break; - default: - node = node->Right; - break; - } - } - - return null; - } - - // Iterate in tree-order through a map, applying action to each KeyValuePair. - private static void IterateMap< TKey, TValue >( StdMap< TKey, TValue >* map, Action< TKey, TValue > action ) - where TKey : unmanaged - where TValue : unmanaged - { - if( map == null || map->Count == 0 ) - { - return; - } - - for( var node = map->SmallestValue; !node->IsNil; node = node->Next() ) - { - action( node->KeyValuePair.Item1, node->KeyValuePair.Item2 ); - } - } - - - // Find a resource in the resource manager by its category, extension and crc-hash - public static ResourceHandle* FindResource( ResourceCategory cat, ResourceType ext, uint crc32 ) - { - ref var manager = ref *ResourceManager; - var catIdx = ( uint )cat >> 0x18; - cat = ( ResourceCategory )( ushort )cat; - ref var category = ref manager->ResourceGraph->ContainerArraySpan[(int) cat]; - var extMap = FindInMap( category.CategoryMapsSpan[ (int) catIdx ].Value, ( uint )ext ); - if( extMap == null ) - { - return null; - } - - var ret = FindInMap( extMap->Value, crc32 ); - return ret == null ? null : ret->Value; - } - - public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph, - int idx ); - - public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph ); - public delegate void ResourceAction( uint crc32, ResourceHandle* graph ); - - // Iteration functions through the resource manager. - public static void IterateGraphs( ExtMapAction action ) - { - ref var manager = ref *ResourceManager; - foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) ) - { - ref var graph = ref manager->ResourceGraph->ContainerArraySpan[(int) resourceType]; - for( var i = 0; i < 20; ++i ) - { - var map = graph.CategoryMapsSpan[i]; - if( map.Value != null ) - { - action( resourceType, map, i ); - } - } - } - } - - public static void IterateExtMap( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, ResourceMapAction action ) - => IterateMap( map, ( ext, m ) => action( ext, m.Value ) ); - - public static void IterateResourceMap( StdMap< uint, Pointer< ResourceHandle > >* map, ResourceAction action ) - => IterateMap( map, ( crc, r ) => action( crc, r.Value ) ); - - public static void IterateResources( ResourceAction action ) - { - IterateGraphs( ( _, extMap, _ ) - => IterateExtMap( extMap, ( _, resourceMap ) - => IterateResourceMap( resourceMap, action ) ) ); - } - - // Update the list of currently replaced resources. - // Only used when the Replaced Resources Tab in the Debug tab is open. - public void UpdateDebugInfo() - { - using var performance = Penumbra.Performance.Measure( PerformanceType.DebugTimes ); - for( var i = 0; i < _debugList.Count; ++i ) - { - var data = _debugList.Values[ i ]; - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if( data.OriginalPath.Path == null ) - { - _debugList.RemoveAt( i-- ); - continue; - } - - var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); - var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); - if( modifiedResource == null ) - { - _debugList.RemoveAt( i-- ); - } - else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource ) - { - _debugList[ _debugList.Keys[ i ] ] = data with - { - OriginalResource = ( Structs.ResourceHandle* )regularResource, - ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource, - }; - } - } - } - - // Prevent resource management weirdness. - private byte ResourceHandleDecRefDetour( ResourceHandle* handle ) - { - if( handle == null ) - { - return 0; - } - - if( handle->RefCount != 0 ) - { - return _decRefHook.Original( handle ); - } - - Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); - return 1; - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs deleted file mode 100644 index ca1612f1..00000000 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ /dev/null @@ -1,301 +0,0 @@ -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using static Penumbra.Interop.Loader.ResourceLoader; -using FileMode = Penumbra.Interop.Structs.FileMode; -using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; - -namespace Penumbra.Interop.Loader; - -public unsafe class FileReadHooks : IDisposable -{ - private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); - - [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] - private readonly Hook _readSqPackHook = null!; - - public FileReadHooks() - { - SignatureHelper.Initialise(this); - _readSqPackHook.Enable(); - } - - public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); - - public event ReadSqPackDelegate? ReadSqPack; - - private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) - { - var callOriginal = true; - ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); - return callOriginal - ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) - : (byte)1; - } - - public void Dispose() - { - _readSqPackHook.Disable(); - _readSqPackHook.Dispose(); - } -} - -public unsafe partial class ResourceLoader -{ - - [Conditional("DEBUG")] - private static void CompareHash(int local, int game, Utf8GamePath path) - { - if (local != game) - Penumbra.Log.Warning($"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}."); - } - - private event Action? PathResolved; - - public ResourceHandle* ResolvePathSync(ResourceCategory category, ResourceType type, ByteString path) - { - var hash = path.Crc32; - return GetResourceHandler(true, *ResourceManager, &category, &type, &hash, path.Path, null, false); - } - - private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.GetResourceHandler); - - ResourceHandle* ret; - if (!Utf8GamePath.FromPointer(path, out var gamePath)) - { - Penumbra.Log.Error("Could not create GamePath from resource path."); - return CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); - } - - CompareHash(ComputeHash(gamePath.Path, pGetResParams), *resourceHash, gamePath); - - ResourceRequested?.Invoke(gamePath, isSync); - - // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = ResolvePath(gamePath, *categoryId, *resourceType, *resourceHash); - PathResolved?.Invoke(gamePath, *resourceType, resolvedPath ?? (gamePath.IsRooted() ? new FullPath(gamePath) : null), data); - if (resolvedPath == null) - { - ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); - ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, null, data); - return ret; - } - - // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash(resolvedPath.Value.InternalName, pGetResParams); - - path = resolvedPath.Value.InternalName.Path; - ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); - ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, resolvedPath.Value, data); - - return ret; - } - - - // Use the default method of path replacement. - public static (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) - { - var resolved = Penumbra.CollectionManager.Default.ResolvePath(path); - return (resolved, Penumbra.CollectionManager.Default.ToResolveData()); - } - - // Try all resolve path subscribers or use the default replacer. - private (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash) - { - if (!DoReplacements || _incMode.Value) - return (null, ResolveData.Invalid); - - path = path.ToLower(); - switch (category) - { - // Only Interface collection. - case ResourceCategory.Ui: - { - var resolved = Penumbra.CollectionManager.Interface.ResolvePath(path); - return (resolved, Penumbra.CollectionManager.Interface.ToResolveData()); - } - // Never allow changing scripts. - case ResourceCategory.UiScript: - case ResourceCategory.GameScript: - return (null, ResolveData.Invalid); - // Use actual resolving. - case ResourceCategory.Chara: - case ResourceCategory.Shader: - case ResourceCategory.Vfx: - case ResourceCategory.Sound: - if (ResolvePathCustomization != null) - foreach (var resolver in ResolvePathCustomization.GetInvocationList()) - { - if (((ResolvePathDelegate)resolver).Invoke(path, category, resourceType, resourceHash, out var ret)) - return ret; - } - - break; - // None of these files are ever associated with specific characters, - // always use the default resolver for now. - case ResourceCategory.Common: - case ResourceCategory.BgCommon: - case ResourceCategory.Bg: - case ResourceCategory.Cut: - case ResourceCategory.Exd: - case ResourceCategory.Music: - default: - break; - } - - return DefaultResolver(path); - } - - - // We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. - public delegate byte ReadFileDelegate(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, - bool isSync); - - [Signature(Sigs.ReadFile)] - public readonly ReadFileDelegate ReadFile = null!; - - // We hook ReadSqPack to redirect rooted files to ReadFile. - public delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); - - [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] - public readonly Hook ReadSqPackHook = null!; - - private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.ReadSqPack); - - if (!DoReplacements) - return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); - - if (fileDescriptor == null || fileDescriptor->ResourceHandle == null) - { - Penumbra.Log.Error("Failure to load file from SqPack: invalid File Descriptor."); - return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); - } - - if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0) - return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); - - // Paths starting with a '|' are handled separately to allow for special treatment. - // They are expected to also have a closing '|'. - if (ResourceLoadCustomization == null || gamePath.Path[0] != (byte)'|') - return DefaultLoadResource(gamePath.Path, resourceManager, fileDescriptor, priority, isSync); - - // Split the path into the special-treatment part (between the first and second '|') - // and the actual path. - byte ret = 0; - var split = gamePath.Path.Split((byte)'|', 3, false); - fileDescriptor->ResourceHandle->FileNameData = split[2].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; - var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui - && ResourceLoadCustomization.GetInvocationList() - .Any(f => ((ResourceLoadCustomizationDelegate)f) - .Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret)); - - if (!funcFound) - ret = DefaultLoadResource(split[2], resourceManager, fileDescriptor, priority, isSync); - - // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; - - return ret; - } - - // Load the resource from an SqPack and trigger the FileLoaded event. - private byte DefaultResourceLoad(ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync) - { - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); - FileLoaded?.Invoke(fileDescriptor->ResourceHandle, path, ret != 0, false); - return ret; - } - - /// Load the resource from a path on the users hard drives. - /// - private byte DefaultRootedResourceLoad(ByteString gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync) - { - // Specify that we are loading unpacked files from the drive. - // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations. - fileDescriptor->FileMode = FileMode.LoadUnpackedResource; - - // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. - var fd = stackalloc char[0x11 + 0x0B + 14]; - fileDescriptor->FileDescriptor = (byte*)fd + 1; - CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); - CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); - - // Use the SE ReadFile function. - var ret = ReadFile(resourceManager, fileDescriptor, priority, isSync); - FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true); - return ret; - } - - // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - internal byte DefaultLoadResource(ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, - bool isSync) - => Utf8GamePath.IsRooted(gamePath) - ? DefaultRootedResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync) - : DefaultResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync); - - private void DisposeHooks() - { - DisableHooks(); - _createFileWHook.Dispose(); - ReadSqPackHook.Dispose(); - GetResourceSyncHook.Dispose(); - GetResourceAsyncHook.Dispose(); - ResourceHandleDestructorHook?.Dispose(); - _incRefHook.Dispose(); - } - - private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) - { - if (pGetResParams == null || !pGetResParams->IsPartialRead) - return path.Crc32; - - // When the game requests file only partially, crc32 includes that information, in format of: - // path/to/file.ext.hex_offset.hex_size - // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - return ByteString.Join( - (byte)'.', - path, - ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), - ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) - ).Crc32; - } - - - // A resource with ref count 0 that gets incremented goes through GetResourceAsync again. - // This means, that if the path determined from that is different than the resources path, - // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. - // This causes some problems and is hopefully prevented with this. - private readonly ThreadLocal _incMode = new(); - private readonly Hook _incRefHook; - - private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle) - { - if (handle->RefCount > 0) - return _incRefHook.Original(handle); - - _incMode.Value = true; - var ret = _incRefHook.Original(handle); - _incMode.Value = false; - return ret; - } -} diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs deleted file mode 100644 index 7baced91..00000000 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.String.Classes; - -namespace Penumbra.Interop.Loader; - -// Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection. -public unsafe partial class ResourceLoader -{ - // Custom ulong flag to signal our files as opposed to SE files. - public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); - - // We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, - // i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. - private readonly HashSet< ulong > _customFileCrc = new(); - - public IReadOnlySet< ulong > CustomFileCrc - => _customFileCrc; - - - // The function that checks a files CRC64 to determine whether it is 'protected'. - // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. - public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - - [Signature( Sigs.CheckFileState, DetourName = nameof( CheckFileStateDetour ) )] - public readonly Hook< CheckFileStatePrototype > CheckFileStateHook = null!; - - private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) - => _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 ); - - - // We use the local functions for our own files in the extern hook. - public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 ); - - [Signature( Sigs.LoadTexFileLocal )] - public readonly LoadTexFileLocalDelegate LoadTexFileLocal = null!; - - public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 ); - - [Signature( Sigs.LoadMdlFileLocal )] - public readonly LoadMdlFileLocalPrototype LoadMdlFileLocal = null!; - - - // We hook the extern functions to just return the local one if given the custom flag as last argument. - public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - - [Signature( Sigs.LoadTexFileExtern, DetourName = nameof( LoadTexFileExternDetour ) )] - public readonly Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; - - private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadTexFileLocal.Invoke( resourceHandle, unk1, unk2, unk3 ) - : LoadTexFileExternHook.Original( resourceHandle, unk1, unk2, unk3, ptr ); - - public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - - - [Signature( Sigs.LoadMdlFileExtern, DetourName = nameof( LoadMdlFileExternDetour ) )] - public readonly Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; - - private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadMdlFileLocal.Invoke( resourceHandle, unk1, unk2 ) - : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); - - - private void AddCrc( Utf8GamePath _, ResourceType type, FullPath? path, object? _2 ) - { - if( path.HasValue && type is ResourceType.Mdl or ResourceType.Tex ) - { - _customFileCrc.Add( path.Value.Crc64 ); - } - } - - private void EnableTexMdlTreatment() - { - PathResolved += AddCrc; - CheckFileStateHook.Enable(); - LoadTexFileExternHook.Enable(); - LoadMdlFileExternHook.Enable(); - } - - private void DisableTexMdlTreatment() - { - PathResolved -= AddCrc; - _customFileCrc.Clear(); - _customFileCrc.TrimExcess(); - CheckFileStateHook.Disable(); - LoadTexFileExternHook.Disable(); - LoadMdlFileExternHook.Disable(); - } - - private void DisposeTexMdlTreatment() - { - CheckFileStateHook.Dispose(); - LoadTexFileExternHook.Dispose(); - LoadMdlFileExternHook.Dispose(); - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 5ad06e93..a7312643 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -1,6 +1,6 @@ using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; +using System.Diagnostics; +using System.Threading; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; @@ -10,114 +10,219 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.Loader; -public unsafe partial class ResourceLoader : IDisposable +public unsafe class ResourceLoader : IDisposable { - // Toggle whether replacing paths is active, independently of hook and event state. - public bool DoReplacements { get; private set; } + private readonly ResourceService _resources; + private readonly FileReadService _fileReadService; + private readonly TexMdlService _texMdlService; - // Hooks are required for everything, even events firing. - public bool HooksEnabled { get; private set; } - - public void EnableReplacements() + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, + CreateFileWHook _) { - if( DoReplacements ) - { - return; - } + _resources = resources; + _fileReadService = fileReadService; + _texMdlService = texMdlService; + ResetResolvePath(); - DoReplacements = true; - EnableTexMdlTreatment(); - EnableHooks(); + _resources.ResourceRequested += ResourceHandler; + _resources.ResourceHandleIncRef += IncRefProtection; + _resources.ResourceHandleDecRef += DecRefProtection; + _fileReadService.ReadSqPack += ReadSqPackDetour; } - public void DisableReplacements() - { - if( !DoReplacements ) - { - return; - } + /// The function to use to resolve a given path. + public Func ResolvePath = null!; - DoReplacements = false; - DisableTexMdlTreatment(); - } + /// Reset the ResolvePath function to always return null. + public void ResetResolvePath() + => ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid); - public void EnableHooks() - { - if( HooksEnabled ) - { - return; - } - - HooksEnabled = true; - _createFileWHook.Enable(); - ReadSqPackHook.Enable(); - GetResourceSyncHook.Enable(); - GetResourceAsyncHook.Enable(); - _incRefHook.Enable(); - } - - public void DisableHooks() - { - if( !HooksEnabled ) - { - return; - } - - HooksEnabled = false; - _createFileWHook.Disable(); - ReadSqPackHook.Disable(); - GetResourceSyncHook.Disable(); - GetResourceAsyncHook.Disable(); - _incRefHook.Disable(); - } - - public ResourceLoader( Penumbra _ ) - { - SignatureHelper.Initialise( this ); - _decRefHook = Hook< ResourceHandleDecRef >.FromAddress( - ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, - ResourceHandleDecRefDetour ); - _incRefHook = Hook< ResourceHandleDestructor >.FromAddress( - ( IntPtr )FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour ); - } - - // Event fired whenever a resource is requested. - public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous ); - public event ResourceRequestedDelegate? ResourceRequested; - - // Event fired whenever a resource is returned. - // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. - // resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object. - public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData ); + public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData); + /// + /// Event fired whenever a resource is returned. + /// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. + /// resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object. + /// public event ResourceLoadedDelegate? ResourceLoaded; + public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ByteString additionalData); - // Event fired whenever a resource is newly loaded. - // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) - // custom is true if the file was loaded from local files instead of the default SqPacks. - public delegate void FileLoadedDelegate( ResourceHandle* resource, ByteString path, bool success, bool custom ); + /// + /// Event fired whenever a resource is newly loaded. + /// ReturnValue indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) + /// custom is true if the file was loaded from local files instead of the default SqPacks. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// public event FileLoadedDelegate? FileLoaded; - // Customization point to control how path resolving is handled. - // Resolving goes through all subscribed functions in arbitrary order until one returns true, - // or uses default resolving if none return true. - public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash, - out (FullPath?, ResolveData) ret ); - - public event ResolvePathDelegate? ResolvePathCustomization; - - // Customize file loading for any GamePaths that start with "|". - // Same procedure as above. - public delegate bool ResourceLoadCustomizationDelegate( ByteString split, ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue ); - - public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization; - public void Dispose() { - DisposeHooks(); - DisposeTexMdlTreatment(); + _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceHandleIncRef -= IncRefProtection; + _resources.ResourceHandleDecRef -= DecRefProtection; + _fileReadService.ReadSqPack -= ReadSqPackDetour; } -} \ No newline at end of file + + private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) + { + if (returnValue != null) + return; + + CompareHash(ComputeHash(path.Path, parameters), hash, path); + + // If no replacements are being made, we still want to be able to trigger the event. + var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : ResolvePath(path, category, type); + if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) + { + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); + return; + } + + _texMdlService.AddCrc(type, resolvedPath); + // Replace the hash and path with the correct one for the replacement. + hash = ComputeHash(resolvedPath.Value.InternalName, parameters); + path = p; + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + ResourceLoaded?.Invoke(returnValue, p, resolvedPath.Value, data); + } + + private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) + { + if (fileDescriptor->ResourceHandle == null) + { + Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor."); + return; + } + + if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0) + { + Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid path specified."); + return; + } + + // Paths starting with a '|' are handled separately to allow for special treatment. + // They are expected to also have a closing '|'. + if (gamePath.Path[0] != (byte)'|') + { + returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty); + return; + } + + // Split the path into the special-treatment part (between the first and second '|') + // and the actual path. + var split = gamePath.Path.Split((byte)'|', 3, false); + fileDescriptor->ResourceHandle->FileNameData = split[2].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; + MtrlForceSync(fileDescriptor, ref isSync); + returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]); + // Return original resource handle path so that they can be loaded separately. + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; + } + + + /// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. + private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, + bool isSync, ByteString additionalData) + { + if (Utf8GamePath.IsRooted(gamePath)) + { + // Specify that we are loading unpacked files from the drive. + // We need to obtain the actual file path in UTF16 (Windows-Unicode) on two locations, + // but we write a pointer to the given string instead and use the CreateFileW hook to handle it, + // because otherwise we are limited to 260 characters. + fileDescriptor->FileMode = FileMode.LoadUnpackedResource; + + // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. + var fd = stackalloc char[0x11 + 0x0B + 14]; + fileDescriptor->FileDescriptor = (byte*)fd + 1; + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); + + // Use the SE ReadFile function. + var ret = _fileReadService.ReadFile(fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true, additionalData); + return ret; + } + else + { + var ret = _fileReadService.ReadDefaultSqPack(fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, false, additionalData); + return ret; + } + } + + /// Special handling for materials. + private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) + { + // Force isSync = true for Materials. I don't really understand why, + // or where the difference even comes from. + // Was called with True on my client and with false on other peoples clients, + // which caused problems. + isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl; + } + + /// + /// A resource with ref count 0 that gets incremented goes through GetResourceAsync again. + /// This means, that if the path determined from that is different than the resources path, + /// a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. + /// This causes some problems and is hopefully prevented with this. + /// + private readonly ThreadLocal _incMode = new(() => false, true); + + /// + private void IncRefProtection(ResourceHandle* handle, ref nint? returnValue) + { + if (handle->RefCount != 0) + return; + + _incMode.Value = true; + returnValue = _resources.IncRef(handle); + _incMode.Value = false; + } + + /// + /// Catch weird errors with invalid decrements of the reference count. + /// + private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) + { + if (handle->RefCount != 0) + return; + + Penumbra.Log.Error( + $"[ResourceLoader] Caught decrease of Reference Counter for {handle->FileName()} at 0x{(ulong)handle} below 0."); + returnValue = 1; + } + + /// Compute the CRC32 hash for a given path together with potential resource parameters. + private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) + { + if (pGetResParams == null || !pGetResParams->IsPartialRead) + return path.Crc32; + + // When the game requests file only partially, crc32 includes that information, in format of: + // path/to/file.ext.hex_offset.hex_size + // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 + return ByteString.Join( + (byte)'.', + path, + ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), + ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) + ).Crc32; + } + + /// + /// In Debug build, compare the hashes the game computes with those Penumbra computes to notice potential changes in the CRC32 algorithm or resource parameters. + /// + [Conditional("DEBUG")] + private static void CompareHash(int local, int game, Utf8GamePath path) + { + if (local != game) + Penumbra.Log.Warning($"[ResourceLoader] Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}."); + } +} diff --git a/Penumbra/Interop/Loader/ResourceManagerService.cs b/Penumbra/Interop/Loader/ResourceManagerService.cs new file mode 100644 index 00000000..fd2b0e4d --- /dev/null +++ b/Penumbra/Interop/Loader/ResourceManagerService.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using FFXIVClientStructs.STD; +using Penumbra.GameData; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop.Loader; + +public unsafe class ResourceManagerService +{ + public ResourceManagerService() + => SignatureHelper.Initialise(this); + + /// The SE Resource Manager as pointer. + public ResourceManager* ResourceManager + => *ResourceManagerAddress; + + /// Find a resource in the resource manager by its category, extension and crc-hash. + public ResourceHandle* FindResource(ResourceCategory cat, ResourceType ext, uint crc32) + { + ref var manager = ref *ResourceManager; + var catIdx = (uint)cat >> 0x18; + cat = (ResourceCategory)(ushort)cat; + ref var category = ref manager.ResourceGraph->ContainerArraySpan[(int)cat]; + var extMap = FindInMap(category.CategoryMapsSpan[(int)catIdx].Value, (uint)ext); + if (extMap == null) + return null; + + var ret = FindInMap(extMap->Value, crc32); + return ret == null ? null : ret->Value; + } + + public delegate void ExtMapAction(ResourceCategory category, StdMap>>>* graph, int idx); + public delegate void ResourceMapAction(uint ext, StdMap>* graph); + public delegate void ResourceAction(uint crc32, ResourceHandle* graph); + + /// Iterate through the entire graph calling an action on every ExtMap. + public void IterateGraphs(ExtMapAction action) + { + ref var manager = ref *ResourceManager; + foreach (var resourceType in Enum.GetValues().SkipLast(1)) + { + ref var graph = ref manager.ResourceGraph->ContainerArraySpan[(int)resourceType]; + for (var i = 0; i < 20; ++i) + { + var map = graph.CategoryMapsSpan[i]; + if (map.Value != null) + action(resourceType, map, i); + } + } + } + + /// Iterate through a specific ExtMap calling an action on every resource map. + public void IterateExtMap(StdMap>>>* map, ResourceMapAction action) + => IterateMap(map, (ext, m) => action(ext, m.Value)); + + /// Iterate through a specific resource map calling an action on every resource. + public void IterateResourceMap(StdMap>* map, ResourceAction action) + => IterateMap(map, (crc, r) => action(crc, r.Value)); + + /// Iterate through the entire graph calling an action on every resource. + public void IterateResources(ResourceAction action) + { + IterateGraphs((_, extMap, _) + => IterateExtMap(extMap, (_, resourceMap) + => IterateResourceMap(resourceMap, action))); + } + + /// A static pointer to the SE Resource Manager. + [Signature(Sigs.ResourceManager, ScanType = ScanType.StaticAddress)] + internal readonly ResourceManager** ResourceManagerAddress = null; + + // Find a key in a StdMap. + private static TValue* FindInMap(StdMap* map, in TKey key) + where TKey : unmanaged, IComparable + where TValue : unmanaged + { + if (map == null || map->Count == 0) + return null; + + var node = map->Head->Parent; + while (!node->IsNil) + { + switch (key.CompareTo(node->KeyValuePair.Item1)) + { + case 0: return &node->KeyValuePair.Item2; + case < 0: + node = node->Left; + break; + default: + node = node->Right; + break; + } + } + + return null; + } + + // Iterate in tree-order through a map, applying action to each KeyValuePair. + private static void IterateMap(StdMap* map, Action action) + where TKey : unmanaged + where TValue : unmanaged + { + if (map == null || map->Count == 0) + return; + + for (var node = map->SmallestValue; !node->IsNil; node = node->Next()) + action(node->KeyValuePair.Item1, node->KeyValuePair.Item2); + } +} diff --git a/Penumbra/Interop/Loader/ResourceHook.cs b/Penumbra/Interop/Loader/ResourceService.cs similarity index 52% rename from Penumbra/Interop/Loader/ResourceHook.cs rename to Penumbra/Interop/Loader/ResourceService.cs index 475ba3ed..447c2c03 100644 --- a/Penumbra/Interop/Loader/ResourceHook.cs +++ b/Penumbra/Interop/Loader/ResourceService.cs @@ -6,24 +6,48 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; -using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; +using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Interop.Loader; -public unsafe class ResourceHook : IDisposable +public unsafe class ResourceService : IDisposable { - public ResourceHook() + private readonly PerformanceTracker _performance; + private readonly ResourceManagerService _resourceManager; + + public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager) { + _performance = performance; + _resourceManager = resourceManager; SignatureHelper.Initialise(this); _getResourceSyncHook.Enable(); _getResourceAsyncHook.Enable(); _resourceHandleDestructorHook.Enable(); + _incRefHook = Hook.FromAddress( + (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, + ResourceHandleIncRefDetour); + _incRefHook.Enable(); + _decRefHook = Hook.FromAddress( + (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, + ResourceHandleDecRefDetour); + _decRefHook.Enable(); + } + + public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path) + { + var hash = path.Crc32; + return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress, + &category, &type, &hash, path.Path, null, false); } public void Dispose() { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); + _resourceHandleDestructorHook.Dispose(); + _incRefHook.Dispose(); + _decRefHook.Dispose(); } #region GetResource @@ -33,24 +57,15 @@ public unsafe class ResourceHook : IDisposable /// The resource type. Should not generally be changed. /// The resource hash. Should generally fit to the path. /// The path of the requested resource. - /// Mainly used for SCD streaming. + /// Mainly used for SCD streaming, can be null. /// Whether to request the resource synchronously or asynchronously. - public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref ByteString path, - ref GetResourceParameters parameters, ref bool sync); + /// The returned resource handle. If this is not null, calling original will be skipped. + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// /// Subscribers should be exception-safe. - public event GetResourcePreDelegate? GetResourcePre; - - /// - /// The returned resource handle obtained from a resource request. Contains all the other information from the request. - /// - public delegate void GetResourcePostDelegate(ref ResourceHandle handle); - - /// - /// Subscribers should be exception-safe. - public event GetResourcePostDelegate? GetResourcePost; - + public event GetResourcePreDelegate? ResourceRequested; private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); @@ -79,15 +94,34 @@ public unsafe class ResourceHook : IDisposable private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) { - var byteString = new ByteString(path); - GetResourcePre?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref byteString, ref *pGetResParams, ref isSync); - var ret = isSync - ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams) - : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams, isUnk); - GetResourcePost?.Invoke(ref *ret); - return ret; + using var performance = _performance.Measure(PerformanceType.GetResourceHandler); + if (!Utf8GamePath.FromPointer(path, out var gamePath)) + { + Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path."); + return isSync + ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams) + : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + } + + ResourceHandle* returnValue = null; + ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, pGetResParams, ref isSync, + ref returnValue); + if (returnValue != null) + return returnValue; + + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk); } + /// Call the original GetResource function. + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path, + GetResourceParameters* resourceParameters = null, bool unk = false) + => sync + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters) + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, + unk); + #endregion private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); @@ -96,9 +130,8 @@ public unsafe class ResourceHook : IDisposable /// Invoked before a resource handle reference count is incremented. /// The resource handle. - /// Whether to call original after the event has run. - /// The return value to use if not calling original. - public delegate void ResourceHandleIncRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref nint returnValue); + /// The return value to use, setting this value will skip calling original. + public delegate void ResourceHandleIncRefDelegate(ResourceHandle* handle, ref nint? returnValue); /// /// @@ -106,21 +139,19 @@ public unsafe class ResourceHook : IDisposable /// public event ResourceHandleIncRefDelegate? ResourceHandleIncRef; - public nint IncRef(ref ResourceHandle handle) - { - fixed (ResourceHandle* ptr = &handle) - { - return _incRefHook.Original(ptr); - } - } + /// + /// Call the game function that increases the reference counter of a resource handle. + /// + public nint IncRef(ResourceHandle* handle) + => _incRefHook.OriginalDisposeSafe(handle); private readonly Hook _incRefHook; + private nint ResourceHandleIncRefDetour(ResourceHandle* handle) { - var callOriginal = true; - var ret = IntPtr.Zero; - ResourceHandleIncRef?.Invoke(ref *handle, ref callOriginal, ref ret); - return callOriginal ? _incRefHook.Original(handle) : ret; + nint? ret = null; + ResourceHandleIncRef?.Invoke(handle, ref ret); + return ret ?? _incRefHook.OriginalDisposeSafe(handle); } #endregion @@ -129,9 +160,8 @@ public unsafe class ResourceHook : IDisposable /// Invoked before a resource handle reference count is decremented. /// The resource handle. - /// Whether to call original after the event has run. - /// The return value to use if not calling original. - public delegate void ResourceHandleDecRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref byte returnValue); + /// The return value to use, setting this value will skip calling original. + public delegate void ResourceHandleDecRefDelegate(ResourceHandle* handle, ref byte? returnValue); /// /// @@ -139,29 +169,29 @@ public unsafe class ResourceHook : IDisposable /// public event ResourceHandleDecRefDelegate? ResourceHandleDecRef; - public byte DecRef(ref ResourceHandle handle) - { - fixed (ResourceHandle* ptr = &handle) - { - return _incRefHook.Original(ptr); - } - } + /// + /// Call the original game function that decreases the reference counter of a resource handle. + /// + public byte DecRef(ResourceHandle* handle) + => _decRefHook.OriginalDisposeSafe(handle); private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle); private readonly Hook _decRefHook; + private byte ResourceHandleDecRefDetour(ResourceHandle* handle) { - var callOriginal = true; - var ret = byte.MinValue; - ResourceHandleDecRef?.Invoke(ref *handle, ref callOriginal, ref ret); - return callOriginal ? _decRefHook!.Original(handle) : ret; + byte? ret = null; + ResourceHandleDecRef?.Invoke(handle, ref ret); + return ret ?? _decRefHook.OriginalDisposeSafe(handle); } #endregion + #region Destructor + /// Invoked before a resource handle is destructed. /// The resource handle. - public delegate void ResourceHandleDtorDelegate(ref ResourceHandle handle); + public delegate void ResourceHandleDtorDelegate(ResourceHandle* handle); /// /// @@ -174,8 +204,8 @@ public unsafe class ResourceHook : IDisposable private nint ResourceHandleDestructorDetour(ResourceHandle* handle) { - ResourceHandleDestructor?.Invoke(ref *handle); - return _resourceHandleDestructorHook!.Original(handle); + ResourceHandleDestructor?.Invoke(handle); + return _resourceHandleDestructorHook.OriginalDisposeSafe(handle); } #endregion diff --git a/Penumbra/Interop/Loader/TexMdlService.cs b/Penumbra/Interop/Loader/TexMdlService.cs new file mode 100644 index 00000000..c60c7b79 --- /dev/null +++ b/Penumbra/Interop/Loader/TexMdlService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Loader; + +public unsafe class TexMdlService +{ + /// Custom ulong flag to signal our files as opposed to SE files. + public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); + + /// + /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, + /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. + /// + public IReadOnlySet CustomFileCrc + => _customFileCrc; + + public TexMdlService() + { + SignatureHelper.Initialise(this); + _checkFileStateHook.Enable(); + _loadTexFileExternHook.Enable(); + _loadMdlFileExternHook.Enable(); + } + + /// Add CRC64 if the given file is a model or texture file and has an associated path. + public void AddCrc(ResourceType type, FullPath? path) + { + if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex) + _customFileCrc.Add(path.Value.Crc64); + } + + /// Add a fixed CRC64 value. + public void AddCrc(ulong crc64) + => _customFileCrc.Add(crc64); + + public void Dispose() + { + _checkFileStateHook.Dispose(); + _loadTexFileExternHook.Dispose(); + _loadMdlFileExternHook.Dispose(); + } + + private readonly HashSet _customFileCrc = new(); + + private delegate IntPtr CheckFileStatePrototype(IntPtr unk1, ulong crc64); + + [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] + private readonly Hook _checkFileStateHook = null!; + + /// + /// The function that checks a files CRC64 to determine whether it is 'protected'. + /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. + /// + private IntPtr CheckFileStateDetour(IntPtr ptr, ulong crc64) + => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); + + + private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3); + + /// We use the local functions for our own files in the extern hook. + [Signature(Sigs.LoadTexFileLocal)] + private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!; + + private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2); + + /// We use the local functions for our own files in the extern hook. + [Signature(Sigs.LoadMdlFileLocal)] + private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; + + + private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4); + + [Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] + private readonly Hook _loadTexFileExternHook = null!; + + /// We hook the extern functions to just return the local one if given the custom flag as last argument. + private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr) + => ptr.Equals(CustomFileFlag) + ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) + : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); + + public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3); + + + [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] + private readonly Hook _loadMdlFileExternHook = null!; + + /// We hook the extern functions to just return the local one if given the custom flag as last argument. + private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr) + => ptr.Equals(CustomFileFlag) + ? _loadMdlFileLocal.Invoke(resourceHandle, unk1, unk2) + : _loadMdlFileExternHook.Original(resourceHandle, unk1, unk2, ptr); +} diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs index 84063fa6..ac732d7c 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -18,8 +18,6 @@ public unsafe class ResidentResourceManager [Signature( Sigs.UnloadPlayerResources )] public readonly ResidentResourceDelegate UnloadPlayerResources = null!; - - public Structs.ResidentResourceManager* Address => *_residentResourceManagerAddress; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 7f0ee266..edfe566b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -144,7 +144,7 @@ public unsafe partial class PathResolver { _lastCreatedCollection = IdentifyCollection(LastGameObject, false); // Change the transparent or 1.0 Decal if necessary. - var decal = new CharacterUtility.DecalReverter(_lastCreatedCollection.ModCollection, UsesDecal(a, c)); + var decal = new CharacterUtility.DecalReverter(Penumbra.ResourceService, _lastCreatedCollection.ModCollection, UsesDecal(a, c)); // Change the rsp parameters. meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal); try diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index c3678af5..2017b1d2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -194,7 +194,7 @@ public unsafe partial class PathResolver _inChangeCustomize = true; var resolveData = GetResolveData( human ); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); - using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); + using var decals = new CharacterUtility.DecalReverter( Penumbra.ResourceService, resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); var ret = _changeCustomize.Original( human, data, skipEquipment ); _inChangeCustomize = false; return ret; diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 016ca38d..37059bf6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; @@ -22,28 +23,28 @@ public unsafe partial class PathResolver // Materials and avfx do contain their own paths to textures and shader packages or atex respectively. // Those are loaded synchronously. // Thus, we need to ensure the correct files are loaded when a material is loaded. - public class SubfileHelper : IDisposable, IReadOnlyCollection< KeyValuePair< IntPtr, ResolveData > > + public class SubfileHelper : IDisposable, IReadOnlyCollection> { private readonly ResourceLoader _loader; private readonly GameEventManager _events; - private readonly ThreadLocal< ResolveData > _mtrlData = new(() => ResolveData.Invalid); - private readonly ThreadLocal< ResolveData > _avfxData = new(() => ResolveData.Invalid); + private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); + private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); - private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); + private readonly ConcurrentDictionary _subFileCollection = new(); - public SubfileHelper( ResourceLoader loader, GameEventManager events ) + public SubfileHelper(ResourceLoader loader, GameEventManager events) { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); _loader = loader; _events = events; } // Check specifically for shpk and tex files whether we are currently in a material load. - public bool HandleSubFiles( ResourceType type, out ResolveData collection ) + public bool HandleSubFiles(ResourceType type, out ResolveData collection) { - switch( type ) + switch (type) { case ResourceType.Tex when _mtrlData.Value.Valid: case ResourceType.Shpk when _mtrlData.Value.Valid: @@ -62,22 +63,20 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - public static void HandleCollection( ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, ResolveData) data ) + public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, ResolveData) data) { - if( nonDefault ) - { - switch( type ) + if (nonDefault) + switch (type) { case ResourceType.Mtrl: case ResourceType.Avfx: - var fullPath = new FullPath( $"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}" ); - data = ( fullPath, resolveData ); + var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}"); + data = (fullPath, resolveData); return; } - } - data = ( resolved, resolveData ); + data = (resolved, resolveData); } public void Enable() @@ -85,9 +84,8 @@ public unsafe partial class PathResolver _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); _apricotResourceLoadHook.Enable(); - _loader.ResourceLoadCustomization += SubfileLoadHandler; - _loader.ResourceLoaded += SubfileContainerRequested; - _events.ResourceHandleDestructor += ResourceDestroyed; + _loader.ResourceLoaded += SubfileContainerRequested; + _events.ResourceHandleDestructor += ResourceDestroyed; } public void Disable() @@ -95,9 +93,8 @@ public unsafe partial class PathResolver _loadMtrlShpkHook.Disable(); _loadMtrlTexHook.Disable(); _apricotResourceLoadHook.Disable(); - _loader.ResourceLoadCustomization -= SubfileLoadHandler; - _loader.ResourceLoaded -= SubfileContainerRequested; - _events.ResourceHandleDestructor -= ResourceDestroyed; + _loader.ResourceLoaded -= SubfileContainerRequested; + _events.ResourceHandleDestructor -= ResourceDestroyed; } public void Dispose() @@ -108,105 +105,77 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Dispose(); } - private void SubfileContainerRequested( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) + private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData) { - switch( handle->FileType ) + switch (handle->FileType) { case ResourceType.Mtrl: case ResourceType.Avfx: - if( handle->FileSize == 0 ) - { - _subFileCollection[ ( IntPtr )handle ] = resolveData; - } + if (handle->FileSize == 0) + _subFileCollection[(nint)handle] = resolveData; break; } } - private void ResourceDestroyed( ResourceHandle* handle ) - => _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); + private void ResourceDestroyed(ResourceHandle* handle) + => _subFileCollection.TryRemove((IntPtr)handle, out _); - // We need to set the correct collection for the actual material path that is loaded - // before actually loading the file. - public static bool SubfileLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) + private delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle); + + [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))] + private readonly Hook _loadMtrlTexHook = null!; + + private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) { - switch( fileDescriptor->ResourceHandle->FileType ) - { - case ResourceType.Mtrl: - // Force isSync = true for this call. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. - ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true ); - return true; - case ResourceType.Avfx: - // Do nothing special right now. - ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); - return true; - - default: - ret = 0; - return false; - } - } - - private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); - - [Signature( Sigs.LoadMtrlTex, DetourName = nameof( LoadMtrlTexDetour ) )] - private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!; - - private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadTextures ); + using var performance = Penumbra.Performance.Measure(PerformanceType.LoadTextures); var old = _mtrlData.Value; - _mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); - var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); + _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); + var ret = _loadMtrlTexHook.Original(mtrlResourceHandle); _mtrlData.Value = old; return ret; } - [Signature( Sigs.LoadMtrlShpk, DetourName = nameof( LoadMtrlShpkDetour ) )] - private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!; + [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))] + private readonly Hook _loadMtrlShpkHook = null!; - private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) + private byte LoadMtrlShpkDetour(IntPtr mtrlResourceHandle) { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadShaders ); + using var performance = Penumbra.Performance.Measure(PerformanceType.LoadShaders); var old = _mtrlData.Value; - _mtrlData.Value = LoadFileHelper( mtrlResourceHandle ); - var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); + _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); + var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle); _mtrlData.Value = old; return ret; } - private ResolveData LoadFileHelper( IntPtr resourceHandle ) + private ResolveData LoadFileHelper(IntPtr resourceHandle) { - if( resourceHandle == IntPtr.Zero ) - { + if (resourceHandle == IntPtr.Zero) return ResolveData.Invalid; - } - return _subFileCollection.TryGetValue( resourceHandle, out var c ) ? c : ResolveData.Invalid; + return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid; } - private delegate byte ApricotResourceLoadDelegate( IntPtr handle, IntPtr unk1, byte unk2 ); + private delegate byte ApricotResourceLoadDelegate(IntPtr handle, IntPtr unk1, byte unk2); - [Signature( Sigs.ApricotResourceLoad, DetourName = nameof( ApricotResourceLoadDetour ) )] - private readonly Hook< ApricotResourceLoadDelegate > _apricotResourceLoadHook = null!; + [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))] + private readonly Hook _apricotResourceLoadHook = null!; - private byte ApricotResourceLoadDetour( IntPtr handle, IntPtr unk1, byte unk2 ) + private byte ApricotResourceLoadDetour(IntPtr handle, IntPtr unk1, byte unk2) { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadApricotResources ); + using var performance = Penumbra.Performance.Measure(PerformanceType.LoadApricotResources); var old = _avfxData.Value; - _avfxData.Value = LoadFileHelper( handle ); - var ret = _apricotResourceLoadHook.Original( handle, unk1, unk2 ); + _avfxData.Value = LoadFileHelper(handle); + var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2); _avfxData.Value = old; return ret; } - public IEnumerator< KeyValuePair< IntPtr, ResolveData > > GetEnumerator() + public IEnumerator> GetEnumerator() => _subFileCollection.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -221,4 +190,4 @@ public unsafe partial class PathResolver internal ResolveData AvfxData => _avfxData.Value; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 140804ce..2d94516d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; @@ -54,7 +55,7 @@ public partial class PathResolver : IDisposable } // The modified resolver that handles game path resolving. - private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data) + public (FullPath?, ResolveData) CharacterResolver(Utf8GamePath gamePath, ResourceType type) { using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver); // Check if the path was marked for a specific collection, @@ -77,8 +78,8 @@ public partial class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out data); - return true; + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); + return pair; } public void Enable() @@ -95,7 +96,6 @@ public partial class PathResolver : IDisposable _meta.Enable(); _subFiles.Enable(); - _loader.ResolvePathCustomization += CharacterResolver; Penumbra.Log.Debug("Character Path Resolver enabled."); } @@ -113,7 +113,6 @@ public partial class PathResolver : IDisposable _meta.Disable(); _subFiles.Disable(); - _loader.ResolvePathCustomization -= CharacterResolver; Penumbra.Log.Debug("Character Path Resolver disabled."); } diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 3ee984b6..cc09a13d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using FFXIVClientStructs.FFXIV.Client.System.Resource; +using System.Diagnostics.CodeAnalysis; using OtterGui.Filesystem; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Meta.Manager; @@ -15,7 +12,6 @@ public partial class MetaManager { private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); private readonly List< ImcManipulation > _imcManipulations = new(); - private static int _imcManagerCount; public void SetImcFiles() { @@ -132,52 +128,11 @@ public partial class MetaManager _imcFiles.Clear(); _imcManipulations.Clear(); - RestoreImcDelegate(); - } - - private static unsafe void SetupImcDelegate() - { - if( _imcManagerCount++ == 0 ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; - } - } - - private static unsafe void RestoreImcDelegate() - { - if( --_imcManagerCount == 0 ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; - } } private FullPath CreateImcPath( Utf8GamePath path ) => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); - - private static unsafe bool ImcLoadHandler( ByteString split, ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) - { - ret = 0; - if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc ) - { - return false; - } - - Penumbra.Log.Verbose( $"Using ImcLoadHandler for path {path}." ); - ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - - var lastUnderscore = split.LastIndexOf( ( byte )'_' ); - var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); - if( ( Penumbra.TempCollections.CollectionByName( name, out var collection ) - || Penumbra.CollectionManager.ByName( name, out collection ) ) - && collection.HasCache - && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) - { - Penumbra.Log.Debug( $"Loaded {path} from file and replaced with IMC from collection {collection.AnonymizedName}." ); - file.Replace( fileDescriptor->ResourceHandle ); - } - - return true; - } + public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + => _imcFiles.TryGetValue(path, out file); } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index a37af335..beda80bc 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -35,7 +35,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public MetaManager( ModCollection collection ) { _collection = collection; - SetupImcDelegate(); if( !Penumbra.CharacterUtility.Ready ) { Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0a85e113..e34bad28 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -83,32 +83,37 @@ public class Penumbra : IDalamudPlugin private readonly PenumbraNew _tmp; public static ItemData ItemData { get; private set; } = null!; + + // TODO + public static ResourceManagerService ResourceManagerService { get; private set; } = null!; + public static CharacterResolver CharacterResolver { get; private set; } = null!; + public static ResourceService ResourceService { get; private set; } = null!; public Penumbra(DalamudPluginInterface pluginInterface) { Log = PenumbraNew.Log; - _tmp = new PenumbraNew(pluginInterface); - Performance = _tmp.Services.GetRequiredService(); - ValidityChecker = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - ItemData = _tmp.Services.GetRequiredService().AwaitedService; - Dalamud = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); try { - ResourceLoader = new ResourceLoader(this); - ResourceLoader.EnableHooks(); - _resourceWatcher = new ResourceWatcher(ResourceLoader); - ResidentResources = new ResidentResourceManager(); + _tmp = new PenumbraNew(pluginInterface); + Performance = _tmp.Services.GetRequiredService(); + ValidityChecker = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + ItemData = _tmp.Services.GetRequiredService().AwaitedService; + Dalamud = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); + ResidentResources = _tmp.Services.GetRequiredService(); + + ResourceManagerService = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService().Measure(StartTimeType.Mods, () => { ModManager = new Mod.Manager(Config.ModDirectory); @@ -126,19 +131,20 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); + ResourceService = _tmp.Services.GetRequiredService(); + ResourceLoader = new ResourceLoader(ResourceService, _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService()); PathResolver = new PathResolver(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), ResourceLoader); + CharacterResolver = new CharacterResolver(Config, CollectionManager, TempCollections, ResourceLoader, PathResolver); + + _resourceWatcher = new ResourceWatcher(Config, ResourceService, ResourceLoader); SetupInterface(); if (Config.EnableMods) { - ResourceLoader.EnableReplacements(); PathResolver.Enable(); } - if (Config.DebugMode) - ResourceLoader.EnableDebug(); - using (var tApi = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api)) { Api = new PenumbraApi(_tmp.Services.GetRequiredService(), this); @@ -171,7 +177,7 @@ public class Penumbra : IDalamudPlugin { using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); var changelog = ConfigWindow.CreateChangelog(); - var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) + var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) { IsOpen = Config.DebugMode, }; @@ -225,7 +231,6 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = enabled; if (enabled) { - ResourceLoader.EnableReplacements(); PathResolver.Enable(); if (CharacterUtility.Ready) { @@ -236,7 +241,6 @@ public class Penumbra : IDalamudPlugin } else { - ResourceLoader.DisableReplacements(); PathResolver.Disable(); if (CharacterUtility.Ready) { @@ -293,7 +297,7 @@ public class Penumbra : IDalamudPlugin ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); - PathResolver?.Dispose(); + CharacterResolver?.Dispose(); // disposes PathResolver, TODO _resourceWatcher?.Dispose(); ResourceLoader?.Dispose(); GameEvents?.Dispose(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 18da6c62..fc1d5e84 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -8,8 +8,10 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop; +using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Services; +using Penumbra.UI.Classes; using Penumbra.Util; namespace Penumbra; @@ -53,9 +55,15 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - - + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + // Add Configuration services.AddTransient() .AddSingleton(); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index e5b0fe84..0a59b517 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -61,8 +61,6 @@ public partial class ConfigWindow DrawDebugTabGeneral(); DrawPerformanceTab(); ImGui.NewLine(); - DrawDebugTabReplacedResources(); - ImGui.NewLine(); DrawPathResolverDebug(); ImGui.NewLine(); DrawActorsDebug(); @@ -134,53 +132,6 @@ public partial class ConfigWindow Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); } - // Draw all resources currently replaced by Penumbra and (if existing) the resources they replace. - // Resources are collected by iterating through the - private static unsafe void DrawDebugTabReplacedResources() - { - if( !ImGui.CollapsingHeader( "Replaced Resources" ) ) - { - return; - } - - Penumbra.ResourceLoader.UpdateDebugInfo(); - - if( Penumbra.ResourceLoader.DebugList.Count == 0 ) - { - return; - } - - using var table = Table( "##ReplacedResources", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - foreach( var data in Penumbra.ResourceLoader.DebugList.Values.ToArray() ) - { - if( data.ManipulatedPath.Crc64 == 0 ) - { - continue; - } - - var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; - var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; - ImGui.TableNextColumn(); - ImGui.TextUnformatted( data.ManipulatedPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( refCountManip.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( data.OriginalPath.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( refCountOrig.ToString() ); - } - } - private static unsafe void DrawActorsDebug() { if( !ImGui.CollapsingHeader( "Actors" ) ) @@ -635,7 +586,7 @@ public partial class ConfigWindow return; } - ResourceLoader.IterateResources( ( _, r ) => + Penumbra.ResourceManagerService.IterateResources( ( _, r ) => { if( r->RefCount < 10000 ) { diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs index ecb294b5..b6d2e46c 100644 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -10,7 +10,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Interop.Loader; using Penumbra.Services; using Penumbra.String.Classes; @@ -46,13 +45,13 @@ public partial class ConfigWindow unsafe { - ResourceLoader.IterateGraphs( DrawCategoryContainer ); + Penumbra.ResourceManagerService.IterateGraphs( DrawCategoryContainer ); } ImGui.NewLine(); unsafe { - ImGui.TextUnformatted( $"Static Address: 0x{( ulong )ResourceLoader.ResourceManager:X} (+0x{( ulong )ResourceLoader.ResourceManager - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); - ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )*ResourceLoader.ResourceManager:X}" ); + ImGui.TextUnformatted( $"Static Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress:X} (+0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); + ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManager:X}" ); } } @@ -82,7 +81,7 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); ImGui.TableHeadersRow(); - ResourceLoader.IterateResourceMap( map, ( hash, r ) => + Penumbra.ResourceManagerService.IterateResourceMap( map, ( hash, r ) => { // Filter unwanted names. if( _resourceManagerFilter.Length != 0 @@ -129,7 +128,7 @@ public partial class ConfigWindow if( tree ) { SetTableWidths(); - ResourceLoader.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); + Penumbra.ResourceManagerService.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index 8a539b4f..56a757b3 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -66,15 +66,6 @@ public partial class ConfigWindow var tmp = Penumbra.Config.DebugMode; if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) { - if( tmp ) - { - Penumbra.ResourceLoader.EnableDebug(); - } - else - { - Penumbra.ResourceLoader.DisableDebug(); - } - Penumbra.Config.DebugMode = tmp; Penumbra.Config.Save(); } @@ -95,11 +86,11 @@ public partial class ConfigWindow + "You usually should not need to do this." ); } - private static void DrawReloadFontsButton() + private void DrawReloadFontsButton() { - if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !FontReloader.Valid ) ) + if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid ) ) { - FontReloader.Reload(); + _fontReloader.Reload(); } } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index e3037d68..d4aa093f 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,6 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Interop; using Penumbra.Services; using Penumbra.UI.Classes; @@ -22,11 +23,14 @@ public partial class ConfigWindow { public const int RootDirectoryMaxLength = 64; private readonly ConfigWindow _window; - + private readonly FontReloader _fontReloader; public ReadOnlySpan Label => "Settings"u8; - public SettingsTab( ConfigWindow window ) - => _window = window; + public SettingsTab( ConfigWindow window, FontReloader fontReloader ) + { + _window = window; + _fontReloader = fontReloader; + } public void DrawHeader() { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index c925ce16..2786a86b 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -7,6 +7,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; +using Penumbra.Interop; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -35,14 +36,14 @@ public sealed partial class ConfigWindow : Window, IDisposable public void SelectMod(Mod mod) => _selector.SelectByValue(mod); - public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher) + public ConfigWindow(CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, Penumbra penumbra, ResourceWatcher watcher) : base(GetLabel()) { _penumbra = penumbra; _resourceWatcher = watcher; ModEditPopup = new ModEditWindow(communicator); - _settingsTab = new SettingsTab(this); + _settingsTab = new SettingsTab(this, fontReloader); _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); _modPanel = new ModPanel(this); _modsTab = new ModsTab(_selector, _modPanel, _penumbra); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 0d344e63..e73bba63 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; using Penumbra.String; @@ -19,38 +23,38 @@ public partial class ResourceWatcher : IDisposable, ITab { public const int DefaultMaxEntries = 1024; - private readonly ResourceLoader _loader; - private readonly List< Record > _records = new(); - private readonly ConcurrentQueue< Record > _newRecords = new(); - private readonly Table _table; - private bool _writeToLog; - private bool _isEnabled; - private string _logFilter = string.Empty; - private Regex? _logRegex; - private int _maxEntries; - private int _newMaxEntries; + private readonly Configuration _config; + private readonly ResourceService _resources; + private readonly ResourceLoader _loader; + private readonly List _records = new(); + private readonly ConcurrentQueue _newRecords = new(); + private readonly Table _table; + private string _logFilter = string.Empty; + private Regex? _logRegex; + private int _newMaxEntries; - public unsafe ResourceWatcher( ResourceLoader loader ) + public unsafe ResourceWatcher(Configuration config, ResourceService resources, ResourceLoader loader) { - _loader = loader; - _table = new Table( _records ); - _loader.ResourceRequested += OnResourceRequested; - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; - UpdateFilter( Penumbra.Config.ResourceLoggingFilter, false ); - _writeToLog = Penumbra.Config.EnableResourceLogging; - _isEnabled = Penumbra.Config.EnableResourceWatcher; - _maxEntries = Penumbra.Config.MaxResourceWatcherRecords; - _newMaxEntries = _maxEntries; + _config = config; + _resources = resources; + _loader = loader; + _table = new Table(_records); + _resources.ResourceRequested += OnResourceRequested; + _resources.ResourceHandleDestructor += OnResourceDestroyed; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.FileLoaded += OnFileLoaded; + UpdateFilter(_config.ResourceLoggingFilter, false); + _newMaxEntries = _config.MaxResourceWatcherRecords; } public unsafe void Dispose() { Clear(); _records.TrimExcess(); - _loader.ResourceRequested -= OnResourceRequested; - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; + _resources.ResourceRequested -= OnResourceRequested; + _resources.ResourceHandleDestructor -= OnResourceDestroyed; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.FileLoaded -= OnFileLoaded; } private void Clear() @@ -67,183 +71,195 @@ public partial class ResourceWatcher : IDisposable, ITab { UpdateRecords(); - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); - if( ImGui.Checkbox( "Enable", ref _isEnabled ) ) + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2); + var isEnabled = _config.EnableResourceWatcher; + if (ImGui.Checkbox("Enable", ref isEnabled)) { - Penumbra.Config.EnableResourceWatcher = _isEnabled; + Penumbra.Config.EnableResourceWatcher = isEnabled; Penumbra.Config.Save(); } ImGui.SameLine(); DrawMaxEntries(); ImGui.SameLine(); - if( ImGui.Button( "Clear" ) ) - { + if (ImGui.Button("Clear")) Clear(); + + ImGui.SameLine(); + var onlyMatching = _config.OnlyAddMatchingResources; + if (ImGui.Checkbox("Store Only Matching", ref onlyMatching)) + { + Penumbra.Config.OnlyAddMatchingResources = onlyMatching; + Penumbra.Config.Save(); } ImGui.SameLine(); - if( ImGui.Checkbox( "Write to Log", ref _writeToLog ) ) + var writeToLog = _config.EnableResourceLogging; + if (ImGui.Checkbox("Write to Log", ref writeToLog)) { - Penumbra.Config.EnableResourceLogging = _writeToLog; + Penumbra.Config.EnableResourceLogging = writeToLog; Penumbra.Config.Save(); } ImGui.SameLine(); DrawFilterInput(); - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2 ); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2); - _table.Draw( ImGui.GetTextLineHeightWithSpacing() ); + _table.Draw(ImGui.GetTextLineHeightWithSpacing()); } private void DrawFilterInput() { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); var tmp = _logFilter; var invalidRegex = _logRegex == null && _logFilter.Length > 0; - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex ); - if( ImGui.InputTextWithHint( "##logFilter", "If path matches this Regex...", ref tmp, 256 ) ) - { - UpdateFilter( tmp, true ); - } + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex); + if (ImGui.InputTextWithHint("##logFilter", "If path matches this Regex...", ref tmp, 256)) + UpdateFilter(tmp, true); } - private void UpdateFilter( string newString, bool config ) + private void UpdateFilter(string newString, bool config) { - if( newString == _logFilter ) - { + if (newString == _logFilter) return; - } _logFilter = newString; try { - _logRegex = new Regex( _logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase ); + _logRegex = new Regex(_logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); } catch { _logRegex = null; } - if( config ) + if (config) { Penumbra.Config.ResourceLoggingFilter = newString; Penumbra.Config.Save(); } } - private bool FilterMatch( ByteString path, out string match ) + private bool FilterMatch(ByteString path, out string match) { match = path.ToString(); - return _logFilter.Length == 0 || ( _logRegex?.IsMatch( match ) ?? false ) || match.Contains( _logFilter, StringComparison.OrdinalIgnoreCase ); + return _logFilter.Length == 0 || (_logRegex?.IsMatch(match) ?? false) || match.Contains(_logFilter, StringComparison.OrdinalIgnoreCase); } private void DrawMaxEntries() { - ImGui.SetNextItemWidth( 80 * ImGuiHelpers.GlobalScale ); - ImGui.InputInt( "Max. Entries", ref _newMaxEntries, 0, 0 ); + ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("Max. Entries", ref _newMaxEntries, 0, 0); var change = ImGui.IsItemDeactivatedAfterEdit(); - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { change = true; _newMaxEntries = DefaultMaxEntries; } - if( _maxEntries != DefaultMaxEntries && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( $"CTRL + Right-Click to reset to default {DefaultMaxEntries}." ); - } + var maxEntries = _config.MaxResourceWatcherRecords; + if (maxEntries != DefaultMaxEntries && ImGui.IsItemHovered()) + ImGui.SetTooltip($"CTRL + Right-Click to reset to default {DefaultMaxEntries}."); - if( !change ) - { + if (!change) return; - } - _newMaxEntries = Math.Max( 16, _newMaxEntries ); - if( _newMaxEntries != _maxEntries ) + _newMaxEntries = Math.Max(16, _newMaxEntries); + if (_newMaxEntries != maxEntries) { - _maxEntries = _newMaxEntries; - Penumbra.Config.MaxResourceWatcherRecords = _maxEntries; + _config.MaxResourceWatcherRecords = _newMaxEntries; Penumbra.Config.Save(); - _records.RemoveRange( 0, _records.Count - _maxEntries ); + if (_newMaxEntries > _records.Count) + _records.RemoveRange(0, _records.Count - _newMaxEntries); } } private void UpdateRecords() { var count = _newRecords.Count; - if( count > 0 ) + if (count > 0) { - while( _newRecords.TryDequeue( out var rec ) && count-- > 0 ) - { - _records.Add( rec ); - } + while (_newRecords.TryDequeue(out var rec) && count-- > 0) + _records.Add(rec); - if( _records.Count > _maxEntries ) - { - _records.RemoveRange( 0, _records.Count - _maxEntries ); - } + if (_records.Count > _config.MaxResourceWatcherRecords) + _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); _table.Reset(); } } - private void OnResourceRequested( Utf8GamePath data, bool synchronous ) + private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if( _writeToLog && FilterMatch( data.Path, out var match ) ) - { - Penumbra.Log.Information( $"[ResourceLoader] [REQ] {match} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); - } + if (_config.EnableResourceLogging && FilterMatch(path.Path, out var match)) + Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); - if( _isEnabled ) + if (_config.EnableResourceWatcher) { - _newRecords.Enqueue( Record.CreateRequest( data.Path, synchronous ) ); + var record = Record.CreateRequest(path.Path, sync); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } } - private unsafe void OnResourceLoaded( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data ) + private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) { - if( _writeToLog ) + if (_config.EnableResourceLogging) { - var log = FilterMatch( path.Path, out var name ); + var log = FilterMatch(path.Path, out var name); var name2 = string.Empty; - if( manipulatedPath != null ) - { - log |= FilterMatch( manipulatedPath.Value.InternalName, out name2 ); - } + if (manipulatedPath != null) + log |= FilterMatch(manipulatedPath.Value.InternalName, out name2); - if( log ) + if (log) { var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; Penumbra.Log.Information( - $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " ); + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) "); } } - if( _isEnabled ) + if (_config.EnableResourceWatcher) { var record = manipulatedPath == null - ? Record.CreateDefaultLoad( path.Path, handle, data.ModCollection ) - : Record.CreateLoad( path.Path, manipulatedPath.Value.InternalName, handle, data.ModCollection ); - _newRecords.Enqueue( record ); + ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection) + : Record.CreateLoad(path.Path, manipulatedPath.Value.InternalName, handle, + data.ModCollection); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } } - private unsafe void OnFileLoaded( ResourceHandle* resource, ByteString path, bool success, bool custom ) + private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) { - if( _writeToLog && FilterMatch( path, out var match ) ) - { + if (_config.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( - $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." ); - } + $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); - if( _isEnabled ) + if (_config.EnableResourceWatcher) { - _newRecords.Enqueue( Record.CreateFileLoad( path, resource, success, custom ) ); + var record = Record.CreateFileLoad(path, resource, success, custom); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } } -} \ No newline at end of file + + private unsafe void OnResourceDestroyed(ResourceHandle* resource) + { + if (_config.EnableResourceLogging && FilterMatch(resource->FileName(), out var match)) + Penumbra.Log.Information( + $"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}."); + + if (_config.EnableResourceWatcher) + { + var record = Record.CreateDestruction(resource); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + } +} From 3fc724b7ee2b32bfeb86437106e04e69d3913578 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Mar 2023 13:19:23 +0100 Subject: [PATCH 0806/2451] Some further stuff. --- Penumbra/Interop/MetaFileManager.cs | 37 ------------------- .../Interop/Resolver/CutsceneCharacters.cs | 3 +- .../Resolver/IdentifiedCollectionCache.cs | 1 + .../Interop/Resolver/PathResolver.Subfiles.cs | 1 + Penumbra/Interop/Resolver/PathResolver.cs | 1 + .../Interop/{ => Services}/FontReloader.cs | 16 ++++---- .../{ => Services}/GameEventManager.cs | 10 ++--- Penumbra/Interop/Services/MetaFileManager.cs | 37 +++++++++++++++++++ .../{ => Services}/ResidentResourceManager.cs | 22 +++++------ Penumbra/Mods/Manager/ModOptionChangeType.cs | 12 +++--- Penumbra/Penumbra.cs | 5 ++- Penumbra/PenumbraNew.cs | 1 + Penumbra/Services/FilenameService.cs | 27 +++++++++++++- Penumbra/UI/ConfigWindow.SettingsTab.cs | 2 +- Penumbra/UI/ConfigWindow.cs | 4 +- 15 files changed, 106 insertions(+), 73 deletions(-) delete mode 100644 Penumbra/Interop/MetaFileManager.cs rename Penumbra/Interop/{ => Services}/FontReloader.cs (68%) rename Penumbra/Interop/{ => Services}/GameEventManager.cs (93%) create mode 100644 Penumbra/Interop/Services/MetaFileManager.cs rename Penumbra/Interop/{ => Services}/ResidentResourceManager.cs (59%) diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs deleted file mode 100644 index c49efa1e..00000000 --- a/Penumbra/Interop/MetaFileManager.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Memory; -using Penumbra.GameData; - -namespace Penumbra.Interop; - -public unsafe class MetaFileManager -{ - public MetaFileManager() - { - SignatureHelper.Initialise( this ); - } - - // Allocate in the games space for file storage. - // We only need this if using any meta file. - [Signature( Sigs.GetFileSpace )] - private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero; - - public IMemorySpace* GetFileSpace() - => ( ( delegate* unmanaged< IMemorySpace* > )_getFileSpaceAddress )(); - - public void* AllocateFileMemory( ulong length, ulong alignment = 0 ) - => GetFileSpace()->Malloc( length, alignment ); - - public void* AllocateFileMemory( int length, int alignment = 0 ) - => AllocateFileMemory( ( ulong )length, ( ulong )alignment ); - - public void* AllocateDefaultMemory( ulong length, ulong alignment = 0 ) - => GetFileSpace()->Malloc( length, alignment ); - - public void* AllocateDefaultMemory( int length, int alignment = 0 ) - => IMemorySpace.GetDefaultSpace()->Malloc( ( ulong )length, ( ulong )alignment ); - - public void Free( IntPtr ptr, int length ) - => IMemorySpace.Free( ( void* )ptr, ( ulong )length ); -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index 9cf765d0..06b4f228 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -4,7 +4,8 @@ using System.Diagnostics; using System.Linq; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; - +using Penumbra.Interop.Services; + namespace Penumbra.Interop.Resolver; public class CutsceneCharacters : IDisposable diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index cad29dc0..7135eebb 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using Penumbra.GameData.Actors; +using Penumbra.Interop.Services; using Penumbra.Services; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index 37059bf6..da976f0b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -11,6 +11,7 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 2d94516d..4050d2f1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String; diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs similarity index 68% rename from Penumbra/Interop/FontReloader.cs rename to Penumbra/Interop/Services/FontReloader.cs index f7e8af27..51f8abc2 100644 --- a/Penumbra/Interop/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -1,11 +1,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.GameData; + +namespace Penumbra.Interop.Services; -namespace Penumbra.Interop; - -// Handle font reloading via game functions. -// May cause a interface flicker while reloading. +/// +/// Handle font reloading via game functions. +/// May cause a interface flicker while reloading. +/// public unsafe class FontReloader { public bool Valid @@ -19,7 +21,7 @@ public unsafe class FontReloader Penumbra.Log.Error("Could not reload fonts, function could not be found."); } - private readonly AtkModule* _atkModule = null!; + private readonly AtkModule* _atkModule = null!; private readonly delegate* unmanaged _reloadFontsFunc = null!; public FontReloader() @@ -36,7 +38,7 @@ public unsafe class FontReloader if (atkModule == null) return; - _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged< AtkModule*, bool, bool, void >*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; + _atkModule = &atkModule->AtkModule; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; } } diff --git a/Penumbra/Interop/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs similarity index 93% rename from Penumbra/Interop/GameEventManager.cs rename to Penumbra/Interop/Services/GameEventManager.cs index 1080e7b8..e6f84f53 100644 --- a/Penumbra/Interop/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -6,8 +6,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Log; using Penumbra.Interop.Structs; - -namespace Penumbra.Interop; + +namespace Penumbra.Interop.Services; public unsafe class GameEventManager : IDisposable { @@ -56,7 +56,7 @@ public unsafe class GameEventManager : IDisposable _characterDtorHook.Original(character); } - public delegate void CharacterDestructorEvent(Character* character); + public delegate void CharacterDestructorEvent(Character* character); public event CharacterDestructorEvent? CharacterDestructor; #endregion @@ -89,7 +89,7 @@ public unsafe class GameEventManager : IDisposable return _copyCharacterHook.Original(target, source, unk); } - public delegate void CopyCharacterEvent(Character* target, Character* source); + public delegate void CopyCharacterEvent(Character* target, Character* source); public event CopyCharacterEvent? CopyCharacter; #endregion @@ -121,7 +121,7 @@ public unsafe class GameEventManager : IDisposable return _resourceHandleDestructorHook!.Original(handle); } - public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); + public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); public event ResourceHandleDestructorEvent? ResourceHandleDestructor; #endregion diff --git a/Penumbra/Interop/Services/MetaFileManager.cs b/Penumbra/Interop/Services/MetaFileManager.cs new file mode 100644 index 00000000..382a6c84 --- /dev/null +++ b/Penumbra/Interop/Services/MetaFileManager.cs @@ -0,0 +1,37 @@ +using System; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using Penumbra.GameData; + +namespace Penumbra.Interop.Services; + +public unsafe class MetaFileManager +{ + public MetaFileManager() + { + SignatureHelper.Initialise(this); + } + + // Allocate in the games space for file storage. + // We only need this if using any meta file. + [Signature(Sigs.GetFileSpace)] + private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero; + + public IMemorySpace* GetFileSpace() + => ((delegate* unmanaged)_getFileSpaceAddress)(); + + public void* AllocateFileMemory(ulong length, ulong alignment = 0) + => GetFileSpace()->Malloc(length, alignment); + + public void* AllocateFileMemory(int length, int alignment = 0) + => AllocateFileMemory((ulong)length, (ulong)alignment); + + public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) + => GetFileSpace()->Malloc(length, alignment); + + public void* AllocateDefaultMemory(int length, int alignment = 0) + => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); + + public void Free(IntPtr ptr, int length) + => IMemorySpace.Free((void*)ptr, (ulong)length); +} \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs similarity index 59% rename from Penumbra/Interop/ResidentResourceManager.cs rename to Penumbra/Interop/Services/ResidentResourceManager.cs index ac732d7c..cd20b889 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -1,21 +1,21 @@ using Dalamud.Utility.Signatures; using Penumbra.GameData; - -namespace Penumbra.Interop; + +namespace Penumbra.Interop.Services; public unsafe class ResidentResourceManager { // A static pointer to the resident resource manager address. - [Signature( Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress )] + [Signature(Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress)] private readonly Structs.ResidentResourceManager** _residentResourceManagerAddress = null; // Some attach and physics files are stored in the resident resource manager, and we need to manually trigger a reload of them to get them to apply. - public delegate void* ResidentResourceDelegate( void* residentResourceManager ); + public delegate void* ResidentResourceDelegate(void* residentResourceManager); - [Signature( Sigs.LoadPlayerResources )] + [Signature(Sigs.LoadPlayerResources)] public readonly ResidentResourceDelegate LoadPlayerResources = null!; - [Signature( Sigs.UnloadPlayerResources )] + [Signature(Sigs.UnloadPlayerResources)] public readonly ResidentResourceDelegate UnloadPlayerResources = null!; public Structs.ResidentResourceManager* Address @@ -23,17 +23,17 @@ public unsafe class ResidentResourceManager public ResidentResourceManager() { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); } // Reload certain player resources by force. public void Reload() { - if( Address != null && Address->NumResources > 0 ) + if (Address != null && Address->NumResources > 0) { - Penumbra.Log.Debug( "Reload of resident resources triggered." ); - UnloadPlayerResources.Invoke( Address ); - LoadPlayerResources.Invoke( Address ); + Penumbra.Log.Debug("Reload of resident resources triggered."); + UnloadPlayerResources.Invoke(Address); + LoadPlayerResources.Invoke(Address); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/ModOptionChangeType.cs b/Penumbra/Mods/Manager/ModOptionChangeType.cs index b4c4947e..3e6ff5c6 100644 --- a/Penumbra/Mods/Manager/ModOptionChangeType.cs +++ b/Penumbra/Mods/Manager/ModOptionChangeType.cs @@ -22,11 +22,13 @@ public enum ModOptionChangeType public static class ModOptionChangeTypeExtension { - // Give information for each type of change. - // If requiresSaving, collections need to be re-saved after this change. - // If requiresReloading, caches need to be manipulated after this change. - // If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. - // Otherwise, caches need to reload the mod itself. + /// + /// Give information for each type of change. + /// If requiresSaving, collections need to be re-saved after this change. + /// If requiresReloading, caches need to be manipulated after this change. + /// If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. + /// Otherwise, caches need to reload the mod itself. + /// public static void HandlingInfo( this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared ) { ( requiresSaving, requiresReloading, wasPrepared ) = type switch diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e34bad28..563e6de9 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -27,9 +27,10 @@ using Penumbra.Interop.Resolver; using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; -using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; - +using Penumbra.Interop.Services; + namespace Penumbra; public class Penumbra : IDalamudPlugin diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index fc1d5e84..429cd5ae 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; +using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 27934da8..0e883149 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; using Dalamud.Plugin; using OtterGui.Filesystem; +using Penumbra.Collections; +using Penumbra.Mods; namespace Penumbra.Services; @@ -25,12 +27,24 @@ public class FilenameService ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); } + /// Obtain the path of a collection file given its name. Returns an empty string if the collection is temporary. + public string CollectionFile(ModCollection collection) + => collection.Index >= 0 ? Path.Combine(CollectionDirectory, $"{collection.Name.RemoveInvalidPathSymbols()}.json") : string.Empty; + + /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) => Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json"); - public string LocalDataFile(string modPath) - => Path.Combine(LocalDataDirectory, $"{modPath}.json"); + /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. + public string LocalDataFile(IModReadable mod) + => mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName); + + /// Obtain the path of the local data file given a mod directory. + public string LocalDataFile(string modDirectory) + => Path.Combine(LocalDataDirectory, $"{Path.GetFileName(modDirectory)}.json"); + + /// Enumerate all collection files. public IEnumerable CollectionFiles { get @@ -40,6 +54,7 @@ public class FilenameService } } + /// Enumerate all local data files. public IEnumerable LocalDataFiles { get @@ -48,4 +63,12 @@ public class FilenameService return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); } } + + /// Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. + public string ModMetaPath(IModReadable mod) + => mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName); + + /// Obtain the path of the meta file given a mod directory. + public string ModMetaPath(string modDirectory) + => Path.Combine(modDirectory, "meta.json"); } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index d4aa093f..e6948019 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,7 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 2786a86b..f212feab 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -7,7 +7,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -35,7 +35,7 @@ public sealed partial class ConfigWindow : Window, IDisposable public void SelectMod(Mod mod) => _selector.SelectByValue(mod); - + public ConfigWindow(CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, Penumbra penumbra, ResourceWatcher watcher) : base(GetLabel()) { From 2670ba52c18089ca834d414d0fdb9a35df93c61e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Mar 2023 14:05:05 +0100 Subject: [PATCH 0807/2451] D --- Penumbra/Services/CommunicatorService.cs | 8 ++++++++ Penumbra/Services/Wrappers.cs | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 47d215c6..b976bef0 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -22,9 +22,17 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + /// + /// Parameter is the type of change. + /// Parameter is the affected mod. + /// Parameter is either null or the old name of the mod. + /// + public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); + public void Dispose() { CollectionChange.Dispose(); TemporaryGlobalModChange.Dispose(); + ModMetaChange.Dispose(); } } diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index 90a3cdd3..e0ab8aae 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -4,7 +4,6 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; using Dalamud.Plugin; -using OtterGui.Classes; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; From dd8c910597075c71d9bceda826441a49854590b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Mar 2023 17:51:05 +0100 Subject: [PATCH 0808/2451] Everything's a service. --- Penumbra/Api/PenumbraApi.cs | 12 +- .../Collections/CollectionManager.Active.cs | 112 ++-- Penumbra/Collections/CollectionManager.cs | 90 +-- .../IndividualCollections.Files.cs | 14 +- .../Collections/ModCollection.Cache.Access.cs | 16 +- Penumbra/Collections/ModCollection.Cache.cs | 8 +- Penumbra/Collections/ModCollection.Changes.cs | 2 +- Penumbra/Collections/ModCollection.File.cs | 169 ++---- .../Collections/ModCollection.Migration.cs | 8 +- Penumbra/Collections/ModCollection.cs | 132 ++--- Penumbra/CommandHandler.cs | 546 +++++++++--------- Penumbra/Configuration.cs | 2 +- Penumbra/Interop/Loader/CharacterResolver.cs | 2 - Penumbra/Interop/ObjectReloader.cs | 370 ------------ Penumbra/Interop/RedrawService.cs | 341 +++++++++++ Penumbra/Interop/Resolver/PathResolver.cs | 1 + Penumbra/Mods/Editor/Mod.Normalization.cs | 12 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 4 +- Penumbra/Mods/Manager/Mod.Manager.Local.cs | 2 - Penumbra/Mods/Manager/Mod.Manager.Options.cs | 354 +++++------- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 8 +- Penumbra/Mods/Manager/Mod.Manager.cs | 42 +- Penumbra/Mods/Mod.BasePath.cs | 3 + Penumbra/Mods/ModFileSystem.cs | 147 +++-- .../Subclasses/Mod.Files.MultiModGroup.cs | 88 ++- Penumbra/Penumbra.cs | 231 +++----- Penumbra/PenumbraNew.cs | 45 +- Penumbra/Services/CommunicatorService.cs | 2 +- Penumbra/Services/ConfigMigrationService.cs | 4 +- Penumbra/Services/FilenameService.cs | 4 +- Penumbra/Services/ValidityChecker.cs | 2 +- Penumbra/UI/Changelog.cs | 386 +++++++++++++ Penumbra/UI/Classes/ItemSwapWindow.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../Classes/ModEditWindow.ShaderPackages.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.cs | 453 ++++++--------- Penumbra/UI/ConfigWindow.Changelog.cs | 350 ----------- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 6 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 4 +- Penumbra/UI/ConfigWindow.cs | 10 +- Penumbra/UI/LaunchButton.cs | 71 ++- Penumbra/UI/WindowSystem.cs | 40 ++ Penumbra/Util/ChatService.cs | 109 ++++ Penumbra/Util/ChatUtil.cs | 54 -- Penumbra/Util/SaveService.cs | 99 ++++ 45 files changed, 2155 insertions(+), 2212 deletions(-) delete mode 100644 Penumbra/Interop/ObjectReloader.cs create mode 100644 Penumbra/Interop/RedrawService.cs create mode 100644 Penumbra/UI/Changelog.cs delete mode 100644 Penumbra/UI/ConfigWindow.Changelog.cs create mode 100644 Penumbra/UI/WindowSystem.cs create mode 100644 Penumbra/Util/ChatService.cs delete mode 100644 Penumbra/Util/ChatUtil.cs create mode 100644 Penumbra/Util/SaveService.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7105ec72..1b581f0f 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -41,12 +41,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _penumbra!.ObjectReloader.GameObjectRedrawn += value; + _penumbra!.RedrawService.GameObjectRedrawn += value; } remove { CheckInitialized(); - _penumbra!.ObjectReloader.GameObjectRedrawn -= value; + _penumbra!.RedrawService.GameObjectRedrawn -= value; } } @@ -206,25 +206,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void RedrawObject(int tableIndex, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(tableIndex, setting); + _penumbra!.RedrawService.RedrawObject(tableIndex, setting); } public void RedrawObject(string name, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(name, setting); + _penumbra!.RedrawService.RedrawObject(name, setting); } public void RedrawObject(GameObject? gameObject, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(gameObject, setting); + _penumbra!.RedrawService.RedrawObject(gameObject, setting); } public void RedrawAll(RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawAll(setting); + _penumbra!.RedrawService.RedrawAll(setting); } public string ResolveDefaultPath(string path) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index fd6da3ad..5f2042dd 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -9,16 +9,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin; using Penumbra.GameData.Actors; -using Penumbra.Util; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { - public sealed partial class Manager + public sealed partial class Manager : ISaveable { public const int Version = 1; @@ -38,8 +37,7 @@ public partial class ModCollection private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. - // TODO - public readonly IndividualCollections Individuals = new(Penumbra.Actors); + public readonly IndividualCollections Individuals; public ModCollection Individual(ActorIdentifier identifier) => Individuals.TryGetCollection(identifier, out var c) ? c : Default; @@ -87,18 +85,12 @@ public partial class ModCollection var newCollection = this[newIdx]; if (newIdx > Empty.Index) - newCollection.CreateCache(); + newCollection.CreateCache(collectionType is CollectionType.Default); switch (collectionType) { case CollectionType.Default: Default = newCollection; - if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) - { - Penumbra.ResidentResources.Reload(); - Default.SetFiles(); - } - break; case CollectionType.Interface: Interface = newCollection; @@ -182,28 +174,25 @@ public partial class ModCollection public void MoveIndividualCollection(int from, int to) { if (Individuals.Move(from, to)) - SaveActiveCollections(); + Penumbra.SaveService.QueueSave(this); } // Obtain the index of a collection by name. private int GetIndexForCollectionName(string name) => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - public static string ActiveCollectionFile(DalamudPluginInterface pi) - => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); - // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections() + private void LoadCollections(FilenameService files) { - var configChanged = !ReadActiveCollections(out var jObject); + var configChanged = !ReadActiveCollections(files, out var jObject); // Load the default collection. var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); var defaultIdx = GetIndexForCollectionName(defaultName); if (defaultIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Default = Empty; @@ -219,7 +208,7 @@ public partial class ModCollection var interfaceIdx = GetIndexForCollectionName(interfaceName); if (interfaceIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Interface = Empty; @@ -235,7 +224,7 @@ public partial class ModCollection var currentIdx = GetIndexForCollectionName(currentName); if (currentIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning); Current = DefaultName; @@ -255,7 +244,8 @@ public partial class ModCollection var idx = GetIndexForCollectionName(typeName); if (idx < 0) { - ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", NotificationType.Warning); configChanged = true; } @@ -271,13 +261,13 @@ public partial class ModCollection // Save any changes and create all required caches. if (configChanged) - SaveActiveCollections(); + Penumbra.SaveService.ImmediateSave(this); } // Migrate ungendered collections to Male and Female for 0.5.9.0. public static void MigrateUngenderedCollections(FilenameService fileNames) { - if (!ReadActiveCollections(out var jObject)) + if (!ReadActiveCollections(fileNames, out var jObject)) return; foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) @@ -314,7 +304,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName(collectionName); if (idx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); dict.Add(player, Empty); @@ -329,48 +319,11 @@ public partial class ModCollection return true; } - public void SaveActiveCollections() - { - Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections), - SaveActiveCollectionsInternal); - } - - internal void SaveActiveCollectionsInternal() - { - // TODO - var file = ActiveCollectionFile(DalamudServices.PluginInterface); - try - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - Penumbra.Log.Verbose("Active Collections saved."); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); - } - } - // Read the active collection file into a jObject. // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(out JObject ret) + private static bool ReadActiveCollections(FilenameService files, out JObject ret) { - // TODO - var file = ActiveCollectionFile(DalamudServices.PluginInterface); + var file = files.ActiveCollectionsFile; if (File.Exists(file)) try { @@ -390,7 +343,7 @@ public partial class ModCollection private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) { if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - SaveActiveCollections(); + Penumbra.SaveService.QueueSave(this); } // Cache handling. Usually recreate caches on the next framework tick, @@ -403,7 +356,7 @@ public partial class ModCollection .Prepend(Default) .Prepend(Interface) .Distinct() - .Select(c => Task.Run(c.CalculateEffectiveFileListInternal)) + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) .ToArray(); Task.WaitAll(tasks); @@ -438,5 +391,32 @@ public partial class ModCollection foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index a07bc5af..f65be1ad 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -8,7 +8,10 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Penumbra.Api; +using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; @@ -16,8 +19,12 @@ public partial class ModCollection { public sealed partial class Manager : IDisposable, IEnumerable { - private readonly Mod.Manager _modManager; - private readonly CommunicatorService _communicator; + private readonly Mod.Manager _modManager; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly Configuration _config; + // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -49,10 +56,16 @@ public partial class ModCollection public IEnumerable GetEnumeratorWithEmpty() => _collections; - public Manager(CommunicatorService communicator, Mod.Manager manager) + public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, + ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals) { - _communicator = communicator; - _modManager = manager; + using var time = timer.Measure(StartTimeType.Collections); + _communicator = communicator; + _characterUtility = characterUtility; + _residentResources = residentResources; + _config = config; + _modManager = manager; + Individuals = individuals; // The collection manager reacts to changes in mods by itself. _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; @@ -61,9 +74,10 @@ public partial class ModCollection _modManager.ModPathChanged += OnModPathChange; _communicator.CollectionChange.Event += SaveOnChange; _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(); - LoadCollections(); - UpdateCurrentCollectionInUse(); + ReadCollections(files); + LoadCollections(files); + UpdateCurrentCollectionInUse(); + CreateNecessaryCaches(); } public void Dispose() @@ -77,7 +91,7 @@ public partial class ModCollection } private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); // Returns true if the name is not empty, it is not the name of the empty collection // and no existing collection results in the same filename as name. @@ -117,8 +131,9 @@ public partial class ModCollection var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add(newCollection); - newCollection.Save(); + _collections.Add(newCollection); + + Penumbra.SaveService.ImmediateSave(newCollection); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); SetCollection(newCollection.Index, CollectionType.Current); @@ -165,8 +180,8 @@ public partial class ModCollection // Clear own inheritances. foreach (var inheritance in collection.Inheritance) collection.ClearSubscriptions(inheritance); - - collection.Delete(); + + Penumbra.SaveService.ImmediateDelete(collection); _collections.RemoveAt(idx); // Clear external inheritances. @@ -227,7 +242,7 @@ public partial class ModCollection case ModPathChangeType.Moved: OnModMovedActive(mod); foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - collection.Save(); + Penumbra.SaveService.QueueSave(collection); break; case ModPathChangeType.StartingReload: @@ -264,7 +279,7 @@ public partial class ModCollection foreach (var collection in this) { if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - collection.Save(); + Penumbra.SaveService.QueueSave(collection); } // Handle changes that reload the mod if the changes did not need to be prepared, @@ -295,7 +310,7 @@ public partial class ModCollection } var defaultCollection = CreateNewEmpty(DefaultCollection); - defaultCollection.Save(); + Penumbra.SaveService.ImmediateSave(defaultCollection); defaultCollection.Index = _collections.Count; _collections.Add(defaultCollection); } @@ -322,39 +337,36 @@ public partial class ModCollection } if (changes) - collection.Save(); + Penumbra.SaveService.ImmediateSave(collection); } } // Read all collection files in the Collection Directory. // Ensure that the default named collection exists, and apply inheritances afterwards. // Duplicate collection files are not deleted, just not added here. - private void ReadCollections() + private void ReadCollections(FilenameService files) { - // TODO - var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface)); var inheritances = new List>(); - if (collectionDir.Exists) - foreach (var file in collectionDir.EnumerateFiles("*.json")) + foreach (var file in files.CollectionFiles) + { + var collection = LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); + + if (this[collection.Name] != null) { - var collection = LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); } + else + { + inheritances.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + } AddDefaultCollection(); ApplyInheritances(inheritances); diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 2cfbb711..2dd67e3c 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -43,7 +43,7 @@ public partial class IndividualCollections if( group.Length == 0 || group.Any( i => !i.IsValid ) ) { changes = true; - ChatUtil.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); continue; } @@ -51,7 +51,7 @@ public partial class IndividualCollections if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) ) { changes = true; - ChatUtil.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", + Penumbra.ChatService.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", NotificationType.Warning ); continue; } @@ -59,14 +59,14 @@ public partial class IndividualCollections if( !Add( group, collection ) ) { changes = true; - ChatUtil.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", + Penumbra.ChatService.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", NotificationType.Warning ); } } catch( Exception e ) { changes = true; - ChatUtil.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); } } @@ -117,7 +117,7 @@ public partial class IndividualCollections } else { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", "Migration Failure", NotificationType.Error ); } @@ -134,13 +134,13 @@ public partial class IndividualCollections } else { - ChatUtil.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + Penumbra.ChatService.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", "Migration Failure", NotificationType.Error ); } } else { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", "Migration Failure", NotificationType.Error ); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index fc72d3dc..8daa3c04 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -28,18 +28,18 @@ public partial class ModCollection public int ChangeCounter { get; private set; } // Only create, do not update. - private void CreateCache() + private void CreateCache(bool isDefault) { if (_cache == null) { - CalculateEffectiveFileList(); + CalculateEffectiveFileList(isDefault); Penumbra.Log.Verbose($"Created new cache for collection {Name}."); } } // Force an update with metadata for this cache. private void ForceCacheUpdate() - => CalculateEffectiveFileList(); + => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); // Handle temporary mods for this collection. public void Apply(Mod.TemporaryMod tempMod, bool created) @@ -121,11 +121,11 @@ public partial class ModCollection // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList() - => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, - CalculateEffectiveFileListInternal); + public void CalculateEffectiveFileList(bool isDefault) + => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => + CalculateEffectiveFileListInternal(isDefault)); - private void CalculateEffectiveFileListInternal() + private void CalculateEffectiveFileListInternal(bool isDefault) { // Skip the empty collection. if (Index == 0) @@ -133,7 +133,7 @@ public partial class ModCollection Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); _cache ??= new Cache(this); - _cache.FullRecalculation(); + _cache.FullRecalculation(isDefault); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 1b60561d..285d119a 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -174,7 +174,7 @@ public partial class ModCollection break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - FullRecalculation(); + FullRecalculation(_collection == Penumbra.CollectionManager.Default); break; } } @@ -182,9 +182,9 @@ public partial class ModCollection // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => FullRecalculation(); + => FullRecalculation(_collection == Penumbra.CollectionManager.Default); - public void FullRecalculation() + public void FullRecalculation(bool isDefault) { ResolvedFiles.Clear(); MetaManipulations.Reset(); @@ -206,7 +206,7 @@ public partial class ModCollection ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index fead3281..9afd0ef2 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -142,7 +142,7 @@ public partial class ModCollection { if( !inherited ) { - Save(); + Penumbra.SaveService.QueueSave(this); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 6474cbf5..6d13deb1 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -1,134 +1,93 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; using Penumbra.Mods; using Penumbra.Services; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using Dalamud.Plugin; +using Newtonsoft.Json; +using Penumbra.Util; namespace Penumbra.Collections; // File operations like saving, loading and deleting for a collection. -public partial class ModCollection +public partial class ModCollection : ISaveable { - public static string CollectionDirectory(DalamudPluginInterface pi) - => Path.Combine( pi.GetPluginConfigDirectory(), "collections" ); - - // We need to remove all invalid path symbols from the collection name to be able to save it to file. - // TODO - public FileInfo FileName - => new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" )); - - // Custom serialization due to shared mod information across managers. - private void SaveCollection() - { - try - { - Penumbra.Log.Debug( $"Saving collection {AnonymizedName}..." ); - var file = FileName; - file.Directory?.Create(); - using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew ); - using var w = new StreamWriter( s, Encoding.UTF8 ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); - j.WriteStartObject(); - j.WritePropertyName( nameof( Version ) ); - j.WriteValue( Version ); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( Name ); - j.WritePropertyName( nameof( Settings ) ); - - // Write all used and unused settings by mod directory name. - j.WriteStartObject(); - for( var i = 0; i < _settings.Count; ++i ) - { - var settings = _settings[ i ]; - if( settings != null ) - { - j.WritePropertyName( Penumbra.ModManager[ i ].ModPath.Name ); - x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); - } - } - - foreach( var (modDir, settings) in _unusedSettings ) - { - j.WritePropertyName( modDir ); - x.Serialize( j, settings ); - } - - j.WriteEndObject(); - - // Inherit by collection name. - j.WritePropertyName( nameof( Inheritance ) ); - x.Serialize( j, Inheritance.Select( c => c.Name ) ); - j.WriteEndObject(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not save collection {AnonymizedName}:\n{e}" ); - } - } - - public void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveCollection ) + Name, SaveCollection ); - - public void Delete() - { - if( Index == 0 ) - { - return; - } - - var file = FileName; - if( !file.Exists ) - { - return; - } - - try - { - file.Delete(); - Penumbra.Log.Information( $"Deleted collection file for {AnonymizedName}." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete collection file for {AnonymizedName}:\n{e}" ); - } - } - // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. - private static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) { - inheritance = Array.Empty< string >(); - if( !file.Exists ) + inheritance = Array.Empty(); + if (!file.Exists) { - Penumbra.Log.Error( "Could not read collection because file does not exist." ); + Penumbra.Log.Error("Could not read collection because file does not exist."); return null; } try { - var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); - var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; - var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + var obj = JObject.Parse(File.ReadAllText(file.FullName)); + var name = obj[nameof(Name)]?.ToObject() ?? string.Empty; + var version = obj[nameof(Version)]?.ToObject() ?? 0; // Custom deserialization that is converted with the constructor. - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings.SavedSettings > >() - ?? new Dictionary< string, ModSettings.SavedSettings >(); - inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + var settings = obj[nameof(Settings)]?.ToObject>() + ?? new Dictionary(); + inheritance = obj[nameof(Inheritance)]?.ToObject>() ?? (IReadOnlyList)Array.Empty(); - return new ModCollection( name, version, settings ); + return new ModCollection(name, version, settings); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not read collection information from file:\n{e}" ); + Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); } return null; } -} \ No newline at end of file + + public string ToFilename(FilenameService fileNames) + => fileNames.CollectionFile(this); + + public string LogName(string _) + => AnonymizedName; + + public string TypeName + => "Collection"; + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create(new JsonSerializerSettings { Formatting = Formatting.Indented }); + j.WriteStartObject(); + j.WritePropertyName(nameof(Version)); + j.WriteValue(Version); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName(nameof(Settings)); + + // Write all used and unused settings by mod directory name. + j.WriteStartObject(); + for (var i = 0; i < _settings.Count; ++i) + { + var settings = _settings[i]; + if (settings != null) + { + j.WritePropertyName(Penumbra.ModManager[i].ModPath.Name); + x.Serialize(j, new ModSettings.SavedSettings(settings, Penumbra.ModManager[i])); + } + } + + foreach (var (modDir, settings) in _unusedSettings) + { + j.WritePropertyName(modDir); + x.Serialize(j, settings); + } + + j.WriteEndObject(); + + // Inherit by collection name. + j.WritePropertyName(nameof(Inheritance)); + x.Serialize(j, Inheritance.Select(c => c.Name)); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index bc940651..ceaac70d 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,6 +1,8 @@ using Penumbra.Mods; using System.Collections.Generic; using System.Linq; +using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; @@ -9,12 +11,12 @@ public sealed partial class ModCollection // Migration to convert ModCollections from older versions to newer. private static class Migration { - public static void Migrate( ModCollection collection ) + public static void Migrate(SaveService saver, ModCollection collection ) { var changes = MigrateV0ToV1( collection ); if( changes ) - { - collection.Save(); + { + saver.ImmediateSave(collection); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index f1060880..a8a4e1f4 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; using OtterGui; +using OtterGui.Classes; +using Penumbra.Services; namespace Penumbra.Collections; @@ -27,16 +29,16 @@ public partial class ModCollection // Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[ ..2 ]}... ({Index})" : $"{Name} ({Index})"; + => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; public int Version { get; private set; } - public int Index { get; private set; } = -1; + public int Index { get; private set; } = -1; // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List< ModSettings? > _settings; + private readonly List _settings; - public IReadOnlyList< ModSettings? > Settings + public IReadOnlyList Settings => _settings; // Returns whether there are settings not in use by any current mod. @@ -47,107 +49,103 @@ public partial class ModCollection => _unusedSettings.Count; // Evaluates the settings along the whole inheritance tree. - public IEnumerable< ModSettings? > ActualSettings - => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + public IEnumerable ActualSettings + => Enumerable.Range(0, _settings.Count).Select(i => this[i].Settings); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary< string, ModSettings.SavedSettings > _unusedSettings; + private readonly Dictionary _unusedSettings; // Constructor for duplication. - private ModCollection( string name, ModCollection duplicate ) + private ModCollection(string name, ModCollection duplicate) { Name = name; Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); - _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + _settings = duplicate._settings.ConvertAll(s => s?.DeepCopy()); + _unusedSettings = duplicate._unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); _inheritance = duplicate._inheritance.ToList(); ModSettingChanged += SaveOnChange; InheritanceChanged += SaveOnChange; } // Constructor for reading from files. - private ModCollection( string name, int version, Dictionary< string, ModSettings.SavedSettings > allSettings ) + private ModCollection(string name, int version, Dictionary allSettings) { Name = name; Version = version; _unusedSettings = allSettings; - _settings = new List< ModSettings? >(); + _settings = new List(); ApplyModSettings(); - Migration.Migrate( this ); + Migration.Migrate(Penumbra.SaveService, this); ModSettingChanged += SaveOnChange; InheritanceChanged += SaveOnChange; } // Create a new, unique empty collection of a given name. - public static ModCollection CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); + public static ModCollection CreateNewEmpty(string name) + => new(name, CurrentVersion, new Dictionary()); // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary( string name, int changeCounter ) + public static ModCollection CreateNewTemporary(string name, int changeCounter) { - var collection = new ModCollection( name, Empty ); + var collection = new ModCollection(name, Empty); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; collection.Index = ~Penumbra.TempCollections.Count; collection.ChangeCounter = changeCounter; - collection.CreateCache(); + collection.CreateCache(false); return collection; } // Duplicate the calling collection to a new, unique collection of a given name. - public ModCollection Duplicate( string name ) + public ModCollection Duplicate(string name) => new(name, this); // Check if a name is valid to use for a collection. // Does not check for uniqueness. - public static bool IsValidName( string name ) - => name.Length > 0 && name.All( c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath() ); + public static bool IsValidName(string name) + => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { var any = _unusedSettings.Count > 0; _unusedSettings.Clear(); - if( any ) - { - Save(); - } + if (any) + Penumbra.SaveService.QueueSave(this); } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private bool AddMod( Mod mod ) + private bool AddMod(Mod mod) { - if( _unusedSettings.TryGetValue( mod.ModPath.Name, out var save ) ) + if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) { - var ret = save.ToSettings( mod, out var settings ); - _settings.Add( settings ); - _unusedSettings.Remove( mod.ModPath.Name ); + var ret = save.ToSettings(mod, out var settings); + _settings.Add(settings); + _unusedSettings.Remove(mod.ModPath.Name); return ret; } - _settings.Add( null ); + _settings.Add(null); return false; } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mod mod, int idx ) + private void RemoveMod(Mod mod, int idx) { - var settings = _settings[ idx ]; - if( settings != null ) - { - _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( settings, mod ); - } + var settings = _settings[idx]; + if (settings != null) + _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - _settings.RemoveAt( idx ); + _settings.RemoveAt(idx); } // Create the always available Empty Collection that will always sit at index 0, // can not be deleted and does never create a cache. private static ModCollection CreateEmpty() { - var collection = CreateNewEmpty( EmptyCollection ); + var collection = CreateNewEmpty(EmptyCollection); collection.Index = 0; collection._settings.Clear(); return collection; @@ -156,10 +154,8 @@ public partial class ModCollection // Move all settings to unused settings for rediscovery. private void PrepareModDiscovery() { - foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) - { - _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( setting!, mod ); - } + foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) + _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); _settings.Clear(); } @@ -168,50 +164,44 @@ public partial class ModCollection // Also fixes invalid settings. private void ApplyModSettings() { - _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); - if( Penumbra.ModManager.Aggregate( false, ( current, mod ) => current | AddMod( mod ) ) ) - { - Save(); - } + _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); + if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) + Penumbra.SaveService.ImmediateSave(this); } - public bool CopyModSettings( int modIdx, string modName, int targetIdx, string targetName ) + public bool CopyModSettings(int modIdx, string modName, int targetIdx, string targetName) { - if( targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0 ) - { + if (targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0) return false; - } // If the source mod exists, convert its settings to saved settings or null if its inheriting. // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = modIdx >= 0 - ? _settings[ modIdx ] != null - ? new ModSettings.SavedSettings( _settings[ modIdx ]!, Penumbra.ModManager[ modIdx ] ) + ? _settings[modIdx] != null + ? new ModSettings.SavedSettings(_settings[modIdx]!, Penumbra.ModManager[modIdx]) : null - : _unusedSettings.TryGetValue( modName, out var s ) + : _unusedSettings.TryGetValue(modName, out var s) ? s : null; - if( targetIdx >= 0 ) + if (targetIdx >= 0) { - if( savedSettings != null ) + if (savedSettings != null) { // The target mod exists and the source settings are not inheriting, convert and fix the settings and copy them. // This triggers multiple events. - savedSettings.Value.ToSettings( Penumbra.ModManager[ targetIdx ], out var settings ); - SetModState( targetIdx, settings.Enabled ); - SetModPriority( targetIdx, settings.Priority ); - foreach( var (value, index) in settings.Settings.WithIndex() ) - { - SetModSetting( targetIdx, index, value ); - } + savedSettings.Value.ToSettings(Penumbra.ModManager[targetIdx], out var settings); + SetModState(targetIdx, settings.Enabled); + SetModPriority(targetIdx, settings.Priority); + foreach (var (value, index) in settings.Settings.WithIndex()) + SetModSetting(targetIdx, index, value); } else { // The target mod exists, but the source is inheriting, set the target to inheriting. // This triggers events. - SetModInheritance( targetIdx, true ); + SetModInheritance(targetIdx, true); } } else @@ -219,14 +209,10 @@ public partial class ModCollection // The target mod does not exist. // Either copy the unused source settings directly if they are not inheriting, // or remove any unused settings for the target if they are inheriting. - if( savedSettings != null ) - { - _unusedSettings[ targetName ] = savedSettings.Value; - } + if (savedSettings != null) + _unusedSettings[targetName] = savedSettings.Value; else - { - _unusedSettings.Remove( targetName ); - } + _unusedSettings.Remove(targetName); } return true; @@ -234,4 +220,4 @@ public partial class ModCollection public override string ToString() => Name; -} \ No newline at end of file +} diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 63922566..760a4870 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Game.Command; +using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; using Penumbra.Api.Enums; @@ -9,227 +10,184 @@ using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI; +using Penumbra.Util; namespace Penumbra; -public static class SeStringBuilderExtensions -{ - public const ushort Green = 504; - public const ushort Yellow = 31; - public const ushort Red = 534; - public const ushort Blue = 517; - public const ushort White = 1; - public const ushort Purple = 541; - - public static SeStringBuilder AddText( this SeStringBuilder sb, string text, int color, bool brackets = false ) - => sb.AddUiForeground( ( ushort )color ).AddText( brackets ? $"[{text}]" : text ).AddUiForegroundOff(); - - public static SeStringBuilder AddGreen( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Green, brackets ); - - public static SeStringBuilder AddYellow( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Yellow, brackets ); - - public static SeStringBuilder AddRed( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Red, brackets ); - - public static SeStringBuilder AddBlue( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Blue, brackets ); - - public static SeStringBuilder AddWhite( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, White, brackets ); - - public static SeStringBuilder AddPurple( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Purple, brackets ); - - public static SeStringBuilder AddCommand( this SeStringBuilder sb, string command, string description ) - => sb.AddText( " 》 " ) - .AddBlue( command ) - .AddText( $" - {description}" ); - - public static SeStringBuilder AddInitialPurple( this SeStringBuilder sb, string word, bool withComma = true ) - => sb.AddPurple( $"[{word[ 0 ]}]" ) - .AddText( withComma ? $"{word[ 1.. ]}, " : word[ 1.. ] ); -} - public class CommandHandler : IDisposable { private const string CommandName = "/penumbra"; private readonly CommandManager _commandManager; - private readonly ObjectReloader _objectReloader; + private readonly RedrawService _redrawService; + private readonly ChatGui _chat; private readonly Configuration _config; - private readonly Penumbra _penumbra; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; private readonly Mod.Manager _modManager; private readonly ModCollection.Manager _collectionManager; + private readonly Penumbra _penumbra; - public CommandHandler( CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, Mod.Manager modManager, - ModCollection.Manager collectionManager, ActorManager actors ) + public CommandHandler(CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, + ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra) { _commandManager = commandManager; - _objectReloader = objectReloader; + _redrawService = redrawService; _config = config; - _penumbra = penumbra; _configWindow = configWindow; _modManager = modManager; _collectionManager = collectionManager; - _actors = actors; - _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) + _actors = actors.AwaitedService; + _chat = chat; + _penumbra = penumbra; + _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", ShowInHelp = true, - } ); + }); } public void Dispose() { - _commandManager.RemoveHandler( CommandName ); + _commandManager.RemoveHandler(CommandName); } - private void OnCommand( string command, string arguments ) + private void OnCommand(string command, string arguments) { - if( arguments.Length == 0 ) - { + if (arguments.Length == 0) arguments = "window"; - } - var argumentList = arguments.Split( ' ', 2 ); - arguments = argumentList.Length == 2 ? argumentList[ 1 ] : string.Empty; + var argumentList = arguments.Split(' ', 2); + arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty; - var _ = argumentList[ 0 ].ToLowerInvariant() switch + var _ = argumentList[0].ToLowerInvariant() switch { - "window" => ToggleWindow( arguments ), - "enable" => SetPenumbraState( arguments, true ), - "disable" => SetPenumbraState( arguments, false ), - "toggle" => SetPenumbraState( arguments, null ), - "reload" => Reload( arguments ), - "redraw" => Redraw( arguments ), - "lockui" => SetUiLockState( arguments ), - "debug" => SetDebug( arguments ), - "collection" => SetCollection( arguments ), - "mod" => SetMod( arguments ), - "bulktag" => SetTag( arguments ), - _ => PrintHelp( argumentList[ 0 ] ), + "window" => ToggleWindow(arguments), + "enable" => SetPenumbraState(arguments, true), + "disable" => SetPenumbraState(arguments, false), + "toggle" => SetPenumbraState(arguments, null), + "reload" => Reload(arguments), + "redraw" => Redraw(arguments), + "lockui" => SetUiLockState(arguments), + "debug" => SetDebug(arguments), + "collection" => SetCollection(arguments), + "mod" => SetMod(arguments), + "bulktag" => SetTag(arguments), + _ => PrintHelp(argumentList[0]), }; } - private static bool PrintHelp( string arguments ) + private bool PrintHelp(string arguments) { - if( !string.Equals( arguments, "help", StringComparison.OrdinalIgnoreCase ) && arguments == "?" ) - { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); - } + if (!string.Equals(arguments, "help", StringComparison.OrdinalIgnoreCase) && arguments == "?") + _chat.Print(new SeStringBuilder().AddText("The given argument ").AddRed(arguments, true) + .AddText(" is not valid. Valid arguments are:").BuiltString); else - { - DalamudServices.Chat.Print( "Valid arguments for /penumbra are:" ); - } + _chat.Print("Valid arguments for /penumbra are:"); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "window", - "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state." ) - .BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) - .BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder() - .AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) - .BuiltString ); + _chat.Print(new SeStringBuilder().AddCommand("window", + "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("enable", "Enable modding and force a redraw of all game objects if it was previously disabled.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("disable", "Disable modding and force a redraw of all game objects if it was previously enabled.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("toggle", "Toggle modding and force a redraw of all game objects.") + .BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("reload", "Rediscover the mod directory and reload all mods.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("collection", "Change your active collection setup. Use without further parameters for more detailed help.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("mod", "Change a specific mods settings. Use without further parameters for more detailed help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") + .BuiltString); return true; } - private bool ToggleWindow( string arguments ) + private bool ToggleWindow(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_configWindow.IsOpen; - if( value == _configWindow.IsOpen ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen; + if (value == _configWindow.IsOpen) return false; - } _configWindow.Toggle(); return true; } - private bool Reload( string _ ) + private bool Reload(string _) { _modManager.DiscoverMods(); - Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); + Print($"Reloaded Penumbra mods. You have {_modManager.Count} mods."); return true; } - private bool Redraw( string arguments ) + private bool Redraw(string arguments) { - if( arguments.Length > 0 ) - { - _objectReloader.RedrawObject( arguments, RedrawType.Redraw ); - } + if (arguments.Length > 0) + _redrawService.RedrawObject(arguments, RedrawType.Redraw); else - { - _objectReloader.RedrawAll( RedrawType.Redraw ); - } + _redrawService.RedrawAll(RedrawType.Redraw); return true; } - private bool SetDebug( string arguments ) + private bool SetDebug(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_config.DebugMode; - if( value == _config.DebugMode ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_config.DebugMode; + if (value == _config.DebugMode) return false; - } - Print( value ? "Debug mode enabled." : "Debug mode disabled." ); + Print(value ? "Debug mode enabled." : "Debug mode disabled."); _config.DebugMode = value; _config.Save(); return true; } - private bool SetPenumbraState( string _, bool? newValue ) + private bool SetPenumbraState(string _, bool? newValue) { var value = newValue ?? !_config.EnableMods; - if( value == _config.EnableMods ) + if (value == _config.EnableMods) { - Print( value + Print(value ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" - : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); + : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"); return false; } - Print( value + Print(value ? "Your mods have been enabled." - : "Your mods have been disabled." ); - return _penumbra.SetEnabled( value ); + : "Your mods have been disabled."); + return _penumbra.SetEnabled(value); } - private bool SetUiLockState( string arguments ) + private bool SetUiLockState(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_config.FixMainWindow; - if( value == _config.FixMainWindow ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_config.FixMainWindow; + if (value == _config.FixMainWindow) return false; - } - if( value ) + if (value) { - Print( "Penumbra UI locked in place." ); + Print("Penumbra UI locked in place."); _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; } else { - Print( "Penumbra UI unlocked." ); - _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + Print("Penumbra UI unlocked."); + _configWindow.Flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); } _config.FixMainWindow = value; @@ -237,260 +195,274 @@ public class CommandHandler : IDisposable return true; } - private bool SetCollection( string arguments ) + private bool SetCollection(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) - .AddText( " | " ).AddGreen( "" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Types are " ).AddBlue( "Base" ).AddText( ", " ).AddBlue( "Ui" ).AddText( ", " ) - .AddBlue( "Selected" ).AddText( ", " ) - .AddBlue( "Individual" ).AddText( ", and all those selectable in Character Groups." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Names are " ).AddYellow( "None" ) - .AddText( ", all collections you have created by their full names, and " ).AddYellow( "Delete" ).AddText( " to remove assignments (not valid for all types)." ) - .BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) - .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) - .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) - .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "n" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) - .AddRed( "[NPC Name]" ).AddText( ", where NPC Type can be " ).AddInitialPurple( "Mount" ).AddInitialPurple( "Companion" ).AddInitialPurple( "Accessory" ) - .AddInitialPurple( "Event NPC" ).AddText( "or " ) - .AddInitialPurple( "Battle NPC", false ).AddText( "." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "o" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) - .AddRed( "[NPC Name]" ).AddText( " | " ).AddWhite( "[Player Name]@" ).AddText( "." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("Use with /penumbra collection ").AddBlue("[Collection Type]") + .AddText(" | ").AddYellow("[Collection Name]") + .AddText(" | ").AddGreen("").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Types are ").AddBlue("Base").AddText(", ") + .AddBlue("Ui").AddText(", ") + .AddBlue("Selected").AddText(", ") + .AddBlue("Individual").AddText(", and all those selectable in Character Groups.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Names are ").AddYellow("None") + .AddText(", all collections you have created by their full names, and ").AddYellow("Delete") + .AddText(" to remove assignments (not valid for all types).") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 If the type is ").AddBlue("Individual") + .AddText(" you need to specify an individual with an identifier of the form:").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("").AddText(" or ").AddGreen("") + .AddText(" or ").AddGreen("") + .AddText(" or ").AddGreen("") + .AddText(" as placeholders for your character, your target, your mouseover or your focus, if they exist.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("p").AddText(" | ") + .AddWhite("[Player Name]@") + .AddText(", if no @ is provided, Any World is used.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("r").AddText(" | ").AddWhite("[Retainer Name]") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("n").AddText(" | ").AddPurple("[NPC Type]") + .AddText(" : ") + .AddRed("[NPC Name]").AddText(", where NPC Type can be ").AddInitialPurple("Mount").AddInitialPurple("Companion") + .AddInitialPurple("Accessory") + .AddInitialPurple("Event NPC").AddText("or ") + .AddInitialPurple("Battle NPC", false).AddText(".").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("o").AddText(" | ").AddPurple("[NPC Type]") + .AddText(" : ") + .AddRed("[NPC Name]").AddText(" | ").AddWhite("[Player Name]@").AddText(".").BuiltString); return true; } - var split = arguments.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); - var typeName = split[ 0 ]; + var split = arguments.Split('|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var typeName = split[0]; - if( !CollectionTypeExtensions.TryParse( typeName, out var type ) ) + if (!CollectionTypeExtensions.TryParse(typeName, out var type)) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(typeName, true) + .AddText(" is not a valid collection type.").BuiltString); return false; } - if( split.Length == 1 ) + if (split.Length == 1) { - DalamudServices.Chat.Print( "There was no collection name provided." ); + _chat.Print("There was no collection name provided."); return false; } - if( !GetModCollection( split[ 1 ], out var collection ) ) - { + if (!GetModCollection(split[1], out var collection)) return false; - } var identifier = ActorIdentifier.Invalid; - if( type is CollectionType.Individual ) + if (type is CollectionType.Individual) { - if( split.Length == 2 ) + if (split.Length == 2) { - DalamudServices.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); + _chat.Print( + "Setting an individual collection requires a collection name and an identifier, but no identifier was provided."); return false; } try { - if( ObjectReloader.GetName( split[ 2 ].ToLowerInvariant(), out var obj ) ) + if (_redrawService.GetName(split[2].ToLowerInvariant(), out var obj)) { - identifier = _actors.FromObject( obj, false, true, true ); - if( !identifier.IsValid ) + identifier = _actors.FromObject(obj, false, true, true); + if (!identifier.IsValid) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) - .AddText( " did not resolve to a game object with a valid identifier." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(split[2]) + .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); return false; } } else { - identifier = _actors.FromUserString( split[ 2 ] ); + identifier = _actors.FromUserString(split[2]); } } - catch( ActorManager.IdentifierParseError e ) + catch (ActorManager.IdentifierParseError e) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( split[ 2 ], true ).AddText( $" could not be converted to an identifier. {e.Message}" ) - .BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true) + .AddText($" could not be converted to an identifier. {e.Message}") + .BuiltString); return false; } } - var oldCollection = _collectionManager.ByType( type, identifier ); - if( collection == oldCollection ) + var oldCollection = _collectionManager.ByType(type, identifier); + if (collection == oldCollection) { - DalamudServices.Chat.Print( collection == null - ? $"The {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}" : string.Empty )} is already unassigned" - : $"{collection.Name} already is the {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _chat.Print(collection == null + ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" + : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return false; } - var individualIndex = _collectionManager.Individuals.Index( identifier ); + var individualIndex = _collectionManager.Individuals.Index(identifier); - if( oldCollection == null ) + if (oldCollection == null) { - if( type.IsSpecial() ) + if (type.IsSpecial()) { - _collectionManager.CreateSpecialCollection( type ); + _collectionManager.CreateSpecialCollection(type); } - else if( identifier.IsValid ) + else if (identifier.IsValid) { - var identifiers = _collectionManager.Individuals.GetGroup( identifier ); + var identifiers = _collectionManager.Individuals.GetGroup(identifier); individualIndex = _collectionManager.Individuals.Count; - _collectionManager.CreateIndividualCollection( identifiers ); + _collectionManager.CreateIndividualCollection(identifiers); } } - else if( collection == null ) + else if (collection == null) { - if( type.IsSpecial() ) + if (type.IsSpecial()) { - _collectionManager.RemoveSpecialCollection( type ); + _collectionManager.RemoveSpecialCollection(type); } - else if( individualIndex >= 0 ) + else if (individualIndex >= 0) { - _collectionManager.RemoveIndividualCollection( individualIndex ); + _collectionManager.RemoveIndividualCollection(individualIndex); } else { - DalamudServices.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _chat.Print( + $"Can not remove the {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); return false; } - Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + Print( + $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } - _collectionManager.SetCollection( collection!, type, individualIndex ); - Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _collectionManager.SetCollection(collection!, type, individualIndex); + Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } - private bool SetMod( string arguments ) + private bool SetMod(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) - .AddPurple( "[Mod Name or Mod Directory Name]" ); - DalamudServices.Chat.Print( seString.BuiltString ); + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]") + .AddText(" | ") + .AddPurple("[Mod Name or Mod Directory Name]"); + _chat.Print(seString.BuiltString); return true; } - var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - if( nameSplit.Length != 2 ) + var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var nameSplit = split.Length != 2 + ? Array.Empty() + : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (nameSplit.Length != 2) { - DalamudServices.Chat.Print( "Not enough arguments provided." ); + _chat.Print("Not enough arguments provided."); return false; } - var state = ConvertToSettingState( split[ 0 ] ); - if( state == -1 ) + var state = ConvertToSettingState(split[0]); + if (state == -1) { - DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } - if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) + if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) + return false; + + if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod)) { + _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.") + .BuiltString); return false; } - if( !_modManager.TryGetMod( nameSplit[ 1 ], nameSplit[ 1 ], out var mod ) ) - { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); - return false; - } - - if( HandleModState( state, collection!, mod ) ) - { + if (HandleModState(state, collection!, mod)) return true; - } - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) - .AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) + .AddText("already had the desired state in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); return false; } - private bool SetTag( string arguments ) + private bool SetTag(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra bulktag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) - .AddPurple( "[Local Tag]" ); - DalamudServices.Chat.Print( seString.BuiltString ); + .AddText("Use with /penumbra bulktag ").AddBlue("[enable|disable|toggle|inherit]").AddText(" ").AddYellow("[Collection Name]") + .AddText(" | ") + .AddPurple("[Local Tag]"); + _chat.Print(seString.BuiltString); return true; } - var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - if( nameSplit.Length != 2 ) + var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var nameSplit = split.Length != 2 + ? Array.Empty() + : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (nameSplit.Length != 2) { - DalamudServices.Chat.Print( "Not enough arguments provided." ); + _chat.Print("Not enough arguments provided."); return false; } - var state = ConvertToSettingState( split[ 0 ] ); + var state = ConvertToSettingState(split[0]); - if( state == -1 ) + if (state == -1) { - DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } - if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) - { + if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; - } - var mods = _modManager.Where( m => m.LocalTags.Contains( nameSplit[ 1 ], StringComparer.OrdinalIgnoreCase ) ).ToList(); + var mods = _modManager.Where(m => m.LocalTags.Contains(nameSplit[1], StringComparer.OrdinalIgnoreCase)).ToList(); - if( mods.Count == 0 ) + if (mods.Count == 0) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The tag ").AddRed(nameSplit[1], true).AddText(" does not match any mods.") + .BuiltString); return false; } var changes = false; - foreach( var mod in mods ) - { - changes |= HandleModState( state, collection!, mod ); - } + foreach (var mod in mods) + changes |= HandleModState(state, collection!, mod); - if( !changes ) - { - Print( () => new SeStringBuilder().AddText( "No mod states were changed in collection " ).AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); - } + if (!changes) + Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true) + .AddText(".").BuiltString); return true; } - private bool GetModCollection( string collectionName, out ModCollection? collection ) + private bool GetModCollection(string collectionName, out ModCollection? collection) { var lowerName = collectionName.ToLowerInvariant(); - if( lowerName == "delete" ) + if (lowerName == "delete") { collection = null; return true; } - collection = string.Equals( lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) + collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager[ lowerName ]; - if( collection == null ) + : _collectionManager[lowerName]; + if (collection == null) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") + .BuiltString); return false; } return true; } - private static bool? ParseTrueFalseToggle( string value ) + private static bool? ParseTrueFalseToggle(string value) => value.ToLowerInvariant() switch { "0" => false, @@ -508,7 +480,7 @@ public class CommandHandler : IDisposable _ => null, }; - private static int ConvertToSettingState( string text ) + private static int ConvertToSettingState(string text) => text.ToLowerInvariant() switch { "enable" => 0, @@ -521,47 +493,49 @@ public class CommandHandler : IDisposable _ => -1, }; - private static bool HandleModState( int settingState, ModCollection collection, Mod mod ) + private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection!.Settings[ mod.Index ]; - switch( settingState ) + var settings = collection!.Settings[mod.Index]; + switch (settingState) { case 0: - if( collection.SetModState( mod.Index, true ) ) + if (collection.SetModState(mod.Index, true)) { - Print( () => new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 1: - if( collection.SetModState( mod.Index, false ) ) + if (collection.SetModState(mod.Index, false)) { - Print( () => new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 2: - var setting = !( settings?.Enabled ?? false ); - if( collection.SetModState( mod.Index, setting ) ) + var setting = !(settings?.Enabled ?? false); + if (collection.SetModState(mod.Index, setting)) { - Print( () => new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) + .AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 3: - if( collection.SetModInheritance( mod.Index, true ) ) + if (collection.SetModInheritance(mod.Index, true)) { - Print( () => new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( " to inherit." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(" to inherit.").BuiltString); return true; } @@ -571,27 +545,21 @@ public class CommandHandler : IDisposable return false; } - private static void Print( string text ) + private void Print(string text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text); } - private static void Print( DefaultInterpolatedStringHandler text ) + private void Print(DefaultInterpolatedStringHandler text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text.ToStringAndClear() ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text.ToStringAndClear()); } - private static void Print( Func text ) + private void Print(Func text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text() ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text()); } -} \ No newline at end of file +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 55eee054..9499ae30 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -29,7 +29,7 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; public bool EnableMods { get; set; } = true; diff --git a/Penumbra/Interop/Loader/CharacterResolver.cs b/Penumbra/Interop/Loader/CharacterResolver.cs index 0231a1e8..68b83b41 100644 --- a/Penumbra/Interop/Loader/CharacterResolver.cs +++ b/Penumbra/Interop/Loader/CharacterResolver.cs @@ -70,12 +70,10 @@ public class CharacterResolver : IDisposable }; } - // TODO public unsafe void Dispose() { _loader.ResetResolvePath(); _loader.FileLoaded -= ImcLoadResource; - _pathResolver.Dispose(); } // Use the default method of path replacement. diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs deleted file mode 100644 index ba24f155..00000000 --- a/Penumbra/Interop/ObjectReloader.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; -using Penumbra.Api; -using Penumbra.Api.Enums; -using Penumbra.GameData; -using Penumbra.GameData.Actors; -using Penumbra.Interop.Structs; -using Penumbra.Services; - -namespace Penumbra.Interop; - -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 ][ Offsets.DisableDrawVfunc ]( actor.Address ); - - private static void EnableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ Offsets.EnableDrawVfunc ]( actor.Address ); - - // Check whether we currently are in GPose. - // Also clear the name list. - private void SetGPose() - { - _inGPose = DalamudServices.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 = DalamudServices.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 = DalamudServices.Objects[ GPosePlayerIdx + i ]; - return true; - } - } - - for( ; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter ) - { - var gPoseName = DalamudServices.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString(); - _gPoseNames[ _gPoseNameCounter ] = gPoseName; - if( gPoseName == null ) - { - break; - } - - if( name == gPoseName ) - { - obj = DalamudServices.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 >= (int) ScreenActor.CharacterScreen and <= ( int) ScreenActor.Card8; - } -} - -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; - - public event GameObjectRedrawnDelegate? GameObjectRedrawn; - - public ObjectReloader() - => DalamudServices.Framework.Update += OnUpdateEvent; - - public void Dispose() - => DalamudServices.Framework.Update -= OnUpdateEvent; - - public static DrawState* ActorDrawState( GameObject actor ) - => ( DrawState* )( &( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->RenderFlags ); - - private static int ObjectTableIndex( GameObject actor ) - => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; - - private static void WriteInvisible( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - *ActorDrawState( actor! ) |= DrawState.Invisibility; - - var gPose = IsGPoseActor( tableIndex ); - if( gPose ) - { - DisableDraw( actor! ); - } - - if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) - { - *ActorDrawState( mount ) |= DrawState.Invisibility; - if( gPose ) - { - DisableDraw( mount ); - } - } - } - - private void WriteVisible( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - - var gPose = IsGPoseActor( tableIndex ); - if( gPose ) - { - EnableDraw( actor! ); - } - - if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) - { - *ActorDrawState( mount ) &= ~DrawState.Invisibility; - if( gPose ) - { - EnableDraw( mount ); - } - } - - GameObjectRedrawn?.Invoke( actor!.Address, tableIndex ); - } - - private void ReloadActor( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - if( actor!.Address == DalamudServices.Targets.Target?.Address ) - { - _target = tableIndex; - } - - _queue.Add( ~tableIndex ); - } - - private void ReloadActorAfterGPose( GameObject? actor ) - { - if( DalamudServices.Objects[ GPosePlayerIdx ] != null ) - { - ReloadActor( actor ); - return; - } - - if( actor != null ) - { - WriteInvisible( actor ); - _afterGPoseQueue.Add( ~ObjectTableIndex( actor ) ); - } - } - - private void HandleTarget() - { - if( _target < 0 ) - { - return; - } - - var actor = DalamudServices.Objects[ _target ]; - if( actor == null || DalamudServices.Targets.Target != null ) - { - return; - } - - DalamudServices.Targets.SetTarget( actor ); - _target = -1; - } - - private void HandleRedraw() - { - if( _queue.Count == 0 ) - { - return; - } - - var numKept = 0; - for( var i = 0; i < _queue.Count; ++i ) - { - var idx = _queue[ i ]; - if( FindCorrectActor( idx < 0 ? ~idx : idx, out var obj ) ) - { - _afterGPoseQueue.Add( idx < 0 ? idx : ~idx ); - } - - if( obj != null ) - { - if( idx < 0 ) - { - WriteInvisible( obj ); - _queue[ numKept++ ] = ObjectTableIndex( obj ); - } - else - { - WriteVisible( obj ); - } - } - } - - _queue.RemoveRange( numKept, _queue.Count - numKept ); - } - - private void HandleAfterGPose() - { - if( _afterGPoseQueue.Count == 0 || _inGPose ) - { - return; - } - - var numKept = 0; - for( var i = 0; i < _afterGPoseQueue.Count; ++i ) - { - var idx = _afterGPoseQueue[ i ]; - if( idx < 0 ) - { - var newIdx = ~idx; - WriteInvisible( DalamudServices.Objects[ newIdx ] ); - _afterGPoseQueue[ numKept++ ] = newIdx; - } - else - { - WriteVisible( DalamudServices.Objects[ idx ] ); - } - } - - _afterGPoseQueue.RemoveRange( numKept, _afterGPoseQueue.Count - numKept ); - } - - private void OnUpdateEvent( object framework ) - { - if( DalamudServices.Conditions[ ConditionFlag.BetweenAreas51 ] - || DalamudServices.Conditions[ ConditionFlag.BetweenAreas ] - || DalamudServices.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) - { - return; - } - - SetGPose(); - HandleRedraw(); - HandleAfterGPose(); - HandleTarget(); - } - - public void RedrawObject( GameObject? actor, RedrawType settings ) - { - switch( settings ) - { - case RedrawType.Redraw: - ReloadActor( actor ); - break; - case RedrawType.AfterGPose: - ReloadActorAfterGPose( actor ); - break; - default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); - } - } - - private static GameObject? GetLocalPlayer() - { - var gPosePlayer = DalamudServices.Objects[ GPosePlayerIdx ]; - return gPosePlayer ?? DalamudServices.Objects[ 0 ]; - } - - public static bool GetName( string lowerName, out GameObject? actor ) - { - ( actor, var ret ) = lowerName switch - { - "" => ( null, true ), - "" => ( GetLocalPlayer(), true ), - "self" => ( GetLocalPlayer(), true ), - "" => ( DalamudServices.Targets.Target, true ), - "target" => ( DalamudServices.Targets.Target, true ), - "" => ( DalamudServices.Targets.FocusTarget, true ), - "focus" => ( DalamudServices.Targets.FocusTarget, true ), - "" => ( DalamudServices.Targets.MouseOverTarget, true ), - "mouseover" => ( DalamudServices.Targets.MouseOverTarget, true ), - _ => ( null, false ), - }; - return ret; - } - - public void RedrawObject( int tableIndex, RedrawType settings ) - { - if( tableIndex >= 0 && tableIndex < DalamudServices.Objects.Length ) - { - RedrawObject( DalamudServices.Objects[ tableIndex ], settings ); - } - } - - public void RedrawObject( string name, RedrawType settings ) - { - var lowerName = name.ToLowerInvariant(); - if( GetName( lowerName, out var target ) ) - { - RedrawObject( target, settings ); - } - else - { - foreach( var actor in DalamudServices.Objects.Where( a => a.Name.ToString().ToLowerInvariant() == lowerName ) ) - { - RedrawObject( actor, settings ); - } - } - } - - public void RedrawAll( RedrawType settings ) - { - foreach( var actor in DalamudServices.Objects ) - { - RedrawObject( actor, settings ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/RedrawService.cs b/Penumbra/Interop/RedrawService.cs new file mode 100644 index 00000000..f738320c --- /dev/null +++ b/Penumbra/Interop/RedrawService.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.GameData.Actors; +using Penumbra.Interop.Structs; +using Penumbra.Services; + +namespace Penumbra.Interop; + +public unsafe partial class RedrawService +{ + 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][Offsets.DisableDrawVfunc](actor.Address); + + private static void EnableDraw(GameObject actor) + => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + + // Check whether we currently are in GPose. + // Also clear the name list. + private void SetGPose() + { + _inGPose = _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 = _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 = _objects[GPosePlayerIdx + i]; + return true; + } + } + + for (; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter) + { + var gPoseName = _objects[GPosePlayerIdx + _gPoseNameCounter]?.Name.ToString(); + _gPoseNames[_gPoseNameCounter] = gPoseName; + if (gPoseName == null) + break; + + if (name == gPoseName) + { + obj = _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 >= (int)ScreenActor.CharacterScreen and <= (int)ScreenActor.Card8; + } +} + +public sealed unsafe partial class RedrawService : IDisposable +{ + private readonly Framework _framework; + private readonly ObjectTable _objects; + private readonly TargetManager _targets; + private readonly Condition _conditions; + + private readonly List _queue = new(100); + private readonly List _afterGPoseQueue = new(GPoseSlots); + private int _target = -1; + + public event GameObjectRedrawnDelegate? GameObjectRedrawn; + + public RedrawService(Framework framework, ObjectTable objects, TargetManager targets, Condition conditions) + { + _framework = framework; + _objects = objects; + _targets = targets; + _conditions = conditions; + _framework.Update += OnUpdateEvent; + } + + public void Dispose() + { + _framework.Update -= OnUpdateEvent; + } + + public static DrawState* ActorDrawState(GameObject actor) + => (DrawState*)(&((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->RenderFlags); + + private static int ObjectTableIndex(GameObject actor) + => ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->ObjectIndex; + + private void WriteInvisible(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + *ActorDrawState(actor!) |= DrawState.Invisibility; + + var gPose = IsGPoseActor(tableIndex); + if (gPose) + DisableDraw(actor!); + + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + { + *ActorDrawState(mount) |= DrawState.Invisibility; + if (gPose) + DisableDraw(mount); + } + } + + private void WriteVisible(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + *ActorDrawState(actor!) &= ~DrawState.Invisibility; + + var gPose = IsGPoseActor(tableIndex); + if (gPose) + EnableDraw(actor!); + + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + { + *ActorDrawState(mount) &= ~DrawState.Invisibility; + if (gPose) + EnableDraw(mount); + } + + GameObjectRedrawn?.Invoke(actor!.Address, tableIndex); + } + + private void ReloadActor(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + if (actor!.Address == _targets.Target?.Address) + _target = tableIndex; + + _queue.Add(~tableIndex); + } + + private void ReloadActorAfterGPose(GameObject? actor) + { + if (_objects[GPosePlayerIdx] != null) + { + ReloadActor(actor); + return; + } + + if (actor != null) + { + WriteInvisible(actor); + _afterGPoseQueue.Add(~ObjectTableIndex(actor)); + } + } + + private void HandleTarget() + { + if (_target < 0) + return; + + var actor = _objects[_target]; + if (actor == null || _targets.Target != null) + return; + + _targets.SetTarget(actor); + _target = -1; + } + + private void HandleRedraw() + { + if (_queue.Count == 0) + return; + + var numKept = 0; + for (var i = 0; i < _queue.Count; ++i) + { + var idx = _queue[i]; + if (FindCorrectActor(idx < 0 ? ~idx : idx, out var obj)) + _afterGPoseQueue.Add(idx < 0 ? idx : ~idx); + + if (obj != null) + { + if (idx < 0) + { + WriteInvisible(obj); + _queue[numKept++] = ObjectTableIndex(obj); + } + else + { + WriteVisible(obj); + } + } + } + + _queue.RemoveRange(numKept, _queue.Count - numKept); + } + + private void HandleAfterGPose() + { + if (_afterGPoseQueue.Count == 0 || _inGPose) + return; + + var numKept = 0; + for (var i = 0; i < _afterGPoseQueue.Count; ++i) + { + var idx = _afterGPoseQueue[i]; + if (idx < 0) + { + var newIdx = ~idx; + WriteInvisible(_objects[newIdx]); + _afterGPoseQueue[numKept++] = newIdx; + } + else + { + WriteVisible(_objects[idx]); + } + } + + _afterGPoseQueue.RemoveRange(numKept, _afterGPoseQueue.Count - numKept); + } + + private void OnUpdateEvent(object framework) + { + if (_conditions[ConditionFlag.BetweenAreas51] + || _conditions[ConditionFlag.BetweenAreas] + || _conditions[ConditionFlag.OccupiedInCutSceneEvent]) + return; + + SetGPose(); + HandleRedraw(); + HandleAfterGPose(); + HandleTarget(); + } + + public void RedrawObject(GameObject? actor, RedrawType settings) + { + switch (settings) + { + case RedrawType.Redraw: + ReloadActor(actor); + break; + case RedrawType.AfterGPose: + ReloadActorAfterGPose(actor); + break; + default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); + } + } + + private static GameObject? GetLocalPlayer() + { + var gPosePlayer = DalamudServices.Objects[GPosePlayerIdx]; + return gPosePlayer ?? DalamudServices.Objects[0]; + } + + public bool GetName(string lowerName, out GameObject? actor) + { + (actor, var ret) = lowerName switch + { + "" => (null, true), + "" => (GetLocalPlayer(), true), + "self" => (GetLocalPlayer(), true), + "" => (_targets.Target, true), + "target" => (_targets.Target, true), + "" => (_targets.FocusTarget, true), + "focus" => (_targets.FocusTarget, true), + "" => (_targets.MouseOverTarget, true), + "mouseover" => (_targets.MouseOverTarget, true), + _ => (null, false), + }; + return ret; + } + + public void RedrawObject(int tableIndex, RedrawType settings) + { + if (tableIndex >= 0 && tableIndex < _objects.Length) + RedrawObject(_objects[tableIndex], settings); + } + + public void RedrawObject(string name, RedrawType settings) + { + var lowerName = name.ToLowerInvariant(); + if (GetName(lowerName, out var target)) + RedrawObject(target, settings); + else + foreach (var actor in _objects.Where(a => a.Name.ToString().ToLowerInvariant() == lowerName)) + RedrawObject(actor, settings); + } + + public void RedrawAll(RedrawType settings) + { + foreach (var actor in _objects) + RedrawObject(actor, settings); + } +} diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4050d2f1..edcb76d5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -53,6 +53,7 @@ public partial class PathResolver : IDisposable _paths = new PathState(this); _meta = new MetaState(_paths.HumanVTable); _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); + Enable(); } // The modified resolver that handles game path resolving. diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs index 0696271d..0b487a8f 100644 --- a/Penumbra/Mods/Editor/Mod.Normalization.cs +++ b/Penumbra/Mods/Editor/Mod.Normalization.cs @@ -62,7 +62,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); } finally { @@ -75,7 +75,7 @@ public partial class Mod { if( Directory.Exists( _normalizationDirName ) ) { - ChatUtil.NotificationMessage( "Could not normalize mod:\n" + Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", NotificationType.Error ); return false; @@ -83,7 +83,7 @@ public partial class Mod if( Directory.Exists( _oldDirName ) ) { - ChatUtil.NotificationMessage( "Could not normalize mod:\n" + Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", NotificationType.Error ); return false; @@ -173,7 +173,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); _redirections = null; } @@ -201,7 +201,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); } return false; @@ -223,7 +223,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); foreach( var dir in _mod.ModPath.EnumerateDirectories() ) { if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 24d3a0e6..0c9c69cc 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -163,7 +163,7 @@ public partial class Mod } // Return the state of the new potential name of a directory. - public static NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) + public NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) { directory = null; if( newName.Length == 0 ) @@ -182,7 +182,7 @@ public partial class Mod return NewDirectoryState.ContainsInvalidSymbols; } - directory = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, fixedNewName ) ); + directory = new DirectoryInfo( Path.Combine( BasePath.FullName, fixedNewName ) ); if( File.Exists( directory.FullName ) ) { return NewDirectoryState.ExistsAsFile; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs index cecf73b4..9b2fc839 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Local.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Local.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using ImGuiScene; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 4e482612..fbc30e64 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; @@ -14,43 +15,37 @@ public sealed partial class Mod { public sealed partial class Manager { - public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ); + public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); public event ModOptionChangeDelegate ModOptionChanged; - public void ChangeModGroupType( Mod mod, int groupIdx, GroupType type ) - { - var group = mod._groups[ groupIdx ]; - if( group.Type == type ) - { - return; - } - - mod._groups[ groupIdx ] = group.Convert( type ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1 ); - } - - public void ChangeModGroupDefaultOption( Mod mod, int groupIdx, uint defaultOption ) + public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { var group = mod._groups[groupIdx]; - if( group.DefaultSettings == defaultOption ) - { + if (group.Type == type) return; - } - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke( ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1 ); + mod._groups[groupIdx] = group.Convert(type); + ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } - public void RenameModGroup( Mod mod, int groupIdx, string newName ) + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) { - var group = mod._groups[ groupIdx ]; - var oldName = group.Name; - if( oldName == newName || !VerifyFileName( mod, group, newName, true ) ) - { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) return; - } - group.DeleteFile( mod.ModPath, groupIdx ); + group.DefaultSettings = defaultOption; + ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + } + + public void RenameModGroup(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(mod, group, newName, true)) + return; + + group.DeleteFile(mod.ModPath, groupIdx); var _ = group switch { @@ -59,61 +54,63 @@ public sealed partial class Mod _ => newName, }; - ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } - public void AddModGroup( Mod mod, GroupType type, string newName ) + public void AddModGroup(Mod mod, GroupType type, string newName) { - if( !VerifyFileName( mod, null, newName, true ) ) - { + if (!VerifyFileName(mod, null, newName, true)) return; - } - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max( o => o.Priority ) + 1; + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - mod._groups.Add( type == GroupType.Multi - ? new MultiModGroup { Name = newName, Priority = maxPriority } - : new SingleModGroup { Name = newName, Priority = maxPriority } ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1 ); - } - - public void DeleteModGroup( Mod mod, int groupIdx ) - { - var group = mod._groups[ groupIdx ]; - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 ); - mod._groups.RemoveAt( groupIdx ); - UpdateSubModPositions( mod, groupIdx ); - group.DeleteFile( mod.ModPath, groupIdx ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); - } - - public void MoveModGroup( Mod mod, int groupIdxFrom, int groupIdxTo ) - { - if( mod._groups.Move( groupIdxFrom, groupIdxTo ) ) - { - UpdateSubModPositions( mod, Math.Min( groupIdxFrom, groupIdxTo ) ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo ); - } - } - - private static void UpdateSubModPositions( Mod mod, int fromGroup ) - { - foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( fromGroup ) ) - { - foreach( var (o, optionIdx) in group.OfType< SubMod >().WithIndex() ) + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup { - o.SetPosition( groupIdx, optionIdx ); + Name = newName, + Priority = maxPriority, } + : new SingleModGroup + { + Name = newName, + Priority = maxPriority, + }); + ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } + + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + group.DeleteFile(mod.ModPath, groupIdx); + ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + } + + public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) + { + if (mod._groups.Move(groupIdxFrom, groupIdxTo)) + { + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } } - public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription ) + private static void UpdateSubModPositions(Mod mod, int fromGroup) { - var group = mod._groups[ groupIdx ]; - if( group.Description == newDescription ) + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) { - return; + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); } + } + + public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + if (group.Description == newDescription) + return; var _ = group switch { @@ -121,29 +118,25 @@ public sealed partial class Mod MultiModGroup m => m.Description = newDescription, _ => newDescription, }; - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } - public void ChangeOptionDescription( Mod mod, int groupIdx, int optionIdx, string newDescription ) + public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod._groups[ groupIdx ]; - var option = group[ optionIdx ]; - if( option.Description == newDescription || option is not SubMod s ) - { + var group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) return; - } s.Description = newDescription; - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } - public void ChangeGroupPriority( Mod mod, int groupIdx, int newPriority ) + public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) { - var group = mod._groups[ groupIdx ]; - if( group.Priority == newPriority ) - { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) return; - } var _ = group switch { @@ -151,193 +144,174 @@ public sealed partial class Mod MultiModGroup m => m.Priority = newPriority, _ => newPriority, }; - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } - public void ChangeOptionPriority( Mod mod, int groupIdx, int optionIdx, int newPriority ) + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) { - switch( mod._groups[ groupIdx ] ) + switch (mod._groups[groupIdx]) { case SingleModGroup: - ChangeGroupPriority( mod, groupIdx, newPriority ); + ChangeGroupPriority(mod, groupIdx, newPriority); break; case MultiModGroup m: - if( m.PrioritizedOptions[ optionIdx ].Priority == newPriority ) - { + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) return; - } - m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority ); - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1 ); + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; } } - public void RenameOption( Mod mod, int groupIdx, int optionIdx, string newName ) + public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch( mod._groups[ groupIdx ] ) + switch (mod._groups[groupIdx]) { case SingleModGroup s: - if( s.OptionData[ optionIdx ].Name == newName ) - { + if (s.OptionData[optionIdx].Name == newName) return; - } - s.OptionData[ optionIdx ].Name = newName; + s.OptionData[optionIdx].Name = newName; break; case MultiModGroup m: - var option = m.PrioritizedOptions[ optionIdx ].Mod; - if( option.Name == newName ) - { + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) return; - } option.Name = newName; break; } - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } - public void AddOption( Mod mod, int groupIdx, string newName ) + public void AddOption(Mod mod, int groupIdx, string newName) { - var group = mod._groups[ groupIdx ]; - var subMod = new SubMod( mod ) { Name = newName }; - subMod.SetPosition( groupIdx, group.Count ); - switch( group ) + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) { case SingleModGroup s: - s.OptionData.Add( subMod ); + s.OptionData.Add(subMod); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( subMod, 0 ) ); + m.PrioritizedOptions.Add((subMod, 0)); break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } - public void AddOption( Mod mod, int groupIdx, ISubMod option, int priority = 0 ) + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) { - if( option is not SubMod o ) - { + if (option is not SubMod o) return; - } - var group = mod._groups[ groupIdx ]; - if( group.Count > 63 ) + var group = mod._groups[groupIdx]; + if (group.Count > 63) { Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group." ); + + "since only up to 64 options are supported in one group."); return; } - o.SetPosition( groupIdx, group.Count ); + o.SetPosition(groupIdx, group.Count); - switch( group ) + switch (group) { case SingleModGroup s: - s.OptionData.Add( o ); + s.OptionData.Add(o); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( o, priority ) ); + m.PrioritizedOptions.Add((o, priority)); break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } - public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) { - var group = mod._groups[ groupIdx ]; - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); - switch( group ) + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + switch (group) { case SingleModGroup s: - s.OptionData.RemoveAt( optionIdx ); + s.OptionData.RemoveAt(optionIdx); break; case MultiModGroup m: - m.PrioritizedOptions.RemoveAt( optionIdx ); + m.PrioritizedOptions.RemoveAt(optionIdx); break; } - group.UpdatePositions( optionIdx ); - ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 ); + group.UpdatePositions(optionIdx); + ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } - public void MoveOption( Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo ) + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) { - var group = mod._groups[ groupIdx ]; - if( group.MoveOption( optionIdxFrom, optionIdxTo ) ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo ); - } + var group = mod._groups[groupIdx]; + if (group.MoveOption(optionIdxFrom, optionIdxTo)) + ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); } - public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } - public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileData.SetEquals( replacements ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData = replacements; - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } - public void OptionAddFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > additions ) + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var subMod = GetSubMod(mod, groupIdx, optionIdx); var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom( additions ); - if( oldCount != subMod.FileData.Count ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1 ); - } + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); } - public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileSwapData.SetEquals( swaps ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData = swaps; - ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } - public static bool VerifyFileName( Mod mod, IModGroup? group, string newName, bool message ) + public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) { var path = newName.RemoveInvalidPathSymbols(); - if( path.Length == 0 - || mod.Groups.Any( o => !ReferenceEquals( o, group ) - && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase ) ) ) + if (path.Length == 0 + || mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) { - if( message ) - { - Penumbra.Log.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); - } + if (message) + _chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); return false; } @@ -345,43 +319,35 @@ public sealed partial class Mod return true; } - private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx ) + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { - if( groupIdx == -1 && optionIdx == 0 ) - { + if (groupIdx == -1 && optionIdx == 0) return mod._default; - } - return mod._groups[ groupIdx ] switch + return mod._groups[groupIdx] switch { - SingleModGroup s => s.OptionData[ optionIdx ], - MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, _ => throw new InvalidOperationException(), }; } - private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) { - if( type == ModOptionChangeType.PrepareChange ) - { + if (type == ModOptionChangeType.PrepareChange) return; - } // File deletion is handled in the actual function. - if( type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved ) + if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) { mod.SaveAllGroups(); } else { - if( groupIdx == -1 ) - { + if (groupIdx == -1) mod.SaveDefaultModDelayed(); - } else - { - IModGroup.SaveDelayed( mod._groups[ groupIdx ], mod.ModPath, groupIdx ); - } + IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); } bool ComputeChangedItems() @@ -396,20 +362,20 @@ public sealed partial class Mod ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), ModOptionChangeType.PriorityChanged => false, ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.OptionMoved => false, ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ) ), + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ) ), + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ) ), + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), ModOptionChangeType.DisplayChange => false, _ => false, }; } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index dace9f57..83927a1e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -130,8 +130,8 @@ public sealed partial class Mod } _exportDirectory = null; - Penumbra.Config.ExportDirectory = string.Empty; - Penumbra.Config.Save(); + _config.ExportDirectory = string.Empty; + _config.Save(); return; } @@ -166,8 +166,8 @@ public sealed partial class Mod if( change ) { - Penumbra.Config.ExportDirectory = dir.FullName; - Penumbra.Config.Save(); + _config.ExportDirectory = dir.FullName; + _config.Save(); } } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 55ef005f..95f40592 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -2,12 +2,13 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Penumbra.Util; namespace Penumbra.Mods; public sealed partial class Mod { - public sealed partial class Manager : IReadOnlyList< Mod > + public sealed partial class Manager : IReadOnlyList { // Set when reading Config and migrating from v4 to v5. public static bool MigrateModBackups = false; @@ -16,55 +17,60 @@ public sealed partial class Mod // Mods are added when they are created or imported. // Mods are removed when they are deleted or when they are toggled in any collection. // Also gets cleared on mod rediscovery. - public readonly HashSet< Mod > NewMods = new(); + public readonly HashSet NewMods = new(); - private readonly List< Mod > _mods = new(); + private readonly List _mods = new(); - public Mod this[ int idx ] - => _mods[ idx ]; + public Mod this[int idx] + => _mods[idx]; - public Mod this[ Index idx ] - => _mods[ idx ]; + public Mod this[Index idx] + => _mods[idx]; public int Count => _mods.Count; - public IEnumerator< Mod > GetEnumerator() + public IEnumerator GetEnumerator() => _mods.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( string modDirectory ) + private readonly Configuration _config; + private readonly ChatService _chat; + + public Manager(StartTracker time, Configuration config, ChatService chat) { + using var timer = time.Measure(StartTimeType.Mods); + _config = config; + _chat = chat; ModDirectoryChanged += OnModDirectoryChange; - SetBaseDirectory( modDirectory, true ); - UpdateExportDirectory( Penumbra.Config.ExportDirectory, false ); + SetBaseDirectory(config.ModDirectory, true); + UpdateExportDirectory(_config.ExportDirectory, false); ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; + DiscoverMods(); } // Try to obtain a mod by its directory name (unique identifier, preferred), // or the first mod of the given name if no directory fits. - public bool TryGetMod( string modDirectory, string modName, [NotNullWhen( true )] out Mod? mod ) + public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) { mod = null; - foreach( var m in _mods ) + foreach (var m in _mods) { - if( string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase) ) + if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) { mod = m; return true; } - if( m.Name == modName ) - { + if (m.Name == modName) mod ??= m; - } } return mod != null; } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 3a849a26..3f5db087 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -18,6 +18,9 @@ public partial class Mod public DirectoryInfo ModPath { get; private set; } public int Index { get; private set; } = -1; + public bool IsTemporary + => Index < 0; + // Unused if Index < 0 but used for special temporary mods. public int Priority => 0; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index fa9fec1d..9ea21820 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -4,52 +4,37 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Plugin; using OtterGui.Filesystem; -using Penumbra.Services; - +using Penumbra.Services; +using Penumbra.Util; + namespace Penumbra.Mods; -public sealed class ModFileSystem : FileSystem< Mod >, IDisposable +public sealed class ModFileSystem : FileSystem, IDisposable, ISaveable { - public static string ModFileSystemFile(DalamudPluginInterface pi) - => Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" ); - - // Save the current sort order. - // Does not save or copy the backup in the current mod directory, - // as this is done on mod directory changes only. - // TODO - private void SaveFilesystem() - { - SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true ); - Penumbra.Log.Verbose( "Saved mod filesystem." ); - } - - private void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveFilesystem ), SaveFilesystem ); + private readonly Mod.Manager _modManager; + private readonly FilenameService _files; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public static ModFileSystem Load() + public ModFileSystem(Mod.Manager modManager, FilenameService files) { - var ret = new ModFileSystem(); - ret.Reload(); - - ret.Changed += ret.OnChange; - Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - Penumbra.ModManager.ModDataChanged += ret.OnDataChange; - Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; - - return ret; + _modManager = modManager; + _files = files; + Reload(); + Changed += OnChange; + _modManager.ModDiscoveryFinished += Reload; + _modManager.ModDataChanged += OnDataChange; + _modManager.ModPathChanged += OnModPathChange; } public void Dispose() { - Penumbra.ModManager.ModPathChanged -= OnModPathChange; - Penumbra.ModManager.ModDiscoveryFinished -= Reload; - Penumbra.ModManager.ModDataChanged -= OnDataChange; + _modManager.ModPathChanged -= OnModPathChange; + _modManager.ModDiscoveryFinished -= Reload; + _modManager.ModDataChanged -= OnDataChange; } - public struct ImportDate : ISortMode< Mod > + public struct ImportDate : ISortMode { public string Name => "Import Date (Older First)"; @@ -57,11 +42,11 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable public string Description => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; - public IEnumerable< IPath > GetChildren( Folder f ) - => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderBy( l => l.Value.ImportDate ) ); + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate)); } - public struct InverseImportDate : ISortMode< Mod > + public struct InverseImportDate : ISortMode { public string Name => "Import Date (Newer First)"; @@ -69,71 +54,61 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable public string Description => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; - public IEnumerable< IPath > GetChildren( Folder f ) - => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderByDescending( l => l.Value.ImportDate ) ); + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate)); } // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() - { + { // TODO - if( Load( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) - { - Save(); - } + if (Load(new FileInfo(_files.FilesystemFile), _modManager, ModToIdentifier, ModToName)) + Penumbra.SaveService.ImmediateSave(this); - Penumbra.Log.Debug( "Reloaded mod filesystem." ); + Penumbra.Log.Debug("Reloaded mod filesystem."); } // Save the filesystem on every filesystem change except full reloading. - private void OnChange( FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3 ) + private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) { - if( type != FileSystemChangeType.Reload ) - { - Save(); - } + if (type != FileSystemChangeType.Reload) + Penumbra.SaveService.QueueSave(this); } // Update sort order when defaulted mod names change. - private void OnDataChange( ModDataChangeType type, Mod mod, string? oldName ) + private void OnDataChange(ModDataChangeType type, Mod mod, string? oldName) { - if( type.HasFlag( ModDataChangeType.Name ) && oldName != null ) + if (type.HasFlag(ModDataChangeType.Name) && oldName != null) { var old = oldName.FixName(); - if( Find( old, out var child ) && child is not Folder ) - { - Rename( child, mod.Name.Text ); - } + if (Find(old, out var child) && child is not Folder) + Rename(child, mod.Name.Text); } } // Update the filesystem if a mod has been added or removed. // Save it, if the mod directory has been moved, since this will change the save format. - private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath ) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath) { - switch( type ) + switch (type) { case ModPathChangeType.Added: var originalName = mod.Name.Text.FixName(); var name = originalName; var counter = 1; - while( Find( name, out _ ) ) - { + while (Find(name, out _)) name = $"{originalName} ({++counter})"; - } - CreateLeaf( Root, name, mod ); + CreateLeaf(Root, name, mod); break; case ModPathChangeType.Deleted: - if( FindLeaf( mod, out var leaf ) ) - { - Delete( leaf ); - } + if (FindLeaf(mod, out var leaf)) + Delete(leaf); break; case ModPathChangeType.Moved: - Save(); + Penumbra.SaveService.QueueSave(this); break; case ModPathChangeType.Reloaded: // Nothing @@ -142,32 +117,44 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable } // Search the entire filesystem for the leaf corresponding to a mod. - public bool FindLeaf( Mod mod, [NotNullWhen( true )] out Leaf? leaf ) + public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf) { - leaf = Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) - .OfType< Leaf >() - .FirstOrDefault( l => l.Value == mod ); + leaf = Root.GetAllDescendants(ISortMode.Lexicographical) + .OfType() + .FirstOrDefault(l => l.Value == mod); return leaf != null; } // Used for saving and loading. - private static string ModToIdentifier( Mod mod ) + private static string ModToIdentifier(Mod mod) => mod.ModPath.Name; - private static string ModToName( Mod mod ) + private static string ModToName(Mod mod) => mod.Name.Text.FixName(); // Return whether a mod has a custom path or is just a numbered default path. - public static bool ModHasDefaultPath( Mod mod, string fullPath ) + public static bool ModHasDefaultPath(Mod mod, string fullPath) { - var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?$" ); - return regex.IsMatch( fullPath ); + var regex = new Regex($@"^{Regex.Escape(ModToName(mod))}( \(\d+\))?$"); + return regex.IsMatch(fullPath); } - private static (string, bool) SaveMod( Mod mod, string fullPath ) + private static (string, bool) SaveMod(Mod mod, string fullPath) // Only save pairs with non-default paths. - => ModHasDefaultPath( mod, fullPath ) - ? ( string.Empty, false ) - : ( ModToIdentifier( mod ), true ); -} \ No newline at end of file + => ModHasDefaultPath(mod, fullPath) + ? (string.Empty, false) + : (ModToIdentifier(mod), true); + + public string ToFilename(FilenameService fileNames) + => fileNames.FilesystemFile; + + public void Save(StreamWriter writer) + => SaveToFile(writer, SaveMod, true); + + public string TypeName + => "Mod File System"; + + public string LogName(string _) + => "to file"; +} diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 90bc687a..70840b24 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -21,69 +21,67 @@ public partial class Mod public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } - public int OptionPriority( Index idx ) - => PrioritizedOptions[ idx ].Priority; + public int OptionPriority(Index idx) + => PrioritizedOptions[idx].Priority; - public ISubMod this[ Index idx ] - => PrioritizedOptions[ idx ].Mod; + public ISubMod this[Index idx] + => PrioritizedOptions[idx].Mod; [JsonIgnore] public int Count => PrioritizedOptions.Count; - public readonly List< (SubMod Mod, int Priority) > PrioritizedOptions = new(); + public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); - public IEnumerator< ISubMod > GetEnumerator() - => PrioritizedOptions.Select( o => o.Mod ).GetEnumerator(); + public IEnumerator GetEnumerator() + => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static MultiModGroup? Load( Mod mod, JObject json, int groupIdx ) + public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { var ret = new MultiModGroup() { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, - DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, }; - if( ret.Name.Length == 0 ) - { + if (ret.Name.Length == 0) return null; - } var options = json["Options"]; - if( options != null ) - { - foreach( var child in options.Children() ) + if (options != null) + foreach (var child in options.Children()) { - if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) + if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - ChatUtil.NotificationMessage( $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( + $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", + NotificationType.Warning); break; } - var subMod = new SubMod( mod ); - subMod.SetPosition( groupIdx, ret.PrioritizedOptions.Count ); - subMod.Load( mod.ModPath, child, out var priority ); - ret.PrioritizedOptions.Add( ( subMod, priority ) ); + var subMod = new SubMod(mod); + subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + subMod.Load(mod.ModPath, child, out var priority); + ret.PrioritizedOptions.Add((subMod, priority)); } - } - ret.DefaultSettings = (uint) (ret.DefaultSettings & ( ( 1ul << ret.Count ) - 1 )); + ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); return ret; } - public IModGroup Convert( GroupType type ) + public IModGroup Convert(GroupType type) { - switch( type ) + switch (type) { case GroupType.Multi: return this; case GroupType.Single: @@ -92,32 +90,28 @@ public partial class Mod Name = Name, Description = Description, Priority = Priority, - DefaultSettings = ( uint )Math.Max( Math.Min( Count - 1, BitOperations.TrailingZeroCount( DefaultSettings) ), 0 ), + DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), }; - multi.OptionData.AddRange( PrioritizedOptions.Select( p => p.Mod ) ); + multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } - public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if( !PrioritizedOptions.Move( optionIdxFrom, optionIdxTo ) ) - { + if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) return false; - } - DefaultSettings = Functions.MoveBit( DefaultSettings, optionIdxFrom, optionIdxTo ); - UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions( int from = 0 ) + public void UpdatePositions(int from = 0) { - foreach( var ((o, _), i) in PrioritizedOptions.WithIndex().Skip( from ) ) - { - o.SetPosition( o.GroupIdx, i ); - } + foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) + o.SetPosition(o.GroupIdx, i); } } -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 563e6de9..51204402 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,11 +29,11 @@ using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; -using Penumbra.Interop.Services; - +using Penumbra.Interop.Services; + namespace Penumbra; -public class Penumbra : IDalamudPlugin +public partial class Penumbra : IDalamudPlugin { public string Name => "Penumbra"; @@ -43,8 +43,10 @@ public class Penumbra : IDalamudPlugin public static readonly string CommitHash = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static Logger Log { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; + public static SaveService SaveService { get; private set; } = null!; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; @@ -60,7 +62,7 @@ public class Penumbra : IDalamudPlugin public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainService StainService { get; private set; } = null!; - + // TODO public static DalamudServices Dalamud { get; private set; } = null!; @@ -68,23 +70,21 @@ public class Penumbra : IDalamudPlugin public static PerformanceTracker Performance { get; private set; } = null!; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; - internal ConfigWindow? ConfigWindow { get; private set; } - private LaunchButton? _launchButton; - private WindowSystem? _windowSystem; - private Changelog? _changelog; - private CommandHandler? _commandHandler; - private readonly ResourceWatcher _resourceWatcher; - private bool _disposed; + public readonly PathResolver PathResolver; + public readonly RedrawService RedrawService; + public readonly ModFileSystem ModFileSystem; + public PenumbraApi Api = null!; + public HttpApi HttpApi = null!; + public PenumbraIpcProviders IpcProviders = null!; + internal ConfigWindow? ConfigWindow { get; private set; } + private PenumbraWindowSystem? _windowSystem; + private CommandHandler? _commandHandler; + private readonly ResourceWatcher _resourceWatcher; + private bool _disposed; private readonly PenumbraNew _tmp; public static ItemData ItemData { get; private set; } = null!; - + // TODO public static ResourceManagerService ResourceManagerService { get; private set; } = null!; public static CharacterResolver CharacterResolver { get; private set; } = null!; @@ -95,67 +95,38 @@ public class Penumbra : IDalamudPlugin Log = PenumbraNew.Log; try { - _tmp = new PenumbraNew(pluginInterface); + _tmp = new PenumbraNew(this, pluginInterface); + ChatService = _tmp.Services.GetRequiredService(); + SaveService = _tmp.Services.GetRequiredService(); Performance = _tmp.Services.GetRequiredService(); ValidityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - ItemData = _tmp.Services.GetRequiredService().AwaitedService; - Dalamud = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); - ResidentResources = _tmp.Services.GetRequiredService(); - - ResourceManagerService = _tmp.Services.GetRequiredService(); - - _tmp.Services.GetRequiredService().Measure(StartTimeType.Mods, () => - { - ModManager = new Mod.Manager(Config.ModDirectory); - ModManager.DiscoverMods(); - }); - - _tmp.Services.GetRequiredService().Measure(StartTimeType.Collections, () => - { - CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService(), ModManager); - CollectionManager.CreateNecessaryCaches(); - }); - - - TempCollections = _tmp.Services.GetRequiredService(); - - ModFileSystem = ModFileSystem.Load(); - ObjectReloader = new ObjectReloader(); - ResourceService = _tmp.Services.GetRequiredService(); - ResourceLoader = new ResourceLoader(ResourceService, _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService()); - PathResolver = new PathResolver(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), ResourceLoader); - CharacterResolver = new CharacterResolver(Config, CollectionManager, TempCollections, ResourceLoader, PathResolver); - - _resourceWatcher = new ResourceWatcher(Config, ResourceService, ResourceLoader); - + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + ItemData = _tmp.Services.GetRequiredService().AwaitedService; + Dalamud = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); + ResidentResources = _tmp.Services.GetRequiredService(); + ResourceManagerService = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); + TempCollections = _tmp.Services.GetRequiredService(); + ModFileSystem = _tmp.Services.GetRequiredService(); + RedrawService = _tmp.Services.GetRequiredService(); + ResourceService = _tmp.Services.GetRequiredService(); + ResourceLoader = _tmp.Services.GetRequiredService(); + PathResolver = _tmp.Services.GetRequiredService(); + CharacterResolver = _tmp.Services.GetRequiredService(); + _resourceWatcher = _tmp.Services.GetRequiredService(); SetupInterface(); - - if (Config.EnableMods) - { - PathResolver.Enable(); - } - - using (var tApi = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api)) - { - Api = new PenumbraApi(_tmp.Services.GetRequiredService(), this); - IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api); - HttpApi = new HttpApi(Api); - if (Config.EnableHttpApi) - HttpApi.CreateWebServer(); - - SubscribeItemLinks(); - } + SetupApi(); ValidityChecker.LogExceptions(); Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); @@ -172,56 +143,47 @@ public class Penumbra : IDalamudPlugin } } + private void SetupApi() + { + using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); + Api = (PenumbraApi)_tmp.Services.GetRequiredService(); + IpcProviders = _tmp.Services.GetRequiredService(); + HttpApi = _tmp.Services.GetRequiredService(); + + if (Config.EnableHttpApi) + HttpApi.CreateWebServer(); + Api.ChangedItemTooltip += it => + { + if (it is Item) + ImGui.TextUnformatted("Left Click to create an item link in chat."); + }; + Api.ChangedItemClicked += (button, it) => + { + if (button == MouseButton.Left && it is Item item) + ChatService.LinkItem(item); + }; + } + private void SetupInterface() { Task.Run(() => { using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); - var changelog = ConfigWindow.CreateChangelog(); - var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) - { - IsOpen = Config.DebugMode, - }; - var btn = new LaunchButton(cfg); - var system = new WindowSystem(Name); - var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, - Actors); - system.AddWindow(cfg); - system.AddWindow(cfg.ModEditPopup); - system.AddWindow(changelog); + var system = _tmp.Services.GetRequiredService(); + _commandHandler = _tmp.Services.GetRequiredService(); if (!_disposed) { - _changelog = changelog; - ConfigWindow = cfg; - _windowSystem = system; - _launchButton = btn; - _commandHandler = cmd; - DalamudServices.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; - DalamudServices.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + _windowSystem = system; + ConfigWindow = system.Window; } else { - cfg.Dispose(); - btn.Dispose(); - cmd.Dispose(); + system.Dispose(); } } ); } - private void DisposeInterface() - { - if (_windowSystem != null) - DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - - _launchButton?.Dispose(); - if (ConfigWindow != null) - { - DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; - ConfigWindow.Dispose(); - } - } - public event Action? EnabledChange; public bool SetEnabled(bool enabled) @@ -237,7 +199,7 @@ public class Penumbra : IDalamudPlugin { CollectionManager.Default.SetFiles(); ResidentResources.Reload(); - ObjectReloader.RedrawAll(RedrawType.Redraw); + RedrawService.RedrawAll(RedrawType.Redraw); } } else @@ -247,7 +209,7 @@ public class Penumbra : IDalamudPlugin { CharacterUtility.ResetAll(); ResidentResources.Reload(); - ObjectReloader.RedrawAll(RedrawType.Redraw); + RedrawService.RedrawAll(RedrawType.Redraw); } } @@ -258,52 +220,15 @@ public class Penumbra : IDalamudPlugin } public void ForceChangelogOpen() - { - if (_changelog != null) - _changelog.ForceOpen = true; - } + => _windowSystem?.ForceChangelogOpen(); - private void SubscribeItemLinks() - { - Api.ChangedItemTooltip += it => - { - if (it is Item) - ImGui.TextUnformatted("Left Click to create an item link in chat."); - }; - Api.ChangedItemClicked += (button, it) => - { - if (button == MouseButton.Left && it is Item item) - ChatUtil.LinkItem(item); - }; - } - public void Dispose() { if (_disposed) return; - - // TODO + _tmp?.Dispose(); _disposed = true; - HttpApi?.Dispose(); - IpcProviders?.Dispose(); - Api?.Dispose(); - _commandHandler?.Dispose(); - StainService?.Dispose(); - ItemData?.Dispose(); - Actors?.Dispose(); - Identifier?.Dispose(); - Framework?.Dispose(); - DisposeInterface(); - ObjectReloader?.Dispose(); - ModFileSystem?.Dispose(); - CollectionManager?.Dispose(); - CharacterResolver?.Dispose(); // disposes PathResolver, TODO - _resourceWatcher?.Dispose(); - ResourceLoader?.Dispose(); - GameEvents?.Dispose(); - CharacterUtility?.Dispose(); - Performance?.Dispose(); } public string GatherSupportInformation() diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 429cd5ae..004fd284 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; @@ -10,8 +11,10 @@ using Penumbra.GameData.Data; using Penumbra.Interop; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Interop.Services; +using Penumbra.Interop.Services; +using Penumbra.Mods; using Penumbra.Services; +using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.Util; @@ -25,7 +28,7 @@ public class PenumbraNew public static readonly Logger Log = new(); public readonly ServiceProvider Services; - public PenumbraNew(DalamudPluginInterface pi) + public PenumbraNew(Penumbra pnumb, DalamudPluginInterface pi) { var startTimer = new StartTracker(); using var time = startTimer.Measure(StartTimeType.Total); @@ -38,11 +41,14 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Dalamud services var dalamud = new DalamudServices(pi); dalamud.AddServices(services); + services.AddSingleton(pnumb); // Add Game Data services.AddSingleton() @@ -63,22 +69,43 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - + .AddSingleton() + .AddSingleton(); + // Add Configuration services.AddTransient() .AddSingleton(); // Add Collection Services services.AddTransient() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Add Mod Services - // TODO - services.AddSingleton(); + services.AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add main services + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Interface - Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add API + services.AddSingleton() + .AddSingleton() + .AddSingleton(); + + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index b976bef0..9d14ae16 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -27,7 +27,7 @@ public class CommunicatorService : IDisposable /// Parameter is the affected mod. /// Parameter is either null or the old name of the mod. /// - public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); + public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); public void Dispose() { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 4d37f693..b187e991 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -306,7 +306,7 @@ public class ConfigMigrationService return; var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); - var defaultCollectionFile = defaultCollection.FileName; + var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection)); if (defaultCollectionFile.Exists) return; @@ -339,7 +339,7 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); - defaultCollection.Save(); + Penumbra.SaveService.ImmediateSave(defaultCollection); } catch (Exception e) { diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 0e883149..d57c854e 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -37,7 +37,7 @@ public class FilenameService /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. - public string LocalDataFile(IModReadable mod) + public string LocalDataFile(Mod mod) => mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName); /// Obtain the path of the local data file given a mod directory. @@ -65,7 +65,7 @@ public class FilenameService } /// Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. - public string ModMetaPath(IModReadable mod) + public string ModMetaPath(Mod mod) => mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName); /// Obtain the path of the meta file given a mod directory. diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index d86167bc..a5bc3f3c 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -38,7 +38,7 @@ public class ValidityChecker public void LogExceptions() { if( ImcExceptions.Count > 0 ) - ChatUtil.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs new file mode 100644 index 00000000..3119e48c --- /dev/null +++ b/Penumbra/UI/Changelog.cs @@ -0,0 +1,386 @@ +using OtterGui.Widgets; + +namespace Penumbra.UI; + +public class PenumbraChangelog +{ + public const int LastChangelogVersion = 0; + + private readonly Configuration _config; + public readonly Changelog Changelog; + + public PenumbraChangelog(Configuration config) + { + _config = config; + Changelog = new Changelog("Penumbra Changelog", ConfigData, Save); + + Add5_7_0(Changelog); + Add5_7_1(Changelog); + Add5_8_0(Changelog); + Add5_8_7(Changelog); + Add5_9_0(Changelog); + Add5_10_0(Changelog); + Add5_11_0(Changelog); + Add5_11_1(Changelog); + Add6_0_0(Changelog); + Add6_0_2(Changelog); + Add6_0_5(Changelog); + Add6_1_0(Changelog); + Add6_1_1(Changelog); + Add6_2_0(Changelog); + Add6_3_0(Changelog); + Add6_4_0(Changelog); + Add6_5_0(Changelog); + Add6_5_2(Changelog); + Add6_6_0(Changelog); + Add6_6_1(Changelog); + } + + #region Changelogs + + private static void Add6_6_1(Changelog log) + => log.NextVersion("Version 0.6.6.1") + .RegisterEntry("Added an option to make successful chat commands not print their success confirmations to chat.") + .RegisterEntry("Fixed an issue with migration of old mods not working anymore (fixes Material UI problems).") + .RegisterEntry("Fixed some issues with using the Assign Current Player and Assign Current Target buttons."); + + private static void Add6_6_0(Changelog log) + => log.NextVersion("Version 0.6.6.0") + .RegisterEntry( + "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively.") + .RegisterEntry( + "Added an option to display Single Selection Groups as a group of radio buttons similar to Multi Selection Groups, when the number of available options is below the specified value. Default value is 2.") + .RegisterEntry("Added a button in option groups to collapse the option list if it has more than 5 available options.") + .RegisterEntry( + "Penumbra now circumvents the games inability to read files at paths longer than 260 UTF16 characters and can also deal with generic unicode symbols in paths.") + .RegisterEntry( + "This means that Penumbra should no longer cause issues when files become too long or when there is a non-ASCII character in them.", + 1) + .RegisterEntry( + "Shorter paths are still better, so restrictions on the root directory have not been relaxed. Mod names should no longer replace non-ASCII symbols on import though.", + 1) + .RegisterEntry( + "Resource logging has been relegated to its own tab with better filtering. Please do not keep resource logging on arbitrarily or set a low record limit if you do, otherwise this eats a lot of performance and memory after a while.") + .RegisterEntry( + "Added a lot of facilities to edit the shader part of .mtrl files and .shpk files themselves in the Advanced Editing Tab (Thanks Ny and aers).") + .RegisterEntry("Added splitting of Multi Selection Groups with too many options when importing .pmp files or adding mods via IPC.") + .RegisterEntry("Discovery, Reloading and Unloading of a specified mod is now possible via HTTP API (Thanks Sebastina).") + .RegisterEntry("Cleaned up the HTTP API somewhat, removed currently useless options.") + .RegisterEntry("Fixed an issue when extracting some textures.") + .RegisterEntry("Fixed an issue with mannequins inheriting individual assignments for the current player when using ownership.") + .RegisterEntry( + "Fixed an issue with the resolving of .phyb and .sklb files for Item Swaps of head or body items with an EST entry but no unique racial model."); + + private static void Add6_5_2(Changelog log) + => log.NextVersion("Version 0.6.5.2") + .RegisterEntry("Updated for game version 6.31 Hotfix.") + .RegisterEntry( + "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)") + .RegisterEntry("Those are now accurately parsed from TTMPs, too.", 1) + .RegisterEntry("Improved launch times somewhat through parallelization of some tasks.") + .RegisterEntry( + "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled.") + .RegisterEntry("Fixed an issue with IMC changes and Mare Synchronos interoperability.") + .RegisterEntry("Fixed an issue with housing mannequins crashing the game when resource logging was enabled.") + .RegisterEntry("Fixed an issue generating Mip Maps for texture import on Wine."); + + private static void Add6_5_0(Changelog log) + => log.NextVersion("Version 0.6.5.0") + .RegisterEntry("Fixed an issue with Item Swaps not using applied IMC changes in some cases.") + .RegisterEntry("Improved error message on texture import when failing to create mip maps (slightly).") + .RegisterEntry("Tried to fix duty party banner identification again, also for the recommendation window this time.") + .RegisterEntry("Added batched IPC to improve Mare performance."); + + private static void Add6_4_0(Changelog log) + => log.NextVersion("Version 0.6.4.0") + .RegisterEntry("Fixed an issue with the identification of actors in the duty group portrait.") + .RegisterEntry("Fixed some issues with wrongly cached actors and resources.") + .RegisterEntry("Fixed animation handling after redraws (notably for PLD idle animations with a shield equipped).") + .RegisterEntry("Fixed an issue with collection listing API skipping one collection.") + .RegisterEntry( + "Fixed an issue with BGM files being sometimes loaded from other collections than the base collection, causing crashes.") + .RegisterEntry( + "Also distinguished file resolving for different file categories (improving performance) and disabled resolving for script files entirely.", + 1) + .RegisterEntry("Some miscellaneous backend changes due to the Glamourer rework."); + + private static void Add6_3_0(Changelog log) + => log.NextVersion("Version 0.6.3.0") + .RegisterEntry("Add an Assign Current Target button for individual assignments") + .RegisterEntry("Try identifying all banner actors correctly for PvE duties, Crystalline Conflict and Mahjong.") + .RegisterEntry("Please let me know if this does not work for anything except identical twins.", 1) + .RegisterEntry("Add handling for the 3 new screen actors (now 8 total, for PvE dutie portraits).") + .RegisterEntry("Update the Battle NPC name database for 6.3.") + .RegisterEntry("Added API/IPC functions to obtain or set group or individual collections.") + .RegisterEntry("Maybe fix a problem with textures sometimes not loading from their corresponding collection.") + .RegisterEntry("Another try to fix a problem with the collection selectors breaking state.") + .RegisterEntry("Fix a problem identifying companions.") + .RegisterEntry("Fix a problem when deleting collections assigned to Groups.") + .RegisterEntry( + "Fix a problem when using the Assign Currently Played Character button and then logging onto a different character without restarting in between.") + .RegisterEntry("Some miscellaneous backend changes."); + + private static void Add6_2_0(Changelog log) + => log.NextVersion("Version 0.6.2.0") + .RegisterEntry("Update Penumbra for .net7, Dalamud API 8 and patch 6.3.") + .RegisterEntry("Add a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)") + .RegisterEntry("Add placeholder options for setting individual collections via chat command.") + .RegisterEntry("Add toggles to swap left and/or right rings separately for ring item swap.") + .RegisterEntry("Add handling for looping sound effects caused by animations in non-base collections.") + .RegisterEntry("Add an option to not use any mods at all in the Inspect/Try-On window.") + .RegisterEntry("Add handling for Mahjong actors.") + .RegisterEntry("Improve hint text for File Swaps in Advanced Editing, also inverted file swap display order.") + .RegisterEntry("Fix a problem where the collection selectors could get desynchronized after adding or deleting collections.") + .RegisterEntry("Fix a problem that could cause setting state to get desynchronized.") + .RegisterEntry("Fix an oversight where some special screen actors did not actually respect the settings made for them.") + .RegisterEntry("Add collection and associated game object to Full Resource Logging.") + .RegisterEntry("Add performance tracking for DEBUG-compiled versions (i.e. testing only).") + .RegisterEntry("Add some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)") + .RegisterEntry("Fix association of some vfx game objects. (0.6.1.3)") + .RegisterEntry("Stop forcing AVFX files to load synchronously. (0.6.1.3)") + .RegisterEntry("Fix an issue when incorporating deduplicated meta files. (0.6.1.2)"); + + private static void Add6_1_1(Changelog log) + => log.NextVersion("Version 0.6.1.1") + .RegisterEntry( + "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod.") + .RegisterEntry("Fix using equipment paths for accessory swaps and thus accessory swaps not working at all") + .RegisterEntry("Fix issues with swaps with gender-locked gear where the models for the other gender do not exist.") + .RegisterEntry("Fix swapping universal hairstyles for midlanders breaking them for other races.") + .RegisterEntry("Add some actual error messages on failure to create item swaps.") + .RegisterEntry("Fix warnings about more than one affected item appearing for single items."); + + private static void Add6_1_0(Changelog log) + => log.NextVersion("Version 0.6.1.0 (Happy New Year! Edition)") + .RegisterEntry("Add a prototype for Item Swapping.") + .RegisterEntry("A new tab in Advanced Editing.", 1) + .RegisterEntry("Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1) + .RegisterEntry("The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" + + " should be used when creating the swap, but you can also just swap unmodded things.", 1) + .RegisterEntry("You can write a swap to a new mod, or to a new option in the currently selected mod.", 1) + .RegisterEntry("The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1) + .RegisterEntry("More detailed help or explanations will be added later.", 1) + .RegisterEntry("Heavily improve Chat Commands. Use /penumbra help for more information.") + .RegisterEntry("Penumbra now considers meta manipulations for Changed Items.") + .RegisterEntry("Penumbra now tries to associate battle voices to specific actors, so that they work in collections.") + .RegisterEntry( + "Heavily improve .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects.") + .RegisterEntry("Improve some file handling for Mare-Interaction.") + .RegisterEntry("Add Equipment Slots to Demihuman IMC Edits.") + .RegisterEntry( + "Add a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files.") + .RegisterEntry("Add an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra.") + .RegisterEntry("Add API to copy mod settings from one mod to another.") + .RegisterEntry("Fix a problem where creating individual collections did not trigger events.") + .RegisterEntry("Add a Hack to support Anamnesis Redrawing better. (0.6.0.6)") + .RegisterEntry("Fix another problem with the aesthetician. (0.6.0.6)") + .RegisterEntry("Fix a problem with the export directory not being respected. (0.6.0.6)"); + + private static void Add6_0_5(Changelog log) + => log.NextVersion("Version 0.6.0.5") + .RegisterEntry("Allow hyphen as last character in player and retainer names.") + .RegisterEntry("Fix various bugs with ownership and GPose.") + .RegisterEntry("Fix collection selectors not updating for new or deleted collections in some cases.") + .RegisterEntry("Fix Chocobos not being recognized correctly.") + .RegisterEntry("Fix some problems with UI actors.") + .RegisterEntry("Fix problems with aesthetician again."); + + private static void Add6_0_2(Changelog log) + => log.NextVersion("Version 0.6.0.2") + .RegisterEntry("Let Bell Retainer collections apply to retainer-named mannequins.") + .RegisterEntry("Added a few informations to a help marker for new individual assignments.") + .RegisterEntry("Fix bug with Demi Human IMC paths.") + .RegisterEntry("Fix Yourself collection not applying to UI actors.") + .RegisterEntry("Fix Yourself collection not applying during aesthetician."); + + private static void Add6_0_0(Changelog log) + => log.NextVersion("Version 0.6.0.0") + .RegisterEntry("Revamped Individual Collections:") + .RegisterEntry("You can now specify individual collections for players (by name) of specific worlds or any world.", 1) + .RegisterEntry("You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", + 1) + .RegisterHighlight( + "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", + 1) + .RegisterHighlight( + "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", + 1) + .RegisterEntry("You can also manually sort your Individual Collections by drag and drop now.", 1) + .RegisterEntry("This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1) + .RegisterEntry("These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", + 1) + .RegisterEntry("General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", + 1) + .RegisterEntry( + "Added NPC Model Parsing, changes in NPC models should now display the names of the changed game objects for most NPCs.") + .RegisterEntry("Changed Items now also display variant or subtype in addition to the model set ID where applicable.") + .RegisterEntry("Collection selectors can now be filtered by name.") + .RegisterEntry("Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths.") + .RegisterEntry("Improved interface for group settings (minimally).") + .RegisterEntry("New Special or Individual Assignments now default to your current Base assignment instead of None.") + .RegisterEntry("Improved Support Info somewhat.") + .RegisterEntry("Added Dye Previews for in-game dyes and dyeing templates in Material Editing.") + .RegisterEntry("Colorset Editing now allows for negative values in all cases.") + .RegisterEntry("Added Export buttons to .mdl and .mtrl previews in Advanced Editing.") + .RegisterEntry("File Selection in the .mdl and .mtrl tabs now shows one associated game path by default and all on hover.") + .RegisterEntry( + "Added the option to reduplicate and normalize a mod, restoring all duplicates and moving the files to appropriate folders. (Duplicates Tab in Advanced Editing)") + .RegisterEntry( + "Added an option to re-export metadata changes to TexTools-typed .meta and .rgsp files. (Meta-Manipulations Tab in Advanced Editing)") + .RegisterEntry("Fixed several bugs with the incorporation of meta changes when not done during TTMP import.") + .RegisterEntry("Fixed a bug with RSP changes on non-base collections not applying correctly in some cases.") + .RegisterEntry("Fixed a bug when dragging options during mod edit.") + .RegisterEntry("Fixed a bug where sometimes the valid folder check caused issues.") + .RegisterEntry("Fixed a bug where collections with inheritances were newly saved on every load.") + .RegisterEntry("Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged).") + .RegisterEntry("Mods without names or invalid mod folders are now warnings instead of errors.") + .RegisterEntry("Added IPC events for mod deletion, addition or moves, and resolving based on game objects.") + .RegisterEntry("Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder.") + .RegisterEntry("A lot of big backend changes."); + + private static void Add5_11_1(Changelog log) + => log.NextVersion("Version 0.5.11.1") + .RegisterEntry( + "The 0.5.11.0 Update exposed an issue in Penumbras file-saving scheme that rarely could cause some, most or even all of your mods to lose their group information.") + .RegisterEntry( + "If this has happened to you, you will need to reimport affected mods, or manually restore their groups. I am very sorry for that.", + 1) + .RegisterEntry( + "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", + 1) + .RegisterHighlight( + "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", + 1); + + private static void Add5_11_0(Changelog log) + => log.NextVersion("Version 0.5.11.0") + .RegisterEntry( + "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections.") + .RegisterEntry("Moved the import date from mod metadata to local data.", 1) + .RegisterEntry("Added Favorites. You can declare mods as favorites and filter for them.", 1) + .RegisterEntry("Added Local Tags. You can apply custom Tags to mods and filter for them.", 1) + .RegisterEntry( + "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported.") + .RegisterEntry("Add backface and transparency toggles to .mtrl editing, as well as a info section.") + .RegisterEntry("Meta Manipulation editing now highlights if the selected ID is 0 or 1.") + .RegisterEntry("Fixed a bug when manually adding EQP or EQDP entries to Mods.") + .RegisterEntry("Updated some tooltips and hints.") + .RegisterEntry("Improved handling of IMC exception problems.") + .RegisterEntry("Fixed a bug with misidentification of equipment decals.") + .RegisterEntry( + "Character collections can now be set via chat command, too. (/penumbra collection character | )") + .RegisterEntry("Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule.") + .RegisterEntry("Added API to delete mods and read and set their pseudo-filesystem paths.", 1) + .RegisterEntry("Added API to check Penumbras enabled state and updates to it.", 1); + + private static void Add5_10_0(Changelog log) + => log.NextVersion("Version 0.5.10.0") + .RegisterEntry("Renamed backup functionality to export functionality.") + .RegisterEntry("A default export directory can now optionally be specified.") + .RegisterEntry("If left blank, exports will still be stored in your mod directory.", 1) + .RegisterEntry("Existing exports corresponding to existing mods will be moved automatically if the export directory is changed.", + 1) + .RegisterEntry("Added buttons to export and import all color set rows at once during material editing.") + .RegisterEntry("Fixed texture import being case sensitive on the extension.") + .RegisterEntry("Fixed special collection selector increasing in size on non-default UI styling.") + .RegisterEntry("Fixed color set rows not importing the dye values during material editing.") + .RegisterEntry("Other miscellaneous small fixes."); + + private static void Add5_9_0(Changelog log) + => log.NextVersion("Version 0.5.9.0") + .RegisterEntry("Special Collections are now split between male and female.") + .RegisterEntry("Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install.") + .RegisterEntry("Fix a bug where cutscene actors were not properly reset and could be misidentified across multiple cutscenes.") + .RegisterEntry("TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions."); + + private static void Add5_8_7(Changelog log) + => log.NextVersion("Version 0.5.8.7") + .RegisterEntry("Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7).") + .RegisterHighlight( + "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", + 1); + + private static void Add5_8_0(Changelog log) + => log.NextVersion("Version 0.5.8.0") + .RegisterEntry("Added choices what Change Logs are to be displayed. It is recommended to just keep showing all.") + .RegisterEntry("Added an Interface Collection assignment.") + .RegisterEntry("All your UI mods will have to be in the interface collection.", 1) + .RegisterEntry("Files that are categorized as UI files by the game will only check for redirections in this collection.", 1) + .RegisterHighlight( + "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1) + .RegisterEntry("New API / IPC for the Interface Collection added.", 1) + .RegisterHighlight("API / IPC consumers should verify whether they need to change resolving to the new collection.", 1) + .RegisterHighlight( + "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being.") + .RegisterEntry( + "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured.") + .RegisterEntry("Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", + 1) + .RegisterEntry("Files that the game loads super early should now be replaceable correctly via base or interface collection.") + .RegisterEntry( + "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)") + .RegisterEntry("Continued Work on the Texture Import/Export Tab:") + .RegisterEntry("Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1) + .RegisterEntry("Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1) + .RegisterEntry("Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1) + .RegisterHighlight( + "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", + 1) + .RegisterEntry( + "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it.") + .RegisterEntry("Collection Selectors now display None at the top if available.") + .RegisterEntry( + "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically.") + .RegisterEntry("Fixed an issue with Actor 201 using Your Character collections in cutscenes.") + .RegisterEntry("Fixed issues with and improved mod option editing.") + .RegisterEntry( + "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use).") + .RegisterEntry("Backend optimizations.") + .RegisterEntry("Changed metadata change system again.", 1) + .RegisterEntry("Improved logging efficiency.", 1); + + private static void Add5_7_1(Changelog log) + => log.NextVersion("Version 0.5.7.1") + .RegisterEntry("Fixed the Changelog window not considering UI Scale correctly.") + .RegisterEntry("Reworked Changelog display slightly."); + + private static void Add5_7_0(Changelog log) + => log.NextVersion("Version 0.5.7.0") + .RegisterEntry("Added a Changelog!") + .RegisterEntry("Files in the UI category will no longer be deduplicated for the moment.") + .RegisterHighlight("If you experience UI-related crashes, please re-import your UI mods.", 1) + .RegisterEntry("This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1) + .RegisterHighlight( + "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", + 1) + .RegisterEntry( + "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files.") + .RegisterEntry( + "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", + 1) + .RegisterHighlight( + "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", + 1) + .RegisterEntry("Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1) + .RegisterEntry( + "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", + 1) + .RegisterEntry("Fixed assigned collections not working correctly on adventurer plates.") + .RegisterEntry("Fixed a wrongly displayed folder line in some circumstances.") + .RegisterEntry("Fixed crash after deleting mod options.") + .RegisterEntry("Fixed Inspect Window collections not working correctly.") + .RegisterEntry("Made identically named options selectable in mod configuration. Do not name your options identically.") + .RegisterEntry("Added some additional functionality for Mare Synchronos."); + + #endregion + + private (int, ChangeLogDisplayType) ConfigData() + => (_config.LastSeenVersion, _config.ChangeLogDisplayType); + + private void Save(int version, ChangeLogDisplayType type) + { + _config.LastSeenVersion = version; + _config.ChangeLogDisplayType = type; + _config.Save(); + } +} diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index c79f46d9..9abeb3f5 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -290,7 +290,7 @@ public class ItemSwapWindow : IDisposable } catch (Exception e) { - ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); + Penumbra.ChatService.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); try { if (optionCreated && _selectedGroup != null) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 5bb82fb3..24887288 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -90,7 +90,7 @@ public partial class ModEditWindow LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - ChatUtil.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } Update(); diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 784c9e24..cef13a4c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -80,12 +80,12 @@ public partial class ModEditWindow } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.ChatService.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", + Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); } ); } @@ -110,7 +110,7 @@ public partial class ModEditWindow } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } @@ -122,7 +122,7 @@ public partial class ModEditWindow catch( Exception e ) { tab.Shpk.SetInvalid(); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.ChatService.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index b1eea353..7c4946a0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Numerics; using System.Text; @@ -30,31 +29,29 @@ public partial class ModEditWindow : Window, IDisposable private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate = false; - public void ChangeMod( Mod mod ) + public void ChangeMod(Mod mod) { - if( mod == _mod ) - { + if (mod == _mod) return; - } _editor?.Dispose(); - _editor = new Editor( mod, mod.Default ); + _editor = new Editor(mod, mod.Default); _mod = mod; SizeConstraints = new WindowSizeConstraints { - MinimumSize = new Vector2( 1240, 600 ), + MinimumSize = new Vector2(1240, 600), MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear(); _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings ); + _swapWindow.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); } - public void ChangeOption( ISubMod? subMod ) - => _editor?.SetSubMod( subMod ); + public void ChangeOption(ISubMod? subMod) + => _editor?.SetSubMod(subMod); public void UpdateModels() => _editor?.ScanModels(); @@ -64,71 +61,53 @@ public partial class ModEditWindow : Window, IDisposable public override void PreDraw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); - var sb = new StringBuilder( 256 ); + var sb = new StringBuilder(256); var redirections = 0; var unused = 0; - var size = _editor!.AvailableFiles.Sum( f => + var size = _editor!.AvailableFiles.Sum(f => { - if( f.SubModUsage.Count > 0 ) - { + if (f.SubModUsage.Count > 0) redirections += f.SubModUsage.Count; - } else - { ++unused; - } return f.FileSize; - } ); + }); var manipulations = 0; var subMods = 0; - var swaps = _mod!.AllSubMods.Sum( m => + var swaps = _mod!.AllSubMods.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; return m.FileSwaps.Count; - } ); - sb.Append( _mod!.Name ); - if( subMods > 1 ) - { - sb.Append( $" | {subMods} Options" ); - } + }); + sb.Append(_mod!.Name); + if (subMods > 1) + sb.Append($" | {subMods} Options"); - if( size > 0 ) - { - sb.Append( $" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize( size )})" ); - } + if (size > 0) + sb.Append($" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize(size)})"); - if( unused > 0 ) - { - sb.Append( $" | {unused} Unused Files" ); - } + if (unused > 0) + sb.Append($" | {unused} Unused Files"); - if( _editor.MissingFiles.Count > 0 ) - { - sb.Append( $" | {_editor.MissingFiles.Count} Missing Files" ); - } + if (_editor.MissingFiles.Count > 0) + sb.Append($" | {_editor.MissingFiles.Count} Missing Files"); - if( redirections > 0 ) - { - sb.Append( $" | {redirections} Redirections" ); - } + if (redirections > 0) + sb.Append($" | {redirections} Redirections"); - if( manipulations > 0 ) - { - sb.Append( $" | {manipulations} Manipulations" ); - } + if (manipulations > 0) + sb.Append($" | {manipulations} Manipulations"); - if( swaps > 0 ) - { - sb.Append( $" | {swaps} Swaps" ); - } + if (swaps > 0) + sb.Append($" | {swaps} Swaps"); _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; - sb.Append( WindowBaseLabel ); + sb.Append(WindowBaseLabel); WindowName = sb.ToString(); } @@ -140,15 +119,13 @@ public partial class ModEditWindow : Window, IDisposable public override void Draw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); - using var tabBar = ImRaii.TabBar( "##tabs" ); - if( !tabBar ) - { + using var tabBar = ImRaii.TabBar("##tabs"); + if (!tabBar) return; - } - _iconSize = new Vector2( ImGui.GetFrameHeight() ); + _iconSize = new Vector2(ImGui.GetFrameHeight()); DrawFileTab(); DrawMetaTab(); DrawSwapTab(); @@ -169,46 +146,40 @@ public partial class ModEditWindow : Window, IDisposable private static string _materialSuffixTo = string.Empty; private static GenderRace _raceCode = GenderRace.Unknown; - private static string RaceCodeName( GenderRace raceCode ) + private static string RaceCodeName(GenderRace raceCode) { - if( raceCode == GenderRace.Unknown ) - { + if (raceCode == GenderRace.Unknown) return "All Races and Genders"; - } var (gender, race) = raceCode.Split(); return $"({raceCode.ToRaceCode()}) {race.ToName()} {gender.ToName()} "; } - private static void DrawRaceCodeCombo( Vector2 buttonSize ) + private static void DrawRaceCodeCombo(Vector2 buttonSize) { - ImGui.SetNextItemWidth( buttonSize.X ); - using var combo = ImRaii.Combo( "##RaceCode", RaceCodeName( _raceCode ) ); - if( !combo ) - { + ImGui.SetNextItemWidth(buttonSize.X); + using var combo = ImRaii.Combo("##RaceCode", RaceCodeName(_raceCode)); + if (!combo) return; - } - foreach( var raceCode in Enum.GetValues< GenderRace >() ) + foreach (var raceCode in Enum.GetValues()) { - if( ImGui.Selectable( RaceCodeName( raceCode ), _raceCode == raceCode ) ) - { + if (ImGui.Selectable(RaceCodeName(raceCode), _raceCode == raceCode)) _raceCode = raceCode; - } } } - public static void Draw( Editor editor, Vector2 buttonSize ) + public static void Draw(Editor editor, Vector2 buttonSize) { - DrawRaceCodeCombo( buttonSize ); + DrawRaceCodeCombo(buttonSize); ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SetNextItemWidth(buttonSize.X); + ImGui.InputTextWithHint("##suffixFrom", "From...", ref _materialSuffixFrom, 32); ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SetNextItemWidth(buttonSize.X); + ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32); ImGui.SameLine(); - var disabled = !Editor.ValidString( _materialSuffixTo ); + var disabled = !Editor.ValidString(_materialSuffixTo); var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." : _materialSuffixFrom == _materialSuffixTo @@ -222,181 +193,149 @@ public partial class ModEditWindow : Window, IDisposable : _raceCode == GenderRace.Unknown ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; - if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) - { - editor.ReplaceAllMaterials( _materialSuffixTo, _materialSuffixFrom, _raceCode ); - } + if (ImGuiUtil.DrawDisabledButton("Change Material Suffix", buttonSize, tt, disabled)) + editor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode); - var anyChanges = editor.ModelFiles.Any( m => m.Changed ); - if( ImGuiUtil.DrawDisabledButton( "Save All Changes", buttonSize, - anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges ) ) - { + var anyChanges = editor.ModelFiles.Any(m => m.Changed); + if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize, + anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges)) editor.SaveAllModels(); - } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Revert All Changes", buttonSize, - anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges ) ) - { + if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize, + anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges)) editor.RestoreAllModels(); - } ImGui.SameLine(); ImGuiComponents.HelpMarker( "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" - + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones."); } } private void DrawMissingFilesTab() { - if( _editor!.MissingFiles.Count == 0 ) - { + if (_editor!.MissingFiles.Count == 0) return; - } - using var tab = ImRaii.TabItem( "Missing Files" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Missing Files"); + if (!tab) return; - } ImGui.NewLine(); - if( ImGui.Button( "Remove Missing Files from Mod" ) ) - { + if (ImGui.Button("Remove Missing Files from Mod")) _editor.RemoveMissingPaths(); - } - using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One); + if (!table) return; - } - foreach( var path in _editor.MissingFiles ) + foreach (var path in _editor.MissingFiles) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( path.FullName ); + ImGui.TextUnformatted(path.FullName); } } private void DrawDuplicatesTab() { - using var tab = ImRaii.TabItem( "Duplicates" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Duplicates"); + if (!tab) return; - } var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; - if( ImGuiUtil.DrawDisabledButton( buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", - !_editor.DuplicatesFinished ) ) - { + if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", + !_editor.DuplicatesFinished)) _editor.StartDuplicateCheck(); - } - const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + const string desc = + "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + "This will also delete all unused files and directories if it succeeds.\n" + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; var modifier = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = _allowReduplicate ? desc : modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; + var tt = _allowReduplicate ? desc : + modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; - if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier ) ) + if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - _mod!.Normalize( Penumbra.ModManager ); + _mod!.Normalize(Penumbra.ModManager); _editor.RevertFiles(); } - if( !_editor.DuplicatesFinished ) + if (!_editor.DuplicatesFinished) { ImGui.SameLine(); - if( ImGui.Button( "Cancel" ) ) - { + if (ImGui.Button("Cancel")) _editor.Cancel(); - } return; } - if( _editor.Duplicates.Count == 0 ) + if (_editor.Duplicates.Count == 0) { ImGui.NewLine(); - ImGui.TextUnformatted( "No duplicates found." ); + ImGui.TextUnformatted("No duplicates found."); return; } - if( ImGui.Button( "Delete and Redirect Duplicates" ) ) - { + if (ImGui.Button("Delete and Redirect Duplicates")) _editor.DeleteDuplicates(); - } - if( _editor.SavedSpace > 0 ) + if (_editor.SavedSpace > 0) { ImGui.SameLine(); - ImGui.TextUnformatted( $"Frees up {Functions.HumanReadableSize( _editor.SavedSpace )} from your hard drive." ); + ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.SavedSpace)} from your hard drive."); } - using var child = ImRaii.Child( "##duptable", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##duptable", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) return; - } - var width = ImGui.CalcTextSize( "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN " ).X; - ImGui.TableSetupColumn( "file", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "NNN.NNN " ).X ); - ImGui.TableSetupColumn( "hash", ImGuiTableColumnFlags.WidthFixed, - ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize( "NNNNNNNN... " ).X ); - foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) ) + var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X; + ImGui.TableSetupColumn("file", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN ").X); + ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed, + ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); + foreach (var (set, size, hash) in _editor.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); - using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], - ImGuiTreeNodeFlags.NoTreePushOnOpen ); + using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], + ImGuiTreeNodeFlags.NoTreePushOnOpen); ImGui.TableNextColumn(); - ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); + ImGuiUtil.RightAlign(Functions.HumanReadableSize(size)); ImGui.TableNextColumn(); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - if( ImGui.GetWindowWidth() > 2 * width ) - { - ImGuiUtil.RightAlign( string.Concat( hash.Select( b => b.ToString( "X2" ) ) ) ); - } + if (ImGui.GetWindowWidth() > 2 * width) + ImGuiUtil.RightAlign(string.Concat(hash.Select(b => b.ToString("X2")))); else - { - ImGuiUtil.RightAlign( string.Concat( hash.Take( 4 ).Select( b => b.ToString( "X2" ) ) ) + "..." ); - } + ImGuiUtil.RightAlign(string.Concat(hash.Take(4).Select(b => b.ToString("X2"))) + "..."); } - if( !tree ) - { + if (!tree) continue; - } using var indent = ImRaii.PushIndent(); - foreach( var duplicate in set.Skip( 1 ) ) + foreach (var duplicate in set.Skip(1)) { ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); - using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); + using var node = ImRaii.TreeNode(duplicate.FullName[(_mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); } } } @@ -404,34 +343,26 @@ public partial class ModEditWindow : Window, IDisposable private void DrawOptionSelectHeader() { const string defaultOption = "Default Option"; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); - var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); - if( ImGuiUtil.DrawDisabledButton( defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor!.CurrentOption.IsDefault ) ) - { - _editor.SetSubMod( _mod!.Default ); - } + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); + var width = new Vector2(ImGui.GetWindowWidth() / 3, 0); + if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", + _editor!.CurrentOption.IsDefault)) + _editor.SetSubMod(_mod!.Default); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false ) ) - { - _editor.SetSubMod( _editor.CurrentOption ); - } + if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + _editor.SetSubMod(_editor.CurrentOption); ImGui.SameLine(); - using var combo = ImRaii.Combo( "##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton ); - if( !combo ) - { + using var combo = ImRaii.Combo("##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton); + if (!combo) return; - } - foreach( var option in _mod!.AllSubMods ) + foreach (var option in _mod!.AllSubMods) { - if( ImGui.Selectable( option.FullName, option == _editor.CurrentOption ) ) - { - _editor.SetSubMod( option ); - } + if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption)) + _editor.SetSubMod(option); } } @@ -440,100 +371,84 @@ public partial class ModEditWindow : Window, IDisposable private void DrawSwapTab() { - using var tab = ImRaii.TabItem( "File Swaps" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("File Swaps"); + if (!tab) return; - } DrawOptionSelectHeader(); - var setsEqual = _editor!.CurrentSwaps.SetEquals( _editor.CurrentOption.FileSwaps ); + var setsEqual = _editor!.CurrentSwaps.SetEquals(_editor.CurrentOption.FileSwaps); var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) - { + if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) _editor.ApplySwaps(); - } ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) - { + if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) _editor.RevertSwaps(); - } - using var child = ImRaii.Child( "##swaps", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##swaps", -Vector2.One, true); + if (!child) return; - } - using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !list ) - { + using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg, -Vector2.One); + if (!list) return; - } var idx = 0; var iconSize = ImGui.GetFrameHeight() * Vector2.One; var pathSize = ImGui.GetContentRegionAvail().X / 2 - iconSize.X; - ImGui.TableSetupColumn( "button", ImGuiTableColumnFlags.WidthFixed, iconSize.X ); - ImGui.TableSetupColumn( "source", ImGuiTableColumnFlags.WidthFixed, pathSize ); - ImGui.TableSetupColumn( "value", ImGuiTableColumnFlags.WidthFixed, pathSize ); + ImGui.TableSetupColumn("button", ImGuiTableColumnFlags.WidthFixed, iconSize.X); + ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize); + ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthFixed, pathSize); - foreach( var (gamePath, file) in _editor!.CurrentSwaps.ToList() ) + foreach (var (gamePath, file) in _editor!.CurrentSwaps.ToList()) { - using var id = ImRaii.PushId( idx++ ); + using var id = ImRaii.PushId(idx++); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true ) ) - { - _editor.CurrentSwaps.Remove( gamePath ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true)) + _editor.CurrentSwaps.Remove(gamePath); ImGui.TableNextColumn(); var tmp = gamePath.Path.ToString(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( "##key", ref tmp, Utf8GamePath.MaxGamePathLength ) - && Utf8GamePath.FromString( tmp, out var path ) - && !_editor.CurrentSwaps.ContainsKey( path ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength) + && Utf8GamePath.FromString(tmp, out var path) + && !_editor.CurrentSwaps.ContainsKey(path)) { - _editor.CurrentSwaps.Remove( gamePath ); - if( path.Length > 0 ) - { - _editor.CurrentSwaps[ path ] = file; - } + _editor.CurrentSwaps.Remove(gamePath); + if (path.Length > 0) + _editor.CurrentSwaps[path] = file; } ImGui.TableNextColumn(); tmp = file.FullName; - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( "##value", ref tmp, Utf8GamePath.MaxGamePathLength ) && tmp.Length > 0 ) - { - _editor.CurrentSwaps[ gamePath ] = new FullPath( tmp ); - } + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) + _editor.CurrentSwaps[gamePath] = new FullPath(tmp); } ImGui.TableNextColumn(); - var addable = Utf8GamePath.FromString( _newSwapKey, out var newPath ) - && newPath.Length > 0 + var addable = Utf8GamePath.FromString(_newSwapKey, out var newPath) + && newPath.Length > 0 && _newSwapValue.Length > 0 - && _newSwapValue != _newSwapKey - && !_editor.CurrentSwaps.ContainsKey( newPath ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, - true ) ) + && _newSwapValue != _newSwapKey + && !_editor.CurrentSwaps.ContainsKey(newPath); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, + true)) { - _editor.CurrentSwaps[ newPath ] = new FullPath( _newSwapValue ); - _newSwapKey = string.Empty; - _newSwapValue = string.Empty; + _editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; } ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength); } /// @@ -544,50 +459,44 @@ public partial class ModEditWindow : Window, IDisposable /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. /// If no redirection is found in either of those options, returns the original path. /// - private FullPath FindBestMatch( Utf8GamePath path ) + private FullPath FindBestMatch(Utf8GamePath path) { - var currentFile = Penumbra.CollectionManager.Current.ResolvePath( path ); - if( currentFile != null ) - { + var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path); + if (currentFile != null) return currentFile.Value; - } - if( _mod != null ) - { - foreach( var option in _mod.Groups.OrderByDescending( g => g.Priority ) - .SelectMany( g => g.WithIndex().OrderByDescending( o => g.OptionPriority( o.Index ) ).Select( g => g.Value ) ) - .Append( _mod.Default ) ) + if (_mod != null) + foreach (var option in _mod.Groups.OrderByDescending(g => g.Priority) + .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) + .Append(_mod.Default)) { - if( option.Files.TryGetValue( path, out var value ) || option.FileSwaps.TryGetValue( path, out value ) ) - { + if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) return value; - } } - } - return new FullPath( path ); + return new FullPath(path); } public ModEditWindow(CommunicatorService communicator) - : base( WindowBaseLabel ) - { - _swapWindow = new ItemSwapWindow( communicator ); - _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", - () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), + : base(WindowBaseLabel) + { + _swapWindow = new ItemSwapWindow(communicator); + _materialTab = new FileEditor("Materials", ".mtrl", + () => _editor?.MtrlFiles ?? Array.Empty(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlTab( this, new MtrlFile( bytes ) ) ); - _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", - () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), + bytes => new MtrlTab(this, new MtrlFile(bytes))); + _modelTab = new FileEditor("Models", ".mdl", + () => _editor?.MdlFiles ?? Array.Empty(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, - null ); - _shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk", - () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), + null); + _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", + () => _editor?.ShpkFiles ?? Array.Empty(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - null ); - _center = new CombinedTexture( _left, _right ); + null); + _center = new CombinedTexture(_left, _right); } public void Dispose() @@ -598,4 +507,4 @@ public partial class ModEditWindow : Window, IDisposable _center.Dispose(); _swapWindow.Dispose(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs deleted file mode 100644 index bbe0e9ad..00000000 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System.Runtime.CompilerServices; -using OtterGui.Widgets; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - public const int LastChangelogVersion = 0; - - public static Changelog CreateChangelog() - { - var ret = new Changelog( "Penumbra Changelog", () => ( Penumbra.Config.LastSeenVersion, Penumbra.Config.ChangeLogDisplayType ), - ( version, type ) => - { - Penumbra.Config.LastSeenVersion = version; - Penumbra.Config.ChangeLogDisplayType = type; - Penumbra.Config.Save(); - } ); - - Add5_7_0( ret ); - Add5_7_1( ret ); - Add5_8_0( ret ); - Add5_8_7( ret ); - Add5_9_0( ret ); - Add5_10_0( ret ); - Add5_11_0( ret ); - Add5_11_1( ret ); - Add6_0_0( ret ); - Add6_0_2( ret ); - Add6_0_5( ret ); - Add6_1_0( ret ); - Add6_1_1( ret ); - Add6_2_0( ret ); - Add6_3_0( ret ); - Add6_4_0( ret ); - Add6_5_0( ret ); - Add6_5_2( ret ); - Add6_6_0( ret ); - Add6_6_1( ret ); - return ret; - } - - private static void Add6_6_1( Changelog log ) - => log.NextVersion( "Version 0.6.6.1" ) - .RegisterEntry( "Added an option to make successful chat commands not print their success confirmations to chat." ) - .RegisterEntry( "Fixed an issue with migration of old mods not working anymore (fixes Material UI problems)." ) - .RegisterEntry( "Fixed some issues with using the Assign Current Player and Assign Current Target buttons." ); - - private static void Add6_6_0( Changelog log ) - => log.NextVersion( "Version 0.6.6.0" ) - .RegisterEntry( "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively." ) - .RegisterEntry( "Added an option to display Single Selection Groups as a group of radio buttons similar to Multi Selection Groups, when the number of available options is below the specified value. Default value is 2." ) - .RegisterEntry( "Added a button in option groups to collapse the option list if it has more than 5 available options." ) - .RegisterEntry( - "Penumbra now circumvents the games inability to read files at paths longer than 260 UTF16 characters and can also deal with generic unicode symbols in paths." ) - .RegisterEntry( "This means that Penumbra should no longer cause issues when files become too long or when there is a non-ASCII character in them.", 1 ) - .RegisterEntry( - "Shorter paths are still better, so restrictions on the root directory have not been relaxed. Mod names should no longer replace non-ASCII symbols on import though.", 1 ) - .RegisterEntry( - "Resource logging has been relegated to its own tab with better filtering. Please do not keep resource logging on arbitrarily or set a low record limit if you do, otherwise this eats a lot of performance and memory after a while." ) - .RegisterEntry( "Added a lot of facilities to edit the shader part of .mtrl files and .shpk files themselves in the Advanced Editing Tab (Thanks Ny and aers)." ) - .RegisterEntry( "Added splitting of Multi Selection Groups with too many options when importing .pmp files or adding mods via IPC." ) - .RegisterEntry( "Discovery, Reloading and Unloading of a specified mod is now possible via HTTP API (Thanks Sebastina)." ) - .RegisterEntry( "Cleaned up the HTTP API somewhat, removed currently useless options." ) - .RegisterEntry( "Fixed an issue when extracting some textures." ) - .RegisterEntry( "Fixed an issue with mannequins inheriting individual assignments for the current player when using ownership." ) - .RegisterEntry( "Fixed an issue with the resolving of .phyb and .sklb files for Item Swaps of head or body items with an EST entry but no unique racial model." ); - - private static void Add6_5_2( Changelog log ) - => log.NextVersion( "Version 0.6.5.2" ) - .RegisterEntry( "Updated for game version 6.31 Hotfix." ) - .RegisterEntry( "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)" ) - .RegisterEntry( "Those are now accurately parsed from TTMPs, too.", 1 ) - .RegisterEntry( "Improved launch times somewhat through parallelization of some tasks." ) - .RegisterEntry( - "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled." ) - .RegisterEntry( "Fixed an issue with IMC changes and Mare Synchronos interoperability." ) - .RegisterEntry( "Fixed an issue with housing mannequins crashing the game when resource logging was enabled." ) - .RegisterEntry( "Fixed an issue generating Mip Maps for texture import on Wine." ); - - private static void Add6_5_0( Changelog log ) - => log.NextVersion( "Version 0.6.5.0" ) - .RegisterEntry( "Fixed an issue with Item Swaps not using applied IMC changes in some cases." ) - .RegisterEntry( "Improved error message on texture import when failing to create mip maps (slightly)." ) - .RegisterEntry( "Tried to fix duty party banner identification again, also for the recommendation window this time." ) - .RegisterEntry( "Added batched IPC to improve Mare performance." ); - - private static void Add6_4_0( Changelog log ) - => log.NextVersion( "Version 0.6.4.0" ) - .RegisterEntry( "Fixed an issue with the identification of actors in the duty group portrait." ) - .RegisterEntry( "Fixed some issues with wrongly cached actors and resources." ) - .RegisterEntry( "Fixed animation handling after redraws (notably for PLD idle animations with a shield equipped)." ) - .RegisterEntry( "Fixed an issue with collection listing API skipping one collection." ) - .RegisterEntry( "Fixed an issue with BGM files being sometimes loaded from other collections than the base collection, causing crashes." ) - .RegisterEntry( "Also distinguished file resolving for different file categories (improving performance) and disabled resolving for script files entirely.", 1 ) - .RegisterEntry( "Some miscellaneous backend changes due to the Glamourer rework." ); - - private static void Add6_3_0( Changelog log ) - => log.NextVersion( "Version 0.6.3.0" ) - .RegisterEntry( "Add an Assign Current Target button for individual assignments" ) - .RegisterEntry( "Try identifying all banner actors correctly for PvE duties, Crystalline Conflict and Mahjong." ) - .RegisterEntry( "Please let me know if this does not work for anything except identical twins.", 1 ) - .RegisterEntry( "Add handling for the 3 new screen actors (now 8 total, for PvE dutie portraits)." ) - .RegisterEntry( "Update the Battle NPC name database for 6.3." ) - .RegisterEntry( "Added API/IPC functions to obtain or set group or individual collections." ) - .RegisterEntry( "Maybe fix a problem with textures sometimes not loading from their corresponding collection." ) - .RegisterEntry( "Another try to fix a problem with the collection selectors breaking state." ) - .RegisterEntry( "Fix a problem identifying companions." ) - .RegisterEntry( "Fix a problem when deleting collections assigned to Groups." ) - .RegisterEntry( "Fix a problem when using the Assign Currently Played Character button and then logging onto a different character without restarting in between." ) - .RegisterEntry( "Some miscellaneous backend changes." ); - - private static void Add6_2_0( Changelog log ) - => log.NextVersion( "Version 0.6.2.0" ) - .RegisterEntry( "Update Penumbra for .net7, Dalamud API 8 and patch 6.3." ) - .RegisterEntry( "Add a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)" ) - .RegisterEntry( "Add placeholder options for setting individual collections via chat command." ) - .RegisterEntry( "Add toggles to swap left and/or right rings separately for ring item swap." ) - .RegisterEntry( "Add handling for looping sound effects caused by animations in non-base collections." ) - .RegisterEntry( "Add an option to not use any mods at all in the Inspect/Try-On window." ) - .RegisterEntry( "Add handling for Mahjong actors." ) - .RegisterEntry( "Improve hint text for File Swaps in Advanced Editing, also inverted file swap display order." ) - .RegisterEntry( "Fix a problem where the collection selectors could get desynchronized after adding or deleting collections." ) - .RegisterEntry( "Fix a problem that could cause setting state to get desynchronized." ) - .RegisterEntry( "Fix an oversight where some special screen actors did not actually respect the settings made for them." ) - .RegisterEntry( "Add collection and associated game object to Full Resource Logging." ) - .RegisterEntry( "Add performance tracking for DEBUG-compiled versions (i.e. testing only)." ) - .RegisterEntry( "Add some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)" ) - .RegisterEntry( "Fix association of some vfx game objects. (0.6.1.3)" ) - .RegisterEntry( "Stop forcing AVFX files to load synchronously. (0.6.1.3)" ) - .RegisterEntry( "Fix an issue when incorporating deduplicated meta files. (0.6.1.2)" ); - - private static void Add6_1_1( Changelog log ) - => log.NextVersion( "Version 0.6.1.1" ) - .RegisterEntry( "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod." ) - .RegisterEntry( "Fix using equipment paths for accessory swaps and thus accessory swaps not working at all" ) - .RegisterEntry( "Fix issues with swaps with gender-locked gear where the models for the other gender do not exist." ) - .RegisterEntry( "Fix swapping universal hairstyles for midlanders breaking them for other races." ) - .RegisterEntry( "Add some actual error messages on failure to create item swaps." ) - .RegisterEntry( "Fix warnings about more than one affected item appearing for single items." ); - - private static void Add6_1_0( Changelog log ) - => log.NextVersion( "Version 0.6.1.0 (Happy New Year! Edition)" ) - .RegisterEntry( "Add a prototype for Item Swapping." ) - .RegisterEntry( "A new tab in Advanced Editing.", 1 ) - .RegisterEntry( "Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1 ) - .RegisterEntry( "The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" - + " should be used when creating the swap, but you can also just swap unmodded things.", 1 ) - .RegisterEntry( "You can write a swap to a new mod, or to a new option in the currently selected mod.", 1 ) - .RegisterEntry( "The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1 ) - .RegisterEntry( "More detailed help or explanations will be added later.", 1 ) - .RegisterEntry( "Heavily improve Chat Commands. Use /penumbra help for more information." ) - .RegisterEntry( "Penumbra now considers meta manipulations for Changed Items." ) - .RegisterEntry( "Penumbra now tries to associate battle voices to specific actors, so that they work in collections." ) - .RegisterEntry( "Heavily improve .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects." ) - .RegisterEntry( "Improve some file handling for Mare-Interaction." ) - .RegisterEntry( "Add Equipment Slots to Demihuman IMC Edits." ) - .RegisterEntry( "Add a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files." ) - .RegisterEntry( "Add an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra." ) - .RegisterEntry( "Add API to copy mod settings from one mod to another." ) - .RegisterEntry( "Fix a problem where creating individual collections did not trigger events." ) - .RegisterEntry( "Add a Hack to support Anamnesis Redrawing better. (0.6.0.6)" ) - .RegisterEntry( "Fix another problem with the aesthetician. (0.6.0.6)" ) - .RegisterEntry( "Fix a problem with the export directory not being respected. (0.6.0.6)" ); - - private static void Add6_0_5( Changelog log ) - => log.NextVersion( "Version 0.6.0.5" ) - .RegisterEntry( "Allow hyphen as last character in player and retainer names." ) - .RegisterEntry( "Fix various bugs with ownership and GPose." ) - .RegisterEntry( "Fix collection selectors not updating for new or deleted collections in some cases." ) - .RegisterEntry( "Fix Chocobos not being recognized correctly." ) - .RegisterEntry( "Fix some problems with UI actors." ) - .RegisterEntry( "Fix problems with aesthetician again." ); - - private static void Add6_0_2( Changelog log ) - => log.NextVersion( "Version 0.6.0.2" ) - .RegisterEntry( "Let Bell Retainer collections apply to retainer-named mannequins." ) - .RegisterEntry( "Added a few informations to a help marker for new individual assignments." ) - .RegisterEntry( "Fix bug with Demi Human IMC paths." ) - .RegisterEntry( "Fix Yourself collection not applying to UI actors." ) - .RegisterEntry( "Fix Yourself collection not applying during aesthetician." ); - - private static void Add6_0_0( Changelog log ) - => log.NextVersion( "Version 0.6.0.0" ) - .RegisterEntry( "Revamped Individual Collections:" ) - .RegisterEntry( "You can now specify individual collections for players (by name) of specific worlds or any world.", 1 ) - .RegisterEntry( "You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", 1 ) - .RegisterHighlight( - "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", - 1 ) - .RegisterHighlight( - "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", - 1 ) - .RegisterEntry( "You can also manually sort your Individual Collections by drag and drop now.", 1 ) - .RegisterEntry( "This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1 ) - .RegisterEntry( "These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", 1 ) - .RegisterEntry( "General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", 1 ) - .RegisterEntry( "Added NPC Model Parsing, changes in NPC models should now display the names of the changed game objects for most NPCs." ) - .RegisterEntry( "Changed Items now also display variant or subtype in addition to the model set ID where applicable." ) - .RegisterEntry( "Collection selectors can now be filtered by name." ) - .RegisterEntry( "Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths." ) - .RegisterEntry( "Improved interface for group settings (minimally)." ) - .RegisterEntry( "New Special or Individual Assignments now default to your current Base assignment instead of None." ) - .RegisterEntry( "Improved Support Info somewhat." ) - .RegisterEntry( "Added Dye Previews for in-game dyes and dyeing templates in Material Editing." ) - .RegisterEntry( "Colorset Editing now allows for negative values in all cases." ) - .RegisterEntry( "Added Export buttons to .mdl and .mtrl previews in Advanced Editing." ) - .RegisterEntry( "File Selection in the .mdl and .mtrl tabs now shows one associated game path by default and all on hover." ) - .RegisterEntry( - "Added the option to reduplicate and normalize a mod, restoring all duplicates and moving the files to appropriate folders. (Duplicates Tab in Advanced Editing)" ) - .RegisterEntry( "Added an option to re-export metadata changes to TexTools-typed .meta and .rgsp files. (Meta-Manipulations Tab in Advanced Editing)" ) - .RegisterEntry( "Fixed several bugs with the incorporation of meta changes when not done during TTMP import." ) - .RegisterEntry( "Fixed a bug with RSP changes on non-base collections not applying correctly in some cases." ) - .RegisterEntry( "Fixed a bug when dragging options during mod edit." ) - .RegisterEntry( "Fixed a bug where sometimes the valid folder check caused issues." ) - .RegisterEntry( "Fixed a bug where collections with inheritances were newly saved on every load." ) - .RegisterEntry( "Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged)." ) - .RegisterEntry( "Mods without names or invalid mod folders are now warnings instead of errors." ) - .RegisterEntry( "Added IPC events for mod deletion, addition or moves, and resolving based on game objects." ) - .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) - .RegisterEntry( "A lot of big backend changes." ); - - private static void Add5_11_1( Changelog log ) - => log.NextVersion( "Version 0.5.11.1" ) - .RegisterEntry( - "The 0.5.11.0 Update exposed an issue in Penumbras file-saving scheme that rarely could cause some, most or even all of your mods to lose their group information." ) - .RegisterEntry( "If this has happened to you, you will need to reimport affected mods, or manually restore their groups. I am very sorry for that.", 1 ) - .RegisterEntry( - "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", - 1 ) - .RegisterHighlight( "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", 1 ); - - private static void Add5_11_0( Changelog log ) - => log.NextVersion( "Version 0.5.11.0" ) - .RegisterEntry( - "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections." ) - .RegisterEntry( "Moved the import date from mod metadata to local data.", 1 ) - .RegisterEntry( "Added Favorites. You can declare mods as favorites and filter for them.", 1 ) - .RegisterEntry( "Added Local Tags. You can apply custom Tags to mods and filter for them.", 1 ) - .RegisterEntry( "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported." ) - .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) - .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) - .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) - .RegisterEntry( "Updated some tooltips and hints." ) - .RegisterEntry( "Improved handling of IMC exception problems." ) - .RegisterEntry( "Fixed a bug with misidentification of equipment decals." ) - .RegisterEntry( "Character collections can now be set via chat command, too. (/penumbra collection character | )" ) - .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ) - .RegisterEntry( "Added API to delete mods and read and set their pseudo-filesystem paths.", 1 ) - .RegisterEntry( "Added API to check Penumbras enabled state and updates to it.", 1 ); - - private static void Add5_10_0( Changelog log ) - => log.NextVersion( "Version 0.5.10.0" ) - .RegisterEntry( "Renamed backup functionality to export functionality." ) - .RegisterEntry( "A default export directory can now optionally be specified." ) - .RegisterEntry( "If left blank, exports will still be stored in your mod directory.", 1 ) - .RegisterEntry( "Existing exports corresponding to existing mods will be moved automatically if the export directory is changed.", - 1 ) - .RegisterEntry( "Added buttons to export and import all color set rows at once during material editing." ) - .RegisterEntry( "Fixed texture import being case sensitive on the extension." ) - .RegisterEntry( "Fixed special collection selector increasing in size on non-default UI styling." ) - .RegisterEntry( "Fixed color set rows not importing the dye values during material editing." ) - .RegisterEntry( "Other miscellaneous small fixes." ); - - private static void Add5_9_0( Changelog log ) - => log.NextVersion( "Version 0.5.9.0" ) - .RegisterEntry( "Special Collections are now split between male and female." ) - .RegisterEntry( "Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install." ) - .RegisterEntry( "Fix a bug where cutscene actors were not properly reset and could be misidentified across multiple cutscenes." ) - .RegisterEntry( "TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions." ); - - private static void Add5_8_7( Changelog log ) - => log.NextVersion( "Version 0.5.8.7" ) - .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7)." ) - .RegisterHighlight( - "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", - 1 ); - - private static void Add5_8_0( Changelog log ) - => log.NextVersion( "Version 0.5.8.0" ) - .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) - .RegisterEntry( "Added an Interface Collection assignment." ) - .RegisterEntry( "All your UI mods will have to be in the interface collection.", 1 ) - .RegisterEntry( "Files that are categorized as UI files by the game will only check for redirections in this collection.", 1 ) - .RegisterHighlight( - "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) - .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) - .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) - .RegisterHighlight( - "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being." ) - .RegisterEntry( - "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured." ) - .RegisterEntry( "Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", - 1 ) - .RegisterEntry( "Files that the game loads super early should now be replaceable correctly via base or interface collection." ) - .RegisterEntry( - "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) - .RegisterEntry( "Continued Work on the Texture Import/Export Tab:" ) - .RegisterEntry( "Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1 ) - .RegisterEntry( "Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1 ) - .RegisterEntry( "Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1 ) - .RegisterHighlight( - "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", - 1 ) - .RegisterEntry( - "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) - .RegisterEntry( "Collection Selectors now display None at the top if available." ) - .RegisterEntry( - "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically." ) - .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) - .RegisterEntry( "Fixed issues with and improved mod option editing." ) - .RegisterEntry( - "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use)." ) - .RegisterEntry( "Backend optimizations." ) - .RegisterEntry( "Changed metadata change system again.", 1 ) - .RegisterEntry( "Improved logging efficiency.", 1 ); - - private static void Add5_7_1( Changelog log ) - => log.NextVersion( "Version 0.5.7.1" ) - .RegisterEntry( "Fixed the Changelog window not considering UI Scale correctly." ) - .RegisterEntry( "Reworked Changelog display slightly." ); - - private static void Add5_7_0( Changelog log ) - => log.NextVersion( "Version 0.5.7.0" ) - .RegisterEntry( "Added a Changelog!" ) - .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) - .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) - .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) - .RegisterHighlight( - "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", - 1 ) - .RegisterEntry( - "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) - .RegisterEntry( - "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", - 1 ) - .RegisterHighlight( - "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", - 1 ) - .RegisterEntry( "Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1 ) - .RegisterEntry( - "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", - 1 ) - .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) - .RegisterEntry( "Fixed a wrongly displayed folder line in some circumstances." ) - .RegisterEntry( "Fixed crash after deleting mod options." ) - .RegisterEntry( "Fixed Inspect Window collections not working correctly." ) - .RegisterEntry( "Made identically named options selectable in mod configuration. Do not name your options identically." ) - .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 45f8f25c..ca5de44b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -239,7 +239,7 @@ public partial class ConfigWindow ImGui.SameLine(); - var nameValid = Mod.Manager.VerifyFileName( mod, null, _newGroupName, false ); + var nameValid = Penumbra.ModManager.VerifyFileName( mod, null, _newGroupName, false ); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, tt, !nameValid, true ) ) @@ -255,7 +255,7 @@ public partial class ConfigWindow { private static string? _currentModDirectory; private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; - + public static void Reset() { _currentModDirectory = null; @@ -269,7 +269,7 @@ public partial class ConfigWindow if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) { _currentModDirectory = tmp; - _state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); + _state = Penumbra.ModManager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); } var (disabled, tt) = _state switch diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index d08a50c6..5be9c8e0 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -111,11 +111,11 @@ public partial class ConfigWindow { if( lower.Length > 0 ) { - _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + _penumbra.RedrawService.RedrawObject( lower, RedrawType.Redraw ); } else { - _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + _penumbra.RedrawService.RedrawAll( RedrawType.Redraw ); } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f212feab..ec149ce2 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -7,7 +7,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop.Services; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -21,6 +21,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly ModFileSystemSelector _selector; private readonly ModPanel _modPanel; public readonly ModEditWindow ModEditPopup; + private readonly Configuration _config; private readonly SettingsTab _settingsTab; private readonly CollectionsTab _collectionsTab; @@ -35,11 +36,13 @@ public sealed partial class ConfigWindow : Window, IDisposable public void SelectMod(Mod mod) => _selector.SelectByValue(mod); - - public ConfigWindow(CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, Penumbra penumbra, ResourceWatcher watcher) + + public ConfigWindow(Configuration config, CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, + Penumbra penumbra, ResourceWatcher watcher) : base(GetLabel()) { _penumbra = penumbra; + _config = config; _resourceWatcher = watcher; ModEditPopup = new ModEditWindow(communicator); @@ -66,6 +69,7 @@ public sealed partial class ConfigWindow : Window, IDisposable MaximumSize = new Vector2(4096, 2160), }; UpdateTutorialStep(); + IsOpen = _config.DebugMode; } private ReadOnlySpan ToLabel(TabType type) diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 39f9cdd1..3aa470f5 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,49 +1,66 @@ using System; using System.IO; using Dalamud.Interface; +using Dalamud.Plugin; using ImGuiScene; -using Penumbra.Services; - + namespace Penumbra.UI; -// A Launch Button used in the title screen of the game, -// using the Dalamud-provided collapsible submenu. +/// +/// A Launch Button used in the title screen of the game, +/// using the Dalamud-provided collapsible submenu. +/// public class LaunchButton : IDisposable { - private readonly ConfigWindow _configWindow; - private TextureWrap? _icon; - private TitleScreenMenu.TitleScreenMenuEntry? _entry; + private readonly ConfigWindow _configWindow; + private readonly UiBuilder _uiBuilder; + private readonly TitleScreenMenu _title; + private readonly string _fileName; - public LaunchButton( ConfigWindow ui ) + private TextureWrap? _icon; + private TitleScreenMenu.TitleScreenMenuEntry? _entry; + + /// + /// Register the launch button to be created on the next draw event. + /// + public LaunchButton(DalamudPluginInterface pi, TitleScreenMenu title, ConfigWindow ui) { + _uiBuilder = pi.UiBuilder; _configWindow = ui; + _title = title; _icon = null; _entry = null; - void CreateEntry() - { - _icon = DalamudServices.PluginInterface.UiBuilder.LoadImage( Path.Combine( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName!, - "tsmLogo.png" ) ); - if( _icon != null ) - { - _entry = DalamudServices.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - } - - DalamudServices.PluginInterface.UiBuilder.Draw -= CreateEntry; - } - - DalamudServices.PluginInterface.UiBuilder.Draw += CreateEntry; + _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); + _uiBuilder.Draw += CreateEntry; } - private void OnTriggered() - => _configWindow.Toggle(); - public void Dispose() { _icon?.Dispose(); - if( _entry != null ) + if (_entry != null) + _title.RemoveEntry(_entry); + } + + /// + /// One-Time event to load the image and create the entry on the first drawn frame, but not before. + /// + private void CreateEntry() + { + try { - DalamudServices.TitleScreenMenu.RemoveEntry( _entry ); + _icon = _uiBuilder.LoadImage(_fileName); + if (_icon != null) + _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); + + _uiBuilder.Draw -= CreateEntry; + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not register title screen menu entry:\n{ex}"); } } -} \ No newline at end of file + + private void OnTriggered() + => _configWindow.Toggle(); +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs new file mode 100644 index 00000000..630033b9 --- /dev/null +++ b/Penumbra/UI/WindowSystem.cs @@ -0,0 +1,40 @@ +using System; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using Penumbra.UI; +using Penumbra.UI.Classes; + +namespace Penumbra; + +public class PenumbraWindowSystem : IDisposable +{ + private readonly UiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + public readonly ConfigWindow Window; + public readonly PenumbraChangelog Changelog; + + public PenumbraWindowSystem(DalamudPluginInterface pi, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, + ModEditWindow editWindow) + { + _uiBuilder = pi.UiBuilder; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); + _windowSystem.AddWindow(changelog.Changelog); + _windowSystem.AddWindow(window); + _windowSystem.AddWindow(editWindow); + + _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.Draw += _windowSystem.Draw; + } + + public void ForceChangelogOpen() + => Changelog.Changelog.ForceOpen = true; + + public void Dispose() + { + _uiBuilder.OpenConfigUi -= Window.Toggle; + _uiBuilder.Draw -= _windowSystem.Draw; + } +} diff --git a/Penumbra/Util/ChatService.cs b/Penumbra/Util/ChatService.cs new file mode 100644 index 00000000..92cf0560 --- /dev/null +++ b/Penumbra/Util/ChatService.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Log; + +namespace Penumbra.Util; + +public class ChatService +{ + private readonly Logger _log; + private readonly UiBuilder _uiBuilder; + private readonly ChatGui _chat; + + public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat) + { + _log = log; + _uiBuilder = pi.UiBuilder; + _chat = chat; + } + + public void LinkItem(Item item) + { + // @formatter:off + var payloadList = new List + { + new UIForegroundPayload((ushort)(0x223 + item.Rarity * 2)), + new UIGlowPayload((ushort)(0x224 + item.Rarity * 2)), + new ItemPayload(item.RowId, false), + new UIForegroundPayload(500), + new UIGlowPayload(501), + new TextPayload($"{(char)SeIconChar.LinkMarker}"), + new UIForegroundPayload(0), + new UIGlowPayload(0), + new TextPayload(item.Name), + new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }), + new RawPayload(new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 }), + }; + // @formatter:on + + var payload = new SeString(payloadList); + + _chat.PrintChat(new XivChatEntry + { + Message = payload, + }); + } + + public void NotificationMessage(string content, string? title = null, NotificationType type = NotificationType.None) + { + var logLevel = type switch + { + NotificationType.None => Logger.LogLevel.Information, + NotificationType.Success => Logger.LogLevel.Information, + NotificationType.Warning => Logger.LogLevel.Warning, + NotificationType.Error => Logger.LogLevel.Error, + NotificationType.Info => Logger.LogLevel.Information, + _ => Logger.LogLevel.Debug, + }; + _uiBuilder.AddNotification(content, title, type); + _log.Message(logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}"); + } +} + +public static class SeStringBuilderExtensions +{ + public const ushort Green = 504; + public const ushort Yellow = 31; + public const ushort Red = 534; + public const ushort Blue = 517; + public const ushort White = 1; + public const ushort Purple = 541; + + public static SeStringBuilder AddText(this SeStringBuilder sb, string text, int color, bool brackets = false) + => sb.AddUiForeground((ushort)color).AddText(brackets ? $"[{text}]" : text).AddUiForegroundOff(); + + public static SeStringBuilder AddGreen(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Green, brackets); + + public static SeStringBuilder AddYellow(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Yellow, brackets); + + public static SeStringBuilder AddRed(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Red, brackets); + + public static SeStringBuilder AddBlue(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Blue, brackets); + + public static SeStringBuilder AddWhite(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, White, brackets); + + public static SeStringBuilder AddPurple(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Purple, brackets); + + public static SeStringBuilder AddCommand(this SeStringBuilder sb, string command, string description) + => sb.AddText(" 》 ") + .AddBlue(command) + .AddText($" - {description}"); + + public static SeStringBuilder AddInitialPurple(this SeStringBuilder sb, string word, bool withComma = true) + => sb.AddPurple($"[{word[0]}]") + .AddText(withComma ? $"{word[1..]}, " : word[1..]); +} diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs deleted file mode 100644 index aff2e67b..00000000 --- a/Penumbra/Util/ChatUtil.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Log; -using Penumbra.Services; - -namespace Penumbra.Util; - -public static class ChatUtil -{ - public static void LinkItem( Item item ) - { - var payloadList = new List< Payload > - { - new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ), - new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ), - new ItemPayload( item.RowId, false ), - new UIForegroundPayload( 500 ), - new UIGlowPayload( 501 ), - new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), - new UIForegroundPayload( 0 ), - new UIGlowPayload( 0 ), - new TextPayload( item.Name ), - new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), - new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ), - }; - - var payload = new SeString( payloadList ); - - DalamudServices.Chat.PrintChat( new XivChatEntry - { - Message = payload, - } ); - } - - public static void NotificationMessage( string content, string? title = null, NotificationType type = NotificationType.None ) - { - var logLevel = type switch - { - NotificationType.None => Logger.LogLevel.Information, - NotificationType.Success => Logger.LogLevel.Information, - NotificationType.Warning => Logger.LogLevel.Warning, - NotificationType.Error => Logger.LogLevel.Error, - NotificationType.Info => Logger.LogLevel.Information, - _ => Logger.LogLevel.Debug, - }; - DalamudServices.PluginInterface.UiBuilder.AddNotification( content, title, type ); - Penumbra.Log.Message( logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}" ); - } -} \ No newline at end of file diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Util/SaveService.cs new file mode 100644 index 00000000..71c8b6b4 --- /dev/null +++ b/Penumbra/Util/SaveService.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using OtterGui.Classes; +using OtterGui.Log; +using Penumbra.Api; +using Penumbra.Services; + +namespace Penumbra.Util; + +/// +/// Any file type that we want to save via SaveService. +/// +public interface ISaveable +{ + /// The full file name of a given object. + public string ToFilename(FilenameService fileNames); + + /// Write the objects data to the given stream writer. + public void Save(StreamWriter writer); + + /// An arbitrary message printed to Debug before saving. + public string LogName(string fileName) + => fileName; + + public string TypeName + => GetType().Name; +} + +public class SaveService +{ + private readonly Logger _log; + private readonly FilenameService _fileNames; + private readonly FrameworkManager _framework; + + public SaveService(Logger log, FilenameService fileNames, FrameworkManager framework) + { + _log = log; + _fileNames = fileNames; + _framework = framework; + } + + /// Queue a save for the next framework tick. + public void QueueSave(ISaveable value) + { + var file = value.ToFilename(_fileNames); + _framework.RegisterDelayed(value.GetType().Name + file, () => + { + ImmediateSave(value); + }); + } + + /// Immediately trigger a save. + public void ImmediateSave(ISaveable value) + { + var name = value.ToFilename(_fileNames); + try + { + if (name.Length == 0) + { + throw new Exception("Invalid object returned empty filename."); + } + + _log.Debug($"Saving {value.TypeName} {value.LogName(name)}..."); + var file = new FileInfo(name); + file.Directory?.Create(); + using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew); + using var w = new StreamWriter(s, Encoding.UTF8); + value.Save(w); + } + catch (Exception ex) + { + _log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}"); + } + } + + public void ImmediateDelete(ISaveable value) + { + var name = value.ToFilename(_fileNames); + try + { + if (name.Length == 0) + { + throw new Exception("Invalid object returned empty filename."); + } + + if (!File.Exists(name)) + return; + + _log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}..."); + File.Delete(name); + } + catch (Exception ex) + { + _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); + } + } +} From 651c7410acda6b39cbae76f565fe6c407f59c808 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Mar 2023 21:39:59 +0100 Subject: [PATCH 0809/2451] Wow, I accidentally the whole UI --- OtterGui | 2 +- Penumbra/Api/IpcTester.cs | 29 +- Penumbra/Api/PenumbraApi.cs | 291 ++++--- .../Collections/CollectionManager.Active.cs | 6 +- Penumbra/Collections/CollectionManager.cs | 112 ++- Penumbra/Collections/CollectionType.cs | 87 ++ Penumbra/Configuration.cs | 118 +-- Penumbra/Import/TexToolsImporter.Gui.cs | 4 +- Penumbra/Import/Textures/Texture.cs | 17 +- Penumbra/Interop/RedrawService.cs | 4 +- .../Resolver/IdentifiedCollectionCache.cs | 4 +- .../Resolver/PathResolver.AnimationState.cs | 16 +- .../Resolver/PathResolver.DrawObjectState.cs | 6 +- .../Resolver/PathResolver.Identification.cs | 4 +- Penumbra/Interop/Resolver/PathResolver.cs | 4 +- Penumbra/Meta/Files/ImcFile.cs | 6 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 2 +- Penumbra/Mods/ItemSwap/Swaps.cs | 2 +- Penumbra/PenumbraNew.cs | 54 +- Penumbra/Services/DalamudServices.cs | 78 +- Penumbra/Services/ServiceWrapper.cs | 9 +- Penumbra/UI/Classes/Colors.cs | 11 +- Penumbra/UI/Classes/Combos.cs | 18 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 14 +- .../UI/Classes/ModEditWindow.FileEditor.cs | 188 ++--- Penumbra/UI/Classes/ModEditWindow.Files.cs | 350 ++++---- .../ModEditWindow.Materials.ColorSet.cs | 4 +- .../ModEditWindow.Materials.MtrlTab.cs | 6 +- .../Classes/ModEditWindow.Materials.Shpk.cs | 508 +++++------- .../UI/Classes/ModEditWindow.Materials.cs | 10 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 38 +- .../Classes/ModEditWindow.ShaderPackages.cs | 30 +- Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs | 134 ++- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 198 ++--- Penumbra/UI/Classes/ModEditWindow.cs | 9 +- .../Classes/ModFileSystemSelector.Filters.cs | 313 ------- Penumbra/UI/Classes/ModFileSystemSelector.cs | 478 ----------- Penumbra/UI/Classes/ModFilter.cs | 59 -- .../Collections.CollectionSelector.cs | 34 + .../Collections.IndividualCollectionUi.cs | 357 ++++++++ .../Collections.InheritanceUi.cs | 302 +++++++ .../UI/CollectionTab/Collections.NpcCombo.cs | 31 + .../CollectionTab/Collections.SpecialCombo.cs | 42 + .../CollectionTab/Collections.WorldCombo.cs | 24 + Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 101 --- .../ConfigWindow.CollectionsTab.Individual.cs | 367 --------- ...ConfigWindow.CollectionsTab.Inheritance.cs | 317 ------- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 307 ------- Penumbra/UI/ConfigWindow.DebugTab.cs | 644 --------------- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 204 ----- Penumbra/UI/ConfigWindow.Misc.cs | 181 ---- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 776 ------------------ Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 210 ----- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 359 -------- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 251 ------ Penumbra/UI/ConfigWindow.ModPanel.cs | 79 -- Penumbra/UI/ConfigWindow.ModsTab.cs | 205 ----- Penumbra/UI/ConfigWindow.ResourceTab.cs | 152 ---- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 111 --- .../UI/ConfigWindow.SettingsTab.General.cs | 351 -------- Penumbra/UI/ConfigWindow.SettingsTab.cs | 374 --------- Penumbra/UI/ConfigWindow.Tutorial.cs | 167 ---- Penumbra/UI/ConfigWindow.cs | 189 ++--- Penumbra/UI/FileDialogService.cs | 154 ++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 773 +++++++++++++++++ Penumbra/UI/ModsTab/ModFilter.cs | 59 ++ Penumbra/UI/ModsTab/ModPanel.cs | 60 ++ .../UI/ModsTab/ModPanelChangedItemsTab.cs | 40 + Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 69 ++ Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 57 ++ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 718 ++++++++++++++++ Penumbra/UI/ModsTab/ModPanelHeader.cs | 224 +++++ Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 348 ++++++++ Penumbra/UI/ModsTab/ModPanelTabBar.cs | 152 ++++ .../ResourceWatcher/ResourceWatcher.Table.cs | 28 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 4 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 100 +++ Penumbra/UI/Tabs/CollectionsTab.cs | 298 +++++++ Penumbra/UI/Tabs/ConfigTabBar.cs | 67 ++ Penumbra/UI/Tabs/DebugTab.cs | 604 ++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 194 +++++ Penumbra/UI/Tabs/ModsTab.cs | 208 +++++ Penumbra/UI/Tabs/ResourceTab.cs | 152 ++++ Penumbra/UI/Tabs/SettingsTab.cs | 751 +++++++++++++++++ Penumbra/UI/TutorialService.cs | 180 ++++ Penumbra/UI/UiHelpers.cs | 236 ++++++ Penumbra/UI/WindowSystem.cs | 17 +- 87 files changed, 7571 insertions(+), 7280 deletions(-) delete mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs delete mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.cs delete mode 100644 Penumbra/UI/Classes/ModFilter.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.NpcCombo.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.WorldCombo.cs delete mode 100644 Penumbra/UI/ConfigWindow.ChangedItemsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.DebugTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.EffectiveTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.Misc.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Edit.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Header.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Settings.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.ResourceTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.General.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.Tutorial.cs create mode 100644 Penumbra/UI/FileDialogService.cs create mode 100644 Penumbra/UI/ModsTab/ModFileSystemSelector.cs create mode 100644 Penumbra/UI/ModsTab/ModFilter.cs create mode 100644 Penumbra/UI/ModsTab/ModPanel.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelConflictsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelEditTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelHeader.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelSettingsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelTabBar.cs create mode 100644 Penumbra/UI/Tabs/ChangedItemsTab.cs create mode 100644 Penumbra/UI/Tabs/CollectionsTab.cs create mode 100644 Penumbra/UI/Tabs/ConfigTabBar.cs create mode 100644 Penumbra/UI/Tabs/DebugTab.cs create mode 100644 Penumbra/UI/Tabs/EffectiveTab.cs create mode 100644 Penumbra/UI/Tabs/ModsTab.cs create mode 100644 Penumbra/UI/Tabs/ResourceTab.cs create mode 100644 Penumbra/UI/Tabs/SettingsTab.cs create mode 100644 Penumbra/UI/TutorialService.cs create mode 100644 Penumbra/UI/UiHelpers.cs diff --git a/OtterGui b/OtterGui index df1cd8b0..e06d547c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit df1cd8b02d729b2e7f585c301105b37c70d81c3e +Subproject commit e06d547c1690212c1ed3d471b0f9798101f06145 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7e7fac3b..7565c8b3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,8 +16,9 @@ using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; -using Penumbra.Services; - +using Penumbra.Services; +using Penumbra.UI; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -450,7 +451,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.RedrawObjectByName.Label, "Redraw by Name" ); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); ImGui.SameLine(); if( ImGui.Button( "Redraw##Name" ) ) @@ -459,17 +460,17 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.RedrawObject.Label, "Redraw Player Character" ); - if( ImGui.Button( "Redraw##pc" ) && DalamudServices.ClientState.LocalPlayer != null ) + if( ImGui.Button( "Redraw##pc" ) && DalamudServices.SClientState.LocalPlayer != null ) { - Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.ClientState.LocalPlayer, RedrawType.Redraw ); + Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw ); } DrawIntro( Ipc.RedrawObjectByIndex.Label, "Redraw by Index" ); var tmp = _redrawIndex; - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.Objects.Length ) ) + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); + if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length ) ) { - _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.Objects.Length ); + _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.SObjects.Length ); } ImGui.SameLine(); @@ -490,12 +491,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn( IntPtr address, int index ) { - if( index < 0 || index > DalamudServices.Objects.Length || address == IntPtr.Zero || DalamudServices.Objects[ index ]?.Address != address ) + if( index < 0 || index > DalamudServices.SObjects.Length || address == IntPtr.Zero || DalamudServices.SObjects[ index ]?.Address != address ) { _lastRedrawnString = "Invalid"; } - _lastRedrawnString = $"{DalamudServices.Objects[ index ]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{DalamudServices.SObjects[ index ]!.Name} (0x{address:X}, {index})"; } } @@ -794,7 +795,7 @@ public class IpcTester : IDisposable DrawIntro( Ipc.GetInterfaceCollectionName.Label, "Interface Collection" ); ImGui.TextUnformatted( Ipc.GetInterfaceCollectionName.Subscriber( _pi ).Invoke() ); DrawIntro( Ipc.GetCharacterCollectionName.Label, "Character" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); var (c, s) = Ipc.GetCharacterCollectionName.Subscriber( _pi ).Invoke( _characterCollectionName ); ImGui.SameLine(); @@ -832,7 +833,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); ImGui.SameLine(); if( ImGui.Button( "Get" ) ) @@ -1182,7 +1183,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.TrySetModPriority.Label, "Set Priority" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.DragInt( "##Priority", ref _settingsPriority ); ImGui.SameLine(); if( ImGui.Button( "Set##Priority" ) ) @@ -1222,7 +1223,7 @@ public class IpcTester : IDisposable } } - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); using( var c = ImRaii.Combo( "##group", preview ) ) { if( c ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1b581f0f..a225a626 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -16,6 +16,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; +using Penumbra.Interop.Loader; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; @@ -27,10 +28,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (int, int) ApiVersion => (4, 19); - private CommunicatorService? _communicator; - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; - private readonly Dictionary _delegates = new(); public event Action? PreSettingsPanelDraw; @@ -83,34 +80,70 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool Valid => _penumbra != null; - public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra) + private CommunicatorService _communicator; + private Penumbra _penumbra; + private Lumina.GameData? _lumina; + + private Mod.Manager _modManager; + private ResourceLoader _resourceLoader; + private Configuration _config; + private ModCollection.Manager _collectionManager; + private DalamudServices _dalamud; + private TempCollectionManager _tempCollections; + private TempModManager _tempMods; + private ActorService _actors; + + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, + Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, + TempModManager tempMods, ActorService actors) { - _communicator = communicator; - _penumbra = penumbra; - _lumina = (Lumina.GameData?)DalamudServices.GameData.GetType() + _communicator = communicator; + _penumbra = penumbra; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + + _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) - ?.GetValue(DalamudServices.GameData); - foreach (var collection in Penumbra.CollectionManager) + ?.GetValue(_dalamud.GameData); + foreach (var collection in _collectionManager) SubscribeToCollection(collection); - _communicator.CollectionChange.Event += SubscribeToNewCollections; - Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; - Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; + _communicator.CollectionChange.Event += SubscribeToNewCollections; + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _modManager.ModPathChanged += ModPathChangeSubscriber; } public unsafe void Dispose() { - Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator!.CollectionChange.Event -= SubscribeToNewCollections; - Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; - _penumbra = null; - _lumina = null; - _communicator = null; - foreach (var collection in Penumbra.CollectionManager) + if (!Valid) + return; + + foreach (var collection in _collectionManager) { if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; } + + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CollectionChange.Event -= SubscribeToNewCollections; + _modManager.ModPathChanged -= ModPathChangeSubscriber; + _lumina = null; + _communicator = null!; + _penumbra = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -118,7 +151,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetModDirectory() { CheckInitialized(); - return Penumbra.Config.ModDirectory; + return _config.ModDirectory; } private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, @@ -134,17 +167,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - Penumbra.ModManager.ModDirectoryChanged += value; + _modManager.ModDirectoryChanged += value; } remove { CheckInitialized(); - Penumbra.ModManager.ModDirectoryChanged -= value; + _modManager.ModDirectoryChanged -= value; } } public bool GetEnabledState() - => Penumbra.Config.EnableMods; + => _config.EnableMods; public event Action? EnabledChange { @@ -163,7 +196,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetConfiguration() { CheckInitialized(); - return JsonConvert.SerializeObject(Penumbra.Config, Formatting.Indented); + return JsonConvert.SerializeObject(_config, Formatting.Indented); } public event ChangedItemHover? ChangedItemTooltip; @@ -181,11 +214,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidArgument; if (tab != TabType.None) - _penumbra!.ConfigWindow.SelectTab = tab; + _penumbra!.ConfigWindow.SelectTab(tab); if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { - if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (_modManager.TryGetMod(modDirectory, modName, out var mod)) _penumbra!.ConfigWindow.SelectMod(mod); else return PenumbraApiEc.ModMissing; @@ -230,19 +263,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Default); + return ResolvePath(path, _modManager, _collectionManager.Default); } public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Interface); + return ResolvePath(path, _modManager, _collectionManager.Interface); } public string ResolvePlayerPath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, PathResolver.PlayerCollection()); + return ResolvePath(path, _modManager, PathResolver.PlayerCollection()); } // TODO: cleanup when incrementing API level @@ -253,14 +286,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); AssociatedCollection(gameObjectIdx, out var collection); - return ResolvePath(path, Penumbra.ModManager, collection); + return ResolvePath(path, _modManager, collection); } public string ResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, - Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId))); + return ResolvePath(path, _modManager, + _collectionManager.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level @@ -270,20 +303,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string[] ReverseResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, }; - var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); } public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, @@ -297,7 +330,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string[] ReverseResolvePlayerPath(string path) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, @@ -310,14 +343,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return (forward, reverse.Select(p => new[] { p, }).ToArray()); var playerCollection = PathResolver.PlayerCollection(); - var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray(); + var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); var reverseResolved = playerCollection.ReverseResolvePaths(reverse); return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } @@ -333,7 +366,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) collection = ModCollection.Empty; if (collection.HasCache) @@ -355,7 +388,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return string.Empty; - var collection = Penumbra.CollectionManager.ByType((CollectionType)type); + var collection = _collectionManager.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } @@ -366,7 +399,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return (PenumbraApiEc.InvalidArgument, string.Empty); - var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; if (collectionName.Length == 0) { @@ -376,11 +409,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type); + _collectionManager.RemoveSpecialCollection((CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -388,14 +421,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type); + _collectionManager.CreateSpecialCollection((CollectionType)type); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - Penumbra.CollectionManager.SetCollection(collection, (CollectionType)type); + _collectionManager.SetCollection(collection, (CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } @@ -404,9 +437,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, Penumbra.CollectionManager.Default.Name); + return (false, false, _collectionManager.Default.Name); - if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) + if (_collectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) return (true, true, collection.Name); AssociatedCollection(gameObjectIdx, out collection); @@ -419,9 +452,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name); + return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name); - var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + var oldCollection = _collectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; if (collectionName.Length == 0) { @@ -431,12 +464,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - var idx = Penumbra.CollectionManager.Individuals.Index(id); - Penumbra.CollectionManager.RemoveIndividualCollection(idx); + var idx = _collectionManager.Individuals.Index(id); + _collectionManager.RemoveIndividualCollection(idx); return (PenumbraApiEc.Success, oldCollection); } - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -444,40 +477,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - var ids = Penumbra.CollectionManager.Individuals.GetGroup(id); - Penumbra.CollectionManager.CreateIndividualCollection(ids); + var ids = _collectionManager.Individuals.GetGroup(id); + _collectionManager.CreateIndividualCollection(ids); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - Penumbra.CollectionManager.SetCollection(collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index(id)); + _collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id)); return (PenumbraApiEc.Success, oldCollection); } public IList GetCollections() { CheckInitialized(); - return Penumbra.CollectionManager.Select(c => c.Name).ToArray(); + return _collectionManager.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Current.Name; + return _collectionManager.Current.Name; } public string GetDefaultCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Default.Name; + return _collectionManager.Default.Name; } public string GetInterfaceCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Interface.Name; + return _collectionManager.Interface.Name; } // TODO: cleanup when incrementing API level @@ -487,9 +520,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) ? (collection.Name, true) - : (Penumbra.CollectionManager.Default.Name, false); + : (_collectionManager.Default.Name, false); } public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) @@ -508,13 +541,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); + return _modManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); } public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) { CheckInitialized(); - return Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + return _modManager.TryGetMod(modDirectory, modName, out var mod) ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) : null; } @@ -523,10 +556,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return (PenumbraApiEc.ModMissing, null); var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; @@ -541,31 +574,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc ReloadMod(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - Penumbra.ModManager.ReloadMod(mod.Index); + _modManager.ReloadMod(mod.Index); return PenumbraApiEc.Success; } public PenumbraApiEc AddMod(string modDirectory) { CheckInitialized(); - var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory))); + var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); if (!dir.Exists) return PenumbraApiEc.FileMissing; - Penumbra.ModManager.AddMod(dir); + _modManager.AddMod(dir); return PenumbraApiEc.Success; } public PenumbraApiEc DeleteMod(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.NothingChanged; - Penumbra.ModManager.DeleteMod(mod.Index); + _modManager.DeleteMod(mod.Index); return PenumbraApiEc.Success; } @@ -593,7 +626,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return (PenumbraApiEc.ModMissing, string.Empty, false); @@ -608,7 +641,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (newPath.Length == 0) return PenumbraApiEc.InvalidArgument; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return PenumbraApiEc.ModMissing; @@ -626,10 +659,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; @@ -639,10 +672,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; @@ -651,10 +684,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; @@ -664,10 +697,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi string optionName) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); @@ -687,10 +720,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi IReadOnlyList optionNames) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); @@ -728,16 +761,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - var sourceModIdx = Penumbra.ModManager + var sourceModIdx = _modManager .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; - var targetModIdx = Penumbra.ModManager + var targetModIdx = _modManager .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in Penumbra.CollectionManager) + foreach (var collection in _collectionManager) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); - else if (Penumbra.CollectionManager.ByName(collectionName, out var collection)) + else if (_collectionManager.ByName(collectionName, out var collection)) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else return PenumbraApiEc.CollectionMissing; @@ -756,8 +789,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!identifier.IsValid) return (PenumbraApiEc.InvalidArgument, string.Empty); - if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier) - || Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier)) + if (!forceOverwriteCharacter && _collectionManager.Individuals.Individuals.ContainsKey(identifier) + || _tempCollections.Collections.Individuals.ContainsKey(identifier)) return (PenumbraApiEc.CharacterCollectionExists, string.Empty); var name = $"{tag}_{character}"; @@ -765,10 +798,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (ret != PenumbraApiEc.Success) return (ret, name); - if (Penumbra.TempCollections.AddIdentifier(name, identifier)) + if (_tempCollections.AddIdentifier(name, identifier)) return (PenumbraApiEc.Success, name); - Penumbra.TempCollections.RemoveTemporaryCollection(name); + _tempCollections.RemoveTemporaryCollection(name); return (PenumbraApiEc.UnknownError, string.Empty); } @@ -778,7 +811,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; - return Penumbra.TempCollections.CreateTemporaryCollection(name).Length > 0 + return _tempCollections.CreateTemporaryCollection(name).Length > 0 ? PenumbraApiEc.Success : PenumbraApiEc.CollectionExists; } @@ -787,23 +820,26 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length) + if (!_actors.Valid) + return PenumbraApiEc.SystemDisposed; + + if (actorIndex < 0 || actorIndex >= DalamudServices.SObjects.Length) return PenumbraApiEc.InvalidArgument; - var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true); + var identifier = _actors.AwaitedService.FromObject(DalamudServices.SObjects[actorIndex], false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!forceAssignment - && (Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier) - || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier))) + && (_tempCollections.Collections.Individuals.ContainsKey(identifier) + || _collectionManager.Individuals.Individuals.ContainsKey(identifier))) return PenumbraApiEc.CharacterCollectionExists; - var group = Penumbra.TempCollections.Collections.GetGroup(identifier); - return Penumbra.TempCollections.AddIdentifier(collection, group) + var group = _tempCollections.Collections.GetGroup(identifier); + return _tempCollections.AddIdentifier(collection, group) ? PenumbraApiEc.Success : PenumbraApiEc.UnknownError; } @@ -811,7 +847,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryCollection(string character) { CheckInitialized(); - return Penumbra.TempCollections.RemoveByCharacterName(character) + return _tempCollections.RemoveByCharacterName(character) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -819,7 +855,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryCollectionByName(string name) { CheckInitialized(); - return Penumbra.TempCollections.RemoveTemporaryCollection(name) + return _tempCollections.RemoveTemporaryCollection(name) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -833,7 +869,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return Penumbra.TempMods.Register(tag, null, p, m, priority) switch + return _tempMods.Register(tag, null, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -844,8 +880,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi int priority) { CheckInitialized(); - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) - && !Penumbra.CollectionManager.ByName(collectionName, out collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection) + && !_collectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; if (!ConvertPaths(paths, out var p)) @@ -854,7 +890,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch + return _tempMods.Register(tag, collection, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -864,7 +900,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return Penumbra.TempMods.Unregister(tag, null, priority) switch + return _tempMods.Unregister(tag, null, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -875,11 +911,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) { CheckInitialized(); - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) - && !Penumbra.CollectionManager.ByName(collectionName, out collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection) + && !_collectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - return Penumbra.TempMods.Unregister(tag, collection, priority) switch + return _tempMods.Unregister(tag, collection, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -903,9 +939,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var identifier = NameToIdentifier(characterName, worldId); - var collection = Penumbra.TempCollections.Collections.TryGetCollection(identifier, out var c) + var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : Penumbra.CollectionManager.Individual(identifier); + : _collectionManager.Individual(identifier); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -938,13 +974,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) + private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { - collection = Penumbra.CollectionManager.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) + collection = _collectionManager.Default; + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.SObjects.Length) return false; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var data = PathResolver.IdentifyCollection(ptr, false); if (data.Valid) collection = data.ModCollection; @@ -953,20 +989,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid) return ActorIdentifier.Invalid; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); - return Penumbra.Actors.FromObject(ptr, out _, false, true, true); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); + return _actors.AwaitedService.FromObject(ptr, out _, false, true, true); } // Resolve a path given by string for a specific collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static string ResolvePath(string path, Mod.Manager _, ModCollection collection) + private string ResolvePath(string path, Mod.Manager _, ModCollection collection) { - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return path; var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; @@ -983,7 +1019,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (Path.IsPathRooted(resolvedPath)) return _lumina?.GetFileFromDisk(resolvedPath); - return DalamudServices.GameData.GetFile(resolvedPath); + return _dalamud.GameData.GetFile(resolvedPath); } catch (Exception e) { @@ -1054,7 +1090,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var name = c.Name; void Del(ModSettingChange type, int idx, int _, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, name, idx >= 0 ? Penumbra.ModManager[idx].ModPath.Name : string.Empty, inherited); + => ModSettingChanged?.Invoke(type, name, idx >= 0 ? _modManager[idx].ModPath.Name : string.Empty, inherited); _delegates[c] = Del; c.ModSettingChanged += Del; @@ -1079,10 +1115,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi => PostSettingsPanelDraw?.Invoke(modDirectory); // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private static ActorIdentifier NameToIdentifier(string name, ushort worldId) + private ActorIdentifier NameToIdentifier(string name, ushort worldId) { + if (!_actors.Valid) + return ActorIdentifier.Invalid; + // Verified to be valid name beforehand. var b = ByteString.FromStringUnsafe(name, false); - return Penumbra.Actors.CreatePlayer(b, worldId); + return _actors.AwaitedService.CreatePlayer(b, worldId); } } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 5f2042dd..61a37961 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -193,7 +193,7 @@ public partial class ModCollection if (defaultIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Default = Empty; configChanged = true; @@ -209,7 +209,7 @@ public partial class ModCollection if (interfaceIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Interface = Empty; configChanged = true; @@ -225,7 +225,7 @@ public partial class ModCollection if (currentIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning); Current = DefaultName; configChanged = true; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index f65be1ad..f0368e5a 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Penumbra.Api; +using Penumbra.GameData.Actors; using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Services; @@ -76,7 +77,7 @@ public partial class ModCollection _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; ReadCollections(files); LoadCollections(files); - UpdateCurrentCollectionInUse(); + UpdateCurrentCollectionInUse(); CreateNecessaryCaches(); } @@ -131,8 +132,8 @@ public partial class ModCollection var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add(newCollection); - + _collections.Add(newCollection); + Penumbra.SaveService.ImmediateSave(newCollection); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); @@ -180,7 +181,7 @@ public partial class ModCollection // Clear own inheritances. foreach (var inheritance in collection.Inheritance) collection.ClearSubscriptions(inheritance); - + Penumbra.SaveService.ImmediateDelete(collection); _collections.RemoveAt(idx); @@ -346,7 +347,7 @@ public partial class ModCollection // Duplicate collection files are not deleted, just not added here. private void ReadCollections(FilenameService files) { - var inheritances = new List>(); + var inheritances = new List>(); foreach (var file in files.CollectionFiles) { var collection = LoadFromFile(file, out var inheritance); @@ -371,5 +372,106 @@ public partial class ModCollection AddDefaultCollection(); ApplyInheritances(inheritances); } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } } } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index de9d80c4..91fcc5a1 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -1,6 +1,8 @@ using Penumbra.GameData.Enums; using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata.Ecma335; namespace Penumbra.Collections; @@ -132,6 +134,91 @@ public static class CollectionTypeExtensions _ => CollectionType.Inactive, }; } + + // @formatter:off + private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; + private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; + // @formatter:on + + /// A list of definite redundancy possibilities. + public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => DefaultList, + CollectionType.MalePlayerCharacter => DefaultList, + CollectionType.FemalePlayerCharacter => DefaultList, + CollectionType.MaleNonPlayerCharacter => DefaultList, + CollectionType.FemaleNonPlayerCharacter => DefaultList, + CollectionType.MaleMidlander => MalePlayerList, + CollectionType.FemaleMidlander => FemalePlayerList, + CollectionType.MaleHighlander => MalePlayerList, + CollectionType.FemaleHighlander => FemalePlayerList, + CollectionType.MaleWildwood => MalePlayerList, + CollectionType.FemaleWildwood => FemalePlayerList, + CollectionType.MaleDuskwight => MalePlayerList, + CollectionType.FemaleDuskwight => FemalePlayerList, + CollectionType.MalePlainsfolk => MalePlayerList, + CollectionType.FemalePlainsfolk => FemalePlayerList, + CollectionType.MaleDunesfolk => MalePlayerList, + CollectionType.FemaleDunesfolk => FemalePlayerList, + CollectionType.MaleSeekerOfTheSun => MalePlayerList, + CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, + CollectionType.MaleKeeperOfTheMoon => MalePlayerList, + CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, + CollectionType.MaleSeawolf => MalePlayerList, + CollectionType.FemaleSeawolf => FemalePlayerList, + CollectionType.MaleHellsguard => MalePlayerList, + CollectionType.FemaleHellsguard => FemalePlayerList, + CollectionType.MaleRaen => MalePlayerList, + CollectionType.FemaleRaen => FemalePlayerList, + CollectionType.MaleXaela => MalePlayerList, + CollectionType.FemaleXaela => FemalePlayerList, + CollectionType.MaleHelion => MalePlayerList, + CollectionType.FemaleHelion => FemalePlayerList, + CollectionType.MaleLost => MalePlayerList, + CollectionType.FemaleLost => FemalePlayerList, + CollectionType.MaleRava => MalePlayerList, + CollectionType.FemaleRava => FemalePlayerList, + CollectionType.MaleVeena => MalePlayerList, + CollectionType.FemaleVeena => FemalePlayerList, + CollectionType.MaleMidlanderNpc => MaleNpcList, + CollectionType.FemaleMidlanderNpc => FemaleNpcList, + CollectionType.MaleHighlanderNpc => MaleNpcList, + CollectionType.FemaleHighlanderNpc => FemaleNpcList, + CollectionType.MaleWildwoodNpc => MaleNpcList, + CollectionType.FemaleWildwoodNpc => FemaleNpcList, + CollectionType.MaleDuskwightNpc => MaleNpcList, + CollectionType.FemaleDuskwightNpc => FemaleNpcList, + CollectionType.MalePlainsfolkNpc => MaleNpcList, + CollectionType.FemalePlainsfolkNpc => FemaleNpcList, + CollectionType.MaleDunesfolkNpc => MaleNpcList, + CollectionType.FemaleDunesfolkNpc => FemaleNpcList, + CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, + CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, + CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, + CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, + CollectionType.MaleSeawolfNpc => MaleNpcList, + CollectionType.FemaleSeawolfNpc => FemaleNpcList, + CollectionType.MaleHellsguardNpc => MaleNpcList, + CollectionType.FemaleHellsguardNpc => FemaleNpcList, + CollectionType.MaleRaenNpc => MaleNpcList, + CollectionType.FemaleRaenNpc => FemaleNpcList, + CollectionType.MaleXaelaNpc => MaleNpcList, + CollectionType.FemaleXaelaNpc => FemaleNpcList, + CollectionType.MaleHelionNpc => MaleNpcList, + CollectionType.FemaleHelionNpc => FemaleNpcList, + CollectionType.MaleLostNpc => MaleNpcList, + CollectionType.FemaleLostNpc => FemaleNpcList, + CollectionType.MaleRavaNpc => MaleNpcList, + CollectionType.FemaleRavaNpc => FemaleNpcList, + CollectionType.MaleVeenaNpc => MaleNpcList, + CollectionType.FemaleVeenaNpc => FemaleNpcList, + CollectionType.Individual => DefaultList, + _ => Array.Empty(), + }; public static CollectionType FromParts( SubRace race, Gender gender, bool npc ) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9499ae30..48b3c29e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,7 +11,7 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -29,25 +29,26 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; - public bool EnableMods { get; set; } = true; - public string ModDirectory { get; set; } = string.Empty; + public bool EnableMods { get; set; } = true; + public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; public bool HideUiWhenUiHidden { get; set; } = false; public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; + public bool UseNoModsInInspect { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public int OptionGroupCollapsibleMin { get; set; } = 5; #if DEBUG public bool DebugMode { get; set; } = true; @@ -63,35 +64,35 @@ public class Configuration : IPluginConfiguration public bool OnlyAddMatchingResources { get; set; } = true; public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - [JsonConverter( typeof( SortModeConverter ) )] - [JsonProperty( Order = int.MaxValue )] - public ISortMode< Mod > SortMode = ISortMode< Mod >.FoldersFirst; + [JsonConverter(typeof(SortModeConverter))] + [JsonProperty(Order = int.MaxValue)] + public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; - public bool OpenFoldersByDefault { get; set; } = false; - public int SingleGroupRadioMax { get; set; } = 2; - public string DefaultImportFolder { get; set; } = string.Empty; - public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public bool ScaleModSelector { get; set; } = false; + public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; + public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool OpenFoldersByDefault { get; set; } = false; + public int SingleGroupRadioMax { get; set; } = 2; + public string DefaultImportFolder { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PrintSuccessfulCommandsToChat { get; set; } = true; - public bool FixMainWindow { get; set; } = false; - public bool AutoDeduplicateOnImport { get; set; } = true; - public bool EnableHttpApi { get; set; } = true; + public bool FixMainWindow { get; set; } = false; + public bool AutoDeduplicateOnImport { get; set; } = true; + public bool EnableHttpApi { get; set; } = true; - public string DefaultModImportPath { get; set; } = string.Empty; - public bool AlwaysOpenDefaultImport { get; set; } = false; - public bool KeepDefaultMetaChanges { get; set; } = false; - public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public string DefaultModImportPath { get; set; } = string.Empty; + public bool AlwaysOpenDefaultImport { get; set; } = false; + public bool KeepDefaultMetaChanges { get; set; } = false; + public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; - public Dictionary< ColorId, uint > Colors { get; set; } - = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); + public Dictionary Colors { get; set; } + = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); /// /// Load the current configuration. @@ -121,6 +122,7 @@ public class Configuration : IPluginConfiguration Error = HandleDeserializationError, }); } + migrator.Migrate(this); } @@ -129,17 +131,17 @@ public class Configuration : IPluginConfiguration { try { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( _fileName, text ); + var text = JsonConvert.SerializeObject(this, Formatting.Indented); + File.WriteAllText(_fileName, text); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not save plugin configuration:\n{e}" ); + Penumbra.Log.Error($"Could not save plugin configuration:\n{e}"); } } public void Save() - => _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); + => _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration); /// Contains some default values or boundaries for config values. public static class Constants @@ -152,41 +154,39 @@ public class Configuration : IPluginConfiguration public const int DefaultScaledSize = 20; public const int MinScaledSize = 5; - public static readonly ISortMode< Mod >[] ValidSortModes = + public static readonly ISortMode[] ValidSortModes = { - ISortMode< Mod >.FoldersFirst, - ISortMode< Mod >.Lexicographical, + ISortMode.FoldersFirst, + ISortMode.Lexicographical, new ModFileSystem.ImportDate(), new ModFileSystem.InverseImportDate(), - ISortMode< Mod >.InverseFoldersFirst, - ISortMode< Mod >.InverseLexicographical, - ISortMode< Mod >.FoldersLast, - ISortMode< Mod >.InverseFoldersLast, - ISortMode< Mod >.InternalOrder, - ISortMode< Mod >.InverseInternalOrder, + ISortMode.InverseFoldersFirst, + ISortMode.InverseLexicographical, + ISortMode.FoldersLast, + ISortMode.InverseFoldersLast, + ISortMode.InternalOrder, + ISortMode.InverseInternalOrder, }; } /// Convert SortMode Types to their name. - private class SortModeConverter : JsonConverter< ISortMode< Mod > > + private class SortModeConverter : JsonConverter> { - public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) + public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer) { - value ??= ISortMode< Mod >.FoldersFirst; - serializer.Serialize( writer, value.GetType().Name ); + value ??= ISortMode.FoldersFirst; + serializer.Serialize(writer, value.GetType().Name); } - public override ISortMode< Mod > ReadJson( JsonReader reader, Type objectType, ISortMode< Mod >? existingValue, + public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue, bool hasExistingValue, - JsonSerializer serializer ) + JsonSerializer serializer) { - var name = serializer.Deserialize< string >( reader ); - if( name == null || !Constants.ValidSortModes.FindFirst( s => s.GetType().Name == name, out var mode ) ) - { - return existingValue ?? ISortMode< Mod >.FoldersFirst; - } + var name = serializer.Deserialize(reader); + if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode)) + return existingValue ?? ISortMode.FoldersFirst; return mode; } } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 6e365e1b..bf997c2c 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -93,12 +93,12 @@ public partial class TexToolsImporter ImGui.TableNextColumn(); if( ex == null ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config) ); ImGui.TextUnformatted( dir?.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ?? "Unknown Directory" ); } else { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value(Penumbra.Config) ); ImGui.TextUnformatted( ex.Message ); ImGuiUtil.HoverTooltip( ex.ToString() ); } diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index f6bb9866..669f81d7 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -10,8 +10,9 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String.Classes; +using Penumbra.UI; using Penumbra.UI.Classes; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -196,7 +197,7 @@ public sealed class Texture : IDisposable return File.OpenRead( Path ); } - var file = DalamudServices.GameData.GetFile( Path ); + var file = DalamudServices.SGameData.GetFile( Path ); return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); } @@ -216,7 +217,7 @@ public sealed class Texture : IDisposable { if( game ) { - if( !DalamudServices.GameData.FileExists( path ) ) + if( !DalamudServices.SGameData.FileExists( path ) ) { continue; } @@ -227,7 +228,7 @@ public sealed class Texture : IDisposable } using var id = ImRaii.PushId( idx ); - using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(), game ) ) + using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config), game ) ) { var p = game ? $"--> {path}" : path[ skipPrefix.. ]; if( ImGui.Selectable( p, path == startPath ) && path != startPath ) @@ -245,12 +246,12 @@ public sealed class Texture : IDisposable ImGuiUtil.HoverTooltip( tooltip ); } - public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogService fileDialog ) { _tmpPath ??= Path; using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); - ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * ImGuiHelpers.GlobalScale ); + new Vector2( UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y ) ); + ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) { @@ -277,7 +278,7 @@ public sealed class Texture : IDisposable } } - manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + fileDialog.OpenFilePicker( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false ); } ImGui.SameLine(); diff --git a/Penumbra/Interop/RedrawService.cs b/Penumbra/Interop/RedrawService.cs index f738320c..6739e95e 100644 --- a/Penumbra/Interop/RedrawService.cs +++ b/Penumbra/Interop/RedrawService.cs @@ -295,8 +295,8 @@ public sealed unsafe partial class RedrawService : IDisposable private static GameObject? GetLocalPlayer() { - var gPosePlayer = DalamudServices.Objects[GPosePlayerIdx]; - return gPosePlayer ?? DalamudServices.Objects[0]; + var gPosePlayer = DalamudServices.SObjects[GPosePlayerIdx]; + return gPosePlayer ?? DalamudServices.SObjects[0]; } public bool GetName(string lowerName, out GameObject? actor) diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index 7135eebb..fa09f64d 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -30,7 +30,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr return; _communicator.CollectionChange.Event += CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged += TerritoryClear; + DalamudServices.SClientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; _enabled = true; } @@ -41,7 +41,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr return; _communicator.CollectionChange.Event -= CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; + DalamudServices.SClientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; _enabled = false; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 65d032de..e39b85f4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -138,9 +138,9 @@ public unsafe partial class PathResolver { var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ]; var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < DalamudServices.Objects.Length ) + if( idx >= 0 && idx < DalamudServices.SObjects.Length ) { - var obj = DalamudServices.Objects[ idx ]; + var obj = DalamudServices.SObjects[ idx ]; return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; } } @@ -204,9 +204,9 @@ public unsafe partial class PathResolver if( timelinePtr != IntPtr.Zero ) { var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); - if( actorIdx >= 0 && actorIdx < DalamudServices.Objects.Length ) + if( actorIdx >= 0 && actorIdx < DalamudServices.SObjects.Length ) { - _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); + _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.SObjects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); } } @@ -234,14 +234,14 @@ public unsafe partial class PathResolver private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) { - var owner = DalamudServices.Objects.SearchById( id ); + var owner = DalamudServices.SObjects.SearchById( id ); if( owner == null ) { return null; } var idx = ( ( GameObject* )owner.Address )->ObjectIndex; - return DalamudServices.Objects[ idx + 1 ]; + return DalamudServices.SObjects[ idx + 1 ]; } private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) @@ -252,8 +252,8 @@ public unsafe partial class PathResolver { var obj = vfxParams->GameObjectType switch { - 0 => DalamudServices.Objects.SearchById( vfxParams->GameObjectId ), - 2 => DalamudServices.Objects[ ( int )vfxParams->GameObjectId ], + 0 => DalamudServices.SObjects.SearchById( vfxParams->GameObjectId ), + 2 => DalamudServices.SObjects[ ( int )vfxParams->GameObjectId ], 4 => GetOwnedObject( vfxParams->GameObjectId ), _ => null, }; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index edfe566b..983dcdb6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -112,7 +112,7 @@ public unsafe partial class PathResolver // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject) { - gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + gameObject = (GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var draw = (DrawObject*)drawObject; if (gameObject != null && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) @@ -244,9 +244,9 @@ public unsafe partial class PathResolver // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - for (var i = 0; i < DalamudServices.Objects.Length; ++i) + for (var i = 0; i < DalamudServices.SObjects.Length; ++i) { - var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i); + var ptr = (GameObject*)DalamudServices.SObjects.GetObjectAddress(i); if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 516d2a2b..a619755a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -38,7 +38,7 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !DalamudServices.ClientState.IsLoggedIn ) + if( !DalamudServices.SClientState.IsLoggedIn ) { var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) @@ -87,7 +87,7 @@ public unsafe partial class PathResolver public static ModCollection PlayerCollection() { using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - var gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( 0 ); + var gameObject = ( GameObject* )DalamudServices.SObjects.GetObjectAddress( 0 ); if( gameObject == null ) { return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index edcb76d5..059a0550 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -29,7 +29,7 @@ public partial class PathResolver : IDisposable private readonly CommunicatorService _communicator; private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO + private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO private static DrawObjectState _drawObjects = null!; // TODO private static readonly BitArray ValidHumanModels; internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO @@ -39,7 +39,7 @@ public partial class PathResolver : IDisposable private readonly SubfileHelper _subFiles; static PathResolver() - => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData); + => ValidHumanModels = GetValidHumanModels(DalamudServices.SGameData); public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) { diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index cc3add2d..32a9c925 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -141,7 +141,7 @@ public unsafe class ImcFile : MetaBaseFile public override void Reset() { - var file = DalamudServices.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.SGameData.GetFile( Path.ToString() ); fixed( byte* ptr = file!.Data ) { MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); @@ -153,7 +153,7 @@ public unsafe class ImcFile : MetaBaseFile : base( 0 ) { Path = manip.GamePath(); - var file = DalamudServices.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.SGameData.GetFile( Path.ToString() ); if( file == null ) { throw new ImcException( manip, Path ); @@ -172,7 +172,7 @@ public unsafe class ImcFile : MetaBaseFile public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) { - var file = DalamudServices.GameData.GetFile( path ); + var file = DalamudServices.SGameData.GetFile( path ); exists = false; if( file == null ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 8358b97d..daefee5c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -39,7 +39,7 @@ public static class ItemSwap return true; } - var file = DalamudServices.GameData.GetFile( path.InternalName.ToString() ); + var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() ); if( file != null ) { data = file.Data; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 6cca0356..8249c189 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -140,7 +140,7 @@ public sealed class FileSwap : Swap } swap.SwapToModded = redirections( swap.SwapToRequestPath ); - swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); + swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() ); swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); swap.FileData = type switch diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 004fd284..9e196424 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -16,7 +16,10 @@ using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.ModTab; +using Penumbra.UI.Tabs; using Penumbra.Util; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; namespace Penumbra; @@ -28,7 +31,7 @@ public class PenumbraNew public static readonly Logger Log = new(); public readonly ServiceProvider Services; - public PenumbraNew(Penumbra pnumb, DalamudPluginInterface pi) + public PenumbraNew(Penumbra penumbra, DalamudPluginInterface pi) { var startTimer = new StartTracker(); using var time = startTimer.Measure(StartTimeType.Total); @@ -48,7 +51,7 @@ public class PenumbraNew // Add Dalamud services var dalamud = new DalamudServices(pi); dalamud.AddServices(services); - services.AddSingleton(pnumb); + services.AddSingleton(penumbra); // Add Game Data services.AddSingleton() @@ -84,28 +87,49 @@ public class PenumbraNew // Add Mod Services services.AddSingleton() .AddSingleton() - .AddSingleton(); - + .AddSingleton(); + // Add main services services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Interface - services.AddSingleton() + services.AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - - // Add API - services.AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); - Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + // Add API + services.AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton() + .AddSingleton(); + + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index a3227b92..7320822d 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -13,9 +13,9 @@ using Dalamud.Plugin; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; - -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + namespace Penumbra.Services; public class DalamudServices @@ -25,39 +25,37 @@ public class DalamudServices pluginInterface.Inject(this); try { - var serviceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); - var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); + var serviceType = + typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); + var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); var interfaceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); if (serviceType == null || configType == null || interfaceType == null) - { return; - } - var configService = serviceType.MakeGenericType(configType); + var configService = serviceType.MakeGenericType(configType); var interfaceService = serviceType.MakeGenericType(interfaceType); - var configGetter = configService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var configGetter = configService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); _interfaceGetter = interfaceService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (configGetter == null || _interfaceGetter == null) - { return; - } _dalamudConfig = configGetter.Invoke(null, null); if (_dalamudConfig != null) { - _saveDalamudConfig = _dalamudConfig.GetType().GetMethod("Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + _saveDalamudConfig = _dalamudConfig.GetType() + .GetMethod("Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (_saveDalamudConfig == null) { - _dalamudConfig = null; + _dalamudConfig = null; _interfaceGetter = null; } } } catch { - _dalamudConfig = null; + _dalamudConfig = null; _saveDalamudConfig = null; - _interfaceGetter = null; + _interfaceGetter = null; } } @@ -65,64 +63,70 @@ public class DalamudServices { services.AddSingleton(PluginInterface); services.AddSingleton(Commands); - services.AddSingleton(GameData); - services.AddSingleton(ClientState); + services.AddSingleton(SGameData); + services.AddSingleton(SClientState); services.AddSingleton(Chat); services.AddSingleton(Framework); services.AddSingleton(Conditions); services.AddSingleton(Targets); - services.AddSingleton(Objects); + services.AddSingleton(SObjects); services.AddSingleton(TitleScreenMenu); services.AddSingleton(GameGui); services.AddSingleton(KeyState); services.AddSingleton(SigScanner); services.AddSingleton(this); } - + // TODO remove static // @formatter:off [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager SGameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState SClientState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable SObjects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on + public UiBuilder UiBuilder + => PluginInterface.UiBuilder; + + public ObjectTable Objects + => SObjects; + + public ClientState ClientState + => SClientState; + + public DataManager GameData + => SGameData; + public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; - private readonly object? _dalamudConfig; + private readonly object? _dalamudConfig; private readonly MethodInfo? _interfaceGetter; - private readonly MethodInfo? _saveDalamudConfig; - + private readonly MethodInfo? _saveDalamudConfig; + public bool GetDalamudConfig(string fieldName, out T? value) { value = default; try { if (_dalamudConfig == null) - { return false; - } var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (getter == null) - { return false; - } var result = getter.GetValue(_dalamudConfig); if (result is not T v) - { return false; - } value = v; return true; @@ -139,22 +143,20 @@ public class DalamudServices try { if (_dalamudConfig == null) - { return false; - } var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (getter == null) - { return false; - } getter.SetValue(_dalamudConfig, value); if (windowFieldName != null) { var inter = _interfaceGetter!.Invoke(null, null); - var settingsWindow = inter?.GetType().GetField("settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(inter); - settingsWindow?.GetType().GetField(windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(settingsWindow, value); + var settingsWindow = inter?.GetType() + .GetField("settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(inter); + settingsWindow?.GetType().GetField(windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?.SetValue(settingsWindow, value); } _saveDalamudConfig!.Invoke(_dalamudConfig, null); @@ -166,4 +168,4 @@ public class DalamudServices return false; } } -} \ No newline at end of file +} diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 403626b6..1adec97f 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using OtterGui.Classes; using Penumbra.Util; namespace Penumbra.Services; @@ -50,7 +49,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper { get { - _task.Wait(); + _task?.Wait(); return Service!; } } @@ -58,8 +57,8 @@ public abstract class AsyncServiceWrapper : IServiceWrapper public bool Valid => Service != null && !_isDisposed; - public event Action? FinishedCreation; - private readonly Task _task; + public event Action? FinishedCreation; + private Task? _task; private bool _isDisposed; @@ -99,6 +98,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper { Service = service; Penumbra.Log.Verbose($"[{Name}] Created."); + _task = null; FinishedCreation?.Invoke(); } }); @@ -110,6 +110,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper return; _isDisposed = true; + _task = null; if (Service is IDisposable d) d.Dispose(); Penumbra.Log.Verbose($"[{Name}] Disposed."); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index a41dc28b..3edc0e9c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -30,8 +30,11 @@ public static class Colors public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF; + public const uint ReniColorButton = 0xFFCC648D; + public const uint ReniColorHovered = 0xFFB070B0; + public const uint ReniColorActive = 0xFF9070E0; - public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { // @formatter:off @@ -53,6 +56,6 @@ public static class Colors // @formatter:on }; - public static uint Value( this ColorId color ) - => Penumbra.Config.Colors.TryGetValue( color, out var value ) ? value : color.Data().DefaultColor; -} \ No newline at end of file + public static uint Value(this ColorId color, Configuration config) + => config.Colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; +} diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 0f56cd77..26f747b7 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -12,34 +12,34 @@ public static class Combos => Race( label, 100, current, out race ); public static bool Race( string label, float unscaledWidth, ModelRace current, out ModelRace race ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1 ); public static bool Gender( string label, Gender current, out Gender gender ) => Gender( label, 120, current, out gender ); public static bool Gender( string label, float unscaledWidth, Gender current, out Gender gender ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1 ); public static bool EqdpEquipSlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); public static bool EqpEquipSlot( string label, float width, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); public static bool AccessorySlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); public static bool SubRace( string label, SubRace current, out SubRace subRace ) - => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1 ); public static bool RspAttribute( string label, RspAttribute current, out RspAttribute attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, + => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1 ); public static bool EstSlot( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); + => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute ); public static bool ImcType( string label, ObjectType current, out ObjectType type ) - => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, + => ImGuiUtil.GenericEnumCombo( label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, ObjectTypeExtensions.ToName ); } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 9abeb3f5..ce3b2dd3 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -332,7 +332,7 @@ public class ItemSwapWindow : IDisposable CreateMod(); ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale); ImGui.Checkbox("Use File Swaps", ref _useFileSwaps); ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n" + "even those available from game files, use File Swaps to default game files where possible."); @@ -356,7 +356,7 @@ public class ItemSwapWindow : IDisposable CreateOption(); ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale); _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection); ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" @@ -415,7 +415,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted( $"Take {article1}" ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) ) { if( combo ) @@ -444,7 +444,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted( $"and put {article2} on {article1}" ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) ) { if( combo ) @@ -636,7 +636,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale); if (ImGui.InputInt("##targetId", ref _targetId, 0, 0)) _targetId = Math.Clamp(_targetId, 0, byte.MaxValue); @@ -650,7 +650,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale); if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0)) _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue); @@ -714,7 +714,7 @@ public class ItemSwapWindow : IDisposable return; ImGui.NewLine(); - DrawHeaderLine(300 * ImGuiHelpers.GlobalScale); + DrawHeaderLine(300 * UiHelpers.Scale); ImGui.NewLine(); DrawSwapBar(); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 1e0b609b..1d722927 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -5,26 +5,27 @@ using System.Numerics; using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private class FileEditor< T > where T : class, IWritable + private class FileEditor where T : class, IWritable { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; - private readonly Func< T, bool, bool > _drawEdit; - private readonly Func< string > _getInitialPath; - private readonly Func< byte[], T? > _parseFile; + private readonly string _tabName; + private readonly string _fileType; + private readonly Func> _getFiles; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private Mod.Editor.FileRegistry? _currentPath; private T? _currentFile; @@ -36,29 +37,28 @@ public partial class ModEditWindow private T? _defaultFile; private Exception? _defaultException; - private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + private IReadOnlyList _list = null!; - private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); + private readonly FileDialogService _fileDialog; - public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile ) + public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func> getFiles, + Func drawEdit, Func getInitialPath, Func? parseFile) { _tabName = tabName; _fileType = fileType; _getFiles = getFiles; _drawEdit = drawEdit; _getInitialPath = getInitialPath; + _fileDialog = fileDialog; _parseFile = parseFile ?? DefaultParseFile; } public void Draw() { _list = _getFiles(); - using var tab = ImRaii.TabItem( _tabName ); - if( !tab ) - { + using var tab = ImRaii.TabItem(_tabName); + if (!tab) return; - } ImGui.NewLine(); DrawFileSelectCombo(); @@ -67,35 +67,35 @@ public partial class ModEditWindow ResetButton(); ImGui.SameLine(); DefaultInput(); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawFilePanel(); } private void DefaultInput() { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); - ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); + ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); _inInput = ImGui.IsItemActive(); - if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { _fileDialog.Reset(); try { - var file = DalamudServices.GameData.GetFile( _defaultPath ); - if( file != null ) + var file = DalamudServices.SGameData.GetFile(_defaultPath); + if (file != null) { _defaultException = null; - _defaultFile = _parseFile( file.Data ); + _defaultFile = _parseFile(file.Data); } else { _defaultFile = null; - _defaultException = new Exception( "File does not exist." ); + _defaultException = new Exception("File does not exist."); } } - catch( Exception e ) + catch (Exception e) { _defaultFile = null; _defaultException = e; @@ -103,25 +103,23 @@ public partial class ModEditWindow } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) - { - _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => - { - if( !success ) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", + _defaultFile == null, true)) + _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, + (success, name) => { - return; - } + if (!success) + return; - try - { - File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); - } - }, _getInitialPath() ); - } + try + { + File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); + } + }, _getInitialPath(), false); _fileDialog.Draw(); } @@ -136,64 +134,58 @@ public partial class ModEditWindow private void DrawFileSelectCombo() { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); - if( !combo ) - { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..."); + if (!combo) return; - } - foreach( var file in _list ) + foreach (var file in _list) { - if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) - { - UpdateCurrentFile( file ); - } + if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath))) + UpdateCurrentFile(file); - if( ImGui.IsItemHovered() ) + if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted( "All Game Paths" ); + ImGui.TextUnformatted("All Game Paths"); ImGui.Separator(); - using var t = ImRaii.Table( "##Tooltip", 2, ImGuiTableFlags.SizingFixedFit ); - foreach( var (option, gamePath) in file.SubModUsage ) + using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); + foreach (var (option, gamePath) in file.SubModUsage) { ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); + UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGui.TextUnformatted( option.FullName ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGui.TextUnformatted(option.FullName); } } - if( file.SubModUsage.Count > 0 ) + if (file.SubModUsage.Count > 0) { ImGui.SameLine(); - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGuiUtil.RightAlign( file.SubModUsage[ 0 ].Item2.Path.ToString() ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); } } } - private static T? DefaultParseFile( byte[] bytes ) - => Activator.CreateInstance( typeof( T ), bytes ) as T; + private static T? DefaultParseFile(byte[] bytes) + => Activator.CreateInstance(typeof(T), bytes) as T; - private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) + private void UpdateCurrentFile(Mod.Editor.FileRegistry path) { - if( ReferenceEquals( _currentPath, path ) ) - { + if (ReferenceEquals(_currentPath, path)) return; - } _changed = false; _currentPath = path; _currentException = null; try { - var bytes = File.ReadAllBytes( _currentPath.File.FullName ); - _currentFile = _parseFile( bytes ); + var bytes = File.ReadAllBytes(_currentPath.File.FullName); + _currentFile = _parseFile(bytes); } - catch( Exception e ) + catch (Exception e) { _currentFile = null; _currentException = e; @@ -202,76 +194,74 @@ public partial class ModEditWindow private void SaveButton() { - if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) + if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) { - File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); + File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); _changed = false; } } private void ResetButton() { - if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed ) ) + if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed)) { var tmp = _currentPath; _currentPath = null; - UpdateCurrentFile( tmp! ); + UpdateCurrentFile(tmp!); } } private void DrawFilePanel() { - using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##filePanel", -Vector2.One, true); + if (!child) return; - } - if( _currentPath != null ) + if (_currentPath != null) { - if( _currentFile == null ) + if (_currentFile == null) { - ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); - if( _currentException != null ) + ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); + if (_currentException != null) { using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _currentException.ToString() ); + ImGuiUtil.TextWrapped(_currentException.ToString()); } } else { - using var id = ImRaii.PushId( 0 ); - _changed |= _drawEdit( _currentFile, false ); + using var id = ImRaii.PushId(0); + _changed |= _drawEdit(_currentFile, false); } } - if( !_inInput && _defaultPath.Length > 0 ) + if (!_inInput && _defaultPath.Length > 0) { - if( _currentPath != null ) + if (_currentPath != null) { ImGui.NewLine(); ImGui.NewLine(); - ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); + ImGui.TextUnformatted($"Preview of {_defaultPath}:"); ImGui.Separator(); } - if( _defaultFile == null ) + if (_defaultFile == null) { - ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); - if( _defaultException != null ) + ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); + if (_defaultException != null) { using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _defaultException.ToString() ); + ImGuiUtil.TextWrapped(_defaultException.ToString()); } } else { - using var id = ImRaii.PushId( 1 ); - _drawEdit( _defaultFile, true ); + using var id = ImRaii.PushId(1); + _drawEdit(_defaultFile, true); } } } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 18f17711..469b10a8 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -14,184 +14,156 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256); - private LowerString _fileFilter = LowerString.Empty; - private bool _showGamePaths = true; - private string _gamePathEdit = string.Empty; - private int _fileIdx = -1; - private int _pathIdx = -1; - private int _folderSkip = 0; - private bool _overviewMode = false; - private LowerString _fileOverviewFilter1 = LowerString.Empty; - private LowerString _fileOverviewFilter2 = LowerString.Empty; - private LowerString _fileOverviewFilter3 = LowerString.Empty; + private readonly HashSet _selectedFiles = new(256); + private LowerString _fileFilter = LowerString.Empty; + private bool _showGamePaths = true; + private string _gamePathEdit = string.Empty; + private int _fileIdx = -1; + private int _pathIdx = -1; + private int _folderSkip = 0; + private bool _overviewMode = false; + private LowerString _fileOverviewFilter1 = LowerString.Empty; + private LowerString _fileOverviewFilter2 = LowerString.Empty; + private LowerString _fileOverviewFilter3 = LowerString.Empty; - private bool CheckFilter( Mod.Editor.FileRegistry registry ) - => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); + private bool CheckFilter(Mod.Editor.FileRegistry registry) + => _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase); - private bool CheckFilter( (Mod.Editor.FileRegistry, int) p ) - => CheckFilter( p.Item1 ); + private bool CheckFilter((Mod.Editor.FileRegistry, int) p) + => CheckFilter(p.Item1); private void DrawFileTab() { - using var tab = ImRaii.TabItem( "File Redirections" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("File Redirections"); + if (!tab) return; - } DrawOptionSelectHeader(); DrawButtonHeader(); - if( _overviewMode ) - { + if (_overviewMode) DrawFileManagementOverview(); - } else - { DrawFileManagementNormal(); - } - using var child = ImRaii.Child( "##files", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##files", -Vector2.One, true); + if (!child) return; - } - if( _overviewMode ) - { + if (_overviewMode) DrawFilesOverviewMode(); - } else - { DrawFilesNormalMode(); - } } private void DrawFilesOverviewMode() { var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); + var skips = ImGuiClip.GetNecessarySkips(height); - using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One ); + using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One); - if( !list ) - { + if (!list) return; - } var width = ImGui.GetContentRegionAvail().X / 8; - ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, width * 3 ); - ImGui.TableSetupColumn( "##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize ); - ImGui.TableSetupColumn( "##option", ImGuiTableColumnFlags.WidthFixed, width * 2 ); + ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, width * 3); + ImGui.TableSetupColumn("##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize); + ImGui.TableSetupColumn("##option", ImGuiTableColumnFlags.WidthFixed, width * 2); var idx = 0; - var files = _editor!.AvailableFiles.SelectMany( f => + var files = _editor!.AvailableFiles.SelectMany(f => { var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 - ? Enumerable.Repeat( ( file, "Unused", string.Empty, 0x40000080u ), 1 ) - : f.SubModUsage.Select( s => ( file, s.Item2.ToString(), s.Item1.FullName, - _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u ) ); - } ); + ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) + : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, + _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + }); - void DrawLine( (string, string, string, uint) data ) + void DrawLine((string, string, string, uint) data) { - using var id = ImRaii.PushId( idx++ ); + using var id = ImRaii.PushId(idx++); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item1 ); + ImGuiUtil.CopyOnClickSelectable(data.Item1); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item2 ); + ImGuiUtil.CopyOnClickSelectable(data.Item2); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item3 ); + ImGuiUtil.CopyOnClickSelectable(data.Item3); } - bool Filter( (string, string, string, uint) data ) - => _fileOverviewFilter1.IsContained( data.Item1 ) - && _fileOverviewFilter2.IsContained( data.Item2 ) - && _fileOverviewFilter3.IsContained( data.Item3 ); + bool Filter((string, string, string, uint) data) + => _fileOverviewFilter1.IsContained(data.Item1) + && _fileOverviewFilter2.IsContained(data.Item2) + && _fileOverviewFilter3.IsContained(data.Item3); - var end = ImGuiClip.FilteredClippedDraw( files, skips, Filter, DrawLine ); - ImGuiClip.DrawEndDummy( end, height ); + var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine); + ImGuiClip.DrawEndDummy(end, height); } private void DrawFilesNormalMode() { - using var list = ImRaii.Table( "##table", 1 ); + using var list = ImRaii.Table("##table", 1); - if( !list ) - { + if (!list) return; - } - foreach( var (registry, i) in _editor!.AvailableFiles.WithIndex().Where( CheckFilter ) ) + foreach (var (registry, i) in _editor!.AvailableFiles.WithIndex().Where(CheckFilter)) { - using var id = ImRaii.PushId( i ); + using var id = ImRaii.PushId(i); ImGui.TableNextColumn(); - DrawSelectable( registry ); + DrawSelectable(registry); - if( !_showGamePaths ) - { + if (!_showGamePaths) continue; - } - using var indent = ImRaii.PushIndent( 50f ); - for( var j = 0; j < registry.SubModUsage.Count; ++j ) + using var indent = ImRaii.PushIndent(50f); + for (var j = 0; j < registry.SubModUsage.Count; ++j) { - var (subMod, gamePath) = registry.SubModUsage[ j ]; - if( subMod != _editor.CurrentOption ) - { + var (subMod, gamePath) = registry.SubModUsage[j]; + if (subMod != _editor.CurrentOption) continue; - } - PrintGamePath( i, j, registry, subMod, gamePath ); + PrintGamePath(i, j, registry, subMod, gamePath); } - PrintNewGamePath( i, registry, _editor.CurrentOption ); + PrintNewGamePath(i, registry, _editor.CurrentOption); } } - private static string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color ) + private static string DrawFileTooltip(Mod.Editor.FileRegistry registry, ColorId color) { (string, int) GetMulti() { - var groups = registry.SubModUsage.GroupBy( s => s.Item1 ).ToArray(); - return ( string.Join( "\n", groups.Select( g => g.Key.Name ) ), groups.Length ); + var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); + return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); } var (text, groupCount) = color switch { - ColorId.ConflictingMod => ( string.Empty, 0 ), - ColorId.NewMod => ( registry.SubModUsage[ 0 ].Item1.Name, 1 ), + ColorId.ConflictingMod => (string.Empty, 0), + ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), ColorId.InheritedMod => GetMulti(), - _ => ( string.Empty, 0 ), + _ => (string.Empty, 0), }; - if( text.Length > 0 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( text ); - } + if (text.Length > 0 && ImGui.IsItemHovered()) + ImGui.SetTooltip(text); - return ( groupCount, registry.SubModUsage.Count ) switch + return (groupCount, registry.SubModUsage.Count) switch { (0, 0) => "(unused)", (1, 1) => "(used 1 time)", @@ -200,95 +172,91 @@ public partial class ModEditWindow }; } - private void DrawSelectable( Mod.Editor.FileRegistry registry ) + private void DrawSelectable(Mod.Editor.FileRegistry registry) { - var selected = _selectedFiles.Contains( registry ); - var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : - registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; - using var c = ImRaii.PushColor( ImGuiCol.Text, color.Value() ); - if( ConfigWindow.Selectable( registry.RelPath.Path, selected ) ) + var selected = _selectedFiles.Contains(registry); + var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : + registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; + using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value(Penumbra.Config)); + if (UiHelpers.Selectable(registry.RelPath.Path, selected)) { - if( selected ) - { - _selectedFiles.Remove( registry ); - } + if (selected) + _selectedFiles.Remove(registry); else - { - _selectedFiles.Add( registry ); - } + _selectedFiles.Add(registry); } - var rightText = DrawFileTooltip( registry, color ); + var rightText = DrawFileTooltip(registry, color); ImGui.SameLine(); - ImGuiUtil.RightAlign( rightText ); + ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath( int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath ) + private void PrintGamePath(int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) { - using var id = ImRaii.PushId( j ); + using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength)) { _fileIdx = i; _pathIdx = j; _gamePathEdit = tmp; } - ImGuiUtil.HoverTooltip( "Clear completely to remove the path from this mod." ); + ImGuiUtil.HoverTooltip("Clear completely to remove the path from this mod."); - if( ImGui.IsItemDeactivatedAfterEdit() ) + if (ImGui.IsItemDeactivatedAfterEdit()) { - if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) ) - { - _editor!.SetGamePath( _fileIdx, _pathIdx, path ); - } + if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) + _editor!.SetGamePath(_fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; } - else if( _fileIdx == i && _pathIdx == j && ( !Utf8GamePath.FromString( _gamePathEdit, out var path, false ) - || !path.IsEmpty && !path.Equals( gamePath ) && !_editor!.CanAddGamePath( path )) ) + else if (_fileIdx == i + && _pathIdx == j + && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + || !path.IsEmpty && !path.Equals(gamePath) && !_editor!.CanAddGamePath(path))) { ImGui.SameLine(); - ImGui.SetCursorPosX( pos ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + ImGui.SetCursorPosX(pos); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } } - private void PrintNewGamePath( int i, Mod.Editor.FileRegistry registry, ISubMod subMod ) + private void PrintNewGamePath(int i, Mod.Editor.FileRegistry registry, ISubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputTextWithHint("##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength)) { _fileIdx = i; _pathIdx = -1; _gamePathEdit = tmp; } - if( ImGui.IsItemDeactivatedAfterEdit() ) + if (ImGui.IsItemDeactivatedAfterEdit()) { - if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty ) - { - _editor!.SetGamePath( _fileIdx, _pathIdx, path ); - } + if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) + _editor!.SetGamePath(_fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; } - else if( _fileIdx == i && _pathIdx == -1 && (!Utf8GamePath.FromString( _gamePathEdit, out var path, false ) - || !path.IsEmpty && !_editor!.CanAddGamePath( path )) ) + else if (_fileIdx == i + && _pathIdx == -1 + && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + || !path.IsEmpty && !_editor!.CanAddGamePath(path))) { ImGui.SameLine(); - ImGui.SetCursorPosX( pos ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + ImGui.SetCursorPosX(pos); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } } @@ -296,115 +264,97 @@ public partial class ModEditWindow { ImGui.NewLine(); - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); - ImGui.SetNextItemWidth( 30 * ImGuiHelpers.GlobalScale ); - 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." ); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, 0)); + ImGui.SetNextItemWidth(30 * UiHelpers.Scale); + 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(); - if( ImGui.Button( "Add Paths" ) ) - { - _editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip ); - } + 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." ); + "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 ) ); - } + 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." ); + ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option."); ImGui.SameLine(); - if( ImGui.Button( "Delete Selected Files" ) ) - { - _editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) ); - } + if (ImGui.Button("Delete Selected Files")) + _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!!!" ); + "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."; - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) ) + if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { var failedFiles = _editor!.ApplyFiles(); - if( failedFiles > 0 ) - { - Penumbra.Log.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." ); - } + if (failedFiles > 0) + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}."); } ImGui.SameLine(); var label = changes ? "Revert Changes" : "Reload Files"; - var length = new Vector2( ImGui.CalcTextSize( "Revert Changes" ).X, 0 ); - if( ImGui.Button( label, length ) ) - { + var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); + if (ImGui.Button(label, length)) _editor!.RevertFiles(); - } - ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." ); + ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh."); ImGui.SameLine(); - ImGui.Checkbox( "Overview Mode", ref _overviewMode ); + ImGui.Checkbox("Overview Mode", ref _overviewMode); } private void DrawFileManagementNormal() { - ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); - LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(250 * UiHelpers.Scale); + LowerString.InputWithHint("##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.Checkbox( "Show Game Paths", ref _showGamePaths ); + ImGui.Checkbox("Show Game Paths", ref _showGamePaths); ImGui.SameLine(); - if( ImGui.Button( "Unselect All" ) ) - { + if (ImGui.Button("Unselect All")) _selectedFiles.Clear(); - } ImGui.SameLine(); - if( ImGui.Button( "Select Visible" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( CheckFilter ) ); - } + if (ImGui.Button("Select Visible")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter)); ImGui.SameLine(); - if( ImGui.Button( "Select Unused" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.SubModUsage.Count == 0 ) ); - } + if (ImGui.Button("Select Unused")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.SubModUsage.Count == 0)); ImGui.SameLine(); - if( ImGui.Button( "Select Used Here" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.CurrentUsage > 0 ) ); - } + if (ImGui.Button("Select Used Here")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.CurrentUsage > 0)); ImGui.SameLine(); - ImGuiUtil.RightAlign( $"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected" ); + ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected"); } private void DrawFileManagementOverview() { - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ) - .Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ) - .Push( ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize ); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize); var width = ImGui.GetContentRegionAvail().X / 8; - ImGui.SetNextItemWidth( width * 3 ); - LowerString.InputWithHint( "##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 3); + LowerString.InputWithHint("##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.SetNextItemWidth( width * 3 ); - LowerString.InputWithHint( "##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 3); + LowerString.InputWithHint("##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.SetNextItemWidth( width * 2 ); - LowerString.InputWithHint( "##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 2); + LowerString.InputWithHint("##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs index bdfae0b8..d259e300 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -229,8 +229,8 @@ public partial class ModEditWindow var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var hasDye = file.ColorDyeSets.Length > colorSetIdx; var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); - var floatSize = 70 * ImGuiHelpers.GlobalScale; - var intSize = 45 * ImGuiHelpers.GlobalScale; + var floatSize = 70 * UiHelpers.Scale; + var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); ColorSetCopyClipboardButton( row, dye ); ImGui.SameLine(); diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 24887288..7c121ca6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : DalamudServices.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + : DalamudServices.SGameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); LoadedShpkPathName = path.ToPath(); } @@ -100,7 +100,7 @@ public partial class ModEditWindow { var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); TextureLabels.Clear(); - TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + TextureLabelWidth = 50f * UiHelpers.Scale; using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { for( var i = 0; i < Mtrl.Textures.Length; ++i ) @@ -112,7 +112,7 @@ public partial class ModEditWindow } } - TextureLabelWidth = TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } public void UpdateShaderKeyLabels() diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 5238d9a0..92e4db6d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -17,36 +17,35 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput( MtrlTab tab, bool disabled ) + private bool DrawPackageNameInput(MtrlTab tab, bool disabled) { var ret = false; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.AssociatedShpk = null; tab.LoadedShpkPath = FullPath.Empty; } - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - tab.LoadShpk( tab.FindAssociatedShpk( out _, out _ ) ); - } + if (ImGui.IsItemDeactivatedAfterEdit()) + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); return ret; } - private static bool DrawShaderFlagsInput( MtrlFile file, bool disabled ) + private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled) { var ret = false; - var shpkFlags = ( int )file.ShaderPackage.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + var shpkFlags = (int)file.ShaderPackage.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - file.ShaderPackage.Flags = ( uint )shpkFlags; + file.ShaderPackage.Flags = (uint)shpkFlags; ret = true; } @@ -57,86 +56,72 @@ public partial class ModEditWindow /// Show the currently associated shpk file, if any, and the buttons to associate /// a specific shpk from your drive, the modded shpk by path or the default shpk. /// - private void DrawCustomAssociations( MtrlTab tab ) + private void DrawCustomAssociations(MtrlTab tab) { var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if( ImGui.Selectable( text ) ) - { - ImGui.SetClipboardText( tab.LoadedShpkPathName ); - } + if (ImGui.Selectable(text)) + ImGui.SetClipboardText(tab.LoadedShpkPathName); - ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - if( ImGui.Button( "Associate Custom .shpk File" ) ) - { - _materialFileDialog.OpenFileDialog( "Associate Custom .shpk File...", ".shpk", ( success, name ) => + if (ImGui.Button("Associate Custom .shpk File")) + _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { - if( !success ) - { - return; - } + if (success) + tab.LoadShpk(new FullPath(name[0])); + }, 1, _mod!.ModPath.FullName, false); - tab.LoadShpk( new FullPath( name ) ); - } ); - } - - var moddedPath = tab.FindAssociatedShpk( out var defaultPath, out var gamePath ); + var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( tab.LoadedShpkPath ) ) ) - { - tab.LoadShpk( moddedPath ); - } + if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), + moddedPath.Equals(tab.LoadedShpkPath))) + tab.LoadShpk(moddedPath); - if( !gamePath.Path.Equals( moddedPath.InternalName ) ) + if (!gamePath.Path.Equals(moddedPath.InternalName)) { ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( tab.LoadedShpkPath.InternalName ) ) ) - { - tab.LoadShpk( new FullPath( gamePath ) ); - } + if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, + gamePath.Path.Equals(tab.LoadedShpkPath.InternalName))) + tab.LoadShpk(new FullPath(gamePath)); } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - private static bool DrawShaderKey( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) { var ret = false; - using var t2 = ImRaii.TreeNode( tab.ShaderKeyLabels[ idx ], disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( !t2 || disabled ) - { + using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); + if (!t2 || disabled) return ret; - } - var key = tab.Mtrl.ShaderPackage.ShaderKeys[ idx ]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) + var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + if (shpkKey.HasValue) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); - if( c ) - { - foreach( var value in shpkKey.Value.Values ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); + if (c) + foreach (var value in shpkKey.Value.Values) { - if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) + if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) { - tab.Mtrl.ShaderPackage.ShaderKeys[ idx ].Value = value; - ret = true; + tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; + ret = true; tab.UpdateShaderKeyLabels(); } } - } } - if( ImGui.Button( "Remove Key" ) ) + if (ImGui.Button("Remove Key")) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx-- ); + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); ret = true; tab.UpdateShaderKeyLabels(); } @@ -144,19 +129,18 @@ public partial class ModEditWindow return ret; } - private static bool DrawNewShaderKey( MtrlTab tab ) + private static bool DrawNewShaderKey(MtrlTab tab) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); var ret = false; - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}" ) ) + using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) { - if( c ) - { - foreach( var idx in tab.MissingShaderKeyIndices ) + if (c) + foreach (var idx in tab.MissingShaderKeyIndices) { - var key = tab.AssociatedShpk!.MaterialKeys[ idx ]; + var key = tab.AssociatedShpk!.MaterialKeys[idx]; - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId ) ) + if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) { tab.NewKeyDefault = key.DefaultValue; tab.NewKeyId = key.Id; @@ -164,17 +148,16 @@ public partial class ModEditWindow tab.UpdateShaderKeyLabels(); } } - } } ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) + if (ImGui.Button("Add Key")) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey { Category = tab.NewKeyId, Value = tab.NewKeyDefault, - } ); + }); ret = true; tab.UpdateShaderKeyLabels(); } @@ -182,70 +165,59 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialShaderKeys( MtrlTab tab, bool disabled ) + private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0 ) ) - { + if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 + && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) return false; - } - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Shader Keys"); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx ) - { - ret |= DrawShaderKey( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) + ret |= DrawShaderKey(tab, disabled, ref idx); - if( !disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0 ) - { - ret |= DrawNewShaderKey( tab ); - } + if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) + ret |= DrawNewShaderKey(tab); return ret; } - private static void DrawMaterialShaders( MtrlTab tab ) + private static void DrawMaterialShaders(MtrlTab tab) { - if( tab.AssociatedShpk == null ) - { + if (tab.AssociatedShpk == null) return; - } - ImRaii.TreeNode( tab.VertexShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( tab.PixelShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); } - private static bool DrawMaterialConstantValues( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[ idx ]; - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( name ); - if( !t2 ) - { + var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode(name); + if (!t2) return false; - } font.Dispose(); - var constant = tab.Mtrl.ShaderPackage.Constants[ idx ]; + var constant = tab.Mtrl.ShaderPackage.Constants[idx]; var ret = false; - var values = tab.Mtrl.GetConstantValues( constant ); - if( values.Length > 0 ) + var values = tab.Mtrl.GetConstantValues(constant); + if (values.Length > 0) { var valueOffset = constant.ByteOffset >> 2; - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { - var paramName = MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( $"{paramName} (at 0x{( valueOffset + valueIdx ) << 2:X4})", ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.UpdateConstantLabels(); @@ -254,24 +226,23 @@ public partial class ModEditWindow } else { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - if( !disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) + if (!disabled + && !tab.HasMalformedMaterialConstants + && tab.OrphanedMaterialValues.Count == 0 + && tab.AliasedMaterialValueCount == 0 + && ImGui.Button("Remove Constant")) { - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx-- ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) + tab.Mtrl.ShaderPackage.ShaderValues = + tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); + for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) { - if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) - { - tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; - } + if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) + tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; } ret = true; @@ -281,21 +252,19 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialOrphans( MtrlTab tab, bool disabled ) + private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({tab.OrphanedMaterialValues.Count})" ); - if( !t2 ) - { + using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); + if (!t2) return false; - } var ret = false; - foreach( var idx in tab.OrphanedMaterialValues ) + foreach (var idx in tab.OrphanedMaterialValues) { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); + if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", + ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.UpdateConstantLabels(); @@ -305,36 +274,34 @@ public partial class ModEditWindow return ret; } - private static bool DrawNewMaterialParam( MtrlTab tab ) + private static bool DrawNewMaterialParam(MtrlTab tab) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) { - using var c = ImRaii.Combo( "##NewConstantId", tab.MissingMaterialConstants[ tab.NewConstantIdx ].Name ); - if( c ) - { - foreach( var (constant, idx) in tab.MissingMaterialConstants.WithIndex() ) + using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); + if (c) + foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) { - if( ImGui.Selectable( constant.Name, constant.Id == tab.NewConstantId ) ) + if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) { tab.NewConstantIdx = idx; tab.NewConstantId = constant.Id; } } - } } ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) + if (ImGui.Button("Add Constant")) { - var (_, _, byteSize) = tab.MissingMaterialConstants[ tab.NewConstantIdx ]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant { Id = tab.NewConstantId, - ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), + ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), ByteSize = byteSize, - } ); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, byteSize >> 2 ); + }); + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); tab.UpdateConstantLabels(); return true; } @@ -342,92 +309,76 @@ public partial class ModEditWindow return false; } - private static bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0 ) ) - { + if (tab.Mtrl.ShaderPackage.Constants.Length == 0 + && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 + && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) return false; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t = ImRaii.TreeNode( tab.MaterialConstantLabel ); - if( !t ) - { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); + if (!t) return false; - } font.Dispose(); var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx ) - { - ret |= DrawMaterialConstantValues( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) + ret |= DrawMaterialConstantValues(tab, disabled, ref idx); - if( tab.OrphanedMaterialValues.Count > 0 ) - { - ret |= DrawMaterialOrphans( tab, disabled ); - } - else if( !disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0 ) - { - ret |= DrawNewMaterialParam( tab ); - } + if (tab.OrphanedMaterialValues.Count > 0) + ret |= DrawMaterialOrphans(tab, disabled); + else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) + ret |= DrawNewMaterialParam(tab); return ret; } - private static bool DrawMaterialSampler( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) { - var (label, filename) = tab.Samplers[ idx ]; - using var tree = ImRaii.TreeNode( label ); - if( !tree ) - { + var (label, filename) = tab.Samplers[idx]; + using var tree = ImRaii.TreeNode(label); + if (!tree) return false; - } - ImRaii.TreeNode( filename, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[ idx ]; + var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) { - fixed( ushort* v2 = &v ) + fixed (ushort* v2 = &v) { - return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags ); + return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); } } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; + + var samplerFlags = (int)sampler.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + { + tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; + ret = true; } - var samplerFlags = ( int )sampler.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Sampler Flags", ref samplerFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + if (!disabled + && tab.OrphanedSamplers.Count == 0 + && tab.AliasedSamplerCount == 0 + && ImGui.Button("Remove Sampler")) { - tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )samplerFlags; - ret = true; - } - - if( !disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button( "Remove Sampler" ) ) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx-- ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); + for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) { - if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) - { - --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; - } + if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) + --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; } ret = true; @@ -438,114 +389,99 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialNewSampler( MtrlTab tab ) + private static bool DrawMaterialNewSampler(MtrlTab tab) { - var (name, id) = tab.MissingSamplers[ tab.NewSamplerIdx ]; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var c = ImRaii.Combo( "##NewSamplerId", $"{name} (ID: 0x{id:X8})" ) ) + var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; + ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); + using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) { - if( c ) - { - foreach( var (sampler, idx) in tab.MissingSamplers.WithIndex() ) + if (c) + foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId ) ) + if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) { tab.NewSamplerIdx = idx; tab.NewSamplerId = sampler.Id; } } - } } ImGui.SameLine(); - if( !ImGui.Button( "Add Sampler" ) ) - { + if (!ImGui.Button("Add Sampler")) return false; - } - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler { SamplerId = tab.NewSamplerId, - TextureIndex = ( byte )tab.Mtrl.Textures.Length, + TextureIndex = (byte)tab.Mtrl.Textures.Length, Flags = 0, - } ); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture + }); + tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture { Path = string.Empty, Flags = 0, - } ); + }); tab.UpdateSamplers(); tab.UpdateTextureLabels(); return true; } - private static bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) + private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && ( disabled || ( tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) ) ) - { + if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 + && tab.Mtrl.Textures.Length == 0 + && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) return false; - } - using var t = ImRaii.TreeNode( "Samplers" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Samplers"); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx ) - { - ret |= DrawMaterialSampler( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) + ret |= DrawMaterialSampler(tab, disabled, ref idx); - if( tab.OrphanedSamplers.Count > 0 ) + if (tab.OrphanedSamplers.Count > 0) { - using var t2 = ImRaii.TreeNode( $"Orphan Textures ({tab.OrphanedSamplers.Count})" ); - if( t2 ) - { - foreach( var idx in tab.OrphanedSamplers ) + using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); + if (t2) + foreach (var idx in tab.OrphanedSamplers) { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); + ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", + ImGuiTreeNodeFlags.Leaf) + .Dispose(); } - } } - else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) + else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) { - ret |= DrawMaterialNewSampler( tab ); + ret |= DrawMaterialNewSampler(tab); } return ret; } - private bool DrawMaterialShaderResources( MtrlTab tab, bool disabled ) + private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) { var ret = false; - if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) - { + if (!ImGui.CollapsingHeader("Advanced Shader Resources")) return ret; - } - ret |= DrawPackageNameInput( tab, disabled ); - ret |= DrawShaderFlagsInput( tab.Mtrl, disabled ); - DrawCustomAssociations( tab ); - ret |= DrawMaterialShaderKeys( tab, disabled ); - DrawMaterialShaders( tab ); - ret |= DrawMaterialConstants( tab, disabled ); - ret |= DrawMaterialSamplers( tab, disabled ); + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab.Mtrl, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); + ret |= DrawMaterialConstants(tab, disabled); + ret |= DrawMaterialSamplers(tab, disabled); return ret; } - private static string? MaterialParamName( bool componentOnly, int offset ) + private static string? MaterialParamName(bool componentOnly, int offset) { - if( offset < 0 ) - { + if (offset < 0) return null; - } - return ( componentOnly, offset & 0x3 ) switch + return (componentOnly, offset & 0x3) switch { (true, 0) => "x", (true, 1) => "y", @@ -559,10 +495,10 @@ public partial class ModEditWindow }; } - private static (string? Name, bool ComponentOnly) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle( int firstComponent, int lastComponent ) - => ( firstComponent, lastComponent ) switch + static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch { (0, 4) => " ", (0, 0) => ".x ", @@ -578,28 +514,22 @@ public partial class ModEditWindow _ => string.Empty, }; - if( valueLength == 0 || valueOffset < 0 ) - { - return ( null, false ); - } + if (valueLength == 0 || valueOffset < 0) + return (null, false); - var firstVector = valueOffset >> 2; - var lastVector = ( valueOffset + valueLength - 1 ) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; - if( firstVector == lastVector ) - { - return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent )}", true ); - } + var firstVector = valueOffset >> 2; + var lastVector = (valueOffset + valueLength - 1) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = (valueOffset + valueLength - 1) & 0x3; + if (firstVector == lastVector) + return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); - var sb = new StringBuilder( 128 ); - sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 ).TrimEnd()}" ); - for( var i = firstVector + 1; i < lastVector; ++i ) - { - sb.Append( $", [{i}]" ); - } + var sb = new StringBuilder(128); + sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); + for (var i = firstVector + 1; i < lastVector; ++i) + sb.Append($", [{i}]"); - sb.Append( $", [{lastVector}]{VectorSwizzle( 0, lastComponent )}" ); - return ( sb.ToString(), false ); + sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); + return (sb.ToString(), false); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 2cf5cd26..7a170803 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -29,8 +29,6 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); DrawOtherMaterialDetails( tab.Mtrl, disabled ); - _materialFileDialog.Draw(); - return !disabled && ret; } @@ -39,7 +37,7 @@ public partial class ModEditWindow var ret = false; using var table = ImRaii.Table( "##Textures", 2 ); ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); @@ -80,7 +78,7 @@ public partial class ModEditWindow ret = true; } - ImGui.SameLine( 200 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); + ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) { @@ -171,7 +169,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); var tmp = info.CurrentMaterials[ 0 ]; if( ImGui.InputText( "##0", ref tmp, 64 ) ) { @@ -184,7 +182,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); tmp = info.CurrentMaterials[ i ]; if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 66ac4a87..e1f348a5 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -117,7 +117,7 @@ public partial class ModEditWindow private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -154,7 +154,7 @@ public partial class ModEditWindow using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) { var value = defaultEntry.HasFlag( flag ); @@ -184,7 +184,7 @@ public partial class ModEditWindow // Values ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); var idx = 0; foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) { @@ -209,7 +209,7 @@ public partial class ModEditWindow private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -321,10 +321,10 @@ public partial class ModEditWindow private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); private static float IdWidth - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; private static float SmallIdWidth - => 45 * ImGuiHelpers.GlobalScale; + => 45 * UiHelpers.Scale; // Convert throwing to null-return if the file does not exist. private static ImcEntry? GetDefault( ImcManipulation imc ) @@ -380,7 +380,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( PrimaryIdTooltip ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. @@ -406,7 +406,7 @@ public partial class ModEditWindow } else { - if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) + if( IdInput( "##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -435,7 +435,7 @@ public partial class ModEditWindow } else { - ImGui.Dummy( new Vector2( 70 * ImGuiHelpers.GlobalScale, 0 ) ); + ImGui.Dummy( new Vector2( 70 * UiHelpers.Scale, 0 ) ); } ImGuiUtil.HoverTooltip( VariantIdTooltip ); @@ -511,7 +511,7 @@ public partial class ModEditWindow // Values using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, @@ -572,7 +572,7 @@ public partial class ModEditWindow private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -669,13 +669,13 @@ public partial class ModEditWindow private static GmpManipulation _new = new(GmpEntry.Default, 1); private static float RotationWidth - => 75 * ImGuiHelpers.GlobalScale; + => 75 * UiHelpers.Scale; private static float UnkWidth - => 50 * ImGuiHelpers.GlobalScale; + => 50 * UiHelpers.Scale; private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -787,7 +787,7 @@ public partial class ModEditWindow private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); private static float FloatWidth - => 150 * ImGuiHelpers.GlobalScale; + => 150 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -847,7 +847,7 @@ public partial class ModEditWindow var value = meta.Entry; ImGui.SetNextItemWidth( FloatWidth ); using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), + def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config), def != value ); if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) { @@ -864,7 +864,7 @@ public partial class ModEditWindow { int tmp = currentId; ImGui.SetNextItemWidth( width ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, border ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border ); using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border ); if( ImGui.InputInt( label, ref tmp, 0 ) ) { @@ -880,7 +880,7 @@ public partial class ModEditWindow private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue ) { using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue ); + defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue ); newValue = currentValue; ImGui.Checkbox( label, ref newValue ); ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); @@ -894,7 +894,7 @@ public partial class ModEditWindow { newValue = currentValue; using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue ); ImGui.SetNextItemWidth( width ); if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) ) diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index cef13a4c..bce1c61f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -67,7 +67,7 @@ public partial class ModEditWindow }; var blob = shader.Blob; - tab.FileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => + tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => { if( !success ) { @@ -87,7 +87,7 @@ public partial class ModEditWindow Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - } ); + }, null, false ); } private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) @@ -97,7 +97,7 @@ public partial class ModEditWindow return; } - tab.FileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => { if( !success ) { @@ -106,7 +106,7 @@ public partial class ModEditWindow try { - shaders[ idx ].Blob = File.ReadAllBytes( name ); + shaders[ idx ].Blob = File.ReadAllBytes(name[0] ); } catch( Exception e ) { @@ -128,7 +128,7 @@ public partial class ModEditWindow } tab.Shpk.SetChanged(); - } ); + }, 1, null, false ); } private static unsafe void DrawRawDisassembly( Shader shader ) @@ -193,7 +193,7 @@ public partial class ModEditWindow var ret = false; if( !disabled ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f ); if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) { ret = true; @@ -285,11 +285,11 @@ public partial class ModEditWindow return false; } - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); ImGui.TableHeadersRow(); var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); @@ -362,7 +362,7 @@ public partial class ModEditWindow using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); if( c ) { @@ -385,7 +385,7 @@ public partial class ModEditWindow using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); if( c ) { @@ -420,7 +420,7 @@ public partial class ModEditWindow DrawShaderPackageStartCombo( tab ); DrawShaderPackageEndCombo( tab ); - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) { tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); @@ -429,7 +429,7 @@ public partial class ModEditWindow var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) ? "The ID is already in use. Please choose a different name." : string.Empty; - if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * ImGuiHelpers.GlobalScale, ImGui.GetFrameHeight() ), tooltip, + if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip, tooltip.Length > 0 ) ) { return false; diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs index 448d8e35..0bcac7a3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using Lumina.Misc; using OtterGui; @@ -16,19 +15,20 @@ public partial class ModEditWindow public readonly ShpkFile Shpk; public string NewMaterialParamName = string.Empty; - public uint NewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu); public short NewMaterialParamStart; public short NewMaterialParamEnd; - public readonly FileDialogManager FileDialog = ConfigWindow.SetupFileManager(); + public readonly FileDialogService FileDialog; public readonly string Header; public readonly string Extension; - public ShpkTab( byte[] bytes ) + public ShpkTab(FileDialogService fileDialog, byte[] bytes) { - Shpk = new ShpkFile( bytes, true ); - Header = $"Shader Package for DirectX {( int )Shpk.DirectXVersion}"; + FileDialog = fileDialog; + Shpk = new ShpkFile(bytes, true); + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", @@ -47,134 +47,130 @@ public partial class ModEditWindow } public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; - public readonly List< string > MalformedParameters = new(); - public readonly HashSet< uint > UsedIds = new(16); - public readonly List< (string Name, short Index) > Orphans = new(16); + public readonly List MalformedParameters = new(); + public readonly HashSet UsedIds = new(16); + public readonly List<(string Name, short Index)> Orphans = new(16); public void Update() { - var materialParams = Shpk.GetConstantById( ShpkFile.MaterialParamsConstantId ); - var numParameters = ( ( Shpk.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 4; + var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4; Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; MalformedParameters.Clear(); UsedIds.Clear(); - foreach( var (param, idx) in Shpk.MaterialParams.WithIndex() ) + foreach (var (param, idx) in Shpk.MaterialParams.WithIndex()) { - UsedIds.Add( param.Id ); + UsedIds.Add(param.Id); var iStart = param.ByteOffset >> 4; - var jStart = ( param.ByteOffset >> 2 ) & 3; - var iEnd = ( param.ByteOffset + param.ByteSize - 1 ) >> 4; - var jEnd = ( ( param.ByteOffset + param.ByteSize - 1 ) >> 2 ) & 3; - if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) + var jStart = (param.ByteOffset >> 2) & 3; + var iEnd = (param.ByteOffset + param.ByteSize - 1) >> 4; + var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; + if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}" ); + MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } - if( iEnd >= numParameters ) + if (iEnd >= numParameters) { MalformedParameters.Add( - $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})" ); + $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})"); continue; } - for( var i = iStart; i <= iEnd; ++i ) + for (var i = iStart; i <= iEnd; ++i) { var end = i == iEnd ? jEnd : 3; - for( var j = i == iStart ? jStart : 0; j <= end; ++j ) + for (var j = i == iStart ? jStart : 0; j <= end; ++j) { - var tt = $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})"; - Matrix[ i, j ] = ( $"0x{param.Id:X8}", tt, ( short )idx, 0 ); + var tt = + $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})"; + Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0); } } } - UpdateOrphans( materialParams ); - UpdateColors( materialParams ); + UpdateOrphans(materialParams); + UpdateColors(materialParams); } - public void UpdateOrphanStart( int orphanStart ) + public void UpdateOrphanStart(int orphanStart) { - var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; - UpdateOrphanStart( orphanStart, oldEnd ); + var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1; + UpdateOrphanStart(orphanStart, oldEnd); } - private void UpdateOrphanStart( int orphanStart, int oldEnd ) + private void UpdateOrphanStart(int orphanStart, int oldEnd) { - var count = Math.Min( NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count ); - NewMaterialParamStart = ( short )orphanStart; - var current = Orphans[ NewMaterialParamStart ].Index; - for( var i = NewMaterialParamStart; i < count; ++i ) + var count = Math.Min(NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count); + NewMaterialParamStart = (short)orphanStart; + var current = Orphans[NewMaterialParamStart].Index; + for (var i = NewMaterialParamStart; i < count; ++i) { - var next = Orphans[ i ].Index; - if( current++ != next ) + var next = Orphans[i].Index; + if (current++ != next) { - NewMaterialParamEnd = ( short )( i - 1 ); + NewMaterialParamEnd = (short)(i - 1); return; } - if( next == oldEnd ) + if (next == oldEnd) { NewMaterialParamEnd = i; return; } } - NewMaterialParamEnd = ( short )( count - 1 ); + NewMaterialParamEnd = (short)(count - 1); } - private void UpdateOrphans( ShpkFile.Resource? materialParams ) + private void UpdateOrphans(ShpkFile.Resource? materialParams) { - var oldStart = Orphans.Count > 0 ? Orphans[ NewMaterialParamStart ].Index : -1; - var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + var oldStart = Orphans.Count > 0 ? Orphans[NewMaterialParamStart].Index : -1; + var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1; Orphans.Clear(); short newMaterialParamStart = 0; - for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) - for( var j = 0; j < 4; ++j ) + for (var i = 0; i < Matrix.GetLength(0); ++i) { - if( !Matrix[ i, j ].Name.IsNullOrEmpty() ) + for (var j = 0; j < 4; ++j) { - continue; - } + if (!Matrix[i, j].Name.IsNullOrEmpty()) + continue; - Matrix[ i, j ] = ( "(none)", string.Empty, -1, 0 ); - var linear = ( short )( 4 * i + j ); - if( oldStart == linear ) - { - newMaterialParamStart = ( short )Orphans.Count; - } + Matrix[i, j] = ("(none)", string.Empty, -1, 0); + var linear = (short)(4 * i + j); + if (oldStart == linear) + newMaterialParamStart = (short)Orphans.Count; - Orphans.Add( ( $"{materialParams?.Name ?? string.Empty}{MaterialParamName( false, linear )}", linear ) ); + Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear)); + } } - if( Orphans.Count == 0 ) - { + if (Orphans.Count == 0) return; - } - UpdateOrphanStart( newMaterialParamStart, oldEnd ); + UpdateOrphanStart(newMaterialParamStart, oldEnd); } - private void UpdateColors( ShpkFile.Resource? materialParams ) + private void UpdateColors(ShpkFile.Resource? materialParams) { var lastIndex = -1; - for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + for (var i = 0; i < Matrix.GetLength(0); ++i) { - var usedComponents = ( materialParams?.Used?[ i ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); - for( var j = 0; j < 4; ++j ) + var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All) + | (materialParams?.UsedDynamically ?? 0); + for (var j = 0; j < 4; ++j) { - var color = ( ( byte )usedComponents & ( 1 << j ) ) != 0 + var color = ((byte)usedComponents & (1 << j)) != 0 ? ColorType.Used : 0; - if( Matrix[ i, j ].Index == lastIndex || Matrix[ i, j ].Index < 0 ) - { + if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0) color |= ColorType.Continuation; - } - lastIndex = Matrix[ i, j ].Index; - Matrix[ i, j ].Color = color; + lastIndex = Matrix[i, j].Index; + Matrix[i, j].Color = color; } } } @@ -185,4 +181,4 @@ public partial class ModEditWindow public byte[] Write() => Shpk.Write(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index c3ab2a14..05a878e9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -2,12 +2,9 @@ using System; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterTex; using Penumbra.Import.Textures; namespace Penumbra.UI.Classes; @@ -18,210 +15,181 @@ public partial class ModEditWindow private readonly Texture _right = new(); private readonly CombinedTexture _center; - private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); - private bool _overlayCollapsed = true; + private bool _overlayCollapsed = true; - private bool _addMipMaps = true; - private int _currentSaveAs = 0; + private bool _addMipMaps = true; + private int _currentSaveAs; private static readonly (string, string)[] SaveAsStrings = { - ( "As Is", "Save the current texture with its own format without additional conversion or compression, if possible." ), - ( "RGBA (Uncompressed)", - "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality." ), - ( "BC3 (Simple Compression)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality." ), - ( "BC7 (Complex Compression)", - "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while." ), + ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), + ("RGBA (Uncompressed)", + "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), + ("BC3 (Simple Compression)", + "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality."), + ("BC7 (Complex Compression)", + "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), }; - private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) + private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize) { - using var child = ImRaii.Child( label, size, true ); - if( !child ) - { + using var child = ImRaii.Child(label, size, true); + if (!child) return; - } - using var id = ImRaii.PushId( label ); - ImGuiUtil.DrawTextButton( label, new Vector2( -1, 0 ), ImGui.GetColorU32( ImGuiCol.FrameBg ) ); + using var id = ImRaii.PushId(label); + ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, - _dialogManager ); - var files = _editor!.TexFiles.SelectMany( f => f.SubModUsage.Select( p => (p.Item2.ToString(), true) ) - .Prepend( (f.File.FullName, false ))); - tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", - files, _mod.ModPath.FullName.Length + 1 ); + tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, + _fileDialog); + var files = _editor!.TexFiles.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + .Prepend((f.File.FullName, false))); + tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", + files, _mod.ModPath.FullName.Length + 1); - if( tex == _left ) - { - _center.DrawMatrixInputLeft( size.X ); - } + if (tex == _left) + _center.DrawMatrixInputLeft(size.X); else - { - _center.DrawMatrixInputRight( size.X ); - } + _center.DrawMatrixInputRight(size.X); ImGui.NewLine(); - using var child2 = ImRaii.Child( "image" ); - if( child2 ) - { - tex.Draw( imageSize ); - } + using var child2 = ImRaii.Child("image"); + if (child2) + tex.Draw(imageSize); } private void SaveAsCombo() { - var (text, desc) = SaveAsStrings[ _currentSaveAs ]; - ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); - using var combo = ImRaii.Combo( "##format", text ); - ImGuiUtil.HoverTooltip( desc ); - if( !combo ) - { + var (text, desc) = SaveAsStrings[_currentSaveAs]; + ImGui.SetNextItemWidth(-ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X); + using var combo = ImRaii.Combo("##format", text); + ImGuiUtil.HoverTooltip(desc); + if (!combo) return; - } - foreach( var ((newText, newDesc), idx) in SaveAsStrings.WithIndex() ) + foreach (var ((newText, newDesc), idx) in SaveAsStrings.WithIndex()) { - if( ImGui.Selectable( newText, idx == _currentSaveAs ) ) - { + if (ImGui.Selectable(newText, idx == _currentSaveAs)) _currentSaveAs = idx; - } - ImGuiUtil.HoverTooltip( newDesc ); + ImGuiUtil.HoverTooltip(newDesc); } } private void MipMapInput() { - ImGui.Checkbox( "##mipMaps", ref _addMipMaps ); + ImGui.Checkbox("##mipMaps", ref _addMipMaps); ImGuiUtil.HoverTooltip( - "Add the appropriate number of MipMaps to the file." ); + "Add the appropriate number of MipMaps to the file."); } - private void DrawOutputChild( Vector2 size, Vector2 imageSize ) + private void DrawOutputChild(Vector2 size, Vector2 imageSize) { - using var child = ImRaii.Child( "Output", size, true ); - if( !child ) - { + using var child = ImRaii.Child("Output", size, true); + if (!child) return; - } - if( _center.IsLoaded ) + if (_center.IsLoaded) { SaveAsCombo(); ImGui.SameLine(); MipMapInput(); - if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) + if (ImGui.Button("Save as TEX", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); - _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => { - if( a ) - { - _center.SaveAsTex( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + }, _mod!.ModPath.FullName, false); } - if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) + if (ImGui.Button("Save as DDS", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => { - if( a ) - { - _center.SaveAsDds( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + }, _mod!.ModPath.FullName, false); } ImGui.NewLine(); - if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) + if (ImGui.Button("Save as PNG", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => { - if( a ) - { - _center.SaveAsPng( b ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsPng(b); + }, _mod!.ModPath.FullName, false); } ImGui.NewLine(); } - if( _center.SaveException != null ) + if (_center.SaveException != null) { - ImGui.TextUnformatted( "Could not save file:" ); - using var color = ImRaii.PushColor( ImGuiCol.Text, 0xFF0000FF ); - ImGuiUtil.TextWrapped( _center.SaveException.ToString() ); + ImGui.TextUnformatted("Could not save file:"); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); + ImGuiUtil.TextWrapped(_center.SaveException.ToString()); } - using var child2 = ImRaii.Child( "image" ); - if( child2 ) - { - _center.Draw( imageSize ); - } + using var child2 = ImRaii.Child("image"); + if (child2) + _center.Draw(imageSize); } private Vector2 GetChildWidth() { var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetTextLineHeight(); - if( _overlayCollapsed ) + if (_overlayCollapsed) { var width = windowWidth - ImGui.GetStyle().FramePadding.X * 3; - return new Vector2( width / 2, -1 ); + return new Vector2(width / 2, -1); } - return new Vector2( ( windowWidth - ImGui.GetStyle().FramePadding.X * 5 ) / 3, -1 ); + return new Vector2((windowWidth - ImGui.GetStyle().FramePadding.X * 5) / 3, -1); } private void DrawTextureTab() { - _dialogManager.Draw(); - - using var tab = ImRaii.TabItem( "Texture Import/Export" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Texture Import/Export"); + if (!tab) return; - } try { var childWidth = GetChildWidth(); - var imageSize = new Vector2( childWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); - DrawInputChild( "Input Texture", _left, childWidth, imageSize ); + var imageSize = new Vector2(childWidth.X - ImGui.GetStyle().FramePadding.X * 2); + DrawInputChild("Input Texture", _left, childWidth, imageSize); ImGui.SameLine(); - DrawOutputChild( childWidth, imageSize ); - if( !_overlayCollapsed ) + DrawOutputChild(childWidth, imageSize); + if (!_overlayCollapsed) { ImGui.SameLine(); - DrawInputChild( "Overlay Texture", _right, childWidth, imageSize ); + DrawInputChild("Overlay Texture", _right, childWidth, imageSize); } ImGui.SameLine(); DrawOverlayCollapseButton(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Unknown Error while drawing textures:\n{e}" ); + Penumbra.Log.Error($"Unknown Error while drawing textures:\n{e}"); } } private void DrawOverlayCollapseButton() { var (label, tooltip) = _overlayCollapsed - ? ( ">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture." ) - : ( "<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any." ); - if( ImGui.Button( label, new Vector2( ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y ) ) ) - { + ? (">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture.") + : ("<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any."); + if (ImGui.Button(label, new Vector2(ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y))) _overlayCollapsed = !_overlayCollapsed; - } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 7c4946a0..d6ad92db 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -477,21 +477,22 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } - public ModEditWindow(CommunicatorService communicator) + public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog) : base(WindowBaseLabel) { + _fileDialog = fileDialog; _swapWindow = new ItemSwapWindow(communicator); - _materialTab = new FileEditor("Materials", ".mtrl", + _materialTab = new FileEditor("Materials", ".mtrl", _fileDialog, () => _editor?.MtrlFiles ?? Array.Empty(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor("Models", ".mdl", + _modelTab = new FileEditor("Models", ".mdl", _fileDialog, () => _editor?.MdlFiles ?? Array.Empty(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); - _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", _fileDialog, () => _editor?.ShpkFiles ?? Array.Empty(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs deleted file mode 100644 index 93a3e9cd..00000000 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ /dev/null @@ -1,313 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Filesystem; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Mods; - -namespace Penumbra.UI.Classes; - -public partial class ModFileSystemSelector -{ - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public struct ModState - { - public ColorId Color; - } - - private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - - private void SetFilterTooltip() - { - FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" - + "Enter c:[string] to filter for mods changing specific items.\n" - + "Enter t:[string] to filter for mods set to specific tags.\n" - + "Enter n:[string] to filter only for mod names and no paths.\n" - + "Enter a:[string] to filter for mods by specific authors."; - } - - // Appropriately identify and set the string filter and its type. - protected override bool ChangeFilter( string filterValue ) - { - ( _modFilter, _filterType ) = filterValue.Length switch - { - 0 => ( LowerString.Empty, -1 ), - > 1 when filterValue[ 1 ] == ':' => - filterValue[ 0 ] switch - { - 'n' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), - 'N' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), - 'a' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), - 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), - 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), - 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), - 't' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), - 'T' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), - _ => ( new LowerString( filterValue ), 0 ), - }, - _ => ( new LowerString( filterValue ), 0 ), - }; - - return true; - } - - // Check the state filter for a specific pair of has/has-not flags. - // Uses count == 0 to check for has-not and count != 0 for has. - // Returns true if it should be filtered and false if not. - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) - { - return count switch - { - 0 when _stateFilter.HasFlag( hasNoFlag ) => false, - 0 => true, - _ when _stateFilter.HasFlag( hasFlag ) => false, - _ => true, - }; - } - - // The overwritten filter method also computes the state. - // Folders have default state and are filtered out on the direct string instead of the other options. - // If any filter is set, they should be hidden by default unless their children are visible, - // or they contain the path search string. - protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) - { - if( path is ModFileSystem.Folder f ) - { - state = default; - return ModFilterExtensions.UnfilteredStateMods != _stateFilter - || FilterValue.Length > 0 && !f.FullName().Contains( FilterValue, IgnoreCase ); - } - - return ApplyFiltersAndState( ( ModFileSystem.Leaf )path, out state ); - } - - // Apply the string filters. - private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod ) - { - return _filterType switch - { - -1 => false, - 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), - 1 => !mod.Name.Contains( _modFilter ), - 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower ), - 4 => !mod.AllTagsLower.Contains( _modFilter.Lower ), - _ => false, // Should never happen - }; - } - - // Only get the text color for a mod if no filters are set. - private static ColorId GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) - { - if( Penumbra.ModManager.NewMods.Contains( mod ) ) - { - return ColorId.NewMod; - } - - if( settings == null ) - { - return ColorId.UndefinedMod; - } - - if( !settings.Enabled ) - { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod; - } - - var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); - if( conflicts.Count == 0 ) - { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod; - } - - return conflicts.Any( c => !c.Solved ) - ? ColorId.ConflictingMod - : ColorId.HandledConflictMod; - } - - private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) - { - var isNew = Penumbra.ModManager.NewMods.Contains( mod ); - // Handle mod details. - if( CheckFlags( mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles ) - || CheckFlags( mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - || CheckFlags( mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - || CheckFlags( mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) - || CheckFlags( isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew ) ) - { - return true; - } - - // Handle Favoritism - if( !_stateFilter.HasFlag( ModFilter.Favorite ) && mod.Favorite - || !_stateFilter.HasFlag( ModFilter.NotFavorite ) && !mod.Favorite ) - { - return true; - } - - // Handle Inheritance - if( collection == Penumbra.CollectionManager.Current ) - { - if( !_stateFilter.HasFlag( ModFilter.Uninherited ) ) - { - return true; - } - } - else - { - state.Color = ColorId.InheritedMod; - if( !_stateFilter.HasFlag( ModFilter.Inherited ) ) - { - return true; - } - } - - // Handle settings. - if( settings == null ) - { - state.Color = ColorId.UndefinedMod; - if( !_stateFilter.HasFlag( ModFilter.Undefined ) - || !_stateFilter.HasFlag( ModFilter.Disabled ) - || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - else if( !settings.Enabled ) - { - state.Color = collection == Penumbra.CollectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod; - if( !_stateFilter.HasFlag( ModFilter.Disabled ) - || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - else - { - if( !_stateFilter.HasFlag( ModFilter.Enabled ) ) - { - return true; - } - - // Conflicts can only be relevant if the mod is enabled. - var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); - if( conflicts.Count > 0 ) - { - if( conflicts.Any( c => !c.Solved ) ) - { - if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return true; - } - - state.Color = ColorId.ConflictingMod; - } - else - { - if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return true; - } - - state.Color = ColorId.HandledConflictMod; - } - } - else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - - // isNew color takes precedence before other colors. - if( isNew ) - { - state.Color = ColorId.NewMod; - } - - return false; - } - - // Combined wrapper for handling all filters and setting state. - private bool ApplyFiltersAndState( ModFileSystem.Leaf leaf, out ModState state ) - { - state = new ModState { Color = ColorId.EnabledMod }; - var mod = leaf.Value; - var (settings, collection) = Penumbra.CollectionManager.Current[ mod.Index ]; - - if( ApplyStringFilters( leaf, mod ) ) - { - return true; - } - - if( _stateFilter != ModFilterExtensions.UnfilteredStateMods ) - { - return CheckStateFilters( mod, settings, collection, ref state ); - } - - state.Color = GetTextColor( mod, settings, collection ); - return false; - } - - private void DrawFilterCombo( ref bool everything ) - { - using var combo = ImRaii.Combo( "##filterCombo", string.Empty, - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); - if( combo ) - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemSpacing with { Y = 3 * ImGuiHelpers.GlobalScale } ); - var flags = ( int )_stateFilter; - - - if( ImGui.Checkbox( "Everything", ref everything ) ) - { - _stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0; - SetFilterDirty(); - } - - ImGui.Dummy( new Vector2( 0, 5 * ImGuiHelpers.GlobalScale ) ); - foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) - { - if( ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ) ) - { - _stateFilter = ( ModFilter )flags; - SetFilterDirty(); - } - } - } - } - - // Add the state filter combo-button to the right of the filter box. - protected override float CustomFilters( float width ) - { - var pos = ImGui.GetCursorPos(); - var remainingWidth = width - ImGui.GetFrameHeight(); - var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); - - var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; - - ImGui.SetCursorPos( comboPos ); - // Draw combo button - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.FilterActive, !everything ); - DrawFilterCombo( ref everything ); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModFilters ); - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - _stateFilter = ModFilterExtensions.UnfilteredStateMods; - SetFilterDirty(); - } - - ImGuiUtil.HoverTooltip( "Filter mods for their activation status.\nRight-Click to clear all filters." ); - ImGui.SetCursorPos( pos ); - return remainingWidth; - } -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs deleted file mode 100644 index c9ba7b13..00000000 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ /dev/null @@ -1,478 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using ImGuiNET; -using OtterGui; -using OtterGui.Filesystem; -using OtterGui.FileSystem.Selector; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Import; -using Penumbra.Mods; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Numerics; -using Penumbra.Api.Enums; -using Penumbra.Services; - -namespace Penumbra.UI.Classes; - -public sealed partial class ModFileSystemSelector : FileSystemSelector -{ - private readonly CommunicatorService _communicator; - private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); - private TexToolsImporter? _import; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - - public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem) - : base(fileSystem, DalamudServices.KeyState) - { - _communicator = communicator; - SubscribeRightClickFolder(EnableDescendants, 10); - SubscribeRightClickFolder(DisableDescendants, 10); - SubscribeRightClickFolder(InheritDescendants, 15); - SubscribeRightClickFolder(OwnDescendants, 15); - SubscribeRightClickFolder(SetDefaultImportFolder, 100); - SubscribeRightClickLeaf(ToggleLeafFavorite, 0); - SubscribeRightClickMain(ClearDefaultImportFolder, 100); - AddButton(AddNewModButton, 0); - AddButton(AddImportModButton, 1); - AddButton(AddHelpButton, 2); - AddButton(DeleteModButton, 1000); - SetFilterTooltip(); - - SelectionChanged += OnSelectionChange; - _communicator.CollectionChange.Event += OnCollectionChange; - Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; - Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; - Penumbra.ModManager.ModDataChanged += OnModDataChange; - Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; - Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, ""); - } - - public override void Dispose() - { - base.Dispose(); - Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; - Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; - Penumbra.ModManager.ModDataChanged -= OnModDataChange; - Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; - Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; - _communicator.CollectionChange.Event -= OnCollectionChange; - _import?.Dispose(); - _import = null; - } - - public new ModFileSystem.Leaf? SelectedLeaf - => base.SelectedLeaf; - - // Customization points. - public override ISortMode SortMode - => Penumbra.Config.SortMode; - - protected override uint ExpandedFolderColor - => ColorId.FolderExpanded.Value(); - - protected override uint CollapsedFolderColor - => ColorId.FolderCollapsed.Value(); - - protected override uint FolderLineColor - => ColorId.FolderLine.Value(); - - protected override bool FoldersDefaultOpen - => Penumbra.Config.OpenFoldersByDefault; - - protected override void DrawPopups() - { - _fileManager.Draw(); - DrawHelpPopup(); - DrawInfoPopup(); - - if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) - try - { - var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); - Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); - Mod.Creator.CreateDefaultFiles(newDir); - Penumbra.ModManager.AddMod(newDir); - _newModName = string.Empty; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); - } - - while (_modsToAdd.TryDequeue(out var dir)) - { - Penumbra.ModManager.AddMod(dir); - var mod = Penumbra.ModManager.LastOrDefault(); - if (mod != null) - { - MoveModToDefaultDirectory(mod); - SelectByValue(mod); - } - } - } - - protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) - { - var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) - .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); - using var id = ImRaii.PushId(leaf.Value.Index); - ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); - } - - - // Add custom context menu items. - private static void EnableDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Enable Descendants")) - SetDescendants(folder, true); - } - - private static void DisableDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Disable Descendants")) - SetDescendants(folder, false); - } - - private static void InheritDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Inherit Descendants")) - SetDescendants(folder, true, true); - } - - private static void OwnDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Stop Inheriting Descendants")) - SetDescendants(folder, false, true); - } - - private static void ToggleLeafFavorite(FileSystem.Leaf mod) - { - if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) - Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); - } - - private static void SetDefaultImportFolder(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Set As Default Import Folder")) - { - var newName = folder.FullName(); - if (newName != Penumbra.Config.DefaultImportFolder) - { - Penumbra.Config.DefaultImportFolder = newName; - Penumbra.Config.Save(); - } - } - } - - private static void ClearDefaultImportFolder() - { - if (ImGui.MenuItem("Clear Default Import Folder") && Penumbra.Config.DefaultImportFolder.Length > 0) - { - Penumbra.Config.DefaultImportFolder = string.Empty; - Penumbra.Config.Save(); - } - } - - - // Add custom buttons. - private string _newModName = string.Empty; - - private static void AddNewModButton(Vector2 size) - { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", - !Penumbra.ModManager.Valid, true)) - ImGui.OpenPopup("Create New Mod"); - } - - // Add an import mods button that opens a file selector. - // Only set the initial directory once. - private bool _hasSetFolder; - - private void AddImportModButton(Vector2 size) - { - var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); - ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport); - if (!button) - return; - - var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null - : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; - _hasSetFolder = true; - - _fileManager.OpenFileDialog("Import Mod Pack", - "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => - { - if (s) - { - _import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), - AddNewMod); - ImGui.OpenPopup("Import Status"); - } - }, 0, modPath); - } - - // Draw the progress information for import. - private void DrawInfoPopup() - { - var display = ImGui.GetIO().DisplaySize; - var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 8; - var size = new Vector2(width * 2, height); - ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); - ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); - if (_import == null || !popup.Success) - return; - - using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) - { - if (child) - _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); - } - - if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) - || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) - { - _import?.Dispose(); - _import = null; - ImGui.CloseCurrentPopup(); - } - } - - // Mods need to be added thread-safely outside of iteration. - private readonly ConcurrentQueue _modsToAdd = new(); - - // Clean up invalid directory if necessary. - // Add successfully extracted mods. - private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) - { - if (error != null) - { - if (dir != null && Directory.Exists(dir.FullName)) - try - { - Directory.Delete(dir.FullName, true); - } - catch (Exception e) - { - Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); - } - - if (error is not OperationCanceledException) - Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); - } - else if (dir != null) - { - _modsToAdd.Enqueue(dir); - } - } - - private void DeleteModButton(Vector2 size) - { - var keys = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = SelectedLeaf == null - ? "No mod selected." - : "Delete the currently selected mod entirely from your drive.\n" - + "This can not be undone."; - if (!keys) - tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; - - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) - && Selected != null) - Penumbra.ModManager.DeleteMod(Selected.Index); - } - - private static void AddHelpButton(Vector2 size) - { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) - ImGui.OpenPopup("ExtendedHelp"); - - ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp); - } - - // Helpers. - private static void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) - { - var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => - { - // Any mod handled here should not stay new. - Penumbra.ModManager.NewMods.Remove(l.Value); - return l.Value; - }); - - if (inherit) - Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled); - else - Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled); - } - - // Automatic cache update functions. - private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) - { - // TODO: maybe make more efficient - SetFilterDirty(); - if (modIdx == Selected?.Index) - OnSelectionChange(Selected, Selected, default); - } - - private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) - { - switch (type) - { - case ModDataChangeType.Name: - case ModDataChangeType.Author: - case ModDataChangeType.ModTags: - case ModDataChangeType.LocalTags: - case ModDataChangeType.Favorite: - SetFilterDirty(); - break; - } - } - - private void OnInheritanceChange(bool _) - { - SetFilterDirty(); - OnSelectionChange(Selected, Selected, default); - } - - private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) - { - if (collectionType != CollectionType.Current || oldCollection == newCollection) - return; - - if (oldCollection != null) - { - oldCollection.ModSettingChanged -= OnSettingChange; - oldCollection.InheritanceChanged -= OnInheritanceChange; - } - - if (newCollection != null) - { - newCollection.ModSettingChanged += OnSettingChange; - newCollection.InheritanceChanged += OnInheritanceChange; - } - - SetFilterDirty(); - OnSelectionChange(Selected, Selected, default); - } - - private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) - { - if (newSelection == null) - { - SelectedSettings = ModSettings.Empty; - SelectedSettingCollection = ModCollection.Empty; - } - else - { - (var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index]; - SelectedSettings = settings ?? ModSettings.Empty; - } - } - - // Keep selections across rediscoveries if possible. - private string _lastSelectedDirectory = string.Empty; - - private void StoreCurrentSelection() - { - _lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty; - ClearSelection(); - } - - private void RestoreLastSelection() - { - if (_lastSelectedDirectory.Length > 0) - { - var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) - .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); - Select(leaf); - _lastSelectedDirectory = string.Empty; - } - } - - // If a default import folder is setup, try to move the given mod in there. - // If the folder does not exist, create it if possible. - private void MoveModToDefaultDirectory(Mod mod) - { - if (Penumbra.Config.DefaultImportFolder.Length == 0) - return; - - try - { - var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) - .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); - if (leaf == null) - throw new Exception("Mod was not found at root."); - - var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); - FileSystem.Move(leaf, folder); - } - catch (Exception e) - { - Penumbra.Log.Warning( - $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}"); - } - } - - private static void DrawHelpPopup() - { - ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => - { - ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Management"); - ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); - ImGui.BulletText( - "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); - indent.Pop(1); - ImGui.BulletText("You can also create empty mod folders and delete mods."); - ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); - ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Selector"); - ImGui.BulletText("Select a mod to obtain more information or change settings."); - ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); - indent.Push(); - ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); - ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(), - "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); - ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); - ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority."); - ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); - ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); - indent.Pop(1); - ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); - indent.Push(); - ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); - indent.Pop(1); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); - ImGui.BulletText("Right-clicking a folder opens a context menu."); - ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); - ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); - indent.Push(); - ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); - ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); - ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); - indent.Pop(1); - ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); - }); - } -} diff --git a/Penumbra/UI/Classes/ModFilter.cs b/Penumbra/UI/Classes/ModFilter.cs deleted file mode 100644 index 3c68f15c..00000000 --- a/Penumbra/UI/Classes/ModFilter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace Penumbra.UI.Classes; - -[Flags] -public enum ModFilter -{ - Enabled = 1 << 0, - Disabled = 1 << 1, - Favorite = 1 << 2, - NotFavorite = 1 << 3, - NoConflict = 1 << 4, - SolvedConflict = 1 << 5, - UnsolvedConflict = 1 << 6, - HasNoMetaManipulations = 1 << 7, - HasMetaManipulations = 1 << 8, - HasNoFileSwaps = 1 << 9, - HasFileSwaps = 1 << 10, - HasConfig = 1 << 11, - HasNoConfig = 1 << 12, - HasNoFiles = 1 << 13, - HasFiles = 1 << 14, - IsNew = 1 << 15, - NotNew = 1 << 16, - Inherited = 1 << 17, - Uninherited = 1 << 18, - Undefined = 1 << 19, -}; - -public static class ModFilterExtensions -{ - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 20 ) - 1 ); - - public static string ToName( this ModFilter filter ) - => filter switch - { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.Favorite => "Favorite", - ModFilter.NotFavorite => "No Favorite", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", - ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - ModFilter.Inherited => "Inherited Configuration", - ModFilter.Uninherited => "Own Configuration", - ModFilter.Undefined => "Not Configured", - _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), - }; -} \ No newline at end of file diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs new file mode 100644 index 00000000..bd9d3e35 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using ImGuiNET; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.GameData.Actors; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionSelector : FilterComboCache +{ + private readonly ModCollection.Manager _collectionManager; + + public CollectionSelector(ModCollection.Manager manager, Func> items) + : base(items) + => _collectionManager = manager; + + public void Draw(string label, float width, int individualIdx) + { + var (_, collection) = _collectionManager.Individuals[individualIdx]; + if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) + _collectionManager.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); + } + + public void Draw(string label, float width, CollectionType type) + { + var current = _collectionManager.ByType(type, ActorIdentifier.Invalid); + if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) + _collectionManager.SetCollection(CurrentSelection, type); + } + + protected override string ToString(ModCollection obj) + => obj.Name; +} diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs new file mode 100644 index 00000000..a29ebb25 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Services; + +namespace Penumbra.UI.CollectionTab; + +public class IndividualCollectionUi +{ + private readonly ActorService _actorService; + private readonly ModCollection.Manager _collectionManager; + private readonly CollectionSelector _withEmpty; + + public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty) + { + _actorService = actors; + _collectionManager = collectionManager; + _withEmpty = withEmpty; + if (_actorService.Valid) + SetupCombos(); + else + _actorService.FinishedCreation += SetupCombos; + } + + /// Draw all individual assignments as well as the options to create a new one. + public void Draw() + { + if (!_ready) + return; + + using var _ = ImRaii.Group(); + using var mainId = ImRaii.PushId("Individual"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Individual {TutorialService.ConditionalIndividual}s"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" + + $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n" + + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections."); + ImGui.Separator(); + for (var i = 0; i < _collectionManager.Individuals.Count; ++i) + { + DrawIndividualAssignment(i); + } + + UiHelpers.DefaultLineSpace(); + DrawNewIndividualCollection(); + } + + public void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) + { + if (type == CollectionType.Individual) + UpdateIdentifiers(); + } + + // Input Selections. + private string _newCharacterName = string.Empty; + private ObjectKind _newKind = ObjectKind.BattleNpc; + + private WorldCombo _worldCombo = null!; + private NpcCombo _mountCombo = null!; + private NpcCombo _companionCombo = null!; + private NpcCombo _ornamentCombo = null!; + private NpcCombo _bnpcCombo = null!; + private NpcCombo _enpcCombo = null!; + + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + + private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty(); + private string _newPlayerTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty(); + private string _newRetainerTooltip = NewRetainerTooltipEmpty; + private ActorIdentifier[] _newNpcIdentifiers = Array.Empty(); + private string _newNpcTooltip = NewNpcTooltipEmpty; + private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty(); + private string _newOwnedTooltip = NewPlayerTooltipEmpty; + + private bool _ready; + + /// Create combos when ready. + private void SetupCombos() + { + _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds); + _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts); + _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions); + _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments); + _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs); + _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs); + _ready = true; + _actorService.FinishedCreation -= SetupCombos; + } + + + private static readonly IReadOnlyList ObjectKinds = new[] + { + ObjectKind.BattleNpc, + ObjectKind.EventNpc, + ObjectKind.Companion, + ObjectKind.MountType, + ObjectKind.Ornament, + }; + + /// Draw the Object Kind Selector. + private bool DrawNewObjectKindOptions(float width) + { + ImGui.SetNextItemWidth(width); + using var combo = ImRaii.Combo("##newKind", _newKind.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var kind in ObjectKinds) + { + if (!ImGui.Selectable(kind.ToName(), _newKind == kind)) + continue; + + _newKind = kind; + ret = true; + } + + return ret; + } + + private int _individualDragDropIdx = -1; + + /// Draw a single individual assignment. + private void DrawIndividualAssignment(int idx) + { + var (name, _) = _collectionManager.Individuals[idx]; + using var id = ImRaii.PushId(idx); + _withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, + false, true)) + _collectionManager.RemoveIndividualCollection(idx); + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(name); + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("Individual", nint.Zero, 0); + _individualDragDropIdx = idx; + } + } + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping("Individual")) + return; + + if (_individualDragDropIdx >= 0) + _collectionManager.MoveIndividualCollection(_individualDragDropIdx, idx); + + _individualDragDropIdx = -1; + } + + private bool DrawNewPlayerCollection(Vector2 buttonWidth, float width) + { + var change = _worldCombo.Draw(width); + ImGui.SameLine(); + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width); + change |= ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip, + _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0)) + { + _collectionManager.CreateIndividualCollection(_newPlayerIdentifiers); + change = true; + } + + return change; + } + + private bool DrawNewNpcCollection(NpcCombo combo, Vector2 buttonWidth, float width) + { + var comboWidth = UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; + var change = DrawNewObjectKindOptions(width); + ImGui.SameLine(); + change |= combo.Draw(comboWidth); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip, + _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0)) + { + _collectionManager.CreateIndividualCollection(_newNpcIdentifiers); + change = true; + } + + return change; + } + + private bool DrawNewOwnedCollection(Vector2 buttonWidth) + { + if (!ImGuiUtil.DrawDisabledButton("Assign Owned NPC", buttonWidth, _newOwnedTooltip, + _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0)) + return false; + + _collectionManager.CreateIndividualCollection(_newOwnedIdentifiers); + return true; + + } + + private bool DrawNewRetainerCollection(Vector2 buttonWidth) + { + if (!ImGuiUtil.DrawDisabledButton("Assign Bell Retainer", buttonWidth, _newRetainerTooltip, + _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0)) + return false; + + _collectionManager.CreateIndividualCollection(_newRetainerIdentifiers); + return true; + + } + + private NpcCombo GetNpcCombo(ObjectKind kind) + => kind switch + { + ObjectKind.BattleNpc => _bnpcCombo, + ObjectKind.EventNpc => _enpcCombo, + ObjectKind.MountType => _mountCombo, + ObjectKind.Companion => _companionCombo, + ObjectKind.Ornament => _ornamentCombo, + _ => throw new NotImplementedException(), + }; + + private void DrawNewIndividualCollection() + { + var width = (UiHelpers.InputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X) / 3; + + var buttonWidth1 = new Vector2(90 * UiHelpers.Scale, 0); + var buttonWidth2 = new Vector2(120 * UiHelpers.Scale, 0); + + var assignWidth = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var change = DrawNewCurrentPlayerCollection(assignWidth); + ImGui.SameLine(); + change |= DrawNewTargetCollection(assignWidth); + + change |= DrawNewPlayerCollection(buttonWidth1, width); + ImGui.SameLine(); + change |= DrawNewRetainerCollection(buttonWidth2); + + var combo = GetNpcCombo(_newKind); + change |= DrawNewNpcCollection(combo, buttonWidth1, width); + ImGui.SameLine(); + change |= DrawNewOwnedCollection(buttonWidth2); + + if (change) + UpdateIdentifiers(); + } + + private bool DrawNewCurrentPlayerCollection(Vector2 width) + { + var player = _actorService.AwaitedService.GetCurrentPlayer(); + var result = _collectionManager.Individuals.CanAdd(player); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No logged-in character detected.", + _ => string.Empty, + }; + + + if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid)) + return false; + + _collectionManager.CreateIndividualCollection(player); + return true; + + } + + private bool DrawNewTargetCollection(Vector2 width) + { + var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true); + var result = _collectionManager.Individuals.CanAdd(target); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No valid character in target detected.", + _ => string.Empty, + }; + if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid)) + { + _collectionManager.CreateIndividualCollection(_collectionManager.Individuals.GetGroup(target)); + return true; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" + + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" + + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); + + return false; + } + + private void UpdateIdentifiers() + { + var combo = GetNpcCombo(_newKind); + _newPlayerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, + _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty(), out _newPlayerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newRetainerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, + Array.Empty(), out _newRetainerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + if (combo.CurrentSelection.Ids != null) + { + _newNpcTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch + { + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newOwnedTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, + _worldCombo.CurrentSelection.Key, _newKind, + combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + } + else + { + _newNpcTooltip = NewNpcTooltipEmpty; + _newOwnedTooltip = NewNpcTooltipEmpty; + _newNpcIdentifiers = Array.Empty(); + _newOwnedIdentifiers = Array.Empty(); + } + } +} diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs new file mode 100644 index 00000000..dc3bc52f --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.CollectionTab; + +public class InheritanceUi +{ + private const int InheritedCollectionHeight = 9; + private const string InheritanceDragDropLabel = "##InheritanceMove"; + + private readonly ModCollection.Manager _collectionManager; + + public InheritanceUi(ModCollection.Manager collectionManager) + => _collectionManager = collectionManager; + + /// Draw the whole inheritance block. + public void Draw() + { + using var group = ImRaii.Group(); + using var id = ImRaii.PushId("##Inheritance"); + ImGui.TextUnformatted($"The {TutorialService.SelectedCollection} inherits from:"); + DrawCurrentCollectionInheritance(); + DrawInheritanceTrashButton(); + DrawNewInheritanceSelection(); + DelayedActions(); + } + + // Keep for reuse. + private readonly HashSet _seenInheritedCollections = new(32); + + // Execute changes only outside of loops. + private ModCollection? _newInheritance; + private ModCollection? _movedInheritance; + private (int, int)? _inheritanceAction; + private ModCollection? _newCurrentCollection; + + /// + /// If an inherited collection is expanded, + /// draw all its flattened, distinct children in order with a tree-line. + /// + private void DrawInheritedChildren(ModCollection collection) + { + using var id = ImRaii.PushId(collection.Index); + using var indent = ImRaii.PushIndent(); + + // Get start point for the lines (top of the selector). + // Tree line stuff. + var lineStart = ImGui.GetCursorScreenPos(); + var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; + var drawList = ImGui.GetWindowDrawList(); + var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale); + lineStart.X += offsetX; + lineStart.Y -= 2 * UiHelpers.Scale; + var lineEnd = lineStart; + + // Skip the collection itself. + foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) + { + // Draw the child, already seen collections are colored as conflicts. + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + _seenInheritedCollections.Contains(inheritance)); + _seenInheritedCollections.Add(inheritance); + + ImRaii.TreeNode(inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); + DrawInheritanceTreeClicks(inheritance, false); + + // Tree line stuff. + if (minRect.X == 0) + continue; + + // Draw the notch and increase the line length. + var midPoint = (minRect.Y + maxRect.Y) / 2f - 1f; + drawList.AddLine(new Vector2(lineStart.X, midPoint), new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, + UiHelpers.Scale); + lineEnd.Y = midPoint; + } + + // Finally, draw the folder line. + drawList.AddLine(lineStart, lineEnd, Colors.MetaInfoText, UiHelpers.Scale); + } + + /// Draw a single primary inherited collection. + private void DrawInheritance(ModCollection collection) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + _seenInheritedCollections.Contains(collection)); + _seenInheritedCollections.Add(collection); + using var tree = ImRaii.TreeNode(collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen); + color.Pop(); + DrawInheritanceTreeClicks(collection, true); + DrawInheritanceDropSource(collection); + DrawInheritanceDropTarget(collection); + + if (tree) + DrawInheritedChildren(collection); + else + // We still want to keep track of conflicts. + _seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance()); + } + + /// Draw the list box containing the current inheritance information. + private void DrawCurrentCollectionInheritance() + { + using var list = ImRaii.ListBox("##inheritanceList", + new Vector2(UiHelpers.InputTextMinusButton, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight)); + if (!list) + return; + + _seenInheritedCollections.Clear(); + _seenInheritedCollections.Add(_collectionManager.Current); + foreach (var collection in _collectionManager.Current.Inheritance.ToList()) + DrawInheritance(collection); + } + + /// Draw a drag and drop button to delete. + private void DrawInheritanceTrashButton() + { + ImGui.SameLine(); + var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight }; + var buttonColor = ImGui.GetColorU32(ImGuiCol.Button); + // Prevent hovering from highlighting the button. + using var color = ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor) + .Push(ImGuiCol.ButtonHovered, buttonColor); + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, + "Drag primary inheritance here to remove it from the list.", false, true); + + using var target = ImRaii.DragDropTarget(); + if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) + _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(_movedInheritance!), -1); + } + + /// + /// Set the current collection, or delete or move an inheritance if the action was triggered during iteration. + /// Can not be done during iteration to keep collections unchanged. + /// + private void DelayedActions() + { + if (_newCurrentCollection != null) + { + _collectionManager.SetCollection(_newCurrentCollection, CollectionType.Current); + _newCurrentCollection = null; + } + + if (_inheritanceAction == null) + return; + + if (_inheritanceAction.Value.Item1 >= 0) + { + if (_inheritanceAction.Value.Item2 == -1) + _collectionManager.Current.RemoveInheritance(_inheritanceAction.Value.Item1); + else + _collectionManager.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + } + + _inheritanceAction = null; + } + + /// + /// Draw the selector to add new inheritances. + /// The add button is only available if the selected collection can actually be added. + /// + private void DrawNewInheritanceSelection() + { + DrawNewInheritanceCombo(); + ImGui.SameLine(); + var inheritance = _collectionManager.Current.CheckValidInheritance(_newInheritance); + var tt = inheritance switch + { + ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", + ModCollection.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", + ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", + ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", + ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", + _ => string.Empty, + }; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, + inheritance != ModCollection.ValidInheritance.Valid, true) + && _collectionManager.Current.AddInheritance(_newInheritance!, true)) + _newInheritance = null; + + if (inheritance != ModCollection.ValidInheritance.Valid) + _newInheritance = null; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), UiHelpers.IconButtonSize, "What is Inheritance?", + false, true)) + ImGui.OpenPopup("InheritanceHelp"); + + ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 21 * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.NewLine(); + ImGui.TextWrapped( + "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod."); + ImGui.NewLine(); + ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); + ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); + ImGui.BulletText( + "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); + ImGui.BulletText( + "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); + ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); + ImGui.BulletText( + "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); + ImGui.NewLine(); + ImGui.TextUnformatted("Example"); + ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); + ImGui.BulletText( + "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); + ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); + ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); + ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); + ImGui.BulletText( + "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); + }); + } + + /// + /// Draw the combo to select new potential inheritances. + /// Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. + /// + private void DrawNewInheritanceCombo() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); + _newInheritance ??= _collectionManager.FirstOrDefault(c + => c != _collectionManager.Current && !_collectionManager.Current.Inheritance.Contains(c)) + ?? ModCollection.Empty; + using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); + if (!combo) + return; + + foreach (var collection in _collectionManager + .Where(c => _collectionManager.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) + .OrderBy(c => c.Name)) + { + if (ImGui.Selectable(collection.Name, _newInheritance == collection)) + _newInheritance = collection; + } + } + + /// + /// Move an inherited collection when dropped onto another. + /// Move is delayed due to collection changes. + /// + private void DrawInheritanceDropTarget(ModCollection collection) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping(InheritanceDragDropLabel)) + return; + + if (_movedInheritance != null) + { + var idx1 = _collectionManager.Current.Inheritance.IndexOf(_movedInheritance); + var idx2 = _collectionManager.Current.Inheritance.IndexOf(collection); + if (idx1 >= 0 && idx2 >= 0) + _inheritanceAction = (idx1, idx2); + } + + _movedInheritance = null; + } + + /// Move an inherited collection. + private void DrawInheritanceDropSource(ModCollection collection) + { + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0); + _movedInheritance = collection; + ImGui.TextUnformatted($"Moving {_movedInheritance?.Name ?? "Unknown"}..."); + } + + /// + /// Ctrl + Right-Click -> Switch current collection to this (for all). + /// Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete). + /// Deletion is delayed due to collection changes. + /// + private void DrawInheritanceTreeClicks(ModCollection collection, bool withDelete) + { + if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + if (withDelete && ImGui.GetIO().KeyShift) + _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(collection), -1); + else + _newCurrentCollection = collection; + } + + ImGuiUtil.HoverTooltip($"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one." + + (withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty)); + } +} diff --git a/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs b/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs new file mode 100644 index 00000000..9e0ffe14 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Penumbra.UI.CollectionTab; + +public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)> +{ + private readonly string _label; + + public NpcCombo(string label, IReadOnlyDictionary names) + : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key).ToList()) + => _label = label; + + protected override string ToString((string Name, uint[] Ids) obj) + => obj.Name; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (name, ids) = Items[globalIdx]; + var ret = ImGui.Selectable(name, selected); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', ids.Select(i => i.ToString()))); + + return ret; + } + + public bool Draw(float width) + => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); +} diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs new file mode 100644 index 00000000..59461f42 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -0,0 +1,42 @@ +using ImGuiNET; +using OtterGui.Classes; +using OtterGui.Widgets; +using Penumbra.Collections; + +namespace Penumbra.UI.CollectionTab; + +public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> +{ + private readonly ModCollection.Manager _collectionManager; + + public (CollectionType, string, string)? CurrentType + => CollectionTypeExtensions.Special[CurrentIdx]; + + public int CurrentIdx; + private readonly float _unscaledWidth; + private readonly string _label; + + public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth) + : base(CollectionTypeExtensions.Special, false) + { + _collectionManager = collectionManager; + _label = label; + _unscaledWidth = unscaledWidth; + } + + public void Draw() + { + var preview = CurrentIdx >= 0 ? Items[CurrentIdx].Item2 : string.Empty; + Draw(_label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * UiHelpers.Scale, + ImGui.GetTextLineHeightWithSpacing()); + } + + protected override string ToString((CollectionType, string, string) obj) + => obj.Item2; + + protected override bool IsVisible(int globalIdx, LowerString filter) + { + var obj = Items[globalIdx]; + return filter.IsContained(obj.Item2) && _collectionManager.ByType(obj.Item1) == null; + } +} \ No newline at end of file diff --git a/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs b/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs new file mode 100644 index 00000000..5441dbaa --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Penumbra.UI.CollectionTab; + +public sealed class WorldCombo : FilterComboCache> +{ + private static readonly KeyValuePair AllWorldPair = new(ushort.MaxValue, "Any World"); + + public WorldCombo(IReadOnlyDictionary worlds) + : base(worlds.OrderBy(kvp => kvp.Value).Prepend(AllWorldPair)) + { + CurrentSelection = AllWorldPair; + CurrentSelectionIdx = 0; + } + + protected override string ToString(KeyValuePair obj) + => obj.Value; + + public bool Draw(float width) + => Draw("##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); +} diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs deleted file mode 100644 index c55fd6d4..00000000 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Draw a simple clipped table containing all changed items. - public class ChangedItemsTab : ITab - { - private readonly ConfigWindow _config; - - public ChangedItemsTab( ConfigWindow config ) - => _config = config; - - public ReadOnlySpan Label - => "Changed Items"u8; - - private LowerString _changedItemFilter = LowerString.Empty; - private LowerString _changedItemModFilter = LowerString.Empty; - - public void DrawContent() - { - // Draw filters. - var varWidth = ImGui.GetContentRegionAvail().X - - 400 * ImGuiHelpers.GlobalScale - - ImGui.GetStyle().ItemSpacing.X; - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( varWidth ); - LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); - - using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); - if( !child ) - { - return; - } - - // Draw table of changed items. - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !list ) - { - return; - } - - const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "mods", flags, varWidth - 120 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "id", flags, 120 * ImGuiHelpers.GlobalScale ); - - var items = Penumbra.CollectionManager.Current.ChangedItems; - var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty - ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) - : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); - ImGuiClip.DrawEndDummy( rest, height ); - } - - - // Functions in here for less pollution. - private bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) - => ( _changedItemFilter.IsEmpty - || ChangedItemName( item.Key, item.Value.Item2 ) - .Contains( _changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase ) ) - && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - - private void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) - { - ImGui.TableNextColumn(); - _config.DrawChangedItem( item.Key, item.Value.Item2, false ); - ImGui.TableNextColumn(); - if( item.Value.Item1.Count > 0 ) - { - ImGui.TextUnformatted( item.Value.Item1[ 0 ].Name ); - if( item.Value.Item1.Count > 1 ) - { - ImGuiUtil.HoverTooltip( string.Join( "\n", item.Value.Item1.Skip( 1 ).Select( m => m.Name ) ) ); - } - } - - ImGui.TableNextColumn(); - if( DrawChangedItemObject( item.Value.Item2, out var text ) ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGuiUtil.RightAlign( text ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs deleted file mode 100644 index d89d554e..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using System.Linq; -using System.Numerics; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Components; -using OtterGui.Widgets; -using Penumbra.GameData.Actors; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class CollectionsTab - { - private sealed class WorldCombo : FilterComboCache< KeyValuePair< ushort, string > > - { - private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "Any World"); - - public WorldCombo( IReadOnlyDictionary< ushort, string > worlds ) - : base( worlds.OrderBy( kvp => kvp.Value ).Prepend( AllWorldPair ) ) - { - CurrentSelection = AllWorldPair; - CurrentSelectionIdx = 0; - } - - protected override string ToString( KeyValuePair< ushort, string > obj ) - => obj.Value; - - public bool Draw( float width ) - => Draw( "##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); - } - - private sealed class NpcCombo : FilterComboCache< (string Name, uint[] Ids) > - { - private readonly string _label; - - public NpcCombo( string label, IReadOnlyDictionary< uint, string > names ) - : base( () => names.GroupBy( kvp => kvp.Value ).Select( g => ( g.Key, g.Select( g => g.Key ).ToArray() ) ).OrderBy( g => g.Key ).ToList() ) - => _label = label; - - protected override string ToString( (string Name, uint[] Ids) obj ) - => obj.Name; - - protected override bool DrawSelectable( int globalIdx, bool selected ) - { - var (name, ids) = Items[ globalIdx ]; - var ret = ImGui.Selectable( name, selected ); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( '\n', ids.Select( i => i.ToString() ) ) ); - } - - return ret; - } - - public bool Draw( float width ) - => Draw( _label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); - } - - - // Input Selections. - private string _newCharacterName = string.Empty; - private ObjectKind _newKind = ObjectKind.BattleNpc; - - private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Data.Worlds); - private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Data.Mounts); - private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Data.Companions); - private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Data.Ornaments); - private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.Data.BNpcs); - private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.Data.ENpcs); - - private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; - private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; - private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; - private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; - private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; - private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; - - private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newPlayerTooltip = NewPlayerTooltipEmpty; - private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newRetainerTooltip = NewRetainerTooltipEmpty; - private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newNpcTooltip = NewNpcTooltipEmpty; - private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newOwnedTooltip = NewPlayerTooltipEmpty; - - private bool DrawNewObjectKindOptions( float width ) - { - ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newKind", _newKind.ToName() ); - if( !combo ) - { - return false; - } - - var ret = false; - foreach( var kind in new[] { ObjectKind.BattleNpc, ObjectKind.EventNpc, ObjectKind.Companion, ObjectKind.MountType, ( ObjectKind )15 } ) // TODO: CS Update - { - if( ImGui.Selectable( kind.ToName(), _newKind == kind ) ) - { - _newKind = kind; - ret = true; - } - } - - return ret; - } - - private int _individualDragDropIdx = -1; - - private void DrawIndividualAssignments() - { - using var _ = ImRaii.Group(); - using var mainId = ImRaii.PushId( "Individual" ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" - + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an Individual Collection takes effect.\n" - + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections." ); - ImGui.Separator(); - for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) - { - var (name, _) = Penumbra.CollectionManager.Individuals[ i ]; - using var id = ImRaii.PushId( i ); - CollectionsWithEmpty.Draw( "##IndividualCombo", _window._inputTextWidth.X, i ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, true ) ) - { - Penumbra.CollectionManager.RemoveIndividualCollection( i ); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( name ); - using( var source = ImRaii.DragDropSource() ) - { - if( source ) - { - ImGui.SetDragDropPayload( "Individual", IntPtr.Zero, 0 ); - _individualDragDropIdx = i; - } - } - - using( var target = ImRaii.DragDropTarget() ) - { - if( !target.Success || !ImGuiUtil.IsDropping( "Individual" ) ) - { - continue; - } - - if( _individualDragDropIdx >= 0 ) - { - Penumbra.CollectionManager.MoveIndividualCollection( _individualDragDropIdx, i ); - } - - _individualDragDropIdx = -1; - } - } - - ImGui.Dummy( _window._defaultSpace ); - DrawNewIndividualCollection(); - } - - private bool DrawNewPlayerCollection( Vector2 buttonWidth, float width ) - { - var change = _worldCombo.Draw( width ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width ); - change |= ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newPlayerIdentifiers ); - change = true; - } - - return change; - } - - private bool DrawNewNpcCollection( NpcCombo combo, Vector2 buttonWidth, float width ) - { - var comboWidth = _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; - var change = DrawNewObjectKindOptions( width ); - ImGui.SameLine(); - change |= combo.Draw( comboWidth ); - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newNpcIdentifiers ); - change = true; - } - - return change; - } - - private bool DrawNewOwnedCollection( Vector2 buttonWidth ) - { - if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newOwnedIdentifiers ); - return true; - } - - return false; - } - - private bool DrawNewRetainerCollection( Vector2 buttonWidth ) - { - if( ImGuiUtil.DrawDisabledButton( "Assign Bell Retainer", buttonWidth, _newRetainerTooltip, _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newRetainerIdentifiers ); - return true; - } - - return false; - } - - private NpcCombo GetNpcCombo( ObjectKind kind ) - => kind switch - { - ObjectKind.BattleNpc => _bnpcCombo, - ObjectKind.EventNpc => _enpcCombo, - ObjectKind.MountType => _mountCombo, - ObjectKind.Companion => _companionCombo, - ( ObjectKind )15 => _ornamentCombo, // TODO: CS update - _ => throw new NotImplementedException(), - }; - - private void DrawNewIndividualCollection() - { - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - - var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); - var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); - - var assignWidth = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); - var change = DrawNewCurrentPlayerCollection( assignWidth ); - ImGui.SameLine(); - change |= DrawNewTargetCollection( assignWidth ); - - change |= DrawNewPlayerCollection( buttonWidth1, width ); - ImGui.SameLine(); - change |= DrawNewRetainerCollection( buttonWidth2 ); - - var combo = GetNpcCombo( _newKind ); - change |= DrawNewNpcCollection( combo, buttonWidth1, width ); - ImGui.SameLine(); - change |= DrawNewOwnedCollection( buttonWidth2 ); - - if( change ) - { - UpdateIdentifiers(); - } - } - - private static bool DrawNewCurrentPlayerCollection( Vector2 width ) - { - var player = Penumbra.Actors.GetCurrentPlayer(); - var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No logged-in character detected.", - _ => string.Empty, - }; - - - if( ImGuiUtil.DrawDisabledButton( "Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( player ); - return true; - } - - return false; - } - - private static bool DrawNewTargetCollection( Vector2 width ) - { - var target = Penumbra.Actors.FromObject( DalamudServices.Targets.Target, false, true, true ); - var result = Penumbra.CollectionManager.Individuals.CanAdd( target ); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No valid character in target detected.", - _ => string.Empty, - }; - if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( Penumbra.CollectionManager.Individuals.GetGroup( target ) ); - return true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" - + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" - + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned." ); - - return false; - } - - private void UpdateIdentifiers() - { - var combo = GetNpcCombo( _newKind ); - _newPlayerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, - Array.Empty< uint >(), out _newPlayerIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, - Array.Empty< uint >(), out _newRetainerIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - if( combo.CurrentSelection.Ids != null ) - { - _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, - combo.CurrentSelection.Ids, out _newNpcIdentifiers ) switch - { - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newOwnedTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, - combo.CurrentSelection.Ids, out _newOwnedIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - } - else - { - _newNpcTooltip = NewNpcTooltipEmpty; - _newOwnedTooltip = NewNpcTooltipEmpty; - _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - } - } - - private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) - { - if( type == CollectionType.Individual ) - { - UpdateIdentifiers(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs deleted file mode 100644 index fd64093f..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ /dev/null @@ -1,317 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class CollectionsTab - { - private const int InheritedCollectionHeight = 9; - private const string InheritanceDragDropLabel = "##InheritanceMove"; - - // Keep for reuse. - private readonly HashSet< ModCollection > _seenInheritedCollections = new(32); - - // Execute changes only outside of loops. - private ModCollection? _newInheritance; - private ModCollection? _movedInheritance; - private (int, int)? _inheritanceAction; - private ModCollection? _newCurrentCollection; - - // Draw the whole inheritance block. - private void DrawInheritanceBlock() - { - using var group = ImRaii.Group(); - using var id = ImRaii.PushId( "##Inheritance" ); - ImGui.TextUnformatted( $"The {SelectedCollection} inherits from:" ); - DrawCurrentCollectionInheritance(); - DrawInheritanceTrashButton(); - DrawNewInheritanceSelection(); - DelayedActions(); - } - - // If an inherited collection is expanded, - // draw all its flattened, distinct children in order with a tree-line. - private void DrawInheritedChildren( ModCollection collection ) - { - using var id = ImRaii.PushId( collection.Index ); - using var indent = ImRaii.PushIndent(); - - // Get start point for the lines (top of the selector). - // Tree line stuff. - var lineStart = ImGui.GetCursorScreenPos(); - var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; - var drawList = ImGui.GetWindowDrawList(); - var lineSize = Math.Max( 0, ImGui.GetStyle().IndentSpacing - 9 * ImGuiHelpers.GlobalScale ); - lineStart.X += offsetX; - lineStart.Y -= 2 * ImGuiHelpers.GlobalScale; - var lineEnd = lineStart; - - // Skip the collection itself. - foreach( var inheritance in collection.GetFlattenedInheritance().Skip( 1 ) ) - { - // Draw the child, already seen collections are colored as conflicts. - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), - _seenInheritedCollections.Contains( inheritance ) ); - _seenInheritedCollections.Add( inheritance ); - - ImRaii.TreeNode( inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); - var (minRect, maxRect) = ( ImGui.GetItemRectMin(), ImGui.GetItemRectMax() ); - DrawInheritanceTreeClicks( inheritance, false ); - - // Tree line stuff. - if( minRect.X == 0 ) - { - continue; - } - - // Draw the notch and increase the line length. - var midPoint = ( minRect.Y + maxRect.Y ) / 2f - 1f; - drawList.AddLine( new Vector2( lineStart.X, midPoint ), new Vector2( lineStart.X + lineSize, midPoint ), Colors.MetaInfoText, - ImGuiHelpers.GlobalScale ); - lineEnd.Y = midPoint; - } - - // Finally, draw the folder line. - drawList.AddLine( lineStart, lineEnd, Colors.MetaInfoText, ImGuiHelpers.GlobalScale ); - } - - // Draw a single primary inherited collection. - private void DrawInheritance( ModCollection collection ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), - _seenInheritedCollections.Contains( collection ) ); - _seenInheritedCollections.Add( collection ); - using var tree = ImRaii.TreeNode( collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen ); - color.Pop(); - DrawInheritanceTreeClicks( collection, true ); - DrawInheritanceDropSource( collection ); - DrawInheritanceDropTarget( collection ); - - if( tree ) - { - DrawInheritedChildren( collection ); - } - else - { - // We still want to keep track of conflicts. - _seenInheritedCollections.UnionWith( collection.GetFlattenedInheritance() ); - } - } - - // Draw the list box containing the current inheritance information. - private void DrawCurrentCollectionInheritance() - { - using var list = ImRaii.ListBox( "##inheritanceList", - new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X, - ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ) ); - if( !list ) - { - return; - } - - _seenInheritedCollections.Clear(); - _seenInheritedCollections.Add( Penumbra.CollectionManager.Current ); - foreach( var collection in Penumbra.CollectionManager.Current.Inheritance.ToList() ) - { - DrawInheritance( collection ); - } - } - - // Draw a drag and drop button to delete. - private void DrawInheritanceTrashButton() - { - ImGui.SameLine(); - var size = new Vector2( _window._iconButtonSize.X, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ); - var buttonColor = ImGui.GetColorU32( ImGuiCol.Button ); - // Prevent hovering from highlighting the button. - using var color = ImRaii.PushColor( ImGuiCol.ButtonActive, buttonColor ) - .Push( ImGuiCol.ButtonHovered, buttonColor ); - ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, - "Drag primary inheritance here to remove it from the list.", false, true ); - - using var target = ImRaii.DragDropTarget(); - if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) - { - _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance! ), -1 ); - } - } - - // Set the current collection, or delete or move an inheritance if the action was triggered during iteration. - // Can not be done during iteration to keep collections unchanged. - private void DelayedActions() - { - if( _newCurrentCollection != null ) - { - Penumbra.CollectionManager.SetCollection( _newCurrentCollection, CollectionType.Current ); - _newCurrentCollection = null; - } - - if( _inheritanceAction == null ) - { - return; - } - - if( _inheritanceAction.Value.Item1 >= 0 ) - { - if( _inheritanceAction.Value.Item2 == -1 ) - { - Penumbra.CollectionManager.Current.RemoveInheritance( _inheritanceAction.Value.Item1 ); - } - else - { - Penumbra.CollectionManager.Current.MoveInheritance( _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2 ); - } - } - - _inheritanceAction = null; - } - - // Draw the selector to add new inheritances. - // The add button is only available if the selected collection can actually be added. - private void DrawNewInheritanceSelection() - { - DrawNewInheritanceCombo(); - ImGui.SameLine(); - var inheritance = Penumbra.CollectionManager.Current.CheckValidInheritance( _newInheritance ); - var tt = inheritance switch - { - ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", - ModCollection.ValidInheritance.Valid => $"Let the {SelectedCollection} inherit from this collection.", - ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", - ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", - ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", - _ => string.Empty, - }; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, - inheritance != ModCollection.ValidInheritance.Valid, true ) - && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance!, true ) ) - { - _newInheritance = null; - } - - if( inheritance != ModCollection.ValidInheritance.Valid ) - { - _newInheritance = null; - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), _window._iconButtonSize, "What is Inheritance?", - false, true ) ) - { - ImGui.OpenPopup( "InheritanceHelp" ); - } - - ImGuiUtil.HelpPopup( "InheritanceHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 21 * ImGui.GetTextLineHeightWithSpacing() ), () => - { - ImGui.NewLine(); - ImGui.TextWrapped( "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod." ); - ImGui.NewLine(); - ImGui.TextUnformatted( "Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'." ); - ImGui.BulletText( "If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections." ); - ImGui.BulletText( "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances." ); - ImGui.BulletText( "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used." ); - ImGui.BulletText( "If no such collection is found, the mod will be treated as disabled." ); - ImGui.BulletText( "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before." ); - ImGui.NewLine(); - ImGui.TextUnformatted( "Example" ); - ImGui.BulletText( "Collection A has the Bibo+ body and a Hempen Camise mod enabled." ); - ImGui.BulletText( "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A." ); - ImGui.BulletText( "Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured." ); - ImGui.BulletText( "Collection D inherits from C and then B and leaves everything unconfigured." ); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText( "B uses Bibo+ settings from A and its own Hempen Camise settings." ); - ImGui.BulletText( "C has Bibo+ disabled and uses A's Hempen Camise settings." ); - ImGui.BulletText( "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)." ); - } ); - } - - // Draw the combo to select new potential inheritances. - // Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. - private void DrawNewInheritanceCombo() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X ); - _newInheritance ??= Penumbra.CollectionManager.FirstOrDefault( c - => c != Penumbra.CollectionManager.Current && !Penumbra.CollectionManager.Current.Inheritance.Contains( c ) ) - ?? ModCollection.Empty; - using var combo = ImRaii.Combo( "##newInheritance", _newInheritance.Name ); - if( !combo ) - { - return; - } - - foreach( var collection in Penumbra.CollectionManager - .Where( c => Penumbra.CollectionManager.Current.CheckValidInheritance( c ) == ModCollection.ValidInheritance.Valid ) - .OrderBy( c => c.Name )) - { - if( ImGui.Selectable( collection.Name, _newInheritance == collection ) ) - { - _newInheritance = collection; - } - } - } - - // Move an inherited collection when dropped onto another. - // Move is delayed due to collection changes. - private void DrawInheritanceDropTarget( ModCollection collection ) - { - using var target = ImRaii.DragDropTarget(); - if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) - { - if( _movedInheritance != null ) - { - var idx1 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance ); - var idx2 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ); - if( idx1 >= 0 && idx2 >= 0 ) - { - _inheritanceAction = ( idx1, idx2 ); - } - } - - _movedInheritance = null; - } - } - - // Move an inherited collection. - private void DrawInheritanceDropSource( ModCollection collection ) - { - using var source = ImRaii.DragDropSource(); - if( source ) - { - ImGui.SetDragDropPayload( InheritanceDragDropLabel, IntPtr.Zero, 0 ); - _movedInheritance = collection; - ImGui.TextUnformatted( $"Moving {_movedInheritance?.Name ?? "Unknown"}..." ); - } - } - - // Ctrl + Right-Click -> Switch current collection to this (for all). - // Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete). - // Deletion is delayed due to collection changes. - private void DrawInheritanceTreeClicks( ModCollection collection, bool withDelete ) - { - if( ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - if( withDelete && ImGui.GetIO().KeyShift ) - { - _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ), -1 ); - } - else - { - _newCurrentCollection = collection; - } - } - - ImGuiUtil.HoverTooltip( $"Control + Right-Click to switch the {SelectedCollection} to this one." - + ( withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty ) ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs deleted file mode 100644 index bac726a2..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Encapsulate for less pollution. - private partial class CollectionsTab : IDisposable, ITab - { - private readonly CommunicatorService _communicator; - private readonly ConfigWindow _window; - - public CollectionsTab( CommunicatorService communicator, ConfigWindow window ) - { - _window = window; - _communicator = communicator; - - _communicator.CollectionChange.Event += UpdateIdentifiers; - } - - public ReadOnlySpan Label - => "Collections"u8; - - public void Dispose() - => _communicator.CollectionChange.Event -= UpdateIdentifiers; - - public void DrawHeader() - => OpenTutorial( BasicTutorialSteps.Collections ); - - public void DrawContent() - { - using var child = ImRaii.Child( "##collections", -Vector2.One ); - if( child ) - { - DrawActiveCollectionSelectors(); - DrawMainSelectors(); - } - } - - // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection; - - // Create a new collection that is either empty or a duplicate of the current collection. - // Resets the new collection name. - private void CreateNewCollection( bool duplicate ) - { - if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) - { - _newCollectionName = string.Empty; - } - } - - // Only gets drawn when actually relevant. - private static void DrawCleanCollectionButton( Vector2 width ) - { - if( Penumbra.CollectionManager.Current.HasUnusedSettings ) - { - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( - $"Clean {Penumbra.CollectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width - , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." - , false ) ) - { - Penumbra.CollectionManager.Current.CleanUnavailableSettings(); - } - } - } - - // Draw the new collection input as well as its buttons. - private void DrawNewCollectionInput( Vector2 width ) - { - // Input for new collection name. Also checks for validity when changed. - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputTextWithHint( "##New Collection", "New Collection Name...", ref _newCollectionName, 64 ) ) - { - _canAddCollection = Penumbra.CollectionManager.CanAddCollection( _newCollectionName, out _ ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of enabled mods." ); - - // Creation buttons. - var tt = _canAddCollection - ? string.Empty - : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; - if( ImGuiUtil.DrawDisabledButton( "Create Empty Collection", width, tt, !_canAddCollection ) ) - { - CreateNewCollection( false ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"Duplicate {SelectedCollection}", width, tt, !_canAddCollection ) ) - { - CreateNewCollection( true ); - } - } - - private void DrawCurrentCollectionSelector( Vector2 width ) - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( SelectedCollection, - "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." ); - - // Deletion conditions. - var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; - var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = deleteCondition - ? modifierHeld ? string.Empty : $"Hold {Penumbra.Config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollection}."; - - if( ImGuiUtil.DrawDisabledButton( $"Delete {SelectedCollection}", width, tt, !deleteCondition || !modifierHeld ) ) - { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); - } - - DrawCleanCollectionButton( width ); - } - - private void DrawDefaultCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( DefaultCollection, - $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," - + "as well as any character for whom no more specific conditions from below apply." ); - } - - private void DrawInterfaceCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( InterfaceCollection, - $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); - } - - private sealed class SpecialCombo : FilterComboBase< (CollectionType, string, string) > - { - public (CollectionType, string, string)? CurrentType - => CollectionTypeExtensions.Special[ CurrentIdx ]; - - public int CurrentIdx; - private readonly float _unscaledWidth; - private readonly string _label; - - public SpecialCombo( string label, float unscaledWidth ) - : base( CollectionTypeExtensions.Special, false ) - { - _label = label; - _unscaledWidth = unscaledWidth; - } - - public void Draw() - { - var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; - Draw( _label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); - } - - protected override string ToString( (CollectionType, string, string) obj ) - => obj.Item2; - - protected override bool IsVisible( int globalIdx, LowerString filter ) - { - var obj = Items[ globalIdx ]; - return filter.IsContained( obj.Item2 ) && Penumbra.CollectionManager.ByType( obj.Item1 ) == null; - } - } - - private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); - - private const string CharacterGroupDescription = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {DefaultCollection},\n" - + $"but all {IndividualAssignments} take precedence before them."; - - - // We do not check for valid character names. - private void DrawNewSpecialCollection() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _specialCollectionCombo.CurrentIdx == -1 - || Penumbra.CollectionManager.ByType( _specialCollectionCombo.CurrentType!.Value.Item1 ) != null ) - { - _specialCollectionCombo.ResetFilter(); - _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null ); - } - - if( _specialCollectionCombo.CurrentType == null ) - { - return; - } - - _specialCollectionCombo.Draw(); - ImGui.SameLine(); - var disabled = _specialCollectionCombo.CurrentType == null; - var tt = disabled - ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + CharacterGroupDescription - : CharacterGroupDescription; - if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) - { - Penumbra.CollectionManager.CreateSpecialCollection( _specialCollectionCombo.CurrentType!.Value.Item1 ); - _specialCollectionCombo.CurrentIdx = -1; - } - } - - private void DrawSpecialCollections() - { - foreach( var (type, name, desc) in CollectionTypeExtensions.Special ) - { - var collection = Penumbra.CollectionManager.ByType( type ); - if( collection != null ) - { - using var id = ImRaii.PushId( ( int )type ); - DrawCollectionSelector( "##SpecialCombo", _window._inputTextWidth.X, type, true ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, true ) ) - { - Penumbra.CollectionManager.RemoveSpecialCollection( type ); - _specialCollectionCombo.ResetFilter(); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker( name, desc ); - } - } - } - - private void DrawSpecialAssignments() - { - using var _ = ImRaii.Group(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( CharacterGroups ); - ImGuiComponents.HelpMarker( CharacterGroupDescription ); - ImGui.Separator(); - DrawSpecialCollections(); - ImGui.Dummy( Vector2.Zero ); - DrawNewSpecialCollection(); - } - - private void DrawActiveCollectionSelectors() - { - ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( BasicTutorialSteps.ActiveCollections ); - if( !open ) - { - return; - } - - ImGui.Dummy( _window._defaultSpace ); - DrawDefaultCollectionSelector(); - OpenTutorial( BasicTutorialSteps.DefaultCollection ); - DrawInterfaceCollectionSelector(); - OpenTutorial( BasicTutorialSteps.InterfaceCollection ); - ImGui.Dummy( _window._defaultSpace ); - - DrawSpecialAssignments(); - OpenTutorial( BasicTutorialSteps.SpecialCollections1 ); - - ImGui.Dummy( _window._defaultSpace ); - - DrawIndividualAssignments(); - OpenTutorial( BasicTutorialSteps.SpecialCollections2 ); - - ImGui.Dummy( _window._defaultSpace ); - } - - private void DrawMainSelectors() - { - ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( BasicTutorialSteps.EditingCollections ); - if( !open ) - { - return; - } - - var width = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); - ImGui.Dummy( _window._defaultSpace ); - DrawCurrentCollectionSelector( width ); - OpenTutorial( BasicTutorialSteps.CurrentCollection ); - ImGui.Dummy( _window._defaultSpace ); - DrawNewCollectionInput( width ); - ImGui.Dummy( _window._defaultSpace ); - DrawInheritanceBlock(); - OpenTutorial( BasicTutorialSteps.Inheritance ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs deleted file mode 100644 index 0a59b517..00000000 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ /dev/null @@ -1,644 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Numerics; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Group; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Widgets; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Files; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; -using Penumbra.Interop.Structs; -using Penumbra.Services; -using Penumbra.String; -using Penumbra.Util; -using static OtterGui.Raii.ImRaii; -using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; -using CharacterUtility = Penumbra.Interop.CharacterUtility; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class DebugTab : ITab - { - private readonly StartTracker _timer; - private readonly ConfigWindow _window; - - public DebugTab( ConfigWindow window, StartTracker timer) - { - _window = window; - _timer = timer; - } - - public ReadOnlySpan Label - => "Debug"u8; - - public bool IsVisible - => Penumbra.Config.DebugMode; - -#if DEBUG - private const string DebugVersionString = "(Debug)"; -#else - private const string DebugVersionString = "(Release)"; -#endif - - public void DrawContent() - { - using var child = Child( "##DebugTab", -Vector2.One ); - if( !child ) - { - return; - } - - DrawDebugTabGeneral(); - DrawPerformanceTab(); - ImGui.NewLine(); - DrawPathResolverDebug(); - ImGui.NewLine(); - DrawActorsDebug(); - ImGui.NewLine(); - DrawDebugCharacterUtility(); - ImGui.NewLine(); - DrawStainTemplates(); - ImGui.NewLine(); - DrawDebugTabMetaLists(); - ImGui.NewLine(); - DrawDebugResidentResources(); - ImGui.NewLine(); - DrawResourceProblems(); - ImGui.NewLine(); - DrawPlayerModelInfo(); - ImGui.NewLine(); - DrawDebugTabIpc(); - ImGui.NewLine(); - } - - // Draw general information about mod and collection state. - private void DrawDebugTabGeneral() - { - if( !ImGui.CollapsingHeader( "General" ) ) - { - return; - } - - using var table = Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); - if( !table ) - { - return; - } - - var manager = Penumbra.ModManager; - PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); - PrintValue( "Git Commit Hash", Penumbra.CommitHash ); - PrintValue( SelectedCollection, Penumbra.CollectionManager.Current.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); - PrintValue( DefaultCollection, Penumbra.CollectionManager.Default.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); - PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); - PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Path Resolver Enabled", _window._penumbra.PathResolver.Enabled.ToString() ); - PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); - } - - private void DrawPerformanceTab() - { - ImGui.NewLine(); - if( ImGui.CollapsingHeader( "Performance" ) ) - { - return; - } - - using( var start = TreeNode( "Startup Performance", ImGuiTreeNodeFlags.DefaultOpen ) ) - { - if( start ) - { - _timer.Draw( "##startTimer", TimingExtensions.ToName ); - ImGui.NewLine(); - } - } - - Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); - } - - private static unsafe void DrawActorsDebug() - { - if( !ImGui.CollapsingHeader( "Actors" ) ) - { - return; - } - - using var table = Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - static void DrawSpecial( string name, ActorIdentifier id ) - { - if( !id.IsValid ) - { - return; - } - - ImGuiUtil.DrawTableColumn( name ); - ImGuiUtil.DrawTableColumn( string.Empty ); - ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( id ) ); - ImGuiUtil.DrawTableColumn( string.Empty ); - } - - DrawSpecial( "Current Player", Penumbra.Actors.GetCurrentPlayer() ); - DrawSpecial( "Current Inspect", Penumbra.Actors.GetInspectPlayer() ); - DrawSpecial( "Current Card", Penumbra.Actors.GetCardPlayer() ); - DrawSpecial( "Current Glamour", Penumbra.Actors.GetGlamourPlayer() ); - - foreach( var obj in DalamudServices.Objects ) - { - ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); - ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); - var identifier = Penumbra.Actors.FromObject( obj, false, true, false ); - ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); - var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); - ImGuiUtil.DrawTableColumn( id ); - } - } - - // Draw information about which draw objects correspond to which game objects - // and which paths are due to be loaded by which collection. - private unsafe void DrawPathResolverDebug() - { - if( !ImGui.CollapsingHeader( "Path Resolver" ) ) - { - return; - } - - ImGui.TextUnformatted( - $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); - using( var drawTree = TreeNode( "Draw Object to Object" ) ) - { - if( drawTree ) - { - using var table = Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ptr.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( idx.ToString() ); - ImGui.TableNextColumn(); - var obj = ( GameObject* )DalamudServices.Objects.GetObjectAddress( idx ); - var (address, name) = - obj != null ? ( $"0x{( ulong )obj:X}", new ByteString( obj->Name ).ToString() ) : ( "NULL", "NULL" ); - ImGui.TextUnformatted( address ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( c.ModCollection.Name ); - } - } - } - } - - using( var pathTree = TreeNode( "Path Collections" ) ) - { - if( pathTree ) - { - using var table = Table( "###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) - { - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.ModCollection.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.AssociatedGameObject.ToString( "X" ) ); - } - } - } - } - - using( var resourceTree = TreeNode( "Subfile Collections" ) ) - { - if( resourceTree ) - { - using var table = Table( "###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - ImGuiUtil.DrawTableColumn( "Current Mtrl Data" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentMtrlData.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentMtrlData.AssociatedGameObject:X}" ); - - ImGuiUtil.DrawTableColumn( "Current Avfx Data" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentAvfxData.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentAvfxData.AssociatedGameObject:X}" ); - - ImGuiUtil.DrawTableColumn( "Current Resources" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.SubfileCount.ToString() ); - ImGui.TableNextColumn(); - - foreach( var (resource, resolve) in _window._penumbra.PathResolver.ResourceCollections ) - { - ImGuiUtil.DrawTableColumn( $"0x{resource:X}" ); - ImGuiUtil.DrawTableColumn( resolve.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{resolve.AssociatedGameObject:X}" ); - } - } - } - } - - using( var identifiedTree = TreeNode( "Identified Collections" ) ) - { - if( identifiedTree ) - { - using var table = Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (address, identifier, collection) in PathResolver.IdentifiedCache ) - { - ImGuiUtil.DrawTableColumn( $"0x{address:X}" ); - ImGuiUtil.DrawTableColumn( identifier.ToString() ); - ImGuiUtil.DrawTableColumn( collection.Name ); - } - } - } - } - - using( var cutsceneTree = TreeNode( "Cutscene Actors" ) ) - { - if( cutsceneTree ) - { - using var table = Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) - { - ImGuiUtil.DrawTableColumn( $"Cutscene Actor {idx}" ); - ImGuiUtil.DrawTableColumn( actor.Name.ToString() ); - } - } - } - } - - using( var groupTree = TreeNode( "Group" ) ) - { - if( groupTree ) - { - using var table = Table( "###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - ImGuiUtil.DrawTableColumn( "Group Members" ); - ImGuiUtil.DrawTableColumn( GroupManager.Instance()->MemberCount.ToString() ); - for( var i = 0; i < 8; ++i ) - { - ImGuiUtil.DrawTableColumn( $"Member #{i}" ); - var member = GroupManager.Instance()->GetPartyMemberByIndex( i ); - ImGuiUtil.DrawTableColumn( member == null ? "NULL" : new ByteString( member->Name ).ToString() ); - } - } - } - } - - using( var bannerTree = TreeNode( "Party Banner" ) ) - { - if( bannerTree ) - { - var agent = &AgentBannerParty.Instance()->AgentBannerInterface; - if( agent->Data == null ) - { - agent = &AgentBannerMIP.Instance()->AgentBannerInterface; - } - - if( agent->Data != null ) - { - using var table = Table( "###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - for( var i = 0; i < 8; ++i ) - { - var c = agent->Character( i ); - ImGuiUtil.DrawTableColumn( $"Character {i}" ); - var name = c->Name1.ToString(); - ImGuiUtil.DrawTableColumn( name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})" ); - } - } - } - else - { - ImGui.TextUnformatted( "INACTIVE" ); - } - } - } - } - - private static unsafe void DrawStainTemplates() - { - if( !ImGui.CollapsingHeader( "Staining Templates" ) ) - { - return; - } - - foreach( var (key, data) in Penumbra.StainService.StmFile.Entries ) - { - using var tree = TreeNode( $"Template {key}" ); - if( !tree ) - { - continue; - } - - using var table = Table( "##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - for( var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i ) - { - var (r, g, b) = data.DiffuseEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - ( r, g, b ) = data.SpecularEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - ( r, g, b ) = data.EmissiveEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - var a = data.SpecularPowerEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{a:F6}" ); - - a = data.GlossEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{a:F6}" ); - } - } - } - - // Draw information about the character utility class from SE, - // displaying all files, their sizes, the default files and the default sizes. - public static unsafe void DrawDebugCharacterUtility() - { - if( !ImGui.CollapsingHeader( "Character Utility" ) ) - { - return; - } - - using var table = Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) - { - var idx = CharacterUtility.RelevantIndices[ i ]; - var intern = new CharacterUtility.InternalIndex( i ); - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resource( idx ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{resource->GetData().Data:X}" ); - if( ImGui.IsItemClicked() ) - { - var (data, length) = resource->GetData(); - if( data != IntPtr.Zero && length > 0 ) - { - ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - } - - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{resource->GetData().Length}" ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResource( intern ).Address:X}" ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResource( intern ).Address, - Penumbra.CharacterUtility.DefaultResource( intern ).Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResource( intern ).Size}" ); - } - } - - private static void DrawDebugTabMetaLists() - { - if( !ImGui.CollapsingHeader( "Metadata Changes" ) ) - { - return; - } - - using var table = Table( "##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - foreach( var list in Penumbra.CharacterUtility.Lists ) - { - ImGuiUtil.DrawTableColumn( list.GlobalIndex.ToString() ); - ImGuiUtil.DrawTableColumn( list.Entries.Count.ToString() ); - ImGuiUtil.DrawTableColumn( string.Join( ", ", list.Entries.Select( e => $"0x{e.Data:X}" ) ) ); - } - } - - // Draw information about the resident resource files. - public unsafe void DrawDebugResidentResources() - { - if( !ImGui.CollapsingHeader( "Resident Resources" ) ) - { - return; - } - - if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) - { - return; - } - - using var table = Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) - { - var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - } - } - - // Draw information about the models, materials and resources currently loaded by the local player. - private static unsafe void DrawPlayerModelInfo() - { - var player = DalamudServices.ClientState.LocalPlayer; - var name = player?.Name.ToString() ?? "NULL"; - if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - using( var t1 = Table( "##table", 2, ImGuiTableFlags.SizingFixedFit ) ) - { - if( t1 ) - { - ImGuiUtil.DrawTableColumn( "Flags" ); - ImGuiUtil.DrawTableColumn( $"{model->UnkFlags_01:X2}" ); - ImGuiUtil.DrawTableColumn( "Has Model In Slot Loaded" ); - ImGuiUtil.DrawTableColumn( $"{model->HasModelInSlotLoaded:X8}" ); - ImGuiUtil.DrawTableColumn( "Has Model Files In Slot Loaded" ); - ImGuiUtil.DrawTableColumn( $"{model->HasModelFilesInSlotLoaded:X8}" ); - } - } - - using var table = Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) - { - Text( imc ); - } - - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - continue; - } - - ImGui.TableNextColumn(); - { - Text( mdl->ResourceHandle ); - } - } - } - - // Draw resources with unusual reference count. - private static unsafe void DrawResourceProblems() - { - var header = ImGui.CollapsingHeader( "Resource Problems" ); - ImGuiUtil.HoverTooltip( "Draw resources with unusually high reference count to detect overflows." ); - if( !header ) - { - return; - } - - using var table = Table( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - Penumbra.ResourceManagerService.IterateResources( ( _, r ) => - { - if( r->RefCount < 10000 ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->Category.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->FileType.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->Id.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ( ( ulong )r ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->RefCount.ToString() ); - ImGui.TableNextColumn(); - ref var name = ref r->FileName; - if( name.Capacity > 15 ) - { - ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); - } - else - { - fixed( byte* ptr = name.Buffer ) - { - ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); - } - } - } ); - } - - - // Draw information about IPC options and availability. - private void DrawDebugTabIpc() - { - if( !ImGui.CollapsingHeader( "IPC" ) ) - { - _window._penumbra.IpcProviders.Tester.UnsubscribeEvents(); - return; - } - - _window._penumbra.IpcProviders.Tester.Draw(); - } - - // Helper to print a property and its value in a 2-column table. - private static void PrintValue( string name, string value ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( value ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs deleted file mode 100644 index a90df004..00000000 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class EffectiveTab : ITab - { - public ReadOnlySpan Label - => "Effective Changes"u8; - - public void DrawContent() - { - SetupEffectiveSizes(); - DrawFilters(); - using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var table = ImRaii.Table( "##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength ); - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); - ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); - - DrawEffectiveRows( Penumbra.CollectionManager.Current, skips, height, - _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); - } - - // Sizes - private float _effectiveLeftTextLength; - private float _effectiveRightTextLength; - private float _effectiveUnscaledArrowLength; - private float _effectiveArrowLength; - - // Filters - private LowerString _effectiveGamePathFilter = LowerString.Empty; - private LowerString _effectiveFilePathFilter = LowerString.Empty; - - // Setup table sizes. - private void SetupEffectiveSizes() - { - if( _effectiveUnscaledArrowLength == 0 ) - { - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - _effectiveUnscaledArrowLength = - ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; - } - - _effectiveArrowLength = _effectiveUnscaledArrowLength * ImGuiHelpers.GlobalScale; - _effectiveLeftTextLength = 450 * ImGuiHelpers.GlobalScale; - _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; - } - - // Draw the header line for filters - private void DrawFilters() - { - var tmp = _effectiveGamePathFilter.Text; - ImGui.SetNextItemWidth( _effectiveLeftTextLength ); - if( ImGui.InputTextWithHint( "##gamePathFilter", "Filter game path...", ref tmp, 256 ) ) - { - _effectiveGamePathFilter = tmp; - } - - ImGui.SameLine( _effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - tmp = _effectiveFilePathFilter.Text; - if( ImGui.InputTextWithHint( "##fileFilter", "Filter file path...", ref tmp, 256 ) ) - { - _effectiveFilePathFilter = tmp; - } - } - - // Draw all rows respecting filters and using clipping. - private void DrawEffectiveRows( ModCollection active, int skips, float height, bool hasFilters ) - { - // We can use the known counts if no filters are active. - var stop = hasFilters - ? ImGuiClip.FilteredClippedDraw( active.ResolvedFiles, skips, CheckFilters, DrawLine ) - : ImGuiClip.ClippedDraw( active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count ); - - var m = active.MetaCache; - // If no meta manipulations are active, we can just draw the end dummy. - if( m is { Count: > 0 } ) - { - // Filters mean we can not use the known counts. - if( hasFilters ) - { - var it2 = m.Select( p => ( p.Key.ToString(), p.Value.Name ) ); - if( stop >= 0 ) - { - ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); - } - else - { - stop = ImGuiClip.FilteredClippedDraw( it2, skips, CheckFilters, DrawLine, ~stop ); - ImGuiClip.DrawEndDummy( stop, height ); - } - } - else - { - if( stop >= 0 ) - { - ImGuiClip.DrawEndDummy( stop + m.Count, height ); - } - else - { - stop = ImGuiClip.ClippedDraw( m, skips, DrawLine, m.Count, ~stop ); - ImGuiClip.DrawEndDummy( stop, height ); - } - } - } - else - { - ImGuiClip.DrawEndDummy( stop, height ); - } - } - - // Draw a line for a game path and its redirected file. - private static void DrawLine( KeyValuePair< Utf8GamePath, ModPath > pair ) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - CopyOnClickSelectable( path.Path ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - CopyOnClickSelectable( name.Path.InternalName ); - ImGuiUtil.HoverTooltip( $"\nChanged by {name.Mod.Name}." ); - } - - // Draw a line for a path and its name. - private static void DrawLine( (string, LowerString) pair ) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( path ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( name ); - } - - // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( KeyValuePair< MetaManipulation, IMod > pair ) - { - var (manipulation, mod) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( mod.Name ); - } - - // Check filters for file replacements. - private bool CheckFilters( KeyValuePair< Utf8GamePath, ModPath > kvp ) - { - var (gamePath, fullPath) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) - { - return false; - } - - return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); - } - - // Check filters for meta manipulations. - private bool CheckFilters( (string, LowerString) kvp ) - { - var (name, path) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains( _effectiveGamePathFilter.Lower ) ) - { - return false; - } - - return _effectiveFilePathFilter.Length == 0 || path.Contains( _effectiveFilePathFilter.Lower ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs deleted file mode 100644 index b683b75e..00000000 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Draw text given by a ByteString. - internal static unsafe void Text( ByteString s ) - => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); - - // Draw text given by a byte pointer. - private static unsafe void Text( byte* s, int length ) - => ImGuiNative.igTextUnformatted( s, s + length ); - - // Draw the name of a resource file. - private static unsafe void Text( ResourceHandle* resource ) - => Text( resource->FileName().Path, resource->FileNameLength ); - - // Draw a ByteString as a selectable. - internal static unsafe bool Selectable( ByteString s, bool selected ) - { - var tmp = ( byte )( selected ? 1 : 0 ); - return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0; - } - - // Apply Changed Item Counters to the Name if necessary. - private static string ChangedItemName( string name, object? data ) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - // Draw a changed item, invoking the Api-Events for clicks and tooltips. - // Also draw the item Id in grey if requested - private void DrawChangedItem( string name, object? data, bool drawId ) - { - name = ChangedItemName( name, data ); - var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; - - if( ret != MouseButton.None ) - { - _penumbra.Api.InvokeClick( ret, data ); - } - - if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - _penumbra.Api.InvokeTooltip( data ); - group.Dispose(); - if( ImGui.GetItemRectSize() == Vector2.Zero ) - { - ImGui.TextUnformatted( "No actions available." ); - } - } - - if( drawId && DrawChangedItemObject( data, out var text ) ) - { - ImGui.SameLine( ImGui.GetContentRegionAvail().X ); - ImGuiUtil.RightJustify( text, ColorId.ItemId.Value() ); - } - } - - private static bool DrawChangedItemObject( object? obj, out string text ) - { - switch( obj ) - { - case Item it: - var quad = ( Quad )it.ModelMain; - text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; - return true; - case ModelChara m: - text = $"({( ( CharacterBase.ModelType )m.Type ).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; - } - } - - // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, - // using an ByteString. - private static unsafe void CopyOnClickSelectable( ByteString text ) - { - if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) - { - ImGuiNative.igSetClipboardText( text.Path ); - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( "Click to copy to clipboard." ); - } - } - - private sealed class CollectionSelector : FilterComboCache< ModCollection > - { - public CollectionSelector( Func> items ) - : base( items ) - { } - - public void Draw( string label, float width, int individualIdx ) - { - var (_, collection) = Penumbra.CollectionManager.Individuals[ individualIdx ]; - if( Draw( label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) - { - Penumbra.CollectionManager.SetCollection( CurrentSelection, CollectionType.Individual, individualIdx ); - } - } - - public void Draw( string label, float width, CollectionType type ) - { - var current = Penumbra.CollectionManager.ByType( type, ActorIdentifier.Invalid ); - if( Draw( label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) - { - Penumbra.CollectionManager.SetCollection( CurrentSelection, type ); - } - } - - protected override string ToString( ModCollection obj ) - => obj.Name; - } - - private static readonly CollectionSelector CollectionsWithEmpty = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).Prepend( ModCollection.Empty ).ToList()); - private static readonly CollectionSelector Collections = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).ToList()); - - // Draw a collection selector of a certain width for a certain type. - private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty ) - => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType ); - - // Set up the file selector with the right flags and custom side bar items. - public static FileDialogManager SetupFileManager() - { - var fileManager = new FileDialogManager - { - AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, - }; - - if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) - { - fileManager.CustomSideBarItems.Add( ( "Downloads", downloadsFolder, FontAwesomeIcon.Download, -1 ) ); - } - - if( Functions.GetQuickAccessFolders( out var folders ) ) - { - foreach( var ((name, path), idx) in folders.WithIndex() ) - { - fileManager.CustomSideBarItems.Add( ( $"{name}##{idx}", path, FontAwesomeIcon.Folder, -1 ) ); - } - } - - // Add Penumbra Root. This is not updated if the root changes right now. - fileManager.CustomSideBarItems.Add( ( "Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0 ) ); - - // Remove Videos and Music. - fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); - fileManager.CustomSideBarItems.Add( ( "Music", string.Empty, 0, -1 ) ); - - return fileManager; - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs deleted file mode 100644 index ca5de44b..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ /dev/null @@ -1,776 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Api.Enums; -using Penumbra.Mods; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - private Vector2 _cellPadding = Vector2.Zero; - private Vector2 _itemSpacing = Vector2.Zero; - - // Draw the edit tab that contains all things concerning editing the mod. - private void DrawEditModTab() - { - using var tab = DrawTab( EditModTabHeader, Tabs.Edit ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##editChild", -Vector2.One ); - if( !child ) - { - return; - } - - _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale }; - _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale }; - - EditButtons(); - EditRegularMeta(); - ImGui.Dummy( _window._defaultSpace ); - - if( Input.Text( "Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, - _window._inputTextWidth.X ) ) - { - try - { - _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); - } - catch( Exception e ) - { - Penumbra.Log.Warning( e.Message ); - } - } - - ImGui.Dummy( _window._defaultSpace ); - var tagIdx = _modTags.Draw( "Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag ); - if( tagIdx >= 0 ) - { - Penumbra.ModManager.ChangeModTag( _mod.Index, tagIdx, editedTag ); - } - - ImGui.Dummy( _window._defaultSpace ); - AddOptionGroup.Draw( _window, _mod ); - ImGui.Dummy( _window._defaultSpace ); - - for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx ) - { - EditGroup( groupIdx ); - } - - EndActions(); - DescriptionEdit.DrawPopup( _window ); - } - - // The general edit row for non-detailed mod edits. - private void EditButtons() - { - var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 ); - var folderExists = Directory.Exists( _mod.ModPath.FullName ); - var tt = folderExists - ? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice." - : $"Mod directory \"{_mod.ModPath.FullName}\" does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", buttonSize, tt, !folderExists ) ) - { - Process.Start( new ProcessStartInfo( _mod.ModPath.FullName ) { UseShellExecute = true } ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Reload the current mod from its files.\n" - + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", - false ) ) - { - Penumbra.ModManager.ReloadMod( _mod.Index ); - } - - BackupButtons( buttonSize ); - MoveDirectory.Draw( _mod, buttonSize ); - - ImGui.Dummy( _window._defaultSpace ); - DrawUpdateBibo( buttonSize ); - - ImGui.Dummy( _window._defaultSpace ); - } - - private void DrawUpdateBibo( Vector2 buttonSize ) - { - if( ImGui.Button( "Update Bibo Material", buttonSize ) ) - { - var editor = new Mod.Editor( _mod, null ); - editor.ReplaceAllMaterials( "bibo", "b" ); - editor.ReplaceAllMaterials( "bibopube", "c" ); - editor.SaveAllModels(); - _window.ModEditPopup.UpdateModels(); - } - - ImGuiUtil.HoverTooltip( - "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" - + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" - + "Use this for outdated mods made for old Bibo bodies.\n" - + "Go to Advanced Editing for more fine-tuned control over material assignment." ); - } - - private void BackupButtons( Vector2 buttonSize ) - { - var backup = new ModBackup( _mod ); - var tt = ModBackup.CreatingBackup - ? "Already exporting a mod." - : backup.Exists - ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." - : $"Create exported archive of current mod at \"{backup.Name}\"."; - if( ImGuiUtil.DrawDisabledButton( "Export Mod", buttonSize, tt, ModBackup.CreatingBackup ) ) - { - backup.CreateAsync(); - } - - ImGui.SameLine(); - tt = backup.Exists - ? $"Delete existing mod export \"{backup.Name}\"." - : $"Exported mod \"{backup.Name}\" does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Delete Export", buttonSize, tt, !backup.Exists ) ) - { - backup.Delete(); - } - - tt = backup.Exists - ? $"Restore mod from exported file \"{backup.Name}\"." - : $"Exported mod \"{backup.Name}\" does not exist."; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Restore From Export", buttonSize, tt, !backup.Exists ) ) - { - backup.Restore(); - } - } - - // Anything about editing the regular meta information about the mod. - private void EditRegularMeta() - { - if( Input.Text( "Name", Input.Name, Input.None, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModName( _mod.Index, newName ); - } - - if( Input.Text( "Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor ); - } - - if( Input.Text( "Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, - _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion ); - } - - if( Input.Text( "Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, - _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); - } - - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - - var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X, 0 ); - if( ImGui.Button( "Edit Description", reducedSize ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, Input.Description ) ); - } - - ImGui.SameLine(); - var fileExists = File.Exists( _mod.MetaFile.FullName ); - var tt = fileExists - ? "Open the metadata json file in the text editor of your choice." - : "The metadata json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", _window._iconButtonSize, tt, - !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); - } - } - - // Do some edits outside of iterations. - private readonly Queue< Action > _delayedActions = new(); - - // Delete a marked group or option outside of iteration. - private void EndActions() - { - while( _delayedActions.TryDequeue( out var action ) ) - { - action.Invoke(); - } - } - - // Text input to add a new option group at the end of the current groups. - private static class AddOptionGroup - { - private static string _newGroupName = string.Empty; - - public static void Reset() - => _newGroupName = string.Empty; - - public static void Draw( ConfigWindow window, Mod mod ) - { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale ) ); - ImGui.SetNextItemWidth( window._inputTextWidth.X - window._iconButtonSize.X - 3 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); - ImGui.SameLine(); - var fileExists = File.Exists( mod.DefaultFile ); - var tt = fileExists - ? "Open the default option json file in the text editor of your choice." - : "The default option json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", window._iconButtonSize, tt, - !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( mod.DefaultFile ) { UseShellExecute = true } ); - } - - ImGui.SameLine(); - - var nameValid = Penumbra.ModManager.VerifyFileName( mod, null, _newGroupName, false ); - tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, - tt, !nameValid, true ) ) - { - Penumbra.ModManager.AddModGroup( mod, GroupType.Single, _newGroupName ); - Reset(); - } - } - } - - // A text input for the new directory name and a button to apply the move. - private static class MoveDirectory - { - private static string? _currentModDirectory; - private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; - - public static void Reset() - { - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; - } - - public static void Draw( Mod mod, Vector2 buttonSize ) - { - ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); - var tmp = _currentModDirectory ?? mod.ModPath.Name; - if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) - { - _currentModDirectory = tmp; - _state = Penumbra.ModManager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); - } - - var (disabled, tt) = _state switch - { - Mod.Manager.NewDirectoryState.Identical => ( true, "Current directory name is identical to new one." ), - Mod.Manager.NewDirectoryState.Empty => ( true, "Please enter a new directory name first." ), - Mod.Manager.NewDirectoryState.NonExisting => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), - Mod.Manager.NewDirectoryState.ExistsEmpty => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), - Mod.Manager.NewDirectoryState.ExistsNonEmpty => ( true, $"{_currentModDirectory} already exists and is not empty." ), - Mod.Manager.NewDirectoryState.ExistsAsFile => ( true, $"{_currentModDirectory} exists as a file." ), - Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => ( true, - $"{_currentModDirectory} contains invalid symbols for FFXIV." ), - _ => ( true, "Unknown error." ), - }; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null ) - { - Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); - Reset(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n" - + "This can currently not be used on pre-existing folders and does not support merges or overwriting." ); - } - } - - // Open a popup to edit a multi-line mod or option description. - private static class DescriptionEdit - { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDesriptionOptionIdx = -1; - private static Mod? _mod; - - public static void OpenPopup( Mod mod, int groupIdx, int optionIdx = -1 ) - { - _newDescriptionIdx = groupIdx; - _newDesriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 - ? mod.Description - : optionIdx < 0 - ? mod.Groups[ groupIdx ].Description - : mod.Groups[ groupIdx ][ optionIdx ].Description; - - _mod = mod; - ImGui.OpenPopup( PopupName ); - } - - public static void DrawPopup( ConfigWindow window ) - { - if( _mod == null ) - { - return; - } - - using var popup = ImRaii.Popup( PopupName ); - if( !popup ) - { - return; - } - - if( ImGui.IsWindowAppearing() ) - { - ImGui.SetKeyboardFocusHere(); - } - - ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) ); - ImGui.Dummy( window._defaultSpace ); - - var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 ); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 ); - - var oldDescription = _newDescriptionIdx == Input.Description - ? _mod.Description - : _mod.Groups[ _newDescriptionIdx ].Description; - - var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; - - if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) ) - { - switch( _newDescriptionIdx ) - { - case Input.Description: - Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); - break; - case >= 0: - if( _newDesriptionOptionIdx < 0 ) - { - Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); - } - else - { - Penumbra.ModManager.ChangeOptionDescription( _mod, _newDescriptionIdx, _newDesriptionOptionIdx, _newDescription ); - } - - break; - } - - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if( ImGui.Button( "Cancel", buttonSize ) - || ImGui.IsKeyPressed( ImGuiKey.Escape ) ) - { - _newDescriptionIdx = Input.None; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); - } - } - } - - private void EditGroup( int groupIdx ) - { - var group = _mod.Groups[ groupIdx ]; - using var id = ImRaii.PushId( groupIdx ); - using var frame = ImRaii.FramedGroup( $"Group #{groupIdx + 1}" ); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding ) - .Push( ImGuiStyleVar.ItemSpacing, _itemSpacing ); - - if( Input.Text( "##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName ); - } - - ImGuiUtil.HoverTooltip( "Group Name" ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, - "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - - if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) - { - Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); - } - - ImGuiUtil.HoverTooltip( "Group Priority" ); - - DrawGroupCombo( group, groupIdx ); - ImGui.SameLine(); - - var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), _window._iconButtonSize, - tt, groupIdx == 0, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) ); - } - - ImGui.SameLine(); - tt = groupIdx == _mod.Groups.Count - 1 - ? "Can not move this group further downwards." - : $"Move this group down to group {groupIdx + 2}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), _window._iconButtonSize, - tt, groupIdx == _mod.Groups.Count - 1, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); - } - - ImGui.SameLine(); - - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, - "Edit group description.", false, true ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - var fileName = group.FileName( _mod.ModPath, groupIdx ); - var fileExists = File.Exists( fileName ); - tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( fileName ) { UseShellExecute = true } ); - } - - ImGui.Dummy( _window._defaultSpace ); - - OptionTable.Draw( this, groupIdx ); - } - - // Draw the table displaying all options and the add new option line. - private static class OptionTable - { - private const string DragDropLabel = "##DragOption"; - - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static int _dragDropGroupIdx = -1; - private static int _dragDropOptionIdx = -1; - - public static void Reset() - { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; - } - - public static void Draw( ModPanel panel, int groupIdx ) - { - using var table = ImRaii.Table( string.Empty, 6, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); - ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, - panel._window._inputTextWidth.X - 72 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() - panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "description", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); - - var group = panel._mod.Groups[ groupIdx ]; - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) - { - EditOption( panel, group, groupIdx, optionIdx ); - } - - DrawNewOption( panel, groupIdx, panel._window._iconButtonSize ); - } - - // Draw a line for a single option. - private static void EditOption( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) - { - var option = group[ optionIdx ]; - using var id = ImRaii.PushId( optionIdx ); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( $"Option #{optionIdx + 1}" ); - Source( group, groupIdx, optionIdx ); - Target( panel, group, groupIdx, optionIdx ); - - ImGui.TableNextColumn(); - - - if( group.Type == GroupType.Single ) - { - if( ImGui.RadioButton( "##default", group.DefaultSettings == optionIdx ) ) - { - Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, ( uint )optionIdx ); - } - - ImGuiUtil.HoverTooltip( $"Set {option.Name} as the default choice for this group." ); - } - else - { - var isDefaultOption = ( ( group.DefaultSettings >> optionIdx ) & 1 ) != 0; - if( ImGui.Checkbox( "##default", ref isDefaultOption ) ) - { - Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, isDefaultOption - ? group.DefaultSettings | ( 1u << optionIdx ) - : group.DefaultSettings & ~( 1u << optionIdx ) ); - } - - ImGuiUtil.HoverTooltip( $"{( isDefaultOption ? "Disable" : "Enable" )} {option.Name} per default in this group." ); - } - - ImGui.TableNextColumn(); - if( Input.Text( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) - { - Penumbra.ModManager.RenameOption( panel._mod, groupIdx, optionIdx, newOptionName ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, "Edit option description.", false, true ) ) - { - panel._delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( panel._mod, groupIdx, optionIdx ) ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) - { - panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); - } - - ImGui.TableNextColumn(); - if( group.Type == GroupType.Multi ) - { - if( Input.Priority( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, - 50 * ImGuiHelpers.GlobalScale ) ) - { - Penumbra.ModManager.ChangeOptionPriority( panel._mod, groupIdx, optionIdx, priority ); - } - - ImGuiUtil.HoverTooltip( "Option priority." ); - } - } - - // Draw the line to add a new option. - private static void DrawNewOption( ModPanel panel, int groupIdx, Vector2 iconButtonSize ) - { - var mod = panel._mod; - var group = mod.Groups[ groupIdx ]; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( $"Option #{group.Count + 1}" ); - Target( panel, group, groupIdx, group.Count ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; - if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) ) - { - _newOptionName = tmp; - _newOptionNameIdx = groupIdx; - } - - ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; - var tt = canAddGroup - ? validName ? "Add a new option to this group." : "Please enter a name for the new option." - : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - tt, !( canAddGroup && validName ), true ) ) - { - Penumbra.ModManager.AddOption( mod, groupIdx, _newOptionName ); - _newOptionName = string.Empty; - } - } - - // Handle drag and drop to move options inside a group or into another group. - private static void Source( IModGroup group, int groupIdx, int optionIdx ) - { - using var source = ImRaii.DragDropSource(); - if( !source ) - { - return; - } - - if( ImGui.SetDragDropPayload( DragDropLabel, IntPtr.Zero, 0 ) ) - { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; - } - - ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); - } - - private static void Target( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) - { - using var target = ImRaii.DragDropTarget(); - if( !target.Success || !ImGuiUtil.IsDropping( DragDropLabel ) ) - { - return; - } - - if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) - { - if( _dragDropGroupIdx == groupIdx ) - { - var sourceOption = _dragDropOptionIdx; - panel._delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( panel._mod, groupIdx, sourceOption, optionIdx ) ); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceGroupIdx = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var sourceGroup = panel._mod.Groups[ sourceGroupIdx ]; - var currentCount = group.Count; - var option = sourceGroup[ sourceOption ]; - var priority = sourceGroup.OptionPriority( _dragDropOptionIdx ); - panel._delayedActions.Enqueue( () => - { - Penumbra.ModManager.DeleteOption( panel._mod, sourceGroupIdx, sourceOption ); - Penumbra.ModManager.AddOption( panel._mod, groupIdx, option, priority ); - Penumbra.ModManager.MoveOption( panel._mod, groupIdx, currentCount, optionIdx ); - } ); - } - } - - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; - } - } - - // Draw a combo to select single or multi group and switch between them. - private void DrawGroupCombo( IModGroup group, int groupIdx ) - { - static string GroupTypeName( GroupType type ) - => type switch - { - GroupType.Single => "Single Group", - GroupType.Multi => "Multi Group", - _ => "Unknown", - }; - - ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); - using var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ); - if( !combo ) - { - return; - } - - if( ImGui.Selectable( GroupTypeName( GroupType.Single ), group.Type == GroupType.Single ) ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Single ); - } - - var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti ); - if( ImGui.Selectable( GroupTypeName( GroupType.Multi ), group.Type == GroupType.Multi ) && canSwitchToMulti ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Multi ); - } - - style.Pop(); - if( !canSwitchToMulti ) - { - ImGuiUtil.HoverTooltip( $"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options." ); - } - } - - // Handles input text and integers in separate fields without buffers for every single one. - private static class Input - { - // Special field indices to reuse the same string buffer. - public const int None = -1; - public const int Name = -2; - public const int Author = -3; - public const int Version = -4; - public const int Website = -5; - public const int Path = -6; - public const int Description = -7; - - // Temporary strings - private static string? _currentEdit; - private static int? _currentGroupPriority; - private static int _currentField = None; - private static int _optionIndex = None; - - public static void Reset() - { - _currentEdit = null; - _currentGroupPriority = null; - _currentField = None; - _optionIndex = None; - } - - public static bool Text( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputText( label, ref tmp, maxLength ) ) - { - _currentEdit = tmp; - _optionIndex = option; - _currentField = field; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) - { - var ret = _currentEdit != oldValue; - value = _currentEdit; - Reset(); - return ret; - } - - value = string.Empty; - return false; - } - - public static bool Priority( string label, int field, int option, int oldValue, out int value, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputInt( label, ref tmp, 0, 0 ) ) - { - _currentGroupPriority = tmp; - _optionIndex = option; - _currentField = field; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) - { - var ret = _currentGroupPriority != oldValue; - value = _currentGroupPriority.Value; - Reset(); - return ret; - } - - value = 0; - return false; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs deleted file mode 100644 index 6297e807..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Diagnostics; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.GameFonts; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - // We use a big, nice game font for the title. - private readonly GameFontHandle _nameFont = - DalamudServices.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); - - // Header data. - private string _modName = string.Empty; - private string _modAuthor = string.Empty; - private string _modVersion = string.Empty; - private string _modWebsite = string.Empty; - private string _modWebsiteButton = string.Empty; - private bool _websiteValid; - - private float _modNameWidth; - private float _modAuthorWidth; - private float _modVersionWidth; - private float _modWebsiteButtonWidth; - private float _secondRowWidth; - - // Draw the header for the current mod, - // consisting of its name, version, author and website, if they exist. - private void DrawModHeader() - { - var offset = DrawModName(); - DrawVersion( offset ); - DrawSecondRow( offset ); - } - - // Draw the mod name in the game font with a 2px border, centered, - // with at least the width of the version space to each side. - private float DrawModName() - { - var decidingWidth = Math.Max( _secondRowWidth, ImGui.GetWindowWidth() ); - var offsetWidth = ( decidingWidth - _modNameWidth ) / 2; - var offsetVersion = _modVersion.Length > 0 - ? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X - : 0; - var offset = Math.Max( offsetWidth, offsetVersion ); - if( offset > 0 ) - { - ImGui.SetCursorPosX( offset ); - } - - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.MetaInfoText ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale ); - using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); - ImGuiUtil.DrawTextButton( _modName, Vector2.Zero, 0 ); - return offset; - } - - // Draw the version in the top-right corner. - private void DrawVersion( float offset ) - { - var oldPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos( new Vector2( 2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X, - ImGui.GetStyle().FramePadding.Y ) ); - ImGuiUtil.TextColored( Colors.MetaInfoText, _modVersion ); - ImGui.SetCursorPos( oldPos ); - } - - // Draw author and website if they exist. The website is a button if it is valid. - // Usually, author begins at the left boundary of the name, - // and website ends at the right boundary of the name. - // If their combined width is larger than the name, they are combined-centered. - private void DrawSecondRow( float offset ) - { - if( _modAuthor.Length == 0 ) - { - if( _modWebsiteButton.Length == 0 ) - { - ImGui.NewLine(); - return; - } - - offset += ( _modNameWidth - _modWebsiteButtonWidth ) / 2; - ImGui.SetCursorPosX( offset ); - DrawWebsite(); - } - else if( _modWebsiteButton.Length == 0 ) - { - offset += ( _modNameWidth - _modAuthorWidth ) / 2; - ImGui.SetCursorPosX( offset ); - DrawAuthor(); - } - else if( _secondRowWidth < _modNameWidth ) - { - ImGui.SetCursorPosX( offset ); - DrawAuthor(); - ImGui.SameLine( offset + _modNameWidth - _modWebsiteButtonWidth ); - DrawWebsite(); - } - else - { - offset -= ( _secondRowWidth - _modNameWidth ) / 2; - if( offset > 0 ) - { - ImGui.SetCursorPosX( offset ); - } - - DrawAuthor(); - ImGui.SameLine(); - DrawWebsite(); - } - } - - // Draw the author text. - private void DrawAuthor() - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGuiUtil.TextColored( Colors.MetaInfoText, "by " ); - ImGui.SameLine(); - style.Pop(); - ImGui.TextUnformatted( _mod.Author ); - } - - // Draw either a website button if the source is a valid website address, - // or a source text if it is not. - private void DrawWebsite() - { - if( _websiteValid ) - { - if( ImGui.SmallButton( _modWebsiteButton ) ) - { - try - { - var process = new ProcessStartInfo( _modWebsite ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( _modWebsite ); - } - else - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGuiUtil.TextColored( Colors.MetaInfoText, "from " ); - ImGui.SameLine(); - style.Pop(); - ImGui.TextUnformatted( _mod.Website ); - } - } - - // Update all mod header data. Should someone change frame padding or item spacing, - // or his default font, this will break, but he will just have to select a different mod to restore. - private void UpdateModData() - { - // Name - var name = $" {_mod.Name} "; - if( name != _modName ) - { - using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); - _modName = name; - _modNameWidth = ImGui.CalcTextSize( name ).X + 2 * ( ImGui.GetStyle().FramePadding.X + 2 * ImGuiHelpers.GlobalScale ); - } - - // Author - var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; - if( author != _modAuthor ) - { - _modAuthor = author; - _modAuthorWidth = ImGui.CalcTextSize( author ).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; - } - - // Version - var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; - if( version != _modVersion ) - { - _modVersion = version; - _modVersionWidth = ImGui.CalcTextSize( version ).X; - } - - // Website - if( _modWebsite != _mod.Website ) - { - _modWebsite = _mod.Website; - _websiteValid = Uri.TryCreate( _modWebsite, UriKind.Absolute, out var uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; - _modWebsiteButtonWidth = _websiteValid - ? ImGui.CalcTextSize( _modWebsiteButton ).X + 2 * ImGui.GetStyle().FramePadding.X - : ImGui.CalcTextSize( _modWebsiteButton ).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs deleted file mode 100644 index 10cac9ba..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private bool _emptySetting; - private bool _inherited; - private SingleArray< ModConflicts > _conflicts = new(); - - private int? _currentPriority; - - private void UpdateSettingsData( ModFileSystemSelector selector ) - { - _settings = selector.SelectedSettings; - _collection = selector.SelectedSettingCollection; - _emptySetting = _settings == ModSettings.Empty; - _inherited = _collection != Penumbra.CollectionManager.Current; - _conflicts = Penumbra.CollectionManager.Current.Conflicts( _mod ); - } - - // Draw the whole settings tab as well as its contents. - private void DrawSettingsTab() - { - using var tab = DrawTab( SettingsTabHeader, Tabs.Settings ); - OpenTutorial( BasicTutorialSteps.ModOptions ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##settings" ); - if( !child ) - { - return; - } - - DrawInheritedWarning(); - ImGui.Dummy( _window._defaultSpace ); - _window._penumbra.Api.InvokePreSettingsPanel( _mod.ModPath.Name ); - DrawEnabledInput(); - OpenTutorial( BasicTutorialSteps.EnablingMods ); - ImGui.SameLine(); - DrawPriorityInput(); - OpenTutorial( BasicTutorialSteps.Priority ); - DrawRemoveSettings(); - - if( _mod.Groups.Count > 0 ) - { - var useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Single && g.Value.Count > Penumbra.Config.SingleGroupRadioMax ) ) - { - ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); - useDummy = false; - DrawSingleGroupCombo( group, idx ); - } - - useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.IsOption ) ) - { - ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); - useDummy = false; - switch( group.Type ) - { - case GroupType.Multi: - DrawMultiGroup( group, idx ); - break; - case GroupType.Single when group.Count <= Penumbra.Config.SingleGroupRadioMax: - DrawSingleGroupRadio( group, idx ); - break; - } - } - } - - ImGui.Dummy( _window._defaultSpace ); - _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); - } - - - // Draw a big red bar if the current setting is inherited. - private void DrawInheritedWarning() - { - if( !_inherited ) - { - return; - } - - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var width = new Vector2( ImGui.GetContentRegionAvail().X, 0 ); - if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", width ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false ); - } - - ImGuiUtil.HoverTooltip( "You can click this button to copy the current settings to the current selection.\n" - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection." ); - } - - // Draw a checkbox for the enabled status of the mod. - private void DrawEnabledInput() - { - var enabled = _settings.Enabled; - if( ImGui.Checkbox( "Enabled", ref enabled ) ) - { - Penumbra.ModManager.NewMods.Remove( _mod ); - Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); - } - } - - // Draw a priority input. - // Priority is changed on deactivation of the input box. - private void DrawPriorityInput() - { - using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority; - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) - { - _currentPriority = priority; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue ) - { - if( _currentPriority != _settings.Priority ) - { - Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value ); - } - - _currentPriority = null; - } - - ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B." ); - } - - // Draw a button to remove the current settings and inherit them instead - // on the top-right corner of the window/tab. - private void DrawRemoveSettings() - { - const string text = "Inherit Settings"; - if( _inherited || _emptySetting ) - { - return; - } - - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll ); - if( ImGui.Button( text ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); - } - - ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled." ); - } - - - // Draw a single group selector as a combo box. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupCombo( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; - ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); - using( var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ) ) - { - if( combo ) - { - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) - { - id.Push( idx2 ); - var option = group[ idx2 ]; - if( ImGui.Selectable( option.Name, idx2 == selectedOption ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); - } - - if( option.Description.Length > 0 ) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetColorU32( ImGuiCol.TextDisabled ) ); - ImGuiUtil.RightAlign( FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X ); - } - - if( hovered ) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted( option.Description ); - } - } - - id.Pop(); - } - } - } - - ImGui.SameLine(); - if( group.Description.Length > 0 ) - { - ImGuiUtil.LabeledHelpMarker( group.Name, group.Description ); - } - else - { - ImGui.TextUnformatted( group.Name ); - } - } - - // Draw a single group selector as a set of radio buttons. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupRadio( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; - Widget.BeginFramedGroup( group.Name, group.Description ); - - void DrawOptions() - { - for( var idx = 0; idx < group.Count; ++idx ) - { - using var i = ImRaii.PushId( idx ); - var option = group[ idx ]; - if( ImGui.RadioButton( option.Name, selectedOption == idx ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx ); - } - - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); - } - } - } - - DrawCollapseHandling( group, DrawOptions ); - - Widget.EndFramedGroup(); - } - - private static void DrawCollapseHandling( IModGroup group, Action draw ) - { - if( group.Count <= 5 ) - { - draw(); - } - else - { - var collapseId = ImGui.GetID( "Collapse" ); - var shown = ImGui.GetStateStorage().GetBool( collapseId, true ); - if( shown ) - { - var pos = ImGui.GetCursorPos(); - ImGui.Dummy( new Vector2( ImGui.GetFrameHeight() ) ); - using( var _ = ImRaii.Group() ) - { - draw(); - } - - var width = ImGui.GetItemRectSize().X; - var endPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos( pos ); - if( ImGui.Button( $"Hide {group.Count} Options", new Vector2( width, 0 ) ) ) - { - ImGui.GetStateStorage().SetBool( collapseId, !shown ); - } - - ImGui.SetCursorPos( endPos ); - } - else - { - var max = group.Max( o => ImGui.CalcTextSize( o.Name ).X ) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; - if( ImGui.Button( $"Show {group.Count} Options", new Vector2( max, 0 ) ) ) - { - ImGui.GetStateStorage().SetBool( collapseId, !shown ); - } - } - } - } - - // Draw a multi group selector as a bordered set of checkboxes. - // If a description is provided, add a help marker in the title. - private void DrawMultiGroup( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; - Widget.BeginFramedGroup( group.Name, group.Description ); - - void DrawOptions() - { - for( var idx = 0; idx < group.Count; ++idx ) - { - using var i = ImRaii.PushId( idx ); - var option = group[ idx ]; - var flag = 1u << idx; - var setting = ( flags & flag ) != 0; - - if( ImGui.Checkbox( option.Name, ref setting ) ) - { - flags = setting ? flags | flag : flags & ~flag; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); - } - - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); - } - } - } - - DrawCollapseHandling( group, DrawOptions ); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - ImGui.OpenPopup( $"##multi{groupIdx}" ); - } - - using var style = ImRaii.PushStyle( ImGuiStyleVar.PopupBorderSize, 1 ); - using var popup = ImRaii.Popup( label ); - if( popup ) - { - ImGui.TextUnformatted( group.Name ); - ImGui.Separator(); - if( ImGui.Selectable( "Enable All" ) ) - { - flags = group.Count == 32 ? uint.MaxValue : ( 1u << group.Count ) - 1u; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); - } - - if( ImGui.Selectable( "Disable All" ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, 0 ); - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs deleted file mode 100644 index 28015b79..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - [Flags] - private enum Tabs - { - Description = 0x01, - Settings = 0x02, - ChangedItems = 0x04, - Conflicts = 0x08, - Edit = 0x10, - }; - - // We want to keep the preferred tab selected even if switching through mods. - private Tabs _preferredTab = Tabs.Settings; - private Tabs _availableTabs = 0; - - // Required to use tabs that can not be closed but have a flag to set them open. - private static readonly ByteString ConflictTabHeader = ByteString.FromSpanUnsafe( "Conflicts"u8, true, false, true ); - private static readonly ByteString DescriptionTabHeader = ByteString.FromSpanUnsafe( "Description"u8, true, false, true ); - private static readonly ByteString SettingsTabHeader = ByteString.FromSpanUnsafe( "Settings"u8, true, false, true ); - private static readonly ByteString ChangedItemsTabHeader = ByteString.FromSpanUnsafe( "Changed Items"u8, true, false, true ); - private static readonly ByteString EditModTabHeader = ByteString.FromSpanUnsafe( "Edit Mod"u8, true, false, true ); - - private readonly TagButtons _modTags = new(); - - private void DrawTabBar() - { - var tabBarHeight = ImGui.GetCursorPosY(); - using var tabBar = ImRaii.TabBar( "##ModTabs" ); - if( !tabBar ) - { - return; - } - - _availableTabs = Tabs.Settings - | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) - | Tabs.Description - | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) - | Tabs.Edit; - - DrawSettingsTab(); - DrawDescriptionTab(); - DrawChangedItemsTab(); - DrawConflictsTab(); - DrawEditModTab(); - DrawAdvancedEditingButton(); - DrawFavoriteButton( tabBarHeight ); - } - - private void DrawAdvancedEditingButton() - { - if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) - { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( _mod.Default ); - _window.ModEditPopup.IsOpen = true; - } - - ImGuiUtil.HoverTooltip( - "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" - + "\t\t- file redirections\n" - + "\t\t- file swaps\n" - + "\t\t- metadata manipulations\n" - + "\t\t- model materials\n" - + "\t\t- duplicates\n" - + "\t\t- textures" ); - } - - private void DrawFavoriteButton( float height ) - { - var oldPos = ImGui.GetCursorPos(); - - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - var size = ImGui.CalcTextSize( FontAwesomeIcon.Star.ToIconString() ) + ImGui.GetStyle().FramePadding * 2; - var newPos = new Vector2( ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height ); - if( ImGui.GetScrollMaxX() > 0 ) - { - newPos.X += ImGui.GetScrollX(); - } - - var rectUpper = ImGui.GetWindowPos() + newPos; - var color = ImGui.IsMouseHoveringRect( rectUpper, rectUpper + size ) ? ImGui.GetColorU32( ImGuiCol.Text ) : - _mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32( ImGuiCol.TextDisabled ); - using var c = ImRaii.PushColor( ImGuiCol.Text, color ) - .Push( ImGuiCol.Button, 0 ) - .Push( ImGuiCol.ButtonHovered, 0 ) - .Push( ImGuiCol.ButtonActive, 0 ); - - ImGui.SetCursorPos( newPos ); - if( ImGui.Button( FontAwesomeIcon.Star.ToIconString() ) ) - { - Penumbra.ModManager.ChangeModFavorite( _mod.Index, !_mod.Favorite ); - } - } - - var hovered = ImGui.IsItemHovered(); - OpenTutorial( BasicTutorialSteps.Favorites ); - - if( hovered ) - { - ImGui.SetTooltip( "Favorite" ); - } - } - - - // Just a simple text box with the wrapped description, if it exists. - private void DrawDescriptionTab() - { - using var tab = DrawTab( DescriptionTabHeader, Tabs.Description ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##description" ); - if( !child ) - { - return; - } - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - var tagIdx = _localTags.Draw( "Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" - + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _mod.LocalTags, - out var editedTag ); - OpenTutorial( BasicTutorialSteps.Tags ); - if( tagIdx >= 0 ) - { - Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); - } - - if( _mod.ModTags.Count > 0 ) - { - _modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false, - ImGui.CalcTextSize( "Local " ).X - ImGui.CalcTextSize( "Mod " ).X ); - } - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - ImGui.Separator(); - - ImGuiUtil.TextWrapped( _mod.Description ); - } - - // A simple clipped list of changed items. - private void DrawChangedItemsTab() - { - using var tab = DrawTab( ChangedItemsTabHeader, Tabs.ChangedItems ); - if( !tab ) - { - return; - } - - using var list = ImRaii.ListBox( "##changedItems", -Vector2.One ); - if( !list ) - { - return; - } - - var zipList = ZipList.FromSortedList( _mod.ChangedItems ); - var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2, true ), height ); - } - - // If any conflicts exist, show them in this tab. - private unsafe void DrawConflictsTab() - { - using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts ); - if( !tab ) - { - return; - } - - using var box = ImRaii.ListBox( "##conflicts", -Vector2.One ); - if( !box ) - { - return; - } - - foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) - { - if( ImGui.Selectable( conflict.Mod2.Name ) && conflict.Mod2 is Mod mod ) - { - _window._selector.SelectByValue( mod ); - } - - ImGui.SameLine(); - using( var color = ImRaii.PushColor( ImGuiCol.Text, - conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) - { - var priority = conflict.Mod2.Index < 0 - ? conflict.Mod2.Priority - : Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority; - ImGui.TextUnformatted( $"(Priority {priority})" ); - } - - using var indent = ImRaii.PushIndent( 30f ); - foreach( var data in conflict.Conflicts ) - { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) > 0, - MetaManipulation m => ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ), - _ => false, - }; - } - } - } - - - // Draw a tab by given name if it is available, and deal with changing the preferred tab. - private ImRaii.IEndObject DrawTab( ByteString name, Tabs flag ) - { - if( !_availableTabs.HasFlag( flag ) ) - { - return ImRaii.IEndObject.Empty; - } - - var flags = _preferredTab == flag ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None; - unsafe - { - var tab = ImRaii.TabItem( name.Path, flags ); - if( ImGui.IsItemClicked() ) - { - _preferredTab = flag; - } - - return tab; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.cs b/Penumbra/UI/ConfigWindow.ModPanel.cs deleted file mode 100644 index d9b1f7ac..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using OtterGui.Widgets; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - - // The basic setup for the mod panel. - // Details are in other files. - private partial class ModPanel : IDisposable - { - private readonly ConfigWindow _window; - - private bool _valid; - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; - private readonly TagButtons _localTags = new(); - - public ModPanel( ConfigWindow window ) - => _window = window; - - public void Dispose() - { - _nameFont.Dispose(); - } - - public void Draw( ModFileSystemSelector selector ) - { - Init( selector ); - if( !_valid ) - { - return; - } - - DrawModHeader(); - DrawTabBar(); - } - - private void Init( ModFileSystemSelector selector ) - { - _valid = selector.Selected != null; - if( !_valid ) - { - return; - } - - _leaf = selector.SelectedLeaf!; - _mod = selector.Selected!; - UpdateSettingsData( selector ); - UpdateModData(); - } - - public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) - { - if( old == mod ) - { - return; - } - - if( mod == null ) - { - _window.ModEditPopup.IsOpen = false; - } - else if( _window.ModEditPopup.IsOpen ) - { - _window.ModEditPopup.ChangeMod( mod ); - } - - _currentPriority = null; - MoveDirectory.Reset(); - OptionTable.Reset(); - Input.Reset(); - AddOptionGroup.Reset(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs deleted file mode 100644 index 5be9c8e0..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ /dev/null @@ -1,205 +0,0 @@ -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.UI.Classes; -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class ModsTab : ITab - { - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _panel; - private readonly Penumbra _penumbra; - - public ModsTab(ModFileSystemSelector selector, ModPanel panel, Penumbra penumbra) - { - _selector = selector; - _panel = panel; - _penumbra = penumbra; - } - - public bool IsVisible - => Penumbra.ModManager.Valid; - - public ReadOnlySpan Label - => "Mods"u8; - - public void DrawHeader() - => OpenTutorial( BasicTutorialSteps.Mods ); - - public void DrawContent() - { - try - { - _selector.Draw( GetModSelectorSize() ); - ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawHeaderLine(); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - - using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight() ), - true, ImGuiWindowFlags.HorizontalScrollbar ) ) - { - style.Pop(); - if( child ) - { - _panel.Draw( _selector ); - } - - style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - } - - style.Push( ImGuiStyleVar.FrameRounding, 0 ); - DrawRedrawLine(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Exception thrown during ModPanel Render:\n{e}" ); - Penumbra.Log.Error( $"{Penumbra.ModManager.Count} Mods\n" - + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" - + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" - + $"{_selector.SortMode.Name} Sort Mode\n" - + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" - + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); - } - } - - private void DrawRedrawLine() - { - if( Penumbra.Config.HideRedrawBar ) - { - SkipTutorial( BasicTutorialSteps.Redrawing ); - return; - } - - var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); - var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); - using( var _ = ImRaii.Group() ) - { - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); - ImGui.SameLine(); - } - - ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); - } - - var hovered = ImGui.IsItemHovered(); - OpenTutorial( BasicTutorialSteps.Redrawing ); - if( hovered ) - { - ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); - } - - void DrawButton( Vector2 size, string label, string lower ) - { - if( ImGui.Button( label, size ) ) - { - if( lower.Length > 0 ) - { - _penumbra.RedrawService.RedrawObject( lower, RedrawType.Redraw ); - } - else - { - _penumbra.RedrawService.RedrawAll( RedrawType.Redraw ); - } - } - - ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); - } - - using var disabled = ImRaii.Disabled( DalamudServices.ClientState.LocalPlayer == null ); - ImGui.SameLine(); - var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; - DrawButton( buttonWidth, "All", string.Empty ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Self", "self" ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Target", "target" ); - ImGui.SameLine(); - DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); - } - - // Draw the header line that can quick switch between collections. - private void DrawHeaderLine() - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - - using( var _ = ImRaii.Group() ) - { - DrawDefaultCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawInheritedCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false ); - } - - OpenTutorial( BasicTutorialSteps.CollectionSelectors ); - - if( !Penumbra.CollectionManager.CurrentCollectionInUse ) - { - ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); - } - } - - private static void DrawDefaultCollectionButton( Vector2 width ) - { - var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})"; - var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; - var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; - var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}." - : isEmpty ? $"The {DefaultCollection} is configured to be empty." - : $"Set the {SelectedCollection} to the configured {DefaultCollection}."; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) - { - Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); - } - } - - private void DrawInheritedCollectionButton( Vector2 width ) - { - var noModSelected = _selector.Selected == null; - var collection = _selector.SelectedSettingCollection; - var modInherited = collection != Penumbra.CollectionManager.Current; - var (name, tt) = (noModSelected, modInherited) switch - { - (true, _ ) => ("Inherited Collection", "No mod selected."), - (false, true ) => ($"Inherited Collection ({collection.Name})", - "Set the current collection to the collection the selected mod inherits its settings from."), - (false, false ) => ("Not Inherited", "The selected mod does not inherit its settings."), - }; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) - { - Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); - } - } - - // Get the correct size for the mod selector based on current config. - private static float GetModSelectorSize() - { - var absoluteSize = Math.Clamp( Penumbra.Config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min( Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100 ) ); - var relativeSize = Penumbra.Config.ScaleModSelector - ? Math.Clamp( Penumbra.Config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize ) - : 0; - return !Penumbra.Config.ScaleModSelector - ? absoluteSize - : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs deleted file mode 100644 index b6d2e46c..00000000 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using FFXIVClientStructs.Interop; -using FFXIVClientStructs.STD; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Services; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class ResourceTab : ITab - { - public ReadOnlySpan Label - => "Resource Manager"u8; - - public bool IsVisible - => Penumbra.Config.DebugMode; - - private float _hashColumnWidth; - private float _pathColumnWidth; - private float _refsColumnWidth; - private string _resourceManagerFilter = string.Empty; - - // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. - public void DrawContent() - { - // Filter for resources containing the input string. - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); - - using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); - if( !child ) - { - return; - } - - unsafe - { - Penumbra.ResourceManagerService.IterateGraphs( DrawCategoryContainer ); - } - ImGui.NewLine(); - unsafe - { - ImGui.TextUnformatted( $"Static Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress:X} (+0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); - ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManager:X}" ); - } - } - - private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) - { - if( map == null ) - { - return; - } - - var label = GetNodeLabel( ( uint )category, ext, map->Count ); - using var tree = ImRaii.TreeNode( label ); - if( !tree || map->Count == 0 ) - { - return; - } - - using var table = ImRaii.Table( "##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth ); - ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); - ImGui.TableHeadersRow(); - - Penumbra.ResourceManagerService.IterateResourceMap( map, ( hash, r ) => - { - // Filter unwanted names. - if( _resourceManagerFilter.Length != 0 - && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - var address = $"0x{( ulong )r:X}"; - ImGuiUtil.TextNextColumn( $"0x{hash:X8}" ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( address ); - - var resource = ( Interop.Structs.ResourceHandle* )r; - ImGui.TableNextColumn(); - Text( resource ); - if( ImGui.IsItemClicked() ) - { - var data = Interop.Structs.ResourceHandle.GetData( resource ); - if( data != null ) - { - var length = ( int )Interop.Structs.ResourceHandle.GetLength( resource ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - } - - ImGuiUtil.HoverTooltip( "Click to copy byte-wise file data to clipboard, if any." ); - - ImGuiUtil.TextNextColumn( r->RefCount.ToString() ); - } ); - } - - // Draw a full category for the resource manager. - private unsafe void DrawCategoryContainer( ResourceCategory category, - StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, int idx ) - { - if( map == null ) - { - return; - } - - using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} (Ex {idx}) - {map->Count}###{( uint )category}_{idx}" ); - if( tree ) - { - SetTableWidths(); - Penumbra.ResourceManagerService.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); - } - } - - // Obtain a label for an extension node. - private static string GetNodeLabel( uint label, uint type, ulong count ) - { - var (lowest, mid1, mid2, highest) = Functions.SplitBytes( type ); - return highest == 0 - ? $"({type:X8}) {( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}" - : $"({type:X8}) {( char )highest}{( char )mid2}{( char )mid1}{( char )lowest} - {count}###{label}{type}"; - } - - // Set the widths for a resource table. - private void SetTableWidths() - { - _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; - _pathColumnWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 300 * ImGuiHelpers.GlobalScale; - _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs deleted file mode 100644 index 56a757b3..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Numerics; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Interop; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab - { - private void DrawAdvancedSettings() - { - var header = ImGui.CollapsingHeader( "Advanced" ); - OpenTutorial( BasicTutorialSteps.AdvancedSettings ); - - if( !header ) - { - 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 ); - Checkbox( "Keep Default Metadata Changes on Import", - "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " - + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", - Penumbra.Config.KeepDefaultMetaChanges, v => Penumbra.Config.KeepDefaultMetaChanges = v ); - DrawWaitForPluginsReflection(); - DrawEnableHttpApiBox(); - DrawEnableDebugModeBox(); - DrawReloadResourceButton(); - DrawReloadFontsButton(); - ImGui.NewLine(); - } - - // Creates and destroys the web server when toggled. - private void DrawEnableHttpApiBox() - { - var http = Penumbra.Config.EnableHttpApi; - if( ImGui.Checkbox( "##http", ref http ) ) - { - if( http ) - { - _window._penumbra.HttpApi.CreateWebServer(); - } - else - { - _window._penumbra.HttpApi.ShutdownWebServer(); - } - - Penumbra.Config.EnableHttpApi = http; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable HTTP API", - "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); - } - - // Should only be used for debugging. - private static void DrawEnableDebugModeBox() - { - var tmp = Penumbra.Config.DebugMode; - if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) - { - Penumbra.Config.DebugMode = tmp; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Debug Mode", - "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( "Reload Resident Resources" ) && Penumbra.CharacterUtility.Ready ) - { - Penumbra.ResidentResources.Reload(); - } - - ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" - + "You usually should not need to do this." ); - } - - private void DrawReloadFontsButton() - { - if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid ) ) - { - _fontReloader.Reload(); - } - } - - private static void DrawWaitForPluginsReflection() - { - if( !Penumbra.Dalamud.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) - { - using var disabled = ImRaii.Disabled(); - Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); - } - else - { - Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => Penumbra.Dalamud.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs deleted file mode 100644 index 6995d7a3..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab - { - private static void Checkbox( string label, string tooltip, bool current, Action< bool > setter ) - { - using var id = ImRaii.PushId( label ); - var tmp = current; - if( ImGui.Checkbox( string.Empty, ref tmp ) && tmp != current ) - { - setter( tmp ); - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( label, tooltip ); - } - - private static int _singleGroupRadioMax = int.MaxValue; - - private void DrawSingleSelectRadioMax() - { - if( _singleGroupRadioMax == int.MaxValue ) - { - _singleGroupRadioMax = Penumbra.Config.SingleGroupRadioMax; - } - - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.DragInt( "##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1 ) ) - { - _singleGroupRadioMax = Math.Max( 1, _singleGroupRadioMax ); - } - - if( ImGui.IsItemDeactivated() ) - { - if( _singleGroupRadioMax != Penumbra.Config.SingleGroupRadioMax ) - { - Penumbra.Config.SingleGroupRadioMax = _singleGroupRadioMax; - Penumbra.Config.Save(); - } - - _singleGroupRadioMax = int.MaxValue; - } - - ImGuiUtil.LabeledHelpMarker( "Upper Limit for Single-Selection Group Radio Buttons", - "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" - + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons." ); - } - - private void DrawModSelectorSettings() - { -#if DEBUG - ImGui.NewLine(); // Due to the timing button. -#endif - if( !ImGui.CollapsingHeader( "General" ) ) - { - OpenTutorial( BasicTutorialSteps.GeneralSettings ); - return; - } - - OpenTutorial( BasicTutorialSteps.GeneralSettings ); - - Checkbox( "Hide Config Window when UI is Hidden", - "Hide the penumbra main window when you manually hide the in-game user interface.", Penumbra.Config.HideUiWhenUiHidden, - v => - { - Penumbra.Config.HideUiWhenUiHidden = v; - DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !v; - } ); - Checkbox( "Hide Config Window when in Cutscenes", - "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, - v => - { - Penumbra.Config.HideUiInCutscenes = v; - DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !v; - } ); - Checkbox( "Hide Config Window when in GPose", - "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, - v => - { - Penumbra.Config.HideUiInGPose = v; - DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !v; - } ); - ImGui.Dummy( _window._defaultSpace ); - - Checkbox( "Print Chat Command Success Messages to Chat", - "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", - Penumbra.Config.PrintSuccessfulCommandsToChat, v => Penumbra.Config.PrintSuccessfulCommandsToChat = v ); - Checkbox( "Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", - Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); - ImGui.Dummy( _window._defaultSpace ); - Checkbox( $"Use {AssignedCollections} in Character Window", - "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", - Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); - Checkbox( $"Use {AssignedCollections} in Adventurer Cards", - "Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.", - Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); - Checkbox( $"Use {AssignedCollections} in Try-On Window", - "Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", - Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v ); - Checkbox( "Use No Mods in Inspect Windows", "Use the empty collection for characters you are inspecting, regardless of the character.\n" - + "Takes precedence before the next option.", Penumbra.Config.UseNoModsInInspect, v => Penumbra.Config.UseNoModsInInspect = v ); - Checkbox( $"Use {AssignedCollections} in Inspect Windows", - "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", - Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); - Checkbox( $"Use {AssignedCollections} based on Ownership", - "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", - Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); - ImGui.Dummy( _window._defaultSpace ); - DrawSingleSelectRadioMax(); - DrawFolderSortType(); - DrawAbsoluteSizeSelector(); - DrawRelativeSizeSelector(); - Checkbox( "Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", - Penumbra.Config.OpenFoldersByDefault, v => - { - Penumbra.Config.OpenFoldersByDefault = v; - _window._selector.SetFilterDirty(); - } ); - - Widget.DoubleModifierSelector( "Mod Deletion Modifier", - "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", _window._inputTextWidth.X, - Penumbra.Config.DeleteModModifier, - v => - { - Penumbra.Config.DeleteModModifier = v; - Penumbra.Config.Save(); - } ); - ImGui.Dummy( _window._defaultSpace ); - Checkbox( "Always Open Import at Default Directory", - "Open the import window at the location specified here every time, forgetting your previous path.", - Penumbra.Config.AlwaysOpenDefaultImport, v => Penumbra.Config.AlwaysOpenDefaultImport = v ); - DrawDefaultModImportPath(); - DrawDefaultModAuthor(); - DrawDefaultModImportFolder(); - DrawDefaultModExportPath(); - - ImGui.NewLine(); - } - - // Store separately to use IsItemDeactivatedAfterEdit. - private float _absoluteSelectorSize = Penumbra.Config.ModSelectorAbsoluteSize; - private int _relativeSelectorSize = Penumbra.Config.ModSelectorScaledSize; - - // Different supported sort modes as a combo. - private void DrawFolderSortType() - { - var sortMode = Penumbra.Config.SortMode; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using var combo = ImRaii.Combo( "##sortMode", sortMode.Name ); - if( combo ) - { - foreach( var val in Configuration.Constants.ValidSortModes ) - { - if( ImGui.Selectable( val.Name, val.GetType() == sortMode.GetType() ) && val.GetType() != sortMode.GetType() ) - { - Penumbra.Config.SortMode = val; - _window._selector.SetFilterDirty(); - Penumbra.Config.Save(); - } - - ImGuiUtil.HoverTooltip( val.Description ); - } - } - - combo.Dispose(); - ImGuiUtil.LabeledHelpMarker( "Sort Mode", "Choose the sort mode for the mod selector in the mods tab." ); - } - - // Absolute size in pixels. - private void DrawAbsoluteSizeSelector() - { - if( ImGuiUtil.DragFloat( "##absoluteSize", ref _absoluteSelectorSize, _window._inputTextWidth.X, 1, - Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f" ) - && _absoluteSelectorSize != Penumbra.Config.ModSelectorAbsoluteSize ) - { - Penumbra.Config.ModSelectorAbsoluteSize = _absoluteSelectorSize; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Absolute Size", - "The minimal absolute size of the mod selector in the mod tab in pixels." ); - } - - // Relative size toggle and percentage. - private void DrawRelativeSizeSelector() - { - var scaleModSelector = Penumbra.Config.ScaleModSelector; - if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) - { - Penumbra.Config.ScaleModSelector = scaleModSelector; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - if( ImGuiUtil.DragInt( "##relativeSize", ref _relativeSelectorSize, _window._inputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, - Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%" ) - && _relativeSelectorSize != Penumbra.Config.ModSelectorScaledSize ) - { - Penumbra.Config.ModSelectorScaledSize = _relativeSelectorSize; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); - } - - private void DrawDefaultModImportPath() - { - var tmp = Penumbra.Config.DefaultModImportPath; - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); - if( ImGui.InputText( "##defaultModImport", ref tmp, 256 ) ) - { - Penumbra.Config.DefaultModImportPath = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##import", _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - var startDir = Directory.Exists( Penumbra.Config.ModDirectory ) ? Penumbra.Config.ModDirectory : "."; - - _dialogManager.OpenFolderDialog( "Choose Default Import Directory", ( b, s ) => - { - Penumbra.Config.DefaultModImportPath = b ? s : Penumbra.Config.DefaultModImportPath; - Penumbra.Config.Save(); - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - - style.Pop(); - ImGuiUtil.LabeledHelpMarker( "Default Mod Import Directory", - "Set the directory that gets opened when using the file picker to import mods for the first time." ); - } - - private string _tempExportDirectory = string.Empty; - - private void DrawDefaultModExportPath() - { - var tmp = Penumbra.Config.ExportDirectory; - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); - if( ImGui.InputText( "##defaultModExport", ref tmp, 256 ) ) - { - _tempExportDirectory = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.ModManager.UpdateExportDirectory( _tempExportDirectory, true ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##export", _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - var startDir = Penumbra.Config.ExportDirectory.Length > 0 && Directory.Exists( Penumbra.Config.ExportDirectory ) - ? Penumbra.Config.ExportDirectory - : Directory.Exists( Penumbra.Config.ModDirectory ) - ? Penumbra.Config.ModDirectory - : "."; - - _dialogManager.OpenFolderDialog( "Choose Default Export Directory", ( b, s ) => - { - Penumbra.ModManager.UpdateExportDirectory( b ? s : Penumbra.Config.ExportDirectory, true ); - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - - style.Pop(); - ImGuiUtil.LabeledHelpMarker( "Default Mod Export Directory", - "Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n" - + "Keep this empty to use the root directory." ); - } - - private void DrawDefaultModAuthor() - { - var tmp = Penumbra.Config.DefaultModAuthor; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputText( "##defaultAuthor", ref tmp, 64 ) ) - { - Penumbra.Config.DefaultModAuthor = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGuiUtil.LabeledHelpMarker( "Default Mod Author", "Set the default author stored for newly created mods." ); - } - - private void DrawDefaultModImportFolder() - { - var tmp = Penumbra.Config.DefaultImportFolder; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputText( "##defaultImportFolder", ref tmp, 64 ) ) - { - Penumbra.Config.DefaultImportFolder = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGuiUtil.LabeledHelpMarker( "Default Mod Import Folder", - "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root." ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs deleted file mode 100644 index e6948019..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ /dev/null @@ -1,374 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Interop.Services; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab : ITab - { - public const int RootDirectoryMaxLength = 64; - private readonly ConfigWindow _window; - private readonly FontReloader _fontReloader; - public ReadOnlySpan Label - => "Settings"u8; - public SettingsTab( ConfigWindow window, FontReloader fontReloader ) - { - _window = window; - _fontReloader = fontReloader; - } - - public void DrawHeader() - { - OpenTutorial( BasicTutorialSteps.Fin ); - OpenTutorial( BasicTutorialSteps.Faq1 ); - OpenTutorial( BasicTutorialSteps.Faq2 ); - OpenTutorial( BasicTutorialSteps.Faq3 ); - } - - public void DrawContent() - { - using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - DrawEnabledBox(); - Checkbox( "Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => - { - Penumbra.Config.FixMainWindow = v; - _window.Flags = v - ? _window.Flags | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize - : _window.Flags & ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); - } ); - - ImGui.NewLine(); - DrawRootFolder(); - DrawRediscoverButton(); - ImGui.NewLine(); - - DrawModSelectorSettings(); - DrawColorSettings(); - DrawAdvancedSettings(); - - _dialogManager.Draw(); - DrawSupportButtons(); - } - - // Changing the base mod directory. - private string? _newModDirectory; - private readonly FileDialogManager _dialogManager = SetupFileManager(); - private bool _dialogOpen; // For toggling on/off. - - // 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 newName, string old, float width, bool saved ) - { - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var w = new Vector2( width, 0 ); - var (text, valid) = CheckPath( newName, old ); - - return ( ImGui.Button( text, w ) || saved ) && valid; - } - - private static (string Text, bool Valid) CheckPath( string newName, string old ) - { - static bool IsSubPathOf( string basePath, string subPath ) - { - if( basePath.Length == 0 ) - { - return false; - } - - var rel = Path.GetRelativePath( basePath, subPath ); - return rel == "." || !rel.StartsWith( '.' ) && !Path.IsPathRooted( rel ); - } - - if( newName.Length > RootDirectoryMaxLength ) - { - return ( $"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false ); - } - - if( Path.GetDirectoryName( newName ) == null ) - { - return ( "Path is not allowed to be a drive root. Please add a directory.", false ); - } - - var desktop = Environment.GetFolderPath( Environment.SpecialFolder.Desktop ); - if( IsSubPathOf( desktop, newName ) ) - { - return ( "Path is not allowed to be on your Desktop.", false ); - } - - var programFiles = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ); - var programFilesX86 = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFilesX86 ); - if( IsSubPathOf( programFiles, newName ) || IsSubPathOf( programFilesX86, newName ) ) - { - return ( "Path is not allowed to be in ProgramFiles.", false ); - } - - var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; - if( IsSubPathOf( dalamud.FullName, newName ) ) - { - return ( "Path is not allowed to be inside your Dalamud directories.", false ); - } - - if( Functions.GetDownloadsFolder( out var downloads ) && IsSubPathOf( downloads, newName ) ) - { - return ( "Path is not allowed to be inside your Downloads folder.", false ); - } - - var gameDir = DalamudServices.GameData.GameData.DataPath.Parent!.Parent!.FullName; - if( IsSubPathOf( gameDir, newName ) ) - { - return ( "Path is not allowed to be inside your game folder.", false ); - } - - return ( $"Press Enter or Click Here to Save (Current Directory: {old})", true ); - } - - // Draw a directory picker button that toggles the directory picker. - // Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. - private void DrawDirectoryPickerButton() - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - _newModDirectory ??= Penumbra.Config.ModDirectory; - // Use the current input as start directory if it exists, - // otherwise the current mod directory, otherwise the current application directory. - var startDir = Directory.Exists( _newModDirectory ) - ? _newModDirectory - : Directory.Exists( Penumbra.Config.ModDirectory ) - ? Penumbra.Config.ModDirectory - : "."; - - _dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => - { - _newModDirectory = b ? s : _newModDirectory; - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - } - - private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) - { - using var _ = ImRaii.PushId( id ); - var ret = ImGui.Button( "Open Directory" ); - ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); - if( ret && condition && Directory.Exists( directory.FullName ) ) - { - Process.Start( new ProcessStartInfo( directory.FullName ) - { - UseShellExecute = true, - } ); - } - } - - // Draw the text input for the mod directory, - // as well as the directory picker button and the enter warning. - private void DrawRootFolder() - { - if( _newModDirectory.IsNullOrEmpty() ) - { - _newModDirectory = Penumbra.Config.ModDirectory; - } - - 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, 64, ImGuiInputTextFlags.EnterReturnsTrue ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker( tt ); - OpenTutorial( BasicTutorialSteps.GeneralTooltips ); - ImGui.SameLine(); - ImGui.TextUnformatted( "Root Directory" ); - ImGuiUtil.HoverTooltip( tt ); - - group.Dispose(); - OpenTutorial( BasicTutorialSteps.ModDirectory ); - ImGui.SameLine(); - var pos = ImGui.GetCursorPosX(); - ImGui.NewLine(); - - if( Penumbra.Config.ModDirectory != _newModDirectory - && _newModDirectory.Length != 0 - && DrawPressEnterWarning( _newModDirectory, Penumbra.Config.ModDirectory, pos, save ) ) - { - Penumbra.ModManager.DiscoverMods( _newModDirectory ); - } - } - - private static void DrawRediscoverButton() - { - DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); - ImGui.SameLine(); - var tt = Penumbra.ModManager.Valid - ? "Force Penumbra to completely re-scan your root directory as if it was restarted." - : "The currently selected folder is not valid. Please select a different folder."; - if( ImGuiUtil.DrawDisabledButton( "Rediscover Mods", Vector2.Zero, tt, !Penumbra.ModManager.Valid ) ) - { - Penumbra.ModManager.DiscoverMods(); - } - } - - private void DrawEnabledBox() - { - var enabled = Penumbra.Config.EnableMods; - if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) - { - _window._penumbra.SetEnabled( enabled ); - } - - OpenTutorial( BasicTutorialSteps.EnableMods ); - } - - private static void DrawColorSettings() - { - if( !ImGui.CollapsingHeader( "Colors" ) ) - { - return; - } - - foreach( var color in Enum.GetValues< ColorId >() ) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; - if( Widget.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) - { - Penumbra.Config.Save(); - } - } - - ImGui.NewLine(); - } - - public static void DrawDiscordButton( float width ) - { - const string address = @"https://discord.gg/kVva7DHV4r"; - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); - if( ImGui.Button( "Join Discord for Support", new Vector2( width, 0 ) ) ) - { - try - { - var process = new ProcessStartInfo( address ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( $"Open {address}" ); - } - - private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; - - public static void DrawSupportButton(Penumbra penumbra) - { - if( ImGui.Button( SupportInfoButtonText ) ) - { - var text = penumbra.GatherSupportInformation(); - ImGui.SetClipboardText( text ); - } - } - - private static void DrawGuideButton( float width ) - { - const string address = @"https://reniguide.info/"; - using var color = ImRaii.PushColor( ImGuiCol.Button, 0xFFCC648D ) - .Push( ImGuiCol.ButtonHovered, 0xFFB070B0 ) - .Push( ImGuiCol.ButtonActive, 0xFF9070E0 ); - if( ImGui.Button( "Beginner's Guides", new Vector2( width, 0 ) ) ) - { - try - { - var process = new ProcessStartInfo( address ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( - $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" - + "Not directly affiliated and potentially, but not usually out of date." ); - } - - private void DrawSupportButtons() - { - var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; - var xPos = ImGui.GetWindowWidth() - width; - if( ImGui.GetScrollMaxY() > 0 ) - { - xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; - } - - ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); - DrawSupportButton(_window._penumbra); - - ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); - DrawDiscordButton( width ); - - ImGui.SetCursorPos( new Vector2( xPos, 2 * ImGui.GetFrameHeightWithSpacing() ) ); - DrawGuideButton( width ); - - ImGui.SetCursorPos( new Vector2( xPos, 3 * ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( "Restart Tutorial", new Vector2( width, 0 ) ) ) - { - Penumbra.Config.TutorialStep = 0; - Penumbra.Config.Save(); - } - - ImGui.SetCursorPos( new Vector2( xPos, 4 * ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( "Show Changelogs", new Vector2( width, 0 ) ) ) - { - _window._penumbra.ForceChangelogOpen(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs deleted file mode 100644 index 6c10f740..00000000 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - public const string SelectedCollection = "Selected Collection"; - public const string DefaultCollection = "Base Collection"; - public const string InterfaceCollection = "Interface Collection"; - public const string ActiveCollections = "Active Collections"; - public const string AssignedCollections = "Assigned Collections"; - public const string GroupAssignment = "Group Assignment"; - public const string CharacterGroups = "Character Groups"; - public const string ConditionalGroup = "Group"; - public const string ConditionalIndividual = "Character"; - public const string IndividualAssignments = "Individual Assignments"; - - public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" - + " - 'self' or '': your own character\n" - + " - 'target' or '': your target\n" - + " - 'focus' or ': your focus target\n" - + " - 'mouseover' or '': the actor you are currently hovering over\n" - + " - any specific actor name to redraw all actors of that exactly matching name."; - - private static void UpdateTutorialStep() - { - var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); - if( tutorial != Penumbra.Config.TutorialStep ) - { - Penumbra.Config.TutorialStep = tutorial; - Penumbra.Config.Save(); - } - } - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public static void OpenTutorial( BasicTutorialSteps step ) - => Tutorial.Open( ( int )step, Penumbra.Config.TutorialStep, v => - { - Penumbra.Config.TutorialStep = v; - Penumbra.Config.Save(); - } ); - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public static void SkipTutorial( BasicTutorialSteps step ) - => Tutorial.Skip( ( int )step, Penumbra.Config.TutorialStep, v => - { - Penumbra.Config.TutorialStep = v; - Penumbra.Config.Save(); - } ); - - public enum BasicTutorialSteps - { - GeneralTooltips, - ModDirectory, - EnableMods, - AdvancedSettings, - GeneralSettings, - Collections, - EditingCollections, - CurrentCollection, - Inheritance, - ActiveCollections, - DefaultCollection, - InterfaceCollection, - SpecialCollections1, - SpecialCollections2, - Mods, - ModImport, - AdvancedHelp, - ModFilters, - CollectionSelectors, - Redrawing, - EnablingMods, - Priority, - ModOptions, - Fin, - Faq1, - Faq2, - Faq3, - Favorites, - Tags, - } - - public static readonly Tutorial Tutorial = new Tutorial() - { - BorderColor = Colors.TutorialBorder, - HighlightColor = Colors.TutorialMarker, - PopupLabel = "Settings Tutorial", - } - .Register( "General Tooltips", "This symbol gives you further information about whatever setting it appears next to.\n\n" - + "Hover over them when you are unsure what something does or how to do something." ) - .Register( "Initial Setup, Step 1: Mod Directory", - "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n" - + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n" - + "The folder should be an empty folder no other applications write to." ) - .Register( "Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not." ) - .Deprecated() - .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" - + "If you do not know what some of these do yet, return to this later!" ) - .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" - + "This is our next stop!\n\n" - + "Go here after setting up your root folder to continue the tutorial!" ) - .Register( "Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" - + "In here, we can create new collections, delete collections, or make them inherit from each other." ) - .Register( $"Initial Setup, Step 5: {SelectedCollection}", - $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." - + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n" ) - .Register( "Inheritance", - "This is a more advanced feature. Click the help button for more information, but we will ignore this for now." ) - .Register( $"Initial Setup, Step 6: {ActiveCollections}", - $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" - + "Any collection assigned here will apply to the game under certain conditions.\n\n" - + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" - + "Open this now to continue." ) - .Register( $"Initial Setup, Step 7: {DefaultCollection}", - $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" - + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" - + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods." ) - .Register( "Interface Collection", - $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" - + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one." ) - .Register( GroupAssignment + 's', - "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" - + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" - + $"{IndividualAssignments} always take precedence before groups." ) - .Register( IndividualAssignments, - "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" - + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n" ) - .Register( "Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" - + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking." ) - .Register( "Initial Setup, Step 9: Mod Import", - "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" - + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" - + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress." ) // TODO - .Register( "Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" - + "Import and select a mod now to continue." ) - .Register( "Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here." ) - .Register( "Collection Selectors", $"This row provides shortcuts to set your {SelectedCollection}.\n\n" - + $"The first button sets it to your {DefaultCollection} (if any).\n\n" - + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" - + "The third is a regular collection selector to let you choose among all your collections." ) - .Register( "Redrawing", - "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" - + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" - + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too." ) - .Register( "Initial Setup, Step 11: Enabling Mods", - "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" - + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance." ) - .Register( "Initial Setup, Step 12: Priority", "If two enabled mods in one collection change the same files, there is a conflict.\n\n" - + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" - + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible." ) - .Register( "Mod Options", "Many mods have options themselves. You can also choose those here.\n\n" - + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately." ) - .Register( "Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n" - + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page." ) - .Register( "FAQ 1", "Penumbra can not easily change which items a mod applies to." ) - .Register( "FAQ 2", - "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) - .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ) - .Register( "Favorites", "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections." ) - .Register( "Tags", "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'." ) - .EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length ); -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ec149ce2..5036e706 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,144 +1,131 @@ using System; using System.Numerics; -using Dalamud.Interface; using Dalamud.Interface.Windowing; +using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop.Services; using Penumbra.Mods; -using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.Tabs; using Penumbra.Util; namespace Penumbra.UI; -public sealed partial class ConfigWindow : Window, IDisposable +public sealed class ConfigWindow : Window { - private readonly Penumbra _penumbra; - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _modPanel; - public readonly ModEditWindow ModEditPopup; - private readonly Configuration _config; + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _config; + private readonly PerformanceTracker _tracker; + private readonly ValidityChecker _validityChecker; + private readonly Penumbra _penumbra; + private readonly ConfigTabBar _configTabs; + private string? _lastException; - private readonly SettingsTab _settingsTab; - private readonly CollectionsTab _collectionsTab; - private readonly ModsTab _modsTab; - private readonly ChangedItemsTab _changedItemsTab; - private readonly EffectiveTab _effectiveTab; - private readonly DebugTab _debugTab; - private readonly ResourceTab _resourceTab; - private readonly ResourceWatcher _resourceWatcher; + public void SelectTab(TabType tab) + => _configTabs.SelectTab = tab; - public TabType SelectTab = TabType.None; public void SelectMod(Mod mod) - => _selector.SelectByValue(mod); + => _configTabs.Mods.SelectMod = mod; - public ConfigWindow(Configuration config, CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, - Penumbra penumbra, ResourceWatcher watcher) - : base(GetLabel()) + + public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, + TutorialService tutorial, Penumbra penumbra, ConfigTabBar configTabs) + : base(GetLabel(checker)) { - _penumbra = penumbra; + _pluginInterface = pi; _config = config; - _resourceWatcher = watcher; + _tracker = tracker; + _validityChecker = checker; + _penumbra = penumbra; + _configTabs = configTabs; - ModEditPopup = new ModEditWindow(communicator); - _settingsTab = new SettingsTab(this, fontReloader); - _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); - _modPanel = new ModPanel(this); - _modsTab = new ModsTab(_selector, _modPanel, _penumbra); - _selector.SelectionChanged += _modPanel.OnSelectionChange; - _collectionsTab = new CollectionsTab(communicator, this); - _changedItemsTab = new ChangedItemsTab(this); - _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab(this, timer); - _resourceTab = new ResourceTab(); - if (Penumbra.Config.FixMainWindow) - Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; - - DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; - DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; - DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(800, 600), MaximumSize = new Vector2(4096, 2160), }; - UpdateTutorialStep(); + tutorial.UpdateTutorialStep(); IsOpen = _config.DebugMode; } - private ReadOnlySpan ToLabel(TabType type) - => type switch - { - TabType.Settings => _settingsTab.Label, - TabType.Mods => _modsTab.Label, - TabType.Collections => _collectionsTab.Label, - TabType.ChangedItems => _changedItemsTab.Label, - TabType.EffectiveChanges => _effectiveTab.Label, - TabType.ResourceWatcher => _resourceWatcher.Label, - TabType.Debug => _debugTab.Label, - TabType.ResourceManager => _resourceTab.Label, - _ => ReadOnlySpan.Empty, - }; + public override void PreDraw() + { + if (_config.FixMainWindow) + Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; + else + Flags &= ~(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); + } public override void Draw() { - using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow); - + using var timer = _tracker.Measure(PerformanceType.UiMainWindow); + UiHelpers.SetupCommonSizes(); try { if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) { - DrawProblemWindow(_penumbra, - $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( + $"There were {_validityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" - + "Please use the Launcher's Repair Game Files function to repair your client installation.", true); + + "Please use the Launcher's Repair Game Files function to repair your client installation."); + DrawImcExceptions(); } else if (!Penumbra.ValidityChecker.IsValidSourceRepo) { - DrawProblemWindow(_penumbra, - $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" + DrawProblemWindow( + $"You are loading a release version of Penumbra from the repository \"{_pluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) { - DrawProblemWindow(_penumbra, - $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + DrawProblemWindow( + $"You are loading a release version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } else if (Penumbra.ValidityChecker.DevPenumbraExists) { - DrawProblemWindow(_penumbra, - $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + DrawProblemWindow( + $"You are loading a installed version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" - + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", - false); + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode."); } else { - SetupSizes(); - if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab, - _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab)) - SelectTab = TabType.None; + _configTabs.Draw(); } + + _lastException = null; } catch (Exception e) { - Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}"); + if (_lastException != null) + { + var text = e.ToString(); + if (text == _lastException) + return; + + _lastException = text; + } + + Penumbra.Log.Error($"Exception thrown during UI Render:\n{_lastException}"); } } - private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions) + private static string GetLabel(ValidityChecker checker) + => checker.Version.Length == 0 + ? "Penumbra###PenumbraConfigWindow" + : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; + + private void DrawProblemWindow(string text) { using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); @@ -148,47 +135,23 @@ public sealed partial class ConfigWindow : Window, IDisposable ImGui.NewLine(); ImGui.NewLine(); - SettingsTab.DrawDiscordButton(0); + UiHelpers.DrawDiscordButton(0); ImGui.SameLine(); - SettingsTab.DrawSupportButton(penumbra); + UiHelpers.DrawSupportButton(_penumbra); ImGui.NewLine(); ImGui.NewLine(); + } - if (withExceptions) + private void DrawImcExceptions() + { + ImGui.TextUnformatted("Exceptions"); + ImGui.Separator(); + using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); + foreach (var exception in _validityChecker.ImcExceptions) { - ImGui.TextUnformatted("Exceptions"); + ImGuiUtil.TextWrapped(exception.ToString()); ImGui.Separator(); - using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); - foreach (var exception in Penumbra.ValidityChecker.ImcExceptions) - { - ImGuiUtil.TextWrapped(exception.ToString()); - ImGui.Separator(); - ImGui.NewLine(); - } + ImGui.NewLine(); } } - - public void Dispose() - { - _selector.Dispose(); - _modPanel.Dispose(); - _collectionsTab.Dispose(); - ModEditPopup.Dispose(); - } - - private static string GetLabel() - => Penumbra.Version.Length == 0 - ? "Penumbra###PenumbraConfigWindow" - : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; - - private Vector2 _defaultSpace; - private Vector2 _inputTextWidth; - private Vector2 _iconButtonSize; - - private void SetupSizes() - { - _defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale); - _inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0); - _iconButtonSize = new Vector2(ImGui.GetFrameHeight()); - } } diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs new file mode 100644 index 00000000..2d16668f --- /dev/null +++ b/Penumbra/UI/FileDialogService.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using ImGuiNET; +using OtterGui; +using Penumbra.Mods; + +namespace Penumbra.UI; + +public class FileDialogService : IDisposable +{ + private readonly Mod.Manager _mods; + private readonly FileDialogManager _manager; + private readonly ConcurrentDictionary _startPaths = new(); + private bool _isOpen; + + public FileDialogService(Mod.Manager mods, Configuration config) + { + _mods = mods; + _manager = SetupFileManager(config.ModDirectory); + + _mods.ModDirectoryChanged += OnModDirectoryChange; + } + + public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.OpenFileDialog(title, filters, CreateCallback(title, callback), selectionCountMax, + GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenFolderPicker(string title, Action callback, string? startPath, bool forceStartPath) + { + _isOpen = true; + _manager.OpenFolderDialog(title, CreateCallback(title, callback), GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenSavePicker(string title, string filters, string defaultFileName, string defaultExtension, Action callback, + string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.SaveFileDialog(title, filters, defaultFileName, defaultExtension, CreateCallback(title, callback), + GetStartPath(title, startPath, forceStartPath)); + } + + public void Close() + { + _isOpen = false; + } + + public void Reset() + { + _isOpen = false; + _manager.Reset(); + } + + public void Draw() + { + if (_isOpen) + _manager.Draw(); + } + + public void Dispose() + { + _startPaths.Clear(); + _manager.Reset(); + _mods.ModDirectoryChanged -= OnModDirectoryChange; + } + + private string? GetStartPath(string title, string? startPath, bool forceStartPath) + { + var path = !forceStartPath && _startPaths.TryGetValue(title, out var p) ? p : startPath; + if (!path.IsNullOrEmpty() && !Directory.Exists(path)) + path = null; + return path; + } + + private Action> CreateCallback(string title, Action> callback) + { + return (valid, list) => + { + _isOpen = false; + _startPaths[title] = GetCurrentLocation(); + callback(valid, list); + }; + } + + private Action CreateCallback(string title, Action callback) + { + return (valid, list) => + { + _isOpen = false; + var loc = GetCurrentLocation(); + if (loc.Length == 2) + loc += '\\'; + _startPaths[title] = loc; + callback(valid, list); + }; + } + + // TODO: maybe change this from reflection when its public. + private string GetCurrentLocation() + => (_manager.GetType().GetField("dialog", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(_manager) as FileDialog) + ?.GetCurrentPath() + ?? "."; + + /// Set up the file selector with the right flags and custom side bar items. + private static FileDialogManager SetupFileManager(string modDirectory) + { + var fileManager = new FileDialogManager + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, + }; + + if (Functions.GetDownloadsFolder(out var downloadsFolder)) + fileManager.CustomSideBarItems.Add(("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1)); + + if (Functions.GetQuickAccessFolders(out var folders)) + foreach (var ((name, path), idx) in folders.WithIndex()) + fileManager.CustomSideBarItems.Add(($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1)); + + // Add Penumbra Root. This is not updated if the root changes right now. + fileManager.CustomSideBarItems.Add(("Root Directory", modDirectory, FontAwesomeIcon.Gamepad, 0)); + + // Remove Videos and Music. + fileManager.CustomSideBarItems.Add(("Videos", string.Empty, 0, -1)); + fileManager.CustomSideBarItems.Add(("Music", string.Empty, 0, -1)); + + return fileManager; + } + + /// Update the Root Directory link on changes. + private void OnModDirectoryChange(string directory, bool valid) + { + var idx = _manager.CustomSideBarItems.IndexOf(t => t.Name == "Root Directory"); + if (idx >= 0) + _manager.CustomSideBarItems.RemoveAt(idx); + + if (!valid) + return; + + if (idx >= 0) + _manager.CustomSideBarItems.Insert(idx, ("Root Directory", directory, FontAwesomeIcon.Gamepad, 0)); + else + _manager.CustomSideBarItems.Add(("Root Directory", directory, FontAwesomeIcon.Gamepad, 0)); + } +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs new file mode 100644 index 00000000..c6384fd0 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +using OtterGui.Raii; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Import; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.ModTab; + +public sealed partial class ModFileSystemSelector : FileSystemSelector +{ + private readonly CommunicatorService _communicator; + private readonly ChatService _chat; + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + private readonly TutorialService _tutorial; + + private TexToolsImporter? _import; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + + public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager, + ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat) + : base(fileSystem, DalamudServices.KeyState) + { + _communicator = communicator; + _modManager = modManager; + _collectionManager = collectionManager; + _config = config; + _tutorial = tutorial; + _fileDialog = fileDialog; + _chat = chat; + + SubscribeRightClickFolder(EnableDescendants, 10); + SubscribeRightClickFolder(DisableDescendants, 10); + SubscribeRightClickFolder(InheritDescendants, 15); + SubscribeRightClickFolder(OwnDescendants, 15); + SubscribeRightClickFolder(SetDefaultImportFolder, 100); + SubscribeRightClickLeaf(ToggleLeafFavorite); + SubscribeRightClickMain(ClearDefaultImportFolder, 100); + AddButton(AddNewModButton, 0); + AddButton(AddImportModButton, 1); + AddButton(AddHelpButton, 2); + AddButton(DeleteModButton, 1000); + SetFilterTooltip(); + + SelectionChanged += OnSelectionChange; + _communicator.CollectionChange.Event += OnCollectionChange; + _collectionManager.Current.ModSettingChanged += OnSettingChange; + _collectionManager.Current.InheritanceChanged += OnInheritanceChange; + _modManager.ModDataChanged += OnModDataChange; + _modManager.ModDiscoveryStarted += StoreCurrentSelection; + _modManager.ModDiscoveryFinished += RestoreLastSelection; + OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, ""); + } + + public override void Dispose() + { + base.Dispose(); + _modManager.ModDiscoveryStarted -= StoreCurrentSelection; + _modManager.ModDiscoveryFinished -= RestoreLastSelection; + _modManager.ModDataChanged -= OnModDataChange; + _collectionManager.Current.ModSettingChanged -= OnSettingChange; + _collectionManager.Current.InheritanceChanged -= OnInheritanceChange; + _communicator.CollectionChange.Event -= OnCollectionChange; + _import?.Dispose(); + _import = null; + } + + public new ModFileSystem.Leaf? SelectedLeaf + => base.SelectedLeaf; + + #region Interface + + // Customization points. + public override ISortMode SortMode + => Penumbra.Config.SortMode; + + protected override uint ExpandedFolderColor + => ColorId.FolderExpanded.Value(_config); + + protected override uint CollapsedFolderColor + => ColorId.FolderCollapsed.Value(_config); + + protected override uint FolderLineColor + => ColorId.FolderLine.Value(_config); + + protected override bool FoldersDefaultOpen + => Penumbra.Config.OpenFoldersByDefault; + + protected override void DrawPopups() + { + DrawHelpPopup(); + DrawInfoPopup(); + + if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) + try + { + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); + _newModName = string.Empty; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); + } + + while (_modsToAdd.TryDequeue(out var dir)) + { + Penumbra.ModManager.AddMod(dir); + var mod = Penumbra.ModManager.LastOrDefault(); + if (mod != null) + { + MoveModToDefaultDirectory(mod); + SelectByValue(mod); + } + } + } + + protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) + { + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value(_config)) + .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); + using var id = ImRaii.PushId(leaf.Value.Index); + ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); + } + + + // Add custom context menu items. + private void EnableDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Enable Descendants")) + SetDescendants(folder, true); + } + + private void DisableDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Disable Descendants")) + SetDescendants(folder, false); + } + + private void InheritDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Inherit Descendants")) + SetDescendants(folder, true, true); + } + + private void OwnDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Stop Inheriting Descendants")) + SetDescendants(folder, false, true); + } + + private void ToggleLeafFavorite(FileSystem.Leaf mod) + { + if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + _modManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); + } + + private void SetDefaultImportFolder(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Set As Default Import Folder")) + { + var newName = folder.FullName(); + if (newName != _config.DefaultImportFolder) + { + _config.DefaultImportFolder = newName; + _config.Save(); + } + } + } + + private void ClearDefaultImportFolder() + { + if (ImGui.MenuItem("Clear Default Import Folder") && _config.DefaultImportFolder.Length > 0) + { + _config.DefaultImportFolder = string.Empty; + _config.Save(); + } + } + + private string _newModName = string.Empty; + + private void AddNewModButton(Vector2 size) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", + !_modManager.Valid, true)) + ImGui.OpenPopup("Create New Mod"); + } + + /// Add an import mods button that opens a file selector. + private void AddImportModButton(Vector2 size) + { + var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); + _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); + if (!button) + return; + + var modPath = !_config.AlwaysOpenDefaultImport ? null + : _config.DefaultModImportPath.Length > 0 ? _config.DefaultModImportPath + : _config.ModDirectory.Length > 0 ? _config.ModDirectory : null; + + _fileDialog.OpenFilePicker("Import Mod Pack", + "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => + { + if (!s) + return; + + _import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), + AddNewMod); + ImGui.OpenPopup("Import Status"); + }, 0, modPath, _config.AlwaysOpenDefaultImport); + } + + /// Draw the progress information for import. + private void DrawInfoPopup() + { + var display = ImGui.GetIO().DisplaySize; + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 8; + var size = new Vector2(width * 2, height); + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); + if (_import == null || !popup.Success) + return; + + using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) + { + if (child) + _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); + } + + if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) + || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) + { + _import?.Dispose(); + _import = null; + ImGui.CloseCurrentPopup(); + } + } + + /// Mods need to be added thread-safely outside of iteration. + private readonly ConcurrentQueue _modsToAdd = new(); + + /// + /// Clean up invalid directory if necessary. + /// Add successfully extracted mods. + /// + private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) + { + if (error != null) + { + if (dir != null && Directory.Exists(dir.FullName)) + try + { + Directory.Delete(dir.FullName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); + } + + if (error is not OperationCanceledException) + Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); + } + else if (dir != null) + { + _modsToAdd.Enqueue(dir); + } + } + + private void DeleteModButton(Vector2 size) + { + var keys = _config.DeleteModModifier.IsActive(); + var tt = SelectedLeaf == null + ? "No mod selected." + : "Delete the currently selected mod entirely from your drive.\n" + + "This can not be undone."; + if (!keys) + tt += $"\nHold {_config.DeleteModModifier} while clicking to delete the mod."; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) + && Selected != null) + _modManager.DeleteMod(Selected.Index); + } + + private void AddHelpButton(Vector2 size) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) + ImGui.OpenPopup("ExtendedHelp"); + + _tutorial.OpenTutorial(BasicTutorialSteps.AdvancedHelp); + } + + private void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) + { + var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => + { + // Any mod handled here should not stay new. + _modManager.NewMods.Remove(l.Value); + return l.Value; + }); + + if (inherit) + _collectionManager.Current.SetMultipleModInheritances(mods, enabled); + else + _collectionManager.Current.SetMultipleModStates(mods, enabled); + } + + /// + /// If a default import folder is setup, try to move the given mod in there. + /// If the folder does not exist, create it if possible. + /// + /// + private void MoveModToDefaultDirectory(Mod mod) + { + if (_config.DefaultImportFolder.Length == 0) + return; + + try + { + var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) + .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); + if (leaf == null) + throw new Exception("Mod was not found at root."); + + var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); + FileSystem.Move(leaf, folder); + } + catch (Exception e) + { + _chat.NotificationMessage( + $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}:\n{e}", "Warning", + NotificationType.Warning); + } + } + + private void DrawHelpPopup() + { + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Management"); + ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); + ImGui.BulletText( + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + indent.Pop(1); + ImGui.BulletText("You can also create empty mod folders and delete mods."); + ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Selector"); + ImGui.BulletText("Select a mod to obtain more information or change settings."); + ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); + indent.Push(); + ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(_config), "enabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(_config), "disabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(_config), "enabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(_config), "disabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(_config), "unconfigured in all inherited collections."); + ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(_config), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); + ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(_config), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); + ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(_config), + "enabled and conflicting with another enabled Mod on the same priority."); + ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(_config), "expanded mod folder."); + ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(_config), "collapsed mod folder"); + indent.Pop(1); + ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + indent.Push(); + ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + indent.Pop(1); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImGui.BulletText("Right-clicking a folder opens a context menu."); + ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); + ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + indent.Push(); + ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); + ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); + ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + indent.Pop(1); + ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + }); + } + + #endregion + + #region Automatic cache update functions. + + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) + { + // TODO: maybe make more efficient + SetFilterDirty(); + if (modIdx == Selected?.Index) + OnSelectionChange(Selected, Selected, default); + } + + private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) + { + switch (type) + { + case ModDataChangeType.Name: + case ModDataChangeType.Author: + case ModDataChangeType.ModTags: + case ModDataChangeType.LocalTags: + case ModDataChangeType.Favorite: + SetFilterDirty(); + break; + } + } + + private void OnInheritanceChange(bool _) + { + SetFilterDirty(); + OnSelectionChange(Selected, Selected, default); + } + + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) + { + if (collectionType != CollectionType.Current || oldCollection == newCollection) + return; + + if (oldCollection != null) + { + oldCollection.ModSettingChanged -= OnSettingChange; + oldCollection.InheritanceChanged -= OnInheritanceChange; + } + + if (newCollection != null) + { + newCollection.ModSettingChanged += OnSettingChange; + newCollection.InheritanceChanged += OnInheritanceChange; + } + + SetFilterDirty(); + OnSelectionChange(Selected, Selected, default); + } + + private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) + { + if (newSelection == null) + { + SelectedSettings = ModSettings.Empty; + SelectedSettingCollection = ModCollection.Empty; + } + else + { + (var settings, SelectedSettingCollection) = _collectionManager.Current[newSelection.Index]; + SelectedSettings = settings ?? ModSettings.Empty; + } + } + + // Keep selections across rediscoveries if possible. + private string _lastSelectedDirectory = string.Empty; + + private void StoreCurrentSelection() + { + _lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty; + ClearSelection(); + } + + private void RestoreLastSelection() + { + if (_lastSelectedDirectory.Length <= 0) + return; + + var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) + .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); + Select(leaf); + _lastSelectedDirectory = string.Empty; + } + + #endregion + + #region Filters + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModState + { + public ColorId Color; + } + + private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + + private void SetFilterTooltip() + { + FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" + + "Enter c:[string] to filter for mods changing specific items.\n" + + "Enter t:[string] to filter for mods set to specific tags.\n" + + "Enter n:[string] to filter only for mod names and no paths.\n" + + "Enter a:[string] to filter for mods by specific authors."; + } + + /// Appropriately identify and set the string filter and its type. + protected override bool ChangeFilter(string filterValue) + { + (_modFilter, _filterType) = filterValue.Length switch + { + 0 => (LowerString.Empty, -1), + > 1 when filterValue[1] == ':' => + filterValue[0] switch + { + 'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), + 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), + 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), + 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), + 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), + 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), + 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + _ => (new LowerString(filterValue), 0), + }, + _ => (new LowerString(filterValue), 0), + }; + + return true; + } + + /// + /// Check the state filter for a specific pair of has/has-not flags. + /// Uses count == 0 to check for has-not and count != 0 for has. + /// Returns true if it should be filtered and false if not. + /// + private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag) + { + return count switch + { + 0 when _stateFilter.HasFlag(hasNoFlag) => false, + 0 => true, + _ when _stateFilter.HasFlag(hasFlag) => false, + _ => true, + }; + } + + /// + /// The overwritten filter method also computes the state. + /// Folders have default state and are filtered out on the direct string instead of the other options. + /// If any filter is set, they should be hidden by default unless their children are visible, + /// or they contain the path search string. + /// + protected override bool ApplyFiltersAndState(FileSystem.IPath path, out ModState state) + { + if (path is ModFileSystem.Folder f) + { + state = default; + return ModFilterExtensions.UnfilteredStateMods != _stateFilter + || FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase); + } + + return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state); + } + + /// Apply the string filters. + private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) + { + return _filterType switch + { + -1 => false, + 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), + 1 => !mod.Name.Contains(_modFilter), + 2 => !mod.Author.Contains(_modFilter), + 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), + 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), + _ => false, // Should never happen + }; + } + + /// Only get the text color for a mod if no filters are set. + private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) + { + if (Penumbra.ModManager.NewMods.Contains(mod)) + return ColorId.NewMod; + + if (settings == null) + return ColorId.UndefinedMod; + + if (!settings.Enabled) + return collection != _collectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod; + + var conflicts = _collectionManager.Current.Conflicts(mod); + if (conflicts.Count == 0) + return collection != _collectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod; + + return conflicts.Any(c => !c.Solved) + ? ColorId.ConflictingMod + : ColorId.HandledConflictMod; + } + + private bool CheckStateFilters(Mod mod, ModSettings? settings, ModCollection collection, ref ModState state) + { + var isNew = _modManager.NewMods.Contains(mod); + // Handle mod details. + if (CheckFlags(mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles) + || CheckFlags(mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps) + || CheckFlags(mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations) + || CheckFlags(mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig) + || CheckFlags(isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew)) + return true; + + // Handle Favoritism + if (!_stateFilter.HasFlag(ModFilter.Favorite) && mod.Favorite + || !_stateFilter.HasFlag(ModFilter.NotFavorite) && !mod.Favorite) + return true; + + // Handle Inheritance + if (collection == _collectionManager.Current) + { + if (!_stateFilter.HasFlag(ModFilter.Uninherited)) + return true; + } + else + { + state.Color = ColorId.InheritedMod; + if (!_stateFilter.HasFlag(ModFilter.Inherited)) + return true; + } + + // Handle settings. + if (settings == null) + { + state.Color = ColorId.UndefinedMod; + if (!_stateFilter.HasFlag(ModFilter.Undefined) + || !_stateFilter.HasFlag(ModFilter.Disabled) + || !_stateFilter.HasFlag(ModFilter.NoConflict)) + return true; + } + else if (!settings.Enabled) + { + state.Color = collection == _collectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod; + if (!_stateFilter.HasFlag(ModFilter.Disabled) + || !_stateFilter.HasFlag(ModFilter.NoConflict)) + return true; + } + else + { + if (!_stateFilter.HasFlag(ModFilter.Enabled)) + return true; + + // Conflicts can only be relevant if the mod is enabled. + var conflicts = _collectionManager.Current.Conflicts(mod); + if (conflicts.Count > 0) + { + if (conflicts.Any(c => !c.Solved)) + { + if (!_stateFilter.HasFlag(ModFilter.UnsolvedConflict)) + return true; + + state.Color = ColorId.ConflictingMod; + } + else + { + if (!_stateFilter.HasFlag(ModFilter.SolvedConflict)) + return true; + + state.Color = ColorId.HandledConflictMod; + } + } + else if (!_stateFilter.HasFlag(ModFilter.NoConflict)) + { + return true; + } + } + + // isNew color takes precedence before other colors. + if (isNew) + state.Color = ColorId.NewMod; + + return false; + } + + /// Combined wrapper for handling all filters and setting state. + private bool ApplyFiltersAndState(ModFileSystem.Leaf leaf, out ModState state) + { + state = new ModState { Color = ColorId.EnabledMod }; + var mod = leaf.Value; + var (settings, collection) = _collectionManager.Current[mod.Index]; + + if (ApplyStringFilters(leaf, mod)) + return true; + + if (_stateFilter != ModFilterExtensions.UnfilteredStateMods) + return CheckStateFilters(mod, settings, collection, ref state); + + state.Color = GetTextColor(mod, settings, collection); + return false; + } + + private void DrawFilterCombo(ref bool everything) + { + using var combo = ImRaii.Combo("##filterCombo", string.Empty, + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest); + if (!combo) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale }); + var flags = (int)_stateFilter; + + + if (ImGui.Checkbox("Everything", ref everything)) + { + _stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0; + SetFilterDirty(); + } + + ImGui.Dummy(new Vector2(0, 5 * UiHelpers.Scale)); + foreach (ModFilter flag in Enum.GetValues(typeof(ModFilter))) + { + if (ImGui.CheckboxFlags(flag.ToName(), ref flags, (int)flag)) + { + _stateFilter = (ModFilter)flags; + SetFilterDirty(); + } + } + } + + /// Add the state filter combo-button to the right of the filter box. + protected override float CustomFilters(float width) + { + var pos = ImGui.GetCursorPos(); + var remainingWidth = width - ImGui.GetFrameHeight(); + var comboPos = new Vector2(pos.X + remainingWidth, pos.Y); + + var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; + + ImGui.SetCursorPos(comboPos); + // Draw combo button + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.FilterActive, !everything); + DrawFilterCombo(ref everything); + _tutorial.OpenTutorial(BasicTutorialSteps.ModFilters); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _stateFilter = ModFilterExtensions.UnfilteredStateMods; + SetFilterDirty(); + } + + ImGuiUtil.HoverTooltip("Filter mods for their activation status.\nRight-Click to clear all filters."); + ImGui.SetCursorPos(pos); + return remainingWidth; + } + + #endregion +} diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs new file mode 100644 index 00000000..4c221b21 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -0,0 +1,59 @@ +using System; + +namespace Penumbra.UI.ModTab; + +[Flags] +public enum ModFilter +{ + Enabled = 1 << 0, + Disabled = 1 << 1, + Favorite = 1 << 2, + NotFavorite = 1 << 3, + NoConflict = 1 << 4, + SolvedConflict = 1 << 5, + UnsolvedConflict = 1 << 6, + HasNoMetaManipulations = 1 << 7, + HasMetaManipulations = 1 << 8, + HasNoFileSwaps = 1 << 9, + HasFileSwaps = 1 << 10, + HasConfig = 1 << 11, + HasNoConfig = 1 << 12, + HasNoFiles = 1 << 13, + HasFiles = 1 << 14, + IsNew = 1 << 15, + NotNew = 1 << 16, + Inherited = 1 << 17, + Uninherited = 1 << 18, + Undefined = 1 << 19, +}; + +public static class ModFilterExtensions +{ + public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 20) - 1); + + public static string ToName(this ModFilter filter) + => filter switch + { + ModFilter.Enabled => "Enabled", + ModFilter.Disabled => "Disabled", + ModFilter.Favorite => "Favorite", + ModFilter.NotFavorite => "No Favorite", + ModFilter.NoConflict => "No Conflicts", + ModFilter.SolvedConflict => "Solved Conflicts", + ModFilter.UnsolvedConflict => "Unsolved Conflicts", + ModFilter.HasNoMetaManipulations => "No Meta Manipulations", + ModFilter.HasMetaManipulations => "Meta Manipulations", + ModFilter.HasNoFileSwaps => "No File Swaps", + ModFilter.HasFileSwaps => "File Swaps", + ModFilter.HasNoConfig => "No Configuration", + ModFilter.HasConfig => "Configuration", + ModFilter.HasNoFiles => "No Files", + ModFilter.HasFiles => "Files", + ModFilter.IsNew => "Newly Imported", + ModFilter.NotNew => "Not Newly Imported", + ModFilter.Inherited => "Inherited Configuration", + ModFilter.Uninherited => "Own Configuration", + ModFilter.Undefined => "Not Configured", + _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), + }; +} \ No newline at end of file diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs new file mode 100644 index 00000000..e6999412 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -0,0 +1,60 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanel : IDisposable +{ + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; + + public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) + { + _selector = selector; + _editWindow = editWindow; + _tabs = tabs; + _header = new ModPanelHeader(pi); + _selector.SelectionChanged += OnSelectionChange; + } + + public void Draw() + { + if (!_valid) + return; + + _header.Draw(); + _tabs.Draw(_mod); + } + + public void Dispose() + { + _selector.SelectionChanged -= OnSelectionChange; + _header.Dispose(); + } + + private bool _valid; + private Mod _mod = null!; + + private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _) + { + if (mod == null || _selector.Selected == null) + { + _editWindow.IsOpen = false; + _valid = false; + } + else + { + if (_editWindow.IsOpen) + _editWindow.ChangeMod(mod); + _valid = true; + _mod = mod; + _header.UpdateModData(_mod); + _tabs.Settings.Reset(); + _tabs.Edit.Reset(); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs new file mode 100644 index 00000000..49f5d8cf --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -0,0 +1,40 @@ +using System; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelChangedItemsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly PenumbraApi _api; + + public ReadOnlySpan Label + => "Changed Items"u8; + + public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector) + { + _api = api; + _selector = selector; + } + + public bool IsVisible + => _selector.Selected!.ChangedItems.Count > 0; + + public void DrawContent() + { + using var list = ImRaii.ListBox("##changedItems", -Vector2.One); + if (!list) + return; + + var zipList = ZipList.FromSortedList(_selector.Selected!.ChangedItems); + var height = ImGui.GetTextLineHeight(); + ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_api, kvp.Item1, kvp.Item2, true), height); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs new file mode 100644 index 00000000..bcdb0f16 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -0,0 +1,69 @@ +using System; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelConflictsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly ModCollection.Manager _collectionManager; + + public ModPanelConflictsTab(ModCollection.Manager collectionManager, ModFileSystemSelector selector) + { + _collectionManager = collectionManager; + _selector = selector; + } + + public ReadOnlySpan Label + => "Conflicts"u8; + + public bool IsVisible + => _collectionManager.Current.Conflicts(_selector.Selected!).Count > 0; + + public void DrawContent() + { + using var box = ImRaii.ListBox("##conflicts", -Vector2.One); + if (!box) + return; + + // Can not be null because otherwise the tab bar is never drawn. + var mod = _selector.Selected!; + foreach (var conflict in Penumbra.CollectionManager.Current.Conflicts(mod)) + { + if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) + _selector.SelectByValue(otherMod); + + ImGui.SameLine(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, + conflict.HasPriority ? ColorId.HandledConflictMod.Value(Penumbra.Config) : ColorId.ConflictingMod.Value(Penumbra.Config))) + { + var priority = conflict.Mod2.Index < 0 + ? conflict.Mod2.Priority + : _collectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + ImGui.TextUnformatted($"(Priority {priority})"); + } + + using var indent = ImRaii.PushIndent(30f); + foreach (var data in conflict.Conflicts) + { + unsafe + { + var _ = data switch + { + Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, + MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), + _ => false, + }; + } + } + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs new file mode 100644 index 00000000..3cc770a2 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -0,0 +1,57 @@ +using System; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelDescriptionTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly TutorialService _tutorial; + private readonly Mod.Manager _modManager; + private readonly TagButtons _localTags = new(); + private readonly TagButtons _modTags = new(); + + public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, Mod.Manager modManager) + { + _selector = selector; + _tutorial = tutorial; + _modManager = modManager; + } + + public ReadOnlySpan Label + => "Description"u8; + + public void DrawContent() + { + using var child = ImRaii.Child("##description"); + if (!child) + return; + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + var tagIdx = _localTags.Draw("Local Tags: ", + "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _selector.Selected!.LocalTags, + out var editedTag); + _tutorial.OpenTutorial(BasicTutorialSteps.Tags); + if (tagIdx >= 0) + _modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag); + + if (_selector.Selected!.ModTags.Count > 0) + _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", + _selector.Selected!.ModTags, out var _, false, + ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + ImGui.Separator(); + + ImGuiUtil.TextWrapped(_selector.Selected!.Description); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs new file mode 100644 index 00000000..89bdff92 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.ModTab; + +public class ModPanelEditTab : ITab +{ + private readonly ChatService _chat; + private readonly Mod.Manager _modManager; + private readonly ModFileSystem _fileSystem; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + + private readonly TagButtons _modTags = new(); + + private Vector2 _cellPadding = Vector2.Zero; + private Vector2 _itemSpacing = Vector2.Zero; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + + public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, + ModEditWindow editWindow) + { + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + } + + public ReadOnlySpan Label + => "Edit Mod"u8; + + public void DrawContent() + { + using var child = ImRaii.Child("##editChild", -Vector2.One); + if (!child) + return; + + _leaf = _selector.SelectedLeaf!; + _mod = _selector.Selected!; + + _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; + _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; + + EditButtons(); + EditRegularMeta(); + UiHelpers.DefaultLineSpace(); + + if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X)) + try + { + _fileSystem.RenameAndMove(_leaf, newPath); + } + catch (Exception e) + { + _chat.NotificationMessage(e.Message, "Warning", NotificationType.Warning); + } + + UiHelpers.DefaultLineSpace(); + var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, + out var editedTag); + if (tagIdx >= 0) + _modManager.ChangeModTag(_mod.Index, tagIdx, editedTag); + + UiHelpers.DefaultLineSpace(); + AddOptionGroup.Draw(_modManager, _mod); + UiHelpers.DefaultLineSpace(); + + for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) + EditGroup(groupIdx); + + EndActions(); + DescriptionEdit.DrawPopup(_modManager); + } + + public void Reset() + { + AddOptionGroup.Reset(); + MoveDirectory.Reset(); + Input.Reset(); + OptionTable.Reset(); + } + + /// The general edit row for non-detailed mod edits. + private void EditButtons() + { + var buttonSize = new Vector2(150 * UiHelpers.Scale, 0); + var folderExists = Directory.Exists(_mod.ModPath.FullName); + var tt = folderExists + ? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice." + : $"Mod directory \"{_mod.ModPath.FullName}\" does not exist."; + if (ImGuiUtil.DrawDisabledButton("Open Mod Directory", buttonSize, tt, !folderExists)) + Process.Start(new ProcessStartInfo(_mod.ModPath.FullName) { UseShellExecute = true }); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n" + + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", + false)) + _modManager.ReloadMod(_mod.Index); + + BackupButtons(buttonSize); + MoveDirectory.Draw(_modManager, _mod, buttonSize); + + UiHelpers.DefaultLineSpace(); + DrawUpdateBibo(buttonSize); + + UiHelpers.DefaultLineSpace(); + } + + private void DrawUpdateBibo(Vector2 buttonSize) + { + if (ImGui.Button("Update Bibo Material", buttonSize)) + { + var editor = new Mod.Editor(_mod, null); + editor.ReplaceAllMaterials("bibo", "b"); + editor.ReplaceAllMaterials("bibopube", "c"); + editor.SaveAllModels(); + _editWindow.UpdateModels(); + } + + ImGuiUtil.HoverTooltip( + "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" + + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" + + "Use this for outdated mods made for old Bibo bodies.\n" + + "Go to Advanced Editing for more fine-tuned control over material assignment."); + } + + private void BackupButtons(Vector2 buttonSize) + { + var backup = new ModBackup(_mod); + var tt = ModBackup.CreatingBackup + ? "Already exporting a mod." + : backup.Exists + ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." + : $"Create exported archive of current mod at \"{backup.Name}\"."; + if (ImGuiUtil.DrawDisabledButton("Export Mod", buttonSize, tt, ModBackup.CreatingBackup)) + backup.CreateAsync(); + + ImGui.SameLine(); + tt = backup.Exists + ? $"Delete existing mod export \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; + if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists)) + backup.Delete(); + + tt = backup.Exists + ? $"Restore mod from exported file \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists)) + backup.Restore(); + } + + /// Anything about editing the regular meta information about the mod. + private void EditRegularMeta() + { + if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) + _modManager.ChangeModName(_mod.Index, newName); + + if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) + Penumbra.ModManager.ChangeModAuthor(_mod.Index, newAuthor); + + if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, + UiHelpers.InputTextWidth.X)) + _modManager.ChangeModVersion(_mod.Index, newVersion); + + if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, + UiHelpers.InputTextWidth.X)) + _modManager.ChangeModWebsite(_mod.Index, newWebsite); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); + + var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); + if (ImGui.Button("Edit Description", reducedSize)) + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + + ImGui.SameLine(); + var fileExists = File.Exists(_mod.MetaFile.FullName); + var tt = fileExists + ? "Open the metadata json file in the text editor of your choice." + : "The metadata json file does not exist."; + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, + !fileExists, true)) + Process.Start(new ProcessStartInfo(_mod.MetaFile.FullName) { UseShellExecute = true }); + } + + /// Do some edits outside of iterations. + private readonly Queue _delayedActions = new(); + + /// Delete a marked group or option outside of iteration. + private void EndActions() + { + while (_delayedActions.TryDequeue(out var action)) + action.Invoke(); + } + + /// Text input to add a new option group at the end of the current groups. + private static class AddOptionGroup + { + private static string _newGroupName = string.Empty; + + public static void Reset() + => _newGroupName = string.Empty; + + public static void Draw(Mod.Manager modManager, Mod mod) + { + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, UiHelpers.ScaleX3); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); + ImGui.SameLine(); + var fileExists = File.Exists(mod.DefaultFile); + var tt = fileExists + ? "Open the default option json file in the text editor of your choice." + : "The default option json file does not exist."; + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", UiHelpers.IconButtonSize, tt, + !fileExists, true)) + Process.Start(new ProcessStartInfo(mod.DefaultFile) { UseShellExecute = true }); + + ImGui.SameLine(); + + var nameValid = modManager.VerifyFileName(mod, null, _newGroupName, false); + tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, + tt, !nameValid, true)) + return; + + modManager.AddModGroup(mod, GroupType.Single, _newGroupName); + Reset(); + } + } + + /// A text input for the new directory name and a button to apply the move. + private static class MoveDirectory + { + private static string? _currentModDirectory; + private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + + public static void Reset() + { + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + + public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize) + { + ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X); + var tmp = _currentModDirectory ?? mod.ModPath.Name; + if (ImGui.InputText("##newModMove", ref tmp, 64)) + { + _currentModDirectory = tmp; + _state = modManager.NewDirectoryValid(mod.ModPath.Name, _currentModDirectory, out _); + } + + var (disabled, tt) = _state switch + { + Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), + Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), + Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), + Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), + Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true, + $"{_currentModDirectory} contains invalid symbols for FFXIV."), + _ => (true, "Unknown error."), + }; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Rename Mod Directory", buttonSize, tt, disabled) && _currentModDirectory != null) + { + modManager.MoveModDirectory(mod.Index, _currentModDirectory); + Reset(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n" + + "This can currently not be used on pre-existing folders and does not support merges or overwriting."); + } + } + + /// Open a popup to edit a multi-line mod or option description. + private static class DescriptionEdit + { + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDescriptionOptionIdx = -1; + private static Mod? _mod; + + public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) + { + _newDescriptionIdx = groupIdx; + _newDescriptionOptionIdx = optionIdx; + _newDescription = groupIdx < 0 + ? mod.Description + : optionIdx < 0 + ? mod.Groups[groupIdx].Description + : mod.Groups[groupIdx][optionIdx].Description; + + _mod = mod; + ImGui.OpenPopup(PopupName); + } + + public static void DrawPopup(Mod.Manager modManager) + { + if (_mod == null) + return; + + using var popup = ImRaii.Popup(PopupName); + if (!popup) + return; + + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + + ImGui.InputTextMultiline("##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2(800, 800)); + UiHelpers.DefaultLineSpace(); + + var buttonSize = ImGuiHelpers.ScaledVector2(100, 0); + var width = 2 * buttonSize.X + + 4 * ImGui.GetStyle().FramePadding.X + + ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); + + var oldDescription = _newDescriptionIdx == Input.Description + ? _mod.Description + : _mod.Groups[_newDescriptionIdx].Description; + + var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; + + if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) + { + switch (_newDescriptionIdx) + { + case Input.Description: + modManager.ChangeModDescription(_mod.Index, _newDescription); + break; + case >= 0: + if (_newDescriptionOptionIdx < 0) + modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + else + modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription); + + break; + } + + ImGui.CloseCurrentPopup(); + } + + ImGui.SameLine(); + if (!ImGui.Button("Cancel", buttonSize) + && !ImGui.IsKeyPressed(ImGuiKey.Escape)) + return; + + _newDescriptionIdx = Input.None; + _newDescription = string.Empty; + ImGui.CloseCurrentPopup(); + } + } + + private void EditGroup(int groupIdx) + { + var group = _mod.Groups[groupIdx]; + using var id = ImRaii.PushId(groupIdx); + using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}"); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding) + .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); + + if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) + _modManager.RenameModGroup(_mod, groupIdx, newGroupName); + + ImGuiUtil.HoverTooltip("Group Name"); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) + _delayedActions.Enqueue(() => _modManager.DeleteModGroup(_mod, groupIdx)); + + ImGui.SameLine(); + + if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) + _modManager.ChangeGroupPriority(_mod, groupIdx, priority); + + ImGuiUtil.HoverTooltip("Group Priority"); + + DrawGroupCombo(group, groupIdx); + ImGui.SameLine(); + + var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, + tt, groupIdx == 0, true)) + _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + + ImGui.SameLine(); + tt = groupIdx == _mod.Groups.Count - 1 + ? "Can not move this group further downwards." + : $"Move this group down to group {groupIdx + 2}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, + tt, groupIdx == _mod.Groups.Count - 1, true)) + _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + + ImGui.SameLine(); + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, + "Edit group description.", false, true)) + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); + + ImGui.SameLine(); + var fileName = group.FileName(_mod.ModPath, groupIdx); + var fileExists = File.Exists(fileName); + tt = fileExists + ? $"Open the {group.Name} json file in the text editor of your choice." + : $"The {group.Name} json file does not exist."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + + UiHelpers.DefaultLineSpace(); + + OptionTable.Draw(this, groupIdx); + } + + /// Draw the table displaying all options and the add new option line. + private static class OptionTable + { + private const string DragDropLabel = "##DragOption"; + + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static int _dragDropGroupIdx = -1; + private static int _dragDropOptionIdx = -1; + + public static void Reset() + { + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + + public static void Draw(ModPanelEditTab panel, int groupIdx) + { + using var table = ImRaii.Table(string.Empty, 6, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, 60 * UiHelpers.Scale); + ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, + UiHelpers.InputTextWidth.X - 72 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); + + var group = panel._mod.Groups[groupIdx]; + for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) + EditOption(panel, group, groupIdx, optionIdx); + + DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); + } + + /// Draw a line for a single option. + private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + { + var option = group[optionIdx]; + using var id = ImRaii.PushId(optionIdx); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable($"Option #{optionIdx + 1}"); + Source(group, groupIdx, optionIdx); + Target(panel, group, groupIdx, optionIdx); + + ImGui.TableNextColumn(); + + + if (group.Type == GroupType.Single) + { + if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) + panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + + ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + else + { + var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; + if (ImGui.Checkbox("##default", ref isDefaultOption)) + panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption + ? group.DefaultSettings | (1u << optionIdx) + : group.DefaultSettings & ~(1u << optionIdx)); + + ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); + } + + ImGui.TableNextColumn(); + if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) + panel._modManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", + false, true)) + panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) + panel._delayedActions.Enqueue(() => panel._modManager.DeleteOption(panel._mod, groupIdx, optionIdx)); + + ImGui.TableNextColumn(); + if (group.Type != GroupType.Multi) + return; + + if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, + 50 * UiHelpers.Scale)) + panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + + ImGuiUtil.HoverTooltip("Option priority."); + } + + /// Draw the line to add a new option. + private static void DrawNewOption(ModPanelEditTab panel, int groupIdx, Vector2 iconButtonSize) + { + var mod = panel._mod; + var group = mod.Groups[groupIdx]; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable($"Option #{group.Count + 1}"); + Target(panel, group, groupIdx, group.Count); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; + if (ImGui.InputTextWithHint("##newOption", "Add new option...", ref tmp, 256)) + { + _newOptionName = tmp; + _newOptionNameIdx = groupIdx; + } + + ImGui.TableNextColumn(); + var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; + var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; + var tt = canAddGroup + ? validName ? "Add a new option to this group." : "Please enter a name for the new option." + : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, + tt, !(canAddGroup && validName), true)) + return; + + panel._modManager.AddOption(mod, groupIdx, _newOptionName); + _newOptionName = string.Empty; + } + + // Handle drag and drop to move options inside a group or into another group. + private static void Source(IModGroup group, int groupIdx, int optionIdx) + { + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) + { + _dragDropGroupIdx = groupIdx; + _dragDropOptionIdx = optionIdx; + } + + ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); + } + + private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) + return; + + if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0) + { + if (_dragDropGroupIdx == groupIdx) + { + var sourceOption = _dragDropOptionIdx; + panel._delayedActions.Enqueue(() => panel._modManager.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceGroupIdx = _dragDropGroupIdx; + var sourceOption = _dragDropOptionIdx; + var sourceGroup = panel._mod.Groups[sourceGroupIdx]; + var currentCount = group.Count; + var option = sourceGroup[sourceOption]; + var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); + panel._delayedActions.Enqueue(() => + { + panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); + panel._modManager.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + }); + } + } + + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + } + + /// Draw a combo to select single or multi group and switch between them. + private void DrawGroupCombo(IModGroup group, int groupIdx) + { + static string GroupTypeName(GroupType type) + => type switch + { + GroupType.Single => "Single Group", + GroupType.Multi => "Multi Group", + _ => "Unknown", + }; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 3 * (UiHelpers.IconButtonSize.X - 4 * UiHelpers.Scale)); + using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); + if (!combo) + return; + + if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) + _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Single); + + var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); + if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) + _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); + + style.Pop(); + if (!canSwitchToMulti) + ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); + } + + /// Handles input text and integers in separate fields without buffers for every single one. + private static class Input + { + // Special field indices to reuse the same string buffer. + public const int None = -1; + public const int Name = -2; + public const int Author = -3; + public const int Version = -4; + public const int Website = -5; + public const int Path = -6; + public const int Description = -7; + + // Temporary strings + private static string? _currentEdit; + private static int? _currentGroupPriority; + private static int _currentField = None; + private static int _optionIndex = None; + + public static void Reset() + { + _currentEdit = null; + _currentGroupPriority = null; + _currentField = None; + _optionIndex = None; + } + + public static bool Text(string label, int field, int option, string oldValue, out string value, uint maxLength, float width) + { + var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; + ImGui.SetNextItemWidth(width); + if (ImGui.InputText(label, ref tmp, maxLength)) + { + _currentEdit = tmp; + _optionIndex = option; + _currentField = field; + } + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null) + { + var ret = _currentEdit != oldValue; + value = _currentEdit; + Reset(); + return ret; + } + + value = string.Empty; + return false; + } + + public static bool Priority(string label, int field, int option, int oldValue, out int value, float width) + { + var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + ImGui.SetNextItemWidth(width); + if (ImGui.InputInt(label, ref tmp, 0, 0)) + { + _currentGroupPriority = tmp; + _optionIndex = option; + _currentField = field; + } + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null) + { + var ret = _currentGroupPriority != oldValue; + value = _currentGroupPriority.Value; + Reset(); + return ret; + } + + value = 0; + return false; + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs new file mode 100644 index 00000000..6c0a7efa --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -0,0 +1,224 @@ +using System; +using System.Diagnostics; +using System.Numerics; +using Dalamud.Interface.GameFonts; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelHeader : IDisposable +{ + /// We use a big, nice game font for the title. + private readonly GameFontHandle _nameFont; + + public ModPanelHeader(DalamudPluginInterface pi) + => _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + + /// + /// Draw the header for the current mod, + /// consisting of its name, version, author and website, if they exist. + /// + public void Draw() + { + var offset = DrawModName(); + DrawVersion(offset); + DrawSecondRow(offset); + } + + /// + /// Update all mod header data. Should someone change frame padding or item spacing, + /// or his default font, this will break, but he will just have to select a different mod to restore. + /// + public void UpdateModData(Mod mod) + { + // Name + var name = $" {mod.Name} "; + if (name != _modName) + { + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + _modName = name; + _modNameWidth = ImGui.CalcTextSize(name).X + 2 * (ImGui.GetStyle().FramePadding.X + 2 * UiHelpers.Scale); + } + + // Author + var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}"; + if (author != _modAuthor) + { + _modAuthor = author; + _modAuthorWidth = ImGui.CalcTextSize(author).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + + // Version + var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty; + if (version != _modVersion) + { + _modVersion = version; + _modVersionWidth = ImGui.CalcTextSize(version).X; + } + + // Website + if (_modWebsite != mod.Website) + { + _modWebsite = mod.Website; + _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); + _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; + _modWebsiteButtonWidth = _websiteValid + ? ImGui.CalcTextSize(_modWebsiteButton).X + 2 * ImGui.GetStyle().FramePadding.X + : ImGui.CalcTextSize(_modWebsiteButton).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + } + + public void Dispose() + { + _nameFont.Dispose(); + } + + // Header data. + private string _modName = string.Empty; + private string _modAuthor = string.Empty; + private string _modVersion = string.Empty; + private string _modWebsite = string.Empty; + private string _modWebsiteButton = string.Empty; + private bool _websiteValid; + + private float _modNameWidth; + private float _modAuthorWidth; + private float _modVersionWidth; + private float _modWebsiteButtonWidth; + private float _secondRowWidth; + + /// + /// Draw the mod name in the game font with a 2px border, centered, + /// with at least the width of the version space to each side. + /// + private float DrawModName() + { + var decidingWidth = Math.Max(_secondRowWidth, ImGui.GetWindowWidth()); + var offsetWidth = (decidingWidth - _modNameWidth) / 2; + var offsetVersion = _modVersion.Length > 0 + ? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + : 0; + var offset = Math.Max(offsetWidth, offsetVersion); + if (offset > 0) + { + ImGui.SetCursorPosX(offset); + } + + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); + return offset; + } + + /// Draw the version in the top-right corner. + private void DrawVersion(float offset) + { + var oldPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(new Vector2(2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X, + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(Colors.MetaInfoText, _modVersion); + ImGui.SetCursorPos(oldPos); + } + + /// + /// Draw author and website if they exist. The website is a button if it is valid. + /// Usually, author begins at the left boundary of the name, + /// and website ends at the right boundary of the name. + /// If their combined width is larger than the name, they are combined-centered. + /// + private void DrawSecondRow(float offset) + { + if (_modAuthor.Length == 0) + { + if (_modWebsiteButton.Length == 0) + { + ImGui.NewLine(); + return; + } + + offset += (_modNameWidth - _modWebsiteButtonWidth) / 2; + ImGui.SetCursorPosX(offset); + DrawWebsite(); + } + else if (_modWebsiteButton.Length == 0) + { + offset += (_modNameWidth - _modAuthorWidth) / 2; + ImGui.SetCursorPosX(offset); + DrawAuthor(); + } + else if (_secondRowWidth < _modNameWidth) + { + ImGui.SetCursorPosX(offset); + DrawAuthor(); + ImGui.SameLine(offset + _modNameWidth - _modWebsiteButtonWidth); + DrawWebsite(); + } + else + { + offset -= (_secondRowWidth - _modNameWidth) / 2; + if (offset > 0) + { + ImGui.SetCursorPosX(offset); + } + + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + } + + /// Draw the author text. + private void DrawAuthor() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGuiUtil.TextColored(Colors.MetaInfoText, "by "); + ImGui.SameLine(); + style.Pop(); + ImGui.TextUnformatted(_modAuthor); + } + + /// + /// Draw either a website button if the source is a valid website address, + /// or a source text if it is not. + /// + private void DrawWebsite() + { + if (_websiteValid) + { + if (ImGui.SmallButton(_modWebsiteButton)) + { + try + { + var process = new ProcessStartInfo(_modWebsite) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + // ignored + } + } + + ImGuiUtil.HoverTooltip(_modWebsite); + } + else + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGuiUtil.TextColored(Colors.MetaInfoText, "from "); + ImGui.SameLine(); + style.Pop(); + ImGui.TextUnformatted(_modWebsite); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs new file mode 100644 index 00000000..1f528904 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -0,0 +1,348 @@ +using System; +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Dalamud.Interface.Components; +using Dalamud.Interface; + +namespace Penumbra.UI.ModTab; + +public class ModPanelSettingsTab : ITab +{ + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly ModFileSystemSelector _selector; + private readonly TutorialService _tutorial; + private readonly PenumbraApi _api; + private readonly Mod.Manager _modManager; + + private bool _inherited; + private ModSettings _settings = null!; + private ModCollection _collection = null!; + private bool _empty; + private int? _currentPriority = null; + + public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector, + TutorialService tutorial, PenumbraApi api, Configuration config) + { + _collectionManager = collectionManager; + _modManager = modManager; + _selector = selector; + _tutorial = tutorial; + _api = api; + _config = config; + } + + public ReadOnlySpan Label + => "Settings"u8; + + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + + public void Reset() + => _currentPriority = null; + + public void DrawContent() + { + using var child = ImRaii.Child("##settings"); + if (!child) + return; + + _settings = _selector.SelectedSettings; + _collection = _selector.SelectedSettingCollection; + _inherited = _collection != _collectionManager.Current; + _empty = _settings == ModSettings.Empty; + + DrawInheritedWarning(); + UiHelpers.DefaultLineSpace(); + _api.InvokePreSettingsPanel(_selector.Selected!.ModPath.Name); + DrawEnabledInput(); + _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + ImGui.SameLine(); + DrawPriorityInput(); + _tutorial.OpenTutorial(BasicTutorialSteps.Priority); + DrawRemoveSettings(); + + if (_selector.Selected!.Groups.Count > 0) + { + var useDummy = true; + foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() + .Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + DrawSingleGroupCombo(group, idx); + } + + useDummy = true; + foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex().Where(g => g.Value.IsOption)) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + switch (group.Type) + { + case GroupType.Multi: + DrawMultiGroup(group, idx); + break; + case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: + DrawSingleGroupRadio(group, idx); + break; + } + } + } + + UiHelpers.DefaultLineSpace(); + _api.InvokePostSettingsPanel(_selector.Selected!.ModPath.Name); + } + + /// Draw a big red bar if the current setting is inherited. + private void DrawInheritedWarning() + { + if (!_inherited) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) + _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, false); + + ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); + } + + /// Draw a checkbox for the enabled status of the mod. + private void DrawEnabledInput() + { + var enabled = _settings.Enabled; + if (ImGui.Checkbox("Enabled", ref enabled)) + { + _modManager.NewMods.Remove(_selector.Selected!); + _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); + } + } + + /// + /// Draw a priority input. + /// Priority is changed on deactivation of the input box. + /// + private void DrawPriorityInput() + { + using var group = ImRaii.Group(); + var priority = _currentPriority ?? _settings.Priority; + ImGui.SetNextItemWidth(50 * UiHelpers.Scale); + if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + _currentPriority = priority; + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) + { + if (_currentPriority != _settings.Priority) + _collectionManager.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); + + _currentPriority = null; + } + + ImGuiUtil.LabeledHelpMarker("Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."); + } + + /// + /// Draw a button to remove the current settings and inherit them instead + /// on the top-right corner of the window/tab. + /// + private void DrawRemoveSettings() + { + const string text = "Inherit Settings"; + if (_inherited || _empty) + return; + + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; + ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if (ImGui.Button(text)) + _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, true); + + ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + + "If no inherited collection has settings for this mod, it will be disabled."); + } + + /// + /// Draw a single group selector as a combo box. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupCombo(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); + using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) + { + if (combo) + for (var idx2 = 0; idx2 < group.Count; ++idx2) + { + id.Push(idx2); + var option = group[idx2]; + if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); + + if (option.Description.Length > 0) + { + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); + } + + if (hovered) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted(option.Description); + } + } + + id.Pop(); + } + } + + ImGui.SameLine(); + if (group.Description.Length > 0) + ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + else + ImGui.TextUnformatted(group.Name); + } + + // Draw a single group selector as a set of radio buttons. + // If a description is provided, add a help marker besides it. + private void DrawSingleGroupRadio(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + Widget.BeginFramedGroup(group.Name, group.Description); + + void DrawOptions() + { + for (var idx = 0; idx < group.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = group[idx]; + if (ImGui.RadioButton(option.Name, selectedOption == idx)) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + DrawCollapseHandling(group, DrawOptions); + + Widget.EndFramedGroup(); + } + + + private void DrawCollapseHandling(IModGroup group, Action draw) + { + if (group.Count <= _config.OptionGroupCollapsibleMin) + { + draw(); + } + else + { + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + if (shown) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy(UiHelpers.IconButtonSize); + using (var _ = ImRaii.Group()) + { + draw(); + } + + var width = ImGui.GetItemRectSize().X; + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(pos); + if (ImGui.Button($"Hide {group.Count} Options", new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + + ImGui.SetCursorPos(endPos); + } + else + { + var max = group.Max(o => ImGui.CalcTextSize(o.Name).X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetFrameHeight() + + ImGui.GetStyle().FramePadding.X; + if (ImGui.Button($"Show {group.Count} Options", new Vector2(max, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + } + } + } + + /// + /// Draw a multi group selector as a bordered set of checkboxes. + /// If a description is provided, add a help marker in the title. + /// + private void DrawMultiGroup(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; + Widget.BeginFramedGroup(group.Name, group.Description); + + void DrawOptions() + { + for (var idx = 0; idx < group.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = group[idx]; + var flag = 1u << idx; + var setting = (flags & flag) != 0; + + if (ImGui.Checkbox(option.Name, ref setting)) + { + flags = setting ? flags | flag : flags & ~flag; + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + } + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + DrawCollapseHandling(group, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); + using var popup = ImRaii.Popup(label); + if (!popup) + return; + + ImGui.TextUnformatted(group.Name); + ImGui.Separator(); + if (ImGui.Selectable("Enable All")) + { + flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + } + + if (ImGui.Selectable("Disable All")) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs new file mode 100644 index 00000000..a946a916 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -0,0 +1,152 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelTabBar +{ + private enum ModPanelTabType + { + Description, + Settings, + ChangedItems, + Conflicts, + Edit, + }; + + public readonly ModPanelSettingsTab Settings; + public readonly ModPanelDescriptionTab Description; + public readonly ModPanelConflictsTab Conflicts; + public readonly ModPanelChangedItemsTab ChangedItems; + public readonly ModPanelEditTab Edit; + private readonly ModEditWindow _modEditWindow; + private readonly Mod.Manager _modManager; + private readonly TutorialService _tutorial; + + public readonly ITab[] Tabs; + private ModPanelTabType _preferredTab = 0; + private Mod? _lastMod = null; + + public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, + ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager, + TutorialService tutorial) + { + _modEditWindow = modEditWindow; + Settings = settings; + Description = description; + Conflicts = conflicts; + ChangedItems = changedItems; + Edit = edit; + _modManager = modManager; + _tutorial = tutorial; + + Tabs = new ITab[] + { + Settings, + Description, + Conflicts, + ChangedItems, + Edit, + }; + } + + public void Draw(Mod mod) + { + var tabBarHeight = ImGui.GetCursorPosY(); + if (_lastMod != mod) + { + _lastMod = mod; + TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(_preferredTab), out _, () => DrawAdvancedEditingButton(mod), Tabs); + } + else + { + TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ReadOnlySpan.Empty, out var label, () => DrawAdvancedEditingButton(mod), + Tabs); + _preferredTab = ToType(label); + } + + DrawFavoriteButton(mod, tabBarHeight); + } + + private ReadOnlySpan ToLabel(ModPanelTabType type) + => type switch + { + ModPanelTabType.Description => Description.Label, + ModPanelTabType.Settings => Settings.Label, + ModPanelTabType.ChangedItems => ChangedItems.Label, + ModPanelTabType.Conflicts => Conflicts.Label, + ModPanelTabType.Edit => Edit.Label, + _ => ReadOnlySpan.Empty, + }; + + private ModPanelTabType ToType(ReadOnlySpan label) + { + if (label == Description.Label) + return ModPanelTabType.Description; + if (label == Settings.Label) + return ModPanelTabType.Settings; + if (label == ChangedItems.Label) + return ModPanelTabType.ChangedItems; + if (label == Conflicts.Label) + return ModPanelTabType.Conflicts; + if (label == Edit.Label) + return ModPanelTabType.Edit; + + return 0; + } + + private void DrawAdvancedEditingButton(Mod mod) + { + if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) + { + _modEditWindow.ChangeMod(mod); + _modEditWindow.ChangeOption(mod.Default); + _modEditWindow.IsOpen = true; + } + + ImGuiUtil.HoverTooltip( + "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" + + "\t\t- file redirections\n" + + "\t\t- file swaps\n" + + "\t\t- metadata manipulations\n" + + "\t\t- model materials\n" + + "\t\t- duplicates\n" + + "\t\t- textures"); + } + + private void DrawFavoriteButton(Mod mod, float height) + { + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + var size = ImGui.CalcTextSize(FontAwesomeIcon.Star.ToIconString()) + ImGui.GetStyle().FramePadding * 2; + var newPos = new Vector2(ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height); + if (ImGui.GetScrollMaxX() > 0) + newPos.X += ImGui.GetScrollX(); + + var rectUpper = ImGui.GetWindowPos() + newPos; + var color = ImGui.IsMouseHoveringRect(rectUpper, rectUpper + size) ? ImGui.GetColorU32(ImGuiCol.Text) : + mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var c = ImRaii.PushColor(ImGuiCol.Text, color) + .Push(ImGuiCol.Button, 0) + .Push(ImGuiCol.ButtonHovered, 0) + .Push(ImGuiCol.ButtonActive, 0); + + ImGui.SetCursorPos(newPos); + if (ImGui.Button(FontAwesomeIcon.Star.ToIconString())) + _modManager.ChangeModFavorite(mod.Index, !mod.Favorite); + } + + var hovered = ImGui.IsItemHovered(); + _tutorial.OpenTutorial(BasicTutorialSteps.Favorites); + + if (hovered) + ImGui.SetTooltip("Favorite"); + } +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs index 0f97d883..1cb787ef 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -42,7 +42,7 @@ public partial class ResourceWatcher private sealed class PathColumn : ColumnString< Record > { public override float Width - => 300 * ImGuiHelpers.GlobalScale; + => 300 * UiHelpers.Scale; public override string ToName( Record item ) => item.Path.ToString(); @@ -51,7 +51,7 @@ public partial class ResourceWatcher => lhs.Path.CompareTo( rhs.Path ); public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.Path, 290 * ImGuiHelpers.GlobalScale ); + => DrawByteString( item.Path, 290 * UiHelpers.Scale ); } private static unsafe void DrawByteString( ByteString path, float length ) @@ -68,7 +68,7 @@ public partial class ResourceWatcher ByteString shortPath; if( fileName != -1 ) { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * ImGuiHelpers.GlobalScale ) ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * UiHelpers.Scale ) ); using var font = ImRaii.PushFont( UiBuilder.IconFont ); ImGui.TextUnformatted( FontAwesomeIcon.EllipsisH.ToIconString() ); ImGui.SameLine(); @@ -98,7 +98,7 @@ public partial class ResourceWatcher => AllFlags = AllRecords; public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.RecordType ); @@ -136,7 +136,7 @@ public partial class ResourceWatcher private sealed class DateColumn : Column< Record > { public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override int Compare( Record lhs, Record rhs ) => lhs.Time.CompareTo( rhs.Time ); @@ -149,7 +149,7 @@ public partial class ResourceWatcher private sealed class CollectionColumn : ColumnString< Record > { public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override string ToName( Record item ) => item.Collection?.Name ?? string.Empty; @@ -158,7 +158,7 @@ public partial class ResourceWatcher private sealed class OriginalPathColumn : ColumnString< Record > { public override float Width - => 200 * ImGuiHelpers.GlobalScale; + => 200 * UiHelpers.Scale; public override string ToName( Record item ) => item.OriginalPath.ToString(); @@ -167,7 +167,7 @@ public partial class ResourceWatcher => lhs.OriginalPath.CompareTo( rhs.OriginalPath ); public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.OriginalPath, 190 * ImGuiHelpers.GlobalScale ); + => DrawByteString( item.OriginalPath, 190 * UiHelpers.Scale ); } private sealed class ResourceCategoryColumn : ColumnFlags< ResourceCategoryFlag, Record > @@ -176,7 +176,7 @@ public partial class ResourceWatcher => AllFlags = ResourceExtensions.AllResourceCategories; public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.Category ); @@ -216,7 +216,7 @@ public partial class ResourceWatcher } public override float Width - => 50 * ImGuiHelpers.GlobalScale; + => 50 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.ResourceType ); @@ -247,7 +247,7 @@ public partial class ResourceWatcher private sealed class HandleColumn : ColumnString< Record > { public override float Width - => 120 * ImGuiHelpers.GlobalScale; + => 120 * UiHelpers.Scale; public override unsafe string ToName( Record item ) => item.Handle == null ? string.Empty : $"0x{( ulong )item.Handle:X}"; @@ -316,7 +316,7 @@ public partial class ResourceWatcher private sealed class CustomLoadColumn : OptBoolColumn { public override float Width - => 60 * ImGuiHelpers.GlobalScale; + => 60 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterFunc( item.CustomLoad ); @@ -328,7 +328,7 @@ public partial class ResourceWatcher private sealed class SynchronousLoadColumn : OptBoolColumn { public override float Width - => 45 * ImGuiHelpers.GlobalScale; + => 45 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterFunc( item.Synchronously ); @@ -340,7 +340,7 @@ public partial class ResourceWatcher private sealed class RefCountColumn : Column< Record > { public override float Width - => 30 * ImGuiHelpers.GlobalScale; + => 30 * UiHelpers.Scale; public override void DrawColumn( Record item, int _ ) => ImGuiUtil.RightAlign( item.RefCount.ToString() ); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index e73bba63..27abb6dc 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -115,7 +115,7 @@ public partial class ResourceWatcher : IDisposable, ITab var tmp = _logFilter; var invalidRegex = _logRegex == null && _logFilter.Length > 0; using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, invalidRegex); if (ImGui.InputTextWithHint("##logFilter", "If path matches this Regex...", ref tmp, 256)) UpdateFilter(tmp, true); } @@ -151,7 +151,7 @@ public partial class ResourceWatcher : IDisposable, ITab private void DrawMaxEntries() { - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(80 * UiHelpers.Scale); ImGui.InputInt("Max. Entries", ref _newMaxEntries, 0, 0); var change = ImGui.IsItemDeactivatedAfterEdit(); if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs new file mode 100644 index 00000000..22eecf7e --- /dev/null +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs; + +public class ChangedItemsTab : ITab +{ + private readonly ModCollection.Manager _collectionManager; + private readonly PenumbraApi _api; + + public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api) + { + _collectionManager = collectionManager; + _api = api; + } + + public ReadOnlySpan Label + => "Changed Items"u8; + + private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemModFilter = LowerString.Empty; + + public void DrawContent() + { + var varWidth = DrawFilters(); + using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); + if (!child) + return; + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips(height); + using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); + if (!list) + return; + + const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; + ImGui.TableSetupColumn("items", flags, 400 * UiHelpers.Scale); + ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale); + ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale); + + var items = _collectionManager.Current.ChangedItems; + var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty + ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) + : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); + ImGuiClip.DrawEndDummy(rest, height); + } + + /// Draw a pair of filters and return the variable width of the flexible column. + private float DrawFilters() + { + var varWidth = ImGui.GetContentRegionAvail().X + - 400 * UiHelpers.Scale + - ImGui.GetStyle().ItemSpacing.X; + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + LowerString.InputWithHint("##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128); + ImGui.SameLine(); + ImGui.SetNextItemWidth(varWidth); + LowerString.InputWithHint("##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128); + return varWidth; + } + + /// Apply the current filters. + private bool FilterChangedItem(KeyValuePair, object?)> item) + => (_changedItemFilter.IsEmpty + || UiHelpers.ChangedItemName(item.Key, item.Value.Item2) + .Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase)) + && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); + + /// Draw a full column for a changed item. + private void DrawChangedItemColumn(KeyValuePair, object?)> item) + { + ImGui.TableNextColumn(); + UiHelpers.DrawChangedItem(_api, item.Key, item.Value.Item2, false); + ImGui.TableNextColumn(); + if (item.Value.Item1.Count > 0) + { + ImGui.TextUnformatted(item.Value.Item1[0].Name); + if (item.Value.Item1.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", item.Value.Item1.Skip(1).Select(m => m.Name))); + } + + ImGui.TableNextColumn(); + if (!UiHelpers.GetChangedItemObject(item.Value.Item2, out var text)) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGuiUtil.RightAlign(text); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs new file mode 100644 index 00000000..4825b4aa --- /dev/null +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -0,0 +1,298 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Services; +using Penumbra.UI.CollectionTab; + +namespace Penumbra.UI.Tabs; + +public class CollectionsTab : IDisposable, ITab +{ + private readonly CommunicatorService _communicator; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly TutorialService _tutorial; + private readonly SpecialCombo _specialCollectionCombo; + + private readonly CollectionSelector _collectionsWithEmpty; + private readonly CollectionSelector _collectionSelector; + private readonly InheritanceUi _inheritance; + private readonly IndividualCollectionUi _individualCollections; + + public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager, + TutorialService tutorial, Configuration config) + { + _communicator = communicator; + _collectionManager = collectionManager; + _tutorial = tutorial; + _config = config; + _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); + _collectionsWithEmpty = new CollectionSelector(_collectionManager, + () => _collectionManager.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); + _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.OrderBy(c => c.Name).ToList()); + _inheritance = new InheritanceUi(_collectionManager); + _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); + + _communicator.CollectionChange.Event += _individualCollections.UpdateIdentifiers; + } + + public ReadOnlySpan Label + => "Collections"u8; + + /// Draw a collection selector of a certain width for a certain type. + public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) + => (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); + + public void Dispose() + => _communicator.CollectionChange.Event -= _individualCollections.UpdateIdentifiers; + + /// Draw a tutorial step regardless of tab selection. + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); + + public void DrawContent() + { + using var child = ImRaii.Child("##collections", -Vector2.One); + if (child) + { + DrawActiveCollectionSelectors(); + DrawMainSelectors(); + } + } + + #region New Collections + + // Input text fields. + private string _newCollectionName = string.Empty; + private bool _canAddCollection; + + /// + /// Create a new collection that is either empty or a duplicate of the current collection. + /// Resets the new collection name. + /// + private void CreateNewCollection(bool duplicate) + { + if (_collectionManager.AddCollection(_newCollectionName, duplicate ? _collectionManager.Current : null)) + _newCollectionName = string.Empty; + } + + /// Draw the Clean Unused Settings button if there are any. + private void DrawCleanCollectionButton(Vector2 width) + { + if (!_collectionManager.Current.HasUnusedSettings) + return; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton( + $"Clean {_collectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." + , false)) + _collectionManager.Current.CleanUnavailableSettings(); + } + + /// Draw the new collection input as well as its buttons. + private void DrawNewCollectionInput(Vector2 width) + { + // Input for new collection name. Also checks for validity when changed. + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) + _canAddCollection = _collectionManager.CanAddCollection(_newCollectionName, out _); + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of enabled mods."); + + // Creation buttons. + var tt = _canAddCollection + ? string.Empty + : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; + if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) + CreateNewCollection(false); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection)) + CreateNewCollection(true); + } + + #endregion + + #region Collection Selection + + /// Draw all collection assignment selections. + private void DrawActiveCollectionSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); + if (!open) + return; + + UiHelpers.DefaultLineSpace(); + + DrawDefaultCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); + DrawInterfaceCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); + UiHelpers.DefaultLineSpace(); + + DrawSpecialAssignments(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); + UiHelpers.DefaultLineSpace(); + + _individualCollections.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); + UiHelpers.DefaultLineSpace(); + } + + private void DrawCurrentCollectionSelector(Vector2 width) + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection, + "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); + + // Deletion conditions. + var deleteCondition = _collectionManager.Current.Name != ModCollection.DefaultCollection; + var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); + var tt = deleteCondition + ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." + : $"You can not delete the collection {ModCollection.DefaultCollection}."; + + if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) + _collectionManager.RemoveCollection(_collectionManager.Current); + + DrawCleanCollectionButton(width); + } + + /// Draw the selector for the default collection assignment. + private void DrawDefaultCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, + $"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," + + "as well as any character for whom no more specific conditions from below apply."); + } + + /// Draw the selector for the interface collection assignment. + private void DrawInterfaceCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, + $"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); + } + + /// Description for character groups used in multiple help markers. + private const string CharacterGroupDescription = + $"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {TutorialService.DefaultCollection},\n" + + $"but all {TutorialService.IndividualAssignments} take precedence before them."; + + /// Draw the entire group assignment section. + private void DrawSpecialAssignments() + { + using var _ = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(TutorialService.CharacterGroups); + ImGuiComponents.HelpMarker(CharacterGroupDescription); + ImGui.Separator(); + DrawSpecialCollections(); + ImGui.Dummy(Vector2.Zero); + DrawNewSpecialCollection(); + } + + /// Draw a new combo to select special collections as well as button to create it. + private void DrawNewSpecialCollection() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (_specialCollectionCombo.CurrentIdx == -1 + || _collectionManager.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) + { + _specialCollectionCombo.ResetFilter(); + _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special + .IndexOf(t => _collectionManager.ByType(t.Item1) == null); + } + + if (_specialCollectionCombo.CurrentType == null) + return; + + _specialCollectionCombo.Draw(); + ImGui.SameLine(); + var disabled = _specialCollectionCombo.CurrentType == null; + var tt = disabled + ? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" + + CharacterGroupDescription + : CharacterGroupDescription; + if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) + return; + + _collectionManager.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); + _specialCollectionCombo.CurrentIdx = -1; + } + + #endregion + + #region Current Collection Editing + + /// Draw the current collection selection, the creation of new collections and the inheritance block. + private void DrawMainSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); + if (!open) + return; + + var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + UiHelpers.DefaultLineSpace(); + + DrawCurrentCollectionSelector(width); + _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + UiHelpers.DefaultLineSpace(); + + DrawNewCollectionInput(width); + UiHelpers.DefaultLineSpace(); + + _inheritance.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); + } + + /// Draw all currently set special collections. + private void DrawSpecialCollections() + { + foreach (var (type, name, desc) in CollectionTypeExtensions.Special) + { + var collection = _collectionManager.ByType(type); + if (collection == null) + continue; + + using var id = ImRaii.PushId((int)type); + DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, + false, true)) + { + _collectionManager.RemoveSpecialCollection(type); + _specialCollectionCombo.ResetFilter(); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker(name, desc); + } + } + + #endregion +} diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs new file mode 100644 index 00000000..0f91f3dc --- /dev/null +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -0,0 +1,67 @@ +using System; +using ImGuiNET; +using OtterGui.Widgets; +using Penumbra.Api.Enums; + +namespace Penumbra.UI.Tabs; + +public class ConfigTabBar +{ + public readonly SettingsTab Settings; + public readonly ModsTab Mods; + public readonly CollectionsTab Collections; + public readonly ChangedItemsTab ChangedItems; + public readonly EffectiveTab Effective; + public readonly DebugTab Debug; + public readonly ResourceTab Resource; + public readonly ResourceWatcher Watcher; + + public readonly ITab[] Tabs; + + /// The tab to select on the next Draw call, if any. + public TabType SelectTab = TabType.None; + + public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, + DebugTab debug, ResourceTab resource, ResourceWatcher watcher) + { + Settings = settings; + Mods = mods; + Collections = collections; + ChangedItems = changedItems; + Effective = effective; + Debug = debug; + Resource = resource; + Watcher = watcher; + Tabs = new ITab[] + { + Settings, + Mods, + Collections, + ChangedItems, + Effective, + Debug, + Resource, + Watcher, + }; + } + + public void Draw() + { + if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out _, () => { }, Tabs)) + SelectTab = TabType.None; + } + + private ReadOnlySpan ToLabel(TabType type) + => type switch + { + TabType.Settings => Settings.Label, + TabType.Mods => Mods.Label, + TabType.Collections => Collections.Label, + TabType.ChangedItems => ChangedItems.Label, + TabType.EffectiveChanges => Effective.Label, + TabType.ResourceWatcher => Watcher.Label, + TabType.Debug => Debug.Label, + TabType.ResourceManager => Resource.Label, + _ => ReadOnlySpan.Empty, + }; +} diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs new file mode 100644 index 00000000..17e3ccea --- /dev/null +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -0,0 +1,604 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Group; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using ImGuiNET; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Files; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; +using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.Util; +using static OtterGui.Raii.ImRaii; +using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; +using CharacterUtility = Penumbra.Interop.CharacterUtility; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; + +namespace Penumbra.UI.Tabs; + +public class DebugTab : ITab +{ + private readonly StartTracker _timer; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly Mod.Manager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly PathResolver _pathResolver; + private readonly ActorService _actorService; + private readonly DalamudServices _dalamud; + private readonly StainService _stains; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly ResourceManagerService _resourceManager; + private readonly PenumbraIpcProviders _ipc; + + public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, + ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, PathResolver pathResolver, ActorService actorService, + DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, + ResourceManagerService resourceManager, PenumbraIpcProviders ipc) + { + _timer = timer; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _validityChecker = validityChecker; + _modManager = modManager; + _httpApi = httpApi; + _pathResolver = pathResolver; + _actorService = actorService; + _dalamud = dalamud; + _stains = stains; + _characterUtility = characterUtility; + _residentResources = residentResources; + _resourceManager = resourceManager; + _ipc = ipc; + } + + public ReadOnlySpan Label + => "Debug"u8; + + public bool IsVisible + => _config.DebugMode; + +#if DEBUG + private const string DebugVersionString = "(Debug)"; +#else + private const string DebugVersionString = "(Release)"; +#endif + + public void DrawContent() + { + using var child = Child("##DebugTab", -Vector2.One); + if (!child) + return; + + DrawDebugTabGeneral(); + DrawPerformanceTab(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); + DrawActorsDebug(); + ImGui.NewLine(); + DrawDebugCharacterUtility(); + ImGui.NewLine(); + DrawStainTemplates(); + ImGui.NewLine(); + DrawDebugTabMetaLists(); + ImGui.NewLine(); + DrawDebugResidentResources(); + ImGui.NewLine(); + DrawResourceProblems(); + ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawDebugTabIpc(); + ImGui.NewLine(); + } + + /// Draw general information about mod and collection state. + private void DrawDebugTabGeneral() + { + if (!ImGui.CollapsingHeader("General")) + return; + + using var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, ImGui.GetTextLineHeightWithSpacing() * 1)); + if (!table) + return; + + PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); + PrintValue("Git Commit Hash", _validityChecker.CommitHash); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Current.Name); + PrintValue(" has Cache", _collectionManager.Current.HasCache.ToString()); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Default.Name); + PrintValue(" has Cache", _collectionManager.Default.HasCache.ToString()); + PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); + PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); + PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); + PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); + PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); + PrintValue("Path Resolver Enabled", _pathResolver.Enabled.ToString()); + PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); + } + + private void DrawPerformanceTab() + { + ImGui.NewLine(); + if (ImGui.CollapsingHeader("Performance")) + return; + + using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (start) + { + _timer.Draw("##startTimer", TimingExtensions.ToName); + ImGui.NewLine(); + } + } + + _performance.Draw("##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName); + } + + private unsafe void DrawActorsDebug() + { + if (!ImGui.CollapsingHeader("Actors")) + return; + + using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + void DrawSpecial(string name, ActorIdentifier id) + { + if (!id.IsValid) + return; + + ImGuiUtil.DrawTableColumn(name); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); + ImGuiUtil.DrawTableColumn(string.Empty); + } + + DrawSpecial("Current Player", _actorService.AwaitedService.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actorService.AwaitedService.GetInspectPlayer()); + DrawSpecial("Current Card", _actorService.AwaitedService.GetCardPlayer()); + DrawSpecial("Current Glamour", _actorService.AwaitedService.GetGlamourPlayer()); + + foreach (var obj in DalamudServices.SObjects) + { + ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); + ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); + ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); + var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn(id); + } + } + + /// + /// Draw information about which draw objects correspond to which game objects + /// and which paths are due to be loaded by which collection. + /// + private unsafe void DrawPathResolverDebug() + { + if (!ImGui.CollapsingHeader("Path Resolver")) + return; + + ImGui.TextUnformatted( + $"Last Game Object: 0x{_pathResolver.LastGameObject:X} ({_pathResolver.LastGameObjectData.ModCollection.Name})"); + using (var drawTree = TreeNode("Draw Object to Object")) + { + if (drawTree) + { + using var table = Table("###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (ptr, (c, idx)) in _pathResolver.DrawObjectMap) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(ptr.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(idx.ToString()); + ImGui.TableNextColumn(); + var obj = (GameObject*)_dalamud.Objects.GetObjectAddress(idx); + var (address, name) = + obj != null ? ($"0x{(ulong)obj:X}", new ByteString(obj->Name).ToString()) : ("NULL", "NULL"); + ImGui.TextUnformatted(address); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(c.ModCollection.Name); + } + } + } + + using (var pathTree = TreeNode("Path Collections")) + { + if (pathTree) + { + using var table = Table("###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (path, collection) in _pathResolver.PathCollections) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collection.ModCollection.Name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collection.AssociatedGameObject.ToString("X")); + } + } + } + + using (var resourceTree = TreeNode("Subfile Collections")) + { + if (resourceTree) + { + using var table = Table("###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGuiUtil.DrawTableColumn("Current Mtrl Data"); + ImGuiUtil.DrawTableColumn(_pathResolver.CurrentMtrlData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentMtrlData.AssociatedGameObject:X}"); + + ImGuiUtil.DrawTableColumn("Current Avfx Data"); + ImGuiUtil.DrawTableColumn(_pathResolver.CurrentAvfxData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentAvfxData.AssociatedGameObject:X}"); + + ImGuiUtil.DrawTableColumn("Current Resources"); + ImGuiUtil.DrawTableColumn(_pathResolver.SubfileCount.ToString()); + ImGui.TableNextColumn(); + + foreach (var (resource, resolve) in _pathResolver.ResourceCollections) + { + ImGuiUtil.DrawTableColumn($"0x{resource:X}"); + ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); + } + } + } + } + + using (var identifiedTree = TreeNode("Identified Collections")) + { + if (identifiedTree) + { + using var table = Table("##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (address, identifier, collection) in PathResolver.IdentifiedCache) + { + ImGuiUtil.DrawTableColumn($"0x{address:X}"); + ImGuiUtil.DrawTableColumn(identifier.ToString()); + ImGuiUtil.DrawTableColumn(collection.Name); + } + } + } + + using (var cutsceneTree = TreeNode("Cutscene Actors")) + { + if (cutsceneTree) + { + using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (idx, actor) in _pathResolver.CutsceneActors) + { + ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}"); + ImGuiUtil.DrawTableColumn(actor.Name.ToString()); + } + } + } + + using (var groupTree = TreeNode("Group")) + { + if (groupTree) + { + using var table = Table("###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGuiUtil.DrawTableColumn("Group Members"); + ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MemberCount.ToString()); + for (var i = 0; i < 8; ++i) + { + ImGuiUtil.DrawTableColumn($"Member #{i}"); + var member = GroupManager.Instance()->GetPartyMemberByIndex(i); + ImGuiUtil.DrawTableColumn(member == null ? "NULL" : new ByteString(member->Name).ToString()); + } + } + } + } + + using (var bannerTree = TreeNode("Party Banner")) + { + if (bannerTree) + { + var agent = &AgentBannerParty.Instance()->AgentBannerInterface; + if (agent->Data == null) + agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + + if (agent->Data != null) + { + using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + for (var i = 0; i < 8; ++i) + { + var c = agent->Character(i); + ImGuiUtil.DrawTableColumn($"Character {i}"); + var name = c->Name1.ToString(); + ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})"); + } + } + else + { + ImGui.TextUnformatted("INACTIVE"); + } + } + } + } + + private void DrawStainTemplates() + { + if (!ImGui.CollapsingHeader("Staining Templates")) + return; + + foreach (var (key, data) in _stains.StmFile.Entries) + { + using var tree = TreeNode($"Template {key}"); + if (!tree) + continue; + + using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + { + var (r, g, b) = data.DiffuseEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + (r, g, b) = data.SpecularEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + (r, g, b) = data.EmissiveEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + var a = data.SpecularPowerEntries[i]; + ImGuiUtil.DrawTableColumn($"{a:F6}"); + + a = data.GlossEntries[i]; + ImGuiUtil.DrawTableColumn($"{a:F6}"); + } + } + } + + /// + /// Draw information about the character utility class from SE, + /// displaying all files, their sizes, the default files and the default sizes. + /// + private unsafe void DrawDebugCharacterUtility() + { + if (!ImGui.CollapsingHeader("Character Utility")) + return; + + using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + { + var idx = CharacterUtility.RelevantIndices[i]; + var intern = new CharacterUtility.InternalIndex(i); + var resource = _characterUtility.Address->Resource(idx); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + ImGui.TableNextColumn(); + ImGui.Selectable($"0x{resource->GetData().Data:X}"); + if (ImGui.IsItemClicked()) + { + var (data, length) = resource->GetData(); + if (data != nint.Zero && length > 0) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + } + + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{resource->GetData().Length}"); + ImGui.TableNextColumn(); + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + } + + private void DrawDebugTabMetaLists() + { + if (!ImGui.CollapsingHeader("Metadata Changes")) + return; + + using var table = Table("##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var list in _characterUtility.Lists) + { + ImGuiUtil.DrawTableColumn(list.GlobalIndex.ToString()); + ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); + ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); + } + } + + /// Draw information about the resident resource files. + private unsafe void DrawDebugResidentResources() + { + if (!ImGui.CollapsingHeader("Resident Resources")) + return; + + if (_residentResources.Address == null || _residentResources.Address->NumResources == 0) + return; + + using var table = Table("##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var i = 0; i < _residentResources.Address->NumResources; ++i) + { + var resource = _residentResources.Address->ResourceList[i]; + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + } + } + + /// Draw information about the models, materials and resources currently loaded by the local player. + private unsafe void DrawPlayerModelInfo() + { + var player = _dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) + return; + + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); + if (model == null) + return; + + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) + { + if (t1) + { + ImGuiUtil.DrawTableColumn("Flags"); + ImGuiUtil.DrawTableColumn($"{model->UnkFlags_01:X2}"); + ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded"); + ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}"); + ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded"); + ImGuiUtil.DrawTableColumn($"{model->HasModelFilesInSlotLoaded:X8}"); + } + } + + using var table = Table($"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableNextColumn(); + ImGui.TableHeader("Slot"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Imc Ptr"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Imc File"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model Ptr"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model File"); + + for (var i = 0; i < model->SlotCount; ++i) + { + var imc = (ResourceHandle*)model->IMCArray[i]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"Slot {i}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(imc == null ? "NULL" : $"0x{(ulong)imc:X}"); + ImGui.TableNextColumn(); + if (imc != null) + UiHelpers.Text(imc); + + var mdl = (RenderModel*)model->ModelArray[i]; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mdl == null ? "NULL" : $"0x{(ulong)mdl:X}"); + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + continue; + + ImGui.TableNextColumn(); + { + UiHelpers.Text(mdl->ResourceHandle); + } + } + } + + /// Draw resources with unusual reference count. + private unsafe void DrawResourceProblems() + { + var header = ImGui.CollapsingHeader("Resource Problems"); + ImGuiUtil.HoverTooltip("Draw resources with unusually high reference count to detect overflows."); + if (!header) + return; + + using var table = Table("##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + _resourceManager.IterateResources((_, r) => + { + if (r->RefCount < 10000) + return; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->Category.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->FileType.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->Id.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(((ulong)r).ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->RefCount.ToString()); + ImGui.TableNextColumn(); + ref var name = ref r->FileName; + if (name.Capacity > 15) + UiHelpers.Text(name.BufferPtr, (int)name.Length); + else + fixed (byte* ptr = name.Buffer) + { + UiHelpers.Text(ptr, (int)name.Length); + } + }); + } + + + /// Draw information about IPC options and availability. + private void DrawDebugTabIpc() + { + if (!ImGui.CollapsingHeader("IPC")) + { + _ipc.Tester.UnsubscribeEvents(); + return; + } + + _ipc.Tester.Draw(); + } + + /// Helper to print a property and its value in a 2-column table. + private static void PrintValue(string name, string value) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(value); + } +} diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs new file mode 100644 index 00000000..45e244d8 --- /dev/null +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Tabs; + +public class EffectiveTab : ITab +{ + private readonly ModCollection.Manager _collectionManager; + + public EffectiveTab(ModCollection.Manager collectionManager) + => _collectionManager = collectionManager; + + public ReadOnlySpan Label + => "Effective Changes"u8; + + public void DrawContent() + { + SetupEffectiveSizes(); + DrawFilters(); + using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); + if (!child) + return; + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips(height); + using var table = ImRaii.Table("##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableSetupColumn("##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); + ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); + + DrawEffectiveRows(_collectionManager.Current, skips, height, + _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); + } + + // Sizes + private float _effectiveLeftTextLength; + private float _effectiveRightTextLength; + private float _effectiveUnscaledArrowLength; + private float _effectiveArrowLength; + + // Filters + private LowerString _effectiveGamePathFilter = LowerString.Empty; + private LowerString _effectiveFilePathFilter = LowerString.Empty; + + /// Setup table sizes. + private void SetupEffectiveSizes() + { + if (_effectiveUnscaledArrowLength == 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + _effectiveUnscaledArrowLength = + ImGui.CalcTextSize(FontAwesomeIcon.LongArrowAltLeft.ToIconString()).X / UiHelpers.Scale; + } + + _effectiveArrowLength = _effectiveUnscaledArrowLength * UiHelpers.Scale; + _effectiveLeftTextLength = 450 * UiHelpers.Scale; + _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; + } + + /// Draw the header line for filters. + private void DrawFilters() + { + var tmp = _effectiveGamePathFilter.Text; + ImGui.SetNextItemWidth(_effectiveLeftTextLength); + if (ImGui.InputTextWithHint("##gamePathFilter", "Filter game path...", ref tmp, 256)) + _effectiveGamePathFilter = tmp; + + ImGui.SameLine(_effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X); + ImGui.SetNextItemWidth(-1); + tmp = _effectiveFilePathFilter.Text; + if (ImGui.InputTextWithHint("##fileFilter", "Filter file path...", ref tmp, 256)) + _effectiveFilePathFilter = tmp; + } + + /// Draw all rows for one collection respecting filters and using clipping. + private void DrawEffectiveRows(ModCollection active, int skips, float height, bool hasFilters) + { + // We can use the known counts if no filters are active. + var stop = hasFilters + ? ImGuiClip.FilteredClippedDraw(active.ResolvedFiles, skips, CheckFilters, DrawLine) + : ImGuiClip.ClippedDraw(active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count); + + var m = active.MetaCache; + // If no meta manipulations are active, we can just draw the end dummy. + if (m is { Count: > 0 }) + { + // Filters mean we can not use the known counts. + if (hasFilters) + { + var it2 = m.Select(p => (p.Key.ToString(), p.Value.Name)); + if (stop >= 0) + { + ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height); + } + else + { + stop = ImGuiClip.FilteredClippedDraw(it2, skips, CheckFilters, DrawLine, ~stop); + ImGuiClip.DrawEndDummy(stop, height); + } + } + else + { + if (stop >= 0) + { + ImGuiClip.DrawEndDummy(stop + m.Count, height); + } + else + { + stop = ImGuiClip.ClippedDraw(m, skips, DrawLine, m.Count, ~stop); + ImGuiClip.DrawEndDummy(stop, height); + } + } + } + else + { + ImGuiClip.DrawEndDummy(stop, height); + } + } + + /// Draw a line for a game path and its redirected file. + private static void DrawLine(KeyValuePair pair) + { + var (path, name) = pair; + ImGui.TableNextColumn(); + UiHelpers.CopyOnClickSelectable(path.Path); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); + ImGui.TableNextColumn(); + UiHelpers.CopyOnClickSelectable(name.Path.InternalName); + ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); + } + + /// Draw a line for a path and its name. + private static void DrawLine((string, LowerString) pair) + { + var (path, name) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(path); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(name); + } + + /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. + private static void DrawLine(KeyValuePair pair) + { + var (manipulation, mod) = pair; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(manipulation.ToString()); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(mod.Name); + } + + /// Check filters for file replacements. + private bool CheckFilters(KeyValuePair kvp) + { + var (gamePath, fullPath) = kvp; + if (_effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains(_effectiveGamePathFilter.Lower)) + return false; + + return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains(_effectiveFilePathFilter.Lower); + } + + /// Check filters for meta manipulations. + private bool CheckFilters((string, LowerString) kvp) + { + var (name, path) = kvp; + if (_effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains(_effectiveGamePathFilter.Lower)) + return false; + + return _effectiveFilePathFilter.Length == 0 || path.Contains(_effectiveFilePathFilter.Lower); + } +} diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs new file mode 100644 index 00000000..6490dc2a --- /dev/null +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -0,0 +1,208 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.UI.Classes; +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Interop; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.ModTab; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; + +namespace Penumbra.UI.Tabs; + +public class ModsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly ModPanel _panel; + private readonly TutorialService _tutorial; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + private readonly RedrawService _redrawService; + private readonly Configuration _config; + private readonly CollectionsTab _collectionsTab; + + public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel, + TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) + { + _modManager = modManager; + _collectionManager = collectionManager; + _selector = selector; + _panel = panel; + _tutorial = tutorial; + _redrawService = redrawService; + _config = config; + _collectionsTab = collectionsTab; + } + + public bool IsVisible + => _modManager.Valid; + + public ReadOnlySpan Label + => "Mods"u8; + + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.Mods); + + public Mod SelectMod + { + set => _selector.SelectByValue(value); + } + + public void DrawContent() + { + try + { + _selector.Draw(GetModSelectorSize()); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawHeaderLine(); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + true, ImGuiWindowFlags.HorizontalScrollbar)) + { + style.Pop(); + if (child) + _panel.Draw(); + + style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + } + + style.Push(ImGuiStyleVar.FrameRounding, 0); + DrawRedrawLine(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); + Penumbra.Log.Error($"{_modManager.Count} Mods\n" + + $"{_collectionManager.Current.AnonymizedName} Current Collection\n" + + $"{_collectionManager.Current.Settings.Count} Settings\n" + + $"{_selector.SortMode.Name} Sort Mode\n" + + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join(", ", _collectionManager.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + } + } + + private void DrawRedrawLine() + { + if (Penumbra.Config.HideRedrawBar) + { + _tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); + return; + } + + var frameHeight = new Vector2(0, ImGui.GetFrameHeight()); + var frameColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using (var _ = ImRaii.Group()) + { + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.DrawTextButton(FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor); + ImGui.SameLine(); + } + + ImGuiUtil.DrawTextButton("Redraw: ", frameHeight, frameColor); + } + + var hovered = ImGui.IsItemHovered(); + _tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); + if (hovered) + ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); + + void DrawButton(Vector2 size, string label, string lower) + { + if (ImGui.Button(label, size)) + { + if (lower.Length > 0) + _redrawService.RedrawObject(lower, RedrawType.Redraw); + else + _redrawService.RedrawAll(RedrawType.Redraw); + } + + ImGuiUtil.HoverTooltip(lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'."); + } + + using var disabled = ImRaii.Disabled(DalamudServices.SClientState.LocalPlayer == null); + ImGui.SameLine(); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; + DrawButton(buttonWidth, "All", string.Empty); + ImGui.SameLine(); + DrawButton(buttonWidth, "Self", "self"); + ImGui.SameLine(); + DrawButton(buttonWidth, "Target", "target"); + ImGui.SameLine(); + DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus"); + } + + /// Draw the header line that can quick switch between collections. + private void DrawHeaderLine() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 8f, 0); + + using (var _ = ImRaii.Group()) + { + DrawDefaultCollectionButton(3 * buttonSize); + ImGui.SameLine(); + DrawInheritedCollectionButton(3 * buttonSize); + ImGui.SameLine(); + _collectionsTab.DrawCollectionSelector("##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false); + } + + _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); + + if (!_collectionManager.CurrentCollectionInUse) + ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); + } + + private void DrawDefaultCollectionButton(Vector2 width) + { + var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Default.Name})"; + var isCurrent = _collectionManager.Default == _collectionManager.Current; + var isEmpty = _collectionManager.Default == ModCollection.Empty; + var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." + : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." + : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) + _collectionManager.SetCollection(_collectionManager.Default, CollectionType.Current); + } + + private void DrawInheritedCollectionButton(Vector2 width) + { + var noModSelected = _selector.Selected == null; + var collection = _selector.SelectedSettingCollection; + var modInherited = collection != _collectionManager.Current; + var (name, tt) = (noModSelected, modInherited) switch + { + (true, _) => ("Inherited Collection", "No mod selected."), + (false, true) => ($"Inherited Collection ({collection.Name})", + "Set the current collection to the collection the selected mod inherits its settings from."), + (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), + }; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) + _collectionManager.SetCollection(collection, CollectionType.Current); + } + + /// Get the correct size for the mod selector based on current config. + private float GetModSelectorSize() + { + var absoluteSize = Math.Clamp(_config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, + Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); + var relativeSize = _config.ScaleModSelector + ? Math.Clamp(_config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) + : 0; + return !_config.ScaleModSelector + ? absoluteSize + : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + } +} diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs new file mode 100644 index 00000000..b77d88a5 --- /dev/null +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Game; +using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using FFXIVClientStructs.STD; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Interop.Loader; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Tabs; + +public class ResourceTab : ITab +{ + private readonly Configuration _config; + private readonly ResourceManagerService _resourceManager; + private readonly SigScanner _sigScanner; + + public ResourceTab(Configuration config, ResourceManagerService resourceManager, SigScanner sigScanner) + { + _config = config; + _resourceManager = resourceManager; + _sigScanner = sigScanner; + } + + public ReadOnlySpan Label + => "Resource Manager"u8; + + public bool IsVisible + => _config.DebugMode; + + /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. + public void DrawContent() + { + // Filter for resources containing the input string. + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength); + + using var child = ImRaii.Child("##ResourceManagerTab", -Vector2.One); + if (!child) + return; + + unsafe + { + Penumbra.ResourceManagerService.IterateGraphs(DrawCategoryContainer); + } + + ImGui.NewLine(); + unsafe + { + ImGui.TextUnformatted( + $"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress:X})"); + ImGui.TextUnformatted($"Actual Address: 0x{(ulong)_resourceManager.ResourceManager:X}"); + } + } + + private float _hashColumnWidth; + private float _pathColumnWidth; + private float _refsColumnWidth; + private string _resourceManagerFilter = string.Empty; + + /// Draw a single resource map. + private unsafe void DrawResourceMap(ResourceCategory category, uint ext, StdMap>* map) + { + if (map == null) + return; + + var label = GetNodeLabel((uint)category, ext, map->Count); + using var tree = ImRaii.TreeNode(label); + if (!tree || map->Count == 0) + return; + + using var table = ImRaii.Table("##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableSetupColumn("Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth); + ImGui.TableSetupColumn("Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth); + ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth); + ImGui.TableHeadersRow(); + + _resourceManager.IterateResourceMap(map, (hash, r) => + { + // Filter unwanted names. + if (_resourceManagerFilter.Length != 0 + && !r->FileName.ToString().Contains(_resourceManagerFilter, StringComparison.OrdinalIgnoreCase)) + return; + + var address = $"0x{(ulong)r:X}"; + ImGuiUtil.TextNextColumn($"0x{hash:X8}"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(address); + + var resource = (Interop.Structs.ResourceHandle*)r; + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + if (ImGui.IsItemClicked()) + { + var data = Interop.Structs.ResourceHandle.GetData(resource); + if (data != null) + { + var length = (int)Interop.Structs.ResourceHandle.GetLength(resource); + ImGui.SetClipboardText(string.Join(" ", + new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); + } + } + + ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any."); + + ImGuiUtil.TextNextColumn(r->RefCount.ToString()); + }); + } + + /// Draw a full category for the resource manager. + private unsafe void DrawCategoryContainer(ResourceCategory category, + StdMap>>>* map, int idx) + { + if (map == null) + return; + + using var tree = ImRaii.TreeNode($"({(uint)category:D2}) {category} (Ex {idx}) - {map->Count}###{(uint)category}_{idx}"); + if (tree) + { + SetTableWidths(); + _resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); + } + } + + /// Obtain a label for an extension node. + private static string GetNodeLabel(uint label, uint type, ulong count) + { + var (lowest, mid1, mid2, highest) = Functions.SplitBytes(type); + return highest == 0 + ? $"({type:X8}) {(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}" + : $"({type:X8}) {(char)highest}{(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}"; + } + + /// Set the widths for a resource table. + private void SetTableWidths() + { + _hashColumnWidth = 100 * UiHelpers.Scale; + _pathColumnWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 300 * UiHelpers.Scale; + _refsColumnWidth = 30 * UiHelpers.Scale; + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs new file mode 100644 index 00000000..60f781c5 --- /dev/null +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -0,0 +1,751 @@ +using System; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Interop; +using Penumbra.Interop.Services; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.Classes; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; + +namespace Penumbra.UI.Tabs; + +public class SettingsTab : ITab +{ + public const int RootDirectoryMaxLength = 64; + + public ReadOnlySpan Label + => "Settings"u8; + + private readonly Configuration _config; + private readonly FontReloader _fontReloader; + private readonly TutorialService _tutorial; + private readonly Penumbra _penumbra; + private readonly FileDialogService _fileDialog; + private readonly Mod.Manager _modManager; + private readonly ModFileSystemSelector _selector; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly DalamudServices _dalamud; + + public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, + FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, + ResidentResourceManager residentResources, DalamudServices dalamud) + { + _config = config; + _fontReloader = fontReloader; + _tutorial = tutorial; + _penumbra = penumbra; + _fileDialog = fileDialog; + _modManager = modManager; + _selector = selector; + _characterUtility = characterUtility; + _residentResources = residentResources; + _dalamud = dalamud; + } + + public void DrawHeader() + { + _tutorial.OpenTutorial(BasicTutorialSteps.Fin); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq1); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq2); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq3); + } + + public void DrawContent() + { + using var child = ImRaii.Child("##SettingsTab", -Vector2.One, false); + if (!child) + return; + + DrawEnabledBox(); + Checkbox("Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, + v => Penumbra.Config.FixMainWindow = v); + + ImGui.NewLine(); + DrawRootFolder(); + DrawDirectoryButtons(); + ImGui.NewLine(); + + DrawGeneralSettings(); + DrawColorSettings(); + DrawAdvancedSettings(); + DrawSupportButtons(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Checkbox(string label, string tooltip, bool current, Action setter) + { + using var id = ImRaii.PushId(label); + var tmp = current; + if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + { + setter(tmp); + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(label, tooltip); + } + + #region Main Settings + + /// + /// 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 newName, string old, float width, bool saved, bool selected) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); + var w = new Vector2(width, 0); + var (text, valid) = CheckRootDirectoryPath(newName, old, selected); + + return (ImGui.Button(text, w) || saved) && valid; + } + + /// Check a potential new root directory for validity and return the button text and whether it is valid. + private static (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) + { + static bool IsSubPathOf(string basePath, string subPath) + { + if (basePath.Length == 0) + return false; + + var rel = Path.GetRelativePath(basePath, subPath); + return rel == "." || !rel.StartsWith('.') && !Path.IsPathRooted(rel); + } + + if (newName.Length > RootDirectoryMaxLength) + return ($"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false); + + if (Path.GetDirectoryName(newName) == null) + return ("Path is not allowed to be a drive root. Please add a directory.", false); + + var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + if (IsSubPathOf(desktop, newName)) + return ("Path is not allowed to be on your Desktop.", false); + + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (IsSubPathOf(programFiles, newName) || IsSubPathOf(programFilesX86, newName)) + return ("Path is not allowed to be in ProgramFiles.", false); + + var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; + if (IsSubPathOf(dalamud.FullName, newName)) + return ("Path is not allowed to be inside your Dalamud directories.", false); + + if (Functions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName)) + return ("Path is not allowed to be inside your Downloads folder.", false); + + var gameDir = DalamudServices.SGameData.GameData.DataPath.Parent!.Parent!.FullName; + if (IsSubPathOf(gameDir, newName)) + return ("Path is not allowed to be inside your game folder.", false); + + return selected + ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) + : ($"Click Here to Save (Current Directory: {old})", true); + } + + /// Changing the base mod directory. + private string? _newModDirectory; + + /// + /// Draw a directory picker button that toggles the directory picker. + /// Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. + /// + private void DrawDirectoryPickerButton() + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + return; + + _newModDirectory ??= _config.ModDirectory; + // Use the current input as start directory if it exists, + // otherwise the current mod directory, otherwise the current application directory. + var startDir = Directory.Exists(_newModDirectory) + ? _newModDirectory + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : "."; + + _fileDialog.OpenFolderPicker("Choose Mod Directory", (b, s) => _newModDirectory = b ? s : _newModDirectory, startDir, false); + } + + /// + /// Draw the text input for the mod directory, + /// as well as the directory picker button and the enter warning. + /// + private void DrawRootFolder() + { + if (_newModDirectory.IsNullOrEmpty()) + _newModDirectory = _config.ModDirectory; + + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + var save = ImGui.InputText("##rootDirectory", ref _newModDirectory, RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + var selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); + + group.Dispose(); + _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); + ImGui.SameLine(); + var pos = ImGui.GetCursorPosX(); + ImGui.NewLine(); + + if (_config.ModDirectory != _newModDirectory + && _newModDirectory.Length != 0 + && DrawPressEnterWarning(_newModDirectory, Penumbra.Config.ModDirectory, pos, save, selected)) + _modManager.DiscoverMods(_newModDirectory); + } + + /// Draw the Open Directory and Rediscovery buttons. + private void DrawDirectoryButtons() + { + UiHelpers.DrawOpenDirectoryButton(0, _modManager.BasePath, _modManager.Valid); + ImGui.SameLine(); + var tt = _modManager.Valid + ? "Force Penumbra to completely re-scan your root directory as if it was restarted." + : "The currently selected folder is not valid. Please select a different folder."; + if (ImGuiUtil.DrawDisabledButton("Rediscover Mods", Vector2.Zero, tt, !_modManager.Valid)) + _modManager.DiscoverMods(); + } + + /// Draw the Enable Mods Checkbox. + private void DrawEnabledBox() + { + var enabled = _config.EnableMods; + if (ImGui.Checkbox("Enable Mods", ref enabled)) + _penumbra.SetEnabled(enabled); + + _tutorial.OpenTutorial(BasicTutorialSteps.EnableMods); + } + + #endregion + + #region General Settings + + /// Draw all settings pertaining to the Mod Selector. + private void DrawGeneralSettings() + { + if (!ImGui.CollapsingHeader("General")) + { + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralSettings); + return; + } + + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralSettings); + + DrawHidingSettings(); + UiHelpers.DefaultLineSpace(); + + DrawMiscSettings(); + UiHelpers.DefaultLineSpace(); + + DrawIdentificationSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModSelectorSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModHandlingSettings(); + ImGui.NewLine(); + } + + private int _singleGroupRadioMax = int.MaxValue; + + /// Draw a selection for the maximum number of single select options displayed as a radio toggle. + private void DrawSingleSelectRadioMax() + { + if (_singleGroupRadioMax == int.MaxValue) + _singleGroupRadioMax = _config.SingleGroupRadioMax; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.DragInt("##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1)) + _singleGroupRadioMax = Math.Max(1, _singleGroupRadioMax); + + if (ImGui.IsItemDeactivated()) + { + if (_singleGroupRadioMax != _config.SingleGroupRadioMax) + { + _config.SingleGroupRadioMax = _singleGroupRadioMax; + _config.Save(); + } + + _singleGroupRadioMax = int.MaxValue; + } + + ImGuiUtil.LabeledHelpMarker("Upper Limit for Single-Selection Group Radio Buttons", + "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" + + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons."); + } + + private int _collapsibleGroupMin = int.MaxValue; + + /// Draw a selection for the minimum number of options after which a group is drawn as collapsible. + private void DrawCollapsibleGroupMin() + { + if (_collapsibleGroupMin == int.MaxValue) + _collapsibleGroupMin = _config.OptionGroupCollapsibleMin; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.DragInt("##CollapsibleGroupMin", ref _collapsibleGroupMin, 0.01f, 1)) + _collapsibleGroupMin = Math.Max(2, _collapsibleGroupMin); + + if (ImGui.IsItemDeactivated()) + { + if (_collapsibleGroupMin != _config.OptionGroupCollapsibleMin) + { + _config.OptionGroupCollapsibleMin = _collapsibleGroupMin; + _config.Save(); + } + + _collapsibleGroupMin = int.MaxValue; + } + + ImGuiUtil.LabeledHelpMarker("Collapsible Option Group Limit", + "Lower Limit for option groups displaying the Collapse/Expand button at the top."); + } + + + /// Draw the window hiding state checkboxes. + private void DrawHidingSettings() + { + Checkbox("Hide Config Window when UI is Hidden", + "Hide the penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, + v => + { + _config.HideUiWhenUiHidden = v; + _dalamud.UiBuilder.DisableUserUiHide = !v; + }); + Checkbox("Hide Config Window when in Cutscenes", + "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, + v => + { + _config.HideUiInCutscenes = v; + _dalamud.UiBuilder.DisableCutsceneUiHide = !v; + }); + Checkbox("Hide Config Window when in GPose", + "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, + v => + { + _config.HideUiInGPose = v; + _dalamud.UiBuilder.DisableGposeUiHide = !v; + }); + } + + /// Draw all settings that do not fit into other categories. + private void DrawMiscSettings() + { + Checkbox("Print Chat Command Success Messages to Chat", + "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", + _config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v); + Checkbox("Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", + _config.HideRedrawBar, v => _config.HideRedrawBar = v); + DrawSingleSelectRadioMax(); + DrawCollapsibleGroupMin(); + } + + /// Draw all settings pertaining to actor identification for collections. + private void DrawIdentificationSettings() + { + Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", + "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", + _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); + Checkbox($"Use {TutorialService.AssignedCollections} in Adventurer Cards", + "Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.", + _config.UseCharacterCollectionsInCards, v => _config.UseCharacterCollectionsInCards = v); + Checkbox($"Use {TutorialService.AssignedCollections} in Try-On Window", + "Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", + _config.UseCharacterCollectionInTryOn, v => _config.UseCharacterCollectionInTryOn = v); + Checkbox("Use No Mods in Inspect Windows", "Use the empty collection for characters you are inspecting, regardless of the character.\n" + + "Takes precedence before the next option.", _config.UseNoModsInInspect, v => _config.UseNoModsInInspect = v); + Checkbox($"Use {TutorialService.AssignedCollections} in Inspect Windows", + "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", + _config.UseCharacterCollectionInInspect, v => _config.UseCharacterCollectionInInspect = v); + Checkbox($"Use {TutorialService.AssignedCollections} based on Ownership", + "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", + _config.UseOwnerNameForCharacterCollection, v => _config.UseOwnerNameForCharacterCollection = v); + } + + /// Different supported sort modes as a combo. + private void DrawFolderSortType() + { + var sortMode = _config.SortMode; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + { + if (combo) + foreach (var val in Configuration.Constants.ValidSortModes) + { + if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + { + _config.SortMode = val; + _selector.SetFilterDirty(); + _config.Save(); + } + + ImGuiUtil.HoverTooltip(val.Description); + } + } + + ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab."); + } + + private float _absoluteSelectorSize = float.NaN; + + /// Draw a selector for the absolute size of the mod selector in pixels. + private void DrawAbsoluteSizeSelector() + { + if (float.IsNaN(_absoluteSelectorSize)) + _absoluteSelectorSize = _config.ModSelectorAbsoluteSize; + + if (ImGuiUtil.DragFloat("##absoluteSize", ref _absoluteSelectorSize, UiHelpers.InputTextWidth.X, 1, + Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f") + && _absoluteSelectorSize != _config.ModSelectorAbsoluteSize) + { + _config.ModSelectorAbsoluteSize = _absoluteSelectorSize; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Mod Selector Absolute Size", + "The minimal absolute size of the mod selector in the mod tab in pixels."); + } + + private int _relativeSelectorSize = int.MaxValue; + + /// Draw a selector for the relative size of the mod selector as a percentage and a toggle to enable relative sizing. + private void DrawRelativeSizeSelector() + { + var scaleModSelector = _config.ScaleModSelector; + if (ImGui.Checkbox("Scale Mod Selector With Window Size", ref scaleModSelector)) + { + _config.ScaleModSelector = scaleModSelector; + _config.Save(); + } + + ImGui.SameLine(); + if (_relativeSelectorSize == int.MaxValue) + _relativeSelectorSize = _config.ModSelectorScaledSize; + if (ImGuiUtil.DragInt("##relativeSize", ref _relativeSelectorSize, UiHelpers.InputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, + Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%") + && _relativeSelectorSize != _config.ModSelectorScaledSize) + { + _config.ModSelectorScaledSize = _relativeSelectorSize; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Mod Selector Relative Size", + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); + } + + /// Draw all settings pertaining to the mod selector. + private void DrawModSelectorSettings() + { + DrawFolderSortType(); + DrawAbsoluteSizeSelector(); + DrawRelativeSizeSelector(); + Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", + _config.OpenFoldersByDefault, v => + { + _config.OpenFoldersByDefault = v; + _selector.SetFilterDirty(); + }); + + Widget.DoubleModifierSelector("Mod Deletion Modifier", + "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", UiHelpers.InputTextWidth.X, + Penumbra.Config.DeleteModModifier, + v => + { + _config.DeleteModModifier = v; + _config.Save(); + }); + } + + /// Draw all settings pertaining to import and export of mods. + private void DrawModHandlingSettings() + { + Checkbox("Always Open Import at Default Directory", + "Open the import window at the location specified here every time, forgetting your previous path.", + _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); + DrawDefaultModImportPath(); + DrawDefaultModAuthor(); + DrawDefaultModImportFolder(); + DrawDefaultModExportPath(); + } + + + /// Draw input for the default import path for a mod. + private void DrawDefaultModImportPath() + { + var tmp = _config.DefaultModImportPath; + var spacing = new Vector2(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + if (ImGui.InputText("##defaultModImport", ref tmp, 256)) + _config.DefaultModImportPath = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##import", UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + { + var startDir = _config.DefaultModImportPath.Length > 0 && Directory.Exists(_config.DefaultModImportPath) + ? _config.DefaultModImportPath + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : null; + + _fileDialog.OpenFolderPicker("Choose Default Import Directory", (b, s) => + { + if (!b) + return; + + _config.DefaultModImportPath = s; + _config.Save(); + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Default Mod Import Directory", + "Set the directory that gets opened when using the file picker to import mods for the first time."); + } + + private string _tempExportDirectory = string.Empty; + + /// Draw input for the default export/backup path for mods. + private void DrawDefaultModExportPath() + { + var tmp = _config.ExportDirectory; + var spacing = new Vector2(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + if (ImGui.InputText("##defaultModExport", ref tmp, 256)) + _tempExportDirectory = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _modManager.UpdateExportDirectory(_tempExportDirectory, true); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + { + var startDir = _config.ExportDirectory.Length > 0 && Directory.Exists(_config.ExportDirectory) + ? _config.ExportDirectory + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : null; + _fileDialog.OpenFolderPicker("Choose Default Export Directory", (b, s) => + { + if (b) + Penumbra.ModManager.UpdateExportDirectory(s, true); + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Default Mod Export Directory", + "Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n" + + "Keep this empty to use the root directory."); + } + + /// Draw input for the default name to input as author into newly generated mods. + private void DrawDefaultModAuthor() + { + var tmp = _config.DefaultModAuthor; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputText("##defaultAuthor", ref tmp, 64)) + _config.DefaultModAuthor = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default Mod Author", "Set the default author stored for newly created mods."); + } + + /// Draw input for the default folder to sort put newly imported mods into. + private void DrawDefaultModImportFolder() + { + var tmp = _config.DefaultImportFolder; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputText("##defaultImportFolder", ref tmp, 64)) + _config.DefaultImportFolder = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default Mod Import Organizational Folder", + "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."); + } + + #endregion + + /// Draw the entire Color subsection. + private void DrawColorSettings() + { + if (!ImGui.CollapsingHeader("Colors")) + return; + + foreach (var color in Enum.GetValues()) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = _config.Colors.TryGetValue(color, out var current) ? current : defaultColor; + if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor)) + _config.Save(); + } + + ImGui.NewLine(); + } + + #region Advanced Settings + + /// Draw all advanced settings. + private void DrawAdvancedSettings() + { + var header = ImGui.CollapsingHeader("Advanced"); + _tutorial.OpenTutorial(BasicTutorialSteps.AdvancedSettings); + + if (!header) + return; + + Checkbox("Auto Deduplicate on Import", + "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", + _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); + Checkbox("Keep Default Metadata Changes on Import", + "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", + _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); + DrawWaitForPluginsReflection(); + DrawEnableHttpApiBox(); + DrawEnableDebugModeBox(); + DrawReloadResourceButton(); + DrawReloadFontsButton(); + ImGui.NewLine(); + } + + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. + private void DrawEnableHttpApiBox() + { + var http = _config.EnableHttpApi; + if (ImGui.Checkbox("##http", ref http)) + { + if (http) + _penumbra.HttpApi.CreateWebServer(); + else + _penumbra.HttpApi.ShutdownWebServer(); + + _config.EnableHttpApi = http; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Enable HTTP API", + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws."); + } + + /// Draw a checkbox to toggle Debug mode. + private void DrawEnableDebugModeBox() + { + var tmp = _config.DebugMode; + if (ImGui.Checkbox("##debugMode", ref tmp) && tmp != _config.DebugMode) + { + _config.DebugMode = tmp; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Enable Debug Mode", + "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load."); + } + + /// Draw a button that reloads resident resources. + private void DrawReloadResourceButton() + { + if (ImGuiUtil.DrawDisabledButton("Reload Resident Resources", Vector2.Zero, + "Reload some specific files that the game keeps in memory at all times.\nYou usually should not need to do this.", + !_characterUtility.Ready)) + _residentResources.Reload(); + } + + /// Draw a button that reloads fonts. + private void DrawReloadFontsButton() + { + if (ImGuiUtil.DrawDisabledButton("Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid)) + _fontReloader.Reload(); + } + + /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. + private void DrawWaitForPluginsReflection() + { + if (!_dalamud.GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool value)) + { + using var disabled = ImRaii.Disabled(); + Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { }); + } + else + { + Checkbox("Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", + value, + v => _dalamud.SetDalamudConfig(DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup")); + } + } + + #endregion + + /// Draw the support button group on the right-hand side of the window. + private void DrawSupportButtons() + { + var width = ImGui.CalcTextSize(UiHelpers.SupportInfoButtonText).X + ImGui.GetStyle().FramePadding.X * 2; + var xPos = ImGui.GetWindowWidth() - width; + // Respect the scroll bar width. + if (ImGui.GetScrollMaxY() > 0) + xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; + + ImGui.SetCursorPos(new Vector2(xPos, ImGui.GetFrameHeightWithSpacing())); + UiHelpers.DrawSupportButton(_penumbra); + + ImGui.SetCursorPos(new Vector2(xPos, 0)); + UiHelpers.DrawDiscordButton(width); + + ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); + UiHelpers.DrawGuideButton(width); + + ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); + if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) + { + Penumbra.Config.TutorialStep = 0; + Penumbra.Config.Save(); + } + + ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); + if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) + _penumbra.ForceChangelogOpen(); + } +} diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs new file mode 100644 index 00000000..b470bcb9 --- /dev/null +++ b/Penumbra/UI/TutorialService.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.CompilerServices; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +/// List of currently available tutorials. +public enum BasicTutorialSteps +{ + GeneralTooltips, + ModDirectory, + EnableMods, + AdvancedSettings, + GeneralSettings, + Collections, + EditingCollections, + CurrentCollection, + Inheritance, + ActiveCollections, + DefaultCollection, + InterfaceCollection, + SpecialCollections1, + SpecialCollections2, + Mods, + ModImport, + AdvancedHelp, + ModFilters, + CollectionSelectors, + Redrawing, + EnablingMods, + Priority, + ModOptions, + Fin, + Faq1, + Faq2, + Faq3, + Favorites, + Tags, +} + +/// Service for the in-game tutorial. +public class TutorialService +{ + public const string SelectedCollection = "Selected Collection"; + public const string DefaultCollection = "Base Collection"; + public const string InterfaceCollection = "Interface Collection"; + public const string ActiveCollections = "Active Collections"; + public const string AssignedCollections = "Assigned Collections"; + public const string GroupAssignment = "Group Assignment"; + public const string CharacterGroups = "Character Groups"; + public const string ConditionalGroup = "Group"; + public const string ConditionalIndividual = "Character"; + public const string IndividualAssignments = "Individual Assignments"; + + public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" + + " - 'self' or '': your own character\n" + + " - 'target' or '': your target\n" + + " - 'focus' or ': your focus target\n" + + " - 'mouseover' or '': the actor you are currently hovering over\n" + + " - any specific actor name to redraw all actors of that exactly matching name."; + + private readonly Configuration _config; + private readonly Tutorial _tutorial; + + public TutorialService(Configuration config) + { + _config = config; + _tutorial = new Tutorial() + { + BorderColor = Colors.TutorialBorder, + HighlightColor = Colors.TutorialMarker, + PopupLabel = "Settings Tutorial", + } + .Register("General Tooltips", "This symbol gives you further information about whatever setting it appears next to.\n\n" + + "Hover over them when you are unsure what something does or how to do something.") + .Register("Initial Setup, Step 1: Mod Directory", + "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n" + + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n" + + "The folder should be an empty folder no other applications write to.") + .Register("Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not.") + .Deprecated() + .Register("General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + + "If you do not know what some of these do yet, return to this later!") + .Register("Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + + "This is our next stop!\n\n" + + "Go here after setting up your root folder to continue the tutorial!") + .Register("Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" + + "In here, we can create new collections, delete collections, or make them inherit from each other.") + .Register($"Initial Setup, Step 5: {SelectedCollection}", + $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." + + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n") + .Register("Inheritance", + "This is a more advanced feature. Click the help button for more information, but we will ignore this for now.") + .Register($"Initial Setup, Step 6: {ActiveCollections}", + $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" + + "Any collection assigned here will apply to the game under certain conditions.\n\n" + + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" + + "Open this now to continue.") + .Register($"Initial Setup, Step 7: {DefaultCollection}", + $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" + + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.") + .Register("Interface Collection", + $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" + + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") + .Register(GroupAssignment + 's', + "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" + + $"{IndividualAssignments} always take precedence before groups.") + .Register(IndividualAssignments, + "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" + + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n") + .Register("Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" + + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking.") + .Register("Initial Setup, Step 9: Mod Import", + "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" + + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") // TODO + .Register("Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" + + "Import and select a mod now to continue.") + .Register("Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here.") + .Register("Collection Selectors", $"This row provides shortcuts to set your {SelectedCollection}.\n\n" + + $"The first button sets it to your {DefaultCollection} (if any).\n\n" + + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" + + "The third is a regular collection selector to let you choose among all your collections.") + .Register("Redrawing", + "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" + + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" + + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too.") + .Register("Initial Setup, Step 11: Enabling Mods", + "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" + + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance.") + .Register("Initial Setup, Step 12: Priority", + "If two enabled mods in one collection change the same files, there is a conflict.\n\n" + + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" + + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible.") + .Register("Mod Options", "Many mods have options themselves. You can also choose those here.\n\n" + + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately.") + .Register("Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n" + + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page.") + .Register("FAQ 1", "Penumbra can not easily change which items a mod applies to.") + .Register("FAQ 2", + "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices.") + .Register("FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing.") + .Register("Favorites", + "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections.") + .Register("Tags", + "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'.") + .EnsureSize(Enum.GetValues().Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OpenTutorial(BasicTutorialSteps step) + => _tutorial.Open((int)step, _config.TutorialStep, v => + { + _config.TutorialStep = v; + _config.Save(); + }); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SkipTutorial(BasicTutorialSteps step) + => _tutorial.Skip((int)step, _config.TutorialStep, v => + { + _config.TutorialStep = v; + _config.Save(); + }); + + /// Update the current tutorial step if tutorials have changed since last update. + public void UpdateTutorialStep() + { + var tutorial = _tutorial.CurrentEnabledId(_config.TutorialStep); + if (tutorial != _config.TutorialStep) + { + _config.TutorialStep = tutorial; + _config.Save(); + } + } +} diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs new file mode 100644 index 00000000..11d06428 --- /dev/null +++ b/Penumbra/UI/UiHelpers.cs @@ -0,0 +1,236 @@ +using System.Diagnostics; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public static class UiHelpers +{ + /// Draw text given by a ByteString. + public static unsafe void Text(ByteString s) + => ImGuiNative.igTextUnformatted(s.Path, s.Path + s.Length); + + /// Draw text given by a byte pointer and length. + public static unsafe void Text(byte* s, int length) + => ImGuiNative.igTextUnformatted(s, s + length); + + /// Draw the name of a resource file. + public static unsafe void Text(ResourceHandle* resource) + => Text(resource->FileName().Path, resource->FileNameLength); + + /// Draw a ByteString as a selectable. + public static unsafe bool Selectable(ByteString s, bool selected) + { + var tmp = (byte)(selected ? 1 : 0); + return ImGuiNative.igSelectable_Bool(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; + } + + /// + /// A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, + /// using an ByteString. + /// + public static unsafe void CopyOnClickSelectable(ByteString text) + { + if (ImGuiNative.igSelectable_Bool(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) + ImGuiNative.igSetClipboardText(text.Path); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to copy to clipboard."); + } + + /// Apply Changed Item Counters to the Name if necessary. + public static string ChangedItemName(string name, object? data) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + + /// + /// Draw a changed item, invoking the Api-Events for clicks and tooltips. + /// Also draw the item Id in grey if requested. + /// + public static void DrawChangedItem(PenumbraApi api, string name, object? data, bool drawId) + { + name = ChangedItemName(name, data); + var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + + if (ret != MouseButton.None) + api.InvokeClick(ret, data); + + if (api.HasTooltip && ImGui.IsItemHovered()) + { + // We can not be sure that any subscriber actually prints something in any case. + // Circumvent ugly blank tooltip with less-ugly useless tooltip. + using var tt = ImRaii.Tooltip(); + using var group = ImRaii.Group(); + api.InvokeTooltip(data); + group.Dispose(); + if (ImGui.GetItemRectSize() == Vector2.Zero) + ImGui.TextUnformatted("No actions available."); + } + + if (!drawId || !GetChangedItemObject(data, out var text)) + return; + + ImGui.SameLine(ImGui.GetContentRegionAvail().X); + ImGuiUtil.RightJustify(text, ColorId.ItemId.Value(Penumbra.Config)); + } + + /// Return more detailed object information in text, if it exists. + public static bool GetChangedItemObject(object? obj, out string text) + { + switch (obj) + { + case Item it: + var quad = (Quad)it.ModelMain; + text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + return true; + case ModelChara m: + text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; + return true; + default: + text = string.Empty; + return false; + } + } + + /// Draw a button to open the official discord server. + /// The desired width of the button. + public static void DrawDiscordButton(float width) + { + const string address = @"https://discord.gg/kVva7DHV4r"; + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.DiscordColor); + if (ImGui.Button("Join Discord for Support", new Vector2(width, 0))) + try + { + var process = new ProcessStartInfo(address) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + Penumbra.ChatService.NotificationMessage($"Unable to open Discord at {address}.", "Error", NotificationType.Error); + } + + ImGuiUtil.HoverTooltip($"Open {address}"); + } + + /// The longest support button text. + public const string SupportInfoButtonText = "Copy Support Info to Clipboard"; + + /// + /// Draw a button that copies the support info to clipboards. + /// + /// + public static void DrawSupportButton(Penumbra penumbra) + { + if (!ImGui.Button(SupportInfoButtonText)) + return; + + var text = penumbra.GatherSupportInformation(); + ImGui.SetClipboardText(text); + Penumbra.ChatService.NotificationMessage($"Copied Support Info to Clipboard.", "Success", NotificationType.Success); + } + + /// Draw a button to open a specific directory in a file explorer. + /// Specific ID for the given type of directory. + /// The directory to open. + /// Whether the button is available. + public static void DrawOpenDirectoryButton(int id, DirectoryInfo directory, bool condition) + { + using var _ = ImRaii.PushId(id); + if (ImGuiUtil.DrawDisabledButton("Open Directory", Vector2.Zero, "Open this directory in your configured file explorer.", + !condition || !Directory.Exists(directory.FullName))) + Process.Start(new ProcessStartInfo(directory.FullName) + { + UseShellExecute = true, + }); + } + + /// Draw the button that opens the ReniGuide. + public static void DrawGuideButton(float width) + { + const string address = @"https://reniguide.info/"; + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.ReniColorButton) + .Push(ImGuiCol.ButtonHovered, Colors.ReniColorHovered) + .Push(ImGuiCol.ButtonActive, Colors.ReniColorActive); + if (ImGui.Button("Beginner's Guides", new Vector2(width, 0))) + try + { + var process = new ProcessStartInfo(address) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + Penumbra.ChatService.NotificationMessage($"Could not open guide at {address} in external browser.", "Error", + NotificationType.Error); + } + + ImGuiUtil.HoverTooltip( + $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" + + "Not directly affiliated and potentially, but not usually out of date."); + } + + /// Draw default vertical space. + public static void DefaultLineSpace() + => ImGui.Dummy(DefaultSpace); + + /// Vertical spacing between groups. + public static Vector2 DefaultSpace; + + /// Width of most input fields. + public static Vector2 InputTextWidth; + + /// Frame Height for square icon buttons. + public static Vector2 IconButtonSize; + + /// Input Text Width with space for an additional button with spacing of 3 between them. + public static float InputTextMinusButton3; + + /// Input Text Width with space for an additional button with spacing of default item spacing between them. + public static float InputTextMinusButton; + + /// Multiples of the current Global Scale + public static float Scale; + + public static float ScaleX2; + public static float ScaleX3; + public static float ScaleX4; + public static float ScaleX5; + + public static void SetupCommonSizes() + { + if (ImGuiHelpers.GlobalScale != Scale) + { + Scale = ImGuiHelpers.GlobalScale; + DefaultSpace = new Vector2(0, 10 * Scale); + InputTextWidth = new Vector2(350f * Scale, 0); + ScaleX2 = Scale * 2; + ScaleX3 = Scale * 3; + ScaleX4 = Scale * 4; + ScaleX5 = Scale * 5; + } + + IconButtonSize = new Vector2(ImGui.GetFrameHeight()); + InputTextMinusButton3 = InputTextWidth.X - IconButtonSize.X - ScaleX3; + InputTextMinusButton = InputTextWidth.X - IconButtonSize.X - ImGui.GetStyle().ItemSpacing.X; + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 630033b9..08143a57 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -11,22 +11,28 @@ public class PenumbraWindowSystem : IDisposable { private readonly UiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; - public PenumbraWindowSystem(DalamudPluginInterface pi, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, - ModEditWindow editWindow) + public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, + LaunchButton _, + ModEditWindow editWindow, FileDialogService fileDialog) { _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; Changelog = changelog; Window = window; _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); - - _uiBuilder.OpenConfigUi += Window.Toggle; - _uiBuilder.Draw += _windowSystem.Draw; + _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.Draw += _windowSystem.Draw; + _uiBuilder.Draw += _fileDialog.Draw; + _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; + _uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes; + _uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden; } public void ForceChangelogOpen() @@ -36,5 +42,6 @@ public class PenumbraWindowSystem : IDisposable { _uiBuilder.OpenConfigUi -= Window.Toggle; _uiBuilder.Draw -= _windowSystem.Draw; + _uiBuilder.Draw -= _fileDialog.Draw; } } From b92a3161b546f6c279661c1aadfb9940b7ee83bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Mar 2023 17:30:09 +0100 Subject: [PATCH 0810/2451] Why is this so much work? --- Penumbra/Import/TexToolsImport.cs | 15 +- Penumbra/Mods/Editor/DuplicateManager.cs | 258 +++++ Penumbra/Mods/Editor/FileRegistry.cs | 57 + Penumbra/Mods/Editor/MdlMaterialEditor.cs | 97 ++ Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 288 ------ Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 58 -- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 290 ------ .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 183 ---- Penumbra/Mods/Editor/Mod.Editor.Meta.cs | 165 --- Penumbra/Mods/Editor/Mod.Editor.cs | 56 - Penumbra/Mods/Editor/Mod.Normalization.cs | 262 ----- Penumbra/Mods/Editor/ModBackup.cs | 4 +- Penumbra/Mods/Editor/ModEditor.cs | 128 +++ Penumbra/Mods/Editor/ModFileCollection.cs | 198 ++++ Penumbra/Mods/Editor/ModFileEditor.cs | 173 ++++ Penumbra/Mods/Editor/ModMetaEditor.cs | 154 +++ Penumbra/Mods/Editor/ModNormalizer.cs | 289 ++++++ Penumbra/Mods/Editor/ModSwapEditor.cs | 51 + Penumbra/Mods/Editor/ModelMaterialInfo.cs | 82 ++ Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 2 +- Penumbra/Mods/Mod.BasePath.cs | 4 +- Penumbra/Mods/Mod.Meta.cs | 2 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 4 +- Penumbra/PenumbraNew.cs | 18 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 268 +++++ .../ItemSwapTab.cs} | 407 ++++---- .../ModEditWindow.Files.cs | 78 +- .../ModEditWindow.Materials.ColorSet.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../ModEditWindow.Materials.Shpk.cs | 2 +- .../ModEditWindow.Materials.cs | 7 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 886 ++++++++++++++++ .../ModEditWindow.Models.cs | 3 +- .../ModEditWindow.ShaderPackages.cs | 4 +- .../ModEditWindow.ShpkTab.cs | 2 +- .../ModEditWindow.Textures.cs | 4 +- .../ModEditWindow.cs | 170 +-- .../UI/Classes/ModEditWindow.FileEditor.cs | 267 ----- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 975 ------------------ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 8 +- Penumbra/UI/ModsTab/ModFilter.cs | 2 +- Penumbra/UI/ModsTab/ModPanel.cs | 12 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +- Penumbra/UI/ModsTab/ModPanelHeader.cs | 2 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 6 +- Penumbra/UI/Tabs/ModsTab.cs | 4 +- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- Penumbra/UI/WindowSystem.cs | 1 + 53 files changed, 3054 insertions(+), 2936 deletions(-) create mode 100644 Penumbra/Mods/Editor/DuplicateManager.cs create mode 100644 Penumbra/Mods/Editor/FileRegistry.cs create mode 100644 Penumbra/Mods/Editor/MdlMaterialEditor.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.Edit.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.Files.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.Meta.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Editor.cs delete mode 100644 Penumbra/Mods/Editor/Mod.Normalization.cs create mode 100644 Penumbra/Mods/Editor/ModEditor.cs create mode 100644 Penumbra/Mods/Editor/ModFileCollection.cs create mode 100644 Penumbra/Mods/Editor/ModFileEditor.cs create mode 100644 Penumbra/Mods/Editor/ModMetaEditor.cs create mode 100644 Penumbra/Mods/Editor/ModNormalizer.cs create mode 100644 Penumbra/Mods/Editor/ModSwapEditor.cs create mode 100644 Penumbra/Mods/Editor/ModelMaterialInfo.cs create mode 100644 Penumbra/UI/AdvancedWindow/FileEditor.cs rename Penumbra/UI/{Classes/ItemSwapWindow.cs => AdvancedWindow/ItemSwapTab.cs} (67%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Files.cs (80%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Materials.ColorSet.cs (99%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Materials.MtrlTab.cs (99%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Materials.Shpk.cs (99%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Materials.cs (96%) create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Models.cs (98%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.ShaderPackages.cs (99%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.ShpkTab.cs (99%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.Textures.cs (98%) rename Penumbra/UI/{Classes => AdvancedWindow}/ModEditWindow.cs (76%) delete mode 100644 Penumbra/UI/Classes/ModEditWindow.FileEditor.cs delete mode 100644 Penumbra/UI/Classes/ModEditWindow.Meta.cs diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index cdcdcd1d..89672cf1 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -32,16 +32,21 @@ public partial class TexToolsImporter : IDisposable public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files, - Action< FileInfo, DirectoryInfo?, Exception? > handler ) - : this( baseDirectory, files.Count, files, handler ) + Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor) + : this( baseDirectory, files.Count, files, handler, config, editor) { } + private readonly Configuration _config; + private readonly ModEditor _editor; + public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, - Action< FileInfo, DirectoryInfo?, Exception? > handler ) + Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor) { _baseDirectory = baseDirectory; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _modPackFiles = modPackFiles; + _config = config; + _editor = editor; _modPackCount = count; ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); _token = _cancellation.Token; @@ -95,10 +100,10 @@ public partial class TexToolsImporter : IDisposable { var directory = VerifyVersionAndImport( file ); ExtractedMods.Add( ( file, directory, null ) ); - if( Penumbra.Config.AutoDeduplicateOnImport ) + if( _config.AutoDeduplicateOnImport ) { State = ImporterState.DeduplicatingFiles; - Mod.Editor.DeduplicateMod( directory ); + _editor.Duplicates.DeduplicateMod( directory ); } } catch( Exception e ) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs new file mode 100644 index 00000000..44197193 --- /dev/null +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class DuplicateManager +{ + private readonly Mod.Manager _modManager; + private readonly SHA256 _hasher = SHA256.Create(); + private readonly ModFileCollection _files; + private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); + + public DuplicateManager(ModFileCollection files, Mod.Manager modManager) + { + _files = files; + _modManager = modManager; + } + + public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates + => _duplicates; + + public long SavedSpace { get; private set; } = 0; + public bool Finished { get; private set; } = true; + + public void StartDuplicateCheck(IEnumerable files) + { + if (!Finished) + return; + + Finished = false; + var filesTmp = files.OrderByDescending(f => f.FileSize).ToArray(); + Task.Run(() => CheckDuplicates(filesTmp)); + } + + public void DeleteDuplicates(Mod mod, ISubMod option, bool useModManager) + { + if (!Finished || _duplicates.Count == 0) + return; + + foreach (var (set, _, _) in _duplicates) + { + if (set.Length < 2) + continue; + + var remaining = set[0]; + foreach (var duplicate in set.Skip(1)) + HandleDuplicate(mod, duplicate, remaining, useModManager); + } + + _duplicates.Clear(); + DeleteEmptyDirectories(mod.ModPath); + _files.UpdateAll(mod, option); + } + + public void Clear() + { + Finished = true; + SavedSpace = 0; + } + + private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) + { + void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + { + var changes = false; + var dict = subMod.Files.ToDictionary(kvp => kvp.Key, + kvp => ChangeDuplicatePath(mod, kvp.Value, duplicate, remaining, kvp.Key, ref changes)); + if (!changes) + return; + + if (useModManager) + { + _modManager.OptionSetFiles(mod, groupIdx, optionIdx, dict); + } + else + { + var sub = (Mod.SubMod)subMod; + sub.FileData = dict; + if (groupIdx == -1) + mod.SaveDefaultMod(); + else + IModGroup.Save(mod.Groups[groupIdx], mod.ModPath, groupIdx); + } + } + + ModEditor.ApplyToAllOptions(mod, HandleSubMod); + + try + { + File.Delete(duplicate.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}"); + } + } + + private static FullPath ChangeDuplicatePath(Mod mod, FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes) + { + if (!value.Equals(from)) + return value; + + changes = true; + Penumbra.Log.Debug($"[DeleteDuplicates] Changing {key} for {mod.Name}\n : {from}\n -> {to}"); + return to; + } + + private void CheckDuplicates(IReadOnlyList files) + { + _duplicates.Clear(); + SavedSpace = 0; + var list = new List(); + var lastSize = -1L; + foreach (var file in files) + { + // Skip any UI Files because deduplication causes weird crashes for those. + if (file.SubModUsage.Any(f => f.Item2.Path.StartsWith("ui/"u8))) + continue; + + if (Finished) + return; + + if (file.FileSize == lastSize) + { + list.Add(file.File); + continue; + } + + if (list.Count >= 2) + CheckMultiDuplicates(list, lastSize); + + lastSize = file.FileSize; + + list.Clear(); + list.Add(file.File); + } + + if (list.Count >= 2) + CheckMultiDuplicates(list, lastSize); + + _duplicates.Sort((a, b) => a.Size != b.Size ? b.Size.CompareTo(a.Size) : a.Paths[0].CompareTo(b.Paths[0])); + Finished = true; + } + + private void CheckMultiDuplicates(IReadOnlyList list, long size) + { + var hashes = list.Select(f => (f, ComputeHash(f))).ToList(); + while (hashes.Count > 0) + { + if (Finished) + return; + + var set = new HashSet { hashes[0].Item1 }; + var hash = hashes[0]; + for (var j = 1; j < hashes.Count; ++j) + { + if (Finished) + return; + + if (CompareHashes(hash.Item2, hashes[j].Item2) && CompareFilesDirectly(hashes[0].Item1, hashes[j].Item1)) + set.Add(hashes[j].Item1); + } + + hashes.RemoveAll(p => set.Contains(p.Item1)); + if (set.Count > 1) + { + _duplicates.Add((set.OrderBy(f => f.FullName.Length).ToArray(), size, hash.Item2)); + SavedSpace += (set.Count - 1) * size; + } + } + } + + private static unsafe bool CompareFilesDirectly(FullPath f1, FullPath f2) + { + if (!f1.Exists || !f2.Exists) + return false; + + using var s1 = File.OpenRead(f1.FullName); + using var s2 = File.OpenRead(f2.FullName); + var buffer1 = stackalloc byte[256]; + var buffer2 = stackalloc byte[256]; + var span1 = new Span(buffer1, 256); + var span2 = new Span(buffer2, 256); + + while (true) + { + var bytes1 = s1.Read(span1); + var bytes2 = s2.Read(span2); + if (bytes1 != bytes2) + return false; + + if (!span1[..bytes1].SequenceEqual(span2[..bytes2])) + return false; + + if (bytes1 < 256) + return true; + } + } + + public static bool CompareHashes(byte[] f1, byte[] f2) + => StructuralComparisons.StructuralEqualityComparer.Equals(f1, f2); + + public byte[] ComputeHash(FullPath f) + { + 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 static 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) + { + Penumbra.Log.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 void DeduplicateMod(DirectoryInfo modDirectory) + { + try + { + var mod = new Mod(modDirectory); + mod.Reload(true, out _); + + Finished = false; + _files.UpdateAll(mod, mod.Default); + CheckDuplicates(_files.Available.OrderByDescending(f => f.FileSize).ToArray()); + DeleteDuplicates(mod, mod.Default, false); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not deduplicate mod {modDirectory.Name}:\n{e}"); + } + } +} diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs new file mode 100644 index 00000000..2ce22ec1 --- /dev/null +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class FileRegistry : IEquatable +{ + public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = new(); + public FullPath File { get; private init; } + public Utf8RelPath RelPath { get; private init; } + public long FileSize { get; private init; } + public int CurrentUsage; + + public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) + { + var fullPath = new FullPath(file.FullName); + if (!fullPath.ToRelPath(modPath, out var relPath)) + { + registry = null; + return false; + } + + registry = new FileRegistry + { + File = fullPath, + RelPath = relPath, + FileSize = file.Length, + CurrentUsage = 0, + }; + return true; + } + + public bool Equals(FileRegistry? other) + { + if (other is null) + return false; + + return ReferenceEquals(this, other) || File.Equals(other.File); + } + + public override bool Equals(object? obj) + { + if (obj is null) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + return obj.GetType() == GetType() && Equals((FileRegistry)obj); + } + + public override int GetHashCode() + => File.GetHashCode(); +} diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs new file mode 100644 index 00000000..dc32869f --- /dev/null +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using OtterGui; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; + +namespace Penumbra.Mods; + +public partial class MdlMaterialEditor +{ + [GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] + private static partial Regex MaterialRegex(); + + private readonly ModFileCollection _files; + + private readonly List _modelFiles = new(); + + public IReadOnlyList ModelFiles + => _modelFiles; + + public MdlMaterialEditor(ModFileCollection files) + => _files = files; + + public void SaveAllModels() + { + foreach (var info in _modelFiles) + info.Save(); + } + + public void RestoreAllModels() + { + foreach (var info in _modelFiles) + info.Restore(); + } + + public void Clear() + { + _modelFiles.Clear(); + } + + /// + /// Go through the currently loaded files and replace all appropriate suffices. + /// Does nothing if toSuffix is invalid. + /// If raceCode is Unknown, apply to all raceCodes. + /// If fromSuffix is empty, apply to all suffices. + /// + public void ReplaceAllMaterials(string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown) + { + if (!ValidString(toSuffix)) + return; + + foreach (var info in _modelFiles) + { + for (var i = 0; i < info.Count; ++i) + { + var (_, def) = info[i]; + var match = MaterialRegex().Match(def); + if (match.Success + && (raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups["RaceCode"].Value) + && (fromSuffix.Length == 0 || fromSuffix == match.Groups["Suffix"].Value)) + info.SetMaterial($"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i); + } + } + } + + /// Non-ASCII encoding is not supported. + public static bool ValidString(string to) + => to.Length != 0 + && to.Length < 16 + && Encoding.UTF8.GetByteCount(to) == to.Length; + + /// Find all model files in the mod that contain skin materials. + public void ScanModels(Mod mod) + { + _modelFiles.Clear(); + foreach (var file in _files.Mdl) + { + try + { + var bytes = File.ReadAllBytes(file.File.FullName); + var mdlFile = new MdlFile(bytes); + var materials = mdlFile.Materials.WithIndex().Where(p => MaterialRegex().IsMatch((string)p.Item1)) + .Select(p => p.Item2).ToArray(); + if (materials.Length > 0) + _modelFiles.Add(new ModelMaterialInfo(file.File, mdlFile, materials)); + } + catch (Exception e) + { + Penumbra.Log.Error($"Unexpected error scanning {mod.Name}'s {file.File.FullName} for materials:\n{e}"); + } + } + } +} diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs deleted file mode 100644 index d60356ac..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - private readonly SHA256 _hasher = SHA256.Create(); - private readonly List< (FullPath[] Paths, long Size, byte[] Hash) > _duplicates = new(); - - public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates - => _duplicates; - - public long SavedSpace { get; private set; } = 0; - - public bool DuplicatesFinished { get; private set; } = true; - - public void DeleteDuplicates( bool useModManager = true ) - { - if( !DuplicatesFinished || _duplicates.Count == 0 ) - { - return; - } - - foreach( var (set, _, _) in _duplicates ) - { - if( set.Length < 2 ) - { - continue; - } - - var remaining = set[ 0 ]; - foreach( var duplicate in set.Skip( 1 ) ) - { - HandleDuplicate( duplicate, remaining, useModManager ); - } - } - - _duplicates.Clear(); - DeleteEmptyDirectories( _mod.ModPath ); - UpdateFiles(); - } - - private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager ) - { - void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) - { - var changes = false; - var dict = subMod.Files.ToDictionary( kvp => kvp.Key, - kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) ); - if( changes ) - { - 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 ); - } - } - } - } - - ApplyToAllOptions( _mod, HandleSubMod ); - - try - { - File.Delete( duplicate.FullName ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" ); - } - } - - - private FullPath ChangeDuplicatePath( FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes ) - { - if( !value.Equals( from ) ) - { - return value; - } - - changes = true; - Penumbra.Log.Debug( $"[DeleteDuplicates] Changing {key} for {_mod.Name}\n : {from}\n -> {to}" ); - return to; - } - - - public void StartDuplicateCheck() - { - if( DuplicatesFinished ) - { - DuplicatesFinished = false; - UpdateFiles(); - var files = _availableFiles.OrderByDescending( f => f.FileSize ).ToArray(); - Task.Run( () => CheckDuplicates( files ) ); - } - } - - private void CheckDuplicates( IReadOnlyList< FileRegistry > files ) - { - _duplicates.Clear(); - SavedSpace = 0; - var list = new List< FullPath >(); - var lastSize = -1L; - foreach( var file in files ) - { - // Skip any UI Files because deduplication causes weird crashes for those. - if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( "ui/"u8 ) ) ) - { - continue; - } - - if( DuplicatesFinished ) - { - return; - } - - if( file.FileSize == lastSize ) - { - list.Add( file.File ); - continue; - } - - if( list.Count >= 2 ) - { - CheckMultiDuplicates( list, lastSize ); - } - - lastSize = file.FileSize; - - list.Clear(); - list.Add( file.File ); - } - - if( list.Count >= 2 ) - { - CheckMultiDuplicates( list, lastSize ); - } - - _duplicates.Sort( ( a, b ) => a.Size != b.Size ? b.Size.CompareTo( a.Size ) : a.Paths[ 0 ].CompareTo( b.Paths[ 0 ] ) ); - DuplicatesFinished = true; - } - - private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size ) - { - var hashes = list.Select( f => ( f, ComputeHash( f ) ) ).ToList(); - while( hashes.Count > 0 ) - { - if( DuplicatesFinished ) - { - return; - } - - var set = new HashSet< FullPath > { hashes[ 0 ].Item1 }; - var hash = hashes[ 0 ]; - for( var j = 1; j < hashes.Count; ++j ) - { - if( DuplicatesFinished ) - { - return; - } - - if( CompareHashes( hash.Item2, hashes[ j ].Item2 ) && CompareFilesDirectly( hashes[ 0 ].Item1, hashes[ j ].Item1 ) ) - { - set.Add( hashes[ j ].Item1 ); - } - } - - hashes.RemoveAll( p => set.Contains( p.Item1 ) ); - if( set.Count > 1 ) - { - _duplicates.Add( ( set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2 ) ); - SavedSpace += ( set.Count - 1 ) * size; - } - } - } - - private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 ) - { - if( !f1.Exists || !f2.Exists ) - { - return false; - } - - using var s1 = File.OpenRead( f1.FullName ); - using var s2 = File.OpenRead( f2.FullName ); - var buffer1 = stackalloc byte[256]; - var buffer2 = stackalloc byte[256]; - var span1 = new Span< byte >( buffer1, 256 ); - var span2 = new Span< byte >( buffer2, 256 ); - - while( true ) - { - var bytes1 = s1.Read( span1 ); - var bytes2 = s2.Read( span2 ); - if( bytes1 != bytes2 ) - { - return false; - } - - if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) ) - { - return false; - } - - if( bytes1 < 256 ) - { - return true; - } - } - } - - public static bool CompareHashes( byte[] f1, byte[] f2 ) - => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); - - public byte[] ComputeHash( FullPath f ) - { - 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 static 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 ) - { - Penumbra.Log.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( true, out _ ); - var editor = new Editor( mod, mod.Default ); - editor.DuplicatesFinished = false; - editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() ); - editor.DeleteDuplicates( false ); - } - catch( Exception e ) - { - Penumbra.Log.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs deleted file mode 100644 index c2221ecc..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - private SubMod _subMod; - - public ISubMod CurrentOption - => _subMod; - - public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); - - public void SetSubMod( ISubMod? subMod ) - { - _subMod = subMod as SubMod ?? _mod._default; - UpdateFiles(); - RevertSwaps(); - RevertManipulations(); - } - - public int ApplyFiles() - { - var dict = new Dictionary< Utf8GamePath, FullPath >(); - var num = 0; - foreach( var file in _availableFiles ) - { - foreach( var path in file.SubModUsage.Where( p => p.Item1 == CurrentOption ) ) - { - num += dict.TryAdd( path.Item2, file.File ) ? 0 : 1; - } - } - - Penumbra.ModManager.OptionSetFiles( _mod, _subMod.GroupIdx, _subMod.OptionIdx, dict ); - UpdateFiles(); - - return num; - } - - public void RevertFiles() - => UpdateFiles(); - - public void ApplySwaps() - { - Penumbra.ModManager.OptionSetFileSwaps( _mod, _subMod.GroupIdx, _subMod.OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); - } - - public void RevertSwaps() - { - CurrentSwaps.SetTo( _subMod.FileSwaps ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs deleted file mode 100644 index ea149216..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ /dev/null @@ -1,290 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - public class FileRegistry : IEquatable< FileRegistry > - { - public readonly List< (ISubMod, Utf8GamePath) > SubModUsage = new(); - public FullPath File { get; private init; } - public Utf8RelPath RelPath { get; private init; } - public long FileSize { get; private init; } - public int CurrentUsage; - - public static bool FromFile( Mod mod, FileInfo file, [NotNullWhen( true )] out FileRegistry? registry ) - { - var fullPath = new FullPath( file.FullName ); - if( !fullPath.ToRelPath( mod.ModPath, out var relPath ) ) - { - registry = null; - return false; - } - - registry = new FileRegistry - { - File = fullPath, - RelPath = relPath, - FileSize = file.Length, - CurrentUsage = 0, - }; - return true; - } - - public bool Equals( FileRegistry? other ) - { - if( other is null ) - { - return false; - } - - return ReferenceEquals( this, other ) || File.Equals( other.File ); - } - - public override bool Equals( object? obj ) - { - if( obj is null ) - { - return false; - } - - if( ReferenceEquals( this, obj ) ) - { - return true; - } - - return obj.GetType() == GetType() && Equals( ( FileRegistry )obj ); - } - - public override int GetHashCode() - => File.GetHashCode(); - } - - // All files in subdirectories of the mod directory. - public IReadOnlyList< FileRegistry > AvailableFiles - => _availableFiles; - - public bool FileChanges { get; private set; } - private List< FileRegistry > _availableFiles = null!; - private List< FileRegistry > _mtrlFiles = null!; - private List< FileRegistry > _mdlFiles = null!; - private List< FileRegistry > _texFiles = null!; - private List< FileRegistry > _shpkFiles = null!; - private readonly HashSet< Utf8GamePath > _usedPaths = new(); - - // All paths that are used in - private readonly SortedSet< FullPath > _missingFiles = new(); - - public IReadOnlySet< FullPath > MissingFiles - => _missingFiles; - - public IReadOnlyList< FileRegistry > MtrlFiles - => _mtrlFiles; - - public IReadOnlyList< FileRegistry > MdlFiles - => _mdlFiles; - - public IReadOnlyList< FileRegistry > TexFiles - => _texFiles; - - public IReadOnlyList< FileRegistry > ShpkFiles - => _shpkFiles; - - // Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths() - { - void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx ) - { - var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key, mod == _subMod ) ) - .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); - if( newDict.Count != mod.Files.Count ) - { - Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict ); - } - } - - ApplyToAllOptions( _mod, HandleSubMod ); - _missingFiles.Clear(); - } - - private bool CheckAgainstMissing( FullPath file, Utf8GamePath key, bool removeUsed ) - { - if( !_missingFiles.Contains( file ) ) - { - return true; - } - - if( removeUsed ) - { - _usedPaths.Remove( key ); - } - - Penumbra.Log.Debug( $"[RemoveMissingPaths] Removing {key} -> {file} from {_mod.Name}." ); - return false; - } - - - // Fetch all files inside subdirectories of the main mod directory. - // Then check which options use them and how often. - private void UpdateFiles() - { - _availableFiles = _mod.ModPath.EnumerateDirectories() - .SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) - .Select( f => FileRegistry.FromFile( _mod, f, out var r ) ? r : null ) - .OfType< FileRegistry >() ) - .ToList(); - _usedPaths.Clear(); - _mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList(); - _mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList(); - _texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList(); - _shpkFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".shpk", StringComparison.OrdinalIgnoreCase ) ).ToList(); - FileChanges = false; - foreach( var subMod in _mod.AllSubMods ) - { - foreach( var (gamePath, file) in subMod.Files ) - { - if( !file.Exists ) - { - _missingFiles.Add( file ); - if( subMod == _subMod ) - { - _usedPaths.Add( gamePath ); - } - } - else - { - var registry = _availableFiles.Find( x => x.File.Equals( file ) ); - if( registry != null ) - { - if( subMod == _subMod ) - { - ++registry.CurrentUsage; - _usedPaths.Add( gamePath ); - } - - registry.SubModUsage.Add( ( subMod, gamePath ) ); - } - } - } - } - } - - // Return whether the given path is already used in the current option. - public bool CanAddGamePath( Utf8GamePath path ) - => !_usedPaths.Contains( path ); - - // Try to set a given path for a given file. - // Returns false if this is not possible. - // If path is empty, it will be deleted instead. - // 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 ) - { - return false; - } - - var registry = _availableFiles[ fileIdx ]; - if( pathIdx > registry.SubModUsage.Count ) - { - return false; - } - - if( ( pathIdx == -1 || pathIdx == registry.SubModUsage.Count ) && !path.IsEmpty ) - { - registry.SubModUsage.Add( ( CurrentOption, path ) ); - ++registry.CurrentUsage; - _usedPaths.Add( path ); - } - else - { - _usedPaths.Remove( registry.SubModUsage[ pathIdx ].Item2 ); - if( path.IsEmpty ) - { - registry.SubModUsage.RemoveAt( pathIdx ); - --registry.CurrentUsage; - } - else - { - registry.SubModUsage[ pathIdx ] = ( registry.SubModUsage[ pathIdx ].Item1, path ); - } - } - - FileChanges = true; - - return true; - } - - // Transform a set of files to the appropriate game paths with the given number of folders skipped, - // and add them to the given option. - public int AddPathsToSelected( IEnumerable< FileRegistry > files, int skipFolders = 0 ) - { - var failed = 0; - foreach( var file in files ) - { - var gamePath = file.RelPath.ToGamePath( skipFolders ); - if( gamePath.IsEmpty ) - { - ++failed; - continue; - } - - if( CanAddGamePath( gamePath ) ) - { - ++file.CurrentUsage; - file.SubModUsage.Add( ( CurrentOption, gamePath ) ); - _usedPaths.Add( gamePath ); - FileChanges = true; - } - else - { - ++failed; - } - } - - return failed; - } - - // Remove all paths in the current option from the given files. - public void RemovePathsFromSelected( IEnumerable< FileRegistry > files ) - { - foreach( var file in files ) - { - file.CurrentUsage = 0; - FileChanges |= file.SubModUsage.RemoveAll( p => p.Item1 == CurrentOption && _usedPaths.Remove( p.Item2 ) ) > 0; - } - } - - // Delete all given files from your filesystem - public void DeleteFiles( IEnumerable< FileRegistry > files ) - { - var deletions = 0; - foreach( var file in files ) - { - try - { - File.Delete( file.File.FullName ); - Penumbra.Log.Debug( $"[DeleteFiles] Deleted {file.File.FullName} from {_mod.Name}." ); - ++deletions; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" ); - } - } - - if( deletions > 0 ) - { - _mod.Reload( false, out _ ); - UpdateFiles(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs deleted file mode 100644 index 45c6707c..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled); - private readonly List< ModelMaterialInfo > _modelFiles = new(); - - public IReadOnlyList< ModelMaterialInfo > ModelFiles - => _modelFiles; - - // Non-ASCII encoding can not be used. - public static bool ValidString( string to ) - => to.Length != 0 - && to.Length < 16 - && Encoding.UTF8.GetByteCount( to ) == to.Length; - - public void SaveAllModels() - { - foreach( var info in _modelFiles ) - { - info.Save(); - } - } - - public void RestoreAllModels() - { - foreach( var info in _modelFiles ) - { - info.Restore(); - } - } - - // Go through the currently loaded files and replace all appropriate suffices. - // Does nothing if toSuffix is invalid. - // If raceCode is Unknown, apply to all raceCodes. - // If fromSuffix is empty, apply to all suffices. - public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown ) - { - if( !ValidString( toSuffix ) ) - { - return; - } - - foreach( var info in _modelFiles ) - { - for( var i = 0; i < info.Count; ++i ) - { - var (_, def) = info[ i ]; - var match = MaterialRegex.Match( def ); - if( match.Success - && ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value ) - && ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) ) - { - info.SetMaterial( $"/mt_c{match.Groups[ "RaceCode" ].Value}b0001_{toSuffix}.mtrl", i ); - } - } - } - } - - // Find all model files in the mod that contain skin materials. - public void ScanModels() - { - _modelFiles.Clear(); - foreach( var file in _mdlFiles.Where( f => f.File.Extension == ".mdl" ) ) - { - try - { - var bytes = File.ReadAllBytes( file.File.FullName ); - var mdlFile = new MdlFile( bytes ); - var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) ) - .Select( p => p.Item2 ).ToArray(); - if( materials.Length > 0 ) - { - _modelFiles.Add( new ModelMaterialInfo( file.File, mdlFile, materials ) ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" ); - } - } - } - - // A class that collects information about skin materials in a model file and handle changes on them. - public class ModelMaterialInfo - { - public readonly FullPath Path; - public readonly MdlFile File; - private readonly string[] _currentMaterials; - private readonly IReadOnlyList< int > _materialIndices; - public bool Changed { get; private set; } - - public IReadOnlyList< string > CurrentMaterials - => _currentMaterials; - - private IEnumerable< string > DefaultMaterials - => _materialIndices.Select( i => File.Materials[ i ] ); - - public (string Current, string Default) this[ int idx ] - => ( _currentMaterials[ idx ], File.Materials[ _materialIndices[ idx ] ] ); - - public int Count - => _materialIndices.Count; - - // Set the skin material to a new value and flag changes appropriately. - public void SetMaterial( string value, int materialIdx ) - { - var mat = File.Materials[ _materialIndices[ materialIdx ] ]; - _currentMaterials[ materialIdx ] = value; - if( mat != value ) - { - Changed = true; - } - else - { - Changed = !_currentMaterials.SequenceEqual( DefaultMaterials ); - } - } - - // Save a changed .mdl file. - public void Save() - { - if( !Changed ) - { - return; - } - - foreach( var (idx, i) in _materialIndices.WithIndex() ) - { - File.Materials[ idx ] = _currentMaterials[ i ]; - } - - try - { - System.IO.File.WriteAllBytes( Path.FullName, File.Write() ); - Changed = false; - } - catch( Exception e ) - { - Restore(); - Penumbra.Log.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" ); - } - } - - // Revert all current changes. - public void Restore() - { - if( !Changed ) - { - return; - } - - foreach( var (idx, i) in _materialIndices.WithIndex() ) - { - _currentMaterials[ i ] = File.Materials[ idx ]; - } - - Changed = false; - } - - public ModelMaterialInfo( FullPath path, MdlFile file, IReadOnlyList< int > indices ) - { - Path = path; - File = file; - _materialIndices = indices; - _currentMaterials = DefaultMaterials.ToArray(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs deleted file mode 100644 index 0eec38ea..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public partial class Editor - { - public struct Manipulations - { - private readonly HashSet< ImcManipulation > _imc = new(); - private readonly HashSet< EqpManipulation > _eqp = new(); - private readonly HashSet< EqdpManipulation > _eqdp = new(); - private readonly HashSet< GmpManipulation > _gmp = new(); - private readonly HashSet< EstManipulation > _est = new(); - private readonly HashSet< RspManipulation > _rsp = new(); - - public bool Changes { get; private set; } = false; - - public IReadOnlySet< ImcManipulation > Imc - => _imc; - - public IReadOnlySet< EqpManipulation > Eqp - => _eqp; - - public IReadOnlySet< EqdpManipulation > Eqdp - => _eqdp; - - public IReadOnlySet< GmpManipulation > Gmp - => _gmp; - - public IReadOnlySet< EstManipulation > Est - => _est; - - public IReadOnlySet< RspManipulation > Rsp - => _rsp; - - public Manipulations() - { } - - public bool CanAdd( MetaManipulation m ) - { - return m.ManipulationType switch - { - MetaManipulation.Type.Imc => !_imc.Contains( m.Imc ), - MetaManipulation.Type.Eqdp => !_eqdp.Contains( m.Eqdp ), - MetaManipulation.Type.Eqp => !_eqp.Contains( m.Eqp ), - MetaManipulation.Type.Est => !_est.Contains( m.Est ), - MetaManipulation.Type.Gmp => !_gmp.Contains( m.Gmp ), - MetaManipulation.Type.Rsp => !_rsp.Contains( m.Rsp ), - _ => false, - }; - } - - public bool Add( MetaManipulation m ) - { - var added = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Add( m.Imc ), - MetaManipulation.Type.Eqdp => _eqdp.Add( m.Eqdp ), - MetaManipulation.Type.Eqp => _eqp.Add( m.Eqp ), - MetaManipulation.Type.Est => _est.Add( m.Est ), - MetaManipulation.Type.Gmp => _gmp.Add( m.Gmp ), - MetaManipulation.Type.Rsp => _rsp.Add( m.Rsp ), - _ => false, - }; - Changes |= added; - return added; - } - - public bool Delete( MetaManipulation m ) - { - var deleted = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove( m.Imc ), - MetaManipulation.Type.Eqdp => _eqdp.Remove( m.Eqdp ), - MetaManipulation.Type.Eqp => _eqp.Remove( m.Eqp ), - MetaManipulation.Type.Est => _est.Remove( m.Est ), - MetaManipulation.Type.Gmp => _gmp.Remove( m.Gmp ), - MetaManipulation.Type.Rsp => _rsp.Remove( m.Rsp ), - _ => false, - }; - Changes |= deleted; - return deleted; - } - - public bool Change( MetaManipulation m ) - => Delete( m ) && Add( m ); - - public bool Set( MetaManipulation m ) - => Delete( m ) | Add( m ); - - public void Clear() - { - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _gmp.Clear(); - _est.Clear(); - _rsp.Clear(); - Changes = true; - } - - public void Split( IEnumerable< MetaManipulation > manips ) - { - Clear(); - foreach( var manip in manips ) - { - switch( manip.ManipulationType ) - { - case MetaManipulation.Type.Imc: - _imc.Add( manip.Imc ); - break; - case MetaManipulation.Type.Eqdp: - _eqdp.Add( manip.Eqdp ); - break; - case MetaManipulation.Type.Eqp: - _eqp.Add( manip.Eqp ); - break; - case MetaManipulation.Type.Est: - _est.Add( manip.Est ); - break; - case MetaManipulation.Type.Gmp: - _gmp.Add( manip.Gmp ); - break; - case MetaManipulation.Type.Rsp: - _rsp.Add( manip.Rsp ); - break; - } - } - - Changes = false; - } - - public IEnumerable< MetaManipulation > Recombine() - => _imc.Select( m => ( MetaManipulation )m ) - .Concat( _eqdp.Select( m => ( MetaManipulation )m ) ) - .Concat( _eqp.Select( m => ( MetaManipulation )m ) ) - .Concat( _est.Select( m => ( MetaManipulation )m ) ) - .Concat( _gmp.Select( m => ( MetaManipulation )m ) ) - .Concat( _rsp.Select( m => ( MetaManipulation )m ) ); - - public void Apply( Mod mod, int groupIdx, int optionIdx ) - { - if( Changes ) - { - Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine().ToHashSet() ); - Changes = false; - } - } - } - - public Manipulations Meta = new(); - - public void RevertManipulations() - => Meta.Split( _subMod.Manipulations ); - - public void ApplyManipulations() - { - Meta.Apply( _mod, _subMod.GroupIdx, _subMod.OptionIdx ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs deleted file mode 100644 index 3120bcf3..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.IO; -using OtterGui; - -namespace Penumbra.Mods; - -public partial class Mod : IMod -{ - public partial class Editor : IDisposable - { - private readonly Mod _mod; - - public Editor( Mod mod, ISubMod? option ) - { - _mod = mod; - _subMod = null!; - SetSubMod( option ); - UpdateFiles(); - ScanModels(); - } - - public void Cancel() - { - DuplicatesFinished = true; - } - - public void Dispose() - => Cancel(); - - // Does not delete the base directory itself even if it is completely empty at the end. - private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) - { - foreach( var subDir in baseDir.GetDirectories() ) - { - ClearEmptySubDirectories( subDir ); - if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) - { - subDir.Delete(); - } - } - } - - // Apply a option action to all available option in a mod, including the default option. - private static void ApplyToAllOptions( Mod mod, Action< ISubMod, int, int > action ) - { - action( mod.Default, -1, 0 ); - foreach( var (group, groupIdx) in mod.Groups.WithIndex() ) - { - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) - { - action( group[ optionIdx ], groupIdx, optionIdx ); - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs deleted file mode 100644 index 0b487a8f..00000000 --- a/Penumbra/Mods/Editor/Mod.Normalization.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; -using OtterGui; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public void Normalize( Manager manager ) - => ModNormalizer.Normalize( manager, this ); - - private struct ModNormalizer - { - private readonly Mod _mod; - private readonly string _normalizationDirName; - private readonly string _oldDirName; - private Dictionary< Utf8GamePath, FullPath >[][]? _redirections = null; - - private ModNormalizer( Mod mod ) - { - _mod = mod; - _normalizationDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalization" ); - _oldDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalizationOld" ); - } - - public static void Normalize( Manager manager, Mod mod ) - { - var normalizer = new ModNormalizer( mod ); - try - { - Penumbra.Log.Debug( $"[Normalization] Starting Normalization of {mod.ModPath.Name}..." ); - if( !normalizer.CheckDirectories() ) - { - return; - } - - Penumbra.Log.Debug( "[Normalization] Copying files to temporary directory structure..." ); - if( !normalizer.CopyNewFiles() ) - { - return; - } - - Penumbra.Log.Debug( "[Normalization] Moving old files out of the way..." ); - if( !normalizer.MoveOldFiles() ) - { - return; - } - - Penumbra.Log.Debug( "[Normalization] Moving new directory structure in place..." ); - if( !normalizer.MoveNewFiles() ) - { - return; - } - - Penumbra.Log.Debug( "[Normalization] Applying new redirections..." ); - normalizer.ApplyRedirections( manager ); - } - catch( Exception e ) - { - Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); - } - finally - { - Penumbra.Log.Debug( "[Normalization] Cleaning up remaining directories..." ); - normalizer.Cleanup(); - } - } - - private bool CheckDirectories() - { - if( Directory.Exists( _normalizationDirName ) ) - { - Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" - + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", - NotificationType.Error ); - return false; - } - - if( Directory.Exists( _oldDirName ) ) - { - Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" - + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", - NotificationType.Error ); - return false; - } - - return true; - } - - private void Cleanup() - { - if( Directory.Exists( _normalizationDirName ) ) - { - try - { - Directory.Delete( _normalizationDirName, true ); - } - catch - { - // ignored - } - } - - if( Directory.Exists( _oldDirName ) ) - { - try - { - foreach( var dir in new DirectoryInfo( _oldDirName ).EnumerateDirectories() ) - { - dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); - } - - Directory.Delete( _oldDirName, true ); - } - catch - { - // ignored - } - } - } - - private bool CopyNewFiles() - { - // We copy all files to a temporary folder to ensure that we can revert the operation on failure. - try - { - var directory = Directory.CreateDirectory( _normalizationDirName ); - _redirections = new Dictionary< Utf8GamePath, FullPath >[_mod.Groups.Count + 1][]; - _redirections[ 0 ] = new Dictionary< Utf8GamePath, FullPath >[] { new(_mod.Default.Files.Count) }; - - // Normalize the default option. - var newDict = new Dictionary< Utf8GamePath, FullPath >( _mod.Default.Files.Count ); - _redirections[ 0 ][ 0 ] = newDict; - foreach( var (gamePath, fullPath) in _mod._default.FileData ) - { - var relPath = new Utf8RelPath( gamePath ).ToString(); - var newFullPath = Path.Combine( directory.FullName, relPath ); - var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, relPath ) ); - Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); - File.Copy( fullPath.FullName, newFullPath, true ); - newDict.Add( gamePath, redirectPath ); - } - - // Normalize all other options. - foreach( var (group, groupIdx) in _mod.Groups.WithIndex() ) - { - _redirections[ groupIdx + 1 ] = new Dictionary< Utf8GamePath, FullPath >[group.Count]; - var groupDir = Creator.CreateModFolder( directory, group.Name ); - - foreach( var option in group.OfType< SubMod >() ) - { - var optionDir = Creator.CreateModFolder( groupDir, option.Name ); - newDict = new Dictionary< Utf8GamePath, FullPath >( option.FileData.Count ); - _redirections[ groupIdx + 1 ][ option.OptionIdx ] = newDict; - foreach( var (gamePath, fullPath) in option.FileData ) - { - var relPath = new Utf8RelPath( gamePath ).ToString(); - var newFullPath = Path.Combine( optionDir.FullName, relPath ); - var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath ) ); - Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! ); - File.Copy( fullPath.FullName, newFullPath, true ); - newDict.Add( gamePath, redirectPath ); - } - } - } - - return true; - } - catch( Exception e ) - { - Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); - _redirections = null; - } - - return false; - } - - private bool MoveOldFiles() - { - try - { - // Clean old directories and files. - var oldDirectory = Directory.CreateDirectory( _oldDirName ); - foreach( var dir in _mod.ModPath.EnumerateDirectories() ) - { - if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) - || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) - { - continue; - } - - dir.MoveTo( Path.Combine( oldDirectory.FullName, dir.Name ) ); - } - - return true; - } - catch( Exception e ) - { - Penumbra.ChatService.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); - } - - return false; - } - - private bool MoveNewFiles() - { - try - { - var mainDir = new DirectoryInfo( _normalizationDirName ); - foreach( var dir in mainDir.EnumerateDirectories() ) - { - dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) ); - } - - mainDir.Delete(); - Directory.Delete( _oldDirName, true ); - return true; - } - catch( Exception e ) - { - Penumbra.ChatService.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); - foreach( var dir in _mod.ModPath.EnumerateDirectories() ) - { - if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) - || dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) ) - { - continue; - } - - try - { - dir.Delete( true ); - } - catch - { - // ignored - } - } - } - - return false; - } - - private void ApplyRedirections( Manager manager ) - { - if( _redirections == null ) - { - return; - } - - foreach( var option in _mod.AllSubMods.OfType< SubMod >() ) - { - manager.OptionSetFiles( _mod, option.GroupIdx, option.OptionIdx, _redirections[ option.GroupIdx + 1 ][ option.OptionIdx ] ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index d554ee0e..fe533489 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -14,10 +14,10 @@ public class ModBackup public readonly string Name; public readonly bool Exists; - public ModBackup( Mod mod ) + public ModBackup( Mod.Manager modManager, Mod mod ) { _mod = mod; - Name = Path.Combine( Penumbra.ModManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp"; + Name = Path.Combine( modManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp"; Exists = File.Exists( Name ); } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs new file mode 100644 index 00000000..c08b7bff --- /dev/null +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using OtterGui; + +namespace Penumbra.Mods; + +public class ModEditor : IDisposable +{ + public readonly ModNormalizer ModNormalizer; + public readonly ModMetaEditor MetaEditor; + public readonly ModFileEditor FileEditor; + public readonly DuplicateManager Duplicates; + public readonly ModFileCollection Files; + public readonly ModSwapEditor SwapEditor; + public readonly MdlMaterialEditor MdlMaterialEditor; + + public Mod? Mod { get; private set; } + public int GroupIdx { get; private set; } + public int OptionIdx { get; private set; } + + public IModGroup? Group { get; private set; } + public ISubMod? Option { get; private set; } + + public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files, + ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor) + { + ModNormalizer = modNormalizer; + MetaEditor = metaEditor; + Files = files; + FileEditor = fileEditor; + Duplicates = duplicates; + SwapEditor = swapEditor; + MdlMaterialEditor = mdlMaterialEditor; + } + + public void LoadMod(Mod mod) + => LoadMod(mod, -1, 0); + + public void LoadMod(Mod mod, int groupIdx, int optionIdx) + { + Mod = mod; + LoadOption(groupIdx, optionIdx, true); + Files.UpdateAll(mod, Option!); + SwapEditor.Revert(Option!); + MetaEditor.Load(Option!); + Duplicates.Clear(); + } + + public void LoadOption(int groupIdx, int optionIdx) + { + LoadOption(groupIdx, optionIdx, true); + SwapEditor.Revert(Option!); + Files.UpdatePaths(Mod!, Option!); + MetaEditor.Load(Option!); + FileEditor.Clear(); + Duplicates.Clear(); + } + + /// Load the correct option by indices for the currently loaded mod if possible, unload if not. + private void LoadOption(int groupIdx, int optionIdx, bool message) + { + if (Mod != null && Mod.Groups.Count > groupIdx) + { + if (groupIdx == -1 && optionIdx == 0) + { + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; + } + + if (groupIdx >= 0) + { + Group = Mod.Groups[groupIdx]; + if (optionIdx >= 0 && optionIdx < Group.Count) + { + Option = Group[optionIdx]; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; + } + } + } + + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + OptionIdx = 0; + if (message) + global::Penumbra.Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); + } + + public void Clear() + { + Duplicates.Clear(); + FileEditor.Clear(); + Files.Clear(); + MetaEditor.Clear(); + Mod = null; + LoadOption(0, 0, false); + } + + public void Dispose() + => Clear(); + + /// Apply a option action to all available option in a mod, including the default option. + public static void ApplyToAllOptions(Mod mod, Action action) + { + action(mod.Default, -1, 0); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + { + for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) + action(group[optionIdx], groupIdx, optionIdx); + } + } + + // Does not delete the base directory itself even if it is completely empty at the end. + public static void ClearEmptySubDirectories(DirectoryInfo baseDir) + { + foreach (var subDir in baseDir.GetDirectories()) + { + ClearEmptySubDirectories(subDir); + if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0) + subDir.Delete(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs new file mode 100644 index 00000000..72eb742b --- /dev/null +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.Win32; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class ModFileCollection : IDisposable +{ + private readonly List _available = new(); + private readonly List _mtrl = new(); + private readonly List _mdl = new(); + private readonly List _tex = new(); + private readonly List _shpk = new(); + + private readonly SortedSet _missing = new(); + private readonly HashSet _usedPaths = new(); + + public IReadOnlySet Missing + => Ready ? _missing : new HashSet(); + + public IReadOnlySet UsedPaths + => Ready ? _usedPaths : new HashSet(); + + public IReadOnlyList Available + => Ready ? _available : Array.Empty(); + + public IReadOnlyList Mtrl + => Ready ? _mtrl : Array.Empty(); + + public IReadOnlyList Mdl + => Ready ? _mdl : Array.Empty(); + + public IReadOnlyList Tex + => Ready ? _tex : Array.Empty(); + + public IReadOnlyList Shpk + => Ready ? _shpk : Array.Empty(); + + public bool Ready { get; private set; } = true; + + public ModFileCollection() + { } + + public void UpdateAll(Mod mod, ISubMod option) + { + UpdateFiles(mod, new CancellationToken()); + UpdatePaths(mod, option, false, new CancellationToken()); + } + + public void UpdatePaths(Mod mod, ISubMod option) + => UpdatePaths(mod, option, true, new CancellationToken()); + + public void Clear() + { + ClearFiles(); + ClearPaths(false, new CancellationToken()); + } + + public void Dispose() + => Clear(); + + public void ClearMissingFiles() + => _missing.Clear(); + + public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + { + _usedPaths.Remove(gamePath); + if (file != null) + { + --file.CurrentUsage; + file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath)); + } + } + + public void RemoveUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); + + public void AddUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + { + _usedPaths.Add(gamePath); + if (file == null) + return; + + ++file.CurrentUsage; + file.SubModUsage.Add((option, gamePath)); + } + + public void AddUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); + + public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) + { + var oldPath = file.SubModUsage[pathIdx]; + _usedPaths.Remove(oldPath.Item2); + if (!gamePath.IsEmpty) + { + _usedPaths.Add(gamePath); + } + else + { + --file.CurrentUsage; + file.SubModUsage.RemoveAt(pathIdx); + } + } + + private void UpdateFiles(Mod mod, CancellationToken tok) + { + tok.ThrowIfCancellationRequested(); + ClearFiles(); + + foreach (var file in mod.ModPath.EnumerateDirectories().SelectMany(d => d.EnumerateFiles("*.*", SearchOption.AllDirectories))) + { + tok.ThrowIfCancellationRequested(); + if (!FileRegistry.FromFile(mod.ModPath, file, out var registry)) + continue; + + _available.Add(registry); + switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant()) + { + case ".mtrl": + _mtrl.Add(registry); + break; + case ".mdl": + _mdl.Add(registry); + break; + case ".tex": + _tex.Add(registry); + break; + case ".shpk": + _shpk.Add(registry); + break; + } + } + } + + private void ClearFiles() + { + _available.Clear(); + _mtrl.Clear(); + _mdl.Clear(); + _tex.Clear(); + _shpk.Clear(); + } + + private void ClearPaths(bool clearRegistries, CancellationToken tok) + { + if (clearRegistries) + foreach (var reg in _available) + { + tok.ThrowIfCancellationRequested(); + reg.CurrentUsage = 0; + reg.SubModUsage.Clear(); + } + + _missing.Clear(); + _usedPaths.Clear(); + } + + private void UpdatePaths(Mod mod, ISubMod option, bool clearRegistries, CancellationToken tok) + { + tok.ThrowIfCancellationRequested(); + ClearPaths(clearRegistries, tok); + + tok.ThrowIfCancellationRequested(); + + foreach (var subMod in mod.AllSubMods) + { + foreach (var (gamePath, file) in subMod.Files) + { + tok.ThrowIfCancellationRequested(); + if (!file.Exists) + { + _missing.Add(file); + if (subMod == option) + _usedPaths.Add(gamePath); + } + else + { + var registry = _available.Find(x => x.File.Equals(file)); + if (registry == null) + continue; + + if (subMod == option) + { + ++registry.CurrentUsage; + _usedPaths.Add(gamePath); + } + + registry.SubModUsage.Add((subMod, gamePath)); + } + } + } + } +} diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs new file mode 100644 index 00000000..031c7485 --- /dev/null +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class ModFileEditor +{ + private readonly ModFileCollection _files; + private readonly Mod.Manager _modManager; + + public bool Changes { get; private set; } + + public ModFileEditor(ModFileCollection files, Mod.Manager modManager) + { + _files = files; + _modManager = modManager; + } + + public void Clear() + { + Changes = false; + } + + public int Apply(Mod mod, Mod.SubMod option) + { + var dict = new Dictionary(); + var num = 0; + foreach (var file in _files.Available) + { + foreach (var path in file.SubModUsage.Where(p => p.Item1 == option)) + num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; + } + + Penumbra.ModManager.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + _files.UpdatePaths(mod, option); + + return num; + } + + public void RevertFiles(Mod mod, ISubMod option) + { + _files.UpdatePaths(mod, option); + Changes = false; + } + + /// Remove all path redirections where the pointed-to file does not exist. + public void RemoveMissingPaths(Mod mod, ISubMod option) + { + void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + { + var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + if (newDict.Count != subMod.Files.Count) + _modManager.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + } + + ModEditor.ApplyToAllOptions(mod, HandleSubMod); + _files.ClearMissingFiles(); + } + + /// Return whether the given path is already used in the current option. + public bool CanAddGamePath(Utf8GamePath path) + => !_files.UsedPaths.Contains(path); + + /// + /// Try to set a given path for a given file. + /// Returns false if this is not possible. + /// If path is empty, it will be deleted instead. + /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. + /// + public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + { + if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > _files.Available.Count) + return false; + + var registry = _files.Available[fileIdx]; + if (pathIdx > registry.SubModUsage.Count) + return false; + + if ((pathIdx == -1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty) + _files.AddUsedPath(option, registry, path); + else + _files.ChangeUsedPath(registry, pathIdx, path); + + Changes = true; + + return true; + } + + /// + /// Transform a set of files to the appropriate game paths with the given number of folders skipped, + /// and add them to the given option. + /// + public int AddPathsToSelected(ISubMod option, IEnumerable files, int skipFolders = 0) + { + var failed = 0; + foreach (var file in files) + { + var gamePath = file.RelPath.ToGamePath(skipFolders); + if (gamePath.IsEmpty) + { + ++failed; + continue; + } + + if (CanAddGamePath(gamePath)) + { + _files.AddUsedPath(option, file, gamePath); + Changes = true; + } + else + { + ++failed; + } + } + + return failed; + } + + /// Remove all paths in the current option from the given files. + public void RemovePathsFromSelected(ISubMod option, IEnumerable files) + { + foreach (var file in files) + { + foreach (var (_, path) in file.SubModUsage.Where(p => p.Item1 == option)) + { + _files.RemoveUsedPath(option, file, path); + Changes = true; + } + } + } + + /// Delete all given files from your filesystem + public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files) + { + var deletions = 0; + foreach (var file in files) + { + try + { + File.Delete(file.File.FullName); + Penumbra.Log.Debug($"[DeleteFiles] Deleted {file.File.FullName} from {mod.Name}."); + ++deletions; + } + catch (Exception e) + { + Penumbra.Log.Error($"[DeleteFiles] Could not delete {file.File.FullName} from {mod.Name}:\n{e}"); + } + } + + if (deletions <= 0) + return; + + mod.Reload(false, out _); + _files.UpdateAll(mod, option); + } + + + private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + { + if (!_files.Missing.Contains(file)) + return true; + + if (removeUsed) + _files.RemoveUsedPath(option, file, key); + + Penumbra.Log.Debug($"[RemoveMissingPaths] Removing {key} -> {file} from {mod.Name}."); + return false; + } +} diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs new file mode 100644 index 00000000..a211398b --- /dev/null +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Linq; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public class ModMetaEditor +{ + private readonly Mod.Manager _modManager; + + private readonly HashSet _imc = new(); + private readonly HashSet _eqp = new(); + private readonly HashSet _eqdp = new(); + private readonly HashSet _gmp = new(); + private readonly HashSet _est = new(); + private readonly HashSet _rsp = new(); + + public ModMetaEditor(Mod.Manager modManager) + => _modManager = modManager; + + public bool Changes { get; private set; } = false; + + public IReadOnlySet Imc + => _imc; + + public IReadOnlySet Eqp + => _eqp; + + public IReadOnlySet Eqdp + => _eqdp; + + public IReadOnlySet Gmp + => _gmp; + + public IReadOnlySet Est + => _est; + + public IReadOnlySet Rsp + => _rsp; + + public bool CanAdd(MetaManipulation m) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), + MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), + MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), + MetaManipulation.Type.Est => !_est.Contains(m.Est), + MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), + MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), + _ => false, + }; + } + + public bool Add(MetaManipulation m) + { + var added = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Add(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), + MetaManipulation.Type.Est => _est.Add(m.Est), + MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), + _ => false, + }; + Changes |= added; + return added; + } + + public bool Delete(MetaManipulation m) + { + var deleted = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Remove(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), + MetaManipulation.Type.Est => _est.Remove(m.Est), + MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), + _ => false, + }; + Changes |= deleted; + return deleted; + } + + public bool Change(MetaManipulation m) + => Delete(m) && Add(m); + + public bool Set(MetaManipulation m) + => Delete(m) | Add(m); + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _gmp.Clear(); + _est.Clear(); + _rsp.Clear(); + Changes = true; + } + + public void Load(ISubMod mod) + => Split(mod.Manipulations); + + public void Apply(Mod mod, int groupIdx, int optionIdx) + { + if (!Changes) + return; + + _modManager.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + Changes = false; + } + + private void Split(IEnumerable manips) + { + Clear(); + foreach (var manip in manips) + { + switch (manip.ManipulationType) + { + case MetaManipulation.Type.Imc: + _imc.Add(manip.Imc); + break; + case MetaManipulation.Type.Eqdp: + _eqdp.Add(manip.Eqdp); + break; + case MetaManipulation.Type.Eqp: + _eqp.Add(manip.Eqp); + break; + case MetaManipulation.Type.Est: + _est.Add(manip.Est); + break; + case MetaManipulation.Type.Gmp: + _gmp.Add(manip.Gmp); + break; + case MetaManipulation.Type.Rsp: + _rsp.Add(manip.Rsp); + break; + } + } + + Changes = false; + } + + public IEnumerable Recombine() + => _imc.Select(m => (MetaManipulation)m) + .Concat(_eqdp.Select(m => (MetaManipulation)m)) + .Concat(_eqp.Select(m => (MetaManipulation)m)) + .Concat(_est.Select(m => (MetaManipulation)m)) + .Concat(_gmp.Select(m => (MetaManipulation)m)) + .Concat(_rsp.Select(m => (MetaManipulation)m)); +} diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs new file mode 100644 index 00000000..9fc02d77 --- /dev/null +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class ModNormalizer +{ + private readonly Mod.Manager _modManager; + private readonly List>> _redirections = new(); + + public Mod Mod { get; private set; } = null!; + private string _normalizationDirName = null!; + private string _oldDirName = null!; + + public int Step { get; private set; } + public int TotalSteps { get; private set; } + + public bool Running + => Step < TotalSteps; + + public ModNormalizer(Mod.Manager modManager) + => _modManager = modManager; + + public void Normalize(Mod mod) + { + if (Step < TotalSteps) + return; + + Mod = mod; + _normalizationDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalization"); + _oldDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalizationOld"); + Step = 0; + TotalSteps = mod.TotalFileCount + 5; + + Task.Run(NormalizeSync); + } + + private void NormalizeSync() + { + try + { + Penumbra.Log.Debug($"[Normalization] Starting Normalization of {Mod.ModPath.Name}..."); + if (!CheckDirectories()) + { + return; + } + + Penumbra.Log.Debug("[Normalization] Copying files to temporary directory structure..."); + if (!CopyNewFiles()) + { + return; + } + + Penumbra.Log.Debug("[Normalization] Moving old files out of the way..."); + if (!MoveOldFiles()) + { + return; + } + + Penumbra.Log.Debug("[Normalization] Moving new directory structure in place..."); + if (!MoveNewFiles()) + { + return; + } + + Penumbra.Log.Debug("[Normalization] Applying new redirections..."); + ApplyRedirections(); + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + } + finally + { + Penumbra.Log.Debug("[Normalization] Cleaning up remaining directories..."); + Cleanup(); + } + } + + private bool CheckDirectories() + { + if (Directory.Exists(_normalizationDirName)) + { + Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n" + + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", + NotificationType.Error); + return false; + } + + if (Directory.Exists(_oldDirName)) + { + Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n" + + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", + NotificationType.Error); + return false; + } + + ++Step; + return true; + } + + private void Cleanup() + { + if (Directory.Exists(_normalizationDirName)) + { + try + { + Directory.Delete(_normalizationDirName, true); + } + catch + { + // ignored + } + } + + if (Directory.Exists(_oldDirName)) + { + try + { + foreach (var dir in new DirectoryInfo(_oldDirName).EnumerateDirectories()) + { + dir.MoveTo(Path.Combine(Mod.ModPath.FullName, dir.Name)); + } + + Directory.Delete(_oldDirName, true); + } + catch + { + // ignored + } + } + + Step = TotalSteps; + } + + private bool CopyNewFiles() + { + // We copy all files to a temporary folder to ensure that we can revert the operation on failure. + try + { + var directory = Directory.CreateDirectory(_normalizationDirName); + for (var i = _redirections.Count; i < Mod.Groups.Count + 1; ++i) + _redirections.Add(new List>()); + + if (_redirections[0].Count == 0) + _redirections[0].Add(new Dictionary(Mod.Default.Files.Count)); + else + { + _redirections[0][0].Clear(); + _redirections[0][0].EnsureCapacity(Mod.Default.Files.Count); + } + + // Normalize the default option. + var newDict = _redirections[0][0]; + foreach (var (gamePath, fullPath) in Mod.Default.Files) + { + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFullPath = Path.Combine(directory.FullName, relPath); + var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, relPath)); + Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); + File.Copy(fullPath.FullName, newFullPath, true); + newDict.Add(gamePath, redirectPath); + ++Step; + } + + // Normalize all other options. + foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) + { + _redirections[groupIdx + 1].EnsureCapacity(group.Count); + for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) + _redirections[groupIdx + 1].Add(new Dictionary()); + + var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); + foreach (var option in group.OfType()) + { + var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name); + + newDict = _redirections[groupIdx + 1][option.OptionIdx]; + newDict.Clear(); + newDict.EnsureCapacity(option.FileData.Count); + foreach (var (gamePath, fullPath) in option.FileData) + { + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFullPath = Path.Combine(optionDir.FullName, relPath); + var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); + Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); + File.Copy(fullPath.FullName, newFullPath, true); + newDict.Add(gamePath, redirectPath); + ++Step; + } + } + } + + return true; + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + } + + return false; + } + + private bool MoveOldFiles() + { + try + { + // Clean old directories and files. + var oldDirectory = Directory.CreateDirectory(_oldDirName); + foreach (var dir in Mod.ModPath.EnumerateDirectories()) + { + if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase) + || dir.FullName.Equals(_normalizationDirName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + dir.MoveTo(Path.Combine(oldDirectory.FullName, dir.Name)); + } + + ++Step; + return true; + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", + NotificationType.Error); + } + + return false; + } + + private bool MoveNewFiles() + { + try + { + var mainDir = new DirectoryInfo(_normalizationDirName); + foreach (var dir in mainDir.EnumerateDirectories()) + { + dir.MoveTo(Path.Combine(Mod.ModPath.FullName, dir.Name)); + } + + mainDir.Delete(); + Directory.Delete(_oldDirName, true); + ++Step; + return true; + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", + NotificationType.Error); + foreach (var dir in Mod.ModPath.EnumerateDirectories()) + { + if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase) + || dir.FullName.Equals(_normalizationDirName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + dir.Delete(true); + } + catch + { + // ignored + } + } + } + + return false; + } + + private void ApplyRedirections() + { + foreach (var option in Mod.AllSubMods.OfType()) + { + _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); + } + + ++Step; + } +} diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs new file mode 100644 index 00000000..0237d08f --- /dev/null +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Penumbra.Mods; +using Penumbra.String.Classes; +using Penumbra.Util; + +public class ModSwapEditor +{ + private readonly Mod.Manager _modManager; + private readonly Dictionary _swaps = new(); + + public IReadOnlyDictionary Swaps + => _swaps; + + public ModSwapEditor(Mod.Manager modManager) + => _modManager = modManager; + + public void Revert(ISubMod option) + { + _swaps.SetTo(option.FileSwaps); + Changes = false; + } + + public void Apply(Mod mod, int groupIdx, int optionIdx) + { + if (Changes) + { + _modManager.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + Changes = false; + } + } + + public bool Changes { get; private set; } + + public void Remove(Utf8GamePath path) + => Changes |= _swaps.Remove(path); + + public void Add(Utf8GamePath path, FullPath file) + => Changes |= _swaps.TryAdd(path, file); + + public void Change(Utf8GamePath path, Utf8GamePath newPath) + { + if (_swaps.Remove(path, out var file)) + Add(newPath, file); + } + + public void Change(Utf8GamePath path, FullPath file) + { + _swaps[path] = file; + Changes = true; + } +} diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs new file mode 100644 index 00000000..dc01ae7d --- /dev/null +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OtterGui; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +/// A class that collects information about skin materials in a model file and handle changes on them. +public class ModelMaterialInfo +{ + public readonly FullPath Path; + public readonly MdlFile File; + private readonly string[] _currentMaterials; + private readonly IReadOnlyList _materialIndices; + public bool Changed { get; private set; } + + public IReadOnlyList CurrentMaterials + => _currentMaterials; + + private IEnumerable DefaultMaterials + => _materialIndices.Select(i => File.Materials[i]); + + public (string Current, string Default) this[int idx] + => (_currentMaterials[idx], File.Materials[_materialIndices[idx]]); + + public int Count + => _materialIndices.Count; + + // Set the skin material to a new value and flag changes appropriately. + public void SetMaterial(string value, int materialIdx) + { + var mat = File.Materials[_materialIndices[materialIdx]]; + _currentMaterials[materialIdx] = value; + if (mat != value) + Changed = true; + else + Changed = !_currentMaterials.SequenceEqual(DefaultMaterials); + } + + // Save a changed .mdl file. + public void Save() + { + if (!Changed) + return; + + foreach (var (idx, i) in _materialIndices.WithIndex()) + File.Materials[idx] = _currentMaterials[i]; + + try + { + System.IO.File.WriteAllBytes(Path.FullName, File.Write()); + Changed = false; + } + catch (Exception e) + { + Restore(); + Penumbra.Log.Error($"Could not write manipulated .mdl file {Path.FullName}:\n{e}"); + } + } + + // Revert all current changes. + public void Restore() + { + if (!Changed) + return; + + foreach (var (idx, i) in _materialIndices.WithIndex()) + _currentMaterials[i] = File.Materials[idx]; + + Changed = false; + } + + public ModelMaterialInfo(FullPath path, MdlFile file, IReadOnlyList indices) + { + Path = path; + File = file; + _materialIndices = indices; + _currentMaterials = DefaultMaterials.ToArray(); + } +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 0c9c69cc..4b93bd24 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -59,7 +59,7 @@ public partial class Mod } MoveDataFile( oldDirectory, dir ); - new ModBackup( mod ).Move( null, dir.Name ); + new ModBackup( this, mod ).Move( null, dir.Name ); dir.Refresh(); mod.ModPath = dir; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 83927a1e..bd7fb0fd 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -158,7 +158,7 @@ public sealed partial class Mod { foreach( var mod in _mods ) { - new ModBackup( mod ).Move( dir.FullName ); + new ModBackup( this, mod ).Move( dir.FullName ); } } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 3f5db087..8423d023 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -25,7 +25,7 @@ public partial class Mod public int Priority => 0; - private Mod( DirectoryInfo modPath ) + internal Mod( DirectoryInfo modPath ) { ModPath = modPath; _default = new SubMod( this ); @@ -51,7 +51,7 @@ public partial class Mod return mod; } - private bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) + internal bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index a03377ec..5ba44286 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -26,7 +26,7 @@ public enum ModDataChangeType : ushort Note = 0x0800, } -public sealed partial class Mod +public sealed partial class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() { diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 981b336b..c089cfda 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -18,7 +18,7 @@ public partial class Mod // The default mod contains setting-independent sets of file replacements, file swaps and meta changes. // Every mod has an default mod, though it may be empty. - private void SaveDefaultMod() + public void SaveDefaultMod() { var defaultFile = DefaultFile; @@ -100,7 +100,7 @@ public partial class Mod // It can be loaded and reloaded from Json. // Nothing is checked for existence or validity when loading. // Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. - private sealed class SubMod : ISubMod + public sealed class SubMod : ISubMod { public string Name { get; set; } = "Default"; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 9e196424..3d52c6be 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -16,10 +16,11 @@ using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; -using Penumbra.UI.ModTab; +using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; using Penumbra.Util; -using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; +using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; namespace Penumbra; @@ -121,7 +122,18 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); + + // Add Mod Editor + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add API services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs new file mode 100644 index 00000000..47d53832 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Data; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public class FileEditor where T : class, IWritable +{ + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly DataManager _gameData; + + public FileEditor(DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, + Func> getFiles, Func drawEdit, Func getInitialPath, + Func? parseFile) + { + _gameData = gameData; + _config = config; + _fileDialog = fileDialog; + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + _getInitialPath = getInitialPath; + _parseFile = parseFile ?? DefaultParseFile; + } + + public void Draw() + { + _list = _getFiles(); + using var tab = ImRaii.TabItem(_tabName); + if (!tab) + return; + + ImGui.NewLine(); + DrawFileSelectCombo(); + SaveButton(); + ImGui.SameLine(); + ResetButton(); + ImGui.SameLine(); + DefaultInput(); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + DrawFilePanel(); + } + + private readonly string _tabName; + private readonly string _fileType; + private readonly Func> _getFiles; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; + + private FileRegistry? _currentPath; + private T? _currentFile; + private Exception? _currentException; + private bool _changed; + + private string _defaultPath = string.Empty; + private bool _inInput; + private T? _defaultFile; + private Exception? _defaultException; + + private IReadOnlyList _list = null!; + + private void DefaultInput() + { + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); + ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); + _inInput = ImGui.IsItemActive(); + if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) + { + _fileDialog.Reset(); + try + { + var file = _gameData.GetFile(_defaultPath); + if (file != null) + { + _defaultException = null; + _defaultFile = _parseFile(file.Data); + } + else + { + _defaultFile = null; + _defaultException = new Exception("File does not exist."); + } + } + catch (Exception e) + { + _defaultFile = null; + _defaultException = e; + } + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", + _defaultFile == null, true)) + _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, + (success, name) => + { + if (!success) + return; + + try + { + File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); + } + }, _getInitialPath(), false); + + _fileDialog.Draw(); + } + + public void Reset() + { + _currentException = null; + _currentPath = null; + _currentFile = null; + _changed = false; + } + + private void DrawFileSelectCombo() + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..."); + if (!combo) + return; + + foreach (var file in _list) + { + if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath))) + UpdateCurrentFile(file); + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted("All Game Paths"); + ImGui.Separator(); + using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); + foreach (var (option, gamePath) in file.SubModUsage) + { + ImGui.TableNextColumn(); + UiHelpers.Text(gamePath.Path); + ImGui.TableNextColumn(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + ImGui.TextUnformatted(option.FullName); + } + } + + if (file.SubModUsage.Count > 0) + { + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); + } + } + } + + private static T? DefaultParseFile(byte[] bytes) + => Activator.CreateInstance(typeof(T), bytes) as T; + + private void UpdateCurrentFile(FileRegistry path) + { + if (ReferenceEquals(_currentPath, path)) + return; + + _changed = false; + _currentPath = path; + _currentException = null; + try + { + var bytes = File.ReadAllBytes(_currentPath.File.FullName); + _currentFile = _parseFile(bytes); + } + catch (Exception e) + { + _currentFile = null; + _currentException = e; + } + } + + private void SaveButton() + { + if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) + { + File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + _changed = false; + } + } + + private void ResetButton() + { + if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed)) + { + var tmp = _currentPath; + _currentPath = null; + UpdateCurrentFile(tmp!); + } + } + + private void DrawFilePanel() + { + using var child = ImRaii.Child("##filePanel", -Vector2.One, true); + if (!child) + return; + + if (_currentPath != null) + { + if (_currentFile == null) + { + ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); + if (_currentException != null) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped(_currentException.ToString()); + } + } + else + { + using var id = ImRaii.PushId(0); + _changed |= _drawEdit(_currentFile, false); + } + } + + if (!_inInput && _defaultPath.Length > 0) + { + if (_currentPath != null) + { + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextUnformatted($"Preview of {_defaultPath}:"); + ImGui.Separator(); + } + + if (_defaultFile == null) + { + ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); + if (_defaultException != null) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped(_defaultException.ToString()); + } + } + else + { + using var id = ImRaii.PushId(1); + _drawEdit(_defaultFile, true); + } + } + } +} diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs similarity index 67% rename from Penumbra/UI/Classes/ItemSwapWindow.cs rename to Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index ce3b2dd3..f6def7d4 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using ImGuiNET; @@ -18,12 +17,90 @@ using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; using Penumbra.Services; -using Penumbra.Util; +using Penumbra.UI.Classes; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; -public class ItemSwapWindow : IDisposable +public class ItemSwapTab : IDisposable, ITab { + private readonly CommunicatorService _communicator; + private readonly ItemService _itemService; + private readonly ModCollection.Manager _collectionManager; + private readonly Mod.Manager _modManager; + private readonly Configuration _config; + + public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager, + Mod.Manager modManager, Configuration config) + { + _communicator = communicator; + _itemService = itemService; + _collectionManager = collectionManager; + _modManager = modManager; + _config = config; + + _selectors = new Dictionary + { + // @formatter:off + [SwapType.Hat] = (new ItemSelector(_itemService, FullEquipType.Head), new ItemSelector(_itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(_itemService, FullEquipType.Body), new ItemSelector(_itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(_itemService, FullEquipType.Hands), new ItemSelector(_itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(_itemService, FullEquipType.Legs), new ItemSelector(_itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(_itemService, FullEquipType.Feet), new ItemSelector(_itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(_itemService, FullEquipType.Ears), new ItemSelector(_itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(_itemService, FullEquipType.Neck), new ItemSelector(_itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(_itemService, FullEquipType.Wrists), new ItemSelector(_itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(_itemService, FullEquipType.Finger), new ItemSelector(_itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + // @formatter:on + }; + + _communicator.CollectionChange.Event += OnCollectionChange; + _collectionManager.Current.ModSettingChanged += OnSettingChange; + } + + /// Update the currently selected mod or its settings. + public void UpdateMod(Mod mod, ModSettings? settings) + { + if (mod == _mod && settings == _modSettings) + return; + + var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; + if (_newModName.Length == 0 || oldDefaultName == _newModName) + _newModName = $"{mod.Name.Text} (Swapped)"; + + _mod = mod; + _modSettings = settings; + _swapData.LoadMod(_mod, _modSettings); + UpdateOption(); + _dirty = true; + } + + public ReadOnlySpan Label + => "Item Swap (WIP)"u8; + + public void DrawContent() + { + ImGui.NewLine(); + DrawHeaderLine(300 * UiHelpers.Scale); + ImGui.NewLine(); + + DrawSwapBar(); + + using var table = ImRaii.ListBox("##swaps", -Vector2.One); + if (_loadException != null) + ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}"); + else if (_swapData.Loaded) + foreach (var swap in _swapData.Swaps) + DrawSwap(swap); + else + ImGui.TextUnformatted(NonExistentText()); + } + + public void Dispose() + { + _communicator.CollectionChange.Event -= OnCollectionChange; + _collectionManager.Current.ModSettingChanged -= OnSettingChange; + } + private enum SwapType { Hat, @@ -45,8 +122,8 @@ public class ItemSwapWindow : IDisposable private class ItemSelector : FilterComboCache<(string, Item)> { - public ItemSelector(FullEquipType type) - : base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) + public ItemSelector(ItemService data, FullEquipType type) + : base(() => data.AwaitedService[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) { } protected override string ToString((string, Item) obj) @@ -63,45 +140,10 @@ public class ItemSwapWindow : IDisposable => type.ToName(); } - private readonly CommunicatorService _communicator; + private readonly Dictionary _selectors; - public ItemSwapWindow(CommunicatorService communicator) - { - _communicator = communicator; - _communicator.CollectionChange.Event += OnCollectionChange; - Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; - } - - public void Dispose() - { - _communicator.CollectionChange.Event -= OnCollectionChange; - Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; - } - - private readonly Dictionary _selectors = new() - { - [SwapType.Hat] = - (new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"), - [SwapType.Top] = - (new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"), - [SwapType.Gloves] = - (new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"), - [SwapType.Pants] = - (new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"), - [SwapType.Shoes] = - (new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"), - [SwapType.Earrings] = - (new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"), - [SwapType.Necklace] = - (new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"), - [SwapType.Bracelet] = - (new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"), - [SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring", - "and put it on this one"), - }; - - private ItemSelector? _weaponSource = null; - private ItemSelector? _weaponTarget = null; + private ItemSelector? _weaponSource; + private ItemSelector? _weaponTarget; private readonly WeaponSelector _slotSelector = new(); private readonly ItemSwapContainer _swapData = new(); @@ -112,40 +154,24 @@ public class ItemSwapWindow : IDisposable private SwapType _lastTab = SwapType.Hair; private Gender _currentGender = Gender.Male; private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId = 0; - private int _sourceId = 0; - private Exception? _loadException = null; - private EquipSlot _slotFrom = EquipSlot.Head; - private EquipSlot _slotTo = EquipSlot.Ears; + private int _targetId; + private int _sourceId; + private Exception? _loadException; + private EquipSlot _slotFrom = EquipSlot.Head; + private EquipSlot _slotTo = EquipSlot.Ears; - private string _newModName = string.Empty; - private string _newGroupName = "Swaps"; - private string _newOptionName = string.Empty; - private IModGroup? _selectedGroup = null; - private bool _subModValid = false; - private bool _useFileSwaps = true; - private bool _useCurrentCollection = false; - private bool _useLeftRing = true; - private bool _useRightRing = true; + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private IModGroup? _selectedGroup; + private bool _subModValid; + private bool _useFileSwaps = true; + private bool _useCurrentCollection; + private bool _useLeftRing = true; + private bool _useRightRing = true; private Item[]? _affectedItems; - public void UpdateMod(Mod mod, ModSettings? settings) - { - if (mod == _mod && settings == _modSettings) - return; - - var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; - if (_newModName.Length == 0 || oldDefaultName == _newModName) - _newModName = $"{mod.Name.Text} (Swapped)"; - - _mod = mod; - _modSettings = settings; - _swapData.LoadMod(_mod, _modSettings); - UpdateOption(); - _dirty = true; - } - private void UpdateState() { if (!_dirty) @@ -167,42 +193,39 @@ public class ItemSwapWindow : IDisposable case SwapType.Necklace: case SwapType.Bracelet: case SwapType.Ring: - var values = _selectors[ _lastTab ]; - if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) - { - _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing ); - } + var values = _selectors[_lastTab]; + if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, + _useCurrentCollection ? _collectionManager.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: - var (_, _, selectorFrom) = GetAccessorySelector( _slotFrom, true ); - var (_, _, selectorTo) = GetAccessorySelector( _slotTo, false ); - if( selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null ) - { - _affectedItems = _swapData.LoadTypeSwap( _slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null); - } + var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); + var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); + if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) + _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, + selectorFrom.CurrentSelection.Item2, + _useCurrentCollection ? _collectionManager.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? Penumbra.CollectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Current : null); break; case SwapType.Weapon: break; } @@ -243,13 +266,13 @@ public class ItemSwapWindow : IDisposable private void CreateMod() { - var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); - Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); + var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateDefaultFiles(newDir); - Penumbra.ModManager.AddMod(newDir); - if (!_swapData.WriteMod(Penumbra.ModManager.Last(), + _modManager.AddMod(newDir); + if (!_swapData.WriteMod(_modManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) - Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1); + _modManager.DeleteMod(_modManager.Count - 1); } private void CreateOption() @@ -273,12 +296,12 @@ public class ItemSwapWindow : IDisposable { if (_selectedGroup == null) { - Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); + _modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); _selectedGroup = _mod.Groups.Last(); groupCreated = true; } - Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + _modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); optionCreated = true; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; @@ -294,11 +317,11 @@ public class ItemSwapWindow : IDisposable try { if (optionCreated && _selectedGroup != null) - Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + _modManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); if (groupCreated) { - Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _selectedGroup = null; } @@ -365,17 +388,17 @@ public class ItemSwapWindow : IDisposable private void DrawSwapBar() { - using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None ); + using var bar = ImRaii.TabBar("##swapBar", ImGuiTabBarFlags.None); - DrawEquipmentSwap( SwapType.Hat ); - DrawEquipmentSwap( SwapType.Top ); - DrawEquipmentSwap( SwapType.Gloves ); - DrawEquipmentSwap( SwapType.Pants ); - DrawEquipmentSwap( SwapType.Shoes ); - DrawEquipmentSwap( SwapType.Earrings ); - DrawEquipmentSwap( SwapType.Necklace ); - DrawEquipmentSwap( SwapType.Bracelet ); - DrawEquipmentSwap( SwapType.Ring ); + DrawEquipmentSwap(SwapType.Hat); + DrawEquipmentSwap(SwapType.Top); + DrawEquipmentSwap(SwapType.Gloves); + DrawEquipmentSwap(SwapType.Pants); + DrawEquipmentSwap(SwapType.Shoes); + DrawEquipmentSwap(SwapType.Earrings); + DrawEquipmentSwap(SwapType.Necklace); + DrawEquipmentSwap(SwapType.Bracelet); + DrawEquipmentSwap(SwapType.Ring); DrawAccessorySwap(); DrawHairSwap(); DrawFaceSwap(); @@ -384,10 +407,10 @@ public class ItemSwapWindow : IDisposable DrawWeaponSwap(); } - private ImRaii.IEndObject DrawTab( SwapType newTab ) + private ImRaii.IEndObject DrawTab(SwapType newTab) { - using var tab = ImRaii.TabItem( newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString() ); - if( tab ) + using var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); + if (tab) { _dirty |= _lastTab != newTab; _lastTab = newTab; @@ -400,82 +423,75 @@ public class ItemSwapWindow : IDisposable private void DrawAccessorySwap() { - using var tab = DrawTab( SwapType.BetweenSlots ); - if( !tab ) - { + using var tab = DrawTab(SwapType.BetweenSlots); + if (!tab) return; - } - using var table = ImRaii.Table( "##settings", 3, ImGuiTableFlags.SizingFixedFit ); - ImGui.TableSetupColumn( "##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "and put them on these" ).X ); + using var table = ImRaii.Table("##settings", 3, ImGuiTableFlags.SizingFixedFit); + ImGui.TableSetupColumn("##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("and put them on these").X); - var (article1, article2, selector) = GetAccessorySelector( _slotFrom, true ); + var (article1, article2, selector) = GetAccessorySelector(_slotFrom, true); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( $"Take {article1}" ); + ImGui.TextUnformatted($"Take {article1}"); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); - using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) ) + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName())) { - if( combo ) - { - foreach( var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head) ) + if (combo) + foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head)) { - if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom ) - { - _dirty = true; - _slotFrom = slot; - if( slot == _slotTo ) - { - _slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s ); - } - } + if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom) + continue; + + _dirty = true; + _slotFrom = slot; + if (slot == _slotTo) + _slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s); } - } } ImGui.TableNextColumn(); - _dirty |= selector.Draw( "##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); - (article1, _, selector) = GetAccessorySelector( _slotTo, false ); + (article1, _, selector) = GetAccessorySelector(_slotTo, false); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( $"and put {article2} on {article1}" ); + ImGui.TextUnformatted($"and put {article2} on {article1}"); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); - using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) ) + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + using (var combo = ImRaii.Combo("##toType", _slotTo.ToName())) { - if( combo ) - { - foreach( var slot in EquipSlotExtensions.AccessorySlots.Where( s => s != _slotFrom ) ) + if (combo) + foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom)) { - if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo ) - { - _dirty = true; - _slotTo = slot; - } + if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo) + continue; + + _dirty = true; + _slotTo = slot; } - } } ImGui.TableNextColumn(); - - _dirty |= selector.Draw( "##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - if( _affectedItems is { Length: > 1 } ) - { - ImGui.SameLine(); - ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, selector.CurrentSelection.Item2 ) ) - .Select( i => i.Name.ToDalamudString().TextValue ) ) ); - } - } + + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + ImGui.GetTextLineHeightWithSpacing()); + if (_affectedItems is not { Length: > 1 }) + return; + + ImGui.SameLine(); + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + Colors.PressEnterWarningBg); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2)) + .Select(i => i.Name.ToDalamudString().TextValue))); } - private (string, string, ItemSelector) GetAccessorySelector( EquipSlot slot, bool source ) + private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) { var (type, article1, article2) = slot switch { @@ -487,8 +503,8 @@ public class ItemSwapWindow : IDisposable EquipSlot.LFinger => (SwapType.Ring, "this", "it"), _ => (SwapType.Ring, "this", "it"), }; - var tuple = _selectors[ type ]; - return (article1, article2, source ? tuple.Source : tuple.Target); + var (itemSelector, target, _, _) = _selectors[type]; + return (article1, article2, source ? itemSelector : target); } private void DrawEquipmentSwap(SwapType type) @@ -524,15 +540,15 @@ public class ItemSwapWindow : IDisposable _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if (_affectedItems is { Length: > 1 }) - { - ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, - Colors.PressEnterWarningBg); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); - } + if (_affectedItems is not { Length: > 1 }) + return; + + ImGui.SameLine(); + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + Colors.PressEnterWarningBg); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) + .Select(i => i.Name.ToDalamudString().TextValue))); } private void DrawHairSwap() @@ -602,14 +618,14 @@ public class ItemSwapWindow : IDisposable ImGui.GetTextLineHeightWithSpacing())) { _dirty = true; - _weaponSource = new ItemSelector(_slotSelector.CurrentSelection); - _weaponTarget = new ItemSelector(_slotSelector.CurrentSelection); + _weaponSource = new ItemSelector(_itemService, _slotSelector.CurrentSelection); + _weaponTarget = new ItemSelector(_itemService, _slotSelector.CurrentSelection); } else { _dirty = _weaponSource == null || _weaponTarget == null; - _weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection); - _weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection); + _weaponSource ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection); + _weaponTarget ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection); } ImGui.TableNextColumn(); @@ -706,29 +722,6 @@ public class ItemSwapWindow : IDisposable _ => string.Empty, }; - - public void DrawItemSwapPanel() - { - using var tab = ImRaii.TabItem("Item Swap (WIP)"); - if (!tab) - return; - - ImGui.NewLine(); - DrawHeaderLine(300 * UiHelpers.Scale); - ImGui.NewLine(); - - DrawSwapBar(); - - using var table = ImRaii.ListBox("##swaps", -Vector2.One); - if (_loadException != null) - ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}"); - else if (_swapData.Loaded) - foreach (var swap in _swapData.Swaps) - DrawSwap(swap); - else - ImGui.TextUnformatted(NonExistentText()); - } - private static void DrawSwap(Swap swap) { var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; @@ -754,10 +747,10 @@ public class ItemSwapWindow : IDisposable private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) { - if (modIdx == _mod?.Index) - { - _swapData.LoadMod(_mod, _modSettings); - _dirty = true; - } + if (modIdx != _mod?.Index) + return; + + _swapData.LoadMod(_mod, _modSettings); + _dirty = true; } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs similarity index 80% rename from Penumbra/UI/Classes/ModEditWindow.Files.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 469b10a8..3d0df39d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -9,27 +9,29 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Mods; using Penumbra.String.Classes; +using Penumbra.UI.Classes; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly HashSet _selectedFiles = new(256); - private LowerString _fileFilter = LowerString.Empty; - private bool _showGamePaths = true; - private string _gamePathEdit = string.Empty; - private int _fileIdx = -1; - private int _pathIdx = -1; - private int _folderSkip = 0; - private bool _overviewMode = false; - private LowerString _fileOverviewFilter1 = LowerString.Empty; - private LowerString _fileOverviewFilter2 = LowerString.Empty; - private LowerString _fileOverviewFilter3 = LowerString.Empty; + private readonly HashSet _selectedFiles = new(256); + private LowerString _fileFilter = LowerString.Empty; + private bool _showGamePaths = true; + private string _gamePathEdit = string.Empty; + private int _fileIdx = -1; + private int _pathIdx = -1; + private int _folderSkip; + private bool _overviewMode; - private bool CheckFilter(Mod.Editor.FileRegistry registry) + private LowerString _fileOverviewFilter1 = LowerString.Empty; + private LowerString _fileOverviewFilter2 = LowerString.Empty; + private LowerString _fileOverviewFilter3 = LowerString.Empty; + + private bool CheckFilter(FileRegistry registry) => _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase); - private bool CheckFilter((Mod.Editor.FileRegistry, int) p) + private bool CheckFilter((FileRegistry, int) p) => CheckFilter(p.Item1); private void DrawFileTab() @@ -74,13 +76,13 @@ public partial class ModEditWindow var idx = 0; - var files = _editor!.AvailableFiles.SelectMany(f => + var files = _editor.Files.Available.SelectMany(f => { var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, - _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + _editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); }); void DrawLine((string, string, string, uint) data) @@ -119,7 +121,7 @@ public partial class ModEditWindow if (!list) return; - foreach (var (registry, i) in _editor!.AvailableFiles.WithIndex().Where(CheckFilter)) + foreach (var (registry, i) in _editor.Files.Available.WithIndex().Where(CheckFilter)) { using var id = ImRaii.PushId(i); ImGui.TableNextColumn(); @@ -133,17 +135,17 @@ public partial class ModEditWindow for (var j = 0; j < registry.SubModUsage.Count; ++j) { var (subMod, gamePath) = registry.SubModUsage[j]; - if (subMod != _editor.CurrentOption) + if (subMod != _editor.Option) continue; PrintGamePath(i, j, registry, subMod, gamePath); } - PrintNewGamePath(i, registry, _editor.CurrentOption); + PrintNewGamePath(i, registry, _editor.Option!); } } - private static string DrawFileTooltip(Mod.Editor.FileRegistry registry, ColorId color) + private static string DrawFileTooltip(FileRegistry registry, ColorId color) { (string, int) GetMulti() { @@ -172,7 +174,7 @@ public partial class ModEditWindow }; } - private void DrawSelectable(Mod.Editor.FileRegistry registry) + private void DrawSelectable(FileRegistry registry) { var selected = _selectedFiles.Contains(registry); var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : @@ -192,7 +194,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -211,7 +213,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) - _editor!.SetGamePath(_fileIdx, _pathIdx, path); + _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; @@ -219,7 +221,7 @@ public partial class ModEditWindow else if (_fileIdx == i && _pathIdx == j && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) - || !path.IsEmpty && !path.Equals(gamePath) && !_editor!.CanAddGamePath(path))) + || !path.IsEmpty && !path.Equals(gamePath) && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); ImGui.SetCursorPosX(pos); @@ -228,7 +230,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, Mod.Editor.FileRegistry registry, ISubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, ISubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); @@ -243,7 +245,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) - _editor!.SetGamePath(_fileIdx, _pathIdx, path); + _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; @@ -251,7 +253,7 @@ public partial class ModEditWindow else if (_fileIdx == i && _pathIdx == -1 && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) - || !path.IsEmpty && !_editor!.CanAddGamePath(path))) + || !path.IsEmpty && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); ImGui.SetCursorPosX(pos); @@ -271,7 +273,7 @@ public partial class ModEditWindow ImGui.SameLine(); spacing.Pop(); if (ImGui.Button("Add Paths")) - _editor!.AddPathsToSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains), _folderSkip); + _editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.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."); @@ -279,25 +281,25 @@ public partial class ModEditWindow ImGui.SameLine(); if (ImGui.Button("Remove Paths")) - _editor!.RemovePathsFromSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains)); + _editor.FileEditor.RemovePathsFromSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains)); ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option."); ImGui.SameLine(); if (ImGui.Button("Delete Selected Files")) - _editor!.DeleteFiles(_editor!.AvailableFiles.Where(_selectedFiles.Contains)); + _editor.FileEditor.DeleteFiles(_editor.Mod!, _editor.Option!, _editor.Files.Available.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!!!"); ImGui.SameLine(); - var changes = _editor!.FileChanges; + var changes = _editor.FileEditor.Changes; var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor!.ApplyFiles(); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!); if (failedFiles > 0) - Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}."); + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); } @@ -305,7 +307,7 @@ public partial class ModEditWindow var label = changes ? "Revert Changes" : "Reload Files"; var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); if (ImGui.Button(label, length)) - _editor!.RevertFiles(); + _editor.FileEditor.RevertFiles(_editor.Mod!, _editor.Option!); ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh."); @@ -325,19 +327,19 @@ public partial class ModEditWindow ImGui.SameLine(); if (ImGui.Button("Select Visible")) - _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter)); + _selectedFiles.UnionWith(_editor.Files.Available.Where(CheckFilter)); ImGui.SameLine(); if (ImGui.Button("Select Unused")) - _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.SubModUsage.Count == 0)); + _selectedFiles.UnionWith(_editor.Files.Available.Where(f => f.SubModUsage.Count == 0)); ImGui.SameLine(); if (ImGui.Button("Select Used Here")) - _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.CurrentUsage > 0)); + _selectedFiles.UnionWith(_editor.Files.Available.Where(f => f.CurrentUsage > 0)); ImGui.SameLine(); - ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected"); + ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected"); } private void DrawFileManagementOverview() diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index d259e300..5a338b9c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -9,7 +9,7 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Functions; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 7c121ca6..a0028a6e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -15,7 +15,7 @@ using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 92e4db6d..16ad708c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -13,7 +13,7 @@ using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs similarity index 96% rename from Penumbra/UI/Classes/ModEditWindow.Materials.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 7a170803..306293af 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,8 +6,9 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { @@ -121,7 +122,7 @@ public partial class ModEditWindow private void DrawMaterialReassignmentTab() { - if( _editor!.ModelFiles.Count == 0 ) + if( _editor.Files.Mdl.Count == 0 ) { return; } @@ -149,7 +150,7 @@ public partial class ModEditWindow } var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) + foreach( var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex() ) { using var id = ImRaii.PushId( idx ); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs new file mode 100644 index 00000000..31dce033 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -0,0 +1,886 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private const string ModelSetIdTooltip = + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string PrimaryIdTooltip = + "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; + + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem("Meta Manipulations"); + if (!tab) + return; + + DrawOptionSelectHeader(); + + var setsEqual = !_editor.MetaEditor.Changes; + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) + _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) + _editor.MetaEditor.Load(_editor.Option!); + + ImGui.SameLine(); + AddFromClipboardButton(); + ImGui.SameLine(); + SetFromClipboardButton(); + ImGui.SameLine(); + CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); + ImGui.SameLine(); + if (ImGui.Button("Write as TexTools Files")) + _mod!.WriteAllTexToolsMeta(); + + using var child = ImRaii.Child("##meta", -Vector2.One, true); + if (!child) + return; + + DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew); + } + + + // The headers for the different meta changes all have basically the same structure for different types. + private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, Action draw, + Action drawNew) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + if (!ImGui.CollapsingHeader($"{items.Count} {label}")) + return; + + using (var table = ImRaii.Table(label, numColumns, flags)) + { + if (table) + { + drawNew(_editor, _iconSize); + foreach (var (item, index) in items.ToArray().WithIndex()) + { + using var id = ImRaii.PushId(index); + draw(item, _editor, _iconSize); + } + } + } + + ImGui.NewLine(); + } + + private static class EqpRow + { + private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); + + private static float IdWidth + => 100 * UiHelpers.Scale; + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, + editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedEqpFile.GetDefault(_new.SetId); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry)); + + // Identifier + ImGui.TableNextColumn(); + if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + _new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), _new.Slot, setId); + + ImGuiUtil.HoverTooltip(ModelSetIdTooltip); + + ImGui.TableNextColumn(); + if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot)) + _new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), slot, _new.SetId); + + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + foreach (var flag in Eqp.EqpAttributes[_new.Slot]) + { + var value = defaultEntry.HasFlag(flag); + Checkmark("##eqp", flag.ToLocalName(), value, value, out _); + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + public static void Draw(EqpManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.SetId.ToString()); + ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); + var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Slot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + + // Values + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + var idx = 0; + foreach (var flag in Eqp.EqpAttributes[meta.Slot]) + { + using var id = ImRaii.PushId(idx++); + var defaultValue = defaultEntry.HasFlag(flag); + var currentValue = meta.Entry.HasFlag(flag); + if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) + editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag)); + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + } + + + private static class EqdpRow + { + private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); + + private static float IdWidth + => 100 * UiHelpers.Scale; + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, + editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var raceCode = Names.CombinedRace(_new.Gender, _new.Race); + var validRaceCode = CharacterUtility.EqdpIdx(raceCode, false) >= 0; + var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : + validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; + var defaultEntry = validRaceCode + ? ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) + : 0; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry)); + + // Identifier + ImGui.TableNextColumn(); + if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + { + var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); + _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); + } + + ImGuiUtil.HoverTooltip(ModelSetIdTooltip); + + ImGui.TableNextColumn(); + if (Combos.Race("##eqdpRace", _new.Race, out var race)) + { + var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId); + _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); + } + + ImGuiUtil.HoverTooltip(ModelRaceTooltip); + + ImGui.TableNextColumn(); + if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) + { + var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId); + _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); + } + + ImGuiUtil.HoverTooltip(GenderTooltip); + + ImGui.TableNextColumn(); + if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) + { + var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId); + _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); + } + + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + var (bit1, bit2) = defaultEntry.ToBits(_new.Slot); + Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _); + ImGui.SameLine(); + Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); + } + + public static void Draw(EqdpManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.SetId.ToString()); + ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Race.ToName()); + ImGuiUtil.HoverTooltip(ModelRaceTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Gender.ToName()); + ImGuiUtil.HoverTooltip(GenderTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Slot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + + // Values + var defaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); + var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); + var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); + ImGui.TableNextColumn(); + if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) + editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); + + ImGui.SameLine(); + if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2)) + editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); + } + } + + private static class ImcRow + { + private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + + private static float IdWidth + => 80 * UiHelpers.Scale; + + private static float SmallIdWidth + => 45 * UiHelpers.Scale; + + // Convert throwing to null-return if the file does not exist. + private static ImcEntry? GetDefault(ImcManipulation imc) + { + try + { + return ImcFile.GetDefault(imc.GamePath(), imc.EquipSlot, imc.Variant, out _); + } + catch + { + return null; + } + } + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, + editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var defaultEntry = GetDefault(_new); + var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; + defaultEntry ??= new ImcEntry(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry.Value)); + + // Identifier + ImGui.TableNextColumn(); + if (Combos.ImcType("##imcType", _new.ObjectType, out var type)) + { + var equipSlot = type switch + { + ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + _new = new ImcManipulation(type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? (ushort)1 : _new.SecondaryId, + _new.Variant, equipSlot, _new.Entry); + } + + ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + + ImGui.TableNextColumn(); + if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry) + .Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGuiUtil.HoverTooltip(PrimaryIdTooltip); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + + ImGui.TableNextColumn(); + // Equipment and accessories are slightly different imcs than other types. + if (_new.ObjectType is ObjectType.Equipment) + { + if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + .Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else if (_new.ObjectType is ObjectType.Accessory) + { + if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + .Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else + { + if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry) + .Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGuiUtil.HoverTooltip(SecondaryIdTooltip); + } + + ImGui.TableNextColumn(); + if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, + _new.Entry).Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGui.TableNextColumn(); + if (_new.ObjectType is ObjectType.DemiHuman) + { + if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + .Copy(GetDefault(_new) + ?? new ImcEntry()); + + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else + { + ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); + } + + ImGuiUtil.HoverTooltip(VariantIdTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + IntDragInput("##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, + 1, byte.MaxValue, 0f); + ImGui.SameLine(); + IntDragInput("##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, + defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f); + ImGui.TableNextColumn(); + IntDragInput("##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, + byte.MaxValue, 0f); + ImGui.SameLine(); + IntDragInput("##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, + 0f); + ImGui.SameLine(); + IntDragInput("##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, + 0f); + ImGui.TableNextColumn(); + for (var i = 0; i < 10; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + Checkmark("##attribute", $"{(char)('A' + i)}", (defaultEntry.Value.AttributeMask & flag) != 0, + (defaultEntry.Value.AttributeMask & flag) != 0, out _); + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + public static void Draw(ImcManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.ObjectType.ToName()); + ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.PrimaryId.ToString()); + ImGuiUtil.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else + { + ImGui.TextUnformatted(meta.SecondaryId.ToString()); + ImGuiUtil.HoverTooltip(SecondaryIdTooltip); + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Variant.ToString()); + ImGuiUtil.HoverTooltip(VariantIdTooltip); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.DemiHuman) + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + + // Values + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.TableNextColumn(); + var defaultEntry = GetDefault(meta) ?? new ImcEntry(); + if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, + defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId })); + + ImGui.SameLine(); + if (IntDragInput("##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, + meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialAnimationId = (byte)materialAnimId })); + + ImGui.TableNextColumn(); + if (IntDragInput("##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, + defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { DecalId = (byte)decalId })); + + ImGui.SameLine(); + if (IntDragInput("##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, + out var vfxId, 0, byte.MaxValue, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { VfxId = (byte)vfxId })); + + ImGui.SameLine(); + if (IntDragInput("##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, + defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { SoundId = (byte)soundId })); + + ImGui.TableNextColumn(); + for (var i = 0; i < 10; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + if (Checkmark("##attribute", $"{(char)('A' + i)}", (meta.Entry.AttributeMask & flag) != 0, + (defaultEntry.AttributeMask & flag) != 0, out var val)) + { + var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; + editor.MetaEditor.Change(meta.Copy(meta.Entry with { AttributeMask = (ushort)attributes })); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + } + + private static class EstRow + { + private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); + + private static float IdWidth + => 100 * UiHelpers.Scale; + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, + editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry)); + + // Identifier + ImGui.TableNextColumn(); + if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + { + var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); + _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); + } + + ImGuiUtil.HoverTooltip(ModelSetIdTooltip); + + ImGui.TableNextColumn(); + if (Combos.Race("##estRace", _new.Race, out var race)) + { + var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); + _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); + } + + ImGuiUtil.HoverTooltip(ModelRaceTooltip); + + ImGui.TableNextColumn(); + if (Combos.Gender("##estGender", _new.Gender, out var gender)) + { + var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); + _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); + } + + ImGuiUtil.HoverTooltip(GenderTooltip); + + ImGui.TableNextColumn(); + if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) + { + var newDefaultEntry = EstFile.GetDefault(slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); + _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); + } + + ImGuiUtil.HoverTooltip(EstTypeTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); + } + + public static void Draw(EstManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.SetId.ToString()); + ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Race.ToName()); + ImGuiUtil.HoverTooltip(ModelRaceTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Gender.ToName()); + ImGuiUtil.HoverTooltip(GenderTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Slot.ToString()); + ImGuiUtil.HoverTooltip(EstTypeTooltip); + + // Values + var defaultEntry = EstFile.GetDefault(meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); + ImGui.TableNextColumn(); + if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, + out var entry, 0, ushort.MaxValue, 0.05f)) + editor.MetaEditor.Change(meta.Copy((ushort)entry)); + } + } + + private static class GmpRow + { + private static GmpManipulation _new = new(GmpEntry.Default, 1); + + private static float RotationWidth + => 75 * UiHelpers.Scale; + + private static float UnkWidth + => 50 * UiHelpers.Scale; + + private static float IdWidth + => 100 * UiHelpers.Scale; + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, + editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedGmpFile.GetDefault(_new.SetId); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry)); + + // Identifier + ImGui.TableNextColumn(); + if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + _new = new GmpManipulation(ExpandedGmpFile.GetDefault(setId), setId); + + ImGuiUtil.HoverTooltip(ModelSetIdTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _); + ImGui.TableNextColumn(); + Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _); + ImGui.TableNextColumn(); + IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, + 360, 0f); + ImGui.SameLine(); + IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, + 360, 0f); + ImGui.SameLine(); + IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, + 360, 0f); + ImGui.TableNextColumn(); + IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f); + ImGui.SameLine(); + IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); + } + + public static void Draw(GmpManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.SetId.ToString()); + ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); + + // Values + var defaultEntry = ExpandedGmpFile.GetDefault(meta.SetId); + ImGui.TableNextColumn(); + if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); + + ImGui.TableNextColumn(); + if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated })); + + ImGui.TableNextColumn(); + if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, + meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA })); + + ImGui.SameLine(); + if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, + meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB })); + + ImGui.SameLine(); + if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, + meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC })); + + ImGui.TableNextColumn(); + if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, + defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA })); + + ImGui.SameLine(); + if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, + defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) + editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkB })); + } + } + + private static class RspRow + { + private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); + + private static float FloatWidth + => 150 * UiHelpers.Scale; + + public static void DrawNew(ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, + editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = CmpFile.GetDefault(_new.SubRace, _new.Attribute); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new.Copy(defaultEntry)); + + // Identifier + ImGui.TableNextColumn(); + if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) + _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(subRace, _new.Attribute)); + + ImGuiUtil.HoverTooltip(RacialTribeTooltip); + + ImGui.TableNextColumn(); + if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) + _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(subRace, attribute)); + + ImGuiUtil.HoverTooltip(ScalingTypeTooltip); + + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(FloatWidth); + ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); + } + + public static void Draw(RspManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.SubRace.ToName()); + ImGuiUtil.HoverTooltip(RacialTribeTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Attribute.ToFullString()); + ImGuiUtil.HoverTooltip(ScalingTypeTooltip); + ImGui.TableNextColumn(); + + // Values + var def = CmpFile.GetDefault(meta.SubRace, meta.Attribute); + var value = meta.Entry; + ImGui.SetNextItemWidth(FloatWidth); + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config), + def != value); + if (ImGui.DragFloat("##rspValue", ref value, 0.001f, 0.01f, 8f) && value is >= 0.01f and <= 8f) + editor.MetaEditor.Change(meta.Copy(value)); + + ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); + } + } + + // A number input for ids with a optional max id of given width. + // Returns true if newId changed against currentId. + private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(width); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImGui.InputInt(label, ref tmp, 0)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + // A checkmark that compares against a default value and shows a tooltip. + // Returns true if newValue is changed against currentValue. + private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), + defaultValue != currentValue); + newValue = currentValue; + ImGui.Checkbox(label, ref newValue); + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); + return newValue != currentValue; + } + + // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + // Returns true if newValue changed against currentValue. + private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, + int minValue, int maxValue, float speed) + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue)) + newValue = Math.Clamp(newValue, minValue, maxValue); + + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); + + return newValue != currentValue; + } + + private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, IEnumerable manipulations) + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) + return; + + var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion); + if (text.Length > 0) + ImGui.SetClipboardText(text); + } + + private void AddFromClipboardButton() + { + if (ImGui.Button("Add from Clipboard")) + { + var clipboard = ImGuiUtil.GetClipboardText(); + + var version = Functions.FromCompressedBase64(clipboard, out var manips); + if (version == MetaManipulation.CurrentVersion && manips != null) + foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) + _editor.MetaEditor.Set(manip); + } + + ImGuiUtil.HoverTooltip( + "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations."); + } + + private void SetFromClipboardButton() + { + if (ImGui.Button("Set from Clipboard")) + { + var clipboard = ImGuiUtil.GetClipboardText(); + var version = Functions.FromCompressedBase64(clipboard, out var manips); + if (version == MetaManipulation.CurrentVersion && manips != null) + { + _editor.MetaEditor.Clear(); + foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) + _editor.MetaEditor.Set(manip); + } + } + + ImGuiUtil.HoverTooltip( + "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); + } + + private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) + editor.MetaEditor.Delete(meta); + } +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs similarity index 98% rename from Penumbra/UI/Classes/ModEditWindow.Models.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 73f2a7dc..b212e791 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -5,8 +5,9 @@ using Penumbra.GameData.Files; using Penumbra.String.Classes; using System.Globalization; using System.Linq; +using Penumbra.UI.AdvancedWindow; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index bce1c61f..b6af9dd9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -14,10 +14,10 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String; -using Penumbra.Util; +using Penumbra.UI.AdvancedWindow; using static Penumbra.GameData.Files.ShpkFile; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 0bcac7a3..1720ec8c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -6,7 +6,7 @@ using OtterGui; using Penumbra.GameData.Data; using Penumbra.GameData.Files; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs similarity index 98% rename from Penumbra/UI/Classes/ModEditWindow.Textures.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 05a878e9..2ddd3ded 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -7,7 +7,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Import.Textures; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { @@ -43,7 +43,7 @@ public partial class ModEditWindow tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog); - var files = _editor!.TexFiles.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, _mod.ModPath.FullName.Length + 1); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs similarity index 76% rename from Penumbra/UI/Classes/ModEditWindow.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d6ad92db..fb9690c2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Numerics; using System.Text; +using Dalamud.Data; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; @@ -12,31 +13,32 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Mods; -using Penumbra.Services; using Penumbra.String.Classes; +using Penumbra.UI.Classes; using Penumbra.Util; using static Penumbra.Mods.Mod; -namespace Penumbra.UI.Classes; +namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow : Window, IDisposable { - private const string WindowBaseLabel = "###SubModEdit"; - internal readonly ItemSwapWindow _swapWindow; + private const string WindowBaseLabel = "###SubModEdit"; + + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; - private Editor? _editor; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; - private bool _allowReduplicate = false; + private bool _allowReduplicate; public void ChangeMod(Mod mod) { if (mod == _mod) return; - _editor?.Dispose(); - _editor = new Editor(mod, mod.Default); - _mod = mod; + _editor.LoadMod(mod, -1, 0); + _mod = mod; SizeConstraints = new WindowSizeConstraints { @@ -47,17 +49,20 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _swapWindow.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); } - public void ChangeOption(ISubMod? subMod) - => _editor?.SetSubMod(subMod); + public void ChangeOption(SubMod? subMod) + => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.GroupIdx ?? 0); public void UpdateModels() - => _editor?.ScanModels(); + { + if (_mod != null) + _editor.MdlMaterialEditor.ScanModels(_mod); + } public override bool DrawConditions() - => _editor != null; + => _mod != null; public override void PreDraw() { @@ -67,7 +72,7 @@ public partial class ModEditWindow : Window, IDisposable var redirections = 0; var unused = 0; - var size = _editor!.AvailableFiles.Sum(f => + var size = _editor.Files.Available.Sum(f => { if (f.SubModUsage.Count > 0) redirections += f.SubModUsage.Count; @@ -89,13 +94,13 @@ public partial class ModEditWindow : Window, IDisposable sb.Append($" | {subMods} Options"); if (size > 0) - sb.Append($" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize(size)})"); + sb.Append($" | {_editor.Files.Available.Count} Files ({Functions.HumanReadableSize(size)})"); if (unused > 0) sb.Append($" | {unused} Unused Files"); - if (_editor.MissingFiles.Count > 0) - sb.Append($" | {_editor.MissingFiles.Count} Missing Files"); + if (_editor.Files.Missing.Count > 0) + sb.Append($" | {_editor.Files.Available.Count} Missing Files"); if (redirections > 0) sb.Append($" | {redirections} Redirections"); @@ -106,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable if (swaps > 0) sb.Append($" | {swaps} Swaps"); - _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; + _allowReduplicate = redirections != _editor.Files.Available.Count || _editor.Files.Available.Count > 0; sb.Append(WindowBaseLabel); WindowName = sb.ToString(); } @@ -136,7 +141,7 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - _swapWindow.DrawItemSwapPanel(); + _itemSwapTab.DrawContent(); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. @@ -169,7 +174,7 @@ public partial class ModEditWindow : Window, IDisposable } } - public static void Draw(Editor editor, Vector2 buttonSize) + public static void Draw(ModEditor editor, Vector2 buttonSize) { DrawRaceCodeCombo(buttonSize); ImGui.SameLine(); @@ -179,7 +184,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SetNextItemWidth(buttonSize.X); ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32); ImGui.SameLine(); - var disabled = !Editor.ValidString(_materialSuffixTo); + var disabled = !MdlMaterialEditor.ValidString(_materialSuffixTo); var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." : _materialSuffixFrom == _materialSuffixTo @@ -194,17 +199,17 @@ public partial class ModEditWindow : Window, IDisposable ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; if (ImGuiUtil.DrawDisabledButton("Change Material Suffix", buttonSize, tt, disabled)) - editor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode); + editor.MdlMaterialEditor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode); - var anyChanges = editor.ModelFiles.Any(m => m.Changed); + var anyChanges = editor.MdlMaterialEditor.ModelFiles.Any(m => m.Changed); if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize, anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges)) - editor.SaveAllModels(); + editor.MdlMaterialEditor.SaveAllModels(); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize, anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges)) - editor.RestoreAllModels(); + editor.MdlMaterialEditor.RestoreAllModels(); ImGui.SameLine(); ImGuiComponents.HelpMarker( @@ -216,7 +221,7 @@ public partial class ModEditWindow : Window, IDisposable private void DrawMissingFilesTab() { - if (_editor!.MissingFiles.Count == 0) + if (_editor.Files.Missing.Count == 0) return; using var tab = ImRaii.TabItem("Missing Files"); @@ -225,7 +230,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.NewLine(); if (ImGui.Button("Remove Missing Files from Mod")) - _editor.RemoveMissingPaths(); + _editor.FileEditor.RemoveMissingPaths(_mod!, _editor.Option!); using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true); if (!child) @@ -235,7 +240,7 @@ public partial class ModEditWindow : Window, IDisposable if (!table) return; - foreach (var path in _editor.MissingFiles) + foreach (var path in _editor.Files.Missing) { ImGui.TableNextColumn(); ImGui.TextUnformatted(path.FullName); @@ -248,37 +253,44 @@ public partial class ModEditWindow : Window, IDisposable if (!tab) return; - var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; + var buttonText = _editor.Duplicates.Finished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", - !_editor.DuplicatesFinished)) - _editor.StartDuplicateCheck(); + !_editor.Duplicates.Finished)) + _editor.Duplicates.StartDuplicateCheck(_editor.Files.Available); const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + "This will also delete all unused files and directories if it succeeds.\n" + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; - var modifier = Penumbra.Config.DeleteModModifier.IsActive(); + var modifier = _config.DeleteModModifier.IsActive(); var tt = _allowReduplicate ? desc : modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - _mod!.Normalize(Penumbra.ModManager); - _editor.RevertFiles(); + _editor.ModNormalizer.Normalize(_mod!); + _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx); } - if (!_editor.DuplicatesFinished) + if (_editor.ModNormalizer.Running) + { + using var popup = ImRaii.Popup("Normalization", ImGuiWindowFlags.Modal); + ImGui.ProgressBar((float)_editor.ModNormalizer.Step / _editor.ModNormalizer.TotalSteps, + new Vector2(300 * UiHelpers.Scale, ImGui.GetFrameHeight()), + $"{_editor.ModNormalizer.Step} / {_editor.ModNormalizer.TotalSteps}"); + } + + if (!_editor.Duplicates.Finished) { ImGui.SameLine(); if (ImGui.Button("Cancel")) - _editor.Cancel(); - + _editor.Duplicates.Clear(); return; } - if (_editor.Duplicates.Count == 0) + if (_editor.Duplicates.Duplicates.Count == 0) { ImGui.NewLine(); ImGui.TextUnformatted("No duplicates found."); @@ -286,12 +298,12 @@ public partial class ModEditWindow : Window, IDisposable } if (ImGui.Button("Delete and Redirect Duplicates")) - _editor.DeleteDuplicates(); + _editor.Duplicates.DeleteDuplicates(_editor.Mod!, _editor.Option!, true); - if (_editor.SavedSpace > 0) + if (_editor.Duplicates.SavedSpace > 0) { ImGui.SameLine(); - ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.SavedSpace)} from your hard drive."); + ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.Duplicates.SavedSpace)} from your hard drive."); } using var child = ImRaii.Child("##duptable", -Vector2.One, true); @@ -307,7 +319,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.TableSetupColumn("size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN ").X); ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed, ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); - foreach (var (set, size, hash) in _editor.Duplicates.Where(s => s.Paths.Length > 1)) + foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], @@ -346,23 +358,23 @@ public partial class ModEditWindow : Window, IDisposable using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); var width = new Vector2(ImGui.GetWindowWidth() / 3, 0); if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor!.CurrentOption.IsDefault)) - _editor.SetSubMod(_mod!.Default); + _editor!.Option!.IsDefault)) + _editor.LoadOption(-1, 0); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) - _editor.SetSubMod(_editor.CurrentOption); + _editor.LoadOption(_editor.GroupIdx, _editor.OptionIdx); ImGui.SameLine(); - using var combo = ImRaii.Combo("##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton); + using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName, ImGuiComboFlags.NoArrowButton); if (!combo) return; - foreach (var option in _mod!.AllSubMods) + foreach (var option in _mod!.AllSubMods.Cast()) { - if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption)) - _editor.SetSubMod(option); + if (ImGui.Selectable(option.FullName, option == _editor.Option)) + _editor.LoadOption(option.GroupIdx, option.OptionIdx); } } @@ -377,16 +389,16 @@ public partial class ModEditWindow : Window, IDisposable DrawOptionSelectHeader(); - var setsEqual = _editor!.CurrentSwaps.SetEquals(_editor.CurrentOption.FileSwaps); + var setsEqual = !_editor!.SwapEditor.Changes; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.ApplySwaps(); + _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) - _editor.RevertSwaps(); + _editor.SwapEditor.Revert(_editor.Option!); using var child = ImRaii.Child("##swaps", -Vector2.One, true); if (!child) @@ -403,30 +415,26 @@ public partial class ModEditWindow : Window, IDisposable ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize); ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthFixed, pathSize); - foreach (var (gamePath, file) in _editor!.CurrentSwaps.ToList()) + foreach (var (gamePath, file) in _editor.SwapEditor.Swaps.ToList()) { using var id = ImRaii.PushId(idx++); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true)) - _editor.CurrentSwaps.Remove(gamePath); + _editor.SwapEditor.Remove(gamePath); ImGui.TableNextColumn(); var tmp = gamePath.Path.ToString(); ImGui.SetNextItemWidth(-1); if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength) && Utf8GamePath.FromString(tmp, out var path) - && !_editor.CurrentSwaps.ContainsKey(path)) - { - _editor.CurrentSwaps.Remove(gamePath); - if (path.Length > 0) - _editor.CurrentSwaps[path] = file; - } + && !_editor.SwapEditor.Swaps.ContainsKey(path)) + _editor.SwapEditor.Change(gamePath, path); ImGui.TableNextColumn(); tmp = file.FullName; ImGui.SetNextItemWidth(-1); if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) - _editor.CurrentSwaps[gamePath] = new FullPath(tmp); + _editor.SwapEditor.Change(gamePath, new FullPath(tmp)); } ImGui.TableNextColumn(); @@ -434,13 +442,13 @@ public partial class ModEditWindow : Window, IDisposable && newPath.Length > 0 && _newSwapValue.Length > 0 && _newSwapValue != _newSwapKey - && !_editor.CurrentSwaps.ContainsKey(newPath); + && !_editor.SwapEditor.Swaps.ContainsKey(newPath); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, true)) { - _editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue); - _newSwapKey = string.Empty; - _newSwapValue = string.Empty; + _editor.SwapEditor.Add(newPath, new FullPath(_newSwapValue)); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; } ImGui.TableNextColumn(); @@ -477,26 +485,21 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } - public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog) + public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, + Configuration config, ModEditor editor) : base(WindowBaseLabel) { - _fileDialog = fileDialog; - _swapWindow = new ItemSwapWindow(communicator); - _materialTab = new FileEditor("Materials", ".mtrl", _fileDialog, - () => _editor?.MtrlFiles ?? Array.Empty(), - DrawMaterialPanel, - () => _mod?.ModPath.FullName ?? string.Empty, + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _fileDialog = fileDialog; + _materialTab = new FileEditor(gameData, config, _fileDialog, "Materials", ".mtrl", + () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor("Models", ".mdl", _fileDialog, - () => _editor?.MdlFiles ?? Array.Empty(), - DrawModelPanel, - () => _mod?.ModPath.FullName ?? string.Empty, - null); - _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", _fileDialog, - () => _editor?.ShpkFiles ?? Array.Empty(), - DrawShaderPackagePanel, - () => _mod?.ModPath.FullName ?? string.Empty, - null); + _modelTab = new FileEditor(gameData, config, _fileDialog, "Models", ".mdl", + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); + _shaderPackageTab = new FileEditor(gameData, config, _fileDialog, "Shader Packages", ".shpk", + () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null); _center = new CombinedTexture(_left, _right); } @@ -506,6 +509,5 @@ public partial class ModEditWindow : Window, IDisposable _left.Dispose(); _right.Dispose(); _center.Dispose(); - _swapWindow.Dispose(); } } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs deleted file mode 100644 index 1d722927..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using System.Reflection; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.Mods; -using Penumbra.Services; -using Penumbra.String.Classes; - -namespace Penumbra.UI.Classes; - -public partial class ModEditWindow -{ - private class FileEditor where T : class, IWritable - { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func> _getFiles; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; - - private Mod.Editor.FileRegistry? _currentPath; - private T? _currentFile; - private Exception? _currentException; - private bool _changed; - - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; - - private IReadOnlyList _list = null!; - - private readonly FileDialogService _fileDialog; - - public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func> getFiles, - Func drawEdit, Func getInitialPath, Func? parseFile) - { - _tabName = tabName; - _fileType = fileType; - _getFiles = getFiles; - _drawEdit = drawEdit; - _getInitialPath = getInitialPath; - _fileDialog = fileDialog; - _parseFile = parseFile ?? DefaultParseFile; - } - - public void Draw() - { - _list = _getFiles(); - using var tab = ImRaii.TabItem(_tabName); - if (!tab) - return; - - ImGui.NewLine(); - DrawFileSelectCombo(); - SaveButton(); - ImGui.SameLine(); - ResetButton(); - ImGui.SameLine(); - DefaultInput(); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - - DrawFilePanel(); - } - - private void DefaultInput() - { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); - ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); - _inInput = ImGui.IsItemActive(); - if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) - { - _fileDialog.Reset(); - try - { - var file = DalamudServices.SGameData.GetFile(_defaultPath); - if (file != null) - { - _defaultException = null; - _defaultFile = _parseFile(file.Data); - } - else - { - _defaultFile = null; - _defaultException = new Exception("File does not exist."); - } - } - catch (Exception e) - { - _defaultFile = null; - _defaultException = e; - } - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", - _defaultFile == null, true)) - _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, - (success, name) => - { - if (!success) - return; - - try - { - File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); - } - catch (Exception e) - { - Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); - } - }, _getInitialPath(), false); - - _fileDialog.Draw(); - } - - public void Reset() - { - _currentException = null; - _currentPath = null; - _currentFile = null; - _changed = false; - } - - private void DrawFileSelectCombo() - { - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..."); - if (!combo) - return; - - foreach (var file in _list) - { - if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath))) - UpdateCurrentFile(file); - - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted("All Game Paths"); - ImGui.Separator(); - using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); - foreach (var (option, gamePath) in file.SubModUsage) - { - ImGui.TableNextColumn(); - UiHelpers.Text(gamePath.Path); - ImGui.TableNextColumn(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); - ImGui.TextUnformatted(option.FullName); - } - } - - if (file.SubModUsage.Count > 0) - { - ImGui.SameLine(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); - ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); - } - } - } - - private static T? DefaultParseFile(byte[] bytes) - => Activator.CreateInstance(typeof(T), bytes) as T; - - private void UpdateCurrentFile(Mod.Editor.FileRegistry path) - { - if (ReferenceEquals(_currentPath, path)) - return; - - _changed = false; - _currentPath = path; - _currentException = null; - try - { - var bytes = File.ReadAllBytes(_currentPath.File.FullName); - _currentFile = _parseFile(bytes); - } - catch (Exception e) - { - _currentFile = null; - _currentException = e; - } - } - - private void SaveButton() - { - if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) - { - File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); - _changed = false; - } - } - - private void ResetButton() - { - if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed)) - { - var tmp = _currentPath; - _currentPath = null; - UpdateCurrentFile(tmp!); - } - } - - private void DrawFilePanel() - { - using var child = ImRaii.Child("##filePanel", -Vector2.One, true); - if (!child) - return; - - if (_currentPath != null) - { - if (_currentFile == null) - { - ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); - if (_currentException != null) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped(_currentException.ToString()); - } - } - else - { - using var id = ImRaii.PushId(0); - _changed |= _drawEdit(_currentFile, false); - } - } - - if (!_inInput && _defaultPath.Length > 0) - { - if (_currentPath != null) - { - ImGui.NewLine(); - ImGui.NewLine(); - ImGui.TextUnformatted($"Preview of {_defaultPath}:"); - ImGui.Separator(); - } - - if (_defaultFile == null) - { - ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); - if (_defaultException != null) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped(_defaultException.ToString()); - } - } - else - { - using var id = ImRaii.PushId(1); - _drawEdit(_defaultFile, true); - } - } - } - } -} diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs deleted file mode 100644 index e1f348a5..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ /dev/null @@ -1,975 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; - -namespace Penumbra.UI.Classes; - -public partial class ModEditWindow -{ - private const string ModelSetIdTooltip = - "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - - private const string PrimaryIdTooltip = - "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; - - private void DrawMetaTab() - { - using var tab = ImRaii.TabItem( "Meta Manipulations" ); - if( !tab ) - { - return; - } - - DrawOptionSelectHeader(); - - var setsEqual = !_editor!.Meta.Changes; - var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; - ImGui.NewLine(); - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.ApplyManipulations(); - } - - ImGui.SameLine(); - tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.RevertManipulations(); - } - - ImGui.SameLine(); - AddFromClipboardButton(); - ImGui.SameLine(); - SetFromClipboardButton(); - ImGui.SameLine(); - CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() ); - ImGui.SameLine(); - if( ImGui.Button( "Write as TexTools Files" ) ) - { - _mod!.WriteAllTexToolsMeta(); - } - - using var child = ImRaii.Child( "##meta", -Vector2.One, true ); - if( !child ) - { - return; - } - - DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew ); - DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew ); - DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew ); - DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew ); - } - - - // The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader< T >( IReadOnlyCollection< T > items, string label, int numColumns, Action< T, Mod.Editor, Vector2 > draw, - Action< Mod.Editor, Vector2 > drawNew ) - { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - if( !ImGui.CollapsingHeader( $"{items.Count} {label}" ) ) - { - return; - } - - using( var table = ImRaii.Table( label, numColumns, flags ) ) - { - if( table ) - { - drawNew( _editor!, _iconSize ); - foreach( var (item, index) in items.ToArray().WithIndex() ) - { - using var id = ImRaii.PushId( index ); - draw( item, _editor!, _iconSize ); - } - } - } - - ImGui.NewLine(); - } - - private static class EqpRow - { - private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize, - editor.Meta.Eqp.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var canAdd = editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) - { - _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), _new.Slot, setId ); - } - - ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); - - ImGui.TableNextColumn(); - if( Combos.EqpEquipSlot( "##eqpSlot", 100, _new.Slot, out var slot ) ) - { - _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId ); - } - - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); - foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) - { - var value = defaultEntry.HasFlag( flag ); - Checkmark( "##eqp", flag.ToLocalName(), value, value, out _ ); - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); - var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId ); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Slot.ToName() ); - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - - // Values - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); - var idx = 0; - foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) - { - using var id = ImRaii.PushId( idx++ ); - var defaultValue = defaultEntry.HasFlag( flag ); - var currentValue = meta.Entry.HasFlag( flag ); - if( Checkmark( "##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value ) ) - { - editor.Meta.Change( meta.Copy( value ? meta.Entry | flag : meta.Entry & ~flag ) ); - } - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - - - private static class EqdpRow - { - private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize, - editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace( _new.Gender, _new.Race ); - var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0; - var canAdd = validRaceCode && editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : - validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; - var defaultEntry = validRaceCode - ? ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ) - : 0; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), setId ); - _new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId ); - } - - ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); - - ImGui.TableNextColumn(); - if( Combos.Race( "##eqdpRace", _new.Race, out var race ) ) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId ); - _new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId ); - } - - ImGuiUtil.HoverTooltip( ModelRaceTooltip ); - - ImGui.TableNextColumn(); - if( Combos.Gender( "##eqdpGender", _new.Gender, out var gender ) ) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ); - _new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId ); - } - - ImGuiUtil.HoverTooltip( GenderTooltip ); - - ImGui.TableNextColumn(); - if( Combos.EqdpEquipSlot( "##eqdpSlot", _new.Slot, out var slot ) ) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId ); - _new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId ); - } - - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); - Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ ); - ImGui.SameLine(); - Checkmark( "Model##eqdpCheck2", string.Empty, bit2, bit2, out _ ); - } - - public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Race.ToName() ); - ImGuiUtil.HoverTooltip( ModelRaceTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Gender.ToName() ); - ImGuiUtil.HoverTooltip( GenderTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Slot.ToName() ); - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - - // Values - var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId ); - var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot ); - var (bit1, bit2) = meta.Entry.ToBits( meta.Slot ); - ImGui.TableNextColumn(); - if( Checkmark( "Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) - { - editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) ) ); - } - - ImGui.SameLine(); - if( Checkmark( "Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) - { - editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) ) ); - } - } - } - - private static class ImcRow - { - private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - // Convert throwing to null-return if the file does not exist. - private static ImcEntry? GetDefault( ImcManipulation imc ) - { - try - { - return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant, out _ ); - } - catch - { - return null; - } - } - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize, - editor.Meta.Imc.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var defaultEntry = GetDefault( _new ); - var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; - defaultEntry ??= new ImcEntry(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry.Value ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( Combos.ImcType( "##imcType", _new.ObjectType, out var type ) ) - { - var equipSlot = type switch - { - ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, _new.Variant, equipSlot, _new.Entry ); - } - - ImGuiUtil.HoverTooltip( ObjectTypeTooltip ); - - ImGui.TableNextColumn(); - if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1 ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGuiUtil.HoverTooltip( PrimaryIdTooltip ); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if( _new.ObjectType is ObjectType.Equipment ) - { - if( Combos.EqpEquipSlot( "##imcSlot", 100, _new.EquipSlot, out var slot ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - } - else if( _new.ObjectType is ObjectType.Accessory ) - { - if( Combos.AccessorySlot( "##imcSlot", _new.EquipSlot, out var slot ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - } - else - { - if( IdInput( "##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGuiUtil.HoverTooltip( SecondaryIdTooltip ); - } - - ImGui.TableNextColumn(); - if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGui.TableNextColumn(); - if( _new.ObjectType is ObjectType.DemiHuman ) - { - if( Combos.EqpEquipSlot( "##imcSlot", 70, _new.EquipSlot, out var slot ) ) - { - _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) - ?? new ImcEntry() ); - } - - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - } - else - { - ImGui.Dummy( new Vector2( 70 * UiHelpers.Scale, 0 ) ); - } - - ImGuiUtil.HoverTooltip( VariantIdTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, - 1, byte.MaxValue, 0f ); - ImGui.SameLine(); - IntDragInput( "##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, - defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f ); - ImGui.TableNextColumn(); - IntDragInput( "##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, - byte.MaxValue, 0f ); - ImGui.SameLine(); - IntDragInput( "##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, - 0f ); - ImGui.SameLine(); - IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, - 0f ); - ImGui.TableNextColumn(); - for( var i = 0; i < 10; ++i ) - { - using var id = ImRaii.PushId( i ); - var flag = 1 << i; - Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( defaultEntry.Value.AttributeMask & flag ) != 0, - ( defaultEntry.Value.AttributeMask & flag ) != 0, out _ ); - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.ObjectType.ToName() ); - ImGuiUtil.HoverTooltip( ObjectTypeTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.PrimaryId.ToString() ); - ImGuiUtil.HoverTooltip( "Primary ID" ); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) - { - ImGui.TextUnformatted( meta.EquipSlot.ToName() ); - ImGuiUtil.HoverTooltip( EquipSlotTooltip ); - } - else - { - ImGui.TextUnformatted( meta.SecondaryId.ToString() ); - ImGuiUtil.HoverTooltip( SecondaryIdTooltip ); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Variant.ToString() ); - ImGuiUtil.HoverTooltip( VariantIdTooltip ); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - if( meta.ObjectType is ObjectType.DemiHuman ) - { - ImGui.TextUnformatted( meta.EquipSlot.ToName() ); - } - - // Values - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); - ImGui.TableNextColumn(); - var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); - if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, - defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { MaterialId = ( byte )materialId } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, - meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } ) ); - } - - ImGui.TableNextColumn(); - if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, - defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { DecalId = ( byte )decalId } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, - out var vfxId, 0, byte.MaxValue, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { VfxId = ( byte )vfxId } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, - defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { SoundId = ( byte )soundId } ) ); - } - - ImGui.TableNextColumn(); - for( var i = 0; i < 10; ++i ) - { - using var id = ImRaii.PushId( i ); - var flag = 1 << i; - if( Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( meta.Entry.AttributeMask & flag ) != 0, - ( defaultEntry.AttributeMask & flag ) != 0, out var val ) ) - { - var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; - editor.Meta.Change( meta.Copy( meta.Entry with { AttributeMask = ( ushort )attributes } ) ); - } - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - - private static class EstRow - { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize, - editor.Meta.Est.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var canAdd = editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) - { - var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), setId ); - _new = new EstManipulation( _new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry ); - } - - ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); - - ImGui.TableNextColumn(); - if( Combos.Race( "##estRace", _new.Race, out var race ) ) - { - var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId ); - _new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry ); - } - - ImGuiUtil.HoverTooltip( ModelRaceTooltip ); - - ImGui.TableNextColumn(); - if( Combos.Gender( "##estGender", _new.Gender, out var gender ) ) - { - var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId ); - _new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry ); - } - - ImGuiUtil.HoverTooltip( GenderTooltip ); - - ImGui.TableNextColumn(); - if( Combos.EstSlot( "##estSlot", _new.Slot, out var slot ) ) - { - var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); - _new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry ); - } - - ImGuiUtil.HoverTooltip( EstTypeTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f ); - } - - public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Race.ToName() ); - ImGuiUtil.HoverTooltip( ModelRaceTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Gender.ToName() ); - ImGuiUtil.HoverTooltip( GenderTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Slot.ToString() ); - ImGuiUtil.HoverTooltip( EstTypeTooltip ); - - // Values - var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); - ImGui.TableNextColumn(); - if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, - out var entry, 0, ushort.MaxValue, 0.05f ) ) - { - editor.Meta.Change( meta.Copy( ( ushort )entry ) ); - } - } - } - - private static class GmpRow - { - private static GmpManipulation _new = new(GmpEntry.Default, 1); - - private static float RotationWidth - => 75 * UiHelpers.Scale; - - private static float UnkWidth - => 50 * UiHelpers.Scale; - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize, - editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var canAdd = editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) - { - _new = new GmpManipulation( ExpandedGmpFile.GetDefault( setId ), setId ); - } - - ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ ); - ImGui.TableNextColumn(); - Checkmark( "##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _ ); - ImGui.TableNextColumn(); - IntDragInput( "##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, - 360, 0f ); - ImGui.SameLine(); - IntDragInput( "##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, - 360, 0f ); - ImGui.SameLine(); - IntDragInput( "##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, - 360, 0f ); - ImGui.TableNextColumn(); - IntDragInput( "##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f ); - ImGui.SameLine(); - IntDragInput( "##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f ); - } - - public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); - - // Values - var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId ); - ImGui.TableNextColumn(); - if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { Enabled = enabled } ) ); - } - - ImGui.TableNextColumn(); - if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { Animated = animated } ) ); - } - - ImGui.TableNextColumn(); - if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, - meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { RotationA = ( ushort )rotationA } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, - meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { RotationB = ( ushort )rotationB } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, - meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { RotationC = ( ushort )rotationC } ) ); - } - - ImGui.TableNextColumn(); - if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, - defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkA } ) ); - } - - ImGui.SameLine(); - if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, - defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) ) - { - editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkB } ) ); - } - } - } - - private static class RspRow - { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); - - private static float FloatWidth - => 150 * UiHelpers.Scale; - - public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize, - editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) ); - ImGui.TableNextColumn(); - var canAdd = editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) - { - editor.Meta.Add( _new.Copy( defaultEntry ) ); - } - - // Identifier - ImGui.TableNextColumn(); - if( Combos.SubRace( "##rspSubRace", _new.SubRace, out var subRace ) ) - { - _new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) ); - } - - ImGuiUtil.HoverTooltip( RacialTribeTooltip ); - - ImGui.TableNextColumn(); - if( Combos.RspAttribute( "##rspAttribute", _new.Attribute, out var attribute ) ) - { - _new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) ); - } - - ImGuiUtil.HoverTooltip( ScalingTypeTooltip ); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( FloatWidth ); - ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f ); - } - - public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - DrawMetaButtons( meta, editor, iconSize ); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.SubRace.ToName() ); - ImGuiUtil.HoverTooltip( RacialTribeTooltip ); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); - ImGui.TextUnformatted( meta.Attribute.ToFullString() ); - ImGuiUtil.HoverTooltip( ScalingTypeTooltip ); - ImGui.TableNextColumn(); - - // Values - var def = CmpFile.GetDefault( meta.SubRace, meta.Attribute ); - var value = meta.Entry; - ImGui.SetNextItemWidth( FloatWidth ); - using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config), - def != value ); - if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) - { - editor.Meta.Change( meta.Copy( value ) ); - } - - ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" ); - } - } - - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. - private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border ) - { - int tmp = currentId; - ImGui.SetNextItemWidth( width ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border ); - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border ); - if( ImGui.InputInt( label, ref tmp, 0 ) ) - { - tmp = Math.Clamp( tmp, minId, maxId ); - } - - newId = ( ushort )tmp; - return newId != currentId; - } - - // A checkmark that compares against a default value and shows a tooltip. - // Returns true if newValue is changed against currentValue. - private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue ) - { - using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue ); - newValue = currentValue; - ImGui.Checkbox( label, ref newValue ); - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); - return newValue != currentValue; - } - - // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - // Returns true if newValue changed against currentValue. - private static bool IntDragInput( string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, - int minValue, int maxValue, float speed ) - { - newValue = currentValue; - using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), - defaultValue != currentValue ); - ImGui.SetNextItemWidth( width ); - if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) ) - { - newValue = Math.Clamp( newValue, minValue, maxValue ); - } - - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); - - return newValue != currentValue; - } - - private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations ) - { - if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) ) - { - return; - } - - var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion ); - if( text.Length > 0 ) - { - ImGui.SetClipboardText( text ); - } - } - - private void AddFromClipboardButton() - { - if( ImGui.Button( "Add from Clipboard" ) ) - { - var clipboard = ImGuiUtil.GetClipboardText(); - - var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == MetaManipulation.CurrentVersion && manips != null ) - { - foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) - { - _editor!.Meta.Set( manip ); - } - } - } - - ImGuiUtil.HoverTooltip( - "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." ); - } - - private void SetFromClipboardButton() - { - if( ImGui.Button( "Set from Clipboard" ) ) - { - var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == MetaManipulation.CurrentVersion && manips != null ) - { - _editor!.Meta.Clear(); - foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) - { - _editor!.Meta.Set( manip ); - } - } - } - - ImGuiUtil.HoverTooltip( - "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." ); - } - - private static void DrawMetaButtons( MetaManipulation meta, Mod.Editor editor, Vector2 iconSize ) - { - ImGui.TableNextColumn(); - CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty< MetaManipulation >().Append( meta ) ); - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true ) ) - { - editor.Meta.Delete( meta ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c6384fd0..493c21e8 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -20,7 +20,7 @@ using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public sealed partial class ModFileSystemSelector : FileSystemSelector { @@ -31,13 +31,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), - AddNewMod); + AddNewMod, _config, _modEditor); ImGui.OpenPopup("Import Status"); }, 0, modPath, _config.AlwaysOpenDefaultImport); } diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs index 4c221b21..03fdc177 100644 --- a/Penumbra/UI/ModsTab/ModFilter.cs +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -1,6 +1,6 @@ using System; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; [Flags] public enum ModFilter diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index e6999412..d85f77ff 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,16 +1,16 @@ using System; using Dalamud.Plugin; using Penumbra.Mods; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanel : IDisposable { - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) { diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 49f5d8cf..b38dd725 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -8,7 +8,7 @@ using OtterGui.Widgets; using Penumbra.Api; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelChangedItemsTab : ITab { diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index bcdb0f16..adce69e3 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -9,7 +9,7 @@ using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelConflictsTab : ITab { diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 3cc770a2..d6d04872 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -7,7 +7,7 @@ using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelDescriptionTab : ITab { diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 89bdff92..8dbc94cb 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -12,10 +12,10 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow; using Penumbra.Util; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelEditTab : ITab { @@ -24,6 +24,7 @@ public class ModPanelEditTab : ITab private readonly ModFileSystem _fileSystem; private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; + private readonly ModEditor _editor; private readonly TagButtons _modTags = new(); @@ -33,13 +34,14 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, - ModEditWindow editWindow) + ModEditWindow editWindow, ModEditor editor) { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _chat = chat; - _editWindow = editWindow; + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + _editor = editor; } public ReadOnlySpan Label @@ -126,10 +128,10 @@ public class ModPanelEditTab : ITab { if (ImGui.Button("Update Bibo Material", buttonSize)) { - var editor = new Mod.Editor(_mod, null); - editor.ReplaceAllMaterials("bibo", "b"); - editor.ReplaceAllMaterials("bibopube", "c"); - editor.SaveAllModels(); + _editor.LoadMod(_mod); + _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); + _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); + _editor.MdlMaterialEditor.SaveAllModels(); _editWindow.UpdateModels(); } @@ -142,7 +144,7 @@ public class ModPanelEditTab : ITab private void BackupButtons(Vector2 buttonSize) { - var backup = new ModBackup(_mod); + var backup = new ModBackup(_modManager, _mod); var tt = ModBackup.CreatingBackup ? "Already exporting a mod." : backup.Exists diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 6c0a7efa..bb24a337 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -9,7 +9,7 @@ using OtterGui.Raii; using Penumbra.Mods; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelHeader : IDisposable { diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1f528904..0127e140 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -13,7 +13,7 @@ using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Dalamud.Interface; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab : ITab { diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index a946a916..39f795d0 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -6,9 +6,9 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Mods; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; public class ModPanelTabBar { @@ -107,7 +107,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption(mod.Default); + _modEditWindow.ChangeOption((Mod.SubMod) mod.Default); _modEditWindow.IsOpen = true; } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 6490dc2a..fc15bc77 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -12,8 +12,8 @@ using Penumbra.Api.Enums; using Penumbra.Interop; using Penumbra.Mods; using Penumbra.Services; -using Penumbra.UI.ModTab; -using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; +using Penumbra.UI.ModsTab; +using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; namespace Penumbra.UI.Tabs; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 60f781c5..0e1e4ce5 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,7 +14,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; -using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; +using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; namespace Penumbra.UI.Tabs; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 08143a57..378aebf0 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow; namespace Penumbra; From 21181370e7e06ccde098cbccd1ccf91ecb45e27a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 Mar 2023 15:58:08 +0100 Subject: [PATCH 0811/2451] Add rudimentary quick move support to folders. --- OtterGui | 2 +- Penumbra/Configuration.cs | 3 +++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +++- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 22 ++++++++++++++------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/OtterGui b/OtterGui index e06d547c..e49a05e1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e06d547c1690212c1ed3d471b0f9798101f06145 +Subproject commit e49a05e1863957144955d1c612343ccfff11563e diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 48b3c29e..54ae8ac5 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -79,6 +79,9 @@ public class Configuration : IPluginConfiguration public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; + public string QuickMoveFolder1 { get; set; } = string.Empty; + public string QuickMoveFolder2 { get; set; } = string.Empty; + public string QuickMoveFolder3 { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PrintSuccessfulCommandsToChat { get; set; } = true; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index fb9690c2..035c9286 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -141,7 +141,9 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - _itemSwapTab.DrawContent(); + using var tab = ImRaii.TabItem("Item Swap (WIP)"); + if (tab) + _itemSwapTab.DrawContent(); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 493c21e8..2ee27bfb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -38,7 +38,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector SetQuickMove(f, 0, _config.QuickMoveFolder1, s => { _config.QuickMoveFolder1 = s; _config.Save(); }), 110); + SubscribeRightClickFolder(f => SetQuickMove(f, 1, _config.QuickMoveFolder2, s => { _config.QuickMoveFolder2 = s; _config.Save(); }), 120); + SubscribeRightClickFolder(f => SetQuickMove(f, 2, _config.QuickMoveFolder3, s => { _config.QuickMoveFolder3 = s; _config.Save(); }), 130); SubscribeRightClickLeaf(ToggleLeafFavorite); + SubscribeRightClickLeaf(l => QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); SubscribeRightClickMain(ClearDefaultImportFolder, 100); + SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); + SubscribeRightClickMain(() => ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); + SubscribeRightClickMain(() => ClearQuickMove(2, _config.QuickMoveFolder3, () => {_config.QuickMoveFolder3 = string.Empty; _config.Save();}), 130); AddButton(AddNewModButton, 0); AddButton(AddImportModButton, 1); AddButton(AddHelpButton, 2); AddButton(DeleteModButton, 1000); + // @formatter:on SetFilterTooltip(); SelectionChanged += OnSelectionChange; From c5ac9f6f08fd81a664592bd6232c8a68776c01fe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 Mar 2023 16:44:07 +0100 Subject: [PATCH 0812/2451] Derp --- Penumbra/Import/MetaFileInfo.cs | 5 ++- Penumbra/Import/TexToolsMeta.cs | 5 ++- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 4 +- Penumbra/Penumbra.cs | 39 +++++--------------- Penumbra/UI/ConfigWindow.cs | 2 +- 5 files changed, 19 insertions(+), 36 deletions(-) diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs index 11a6664b..99a4ff9b 100644 --- a/Penumbra/Import/MetaFileInfo.cs +++ b/Penumbra/Import/MetaFileInfo.cs @@ -1,5 +1,6 @@ using Penumbra.GameData.Enums; using System.Text.RegularExpressions; +using Penumbra.GameData; namespace Penumbra.Import; @@ -52,10 +53,10 @@ public class MetaFileInfo }; } - public MetaFileInfo( string fileName ) + public MetaFileInfo( IGamePathParser parser, string fileName ) { // Set the primary type from the gamePath start. - PrimaryType = Penumbra.GamePathParser.PathToObjectType( fileName ); + PrimaryType = parser.PathToObjectType( fileName ); PrimaryId = 0; SecondaryType = BodySlot.Unknown; SecondaryId = 0; diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 8a8bb193..fd01de02 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using Penumbra.GameData; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -28,7 +29,7 @@ public partial class TexToolsMeta public readonly List< MetaManipulation > MetaManipulations = new(); private readonly bool _keepDefault = false; - public TexToolsMeta( byte[] data, bool keepDefault ) + public TexToolsMeta( IGamePathParser parser, byte[] data, bool keepDefault ) { _keepDefault = keepDefault; try @@ -36,7 +37,7 @@ public partial class TexToolsMeta using var reader = new BinaryReader( new MemoryStream( data ) ); Version = reader.ReadUInt32(); FilePath = ReadNullTerminated( reader ); - var metaInfo = new MetaFileInfo( FilePath ); + var metaInfo = new MetaFileInfo( parser, FilePath ); var numHeaders = reader.ReadUInt32(); var headerSize = reader.ReadUInt32(); var headerStart = reader.ReadUInt32(); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index c089cfda..9e609891 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -204,7 +204,7 @@ public partial class Mod continue; } - var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); + var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); deleteList.Add( file.FullName ); ManipulationData.UnionWith( meta.MetaManipulations ); @@ -288,7 +288,7 @@ public partial class Mod { var x = file.EndsWith( "rgsp" ) ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) - : new TexToolsMeta( data, Penumbra.Config.KeepDefaultMetaChanges ); + : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); meta.UnionWith( x.MetaManipulations ); } catch diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 51204402..c7988881 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -33,16 +33,11 @@ using Penumbra.Interop.Services; namespace Penumbra; -public partial class Penumbra : IDalamudPlugin +public class Penumbra : IDalamudPlugin { public string Name => "Penumbra"; - public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; - - public static readonly string CommitHash = - Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static Logger Log { get; private set; } = null!; public static ChatService ChatService { get; private set; } = null!; public static SaveService SaveService { get; private set; } = null!; @@ -64,8 +59,6 @@ public partial class Penumbra : IDalamudPlugin public static StainService StainService { get; private set; } = null!; // TODO - public static DalamudServices Dalamud { get; private set; } = null!; - public static ValidityChecker ValidityChecker { get; private set; } = null!; public static PerformanceTracker Performance { get; private set; } = null!; @@ -73,21 +66,14 @@ public partial class Penumbra : IDalamudPlugin public readonly PathResolver PathResolver; public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; - public PenumbraApi Api = null!; public HttpApi HttpApi = null!; - public PenumbraIpcProviders IpcProviders = null!; internal ConfigWindow? ConfigWindow { get; private set; } private PenumbraWindowSystem? _windowSystem; - private CommandHandler? _commandHandler; - private readonly ResourceWatcher _resourceWatcher; private bool _disposed; private readonly PenumbraNew _tmp; - public static ItemData ItemData { get; private set; } = null!; - // TODO public static ResourceManagerService ResourceManagerService { get; private set; } = null!; - public static CharacterResolver CharacterResolver { get; private set; } = null!; public static ResourceService ResourceService { get; private set; } = null!; public Penumbra(DalamudPluginInterface pluginInterface) @@ -110,8 +96,6 @@ public partial class Penumbra : IDalamudPlugin Identifier = _tmp.Services.GetRequiredService().AwaitedService; GamePathParser = _tmp.Services.GetRequiredService(); StainService = _tmp.Services.GetRequiredService(); - ItemData = _tmp.Services.GetRequiredService().AwaitedService; - Dalamud = _tmp.Services.GetRequiredService(); TempMods = _tmp.Services.GetRequiredService(); ResidentResources = _tmp.Services.GetRequiredService(); ResourceManagerService = _tmp.Services.GetRequiredService(); @@ -123,14 +107,12 @@ public partial class Penumbra : IDalamudPlugin ResourceService = _tmp.Services.GetRequiredService(); ResourceLoader = _tmp.Services.GetRequiredService(); PathResolver = _tmp.Services.GetRequiredService(); - CharacterResolver = _tmp.Services.GetRequiredService(); - _resourceWatcher = _tmp.Services.GetRequiredService(); SetupInterface(); SetupApi(); ValidityChecker.LogExceptions(); - Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); - OtterTex.NativeDll.Initialize(DalamudServices.PluginInterface.AssemblyLocation.DirectoryName); + Log.Information($"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); if (CharacterUtility.Ready) @@ -146,18 +128,17 @@ public partial class Penumbra : IDalamudPlugin private void SetupApi() { using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); - Api = (PenumbraApi)_tmp.Services.GetRequiredService(); - IpcProviders = _tmp.Services.GetRequiredService(); + var api = _tmp.Services.GetRequiredService(); HttpApi = _tmp.Services.GetRequiredService(); - + _tmp.Services.GetRequiredService(); if (Config.EnableHttpApi) HttpApi.CreateWebServer(); - Api.ChangedItemTooltip += it => + api.ChangedItemTooltip += it => { if (it is Item) ImGui.TextUnformatted("Left Click to create an item link in chat."); }; - Api.ChangedItemClicked += (button, it) => + api.ChangedItemClicked += (button, it) => { if (button == MouseButton.Left && it is Item item) ChatService.LinkItem(item); @@ -170,7 +151,7 @@ public partial class Penumbra : IDalamudPlugin { using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); var system = _tmp.Services.GetRequiredService(); - _commandHandler = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); if (!_disposed) { _windowSystem = system; @@ -237,8 +218,8 @@ public partial class Penumbra : IDalamudPlugin var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); - sb.Append($"> **`Plugin Version: `** {Version}\n"); - sb.Append($"> **`Commit Hash: `** {CommitHash}\n"); + sb.Append($"> **`Plugin Version: `** {ValidityChecker.Version}\n"); + sb.Append($"> **`Commit Hash: `** {ValidityChecker.CommitHash}\n"); sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n"); sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 5036e706..9d4883b1 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -123,7 +123,7 @@ public sealed class ConfigWindow : Window private static string GetLabel(ValidityChecker checker) => checker.Version.Length == 0 ? "Penumbra###PenumbraConfigWindow" - : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; + : $"Penumbra v{checker.Version}###PenumbraConfigWindow"; private void DrawProblemWindow(string text) { From b6d6993c9f80899c9dfcb43518310a14ba30d6cf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 Mar 2023 17:25:18 +0100 Subject: [PATCH 0813/2451] tmp --- Penumbra/Interop/Loader/CharacterResolver.cs | 2 +- ...tsceneCharacters.cs => CutsceneService.cs} | 24 +++++------ .../Resolver/IdentifiedCollectionCache.cs | 40 ++++++++++--------- .../Resolver/PathResolver.PathState.cs | 10 ++++- Penumbra/Interop/Resolver/PathResolver.cs | 24 ++++++++--- Penumbra/PenumbraNew.cs | 11 +++-- Penumbra/Services/Wrappers.cs | 2 +- 7 files changed, 70 insertions(+), 43 deletions(-) rename Penumbra/Interop/Resolver/{CutsceneCharacters.cs => CutsceneService.cs} (78%) diff --git a/Penumbra/Interop/Loader/CharacterResolver.cs b/Penumbra/Interop/Loader/CharacterResolver.cs index 68b83b41..640cc0d9 100644 --- a/Penumbra/Interop/Loader/CharacterResolver.cs +++ b/Penumbra/Interop/Loader/CharacterResolver.cs @@ -76,7 +76,7 @@ public class CharacterResolver : IDisposable _loader.FileLoaded -= ImcLoadResource; } - // Use the default method of path replacement. + /// Use the default method of path replacement. private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) { var resolved = _collectionManager.Default.ResolvePath(path); diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneService.cs similarity index 78% rename from Penumbra/Interop/Resolver/CutsceneCharacters.cs rename to Penumbra/Interop/Resolver/CutsceneService.cs index 06b4f228..4bb8799c 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneService.cs @@ -8,7 +8,7 @@ using Penumbra.Interop.Services; namespace Penumbra.Interop.Resolver; -public class CutsceneCharacters : IDisposable +public class CutsceneService : IDisposable { public const int CutsceneStartIdx = 200; public const int CutsceneSlots = 40; @@ -23,7 +23,7 @@ public class CutsceneCharacters : IDisposable .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public CutsceneCharacters(ObjectTable objects, GameEventManager events) + public CutsceneService(ObjectTable objects, GameEventManager events) { _objects = objects; _events = events; @@ -69,19 +69,19 @@ public class CutsceneCharacters : IDisposable private unsafe void OnCharacterDestructor(Character* character) { - if (character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) - { - var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[idx] = -1; - } + if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) + return; + + var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = -1; } private unsafe void OnCharacterCopy(Character* target, Character* source) { - if (target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx) - { - var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); - } + if (target == null || target->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) + return; + + var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } } diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index fa09f64d..83739dca 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -1,27 +1,31 @@ using System; using System.Collections; using System.Collections.Generic; +using Dalamud.Game.ClientState; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; using Penumbra.GameData.Actors; -using Penumbra.Interop.Services; +using Penumbra.Interop.Services; using Penumbra.Services; namespace Penumbra.Interop.Resolver; -public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> { private readonly CommunicatorService _communicator; private readonly GameEventManager _events; - private readonly Dictionary _cache = new(317); - private bool _dirty = false; - private bool _enabled = false; + private readonly ClientState _clientState; + private readonly Dictionary _cache = new(317); + private bool _dirty; + private bool _enabled; - public IdentifiedCollectionCache(CommunicatorService communicator, GameEventManager events) + public IdentifiedCollectionCache(ClientState clientState, CommunicatorService communicator, GameEventManager events) { + _clientState = clientState; _communicator = communicator; _events = events; + Enable(); } public void Enable() @@ -29,10 +33,10 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr if (_enabled) return; - _communicator.CollectionChange.Event += CollectionChangeClear; - DalamudServices.SClientState.TerritoryChanged += TerritoryClear; - _events.CharacterDestructor += OnCharacterDestruct; - _enabled = true; + _communicator.CollectionChange.Event += CollectionChangeClear; + _clientState.TerritoryChanged += TerritoryClear; + _events.CharacterDestructor += OnCharacterDestruct; + _enabled = true; } public void Disable() @@ -40,10 +44,10 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr if (!_enabled) return; - _communicator.CollectionChange.Event -= CollectionChangeClear; - DalamudServices.SClientState.TerritoryChanged -= TerritoryClear; - _events.CharacterDestructor -= OnCharacterDestruct; - _enabled = false; + _communicator.CollectionChange.Event -= CollectionChangeClear; + _clientState.TerritoryChanged -= TerritoryClear; + _events.CharacterDestructor -= OnCharacterDestruct; + _enabled = false; } public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) @@ -54,7 +58,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr _cache.Clear(); } - _cache[(IntPtr)data] = (identifier, collection); + _cache[(nint)data] = (identifier, collection); return collection.ToResolveData(data); } @@ -65,7 +69,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr _dirty = false; _cache.Clear(); } - else if (_cache.TryGetValue((IntPtr)gameObject, out var p)) + else if (_cache.TryGetValue((nint)gameObject, out var p)) { resolve = p.Item2.ToResolveData(gameObject); return true; @@ -81,7 +85,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr GC.SuppressFinalize(this); } - public IEnumerator<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() + public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() { foreach (var (address, (identifier, collection)) in _cache) { @@ -108,5 +112,5 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr => _dirty = _cache.Count > 0; private void OnCharacterDestruct(Character* character) - => _cache.Remove((IntPtr)character); + => _cache.Remove((nint)character); } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 2b4aceca..1cf2b206 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData; @@ -33,6 +34,8 @@ public unsafe partial class PathResolver // This map links files to their corresponding collection, if it is non-default. private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); + private readonly ThreadLocal _resolveData = new ThreadLocal(() => ResolveData.Invalid, true); + public PathState( PathResolver parent ) { SignatureHelper.Initialise( this ); @@ -60,6 +63,7 @@ public unsafe partial class PathResolver public void Dispose() { + _resolveData.Dispose(); _human.Dispose(); _weapon.Dispose(); _demiHuman.Dispose(); @@ -76,7 +80,10 @@ public unsafe partial class PathResolver => _pathCollections.TryGetValue( path, out collection ); public bool Consume( ByteString path, out ResolveData collection ) - => _pathCollections.TryRemove( path, out collection ); + { + collection = _resolveData.IsValueCreated && _resolveData.Value.Valid ? _resolveData.Value : ResolveData.Invalid; + return _pathCollections.TryRemove(path, out collection); + } // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] @@ -89,6 +96,7 @@ public unsafe partial class PathResolver var gamePath = new ByteString( ( byte* )path ); SetCollection( gameObject, gamePath, collection ); + _resolveData.Value = collection.ToResolveData(gameObject); return path; } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 059a0550..3b805acd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,16 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; +using Dalamud.Game.ClientState; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; @@ -18,6 +16,20 @@ using Penumbra.Util; namespace Penumbra.Interop.Resolver; +//public class PathResolver2 : IDisposable +//{ +// public readonly CutsceneService Cutscenes; +// public readonly IdentifiedCollectionCache Identified; +// +// public PathResolver(StartTracker timer, CutsceneService cutscenes, IdentifiedCollectionCache identified) +// { +// using var t = timer.Measure(StartTimeType.PathResolver); +// Cutscenes = cutscenes; +// Identified = identified; +// } +//} + + // The Path Resolver handles character collections. // It will hook any path resolving functions for humans, // as well as DrawObject creation. @@ -29,7 +41,7 @@ public partial class PathResolver : IDisposable private readonly CommunicatorService _communicator; private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO + private static readonly CutsceneService Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO private static DrawObjectState _drawObjects = null!; // TODO private static readonly BitArray ValidHumanModels; internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO @@ -41,11 +53,11 @@ public partial class PathResolver : IDisposable static PathResolver() => ValidHumanModels = GetValidHumanModels(DalamudServices.SGameData); - public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) + public unsafe PathResolver(IdentifiedCollectionCache cache, StartTracker timer, ClientState clientState, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) { using var tApi = timer.Measure(StartTimeType.PathResolver); _communicator = communicator; - IdentifiedCache = new IdentifiedCollectionCache(communicator, events); + IdentifiedCache = cache; SignatureHelper.Initialise(this); _drawObjects = new DrawObjectState(_communicator); _loader = loader; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 3d52c6be..c74af610 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -15,7 +15,6 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; -using Penumbra.UI.Classes; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; @@ -60,12 +59,12 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton(); - + // Add Game Services services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -74,7 +73,11 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); + + // Add PathResolver + services.AddSingleton() + .AddSingleton(); // Add Configuration services.AddTransient() diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index e0ab8aae..131507cc 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -29,7 +29,7 @@ public sealed class ItemService : AsyncServiceWrapper public sealed class ActorService : AsyncServiceWrapper { public ActorService(StartTracker tracker, DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, - Framework framework, DataManager gameData, GameGui gui, CutsceneCharacters cutscene) + Framework framework, DataManager gameData, GameGui gui, CutsceneService cutscene) : base(nameof(ActorService), tracker, StartTimeType.Actors, () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) { } From 045c84512feabbe5d8ce6e45d6456f5afe847cb5 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 23 Mar 2023 02:52:51 +0100 Subject: [PATCH 0814/2451] On-Screen resource tree & quick import --- Penumbra/Interop/ResourceTree.cs | 613 ++++++++++++++++++ Penumbra/Interop/Structs/HumanExt.cs | 17 + Penumbra/Interop/Structs/Material.cs | 10 +- Penumbra/Interop/Structs/ResourceHandle.cs | 5 +- Penumbra/Interop/Structs/WeaponExt.cs | 14 + .../UI/Classes/ModEditWindow.FileEditor.cs | 38 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 1 + .../UI/Classes/ModEditWindow.QuickImport.cs | 352 ++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 7 +- Penumbra/UI/ConfigWindow.OnScreenTab.cs | 150 +++++ Penumbra/UI/ConfigWindow.cs | 5 +- 11 files changed, 1194 insertions(+), 18 deletions(-) create mode 100644 Penumbra/Interop/ResourceTree.cs create mode 100644 Penumbra/Interop/Structs/HumanExt.cs create mode 100644 Penumbra/Interop/Structs/WeaponExt.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.QuickImport.cs create mode 100644 Penumbra/UI/ConfigWindow.OnScreenTab.cs diff --git a/Penumbra/Interop/ResourceTree.cs b/Penumbra/Interop/ResourceTree.cs new file mode 100644 index 00000000..31c84cd6 --- /dev/null +++ b/Penumbra/Interop/ResourceTree.cs @@ -0,0 +1,613 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Game.ClientState.Objects.SubKinds; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.Interop.Resolver; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using Objects = Dalamud.Game.ClientState.Objects.Types; + +namespace Penumbra.Interop; + +public class ResourceTree +{ + public readonly string Name; + public readonly nint SourceAddress; + public readonly string CollectionName; + public readonly List Nodes; + + public ResourceTree( string name, nint sourceAddress, string collectionName ) + { + Name = name; + SourceAddress = sourceAddress; + CollectionName = collectionName; + Nodes = new(); + } + + public static ResourceTree[] FromObjectTable( bool withNames = true ) + { + var cache = new FileCache(); + + return Dalamud.Objects + .OfType() + .Select( chara => FromCharacter( chara, cache, withNames ) ) + .OfType() + .ToArray(); + } + + public static IEnumerable<(Objects.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, bool withNames = true ) + { + var cache = new FileCache(); + foreach( var chara in characters ) + { + var tree = FromCharacter( chara, cache, withNames ); + if( tree != null ) + { + yield return (chara, tree); + } + } + } + + public static unsafe ResourceTree? FromCharacter( Objects.Character chara, bool withNames = true ) + { + return FromCharacter( chara, new FileCache(), withNames ); + } + + private static unsafe ResourceTree? FromCharacter( Objects.Character chara, FileCache cache, bool withNames = true ) + { + var charaStruct = ( Character* )chara.Address; + var gameObjStruct = &charaStruct->GameObject; + var model = ( CharacterBase* )gameObjStruct->GetDrawObject(); + if( model == null ) + { + return null; + } + + var equipment = new ReadOnlySpan( charaStruct->EquipSlotData, 10 ); + // var customize = new ReadOnlySpan( charaStruct->CustomizeData, 26 ); + + var collectionResolveData = PathResolver.IdentifyCollection( gameObjStruct, true ); + if( !collectionResolveData.Valid ) + { + return null; + } + var collection = collectionResolveData.ModCollection; + + var tree = new ResourceTree( chara.Name.ToString(), new nint( charaStruct ), collection.Name ); + + var globalContext = new GlobalResolveContext( + FileCache: cache, + Collection: collection, + Skeleton: charaStruct->ModelSkeletonId, + WithNames: withNames + ); + + for( var i = 0; i < model->SlotCount; ++i ) + { + var context = globalContext.CreateContext( + Slot: ( i < equipment.Length ) ? ( ( uint )i ).ToEquipSlot() : EquipSlot.Unknown, + Equipment: ( i < equipment.Length ) ? equipment[i] : default + ); + + var imc = ( ResourceHandle* )model->IMCArray[i]; + var imcNode = context.CreateNodeFromImc( imc ); + if( imcNode != null ) + { + tree.Nodes.Add( withNames ? imcNode.WithName( imcNode.Name ?? $"IMC #{i}" ) : imcNode ); + } + + var mdl = ( RenderModel* )model->ModelArray[i]; + var mdlNode = context.CreateNodeFromRenderModel( mdl ); + if( mdlNode != null ) + { + tree.Nodes.Add( withNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode ); + } + } + + if( chara is PlayerCharacter ) + { + AddHumanResources( tree, globalContext, ( HumanExt* )model ); + } + + return tree; + } + + private static unsafe void AddHumanResources( ResourceTree tree, GlobalResolveContext globalContext, HumanExt* human ) + { + var prependIndex = 0; + + var firstWeapon = ( WeaponExt* )human->Human.CharacterBase.DrawObject.Object.ChildObject; + if( firstWeapon != null ) + { + var weapon = firstWeapon; + var weaponIndex = 0; + do + { + var weaponContext = globalContext.CreateContext( + Slot: EquipSlot.MainHand, + Equipment: new EquipmentRecord( weapon->Weapon.ModelSetId, ( byte )weapon->Weapon.Variant, ( byte )weapon->Weapon.ModelUnknown ) + ); + + var weaponMdlNode = weaponContext.CreateNodeFromRenderModel( *weapon->WeaponRenderModel ); + if( weaponMdlNode != null ) + { + tree.Nodes.Insert( prependIndex++, globalContext.WithNames ? weaponMdlNode.WithName( weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}" ) : weaponMdlNode ); + } + + weapon = ( WeaponExt* )weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject; + ++weaponIndex; + } while( weapon != null && weapon != firstWeapon ); + } + + var context = globalContext.CreateContext( + Slot: EquipSlot.Unknown, + Equipment: default + ); + + var skeletonNode = context.CreateHumanSkeletonNode( human->Human.RaceSexId ); + if( skeletonNode != null ) + { + tree.Nodes.Add( globalContext.WithNames ? skeletonNode.WithName( skeletonNode.Name ?? "Skeleton" ) : skeletonNode ); + } + + var decalNode = context.CreateNodeFromTex( human->Decal ); + if( decalNode != null ) + { + tree.Nodes.Add( globalContext.WithNames ? decalNode.WithName( decalNode.Name ?? "Face Decal" ) : decalNode ); + } + + var legacyDecalNode = context.CreateNodeFromTex( human->LegacyBodyDecal ); + if( legacyDecalNode != null ) + { + tree.Nodes.Add( globalContext.WithNames ? legacyDecalNode.WithName( legacyDecalNode.Name ?? "Legacy Body Decal" ) : legacyDecalNode ); + } + } + + private static unsafe bool CreateOwnedGamePath( byte* rawGamePath, out Utf8GamePath gamePath, bool addDx11Prefix = false, bool isShader = false ) + { + if( rawGamePath == null ) + { + gamePath = default; + return false; + } + + if( isShader ) + { + var path = $"shader/sm5/shpk/{new ByteString( rawGamePath )}"; + return Utf8GamePath.FromString( path, out gamePath ); + } + + if( addDx11Prefix ) + { + var unprefixed = MemoryMarshal.CreateReadOnlySpanFromNullTerminated( rawGamePath ); + var lastDirectorySeparator = unprefixed.LastIndexOf( ( byte )'/' ); + if( unprefixed[lastDirectorySeparator + 1] != ( byte )'-' || unprefixed[lastDirectorySeparator + 2] != ( byte )'-' ) + { + Span prefixed = stackalloc byte[unprefixed.Length + 2]; + unprefixed[..( lastDirectorySeparator + 1 )].CopyTo( prefixed ); + prefixed[lastDirectorySeparator + 1] = ( byte )'-'; + prefixed[lastDirectorySeparator + 2] = ( byte )'-'; + unprefixed[( lastDirectorySeparator + 1 )..].CopyTo( prefixed[( lastDirectorySeparator + 3 )..] ); + + if( !Utf8GamePath.FromSpan( prefixed, out gamePath ) ) + { + return false; + } + + gamePath = gamePath.Clone(); + return true; + } + } + + if( !Utf8GamePath.FromPointer( rawGamePath, out gamePath ) ) + { + return false; + } + + gamePath = gamePath.Clone(); + return true; + } + + [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 4 )] + private readonly record struct EquipmentRecord( ushort SetId, byte Variant, byte Dye ); + + private class FileCache + { + private readonly Dictionary Materials = new(); + private readonly Dictionary ShaderPackages = new(); + + public MtrlFile? ReadMaterial( FullPath path ) + { + return ReadFile( path, Materials, bytes => new MtrlFile( bytes ) ); + } + + public ShpkFile? ReadShaderPackage( FullPath path ) + { + return ReadFile( path, ShaderPackages, bytes => new ShpkFile( bytes ) ); + } + + private static T? ReadFile( FullPath path, Dictionary cache, Func parseFile ) where T : class + { + if( path.FullName.Length == 0 ) + { + return null; + } + + if( cache.TryGetValue( path, out var cached ) ) + { + return cached; + } + + var pathStr = path.ToPath(); + T? parsed; + try + { + if( path.IsRooted ) + { + parsed = parseFile( File.ReadAllBytes( pathStr ) ); + } + else + { + var bytes = Dalamud.GameData.GetFile( pathStr )?.Data; + parsed = ( bytes != null ) ? parseFile( bytes ) : null; + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not read file {pathStr}:\n{e}" ); + parsed = null; + } + cache.Add( path, parsed ); + + return parsed; + } + } + + private record class GlobalResolveContext( FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames ) + { + public ResolveContext CreateContext( EquipSlot Slot, EquipmentRecord Equipment ) + => new( FileCache, Collection, Skeleton, WithNames, Slot, Equipment ); + } + + private record class ResolveContext( FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, EquipmentRecord Equipment ) + { + private unsafe Node? CreateNodeFromGamePath( ResourceType type, nint sourceAddress, byte* rawGamePath, bool @internal, bool addDx11Prefix = false, bool isShader = false ) + { + if( !CreateOwnedGamePath( rawGamePath, out var gamePath, addDx11Prefix, isShader ) ) + { + return null; + } + + return CreateNodeFromGamePath( type, sourceAddress, gamePath, @internal ); + } + + private unsafe Node CreateNodeFromGamePath( ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal ) + => new( null, type, sourceAddress, gamePath, FilterFullPath( Collection.ResolvePath( gamePath ) ?? new FullPath( gamePath ) ), @internal ); + + private unsafe Node? CreateNodeFromResourceHandle( ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, bool withName ) + { + if( handle == null ) + { + return null; + } + + var name = handle->FileNameAsSpan(); + if( name.Length == 0 ) + { + return null; + } + + if( name[0] == ( byte )'|' ) + { + name = name[1..]; + var pos = name.IndexOf( ( byte )'|' ); + if( pos < 0 ) + { + return null; + } + name = name[( pos + 1 )..]; + } + + var fullPath = new FullPath( Encoding.UTF8.GetString( name ) ); + var gamePaths = Collection.ReverseResolvePath( fullPath ).ToList(); + fullPath = FilterFullPath( fullPath ); + + if( gamePaths.Count > 1 ) + { + gamePaths = Filter( gamePaths ); + } + + if( gamePaths.Count == 1 ) + { + return new Node( withName ? GuessNameFromPath( gamePaths[0] ) : null, type, sourceAddress, gamePaths[0], fullPath, @internal ); + } + else + { + Penumbra.Log.Information( $"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:" ); + foreach( var gamePath in gamePaths ) + { + Penumbra.Log.Information( $"Game path: {gamePath}" ); + } + + return new Node( null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal ); + } + } + + public unsafe Node? CreateHumanSkeletonNode( ushort raceSexId ) + { + var raceSexIdStr = raceSexId.ToString( "D4" ); + var path = $"chara/human/c{raceSexIdStr}/skeleton/base/b0001/skl_c{raceSexIdStr}b0001.sklb"; + + if( !Utf8GamePath.FromString( path, out var gamePath ) ) + { + return null; + } + + return CreateNodeFromGamePath( ResourceType.Sklb, 0, gamePath, false ); + } + + public unsafe Node? CreateNodeFromImc( ResourceHandle* imc ) + { + var node = CreateNodeFromResourceHandle( ResourceType.Imc, new nint( imc ), imc, true, false ); + if( node == null ) + { + return null; + } + if( WithNames ) + { + var name = GuessModelName( node.GamePath ); + node = node.WithName( ( name != null ) ? $"IMC: {name}" : null ); + } + + return node; + } + + public unsafe Node? CreateNodeFromTex( ResourceHandle* tex ) + { + return CreateNodeFromResourceHandle( ResourceType.Tex, new nint( tex ), tex, false, WithNames ); + } + + public unsafe Node? CreateNodeFromRenderModel( RenderModel* mdl ) + { + if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + { + return null; + } + + var node = CreateNodeFromResourceHandle( ResourceType.Mdl, new nint( mdl ), mdl->ResourceHandle, false, false ); + if( node == null ) + { + return null; + } + if( WithNames ) + { + node = node.WithName( GuessModelName( node.GamePath ) ); + } + + for( var i = 0; i < mdl->MaterialCount; i++ ) + { + var mtrl = ( Material* )mdl->Materials[i]; + var mtrlNode = CreateNodeFromMaterial( mtrl ); + if( mtrlNode != null ) + { + // Don't keep the material's name if it's redundant with the model's name. + node.Children.Add( WithNames ? mtrlNode.WithName( ( string.Equals( mtrlNode.Name, node.Name, StringComparison.Ordinal ) ? null : mtrlNode.Name ) ?? $"Material #{i}" ) : mtrlNode ); + } + } + + return node; + } + + private unsafe Node? CreateNodeFromMaterial( Material* mtrl ) + { + if( mtrl == null ) + { + return null; + } + + var resource = ( MtrlResource* )mtrl->ResourceHandle; + var node = CreateNodeFromResourceHandle( ResourceType.Mtrl, new nint( mtrl ), &resource->Handle, false, WithNames ); + if( node == null ) + { + return null; + } + var mtrlFile = WithNames ? FileCache.ReadMaterial( node.FullPath ) : null; + + var shpkNode = CreateNodeFromGamePath( ResourceType.Shpk, 0, resource->ShpkString, false, isShader: true ); + if( shpkNode != null ) + { + node.Children.Add( WithNames ? shpkNode.WithName( "Shader Package" ) : shpkNode ); + } + var shpkFile = ( WithNames && shpkNode != null ) ? FileCache.ReadShaderPackage( shpkNode.FullPath ) : null; + var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; + + for( var i = 0; i < resource->NumTex; i++ ) + { + var texNode = CreateNodeFromGamePath( ResourceType.Tex, 0, resource->TexString( i ), false, addDx11Prefix: resource->TexIsDX11( i ) ); + if( texNode != null ) + { + if( WithNames ) + { + var name = ( samplers != null && i < samplers.Count ) ? samplers[i].Item2?.Name : null; + node.Children.Add( texNode.WithName( name ?? $"Texture #{i}" ) ); + } + else + { + node.Children.Add( texNode ); + } + } + } + + return node; + } + + private static FullPath FilterFullPath( FullPath fullPath ) + { + if( !fullPath.IsRooted ) + { + return fullPath; + } + + var relPath = Path.GetRelativePath( Penumbra.Config.ModDirectory, fullPath.FullName ); + if( relPath == "." || !relPath.StartsWith( '.' ) && !Path.IsPathRooted( relPath ) ) + { + return fullPath.Exists ? fullPath : FullPath.Empty; + } + return FullPath.Empty; + } + + private List Filter( List gamePaths ) + { + var filtered = new List( gamePaths.Count ); + foreach( var path in gamePaths ) + { + // In doubt, keep the paths. + if( IsMatch( path.ToString().Split( new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries ) ) ?? true ) + { + filtered.Add( path ); + } + } + + return filtered; + } + + private bool? IsMatch( ReadOnlySpan path ) + => SafeGet( path, 0 ) switch + { + "chara" => SafeGet( path, 1 ) switch + { + "accessory" => IsMatchEquipment( path[2..], $"a{Equipment.SetId:D4}" ), + "equipment" => IsMatchEquipment( path[2..], $"e{Equipment.SetId:D4}" ), + "monster" => SafeGet( path, 2 ) == $"m{Skeleton:D4}", + "weapon" => IsMatchEquipment( path[2..], $"w{Equipment.SetId:D4}" ), + _ => null, + }, + _ => null, + }; + + private bool? IsMatchEquipment( ReadOnlySpan path, string equipmentDir ) + => SafeGet( path, 0 ) == equipmentDir + ? SafeGet( path, 1 ) switch + { + "material" => SafeGet( path, 2 ) == $"v{Equipment.Variant:D4}", + _ => null, + } + : false; + + private string? GuessModelName( Utf8GamePath gamePath ) + { + var path = gamePath.ToString().Split( new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); + // Weapons intentionally left out. + var isEquipment = SafeGet( path, 0 ) == "chara" && SafeGet( path, 1 ) is "accessory" or "equipment"; + if( isEquipment ) + { + foreach( var item in Penumbra.Identifier.Identify( Equipment.SetId, Equipment.Variant, Slot.ToSlot() ) ) + { + return Slot switch + { + EquipSlot.RFinger => "R: ", + EquipSlot.LFinger => "L: ", + _ => string.Empty, + } + item.Name.ToString(); + } + } + var nameFromPath = GuessNameFromPath( gamePath ); + if( nameFromPath != null ) + { + return nameFromPath; + } + if( isEquipment ) + { + return Slot.ToName(); + } + + return null; + } + + private static string? GuessNameFromPath( Utf8GamePath gamePath ) + { + foreach( var obj in Penumbra.Identifier.Identify( gamePath.ToString() ) ) + { + var name = obj.Key; + if( name.StartsWith( "Customization:" ) ) + { + name = name[14..].Trim(); + } + if( name != "Unknown" ) + { + return name; + } + } + + return null; + } + + private static string? SafeGet( ReadOnlySpan array, Index index ) + { + var i = index.GetOffset( array.Length ); + return ( i >= 0 && i < array.Length ) ? array[i] : null; + } + } + + public class Node + { + public readonly string? Name; + public readonly ResourceType Type; + public readonly nint SourceAddress; + public readonly Utf8GamePath GamePath; + public readonly Utf8GamePath[] PossibleGamePaths; + public readonly FullPath FullPath; + public readonly bool Internal; + public readonly List Children; + + public Node( string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal ) + { + Name = name; + Type = type; + SourceAddress = sourceAddress; + GamePath = gamePath; + PossibleGamePaths = new[] { gamePath }; + FullPath = fullPath; + Internal = @internal; + Children = new(); + } + + public Node( string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, bool @internal ) + { + Name = name; + Type = type; + SourceAddress = sourceAddress; + GamePath = ( possibleGamePaths.Length == 1 ) ? possibleGamePaths[0] : Utf8GamePath.Empty; + PossibleGamePaths = possibleGamePaths; + FullPath = fullPath; + Internal = @internal; + Children = new(); + } + + private Node( string? name, Node originalNode ) + { + Name = name; + Type = originalNode.Type; + SourceAddress = originalNode.SourceAddress; + GamePath = originalNode.GamePath; + PossibleGamePaths = originalNode.PossibleGamePaths; + FullPath = originalNode.FullPath; + Internal = originalNode.Internal; + Children = originalNode.Children; + } + + public Node WithName( string? name ) + => string.Equals( Name, name, StringComparison.Ordinal ) ? this : new Node( name, this ); + } +} diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs new file mode 100644 index 00000000..7af5cee4 --- /dev/null +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct HumanExt +{ + [FieldOffset( 0x0 )] + public Human Human; + + [FieldOffset( 0x9E8 )] + public ResourceHandle* Decal; + + [FieldOffset( 0x9F0 )] + public ResourceHandle* LegacyBodyDecal; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index dbd7c2b0..7cee271e 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -12,12 +12,8 @@ public unsafe struct Material [FieldOffset( 0x28 )] public void* MaterialData; - [FieldOffset( 0x48 )] - public Texture* Tex1; + [FieldOffset( 0x30 )] + public void** Textures; - [FieldOffset( 0x60 )] - public Texture* Tex2; - - [FieldOffset( 0x78 )] - public Texture* Tex3; + public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1]; } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 77c212ef..4de81903 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -58,8 +58,11 @@ public unsafe struct ResourceHandle public ByteString FileName() => ByteString.FromByteStringUnsafe( FileNamePtr(), FileNameLength, true ); + public ReadOnlySpan< byte > FileNameAsSpan() + => new( FileNamePtr(), FileNameLength ); + public bool GamePath( out Utf8GamePath path ) - => Utf8GamePath.FromSpan( new ReadOnlySpan< byte >( FileNamePtr(), FileNameLength ), out path ); + => Utf8GamePath.FromSpan( FileNameAsSpan(), out path ); [FieldOffset( 0x00 )] public void** VTable; diff --git a/Penumbra/Interop/Structs/WeaponExt.cs b/Penumbra/Interop/Structs/WeaponExt.cs new file mode 100644 index 00000000..de7038d7 --- /dev/null +++ b/Penumbra/Interop/Structs/WeaponExt.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct WeaponExt +{ + [FieldOffset( 0x0 )] + public Weapon Weapon; + + [FieldOffset( 0xA8 )] + public RenderModel** WeaponRenderModel; +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 19630beb..4e283773 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using System.Reflection; using Dalamud.Interface; @@ -18,6 +19,7 @@ public partial class ModEditWindow { private class FileEditor< T > where T : class, IWritable { + private readonly ModEditWindow _owner; private readonly string _tabName; private readonly string _fileType; private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; @@ -30,18 +32,23 @@ public partial class ModEditWindow private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; + private string _defaultPath = string.Empty; + private bool _inInput; + private Utf8GamePath _defaultPathUtf8; + private bool _isDefaultPathUtf8Valid; + private T? _defaultFile; + private Exception? _defaultException; + + private QuickImportAction? _quickImport; private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); - public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, + public FileEditor( ModEditWindow owner, string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile ) { + _owner = owner; _tabName = tabName; _fileType = fileType; _getFiles = getFiles; @@ -56,6 +63,7 @@ public partial class ModEditWindow using var tab = ImRaii.TabItem( _tabName ); if( !tab ) { + _quickImport = null; return; } @@ -74,11 +82,13 @@ public partial class ModEditWindow private void DefaultInput() { using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 2 * (3 * ImGuiHelpers.GlobalScale + ImGui.GetFrameHeight() ) ); ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); _inInput = ImGui.IsItemActive(); if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) { + _isDefaultPathUtf8Valid = Utf8GamePath.FromString( _defaultPath, out _defaultPathUtf8, true ); + _quickImport = null; _fileDialog.Reset(); try { @@ -122,6 +132,22 @@ public partial class ModEditWindow }, _getInitialPath() ); } + _quickImport ??= QuickImportAction.Prepare( _owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile ); + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true ) ) + { + try + { + UpdateCurrentFile( _quickImport.Execute() ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}" ); + } + _quickImport = null; + } + _fileDialog.Draw(); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 18f17711..d7bd57d4 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; diff --git a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs new file mode 100644 index 00000000..f300bd34 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using ImGuiNET; +using Lumina.Data; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Interop; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private ResourceTree[]? _quickImportTrees; + private HashSet? _quickImportUnfolded; + private Dictionary? _quickImportWritables; + private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; + + private readonly FileDialogManager _quickImportFileDialog = ConfigWindow.SetupFileManager(); + + private void DrawQuickImportTab() + { + using var tab = ImRaii.TabItem( "Import from Screen" ); + if( !tab ) + { + _quickImportActions = null; + return; + } + + _quickImportUnfolded ??= new(); + _quickImportWritables ??= new(); + _quickImportActions ??= new(); + + if( ImGui.Button( "Refresh Character List" ) ) + { + try + { + _quickImportTrees = ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" ); + _quickImportTrees = Array.Empty(); + } + _quickImportUnfolded.Clear(); + _quickImportWritables.Clear(); + _quickImportActions.Clear(); + } + + try + { + _quickImportTrees ??= ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" ); + _quickImportTrees ??= Array.Empty(); + } + + foreach( var (tree, index) in _quickImportTrees.WithIndex() ) + { + if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + { + continue; + } + using var id = ImRaii.PushId( index ); + + ImGui.Text( $"Collection: {tree.CollectionName}" ); + + using var table = ImRaii.Table( "##ResourceTree", 4, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + continue; + } + + ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f ); + ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f ); + ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); + ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthFixed, 3 * ImGuiHelpers.GlobalScale + 2 * ImGui.GetFrameHeight() ); + ImGui.TableHeadersRow(); + + DrawQuickImportNodes( tree.Nodes, 0 ); + } + + _quickImportFileDialog.Draw(); + } + + private void DrawQuickImportNodes( IEnumerable resourceNodes, int level ) + { + var debugMode = Penumbra.Config.DebugMode; + var frameHeight = ImGui.GetFrameHeight(); + foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) + { + if( resourceNode.Internal && !debugMode ) + { + continue; + } + using var id = ImRaii.PushId( index ); + ImGui.TableNextColumn(); + var unfolded = _quickImportUnfolded!.Contains( resourceNode ); + using( var indent = ImRaii.PushIndent( level ) ) + { + ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); + if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) + { + if( unfolded ) + { + _quickImportUnfolded.Remove( resourceNode ); + } + else + { + _quickImportUnfolded.Add( resourceNode ); + } + unfolded = !unfolded; + } + if( debugMode ) + { + ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString("X" + nint.Size * 2)}" ); + } + } + ImGui.TableNextColumn(); + var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; + ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch + { + 0 => "(none)", + 1 => resourceNode.GamePath.ToString(), + _ => "(multiple)", + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + if( hasGamePaths ) + { + var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( allPaths ); + } + ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); + } + ImGui.TableNextColumn(); + var hasFullPath = resourceNode.FullPath.FullName.Length > 0; + if( hasFullPath ) + { + ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); + } + ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); + } + else + { + ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); + } + ImGui.TableNextColumn(); + using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) ) + { + if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) ) + { + var path = resourceNode.FullPath.ToPath(); + if( resourceNode.FullPath.IsRooted ) + { + writable = new RawFileWritable( path ); + } + else + { + var file = Dalamud.GameData.GetFile( path ); + writable = ( file == null ) ? null : new RawGameFileWritable( file ); + } + _quickImportWritables.Add( resourceNode.FullPath, writable ); + } + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( frameHeight ), "Export this file.", !hasFullPath || writable == null, true ) ) + { + var fullPathStr = resourceNode.FullPath.FullName; + var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr ); + _quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, writable!.Write() ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" ); + } + } ); + } + ImGui.SameLine(); + if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) ) + { + quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable ); + _quickImportActions.Add( (resourceNode.GamePath, writable), quickImport ); + } + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), new Vector2( frameHeight ), $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) ) + { + quickImport.Execute(); + _quickImportActions.Remove( (resourceNode.GamePath, writable) ); + } + } + if( unfolded ) + { + DrawQuickImportNodes( resourceNode.Children, level + 1 ); + } + } + } + + private record class RawFileWritable( string Path ) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => File.ReadAllBytes( Path ); + } + + private record class RawGameFileWritable( FileResource FileResource ) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => FileResource.Data; + } + + private class QuickImportAction + { + public const string FallbackOptionName = "the current option"; + + private readonly string _optionName; + private readonly Utf8GamePath _gamePath; + private readonly Mod.Editor? _editor; + private readonly IWritable? _file; + private readonly string? _targetPath; + private readonly int _subDirs; + + public string OptionName => _optionName; + public Utf8GamePath GamePath => _gamePath; + public bool CanExecute => !_gamePath.IsEmpty && _editor != null && _file != null && _targetPath != null; + + /// + /// Creates a non-executable QuickImportAction. + /// + private QuickImportAction( string optionName, Utf8GamePath gamePath ) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = null; + _file = null; + _targetPath = null; + _subDirs = 0; + } + + /// + /// Creates an executable QuickImportAction. + /// + private QuickImportAction( string optionName, Utf8GamePath gamePath, Mod.Editor editor, IWritable file, string targetPath, int subDirs ) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = editor; + _file = file; + _targetPath = targetPath; + _subDirs = subDirs; + } + + public static QuickImportAction Prepare( ModEditWindow owner, Utf8GamePath gamePath, IWritable? file ) + { + var editor = owner._editor; + if( editor == null ) + { + return new QuickImportAction( FallbackOptionName, gamePath ); + } + var subMod = editor.CurrentOption; + var optionName = subMod.FullName; + if( gamePath.IsEmpty || file == null || editor.FileChanges ) + { + return new QuickImportAction( optionName, gamePath ); + } + if( subMod.Files.ContainsKey( gamePath ) || subMod.FileSwaps.ContainsKey( gamePath ) ) + { + return new QuickImportAction( optionName, gamePath ); + } + var mod = owner._mod; + if( mod == null ) + { + return new QuickImportAction( optionName, gamePath ); + } + var ( preferredPath, subDirs ) = GetPreferredPath( mod, subMod ); + var targetPath = new FullPath( Path.Combine( preferredPath.FullName, gamePath.ToString() ) ).FullName; + if( File.Exists( targetPath ) ) + { + return new QuickImportAction( optionName, gamePath ); + } + + return new QuickImportAction( optionName, gamePath, editor, file, targetPath, subDirs ); + } + + public Mod.Editor.FileRegistry Execute() + { + if( !CanExecute ) + { + throw new InvalidOperationException(); + } + var directory = Path.GetDirectoryName( _targetPath ); + if( directory != null ) + { + Directory.CreateDirectory( directory ); + } + File.WriteAllBytes( _targetPath!, _file!.Write() ); + _editor!.RevertFiles(); + var fileRegistry = _editor.AvailableFiles.First( file => file.File.FullName == _targetPath ); + _editor.AddPathsToSelected( new Mod.Editor.FileRegistry[] { fileRegistry }, _subDirs ); + _editor.ApplyFiles(); + + return fileRegistry; + } + + private static (DirectoryInfo, int) GetPreferredPath( Mod mod, ISubMod subMod ) + { + var path = mod.ModPath; + var subDirs = 0; + if( subMod != mod.Default ) + { + var name = subMod.Name; + var fullName = subMod.FullName; + if( fullName.EndsWith( ": " + name ) ) + { + path = Mod.Creator.NewOptionDirectory( path, fullName[..^( name.Length + 2 )] ); + path = Mod.Creator.NewOptionDirectory( path, name ); + subDirs = 2; + } + else + { + path = Mod.Creator.NewOptionDirectory( path, fullName ); + subDirs = 1; + } + } + + return (path, subDirs); + } + } +} diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 3fe48126..83b5d103 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -153,6 +153,7 @@ public partial class ModEditWindow : Window, IDisposable DrawSwapTab(); DrawMissingFilesTab(); DrawDuplicatesTab(); + DrawQuickImportTab(); DrawMaterialReassignmentTab(); _modelTab.Draw(); _materialTab.Draw(); @@ -570,17 +571,17 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) { - _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", + _materialTab = new FileEditor< MtrlTab >( this, "Materials", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab( this, new MtrlFile( bytes ) ) ); - _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", + _modelTab = new FileEditor< MdlFile >( this, "Models", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null ); - _shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor< ShpkTab >( this, "Shader Packages", ".shpk", () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/ConfigWindow.OnScreenTab.cs b/Penumbra/UI/ConfigWindow.OnScreenTab.cs new file mode 100644 index 00000000..d71b44c0 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.OnScreenTab.cs @@ -0,0 +1,150 @@ + +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Interop; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private class OnScreenTab : ITab + { + public ReadOnlySpan Label + => "On-Screen"u8; + + public void DrawContent() + { + _unfolded ??= new(); + + if( ImGui.Button( "Refresh Character List" ) ) + { + try + { + _trees = ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for On-Screen tab:\n{e}" ); + _trees = Array.Empty(); + } + _unfolded.Clear(); + } + + try + { + _trees ??= ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for On-Screen tab:\n{e}" ); + _trees ??= Array.Empty(); + } + + foreach( var (tree, index) in _trees.WithIndex() ) + { + if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + { + continue; + } + using var id = ImRaii.PushId( index ); + + ImGui.Text( $"Collection: {tree.CollectionName}" ); + + using var table = ImRaii.Table( "##ResourceTree", 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + continue; + } + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f ); + ImGui.TableSetupColumn( "Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f ); + ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); + ImGui.TableHeadersRow(); + + DrawNodes( tree.Nodes, 0 ); + } + } + + private void DrawNodes( IEnumerable resourceNodes, int level ) + { + var debugMode = Penumbra.Config.DebugMode; + var frameHeight = ImGui.GetFrameHeight(); + foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) + { + if( resourceNode.Internal && !debugMode ) + { + continue; + } + using var id = ImRaii.PushId( index ); + ImGui.TableNextColumn(); + var unfolded = _unfolded!.Contains( resourceNode ); + using( var indent = ImRaii.PushIndent( level ) ) + { + ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); + if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) + { + if( unfolded ) + { + _unfolded.Remove( resourceNode ); + } + else + { + _unfolded.Add( resourceNode ); + } + unfolded = !unfolded; + } + if( debugMode ) + { + ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString( "X" + nint.Size * 2 )}" ); + } + } + ImGui.TableNextColumn(); + var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; + ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch + { + 0 => "(none)", + 1 => resourceNode.GamePath.ToString(), + _ => "(multiple)", + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + if( hasGamePaths ) + { + var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( allPaths ); + } + ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); + } + ImGui.TableNextColumn(); + var hasFullPath = resourceNode.FullPath.FullName.Length > 0; + if( hasFullPath ) + { + ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); + } + ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); + } + else + { + ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); + ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); + } + if( unfolded ) + { + DrawNodes( resourceNode.Children, level + 1 ); + } + } + } + + private ResourceTree[]? _trees; + private HashSet? _unfolded; + } +} diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f836d7e5..9065c369 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -25,6 +25,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly ModsTab _modsTab; private readonly ChangedItemsTab _changedItemsTab; private readonly EffectiveTab _effectiveTab; + private readonly OnScreenTab _onScreenTab; private readonly DebugTab _debugTab; private readonly ResourceTab _resourceTab; private readonly ResourceWatcher _resourceWatcher; @@ -47,6 +48,7 @@ public sealed partial class ConfigWindow : Window, IDisposable _collectionsTab = new CollectionsTab( this ); _changedItemsTab = new ChangedItemsTab( this ); _effectiveTab = new EffectiveTab(); + _onScreenTab = new OnScreenTab(); _debugTab = new DebugTab( this ); _resourceTab = new ResourceTab(); if( Penumbra.Config.FixMainWindow ) @@ -74,6 +76,7 @@ public sealed partial class ConfigWindow : Window, IDisposable TabType.Collections => _collectionsTab.Label, TabType.ChangedItems => _changedItemsTab.Label, TabType.EffectiveChanges => _effectiveTab.Label, + TabType.OnScreen => _onScreenTab.Label, TabType.ResourceWatcher => _resourceWatcher.Label, TabType.Debug => _debugTab.Label, TabType.ResourceManager => _resourceTab.Label, @@ -120,7 +123,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { SetupSizes(); if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab, - _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) ) + _changedItemsTab, _effectiveTab, _onScreenTab, _resourceWatcher, _debugTab, _resourceTab ) ) { SelectTab = TabType.None; } From 14eddac6f710a18cfba976dd9c1096ecdfb1a116 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 23 Mar 2023 13:47:36 +0100 Subject: [PATCH 0815/2451] Better handling of sub-objects, better headers --- Penumbra/Interop/ResourceTree.cs | 121 ++++++++++++++---- Penumbra/Interop/Structs/WeaponExt.cs | 14 -- .../UI/Classes/ModEditWindow.QuickImport.cs | 10 +- Penumbra/UI/ConfigWindow.OnScreenTab.cs | 10 +- 4 files changed, 109 insertions(+), 46 deletions(-) delete mode 100644 Penumbra/Interop/Structs/WeaponExt.cs diff --git a/Penumbra/Interop/ResourceTree.cs b/Penumbra/Interop/ResourceTree.cs index 31c84cd6..3c1cfb70 100644 --- a/Penumbra/Interop/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree.cs @@ -4,12 +4,14 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; using Penumbra.Collections; +using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Interop.Resolver; @@ -17,6 +19,7 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Objects = Dalamud.Game.ClientState.Objects.Types; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace Penumbra.Interop; @@ -24,23 +27,24 @@ public class ResourceTree { public readonly string Name; public readonly nint SourceAddress; + public readonly bool PlayerRelated; public readonly string CollectionName; public readonly List Nodes; - public ResourceTree( string name, nint sourceAddress, string collectionName ) + public ResourceTree( string name, nint sourceAddress, bool playerRelated, string collectionName ) { Name = name; SourceAddress = sourceAddress; + PlayerRelated = playerRelated; CollectionName = collectionName; Nodes = new(); } public static ResourceTree[] FromObjectTable( bool withNames = true ) { - var cache = new FileCache(); + var cache = new TreeBuildCache(); - return Dalamud.Objects - .OfType() + return cache.Characters .Select( chara => FromCharacter( chara, cache, withNames ) ) .OfType() .ToArray(); @@ -48,7 +52,7 @@ public class ResourceTree public static IEnumerable<(Objects.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, bool withNames = true ) { - var cache = new FileCache(); + var cache = new TreeBuildCache(); foreach( var chara in characters ) { var tree = FromCharacter( chara, cache, withNames ); @@ -61,11 +65,16 @@ public class ResourceTree public static unsafe ResourceTree? FromCharacter( Objects.Character chara, bool withNames = true ) { - return FromCharacter( chara, new FileCache(), withNames ); + return FromCharacter( chara, new TreeBuildCache(), withNames ); } - private static unsafe ResourceTree? FromCharacter( Objects.Character chara, FileCache cache, bool withNames = true ) + private static unsafe ResourceTree? FromCharacter( Objects.Character chara, TreeBuildCache cache, bool withNames = true ) { + if( !chara.IsValid() ) + { + return null; + } + var charaStruct = ( Character* )chara.Address; var gameObjStruct = &charaStruct->GameObject; var model = ( CharacterBase* )gameObjStruct->GetDrawObject(); @@ -84,7 +93,8 @@ public class ResourceTree } var collection = collectionResolveData.ModCollection; - var tree = new ResourceTree( chara.Name.ToString(), new nint( charaStruct ), collection.Name ); + var name = GetCharacterName( chara, cache ); + var tree = new ResourceTree( name.Name, new nint( charaStruct ), name.PlayerRelated, collection.Name ); var globalContext = new GlobalResolveContext( FileCache: cache, @@ -123,31 +133,66 @@ public class ResourceTree return tree; } + private static unsafe (string Name, bool PlayerRelated) GetCharacterName( Objects.Character chara, TreeBuildCache cache ) + { + var identifier = Penumbra.Actors.FromObject( ( GameObject* )chara.Address, out var owner, true, false, false ); + var name = chara.Name.ToString().Trim(); + var playerRelated = true; + + if( chara.ObjectKind != ObjectKind.Player ) + { + name = $"{name} ({chara.ObjectKind.ToName()})".Trim(); + playerRelated = false; + } + + if( identifier.Type == IdentifierType.Owned && cache.CharactersById.TryGetValue( owner->ObjectID, out var ownerChara ) ) + { + var ownerName = GetCharacterName( ownerChara, cache ); + name = $"[{ownerName.Name}] {name}".Trim(); + playerRelated |= ownerName.PlayerRelated; + } + + return (name, playerRelated); + } + private static unsafe void AddHumanResources( ResourceTree tree, GlobalResolveContext globalContext, HumanExt* human ) { - var prependIndex = 0; - - var firstWeapon = ( WeaponExt* )human->Human.CharacterBase.DrawObject.Object.ChildObject; - if( firstWeapon != null ) + var firstSubObject = ( CharacterBase* )human->Human.CharacterBase.DrawObject.Object.ChildObject; + if( firstSubObject != null ) { - var weapon = firstWeapon; - var weaponIndex = 0; + var subObjectNodes = new List(); + var subObject = firstSubObject; + var subObjectIndex = 0; do { - var weaponContext = globalContext.CreateContext( - Slot: EquipSlot.MainHand, - Equipment: new EquipmentRecord( weapon->Weapon.ModelSetId, ( byte )weapon->Weapon.Variant, ( byte )weapon->Weapon.ModelUnknown ) + var weapon = ( subObject->GetModelType() == CharacterBase.ModelType.Weapon ) ? ( Weapon* )subObject : null; + var subObjectNamePrefix = ( weapon != null ) ? "Weapon" : "Fashion Acc."; + var subObjectContext = globalContext.CreateContext( + Slot: ( weapon != null ) ? EquipSlot.MainHand : EquipSlot.Unknown, + Equipment: ( weapon != null ) ? new EquipmentRecord( weapon->ModelSetId, ( byte )weapon->Variant, ( byte )weapon->ModelUnknown ) : default ); - var weaponMdlNode = weaponContext.CreateNodeFromRenderModel( *weapon->WeaponRenderModel ); - if( weaponMdlNode != null ) + for( var i = 0; i < subObject->SlotCount; ++i ) { - tree.Nodes.Insert( prependIndex++, globalContext.WithNames ? weaponMdlNode.WithName( weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}" ) : weaponMdlNode ); + var imc = ( ResourceHandle* )subObject->IMCArray[i]; + var imcNode = subObjectContext.CreateNodeFromImc( imc ); + if( imcNode != null ) + { + subObjectNodes.Add( globalContext.WithNames ? imcNode.WithName( imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}" ) : imcNode ); + } + + var mdl = ( RenderModel* )subObject->ModelArray[i]; + var mdlNode = subObjectContext.CreateNodeFromRenderModel( mdl ); + if( mdlNode != null ) + { + subObjectNodes.Add( globalContext.WithNames ? mdlNode.WithName( mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}" ) : mdlNode ); + } } - weapon = ( WeaponExt* )weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject; - ++weaponIndex; - } while( weapon != null && weapon != firstWeapon ); + subObject = ( CharacterBase* )subObject->DrawObject.Object.NextSiblingObject; + ++subObjectIndex; + } while( subObject != null && subObject != firstSubObject ); + tree.Nodes.InsertRange( 0, subObjectNodes ); } var context = globalContext.CreateContext( @@ -222,10 +267,30 @@ public class ResourceTree [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 4 )] private readonly record struct EquipmentRecord( ushort SetId, byte Variant, byte Dye ); - private class FileCache + private class TreeBuildCache { - private readonly Dictionary Materials = new(); - private readonly Dictionary ShaderPackages = new(); + private readonly Dictionary Materials = new(); + private readonly Dictionary ShaderPackages = new(); + public readonly List Characters; + public readonly Dictionary CharactersById; + + public TreeBuildCache() + { + Characters = new(); + CharactersById = new(); + foreach( var chara in Dalamud.Objects.OfType() ) + { + if( chara.IsValid() ) + { + Characters.Add( chara ); + if( chara.ObjectId != Objects.GameObject.InvalidGameObjectId && !CharactersById.TryAdd( chara.ObjectId, chara ) ) + { + var existingChara = CharactersById[chara.ObjectId]; + Penumbra.Log.Warning( $"Duplicate character ID {chara.ObjectId:X8} (old: {existingChara.Name} {existingChara.ObjectKind}, new: {chara.Name} {chara.ObjectKind})" ); + } + } + } + } public MtrlFile? ReadMaterial( FullPath path ) { @@ -274,13 +339,13 @@ public class ResourceTree } } - private record class GlobalResolveContext( FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames ) + private record class GlobalResolveContext( TreeBuildCache FileCache, ModCollection Collection, int Skeleton, bool WithNames ) { public ResolveContext CreateContext( EquipSlot Slot, EquipmentRecord Equipment ) => new( FileCache, Collection, Skeleton, WithNames, Slot, Equipment ); } - private record class ResolveContext( FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, EquipmentRecord Equipment ) + private record class ResolveContext( TreeBuildCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, EquipmentRecord Equipment ) { private unsafe Node? CreateNodeFromGamePath( ResourceType type, nint sourceAddress, byte* rawGamePath, bool @internal, bool addDx11Prefix = false, bool isShader = false ) { diff --git a/Penumbra/Interop/Structs/WeaponExt.cs b/Penumbra/Interop/Structs/WeaponExt.cs deleted file mode 100644 index de7038d7..00000000 --- a/Penumbra/Interop/Structs/WeaponExt.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.Interop.Structs; - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct WeaponExt -{ - [FieldOffset( 0x0 )] - public Weapon Weapon; - - [FieldOffset( 0xA8 )] - public RenderModel** WeaponRenderModel; -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs index f300bd34..9bba2ea1 100644 --- a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs @@ -64,11 +64,17 @@ public partial class ModEditWindow _quickImportTrees ??= Array.Empty(); } + var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green + foreach( var (tree, index) in _quickImportTrees.WithIndex() ) { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) { - continue; + if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + { + continue; + } } using var id = ImRaii.PushId( index ); diff --git a/Penumbra/UI/ConfigWindow.OnScreenTab.cs b/Penumbra/UI/ConfigWindow.OnScreenTab.cs index d71b44c0..8baaf64f 100644 --- a/Penumbra/UI/ConfigWindow.OnScreenTab.cs +++ b/Penumbra/UI/ConfigWindow.OnScreenTab.cs @@ -45,11 +45,17 @@ public partial class ConfigWindow _trees ??= Array.Empty(); } + var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green + foreach( var (tree, index) in _trees.WithIndex() ) { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) { - continue; + if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + { + continue; + } } using var id = ImRaii.PushId( index ); From 7a6384bd2213efdd6f672ccc30600fbb24cd1149 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 16:39:29 +0100 Subject: [PATCH 0816/2451] Path Resolver unfiddled and somewhat optimized. --- Penumbra/Api/PenumbraApi.cs | 77 ++-- Penumbra/Interop/Loader/CharacterResolver.cs | 104 ------ .../Interop/Resolver/AnimationHookService.cs | 308 +++++++++++++++ .../Interop/Resolver/CollectionResolver.cs | 260 +++++++++++++ Penumbra/Interop/Resolver/CutsceneService.cs | 41 +- Penumbra/Interop/Resolver/DrawObjectState.cs | 150 ++++++++ .../Resolver/IdentifiedCollectionCache.cs | 38 +- Penumbra/Interop/Resolver/MetaState.cs | 267 +++++++++++++ .../Resolver/PathResolver.AnimationState.cs | 350 ------------------ .../Resolver/PathResolver.DrawObjectState.cs | 255 ------------- .../Resolver/PathResolver.Identification.cs | 187 ---------- .../Interop/Resolver/PathResolver.Meta.cs | 220 ----------- .../Resolver/PathResolver.PathState.cs | 116 ------ .../Resolver/PathResolver.ResolverHooks.cs | 280 -------------- .../Interop/Resolver/PathResolver.Subfiles.cs | 194 ---------- Penumbra/Interop/Resolver/PathResolver.cs | 344 +++++++---------- Penumbra/Interop/Resolver/PathState.cs | 94 +++++ Penumbra/Interop/Resolver/ResolvePathHooks.cs | 249 +++++++++++++ Penumbra/Interop/Resolver/SubfileHelper.cs | 182 +++++++++ Penumbra/Interop/Services/GameEventManager.cs | 143 ++++++- Penumbra/Penumbra.cs | 8 +- Penumbra/PenumbraNew.cs | 15 +- Penumbra/Services/CommunicatorService.cs | 18 + Penumbra/UI/Tabs/DebugTab.cs | 145 ++++---- 24 files changed, 1976 insertions(+), 2069 deletions(-) delete mode 100644 Penumbra/Interop/Loader/CharacterResolver.cs create mode 100644 Penumbra/Interop/Resolver/AnimationHookService.cs create mode 100644 Penumbra/Interop/Resolver/CollectionResolver.cs create mode 100644 Penumbra/Interop/Resolver/DrawObjectState.cs create mode 100644 Penumbra/Interop/Resolver/MetaState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.AnimationState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Identification.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Meta.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.PathState.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs delete mode 100644 Penumbra/Interop/Resolver/PathResolver.Subfiles.cs create mode 100644 Penumbra/Interop/Resolver/PathState.cs create mode 100644 Penumbra/Interop/Resolver/ResolvePathHooks.cs create mode 100644 Penumbra/Interop/Resolver/SubfileHelper.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index a225a626..8b3f6a4b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; @@ -53,13 +54,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi { add { + if (value == null) + return; CheckInitialized(); - PathResolver.DrawObjectState.CreatingCharacterBase += value; + _communicator.CreatingCharacterBase.Event += new Action(value); } remove { + if (value == null) + return; CheckInitialized(); - PathResolver.DrawObjectState.CreatingCharacterBase -= value; + _communicator.CreatingCharacterBase.Event -= new Action(value); } } @@ -67,13 +72,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi { add { + if (value == null) + return; CheckInitialized(); - PathResolver.DrawObjectState.CreatedCharacterBase += value; + _communicator.CreatedCharacterBase.Event += new Action(value); } remove { + if (value == null) + return; CheckInitialized(); - PathResolver.DrawObjectState.CreatedCharacterBase -= value; + _communicator.CreatedCharacterBase.Event -= new Action(value); } } @@ -92,21 +101,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi private TempCollectionManager _tempCollections; private TempModManager _tempMods; private ActorService _actors; + private CollectionResolver _collectionResolver; + private CutsceneService _cutsceneService; public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorService actors) + TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) { - _communicator = communicator; - _penumbra = penumbra; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; + _communicator = communicator; + _penumbra = penumbra; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) @@ -144,6 +157,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _tempCollections = null!; _tempMods = null!; _actors = null!; + _collectionResolver = null!; + _cutsceneService = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -157,7 +172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) { - if (resolveData.AssociatedGameObject != IntPtr.Zero) + if (resolveData.AssociatedGameObject != nint.Zero) GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), manipulatedPath?.ToString() ?? originalPath.ToString()); } @@ -275,7 +290,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolvePlayerPath(string path) { CheckInitialized(); - return ResolvePath(path, _modManager, PathResolver.PlayerCollection()); + return ResolvePath(path, _modManager, _collectionResolver.PlayerCollection()); } // TODO: cleanup when incrementing API level @@ -336,7 +351,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi path, }; - var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); + var ret = _collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); } @@ -349,7 +364,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi p, }).ToArray()); - var playerCollection = PathResolver.PlayerCollection(); + var playerCollection = _collectionResolver.PlayerCollection(); var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); var reverseResolved = playerCollection.ReverseResolvePaths(reverse); return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); @@ -525,17 +540,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi : (_collectionManager.Default.Name, false); } - public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) + public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) { - CheckInitialized(); - var (obj, collection) = PathResolver.IdentifyDrawObject(drawObject); - return (obj, collection.ModCollection.Name); + CheckInitialized(); + var data = _collectionResolver.IdentifyCollection((DrawObject*) drawObject, true); + return (data.AssociatedGameObject, data.ModCollection.Name); } public int GetCutsceneParentIndex(int actorIdx) { CheckInitialized(); - return _penumbra!.PathResolver.CutsceneActor(actorIdx); + return _cutsceneService.GetParentIndex(actorIdx); } public IList<(string, string)> GetModList() @@ -823,10 +838,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_actors.Valid) return PenumbraApiEc.SystemDisposed; - if (actorIndex < 0 || actorIndex >= DalamudServices.SObjects.Length) + if (actorIndex < 0 || actorIndex >= _dalamud.Objects.Length) return PenumbraApiEc.InvalidArgument; - var identifier = _actors.AwaitedService.FromObject(DalamudServices.SObjects[actorIndex], false, false, true); + var identifier = _actors.AwaitedService.FromObject(_dalamud.Objects[actorIndex], false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; @@ -926,7 +941,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetPlayerMetaManipulations() { CheckInitialized(); - var collection = PathResolver.PlayerCollection(); + var collection = _collectionResolver.PlayerCollection(); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -977,11 +992,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = _collectionManager.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.SObjects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) return false; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); - var data = PathResolver.IdentifyCollection(ptr, false); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx); + var data = _collectionResolver.IdentifyCollection(ptr, false); if (data.Valid) collection = data.ModCollection; @@ -994,7 +1009,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid) return ActorIdentifier.Invalid; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx); return _actors.AwaitedService.FromObject(ptr, out _, false, true, true); } diff --git a/Penumbra/Interop/Loader/CharacterResolver.cs b/Penumbra/Interop/Loader/CharacterResolver.cs deleted file mode 100644 index 640cc0d9..00000000 --- a/Penumbra/Interop/Loader/CharacterResolver.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Api; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Resolver; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; - -namespace Penumbra.Interop.Loader; - -public class CharacterResolver : IDisposable -{ - private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; - private readonly TempCollectionManager _tempCollections; - private readonly ResourceLoader _loader; - private readonly PathResolver _pathResolver; - - public unsafe CharacterResolver(Configuration config, ModCollection.Manager collectionManager, TempCollectionManager tempCollections, - ResourceLoader loader, PathResolver pathResolver) - { - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _loader = loader; - _pathResolver = pathResolver; - - _loader.ResolvePath = ResolvePath; - _loader.FileLoaded += ImcLoadResource; - } - - /// Obtain a temporary or permanent collection by name. - public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection); - - /// Try to resolve the given game path to the replaced path. - public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) - { - // Check if mods are enabled or if we are in a inc-ref at 0 reference count situation. - if (!_config.EnableMods) - return (null, ResolveData.Invalid); - - path = path.ToLower(); - return category switch - { - // Only Interface collection. - ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path), - _collectionManager.Interface.ToResolveData()), - // Never allow changing scripts. - ResourceCategory.UiScript => (null, ResolveData.Invalid), - ResourceCategory.GameScript => (null, ResolveData.Invalid), - // Use actual resolving. - ResourceCategory.Chara => _pathResolver.CharacterResolver(path, resourceType), - ResourceCategory.Shader => _pathResolver.CharacterResolver(path, resourceType), - ResourceCategory.Vfx => _pathResolver.CharacterResolver(path, resourceType), - ResourceCategory.Sound => _pathResolver.CharacterResolver(path, resourceType), - // None of these files are ever associated with specific characters, - // always use the default resolver for now. - ResourceCategory.Common => DefaultResolver(path), - ResourceCategory.BgCommon => DefaultResolver(path), - ResourceCategory.Bg => DefaultResolver(path), - ResourceCategory.Cut => DefaultResolver(path), - ResourceCategory.Exd => DefaultResolver(path), - ResourceCategory.Music => DefaultResolver(path), - _ => DefaultResolver(path), - }; - } - - public unsafe void Dispose() - { - _loader.ResetResolvePath(); - _loader.FileLoaded -= ImcLoadResource; - } - - /// Use the default method of path replacement. - private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) - { - var resolved = _collectionManager.Default.ResolvePath(path); - return (resolved, _collectionManager.Default.ToResolveData()); - } - - /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData) - { - if (resource->FileType != ResourceType.Imc) - return; - - var lastUnderscore = additionalData.LastIndexOf((byte)'_'); - var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString(); - if (Utf8GamePath.FromByteString(path, out var gamePath) - && CollectionByName(name, out var collection) - && collection.HasCache - && collection.GetImcFile(gamePath, out var file)) - { - file.Replace(resource); - Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } - } -} diff --git a/Penumbra/Interop/Resolver/AnimationHookService.cs b/Penumbra/Interop/Resolver/AnimationHookService.cs new file mode 100644 index 00000000..b67823d9 --- /dev/null +++ b/Penumbra/Interop/Resolver/AnimationHookService.cs @@ -0,0 +1,308 @@ +using System; +using System.Threading; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.Util; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; + +namespace Penumbra.Interop.Resolver; + +public unsafe class AnimationHookService : IDisposable +{ + private readonly PerformanceTracker _performance; + private readonly ObjectTable _objects; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly CollectionResolver _resolver; + + private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); + private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); + + public AnimationHookService(PerformanceTracker performance, ObjectTable objects, CollectionResolver collectionResolver, + DrawObjectState drawObjectState, CollectionResolver resolver) + { + _performance = performance; + _objects = objects; + _collectionResolver = collectionResolver; + _drawObjectState = drawObjectState; + _resolver = resolver; + + SignatureHelper.Initialise(this); + + _loadCharacterSoundHook.Enable(); + _loadTimelineResourcesHook.Enable(); + _characterBaseLoadAnimationHook.Enable(); + _loadSomePapHook.Enable(); + _someActionLoadHook.Enable(); + _loadCharacterVfxHook.Enable(); + _loadAreaVfxHook.Enable(); + _scheduleClipUpdateHook.Enable(); + } + + public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) + { + switch (type) + { + case ResourceType.Scd: + if (_characterSoundData.IsValueCreated && _characterSoundData.Value.Valid) + { + resolveData = _characterSoundData.Value; + return true; + } + + if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Avfx: + case ResourceType.Atex: + if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + } + + var lastObj = _drawObjectState.LastGameObject; + if (lastObj != nint.Zero) + { + resolveData = _resolver.IdentifyCollection((GameObject*)lastObj, true); + return true; + } + + resolveData = ResolveData.Invalid; + return false; + } + + public void Dispose() + { + _loadCharacterSoundHook.Dispose(); + _loadTimelineResourcesHook.Dispose(); + _characterBaseLoadAnimationHook.Dispose(); + _loadSomePapHook.Dispose(); + _someActionLoadHook.Dispose(); + _loadCharacterVfxHook.Dispose(); + _loadAreaVfxHook.Dispose(); + _scheduleClipUpdateHook.Dispose(); + } + + /// Characters load some of their voice lines or whatever with this function. + private delegate IntPtr LoadCharacterSound(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7); + + [Signature(Sigs.LoadCharacterSound, DetourName = nameof(LoadCharacterSoundDetour))] + private readonly Hook _loadCharacterSoundHook = null!; + + private IntPtr LoadCharacterSoundDetour(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7) + { + using var performance = _performance.Measure(PerformanceType.LoadSound); + var last = _characterSoundData.Value; + _characterSoundData.Value = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var ret = _loadCharacterSoundHook.Original(character, unk1, unk2, unk3, unk4, unk5, unk6, unk7); + _characterSoundData.Value = last; + return ret; + } + + /// + /// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. + /// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. + /// + private delegate ulong LoadTimelineResourcesDelegate(IntPtr timeline); + + [Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))] + private readonly Hook _loadTimelineResourcesHook = null!; + + private ulong LoadTimelineResourcesDetour(IntPtr timeline) + { + using var performance = _performance.Measure(PerformanceType.TimelineResources); + var last = _animationLoadData.Value; + _animationLoadData.Value = GetDataFromTimeline(timeline); + var ret = _loadTimelineResourcesHook.Original(timeline); + _animationLoadData.Value = last; + return ret; + } + + /// + /// Probably used when the base idle animation gets loaded. + /// Make it aware of the correct collection to load the correct pap files. + /// + private delegate void CharacterBaseNoArgumentDelegate(IntPtr drawBase); + + [Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))] + private readonly Hook _characterBaseLoadAnimationHook = null!; + + private void CharacterBaseLoadAnimationDetour(IntPtr drawObject) + { + using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation); + var last = _animationLoadData.Value; + var lastObj = _drawObjectState.LastGameObject; + if (lastObj == nint.Zero && _drawObjectState.TryGetValue(drawObject, out var p)) + lastObj = p.Item1; + _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true); + _characterBaseLoadAnimationHook.Original(drawObject); + _animationLoadData.Value = last; + } + + /// Unknown what exactly this is but it seems to load a bunch of paps. + private delegate void LoadSomePap(IntPtr a1, int a2, IntPtr a3, int a4); + + [Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))] + private readonly Hook _loadSomePapHook = null!; + + private void LoadSomePapDetour(IntPtr a1, int a2, IntPtr a3, int a4) + { + using var performance = _performance.Measure(PerformanceType.LoadPap); + var timelinePtr = a1 + Offsets.TimeLinePtr; + var last = _animationLoadData.Value; + if (timelinePtr != IntPtr.Zero) + { + var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); + if (actorIdx >= 0 && actorIdx < _objects.Length) + _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true); + } + + _loadSomePapHook.Original(a1, a2, a3, a4); + _animationLoadData.Value = last; + } + + /// Seems to load character actions when zoning or changing class, maybe. + [Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))] + private readonly Hook _someActionLoadHook = null!; + + private void SomeActionLoadDetour(nint gameObject) + { + using var performance = _performance.Measure(PerformanceType.LoadAction); + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)gameObject, true); + _someActionLoadHook.Original(gameObject); + _animationLoadData.Value = last; + } + + /// Load a VFX specifically for a character. + private delegate IntPtr LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); + + [Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))] + private readonly Hook _loadCharacterVfxHook = null!; + + private IntPtr LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) + { + using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx); + var last = _animationLoadData.Value; + if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1)) + { + var obj = vfxParams->GameObjectType switch + { + 0 => _objects.SearchById(vfxParams->GameObjectId), + 2 => _objects[(int)vfxParams->GameObjectId], + 4 => GetOwnedObject(vfxParams->GameObjectId), + _ => null, + }; + _animationLoadData.Value = obj != null + ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) + : ResolveData.Invalid; + } + else + { + _animationLoadData.Value = ResolveData.Invalid; + } + + var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); +#if DEBUG + var path = new ByteString(vfxPath); + Penumbra.Log.Verbose( + $"Load Character VFX: {path} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> " + + $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); +#endif + _animationLoadData.Value = last; + return ret; + } + + /// Load a ground-based area VFX. + private delegate nint LoadAreaVfxDelegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); + + [Signature(Sigs.LoadAreaVfx, DetourName = nameof(LoadAreaVfxDetour))] + private readonly Hook _loadAreaVfxHook = null!; + + private nint LoadAreaVfxDetour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) + { + using var performance = _performance.Measure(PerformanceType.LoadAreaVfx); + var last = _animationLoadData.Value; + _animationLoadData.Value = caster != null + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; + + var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3); +#if DEBUG + Penumbra.Log.Verbose( + $"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}" + + $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); +#endif + _animationLoadData.Value = last; + return ret; + } + + + /// Called when some action timelines update. + private delegate void ScheduleClipUpdate(ClipScheduler* x); + + [Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))] + private readonly Hook _scheduleClipUpdateHook = null!; + + private void ScheduleClipUpdateDetour(ClipScheduler* x) + { + using var performance = _performance.Measure(PerformanceType.ScheduleClipUpdate); + var last = _animationLoadData.Value; + var timeline = x->SchedulerTimeline; + _animationLoadData.Value = GetDataFromTimeline(timeline); + _scheduleClipUpdateHook.Original(x); + _animationLoadData.Value = last; + } + + /// Search an object by its id, then get its minion/mount/ornament. + private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) + { + var owner = _objects.SearchById(id); + if (owner == null) + return null; + + var idx = ((GameObject*)owner.Address)->ObjectIndex; + return _objects[idx + 1]; + } + + /// Use timelines vfuncs to obtain the associated game object. + private ResolveData GetDataFromTimeline(IntPtr timeline) + { + try + { + if (timeline != IntPtr.Zero) + { + var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; + var idx = getGameObjectIdx(timeline); + if (idx >= 0 && idx < _objects.Length) + { + var obj = (GameObject*)_objects.GetObjectAddress(idx); + return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid; + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}"); + } + + return ResolveData.Invalid; + } +} diff --git a/Penumbra/Interop/Resolver/CollectionResolver.cs b/Penumbra/Interop/Resolver/CollectionResolver.cs new file mode 100644 index 00000000..1b1a145d --- /dev/null +++ b/Penumbra/Interop/Resolver/CollectionResolver.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections; +using System.Linq; +using Dalamud.Data; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.Services; +using Penumbra.Util; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; + +namespace Penumbra.Interop.Resolver; + +public unsafe class CollectionResolver +{ + private readonly PerformanceTracker _performance; + private readonly IdentifiedCollectionCache _cache; + private readonly BitArray _validHumanModels; + + private readonly ClientState _clientState; + private readonly GameGui _gameGui; + private readonly ActorService _actors; + private readonly CutsceneService _cutscenes; + + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly TempCollectionManager _tempCollections; + private readonly DrawObjectState _drawObjectState; + + public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, + DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager, + TempCollectionManager tempCollections, DrawObjectState drawObjectState) + { + _performance = performance; + _cache = cache; + _clientState = clientState; + _gameGui = gameGui; + _actors = actors; + _cutscenes = cutscenes; + _config = config; + _collectionManager = collectionManager; + _tempCollections = tempCollections; + _drawObjectState = drawObjectState; + _validHumanModels = GetValidHumanModels(gameData); + } + + /// + /// Get the collection applying to the current player character + /// or the Yourself or Default collection if no player exists. + /// + public ModCollection PlayerCollection() + { + using var performance = _performance.Measure(PerformanceType.IdentifyCollection); + var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero); + if (gameObject == null) + return _collectionManager.ByType(CollectionType.Yourself) + ?? _collectionManager.Default; + + var player = _actors.AwaitedService.GetCurrentPlayer(); + return CollectionByIdentifier(player) + ?? CheckYourself(player, gameObject) + ?? CollectionByAttributes(gameObject) + ?? _collectionManager.Default; + } + + /// Identify the correct collection for a game object. + public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache) + { + using var t = _performance.Measure(PerformanceType.IdentifyCollection); + + if (gameObject == null) + return _collectionManager.Default.ToResolveData(); + + try + { + if (useCache && _cache.TryGetValue(gameObject, out var data)) + return data; + + if (LoginScreen(gameObject, out data)) + return data; + + if (Aesthetician(gameObject, out data)) + return data; + + return DefaultState(gameObject); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error identifying collection:\n{ex}"); + return _collectionManager.Default.ToResolveData(gameObject); + } + } + + /// Identify the correct collection for the last created game object. + public ResolveData IdentifyLastGameObjectCollection(bool useCache) + => IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache); + + /// Identify the correct collection for a draw object. + public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) + { + var obj = (GameObject*)(_drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + ? gameObject.Item1 + : _drawObjectState.LastGameObject); + return IdentifyCollection(obj, useCache); + } + + /// Return whether the given ModelChara id refers to a human-type model. + public bool IsModelHuman(uint modelCharaId) + => modelCharaId < _validHumanModels.Length && _validHumanModels[(int)modelCharaId]; + + /// Return whether the given character has a human model. + public bool IsModelHuman(Character* character) + => character != null && IsModelHuman((uint)character->ModelCharaId); + + /// + /// Used if on the Login screen. Names are populated after actors are drawn, + /// so it is not possible to fetch names from the ui list. + /// Actors are also not named. So use Yourself > Players > Racial > Default. + /// + private bool LoginScreen(GameObject* gameObject, out ResolveData ret) + { + // Also check for empty names because sometimes named other characters + // might be loaded before being officially logged in. + if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0') + { + ret = ResolveData.Invalid; + return false; + } + + var collection2 = _collectionManager.ByType(CollectionType.Yourself) + ?? CollectionByAttributes(gameObject) + ?? _collectionManager.Default; + ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject); + return true; + } + + /// Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. + private bool Aesthetician(GameObject* gameObject, out ResolveData ret) + { + if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) + { + ret = ResolveData.Invalid; + return false; + } + + var player = _actors.AwaitedService.GetCurrentPlayer(); + var collection2 = (player.IsValid ? CollectionByIdentifier(player) : null) + ?? _collectionManager.ByType(CollectionType.Yourself) + ?? CollectionByAttributes(gameObject) + ?? _collectionManager.Default; + ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject); + return true; + } + + /// + /// Used when no special state is active. + /// Use individual identifiers first, then Yourself, then group attributes, then ownership settings and last base. + /// + private ResolveData DefaultState(GameObject* gameObject) + { + var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false); + if (identifier.Type is IdentifierType.Special) + { + (identifier, var type) = _collectionManager.Individuals.ConvertSpecialIdentifier(identifier); + if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) + return _cache.Set(ModCollection.Empty, identifier, gameObject); + } + + var collection = CollectionByIdentifier(identifier) + ?? CheckYourself(identifier, gameObject) + ?? CollectionByAttributes(gameObject) + ?? CheckOwnedCollection(identifier, owner) + ?? _collectionManager.Default; + + return _cache.Set(collection, identifier, gameObject); + } + + /// Check both temporary and permanent character collections. Temporary first. + private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) + => _tempCollections.Collections.TryGetCollection(identifier, out var collection) + || _collectionManager.Individuals.TryGetCollection(identifier, out collection) + ? collection + : null; + + /// Check for the Yourself collection. + private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor) + { + if (actor->ObjectIndex == 0 + || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 + || identifier.Equals(_actors.AwaitedService.GetCurrentPlayer())) + return _collectionManager.ByType(CollectionType.Yourself); + + return null; + } + + /// Check special collections given the actor. + private ModCollection? CollectionByAttributes(GameObject* actor) + { + if (!actor->IsCharacter()) + return null; + + // Only handle human models. + var character = (Character*)actor; + if (!IsModelHuman((uint)character->ModelCharaId)) + return null; + + var bodyType = character->CustomizeData[2]; + var collection = bodyType switch + { + 3 => _collectionManager.ByType(CollectionType.NonPlayerElderly), + 4 => _collectionManager.ByType(CollectionType.NonPlayerChild), + _ => null, + }; + if (collection != null) + return collection; + + var race = (SubRace)character->CustomizeData[4]; + var gender = (Gender)(character->CustomizeData[1] + 1); + var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; + + var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); + collection = _collectionManager.ByType(type); + collection ??= _collectionManager.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); + return collection; + } + + /// Get the collection applying to the owner if it is available. + private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner) + { + if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) + return null; + + var id = _actors.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, + ObjectKind.None, + uint.MaxValue); + return CheckYourself(id, owner) + ?? CollectionByAttributes(owner); + } + + /// + /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. + /// + private static BitArray GetValidHumanModels(DataManager gameData) + { + var sheet = gameData.GetExcelSheet()!; + var ret = new BitArray((int)sheet.RowCount, false); + foreach (var (_, idx) in sheet.WithIndex().Where(p => p.Value.Type == (byte)CharacterBase.ModelType.Human)) + ret[idx] = true; + + return ret; + } +} diff --git a/Penumbra/Interop/Resolver/CutsceneService.cs b/Penumbra/Interop/Resolver/CutsceneService.cs index 4bb8799c..54a86772 100644 --- a/Penumbra/Interop/Resolver/CutsceneService.cs +++ b/Penumbra/Interop/Resolver/CutsceneService.cs @@ -4,15 +4,16 @@ using System.Diagnostics; using System.Linq; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.Interop.Services; - +using Penumbra.GameData.Actors; +using Penumbra.Interop.Services; + namespace Penumbra.Interop.Resolver; public class CutsceneService : IDisposable { - public const int CutsceneStartIdx = 200; - public const int CutsceneSlots = 40; - public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; + public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; + public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; + public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; private readonly GameEventManager _events; private readonly ObjectTable _objects; @@ -23,16 +24,19 @@ public class CutsceneService : IDisposable .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public CutsceneService(ObjectTable objects, GameEventManager events) + public unsafe CutsceneService(ObjectTable objects, GameEventManager events) { - _objects = objects; - _events = events; - Enable(); + _objects = objects; + _events = events; + _events.CopyCharacter += OnCharacterCopy; + _events.CharacterDestructor += OnCharacterDestructor; } - // Get the related actor to a cutscene actor. - // Does not check for valid input index. - // Returns null if no connected actor is set or the actor does not exist anymore. + /// + /// Get the related actor to a cutscene actor. + /// Does not check for valid input index. + /// Returns null if no connected actor is set or the actor does not exist anymore. + /// public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] { get @@ -43,7 +47,7 @@ public class CutsceneService : IDisposable } } - // Return the currently set index of a parent or -1 if none is set or the index is invalid. + /// Return the currently set index of a parent or -1 if none is set or the index is invalid. public int GetParentIndex(int idx) { if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) @@ -52,21 +56,12 @@ public class CutsceneService : IDisposable return -1; } - public unsafe void Enable() - { - _events.CopyCharacter += OnCharacterCopy; - _events.CharacterDestructor += OnCharacterDestructor; - } - - public unsafe void Disable() + public unsafe void Dispose() { _events.CopyCharacter -= OnCharacterCopy; _events.CharacterDestructor -= OnCharacterDestructor; } - public void Dispose() - => Disable(); - private unsafe void OnCharacterDestructor(Character* character) { if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) diff --git a/Penumbra/Interop/Resolver/DrawObjectState.cs b/Penumbra/Interop/Resolver/DrawObjectState.cs new file mode 100644 index 00000000..b7e1ca1d --- /dev/null +++ b/Penumbra/Interop/Resolver/DrawObjectState.cs @@ -0,0 +1,150 @@ +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using Dalamud.Game.ClientState.Objects; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Api; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Services; +using Penumbra.String.Classes; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; + +namespace Penumbra.Interop.Resolver; + +public class DrawObjectState : IDisposable, IReadOnlyDictionary +{ + private readonly ObjectTable _objects; + private readonly GameEventManager _gameEvents; + + private readonly Dictionary _drawObjectToGameObject = new(); + + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + + public nint LastGameObject + => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; + + public DrawObjectState(ObjectTable objects, GameEventManager gameEvents) + { + SignatureHelper.Initialise(this); + _enableDrawHook.Enable(); + _objects = objects; + _gameEvents = gameEvents; + _gameEvents.WeaponReloading += OnWeaponReloading; + _gameEvents.WeaponReloaded += OnWeaponReloaded; + _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; + _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + InitializeDrawObjects(); + } + + public bool ContainsKey(nint key) + => _drawObjectToGameObject.ContainsKey(key); + + public IEnumerator> GetEnumerator() + => _drawObjectToGameObject.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _drawObjectToGameObject.Count; + + public bool TryGetValue(nint drawObject, out (nint, bool) gameObject) + => _drawObjectToGameObject.TryGetValue(drawObject, out gameObject); + + public (nint, bool) this[nint key] + => _drawObjectToGameObject[key]; + + public IEnumerable Keys + => _drawObjectToGameObject.Keys; + + public IEnumerable<(nint, bool)> Values + => _drawObjectToGameObject.Values; + + public void Dispose() + { + _gameEvents.WeaponReloading -= OnWeaponReloading; + _gameEvents.WeaponReloaded -= OnWeaponReloaded; + _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; + _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; + _enableDrawHook.Dispose(); + } + + private void OnWeaponReloading(nint _, nint gameObject) + => _lastGameObject.Value!.Enqueue(gameObject); + + private unsafe void OnWeaponReloaded(nint _, nint gameObject) + { + _lastGameObject.Value!.Dequeue(); + IterateDrawObjectTree((Object*) ((GameObject*) gameObject)->DrawObject, gameObject, false, false); + } + + private void OnCharacterBaseDestructor(nint characterBase) + => _drawObjectToGameObject.Remove(characterBase); + + private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + { + var gameObject = LastGameObject; + if (gameObject != nint.Zero) + _drawObjectToGameObject[drawObject] = (gameObject, false); + } + + /// + /// Find all current DrawObjects used in the GameObject table. + /// We do not iterate the Dalamud table because it does not work when not logged in. + /// + private unsafe void InitializeDrawObjects() + { + for (var i = 0; i < _objects.Length; ++i) + { + var ptr = (GameObject*)_objects.GetObjectAddress(i); + if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) + IterateDrawObjectTree(&ptr->DrawObject->Object, (nint)ptr, false, false); + } + } + + private unsafe void IterateDrawObjectTree(Object* drawObject, nint gameObject, bool isChild, bool iterate) + { + if (drawObject == null) + return; + + _drawObjectToGameObject[(nint)drawObject] = (gameObject, isChild); + IterateDrawObjectTree(drawObject->ChildObject, gameObject, true, true); + if (!iterate) + return; + + var nextSibling = drawObject->NextSiblingObject; + while (nextSibling != null && nextSibling != drawObject) + { + IterateDrawObjectTree(nextSibling, gameObject, true, false); + nextSibling = nextSibling->NextSiblingObject; + } + + var prevSibling = drawObject->PreviousSiblingObject; + while (prevSibling != null && prevSibling != drawObject) + { + IterateDrawObjectTree(prevSibling, gameObject, true, false); + prevSibling = prevSibling->PreviousSiblingObject; + } + } + + /// + /// EnableDraw is what creates DrawObjects for gameObjects, + /// so we always keep track of the current GameObject to be able to link it to the DrawObject. + /// + private delegate void EnableDrawDelegate(nint gameObject, nint b, nint c, nint d); + + [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] + private readonly Hook _enableDrawHook = null!; + + private void EnableDrawDetour(nint gameObject, nint b, nint c, nint d) + { + _lastGameObject.Value!.Enqueue(gameObject); + _enableDrawHook.Original.Invoke(gameObject, b, c, d); + _lastGameObject.Value!.TryDequeue(out _); + } +} diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index 83739dca..430f03b0 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -13,41 +13,21 @@ namespace Penumbra.Interop.Resolver; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> { - private readonly CommunicatorService _communicator; - private readonly GameEventManager _events; - private readonly ClientState _clientState; - private readonly Dictionary _cache = new(317); - private bool _dirty; - private bool _enabled; + private readonly CommunicatorService _communicator; + private readonly GameEventManager _events; + private readonly ClientState _clientState; + private readonly Dictionary _cache = new(317); + private bool _dirty; public IdentifiedCollectionCache(ClientState clientState, CommunicatorService communicator, GameEventManager events) { _clientState = clientState; _communicator = communicator; _events = events; - Enable(); - } - - public void Enable() - { - if (_enabled) - return; _communicator.CollectionChange.Event += CollectionChangeClear; _clientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; - _enabled = true; - } - - public void Disable() - { - if (!_enabled) - return; - - _communicator.CollectionChange.Event -= CollectionChangeClear; - _clientState.TerritoryChanged -= TerritoryClear; - _events.CharacterDestructor -= OnCharacterDestruct; - _enabled = false; } public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) @@ -81,8 +61,9 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A public void Dispose() { - Disable(); - GC.SuppressFinalize(this); + _communicator.CollectionChange.Event -= CollectionChangeClear; + _clientState.TerritoryChanged -= TerritoryClear; + _events.CharacterDestructor -= OnCharacterDestruct; } public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() @@ -96,9 +77,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A } } - ~IdentifiedCollectionCache() - => Dispose(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/Penumbra/Interop/Resolver/MetaState.cs b/Penumbra/Interop/Resolver/MetaState.cs new file mode 100644 index 00000000..cfe9419f --- /dev/null +++ b/Penumbra/Interop/Resolver/MetaState.cs @@ -0,0 +1,267 @@ +using System; +using System.Linq; +using System.Threading; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Services; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; +using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; +using static Penumbra.GameData.Enums.GenderRace; + +namespace Penumbra.Interop.Resolver; + +// State: 6.35 +// GetSlotEqpData seems to be the only function using the EQP table. +// It is only called by CheckSlotsForUnload (called by UpdateModels), +// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete) +// and a unnamed function called by UpdateRender. +// It seems to be enough to change the EQP entries for UpdateModels. + +// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables. +// They are called by ResolveMdlPath, UpdateModels and SetupConnectorModelAttributes, +// which is called by SetupModelAttributes, which is called by OnModelLoadComplete and UpdateModels. +// It seems to be enough to change EQDP on UpdateModels and ResolveMDLPath. + +// EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by +// ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath. + +// RSP height entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF" +// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05" +// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" +// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, +// ChangeCustomize and RspSetupCharacter, which is hooked here. + +// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. +public unsafe class MetaState : IDisposable +{ + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private readonly CommunicatorService _communicator; + private readonly PerformanceTracker _performance; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceService _resources; + private readonly GameEventManager _gameEventManager; + + private ResolveData _lastCreatedCollection = ResolveData.Invalid; + private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; + + public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, + ResourceService resources, GameEventManager gameEventManager) + { + _performance = performance; + _communicator = communicator; + _collectionResolver = collectionResolver; + _resources = resources; + _gameEventManager = gameEventManager; + SignatureHelper.Initialise(this); + _onModelLoadCompleteHook = Hook.FromAddress(_humanVTable[58], OnModelLoadCompleteDetour); + _getEqpIndirectHook.Enable(); + _updateModelsHook.Enable(); + _onModelLoadCompleteHook.Enable(); + _setupVisorHook.Enable(); + _rspSetupCharacterHook.Enable(); + _changeCustomize.Enable(); + _gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase; + _gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated; + } + + public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) + { + if (type == ResourceType.Tex + && _lastCreatedCollection.Valid + && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) + { + resolveData = _lastCreatedCollection; + return true; + } + + resolveData = ResolveData.Invalid; + return false; + } + + public static DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) + { + var races = race.Dependencies(); + if (races.Length == 0) + return DisposableContainer.Empty; + + var equipmentEnumerable = equipment + ? races.Select(r => collection.TemporarilySetEqdpFile(r, false)) + : Array.Empty().AsEnumerable(); + var accessoryEnumerable = accessory + ? races.Select(r => collection.TemporarilySetEqdpFile(r, true)) + : Array.Empty().AsEnumerable(); + return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable)); + } + + public static GenderRace GetHumanGenderRace(nint human) + => (GenderRace)((Human*)human)->RaceSexId; + + public void Dispose() + { + _getEqpIndirectHook.Dispose(); + _updateModelsHook.Dispose(); + _onModelLoadCompleteHook.Dispose(); + _setupVisorHook.Dispose(); + _rspSetupCharacterHook.Dispose(); + _changeCustomize.Dispose(); + _gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase; + _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; + } + + private void OnCreatingCharacterBase(uint modelCharaId, nint customize, nint equipData) + { + _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); + if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) + _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, + _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); + + var decal = new CharacterUtility.DecalReverter(_resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); + var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); + _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. + _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); + } + + private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject) + { + _characterBaseCreateMetaChanges.Dispose(); + _characterBaseCreateMetaChanges = DisposableContainer.Empty; + if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) + _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, + _lastCreatedCollection.ModCollection.Name, drawObject); + _lastCreatedCollection = ResolveData.Invalid; + } + + private delegate void OnModelLoadCompleteDelegate(nint drawObject); + private readonly Hook _onModelLoadCompleteHook; + + private void OnModelLoadCompleteDetour(nint drawObject) + { + var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); + _onModelLoadCompleteHook.Original.Invoke(drawObject); + } + + private delegate void UpdateModelDelegate(nint drawObject); + + [Signature(Sigs.UpdateModel, DetourName = nameof(UpdateModelsDetour))] + private readonly Hook _updateModelsHook = null!; + + private void UpdateModelsDetour(nint drawObject) + { + // Shortcut because this is called all the time. + // Same thing is checked at the beginning of the original function. + if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0) + return; + + using var performance = _performance.Measure(PerformanceType.UpdateModels); + + var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); + _updateModelsHook.Original.Invoke(drawObject); + } + + private static GenderRace GetDrawObjectGenderRace(nint drawObject) + { + var draw = (DrawObject*)drawObject; + if (draw->Object.GetObjectType() != ObjectType.CharacterBase) + return Unknown; + + var c = (CharacterBase*)drawObject; + return c->GetModelType() == CharacterBase.ModelType.Human + ? GetHumanGenderRace(drawObject) + : Unknown; + } + + [Signature(Sigs.GetEqpIndirect, DetourName = nameof(GetEqpIndirectDetour))] + private readonly Hook _getEqpIndirectHook = null!; + + private void GetEqpIndirectDetour(nint drawObject) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0) + return; + + using var performance = _performance.Measure(PerformanceType.GetEqp); + var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); + _getEqpIndirectHook.Original(drawObject); + } + + + // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, + // but it only applies a changed gmp file after a redraw for some reason. + private delegate byte SetupVisorDelegate(nint drawObject, ushort modelId, byte visorState); + + [Signature(Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] + private readonly Hook _setupVisorHook = null!; + + private byte SetupVisorDetour(nint drawObject, ushort modelId, byte visorState) + { + using var performance = _performance.Measure(PerformanceType.SetupVisor); + var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(); + return _setupVisorHook.Original(drawObject, modelId, visorState); + } + + // RSP + private delegate void RspSetupCharacterDelegate(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5); + + [Signature(Sigs.RspSetupCharacter, DetourName = nameof(RspSetupCharacterDetour))] + private readonly Hook _rspSetupCharacterHook = null!; + + private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5) + { + if (_inChangeCustomize) + { + _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); + } + else + { + using var performance = _performance.Measure(PerformanceType.SetupCharacter); + var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); + _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); + } + } + + /// ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. + private bool _inChangeCustomize; + + private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment); + + [Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))] + private readonly Hook _changeCustomize = null!; + + private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment) + { + using var performance = _performance.Measure(PerformanceType.ChangeCustomize); + _inChangeCustomize = true; + var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); + using var decals = + new CharacterUtility.DecalReverter(_resources, resolveData.ModCollection, UsesDecal(0, data)); + var ret = _changeCustomize.Original(human, data, skipEquipment); + _inChangeCustomize = false; + return ret; + } + + /// + /// Check the customize array for the FaceCustomization byte and the last bit of that. + /// Also check for humans. + /// + public static bool UsesDecal(uint modelId, nint customizeData) + => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; +} diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs deleted file mode 100644 index e39b85f4..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Services; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - public class AnimationState - { - private readonly DrawObjectState _drawObjectState; - - private ResolveData _animationLoadData = ResolveData.Invalid; - private ResolveData _characterSoundData = ResolveData.Invalid; - - public AnimationState( DrawObjectState drawObjectState ) - { - _drawObjectState = drawObjectState; - SignatureHelper.Initialise( this ); - } - - public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData ) - { - switch( type ) - { - case ResourceType.Scd: - if( _characterSoundData.Valid ) - { - resolveData = _characterSoundData; - return true; - } - - if( _animationLoadData.Valid ) - { - resolveData = _animationLoadData; - return true; - } - - break; - case ResourceType.Tmb: - case ResourceType.Pap: - case ResourceType.Avfx: - case ResourceType.Atex: - if( _animationLoadData.Valid ) - { - resolveData = _animationLoadData; - return true; - } - - break; - } - - if( _drawObjectState.LastGameObject != null ) - { - resolveData = _drawObjectState.LastCreatedCollection; - return true; - } - - resolveData = ResolveData.Invalid; - return false; - } - - public void Enable() - { - _loadTimelineResourcesHook.Enable(); - _characterBaseLoadAnimationHook.Enable(); - _loadSomePapHook.Enable(); - _someActionLoadHook.Enable(); - _loadCharacterSoundHook.Enable(); - _loadCharacterVfxHook.Enable(); - _loadAreaVfxHook.Enable(); - _scheduleClipUpdateHook.Enable(); - - //_loadSomeAvfxHook.Enable(); - //_someOtherAvfxHook.Enable(); - } - - public void Disable() - { - _loadTimelineResourcesHook.Disable(); - _characterBaseLoadAnimationHook.Disable(); - _loadSomePapHook.Disable(); - _someActionLoadHook.Disable(); - _loadCharacterSoundHook.Disable(); - _loadCharacterVfxHook.Disable(); - _loadAreaVfxHook.Disable(); - _scheduleClipUpdateHook.Disable(); - - //_loadSomeAvfxHook.Disable(); - //_someOtherAvfxHook.Disable(); - } - - public void Dispose() - { - _loadTimelineResourcesHook.Dispose(); - _characterBaseLoadAnimationHook.Dispose(); - _loadSomePapHook.Dispose(); - _someActionLoadHook.Dispose(); - _loadCharacterSoundHook.Dispose(); - _loadCharacterVfxHook.Dispose(); - _loadAreaVfxHook.Dispose(); - _scheduleClipUpdateHook.Dispose(); - - //_loadSomeAvfxHook.Dispose(); - //_someOtherAvfxHook.Dispose(); - } - - // Characters load some of their voice lines or whatever with this function. - private delegate IntPtr LoadCharacterSound( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ); - - [Signature( Sigs.LoadCharacterSound, DetourName = nameof( LoadCharacterSoundDetour ) )] - private readonly Hook< LoadCharacterSound > _loadCharacterSoundHook = null!; - - private IntPtr LoadCharacterSoundDetour( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadSound ); - var last = _characterSoundData; - _characterSoundData = IdentifyCollection( ( GameObject* )character, true ); - var ret = _loadCharacterSoundHook.Original( character, unk1, unk2, unk3, unk4, unk5, unk6, unk7 ); - _characterSoundData = last; - return ret; - } - - private static ResolveData GetDataFromTimeline( IntPtr timeline ) - { - try - { - if( timeline != IntPtr.Zero ) - { - var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ]; - var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < DalamudServices.SObjects.Length ) - { - var obj = DalamudServices.SObjects[ idx ]; - return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; - } - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error getting timeline data for 0x{timeline:X}:\n{e}" ); - } - - return ResolveData.Invalid; - } - - // The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. - // We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. - private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline ); - - [Signature( Sigs.LoadTimelineResources, DetourName = nameof( LoadTimelineResourcesDetour ) )] - private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!; - - private ulong LoadTimelineResourcesDetour( IntPtr timeline ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.TimelineResources ); - var old = _animationLoadData; - _animationLoadData = GetDataFromTimeline( timeline ); - var ret = _loadTimelineResourcesHook.Original( timeline ); - _animationLoadData = old; - return ret; - } - - // Probably used when the base idle animation gets loaded. - // Make it aware of the correct collection to load the correct pap files. - private delegate void CharacterBaseNoArgumentDelegate( IntPtr drawBase ); - - [Signature( Sigs.CharacterBaseLoadAnimation, DetourName = nameof( CharacterBaseLoadAnimationDetour ) )] - private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!; - - private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterBaseAnimation ); - var last = _animationLoadData; - _animationLoadData = _drawObjectState.LastCreatedCollection.Valid - ? _drawObjectState.LastCreatedCollection - : FindParent( drawObject, out var collection ) != null - ? collection - : Penumbra.CollectionManager.Default.ToResolveData(); - _characterBaseLoadAnimationHook.Original( drawObject ); - _animationLoadData = last; - } - - // Unknown what exactly this is but it seems to load a bunch of paps. - private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); - - [Signature( Sigs.LoadSomePap, DetourName = nameof( LoadSomePapDetour ) )] - private readonly Hook< LoadSomePap > _loadSomePapHook = null!; - - private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadPap ); - var timelinePtr = a1 + Offsets.TimeLinePtr; - var last = _animationLoadData; - if( timelinePtr != IntPtr.Zero ) - { - var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); - if( actorIdx >= 0 && actorIdx < DalamudServices.SObjects.Length ) - { - _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.SObjects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); - } - } - - _loadSomePapHook.Original( a1, a2, a3, a4 ); - _animationLoadData = last; - } - - // Seems to load character actions when zoning or changing class, maybe. - [Signature( Sigs.LoadSomeAction, DetourName = nameof( SomeActionLoadDetour ) )] - private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!; - - private void SomeActionLoadDetour( IntPtr gameObject ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAction ); - var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); - _someActionLoadHook.Original( gameObject ); - _animationLoadData = last; - } - - private delegate IntPtr LoadCharacterVfxDelegate( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ); - - [Signature( Sigs.LoadCharacterVfx, DetourName = nameof( LoadCharacterVfxDetour ) )] - private readonly Hook< LoadCharacterVfxDelegate > _loadCharacterVfxHook = null!; - - private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) - { - var owner = DalamudServices.SObjects.SearchById( id ); - if( owner == null ) - { - return null; - } - - var idx = ( ( GameObject* )owner.Address )->ObjectIndex; - return DalamudServices.SObjects[ idx + 1 ]; - } - - private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterVfx ); - var last = _animationLoadData; - if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) ) - { - var obj = vfxParams->GameObjectType switch - { - 0 => DalamudServices.SObjects.SearchById( vfxParams->GameObjectId ), - 2 => DalamudServices.SObjects[ ( int )vfxParams->GameObjectId ], - 4 => GetOwnedObject( vfxParams->GameObjectId ), - _ => null, - }; - _animationLoadData = obj != null - ? IdentifyCollection( ( GameObject* )obj.Address, true ) - : ResolveData.Invalid; - } - else - { - _animationLoadData = ResolveData.Invalid; - } - - var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 ); -#if DEBUG - var path = new ByteString( vfxPath ); - Penumbra.Log.Verbose( - $"Load Character VFX: {path} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); -#endif - _animationLoadData = last; - return ret; - } - - private delegate IntPtr LoadAreaVfxDelegate( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ); - - [Signature( Sigs.LoadAreaVfx, DetourName = nameof( LoadAreaVfxDetour ) )] - private readonly Hook< LoadAreaVfxDelegate > _loadAreaVfxHook = null!; - - private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAreaVfx ); - var last = _animationLoadData; - if( caster != null ) - { - _animationLoadData = IdentifyCollection( caster, true ); - } - else - { - _animationLoadData = ResolveData.Invalid; - } - - var ret = _loadAreaVfxHook.Original( vfxId, pos, caster, unk1, unk2, unk3 ); -#if DEBUG - Penumbra.Log.Verbose( - $"Load Area VFX: {vfxId}, {pos[ 0 ]} {pos[ 1 ]} {pos[ 2 ]} {( caster != null ? new ByteString( caster->GetName() ).ToString() : "Unknown" )} {unk1} {unk2} {unk3} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" ); -#endif - _animationLoadData = last; - return ret; - } - - - private delegate void ScheduleClipUpdate( ClipScheduler* x ); - - [Signature( Sigs.ScheduleClipUpdate, DetourName = nameof( ScheduleClipUpdateDetour ) )] - private readonly Hook< ScheduleClipUpdate > _scheduleClipUpdateHook = null!; - - private void ScheduleClipUpdateDetour( ClipScheduler* x ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.ScheduleClipUpdate ); - var old = _animationLoadData; - var timeline = x->SchedulerTimeline; - _animationLoadData = GetDataFromTimeline( timeline ); - _scheduleClipUpdateHook.Original( x ); - _animationLoadData = old; - } - - // ========== Those hooks seem to be superseded by LoadCharacterVfx ========= - - // public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ); - // - // [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] - // private readonly Hook _loadSomeAvfxHook = null!; - // - // private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) - // { - // var last = _animationLoadData; - // _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); - // var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); - // _animationLoadData = last; - // return ret; - // } - // - // [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )] - // private readonly Hook _someOtherAvfxHook = null!; - // - // private void SomeOtherAvfxDetour( IntPtr unk ) - // { - // var last = _animationLoadData; - // var gameObject = ( GameObject* )( unk - 0x8D0 ); - // _animationLoadData = IdentifyCollection( gameObject, true ); - // _someOtherAvfxHook.Original( unk ); - // _animationLoadData = last; - // } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs deleted file mode 100644 index 983dcdb6..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using Penumbra.Collections; -using System; -using System.Collections.Generic; -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.Api; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Classes; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.String.Classes; -using Penumbra.Util; -using Penumbra.Services; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - public class DrawObjectState - { - private readonly CommunicatorService _communicator; - public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; - public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; - - public IEnumerable> DrawObjects - => _drawObjectToObject; - - public int Count - => _drawObjectToObject.Count; - - public bool TryGetValue(IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject) - { - gameObject = null; - if (!_drawObjectToObject.TryGetValue(drawObject, out value)) - return false; - - var gameObjectIdx = value.Item2; - return VerifyEntry(drawObject, gameObjectIdx, out gameObject); - } - - - // Set and update a parent object if it exists and a last game object is set. - public ResolveData CheckParentDrawObject(IntPtr drawObject, IntPtr parentObject) - { - if (parentObject == IntPtr.Zero && LastGameObject != null) - { - var collection = IdentifyCollection(LastGameObject, true); - _drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex); - return collection; - } - - return ResolveData.Invalid; - } - - - public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) - { - if (type == ResourceType.Tex - && LastCreatedCollection.Valid - && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) - { - resolveData = LastCreatedCollection; - return true; - } - - resolveData = ResolveData.Invalid; - return false; - } - - - public ResolveData LastCreatedCollection - => _lastCreatedCollection; - - public GameObject* LastGameObject { get; private set; } - - public DrawObjectState(CommunicatorService communicator) - { - SignatureHelper.Initialise(this); - _communicator = communicator; - } - - public void Enable() - { - _characterBaseCreateHook.Enable(); - _characterBaseDestructorHook.Enable(); - _enableDrawHook.Enable(); - _weaponReloadHook.Enable(); - InitializeDrawObjects(); - _communicator.CollectionChange.Event += CheckCollections; - } - - public void Disable() - { - _characterBaseCreateHook.Disable(); - _characterBaseDestructorHook.Disable(); - _enableDrawHook.Disable(); - _weaponReloadHook.Disable(); - _communicator.CollectionChange.Event -= CheckCollections; - } - - public void Dispose() - { - Disable(); - _characterBaseCreateHook.Dispose(); - _characterBaseDestructorHook.Dispose(); - _enableDrawHook.Dispose(); - _weaponReloadHook.Dispose(); - } - - // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. - private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject) - { - gameObject = (GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); - var draw = (DrawObject*)drawObject; - if (gameObject != null - && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) - return true; - - gameObject = null; - _drawObjectToObject.Remove(drawObject); - return false; - } - - // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. - // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary _drawObjectToObject = new(); - private ResolveData _lastCreatedCollection = ResolveData.Invalid; - - // Keep track of created DrawObjects that are CharacterBase, - // and use the last game object that called EnableDraw to link them. - private delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d); - - [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))] - private readonly Hook _characterBaseCreateHook = null!; - - private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterBaseCreate); - - var meta = DisposableContainer.Empty; - if (LastGameObject != null) - { - _lastCreatedCollection = IdentifyCollection(LastGameObject, false); - // Change the transparent or 1.0 Decal if necessary. - var decal = new CharacterUtility.DecalReverter(Penumbra.ResourceService, _lastCreatedCollection.ModCollection, UsesDecal(a, c)); - // Change the rsp parameters. - meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal); - try - { - var modelPtr = &a; - CreatingCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, (IntPtr)modelPtr, b, c); - } - catch (Exception e) - { - Penumbra.Log.Error($"Unknown Error during CreatingCharacterBase:\n{e}"); - } - } - - var ret = _characterBaseCreateHook.Original(a, b, c, d); - try - { - if (LastGameObject != null && ret != IntPtr.Zero) - { - _drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex); - CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret); - } - } - finally - { - meta.Dispose(); - } - - return ret; - } - - // Check the customize array for the FaceCustomization byte and the last bit of that. - // Also check for humans. - public static bool UsesDecal(uint modelId, IntPtr customizeData) - => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; - - - // Remove DrawObjects from the list when they are destroyed. - private delegate void CharacterBaseDestructorDelegate(IntPtr drawBase); - - [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))] - private readonly Hook _characterBaseDestructorHook = null!; - - private void CharacterBaseDestructorDetour(IntPtr drawBase) - { - _drawObjectToObject.Remove(drawBase); - _characterBaseDestructorHook!.Original.Invoke(drawBase); - } - - - // EnableDraw is what creates DrawObjects for gameObjects, - // so we always keep track of the current GameObject to be able to link it to the DrawObject. - private delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d); - - [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] - private readonly Hook _enableDrawHook = null!; - - private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d) - { - var oldObject = LastGameObject; - LastGameObject = (GameObject*)gameObject; - _enableDrawHook!.Original.Invoke(gameObject, b, c, d); - LastGameObject = oldObject; - } - - // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, - // so we use that. - private delegate void WeaponReloadFunc(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7); - - [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))] - private readonly Hook _weaponReloadHook = null!; - - public void WeaponReloadDetour(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7) - { - var oldGame = LastGameObject; - LastGameObject = *(GameObject**)(a1 + 8); - _weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7); - LastGameObject = oldGame; - } - - // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) - { - if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface) - return; - - foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray()) - { - if (!VerifyEntry(key, idx, out var obj)) - _drawObjectToObject.Remove(key); - - var newCollection = IdentifyCollection(obj, false); - _drawObjectToObject[key] = (newCollection, idx); - } - } - - // Find all current DrawObjects used in the GameObject table. - // We do not iterate the Dalamud table because it does not work when not logged in. - private void InitializeDrawObjects() - { - for (var i = 0; i < DalamudServices.SObjects.Length; ++i) - { - var ptr = (GameObject*)DalamudServices.SObjects.GetObjectAddress(i); - if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) - _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); - } - } - } -} diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs deleted file mode 100644 index a619755a..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections; -using System.Linq; -using Dalamud.Data; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Lumina.Excel.GeneratedSheets; -using OtterGui; -using Penumbra.Collections; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.Services; -using Penumbra.Util; -using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - // Identify the correct collection for a GameObject by index and name. - public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - - if( gameObject == null ) - { - return new ResolveData( Penumbra.CollectionManager.Default ); - } - - try - { - if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) ) - { - return data; - } - - // Login screen. Names are populated after actors are drawn, - // so it is not possible to fetch names from the ui list. - // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !DalamudServices.SClientState.IsLoggedIn ) - { - var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; - return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); - } - - // Aesthetician. The relevant actor is yourself, so use player collection when possible. - if( DalamudServices.GameGui.GetAddonByName( "ScreenLog" ) == IntPtr.Zero ) - { - var player = Penumbra.Actors.GetCurrentPlayer(); - var collection2 = ( player.IsValid ? CollectionByIdentifier( player ) : null ) - ?? Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; - return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject ); - } - - var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false, false ); - if( identifier.Type is IdentifierType.Special ) - { - ( identifier, var type ) = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier ); - if( Penumbra.Config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect ) - { - return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject ); - } - } - - var collection = CollectionByIdentifier( identifier ) - ?? CheckYourself( identifier, gameObject ) - ?? CollectionByAttributes( gameObject ) - ?? CheckOwnedCollection( identifier, owner ) - ?? Penumbra.CollectionManager.Default; - - return IdentifiedCache.Set( collection, identifier, gameObject ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error identifying collection:\n{e}" ); - return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); - } - } - - // Get the collection applying to the current player character - // or the default collection if no player exists. - public static ModCollection PlayerCollection() - { - using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - var gameObject = ( GameObject* )DalamudServices.SObjects.GetObjectAddress( 0 ); - if( gameObject == null ) - { - return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? Penumbra.CollectionManager.Default; - } - - var player = Penumbra.Actors.GetCurrentPlayer(); - return CollectionByIdentifier( player ) - ?? CheckYourself( player, gameObject ) - ?? CollectionByAttributes( gameObject ) - ?? Penumbra.CollectionManager.Default; - } - - // Check both temporary and permanent character collections. Temporary first. - private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) - => Penumbra.TempCollections.Collections.TryGetCollection( identifier, out var collection ) - || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) - ? collection - : null; - - - // Check for the Yourself collection. - private static ModCollection? CheckYourself( ActorIdentifier identifier, GameObject* actor ) - { - if( actor->ObjectIndex == 0 - || Cutscenes.GetParentIndex( actor->ObjectIndex ) == 0 - || identifier.Equals( Penumbra.Actors.GetCurrentPlayer() ) ) - { - return Penumbra.CollectionManager.ByType( CollectionType.Yourself ); - } - - return null; - } - - // Check special collections given the actor. - private static ModCollection? CollectionByAttributes( GameObject* actor ) - { - if( !actor->IsCharacter() ) - { - return null; - } - - // Only handle human models. - var character = ( Character* )actor; - if( character->ModelCharaId >= 0 && character->ModelCharaId < ValidHumanModels.Count && ValidHumanModels[ character->ModelCharaId ] ) - { - var bodyType = character->CustomizeData[2]; - var collection = bodyType switch - { - 3 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerElderly ), - 4 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerChild ), - _ => null, - }; - if( collection != null ) - return collection; - - var race = ( SubRace )character->CustomizeData[ 4 ]; - var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); - var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; - - var type = CollectionTypeExtensions.FromParts( race, gender, isNpc ); - collection = Penumbra.CollectionManager.ByType( type ); - collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) ); - return collection; - } - - return null; - } - - // Get the collection applying to the owner if it is available. - private static ModCollection? CheckOwnedCollection( ActorIdentifier identifier, GameObject* owner ) - { - if( identifier.Type != IdentifierType.Owned || !Penumbra.Config.UseOwnerNameForCharacterCollection || owner == null ) - { - return null; - } - - var id = Penumbra.Actors.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); - return CheckYourself( id, owner ) - ?? CollectionByAttributes( owner ); - } - - /// - /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. - /// - private static BitArray GetValidHumanModels( DataManager gameData ) - { - var sheet = gameData.GetExcelSheet< ModelChara >()!; - var ret = new BitArray( ( int )sheet.RowCount, false ); - foreach( var (_, idx) in sheet.WithIndex().Where( p => p.Value.Type == ( byte )CharacterBase.ModelType.Human ) ) - { - ret[ idx ] = true; - } - - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs deleted file mode 100644 index 2017b1d2..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Linq; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Classes; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.Util; -using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; -using static Penumbra.GameData.Enums.GenderRace; - -namespace Penumbra.Interop.Resolver; - -// State: 6.08 Hotfix. -// GetSlotEqpData seems to be the only function using the EQP table. -// It is only called by CheckSlotsForUnload (called by UpdateModels), -// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete) -// and a unnamed function called by UpdateRender. -// It seems to be enough to change the EQP entries for UpdateModels. - -// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables. -// They are called by ResolveMdlPath, UpdateModels and SetupConnectorModelAttributes, -// which is called by SetupModelAttributes, which is called by OnModelLoadComplete and UpdateModels. -// It seems to be enough to change EQDP on UpdateModels and ResolveMDLPath. - -// EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by -// ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath. - -// RSP height entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF" -// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05" -// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" -// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, -// ChangeCustomize and RspSetupCharacter, which is hooked here. - -// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. -public unsafe partial class PathResolver -{ - public class MetaState : IDisposable - { - public MetaState( IntPtr* humanVTable ) - { - SignatureHelper.Initialise( this ); - _onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour ); - } - - public void Enable() - { - _getEqpIndirectHook.Enable(); - _updateModelsHook.Enable(); - _onModelLoadCompleteHook.Enable(); - _setupVisorHook.Enable(); - _rspSetupCharacterHook.Enable(); - _changeCustomize.Enable(); - } - - public void Disable() - { - _getEqpIndirectHook.Disable(); - _updateModelsHook.Disable(); - _onModelLoadCompleteHook.Disable(); - _setupVisorHook.Disable(); - _rspSetupCharacterHook.Disable(); - _changeCustomize.Disable(); - } - - public void Dispose() - { - _getEqpIndirectHook.Dispose(); - _updateModelsHook.Dispose(); - _onModelLoadCompleteHook.Dispose(); - _setupVisorHook.Dispose(); - _rspSetupCharacterHook.Dispose(); - _changeCustomize.Dispose(); - } - - private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject ); - private readonly Hook< OnModelLoadCompleteDelegate > _onModelLoadCompleteHook; - - private void OnModelLoadCompleteDetour( IntPtr drawObject ) - { - var collection = GetResolveData( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); - _onModelLoadCompleteHook.Original.Invoke( drawObject ); - } - - private delegate void UpdateModelDelegate( IntPtr drawObject ); - - [Signature( Sigs.UpdateModel, DetourName = nameof( UpdateModelsDetour ) )] - private readonly Hook< UpdateModelDelegate > _updateModelsHook = null!; - - private void UpdateModelsDetour( IntPtr drawObject ) - { - // Shortcut because this is called all the time. - // Same thing is checked at the beginning of the original function. - if( *( int* )( drawObject + Offsets.UpdateModelSkip ) == 0 ) - { - return; - } - - using var performance = Penumbra.Performance.Measure( PerformanceType.UpdateModels ); - - var collection = GetResolveData( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); - _updateModelsHook.Original.Invoke( drawObject ); - } - - private static GenderRace GetDrawObjectGenderRace( IntPtr drawObject ) - { - var draw = ( DrawObject* )drawObject; - if( draw->Object.GetObjectType() == ObjectType.CharacterBase ) - { - var c = ( CharacterBase* )drawObject; - if( c->GetModelType() == CharacterBase.ModelType.Human ) - { - return GetHumanGenderRace( drawObject ); - } - } - - return Unknown; - } - - public static GenderRace GetHumanGenderRace( IntPtr human ) - => ( GenderRace )( ( Human* )human )->RaceSexId; - - [Signature( Sigs.GetEqpIndirect, DetourName = nameof( GetEqpIndirectDetour ) )] - private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!; - - private void GetEqpIndirectDetour( IntPtr drawObject ) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if( ( *( byte* )( drawObject + Offsets.GetEqpIndirectSkip1 ) & 1 ) == 0 || *( ulong* )( drawObject + Offsets.GetEqpIndirectSkip2 ) == 0 ) - { - return; - } - - using var performance = Penumbra.Performance.Measure( PerformanceType.GetEqp ); - var resolveData = GetResolveData( drawObject ); - using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); - _getEqpIndirectHook.Original( drawObject ); - } - - - // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, - // but it only applies a changed gmp file after a redraw for some reason. - private delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState ); - - [Signature( Sigs.SetupVisor, DetourName = nameof( SetupVisorDetour ) )] - private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!; - - private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.SetupVisor ); - var resolveData = GetResolveData( drawObject ); - using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(); - return _setupVisorHook.Original( drawObject, modelId, visorState ); - } - - // RSP - private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ); - - [Signature( Sigs.RspSetupCharacter, DetourName = nameof( RspSetupCharacterDetour ) )] - private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!; - - private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 ) - { - if( _inChangeCustomize ) - { - _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); - } - else - { - using var performance = Penumbra.Performance.Measure( PerformanceType.SetupCharacter ); - var resolveData = GetResolveData( drawObject ); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); - _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); - } - } - - // ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. - private bool _inChangeCustomize; - private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment ); - - [Signature( Sigs.ChangeCustomize, DetourName = nameof( ChangeCustomizeDetour ) )] - private readonly Hook< ChangeCustomizeDelegate > _changeCustomize = null!; - - private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment ) - { - using var performance = Penumbra.Performance.Measure( PerformanceType.ChangeCustomize ); - _inChangeCustomize = true; - var resolveData = GetResolveData( human ); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); - using var decals = new CharacterUtility.DecalReverter( Penumbra.ResourceService, resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); - var ret = _changeCustomize.Original( human, data, skipEquipment ); - _inChangeCustomize = false; - return ret; - } - - public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory ) - { - var races = race.Dependencies(); - if( races.Length == 0 ) - { - return DisposableContainer.Empty; - } - - var equipmentEnumerable = equipment - ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) - : Array.Empty< IDisposable? >().AsEnumerable(); - var accessoryEnumerable = accessory - ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) - : Array.Empty< IDisposable? >().AsEnumerable(); - return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs deleted file mode 100644 index 1cf2b206..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using Dalamud.Utility.Signatures; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.String; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - public class PathState : IDisposable - { - [Signature( Sigs.HumanVTable, ScanType = ScanType.StaticAddress )] - public readonly IntPtr* HumanVTable = null!; - - [Signature( Sigs.WeaponVTable, ScanType = ScanType.StaticAddress )] - private readonly IntPtr* _weaponVTable = null!; - - [Signature( Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress )] - private readonly IntPtr* _demiHumanVTable = null!; - - [Signature( Sigs.MonsterVTable, ScanType = ScanType.StaticAddress )] - private readonly IntPtr* _monsterVTable = null!; - - private readonly ResolverHooks _human; - private readonly ResolverHooks _weapon; - private readonly ResolverHooks _demiHuman; - private readonly ResolverHooks _monster; - - // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new(); - - private readonly ThreadLocal _resolveData = new ThreadLocal(() => ResolveData.Invalid, true); - - public PathState( PathResolver parent ) - { - SignatureHelper.Initialise( this ); - _human = new ResolverHooks( parent, HumanVTable, ResolverHooks.Type.Human ); - _weapon = new ResolverHooks( parent, _weaponVTable, ResolverHooks.Type.Weapon ); - _demiHuman = new ResolverHooks( parent, _demiHumanVTable, ResolverHooks.Type.Other ); - _monster = new ResolverHooks( parent, _monsterVTable, ResolverHooks.Type.Other ); - } - - public void Enable() - { - _human.Enable(); - _weapon.Enable(); - _demiHuman.Enable(); - _monster.Enable(); - } - - public void Disable() - { - _human.Disable(); - _weapon.Disable(); - _demiHuman.Disable(); - _monster.Disable(); - } - - public void Dispose() - { - _resolveData.Dispose(); - _human.Dispose(); - _weapon.Dispose(); - _demiHuman.Dispose(); - _monster.Dispose(); - } - - public int Count - => _pathCollections.Count; - - public IEnumerable< KeyValuePair< ByteString, ResolveData > > Paths - => _pathCollections; - - public bool TryGetValue( ByteString path, out ResolveData collection ) - => _pathCollections.TryGetValue( path, out collection ); - - public bool Consume( ByteString path, out ResolveData collection ) - { - collection = _resolveData.IsValueCreated && _resolveData.Value.Valid ? _resolveData.Value : ResolveData.Invalid; - return _pathCollections.TryRemove(path, out collection); - } - - // Just add or remove the resolved path. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - public IntPtr ResolvePath( IntPtr gameObject, ModCollection collection, IntPtr path ) - { - if( path == IntPtr.Zero ) - { - return path; - } - - var gamePath = new ByteString( ( byte* )path ); - SetCollection( gameObject, gamePath, collection ); - _resolveData.Value = collection.ToResolveData(gameObject); - return path; - } - - // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection ) - { - if( _pathCollections.ContainsKey( path ) || path.IsOwned ) - { - _pathCollections[ path ] = collection.ToResolveData( gameObject ); - } - else - { - _pathCollections[ path.Clone() ] = collection.ToResolveData( gameObject ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs deleted file mode 100644 index 1568b363..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Classes; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Interop.Resolver; - -public partial class PathResolver -{ - public unsafe class ResolverHooks : IDisposable - { - public enum Type - { - Human, - Weapon, - Other, - } - - private delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); - private delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ); - private delegate IntPtr MaterialResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ); - private delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 ); - - private readonly Hook< GeneralResolveDelegate > _resolveDecalPathHook; - private readonly Hook< EidResolveDelegate > _resolveEidPathHook; - private readonly Hook< GeneralResolveDelegate > _resolveImcPathHook; - private readonly Hook< MPapResolveDelegate > _resolveMPapPathHook; - private readonly Hook< GeneralResolveDelegate > _resolveMdlPathHook; - private readonly Hook< MaterialResolveDelegate > _resolveMtrlPathHook; - private readonly Hook< MaterialResolveDelegate > _resolvePapPathHook; - private readonly Hook< GeneralResolveDelegate > _resolvePhybPathHook; - private readonly Hook< GeneralResolveDelegate > _resolveSklbPathHook; - private readonly Hook< GeneralResolveDelegate > _resolveSkpPathHook; - private readonly Hook< EidResolveDelegate > _resolveTmbPathHook; - private readonly Hook< MaterialResolveDelegate > _resolveVfxPathHook; - - private readonly PathResolver _parent; - - public ResolverHooks( PathResolver parent, IntPtr* vTable, Type type ) - { - _parent = parent; - _resolveDecalPathHook = Create< GeneralResolveDelegate >( vTable[ 83 ], type, ResolveDecalWeapon, ResolveDecal ); - _resolveEidPathHook = Create< EidResolveDelegate >( vTable[ 85 ], type, ResolveEidWeapon, ResolveEid ); - _resolveImcPathHook = Create< GeneralResolveDelegate >( vTable[ 81 ], type, ResolveImcWeapon, ResolveImc ); - _resolveMPapPathHook = Create< MPapResolveDelegate >( vTable[ 79 ], type, ResolveMPapWeapon, ResolveMPap ); - _resolveMdlPathHook = Create< GeneralResolveDelegate >( vTable[ 73 ], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman ); - _resolveMtrlPathHook = Create< MaterialResolveDelegate >( vTable[ 82 ], type, ResolveMtrlWeapon, ResolveMtrl ); - _resolvePapPathHook = Create< MaterialResolveDelegate >( vTable[ 76 ], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman ); - _resolvePhybPathHook = Create< GeneralResolveDelegate >( vTable[ 75 ], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman ); - _resolveSklbPathHook = Create< GeneralResolveDelegate >( vTable[ 72 ], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman ); - _resolveSkpPathHook = Create< GeneralResolveDelegate >( vTable[ 74 ], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman ); - _resolveTmbPathHook = Create< EidResolveDelegate >( vTable[ 77 ], type, ResolveTmbWeapon, ResolveTmb ); - _resolveVfxPathHook = Create< MaterialResolveDelegate >( vTable[ 84 ], type, ResolveVfxWeapon, ResolveVfx ); - } - - public void Enable() - { - _resolveDecalPathHook.Enable(); - _resolveEidPathHook.Enable(); - _resolveImcPathHook.Enable(); - _resolveMPapPathHook.Enable(); - _resolveMdlPathHook.Enable(); - _resolveMtrlPathHook.Enable(); - _resolvePapPathHook.Enable(); - _resolvePhybPathHook.Enable(); - _resolveSklbPathHook.Enable(); - _resolveSkpPathHook.Enable(); - _resolveTmbPathHook.Enable(); - _resolveVfxPathHook.Enable(); - } - - public void Disable() - { - _resolveDecalPathHook.Disable(); - _resolveEidPathHook.Disable(); - _resolveImcPathHook.Disable(); - _resolveMPapPathHook.Disable(); - _resolveMdlPathHook.Disable(); - _resolveMtrlPathHook.Disable(); - _resolvePapPathHook.Disable(); - _resolvePhybPathHook.Disable(); - _resolveSklbPathHook.Disable(); - _resolveSkpPathHook.Disable(); - _resolveTmbPathHook.Disable(); - _resolveVfxPathHook.Disable(); - } - - public void Dispose() - { - _resolveDecalPathHook.Dispose(); - _resolveEidPathHook.Dispose(); - _resolveImcPathHook.Dispose(); - _resolveMPapPathHook.Dispose(); - _resolveMdlPathHook.Dispose(); - _resolveMtrlPathHook.Dispose(); - _resolvePapPathHook.Dispose(); - _resolvePhybPathHook.Dispose(); - _resolveSklbPathHook.Dispose(); - _resolveSkpPathHook.Dispose(); - _resolveTmbPathHook.Dispose(); - _resolveVfxPathHook.Dispose(); - } - - private IntPtr ResolveDecal( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveEid( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveImc( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMPap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolvePath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMdl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - => ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); - - private IntPtr ResolveMtrl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolvePap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolvePhyb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveSklb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveSkp( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveTmb( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolvePath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveVfx( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolvePath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - - private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - { - DisposableContainer Get() - { - if( modelType > 9 ) - { - return DisposableContainer.Empty; - } - - var data = GetResolveData( drawObject ); - return MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4); - } - - using var eqdp = Get(); - return ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); - } - - private IntPtr ResolvePapHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - { - using var est = GetEstChanges( drawObject ); - return ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - } - - private IntPtr ResolvePhybHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = GetEstChanges( drawObject ); - return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); - } - - private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = GetEstChanges( drawObject ); - return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); - } - - private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - using var est = GetEstChanges( drawObject ); - return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); - } - - private static DisposableContainer GetEstChanges( IntPtr drawObject ) - { - var data = GetResolveData( drawObject ); - return new DisposableContainer( data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Face ), - data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Body ), - data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Hair ), - data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Head ) ); - } - - private IntPtr ResolveDecalWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveEidWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolveWeaponPath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveImcWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveMPapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 ) - => ResolveWeaponPath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolveMdlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) - => ResolveWeaponPath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); - - private IntPtr ResolveMtrlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolvePapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - private IntPtr ResolvePhybWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveSklbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveSkpWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - => ResolveWeaponPath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); - - private IntPtr ResolveTmbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 ) - => ResolveWeaponPath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) ); - - private IntPtr ResolveVfxWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) - => ResolveWeaponPath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); - - - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other, T human ) where T : Delegate - { - var del = type switch - { - Type.Human => human, - Type.Weapon => weapon, - _ => other, - }; - return Hook< T >.FromAddress( address, del ); - } - - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other ) where T : Delegate - => Create( address, type, weapon, other, other ); - - - // Implementation - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePath( IntPtr drawObject, IntPtr path ) - => _parent._paths.ResolvePath( ( IntPtr? )FindParent( drawObject, out _ ) ?? IntPtr.Zero, - FindParent( drawObject, out var collection ) == null - ? Penumbra.CollectionManager.Default - : collection.ModCollection, path ); - - // Weapons have the characters DrawObject as a parent, - // but that may not be set yet when creating a new object, so we have to do the same detour - // as for Human DrawObjects that are just being created. - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolveWeaponPath( IntPtr drawObject, IntPtr path ) - { - var parent = FindParent( drawObject, out var collection ); - if( parent != null ) - { - return _parent._paths.ResolvePath( ( IntPtr )parent, collection.ModCollection, path ); - } - - var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; - var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject ); - if( parentCollection.Valid ) - { - return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path ); - } - - parent = FindParent( parentObject, out collection ); - return _parent._paths.ResolvePath( ( IntPtr? )parent ?? IntPtr.Zero, parent == null - ? Penumbra.CollectionManager.Default - : collection.ModCollection, path ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs deleted file mode 100644 index da976f0b..00000000 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Interop.Resolver; - -public unsafe partial class PathResolver -{ - // Materials and avfx do contain their own paths to textures and shader packages or atex respectively. - // Those are loaded synchronously. - // Thus, we need to ensure the correct files are loaded when a material is loaded. - public class SubfileHelper : IDisposable, IReadOnlyCollection> - { - private readonly ResourceLoader _loader; - private readonly GameEventManager _events; - - private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); - private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); - - private readonly ConcurrentDictionary _subFileCollection = new(); - - public SubfileHelper(ResourceLoader loader, GameEventManager events) - { - SignatureHelper.Initialise(this); - - _loader = loader; - _events = events; - } - - // Check specifically for shpk and tex files whether we are currently in a material load. - public bool HandleSubFiles(ResourceType type, out ResolveData collection) - { - switch (type) - { - case ResourceType.Tex when _mtrlData.Value.Valid: - case ResourceType.Shpk when _mtrlData.Value.Valid: - collection = _mtrlData.Value; - return true; - case ResourceType.Scd when _avfxData.Value.Valid: - collection = _avfxData.Value; - return true; - case ResourceType.Atex when _avfxData.Value.Valid: - collection = _avfxData.Value; - return true; - } - - collection = ResolveData.Invalid; - return false; - } - - // Materials need to be set per collection so they can load their textures independently from each other. - public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, ResolveData) data) - { - if (nonDefault) - switch (type) - { - case ResourceType.Mtrl: - case ResourceType.Avfx: - var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}"); - data = (fullPath, resolveData); - return; - } - - data = (resolved, resolveData); - } - - public void Enable() - { - _loadMtrlShpkHook.Enable(); - _loadMtrlTexHook.Enable(); - _apricotResourceLoadHook.Enable(); - _loader.ResourceLoaded += SubfileContainerRequested; - _events.ResourceHandleDestructor += ResourceDestroyed; - } - - public void Disable() - { - _loadMtrlShpkHook.Disable(); - _loadMtrlTexHook.Disable(); - _apricotResourceLoadHook.Disable(); - _loader.ResourceLoaded -= SubfileContainerRequested; - _events.ResourceHandleDestructor -= ResourceDestroyed; - } - - public void Dispose() - { - Disable(); - _loadMtrlShpkHook.Dispose(); - _loadMtrlTexHook.Dispose(); - _apricotResourceLoadHook.Dispose(); - } - - private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData) - { - switch (handle->FileType) - { - case ResourceType.Mtrl: - case ResourceType.Avfx: - if (handle->FileSize == 0) - _subFileCollection[(nint)handle] = resolveData; - - break; - } - } - - private void ResourceDestroyed(ResourceHandle* handle) - => _subFileCollection.TryRemove((IntPtr)handle, out _); - - private delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle); - - [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))] - private readonly Hook _loadMtrlTexHook = null!; - - private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.LoadTextures); - var old = _mtrlData.Value; - _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); - var ret = _loadMtrlTexHook.Original(mtrlResourceHandle); - _mtrlData.Value = old; - return ret; - } - - [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))] - private readonly Hook _loadMtrlShpkHook = null!; - - private byte LoadMtrlShpkDetour(IntPtr mtrlResourceHandle) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.LoadShaders); - var old = _mtrlData.Value; - _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); - var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle); - _mtrlData.Value = old; - return ret; - } - - private ResolveData LoadFileHelper(IntPtr resourceHandle) - { - if (resourceHandle == IntPtr.Zero) - return ResolveData.Invalid; - - return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid; - } - - - private delegate byte ApricotResourceLoadDelegate(IntPtr handle, IntPtr unk1, byte unk2); - - [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))] - private readonly Hook _apricotResourceLoadHook = null!; - - - private byte ApricotResourceLoadDetour(IntPtr handle, IntPtr unk1, byte unk2) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.LoadApricotResources); - var old = _avfxData.Value; - _avfxData.Value = LoadFileHelper(handle); - var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2); - _avfxData.Value = old; - return ret; - } - - public IEnumerator> GetEnumerator() - => _subFileCollection.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _subFileCollection.Count; - - internal ResolveData MtrlData - => _mtrlData.Value; - - internal ResolveData AvfxData - => _avfxData.Value; - } -} diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 3b805acd..59992d16 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,206 +1,140 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Dalamud.Game.ClientState; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Services; -using Penumbra.Services; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Interop.Resolver; - -//public class PathResolver2 : IDisposable -//{ -// public readonly CutsceneService Cutscenes; -// public readonly IdentifiedCollectionCache Identified; -// -// public PathResolver(StartTracker timer, CutsceneService cutscenes, IdentifiedCollectionCache identified) -// { -// using var t = timer.Measure(StartTimeType.PathResolver); -// Cutscenes = cutscenes; -// Identified = identified; -// } -//} +using System; +using System.Diagnostics.CodeAnalysis; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.Util; - -// The Path Resolver handles character collections. -// It will hook any path resolving functions for humans, -// as well as DrawObject creation. -// It links draw objects to actors, and actors to character collections, -// to resolve paths for character collections. -public partial class PathResolver : IDisposable -{ - public bool Enabled { get; private set; } - - private readonly CommunicatorService _communicator; - private readonly ResourceLoader _loader; - private static readonly CutsceneService Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO - private static DrawObjectState _drawObjects = null!; // TODO - private static readonly BitArray ValidHumanModels; - internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO - private readonly AnimationState _animations; - private readonly PathState _paths; - private readonly MetaState _meta; - private readonly SubfileHelper _subFiles; - - static PathResolver() - => ValidHumanModels = GetValidHumanModels(DalamudServices.SGameData); - - public unsafe PathResolver(IdentifiedCollectionCache cache, StartTracker timer, ClientState clientState, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) - { - using var tApi = timer.Measure(StartTimeType.PathResolver); - _communicator = communicator; - IdentifiedCache = cache; - SignatureHelper.Initialise(this); - _drawObjects = new DrawObjectState(_communicator); - _loader = loader; - _animations = new AnimationState(_drawObjects); - _paths = new PathState(this); - _meta = new MetaState(_paths.HumanVTable); - _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); - Enable(); - } - - // The modified resolver that handles game path resolving. - public (FullPath?, ResolveData) CharacterResolver(Utf8GamePath gamePath, ResourceType type) - { - using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver); - // Check if the path was marked for a specific collection, - // or if it is a file loaded by a material, and if we are currently in a material load, - // or if it is a face decal path and the current mod collection is set. - // If not use the default collection. - // We can remove paths after they have actually been loaded. - // A potential next request will add the path anew. - var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData) - || _paths.Consume(gamePath.Path, out resolveData) - || _animations.HandleFiles(type, gamePath, out resolveData) - || _drawObjects.HandleDecalFile(type, gamePath, out resolveData); - if (!nonDefault || !resolveData.Valid) - resolveData = Penumbra.CollectionManager.Default.ToResolveData(); - - // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = resolveData.ModCollection.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. - // We also need to handle defaulted materials against a non-default collection. - var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); - return pair; - } - - public void Enable() - { - if (Enabled) - return; - - Enabled = true; - Cutscenes.Enable(); - _drawObjects.Enable(); - IdentifiedCache.Enable(); - _animations.Enable(); - _paths.Enable(); - _meta.Enable(); - _subFiles.Enable(); - - Penumbra.Log.Debug("Character Path Resolver enabled."); - } - - public void Disable() - { - if (!Enabled) - return; - - Enabled = false; - _animations.Disable(); - _drawObjects.Disable(); - Cutscenes.Disable(); - IdentifiedCache.Disable(); - _paths.Disable(); - _meta.Disable(); - _subFiles.Disable(); - - Penumbra.Log.Debug("Character Path Resolver disabled."); - } - - public void Dispose() - { - Disable(); - _paths.Dispose(); - _animations.Dispose(); - _drawObjects.Dispose(); - Cutscenes.Dispose(); - IdentifiedCache.Dispose(); - _meta.Dispose(); - _subFiles.Dispose(); - } - - public static unsafe (IntPtr, ResolveData) IdentifyDrawObject(IntPtr drawObject) - { - var parent = FindParent(drawObject, out var resolveData); - return ((IntPtr)parent, resolveData); - } - - public int CutsceneActor(int idx) - => Cutscenes.GetParentIndex(idx); - - // Use the stored information to find the GameObject and Collection linked to a DrawObject. - public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData) - { - if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject)) - { - resolveData = data.Item1; - return gameObject; - } - - if (_drawObjects.LastGameObject != null - && (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject)) - { - resolveData = IdentifyCollection(_drawObjects.LastGameObject, true); - return _drawObjects.LastGameObject; - } - - resolveData = IdentifyCollection(null, true); - return null; - } - - private static unsafe ResolveData GetResolveData(IntPtr drawObject) - { - var _ = FindParent(drawObject, out var resolveData); - return resolveData; - } - - internal IEnumerable> PathCollections - => _paths.Paths; - - internal IEnumerable> DrawObjectMap - => _drawObjects.DrawObjects; - - internal IEnumerable> CutsceneActors - => Cutscenes.Actors; - - internal IEnumerable> ResourceCollections - => _subFiles; - - internal int SubfileCount - => _subFiles.Count; - - internal ResolveData CurrentMtrlData - => _subFiles.MtrlData; - - internal ResolveData CurrentAvfxData - => _subFiles.AvfxData; - - internal ResolveData LastGameObjectData - => _drawObjects.LastCreatedCollection; - - internal unsafe nint LastGameObject - => (nint)_drawObjects.LastGameObject; -} +namespace Penumbra.Interop.Resolver; + +public class PathResolver : IDisposable +{ + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly TempCollectionManager _tempCollections; + private readonly ResourceLoader _loader; + + private readonly AnimationHookService _animationHookService; + private readonly SubfileHelper _subfileHelper; + private readonly PathState _pathState; + private readonly MetaState _metaState; + + public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, + TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, + PathState pathState, MetaState metaState) + { + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _tempCollections = tempCollections; + _animationHookService = animationHookService; + _subfileHelper = subfileHelper; + _pathState = pathState; + _metaState = metaState; + _loader = loader; + _loader.ResolvePath = ResolvePath; + _loader.FileLoaded += ImcLoadResource; + } + + /// Obtain a temporary or permanent collection by name. + public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection); + + /// Try to resolve the given game path to the replaced path. + public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) + { + // Check if mods are enabled or if we are in a inc-ref at 0 reference count situation. + if (!_config.EnableMods) + return (null, ResolveData.Invalid); + + path = path.ToLower(); + return category switch + { + // Only Interface collection. + ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path), + _collectionManager.Interface.ToResolveData()), + // Never allow changing scripts. + ResourceCategory.UiScript => (null, ResolveData.Invalid), + ResourceCategory.GameScript => (null, ResolveData.Invalid), + // Use actual resolving. + ResourceCategory.Chara => Resolve(path, resourceType), + ResourceCategory.Shader => Resolve(path, resourceType), + ResourceCategory.Vfx => Resolve(path, resourceType), + ResourceCategory.Sound => Resolve(path, resourceType), + // None of these files are ever associated with specific characters, + // always use the default resolver for now. + ResourceCategory.Common => DefaultResolver(path), + ResourceCategory.BgCommon => DefaultResolver(path), + ResourceCategory.Bg => DefaultResolver(path), + ResourceCategory.Cut => DefaultResolver(path), + ResourceCategory.Exd => DefaultResolver(path), + ResourceCategory.Music => DefaultResolver(path), + _ => DefaultResolver(path), + }; + } + + public (FullPath?, ResolveData) Resolve(Utf8GamePath gamePath, ResourceType type) + { + using var performance = _performance.Measure(PerformanceType.CharacterResolver); + // Check if the path was marked for a specific collection, + // or if it is a file loaded by a material, and if we are currently in a material load, + // or if it is a face decal path and the current mod collection is set. + // If not use the default collection. + // We can remove paths after they have actually been loaded. + // A potential next request will add the path anew. + var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData) + || _pathState.Consume(gamePath.Path, out resolveData) + || _animationHookService.HandleFiles(type, gamePath, out resolveData) + || _metaState.HandleDecalFile(type, gamePath, out resolveData); + if (!nonDefault || !resolveData.Valid) + resolveData = _collectionManager.Default.ToResolveData(); + + // Resolve using character/default collection first, otherwise forced, as usual. + var resolved = resolveData.ModCollection.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. + // We also need to handle defaulted materials against a non-default collection. + var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); + return pair; + } + + public unsafe void Dispose() + { + _loader.ResetResolvePath(); + _loader.FileLoaded -= ImcLoadResource; + } + + /// Use the default method of path replacement. + private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) + { + var resolved = _collectionManager.Default.ResolvePath(path); + return (resolved, _collectionManager.Default.ToResolveData()); + } + + /// After loading an IMC file, replace its contents with the modded IMC file. + private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData) + { + if (resource->FileType != ResourceType.Imc) + return; + + var lastUnderscore = additionalData.LastIndexOf((byte)'_'); + var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString(); + if (Utf8GamePath.FromByteString(path, out var gamePath) + && CollectionByName(name, out var collection) + && collection.HasCache + && collection.GetImcFile(gamePath, out var file)) + { + file.Replace(resource); + Penumbra.Log.Verbose( + $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } + } +} diff --git a/Penumbra/Interop/Resolver/PathState.cs b/Penumbra/Interop/Resolver/PathState.cs new file mode 100644 index 00000000..a3a4064c --- /dev/null +++ b/Penumbra/Interop/Resolver/PathState.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.String; + +namespace Penumbra.Interop.Resolver; + +public unsafe class PathState : IDisposable +{ + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _weaponVTable = null!; + + [Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _demiHumanVTable = null!; + + [Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _monsterVTable = null!; + + public readonly CollectionResolver CollectionResolver; + + private readonly ResolvePathHooks _human; + private readonly ResolvePathHooks _weapon; + private readonly ResolvePathHooks _demiHuman; + private readonly ResolvePathHooks _monster; + + private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); + + public IList CurrentData + => _resolveData.Values; + + public PathState(CollectionResolver collectionResolver) + { + SignatureHelper.Initialise(this); + CollectionResolver = collectionResolver; + _human = new ResolvePathHooks(this, _humanVTable, ResolvePathHooks.Type.Human); + _weapon = new ResolvePathHooks(this, _weaponVTable, ResolvePathHooks.Type.Weapon); + _demiHuman = new ResolvePathHooks(this, _demiHumanVTable, ResolvePathHooks.Type.Other); + _monster = new ResolvePathHooks(this, _monsterVTable, ResolvePathHooks.Type.Other); + _human.Enable(); + _weapon.Enable(); + _demiHuman.Enable(); + _monster.Enable(); + } + + + public void Dispose() + { + _resolveData.Dispose(); + _human.Dispose(); + _weapon.Dispose(); + _demiHuman.Dispose(); + _monster.Dispose(); + } + + public bool Consume(ByteString path, out ResolveData collection) + { + if (_resolveData.IsValueCreated) + { + collection = _resolveData.Value; + _resolveData.Value = ResolveData.Invalid; + return collection.Valid; + } + + collection = ResolveData.Invalid; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public nint ResolvePath(nint gameObject, ModCollection collection, nint path) + { + if (path == nint.Zero) + return path; + + _resolveData.Value = collection.ToResolveData(gameObject); + return path; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public nint ResolvePath(ResolveData data, nint path) + { + if (path == nint.Zero) + return path; + + _resolveData.Value = data; + return path; + } +} diff --git a/Penumbra/Interop/Resolver/ResolvePathHooks.cs b/Penumbra/Interop/Resolver/ResolvePathHooks.cs new file mode 100644 index 00000000..f2945bcc --- /dev/null +++ b/Penumbra/Interop/Resolver/ResolvePathHooks.cs @@ -0,0 +1,249 @@ +using System; +using System.Runtime.CompilerServices; +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Resolver; + +public unsafe class ResolvePathHooks : IDisposable +{ + public enum Type + { + Human, + Weapon, + Other, + } + + private delegate nint GeneralResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4); + private delegate nint MPapResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, uint unk5); + private delegate nint MaterialResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5); + private delegate nint EidResolveDelegate(nint drawObject, nint path, nint unk3); + + private readonly Hook _resolveDecalPathHook; + private readonly Hook _resolveEidPathHook; + private readonly Hook _resolveImcPathHook; + private readonly Hook _resolveMPapPathHook; + private readonly Hook _resolveMdlPathHook; + private readonly Hook _resolveMtrlPathHook; + private readonly Hook _resolvePapPathHook; + private readonly Hook _resolvePhybPathHook; + private readonly Hook _resolveSklbPathHook; + private readonly Hook _resolveSkpPathHook; + private readonly Hook _resolveTmbPathHook; + private readonly Hook _resolveVfxPathHook; + + private readonly PathState _parent; + + public ResolvePathHooks(PathState parent, nint* vTable, Type type) + { + _parent = parent; + _resolveDecalPathHook = Create(vTable[83], type, ResolveDecalWeapon, ResolveDecal); + _resolveEidPathHook = Create(vTable[85], type, ResolveEidWeapon, ResolveEid); + _resolveImcPathHook = Create(vTable[81], type, ResolveImcWeapon, ResolveImc); + _resolveMPapPathHook = Create(vTable[79], type, ResolveMPapWeapon, ResolveMPap); + _resolveMdlPathHook = Create(vTable[73], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman); + _resolveMtrlPathHook = Create(vTable[82], type, ResolveMtrlWeapon, ResolveMtrl); + _resolvePapPathHook = Create(vTable[76], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman); + _resolvePhybPathHook = Create(vTable[75], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman); + _resolveSklbPathHook = Create(vTable[72], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman); + _resolveSkpPathHook = Create(vTable[74], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman); + _resolveTmbPathHook = Create(vTable[77], type, ResolveTmbWeapon, ResolveTmb); + _resolveVfxPathHook = Create(vTable[84], type, ResolveVfxWeapon, ResolveVfx); + } + + public void Enable() + { + _resolveDecalPathHook.Enable(); + _resolveEidPathHook.Enable(); + _resolveImcPathHook.Enable(); + _resolveMPapPathHook.Enable(); + _resolveMdlPathHook.Enable(); + _resolveMtrlPathHook.Enable(); + _resolvePapPathHook.Enable(); + _resolvePhybPathHook.Enable(); + _resolveSklbPathHook.Enable(); + _resolveSkpPathHook.Enable(); + _resolveTmbPathHook.Enable(); + _resolveVfxPathHook.Enable(); + } + + public void Disable() + { + _resolveDecalPathHook.Disable(); + _resolveEidPathHook.Disable(); + _resolveImcPathHook.Disable(); + _resolveMPapPathHook.Disable(); + _resolveMdlPathHook.Disable(); + _resolveMtrlPathHook.Disable(); + _resolvePapPathHook.Disable(); + _resolvePhybPathHook.Disable(); + _resolveSklbPathHook.Disable(); + _resolveSkpPathHook.Disable(); + _resolveTmbPathHook.Disable(); + _resolveVfxPathHook.Disable(); + } + + public void Dispose() + { + _resolveDecalPathHook.Dispose(); + _resolveEidPathHook.Dispose(); + _resolveImcPathHook.Dispose(); + _resolveMPapPathHook.Dispose(); + _resolveMdlPathHook.Dispose(); + _resolveMtrlPathHook.Dispose(); + _resolvePapPathHook.Dispose(); + _resolvePhybPathHook.Dispose(); + _resolveSklbPathHook.Dispose(); + _resolveSkpPathHook.Dispose(); + _resolveTmbPathHook.Dispose(); + _resolveVfxPathHook.Dispose(); + } + + private nint ResolveDecal(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveEid(nint drawObject, nint path, nint unk3) + => ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3)); + + private nint ResolveImc(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveMPap(nint drawObject, nint path, nint unk3, uint unk4, uint unk5) + => ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolveMdl(nint drawObject, nint path, nint unk3, uint modelType) + => ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); + + private nint ResolveMtrl(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolvePap(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolvePhyb(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveSklb(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveSkp(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveTmb(nint drawObject, nint path, nint unk3) + => ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3)); + + private nint ResolveVfx(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + + private nint ResolveMdlHuman(nint drawObject, nint path, nint unk3, uint modelType) + { + var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + using var eqdp = modelType > 9 + ? DisposableContainer.Empty + : MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), modelType < 5, modelType > 4); + return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); + } + + private nint ResolvePapHuman(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + { + using var est = GetEstChanges(drawObject, out var data); + return ResolvePath(data, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + } + + private nint ResolvePhybHuman(nint drawObject, nint path, nint unk3, uint unk4) + { + using var est = GetEstChanges(drawObject, out var data); + return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); + } + + private nint ResolveSklbHuman(nint drawObject, nint path, nint unk3, uint unk4) + { + using var est = GetEstChanges(drawObject, out var data); + return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); + } + + private nint ResolveSkpHuman(nint drawObject, nint path, nint unk3, uint unk4) + { + using var est = GetEstChanges(drawObject, out var data); + return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); + } + + private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) + { + data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Face), + data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Body), + data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Hair), + data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Head)); + } + + private nint ResolveDecalWeapon(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveEidWeapon(nint drawObject, nint path, nint unk3) + => ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3)); + + private nint ResolveImcWeapon(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveMPapWeapon(nint drawObject, nint path, nint unk3, uint unk4, uint unk5) + => ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolveMdlWeapon(nint drawObject, nint path, nint unk3, uint modelType) + => ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); + + private nint ResolveMtrlWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolvePapWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + private nint ResolvePhybWeapon(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveSklbWeapon(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveSkpWeapon(nint drawObject, nint path, nint unk3, uint unk4) + => ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); + + private nint ResolveTmbWeapon(nint drawObject, nint path, nint unk3) + => ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3)); + + private nint ResolveVfxWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + => ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5)); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static Hook Create(nint address, Type type, T weapon, T other, T human) where T : Delegate + { + var del = type switch + { + Type.Human => human, + Type.Weapon => weapon, + _ => other, + }; + return Hook.FromAddress(address, del); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static Hook Create(nint address, Type type, T weapon, T other) where T : Delegate + => Create(address, type, weapon, other, other); + + + // Implementation + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint ResolvePath(nint drawObject, nint path) + { + var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(data, path); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint ResolvePath(ResolveData data, nint path) + => _parent.ResolvePath(data, path); +} diff --git a/Penumbra/Interop/Resolver/SubfileHelper.cs b/Penumbra/Interop/Resolver/SubfileHelper.cs new file mode 100644 index 00000000..323ca9c1 --- /dev/null +++ b/Penumbra/Interop/Resolver/SubfileHelper.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Interop.Resolver; + +/// +/// Materials and avfx do contain their own paths to textures and shader packages or atex respectively. +/// Those are loaded synchronously. +/// Thus, we need to ensure the correct files are loaded when a material is loaded. +/// +public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> +{ + private readonly PerformanceTracker _performance; + private readonly ResourceLoader _loader; + private readonly GameEventManager _events; + + private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); + private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); + + private readonly ConcurrentDictionary _subFileCollection = new(); + + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events) + { + SignatureHelper.Initialise(this); + + _performance = performance; + _loader = loader; + _events = events; + + _loadMtrlShpkHook.Enable(); + _loadMtrlTexHook.Enable(); + _apricotResourceLoadHook.Enable(); + _loader.ResourceLoaded += SubfileContainerRequested; + _events.ResourceHandleDestructor += ResourceDestroyed; + } + + + public IEnumerator> GetEnumerator() + => _subFileCollection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _subFileCollection.Count; + + public ResolveData MtrlData + => _mtrlData.IsValueCreated ? _mtrlData.Value : ResolveData.Invalid; + + public ResolveData AvfxData + => _avfxData.IsValueCreated ? _avfxData.Value : ResolveData.Invalid; + + /// + /// Check specifically for shpk and tex files whether we are currently in a material load, + /// and for scd and atex files whether we are in an avfx load. + public bool HandleSubFiles(ResourceType type, out ResolveData collection) + { + switch (type) + { + case ResourceType.Tex when _mtrlData.Value.Valid: + case ResourceType.Shpk when _mtrlData.Value.Valid: + collection = _mtrlData.Value; + return true; + case ResourceType.Scd when _avfxData.Value.Valid: + case ResourceType.Atex when _avfxData.Value.Valid: + collection = _avfxData.Value; + return true; + } + + collection = ResolveData.Invalid; + return false; + } + + /// Materials and AVFX need to be set per collection so they can load their textures independently from each other. + public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, ResolveData) data) + { + if (nonDefault) + switch (type) + { + case ResourceType.Mtrl: + case ResourceType.Avfx: + var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}"); + data = (fullPath, resolveData); + return; + } + + data = (resolved, resolveData); + } + + public void Dispose() + { + _loader.ResourceLoaded -= SubfileContainerRequested; + _events.ResourceHandleDestructor -= ResourceDestroyed; + _loadMtrlShpkHook.Dispose(); + _loadMtrlTexHook.Dispose(); + _apricotResourceLoadHook.Dispose(); + } + + private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData) + { + switch (handle->FileType) + { + case ResourceType.Mtrl: + case ResourceType.Avfx: + if (handle->FileSize == 0) + _subFileCollection[(nint)handle] = resolveData; + + break; + } + } + + private void ResourceDestroyed(ResourceHandle* handle) + => _subFileCollection.TryRemove((nint)handle, out _); + + private delegate byte LoadMtrlFilesDelegate(nint mtrlResourceHandle); + + [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))] + private readonly Hook _loadMtrlTexHook = null!; + + private byte LoadMtrlTexDetour(nint mtrlResourceHandle) + { + using var performance = _performance.Measure(PerformanceType.LoadTextures); + var last = _mtrlData.Value; + _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); + var ret = _loadMtrlTexHook.Original(mtrlResourceHandle); + _mtrlData.Value = last; + return ret; + } + + [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))] + private readonly Hook _loadMtrlShpkHook = null!; + + private byte LoadMtrlShpkDetour(nint mtrlResourceHandle) + { + using var performance = _performance.Measure(PerformanceType.LoadShaders); + var last = _mtrlData.Value; + _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); + var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle); + _mtrlData.Value = last; + return ret; + } + + private ResolveData LoadFileHelper(nint resourceHandle) + { + if (resourceHandle == nint.Zero) + return ResolveData.Invalid; + + return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid; + } + + + private delegate byte ApricotResourceLoadDelegate(nint handle, nint unk1, byte unk2); + + [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))] + private readonly Hook _apricotResourceLoadHook = null!; + + private byte ApricotResourceLoadDetour(nint handle, nint unk1, byte unk2) + { + using var performance = _performance.Measure(PerformanceType.LoadApricotResources); + var last = _avfxData.Value; + _avfxData.Value = LoadFileHelper(handle); + var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2); + _avfxData.Value = last; + return ret; + } +} diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index e6f84f53..ed28f5e7 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -4,21 +4,32 @@ using Penumbra.GameData; using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using OtterGui.Log; using Penumbra.Interop.Structs; - + namespace Penumbra.Interop.Services; public unsafe class GameEventManager : IDisposable { private const string Prefix = $"[{nameof(GameEventManager)}]"; + public event CharacterDestructorEvent? CharacterDestructor; + public event CopyCharacterEvent? CopyCharacter; + public event ResourceHandleDestructorEvent? ResourceHandleDestructor; + public event CreatingCharacterBaseEvent? CreatingCharacterBase; + public event CharacterBaseCreatedEvent? CharacterBaseCreated; + public event CharacterBaseDestructorEvent? CharacterBaseDestructor; + public event WeaponReloadingEvent? WeaponReloading; + public event WeaponReloadedEvent? WeaponReloaded; + public GameEventManager() { SignatureHelper.Initialise(this); _characterDtorHook.Enable(); _copyCharacterHook.Enable(); _resourceHandleDestructorHook.Enable(); + _characterBaseCreateHook.Enable(); + _characterBaseDestructorHook.Enable(); + _weaponReloadHook.Enable(); Penumbra.Log.Verbose($"{Prefix} Created."); } @@ -27,6 +38,9 @@ public unsafe class GameEventManager : IDisposable _characterDtorHook.Dispose(); _copyCharacterHook.Dispose(); _resourceHandleDestructorHook.Dispose(); + _characterBaseCreateHook.Dispose(); + _characterBaseDestructorHook.Dispose(); + _weaponReloadHook.Dispose(); Penumbra.Log.Verbose($"{Prefix} Disposed."); } @@ -57,13 +71,12 @@ public unsafe class GameEventManager : IDisposable } public delegate void CharacterDestructorEvent(Character* character); - public event CharacterDestructorEvent? CharacterDestructor; #endregion #region Copy Character - private unsafe delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk); + private delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk); [Signature(Sigs.CopyCharacter, DetourName = nameof(CopyCharacterDetour))] private readonly Hook _copyCharacterHook = null!; @@ -90,7 +103,6 @@ public unsafe class GameEventManager : IDisposable } public delegate void CopyCharacterEvent(Character* target, Character* source); - public event CopyCharacterEvent? CopyCharacter; #endregion @@ -122,7 +134,126 @@ public unsafe class GameEventManager : IDisposable } public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); - public event ResourceHandleDestructorEvent? ResourceHandleDestructor; + + #endregion + + #region CharacterBaseCreate + + private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d); + + [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))] + private readonly Hook _characterBaseCreateHook = null!; + + private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d) + { + if (CreatingCharacterBase != null) + foreach (var subscriber in CreatingCharacterBase.GetInvocationList()) + { + try + { + ((CreatingCharacterBaseEvent)subscriber).Invoke(a, b, c); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); + } + } + + var ret = _characterBaseCreateHook.Original(a, b, c, d); + if (CharacterBaseCreated != null) + foreach (var subscriber in CharacterBaseCreated.GetInvocationList()) + { + try + { + ((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); + } + } + + return ret; + } + + public delegate void CreatingCharacterBaseEvent(uint modelCharaId, nint customize, nint equipment); + public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject); + + #endregion + + #region CharacterBase Destructor + + public delegate void CharacterBaseDestructorEvent(nint drawBase); + + [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))] + private readonly Hook _characterBaseDestructorHook = null!; + + private void CharacterBaseDestructorDetour(IntPtr drawBase) + { + if (CharacterBaseDestructor != null) + foreach (var subscriber in CharacterBaseDestructor.GetInvocationList()) + { + try + { + ((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); + } + } + + _characterBaseDestructorHook.Original.Invoke(drawBase); + } + + #endregion + + #region Weapon Reload + + private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7); + + [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))] + private readonly Hook _weaponReloadHook = null!; + + private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7) + { + var gameObject = *(nint*)(a1 + 8); + if (WeaponReloading != null) + foreach (var subscriber in WeaponReloading.GetInvocationList()) + { + try + { + ((WeaponReloadingEvent)subscriber).Invoke(a1, gameObject); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); + } + } + + _weaponReloadHook.Original(a1, a2, a3, a4, a5, a6, a7); + + if (WeaponReloaded != null) + foreach (var subscriber in WeaponReloaded.GetInvocationList()) + { + try + { + ((WeaponReloadedEvent)subscriber).Invoke(a1, gameObject); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); + } + } + } + + public delegate void WeaponReloadingEvent(nint drawDataContainer, nint gameObject); + public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject); #endregion } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c7988881..5ea33c2c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -106,7 +106,11 @@ public class Penumbra : IDalamudPlugin RedrawService = _tmp.Services.GetRequiredService(); ResourceService = _tmp.Services.GetRequiredService(); ResourceLoader = _tmp.Services.GetRequiredService(); - PathResolver = _tmp.Services.GetRequiredService(); + using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) + { + PathResolver = _tmp.Services.GetRequiredService(); + } + SetupInterface(); SetupApi(); @@ -175,7 +179,6 @@ public class Penumbra : IDalamudPlugin Config.EnableMods = enabled; if (enabled) { - PathResolver.Enable(); if (CharacterUtility.Ready) { CollectionManager.Default.SetFiles(); @@ -185,7 +188,6 @@ public class Penumbra : IDalamudPlugin } else { - PathResolver.Disable(); if (CharacterUtility.Ready) { CharacterUtility.ResetAll(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index c74af610..37a099af 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -93,11 +93,20 @@ public class PenumbraNew .AddSingleton() .AddSingleton(); - // Add main services + // Add Resource services services.AddSingleton() - .AddSingleton() - .AddSingleton() .AddSingleton(); + + // Add Path Resolver + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Interface services.AddSingleton() diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 9d14ae16..0e69276a 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -29,10 +29,28 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); + /// + /// Parameter is the game object for which a draw object is created. + /// Parameter is the name of the applied collection. + /// Parameter is a pointer to the model id (an uint). + /// Parameter is a pointer to the customize array. + /// Parameter is a pointer to the equip data array. + /// + public readonly EventWrapper CreatingCharacterBase = new(nameof(CreatingCharacterBase)); + + /// + /// Parameter is the game object for which a draw object is created. + /// Parameter is the name of the applied collection. + /// Parameter is the created draw object. + /// + public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); + public void Dispose() { CollectionChange.Dispose(); TemporaryGlobalModChange.Dispose(); ModMetaChange.Dispose(); + CreatingCharacterBase.Dispose(); + CreatedCharacterBase.Dispose(); } } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 17e3ccea..b20807dc 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.Interop; using ImGuiNET; using OtterGui; using OtterGui.Widgets; @@ -30,42 +31,54 @@ namespace Penumbra.UI.Tabs; public class DebugTab : ITab { - private readonly StartTracker _timer; - private readonly PerformanceTracker _performance; - private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; - private readonly Mod.Manager _modManager; - private readonly ValidityChecker _validityChecker; - private readonly HttpApi _httpApi; - private readonly PathResolver _pathResolver; - private readonly ActorService _actorService; - private readonly DalamudServices _dalamud; - private readonly StainService _stains; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly ResourceManagerService _resourceManager; - private readonly PenumbraIpcProviders _ipc; + private readonly StartTracker _timer; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly Mod.Manager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly ActorService _actorService; + private readonly DalamudServices _dalamud; + private readonly StainService _stains; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly ResourceManagerService _resourceManager; + private readonly PenumbraIpcProviders _ipc; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly PathState _pathState; + private readonly SubfileHelper _subfileHelper; + private readonly IdentifiedCollectionCache _identifiedCollectionCache; + private readonly CutsceneService _cutsceneService; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, - ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, PathResolver pathResolver, ActorService actorService, + ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, - ResourceManagerService resourceManager, PenumbraIpcProviders ipc) + ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, + DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, + CutsceneService cutsceneService) { - _timer = timer; - _performance = performance; - _config = config; - _collectionManager = collectionManager; - _validityChecker = validityChecker; - _modManager = modManager; - _httpApi = httpApi; - _pathResolver = pathResolver; - _actorService = actorService; - _dalamud = dalamud; - _stains = stains; - _characterUtility = characterUtility; - _residentResources = residentResources; - _resourceManager = resourceManager; - _ipc = ipc; + _timer = timer; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _validityChecker = validityChecker; + _modManager = modManager; + _httpApi = httpApi; + _actorService = actorService; + _dalamud = dalamud; + _stains = stains; + _characterUtility = characterUtility; + _residentResources = residentResources; + _resourceManager = resourceManager; + _ipc = ipc; + _collectionResolver = collectionResolver; + _drawObjectState = drawObjectState; + _pathState = pathState; + _subfileHelper = subfileHelper; + _identifiedCollectionCache = identifiedCollectionCache; + _cutsceneService = cutsceneService; } public ReadOnlySpan Label @@ -131,7 +144,6 @@ public class DebugTab : ITab PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); - PrintValue("Path Resolver Enabled", _pathResolver.Enabled.ToString()); PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); } @@ -200,28 +212,33 @@ public class DebugTab : ITab return; ImGui.TextUnformatted( - $"Last Game Object: 0x{_pathResolver.LastGameObject:X} ({_pathResolver.LastGameObjectData.ModCollection.Name})"); + $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Name})"); using (var drawTree = TreeNode("Draw Object to Object")) { if (drawTree) { - using var table = Table("###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (ptr, (c, idx)) in _pathResolver.DrawObjectMap) + foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState + .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) + .ThenBy(kvp => kvp.Value.Item2) + .ThenBy(kvp => kvp.Key)) { + var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); - ImGui.TextUnformatted(ptr.ToString("X")); + ImGui.TextUnformatted($"0x{drawObject:X}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(idx.ToString()); + ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); ImGui.TableNextColumn(); - var obj = (GameObject*)_dalamud.Objects.GetObjectAddress(idx); - var (address, name) = - obj != null ? ($"0x{(ulong)obj:X}", new ByteString(obj->Name).ToString()) : ("NULL", "NULL"); + ImGui.TextUnformatted(child ? "Child" : "Main"); + ImGui.TableNextColumn(); + var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); ImGui.TextUnformatted(address); ImGui.TableNextColumn(); ImGui.TextUnformatted(name); ImGui.TableNextColumn(); - ImGui.TextUnformatted(c.ModCollection.Name); + var collection = _collectionResolver.IdentifyCollection(gameObject, true); + ImGui.TextUnformatted(collection.ModCollection.Name); } } } @@ -230,16 +247,14 @@ public class DebugTab : ITab { if (pathTree) { - using var table = Table("###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (path, collection) in _pathResolver.PathCollections) + foreach (var data in _pathState.CurrentData) { ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + ImGui.TextUnformatted($"{data.AssociatedGameObject:X}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(collection.ModCollection.Name); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(collection.AssociatedGameObject.ToString("X")); + ImGui.TextUnformatted(data.ModCollection.Name); } } } @@ -248,26 +263,30 @@ public class DebugTab : ITab { if (resourceTree) { - using var table = Table("###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###ResourceCollectionResolverTable", 4, ImGuiTableFlags.SizingFixedFit); if (table) { ImGuiUtil.DrawTableColumn("Current Mtrl Data"); - ImGuiUtil.DrawTableColumn(_pathResolver.CurrentMtrlData.ModCollection.Name); - ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentMtrlData.AssociatedGameObject:X}"); - - ImGuiUtil.DrawTableColumn("Current Avfx Data"); - ImGuiUtil.DrawTableColumn(_pathResolver.CurrentAvfxData.ModCollection.Name); - ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentAvfxData.AssociatedGameObject:X}"); - - ImGuiUtil.DrawTableColumn("Current Resources"); - ImGuiUtil.DrawTableColumn(_pathResolver.SubfileCount.ToString()); + ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.MtrlData.AssociatedGameObject:X}"); ImGui.TableNextColumn(); - foreach (var (resource, resolve) in _pathResolver.ResourceCollections) + ImGuiUtil.DrawTableColumn("Current Avfx Data"); + ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.AvfxData.AssociatedGameObject:X}"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Current Resources"); + ImGuiUtil.DrawTableColumn(_subfileHelper.Count.ToString()); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + foreach (var (resource, resolve) in _subfileHelper) { ImGuiUtil.DrawTableColumn($"0x{resource:X}"); ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); + ImGuiUtil.DrawTableColumn($"{((ResourceHandle*)resource)->FileName()}"); } } } @@ -277,10 +296,12 @@ public class DebugTab : ITab { if (identifiedTree) { - using var table = Table("##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit); + using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (address, identifier, collection) in PathResolver.IdentifiedCache) + foreach (var (address, identifier, collection) in _identifiedCollectionCache + .OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex)) { + ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{address:X}"); ImGuiUtil.DrawTableColumn(identifier.ToString()); ImGuiUtil.DrawTableColumn(collection.Name); @@ -294,7 +315,7 @@ public class DebugTab : ITab { using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (idx, actor) in _pathResolver.CutsceneActors) + foreach (var (idx, actor) in _cutsceneService.Actors) { ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}"); ImGuiUtil.DrawTableColumn(actor.Name.ToString()); From 046ef4d72d321185354ca9f6468513c4c636bc49 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 23 Mar 2023 13:59:24 +0100 Subject: [PATCH 0817/2451] Deduplicate UI code --- .../UI/Classes/ModEditWindow.QuickImport.cs | 206 ++++-------------- Penumbra/UI/Classes/ResourceTreeViewer.cs | 174 +++++++++++++++ Penumbra/UI/ConfigWindow.OnScreenTab.cs | 143 +----------- 3 files changed, 220 insertions(+), 303 deletions(-) create mode 100644 Penumbra/UI/Classes/ResourceTreeViewer.cs diff --git a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs index 9bba2ea1..c3f394c8 100644 --- a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs @@ -18,8 +18,7 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private ResourceTree[]? _quickImportTrees; - private HashSet? _quickImportUnfolded; + private ResourceTreeViewer? _quickImportViewer; private Dictionary? _quickImportWritables; private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; @@ -34,191 +33,68 @@ public partial class ModEditWindow return; } - _quickImportUnfolded ??= new(); + _quickImportViewer ??= new( "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions ); _quickImportWritables ??= new(); _quickImportActions ??= new(); - if( ImGui.Button( "Refresh Character List" ) ) - { - try - { - _quickImportTrees = ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" ); - _quickImportTrees = Array.Empty(); - } - _quickImportUnfolded.Clear(); - _quickImportWritables.Clear(); - _quickImportActions.Clear(); - } - - try - { - _quickImportTrees ??= ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" ); - _quickImportTrees ??= Array.Empty(); - } - - var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green - - foreach( var (tree, index) in _quickImportTrees.WithIndex() ) - { - using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) - { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) - { - continue; - } - } - using var id = ImRaii.PushId( index ); - - ImGui.Text( $"Collection: {tree.CollectionName}" ); - - using var table = ImRaii.Table( "##ResourceTree", 4, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f ); - ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f ); - ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); - ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthFixed, 3 * ImGuiHelpers.GlobalScale + 2 * ImGui.GetFrameHeight() ); - ImGui.TableHeadersRow(); - - DrawQuickImportNodes( tree.Nodes, 0 ); - } + _quickImportViewer.Draw(); _quickImportFileDialog.Draw(); } - private void DrawQuickImportNodes( IEnumerable resourceNodes, int level ) + private void OnQuickImportRefresh() { - var debugMode = Penumbra.Config.DebugMode; - var frameHeight = ImGui.GetFrameHeight(); - foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) + _quickImportWritables?.Clear(); + _quickImportActions?.Clear(); + } + + private void DrawQuickImportActions( ResourceTree.Node resourceNode, Vector2 buttonSize ) + { + if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) ) { - if( resourceNode.Internal && !debugMode ) + var path = resourceNode.FullPath.ToPath(); + if( resourceNode.FullPath.IsRooted ) { - continue; - } - using var id = ImRaii.PushId( index ); - ImGui.TableNextColumn(); - var unfolded = _quickImportUnfolded!.Contains( resourceNode ); - using( var indent = ImRaii.PushIndent( level ) ) - { - ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); - if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) - { - if( unfolded ) - { - _quickImportUnfolded.Remove( resourceNode ); - } - else - { - _quickImportUnfolded.Add( resourceNode ); - } - unfolded = !unfolded; - } - if( debugMode ) - { - ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString("X" + nint.Size * 2)}" ); - } - } - ImGui.TableNextColumn(); - var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; - ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch - { - 0 => "(none)", - 1 => resourceNode.GamePath.ToString(), - _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - if( hasGamePaths ) - { - var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( allPaths ); - } - ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); - } - ImGui.TableNextColumn(); - var hasFullPath = resourceNode.FullPath.FullName.Length > 0; - if( hasFullPath ) - { - ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); - } - ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); + writable = new RawFileWritable( path ); } else { - ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); + var file = Dalamud.GameData.GetFile( path ); + writable = ( file == null ) ? null : new RawGameFileWritable( file ); } - ImGui.TableNextColumn(); - using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) ) + _quickImportWritables.Add( resourceNode.FullPath, writable ); + } + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true ) ) + { + var fullPathStr = resourceNode.FullPath.FullName; + var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr ); + _quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) => { - if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) ) + if( !success ) { - var path = resourceNode.FullPath.ToPath(); - if( resourceNode.FullPath.IsRooted ) - { - writable = new RawFileWritable( path ); - } - else - { - var file = Dalamud.GameData.GetFile( path ); - writable = ( file == null ) ? null : new RawGameFileWritable( file ); - } - _quickImportWritables.Add( resourceNode.FullPath, writable ); + return; } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( frameHeight ), "Export this file.", !hasFullPath || writable == null, true ) ) - { - var fullPathStr = resourceNode.FullPath.FullName; - var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr ); - _quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) => - { - if( !success ) - { - return; - } - try - { - File.WriteAllBytes( name, writable!.Write() ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" ); - } - } ); - } - ImGui.SameLine(); - if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) ) + try { - quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable ); - _quickImportActions.Add( (resourceNode.GamePath, writable), quickImport ); + File.WriteAllBytes( name, writable!.Write() ); } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), new Vector2( frameHeight ), $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) ) + catch( Exception e ) { - quickImport.Execute(); - _quickImportActions.Remove( (resourceNode.GamePath, writable) ); + Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" ); } - } - if( unfolded ) - { - DrawQuickImportNodes( resourceNode.Children, level + 1 ); - } + } ); + } + ImGui.SameLine(); + if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) ) + { + quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable ); + _quickImportActions.Add( (resourceNode.GamePath, writable), quickImport ); + } + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) ) + { + quickImport.Execute(); + _quickImportActions.Remove( (resourceNode.GamePath, writable) ); } } diff --git a/Penumbra/UI/Classes/ResourceTreeViewer.cs b/Penumbra/UI/Classes/ResourceTreeViewer.cs new file mode 100644 index 00000000..2615db42 --- /dev/null +++ b/Penumbra/UI/Classes/ResourceTreeViewer.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.Interop; + +namespace Penumbra.UI.Classes; + +public class ResourceTreeViewer +{ + private readonly string _name; + private readonly int _actionCapacity; + private readonly Action _onRefresh; + private readonly Action _drawActions; + private readonly HashSet _unfolded; + private ResourceTree[]? _trees; + + public ResourceTreeViewer( string name, int actionCapacity, Action onRefresh, Action drawActions ) + { + _name = name; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new(); + _trees = null; + } + + public void Draw() + { + if( ImGui.Button( "Refresh Character List" ) ) + { + try + { + _trees = ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); + _trees = Array.Empty(); + } + _unfolded.Clear(); + _onRefresh(); + } + + try + { + _trees ??= ResourceTree.FromObjectTable(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); + _trees ??= Array.Empty(); + } + + var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green + + foreach( var (tree, index) in _trees.WithIndex() ) + { + using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) + { + if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) + { + continue; + } + } + using var id = ImRaii.PushId( index ); + + ImGui.Text( $"Collection: {tree.CollectionName}" ); + + using var table = ImRaii.Table( "##ResourceTree", ( _actionCapacity > 0 ) ? 4 : 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + continue; + } + + ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f ); + ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f ); + ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); + if( _actionCapacity > 0 ) + { + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight() ); + } + ImGui.TableHeadersRow(); + + DrawNodes( tree.Nodes, 0 ); + } + } + + private void DrawNodes( IEnumerable resourceNodes, int level ) + { + var debugMode = Penumbra.Config.DebugMode; + var frameHeight = ImGui.GetFrameHeight(); + var cellHeight = ( _actionCapacity > 0 ) ? frameHeight : 0.0f; + foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) + { + if( resourceNode.Internal && !debugMode ) + { + continue; + } + using var id = ImRaii.PushId( index ); + ImGui.TableNextColumn(); + var unfolded = _unfolded!.Contains( resourceNode ); + using( var indent = ImRaii.PushIndent( level ) ) + { + ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); + if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) + { + if( unfolded ) + { + _unfolded.Remove( resourceNode ); + } + else + { + _unfolded.Add( resourceNode ); + } + unfolded = !unfolded; + } + if( debugMode ) + { + ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString( "X" + nint.Size * 2 )}" ); + } + } + ImGui.TableNextColumn(); + var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; + ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch + { + 0 => "(none)", + 1 => resourceNode.GamePath.ToString(), + _ => "(multiple)", + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); + if( hasGamePaths ) + { + var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( allPaths ); + } + ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); + } + ImGui.TableNextColumn(); + if( resourceNode.FullPath.FullName.Length > 0 ) + { + ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); + if( ImGui.IsItemClicked() ) + { + ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); + } + ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); + } + else + { + ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); + ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); + } + if( _actionCapacity > 0 ) + { + ImGui.TableNextColumn(); + using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) ) + { + _drawActions( resourceNode, new Vector2( frameHeight ) ); + } + } + if( unfolded ) + { + DrawNodes( resourceNode.Children, level + 1 ); + } + } + } +} diff --git a/Penumbra/UI/ConfigWindow.OnScreenTab.cs b/Penumbra/UI/ConfigWindow.OnScreenTab.cs index 8baaf64f..786fbca8 100644 --- a/Penumbra/UI/ConfigWindow.OnScreenTab.cs +++ b/Penumbra/UI/ConfigWindow.OnScreenTab.cs @@ -1,12 +1,6 @@ - using System; -using System.Collections.Generic; -using System.Numerics; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Interop; +using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -14,143 +8,16 @@ public partial class ConfigWindow { private class OnScreenTab : ITab { + private ResourceTreeViewer? _viewer; + public ReadOnlySpan Label => "On-Screen"u8; public void DrawContent() { - _unfolded ??= new(); + _viewer ??= new( "On-Screen tab", 0, delegate { }, delegate { } ); - if( ImGui.Button( "Refresh Character List" ) ) - { - try - { - _trees = ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for On-Screen tab:\n{e}" ); - _trees = Array.Empty(); - } - _unfolded.Clear(); - } - - try - { - _trees ??= ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for On-Screen tab:\n{e}" ); - _trees ??= Array.Empty(); - } - - var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green - - foreach( var (tree, index) in _trees.WithIndex() ) - { - using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) - { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) - { - continue; - } - } - using var id = ImRaii.PushId( index ); - - ImGui.Text( $"Collection: {tree.CollectionName}" ); - - using var table = ImRaii.Table( "##ResourceTree", 3, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f ); - ImGui.TableSetupColumn( "Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f ); - ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); - ImGui.TableHeadersRow(); - - DrawNodes( tree.Nodes, 0 ); - } + _viewer.Draw(); } - - private void DrawNodes( IEnumerable resourceNodes, int level ) - { - var debugMode = Penumbra.Config.DebugMode; - var frameHeight = ImGui.GetFrameHeight(); - foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) - { - if( resourceNode.Internal && !debugMode ) - { - continue; - } - using var id = ImRaii.PushId( index ); - ImGui.TableNextColumn(); - var unfolded = _unfolded!.Contains( resourceNode ); - using( var indent = ImRaii.PushIndent( level ) ) - { - ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); - if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) - { - if( unfolded ) - { - _unfolded.Remove( resourceNode ); - } - else - { - _unfolded.Add( resourceNode ); - } - unfolded = !unfolded; - } - if( debugMode ) - { - ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString( "X" + nint.Size * 2 )}" ); - } - } - ImGui.TableNextColumn(); - var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; - ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch - { - 0 => "(none)", - 1 => resourceNode.GamePath.ToString(), - _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - if( hasGamePaths ) - { - var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( allPaths ); - } - ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); - } - ImGui.TableNextColumn(); - var hasFullPath = resourceNode.FullPath.FullName.Length > 0; - if( hasFullPath ) - { - ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); - } - ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); - } - else - { - ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) ); - ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); - } - if( unfolded ) - { - DrawNodes( resourceNode.Children, level + 1 ); - } - } - } - - private ResourceTree[]? _trees; - private HashSet? _unfolded; } } From e6b17d536bbc31c06992444579e6802aca48cd5b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 17:51:02 +0100 Subject: [PATCH 0818/2451] Cleanup and fit ResourceTree to new paradigm. --- Penumbra/Interop/ResourceTree.cs | 678 ------------------ Penumbra/Interop/ResourceTree/FileCache.cs | 60 ++ .../Interop/ResourceTree/ResolveContext.cs | 289 ++++++++ Penumbra/Interop/ResourceTree/ResourceNode.cs | 61 ++ Penumbra/Interop/ResourceTree/ResourceTree.cs | 99 +++ .../ResourceTree/ResourceTreeFactory.cs | 76 ++ Penumbra/Interop/Structs/WeaponExt.cs | 14 + Penumbra/PenumbraNew.cs | 4 +- 8 files changed, 602 insertions(+), 679 deletions(-) delete mode 100644 Penumbra/Interop/ResourceTree.cs create mode 100644 Penumbra/Interop/ResourceTree/FileCache.cs create mode 100644 Penumbra/Interop/ResourceTree/ResolveContext.cs create mode 100644 Penumbra/Interop/ResourceTree/ResourceNode.cs create mode 100644 Penumbra/Interop/ResourceTree/ResourceTree.cs create mode 100644 Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs create mode 100644 Penumbra/Interop/Structs/WeaponExt.cs diff --git a/Penumbra/Interop/ResourceTree.cs b/Penumbra/Interop/ResourceTree.cs deleted file mode 100644 index 3c1cfb70..00000000 --- a/Penumbra/Interop/ResourceTree.cs +++ /dev/null @@ -1,678 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.SubKinds; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui; -using Penumbra.Collections; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.Interop.Resolver; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; -using Objects = Dalamud.Game.ClientState.Objects.Types; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; - -namespace Penumbra.Interop; - -public class ResourceTree -{ - public readonly string Name; - public readonly nint SourceAddress; - public readonly bool PlayerRelated; - public readonly string CollectionName; - public readonly List Nodes; - - public ResourceTree( string name, nint sourceAddress, bool playerRelated, string collectionName ) - { - Name = name; - SourceAddress = sourceAddress; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new(); - } - - public static ResourceTree[] FromObjectTable( bool withNames = true ) - { - var cache = new TreeBuildCache(); - - return cache.Characters - .Select( chara => FromCharacter( chara, cache, withNames ) ) - .OfType() - .ToArray(); - } - - public static IEnumerable<(Objects.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, bool withNames = true ) - { - var cache = new TreeBuildCache(); - foreach( var chara in characters ) - { - var tree = FromCharacter( chara, cache, withNames ); - if( tree != null ) - { - yield return (chara, tree); - } - } - } - - public static unsafe ResourceTree? FromCharacter( Objects.Character chara, bool withNames = true ) - { - return FromCharacter( chara, new TreeBuildCache(), withNames ); - } - - private static unsafe ResourceTree? FromCharacter( Objects.Character chara, TreeBuildCache cache, bool withNames = true ) - { - if( !chara.IsValid() ) - { - return null; - } - - var charaStruct = ( Character* )chara.Address; - var gameObjStruct = &charaStruct->GameObject; - var model = ( CharacterBase* )gameObjStruct->GetDrawObject(); - if( model == null ) - { - return null; - } - - var equipment = new ReadOnlySpan( charaStruct->EquipSlotData, 10 ); - // var customize = new ReadOnlySpan( charaStruct->CustomizeData, 26 ); - - var collectionResolveData = PathResolver.IdentifyCollection( gameObjStruct, true ); - if( !collectionResolveData.Valid ) - { - return null; - } - var collection = collectionResolveData.ModCollection; - - var name = GetCharacterName( chara, cache ); - var tree = new ResourceTree( name.Name, new nint( charaStruct ), name.PlayerRelated, collection.Name ); - - var globalContext = new GlobalResolveContext( - FileCache: cache, - Collection: collection, - Skeleton: charaStruct->ModelSkeletonId, - WithNames: withNames - ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var context = globalContext.CreateContext( - Slot: ( i < equipment.Length ) ? ( ( uint )i ).ToEquipSlot() : EquipSlot.Unknown, - Equipment: ( i < equipment.Length ) ? equipment[i] : default - ); - - var imc = ( ResourceHandle* )model->IMCArray[i]; - var imcNode = context.CreateNodeFromImc( imc ); - if( imcNode != null ) - { - tree.Nodes.Add( withNames ? imcNode.WithName( imcNode.Name ?? $"IMC #{i}" ) : imcNode ); - } - - var mdl = ( RenderModel* )model->ModelArray[i]; - var mdlNode = context.CreateNodeFromRenderModel( mdl ); - if( mdlNode != null ) - { - tree.Nodes.Add( withNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode ); - } - } - - if( chara is PlayerCharacter ) - { - AddHumanResources( tree, globalContext, ( HumanExt* )model ); - } - - return tree; - } - - private static unsafe (string Name, bool PlayerRelated) GetCharacterName( Objects.Character chara, TreeBuildCache cache ) - { - var identifier = Penumbra.Actors.FromObject( ( GameObject* )chara.Address, out var owner, true, false, false ); - var name = chara.Name.ToString().Trim(); - var playerRelated = true; - - if( chara.ObjectKind != ObjectKind.Player ) - { - name = $"{name} ({chara.ObjectKind.ToName()})".Trim(); - playerRelated = false; - } - - if( identifier.Type == IdentifierType.Owned && cache.CharactersById.TryGetValue( owner->ObjectID, out var ownerChara ) ) - { - var ownerName = GetCharacterName( ownerChara, cache ); - name = $"[{ownerName.Name}] {name}".Trim(); - playerRelated |= ownerName.PlayerRelated; - } - - return (name, playerRelated); - } - - private static unsafe void AddHumanResources( ResourceTree tree, GlobalResolveContext globalContext, HumanExt* human ) - { - var firstSubObject = ( CharacterBase* )human->Human.CharacterBase.DrawObject.Object.ChildObject; - if( firstSubObject != null ) - { - var subObjectNodes = new List(); - var subObject = firstSubObject; - var subObjectIndex = 0; - do - { - var weapon = ( subObject->GetModelType() == CharacterBase.ModelType.Weapon ) ? ( Weapon* )subObject : null; - var subObjectNamePrefix = ( weapon != null ) ? "Weapon" : "Fashion Acc."; - var subObjectContext = globalContext.CreateContext( - Slot: ( weapon != null ) ? EquipSlot.MainHand : EquipSlot.Unknown, - Equipment: ( weapon != null ) ? new EquipmentRecord( weapon->ModelSetId, ( byte )weapon->Variant, ( byte )weapon->ModelUnknown ) : default - ); - - for( var i = 0; i < subObject->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )subObject->IMCArray[i]; - var imcNode = subObjectContext.CreateNodeFromImc( imc ); - if( imcNode != null ) - { - subObjectNodes.Add( globalContext.WithNames ? imcNode.WithName( imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}" ) : imcNode ); - } - - var mdl = ( RenderModel* )subObject->ModelArray[i]; - var mdlNode = subObjectContext.CreateNodeFromRenderModel( mdl ); - if( mdlNode != null ) - { - subObjectNodes.Add( globalContext.WithNames ? mdlNode.WithName( mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}" ) : mdlNode ); - } - } - - subObject = ( CharacterBase* )subObject->DrawObject.Object.NextSiblingObject; - ++subObjectIndex; - } while( subObject != null && subObject != firstSubObject ); - tree.Nodes.InsertRange( 0, subObjectNodes ); - } - - var context = globalContext.CreateContext( - Slot: EquipSlot.Unknown, - Equipment: default - ); - - var skeletonNode = context.CreateHumanSkeletonNode( human->Human.RaceSexId ); - if( skeletonNode != null ) - { - tree.Nodes.Add( globalContext.WithNames ? skeletonNode.WithName( skeletonNode.Name ?? "Skeleton" ) : skeletonNode ); - } - - var decalNode = context.CreateNodeFromTex( human->Decal ); - if( decalNode != null ) - { - tree.Nodes.Add( globalContext.WithNames ? decalNode.WithName( decalNode.Name ?? "Face Decal" ) : decalNode ); - } - - var legacyDecalNode = context.CreateNodeFromTex( human->LegacyBodyDecal ); - if( legacyDecalNode != null ) - { - tree.Nodes.Add( globalContext.WithNames ? legacyDecalNode.WithName( legacyDecalNode.Name ?? "Legacy Body Decal" ) : legacyDecalNode ); - } - } - - private static unsafe bool CreateOwnedGamePath( byte* rawGamePath, out Utf8GamePath gamePath, bool addDx11Prefix = false, bool isShader = false ) - { - if( rawGamePath == null ) - { - gamePath = default; - return false; - } - - if( isShader ) - { - var path = $"shader/sm5/shpk/{new ByteString( rawGamePath )}"; - return Utf8GamePath.FromString( path, out gamePath ); - } - - if( addDx11Prefix ) - { - var unprefixed = MemoryMarshal.CreateReadOnlySpanFromNullTerminated( rawGamePath ); - var lastDirectorySeparator = unprefixed.LastIndexOf( ( byte )'/' ); - if( unprefixed[lastDirectorySeparator + 1] != ( byte )'-' || unprefixed[lastDirectorySeparator + 2] != ( byte )'-' ) - { - Span prefixed = stackalloc byte[unprefixed.Length + 2]; - unprefixed[..( lastDirectorySeparator + 1 )].CopyTo( prefixed ); - prefixed[lastDirectorySeparator + 1] = ( byte )'-'; - prefixed[lastDirectorySeparator + 2] = ( byte )'-'; - unprefixed[( lastDirectorySeparator + 1 )..].CopyTo( prefixed[( lastDirectorySeparator + 3 )..] ); - - if( !Utf8GamePath.FromSpan( prefixed, out gamePath ) ) - { - return false; - } - - gamePath = gamePath.Clone(); - return true; - } - } - - if( !Utf8GamePath.FromPointer( rawGamePath, out gamePath ) ) - { - return false; - } - - gamePath = gamePath.Clone(); - return true; - } - - [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 4 )] - private readonly record struct EquipmentRecord( ushort SetId, byte Variant, byte Dye ); - - private class TreeBuildCache - { - private readonly Dictionary Materials = new(); - private readonly Dictionary ShaderPackages = new(); - public readonly List Characters; - public readonly Dictionary CharactersById; - - public TreeBuildCache() - { - Characters = new(); - CharactersById = new(); - foreach( var chara in Dalamud.Objects.OfType() ) - { - if( chara.IsValid() ) - { - Characters.Add( chara ); - if( chara.ObjectId != Objects.GameObject.InvalidGameObjectId && !CharactersById.TryAdd( chara.ObjectId, chara ) ) - { - var existingChara = CharactersById[chara.ObjectId]; - Penumbra.Log.Warning( $"Duplicate character ID {chara.ObjectId:X8} (old: {existingChara.Name} {existingChara.ObjectKind}, new: {chara.Name} {chara.ObjectKind})" ); - } - } - } - } - - public MtrlFile? ReadMaterial( FullPath path ) - { - return ReadFile( path, Materials, bytes => new MtrlFile( bytes ) ); - } - - public ShpkFile? ReadShaderPackage( FullPath path ) - { - return ReadFile( path, ShaderPackages, bytes => new ShpkFile( bytes ) ); - } - - private static T? ReadFile( FullPath path, Dictionary cache, Func parseFile ) where T : class - { - if( path.FullName.Length == 0 ) - { - return null; - } - - if( cache.TryGetValue( path, out var cached ) ) - { - return cached; - } - - var pathStr = path.ToPath(); - T? parsed; - try - { - if( path.IsRooted ) - { - parsed = parseFile( File.ReadAllBytes( pathStr ) ); - } - else - { - var bytes = Dalamud.GameData.GetFile( pathStr )?.Data; - parsed = ( bytes != null ) ? parseFile( bytes ) : null; - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not read file {pathStr}:\n{e}" ); - parsed = null; - } - cache.Add( path, parsed ); - - return parsed; - } - } - - private record class GlobalResolveContext( TreeBuildCache FileCache, ModCollection Collection, int Skeleton, bool WithNames ) - { - public ResolveContext CreateContext( EquipSlot Slot, EquipmentRecord Equipment ) - => new( FileCache, Collection, Skeleton, WithNames, Slot, Equipment ); - } - - private record class ResolveContext( TreeBuildCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, EquipmentRecord Equipment ) - { - private unsafe Node? CreateNodeFromGamePath( ResourceType type, nint sourceAddress, byte* rawGamePath, bool @internal, bool addDx11Prefix = false, bool isShader = false ) - { - if( !CreateOwnedGamePath( rawGamePath, out var gamePath, addDx11Prefix, isShader ) ) - { - return null; - } - - return CreateNodeFromGamePath( type, sourceAddress, gamePath, @internal ); - } - - private unsafe Node CreateNodeFromGamePath( ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal ) - => new( null, type, sourceAddress, gamePath, FilterFullPath( Collection.ResolvePath( gamePath ) ?? new FullPath( gamePath ) ), @internal ); - - private unsafe Node? CreateNodeFromResourceHandle( ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, bool withName ) - { - if( handle == null ) - { - return null; - } - - var name = handle->FileNameAsSpan(); - if( name.Length == 0 ) - { - return null; - } - - if( name[0] == ( byte )'|' ) - { - name = name[1..]; - var pos = name.IndexOf( ( byte )'|' ); - if( pos < 0 ) - { - return null; - } - name = name[( pos + 1 )..]; - } - - var fullPath = new FullPath( Encoding.UTF8.GetString( name ) ); - var gamePaths = Collection.ReverseResolvePath( fullPath ).ToList(); - fullPath = FilterFullPath( fullPath ); - - if( gamePaths.Count > 1 ) - { - gamePaths = Filter( gamePaths ); - } - - if( gamePaths.Count == 1 ) - { - return new Node( withName ? GuessNameFromPath( gamePaths[0] ) : null, type, sourceAddress, gamePaths[0], fullPath, @internal ); - } - else - { - Penumbra.Log.Information( $"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:" ); - foreach( var gamePath in gamePaths ) - { - Penumbra.Log.Information( $"Game path: {gamePath}" ); - } - - return new Node( null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal ); - } - } - - public unsafe Node? CreateHumanSkeletonNode( ushort raceSexId ) - { - var raceSexIdStr = raceSexId.ToString( "D4" ); - var path = $"chara/human/c{raceSexIdStr}/skeleton/base/b0001/skl_c{raceSexIdStr}b0001.sklb"; - - if( !Utf8GamePath.FromString( path, out var gamePath ) ) - { - return null; - } - - return CreateNodeFromGamePath( ResourceType.Sklb, 0, gamePath, false ); - } - - public unsafe Node? CreateNodeFromImc( ResourceHandle* imc ) - { - var node = CreateNodeFromResourceHandle( ResourceType.Imc, new nint( imc ), imc, true, false ); - if( node == null ) - { - return null; - } - if( WithNames ) - { - var name = GuessModelName( node.GamePath ); - node = node.WithName( ( name != null ) ? $"IMC: {name}" : null ); - } - - return node; - } - - public unsafe Node? CreateNodeFromTex( ResourceHandle* tex ) - { - return CreateNodeFromResourceHandle( ResourceType.Tex, new nint( tex ), tex, false, WithNames ); - } - - public unsafe Node? CreateNodeFromRenderModel( RenderModel* mdl ) - { - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - return null; - } - - var node = CreateNodeFromResourceHandle( ResourceType.Mdl, new nint( mdl ), mdl->ResourceHandle, false, false ); - if( node == null ) - { - return null; - } - if( WithNames ) - { - node = node.WithName( GuessModelName( node.GamePath ) ); - } - - for( var i = 0; i < mdl->MaterialCount; i++ ) - { - var mtrl = ( Material* )mdl->Materials[i]; - var mtrlNode = CreateNodeFromMaterial( mtrl ); - if( mtrlNode != null ) - { - // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add( WithNames ? mtrlNode.WithName( ( string.Equals( mtrlNode.Name, node.Name, StringComparison.Ordinal ) ? null : mtrlNode.Name ) ?? $"Material #{i}" ) : mtrlNode ); - } - } - - return node; - } - - private unsafe Node? CreateNodeFromMaterial( Material* mtrl ) - { - if( mtrl == null ) - { - return null; - } - - var resource = ( MtrlResource* )mtrl->ResourceHandle; - var node = CreateNodeFromResourceHandle( ResourceType.Mtrl, new nint( mtrl ), &resource->Handle, false, WithNames ); - if( node == null ) - { - return null; - } - var mtrlFile = WithNames ? FileCache.ReadMaterial( node.FullPath ) : null; - - var shpkNode = CreateNodeFromGamePath( ResourceType.Shpk, 0, resource->ShpkString, false, isShader: true ); - if( shpkNode != null ) - { - node.Children.Add( WithNames ? shpkNode.WithName( "Shader Package" ) : shpkNode ); - } - var shpkFile = ( WithNames && shpkNode != null ) ? FileCache.ReadShaderPackage( shpkNode.FullPath ) : null; - var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; - - for( var i = 0; i < resource->NumTex; i++ ) - { - var texNode = CreateNodeFromGamePath( ResourceType.Tex, 0, resource->TexString( i ), false, addDx11Prefix: resource->TexIsDX11( i ) ); - if( texNode != null ) - { - if( WithNames ) - { - var name = ( samplers != null && i < samplers.Count ) ? samplers[i].Item2?.Name : null; - node.Children.Add( texNode.WithName( name ?? $"Texture #{i}" ) ); - } - else - { - node.Children.Add( texNode ); - } - } - } - - return node; - } - - private static FullPath FilterFullPath( FullPath fullPath ) - { - if( !fullPath.IsRooted ) - { - return fullPath; - } - - var relPath = Path.GetRelativePath( Penumbra.Config.ModDirectory, fullPath.FullName ); - if( relPath == "." || !relPath.StartsWith( '.' ) && !Path.IsPathRooted( relPath ) ) - { - return fullPath.Exists ? fullPath : FullPath.Empty; - } - return FullPath.Empty; - } - - private List Filter( List gamePaths ) - { - var filtered = new List( gamePaths.Count ); - foreach( var path in gamePaths ) - { - // In doubt, keep the paths. - if( IsMatch( path.ToString().Split( new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries ) ) ?? true ) - { - filtered.Add( path ); - } - } - - return filtered; - } - - private bool? IsMatch( ReadOnlySpan path ) - => SafeGet( path, 0 ) switch - { - "chara" => SafeGet( path, 1 ) switch - { - "accessory" => IsMatchEquipment( path[2..], $"a{Equipment.SetId:D4}" ), - "equipment" => IsMatchEquipment( path[2..], $"e{Equipment.SetId:D4}" ), - "monster" => SafeGet( path, 2 ) == $"m{Skeleton:D4}", - "weapon" => IsMatchEquipment( path[2..], $"w{Equipment.SetId:D4}" ), - _ => null, - }, - _ => null, - }; - - private bool? IsMatchEquipment( ReadOnlySpan path, string equipmentDir ) - => SafeGet( path, 0 ) == equipmentDir - ? SafeGet( path, 1 ) switch - { - "material" => SafeGet( path, 2 ) == $"v{Equipment.Variant:D4}", - _ => null, - } - : false; - - private string? GuessModelName( Utf8GamePath gamePath ) - { - var path = gamePath.ToString().Split( new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - // Weapons intentionally left out. - var isEquipment = SafeGet( path, 0 ) == "chara" && SafeGet( path, 1 ) is "accessory" or "equipment"; - if( isEquipment ) - { - foreach( var item in Penumbra.Identifier.Identify( Equipment.SetId, Equipment.Variant, Slot.ToSlot() ) ) - { - return Slot switch - { - EquipSlot.RFinger => "R: ", - EquipSlot.LFinger => "L: ", - _ => string.Empty, - } + item.Name.ToString(); - } - } - var nameFromPath = GuessNameFromPath( gamePath ); - if( nameFromPath != null ) - { - return nameFromPath; - } - if( isEquipment ) - { - return Slot.ToName(); - } - - return null; - } - - private static string? GuessNameFromPath( Utf8GamePath gamePath ) - { - foreach( var obj in Penumbra.Identifier.Identify( gamePath.ToString() ) ) - { - var name = obj.Key; - if( name.StartsWith( "Customization:" ) ) - { - name = name[14..].Trim(); - } - if( name != "Unknown" ) - { - return name; - } - } - - return null; - } - - private static string? SafeGet( ReadOnlySpan array, Index index ) - { - var i = index.GetOffset( array.Length ); - return ( i >= 0 && i < array.Length ) ? array[i] : null; - } - } - - public class Node - { - public readonly string? Name; - public readonly ResourceType Type; - public readonly nint SourceAddress; - public readonly Utf8GamePath GamePath; - public readonly Utf8GamePath[] PossibleGamePaths; - public readonly FullPath FullPath; - public readonly bool Internal; - public readonly List Children; - - public Node( string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal ) - { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = gamePath; - PossibleGamePaths = new[] { gamePath }; - FullPath = fullPath; - Internal = @internal; - Children = new(); - } - - public Node( string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, bool @internal ) - { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = ( possibleGamePaths.Length == 1 ) ? possibleGamePaths[0] : Utf8GamePath.Empty; - PossibleGamePaths = possibleGamePaths; - FullPath = fullPath; - Internal = @internal; - Children = new(); - } - - private Node( string? name, Node originalNode ) - { - Name = name; - Type = originalNode.Type; - SourceAddress = originalNode.SourceAddress; - GamePath = originalNode.GamePath; - PossibleGamePaths = originalNode.PossibleGamePaths; - FullPath = originalNode.FullPath; - Internal = originalNode.Internal; - Children = originalNode.Children; - } - - public Node WithName( string? name ) - => string.Equals( Name, name, StringComparison.Ordinal ) ? this : new Node( name, this ); - } -} diff --git a/Penumbra/Interop/ResourceTree/FileCache.cs b/Penumbra/Interop/ResourceTree/FileCache.cs new file mode 100644 index 00000000..c6c9ac9d --- /dev/null +++ b/Penumbra/Interop/ResourceTree/FileCache.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Data; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.ResourceTree; + +internal class FileCache +{ + private readonly DataManager _dataManager; + private readonly Dictionary _materials = new(); + private readonly Dictionary _shaderPackages = new(); + + public FileCache(DataManager dataManager) + => _dataManager = dataManager; + + /// Try to read a material file from the given path and cache it on success. + public MtrlFile? ReadMaterial(FullPath path) + => ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes)); + + /// Try to read a shpk file from the given path and cache it on success. + public ShpkFile? ReadShaderPackage(FullPath path) + => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); + + private static T? ReadFile(DataManager dataManager, FullPath path, Dictionary cache, Func parseFile) + where T : class + { + if (path.FullName.Length == 0) + return null; + + if (cache.TryGetValue(path, out var cached)) + return cached; + + var pathStr = path.ToPath(); + T? parsed; + try + { + if (path.IsRooted) + { + parsed = parseFile(File.ReadAllBytes(pathStr)); + } + else + { + var bytes = dataManager.GetFile(pathStr)?.Data; + parsed = bytes != null ? parseFile(bytes) : null; + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read file {pathStr}:\n{e}"); + parsed = null; + } + + cache.Add(path, parsed); + + return parsed; + } +} diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs new file mode 100644 index 00000000..82d8af29 --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.ResourceTree; + +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames) +{ + public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) + => new(Config, Identifier, FileCache, Collection, Skeleton, WithNames, slot, equipment); +} + +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, + CharacterArmor Equipment) +{ + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal) + { + if (gamePath.IsEmpty) + return null; + if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) + return null; + + return CreateNodeFromGamePath(ResourceType.Shpk, sourceAddress, path, @internal); + } + + private ResourceNode? CreateNodeFromTex(nint sourceAddress, ByteString gamePath, bool @internal, bool dx11) + { + if (dx11) + { + var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); + if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) + return null; + + if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-') + { + Span prefixed = stackalloc byte[gamePath.Length + 3]; + gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); + prefixed[lastDirectorySeparator + 1] = (byte)'-'; + prefixed[lastDirectorySeparator + 2] = (byte)'-'; + gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + + if (!Utf8GamePath.FromSpan(prefixed, out var tmp)) + return null; + + gamePath = tmp.Path.Clone(); + } + } + + if (!Utf8GamePath.FromByteString(gamePath, out var path)) + return null; + + return CreateNodeFromGamePath(ResourceType.Tex, sourceAddress, path, @internal); + } + + private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) + => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); + + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + bool withName) + { + if (handle == null) + return null; + + var name = handle->FileName(); + if (name.IsEmpty) + return null; + + if (name[0] == (byte)'|') + { + var pos = name.IndexOf((byte)'|', 1); + if (pos < 0) + return null; + + name = name.Substring(pos + 1); + } + + var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); + fullPath = FilterFullPath(fullPath); + + if (gamePaths.Count > 1) + gamePaths = Filter(gamePaths); + + if (gamePaths.Count == 1) + return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, sourceAddress, gamePaths[0], fullPath, @internal); + + Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); + foreach (var gamePath in gamePaths) + Penumbra.Log.Information($"Game path: {gamePath}"); + + return new ResourceNode(null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal); + } + public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) + { + var raceSexIdStr = gr.ToRaceCode(); + var path = $"chara/human/c{raceSexIdStr}/skeleton/base/b0001/skl_c{raceSexIdStr}b0001.sklb"; + + if (!Utf8GamePath.FromString(path, out var gamePath)) + return null; + + return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false); + } + + public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) + { + var node = CreateNodeFromResourceHandle(ResourceType.Imc, (nint) imc, imc, true, false); + if (node == null) + return null; + + if (WithNames) + { + var name = GuessModelName(node.GamePath); + node = node.WithName(name != null ? $"IMC: {name}" : null); + } + + return node; + } + + public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) + => CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); + + public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) + { + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + return null; + + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); + if (node == null) + return null; + + if (WithNames) + node = node.WithName(GuessModelName(node.GamePath)); + + for (var i = 0; i < mdl->MaterialCount; i++) + { + var mtrl = (Material*)mdl->Materials[i]; + var mtrlNode = CreateNodeFromMaterial(mtrl); + if (mtrlNode != null) + // Don't keep the material's name if it's redundant with the model's name. + node.Children.Add(WithNames + ? mtrlNode.WithName((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) + ?? $"Material #{i}") + : mtrlNode); + } + + return node; + } + + private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) + { + if (mtrl == null) + return null; + + var resource = (MtrlResource*)mtrl->ResourceHandle; + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); + if (node == null) + return null; + + var mtrlFile = WithNames ? FileCache.ReadMaterial(node.FullPath) : null; + + var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); + if (shpkNode != null) + node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); + var shpkFile = WithNames && shpkNode != null ? FileCache.ReadShaderPackage(shpkNode.FullPath) : null; + var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; + + for (var i = 0; i < resource->NumTex; i++) + { + var texNode = CreateNodeFromTex(nint.Zero, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + if (texNode == null) + continue; + + if (WithNames) + { + var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null; + node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); + } + else + { + node.Children.Add(texNode); + } + } + + return node; + } + + private FullPath FilterFullPath(FullPath fullPath) + { + if (!fullPath.IsRooted) + return fullPath; + + var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); + if (relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) + return fullPath.Exists ? fullPath : FullPath.Empty; + + return FullPath.Empty; + } + + private List Filter(List gamePaths) + { + var filtered = new List(gamePaths.Count); + foreach (var path in gamePaths) + { + // In doubt, keep the paths. + if (IsMatch(path.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries)) + ?? true) + filtered.Add(path); + } + + return filtered; + } + + private bool? IsMatch(ReadOnlySpan path) + => SafeGet(path, 0) switch + { + "chara" => SafeGet(path, 1) switch + { + "accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Value:D4}"), + "equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Value:D4}"), + "monster" => SafeGet(path, 2) == $"m{Skeleton:D4}", + "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Value:D4}"), + _ => null, + }, + _ => null, + }; + + private bool? IsMatchEquipment(ReadOnlySpan path, string equipmentDir) + => SafeGet(path, 0) == equipmentDir + ? SafeGet(path, 1) switch + { + "material" => SafeGet(path, 2) == $"v{Equipment.Variant:D4}", + _ => null, + } + : false; + + private string? GuessModelName(Utf8GamePath gamePath) + { + var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); + // Weapons intentionally left out. + var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment"; + if (isEquipment) + foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) + { + return Slot switch + { + EquipSlot.RFinger => "R: ", + EquipSlot.LFinger => "L: ", + _ => string.Empty, + } + + item.Name.ToString(); + } + + var nameFromPath = GuessNameFromPath(gamePath); + if (nameFromPath != null) + return nameFromPath; + + return isEquipment ? Slot.ToName() : null; + } + + private string? GuessNameFromPath(Utf8GamePath gamePath) + { + foreach (var obj in Identifier.Identify(gamePath.ToString())) + { + var name = obj.Key; + if (name.StartsWith("Customization:")) + name = name[14..].Trim(); + if (name != "Unknown") + return name; + } + + return null; + } + + private static string? SafeGet(ReadOnlySpan array, Index index) + { + var i = index.GetOffset(array.Length); + return i >= 0 && i < array.Length ? array[i] : null; + } +} diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs new file mode 100644 index 00000000..dc0c5fcb --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Penumbra.GameData.Enums; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.ResourceTree; + +public class ResourceNode +{ + public readonly string? Name; + public readonly ResourceType Type; + public readonly nint SourceAddress; + public readonly Utf8GamePath GamePath; + public readonly Utf8GamePath[] PossibleGamePaths; + public readonly FullPath FullPath; + public readonly bool Internal; + public readonly List Children; + + public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal) + { + Name = name; + Type = type; + SourceAddress = sourceAddress; + GamePath = gamePath; + PossibleGamePaths = new[] + { + gamePath, + }; + FullPath = fullPath; + Internal = @internal; + Children = new List(); + } + + public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + bool @internal) + { + Name = name; + Type = type; + SourceAddress = sourceAddress; + GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; + PossibleGamePaths = possibleGamePaths; + FullPath = fullPath; + Internal = @internal; + Children = new List(); + } + + private ResourceNode(string? name, ResourceNode originalResourceNode) + { + Name = name; + Type = originalResourceNode.Type; + SourceAddress = originalResourceNode.SourceAddress; + GamePath = originalResourceNode.GamePath; + PossibleGamePaths = originalResourceNode.PossibleGamePaths; + FullPath = originalResourceNode.FullPath; + Internal = originalResourceNode.Internal; + Children = originalResourceNode.Children; + } + + public ResourceNode WithName(string? name) + => string.Equals(Name, name, StringComparison.Ordinal) ? this : new ResourceNode(name, this); +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs new file mode 100644 index 00000000..b30b5f0e --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.ResourceTree; + +public class ResourceTree +{ + public readonly string Name; + public readonly nint SourceAddress; + public readonly string CollectionName; + public readonly List Nodes; + + public ResourceTree(string name, nint sourceAddress, string collectionName) + { + Name = name; + SourceAddress = sourceAddress; + CollectionName = collectionName; + Nodes = new List(); + } + + internal unsafe void LoadResources(GlobalResolveContext globalContext) + { + var character = (Character*)SourceAddress; + var model = (CharacterBase*) character->GameObject.GetDrawObject(); + var equipment = new ReadOnlySpan(character->EquipSlotData, 10); + // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); + + for (var i = 0; i < model->SlotCount; ++i) + { + var context = globalContext.CreateContext( + i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown, + i < equipment.Length ? equipment[i] : default + ); + + var imc = (ResourceHandle*)model->IMCArray[i]; + var imcNode = context.CreateNodeFromImc(imc); + if (imcNode != null) + Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); + + var mdl = (RenderModel*)model->ModelArray[i]; + var mdlNode = context.CreateNodeFromRenderModel(mdl); + if (mdlNode != null) + Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); + } + + if (character->GameObject.GetObjectKind() == (byte) ObjectKind.Pc) + AddHumanResources(globalContext, (HumanExt*)model); + } + + private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human) + { + var prependIndex = 0; + + var firstWeapon = (WeaponExt*)human->Human.CharacterBase.DrawObject.Object.ChildObject; + if (firstWeapon != null) + { + var weapon = firstWeapon; + var weaponIndex = 0; + do + { + var weaponContext = globalContext.CreateContext( + slot: EquipSlot.MainHand, + equipment: new CharacterArmor(weapon->Weapon.ModelSetId, (byte)weapon->Weapon.Variant, (byte)weapon->Weapon.ModelUnknown) + ); + + var weaponMdlNode = weaponContext.CreateNodeFromRenderModel(*weapon->WeaponRenderModel); + if (weaponMdlNode != null) + Nodes.Insert(prependIndex++, + globalContext.WithNames ? weaponMdlNode.WithName(weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}") : weaponMdlNode); + + weapon = (WeaponExt*)weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject; + ++weaponIndex; + } while (weapon != null && weapon != firstWeapon); + } + + var context = globalContext.CreateContext( + EquipSlot.Unknown, + default + ); + + var skeletonNode = context.CreateHumanSkeletonNode((GenderRace) human->Human.RaceSexId); + if (skeletonNode != null) + Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); + + var decalNode = context.CreateNodeFromTex(human->Decal); + if (decalNode != null) + Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); + + var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); + if (legacyDecalNode != null) + Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + } +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs new file mode 100644 index 00000000..a90d688e --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Dalamud.Game.ClientState.Objects; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData; +using Penumbra.Interop.Resolver; +using Penumbra.Services; + +namespace Penumbra.Interop.ResourceTree; + +public class ResourceTreeFactory +{ + private readonly DataManager _gameData; + private readonly ObjectTable _objects; + private readonly CollectionResolver _collectionResolver; + private readonly IdentifierService _identifier; + private readonly Configuration _config; + + public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier, + Configuration config) + { + _gameData = gameData; + _objects = objects; + _collectionResolver = resolver; + _identifier = identifier; + _config = config; + } + + public ResourceTree[] FromObjectTable(bool withNames = true) + { + var cache = new FileCache(_gameData); + + return _objects + .OfType() + .Select(c => FromCharacter(c, cache, withNames)) + .OfType() + .ToArray(); + } + + public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( + IEnumerable characters, + bool withNames = true) + { + var cache = new FileCache(_gameData); + foreach (var character in characters) + { + var tree = FromCharacter(character, cache, withNames); + if (tree != null) + yield return (character, tree); + } + } + + public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) + => FromCharacter(character, new FileCache(_gameData), withNames); + + private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, FileCache cache, + bool withNames = true) + { + var gameObjStruct = (GameObject*)character.Address; + if (gameObjStruct->GetDrawObject() == null) + return null; + + var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true); + if (!collectionResolveData.Valid) + return null; + + var tree = new ResourceTree(character.Name.ToString(), (nint)gameObjStruct, collectionResolveData.ModCollection.Name); + var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, + ((Character*)gameObjStruct)->ModelCharaId, + withNames); + tree.LoadResources(globalContext); + return tree; + } +} diff --git a/Penumbra/Interop/Structs/WeaponExt.cs b/Penumbra/Interop/Structs/WeaponExt.cs new file mode 100644 index 00000000..de7038d7 --- /dev/null +++ b/Penumbra/Interop/Structs/WeaponExt.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct WeaponExt +{ + [FieldOffset( 0x0 )] + public Weapon Weapon; + + [FieldOffset( 0xA8 )] + public RenderModel** WeaponRenderModel; +} \ No newline at end of file diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 37a099af..00e32edd 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; +using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; @@ -95,7 +96,8 @@ public class PenumbraNew // Add Resource services services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Add Path Resolver services.AddSingleton() From 49f1e2dcde010db85c4adef007e9f5b83363097d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 18:54:16 +0100 Subject: [PATCH 0819/2451] Hopefully merge the rest of the changes correctly. --- Penumbra.Api | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 10 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 64 +++-- .../ResourceTree/ResourceTreeFactory.cs | 51 +++- .../{FileCache.cs => TreeBuildCache.cs} | 18 +- Penumbra/Interop/Structs/WeaponExt.cs | 14 -- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/UI/AdvancedWindow/FileEditor.cs | 40 ++- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 1 - .../ModEditWindow.QuickImport.cs | 226 +++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 34 +-- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 169 +++++++++++++ .../UI/Classes/ModEditWindow.QuickImport.cs | 234 ------------------ Penumbra/UI/Classes/ResourceTreeViewer.cs | 174 ------------- Penumbra/UI/ConfigWindow.OnScreenTab.cs | 23 -- Penumbra/UI/Tabs/ConfigTabBar.cs | 48 ++-- Penumbra/UI/Tabs/OnScreenTab.cs | 28 +++ Penumbra/UI/WindowSystem.cs | 1 - 20 files changed, 606 insertions(+), 536 deletions(-) rename Penumbra/Interop/ResourceTree/{FileCache.cs => TreeBuildCache.cs} (70%) delete mode 100644 Penumbra/Interop/Structs/WeaponExt.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs create mode 100644 Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs delete mode 100644 Penumbra/UI/Classes/ModEditWindow.QuickImport.cs delete mode 100644 Penumbra/UI/Classes/ResourceTreeViewer.cs delete mode 100644 Penumbra/UI/ConfigWindow.OnScreenTab.cs create mode 100644 Penumbra/UI/Tabs/OnScreenTab.cs diff --git a/Penumbra.Api b/Penumbra.Api index f66e49bd..abdc732b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc +Subproject commit abdc732be8b36061dc35bb72e25f1dc4876d5286 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 82d8af29..5b93a726 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -13,13 +13,13 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames) +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) { public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, FileCache, Collection, Skeleton, WithNames, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -166,12 +166,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - var mtrlFile = WithNames ? FileCache.ReadMaterial(node.FullPath) : null; + var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); if (shpkNode != null) node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); - var shpkFile = WithNames && shpkNode != null ? FileCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; for (var i = 0; i < resource->NumTex; i++) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b30b5f0e..e0cb1a95 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -13,13 +13,15 @@ public class ResourceTree { public readonly string Name; public readonly nint SourceAddress; + public readonly bool PlayerRelated; public readonly string CollectionName; public readonly List Nodes; - public ResourceTree(string name, nint sourceAddress, string collectionName) + public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName) { Name = name; SourceAddress = sourceAddress; + PlayerRelated = playerRelated; CollectionName = collectionName; Nodes = new List(); } @@ -27,7 +29,7 @@ public class ResourceTree internal unsafe void LoadResources(GlobalResolveContext globalContext) { var character = (Character*)SourceAddress; - var model = (CharacterBase*) character->GameObject.GetDrawObject(); + var model = (CharacterBase*)character->GameObject.GetDrawObject(); var equipment = new ReadOnlySpan(character->EquipSlotData, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); @@ -49,42 +51,54 @@ public class ResourceTree Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); } - if (character->GameObject.GetObjectKind() == (byte) ObjectKind.Pc) + if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); - } + } private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human) { - var prependIndex = 0; - - var firstWeapon = (WeaponExt*)human->Human.CharacterBase.DrawObject.Object.ChildObject; - if (firstWeapon != null) + var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject; + if (firstSubObject != null) { - var weapon = firstWeapon; - var weaponIndex = 0; + var subObjectNodes = new List(); + var subObject = firstSubObject; + var subObjectIndex = 0; do { - var weaponContext = globalContext.CreateContext( - slot: EquipSlot.MainHand, - equipment: new CharacterArmor(weapon->Weapon.ModelSetId, (byte)weapon->Weapon.Variant, (byte)weapon->Weapon.ModelUnknown) + var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; + var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + var subObjectContext = globalContext.CreateContext( + weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown, + weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default ); - var weaponMdlNode = weaponContext.CreateNodeFromRenderModel(*weapon->WeaponRenderModel); - if (weaponMdlNode != null) - Nodes.Insert(prependIndex++, - globalContext.WithNames ? weaponMdlNode.WithName(weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}") : weaponMdlNode); + for (var i = 0; i < subObject->SlotCount; ++i) + { + var imc = (ResourceHandle*)subObject->IMCArray[i]; + var imcNode = subObjectContext.CreateNodeFromImc(imc); + if (imcNode != null) + subObjectNodes.Add(globalContext.WithNames + ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") + : imcNode); - weapon = (WeaponExt*)weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject; - ++weaponIndex; - } while (weapon != null && weapon != firstWeapon); + var mdl = (RenderModel*)subObject->ModelArray[i]; + var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + if (mdlNode != null) + subObjectNodes.Add(globalContext.WithNames + ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") + : mdlNode); + } + + subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; + ++subObjectIndex; + } while (subObject != null && subObject != firstSubObject); + + Nodes.InsertRange(0, subObjectNodes); } - var context = globalContext.CreateContext( - EquipSlot.Unknown, - default - ); + var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var skeletonNode = context.CreateHumanSkeletonNode((GenderRace) human->Human.RaceSexId); + var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); if (skeletonNode != null) Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a90d688e..b177e404 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.Interop.Resolver; using Penumbra.Services; @@ -17,23 +18,24 @@ public class ResourceTreeFactory private readonly CollectionResolver _collectionResolver; private readonly IdentifierService _identifier; private readonly Configuration _config; + private readonly ActorService _actors; public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier, - Configuration config) + Configuration config, ActorService actors) { _gameData = gameData; _objects = objects; _collectionResolver = resolver; _identifier = identifier; _config = config; + _actors = actors; } public ResourceTree[] FromObjectTable(bool withNames = true) { - var cache = new FileCache(_gameData); + var cache = new TreeBuildCache(_objects, _gameData); - return _objects - .OfType() + return cache.Characters .Select(c => FromCharacter(c, cache, withNames)) .OfType() .ToArray(); @@ -43,7 +45,7 @@ public class ResourceTreeFactory IEnumerable characters, bool withNames = true) { - var cache = new FileCache(_gameData); + var cache = new TreeBuildCache(_objects, _gameData); foreach (var character in characters) { var tree = FromCharacter(character, cache, withNames); @@ -53,11 +55,14 @@ public class ResourceTreeFactory } public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) - => FromCharacter(character, new FileCache(_gameData), withNames); + => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); - private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, FileCache cache, + private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, bool withNames = true) { + if (!character.IsValid()) + return null; + var gameObjStruct = (GameObject*)character.Address; if (gameObjStruct->GetDrawObject() == null) return null; @@ -66,11 +71,37 @@ public class ResourceTreeFactory if (!collectionResolveData.Valid) return null; - var tree = new ResourceTree(character.Name.ToString(), (nint)gameObjStruct, collectionResolveData.ModCollection.Name); + var (name, related) = GetCharacterName(character, cache); + var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->ModelCharaId, - withNames); + ((Character*)gameObjStruct)->ModelCharaId, withNames); tree.LoadResources(globalContext); return tree; } + + private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, + TreeBuildCache cache) + { + var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false); + string name; + bool playerRelated; + switch (identifier.Type) + { + case IdentifierType.Player: + name = identifier.PlayerName.ToString(); + playerRelated = true; + break; + case IdentifierType.Owned when cache.CharactersById.TryGetValue(owner->ObjectID, out var ownerChara): + var ownerName = GetCharacterName(ownerChara, cache); + name = $"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})"; + playerRelated = ownerName.PlayerRelated; + break; + default: + name = $"{character.Name} ({identifier.Kind.ToName()})"; + playerRelated = false; + break; + } + + return (name, playerRelated); + } } diff --git a/Penumbra/Interop/ResourceTree/FileCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs similarity index 70% rename from Penumbra/Interop/ResourceTree/FileCache.cs rename to Penumbra/Interop/ResourceTree/TreeBuildCache.cs index c6c9ac9d..4e432dd4 100644 --- a/Penumbra/Interop/ResourceTree/FileCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,20 +1,32 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Dalamud.Data; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Files; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal class FileCache +internal class TreeBuildCache { private readonly DataManager _dataManager; private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); + public readonly List Characters; + public readonly Dictionary CharactersById; - public FileCache(DataManager dataManager) - => _dataManager = dataManager; + public TreeBuildCache(ObjectTable objects, DataManager dataManager) + { + _dataManager = dataManager; + Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast().ToList(); + CharactersById = Characters + .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) + .GroupBy(c => c.ObjectId) + .ToDictionary(c => c.Key, c => c.First()); + } /// Try to read a material file from the given path and cache it on success. public MtrlFile? ReadMaterial(FullPath path) diff --git a/Penumbra/Interop/Structs/WeaponExt.cs b/Penumbra/Interop/Structs/WeaponExt.cs deleted file mode 100644 index de7038d7..00000000 --- a/Penumbra/Interop/Structs/WeaponExt.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.Interop.Structs; - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct WeaponExt -{ - [FieldOffset( 0x0 )] - public Weapon Weapon; - - [FieldOffset( 0xA8 )] - public RenderModel** WeaponRenderModel; -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 031c7485..2e46314c 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -40,7 +40,7 @@ public class ModFileEditor return num; } - public void RevertFiles(Mod mod, ISubMod option) + public void Revert(Mod mod, ISubMod option) { _files.UpdatePaths(mod, option); Changes = false; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 00e32edd..08f4a0dd 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -133,6 +133,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 47d53832..313d0f26 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -20,11 +20,13 @@ public class FileEditor where T : class, IWritable private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly DataManager _gameData; + private readonly ModEditWindow _owner; - public FileEditor(DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, + public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, Func? parseFile) { + _owner = owner; _gameData = gameData; _config = config; _fileDialog = fileDialog; @@ -41,7 +43,10 @@ public class FileEditor where T : class, IWritable _list = _getFiles(); using var tab = ImRaii.TabItem(_tabName); if (!tab) + { + _quickImport = null; return; + } ImGui.NewLine(); DrawFileSelectCombo(); @@ -67,21 +72,27 @@ public class FileEditor where T : class, IWritable private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; + private string _defaultPath = string.Empty; + private bool _inInput; + private Utf8GamePath _defaultPathUtf8; + private bool _isDefaultPathUtf8Valid; + private T? _defaultFile; + private Exception? _defaultException; private IReadOnlyList _list = null!; + private ModEditWindow.QuickImportAction? _quickImport; + private void DefaultInput() { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = UiHelpers.ScaleX3 }); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (UiHelpers.ScaleX3 + ImGui.GetFrameHeight())); ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); _inInput = ImGui.IsItemActive(); if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { + _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); + _quickImport = null; _fileDialog.Reset(); try { @@ -123,6 +134,21 @@ public class FileEditor where T : class, IWritable } }, _getInitialPath(), false); + _quickImport ??= ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) + { + try + { + UpdateCurrentFile(_quickImport.Execute()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}"); + } + _quickImport = null; + } + _fileDialog.Draw(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 3d0df39d..55eecdae 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -307,7 +307,7 @@ public partial class ModEditWindow var label = changes ? "Revert Changes" : "Reload Files"; var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); if (ImGui.Button(label, length)) - _editor.FileEditor.RevertFiles(_editor.Mod!, _editor.Option!); + _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 306293af..d7e23ac3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,7 +6,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs new file mode 100644 index 00000000..29671c7c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Data; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Interop.ResourceTree; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private ResourceTreeViewer? _quickImportViewer; + private Dictionary? _quickImportWritables; + private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; + private void DrawQuickImportTab() + { + using var tab = ImRaii.TabItem("Import from Screen"); + if (!tab) + { + _quickImportActions = null; + return; + } + + _quickImportViewer ??= new ResourceTreeViewer(_config, _resourceTreeFactory, "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportWritables ??= new Dictionary(); + _quickImportActions ??= new Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>(); + + _quickImportViewer.Draw(); + } + + private void OnQuickImportRefresh() + { + _quickImportWritables?.Clear(); + _quickImportActions?.Clear(); + } + + private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) + { + if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable)) + { + var path = resourceNode.FullPath.ToPath(); + if (resourceNode.FullPath.IsRooted) + { + writable = new RawFileWritable(path); + } + else + { + var file = _gameData.GetFile(path); + writable = file == null ? null : new RawGameFileWritable(file); + } + _quickImportWritables.Add(resourceNode.FullPath, writable); + } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) + { + var fullPathStr = resourceNode.FullPath.FullName; + var ext = resourceNode.PossibleGamePaths.Length == 1 ? Path.GetExtension(resourceNode.GamePath.ToString()) : Path.GetExtension(fullPathStr); + _fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, (success, name) => + { + if (!success) + return; + + try + { + File.WriteAllBytes(name, writable!.Write()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); + } + }, null, false); + } + ImGui.SameLine(); + if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) + { + quickImport = QuickImportAction.Prepare(this, resourceNode.GamePath, writable); + _quickImportActions.Add((resourceNode.GamePath, writable), quickImport); + } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) + { + quickImport.Execute(); + _quickImportActions.Remove((resourceNode.GamePath, writable)); + } + } + + private record class RawFileWritable(string Path) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => File.ReadAllBytes(Path); + } + + private record class RawGameFileWritable(FileResource FileResource) : IWritable + { + public bool Valid => true; + + public byte[] Write() + => FileResource.Data; + } + + public class QuickImportAction + { + public const string FallbackOptionName = "the current option"; + + private readonly string _optionName; + private readonly Utf8GamePath _gamePath; + private readonly ModEditor _editor; + private readonly IWritable? _file; + private readonly string? _targetPath; + private readonly int _subDirs; + + public string OptionName => _optionName; + public Utf8GamePath GamePath => _gamePath; + public bool CanExecute => !_gamePath.IsEmpty && _editor.Mod != null && _file != null && _targetPath != null; + + /// + /// Creates a non-executable QuickImportAction. + /// + private QuickImportAction(ModEditor editor, string optionName, Utf8GamePath gamePath) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = editor; + _file = null; + _targetPath = null; + _subDirs = 0; + } + + /// + /// Creates an executable QuickImportAction. + /// + private QuickImportAction(string optionName, Utf8GamePath gamePath, ModEditor editor, IWritable file, string targetPath, int subDirs) + { + _optionName = optionName; + _gamePath = gamePath; + _editor = editor; + _file = file; + _targetPath = targetPath; + _subDirs = subDirs; + } + + public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file) + { + var editor = owner._editor; + if (editor == null) + { + return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); + } + var subMod = editor.Option; + var optionName = subMod!.FullName; + if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) + { + return new QuickImportAction(editor, optionName, gamePath); + } + if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) + { + return new QuickImportAction(editor, optionName, gamePath); + } + var mod = owner._mod; + if (mod == null) + { + return new QuickImportAction(editor, optionName, gamePath); + } + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod); + var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; + if (File.Exists(targetPath)) + { + return new QuickImportAction(editor, optionName, gamePath); + } + + return new QuickImportAction(optionName, gamePath, editor, file, targetPath, subDirs); + } + + public FileRegistry Execute() + { + if (!CanExecute) + { + throw new InvalidOperationException(); + } + var directory = Path.GetDirectoryName(_targetPath); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + File.WriteAllBytes(_targetPath!, _file!.Write()); + _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); + var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); + _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); + _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!); + + return fileRegistry; + } + + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) + { + var path = mod.ModPath; + var subDirs = 0; + if (subMod == mod.Default) + return (path, subDirs); + + var name = subMod.Name; + var fullName = subMod.FullName; + if (fullName.EndsWith(": " + name)) + { + path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); + path = Mod.Creator.NewOptionDirectory(path, name); + subDirs = 2; + } + else + { + path = Mod.Creator.NewOptionDirectory(path, fullName); + subDirs = 1; + } + + return (path, subDirs); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 035c9286..d6d33175 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; +using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -24,12 +25,14 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly ResourceTreeFactory _resourceTreeFactory; + private readonly DataManager _gameData; private Mod? _mod; - private Vector2 _iconSize = Vector2.Zero; + private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; public void ChangeMod(Mod mod) @@ -136,13 +139,14 @@ public partial class ModEditWindow : Window, IDisposable DrawSwapTab(); DrawMissingFilesTab(); DrawDuplicatesTab(); + DrawQuickImportTab(); DrawMaterialReassignmentTab(); _modelTab.Draw(); _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - using var tab = ImRaii.TabItem("Item Swap (WIP)"); - if (tab) + using var tab = ImRaii.TabItem("Item Swap (WIP)"); + if (tab) _itemSwapTab.DrawContent(); } @@ -488,19 +492,21 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory) : base(WindowBaseLabel) { - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _fileDialog = fileDialog; - _materialTab = new FileEditor(gameData, config, _fileDialog, "Materials", ".mtrl", + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _gameData = gameData; + _resourceTreeFactory = resourceTreeFactory; + _fileDialog = fileDialog; + _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor(gameData, config, _fileDialog, "Models", ".mdl", + _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); - _shaderPackageTab = new FileEditor(gameData, config, _fileDialog, "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null); _center = new CombinedTexture(_left, _right); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs new file mode 100644 index 00000000..1a452bde --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.UI.AdvancedWindow; + +public class ResourceTreeViewer +{ + private readonly Configuration _config; + private readonly ResourceTreeFactory _treeFactory; + private readonly string _name; + private readonly int _actionCapacity; + private readonly Action _onRefresh; + private readonly Action _drawActions; + private readonly HashSet _unfolded; + private ResourceTree[]? _trees; + + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, string name, int actionCapacity, Action onRefresh, + Action drawActions) + { + _config = config; + _treeFactory = treeFactory; + _name = name; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new HashSet(); + _trees = null; + } + + public void Draw() + { + if (ImGui.Button("Refresh Character List")) + { + try + { + _trees = _treeFactory.FromObjectTable(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); + _trees = Array.Empty(); + } + + _unfolded.Clear(); + _onRefresh(); + } + + try + { + _trees ??= _treeFactory.FromObjectTable(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); + _trees ??= Array.Empty(); + } + + var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); + var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green + + foreach (var (tree, index) in _trees.WithIndex()) + { + using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + { + if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + continue; + } + + using var id = ImRaii.PushId(index); + + ImGui.Text($"Collection: {tree.CollectionName}"); + + using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + if (_actionCapacity > 0) + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + + DrawNodes(tree.Nodes, 0); + } + } + + private void DrawNodes(IEnumerable resourceNodes, int level) + { + var debugMode = _config.DebugMode; + var frameHeight = ImGui.GetFrameHeight(); + var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) + { + if (resourceNode.Internal && !debugMode) + continue; + + using var id = ImRaii.PushId(index); + ImGui.TableNextColumn(); + var unfolded = _unfolded!.Contains(resourceNode); + using (var indent = ImRaii.PushIndent(level)) + { + ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); + if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) + { + if (unfolded) + _unfolded.Remove(resourceNode); + else + _unfolded.Add(resourceNode); + unfolded = !unfolded; + } + + if (debugMode) + ImGuiUtil.HoverTooltip( + $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}"); + } + + ImGui.TableNextColumn(); + var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; + ImGui.Selectable(resourceNode.PossibleGamePaths.Length switch + { + 0 => "(none)", + 1 => resourceNode.GamePath.ToString(), + _ => "(multiple)", + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + if (hasGamePaths) + { + var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(allPaths); + ImGuiUtil.HoverTooltip($"{allPaths}\n\nClick to copy to clipboard."); + } + + ImGui.TableNextColumn(); + if (resourceNode.FullPath.FullName.Length > 0) + { + ImGui.Selectable(resourceNode.FullPath.ToString(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(resourceNode.FullPath.ToString()); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath}\n\nClick to copy to clipboard."); + } + else + { + ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); + } + + if (_actionCapacity > 0) + { + ImGui.TableNextColumn(); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); + _drawActions(resourceNode, new Vector2(frameHeight)); + } + + if (unfolded) + DrawNodes(resourceNode.Children, level + 1); + } + } +} diff --git a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs b/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs deleted file mode 100644 index c3f394c8..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.QuickImport.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using ImGuiNET; -using Lumina.Data; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.Interop; -using Penumbra.Mods; -using Penumbra.String.Classes; - -namespace Penumbra.UI.Classes; - -public partial class ModEditWindow -{ - private ResourceTreeViewer? _quickImportViewer; - private Dictionary? _quickImportWritables; - private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; - - private readonly FileDialogManager _quickImportFileDialog = ConfigWindow.SetupFileManager(); - - private void DrawQuickImportTab() - { - using var tab = ImRaii.TabItem( "Import from Screen" ); - if( !tab ) - { - _quickImportActions = null; - return; - } - - _quickImportViewer ??= new( "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions ); - _quickImportWritables ??= new(); - _quickImportActions ??= new(); - - _quickImportViewer.Draw(); - - _quickImportFileDialog.Draw(); - } - - private void OnQuickImportRefresh() - { - _quickImportWritables?.Clear(); - _quickImportActions?.Clear(); - } - - private void DrawQuickImportActions( ResourceTree.Node resourceNode, Vector2 buttonSize ) - { - if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) ) - { - var path = resourceNode.FullPath.ToPath(); - if( resourceNode.FullPath.IsRooted ) - { - writable = new RawFileWritable( path ); - } - else - { - var file = Dalamud.GameData.GetFile( path ); - writable = ( file == null ) ? null : new RawGameFileWritable( file ); - } - _quickImportWritables.Add( resourceNode.FullPath, writable ); - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true ) ) - { - var fullPathStr = resourceNode.FullPath.FullName; - var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr ); - _quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, writable!.Write() ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" ); - } - } ); - } - ImGui.SameLine(); - if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) ) - { - quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable ); - _quickImportActions.Add( (resourceNode.GamePath, writable), quickImport ); - } - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) ) - { - quickImport.Execute(); - _quickImportActions.Remove( (resourceNode.GamePath, writable) ); - } - } - - private record class RawFileWritable( string Path ) : IWritable - { - public bool Valid => true; - - public byte[] Write() - => File.ReadAllBytes( Path ); - } - - private record class RawGameFileWritable( FileResource FileResource ) : IWritable - { - public bool Valid => true; - - public byte[] Write() - => FileResource.Data; - } - - private class QuickImportAction - { - public const string FallbackOptionName = "the current option"; - - private readonly string _optionName; - private readonly Utf8GamePath _gamePath; - private readonly Mod.Editor? _editor; - private readonly IWritable? _file; - private readonly string? _targetPath; - private readonly int _subDirs; - - public string OptionName => _optionName; - public Utf8GamePath GamePath => _gamePath; - public bool CanExecute => !_gamePath.IsEmpty && _editor != null && _file != null && _targetPath != null; - - /// - /// Creates a non-executable QuickImportAction. - /// - private QuickImportAction( string optionName, Utf8GamePath gamePath ) - { - _optionName = optionName; - _gamePath = gamePath; - _editor = null; - _file = null; - _targetPath = null; - _subDirs = 0; - } - - /// - /// Creates an executable QuickImportAction. - /// - private QuickImportAction( string optionName, Utf8GamePath gamePath, Mod.Editor editor, IWritable file, string targetPath, int subDirs ) - { - _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = file; - _targetPath = targetPath; - _subDirs = subDirs; - } - - public static QuickImportAction Prepare( ModEditWindow owner, Utf8GamePath gamePath, IWritable? file ) - { - var editor = owner._editor; - if( editor == null ) - { - return new QuickImportAction( FallbackOptionName, gamePath ); - } - var subMod = editor.CurrentOption; - var optionName = subMod.FullName; - if( gamePath.IsEmpty || file == null || editor.FileChanges ) - { - return new QuickImportAction( optionName, gamePath ); - } - if( subMod.Files.ContainsKey( gamePath ) || subMod.FileSwaps.ContainsKey( gamePath ) ) - { - return new QuickImportAction( optionName, gamePath ); - } - var mod = owner._mod; - if( mod == null ) - { - return new QuickImportAction( optionName, gamePath ); - } - var ( preferredPath, subDirs ) = GetPreferredPath( mod, subMod ); - var targetPath = new FullPath( Path.Combine( preferredPath.FullName, gamePath.ToString() ) ).FullName; - if( File.Exists( targetPath ) ) - { - return new QuickImportAction( optionName, gamePath ); - } - - return new QuickImportAction( optionName, gamePath, editor, file, targetPath, subDirs ); - } - - public Mod.Editor.FileRegistry Execute() - { - if( !CanExecute ) - { - throw new InvalidOperationException(); - } - var directory = Path.GetDirectoryName( _targetPath ); - if( directory != null ) - { - Directory.CreateDirectory( directory ); - } - File.WriteAllBytes( _targetPath!, _file!.Write() ); - _editor!.RevertFiles(); - var fileRegistry = _editor.AvailableFiles.First( file => file.File.FullName == _targetPath ); - _editor.AddPathsToSelected( new Mod.Editor.FileRegistry[] { fileRegistry }, _subDirs ); - _editor.ApplyFiles(); - - return fileRegistry; - } - - private static (DirectoryInfo, int) GetPreferredPath( Mod mod, ISubMod subMod ) - { - var path = mod.ModPath; - var subDirs = 0; - if( subMod != mod.Default ) - { - var name = subMod.Name; - var fullName = subMod.FullName; - if( fullName.EndsWith( ": " + name ) ) - { - path = Mod.Creator.NewOptionDirectory( path, fullName[..^( name.Length + 2 )] ); - path = Mod.Creator.NewOptionDirectory( path, name ); - subDirs = 2; - } - else - { - path = Mod.Creator.NewOptionDirectory( path, fullName ); - subDirs = 1; - } - } - - return (path, subDirs); - } - } -} diff --git a/Penumbra/UI/Classes/ResourceTreeViewer.cs b/Penumbra/UI/Classes/ResourceTreeViewer.cs deleted file mode 100644 index 2615db42..00000000 --- a/Penumbra/UI/Classes/ResourceTreeViewer.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui.Raii; -using OtterGui; -using Penumbra.Interop; - -namespace Penumbra.UI.Classes; - -public class ResourceTreeViewer -{ - private readonly string _name; - private readonly int _actionCapacity; - private readonly Action _onRefresh; - private readonly Action _drawActions; - private readonly HashSet _unfolded; - private ResourceTree[]? _trees; - - public ResourceTreeViewer( string name, int actionCapacity, Action onRefresh, Action drawActions ) - { - _name = name; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = new(); - _trees = null; - } - - public void Draw() - { - if( ImGui.Button( "Refresh Character List" ) ) - { - try - { - _trees = ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); - _trees = Array.Empty(); - } - _unfolded.Clear(); - _onRefresh(); - } - - try - { - _trees ??= ResourceTree.FromObjectTable(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" ); - _trees ??= Array.Empty(); - } - - var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green - - foreach( var (tree, index) in _trees.WithIndex() ) - { - using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) ) - { - if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) ) - { - continue; - } - } - using var id = ImRaii.PushId( index ); - - ImGui.Text( $"Collection: {tree.CollectionName}" ); - - using var table = ImRaii.Table( "##ResourceTree", ( _actionCapacity > 0 ) ? 4 : 3, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f ); - ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f ); - ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f ); - if( _actionCapacity > 0 ) - { - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight() ); - } - ImGui.TableHeadersRow(); - - DrawNodes( tree.Nodes, 0 ); - } - } - - private void DrawNodes( IEnumerable resourceNodes, int level ) - { - var debugMode = Penumbra.Config.DebugMode; - var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = ( _actionCapacity > 0 ) ? frameHeight : 0.0f; - foreach( var (resourceNode, index) in resourceNodes.WithIndex() ) - { - if( resourceNode.Internal && !debugMode ) - { - continue; - } - using var id = ImRaii.PushId( index ); - ImGui.TableNextColumn(); - var unfolded = _unfolded!.Contains( resourceNode ); - using( var indent = ImRaii.PushIndent( level ) ) - { - ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name ); - if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 ) - { - if( unfolded ) - { - _unfolded.Remove( resourceNode ); - } - else - { - _unfolded.Add( resourceNode ); - } - unfolded = !unfolded; - } - if( debugMode ) - { - ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString( "X" + nint.Size * 2 )}" ); - } - } - ImGui.TableNextColumn(); - var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0; - ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch - { - 0 => "(none)", - 1 => resourceNode.GamePath.ToString(), - _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - if( hasGamePaths ) - { - var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( allPaths ); - } - ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." ); - } - ImGui.TableNextColumn(); - if( resourceNode.FullPath.FullName.Length > 0 ) - { - ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( resourceNode.FullPath.ToString() ); - } - ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." ); - } - else - { - ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) ); - ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." ); - } - if( _actionCapacity > 0 ) - { - ImGui.TableNextColumn(); - using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) ) - { - _drawActions( resourceNode, new Vector2( frameHeight ) ); - } - } - if( unfolded ) - { - DrawNodes( resourceNode.Children, level + 1 ); - } - } - } -} diff --git a/Penumbra/UI/ConfigWindow.OnScreenTab.cs b/Penumbra/UI/ConfigWindow.OnScreenTab.cs deleted file mode 100644 index 786fbca8..00000000 --- a/Penumbra/UI/ConfigWindow.OnScreenTab.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using OtterGui.Widgets; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class OnScreenTab : ITab - { - private ResourceTreeViewer? _viewer; - - public ReadOnlySpan Label - => "On-Screen"u8; - - public void DrawContent() - { - _viewer ??= new( "On-Screen tab", 0, delegate { }, delegate { } ); - - _viewer.Draw(); - } - } -} diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 0f91f3dc..a69b6cd7 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -7,14 +7,15 @@ namespace Penumbra.UI.Tabs; public class ConfigTabBar { - public readonly SettingsTab Settings; - public readonly ModsTab Mods; - public readonly CollectionsTab Collections; + public readonly SettingsTab Settings; + public readonly ModsTab Mods; + public readonly CollectionsTab Collections; public readonly ChangedItemsTab ChangedItems; - public readonly EffectiveTab Effective; - public readonly DebugTab Debug; - public readonly ResourceTab Resource; + public readonly EffectiveTab Effective; + public readonly DebugTab Debug; + public readonly ResourceTab Resource; public readonly ResourceWatcher Watcher; + public readonly OnScreenTab OnScreenTab; public readonly ITab[] Tabs; @@ -22,16 +23,17 @@ public class ConfigTabBar public TabType SelectTab = TabType.None; public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, - DebugTab debug, ResourceTab resource, ResourceWatcher watcher) + DebugTab debug, ResourceTab resource, ResourceWatcher watcher, OnScreenTab onScreenTab) { - Settings = settings; - Mods = mods; - Collections = collections; + Settings = settings; + Mods = mods; + Collections = collections; ChangedItems = changedItems; - Effective = effective; - Debug = debug; - Resource = resource; - Watcher = watcher; + Effective = effective; + Debug = debug; + Resource = resource; + Watcher = watcher; + OnScreenTab = onScreenTab; Tabs = new ITab[] { Settings, @@ -39,6 +41,7 @@ public class ConfigTabBar Collections, ChangedItems, Effective, + OnScreenTab, Debug, Resource, Watcher, @@ -54,14 +57,15 @@ public class ConfigTabBar private ReadOnlySpan ToLabel(TabType type) => type switch { - TabType.Settings => Settings.Label, - TabType.Mods => Mods.Label, - TabType.Collections => Collections.Label, - TabType.ChangedItems => ChangedItems.Label, + TabType.Settings => Settings.Label, + TabType.Mods => Mods.Label, + TabType.Collections => Collections.Label, + TabType.ChangedItems => ChangedItems.Label, TabType.EffectiveChanges => Effective.Label, - TabType.ResourceWatcher => Watcher.Label, - TabType.Debug => Debug.Label, - TabType.ResourceManager => Resource.Label, - _ => ReadOnlySpan.Empty, + TabType.OnScreen => OnScreenTab.Label, + TabType.ResourceWatcher => Watcher.Label, + TabType.Debug => Debug.Label, + TabType.ResourceManager => Resource.Label, + _ => ReadOnlySpan.Empty, }; } diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs new file mode 100644 index 00000000..5fe9b8cc --- /dev/null +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -0,0 +1,28 @@ +using System; +using OtterGui.Widgets; +using Penumbra.Interop.ResourceTree; +using Penumbra.UI.AdvancedWindow; + +namespace Penumbra.UI.Tabs; + +public class OnScreenTab : ITab +{ + private readonly Configuration _config; + private readonly ResourceTreeFactory _treeFactory; + private ResourceTreeViewer? _viewer; + + public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) + { + _config = config; + _treeFactory = treeFactory; + } + + public ReadOnlySpan Label + => "On-Screen"u8; + + public void DrawContent() + { + _viewer ??= new ResourceTreeViewer(_config, _treeFactory, "On-Screen tab", 0, delegate { }, delegate { }); + _viewer.Draw(); + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 378aebf0..f2f0a8b6 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -3,7 +3,6 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI; -using Penumbra.UI.Classes; using Penumbra.UI.AdvancedWindow; namespace Penumbra; From 56286e01238ba303a788d1cdccecb34984021eec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 20:30:23 +0100 Subject: [PATCH 0820/2451] Rename interop folders --- .../Interop/{Resolver => PathResolving}/AnimationHookService.cs | 0 .../Interop/{Resolver => PathResolving}/CollectionResolver.cs | 0 Penumbra/Interop/{Resolver => PathResolving}/CutsceneService.cs | 0 Penumbra/Interop/{Resolver => PathResolving}/DrawObjectState.cs | 0 .../{Resolver => PathResolving}/IdentifiedCollectionCache.cs | 2 +- Penumbra/Interop/{Resolver => PathResolving}/MetaState.cs | 0 Penumbra/Interop/{Resolver => PathResolving}/PathResolver.cs | 0 Penumbra/Interop/{Resolver => PathResolving}/PathState.cs | 0 .../Interop/{Resolver => PathResolving}/ResolvePathHooks.cs | 0 Penumbra/Interop/{Resolver => PathResolving}/SubfileHelper.cs | 0 Penumbra/Interop/{Loader => ResourceLoading}/CreateFileWHook.cs | 0 Penumbra/Interop/{Loader => ResourceLoading}/FileReadService.cs | 0 Penumbra/Interop/{Loader => ResourceLoading}/ResourceLoader.cs | 0 .../{Loader => ResourceLoading}/ResourceManagerService.cs | 0 Penumbra/Interop/{Loader => ResourceLoading}/ResourceService.cs | 0 Penumbra/Interop/{Loader => ResourceLoading}/TexMdlService.cs | 0 Penumbra/Interop/{ => Services}/RedrawService.cs | 0 17 files changed, 1 insertion(+), 1 deletion(-) rename Penumbra/Interop/{Resolver => PathResolving}/AnimationHookService.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/CollectionResolver.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/CutsceneService.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/DrawObjectState.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/IdentifiedCollectionCache.cs (98%) rename Penumbra/Interop/{Resolver => PathResolving}/MetaState.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/PathResolver.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/PathState.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/ResolvePathHooks.cs (100%) rename Penumbra/Interop/{Resolver => PathResolving}/SubfileHelper.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/CreateFileWHook.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/FileReadService.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/ResourceLoader.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/ResourceManagerService.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/ResourceService.cs (100%) rename Penumbra/Interop/{Loader => ResourceLoading}/TexMdlService.cs (100%) rename Penumbra/Interop/{ => Services}/RedrawService.cs (100%) diff --git a/Penumbra/Interop/Resolver/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs similarity index 100% rename from Penumbra/Interop/Resolver/AnimationHookService.cs rename to Penumbra/Interop/PathResolving/AnimationHookService.cs diff --git a/Penumbra/Interop/Resolver/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs similarity index 100% rename from Penumbra/Interop/Resolver/CollectionResolver.cs rename to Penumbra/Interop/PathResolving/CollectionResolver.cs diff --git a/Penumbra/Interop/Resolver/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs similarity index 100% rename from Penumbra/Interop/Resolver/CutsceneService.cs rename to Penumbra/Interop/PathResolving/CutsceneService.cs diff --git a/Penumbra/Interop/Resolver/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs similarity index 100% rename from Penumbra/Interop/Resolver/DrawObjectState.cs rename to Penumbra/Interop/PathResolving/DrawObjectState.cs diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs similarity index 98% rename from Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs rename to Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 430f03b0..0d60e72b 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -9,7 +9,7 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Services; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> { diff --git a/Penumbra/Interop/Resolver/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs similarity index 100% rename from Penumbra/Interop/Resolver/MetaState.cs rename to Penumbra/Interop/PathResolving/MetaState.cs diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs similarity index 100% rename from Penumbra/Interop/Resolver/PathResolver.cs rename to Penumbra/Interop/PathResolving/PathResolver.cs diff --git a/Penumbra/Interop/Resolver/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs similarity index 100% rename from Penumbra/Interop/Resolver/PathState.cs rename to Penumbra/Interop/PathResolving/PathState.cs diff --git a/Penumbra/Interop/Resolver/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs similarity index 100% rename from Penumbra/Interop/Resolver/ResolvePathHooks.cs rename to Penumbra/Interop/PathResolving/ResolvePathHooks.cs diff --git a/Penumbra/Interop/Resolver/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs similarity index 100% rename from Penumbra/Interop/Resolver/SubfileHelper.cs rename to Penumbra/Interop/PathResolving/SubfileHelper.cs diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs similarity index 100% rename from Penumbra/Interop/Loader/CreateFileWHook.cs rename to Penumbra/Interop/ResourceLoading/CreateFileWHook.cs diff --git a/Penumbra/Interop/Loader/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs similarity index 100% rename from Penumbra/Interop/Loader/FileReadService.cs rename to Penumbra/Interop/ResourceLoading/FileReadService.cs diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs similarity index 100% rename from Penumbra/Interop/Loader/ResourceLoader.cs rename to Penumbra/Interop/ResourceLoading/ResourceLoader.cs diff --git a/Penumbra/Interop/Loader/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs similarity index 100% rename from Penumbra/Interop/Loader/ResourceManagerService.cs rename to Penumbra/Interop/ResourceLoading/ResourceManagerService.cs diff --git a/Penumbra/Interop/Loader/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs similarity index 100% rename from Penumbra/Interop/Loader/ResourceService.cs rename to Penumbra/Interop/ResourceLoading/ResourceService.cs diff --git a/Penumbra/Interop/Loader/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs similarity index 100% rename from Penumbra/Interop/Loader/TexMdlService.cs rename to Penumbra/Interop/ResourceLoading/TexMdlService.cs diff --git a/Penumbra/Interop/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs similarity index 100% rename from Penumbra/Interop/RedrawService.cs rename to Penumbra/Interop/Services/RedrawService.cs From 7bad13154219c6a8091b2772b6988468e9dc8552 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 20:35:33 +0100 Subject: [PATCH 0821/2451] Start restructuring CharacterUtility --- Penumbra/Interop/CharacterUtility.cs | 159 ++++++++-------- Penumbra/Interop/Structs/CharacterUtility.cs | 170 ------------------ .../Interop/Structs/CharacterUtilityData.cs | 99 ++++++++++ Penumbra/Interop/Structs/MetaIndex.cs | 74 ++++++++ 4 files changed, 250 insertions(+), 252 deletions(-) delete mode 100644 Penumbra/Interop/Structs/CharacterUtility.cs create mode 100644 Penumbra/Interop/Structs/CharacterUtilityData.cs create mode 100644 Penumbra/Interop/Structs/MetaIndex.cs diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 0f3a46d5..63962195 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -4,151 +4,146 @@ using System.Linq; using Dalamud.Game; using Dalamud.Utility.Signatures; using Penumbra.GameData; - +using Penumbra.Interop.Structs; + namespace Penumbra.Interop; public unsafe partial class CharacterUtility : IDisposable { - public record struct InternalIndex( int Value ); + public record struct InternalIndex(int Value); - // A static pointer to the CharacterUtility address. - [Signature( Sigs.CharacterUtility, ScanType = ScanType.StaticAddress )] - private readonly Structs.CharacterUtility** _characterUtilityAddress = null; + /// A static pointer to the CharacterUtility address. + [Signature(Sigs.CharacterUtility, ScanType = ScanType.StaticAddress)] + private readonly CharacterUtilityData** _characterUtilityAddress = null; - // Only required for migration anymore. - public delegate void LoadResources( Structs.CharacterUtility* address ); + /// Only required for migration anymore. + public delegate void LoadResources(CharacterUtilityData* address); - [Signature( Sigs.LoadCharacterResources )] + [Signature(Sigs.LoadCharacterResources)] public readonly LoadResources LoadCharacterResourcesFunc = null!; public void LoadCharacterResources() - => LoadCharacterResourcesFunc.Invoke( Address ); + => LoadCharacterResourcesFunc.Invoke(Address); - public Structs.CharacterUtility* Address + public CharacterUtilityData* Address => *_characterUtilityAddress; - public bool Ready { get; private set; } + public bool Ready { get; private set; } public event Action LoadingFinished; - private IntPtr _defaultTransparentResource; - private IntPtr _defaultDecalResource; + public IntPtr DefaultTransparentResource { get; private set; } + public IntPtr DefaultDecalResource { get; private set; } - // The relevant indices depend on which meta manipulations we allow for. - // The defines are set in the project configuration. - public static readonly Structs.CharacterUtility.Index[] - RelevantIndices = Enum.GetValues< Structs.CharacterUtility.Index >(); + /// + /// The relevant indices depend on which meta manipulations we allow for. + /// The defines are set in the project configuration. + /// + public static readonly MetaIndex[] + RelevantIndices = Enum.GetValues(); public static readonly InternalIndex[] ReverseIndices - = Enumerable.Range( 0, Structs.CharacterUtility.TotalNumResources ) - .Select( i => new InternalIndex( Array.IndexOf( RelevantIndices, ( Structs.CharacterUtility.Index )i ) ) ) - .ToArray(); + = Enumerable.Range(0, CharacterUtilityData.TotalNumResources) + .Select(i => new InternalIndex(Array.IndexOf(RelevantIndices, (MetaIndex)i))) + .ToArray(); - private readonly List[] _lists = Enumerable.Range( 0, RelevantIndices.Length ) - .Select( idx => new List( new InternalIndex( idx ) ) ) - .ToArray(); + private readonly List[] _lists = Enumerable.Range(0, RelevantIndices.Length) + .Select(idx => new List(new InternalIndex(idx))) + .ToArray(); - public IReadOnlyList< List > Lists + public IReadOnlyList Lists => _lists; - public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) - => _lists[ idx.Value ].DefaultResource; + public (IntPtr Address, int Size) DefaultResource(InternalIndex idx) + => _lists[idx.Value].DefaultResource; private readonly Framework _framework; public CharacterUtility(Framework framework) { - SignatureHelper.Initialise( this ); + SignatureHelper.Initialise(this); _framework = framework; - LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." ); - LoadDefaultResources( null! ); - if( !Ready ) - { + LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); + LoadDefaultResources(null!); + if (!Ready) _framework.Update += LoadDefaultResources; - } } - // We store the default data of the resources so we can always restore them. - private void LoadDefaultResources( object _ ) + /// We store the default data of the resources so we can always restore them. + private void LoadDefaultResources(object _) { - if( Address == null ) - { + if (Address == null) return; - } var anyMissing = false; - for( var i = 0; i < RelevantIndices.Length; ++i ) + for (var i = 0; i < RelevantIndices.Length; ++i) { - var list = _lists[ i ]; - if( !list.Ready ) - { - var resource = Address->Resource( RelevantIndices[ i ] ); - var (data, length) = resource->GetData(); - list.SetDefaultResource( data, length ); - anyMissing |= !_lists[ i ].Ready; - } + var list = _lists[i]; + if (list.Ready) + continue; + + var resource = Address->Resource(RelevantIndices[i]); + var (data, length) = resource->GetData(); + list.SetDefaultResource(data, length); + anyMissing |= !_lists[i].Ready; } - if( _defaultTransparentResource == IntPtr.Zero ) + if (DefaultTransparentResource == IntPtr.Zero) { - _defaultTransparentResource = ( IntPtr )Address->TransparentTexResource; - anyMissing |= _defaultTransparentResource == IntPtr.Zero; + DefaultTransparentResource = (IntPtr)Address->TransparentTexResource; + anyMissing |= DefaultTransparentResource == IntPtr.Zero; } - if( _defaultDecalResource == IntPtr.Zero ) + if (DefaultDecalResource == IntPtr.Zero) { - _defaultDecalResource = ( IntPtr )Address->DecalTexResource; - anyMissing |= _defaultDecalResource == IntPtr.Zero; + DefaultDecalResource = (IntPtr)Address->DecalTexResource; + anyMissing |= DefaultDecalResource == IntPtr.Zero; } - if( !anyMissing ) - { - Ready = true; - _framework.Update -= LoadDefaultResources; - LoadingFinished.Invoke(); - } + if (anyMissing) + return; + + Ready = true; + _framework.Update -= LoadDefaultResources; + LoadingFinished.Invoke(); } - public void SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) + public void SetResource(MetaIndex resourceIdx, IntPtr data, int length) { - var idx = ReverseIndices[ ( int )resourceIdx ]; - var list = _lists[ idx.Value ]; - list.SetResource( data, length ); + var idx = ReverseIndices[(int)resourceIdx]; + var list = _lists[idx.Value]; + list.SetResource(data, length); } - public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) + public void ResetResource(MetaIndex resourceIdx) { - var idx = ReverseIndices[ ( int )resourceIdx ]; - var list = _lists[ idx.Value ]; + var idx = ReverseIndices[(int)resourceIdx]; + var list = _lists[idx.Value]; list.ResetResource(); } - public List.MetaReverter TemporarilySetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) + public List.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, IntPtr data, int length) { - var idx = ReverseIndices[ ( int )resourceIdx ]; - var list = _lists[ idx.Value ]; - return list.TemporarilySetResource( data, length ); + var idx = ReverseIndices[(int)resourceIdx]; + var list = _lists[idx.Value]; + return list.TemporarilySetResource(data, length); } - public List.MetaReverter TemporarilyResetResource( Structs.CharacterUtility.Index resourceIdx ) + public List.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx) { - var idx = ReverseIndices[ ( int )resourceIdx ]; - var list = _lists[ idx.Value ]; + var idx = ReverseIndices[(int)resourceIdx]; + var list = _lists[idx.Value]; return list.TemporarilyResetResource(); } - // Return all relevant resources to the default resource. + /// Return all relevant resources to the default resource. public void ResetAll() { - foreach( var list in _lists ) - { + foreach (var list in _lists) list.Dispose(); - } - Address->TransparentTexResource = ( Structs.TextureResourceHandle* )_defaultTransparentResource; - Address->DecalTexResource = ( Structs.TextureResourceHandle* )_defaultDecalResource; + Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; + Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; } public void Dispose() - { - ResetAll(); - } -} \ No newline at end of file + => ResetAll(); +} diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs deleted file mode 100644 index e491de7b..00000000 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.InteropServices; -using Penumbra.GameData.Enums; - -namespace Penumbra.Interop.Structs; - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct CharacterUtility -{ - public enum Index : int - { - Eqp = 0, - Gmp = 2, - - Eqdp0101 = 3, - Eqdp0201, - Eqdp0301, - Eqdp0401, - Eqdp0501, - Eqdp0601, - Eqdp0701, - Eqdp0801, - Eqdp0901, - Eqdp1001, - Eqdp1101, - Eqdp1201, - Eqdp1301, - Eqdp1401, - Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, - Eqdp1801, - Eqdp0104, - Eqdp0204, - Eqdp0504, - Eqdp0604, - Eqdp0704, - Eqdp0804, - Eqdp1304, - Eqdp1404, - Eqdp9104, - Eqdp9204, - - Eqdp0101Acc, - Eqdp0201Acc, - Eqdp0301Acc, - Eqdp0401Acc, - Eqdp0501Acc, - Eqdp0601Acc, - Eqdp0701Acc, - Eqdp0801Acc, - Eqdp0901Acc, - Eqdp1001Acc, - Eqdp1101Acc, - Eqdp1201Acc, - Eqdp1301Acc, - Eqdp1401Acc, - Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, - Eqdp1801Acc, - Eqdp0104Acc, - Eqdp0204Acc, - Eqdp0504Acc, - Eqdp0604Acc, - Eqdp0704Acc, - Eqdp0804Acc, - Eqdp1304Acc, - Eqdp1404Acc, - Eqdp9104Acc, - Eqdp9204Acc, - - HumanCmp = 64, - FaceEst, - HairEst, - HeadEst, - BodyEst, - } - - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - - public static readonly Index[] EqdpIndices = Enum.GetNames< Index >() - .Zip( Enum.GetValues< Index >() ) - .Where( n => n.First.StartsWith( "Eqdp" ) ) - .Select( n => n.Second ).ToArray(); - - public const int TotalNumResources = 87; - - public static Index EqdpIdx( GenderRace raceCode, bool accessory ) - => +( int )raceCode switch - { - 0101 => accessory ? Index.Eqdp0101Acc : Index.Eqdp0101, - 0201 => accessory ? Index.Eqdp0201Acc : Index.Eqdp0201, - 0301 => accessory ? Index.Eqdp0301Acc : Index.Eqdp0301, - 0401 => accessory ? Index.Eqdp0401Acc : Index.Eqdp0401, - 0501 => accessory ? Index.Eqdp0501Acc : Index.Eqdp0501, - 0601 => accessory ? Index.Eqdp0601Acc : Index.Eqdp0601, - 0701 => accessory ? Index.Eqdp0701Acc : Index.Eqdp0701, - 0801 => accessory ? Index.Eqdp0801Acc : Index.Eqdp0801, - 0901 => accessory ? Index.Eqdp0901Acc : Index.Eqdp0901, - 1001 => accessory ? Index.Eqdp1001Acc : Index.Eqdp1001, - 1101 => accessory ? Index.Eqdp1101Acc : Index.Eqdp1101, - 1201 => accessory ? Index.Eqdp1201Acc : Index.Eqdp1201, - 1301 => accessory ? Index.Eqdp1301Acc : Index.Eqdp1301, - 1401 => accessory ? Index.Eqdp1401Acc : Index.Eqdp1401, - 1501 => accessory ? Index.Eqdp1501Acc : Index.Eqdp1501, - //1601 => accessory ? RelevantIndex.Eqdp1601Acc : RelevantIndex.Eqdp1601, Female Hrothgar - 1701 => accessory ? Index.Eqdp1701Acc : Index.Eqdp1701, - 1801 => accessory ? Index.Eqdp1801Acc : Index.Eqdp1801, - 0104 => accessory ? Index.Eqdp0104Acc : Index.Eqdp0104, - 0204 => accessory ? Index.Eqdp0204Acc : Index.Eqdp0204, - 0504 => accessory ? Index.Eqdp0504Acc : Index.Eqdp0504, - 0604 => accessory ? Index.Eqdp0604Acc : Index.Eqdp0604, - 0704 => accessory ? Index.Eqdp0704Acc : Index.Eqdp0704, - 0804 => accessory ? Index.Eqdp0804Acc : Index.Eqdp0804, - 1304 => accessory ? Index.Eqdp1304Acc : Index.Eqdp1304, - 1404 => accessory ? Index.Eqdp1404Acc : Index.Eqdp1404, - 9104 => accessory ? Index.Eqdp9104Acc : Index.Eqdp9104, - 9204 => accessory ? Index.Eqdp9204Acc : Index.Eqdp9204, - _ => ( Index )( -1 ), - }; - - [FieldOffset( 0 )] - public void* VTable; - - [FieldOffset( 8 )] - public fixed ulong Resources[TotalNumResources]; - - [FieldOffset( 8 + ( int )Index.Eqp * 8 )] - public ResourceHandle* EqpResource; - - [FieldOffset( 8 + ( int )Index.Gmp * 8 )] - public ResourceHandle* GmpResource; - - public ResourceHandle* Resource( int idx ) - => ( ResourceHandle* )Resources[ idx ]; - - public ResourceHandle* Resource( Index idx ) - => Resource( ( int )idx ); - - public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) - => Resource( ( int )EqdpIdx( raceCode, accessory ) ); - - [FieldOffset( 8 + ( int )Index.HumanCmp * 8 )] - public ResourceHandle* HumanCmpResource; - - [FieldOffset( 8 + ( int )Index.FaceEst * 8 )] - public ResourceHandle* FaceEstResource; - - [FieldOffset( 8 + ( int )Index.HairEst * 8 )] - public ResourceHandle* HairEstResource; - - [FieldOffset( 8 + ( int )Index.BodyEst * 8 )] - public ResourceHandle* BodyEstResource; - - [FieldOffset( 8 + ( int )Index.HeadEst * 8 )] - public ResourceHandle* HeadEstResource; - - [FieldOffset( 8 + IndexTransparentTex * 8 )] - public TextureResourceHandle* TransparentTexResource; - - [FieldOffset( 8 + IndexDecalTex * 8 )] - public TextureResourceHandle* DecalTexResource; - - // not included resources have no known use case. -} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs new file mode 100644 index 00000000..b273091b --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterUtilityData +{ + public const int IndexTransparentTex = 72; + public const int IndexDecalTex = 73; + + public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() + .Zip( Enum.GetValues< MetaIndex >() ) + .Where( n => n.First.StartsWith( "Eqdp" ) ) + .Select( n => n.Second ).ToArray(); + + public const int TotalNumResources = 87; + + /// Obtain the index for the eqdp file corresponding to the given race code and accessory. + public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) + => +( int )raceCode switch + { + 0101 => accessory ? MetaIndex.Eqdp0101Acc : MetaIndex.Eqdp0101, + 0201 => accessory ? MetaIndex.Eqdp0201Acc : MetaIndex.Eqdp0201, + 0301 => accessory ? MetaIndex.Eqdp0301Acc : MetaIndex.Eqdp0301, + 0401 => accessory ? MetaIndex.Eqdp0401Acc : MetaIndex.Eqdp0401, + 0501 => accessory ? MetaIndex.Eqdp0501Acc : MetaIndex.Eqdp0501, + 0601 => accessory ? MetaIndex.Eqdp0601Acc : MetaIndex.Eqdp0601, + 0701 => accessory ? MetaIndex.Eqdp0701Acc : MetaIndex.Eqdp0701, + 0801 => accessory ? MetaIndex.Eqdp0801Acc : MetaIndex.Eqdp0801, + 0901 => accessory ? MetaIndex.Eqdp0901Acc : MetaIndex.Eqdp0901, + 1001 => accessory ? MetaIndex.Eqdp1001Acc : MetaIndex.Eqdp1001, + 1101 => accessory ? MetaIndex.Eqdp1101Acc : MetaIndex.Eqdp1101, + 1201 => accessory ? MetaIndex.Eqdp1201Acc : MetaIndex.Eqdp1201, + 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, + 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, + 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, + //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, + 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, + 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, + 0204 => accessory ? MetaIndex.Eqdp0204Acc : MetaIndex.Eqdp0204, + 0504 => accessory ? MetaIndex.Eqdp0504Acc : MetaIndex.Eqdp0504, + 0604 => accessory ? MetaIndex.Eqdp0604Acc : MetaIndex.Eqdp0604, + 0704 => accessory ? MetaIndex.Eqdp0704Acc : MetaIndex.Eqdp0704, + 0804 => accessory ? MetaIndex.Eqdp0804Acc : MetaIndex.Eqdp0804, + 1304 => accessory ? MetaIndex.Eqdp1304Acc : MetaIndex.Eqdp1304, + 1404 => accessory ? MetaIndex.Eqdp1404Acc : MetaIndex.Eqdp1404, + 9104 => accessory ? MetaIndex.Eqdp9104Acc : MetaIndex.Eqdp9104, + 9204 => accessory ? MetaIndex.Eqdp9204Acc : MetaIndex.Eqdp9204, + _ => ( MetaIndex )( -1 ), + }; + + [FieldOffset( 0 )] + public void* VTable; + + [FieldOffset( 8 )] + public fixed ulong Resources[TotalNumResources]; + + [FieldOffset( 8 + ( int )MetaIndex.Eqp * 8 )] + public ResourceHandle* EqpResource; + + [FieldOffset( 8 + ( int )MetaIndex.Gmp * 8 )] + public ResourceHandle* GmpResource; + + public ResourceHandle* Resource( int idx ) + => ( ResourceHandle* )Resources[ idx ]; + + public ResourceHandle* Resource( MetaIndex idx ) + => Resource( ( int )idx ); + + public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) + => Resource( ( int )EqdpIdx( raceCode, accessory ) ); + + [FieldOffset( 8 + ( int )MetaIndex.HumanCmp * 8 )] + public ResourceHandle* HumanCmpResource; + + [FieldOffset( 8 + ( int )MetaIndex.FaceEst * 8 )] + public ResourceHandle* FaceEstResource; + + [FieldOffset( 8 + ( int )MetaIndex.HairEst * 8 )] + public ResourceHandle* HairEstResource; + + [FieldOffset( 8 + ( int )MetaIndex.BodyEst * 8 )] + public ResourceHandle* BodyEstResource; + + [FieldOffset( 8 + ( int )MetaIndex.HeadEst * 8 )] + public ResourceHandle* HeadEstResource; + + [FieldOffset( 8 + IndexTransparentTex * 8 )] + public TextureResourceHandle* TransparentTexResource; + + [FieldOffset( 8 + IndexDecalTex * 8 )] + public TextureResourceHandle* DecalTexResource; + + // not included resources have no known use case. +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs new file mode 100644 index 00000000..65302264 --- /dev/null +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -0,0 +1,74 @@ +namespace Penumbra.Interop.Structs; + +/// Indices for the different meta files contained in CharacterUtility. +public enum MetaIndex : int +{ + Eqp = 0, + Gmp = 2, + + Eqdp0101 = 3, + Eqdp0201, + Eqdp0301, + Eqdp0401, + Eqdp0501, + Eqdp0601, + Eqdp0701, + Eqdp0801, + Eqdp0901, + Eqdp1001, + Eqdp1101, + Eqdp1201, + Eqdp1301, + Eqdp1401, + Eqdp1501, + + //Eqdp1601, // TODO: female Hrothgar + Eqdp1701 = Eqdp1501 + 2, + Eqdp1801, + Eqdp0104, + Eqdp0204, + Eqdp0504, + Eqdp0604, + Eqdp0704, + Eqdp0804, + Eqdp1304, + Eqdp1404, + Eqdp9104, + Eqdp9204, + + Eqdp0101Acc, + Eqdp0201Acc, + Eqdp0301Acc, + Eqdp0401Acc, + Eqdp0501Acc, + Eqdp0601Acc, + Eqdp0701Acc, + Eqdp0801Acc, + Eqdp0901Acc, + Eqdp1001Acc, + Eqdp1101Acc, + Eqdp1201Acc, + Eqdp1301Acc, + Eqdp1401Acc, + Eqdp1501Acc, + + //Eqdp1601Acc, // TODO: female Hrothgar + Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1801Acc, + Eqdp0104Acc, + Eqdp0204Acc, + Eqdp0504Acc, + Eqdp0604Acc, + Eqdp0704Acc, + Eqdp0804Acc, + Eqdp1304Acc, + Eqdp1404Acc, + Eqdp9104Acc, + Eqdp9204Acc, + + HumanCmp = 64, + FaceEst, + HairEst, + HeadEst, + BodyEst, +} From f38a25229588d57dfbd7eeab1ab75e8cd984c285 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 20:42:34 +0100 Subject: [PATCH 0822/2451] More renaming... --- Penumbra/Api/PenumbraApi.cs | 4 +- Penumbra/Collections/CollectionManager.cs | 1 + .../Collections/ModCollection.Cache.Access.cs | 24 +-- Penumbra/CommandHandler.cs | 1 + .../Interop/CharacterUtility.DecalReverter.cs | 66 ------- Penumbra/Interop/CharacterUtility.List.cs | 166 ------------------ .../PathResolving/AnimationHookService.cs | 2 +- .../PathResolving/CollectionResolver.cs | 2 +- .../Interop/PathResolving/CutsceneService.cs | 2 +- .../Interop/PathResolving/DrawObjectState.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 26 +-- .../Interop/PathResolving/PathResolver.cs | 4 +- Penumbra/Interop/PathResolving/PathState.cs | 2 +- .../Interop/PathResolving/ResolvePathHooks.cs | 2 +- .../Interop/PathResolving/SubfileHelper.cs | 4 +- .../ResourceLoading/CreateFileWHook.cs | 2 +- .../ResourceLoading/FileReadService.cs | 2 +- .../Interop/ResourceLoading/ResourceLoader.cs | 2 +- .../ResourceLoading/ResourceManagerService.cs | 2 +- .../ResourceLoading/ResourceService.cs | 2 +- .../Interop/ResourceLoading/TexMdlService.cs | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 2 +- .../Interop/Services/CharacterUtility.List.cs | 166 ++++++++++++++++++ .../{ => Services}/CharacterUtility.cs | 40 ++--- Penumbra/Interop/Services/DecalReverter.cs | 61 +++++++ Penumbra/Interop/Services/RedrawService.cs | 2 +- Penumbra/Meta/Files/CmpFile.cs | 7 +- Penumbra/Meta/Files/EqdpFile.cs | 7 +- Penumbra/Meta/Files/EqpGmpFile.cs | 13 +- Penumbra/Meta/Files/EstFile.cs | 11 +- Penumbra/Meta/Files/EvpFile.cs | 3 +- Penumbra/Meta/Files/MetaBaseFile.cs | 5 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 9 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 23 +-- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 9 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 27 +-- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 9 +- Penumbra/Meta/Manager/MetaManager.cs | 49 +++--- .../Meta/Manipulations/EqdpManipulation.cs | 4 +- .../Meta/Manipulations/EqpManipulation.cs | 4 +- .../Meta/Manipulations/EstManipulation.cs | 12 +- .../Meta/Manipulations/GmpManipulation.cs | 4 +- .../Meta/Manipulations/ImcManipulation.cs | 4 +- .../Meta/Manipulations/MetaManipulation.cs | 2 +- .../Meta/Manipulations/RspManipulation.cs | 4 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 4 +- Penumbra/Penumbra.cs | 6 +- Penumbra/PenumbraNew.cs | 5 +- Penumbra/Services/Wrappers.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- Penumbra/UI/Tabs/DebugTab.cs | 8 +- Penumbra/UI/Tabs/ModsTab.cs | 1 + Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 1 + 55 files changed, 423 insertions(+), 407 deletions(-) delete mode 100644 Penumbra/Interop/CharacterUtility.DecalReverter.cs delete mode 100644 Penumbra/Interop/CharacterUtility.List.cs create mode 100644 Penumbra/Interop/Services/CharacterUtility.List.cs rename Penumbra/Interop/{ => Services}/CharacterUtility.cs (79%) create mode 100644 Penumbra/Interop/Services/DecalReverter.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8b3f6a4b..3c666dc4 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -3,7 +3,7 @@ using Lumina.Data; using Newtonsoft.Json; using OtterGui; using Penumbra.Collections; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -17,7 +17,7 @@ using System.Runtime.CompilerServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index f0368e5a..0e3fb35a 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -13,6 +13,7 @@ using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.Util; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8daa3c04..4ebcca8f 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; +using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Collections; @@ -151,7 +153,7 @@ public partial class ModCollection } } - public void SetMetaFile(Interop.Structs.CharacterUtility.Index idx) + public void SetMetaFile(MetaIndex idx) { if (_cache == null) Penumbra.CharacterUtility.ResetResource(idx); @@ -160,23 +162,23 @@ public partial class ModCollection } // Used for short periods of changed files. - public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => _cache?.MetaManipulations.TemporarilySetEqdpFile(genderRace, accessory) - ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.EqdpIdx(genderRace, accessory)); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); - public CharacterUtility.List.MetaReverter TemporarilySetEqpFile() + public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() => _cache?.MetaManipulations.TemporarilySetEqpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Eqp); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Eqp); - public CharacterUtility.List.MetaReverter TemporarilySetGmpFile() + public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() => _cache?.MetaManipulations.TemporarilySetGmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.Gmp); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Gmp); - public CharacterUtility.List.MetaReverter TemporarilySetCmpFile() + public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() => _cache?.MetaManipulations.TemporarilySetCmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtility.Index.HumanCmp); + ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.HumanCmp); - public CharacterUtility.List.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) => _cache?.MetaManipulations.TemporarilySetEstFile(type) - ?? Penumbra.CharacterUtility.TemporarilyResetResource((Interop.Structs.CharacterUtility.Index)type); + ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 760a4870..120d1787 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -9,6 +9,7 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; diff --git a/Penumbra/Interop/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs deleted file mode 100644 index b49e0795..00000000 --- a/Penumbra/Interop/CharacterUtility.DecalReverter.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; -using Penumbra.String.Classes; - -namespace Penumbra.Interop; - -public unsafe partial class CharacterUtility -{ - public sealed class DecalReverter : IDisposable - { - public static readonly Utf8GamePath DecalPath = - Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty; - - public static readonly Utf8GamePath TransparentPath = - Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; - - private readonly Structs.TextureResourceHandle* _decal; - private readonly Structs.TextureResourceHandle* _transparent; - - public DecalReverter( ResourceService resources, ModCollection? collection, bool doDecal ) - { - var ptr = Penumbra.CharacterUtility.Address; - _decal = null; - _transparent = null; - if( doDecal ) - { - var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path; - var decalHandle = resources.GetResource( ResourceCategory.Chara, ResourceType.Tex, decalPath ); - _decal = ( Structs.TextureResourceHandle* )decalHandle; - if( _decal != null ) - { - ptr->DecalTexResource = _decal; - } - } - else - { - var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path; - var transparentHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, transparentPath); - _transparent = ( Structs.TextureResourceHandle* )transparentHandle; - if( _transparent != null ) - { - ptr->TransparentTexResource = _transparent; - } - } - } - - public void Dispose() - { - var ptr = Penumbra.CharacterUtility.Address; - if( _decal != null ) - { - ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource; - --_decal->Handle.RefCount; - } - - if( _transparent != null ) - { - ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource; - --_transparent->Handle.RefCount; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs deleted file mode 100644 index 3cf137e8..00000000 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Penumbra.Interop; - -public unsafe partial class CharacterUtility -{ - public class List : IDisposable - { - private readonly LinkedList< MetaReverter > _entries = new(); - public readonly InternalIndex Index; - public readonly Structs.CharacterUtility.Index GlobalIndex; - - public IReadOnlyCollection< MetaReverter > Entries - => _entries; - - private IntPtr _defaultResourceData = IntPtr.Zero; - private int _defaultResourceSize = 0; - public bool Ready { get; private set; } = false; - - public List( InternalIndex index ) - { - Index = index; - GlobalIndex = RelevantIndices[ index.Value ]; - } - - public void SetDefaultResource( IntPtr data, int size ) - { - if( !Ready ) - { - _defaultResourceData = data; - _defaultResourceSize = size; - Ready = _defaultResourceData != IntPtr.Zero && size != 0; - if( _entries.Count > 0 ) - { - var first = _entries.First!.Value; - SetResource( first.Data, first.Length ); - } - } - } - - public (IntPtr Address, int Size) DefaultResource - => ( _defaultResourceData, _defaultResourceSize ); - - public MetaReverter TemporarilySetResource( IntPtr data, int length ) - { - Penumbra.Log.Verbose( $"Temporarily set resource {GlobalIndex} to 0x{( ulong )data:X} ({length} bytes)." ); - var reverter = new MetaReverter( this, data, length ); - _entries.AddFirst( reverter ); - SetResourceInternal( data, length ); - return reverter; - } - - public MetaReverter TemporarilyResetResource() - { - Penumbra.Log.Verbose( - $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); - var reverter = new MetaReverter( this ); - _entries.AddFirst( reverter ); - ResetResourceInternal(); - return reverter; - } - - public void SetResource( IntPtr data, int length ) - { - Penumbra.Log.Verbose( $"Set resource {GlobalIndex} to 0x{( ulong )data:X} ({length} bytes)." ); - SetResourceInternal( data, length ); - } - - public void ResetResource() - { - Penumbra.Log.Verbose( $"Reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); - ResetResourceInternal(); - } - - - // Set the currently stored data of this resource to new values. - private void SetResourceInternal( IntPtr data, int length ) - { - if( Ready ) - { - var resource = Penumbra.CharacterUtility.Address->Resource( GlobalIndex ); - resource->SetData( data, length ); - } - } - - // Reset the currently stored data of this resource to its default values. - private void ResetResourceInternal() - => SetResourceInternal( _defaultResourceData, _defaultResourceSize ); - - private void SetResourceToDefaultCollection() - => Penumbra.CollectionManager.Default.SetMetaFile( GlobalIndex ); - - public void Dispose() - { - if( _entries.Count > 0 ) - { - foreach( var entry in _entries ) - { - entry.Disposed = true; - } - - _entries.Clear(); - } - - ResetResourceInternal(); - } - - public sealed class MetaReverter : IDisposable - { - public readonly List List; - public readonly IntPtr Data; - public readonly int Length; - public readonly bool Resetter; - public bool Disposed; - - public MetaReverter( List list, IntPtr data, int length ) - { - List = list; - Data = data; - Length = length; - } - - public MetaReverter( List list ) - { - List = list; - Data = IntPtr.Zero; - Length = 0; - Resetter = true; - } - - public void Dispose() - { - if( !Disposed ) - { - var list = List._entries; - var wasCurrent = ReferenceEquals( this, list.First?.Value ); - list.Remove( this ); - if( !wasCurrent ) - { - return; - } - - if( list.Count == 0 ) - { - List.SetResourceToDefaultCollection(); - } - else - { - var next = list.First!.Value; - if( next.Resetter ) - { - List.ResetResourceInternal(); - } - else - { - List.SetResourceInternal( next.Data, next.Length ); - } - } - - Disposed = true; - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index b67823d9..ba47289f 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -12,7 +12,7 @@ using Penumbra.String.Classes; using Penumbra.Util; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public unsafe class AnimationHookService : IDisposable { diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1b1a145d..e4a7a0a9 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -17,7 +17,7 @@ using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public unsafe class CollectionResolver { diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 54a86772..2c59a086 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -7,7 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public class CutsceneService : IDisposable { diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index b7e1ca1d..e5ce7026 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -14,7 +14,7 @@ using Penumbra.Interop.Services; using Penumbra.String.Classes; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public class DrawObjectState : IDisposable, IReadOnlyDictionary { diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index cfe9419f..febd7414 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -8,7 +8,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; @@ -16,7 +16,7 @@ using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using static Penumbra.GameData.Enums.GenderRace; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; // State: 6.35 // GetSlotEqpData seems to be the only function using the EQP table. @@ -45,23 +45,25 @@ public unsafe class MetaState : IDisposable [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; - private readonly CommunicatorService _communicator; - private readonly PerformanceTracker _performance; - private readonly CollectionResolver _collectionResolver; - private readonly ResourceService _resources; - private readonly GameEventManager _gameEventManager; + private readonly CommunicatorService _communicator; + private readonly PerformanceTracker _performance; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceService _resources; + private readonly GameEventManager _gameEventManager; + private readonly Services.CharacterUtility _characterUtility; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceService resources, GameEventManager gameEventManager) + ResourceService resources, GameEventManager gameEventManager, Services.CharacterUtility characterUtility) { _performance = performance; _communicator = communicator; _collectionResolver = collectionResolver; _resources = resources; _gameEventManager = gameEventManager; + _characterUtility = characterUtility; SignatureHelper.Initialise(this); _onModelLoadCompleteHook = Hook.FromAddress(_humanVTable[58], OnModelLoadCompleteDetour); _getEqpIndirectHook.Enable(); @@ -86,7 +88,7 @@ public unsafe class MetaState : IDisposable resolveData = ResolveData.Invalid; return false; - } + } public static DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) { @@ -104,7 +106,7 @@ public unsafe class MetaState : IDisposable } public static GenderRace GetHumanGenderRace(nint human) - => (GenderRace)((Human*)human)->RaceSexId; + => (GenderRace)((Human*)human)->RaceSexId; public void Dispose() { @@ -125,7 +127,7 @@ public unsafe class MetaState : IDisposable _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); - var decal = new CharacterUtility.DecalReverter(_resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); + var decal = new DecalReverter(_characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); @@ -252,7 +254,7 @@ public unsafe class MetaState : IDisposable var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); using var decals = - new CharacterUtility.DecalReverter(_resources, resolveData.ModCollection, UsesDecal(0, data)); + new DecalReverter(_characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); var ret = _changeCustomize.Original(human, data, skipEquipment); _inChangeCustomize = false; return ret; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 59992d16..ea1bb2b1 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -4,13 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public class PathResolver : IDisposable { diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index a3a4064c..440aed32 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -7,7 +7,7 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.String; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public unsafe class PathState : IDisposable { diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index f2945bcc..fab9fd92 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -6,7 +6,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; public unsafe class ResolvePathHooks : IDisposable { diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 323ca9c1..83393d40 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -8,14 +8,14 @@ using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; -namespace Penumbra.Interop.Resolver; +namespace Penumbra.Interop.PathResolving; /// /// Materials and avfx do contain their own paths to textures and shader packages or atex respectively. diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index a16d5db7..3f8c8d27 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -7,7 +7,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; /// /// To allow XIV to load files of arbitrary path length, diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 6b89b576..b09d568e 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -8,7 +8,7 @@ using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.Util; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; public unsafe class FileReadService : IDisposable { diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index a7312643..3ec38c87 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -8,7 +8,7 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; public unsafe class ResourceLoader : IDisposable { diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index fd2b0e4d..4458e699 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -8,7 +8,7 @@ using FFXIVClientStructs.STD; using Penumbra.GameData; using Penumbra.GameData.Enums; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; public unsafe class ResourceManagerService { diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 447c2c03..1027ed5f 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -9,7 +9,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; public unsafe class ResourceService : IDisposable { diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index c60c7b79..e3d48cec 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -7,7 +7,7 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.String.Classes; -namespace Penumbra.Interop.Loader; +namespace Penumbra.Interop.ResourceLoading; public unsafe class TexMdlService { diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index b177e404..161f896f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData; using Penumbra.GameData.Actors; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.PathResolving; using Penumbra.Services; namespace Penumbra.Interop.ResourceTree; diff --git a/Penumbra/Interop/Services/CharacterUtility.List.cs b/Penumbra/Interop/Services/CharacterUtility.List.cs new file mode 100644 index 00000000..3e847c0f --- /dev/null +++ b/Penumbra/Interop/Services/CharacterUtility.List.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Services; + +public unsafe partial class CharacterUtility +{ + public class MetaList : IDisposable + { + private readonly LinkedList _entries = new(); + public readonly InternalIndex Index; + public readonly MetaIndex GlobalMetaIndex; + + public IReadOnlyCollection Entries + => _entries; + + private nint _defaultResourceData = nint.Zero; + private int _defaultResourceSize = 0; + public bool Ready { get; private set; } = false; + + public MetaList(InternalIndex index) + { + Index = index; + GlobalMetaIndex = RelevantIndices[index.Value]; + } + + public void SetDefaultResource(nint data, int size) + { + if (Ready) + return; + + _defaultResourceData = data; + _defaultResourceSize = size; + Ready = _defaultResourceData != nint.Zero && size != 0; + if (_entries.Count <= 0) + return; + + var first = _entries.First!.Value; + SetResource(first.Data, first.Length); + } + + public (nint Address, int Size) DefaultResource + => (_defaultResourceData, _defaultResourceSize); + + public MetaReverter TemporarilySetResource(nint data, int length) + { +#if false + Penumbra.Log.Verbose($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); +#endif + var reverter = new MetaReverter(this, data, length); + _entries.AddFirst(reverter); + SetResourceInternal(data, length); + return reverter; + } + + public MetaReverter TemporarilyResetResource() + { +#if false + Penumbra.Log.Verbose( + $"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); +#endif + var reverter = new MetaReverter(this); + _entries.AddFirst(reverter); + ResetResourceInternal(); + return reverter; + } + + public void SetResource(nint data, int length) + { +#if false + Penumbra.Log.Verbose($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); +#endif + SetResourceInternal(data, length); + } + + public void ResetResource() + { +#if false + Penumbra.Log.Verbose($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); +#endif + ResetResourceInternal(); + } + + // Set the currently stored data of this resource to new values. + private void SetResourceInternal(nint data, int length) + { + if (!Ready) + return; + + var resource = Penumbra.CharacterUtility.Address->Resource(GlobalMetaIndex); + resource->SetData(data, length); + } + + // Reset the currently stored data of this resource to its default values. + private void ResetResourceInternal() + => SetResourceInternal(_defaultResourceData, _defaultResourceSize); + + private void SetResourceToDefaultCollection() + => Penumbra.CollectionManager.Default.SetMetaFile(GlobalMetaIndex); + + public void Dispose() + { + if (_entries.Count > 0) + { + foreach (var entry in _entries) + entry.Disposed = true; + + _entries.Clear(); + } + + ResetResourceInternal(); + } + + public sealed class MetaReverter : IDisposable + { + public readonly MetaList MetaList; + public readonly nint Data; + public readonly int Length; + public readonly bool Resetter; + public bool Disposed; + + public MetaReverter(MetaList metaList, nint data, int length) + { + MetaList = metaList; + Data = data; + Length = length; + } + + public MetaReverter(MetaList metaList) + { + MetaList = metaList; + Data = nint.Zero; + Length = 0; + Resetter = true; + } + + public void Dispose() + { + if (Disposed) + return; + + var list = MetaList._entries; + var wasCurrent = ReferenceEquals(this, list.First?.Value); + list.Remove(this); + if (!wasCurrent) + return; + + if (list.Count == 0) + { + MetaList.SetResourceToDefaultCollection(); + } + else + { + var next = list.First!.Value; + if (next.Resetter) + MetaList.ResetResourceInternal(); + else + MetaList.SetResourceInternal(next.Data, next.Length); + } + + Disposed = true; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs similarity index 79% rename from Penumbra/Interop/CharacterUtility.cs rename to Penumbra/Interop/Services/CharacterUtility.cs index 63962195..17052101 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -6,7 +6,7 @@ using Dalamud.Utility.Signatures; using Penumbra.GameData; using Penumbra.Interop.Structs; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Services; public unsafe partial class CharacterUtility : IDisposable { @@ -16,7 +16,7 @@ public unsafe partial class CharacterUtility : IDisposable [Signature(Sigs.CharacterUtility, ScanType = ScanType.StaticAddress)] private readonly CharacterUtilityData** _characterUtilityAddress = null; - /// Only required for migration anymore. + /// Only required for migration anymore. public delegate void LoadResources(CharacterUtilityData* address); [Signature(Sigs.LoadCharacterResources)] @@ -30,12 +30,12 @@ public unsafe partial class CharacterUtility : IDisposable public bool Ready { get; private set; } public event Action LoadingFinished; - public IntPtr DefaultTransparentResource { get; private set; } - public IntPtr DefaultDecalResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } - /// + /// /// The relevant indices depend on which meta manipulations we allow for. - /// The defines are set in the project configuration. + /// The defines are set in the project configuration. /// public static readonly MetaIndex[] RelevantIndices = Enum.GetValues(); @@ -45,14 +45,14 @@ public unsafe partial class CharacterUtility : IDisposable .Select(i => new InternalIndex(Array.IndexOf(RelevantIndices, (MetaIndex)i))) .ToArray(); - private readonly List[] _lists = Enumerable.Range(0, RelevantIndices.Length) - .Select(idx => new List(new InternalIndex(idx))) + private readonly MetaList[] _lists = Enumerable.Range(0, RelevantIndices.Length) + .Select(idx => new MetaList(new InternalIndex(idx))) .ToArray(); - public IReadOnlyList Lists + public IReadOnlyList Lists => _lists; - public (IntPtr Address, int Size) DefaultResource(InternalIndex idx) + public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; private readonly Framework _framework; @@ -67,7 +67,7 @@ public unsafe partial class CharacterUtility : IDisposable _framework.Update += LoadDefaultResources; } - /// We store the default data of the resources so we can always restore them. + /// We store the default data of the resources so we can always restore them. private void LoadDefaultResources(object _) { if (Address == null) @@ -86,16 +86,16 @@ public unsafe partial class CharacterUtility : IDisposable anyMissing |= !_lists[i].Ready; } - if (DefaultTransparentResource == IntPtr.Zero) + if (DefaultTransparentResource == nint.Zero) { - DefaultTransparentResource = (IntPtr)Address->TransparentTexResource; - anyMissing |= DefaultTransparentResource == IntPtr.Zero; + DefaultTransparentResource = (nint)Address->TransparentTexResource; + anyMissing |= DefaultTransparentResource == nint.Zero; } - if (DefaultDecalResource == IntPtr.Zero) + if (DefaultDecalResource == nint.Zero) { - DefaultDecalResource = (IntPtr)Address->DecalTexResource; - anyMissing |= DefaultDecalResource == IntPtr.Zero; + DefaultDecalResource = (nint)Address->DecalTexResource; + anyMissing |= DefaultDecalResource == nint.Zero; } if (anyMissing) @@ -106,7 +106,7 @@ public unsafe partial class CharacterUtility : IDisposable LoadingFinished.Invoke(); } - public void SetResource(MetaIndex resourceIdx, IntPtr data, int length) + public void SetResource(MetaIndex resourceIdx, nint data, int length) { var idx = ReverseIndices[(int)resourceIdx]; var list = _lists[idx.Value]; @@ -120,14 +120,14 @@ public unsafe partial class CharacterUtility : IDisposable list.ResetResource(); } - public List.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, IntPtr data, int length) + public MetaList.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, nint data, int length) { var idx = ReverseIndices[(int)resourceIdx]; var list = _lists[idx.Value]; return list.TemporarilySetResource(data, length); } - public List.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx) + public MetaList.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx) { var idx = ReverseIndices[(int)resourceIdx]; var list = _lists[idx.Value]; diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs new file mode 100644 index 00000000..cc9c6403 --- /dev/null +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -0,0 +1,61 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class DecalReverter : IDisposable +{ + public static readonly Utf8GamePath DecalPath = + Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty; + + public static readonly Utf8GamePath TransparentPath = + Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; + + private readonly CharacterUtility _utility; + private readonly Structs.TextureResourceHandle* _decal; + private readonly Structs.TextureResourceHandle* _transparent; + + public DecalReverter(CharacterUtility utility, ResourceService resources, ModCollection? collection, bool doDecal) + { + _utility = utility; + var ptr = _utility.Address; + _decal = null; + _transparent = null; + if (doDecal) + { + var decalPath = collection?.ResolvePath(DecalPath)?.InternalName ?? DecalPath.Path; + var decalHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, decalPath); + _decal = (Structs.TextureResourceHandle*)decalHandle; + if (_decal != null) + ptr->DecalTexResource = _decal; + } + else + { + var transparentPath = collection?.ResolvePath(TransparentPath)?.InternalName ?? TransparentPath.Path; + var transparentHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, transparentPath); + _transparent = (Structs.TextureResourceHandle*)transparentHandle; + if (_transparent != null) + ptr->TransparentTexResource = _transparent; + } + } + + public void Dispose() + { + var ptr = _utility.Address; + if (_decal != null) + { + ptr->DecalTexResource = (Structs.TextureResourceHandle*)_utility.DefaultDecalResource; + --_decal->Handle.RefCount; + } + + if (_transparent != null) + { + ptr->TransparentTexResource = (Structs.TextureResourceHandle*)_utility.DefaultTransparentResource; + --_transparent->Handle.RefCount; + } + } +} diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 6739e95e..d85204b1 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -14,7 +14,7 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; using Penumbra.Services; -namespace Penumbra.Interop; +namespace Penumbra.Interop.Services; public unsafe partial class RedrawService { diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 6f4b4842..1696abf6 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -3,6 +3,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using System.Collections.Generic; +using Penumbra.Interop.Services; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -11,8 +12,8 @@ namespace Penumbra.Meta.Files; // We only support manipulating the racial scaling parameters at the moment. public sealed unsafe class CmpFile : MetaBaseFile { - public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = - Interop.CharacterUtility.ReverseIndices[ ( int )CharacterUtility.Index.HumanCmp ]; + public static readonly CharacterUtility.InternalIndex InternalIndex = + CharacterUtility.ReverseIndices[ ( int )MetaIndex.HumanCmp ]; private const int RacialScalingStart = 0x2A800; @@ -34,7 +35,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public CmpFile() - : base( CharacterUtility.Index.HumanCmp ) + : base( MetaIndex.HumanCmp ) { AllocateData( DefaultData.Length ); Reset(); diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index f3c54ca6..0122fe5a 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String.Functions; @@ -96,7 +97,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } public ExpandedEqdpFile( GenderRace raceCode, bool accessory ) - : base( CharacterUtility.EqdpIdx( raceCode, accessory ) ) + : base( CharacterUtilityData.EqdpIdx( raceCode, accessory ) ) { var def = ( byte* )DefaultData.Data; var blockSize = *( ushort* )( def + IdentifierSize ); @@ -114,7 +115,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public EqdpEntry GetDefault( int setIdx ) => GetDefault( Index, setIdx ); - public static EqdpEntry GetDefault( Interop.CharacterUtility.InternalIndex idx, int setIdx ) + public static EqdpEntry GetDefault( CharacterUtility.InternalIndex idx, int setIdx ) => GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( idx ).Address, setIdx ); public static EqdpEntry GetDefault( byte* data, int setIdx ) @@ -139,5 +140,5 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) - => GetDefault( Interop.CharacterUtility.ReverseIndices[ ( int )CharacterUtility.EqdpIdx( raceCode, accessory ) ], setIdx ); + => GetDefault( CharacterUtility.ReverseIndices[ ( int )CharacterUtilityData.EqdpIdx( raceCode, accessory ) ], setIdx ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 8134ed60..8ad3e95f 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Numerics; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String.Functions; @@ -75,13 +76,13 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } public ExpandedEqpGmpBase( bool gmp ) - : base( gmp ? CharacterUtility.Index.Gmp : CharacterUtility.Index.Eqp ) + : base( gmp ? MetaIndex.Gmp : MetaIndex.Eqp ) { AllocateData( MaxSize ); Reset(); } - protected static ulong GetDefaultInternal( Interop.CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def ) + protected static ulong GetDefaultInternal( CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def ) { var data = ( byte* )Penumbra.CharacterUtility.DefaultResource(fileIndex).Address; if( setIdx == 0 ) @@ -111,8 +112,8 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable { - public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = - Interop.CharacterUtility.ReverseIndices[ (int) CharacterUtility.Index.Eqp ]; + public static readonly CharacterUtility.InternalIndex InternalIndex = + CharacterUtility.ReverseIndices[ (int) MetaIndex.Eqp ]; public ExpandedEqpFile() : base( false ) @@ -158,8 +159,8 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable { - public static readonly Interop.CharacterUtility.InternalIndex InternalIndex = - Interop.CharacterUtility.ReverseIndices[( int )CharacterUtility.Index.Gmp]; + public static readonly CharacterUtility.InternalIndex InternalIndex = + CharacterUtility.ReverseIndices[( int )MetaIndex.Gmp]; public ExpandedGmpFile() : base( true ) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index d0ab8510..d2d36f28 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; @@ -175,7 +176,7 @@ public sealed unsafe class EstFile : MetaBaseFile } public EstFile( EstManipulation.EstType estType ) - : base( ( CharacterUtility.Index )estType ) + : base( ( MetaIndex )estType ) { var length = DefaultData.Length; AllocateData( length + IncreaseSize ); @@ -185,7 +186,7 @@ public sealed unsafe class EstFile : MetaBaseFile public ushort GetDefault( GenderRace genderRace, ushort setId ) => GetDefault( Index, genderRace, setId ); - public static ushort GetDefault( Interop.CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId ) + public static ushort GetDefault( CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId ) { var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( index ).Address; var count = *( int* )data; @@ -199,9 +200,9 @@ public sealed unsafe class EstFile : MetaBaseFile return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize ); } - public static ushort GetDefault( CharacterUtility.Index index, GenderRace genderRace, ushort setId ) - => GetDefault( Interop.CharacterUtility.ReverseIndices[ ( int )index ], genderRace, setId ); + public static ushort GetDefault( MetaIndex metaIndex, GenderRace genderRace, ushort setId ) + => GetDefault( CharacterUtility.ReverseIndices[ ( int )metaIndex ], genderRace, setId ); public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) - => GetDefault( ( CharacterUtility.Index )estType, genderRace, setId ); + => GetDefault( ( MetaIndex )estType, genderRace, setId ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index 5b72c449..c475011a 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; @@ -64,6 +65,6 @@ public unsafe class EvpFile : MetaBaseFile } public EvpFile() - : base( ( Interop.Structs.CharacterUtility.Index )1 ) // TODO: Name + : base( ( MetaIndex )1 ) // TODO: Name { } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index f90ac48d..4307a78d 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,7 +1,8 @@ using System; using Dalamud.Memory; +using Penumbra.Interop.Structs; using Penumbra.String.Functions; -using CharacterUtility = Penumbra.Interop.CharacterUtility; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Meta.Files; @@ -11,7 +12,7 @@ public unsafe class MetaBaseFile : IDisposable public int Length { get; private set; } public CharacterUtility.InternalIndex Index { get; } - public MetaBaseFile( Interop.Structs.CharacterUtility.Index idx ) + public MetaBaseFile( MetaIndex idx ) => Index = CharacterUtility.ReverseIndices[ ( int )idx ]; protected (IntPtr Data, int Length) DefaultData diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index 8cd17a1e..f1267220 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using OtterGui.Filesystem; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -13,13 +14,13 @@ public partial class MetaManager private readonly List< RspManipulation > _cmpManipulations = new(); public void SetCmpFiles() - => SetFile( _cmpFile, CharacterUtility.Index.HumanCmp ); + => SetFile( _cmpFile, MetaIndex.HumanCmp ); public static void ResetCmpFiles() - => SetFile( null, CharacterUtility.Index.HumanCmp ); + => SetFile( null, MetaIndex.HumanCmp ); - public Interop.CharacterUtility.List.MetaReverter TemporarilySetCmpFile() - => TemporarilySetFile( _cmpFile, CharacterUtility.Index.HumanCmp ); + public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() + => TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp ); public void ResetCmp() { diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 35857be0..f5667f68 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -4,6 +4,7 @@ using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.Enums; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -12,24 +13,24 @@ namespace Penumbra.Meta.Manager; public partial class MetaManager { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtility.EqdpIndices.Length]; // TODO: female Hrothgar + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar private readonly List< EqdpManipulation > _eqdpManipulations = new(); public void SetEqdpFiles() { - for( var i = 0; i < CharacterUtility.EqdpIndices.Length; ++i ) + for( var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i ) { - SetFile( _eqdpFiles[ i ], CharacterUtility.EqdpIndices[ i ] ); + SetFile( _eqdpFiles[ i ], CharacterUtilityData.EqdpIndices[ i ] ); } } - public Interop.CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) + public CharacterUtility.MetaList.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) { - var idx = CharacterUtility.EqdpIdx( genderRace, accessory ); + var idx = CharacterUtilityData.EqdpIdx( genderRace, accessory ); if( ( int )idx != -1 ) { - var i = CharacterUtility.EqdpIndices.IndexOf( idx ); + var i = CharacterUtilityData.EqdpIndices.IndexOf( idx ); if( i != -1 ) { return TemporarilySetFile( _eqdpFiles[ i ], idx ); @@ -41,7 +42,7 @@ public partial class MetaManager public static void ResetEqdpFiles() { - foreach( var idx in CharacterUtility.EqdpIndices ) + foreach( var idx in CharacterUtilityData.EqdpIndices ) { SetFile( null, idx ); } @@ -51,7 +52,7 @@ public partial class MetaManager { foreach( var file in _eqdpFiles.OfType< ExpandedEqdpFile >() ) { - var relevant = Interop.CharacterUtility.RelevantIndices[ file.Index.Value ]; + var relevant = CharacterUtility.RelevantIndices[ file.Index.Value ]; file.Reset( _eqdpManipulations.Where( m => m.FileIndex() == relevant ).Select( m => ( int )m.SetId ) ); } @@ -61,7 +62,7 @@ public partial class MetaManager public bool ApplyMod( EqdpManipulation manip ) { _eqdpManipulations.AddOrReplace( manip ); - var file = _eqdpFiles[ Array.IndexOf( CharacterUtility.EqdpIndices, manip.FileIndex() ) ] ??= + var file = _eqdpFiles[ Array.IndexOf( CharacterUtilityData.EqdpIndices, manip.FileIndex() ) ] ??= new ExpandedEqdpFile( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory() ); // TODO: female Hrothgar return manip.Apply( file ); } @@ -71,7 +72,7 @@ public partial class MetaManager if( _eqdpManipulations.Remove( manip ) ) { var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory(), manip.SetId ); - var file = _eqdpFiles[ Array.IndexOf( CharacterUtility.EqdpIndices, manip.FileIndex() ) ]!; + var file = _eqdpFiles[ Array.IndexOf( CharacterUtilityData.EqdpIndices, manip.FileIndex() ) ]!; manip = new EqdpManipulation( def, manip.Slot, manip.Gender, manip.Race, manip.SetId ); return manip.Apply( file ); } @@ -81,7 +82,7 @@ public partial class MetaManager public ExpandedEqdpFile? EqdpFile( GenderRace race, bool accessory ) => _eqdpFiles - [ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar + [ Array.IndexOf( CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar public void DisposeEqdp() { diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 7a5da091..96194f9d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using OtterGui.Filesystem; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -13,13 +14,13 @@ public partial class MetaManager private readonly List< EqpManipulation > _eqpManipulations = new(); public void SetEqpFiles() - => SetFile( _eqpFile, CharacterUtility.Index.Eqp ); + => SetFile( _eqpFile, MetaIndex.Eqp ); public static void ResetEqpFiles() - => SetFile( null, CharacterUtility.Index.Eqp ); + => SetFile( null, MetaIndex.Eqp ); - public Interop.CharacterUtility.List.MetaReverter TemporarilySetEqpFile() - => TemporarilySetFile( _eqpFile, CharacterUtility.Index.Eqp ); + public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() + => TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); public void ResetEqp() { diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index 193e749d..7e4a92db 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using OtterGui.Filesystem; using Penumbra.GameData.Enums; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -19,28 +20,28 @@ public partial class MetaManager public void SetEstFiles() { - SetFile( _estFaceFile, CharacterUtility.Index.FaceEst ); - SetFile( _estHairFile, CharacterUtility.Index.HairEst ); - SetFile( _estBodyFile, CharacterUtility.Index.BodyEst ); - SetFile( _estHeadFile, CharacterUtility.Index.HeadEst ); + SetFile( _estFaceFile, MetaIndex.FaceEst ); + SetFile( _estHairFile, MetaIndex.HairEst ); + SetFile( _estBodyFile, MetaIndex.BodyEst ); + SetFile( _estHeadFile, MetaIndex.HeadEst ); } public static void ResetEstFiles() { - SetFile( null, CharacterUtility.Index.FaceEst ); - SetFile( null, CharacterUtility.Index.HairEst ); - SetFile( null, CharacterUtility.Index.BodyEst ); - SetFile( null, CharacterUtility.Index.HeadEst ); + SetFile( null, MetaIndex.FaceEst ); + SetFile( null, MetaIndex.HairEst ); + SetFile( null, MetaIndex.BodyEst ); + SetFile( null, MetaIndex.HeadEst ); } - public Interop.CharacterUtility.List.MetaReverter? TemporarilySetEstFile(EstManipulation.EstType type) + public CharacterUtility.MetaList.MetaReverter? TemporarilySetEstFile(EstManipulation.EstType type) { var (file, idx) = type switch { - EstManipulation.EstType.Face => ( _estFaceFile, CharacterUtility.Index.FaceEst ), - EstManipulation.EstType.Hair => ( _estHairFile, CharacterUtility.Index.HairEst ), - EstManipulation.EstType.Body => ( _estBodyFile, CharacterUtility.Index.BodyEst ), - EstManipulation.EstType.Head => ( _estHeadFile, CharacterUtility.Index.HeadEst ), + EstManipulation.EstType.Face => ( _estFaceFile, MetaIndex.FaceEst ), + EstManipulation.EstType.Hair => ( _estHairFile, MetaIndex.HairEst ), + EstManipulation.EstType.Body => ( _estBodyFile, MetaIndex.BodyEst ), + EstManipulation.EstType.Head => ( _estHeadFile, MetaIndex.HeadEst ), _ => ( null, 0 ), }; diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 02e1dc78..bb7e764b 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using OtterGui.Filesystem; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -13,13 +14,13 @@ public partial class MetaManager private readonly List< GmpManipulation > _gmpManipulations = new(); public void SetGmpFiles() - => SetFile( _gmpFile, CharacterUtility.Index.Gmp ); + => SetFile( _gmpFile, MetaIndex.Gmp ); public static void ResetGmpFiles() - => SetFile( null, CharacterUtility.Index.Gmp ); + => SetFile( null, MetaIndex.Gmp ); - public Interop.CharacterUtility.List.MetaReverter TemporarilySetGmpFile() - => TemporarilySetFile( _gmpFile, CharacterUtility.Index.Gmp ); + public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() + => TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); public void ResetGmp() { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index beda80bc..1eb192fc 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OtterGui; using Penumbra.Collections; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -162,36 +163,36 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM Penumbra.Log.Debug( $"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations." ); } - public void SetFile( CharacterUtility.Index index ) + public void SetFile( MetaIndex metaIndex ) { - switch( index ) + switch( metaIndex ) { - case CharacterUtility.Index.Eqp: - SetFile( _eqpFile, index ); + case MetaIndex.Eqp: + SetFile( _eqpFile, metaIndex ); break; - case CharacterUtility.Index.Gmp: - SetFile( _gmpFile, index ); + case MetaIndex.Gmp: + SetFile( _gmpFile, metaIndex ); break; - case CharacterUtility.Index.HumanCmp: - SetFile( _cmpFile, index ); + case MetaIndex.HumanCmp: + SetFile( _cmpFile, metaIndex ); break; - case CharacterUtility.Index.FaceEst: - SetFile( _estFaceFile, index ); + case MetaIndex.FaceEst: + SetFile( _estFaceFile, metaIndex ); break; - case CharacterUtility.Index.HairEst: - SetFile( _estHairFile, index ); + case MetaIndex.HairEst: + SetFile( _estHairFile, metaIndex ); break; - case CharacterUtility.Index.HeadEst: - SetFile( _estHeadFile, index ); + case MetaIndex.HeadEst: + SetFile( _estHeadFile, metaIndex ); break; - case CharacterUtility.Index.BodyEst: - SetFile( _estBodyFile, index ); + case MetaIndex.BodyEst: + SetFile( _estBodyFile, metaIndex ); break; default: - var i = CharacterUtility.EqdpIndices.IndexOf( index ); + var i = CharacterUtilityData.EqdpIndices.IndexOf( metaIndex ); if( i != -1 ) { - SetFile( _eqdpFiles[ i ], index ); + SetFile( _eqdpFiles[ i ], metaIndex ); } break; @@ -199,21 +200,21 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe void SetFile( MetaBaseFile? file, CharacterUtility.Index index ) + private static unsafe void SetFile( MetaBaseFile? file, MetaIndex metaIndex ) { if( file == null ) { - Penumbra.CharacterUtility.ResetResource( index ); + Penumbra.CharacterUtility.ResetResource( metaIndex ); } else { - Penumbra.CharacterUtility.SetResource( index, ( IntPtr )file.Data, file.Length ); + Penumbra.CharacterUtility.SetResource( metaIndex, ( IntPtr )file.Data, file.Length ); } } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe Interop.CharacterUtility.List.MetaReverter TemporarilySetFile( MetaBaseFile? file, CharacterUtility.Index index ) + private static unsafe CharacterUtility.MetaList.MetaReverter TemporarilySetFile( MetaBaseFile? file, MetaIndex metaIndex ) => file == null - ? Penumbra.CharacterUtility.TemporarilyResetResource( index ) - : Penumbra.CharacterUtility.TemporarilySetResource( index, ( IntPtr )file.Data, file.Length ); + ? Penumbra.CharacterUtility.TemporarilyResetResource( metaIndex ) + : Penumbra.CharacterUtility.TemporarilySetResource( metaIndex, ( IntPtr )file.Data, file.Length ); } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 92ebbb2c..3e927407 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -81,8 +81,8 @@ public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > return set != 0 ? set : Slot.CompareTo( other.Slot ); } - public CharacterUtility.Index FileIndex() - => CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); + public MetaIndex FileIndex() + => CharacterUtilityData.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); public bool Apply( ExpandedEqdpFile file ) { diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 5734d0d2..9d9010d2 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -52,8 +52,8 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > return set != 0 ? set : Slot.CompareTo( other.Slot ); } - public CharacterUtility.Index FileIndex() - => CharacterUtility.Index.Eqp; + public MetaIndex FileIndex() + => MetaIndex.Eqp; public bool Apply( ExpandedEqpFile file ) { diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 3891cc6f..24c22024 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -13,10 +13,10 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > { public enum EstType : byte { - Hair = CharacterUtility.Index.HairEst, - Face = CharacterUtility.Index.FaceEst, - Body = CharacterUtility.Index.BodyEst, - Head = CharacterUtility.Index.HeadEst, + Hair = MetaIndex.HairEst, + Face = MetaIndex.FaceEst, + Body = MetaIndex.BodyEst, + Head = MetaIndex.HeadEst, } public static string ToName( EstType type ) @@ -89,8 +89,8 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > return s != 0 ? s : SetId.CompareTo( other.SetId ); } - public CharacterUtility.Index FileIndex() - => ( CharacterUtility.Index )Slot; + public MetaIndex FileIndex() + => ( MetaIndex )Slot; public bool Apply( EstFile file ) { diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 23a165c2..b8464abf 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -37,8 +37,8 @@ public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > public int CompareTo( GmpManipulation other ) => SetId.CompareTo( other.SetId ); - public CharacterUtility.Index FileIndex() - => CharacterUtility.Index.Gmp; + public MetaIndex FileIndex() + => MetaIndex.Gmp; public bool Apply( ExpandedGmpFile file ) { diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index f4656a79..2db291bd 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -142,8 +142,8 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return b != 0 ? b : Variant.CompareTo( other.Variant ); } - public CharacterUtility.Index FileIndex() - => ( CharacterUtility.Index )( -1 ); + public MetaIndex FileIndex() + => ( MetaIndex )( -1 ); public Utf8GamePath GamePath() { diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 1d4b370b..98d138dd 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -9,7 +9,7 @@ namespace Penumbra.Meta.Manipulations; public interface IMetaManipulation { - public CharacterUtility.Index FileIndex(); + public MetaIndex FileIndex(); } public interface IMetaManipulation< T > diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index e69ec0a1..397f80dc 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -49,8 +49,8 @@ public readonly struct RspManipulation : IMetaManipulation< RspManipulation > return s != 0 ? s : Attribute.CompareTo( other.Attribute ); } - public CharacterUtility.Index FileIndex() - => CharacterUtility.Index.HumanCmp; + public MetaIndex FileIndex() + => MetaIndex.HumanCmp; public bool Apply( CmpFile file ) { diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 22e37eba..97398d87 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -59,7 +59,7 @@ public static class EquipmentSwap case Gender.FemaleNpc when skipFemale: continue; } - if( CharacterUtility.EqdpIdx( gr, true ) < 0 ) + if( CharacterUtilityData.EqdpIdx( gr, true ) < 0 ) { continue; } @@ -147,7 +147,7 @@ public static class EquipmentSwap case Gender.FemaleNpc when skipFemale: continue; } - if( CharacterUtility.EqdpIdx( gr, isAccessory ) < 0 ) + if( CharacterUtilityData.EqdpIdx( gr, isAccessory ) < 0 ) { continue; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5ea33c2c..8b31e9e0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,10 +22,10 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.PathResolving; using Penumbra.Mods; -using CharacterUtility = Penumbra.Interop.CharacterUtility; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 08f4a0dd..ad9344a4 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -9,8 +9,8 @@ using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; using Penumbra.Mods; @@ -20,6 +20,7 @@ using Penumbra.UI.AdvancedWindow; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; using Penumbra.Util; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; namespace Penumbra; diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index 131507cc..8b88fb09 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -7,7 +7,7 @@ using Dalamud.Plugin; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.PathResolving; using Penumbra.Util; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 31dce033..242c49dc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -199,7 +199,7 @@ public partial class ModEditWindow editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); var raceCode = Names.CombinedRace(_new.Gender, _new.Race); - var validRaceCode = CharacterUtility.EqdpIdx(raceCode, false) >= 0; + var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 27abb6dc..66ece068 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -11,7 +11,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index b20807dc..2c89e673 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -14,8 +14,8 @@ using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Mods; using Penumbra.Services; @@ -23,7 +23,7 @@ using Penumbra.String; using Penumbra.Util; using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; -using CharacterUtility = Penumbra.Interop.CharacterUtility; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; @@ -467,7 +467,7 @@ public class DebugTab : ITab foreach (var list in _characterUtility.Lists) { - ImGuiUtil.DrawTableColumn(list.GlobalIndex.ToString()); + ImGuiUtil.DrawTableColumn(list.GlobalMetaIndex.ToString()); ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index fc15bc77..f6e0f2ed 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -10,6 +10,7 @@ using Dalamud.Interface; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index b77d88a5..c1724b8e 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -11,7 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Interop.Loader; +using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 0e1e4ce5..a0dd3c9f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; namespace Penumbra.UI.Tabs; From 174e640c45c7c94c9f1a40cf6dcc78801122594f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Mar 2023 21:43:40 +0100 Subject: [PATCH 0823/2451] Move TexTools around. --- Penumbra/Configuration.cs | 2 +- Penumbra/Import/MetaFileInfo.cs | 122 ---------------- .../Import/{ => Structs}/ImporterState.cs | 2 +- Penumbra/Import/Structs/MetaFileInfo.cs | 104 +++++++++++++ .../Import/{ => Structs}/StreamDisposer.cs | 8 +- .../Import/{ => Structs}/TexToolsStructs.cs | 28 ++-- Penumbra/Import/TexToolsImport.cs | 2 + Penumbra/Import/TexToolsImporter.Archives.cs | 1 + Penumbra/Import/TexToolsImporter.Gui.cs | 1 + Penumbra/Import/TexToolsImporter.ModPack.cs | 1 + .../Import/TexToolsMeta.Deserialization.cs | 1 + Penumbra/Import/TexToolsMeta.cs | 1 + Penumbra/Import/Textures/Texture.cs | 1 - Penumbra/Import/Textures/TextureImporter.cs | 6 +- .../Interop/Services/CharacterUtility.List.cs | 4 +- Penumbra/Mods/Editor/ModBackup.cs | 137 +++++++++--------- Penumbra/Mods/Editor/ModEditor.cs | 16 +- Penumbra/Mods/Mod.Creator.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 1 + 19 files changed, 212 insertions(+), 228 deletions(-) delete mode 100644 Penumbra/Import/MetaFileInfo.cs rename Penumbra/Import/{ => Structs}/ImporterState.cs (77%) create mode 100644 Penumbra/Import/Structs/MetaFileInfo.cs rename Penumbra/Import/{ => Structs}/StreamDisposer.cs (74%) rename Penumbra/Import/{ => Structs}/TexToolsStructs.cs (69%) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 54ae8ac5..b1468c2c 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -9,7 +9,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.GameData.Enums; -using Penumbra.Import; +using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs deleted file mode 100644 index 99a4ff9b..00000000 --- a/Penumbra/Import/MetaFileInfo.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Penumbra.GameData.Enums; -using System.Text.RegularExpressions; -using Penumbra.GameData; - -namespace Penumbra.Import; - -// Obtain information what type of object is manipulated -// by the given .meta file from TexTools, using its name. -public class MetaFileInfo -{ - private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex - private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex - private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex - private const string Pir = @"\k'PrimaryId'"; // language=regex - private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex - private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex - private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex - private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex - private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex - private const string Ext = @"\.meta"; - - // These are the valid regexes for .meta files that we are able to support at the moment. - private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled); - private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled); - - public readonly ObjectType PrimaryType; - public readonly BodySlot SecondaryType; - public readonly ushort PrimaryId; - public readonly ushort SecondaryId; - public readonly EquipSlot EquipSlot = EquipSlot.Unknown; - public readonly CustomizationType CustomizationType = CustomizationType.Unknown; - - private static bool ValidType( ObjectType type ) - { - return type switch - { - ObjectType.Accessory => true, - ObjectType.Character => true, - ObjectType.Equipment => true, - ObjectType.DemiHuman => true, - ObjectType.Housing => true, - ObjectType.Monster => true, - ObjectType.Weapon => true, - ObjectType.Icon => false, - ObjectType.Font => false, - ObjectType.Interface => false, - ObjectType.LoadingScreen => false, - ObjectType.Map => false, - ObjectType.Vfx => false, - ObjectType.Unknown => false, - ObjectType.World => false, - _ => false, - }; - } - - public MetaFileInfo( IGamePathParser parser, string fileName ) - { - // Set the primary type from the gamePath start. - PrimaryType = parser.PathToObjectType( fileName ); - PrimaryId = 0; - SecondaryType = BodySlot.Unknown; - SecondaryId = 0; - // Not all types of objects can have valid meta data manipulation. - if( !ValidType( PrimaryType ) ) - { - PrimaryType = ObjectType.Unknown; - return; - } - - // Housing files have a separate regex that just contains the primary id. - if( PrimaryType == ObjectType.Housing ) - { - var housingMatch = HousingMeta.Match( fileName ); - if( housingMatch.Success ) - { - PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value ); - } - - return; - } - - // Non-housing is in chara/. - var match = CharaMeta.Match( fileName ); - if( !match.Success ) - { - return; - } - - // The primary ID has to be available for every object. - PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value ); - - // Depending on slot, we can set equip slot or customization type. - if( match.Groups[ "Slot" ].Success ) - { - switch( PrimaryType ) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) ) - { - EquipSlot = tmpSlot; - } - - break; - case ObjectType.Character: - if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) ) - { - CustomizationType = tmpCustom; - } - - break; - } - } - - // Secondary type and secondary id are for weapons and demihumans. - if( match.Groups[ "SecondaryType" ].Success - && Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) ) - { - SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Import/ImporterState.cs b/Penumbra/Import/Structs/ImporterState.cs similarity index 77% rename from Penumbra/Import/ImporterState.cs rename to Penumbra/Import/Structs/ImporterState.cs index 8d576f97..9ab2ab9a 100644 --- a/Penumbra/Import/ImporterState.cs +++ b/Penumbra/Import/Structs/ImporterState.cs @@ -1,4 +1,4 @@ -namespace Penumbra.Import; +namespace Penumbra.Import.Structs; public enum ImporterState { diff --git a/Penumbra/Import/Structs/MetaFileInfo.cs b/Penumbra/Import/Structs/MetaFileInfo.cs new file mode 100644 index 00000000..3af2db34 --- /dev/null +++ b/Penumbra/Import/Structs/MetaFileInfo.cs @@ -0,0 +1,104 @@ +using Penumbra.GameData.Enums; +using System.Text.RegularExpressions; +using Penumbra.GameData; + +namespace Penumbra.Import.Structs; + +/// +/// Obtain information what type of object is manipulated +/// by the given .meta file from TexTools, using its name. +/// +public partial struct MetaFileInfo +{ + // These are the valid regexes for .meta files that we are able to support at the moment. + [GeneratedRegex(@"bgcommon/hou/(?'Type1'[a-z]*)/general/(?'Id1'\d{4})/\k'Id1'\.meta", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] + private static partial Regex HousingMeta(); + + [GeneratedRegex( + @"chara/(?'Type1'[a-z]*)/(?'Pre1'[a-z])(?'Id1'\d{4})(/obj/(?'Type2'[a-z]*)/(?'Pre2'[a-z])(?'Id2'\d{4}))?/\k'Pre1'\k'Id1'(\k'Pre2'\k'Id2')?(_(?'Slot'[a-z]{3}))?\\.meta", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] + private static partial Regex CharaMeta(); + + public readonly ObjectType PrimaryType; + public readonly BodySlot SecondaryType; + public readonly ushort PrimaryId; + public readonly ushort SecondaryId; + public readonly EquipSlot EquipSlot = EquipSlot.Unknown; + public readonly CustomizationType CustomizationType = CustomizationType.Unknown; + + private static bool ValidType(ObjectType type) + => type switch + { + ObjectType.Accessory => true, + ObjectType.Character => true, + ObjectType.Equipment => true, + ObjectType.DemiHuman => true, + ObjectType.Housing => true, + ObjectType.Monster => true, + ObjectType.Weapon => true, + ObjectType.Icon => false, + ObjectType.Font => false, + ObjectType.Interface => false, + ObjectType.LoadingScreen => false, + ObjectType.Map => false, + ObjectType.Vfx => false, + ObjectType.Unknown => false, + ObjectType.World => false, + _ => false, + }; + + public MetaFileInfo(IGamePathParser parser, string fileName) + { + // Set the primary type from the gamePath start. + PrimaryType = parser.PathToObjectType(fileName); + PrimaryId = 0; + SecondaryType = BodySlot.Unknown; + SecondaryId = 0; + // Not all types of objects can have valid meta data manipulation. + if (!ValidType(PrimaryType)) + { + PrimaryType = ObjectType.Unknown; + return; + } + + // Housing files have a separate regex that just contains the primary id. + if (PrimaryType == ObjectType.Housing) + { + var housingMatch = HousingMeta().Match(fileName); + if (housingMatch.Success) + PrimaryId = ushort.Parse(housingMatch.Groups["Id1"].Value); + + return; + } + + // Non-housing is in chara/. + var match = CharaMeta().Match(fileName); + if (!match.Success) + return; + + // The primary ID has to be available for every object. + PrimaryId = ushort.Parse(match.Groups["Id1"].Value); + + // Depending on slot, we can set equip slot or customization type. + if (match.Groups["Slot"].Success) + switch (PrimaryType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + if (Names.SuffixToEquipSlot.TryGetValue(match.Groups["Slot"].Value, out var tmpSlot)) + EquipSlot = tmpSlot; + + break; + case ObjectType.Character: + if (Names.SuffixToCustomizationType.TryGetValue(match.Groups["Slot"].Value, out var tmpCustom)) + CustomizationType = tmpCustom; + + break; + } + + // Secondary type and secondary id are for weapons and demihumans. + if (match.Groups["Type2"].Success && Names.StringToBodySlot.TryGetValue(match.Groups["Type2"].Value, out SecondaryType)) + SecondaryId = ushort.Parse(match.Groups["Id2"].Value); + } +} diff --git a/Penumbra/Import/StreamDisposer.cs b/Penumbra/Import/Structs/StreamDisposer.cs similarity index 74% rename from Penumbra/Import/StreamDisposer.cs rename to Penumbra/Import/Structs/StreamDisposer.cs index fb5ccef4..65c67585 100644 --- a/Penumbra/Import/StreamDisposer.cs +++ b/Penumbra/Import/Structs/StreamDisposer.cs @@ -2,15 +2,15 @@ using Penumbra.Util; using System; using System.IO; -namespace Penumbra.Import; +namespace Penumbra.Import.Structs; // Create an automatically disposing SqPack stream. public class StreamDisposer : PenumbraSqPackStream, IDisposable { private readonly FileStream _fileStream; - public StreamDisposer( FileStream stream ) - : base( stream ) + public StreamDisposer(FileStream stream) + : base(stream) => _fileStream = stream; public new void Dispose() @@ -20,6 +20,6 @@ public class StreamDisposer : PenumbraSqPackStream, IDisposable base.Dispose(); _fileStream.Dispose(); - File.Delete( filePath ); + File.Delete(filePath); } } \ No newline at end of file diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/Structs/TexToolsStructs.cs similarity index 69% rename from Penumbra/Import/TexToolsStructs.cs rename to Penumbra/Import/Structs/TexToolsStructs.cs index da01dda2..cdd70c53 100644 --- a/Penumbra/Import/TexToolsStructs.cs +++ b/Penumbra/Import/Structs/TexToolsStructs.cs @@ -1,7 +1,7 @@ using System; using Penumbra.Api.Enums; -namespace Penumbra.Import; +namespace Penumbra.Import.Structs; internal static class DefaultTexToolsData { @@ -27,7 +27,7 @@ internal class SimpleMod internal class ModPackPage { public int PageIndex = 0; - public ModGroup[] ModGroups = Array.Empty< ModGroup >(); + public ModGroup[] ModGroups = Array.Empty(); } [Serializable] @@ -35,7 +35,7 @@ internal class ModGroup { public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; - public OptionList[] OptionList = Array.Empty< OptionList >(); + public OptionList[] OptionList = Array.Empty(); public string Description = string.Empty; } @@ -45,7 +45,7 @@ internal class OptionList public string Name = string.Empty; public string Description = string.Empty; public string ImagePath = string.Empty; - public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >(); + public SimpleMod[] ModsJsons = Array.Empty(); public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; public bool IsChecked = false; @@ -60,18 +60,18 @@ internal class ExtendedModPack public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; public string Url = string.Empty; - public ModPackPage[] ModPackPages = Array.Empty< ModPackPage >(); - public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); + public ModPackPage[] ModPackPages = Array.Empty(); + public SimpleMod[] SimpleModsList = Array.Empty(); } [Serializable] internal class SimpleModPack { - public string TtmpVersion = string.Empty; - public string Name = DefaultTexToolsData.Name; - public string Author = DefaultTexToolsData.Author; - public string Version = string.Empty; - public string Description = DefaultTexToolsData.Description; - public string Url = string.Empty; - public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >(); -} \ No newline at end of file + public string TtmpVersion = string.Empty; + public string Name = DefaultTexToolsData.Name; + public string Author = DefaultTexToolsData.Author; + public string Version = string.Empty; + public string Description = DefaultTexToolsData.Description; + public string Url = string.Empty; + public SimpleMod[] SimpleModsList = Array.Empty(); +} diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 89672cf1..548cf90a 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; +using Penumbra.Import.Structs; using Penumbra.Mods; using FileMode = System.IO.FileMode; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 9d9f5a69..c986cc78 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -2,6 +2,7 @@ using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; +using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; using SharpCompress.Archives.Rar; diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index bf997c2c..8c5ad81b 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -3,6 +3,7 @@ using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Import.Structs; using Penumbra.UI.Classes; namespace Penumbra.Import; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 33e3c918..fd141ef3 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Util; using SharpCompress.Archives.Zip; diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 252a1720..e97312b0 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -3,6 +3,7 @@ using System.IO; using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Import.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index fd01de02..cbf1e9fa 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using Penumbra.GameData; +using Penumbra.Import.Structs; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 669f81d7..d37c8967 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using ImGuiScene; using Lumina.Data.Files; diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs index 7e62830f..74bef485 100644 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ b/Penumbra/Import/Textures/TextureImporter.cs @@ -1,10 +1,10 @@ +using System; +using System.IO; using Lumina.Data.Files; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.IO; -namespace Penumbra.Import.Dds; +namespace Penumbra.Import.Textures; public static class TextureImporter { diff --git a/Penumbra/Interop/Services/CharacterUtility.List.cs b/Penumbra/Interop/Services/CharacterUtility.List.cs index 3e847c0f..1fc33efb 100644 --- a/Penumbra/Interop/Services/CharacterUtility.List.cs +++ b/Penumbra/Interop/Services/CharacterUtility.List.cs @@ -82,7 +82,7 @@ public unsafe partial class CharacterUtility ResetResourceInternal(); } - // Set the currently stored data of this resource to new values. + /// Set the currently stored data of this resource to new values. private void SetResourceInternal(nint data, int length) { if (!Ready) @@ -92,7 +92,7 @@ public unsafe partial class CharacterUtility resource->SetData(data, length); } - // Reset the currently stored data of this resource to its default values. + /// Reset the currently stored data of this resource to its default values. private void ResetResourceInternal() => SetResourceInternal(_defaultResourceData, _defaultResourceSize); diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index fe533489..21f8792b 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -5,141 +5,136 @@ using System.Threading.Tasks; namespace Penumbra.Mods; -// Utility to create and apply a zipped backup of a mod. +/// Utility to create and apply a zipped backup of a mod. public class ModBackup { public static bool CreatingBackup { get; private set; } - private readonly Mod _mod; - public readonly string Name; - public readonly bool Exists; + private readonly Mod.Manager _modManager; + private readonly Mod _mod; + public readonly string Name; + public readonly bool Exists; - public ModBackup( Mod.Manager modManager, Mod mod ) + public ModBackup(Mod.Manager modManager, Mod mod) { - _mod = mod; - Name = Path.Combine( modManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp"; - Exists = File.Exists( Name ); + _modManager = modManager; + _mod = mod; + Name = Path.Combine(_modManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; + Exists = File.Exists(Name); } - // Migrate file extensions. - public static void MigrateZipToPmp( Mod.Manager manager ) + /// Migrate file extensions. + public static void MigrateZipToPmp(Mod.Manager manager) { - foreach( var mod in manager ) + foreach (var mod in manager) { var pmpName = mod.ModPath + ".pmp"; var zipName = mod.ModPath + ".zip"; - if( File.Exists( zipName ) ) - { - try - { - if( !File.Exists( pmpName ) ) - { - File.Move( zipName, pmpName ); - } - else - { - File.Delete( zipName ); - } + if (!File.Exists(zipName)) + continue; - Penumbra.Log.Information( $"Migrated mod export from {zipName} to {pmpName}." ); - } - catch( Exception e ) - { - Penumbra.Log.Warning( $"Could not migrate mod export of {mod.ModPath} from .pmp to .zip:\n{e}" ); - } + try + { + if (!File.Exists(pmpName)) + File.Move(zipName, pmpName); + else + File.Delete(zipName); + + Penumbra.Log.Information($"Migrated mod export from {zipName} to {pmpName}."); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not migrate mod export of {mod.ModPath} from .pmp to .zip:\n{e}"); } } } - // Move and/or rename an exported mod. - // This object is unusable afterwards. - public void Move( string? newBasePath = null, string? newName = null ) + /// + /// Move and/or rename an exported mod. + /// This object is unusable afterwards. + /// + public void Move(string? newBasePath = null, string? newName = null) { - if( CreatingBackup || !Exists ) - { + if (CreatingBackup || !Exists) return; - } try { - newBasePath ??= Path.GetDirectoryName( Name ) ?? string.Empty; - newName = newName == null ? Path.GetFileName( Name ) : newName + ".pmp"; - var newPath = Path.Combine( newBasePath, newName ); - File.Move( Name, newPath ); + newBasePath ??= Path.GetDirectoryName(Name) ?? string.Empty; + newName = newName == null ? Path.GetFileName(Name) : newName + ".pmp"; + var newPath = Path.Combine(newBasePath, newName); + File.Move(Name, newPath); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Warning( $"Could not move mod export file {Name}:\n{e}" ); + Penumbra.Log.Warning($"Could not move mod export file {Name}:\n{e}"); } } - // Create a backup zip without blocking the main thread. + /// Create a backup zip without blocking the main thread. public async void CreateAsync() { - if( CreatingBackup ) - { + if (CreatingBackup) return; - } CreatingBackup = true; - await Task.Run( Create ); + await Task.Run(Create); CreatingBackup = false; } - - // Create a backup. Overwrites pre-existing backups. + /// Create a backup. Overwrites pre-existing backups. private void Create() { try { Delete(); - ZipFile.CreateFromDirectory( _mod.ModPath.FullName, Name, CompressionLevel.Optimal, false ); - Penumbra.Log.Debug( $"Created export file {Name} from {_mod.ModPath.FullName}." ); + ZipFile.CreateFromDirectory(_mod.ModPath.FullName, Name, CompressionLevel.Optimal, false); + Penumbra.Log.Debug($"Created export file {Name} from {_mod.ModPath.FullName}."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not export mod {_mod.Name} to \"{Name}\":\n{e}" ); + Penumbra.Log.Error($"Could not export mod {_mod.Name} to \"{Name}\":\n{e}"); } } - // Delete a pre-existing backup. + /// Delete a pre-existing backup. public void Delete() { - if( !Exists ) - { + if (!Exists) return; - } try { - File.Delete( Name ); - Penumbra.Log.Debug( $"Deleted export file {Name}." ); + File.Delete(Name); + Penumbra.Log.Debug($"Deleted export file {Name}."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not delete file \"{Name}\":\n{e}" ); + Penumbra.Log.Error($"Could not delete file \"{Name}\":\n{e}"); } } - // Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar. - // Does an automatic reload after extraction. + /// + /// Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar. + /// Does an automatic reload after extraction. + /// public void Restore() { try { - if( Directory.Exists( _mod.ModPath.FullName ) ) + if (Directory.Exists(_mod.ModPath.FullName)) { - Directory.Delete( _mod.ModPath.FullName, true ); - Penumbra.Log.Debug( $"Deleted mod folder {_mod.ModPath.FullName}." ); + Directory.Delete(_mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted mod folder {_mod.ModPath.FullName}."); } - ZipFile.ExtractToDirectory( Name, _mod.ModPath.FullName ); - Penumbra.Log.Debug( $"Extracted exported file {Name} to {_mod.ModPath.FullName}." ); - Penumbra.ModManager.ReloadMod( _mod.Index ); + ZipFile.ExtractToDirectory(Name, _mod.ModPath.FullName); + Penumbra.Log.Debug($"Extracted exported file {Name} to {_mod.ModPath.FullName}."); + _modManager.ReloadMod(_mod.Index); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not restore {_mod.Name} from export \"{Name}\":\n{e}" ); + Penumbra.Log.Error($"Could not restore {_mod.Name} from export \"{Name}\":\n{e}"); } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index c08b7bff..0b41d7c3 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -24,12 +24,12 @@ public class ModEditor : IDisposable public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files, ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor) { - ModNormalizer = modNormalizer; - MetaEditor = metaEditor; - Files = files; - FileEditor = fileEditor; - Duplicates = duplicates; - SwapEditor = swapEditor; + ModNormalizer = modNormalizer; + MetaEditor = metaEditor; + Files = files; + FileEditor = fileEditor; + Duplicates = duplicates; + SwapEditor = swapEditor; MdlMaterialEditor = mdlMaterialEditor; } @@ -88,7 +88,7 @@ public class ModEditor : IDisposable GroupIdx = -1; OptionIdx = 0; if (message) - global::Penumbra.Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); + Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); } public void Clear() @@ -125,4 +125,4 @@ public class ModEditor : IDisposable subDir.Delete(); } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 0fb2e5a1..0074cf77 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -10,7 +10,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Import; +using Penumbra.Import.Structs; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2ee27bfb..49b8ff3d 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -15,6 +15,7 @@ using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Import; +using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; From c8415e3079214d87a25a350705be5db577ac5cb0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Mar 2023 00:28:36 +0100 Subject: [PATCH 0824/2451] Start ModManager dissemination.... --- Penumbra/Api/IpcTester.cs | 1368 ++++++++--------- Penumbra/Api/PenumbraIpcProviders.cs | 5 +- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/ModCollection.File.cs | 2 +- Penumbra/Configuration.cs | 48 +- Penumbra/Import/TexToolsImport.cs | 14 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +- Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 174 +-- Penumbra/Mods/Manager/Mod.Manager.Local.cs | 63 +- Penumbra/Mods/Manager/Mod.Manager.Meta.cs | 71 - Penumbra/Mods/Manager/Mod.Manager.Options.cs | 18 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.cs | 11 +- Penumbra/Mods/Manager/ModDataChangeType.cs | 21 + Penumbra/Mods/Manager/ModDataEditor.cs | 362 +++++ Penumbra/Mods/Mod.BasePath.cs | 27 +- Penumbra/Mods/Mod.Creator.cs | 17 +- Penumbra/Mods/Mod.LocalData.cs | 153 +- Penumbra/Mods/Mod.Meta.Migration.cs | 213 +-- Penumbra/Mods/Mod.Meta.cs | 145 +- Penumbra/Mods/Mod.TemporaryMod.cs | 6 +- Penumbra/Mods/ModFileSystem.cs | 28 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/Services/CommunicatorService.cs | 8 + Penumbra/Services/FilenameService.cs | 4 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 40 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 16 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- Penumbra/Util/SaveService.cs | 10 +- 34 files changed, 1305 insertions(+), 1542 deletions(-) delete mode 100644 Penumbra/Mods/Manager/Mod.Manager.Meta.cs create mode 100644 Penumbra/Mods/Manager/ModDataChangeType.cs create mode 100644 Penumbra/Mods/Manager/ModDataEditor.cs diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7565c8b3..b48128e0 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -38,20 +38,20 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Temporary _temporary; - public IpcTester( DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders ) + public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager) { _ipcProviders = ipcProviders; - _pluginState = new PluginState( pi ); - _configuration = new Configuration( pi ); - _ui = new Ui( pi ); - _redrawing = new Redrawing( pi ); - _gameState = new GameState( pi ); - _resolve = new Resolve( pi ); - _collections = new Collections( pi ); - _meta = new Meta( pi ); - _mods = new Mods( pi ); - _modSettings = new ModSettings( pi ); - _temporary = new Temporary( pi ); + _pluginState = new PluginState(pi); + _configuration = new Configuration(pi); + _ui = new Ui(pi); + _redrawing = new Redrawing(pi); + _gameState = new GameState(pi); + _resolve = new Resolve(pi); + _collections = new Collections(pi); + _meta = new Meta(pi); + _mods = new Mods(pi); + _modSettings = new ModSettings(pi); + _temporary = new Temporary(pi, modManager); UnsubscribeEvents(); } @@ -60,7 +60,7 @@ public class IpcTester : IDisposable try { SubscribeEvents(); - ImGui.TextUnformatted( $"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}" ); + ImGui.TextUnformatted($"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}"); _pluginState.Draw(); _configuration.Draw(); _ui.Draw(); @@ -75,15 +75,15 @@ public class IpcTester : IDisposable _temporary.DrawCollections(); _temporary.DrawMods(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Error during IPC Tests:\n{e}" ); + Penumbra.Log.Error($"Error during IPC Tests:\n{e}"); } } private void SubscribeEvents() { - if( !_subscribed ) + if (!_subscribed) { _pluginState.Initialized.Enable(); _pluginState.Disposed.Enable(); @@ -105,7 +105,7 @@ public class IpcTester : IDisposable public void UnsubscribeEvents() { - if( _subscribed ) + if (_subscribed) { _pluginState.Initialized.Disable(); _pluginState.Disposed.Disable(); @@ -148,131 +148,121 @@ public class IpcTester : IDisposable _subscribed = false; } - private static void DrawIntro( string label, string info ) + private static void DrawIntro(string label, string info) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( label ); + ImGui.TextUnformatted(label); ImGui.TableNextColumn(); - ImGui.TextUnformatted( info ); + ImGui.TextUnformatted(info); ImGui.TableNextColumn(); } private class PluginState { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber Initialized; - public readonly EventSubscriber Disposed; - public readonly EventSubscriber< bool > EnabledChange; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber Initialized; + public readonly EventSubscriber Disposed; + public readonly EventSubscriber EnabledChange; - private readonly List< DateTimeOffset > _initializedList = new(); - private readonly List< DateTimeOffset > _disposedList = new(); + private readonly List _initializedList = new(); + private readonly List _disposedList = new(); private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; private bool? _lastEnabledValue; - public PluginState( DalamudPluginInterface pi ) + public PluginState(DalamudPluginInterface pi) { _pi = pi; - Initialized = Ipc.Initialized.Subscriber( pi, AddInitialized ); - Disposed = Ipc.Disposed.Subscriber( pi, AddDisposed ); - EnabledChange = Ipc.EnabledChange.Subscriber( pi, SetLastEnabled ); + Initialized = Ipc.Initialized.Subscriber(pi, AddInitialized); + Disposed = Ipc.Disposed.Subscriber(pi, AddDisposed); + EnabledChange = Ipc.EnabledChange.Subscriber(pi, SetLastEnabled); } public void Draw() { - using var _ = ImRaii.TreeNode( "Plugin State" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Plugin State"); + if (!_) return; - } - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - void DrawList( string label, string text, List< DateTimeOffset > list ) + void DrawList(string label, string text, List list) { - DrawIntro( label, text ); - if( list.Count == 0 ) + DrawIntro(label, text); + if (list.Count == 0) { - ImGui.TextUnformatted( "Never" ); + ImGui.TextUnformatted("Never"); } else { - ImGui.TextUnformatted( list[ ^1 ].LocalDateTime.ToString( CultureInfo.CurrentCulture ) ); - if( list.Count > 1 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( "\n", - list.SkipLast( 1 ).Select( t => t.LocalDateTime.ToString( CultureInfo.CurrentCulture ) ) ) ); - } + ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture)); + if (list.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", + list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture)))); } } - DrawList( Ipc.Initialized.Label, "Last Initialized", _initializedList ); - DrawList( Ipc.Disposed.Label, "Last Disposed", _disposedList ); - DrawIntro( Ipc.ApiVersions.Label, "Current Version" ); - var (breaking, features) = Ipc.ApiVersions.Subscriber( _pi ).Invoke(); - ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); - DrawIntro( Ipc.GetEnabledState.Label, "Current State" ); - ImGui.TextUnformatted( $"{Ipc.GetEnabledState.Subscriber( _pi ).Invoke()}" ); - DrawIntro( Ipc.EnabledChange.Label, "Last Change" ); - ImGui.TextUnformatted( _lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never" ); + DrawList(Ipc.Initialized.Label, "Last Initialized", _initializedList); + DrawList(Ipc.Disposed.Label, "Last Disposed", _disposedList); + DrawIntro(Ipc.ApiVersions.Label, "Current Version"); + var (breaking, features) = Ipc.ApiVersions.Subscriber(_pi).Invoke(); + ImGui.TextUnformatted($"{breaking}.{features:D4}"); + DrawIntro(Ipc.GetEnabledState.Label, "Current State"); + ImGui.TextUnformatted($"{Ipc.GetEnabledState.Subscriber(_pi).Invoke()}"); + DrawIntro(Ipc.EnabledChange.Label, "Last Change"); + ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); } private void AddInitialized() - => _initializedList.Add( DateTimeOffset.UtcNow ); + => _initializedList.Add(DateTimeOffset.UtcNow); private void AddDisposed() - => _disposedList.Add( DateTimeOffset.UtcNow ); + => _disposedList.Add(DateTimeOffset.UtcNow); - private void SetLastEnabled( bool val ) - => ( _lastEnabledChange, _lastEnabledValue ) = ( DateTimeOffset.Now, val ); + private void SetLastEnabled(bool val) + => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); } private class Configuration { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber< string, bool > ModDirectoryChanged; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber ModDirectoryChanged; private string _currentConfiguration = string.Empty; private string _lastModDirectory = string.Empty; private bool _lastModDirectoryValid; private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; - public Configuration( DalamudPluginInterface pi ) + public Configuration(DalamudPluginInterface pi) { _pi = pi; - ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber( pi, UpdateModDirectoryChanged ); + ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); } public void Draw() { - using var _ = ImRaii.TreeNode( "Configuration" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Configuration"); + if (!_) return; - } - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( Ipc.GetModDirectory.Label, "Current Mod Directory" ); - ImGui.TextUnformatted( Ipc.GetModDirectory.Subscriber( _pi ).Invoke() ); - DrawIntro( Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change" ); - ImGui.TextUnformatted( _lastModDirectoryTime > DateTimeOffset.MinValue - ? $"{_lastModDirectory} ({( _lastModDirectoryValid ? "Valid" : "Invalid" )}) at {_lastModDirectoryTime}" - : "None" ); - DrawIntro( Ipc.GetConfiguration.Label, "Configuration" ); - if( ImGui.Button( "Get" ) ) + DrawIntro(Ipc.GetModDirectory.Label, "Current Mod Directory"); + ImGui.TextUnformatted(Ipc.GetModDirectory.Subscriber(_pi).Invoke()); + DrawIntro(Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change"); + ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue + ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}" + : "None"); + DrawIntro(Ipc.GetConfiguration.Label, "Configuration"); + if (ImGui.Button("Get")) { - _currentConfiguration = Ipc.GetConfiguration.Subscriber( _pi ).Invoke(); - ImGui.OpenPopup( "Config Popup" ); + _currentConfiguration = Ipc.GetConfiguration.Subscriber(_pi).Invoke(); + ImGui.OpenPopup("Config Popup"); } DrawConfigPopup(); @@ -280,33 +270,31 @@ public class IpcTester : IDisposable private void DrawConfigPopup() { - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var popup = ImRaii.Popup( "Config Popup" ); - if( popup ) + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var popup = ImRaii.Popup("Config Popup"); + if (popup) { - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGuiUtil.TextWrapped( _currentConfiguration ); + ImGuiUtil.TextWrapped(_currentConfiguration); } - if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) - { + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); - } } } - private void UpdateModDirectoryChanged( string path, bool valid ) - => ( _lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime ) = ( path, valid, DateTimeOffset.Now ); + private void UpdateModDirectoryChanged(string path, bool valid) + => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now); } private class Ui { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber< string > PreSettingsDraw; - public readonly EventSubscriber< string > PostSettingsDraw; - public readonly EventSubscriber< ChangedItemType, uint > Tooltip; - public readonly EventSubscriber< MouseButton, ChangedItemType, uint > Click; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber PreSettingsDraw; + public readonly EventSubscriber PostSettingsDraw; + public readonly EventSubscriber Tooltip; + public readonly EventSubscriber Click; private string _lastDrawnMod = string.Empty; private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; @@ -318,194 +306,161 @@ public class IpcTester : IDisposable private string _modName = string.Empty; private PenumbraApiEc _ec = PenumbraApiEc.Success; - public Ui( DalamudPluginInterface pi ) + public Ui(DalamudPluginInterface pi) { _pi = pi; - PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber( pi, UpdateLastDrawnMod ); - PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber( pi, UpdateLastDrawnMod ); - Tooltip = Ipc.ChangedItemTooltip.Subscriber( pi, AddedTooltip ); - Click = Ipc.ChangedItemClick.Subscriber( pi, AddedClick ); + PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); + Tooltip = Ipc.ChangedItemTooltip.Subscriber(pi, AddedTooltip); + Click = Ipc.ChangedItemClick.Subscriber(pi, AddedClick); } public void Draw() { - using var _ = ImRaii.TreeNode( "UI" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("UI"); + if (!_) return; - } - using( var combo = ImRaii.Combo( "Tab to Open at", _selectTab.ToString() ) ) + using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString())) { - if( combo ) - { - foreach( var val in Enum.GetValues< TabType >() ) + if (combo) + foreach (var val in Enum.GetValues()) { - if( ImGui.Selectable( val.ToString(), _selectTab == val ) ) - { + if (ImGui.Selectable(val.ToString(), _selectTab == val)) _selectTab = val; - } } - } } - ImGui.InputTextWithHint( "##openMod", "Mod to Open at...", ref _modName, 256 ); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( Ipc.PostSettingsDraw.Label, "Last Drawn Mod" ); - ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); + DrawIntro(Ipc.PostSettingsDraw.Label, "Last Drawn Mod"); + ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); - DrawIntro( Ipc.ChangedItemTooltip.Label, "Add Tooltip" ); - if( ImGui.Checkbox( "##tooltip", ref _subscribedToTooltip ) ) + DrawIntro(Ipc.ChangedItemTooltip.Label, "Add Tooltip"); + if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip)) { - if( _subscribedToTooltip ) - { + if (_subscribedToTooltip) Tooltip.Enable(); - } else - { Tooltip.Disable(); - } } ImGui.SameLine(); - ImGui.TextUnformatted( _lastHovered ); + ImGui.TextUnformatted(_lastHovered); - DrawIntro( Ipc.ChangedItemClick.Label, "Subscribe Click" ); - if( ImGui.Checkbox( "##click", ref _subscribedToClick ) ) + DrawIntro(Ipc.ChangedItemClick.Label, "Subscribe Click"); + if (ImGui.Checkbox("##click", ref _subscribedToClick)) { - if( _subscribedToClick ) - { + if (_subscribedToClick) Click.Enable(); - } else - { Click.Disable(); - } } ImGui.SameLine(); - ImGui.TextUnformatted( _lastClicked ); - DrawIntro( Ipc.OpenMainWindow.Label, "Open Mod Window" ); - if( ImGui.Button( "Open##window" ) ) - { - _ec = Ipc.OpenMainWindow.Subscriber( _pi ).Invoke( _selectTab, _modName, _modName ); - } + ImGui.TextUnformatted(_lastClicked); + DrawIntro(Ipc.OpenMainWindow.Label, "Open Mod Window"); + if (ImGui.Button("Open##window")) + _ec = Ipc.OpenMainWindow.Subscriber(_pi).Invoke(_selectTab, _modName, _modName); ImGui.SameLine(); - ImGui.TextUnformatted( _ec.ToString() ); + ImGui.TextUnformatted(_ec.ToString()); - DrawIntro( Ipc.CloseMainWindow.Label, "Close Mod Window" ); - if( ImGui.Button( "Close##window" ) ) - { - Ipc.CloseMainWindow.Subscriber( _pi ).Invoke(); - } + DrawIntro(Ipc.CloseMainWindow.Label, "Close Mod Window"); + if (ImGui.Button("Close##window")) + Ipc.CloseMainWindow.Subscriber(_pi).Invoke(); } - private void UpdateLastDrawnMod( string name ) - => ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now ); + private void UpdateLastDrawnMod(string name) + => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); - private void AddedTooltip( ChangedItemType type, uint id ) + private void AddedTooltip(ChangedItemType type, uint id) { - _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; - ImGui.TextUnformatted( "IPC Test Successful" ); + _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; + ImGui.TextUnformatted("IPC Test Successful"); } - private void AddedClick( MouseButton button, ChangedItemType type, uint id ) + private void AddedClick(MouseButton button, ChangedItemType type, uint id) { - _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; + _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; } } private class Redrawing { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber< IntPtr, int > Redrawn; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber Redrawn; private string _redrawName = string.Empty; private int _redrawIndex = 0; private string _lastRedrawnString = "None"; - public Redrawing( DalamudPluginInterface pi ) + public Redrawing(DalamudPluginInterface pi) { _pi = pi; - Redrawn = Ipc.GameObjectRedrawn.Subscriber( pi, SetLastRedrawn ); + Redrawn = Ipc.GameObjectRedrawn.Subscriber(pi, SetLastRedrawn); } public void Draw() { - using var _ = ImRaii.TreeNode( "Redrawing" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Redrawing"); + if (!_) return; - } - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( Ipc.RedrawObjectByName.Label, "Redraw by Name" ); - ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); - ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); + DrawIntro(Ipc.RedrawObjectByName.Label, "Redraw by Name"); + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32); ImGui.SameLine(); - if( ImGui.Button( "Redraw##Name" ) ) - { - Ipc.RedrawObjectByName.Subscriber( _pi ).Invoke( _redrawName, RedrawType.Redraw ); - } + if (ImGui.Button("Redraw##Name")) + Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw); - DrawIntro( Ipc.RedrawObject.Label, "Redraw Player Character" ); - if( ImGui.Button( "Redraw##pc" ) && DalamudServices.SClientState.LocalPlayer != null ) - { - Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw ); - } + DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character"); + if (ImGui.Button("Redraw##pc") && DalamudServices.SClientState.LocalPlayer != null) + Ipc.RedrawObject.Subscriber(_pi).Invoke(DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw); - DrawIntro( Ipc.RedrawObjectByIndex.Label, "Redraw by Index" ); + DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); var tmp = _redrawIndex; - ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); - if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length ) ) - { - _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.SObjects.Length ); - } + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length)) + _redrawIndex = Math.Clamp(tmp, 0, DalamudServices.SObjects.Length); ImGui.SameLine(); - if( ImGui.Button( "Redraw##Index" ) ) - { - Ipc.RedrawObjectByIndex.Subscriber( _pi ).Invoke( _redrawIndex, RedrawType.Redraw ); - } + if (ImGui.Button("Redraw##Index")) + Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw); - DrawIntro( Ipc.RedrawAll.Label, "Redraw All" ); - if( ImGui.Button( "Redraw##All" ) ) - { - Ipc.RedrawAll.Subscriber( _pi ).Invoke( RedrawType.Redraw ); - } + DrawIntro(Ipc.RedrawAll.Label, "Redraw All"); + if (ImGui.Button("Redraw##All")) + Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw); - DrawIntro( Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:" ); - ImGui.TextUnformatted( _lastRedrawnString ); + DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:"); + ImGui.TextUnformatted(_lastRedrawnString); } - private void SetLastRedrawn( IntPtr address, int index ) + private void SetLastRedrawn(IntPtr address, int index) { - if( index < 0 || index > DalamudServices.SObjects.Length || address == IntPtr.Zero || DalamudServices.SObjects[ index ]?.Address != address ) - { + if (index < 0 + || index > DalamudServices.SObjects.Length + || address == IntPtr.Zero + || DalamudServices.SObjects[index]?.Address != address) _lastRedrawnString = "Invalid"; - } - _lastRedrawnString = $"{DalamudServices.SObjects[ index ]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{DalamudServices.SObjects[index]!.Name} (0x{address:X}, {index})"; } } private class GameState { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr > CharacterBaseCreating; - public readonly EventSubscriber< IntPtr, string, IntPtr > CharacterBaseCreated; - public readonly EventSubscriber< IntPtr, string, string > GameObjectResourcePathResolved; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber CharacterBaseCreating; + public readonly EventSubscriber CharacterBaseCreated; + public readonly EventSubscriber GameObjectResourcePathResolved; private string _lastCreatedGameObjectName = string.Empty; @@ -519,94 +474,85 @@ public class IpcTester : IDisposable private IntPtr _currentDrawObject = IntPtr.Zero; private int _currentCutsceneActor = 0; - public GameState( DalamudPluginInterface pi ) + public GameState(DalamudPluginInterface pi) { _pi = pi; - CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber( pi, UpdateLastCreated ); - CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber( pi, UpdateLastCreated2 ); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber( pi, UpdateGameObjectResourcePath ); + CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); + CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); + GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); } public void Draw() { - using var _ = ImRaii.TreeNode( "Game State" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Game State"); + if (!_) return; - } - if( ImGui.InputTextWithHint( "##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, - ImGuiInputTextFlags.CharsHexadecimal ) ) - { - _currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp ) + if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, + ImGuiInputTextFlags.CharsHexadecimal)) + _currentDrawObject = IntPtr.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var tmp) ? tmp : IntPtr.Zero; - } - ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 ); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( Ipc.GetDrawObjectInfo.Label, "Draw Object Info" ); - if( _currentDrawObject == IntPtr.Zero ) + DrawIntro(Ipc.GetDrawObjectInfo.Label, "Draw Object Info"); + if (_currentDrawObject == IntPtr.Zero) { - ImGui.TextUnformatted( "Invalid" ); + ImGui.TextUnformatted("Invalid"); } else { - var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber( _pi ).Invoke( _currentDrawObject ); - ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); + var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber(_pi).Invoke(_currentDrawObject); + ImGui.TextUnformatted(ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}"); } - DrawIntro( Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent" ); - ImGui.TextUnformatted( Ipc.GetCutsceneParentIndex.Subscriber( _pi ).Invoke( _currentCutsceneActor ).ToString() ); + DrawIntro(Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent"); + ImGui.TextUnformatted(Ipc.GetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor).ToString()); - DrawIntro( Ipc.CreatingCharacterBase.Label, "Last Drawobject created" ); - if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) - { - ImGui.TextUnformatted( _lastCreatedDrawObject != IntPtr.Zero + DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created"); + if (_lastCreatedGameObjectTime < DateTimeOffset.Now) + ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" - : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); - } + : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"); - DrawIntro( Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved" ); - if( _lastResolvedGamePathTime < DateTimeOffset.Now ) - { + DrawIntro(Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved"); + if (_lastResolvedGamePathTime < DateTimeOffset.Now) ImGui.TextUnformatted( - $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}" ); - } + $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}"); } - private void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) + private void UpdateLastCreated(IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4) { - _lastCreatedGameObjectName = GetObjectName( gameObject ); + _lastCreatedGameObjectName = GetObjectName(gameObject); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = IntPtr.Zero; } - private void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) + private void UpdateLastCreated2(IntPtr gameObject, string _, IntPtr drawObject) { - _lastCreatedGameObjectName = GetObjectName( gameObject ); + _lastCreatedGameObjectName = GetObjectName(gameObject); _lastCreatedGameObjectTime = DateTimeOffset.Now; _lastCreatedDrawObject = drawObject; } - private void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) + private void UpdateGameObjectResourcePath(IntPtr gameObject, string gamePath, string fullPath) { - _lastResolvedObject = GetObjectName( gameObject ); + _lastResolvedObject = GetObjectName(gameObject); _lastResolvedGamePath = gamePath; _lastResolvedFullPath = fullPath; _lastResolvedGamePathTime = DateTimeOffset.Now; } - private static unsafe string GetObjectName( IntPtr gameObject ) + private static unsafe string GetObjectName(IntPtr gameObject) { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; var name = obj != null ? obj->GetName() : null; - return name != null ? new ByteString( name ).ToString() : "Unknown"; + return name != null ? new ByteString(name).ToString() : "Unknown"; } } @@ -619,124 +565,110 @@ public class IpcTester : IDisposable private string _currentReversePath = string.Empty; private int _currentReverseIdx = 0; - public Resolve( DalamudPluginInterface pi ) + public Resolve(DalamudPluginInterface pi) => _pi = pi; public void Draw() { - using var _ = ImRaii.TreeNode( "Resolving" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Resolving"); + if (!_) return; - } - ImGui.InputTextWithHint( "##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength ); - ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 ); - ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, - Utf8GamePath.MaxGamePathLength ); - ImGui.InputInt( "##resolveIdx", ref _currentReverseIdx, 0, 0 ); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); + ImGui.InputTextWithHint("##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32); + ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, + Utf8GamePath.MaxGamePathLength); + ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( Ipc.ResolveDefaultPath.Label, "Default Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( Ipc.ResolveDefaultPath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); - } + DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(_pi).Invoke(_currentResolvePath)); - DrawIntro( Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( Ipc.ResolveInterfacePath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); - } + DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(_pi).Invoke(_currentResolvePath)); - DrawIntro( Ipc.ResolvePlayerPath.Label, "Player Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( Ipc.ResolvePlayerPath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); - } + DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(_pi).Invoke(_currentResolvePath)); - DrawIntro( Ipc.ResolveCharacterPath.Label, "Character Collection Resolve" ); - if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) - { - ImGui.TextUnformatted( Ipc.ResolveCharacterPath.Subscriber( _pi ).Invoke( _currentResolvePath, _currentResolveCharacter ) ); - } + DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve"); + if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0) + ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentResolveCharacter)); - DrawIntro( Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( Ipc.ResolveGameObjectPath.Subscriber( _pi ).Invoke( _currentResolvePath, _currentReverseIdx ) ); - } + DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentReverseIdx)); - DrawIntro( Ipc.ReverseResolvePath.Label, "Reversed Game Paths" ); - if( _currentReversePath.Length > 0 ) + DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths"); + if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePath.Subscriber( _pi ).Invoke( _currentReversePath, _currentResolveCharacter ); - if( list.Length > 0 ) + var list = Ipc.ReverseResolvePath.Subscriber(_pi).Invoke(_currentReversePath, _currentResolveCharacter); + if (list.Length > 0) { - ImGui.TextUnformatted( list[ 0 ] ); - if( list.Length > 1 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); - } + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); } } - DrawIntro( Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)" ); - if( _currentReversePath.Length > 0 ) + DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); + if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePlayerPath.Subscriber( _pi ).Invoke( _currentReversePath ); - if( list.Length > 0 ) + var list = Ipc.ReverseResolvePlayerPath.Subscriber(_pi).Invoke(_currentReversePath); + if (list.Length > 0) { - ImGui.TextUnformatted( list[ 0 ] ); - if( list.Length > 1 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); - } + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); } } - DrawIntro( Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)" ); - if( _currentReversePath.Length > 0 ) + DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); + if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolveGameObjectPath.Subscriber( _pi ).Invoke( _currentReversePath, _currentReverseIdx ); - if( list.Length > 0 ) + var list = Ipc.ReverseResolveGameObjectPath.Subscriber(_pi).Invoke(_currentReversePath, _currentReverseIdx); + if (list.Length > 0) { - ImGui.TextUnformatted( list[ 0 ] ); - if( list.Length > 1 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); - } + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); } } - DrawIntro( Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)" ); - if( _currentResolvePath.Length > 0 || _currentReversePath.Length > 0 ) + DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (_currentResolvePath.Length > 0 || _currentReversePath.Length > 0) { - var forwardArray = _currentResolvePath.Length > 0 ? new[] { _currentResolvePath } : Array.Empty< string >(); - var reverseArray = _currentReversePath.Length > 0 ? new[] { _currentReversePath } : Array.Empty< string >(); - var ret = Ipc.ResolvePlayerPaths.Subscriber( _pi ).Invoke( forwardArray, reverseArray ); - var text = string.Empty; - if( ret.Item1.Length > 0 ) - { - if( ret.Item2.Length > 0 ) + var forwardArray = _currentResolvePath.Length > 0 + ? new[] { - text = $"Forward: {ret.Item1[ 0 ]} | Reverse: {string.Join( "; ", ret.Item2[ 0 ] )}."; + _currentResolvePath, } + : Array.Empty(); + var reverseArray = _currentReversePath.Length > 0 + ? new[] + { + _currentReversePath, + } + : Array.Empty(); + var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray); + var text = string.Empty; + if (ret.Item1.Length > 0) + { + if (ret.Item2.Length > 0) + text = $"Forward: {ret.Item1[0]} | Reverse: {string.Join("; ", ret.Item2[0])}."; else - { - text = $"Forward: {ret.Item1[ 0 ]}."; - } + text = $"Forward: {ret.Item1[0]}."; } - else if( ret.Item2.Length > 0 ) + else if (ret.Item2.Length > 0) { - text = $"Reverse: {string.Join( "; ", ret.Item2[ 0 ] )}."; + text = $"Reverse: {string.Join("; ", ret.Item2[0])}."; } - ImGui.TextUnformatted( text ); + ImGui.TextUnformatted(text); } } } @@ -751,95 +683,85 @@ public class IpcTester : IDisposable private bool _allowDeletion = true; private ApiCollectionType _type = ApiCollectionType.Current; - private string _characterCollectionName = string.Empty; - private IList< string > _collections = new List< string >(); - private string _changedItemCollection = string.Empty; - private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >(); - private PenumbraApiEc _returnCode = PenumbraApiEc.Success; - private string? _oldCollection = null; + private string _characterCollectionName = string.Empty; + private IList _collections = new List(); + private string _changedItemCollection = string.Empty; + private IReadOnlyDictionary _changedItems = new Dictionary(); + private PenumbraApiEc _returnCode = PenumbraApiEc.Success; + private string? _oldCollection = null; - public Collections( DalamudPluginInterface pi ) + public Collections(DalamudPluginInterface pi) => _pi = pi; public void Draw() { - using var _ = ImRaii.TreeNode( "Collections" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Collections"); + if (!_) return; - } - ImGuiUtil.GenericEnumCombo( "Collection Type", 200, _type, out _type, t => ( ( CollectionType )t ).ToName() ); - ImGui.InputInt( "Object Index##Collections", ref _objectIdx, 0, 0 ); - ImGui.InputText( "Collection Name##Collections", ref _collectionName, 64 ); - ImGui.Checkbox( "Allow Assignment Creation", ref _allowCreation ); + ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); + ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); + ImGui.InputText("Collection Name##Collections", ref _collectionName, 64); + ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); ImGui.SameLine(); - ImGui.Checkbox( "Allow Assignment Deletion", ref _allowDeletion ); + ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( "Last Return Code", _returnCode.ToString() ); - if( _oldCollection != null ) - { - ImGui.TextUnformatted( _oldCollection.Length == 0 ? "Created" : _oldCollection ); - } + DrawIntro("Last Return Code", _returnCode.ToString()); + if (_oldCollection != null) + ImGui.TextUnformatted(_oldCollection.Length == 0 ? "Created" : _oldCollection); - DrawIntro( Ipc.GetCurrentCollectionName.Label, "Current Collection" ); - ImGui.TextUnformatted( Ipc.GetCurrentCollectionName.Subscriber( _pi ).Invoke() ); - DrawIntro( Ipc.GetDefaultCollectionName.Label, "Default Collection" ); - ImGui.TextUnformatted( Ipc.GetDefaultCollectionName.Subscriber( _pi ).Invoke() ); - DrawIntro( Ipc.GetInterfaceCollectionName.Label, "Interface Collection" ); - ImGui.TextUnformatted( Ipc.GetInterfaceCollectionName.Subscriber( _pi ).Invoke() ); - DrawIntro( Ipc.GetCharacterCollectionName.Label, "Character" ); - ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); - ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); - var (c, s) = Ipc.GetCharacterCollectionName.Subscriber( _pi ).Invoke( _characterCollectionName ); + DrawIntro(Ipc.GetCurrentCollectionName.Label, "Current Collection"); + ImGui.TextUnformatted(Ipc.GetCurrentCollectionName.Subscriber(_pi).Invoke()); + DrawIntro(Ipc.GetDefaultCollectionName.Label, "Default Collection"); + ImGui.TextUnformatted(Ipc.GetDefaultCollectionName.Subscriber(_pi).Invoke()); + DrawIntro(Ipc.GetInterfaceCollectionName.Label, "Interface Collection"); + ImGui.TextUnformatted(Ipc.GetInterfaceCollectionName.Subscriber(_pi).Invoke()); + DrawIntro(Ipc.GetCharacterCollectionName.Label, "Character"); + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + ImGui.InputTextWithHint("##characterCollectionName", "Character Name...", ref _characterCollectionName, 64); + var (c, s) = Ipc.GetCharacterCollectionName.Subscriber(_pi).Invoke(_characterCollectionName); ImGui.SameLine(); - ImGui.TextUnformatted( $"{c}, {( s ? "Custom" : "Default" )}" ); + ImGui.TextUnformatted($"{c}, {(s ? "Custom" : "Default")}"); - DrawIntro( Ipc.GetCollections.Label, "Collections" ); - if( ImGui.Button( "Get##Collections" ) ) + DrawIntro(Ipc.GetCollections.Label, "Collections"); + if (ImGui.Button("Get##Collections")) { - _collections = Ipc.GetCollections.Subscriber( _pi ).Invoke(); - ImGui.OpenPopup( "Collections" ); + _collections = Ipc.GetCollections.Subscriber(_pi).Invoke(); + ImGui.OpenPopup("Collections"); } - DrawIntro( Ipc.GetCollectionForType.Label, "Get Special Collection" ); - var name = Ipc.GetCollectionForType.Subscriber( _pi ).Invoke( _type ); - ImGui.TextUnformatted( name.Length == 0 ? "Unassigned" : name ); - DrawIntro( Ipc.SetCollectionForType.Label, "Set Special Collection" ); - if( ImGui.Button( "Set##TypeCollection" ) ) - { - ( _returnCode, _oldCollection ) = Ipc.SetCollectionForType.Subscriber( _pi ).Invoke( _type, _collectionName, _allowCreation, _allowDeletion ); - } + DrawIntro(Ipc.GetCollectionForType.Label, "Get Special Collection"); + var name = Ipc.GetCollectionForType.Subscriber(_pi).Invoke(_type); + ImGui.TextUnformatted(name.Length == 0 ? "Unassigned" : name); + DrawIntro(Ipc.SetCollectionForType.Label, "Set Special Collection"); + if (ImGui.Button("Set##TypeCollection")) + (_returnCode, _oldCollection) = + Ipc.SetCollectionForType.Subscriber(_pi).Invoke(_type, _collectionName, _allowCreation, _allowDeletion); - DrawIntro( Ipc.GetCollectionForObject.Label, "Get Object Collection" ); - ( var valid, var individual, name ) = Ipc.GetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx ); + DrawIntro(Ipc.GetCollectionForObject.Label, "Get Object Collection"); + (var valid, var individual, name) = Ipc.GetCollectionForObject.Subscriber(_pi).Invoke(_objectIdx); ImGui.TextUnformatted( - $"{( valid ? "Valid" : "Invalid" )} Object, {( name.Length == 0 ? "Unassigned" : name )}{( individual ? " (Individual Assignment)" : string.Empty )}" ); - DrawIntro( Ipc.SetCollectionForObject.Label, "Set Object Collection" ); - if( ImGui.Button( "Set##ObjectCollection" ) ) - { - ( _returnCode, _oldCollection ) = Ipc.SetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx, _collectionName, _allowCreation, _allowDeletion ); - } + $"{(valid ? "Valid" : "Invalid")} Object, {(name.Length == 0 ? "Unassigned" : name)}{(individual ? " (Individual Assignment)" : string.Empty)}"); + DrawIntro(Ipc.SetCollectionForObject.Label, "Set Object Collection"); + if (ImGui.Button("Set##ObjectCollection")) + (_returnCode, _oldCollection) = Ipc.SetCollectionForObject.Subscriber(_pi) + .Invoke(_objectIdx, _collectionName, _allowCreation, _allowDeletion); - if( _returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty() ) - { + if (_returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty()) _oldCollection = null; - } - DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); - ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); - ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); + DrawIntro(Ipc.GetChangedItems.Label, "Changed Item List"); + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + ImGui.InputTextWithHint("##changedCollection", "Collection Name...", ref _changedItemCollection, 64); ImGui.SameLine(); - if( ImGui.Button( "Get" ) ) + if (ImGui.Button("Get")) { - _changedItems = Ipc.GetChangedItems.Subscriber( _pi ).Invoke( _changedItemCollection ); - ImGui.OpenPopup( "Changed Item List" ); + _changedItems = Ipc.GetChangedItems.Subscriber(_pi).Invoke(_changedItemCollection); + ImGui.OpenPopup("Changed Item List"); } DrawChangedItemPopup(); @@ -848,42 +770,30 @@ public class IpcTester : IDisposable private void DrawChangedItemPopup() { - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var p = ImRaii.Popup( "Changed Item List" ); - if( !p ) - { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Changed Item List"); + if (!p) return; - } - foreach( var item in _changedItems ) - { - ImGui.TextUnformatted( item.Key ); - } + foreach (var item in _changedItems) + ImGui.TextUnformatted(item.Key); - if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) - { + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); - } } private void DrawCollectionPopup() { - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var p = ImRaii.Popup( "Collections" ); - if( !p ) - { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Collections"); + if (!p) return; - } - foreach( var collection in _collections ) - { - ImGui.TextUnformatted( collection ); - } + foreach (var collection in _collections) + ImGui.TextUnformatted(collection); - if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) - { + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); - } } } @@ -894,44 +804,40 @@ public class IpcTester : IDisposable private string _characterName = string.Empty; private int _gameObjectIndex = 0; - public Meta( DalamudPluginInterface pi ) + public Meta(DalamudPluginInterface pi) => _pi = pi; public void Draw() { - using var _ = ImRaii.TreeNode( "Meta" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Meta"); + if (!_) return; - } - ImGui.InputTextWithHint( "##characterName", "Character Name...", ref _characterName, 64 ); - ImGui.InputInt( "##metaIdx", ref _gameObjectIndex, 0, 0 ); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + ImGui.InputTextWithHint("##characterName", "Character Name...", ref _characterName, 64); + ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; + + DrawIntro(Ipc.GetMetaManipulations.Label, "Meta Manipulations"); + if (ImGui.Button("Copy to Clipboard")) + { + var base64 = Ipc.GetMetaManipulations.Subscriber(_pi).Invoke(_characterName); + ImGui.SetClipboardText(base64); } - DrawIntro( Ipc.GetMetaManipulations.Label, "Meta Manipulations" ); - if( ImGui.Button( "Copy to Clipboard" ) ) + DrawIntro(Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations"); + if (ImGui.Button("Copy to Clipboard##Player")) { - var base64 = Ipc.GetMetaManipulations.Subscriber( _pi ).Invoke( _characterName ); - ImGui.SetClipboardText( base64 ); + var base64 = Ipc.GetPlayerMetaManipulations.Subscriber(_pi).Invoke(); + ImGui.SetClipboardText(base64); } - DrawIntro( Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations" ); - if( ImGui.Button( "Copy to Clipboard##Player" ) ) + DrawIntro(Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations"); + if (ImGui.Button("Copy to Clipboard##GameObject")) { - var base64 = Ipc.GetPlayerMetaManipulations.Subscriber( _pi ).Invoke(); - ImGui.SetClipboardText( base64 ); - } - - DrawIntro( Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations" ); - if( ImGui.Button( "Copy to Clipboard##GameObject" ) ) - { - var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber( _pi ).Invoke( _gameObjectIndex ); - ImGui.SetClipboardText( base64 ); + var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber(_pi).Invoke(_gameObjectIndex); + ImGui.SetClipboardText(base64); } } } @@ -940,18 +846,18 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; - private PenumbraApiEc _lastReloadEc; - private PenumbraApiEc _lastAddEc; - private PenumbraApiEc _lastDeleteEc; - private PenumbraApiEc _lastSetPathEc; - private IList< (string, string) > _mods = new List< (string, string) >(); + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private IList<(string, string)> _mods = new List<(string, string)>(); - public readonly EventSubscriber< string > DeleteSubscriber; - public readonly EventSubscriber< string > AddSubscriber; - public readonly EventSubscriber< string, string > MoveSubscriber; + public readonly EventSubscriber DeleteSubscriber; + public readonly EventSubscriber AddSubscriber; + public readonly EventSubscriber MoveSubscriber; private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; private string _lastDeletedMod = string.Empty; @@ -961,137 +867,113 @@ public class IpcTester : IDisposable private string _lastMovedModFrom = string.Empty; private string _lastMovedModTo = string.Empty; - public Mods( DalamudPluginInterface pi ) + public Mods(DalamudPluginInterface pi) { _pi = pi; - DeleteSubscriber = Ipc.ModDeleted.Subscriber( pi, s => + DeleteSubscriber = Ipc.ModDeleted.Subscriber(pi, s => { _lastDeletedModTime = DateTimeOffset.UtcNow; _lastDeletedMod = s; - } ); - AddSubscriber = Ipc.ModAdded.Subscriber( pi, s => + }); + AddSubscriber = Ipc.ModAdded.Subscriber(pi, s => { _lastAddedModTime = DateTimeOffset.UtcNow; _lastAddedMod = s; - } ); - MoveSubscriber = Ipc.ModMoved.Subscriber( pi, ( s1, s2 ) => + }); + MoveSubscriber = Ipc.ModMoved.Subscriber(pi, (s1, s2) => { _lastMovedModTime = DateTimeOffset.UtcNow; _lastMovedModFrom = s1; _lastMovedModTo = s2; - } ); + }); } public void Draw() { - using var _ = ImRaii.TreeNode( "Mods" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Mods"); + if (!_) return; - } - ImGui.InputTextWithHint( "##modDir", "Mod Directory Name...", ref _modDirectory, 100 ); - ImGui.InputTextWithHint( "##modName", "Mod Name...", ref _modName, 100 ); - ImGui.InputTextWithHint( "##path", "New Path...", ref _pathInput, 100 ); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); + ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); + ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; + + DrawIntro(Ipc.GetMods.Label, "Mods"); + if (ImGui.Button("Get##Mods")) + { + _mods = Ipc.GetMods.Subscriber(_pi).Invoke(); + ImGui.OpenPopup("Mods"); } - DrawIntro( Ipc.GetMods.Label, "Mods" ); - if( ImGui.Button( "Get##Mods" ) ) - { - _mods = Ipc.GetMods.Subscriber( _pi ).Invoke(); - ImGui.OpenPopup( "Mods" ); - } - - DrawIntro( Ipc.ReloadMod.Label, "Reload Mod" ); - if( ImGui.Button( "Reload" ) ) - { - _lastReloadEc = Ipc.ReloadMod.Subscriber( _pi ).Invoke( _modDirectory, _modName ); - } + DrawIntro(Ipc.ReloadMod.Label, "Reload Mod"); + if (ImGui.Button("Reload")) + _lastReloadEc = Ipc.ReloadMod.Subscriber(_pi).Invoke(_modDirectory, _modName); ImGui.SameLine(); - ImGui.TextUnformatted( _lastReloadEc.ToString() ); + ImGui.TextUnformatted(_lastReloadEc.ToString()); - DrawIntro( Ipc.AddMod.Label, "Add Mod" ); - if( ImGui.Button( "Add" ) ) - { - _lastAddEc = Ipc.AddMod.Subscriber( _pi ).Invoke( _modDirectory ); - } + DrawIntro(Ipc.AddMod.Label, "Add Mod"); + if (ImGui.Button("Add")) + _lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory); ImGui.SameLine(); - ImGui.TextUnformatted( _lastAddEc.ToString() ); + ImGui.TextUnformatted(_lastAddEc.ToString()); - DrawIntro( Ipc.DeleteMod.Label, "Delete Mod" ); - if( ImGui.Button( "Delete" ) ) - { - _lastDeleteEc = Ipc.DeleteMod.Subscriber( _pi ).Invoke( _modDirectory, _modName ); - } + DrawIntro(Ipc.DeleteMod.Label, "Delete Mod"); + if (ImGui.Button("Delete")) + _lastDeleteEc = Ipc.DeleteMod.Subscriber(_pi).Invoke(_modDirectory, _modName); ImGui.SameLine(); - ImGui.TextUnformatted( _lastDeleteEc.ToString() ); + ImGui.TextUnformatted(_lastDeleteEc.ToString()); - DrawIntro( Ipc.GetModPath.Label, "Current Path" ); - var (ec, path, def) = Ipc.GetModPath.Subscriber( _pi ).Invoke( _modDirectory, _modName ); - ImGui.TextUnformatted( $"{path} ({( def ? "Custom" : "Default" )}) [{ec}]" ); + DrawIntro(Ipc.GetModPath.Label, "Current Path"); + var (ec, path, def) = Ipc.GetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName); + ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")}) [{ec}]"); - DrawIntro( Ipc.SetModPath.Label, "Set Path" ); - if( ImGui.Button( "Set" ) ) - { - _lastSetPathEc = Ipc.SetModPath.Subscriber( _pi ).Invoke( _modDirectory, _modName, _pathInput ); - } + DrawIntro(Ipc.SetModPath.Label, "Set Path"); + if (ImGui.Button("Set")) + _lastSetPathEc = Ipc.SetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName, _pathInput); ImGui.SameLine(); - ImGui.TextUnformatted( _lastSetPathEc.ToString() ); + ImGui.TextUnformatted(_lastSetPathEc.ToString()); - DrawIntro( Ipc.ModDeleted.Label, "Last Mod Deleted" ); - if( _lastDeletedModTime > DateTimeOffset.UnixEpoch ) - { - ImGui.TextUnformatted( $"{_lastDeletedMod} at {_lastDeletedModTime}" ); - } + DrawIntro(Ipc.ModDeleted.Label, "Last Mod Deleted"); + if (_lastDeletedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}"); - DrawIntro( Ipc.ModAdded.Label, "Last Mod Added" ); - if( _lastAddedModTime > DateTimeOffset.UnixEpoch ) - { - ImGui.TextUnformatted( $"{_lastAddedMod} at {_lastAddedModTime}" ); - } + DrawIntro(Ipc.ModAdded.Label, "Last Mod Added"); + if (_lastAddedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}"); - DrawIntro( Ipc.ModMoved.Label, "Last Mod Moved" ); - if( _lastMovedModTime > DateTimeOffset.UnixEpoch ) - { - ImGui.TextUnformatted( $"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}" ); - } + DrawIntro(Ipc.ModMoved.Label, "Last Mod Moved"); + if (_lastMovedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}"); DrawModsPopup(); } private void DrawModsPopup() { - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var p = ImRaii.Popup( "Mods" ); - if( !p ) - { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Mods"); + if (!p) return; - } - foreach( var (modDir, modName) in _mods ) - { - ImGui.TextUnformatted( $"{modDir}: {modName}" ); - } + foreach (var (modDir, modName) in _mods) + ImGui.TextUnformatted($"{modDir}: {modName}"); - if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) - { + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); - } } } private class ModSettings { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber< ModSettingChange, string, string, bool > SettingChanged; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber SettingChanged; private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; private ModSettingChange _lastSettingChangeType; @@ -1100,60 +982,57 @@ public class IpcTester : IDisposable private bool _lastSettingChangeInherited; private DateTimeOffset _lastSettingChange; - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private string _settingsCollection = string.Empty; - private bool _settingsAllowInheritance = true; - private bool _settingsInherit = false; - private bool _settingsEnabled = false; - private int _settingsPriority = 0; - private IDictionary< string, (IList< string >, GroupType) >? _availableSettings; - private IDictionary< string, IList< string > >? _currentSettings = null; + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private string _settingsCollection = string.Empty; + private bool _settingsAllowInheritance = true; + private bool _settingsInherit = false; + private bool _settingsEnabled = false; + private int _settingsPriority = 0; + private IDictionary, GroupType)>? _availableSettings; + private IDictionary>? _currentSettings = null; - public ModSettings( DalamudPluginInterface pi ) + public ModSettings(DalamudPluginInterface pi) { _pi = pi; - SettingChanged = Ipc.ModSettingChanged.Subscriber( pi, UpdateLastModSetting ); + SettingChanged = Ipc.ModSettingChanged.Subscriber(pi, UpdateLastModSetting); } public void Draw() { - using var _ = ImRaii.TreeNode( "Mod Settings" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Mod Settings"); + if (!_) return; - } - ImGui.InputTextWithHint( "##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100 ); - ImGui.InputTextWithHint( "##settingsName", "Mod Name...", ref _settingsModName, 100 ); - ImGui.InputTextWithHint( "##settingsCollection", "Collection...", ref _settingsCollection, 100 ); - ImGui.Checkbox( "Allow Inheritance", ref _settingsAllowInheritance ); + ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); + ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); + ImGui.InputTextWithHint("##settingsCollection", "Collection...", ref _settingsCollection, 100); + ImGui.Checkbox("Allow Inheritance", ref _settingsAllowInheritance); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( "Last Error", _lastSettingsError.ToString() ); - DrawIntro( Ipc.ModSettingChanged.Label, "Last Mod Setting Changed" ); - ImGui.TextUnformatted( _lastSettingChangeMod.Length > 0 - ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{( _lastSettingChangeInherited ? " (Inherited)" : string.Empty )} at {_lastSettingChange}" - : "None" ); - DrawIntro( Ipc.GetAvailableModSettings.Label, "Get Available Settings" ); - if( ImGui.Button( "Get##Available" ) ) + DrawIntro("Last Error", _lastSettingsError.ToString()); + DrawIntro(Ipc.ModSettingChanged.Label, "Last Mod Setting Changed"); + ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0 + ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}" + : "None"); + DrawIntro(Ipc.GetAvailableModSettings.Label, "Get Available Settings"); + if (ImGui.Button("Get##Available")) { - _availableSettings = Ipc.GetAvailableModSettings.Subscriber( _pi ).Invoke( _settingsModDirectory, _settingsModName ); + _availableSettings = Ipc.GetAvailableModSettings.Subscriber(_pi).Invoke(_settingsModDirectory, _settingsModName); _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; } - DrawIntro( Ipc.GetCurrentModSettings.Label, "Get Current Settings" ); - if( ImGui.Button( "Get##Current" ) ) + DrawIntro(Ipc.GetCurrentModSettings.Label, "Get Current Settings"); + if (ImGui.Button("Get##Current")) { - var ret = Ipc.GetCurrentModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance ); + var ret = Ipc.GetCurrentModSettings.Subscriber(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance); _lastSettingsError = ret.Item1; - if( ret.Item1 == PenumbraApiEc.Success ) + if (ret.Item1 == PenumbraApiEc.Success) { _settingsEnabled = ret.Item2?.Item1 ?? false; _settingsInherit = ret.Item2?.Item4 ?? false; @@ -1166,107 +1045,88 @@ public class IpcTester : IDisposable } } - DrawIntro( Ipc.TryInheritMod.Label, "Inherit Mod" ); - ImGui.Checkbox( "##inherit", ref _settingsInherit ); + DrawIntro(Ipc.TryInheritMod.Label, "Inherit Mod"); + ImGui.Checkbox("##inherit", ref _settingsInherit); ImGui.SameLine(); - if( ImGui.Button( "Set##Inherit" ) ) - { - _lastSettingsError = Ipc.TryInheritMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit ); - } + if (ImGui.Button("Set##Inherit")) + _lastSettingsError = Ipc.TryInheritMod.Subscriber(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit); - DrawIntro( Ipc.TrySetMod.Label, "Set Enabled" ); - ImGui.Checkbox( "##enabled", ref _settingsEnabled ); + DrawIntro(Ipc.TrySetMod.Label, "Set Enabled"); + ImGui.Checkbox("##enabled", ref _settingsEnabled); ImGui.SameLine(); - if( ImGui.Button( "Set##Enabled" ) ) - { - _lastSettingsError = Ipc.TrySetMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled ); - } + if (ImGui.Button("Set##Enabled")) + _lastSettingsError = Ipc.TrySetMod.Subscriber(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled); - DrawIntro( Ipc.TrySetModPriority.Label, "Set Priority" ); - ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); - ImGui.DragInt( "##Priority", ref _settingsPriority ); + DrawIntro(Ipc.TrySetModPriority.Label, "Set Priority"); + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + ImGui.DragInt("##Priority", ref _settingsPriority); ImGui.SameLine(); - if( ImGui.Button( "Set##Priority" ) ) - { - _lastSettingsError = Ipc.TrySetModPriority.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); - } + if (ImGui.Button("Set##Priority")) + _lastSettingsError = Ipc.TrySetModPriority.Subscriber(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority); - DrawIntro( Ipc.CopyModSettings.Label, "Copy Mod Settings" ); - if( ImGui.Button( "Copy Settings" ) ) - { - _lastSettingsError = Ipc.CopyModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName ); - } + DrawIntro(Ipc.CopyModSettings.Label, "Copy Mod Settings"); + if (ImGui.Button("Copy Settings")) + _lastSettingsError = Ipc.CopyModSettings.Subscriber(_pi).Invoke(_settingsCollection, _settingsModDirectory, _settingsModName); - ImGuiUtil.HoverTooltip( "Copy settings from Mod Directory Name to Mod Name (as directory) in collection." ); + ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection."); - DrawIntro( Ipc.TrySetModSetting.Label, "Set Setting(s)" ); - if( _availableSettings == null ) - { + DrawIntro(Ipc.TrySetModSetting.Label, "Set Setting(s)"); + if (_availableSettings == null) return; - } - foreach( var (group, (list, type)) in _availableSettings ) + foreach (var (group, (list, type)) in _availableSettings) { - using var id = ImRaii.PushId( group ); - var preview = list.Count > 0 ? list[ 0 ] : string.Empty; - IList< string > current; - if( _currentSettings != null && _currentSettings.TryGetValue( group, out current! ) && current.Count > 0 ) + using var id = ImRaii.PushId(group); + var preview = list.Count > 0 ? list[0] : string.Empty; + IList current; + if (_currentSettings != null && _currentSettings.TryGetValue(group, out current!) && current.Count > 0) { - preview = current[ 0 ]; + preview = current[0]; } else { - current = new List< string >(); - if( _currentSettings != null ) - { - _currentSettings[ group ] = current; - } + current = new List(); + if (_currentSettings != null) + _currentSettings[group] = current; } - ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); - using( var c = ImRaii.Combo( "##group", preview ) ) + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + using (var c = ImRaii.Combo("##group", preview)) { - if( c ) - { - foreach( var s in list ) + if (c) + foreach (var s in list) { - var contained = current.Contains( s ); - if( ImGui.Checkbox( s, ref contained ) ) + var contained = current.Contains(s); + if (ImGui.Checkbox(s, ref contained)) { - if( contained ) - { - current.Add( s ); - } + if (contained) + current.Add(s); else - { - current.Remove( s ); - } + current.Remove(s); } } - } } ImGui.SameLine(); - if( ImGui.Button( "Set##setting" ) ) + if (ImGui.Button("Set##setting")) { - if( type == GroupType.Single ) - { - _lastSettingsError = Ipc.TrySetModSetting.Subscriber( _pi ).Invoke( _settingsCollection, - _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty ); - } + if (type == GroupType.Single) + _lastSettingsError = Ipc.TrySetModSetting.Subscriber(_pi).Invoke(_settingsCollection, + _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[0] : string.Empty); else - { - _lastSettingsError = Ipc.TrySetModSettings.Subscriber( _pi ).Invoke( _settingsCollection, - _settingsModDirectory, _settingsModName, group, current.ToArray() ); - } + _lastSettingsError = Ipc.TrySetModSettings.Subscriber(_pi).Invoke(_settingsCollection, + _settingsModDirectory, _settingsModName, group, current.ToArray()); } ImGui.SameLine(); - ImGui.TextUnformatted( group ); + ImGui.TextUnformatted(group); } } - private void UpdateLastModSetting( ModSettingChange type, string collection, string mod, bool inherited ) + private void UpdateLastModSetting(ModSettingChange type, string collection, string mod, bool inherited) { _lastSettingChangeType = type; _lastSettingChangeCollection = collection; @@ -1278,10 +1138,14 @@ public class IpcTester : IDisposable private class Temporary { - public readonly DalamudPluginInterface _pi; + private readonly DalamudPluginInterface _pi; + private readonly Mod.Manager _modManager; - public Temporary( DalamudPluginInterface pi ) - => _pi = pi; + public Temporary(DalamudPluginInterface pi, Mod.Manager modManager) + { + _pi = pi; + _modManager = modManager; + } public string LastCreatedCollectionName = string.Empty; @@ -1297,182 +1161,152 @@ public class IpcTester : IDisposable public void Draw() { - using var _ = ImRaii.TreeNode( "Temporary" ); - if( !_ ) - { + using var _ = ImRaii.TreeNode("Temporary"); + if (!_) return; - } - ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 ); - ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 ); - ImGui.InputInt( "##tempActorIndex", ref _tempActorIndex, 0, 0 ); - ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 ); - ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 ); - ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 ); - ImGui.InputTextWithHint( "##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256 ); - ImGui.Checkbox( "Force Character Collection Overwrite", ref _forceOverwrite ); + ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); + ImGui.InputTextWithHint("##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32); + ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); + ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) return; - } - DrawIntro( "Last Error", _lastTempError.ToString() ); - DrawIntro( "Last Created Collection", LastCreatedCollectionName ); - DrawIntro( Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection" ); + DrawIntro("Last Error", _lastTempError.ToString()); + DrawIntro("Last Created Collection", LastCreatedCollectionName); + DrawIntro(Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection"); #pragma warning disable 0612 - if( ImGui.Button( "Create##Collection" ) ) - { - ( _lastTempError, LastCreatedCollectionName ) = Ipc.CreateTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempCharacterName, _forceOverwrite ); - } + if (ImGui.Button("Create##Collection")) + (_lastTempError, LastCreatedCollectionName) = Ipc.CreateTemporaryCollection.Subscriber(_pi) + .Invoke(_tempCollectionName, _tempCharacterName, _forceOverwrite); - DrawIntro( Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection" ); - if( ImGui.Button( "Create##NamedCollection" ) ) - { - _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName ); - } + DrawIntro(Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection"); + if (ImGui.Button("Create##NamedCollection")) + _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName); - DrawIntro( Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character" ); - if( ImGui.Button( "Delete##Collection" ) ) - { - _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber( _pi ).Invoke( _tempCharacterName ); - } + DrawIntro(Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character"); + if (ImGui.Button("Delete##Collection")) + _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber(_pi).Invoke(_tempCharacterName); #pragma warning restore 0612 - DrawIntro( Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection" ); - if( ImGui.Button( "Delete##NamedCollection" ) ) + DrawIntro(Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection"); + if (ImGui.Button("Delete##NamedCollection")) + _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber(_pi).Invoke(_tempCollectionName); + + DrawIntro(Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection"); + if (ImGui.Button("Assign##NamedCollection")) + _lastTempError = Ipc.AssignTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName, _tempActorIndex, _forceOverwrite); + + DrawIntro(Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection"); + if (ImGui.Button("Add##Mod")) + _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); + + DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); + if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, + "Copies the effective list from the collection named in Temporary Mod Name...", + !Penumbra.CollectionManager.ByName(_tempModName, out var copyCollection)) + && copyCollection is { HasCache: true }) { - _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber( _pi ).Invoke( _tempCollectionName ); + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), + MetaManipulation.CurrentVersion); + _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, files, manips, 999); } - DrawIntro( Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection" ); - if( ImGui.Button( "Assign##NamedCollection" ) ) - { - _lastTempError = Ipc.AssignTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempActorIndex, _forceOverwrite ); - } + DrawIntro(Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections"); + if (ImGui.Button("Add##All")) + _lastTempError = Ipc.AddTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); - DrawIntro( Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection" ); - if( ImGui.Button( "Add##Mod" ) ) - { - _lastTempError = Ipc.AddTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, - new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); - } + DrawIntro(Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection"); + if (ImGui.Button("Remove##Mod")) + _lastTempError = Ipc.RemoveTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, int.MaxValue); - DrawIntro( Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection" ); - if( ImGuiUtil.DrawDisabledButton( "Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...", - !Penumbra.CollectionManager.ByName( _tempModName, out var copyCollection ) ) - && copyCollection is { HasCache: true } ) - { - var files = copyCollection.ResolvedFiles.ToDictionary( kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString() ); - var manips = Functions.ToCompressedBase64( copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(), - MetaManipulation.CurrentVersion ); - _lastTempError = Ipc.AddTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, files, manips, 999 ); - } - - DrawIntro( Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections" ); - if( ImGui.Button( "Add##All" ) ) - { - _lastTempError = Ipc.AddTemporaryModAll.Subscriber( _pi ).Invoke( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); - } - - DrawIntro( Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection" ); - if( ImGui.Button( "Remove##Mod" ) ) - { - _lastTempError = Ipc.RemoveTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, int.MaxValue ); - } - - DrawIntro( Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections" ); - if( ImGui.Button( "Remove##ModAll" ) ) - { - _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber( _pi ).Invoke( _tempModName, int.MaxValue ); - } + DrawIntro(Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); + if (ImGui.Button("Remove##ModAll")) + _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, int.MaxValue); } public void DrawCollections() { - using var collTree = ImRaii.TreeNode( "Collections##TempCollections" ); - if( !collTree ) - { + using var collTree = ImRaii.TreeNode("Collections##TempCollections"); + if (!collTree) return; - } - using var table = ImRaii.Table( "##collTree", 5 ); - if( !table ) - { + using var table = ImRaii.Table("##collTree", 5); + if (!table) return; - } - foreach( var collection in Penumbra.TempCollections.Values ) + foreach (var collection in Penumbra.TempCollections.Values) { ImGui.TableNextColumn(); - var character = Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; - if( ImGui.Button( $"Save##{collection.Name}" ) ) - { - Mod.TemporaryMod.SaveTempCollection( collection, character ); - } + var character = Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) + .FirstOrDefault() + ?? "Unknown"; + if (ImGui.Button($"Save##{collection.Name}")) + Mod.TemporaryMod.SaveTempCollection(_modManager, collection, character); - ImGuiUtil.DrawTableColumn( collection.Name ); - ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() ); - ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" ); - ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); + ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); + ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); + ImGuiUtil.DrawTableColumn(string.Join(", ", + Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); } } public void DrawMods() { - using var modTree = ImRaii.TreeNode( "Mods##TempMods" ); - if( !modTree ) - { + using var modTree = ImRaii.TreeNode("Mods##TempMods"); + if (!modTree) return; - } - using var table = ImRaii.Table( "##modTree", 5 ); + using var table = ImRaii.Table("##modTree", 5); - void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) + void PrintList(string collectionName, IReadOnlyList list) { - foreach( var mod in list ) + foreach (var mod in list) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Name ); + ImGui.TextUnformatted(mod.Name); ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TextUnformatted(mod.Priority.ToString()); ImGui.TableNextColumn(); - ImGui.TextUnformatted( collectionName ); + ImGui.TextUnformatted(collectionName); ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); - if( ImGui.IsItemHovered() ) + ImGui.TextUnformatted(mod.Default.Files.Count.ToString()); + if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - foreach( var (path, file) in mod.Default.Files ) - { - ImGui.TextUnformatted( $"{path} -> {file}" ); - } + foreach (var (path, file) in mod.Default.Files) + ImGui.TextUnformatted($"{path} -> {file}"); } ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); - if( ImGui.IsItemHovered() ) + ImGui.TextUnformatted(mod.TotalManipulations.ToString()); + if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - foreach( var manip in mod.Default.Manipulations ) - { - ImGui.TextUnformatted( manip.ToString() ); - } + foreach (var manip in mod.Default.Manipulations) + ImGui.TextUnformatted(manip.ToString()); } } } - if( table ) + if (table) { - PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); - foreach( var (collection, list) in Penumbra.TempMods.Mods ) - { - PrintList( collection.Name, list ); - } + PrintList("All", Penumbra.TempMods.ModsForAllCollections); + foreach (var (collection, list) in Penumbra.TempMods.Mods) + PrintList(collection.Name, list); } } } -} \ No newline at end of file +} diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 90d316d0..50768ddf 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; +using Penumbra.Mods; namespace Penumbra.Api; @@ -111,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; - public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api ) + public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager ) { Api = api; @@ -219,7 +220,7 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); - Tester = new IpcTester( pi, this ); + Tester = new IpcTester( pi, this, modManager ); Initialized.Invoke(); } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 61a37961..1d571468 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -17,7 +17,7 @@ namespace Penumbra.Collections; public partial class ModCollection { - public sealed partial class Manager : ISaveable + public sealed partial class Manager : ISavable { public const int Version = 1; diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 6d13deb1..c2516519 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -11,7 +11,7 @@ using Penumbra.Util; namespace Penumbra.Collections; // File operations like saving, loading and deleting for a collection. -public partial class ModCollection : ISaveable +public partial class ModCollection : ISavable { // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b1468c2c..ff2a5e64 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -9,23 +9,21 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.GameData.Enums; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.Util; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public class Configuration : IPluginConfiguration +public class Configuration : IPluginConfiguration, ISavable { [JsonIgnore] - private readonly string _fileName; - - [JsonIgnore] - private readonly FrameworkManager _framework; + private readonly SaveService _saveService; public int Version { get; set; } = Constants.CurrentVersion; @@ -101,14 +99,13 @@ public class Configuration : IPluginConfiguration /// Load the current configuration. /// Includes adding new colors and migrating from old versions. /// - public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework) + public Configuration(FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService) { - _fileName = fileNames.ConfigFile; - _framework = framework; - Load(migrator); + _saveService = saveService; + Load(fileNames, migrator); } - public void Load(ConfigMigrationService migrator) + public void Load(FilenameService fileNames, ConfigMigrationService migrator) { static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { @@ -117,9 +114,9 @@ public class Configuration : IPluginConfiguration errorArgs.ErrorContext.Handled = true; } - if (File.Exists(_fileName)) + if (File.Exists(fileNames.ConfigFile)) { - var text = File.ReadAllText(_fileName); + var text = File.ReadAllText(fileNames.ConfigFile); JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, @@ -130,21 +127,8 @@ public class Configuration : IPluginConfiguration } /// Save the current configuration. - private void SaveConfiguration() - { - try - { - var text = JsonConvert.SerializeObject(this, Formatting.Indented); - File.WriteAllText(_fileName, text); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not save plugin configuration:\n{e}"); - } - } - public void Save() - => _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration); + => _saveService.QueueSave(this); /// Contains some default values or boundaries for config values. public static class Constants @@ -192,4 +176,14 @@ public class Configuration : IPluginConfiguration return mode; } } + + public string ToFilename(FilenameService fileNames) + => fileNames.ConfigFile; + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } } diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 548cf90a..974c8b2e 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; +using Penumbra.Api; using Penumbra.Import.Structs; using Penumbra.Mods; using FileMode = System.IO.FileMode; @@ -33,22 +34,19 @@ public partial class TexToolsImporter : IDisposable public ImporterState State { get; private set; } public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; - public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files, - Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor) - : this( baseDirectory, files.Count, files, handler, config, editor) - { } - private readonly Configuration _config; - private readonly ModEditor _editor; + private readonly ModEditor _editor; + private readonly Mod.Manager _modManager; public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, - Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor) + Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager) { _baseDirectory = baseDirectory; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _modPackFiles = modPackFiles; _config = config; - _editor = editor; + _editor = editor; + _modManager = modManager; _modPackCount = count; ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); _token = _cancellation.Token; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index fd141ef3..a8cb6608 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -35,7 +35,7 @@ public partial class TexToolsImporter _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); // Create a new ModMeta from the TTMP mod list info - Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); + _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); @@ -90,7 +90,7 @@ public partial class TexToolsImporter Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); - Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) + _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url ); @@ -135,7 +135,7 @@ public partial class TexToolsImporter _currentModName = modList.Name; _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); - Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); + _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); if( _currentNumOptions == 0 ) { diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 44197193..8f480851 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -243,7 +243,7 @@ public class DuplicateManager try { var mod = new Mod(modDirectory); - mod.Reload(true, out _); + mod.Reload(_modManager, true, out _); Finished = false; _files.UpdateAll(mod, mod.Default); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 2e46314c..5215973c 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -154,7 +154,7 @@ public class ModFileEditor if (deletions <= 0) return; - mod.Reload(false, out _); + mod.Reload(_modManager, false, out _); _files.UpdateAll(mod, option); } diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 4b93bd24..65e3e10f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -8,20 +8,20 @@ public partial class Mod { public partial class Manager { - public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ); + public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory); public event ModPathChangeDelegate ModPathChanged; // Rename/Move a mod directory. // Updates all collection settings and sort order settings. - public void MoveModDirectory( int idx, string newName ) + public void MoveModDirectory(int idx, string newName) { - var mod = this[ idx ]; + var mod = this[idx]; var oldName = mod.Name; var oldDirectory = mod.ModPath; - switch( NewDirectoryValid( oldDirectory.Name, newName, out var dir ) ) + switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) { case NewDirectoryState.NonExisting: // Nothing to do @@ -29,11 +29,11 @@ public partial class Mod case NewDirectoryState.ExistsEmpty: try { - Directory.Delete( dir!.FullName ); + Directory.Delete(dir!.FullName); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" ); + Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); return; } @@ -50,105 +50,97 @@ public partial class Mod try { - Directory.Move( oldDirectory.FullName, dir!.FullName ); + Directory.Move(oldDirectory.FullName, dir!.FullName); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" ); + Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); return; } - MoveDataFile( oldDirectory, dir ); - new ModBackup( this, mod ).Move( null, dir.Name ); + DataEditor.MoveDataFile(oldDirectory, dir); + new ModBackup(this, mod).Move(null, dir.Name); dir.Refresh(); mod.ModPath = dir; - if( !mod.Reload( false, out var metaChange ) ) + if (!mod.Reload(this, false, out var metaChange)) { - Penumbra.Log.Error( $"Error reloading moved mod {mod.Name}." ); + Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); return; } - ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, dir ); - if( metaChange != ModDataChangeType.None ) - { - ModDataChanged?.Invoke( metaChange, mod, oldName ); - } + ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); } - // Reload a mod without changing its base directory. - // If the base directory does not exist anymore, the mod will be deleted. - public void ReloadMod( int idx ) + /// + /// Reload a mod without changing its base directory. + /// If the base directory does not exist anymore, the mod will be deleted. + /// + public void ReloadMod(int idx) { - var mod = this[ idx ]; + var mod = this[idx]; var oldName = mod.Name; - ModPathChanged.Invoke( ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); - if( !mod.Reload( true, out var metaChange ) ) + ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); + if (!mod.Reload(this, true, out var metaChange)) { - Penumbra.Log.Warning( mod.Name.Length == 0 + Penumbra.Log.Warning(mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead." ); + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - DeleteMod( idx ); + DeleteMod(idx); return; } - ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); - if( metaChange != ModDataChangeType.None ) - { - ModDataChanged?.Invoke( metaChange, mod, oldName ); - } + ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); } - // Delete a mod by its index. The event is invoked before the mod is removed from the list. - // Deletes from filesystem as well as from internal data. - // Updates indices of later mods. - public void DeleteMod( int idx ) + /// + /// Delete a mod by its index. The event is invoked before the mod is removed from the list. + /// Deletes from filesystem as well as from internal data. + /// Updates indices of later mods. + /// + public void DeleteMod(int idx) { - var mod = this[ idx ]; - if( Directory.Exists( mod.ModPath.FullName ) ) - { + var mod = this[idx]; + if (Directory.Exists(mod.ModPath.FullName)) try { - Directory.Delete( mod.ModPath.FullName, true ); - Penumbra.Log.Debug( $"Deleted directory {mod.ModPath.FullName} for {mod.Name}." ); + Directory.Delete(mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" ); + Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); } - } - ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null ); - _mods.RemoveAt( idx ); - foreach( var remainingMod in _mods.Skip( idx ) ) - { + ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); + _mods.RemoveAt(idx); + foreach (var remainingMod in _mods.Skip(idx)) --remainingMod.Index; - } - Penumbra.Log.Debug( $"Deleted mod {mod.Name}." ); + Penumbra.Log.Debug($"Deleted mod {mod.Name}."); } - // Load a new mod and add it to the manager if successful. - public void AddMod( DirectoryInfo modFolder ) + /// Load a new mod and add it to the manager if successful. + public void AddMod(DirectoryInfo modFolder) { - if( _mods.Any( m => m.ModPath.Name == modFolder.Name ) ) - { + if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) return; - } - Creator.SplitMultiGroups( modFolder ); - var mod = LoadMod( modFolder, true ); - if( mod == null ) - { + Creator.SplitMultiGroups(modFolder); + var mod = LoadMod(this, modFolder, true); + if (mod == null) return; - } mod.Index = _mods.Count; - _mods.Add( mod ); - ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.ModPath ); - Penumbra.Log.Debug( $"Added new mod {mod.Name} from {modFolder.FullName}." ); + _mods.Add(mod); + ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); + Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); } public enum NewDirectoryState @@ -162,66 +154,52 @@ public partial class Mod Empty, } - // Return the state of the new potential name of a directory. - public NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) + /// Return the state of the new potential name of a directory. + public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) { directory = null; - if( newName.Length == 0 ) - { + if (newName.Length == 0) return NewDirectoryState.Empty; - } - if( oldName == newName ) - { + if (oldName == newName) return NewDirectoryState.Identical; - } - var fixedNewName = Creator.ReplaceBadXivSymbols( newName ); - if( fixedNewName != newName ) - { + var fixedNewName = Creator.ReplaceBadXivSymbols(newName); + if (fixedNewName != newName) return NewDirectoryState.ContainsInvalidSymbols; - } - directory = new DirectoryInfo( Path.Combine( BasePath.FullName, fixedNewName ) ); - if( File.Exists( directory.FullName ) ) - { + directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); + if (File.Exists(directory.FullName)) return NewDirectoryState.ExistsAsFile; - } - if( !Directory.Exists( directory.FullName ) ) - { + if (!Directory.Exists(directory.FullName)) return NewDirectoryState.NonExisting; - } - if( directory.EnumerateFileSystemInfos().Any() ) - { + if (directory.EnumerateFileSystemInfos().Any()) return NewDirectoryState.ExistsNonEmpty; - } return NewDirectoryState.ExistsEmpty; } - // Add new mods to NewMods and remove deleted mods from NewMods. - private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory ) + /// Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) { - switch( type ) + switch (type) { case ModPathChangeType.Added: - NewMods.Add( mod ); + NewMods.Add(mod); break; case ModPathChangeType.Deleted: - NewMods.Remove( mod ); + NewMods.Remove(mod); break; case ModPathChangeType.Moved: - if( oldDirectory != null && newDirectory != null ) - { - MoveDataFile( oldDirectory, newDirectory ); - } + if (oldDirectory != null && newDirectory != null) + DataEditor.MoveDataFile(oldDirectory, newDirectory); break; } } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs index 9b2fc839..f838677f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Local.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Local.cs @@ -7,67 +7,6 @@ public sealed partial class Mod { public partial class Manager { - public void ChangeModFavorite( Index idx, bool state ) - { - var mod = this[ idx ]; - if( mod.Favorite != state ) - { - mod.Favorite = state; - mod.SaveLocalData(); - ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); - } - } - - public void ChangeModNote( Index idx, string newNote ) - { - var mod = this[ idx ]; - if( mod.Note != newNote ) - { - mod.Note = newNote; - mod.SaveLocalData(); - ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); - } - } - - - private void ChangeTag( Index idx, int tagIdx, string newTag, bool local ) - { - var mod = this[ idx ]; - var which = local ? mod.LocalTags : mod.ModTags; - if( tagIdx < 0 || tagIdx > which.Count ) - { - return; - } - - ModDataChangeType flags = 0; - if( tagIdx == which.Count ) - { - flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null ); - } - else - { - var tmp = which.ToArray(); - tmp[ tagIdx ] = newTag; - flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null ); - } - - if( flags.HasFlag( ModDataChangeType.ModTags ) ) - { - mod.SaveMeta(); - } - - if( flags.HasFlag( ModDataChangeType.LocalTags ) ) - { - mod.SaveLocalData(); - } - - if( flags != 0 ) - { - ModDataChanged?.Invoke( flags, mod, null ); - } - } - - public void ChangeLocalTag( Index idx, int tagIdx, string newTag ) - => ChangeTag( idx, tagIdx, newTag, true ); + } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs deleted file mode 100644 index 9c4d48ee..00000000 --- a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public partial class Manager - { - public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName ); - public event ModDataChangeDelegate? ModDataChanged; - - public void ChangeModName( Index idx, string newName ) - { - var mod = this[ idx ]; - if( mod.Name.Text != newName ) - { - var oldName = mod.Name; - mod.Name = newName; - mod.SaveMeta(); - ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text ); - } - } - - public void ChangeModAuthor( Index idx, string newAuthor ) - { - var mod = this[ idx ]; - if( mod.Author != newAuthor ) - { - mod.Author = newAuthor; - mod.SaveMeta(); - ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null ); - } - } - - public void ChangeModDescription( Index idx, string newDescription ) - { - var mod = this[ idx ]; - if( mod.Description != newDescription ) - { - mod.Description = newDescription; - mod.SaveMeta(); - ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null ); - } - } - - public void ChangeModVersion( Index idx, string newVersion ) - { - var mod = this[ idx ]; - if( mod.Version != newVersion ) - { - mod.Version = newVersion; - mod.SaveMeta(); - ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null ); - } - } - - public void ChangeModWebsite( Index idx, string newWebsite ) - { - var mod = this[ idx ]; - if( mod.Website != newWebsite ) - { - mod.Website = newWebsite; - mod.SaveMeta(); - ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null ); - } - } - - public void ChangeModTag( Index idx, int tagIdx, string newTag ) - => ChangeTag( idx, tagIdx, newTag, false ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index fbc30e64..a9b11b26 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -305,18 +305,18 @@ public sealed partial class Mod public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) { var path = newName.RemoveInvalidPathSymbols(); - if (path.Length == 0 - || mod.Groups.Any(o => !ReferenceEquals(o, group) + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - { - if (message) - _chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); + return true; - return false; - } + if (message) + Penumbra.ChatService.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); + + return false; - return true; } private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index bd7fb0fd..79dd780b 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -97,7 +97,7 @@ public sealed partial class Mod var queue = new ConcurrentQueue< Mod >(); Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir => { - var mod = LoadMod( dir, false ); + var mod = LoadMod( this, dir, false ); if( mod != null ) { queue.Enqueue( mod ); diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 95f40592..0e47f3ae 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Mods; @@ -36,14 +37,16 @@ public sealed partial class Mod IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private readonly Configuration _config; - private readonly ChatService _chat; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + public readonly ModDataEditor DataEditor; - public Manager(StartTracker time, Configuration config, ChatService chat) + public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) { using var timer = time.Measure(StartTimeType.Mods); _config = config; - _chat = chat; + _communicator = communicator; + DataEditor = dataEditor; ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory(config.ModDirectory, true); UpdateExportDirectory(_config.ExportDirectory, false); diff --git a/Penumbra/Mods/Manager/ModDataChangeType.cs b/Penumbra/Mods/Manager/ModDataChangeType.cs new file mode 100644 index 00000000..eccf83cb --- /dev/null +++ b/Penumbra/Mods/Manager/ModDataChangeType.cs @@ -0,0 +1,21 @@ +using System; + +namespace Penumbra.Mods; + +[Flags] +public enum ModDataChangeType : ushort +{ + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, +} diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs new file mode 100644 index 00000000..7b74f83a --- /dev/null +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -0,0 +1,362 @@ +using System; +using System.IO; +using System.Linq; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public class ModDataEditor +{ + private readonly FilenameService _filenameService; + private readonly SaveService _saveService; + private readonly CommunicatorService _communicatorService; + + public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService) + { + _filenameService = filenameService; + _saveService = saveService; + _communicatorService = communicatorService; + } + + public string MetaFile(Mod mod) + => _filenameService.ModMetaPath(mod); + + public string DataFile(Mod mod) + => _filenameService.LocalDataFile(mod); + + /// Create the file containing the meta information about a mod from scratch. + public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, + string? website) + { + var mod = new Mod(directory); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!); + mod.Author = author != null ? new LowerString(author) : mod.Author; + mod.Description = description ?? mod.Description; + mod.Version = version ?? mod.Version; + mod.Website = website ?? mod.Website; + _saveService.ImmediateSave(new ModMeta(mod)); + } + + public ModDataChangeType LoadLocalData(Mod mod) + { + var dataFile = _filenameService.LocalDataFile(mod); + + var importDate = 0L; + var localTags = Enumerable.Empty(); + var favorite = false; + var note = string.Empty; + + var save = true; + if (File.Exists(dataFile)) + { + save = false; + try + { + var text = File.ReadAllText(dataFile); + var json = JObject.Parse(text); + + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = json[nameof(Mod.LocalTags)]?.Values().OfType() ?? localTags; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load local mod data:\n{e}"); + } + } + + if (importDate == 0) + importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + ModDataChangeType changes = 0; + if (mod.ImportDate != importDate) + { + mod.ImportDate = importDate; + changes |= ModDataChangeType.ImportDate; + } + + changes |= mod.UpdateTags(null, localTags); + + if (mod.Favorite != favorite) + { + mod.Favorite = favorite; + changes |= ModDataChangeType.Favorite; + } + + if (mod.Note != note) + { + mod.Note = note; + changes |= ModDataChangeType.Note; + } + + if (save) + _saveService.QueueSave(new ModData(mod)); + + return changes; + } + + public ModDataChangeType LoadMeta(Mod mod) + { + var metaFile = _filenameService.ModMetaPath(mod); + if (!File.Exists(metaFile)) + { + Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); + return ModDataChangeType.Deletion; + } + + try + { + var text = File.ReadAllText(metaFile); + var json = JObject.Parse(text); + + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; + var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; + var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; + var newFileVersion = json[nameof(Mod.FileVersion)]?.Value() ?? 0; + var importDate = json[nameof(Mod.ImportDate)]?.Value(); + var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); + + ModDataChangeType changes = 0; + if (mod.Name != newName) + { + changes |= ModDataChangeType.Name; + mod.Name = newName; + } + + if (mod.Author != newAuthor) + { + changes |= ModDataChangeType.Author; + mod.Author = newAuthor; + } + + if (mod.Description != newDescription) + { + changes |= ModDataChangeType.Description; + mod.Description = newDescription; + } + + if (mod.Version != newVersion) + { + changes |= ModDataChangeType.Version; + mod.Version = newVersion; + } + + if (mod.Website != newWebsite) + { + changes |= ModDataChangeType.Website; + mod.Website = newWebsite; + } + + if (mod.FileVersion != newFileVersion) + { + mod.FileVersion = newFileVersion; + if (Mod.Migration.Migrate(mod, json)) + { + changes |= ModDataChangeType.Migration; + _saveService.ImmediateSave(new ModMeta(mod)); + } + } + + if (importDate != null && mod.ImportDate != importDate.Value) + { + mod.ImportDate = importDate.Value; + changes |= ModDataChangeType.ImportDate; + } + + changes |= mod.UpdateTags(modTags, null); + + return changes; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load mod meta:\n{e}"); + return ModDataChangeType.Deletion; + } + } + + public void ChangeModName(Mod mod, string newName) + { + if (mod.Name.Text == newName) + return; + + var oldName = mod.Name; + mod.Name = newName; + _saveService.QueueSave(new ModMeta(mod)); + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); + } + + public void ChangeModAuthor(Mod mod, string newAuthor) + { + if (mod.Author == newAuthor) + return; + + mod.Author = newAuthor; + _saveService.QueueSave(new ModMeta(mod)); + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); + } + + public void ChangeModDescription(Mod mod, string newDescription) + { + if (mod.Description == newDescription) + return; + + mod.Description = newDescription; + _saveService.QueueSave(new ModMeta(mod)); + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); + } + + public void ChangeModVersion(Mod mod, string newVersion) + { + if (mod.Version == newVersion) + return; + + mod.Version = newVersion; + _saveService.QueueSave(new ModMeta(mod)); + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); + } + + public void ChangeModWebsite(Mod mod, string newWebsite) + { + if (mod.Website == newWebsite) + return; + + mod.Website = newWebsite; + _saveService.QueueSave(new ModMeta(mod)); + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); + } + + public void ChangeModTag(Mod mod, int tagIdx, string newTag) + => ChangeTag(mod, tagIdx, newTag, false); + + public void ChangeLocalTag(Mod mod, int tagIdx, string newTag) + => ChangeTag(mod, tagIdx, newTag, true); + + public void ChangeModFavorite(Mod mod, bool state) + { + if (mod.Favorite == state) + return; + + mod.Favorite = state; + _saveService.QueueSave(new ModData(mod)); + ; + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); + } + + public void ChangeModNote(Mod mod, string newNote) + { + if (mod.Note == newNote) + return; + + mod.Note = newNote; + _saveService.QueueSave(new ModData(mod)); + ; + _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); + } + + + private void ChangeTag(Mod mod, int tagIdx, string newTag, bool local) + { + var which = local ? mod.LocalTags : mod.ModTags; + if (tagIdx < 0 || tagIdx > which.Count) + return; + + ModDataChangeType flags = 0; + if (tagIdx == which.Count) + { + flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null); + } + else + { + var tmp = which.ToArray(); + tmp[tagIdx] = newTag; + flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null); + } + + if (flags.HasFlag(ModDataChangeType.ModTags)) + _saveService.QueueSave(new ModMeta(mod)); + + if (flags.HasFlag(ModDataChangeType.LocalTags)) + _saveService.QueueSave(new ModData(mod)); + + if (flags != 0) + _communicatorService.ModDataChanged.Invoke(flags, mod, null); + } + + public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod) + { + var oldFile = _filenameService.LocalDataFile(oldMod.Name); + var newFile = _filenameService.LocalDataFile(newMod.Name); + if (!File.Exists(oldFile)) + return; + + try + { + File.Move(oldFile, newFile, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}"); + } + } + + + private readonly struct ModMeta : ISavable + { + private readonly Mod _mod; + + public ModMeta(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.ModMetaPath(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, + { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, + { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, + { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, + { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, + { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + } + + private readonly struct ModData : ISavable + { + private readonly Mod _mod; + + public ModData(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.LocalDataFile(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, + { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, + { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, + { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + } +} diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 8423d023..c7367a68 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -16,6 +16,8 @@ public enum ModPathChangeType public partial class Mod { public DirectoryInfo ModPath { get; private set; } + public string Identifier + => Index >= 0 ? ModPath.Name : Name; public int Index { get; private set; } = -1; public bool IsTemporary @@ -31,7 +33,7 @@ public partial class Mod _default = new SubMod( this ); } - private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges ) + private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) { modPath.Refresh(); if( !modPath.Exists ) @@ -40,18 +42,17 @@ public partial class Mod return null; } - var mod = new Mod( modPath ); - if( !mod.Reload( incorporateMetaChanges, out _ ) ) - { - // Can not be base path not existing because that is checked before. - Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." ); - return null; - } + var mod = new Mod(modPath); + if (mod.Reload(modManager, incorporateMetaChanges, out _)) + return mod; + + // Can not be base path not existing because that is checked before. + Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." ); + return null; - return mod; } - internal bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) + internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); @@ -60,19 +61,19 @@ public partial class Mod return false; } - modDataChange = LoadMeta(); + modDataChange = modManager.DataEditor.LoadMeta(this); if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 ) { return false; } - LoadLocalData(); + modManager.DataEditor.LoadLocalData(this); LoadDefaultOption(); LoadAllGroups(); if( incorporateMetaChanges ) { - IncorporateAllMetaChanges( true ); + IncorporateAllMetaChanges(true); } ComputeChangedItems(); diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 0074cf77..f67dbf33 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -64,19 +64,6 @@ public partial class Mod return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); } - /// Create the file containing the meta information about a mod from scratch. - public static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, - string? website ) - { - var mod = new Mod( directory ); - mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! ); - mod.Author = author != null ? new LowerString( author ) : mod.Author; - mod.Description = description ?? mod.Description; - mod.Version = version ?? mod.Version; - mod.Website = website ?? mod.Website; - mod.SaveMetaFile(); // Not delayed. - } - /// Create a file for an option group from given data. public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) @@ -147,13 +134,11 @@ public partial class Mod internal static void CreateDefaultFiles( DirectoryInfo directory ) { var mod = new Mod( directory ); - mod.Reload( false, out _ ); + mod.Reload( Penumbra.ModManager, false, out _ ); foreach( var file in mod.FindUnusedFiles() ) { if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - { mod._default.FileData.TryAdd( gamePath, file ); - } } mod._default.IncorporateMetaChanges( directory, true ); diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index 57699fcb..29e11f3d 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -3,145 +3,30 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Plugin; -using Newtonsoft.Json; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Mods; public sealed partial class Mod { - public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi) - => new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" )); + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; private set; } = Array.Empty(); - public IReadOnlyList< string > LocalTags { get; private set; } = Array.Empty< string >(); + public string AllTagsLower { get; private set; } = string.Empty; + public string Note { get; internal set; } = string.Empty; + public bool Favorite { get; internal set; } = false; - public string AllTagsLower { get; private set; } = string.Empty; - public string Note { get; private set; } = string.Empty; - public bool Favorite { get; private set; } = false; - - private FileInfo LocalDataFile - => new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" )); - - private ModDataChangeType LoadLocalData() + internal ModDataChangeType UpdateTags(IEnumerable? newModTags, IEnumerable? newLocalTags) { - var dataFile = LocalDataFile; - - var importDate = 0L; - var localTags = Enumerable.Empty< string >(); - var favorite = false; - var note = string.Empty; - - var save = true; - if( File.Exists( dataFile.FullName ) ) - { - save = false; - try - { - var text = File.ReadAllText( dataFile.FullName ); - var json = JObject.Parse( text ); - - importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate; - favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite; - note = json[ nameof( Note ) ]?.Value< string >() ?? note; - localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not load local mod data:\n{e}" ); - } - } - - if( importDate == 0 ) - { - importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - } - - ModDataChangeType changes = 0; - if( ImportDate != importDate ) - { - ImportDate = importDate; - changes |= ModDataChangeType.ImportDate; - } - - changes |= UpdateTags( null, localTags ); - - if( Favorite != favorite ) - { - Favorite = favorite; - changes |= ModDataChangeType.Favorite; - } - - if( Note != note ) - { - Note = note; - changes |= ModDataChangeType.Note; - } - - if( save ) - { - SaveLocalDataFile(); - } - - return changes; - } - - private void SaveLocalData() - => Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile ); - - private void SaveLocalDataFile() - { - var dataFile = LocalDataFile; - try - { - var jObject = new JObject - { - { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, - { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, - { nameof( LocalTags ), JToken.FromObject( LocalTags ) }, - { nameof( Note ), JToken.FromObject( Note ) }, - { nameof( Favorite ), JToken.FromObject( Favorite ) }, - }; - dataFile.Directory!.Create(); - File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" ); - } - } - - private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod ) - { - var oldFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" ); - var newFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" ); - if( File.Exists( oldFile ) ) - { - try - { - File.Move( oldFile, newFile, true ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" ); - } - } - } - - private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags ) - { - if( newModTags == null && newLocalTags == null ) - { + if (newModTags == null && newLocalTags == null) return 0; - } ModDataChangeType type = 0; - if( newModTags != null ) + if (newModTags != null) { - var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray(); - if( !modTags.SequenceEqual( ModTags ) ) + var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray(); + if (!modTags.SequenceEqual(ModTags)) { newLocalTags ??= LocalTags; ModTags = modTags; @@ -149,21 +34,19 @@ public sealed partial class Mod } } - if( newLocalTags != null ) + if (newLocalTags != null) { - var localTags = newLocalTags!.Where( t => t.Length > 0 && !ModTags.Contains( t ) ).Distinct().ToArray(); - if( !localTags.SequenceEqual( LocalTags ) ) + var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray(); + if (!localTags.SequenceEqual(LocalTags)) { LocalTags = localTags; type |= ModDataChangeType.LocalTags; } } - if( type != 0 ) - { - AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) ); - } + if (type != 0) + AllTagsLower = string.Join('\0', ModTags.Concat(LocalTags).Select(s => s.ToLowerInvariant())); return type; } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index fa16f4c2..64501009 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -13,146 +13,115 @@ namespace Penumbra.Mods; public sealed partial class Mod { - private static class Migration + public static partial class Migration { - public static bool Migrate( Mod mod, JObject json ) - { - var ret = MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod ); - if( ret ) - { - // Immediately save on migration. - mod.SaveMetaFile(); - } + public static bool Migrate(Mod mod, JObject json) + => MigrateV0ToV1(mod, json) || MigrateV1ToV2(mod) || MigrateV2ToV3(mod); - return ret; - } - - private static bool MigrateV2ToV3( Mod mod ) + private static bool MigrateV2ToV3(Mod mod) { - if( mod.FileVersion > 2 ) - { + if (mod.FileVersion > 2) return false; - } // Remove import time. mod.FileVersion = 3; return true; } + [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + private static partial Regex GroupRegex(); - private static readonly Regex GroupRegex = new( @"group_\d{3}_", RegexOptions.Compiled ); - private static bool MigrateV1ToV2( Mod mod ) + private static bool MigrateV1ToV2(Mod mod) { - if( mod.FileVersion > 1 ) - { + if (mod.FileVersion > 1) return false; - } - if (!mod.GroupFiles.All( g => GroupRegex.IsMatch( g.Name ))) - { - foreach( var (group, index) in mod.GroupFiles.WithIndex().ToArray() ) + if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) + foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) { - var newName = Regex.Replace( group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled ); + var newName = Regex.Replace(group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled); try { - if( newName != group.Name ) - { - group.MoveTo( Path.Combine( group.DirectoryName ?? string.Empty, newName ), false ); - } + if (newName != group.Name) + group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); + Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}"); } } - } mod.FileVersion = 2; return true; } - private static bool MigrateV0ToV1( Mod mod, JObject json ) + private static bool MigrateV0ToV1(Mod mod, JObject json) { - if( mod.FileVersion > 0 ) - { + if (mod.FileVersion > 0) return false; - } - var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() - ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var swaps = json["FileSwaps"]?.ToObject>() + ?? new Dictionary(); + var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); var priority = 1; - var seenMetaFiles = new HashSet< FullPath >(); - foreach( var group in groups.Values ) - { - ConvertGroup( mod, group, ref priority, seenMetaFiles ); - } + var seenMetaFiles = new HashSet(); + foreach (var group in groups.Values) + ConvertGroup(mod, group, ref priority, seenMetaFiles); - foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) ) + foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) { - if( unusedFile.ToGamePath( mod.ModPath, out var gamePath ) - && !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) - { - Penumbra.Log.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); - } + if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) + && !mod._default.FileData.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); } mod._default.FileSwapData.Clear(); - mod._default.FileSwapData.EnsureCapacity( swaps.Count ); - foreach( var (gamePath, swapPath) in swaps ) - { - mod._default.FileSwapData.Add( gamePath, swapPath ); - } + mod._default.FileSwapData.EnsureCapacity(swaps.Count); + foreach (var (gamePath, swapPath) in swaps) + mod._default.FileSwapData.Add(gamePath, swapPath); - mod._default.IncorporateMetaChanges( mod.ModPath, true ); - foreach( var (group, index) in mod.Groups.WithIndex() ) - { - IModGroup.Save( group, mod.ModPath, index ); - } + mod._default.IncorporateMetaChanges(mod.ModPath, true); + foreach (var (group, index) in mod.Groups.WithIndex()) + IModGroup.Save(group, mod.ModPath, index); // Delete meta files. - foreach( var file in seenMetaFiles.Where( f => f.Exists ) ) + foreach (var file in seenMetaFiles.Where(f => f.Exists)) { try { - File.Delete( file.FullName ); + File.Delete(file.FullName); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Warning( $"Could not delete meta file {file.FullName} during migration:\n{e}" ); + Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}"); } } // Delete old meta files. - var oldMetaFile = Path.Combine( mod.ModPath.FullName, "metadata_manipulations.json" ); - if( File.Exists( oldMetaFile ) ) - { + var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json"); + if (File.Exists(oldMetaFile)) try { - File.Delete( oldMetaFile ); + File.Delete(oldMetaFile); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Warning( $"Could not delete old meta file {oldMetaFile} during migration:\n{e}" ); + Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); } - } mod.FileVersion = 1; mod.SaveDefaultMod(); - mod.SaveMetaFile(); return true; } - private static void ConvertGroup( Mod mod, OptionGroupV0 group, ref int priority, HashSet< FullPath > seenMetaFiles ) + private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) { - if( group.Options.Count == 0 ) - { + if (group.Options.Count == 0) return; - } - switch( group.SelectionType ) + switch (group.SelectionType) { case GroupType.Multi: @@ -163,17 +132,15 @@ public sealed partial class Mod Priority = priority++, Description = string.Empty, }; - mod._groups.Add( newMultiGroup ); - foreach( var option in group.Options ) - { - newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod, option, seenMetaFiles ), optionPriority++ ) ); - } + mod._groups.Add(newMultiGroup); + foreach (var option in group.Options) + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); break; case GroupType.Single: - if( group.Options.Count == 1 ) + if (group.Options.Count == 1) { - AddFilesToSubMod( mod._default, mod.ModPath, group.Options[ 0 ], seenMetaFiles ); + AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); return; } @@ -183,38 +150,32 @@ public sealed partial class Mod Priority = priority++, Description = string.Empty, }; - mod._groups.Add( newSingleGroup ); - foreach( var option in group.Options ) - { - newSingleGroup.OptionData.Add( SubModFromOption( mod, option, seenMetaFiles ) ); - } + mod._groups.Add(newSingleGroup); + foreach (var option in group.Options) + newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); break; } } - private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles ) + private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) { - foreach( var (relPath, gamePaths) in option.OptionFiles ) + foreach (var (relPath, gamePaths) in option.OptionFiles) { - var fullPath = new FullPath( basePath, relPath ); - foreach( var gamePath in gamePaths ) - { - mod.FileData.TryAdd( gamePath, fullPath ); - } + var fullPath = new FullPath(basePath, relPath); + foreach (var gamePath in gamePaths) + mod.FileData.TryAdd(gamePath, fullPath); - if( fullPath.Extension is ".meta" or ".rgsp" ) - { - seenMetaFiles.Add( fullPath ); - } + if (fullPath.Extension is ".meta" or ".rgsp") + seenMetaFiles.Add(fullPath); } } - private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) + private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SubMod( mod ) { Name = option.OptionName }; - AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); - subMod.IncorporateMetaChanges( mod.ModPath, false ); + var subMod = new SubMod(mod) { Name = option.OptionName }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + subMod.IncorporateMetaChanges(mod.ModPath, false); return subMod; } @@ -223,8 +184,8 @@ public sealed partial class Mod public string OptionName = string.Empty; public string OptionDesc = string.Empty; - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] - public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); + [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter))] + public Dictionary> OptionFiles = new(); public OptionV0() { } @@ -234,53 +195,49 @@ public sealed partial class Mod { public string GroupName = string.Empty; - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public GroupType SelectionType = GroupType.Single; - public List< OptionV0 > Options = new(); + public List Options = new(); public OptionGroupV0() { } } // Not used anymore, but required for migration. - private class SingleOrArrayConverter< T > : JsonConverter + private class SingleOrArrayConverter : JsonConverter { - public override bool CanConvert( Type objectType ) - => objectType == typeof( HashSet< T > ); + public override bool CanConvert(Type objectType) + => objectType == typeof(HashSet); - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - var token = JToken.Load( reader ); + var token = JToken.Load(reader); - if( token.Type == JTokenType.Array ) - { - return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); - } + if (token.Type == JTokenType.Array) + return token.ToObject>() ?? new HashSet(); - var tmp = token.ToObject< T >(); + var tmp = token.ToObject(); return tmp != null - ? new HashSet< T > { tmp } - : new HashSet< T >(); + ? new HashSet { tmp } + : new HashSet(); } public override bool CanWrite => true; - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { writer.WriteStartArray(); - if( value != null ) + if (value != null) { - var v = ( HashSet< T > )value; - foreach( var val in v ) - { - serializer.Serialize( writer, val?.ToString() ); - } + var v = (HashSet)value; + foreach (var val in v) + serializer.Serialize(writer, val?.ToString()); } writer.WriteEndArray(); } } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 5ba44286..16ae4d1f 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -1,31 +1,9 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OtterGui.Classes; namespace Penumbra.Mods; -[Flags] -public enum ModDataChangeType : ushort -{ - None = 0x0000, - Name = 0x0001, - Author = 0x0002, - Description = 0x0004, - Version = 0x0008, - Website = 0x0010, - Deletion = 0x0020, - Migration = 0x0040, - ModTags = 0x0080, - ImportDate = 0x0100, - Favorite = 0x0200, - LocalTags = 0x0400, - Note = 0x0800, -} - public sealed partial class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() @@ -36,122 +14,13 @@ public sealed partial class Mod : IMod }; public const uint CurrentFileVersion = 3; - public uint FileVersion { get; private set; } = CurrentFileVersion; - public LowerString Name { get; private set; } = "New Mod"; - public LowerString Author { get; private set; } = LowerString.Empty; - public string Description { get; private set; } = string.Empty; - public string Version { get; private set; } = string.Empty; - public string Website { get; private set; } = string.Empty; - public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >(); - - internal FileInfo MetaFile - => new(Path.Combine( ModPath.FullName, "meta.json" )); - - private ModDataChangeType LoadMeta() - { - var metaFile = MetaFile; - if( !File.Exists( metaFile.FullName ) ) - { - Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." ); - return ModDataChangeType.Deletion; - } - - try - { - var text = File.ReadAllText( metaFile.FullName ); - var json = JObject.Parse( text ); - - var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty; - var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty; - var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty; - var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; - var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; - var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; - var importDate = json[ nameof( ImportDate ) ]?.Value< long >(); - var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >(); - - ModDataChangeType changes = 0; - if( Name != newName ) - { - changes |= ModDataChangeType.Name; - Name = newName; - } - - if( Author != newAuthor ) - { - changes |= ModDataChangeType.Author; - Author = newAuthor; - } - - if( Description != newDescription ) - { - changes |= ModDataChangeType.Description; - Description = newDescription; - } - - if( Version != newVersion ) - { - changes |= ModDataChangeType.Version; - Version = newVersion; - } - - if( Website != newWebsite ) - { - changes |= ModDataChangeType.Website; - Website = newWebsite; - } - - if( FileVersion != newFileVersion ) - { - FileVersion = newFileVersion; - if( Migration.Migrate( this, json ) ) - { - changes |= ModDataChangeType.Migration; - } - } - - if( importDate != null && ImportDate != importDate.Value ) - { - ImportDate = importDate.Value; - changes |= ModDataChangeType.ImportDate; - } - - changes |= UpdateTags( modTags, null ); - - return changes; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not load mod meta:\n{e}" ); - return ModDataChangeType.Deletion; - } - } - - private void SaveMeta() - => Penumbra.Framework.RegisterDelayed( nameof( SaveMetaFile ) + ModPath.Name, SaveMetaFile ); - - private void SaveMetaFile() - { - var metaFile = MetaFile; - try - { - var jObject = new JObject - { - { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, - { nameof( Name ), JToken.FromObject( Name ) }, - { nameof( Author ), JToken.FromObject( Author ) }, - { nameof( Description ), JToken.FromObject( Description ) }, - { nameof( Version ), JToken.FromObject( Version ) }, - { nameof( Website ), JToken.FromObject( Website ) }, - { nameof( ModTags ), JToken.FromObject( ModTags ) }, - }; - File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); - } - } + public uint FileVersion { get; internal set; } = CurrentFileVersion; + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public IReadOnlyList< string > ModTags { get; internal set; } = Array.Empty< string >(); public override string ToString() => Name.Text; diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 6de0a682..e686ad0d 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -46,14 +46,14 @@ public sealed partial class Mod _default.ManipulationData = manips; } - public static void SaveTempCollection( ModCollection collection, string? character = null ) + public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null ) { DirectoryInfo? dir = null; try { dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); - Creator.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, + modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); var defaultMod = mod._default; @@ -88,7 +88,7 @@ public sealed partial class Mod } mod.SaveDefaultMod(); - Penumbra.ModManager.AddMod( dir ); + modManager.AddMod( dir ); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); } catch( Exception e ) diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 9ea21820..9bd7488a 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -10,28 +10,30 @@ using Penumbra.Util; namespace Penumbra.Mods; -public sealed class ModFileSystem : FileSystem, IDisposable, ISaveable +public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { - private readonly Mod.Manager _modManager; - private readonly FilenameService _files; + private readonly Mod.Manager _modManager; + private readonly CommunicatorService _communicator; + private readonly FilenameService _files; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(Mod.Manager modManager, FilenameService files) + public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files) { - _modManager = modManager; - _files = files; + _modManager = modManager; + _communicator = communicator; + _files = files; Reload(); - Changed += OnChange; - _modManager.ModDiscoveryFinished += Reload; - _modManager.ModDataChanged += OnDataChange; - _modManager.ModPathChanged += OnModPathChange; + Changed += OnChange; + _modManager.ModDiscoveryFinished += Reload; + _communicator.ModDataChanged.Event += OnDataChange; + _modManager.ModPathChanged += OnModPathChange; } public void Dispose() { - _modManager.ModPathChanged -= OnModPathChange; - _modManager.ModDiscoveryFinished -= Reload; - _modManager.ModDataChanged -= OnDataChange; + _modManager.ModPathChanged -= OnModPathChange; + _modManager.ModDiscoveryFinished -= Reload; + _communicator.ModDataChanged.Event -= OnDataChange; } public struct ImportDate : ISortMode diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index ad9344a4..30ea8f73 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -92,6 +92,7 @@ public class PenumbraNew // Add Mod Services services.AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 0e69276a..3feb5b91 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -45,6 +45,13 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); + /// + /// Parameter is the type of data change for the mod, which can be multiple flags. + /// Parameter is the changed mod. + /// Parameter is the old name of the mod in case of a name change, and null otherwise. + /// + public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); + public void Dispose() { CollectionChange.Dispose(); @@ -52,5 +59,6 @@ public class CommunicatorService : IDisposable ModMetaChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); + ModDataChanged.Dispose(); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index d57c854e..26721257 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -38,7 +38,7 @@ public class FilenameService /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. public string LocalDataFile(Mod mod) - => mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName); + => LocalDataFile(mod.ModPath.FullName); /// Obtain the path of the local data file given a mod directory. public string LocalDataFile(string modDirectory) @@ -66,7 +66,7 @@ public class FilenameService /// Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. public string ModMetaPath(Mod mod) - => mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName); + => ModMetaPath(mod.ModPath.FullName); /// Obtain the path of the meta file given a mod directory. public string ModMetaPath(string modDirectory) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index f6def7d4..3a5d8ce6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -267,7 +267,7 @@ public class ItemSwapTab : IDisposable, ITab private void CreateMod() { var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName); - Mod.Creator.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); + _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); if (!_swapData.WriteMod(_modManager.Last(), diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d6d33175..d7e7efa1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -150,7 +150,7 @@ public partial class ModEditWindow : Window, IDisposable _itemSwapTab.DrawContent(); } - // A row of three buttonSizes and a help marker that can be used for material suffix changing. + /// A row of three buttonSizes and a help marker that can be used for material suffix changing. private static class MaterialSuffix { private static string _materialSuffixFrom = string.Empty; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 49b8ff3d..3b4f2232 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -15,7 +15,7 @@ using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Import; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -78,7 +78,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector.Leaf mod) { if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) - _modManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); + _modManager.DataEditor.ChangeModFavorite(mod.Value, !mod.Value.Favorite); } private void SetDefaultImportFolder(ModFileSystem.Folder folder) { - if (ImGui.MenuItem("Set As Default Import Folder")) - { - var newName = folder.FullName(); - if (newName != _config.DefaultImportFolder) - { - _config.DefaultImportFolder = newName; - _config.Save(); - } - } + if (!ImGui.MenuItem("Set As Default Import Folder")) + return; + + var newName = folder.FullName(); + if (newName == _config.DefaultImportFolder) + return; + + _config.DefaultImportFolder = newName; + _config.Save(); } private void ClearDefaultImportFolder() { - if (ImGui.MenuItem("Clear Default Import Folder") && _config.DefaultImportFolder.Length > 0) - { - _config.DefaultImportFolder = string.Empty; - _config.Save(); - } + if (!ImGui.MenuItem("Clear Default Import Folder") || _config.DefaultImportFolder.Length <= 0) + return; + + _config.DefaultImportFolder = string.Empty; + _config.Save(); } private string _newModName = string.Empty; @@ -241,7 +241,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), - AddNewMod, _config, _modEditor); + AddNewMod, _config, _modEditor, _modManager); ImGui.OpenPopup("Import Status"); }, 0, modPath, _config.AlwaysOpenDefaultImport); } diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index d6d04872..3d65e85a 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -42,7 +42,7 @@ public class ModPanelDescriptionTab : ITab out var editedTag); _tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) - _modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag); + _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag); if (_selector.Selected!.ModTags.Count > 0) _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 8dbc94cb..c3c496cb 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -77,7 +77,7 @@ public class ModPanelEditTab : ITab var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag); if (tagIdx >= 0) - _modManager.ChangeModTag(_mod.Index, tagIdx, editedTag); + _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); UiHelpers.DefaultLineSpace(); AddOptionGroup.Draw(_modManager, _mod); @@ -172,18 +172,18 @@ public class ModPanelEditTab : ITab private void EditRegularMeta() { if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) - _modManager.ChangeModName(_mod.Index, newName); + _modManager.DataEditor.ChangeModName(_mod, newName); if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) - Penumbra.ModManager.ChangeModAuthor(_mod.Index, newAuthor); + _modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, UiHelpers.InputTextWidth.X)) - _modManager.ChangeModVersion(_mod.Index, newVersion); + _modManager.DataEditor.ChangeModVersion(_mod, newVersion); if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, UiHelpers.InputTextWidth.X)) - _modManager.ChangeModWebsite(_mod.Index, newWebsite); + _modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); @@ -192,13 +192,13 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); ImGui.SameLine(); - var fileExists = File.Exists(_mod.MetaFile.FullName); + var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod)); var tt = fileExists ? "Open the metadata json file in the text editor of your choice." : "The metadata json file does not exist."; if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(_mod.MetaFile.FullName) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(_modManager.DataEditor.MetaFile(_mod)) { UseShellExecute = true }); } /// Do some edits outside of iterations. @@ -349,7 +349,7 @@ public class ModPanelEditTab : ITab switch (_newDescriptionIdx) { case Input.Description: - modManager.ChangeModDescription(_mod.Index, _newDescription); + modManager.DataEditor.ChangeModDescription(_mod, _newDescription); break; case >= 0: if (_newDescriptionOptionIdx < 0) diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 39f795d0..2ea6e39e 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -140,7 +140,7 @@ public class ModPanelTabBar ImGui.SetCursorPos(newPos); if (ImGui.Button(FontAwesomeIcon.Star.ToIconString())) - _modManager.ChangeModFavorite(mod.Index, !mod.Favorite); + _modManager.DataEditor.ChangeModFavorite(mod, !mod.Favorite); } var hovered = ImGui.IsItemHovered(); diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Util/SaveService.cs index 71c8b6b4..221ead81 100644 --- a/Penumbra/Util/SaveService.cs +++ b/Penumbra/Util/SaveService.cs @@ -1,10 +1,8 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Text; using OtterGui.Classes; using OtterGui.Log; -using Penumbra.Api; using Penumbra.Services; namespace Penumbra.Util; @@ -12,7 +10,7 @@ namespace Penumbra.Util; /// /// Any file type that we want to save via SaveService. /// -public interface ISaveable +public interface ISavable { /// The full file name of a given object. public string ToFilename(FilenameService fileNames); @@ -42,7 +40,7 @@ public class SaveService } /// Queue a save for the next framework tick. - public void QueueSave(ISaveable value) + public void QueueSave(ISavable value) { var file = value.ToFilename(_fileNames); _framework.RegisterDelayed(value.GetType().Name + file, () => @@ -52,7 +50,7 @@ public class SaveService } /// Immediately trigger a save. - public void ImmediateSave(ISaveable value) + public void ImmediateSave(ISavable value) { var name = value.ToFilename(_fileNames); try @@ -75,7 +73,7 @@ public class SaveService } } - public void ImmediateDelete(ISaveable value) + public void ImmediateDelete(ISavable value) { var name = value.ToFilename(_fileNames); try From 5cad575c2e19f903f93a7bd1c893451cd8e8db59 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Mar 2023 18:32:27 +0100 Subject: [PATCH 0825/2451] Fix FileSystemSelector bug and add notifications on failures. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index e49a05e1..2cc26d04 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e49a05e1863957144955d1c612343ccfff11563e +Subproject commit 2cc26d04a0ec162b71544ff164d1ca768fb90c95 diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 3b4f2232..64febc57 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -41,7 +41,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Mods need to be added thread-safely outside of iteration. @@ -423,6 +423,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Penumbra.ChatService.NotificationMessage(e.Message, "Failure", NotificationType.Warning); + #endregion #region Automatic cache update functions. From 45b26030ccb54b5499e50b680a0a851867c6651f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Mar 2023 18:33:22 +0100 Subject: [PATCH 0826/2451] Fix button sizing for collapsible groups, fix default tab to be settings, fix bug with item spacing style. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 42 +++++++++++++--------- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/OtterGui b/OtterGui index 2cc26d04..cb99fa45 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2cc26d04a0ec162b71544ff164d1ca768fb90c95 +Subproject commit cb99fa45c796cd1385281e3b690151623f4ed549 diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index c3c496cb..9697f162 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -221,7 +221,7 @@ public class ModPanelEditTab : ITab public static void Draw(Mod.Manager modManager, Mod mod) { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, UiHelpers.ScaleX3); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); ImGui.SameLine(); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 0127e140..adcbbf5d 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -225,7 +225,7 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; - Widget.BeginFramedGroup(group.Name, group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); void DrawOptions() { @@ -236,21 +236,21 @@ public class ModPanelSettingsTab : ITab if (ImGui.RadioButton(option.Name, selectedOption == idx)) _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); - if (option.Description.Length > 0) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } + if (option.Description.Length <= 0) + continue; + + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); } } - DrawCollapseHandling(group, DrawOptions); + DrawCollapseHandling(group, minWidth, DrawOptions); Widget.EndFramedGroup(); } - private void DrawCollapseHandling(IModGroup group, Action draw) + private void DrawCollapseHandling(IModGroup group, float minWidth, Action draw) { if (group.Count <= _config.OptionGroupCollapsibleMin) { @@ -258,8 +258,13 @@ public class ModPanelSettingsTab : ITab } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var buttonTextShow = $"Show {group.Count} Options"; + var buttonTextHide = $"Hide {group.Count} Options"; + var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + + 2 * ImGui.GetStyle().FramePadding.X; + minWidth = Math.Max(buttonWidth, minWidth); if (shown) { var pos = ImGui.GetCursorPos(); @@ -269,21 +274,24 @@ public class ModPanelSettingsTab : ITab draw(); } - var width = ImGui.GetItemRectSize().X; - var endPos = ImGui.GetCursorPos(); + + + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); - if (ImGui.Button($"Hide {group.Count} Options", new Vector2(width, 0))) + if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); ImGui.SetCursorPos(endPos); } else { - var max = group.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = group.Max(o => ImGui.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; - if (ImGui.Button($"Show {group.Count} Options", new Vector2(max, 0))) + var width = Math.Max(optionWidth, minWidth); + if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); } } @@ -297,7 +305,7 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - Widget.BeginFramedGroup(group.Name, group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); void DrawOptions() { @@ -322,7 +330,7 @@ public class ModPanelSettingsTab : ITab } } - DrawCollapseHandling(group, DrawOptions); + DrawCollapseHandling(group, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 2ea6e39e..d0006b17 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -31,7 +31,7 @@ public class ModPanelTabBar private readonly TutorialService _tutorial; public readonly ITab[] Tabs; - private ModPanelTabType _preferredTab = 0; + private ModPanelTabType _preferredTab = ModPanelTabType.Settings; private Mod? _lastMod = null; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, From 9ee8ab73ecad129b770e6bdb4ad93ba5842e29ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 11:46:21 +0100 Subject: [PATCH 0827/2451] Fix dissolving not working. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index cb99fa45..39e24bae 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cb99fa45c796cd1385281e3b690151623f4ed549 +Subproject commit 39e24baebcb450ccd1e7103fd428ee3020c28702 From 831990949f57e457bc43a8d2bea67d7fc2e44786 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 12:21:55 +0100 Subject: [PATCH 0828/2451] Add correct handling of forceAssignment and correct iteration while deleting. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 10 ++++++++-- Penumbra/Api/TempModManager.cs | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index f66e49bd..d87dfa44 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc +Subproject commit d87dfa44ff6efcf4fe576d8a877c78f4ac0dc893 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index da560ca9..fba7cecb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -905,8 +905,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.CollectionMissing; } - if( !forceAssignment - && ( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) ) + if( forceAssignment ) + { + if( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) && !Penumbra.TempMods.Collections.Delete( identifier ) ) + { + return PenumbraApiEc.AssignmentDeletionFailed; + } + } + else if( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) { return PenumbraApiEc.CharacterCollectionExists; } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 08393ebd..1034ca0d 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -187,7 +187,7 @@ public class TempModManager if( Collections[ i ].Collection == collection ) { CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName ); - Collections.Delete( i ); + Collections.Delete( i-- ); } } From fb2fe05409cbf6c6389614969c9c705d0188dd66 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 12:34:47 +0100 Subject: [PATCH 0829/2451] Merge API changes. --- Penumbra/Api/PenumbraApi.cs | 12 +++++++++--- Penumbra/Api/TempCollectionManager.cs | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 3c666dc4..913deaf3 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -848,10 +848,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_tempCollections.CollectionByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!forceAssignment - && (_tempCollections.Collections.Individuals.ContainsKey(identifier) - || _collectionManager.Individuals.Individuals.ContainsKey(identifier))) + if (forceAssignment) + { + if (_tempCollections.Collections.Individuals.ContainsKey(identifier) && !_tempCollections.Collections.Delete(identifier)) + return PenumbraApiEc.AssignmentDeletionFailed; + } + else if (_tempCollections.Collections.Individuals.ContainsKey(identifier) + || _collectionManager.Individuals.Individuals.ContainsKey(identifier)) + { return PenumbraApiEc.CharacterCollectionExists; + } var group = _tempCollections.Collections.GetGroup(identifier); return _tempCollections.AddIdentifier(collection, group) diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Api/TempCollectionManager.cs index fab7eace..18a7e43f 100644 --- a/Penumbra/Api/TempCollectionManager.cs +++ b/Penumbra/Api/TempCollectionManager.cs @@ -67,11 +67,11 @@ public class TempCollectionManager : IDisposable collection.ClearCache(); for (var i = 0; i < Collections.Count; ++i) { - if (Collections[i].Collection == collection) - { - _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); - Collections.Delete(i); - } + if (Collections[i].Collection != collection) + continue; + + _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); + Collections.Delete(i--); } return true; From 348da388790ccd9d439f7bcda5f2399eb7c7d36e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 25 Mar 2023 11:36:14 +0000 Subject: [PATCH 0830/2451] [CI] Updating repo.json for refs/tags/0.6.6.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 599faae9..66e305b5 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.3", - "TestingAssemblyVersion": "0.6.6.3", + "AssemblyVersion": "0.6.6.4", + "TestingAssemblyVersion": "0.6.6.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From d58a3e0fe7675bf100e74c39c297ee78cb69fa2a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 12:53:47 +0100 Subject: [PATCH 0831/2451] Fix bug with default mod settings other than 0. --- Penumbra/Collections/ModCollection.Changes.cs | 17 ++++++++--------- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 10 +++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 9afd0ef2..801a135d 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -93,16 +93,15 @@ public partial class ModCollection public bool SetModSetting( int idx, int groupIdx, uint newValue ) { var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; - var oldValue = settings?[ groupIdx ] ?? 0; - if( oldValue != newValue ) - { - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); - ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); - return true; - } + var oldValue = settings?[ groupIdx ] ?? Penumbra.ModManager[idx].Groups[groupIdx].DefaultSettings; + if (oldValue == newValue) + return false; + + var inheritance = FixInheritance( idx, false ); + _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); + return true; - return false; } // Change one of the available mod settings for mod idx discerned by type. diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index adcbbf5d..b0936d90 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -122,11 +122,11 @@ public class ModPanelSettingsTab : ITab private void DrawEnabledInput() { var enabled = _settings.Enabled; - if (ImGui.Checkbox("Enabled", ref enabled)) - { - _modManager.NewMods.Remove(_selector.Selected!); - _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); - } + if (!ImGui.Checkbox("Enabled", ref enabled)) + return; + + _modManager.NewMods.Remove(_selector.Selected!); + _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); } /// From 355206e0cfc2f835cfe9b9414ab7e4d284044a04 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 16:12:14 +0100 Subject: [PATCH 0832/2451] Cleanup --- Penumbra/Collections/ModCollection.Changes.cs | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 801a135d..44c622a1 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -16,13 +16,12 @@ public partial class ModCollection // Enable or disable the mod inheritance of mod idx. public bool SetModInheritance( int idx, bool inherit ) { - if( FixInheritance( idx, inherit ) ) - { - ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); - return true; - } + if (!FixInheritance(idx, inherit)) + return false; + + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); + return true; - return false; } // Set the enabled state mod idx to newValue if it differs from the current enabled state. @@ -30,24 +29,21 @@ public partial class ModCollection public bool SetModState( int idx, bool newValue ) { var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; - if( newValue != oldValue ) - { - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Enabled = newValue; - ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); - return true; - } + if (newValue == oldValue) + return false; + + var inheritance = FixInheritance( idx, false ); + _settings[ idx ]!.Enabled = newValue; + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); + return true; - return false; } // Enable or disable the mod inheritance of every mod in mods. public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) { if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) - { ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, 0, false ); - } } // Set the enabled state of every mod in mods to the new value. @@ -58,12 +54,12 @@ public partial class ModCollection foreach( var mod in mods ) { var oldValue = _settings[ mod.Index ]?.Enabled; - if( newValue != oldValue ) - { - FixInheritance( mod.Index, false ); - _settings[ mod.Index ]!.Enabled = newValue; - changes = true; - } + if (newValue == oldValue) + continue; + + FixInheritance( mod.Index, false ); + _settings[ mod.Index ]!.Enabled = newValue; + changes = true; } if( changes ) @@ -77,15 +73,14 @@ public partial class ModCollection public bool SetModPriority( int idx, int newValue ) { var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; - if( newValue != oldValue ) - { - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Priority = newValue; - ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); - return true; - } + if (newValue == oldValue) + return false; + + var inheritance = FixInheritance( idx, false ); + _settings[ idx ]!.Priority = newValue; + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); + return true; - return false; } // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. @@ -126,9 +121,7 @@ public partial class ModCollection { var settings = _settings[ idx ]; if( inherit == ( settings == null ) ) - { return false; - } _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager[ idx ] ); return true; @@ -140,8 +133,6 @@ public partial class ModCollection private void SaveOnChange( bool inherited ) { if( !inherited ) - { Penumbra.SaveService.QueueSave(this); - } } } \ No newline at end of file From c958935f40520c5f3e714bc7857c2803f2c12f5c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Mar 2023 16:55:39 +0100 Subject: [PATCH 0833/2451] Run command registration on framework. --- Penumbra/CommandHandler.cs | 19 +++++++++++-------- Penumbra/Penumbra.cs | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4f53a7b6..30415ed8 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Runtime.CompilerServices; +using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; @@ -66,7 +67,8 @@ public class CommandHandler : IDisposable private readonly Mod.Manager _modManager; private readonly ModCollection.Manager _collectionManager; - public CommandHandler( CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, Mod.Manager modManager, + public CommandHandler( Framework framework, CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, + Mod.Manager modManager, ModCollection.Manager collectionManager, ActorManager actors ) { _commandManager = commandManager; @@ -77,17 +79,18 @@ public class CommandHandler : IDisposable _modManager = modManager; _collectionManager = collectionManager; _actors = actors; - _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) + framework.RunOnFrameworkThread( () => { - HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", - ShowInHelp = true, + _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) + { + HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", + ShowInHelp = true, + } ); } ); } public void Dispose() - { - _commandManager.RemoveHandler( CommandName ); - } + => _commandManager.RemoveHandler( CommandName ); private void OnCommand( string command, string arguments ) { @@ -586,7 +589,7 @@ public class CommandHandler : IDisposable } } - private static void Print( Func text ) + private static void Print( Func< SeString > text ) { if( Penumbra.Config.PrintSuccessfulCommandsToChat ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8d8c70d2..a8e5eb7f 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin }; var btn = new LaunchButton( cfg ); var system = new WindowSystem( Name ); - var cmd = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); + var cmd = new CommandHandler( Dalamud.Framework, Dalamud.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); system.AddWindow( cfg ); system.AddWindow( cfg.ModEditPopup ); system.AddWindow( changelog ); From 182546ee101561f8512fad54da445462afab356f Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 25 Mar 2023 15:58:18 +0000 Subject: [PATCH 0834/2451] [CI] Updating repo.json for refs/tags/0.6.6.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 66e305b5..8b41b9cc 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.4", - "TestingAssemblyVersion": "0.6.6.4", + "AssemblyVersion": "0.6.6.5", + "TestingAssemblyVersion": "0.6.6.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ef9022a74684f3fb956a70c4324f9981d7584f87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 Mar 2023 12:37:22 +0200 Subject: [PATCH 0835/2451] Stuff. --- Penumbra/Api/IpcTester.cs | 4 +- Penumbra/Api/TempCollectionManager.cs | 2 +- Penumbra/Api/TempModManager.cs | 20 ++-- Penumbra/Collections/CollectionManager.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Collections/ModCollection.Cache.cs | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 77 ++---------- Penumbra/Mods/Mod.LocalData.cs | 27 +++++ Penumbra/Mods/Mod.Meta.cs | 52 ++++++-- Penumbra/Mods/Mod.TemporaryMod.cs | 111 ------------------ Penumbra/Mods/TemporaryMod.cs | 106 +++++++++++++++++ Penumbra/Services/CommunicatorService.cs | 2 +- 12 files changed, 205 insertions(+), 204 deletions(-) delete mode 100644 Penumbra/Mods/Mod.TemporaryMod.cs create mode 100644 Penumbra/Mods/TemporaryMod.cs diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index b48128e0..104a2079 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1253,7 +1253,7 @@ public class IpcTester : IDisposable .FirstOrDefault() ?? "Unknown"; if (ImGui.Button($"Save##{collection.Name}")) - Mod.TemporaryMod.SaveTempCollection(_modManager, collection, character); + TemporaryMod.SaveTempCollection(_modManager, collection, character); ImGuiUtil.DrawTableColumn(collection.Name); ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); @@ -1271,7 +1271,7 @@ public class IpcTester : IDisposable using var table = ImRaii.Table("##modTree", 5); - void PrintList(string collectionName, IReadOnlyList list) + void PrintList(string collectionName, IReadOnlyList list) { foreach (var mod in list) { diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Api/TempCollectionManager.cs index 18a7e43f..7ed67f53 100644 --- a/Penumbra/Api/TempCollectionManager.cs +++ b/Penumbra/Api/TempCollectionManager.cs @@ -31,7 +31,7 @@ public class TempCollectionManager : IDisposable _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; } - private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) => TempModManager.OnGlobalModChange(_customCollections.Values, mod, created, removed); public int Count diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 09625a73..07e65c36 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -20,8 +20,8 @@ public class TempModManager : IDisposable { private readonly CommunicatorService _communicator; - private readonly Dictionary> _mods = new(); - private readonly List _modsForAllCollections = new(); + private readonly Dictionary> _mods = new(); + private readonly List _modsForAllCollections = new(); public TempModManager(CommunicatorService communicator) { @@ -34,10 +34,10 @@ public class TempModManager : IDisposable _communicator.CollectionChange.Event -= OnCollectionChange; } - public IReadOnlyDictionary> Mods + public IReadOnlyDictionary> Mods => _mods; - public IReadOnlyList ModsForAllCollections + public IReadOnlyList ModsForAllCollections => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, @@ -74,7 +74,7 @@ public class TempModManager : IDisposable } // Apply any new changes to the temporary mod. - private void ApplyModChange(Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed) + private void ApplyModChange(TemporaryMod mod, ModCollection? collection, bool created, bool removed) { if (collection != null) { @@ -92,7 +92,7 @@ public class TempModManager : IDisposable /// /// Apply a mod change to a set of collections. /// - public static void OnGlobalModChange(IEnumerable collections, Mod.TemporaryMod mod, bool created, bool removed) + public static void OnGlobalModChange(IEnumerable collections, TemporaryMod mod, bool created, bool removed) { if (removed) foreach (var c in collections) @@ -104,9 +104,9 @@ public class TempModManager : IDisposable // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Returns the found or created mod and whether it was newly created. - private Mod.TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) + private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) { - List list; + List list; if (collection == null) { list = _modsForAllCollections; @@ -117,14 +117,14 @@ public class TempModManager : IDisposable } else { - list = new List(); + list = new List(); _mods.Add(collection, list); } var mod = list.Find(m => m.Priority == priority && m.Name == tag); if (mod == null) { - mod = new Mod.TemporaryMod() + mod = new TemporaryMod() { Name = tag, Priority = priority, diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 0e3fb35a..4b883dd3 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -92,7 +92,7 @@ public partial class ModCollection _modManager.ModPathChanged -= OnModPathChange; } - private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) => TempModManager.OnGlobalModChange(_collections, mod, created, removed); // Returns true if the name is not empty, it is not the name of the empty collection diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 4ebcca8f..37bfafd5 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -44,7 +44,7 @@ public partial class ModCollection => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); // Handle temporary mods for this collection. - public void Apply(Mod.TemporaryMod tempMod, bool created) + public void Apply(TemporaryMod tempMod, bool created) { if (created) _cache?.AddMod(tempMod, tempMod.TotalManipulations > 0); @@ -52,7 +52,7 @@ public partial class ModCollection _cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0); } - public void Remove(Mod.TemporaryMod tempMod) + public void Remove(TemporaryMod tempMod) { _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 285d119a..cd148344 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -192,7 +192,7 @@ public partial class ModCollection // Add all forced redirects. foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( - Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< Mod.TemporaryMod >() ) ) + Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) { AddMod( tempMod, false ); } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7b74f83a..27253ff1 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -39,7 +39,7 @@ public class ModDataEditor mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - _saveService.ImmediateSave(new ModMeta(mod)); + _saveService.ImmediateSave(new Mod.ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) @@ -96,7 +96,7 @@ public class ModDataEditor } if (save) - _saveService.QueueSave(new ModData(mod)); + _saveService.QueueSave(new Mod.ModData(mod)); return changes; } @@ -161,7 +161,7 @@ public class ModDataEditor if (Mod.Migration.Migrate(mod, json)) { changes |= ModDataChangeType.Migration; - _saveService.ImmediateSave(new ModMeta(mod)); + _saveService.ImmediateSave(new Mod.ModMeta(mod)); } } @@ -189,7 +189,7 @@ public class ModDataEditor var oldName = mod.Name; mod.Name = newName; - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); } @@ -199,7 +199,7 @@ public class ModDataEditor return; mod.Author = newAuthor; - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); } @@ -209,7 +209,7 @@ public class ModDataEditor return; mod.Description = newDescription; - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); } @@ -219,7 +219,7 @@ public class ModDataEditor return; mod.Version = newVersion; - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); } @@ -229,7 +229,7 @@ public class ModDataEditor return; mod.Website = newWebsite; - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); } @@ -245,7 +245,7 @@ public class ModDataEditor return; mod.Favorite = state; - _saveService.QueueSave(new ModData(mod)); + _saveService.QueueSave(new Mod.ModData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -256,7 +256,7 @@ public class ModDataEditor return; mod.Note = newNote; - _saveService.QueueSave(new ModData(mod)); + _saveService.QueueSave(new Mod.ModData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -281,10 +281,10 @@ public class ModDataEditor } if (flags.HasFlag(ModDataChangeType.ModTags)) - _saveService.QueueSave(new ModMeta(mod)); + _saveService.QueueSave(new Mod.ModMeta(mod)); if (flags.HasFlag(ModDataChangeType.LocalTags)) - _saveService.QueueSave(new ModData(mod)); + _saveService.QueueSave(new Mod.ModData(mod)); if (flags != 0) _communicatorService.ModDataChanged.Invoke(flags, mod, null); @@ -306,57 +306,4 @@ public class ModDataEditor Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}"); } } - - - private readonly struct ModMeta : ISavable - { - private readonly Mod _mod; - - public ModMeta(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.ModMetaPath(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) }, - { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, - { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, - { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, - { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, - { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, - { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } - - private readonly struct ModData : ISavable - { - private readonly Mod _mod; - - public ModData(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.LocalDataFile(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) }, - { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, - { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, - { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, - { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } } diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index 29e11f3d..71a73f0f 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Newtonsoft.Json; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods; @@ -49,4 +51,29 @@ public sealed partial class Mod return type; } + + internal readonly struct ModData : ISavable + { + private readonly Mod _mod; + + public ModData(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.LocalDataFile(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(ImportDate), JToken.FromObject(_mod.ImportDate) }, + { nameof(LocalTags), JToken.FromObject(_mod.LocalTags) }, + { nameof(Note), JToken.FromObject(_mod.Note) }, + { nameof(Favorite), JToken.FromObject(_mod.Favorite) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + } } diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 16ae4d1f..0e29a12c 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OtterGui.Classes; +using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods; - + public sealed partial class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() @@ -13,15 +18,42 @@ public sealed partial class Mod : IMod Priority = int.MaxValue, }; - public const uint CurrentFileVersion = 3; - public uint FileVersion { get; internal set; } = CurrentFileVersion; - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public IReadOnlyList< string > ModTags { get; internal set; } = Array.Empty< string >(); + public const uint CurrentFileVersion = 3; + public uint FileVersion { get; internal set; } = CurrentFileVersion; + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); public override string ToString() => Name.Text; -} \ No newline at end of file + + internal readonly struct ModMeta : ISavable + { + private readonly Mod _mod; + + public ModMeta(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.ModMetaPath(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(Name), JToken.FromObject(_mod.Name) }, + { nameof(Author), JToken.FromObject(_mod.Author) }, + { nameof(Description), JToken.FromObject(_mod.Description) }, + { nameof(Version), JToken.FromObject(_mod.Version) }, + { nameof(Website), JToken.FromObject(_mod.Website) }, + { nameof(ModTags), JToken.FromObject(_mod.ModTags) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + } +} diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs deleted file mode 100644 index e686ad0d..00000000 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using OtterGui.Classes; -using Penumbra.Collections; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public class TemporaryMod : IMod - { - public LowerString Name { get; init; } = LowerString.Empty; - public int Index { get; init; } = -2; - public int Priority { get; init; } = int.MaxValue; - - public int TotalManipulations - => Default.Manipulations.Count; - - public ISubMod Default - => _default; - - public IReadOnlyList< IModGroup > Groups - => Array.Empty< IModGroup >(); - - public IEnumerable< ISubMod > AllSubMods - => new[] { Default }; - - private readonly SubMod _default; - - public TemporaryMod() - => _default = new SubMod( this ); - - public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) - => _default.FileData[ gamePath ] = fullPath; - - public bool SetManipulation( MetaManipulation manip ) - => _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip ); - - public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) - { - _default.FileData = dict; - _default.ManipulationData = manips; - } - - public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null ) - { - DirectoryInfo? dir = null; - try - { - dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); - var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); - modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); - var mod = new Mod( dir ); - var defaultMod = mod._default; - foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) - { - if( gamePath.Path.EndsWith( ".imc"u8 ) ) - { - continue; - } - - var targetPath = fullPath.Path.FullName; - if( fullPath.Path.Name.StartsWith( '|' ) ) - { - targetPath = targetPath.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries ).Last(); - } - - if( Path.IsPathRooted(targetPath) ) - { - var target = Path.Combine( fileDir.FullName, Path.GetFileName(targetPath) ); - File.Copy( targetPath, target, true ); - defaultMod.FileData[ gamePath ] = new FullPath( target ); - } - else - { - defaultMod.FileSwapData[ gamePath ] = new FullPath(targetPath); - } - } - - foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) - { - defaultMod.ManipulationData.Add( manip ); - } - - mod.SaveDefaultMod(); - modManager.AddMod( dir ); - Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); - if( dir != null && Directory.Exists( dir.FullName ) ) - { - try - { - Directory.Delete( dir.FullName, true ); - } - catch - { - // ignored - } - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs new file mode 100644 index 00000000..5bf37c95 --- /dev/null +++ b/Penumbra/Mods/TemporaryMod.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods; + +public class TemporaryMod : IMod +{ + public LowerString Name { get; init; } = LowerString.Empty; + public int Index { get; init; } = -2; + public int Priority { get; init; } = int.MaxValue; + + public int TotalManipulations + => Default.Manipulations.Count; + + public ISubMod Default + => _default; + + public IReadOnlyList< IModGroup > Groups + => Array.Empty< IModGroup >(); + + public IEnumerable< ISubMod > AllSubMods + => new[] { Default }; + + private readonly Mod.SubMod _default; + + public TemporaryMod() + => _default = new Mod.SubMod( this ); + + public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) + => _default.FileData[ gamePath ] = fullPath; + + public bool SetManipulation( MetaManipulation manip ) + => _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip ); + + public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) + { + _default.FileData = dict; + _default.ManipulationData = manips; + } + + public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null ) + { + DirectoryInfo? dir = null; + try + { + dir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); + modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, + $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); + var mod = new Mod( dir ); + var defaultMod = (Mod.SubMod) mod.Default; + foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) + { + if( gamePath.Path.EndsWith( ".imc"u8 ) ) + { + continue; + } + + var targetPath = fullPath.Path.FullName; + if( fullPath.Path.Name.StartsWith( '|' ) ) + { + targetPath = targetPath.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries ).Last(); + } + + if( Path.IsPathRooted(targetPath) ) + { + var target = Path.Combine( fileDir.FullName, Path.GetFileName(targetPath) ); + File.Copy( targetPath, target, true ); + defaultMod.FileData[ gamePath ] = new FullPath( target ); + } + else + { + defaultMod.FileSwapData[ gamePath ] = new FullPath(targetPath); + } + } + + foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) + defaultMod.ManipulationData.Add( manip ); + + mod.SaveDefaultMod(); + modManager.AddMod( dir ); + Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); + if( dir != null && Directory.Exists( dir.FullName ) ) + { + try + { + Directory.Delete( dir.FullName, true ); + } + catch + { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 3feb5b91..f4381883 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -20,7 +20,7 @@ public class CommunicatorService : IDisposable /// Parameter is whether the mod was newly created. /// Parameter is whether the mod was deleted. /// - public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); /// /// Parameter is the type of change. From ccdafcf85d52c1d485dd90cec1dabfcfe6ea5455 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 Mar 2023 18:02:32 +0200 Subject: [PATCH 0836/2451] More Stuff. --- Penumbra/Collections/ModCollection.cs | 2 -- Penumbra/Collections/ResolveData.cs | 30 +++++++++--------- Penumbra/Mods/Manager/ModDataEditor.cs | 7 ++--- Penumbra/Mods/Mod.LocalData.cs | 4 ++- Penumbra/Mods/Mod.Meta.Migration.cs | 43 ++++++++++++++------------ Penumbra/Mods/Mod.Meta.cs | 6 ++-- Penumbra/Penumbra.cs | 7 ++--- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- 8 files changed, 49 insertions(+), 52 deletions(-) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index a8a4e1f4..54f1f9a8 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; using OtterGui; -using OtterGui.Classes; -using Penumbra.Services; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 485f2c08..f816069e 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.String; namespace Penumbra.Collections; @@ -25,14 +24,14 @@ public readonly struct ResolveData AssociatedGameObject = IntPtr.Zero; } - public ResolveData( ModCollection collection, IntPtr gameObject ) + public ResolveData(ModCollection collection, IntPtr gameObject) { _modCollection = collection; AssociatedGameObject = gameObject; } - public ResolveData( ModCollection collection ) - : this( collection, IntPtr.Zero ) + public ResolveData(ModCollection collection) + : this(collection, IntPtr.Zero) { } public override string ToString() @@ -40,19 +39,18 @@ public readonly struct ResolveData public unsafe string AssociatedName() { - if( AssociatedGameObject == IntPtr.Zero ) - { + if (AssociatedGameObject == IntPtr.Zero) return "no associated object."; - } try { - var id = Penumbra.Actors.FromObject( ( GameObject* )AssociatedGameObject, out _, false, true, true ); - if( id.IsValid ) + var id = Penumbra.Actors.FromObject((GameObject*)AssociatedGameObject, out _, false, true, true); + if (id.IsValid) { var name = id.ToString(); - var parts = name.Split( ' ', 3 ); - return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); + var parts = name.Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); } } catch @@ -66,12 +64,12 @@ public readonly struct ResolveData public static class ResolveDataExtensions { - public static ResolveData ToResolveData( this ModCollection collection ) + public static ResolveData ToResolveData(this ModCollection collection) => new(collection); - public static ResolveData ToResolveData( this ModCollection collection, IntPtr ptr ) + public static ResolveData ToResolveData(this ModCollection collection, IntPtr ptr) => new(collection, ptr); - public static unsafe ResolveData ToResolveData( this ModCollection collection, void* ptr ) - => new(collection, ( IntPtr )ptr); -} \ No newline at end of file + public static unsafe ResolveData ToResolveData(this ModCollection collection, void* ptr) + => new(collection, (IntPtr)ptr); +} diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 27253ff1..2f236b5f 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -120,7 +120,7 @@ public class ModDataEditor var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(Mod.FileVersion)]?.Value() ?? 0; + var newFileVersion = json[nameof(Mod.ModMeta.FileVersion)]?.Value() ?? 0; var importDate = json[nameof(Mod.ImportDate)]?.Value(); var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); @@ -155,10 +155,9 @@ public class ModDataEditor mod.Website = newWebsite; } - if (mod.FileVersion != newFileVersion) + if (newFileVersion != Mod.ModMeta.FileVersion) { - mod.FileVersion = newFileVersion; - if (Mod.Migration.Migrate(mod, json)) + if (Mod.Migration.Migrate(mod, json, ref newFileVersion)) { changes |= ModDataChangeType.Migration; _saveService.ImmediateSave(new Mod.ModMeta(mod)); diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index 71a73f0f..1c0ae1e0 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -54,6 +54,8 @@ public sealed partial class Mod internal readonly struct ModData : ISavable { + public const int FileVersion = 3; + private readonly Mod _mod; public ModData(Mod mod) @@ -66,7 +68,7 @@ public sealed partial class Mod { var jObject = new JObject { - { nameof(FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(FileVersion), JToken.FromObject(FileVersion) }, { nameof(ImportDate), JToken.FromObject(_mod.ImportDate) }, { nameof(LocalTags), JToken.FromObject(_mod.LocalTags) }, { nameof(Note), JToken.FromObject(_mod.Note) }, diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 64501009..5a07fd29 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -15,31 +15,34 @@ public sealed partial class Mod { public static partial class Migration { - public static bool Migrate(Mod mod, JObject json) - => MigrateV0ToV1(mod, json) || MigrateV1ToV2(mod) || MigrateV2ToV3(mod); - - private static bool MigrateV2ToV3(Mod mod) - { - if (mod.FileVersion > 2) - return false; - - // Remove import time. - mod.FileVersion = 3; - return true; - } - [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex GroupRegex(); - private static bool MigrateV1ToV2(Mod mod) + [GeneratedRegex("^group_", RegexOptions.Compiled)] + private static partial Regex GroupStartRegex(); + + public static bool Migrate(Mod mod, JObject json, ref uint fileVersion) + => MigrateV0ToV1(mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); + + private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) { - if (mod.FileVersion > 1) + if (fileVersion > 2) + return false; + + // Remove import time. + fileVersion = 3; + return true; + } + + private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) + { + if (fileVersion > 1) return false; if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) { - var newName = Regex.Replace(group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled); + var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); try { if (newName != group.Name) @@ -51,14 +54,14 @@ public sealed partial class Mod } } - mod.FileVersion = 2; + fileVersion = 2; return true; } - private static bool MigrateV0ToV1(Mod mod, JObject json) + private static bool MigrateV0ToV1(Mod mod, JObject json, ref uint fileVersion) { - if (mod.FileVersion > 0) + if (fileVersion > 0) return false; var swaps = json["FileSwaps"]?.ToObject>() @@ -110,7 +113,7 @@ public sealed partial class Mod Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); } - mod.FileVersion = 1; + fileVersion = 1; mod.SaveDefaultMod(); return true; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 0e29a12c..3b716298 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -18,8 +18,6 @@ public sealed partial class Mod : IMod Priority = int.MaxValue, }; - public const uint CurrentFileVersion = 3; - public uint FileVersion { get; internal set; } = CurrentFileVersion; public LowerString Name { get; internal set; } = "New Mod"; public LowerString Author { get; internal set; } = LowerString.Empty; public string Description { get; internal set; } = string.Empty; @@ -32,6 +30,8 @@ public sealed partial class Mod : IMod internal readonly struct ModMeta : ISavable { + public const uint FileVersion = 3; + private readonly Mod _mod; public ModMeta(Mod mod) @@ -44,7 +44,7 @@ public sealed partial class Mod : IMod { var jObject = new JObject { - { nameof(FileVersion), JToken.FromObject(_mod.FileVersion) }, + { nameof(FileVersion), JToken.FromObject(FileVersion) }, { nameof(Name), JToken.FromObject(_mod.Name) }, { nameof(Author), JToken.FromObject(_mod.Author) }, { nameof(Description), JToken.FromObject(_mod.Description) }, diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8b31e9e0..46b8ad3a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -72,9 +72,6 @@ public class Penumbra : IDalamudPlugin private bool _disposed; private readonly PenumbraNew _tmp; - // TODO - public static ResourceManagerService ResourceManagerService { get; private set; } = null!; - public static ResourceService ResourceService { get; private set; } = null!; public Penumbra(DalamudPluginInterface pluginInterface) { @@ -98,13 +95,13 @@ public class Penumbra : IDalamudPlugin StainService = _tmp.Services.GetRequiredService(); TempMods = _tmp.Services.GetRequiredService(); ResidentResources = _tmp.Services.GetRequiredService(); - ResourceManagerService = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); ModManager = _tmp.Services.GetRequiredService(); CollectionManager = _tmp.Services.GetRequiredService(); TempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); - ResourceService = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); ResourceLoader = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index c1724b8e..95ff0d2a 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -48,7 +48,7 @@ public class ResourceTab : ITab unsafe { - Penumbra.ResourceManagerService.IterateGraphs(DrawCategoryContainer); + _resourceManager.IterateGraphs(DrawCategoryContainer); } ImGui.NewLine(); From 1253079968322b76c1ef8e351a8f69e6550e445d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 Mar 2023 15:22:39 +0200 Subject: [PATCH 0837/2451] Move Mod.Manager and ModCollection.Manager to outer scope and required changes. --- Penumbra/Api/IpcTester.cs | 6 +- Penumbra/Api/PenumbraApi.cs | 10 +- Penumbra/Api/PenumbraIpcProviders.cs | 2 +- .../Collections/CollectionManager.Active.cs | 841 ++++++++------- Penumbra/Collections/CollectionManager.cs | 835 ++++++++------- .../IndividualCollections.Files.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 25 +- Penumbra/Collections/ModCollection.Cache.cs | 963 +++++++++--------- Penumbra/Collections/ModCollection.File.cs | 2 +- .../Collections/ModCollection.Inheritance.cs | 4 +- Penumbra/Collections/ModCollection.cs | 16 +- Penumbra/CommandHandler.cs | 6 +- Penumbra/Import/TexToolsImport.cs | 4 +- .../PathResolving/CollectionResolver.cs | 4 +- .../Interop/PathResolving/PathResolver.cs | 4 +- Penumbra/Mods/Editor/DuplicateManager.cs | 6 +- Penumbra/Mods/Editor/ModBackup.cs | 8 +- Penumbra/Mods/Editor/ModFileEditor.cs | 6 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 8 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 4 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 341 +++---- Penumbra/Mods/Manager/Mod.Manager.Local.cs | 12 - Penumbra/Mods/Manager/Mod.Manager.Options.cs | 700 +++++++------ Penumbra/Mods/Manager/Mod.Manager.Root.cs | 277 +++-- Penumbra/Mods/Manager/Mod.Manager.cs | 115 +-- Penumbra/Mods/Manager/ModDataEditor.cs | 1 - Penumbra/Mods/Mod.BasePath.cs | 8 +- Penumbra/Mods/Mod.ChangedItems.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 16 +- Penumbra/Mods/ModFileSystem.cs | 4 +- .../Subclasses/Mod.Files.MultiModGroup.cs | 178 ++-- .../Subclasses/Mod.Files.SingleModGroup.cs | 195 ++-- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 418 ++++---- Penumbra/Mods/Subclasses/ModSettings.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 8 +- Penumbra/Penumbra.cs | 8 +- Penumbra/PenumbraNew.cs | 4 +- Penumbra/Services/ConfigMigrationService.cs | 10 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 8 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../ModEditWindow.QuickImport.cs | 2 +- .../Collections.CollectionSelector.cs | 4 +- .../Collections.IndividualCollectionUi.cs | 4 +- .../Collections.InheritanceUi.cs | 4 +- .../CollectionTab/Collections.SpecialCombo.cs | 4 +- Penumbra/UI/FileDialogService.cs | 4 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 8 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 6 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 6 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 4 +- Penumbra/UI/Tabs/CollectionsTab.cs | 4 +- Penumbra/UI/Tabs/DebugTab.cs | 8 +- Penumbra/UI/Tabs/EffectiveTab.cs | 4 +- Penumbra/UI/Tabs/ModsTab.cs | 6 +- Penumbra/UI/Tabs/SettingsTab.cs | 4 +- 59 files changed, 2562 insertions(+), 2615 deletions(-) delete mode 100644 Penumbra/Mods/Manager/Mod.Manager.Local.cs diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 104a2079..c98ac854 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -38,7 +38,7 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Temporary _temporary; - public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager) + public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager) { _ipcProviders = ipcProviders; _pluginState = new PluginState(pi); @@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable private class Temporary { private readonly DalamudPluginInterface _pi; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; - public Temporary(DalamudPluginInterface pi, Mod.Manager modManager) + public Temporary(DalamudPluginInterface pi, ModManager modManager) { _pi = pi; _modManager = modManager; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 913deaf3..5b5df883 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -93,10 +93,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi private Penumbra _penumbra; private Lumina.GameData? _lumina; - private Mod.Manager _modManager; + private ModManager _modManager; private ResourceLoader _resourceLoader; private Configuration _config; - private ModCollection.Manager _collectionManager; + private CollectionManager _collectionManager; private DalamudServices _dalamud; private TempCollectionManager _tempCollections; private TempModManager _tempMods; @@ -104,8 +104,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi private CollectionResolver _collectionResolver; private CutsceneService _cutsceneService; - public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, - Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader, + Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) { _communicator = communicator; @@ -1021,7 +1021,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Resolve a path given by string for a specific collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private string ResolvePath(string path, Mod.Manager _, ModCollection collection) + private string ResolvePath(string path, ModManager _, ModCollection collection) { if (!_config.EnableMods) return path; diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 50768ddf..e6d135bb 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; - public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager ) + public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager ) { Api = api; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 1d571468..f685e886 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,422 +1,419 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Mods; -using Penumbra.UI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal.Notifications; -using Penumbra.GameData.Actors; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public partial class ModCollection -{ - public sealed partial class Manager : ISavable - { - public const int Version = 1; - - // The collection currently selected for changing settings. - public ModCollection Current { get; private set; } = Empty; - - // The collection currently selected is in use either as an active collection or through inheritance. - public bool CurrentCollectionInUse { get; private set; } - - // The collection used for general file redirections and all characters not specifically named. - public ModCollection Default { get; private set; } = Empty; - - // The collection used for all files categorized as UI files. - public ModCollection Interface { get; private set; } = Empty; - - // A single collection that can not be deleted as a fallback for the current collection. - private ModCollection DefaultName { get; set; } = Empty; - - // The list of character collections. - public readonly IndividualCollections Individuals; - - public ModCollection Individual(ActorIdentifier identifier) - => Individuals.TryGetCollection(identifier, out var c) ? c : Default; - - // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; - - // Return the configured collection for the given type or null. - // Does not handle Inactive, use ByName instead. - public ModCollection? ByType(CollectionType type) - => ByType(type, ActorIdentifier.Invalid); - - public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) - { - if (type.IsSpecial()) - return _specialCollections[(int)type]; - - return type switch - { - CollectionType.Default => Default, - CollectionType.Interface => Interface, - CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, - _ => null, - }; - } - - // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) - { - var oldCollectionIdx = collectionType switch - { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count - ? -1 - : Individuals[individualIndex].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, - _ => -1, - }; - - if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) - return; - - var newCollection = this[newIdx]; - if (newIdx > Empty.Index) - newCollection.CreateCache(collectionType is CollectionType.Default); - - switch (collectionType) - { - case CollectionType.Default: - Default = newCollection; - break; - case CollectionType.Interface: - Interface = newCollection; - break; - case CollectionType.Current: - Current = newCollection; - break; - case CollectionType.Individual: - if (!Individuals.ChangeCollection(individualIndex, newCollection)) - { - RemoveCache(newIdx); - return; - } - - break; - default: - _specialCollections[(int)collectionType] = newCollection; - break; - } - - RemoveCache(oldCollectionIdx); - - UpdateCurrentCollectionInUse(); - _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, - collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); - } - - private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = _specialCollections - .OfType() - .Prepend(Interface) - .Prepend(Default) - .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - - public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) - => SetCollection(collection.Index, collectionType, individualIndex); - - // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) - return false; - - _specialCollections[(int)collectionType] = Default; - _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); - return true; - } - - // Remove a special collection if it exists - public void RemoveSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial()) - return; - - var old = _specialCollections[(int)collectionType]; - if (old != null) - { - _specialCollections[(int)collectionType] = null; - _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); - } - } - - // Wrappers around Individual Collection handling. - public void CreateIndividualCollection(params ActorIdentifier[] identifiers) - { - if (Individuals.Add(identifiers, Default)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); - } - - public void RemoveIndividualCollection(int individualIndex) - { - if (individualIndex < 0 || individualIndex >= Individuals.Count) - return; - - var (name, old) = Individuals[individualIndex]; - if (Individuals.Delete(individualIndex)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); - } - - public void MoveIndividualCollection(int from, int to) - { - if (Individuals.Move(from, to)) - Penumbra.SaveService.QueueSave(this); - } - - // Obtain the index of a collection by name. - private int GetIndexForCollectionName(string name) - => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - - // Load default, current, special, and character collections from config. - // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections(FilenameService files) - { - var configChanged = !ReadActiveCollections(files, out var jObject); - - // Load the default collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); - var defaultIdx = GetIndexForCollectionName(defaultName); - if (defaultIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning); - Default = Empty; - configChanged = true; - } - else - { - Default = this[defaultIdx]; - } - - // Load the interface collection. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName(interfaceName); - if (interfaceIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", - "Load Failure", NotificationType.Warning); - Interface = Empty; - configChanged = true; - } - else - { - Interface = this[interfaceIdx]; - } - - // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? DefaultCollection; - var currentIdx = GetIndexForCollectionName(currentName); - if (currentIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", - "Load Failure", NotificationType.Warning); - Current = DefaultName; - configChanged = true; - } - else - { - Current = this[currentIdx]; - } - - // Load special collections. - foreach (var (type, name, _) in CollectionTypeExtensions.Special) - { - var typeName = jObject[type.ToString()]?.ToObject(); - if (typeName != null) - { - var idx = GetIndexForCollectionName(typeName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", - "Load Failure", - NotificationType.Warning); - configChanged = true; - } - else - { - _specialCollections[(int)type] = this[idx]; - } - } - } - - configChanged |= MigrateIndividualCollections(jObject); - configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); - - // Save any changes and create all required caches. - if (configChanged) - Penumbra.SaveService.ImmediateSave(this); - } - - // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections(FilenameService fileNames) - { - if (!ReadActiveCollections(fileNames, out var jObject)) - return; - - foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) - { - var oldName = type.ToString()[4..]; - var value = jObject[oldName]; - if (value == null) - continue; - - jObject.Remove(oldName); - jObject.Add("Male" + oldName, value); - jObject.Add("Female" + oldName, value); - } - - using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - jObject.WriteTo(j); - } - - // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject) - { - var version = jObject[nameof(Version)]?.Value() ?? 0; - if (version > 0) - return false; - - // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary(characters.Count); - foreach (var (player, collectionName) in characters) - { - var idx = GetIndexForCollectionName(collectionName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning); - dict.Add(player, Empty); - } - else - { - dict.Add(player, this[idx]); - } - } - - Individuals.Migrate0To1(dict); - return true; - } - - // Read the active collection file into a jObject. - // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(FilenameService files, out JObject ret) - { - var file = files.ActiveCollectionsFile; - if (File.Exists(file)) - try - { - ret = JObject.Parse(File.ReadAllText(file)); - return true; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); - } - - ret = new JObject(); - return false; - } - - // Save if any of the active collections is changed. - private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) - { - if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - Penumbra.SaveService.QueueSave(this); - } - - // Cache handling. Usually recreate caches on the next framework tick, - // but at launch create all of them at once. - public void CreateNecessaryCaches() - { - var tasks = _specialCollections.OfType() - .Concat(Individuals.Select(p => p.Collection)) - .Prepend(Current) - .Prepend(Default) - .Prepend(Interface) - .Distinct() - .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) - .ToArray(); - - Task.WaitAll(tasks); - } - - private void RemoveCache(int idx) - { - if (idx != Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All(c => c == null || c.Index != idx) - && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) - _collections[idx].ClearCache(); - } - - // Recalculate effective files for active collections on events. - private void OnModAddedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.AddMod(mod, true); - } - - private void OnModRemovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.RemoveMod(mod, true); - } - - private void OnModMovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.ReloadMod(mod, true); - } - - public string ToFilename(FilenameService fileNames) - => fileNames.ActiveCollectionsFile; - - public string TypeName - => "Active Collections"; - - public string LogName(string _) - => "to file"; - - public void Save(StreamWriter writer) - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - } - } -} +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Mods; +using Penumbra.UI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public sealed partial class CollectionManager : ISavable +{ + public const int Version = 1; + + // The collection currently selected for changing settings. + public ModCollection Current { get; private set; } = ModCollection.Empty; + + // The collection currently selected is in use either as an active collection or through inheritance. + public bool CurrentCollectionInUse { get; private set; } + + // The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } = ModCollection.Empty; + + // The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } = ModCollection.Empty; + + // A single collection that can not be deleted as a fallback for the current collection. + private ModCollection DefaultName { get; set; } = ModCollection.Empty; + + // The list of character collections. + public readonly IndividualCollections Individuals; + + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; + + // Special Collections + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; + + // Return the configured collection for the given type or null. + // Does not handle Inactive, use ByName instead. + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); + + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) + { + if (type.IsSpecial()) + return _specialCollections[(int)type]; + + return type switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, + _ => null, + }; + } + + // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) + { + var oldCollectionIdx = collectionType switch + { + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, + CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count + ? -1 + : Individuals[individualIndex].Collection.Index, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, + _ => -1, + }; + + if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) + return; + + var newCollection = this[newIdx]; + if (newIdx > ModCollection.Empty.Index) + newCollection.CreateCache(collectionType is CollectionType.Default); + + switch (collectionType) + { + case CollectionType.Default: + Default = newCollection; + break; + case CollectionType.Interface: + Interface = newCollection; + break; + case CollectionType.Current: + Current = newCollection; + break; + case CollectionType.Individual: + if (!Individuals.ChangeCollection(individualIndex, newCollection)) + { + RemoveCache(newIdx); + return; + } + + break; + default: + _specialCollections[(int)collectionType] = newCollection; + break; + } + + RemoveCache(oldCollectionIdx); + + UpdateCurrentCollectionInUse(); + _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); + } + + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = _specialCollections + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + => SetCollection(collection.Index, collectionType, individualIndex); + + // Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) + return false; + + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); + return true; + } + + // Remove a special collection if it exists + public void RemoveSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial()) + return; + + var old = _specialCollections[(int)collectionType]; + if (old != null) + { + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); + } + } + + // Wrappers around Individual Collection handling. + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) + { + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); + } + + public void RemoveIndividualCollection(int individualIndex) + { + if (individualIndex < 0 || individualIndex >= Individuals.Count) + return; + + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); + } + + public void MoveIndividualCollection(int from, int to) + { + if (Individuals.Move(from, to)) + Penumbra.SaveService.QueueSave(this); + } + + // Obtain the index of a collection by name. + private int GetIndexForCollectionName(string name) + => name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name); + + // Load default, current, special, and character collections from config. + // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. + private void LoadCollections(FilenameService files) + { + var configChanged = !ReadActiveCollections(files, out var jObject); + + // Load the default collection. + var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name); + var defaultIdx = GetIndexForCollectionName(defaultName); + if (defaultIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = this[defaultIdx]; + } + + // Load the interface collection. + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceIdx = GetIndexForCollectionName(interfaceName); + if (interfaceIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = this[interfaceIdx]; + } + + // Load the current collection. + var currentName = jObject[nameof(Current)]?.ToObject() ?? ModCollection.DefaultCollection; + var currentIdx = GetIndexForCollectionName(currentName); + if (currentIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.", + "Load Failure", NotificationType.Warning); + Current = DefaultName; + configChanged = true; + } + else + { + Current = this[currentIdx]; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) + { + var idx = GetIndexForCollectionName(typeName); + if (idx < 0) + { + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", + NotificationType.Warning); + configChanged = true; + } + else + { + _specialCollections[(int)type] = this[idx]; + } + } + } + + configChanged |= MigrateIndividualCollections(jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); + + // Save any changes and create all required caches. + if (configChanged) + Penumbra.SaveService.ImmediateSave(this); + } + + // Migrate ungendered collections to Male and Female for 0.5.9.0. + public static void MigrateUngenderedCollections(FilenameService fileNames) + { + if (!ReadActiveCollections(fileNames, out var jObject)) + return; + + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) + { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) + continue; + + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); + } + + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObject.WriteTo(j); + } + + // Migrate individual collections to Identifiers for 0.6.0. + private bool MigrateIndividualCollections(JObject jObject) + { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) + return false; + + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) + { + var idx = GetIndexForCollectionName(collectionName); + if (idx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, ModCollection.Empty); + } + else + { + dict.Add(player, this[idx]); + } + } + + Individuals.Migrate0To1(dict); + return true; + } + + // Read the active collection file into a jObject. + // Returns true if this is successful, false if the file does not exist or it is unsuccessful. + private static bool ReadActiveCollections(FilenameService files, out JObject ret) + { + var file = files.ActiveCollectionsFile; + if (File.Exists(file)) + try + { + ret = JObject.Parse(File.ReadAllText(file)); + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); + } + + ret = new JObject(); + return false; + } + + // Save if any of the active collections is changed. + private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) + { + if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) + Penumbra.SaveService.QueueSave(this); + } + + // Cache handling. Usually recreate caches on the next framework tick, + // but at launch create all of them at once. + public void CreateNecessaryCaches() + { + var tasks = _specialCollections.OfType() + .Concat(Individuals.Select(p => p.Collection)) + .Prepend(Current) + .Prepend(Default) + .Prepend(Interface) + .Distinct() + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) + .ToArray(); + + Task.WaitAll(tasks); + } + + private void RemoveCache(int idx) + { + if (idx != ModCollection.Empty.Index + && idx != Default.Index + && idx != Interface.Index + && idx != Current.Index + && _specialCollections.All(c => c == null || c.Index != idx) + && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) + _collections[idx].ClearCache(); + } + + // Recalculate effective files for active collections on events. + private void OnModAddedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); + } + + private void OnModRemovedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); + } + + private void OnModMovedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 4b883dd3..d03d3ddf 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Collections; -public partial class ModCollection +public sealed partial class CollectionManager : IDisposable, IEnumerable { - public sealed partial class Manager : IDisposable, IEnumerable + private readonly Mods.ModManager _modManager; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly Configuration _config; + + + // The empty collection is always available and always has index 0. + // It can not be deleted or moved. + private readonly List _collections = new() { - private readonly Mod.Manager _modManager; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly Configuration _config; + ModCollection.Empty, + }; + public ModCollection this[Index idx] + => _collections[idx]; - // The empty collection is always available and always has index 0. - // It can not be deleted or moved. - private readonly List _collections = new() + public ModCollection? this[string name] + => ByName(name, out var c) ? c : null; + + public int Count + => _collections.Count; + + // Obtain a collection case-independently by name. + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + + // Default enumeration skips the empty collection. + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public IEnumerable GetEnumeratorWithEmpty() + => _collections; + + public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, + ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals) + { + using var time = timer.Measure(StartTimeType.Collections); + _communicator = communicator; + _characterUtility = characterUtility; + _residentResources = residentResources; + _config = config; + _modManager = modManager; + Individuals = individuals; + + // The collection manager reacts to changes in mods by itself. + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChange; + _communicator.CollectionChange.Event += SaveOnChange; + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + ReadCollections(files); + LoadCollections(files); + UpdateCurrentCollectionInUse(); + CreateNecessaryCaches(); + } + + public void Dispose() + { + _communicator.CollectionChange.Event -= SaveOnChange; + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChange; + } + + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + + // Returns true if the name is not empty, it is not the name of the empty collection + // and no existing collection results in the same filename as name. + public bool CanAddCollection(string name, out string fixedName) + { + if (!ModCollection.IsValidName(name)) { - Empty, - }; - - public ModCollection this[Index idx] - => _collections[idx]; - - public ModCollection? this[string name] - => ByName(name, out var c) ? c : null; - - public int Count - => _collections.Count; - - // Obtain a collection case-independently by name. - public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - - // Default enumeration skips the empty collection. - public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public IEnumerable GetEnumeratorWithEmpty() - => _collections; - - public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals) - { - using var time = timer.Measure(StartTimeType.Collections); - _communicator = communicator; - _characterUtility = characterUtility; - _residentResources = residentResources; - _config = config; - _modManager = manager; - Individuals = individuals; - - // The collection manager reacts to changes in mods by itself. - _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModOptionChanged += OnModOptionsChanged; - _modManager.ModPathChanged += OnModPathChange; - _communicator.CollectionChange.Event += SaveOnChange; - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(files); - LoadCollections(files); - UpdateCurrentCollectionInUse(); - CreateNecessaryCaches(); + fixedName = string.Empty; + return false; } - public void Dispose() + name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if (name.Length == 0 + || name == ModCollection.Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) { - _communicator.CollectionChange.Event -= SaveOnChange; - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; - _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModOptionChanged -= OnModOptionsChanged; - _modManager.ModPathChanged -= OnModPathChange; + fixedName = string.Empty; + return false; } - private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + fixedName = name; + return true; + } - // Returns true if the name is not empty, it is not the name of the empty collection - // and no existing collection results in the same filename as name. - public bool CanAddCollection(string name, out string fixedName) + // Add a new collection of the given name. + // If duplicate is not-null, the new collection will be a duplicate of it. + // If the name of the collection would result in an already existing filename, skip it. + // Returns true if the collection was successfully created and fires a Inactive event. + // Also sets the current collection to the new collection afterwards. + public bool AddCollection(string name, ModCollection? duplicate) + { + if (!CanAddCollection(name, out var fixedName)) { - if (!IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if (name.Length == 0 - || name == Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; + Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); + return false; } - // Add a new collection of the given name. - // If duplicate is not-null, the new collection will be a duplicate of it. - // If the name of the collection would result in an already existing filename, skip it. - // Returns true if the collection was successfully created and fires a Inactive event. - // Also sets the current collection to the new collection afterwards. - public bool AddCollection(string name, ModCollection? duplicate) + var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); + newCollection.Index = _collections.Count; + _collections.Add(newCollection); + + Penumbra.SaveService.ImmediateSave(newCollection); + Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + SetCollection(newCollection.Index, CollectionType.Current); + return true; + } + + // Remove the given collection if it exists and is neither the empty nor the default-named collection. + // If the removed collection was active, it also sets the corresponding collection to the appropriate default. + // Also removes the collection from inheritances of all other collections. + public bool RemoveCollection(int idx) + { + if (idx <= ModCollection.Empty.Index || idx >= _collections.Count) { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); - return false; - } - - var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); - newCollection.Index = _collections.Count; - _collections.Add(newCollection); - - Penumbra.SaveService.ImmediateSave(newCollection); - Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); - SetCollection(newCollection.Index, CollectionType.Current); - return true; + Penumbra.Log.Error("Can not remove the empty collection."); + return false; } - // Remove the given collection if it exists and is neither the empty nor the default-named collection. - // If the removed collection was active, it also sets the corresponding collection to the appropriate default. - // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection(int idx) + if (idx == DefaultName.Index) { - if (idx <= Empty.Index || idx >= _collections.Count) - { - Penumbra.Log.Error("Can not remove the empty collection."); - return false; - } - - if (idx == DefaultName.Index) - { - Penumbra.Log.Error("Can not remove the default collection."); - return false; - } - - if (idx == Current.Index) - SetCollection(DefaultName.Index, CollectionType.Current); - - if (idx == Default.Index) - SetCollection(Empty.Index, CollectionType.Default); - - for (var i = 0; i < _specialCollections.Length; ++i) - { - if (idx == _specialCollections[i]?.Index) - SetCollection(Empty, (CollectionType)i); - } - - for (var i = 0; i < Individuals.Count; ++i) - { - if (Individuals[i].Collection.Index == idx) - SetCollection(Empty, CollectionType.Individual, i); - } - - var collection = _collections[idx]; - - // Clear own inheritances. - foreach (var inheritance in collection.Inheritance) - collection.ClearSubscriptions(inheritance); - - Penumbra.SaveService.ImmediateDelete(collection); - _collections.RemoveAt(idx); - - // Clear external inheritances. - foreach (var c in _collections) - { - var inheritedIdx = c._inheritance.IndexOf(collection); - if (inheritedIdx >= 0) - c.RemoveInheritance(inheritedIdx); - - if (c.Index > idx) - --c.Index; - } - - Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); - return true; + Penumbra.Log.Error("Can not remove the default collection."); + return false; } - public bool RemoveCollection(ModCollection collection) - => RemoveCollection(collection.Index); + if (idx == Current.Index) + SetCollection(DefaultName.Index, CollectionType.Current); - private void OnModDiscoveryStarted() + if (idx == Default.Index) + SetCollection(ModCollection.Empty.Index, CollectionType.Default); + + for (var i = 0; i < _specialCollections.Length; ++i) { - foreach (var collection in this) - collection.PrepareModDiscovery(); + if (idx == _specialCollections[i]?.Index) + SetCollection(ModCollection.Empty, (CollectionType)i); } - private void OnModDiscoveryFinished() + for (var i = 0; i < Individuals.Count; ++i) { - // First, re-apply all mod settings. - foreach (var collection in this) - collection.ApplyModSettings(); - - // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach (var collection in this.Where(c => c.HasCache)) - collection.ForceCacheUpdate(); + if (Individuals[i].Collection.Index == idx) + SetCollection(ModCollection.Empty, CollectionType.Individual, i); } + var collection = _collections[idx]; - // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) + // Clear own inheritances. + foreach (var inheritance in collection.Inheritance) + collection.ClearSubscriptions(inheritance); + + Penumbra.SaveService.ImmediateDelete(collection); + _collections.RemoveAt(idx); + + // Clear external inheritances. + foreach (var c in _collections) { - switch (type) - { - case ModPathChangeType.Added: - foreach (var collection in this) - collection.AddMod(mod); + var inheritedIdx = c._inheritance.IndexOf(collection); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); - OnModAddedActive(mod); - break; - case ModPathChangeType.Deleted: - OnModRemovedActive(mod); - foreach (var collection in this) - collection.RemoveMod(mod, mod.Index); - - break; - case ModPathChangeType.Moved: - OnModMovedActive(mod); - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - Penumbra.SaveService.QueueSave(collection); - - break; - case ModPathChangeType.StartingReload: - OnModRemovedActive(mod); - break; - case ModPathChangeType.Reloaded: - OnModAddedActive(mod); - break; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + if (c.Index > idx) + --c.Index; } - // Automatically update all relevant collections when a mod is changed. - // This means saving if options change in a way where the settings may change and the collection has settings for this mod. - // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); + return true; + } + + public bool RemoveCollection(ModCollection collection) + => RemoveCollection(collection.Index); + + private void OnModDiscoveryStarted() + { + foreach (var collection in this) + collection.PrepareModDiscovery(); + } + + private void OnModDiscoveryFinished() + { + // First, re-apply all mod settings. + foreach (var collection in this) + collection.ApplyModSettings(); + + // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. + foreach (var collection in this.Where(c => c.HasCache)) + collection.ForceCacheUpdate(); + } + + + // A changed mod path forces changes for all collections, active and inactive. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) { - // Handle changes that break revertability. - if (type == ModOptionChangeType.PrepareChange) - { - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - collection._cache!.RemoveMod(mod, false); - } - - return; - } - - type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); - - // Handle changes that require overwriting the collection. - if (requiresSaving) + case ModPathChangeType.Added: foreach (var collection in this) - { - if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - Penumbra.SaveService.QueueSave(collection); - } + collection.AddMod(mod); - // Handle changes that reload the mod if the changes did not need to be prepared, - // or re-add the mod if they were prepared. - if (recomputeList) - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - { - if (reload) - collection._cache!.ReloadMod(mod, true); - else - collection._cache!.AddMod(mod, true); - } - } - } + OnModAddedActive(mod); + break; + case ModPathChangeType.Deleted: + OnModRemovedActive(mod); + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); - // Add the collection with the default name if it does not exist. - // It should always be ensured that it exists, otherwise it will be created. - // This can also not be deleted, so there are always at least the empty and a collection with default name. - private void AddDefaultCollection() - { - var idx = GetIndexForCollectionName(DefaultCollection); - if (idx >= 0) - { - DefaultName = this[idx]; - return; - } + break; + case ModPathChangeType.Moved: + OnModMovedActive(mod); + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + Penumbra.SaveService.QueueSave(collection); - var defaultCollection = CreateNewEmpty(DefaultCollection); - Penumbra.SaveService.ImmediateSave(defaultCollection); - defaultCollection.Index = _collections.Count; - _collections.Add(defaultCollection); - } - - // Inheritances can not be setup before all collections are read, - // so this happens after reading the collections. - private void ApplyInheritances(IEnumerable> inheritances) - { - foreach (var (collection, inheritance) in this.Zip(inheritances)) - { - var changes = false; - foreach (var subCollectionName in inheritance) - { - if (!ByName(subCollectionName, out var subCollection)) - { - changes = true; - Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); - } - else if (!collection.AddInheritance(subCollection, false)) - { - changes = true; - Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); - } - } - - if (changes) - Penumbra.SaveService.ImmediateSave(collection); - } - } - - // Read all collection files in the Collection Directory. - // Ensure that the default named collection exists, and apply inheritances afterwards. - // Duplicate collection files are not deleted, just not added here. - private void ReadCollections(FilenameService files) - { - var inheritances = new List>(); - foreach (var file in files.CollectionFiles) - { - var collection = LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } - } - - AddDefaultCollection(); - ApplyInheritances(inheritances); - } - - public string RedundancyCheck(CollectionType type, ActorIdentifier id) - { - var checkAssignment = ByType(type, id); - if (checkAssignment == null) - return string.Empty; - - switch (type) - { - // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. - case CollectionType.Individual: - switch (id.Type) - { - case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: - { - var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." - : string.Empty; - } - case IdentifierType.Owned: - if (id.HomeWorld != ushort.MaxValue) - { - var global = ByType(CollectionType.Individual, - Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) - return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; - } - - var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." - : string.Empty; - } - break; - // The group of all Characters is redundant if they are all equal to Default or unassigned. - case CollectionType.MalePlayerCharacter: - case CollectionType.MaleNonPlayerCharacter: - case CollectionType.FemalePlayerCharacter: - case CollectionType.FemaleNonPlayerCharacter: - var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; - var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; - var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; - var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; - if (first.Index == second.Index - && first.Index == third.Index - && first.Index == fourth.Index - && first.Index == Default.Index) - return - "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" - + "You can keep just the Default Assignment."; - - break; - // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. - case CollectionType.NonPlayerChild: - case CollectionType.NonPlayerElderly: - var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); - var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); - var collection1 = CollectionType.MaleNonPlayerCharacter; - var collection2 = CollectionType.FemaleNonPlayerCharacter; - if (maleNpc == null) - { - maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection1 = CollectionType.Default; - } - - if (femaleNpc == null) - { - femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection2 = CollectionType.Default; - } - - return collection1 == collection2 - ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." - : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; - - // For other assignments, check the inheritance order, unassigned means fall-through, - // assigned needs identical assignments to be redundant. - default: - var group = type.InheritanceOrder(); - foreach (var parentType in group) - { - var assignment = ByType(parentType); - if (assignment == null) - continue; - - if (assignment.Index == checkAssignment.Index) - return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; - } - - break; - } - - return string.Empty; + break; + case ModPathChangeType.StartingReload: + OnModRemovedActive(mod); + break; + case ModPathChangeType.Reloaded: + OnModAddedActive(mod); + break; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } + + // Automatically update all relevant collections when a mod is changed. + // This means saving if options change in a way where the settings may change and the collection has settings for this mod. + // And also updating effective file and meta manipulation lists if necessary. + private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + // Handle changes that break revertability. + if (type == ModOptionChangeType.PrepareChange) + { + foreach (var collection in this.Where(c => c.HasCache)) + { + if (collection[mod.Index].Settings is { Enabled: true }) + collection._cache!.RemoveMod(mod, false); + } + + return; + } + + type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); + + // Handle changes that require overwriting the collection. + if (requiresSaving) + foreach (var collection in this) + { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + Penumbra.SaveService.QueueSave(collection); + } + + // Handle changes that reload the mod if the changes did not need to be prepared, + // or re-add the mod if they were prepared. + if (recomputeList) + foreach (var collection in this.Where(c => c.HasCache)) + { + if (collection[mod.Index].Settings is { Enabled: true }) + { + if (reload) + collection._cache!.ReloadMod(mod, true); + else + collection._cache!.AddMod(mod, true); + } + } + } + + // Add the collection with the default name if it does not exist. + // It should always be ensured that it exists, otherwise it will be created. + // This can also not be deleted, so there are always at least the empty and a collection with default name. + private void AddDefaultCollection() + { + var idx = GetIndexForCollectionName(ModCollection.DefaultCollection); + if (idx >= 0) + { + DefaultName = this[idx]; + return; + } + + var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection); + Penumbra.SaveService.ImmediateSave(defaultCollection); + defaultCollection.Index = _collections.Count; + _collections.Add(defaultCollection); + } + + // Inheritances can not be setup before all collections are read, + // so this happens after reading the collections. + private void ApplyInheritances(IEnumerable> inheritances) + { + foreach (var (collection, inheritance) in this.Zip(inheritances)) + { + var changes = false; + foreach (var subCollectionName in inheritance) + { + if (!ByName(subCollectionName, out var subCollection)) + { + changes = true; + Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); + } + else if (!collection.AddInheritance(subCollection, false)) + { + changes = true; + Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); + } + } + + if (changes) + Penumbra.SaveService.ImmediateSave(collection); + } + } + + // Read all collection files in the Collection Directory. + // Ensure that the default named collection exists, and apply inheritances afterwards. + // Duplicate collection files are not deleted, just not added here. + private void ReadCollections(FilenameService files) + { + var inheritances = new List>(); + foreach (var file in files.CollectionFiles) + { + var collection = ModCollection.LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); + + if (this[collection.Name] != null) + { + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); + } + else + { + inheritances.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + } + + AddDefaultCollection(); + ApplyInheritances(inheritances); + } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } } diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 2dd67e3c..498688ed 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -26,7 +26,7 @@ public partial class IndividualCollections return ret; } - public bool ReadJObject( JArray? obj, ModCollection.Manager manager ) + public bool ReadJObject( JArray? obj, CollectionManager manager ) { if( obj == null ) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 37bfafd5..355f17b3 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; -using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -20,27 +19,27 @@ namespace Penumbra.Collections; public partial class ModCollection { // Only active collections need to have a cache. - private Cache? _cache; + internal ModCollectionCache? _cache; public bool HasCache => _cache != null; // Count the number of changes of the effective file list. // This is used for material and imc changes. - public int ChangeCounter { get; private set; } + public int ChangeCounter { get; internal set; } // Only create, do not update. - private void CreateCache(bool isDefault) + internal void CreateCache(bool isDefault) { - if (_cache == null) - { - CalculateEffectiveFileList(isDefault); - Penumbra.Log.Verbose($"Created new cache for collection {Name}."); - } + if (_cache != null) + return; + + CalculateEffectiveFileList(isDefault); + Penumbra.Log.Verbose($"Created new cache for collection {Name}."); } // Force an update with metadata for this cache. - private void ForceCacheUpdate() + internal void ForceCacheUpdate() => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); // Handle temporary mods for this collection. @@ -83,7 +82,7 @@ public partial class ModCollection } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) + internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) { if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) return true; @@ -127,14 +126,14 @@ public partial class ModCollection => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => CalculateEffectiveFileListInternal(isDefault)); - private void CalculateEffectiveFileListInternal(bool isDefault) + internal void CalculateEffectiveFileListInternal(bool isDefault) { // Skip the empty collection. if (Index == 0) return; Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); - _cache ??= new Cache(this); + _cache ??= new ModCollectionCache(this); _cache.FullRecalculation(isDefault); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index cd148344..e25f65bf 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -15,532 +15,531 @@ namespace Penumbra.Collections; public record struct ModPath( IMod Mod, FullPath Path ); public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); -public partial class ModCollection +/// +/// The Cache contains all required temporary data to use a collection. +/// It will only be setup if a collection gets activated in any way. +/// +internal class ModCollectionCache : IDisposable { - // The Cache contains all required temporary data to use a collection. - // It will only be setup if a collection gets activated in any way. - private class Cache : IDisposable + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); + + public IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _conflicts.Values; + + public SingleArray< ModConflicts > Conflicts( IMod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + + private int _changedItemsSaveCounter = -1; + + // Obtain currently changed items. Computes them if they haven't been computed before. + public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems { - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); - - public IEnumerable< SingleArray< ModConflicts > > AllConflicts - => _conflicts.Values; - - public SingleArray< ModConflicts > Conflicts( IMod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); - - private int _changedItemsSaveCounter = -1; - - // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems + get { - get + SetChangedItems(); + return _changedItems; + } + } + + // The cache reacts through events on its collection changing. + public ModCollectionCache( ModCollection collection ) + { + _collection = collection; + MetaManipulations = new MetaManager( _collection ); + _collection.ModSettingChanged += OnModSettingChange; + _collection.InheritanceChanged += OnInheritanceChange; + if( !Penumbra.CharacterUtility.Ready ) + { + Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; + } + } + + public void Dispose() + { + MetaManipulations.Dispose(); + _collection.ModSettingChanged -= OnModSettingChange; + _collection.InheritanceChanged -= OnInheritanceChange; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + // Resolve a given game path according to this collection. + public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.Path.IsRooted && !candidate.Path.Exists ) + { + return null; + } + + return candidate.Path; + } + + // For a given full path, find all game paths that currently use this file. + public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) + { + var needle = localFilePath.FullName.ToLower(); + if( localFilePath.IsRooted ) + { + needle = needle.Replace( '/', '\\' ); + } + + var iterator = ResolvedFiles + .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) + .Select( kvp => kvp.Key ); + + // For files that are not rooted, try to add themselves. + if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) + { + iterator = iterator.Prepend( utf8 ); + } + + return iterator; + } + + // Reverse resolve multiple paths at once for efficiency. + public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) + { + if( fullPaths.Count == 0 ) + return Array.Empty< HashSet< Utf8GamePath > >(); + + var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; + var dict = new Dictionary< FullPath, int >( fullPaths.Count ); + foreach( var (path, idx) in fullPaths.WithIndex() ) + { + dict[ new FullPath(path) ] = idx; + ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) + ? new HashSet< Utf8GamePath > { utf8 } + : new HashSet< Utf8GamePath >(); + } + + foreach( var (game, full) in ResolvedFiles ) + { + if( dict.TryGetValue( full.Path, out var idx ) ) { - SetChangedItems(); - return _changedItems; + ret[ idx ].Add( game ); } } - // The cache reacts through events on its collection changing. - public Cache( ModCollection collection ) + return ret; + } + + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) + { + switch( type ) { - _collection = collection; - MetaManipulations = new MetaManager( _collection ); - _collection.ModSettingChanged += OnModSettingChange; - _collection.InheritanceChanged += OnInheritanceChange; - if( !Penumbra.CharacterUtility.Ready ) - { - Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; - } - } - - public void Dispose() - { - MetaManipulations.Dispose(); - _collection.ModSettingChanged -= OnModSettingChange; - _collection.InheritanceChanged -= OnInheritanceChange; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - - // Resolve a given game path according to this collection. - public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) - { - return null; - } - - return candidate.Path; - } - - // For a given full path, find all game paths that currently use this file. - public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) - { - var needle = localFilePath.FullName.ToLower(); - if( localFilePath.IsRooted ) - { - needle = needle.Replace( '/', '\\' ); - } - - var iterator = ResolvedFiles - .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) - .Select( kvp => kvp.Key ); - - // For files that are not rooted, try to add themselves. - if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) - { - iterator = iterator.Prepend( utf8 ); - } - - return iterator; - } - - // Reverse resolve multiple paths at once for efficiency. - public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) - { - if( fullPaths.Count == 0 ) - return Array.Empty< HashSet< Utf8GamePath > >(); - - var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; - var dict = new Dictionary< FullPath, int >( fullPaths.Count ); - foreach( var (path, idx) in fullPaths.WithIndex() ) - { - dict[ new FullPath(path) ] = idx; - ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) - ? new HashSet< Utf8GamePath > { utf8 } - : new HashSet< Utf8GamePath >(); - } - - foreach( var (game, full) in ResolvedFiles ) - { - if( dict.TryGetValue( full.Path, out var idx ) ) + case ModSettingChange.Inheritance: + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + break; + case ModSettingChange.EnableState: + if( oldValue == 0 ) { - ret[ idx ].Add( game ); + 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 ); + } + + break; + case ModSettingChange.Priority: + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + } + + break; + case ModSettingChange.Setting: + if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + } + + break; + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + FullRecalculation(_collection == Penumbra.CollectionManager.Default); + break; + } + } + + // Inheritance changes are too big to check for relevance, + // just recompute everything. + private void OnInheritanceChange( bool _ ) + => FullRecalculation(_collection == Penumbra.CollectionManager.Default); + + public void FullRecalculation(bool isDefault) + { + ResolvedFiles.Clear(); + MetaManipulations.Reset(); + _conflicts.Clear(); + + // Add all forced redirects. + foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( + Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) + { + AddMod( tempMod, false ); + } + + foreach( var mod in Penumbra.ModManager ) + { + AddMod( mod, false ); + } + + AddMetaFiles(); + + ++_collection.ChangeCounter; + + if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + + public void ReloadMod( IMod mod, bool addMetaChanges ) + { + RemoveMod( mod, addMetaChanges ); + AddMod( mod, addMetaChanges ); + } + + public void RemoveMod( IMod mod, bool addMetaChanges ) + { + var conflicts = Conflicts( mod ); + + foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) + { + if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) + { + continue; + } + + if( modPath.Mod == mod ) + { + ResolvedFiles.Remove( path ); + } + } + + foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) + { + if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) + { + MetaManipulations.RevertMod( manipulation ); + } + } + + _conflicts.Remove( mod ); + foreach( var conflict in conflicts ) + { + if( conflict.HasPriority ) + { + ReloadMod( conflict.Mod2, false ); + } + else + { + var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); + if( newConflicts.Count > 0 ) + { + _conflicts[ conflict.Mod2 ] = newConflicts; + } + else + { + _conflicts.Remove( conflict.Mod2 ); } } - - return ret; } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) + if( addMetaChanges ) { - switch( type ) - { - case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - break; - case ModSettingChange.EnableState: - if( oldValue == 0 ) - { - 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 ); - } - - break; - case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.MultiInheritance: - case ModSettingChange.MultiEnableState: - FullRecalculation(_collection == Penumbra.CollectionManager.Default); - break; - } - } - - // Inheritance changes are too big to check for relevance, - // just recompute everything. - private void OnInheritanceChange( bool _ ) - => FullRecalculation(_collection == Penumbra.CollectionManager.Default); - - public void FullRecalculation(bool isDefault) - { - ResolvedFiles.Clear(); - MetaManipulations.Reset(); - _conflicts.Clear(); - - // Add all forced redirects. - foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( - Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) - { - AddMod( tempMod, false ); - } - - foreach( var mod in Penumbra.ModManager ) - { - AddMod( mod, false ); - } - - AddMetaFiles(); - ++_collection.ChangeCounter; - - if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); } } + } - public void ReloadMod( IMod mod, bool addMetaChanges ) + + // Add all files and possibly manipulations of a given mod according to its settings in this collection. + public void AddMod( IMod mod, bool addMetaChanges ) + { + if( mod.Index >= 0 ) { - RemoveMod( mod, addMetaChanges ); - AddMod( mod, addMetaChanges ); - } - - public void RemoveMod( IMod mod, bool addMetaChanges ) - { - var conflicts = Conflicts( mod ); - - foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) { - if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) + return; + } + + foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) + { + if( group.Count == 0 ) { continue; } - if( modPath.Mod == mod ) + var config = settings.Settings[ groupIndex ]; + switch( group.Type ) { - ResolvedFiles.Remove( path ); - } - } - - foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) - { - if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) - { - MetaManipulations.RevertMod( manipulation ); - } - } - - _conflicts.Remove( mod ); - foreach( var conflict in conflicts ) - { - if( conflict.HasPriority ) - { - ReloadMod( conflict.Mod2, false ); - } - else - { - var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); - if( newConflicts.Count > 0 ) + case GroupType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case GroupType.Multi: { - _conflicts[ conflict.Mod2 ] = newConflicts; - } - else - { - _conflicts.Remove( conflict.Mod2 ); - } - } - } - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - } - - - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod( IMod mod, bool addMetaChanges ) - { - if( mod.Index >= 0 ) - { - var settings = _collection[ mod.Index ].Settings; - if( settings is not { Enabled: true } ) - { - return; - } - - foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) - { - if( group.Count == 0 ) - { - continue; - } - - var config = settings.Settings[ groupIndex ]; - switch( group.Type ) - { - case GroupType.Single: - AddSubMod( group[ ( int )config ], mod ); - break; - case GroupType.Multi: + foreach( var (option, _) in group.WithIndex() + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) { - foreach( var (option, _) in group.WithIndex() - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) - { - AddSubMod( option, mod ); - } - - break; + AddSubMod( option, mod ); } + + break; } } } - - AddSubMod( mod.Default, mod ); - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( mod.TotalManipulations > 0 ) - { - AddMetaFiles(); - } - - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } } - // Add all files and possibly manipulations of a specific submod - private void AddSubMod( ISubMod subMod, IMod parentMod ) - { - foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) - { - AddFile( path, file, parentMod ); - } + AddSubMod( mod.Default, mod ); - foreach( var manip in subMod.Manipulations ) - { - AddManipulation( manip, parentMod ); - } - } - - // Add a specific file redirection, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) - { - if( !CheckFullPath( path, file ) ) - { - return; - } - - if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) - { - return; - } - - var modPath = ResolvedFiles[ path ]; - // Lower prioritized option in the same mod. - if( mod == modPath.Mod ) - { - return; - } - - if( AddConflict( path, mod, modPath.Mod ) ) - { - ResolvedFiles[ path ] = new ModPath( mod, file ); - } - } - - - // Remove all empty conflict sets for a given mod with the given conflicts. - // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) - { - var changedConflicts = oldConflicts.Remove( c => - { - if( c.Conflicts.Count == 0 ) - { - if( transitive ) - { - RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); - } - - return true; - } - - return false; - } ); - if( changedConflicts.Count == 0 ) - { - _conflicts.Remove( mod ); - } - else - { - _conflicts[ mod ] = changedConflicts; - } - } - - // Add a new conflict between the added mod and the existing mod. - // Update all other existing conflicts between the existing mod and other mods if necessary. - // Returns if the added mod takes priority before the existing mod. - private bool AddConflict( object data, IMod addedMod, IMod existingMod ) - { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; - - if( existingPriority < addedPriority ) - { - var tmpConflicts = Conflicts( existingMod ); - foreach( var conflict in tmpConflicts ) - { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) - { - AddConflict( data, addedMod, conflict.Mod2 ); - } - } - - RemoveEmptyConflicts( existingMod, tmpConflicts, true ); - } - - var addedConflicts = Conflicts( addedMod ); - var existingConflicts = Conflicts( existingMod ); - if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) - { - // Only need to change one list since both conflict lists refer to the same list. - oldConflicts.Conflicts.Add( data ); - } - else - { - // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, - existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, - existingPriority >= addedPriority, - existingPriority != addedPriority ) ); - } - - return existingPriority < addedPriority; - } - - // Add a specific manipulation, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, IMod mod ) - { - if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - return; - } - - // Lower prioritized option in the same mod. - if( mod == existingMod ) - { - return; - } - - if( AddConflict( manip, mod, existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - } - - - // Add all necessary meta file redirects. - private void AddMetaFiles() - => MetaManipulations.SetImcFiles(); - - // Increment the counter to ensure new files are loaded after applying meta changes. - private void IncrementCounter() + if( addMetaChanges ) { ++_collection.ChangeCounter; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - - - // Identify and record all manipulated objects for this entire collection. - private void SetChangedItems() - { - if( _changedItemsSaveCounter == _collection.ChangeCounter ) + if( mod.TotalManipulations > 0 ) { - return; + AddMetaFiles(); } - try + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { - _changedItemsSaveCounter = _collection.ChangeCounter; - _changedItems.Clear(); - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = Penumbra.Identifier; - var items = new SortedList< string, object? >( 512 ); - - void AddItems( IMod mod ) - { - foreach( var (name, obj) in items ) - { - if( !_changedItems.TryGetValue( name, out var data ) ) - { - _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); - } - else if( !data.Item1.Contains( mod ) ) - { - _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); - } - else if( obj is int x && data.Item2 is int y ) - { - _changedItems[ name ] = ( data.Item1, x + y ); - } - } - - items.Clear(); - } - - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) - { - identifier.Identify( items, resolved.ToString() ); - AddItems( modPath.Mod ); - } - - foreach( var (manip, mod) in MetaManipulations ) - { - Mod.ComputeChangedItems( items, manip ); - AddItems( mod ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Unknown Error:\n{e}" ); + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); } } } + + // Add all files and possibly manipulations of a specific submod + private void AddSubMod( ISubMod subMod, IMod parentMod ) + { + foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) + { + AddFile( path, file, parentMod ); + } + + foreach( var manip in subMod.Manipulations ) + { + AddManipulation( manip, parentMod ); + } + } + + // Add a specific file redirection, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) + { + if( !ModCollection.CheckFullPath( path, file ) ) + { + return; + } + + if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) + { + return; + } + + var modPath = ResolvedFiles[ path ]; + // Lower prioritized option in the same mod. + if( mod == modPath.Mod ) + { + return; + } + + if( AddConflict( path, mod, modPath.Mod ) ) + { + ResolvedFiles[ path ] = new ModPath( mod, file ); + } + } + + + // Remove all empty conflict sets for a given mod with the given conflicts. + // If transitive is true, also removes the corresponding version of the other mod. + private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + { + var changedConflicts = oldConflicts.Remove( c => + { + if( c.Conflicts.Count == 0 ) + { + if( transitive ) + { + RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); + } + + return true; + } + + return false; + } ); + if( changedConflicts.Count == 0 ) + { + _conflicts.Remove( mod ); + } + else + { + _conflicts[ mod ] = changedConflicts; + } + } + + // Add a new conflict between the added mod and the existing mod. + // Update all other existing conflicts between the existing mod and other mods if necessary. + // Returns if the added mod takes priority before the existing mod. + private bool AddConflict( object data, IMod addedMod, IMod existingMod ) + { + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; + + if( existingPriority < addedPriority ) + { + var tmpConflicts = Conflicts( existingMod ); + foreach( var conflict in tmpConflicts ) + { + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + { + AddConflict( data, addedMod, conflict.Mod2 ); + } + } + + RemoveEmptyConflicts( existingMod, tmpConflicts, true ); + } + + var addedConflicts = Conflicts( addedMod ); + var existingConflicts = Conflicts( existingMod ); + if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) + { + // Only need to change one list since both conflict lists refer to the same list. + oldConflicts.Conflicts.Add( data ); + } + else + { + // Add the same conflict list to both conflict directions. + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + existingPriority != addedPriority ) ); + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + existingPriority >= addedPriority, + existingPriority != addedPriority ) ); + } + + return existingPriority < addedPriority; + } + + // Add a specific manipulation, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddManipulation( MetaManipulation manip, IMod mod ) + { + if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + return; + } + + // Lower prioritized option in the same mod. + if( mod == existingMod ) + { + return; + } + + if( AddConflict( manip, mod, existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + } + } + + + // Add all necessary meta file redirects. + private void AddMetaFiles() + => MetaManipulations.SetImcFiles(); + + // Increment the counter to ensure new files are loaded after applying meta changes. + private void IncrementCounter() + { + ++_collection.ChangeCounter; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + + // Identify and record all manipulated objects for this entire collection. + private void SetChangedItems() + { + if( _changedItemsSaveCounter == _collection.ChangeCounter ) + { + return; + } + + try + { + _changedItemsSaveCounter = _collection.ChangeCounter; + _changedItems.Clear(); + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = Penumbra.Identifier; + var items = new SortedList< string, object? >( 512 ); + + void AddItems( IMod mod ) + { + foreach( var (name, obj) in items ) + { + if( !_changedItems.TryGetValue( name, out var data ) ) + { + _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); + } + else if( !data.Item1.Contains( mod ) ) + { + _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + } + else if( obj is int x && data.Item2 is int y ) + { + _changedItems[ name ] = ( data.Item1, x + y ); + } + } + + items.Clear(); + } + + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) + { + identifier.Identify( items, resolved.ToString() ); + AddItems( modPath.Mod ); + } + + foreach( var (manip, mod) in MetaManipulations ) + { + Mod.ComputeChangedItems( items, manip ); + AddItems( mod ); + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Unknown Error:\n{e}" ); + } + } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index c2516519..dd632f71 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -15,7 +15,7 @@ public partial class ModCollection : ISavable { // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. - private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) + internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) { inheritance = Array.Empty(); if (!file.Exists) diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index d9eecdbd..4c694f88 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -16,7 +16,7 @@ public partial class ModCollection // The bool signifies whether the change was in an already inherited collection. public event Action< bool > InheritanceChanged; - private readonly List< ModCollection > _inheritance = new(); + internal readonly List< ModCollection > _inheritance = new(); public IReadOnlyList< ModCollection > Inheritance => _inheritance; @@ -98,7 +98,7 @@ public partial class ModCollection Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." ); } - private void ClearSubscriptions( ModCollection other ) + internal void ClearSubscriptions( ModCollection other ) { other.ModSettingChanged -= OnInheritedModSettingChange; other.InheritanceChanged -= OnInheritedInheritanceChange; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 54f1f9a8..6215dc03 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -23,18 +23,18 @@ public partial class ModCollection // The collection name can contain invalid path characters, // but after removing those and going to lower case it has to be unique. - public string Name { get; private init; } + public string Name { get; internal init; } // Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; - public int Version { get; private set; } - public int Index { get; private set; } = -1; + public int Version { get; internal set; } + public int Index { get; internal set; } = -1; // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List _settings; + internal readonly List _settings; public IReadOnlyList Settings => _settings; @@ -115,7 +115,7 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private bool AddMod(Mod mod) + internal bool AddMod(Mod mod) { if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) { @@ -130,7 +130,7 @@ public partial class ModCollection } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod(Mod mod, int idx) + internal void RemoveMod(Mod mod, int idx) { var settings = _settings[idx]; if (settings != null) @@ -150,7 +150,7 @@ public partial class ModCollection } // Move all settings to unused settings for rediscovery. - private void PrepareModDiscovery() + internal void PrepareModDiscovery() { foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); @@ -160,7 +160,7 @@ public partial class ModCollection // Apply all mod settings from unused settings to the current set of mods. // Also fixes invalid settings. - private void ApplyModSettings() + internal void ApplyModSettings() { _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 97abbbef..bb1ce79a 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -27,12 +27,12 @@ public class CommandHandler : IDisposable private readonly Configuration _config; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; - private readonly Mod.Manager _modManager; - private readonly ModCollection.Manager _collectionManager; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; private readonly Penumbra _penumbra; public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, - ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra) + ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra) { _commandManager = commandManager; _redrawService = redrawService; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 974c8b2e..56b39f6e 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable private readonly Configuration _config; private readonly ModEditor _editor; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, - Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager) + Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager) { _baseDirectory = baseDirectory; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index e4a7a0a9..cb271d7e 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -31,12 +31,12 @@ public unsafe class CollectionResolver private readonly CutsceneService _cutscenes; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly DrawObjectState _drawObjectState; public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, - DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager, + DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, DrawObjectState drawObjectState) { _performance = performance; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index ea1bb2b1..734971c5 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -16,7 +16,7 @@ public class PathResolver : IDisposable { private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly ResourceLoader _loader; @@ -25,7 +25,7 @@ public class PathResolver : IDisposable private readonly PathState _pathState; private readonly MetaState _metaState; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, + public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, PathState pathState, MetaState metaState) { diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 8f480851..2d3026ba 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -11,12 +11,12 @@ namespace Penumbra.Mods; public class DuplicateManager { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly SHA256 _hasher = SHA256.Create(); private readonly ModFileCollection _files; private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - public DuplicateManager(ModFileCollection files, Mod.Manager modManager) + public DuplicateManager(ModFileCollection files, ModManager modManager) { _files = files; _modManager = modManager; @@ -80,7 +80,7 @@ public class DuplicateManager } else { - var sub = (Mod.SubMod)subMod; + var sub = (SubMod)subMod; sub.FileData = dict; if (groupIdx == -1) mod.SaveDefaultMod(); diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 21f8792b..e2ab4994 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -10,12 +10,12 @@ public class ModBackup { public static bool CreatingBackup { get; private set; } - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly Mod _mod; public readonly string Name; public readonly bool Exists; - public ModBackup(Mod.Manager modManager, Mod mod) + public ModBackup(ModManager modManager, Mod mod) { _modManager = modManager; _mod = mod; @@ -24,9 +24,9 @@ public class ModBackup } /// Migrate file extensions. - public static void MigrateZipToPmp(Mod.Manager manager) + public static void MigrateZipToPmp(ModManager modManager) { - foreach (var mod in manager) + foreach (var mod in modManager) { var pmpName = mod.ModPath + ".pmp"; var zipName = mod.ModPath + ".zip"; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 5215973c..29a06c44 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -9,11 +9,11 @@ namespace Penumbra.Mods; public class ModFileEditor { private readonly ModFileCollection _files; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; public bool Changes { get; private set; } - public ModFileEditor(ModFileCollection files, Mod.Manager modManager) + public ModFileEditor(ModFileCollection files, ModManager modManager) { _files = files; _modManager = modManager; @@ -24,7 +24,7 @@ public class ModFileEditor Changes = false; } - public int Apply(Mod mod, Mod.SubMod option) + public int Apply(Mod mod, SubMod option) { var dict = new Dictionary(); var num = 0; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index a211398b..f536935d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -6,7 +6,7 @@ namespace Penumbra.Mods; public class ModMetaEditor { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly HashSet _imc = new(); private readonly HashSet _eqp = new(); @@ -15,7 +15,7 @@ public class ModMetaEditor private readonly HashSet _est = new(); private readonly HashSet _rsp = new(); - public ModMetaEditor(Mod.Manager modManager) + public ModMetaEditor(ModManager modManager) => _modManager = modManager; public bool Changes { get; private set; } = false; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9fc02d77..aff491c7 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -11,7 +11,7 @@ namespace Penumbra.Mods; public class ModNormalizer { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -24,7 +24,7 @@ public class ModNormalizer public bool Running => Step < TotalSteps; - public ModNormalizer(Mod.Manager modManager) + public ModNormalizer(ModManager modManager) => _modManager = modManager; public void Normalize(Mod mod) @@ -177,7 +177,7 @@ public class ModNormalizer _redirections[groupIdx + 1].Add(new Dictionary()); var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); - foreach (var option in group.OfType()) + foreach (var option in group.OfType()) { var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name); @@ -279,7 +279,7 @@ public class ModNormalizer private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods.OfType()) + foreach (var option in Mod.AllSubMods.OfType()) { _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0237d08f..29da93c1 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -5,13 +5,13 @@ using Penumbra.Util; public class ModSwapEditor { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly Dictionary _swaps = new(); public IReadOnlyDictionary Swaps => _swaps; - public ModSwapEditor(Mod.Manager modManager) + public ModSwapEditor(ModManager modManager) => _modManager = modManager; public void Revert(ISubMod option) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 65e3e10f..96e206fa 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -4,202 +4,199 @@ using System.Linq; namespace Penumbra.Mods; -public partial class Mod +public partial class ModManager { - public partial class Manager + public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory); + + public event ModPathChangeDelegate ModPathChanged; + + // Rename/Move a mod directory. + // Updates all collection settings and sort order settings. + public void MoveModDirectory(int idx, string newName) { - public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory); + var mod = this[idx]; + var oldName = mod.Name; + var oldDirectory = mod.ModPath; - public event ModPathChangeDelegate ModPathChanged; - - // Rename/Move a mod directory. - // Updates all collection settings and sort order settings. - public void MoveModDirectory(int idx, string newName) + switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) { - var mod = this[idx]; - var oldName = mod.Name; - var oldDirectory = mod.ModPath; - - switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) - { - case NewDirectoryState.NonExisting: - // Nothing to do - break; - case NewDirectoryState.ExistsEmpty: - try - { - Directory.Delete(dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); - return; - } - - break; - // Should be caught beforehand. - case NewDirectoryState.ExistsNonEmpty: - case NewDirectoryState.ExistsAsFile: - case NewDirectoryState.ContainsInvalidSymbols: - // Nothing to do at all. - case NewDirectoryState.Identical: - default: - return; - } - - try - { - Directory.Move(oldDirectory.FullName, dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); - return; - } - - DataEditor.MoveDataFile(oldDirectory, dir); - new ModBackup(this, mod).Move(null, dir.Name); - - dir.Refresh(); - mod.ModPath = dir; - if (!mod.Reload(this, false, out var metaChange)) - { - Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Reload a mod without changing its base directory. - /// If the base directory does not exist anymore, the mod will be deleted. - /// - public void ReloadMod(int idx) - { - var mod = this[idx]; - var oldName = mod.Name; - - ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!mod.Reload(this, true, out var metaChange)) - { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(idx); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Delete a mod by its index. The event is invoked before the mod is removed from the list. - /// Deletes from filesystem as well as from internal data. - /// Updates indices of later mods. - /// - public void DeleteMod(int idx) - { - var mod = this[idx]; - if (Directory.Exists(mod.ModPath.FullName)) + case NewDirectoryState.NonExisting: + // Nothing to do + break; + case NewDirectoryState.ExistsEmpty: try { - Directory.Delete(mod.ModPath.FullName, true); - Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); + Directory.Delete(dir!.FullName); } catch (Exception e) { - Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); + Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); + return; } - ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - _mods.RemoveAt(idx); - foreach (var remainingMod in _mods.Skip(idx)) - --remainingMod.Index; - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); - } - - /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) - { - if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) + break; + // Should be caught beforehand. + case NewDirectoryState.ExistsNonEmpty: + case NewDirectoryState.ExistsAsFile: + case NewDirectoryState.ContainsInvalidSymbols: + // Nothing to do at all. + case NewDirectoryState.Identical: + default: return; - - Creator.SplitMultiGroups(modFolder); - var mod = LoadMod(this, modFolder, true); - if (mod == null) - return; - - mod.Index = _mods.Count; - _mods.Add(mod); - ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); - Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); } - public enum NewDirectoryState + try { - NonExisting, - ExistsEmpty, - ExistsNonEmpty, - ExistsAsFile, - ContainsInvalidSymbols, - Identical, - Empty, + Directory.Move(oldDirectory.FullName, dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); + return; } - /// Return the state of the new potential name of a directory. - public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + DataEditor.MoveDataFile(oldDirectory, dir); + new ModBackup(this, mod).Move(null, dir.Name); + + dir.Refresh(); + mod.ModPath = dir; + if (!mod.Reload(this, false, out var metaChange)) { - directory = null; - if (newName.Length == 0) - return NewDirectoryState.Empty; - - if (oldName == newName) - return NewDirectoryState.Identical; - - var fixedNewName = Creator.ReplaceBadXivSymbols(newName); - if (fixedNewName != newName) - return NewDirectoryState.ContainsInvalidSymbols; - - directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); - if (File.Exists(directory.FullName)) - return NewDirectoryState.ExistsAsFile; - - if (!Directory.Exists(directory.FullName)) - return NewDirectoryState.NonExisting; - - if (directory.EnumerateFileSystemInfos().Any()) - return NewDirectoryState.ExistsNonEmpty; - - return NewDirectoryState.ExistsEmpty; + Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); + return; } + ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } - /// Add new mods to NewMods and remove deleted mods from NewMods. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) + /// + /// Reload a mod without changing its base directory. + /// If the base directory does not exist anymore, the mod will be deleted. + /// + public void ReloadMod(int idx) + { + var mod = this[idx]; + var oldName = mod.Name; + + ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); + if (!mod.Reload(this, true, out var metaChange)) { - switch (type) + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); + + DeleteMod(idx); + return; + } + + ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + /// + /// Delete a mod by its index. The event is invoked before the mod is removed from the list. + /// Deletes from filesystem as well as from internal data. + /// Updates indices of later mods. + /// + public void DeleteMod(int idx) + { + var mod = this[idx]; + if (Directory.Exists(mod.ModPath.FullName)) + try { - case ModPathChangeType.Added: - NewMods.Add(mod); - break; - case ModPathChangeType.Deleted: - NewMods.Remove(mod); - break; - case ModPathChangeType.Moved: - if (oldDirectory != null && newDirectory != null) - DataEditor.MoveDataFile(oldDirectory, newDirectory); - - break; + Directory.Delete(mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); + } + + ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); + _mods.RemoveAt(idx); + foreach (var remainingMod in _mods.Skip(idx)) + --remainingMod.Index; + + Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + } + + /// Load a new mod and add it to the manager if successful. + public void AddMod(DirectoryInfo modFolder) + { + if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) + return; + + Mod.Creator.SplitMultiGroups(modFolder); + var mod = Mod.LoadMod(this, modFolder, true); + if (mod == null) + return; + + mod.Index = _mods.Count; + _mods.Add(mod); + ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); + Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); + } + + public enum NewDirectoryState + { + NonExisting, + ExistsEmpty, + ExistsNonEmpty, + ExistsAsFile, + ContainsInvalidSymbols, + Identical, + Empty, + } + + /// Return the state of the new potential name of a directory. + public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + { + directory = null; + if (newName.Length == 0) + return NewDirectoryState.Empty; + + if (oldName == newName) + return NewDirectoryState.Identical; + + var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); + if (fixedNewName != newName) + return NewDirectoryState.ContainsInvalidSymbols; + + directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); + if (File.Exists(directory.FullName)) + return NewDirectoryState.ExistsAsFile; + + if (!Directory.Exists(directory.FullName)) + return NewDirectoryState.NonExisting; + + if (directory.EnumerateFileSystemInfos().Any()) + return NewDirectoryState.ExistsNonEmpty; + + return NewDirectoryState.ExistsEmpty; + } + + + /// Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Added: + NewMods.Add(mod); + break; + case ModPathChangeType.Deleted: + NewMods.Remove(mod); + break; + case ModPathChangeType.Moved: + if (oldDirectory != null && newDirectory != null) + DataEditor.MoveDataFile(oldDirectory, newDirectory); + + break; } } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs deleted file mode 100644 index f838677f..00000000 --- a/Penumbra/Mods/Manager/Mod.Manager.Local.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public partial class Manager - { - - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index a9b11b26..0c86e82d 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -11,371 +11,367 @@ using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager { - public sealed partial class Manager + public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); + public event ModOptionChangeDelegate ModOptionChanged; + + public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { - public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); - public event ModOptionChangeDelegate ModOptionChanged; + var group = mod._groups[groupIdx]; + if (group.Type == type) + return; - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) + mod._groups[groupIdx] = group.Convert(type); + ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); + } + + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) + { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + } + + public void RenameModGroup(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(mod, group, newName, true)) + return; + + group.DeleteFile(mod.ModPath, groupIdx); + + var _ = group switch { - var group = mod._groups[groupIdx]; - if (group.Type == type) - return; + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; - mod._groups[groupIdx] = group.Convert(type); - ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } + ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); + } - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) - { - var group = mod._groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; + public void AddModGroup(Mod mod, GroupType type, string newName) + { + if (!VerifyFileName(mod, null, newName, true)) + return; - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - group.DeleteFile(mod.ModPath, groupIdx); - - var _ = group switch + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } - - public void AddModGroup(Mod mod, GroupType type, string newName) - { - if (!VerifyFileName(mod, null, newName, true)) - return; - - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - - mod._groups.Add(type == GroupType.Multi - ? new MultiModGroup - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup - { - Name = newName, - Priority = maxPriority, - }); - ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); - } - - public void DeleteModGroup(Mod mod, int groupIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod._groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); - group.DeleteFile(mod.ModPath, groupIdx); - ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (mod._groups.Move(groupIdxFrom, groupIdxTo)) - { - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); + Name = newName, + Priority = maxPriority, } - } - - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + : new SingleModGroup { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } + Name = newName, + Priority = maxPriority, + }); + ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + group.DeleteFile(mod.ModPath, groupIdx); + ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + } + + public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) + { + if (mod._groups.Move(groupIdxFrom, groupIdxTo)) { - var group = mod._groups[groupIdx]; - if (group.Description == newDescription) - return; - - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) - return; - - s.Description = newDescription; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) - { - var group = mod._groups[groupIdx]; - if (group.Priority == newPriority) - return; - - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) - return; - - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void AddOption(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) - { - if (option is not SubMod o) - return; - - var group = mod._groups[groupIdx]; - if (group.Count > 63) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(o); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((o, priority)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); - break; - } - - group.UpdatePositions(optionIdx); - ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); - } - - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) - { - var group = mod._groups[groupIdx]; - if (group.MoveOption(optionIdxFrom, optionIdxTo)) - ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); - } - - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData = replacements; - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData = swaps; - ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); - } - - public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.ChatService.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); - - return false; - - } - - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) - { - if (groupIdx == -1 && optionIdx == 0) - return mod._default; - - return mod._groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; - } - - private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) - { - if (type == ModOptionChangeType.PrepareChange) - return; - - // File deletion is handled in the actual function. - if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) - { - mod.SaveAllGroups(); - } - else - { - if (groupIdx == -1) - mod.SaveDefaultModDelayed(); - else - IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); - } - - bool ComputeChangedItems() - { - mod.ComputeChangedItems(); - return true; - } - - // State can not change on adding groups, as they have no immediate options. - var unused = type switch - { - ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), - ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), - ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), - ModOptionChangeType.DisplayChange => false, - _ => false, - }; + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } } + + private static void UpdateSubModPositions(Mod mod, int fromGroup) + { + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + { + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); + } + } + + public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + if (group.Description == newDescription) + return; + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); + } + + public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) + return; + + s.Description = newDescription; + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) + { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) + return; + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); + } + + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup: + ChangeGroupPriority(mod, groupIdx, newPriority); + break; + case MultiModGroup m: + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + return; + + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); + return; + } + } + + public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup s: + if (s.OptionData[optionIdx].Name == newName) + return; + + s.OptionData[optionIdx].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) + return; + + option.Name = newName; + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + public void AddOption(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(subMod); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((subMod, 0)); + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + { + if (option is not SubMod o) + return; + + var group = mod._groups[groupIdx]; + if (group.Count > 63) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + + "since only up to 64 options are supported in one group."); + return; + } + + o.SetPosition(groupIdx, group.Count); + + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(o); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((o, priority)); + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + switch (group) + { + case SingleModGroup s: + s.OptionData.RemoveAt(optionIdx); + + break; + case MultiModGroup m: + m.PrioritizedOptions.RemoveAt(optionIdx); + break; + } + + group.UpdatePositions(optionIdx); + ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + } + + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + { + var group = mod._groups[groupIdx]; + if (group.MoveOption(optionIdxFrom, optionIdxTo)) + ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + } + + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.ManipulationData = manipulations; + ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + } + + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileData = replacements; + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + } + + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + } + + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileSwapData = swaps; + ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + } + + public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.ChatService.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); + + return false; + } + + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + { + if (groupIdx == -1 && optionIdx == 0) + return mod._default; + + return mod._groups[groupIdx] switch + { + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, + _ => throw new InvalidOperationException(), + }; + } + + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + { + if (type == ModOptionChangeType.PrepareChange) + return; + + // File deletion is handled in the actual function. + if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) + { + mod.SaveAllGroups(); + } + else + { + if (groupIdx == -1) + mod.SaveDefaultModDelayed(); + else + IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); + } + + bool ComputeChangedItems() + { + mod.ComputeChangedItems(); + return true; + } + + // State can not change on adding groups, as they have no immediate options. + var unused = type switch + { + ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), + ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), + ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), + ModOptionChangeType.DisplayChange => false, + _ => false, + }; + } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 79dd780b..0377e09b 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -6,169 +6,144 @@ using System.Threading.Tasks; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager { - public sealed partial class Manager + public DirectoryInfo BasePath { get; private set; } = null!; + private DirectoryInfo? _exportDirectory; + + public DirectoryInfo ExportDirectory + => _exportDirectory ?? BasePath; + + public bool Valid { get; private set; } + + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; + public event Action ModDirectoryChanged; + + // Change the mod base directory and discover available mods. + public void DiscoverMods(string newDir) { - public DirectoryInfo BasePath { get; private set; } = null!; - private DirectoryInfo? _exportDirectory; + SetBaseDirectory(newDir, false); + DiscoverMods(); + } - public DirectoryInfo ExportDirectory - => _exportDirectory ?? BasePath; + // Set the mod base directory. + // If its not the first time, check if it is the same directory as before. + // Also checks if the directory is available and tries to create it if it is not. + private void SetBaseDirectory(string newPath, bool firstTime) + { + if (!firstTime && string.Equals(newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + return; - public bool Valid { get; private set; } - - public event Action? ModDiscoveryStarted; - public event Action? ModDiscoveryFinished; - public event Action< string, bool > ModDirectoryChanged; - - // Change the mod base directory and discover available mods. - public void DiscoverMods( string newDir ) + if (newPath.Length == 0) { - SetBaseDirectory( newDir, false ); - DiscoverMods(); + Valid = false; + BasePath = new DirectoryInfo("."); + if (Penumbra.Config.ModDirectory != BasePath.FullName) + ModDirectoryChanged.Invoke(string.Empty, false); } - - // Set the mod base directory. - // If its not the first time, check if it is the same directory as before. - // Also checks if the directory is available and tries to create it if it is not. - private void SetBaseDirectory( string newPath, bool firstTime ) + else { - if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - if( newPath.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - ModDirectoryChanged.Invoke( string.Empty, false ); - } - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - BasePath = newDir; - Valid = Directory.Exists( newDir.FullName ); - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - ModDirectoryChanged.Invoke( BasePath.FullName, Valid ); - } - } - } - - private static void OnModDirectoryChange( string newPath, bool _ ) - { - Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." ); - Penumbra.Config.ModDirectory = newPath; - Penumbra.Config.Save(); - } - - // Discover new mods. - public void DiscoverMods() - { - NewMods.Clear(); - ModDiscoveryStarted?.Invoke(); - _mods.Clear(); - BasePath.Refresh(); - - if( Valid && BasePath.Exists ) - { - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, - }; - var queue = new ConcurrentQueue< Mod >(); - Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir => - { - var mod = LoadMod( this, dir, false ); - if( mod != null ) - { - queue.Enqueue( mod ); - } - } ); - - foreach( var mod in queue ) - { - mod.Index = _mods.Count; - _mods.Add( mod ); - } - } - - ModDiscoveryFinished?.Invoke(); - Penumbra.Log.Information( "Rediscovered mods." ); - - if( MigrateModBackups ) - { - ModBackup.MigrateZipToPmp( this ); - } - } - - public void UpdateExportDirectory( string newDirectory, bool change ) - { - if( newDirectory.Length == 0 ) - { - if( _exportDirectory == null ) - { - return; - } - - _exportDirectory = null; - _config.ExportDirectory = string.Empty; - _config.Save(); - return; - } - - var dir = new DirectoryInfo( newDirectory ); - if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - if( !dir.Exists ) - { + var newDir = new DirectoryInfo(newPath); + if (!newDir.Exists) try { - Directory.CreateDirectory( dir.FullName ); + Directory.CreateDirectory(newDir.FullName); + newDir.Refresh(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" ); - return; + Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); } - } - if( change ) - { - foreach( var mod in _mods ) - { - new ModBackup( this, mod ).Move( dir.FullName ); - } - } - - _exportDirectory = dir; - - if( change ) - { - _config.ExportDirectory = dir.FullName; - _config.Save(); - } + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + if (Penumbra.Config.ModDirectory != BasePath.FullName) + ModDirectoryChanged.Invoke(BasePath.FullName, Valid); } } -} \ No newline at end of file + + private static void OnModDirectoryChange(string newPath, bool _) + { + Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}."); + Penumbra.Config.ModDirectory = newPath; + Penumbra.Config.Save(); + } + + // Discover new mods. + public void DiscoverMods() + { + NewMods.Clear(); + ModDiscoveryStarted?.Invoke(); + _mods.Clear(); + BasePath.Refresh(); + + if (Valid && BasePath.Exists) + { + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Mod.LoadMod(this, dir, false); + if (mod != null) + queue.Enqueue(mod); + }); + + foreach (var mod in queue) + { + mod.Index = _mods.Count; + _mods.Add(mod); + } + } + + ModDiscoveryFinished?.Invoke(); + Penumbra.Log.Information("Rediscovered mods."); + + if (MigrateModBackups) + ModBackup.MigrateZipToPmp(this); + } + + public void UpdateExportDirectory(string newDirectory, bool change) + { + if (newDirectory.Length == 0) + { + if (_exportDirectory == null) + return; + + _exportDirectory = null; + _config.ExportDirectory = string.Empty; + _config.Save(); + return; + } + + var dir = new DirectoryInfo(newDirectory); + if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) + return; + + if (!dir.Exists) + try + { + Directory.CreateDirectory(dir.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); + return; + } + + if (change) + foreach (var mod in _mods) + new ModBackup(this, mod).Move(dir.FullName); + + _exportDirectory = dir; + + if (change) + { + _config.ExportDirectory = dir.FullName; + _config.Save(); + } + } +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 0e47f3ae..dd40e9d4 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -7,73 +7,70 @@ using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager : IReadOnlyList { - public sealed partial class Manager : IReadOnlyList + // Set when reading Config and migrating from v4 to v5. + public static bool MigrateModBackups = false; + + // An easily accessible set of new mods. + // Mods are added when they are created or imported. + // Mods are removed when they are deleted or when they are toggled in any collection. + // Also gets cleared on mod rediscovery. + public readonly HashSet NewMods = new(); + + private readonly List _mods = new(); + + public Mod this[int idx] + => _mods[idx]; + + public Mod this[Index idx] + => _mods[idx]; + + public int Count + => _mods.Count; + + public IEnumerator GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + public readonly ModDataEditor DataEditor; + + public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) { - // Set when reading Config and migrating from v4 to v5. - public static bool MigrateModBackups = false; + using var timer = time.Measure(StartTimeType.Mods); + _config = config; + _communicator = communicator; + DataEditor = dataEditor; + ModDirectoryChanged += OnModDirectoryChange; + SetBaseDirectory(config.ModDirectory, true); + UpdateExportDirectory(_config.ExportDirectory, false); + ModOptionChanged += OnModOptionChange; + ModPathChanged += OnModPathChange; + DiscoverMods(); + } - // An easily accessible set of new mods. - // Mods are added when they are created or imported. - // Mods are removed when they are deleted or when they are toggled in any collection. - // Also gets cleared on mod rediscovery. - public readonly HashSet NewMods = new(); - private readonly List _mods = new(); - - public Mod this[int idx] - => _mods[idx]; - - public Mod this[Index idx] - => _mods[idx]; - - public int Count - => _mods.Count; - - public IEnumerator GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - public readonly ModDataEditor DataEditor; - - public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) + // Try to obtain a mod by its directory name (unique identifier, preferred), + // or the first mod of the given name if no directory fits. + public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in _mods) { - using var timer = time.Measure(StartTimeType.Mods); - _config = config; - _communicator = communicator; - DataEditor = dataEditor; - ModDirectoryChanged += OnModDirectoryChange; - SetBaseDirectory(config.ModDirectory, true); - UpdateExportDirectory(_config.ExportDirectory, false); - ModOptionChanged += OnModOptionChange; - ModPathChanged += OnModPathChange; - DiscoverMods(); - } - - - // Try to obtain a mod by its directory name (unique identifier, preferred), - // or the first mod of the given name if no directory fits. - public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) - { - mod = null; - foreach (var m in _mods) + if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) - { - mod = m; - return true; - } - - if (m.Name == modName) - mod ??= m; + mod = m; + return true; } - return mod != null; + if (m.Name == modName) + mod ??= m; } + + return mod != null; } } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 2f236b5f..09a24bba 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Linq; using Dalamud.Utility; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Services; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index c7367a68..fb71ade6 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -15,10 +15,10 @@ public enum ModPathChangeType public partial class Mod { - public DirectoryInfo ModPath { get; private set; } + public DirectoryInfo ModPath { get; internal set; } public string Identifier => Index >= 0 ? ModPath.Name : Name; - public int Index { get; private set; } = -1; + public int Index { get; internal set; } = -1; public bool IsTemporary => Index < 0; @@ -33,7 +33,7 @@ public partial class Mod _default = new SubMod( this ); } - private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) + public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) { modPath.Refresh(); if( !modPath.Exists ) @@ -52,7 +52,7 @@ public partial class Mod } - internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) + internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index 7c066681..cdffcee9 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -12,7 +12,7 @@ public sealed partial class Mod public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; - private void ComputeChangedItems() + internal void ComputeChangedItems() { ChangedItems.Clear(); foreach( var gamePath in AllRedirects ) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a12b29fc..6091ba1b 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -18,15 +18,15 @@ public partial class Mod public IReadOnlyList< IModGroup > Groups => _groups; - private readonly SubMod _default; - private readonly List< IModGroup > _groups = new(); + internal readonly SubMod _default; + internal readonly List< IModGroup > _groups = new(); - public int TotalFileCount { get; private set; } - public int TotalSwapCount { get; private set; } - public int TotalManipulations { get; private set; } - public bool HasOptions { get; private set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public bool HasOptions { get; internal set; } - private bool SetCounts() + internal bool SetCounts() { TotalFileCount = 0; TotalSwapCount = 0; @@ -120,7 +120,7 @@ public partial class Mod // Delete all existing group files and save them anew. // Used when indices change in complex ways. - private void SaveAllGroups() + internal void SaveAllGroups() { foreach( var file in GroupFiles ) { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 9bd7488a..c16c172e 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -12,12 +12,12 @@ namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly CommunicatorService _communicator; private readonly FilenameService _files; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files) + public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files) { _modManager = modManager; _communicator = communicator; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 70840b24..44314290 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -15,103 +15,105 @@ namespace Penumbra.Mods; public partial class Mod { - // Groups that allow all available options to be selected at once. - private sealed class MultiModGroup : IModGroup + +} + +/// Groups that allow all available options to be selected at once. +public sealed class MultiModGroup : IModGroup +{ + public GroupType Type + => GroupType.Multi; + + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } + + public int OptionPriority(Index idx) + => PrioritizedOptions[idx].Priority; + + public ISubMod this[Index idx] + => PrioritizedOptions[idx].Mod; + + [JsonIgnore] + public int Count + => PrioritizedOptions.Count; + + public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); + + public IEnumerator GetEnumerator() + => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { - public GroupType Type - => GroupType.Multi; - - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } - - public int OptionPriority(Index idx) - => PrioritizedOptions[idx].Priority; - - public ISubMod this[Index idx] - => PrioritizedOptions[idx].Mod; - - [JsonIgnore] - public int Count - => PrioritizedOptions.Count; - - public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); - - public IEnumerator GetEnumerator() - => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) + var ret = new MultiModGroup() { - var ret = new MultiModGroup() + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, + }; + if (ret.Name.Length == 0) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, - }; - if (ret.Name.Length == 0) - return null; - - var options = json["Options"]; - if (options != null) - foreach (var child in options.Children()) + if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) - { - Penumbra.ChatService.NotificationMessage( - $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", - NotificationType.Warning); - break; - } - - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); - subMod.Load(mod.ModPath, child, out var priority); - ret.PrioritizedOptions.Add((subMod, priority)); + Penumbra.ChatService.NotificationMessage( + $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", + NotificationType.Warning); + break; } - ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); - - return ret; - } - - public IModGroup Convert(GroupType type) - { - switch (type) - { - case GroupType.Multi: return this; - case GroupType.Single: - var multi = new SingleModGroup() - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), - }; - multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); - return multi; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); + var subMod = new SubMod(mod); + subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + subMod.Load(mod.ModPath, child, out var priority); + ret.PrioritizedOptions.Add((subMod, priority)); } - } - public bool MoveOption(int optionIdxFrom, int optionIdxTo) + ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); + + return ret; + } + + public IModGroup Convert(GroupType type) + { + switch (type) { - if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) - return false; - - DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); - return true; - } - - public void UpdatePositions(int from = 0) - { - foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); + case GroupType.Multi: return this; + case GroupType.Single: + var multi = new SingleModGroup() + { + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), + }; + multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); + return multi; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } + + public bool MoveOption(int optionIdxFrom, int optionIdxTo) + { + if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) + return false; + + DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); + return true; + } + + public void UpdatePositions(int from = 0) + { + foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) + o.SetPosition(o.GroupIdx, i); + } } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index cfec230e..b330c00d 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -10,122 +10,119 @@ using Penumbra.Api.Enums; namespace Penumbra.Mods; -public partial class Mod +/// Groups that allow only one of their available options to be selected. +public sealed class SingleModGroup : IModGroup { - // Groups that allow only one of their available options to be selected. - private sealed class SingleModGroup : IModGroup + public GroupType Type + => GroupType.Single; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } + + public readonly List< SubMod > OptionData = new(); + + public int OptionPriority( Index _ ) + => Priority; + + public ISubMod this[ Index idx ] + => OptionData[ idx ]; + + [JsonIgnore] + public int Count + => OptionData.Count; + + public IEnumerator< ISubMod > GetEnumerator() + => OptionData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) { - public GroupType Type - => GroupType.Single; - - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } - - public readonly List< SubMod > OptionData = new(); - - public int OptionPriority( Index _ ) - => Priority; - - public ISubMod this[ Index idx ] - => OptionData[ idx ]; - - [JsonIgnore] - public int Count - => OptionData.Count; - - public IEnumerator< ISubMod > GetEnumerator() - => OptionData.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) + var options = json[ "Options" ]; + var ret = new SingleModGroup { - var options = json[ "Options" ]; - var ret = new SingleModGroup - { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, - DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, - }; - if( ret.Name.Length == 0 ) - { - return null; - } + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, + }; + if( ret.Name.Length == 0 ) + { + return null; + } - if( options != null ) + if( options != null ) + { + foreach( var child in options.Children() ) { - foreach( var child in options.Children() ) + var subMod = new SubMod( mod ); + subMod.SetPosition( groupIdx, ret.OptionData.Count ); + subMod.Load( mod.ModPath, child, out _ ); + ret.OptionData.Add( subMod ); + } + } + + if( ( int )ret.DefaultSettings >= ret.Count ) + ret.DefaultSettings = 0; + + return ret; + } + + public IModGroup Convert( GroupType type ) + { + switch( type ) + { + case GroupType.Single: return this; + case GroupType.Multi: + var multi = new MultiModGroup() { - var subMod = new SubMod( mod ); - subMod.SetPosition( groupIdx, ret.OptionData.Count ); - subMod.Load( mod.ModPath, child, out _ ); - ret.OptionData.Add( subMod ); - } - } + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = 1u << ( int )DefaultSettings, + }; + multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); + return multi; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } - if( ( int )ret.DefaultSettings >= ret.Count ) - ret.DefaultSettings = 0; - - return ret; + public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + { + if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) + { + return false; } - public IModGroup Convert( GroupType type ) + // Update default settings with the move. + if( DefaultSettings == optionIdxFrom ) { - switch( type ) + DefaultSettings = ( uint )optionIdxTo; + } + else if( optionIdxFrom < optionIdxTo ) + { + if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) { - case GroupType.Single: return this; - case GroupType.Multi: - var multi = new MultiModGroup() - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = 1u << ( int )DefaultSettings, - }; - multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); - return multi; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + --DefaultSettings; } } - - public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) { - if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) - { - return false; - } - - // Update default settings with the move. - if( DefaultSettings == optionIdxFrom ) - { - DefaultSettings = ( uint )optionIdxTo; - } - else if( optionIdxFrom < optionIdxTo ) - { - if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) - { - --DefaultSettings; - } - } - else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) - { - ++DefaultSettings; - } - - UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); - return true; + ++DefaultSettings; } - public void UpdatePositions( int from = 0 ) + UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + return true; + } + + public void UpdatePositions( int from = 0 ) + { + foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) { - foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) - { - o.SetPosition( o.GroupIdx, i ); - } + o.SetPosition( o.GroupIdx, i ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 9e609891..f1a4a24c 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -36,7 +36,7 @@ public partial class Mod ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 ); } - private void SaveDefaultModDelayed() + internal void SaveDefaultModDelayed() => Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod ); private void LoadDefaultOption() @@ -92,233 +92,237 @@ public partial class Mod } - // A sub mod is a collection of - // - file replacements - // - file swaps - // - meta manipulations - // that can be used either as an option or as the default data for a mod. - // It can be loaded and reloaded from Json. - // Nothing is checked for existence or validity when loading. - // Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. - public sealed class SubMod : ISubMod + +} + +/// +/// A sub mod is a collection of +/// - file replacements +/// - file swaps +/// - meta manipulations +/// that can be used either as an option or as the default data for a mod. +/// It can be loaded and reloaded from Json. +/// Nothing is checked for existence or validity when loading. +/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. +/// + public sealed class SubMod : ISubMod +{ + public string Name { get; set; } = "Default"; + + public string FullName + => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + internal IMod ParentMod { get; private init; } + internal int GroupIdx { get; private set; } + internal int OptionIdx { get; private set; } + + public bool IsDefault + => GroupIdx < 0; + + public Dictionary< Utf8GamePath, FullPath > FileData = new(); + public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public HashSet< MetaManipulation > ManipulationData = new(); + + public SubMod( IMod parentMod ) + => ParentMod = parentMod; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlySet< MetaManipulation > Manipulations + => ManipulationData; + + public void SetPosition( int groupIdx, int optionIdx ) { - public string Name { get; set; } = "Default"; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + } - public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + public void Load( DirectoryInfo basePath, JToken json, out int priority ) + { + FileData.Clear(); + FileSwapData.Clear(); + ManipulationData.Clear(); - public string Description { get; set; } = string.Empty; + // Every option has a name, but priorities are only relevant for multi group options. + Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; + Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; + priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; - internal IMod ParentMod { get; private init; } - internal int GroupIdx { get; private set; } - internal int OptionIdx { get; private set; } - - public bool IsDefault - => GroupIdx < 0; - - public Dictionary< Utf8GamePath, FullPath > FileData = new(); - public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - public HashSet< MetaManipulation > ManipulationData = new(); - - public SubMod( IMod parentMod ) - => ParentMod = parentMod; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files - => FileData; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps - => FileSwapData; - - public IReadOnlySet< MetaManipulation > Manipulations - => ManipulationData; - - public void SetPosition( int groupIdx, int optionIdx ) + var files = ( JObject? )json[ nameof( Files ) ]; + if( files != null ) { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - } - - public void Load( DirectoryInfo basePath, JToken json, out int priority ) - { - FileData.Clear(); - FileSwapData.Clear(); - ManipulationData.Clear(); - - // Every option has a name, but priorities are only relevant for multi group options. - Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; - Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; - priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; - - var files = ( JObject? )json[ nameof( Files ) ]; - if( files != null ) + foreach( var property in files.Properties() ) { - foreach( var property in files.Properties() ) + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); - } - } - } - - var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; - if( swaps != null ) - { - foreach( var property in swaps.Properties() ) - { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); - } - } - } - - var manips = json[ nameof( Manipulations ) ]; - if( manips != null ) - { - foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) - { - ManipulationData.Add( s ); + FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); } } } - // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. - // If delete is true, the files are deleted afterwards. - public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; + if( swaps != null ) { - var deleteList = new List< string >(); - var oldSize = ManipulationData.Count; - var deleteString = delete ? "with deletion." : "without deletion."; - foreach( var (key, file) in Files.ToList() ) + foreach( var property in swaps.Properties() ) { - var ext1 = key.Extension().AsciiToLower().ToString(); - var ext2 = file.Extension.ToLowerInvariant(); - try + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) { - if( ext1 == ".meta" || ext2 == ".meta" ) - { - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); - ManipulationData.UnionWith( meta.MetaManipulations ); - } - else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) - { - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); - - ManipulationData.UnionWith( rgsp.MetaManipulations ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); - } - } - - DeleteDeleteList( deleteList, delete ); - return ( oldSize < ManipulationData.Count, deleteList ); - } - - internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) - { - if( !delete ) - { - return; - } - - foreach( var file in deleteList ) - { - try - { - File.Delete( file ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); } } } - public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + var manips = json[ nameof( Manipulations ) ]; + if( manips != null ) { - var files = TexToolsMeta.ConvertToTexTools( Manipulations ); - - foreach( var (file, data) in files ) + foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { - var path = Path.Combine( basePath.FullName, file ); - try - { - Directory.CreateDirectory( Path.GetDirectoryName( path )! ); - File.WriteAllBytes( path, data ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); - } - } - - if( test ) - { - TestMetaWriting( files ); - } - } - - [Conditional( "DEBUG" )] - private void TestMetaWriting( Dictionary< string, byte[] > files ) - { - var meta = new HashSet< MetaManipulation >( Manipulations.Count ); - foreach( var (file, data) in files ) - { - try - { - var x = file.EndsWith( "rgsp" ) - ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) - : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); - meta.UnionWith( x.MetaManipulations ); - } - catch - { - // ignored - } - } - - if( !Manipulations.SetEquals( meta ) ) - { - Penumbra.Log.Information( "Meta Sets do not equal." ); - foreach( var (m1, m2) in Manipulations.Zip( meta ) ) - { - Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); - } - - foreach( var m in Manipulations.Skip( meta.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } - - foreach( var m in meta.Skip( Manipulations.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } - } - else - { - Penumbra.Log.Information( "Meta Sets are equal." ); + ManipulationData.Add( s ); } } } + + // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. + // If delete is true, the files are deleted afterwards. + public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + { + var deleteList = new List< string >(); + var oldSize = ManipulationData.Count; + var deleteString = delete ? "with deletion." : "without deletion."; + foreach( var (key, file) in Files.ToList() ) + { + var ext1 = key.Extension().AsciiToLower().ToString(); + var ext2 = file.Extension.ToLowerInvariant(); + try + { + if( ext1 == ".meta" || ext2 == ".meta" ) + { + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); + Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); + ManipulationData.UnionWith( meta.MetaManipulations ); + } + else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) + { + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); + Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); + + ManipulationData.UnionWith( rgsp.MetaManipulations ); + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + } + } + + DeleteDeleteList( deleteList, delete ); + return ( oldSize < ManipulationData.Count, deleteList ); + } + + internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) + { + if( !delete ) + { + return; + } + + foreach( var file in deleteList ) + { + try + { + File.Delete( file ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + } + } + } + + public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + { + var files = TexToolsMeta.ConvertToTexTools( Manipulations ); + + foreach( var (file, data) in files ) + { + var path = Path.Combine( basePath.FullName, file ); + try + { + Directory.CreateDirectory( Path.GetDirectoryName( path )! ); + File.WriteAllBytes( path, data ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); + } + } + + if( test ) + { + TestMetaWriting( files ); + } + } + + [Conditional("DEBUG" )] + private void TestMetaWriting( Dictionary< string, byte[] > files ) + { + var meta = new HashSet< MetaManipulation >( Manipulations.Count ); + foreach( var (file, data) in files ) + { + try + { + var x = file.EndsWith( "rgsp" ) + ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) + : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); + meta.UnionWith( x.MetaManipulations ); + } + catch + { + // ignored + } + } + + if( !Manipulations.SetEquals( meta ) ) + { + Penumbra.Log.Information( "Meta Sets do not equal." ); + foreach( var (m1, m2) in Manipulations.Zip( meta ) ) + { + Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); + } + + foreach( var m in Manipulations.Skip( meta.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + + foreach( var m in meta.Skip( Manipulations.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + } + else + { + Penumbra.Log.Information( "Meta Sets are equal." ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 845456ae..a9239078 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -10,7 +10,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -// Contains the settings for a given mod. +/// Contains the settings for a given mod. public class ModSettings { public static readonly ModSettings Empty = new(); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 5bf37c95..d46d00d5 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -27,10 +27,10 @@ public class TemporaryMod : IMod public IEnumerable< ISubMod > AllSubMods => new[] { Default }; - private readonly Mod.SubMod _default; + private readonly SubMod _default; public TemporaryMod() - => _default = new Mod.SubMod( this ); + => _default = new SubMod( this ); public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) => _default.FileData[ gamePath ] = fullPath; @@ -44,7 +44,7 @@ public class TemporaryMod : IMod _default.ManipulationData = manips; } - public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null ) + public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null ) { DirectoryInfo? dir = null; try @@ -54,7 +54,7 @@ public class TemporaryMod : IMod modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); - var defaultMod = (Mod.SubMod) mod.Default; + var defaultMod = (SubMod) mod.Default; foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) { if( gamePath.Path.EndsWith( ".imc"u8 ) ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 46b8ad3a..85a7c717 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -47,8 +47,8 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Manager ModManager { get; private set; } = null!; - public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; @@ -96,8 +96,8 @@ public class Penumbra : IDalamudPlugin TempMods = _tmp.Services.GetRequiredService(); ResidentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModManager = _tmp.Services.GetRequiredService(); - CollectionManager = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); TempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 30ea8f73..fcf2c386 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -88,12 +88,12 @@ public class PenumbraNew // Add Collection Services services.AddTransient() .AddSingleton() - .AddSingleton(); + .AddSingleton(); // Add Mod Services services.AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton(); // Add Resource services diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index b187e991..a2f8b55f 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -87,7 +87,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - ModCollection.Manager.MigrateUngenderedCollections(_fileNames); + CollectionManager.MigrateUngenderedCollections(_fileNames); _config.Version = 7; } @@ -113,7 +113,7 @@ public class ConfigMigrationService if (_config.Version != 4) return; - Mod.Manager.MigrateModBackups = true; + ModManager.MigrateModBackups = true; _config.Version = 5; } @@ -257,11 +257,11 @@ public class ConfigMigrationService using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); - j.WritePropertyName(nameof(ModCollection.Manager.Default)); + j.WritePropertyName(nameof(CollectionManager.Default)); j.WriteValue(def); - j.WritePropertyName(nameof(ModCollection.Manager.Interface)); + j.WritePropertyName(nameof(CollectionManager.Interface)); j.WriteValue(ui); - j.WritePropertyName(nameof(ModCollection.Manager.Current)); + j.WritePropertyName(nameof(CollectionManager.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3a5d8ce6..48333d6f 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab { private readonly CommunicatorService _communicator; private readonly ItemService _itemService; - private readonly ModCollection.Manager _collectionManager; - private readonly Mod.Manager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly Configuration _config; - public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager, - Mod.Manager modManager, Configuration config) + public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, + ModManager modManager, Configuration config) { _communicator = communicator; _itemService = itemService; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 55eecdae..ad0e7fa2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -297,7 +297,7 @@ public partial class ModEditWindow var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); if (failedFiles > 0) Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 29671c7c..fae1f0a0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -194,7 +194,7 @@ public partial class ModEditWindow _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!); + _editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!); return fileRegistry; } diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs index bd9d3e35..1ff60559 100644 --- a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab; public sealed class CollectionSelector : FilterComboCache { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public CollectionSelector(ModCollection.Manager manager, Func> items) + public CollectionSelector(CollectionManager manager, Func> items) : base(items) => _collectionManager = manager; diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs index a29ebb25..b7bcc0f5 100644 --- a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -16,10 +16,10 @@ namespace Penumbra.UI.CollectionTab; public class IndividualCollectionUi { private readonly ActorService _actorService; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly CollectionSelector _withEmpty; - public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty) + public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty) { _actorService = actors; _collectionManager = collectionManager; diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs index dc3bc52f..57a51ab1 100644 --- a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -16,9 +16,9 @@ public class InheritanceUi private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public InheritanceUi(ModCollection.Manager collectionManager) + public InheritanceUi(CollectionManager collectionManager) => _collectionManager = collectionManager; /// Draw the whole inheritance block. diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs index 59461f42..5af7c578 100644 --- a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -7,7 +7,7 @@ namespace Penumbra.UI.CollectionTab; public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; public (CollectionType, string, string)? CurrentType => CollectionTypeExtensions.Special[CurrentIdx]; @@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri private readonly float _unscaledWidth; private readonly string _label; - public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth) + public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth) : base(CollectionTypeExtensions.Special, false) { _collectionManager = collectionManager; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 2d16668f..650014c8 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -14,12 +14,12 @@ namespace Penumbra.UI; public class FileDialogService : IDisposable { - private readonly Mod.Manager _mods; + private readonly ModManager _mods; private readonly FileDialogManager _manager; private readonly ConcurrentDictionary _startPaths = new(); private bool _isOpen; - public FileDialogService(Mod.Manager mods, Configuration config) + public FileDialogService(ModManager mods, Configuration config) { _mods = mods; _manager = SetupFileManager(config.ModDirectory); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 64febc57..cbc834ac 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -29,8 +29,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector _newGroupName = string.Empty; - public static void Draw(Mod.Manager modManager, Mod mod) + public static void Draw(ModManager modManager, Mod mod) { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); @@ -250,15 +250,15 @@ public class ModPanelEditTab : ITab private static class MoveDirectory { private static string? _currentModDirectory; - private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; public static void Reset() { _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; + _state = ModManager.NewDirectoryState.Identical; } - public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize) + public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize) { ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X); var tmp = _currentModDirectory ?? mod.ModPath.Name; @@ -270,13 +270,13 @@ public class ModPanelEditTab : ITab var (disabled, tt) = _state switch { - Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), - Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), - Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), - Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), - Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true, + ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), + ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), + ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), + ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), + ModManager.NewDirectoryState.ContainsInvalidSymbols => (true, $"{_currentModDirectory} contains invalid symbols for FFXIV."), _ => (true, "Unknown error."), }; @@ -317,7 +317,7 @@ public class ModPanelEditTab : ITab ImGui.OpenPopup(PopupName); } - public static void DrawPopup(Mod.Manager modManager) + public static void DrawPopup(ModManager modManager) { if (_mod == null) return; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index b0936d90..72dbe9fa 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab : ITab { private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; private readonly PenumbraApi _api; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private bool _inherited; private ModSettings _settings = null!; @@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab private bool _empty; private int? _currentPriority = null; - public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector, + public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, TutorialService tutorial, PenumbraApi api, Configuration config) { _collectionManager = collectionManager; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index d0006b17..77f2b1f2 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -27,7 +27,7 @@ public class ModPanelTabBar public readonly ModPanelChangedItemsTab ChangedItems; public readonly ModPanelEditTab Edit; private readonly ModEditWindow _modEditWindow; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly TutorialService _tutorial; public readonly ITab[] Tabs; @@ -35,7 +35,7 @@ public class ModPanelTabBar private Mod? _lastMod = null; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, - ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager, + ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, TutorialService tutorial) { _modEditWindow = modEditWindow; @@ -107,7 +107,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((Mod.SubMod) mod.Default); + _modEditWindow.ChangeOption((SubMod) mod.Default); _modEditWindow.IsOpen = true; } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 22eecf7e..e5f9083e 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -16,10 +16,10 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly PenumbraApi _api; - public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api) + public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api) { _collectionManager = collectionManager; _api = api; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 4825b4aa..afd739a8 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab { private readonly CommunicatorService _communicator; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TutorialService _tutorial; private readonly SpecialCombo _specialCollectionCombo; @@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab private readonly InheritanceUi _inheritance; private readonly IndividualCollectionUi _individualCollections; - public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager, + public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager, TutorialService tutorial, Configuration config) { _communicator = communicator; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 2c89e673..09dbc097 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,8 +34,8 @@ public class DebugTab : ITab private readonly StartTracker _timer; private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; - private readonly Mod.Manager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly ValidityChecker _validityChecker; private readonly HttpApi _httpApi; private readonly ActorService _actorService; @@ -52,8 +52,8 @@ public class DebugTab : ITab private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly CutsceneService _cutsceneService; - public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, - ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService, + public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, + ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 45e244d8..5f455189 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -17,9 +17,9 @@ namespace Penumbra.UI.Tabs; public class EffectiveTab : ITab { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public EffectiveTab(ModCollection.Manager collectionManager) + public EffectiveTab(CollectionManager collectionManager) => _collectionManager = collectionManager; public ReadOnlySpan Label diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index f6e0f2ed..5c1340a5 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -23,13 +23,13 @@ public class ModsTab : ITab private readonly ModFileSystemSelector _selector; private readonly ModPanel _panel; private readonly TutorialService _tutorial; - private readonly Mod.Manager _modManager; - private readonly ModCollection.Manager _collectionManager; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; private readonly RedrawService _redrawService; private readonly Configuration _config; private readonly CollectionsTab _collectionsTab; - public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel, + public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) { _modManager = modManager; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a0dd3c9f..039a648f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -31,14 +31,14 @@ public class SettingsTab : ITab private readonly TutorialService _tutorial; private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly DalamudServices _dalamud; public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, - FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, + FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud) { _config = config; From fbe2ed1a7117ec812a495acfdfdc2a6010b5d2dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 Mar 2023 17:09:19 +0200 Subject: [PATCH 0838/2451] Bunch of work on Option Editor. --- Penumbra/Collections/CollectionManager.cs | 21 +- Penumbra/Mods/Editor/DuplicateManager.cs | 18 +- Penumbra/Mods/Editor/ModFileEditor.cs | 4 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 7 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 10 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 8 +- Penumbra/Mods/Manager/Mod.Manager.cs | 113 ++- ....Manager.Options.cs => ModOptionEditor.cs} | 763 +++++++++--------- Penumbra/Mods/Mod.BasePath.cs | 2 +- Penumbra/Mods/Mod.Creator.cs | 8 +- Penumbra/Mods/Mod.Files.cs | 29 +- Penumbra/Mods/Mod.Meta.Migration.cs | 4 +- Penumbra/Mods/Subclasses/IModGroup.cs | 135 ++-- Penumbra/Penumbra.cs | 69 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/Services/CommunicatorService.cs | 12 +- Penumbra/Services/FilenameService.cs | 17 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 12 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 88 +- Penumbra/Util/SaveService.cs | 21 + 21 files changed, 749 insertions(+), 595 deletions(-) rename Penumbra/Mods/Manager/{Mod.Manager.Options.cs => ModOptionEditor.cs} (54%) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index d03d3ddf..c1510686 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -19,8 +19,9 @@ namespace Penumbra.Collections; public sealed partial class CollectionManager : IDisposable, IEnumerable { - private readonly Mods.ModManager _modManager; + private readonly ModManager _modManager; private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly Configuration _config; @@ -57,7 +58,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable _collections; public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals) + ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals, + SaveService saveService) { using var time = timer.Measure(StartTimeType.Collections); _communicator = communicator; @@ -65,12 +67,13 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable _duplicates = new(); - public DuplicateManager(ModFileCollection files, ModManager modManager) + public DuplicateManager(ModFileCollection files, ModManager modManager, SaveService saveService) { - _files = files; - _modManager = modManager; + _files = files; + _modManager = modManager; + _saveService = saveService; } public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates @@ -76,16 +79,13 @@ public class DuplicateManager if (useModManager) { - _modManager.OptionSetFiles(mod, groupIdx, optionIdx, dict); + _modManager.OptionEditor.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); + _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 29a06c44..d5e5fea6 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -34,7 +34,7 @@ public class ModFileEditor num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - Penumbra.ModManager.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + _modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); _files.UpdatePaths(mod, option); return num; @@ -54,7 +54,7 @@ public class ModFileEditor var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (newDict.Count != subMod.Files.Count) - _modManager.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); } ModEditor.ApplyToAllOptions(mod, HandleSubMod); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index f536935d..b1dced58 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -109,7 +109,7 @@ public class ModMetaEditor if (!Changes) return; - _modManager.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + _modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index aff491c7..bdd968cd 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -11,7 +11,7 @@ namespace Penumbra.Mods; public class ModNormalizer { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -280,9 +280,8 @@ public class ModNormalizer private void ApplyRedirections() { foreach (var option in Mod.AllSubMods.OfType()) - { - _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); - } + _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, + _redirections[option.GroupIdx + 1][option.OptionIdx]); ++Step; } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 29da93c1..e411ad70 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -22,11 +22,11 @@ public class ModSwapEditor public void Apply(Mod mod, int groupIdx, int optionIdx) { - if (Changes) - { - _modManager.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); - Changes = false; - } + if (!Changes) + return; + + _modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + Changes = false; } public bool Changes { get; private set; } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 6b6e3111..964aee70 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -38,7 +38,7 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) + public bool WriteMod( ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) { var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); @@ -82,9 +82,9 @@ public class ItemSwapContainer } } - Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); - Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); - Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); + manager.OptionEditor.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); + manager.OptionEditor.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); + manager.OptionEditor.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); return true; } catch( Exception e ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index dd40e9d4..7e3310e4 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -2,12 +2,76 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class ModManager : IReadOnlyList +public sealed class ModManager2 : IReadOnlyList, IDisposable +{ + public readonly ModDataEditor DataEditor; + public readonly ModOptionEditor OptionEditor; + + /// + /// An easily accessible set of new mods. + /// Mods are added when they are created or imported. + /// Mods are removed when they are deleted or when they are toggled in any collection. + /// Also gets cleared on mod rediscovery. + /// + public readonly HashSet NewMods = new(); + + public Mod this[int idx] + => _mods[idx]; + + public Mod this[Index idx] + => _mods[idx]; + + public int Count + => _mods.Count; + + public IEnumerator GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Try to obtain a mod by its directory name (unique identifier, preferred), + /// or the first mod of the given name if no directory fits. + /// + public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in _mods) + { + if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) + { + mod = m; + return true; + } + + if (m.Name == modName) + mod ??= m; + } + + return mod != null; + } + + /// The actual list of mods. + private readonly List _mods = new(); + + public ModManager2(ModDataEditor dataEditor, ModOptionEditor optionEditor) + { + DataEditor = dataEditor; + OptionEditor = optionEditor; + } + + public void Dispose() + { } +} + +public sealed partial class ModManager : IReadOnlyList, IDisposable { // Set when reading Config and migrating from v4 to v5. public static bool MigrateModBackups = false; @@ -38,21 +102,29 @@ public sealed partial class ModManager : IReadOnlyList private readonly Configuration _config; private readonly CommunicatorService _communicator; public readonly ModDataEditor DataEditor; + public readonly ModOptionEditor OptionEditor; - public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) + public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, + ModOptionEditor optionEditor) { using var timer = time.Measure(StartTimeType.Mods); _config = config; _communicator = communicator; DataEditor = dataEditor; + OptionEditor = optionEditor; ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory(config.ModDirectory, true); UpdateExportDirectory(_config.ExportDirectory, false); - ModOptionChanged += OnModOptionChange; - ModPathChanged += OnModPathChange; + _communicator.ModOptionChanged.Event += OnModOptionChange; + ModPathChanged += OnModPathChange; DiscoverMods(); } + public void Dispose() + { + _communicator.ModOptionChanged.Event -= OnModOptionChange; + } + // Try to obtain a mod by its directory name (unique identifier, preferred), // or the first mod of the given name if no directory fits. @@ -73,4 +145,37 @@ public sealed partial class ModManager : IReadOnlyList return mod != null; } + + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + { + if (type == ModOptionChangeType.PrepareChange) + return; + + bool ComputeChangedItems() + { + mod.ComputeChangedItems(); + return true; + } + + // State can not change on adding groups, as they have no immediate options. + var unused = type switch + { + ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), + ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), + ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), + ModOptionChangeType.DisplayChange => false, + _ => false, + }; + } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs similarity index 54% rename from Penumbra/Mods/Manager/Mod.Manager.Options.cs rename to Penumbra/Mods/Manager/ModOptionEditor.cs index 0c86e82d..974dd837 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,377 +1,386 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class ModManager -{ - public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); - public event ModOptionChangeDelegate ModOptionChanged; - - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) - { - var group = mod._groups[groupIdx]; - if (group.Type == type) - return; - - mod._groups[groupIdx] = group.Convert(type); - ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } - - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) - { - var group = mod._groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; - - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } - - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - group.DeleteFile(mod.ModPath, groupIdx); - - var _ = group switch - { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } - - public void AddModGroup(Mod mod, GroupType type, string newName) - { - if (!VerifyFileName(mod, null, newName, true)) - return; - - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - - mod._groups.Add(type == GroupType.Multi - ? new MultiModGroup - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup - { - Name = newName, - Priority = maxPriority, - }); - ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); - } - - public void DeleteModGroup(Mod mod, int groupIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod._groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); - group.DeleteFile(mod.ModPath, groupIdx); - ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (mod._groups.Move(groupIdxFrom, groupIdxTo)) - { - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); - } - } - - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) - { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } - - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - if (group.Description == newDescription) - return; - - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) - return; - - s.Description = newDescription; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) - { - var group = mod._groups[groupIdx]; - if (group.Priority == newPriority) - return; - - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) - return; - - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void AddOption(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) - { - if (option is not SubMod o) - return; - - var group = mod._groups[groupIdx]; - if (group.Count > 63) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(o); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((o, priority)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); - break; - } - - group.UpdatePositions(optionIdx); - ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); - } - - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) - { - var group = mod._groups[groupIdx]; - if (group.MoveOption(optionIdxFrom, optionIdxTo)) - ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); - } - - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData = replacements; - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData = swaps; - ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); - } - - public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.ChatService.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); - - return false; - } - - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) - { - if (groupIdx == -1 && optionIdx == 0) - return mod._default; - - return mod._groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; - } - - private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) - { - if (type == ModOptionChangeType.PrepareChange) - return; - - // File deletion is handled in the actual function. - if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) - { - mod.SaveAllGroups(); - } - else - { - if (groupIdx == -1) - mod.SaveDefaultModDelayed(); - else - IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); - } - - bool ComputeChangedItems() - { - mod.ComputeChangedItems(); - return true; - } - - // State can not change on adding groups, as they have no immediate options. - var unused = type switch - { - ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), - ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), - ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), - ModOptionChangeType.DisplayChange => false, - _ => false, - }; - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods; + + + +public class ModOptionEditor +{ + private readonly CommunicatorService _communicator; + private readonly FilenameService _filenames; + private readonly SaveService _saveService; + + public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames) + { + _communicator = communicator; + _saveService = saveService; + _filenames = filenames; + } + + /// Change the type of a group given by mod and index to type, if possible. + public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) + { + var group = mod._groups[groupIdx]; + if (group.Type == type) + return; + + mod._groups[groupIdx] = group.Convert(type); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); + } + + /// Change the settings stored as default options in a mod. + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) + { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + } + + /// Rename an option group if possible. + public void RenameModGroup(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(mod, group, newName, true)) + return; + + _saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx)); + var _ = group switch + { + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; + + _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); + } + + /// Add a new mod, empty option group of the given type and name. + public void AddModGroup(Mod mod, GroupType type, string newName) + { + if (!VerifyFileName(mod, null, newName, true)) + return; + + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; + + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup + { + Name = newName, + Priority = maxPriority, + } + : new SingleModGroup + { + Name = newName, + Priority = maxPriority, + }); + _saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } + + /// Delete a given option group. Fires an event to prepare before actually deleting. + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + _saveService.SaveAllOptionGroups(mod); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + } + + /// Move the index of a given option group. + public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) + { + if (!mod._groups.Move(groupIdxFrom, groupIdxTo)) + return; + + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + _saveService.SaveAllOptionGroups(mod); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); + } + + /// Change the description of the given option group. + public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + if (group.Description == newDescription) + return; + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); + } + + /// Change the description of the given option. + public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) + return; + + s.Description = newDescription; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + /// Change the internal priority of the given option group. + public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) + { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) + return; + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); + } + + /// Change the internal priority of the given option. + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup: + ChangeGroupPriority(mod, groupIdx, newPriority); + break; + case MultiModGroup m: + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + return; + + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); + return; + } + } + + /// Rename the given option. + public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup s: + if (s.OptionData[optionIdx].Name == newName) + return; + + s.OptionData[optionIdx].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) + return; + + option.Name = newName; + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + /// Add a new empty option of the given name for the given group. + public void AddOption(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(subMod); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((subMod, 0)); + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + /// Add an existing option to a given group with a given priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + { + if (option is not SubMod o) + return; + + var group = mod._groups[groupIdx]; + if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return; + } + + o.SetPosition(groupIdx, group.Count); + + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(o); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((o, priority)); + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + /// Delete the given option from the given group. + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + { + var group = mod._groups[groupIdx]; + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + switch (group) + { + case SingleModGroup s: + s.OptionData.RemoveAt(optionIdx); + + break; + case MultiModGroup m: + m.PrioritizedOptions.RemoveAt(optionIdx); + break; + } + + group.UpdatePositions(optionIdx); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + } + + /// Move an option inside the given option group. + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + { + var group = mod._groups[groupIdx]; + if (!group.MoveOption(optionIdxFrom, optionIdxTo)) + return; + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + } + + /// Set the meta manipulations for a given option. Replaces existing manipulations. + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.ManipulationData = manipulations; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + } + + /// Set the file redirections for a given option. Replaces existing redirections. + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileData = replacements; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + } + + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + { + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + } + } + + /// Set the file swaps for a given option. Replaces existing swaps. + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileSwapData = swaps; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + } + + + /// Verify that a new option group name is unique in this mod. + public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.ChatService.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); + + return false; + } + + /// Update the indices stored in options from a given group on. + private static void UpdateSubModPositions(Mod mod, int fromGroup) + { + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + { + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); + } + } + + /// Get the correct option for the given group and option index. + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + { + if (groupIdx == -1 && optionIdx == 0) + return mod._default; + + return mod._groups[groupIdx] switch + { + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, + _ => throw new InvalidOperationException(), + }; + } +} diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index fb71ade6..41da763b 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -101,7 +101,7 @@ public partial class Mod if( changes ) { - SaveAllGroups(); + Penumbra.SaveService.SaveAllOptionGroups(this); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index f67dbf33..87ebd502 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -4,10 +4,8 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Import.Structs; @@ -79,8 +77,8 @@ public partial class Mod Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.Save( group, baseFolder, index ); + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); break; } case GroupType.Single: @@ -93,7 +91,7 @@ public partial class Mod DefaultSettings = defaultSettings, }; group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.Save( group, baseFolder, index ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); break; } } diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 6091ba1b..979a13fc 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -103,7 +103,7 @@ public partial class Mod var group = LoadModGroup( this, file, _groups.Count ); if( group != null && _groups.All( g => g.Name != group.Name ) ) { - changes = changes || group.FileName( ModPath, _groups.Count ) != file.FullName; + changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName; _groups.Add( group ); } else @@ -114,32 +114,7 @@ public partial class Mod if( changes ) { - SaveAllGroups(); - } - } - - // Delete all existing group files and save them anew. - // Used when indices change in complex ways. - internal void SaveAllGroups() - { - foreach( var file in GroupFiles ) - { - try - { - if( file.Exists ) - { - file.Delete(); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete outdated group file {file}:\n{e}" ); - } - } - - foreach( var (group, index) in _groups.WithIndex() ) - { - IModGroup.Save( group, ModPath, index ); + Penumbra.SaveService.SaveAllOptionGroups(this); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 5a07fd29..9b09c294 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -85,8 +85,8 @@ public sealed partial class Mod mod._default.FileSwapData.Add(gamePath, swapPath); mod._default.IncorporateMetaChanges(mod.ModPath, true); - foreach (var (group, index) in mod.Groups.WithIndex()) - IModGroup.Save(group, mod.ModPath, index); + foreach (var (_, index) in mod.Groups.WithIndex()) + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 0c1ebf2f..45db2b59 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -2,24 +2,25 @@ using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; -using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods; -public interface IModGroup : IEnumerable< ISubMod > +public interface IModGroup : IEnumerable { public const int MaxMultiOptions = 32; - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public int Priority { get; } - public uint DefaultSettings { get; set; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public int Priority { get; } + public uint DefaultSettings { get; set; } - public int OptionPriority( Index optionIdx ); + public int OptionPriority(Index optionIdx); - public ISubMod this[ Index idx ] { get; } + public ISubMod this[Index idx] { get; } public int Count { get; } @@ -28,72 +29,76 @@ public interface IModGroup : IEnumerable< ISubMod > { GroupType.Single => Count > 1, GroupType.Multi => Count > 0, - _ => false, + _ => false, }; - public string FileName( DirectoryInfo basePath, int groupIdx ) - => Path.Combine( basePath.FullName, $"group_{groupIdx + 1:D3}_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); + public IModGroup Convert(GroupType type); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public void UpdatePositions(int from = 0); +} - public void DeleteFile( DirectoryInfo basePath, int groupIdx ) +public readonly struct ModSaveGroup : ISavable +{ + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly ISubMod? _defaultMod; + + public ModSaveGroup(Mod mod, int groupIdx) { - var file = FileName( basePath, groupIdx ); - if( !File.Exists( file ) ) - { - return; - } - - try - { - File.Delete( file ); - Penumbra.Log.Debug( $"Deleted group file {file} for group {groupIdx + 1}: {Name}." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete file {file}:\n{e}" ); - throw; - } + _basePath = mod.ModPath; + if (_groupIdx < 0) + _defaultMod = mod.Default; + else + _group = mod.Groups[groupIdx]; + _groupIdx = groupIdx; } - public static void SaveDelayed( IModGroup group, DirectoryInfo basePath, int groupIdx ) + public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx) { - Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", - () => SaveModGroup( group, basePath, groupIdx ) ); + _basePath = basePath; + _group = group; + _groupIdx = groupIdx; } - public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx ) - => SaveModGroup( group, basePath, groupIdx ); - - private static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx ) + public ModSaveGroup(DirectoryInfo basePath, ISubMod @default) { - var file = group.FileName( basePath, groupIdx ); - using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); - using var writer = new StreamWriter( s ); - using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - j.WriteStartObject(); - j.WritePropertyName( nameof( group.Name ) ); - j.WriteValue( group.Name ); - j.WritePropertyName( nameof( group.Description ) ); - j.WriteValue( group.Description ); - j.WritePropertyName( nameof( group.Priority ) ); - j.WriteValue( group.Priority ); - j.WritePropertyName( nameof( Type ) ); - j.WriteValue( group.Type.ToString() ); - j.WritePropertyName( nameof( group.DefaultSettings ) ); - j.WriteValue( group.DefaultSettings ); - j.WritePropertyName( "Options" ); - j.WriteStartArray(); - for( var idx = 0; idx < group.Count; ++idx ) + _basePath = basePath; + _groupIdx = -1; + _defaultMod = @default; + } + + public string ToFilename(FilenameService fileNames) + => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + if (_groupIdx >= 0) { - ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == GroupType.Multi ? group.OptionPriority( idx ) : null ); + j.WriteStartObject(); + j.WritePropertyName(nameof(_group.Name)); + j.WriteValue(_group!.Name); + j.WritePropertyName(nameof(_group.Description)); + j.WriteValue(_group.Description); + j.WritePropertyName(nameof(_group.Priority)); + j.WriteValue(_group.Priority); + j.WritePropertyName(nameof(Type)); + j.WriteValue(_group.Type.ToString()); + j.WritePropertyName(nameof(_group.DefaultSettings)); + j.WriteValue(_group.DefaultSettings); + j.WritePropertyName("Options"); + j.WriteStartArray(); + for (var idx = 0; idx < _group.Count; ++idx) + ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type == GroupType.Multi ? _group.OptionPriority(idx) : null); + + j.WriteEndArray(); + j.WriteEndObject(); + } + else + { + ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); } - - j.WriteEndArray(); - j.WriteEndObject(); - Penumbra.Log.Debug( $"Saved group file {file} for group {groupIdx + 1}: {group.Name}." ); } - - public IModGroup Convert( GroupType type ); - public bool MoveOption( int optionIdxFrom, int optionIdxTo ); - public void UpdatePositions( int from = 0 ); -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 85a7c717..78931cf0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -38,17 +38,18 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static Logger Log { get; private set; } = null!; - public static ChatService ChatService { get; private set; } = null!; - public static SaveService SaveService { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; + public static FilenameService Filenames { get; private set; } = null!; + public static SaveService SaveService { get; private set; } = null!; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static ModManager ModManager { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; @@ -63,13 +64,13 @@ public class Penumbra : IDalamudPlugin public static PerformanceTracker Performance { get; private set; } = null!; - public readonly PathResolver PathResolver; - public readonly RedrawService RedrawService; - public readonly ModFileSystem ModFileSystem; - public HttpApi HttpApi = null!; - internal ConfigWindow? ConfigWindow { get; private set; } - private PenumbraWindowSystem? _windowSystem; - private bool _disposed; + public readonly PathResolver PathResolver; + public readonly RedrawService RedrawService; + public readonly ModFileSystem ModFileSystem; + public HttpApi HttpApi = null!; + internal ConfigWindow? ConfigWindow { get; private set; } + private PenumbraWindowSystem? _windowSystem; + private bool _disposed; private readonly PenumbraNew _tmp; @@ -80,29 +81,30 @@ public class Penumbra : IDalamudPlugin { _tmp = new PenumbraNew(this, pluginInterface); ChatService = _tmp.Services.GetRequiredService(); + Filenames = _tmp.Services.GetRequiredService(); SaveService = _tmp.Services.GetRequiredService(); Performance = _tmp.Services.GetRequiredService(); ValidityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); - ResidentResources = _tmp.Services.GetRequiredService(); + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); + ResidentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModManager = _tmp.Services.GetRequiredService(); - CollectionManager = _tmp.Services.GetRequiredService(); - TempCollections = _tmp.Services.GetRequiredService(); - ModFileSystem = _tmp.Services.GetRequiredService(); - RedrawService = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); + TempCollections = _tmp.Services.GetRequiredService(); + ModFileSystem = _tmp.Services.GetRequiredService(); + RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ResourceLoader = _tmp.Services.GetRequiredService(); + ResourceLoader = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { PathResolver = _tmp.Services.GetRequiredService(); @@ -112,7 +114,8 @@ public class Penumbra : IDalamudPlugin SetupApi(); ValidityChecker.LogExceptions(); - Log.Information($"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + Log.Information( + $"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); @@ -129,8 +132,8 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); - var api = _tmp.Services.GetRequiredService(); - HttpApi = _tmp.Services.GetRequiredService(); + var api = _tmp.Services.GetRequiredService(); + HttpApi = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); if (Config.EnableHttpApi) HttpApi.CreateWebServer(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index fcf2c386..9d976a9a 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -93,6 +93,7 @@ public class PenumbraNew // Add Mod Services services.AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index f4381883..18c64eac 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -45,13 +45,22 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); - /// + /// /// Parameter is the type of data change for the mod, which can be multiple flags. /// Parameter is the changed mod. /// Parameter is the old name of the mod in case of a name change, and null otherwise. /// public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); + /// + /// Parameter is the type option change. + /// Parameter is the changed mod. + /// Parameter is the index of the changed group inside the mod. + /// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. + /// Parameter is the index of the group an option was moved to. + /// + public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged)); + public void Dispose() { CollectionChange.Dispose(); @@ -60,5 +69,6 @@ public class CommunicatorService : IDisposable CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); ModDataChanged.Dispose(); + ModOptionChanged.Dispose(); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 26721257..d7060e05 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -71,4 +71,21 @@ public class FilenameService /// Obtain the path of the meta file given a mod directory. public string ModMetaPath(string modDirectory) => Path.Combine(modDirectory, "meta.json"); + + /// Obtain the path of the file describing a given option group by its index and the mod. If the index is < 0, return the path for the default mod file. + public string OptionGroupFile(Mod mod, int index) + => OptionGroupFile(mod.ModPath.FullName, index, index >= 0 ? mod.Groups[index].Name : string.Empty); + + /// Obtain the path of the file describing a given option group by its index, name and basepath. If the index is < 0, return the path for the default mod file. + public string OptionGroupFile(string basePath, int index, string name) + { + var fileName = index >= 0 + ? $"group_{index + 1:D3}_{name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" + : "default_mod.json"; + return Path.Combine(basePath, fileName); + } + + /// Enumerate all group files for a given mod. + public IEnumerable GetOptionGroupFiles(Mod mod) + => mod.ModPath.EnumerateFiles("group_*.json"); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 48333d6f..9a0af228 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -270,7 +270,7 @@ public class ItemSwapTab : IDisposable, ITab _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager.Last(), + if (!_swapData.WriteMod(_modManager, _modManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) _modManager.DeleteMod(_modManager.Count - 1); } @@ -296,16 +296,16 @@ public class ItemSwapTab : IDisposable, ITab { if (_selectedGroup == null) { - _modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); + _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); _selectedGroup = _mod.Groups.Last(); groupCreated = true; } - _modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); optionCreated = true; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, + if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) throw new Exception("Failure writing files for mod swap."); @@ -317,11 +317,11 @@ public class ItemSwapTab : IDisposable, ITab try { if (optionCreated && _selectedGroup != null) - _modManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); if (groupCreated) { - _modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _selectedGroup = null; } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 3d0068d2..f8a82b75 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Util; @@ -20,7 +21,8 @@ namespace Penumbra.UI.ModsTab; public class ModPanelEditTab : ITab { private readonly ChatService _chat; - private readonly ModManager _modManager; + private readonly FilenameService _filenames; + private readonly ModManager _modManager; private readonly ModFileSystem _fileSystem; private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; @@ -34,14 +36,15 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, - ModEditWindow editWindow, ModEditor editor) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames) { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _chat = chat; - _editWindow = editWindow; - _editor = editor; + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + _editor = editor; + _filenames = filenames; } public ReadOnlySpan Label @@ -129,7 +132,7 @@ public class ModPanelEditTab : ITab if (ImGui.Button("Update Bibo Material", buttonSize)) { _editor.LoadMod(_mod); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); + _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); _editor.MdlMaterialEditor.SaveAllModels(); _editWindow.UpdateModels(); @@ -189,7 +192,7 @@ public class ModPanelEditTab : ITab var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description)); ImGui.SameLine(); var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod)); @@ -235,13 +238,13 @@ public class ModPanelEditTab : ITab ImGui.SameLine(); - var nameValid = modManager.VerifyFileName(mod, null, _newGroupName, false); + var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, !nameValid, true)) return; - modManager.AddModGroup(mod, GroupType.Single, _newGroupName); + modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); Reset(); } } @@ -249,7 +252,7 @@ public class ModPanelEditTab : ITab /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { - private static string? _currentModDirectory; + private static string? _currentModDirectory; private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; public static void Reset() @@ -297,14 +300,16 @@ public class ModPanelEditTab : ITab /// Open a popup to edit a multi-line mod or option description. private static class DescriptionEdit { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDescriptionOptionIdx = -1; + private static Mod? _mod; + private static FilenameService? _fileNames; - public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) + public static void OpenPopup(FilenameService filenames, Mod mod, int groupIdx, int optionIdx = -1) { + _fileNames = filenames; _newDescriptionIdx = groupIdx; _newDescriptionOptionIdx = optionIdx; _newDescription = groupIdx < 0 @@ -353,9 +358,10 @@ public class ModPanelEditTab : ITab break; case >= 0: if (_newDescriptionOptionIdx < 0) - modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); else - modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription); + modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, + _newDescription); break; } @@ -384,18 +390,18 @@ public class ModPanelEditTab : ITab .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.RenameModGroup(_mod, groupIdx, newGroupName); + _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName); ImGuiUtil.HoverTooltip("Group Name"); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.DeleteModGroup(_mod, groupIdx)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx)); ImGui.SameLine(); if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.ChangeGroupPriority(_mod, groupIdx, priority); + _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority); ImGuiUtil.HoverTooltip("Group Priority"); @@ -405,7 +411,7 @@ public class ModPanelEditTab : ITab var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); ImGui.SameLine(); tt = groupIdx == _mod.Groups.Count - 1 @@ -413,16 +419,16 @@ public class ModPanelEditTab : ITab : $"Move this group down to group {groupIdx + 2}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1)); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, groupIdx)); ImGui.SameLine(); - var fileName = group.FileName(_mod.ModPath, groupIdx); + var fileName = _filenames.OptionGroupFile(_mod, groupIdx); var fileExists = File.Exists(fileName); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." @@ -491,7 +497,7 @@ public class ModPanelEditTab : ITab if (group.Type == GroupType.Single) { if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) - panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } @@ -499,7 +505,7 @@ public class ModPanelEditTab : ITab { var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption ? group.DefaultSettings | (1u << optionIdx) : group.DefaultSettings & ~(1u << optionIdx)); @@ -508,17 +514,17 @@ public class ModPanelEditTab : ITab ImGui.TableNextColumn(); if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._filenames, panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.DeleteOption(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); if (group.Type != GroupType.Multi) @@ -526,7 +532,7 @@ public class ModPanelEditTab : ITab if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, 50 * UiHelpers.Scale)) - panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); ImGuiUtil.HoverTooltip("Option priority."); } @@ -560,7 +566,7 @@ public class ModPanelEditTab : ITab tt, !(canAddGroup && validName), true)) return; - panel._modManager.AddOption(mod, groupIdx, _newOptionName); + panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); _newOptionName = string.Empty; } @@ -591,7 +597,7 @@ public class ModPanelEditTab : ITab if (_dragDropGroupIdx == groupIdx) { var sourceOption = _dragDropOptionIdx; - panel._delayedActions.Enqueue(() => panel._modManager.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); } else { @@ -604,9 +610,9 @@ public class ModPanelEditTab : ITab var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); panel._delayedActions.Enqueue(() => { - panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.AddOption(panel._mod, groupIdx, option, priority); - panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); + panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); }); } } @@ -633,12 +639,12 @@ public class ModPanelEditTab : ITab return; if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) - _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Single); + _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) - _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); + _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); style.Pop(); if (!canSwitchToMulti) diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Util/SaveService.cs index 221ead81..8b256504 100644 --- a/Penumbra/Util/SaveService.cs +++ b/Penumbra/Util/SaveService.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using OtterGui.Classes; using OtterGui.Log; +using Penumbra.Mods; using Penumbra.Services; namespace Penumbra.Util; @@ -94,4 +95,24 @@ public class SaveService _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); } } + + /// Immediately delete all existing option group files for a mod and save them anew. + public void SaveAllOptionGroups(Mod mod) + { + foreach (var file in _fileNames.GetOptionGroupFiles(mod)) + { + try + { + if (file.Exists) + file.Delete(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete outdated group file {file}:\n{e}"); + } + } + + for (var i = 0; i < mod.Groups.Count; ++i) + ImmediateSave(new ModSaveGroup(mod, i)); + } } From c31a2f5a421c17ec01f274eda9598ea111f55d23 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 Mar 2023 18:09:41 +0200 Subject: [PATCH 0839/2451] Remove SaveDefaultMod. --- Penumbra/Mods/Mod.Creator.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 54 +++++++++++- Penumbra/Mods/Mod.Meta.Migration.cs | 2 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 85 ------------------- ...iles.MultiModGroup.cs => MultiModGroup.cs} | 6 -- ...es.SingleModGroup.cs => SingleModGroup.cs} | 0 Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/PenumbraNew.cs | 2 - Penumbra/UI/ModsTab/ModPanelEditTab.cs | 9 +- 9 files changed, 60 insertions(+), 102 deletions(-) rename Penumbra/Mods/Subclasses/{Mod.Files.MultiModGroup.cs => MultiModGroup.cs} (98%) rename Penumbra/Mods/Subclasses/{Mod.Files.SingleModGroup.cs => SingleModGroup.cs} (100%) diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 87ebd502..86baa7d9 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -140,7 +140,7 @@ public partial class Mod } mod._default.IncorporateMetaChanges( directory, true ); - mod.SaveDefaultMod(); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); } /// Return the name of a new valid directory based on the base directory and the given name. diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 979a13fc..97910191 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; -using OtterGui; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -113,8 +112,59 @@ public partial class Mod } if( changes ) - { Penumbra.SaveService.SaveAllOptionGroups(this); + } + + private void LoadDefaultOption() + { + var defaultFile = Penumbra.Filenames.OptionGroupFile(this, -1); + _default.SetPosition(-1, 0); + try + { + if (!File.Exists(defaultFile)) + { + _default.Load(ModPath, new JObject(), out _); + } + else + { + _default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not parse default file for {Name}:\n{e}"); } } + + public void WriteAllTexToolsMeta() + { + try + { + _default.WriteTexToolsMeta(ModPath); + foreach (var group in Groups) + { + var dir = Creator.NewOptionDirectory(ModPath, group.Name); + if (!dir.Exists) + { + dir.Create(); + } + + foreach (var option in group.OfType()) + { + var optionDir = Creator.NewOptionDirectory(dir, option.Name); + if (!optionDir.Exists) + { + optionDir.Create(); + } + + option.WriteTexToolsMeta(optionDir); + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); + } + } + } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 9b09c294..d993cef0 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -114,7 +114,7 @@ public sealed partial class Mod } fileVersion = 1; - mod.SaveDefaultMod(); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); return true; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index f1a4a24c..8b5ba641 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Import; using Penumbra.Meta.Manipulations; @@ -11,90 +10,6 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class Mod -{ - internal string DefaultFile - => Path.Combine( ModPath.FullName, "default_mod.json" ); - - // The default mod contains setting-independent sets of file replacements, file swaps and meta changes. - // Every mod has an default mod, though it may be empty. - public void SaveDefaultMod() - { - var defaultFile = DefaultFile; - - using var stream = File.Exists( defaultFile ) - ? File.Open( defaultFile, FileMode.Truncate ) - : File.Open( defaultFile, FileMode.CreateNew ); - - using var w = new StreamWriter( stream ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var serializer = new JsonSerializer - { - Formatting = Formatting.Indented, - }; - ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 ); - } - - internal void SaveDefaultModDelayed() - => Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod ); - - private void LoadDefaultOption() - { - var defaultFile = DefaultFile; - _default.SetPosition( -1, 0 ); - try - { - if( !File.Exists( defaultFile ) ) - { - _default.Load( ModPath, new JObject(), out _ ); - } - else - { - _default.Load( ModPath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not parse default file for {Name}:\n{e}" ); - } - } - - public void WriteAllTexToolsMeta() - { - try - { - _default.WriteTexToolsMeta( ModPath ); - foreach( var group in Groups ) - { - var dir = Creator.NewOptionDirectory( ModPath, group.Name ); - if( !dir.Exists ) - { - dir.Create(); - } - - foreach( var option in group.OfType< SubMod >() ) - { - var optionDir = Creator.NewOptionDirectory( dir, option.Name ); - if( !optionDir.Exists ) - { - optionDir.Create(); - } - - option.WriteTexToolsMeta( optionDir ); - } - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error writing TexToolsMeta:\n{e}" ); - } - } - - - -} - /// /// A sub mod is a collection of /// - file replacements diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs similarity index 98% rename from Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs rename to Penumbra/Mods/Subclasses/MultiModGroup.cs index 44314290..0383f763 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -9,14 +9,8 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Util; namespace Penumbra.Mods; - -public partial class Mod -{ - -} /// Groups that allow all available options to be selected at once. public sealed class MultiModGroup : IModGroup diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs similarity index 100% rename from Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs rename to Penumbra/Mods/Subclasses/SingleModGroup.cs diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index d46d00d5..b7c0d6c9 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -83,7 +83,7 @@ public class TemporaryMod : IMod foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) defaultMod.ManipulationData.Add( manip ); - mod.SaveDefaultMod(); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(dir, defaultMod)); modManager.AddMod( dir ); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); } diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 9d976a9a..6766d3e8 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -1,14 +1,12 @@ using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Data; -using Penumbra.Interop; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceTree; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f8a82b75..bb1b6d75 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -83,7 +83,7 @@ public class ModPanelEditTab : ITab _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(_modManager, _mod); + AddOptionGroup.Draw(_filenames, _modManager, _mod); UiHelpers.DefaultLineSpace(); for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) @@ -222,19 +222,20 @@ public class ModPanelEditTab : ITab public static void Reset() => _newGroupName = string.Empty; - public static void Draw(ModManager modManager, Mod mod) + public static void Draw(FilenameService filenames, ModManager modManager, Mod mod) { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); ImGui.SameLine(); - var fileExists = File.Exists(mod.DefaultFile); + var defaultFile = filenames.OptionGroupFile(mod, -1); + var fileExists = File.Exists(defaultFile); var tt = fileExists ? "Open the default option json file in the text editor of your choice." : "The default option json file does not exist."; if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(mod.DefaultFile) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(defaultFile) { UseShellExecute = true }); ImGui.SameLine(); From 2b7292adb832e1075a4f35a3b5eda2cba234a03a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 Mar 2023 16:57:42 +0200 Subject: [PATCH 0840/2451] Fix issue with import state popup --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index cbc834ac..d1ec76ad 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -38,6 +38,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector SortMode - => Penumbra.Config.SortMode; + => _config.SortMode; protected override uint ExpandedFolderColor => ColorId.FolderExpanded.Value(_config); @@ -116,7 +118,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector ColorId.FolderLine.Value(_config); protected override bool FoldersDefaultOpen - => Penumbra.Config.OpenFoldersByDefault; + => _config.OpenFoldersByDefault; protected override void DrawPopups() { @@ -129,7 +131,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Add an import mods button that opens a file selector. private void AddImportModButton(Vector2 size) { + _infoPopupId = ImGui.GetID("Import Status"); var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); @@ -242,7 +245,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), AddNewMod, _config, _modEditor, _modManager); - ImGui.OpenPopup("Import Status"); + ImGui.OpenPopup(_infoPopupId); }, 0, modPath, _config.AlwaysOpenDefaultImport); } @@ -255,6 +258,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Date: Tue, 28 Mar 2023 16:58:20 +0200 Subject: [PATCH 0841/2451] Add ExportManager. --- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- Penumbra/Mods/Editor/ModBackup.cs | 22 +++-- Penumbra/Mods/Manager/ExportManager.cs | 89 +++++++++++++++++++ Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 1 - Penumbra/Mods/Manager/Mod.Manager.Root.cs | 46 ---------- Penumbra/Mods/Manager/Mod.Manager.cs | 1 - Penumbra/Mods/Subclasses/IModGroup.cs | 4 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/UI/FileDialogService.cs | 20 +++-- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 29 +++--- Penumbra/UI/Tabs/SettingsTab.cs | 15 ++-- 11 files changed, 141 insertions(+), 89 deletions(-) create mode 100644 Penumbra/Mods/Manager/ExportManager.cs diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index febd7414..82dc3914 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -256,7 +256,7 @@ public unsafe class MetaState : IDisposable using var decals = new DecalReverter(_characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); var ret = _changeCustomize.Original(human, data, skipEquipment); - _inChangeCustomize = false; + _inChangeCustomize = false; return ret; } diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index e2ab4994..083fb803 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -10,17 +10,15 @@ public class ModBackup { public static bool CreatingBackup { get; private set; } - private readonly ModManager _modManager; - private readonly Mod _mod; - public readonly string Name; - public readonly bool Exists; + private readonly Mod _mod; + public readonly string Name; + public readonly bool Exists; - public ModBackup(ModManager modManager, Mod mod) - { - _modManager = modManager; - _mod = mod; - Name = Path.Combine(_modManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; - Exists = File.Exists(Name); + public ModBackup(ExportManager exportManager, Mod mod) + { + _mod = mod; + Name = Path.Combine(exportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; + Exists = File.Exists(Name); } /// Migrate file extensions. @@ -118,7 +116,7 @@ public class ModBackup /// Restore a mod from a pre-existing backup. Does not check if the mod contained in the backup is even similar. /// Does an automatic reload after extraction. /// - public void Restore() + public void Restore(ModManager modManager) { try { @@ -130,7 +128,7 @@ public class ModBackup ZipFile.ExtractToDirectory(Name, _mod.ModPath.FullName); Penumbra.Log.Debug($"Extracted exported file {Name} to {_mod.ModPath.FullName}."); - _modManager.ReloadMod(_mod.Index); + modManager.ReloadMod(_mod.Index); } catch (Exception e) { diff --git a/Penumbra/Mods/Manager/ExportManager.cs b/Penumbra/Mods/Manager/ExportManager.cs new file mode 100644 index 00000000..d0ecb6a3 --- /dev/null +++ b/Penumbra/Mods/Manager/ExportManager.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; + +namespace Penumbra.Mods; + +public class ExportManager : IDisposable +{ + private readonly Configuration _config; + private readonly ModManager _modManager; + + private DirectoryInfo? _exportDirectory; + + public DirectoryInfo ExportDirectory + => _exportDirectory ?? _modManager.BasePath; + + public ExportManager(Configuration config, ModManager modManager) + { + _config = config; + _modManager = modManager; + UpdateExportDirectory(_config.ExportDirectory, false); + _modManager.ModPathChanged += OnModPathChange; + } + + /// + public void UpdateExportDirectory(string newDirectory) + => UpdateExportDirectory(newDirectory, true); + + /// + /// Update the export directory to a new directory. Can also reset it to null with empty input. + /// If the directory is changed, all existing backups will be moved to the new one. + /// + /// The new directory name. + /// Can be used to stop saving for the initial setting + private void UpdateExportDirectory(string newDirectory, bool change) + { + if (newDirectory.Length == 0) + { + if (_exportDirectory == null) + return; + + _exportDirectory = null; + _config.ExportDirectory = string.Empty; + _config.Save(); + return; + } + + var dir = new DirectoryInfo(newDirectory); + if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) + return; + + if (!dir.Exists) + try + { + Directory.CreateDirectory(dir.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); + return; + } + + if (change) + foreach (var mod in _modManager) + new ModBackup(this, mod).Move(dir.FullName); + + _exportDirectory = dir; + + if (!change) + return; + + _config.ExportDirectory = dir.FullName; + _config.Save(); + } + + public void Dispose() + => _modManager.ModPathChanged -= OnModPathChange; + + /// Automatically migrate the backup file to the new name if any exists. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + if (type is not ModPathChangeType.Moved || oldDirectory == null || newDirectory == null) + return; + + mod.ModPath = oldDirectory; + new ModBackup(this, mod).Move(null, newDirectory.Name); + mod.ModPath = newDirectory; + } +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 96e206fa..25340e84 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -57,7 +57,6 @@ public partial class ModManager } DataEditor.MoveDataFile(oldDirectory, dir); - new ModBackup(this, mod).Move(null, dir.Name); dir.Refresh(); mod.ModPath = dir; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 0377e09b..35f8d900 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -9,11 +9,6 @@ namespace Penumbra.Mods; public sealed partial class ModManager { public DirectoryInfo BasePath { get; private set; } = null!; - private DirectoryInfo? _exportDirectory; - - public DirectoryInfo ExportDirectory - => _exportDirectory ?? BasePath; - public bool Valid { get; private set; } public event Action? ModDiscoveryStarted; @@ -105,45 +100,4 @@ public sealed partial class ModManager if (MigrateModBackups) ModBackup.MigrateZipToPmp(this); } - - public void UpdateExportDirectory(string newDirectory, bool change) - { - if (newDirectory.Length == 0) - { - if (_exportDirectory == null) - return; - - _exportDirectory = null; - _config.ExportDirectory = string.Empty; - _config.Save(); - return; - } - - var dir = new DirectoryInfo(newDirectory); - if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) - return; - - if (!dir.Exists) - try - { - Directory.CreateDirectory(dir.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); - return; - } - - if (change) - foreach (var mod in _mods) - new ModBackup(this, mod).Move(dir.FullName); - - _exportDirectory = dir; - - if (change) - { - _config.ExportDirectory = dir.FullName; - _config.Save(); - } - } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 7e3310e4..95fbe2ac 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -114,7 +114,6 @@ public sealed partial class ModManager : IReadOnlyList, IDisposable OptionEditor = optionEditor; ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory(config.ModDirectory, true); - UpdateExportDirectory(_config.ExportDirectory, false); _communicator.ModOptionChanged.Event += OnModOptionChange; ModPathChanged += OnModPathChange; DiscoverMods(); diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 45db2b59..c5087711 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -47,11 +47,11 @@ public readonly struct ModSaveGroup : ISavable public ModSaveGroup(Mod mod, int groupIdx) { _basePath = mod.ModPath; + _groupIdx = groupIdx; if (_groupIdx < 0) _defaultMod = mod.Default; else - _group = mod.Groups[groupIdx]; - _groupIdx = groupIdx; + _group = mod.Groups[_groupIdx]; } public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx) diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 6766d3e8..afba2adf 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -93,6 +93,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); // Add Resource services diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 650014c8..061f53ad 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -87,8 +88,9 @@ public class FileDialogService : IDisposable return (valid, list) => { _isOpen = false; - _startPaths[title] = GetCurrentLocation(); - callback(valid, list); + var loc = HandleRoot(GetCurrentLocation()); + _startPaths[title] = loc; + callback(valid, list.Select(HandleRoot).ToList()); }; } @@ -97,14 +99,20 @@ public class FileDialogService : IDisposable return (valid, list) => { _isOpen = false; - var loc = GetCurrentLocation(); - if (loc.Length == 2) - loc += '\\'; + var loc = HandleRoot(GetCurrentLocation()); _startPaths[title] = loc; - callback(valid, list); + callback(valid, HandleRoot(list)); }; } + private static string HandleRoot(string path) + { + if (path.Length == 2 && path[1] == ':') + return path + '\\'; + + return path; + } + // TODO: maybe change this from reflection when its public. private string GetCurrentLocation() => (_manager.GetType().GetField("dialog", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(_manager) as FileDialog) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index bb1b6d75..83d09f9d 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -23,6 +23,7 @@ public class ModPanelEditTab : ITab private readonly ChatService _chat; private readonly FilenameService _filenames; private readonly ModManager _modManager; + private readonly ExportManager _exportManager; private readonly ModFileSystem _fileSystem; private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; @@ -36,15 +37,16 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ExportManager exportManager) { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _chat = chat; - _editWindow = editWindow; - _editor = editor; - _filenames = filenames; + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + _editor = editor; + _filenames = filenames; + _exportManager = exportManager; } public ReadOnlySpan Label @@ -147,7 +149,7 @@ public class ModPanelEditTab : ITab private void BackupButtons(Vector2 buttonSize) { - var backup = new ModBackup(_modManager, _mod); + var backup = new ModBackup(_exportManager, _mod); var tt = ModBackup.CreatingBackup ? "Already exporting a mod." : backup.Exists @@ -168,7 +170,7 @@ public class ModPanelEditTab : ITab : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists)) - backup.Restore(); + backup.Restore(_modManager); } /// Anything about editing the regular meta information about the mod. @@ -306,11 +308,11 @@ public class ModPanelEditTab : ITab private static int _newDescriptionIdx = -1; private static int _newDescriptionOptionIdx = -1; private static Mod? _mod; - private static FilenameService? _fileNames; + private static FilenameService? _fileNames; public static void OpenPopup(FilenameService filenames, Mod mod, int groupIdx, int optionIdx = -1) { - _fileNames = filenames; + _fileNames = filenames; _newDescriptionIdx = groupIdx; _newDescriptionOptionIdx = optionIdx; _newDescription = groupIdx < 0 @@ -598,7 +600,8 @@ public class ModPanelEditTab : ITab if (_dragDropGroupIdx == groupIdx) { var sourceOption = _dragDropOptionIdx; - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + panel._delayedActions.Enqueue( + () => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); } else { diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 039a648f..a920e723 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -9,7 +9,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; @@ -31,7 +30,8 @@ public class SettingsTab : ITab private readonly TutorialService _tutorial; private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; + private readonly ModManager _modManager; + private readonly ExportManager _exportManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; @@ -39,7 +39,7 @@ public class SettingsTab : ITab public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud) + ResidentResourceManager residentResources, DalamudServices dalamud, ExportManager exportManager) { _config = config; _fontReloader = fontReloader; @@ -51,6 +51,7 @@ public class SettingsTab : ITab _characterUtility = characterUtility; _residentResources = residentResources; _dalamud = dalamud; + _exportManager = exportManager; } public void DrawHeader() @@ -114,7 +115,7 @@ public class SettingsTab : ITab /// Check a potential new root directory for validity and return the button text and whether it is valid. private static (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) - { + { static bool IsSubPathOf(string basePath, string subPath) { if (basePath.Length == 0) @@ -127,7 +128,7 @@ public class SettingsTab : ITab if (newName.Length > RootDirectoryMaxLength) return ($"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false); - if (Path.GetDirectoryName(newName) == null) + if (Path.GetDirectoryName(newName).IsNullOrEmpty()) return ("Path is not allowed to be a drive root. Please add a directory.", false); var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); @@ -553,7 +554,7 @@ public class SettingsTab : ITab _tempExportDirectory = tmp; if (ImGui.IsItemDeactivatedAfterEdit()) - _modManager.UpdateExportDirectory(_tempExportDirectory, true); + _exportManager.UpdateExportDirectory(_tempExportDirectory); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize, @@ -567,7 +568,7 @@ public class SettingsTab : ITab _fileDialog.OpenFolderPicker("Choose Default Export Directory", (b, s) => { if (b) - Penumbra.ModManager.UpdateExportDirectory(s, true); + _exportManager.UpdateExportDirectory(s); }, startDir, false); } From 185be81e739bad9b9eb9a37bc5ce0d58fc02e7ee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Mar 2023 14:42:34 +0200 Subject: [PATCH 0842/2451] Fix some issues with ResourceWatcher. --- Penumbra/Collections/ResolveData.cs | 25 +- Penumbra/Configuration.cs | 6 +- .../Interop/ResourceLoading/ResourceLoader.cs | 5 +- .../ResourceLoading/ResourceService.cs | 4 +- .../ResourceWatcher/ResourceWatcher.Record.cs | 221 ++++--- .../ResourceWatcher.RecordType.cs | 17 - .../ResourceWatcher/ResourceWatcher.Table.cs | 626 +++++++++--------- .../UI/ResourceWatcher/ResourceWatcher.cs | 144 ++-- 8 files changed, 519 insertions(+), 529 deletions(-) delete mode 100644 Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index f816069e..efabaaf2 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -36,30 +37,6 @@ public readonly struct ResolveData public override string ToString() => ModCollection.Name; - - public unsafe string AssociatedName() - { - if (AssociatedGameObject == IntPtr.Zero) - return "no associated object."; - - try - { - var id = Penumbra.Actors.FromObject((GameObject*)AssociatedGameObject, out _, false, true, true); - if (id.IsValid) - { - var name = id.ToString(); - var parts = name.Split(' ', 3); - return string.Join(" ", - parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); - } - } - catch - { - // ignored - } - - return $"0x{AssociatedGameObject:X}"; - } } public static class ResolveDataExtensions diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ff2a5e64..efe21ffe 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -62,9 +62,9 @@ public class Configuration : IPluginConfiguration, ISavable public bool OnlyAddMatchingResources { get; set; } = true; public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; [JsonConverter(typeof(SortModeConverter))] diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 3ec38c87..5d7ba16a 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -66,7 +66,7 @@ public unsafe class ResourceLoader : IDisposable _fileReadService.ReadSqPack -= ReadSqPackDetour; } - private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (returnValue != null) @@ -86,9 +86,10 @@ public unsafe class ResourceLoader : IDisposable _texMdlService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); + var oldPath = path; path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); - ResourceLoaded?.Invoke(returnValue, p, resolvedPath.Value, data); + ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 1027ed5f..bced539c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -60,7 +60,7 @@ public unsafe class ResourceService : IDisposable /// Mainly used for SCD streaming, can be null. /// Whether to request the resource synchronously or asynchronously. /// The returned resource handle. If this is not null, calling original will be skipped. - public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// @@ -104,7 +104,7 @@ public unsafe class ResourceService : IDisposable } ResourceHandle* returnValue = null; - ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, pGetResParams, ref isSync, + ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, gamePath, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs index 3cc82173..9b54cb85 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs @@ -7,112 +7,123 @@ using Penumbra.String; namespace Penumbra.UI; -public partial class ResourceWatcher +[Flags] +public enum RecordType : byte { - private unsafe struct Record + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, +} + +internal unsafe struct Record +{ + public DateTime Time; + public ByteString Path; + public ByteString OriginalPath; + public string AssociatedGameObject; + public ModCollection? Collection; + public ResourceHandle* Handle; + public ResourceTypeFlag ResourceType; + public ResourceCategoryFlag Category; + public uint RefCount; + public RecordType RecordType; + public OptionalBool Synchronously; + public OptionalBool ReturnValue; + public OptionalBool CustomLoad; + + public static Record CreateRequest(ByteString path, bool sync) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = null, + ResourceType = ResourceExtensions.Type(path).ToFlag(), + Category = ResourceExtensions.Category(path).ToFlag(), + RefCount = 0, + RecordType = RecordType.Request, + Synchronously = sync, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, + }; + + public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { - public DateTime Time; - public ByteString Path; - public ByteString OriginalPath; - public ModCollection? Collection; - public ResourceHandle* Handle; - public ResourceTypeFlag ResourceType; - public ResourceCategoryFlag Category; - public uint RefCount; - public RecordType RecordType; - public OptionalBool Synchronously; - public OptionalBool ReturnValue; - public OptionalBool CustomLoad; - - public static Record CreateRequest( ByteString path, bool sync ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, - Collection = null, - Handle = null, - ResourceType = ResourceExtensions.Type( path ).ToFlag(), - Category = ResourceExtensions.Category( path ).ToFlag(), - RefCount = 0, - RecordType = RecordType.Request, - Synchronously = sync, - ReturnValue = OptionalBool.Null, - CustomLoad = OptionalBool.Null, - }; - - public static Record CreateDefaultLoad( ByteString path, ResourceHandle* handle, ModCollection collection ) + path = path.IsOwned ? path : path.Clone(); + return new Record { - path = path.IsOwned ? path : path.Clone(); - return new Record - { - Time = DateTime.UtcNow, - Path = path, - OriginalPath = path, - Collection = collection, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.ResourceLoad, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = false, - }; - } - - public static Record CreateLoad( ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), - Collection = collection, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.ResourceLoad, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = true, - }; - - public static Record CreateDestruction( ResourceHandle* handle ) - { - var path = handle->FileName().Clone(); - return new Record - { - Time = DateTime.UtcNow, - Path = path, - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.Destruction, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = OptionalBool.Null, - }; - } - - public static Record CreateFileLoad( ByteString path, ResourceHandle* handle, bool ret, bool custom ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.FileLoad, - Synchronously = OptionalBool.Null, - ReturnValue = ret, - CustomLoad = custom, - }; + Time = DateTime.UtcNow, + Path = path, + OriginalPath = path, + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = false, + AssociatedGameObject = associatedGameObject, + }; } -} \ No newline at end of file + + public static Record CreateLoad(ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection, + string associatedGameObject) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = true, + AssociatedGameObject = associatedGameObject, + }; + + public static Record CreateDestruction(ResourceHandle* handle) + { + var path = handle->FileName().Clone(); + return new Record + { + Time = DateTime.UtcNow, + Path = path, + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.Destruction, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + }; + } + + public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.FileLoad, + Synchronously = OptionalBool.Null, + ReturnValue = ret, + CustomLoad = custom, + }; +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs deleted file mode 100644 index 3b3fed73..00000000 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Penumbra.UI; - -public partial class ResourceWatcher -{ - [Flags] - public enum RecordType : byte - { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, - } - - public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; -} \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs index 1cb787ef..d5ff7d7f 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -13,340 +13,328 @@ using Penumbra.String; namespace Penumbra.UI; -public partial class ResourceWatcher +internal sealed class ResourceWatcherTable : Table { - private sealed class Table : Table< Record > + public ResourceWatcherTable(Configuration config, ICollection records) + : base("##records", + records, + new PathColumn { Label = "Path" }, + new RecordTypeColumn(config) { Label = "Record" }, + new CollectionColumn { Label = "Collection" }, + new ObjectColumn { Label = "Game Object" }, + new CustomLoadColumn { Label = "Custom" }, + new SynchronousLoadColumn { Label = "Sync" }, + new OriginalPathColumn { Label = "Original Path" }, + new ResourceCategoryColumn { Label = "Category" }, + new ResourceTypeColumn { Label = "Type" }, + new HandleColumn { Label = "Resource" }, + new RefCountColumn { Label = "#Ref" }, + new DateColumn { Label = "Time" } + ) + { } + + public void Reset() + => FilterDirty = true; + + private sealed class PathColumn : ColumnString { - private static readonly PathColumn Path = new() { Label = "Path" }; - private static readonly RecordTypeColumn RecordType = new() { Label = "Record" }; - private static readonly DateColumn Date = new() { Label = "Time" }; + public override float Width + => 300 * UiHelpers.Scale; - private static readonly CollectionColumn Coll = new() { Label = "Collection" }; - private static readonly CustomLoadColumn Custom = new() { Label = "Custom" }; - private static readonly SynchronousLoadColumn Sync = new() { Label = "Sync" }; + public override string ToName(Record item) + => item.Path.ToString(); - private static readonly OriginalPathColumn Orig = new() { Label = "Original Path" }; - private static readonly ResourceCategoryColumn Cat = new() { Label = "Category" }; - private static readonly ResourceTypeColumn Type = new() { Label = "Type" }; + public override int Compare(Record lhs, Record rhs) + => lhs.Path.CompareTo(rhs.Path); - private static readonly HandleColumn Handle = new() { Label = "Resource" }; - private static readonly RefCountColumn Ref = new() { Label = "#Ref" }; + public override void DrawColumn(Record item, int _) + => DrawByteString(item.Path, 280 * UiHelpers.Scale); + } - public Table( ICollection< Record > records ) - : base( "##records", records, Path, RecordType, Coll, Custom, Sync, Orig, Cat, Type, Handle, Ref, Date ) - { } - - public void Reset() - => FilterDirty = true; - - private sealed class PathColumn : ColumnString< Record > + private static unsafe void DrawByteString(ByteString path, float length) + { + Vector2 vec; + ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); + if (vec.X <= length) { - public override float Width - => 300 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.Path.ToString(); - - public override int Compare( Record lhs, Record rhs ) - => lhs.Path.CompareTo( rhs.Path ); - - public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.Path, 290 * UiHelpers.Scale ); + ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); } - - private static unsafe void DrawByteString( ByteString path, float length ) + else { - Vector2 vec; - ImGuiNative.igCalcTextSize( &vec, path.Path, path.Path + path.Length, 0, 0 ); - if( vec.X <= length ) + var fileName = path.LastIndexOf((byte)'/'); + ByteString shortPath; + if (fileName != -1) { - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString()); + ImGui.SameLine(); + shortPath = path.Substring(fileName, path.Length - fileName); } else { - var fileName = path.LastIndexOf( ( byte )'/' ); - ByteString shortPath; - if( fileName != -1 ) - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * UiHelpers.Scale ) ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( FontAwesomeIcon.EllipsisH.ToIconString() ); - ImGui.SameLine(); - shortPath = path.Substring( fileName, path.Length - fileName ); - } - else - { - shortPath = path; - } - - ImGuiNative.igTextUnformatted( shortPath.Path, shortPath.Path + shortPath.Length ); - if( ImGui.IsItemClicked() ) - { - ImGuiNative.igSetClipboardText( path.Path ); - } - - if( ImGui.IsItemHovered() ) - { - ImGuiNative.igSetTooltip( path.Path ); - } - } - } - - private sealed class RecordTypeColumn : ColumnFlags< RecordType, Record > - { - public RecordTypeColumn() - => AllFlags = AllRecords; - - public override float Width - => 80 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.RecordType ); - - public override RecordType FilterValue - => Penumbra.Config.ResourceWatcherRecordTypes; - - protected override void SetValue( RecordType value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherRecordTypes |= value; - } - else - { - Penumbra.Config.ResourceWatcherRecordTypes &= ~value; - } - - Penumbra.Config.Save(); + shortPath = path; } - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.RecordType switch - { - ResourceWatcher.RecordType.Request => "REQ", - ResourceWatcher.RecordType.ResourceLoad => "LOAD", - ResourceWatcher.RecordType.FileLoad => "FILE", - ResourceWatcher.RecordType.Destruction => "DEST", - _ => string.Empty, - } ); - } - } + ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length); + if (ImGui.IsItemClicked()) + ImGuiNative.igSetClipboardText(path.Path); - private sealed class DateColumn : Column< Record > - { - public override float Width - => 80 * UiHelpers.Scale; - - public override int Compare( Record lhs, Record rhs ) - => lhs.Time.CompareTo( rhs.Time ); - - public override void DrawColumn( Record item, int _ ) - => ImGui.TextUnformatted( $"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}" ); - } - - - private sealed class CollectionColumn : ColumnString< Record > - { - public override float Width - => 80 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.Collection?.Name ?? string.Empty; - } - - private sealed class OriginalPathColumn : ColumnString< Record > - { - public override float Width - => 200 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.OriginalPath.ToString(); - - public override int Compare( Record lhs, Record rhs ) - => lhs.OriginalPath.CompareTo( rhs.OriginalPath ); - - public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.OriginalPath, 190 * UiHelpers.Scale ); - } - - private sealed class ResourceCategoryColumn : ColumnFlags< ResourceCategoryFlag, Record > - { - public ResourceCategoryColumn() - => AllFlags = ResourceExtensions.AllResourceCategories; - - public override float Width - => 80 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.Category ); - - public override ResourceCategoryFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceCategories; - - protected override void SetValue( ResourceCategoryFlag value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherResourceCategories |= value; - } - else - { - Penumbra.Config.ResourceWatcherResourceCategories &= ~value; - } - - Penumbra.Config.Save(); - } - - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.Category.ToString() ); - } - } - - private sealed class ResourceTypeColumn : ColumnFlags< ResourceTypeFlag, Record > - { - public ResourceTypeColumn() - { - AllFlags = Enum.GetValues< ResourceTypeFlag >().Aggregate( ( v, f ) => v | f ); - for( var i = 0; i < Names.Length; ++i ) - { - Names[ i ] = Names[ i ].ToLowerInvariant(); - } - } - - public override float Width - => 50 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.ResourceType ); - - public override ResourceTypeFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceTypes; - - protected override void SetValue( ResourceTypeFlag value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherResourceTypes |= value; - } - else - { - Penumbra.Config.ResourceWatcherResourceTypes &= ~value; - } - - Penumbra.Config.Save(); - } - - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.ResourceType.ToString().ToLowerInvariant() ); - } - } - - private sealed class HandleColumn : ColumnString< Record > - { - public override float Width - => 120 * UiHelpers.Scale; - - public override unsafe string ToName( Record item ) - => item.Handle == null ? string.Empty : $"0x{( ulong )item.Handle:X}"; - - public override unsafe void DrawColumn( Record item, int _ ) - { - using var font = ImRaii.PushFont( UiBuilder.MonoFont, item.Handle != null ); - ImGuiUtil.RightAlign( ToName( item ) ); - } - } - - [Flags] - private enum BoolEnum : byte - { - True = 0x01, - False = 0x02, - Unknown = 0x04, - } - - private class OptBoolColumn : ColumnFlags< BoolEnum, Record > - { - private BoolEnum _filter; - - public OptBoolColumn() - { - AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; - _filter = AllFlags; - Flags &= ~ImGuiTableColumnFlags.NoSort; - } - - protected bool FilterFunc( OptionalBool b ) - => b.Value switch - { - null => _filter.HasFlag( BoolEnum.Unknown ), - true => _filter.HasFlag( BoolEnum.True ), - false => _filter.HasFlag( BoolEnum.False ), - }; - - public override BoolEnum FilterValue - => _filter; - - protected override void SetValue( BoolEnum value, bool enable ) - { - if( enable ) - { - _filter |= value; - } - else - { - _filter &= ~value; - } - } - - protected static void DrawColumn( OptionalBool b ) - { - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( b.Value switch - { - null => string.Empty, - true => FontAwesomeIcon.Check.ToIconString(), - false => FontAwesomeIcon.Times.ToIconString(), - } ); - } - } - - private sealed class CustomLoadColumn : OptBoolColumn - { - public override float Width - => 60 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterFunc( item.CustomLoad ); - - public override void DrawColumn( Record item, int idx ) - => DrawColumn( item.CustomLoad ); - } - - private sealed class SynchronousLoadColumn : OptBoolColumn - { - public override float Width - => 45 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterFunc( item.Synchronously ); - - public override void DrawColumn( Record item, int idx ) - => DrawColumn( item.Synchronously ); - } - - private sealed class RefCountColumn : Column< Record > - { - public override float Width - => 30 * UiHelpers.Scale; - - public override void DrawColumn( Record item, int _ ) - => ImGuiUtil.RightAlign( item.RefCount.ToString() ); - - public override int Compare( Record lhs, Record rhs ) - => lhs.RefCount.CompareTo( rhs.RefCount ); + if (ImGui.IsItemHovered()) + ImGuiNative.igSetTooltip(path.Path); } } -} \ No newline at end of file + + private sealed class RecordTypeColumn : ColumnFlags + { + private readonly Configuration _config; + + public RecordTypeColumn(Configuration config) + { + AllFlags = ResourceWatcher.AllRecords; + _config = config; + } + + public override float Width + => 80 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.RecordType); + + public override RecordType FilterValue + => _config.ResourceWatcherRecordTypes; + + protected override void SetValue(RecordType value, bool enable) + { + if (enable) + _config.ResourceWatcherRecordTypes |= value; + else + _config.ResourceWatcherRecordTypes &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.RecordType switch + { + RecordType.Request => "REQ", + RecordType.ResourceLoad => "LOAD", + RecordType.FileLoad => "FILE", + RecordType.Destruction => "DEST", + _ => string.Empty, + }); + } + } + + private sealed class DateColumn : Column + { + public override float Width + => 80 * UiHelpers.Scale; + + public override int Compare(Record lhs, Record rhs) + => lhs.Time.CompareTo(rhs.Time); + + public override void DrawColumn(Record item, int _) + => ImGui.TextUnformatted($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}"); + } + + + private sealed class CollectionColumn : ColumnString + { + public override float Width + => 80 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.Collection?.Name ?? string.Empty; + } + + private sealed class ObjectColumn : ColumnString + { + public override float Width + => 200 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.AssociatedGameObject; + } + + private sealed class OriginalPathColumn : ColumnString + { + public override float Width + => 200 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.OriginalPath.ToString(); + + public override int Compare(Record lhs, Record rhs) + => lhs.OriginalPath.CompareTo(rhs.OriginalPath); + + public override void DrawColumn(Record item, int _) + => DrawByteString(item.OriginalPath, 190 * UiHelpers.Scale); + } + + private sealed class ResourceCategoryColumn : ColumnFlags + { + public ResourceCategoryColumn() + => AllFlags = ResourceExtensions.AllResourceCategories; + + public override float Width + => 80 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.Category); + + public override ResourceCategoryFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceCategories; + + protected override void SetValue(ResourceCategoryFlag value, bool enable) + { + if (enable) + Penumbra.Config.ResourceWatcherResourceCategories |= value; + else + Penumbra.Config.ResourceWatcherResourceCategories &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.Category.ToString()); + } + } + + private sealed class ResourceTypeColumn : ColumnFlags + { + public ResourceTypeColumn() + { + AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); + for (var i = 0; i < Names.Length; ++i) + Names[i] = Names[i].ToLowerInvariant(); + } + + public override float Width + => 50 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.ResourceType); + + public override ResourceTypeFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceTypes; + + protected override void SetValue(ResourceTypeFlag value, bool enable) + { + if (enable) + Penumbra.Config.ResourceWatcherResourceTypes |= value; + else + Penumbra.Config.ResourceWatcherResourceTypes &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.ResourceType.ToString().ToLowerInvariant()); + } + } + + private sealed class HandleColumn : ColumnString + { + public override float Width + => 120 * UiHelpers.Scale; + + public override unsafe string ToName(Record item) + => item.Handle == null ? string.Empty : $"0x{(ulong)item.Handle:X}"; + + public override unsafe void DrawColumn(Record item, int _) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null); + ImGuiUtil.RightAlign(ToName(item)); + } + } + + [Flags] + private enum BoolEnum : byte + { + True = 0x01, + False = 0x02, + Unknown = 0x04, + } + + private class OptBoolColumn : ColumnFlags + { + private BoolEnum _filter; + + public OptBoolColumn() + { + AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; + _filter = AllFlags; + Flags &= ~ImGuiTableColumnFlags.NoSort; + } + + protected bool FilterFunc(OptionalBool b) + => b.Value switch + { + null => _filter.HasFlag(BoolEnum.Unknown), + true => _filter.HasFlag(BoolEnum.True), + false => _filter.HasFlag(BoolEnum.False), + }; + + public override BoolEnum FilterValue + => _filter; + + protected override void SetValue(BoolEnum value, bool enable) + { + if (enable) + _filter |= value; + else + _filter &= ~value; + } + + protected static void DrawColumn(OptionalBool b) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(b.Value switch + { + null => string.Empty, + true => FontAwesomeIcon.Check.ToIconString(), + false => FontAwesomeIcon.Times.ToIconString(), + }); + } + } + + private sealed class CustomLoadColumn : OptBoolColumn + { + public override float Width + => 60 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterFunc(item.CustomLoad); + + public override void DrawColumn(Record item, int idx) + => DrawColumn(item.CustomLoad); + } + + private sealed class SynchronousLoadColumn : OptBoolColumn + { + public override float Width + => 45 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterFunc(item.Synchronously); + + public override void DrawColumn(Record item, int idx) + => DrawColumn(item.Synchronously); + } + + private sealed class RefCountColumn : Column + { + public override float Width + => 30 * UiHelpers.Scale; + + public override void DrawColumn(Record item, int _) + => ImGuiUtil.RightAlign(item.RefCount.ToString()); + + public override int Compare(Record lhs, Record rhs) + => lhs.RefCount.CompareTo(rhs.RefCount); + } +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 66ece068..8b054033 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; +using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -21,24 +22,27 @@ namespace Penumbra.UI; public partial class ResourceWatcher : IDisposable, ITab { - public const int DefaultMaxEntries = 1024; + public const int DefaultMaxEntries = 1024; + public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; private readonly Configuration _config; private readonly ResourceService _resources; private readonly ResourceLoader _loader; + private readonly ActorService _actors; private readonly List _records = new(); private readonly ConcurrentQueue _newRecords = new(); - private readonly Table _table; + private readonly ResourceWatcherTable _table; private string _logFilter = string.Empty; private Regex? _logRegex; private int _newMaxEntries; - public unsafe ResourceWatcher(Configuration config, ResourceService resources, ResourceLoader loader) + public unsafe ResourceWatcher(ActorService actors, Configuration config, ResourceService resources, ResourceLoader loader) { + _actors = actors; _config = config; _resources = resources; _loader = loader; - _table = new Table(_records); + _table = new ResourceWatcherTable(config, _records); _resources.ResourceRequested += OnResourceRequested; _resources.ResourceHandleDestructor += OnResourceDestroyed; _loader.ResourceLoaded += OnResourceLoaded; @@ -75,8 +79,8 @@ public partial class ResourceWatcher : IDisposable, ITab var isEnabled = _config.EnableResourceWatcher; if (ImGui.Checkbox("Enable", ref isEnabled)) { - Penumbra.Config.EnableResourceWatcher = isEnabled; - Penumbra.Config.Save(); + _config.EnableResourceWatcher = isEnabled; + _config.Save(); } ImGui.SameLine(); @@ -89,16 +93,16 @@ public partial class ResourceWatcher : IDisposable, ITab var onlyMatching = _config.OnlyAddMatchingResources; if (ImGui.Checkbox("Store Only Matching", ref onlyMatching)) { - Penumbra.Config.OnlyAddMatchingResources = onlyMatching; - Penumbra.Config.Save(); + _config.OnlyAddMatchingResources = onlyMatching; + _config.Save(); } ImGui.SameLine(); var writeToLog = _config.EnableResourceLogging; if (ImGui.Checkbox("Write to Log", ref writeToLog)) { - Penumbra.Config.EnableResourceLogging = writeToLog; - Penumbra.Config.Save(); + _config.EnableResourceLogging = writeToLog; + _config.Save(); } ImGui.SameLine(); @@ -137,8 +141,8 @@ public partial class ResourceWatcher : IDisposable, ITab if (config) { - Penumbra.Config.ResourceLoggingFilter = newString; - Penumbra.Config.Save(); + _config.ResourceLoggingFilter = newString; + _config.Save(); } } @@ -168,43 +172,44 @@ public partial class ResourceWatcher : IDisposable, ITab return; _newMaxEntries = Math.Max(16, _newMaxEntries); - if (_newMaxEntries != maxEntries) - { - _config.MaxResourceWatcherRecords = _newMaxEntries; - Penumbra.Config.Save(); - if (_newMaxEntries > _records.Count) - _records.RemoveRange(0, _records.Count - _newMaxEntries); - } + if (_newMaxEntries == maxEntries) + return; + + _config.MaxResourceWatcherRecords = _newMaxEntries; + _config.Save(); + if (_newMaxEntries > _records.Count) + _records.RemoveRange(0, _records.Count - _newMaxEntries); } private void UpdateRecords() { var count = _newRecords.Count; - if (count > 0) - { - while (_newRecords.TryDequeue(out var rec) && count-- > 0) - _records.Add(rec); + if (count <= 0) + return; - if (_records.Count > _config.MaxResourceWatcherRecords) - _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); + while (_newRecords.TryDequeue(out var rec) && count-- > 0) + _records.Add(rec); - _table.Reset(); - } + if (_records.Count > _config.MaxResourceWatcherRecords) + _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); + + _table.Reset(); } private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if (_config.EnableResourceLogging && FilterMatch(path.Path, out var match)) + if (_config.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); - if (_config.EnableResourceWatcher) - { - var record = Record.CreateRequest(path.Path, sync); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateRequest(original.Path, sync); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) @@ -220,19 +225,18 @@ public partial class ResourceWatcher : IDisposable, ITab { var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; Penumbra.Log.Information( - $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) "); + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); } } - if (_config.EnableResourceWatcher) - { - var record = manipulatedPath == null - ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection) - : Record.CreateLoad(path.Path, manipulatedPath.Value.InternalName, handle, - data.ModCollection); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = manipulatedPath == null + ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) + : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data)); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) @@ -241,12 +245,12 @@ public partial class ResourceWatcher : IDisposable, ITab Penumbra.Log.Information( $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); - if (_config.EnableResourceWatcher) - { - var record = Record.CreateFileLoad(path, resource, success, custom); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateFileLoad(path, resource, success, custom); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnResourceDestroyed(ResourceHandle* resource) @@ -255,11 +259,37 @@ public partial class ResourceWatcher : IDisposable, ITab Penumbra.Log.Information( $"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}."); - if (_config.EnableResourceWatcher) + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateDestruction(resource); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + + public unsafe string Name(ResolveData resolve, string none = "") + { + if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Valid) + return none; + + try { - var record = Record.CreateDestruction(resource); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + var id = _actors.AwaitedService.FromObject((GameObject*)resolve.AssociatedGameObject, out _, false, true, true); + if (id.IsValid) + { + if (id.Type is not (IdentifierType.Player or IdentifierType.Owned)) + return id.ToString(); + + var parts = id.ToString().Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); + } } + catch + { + // ignored + } + + return $"0x{resolve.AssociatedGameObject:X}"; } } From 3f86698615ac6a1141f92ae0ffacd4a2d16a04b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Mar 2023 14:43:14 +0200 Subject: [PATCH 0843/2451] Make OnScreen a task. --- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 6 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 3 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 10 +- .../ModEditWindow.QuickImport.cs | 133 +++++++++--------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 26 ++-- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 116 ++++++++------- Penumbra/UI/Tabs/OnScreenTab.cs | 14 +- 7 files changed, 157 insertions(+), 151 deletions(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 25340e84..b6e50c80 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -11,8 +11,10 @@ public partial class ModManager public event ModPathChangeDelegate ModPathChanged; - // Rename/Move a mod directory. - // Updates all collection settings and sort order settings. + /// + /// Rename/Move a mod directory. + /// Updates all collection settings and sort order settings. + /// public void MoveModDirectory(int idx, string newName) { var mod = this[idx]; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 35f8d900..575954d5 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,11 +1,10 @@ -using Penumbra.UI.Classes; using System; using System.Collections.Concurrent; using System.IO; using System.Threading.Tasks; namespace Penumbra.Mods; - + public sealed partial class ModManager { public DirectoryInfo BasePath { get; private set; } = null!; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 974dd837..8e96b3e2 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -12,19 +12,15 @@ using Penumbra.Util; namespace Penumbra.Mods; - - public class ModOptionEditor { private readonly CommunicatorService _communicator; - private readonly FilenameService _filenames; private readonly SaveService _saveService; - public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames) + public ModOptionEditor(CommunicatorService communicator, SaveService saveService) { _communicator = communicator; _saveService = saveService; - _filenames = filenames; } /// Change the type of a group given by mod and index to type, if possible. @@ -209,7 +205,7 @@ public class ModOptionEditor /// Add a new empty option of the given name for the given group. public void AddOption(Mod mod, int groupIdx, string newName) { - var group = mod._groups[groupIdx]; + var group = mod._groups[groupIdx]; var subMod = new SubMod(mod) { Name = newName }; subMod.SetPosition(groupIdx, group.Count); switch (group) @@ -319,7 +315,7 @@ public class ModOptionEditor /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); + var subMod = GetSubMod(mod, groupIdx, optionIdx); var oldCount = subMod.FileData.Count; subMod.FileData.AddFrom(additions); if (oldCount != subMod.FileData.Count) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index fae1f0a0..2d4415d9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -12,34 +12,31 @@ using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.String.Classes; - + namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private ResourceTreeViewer? _quickImportViewer; - private Dictionary? _quickImportWritables; - private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions; + private readonly ResourceTreeViewer _quickImportViewer; + private readonly Dictionary _quickImportWritables = new(); + private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); + private void DrawQuickImportTab() { using var tab = ImRaii.TabItem("Import from Screen"); if (!tab) { - _quickImportActions = null; + _quickImportActions.Clear(); return; } - _quickImportViewer ??= new ResourceTreeViewer(_config, _resourceTreeFactory, "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions); - _quickImportWritables ??= new Dictionary(); - _quickImportActions ??= new Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>(); - _quickImportViewer.Draw(); } private void OnQuickImportRefresh() { - _quickImportWritables?.Clear(); - _quickImportActions?.Clear(); + _quickImportWritables.Clear(); + _quickImportActions.Clear(); } private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) @@ -56,34 +53,43 @@ public partial class ModEditWindow var file = _gameData.GetFile(path); writable = file == null ? null : new RawGameFileWritable(file); } + _quickImportWritables.Add(resourceNode.FullPath, writable); } - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", + resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) { var fullPathStr = resourceNode.FullPath.FullName; - var ext = resourceNode.PossibleGamePaths.Length == 1 ? Path.GetExtension(resourceNode.GamePath.ToString()) : Path.GetExtension(fullPathStr); - _fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, (success, name) => - { - if (!success) - return; + var ext = resourceNode.PossibleGamePaths.Length == 1 + ? Path.GetExtension(resourceNode.GamePath.ToString()) + : Path.GetExtension(fullPathStr); + _fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, + (success, name) => + { + if (!success) + return; - try - { - File.WriteAllBytes(name, writable!.Write()); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); - } - }, null, false); + try + { + File.WriteAllBytes(name, writable!.Write()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); + } + }, null, false); } + ImGui.SameLine(); if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) { quickImport = QuickImportAction.Prepare(this, resourceNode.GamePath, writable); _quickImportActions.Add((resourceNode.GamePath, writable), quickImport); } - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, + $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) { quickImport.Execute(); _quickImportActions.Remove((resourceNode.GamePath, writable)); @@ -92,7 +98,8 @@ public partial class ModEditWindow private record class RawFileWritable(string Path) : IWritable { - public bool Valid => true; + public bool Valid + => true; public byte[] Write() => File.ReadAllBytes(Path); @@ -100,7 +107,8 @@ public partial class ModEditWindow private record class RawGameFileWritable(FileResource FileResource) : IWritable { - public bool Valid => true; + public bool Valid + => true; public byte[] Write() => FileResource.Data; @@ -110,16 +118,21 @@ public partial class ModEditWindow { public const string FallbackOptionName = "the current option"; - private readonly string _optionName; + private readonly string _optionName; private readonly Utf8GamePath _gamePath; - private readonly ModEditor _editor; - private readonly IWritable? _file; - private readonly string? _targetPath; - private readonly int _subDirs; + private readonly ModEditor _editor; + private readonly IWritable? _file; + private readonly string? _targetPath; + private readonly int _subDirs; - public string OptionName => _optionName; - public Utf8GamePath GamePath => _gamePath; - public bool CanExecute => !_gamePath.IsEmpty && _editor.Mod != null && _file != null && _targetPath != null; + public string OptionName + => _optionName; + + public Utf8GamePath GamePath + => _gamePath; + + public bool CanExecute + => !_gamePath.IsEmpty && _editor.Mod != null && _file != null && _targetPath != null; /// /// Creates a non-executable QuickImportAction. @@ -127,11 +140,11 @@ public partial class ModEditWindow private QuickImportAction(ModEditor editor, string optionName, Utf8GamePath gamePath) { _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = null; + _gamePath = gamePath; + _editor = editor; + _file = null; _targetPath = null; - _subDirs = 0; + _subDirs = 0; } /// @@ -140,41 +153,35 @@ public partial class ModEditWindow private QuickImportAction(string optionName, Utf8GamePath gamePath, ModEditor editor, IWritable file, string targetPath, int subDirs) { _optionName = optionName; - _gamePath = gamePath; - _editor = editor; - _file = file; + _gamePath = gamePath; + _editor = editor; + _file = file; _targetPath = targetPath; - _subDirs = subDirs; + _subDirs = subDirs; } public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file) { var editor = owner._editor; if (editor == null) - { return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); - } - var subMod = editor.Option; + + var subMod = editor.Option; var optionName = subMod!.FullName; if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) - { return new QuickImportAction(editor, optionName, gamePath); - } + if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) - { return new QuickImportAction(editor, optionName, gamePath); - } + var mod = owner._mod; if (mod == null) - { return new QuickImportAction(editor, optionName, gamePath); - } + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) - { return new QuickImportAction(editor, optionName, gamePath); - } return new QuickImportAction(optionName, gamePath, editor, file, targetPath, subDirs); } @@ -182,26 +189,26 @@ public partial class ModEditWindow public FileRegistry Execute() { if (!CanExecute) - { throw new InvalidOperationException(); - } + var directory = Path.GetDirectoryName(_targetPath); if (directory != null) - { Directory.CreateDirectory(directory); - } File.WriteAllBytes(_targetPath!, _file!.Write()); _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); - _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!); + _editor.FileEditor.AddPathsToSelected(_editor.Option!, new[] + { + fileRegistry, + }, _subDirs); + _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); return fileRegistry; } private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) { - var path = mod.ModPath; + var path = mod.ModPath; var subDirs = 0; if (subMod == mod.Default) return (path, subDirs); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d7e7efa1..bcc8022e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -17,7 +17,6 @@ using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; -using static Penumbra.Mods.Mod; namespace Penumbra.UI.AdvancedWindow; @@ -25,11 +24,10 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly ResourceTreeFactory _resourceTreeFactory; - private readonly DataManager _gameData; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly DataManager _gameData; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -56,7 +54,7 @@ public partial class ModEditWindow : Window, IDisposable } public void ChangeOption(SubMod? subMod) - => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.GroupIdx ?? 0); + => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); public void UpdateModels() { @@ -495,12 +493,11 @@ public partial class ModEditWindow : Window, IDisposable Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory) : base(WindowBaseLabel) { - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _gameData = gameData; - _resourceTreeFactory = resourceTreeFactory; - _fileDialog = fileDialog; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _gameData = gameData; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); @@ -508,7 +505,8 @@ public partial class ModEditWindow : Window, IDisposable () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null); - _center = new CombinedTexture(_left, _right); + _center = new CombinedTexture(_left, _right); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); } public void Dispose() diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 1a452bde..4d8c77a7 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,98 +1,106 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { - private readonly Configuration _config; + private readonly Configuration _config; private readonly ResourceTreeFactory _treeFactory; - private readonly string _name; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; private readonly HashSet _unfolded; - private ResourceTree[]? _trees; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, string name, int actionCapacity, Action onRefresh, + private Task? _task; + + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh, Action drawActions) { _config = config; _treeFactory = treeFactory; - _name = name; _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; _unfolded = new HashSet(); - _trees = null; } public void Draw() { - if (ImGui.Button("Refresh Character List")) - { - try - { - _trees = _treeFactory.FromObjectTable(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); - _trees = Array.Empty(); - } + if (ImGui.Button("Refresh Character List") || _task == null) + _task = RefreshCharacterList(); - _unfolded.Clear(); - _onRefresh(); - } - - try - { - _trees ??= _treeFactory.FromObjectTable(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}"); - _trees ??= Array.Empty(); - } + using var child = ImRaii.Child("##Data"); + if (!child) + return; var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green - - foreach (var (tree, index) in _trees.WithIndex()) + if (!_task.IsCompleted) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + ImGui.NewLine(); + ImGui.TextUnformatted("Calculating character list..."); + } + else if (_task.Exception != null) + { + ImGui.NewLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGui.TextUnformatted($"Error during calculation of character list:\n\n{_task.Exception}"); + } + else if (_task.IsCompletedSuccessfully) + { + foreach (var (tree, index) in _task.Result.WithIndex()) { - if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + { + if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + continue; + } + + using var id = ImRaii.PushId(index); + + ImGui.Text($"Collection: {tree.CollectionName}"); + + using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) continue; + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + if (_actionCapacity > 0) + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + + DrawNodes(tree.Nodes, 0); } - - using var id = ImRaii.PushId(index); - - ImGui.Text($"Collection: {tree.CollectionName}"); - - using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - continue; - - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - if (_actionCapacity > 0) - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, - (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); - ImGui.TableHeadersRow(); - - DrawNodes(tree.Nodes, 0); } } + private Task RefreshCharacterList() + => Task.Run(() => + { + try + { + return _treeFactory.FromObjectTable(); + } + finally + { + _unfolded.Clear(); + _onRefresh(); + } + }); + private void DrawNodes(IEnumerable resourceNodes, int level) { var debugMode = _config.DebugMode; @@ -105,7 +113,7 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); - var unfolded = _unfolded!.Contains(resourceNode); + var unfolded = _unfolded.Contains(resourceNode); using (var indent = ImRaii.PushIndent(level)) { ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 5fe9b8cc..0ebc7dbd 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -7,22 +7,18 @@ namespace Penumbra.UI.Tabs; public class OnScreenTab : ITab { - private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; - private ResourceTreeViewer? _viewer; + private readonly Configuration _config; + private ResourceTreeViewer _viewer; public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) { - _config = config; - _treeFactory = treeFactory; + _config = config; + _viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); } public ReadOnlySpan Label => "On-Screen"u8; public void DrawContent() - { - _viewer ??= new ResourceTreeViewer(_config, _treeFactory, "On-Screen tab", 0, delegate { }, delegate { }); - _viewer.Draw(); - } + => _viewer.Draw(); } From 1541cdb78d2daff40fdb46fd55d1dec4863affcc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Mar 2023 14:44:30 +0200 Subject: [PATCH 0844/2451] Add Mount animation hook. --- .../PathResolving/AnimationHookService.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index ba47289f..787f13d0 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -3,6 +3,7 @@ using System.Threading; using Dalamud.Game.ClientState.Objects; using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -44,6 +45,7 @@ public unsafe class AnimationHookService : IDisposable _loadCharacterVfxHook.Enable(); _loadAreaVfxHook.Enable(); _scheduleClipUpdateHook.Enable(); + _unkMountAnimationHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -98,6 +100,7 @@ public unsafe class AnimationHookService : IDisposable _loadCharacterVfxHook.Dispose(); _loadAreaVfxHook.Dispose(); _scheduleClipUpdateHook.Dispose(); + _unkMountAnimationHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -305,4 +308,18 @@ public unsafe class AnimationHookService : IDisposable return ResolveData.Invalid; } + + + private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); + + [Signature("48 89 5C 24 ?? 48 89 6C 24 ?? 89 54 24", DetourName = nameof(UnkMountAnimationDetour))] + private readonly Hook _unkMountAnimationHook = null!; + + private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) + { + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); + _unkMountAnimationHook.Original(drawObject, unk1, unk2, unk3); + _animationLoadData.Value = last; + } } From 70c1a2604fd11c49102dd2383192674d89e94403 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 Mar 2023 23:29:01 +0200 Subject: [PATCH 0845/2451] ModManager2 --- Penumbra/Mods/Editor/ModBackup.cs | 9 +- Penumbra/Mods/Manager/Mod.Manager.cs | 347 +++++++++++++++++++---- Penumbra/Mods/Manager/ModOptionEditor.cs | 1 + Penumbra/Mods/Manager/ModStorage.cs | 66 +++++ Penumbra/Mods/Mod.Files.cs | 91 +++--- Penumbra/Mods/ModCache.cs | 26 ++ Penumbra/Mods/ModCacheManager.cs | 272 ++++++++++++++++++ Penumbra/Penumbra.cs | 1 + Penumbra/PenumbraNew.cs | 5 +- Penumbra/Services/CommunicatorService.cs | 60 +++- Penumbra/Util/EventWrapper.cs | 54 ++++ 11 files changed, 815 insertions(+), 117 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModStorage.cs create mode 100644 Penumbra/Mods/ModCache.cs create mode 100644 Penumbra/Mods/ModCacheManager.cs diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 083fb803..b675ca6b 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Threading.Tasks; @@ -7,7 +8,9 @@ namespace Penumbra.Mods; /// Utility to create and apply a zipped backup of a mod. public class ModBackup -{ +{ + /// Set when reading Config and migrating from v4 to v5. + public static bool MigrateModBackups = false; public static bool CreatingBackup { get; private set; } private readonly Mod _mod; @@ -22,9 +25,9 @@ public class ModBackup } /// Migrate file extensions. - public static void MigrateZipToPmp(ModManager modManager) + public static void MigrateZipToPmp(IEnumerable modStorage) { - foreach (var mod in modManager) + foreach (var mod in modStorage) { var pmpName = mod.ModPath + ".pmp"; var zipName = mod.ModPath + ".zip"; diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 95fbe2ac..febe8209 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -1,74 +1,325 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; +using System.Threading.Tasks; using Penumbra.Services; -using Penumbra.Util; - +using Penumbra.Util; namespace Penumbra.Mods; - -public sealed class ModManager2 : IReadOnlyList, IDisposable + +/// Describes the state of a potential move-target for a mod. +public enum NewDirectoryState { + NonExisting, + ExistsEmpty, + ExistsNonEmpty, + ExistsAsFile, + ContainsInvalidSymbols, + Identical, + Empty, +} + +public sealed class ModManager2 : ModStorage +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + public readonly ModDataEditor DataEditor; public readonly ModOptionEditor OptionEditor; - /// - /// An easily accessible set of new mods. - /// Mods are added when they are created or imported. - /// Mods are removed when they are deleted or when they are toggled in any collection. - /// Also gets cleared on mod rediscovery. - /// - public readonly HashSet NewMods = new(); + public DirectoryInfo BasePath { get; private set; } = null!; + public bool Valid { get; private set; } - public Mod this[int idx] - => _mods[idx]; - - public Mod this[Index idx] - => _mods[idx]; - - public int Count - => _mods.Count; - - public IEnumerator GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - /// - /// Try to obtain a mod by its directory name (unique identifier, preferred), - /// or the first mod of the given name if no directory fits. - /// - public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) + public ModManager2(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor) { - mod = null; - foreach (var m in _mods) - { - if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) + _config = config; + _communicator = communicator; + DataEditor = dataEditor; + OptionEditor = optionEditor; + } + + /// Change the mod base directory and discover available mods. + public void DiscoverMods(string newDir) + { + SetBaseDirectory(newDir, false); + DiscoverMods(); + } + + /// + /// Discover mods without changing the root directory. + /// + public void DiscoverMods() + { + _communicator.ModDiscoveryStarted.Invoke(); + NewMods.Clear(); + Mods.Clear(); + BasePath.Refresh(); + + if (Valid && BasePath.Exists) + ScanMods(); + + _communicator.ModDiscoveryFinished.Invoke(); + Penumbra.Log.Information("Rediscovered mods."); + + if (ModBackup.MigrateModBackups) + ModBackup.MigrateZipToPmp(this); + } + + /// Load a new mod and add it to the manager if successful. + public void AddMod(DirectoryInfo modFolder) + { + if (this.Any(m => m.ModPath.Name == modFolder.Name)) + return; + + Mod.Creator.SplitMultiGroups(modFolder); + var mod = Mod.LoadMod(Penumbra.ModManager, modFolder, true); + if (mod == null) + return; + + mod.Index = Count; + Mods.Add(mod); + _communicator.ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); + Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); + } + + /// + /// Delete a mod. The event is invoked before the mod is removed from the list. + /// Deletes from filesystem as well as from internal data. + /// Updates indices of later mods. + /// + public void DeleteMod(Mod mod) + { + if (Directory.Exists(mod.ModPath.FullName)) + try { - mod = m; - return true; + Directory.Delete(mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); } - if (m.Name == modName) - mod ??= m; + _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); + Mods.RemoveAt(mod.Index); + foreach (var remainingMod in this.Skip(mod.Index)) + --remainingMod.Index; + + Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + } + + /// + /// Reload a mod without changing its base directory. + /// If the base directory does not exist anymore, the mod will be deleted. + /// + public void ReloadMod(Mod mod) + { + var oldName = mod.Name; + + _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); + if (!mod.Reload(Penumbra.ModManager, true, out var metaChange)) + { + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); + + DeleteMod(mod); + return; } - return mod != null; - } - - /// The actual list of mods. - private readonly List _mods = new(); - - public ModManager2(ModDataEditor dataEditor, ModOptionEditor optionEditor) + _communicator.ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + + /// + /// Rename/Move a mod directory. + /// Updates all collection settings and sort order settings. + /// + public void MoveModDirectory(Mod mod, string newName) { - DataEditor = dataEditor; - OptionEditor = optionEditor; + var oldName = mod.Name; + var oldDirectory = mod.ModPath; + + switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) + { + case NewDirectoryState.NonExisting: + // Nothing to do + break; + case NewDirectoryState.ExistsEmpty: + try + { + Directory.Delete(dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); + return; + } + + break; + // Should be caught beforehand. + case NewDirectoryState.ExistsNonEmpty: + case NewDirectoryState.ExistsAsFile: + case NewDirectoryState.ContainsInvalidSymbols: + // Nothing to do at all. + case NewDirectoryState.Identical: + default: + return; + } + + try + { + Directory.Move(oldDirectory.FullName, dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); + return; + } + + DataEditor.MoveDataFile(oldDirectory, dir); + + dir.Refresh(); + mod.ModPath = dir; + if (!mod.Reload(Penumbra.ModManager, false, out var metaChange)) + { + Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); + return; + } + + _communicator.ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + /// Return the state of the new potential name of a directory. + public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + { + directory = null; + if (newName.Length == 0) + return NewDirectoryState.Empty; + + if (oldName == newName) + return NewDirectoryState.Identical; + + var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); + if (fixedNewName != newName) + return NewDirectoryState.ContainsInvalidSymbols; + + directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); + if (File.Exists(directory.FullName)) + return NewDirectoryState.ExistsAsFile; + + if (!Directory.Exists(directory.FullName)) + return NewDirectoryState.NonExisting; + + if (directory.EnumerateFileSystemInfos().Any()) + return NewDirectoryState.ExistsNonEmpty; + + return NewDirectoryState.ExistsEmpty; } + + /// Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Added: + NewMods.Add(mod); + break; + case ModPathChangeType.Deleted: + NewMods.Remove(mod); + break; + case ModPathChangeType.Moved: + if (oldDirectory != null && newDirectory != null) + DataEditor.MoveDataFile(oldDirectory, newDirectory); + + break; + } + } + public void Dispose() { } + + /// + /// Set the mod base directory. + /// If its not the first time, check if it is the same directory as before. + /// Also checks if the directory is available and tries to create it if it is not. + /// + private void SetBaseDirectory(string newPath, bool firstTime) + { + if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + return; + + if (newPath.Length == 0) + { + Valid = false; + BasePath = new DirectoryInfo("."); + if (_config.ModDirectory != BasePath.FullName) + TriggerModDirectoryChange(string.Empty, false); + } + else + { + var newDir = new DirectoryInfo(newPath); + if (!newDir.Exists) + try + { + Directory.CreateDirectory(newDir.FullName); + newDir.Refresh(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); + } + + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + if (_config.ModDirectory != BasePath.FullName) + TriggerModDirectoryChange(BasePath.FullName, Valid); + } + } + + private void TriggerModDirectoryChange(string newPath, bool valid) + { + _config.ModDirectory = newPath; + _config.Save(); + Penumbra.Log.Information($"Set new mod base directory from {_config.ModDirectory} to {newPath}."); + _communicator.ModDirectoryChanged.Invoke(newPath, valid); + } + + + + /// + /// Iterate through available mods with multiple threads and queue their loads, + /// then add the mods from the queue. + /// + private void ScanMods() + { + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Mod.LoadMod(Penumbra.ModManager, dir, false); + if (mod != null) + queue.Enqueue(mod); + }); + + foreach (var mod in queue) + { + mod.Index = Count; + Mods.Add(mod); + } + } } public sealed partial class ModManager : IReadOnlyList, IDisposable diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 8e96b3e2..88cc9e75 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -32,6 +32,7 @@ public class ModOptionEditor mod._groups[groupIdx] = group.Convert(type); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + mod.HasOptions = mod.Groups.Any(o => o.IsOption); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs new file mode 100644 index 00000000..dbf5c46a --- /dev/null +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Penumbra.Mods; + +public class ModStorage : IReadOnlyList +{ + /// The actual list of mods. + protected readonly List Mods = new(); + + public int Count + => Mods.Count; + + public Mod this[int idx] + => Mods[idx]; + + public Mod this[Index idx] + => Mods[idx]; + + public IEnumerator GetEnumerator() + => Mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Try to obtain a mod by its directory name (unique identifier, preferred), + /// or the first mod of the given name if no directory fits. + /// + public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in Mods) + { + if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) + { + mod = m; + return true; + } + + if (m.Name == modName) + mod ??= m; + } + + return mod != null; + } + + /// + /// An easily accessible set of new mods. + /// Mods are added when they are created or imported. + /// Mods are removed when they are deleted or when they are toggled in any collection. + /// Also gets cleared on mod rediscovery. + /// + protected readonly HashSet NewMods = new(); + + public bool IsNew(Mod mod) + => NewMods.Contains(mod); + + public void SetNew(Mod mod) + => NewMods.Add(mod); + + public void SetKnown(Mod mod) + => NewMods.Remove(mod); +} diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 97910191..bd11d477 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -14,80 +14,78 @@ public partial class Mod public ISubMod Default => _default; - public IReadOnlyList< IModGroup > Groups + public IReadOnlyList Groups => _groups; - internal readonly SubMod _default; - internal readonly List< IModGroup > _groups = new(); + internal readonly SubMod _default; + internal readonly List _groups = new(); - public int TotalFileCount { get; internal set; } - public int TotalSwapCount { get; internal set; } - public int TotalManipulations { get; internal set; } - public bool HasOptions { get; internal set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public bool HasOptions { get; internal set; } internal bool SetCounts() { TotalFileCount = 0; TotalSwapCount = 0; TotalManipulations = 0; - foreach( var s in AllSubMods ) + foreach (var s in AllSubMods) { TotalFileCount += s.Files.Count; TotalSwapCount += s.FileSwaps.Count; TotalManipulations += s.Manipulations.Count; } - HasOptions = _groups.Any( o + HasOptions = _groups.Any(o => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 - || o is SingleModGroup s && s.OptionData.Count > 1 ); + || o is SingleModGroup s && s.OptionData.Count > 1); return true; } - public IEnumerable< ISubMod > AllSubMods - => _groups.SelectMany( o => o ).Prepend( _default ); + public IEnumerable AllSubMods + => _groups.SelectMany(o => o).Prepend(_default); - public IEnumerable< MetaManipulation > AllManipulations - => AllSubMods.SelectMany( s => s.Manipulations ); + public IEnumerable AllManipulations + => AllSubMods.SelectMany(s => s.Manipulations); - public IEnumerable< Utf8GamePath > AllRedirects - => AllSubMods.SelectMany( s => s.Files.Keys.Concat( s.FileSwaps.Keys ) ); + public IEnumerable AllRedirects + => AllSubMods.SelectMany(s => s.Files.Keys.Concat(s.FileSwaps.Keys)); - public IEnumerable< FullPath > AllFiles - => AllSubMods.SelectMany( o => o.Files ) - .Select( p => p.Value ); + public IEnumerable AllFiles + => AllSubMods.SelectMany(o => o.Files) + .Select(p => p.Value); - public IEnumerable< FileInfo > GroupFiles - => ModPath.EnumerateFiles( "group_*.json" ); + public IEnumerable GroupFiles + => ModPath.EnumerateFiles("group_*.json"); - public List< FullPath > FindUnusedFiles() + public List FindUnusedFiles() { var modFiles = AllFiles.ToHashSet(); return ModPath.EnumerateDirectories() - .SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - .Select( f => new FullPath( f ) ) - .Where( f => !modFiles.Contains( f ) ) - .ToList(); + .SelectMany(f => f.EnumerateFiles("*", SearchOption.AllDirectories)) + .Select(f => new FullPath(f)) + .Where(f => !modFiles.Contains(f)) + .ToList(); } - private static IModGroup? LoadModGroup( Mod mod, FileInfo file, int groupIdx ) + private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) { - if( !File.Exists( file.FullName ) ) - { + if (!File.Exists(file.FullName)) return null; - } try { - var json = JObject.Parse( File.ReadAllText( file.FullName ) ); - switch( json[ nameof( Type ) ]?.ToObject< GroupType >() ?? GroupType.Single ) + var json = JObject.Parse(File.ReadAllText(file.FullName)); + switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) { - case GroupType.Multi: return MultiModGroup.Load( mod, json, groupIdx ); - case GroupType.Single: return SingleModGroup.Load( mod, json, groupIdx ); + case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx); + case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx); } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not read mod group from {file.FullName}:\n{e}" ); + Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}"); } return null; @@ -97,13 +95,13 @@ public partial class Mod { _groups.Clear(); var changes = false; - foreach( var file in GroupFiles ) + foreach (var file in GroupFiles) { - var group = LoadModGroup( this, file, _groups.Count ); - if( group != null && _groups.All( g => g.Name != group.Name ) ) + var group = LoadModGroup(this, file, _groups.Count); + if (group != null && _groups.All(g => g.Name != group.Name)) { changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName; - _groups.Add( group ); + _groups.Add(group); } else { @@ -111,7 +109,7 @@ public partial class Mod } } - if( changes ) + if (changes) Penumbra.SaveService.SaveAllOptionGroups(this); } @@ -122,13 +120,9 @@ public partial class Mod try { if (!File.Exists(defaultFile)) - { _default.Load(ModPath, new JObject(), out _); - } else - { _default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); - } } catch (Exception e) { @@ -145,17 +139,13 @@ public partial class Mod { var dir = Creator.NewOptionDirectory(ModPath, group.Name); if (!dir.Exists) - { dir.Create(); - } foreach (var option in group.OfType()) { var optionDir = Creator.NewOptionDirectory(dir, option.Name); if (!optionDir.Exists) - { optionDir.Create(); - } option.WriteTexToolsMeta(optionDir); } @@ -166,5 +156,4 @@ public partial class Mod Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); } } - -} \ No newline at end of file +} diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs new file mode 100644 index 00000000..3fb6d3f0 --- /dev/null +++ b/Penumbra/Mods/ModCache.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Penumbra.Mods; + +public class ModCache +{ + public int TotalFileCount; + public int TotalSwapCount; + public int TotalManipulations; + public bool HasOptions; + + public SortedList ChangedItems = new(); + public string LowerChangedItemsString = string.Empty; + public string AllTagsLower = string.Empty; + + public void Reset() + { + TotalFileCount = 0; + TotalSwapCount = 0; + TotalManipulations = 0; + HasOptions = false; + ChangedItems.Clear(); + LowerChangedItemsString = string.Empty; + AllTagsLower = string.Empty; + } +} diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/ModCacheManager.cs new file mode 100644 index 00000000..69787644 --- /dev/null +++ b/Penumbra/Mods/ModCacheManager.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.Services; + +namespace Penumbra.Mods; + +public class ModCacheManager : IDisposable, IReadOnlyList +{ + private readonly CommunicatorService _communicator; + private readonly IdentifierService _identifier; + private readonly IReadOnlyList _modManager; + + private readonly List _cache = new(); + + // TODO ModManager2 + public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager) + { + _communicator = communicator; + _identifier = identifier; + _modManager = modManager; + + _communicator.ModOptionChanged.Event += OnModOptionChange; + _communicator.ModPathChanged.Event += OnModPathChange; + _communicator.ModDataChanged.Event += OnModDataChange; + _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; + if (!identifier.Valid) + identifier.FinishedCreation += OnIdentifierCreation; + OnModDiscoveryFinished(); + } + + public IEnumerator GetEnumerator() + => _cache.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count { get; private set; } + + public ModCache this[int index] + => _cache[index]; + + public ModCache this[Mod mod] + => _cache[mod.Index]; + + public void Dispose() + { + _communicator.ModOptionChanged.Event -= OnModOptionChange; + _communicator.ModPathChanged.Event -= OnModPathChange; + _communicator.ModDataChanged.Event -= OnModDataChange; + _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; + } + + /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. + public static void ComputeChangedItems(IObjectIdentifier identifier, IDictionary changedItems, MetaManipulation manip) + { + switch (manip.ManipulationType) + { + case MetaManipulation.Type.Imc: + switch (manip.Imc.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + identifier.Identify(changedItems, + GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Weapon: + identifier.Identify(changedItems, + GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + case ObjectType.DemiHuman: + identifier.Identify(changedItems, + GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Monster: + identifier.Identify(changedItems, + GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + } + + break; + case MetaManipulation.Type.Eqdp: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); + break; + case MetaManipulation.Type.Eqp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); + break; + case MetaManipulation.Type.Est: + switch (manip.Est.Slot) + { + case EstManipulation.EstType.Hair: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Face: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Body: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Body)); + break; + case EstManipulation.EstType.Head: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Head)); + break; + } + + break; + case MetaManipulation.Type.Gmp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + break; + case MetaManipulation.Type.Rsp: + changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); + break; + } + } + + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + { + ModCache cache; + switch (type) + { + case ModOptionChangeType.GroupAdded: + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.OptionAdded: + case ModOptionChangeType.OptionDeleted: + cache = EnsureCount(mod); + UpdateChangedItems(cache, mod); + UpdateCounts(cache, mod); + break; + case ModOptionChangeType.GroupTypeChanged: + UpdateHasOptions(EnsureCount(mod), mod); + break; + case ModOptionChangeType.OptionFilesChanged: + case ModOptionChangeType.OptionFilesAdded: + cache = EnsureCount(mod); + UpdateChangedItems(cache, mod); + UpdateFileCount(cache, mod); + break; + case ModOptionChangeType.OptionSwapsChanged: + cache = EnsureCount(mod); + UpdateChangedItems(cache, mod); + UpdateSwapCount(cache, mod); + break; + case ModOptionChangeType.OptionMetaChanged: + cache = EnsureCount(mod); + UpdateChangedItems(cache, mod); + UpdateMetaCount(cache, mod); + break; + } + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? @new) + { + switch (type) + { + case ModPathChangeType.Added: + case ModPathChangeType.Reloaded: + Refresh(EnsureCount(mod), mod); + break; + case ModPathChangeType.Deleted: + --Count; + var oldCache = _cache[mod.Index]; + oldCache.Reset(); + for (var i = mod.Index; i < Count; ++i) + _cache[i] = _cache[i + 1]; + _cache[Count] = oldCache; + break; + } + } + + private void OnModDataChange(ModDataChangeType type, Mod mod, string? _) + { + if ((type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) != 0) + UpdateTags(EnsureCount(mod), mod); + } + + private void OnModDiscoveryFinished() + { + if (_modManager.Count > _cache.Count) + _cache.AddRange(Enumerable.Range(0, _modManager.Count - _cache.Count).Select(_ => new ModCache())); + + Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { Refresh(_cache[idx], _modManager[idx]); }); + } + + private void OnIdentifierCreation() + { + Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { UpdateChangedItems(_cache[idx], _modManager[idx]); }); + _identifier.FinishedCreation -= OnIdentifierCreation; + } + + private static void UpdateFileCount(ModCache cache, Mod mod) + => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); + + private static void UpdateSwapCount(ModCache cache, Mod mod) + => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + + private static void UpdateMetaCount(ModCache cache, Mod mod) + => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Manipulations.Count); + + private static void UpdateHasOptions(ModCache cache, Mod mod) + => cache.HasOptions = mod.Groups.Any(o => o.IsOption); + + private static void UpdateTags(ModCache cache, Mod mod) + => cache.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant())); + + private void UpdateChangedItems(ModCache cache, Mod mod) + { + cache.ChangedItems.Clear(); + if (!_identifier.Valid) + return; + + foreach (var gamePath in mod.AllRedirects) + _identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString()); + + foreach (var manip in mod.AllManipulations) + ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip); + + cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + } + + private static void UpdateCounts(ModCache cache, Mod mod) + { + cache.TotalFileCount = mod.Default.Files.Count; + cache.TotalSwapCount = mod.Default.FileSwaps.Count; + cache.TotalManipulations = mod.Default.Manipulations.Count; + cache.HasOptions = false; + foreach (var group in mod.Groups) + { + cache.HasOptions |= group.IsOption; + foreach (var s in group) + { + cache.TotalFileCount += s.Files.Count; + cache.TotalSwapCount += s.FileSwaps.Count; + cache.TotalManipulations += s.Manipulations.Count; + } + } + } + + private void Refresh(ModCache cache, Mod mod) + { + UpdateTags(cache, mod); + UpdateCounts(cache, mod); + UpdateChangedItems(cache, mod); + } + + private ModCache EnsureCount(Mod mod) + { + if (mod.Index < Count) + return _cache[mod.Index]; + + + if (mod.Index >= _cache.Count) + _cache.AddRange(Enumerable.Range(0, mod.Index - _cache.Count).Select(_ => new ModCache())); + else if (mod.Index >= Count) + for (var i = Count; i <= mod.Index; ++i) + _cache[i].Reset(); + + return _cache[mod.Index]; + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 78931cf0..283de2bb 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -105,6 +105,7 @@ public class Penumbra : IDalamudPlugin RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); ResourceLoader = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { PathResolver = _tmp.Services.GetRequiredService(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index afba2adf..73c642ac 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -149,8 +149,9 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add API services.AddSingleton() diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 18c64eac..55f057a3 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Penumbra.Collections; using Penumbra.Mods; using Penumbra.Util; @@ -7,7 +8,9 @@ namespace Penumbra.Services; public class CommunicatorService : IDisposable { - /// + /// + /// Triggered whenever collection setup is changed. + /// /// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) /// Parameter is the old collection, or null on additions. /// Parameter is the new collection, or null on deletions. @@ -15,21 +18,18 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper CollectionChange = new(nameof(CollectionChange)); - /// + /// + /// Triggered whenever a temporary mod for all collections is changed. + /// /// Parameter added, deleted or edited temporary mod. /// Parameter is whether the mod was newly created. /// Parameter is whether the mod was deleted. /// public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); - /// - /// Parameter is the type of change. - /// Parameter is the affected mod. - /// Parameter is either null or the old name of the mod. - /// - public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); - - /// + /// + /// Triggered whenever a character base draw object is being created by the game. + /// /// Parameter is the game object for which a draw object is created. /// Parameter is the name of the applied collection. /// Parameter is a pointer to the model id (an uint). @@ -45,14 +45,18 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); - /// + /// + /// Triggered whenever mod meta data or local data is changed. + /// /// Parameter is the type of data change for the mod, which can be multiple flags. /// Parameter is the changed mod. /// Parameter is the old name of the mod in case of a name change, and null otherwise. /// public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); - /// + /// + /// Triggered whenever an option of a mod is changed inside the mod. + /// /// Parameter is the type option change. /// Parameter is the changed mod. /// Parameter is the index of the changed group inside the mod. @@ -61,14 +65,44 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged)); + + /// Triggered whenever mods are prepared to be rediscovered. + public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted)); + + /// Triggered whenever a new mod discovery has finished. + public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished)); + + /// + /// Triggered whenever the mod root directory changes. + /// + /// Parameter is the full path of the new directory. + /// Parameter is whether the new directory is valid. + /// + /// + public readonly EventWrapper ModDirectoryChanged = new(nameof(ModDirectoryChanged)); + + /// + /// Triggered whenever a mod is added, deleted, moved or reloaded. + /// + /// Parameter is the type of change. + /// Parameter is the changed mod. + /// Parameter is the old directory on deletion, move or reload and null on addition. + /// Parameter is the new directory on addition, move or reload and null on deletion. + /// + /// + public EventWrapper ModPathChanged = new(nameof(ModPathChanged)); + public void Dispose() { CollectionChange.Dispose(); TemporaryGlobalModChange.Dispose(); - ModMetaChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); ModDataChanged.Dispose(); ModOptionChanged.Dispose(); + ModDiscoveryStarted.Dispose(); + ModDiscoveryFinished.Dispose(); + ModDirectoryChanged.Dispose(); + ModPathChanged.Dispose(); } } diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs index e25cc99c..2472e74d 100644 --- a/Penumbra/Util/EventWrapper.cs +++ b/Penumbra/Util/EventWrapper.cs @@ -58,6 +58,60 @@ public readonly struct EventWrapper : IDisposable } } +public readonly struct EventWrapper : IDisposable +{ + private readonly string _name; + private readonly List> _event = new(); + + public EventWrapper(string name) + => _name = name; + + public void Invoke(T1 arg1) + { + lock (_event) + { + foreach (var action in _event) + { + try + { + action.Invoke(arg1); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + } + } + } + } + + public void Dispose() + { + lock (_event) + { + _event.Clear(); + } + } + + public event Action Event + { + add + { + lock (_event) + { + if (_event.All(a => a != value)) + _event.Add(value); + } + } + remove + { + lock (_event) + { + _event.Remove(value); + } + } + } +} + public readonly struct EventWrapper : IDisposable { private readonly string _name; From afa11f85e28fb0fd5806667c4683365aa97dc98b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 Mar 2023 23:51:13 +0200 Subject: [PATCH 0846/2451] Use ModManager2 --- Penumbra/Api/PenumbraApi.cs | 50 +++-- Penumbra/Collections/CollectionManager.cs | 13 +- Penumbra/Mods/Editor/ModBackup.cs | 2 +- Penumbra/Mods/Manager/ExportManager.cs | 17 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 203 ------------------ Penumbra/Mods/Manager/Mod.Manager.Root.cs | 102 --------- Penumbra/Mods/Manager/Mod.Manager.cs | 174 +++------------ Penumbra/Mods/ModCacheManager.cs | 1 - Penumbra/Mods/ModFileSystem.cs | 16 +- Penumbra/PenumbraNew.cs | 6 +- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- Penumbra/UI/FileDialogService.cs | 17 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 40 ++-- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 24 +-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 16 files changed, 131 insertions(+), 542 deletions(-) delete mode 100644 Penumbra/Mods/Manager/Mod.Manager.BasePath.cs delete mode 100644 Penumbra/Mods/Manager/Mod.Manager.Root.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5b5df883..8b6adafc 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -56,6 +56,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if (value == null) return; + CheckInitialized(); _communicator.CreatingCharacterBase.Event += new Action(value); } @@ -63,6 +64,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if (value == null) return; + CheckInitialized(); _communicator.CreatingCharacterBase.Event -= new Action(value); } @@ -74,6 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if (value == null) return; + CheckInitialized(); _communicator.CreatedCharacterBase.Event += new Action(value); } @@ -81,6 +84,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if (value == null) return; + CheckInitialized(); _communicator.CreatedCharacterBase.Event -= new Action(value); } @@ -93,10 +97,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi private Penumbra _penumbra; private Lumina.GameData? _lumina; - private ModManager _modManager; + private ModManager _modManager; private ResourceLoader _resourceLoader; private Configuration _config; - private CollectionManager _collectionManager; + private CollectionManager _collectionManager; private DalamudServices _dalamud; private TempCollectionManager _tempCollections; private TempModManager _tempMods; @@ -108,18 +112,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) { - _communicator = communicator; - _penumbra = penumbra; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; + _communicator = communicator; + _penumbra = penumbra; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) @@ -129,13 +133,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.CollectionChange.Event += SubscribeToNewCollections; _resourceLoader.ResourceLoaded += OnResourceLoaded; - _modManager.ModPathChanged += ModPathChangeSubscriber; + _communicator.ModPathChanged.Event += ModPathChangeSubscriber; } public unsafe void Dispose() { if (!Valid) - return; + return; foreach (var collection in _collectionManager) { @@ -145,7 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded -= OnResourceLoaded; _communicator.CollectionChange.Event -= SubscribeToNewCollections; - _modManager.ModPathChanged -= ModPathChangeSubscriber; + _communicator.ModPathChanged.Event -= ModPathChangeSubscriber; _lumina = null; _communicator = null!; _penumbra = null!; @@ -182,12 +186,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _modManager.ModDirectoryChanged += value; + _communicator.ModDirectoryChanged.Event += value; } remove { CheckInitialized(); - _modManager.ModDirectoryChanged -= value; + _communicator.ModDirectoryChanged.Event -= value; } } @@ -542,8 +546,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) { - CheckInitialized(); - var data = _collectionResolver.IdentifyCollection((DrawObject*) drawObject, true); + CheckInitialized(); + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); return (data.AssociatedGameObject, data.ModCollection.Name); } @@ -592,7 +596,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - _modManager.ReloadMod(mod.Index); + _modManager.ReloadMod(mod); return PenumbraApiEc.Success; } @@ -613,7 +617,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.NothingChanged; - _modManager.DeleteMod(mod.Index); + _modManager.DeleteMod(mod); return PenumbraApiEc.Success; } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index c1510686..0f2694ba 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -9,7 +9,6 @@ using System.IO; using System.Linq; using Penumbra.Api; using Penumbra.GameData.Actors; -using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.Util; @@ -71,10 +70,10 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable _exportDirectory ?? _modManager.BasePath; - public ExportManager(Configuration config, ModManager modManager) + public ExportManager(Configuration config, CommunicatorService communicator, ModManager modManager) { - _config = config; - _modManager = modManager; + _config = config; + _communicator = communicator; + _modManager = modManager; UpdateExportDirectory(_config.ExportDirectory, false); - _modManager.ModPathChanged += OnModPathChange; + _communicator.ModPathChanged.Event += OnModPathChange; } /// @@ -73,7 +76,7 @@ public class ExportManager : IDisposable } public void Dispose() - => _modManager.ModPathChanged -= OnModPathChange; + => _communicator.ModPathChanged.Event -= OnModPathChange; /// Automatically migrate the backup file to the new name if any exists. private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs deleted file mode 100644 index b6e50c80..00000000 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.IO; -using System.Linq; - -namespace Penumbra.Mods; - -public partial class ModManager -{ - public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory); - - public event ModPathChangeDelegate ModPathChanged; - - /// - /// Rename/Move a mod directory. - /// Updates all collection settings and sort order settings. - /// - public void MoveModDirectory(int idx, string newName) - { - var mod = this[idx]; - var oldName = mod.Name; - var oldDirectory = mod.ModPath; - - switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) - { - case NewDirectoryState.NonExisting: - // Nothing to do - break; - case NewDirectoryState.ExistsEmpty: - try - { - Directory.Delete(dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); - return; - } - - break; - // Should be caught beforehand. - case NewDirectoryState.ExistsNonEmpty: - case NewDirectoryState.ExistsAsFile: - case NewDirectoryState.ContainsInvalidSymbols: - // Nothing to do at all. - case NewDirectoryState.Identical: - default: - return; - } - - try - { - Directory.Move(oldDirectory.FullName, dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); - return; - } - - DataEditor.MoveDataFile(oldDirectory, dir); - - dir.Refresh(); - mod.ModPath = dir; - if (!mod.Reload(this, false, out var metaChange)) - { - Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Reload a mod without changing its base directory. - /// If the base directory does not exist anymore, the mod will be deleted. - /// - public void ReloadMod(int idx) - { - var mod = this[idx]; - var oldName = mod.Name; - - ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!mod.Reload(this, true, out var metaChange)) - { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(idx); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Delete a mod by its index. The event is invoked before the mod is removed from the list. - /// Deletes from filesystem as well as from internal data. - /// Updates indices of later mods. - /// - public void DeleteMod(int idx) - { - var mod = this[idx]; - if (Directory.Exists(mod.ModPath.FullName)) - try - { - Directory.Delete(mod.ModPath.FullName, true); - Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); - } - - ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - _mods.RemoveAt(idx); - foreach (var remainingMod in _mods.Skip(idx)) - --remainingMod.Index; - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); - } - - /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) - { - if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) - return; - - Mod.Creator.SplitMultiGroups(modFolder); - var mod = Mod.LoadMod(this, modFolder, true); - if (mod == null) - return; - - mod.Index = _mods.Count; - _mods.Add(mod); - ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); - Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); - } - - public enum NewDirectoryState - { - NonExisting, - ExistsEmpty, - ExistsNonEmpty, - ExistsAsFile, - ContainsInvalidSymbols, - Identical, - Empty, - } - - /// Return the state of the new potential name of a directory. - public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) - { - directory = null; - if (newName.Length == 0) - return NewDirectoryState.Empty; - - if (oldName == newName) - return NewDirectoryState.Identical; - - var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); - if (fixedNewName != newName) - return NewDirectoryState.ContainsInvalidSymbols; - - directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); - if (File.Exists(directory.FullName)) - return NewDirectoryState.ExistsAsFile; - - if (!Directory.Exists(directory.FullName)) - return NewDirectoryState.NonExisting; - - if (directory.EnumerateFileSystemInfos().Any()) - return NewDirectoryState.ExistsNonEmpty; - - return NewDirectoryState.ExistsEmpty; - } - - - /// Add new mods to NewMods and remove deleted mods from NewMods. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { - case ModPathChangeType.Added: - NewMods.Add(mod); - break; - case ModPathChangeType.Deleted: - NewMods.Remove(mod); - break; - case ModPathChangeType.Moved: - if (oldDirectory != null && newDirectory != null) - DataEditor.MoveDataFile(oldDirectory, newDirectory); - - break; - } - } -} diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs deleted file mode 100644 index 575954d5..00000000 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Threading.Tasks; - -namespace Penumbra.Mods; - -public sealed partial class ModManager -{ - public DirectoryInfo BasePath { get; private set; } = null!; - public bool Valid { get; private set; } - - public event Action? ModDiscoveryStarted; - public event Action? ModDiscoveryFinished; - public event Action ModDirectoryChanged; - - // Change the mod base directory and discover available mods. - public void DiscoverMods(string newDir) - { - SetBaseDirectory(newDir, false); - DiscoverMods(); - } - - // Set the mod base directory. - // If its not the first time, check if it is the same directory as before. - // Also checks if the directory is available and tries to create it if it is not. - private void SetBaseDirectory(string newPath, bool firstTime) - { - if (!firstTime && string.Equals(newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase)) - return; - - if (newPath.Length == 0) - { - Valid = false; - BasePath = new DirectoryInfo("."); - if (Penumbra.Config.ModDirectory != BasePath.FullName) - ModDirectoryChanged.Invoke(string.Empty, false); - } - else - { - var newDir = new DirectoryInfo(newPath); - if (!newDir.Exists) - try - { - Directory.CreateDirectory(newDir.FullName); - newDir.Refresh(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); - } - - BasePath = newDir; - Valid = Directory.Exists(newDir.FullName); - if (Penumbra.Config.ModDirectory != BasePath.FullName) - ModDirectoryChanged.Invoke(BasePath.FullName, Valid); - } - } - - private static void OnModDirectoryChange(string newPath, bool _) - { - Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}."); - Penumbra.Config.ModDirectory = newPath; - Penumbra.Config.Save(); - } - - // Discover new mods. - public void DiscoverMods() - { - NewMods.Clear(); - ModDiscoveryStarted?.Invoke(); - _mods.Clear(); - BasePath.Refresh(); - - if (Valid && BasePath.Exists) - { - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, - }; - var queue = new ConcurrentQueue(); - Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => - { - var mod = Mod.LoadMod(this, dir, false); - if (mod != null) - queue.Enqueue(mod); - }); - - foreach (var mod in queue) - { - mod.Index = _mods.Count; - _mods.Add(mod); - } - } - - ModDiscoveryFinished?.Invoke(); - Penumbra.Log.Information("Rediscovered mods."); - - if (MigrateModBackups) - ModBackup.MigrateZipToPmp(this); - } -} diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index febe8209..050c4ed1 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -1,15 +1,12 @@ using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; using System.IO; using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Penumbra.Services; -using Penumbra.Util; + namespace Penumbra.Mods; - + /// Describes the state of a potential move-target for a mod. public enum NewDirectoryState { @@ -20,25 +17,27 @@ public enum NewDirectoryState ContainsInvalidSymbols, Identical, Empty, -} +} -public sealed class ModManager2 : ModStorage +public sealed class ModManager : ModStorage { private readonly Configuration _config; - private readonly CommunicatorService _communicator; + private readonly CommunicatorService _communicator; public readonly ModDataEditor DataEditor; public readonly ModOptionEditor OptionEditor; public DirectoryInfo BasePath { get; private set; } = null!; - public bool Valid { get; private set; } + public bool Valid { get; private set; } - public ModManager2(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor) + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor) { _config = config; _communicator = communicator; DataEditor = dataEditor; - OptionEditor = optionEditor; + OptionEditor = optionEditor; + SetBaseDirectory(config.ModDirectory, true); + DiscoverMods(); } /// Change the mod base directory and discover available mods. @@ -75,7 +74,7 @@ public sealed class ModManager2 : ModStorage return; Mod.Creator.SplitMultiGroups(modFolder); - var mod = Mod.LoadMod(Penumbra.ModManager, modFolder, true); + var mod = Mod.LoadMod(this, modFolder, true); if (mod == null) return; @@ -133,16 +132,16 @@ public sealed class ModManager2 : ModStorage _communicator.ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); if (metaChange != ModDataChangeType.None) _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - - /// + } + + + /// /// Rename/Move a mod directory. - /// Updates all collection settings and sort order settings. - /// + /// Updates all collection settings and sort order settings. + /// public void MoveModDirectory(Mod mod, string newName) { - var oldName = mod.Name; + var oldName = mod.Name; var oldDirectory = mod.ModPath; switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) @@ -195,8 +194,8 @@ public sealed class ModManager2 : ModStorage _communicator.ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); if (metaChange != ModDataChangeType.None) _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - + } + /// Return the state of the new potential name of a directory. public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) { @@ -243,16 +242,16 @@ public sealed class ModManager2 : ModStorage break; } - } + } public void Dispose() { } - /// + /// /// Set the mod base directory. /// If its not the first time, check if it is the same directory as before. - /// Also checks if the directory is available and tries to create it if it is not. - /// + /// Also checks if the directory is available and tries to create it if it is not. + /// private void SetBaseDirectory(string newPath, bool firstTime) { if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) @@ -260,7 +259,7 @@ public sealed class ModManager2 : ModStorage if (newPath.Length == 0) { - Valid = false; + Valid = false; BasePath = new DirectoryInfo("."); if (_config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(string.Empty, false); @@ -280,8 +279,8 @@ public sealed class ModManager2 : ModStorage } BasePath = newDir; - Valid = Directory.Exists(newDir.FullName); - if (_config.ModDirectory != BasePath.FullName) + Valid = Directory.Exists(newDir.FullName); + if (!firstTime && _config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(BasePath.FullName, Valid); } } @@ -295,10 +294,9 @@ public sealed class ModManager2 : ModStorage } - - /// + /// /// Iterate through available mods with multiple threads and queue their loads, - /// then add the mods from the queue. + /// then add the mods from the queue. /// private void ScanMods() { @@ -309,7 +307,7 @@ public sealed class ModManager2 : ModStorage var queue = new ConcurrentQueue(); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => { - var mod = Mod.LoadMod(Penumbra.ModManager, dir, false); + var mod = Mod.LoadMod(this, dir, false); if (mod != null) queue.Enqueue(mod); }); @@ -321,111 +319,3 @@ public sealed class ModManager2 : ModStorage } } } - -public sealed partial class ModManager : IReadOnlyList, IDisposable -{ - // Set when reading Config and migrating from v4 to v5. - public static bool MigrateModBackups = false; - - // An easily accessible set of new mods. - // Mods are added when they are created or imported. - // Mods are removed when they are deleted or when they are toggled in any collection. - // Also gets cleared on mod rediscovery. - public readonly HashSet NewMods = new(); - - private readonly List _mods = new(); - - public Mod this[int idx] - => _mods[idx]; - - public Mod this[Index idx] - => _mods[idx]; - - public int Count - => _mods.Count; - - public IEnumerator GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - public readonly ModDataEditor DataEditor; - public readonly ModOptionEditor OptionEditor; - - public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, - ModOptionEditor optionEditor) - { - using var timer = time.Measure(StartTimeType.Mods); - _config = config; - _communicator = communicator; - DataEditor = dataEditor; - OptionEditor = optionEditor; - ModDirectoryChanged += OnModDirectoryChange; - SetBaseDirectory(config.ModDirectory, true); - _communicator.ModOptionChanged.Event += OnModOptionChange; - ModPathChanged += OnModPathChange; - DiscoverMods(); - } - - public void Dispose() - { - _communicator.ModOptionChanged.Event -= OnModOptionChange; - } - - - // Try to obtain a mod by its directory name (unique identifier, preferred), - // or the first mod of the given name if no directory fits. - public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) - { - mod = null; - foreach (var m in _mods) - { - if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) - { - mod = m; - return true; - } - - if (m.Name == modName) - mod ??= m; - } - - return mod != null; - } - - private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) - { - if (type == ModOptionChangeType.PrepareChange) - return; - - bool ComputeChangedItems() - { - mod.ComputeChangedItems(); - return true; - } - - // State can not change on adding groups, as they have no immediate options. - var unused = type switch - { - ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), - ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), - ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), - ModOptionChangeType.DisplayChange => false, - _ => false, - }; - } -} diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/ModCacheManager.cs index 69787644..632c5a31 100644 --- a/Penumbra/Mods/ModCacheManager.cs +++ b/Penumbra/Mods/ModCacheManager.cs @@ -20,7 +20,6 @@ public class ModCacheManager : IDisposable, IReadOnlyList private readonly List _cache = new(); - // TODO ModManager2 public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager) { _communicator = communicator; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index c16c172e..42ff4381 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -12,7 +12,7 @@ namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly CommunicatorService _communicator; private readonly FilenameService _files; @@ -23,17 +23,17 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable _communicator = communicator; _files = files; Reload(); - Changed += OnChange; - _modManager.ModDiscoveryFinished += Reload; - _communicator.ModDataChanged.Event += OnDataChange; - _modManager.ModPathChanged += OnModPathChange; + Changed += OnChange; + _communicator.ModDiscoveryFinished.Event += Reload; + _communicator.ModDataChanged.Event += OnDataChange; + _communicator.ModPathChanged.Event += OnModPathChange; } public void Dispose() { - _modManager.ModPathChanged -= OnModPathChange; - _modManager.ModDiscoveryFinished -= Reload; - _communicator.ModDataChanged.Event -= OnDataChange; + _communicator.ModPathChanged.Event -= OnModPathChange; + _communicator.ModDiscoveryFinished.Event -= Reload; + _communicator.ModDataChanged.Event -= OnDataChange; } public struct ImportDate : ISortMode diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 73c642ac..23bfefe5 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -94,7 +94,8 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Add Resource services services.AddSingleton() @@ -150,8 +151,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); // Add API services.AddSingleton() diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index a2f8b55f..c2d64f92 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -113,7 +113,7 @@ public class ConfigMigrationService if (_config.Version != 4) return; - ModManager.MigrateModBackups = true; + ModBackup.MigrateModBackups = true; _config.Version = 5; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 9a0af228..c50449a6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -270,9 +270,9 @@ public class ItemSwapTab : IDisposable, ITab _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager, _modManager.Last(), + if (!_swapData.WriteMod(_modManager, _modManager[^1], _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) - _modManager.DeleteMod(_modManager.Count - 1); + _modManager.DeleteMod(_modManager[^1]); } private void CreateOption() diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 061f53ad..c6a7d451 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -9,23 +9,22 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; -using Penumbra.Mods; +using Penumbra.Services; namespace Penumbra.UI; public class FileDialogService : IDisposable { - private readonly ModManager _mods; + private readonly CommunicatorService _communicator; private readonly FileDialogManager _manager; private readonly ConcurrentDictionary _startPaths = new(); private bool _isOpen; - public FileDialogService(ModManager mods, Configuration config) + public FileDialogService(CommunicatorService communicator, Configuration config) { - _mods = mods; - _manager = SetupFileManager(config.ModDirectory); - - _mods.ModDirectoryChanged += OnModDirectoryChange; + _communicator = communicator; + _manager = SetupFileManager(config.ModDirectory); + _communicator.ModDirectoryChanged.Event += OnModDirectoryChange; } public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, @@ -72,7 +71,7 @@ public class FileDialogService : IDisposable { _startPaths.Clear(); _manager.Reset(); - _mods.ModDirectoryChanged -= OnModDirectoryChange; + _communicator.ModDirectoryChanged.Event -= OnModDirectoryChange; } private string? GetStartPath(string title, string? startPath, bool forceStartPath) @@ -87,7 +86,7 @@ public class FileDialogService : IDisposable { return (valid, list) => { - _isOpen = false; + _isOpen = false; var loc = HandleRoot(GetCurrentLocation()); _startPaths[title] = loc; callback(valid, list.Select(HandleRoot).ToList()); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index d1ec76ad..0ea3b734 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -25,20 +25,20 @@ namespace Penumbra.UI.ModsTab; public sealed partial class ModFileSystemSelector : FileSystemSelector { - private readonly CommunicatorService _communicator; - private readonly ChatService _chat; - private readonly Configuration _config; - private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; - private readonly CollectionManager _collectionManager; - private readonly TutorialService _tutorial; - private readonly ModEditor _modEditor; + private readonly CommunicatorService _communicator; + private readonly ChatService _chat; + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly TutorialService _tutorial; + private readonly ModEditor _modEditor; private TexToolsImporter? _import; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - private uint _infoPopupId = 0; + private uint _infoPopupId = 0; public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat, @@ -81,16 +81,16 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector.Lexicographical).OfType().Select(l => { // Any mod handled here should not stay new. - _modManager.NewMods.Remove(l.Value); + _modManager.SetKnown(l.Value); return l.Value; }); @@ -428,7 +428,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Penumbra.ChatService.NotificationMessage(e.Message, "Failure", NotificationType.Warning); + => Penumbra.ChatService.NotificationMessage(e.Message, "Failure", NotificationType.Warning); #endregion @@ -618,7 +618,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) { - if (Penumbra.ModManager.NewMods.Contains(mod)) + if (_modManager.IsNew(mod)) return ColorId.NewMod; if (settings == null) @@ -638,7 +638,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector A text input for the new directory name and a button to apply the move. private static class MoveDirectory { - private static string? _currentModDirectory; - private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; + private static string? _currentModDirectory; + private static NewDirectoryState _state = NewDirectoryState.Identical; public static void Reset() { _currentModDirectory = null; - _state = ModManager.NewDirectoryState.Identical; + _state = NewDirectoryState.Identical; } public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize) @@ -276,20 +276,20 @@ public class ModPanelEditTab : ITab var (disabled, tt) = _state switch { - ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), - ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), - ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), - ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), - ModManager.NewDirectoryState.ContainsInvalidSymbols => (true, + NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), + NewDirectoryState.Empty => (true, "Please enter a new directory name first."), + NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), + NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), + NewDirectoryState.ContainsInvalidSymbols => (true, $"{_currentModDirectory} contains invalid symbols for FFXIV."), _ => (true, "Unknown error."), }; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Rename Mod Directory", buttonSize, tt, disabled) && _currentModDirectory != null) { - modManager.MoveModDirectory(mod.Index, _currentModDirectory); + modManager.MoveModDirectory(mod, _currentModDirectory); Reset(); } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 72dbe9fa..26702c5a 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -125,7 +125,7 @@ public class ModPanelSettingsTab : ITab if (!ImGui.Checkbox("Enabled", ref enabled)) return; - _modManager.NewMods.Remove(_selector.Selected!); + _modManager.SetKnown(_selector.Selected!); _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); } From 2ffbd7bebaf27091f0ae1b005d1ff8375831963b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 01:27:27 +0200 Subject: [PATCH 0847/2451] Fix Item Swaps not updating when mod is changed. --- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index c50449a6..3182dd30 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -55,6 +55,7 @@ public class ItemSwapTab : IDisposable, ITab _communicator.CollectionChange.Event += OnCollectionChange; _collectionManager.Current.ModSettingChanged += OnSettingChange; + _communicator.ModOptionChanged.Event += OnModOptionChange; } /// Update the currently selected mod or its settings. @@ -99,6 +100,7 @@ public class ItemSwapTab : IDisposable, ITab { _communicator.CollectionChange.Event -= OnCollectionChange; _collectionManager.Current.ModSettingChanged -= OnSettingChange; + _communicator.ModOptionChanged.Event -= OnModOptionChange; } private enum SwapType @@ -753,4 +755,12 @@ public class ItemSwapTab : IDisposable, ITab _swapData.LoadMod(_mod, _modSettings); _dirty = true; } + + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) + { + if (type is ModOptionChangeType.PrepareChange || mod != _mod) + return; + _swapData.LoadMod(_mod, _modSettings); + _dirty = true; + } } From e79b1104296df107eb0f9c2aab4391b26f1505f5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 01:27:47 +0200 Subject: [PATCH 0848/2451] Remove cached data from mod and use ModCaches where required. --- Penumbra/Collections/ModCollection.Cache.cs | 4 +- Penumbra/Mods/Editor/IMod.cs | 2 - Penumbra/Mods/Editor/ModNormalizer.cs | 10 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 1 - Penumbra/Mods/Mod.BasePath.cs | 2 - Penumbra/Mods/Mod.ChangedItems.cs | 91 ------------------- Penumbra/Mods/Mod.Files.cs | 23 ----- Penumbra/Mods/Mod.LocalData.cs | 4 - Penumbra/Mods/ModCacheManager.cs | 2 +- Penumbra/Penumbra.cs | 13 +-- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 12 ++- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 19 ++-- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 14 +-- 14 files changed, 44 insertions(+), 155 deletions(-) delete mode 100644 Penumbra/Mods/Mod.ChangedItems.cs diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index e25f65bf..576652ed 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -321,7 +321,7 @@ internal class ModCollectionCache : IDisposable if( addMetaChanges ) { ++_collection.ChangeCounter; - if( mod.TotalManipulations > 0 ) + if(Penumbra.ModCaches[mod.Index].TotalManipulations > 0 ) { AddMetaFiles(); } @@ -533,7 +533,7 @@ internal class ModCollectionCache : IDisposable foreach( var (manip, mod) in MetaManipulations ) { - Mod.ComputeChangedItems( items, manip ); + ModCacheManager.ComputeChangedItems(identifier, items, manip ); AddItems( mod ); } } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 5b7b0f20..658922cf 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -10,8 +10,6 @@ public interface IMod public int Index { get; } public int Priority { get; } - public int TotalManipulations { get; } - public ISubMod Default { get; } public IReadOnlyList< IModGroup > Groups { get; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index bdd968cd..604fc9e5 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -12,6 +12,7 @@ namespace Penumbra.Mods; public class ModNormalizer { private readonly ModManager _modManager; + private readonly ModCacheManager _modCacheManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -24,8 +25,11 @@ public class ModNormalizer public bool Running => Step < TotalSteps; - public ModNormalizer(ModManager modManager) - => _modManager = modManager; + public ModNormalizer(ModManager modManager, ModCacheManager modCacheManager) + { + _modManager = modManager; + _modCacheManager = modCacheManager; + } public void Normalize(Mod mod) { @@ -36,7 +40,7 @@ public class ModNormalizer _normalizationDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalization"); _oldDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalizationOld"); Step = 0; - TotalSteps = mod.TotalFileCount + 5; + TotalSteps = _modCacheManager[mod].TotalFileCount + 5; Task.Run(NormalizeSync); } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 88cc9e75..8e96b3e2 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -32,7 +32,6 @@ public class ModOptionEditor mod._groups[groupIdx] = group.Convert(type); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); - mod.HasOptions = mod.Groups.Any(o => o.IsOption); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 41da763b..e7f1da0b 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -76,8 +76,6 @@ public partial class Mod IncorporateAllMetaChanges(true); } - ComputeChangedItems(); - SetCounts(); return true; } diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs deleted file mode 100644 index cdffcee9..00000000 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public SortedList< string, object? > ChangedItems { get; } = new(); - public string LowerChangedItemsString { get; private set; } = string.Empty; - - internal void ComputeChangedItems() - { - ChangedItems.Clear(); - foreach( var gamePath in AllRedirects ) - { - Penumbra.Identifier.Identify( ChangedItems, gamePath.ToString() ); - } - - foreach( var manip in AllManipulations ) - { - ComputeChangedItems( ChangedItems, manip ); - } - - LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); - } - - public static void ComputeChangedItems( SortedList< string, object? > changedItems, MetaManipulation manip ) - { - switch( manip.ManipulationType ) - { - case MetaManipulation.Type.Imc: - switch( manip.Imc.ObjectType ) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - Penumbra.Identifier.Identify( changedItems, - GamePaths.Equipment.Mtrl.Path( manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, "a" ) ); - break; - case ObjectType.Weapon: - Penumbra.Identifier.Identify( changedItems, GamePaths.Weapon.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a" ) ); - break; - case ObjectType.DemiHuman: - Penumbra.Identifier.Identify( changedItems, - GamePaths.DemiHuman.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, "a" ) ); - break; - case ObjectType.Monster: - Penumbra.Identifier.Identify( changedItems, GamePaths.Monster.Mtrl.Path( manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a" ) ); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - Penumbra.Identifier.Identify( changedItems, - GamePaths.Equipment.Mdl.Path( manip.Eqdp.SetId, Names.CombinedRace( manip.Eqdp.Gender, manip.Eqdp.Race ), manip.Eqdp.Slot ) ); - break; - case MetaManipulation.Type.Eqp: - Penumbra.Identifier.Identify( changedItems, GamePaths.Equipment.Mdl.Path( manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot ) ); - break; - case MetaManipulation.Type.Est: - switch( manip.Est.Slot ) - { - case EstManipulation.EstType.Hair: - changedItems.TryAdd( $"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null ); - break; - case EstManipulation.EstType.Face: - changedItems.TryAdd( $"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null ); - break; - case EstManipulation.EstType.Body: - Penumbra.Identifier.Identify( changedItems, - GamePaths.Equipment.Mdl.Path( manip.Est.SetId, Names.CombinedRace( manip.Est.Gender, manip.Est.Race ), EquipSlot.Body ) ); - break; - case EstManipulation.EstType.Head: - Penumbra.Identifier.Identify( changedItems, - GamePaths.Equipment.Mdl.Path( manip.Est.SetId, Names.CombinedRace( manip.Est.Gender, manip.Est.Race ), EquipSlot.Head ) ); - break; - } - - break; - case MetaManipulation.Type.Gmp: - Penumbra.Identifier.Identify( changedItems, GamePaths.Equipment.Mdl.Path( manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head ) ); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd( $"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null ); - break; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index bd11d477..078e5a95 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -20,29 +20,6 @@ public partial class Mod internal readonly SubMod _default; internal readonly List _groups = new(); - public int TotalFileCount { get; internal set; } - public int TotalSwapCount { get; internal set; } - public int TotalManipulations { get; internal set; } - public bool HasOptions { get; internal set; } - - internal bool SetCounts() - { - TotalFileCount = 0; - TotalSwapCount = 0; - TotalManipulations = 0; - foreach (var s in AllSubMods) - { - TotalFileCount += s.Files.Count; - TotalSwapCount += s.FileSwaps.Count; - TotalManipulations += s.Manipulations.Count; - } - - HasOptions = _groups.Any(o - => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 - || o is SingleModGroup s && s.OptionData.Count > 1); - return true; - } - public IEnumerable AllSubMods => _groups.SelectMany(o => o).Prepend(_default); diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs index 1c0ae1e0..af79191f 100644 --- a/Penumbra/Mods/Mod.LocalData.cs +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -15,7 +15,6 @@ public sealed partial class Mod public IReadOnlyList LocalTags { get; private set; } = Array.Empty(); - public string AllTagsLower { get; private set; } = string.Empty; public string Note { get; internal set; } = string.Empty; public bool Favorite { get; internal set; } = false; @@ -46,9 +45,6 @@ public sealed partial class Mod } } - if (type != 0) - AllTagsLower = string.Join('\0', ModTags.Concat(LocalTags).Select(s => s.ToLowerInvariant())); - return type; } diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/ModCacheManager.cs index 632c5a31..365ac52e 100644 --- a/Penumbra/Mods/ModCacheManager.cs +++ b/Penumbra/Mods/ModCacheManager.cs @@ -36,7 +36,7 @@ public class ModCacheManager : IDisposable, IReadOnlyList } public IEnumerator GetEnumerator() - => _cache.GetEnumerator(); + => _cache.Take(Count).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 283de2bb..dc7f78a6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -49,6 +49,7 @@ public class Penumbra : IDalamudPlugin public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; + public static ModCacheManager ModCaches { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; @@ -105,7 +106,7 @@ public class Penumbra : IDalamudPlugin RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); ResourceLoader = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); + ModCaches = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { PathResolver = _tmp.Services.GetRequiredService(); @@ -238,13 +239,13 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); sb.AppendLine("**Mods**"); sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n"); - sb.Append($"> **`Mods with Config: `** {ModManager.Count(m => m.HasOptions)}\n"); + sb.Append($"> **`Mods with Config: `** {ModCaches.Count(m => m.HasOptions)}\n"); sb.Append( - $"> **`Mods with File Redirections: `** {ModManager.Count(m => m.TotalFileCount > 0)}, Total: {ModManager.Sum(m => m.TotalFileCount)}\n"); + $"> **`Mods with File Redirections: `** {ModCaches.Count(m => m.TotalFileCount > 0)}, Total: {ModCaches.Sum(m => m.TotalFileCount)}\n"); sb.Append( - $"> **`Mods with FileSwaps: `** {ModManager.Count(m => m.TotalSwapCount > 0)}, Total: {ModManager.Sum(m => m.TotalSwapCount)}\n"); + $"> **`Mods with FileSwaps: `** {ModCaches.Count(m => m.TotalSwapCount > 0)}, Total: {ModCaches.Sum(m => m.TotalSwapCount)}\n"); sb.Append( - $"> **`Mods with Meta Manipulations:`** {ModManager.Count(m => m.TotalManipulations > 0)}, Total {ModManager.Sum(m => m.TotalManipulations)}\n"); + $"> **`Mods with Meta Manipulations:`** {ModCaches.Count(m => m.TotalManipulations > 0)}, Total {ModCaches.Sum(m => m.TotalManipulations)}\n"); sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n"); sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n"); @@ -265,7 +266,7 @@ public class Penumbra : IDalamudPlugin => sb.Append($"**Collection {c.AnonymizedName}**\n" + $"> **`Inheritances: `** {c.Inheritance.Count}\n" + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" - + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? 0 : x.Conflicts.Count)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count)}\n"); + + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index ad0e7fa2..8cc1060b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -82,7 +82,7 @@ public partial class ModEditWindow return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, - _editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + _editor.Option! == s.Item1 && _modCaches[_mod!].HasOptions ? 0x40008000u : 0u)); }); void DrawLine((string, string, string, uint) data) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index bcc8022e..c59d4784 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -24,10 +24,11 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly DataManager _gameData; + private readonly ModEditor _editor; + private readonly ModCacheManager _modCaches; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly DataManager _gameData; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -490,12 +491,13 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches) : base(WindowBaseLabel) { _itemSwapTab = itemSwapTab; _config = config; _editor = editor; + _modCaches = modCaches; _gameData = gameData; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0ea3b734..d03cfab2 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -30,6 +30,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), 1 => !mod.Name.Contains(_modFilter), 2 => !mod.Author.Contains(_modFilter), - 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), - 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), + 3 => !_modCaches[mod].LowerChangedItemsString.Contains(_modFilter.Lower), + 4 => !_modCaches[mod].AllTagsLower.Contains(_modFilter.Lower), _ => false, // Should never happen }; } @@ -639,12 +641,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Label => "Changed Items"u8; - public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector) + public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector, ModCacheManager modCaches) { - _api = api; - _selector = selector; + _api = api; + _selector = selector; + _modCaches = modCaches; } public bool IsVisible - => _selector.Selected!.ChangedItems.Count > 0; + => _modCaches[_selector.Selected!].ChangedItems.Count > 0; public void DrawContent() { @@ -33,7 +35,7 @@ public class ModPanelChangedItemsTab : ITab if (!list) return; - var zipList = ZipList.FromSortedList(_selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList(_modCaches[_selector.Selected!].ChangedItems); var height = ImGui.GetTextLineHeight(); ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_api, kvp.Item1, kvp.Item2, true), height); } From a2fd070c86018b6f5717c4e0ef6e2ad800e78a0c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 02:06:39 +0200 Subject: [PATCH 0849/2451] Fix regex issue --- Penumbra/Import/Structs/MetaFileInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Structs/MetaFileInfo.cs b/Penumbra/Import/Structs/MetaFileInfo.cs index 3af2db34..53349f5a 100644 --- a/Penumbra/Import/Structs/MetaFileInfo.cs +++ b/Penumbra/Import/Structs/MetaFileInfo.cs @@ -12,12 +12,12 @@ public partial struct MetaFileInfo { // These are the valid regexes for .meta files that we are able to support at the moment. [GeneratedRegex(@"bgcommon/hou/(?'Type1'[a-z]*)/general/(?'Id1'\d{4})/\k'Id1'\.meta", - RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] + RegexOptions.Compiled | RegexOptions.ExplicitCapture)] private static partial Regex HousingMeta(); [GeneratedRegex( @"chara/(?'Type1'[a-z]*)/(?'Pre1'[a-z])(?'Id1'\d{4})(/obj/(?'Type2'[a-z]*)/(?'Pre2'[a-z])(?'Id2'\d{4}))?/\k'Pre1'\k'Id1'(\k'Pre2'\k'Id2')?(_(?'Slot'[a-z]{3}))?\\.meta", - RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] + RegexOptions.Compiled | RegexOptions.ExplicitCapture)] private static partial Regex CharaMeta(); public readonly ObjectType PrimaryType; From 49f1f7020fc34584a722102bec0e8b2cd6f8609b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 18:35:45 +0200 Subject: [PATCH 0850/2451] Fix some bugs. --- Penumbra/Import/Structs/MetaFileInfo.cs | 2 +- Penumbra/Mods/Editor/ModFileCollection.cs | 1 + Penumbra/Mods/Editor/ModFileEditor.cs | 9 +++++++-- .../Mods/Manager/{Mod.Manager.cs => ModManager.cs} | 0 Penumbra/Mods/ModCacheManager.cs | 10 +++++----- 5 files changed, 14 insertions(+), 8 deletions(-) rename Penumbra/Mods/Manager/{Mod.Manager.cs => ModManager.cs} (100%) diff --git a/Penumbra/Import/Structs/MetaFileInfo.cs b/Penumbra/Import/Structs/MetaFileInfo.cs index 53349f5a..81b869d9 100644 --- a/Penumbra/Import/Structs/MetaFileInfo.cs +++ b/Penumbra/Import/Structs/MetaFileInfo.cs @@ -16,7 +16,7 @@ public partial struct MetaFileInfo private static partial Regex HousingMeta(); [GeneratedRegex( - @"chara/(?'Type1'[a-z]*)/(?'Pre1'[a-z])(?'Id1'\d{4})(/obj/(?'Type2'[a-z]*)/(?'Pre2'[a-z])(?'Id2'\d{4}))?/\k'Pre1'\k'Id1'(\k'Pre2'\k'Id2')?(_(?'Slot'[a-z]{3}))?\\.meta", + @"chara/(?'Type1'[a-z]*)/(?'Pre1'[a-z])(?'Id1'\d{4})(/obj/(?'Type2'[a-z]*)/(?'Pre2'[a-z])(?'Id2'\d{4}))?/\k'Pre1'\k'Id1'(\k'Pre2'\k'Id2')?(_(?'Slot'[a-z]{3}))?\.meta", RegexOptions.Compiled | RegexOptions.ExplicitCapture)] private static partial Regex CharaMeta(); diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 72eb742b..9905fbcb 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -98,6 +98,7 @@ public class ModFileCollection : IDisposable _usedPaths.Remove(oldPath.Item2); if (!gamePath.IsEmpty) { + file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath); _usedPaths.Add(gamePath); } else diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index d5e5fea6..c9813b33 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -36,7 +36,7 @@ public class ModFileEditor _modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); _files.UpdatePaths(mod, option); - + Changes = false; return num; } @@ -125,10 +125,15 @@ public class ModFileEditor { foreach (var file in files) { - foreach (var (_, path) in file.SubModUsage.Where(p => p.Item1 == option)) + for (var i = 0; i < file.SubModUsage.Count; ++i) { + var (opt, path) = file.SubModUsage[i]; + if (option != opt) + continue; + _files.RemoveUsedPath(option, file, path); Changes = true; + --i; } } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/ModManager.cs similarity index 100% rename from Penumbra/Mods/Manager/Mod.Manager.cs rename to Penumbra/Mods/Manager/ModManager.cs diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/ModCacheManager.cs index 365ac52e..3350119f 100644 --- a/Penumbra/Mods/ModCacheManager.cs +++ b/Penumbra/Mods/ModCacheManager.cs @@ -191,6 +191,7 @@ public class ModCacheManager : IDisposable, IReadOnlyList _cache.AddRange(Enumerable.Range(0, _modManager.Count - _cache.Count).Select(_ => new ModCache())); Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { Refresh(_cache[idx], _modManager[idx]); }); + Count = _modManager.Count; } private void OnIdentifierCreation() @@ -261,11 +262,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList if (mod.Index >= _cache.Count) - _cache.AddRange(Enumerable.Range(0, mod.Index - _cache.Count).Select(_ => new ModCache())); - else if (mod.Index >= Count) - for (var i = Count; i <= mod.Index; ++i) - _cache[i].Reset(); - + _cache.AddRange(Enumerable.Range(0, mod.Index + 1 - _cache.Count).Select(_ => new ModCache())); + for (var i = Count; i < mod.Index; ++i) + Refresh(_cache[i], _modManager[i]); + Count = mod.Index + 1; return _cache[mod.Index]; } } From a1e9c4469763afa7725297b4dcb01bb72c739d6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 18:50:40 +0200 Subject: [PATCH 0851/2451] Test different action. --- .github/workflows/release.yml | 1 + .github/workflows/test_release.yml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 490a171f..89ea2b56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,7 @@ jobs: git fetch origin master git fetch origin test + git branch -f test ${{ github.sha }} git checkout master git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 33afd0ee..6ae471be 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -77,9 +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 branch -f test origin/master + git branch -f test ${{ github.sha }} git checkout test git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true From 113078af9040ae1772ea602b22e430c42b9d395d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Mar 2023 20:44:17 +0200 Subject: [PATCH 0852/2451] Namespace movement. --- Penumbra/Api/IpcTester.cs | 1 + Penumbra/Api/PenumbraApi.cs | 1 + Penumbra/Api/PenumbraIpcProviders.cs | 1 + Penumbra/Collections/CollectionManager.cs | 1 + Penumbra/Collections/ModCollection.Cache.cs | 3 +- Penumbra/CommandHandler.cs | 1 + Penumbra/Import/TexToolsImport.cs | 3 +- Penumbra/Mods/Editor/DuplicateManager.cs | 1 + Penumbra/Mods/Editor/ModBackup.cs | 1 + Penumbra/Mods/Editor/ModMetaEditor.cs | 1 + Penumbra/Mods/Editor/ModNormalizer.cs | 1 + Penumbra/Mods/Editor/ModSwapEditor.cs | 1 + Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 1 + Penumbra/Mods/Manager/ExportManager.cs | 2 +- .../Mods/{ => Manager}/ModCacheManager.cs | 0 Penumbra/Mods/Manager/ModDataChangeType.cs | 21 ------- Penumbra/Mods/Manager/ModDataEditor.cs | 18 ++++++ Penumbra/Mods/Manager/ModOptionChangeType.cs | 54 ------------------ Penumbra/Mods/Manager/ModOptionEditor.cs | 55 ++++++++++++++++++- Penumbra/Mods/Manager/ModStorage.cs | 2 +- Penumbra/Mods/Mod.BasePath.cs | 1 + Penumbra/Mods/ModCache.cs | 22 ++++---- Penumbra/Mods/ModFileSystem.cs | 1 + Penumbra/Mods/Subclasses/ModSettings.cs | 1 + Penumbra/Mods/TemporaryMod.cs | 1 + Penumbra/Penumbra.cs | 3 +- Penumbra/Penumbra.csproj.DotSettings | 5 -- Penumbra/PenumbraNew.cs | 3 +- Penumbra/Services/CommunicatorService.cs | 1 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 1 + .../UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelTabBar.cs | 1 + Penumbra/UI/Tabs/DebugTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 1 + Penumbra/UI/Tabs/SettingsTab.cs | 1 + 40 files changed, 121 insertions(+), 100 deletions(-) rename Penumbra/Mods/{ => Manager}/ModCacheManager.cs (100%) delete mode 100644 Penumbra/Mods/Manager/ModDataChangeType.cs delete mode 100644 Penumbra/Mods/Manager/ModOptionChangeType.cs delete mode 100644 Penumbra/Penumbra.csproj.DotSettings diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index c98ac854..da348667 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,6 +16,7 @@ using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8b6adafc..4e49ab0a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -18,6 +18,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; +using Penumbra.Mods.Manager; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index e6d135bb..0a28f0de 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Mods; +using Penumbra.Mods.Manager; namespace Penumbra.Api; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 0f2694ba..d3628c27 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -10,6 +10,7 @@ using System.Linq; using Penumbra.Api; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 576652ed..7c7fa08a 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -9,7 +9,8 @@ using System.IO; using System.Linq; using Penumbra.Api.Enums; using Penumbra.String.Classes; - +using Penumbra.Mods.Manager; + namespace Penumbra.Collections; public record struct ModPath( IMod Mod, FullPath Path ); diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index bb1ce79a..44415ff9 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -11,6 +11,7 @@ using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Util; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 56b39f6e..a1478e5b 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -8,8 +8,9 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Penumbra.Api; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using FileMode = System.IO.FileMode; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 0472bb09..20b6e019 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 991927f5..72162b95 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Threading.Tasks; +using Penumbra.Mods.Manager; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index b1dced58..dbd42aa3 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Manager; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 604fc9e5..395d71dd 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; using OtterGui; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index e411ad70..58ef10a0 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 964aee70..c6f1b607 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.Mods.Manager; namespace Penumbra.Mods.ItemSwap; diff --git a/Penumbra/Mods/Manager/ExportManager.cs b/Penumbra/Mods/Manager/ExportManager.cs index f4078098..a59315d6 100644 --- a/Penumbra/Mods/Manager/ExportManager.cs +++ b/Penumbra/Mods/Manager/ExportManager.cs @@ -2,7 +2,7 @@ using System; using System.IO; using Penumbra.Services; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public class ExportManager : IDisposable { diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs similarity index 100% rename from Penumbra/Mods/ModCacheManager.cs rename to Penumbra/Mods/Manager/ModCacheManager.cs diff --git a/Penumbra/Mods/Manager/ModDataChangeType.cs b/Penumbra/Mods/Manager/ModDataChangeType.cs deleted file mode 100644 index eccf83cb..00000000 --- a/Penumbra/Mods/Manager/ModDataChangeType.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Penumbra.Mods; - -[Flags] -public enum ModDataChangeType : ushort -{ - None = 0x0000, - Name = 0x0001, - Author = 0x0002, - Description = 0x0004, - Version = 0x0008, - Website = 0x0010, - Deletion = 0x0020, - Migration = 0x0040, - ModTags = 0x0080, - ImportDate = 0x0100, - Favorite = 0x0200, - LocalTags = 0x0400, - Note = 0x0800, -} diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 09a24bba..48f13514 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -9,6 +9,24 @@ using Penumbra.Util; namespace Penumbra.Mods; +[Flags] +public enum ModDataChangeType : ushort +{ + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, +} + public class ModDataEditor { private readonly FilenameService _filenameService; diff --git a/Penumbra/Mods/Manager/ModOptionChangeType.cs b/Penumbra/Mods/Manager/ModOptionChangeType.cs deleted file mode 100644 index 3e6ff5c6..00000000 --- a/Penumbra/Mods/Manager/ModOptionChangeType.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Penumbra.Mods; - -public enum ModOptionChangeType -{ - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionFilesAdded, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, - PrepareChange, - DefaultOptionChanged, -} - -public static class ModOptionChangeTypeExtension -{ - /// - /// Give information for each type of change. - /// If requiresSaving, collections need to be re-saved after this change. - /// If requiresReloading, caches need to be manipulated after this change. - /// If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. - /// Otherwise, caches need to reload the mod itself. - /// - public static void HandlingInfo( this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared ) - { - ( requiresSaving, requiresReloading, wasPrepared ) = type switch - { - ModOptionChangeType.GroupRenamed => ( true, false, false ), - ModOptionChangeType.GroupAdded => ( true, false, false ), - ModOptionChangeType.GroupDeleted => ( true, true, false ), - ModOptionChangeType.GroupMoved => ( true, false, false ), - ModOptionChangeType.GroupTypeChanged => ( true, true, true ), - ModOptionChangeType.PriorityChanged => ( true, true, true ), - ModOptionChangeType.OptionAdded => ( true, true, true ), - ModOptionChangeType.OptionDeleted => ( true, true, false ), - ModOptionChangeType.OptionMoved => ( true, false, false ), - ModOptionChangeType.OptionFilesChanged => ( false, true, false ), - ModOptionChangeType.OptionFilesAdded => ( false, true, true ), - ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), - ModOptionChangeType.OptionMetaChanged => ( false, true, false ), - ModOptionChangeType.DisplayChange => ( false, false, false ), - ModOptionChangeType.DefaultOptionChanged => ( true, false, false ), - _ => ( false, false, false ), - }; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 8e96b3e2..b1029822 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -10,7 +10,27 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, + DefaultOptionChanged, +} public class ModOptionEditor { @@ -380,3 +400,36 @@ public class ModOptionEditor }; } } + +public static class ModOptionChangeTypeExtension +{ + /// + /// Give information for each type of change. + /// If requiresSaving, collections need to be re-saved after this change. + /// If requiresReloading, caches need to be manipulated after this change. + /// If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. + /// Otherwise, caches need to reload the mod itself. + /// + public static void HandlingInfo(this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared) + { + (requiresSaving, requiresReloading, wasPrepared) = type switch + { + ModOptionChangeType.GroupRenamed => (true, false, false), + ModOptionChangeType.GroupAdded => (true, false, false), + ModOptionChangeType.GroupDeleted => (true, true, false), + ModOptionChangeType.GroupMoved => (true, false, false), + ModOptionChangeType.GroupTypeChanged => (true, true, true), + ModOptionChangeType.PriorityChanged => (true, true, true), + ModOptionChangeType.OptionAdded => (true, true, true), + ModOptionChangeType.OptionDeleted => (true, true, false), + ModOptionChangeType.OptionMoved => (true, false, false), + ModOptionChangeType.OptionFilesChanged => (false, true, false), + ModOptionChangeType.OptionFilesAdded => (false, true, true), + ModOptionChangeType.OptionSwapsChanged => (false, true, false), + ModOptionChangeType.OptionMetaChanged => (false, true, false), + ModOptionChangeType.DisplayChange => (false, false, false), + ModOptionChangeType.DefaultOptionChanged => (true, false, false), + _ => (false, false, false), + }; + } +} diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index dbf5c46a..3aa6d31f 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -3,7 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public class ModStorage : IReadOnlyList { diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index e7f1da0b..eb9571c2 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.Mods.Manager; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs index 3fb6d3f0..7602fb95 100644 --- a/Penumbra/Mods/ModCache.cs +++ b/Penumbra/Mods/ModCache.cs @@ -1,26 +1,26 @@ using System.Collections.Generic; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public class ModCache { - public int TotalFileCount; - public int TotalSwapCount; - public int TotalManipulations; + public int TotalFileCount; + public int TotalSwapCount; + public int TotalManipulations; public bool HasOptions; - public SortedList ChangedItems = new(); - public string LowerChangedItemsString = string.Empty; - public string AllTagsLower = string.Empty; + public SortedList ChangedItems = new(); + public string LowerChangedItemsString = string.Empty; + public string AllTagsLower = string.Empty; public void Reset() { - TotalFileCount = 0; - TotalSwapCount = 0; + TotalFileCount = 0; + TotalSwapCount = 0; TotalManipulations = 0; - HasOptions = false; + HasOptions = false; ChangedItems.Clear(); LowerChangedItemsString = string.Empty; - AllTagsLower = string.Empty; + AllTagsLower = string.Empty; } } diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 42ff4381..3ee5fa66 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index a9239078..441ffea9 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index b7c0d6c9..d7265093 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -5,6 +5,7 @@ using System.Linq; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index dc7f78a6..c13ede76 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -30,7 +30,8 @@ using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; using Penumbra.Interop.Services; - +using Penumbra.Mods.Manager; + namespace Penumbra; public class Penumbra : IDalamudPlugin diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings deleted file mode 100644 index d89860c0..00000000 --- a/Penumbra/Penumbra.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 23bfefe5..93abde46 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -20,7 +20,8 @@ using Penumbra.UI.Tabs; using Penumbra.Util; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; - +using Penumbra.Mods.Manager; + namespace Penumbra; public class PenumbraNew diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 55f057a3..fea11316 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -2,6 +2,7 @@ using System; using System.IO; using Penumbra.Collections; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3182dd30..35737920 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -16,6 +16,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index c59d4784..d996be65 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -14,6 +14,7 @@ using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index d03cfab2..bc84f757 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -17,6 +17,7 @@ using Penumbra.Collections; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 2bb78f04..e70ddd57 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -6,7 +6,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Mods; +using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 1728acfa..9cb229d1 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui; using OtterGui.Widgets; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 4b207281..6e9bd34c 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Util; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 26702c5a..ffae30d2 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -12,6 +12,7 @@ using Penumbra.Mods; using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Dalamud.Interface; +using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 77f2b1f2..59dce714 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 09dbc097..f9259861 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.Interop; +using FFXIVClientStructs.Interop; using ImGuiNET; using OtterGui; using OtterGui.Widgets; @@ -18,6 +18,7 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; using Penumbra.Util; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 5c1340a5..37e0c24a 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -12,6 +12,7 @@ using Penumbra.Api.Enums; using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.ModsTab; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a920e723..8d51ec65 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Interop.Services; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; From d4738934f8c364605996ae3bbf49e42b5b7cd63b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Apr 2023 14:22:17 +0200 Subject: [PATCH 0853/2451] Fix Resource Watcher crash --- .../ResourceWatcher/ResourceWatcher.Record.cs | 50 ++++++++++--------- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs index 9b54cb85..e438be27 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs @@ -95,35 +95,37 @@ internal unsafe struct Record var path = handle->FileName().Clone(); return new Record { - Time = DateTime.UtcNow, - Path = path, - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.Destruction, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = OptionalBool.Null, + Time = DateTime.UtcNow, + Path = path, + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.Destruction, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, }; } public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom) => new() { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.FileLoad, - Synchronously = OptionalBool.Null, - ReturnValue = ret, - CustomLoad = custom, + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.FileLoad, + Synchronously = OptionalBool.Null, + ReturnValue = ret, + CustomLoad = custom, + AssociatedGameObject = string.Empty, }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 8b054033..ec0274b6 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -20,7 +20,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI; -public partial class ResourceWatcher : IDisposable, ITab +public class ResourceWatcher : IDisposable, ITab { public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; From c12dbf3f8ab6f070f63c24fd0d481659b6037ba8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Apr 2023 14:23:50 +0200 Subject: [PATCH 0854/2451] Fix early loaded weapons blocking the identified collections cache with unprepared customize arrays. --- .../PathResolving/CollectionResolver.cs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index cb271d7e..65fc0771 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -31,7 +31,7 @@ public unsafe class CollectionResolver private readonly CutsceneService _cutscenes; private readonly Configuration _config; - private readonly CollectionManager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly DrawObjectState _drawObjectState; @@ -65,9 +65,10 @@ public unsafe class CollectionResolver ?? _collectionManager.Default; var player = _actors.AwaitedService.GetCurrentPlayer(); + var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) - ?? CollectionByAttributes(gameObject) + ?? CollectionByAttributes(gameObject, ref _) ?? _collectionManager.Default; } @@ -101,7 +102,7 @@ public unsafe class CollectionResolver /// Identify the correct collection for the last created game object. public ResolveData IdentifyLastGameObjectCollection(bool useCache) - => IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache); + => IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache); /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) @@ -126,7 +127,7 @@ public unsafe class CollectionResolver /// Actors are also not named. So use Yourself > Players > Racial > Default. /// private bool LoginScreen(GameObject* gameObject, out ResolveData ret) - { + { // Also check for empty names because sometimes named other characters // might be loaded before being officially logged in. if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0') @@ -135,10 +136,11 @@ public unsafe class CollectionResolver return false; } - var collection2 = _collectionManager.ByType(CollectionType.Yourself) - ?? CollectionByAttributes(gameObject) + var notYetReady = false; + var collection = _collectionManager.ByType(CollectionType.Yourself) + ?? CollectionByAttributes(gameObject, ref notYetReady) ?? _collectionManager.Default; - ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject); + ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -151,12 +153,13 @@ public unsafe class CollectionResolver return false; } - var player = _actors.AwaitedService.GetCurrentPlayer(); - var collection2 = (player.IsValid ? CollectionByIdentifier(player) : null) + var player = _actors.AwaitedService.GetCurrentPlayer(); + var notYetReady = false; + var collection = (player.IsValid ? CollectionByIdentifier(player) : null) ?? _collectionManager.ByType(CollectionType.Yourself) - ?? CollectionByAttributes(gameObject) + ?? CollectionByAttributes(gameObject, ref notYetReady) ?? _collectionManager.Default; - ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject); + ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -174,13 +177,14 @@ public unsafe class CollectionResolver return _cache.Set(ModCollection.Empty, identifier, gameObject); } + var notYetReady = false; var collection = CollectionByIdentifier(identifier) ?? CheckYourself(identifier, gameObject) - ?? CollectionByAttributes(gameObject) - ?? CheckOwnedCollection(identifier, owner) + ?? CollectionByAttributes(gameObject, ref notYetReady) + ?? CheckOwnedCollection(identifier, owner, ref notYetReady) ?? _collectionManager.Default; - return _cache.Set(collection, identifier, gameObject); + return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject); } /// Check both temporary and permanent character collections. Temporary first. @@ -201,8 +205,8 @@ public unsafe class CollectionResolver return null; } - /// Check special collections given the actor. - private ModCollection? CollectionByAttributes(GameObject* actor) + /// Check special collections given the actor. Returns notYetReady if the customize array is not filled. + private ModCollection? CollectionByAttributes(GameObject* actor, ref bool notYetReady) { if (!actor->IsCharacter()) return null; @@ -212,6 +216,12 @@ public unsafe class CollectionResolver if (!IsModelHuman((uint)character->ModelCharaId)) return null; + if (character->CustomizeData[0] == 0) + { + notYetReady = true; + return null; + } + var bodyType = character->CustomizeData[2]; var collection = bodyType switch { @@ -233,7 +243,7 @@ public unsafe class CollectionResolver } /// Get the collection applying to the owner if it is available. - private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner) + private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady) { if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) return null; @@ -242,7 +252,7 @@ public unsafe class CollectionResolver ObjectKind.None, uint.MaxValue); return CheckYourself(id, owner) - ?? CollectionByAttributes(owner); + ?? CollectionByAttributes(owner, ref notYetReady); } /// From 577669b21f923f89e8a59712b101c8ca209f074c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Apr 2023 14:24:12 +0200 Subject: [PATCH 0855/2451] Some mod movement. --- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImporter.Archives.cs | 8 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 22 +- Penumbra/Mods/Editor/ModFileEditor.cs | 1 + Penumbra/Mods/Editor/ModNormalizer.cs | 4 +- Penumbra/Mods/Manager/ModCacheManager.cs | 32 +- Penumbra/Mods/Manager/ModDataEditor.cs | 62 +-- Penumbra/Mods/{ => Manager}/ModFileSystem.cs | 3 +- Penumbra/Mods/Manager/ModManager.cs | 8 +- Penumbra/Mods/Manager/ModMigration.cs | 244 +++++++++ Penumbra/Mods/Mod.Creator.cs | 511 +++++++++--------- Penumbra/Mods/Mod.Files.cs | 4 +- Penumbra/Mods/Mod.LocalData.cs | 77 --- Penumbra/Mods/Mod.Meta.Migration.cs | 246 --------- Penumbra/Mods/Mod.Meta.cs | 59 -- Penumbra/Mods/Mod.cs | 35 ++ Penumbra/Mods/ModCache.cs | 2 +- Penumbra/Mods/ModLocalData.cs | 67 +++ Penumbra/Mods/ModMeta.cs | 36 ++ Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/Penumbra.cs | 1 - Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 +- .../ModEditWindow.QuickImport.cs | 6 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 4 +- Penumbra/Util/SaveService.cs | 15 +- 26 files changed, 726 insertions(+), 732 deletions(-) rename Penumbra/Mods/{ => Manager}/ModFileSystem.cs (99%) create mode 100644 Penumbra/Mods/Manager/ModMigration.cs delete mode 100644 Penumbra/Mods/Mod.LocalData.cs delete mode 100644 Penumbra/Mods/Mod.Meta.Migration.cs delete mode 100644 Penumbra/Mods/Mod.Meta.cs create mode 100644 Penumbra/Mods/Mod.cs create mode 100644 Penumbra/Mods/ModLocalData.cs create mode 100644 Penumbra/Mods/ModMeta.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 4e49ab0a..56251935 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -828,7 +828,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) + if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; return _tempCollections.CreateTemporaryCollection(name).Length > 0 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index efe21ffe..89b29978 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,6 +11,7 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index c986cc78..6234e9ca 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -45,7 +45,7 @@ public partial class TexToolsImporter }; Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." ); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -100,18 +100,18 @@ public partial class TexToolsImporter // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if( leadDir ) { - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, baseName, false ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, baseName, false ); Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); Directory.Delete( oldName ); } else { - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, name, false ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, name, false ); Directory.Move( oldName, _currentModDirectory.FullName ); } _currentModDirectory.Refresh(); - Mod.Creator.SplitMultiGroups( _currentModDirectory ); + ModCreator.SplitMultiGroups( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index a8cb6608..5c06dcdc 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -33,14 +33,14 @@ public partial class TexToolsImporter var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); // Create a new ModMeta from the TTMP mod list info _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList ); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -89,7 +89,7 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url ); @@ -97,7 +97,7 @@ public partial class TexToolsImporter // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -134,7 +134,7 @@ public partial class TexToolsImporter _currentNumOptions = GetOptionCount( modList ); _currentModName = modList.Name; - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); if( _currentNumOptions == 0 ) @@ -172,7 +172,7 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = Mod.Creator.NewSubFolderName( _currentModDirectory, name ) + var groupFolder = ModCreator.NewSubFolderName( _currentModDirectory, name ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); @@ -182,10 +182,10 @@ public partial class TexToolsImporter var option = allOptions[ i + optionIdx ]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = Mod.Creator.NewSubFolderName( groupFolder, option.Name ) + var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name ) ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( Mod.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); + options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); if( option.IsChecked ) { defaultSettings = group.SelectionType == GroupType.Multi @@ -206,12 +206,12 @@ public partial class TexToolsImporter if( empty != null ) { _currentOptionName = empty.Name; - options.Insert( 0, Mod.Creator.CreateEmptySubMod( empty.Name ) ); + options.Insert( 0, ModCreator.CreateEmptySubMod( empty.Name ) ); defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; } } - Mod.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, defaultSettings ?? 0, group.Description, options ); ++groupPriority; } @@ -219,7 +219,7 @@ public partial class TexToolsImporter } ResetStreamDisposer(); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index c9813b33..9e649fbb 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 395d71dd..d920adc4 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -181,10 +181,10 @@ public class ModNormalizer for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) _redirections[groupIdx + 1].Add(new Dictionary()); - var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); + var groupDir = ModCreator.CreateModFolder(directory, group.Name); foreach (var option in group.OfType()) { - var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name); + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name); newDict = _redirections[groupIdx + 1][option.OptionIdx]; newDict.Clear(); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 3350119f..85637707 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -10,25 +10,25 @@ using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Services; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable, IReadOnlyList { private readonly CommunicatorService _communicator; - private readonly IdentifierService _identifier; - private readonly IReadOnlyList _modManager; + private readonly IdentifierService _identifier; + private readonly IReadOnlyList _modManager; private readonly List _cache = new(); public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager) { _communicator = communicator; - _identifier = identifier; - _modManager = modManager; + _identifier = identifier; + _modManager = modManager; - _communicator.ModOptionChanged.Event += OnModOptionChange; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.ModDataChanged.Event += OnModDataChange; + _communicator.ModOptionChanged.Event += OnModOptionChange; + _communicator.ModPathChanged.Event += OnModPathChange; + _communicator.ModDataChanged.Event += OnModDataChange; _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; if (!identifier.Valid) identifier.FinishedCreation += OnIdentifierCreation; @@ -51,9 +51,9 @@ public class ModCacheManager : IDisposable, IReadOnlyList public void Dispose() { - _communicator.ModOptionChanged.Event -= OnModOptionChange; - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDataChanged.Event -= OnModDataChange; + _communicator.ModOptionChanged.Event -= OnModOptionChange; + _communicator.ModPathChanged.Event -= OnModPathChange; + _communicator.ModDataChanged.Event -= OnModDataChange; _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; } @@ -232,17 +232,17 @@ public class ModCacheManager : IDisposable, IReadOnlyList private static void UpdateCounts(ModCache cache, Mod mod) { - cache.TotalFileCount = mod.Default.Files.Count; - cache.TotalSwapCount = mod.Default.FileSwaps.Count; + cache.TotalFileCount = mod.Default.Files.Count; + cache.TotalSwapCount = mod.Default.FileSwaps.Count; cache.TotalManipulations = mod.Default.Manipulations.Count; - cache.HasOptions = false; + cache.HasOptions = false; foreach (var group in mod.Groups) { cache.HasOptions |= group.IsOption; foreach (var s in group) { - cache.TotalFileCount += s.Files.Count; - cache.TotalSwapCount += s.FileSwaps.Count; + cache.TotalFileCount += s.Files.Count; + cache.TotalSwapCount += s.FileSwaps.Count; cache.TotalManipulations += s.Manipulations.Count; } } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 48f13514..86bd826e 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -7,7 +7,7 @@ using OtterGui.Classes; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; [Flags] public enum ModDataChangeType : ushort @@ -25,27 +25,25 @@ public enum ModDataChangeType : ushort Favorite = 0x0200, LocalTags = 0x0400, Note = 0x0800, -} +} public class ModDataEditor { - private readonly FilenameService _filenameService; private readonly SaveService _saveService; private readonly CommunicatorService _communicatorService; - public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService) + public ModDataEditor(SaveService saveService, CommunicatorService communicatorService) { - _filenameService = filenameService; _saveService = saveService; _communicatorService = communicatorService; } public string MetaFile(Mod mod) - => _filenameService.ModMetaPath(mod); + => _saveService.FileNames.ModMetaPath(mod); public string DataFile(Mod mod) - => _filenameService.LocalDataFile(mod); - + => _saveService.FileNames.LocalDataFile(mod); + /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, string? website) @@ -56,12 +54,12 @@ public class ModDataEditor mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - _saveService.ImmediateSave(new Mod.ModMeta(mod)); + _saveService.ImmediateSave(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) { - var dataFile = _filenameService.LocalDataFile(mod); + var dataFile = _saveService.FileNames.LocalDataFile(mod); var importDate = 0L; var localTags = Enumerable.Empty(); @@ -98,7 +96,7 @@ public class ModDataEditor changes |= ModDataChangeType.ImportDate; } - changes |= mod.UpdateTags(null, localTags); + changes |= ModLocalData.UpdateTags(mod, null, localTags); if (mod.Favorite != favorite) { @@ -113,14 +111,14 @@ public class ModDataEditor } if (save) - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); return changes; } public ModDataChangeType LoadMeta(Mod mod) { - var metaFile = _filenameService.ModMetaPath(mod); + var metaFile = _saveService.FileNames.ModMetaPath(mod); if (!File.Exists(metaFile)) { Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); @@ -137,7 +135,7 @@ public class ModDataEditor var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(Mod.ModMeta.FileVersion)]?.Value() ?? 0; + var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; var importDate = json[nameof(Mod.ImportDate)]?.Value(); var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); @@ -172,14 +170,12 @@ public class ModDataEditor mod.Website = newWebsite; } - if (newFileVersion != Mod.ModMeta.FileVersion) - { - if (Mod.Migration.Migrate(mod, json, ref newFileVersion)) + if (newFileVersion != ModMeta.FileVersion) + if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion)) { changes |= ModDataChangeType.Migration; - _saveService.ImmediateSave(new Mod.ModMeta(mod)); + _saveService.ImmediateSave(new ModMeta(mod)); } - } if (importDate != null && mod.ImportDate != importDate.Value) { @@ -187,7 +183,7 @@ public class ModDataEditor changes |= ModDataChangeType.ImportDate; } - changes |= mod.UpdateTags(modTags, null); + changes |= ModLocalData.UpdateTags(mod, modTags, null); return changes; } @@ -205,7 +201,7 @@ public class ModDataEditor var oldName = mod.Name; mod.Name = newName; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); } @@ -215,7 +211,7 @@ public class ModDataEditor return; mod.Author = newAuthor; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); } @@ -225,7 +221,7 @@ public class ModDataEditor return; mod.Description = newDescription; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); } @@ -235,7 +231,7 @@ public class ModDataEditor return; mod.Version = newVersion; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); } @@ -245,7 +241,7 @@ public class ModDataEditor return; mod.Website = newWebsite; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); } @@ -261,7 +257,7 @@ public class ModDataEditor return; mod.Favorite = state; - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -272,7 +268,7 @@ public class ModDataEditor return; mod.Note = newNote; - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -287,20 +283,20 @@ public class ModDataEditor ModDataChangeType flags = 0; if (tagIdx == which.Count) { - flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null); + flags = ModLocalData.UpdateTags(mod, local ? null : which.Append(newTag), local ? which.Append(newTag) : null); } else { var tmp = which.ToArray(); tmp[tagIdx] = newTag; - flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null); + flags = ModLocalData.UpdateTags(mod, local ? null : tmp, local ? tmp : null); } if (flags.HasFlag(ModDataChangeType.ModTags)) - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); if (flags.HasFlag(ModDataChangeType.LocalTags)) - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); if (flags != 0) _communicatorService.ModDataChanged.Invoke(flags, mod, null); @@ -308,8 +304,8 @@ public class ModDataEditor public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod) { - var oldFile = _filenameService.LocalDataFile(oldMod.Name); - var newFile = _filenameService.LocalDataFile(newMod.Name); + var oldFile = _saveService.FileNames.LocalDataFile(oldMod.Name); + var newFile = _saveService.FileNames.LocalDataFile(newMod.Name); if (!File.Exists(oldFile)) return; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs similarity index 99% rename from Penumbra/Mods/ModFileSystem.cs rename to Penumbra/Mods/Manager/ModFileSystem.cs index 3ee5fa66..7f5d3070 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -5,11 +5,10 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; -using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 050c4ed1..7acb2417 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Penumbra.Services; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; /// Describes the state of a potential move-target for a mod. public enum NewDirectoryState @@ -35,7 +35,7 @@ public sealed class ModManager : ModStorage _config = config; _communicator = communicator; DataEditor = dataEditor; - OptionEditor = optionEditor; + OptionEditor = optionEditor; SetBaseDirectory(config.ModDirectory, true); DiscoverMods(); } @@ -73,7 +73,7 @@ public sealed class ModManager : ModStorage if (this.Any(m => m.ModPath.Name == modFolder.Name)) return; - Mod.Creator.SplitMultiGroups(modFolder); + ModCreator.SplitMultiGroups(modFolder); var mod = Mod.LoadMod(this, modFolder, true); if (mod == null) return; @@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage if (oldName == newName) return NewDirectoryState.Identical; - var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); + var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); if (fixedNewName != newName) return NewDirectoryState.ContainsInvalidSymbols; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs new file mode 100644 index 00000000..196c7ed5 --- /dev/null +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Api.Enums; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods.Manager; + +public static partial class ModMigration +{ + [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + private static partial Regex GroupRegex(); + + [GeneratedRegex("^group_", RegexOptions.Compiled)] + private static partial Regex GroupStartRegex(); + + public static bool Migrate(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + => MigrateV0ToV1(saveService, mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); + + private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) + { + if (fileVersion > 2) + return false; + + // Remove import time. + fileVersion = 3; + return true; + } + + private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) + { + if (fileVersion > 1) + return false; + + if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) + foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) + { + var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); + try + { + if (newName != group.Name) + group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}"); + } + } + + fileVersion = 2; + + return true; + } + + private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + { + if (fileVersion > 0) + return false; + + var swaps = json["FileSwaps"]?.ToObject>() + ?? new Dictionary(); + var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); + var priority = 1; + var seenMetaFiles = new HashSet(); + foreach (var group in groups.Values) + ConvertGroup(mod, group, ref priority, seenMetaFiles); + + foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) + { + if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) + && !mod._default.FileData.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); + } + + mod._default.FileSwapData.Clear(); + mod._default.FileSwapData.EnsureCapacity(swaps.Count); + foreach (var (gamePath, swapPath) in swaps) + mod._default.FileSwapData.Add(gamePath, swapPath); + + mod._default.IncorporateMetaChanges(mod.ModPath, true); + foreach (var (_, index) in mod.Groups.WithIndex()) + saveService.ImmediateSave(new ModSaveGroup(mod, index)); + + // Delete meta files. + foreach (var file in seenMetaFiles.Where(f => f.Exists)) + { + try + { + File.Delete(file.FullName); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}"); + } + } + + // Delete old meta files. + var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json"); + if (File.Exists(oldMetaFile)) + try + { + File.Delete(oldMetaFile); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); + } + + fileVersion = 1; + saveService.ImmediateSave(new ModSaveGroup(mod, -1)); + + return true; + } + + private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) + { + if (group.Options.Count == 0) + return; + + switch (group.SelectionType) + { + case GroupType.Multi: + + var optionPriority = 0; + var newMultiGroup = new MultiModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add(newMultiGroup); + foreach (var option in group.Options) + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); + + break; + case GroupType.Single: + if (group.Options.Count == 1) + { + AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); + return; + } + + var newSingleGroup = new SingleModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add(newSingleGroup); + foreach (var option in group.Options) + newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); + + break; + } + } + + private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) + { + foreach (var (relPath, gamePaths) in option.OptionFiles) + { + var fullPath = new FullPath(basePath, relPath); + foreach (var gamePath in gamePaths) + mod.FileData.TryAdd(gamePath, fullPath); + + if (fullPath.Extension is ".meta" or ".rgsp") + seenMetaFiles.Add(fullPath); + } + } + + private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) + { + var subMod = new SubMod(mod) { Name = option.OptionName }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + subMod.IncorporateMetaChanges(mod.ModPath, false); + return subMod; + } + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter))] + public Dictionary> OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public GroupType SelectionType = GroupType.Single; + + public List Options = new(); + + public OptionGroupV0() + { } + } + + // Not used anymore, but required for migration. + private class SingleOrArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + => objectType == typeof(HashSet); + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + + if (token.Type == JTokenType.Array) + return token.ToObject>() ?? new HashSet(); + + var tmp = token.ToObject(); + return tmp != null + ? new HashSet { tmp } + : new HashSet(); + } + + public override bool CanWrite + => true; + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + writer.WriteStartArray(); + if (value != null) + { + var v = (HashSet)value; + foreach (var val in v) + serializer.Serialize(writer, val?.ToString()); + } + + writer.WriteEndArray(); + } + } +} diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 86baa7d9..97b4fe4a 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -13,273 +13,270 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class Mod +internal static partial class ModCreator { - internal static partial class Creator + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ /// + /// + /// + /// + /// + public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { - /// - /// Create and return a new directory based on the given directory and name, that is
- /// - Not Empty.
- /// - Unique, by appending (digit) for duplicates.
- /// - Containing no symbols invalid for FFXIV or windows paths.
- ///
- /// - /// - /// - /// - /// - public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) + var name = modListName; + if( name.Length == 0 ) { - var name = modListName; - if( name.Length == 0 ) - { - name = "_"; - } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - if( newModFolder.Length == 0 ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - if( create ) - { - Directory.CreateDirectory( newModFolder ); - } - - return new DirectoryInfo( newModFolder ); + name = "_"; } - /// - /// Create the name for a group or option subfolder based on its parent folder and given name. - /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. - /// - public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + var newModFolderBase = NewOptionDirectory( outDirectory, name ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + if( newModFolder.Length == 0 ) { - var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); } - /// Create a file for an option group from given data. - public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) + if( create ) { - switch( type ) - { - case GroupType.Multi: - { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - case GroupType.Single: - { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange( subMods.OfType< SubMod >() ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - } + Directory.CreateDirectory( newModFolder ); } - /// Create the data for a given sub mod from its data and the folder it is based on. - public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) - { - var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) - .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) - .Where( t => t.Item1 ); - - var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; - foreach( var (_, gamePath, file) in list ) - { - mod.FileData.TryAdd( gamePath, file ); - } - - mod.IncorporateMetaChanges( baseFolder, true ); - return mod; - } - - /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - - /// - /// Create the default data file from all unused files that were not handled before - /// and are used in sub mods. - /// - internal static void CreateDefaultFiles( DirectoryInfo directory ) - { - var mod = new Mod( directory ); - mod.Reload( Penumbra.ModManager, false, out _ ); - foreach( var file in mod.FindUnusedFiles() ) - { - if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - mod._default.FileData.TryAdd( gamePath, file ); - } - - mod._default.IncorporateMetaChanges( directory, true ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); - } - - /// Return the name of a new valid directory based on the base directory and the given name. - public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); - - /// Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) - { - switch( s ) - { - case ".": return replacement; - case "..": return replacement + replacement; - } - - StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) - { - if( c.IsInvalidInPath() ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } - - public static void SplitMultiGroups( DirectoryInfo baseDir ) - { - var mod = new Mod( baseDir ); - - var files = mod.GroupFiles.ToList(); - var idx = 0; - var reorder = false; - foreach( var groupFile in files ) - { - ++idx; - try - { - if( reorder ) - { - var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; - Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); - groupFile.MoveTo( newName, false ); - } - } - catch( Exception ex ) - { - throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); - } - - try - { - var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); - if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) - { - continue; - } - - var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; - if( name.Length == 0 ) - { - continue; - } - - - var options = json[ "Options" ]?.Children().ToList(); - if( options == null ) - { - continue; - } - - if( options.Count <= IModGroup.MaxMultiOptions ) - { - continue; - } - - Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); - var clone = json.DeepClone(); - reorder = true; - foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var newOptions = clone[ "Options" ]!.Children().ToList(); - foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var match = DuplicateNumber().Match( name ); - var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; - name = match.Success ? name[ ..4 ] : name; - var oldName = $"{name}, Part {startNumber}"; - var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - var newName = $"{name}, Part {startNumber + 1}"; - var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - json[ nameof( IModGroup.Name ) ] = oldName; - clone[ nameof( IModGroup.Name ) ] = newName; - - clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; - - Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); - using( var oldFile = File.CreateText( oldPath ) ) - { - using var j = new JsonTextWriter( oldFile ) - { - Formatting = Formatting.Indented, - }; - json.WriteTo( j ); - } - - Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); - using( var newFile = File.CreateText( newPath ) ) - { - using var j = new JsonTextWriter( newFile ) - { - Formatting = Formatting.Indented, - }; - clone.WriteTo( j ); - } - - Penumbra.Log.Debug( - $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); - groupFile.Delete(); - } - catch( Exception ex ) - { - throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); - } - } - } - - [GeneratedRegex( @", Part (\d+)$", RegexOptions.NonBacktracking )] - private static partial Regex DuplicateNumber(); + return new DirectoryInfo( newModFolder ); } + + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + { + var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + } + + /// Create a file for an option group from given data. + public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, + int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) + { + switch( type ) + { + case GroupType.Multi: + { + var group = new MultiModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + case GroupType.Single: + { + var group = new SingleModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + } + } + + /// Create the data for a given sub mod from its data and the folder it is based on. + public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = option.Name, + Description = option.Description, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + /// Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod( string name ) + => new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = name, + }; + + /// + /// Create the default data file from all unused files that were not handled before + /// and are used in sub mods. + /// + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod( directory ); + mod.Reload( Penumbra.ModManager, false, out _ ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + mod._default.FileData.TryAdd( gamePath, file ); + } + + mod._default.IncorporateMetaChanges( directory, true ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); + } + + /// Return the name of a new valid directory based on the base directory and the given name. + public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); + + /// Normalize for nicer names, and remove invalid symbols or invalid paths. + public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) + { + switch( s ) + { + case ".": return replacement; + case "..": return replacement + replacement; + } + + StringBuilder sb = new(s.Length); + foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) + { + if( c.IsInvalidInPath() ) + { + sb.Append( replacement ); + } + else + { + sb.Append( c ); + } + } + + return sb.ToString(); + } + + public static void SplitMultiGroups( DirectoryInfo baseDir ) + { + var mod = new Mod( baseDir ); + + var files = mod.GroupFiles.ToList(); + var idx = 0; + var reorder = false; + foreach( var groupFile in files ) + { + ++idx; + try + { + if( reorder ) + { + var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; + Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); + groupFile.MoveTo( newName, false ); + } + } + catch( Exception ex ) + { + throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); + } + + try + { + var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); + if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) + { + continue; + } + + var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; + if( name.Length == 0 ) + { + continue; + } + + + var options = json[ "Options" ]?.Children().ToList(); + if( options == null ) + { + continue; + } + + if( options.Count <= IModGroup.MaxMultiOptions ) + { + continue; + } + + Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); + var clone = json.DeepClone(); + reorder = true; + foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var newOptions = clone[ "Options" ]!.Children().ToList(); + foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var match = DuplicateNumber().Match( name ); + var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; + name = match.Success ? name[ ..4 ] : name; + var oldName = $"{name}, Part {startNumber}"; + var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + var newName = $"{name}, Part {startNumber + 1}"; + var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + json[ nameof( IModGroup.Name ) ] = oldName; + clone[ nameof( IModGroup.Name ) ] = newName; + + clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; + + Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); + using( var oldFile = File.CreateText( oldPath ) ) + { + using var j = new JsonTextWriter( oldFile ) + { + Formatting = Formatting.Indented, + }; + json.WriteTo( j ); + } + + Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); + using( var newFile = File.CreateText( newPath ) ) + { + using var j = new JsonTextWriter( newFile ) + { + Formatting = Formatting.Indented, + }; + clone.WriteTo( j ); + } + + Penumbra.Log.Debug( + $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); + groupFile.Delete(); + } + catch( Exception ex ) + { + throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); + } + } + } + + [GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking )] + private static partial Regex DuplicateNumber(); } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 078e5a95..cd9071bd 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -114,13 +114,13 @@ public partial class Mod _default.WriteTexToolsMeta(ModPath); foreach (var group in Groups) { - var dir = Creator.NewOptionDirectory(ModPath, group.Name); + var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); if (!dir.Exists) dir.Create(); foreach (var option in group.OfType()) { - var optionDir = Creator.NewOptionDirectory(dir, option.Name); + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); if (!optionDir.Exists) optionDir.Create(); diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs deleted file mode 100644 index af79191f..00000000 --- a/Penumbra/Mods/Mod.LocalData.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - - public IReadOnlyList LocalTags { get; private set; } = Array.Empty(); - - public string Note { get; internal set; } = string.Empty; - public bool Favorite { get; internal set; } = false; - - internal ModDataChangeType UpdateTags(IEnumerable? newModTags, IEnumerable? newLocalTags) - { - if (newModTags == null && newLocalTags == null) - return 0; - - ModDataChangeType type = 0; - if (newModTags != null) - { - var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray(); - if (!modTags.SequenceEqual(ModTags)) - { - newLocalTags ??= LocalTags; - ModTags = modTags; - type |= ModDataChangeType.ModTags; - } - } - - if (newLocalTags != null) - { - var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray(); - if (!localTags.SequenceEqual(LocalTags)) - { - LocalTags = localTags; - type |= ModDataChangeType.LocalTags; - } - } - - return type; - } - - internal readonly struct ModData : ISavable - { - public const int FileVersion = 3; - - private readonly Mod _mod; - - public ModData(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.LocalDataFile(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(ImportDate), JToken.FromObject(_mod.ImportDate) }, - { nameof(LocalTags), JToken.FromObject(_mod.LocalTags) }, - { nameof(Note), JToken.FromObject(_mod.Note) }, - { nameof(Favorite), JToken.FromObject(_mod.Favorite) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } -} diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs deleted file mode 100644 index d993cef0..00000000 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Api.Enums; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public static partial class Migration - { - [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] - private static partial Regex GroupRegex(); - - [GeneratedRegex("^group_", RegexOptions.Compiled)] - private static partial Regex GroupStartRegex(); - - public static bool Migrate(Mod mod, JObject json, ref uint fileVersion) - => MigrateV0ToV1(mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); - - private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) - { - if (fileVersion > 2) - return false; - - // Remove import time. - fileVersion = 3; - return true; - } - - private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) - { - if (fileVersion > 1) - return false; - - if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) - foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) - { - var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); - try - { - if (newName != group.Name) - group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}"); - } - } - - fileVersion = 2; - - return true; - } - - private static bool MigrateV0ToV1(Mod mod, JObject json, ref uint fileVersion) - { - if (fileVersion > 0) - return false; - - var swaps = json["FileSwaps"]?.ToObject>() - ?? new Dictionary(); - var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); - var priority = 1; - var seenMetaFiles = new HashSet(); - foreach (var group in groups.Values) - ConvertGroup(mod, group, ref priority, seenMetaFiles); - - foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) - { - if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod._default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); - } - - mod._default.FileSwapData.Clear(); - mod._default.FileSwapData.EnsureCapacity(swaps.Count); - foreach (var (gamePath, swapPath) in swaps) - mod._default.FileSwapData.Add(gamePath, swapPath); - - mod._default.IncorporateMetaChanges(mod.ModPath, true); - foreach (var (_, index) in mod.Groups.WithIndex()) - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index)); - - // Delete meta files. - foreach (var file in seenMetaFiles.Where(f => f.Exists)) - { - try - { - File.Delete(file.FullName); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}"); - } - } - - // Delete old meta files. - var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json"); - if (File.Exists(oldMetaFile)) - try - { - File.Delete(oldMetaFile); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); - } - - fileVersion = 1; - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); - - return true; - } - - private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) - { - if (group.Options.Count == 0) - return; - - switch (group.SelectionType) - { - case GroupType.Multi: - - var optionPriority = 0; - var newMultiGroup = new MultiModGroup() - { - Name = group.GroupName, - Priority = priority++, - Description = string.Empty, - }; - mod._groups.Add(newMultiGroup); - foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); - - break; - case GroupType.Single: - if (group.Options.Count == 1) - { - AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); - return; - } - - var newSingleGroup = new SingleModGroup() - { - Name = group.GroupName, - Priority = priority++, - Description = string.Empty, - }; - mod._groups.Add(newSingleGroup); - foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); - - break; - } - } - - private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) - { - foreach (var (relPath, gamePaths) in option.OptionFiles) - { - var fullPath = new FullPath(basePath, relPath); - foreach (var gamePath in gamePaths) - mod.FileData.TryAdd(gamePath, fullPath); - - if (fullPath.Extension is ".meta" or ".rgsp") - seenMetaFiles.Add(fullPath); - } - } - - private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) - { - var subMod = new SubMod(mod) { Name = option.OptionName }; - AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - subMod.IncorporateMetaChanges(mod.ModPath, false); - return subMod; - } - - private struct OptionV0 - { - public string OptionName = string.Empty; - public string OptionDesc = string.Empty; - - [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter))] - public Dictionary> OptionFiles = new(); - - public OptionV0() - { } - } - - private struct OptionGroupV0 - { - public string GroupName = string.Empty; - - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public GroupType SelectionType = GroupType.Single; - - public List Options = new(); - - public OptionGroupV0() - { } - } - - // Not used anymore, but required for migration. - private class SingleOrArrayConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(HashSet); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var token = JToken.Load(reader); - - if (token.Type == JTokenType.Array) - return token.ToObject>() ?? new HashSet(); - - var tmp = token.ToObject(); - return tmp != null - ? new HashSet { tmp } - : new HashSet(); - } - - public override bool CanWrite - => true; - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteStartArray(); - if (value != null) - { - var v = (HashSet)value; - foreach (var val in v) - serializer.Serialize(writer, val?.ToString()); - } - - writer.WriteEndArray(); - } - } - } -} diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs deleted file mode 100644 index 3b716298..00000000 --- a/Penumbra/Mods/Mod.Meta.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Classes; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class Mod : IMod -{ - public static readonly TemporaryMod ForcedFiles = new() - { - Name = "Forced Files", - Index = -1, - Priority = int.MaxValue, - }; - - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); - - public override string ToString() - => Name.Text; - - internal readonly struct ModMeta : ISavable - { - public const uint FileVersion = 3; - - private readonly Mod _mod; - - public ModMeta(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.ModMetaPath(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Name), JToken.FromObject(_mod.Name) }, - { nameof(Author), JToken.FromObject(_mod.Author) }, - { nameof(Description), JToken.FromObject(_mod.Description) }, - { nameof(Version), JToken.FromObject(_mod.Version) }, - { nameof(Website), JToken.FromObject(_mod.Website) }, - { nameof(ModTags), JToken.FromObject(_mod.ModTags) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } -} diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs new file mode 100644 index 00000000..bc7a2c4d --- /dev/null +++ b/Penumbra/Mods/Mod.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public sealed partial class Mod : IMod +{ + public static readonly TemporaryMod ForcedFiles = new() + { + Name = "Forced Files", + Index = -1, + Priority = int.MaxValue, + }; + + // Meta Data + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); + + + // Local Data + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; internal set; } = Array.Empty(); + public string Note { get; internal set; } = string.Empty; + public bool Favorite { get; internal set; } = false; + + + // Access + public override string ToString() + => Name.Text; +} diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs index 7602fb95..e6848034 100644 --- a/Penumbra/Mods/ModCache.cs +++ b/Penumbra/Mods/ModCache.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods; public class ModCache { diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs new file mode 100644 index 00000000..71aae013 --- /dev/null +++ b/Penumbra/Mods/ModLocalData.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public readonly struct ModLocalData : ISavable +{ + public const int FileVersion = 3; + + private readonly Mod _mod; + + public ModLocalData(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.LocalDataFile(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(FileVersion) }, + { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, + { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, + { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, + { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + + internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable? newModTags, IEnumerable? newLocalTags) + { + if (newModTags == null && newLocalTags == null) + return 0; + + ModDataChangeType type = 0; + if (newModTags != null) + { + var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray(); + if (!modTags.SequenceEqual(mod.ModTags)) + { + newLocalTags ??= mod.LocalTags; + mod.ModTags = modTags; + type |= ModDataChangeType.ModTags; + } + } + + if (newLocalTags != null) + { + var localTags = newLocalTags!.Where(t => t.Length > 0 && !mod.ModTags.Contains(t)).Distinct().ToArray(); + if (!localTags.SequenceEqual(mod.LocalTags)) + { + mod.LocalTags = localTags; + type |= ModDataChangeType.LocalTags; + } + } + + return type; + } +} diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs new file mode 100644 index 00000000..66979345 --- /dev/null +++ b/Penumbra/Mods/ModMeta.cs @@ -0,0 +1,36 @@ +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public readonly struct ModMeta : ISavable +{ + public const uint FileVersion = 3; + + private readonly Mod _mod; + + public ModMeta(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.ModMetaPath(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(FileVersion) }, + { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, + { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, + { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, + { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, + { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, + { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } +} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index d7265093..3f0664cb 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -50,7 +50,7 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + dir = ModCreator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c13ede76..24fcd271 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -24,7 +24,6 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 35737920..17c58dc6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -269,9 +269,9 @@ public class ItemSwapTab : IDisposable, ITab private void CreateMod() { - var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName); + var newDir = ModCreator.CreateModFolder(_modManager.BasePath, _newModName); _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); - Mod.Creator.CreateDefaultFiles(newDir); + ModCreator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); if (!_swapData.WriteMod(_modManager, _modManager[^1], _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) @@ -290,7 +290,7 @@ public class ItemSwapTab : IDisposable, ITab try { optionFolderName = - Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), + ModCreator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), _newOptionName); if (optionFolderName?.Exists == true) throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 2d4415d9..b90d607e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -217,13 +217,13 @@ public partial class ModEditWindow var fullName = subMod.FullName; if (fullName.EndsWith(": " + name)) { - path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); - path = Mod.Creator.NewOptionDirectory(path, name); + path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); + path = ModCreator.NewOptionDirectory(path, name); subDirs = 2; } else { - path = Mod.Creator.NewOptionDirectory(path, fullName); + path = ModCreator.NewOptionDirectory(path, fullName); subDirs = 1; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index bc84f757..72e9a362 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -131,9 +131,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Queue a save for the next framework tick.
public void QueueSave(ISavable value) { - var file = value.ToFilename(_fileNames); + var file = value.ToFilename(FileNames); _framework.RegisterDelayed(value.GetType().Name + file, () => { ImmediateSave(value); @@ -53,7 +54,7 @@ public class SaveService /// Immediately trigger a save. public void ImmediateSave(ISavable value) { - var name = value.ToFilename(_fileNames); + var name = value.ToFilename(FileNames); try { if (name.Length == 0) @@ -76,7 +77,7 @@ public class SaveService public void ImmediateDelete(ISavable value) { - var name = value.ToFilename(_fileNames); + var name = value.ToFilename(FileNames); try { if (name.Length == 0) @@ -99,7 +100,7 @@ public class SaveService /// Immediately delete all existing option group files for a mod and save them anew. public void SaveAllOptionGroups(Mod mod) { - foreach (var file in _fileNames.GetOptionGroupFiles(mod)) + foreach (var file in FileNames.GetOptionGroupFiles(mod)) { try { From e9ab9a71a81654dd19ef85e9621911607143ea8d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Apr 2023 23:01:56 +0200 Subject: [PATCH 0856/2451] Fix Refresh Data and Reload Files. --- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 9e649fbb..cf1ff027 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -43,7 +43,7 @@ public class ModFileEditor public void Revert(Mod mod, ISubMod option) { - _files.UpdatePaths(mod, option); + _files.UpdateAll(mod, option); Changes = false; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d996be65..d4980936 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -369,7 +369,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) - _editor.LoadOption(_editor.GroupIdx, _editor.OptionIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); ImGui.SameLine(); From 5a817db0692e2713f9ae7d573b0c27e15ff22530 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Apr 2023 12:14:43 +0200 Subject: [PATCH 0857/2451] Make Individual Collection lookup thread-safe by locking. --- Penumbra/Api/PenumbraApi.cs | 14 +- Penumbra/Api/TempCollectionManager.cs | 2 +- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/IndividualCollections.cs | 232 ++++++++++-------- 4 files changed, 134 insertions(+), 116 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 56251935..af3fec75 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -459,7 +459,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!id.IsValid) return (false, false, _collectionManager.Default.Name); - if (_collectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) + if (_collectionManager.Individuals.TryGetValue(id, out var collection)) return (true, true, collection.Name); AssociatedCollection(gameObjectIdx, out collection); @@ -474,7 +474,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!id.IsValid) return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name); - var oldCollection = _collectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + var oldCollection = _collectionManager.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; if (collectionName.Length == 0) { @@ -809,8 +809,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!identifier.IsValid) return (PenumbraApiEc.InvalidArgument, string.Empty); - if (!forceOverwriteCharacter && _collectionManager.Individuals.Individuals.ContainsKey(identifier) - || _tempCollections.Collections.Individuals.ContainsKey(identifier)) + if (!forceOverwriteCharacter && _collectionManager.Individuals.ContainsKey(identifier) + || _tempCollections.Collections.ContainsKey(identifier)) return (PenumbraApiEc.CharacterCollectionExists, string.Empty); var name = $"{tag}_{character}"; @@ -855,11 +855,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (forceAssignment) { - if (_tempCollections.Collections.Individuals.ContainsKey(identifier) && !_tempCollections.Collections.Delete(identifier)) + if (_tempCollections.Collections.ContainsKey(identifier) && !_tempCollections.Collections.Delete(identifier)) return PenumbraApiEc.AssignmentDeletionFailed; } - else if (_tempCollections.Collections.Individuals.ContainsKey(identifier) - || _collectionManager.Individuals.Individuals.ContainsKey(identifier)) + else if (_tempCollections.Collections.ContainsKey(identifier) + || _collectionManager.Individuals.ContainsKey(identifier)) { return PenumbraApiEc.CharacterCollectionExists; } diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Api/TempCollectionManager.cs index 7ed67f53..455751c6 100644 --- a/Penumbra/Api/TempCollectionManager.cs +++ b/Penumbra/Api/TempCollectionManager.cs @@ -114,6 +114,6 @@ public class TempCollectionManager : IDisposable return false; var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); - return Collections.Individuals.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); + return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); } } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index f685e886..50bbefea 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -58,7 +58,7 @@ public sealed partial class CollectionManager : ISavable CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, + CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null, _ => null, }; } diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index d52fd69f..e5de838a 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; @@ -11,18 +12,15 @@ namespace Penumbra.Collections; public sealed partial class IndividualCollections { - private readonly ActorManager _actorManager; - private readonly List< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); - private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); + private readonly ActorManager _actorManager; + private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); + private readonly Dictionary _individuals = new(); - public IReadOnlyList< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments + public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments => _assignments; - public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals - => _individuals; - // TODO - public IndividualCollections( ActorService actorManager ) + public IndividualCollections(ActorService actorManager) => _actorManager = actorManager.AwaitedService; public IndividualCollections(ActorManager actorManager) @@ -35,196 +33,216 @@ public sealed partial class IndividualCollections Invalid, } - public AddResult CanAdd( params ActorIdentifier[] identifiers ) + public bool TryGetValue(ActorIdentifier identifier, [NotNullWhen(true)] out ModCollection? collection) { - if( identifiers.Length == 0 ) + lock (_individuals) { - return AddResult.Invalid; + return _individuals.TryGetValue(identifier, out collection); } - - if( identifiers.Any( i => !i.IsValid ) ) - { - return AddResult.Invalid; - } - - if( identifiers.Any( Individuals.ContainsKey ) ) - { - return AddResult.AlreadySet; - } - - return AddResult.Valid; } - public AddResult CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) + public bool ContainsKey(ActorIdentifier identifier) { - identifiers = Array.Empty< ActorIdentifier >(); + lock (_individuals) + { + return _individuals.ContainsKey(identifier); + } + } - switch( type ) + public AddResult CanAdd(params ActorIdentifier[] identifiers) + { + if (identifiers.Length == 0) + return AddResult.Invalid; + + if (identifiers.Any(i => !i.IsValid)) + return AddResult.Invalid; + + bool set; + lock (_individuals) + { + set = identifiers.Any(_individuals.ContainsKey); + } + + return set ? AddResult.AlreadySet : AddResult.Valid; + } + + public AddResult CanAdd(IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable dataIds, + out ActorIdentifier[] identifiers) + { + identifiers = Array.Empty(); + + switch (type) { case IdentifierType.Player: - if( !ByteString.FromString( name, out var playerName ) ) - { + if (!ByteString.FromString(name, out var playerName)) return AddResult.Invalid; - } - identifiers = new[] { _actorManager.CreatePlayer( playerName, homeWorld ) }; + identifiers = new[] + { + _actorManager.CreatePlayer(playerName, homeWorld), + }; break; case IdentifierType.Retainer: - if( !ByteString.FromString( name, out var retainerName ) ) - { + if (!ByteString.FromString(name, out var retainerName)) return AddResult.Invalid; - } - identifiers = new[] { _actorManager.CreateRetainer( retainerName, 0 ) }; + identifiers = new[] + { + _actorManager.CreateRetainer(retainerName, 0), + }; break; case IdentifierType.Owned: - if( !ByteString.FromString( name, out var ownerName ) ) - { + if (!ByteString.FromString(name, out var ownerName)) return AddResult.Invalid; - } - identifiers = dataIds.Select( id => _actorManager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); + identifiers = dataIds.Select(id => _actorManager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); break; case IdentifierType.Npc: - identifiers = dataIds.Select( id => _actorManager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray(); + identifiers = dataIds + .Select(id => _actorManager.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray(); break; default: - identifiers = Array.Empty< ActorIdentifier >(); + identifiers = Array.Empty(); break; } - return CanAdd( identifiers ); + return CanAdd(identifiers); } - public ActorIdentifier[] GetGroup( ActorIdentifier identifier ) + public ActorIdentifier[] GetGroup(ActorIdentifier identifier) { - if( !identifier.IsValid ) - { - return Array.Empty< ActorIdentifier >(); - } + if (!identifier.IsValid) + return Array.Empty(); - static ActorIdentifier[] CreateNpcs( ActorManager manager, ActorIdentifier identifier ) + static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) { - var name = manager.Data.ToName( identifier.Kind, identifier.DataId ); + var name = manager.Data.ToName(identifier.Kind, identifier.DataId); var table = identifier.Kind switch { ObjectKind.BattleNpc => manager.Data.BNpcs, ObjectKind.EventNpc => manager.Data.ENpcs, ObjectKind.Companion => manager.Data.Companions, ObjectKind.MountType => manager.Data.Mounts, - ( ObjectKind )15 => manager.Data.Ornaments, + (ObjectKind)15 => manager.Data.Ornaments, _ => throw new NotImplementedException(), }; - return table.Where( kvp => kvp.Value == name ) - .Select( kvp => manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); + return table.Where(kvp => kvp.Value == name) + .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, + kvp.Key)).ToArray(); } return identifier.Type switch { - IdentifierType.Player => new[] { identifier.CreatePermanent() }, - IdentifierType.Special => new[] { identifier }, - IdentifierType.Retainer => new[] { identifier.CreatePermanent() }, - IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), - IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), - _ => Array.Empty< ActorIdentifier >(), + IdentifierType.Player => new[] + { + identifier.CreatePermanent(), + }, + IdentifierType.Special => new[] + { + identifier, + }, + IdentifierType.Retainer => new[] + { + identifier.CreatePermanent(), + }, + IdentifierType.Owned => CreateNpcs(_actorManager, identifier.CreatePermanent()), + IdentifierType.Npc => CreateNpcs(_actorManager, identifier), + _ => Array.Empty(), }; } - internal bool Add( ActorIdentifier[] identifiers, ModCollection collection ) + internal bool Add(ActorIdentifier[] identifiers, ModCollection collection) { - if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid ) - { + if (identifiers.Length == 0 || !identifiers[0].IsValid) return false; - } - var name = DisplayString( identifiers[ 0 ] ); - return Add( name, identifiers, collection ); + var name = DisplayString(identifiers[0]); + return Add(name, identifiers, collection); } - private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + private bool Add(string displayName, ActorIdentifier[] identifiers, ModCollection collection) { - if( CanAdd( identifiers ) != AddResult.Valid - || displayName.Length == 0 - || _assignments.Any( a => a.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) ) - { + if (CanAdd(identifiers) != AddResult.Valid + || displayName.Length == 0 + || _assignments.Any(a => a.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase))) return false; - } - for( var i = 0; i < identifiers.Length; ++i ) + for (var i = 0; i < identifiers.Length; ++i) { - identifiers[ i ] = identifiers[ i ].CreatePermanent(); - _individuals.Add( identifiers[ i ], collection ); + identifiers[i] = identifiers[i].CreatePermanent(); + lock (_individuals) + { + _individuals.Add(identifiers[i], collection); + } } - _assignments.Add( ( displayName, identifiers, collection ) ); + _assignments.Add((displayName, identifiers, collection)); return true; } - internal bool ChangeCollection( ActorIdentifier identifier, ModCollection newCollection ) - => ChangeCollection( DisplayString( identifier ), newCollection ); + internal bool ChangeCollection(ActorIdentifier identifier, ModCollection newCollection) + => ChangeCollection(DisplayString(identifier), newCollection); - internal bool ChangeCollection( string displayName, ModCollection newCollection ) - => ChangeCollection( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ), newCollection ); + internal bool ChangeCollection(string displayName, ModCollection newCollection) + => ChangeCollection(_assignments.FindIndex(t => t.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase)), newCollection); - internal bool ChangeCollection( int displayIndex, ModCollection newCollection ) + internal bool ChangeCollection(int displayIndex, ModCollection newCollection) { - if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments[ displayIndex ].Collection == newCollection ) - { + if (displayIndex < 0 || displayIndex >= _assignments.Count || _assignments[displayIndex].Collection == newCollection) return false; - } - _assignments[ displayIndex ] = _assignments[ displayIndex ] with { Collection = newCollection }; - foreach( var identifier in _assignments[ displayIndex ].Identifiers ) + _assignments[displayIndex] = _assignments[displayIndex] with { Collection = newCollection }; + lock (_individuals) { - _individuals[ identifier ] = newCollection; + foreach (var identifier in _assignments[displayIndex].Identifiers) + _individuals[identifier] = newCollection; } return true; } - internal bool Delete( ActorIdentifier identifier ) - => Delete( Index( identifier ) ); + internal bool Delete(ActorIdentifier identifier) + => Delete(Index(identifier)); - internal bool Delete( string displayName ) - => Delete( Index( displayName ) ); + internal bool Delete(string displayName) + => Delete(Index(displayName)); - internal bool Delete( int displayIndex ) + internal bool Delete(int displayIndex) { - if( displayIndex < 0 || displayIndex >= _assignments.Count ) - { + if (displayIndex < 0 || displayIndex >= _assignments.Count) return false; - } - var (name, identifiers, _) = _assignments[ displayIndex ]; - _assignments.RemoveAt( displayIndex ); - foreach( var identifier in identifiers ) + var (name, identifiers, _) = _assignments[displayIndex]; + _assignments.RemoveAt(displayIndex); + lock (_individuals) { - _individuals.Remove( identifier ); + foreach (var identifier in identifiers) + _individuals.Remove(identifier); } return true; } - internal bool Move( int from, int to ) - => _assignments.Move( from, to ); + internal bool Move(int from, int to) + => _assignments.Move(from, to); - internal int Index( string displayName ) - => _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ); + internal int Index(string displayName) + => _assignments.FindIndex(t => t.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase)); - internal int Index( ActorIdentifier identifier ) - => identifier.IsValid ? Index( DisplayString( identifier ) ) : -1; + internal int Index(ActorIdentifier identifier) + => identifier.IsValid ? Index(DisplayString(identifier)) : -1; - private string DisplayString( ActorIdentifier identifier ) + private string DisplayString(ActorIdentifier identifier) { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => - $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})'s {_actorManager.Data.ToName( identifier.Kind, identifier.DataId )}", - IdentifierType.Npc => $"{_actorManager.Data.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", + $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})'s {_actorManager.Data.ToName(identifier.Kind, identifier.DataId)}", + IdentifierType.Npc => $"{_actorManager.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", _ => string.Empty, }; } -} \ No newline at end of file +} From f85fc46fb797c5f0317e5762e39020cc63f9acfd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Apr 2023 15:47:33 +0200 Subject: [PATCH 0858/2451] Now that's a collection manager. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 6 +- Penumbra/Api/PenumbraApi.cs | 137 ++-- Penumbra/Api/TempModManager.cs | 7 +- .../Collections/CollectionManager.Active.cs | 419 ------------- Penumbra/Collections/CollectionManager.cs | 480 -------------- Penumbra/Collections/CollectionType.cs | 584 ------------------ .../IndividualCollections.Files.cs | 149 ----- .../Manager/ActiveCollectionMigration.cs | 67 ++ .../Collections/Manager/ActiveCollections.cs | 469 ++++++++++++++ .../Manager/CollectionCacheManager.cs | 173 ++++++ .../Collections/Manager/CollectionManager.cs | 20 + .../Collections/Manager/CollectionStorage.cs | 306 +++++++++ .../Collections/Manager/CollectionType.cs | 583 +++++++++++++++++ .../IndividualCollections.Access.cs | 2 +- .../Manager/IndividualCollections.Files.cs | 140 +++++ .../{ => Manager}/IndividualCollections.cs | 4 +- .../Collections/Manager/InheritanceManager.cs | 68 ++ .../Manager}/TempCollectionManager.cs | 16 +- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 10 +- Penumbra/Collections/ModCollection.cs | 7 +- Penumbra/Collections/ResolveData.cs | 15 +- Penumbra/CommandHandler.cs | 34 +- .../PathResolving/CollectionResolver.cs | 36 +- .../Interop/PathResolving/DrawObjectState.cs | 4 - .../IdentifiedCollectionCache.cs | 5 +- .../Interop/PathResolving/PathResolver.cs | 14 +- .../Interop/Services/CharacterUtility.List.cs | 2 +- Penumbra/Meta/Manager/MetaManager.cs | 2 +- Penumbra/Mods/Manager/ExportManager.cs | 4 +- Penumbra/Mods/Manager/ModCacheManager.cs | 16 +- Penumbra/Mods/Manager/ModFileSystem.cs | 17 +- Penumbra/Penumbra.cs | 24 +- Penumbra/PenumbraNew.cs | 6 +- Penumbra/Services/CommunicatorService.cs | 258 +++++--- Penumbra/Services/ConfigMigrationService.cs | 17 +- Penumbra/Services/FilenameService.cs | 6 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 25 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- .../Collections.CollectionSelector.cs | 9 +- .../Collections.IndividualCollectionUi.cs | 33 +- .../Collections.InheritanceUi.cs | 31 +- .../CollectionTab/Collections.SpecialCombo.cs | 3 +- Penumbra/UI/FileDialogService.cs | 4 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 51 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 8 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 21 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 6 +- Penumbra/UI/Tabs/CollectionsTab.cs | 35 +- Penumbra/UI/Tabs/DebugTab.cs | 19 +- Penumbra/UI/Tabs/EffectiveTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 23 +- Penumbra/UI/TutorialService.cs | 4 +- Penumbra/Util/EventWrapper.cs | 360 +++-------- 55 files changed, 2433 insertions(+), 2317 deletions(-) delete mode 100644 Penumbra/Collections/CollectionManager.Active.cs delete mode 100644 Penumbra/Collections/CollectionManager.cs delete mode 100644 Penumbra/Collections/CollectionType.cs delete mode 100644 Penumbra/Collections/IndividualCollections.Files.cs create mode 100644 Penumbra/Collections/Manager/ActiveCollectionMigration.cs create mode 100644 Penumbra/Collections/Manager/ActiveCollections.cs create mode 100644 Penumbra/Collections/Manager/CollectionCacheManager.cs create mode 100644 Penumbra/Collections/Manager/CollectionManager.cs create mode 100644 Penumbra/Collections/Manager/CollectionStorage.cs create mode 100644 Penumbra/Collections/Manager/CollectionType.cs rename Penumbra/Collections/{ => Manager}/IndividualCollections.Access.cs (99%) create mode 100644 Penumbra/Collections/Manager/IndividualCollections.Files.cs rename Penumbra/Collections/{ => Manager}/IndividualCollections.cs (99%) create mode 100644 Penumbra/Collections/Manager/InheritanceManager.cs rename Penumbra/{Api => Collections/Manager}/TempCollectionManager.cs (86%) diff --git a/Penumbra.Api b/Penumbra.Api index d87dfa44..6c6533ac 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit d87dfa44ff6efcf4fe576d8a877c78f4ac0dc893 +Subproject commit 6c6533ac60ee6e5e401bb9a65b31ad843d1757cd diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index da348667..da216702 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -12,14 +12,14 @@ using System.Numerics; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; -using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; - +using Penumbra.Collections.Manager; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -1213,7 +1213,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...", - !Penumbra.CollectionManager.ByName(_tempModName, out var copyCollection)) + !Penumbra.CollectionManager.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index af3fec75..5e4c44a7 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -22,6 +22,7 @@ using Penumbra.Mods.Manager; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; +using Penumbra.Collections.Manager; namespace Penumbra.Api; @@ -59,7 +60,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Event += new Action(value); + _communicator.CreatingCharacterBase.Subscribe(new Action(value)); } remove { @@ -67,7 +68,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Event -= new Action(value); + _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); } } @@ -79,7 +80,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Event += new Action(value); + _communicator.CreatedCharacterBase.Subscribe(new Action(value)); } remove { @@ -87,7 +88,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Event -= new Action(value); + _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); } } @@ -129,12 +130,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) ?.GetValue(_dalamud.GameData); - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) SubscribeToCollection(collection); - _communicator.CollectionChange.Event += SubscribeToNewCollections; - _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Event += ModPathChangeSubscriber; + _communicator.CollectionChange.Subscribe(SubscribeToNewCollections); + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); } public unsafe void Dispose() @@ -142,28 +143,28 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Valid) return; - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) { if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; } - _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.CollectionChange.Event -= SubscribeToNewCollections; - _communicator.ModPathChanged.Event -= ModPathChangeSubscriber; - _lumina = null; - _communicator = null!; - _penumbra = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _dalamud = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CollectionChange.Unsubscribe(SubscribeToNewCollections); + _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); + _lumina = null; + _communicator = null!; + _penumbra = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; + _collectionResolver = null!; + _cutsceneService = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -187,12 +188,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _communicator.ModDirectoryChanged.Event += value; + _communicator.ModDirectoryChanged.Subscribe(value!); } remove { CheckInitialized(); - _communicator.ModDirectoryChanged.Event -= value; + _communicator.ModDirectoryChanged.Unsubscribe(value!); } } @@ -283,13 +284,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Default); + return ResolvePath(path, _modManager, _collectionManager.Active.Default); } public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Interface); + return ResolvePath(path, _modManager, _collectionManager.Active.Interface); } public string ResolvePlayerPath(string path) @@ -313,7 +314,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return ResolvePath(path, _modManager, - _collectionManager.Individual(NameToIdentifier(characterName, worldId))); + _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level @@ -329,7 +330,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi path, }; - var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); } @@ -386,7 +387,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) collection = ModCollection.Empty; if (collection.HasCache) @@ -408,7 +409,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return string.Empty; - var collection = _collectionManager.ByType((CollectionType)type); + var collection = _collectionManager.Active.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } @@ -419,7 +420,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return (PenumbraApiEc.InvalidArgument, string.Empty); - var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + var oldCollection = _collectionManager.Active.ByType((CollectionType)type)?.Name ?? string.Empty; if (collectionName.Length == 0) { @@ -429,11 +430,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - _collectionManager.RemoveSpecialCollection((CollectionType)type); + _collectionManager.Active.RemoveSpecialCollection((CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -441,14 +442,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - _collectionManager.CreateSpecialCollection((CollectionType)type); + _collectionManager.Active.CreateSpecialCollection((CollectionType)type); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - _collectionManager.SetCollection(collection, (CollectionType)type); + _collectionManager.Active.SetCollection(collection, (CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } @@ -457,9 +458,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, _collectionManager.Default.Name); + return (false, false, _collectionManager.Active.Default.Name); - if (_collectionManager.Individuals.TryGetValue(id, out var collection)) + if (_collectionManager.Active.Individuals.TryGetValue(id, out var collection)) return (true, true, collection.Name); AssociatedCollection(gameObjectIdx, out collection); @@ -472,9 +473,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name); + return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Active.Default.Name); - var oldCollection = _collectionManager.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + var oldCollection = _collectionManager.Active.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; if (collectionName.Length == 0) { @@ -484,12 +485,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - var idx = _collectionManager.Individuals.Index(id); - _collectionManager.RemoveIndividualCollection(idx); + var idx = _collectionManager.Active.Individuals.Index(id); + _collectionManager.Active.RemoveIndividualCollection(idx); return (PenumbraApiEc.Success, oldCollection); } - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -497,40 +498,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - var ids = _collectionManager.Individuals.GetGroup(id); - _collectionManager.CreateIndividualCollection(ids); + var ids = _collectionManager.Active.Individuals.GetGroup(id); + _collectionManager.Active.CreateIndividualCollection(ids); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - _collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id)); + _collectionManager.Active.SetCollection(collection, CollectionType.Individual, _collectionManager.Active.Individuals.Index(id)); return (PenumbraApiEc.Success, oldCollection); } public IList GetCollections() { CheckInitialized(); - return _collectionManager.Select(c => c.Name).ToArray(); + return _collectionManager.Storage.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() { CheckInitialized(); - return _collectionManager.Current.Name; + return _collectionManager.Active.Current.Name; } public string GetDefaultCollection() { CheckInitialized(); - return _collectionManager.Default.Name; + return _collectionManager.Active.Default.Name; } public string GetInterfaceCollection() { CheckInitialized(); - return _collectionManager.Interface.Name; + return _collectionManager.Active.Interface.Name; } // TODO: cleanup when incrementing API level @@ -540,9 +541,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + return _collectionManager.Active.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) ? (collection.Name, true) - : (_collectionManager.Default.Name, false); + : (_collectionManager.Active.Default.Name, false); } public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) @@ -576,7 +577,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, null); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -679,7 +680,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -692,7 +693,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -704,7 +705,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -717,7 +718,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi string optionName) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -740,7 +741,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi IReadOnlyList optionNames) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -788,9 +789,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); - else if (_collectionManager.ByName(collectionName, out var collection)) + else if (_collectionManager.Storage.ByName(collectionName, out var collection)) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else return PenumbraApiEc.CollectionMissing; @@ -809,7 +810,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!identifier.IsValid) return (PenumbraApiEc.InvalidArgument, string.Empty); - if (!forceOverwriteCharacter && _collectionManager.Individuals.ContainsKey(identifier) + if (!forceOverwriteCharacter && _collectionManager.Active.Individuals.ContainsKey(identifier) || _tempCollections.Collections.ContainsKey(identifier)) return (PenumbraApiEc.CharacterCollectionExists, string.Empty); @@ -859,7 +860,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.AssignmentDeletionFailed; } else if (_tempCollections.Collections.ContainsKey(identifier) - || _collectionManager.Individuals.ContainsKey(identifier)) + || _collectionManager.Active.Individuals.ContainsKey(identifier)) { return PenumbraApiEc.CharacterCollectionExists; } @@ -907,7 +908,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.ByName(collectionName, out collection)) + && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; if (!ConvertPaths(paths, out var p)) @@ -938,7 +939,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.ByName(collectionName, out collection)) + && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; return _tempMods.Unregister(tag, collection, priority) switch @@ -967,7 +968,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var identifier = NameToIdentifier(characterName, worldId); var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : _collectionManager.Individual(identifier); + : _collectionManager.Active.Individual(identifier); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1002,7 +1003,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { - collection = _collectionManager.Default; + collection = _collectionManager.Active.Default; if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) return false; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 07e65c36..747d49cd 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -5,7 +5,8 @@ using Penumbra.Mods; using System.Collections.Generic; using Penumbra.Services; using Penumbra.String.Classes; - +using Penumbra.Collections.Manager; + namespace Penumbra.Api; public enum RedirectResult @@ -26,12 +27,12 @@ public class TempModManager : IDisposable public TempModManager(CommunicatorService communicator) { _communicator = communicator; - _communicator.CollectionChange.Event += OnCollectionChange; + _communicator.CollectionChange.Subscribe(OnCollectionChange); } public void Dispose() { - _communicator.CollectionChange.Event -= OnCollectionChange; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); } public IReadOnlyDictionary> Mods diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs deleted file mode 100644 index 50bbefea..00000000 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ /dev/null @@ -1,419 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Mods; -using Penumbra.UI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal.Notifications; -using Penumbra.GameData.Actors; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public sealed partial class CollectionManager : ISavable -{ - public const int Version = 1; - - // The collection currently selected for changing settings. - public ModCollection Current { get; private set; } = ModCollection.Empty; - - // The collection currently selected is in use either as an active collection or through inheritance. - public bool CurrentCollectionInUse { get; private set; } - - // The collection used for general file redirections and all characters not specifically named. - public ModCollection Default { get; private set; } = ModCollection.Empty; - - // The collection used for all files categorized as UI files. - public ModCollection Interface { get; private set; } = ModCollection.Empty; - - // A single collection that can not be deleted as a fallback for the current collection. - private ModCollection DefaultName { get; set; } = ModCollection.Empty; - - // The list of character collections. - public readonly IndividualCollections Individuals; - - public ModCollection Individual(ActorIdentifier identifier) - => Individuals.TryGetCollection(identifier, out var c) ? c : Default; - - // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; - - // Return the configured collection for the given type or null. - // Does not handle Inactive, use ByName instead. - public ModCollection? ByType(CollectionType type) - => ByType(type, ActorIdentifier.Invalid); - - public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) - { - if (type.IsSpecial()) - return _specialCollections[(int)type]; - - return type switch - { - CollectionType.Default => Default, - CollectionType.Interface => Interface, - CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null, - _ => null, - }; - } - - // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) - { - var oldCollectionIdx = collectionType switch - { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count - ? -1 - : Individuals[individualIndex].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, - _ => -1, - }; - - if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) - return; - - var newCollection = this[newIdx]; - if (newIdx > ModCollection.Empty.Index) - newCollection.CreateCache(collectionType is CollectionType.Default); - - switch (collectionType) - { - case CollectionType.Default: - Default = newCollection; - break; - case CollectionType.Interface: - Interface = newCollection; - break; - case CollectionType.Current: - Current = newCollection; - break; - case CollectionType.Individual: - if (!Individuals.ChangeCollection(individualIndex, newCollection)) - { - RemoveCache(newIdx); - return; - } - - break; - default: - _specialCollections[(int)collectionType] = newCollection; - break; - } - - RemoveCache(oldCollectionIdx); - - UpdateCurrentCollectionInUse(); - _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, - collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); - } - - private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = _specialCollections - .OfType() - .Prepend(Interface) - .Prepend(Default) - .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - - public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) - => SetCollection(collection.Index, collectionType, individualIndex); - - // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) - return false; - - _specialCollections[(int)collectionType] = Default; - _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); - return true; - } - - // Remove a special collection if it exists - public void RemoveSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial()) - return; - - var old = _specialCollections[(int)collectionType]; - if (old != null) - { - _specialCollections[(int)collectionType] = null; - _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); - } - } - - // Wrappers around Individual Collection handling. - public void CreateIndividualCollection(params ActorIdentifier[] identifiers) - { - if (Individuals.Add(identifiers, Default)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); - } - - public void RemoveIndividualCollection(int individualIndex) - { - if (individualIndex < 0 || individualIndex >= Individuals.Count) - return; - - var (name, old) = Individuals[individualIndex]; - if (Individuals.Delete(individualIndex)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); - } - - public void MoveIndividualCollection(int from, int to) - { - if (Individuals.Move(from, to)) - Penumbra.SaveService.QueueSave(this); - } - - // Obtain the index of a collection by name. - private int GetIndexForCollectionName(string name) - => name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name); - - // Load default, current, special, and character collections from config. - // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections(FilenameService files) - { - var configChanged = !ReadActiveCollections(files, out var jObject); - - // Load the default collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name); - var defaultIdx = GetIndexForCollectionName(defaultName); - if (defaultIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", - NotificationType.Warning); - Default = ModCollection.Empty; - configChanged = true; - } - else - { - Default = this[defaultIdx]; - } - - // Load the interface collection. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName(interfaceName); - if (interfaceIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", - "Load Failure", NotificationType.Warning); - Interface = ModCollection.Empty; - configChanged = true; - } - else - { - Interface = this[interfaceIdx]; - } - - // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? ModCollection.DefaultCollection; - var currentIdx = GetIndexForCollectionName(currentName); - if (currentIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.", - "Load Failure", NotificationType.Warning); - Current = DefaultName; - configChanged = true; - } - else - { - Current = this[currentIdx]; - } - - // Load special collections. - foreach (var (type, name, _) in CollectionTypeExtensions.Special) - { - var typeName = jObject[type.ToString()]?.ToObject(); - if (typeName != null) - { - var idx = GetIndexForCollectionName(typeName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", - "Load Failure", - NotificationType.Warning); - configChanged = true; - } - else - { - _specialCollections[(int)type] = this[idx]; - } - } - } - - configChanged |= MigrateIndividualCollections(jObject); - configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); - - // Save any changes and create all required caches. - if (configChanged) - Penumbra.SaveService.ImmediateSave(this); - } - - // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections(FilenameService fileNames) - { - if (!ReadActiveCollections(fileNames, out var jObject)) - return; - - foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) - { - var oldName = type.ToString()[4..]; - var value = jObject[oldName]; - if (value == null) - continue; - - jObject.Remove(oldName); - jObject.Add("Male" + oldName, value); - jObject.Add("Female" + oldName, value); - } - - using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - jObject.WriteTo(j); - } - - // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject) - { - var version = jObject[nameof(Version)]?.Value() ?? 0; - if (version > 0) - return false; - - // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary(characters.Count); - foreach (var (player, collectionName) in characters) - { - var idx = GetIndexForCollectionName(collectionName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", - NotificationType.Warning); - dict.Add(player, ModCollection.Empty); - } - else - { - dict.Add(player, this[idx]); - } - } - - Individuals.Migrate0To1(dict); - return true; - } - - // Read the active collection file into a jObject. - // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(FilenameService files, out JObject ret) - { - var file = files.ActiveCollectionsFile; - if (File.Exists(file)) - try - { - ret = JObject.Parse(File.ReadAllText(file)); - return true; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); - } - - ret = new JObject(); - return false; - } - - // Save if any of the active collections is changed. - private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) - { - if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - Penumbra.SaveService.QueueSave(this); - } - - // Cache handling. Usually recreate caches on the next framework tick, - // but at launch create all of them at once. - public void CreateNecessaryCaches() - { - var tasks = _specialCollections.OfType() - .Concat(Individuals.Select(p => p.Collection)) - .Prepend(Current) - .Prepend(Default) - .Prepend(Interface) - .Distinct() - .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) - .ToArray(); - - Task.WaitAll(tasks); - } - - private void RemoveCache(int idx) - { - if (idx != ModCollection.Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All(c => c == null || c.Index != idx) - && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) - _collections[idx].ClearCache(); - } - - // Recalculate effective files for active collections on events. - private void OnModAddedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.AddMod(mod, true); - } - - private void OnModRemovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.RemoveMod(mod, true); - } - - private void OnModMovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.ReloadMod(mod, true); - } - - public string ToFilename(FilenameService fileNames) - => fileNames.ActiveCollectionsFile; - - public string TypeName - => "Active Collections"; - - public string LogName(string _) - => "to file"; - - public void Save(StreamWriter writer) - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs deleted file mode 100644 index d3628c27..00000000 --- a/Penumbra/Collections/CollectionManager.cs +++ /dev/null @@ -1,480 +0,0 @@ -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Mods; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Penumbra.Api; -using Penumbra.GameData.Actors; -using Penumbra.Interop.Services; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.Util; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; - -namespace Penumbra.Collections; - -public sealed partial class CollectionManager : IDisposable, IEnumerable -{ - private readonly ModManager _modManager; - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly Configuration _config; - - - // The empty collection is always available and always has index 0. - // It can not be deleted or moved. - private readonly List _collections = new() - { - ModCollection.Empty, - }; - - public ModCollection this[Index idx] - => _collections[idx]; - - public ModCollection? this[string name] - => ByName(name, out var c) ? c : null; - - public int Count - => _collections.Count; - - // Obtain a collection case-independently by name. - public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - - // Default enumeration skips the empty collection. - public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public IEnumerable GetEnumeratorWithEmpty() - => _collections; - - public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals, - SaveService saveService) - { - using var time = timer.Measure(StartTimeType.Collections); - _communicator = communicator; - _characterUtility = characterUtility; - _residentResources = residentResources; - _config = config; - _modManager = modManager; - _saveService = saveService; - Individuals = individuals; - - // The collection manager reacts to changes in mods by itself. - _communicator.ModDiscoveryStarted.Event += OnModDiscoveryStarted; - _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; - _communicator.ModOptionChanged.Event += OnModOptionsChanged; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.CollectionChange.Event += SaveOnChange; - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(files); - LoadCollections(files); - UpdateCurrentCollectionInUse(); - CreateNecessaryCaches(); - } - - public void Dispose() - { - _communicator.CollectionChange.Event -= SaveOnChange; - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; - _communicator.ModDiscoveryStarted.Event -= OnModDiscoveryStarted; - _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; - _communicator.ModOptionChanged.Event -= OnModOptionsChanged; - _communicator.ModPathChanged.Event -= OnModPathChange; - } - - private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); - - // Returns true if the name is not empty, it is not the name of the empty collection - // and no existing collection results in the same filename as name. - public bool CanAddCollection(string name, out string fixedName) - { - if (!ModCollection.IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if (name.Length == 0 - || name == ModCollection.Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; - } - - // Add a new collection of the given name. - // If duplicate is not-null, the new collection will be a duplicate of it. - // If the name of the collection would result in an already existing filename, skip it. - // Returns true if the collection was successfully created and fires a Inactive event. - // Also sets the current collection to the new collection afterwards. - public bool AddCollection(string name, ModCollection? duplicate) - { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); - return false; - } - - var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); - newCollection.Index = _collections.Count; - _collections.Add(newCollection); - - Penumbra.SaveService.ImmediateSave(newCollection); - Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); - SetCollection(newCollection.Index, CollectionType.Current); - return true; - } - - // Remove the given collection if it exists and is neither the empty nor the default-named collection. - // If the removed collection was active, it also sets the corresponding collection to the appropriate default. - // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection(int idx) - { - if (idx <= ModCollection.Empty.Index || idx >= _collections.Count) - { - Penumbra.Log.Error("Can not remove the empty collection."); - return false; - } - - if (idx == DefaultName.Index) - { - Penumbra.Log.Error("Can not remove the default collection."); - return false; - } - - if (idx == Current.Index) - SetCollection(DefaultName.Index, CollectionType.Current); - - if (idx == Default.Index) - SetCollection(ModCollection.Empty.Index, CollectionType.Default); - - for (var i = 0; i < _specialCollections.Length; ++i) - { - if (idx == _specialCollections[i]?.Index) - SetCollection(ModCollection.Empty, (CollectionType)i); - } - - for (var i = 0; i < Individuals.Count; ++i) - { - if (Individuals[i].Collection.Index == idx) - SetCollection(ModCollection.Empty, CollectionType.Individual, i); - } - - var collection = _collections[idx]; - - // Clear own inheritances. - foreach (var inheritance in collection.Inheritance) - collection.ClearSubscriptions(inheritance); - - Penumbra.SaveService.ImmediateDelete(collection); - _collections.RemoveAt(idx); - - // Clear external inheritances. - foreach (var c in _collections) - { - var inheritedIdx = c._inheritance.IndexOf(collection); - if (inheritedIdx >= 0) - c.RemoveInheritance(inheritedIdx); - - if (c.Index > idx) - --c.Index; - } - - Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); - return true; - } - - public bool RemoveCollection(ModCollection collection) - => RemoveCollection(collection.Index); - - private void OnModDiscoveryStarted() - { - foreach (var collection in this) - collection.PrepareModDiscovery(); - } - - private void OnModDiscoveryFinished() - { - // First, re-apply all mod settings. - foreach (var collection in this) - collection.ApplyModSettings(); - - // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach (var collection in this.Where(c => c.HasCache)) - collection.ForceCacheUpdate(); - } - - - // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { - case ModPathChangeType.Added: - foreach (var collection in this) - collection.AddMod(mod); - - OnModAddedActive(mod); - break; - case ModPathChangeType.Deleted: - OnModRemovedActive(mod); - foreach (var collection in this) - collection.RemoveMod(mod, mod.Index); - - break; - case ModPathChangeType.Moved: - OnModMovedActive(mod); - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - Penumbra.SaveService.QueueSave(collection); - - break; - case ModPathChangeType.StartingReload: - OnModRemovedActive(mod); - break; - case ModPathChangeType.Reloaded: - OnModAddedActive(mod); - break; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } - - // Automatically update all relevant collections when a mod is changed. - // This means saving if options change in a way where the settings may change and the collection has settings for this mod. - // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) - { - // Handle changes that break revertability. - if (type == ModOptionChangeType.PrepareChange) - { - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - collection._cache!.RemoveMod(mod, false); - } - - return; - } - - type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); - - // Handle changes that require overwriting the collection. - if (requiresSaving) - foreach (var collection in this) - { - if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - _saveService.QueueSave(collection); - } - - // Handle changes that reload the mod if the changes did not need to be prepared, - // or re-add the mod if they were prepared. - if (recomputeList) - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - { - if (reload) - collection._cache!.ReloadMod(mod, true); - else - collection._cache!.AddMod(mod, true); - } - } - } - - // Add the collection with the default name if it does not exist. - // It should always be ensured that it exists, otherwise it will be created. - // This can also not be deleted, so there are always at least the empty and a collection with default name. - private void AddDefaultCollection() - { - var idx = GetIndexForCollectionName(ModCollection.DefaultCollection); - if (idx >= 0) - { - DefaultName = this[idx]; - return; - } - - var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection); - _saveService.ImmediateSave(defaultCollection); - defaultCollection.Index = _collections.Count; - _collections.Add(defaultCollection); - } - - // Inheritances can not be setup before all collections are read, - // so this happens after reading the collections. - private void ApplyInheritances(IEnumerable> inheritances) - { - foreach (var (collection, inheritance) in this.Zip(inheritances)) - { - var changes = false; - foreach (var subCollectionName in inheritance) - { - if (!ByName(subCollectionName, out var subCollection)) - { - changes = true; - Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); - } - else if (!collection.AddInheritance(subCollection, false)) - { - changes = true; - Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); - } - } - - if (changes) - _saveService.ImmediateSave(collection); - } - } - - // Read all collection files in the Collection Directory. - // Ensure that the default named collection exists, and apply inheritances afterwards. - // Duplicate collection files are not deleted, just not added here. - private void ReadCollections(FilenameService files) - { - var inheritances = new List>(); - foreach (var file in files.CollectionFiles) - { - var collection = ModCollection.LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } - } - - AddDefaultCollection(); - ApplyInheritances(inheritances); - } - - public string RedundancyCheck(CollectionType type, ActorIdentifier id) - { - var checkAssignment = ByType(type, id); - if (checkAssignment == null) - return string.Empty; - - switch (type) - { - // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. - case CollectionType.Individual: - switch (id.Type) - { - case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: - { - var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." - : string.Empty; - } - case IdentifierType.Owned: - if (id.HomeWorld != ushort.MaxValue) - { - var global = ByType(CollectionType.Individual, - Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) - return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; - } - - var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." - : string.Empty; - } - - break; - // The group of all Characters is redundant if they are all equal to Default or unassigned. - case CollectionType.MalePlayerCharacter: - case CollectionType.MaleNonPlayerCharacter: - case CollectionType.FemalePlayerCharacter: - case CollectionType.FemaleNonPlayerCharacter: - var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; - var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; - var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; - var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; - if (first.Index == second.Index - && first.Index == third.Index - && first.Index == fourth.Index - && first.Index == Default.Index) - return - "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" - + "You can keep just the Default Assignment."; - - break; - // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. - case CollectionType.NonPlayerChild: - case CollectionType.NonPlayerElderly: - var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); - var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); - var collection1 = CollectionType.MaleNonPlayerCharacter; - var collection2 = CollectionType.FemaleNonPlayerCharacter; - if (maleNpc == null) - { - maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection1 = CollectionType.Default; - } - - if (femaleNpc == null) - { - femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection2 = CollectionType.Default; - } - - return collection1 == collection2 - ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." - : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; - - // For other assignments, check the inheritance order, unassigned means fall-through, - // assigned needs identical assignments to be redundant. - default: - var group = type.InheritanceOrder(); - foreach (var parentType in group) - { - var assignment = ByType(parentType); - if (assignment == null) - continue; - - if (assignment.Index == checkAssignment.Index) - return - $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; - } - - break; - } - - return string.Empty; - } -} diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs deleted file mode 100644 index 91fcc5a1..00000000 --- a/Penumbra/Collections/CollectionType.cs +++ /dev/null @@ -1,584 +0,0 @@ -using Penumbra.GameData.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Metadata.Ecma335; - -namespace Penumbra.Collections; - -public enum CollectionType : byte -{ - // Special Collections - Yourself = Api.Enums.ApiCollectionType.Yourself, - - MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, - FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, - MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, - FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, - NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, - NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, - - MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, - FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, - MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, - FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, - - MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, - FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, - MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, - FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, - - MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, - FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, - MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, - FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, - - MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, - FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, - MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, - FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, - - MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, - FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, - MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, - FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, - - MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, - FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, - MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, - FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, - - MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, - FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, - MaleLost = Api.Enums.ApiCollectionType.MaleLost, - FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, - - MaleRava = Api.Enums.ApiCollectionType.MaleRava, - FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, - MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, - FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, - - MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, - FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, - MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, - FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, - - MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, - FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, - MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, - FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, - - MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, - FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, - MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, - FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, - - MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, - FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, - MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, - FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, - - MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, - FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, - MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, - FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, - - MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, - FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, - MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, - FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, - - MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, - FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, - MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, - FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, - - MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, - FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, - MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, - FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, - - Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed - Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed - Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed - Individual, // An individual collection was changed - Inactive, // A collection was added or removed - Temporary, // A temporary collections was set or deleted via IPC -} - -public static class CollectionTypeExtensions -{ - public static bool IsSpecial( this CollectionType collectionType ) - => collectionType < CollectionType.Default; - - public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >() - .Where( IsSpecial ) - .Select( s => ( s, s.ToName(), s.ToDescription() ) ) - .ToArray(); - - public static CollectionType FromParts( Gender gender, bool npc ) - { - gender = gender switch - { - Gender.MaleNpc => Gender.Male, - Gender.FemaleNpc => Gender.Female, - _ => gender, - }; - - return ( gender, npc ) switch - { - (Gender.Male, false) => CollectionType.MalePlayerCharacter, - (Gender.Female, false) => CollectionType.FemalePlayerCharacter, - (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, - (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, - _ => CollectionType.Inactive, - }; - } - - // @formatter:off - private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; - private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; - // @formatter:on - - /// A list of definite redundancy possibilities. - public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) - => collectionType switch - { - CollectionType.Yourself => DefaultList, - CollectionType.MalePlayerCharacter => DefaultList, - CollectionType.FemalePlayerCharacter => DefaultList, - CollectionType.MaleNonPlayerCharacter => DefaultList, - CollectionType.FemaleNonPlayerCharacter => DefaultList, - CollectionType.MaleMidlander => MalePlayerList, - CollectionType.FemaleMidlander => FemalePlayerList, - CollectionType.MaleHighlander => MalePlayerList, - CollectionType.FemaleHighlander => FemalePlayerList, - CollectionType.MaleWildwood => MalePlayerList, - CollectionType.FemaleWildwood => FemalePlayerList, - CollectionType.MaleDuskwight => MalePlayerList, - CollectionType.FemaleDuskwight => FemalePlayerList, - CollectionType.MalePlainsfolk => MalePlayerList, - CollectionType.FemalePlainsfolk => FemalePlayerList, - CollectionType.MaleDunesfolk => MalePlayerList, - CollectionType.FemaleDunesfolk => FemalePlayerList, - CollectionType.MaleSeekerOfTheSun => MalePlayerList, - CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, - CollectionType.MaleKeeperOfTheMoon => MalePlayerList, - CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, - CollectionType.MaleSeawolf => MalePlayerList, - CollectionType.FemaleSeawolf => FemalePlayerList, - CollectionType.MaleHellsguard => MalePlayerList, - CollectionType.FemaleHellsguard => FemalePlayerList, - CollectionType.MaleRaen => MalePlayerList, - CollectionType.FemaleRaen => FemalePlayerList, - CollectionType.MaleXaela => MalePlayerList, - CollectionType.FemaleXaela => FemalePlayerList, - CollectionType.MaleHelion => MalePlayerList, - CollectionType.FemaleHelion => FemalePlayerList, - CollectionType.MaleLost => MalePlayerList, - CollectionType.FemaleLost => FemalePlayerList, - CollectionType.MaleRava => MalePlayerList, - CollectionType.FemaleRava => FemalePlayerList, - CollectionType.MaleVeena => MalePlayerList, - CollectionType.FemaleVeena => FemalePlayerList, - CollectionType.MaleMidlanderNpc => MaleNpcList, - CollectionType.FemaleMidlanderNpc => FemaleNpcList, - CollectionType.MaleHighlanderNpc => MaleNpcList, - CollectionType.FemaleHighlanderNpc => FemaleNpcList, - CollectionType.MaleWildwoodNpc => MaleNpcList, - CollectionType.FemaleWildwoodNpc => FemaleNpcList, - CollectionType.MaleDuskwightNpc => MaleNpcList, - CollectionType.FemaleDuskwightNpc => FemaleNpcList, - CollectionType.MalePlainsfolkNpc => MaleNpcList, - CollectionType.FemalePlainsfolkNpc => FemaleNpcList, - CollectionType.MaleDunesfolkNpc => MaleNpcList, - CollectionType.FemaleDunesfolkNpc => FemaleNpcList, - CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, - CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, - CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, - CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, - CollectionType.MaleSeawolfNpc => MaleNpcList, - CollectionType.FemaleSeawolfNpc => FemaleNpcList, - CollectionType.MaleHellsguardNpc => MaleNpcList, - CollectionType.FemaleHellsguardNpc => FemaleNpcList, - CollectionType.MaleRaenNpc => MaleNpcList, - CollectionType.FemaleRaenNpc => FemaleNpcList, - CollectionType.MaleXaelaNpc => MaleNpcList, - CollectionType.FemaleXaelaNpc => FemaleNpcList, - CollectionType.MaleHelionNpc => MaleNpcList, - CollectionType.FemaleHelionNpc => FemaleNpcList, - CollectionType.MaleLostNpc => MaleNpcList, - CollectionType.FemaleLostNpc => FemaleNpcList, - CollectionType.MaleRavaNpc => MaleNpcList, - CollectionType.FemaleRavaNpc => FemaleNpcList, - CollectionType.MaleVeenaNpc => MaleNpcList, - CollectionType.FemaleVeenaNpc => FemaleNpcList, - CollectionType.Individual => DefaultList, - _ => Array.Empty(), - }; - - public static CollectionType FromParts( SubRace race, Gender gender, bool npc ) - { - gender = gender switch - { - Gender.MaleNpc => Gender.Male, - Gender.FemaleNpc => Gender.Female, - _ => gender, - }; - - return ( race, gender, npc ) switch - { - (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, - (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, - (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, - (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, - (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, - (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, - (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, - (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, - (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, - (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, - (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, - (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, - (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, - (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, - - (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, - (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, - (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, - (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, - (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, - (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, - (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, - (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, - (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, - (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, - (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, - (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, - (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, - (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, - - (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, - (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, - (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, - (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, - (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, - (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, - (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, - (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, - (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, - (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, - (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, - - (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, - (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, - (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, - (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, - (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, - (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, - (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, - (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, - (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, - (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, - (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, - _ => CollectionType.Inactive, - }; - } - - public static bool TryParse( string text, out CollectionType type ) - { - if( Enum.TryParse( text, true, out type ) ) - { - return type is not CollectionType.Inactive and not CollectionType.Temporary; - } - - if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Individual; - return true; - } - - if( string.Equals( text, "base", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Default; - return true; - } - - if( string.Equals( text, "ui", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Interface; - return true; - } - - if( string.Equals( text, "selected", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Current; - return true; - } - - foreach( var t in Enum.GetValues< CollectionType >() ) - { - if( t is CollectionType.Inactive or CollectionType.Temporary ) - { - continue; - } - - if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) ) - { - type = t; - return true; - } - } - - return false; - } - - public static string ToName( this CollectionType collectionType ) - => collectionType switch - { - CollectionType.Yourself => "Your Character", - CollectionType.NonPlayerChild => "Non-Player Children", - CollectionType.NonPlayerElderly => "Non-Player Elderly", - CollectionType.MalePlayerCharacter => "Male Player Characters", - CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", - CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", - CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", - CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", - CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", - CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", - CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", - CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", - CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", - CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", - CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", - CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", - CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", - CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", - CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", - CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", - CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", - CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", - CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", - CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", - CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", - CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", - CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", - CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", - CollectionType.FemalePlayerCharacter => "Female Player Characters", - CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", - CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", - CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", - CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", - CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", - CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", - CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", - CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", - CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", - CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", - CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", - CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", - CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", - CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", - CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", - CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", - CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", - CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", - CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", - CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", - CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", - CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", - CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", - CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", - CollectionType.Inactive => "Collection", - CollectionType.Default => "Default", - CollectionType.Interface => "Interface", - CollectionType.Individual => "Individual", - CollectionType.Current => "Current", - _ => string.Empty, - }; - - public static string ToDescription( this CollectionType collectionType ) - => collectionType switch - { - CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerChild => - "This collection applies to all non-player characters with a child body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerElderly => - "This collection applies to all non-player characters with an elderly body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.MalePlayerCharacter => - "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", - CollectionType.MaleNonPlayerCharacter => - "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.MaleMidlander => - "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlander => - "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwood => - "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwight => - "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolk => - "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolk => - "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSun => - "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoon => - "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolf => - "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguard => - "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaen => - "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaela => - "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelion => - "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLost => - "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRava => - "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeena => - "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.MaleMidlanderNpc => - "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlanderNpc => - "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwoodNpc => - "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwightNpc => - "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolkNpc => - "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolkNpc => - "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSunNpc => - "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoonNpc => - "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolfNpc => - "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguardNpc => - "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaenNpc => - "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaelaNpc => - "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelionNpc => - "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLostNpc => - "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRavaNpc => - "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeenaNpc => - "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemalePlayerCharacter => - "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", - CollectionType.FemaleNonPlayerCharacter => - "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.FemaleMidlander => - "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlander => - "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwood => - "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwight => - "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolk => - "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolk => - "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSun => - "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoon => - "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolf => - "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguard => - "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaen => - "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaela => - "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelion => - "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLost => - "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRava => - "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeena => - "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemaleMidlanderNpc => - "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlanderNpc => - "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwoodNpc => - "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwightNpc => - "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolkNpc => - "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolkNpc => - "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSunNpc => - "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoonNpc => - "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolfNpc => - "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguardNpc => - "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaenNpc => - "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaelaNpc => - "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelionNpc => - "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLostNpc => - "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRavaNpc => - "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeenaNpc => - "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", - _ => string.Empty, - }; -} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs deleted file mode 100644 index 498688ed..00000000 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Actors; -using Penumbra.String; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public partial class IndividualCollections -{ - public JArray ToJObject() - { - var ret = new JArray(); - foreach( var (name, identifiers, collection) in Assignments ) - { - var tmp = identifiers[0].ToJson(); - tmp.Add( "Collection", collection.Name ); - tmp.Add( "Display", name ); - ret.Add( tmp ); - } - - return ret; - } - - public bool ReadJObject( JArray? obj, CollectionManager manager ) - { - if( obj == null ) - { - return true; - } - - var changes = false; - foreach( var data in obj ) - { - try - { - var identifier = Penumbra.Actors.FromJson( data as JObject ); - var group = GetGroup( identifier ); - if( group.Length == 0 || group.Any( i => !i.IsValid ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); - continue; - } - - var collectionName = data[ "Collection" ]?.ToObject< string >() ?? string.Empty; - if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", - NotificationType.Warning ); - continue; - } - - if( !Add( group, collection ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", - NotificationType.Warning ); - } - } - catch( Exception e ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); - } - } - - return changes; - } - - internal void Migrate0To1( Dictionary< string, ModCollection > old ) - { - static bool FindDataId( string name, IReadOnlyDictionary< uint, string > data, out uint dataId ) - { - var kvp = data.FirstOrDefault( kvp => kvp.Value.Equals( name, StringComparison.OrdinalIgnoreCase ), - new KeyValuePair< uint, string >( uint.MaxValue, string.Empty ) ); - dataId = kvp.Key; - return kvp.Value.Length > 0; - } - - foreach( var (name, collection) in old ) - { - var kind = ObjectKind.None; - var lowerName = name.ToLowerInvariant(); - // Prefer matching NPC names, fewer false positives than preferring players. - if( FindDataId( lowerName, _actorManager.Data.Companions, out var dataId ) ) - { - kind = ObjectKind.Companion; - } - else if( FindDataId( lowerName, _actorManager.Data.Mounts, out dataId ) ) - { - kind = ObjectKind.MountType; - } - else if( FindDataId( lowerName, _actorManager.Data.BNpcs, out dataId ) ) - { - kind = ObjectKind.BattleNpc; - } - else if( FindDataId( lowerName, _actorManager.Data.ENpcs, out dataId ) ) - { - kind = ObjectKind.EventNpc; - } - - var identifier = _actorManager.CreateNpc( kind, dataId ); - if( identifier.IsValid ) - { - // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. - var group = GetGroup( identifier ); - var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); - if( Add( $"{_actorManager.Data.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) - { - Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); - } - else - { - Penumbra.ChatService.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - // If it is not a valid NPC name, check if it can be a player name. - else if( ActorManager.VerifyPlayerName( name ) ) - { - identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); - var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); - // Try to migrate the player name without logging full names. - if( Add( $"{name} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) - { - Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); - } - else - { - Penumbra.ChatService.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - else - { - Penumbra.ChatService.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs new file mode 100644 index 00000000..7126d0e2 --- /dev/null +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Services; + +namespace Penumbra.Collections.Manager; + +public static class ActiveCollectionMigration +{ + /// Migrate ungendered collections to Male and Female for 0.5.9.0. + public static void MigrateUngenderedCollections(FilenameService fileNames) + { + if (!ActiveCollections.Load(fileNames, out var jObject)) + return; + + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) + { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) + continue; + + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); + } + + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObject.WriteTo(j); + } + + /// Migrate individual collections to Identifiers for 0.6.0. + public static bool MigrateIndividualCollections(CollectionStorage storage, IndividualCollections individuals, JObject jObject) + { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) + return false; + + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) + { + if (!storage.ByName(collectionName, out var collection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, ModCollection.Empty); + } + else + { + dict.Add(player, collection); + } + } + + individuals.Migrate0To1(dict); + return true; + } +} diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs new file mode 100644 index 00000000..7b99b320 --- /dev/null +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.UI; +using Penumbra.Util; +using static OtterGui.Raii.ImRaii; + +namespace Penumbra.Collections.Manager; + +public class ActiveCollections : ISavable, IDisposable +{ + public const int Version = 1; + + private readonly CollectionStorage _storage; + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + public ActiveCollections(CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) + { + _storage = storage; + _communicator = communicator; + _saveService = saveService; + Current = storage.DefaultNamed; + Default = storage.DefaultNamed; + Interface = storage.DefaultNamed; + Individuals = new IndividualCollections(actors.AwaitedService); + _communicator.CollectionChange.Subscribe(OnCollectionChange); + LoadCollections(); + } + + public void Dispose() + => _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + + /// The collection currently selected for changing settings. + public ModCollection Current { get; private set; } + + /// Whether the currently selected collection is used either directly via assignment or via inheritance. + public bool CurrentCollectionInUse { get; private set; } + + /// The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } + + /// The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } + + /// The list of individual assignments. + public readonly IndividualCollections Individuals; + + /// Get the collection assigned to an individual or Default if unassigned. + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; + + /// The list of group assignments. + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; + + /// Return all actually assigned group assignments. + public IEnumerable> SpecialAssignments + { + get + { + for (var i = 0; i < _specialCollections.Length; ++i) + { + var collection = _specialCollections[i]; + if (collection != null) + yield return new KeyValuePair((CollectionType)i, collection); + } + } + } + + /// + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); + + /// Return the configured collection for the given type or null. + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) + { + if (type.IsSpecial()) + return _specialCollections[(int)type]; + + return type switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null, + _ => null, + }; + } + + /// Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) + return false; + + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); + return true; + } + + /// Remove a special collection if it exists + public void RemoveSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial()) + return; + + var old = _specialCollections[(int)collectionType]; + if (old == null) + return; + + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); + } + + /// Create an individual collection if possible. + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) + { + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); + } + + /// Remove an individual collection if it exists. + public void RemoveIndividualCollection(int individualIndex) + { + if (individualIndex < 0 || individualIndex >= Individuals.Count) + return; + + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); + } + + /// Move an individual collection from one index to another. + public void MoveIndividualCollection(int from, int to) + { + if (Individuals.Move(from, to)) + _saveService.QueueSave(this); + } + + /// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + { + var oldCollection = collectionType switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection, + CollectionType.Individual => null, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType] ?? Default, + _ => null, + }; + + if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count) + return; + + switch (collectionType) + { + case CollectionType.Default: + Default = collection; + break; + case CollectionType.Interface: + Interface = collection; + break; + case CollectionType.Current: + Current = collection; + break; + case CollectionType.Individual: + if (!Individuals.ChangeCollection(individualIndex, collection)) + return; + + break; + default: + _specialCollections[(int)collectionType] = collection; + break; + } + + UpdateCurrentCollectionInUse(); + _communicator.CollectionChange.Invoke(collectionType, oldCollection, collection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } + + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = _specialCollections + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + + /// Save if any of the active collections is changed and set new collections to Current. + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3) + { + if (collectionType is CollectionType.Inactive) + { + if (newCollection != null) + { + SetCollection(newCollection, CollectionType.Current); + } + else if (oldCollection != null) + { + if (oldCollection == Default) + SetCollection(ModCollection.Empty, CollectionType.Default); + if (oldCollection == Interface) + SetCollection(ModCollection.Empty, CollectionType.Interface); + if (oldCollection == Current) + SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); + + for (var i = 0; i < _specialCollections.Length; ++i) + { + if (oldCollection == _specialCollections[i]) + SetCollection(ModCollection.Empty, (CollectionType)i); + } + + for (var i = 0; i < Individuals.Count; ++i) + { + if (oldCollection == Individuals[i].Collection) + SetCollection(ModCollection.Empty, CollectionType.Individual, i); + } + } + } + else if (collectionType is not CollectionType.Temporary) + { + _saveService.QueueSave(this); + } + } + + /// + /// Load default, current, special, and character collections from config. + /// Then create caches. If a collection does not exist anymore, reset it to an appropriate default. + /// + private void LoadCollections() + { + var configChanged = !Load(_saveService.FileNames, out var jObject); + + // Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed. + var defaultName = jObject[nameof(Default)]?.ToObject() + ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); + if (!_storage.ByName(defaultName, out var defaultCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = defaultCollection; + } + + // Load the interface collection. If no string is set, use the name of whatever was set as Default. + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + if (!_storage.ByName(interfaceName, out var interfaceCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = interfaceCollection; + } + + // Load the current collection. + var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; + if (!_storage.ByName(currentName, out var currentCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + "Load Failure", NotificationType.Warning); + Current = _storage.DefaultNamed; + configChanged = true; + } + else + { + Current = currentCollection; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) + { + if (!_storage.ByName(typeName, out var typeCollection)) + { + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", + NotificationType.Warning); + configChanged = true; + } + else + { + _specialCollections[(int)type] = typeCollection; + } + } + } + + configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, _storage); + + // Save any changes and create all required caches. + if (configChanged) + _saveService.ImmediateSave(this); + } + + /// + /// Read the active collection file into a jObject. + /// Returns true if this is successful, false if the file does not exist or it is unsuccessful. + /// + public static bool Load(FilenameService fileNames, out JObject ret) + { + var file = fileNames.ActiveCollectionsFile; + if (File.Exists(file)) + try + { + ret = JObject.Parse(File.ReadAllText(file)); + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); + } + + ret = new JObject(); + return false; + } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return + $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } +} diff --git a/Penumbra/Collections/Manager/CollectionCacheManager.cs b/Penumbra/Collections/Manager/CollectionCacheManager.cs new file mode 100644 index 00000000..a7ace6c1 --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionCacheManager.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Penumbra.Api; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Collections.Manager; + +public class CollectionCacheManager : IDisposable, IReadOnlyDictionary +{ + private readonly ActiveCollections _active; + private readonly CommunicatorService _communicator; + + private readonly Dictionary _cache = new(); + + public int Count + => _cache.Count; + + public IEnumerator> GetEnumerator() + => _cache.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public bool ContainsKey(ModCollection key) + => _cache.ContainsKey(key); + + public bool TryGetValue(ModCollection key, [NotNullWhen(true)] out ModCollectionCache? value) + => _cache.TryGetValue(key, out value); + + public ModCollectionCache this[ModCollection key] + => _cache[key]; + + public IEnumerable Keys + => _cache.Keys; + + public IEnumerable Values + => _cache.Values; + + public IEnumerable Active + => _cache.Keys.Where(c => c.Index > ModCollection.Empty.Index); + + public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator) + { + _active = active; + _communicator = communicator; + + _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); + _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); + CreateNecessaryCaches(); + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.ModPathChanged.Unsubscribe(OnModChangeAddition); + _communicator.ModPathChanged.Unsubscribe(OnModChangeRemoval); + _communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + } + + /// + /// Cache handling. Usually recreate caches on the next framework tick, + /// but at launch create all of them at once. + /// + public void CreateNecessaryCaches() + { + var tasks = _active.SpecialAssignments.Select(p => p.Value) + .Concat(_active.Individuals.Select(p => p.Collection)) + .Prepend(_active.Current) + .Prepend(_active.Default) + .Prepend(_active.Interface) + .Distinct() + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == _active.Default))) + .ToArray(); + + Task.WaitAll(tasks); + } + + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) + { + if (type is CollectionType.Inactive) + return; + + var isDefault = type is CollectionType.Default; + if (newCollection?.Index > ModCollection.Empty.Index) + { + newCollection.CreateCache(isDefault); + _cache.TryAdd(newCollection, newCollection._cache!); + } + + RemoveCache(old); + } + + private void OnModChangeRemoval(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath) + { + switch (type) + { + case ModPathChangeType.Deleted: + case ModPathChangeType.StartingReload: + foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); + break; + case ModPathChangeType.Moved: + foreach (var collection in _cache.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); + break; + } + } + + private void OnModChangeAddition(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath) + { + if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) + return; + + foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); + } + + /// Apply a mod change to all collections with a cache. + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_cache.Keys, mod, created, removed); + + /// Remove a cache from a collection if it is active. + private void RemoveCache(ModCollection? collection) + { + if (collection != null + && collection.Index > ModCollection.Empty.Index + && collection.Index != _active.Default.Index + && collection.Index != _active.Interface.Index + && collection.Index != _active.Current.Index + && _active.SpecialAssignments.All(c => c.Value.Index != collection.Index) + && _active.Individuals.All(c => c.Collection.Index != collection.Index)) + { + _cache.Remove(collection); + collection.ClearCache(); + } + } + + /// Prepare Changes by removing mods from caches with collections or add or reload mods. + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + if (type is ModOptionChangeType.PrepareChange) + { + foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + collection._cache!.RemoveMod(mod, false); + + return; + } + + type.HandlingInfo(out _, out var recomputeList, out var reload); + + if (!recomputeList) + return; + + foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + { + if (reload) + collection._cache!.ReloadMod(mod, true); + else + collection._cache!.AddMod(mod, true); + } + } +} diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs new file mode 100644 index 00000000..b124b7db --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -0,0 +1,20 @@ +namespace Penumbra.Collections.Manager; + +public class CollectionManager +{ + public readonly CollectionStorage Storage; + public readonly ActiveCollections Active; + public readonly InheritanceManager Inheritances; + public readonly CollectionCacheManager Caches; + public readonly TempCollectionManager Temp; + + public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances, + CollectionCacheManager caches, TempCollectionManager temp) + { + Storage = storage; + Active = active; + Inheritances = inheritances; + Caches = caches; + Temp = temp; + } +} diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs new file mode 100644 index 00000000..e363c106 --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections.Manager; + +public class CollectionStorage : IReadOnlyList, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + /// The empty collection is always available at Index 0. + private readonly List _collections = new() + { + ModCollection.Empty, + }; + + public readonly ModCollection DefaultNamed; + + /// Default enumeration skips the empty collection. + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public IEnumerator GetEnumeratorWithEmpty() + => _collections.GetEnumerator(); + + public int Count + => _collections.Count; + + public ModCollection this[int index] + => _collections[index]; + + /// Find a collection by its name. If the name is empty or None, the empty collection is returned. + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + { + if (name.Length != 0) + return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + + collection = ModCollection.Empty; + return true; + } + + public CollectionStorage(CommunicatorService communicator, SaveService saveService) + { + _communicator = communicator; + _saveService = saveService; + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); + _communicator.ModPathChanged.Subscribe(OnModPathChange, 10); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, 100); + ReadCollections(out DefaultNamed); + } + + public void Dispose() + { + _communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + } + + /// + /// Returns true if the name is not empty, it is not the name of the empty collection + /// and no existing collection results in the same filename as name. Also returns the fixed name. + /// + public bool CanAddCollection(string name, out string fixedName) + { + if (!IsValidName(name)) + { + fixedName = string.Empty; + return false; + } + + name = name.ToLowerInvariant(); + if (name.Length == 0 + || name == ModCollection.Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.ToLowerInvariant() == name)) + { + fixedName = string.Empty; + return false; + } + + fixedName = name; + return true; + } + + /// + /// Add a new collection of the given name. + /// If duplicate is not-null, the new collection will be a duplicate of it. + /// If the name of the collection would result in an already existing filename, skip it. + /// Returns true if the collection was successfully created and fires a Inactive event. + /// Also sets the current collection to the new collection afterwards. + /// + public bool AddCollection(string name, ModCollection? duplicate) + { + if (!CanAddCollection(name, out var fixedName)) + { + Penumbra.ChatService.NotificationMessage( + $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", "Warning", + NotificationType.Warning); + return false; + } + + var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); + newCollection.Index = _collections.Count; + _collections.Add(newCollection); + + _saveService.ImmediateSave(newCollection); + Penumbra.ChatService.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", + NotificationType.Success); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + return true; + } + + /// Whether the given collection can be deleted. + public bool CanRemoveCollection(ModCollection collection) + => collection.Index > ModCollection.Empty.Index && collection.Index < Count && collection.Index != DefaultNamed.Index; + + /// + /// Remove the given collection if it exists and is neither the empty nor the default-named collection. + /// + public bool RemoveCollection(ModCollection collection) + { + if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) + { + Penumbra.ChatService.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error); + return false; + } + + if (collection.Index == DefaultNamed.Index) + { + Penumbra.ChatService.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error); + return false; + } + + _saveService.ImmediateDelete(collection); + _collections.RemoveAt(collection.Index); + // Update indices. + for (var i = collection.Index; i < Count; ++i) + _collections[i].Index = i; + + Penumbra.ChatService.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); + return true; + } + + /// Stored after loading to be consumed and passed to the inheritance manager later. + private List>? _inheritancesByName = new(); + + /// Return an enumerable of collections and the collections they should inherit. + public IEnumerable<(ModCollection Collection, IReadOnlyList Inheritance, bool LoadChanges)> ConsumeInheritanceNames() + { + if (_inheritancesByName == null) + throw new Exception("Inheritances were already consumed. This method can not be called twice."); + + var inheritances = _inheritancesByName; + _inheritancesByName = null; + var list = new List(); + foreach (var (collection, inheritance) in _collections.Zip(inheritances)) + { + list.Clear(); + var changes = false; + foreach (var subCollectionName in inheritance) + { + if (ByName(subCollectionName, out var subCollection)) + { + list.Add(subCollection); + } + else + { + Penumbra.ChatService.NotificationMessage( + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", + NotificationType.Warning); + changes = true; + } + } + + yield return (collection, list, changes); + } + } + + /// + /// Check if a name is valid to use for a collection. + /// Does not check for uniqueness. + /// + private static bool IsValidName(string name) + => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); + + /// + /// Read all collection files in the Collection Directory. + /// Ensure that the default named collection exists, and apply inheritances afterwards. + /// Duplicate collection files are not deleted, just not added here. + /// + private void ReadCollections(out ModCollection defaultNamedCollection) + { + _inheritancesByName?.Clear(); + _inheritancesByName?.Add(Array.Empty()); // None. + + foreach (var file in _saveService.FileNames.CollectionFiles) + { + var collection = ModCollection.LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (ByName(collection.Name, out _)) + { + Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {collection.Name} already exists. Import skipped.", + "Warning", NotificationType.Warning); + continue; + } + + var correctName = _saveService.FileNames.CollectionFile(collection); + if (file.FullName != correctName) + Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", + NotificationType.Warning); + + _inheritancesByName?.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + + defaultNamedCollection = SetDefaultNamedCollection(); + } + + /// + /// Add the collection with the default name if it does not exist. + /// It should always be ensured that it exists, otherwise it will be created. + /// This can also not be deleted, so there are always at least the empty and a collection with default name. + /// + private ModCollection SetDefaultNamedCollection() + { + if (ByName(ModCollection.DefaultCollectionName, out var collection)) + return collection; + + if (AddCollection(ModCollection.DefaultCollectionName, null)) + return _collections[^1]; + + Penumbra.ChatService.NotificationMessage( + $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", "Error", + NotificationType.Error); + return Count > 1 ? _collections[1] : _collections[0]; + } + + /// Move all settings in all collections to unused settings. + private void OnModDiscoveryStarted() + { + foreach (var collection in this) + collection.PrepareModDiscovery(); + } + + /// Restore all settings in all collections to mods. + private void OnModDiscoveryFinished() + { + // Re-apply all mod settings. + foreach (var collection in this) + collection.ApplyModSettings(); + } + + /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Added: + foreach (var collection in this) + collection.AddMod(mod); + break; + case ModPathChangeType.Deleted: + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); + break; + case ModPathChangeType.Moved: + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + _saveService.QueueSave(collection); + break; + } + } + + /// Save all collections where the mod has settings and the change requires saving. + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + type.HandlingInfo(out var requiresSaving, out _, out _); + if (!requiresSaving) + return; + + foreach (var collection in this) + { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + _saveService.QueueSave(collection); + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs new file mode 100644 index 00000000..52b48d9b --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -0,0 +1,583 @@ +using Penumbra.GameData.Enums; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Collections.Manager; + +public enum CollectionType : byte +{ + // Special Collections + Yourself = Api.Enums.ApiCollectionType.Yourself, + + MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, + FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, + MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, + FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, + NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, + NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, + + MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, + FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, + MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, + FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, + + MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, + FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, + MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, + FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, + + MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, + FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, + MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, + FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, + + MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, + FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, + FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, + + MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, + FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, + MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, + FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, + + MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, + FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, + MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, + FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, + + MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, + FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, + MaleLost = Api.Enums.ApiCollectionType.MaleLost, + FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, + + MaleRava = Api.Enums.ApiCollectionType.MaleRava, + FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, + MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, + FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, + + MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, + FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, + MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, + FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, + + MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, + FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, + MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, + FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, + + MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, + FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, + MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, + FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, + + MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, + FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, + + MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, + FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, + MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, + FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, + + MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, + FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, + MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, + FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, + + MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, + FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, + MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, + FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, + + MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, + FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, + MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, + FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, + + Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed + Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed + Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed + Individual, // An individual collection was changed + Inactive, // A collection was added or removed + Temporary, // A temporary collections was set or deleted via IPC +} + +public static class CollectionTypeExtensions +{ + public static bool IsSpecial(this CollectionType collectionType) + => collectionType < CollectionType.Default; + + public static readonly (CollectionType, string, string)[] Special = Enum.GetValues() + .Where(IsSpecial) + .Select(s => (s, s.ToName(), s.ToDescription())) + .ToArray(); + + public static CollectionType FromParts(Gender gender, bool npc) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return (gender, npc) switch + { + (Gender.Male, false) => CollectionType.MalePlayerCharacter, + (Gender.Female, false) => CollectionType.FemalePlayerCharacter, + (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, + (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, + _ => CollectionType.Inactive, + }; + } + + // @formatter:off + private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; + private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; + // @formatter:on + + /// A list of definite redundancy possibilities. + public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => DefaultList, + CollectionType.MalePlayerCharacter => DefaultList, + CollectionType.FemalePlayerCharacter => DefaultList, + CollectionType.MaleNonPlayerCharacter => DefaultList, + CollectionType.FemaleNonPlayerCharacter => DefaultList, + CollectionType.MaleMidlander => MalePlayerList, + CollectionType.FemaleMidlander => FemalePlayerList, + CollectionType.MaleHighlander => MalePlayerList, + CollectionType.FemaleHighlander => FemalePlayerList, + CollectionType.MaleWildwood => MalePlayerList, + CollectionType.FemaleWildwood => FemalePlayerList, + CollectionType.MaleDuskwight => MalePlayerList, + CollectionType.FemaleDuskwight => FemalePlayerList, + CollectionType.MalePlainsfolk => MalePlayerList, + CollectionType.FemalePlainsfolk => FemalePlayerList, + CollectionType.MaleDunesfolk => MalePlayerList, + CollectionType.FemaleDunesfolk => FemalePlayerList, + CollectionType.MaleSeekerOfTheSun => MalePlayerList, + CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, + CollectionType.MaleKeeperOfTheMoon => MalePlayerList, + CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, + CollectionType.MaleSeawolf => MalePlayerList, + CollectionType.FemaleSeawolf => FemalePlayerList, + CollectionType.MaleHellsguard => MalePlayerList, + CollectionType.FemaleHellsguard => FemalePlayerList, + CollectionType.MaleRaen => MalePlayerList, + CollectionType.FemaleRaen => FemalePlayerList, + CollectionType.MaleXaela => MalePlayerList, + CollectionType.FemaleXaela => FemalePlayerList, + CollectionType.MaleHelion => MalePlayerList, + CollectionType.FemaleHelion => FemalePlayerList, + CollectionType.MaleLost => MalePlayerList, + CollectionType.FemaleLost => FemalePlayerList, + CollectionType.MaleRava => MalePlayerList, + CollectionType.FemaleRava => FemalePlayerList, + CollectionType.MaleVeena => MalePlayerList, + CollectionType.FemaleVeena => FemalePlayerList, + CollectionType.MaleMidlanderNpc => MaleNpcList, + CollectionType.FemaleMidlanderNpc => FemaleNpcList, + CollectionType.MaleHighlanderNpc => MaleNpcList, + CollectionType.FemaleHighlanderNpc => FemaleNpcList, + CollectionType.MaleWildwoodNpc => MaleNpcList, + CollectionType.FemaleWildwoodNpc => FemaleNpcList, + CollectionType.MaleDuskwightNpc => MaleNpcList, + CollectionType.FemaleDuskwightNpc => FemaleNpcList, + CollectionType.MalePlainsfolkNpc => MaleNpcList, + CollectionType.FemalePlainsfolkNpc => FemaleNpcList, + CollectionType.MaleDunesfolkNpc => MaleNpcList, + CollectionType.FemaleDunesfolkNpc => FemaleNpcList, + CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, + CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, + CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, + CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, + CollectionType.MaleSeawolfNpc => MaleNpcList, + CollectionType.FemaleSeawolfNpc => FemaleNpcList, + CollectionType.MaleHellsguardNpc => MaleNpcList, + CollectionType.FemaleHellsguardNpc => FemaleNpcList, + CollectionType.MaleRaenNpc => MaleNpcList, + CollectionType.FemaleRaenNpc => FemaleNpcList, + CollectionType.MaleXaelaNpc => MaleNpcList, + CollectionType.FemaleXaelaNpc => FemaleNpcList, + CollectionType.MaleHelionNpc => MaleNpcList, + CollectionType.FemaleHelionNpc => FemaleNpcList, + CollectionType.MaleLostNpc => MaleNpcList, + CollectionType.FemaleLostNpc => FemaleNpcList, + CollectionType.MaleRavaNpc => MaleNpcList, + CollectionType.FemaleRavaNpc => FemaleNpcList, + CollectionType.MaleVeenaNpc => MaleNpcList, + CollectionType.FemaleVeenaNpc => FemaleNpcList, + CollectionType.Individual => DefaultList, + _ => Array.Empty(), + }; + + public static CollectionType FromParts(SubRace race, Gender gender, bool npc) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return (race, gender, npc) switch + { + (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, + (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, + (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, + (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, + (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, + (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, + (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, + (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, + (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, + (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, + (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, + (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, + (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, + + (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, + (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, + (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, + (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, + (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, + (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, + (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, + (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, + (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, + (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, + (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, + (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, + (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, + + (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, + (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, + (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, + (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, + (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, + (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, + (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, + (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, + (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, + (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, + + (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, + (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, + (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, + (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, + (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, + (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, + (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, + (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, + (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, + (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, + _ => CollectionType.Inactive, + }; + } + + public static bool TryParse(string text, out CollectionType type) + { + if (Enum.TryParse(text, true, out type)) + { + return type is not CollectionType.Inactive and not CollectionType.Temporary; + } + + if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Individual; + return true; + } + + if (string.Equals(text, "base", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Default; + return true; + } + + if (string.Equals(text, "ui", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Interface; + return true; + } + + if (string.Equals(text, "selected", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Current; + return true; + } + + foreach (var t in Enum.GetValues()) + { + if (t is CollectionType.Inactive or CollectionType.Temporary) + { + continue; + } + + if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase)) + { + type = t; + return true; + } + } + + return false; + } + + public static string ToName(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => "Your Character", + CollectionType.NonPlayerChild => "Non-Player Children", + CollectionType.NonPlayerElderly => "Non-Player Elderly", + CollectionType.MalePlayerCharacter => "Male Player Characters", + CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", + CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", + CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", + CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", + CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", + CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", + CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", + CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", + CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", + CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", + CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", + CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", + CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", + CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", + CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", + CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", + CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", + CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", + CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", + CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", + CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", + CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", + CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", + CollectionType.FemalePlayerCharacter => "Female Player Characters", + CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", + CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", + CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", + CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", + CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", + CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", + CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", + CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", + CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", + CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", + CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", + CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", + CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", + CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", + CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", + CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", + CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", + CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", + CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", + CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", + CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", + CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", + CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", + CollectionType.Inactive => "Collection", + CollectionType.Default => "Default", + CollectionType.Interface => "Interface", + CollectionType.Individual => "Individual", + CollectionType.Current => "Current", + _ => string.Empty, + }; + + public static string ToDescription(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerChild => + "This collection applies to all non-player characters with a child body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerElderly => + "This collection applies to all non-player characters with an elderly body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.MalePlayerCharacter => + "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", + CollectionType.MaleNonPlayerCharacter => + "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.MaleMidlander => + "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlander => + "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwood => + "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwight => + "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolk => + "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolk => + "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSun => + "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoon => + "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolf => + "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguard => + "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaen => + "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaela => + "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelion => + "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLost => + "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRava => + "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeena => + "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.MaleMidlanderNpc => + "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlanderNpc => + "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwoodNpc => + "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwightNpc => + "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolkNpc => + "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolkNpc => + "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSunNpc => + "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoonNpc => + "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolfNpc => + "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguardNpc => + "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaenNpc => + "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaelaNpc => + "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelionNpc => + "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLostNpc => + "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRavaNpc => + "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeenaNpc => + "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemalePlayerCharacter => + "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", + CollectionType.FemaleNonPlayerCharacter => + "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.FemaleMidlander => + "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlander => + "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwood => + "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwight => + "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolk => + "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolk => + "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSun => + "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoon => + "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolf => + "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguard => + "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaen => + "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaela => + "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelion => + "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLost => + "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRava => + "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeena => + "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemaleMidlanderNpc => + "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlanderNpc => + "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwoodNpc => + "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwightNpc => + "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolkNpc => + "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolkNpc => + "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSunNpc => + "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoonNpc => + "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolfNpc => + "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguardNpc => + "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaenNpc => + "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaelaNpc => + "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelionNpc => + "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLostNpc => + "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRavaNpc => + "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeenaNpc => + "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs similarity index 99% rename from Penumbra/Collections/IndividualCollections.Access.cs rename to Penumbra/Collections/Manager/IndividualCollections.Access.cs index 0b43baf3..32e7fd17 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -7,7 +7,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; using Penumbra.String; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > { diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs new file mode 100644 index 00000000..bc0d5737 --- /dev/null +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.String; + +namespace Penumbra.Collections.Manager; + +public partial class IndividualCollections +{ + public JArray ToJObject() + { + var ret = new JArray(); + foreach (var (name, identifiers, collection) in Assignments) + { + var tmp = identifiers[0].ToJson(); + tmp.Add("Collection", collection.Name); + tmp.Add("Display", name); + ret.Add(tmp); + } + + return ret; + } + + public bool ReadJObject(JArray? obj, CollectionStorage storage) + { + if (obj == null) + return true; + + var changes = false; + foreach (var data in obj) + { + try + { + var identifier = _actorManager.FromJson(data as JObject); + var group = GetGroup(identifier); + if (group.Length == 0 || group.Any(i => !i.IsValid)) + { + changes = true; + Penumbra.ChatService.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure", + NotificationType.Warning); + continue; + } + + var collectionName = data["Collection"]?.ToObject() ?? string.Empty; + if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + { + changes = true; + Penumbra.ChatService.NotificationMessage( + $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + "Load Failure", + NotificationType.Warning); + continue; + } + + if (!Add(group, collection)) + { + changes = true; + Penumbra.ChatService.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + "Load Failure", + NotificationType.Warning); + } + } + catch (Exception e) + { + changes = true; + Penumbra.ChatService.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", + NotificationType.Error); + } + } + + return changes; + } + + internal void Migrate0To1(Dictionary old) + { + static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + foreach (var (name, collection) in old) + { + var kind = ObjectKind.None; + var lowerName = name.ToLowerInvariant(); + // Prefer matching NPC names, fewer false positives than preferring players. + if (FindDataId(lowerName, _actorManager.Data.Companions, out var dataId)) + kind = ObjectKind.Companion; + else if (FindDataId(lowerName, _actorManager.Data.Mounts, out dataId)) + kind = ObjectKind.MountType; + else if (FindDataId(lowerName, _actorManager.Data.BNpcs, out dataId)) + kind = ObjectKind.BattleNpc; + else if (FindDataId(lowerName, _actorManager.Data.ENpcs, out dataId)) + kind = ObjectKind.EventNpc; + + var identifier = _actorManager.CreateNpc(kind, dataId); + if (identifier.IsValid) + { + // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. + var group = GetGroup(identifier); + var ids = string.Join(", ", group.Select(i => i.DataId.ToString())); + if (Add($"{_actorManager.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) + Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); + else + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + // If it is not a valid NPC name, check if it can be a player name. + else if (ActorManager.VerifyPlayerName(name)) + { + identifier = _actorManager.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); + var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); + // Try to migrate the player name without logging full names. + if (Add($"{name} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", new[] + { + identifier, + }, collection)) + Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); + else + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + else + { + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + } + } +} diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs similarity index 99% rename from Penumbra/Collections/IndividualCollections.cs rename to Penumbra/Collections/Manager/IndividualCollections.cs index e5de838a..28059ecf 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -8,7 +8,7 @@ using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections { @@ -234,7 +234,7 @@ public sealed partial class IndividualCollections => identifier.IsValid ? Index(DisplayString(identifier)) : -1; private string DisplayString(ActorIdentifier identifier) - { + { return identifier.Type switch { IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs new file mode 100644 index 00000000..06227fdf --- /dev/null +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -0,0 +1,68 @@ +using System; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections.Manager; + +public class InheritanceManager : IDisposable +{ + private readonly CollectionStorage _storage; + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator) + { + _storage = storage; + _saveService = saveService; + _communicator = communicator; + + ApplyInheritances(); + _communicator.CollectionChange.Subscribe(OnCollectionChange); + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + } + + /// + /// Inheritances can not be setup before all collections are read, + /// so this happens after reading the collections in the constructor, consuming the stored strings. + /// + private void ApplyInheritances() + { + foreach (var (collection, inheritances, changes) in _storage.ConsumeInheritanceNames()) + { + var localChanges = changes; + foreach (var subCollection in inheritances) + { + if (collection.AddInheritance(subCollection, false)) + continue; + + localChanges = true; + Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", + NotificationType.Warning); + } + + if (localChanges) + _saveService.ImmediateSave(collection); + } + } + + private void OnCollectionChange(CollectionType collectionType, ModCollection? old, ModCollection? newCollection, string _3) + { + if (collectionType is not CollectionType.Inactive || old == null) + return; + + foreach (var inheritance in old.Inheritance) + old.ClearSubscriptions(inheritance); + + foreach (var c in _storage) + { + var inheritedIdx = c._inheritance.IndexOf(old); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); + } + } +} diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs similarity index 86% rename from Penumbra/Api/TempCollectionManager.cs rename to Penumbra/Collections/Manager/TempCollectionManager.cs index 455751c6..0eed53d6 100644 --- a/Penumbra/Api/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Penumbra.Collections; +using Penumbra.Api; using Penumbra.GameData.Actors; using Penumbra.Mods; using Penumbra.Services; using Penumbra.String; -namespace Penumbra.Api; +namespace Penumbra.Collections.Manager; public class TempCollectionManager : IDisposable { @@ -16,19 +16,21 @@ public class TempCollectionManager : IDisposable public readonly IndividualCollections Collections; private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; private readonly Dictionary _customCollections = new(); - public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections) + public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage) { _communicator = communicator; - Collections = collections; + _storage = storage; + Collections = new IndividualCollections(actors); - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); } public void Dispose() { - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange); } private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) @@ -45,7 +47,7 @@ public class TempCollectionManager : IDisposable public string CreateTemporaryCollection(string name) { - if (Penumbra.CollectionManager.ByName(name, out _)) + if (_storage.ByName(name, out _)) return string.Empty; if (GlobalChangeCounter == int.MaxValue) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 355f17b3..3dba6903 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -40,7 +40,7 @@ public partial class ModCollection // Force an update with metadata for this cache. internal void ForceCacheUpdate() - => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); + => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Active.Default); // Handle temporary mods for this collection. public void Apply(TemporaryMod tempMod, bool created) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 7c7fa08a..4a3c008c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -20,7 +20,7 @@ public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriorit /// The Cache contains all required temporary data to use a collection. /// It will only be setup if a collection gets activated in any way. ///
-internal class ModCollectionCache : IDisposable +public class ModCollectionCache : IDisposable { private readonly ModCollection _collection; private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); @@ -175,7 +175,7 @@ internal class ModCollectionCache : IDisposable break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - FullRecalculation(_collection == Penumbra.CollectionManager.Default); + FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); break; } } @@ -183,7 +183,7 @@ internal class ModCollectionCache : IDisposable // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => FullRecalculation(_collection == Penumbra.CollectionManager.Default); + => FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); public void FullRecalculation(bool isDefault) { @@ -269,7 +269,7 @@ internal class ModCollectionCache : IDisposable if( addMetaChanges ) { ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -327,7 +327,7 @@ internal class ModCollectionCache : IDisposable AddMetaFiles(); } - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 6215dc03..67913760 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -16,7 +16,7 @@ namespace Penumbra.Collections; public partial class ModCollection { public const int CurrentVersion = 1; - public const string DefaultCollection = "Default"; + public const string DefaultCollectionName = "Default"; public const string EmptyCollection = "None"; public static readonly ModCollection Empty = CreateEmpty(); @@ -100,11 +100,6 @@ public partial class ModCollection public ModCollection Duplicate(string name) => new(name, this); - // Check if a name is valid to use for a collection. - // Does not check for uniqueness. - public static bool IsValidName(string name) - => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); - // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index efabaaf2..c604d572 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,7 +1,4 @@ using System; -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -14,7 +11,7 @@ public readonly struct ResolveData public ModCollection ModCollection => _modCollection ?? ModCollection.Empty; - public readonly IntPtr AssociatedGameObject; + public readonly nint AssociatedGameObject; public bool Valid => _modCollection != null; @@ -22,17 +19,17 @@ public readonly struct ResolveData public ResolveData() { _modCollection = null!; - AssociatedGameObject = IntPtr.Zero; + AssociatedGameObject = nint.Zero; } - public ResolveData(ModCollection collection, IntPtr gameObject) + public ResolveData(ModCollection collection, nint gameObject) { _modCollection = collection; AssociatedGameObject = gameObject; } public ResolveData(ModCollection collection) - : this(collection, IntPtr.Zero) + : this(collection, nint.Zero) { } public override string ToString() @@ -44,9 +41,9 @@ public static class ResolveDataExtensions public static ResolveData ToResolveData(this ModCollection collection) => new(collection); - public static ResolveData ToResolveData(this ModCollection collection, IntPtr ptr) + public static ResolveData ToResolveData(this ModCollection collection, nint ptr) => new(collection, ptr); public static unsafe ResolveData ToResolveData(this ModCollection collection, void* ptr) - => new(collection, (IntPtr)ptr); + => new(collection, (nint)ptr); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 44415ff9..96acda5f 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -8,6 +8,7 @@ using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; @@ -291,7 +292,7 @@ public class CommandHandler : IDisposable } } - var oldCollection = _collectionManager.ByType(type, identifier); + var oldCollection = _collectionManager.Active.ByType(type, identifier); if (collection == oldCollection) { _chat.Print(collection == null @@ -300,30 +301,30 @@ public class CommandHandler : IDisposable return false; } - var individualIndex = _collectionManager.Individuals.Index(identifier); + var individualIndex = _collectionManager.Active.Individuals.Index(identifier); if (oldCollection == null) { if (type.IsSpecial()) { - _collectionManager.CreateSpecialCollection(type); + _collectionManager.Active.CreateSpecialCollection(type); } else if (identifier.IsValid) { - var identifiers = _collectionManager.Individuals.GetGroup(identifier); - individualIndex = _collectionManager.Individuals.Count; - _collectionManager.CreateIndividualCollection(identifiers); + var identifiers = _collectionManager.Active.Individuals.GetGroup(identifier); + individualIndex = _collectionManager.Active.Individuals.Count; + _collectionManager.Active.CreateIndividualCollection(identifiers); } } else if (collection == null) { if (type.IsSpecial()) { - _collectionManager.RemoveSpecialCollection(type); + _collectionManager.Active.RemoveSpecialCollection(type); } else if (individualIndex >= 0) { - _collectionManager.RemoveIndividualCollection(individualIndex); + _collectionManager.Active.RemoveIndividualCollection(individualIndex); } else { @@ -337,7 +338,7 @@ public class CommandHandler : IDisposable return true; } - _collectionManager.SetCollection(collection!, type, individualIndex); + _collectionManager.Active.SetCollection(collection!, type, individualIndex); Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } @@ -454,15 +455,14 @@ public class CommandHandler : IDisposable collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager[lowerName]; - if (collection == null) - { - _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") - .BuiltString); - return false; - } + : _collectionManager.Storage.ByName(lowerName, out var c) ? c : null; + if (collection != null) + return true; + + _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") + .BuiltString); + return false; - return true; } private static bool? ParseTrueFalseToggle(string value) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 65fc0771..6b314ca2 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -7,8 +7,8 @@ using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Lumina.Excel.GeneratedSheets; using OtterGui; -using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Services; @@ -61,15 +61,15 @@ public unsafe class CollectionResolver using var performance = _performance.Measure(PerformanceType.IdentifyCollection); var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero); if (gameObject == null) - return _collectionManager.ByType(CollectionType.Yourself) - ?? _collectionManager.Default; + return _collectionManager.Active.ByType(CollectionType.Yourself) + ?? _collectionManager.Active.Default; var player = _actors.AwaitedService.GetCurrentPlayer(); var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) ?? CollectionByAttributes(gameObject, ref _) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; } /// Identify the correct collection for a game object. @@ -78,7 +78,7 @@ public unsafe class CollectionResolver using var t = _performance.Measure(PerformanceType.IdentifyCollection); if (gameObject == null) - return _collectionManager.Default.ToResolveData(); + return _collectionManager.Active.Default.ToResolveData(); try { @@ -96,7 +96,7 @@ public unsafe class CollectionResolver catch (Exception ex) { Penumbra.Log.Error($"Error identifying collection:\n{ex}"); - return _collectionManager.Default.ToResolveData(gameObject); + return _collectionManager.Active.Default.ToResolveData(gameObject); } } @@ -137,9 +137,9 @@ public unsafe class CollectionResolver } var notYetReady = false; - var collection = _collectionManager.ByType(CollectionType.Yourself) + var collection = _collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -156,9 +156,9 @@ public unsafe class CollectionResolver var player = _actors.AwaitedService.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) - ?? _collectionManager.ByType(CollectionType.Yourself) + ?? _collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -172,7 +172,7 @@ public unsafe class CollectionResolver var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false); if (identifier.Type is IdentifierType.Special) { - (identifier, var type) = _collectionManager.Individuals.ConvertSpecialIdentifier(identifier); + (identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) return _cache.Set(ModCollection.Empty, identifier, gameObject); } @@ -182,7 +182,7 @@ public unsafe class CollectionResolver ?? CheckYourself(identifier, gameObject) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? CheckOwnedCollection(identifier, owner, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject); } @@ -190,7 +190,7 @@ public unsafe class CollectionResolver /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) => _tempCollections.Collections.TryGetCollection(identifier, out var collection) - || _collectionManager.Individuals.TryGetCollection(identifier, out collection) + || _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) ? collection : null; @@ -200,7 +200,7 @@ public unsafe class CollectionResolver if (actor->ObjectIndex == 0 || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 || identifier.Equals(_actors.AwaitedService.GetCurrentPlayer())) - return _collectionManager.ByType(CollectionType.Yourself); + return _collectionManager.Active.ByType(CollectionType.Yourself); return null; } @@ -225,8 +225,8 @@ public unsafe class CollectionResolver var bodyType = character->CustomizeData[2]; var collection = bodyType switch { - 3 => _collectionManager.ByType(CollectionType.NonPlayerElderly), - 4 => _collectionManager.ByType(CollectionType.NonPlayerChild), + 3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly), + 4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild), _ => null, }; if (collection != null) @@ -237,8 +237,8 @@ public unsafe class CollectionResolver var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); - collection = _collectionManager.ByType(type); - collection ??= _collectionManager.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); + collection = _collectionManager.Active.ByType(type); + collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); return collection; } diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index e5ce7026..8273aed3 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,17 +1,13 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using Penumbra.Collections; using System; using System.Collections; using System.Collections.Generic; using System.Threading; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.Api; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Services; -using Penumbra.String.Classes; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 0d60e72b..58ae0d92 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Services; @@ -25,7 +26,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A _communicator = communicator; _events = events; - _communicator.CollectionChange.Event += CollectionChangeClear; + _communicator.CollectionChange.Subscribe(CollectionChangeClear); _clientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; } @@ -61,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A public void Dispose() { - _communicator.CollectionChange.Event -= CollectionChangeClear; + _communicator.CollectionChange.Unsubscribe(CollectionChangeClear); _clientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; } diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 734971c5..a05497be 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,8 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; @@ -44,7 +44,7 @@ public class PathResolver : IDisposable /// Obtain a temporary or permanent collection by name. public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection); + => _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection); /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -57,8 +57,8 @@ public class PathResolver : IDisposable return category switch { // Only Interface collection. - ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path), - _collectionManager.Interface.ToResolveData()), + ResourceCategory.Ui => (_collectionManager.Active.Interface.ResolvePath(path), + _collectionManager.Active.Interface.ToResolveData()), // Never allow changing scripts. ResourceCategory.UiScript => (null, ResolveData.Invalid), ResourceCategory.GameScript => (null, ResolveData.Invalid), @@ -93,7 +93,7 @@ public class PathResolver : IDisposable || _animationHookService.HandleFiles(type, gamePath, out resolveData) || _metaState.HandleDecalFile(type, gamePath, out resolveData); if (!nonDefault || !resolveData.Valid) - resolveData = _collectionManager.Default.ToResolveData(); + resolveData = _collectionManager.Active.Default.ToResolveData(); // Resolve using character/default collection first, otherwise forced, as usual. var resolved = resolveData.ModCollection.ResolvePath(gamePath); @@ -115,8 +115,8 @@ public class PathResolver : IDisposable /// Use the default method of path replacement. private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) { - var resolved = _collectionManager.Default.ResolvePath(path); - return (resolved, _collectionManager.Default.ToResolveData()); + var resolved = _collectionManager.Active.Default.ResolvePath(path); + return (resolved, _collectionManager.Active.Default.ToResolveData()); } /// After loading an IMC file, replace its contents with the modded IMC file. diff --git a/Penumbra/Interop/Services/CharacterUtility.List.cs b/Penumbra/Interop/Services/CharacterUtility.List.cs index 1fc33efb..abe024af 100644 --- a/Penumbra/Interop/Services/CharacterUtility.List.cs +++ b/Penumbra/Interop/Services/CharacterUtility.List.cs @@ -97,7 +97,7 @@ public unsafe partial class CharacterUtility => SetResourceInternal(_defaultResourceData, _defaultResourceSize); private void SetResourceToDefaultCollection() - => Penumbra.CollectionManager.Default.SetMetaFile(GlobalMetaIndex); + => Penumbra.CollectionManager.Active.Default.SetMetaFile(GlobalMetaIndex); public void Dispose() { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 1eb192fc..bd3a6086 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -153,7 +153,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM : 0; } - if( Penumbra.CollectionManager.Default == _collection ) + if( Penumbra.CollectionManager.Active.Default == _collection ) { SetFiles(); Penumbra.ResidentResources.Reload(); diff --git a/Penumbra/Mods/Manager/ExportManager.cs b/Penumbra/Mods/Manager/ExportManager.cs index a59315d6..3d091105 100644 --- a/Penumbra/Mods/Manager/ExportManager.cs +++ b/Penumbra/Mods/Manager/ExportManager.cs @@ -21,7 +21,7 @@ public class ExportManager : IDisposable _communicator = communicator; _modManager = modManager; UpdateExportDirectory(_config.ExportDirectory, false); - _communicator.ModPathChanged.Event += OnModPathChange; + _communicator.ModPathChanged.Subscribe(OnModPathChange); } /// @@ -76,7 +76,7 @@ public class ExportManager : IDisposable } public void Dispose() - => _communicator.ModPathChanged.Event -= OnModPathChange; + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); /// Automatically migrate the backup file to the new name if any exists. private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 85637707..a133e9ed 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -26,10 +26,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList _identifier = identifier; _modManager = modManager; - _communicator.ModOptionChanged.Event += OnModOptionChange; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.ModDataChanged.Event += OnModDataChange; - _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; + _communicator.ModOptionChanged.Subscribe(OnModOptionChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange); + _communicator.ModDataChanged.Subscribe(OnModDataChange); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); if (!identifier.Valid) identifier.FinishedCreation += OnIdentifierCreation; OnModDiscoveryFinished(); @@ -51,10 +51,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList public void Dispose() { - _communicator.ModOptionChanged.Event -= OnModOptionChange; - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDataChanged.Event -= OnModDataChange; - _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModDataChanged.Unsubscribe(OnModDataChange); + _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); } /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 7f5d3070..2d5201ad 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -5,10 +5,11 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { @@ -23,17 +24,17 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable _communicator = communicator; _files = files; Reload(); - Changed += OnChange; - _communicator.ModDiscoveryFinished.Event += Reload; - _communicator.ModDataChanged.Event += OnDataChange; - _communicator.ModPathChanged.Event += OnModPathChange; + Changed += OnChange; + _communicator.ModDiscoveryFinished.Subscribe(Reload); + _communicator.ModDataChanged.Subscribe(OnDataChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange); } public void Dispose() { - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDiscoveryFinished.Event -= Reload; - _communicator.ModDataChanged.Event -= OnDataChange; + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModDiscoveryFinished.Unsubscribe(Reload); + _communicator.ModDataChanged.Unsubscribe(OnDataChange); } public struct ImportDate : ISortMode diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 24fcd271..6265e3fd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,8 +29,10 @@ using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; using Penumbra.Interop.Services; -using Penumbra.Mods.Manager; - +using Penumbra.Mods.Manager; +using Penumbra.Collections.Manager; +using Penumbra.Mods; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -183,7 +185,7 @@ public class Penumbra : IDalamudPlugin { if (CharacterUtility.Ready) { - CollectionManager.Default.SetFiles(); + CollectionManager.Active.Default.SetFiles(); ResidentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } @@ -269,23 +271,23 @@ public class Penumbra : IDalamudPlugin + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); sb.AppendLine("**Collections**"); - sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); + sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n"); - sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count}\n"); + sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var collection = CollectionManager.ByType(type); + var collection = CollectionManager.Active.ByType(type); if (collection != null) sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); } - foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments) + foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments) sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); - foreach (var collection in CollectionManager.Where(c => c.HasCache)) + foreach (var collection in CollectionManager.Caches.Active) PrintCollection(collection); return sb.ToString(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 93abde46..f7e2da03 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -5,6 +5,7 @@ using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop.ResourceLoading; @@ -85,7 +86,10 @@ public class PenumbraNew .AddSingleton(); // Add Collection Services - services.AddTransient() + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index fea11316..8a47ff40 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,97 +1,207 @@ using System; using System.IO; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Services; +/// +/// Triggered whenever collection setup is changed. +/// +/// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) +/// Parameter is the old collection, or null on additions. +/// Parameter is the new collection, or null on deletions. +/// Parameter is the display name for Individual collections or an empty string otherwise. +/// +public sealed class CollectionChange : EventWrapper> +{ + public CollectionChange() + : base(nameof(CollectionChange)) + { } + + public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) + => Invoke(this, collectionType, oldCollection, newCollection, displayName); +} + +/// +/// Triggered whenever a temporary mod for all collections is changed. +/// +/// Parameter added, deleted or edited temporary mod. +/// Parameter is whether the mod was newly created. +/// Parameter is whether the mod was deleted. +/// +public sealed class TemporaryGlobalModChange : EventWrapper> +{ + public TemporaryGlobalModChange() + : base(nameof(TemporaryGlobalModChange)) + { } + + public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) + => Invoke(this, temporaryMod, newlyCreated, deleted); +} + +/// +/// Triggered whenever a character base draw object is being created by the game. +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is a pointer to the model id (an uint). +/// Parameter is a pointer to the customize array. +/// Parameter is a pointer to the equip data array. +/// +public sealed class CreatingCharacterBase : EventWrapper> +{ + public CreatingCharacterBase() + : base(nameof(CreatingCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) + => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); +} + +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is the created draw object. +/// +public sealed class CreatedCharacterBase : EventWrapper> +{ + public CreatedCharacterBase() + : base(nameof(CreatedCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) + => Invoke(this, gameObject, appliedCollectionName, drawObject); +} + +/// +/// Triggered whenever mod meta data or local data is changed. +/// +/// Parameter is the type of data change for the mod, which can be multiple flags. +/// Parameter is the changed mod. +/// Parameter is the old name of the mod in case of a name change, and null otherwise. +/// +public sealed class ModDataChanged : EventWrapper> +{ + public ModDataChanged() + : base(nameof(ModDataChanged)) + { } + + public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) + => Invoke(this, changeType, mod, oldName); +} + +/// +/// Triggered whenever an option of a mod is changed inside the mod. +/// +/// Parameter is the type option change. +/// Parameter is the changed mod. +/// Parameter is the index of the changed group inside the mod. +/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. +/// Parameter is the index of the group an option was moved to. +/// +public sealed class ModOptionChanged : EventWrapper> +{ + public ModOptionChanged() + : base(nameof(ModOptionChanged)) + { } + + public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) + => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); +} + +/// Triggered whenever mods are prepared to be rediscovered. +public sealed class ModDiscoveryStarted : EventWrapper +{ + public ModDiscoveryStarted() + : base(nameof(ModDiscoveryStarted)) + { } + + public void Invoke() + => EventWrapper.Invoke(this); +} + +/// Triggered whenever a new mod discovery has finished. +public sealed class ModDiscoveryFinished : EventWrapper +{ + public ModDiscoveryFinished() + : base(nameof(ModDiscoveryFinished)) + { } + + public void Invoke() + => Invoke(this); +} + +/// +/// Triggered whenever the mod root directory changes. +/// +/// Parameter is the full path of the new directory. +/// Parameter is whether the new directory is valid. +/// +/// +public sealed class ModDirectoryChanged : EventWrapper> +{ + public ModDirectoryChanged() + : base(nameof(ModDirectoryChanged)) + { } + + public void Invoke(string newModDirectory, bool newDirectoryValid) + => Invoke(this, newModDirectory, newDirectoryValid); +} + +/// +/// Triggered whenever a mod is added, deleted, moved or reloaded. +/// +/// Parameter is the type of change. +/// Parameter is the changed mod. +/// Parameter is the old directory on deletion, move or reload and null on addition. +/// Parameter is the new directory on addition, move or reload and null on deletion. +/// +/// +public sealed class ModPathChanged : EventWrapper> +{ + public ModPathChanged() + : base(nameof(ModPathChanged)) + { } + + public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) + => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); +} + public class CommunicatorService : IDisposable { - /// - /// Triggered whenever collection setup is changed. - /// - /// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) - /// Parameter is the old collection, or null on additions. - /// Parameter is the new collection, or null on deletions. - /// Parameter is the display name for Individual collections or an empty string otherwise. - /// - public readonly EventWrapper CollectionChange = new(nameof(CollectionChange)); + /// + public readonly CollectionChange CollectionChange = new(); - /// - /// Triggered whenever a temporary mod for all collections is changed. - /// - /// Parameter added, deleted or edited temporary mod. - /// Parameter is whether the mod was newly created. - /// Parameter is whether the mod was deleted. - /// - public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + /// + public readonly TemporaryGlobalModChange TemporaryGlobalModChange = new(); - /// - /// Triggered whenever a character base draw object is being created by the game. - /// - /// Parameter is the game object for which a draw object is created. - /// Parameter is the name of the applied collection. - /// Parameter is a pointer to the model id (an uint). - /// Parameter is a pointer to the customize array. - /// Parameter is a pointer to the equip data array. - /// - public readonly EventWrapper CreatingCharacterBase = new(nameof(CreatingCharacterBase)); + /// + public readonly CreatingCharacterBase CreatingCharacterBase = new(); - /// - /// Parameter is the game object for which a draw object is created. - /// Parameter is the name of the applied collection. - /// Parameter is the created draw object. - /// - public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); + /// + public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// - /// Triggered whenever mod meta data or local data is changed. - /// - /// Parameter is the type of data change for the mod, which can be multiple flags. - /// Parameter is the changed mod. - /// Parameter is the old name of the mod in case of a name change, and null otherwise. - /// - public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); + /// + public readonly ModDataChanged ModDataChanged = new(); - /// - /// Triggered whenever an option of a mod is changed inside the mod. - /// - /// Parameter is the type option change. - /// Parameter is the changed mod. - /// Parameter is the index of the changed group inside the mod. - /// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. - /// Parameter is the index of the group an option was moved to. - /// - public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged)); + /// + public readonly ModOptionChanged ModOptionChanged = new(); + /// + public readonly ModDiscoveryStarted ModDiscoveryStarted = new(); - /// Triggered whenever mods are prepared to be rediscovered. - public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted)); + /// + public readonly ModDiscoveryFinished ModDiscoveryFinished = new(); - /// Triggered whenever a new mod discovery has finished. - public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished)); + /// + public readonly ModDirectoryChanged ModDirectoryChanged = new(); - /// - /// Triggered whenever the mod root directory changes. - /// - /// Parameter is the full path of the new directory. - /// Parameter is whether the new directory is valid. - /// - /// - public readonly EventWrapper ModDirectoryChanged = new(nameof(ModDirectoryChanged)); - - /// - /// Triggered whenever a mod is added, deleted, moved or reloaded. - /// - /// Parameter is the type of change. - /// Parameter is the changed mod. - /// Parameter is the old directory on deletion, move or reload and null on addition. - /// Parameter is the new directory on addition, move or reload and null on deletion. - /// - /// - public EventWrapper ModPathChanged = new(nameof(ModPathChanged)); + /// + public readonly ModPathChanged ModPathChanged = new(); public void Dispose() { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index c2d64f92..422917a5 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.UI.Classes; using SixLabors.ImageSharp; @@ -25,8 +26,8 @@ public class ConfigMigrationService private Configuration _config = null!; private JObject _data = null!; - public string CurrentCollection = ModCollection.DefaultCollection; - public string DefaultCollection = ModCollection.DefaultCollection; + public string CurrentCollection = ModCollection.DefaultCollectionName; + public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = new(); public Dictionary ModSortOrder = new(); @@ -87,7 +88,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - CollectionManager.MigrateUngenderedCollections(_fileNames); + ActiveCollectionMigration.MigrateUngenderedCollections(_fileNames); _config.Version = 7; } @@ -257,11 +258,11 @@ public class ConfigMigrationService using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); - j.WritePropertyName(nameof(CollectionManager.Default)); + j.WritePropertyName(nameof(ActiveCollections.Default)); j.WriteValue(def); - j.WritePropertyName(nameof(CollectionManager.Interface)); + j.WritePropertyName(nameof(ActiveCollections.Interface)); j.WriteValue(ui); - j.WritePropertyName(nameof(CollectionManager.Current)); + j.WritePropertyName(nameof(ActiveCollections.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { @@ -305,7 +306,7 @@ public class ConfigMigrationService if (!collectionJson.Exists) return; - var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); + var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollectionName); var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection)); if (defaultCollectionFile.Exists) return; @@ -338,7 +339,7 @@ public class ConfigMigrationService if (!InvertModListOrder) dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); - defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); + defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollectionName, dict); Penumbra.SaveService.ImmediateSave(defaultCollection); } catch (Exception e) diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index d7060e05..b5fa5487 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -27,13 +27,13 @@ public class FilenameService ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); } - /// Obtain the path of a collection file given its name. Returns an empty string if the collection is temporary. + /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => collection.Index >= 0 ? Path.Combine(CollectionDirectory, $"{collection.Name.RemoveInvalidPathSymbols()}.json") : string.Empty; + => CollectionFile(collection.Name); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) - => Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json"); + => Path.Combine(CollectionDirectory, $"{collectionName}.json"); /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 17c58dc6..cc5e7cb6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; @@ -54,9 +55,9 @@ public class ItemSwapTab : IDisposable, ITab // @formatter:on }; - _communicator.CollectionChange.Event += OnCollectionChange; - _collectionManager.Current.ModSettingChanged += OnSettingChange; - _communicator.ModOptionChanged.Event += OnModOptionChange; + _communicator.CollectionChange.Subscribe(OnCollectionChange); + _collectionManager.Active.Current.ModSettingChanged += OnSettingChange; + _communicator.ModOptionChanged.Subscribe(OnModOptionChange); } /// Update the currently selected mod or its settings. @@ -99,9 +100,9 @@ public class ItemSwapTab : IDisposable, ITab public void Dispose() { - _communicator.CollectionChange.Event -= OnCollectionChange; - _collectionManager.Current.ModSettingChanged -= OnSettingChange; - _communicator.ModOptionChanged.Event -= OnModOptionChange; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _collectionManager.Active.Current.ModSettingChanged -= OnSettingChange; + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); } private enum SwapType @@ -199,7 +200,7 @@ public class ItemSwapTab : IDisposable, ITab var values = _selectors[_lastTab]; if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, - _useCurrentCollection ? _collectionManager.Current : null, _useRightRing, _useLeftRing); + _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: @@ -208,27 +209,27 @@ public class ItemSwapTab : IDisposable, ITab if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Weapon: break; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d4980936..59a78306 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -52,7 +52,7 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Active.Current[mod.Index].Settings); } public void ChangeOption(SubMod? subMod) @@ -475,7 +475,7 @@ public partial class ModEditWindow : Window, IDisposable /// private FullPath FindBestMatch(Utf8GamePath path) { - var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path); + var currentFile = Penumbra.CollectionManager.Active.Current.ResolvePath(path); if (currentFile != null) return currentFile.Value; diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs index 1ff60559..41aa1437 100644 --- a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using ImGuiNET; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; @@ -17,16 +18,16 @@ public sealed class CollectionSelector : FilterComboCache public void Draw(string label, float width, int individualIdx) { - var (_, collection) = _collectionManager.Individuals[individualIdx]; + var (_, collection) = _collectionManager.Active.Individuals[individualIdx]; if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); + _collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); } public void Draw(string label, float width, CollectionType type) { - var current = _collectionManager.ByType(type, ActorIdentifier.Invalid); + var current = _collectionManager.Active.ByType(type, ActorIdentifier.Invalid); if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.SetCollection(CurrentSelection, type); + _collectionManager.Active.SetCollection(CurrentSelection, type); } protected override string ToString(ModCollection obj) diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs index b7bcc0f5..7867f2b3 100644 --- a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -8,6 +8,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Services; @@ -45,7 +46,7 @@ public class IndividualCollectionUi + $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n" + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections."); ImGui.Separator(); - for (var i = 0; i < _collectionManager.Individuals.Count; ++i) + for (var i = 0; i < _collectionManager.Active.Individuals.Count; ++i) { DrawIndividualAssignment(i); } @@ -138,13 +139,13 @@ public class IndividualCollectionUi /// Draw a single individual assignment. private void DrawIndividualAssignment(int idx) { - var (name, _) = _collectionManager.Individuals[idx]; + var (name, _) = _collectionManager.Active.Individuals[idx]; using var id = ImRaii.PushId(idx); _withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, false, true)) - _collectionManager.RemoveIndividualCollection(idx); + _collectionManager.Active.RemoveIndividualCollection(idx); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); @@ -163,7 +164,7 @@ public class IndividualCollectionUi return; if (_individualDragDropIdx >= 0) - _collectionManager.MoveIndividualCollection(_individualDragDropIdx, idx); + _collectionManager.Active.MoveIndividualCollection(_individualDragDropIdx, idx); _individualDragDropIdx = -1; } @@ -178,7 +179,7 @@ public class IndividualCollectionUi if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0)) { - _collectionManager.CreateIndividualCollection(_newPlayerIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newPlayerIdentifiers); change = true; } @@ -196,7 +197,7 @@ public class IndividualCollectionUi if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0)) { - _collectionManager.CreateIndividualCollection(_newNpcIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newNpcIdentifiers); change = true; } @@ -209,7 +210,7 @@ public class IndividualCollectionUi _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0)) return false; - _collectionManager.CreateIndividualCollection(_newOwnedIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newOwnedIdentifiers); return true; } @@ -220,7 +221,7 @@ public class IndividualCollectionUi _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0)) return false; - _collectionManager.CreateIndividualCollection(_newRetainerIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newRetainerIdentifiers); return true; } @@ -264,7 +265,7 @@ public class IndividualCollectionUi private bool DrawNewCurrentPlayerCollection(Vector2 width) { var player = _actorService.AwaitedService.GetCurrentPlayer(); - var result = _collectionManager.Individuals.CanAdd(player); + var result = _collectionManager.Active.Individuals.CanAdd(player); var tt = result switch { IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", @@ -277,7 +278,7 @@ public class IndividualCollectionUi if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid)) return false; - _collectionManager.CreateIndividualCollection(player); + _collectionManager.Active.CreateIndividualCollection(player); return true; } @@ -285,7 +286,7 @@ public class IndividualCollectionUi private bool DrawNewTargetCollection(Vector2 width) { var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true); - var result = _collectionManager.Individuals.CanAdd(target); + var result = _collectionManager.Active.Individuals.CanAdd(target); var tt = result switch { IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", @@ -295,7 +296,7 @@ public class IndividualCollectionUi }; if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid)) { - _collectionManager.CreateIndividualCollection(_collectionManager.Individuals.GetGroup(target)); + _collectionManager.Active.CreateIndividualCollection(_collectionManager.Active.Individuals.GetGroup(target)); return true; } @@ -311,7 +312,7 @@ public class IndividualCollectionUi private void UpdateIdentifiers() { var combo = GetNpcCombo(_newKind); - _newPlayerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, + _newPlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, Array.Empty(), out _newPlayerIdentifiers) switch { @@ -320,7 +321,7 @@ public class IndividualCollectionUi IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - _newRetainerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, + _newRetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, Array.Empty(), out _newRetainerIdentifiers) switch { _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, @@ -330,13 +331,13 @@ public class IndividualCollectionUi }; if (combo.CurrentSelection.Ids != null) { - _newNpcTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + _newNpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch { IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - _newOwnedTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, + _newOwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch { diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs index 57a51ab1..31044cae 100644 --- a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -7,6 +7,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; @@ -116,8 +117,8 @@ public class InheritanceUi return; _seenInheritedCollections.Clear(); - _seenInheritedCollections.Add(_collectionManager.Current); - foreach (var collection in _collectionManager.Current.Inheritance.ToList()) + _seenInheritedCollections.Add(_collectionManager.Active.Current); + foreach (var collection in _collectionManager.Active.Current.Inheritance.ToList()) DrawInheritance(collection); } @@ -135,7 +136,7 @@ public class InheritanceUi using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance!), -1); } /// @@ -146,7 +147,7 @@ public class InheritanceUi { if (_newCurrentCollection != null) { - _collectionManager.SetCollection(_newCurrentCollection, CollectionType.Current); + _collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current); _newCurrentCollection = null; } @@ -156,9 +157,9 @@ public class InheritanceUi if (_inheritanceAction.Value.Item1 >= 0) { if (_inheritanceAction.Value.Item2 == -1) - _collectionManager.Current.RemoveInheritance(_inheritanceAction.Value.Item1); + _collectionManager.Active.Current.RemoveInheritance(_inheritanceAction.Value.Item1); else - _collectionManager.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + _collectionManager.Active.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); } _inheritanceAction = null; @@ -172,7 +173,7 @@ public class InheritanceUi { DrawNewInheritanceCombo(); ImGui.SameLine(); - var inheritance = _collectionManager.Current.CheckValidInheritance(_newInheritance); + var inheritance = _collectionManager.Active.Current.CheckValidInheritance(_newInheritance); var tt = inheritance switch { ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", @@ -184,7 +185,7 @@ public class InheritanceUi }; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, inheritance != ModCollection.ValidInheritance.Valid, true) - && _collectionManager.Current.AddInheritance(_newInheritance!, true)) + && _collectionManager.Active.Current.AddInheritance(_newInheritance!, true)) _newInheritance = null; if (inheritance != ModCollection.ValidInheritance.Valid) @@ -232,15 +233,15 @@ public class InheritanceUi private void DrawNewInheritanceCombo() { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); - _newInheritance ??= _collectionManager.FirstOrDefault(c - => c != _collectionManager.Current && !_collectionManager.Current.Inheritance.Contains(c)) + _newInheritance ??= _collectionManager.Storage.FirstOrDefault(c + => c != _collectionManager.Active.Current && !_collectionManager.Active.Current.Inheritance.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); if (!combo) return; - foreach (var collection in _collectionManager - .Where(c => _collectionManager.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) + foreach (var collection in _collectionManager.Storage + .Where(c => _collectionManager.Active.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) .OrderBy(c => c.Name)) { if (ImGui.Selectable(collection.Name, _newInheritance == collection)) @@ -260,8 +261,8 @@ public class InheritanceUi if (_movedInheritance != null) { - var idx1 = _collectionManager.Current.Inheritance.IndexOf(_movedInheritance); - var idx2 = _collectionManager.Current.Inheritance.IndexOf(collection); + var idx1 = _collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance); + var idx2 = _collectionManager.Active.Current.Inheritance.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -291,7 +292,7 @@ public class InheritanceUi if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(collection), -1); + _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs index 5af7c578..91b7f491 100644 --- a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; namespace Penumbra.UI.CollectionTab; @@ -37,6 +38,6 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri protected override bool IsVisible(int globalIdx, LowerString filter) { var obj = Items[globalIdx]; - return filter.IsContained(obj.Item2) && _collectionManager.ByType(obj.Item1) == null; + return filter.IsContained(obj.Item2) && _collectionManager.Active.ByType(obj.Item1) == null; } } \ No newline at end of file diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index c6a7d451..b1956796 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -24,7 +24,7 @@ public class FileDialogService : IDisposable { _communicator = communicator; _manager = SetupFileManager(config.ModDirectory); - _communicator.ModDirectoryChanged.Event += OnModDirectoryChange; + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange); } public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, @@ -71,7 +71,7 @@ public class FileDialogService : IDisposable { _startPaths.Clear(); _manager.Reset(); - _communicator.ModDirectoryChanged.Event -= OnModDirectoryChange; + _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChange); } private string? GetStartPath(string title, string? startPath, bool forceStartPath) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 72e9a362..1d072d89 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,10 +14,11 @@ using OtterGui.FileSystem.Selector; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; @@ -79,25 +80,25 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector @@ -495,7 +496,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector !c.Solved) ? ColorId.ConflictingMod @@ -657,7 +658,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector 0) { if (conflicts.Any(c => !c.Solved)) @@ -727,7 +728,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector "Conflicts"u8; public bool IsVisible - => _collectionManager.Current.Conflicts(_selector.Selected!).Count > 0; + => _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0; public void DrawContent() { @@ -36,7 +36,7 @@ public class ModPanelConflictsTab : ITab // Can not be null because otherwise the tab bar is never drawn. var mod = _selector.Selected!; - foreach (var conflict in Penumbra.CollectionManager.Current.Conflicts(mod)) + foreach (var conflict in _collectionManager.Active.Current.Conflicts(mod)) { if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) _selector.SelectByValue(otherMod); @@ -47,7 +47,7 @@ public class ModPanelConflictsTab : ITab { var priority = conflict.Mod2.Index < 0 ? conflict.Mod2.Priority - : _collectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + : _collectionManager.Active.Current[conflict.Mod2.Index].Settings!.Priority; ImGui.TextUnformatted($"(Priority {priority})"); } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index ffae30d2..f4407841 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -12,6 +12,7 @@ using Penumbra.Mods; using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Dalamud.Interface; +using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; @@ -59,7 +60,7 @@ public class ModPanelSettingsTab : ITab _settings = _selector.SelectedSettings; _collection = _selector.SelectedSettingCollection; - _inherited = _collection != _collectionManager.Current; + _inherited = _collection != _collectionManager.Active.Current; _empty = _settings == ModSettings.Empty; DrawInheritedWarning(); @@ -113,7 +114,7 @@ public class ModPanelSettingsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, false); + _collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -127,7 +128,7 @@ public class ModPanelSettingsTab : ITab return; _modManager.SetKnown(_selector.Selected!); - _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); + _collectionManager.Active.Current.SetModState(_selector.Selected!.Index, enabled); } /// @@ -145,7 +146,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority) - _collectionManager.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); + _collectionManager.Active.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); _currentPriority = null; } @@ -167,7 +168,7 @@ public class ModPanelSettingsTab : ITab var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, true); + _collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); @@ -190,7 +191,7 @@ public class ModPanelSettingsTab : ITab id.Push(idx2); var option = group[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); if (option.Description.Length > 0) { @@ -235,7 +236,7 @@ public class ModPanelSettingsTab : ITab using var i = ImRaii.PushId(idx); var option = group[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); if (option.Description.Length <= 0) continue; @@ -320,7 +321,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.Checkbox(option.Name, ref setting)) { flags = setting ? flags | flag : flags & ~flag; - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); } if (option.Description.Length > 0) @@ -348,10 +349,10 @@ public class ModPanelSettingsTab : ITab if (ImGui.Selectable("Enable All")) { flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); } if (ImGui.Selectable("Disable All")) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index e5f9083e..7f517634 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -8,7 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -17,7 +17,7 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { private readonly CollectionManager _collectionManager; - private readonly PenumbraApi _api; + private readonly PenumbraApi _api; public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api) { @@ -49,7 +49,7 @@ public class ChangedItemsTab : ITab ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale); ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale); - var items = _collectionManager.Current.ChangedItems; + var items = _collectionManager.Active.Current.ChangedItems; var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index afd739a8..18dd3b95 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -8,6 +8,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Services; using Penumbra.UI.CollectionTab; @@ -35,12 +36,12 @@ public class CollectionsTab : IDisposable, ITab _config = config; _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); _collectionsWithEmpty = new CollectionSelector(_collectionManager, - () => _collectionManager.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); - _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.OrderBy(c => c.Name).ToList()); + () => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); + _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); _inheritance = new InheritanceUi(_collectionManager); _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); - _communicator.CollectionChange.Event += _individualCollections.UpdateIdentifiers; + _communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); } public ReadOnlySpan Label @@ -51,7 +52,7 @@ public class CollectionsTab : IDisposable, ITab => (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); public void Dispose() - => _communicator.CollectionChange.Event -= _individualCollections.UpdateIdentifiers; + => _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); /// Draw a tutorial step regardless of tab selection. public void DrawHeader() @@ -79,22 +80,22 @@ public class CollectionsTab : IDisposable, ITab /// private void CreateNewCollection(bool duplicate) { - if (_collectionManager.AddCollection(_newCollectionName, duplicate ? _collectionManager.Current : null)) + if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null)) _newCollectionName = string.Empty; } /// Draw the Clean Unused Settings button if there are any. private void DrawCleanCollectionButton(Vector2 width) { - if (!_collectionManager.Current.HasUnusedSettings) + if (!_collectionManager.Active.Current.HasUnusedSettings) return; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton( - $"Clean {_collectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + $"Clean {_collectionManager.Active.Current.NumUnusedSettings} Unused Settings###CleanSettings", width , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." , false)) - _collectionManager.Current.CleanUnavailableSettings(); + _collectionManager.Active.Current.CleanUnavailableSettings(); } /// Draw the new collection input as well as its buttons. @@ -103,7 +104,7 @@ public class CollectionsTab : IDisposable, ITab // Input for new collection name. Also checks for validity when changed. ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) - _canAddCollection = _collectionManager.CanAddCollection(_newCollectionName, out _); + _canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); ImGui.SameLine(); ImGuiComponents.HelpMarker( @@ -161,14 +162,14 @@ public class CollectionsTab : IDisposable, ITab "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); // Deletion conditions. - var deleteCondition = _collectionManager.Current.Name != ModCollection.DefaultCollection; + var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); var tt = deleteCondition ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollection}."; + : $"You can not delete the collection {ModCollection.DefaultCollectionName}."; if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) - _collectionManager.RemoveCollection(_collectionManager.Current); + _collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); DrawCleanCollectionButton(width); } @@ -218,11 +219,11 @@ public class CollectionsTab : IDisposable, ITab { ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (_specialCollectionCombo.CurrentIdx == -1 - || _collectionManager.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) + || _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) { _specialCollectionCombo.ResetFilter(); _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf(t => _collectionManager.ByType(t.Item1) == null); + .IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); } if (_specialCollectionCombo.CurrentType == null) @@ -238,7 +239,7 @@ public class CollectionsTab : IDisposable, ITab if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) return; - _collectionManager.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); + _collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); _specialCollectionCombo.CurrentIdx = -1; } @@ -274,7 +275,7 @@ public class CollectionsTab : IDisposable, ITab { foreach (var (type, name, desc) in CollectionTypeExtensions.Special) { - var collection = _collectionManager.ByType(type); + var collection = _collectionManager.Active.ByType(type); if (collection == null) continue; @@ -284,7 +285,7 @@ public class CollectionsTab : IDisposable, ITab if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, false, true)) { - _collectionManager.RemoveSpecialCollection(type); + _collectionManager.Active.RemoveSpecialCollection(type); _specialCollectionCombo.ResetFilter(); } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index f9259861..64fece1c 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -11,13 +11,12 @@ using ImGuiNET; using OtterGui; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; @@ -35,8 +34,8 @@ public class DebugTab : ITab private readonly StartTracker _timer; private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly ValidityChecker _validityChecker; private readonly HttpApi _httpApi; private readonly ActorService _actorService; @@ -136,10 +135,10 @@ public class DebugTab : ITab PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Current.Name); - PrintValue(" has Cache", _collectionManager.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Default.Name); - PrintValue(" has Cache", _collectionManager.Default.HasCache.ToString()); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); + PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); + PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); @@ -221,7 +220,7 @@ public class DebugTab : ITab using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); if (table) foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState - .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) + .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) .ThenBy(kvp => kvp.Value.Item2) .ThenBy(kvp => kvp.Key)) { @@ -299,7 +298,7 @@ public class DebugTab : ITab { using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (address, identifier, collection) in _identifiedCollectionCache + foreach (var (address, identifier, collection) in _identifiedCollectionCache .OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex)) { ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}"); diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 5f455189..a578e8d2 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.String.Classes; @@ -43,7 +44,7 @@ public class EffectiveTab : ITab ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); - DrawEffectiveRows(_collectionManager.Current, skips, height, + DrawEffectiveRows(_collectionManager.Active.Current, skips, height, _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 37e0c24a..b454fa3b 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -16,7 +16,8 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.ModsTab; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; - +using Penumbra.Collections.Manager; + namespace Penumbra.UI.Tabs; public class ModsTab : ITab @@ -85,12 +86,12 @@ public class ModsTab : ITab { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"{_modManager.Count} Mods\n" - + $"{_collectionManager.Current.AnonymizedName} Current Collection\n" - + $"{_collectionManager.Current.Settings.Count} Settings\n" + + $"{_collectionManager.Active.Current.AnonymizedName} Current Collection\n" + + $"{_collectionManager.Active.Current.Settings.Count} Settings\n" + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _collectionManager.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{string.Join(", ", _collectionManager.Active.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); } } @@ -163,27 +164,27 @@ public class ModsTab : ITab _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); - if (!_collectionManager.CurrentCollectionInUse) + if (!_collectionManager.Active.CurrentCollectionInUse) ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } private void DrawDefaultCollectionButton(Vector2 width) { - var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Default.Name})"; - var isCurrent = _collectionManager.Default == _collectionManager.Current; - var isEmpty = _collectionManager.Default == ModCollection.Empty; + var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Active.Default.Name})"; + var isCurrent = _collectionManager.Active.Default == _collectionManager.Active.Current; + var isEmpty = _collectionManager.Active.Default == ModCollection.Empty; var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) - _collectionManager.SetCollection(_collectionManager.Default, CollectionType.Current); + _collectionManager.Active.SetCollection(_collectionManager.Active.Default, CollectionType.Current); } private void DrawInheritedCollectionButton(Vector2 width) { var noModSelected = _selector.Selected == null; var collection = _selector.SelectedSettingCollection; - var modInherited = collection != _collectionManager.Current; + var modInherited = collection != _collectionManager.Active.Current; var (name, tt) = (noModSelected, modInherited) switch { (true, _) => ("Inherited Collection", "No mod selected."), @@ -192,7 +193,7 @@ public class ModsTab : ITab (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), }; if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) - _collectionManager.SetCollection(collection, CollectionType.Current); + _collectionManager.Active.SetCollection(collection, CollectionType.Current); } /// Get the correct size for the mod selector based on current config. diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index b470bcb9..2426160a 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -90,7 +90,7 @@ public class TutorialService + "In here, we can create new collections, delete collections, or make them inherit from each other.") .Register($"Initial Setup, Step 5: {SelectedCollection}", $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." - + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n") + + $"We should already have a collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") .Register("Inheritance", "This is a more advanced feature. Click the help button for more information, but we will ignore this for now.") .Register($"Initial Setup, Step 6: {ActiveCollections}", @@ -99,7 +99,7 @@ public class TutorialService + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" + "Open this now to continue.") .Register($"Initial Setup, Step 7: {DefaultCollection}", - $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollectionName} - is the main one.\n\n" + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.") .Register("Interface Collection", diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs index 2472e74d..cc374df1 100644 --- a/Penumbra/Util/EventWrapper.cs +++ b/Penumbra/Util/EventWrapper.cs @@ -4,32 +4,14 @@ using System.Linq; namespace Penumbra.Util; -public readonly struct EventWrapper : IDisposable +public abstract class EventWrapper : IDisposable where T : Delegate { - private readonly string _name; - private readonly List _event = new(); + private readonly string _name; + private readonly List<(object Subscriber, int Priority)> _event = new(); - public EventWrapper(string name) + protected EventWrapper(string name) => _name = name; - public void Invoke() - { - lock (_event) - { - foreach (var action in _event) - { - try - { - action.Invoke(); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - public void Dispose() { lock (_event) @@ -38,345 +20,165 @@ public readonly struct EventWrapper : IDisposable } } - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1) + public void Subscribe(T subscriber, int priority = 0) { lock (_event) { - foreach (var action in _event) + var existingIdx = _event.FindIndex(p => (T) p.Subscriber == subscriber); + var idx = _event.FindIndex(p => p.Priority > priority); + if (idx == existingIdx) + { + if (idx < 0) + _event.Add((subscriber, priority)); + else + _event[idx] = (subscriber, priority); + } + else + { + if (idx < 0) + _event.Add((subscriber, priority)); + else + _event.Insert(idx, (subscriber, priority)); + + if (existingIdx >= 0) + _event.RemoveAt(existingIdx < idx ? existingIdx : existingIdx + 1); + } + } + } + + public void Unsubscribe(T subscriber) + { + lock (_event) + { + var idx = _event.FindIndex(p => (T) p.Subscriber == subscriber); + if (idx >= 0) + _event.RemoveAt(idx); + } + } + + + protected static void Invoke(EventWrapper wrapper) + { + lock (wrapper._event) + { + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1); + ((Action)action).Invoke(); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2); + ((Action)action).Invoke(a); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3); + ((Action)action).Invoke(a, b); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4); + ((Action)action).Invoke(a, b, c); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4, arg5); + ((Action)action).Invoke(a, b, c, d); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6); + ((Action)action).Invoke(a, b, c, d, e); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); + try + { + ((Action)action).Invoke(a, b, c, d, e, f); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); + } } } } From 0ed1a81c2999b17740ecf8bb5ce865785c4df083 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Apr 2023 00:29:19 +0200 Subject: [PATCH 0859/2451] Some stuff. --- Penumbra/Collections/Manager/ActiveCollections.cs | 1 + Penumbra/Interop/ResourceTree/ResourceTree.cs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 7b99b320..2ec3a33b 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -33,6 +33,7 @@ public class ActiveCollections : ISavable, IDisposable Individuals = new IndividualCollections(actors.AwaitedService); _communicator.CollectionChange.Subscribe(OnCollectionChange); LoadCollections(); + UpdateCurrentCollectionInUse(); } public void Dispose() diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index e0cb1a95..2cec991d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; namespace Penumbra.Interop.ResourceTree; @@ -17,6 +18,10 @@ public class ResourceTree public readonly string CollectionName; public readonly List Nodes; + public int ModelId; + public CustomizeData CustomizeData; + public GenderRace RaceCode; + public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName) { Name = name; @@ -32,6 +37,9 @@ public class ResourceTree var model = (CharacterBase*)character->GameObject.GetDrawObject(); var equipment = new ReadOnlySpan(character->EquipSlotData, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); + ModelId = character->ModelCharaId; + CustomizeData = character->DrawData.CustomizeData; + RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace) ((Human*)model)->RaceSexId : GenderRace.Unknown; for (var i = 0; i < model->SlotCount; ++i) { From 69ce929c5e02e1611a5119285b3ddbd8afad9e65 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Fri, 7 Apr 2023 09:39:19 -0500 Subject: [PATCH 0860/2451] Update ModFileSystemSelector.cs add functions to allow penumbra to respond to requests to unpack mods. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 44 ++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 1d072d89..25ac6275 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -12,6 +13,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; +using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -36,6 +38,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector _modUnpackQueue = new Queue(); private TexToolsImporter? _import; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; @@ -82,7 +85,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), AddNewMod, _config, _modEditor, _modManager); ImGui.OpenPopup(_infoPopupId); }, 0, modPath, _config.AlwaysOpenDefaultImport); } + private void ExternalImportListener() + { + if (_modUnpackQueue.Count > 0) + { + // Attempt to avoid triggering if other mods are already unpacking + if (!_modsCurrentlyUnpacking) + { + string modPackagePath = _modUnpackQueue.Dequeue(); + if (File.Exists(modPackagePath)) + { + _modsCurrentlyUnpacking = true; + var modPath = !_config.AlwaysOpenDefaultImport ? null + : _config.DefaultModImportPath.Length > 0 ? _config.DefaultModImportPath + : _config.ModDirectory.Length > 0 ? _config.ModDirectory : null; + + _import = new TexToolsImporter(Penumbra.ModManager.BasePath, 1, new List() { new FileInfo(modPackagePath) }, AddNewMod, + _config, _modEditor, _modManager); + ImGui.OpenPopup(_infoPopupId); + } + } + } + } + + /// + /// Unpacks the specified standalone package + /// + /// The package to unpack + public void ImportStandaloneModPackage(string modPackagePath) + { + _modUnpackQueue.Enqueue(modPackagePath); + } + /// Draw the progress information for import. private void DrawInfoPopup() { @@ -310,6 +346,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Date: Fri, 7 Apr 2023 09:40:14 -0500 Subject: [PATCH 0861/2451] Update HttpApi.cs allows external applications to tell penumbra about a mod package to unpack --- Penumbra/Api/HttpApi.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index ee87d026..ef56915c 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -16,6 +16,8 @@ public class HttpApi : IDisposable [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); + [Route( HttpVerbs.Post, "/unpackmod" )] public partial Task UnpackMod(); + [Route( HttpVerbs.Post, "/openwindow")] public partial Task OpenWindow(); // @formatter:on } @@ -110,6 +112,23 @@ public class HttpApi : IDisposable // Reload the mod by path or name, which will also remove no-longer existing mods. _api.ReloadMod( data.Path, data.Name ); } + public async partial Task UnpackMod() + { + var data = await HttpContext.GetRequestDataAsync< ModUnpackData >(); + Penumbra.Log.Debug( $"[HTTP] {nameof( UnpackMod )} triggered with {data}." ); + // Unpack the mod package if its valid. + if( data.Path.Length != 0 ) + { + _api.UnpackMod( data.Path ); + } + } + + public async partial Task OpenWindow() + { + Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); + // Open the penumbra window + _api.OpenMainWindow(TabType.Mods, "", ""); + } private record ModReloadData( string Path, string Name ) { @@ -117,6 +136,11 @@ public class HttpApi : IDisposable : this( string.Empty, string.Empty ) { } } + private record ModUnpackData( string Path) + { + public ModUnpackData() + : this( string.Empty) { } + } private record RedrawData( string Name, RedrawType Type, int ObjectTableIndex ) { From 3f4cd67dae0c23a7023b73ab744a119b196be435 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Fri, 7 Apr 2023 09:40:52 -0500 Subject: [PATCH 0862/2451] Add ExternalModImporter.cs allows access to ModFileSystemSelector actions to HTTP API --- Penumbra/Api/ExternalModImporter.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Penumbra/Api/ExternalModImporter.cs diff --git a/Penumbra/Api/ExternalModImporter.cs b/Penumbra/Api/ExternalModImporter.cs new file mode 100644 index 00000000..49005c50 --- /dev/null +++ b/Penumbra/Api/ExternalModImporter.cs @@ -0,0 +1,22 @@ +using Dalamud.Game.ClientState.Keys; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; +using System; +using System.IO; +using System.Linq; + +namespace Penumbra.Api { + public class ExternalModImporter { + private static ModFileSystemSelector modFileSystemSelectorInstance; + + public static ModFileSystemSelector ModFileSystemSelectorInstance { get => modFileSystemSelectorInstance; set => modFileSystemSelectorInstance = value; } + + public static void UnpackMod(string modPackagePath) + { + modFileSystemSelectorInstance.ImportStandaloneModPackage(modPackagePath); + } + } +} From 2bfd5d138fe3a932dddc7eea0ee14068b9c222d3 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Fri, 7 Apr 2023 09:42:01 -0500 Subject: [PATCH 0863/2451] Update PenumbraApi.cs Add method for unpacking mod. --- Penumbra/Api/PenumbraApi.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5e4c44a7..6388362a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -602,6 +602,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } + public PenumbraApiEc UnpackMod(string modFilePackagePath) + { + if (File.Exists(modFilePackagePath)) + { + ExternalModImporter.UnpackMod(modFilePackagePath); + return PenumbraApiEc.Success; + } + else + { + return PenumbraApiEc.FileMissing; + } + } + public PenumbraApiEc AddMod(string modDirectory) { CheckInitialized(); From 19efd766fc32b0030a58d356bc60aa11a0a09f44 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Fri, 7 Apr 2023 10:16:22 -0500 Subject: [PATCH 0864/2451] Update ModFileSystemSelector.cs re-add forgotten reference during rebase. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 25ac6275..c78aa099 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -91,6 +91,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Date: Sat, 8 Apr 2023 20:50:30 +0200 Subject: [PATCH 0865/2451] Add UnpackMod to API --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 6c6533ac..13ade28e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 6c6533ac60ee6e5e401bb9a65b31ad843d1757cd +Subproject commit 13ade28e21bed02e16bbd081b2e6567382cf69bd From bfb630d317bdf553e46869f3f18be8e0a071f34b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Apr 2023 20:53:11 +0200 Subject: [PATCH 0866/2451] HTTP Api formatting. --- Penumbra/Api/HttpApi.cs | 88 ++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index ef56915c..1b9cc33e 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -26,13 +26,11 @@ public class HttpApi : IDisposable private readonly IPenumbraApi _api; private WebServer? _server; - public HttpApi( IPenumbraApi api ) + public HttpApi(IPenumbraApi api) { _api = api; - if( Penumbra.Config.EnableHttpApi ) - { + if (Penumbra.Config.EnableHttpApi) CreateWebServer(); - } } public bool Enabled @@ -42,13 +40,13 @@ public class HttpApi : IDisposable { ShutdownWebServer(); - _server = new WebServer( o => o - .WithUrlPrefix( Prefix ) - .WithMode( HttpListenerMode.EmbedIO ) ) - .WithCors( Prefix ) - .WithWebApi( "/api", m => m.WithController( () => new Controller( _api ) ) ); + _server = new WebServer(o => o + .WithUrlPrefix(Prefix) + .WithMode(HttpListenerMode.EmbedIO)) + .WithCors(Prefix) + .WithWebApi("/api", m => m.WithController(() => new Controller(_api))); - _server.StateChanged += ( _, e ) => Penumbra.Log.Information( $"WebServer New State - {e.NewState}" ); + _server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}"); _server.RunAsync(); } @@ -65,88 +63,80 @@ public class HttpApi : IDisposable { private readonly IPenumbraApi _api; - public Controller( IPenumbraApi api ) + public Controller(IPenumbraApi api) => _api = api; public partial object? GetMods() { - Penumbra.Log.Debug( $"[HTTP] {nameof( GetMods )} triggered." ); + Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); return _api.GetModList(); } public async partial Task Redraw() { - var data = await HttpContext.GetRequestDataAsync< RedrawData >(); - Penumbra.Log.Debug( $"[HTTP] {nameof( Redraw )} triggered with {data}." ); - if( data.ObjectTableIndex >= 0 ) - { - _api.RedrawObject( data.ObjectTableIndex, data.Type ); - } - else if( data.Name.Length > 0 ) - { - _api.RedrawObject( data.Name, data.Type ); - } + var data = await HttpContext.GetRequestDataAsync(); + Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); + if (data.ObjectTableIndex >= 0) + _api.RedrawObject(data.ObjectTableIndex, data.Type); + else if (data.Name.Length > 0) + _api.RedrawObject(data.Name, data.Type); else - { - _api.RedrawAll( data.Type ); - } + _api.RedrawAll(data.Type); } public partial void RedrawAll() { - Penumbra.Log.Debug( $"[HTTP] {nameof( RedrawAll )} triggered." ); - _api.RedrawAll( RedrawType.Redraw ); + Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered."); + _api.RedrawAll(RedrawType.Redraw); } public async partial Task ReloadMod() { - var data = await HttpContext.GetRequestDataAsync< ModReloadData >(); - Penumbra.Log.Debug( $"[HTTP] {nameof( ReloadMod )} triggered with {data}." ); + var data = await HttpContext.GetRequestDataAsync(); + Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}."); // Add the mod if it is not already loaded and if the directory name is given. // AddMod returns Success if the mod is already loaded. - if( data.Path.Length != 0 ) - { - _api.AddMod( data.Path ); - } + if (data.Path.Length != 0) + _api.AddMod(data.Path); // Reload the mod by path or name, which will also remove no-longer existing mods. - _api.ReloadMod( data.Path, data.Name ); + _api.ReloadMod(data.Path, data.Name); } + public async partial Task UnpackMod() { - var data = await HttpContext.GetRequestDataAsync< ModUnpackData >(); - Penumbra.Log.Debug( $"[HTTP] {nameof( UnpackMod )} triggered with {data}." ); + var data = await HttpContext.GetRequestDataAsync(); + Penumbra.Log.Debug($"[HTTP] {nameof(UnpackMod)} triggered with {data}."); // Unpack the mod package if its valid. - if( data.Path.Length != 0 ) - { - _api.UnpackMod( data.Path ); - } + if (data.Path.Length != 0) + _api.UnpackMod(data.Path); } public async partial Task OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); - // Open the penumbra window - _api.OpenMainWindow(TabType.Mods, "", ""); + _api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } - private record ModReloadData( string Path, string Name ) + private record ModReloadData(string Path, string Name) { public ModReloadData() - : this( string.Empty, string.Empty ) + : this(string.Empty, string.Empty) { } } - private record ModUnpackData( string Path) + + private record ModUnpackData(string Path) { public ModUnpackData() - : this( string.Empty) { } + : this(string.Empty) + { } } - private record RedrawData( string Name, RedrawType Type, int ObjectTableIndex ) + private record RedrawData(string Name, RedrawType Type, int ObjectTableIndex) { public RedrawData() - : this( string.Empty, RedrawType.Redraw, -1 ) + : this(string.Empty, RedrawType.Redraw, -1) { } } } -} \ No newline at end of file +} From bbfc9a0a6fa5cc98024a710a14ac28739a53853f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Apr 2023 22:29:43 +0200 Subject: [PATCH 0867/2451] Rework around a saner import popup and decouple logic from interface. --- Penumbra.Api | 2 +- Penumbra/Api/ExternalModImporter.cs | 22 -- Penumbra/Api/HttpApi.cs | 27 +- Penumbra/Api/IpcTester.cs | 28 +- Penumbra/Api/PenumbraApi.cs | 32 +- Penumbra/Api/PenumbraIpcProviders.cs | 368 +++++++++--------- Penumbra/Import/TexToolsImport.cs | 4 +- Penumbra/Mods/Editor/ModBackup.cs | 4 +- .../{ExportManager.cs => ModExportManager.cs} | 6 +- Penumbra/Mods/Manager/ModImportManager.cs | 123 ++++++ Penumbra/PenumbraNew.cs | 22 +- Penumbra/UI/ImportPopup.cs | 64 +++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 139 +------ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 22 +- Penumbra/UI/Tabs/SettingsTab.cs | 12 +- Penumbra/UI/WindowSystem.cs | 14 +- 16 files changed, 478 insertions(+), 411 deletions(-) delete mode 100644 Penumbra/Api/ExternalModImporter.cs rename Penumbra/Mods/Manager/{ExportManager.cs => ModExportManager.cs} (92%) create mode 100644 Penumbra/Mods/Manager/ModImportManager.cs create mode 100644 Penumbra/UI/ImportPopup.cs diff --git a/Penumbra.Api b/Penumbra.Api index 13ade28e..d7e8c8c4 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 13ade28e21bed02e16bbd081b2e6567382cf69bd +Subproject commit d7e8c8c44d92bd50764394af51ac24cb07f362dc diff --git a/Penumbra/Api/ExternalModImporter.cs b/Penumbra/Api/ExternalModImporter.cs deleted file mode 100644 index 49005c50..00000000 --- a/Penumbra/Api/ExternalModImporter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Dalamud.Game.ClientState.Keys; -using OtterGui.Filesystem; -using OtterGui.FileSystem.Selector; -using Penumbra.Mods; -using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; -using System; -using System.IO; -using System.Linq; - -namespace Penumbra.Api { - public class ExternalModImporter { - private static ModFileSystemSelector modFileSystemSelectorInstance; - - public static ModFileSystemSelector ModFileSystemSelectorInstance { get => modFileSystemSelectorInstance; set => modFileSystemSelectorInstance = value; } - - public static void UnpackMod(string modPackagePath) - { - modFileSystemSelectorInstance.ImportStandaloneModPackage(modPackagePath); - } - } -} diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 1b9cc33e..e6d31104 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -12,12 +12,12 @@ public class HttpApi : IDisposable private partial class Controller : WebApiController { // @formatter:off - [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); - [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); - [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); - [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); - [Route( HttpVerbs.Post, "/unpackmod" )] public partial Task UnpackMod(); - [Route( HttpVerbs.Post, "/openwindow")] public partial Task OpenWindow(); + [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); + [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); + [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); + [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); + [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); + [Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); // @formatter:on } @@ -103,16 +103,15 @@ public class HttpApi : IDisposable _api.ReloadMod(data.Path, data.Name); } - public async partial Task UnpackMod() + public async partial Task InstallMod() { - var data = await HttpContext.GetRequestDataAsync(); - Penumbra.Log.Debug($"[HTTP] {nameof(UnpackMod)} triggered with {data}."); - // Unpack the mod package if its valid. + var data = await HttpContext.GetRequestDataAsync(); + Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}."); if (data.Path.Length != 0) - _api.UnpackMod(data.Path); + _api.InstallMod(data.Path); } - public async partial Task OpenWindow() + public partial void OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); _api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); @@ -125,9 +124,9 @@ public class HttpApi : IDisposable { } } - private record ModUnpackData(string Path) + private record ModInstallData(string Path) { - public ModUnpackData() + public ModInstallData() : this(string.Empty) { } } diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index da216702..467bfa7e 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -18,8 +18,8 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; -using Penumbra.Collections.Manager; - +using Penumbra.Collections.Manager; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -847,13 +847,15 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private string _newInstallPath = string.Empty; private PenumbraApiEc _lastReloadEc; private PenumbraApiEc _lastAddEc; private PenumbraApiEc _lastDeleteEc; private PenumbraApiEc _lastSetPathEc; + private PenumbraApiEc _lastInstallEc; private IList<(string, string)> _mods = new List<(string, string)>(); public readonly EventSubscriber DeleteSubscriber; @@ -895,9 +897,10 @@ public class IpcTester : IDisposable if (!_) return; - ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); - ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); - ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); + ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100); + ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); + ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); + ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -916,6 +919,13 @@ public class IpcTester : IDisposable ImGui.SameLine(); ImGui.TextUnformatted(_lastReloadEc.ToString()); + DrawIntro(Ipc.InstallMod.Label, "Install Mod"); + if (ImGui.Button("Install")) + _lastInstallEc = Ipc.InstallMod.Subscriber(_pi).Invoke(_newInstallPath); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastInstallEc.ToString()); + DrawIntro(Ipc.AddMod.Label, "Add Mod"); if (ImGui.Button("Add")) _lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory); @@ -1140,7 +1150,7 @@ public class IpcTester : IDisposable private class Temporary { private readonly DalamudPluginInterface _pi; - private readonly ModManager _modManager; + private readonly ModManager _modManager; public Temporary(DalamudPluginInterface pi, ModManager modManager) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 6388362a..63ff92f4 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -109,23 +109,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi private ActorService _actors; private CollectionResolver _collectionResolver; private CutsceneService _cutsceneService; + private ModImportManager _modImportManager; public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) + TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager) { - _communicator = communicator; - _penumbra = penumbra; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; + _communicator = communicator; + _penumbra = penumbra; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _modImportManager = modImportManager; _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) @@ -602,11 +604,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } - public PenumbraApiEc UnpackMod(string modFilePackagePath) + public PenumbraApiEc InstallMod(string modFilePackagePath) { if (File.Exists(modFilePackagePath)) { - ExternalModImporter.UnpackMod(modFilePackagePath); + _modImportManager.AddUnpack(modFilePackagePath); return PenumbraApiEc.Success; } else diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 0a28f0de..1457b3a6 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -10,7 +10,7 @@ using Penumbra.Mods.Manager; namespace Penumbra.Api; -using CurrentSettings = ValueTuple< PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)? >; +using CurrentSettings = ValueTuple>, bool)?>; public class PenumbraIpcProviders : IDisposable { @@ -18,210 +18,215 @@ public class PenumbraIpcProviders : IDisposable internal readonly IpcTester Tester; // Plugin State - internal readonly EventProvider Initialized; - internal readonly EventProvider Disposed; - internal readonly FuncProvider< int > ApiVersion; - internal readonly FuncProvider< (int Breaking, int Features) > ApiVersions; - internal readonly FuncProvider< bool > GetEnabledState; - internal readonly EventProvider< bool > EnabledChange; + internal readonly EventProvider Initialized; + internal readonly EventProvider Disposed; + internal readonly FuncProvider ApiVersion; + internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions; + internal readonly FuncProvider GetEnabledState; + internal readonly EventProvider EnabledChange; // Configuration - internal readonly FuncProvider< string > GetModDirectory; - internal readonly FuncProvider< string > GetConfiguration; - internal readonly EventProvider< string, bool > ModDirectoryChanged; + internal readonly FuncProvider GetModDirectory; + internal readonly FuncProvider GetConfiguration; + internal readonly EventProvider ModDirectoryChanged; // UI - internal readonly EventProvider< string > PreSettingsDraw; - internal readonly EventProvider< string > PostSettingsDraw; - internal readonly EventProvider< ChangedItemType, uint > ChangedItemTooltip; - internal readonly EventProvider< MouseButton, ChangedItemType, uint > ChangedItemClick; - internal readonly FuncProvider< TabType, string, string, PenumbraApiEc > OpenMainWindow; - internal readonly ActionProvider CloseMainWindow; + internal readonly EventProvider PreSettingsDraw; + internal readonly EventProvider PostSettingsDraw; + internal readonly EventProvider ChangedItemTooltip; + internal readonly EventProvider ChangedItemClick; + internal readonly FuncProvider OpenMainWindow; + internal readonly ActionProvider CloseMainWindow; // Redrawing - internal readonly ActionProvider< RedrawType > RedrawAll; - internal readonly ActionProvider< GameObject, RedrawType > RedrawObject; - internal readonly ActionProvider< int, RedrawType > RedrawObjectByIndex; - internal readonly ActionProvider< string, RedrawType > RedrawObjectByName; - internal readonly EventProvider< nint, int > GameObjectRedrawn; + internal readonly ActionProvider RedrawAll; + internal readonly ActionProvider RedrawObject; + internal readonly ActionProvider RedrawObjectByIndex; + internal readonly ActionProvider RedrawObjectByName; + internal readonly EventProvider GameObjectRedrawn; // Game State - internal readonly FuncProvider< nint, (nint, string) > GetDrawObjectInfo; - internal readonly FuncProvider< int, int > GetCutsceneParentIndex; - internal readonly EventProvider< nint, string, nint, nint, nint > CreatingCharacterBase; - internal readonly EventProvider< nint, string, nint > CreatedCharacterBase; - internal readonly EventProvider< nint, string, string > GameObjectResourcePathResolved; + internal readonly FuncProvider GetDrawObjectInfo; + internal readonly FuncProvider GetCutsceneParentIndex; + internal readonly EventProvider CreatingCharacterBase; + internal readonly EventProvider CreatedCharacterBase; + internal readonly EventProvider GameObjectResourcePathResolved; // Resolve - internal readonly FuncProvider< string, string > ResolveDefaultPath; - internal readonly FuncProvider< string, string > ResolveInterfacePath; - internal readonly FuncProvider< string, string > ResolvePlayerPath; - internal readonly FuncProvider< string, int, string > ResolveGameObjectPath; - internal readonly FuncProvider< string, string, string > ResolveCharacterPath; - internal readonly FuncProvider< string, string, string[] > ReverseResolvePath; - internal readonly FuncProvider< string, int, string[] > ReverseResolveGameObjectPath; - internal readonly FuncProvider< string, string[] > ReverseResolvePlayerPath; - internal readonly FuncProvider< string[], string[], (string[], string[][]) > ResolvePlayerPaths; + internal readonly FuncProvider ResolveDefaultPath; + internal readonly FuncProvider ResolveInterfacePath; + internal readonly FuncProvider ResolvePlayerPath; + internal readonly FuncProvider ResolveGameObjectPath; + internal readonly FuncProvider ResolveCharacterPath; + internal readonly FuncProvider ReverseResolvePath; + internal readonly FuncProvider ReverseResolveGameObjectPath; + internal readonly FuncProvider ReverseResolvePlayerPath; + internal readonly FuncProvider ResolvePlayerPaths; // Collections - internal readonly FuncProvider< IList< string > > GetCollections; - internal readonly FuncProvider< string > GetCurrentCollectionName; - internal readonly FuncProvider< string > GetDefaultCollectionName; - internal readonly FuncProvider< string > GetInterfaceCollectionName; - internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; - internal readonly FuncProvider< ApiCollectionType, string > GetCollectionForType; - internal readonly FuncProvider< ApiCollectionType, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForType; - internal readonly FuncProvider< int, (bool, bool, string) > GetCollectionForObject; - internal readonly FuncProvider< int, string, bool, bool, (PenumbraApiEc, string) > SetCollectionForObject; - internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; + internal readonly FuncProvider> GetCollections; + internal readonly FuncProvider GetCurrentCollectionName; + internal readonly FuncProvider GetDefaultCollectionName; + internal readonly FuncProvider GetInterfaceCollectionName; + internal readonly FuncProvider GetCharacterCollectionName; + internal readonly FuncProvider GetCollectionForType; + internal readonly FuncProvider SetCollectionForType; + internal readonly FuncProvider GetCollectionForObject; + internal readonly FuncProvider SetCollectionForObject; + internal readonly FuncProvider> GetChangedItems; // Meta - internal readonly FuncProvider< string > GetPlayerMetaManipulations; - internal readonly FuncProvider< string, string > GetMetaManipulations; - internal readonly FuncProvider< int, string > GetGameObjectMetaManipulations; + internal readonly FuncProvider GetPlayerMetaManipulations; + internal readonly FuncProvider GetMetaManipulations; + internal readonly FuncProvider GetGameObjectMetaManipulations; // Mods - internal readonly FuncProvider< IList< (string, string) > > GetMods; - internal readonly FuncProvider< string, string, PenumbraApiEc > ReloadMod; - internal readonly FuncProvider< string, PenumbraApiEc > AddMod; - internal readonly FuncProvider< string, string, PenumbraApiEc > DeleteMod; - internal readonly FuncProvider< string, string, (PenumbraApiEc, string, bool) > GetModPath; - internal readonly FuncProvider< string, string, string, PenumbraApiEc > SetModPath; - internal readonly EventProvider< string > ModDeleted; - internal readonly EventProvider< string > ModAdded; - internal readonly EventProvider< string, string > ModMoved; + internal readonly FuncProvider> GetMods; + internal readonly FuncProvider ReloadMod; + internal readonly FuncProvider InstallMod; + internal readonly FuncProvider AddMod; + internal readonly FuncProvider DeleteMod; + internal readonly FuncProvider GetModPath; + internal readonly FuncProvider SetModPath; + internal readonly EventProvider ModDeleted; + internal readonly EventProvider ModAdded; + internal readonly EventProvider ModMoved; // ModSettings - internal readonly FuncProvider< string, string, IDictionary< string, (IList< string >, GroupType) >? > GetAvailableModSettings; - internal readonly FuncProvider< string, string, string, bool, CurrentSettings > GetCurrentModSettings; - internal readonly FuncProvider< string, string, string, bool, PenumbraApiEc > TryInheritMod; - internal readonly FuncProvider< string, string, string, bool, PenumbraApiEc > TrySetMod; - internal readonly FuncProvider< string, string, string, int, PenumbraApiEc > TrySetModPriority; - internal readonly FuncProvider< string, string, string, string, string, PenumbraApiEc > TrySetModSetting; - internal readonly FuncProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > TrySetModSettings; - internal readonly EventProvider< ModSettingChange, string, string, bool > ModSettingChanged; - internal readonly FuncProvider< string, string, string, PenumbraApiEc > CopyModSettings; + internal readonly FuncProvider, GroupType)>?> GetAvailableModSettings; + internal readonly FuncProvider GetCurrentModSettings; + internal readonly FuncProvider TryInheritMod; + internal readonly FuncProvider TrySetMod; + internal readonly FuncProvider TrySetModPriority; + internal readonly FuncProvider TrySetModSetting; + internal readonly FuncProvider, PenumbraApiEc> TrySetModSettings; + internal readonly EventProvider ModSettingChanged; + internal readonly FuncProvider CopyModSettings; // Temporary - internal readonly FuncProvider< string, string, bool, (PenumbraApiEc, string) > CreateTemporaryCollection; - internal readonly FuncProvider< string, PenumbraApiEc > RemoveTemporaryCollection; - internal readonly FuncProvider< string, PenumbraApiEc > CreateNamedTemporaryCollection; - internal readonly FuncProvider< string, PenumbraApiEc > RemoveTemporaryCollectionByName; - internal readonly FuncProvider< string, int, bool, PenumbraApiEc > AssignTemporaryCollection; - internal readonly FuncProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryModAll; - internal readonly FuncProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryMod; - internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; - internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; + internal readonly FuncProvider CreateTemporaryCollection; + internal readonly FuncProvider RemoveTemporaryCollection; + internal readonly FuncProvider CreateNamedTemporaryCollection; + internal readonly FuncProvider RemoveTemporaryCollectionByName; + internal readonly FuncProvider AssignTemporaryCollection; + internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryModAll; + internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryMod; + internal readonly FuncProvider RemoveTemporaryModAll; + internal readonly FuncProvider RemoveTemporaryMod; - public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager ) + public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager) { Api = api; // Plugin State - Initialized = Ipc.Initialized.Provider( pi ); - Disposed = Ipc.Disposed.Provider( pi ); - ApiVersion = Ipc.ApiVersion.Provider( pi, DeprecatedVersion ); - ApiVersions = Ipc.ApiVersions.Provider( pi, () => Api.ApiVersion ); - GetEnabledState = Ipc.GetEnabledState.Provider( pi, Api.GetEnabledState ); - EnabledChange = Ipc.EnabledChange.Provider( pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent ); + Initialized = Ipc.Initialized.Provider(pi); + Disposed = Ipc.Disposed.Provider(pi); + ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion); + ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion); + GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState); + EnabledChange = + Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent); // Configuration - GetModDirectory = Ipc.GetModDirectory.Provider( pi, Api.GetModDirectory ); - GetConfiguration = Ipc.GetConfiguration.Provider( pi, Api.GetConfiguration ); - ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider( pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a ); + GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory); + GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration); + ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a); // UI - PreSettingsDraw = Ipc.PreSettingsDraw.Provider( pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a ); - PostSettingsDraw = Ipc.PostSettingsDraw.Provider( pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a ); - ChangedItemTooltip = Ipc.ChangedItemTooltip.Provider( pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip ); - ChangedItemClick = Ipc.ChangedItemClick.Provider( pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick ); - OpenMainWindow = Ipc.OpenMainWindow.Provider( pi, Api.OpenMainWindow ); - CloseMainWindow = Ipc.CloseMainWindow.Provider( pi, Api.CloseMainWindow ); + PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); + PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a); + ChangedItemTooltip = + Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip); + ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick); + OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow); + CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow); // Redrawing - RedrawAll = Ipc.RedrawAll.Provider( pi, Api.RedrawAll ); - RedrawObject = Ipc.RedrawObject.Provider( pi, Api.RedrawObject ); - RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider( pi, Api.RedrawObject ); - RedrawObjectByName = Ipc.RedrawObjectByName.Provider( pi, Api.RedrawObject ); - GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider( pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn, () => Api.GameObjectRedrawn -= OnGameObjectRedrawn ); + RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll); + RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject); + RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject); + RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject); + GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn, + () => Api.GameObjectRedrawn -= OnGameObjectRedrawn); // Game State - GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider( pi, Api.GetDrawObjectInfo ); - GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider( pi, Api.GetCutsceneParentIndex ); - CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider( pi, + GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo); + GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex); + CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi, () => Api.CreatingCharacterBase += CreatingCharacterBaseEvent, - () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent ); - CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider( pi, + () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent); + CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi, () => Api.CreatedCharacterBase += CreatedCharacterBaseEvent, - () => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent ); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider( pi, + () => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent); + GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi, () => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent, - () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent ); + () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent); // Resolve - ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider( pi, Api.ResolveDefaultPath ); - ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider( pi, Api.ResolveInterfacePath ); - ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider( pi, Api.ResolvePlayerPath ); - ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider( pi, Api.ResolveGameObjectPath ); - ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider( pi, Api.ResolvePath ); - ReverseResolvePath = Ipc.ReverseResolvePath.Provider( pi, Api.ReverseResolvePath ); - ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider( pi, Api.ReverseResolveGameObjectPath ); - ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider( pi, Api.ReverseResolvePlayerPath ); - ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider( pi, Api.ResolvePlayerPaths ); + ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath); + ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath); + ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath); + ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath); + ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath); + ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath); + ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath); + ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath); + ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths); // Collections - GetCollections = Ipc.GetCollections.Provider( pi, Api.GetCollections ); - GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider( pi, Api.GetCurrentCollection ); - GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider( pi, Api.GetDefaultCollection ); - GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider( pi, Api.GetInterfaceCollection ); - GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider( pi, Api.GetCharacterCollection ); - GetCollectionForType = Ipc.GetCollectionForType.Provider( pi, Api.GetCollectionForType ); - SetCollectionForType = Ipc.SetCollectionForType.Provider( pi, Api.SetCollectionForType ); - GetCollectionForObject = Ipc.GetCollectionForObject.Provider( pi, Api.GetCollectionForObject ); - SetCollectionForObject = Ipc.SetCollectionForObject.Provider( pi, Api.SetCollectionForObject ); - GetChangedItems = Ipc.GetChangedItems.Provider( pi, Api.GetChangedItemsForCollection ); + GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections); + GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection); + GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection); + GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection); + GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection); + GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType); + SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType); + GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject); + SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject); + GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection); // Meta - GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider( pi, Api.GetPlayerMetaManipulations ); - GetMetaManipulations = Ipc.GetMetaManipulations.Provider( pi, Api.GetMetaManipulations ); - GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider( pi, Api.GetGameObjectMetaManipulations ); + GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations); + GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations); + GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations); // Mods - GetMods = Ipc.GetMods.Provider( pi, Api.GetModList ); - ReloadMod = Ipc.ReloadMod.Provider( pi, Api.ReloadMod ); - AddMod = Ipc.AddMod.Provider( pi, Api.AddMod ); - DeleteMod = Ipc.DeleteMod.Provider( pi, Api.DeleteMod ); - GetModPath = Ipc.GetModPath.Provider( pi, Api.GetModPath ); - SetModPath = Ipc.SetModPath.Provider( pi, Api.SetModPath ); - ModDeleted = Ipc.ModDeleted.Provider( pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent ); - ModAdded = Ipc.ModAdded.Provider( pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent ); - ModMoved = Ipc.ModMoved.Provider( pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent ); + GetMods = Ipc.GetMods.Provider(pi, Api.GetModList); + ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod); + InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod); + AddMod = Ipc.AddMod.Provider(pi, Api.AddMod); + DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod); + GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath); + SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath); + ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent); + ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent); + ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent); // ModSettings - GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider( pi, Api.GetAvailableModSettings ); - GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider( pi, Api.GetCurrentModSettings ); - TryInheritMod = Ipc.TryInheritMod.Provider( pi, Api.TryInheritMod ); - TrySetMod = Ipc.TrySetMod.Provider( pi, Api.TrySetMod ); - TrySetModPriority = Ipc.TrySetModPriority.Provider( pi, Api.TrySetModPriority ); - TrySetModSetting = Ipc.TrySetModSetting.Provider( pi, Api.TrySetModSetting ); - TrySetModSettings = Ipc.TrySetModSettings.Provider( pi, Api.TrySetModSettings ); - ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, + GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings); + GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings); + TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod); + TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod); + TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority); + TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting); + TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings); + ModSettingChanged = Ipc.ModSettingChanged.Provider(pi, () => Api.ModSettingChanged += ModSettingChangedEvent, - () => Api.ModSettingChanged -= ModSettingChangedEvent ); - CopyModSettings = Ipc.CopyModSettings.Provider( pi, Api.CopyModSettings ); + () => Api.ModSettingChanged -= ModSettingChangedEvent); + CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings); // Temporary - CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider( pi, Api.CreateTemporaryCollection ); - RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider( pi, Api.RemoveTemporaryCollection ); - CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider( pi, Api.CreateNamedTemporaryCollection ); - RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider( pi, Api.RemoveTemporaryCollectionByName ); - AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider( pi, Api.AssignTemporaryCollection ); - AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider( pi, Api.AddTemporaryModAll ); - AddTemporaryMod = Ipc.AddTemporaryMod.Provider( pi, Api.AddTemporaryMod ); - RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); - RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); + CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection); + RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection); + CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection); + RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName); + AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection); + AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll); + AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod); + RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); + RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - Tester = new IpcTester( pi, this, modManager ); + Tester = new IpcTester(pi, this, modManager); Initialized.Invoke(); } @@ -295,6 +300,7 @@ public class PenumbraIpcProviders : IDisposable // Mods GetMods.Dispose(); ReloadMod.Dispose(); + InstallMod.Dispose(); AddMod.Dispose(); DeleteMod.Dispose(); GetModPath.Dispose(); @@ -332,46 +338,46 @@ public class PenumbraIpcProviders : IDisposable // Wrappers private int DeprecatedVersion() { - Penumbra.Log.Warning( $"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead." ); + Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead."); return Api.ApiVersion.Breaking; } - private void OnClick( MouseButton click, object? item ) + private void OnClick(MouseButton click, object? item) { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ChangedItemClick.Invoke( click, type, id ); + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); + ChangedItemClick.Invoke(click, type, id); } - private void OnTooltip( object? item ) + private void OnTooltip(object? item) { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ChangedItemTooltip.Invoke( type, id ); + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); + ChangedItemTooltip.Invoke(type, id); } - private void EnabledChangeEvent( bool value ) - => EnabledChange.Invoke( value ); + private void EnabledChangeEvent(bool value) + => EnabledChange.Invoke(value); - private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) - => GameObjectRedrawn.Invoke( objectAddress, objectTableIndex ); + private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex) + => GameObjectRedrawn.Invoke(objectAddress, objectTableIndex); - private void CreatingCharacterBaseEvent( IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData ) - => CreatingCharacterBase.Invoke( gameObject, collectionName, modelId, customize, equipData ); + private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData) + => CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData); - private void CreatedCharacterBaseEvent( IntPtr gameObject, string collectionName, IntPtr drawObject ) - => CreatedCharacterBase.Invoke( gameObject, collectionName, drawObject ); + private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject) + => CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject); - private void GameObjectResourceResolvedEvent( IntPtr gameObject, string gamePath, string localPath ) - => GameObjectResourcePathResolved.Invoke( gameObject, gamePath, localPath ); + private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath) + => GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath); - private void ModSettingChangedEvent( ModSettingChange type, string collection, string mod, bool inherited ) - => ModSettingChanged.Invoke( type, collection, mod, inherited ); + private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited) + => ModSettingChanged.Invoke(type, collection, mod, inherited); - private void ModDeletedEvent( string name ) - => ModDeleted.Invoke( name ); + private void ModDeletedEvent(string name) + => ModDeleted.Invoke(name); - private void ModAddedEvent( string name ) - => ModAdded.Invoke( name ); + private void ModAddedEvent(string name) + => ModAdded.Invoke(name); - private void ModMovedEvent( string from, string to ) - => ModMoved.Invoke( from, to ); -} \ No newline at end of file + private void ModMovedEvent(string from, string to) + => ModMoved.Invoke(from, to); +} diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index a1478e5b..dff1c921 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -39,10 +39,10 @@ public partial class TexToolsImporter : IDisposable private readonly ModEditor _editor; private readonly ModManager _modManager; - public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, + public TexToolsImporter( int count, IEnumerable< FileInfo > modPackFiles, Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager) { - _baseDirectory = baseDirectory; + _baseDirectory = modManager.BasePath; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _modPackFiles = modPackFiles; _config = config; diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 72162b95..72680091 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -18,10 +18,10 @@ public class ModBackup public readonly string Name; public readonly bool Exists; - public ModBackup(ExportManager exportManager, Mod mod) + public ModBackup(ModExportManager modExportManager, Mod mod) { _mod = mod; - Name = Path.Combine(exportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; + Name = Path.Combine(modExportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; Exists = File.Exists(Name); } diff --git a/Penumbra/Mods/Manager/ExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs similarity index 92% rename from Penumbra/Mods/Manager/ExportManager.cs rename to Penumbra/Mods/Manager/ModExportManager.cs index 3d091105..6396e1f9 100644 --- a/Penumbra/Mods/Manager/ExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -4,7 +4,7 @@ using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ExportManager : IDisposable +public class ModExportManager : IDisposable { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -15,7 +15,7 @@ public class ExportManager : IDisposable public DirectoryInfo ExportDirectory => _exportDirectory ?? _modManager.BasePath; - public ExportManager(Configuration config, CommunicatorService communicator, ModManager modManager) + public ModExportManager(Configuration config, CommunicatorService communicator, ModManager modManager) { _config = config; _communicator = communicator; @@ -89,4 +89,4 @@ public class ExportManager : IDisposable new ModBackup(this, mod).Move(null, newDirectory.Name); mod.ModPath = newDirectory; } -} +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs new file mode 100644 index 00000000..84aa2c7f --- /dev/null +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.Import; + +namespace Penumbra.Mods.Manager; + +public class ModImportManager : IDisposable +{ + private readonly ModManager _modManager; + private readonly Configuration _config; + private readonly ModEditor _modEditor; + + private readonly ConcurrentQueue _modsToUnpack = new(); + + /// Mods need to be added thread-safely outside of iteration. + private readonly ConcurrentQueue _modsToAdd = new(); + + private TexToolsImporter? _import; + + public ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) + { + _modManager = modManager; + _config = config; + _modEditor = modEditor; + } + + public void TryUnpacking() + { + if (Importing || !_modsToUnpack.TryDequeue(out var newMods)) + return; + + var files = newMods.Where(s => + { + if (File.Exists(s)) + return true; + + Penumbra.ChatService.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", "Warning", + NotificationType.Warning); + return false; + + }).Select(s => new FileInfo(s)).ToArray(); + + if (files.Length == 0) + return; + + _import = new TexToolsImporter(files.Length, files, AddNewMod, _config, _modEditor, _modManager); + } + + public bool Importing + => _import != null; + + public bool IsImporting([NotNullWhen(true)] out TexToolsImporter? importer) + { + importer = _import; + return _import != null; + } + + public void AddUnpack(IEnumerable paths) + => _modsToUnpack.Enqueue(paths.ToArray()); + + public void AddUnpack(params string[] paths) + => _modsToUnpack.Enqueue(paths); + + public void ClearImport() + { + _import?.Dispose(); + _import = null; + } + + + public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod) + { + if (!_modsToAdd.TryDequeue(out var directory)) + { + mod = null; + return false; + } + + _modManager.AddMod(directory); + mod = _modManager.LastOrDefault(); + return mod != null && mod.ModPath == directory; + } + + public void Dispose() + { + ClearImport(); + _modsToAdd.Clear(); + _modsToUnpack.Clear(); + } + + /// + /// Clean up invalid directory if necessary. + /// Add successfully extracted mods. + /// + private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) + { + if (error != null) + { + if (dir != null && Directory.Exists(dir.FullName)) + try + { + Directory.Delete(dir.FullName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); + } + + if (error is not OperationCanceledException) + Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); + } + else if (dir != null) + { + _modsToAdd.Enqueue(dir); + } + } +} diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index f7e2da03..58dfd7ac 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -21,8 +21,8 @@ using Penumbra.UI.Tabs; using Penumbra.Util; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; -using Penumbra.Mods.Manager; - +using Penumbra.Mods.Manager; + namespace Penumbra; public class PenumbraNew @@ -61,7 +61,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton(); - + // Add Game Services services.AddSingleton() .AddSingleton() @@ -75,8 +75,8 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - + .AddSingleton(); + // Add PathResolver services.AddSingleton() .AddSingleton(); @@ -98,7 +98,8 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -106,7 +107,7 @@ public class PenumbraNew services.AddSingleton() .AddSingleton() .AddSingleton(); - + // Add Path Resolver services.AddSingleton() .AddSingleton() @@ -116,7 +117,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); // Add Interface services.AddSingleton() @@ -131,6 +132,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -147,7 +149,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton(); - + // Add Mod Editor services.AddSingleton() .AddSingleton() @@ -156,7 +158,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); // Add API services.AddSingleton() diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs new file mode 100644 index 00000000..f11cd492 --- /dev/null +++ b/Penumbra/UI/ImportPopup.cs @@ -0,0 +1,64 @@ +using System; +using System.Numerics; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.Import.Structs; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI; + +/// Draw the progress information for import. +public sealed class ImportPopup : Window +{ + private readonly ModImportManager _modImportManager; + + public ImportPopup(ModImportManager modImportManager) + : base("Penumbra Import Status", + ImGuiWindowFlags.Modal + | ImGuiWindowFlags.Popup + | ImGuiWindowFlags.NoCollapse + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoBringToFrontOnFocus, true) + { + _modImportManager = modImportManager; + IsOpen = true; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = Vector2.Zero, + MaximumSize = Vector2.Zero, + }; + } + + public override void Draw() + { + _modImportManager.TryUnpacking(); + if (!_modImportManager.IsImporting(out var import)) + return; + + ImGui.OpenPopup("##importPopup"); + + var display = ImGui.GetIO().DisplaySize; + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 8; + var size = new Vector2(width * 2, height); + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup("##importPopup", ImGuiWindowFlags.Modal); + using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) + { + if (child) + import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); + } + + if ((import.State != ImporterState.Done || !ImGui.Button("Close", -Vector2.UnitX)) + && (import.State == ImporterState.Done || !import.DrawCancelButton(-Vector2.UnitX))) + return; + + _modImportManager.ClearImport(); + } +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c78aa099..b6099c64 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; @@ -13,12 +10,9 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; -using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.Import; -using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -27,7 +21,7 @@ using Penumbra.Util; namespace Penumbra.UI.ModsTab; -public sealed partial class ModFileSystemSelector : FileSystemSelector +public sealed class ModFileSystemSelector : FileSystemSelector { private readonly CommunicatorService _communicator; private readonly ChatService _chat; @@ -37,18 +31,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector _modUnpackQueue = new Queue(); - - private TexToolsImporter? _import; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - - private uint _infoPopupId = 0; + private readonly ModImportManager _modImportManager; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat, - ModEditor modEditor, ModCacheManager modCaches) + ModCacheManager modCaches, ModImportManager modImportManager) : base(fileSystem, DalamudServices.KeyState, HandleException) { _communicator = communicator; @@ -58,8 +47,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Add an import mods button that opens a file selector. private void AddImportModButton(Vector2 size) { - _infoPopupId = ImGui.GetID("Import Status"); - ExternalImportListener(); var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); @@ -251,105 +229,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), - AddNewMod, _config, _modEditor, _modManager); - ImGui.OpenPopup(_infoPopupId); + + _modImportManager.AddUnpack(f); }, 0, modPath, _config.AlwaysOpenDefaultImport); } - private void ExternalImportListener() - { - if (_modUnpackQueue.Count > 0) - { - // Attempt to avoid triggering if other mods are already unpacking - if (!_modsCurrentlyUnpacking) - { - string modPackagePath = _modUnpackQueue.Dequeue(); - if (File.Exists(modPackagePath)) - { - _modsCurrentlyUnpacking = true; - var modPath = !_config.AlwaysOpenDefaultImport ? null - : _config.DefaultModImportPath.Length > 0 ? _config.DefaultModImportPath - : _config.ModDirectory.Length > 0 ? _config.ModDirectory : null; - - _import = new TexToolsImporter(Penumbra.ModManager.BasePath, 1, new List() { new FileInfo(modPackagePath) }, AddNewMod, - _config, _modEditor, _modManager); - ImGui.OpenPopup(_infoPopupId); - } - } - } - } - - /// - /// Unpacks the specified standalone package - /// - /// The package to unpack - public void ImportStandaloneModPackage(string modPackagePath) - { - _modUnpackQueue.Enqueue(modPackagePath); - } - - /// Draw the progress information for import. - private void DrawInfoPopup() - { - var display = ImGui.GetIO().DisplaySize; - var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 8; - var size = new Vector2(width * 2, height); - ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); - ImGui.SetNextWindowSize(size); - var infoPopupId = ImGui.GetID("Import Status"); - using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); - if (_import == null || !popup.Success) - return; - - using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) - { - if (child) - _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); - } - - if ((_import.State != ImporterState.Done || !ImGui.Button("Close", -Vector2.UnitX)) - && (_import.State == ImporterState.Done || !_import.DrawCancelButton(-Vector2.UnitX))) - return; - - _import?.Dispose(); - _import = null; - ImGui.CloseCurrentPopup(); - } - - /// Mods need to be added thread-safely outside of iteration. - private readonly ConcurrentQueue _modsToAdd = new(); - - /// - /// Clean up invalid directory if necessary. - /// Add successfully extracted mods. - /// - private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) - { - if (error != null) - { - if (dir != null && Directory.Exists(dir.FullName)) - try - { - Directory.Delete(dir.FullName, true); - } - catch (Exception e) - { - Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); - } - - if (error is not OperationCanceledException) - Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); - } - else if (dir != null) - { - _modsToAdd.Enqueue(dir); - } - _modsCurrentlyUnpacking = false; - } - private void DeleteModButton(Vector2 size) { var keys = _config.DeleteModModifier.IsActive(); @@ -573,7 +457,6 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Label @@ -150,7 +150,7 @@ public class ModPanelEditTab : ITab private void BackupButtons(Vector2 buttonSize) { - var backup = new ModBackup(_exportManager, _mod); + var backup = new ModBackup(_modExportManager, _mod); var tt = ModBackup.CreatingBackup ? "Already exporting a mod." : backup.Exists diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 8d51ec65..d29ce49a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -32,7 +32,7 @@ public class SettingsTab : ITab private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; - private readonly ExportManager _exportManager; + private readonly ModExportManager _modExportManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; @@ -40,7 +40,7 @@ public class SettingsTab : ITab public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud, ExportManager exportManager) + ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager) { _config = config; _fontReloader = fontReloader; @@ -52,7 +52,7 @@ public class SettingsTab : ITab _characterUtility = characterUtility; _residentResources = residentResources; _dalamud = dalamud; - _exportManager = exportManager; + _modExportManager = modExportManager; } public void DrawHeader() @@ -116,7 +116,7 @@ public class SettingsTab : ITab /// Check a potential new root directory for validity and return the button text and whether it is valid. private static (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) - { + { static bool IsSubPathOf(string basePath, string subPath) { if (basePath.Length == 0) @@ -555,7 +555,7 @@ public class SettingsTab : ITab _tempExportDirectory = tmp; if (ImGui.IsItemDeactivatedAfterEdit()) - _exportManager.UpdateExportDirectory(_tempExportDirectory); + _modExportManager.UpdateExportDirectory(_tempExportDirectory); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize, @@ -569,7 +569,7 @@ public class SettingsTab : ITab _fileDialog.OpenFolderPicker("Choose Default Export Directory", (b, s) => { if (b) - _exportManager.UpdateExportDirectory(s); + _modExportManager.UpdateExportDirectory(s); }, startDir, false); } diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index f2f0a8b6..4756accf 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -16,17 +16,17 @@ public class PenumbraWindowSystem : IDisposable public readonly PenumbraChangelog Changelog; public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, - LaunchButton _, - ModEditWindow editWindow, FileDialogService fileDialog) + LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); + _windowSystem.AddWindow(importPopup); _uiBuilder.OpenConfigUi += Window.Toggle; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; From 4294b18bcb5ccab974393f08fc2c3af0d6d06ec7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Apr 2023 02:42:27 +0200 Subject: [PATCH 0868/2451] Fix issue with tutorial window and double necessary click --- Penumbra/UI/ImportPopup.cs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index f11cd492..0a5f160e 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -14,16 +14,12 @@ public sealed class ImportPopup : Window private readonly ModImportManager _modImportManager; public ImportPopup(ModImportManager modImportManager) - : base("Penumbra Import Status", - ImGuiWindowFlags.Modal - | ImGuiWindowFlags.Popup - | ImGuiWindowFlags.NoCollapse + : base("Penumbra Import Status", + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoInputs - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoBringToFrontOnFocus, true) + | ImGuiWindowFlags.NoInputs, true) { _modImportManager = modImportManager; IsOpen = true; @@ -40,7 +36,9 @@ public sealed class ImportPopup : Window if (!_modImportManager.IsImporting(out var import)) return; - ImGui.OpenPopup("##importPopup"); + const string importPopup = "##importPopup"; + if (!ImGui.IsPopupOpen(importPopup)) + ImGui.OpenPopup(importPopup); var display = ImGui.GetIO().DisplaySize; var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); @@ -48,17 +46,17 @@ public sealed class ImportPopup : Window var size = new Vector2(width * 2, height); ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup("##importPopup", ImGuiWindowFlags.Modal); + using var popup = ImRaii.Popup(importPopup, ImGuiWindowFlags.Modal); using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { if (child) import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); } - if ((import.State != ImporterState.Done || !ImGui.Button("Close", -Vector2.UnitX)) - && (import.State == ImporterState.Done || !import.DrawCancelButton(-Vector2.UnitX))) - return; - - _modImportManager.ClearImport(); + var terminate = import.State == ImporterState.Done + ? ImGui.Button("Close", -Vector2.UnitX) + : import.DrawCancelButton(-Vector2.UnitX); + if (terminate) + _modImportManager.ClearImport(); } } From c527d19117711e4d8c79b46bf33bb5a209428012 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 10 Apr 2023 00:31:29 +0200 Subject: [PATCH 0869/2451] Remove some static dependencies. --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 15 ++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 10 +++++++--- Penumbra/Penumbra.cs | 15 +-------------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 7 ++++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 18 ++++++++++-------- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 97398d87..1f1cecfb 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -33,7 +34,7 @@ public static class EquipmentSwap : Array.Empty< EquipSlot >(); } - public static Item[] CreateTypeSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, + public static Item[] CreateTypeSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo ) { LookupItem( itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom ); @@ -43,7 +44,7 @@ public static class EquipmentSwap throw new ItemSwap.InvalidItemTypeException(); } - var ( imcFileFrom, variants, affectedItems ) = GetVariants( slotFrom, idFrom, idTo, variantFrom ); + var ( imcFileFrom, variants, affectedItems ) = GetVariants( identifier, slotFrom, idFrom, idTo, variantFrom ); var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default ); var imcFileTo = new ImcFile( imcManip ); var skipFemale = false; @@ -96,7 +97,7 @@ public static class EquipmentSwap return affectedItems; } - public static Item[] CreateItemSwap( List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, + public static Item[] CreateItemSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, Item itemTo, bool rFinger = true, bool lFinger = true ) { // Check actual ids, variants and slots. We only support using the same slot. @@ -122,7 +123,7 @@ public static class EquipmentSwap var affectedItems = Array.Empty< Item >(); foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) { - ( var imcFileFrom, var variants, affectedItems ) = GetVariants( slot, idFrom, idTo, variantFrom ); + ( var imcFileFrom, var variants, affectedItems ) = GetVariants( identifier, slot, idFrom, idTo, variantFrom ); var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); var imcFileTo = new ImcFile( imcManip ); @@ -250,7 +251,7 @@ public static class EquipmentSwap variant = ( byte )( ( Quad )i.ModelMain ).B; } - private static (ImcFile, byte[], Item[]) GetVariants( EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom ) + private static (ImcFile, byte[], Item[]) GetVariants( IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom ) { var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default ); var imc = new ImcFile( entry ); @@ -258,12 +259,12 @@ public static class EquipmentSwap byte[] variants; if( idFrom.Value == idTo.Value ) { - items = Penumbra.Identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); + items = identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); variants = new[] { variantFrom }; } else { - items = Penumbra.Identifier.Identify( slotFrom.IsEquipment() + items = identifier.Identify( slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index c6f1b607..2ca70cda 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -8,12 +8,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.GameData; using Penumbra.Mods.Manager; namespace Penumbra.Mods.ItemSwap; public class ItemSwapContainer { + private readonly IObjectIdentifier _identifier; + private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); private HashSet< MetaManipulation > _modManipulations = new(); @@ -109,8 +112,9 @@ public class ItemSwapContainer } } - public ItemSwapContainer() + public ItemSwapContainer(IObjectIdentifier identifier) { + _identifier = identifier; LoadMod( null, null ); } @@ -129,7 +133,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); + var ret = EquipmentSwap.CreateItemSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); Loaded = true; return ret; } @@ -138,7 +142,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateTypeSwap( Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + var ret = EquipmentSwap.CreateTypeSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); Loaded = true; return ret; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6265e3fd..6dbcd3a8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,10 +1,8 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; -using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; using Lumina.Excel.GeneratedSheets; @@ -12,16 +10,13 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Log; -using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.Interop; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -48,14 +43,12 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; public static ModCacheManager ModCaches { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!; @@ -65,9 +58,6 @@ public class Penumbra : IDalamudPlugin // TODO public static ValidityChecker ValidityChecker { get; private set; } = null!; - public static PerformanceTracker Performance { get; private set; } = null!; - - public readonly PathResolver PathResolver; public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; public HttpApi HttpApi = null!; @@ -86,12 +76,10 @@ public class Penumbra : IDalamudPlugin ChatService = _tmp.Services.GetRequiredService(); Filenames = _tmp.Services.GetRequiredService(); SaveService = _tmp.Services.GetRequiredService(); - Performance = _tmp.Services.GetRequiredService(); ValidityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); Config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); MetaFileManager = _tmp.Services.GetRequiredService(); Framework = _tmp.Services.GetRequiredService(); Actors = _tmp.Services.GetRequiredService().AwaitedService; @@ -107,11 +95,10 @@ public class Penumbra : IDalamudPlugin ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ResourceLoader = _tmp.Services.GetRequiredService(); ModCaches = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { - PathResolver = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); } SetupInterface(); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index cc5e7cb6..57c52fd3 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -29,16 +29,17 @@ public class ItemSwapTab : IDisposable, ITab private readonly ItemService _itemService; private readonly CollectionManager _collectionManager; private readonly ModManager _modManager; - private readonly Configuration _config; + private readonly Configuration _config; public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, Configuration config) + ModManager modManager, Configuration config, IdentifierService identifier) { _communicator = communicator; _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; _config = config; + _swapData = new ItemSwapContainer(identifier.AwaitedService); _selectors = new Dictionary { @@ -149,7 +150,7 @@ public class ItemSwapTab : IDisposable, ITab private ItemSelector? _weaponSource; private ItemSelector? _weaponTarget; private readonly WeaponSelector _slotSelector = new(); - private readonly ItemSwapContainer _swapData = new(); + private readonly ItemSwapContainer _swapData; private Mod? _mod; private ModSettings? _modSettings; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 59a78306..68265a43 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -25,11 +25,12 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly ModEditor _editor; - private readonly ModCacheManager _modCaches; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly DataManager _gameData; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly ModCacheManager _modCaches; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly DataManager _gameData; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -69,7 +70,7 @@ public partial class ModEditWindow : Window, IDisposable public override void PreDraw() { - using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); + using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow); var sb = new StringBuilder(256); @@ -127,7 +128,7 @@ public partial class ModEditWindow : Window, IDisposable public override void Draw() { - using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); + using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow); using var tabBar = ImRaii.TabBar("##tabs"); if (!tabBar) @@ -491,10 +492,11 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } - public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches) : base(WindowBaseLabel) { + _performance = performance; _itemSwapTab = itemSwapTab; _config = config; _editor = editor; From d908f22a1714bf89a7a6452339138cbb60d3e1fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 10 Apr 2023 15:06:16 +0200 Subject: [PATCH 0870/2451] Maybe prevent weird GetName crashes. --- Penumbra/Api/IpcTester.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 467bfa7e..eb9dbe07 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -552,8 +552,8 @@ public class IpcTester : IDisposable private static unsafe string GetObjectName(IntPtr gameObject) { var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; - var name = obj != null ? obj->GetName() : null; - return name != null ? new ByteString(name).ToString() : "Unknown"; + var name = obj != null ? obj->Name : null; + return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; } } From 51ce6d1038f700b18fba19245ab4ba0754a7bf9f Mon Sep 17 00:00:00 2001 From: Caraxi Date: Tue, 11 Apr 2023 07:17:52 +0930 Subject: [PATCH 0871/2451] Fix double `by` in mod name display --- Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs index 4893c6da..1075c9cd 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs @@ -176,7 +176,7 @@ public partial class ConfigWindow } // Author - var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; + var author = _mod.Author.IsEmpty ? string.Empty : $"{_mod.Author}"; if( author != _modAuthor ) { _modAuthor = author; From 3f33bab296e6a4879e14cc18746ac9704562b323 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Apr 2023 11:28:56 +0200 Subject: [PATCH 0872/2451] Lots of collection progress. --- Penumbra/Api/PenumbraApi.cs | 74 ++---- .../Manager/CollectionCacheManager.cs | 71 +++++- .../Collections/Manager/CollectionEditor.cs | 230 ++++++++++++++++++ .../Collections/Manager/CollectionManager.cs | 4 +- .../Collections/Manager/InheritanceManager.cs | 114 ++++++++- Penumbra/Collections/ModCollection.Cache.cs | 63 +---- Penumbra/Collections/ModCollection.Changes.cs | 138 ----------- Penumbra/Collections/ModCollection.File.cs | 6 +- .../Collections/ModCollection.Inheritance.cs | 170 ------------- .../Collections/ModCollection.Migration.cs | 1 - Penumbra/Collections/ModCollection.cs | 118 ++++----- Penumbra/CommandHandler.cs | 92 +++---- Penumbra/Penumbra.cs | 2 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/Services/CommunicatorService.cs | 64 ++++- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 26 +- .../Collections.InheritanceUi.cs | 38 +-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 39 ++- Penumbra/UI/ModsTab/ModPanelHeader.cs | 6 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 41 ++-- Penumbra/UI/Tabs/ModsTab.cs | 2 +- 22 files changed, 666 insertions(+), 636 deletions(-) create mode 100644 Penumbra/Collections/Manager/CollectionEditor.cs delete mode 100644 Penumbra/Collections/ModCollection.Changes.cs delete mode 100644 Penumbra/Collections/ModCollection.Inheritance.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 63ff92f4..40b135e5 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -31,8 +31,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (int, int) ApiVersion => (4, 19); - private readonly Dictionary _delegates = new(); - public event Action? PreSettingsPanelDraw; public event Action? PostSettingsPanelDraw; @@ -110,10 +108,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi private CollectionResolver _collectionResolver; private CutsceneService _cutsceneService; private ModImportManager _modImportManager; + private CollectionEditor _collectionEditor; public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager) + TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor) { _communicator = communicator; _penumbra = penumbra; @@ -127,17 +126,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi _actors = actors; _collectionResolver = collectionResolver; _cutsceneService = cutsceneService; - _modImportManager = modImportManager; + _modImportManager = modImportManager; + _collectionEditor = collectionEditor; _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) ?.GetValue(_dalamud.GameData); - foreach (var collection in _collectionManager.Storage) - SubscribeToCollection(collection); - _communicator.CollectionChange.Subscribe(SubscribeToNewCollections); _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); + _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange, -1000); } public unsafe void Dispose() @@ -145,15 +143,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Valid) return; - foreach (var collection in _collectionManager.Storage) - { - if (_delegates.TryGetValue(collection, out var del)) - collection.ModSettingChanged -= del; - } - _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.CollectionChange.Unsubscribe(SubscribeToNewCollections); - _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); + _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); + _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _lumina = null; _communicator = null!; _penumbra = null!; @@ -167,6 +159,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _actors = null!; _collectionResolver = null!; _cutsceneService = null!; + _modImportManager = null!; + _collectionEditor = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -702,7 +696,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; - return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModInheritance(collection, mod, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) @@ -714,7 +708,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModState(collection, mod, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) @@ -726,7 +720,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModPriority(collection, mod, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, @@ -749,7 +743,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; - return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, @@ -789,7 +783,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -797,17 +791,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - var sourceModIdx = _modManager - .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index - ?? -1; - var targetModIdx = _modManager - .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index - ?? -1; + var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase)); + var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(collectionName)) foreach (var collection in _collectionManager.Storage) - collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); else if (_collectionManager.Storage.ByName(collectionName, out var collection)) - collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); else return PenumbraApiEc.CollectionMissing; @@ -1127,29 +1117,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } - private void SubscribeToCollection(ModCollection c) - { - var name = c.Name; - - void Del(ModSettingChange type, int idx, int _, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, name, idx >= 0 ? _modManager[idx].ModPath.Name : string.Empty, inherited); - - _delegates[c] = Del; - c.ModSettingChanged += Del; - } - - private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) - { - if (type != CollectionType.Inactive) - return; - - if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del)) - oldCollection.ModSettingChanged -= del; - - if (newCollection != null) - SubscribeToCollection(newCollection); - } - public void InvokePreSettingsPanel(string modDirectory) => PreSettingsPanelDraw?.Invoke(modDirectory); @@ -1166,4 +1133,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var b = ByteString.FromStringUnsafe(name, false); return _actors.AwaitedService.CreatePlayer(b, worldId); } + + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) + => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); } diff --git a/Penumbra/Collections/Manager/CollectionCacheManager.cs b/Penumbra/Collections/Manager/CollectionCacheManager.cs index a7ace6c1..56a6e3a3 100644 --- a/Penumbra/Collections/Manager/CollectionCacheManager.cs +++ b/Penumbra/Collections/Manager/CollectionCacheManager.cs @@ -6,6 +6,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -16,6 +18,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary _cache = new(); @@ -46,17 +49,23 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary Active => _cache.Keys.Where(c => c.Index > ModCollection.Empty.Index); - public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator) + public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator, CharacterUtility characterUtility) { - _active = active; - _communicator = communicator; + _active = active; + _communicator = communicator; + _characterUtility = characterUtility; _communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange); + _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); CreateNecessaryCaches(); + + if (!_characterUtility.Ready) + _characterUtility.LoadingFinished += IncrementCounters; } public void Dispose() @@ -66,6 +75,9 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary @@ -170,4 +182,57 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary Increment the counter to ensure new files are loaded after applying meta changes.
+ private void IncrementCounters() + { + foreach (var (collection, _) in _cache) + ++collection.ChangeCounter; + _characterUtility.LoadingFinished -= IncrementCounters; + } + + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) + { + if (collection._cache == null) + return; + + switch (type) + { + case ModSettingChange.Inheritance: + collection._cache.ReloadMod(mod!, true); + break; + case ModSettingChange.EnableState: + if (oldValue == 0) + collection._cache.AddMod(mod!, true); + else if (oldValue == 1) + collection._cache.RemoveMod(mod!, true); + else if (collection[mod!.Index].Settings?.Enabled == true) + collection._cache.ReloadMod(mod!, true); + else + collection._cache.RemoveMod(mod!, true); + + break; + case ModSettingChange.Priority: + if (collection._cache.Conflicts(mod!).Count > 0) + collection._cache.ReloadMod(mod!, true); + + break; + case ModSettingChange.Setting: + if (collection[mod!.Index].Settings?.Enabled == true) + collection._cache.ReloadMod(mod!, true); + + break; + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + collection._cache.FullRecalculation(collection == _active.Default); + break; + } + } + + /// + /// Inheritance changes are too big to check for relevance, + /// just recompute everything. + /// + private void OnCollectionInheritanceChange(ModCollection collection, bool _) + => collection._cache?.FullRecalculation(collection == _active.Default); } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs new file mode 100644 index 00000000..57a7b8f6 --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using OtterGui; +using Penumbra.Api.Enums; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections.Manager; + +public class CollectionEditor +{ + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + public CollectionEditor(SaveService saveService, CommunicatorService communicator) + { + _saveService = saveService; + _communicator = communicator; + } + + /// Enable or disable the mod inheritance of mod idx. + public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) + { + if (!FixInheritance(collection, mod, inherit)) + return false; + + InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? 0 : 1, 0); + return true; + } + + /// + /// Set the enabled state mod idx to newValue if it differs from the current enabled state. + /// If the mod is currently inherited, stop the inheritance. + /// + public bool SetModState(ModCollection collection, Mod mod, bool newValue) + { + var oldValue = collection._settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; + if (newValue == oldValue) + return false; + + var inheritance = FixInheritance(collection, mod, false); + collection._settings[mod.Index]!.Enabled = newValue; + InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0); + return true; + } + + /// Enable or disable the mod inheritance of every mod in mods. + public void SetMultipleModInheritances(ModCollection collection, IEnumerable mods, bool inherit) + { + if (!mods.Aggregate(false, (current, mod) => current | FixInheritance(collection, mod, inherit))) + return; + + InvokeChange(collection, ModSettingChange.MultiInheritance, null, -1, 0); + } + + /// + /// Set the enabled state of every mod in mods to the new value. + /// If the mod is currently inherited, stop the inheritance. + /// + public void SetMultipleModStates(ModCollection collection, IEnumerable mods, bool newValue) + { + var changes = false; + foreach (var mod in mods) + { + var oldValue = collection._settings[mod.Index]?.Enabled; + if (newValue == oldValue) + continue; + + FixInheritance(collection, mod, false); + collection._settings[mod.Index]!.Enabled = newValue; + changes = true; + } + + if (!changes) + return; + + InvokeChange(collection, ModSettingChange.MultiEnableState, null, -1, 0); + } + + /// + /// Set the priority of mod idx to newValue if it differs from the current priority. + /// If the mod is currently inherited, stop the inheritance. + /// + public bool SetModPriority(ModCollection collection, Mod mod, int newValue) + { + var oldValue = collection._settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0; + if (newValue == oldValue) + return false; + + var inheritance = FixInheritance(collection, mod, false); + collection._settings[mod.Index]!.Priority = newValue; + InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0); + return true; + } + + /// + /// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. + /// /// If the mod is currently inherited, stop the inheritance. + /// + public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, uint newValue) + { + var settings = collection._settings[mod.Index] != null + ? collection._settings[mod.Index]!.Settings + : collection[mod.Index].Settings?.Settings; + var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings; + if (oldValue == newValue) + return false; + + var inheritance = FixInheritance(collection, mod, false); + collection._settings[mod.Index]!.SetValue(mod, groupIdx, newValue); + InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx); + return true; + } + + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. + public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) + { + if (targetName.Length == 0 && targetMod == null || sourceName.Length == 0) + return false; + + // If the source mod exists, convert its settings to saved settings or null if its inheriting. + // If it does not exist, check unused settings. + // If it does not exist and has no unused settings, also use null. + ModSettings.SavedSettings? savedSettings = sourceMod != null + ? collection._settings[sourceMod.Index] != null + ? new ModSettings.SavedSettings(collection._settings[sourceMod.Index]!, sourceMod) + : null + : collection._unusedSettings.TryGetValue(sourceName, out var s) + ? s + : null; + + if (targetMod != null) + { + if (savedSettings != null) + { + // The target mod exists and the source settings are not inheriting, convert and fix the settings and copy them. + // This triggers multiple events. + savedSettings.Value.ToSettings(targetMod, out var settings); + SetModState(collection, targetMod, settings.Enabled); + SetModPriority(collection, targetMod, settings.Priority); + foreach (var (value, index) in settings.Settings.WithIndex()) + SetModSetting(collection, targetMod, index, value); + } + else + { + // The target mod exists, but the source is inheriting, set the target to inheriting. + // This triggers events. + SetModInheritance(collection, targetMod, true); + } + } + else + { + // The target mod does not exist. + // Either copy the unused source settings directly if they are not inheriting, + // or remove any unused settings for the target if they are inheriting. + if (savedSettings != null) + collection._unusedSettings[targetName] = savedSettings.Value; + else + collection._unusedSettings.Remove(targetName); + } + + return true; + } + + /// + /// Change one of the available mod settings for mod idx discerned by type. + /// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. + /// The setting will also be automatically fixed if it is invalid for that setting group. + /// For boolean parameters, newValue == 0 will be treated as false and != 0 as true. + /// + public bool ChangeModSetting(ModCollection collection, ModSettingChange type, Mod mod, int newValue, int groupIdx) + { + return type switch + { + ModSettingChange.Inheritance => SetModInheritance(collection, mod, newValue != 0), + ModSettingChange.EnableState => SetModState(collection, mod, newValue != 0), + ModSettingChange.Priority => SetModPriority(collection, mod, newValue), + ModSettingChange.Setting => SetModSetting(collection, mod, groupIdx, (uint)newValue), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + } + + /// + /// Set inheritance of a mod without saving, + /// to be used as an intermediary. + /// + private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit) + { + var settings = collection._settings[mod.Index]; + if (inherit == (settings == null)) + return false; + + collection._settings[mod.Index] = inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + return true; + } + + /// Queue saves and trigger changes for any non-inherited change in a collection, then trigger changes for all inheritors. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + { + _saveService.QueueSave(changedCollection); + _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); + RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); + } + + /// Trigger changes in all inherited collections. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + { + foreach (var directInheritor in directParent.DirectParentOf) + { + switch (type) + { + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + _communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); + break; + default: + if (directInheritor._settings[mod!.Index] == null) + _communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); + break; + } + + RecurseInheritors(directInheritor, type, mod, oldValue, groupIdx); + } + } +} diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index b124b7db..5e1c5781 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -7,14 +7,16 @@ public class CollectionManager public readonly InheritanceManager Inheritances; public readonly CollectionCacheManager Caches; public readonly TempCollectionManager Temp; + public readonly CollectionEditor Editor; public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances, - CollectionCacheManager caches, TempCollectionManager temp) + CollectionCacheManager caches, TempCollectionManager temp, CollectionEditor editor) { Storage = storage; Active = active; Inheritances = inheritances; Caches = caches; Temp = temp; + Editor = editor; } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 06227fdf..e5034ece 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,12 +1,34 @@ using System; +using System.Collections.Generic; +using System.Linq; using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Filesystem; using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Collections.Manager; +/// +/// ModCollections can inherit from an arbitrary number of other collections. +/// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. +/// Circular dependencies are resolved by distinctness. +/// public class InheritanceManager : IDisposable { + public enum ValidInheritance + { + Valid, + /// Can not inherit from self + Self, + /// Can not inherit from the empty collection + Empty, + /// Already inherited from + Contained, + /// Inheritance would lead to a circle. + Circle, + } + private readonly CollectionStorage _storage; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; @@ -26,22 +48,88 @@ public class InheritanceManager : IDisposable _communicator.CollectionChange.Unsubscribe(OnCollectionChange); } + /// Check whether a collection can be inherited from. + public static ValidInheritance CheckValidInheritance(ModCollection potentialInheritor, ModCollection? potentialParent) + { + if (potentialParent == null || ReferenceEquals(potentialParent, ModCollection.Empty)) + return ValidInheritance.Empty; + + if (ReferenceEquals(potentialParent, potentialInheritor)) + return ValidInheritance.Self; + + if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent)) + return ValidInheritance.Contained; + + if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor))) + return ValidInheritance.Circle; + + return ValidInheritance.Valid; + } + + /// + /// Add a new collection to the inheritance list. + /// We do not check if this collection would be visited before, + /// only that it is unique in the list itself. + /// + public bool AddInheritance(ModCollection inheritor, ModCollection parent) + => AddInheritance(inheritor, parent, true); + + /// Remove an existing inheritance from a collection. + public void RemoveInheritance(ModCollection inheritor, int idx) + { + var parent = inheritor.DirectlyInheritsFrom[idx]; + ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); + ((List)parent.DirectParentOf).Remove(inheritor); + _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); + RecurseInheritanceChanges(inheritor); + Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); + } + + /// Order in the inheritance list is relevant. + public void MoveInheritance(ModCollection inheritor, int from, int to) + { + if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) + return; + + _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); + RecurseInheritanceChanges(inheritor); + Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); + } + + /// + private bool AddInheritance(ModCollection inheritor, ModCollection parent, bool invokeEvent) + { + if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid) + return false; + + ((List)inheritor.DirectlyInheritsFrom).Add(parent); + ((List)parent.DirectParentOf).Add(inheritor); + if (invokeEvent) + { + _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); + RecurseInheritanceChanges(inheritor); + } + + Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances."); + return true; + } + /// /// Inheritances can not be setup before all collections are read, /// so this happens after reading the collections in the constructor, consuming the stored strings. /// private void ApplyInheritances() { - foreach (var (collection, inheritances, changes) in _storage.ConsumeInheritanceNames()) + foreach (var (collection, directParents, changes) in _storage.ConsumeInheritanceNames()) { var localChanges = changes; - foreach (var subCollection in inheritances) + foreach (var parent in directParents) { - if (collection.AddInheritance(subCollection, false)) + if (AddInheritance(collection, parent, false)) continue; localChanges = true; - Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", + Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {parent.Name}, removed.", "Warning", NotificationType.Warning); } @@ -55,14 +143,22 @@ public class InheritanceManager : IDisposable if (collectionType is not CollectionType.Inactive || old == null) return; - foreach (var inheritance in old.Inheritance) - old.ClearSubscriptions(inheritance); - foreach (var c in _storage) { - var inheritedIdx = c._inheritance.IndexOf(old); + var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old); if (inheritedIdx >= 0) - c.RemoveInheritance(inheritedIdx); + RemoveInheritance(c, inheritedIdx); + + ((List)c.DirectParentOf).Remove(old); + } + } + + private void RecurseInheritanceChanges(ModCollection newInheritor) + { + foreach (var inheritor in newInheritor.DirectParentOf) + { + _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); + RecurseInheritanceChanges(inheritor); } } } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 4a3c008c..5ed7f801 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -50,21 +50,12 @@ public class ModCollectionCache : IDisposable public ModCollectionCache( ModCollection collection ) { _collection = collection; - MetaManipulations = new MetaManager( _collection ); - _collection.ModSettingChanged += OnModSettingChange; - _collection.InheritanceChanged += OnInheritanceChange; - if( !Penumbra.CharacterUtility.Ready ) - { - Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; - } + MetaManipulations = new MetaManager( _collection ); } public void Dispose() { MetaManipulations.Dispose(); - _collection.ModSettingChanged -= OnModSettingChange; - _collection.InheritanceChanged -= OnInheritanceChange; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; } // Resolve a given game path according to this collection. @@ -133,58 +124,6 @@ public class ModCollectionCache : IDisposable return ret; } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) - { - switch( type ) - { - case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - break; - case ModSettingChange.EnableState: - if( oldValue == 0 ) - { - 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 ); - } - - break; - case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.MultiInheritance: - case ModSettingChange.MultiEnableState: - FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); - break; - } - } - - // Inheritance changes are too big to check for relevance, - // just recompute everything. - private void OnInheritanceChange( bool _ ) - => FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); - public void FullRecalculation(bool isDefault) { ResolvedFiles.Clear(); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs deleted file mode 100644 index 44c622a1..00000000 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.Linq; -using Penumbra.Api.Enums; - -namespace Penumbra.Collections; - -public partial class ModCollection -{ - // If the change type is a bool, oldValue will be 1 for true and 0 for false. - // optionName will only be set for type == Setting. - public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ); - public event ModSettingChangeDelegate ModSettingChanged; - - // Enable or disable the mod inheritance of mod idx. - public bool SetModInheritance( int idx, bool inherit ) - { - if (!FixInheritance(idx, inherit)) - return false; - - ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); - return true; - - } - - // Set the enabled state mod idx to newValue if it differs from the current enabled state. - // If mod idx is currently inherited, stop the inheritance. - public bool SetModState( int idx, bool newValue ) - { - var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; - if (newValue == oldValue) - return false; - - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Enabled = newValue; - ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); - return true; - - } - - // Enable or disable the mod inheritance of every mod in mods. - public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) - { - if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) - ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, 0, false ); - } - - // Set the enabled state of every mod in mods to the new value. - // If the mod is currently inherited, stop the inheritance. - public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue ) - { - var changes = false; - foreach( var mod in mods ) - { - var oldValue = _settings[ mod.Index ]?.Enabled; - if (newValue == oldValue) - continue; - - FixInheritance( mod.Index, false ); - _settings[ mod.Index ]!.Enabled = newValue; - changes = true; - } - - if( changes ) - { - ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, 0, false ); - } - } - - // Set the priority of mod idx to newValue if it differs from the current priority. - // If mod idx is currently inherited, stop the inheritance. - public bool SetModPriority( int idx, int newValue ) - { - var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; - if (newValue == oldValue) - return false; - - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Priority = newValue; - ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); - return true; - - } - - // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. - // If mod idx is currently inherited, stop the inheritance. - public bool SetModSetting( int idx, int groupIdx, uint newValue ) - { - var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; - var oldValue = settings?[ groupIdx ] ?? Penumbra.ModManager[idx].Groups[groupIdx].DefaultSettings; - if (oldValue == newValue) - return false; - - var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); - ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); - return true; - - } - - // Change one of the available mod settings for mod idx discerned by type. - // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. - // The setting will also be automatically fixed if it is invalid for that setting group. - // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - public bool ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) - { - return type switch - { - ModSettingChange.Inheritance => SetModInheritance( idx, newValue != 0 ), - ModSettingChange.EnableState => SetModState( idx, newValue != 0 ), - ModSettingChange.Priority => SetModPriority( idx, newValue ), - ModSettingChange.Setting => SetModSetting( idx, groupIdx, ( uint )newValue ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - } - - // Set inheritance of a mod without saving, - // to be used as an intermediary. - private bool FixInheritance( int idx, bool inherit ) - { - var settings = _settings[ idx ]; - if( inherit == ( settings == null ) ) - return false; - - _settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager[ idx ] ); - return true; - } - - private void SaveOnChange( ModSettingChange _1, int _2, int _3, int _4, bool inherited ) - => SaveOnChange( inherited ); - - private void SaveOnChange( bool inherited ) - { - if( !inherited ) - Penumbra.SaveService.QueueSave(this); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index dd632f71..f688b5ca 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -32,7 +32,7 @@ public partial class ModCollection : ISavable // Custom deserialization that is converted with the constructor. var settings = obj[nameof(Settings)]?.ToObject>() ?? new Dictionary(); - inheritance = obj[nameof(Inheritance)]?.ToObject>() ?? (IReadOnlyList)Array.Empty(); + inheritance = obj["Inheritance"]?.ToObject>() ?? (IReadOnlyList)Array.Empty(); return new ModCollection(name, version, settings); } @@ -86,8 +86,8 @@ public partial class ModCollection : ISavable j.WriteEndObject(); // Inherit by collection name. - j.WritePropertyName(nameof(Inheritance)); - x.Serialize(j, Inheritance.Select(c => c.Name)); + j.WritePropertyName("Inheritance"); + x.Serialize(j, DirectlyInheritsFrom.Select(c => c.Name)); j.WriteEndObject(); } } diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs deleted file mode 100644 index 4c694f88..00000000 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ /dev/null @@ -1,170 +0,0 @@ -using OtterGui.Filesystem; -using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.Linq; -using Penumbra.Api.Enums; - -namespace Penumbra.Collections; - -// ModCollections can inherit from an arbitrary number of other collections. -// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. -// Circular dependencies are resolved by distinctness. -public partial class ModCollection -{ - // A change in inheritance usually requires complete recomputation. - // The bool signifies whether the change was in an already inherited collection. - public event Action< bool > InheritanceChanged; - - internal readonly List< ModCollection > _inheritance = new(); - - public IReadOnlyList< ModCollection > Inheritance - => _inheritance; - - // Iterate over all collections inherited from in depth-first order. - // Skip already visited collections to avoid circular dependencies. - public IEnumerable< ModCollection > GetFlattenedInheritance() - => InheritedCollections( this ).Distinct(); - - // All inherited collections in application order without filtering for duplicates. - private static IEnumerable< ModCollection > InheritedCollections( ModCollection collection ) - => collection.Inheritance.SelectMany( InheritedCollections ).Prepend( collection ); - - // Reasons why a collection can not be inherited from. - public enum ValidInheritance - { - Valid, - Self, // Can not inherit from self - Empty, // Can not inherit from the empty collection - Contained, // Already inherited from - Circle, // Inheritance would lead to a circle. - } - - // Check whether a collection can be inherited from. - public ValidInheritance CheckValidInheritance( ModCollection? collection ) - { - if( collection == null || ReferenceEquals( collection, Empty ) ) - { - return ValidInheritance.Empty; - } - - if( ReferenceEquals( collection, this ) ) - { - return ValidInheritance.Self; - } - - if( _inheritance.Contains( collection ) ) - { - return ValidInheritance.Contained; - } - - if( InheritedCollections( collection ).Any( c => c == this ) ) - { - return ValidInheritance.Circle; - } - - return ValidInheritance.Valid; - } - - // Add a new collection to the inheritance list. - // We do not check if this collection would be visited before, - // only that it is unique in the list itself. - public bool AddInheritance( ModCollection collection, bool invokeEvent ) - { - if( CheckValidInheritance( collection ) != ValidInheritance.Valid ) - { - return false; - } - - _inheritance.Add( collection ); - // Changes in inherited collections may need to trigger further changes here. - collection.ModSettingChanged += OnInheritedModSettingChange; - collection.InheritanceChanged += OnInheritedInheritanceChange; - if( invokeEvent ) - { - InheritanceChanged.Invoke( false ); - } - - Penumbra.Log.Debug( $"Added {collection.AnonymizedName} to {AnonymizedName} inheritances." ); - return true; - } - - public void RemoveInheritance( int idx ) - { - var inheritance = _inheritance[ idx ]; - ClearSubscriptions( inheritance ); - _inheritance.RemoveAt( idx ); - InheritanceChanged.Invoke( false ); - Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." ); - } - - internal void ClearSubscriptions( ModCollection other ) - { - other.ModSettingChanged -= OnInheritedModSettingChange; - other.InheritanceChanged -= OnInheritedInheritanceChange; - } - - // Order in the inheritance list is relevant. - public void MoveInheritance( int from, int to ) - { - if( _inheritance.Move( from, to ) ) - { - InheritanceChanged.Invoke( false ); - Penumbra.Log.Debug( $"Moved {AnonymizedName}s inheritance {from} to {to}." ); - } - } - - // Carry changes in collections inherited from forward if they are relevant for this collection. - private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) - { - switch( type ) - { - case ModSettingChange.MultiInheritance: - case ModSettingChange.MultiEnableState: - ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); - return; - default: - if( modIdx < 0 || modIdx >= _settings.Count ) - { - Penumbra.Log.Warning( - $"Collection state broken, Mod {modIdx} in inheritance does not exist. ({_settings.Count} mods exist)." ); - return; - } - - if( _settings[ modIdx ] == null ) - { - ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); - } - - return; - } - } - - private void OnInheritedInheritanceChange( bool _ ) - => InheritanceChanged.Invoke( true ); - - // Obtain the actual settings for a given mod via index. - // Also returns the collection the settings are taken from. - // If no collection provides settings for this mod, this collection is returned together with null. - public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] - { - get - { - if( Index <= 0 ) - { - return ( ModSettings.Empty, this ); - } - - foreach( var collection in GetFlattenedInheritance() ) - { - var settings = collection._settings[ idx ]; - if( settings != null ) - { - return ( settings, collection ); - } - } - - return ( null, this ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index ceaac70d..e03268a4 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,7 +1,6 @@ using Penumbra.Mods; using System.Collections.Generic; using System.Linq; -using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 67913760..363ee5e4 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -15,9 +15,9 @@ namespace Penumbra.Collections; // - any change in settings or inheritance of the collection causes a Save. public partial class ModCollection { - public const int CurrentVersion = 1; + public const int CurrentVersion = 1; public const string DefaultCollectionName = "Default"; - public const string EmptyCollection = "None"; + public const string EmptyCollection = "None"; public static readonly ModCollection Empty = CreateEmpty(); @@ -51,18 +51,18 @@ public partial class ModCollection => Enumerable.Range(0, _settings.Count).Select(i => this[i].Settings); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary _unusedSettings; + internal readonly Dictionary _unusedSettings; // Constructor for duplication. private ModCollection(string name, ModCollection duplicate) { - Name = name; - Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll(s => s?.DeepCopy()); - _unusedSettings = duplicate._unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); - _inheritance = duplicate._inheritance.ToList(); - ModSettingChanged += SaveOnChange; - InheritanceChanged += SaveOnChange; + Name = name; + Version = duplicate.Version; + _settings = duplicate._settings.ConvertAll(s => s?.DeepCopy()); + _unusedSettings = duplicate._unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); + DirectlyInheritsFrom = duplicate.DirectlyInheritsFrom.ToList(); + foreach (var c in DirectlyInheritsFrom) + ((List)c.DirectParentOf).Add(this); } // Constructor for reading from files. @@ -76,8 +76,6 @@ public partial class ModCollection ApplyModSettings(); Migration.Migrate(Penumbra.SaveService, this); - ModSettingChanged += SaveOnChange; - InheritanceChanged += SaveOnChange; } // Create a new, unique empty collection of a given name. @@ -87,11 +85,11 @@ public partial class ModCollection // Create a new temporary collection that does not save and has a negative index. public static ModCollection CreateNewTemporary(string name, int changeCounter) { - var collection = new ModCollection(name, Empty); - collection.ModSettingChanged -= collection.SaveOnChange; - collection.InheritanceChanged -= collection.SaveOnChange; - collection.Index = ~Penumbra.TempCollections.Count; - collection.ChangeCounter = changeCounter; + var collection = new ModCollection(name, Empty) + { + Index = ~Penumbra.TempCollections.Count, + ChangeCounter = changeCounter, + }; collection.CreateCache(false); return collection; } @@ -162,55 +160,43 @@ public partial class ModCollection Penumbra.SaveService.ImmediateSave(this); } - public bool CopyModSettings(int modIdx, string modName, int targetIdx, string targetName) - { - if (targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0) - return false; - - // If the source mod exists, convert its settings to saved settings or null if its inheriting. - // If it does not exist, check unused settings. - // If it does not exist and has no unused settings, also use null. - ModSettings.SavedSettings? savedSettings = modIdx >= 0 - ? _settings[modIdx] != null - ? new ModSettings.SavedSettings(_settings[modIdx]!, Penumbra.ModManager[modIdx]) - : null - : _unusedSettings.TryGetValue(modName, out var s) - ? s - : null; - - if (targetIdx >= 0) - { - if (savedSettings != null) - { - // The target mod exists and the source settings are not inheriting, convert and fix the settings and copy them. - // This triggers multiple events. - savedSettings.Value.ToSettings(Penumbra.ModManager[targetIdx], out var settings); - SetModState(targetIdx, settings.Enabled); - SetModPriority(targetIdx, settings.Priority); - foreach (var (value, index) in settings.Settings.WithIndex()) - SetModSetting(targetIdx, index, value); - } - else - { - // The target mod exists, but the source is inheriting, set the target to inheriting. - // This triggers events. - SetModInheritance(targetIdx, true); - } - } - else - { - // The target mod does not exist. - // Either copy the unused source settings directly if they are not inheriting, - // or remove any unused settings for the target if they are inheriting. - if (savedSettings != null) - _unusedSettings[targetName] = savedSettings.Value; - else - _unusedSettings.Remove(targetName); - } - - return true; - } - public override string ToString() => Name; + + /// + /// Obtain the actual settings for a given mod via index. + /// Also returns the collection the settings are taken from. + /// If no collection provides settings for this mod, this collection is returned together with null. + /// + public (ModSettings? Settings, ModCollection Collection) this[Index idx] + { + get + { + if (Index <= 0) + return (ModSettings.Empty, this); + + foreach (var collection in GetFlattenedInheritance()) + { + var settings = collection._settings[idx]; + if (settings != null) + return (settings, collection); + } + + return (null, this); + } + } + + public readonly IReadOnlyList DirectlyInheritsFrom = new List(); + public readonly IReadOnlyList DirectParentOf = new List(); + + /// All inherited collections in application order without filtering for duplicates. + public static IEnumerable InheritedCollections(ModCollection collection) + => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); + + /// + /// Iterate over all collections inherited from in depth-first order. + /// Skip already visited collections to avoid circular dependencies. + /// + public IEnumerable GetFlattenedInheritance() + => InheritedCollections(this).Distinct(); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 96acda5f..bdc16d75 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -8,7 +8,7 @@ using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Collections.Manager; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; @@ -23,18 +23,20 @@ public class CommandHandler : IDisposable { private const string CommandName = "/penumbra"; - private readonly CommandManager _commandManager; - private readonly RedrawService _redrawService; - private readonly ChatGui _chat; - private readonly Configuration _config; - private readonly ConfigWindow _configWindow; - private readonly ActorManager _actors; - private readonly ModManager _modManager; + private readonly CommandManager _commandManager; + private readonly RedrawService _redrawService; + private readonly ChatGui _chat; + private readonly Configuration _config; + private readonly ConfigWindow _configWindow; + private readonly ActorManager _actors; + private readonly ModManager _modManager; private readonly CollectionManager _collectionManager; - private readonly Penumbra _penumbra; + private readonly Penumbra _penumbra; + private readonly CollectionEditor _collectionEditor; public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, - ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra) + ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra, + CollectionEditor collectionEditor) { _commandManager = commandManager; _redrawService = redrawService; @@ -45,6 +47,7 @@ public class CommandHandler : IDisposable _actors = actors.AwaitedService; _chat = chat; _penumbra = penumbra; + _collectionEditor = collectionEditor; framework.RunOnFrameworkThread(() => { _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) @@ -455,14 +458,15 @@ public class CommandHandler : IDisposable collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager.Storage.ByName(lowerName, out var c) ? c : null; + : _collectionManager.Storage.ByName(lowerName, out var c) + ? c + : null; if (collection != null) return true; _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") .BuiltString); return false; - } private static bool? ParseTrueFalseToggle(string value) @@ -498,51 +502,47 @@ public class CommandHandler : IDisposable private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection!.Settings[mod.Index]; + var settings = collection.Settings[mod.Index]; switch (settingState) { case 0: - if (collection.SetModState(mod.Index, true)) - { - Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) - .AddText(".").BuiltString); - return true; - } + if (!_collectionEditor.SetModState(collection, mod, true)) + return false; + + Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); + return true; - return false; case 1: - if (collection.SetModState(mod.Index, false)) - { - Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) - .AddText(".").BuiltString); - return true; - } + if (!_collectionEditor.SetModState(collection, mod, false)) + return false; + + Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); + return true; - return false; case 2: var setting = !(settings?.Enabled ?? false); - if (collection.SetModState(mod.Index, setting)) - { - Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) - .AddText(" in collection ") - .AddYellow(collection.Name, true) - .AddText(".").BuiltString); - return true; - } + if (!_collectionEditor.SetModState(collection, mod, setting)) + return false; + + Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) + .AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); + return true; - return false; case 3: - if (collection.SetModInheritance(mod.Index, true)) - { - Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) - .AddText(" to inherit.").BuiltString); - return true; - } + if (!_collectionEditor.SetModInheritance(collection, mod, true)) + return false; + + Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(" to inherit.").BuiltString); + return true; - return false; } return false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6dbcd3a8..dafac640 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -253,7 +253,7 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c) => sb.Append($"**Collection {c.AnonymizedName}**\n" - + $"> **`Inheritances: `** {c.Inheritance.Count}\n" + + $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n" + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 58dfd7ac..ccc893d3 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -91,6 +91,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); // Add Mod Services diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 8a47ff40..f3a9a389 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Mods; @@ -171,6 +173,44 @@ public sealed class ModPathChanged : EventWrapper Invoke(this, changeType, mod, oldModDirectory, newModDirectory); } +/// +/// Triggered whenever a mod setting is changed. +/// +/// Parameter is the collection in which the setting was changed. +/// Parameter is the type of change. +/// Parameter is the mod the setting was changed for, unless it was a multi-change. +/// Parameter is the old value of the setting before the change as int. +/// Parameter is the index of the changed group if the change type is Setting. +/// Parameter is whether the change was inherited from another collection. +/// +/// +public sealed class ModSettingChanged : EventWrapper> +{ + public ModSettingChanged() + : base(nameof(ModSettingChanged)) + { } + + public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) + => Invoke(this, collection, type, mod, oldValue, groupIdx, inherited); +} + +/// +/// Triggered whenever a collections inheritances change. +/// +/// Parameter is the collection whose ancestors were changed. +/// Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). +/// +/// +public sealed class CollectionInheritanceChanged : EventWrapper> +{ + public CollectionInheritanceChanged() + : base(nameof(CollectionInheritanceChanged)) + { } + + public void Invoke(ModCollection collection, bool inherited) + => Invoke(this, collection, inherited); +} + public class CommunicatorService : IDisposable { /// @@ -179,30 +219,36 @@ public class CommunicatorService : IDisposable /// public readonly TemporaryGlobalModChange TemporaryGlobalModChange = new(); - /// + /// public readonly CreatingCharacterBase CreatingCharacterBase = new(); - /// + /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// + /// public readonly ModDataChanged ModDataChanged = new(); - /// + /// public readonly ModOptionChanged ModOptionChanged = new(); - /// + /// public readonly ModDiscoveryStarted ModDiscoveryStarted = new(); - /// + /// public readonly ModDiscoveryFinished ModDiscoveryFinished = new(); - /// + /// public readonly ModDirectoryChanged ModDirectoryChanged = new(); - /// + /// public readonly ModPathChanged ModPathChanged = new(); + /// + public readonly ModSettingChanged ModSettingChanged = new(); + + /// + public readonly CollectionInheritanceChanged CollectionInheritanceChanged = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -215,5 +261,7 @@ public class CommunicatorService : IDisposable ModDiscoveryFinished.Dispose(); ModDirectoryChanged.Dispose(); ModPathChanged.Dispose(); + ModSettingChanged.Dispose(); + CollectionInheritanceChanged.Dispose(); } } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 422917a5..ddb6aba1 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -200,7 +200,7 @@ public class ConfigMigrationService if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) continue; - jObject[nameof(ModCollection.Inheritance)] = JToken.FromObject(new List { ForcedCollection }); + jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); File.WriteAllText(collection.FullName, jObject.ToString()); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 57c52fd3..70c5cb75 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -57,7 +57,8 @@ public class ItemSwapTab : IDisposable, ITab }; _communicator.CollectionChange.Subscribe(OnCollectionChange); - _collectionManager.Active.Current.ModSettingChanged += OnSettingChange; + _communicator.ModSettingChanged.Subscribe(OnSettingChange); + _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange); _communicator.ModOptionChanged.Subscribe(OnModOptionChange); } @@ -102,7 +103,8 @@ public class ItemSwapTab : IDisposable, ITab public void Dispose() { _communicator.CollectionChange.Unsubscribe(OnCollectionChange); - _collectionManager.Active.Current.ModSettingChanged -= OnSettingChange; + _communicator.ModSettingChanged.Unsubscribe(OnSettingChange); + _communicator.CollectionInheritanceChanged.Unsubscribe(OnInheritanceChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); } @@ -744,21 +746,29 @@ public class ItemSwapTab : IDisposable, ITab if (collectionType != CollectionType.Current || _mod == null || newCollection == null) return; - UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null); - newCollection.ModSettingChanged += OnSettingChange; - if (oldCollection != null) - oldCollection.ModSettingChanged -= OnSettingChange; + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); } - private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) + private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) { - if (modIdx != _mod?.Index) + + if (collection != _collectionManager.Active.Current || mod != _mod) return; _swapData.LoadMod(_mod, _modSettings); _dirty = true; } + private void OnInheritanceChange(ModCollection collection, bool _) + { + if (collection != _collectionManager.Active.Current || _mod == null) + return; + + UpdateMod(_mod, collection[_mod.Index].Settings); + _swapData.LoadMod(_mod, _modSettings); + _dirty = true; + } + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) { if (type is ModOptionChangeType.PrepareChange || mod != _mod) diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs index 31044cae..6a43f5b2 100644 --- a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -17,7 +17,7 @@ public class InheritanceUi private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly CollectionManager _collectionManager; + private readonly CollectionManager _collectionManager; public InheritanceUi(CollectionManager collectionManager) => _collectionManager = collectionManager; @@ -118,7 +118,7 @@ public class InheritanceUi _seenInheritedCollections.Clear(); _seenInheritedCollections.Add(_collectionManager.Active.Current); - foreach (var collection in _collectionManager.Active.Current.Inheritance.ToList()) + foreach (var collection in _collectionManager.Active.Current.DirectlyInheritsFrom.ToList()) DrawInheritance(collection); } @@ -136,7 +136,7 @@ public class InheritanceUi using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); } /// @@ -157,9 +157,9 @@ public class InheritanceUi if (_inheritanceAction.Value.Item1 >= 0) { if (_inheritanceAction.Value.Item2 == -1) - _collectionManager.Active.Current.RemoveInheritance(_inheritanceAction.Value.Item1); + _collectionManager.Inheritances.RemoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1); else - _collectionManager.Active.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + _collectionManager.Inheritances.MoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); } _inheritanceAction = null; @@ -173,22 +173,22 @@ public class InheritanceUi { DrawNewInheritanceCombo(); ImGui.SameLine(); - var inheritance = _collectionManager.Active.Current.CheckValidInheritance(_newInheritance); + var inheritance = InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, _newInheritance); var tt = inheritance switch { - ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", - ModCollection.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", - ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", - ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", - ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", + InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.", + InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", + InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.", + InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.", + InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", _ => string.Empty, }; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, - inheritance != ModCollection.ValidInheritance.Valid, true) - && _collectionManager.Active.Current.AddInheritance(_newInheritance!, true)) + inheritance != InheritanceManager.ValidInheritance.Valid, true) + && _collectionManager.Inheritances.AddInheritance(_collectionManager.Active.Current, _newInheritance!)) _newInheritance = null; - if (inheritance != ModCollection.ValidInheritance.Valid) + if (inheritance != InheritanceManager.ValidInheritance.Valid) _newInheritance = null; ImGui.SameLine(); @@ -234,14 +234,14 @@ public class InheritanceUi { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); _newInheritance ??= _collectionManager.Storage.FirstOrDefault(c - => c != _collectionManager.Active.Current && !_collectionManager.Active.Current.Inheritance.Contains(c)) + => c != _collectionManager.Active.Current && !_collectionManager.Active.Current.DirectlyInheritsFrom.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); if (!combo) return; foreach (var collection in _collectionManager.Storage - .Where(c => _collectionManager.Active.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) + .Where(c => InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, c) == InheritanceManager.ValidInheritance.Valid) .OrderBy(c => c.Name)) { if (ImGui.Selectable(collection.Name, _newInheritance == collection)) @@ -261,8 +261,8 @@ public class InheritanceUi if (_movedInheritance != null) { - var idx1 = _collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance); - var idx2 = _collectionManager.Active.Current.Inheritance.IndexOf(collection); + var idx1 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); + var idx2 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -292,7 +292,7 @@ public class InheritanceUi if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(collection), -1); + _inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index b6099c64..c33003e4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -73,9 +73,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector @@ -360,11 +360,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector @@ -146,7 +146,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority) - _collectionManager.Active.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, _currentPriority.Value); _currentPriority = null; } @@ -168,7 +168,7 @@ public class ModPanelSettingsTab : ITab var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, true); + _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); @@ -191,7 +191,7 @@ public class ModPanelSettingsTab : ITab id.Push(idx2); var option = group[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) { @@ -227,7 +227,7 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); void DrawOptions() { @@ -236,7 +236,7 @@ public class ModPanelSettingsTab : ITab using var i = ImRaii.PushId(idx); var option = group[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx); if (option.Description.Length <= 0) continue; @@ -264,7 +264,7 @@ public class ModPanelSettingsTab : ITab var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {group.Count} Options"; var buttonTextHide = $"Hide {group.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); if (shown) @@ -276,10 +276,9 @@ public class ModPanelSettingsTab : ITab draw(); } - - - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); - var endPos = ImGui.GetCursorPos(); + + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); @@ -292,7 +291,7 @@ public class ModPanelSettingsTab : ITab + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; - var width = Math.Max(optionWidth, minWidth); + var width = Math.Max(optionWidth, minWidth); if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); } @@ -305,9 +304,9 @@ public class ModPanelSettingsTab : ITab /// private void DrawMultiGroup(IModGroup group, int groupIdx) { - using var id = ImRaii.PushId(groupIdx); - var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + using var id = ImRaii.PushId(groupIdx); + var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); void DrawOptions() { @@ -321,7 +320,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.Checkbox(option.Name, ref setting)) { flags = setting ? flags | flag : flags & ~flag; - _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); } if (option.Description.Length > 0) @@ -349,10 +348,10 @@ public class ModPanelSettingsTab : ITab if (ImGui.Selectable("Enable All")) { flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; - _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); } if (ImGui.Selectable("Disable All")) - _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, 0); } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index b454fa3b..2437f96e 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -91,7 +91,7 @@ public class ModsTab : ITab + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _collectionManager.Active.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{string.Join(", ", _collectionManager.Active.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); } } From 8c28f0c6e32a018a92fbe37a991cf00ace723fc9 Mon Sep 17 00:00:00 2001 From: Jacob Keller Date: Tue, 11 Apr 2023 20:47:40 -0700 Subject: [PATCH 0873/2451] Ignore hidden files when generating the unused files list. Don't include hidden files in the unused file list for a mod. These files are typically things such as .git or otherwise intended to be hidden and shouldn't be included as part of the list of files which could be used. --- Penumbra/Mods/Mod.Files.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a12b29fc..444f1fa9 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -65,6 +65,7 @@ public partial class Mod var modFiles = AllFiles.ToHashSet(); return ModPath.EnumerateDirectories() .SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + .Where( f => !f.Attributes.HasFlag( FileAttributes.Hidden ) ) .Select( f => new FullPath( f ) ) .Where( f => !modFiles.Contains( f ) ) .ToList(); @@ -142,4 +143,4 @@ public partial class Mod IModGroup.Save( group, ModPath, index ); } } -} \ No newline at end of file +} From 2bf80dfa6b712eac9c3d841137a89b07556dc1b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Apr 2023 16:26:00 +0200 Subject: [PATCH 0874/2451] Only search through non-hidden files in a few places. --- OtterGui | 2 +- Penumbra/Mods/Editor/ModFileCollection.cs | 3 ++- Penumbra/Mods/Mod.Creator.cs | 3 ++- Penumbra/Mods/Mod.Files.cs | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 39e24bae..37789067 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 39e24baebcb450ccd1e7103fd428ee3020c28702 +Subproject commit 377890671cba2e9edb8e792be3bf8f0a84a8b741 diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 9905fbcb..fa3d5614 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using Microsoft.Win32; +using OtterGui; using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -113,7 +114,7 @@ public class ModFileCollection : IDisposable tok.ThrowIfCancellationRequested(); ClearFiles(); - foreach (var file in mod.ModPath.EnumerateDirectories().SelectMany(d => d.EnumerateFiles("*.*", SearchOption.AllDirectories))) + foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles)) { tok.ThrowIfCancellationRequested(); if (!FileRegistry.FromFile(mod.ModPath, file, out var registry)) diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 97b4fe4a..6233ece9 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Import.Structs; @@ -98,7 +99,7 @@ internal static partial class ModCreator /// Create the data for a given sub mod from its data and the folder it is based on. public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) { - var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + var list = optionFolder.EnumerateNonHiddenFiles() .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) .Where( t => t.Item1 ); diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 4528bb22..8326b4fb 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -40,9 +41,8 @@ public partial class Mod { var modFiles = AllFiles.ToHashSet(); return ModPath.EnumerateDirectories() - .Where(d => !d.Attributes.HasFlag( FileAttributes.Hidden ) ) - .SelectMany(f => f.EnumerateFiles("*", SearchOption.AllDirectories)) - .Where( f => !f.Attributes.HasFlag( FileAttributes.Hidden ) ) + .Where(d => !d.IsHidden()) + .SelectMany(FileExtensions.EnumerateNonHiddenFiles) .Select(f => new FullPath(f)) .Where(f => !modFiles.Contains(f)) .ToList(); From e86899c9436ee32602fd555e3c642185dab9d976 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Apr 2023 20:29:08 +0200 Subject: [PATCH 0875/2451] tmp --- .../Collections/Manager/CollectionEditor.cs | 44 +-- .../Collections/Manager/CollectionManager.cs | 2 + .../Collections/Manager/CollectionStorage.cs | 48 +-- .../Collections/Manager/InheritanceManager.cs | 14 +- .../Manager/TempCollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.File.cs | 100 +++--- .../Collections/ModCollection.Migration.cs | 76 ++--- Penumbra/Collections/ModCollection.cs | 308 +++++++++--------- Penumbra/PenumbraNew.cs | 3 +- Penumbra/Services/ConfigMigrationService.cs | 112 +++---- Penumbra/UI/Tabs/CollectionsTab.cs | 6 +- 11 files changed, 362 insertions(+), 353 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 57a7b8f6..4000f504 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; @@ -14,11 +15,13 @@ public class CollectionEditor { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; + private readonly ModStorage _modStorage; - public CollectionEditor(SaveService saveService, CommunicatorService communicator) + public CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) { _saveService = saveService; _communicator = communicator; + _modStorage = modStorage; } /// Enable or disable the mod inheritance of mod idx. @@ -37,12 +40,12 @@ public class CollectionEditor ///
public bool SetModState(ModCollection collection, Mod mod, bool newValue) { - var oldValue = collection._settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; + var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - collection._settings[mod.Index]!.Enabled = newValue; + ((List)collection.Settings)[mod.Index]!.Enabled = newValue; InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0); return true; } @@ -65,13 +68,13 @@ public class CollectionEditor var changes = false; foreach (var mod in mods) { - var oldValue = collection._settings[mod.Index]?.Enabled; + var oldValue = collection.Settings[mod.Index]?.Enabled; if (newValue == oldValue) continue; FixInheritance(collection, mod, false); - collection._settings[mod.Index]!.Enabled = newValue; - changes = true; + ((List)collection.Settings)[mod.Index]!.Enabled = newValue; + changes = true; } if (!changes) @@ -86,12 +89,12 @@ public class CollectionEditor ///
public bool SetModPriority(ModCollection collection, Mod mod, int newValue) { - var oldValue = collection._settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0; + var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - collection._settings[mod.Index]!.Priority = newValue; + ((List)collection.Settings)[mod.Index]!.Priority = newValue; InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0); return true; } @@ -102,15 +105,15 @@ public class CollectionEditor ///
public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, uint newValue) { - var settings = collection._settings[mod.Index] != null - ? collection._settings[mod.Index]!.Settings + var settings = collection.Settings[mod.Index] != null + ? collection.Settings[mod.Index]!.Settings : collection[mod.Index].Settings?.Settings; var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings; if (oldValue == newValue) return false; var inheritance = FixInheritance(collection, mod, false); - collection._settings[mod.Index]!.SetValue(mod, groupIdx, newValue); + ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx); return true; } @@ -125,10 +128,10 @@ public class CollectionEditor // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = sourceMod != null - ? collection._settings[sourceMod.Index] != null - ? new ModSettings.SavedSettings(collection._settings[sourceMod.Index]!, sourceMod) + ? collection.Settings[sourceMod.Index] != null + ? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod) : null - : collection._unusedSettings.TryGetValue(sourceName, out var s) + : collection.UnusedSettings.TryGetValue(sourceName, out var s) ? s : null; @@ -157,9 +160,9 @@ public class CollectionEditor // Either copy the unused source settings directly if they are not inheriting, // or remove any unused settings for the target if they are inheriting. if (savedSettings != null) - collection._unusedSettings[targetName] = savedSettings.Value; + ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; else - collection._unusedSettings.Remove(targetName); + ((Dictionary)collection.UnusedSettings).Remove(targetName); } return true; @@ -189,11 +192,12 @@ public class CollectionEditor ///
private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit) { - var settings = collection._settings[mod.Index]; + var settings = collection.Settings[mod.Index]; if (inherit == (settings == null)) return false; - collection._settings[mod.Index] = inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + ((List)collection.Settings)[mod.Index] = + inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); return true; } @@ -201,7 +205,7 @@ public class CollectionEditor [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) { - _saveService.QueueSave(changedCollection); + _saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection)); _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } @@ -219,7 +223,7 @@ public class CollectionEditor _communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: - if (directInheritor._settings[mod!.Index] == null) + if (directInheritor.Settings[mod!.Index] == null) _communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index 5e1c5781..45e10c6a 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -19,4 +19,6 @@ public class CollectionManager Temp = temp; Editor = editor; } + + } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index e363c106..be8db099 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -18,6 +18,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; + private readonly ModStorage _modStorage; /// The empty collection is always available at Index 0. private readonly List _collections = new() @@ -34,9 +35,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerator GetEnumeratorWithEmpty() - => _collections.GetEnumerator(); - public int Count => _collections.Count; @@ -53,10 +51,11 @@ public class CollectionStorage : IReadOnlyList, IDisposable return true; } - public CollectionStorage(CommunicatorService communicator, SaveService saveService) + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) { _communicator = communicator; _saveService = saveService; + _modStorage = modStorage; _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); _communicator.ModPathChanged.Subscribe(OnModPathChange, 10); @@ -114,21 +113,16 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } - var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); - newCollection.Index = _collections.Count; + var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count); _collections.Add(newCollection); - _saveService.ImmediateSave(newCollection); + _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.ChatService.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", NotificationType.Success); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); return true; } - /// Whether the given collection can be deleted. - public bool CanRemoveCollection(ModCollection collection) - => collection.Index > ModCollection.Empty.Index && collection.Index < Count && collection.Index != DefaultNamed.Index; - /// /// Remove the given collection if it exists and is neither the empty nor the default-named collection. /// @@ -146,7 +140,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } - _saveService.ImmediateDelete(collection); + _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); _collections.RemoveAt(collection.Index); // Update indices. for (var i = collection.Index; i < Count; ++i) @@ -192,6 +186,15 @@ public class CollectionStorage : IReadOnlyList, IDisposable } } + /// Remove all settings for not currently-installed mods from the given collection. + public void CleanUnavailableSettings(ModCollection collection) + { + var any = collection.UnusedSettings.Count > 0; + ((Dictionary)collection.UnusedSettings).Clear(); + if (any) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + /// /// Check if a name is valid to use for a collection. /// Does not check for uniqueness. @@ -211,24 +214,23 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var file in _saveService.FileNames.CollectionFiles) { - var collection = ModCollection.LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) + if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance) || !IsValidName(name)) continue; - if (ByName(collection.Name, out _)) + if (ByName(name, out _)) { - Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {collection.Name} already exists. Import skipped.", + Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", "Warning", NotificationType.Warning); continue; } + var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", NotificationType.Warning); _inheritancesByName?.Add(inheritance); - collection.Index = _collections.Count; _collections.Add(collection); } @@ -258,7 +260,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable private void OnModDiscoveryStarted() { foreach (var collection in this) - collection.PrepareModDiscovery(); + collection.PrepareModDiscovery(_modStorage); } /// Restore all settings in all collections to mods. @@ -266,7 +268,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable { // Re-apply all mod settings. foreach (var collection in this) - collection.ApplyModSettings(); + collection.ApplyModSettings(_saveService, _modStorage); } /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. @@ -281,11 +283,11 @@ public class CollectionStorage : IReadOnlyList, IDisposable break; case ModPathChangeType.Deleted: foreach (var collection in this) - collection.RemoveMod(mod, mod.Index); + collection.RemoveMod(mod); break; case ModPathChangeType.Moved: foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - _saveService.QueueSave(collection); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; } } @@ -299,8 +301,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { - if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - _saveService.QueueSave(collection); + if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } } \ No newline at end of file diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index e5034ece..1378ae56 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; @@ -19,12 +20,16 @@ public class InheritanceManager : IDisposable public enum ValidInheritance { Valid, + /// Can not inherit from self Self, + /// Can not inherit from the empty collection Empty, + /// Already inherited from Contained, + /// Inheritance would lead to a circle. Circle, } @@ -32,12 +37,14 @@ public class InheritanceManager : IDisposable private readonly CollectionStorage _storage; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; + private readonly ModStorage _modStorage; - public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator) + public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator, ModStorage modStorage) { _storage = storage; _saveService = saveService; _communicator = communicator; + _modStorage = modStorage; ApplyInheritances(); _communicator.CollectionChange.Subscribe(OnCollectionChange); @@ -80,6 +87,7 @@ public class InheritanceManager : IDisposable var parent = inheritor.DirectlyInheritsFrom[idx]; ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); ((List)parent.DirectParentOf).Remove(inheritor); + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); @@ -91,6 +99,7 @@ public class InheritanceManager : IDisposable if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) return; + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); @@ -106,6 +115,7 @@ public class InheritanceManager : IDisposable ((List)parent.DirectParentOf).Add(inheritor); if (invokeEvent) { + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); } @@ -134,7 +144,7 @@ public class InheritanceManager : IDisposable } if (localChanges) - _saveService.ImmediateSave(collection); + _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 0eed53d6..42bbea19 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -52,7 +52,7 @@ public class TempCollectionManager : IDisposable if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; - var collection = ModCollection.CreateNewTemporary(name, GlobalChangeCounter++); + var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) return collection.Name; diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index f688b5ca..6bb1a5af 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -6,49 +6,30 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; +using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Collections; -// File operations like saving, loading and deleting for a collection. -public partial class ModCollection : ISavable +/// +/// Handle saving and loading a collection. +/// +internal readonly struct ModCollectionSave : ISavable { - // Since inheritances depend on other collections existing, - // we return them as a list to be applied after reading all collections. - internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) + private readonly ModStorage _modStorage; + private readonly ModCollection _modCollection; + + public ModCollectionSave(ModStorage modStorage, ModCollection modCollection) { - inheritance = Array.Empty(); - if (!file.Exists) - { - Penumbra.Log.Error("Could not read collection because file does not exist."); - return null; - } - - try - { - var obj = JObject.Parse(File.ReadAllText(file.FullName)); - var name = obj[nameof(Name)]?.ToObject() ?? string.Empty; - var version = obj[nameof(Version)]?.ToObject() ?? 0; - // Custom deserialization that is converted with the constructor. - var settings = obj[nameof(Settings)]?.ToObject>() - ?? new Dictionary(); - inheritance = obj["Inheritance"]?.ToObject>() ?? (IReadOnlyList)Array.Empty(); - - return new ModCollection(name, version, settings); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); - } - - return null; + _modStorage = modStorage; + _modCollection = modCollection; } public string ToFilename(FilenameService fileNames) - => fileNames.CollectionFile(this); + => fileNames.CollectionFile(_modCollection); public string LogName(string _) - => AnonymizedName; + => _modCollection.AnonymizedName; public string TypeName => "Collection"; @@ -59,25 +40,25 @@ public partial class ModCollection : ISavable j.Formatting = Formatting.Indented; var x = JsonSerializer.Create(new JsonSerializerSettings { Formatting = Formatting.Indented }); j.WriteStartObject(); - j.WritePropertyName(nameof(Version)); - j.WriteValue(Version); - j.WritePropertyName(nameof(Name)); - j.WriteValue(Name); - j.WritePropertyName(nameof(Settings)); + j.WritePropertyName("Version"); + j.WriteValue(ModCollection.CurrentVersion); + j.WritePropertyName(nameof(ModCollection.Name)); + j.WriteValue(_modCollection.Name); + j.WritePropertyName(nameof(ModCollection.Settings)); // Write all used and unused settings by mod directory name. j.WriteStartObject(); - for (var i = 0; i < _settings.Count; ++i) + for (var i = 0; i < _modCollection.Settings.Count; ++i) { - var settings = _settings[i]; + var settings = _modCollection.Settings[i]; if (settings != null) { - j.WritePropertyName(Penumbra.ModManager[i].ModPath.Name); - x.Serialize(j, new ModSettings.SavedSettings(settings, Penumbra.ModManager[i])); + j.WritePropertyName(_modStorage[i].ModPath.Name); + x.Serialize(j, new ModSettings.SavedSettings(settings, _modStorage[i])); } } - foreach (var (modDir, settings) in _unusedSettings) + foreach (var (modDir, settings) in _modCollection.UnusedSettings) { j.WritePropertyName(modDir); x.Serialize(j, settings); @@ -87,7 +68,40 @@ public partial class ModCollection : ISavable // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, DirectlyInheritsFrom.Select(c => c.Name)); + x.Serialize(j, _modCollection.DirectlyInheritsFrom.Select(c => c.Name)); j.WriteEndObject(); } + + public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary settings, + out IReadOnlyList inheritance) + { + settings = new Dictionary(); + inheritance = Array.Empty(); + if (!file.Exists) + { + Penumbra.Log.Error("Could not read collection because file does not exist."); + name = string.Empty; + + version = 0; + return false; + } + + try + { + var obj = JObject.Parse(File.ReadAllText(file.FullName)); + name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; + version = obj["Version"]?.ToObject() ?? 0; + // Custom deserialization that is converted with the constructor. + settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; + inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; + return true; + } + catch (Exception e) + { + name = string.Empty; + version = 0; + Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); + return false; + } + } } diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index e03268a4..02ecea47 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,58 +1,48 @@ using Penumbra.Mods; using System.Collections.Generic; using System.Linq; +using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Collections; -public sealed partial class ModCollection +/// Migration to convert ModCollections from older versions to newer. +internal static class ModCollectionMigration { - // Migration to convert ModCollections from older versions to newer. - private static class Migration + /// Migrate a mod collection to the current version. + public static void Migrate(SaveService saver, ModStorage mods, int version, ModCollection collection) + { + var changes = MigrateV0ToV1(collection, ref version); + if (changes) + saver.ImmediateSave(new ModCollectionSave(mods, collection)); + } + + /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. + private static bool MigrateV0ToV1(ModCollection collection, ref int version) { - public static void Migrate(SaveService saver, ModCollection collection ) + if (version > 0) + return false; + + version = 1; + + // Remove all completely defaulted settings from active and inactive mods. + for (var i = 0; i < collection.Settings.Count; ++i) { - var changes = MigrateV0ToV1( collection ); - if( changes ) - { - saver.ImmediateSave(collection); - } + if (SettingIsDefaultV0(collection.Settings[i])) + ((List)collection.Settings)[i] = null; } - private static bool MigrateV0ToV1( ModCollection collection ) - { - if( collection.Version > 0 ) - { - return false; - } + foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) + ((Dictionary)collection.UnusedSettings).Remove(key); - collection.Version = 1; - - // Remove all completely defaulted settings from active and inactive mods. - for( var i = 0; i < collection._settings.Count; ++i ) - { - if( SettingIsDefaultV0( collection._settings[ i ] ) ) - { - collection._settings[ i ] = null; - } - } - - foreach( var (key, _) in collection._unusedSettings.Where( kvp => SettingIsDefaultV0( kvp.Value ) ).ToList() ) - { - collection._unusedSettings.Remove( key ); - } - - return true; - } - - // We treat every completely defaulted setting as inheritance-ready. - private static bool SettingIsDefaultV0( ModSettings.SavedSettings setting ) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); - - private static bool SettingIsDefaultV0( ModSettings? setting ) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 ); + return true; } - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings.SavedSettings > allSettings ) - => new(name, 0, allSettings); -} \ No newline at end of file + /// We treat every completely defaulted setting as inheritance-ready. + private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); + + /// + private static bool SettingIsDefaultV0(ModSettings? setting) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0); +} diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 363ee5e4..fc0ac1b7 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,168 +1,73 @@ -using OtterGui.Filesystem; using Penumbra.Mods; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using OtterGui; +using Penumbra.Mods.Manager; +using Penumbra.Util; namespace Penumbra.Collections; -// A ModCollection is a named set of ModSettings to all of the users' installed mods. -// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. -// Invariants: -// - Index is the collections index in the ModCollection.Manager -// - Settings has the same size as ModManager.Mods. -// - any change in settings or inheritance of the collection causes a Save. +/// +/// A ModCollection is a named set of ModSettings to all of the users' installed mods. +/// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. +/// Invariants: +/// - Index is the collections index in the ModCollection.Manager +/// - Settings has the same size as ModManager.Mods. +/// - any change in settings or inheritance of the collection causes a Save. +/// - the name can not contain invalid path characters and has to be unique when lower-cased. +/// public partial class ModCollection { public const int CurrentVersion = 1; public const string DefaultCollectionName = "Default"; - public const string EmptyCollection = "None"; + public const string EmptyCollectionName = "None"; - public static readonly ModCollection Empty = CreateEmpty(); + /// + /// Create the always available Empty Collection that will always sit at index 0, + /// can not be deleted and does never create a cache. + /// + public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0); - // The collection name can contain invalid path characters, - // but after removing those and going to lower case it has to be unique. + /// The name of a collection can not contain characters invalid in a path. public string Name { get; internal init; } - // Get the first two letters of a collection name and its Index (or None if it is the empty collection). - public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; - - public int Version { get; internal set; } - public int Index { get; internal set; } = -1; - - // If a ModSetting is null, it can be inherited from other collections. - // If no collection provides a setting for the mod, it is just disabled. - internal readonly List _settings; - - public IReadOnlyList Settings - => _settings; - - // Returns whether there are settings not in use by any current mod. - public bool HasUnusedSettings - => _unusedSettings.Count > 0; - - public int NumUnusedSettings - => _unusedSettings.Count; - - // Evaluates the settings along the whole inheritance tree. - public IEnumerable ActualSettings - => Enumerable.Range(0, _settings.Count).Select(i => this[i].Settings); - - // Settings for deleted mods will be kept via directory name. - internal readonly Dictionary _unusedSettings; - - // Constructor for duplication. - private ModCollection(string name, ModCollection duplicate) - { - Name = name; - Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll(s => s?.DeepCopy()); - _unusedSettings = duplicate._unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); - DirectlyInheritsFrom = duplicate.DirectlyInheritsFrom.ToList(); - foreach (var c in DirectlyInheritsFrom) - ((List)c.DirectParentOf).Add(this); - } - - // Constructor for reading from files. - private ModCollection(string name, int version, Dictionary allSettings) - { - Name = name; - Version = version; - _unusedSettings = allSettings; - - _settings = new List(); - ApplyModSettings(); - - Migration.Migrate(Penumbra.SaveService, this); - } - - // Create a new, unique empty collection of a given name. - public static ModCollection CreateNewEmpty(string name) - => new(name, CurrentVersion, new Dictionary()); - - // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary(string name, int changeCounter) - { - var collection = new ModCollection(name, Empty) - { - Index = ~Penumbra.TempCollections.Count, - ChangeCounter = changeCounter, - }; - collection.CreateCache(false); - return collection; - } - - // Duplicate the calling collection to a new, unique collection of a given name. - public ModCollection Duplicate(string name) - => new(name, this); - - // Remove all settings for not currently-installed mods. - public void CleanUnavailableSettings() - { - var any = _unusedSettings.Count > 0; - _unusedSettings.Clear(); - if (any) - Penumbra.SaveService.QueueSave(this); - } - - // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - internal bool AddMod(Mod mod) - { - if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) - { - var ret = save.ToSettings(mod, out var settings); - _settings.Add(settings); - _unusedSettings.Remove(mod.ModPath.Name); - return ret; - } - - _settings.Add(null); - return false; - } - - // Move settings from the current mod list to the unused mod settings. - internal void RemoveMod(Mod mod, int idx) - { - var settings = _settings[idx]; - if (settings != null) - _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - - _settings.RemoveAt(idx); - } - - // Create the always available Empty Collection that will always sit at index 0, - // can not be deleted and does never create a cache. - private static ModCollection CreateEmpty() - { - var collection = CreateNewEmpty(EmptyCollection); - collection.Index = 0; - collection._settings.Clear(); - return collection; - } - - // Move all settings to unused settings for rediscovery. - internal void PrepareModDiscovery() - { - foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) - _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); - - _settings.Clear(); - } - - // Apply all mod settings from unused settings to the current set of mods. - // Also fixes invalid settings. - internal void ApplyModSettings() - { - _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); - if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) - Penumbra.SaveService.ImmediateSave(this); - } - public override string ToString() => Name; + /// Get the first two letters of a collection name and its Index (or None if it is the empty collection). + public string AnonymizedName + => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; + + /// The index of the collection is set and kept up-to-date by the CollectionManager. + public int Index { get; internal set; } + + /// + /// If a ModSetting is null, it can be inherited from other collections. + /// If no collection provides a setting for the mod, it is just disabled. + /// + public readonly IReadOnlyList Settings; + + /// Settings for deleted mods will be kept via the mods identifier (directory name). + public readonly IReadOnlyDictionary UnusedSettings; + + /// Contains all direct parent collections this collection inherits settings from. + public readonly IReadOnlyList DirectlyInheritsFrom; + + /// Contains all direct child collections that inherit from this collection. + public readonly IReadOnlyList DirectParentOf = new List(); + + /// All inherited collections in application order without filtering for duplicates. + public static IEnumerable InheritedCollections(ModCollection collection) + => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); + + /// + /// Iterate over all collections inherited from in depth-first order. + /// Skip already visited collections to avoid circular dependencies. + /// + public IEnumerable GetFlattenedInheritance() + => InheritedCollections(this).Distinct(); + /// /// Obtain the actual settings for a given mod via index. /// Also returns the collection the settings are taken from. @@ -177,7 +82,7 @@ public partial class ModCollection foreach (var collection in GetFlattenedInheritance()) { - var settings = collection._settings[idx]; + var settings = collection.Settings[idx]; if (settings != null) return (settings, collection); } @@ -186,17 +91,104 @@ public partial class ModCollection } } - public readonly IReadOnlyList DirectlyInheritsFrom = new List(); - public readonly IReadOnlyList DirectParentOf = new List(); - - /// All inherited collections in application order without filtering for duplicates. - public static IEnumerable InheritedCollections(ModCollection collection) - => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); + /// Evaluates all settings along the whole inheritance tree. + public IEnumerable ActualSettings + => Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings); /// - /// Iterate over all collections inherited from in depth-first order. - /// Skip already visited collections to avoid circular dependencies. + /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. /// - public IEnumerable GetFlattenedInheritance() - => InheritedCollections(this).Distinct(); + public ModCollection Duplicate(string name, int index) + { + Debug.Assert(index > 0, "Collection duplicated with non-positive index."); + return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), + DirectlyInheritsFrom.ToList(), UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + } + + /// Constructor for reading from files. + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index, + Dictionary allSettings) + { + Debug.Assert(index > 0, "Collection read with non-positive index."); + var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings); + ret.ApplyModSettings(saver, mods); + ModCollectionMigration.Migrate(saver, mods, version, ret); + return ret; + } + + /// Constructor for temporary collections. + public static ModCollection CreateTemporary(string name, int index, int changeCounter) + { + Debug.Assert(index < 0, "Temporary collection created with non-negative index."); + var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List(), new List(), + new Dictionary()); + ret.CreateCache(false); + return ret; + } + + /// Constructor for empty collections. + public static ModCollection CreateEmpty(string name, int index) + { + Debug.Assert(index >= 0, "Empty collection created with negative index."); + return new ModCollection(name, index, 0, CurrentVersion, new List(), new List(), + new Dictionary()); + } + + /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + internal bool AddMod(Mod mod) + { + if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save)) + { + var ret = save.ToSettings(mod, out var settings); + ((List)Settings).Add(settings); + ((Dictionary)UnusedSettings).Remove(mod.ModPath.Name); + return ret; + } + + ((List)Settings).Add(null); + return false; + } + + /// Move settings from the current mod list to the unused mod settings. + internal void RemoveMod(Mod mod) + { + var settings = Settings[mod.Index]; + if (settings != null) + ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); + + ((List)Settings).RemoveAt(mod.Index); + } + + /// Move all settings to unused settings for rediscovery. + internal void PrepareModDiscovery(ModStorage mods) + { + foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null)) + ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); + + ((List)Settings).Clear(); + } + + /// + /// Apply all mod settings from unused settings to the current set of mods. + /// Also fixes invalid settings. + /// + internal void ApplyModSettings(SaveService saver, ModStorage mods) + { + ((List)Settings).Capacity = Math.Max(((List)Settings).Capacity, mods.Count); + if (mods.Aggregate(false, (current, mod) => current | AddMod(mod))) + saver.ImmediateSave(new ModCollectionSave(mods, this)); + } + + private ModCollection(string name, int index, int changeCounter, int version, List appliedSettings, + List inheritsFrom, Dictionary settings) + { + Name = name; + Index = index; + ChangeCounter = changeCounter; + Settings = appliedSettings; + UnusedSettings = settings; + DirectlyInheritsFrom = inheritsFrom; + foreach (var c in DirectlyInheritsFrom) + ((List)c.DirectParentOf).Add(this); + } } diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index ccc893d3..dc77ee37 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -102,7 +102,8 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(s => (ModStorage) s.GetRequiredService()); // Add Resource services services.AddSingleton() diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index ddb6aba1..fde8ea62 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -9,7 +9,9 @@ using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.UI.Classes; +using Penumbra.Util; using SixLabors.ImageSharp; namespace Penumbra.Services; @@ -20,40 +22,32 @@ namespace Penumbra.Services; /// public class ConfigMigrationService { - private readonly FilenameService _fileNames; - private readonly DalamudPluginInterface _pluginInterface; + private readonly SaveService _saveService; private Configuration _config = null!; - private JObject _data = null!; + private JObject _data = null!; - public string CurrentCollection = ModCollection.DefaultCollectionName; - public string DefaultCollection = ModCollection.DefaultCollectionName; - public string ForcedCollection = string.Empty; + public string CurrentCollection = ModCollection.DefaultCollectionName; + public string DefaultCollection = ModCollection.DefaultCollectionName; + public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = new(); - public Dictionary ModSortOrder = new(); - public bool InvertModListOrder; - public bool SortFoldersFirst; - public SortModeV3 SortMode = SortModeV3.FoldersFirst; + public Dictionary ModSortOrder = new(); + public bool InvertModListOrder; + public bool SortFoldersFirst; + public SortModeV3 SortMode = SortModeV3.FoldersFirst; - public ConfigMigrationService(FilenameService fileNames, DalamudPluginInterface pi) - { - _fileNames = fileNames; - _pluginInterface = pi; - } + public ConfigMigrationService(SaveService saveService) + => _saveService = saveService; /// Add missing colors to the dictionary if necessary. private static void AddColors(Configuration config, bool forceSave) { var save = false; foreach (var color in Enum.GetValues()) - { save |= config.Colors.TryAdd(color, color.Data().DefaultColor); - } if (save || forceSave) - { config.Save(); - } } public void Migrate(Configuration config) @@ -63,13 +57,13 @@ public class ConfigMigrationService // because it stayed alive for a bunch of people for some reason. DeleteMetaTmp(); - if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_fileNames.ConfigFile)) + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile)) { AddColors(config, false); return; } - _data = JObject.Parse(File.ReadAllText(_fileNames.ConfigFile)); + _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); CreateBackup(); Version0To1(); @@ -88,7 +82,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - ActiveCollectionMigration.MigrateUngenderedCollections(_fileNames); + ActiveCollectionMigration.MigrateUngenderedCollections(_saveService.FileNames); _config.Version = 7; } @@ -115,7 +109,7 @@ public class ConfigMigrationService return; ModBackup.MigrateModBackups = true; - _config.Version = 5; + _config.Version = 5; } // SortMode was changed from an enum to a type. @@ -127,15 +121,15 @@ public class ConfigMigrationService SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; _config.SortMode = SortMode switch { - SortModeV3.FoldersFirst => ISortMode.FoldersFirst, - SortModeV3.Lexicographical => ISortMode.Lexicographical, - SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, + SortModeV3.FoldersFirst => ISortMode.FoldersFirst, + SortModeV3.Lexicographical => ISortMode.Lexicographical, + SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, - SortModeV3.FoldersLast => ISortMode.FoldersLast, - SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, - SortModeV3.InternalOrder => ISortMode.InternalOrder, - SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, - _ => ISortMode.FoldersFirst, + SortModeV3.FoldersLast => ISortMode.FoldersLast, + SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, + SortModeV3.InternalOrder => ISortMode.InternalOrder, + SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, + _ => ISortMode.FoldersFirst, }; _config.Version = 4; } @@ -147,8 +141,8 @@ public class ConfigMigrationService return; SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; - SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; - _config.Version = 3; + SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; + _config.Version = 3; } // The forced collection was removed due to general inheritance. @@ -192,7 +186,7 @@ public class ConfigMigrationService return; // Add the previous forced collection to all current collections except itself as an inheritance. - foreach (var collection in _fileNames.CollectionFiles) + foreach (var collection in _saveService.FileNames.CollectionFiles) { try { @@ -215,10 +209,10 @@ public class ConfigMigrationService private void ResettleSortOrder() { ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; - var file = _fileNames.FilesystemFile; + var file = _saveService.FileNames.FilesystemFile; using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); + using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); j.WritePropertyName("Data"); @@ -239,10 +233,10 @@ public class ConfigMigrationService // Move the active collections to their own file. private void ResettleCollectionSettings() { - CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; - DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; + CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; + DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; - SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, + SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); } @@ -250,12 +244,12 @@ public class ConfigMigrationService private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, IEnumerable<(CollectionType, string)> special) { - var file = _fileNames.ActiveCollectionsFile; + var file = _saveService.FileNames.ActiveCollectionsFile; try { using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); + using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); j.WritePropertyName(nameof(ActiveCollections.Default)); @@ -295,19 +289,18 @@ public class ConfigMigrationService return; _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; - _config.Version = 1; + _config.Version = 1; ResettleCollectionJson(); } - // Move the previous mod configurations to a new default collection file. + /// Move the previous mod configurations to a new default collection file. private void ResettleCollectionJson() { var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); if (!collectionJson.Exists) return; - var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollectionName); - var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection)); + var defaultCollectionFile = new FileInfo(_saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; @@ -317,18 +310,18 @@ public class ConfigMigrationService var data = JArray.Parse(text); var maxPriority = 0; - var dict = new Dictionary(); + var dict = new Dictionary(); foreach (var setting in data.Cast()) { - var modName = (string)setting["FolderName"]!; - var enabled = (bool)setting["Enabled"]!; + var modName = (string)setting["FolderName"]!; + var enabled = (bool)setting["Enabled"]!; var priority = (int)setting["Priority"]!; var settings = setting["Settings"]!.ToObject>() ?? setting["Conf"]!.ToObject>(); dict[modName] = new ModSettings.SavedSettings() { - Enabled = enabled, + Enabled = enabled, Priority = priority, Settings = settings!, }; @@ -339,8 +332,9 @@ public class ConfigMigrationService if (!InvertModListOrder) dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); - defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollectionName, dict); - Penumbra.SaveService.ImmediateSave(defaultCollection); + var emptyStorage = new ModStorage(); + var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict); + _saveService.ImmediateSave(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) { @@ -352,7 +346,7 @@ public class ConfigMigrationService // Create a backup of the configuration file specifically. private void CreateBackup() { - var name = _fileNames.ConfigFile; + var name = _saveService.FileNames.ConfigFile; var bakName = name + ".bak"; try { @@ -366,13 +360,13 @@ public class ConfigMigrationService public enum SortModeV3 : byte { - FoldersFirst = 0x00, - Lexicographical = 0x01, - InverseFoldersFirst = 0x02, + FoldersFirst = 0x00, + Lexicographical = 0x01, + InverseFoldersFirst = 0x02, InverseLexicographical = 0x03, - FoldersLast = 0x04, - InverseFoldersLast = 0x05, - InternalOrder = 0x06, - InternalOrderInverse = 0x07, + FoldersLast = 0x04, + InverseFoldersLast = 0x05, + InternalOrder = 0x06, + InternalOrderInverse = 0x07, } } diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 18dd3b95..0cf6474a 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -87,15 +87,15 @@ public class CollectionsTab : IDisposable, ITab /// Draw the Clean Unused Settings button if there are any. private void DrawCleanCollectionButton(Vector2 width) { - if (!_collectionManager.Active.Current.HasUnusedSettings) + if (_collectionManager.Active.Current.UnusedSettings.Count == 0) return; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton( - $"Clean {_collectionManager.Active.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + $"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." , false)) - _collectionManager.Active.Current.CleanUnavailableSettings(); + _collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current); } /// Draw the new collection input as well as its buttons. From 828cd07df0f81f4b4323c4a8c6155d9212503469 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Apr 2023 21:21:26 +0200 Subject: [PATCH 0876/2451] Fix an imgui clipping issue. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 37789067..36c2f5f7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 377890671cba2e9edb8e792be3bf8f0a84a8b741 +Subproject commit 36c2f5f7e5af017b4ce6737f0ef7add873335cc7 From 0108e5163636349e8665790315fd8c8517801c70 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Apr 2023 16:22:06 +0200 Subject: [PATCH 0877/2451] Some renaming --- .../CollectionCacheManager.cs | 2 + .../Collections/Cache/ModCollectionCache.cs | 485 ++++++++++++++++++ .../Collections/Manager/CollectionManager.cs | 2 - .../Manager/IndividualCollections.Files.cs | 1 - .../ModCollectionMigration.cs} | 16 +- .../Collections/ModCollection.Cache.Access.cs | 9 +- Penumbra/Collections/ModCollection.Cache.cs | 485 ------------------ Penumbra/Collections/ModCollection.cs | 3 +- ...ollection.File.cs => ModCollectionSave.cs} | 0 Penumbra/Collections/ResolveData.cs | 8 +- Penumbra/PenumbraNew.cs | 3 +- Penumbra/UI/Tabs/EffectiveTab.cs | 1 + 12 files changed, 509 insertions(+), 506 deletions(-) rename Penumbra/Collections/{Manager => Cache}/CollectionCacheManager.cs (96%) create mode 100644 Penumbra/Collections/Cache/ModCollectionCache.cs rename Penumbra/Collections/{ModCollection.Migration.cs => Manager/ModCollectionMigration.cs} (97%) delete mode 100644 Penumbra/Collections/ModCollection.Cache.cs rename Penumbra/Collections/{ModCollection.File.cs => ModCollectionSave.cs} (100%) diff --git a/Penumbra/Collections/Manager/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs similarity index 96% rename from Penumbra/Collections/Manager/CollectionCacheManager.cs rename to Penumbra/Collections/Cache/CollectionCacheManager.cs index 56a6e3a3..d2604418 100644 --- a/Penumbra/Collections/Manager/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.Collections.Cache; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -20,6 +21,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary private readonly Dictionary _cache = new(); public int Count diff --git a/Penumbra/Collections/Cache/ModCollectionCache.cs b/Penumbra/Collections/Cache/ModCollectionCache.cs new file mode 100644 index 00000000..32cc9a8e --- /dev/null +++ b/Penumbra/Collections/Cache/ModCollectionCache.cs @@ -0,0 +1,485 @@ +using OtterGui; +using OtterGui.Classes; +using Penumbra.Meta.Manager; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.Api.Enums; +using Penumbra.String.Classes; +using Penumbra.Mods.Manager; + +namespace Penumbra.Collections.Cache; + +public record struct ModPath(IMod Mod, FullPath Path); +public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, bool Solved); + +/// +/// The Cache contains all required temporary data to use a collection. +/// It will only be setup if a collection gets activated in any way. +/// +public class ModCollectionCache : IDisposable +{ + private readonly ModCollection _collection; + private readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary> _conflicts = new(); + + public IEnumerable> AllConflicts + => _conflicts.Values; + + public SingleArray Conflicts(IMod mod) + => _conflicts.TryGetValue(mod, out var c) ? c : new SingleArray(); + + private int _changedItemsSaveCounter = -1; + + // Obtain currently changed items. Computes them if they haven't been computed before. + public IReadOnlyDictionary, object?)> ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } + + // The cache reacts through events on its collection changing. + public ModCollectionCache(ModCollection collection) + { + _collection = collection; + MetaManipulations = new MetaManager(_collection); + } + + public void Dispose() + { + MetaManipulations.Dispose(); + } + + // Resolve a given game path according to this collection. + public FullPath? ResolvePath(Utf8GamePath gameResourcePath) + { + if (!ResolvedFiles.TryGetValue(gameResourcePath, out var candidate)) + { + return null; + } + + if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.Path.IsRooted && !candidate.Path.Exists) + { + return null; + } + + return candidate.Path; + } + + // For a given full path, find all game paths that currently use this file. + public IEnumerable ReverseResolvePath(FullPath localFilePath) + { + var needle = localFilePath.FullName.ToLower(); + if (localFilePath.IsRooted) + { + needle = needle.Replace('/', '\\'); + } + + var iterator = ResolvedFiles + .Where(f => string.Equals(f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase)) + .Select(kvp => kvp.Key); + + // For files that are not rooted, try to add themselves. + if (!localFilePath.IsRooted && Utf8GamePath.FromString(localFilePath.FullName, out var utf8)) + { + iterator = iterator.Prepend(utf8); + } + + return iterator; + } + + // Reverse resolve multiple paths at once for efficiency. + public HashSet[] ReverseResolvePaths(IReadOnlyCollection fullPaths) + { + if (fullPaths.Count == 0) + return Array.Empty>(); + + var ret = new HashSet[fullPaths.Count]; + var dict = new Dictionary(fullPaths.Count); + foreach (var (path, idx) in fullPaths.WithIndex()) + { + dict[new FullPath(path)] = idx; + ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8) + ? new HashSet { utf8 } + : new HashSet(); + } + + foreach (var (game, full) in ResolvedFiles) + { + if (dict.TryGetValue(full.Path, out var idx)) + { + ret[idx].Add(game); + } + } + + return ret; + } + + public void FullRecalculation(bool isDefault) + { + ResolvedFiles.Clear(); + MetaManipulations.Reset(); + _conflicts.Clear(); + + // Add all forced redirects. + foreach (var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( + Penumbra.TempMods.Mods.TryGetValue(_collection, out var list) ? list : Array.Empty())) + { + AddMod(tempMod, false); + } + + foreach (var mod in Penumbra.ModManager) + { + AddMod(mod, false); + } + + AddMetaFiles(); + + ++_collection.ChangeCounter; + + if (isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + + public void ReloadMod(IMod mod, bool addMetaChanges) + { + RemoveMod(mod, addMetaChanges); + AddMod(mod, addMetaChanges); + } + + public void RemoveMod(IMod mod, bool addMetaChanges) + { + var conflicts = Conflicts(mod); + + foreach (var (path, _) in mod.AllSubMods.SelectMany(s => s.Files.Concat(s.FileSwaps))) + { + if (!ResolvedFiles.TryGetValue(path, out var modPath)) + { + continue; + } + + if (modPath.Mod == mod) + { + ResolvedFiles.Remove(path); + } + } + + foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations)) + { + if (MetaManipulations.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod) + { + MetaManipulations.RevertMod(manipulation); + } + } + + _conflicts.Remove(mod); + foreach (var conflict in conflicts) + { + if (conflict.HasPriority) + { + ReloadMod(conflict.Mod2, false); + } + else + { + var newConflicts = Conflicts(conflict.Mod2).Remove(c => c.Mod2 == mod); + if (newConflicts.Count > 0) + { + _conflicts[conflict.Mod2] = newConflicts; + } + else + { + _conflicts.Remove(conflict.Mod2); + } + } + } + + if (addMetaChanges) + { + ++_collection.ChangeCounter; + if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + } + + + // Add all files and possibly manipulations of a given mod according to its settings in this collection. + public void AddMod(IMod mod, bool addMetaChanges) + { + if (mod.Index >= 0) + { + var settings = _collection[mod.Index].Settings; + if (settings is not { Enabled: true }) + { + return; + } + + foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority)) + { + if (group.Count == 0) + { + continue; + } + + var config = settings.Settings[groupIndex]; + switch (group.Type) + { + case GroupType.Single: + AddSubMod(group[(int)config], mod); + break; + case GroupType.Multi: + { + foreach (var (option, _) in group.WithIndex() + .Where(p => (1 << p.Item2 & config) != 0) + .OrderByDescending(p => group.OptionPriority(p.Item2))) + { + AddSubMod(option, mod); + } + + break; + } + } + } + } + + AddSubMod(mod.Default, mod); + + if (addMetaChanges) + { + ++_collection.ChangeCounter; + if (Penumbra.ModCaches[mod.Index].TotalManipulations > 0) + { + AddMetaFiles(); + } + + if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + } + + // Add all files and possibly manipulations of a specific submod + private void AddSubMod(ISubMod subMod, IMod parentMod) + { + foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) + { + AddFile(path, file, parentMod); + } + + foreach (var manip in subMod.Manipulations) + { + AddManipulation(manip, parentMod); + } + } + + // Add a specific file redirection, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddFile(Utf8GamePath path, FullPath file, IMod mod) + { + if (!ModCollection.CheckFullPath(path, file)) + { + return; + } + + if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) + { + return; + } + + var modPath = ResolvedFiles[path]; + // Lower prioritized option in the same mod. + if (mod == modPath.Mod) + { + return; + } + + if (AddConflict(path, mod, modPath.Mod)) + { + ResolvedFiles[path] = new ModPath(mod, file); + } + } + + + // Remove all empty conflict sets for a given mod with the given conflicts. + // If transitive is true, also removes the corresponding version of the other mod. + private void RemoveEmptyConflicts(IMod mod, SingleArray oldConflicts, bool transitive) + { + var changedConflicts = oldConflicts.Remove(c => + { + if (c.Conflicts.Count == 0) + { + if (transitive) + { + RemoveEmptyConflicts(c.Mod2, Conflicts(c.Mod2), false); + } + + return true; + } + + return false; + }); + if (changedConflicts.Count == 0) + { + _conflicts.Remove(mod); + } + else + { + _conflicts[mod] = changedConflicts; + } + } + + // Add a new conflict between the added mod and the existing mod. + // Update all other existing conflicts between the existing mod and other mods if necessary. + // Returns if the added mod takes priority before the existing mod. + private bool AddConflict(object data, IMod addedMod, IMod existingMod) + { + var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority; + + if (existingPriority < addedPriority) + { + var tmpConflicts = Conflicts(existingMod); + foreach (var conflict in tmpConflicts) + { + if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0) + { + AddConflict(data, addedMod, conflict.Mod2); + } + } + + RemoveEmptyConflicts(existingMod, tmpConflicts, true); + } + + var addedConflicts = Conflicts(addedMod); + var existingConflicts = Conflicts(existingMod); + if (addedConflicts.FindFirst(c => c.Mod2 == existingMod, out var oldConflicts)) + { + // Only need to change one list since both conflict lists refer to the same list. + oldConflicts.Conflicts.Add(data); + } + else + { + // Add the same conflict list to both conflict directions. + var conflictList = new List { data }; + _conflicts[addedMod] = addedConflicts.Append(new ModConflicts(existingMod, conflictList, existingPriority < addedPriority, + existingPriority != addedPriority)); + _conflicts[existingMod] = existingConflicts.Append(new ModConflicts(addedMod, conflictList, + existingPriority >= addedPriority, + existingPriority != addedPriority)); + } + + return existingPriority < addedPriority; + } + + // Add a specific manipulation, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddManipulation(MetaManipulation manip, IMod mod) + { + if (!MetaManipulations.TryGetValue(manip, out var existingMod)) + { + MetaManipulations.ApplyMod(manip, mod); + return; + } + + // Lower prioritized option in the same mod. + if (mod == existingMod) + { + return; + } + + if (AddConflict(manip, mod, existingMod)) + { + MetaManipulations.ApplyMod(manip, mod); + } + } + + + // Add all necessary meta file redirects. + private void AddMetaFiles() + => MetaManipulations.SetImcFiles(); + + // Increment the counter to ensure new files are loaded after applying meta changes. + private void IncrementCounter() + { + ++_collection.ChangeCounter; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + + // Identify and record all manipulated objects for this entire collection. + private void SetChangedItems() + { + if (_changedItemsSaveCounter == _collection.ChangeCounter) + { + return; + } + + try + { + _changedItemsSaveCounter = _collection.ChangeCounter; + _changedItems.Clear(); + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = Penumbra.Identifier; + var items = new SortedList(512); + + void AddItems(IMod mod) + { + foreach (var (name, obj) in items) + { + if (!_changedItems.TryGetValue(name, out var data)) + { + _changedItems.Add(name, (new SingleArray(mod), obj)); + } + else if (!data.Item1.Contains(mod)) + { + _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj); + } + else if (obj is int x && data.Item2 is int y) + { + _changedItems[name] = (data.Item1, x + y); + } + } + + items.Clear(); + } + + foreach (var (resolved, modPath) in ResolvedFiles.Where(file => !file.Key.Path.EndsWith("imc"u8))) + { + identifier.Identify(items, resolved.ToString()); + AddItems(modPath.Mod); + } + + foreach (var (manip, mod) in MetaManipulations) + { + ModCacheManager.ComputeChangedItems(identifier, items, manip); + AddItems(mod); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Unknown Error:\n{e}"); + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index 45e10c6a..5e1c5781 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -19,6 +19,4 @@ public class CollectionManager Temp = temp; Editor = editor; } - - } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index bc0d5737..8adf03fa 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -4,7 +4,6 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; -using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.String; diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs similarity index 97% rename from Penumbra/Collections/ModCollection.Migration.cs rename to Penumbra/Collections/Manager/ModCollectionMigration.cs index 02ecea47..c0ddd0e4 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -4,20 +4,20 @@ using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Util; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; /// Migration to convert ModCollections from older versions to newer. internal static class ModCollectionMigration { - /// Migrate a mod collection to the current version. + /// Migrate a mod collection to the current version. public static void Migrate(SaveService saver, ModStorage mods, int version, ModCollection collection) - { + { var changes = MigrateV0ToV1(collection, ref version); if (changes) saver.ImmediateSave(new ModCollectionSave(mods, collection)); } - - /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. + + /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. private static bool MigrateV0ToV1(ModCollection collection, ref int version) { if (version > 0) @@ -38,10 +38,10 @@ internal static class ModCollectionMigration return true; } - /// We treat every completely defaulted setting as inheritance-ready. + /// We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); - + => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); + /// private static bool SettingIsDefaultV0(ModSettings? setting) => setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3dba6903..d85e3256 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -13,7 +13,8 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; - +using Penumbra.Collections.Cache; + namespace Penumbra.Collections; public partial class ModCollection @@ -24,8 +25,10 @@ public partial class ModCollection public bool HasCache => _cache != null; - // Count the number of changes of the effective file list. - // This is used for material and imc changes. + /// + /// Count the number of changes of the effective file list. + /// This is used for material and imc changes. + /// public int ChangeCounter { get; internal set; } // Only create, do not update. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs deleted file mode 100644 index 5ed7f801..00000000 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ /dev/null @@ -1,485 +0,0 @@ -using OtterGui; -using OtterGui.Classes; -using Penumbra.Meta.Manager; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.Api.Enums; -using Penumbra.String.Classes; -using Penumbra.Mods.Manager; - -namespace Penumbra.Collections; - -public record struct ModPath( IMod Mod, FullPath Path ); -public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); - -/// -/// The Cache contains all required temporary data to use a collection. -/// It will only be setup if a collection gets activated in any way. -/// -public class ModCollectionCache : IDisposable -{ - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); - - public IEnumerable< SingleArray< ModConflicts > > AllConflicts - => _conflicts.Values; - - public SingleArray< ModConflicts > Conflicts( IMod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); - - private int _changedItemsSaveCounter = -1; - - // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - // The cache reacts through events on its collection changing. - public ModCollectionCache( ModCollection collection ) - { - _collection = collection; - MetaManipulations = new MetaManager( _collection ); - } - - public void Dispose() - { - MetaManipulations.Dispose(); - } - - // Resolve a given game path according to this collection. - public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) - { - return null; - } - - return candidate.Path; - } - - // For a given full path, find all game paths that currently use this file. - public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) - { - var needle = localFilePath.FullName.ToLower(); - if( localFilePath.IsRooted ) - { - needle = needle.Replace( '/', '\\' ); - } - - var iterator = ResolvedFiles - .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) - .Select( kvp => kvp.Key ); - - // For files that are not rooted, try to add themselves. - if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) - { - iterator = iterator.Prepend( utf8 ); - } - - return iterator; - } - - // Reverse resolve multiple paths at once for efficiency. - public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) - { - if( fullPaths.Count == 0 ) - return Array.Empty< HashSet< Utf8GamePath > >(); - - var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; - var dict = new Dictionary< FullPath, int >( fullPaths.Count ); - foreach( var (path, idx) in fullPaths.WithIndex() ) - { - dict[ new FullPath(path) ] = idx; - ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) - ? new HashSet< Utf8GamePath > { utf8 } - : new HashSet< Utf8GamePath >(); - } - - foreach( var (game, full) in ResolvedFiles ) - { - if( dict.TryGetValue( full.Path, out var idx ) ) - { - ret[ idx ].Add( game ); - } - } - - return ret; - } - - public void FullRecalculation(bool isDefault) - { - ResolvedFiles.Clear(); - MetaManipulations.Reset(); - _conflicts.Clear(); - - // Add all forced redirects. - foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( - Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) - { - AddMod( tempMod, false ); - } - - foreach( var mod in Penumbra.ModManager ) - { - AddMod( mod, false ); - } - - AddMetaFiles(); - - ++_collection.ChangeCounter; - - if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - - public void ReloadMod( IMod mod, bool addMetaChanges ) - { - RemoveMod( mod, addMetaChanges ); - AddMod( mod, addMetaChanges ); - } - - public void RemoveMod( IMod mod, bool addMetaChanges ) - { - var conflicts = Conflicts( mod ); - - foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) - { - if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) - { - continue; - } - - if( modPath.Mod == mod ) - { - ResolvedFiles.Remove( path ); - } - } - - foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) - { - if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) - { - MetaManipulations.RevertMod( manipulation ); - } - } - - _conflicts.Remove( mod ); - foreach( var conflict in conflicts ) - { - if( conflict.HasPriority ) - { - ReloadMod( conflict.Mod2, false ); - } - else - { - var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); - if( newConflicts.Count > 0 ) - { - _conflicts[ conflict.Mod2 ] = newConflicts; - } - else - { - _conflicts.Remove( conflict.Mod2 ); - } - } - } - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - } - - - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod( IMod mod, bool addMetaChanges ) - { - if( mod.Index >= 0 ) - { - var settings = _collection[ mod.Index ].Settings; - if( settings is not { Enabled: true } ) - { - return; - } - - foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) - { - if( group.Count == 0 ) - { - continue; - } - - var config = settings.Settings[ groupIndex ]; - switch( group.Type ) - { - case GroupType.Single: - AddSubMod( group[ ( int )config ], mod ); - break; - case GroupType.Multi: - { - foreach( var (option, _) in group.WithIndex() - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) - { - AddSubMod( option, mod ); - } - - break; - } - } - } - } - - AddSubMod( mod.Default, mod ); - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if(Penumbra.ModCaches[mod.Index].TotalManipulations > 0 ) - { - AddMetaFiles(); - } - - if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - } - - // Add all files and possibly manipulations of a specific submod - private void AddSubMod( ISubMod subMod, IMod parentMod ) - { - foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) - { - AddFile( path, file, parentMod ); - } - - foreach( var manip in subMod.Manipulations ) - { - AddManipulation( manip, parentMod ); - } - } - - // Add a specific file redirection, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) - { - if( !ModCollection.CheckFullPath( path, file ) ) - { - return; - } - - if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) - { - return; - } - - var modPath = ResolvedFiles[ path ]; - // Lower prioritized option in the same mod. - if( mod == modPath.Mod ) - { - return; - } - - if( AddConflict( path, mod, modPath.Mod ) ) - { - ResolvedFiles[ path ] = new ModPath( mod, file ); - } - } - - - // Remove all empty conflict sets for a given mod with the given conflicts. - // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) - { - var changedConflicts = oldConflicts.Remove( c => - { - if( c.Conflicts.Count == 0 ) - { - if( transitive ) - { - RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); - } - - return true; - } - - return false; - } ); - if( changedConflicts.Count == 0 ) - { - _conflicts.Remove( mod ); - } - else - { - _conflicts[ mod ] = changedConflicts; - } - } - - // Add a new conflict between the added mod and the existing mod. - // Update all other existing conflicts between the existing mod and other mods if necessary. - // Returns if the added mod takes priority before the existing mod. - private bool AddConflict( object data, IMod addedMod, IMod existingMod ) - { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; - - if( existingPriority < addedPriority ) - { - var tmpConflicts = Conflicts( existingMod ); - foreach( var conflict in tmpConflicts ) - { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) - { - AddConflict( data, addedMod, conflict.Mod2 ); - } - } - - RemoveEmptyConflicts( existingMod, tmpConflicts, true ); - } - - var addedConflicts = Conflicts( addedMod ); - var existingConflicts = Conflicts( existingMod ); - if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) - { - // Only need to change one list since both conflict lists refer to the same list. - oldConflicts.Conflicts.Add( data ); - } - else - { - // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, - existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, - existingPriority >= addedPriority, - existingPriority != addedPriority ) ); - } - - return existingPriority < addedPriority; - } - - // Add a specific manipulation, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, IMod mod ) - { - if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - return; - } - - // Lower prioritized option in the same mod. - if( mod == existingMod ) - { - return; - } - - if( AddConflict( manip, mod, existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - } - - - // Add all necessary meta file redirects. - private void AddMetaFiles() - => MetaManipulations.SetImcFiles(); - - // Increment the counter to ensure new files are loaded after applying meta changes. - private void IncrementCounter() - { - ++_collection.ChangeCounter; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - - - // Identify and record all manipulated objects for this entire collection. - private void SetChangedItems() - { - if( _changedItemsSaveCounter == _collection.ChangeCounter ) - { - return; - } - - try - { - _changedItemsSaveCounter = _collection.ChangeCounter; - _changedItems.Clear(); - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = Penumbra.Identifier; - var items = new SortedList< string, object? >( 512 ); - - void AddItems( IMod mod ) - { - foreach( var (name, obj) in items ) - { - if( !_changedItems.TryGetValue( name, out var data ) ) - { - _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); - } - else if( !data.Item1.Contains( mod ) ) - { - _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); - } - else if( obj is int x && data.Item2 is int y ) - { - _changedItems[ name ] = ( data.Item1, x + y ); - } - } - - items.Clear(); - } - - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) - { - identifier.Identify( items, resolved.ToString() ); - AddItems( modPath.Mod ); - } - - foreach( var (manip, mod) in MetaManipulations ) - { - ModCacheManager.ComputeChangedItems(identifier, items, manip ); - AddItems( mod ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Unknown Error:\n{e}" ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index fc0ac1b7..026516cd 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -5,7 +5,8 @@ using System.Diagnostics; using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Util; - +using Penumbra.Collections.Manager; + namespace Penumbra.Collections; /// diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollectionSave.cs similarity index 100% rename from Penumbra/Collections/ModCollection.File.cs rename to Penumbra/Collections/ModCollectionSave.cs diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index c604d572..e0901e98 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -16,11 +16,9 @@ public readonly struct ResolveData public bool Valid => _modCollection != null; - public ResolveData() - { - _modCollection = null!; - AssociatedGameObject = nint.Zero; - } + public ResolveData() + : this(null!, nint.Zero) + { } public ResolveData(ModCollection collection, nint gameObject) { diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index dc77ee37..d1227472 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -22,7 +22,8 @@ using Penumbra.Util; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using Penumbra.Mods.Manager; - +using Penumbra.Collections.Cache; + namespace Penumbra; public class PenumbraNew diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index a578e8d2..d66c680e 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; From 85fb98b557280e513033be95f693e3e4d921a80b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Apr 2023 22:26:14 +0200 Subject: [PATCH 0878/2451] tmp --- ...dCollectionCache.cs => CollectionCache.cs} | 153 +++--------- .../Cache/CollectionCacheManager.cs | 234 ++++++++++++------ .../Cache/MetaCache.Cmp.cs} | 4 +- .../Cache/MetaCache.Eqdp.cs} | 4 +- .../Cache/MetaCache.Eqp.cs} | 4 +- .../Cache/MetaCache.Est.cs} | 4 +- .../Cache/MetaCache.Gmp.cs} | 4 +- .../Cache/MetaCache.Imc.cs} | 4 +- .../Cache/MetaCache.cs} | 19 +- .../Collections/Manager/ActiveCollections.cs | 4 +- .../Collections/Manager/CollectionManager.cs | 2 + .../Manager/TempCollectionManager.cs | 19 +- .../Collections/ModCollection.Cache.Access.cs | 50 +--- Penumbra/Collections/ModCollection.cs | 7 +- Penumbra/Penumbra.cs | 11 +- 15 files changed, 230 insertions(+), 293 deletions(-) rename Penumbra/Collections/Cache/{ModCollectionCache.cs => CollectionCache.cs} (79%) rename Penumbra/{Meta/Manager/MetaManager.Cmp.cs => Collections/Cache/MetaCache.Cmp.cs} (95%) rename Penumbra/{Meta/Manager/MetaManager.Eqdp.cs => Collections/Cache/MetaCache.Eqdp.cs} (97%) rename Penumbra/{Meta/Manager/MetaManager.Eqp.cs => Collections/Cache/MetaCache.Eqp.cs} (95%) rename Penumbra/{Meta/Manager/MetaManager.Est.cs => Collections/Cache/MetaCache.Est.cs} (98%) rename Penumbra/{Meta/Manager/MetaManager.Gmp.cs => Collections/Cache/MetaCache.Gmp.cs} (95%) rename Penumbra/{Meta/Manager/MetaManager.Imc.cs => Collections/Cache/MetaCache.Imc.cs} (97%) rename Penumbra/{Meta/Manager/MetaManager.cs => Collections/Cache/MetaCache.cs} (90%) diff --git a/Penumbra/Collections/Cache/ModCollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs similarity index 79% rename from Penumbra/Collections/Cache/ModCollectionCache.cs rename to Penumbra/Collections/Cache/CollectionCache.cs index 32cc9a8e..de95cae6 100644 --- a/Penumbra/Collections/Cache/ModCollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -1,6 +1,5 @@ using OtterGui; using OtterGui.Classes; -using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; @@ -9,24 +8,24 @@ using System.IO; using System.Linq; using Penumbra.Api.Enums; using Penumbra.String.Classes; -using Penumbra.Mods.Manager; - +using Penumbra.Mods.Manager; + namespace Penumbra.Collections.Cache; public record struct ModPath(IMod Mod, FullPath Path); public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, bool Solved); -/// +/// /// The Cache contains all required temporary data to use a collection. -/// It will only be setup if a collection gets activated in any way. +/// It will only be setup if a collection gets activated in any way. /// public class ModCollectionCache : IDisposable { - private readonly ModCollection _collection; - private readonly SortedList, object?)> _changedItems = new(); - public readonly Dictionary ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary> _conflicts = new(); + private readonly ModCollection _collection; + public readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaCache MetaManipulations; + public readonly Dictionary> _conflicts = new(); public IEnumerable> AllConflicts => _conflicts.Values; @@ -49,8 +48,8 @@ public class ModCollectionCache : IDisposable // The cache reacts through events on its collection changing. public ModCollectionCache(ModCollection collection) { - _collection = collection; - MetaManipulations = new MetaManager(_collection); + _collection = collection; + MetaManipulations = new MetaCache(_collection); } public void Dispose() @@ -62,15 +61,11 @@ public class ModCollectionCache : IDisposable public FullPath? ResolvePath(Utf8GamePath gameResourcePath) { if (!ResolvedFiles.TryGetValue(gameResourcePath, out var candidate)) - { return null; - } if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists) - { + || candidate.Path.IsRooted && !candidate.Path.Exists) return null; - } return candidate.Path; } @@ -80,9 +75,7 @@ public class ModCollectionCache : IDisposable { var needle = localFilePath.FullName.ToLower(); if (localFilePath.IsRooted) - { needle = needle.Replace('/', '\\'); - } var iterator = ResolvedFiles .Where(f => string.Equals(f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase)) @@ -90,9 +83,7 @@ public class ModCollectionCache : IDisposable // For files that are not rooted, try to add themselves. if (!localFilePath.IsRooted && Utf8GamePath.FromString(localFilePath.FullName, out var utf8)) - { iterator = iterator.Prepend(utf8); - } return iterator; } @@ -103,7 +94,7 @@ public class ModCollectionCache : IDisposable if (fullPaths.Count == 0) return Array.Empty>(); - var ret = new HashSet[fullPaths.Count]; + var ret = new HashSet[fullPaths.Count]; var dict = new Dictionary(fullPaths.Count); foreach (var (path, idx) in fullPaths.WithIndex()) { @@ -116,43 +107,12 @@ public class ModCollectionCache : IDisposable foreach (var (game, full) in ResolvedFiles) { if (dict.TryGetValue(full.Path, out var idx)) - { ret[idx].Add(game); - } } return ret; } - public void FullRecalculation(bool isDefault) - { - ResolvedFiles.Clear(); - MetaManipulations.Reset(); - _conflicts.Clear(); - - // Add all forced redirects. - foreach (var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( - Penumbra.TempMods.Mods.TryGetValue(_collection, out var list) ? list : Array.Empty())) - { - AddMod(tempMod, false); - } - - foreach (var mod in Penumbra.ModManager) - { - AddMod(mod, false); - } - - AddMetaFiles(); - - ++_collection.ChangeCounter; - - if (isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - public void ReloadMod(IMod mod, bool addMetaChanges) { RemoveMod(mod, addMetaChanges); @@ -166,22 +126,16 @@ public class ModCollectionCache : IDisposable foreach (var (path, _) in mod.AllSubMods.SelectMany(s => s.Files.Concat(s.FileSwaps))) { if (!ResolvedFiles.TryGetValue(path, out var modPath)) - { continue; - } if (modPath.Mod == mod) - { ResolvedFiles.Remove(path); - } } foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations)) { if (MetaManipulations.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod) - { MetaManipulations.RevertMod(manipulation); - } } _conflicts.Remove(mod); @@ -195,13 +149,9 @@ public class ModCollectionCache : IDisposable { var newConflicts = Conflicts(conflict.Mod2).Remove(c => c.Mod2 == mod); if (newConflicts.Count > 0) - { _conflicts[conflict.Mod2] = newConflicts; - } else - { _conflicts.Remove(conflict.Mod2); - } } } @@ -224,16 +174,12 @@ public class ModCollectionCache : IDisposable { var settings = _collection[mod.Index].Settings; if (settings is not { Enabled: true }) - { return; - } foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority)) { if (group.Count == 0) - { continue; - } var config = settings.Settings[groupIndex]; switch (group.Type) @@ -241,17 +187,15 @@ public class ModCollectionCache : IDisposable case GroupType.Single: AddSubMod(group[(int)config], mod); break; - case GroupType.Multi: - { - foreach (var (option, _) in group.WithIndex() - .Where(p => (1 << p.Item2 & config) != 0) - .OrderByDescending(p => group.OptionPriority(p.Item2))) - { - AddSubMod(option, mod); - } - - break; - } + case GroupType.Multi: + { + foreach (var (option, _) in group.WithIndex() + .Where(p => ((1 << p.Item2) & config) != 0) + .OrderByDescending(p => group.OptionPriority(p.Item2))) + AddSubMod(option, mod); + + break; + } } } } @@ -262,9 +206,7 @@ public class ModCollectionCache : IDisposable { ++_collection.ChangeCounter; if (Penumbra.ModCaches[mod.Index].TotalManipulations > 0) - { AddMetaFiles(); - } if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) { @@ -278,14 +220,10 @@ public class ModCollectionCache : IDisposable private void AddSubMod(ISubMod subMod, IMod parentMod) { foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) - { AddFile(path, file, parentMod); - } foreach (var manip in subMod.Manipulations) - { AddManipulation(manip, parentMod); - } } // Add a specific file redirection, handling potential conflicts. @@ -295,26 +233,18 @@ public class ModCollectionCache : IDisposable private void AddFile(Utf8GamePath path, FullPath file, IMod mod) { if (!ModCollection.CheckFullPath(path, file)) - { return; - } if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) - { return; - } var modPath = ResolvedFiles[path]; // Lower prioritized option in the same mod. if (mod == modPath.Mod) - { return; - } if (AddConflict(path, mod, modPath.Mod)) - { ResolvedFiles[path] = new ModPath(mod, file); - } } @@ -327,9 +257,7 @@ public class ModCollectionCache : IDisposable if (c.Conflicts.Count == 0) { if (transitive) - { RemoveEmptyConflicts(c.Mod2, Conflicts(c.Mod2), false); - } return true; } @@ -337,13 +265,9 @@ public class ModCollectionCache : IDisposable return false; }); if (changedConflicts.Count == 0) - { _conflicts.Remove(mod); - } else - { _conflicts[mod] = changedConflicts; - } } // Add a new conflict between the added mod and the existing mod. @@ -351,7 +275,7 @@ public class ModCollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) @@ -360,16 +284,14 @@ public class ModCollectionCache : IDisposable foreach (var conflict in tmpConflicts) { if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0) - { + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0) AddConflict(data, addedMod, conflict.Mod2); - } } RemoveEmptyConflicts(existingMod, tmpConflicts, true); } - var addedConflicts = Conflicts(addedMod); + var addedConflicts = Conflicts(addedMod); var existingConflicts = Conflicts(existingMod); if (addedConflicts.FindFirst(c => c.Mod2 == existingMod, out var oldConflicts)) { @@ -404,36 +326,23 @@ public class ModCollectionCache : IDisposable // Lower prioritized option in the same mod. if (mod == existingMod) - { return; - } if (AddConflict(manip, mod, existingMod)) - { MetaManipulations.ApplyMod(manip, mod); - } } // Add all necessary meta file redirects. - private void AddMetaFiles() + public void AddMetaFiles() => MetaManipulations.SetImcFiles(); - // Increment the counter to ensure new files are loaded after applying meta changes. - private void IncrementCounter() - { - ++_collection.ChangeCounter; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { if (_changedItemsSaveCounter == _collection.ChangeCounter) - { return; - } try { @@ -442,24 +351,18 @@ public class ModCollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = Penumbra.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { foreach (var (name, obj) in items) { if (!_changedItems.TryGetValue(name, out var data)) - { _changedItems.Add(name, (new SingleArray(mod), obj)); - } else if (!data.Item1.Contains(mod)) - { _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj); - } else if (obj is int x && data.Item2 is int y) - { _changedItems[name] = (data.Item1, x + y); - } } items.Clear(); @@ -482,4 +385,4 @@ public class ModCollectionCache : IDisposable Penumbra.Log.Error($"Unknown Error:\n{e}"); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index d2604418..aa24d208 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,61 +1,53 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.Collections.Cache; +using Penumbra.Collections.Manager; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; -namespace Penumbra.Collections.Manager; +namespace Penumbra.Collections.Cache; -public class CollectionCacheManager : IDisposable, IReadOnlyDictionary +public class CollectionCacheManager : IDisposable { - private readonly ActiveCollections _active; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _characterUtility; + private readonly FrameworkManager _framework; + private readonly ActiveCollections _active; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _characterUtility; + private readonly TempModManager _tempMods; + private readonly ModStorage _modStorage; + private readonly ModCacheManager _modCaches; + private readonly Configuration _config; + private readonly ResidentResourceManager _resources; - private readonly List<(ModCollectionCache, int ChangeCounter)> - private readonly Dictionary _cache = new(); + private readonly Dictionary _caches = new(); public int Count - => _cache.Count; + => _caches.Count; - public IEnumerator> GetEnumerator() - => _cache.GetEnumerator(); + public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active + => _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value)); - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public bool ContainsKey(ModCollection key) - => _cache.ContainsKey(key); - - public bool TryGetValue(ModCollection key, [NotNullWhen(true)] out ModCollectionCache? value) - => _cache.TryGetValue(key, out value); - - public ModCollectionCache this[ModCollection key] - => _cache[key]; - - public IEnumerable Keys - => _cache.Keys; - - public IEnumerable Values - => _cache.Values; - - public IEnumerable Active - => _cache.Keys.Where(c => c.Index > ModCollection.Empty.Index); - - public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator, CharacterUtility characterUtility) + public CollectionCacheManager(FrameworkManager framework, ActiveCollections active, CommunicatorService communicator, + CharacterUtility characterUtility, TempModManager tempMods, ModStorage modStorage, Configuration config, + ResidentResourceManager resources, ModCacheManager modCaches) { + _framework = framework; _active = active; _communicator = communicator; _characterUtility = characterUtility; + _tempMods = tempMods; + _modStorage = modStorage; + _config = config; + _resources = resources; + _modCaches = modCaches; _communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); @@ -82,50 +74,103 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary - /// Cache handling. Usually recreate caches on the next framework tick, - /// but at launch create all of them at once. - /// - public void CreateNecessaryCaches() + /// Only creates a new cache, does not update an existing one. + public bool CreateCache(ModCollection collection) { - var tasks = _active.SpecialAssignments.Select(p => p.Value) - .Concat(_active.Individuals.Select(p => p.Collection)) - .Prepend(_active.Current) - .Prepend(_active.Default) - .Prepend(_active.Interface) - .Distinct() - .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == _active.Default))) - .ToArray(); + if (_caches.ContainsKey(collection) || collection.Index == ModCollection.Empty.Index) + return false; - Task.WaitAll(tasks); + var cache = new CollectionCache(collection); + _caches.Add(collection, cache); + collection._cache = cache; + Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); + return true; + } + + /// + /// Update the effective file list for the given cache. + /// Does not create caches. + /// + public void CalculateEffectiveFileList(ModCollection collection) + => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, + () => CalculateEffectiveFileListInternal(collection)); + + private void CalculateEffectiveFileListInternal(ModCollection collection) + { + // Skip the empty collection. + if (collection.Index == 0) + return; + + Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); + if (!_caches.TryGetValue(collection, out var cache)) + { + Penumbra.Log.Error( + $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); + return; + } + + FullRecalculation(collection, cache); + + Penumbra.Log.Debug( + $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); + } + + private void FullRecalculation(ModCollection collection, CollectionCache cache) + { + cache.ResolvedFiles.Clear(); + cache.MetaManipulations.Reset(); + cache._conflicts.Clear(); + + // Add all forced redirects. + foreach (var tempMod in _tempMods.ModsForAllCollections.Concat( + _tempMods.Mods.TryGetValue(collection, out var list) ? list : Array.Empty())) + cache.AddMod(tempMod, false); + + foreach (var mod in _modStorage) + cache.AddMod(mod, false); + + cache.AddMetaFiles(); + + ++collection.ChangeCounter; + + if (_active.Default != collection || !_characterUtility.Ready || !_config.EnableMods) + return; + + _resources.Reload(); + cache.MetaManipulations.SetFiles(); } private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) { - if (type is CollectionType.Inactive) - return; - - var isDefault = type is CollectionType.Default; - if (newCollection?.Index > ModCollection.Empty.Index) + if (type is CollectionType.Temporary) { - newCollection.CreateCache(isDefault); - _cache.TryAdd(newCollection, newCollection._cache!); - } + if (newCollection != null && CreateCache(newCollection)) + CalculateEffectiveFileList(newCollection); - RemoveCache(old); + if (old != null) + ClearCache(old); + } + else + { + RemoveCache(old); + + if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection)) + CalculateEffectiveFileList(newCollection); + } } + private void OnModChangeRemoval(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath) { switch (type) { case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: - foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _caches.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) collection._cache!.RemoveMod(mod, true); break; case ModPathChangeType.Moved: - foreach (var collection in _cache.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _caches.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); break; } @@ -136,13 +181,13 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _caches.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) collection._cache!.AddMod(mod, true); } /// Apply a mod change to all collections with a cache. private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_cache.Keys, mod, created, removed); + => TempModManager.OnGlobalModChange(_caches.Keys, mod, created, removed); /// Remove a cache from a collection if it is active. private void RemoveCache(ModCollection? collection) @@ -154,10 +199,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary c.Value.Index != collection.Index) && _active.Individuals.All(c => c.Collection.Index != collection.Index)) - { - _cache.Remove(collection); - collection.ClearCache(); - } + ClearCache(collection); } /// Prepare Changes by removing mods from caches with collections or add or reload mods. @@ -165,7 +207,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _caches.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) collection._cache!.RemoveMod(mod, false); return; @@ -176,7 +218,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _caches.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) { if (reload) collection._cache!.ReloadMod(mod, true); @@ -188,45 +230,45 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary Increment the counter to ensure new files are loaded after applying meta changes. private void IncrementCounters() { - foreach (var (collection, _) in _cache) + foreach (var (collection, _) in _caches) ++collection.ChangeCounter; _characterUtility.LoadingFinished -= IncrementCounters; } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) { - if (collection._cache == null) + if (!_caches.TryGetValue(collection, out var cache)) return; switch (type) { case ModSettingChange.Inheritance: - collection._cache.ReloadMod(mod!, true); + cache.ReloadMod(mod!, true); break; case ModSettingChange.EnableState: if (oldValue == 0) - collection._cache.AddMod(mod!, true); + cache.AddMod(mod!, true); else if (oldValue == 1) - collection._cache.RemoveMod(mod!, true); + cache.RemoveMod(mod!, true); else if (collection[mod!.Index].Settings?.Enabled == true) - collection._cache.ReloadMod(mod!, true); + cache.ReloadMod(mod!, true); else - collection._cache.RemoveMod(mod!, true); + cache.RemoveMod(mod!, true); break; case ModSettingChange.Priority: - if (collection._cache.Conflicts(mod!).Count > 0) - collection._cache.ReloadMod(mod!, true); + if (cache.Conflicts(mod!).Count > 0) + cache.ReloadMod(mod!, true); break; case ModSettingChange.Setting: if (collection[mod!.Index].Settings?.Enabled == true) - collection._cache.ReloadMod(mod!, true); + cache.ReloadMod(mod!, true); break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - collection._cache.FullRecalculation(collection == _active.Default); + FullRecalculation(collection, cache); break; } } @@ -236,5 +278,37 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary private void OnCollectionInheritanceChange(ModCollection collection, bool _) - => collection._cache?.FullRecalculation(collection == _active.Default); + { + if (_caches.TryGetValue(collection, out var cache)) + FullRecalculation(collection, cache); + } + + /// Clear the current cache of a collection. + private void ClearCache(ModCollection collection) + { + if (!_caches.Remove(collection, out var cache)) + return; + + cache.Dispose(); + collection._cache = null; + Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}."); + } + + /// + /// Cache handling. Usually recreate caches on the next framework tick, + /// but at launch create all of them at once. + /// + private void CreateNecessaryCaches() + { + var tasks = _active.SpecialAssignments.Select(p => p.Value) + .Concat(_active.Individuals.Select(p => p.Collection)) + .Prepend(_active.Current) + .Prepend(_active.Default) + .Prepend(_active.Interface) + .Distinct() + .Select(c => CreateCache(c) ? Task.Run(() => CalculateEffectiveFileListInternal(c)) : Task.CompletedTask) + .ToArray(); + + Task.WaitAll(tasks); + } } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Collections/Cache/MetaCache.Cmp.cs similarity index 95% rename from Penumbra/Meta/Manager/MetaManager.Cmp.cs rename to Penumbra/Collections/Cache/MetaCache.Cmp.cs index f1267220..35d97eee 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Collections/Cache/MetaCache.Cmp.cs @@ -6,9 +6,9 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private CmpFile? _cmpFile = null; private readonly List< RspManipulation > _cmpManipulations = new(); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Collections/Cache/MetaCache.Eqdp.cs similarity index 97% rename from Penumbra/Meta/Manager/MetaManager.Eqdp.cs rename to Penumbra/Collections/Cache/MetaCache.Eqdp.cs index f5667f68..5bac9169 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Collections/Cache/MetaCache.Eqdp.cs @@ -9,9 +9,9 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Collections/Cache/MetaCache.Eqp.cs similarity index 95% rename from Penumbra/Meta/Manager/MetaManager.Eqp.cs rename to Penumbra/Collections/Cache/MetaCache.Eqp.cs index 96194f9d..ce0829a0 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Collections/Cache/MetaCache.Eqp.cs @@ -6,9 +6,9 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private ExpandedEqpFile? _eqpFile = null; private readonly List< EqpManipulation > _eqpManipulations = new(); diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Collections/Cache/MetaCache.Est.cs similarity index 98% rename from Penumbra/Meta/Manager/MetaManager.Est.cs rename to Penumbra/Collections/Cache/MetaCache.Est.cs index 7e4a92db..6815be30 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Collections/Cache/MetaCache.Est.cs @@ -7,9 +7,9 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private EstFile? _estFaceFile = null; private EstFile? _estHairFile = null; diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Collections/Cache/MetaCache.Gmp.cs similarity index 95% rename from Penumbra/Meta/Manager/MetaManager.Gmp.cs rename to Penumbra/Collections/Cache/MetaCache.Gmp.cs index bb7e764b..f83efe77 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Collections/Cache/MetaCache.Gmp.cs @@ -6,9 +6,9 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private ExpandedGmpFile? _gmpFile = null; private readonly List< GmpManipulation > _gmpManipulations = new(); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Collections/Cache/MetaCache.Imc.cs similarity index 97% rename from Penumbra/Meta/Manager/MetaManager.Imc.cs rename to Penumbra/Collections/Cache/MetaCache.Imc.cs index cc09a13d..89e55b1b 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Collections/Cache/MetaCache.Imc.cs @@ -6,9 +6,9 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager +public partial class MetaCache { private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); private readonly List< ImcManipulation > _imcManipulations = new(); diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Collections/Cache/MetaCache.cs similarity index 90% rename from Penumbra/Meta/Manager/MetaManager.cs rename to Penumbra/Collections/Cache/MetaCache.cs index bd3a6086..0b6fa942 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -4,16 +4,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OtterGui; -using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -namespace Penumbra.Meta.Manager; +namespace Penumbra.Collections.Cache; -public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaManipulation, IMod > > +public partial class MetaCache : IDisposable, IEnumerable< KeyValuePair< MetaManipulation, IMod > > { private readonly Dictionary< MetaManipulation, IMod > _manipulations = new(); private readonly ModCollection _collection; @@ -33,7 +32,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public MetaManager( ModCollection collection ) + public MetaCache( ModCollection collection ) { _collection = collection; if( !Penumbra.CharacterUtility.Ready ) @@ -116,12 +115,12 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM // but they do require the file space to be ready. return manip.ManipulationType switch { - MetaManipulation.Type.Eqp => RevertMod( manip.Eqp ), - MetaManipulation.Type.Gmp => RevertMod( manip.Gmp ), - MetaManipulation.Type.Eqdp => RevertMod( manip.Eqdp ), - MetaManipulation.Type.Est => RevertMod( manip.Est ), - MetaManipulation.Type.Rsp => RevertMod( manip.Rsp ), - MetaManipulation.Type.Imc => RevertMod( manip.Imc ), + MetaManipulation.Type.Eqp => RevertMod( (MetaManipulation)manip.Eqp ), + MetaManipulation.Type.Gmp => RevertMod( (MetaManipulation)manip.Gmp ), + MetaManipulation.Type.Eqdp => RevertMod( (MetaManipulation)manip.Eqdp ), + MetaManipulation.Type.Est => RevertMod( (MetaManipulation)manip.Est ), + MetaManipulation.Type.Rsp => RevertMod( (MetaManipulation)manip.Rsp ), + MetaManipulation.Type.Imc => RevertMod( (MetaManipulation)manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 2ec3a33b..8fdeaa08 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -262,7 +262,7 @@ public class ActiveCollections : ISavable, IDisposable /// /// Load default, current, special, and character collections from config. - /// Then create caches. If a collection does not exist anymore, reset it to an appropriate default. + /// If a collection does not exist anymore, reset it to an appropriate default. /// private void LoadCollections() { @@ -338,7 +338,7 @@ public class ActiveCollections : ISavable, IDisposable configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, _storage); - // Save any changes and create all required caches. + // Save any changes. if (configChanged) _saveService.ImmediateSave(this); } diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index 5e1c5781..16bf754c 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -1,3 +1,5 @@ +using Penumbra.Collections.Cache; + namespace Penumbra.Collections.Manager; public class CollectionManager diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 42bbea19..8c64c8e0 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -54,9 +54,12 @@ public class TempCollectionManager : IDisposable GlobalChangeCounter = 0; var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) + { + // Temporary collection created. + _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty); return collection.Name; + } - collection.ClearCache(); return string.Empty; } @@ -66,12 +69,12 @@ public class TempCollectionManager : IDisposable return false; GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); - collection.ClearCache(); for (var i = 0; i < Collections.Count; ++i) { if (Collections[i].Collection != collection) continue; + // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); Collections.Delete(i--); } @@ -81,13 +84,13 @@ public class TempCollectionManager : IDisposable public bool AddIdentifier(ModCollection collection, params ActorIdentifier[] identifiers) { - if (Collections.Add(identifiers, collection)) - { - _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); - return true; - } + if (!Collections.Add(identifiers, collection)) + return false; + + // Temporary collection assignment added. + _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); + return true; - return false; } public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index d85e3256..f0bb1f29 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,6 +1,5 @@ using OtterGui.Classes; using Penumbra.GameData.Enums; -using Penumbra.Meta.Manager; using Penumbra.Mods; using System; using System.Collections.Generic; @@ -25,25 +24,6 @@ public partial class ModCollection public bool HasCache => _cache != null; - /// - /// Count the number of changes of the effective file list. - /// This is used for material and imc changes. - /// - public int ChangeCounter { get; internal set; } - - // Only create, do not update. - internal void CreateCache(bool isDefault) - { - if (_cache != null) - return; - - CalculateEffectiveFileList(isDefault); - Penumbra.Log.Verbose($"Created new cache for collection {Name}."); - } - - // Force an update with metadata for this cache. - internal void ForceCacheUpdate() - => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Active.Default); // Handle temporary mods for this collection. public void Apply(TemporaryMod tempMod, bool created) @@ -59,15 +39,6 @@ public partial class ModCollection _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); } - - // Clear the current cache. - internal void ClearCache() - { - _cache?.Dispose(); - _cache = null; - Penumbra.Log.Verbose($"Cleared cache of collection {Name}."); - } - public IEnumerable ReverseResolvePath(FullPath path) => _cache?.ReverseResolvePath(path) ?? Array.Empty(); @@ -99,7 +70,7 @@ public partial class ModCollection => _cache!.ResolvedFiles.Remove(path); // Obtain data from the cache. - internal MetaManager? MetaCache + internal MetaCache? MetaCache => _cache?.MetaManipulations; public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) @@ -123,25 +94,6 @@ public partial class ModCollection internal SingleArray Conflicts(Mod mod) => _cache?.Conflicts(mod) ?? new SingleArray(); - // Update the effective file list for the given cache. - // Creates a cache if necessary. - public void CalculateEffectiveFileList(bool isDefault) - => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => - CalculateEffectiveFileListInternal(isDefault)); - - internal void CalculateEffectiveFileListInternal(bool isDefault) - { - // Skip the empty collection. - if (Index == 0) - return; - - Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); - _cache ??= new ModCollectionCache(this); - _cache.FullRecalculation(isDefault); - - Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); - } - public void SetFiles() { if (_cache == null) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 026516cd..84e47897 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -43,6 +43,12 @@ public partial class ModCollection /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } + /// + /// Count the number of changes of the effective file list. + /// This is used for material and imc changes. + /// + public int ChangeCounter { get; internal set; } + /// /// If a ModSetting is null, it can be inherited from other collections. /// If no collection provides a setting for the mod, it is just disabled. @@ -123,7 +129,6 @@ public partial class ModCollection Debug.Assert(index < 0, "Temporary collection created with non-negative index."); var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List(), new List(), new Dictionary()); - ret.CreateCache(false); return ret; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index dafac640..24f13749 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -15,6 +15,7 @@ using Penumbra.Api.Enums; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; @@ -49,7 +50,6 @@ public class Penumbra : IDalamudPlugin public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; - public static FrameworkManager Framework { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; @@ -81,7 +81,6 @@ public class Penumbra : IDalamudPlugin Config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); Actors = _tmp.Services.GetRequiredService().AwaitedService; Identifier = _tmp.Services.GetRequiredService().AwaitedService; GamePathParser = _tmp.Services.GetRequiredService(); @@ -251,7 +250,7 @@ public class Penumbra : IDalamudPlugin return name + ':'; } - void PrintCollection(ModCollection c) + void PrintCollection(ModCollection c, CollectionCache _) => sb.Append($"**Collection {c.AnonymizedName}**\n" + $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n" + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" @@ -260,7 +259,7 @@ public class Penumbra : IDalamudPlugin sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - TempCollections.Count}\n"); sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n"); sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n"); sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n"); @@ -274,8 +273,8 @@ public class Penumbra : IDalamudPlugin foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments) sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); - foreach (var collection in CollectionManager.Caches.Active) - PrintCollection(collection); + foreach (var (collection, cache) in CollectionManager.Caches.Active) + PrintCollection(collection, cache); return sb.ToString(); } From 9037166d92984267529b8a530fe668a3d8ae2ac3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Apr 2023 20:38:02 +0200 Subject: [PATCH 0879/2451] Add some logging, fix som bugs --- OtterGui | 2 +- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Api/TempModManager.cs | 13 ++++++++++-- Penumbra/Collections/Cache/CollectionCache.cs | 10 ++++++---- .../Manager/TempCollectionManager.cs | 7 +++++++ .../Collections/ModCollection.Cache.Access.cs | 3 +-- Penumbra/Interop/Services/GameEventManager.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 20 +++++++++---------- Penumbra/Mods/ModCache.cs | 20 +++++++++---------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 16 +++++++-------- 12 files changed, 58 insertions(+), 41 deletions(-) diff --git a/OtterGui b/OtterGui index 36c2f5f7..8ebcbf3e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 36c2f5f7e5af017b4ce6737f0ef7add873335cc7 +Subproject commit 8ebcbf3e78ed498be35fa2b9a13d9765d109c428 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 40b135e5..8d1f7a10 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -834,7 +834,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name) + if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name || name.Contains('|')) return PenumbraApiEc.InvalidArgument; return _tempCollections.CreateTemporaryCollection(name).Length > 0 diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 747d49cd..c28a10f7 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -44,7 +44,8 @@ public class TempModManager : IDisposable public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, HashSet manips, int priority) { - var mod = GetOrCreateMod(tag, collection, priority, out var created); + var mod = GetOrCreateMod(tag, collection, priority, out var created); + Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); mod.SetAll(dict, manips); ApplyModChange(mod, collection, created, false); return RedirectResult.Success; @@ -52,10 +53,11 @@ public class TempModManager : IDisposable public RedirectResult Unregister(string tag, ModCollection? collection, int? priority) { + Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}..."); var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; if (list == null) return RedirectResult.NotRegistered; - + var removed = list.RemoveAll(m => { if (m.Name != tag || priority != null && m.Priority != priority.Value) @@ -80,12 +82,19 @@ public class TempModManager : IDisposable if (collection != null) { if (removed) + { + Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); collection.Remove(mod); + } else + { + Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); collection.Apply(mod, created); + } } else { + Penumbra.Log.Verbose($"Triggering global mod change for {(created ? "new " : string.Empty)}temporary Mod {mod.Name}."); _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); } } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index de95cae6..7c58e5bf 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -19,12 +19,13 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// The Cache contains all required temporary data to use a collection. /// It will only be setup if a collection gets activated in any way. /// -public class ModCollectionCache : IDisposable +public class CollectionCache : IDisposable { + private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; public readonly SortedList, object?)> _changedItems = new(); public readonly Dictionary ResolvedFiles = new(); - public readonly MetaCache MetaManipulations; + public readonly MetaCache MetaManipulations; public readonly Dictionary> _conflicts = new(); public IEnumerable> AllConflicts @@ -46,8 +47,9 @@ public class ModCollectionCache : IDisposable } // The cache reacts through events on its collection changing. - public ModCollectionCache(ModCollection collection) + public CollectionCache(CollectionCacheManager manager, ModCollection collection) { + _manager = manager; _collection = collection; MetaManipulations = new MetaCache(_collection); } @@ -205,7 +207,7 @@ public class ModCollectionCache : IDisposable if (addMetaChanges) { ++_collection.ChangeCounter; - if (Penumbra.ModCaches[mod.Index].TotalManipulations > 0) + if ((mod is TemporaryMod temp ? temp.TotalManipulations : Penumbra.ModCaches[mod.Index].TotalManipulations) > 0) AddMetaFiles(); if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 8c64c8e0..29733382 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -53,6 +53,7 @@ public class TempCollectionManager : IDisposable if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); + Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}."); if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) { // Temporary collection created. @@ -66,8 +67,12 @@ public class TempCollectionManager : IDisposable public bool RemoveTemporaryCollection(string collectionName) { if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection)) + { + Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist."); return false; + } + Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}."); GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { @@ -76,6 +81,7 @@ public class TempCollectionManager : IDisposable // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); + Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}."); Collections.Delete(i--); } @@ -88,6 +94,7 @@ public class TempCollectionManager : IDisposable return false; // Temporary collection assignment added. + Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}."); _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); return true; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index f0bb1f29..f37d9163 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -19,7 +18,7 @@ namespace Penumbra.Collections; public partial class ModCollection { // Only active collections need to have a cache. - internal ModCollectionCache? _cache; + internal CollectionCache? _cache; public bool HasCache => _cache != null; diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index ed28f5e7..dcd5585f 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -129,7 +129,7 @@ public unsafe class GameEventManager : IDisposable } } - Penumbra.Log.Verbose($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}."); + Penumbra.Log.Excessive($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}."); return _resourceHandleDestructorHook!.Original(handle); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index a133e9ed..6ef43454 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -15,16 +15,16 @@ namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable, IReadOnlyList { private readonly CommunicatorService _communicator; - private readonly IdentifierService _identifier; - private readonly IReadOnlyList _modManager; + private readonly IdentifierService _identifier; + private readonly ModStorage _modManager; private readonly List _cache = new(); - public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager) + public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModStorage modStorage) { _communicator = communicator; - _identifier = identifier; - _modManager = modManager; + _identifier = identifier; + _modManager = modStorage; _communicator.ModOptionChanged.Subscribe(OnModOptionChange); _communicator.ModPathChanged.Subscribe(OnModPathChange); @@ -232,17 +232,17 @@ public class ModCacheManager : IDisposable, IReadOnlyList private static void UpdateCounts(ModCache cache, Mod mod) { - cache.TotalFileCount = mod.Default.Files.Count; - cache.TotalSwapCount = mod.Default.FileSwaps.Count; + cache.TotalFileCount = mod.Default.Files.Count; + cache.TotalSwapCount = mod.Default.FileSwaps.Count; cache.TotalManipulations = mod.Default.Manipulations.Count; - cache.HasOptions = false; + cache.HasOptions = false; foreach (var group in mod.Groups) { cache.HasOptions |= group.IsOption; foreach (var s in group) { - cache.TotalFileCount += s.Files.Count; - cache.TotalSwapCount += s.FileSwaps.Count; + cache.TotalFileCount += s.Files.Count; + cache.TotalSwapCount += s.FileSwaps.Count; cache.TotalManipulations += s.Manipulations.Count; } } diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs index e6848034..76e01bf6 100644 --- a/Penumbra/Mods/ModCache.cs +++ b/Penumbra/Mods/ModCache.cs @@ -4,23 +4,23 @@ namespace Penumbra.Mods; public class ModCache { - public int TotalFileCount; - public int TotalSwapCount; - public int TotalManipulations; + public int TotalFileCount; + public int TotalSwapCount; + public int TotalManipulations; public bool HasOptions; - public SortedList ChangedItems = new(); - public string LowerChangedItemsString = string.Empty; - public string AllTagsLower = string.Empty; + public readonly SortedList ChangedItems = new(); + public string LowerChangedItemsString = string.Empty; + public string AllTagsLower = string.Empty; public void Reset() { - TotalFileCount = 0; - TotalSwapCount = 0; + TotalFileCount = 0; + TotalSwapCount = 0; TotalManipulations = 0; - HasOptions = false; + HasOptions = false; ChangedItems.Clear(); LowerChangedItemsString = string.Empty; - AllTagsLower = string.Empty; + AllTagsLower = string.Empty; } } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 929a8276..1721480b 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -743,7 +743,7 @@ public class ItemSwapTab : IDisposable, ITab private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) { - if (collectionType != CollectionType.Current || _mod == null || newCollection == null) + if (collectionType is not CollectionType.Current || _mod == null || newCollection == null) return; UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c33003e4..448ddd00 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -395,7 +395,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Sat, 15 Apr 2023 20:40:00 +0200 Subject: [PATCH 0880/2451] meta tmp --- Penumbra/Collections/Cache/CmpCache.cs | 49 +++++ .../Cache/CollectionCacheManager.cs | 44 ++-- Penumbra/Collections/Cache/EqdpCache.cs | 82 +++++++ Penumbra/Collections/Cache/EqpCache.cs | 53 +++++ .../Cache/{MetaCache.Est.cs => EstCache.cs} | 66 +++--- .../Cache/{MetaCache.Gmp.cs => GmpCache.cs} | 39 ++-- .../Cache/{MetaCache.Imc.cs => ImcCache.cs} | 64 ++---- Penumbra/Collections/Cache/MetaCache.Cmp.cs | 61 ------ Penumbra/Collections/Cache/MetaCache.Eqdp.cs | 97 --------- Penumbra/Collections/Cache/MetaCache.Eqp.cs | 62 ------ Penumbra/Collections/Cache/MetaCache.cs | 200 +++++++----------- 11 files changed, 356 insertions(+), 461 deletions(-) create mode 100644 Penumbra/Collections/Cache/CmpCache.cs create mode 100644 Penumbra/Collections/Cache/EqdpCache.cs create mode 100644 Penumbra/Collections/Cache/EqpCache.cs rename Penumbra/Collections/Cache/{MetaCache.Est.cs => EstCache.cs} (60%) rename Penumbra/Collections/Cache/{MetaCache.Gmp.cs => GmpCache.cs} (53%) rename Penumbra/Collections/Cache/{MetaCache.Imc.cs => ImcCache.cs} (61%) delete mode 100644 Penumbra/Collections/Cache/MetaCache.Cmp.cs delete mode 100644 Penumbra/Collections/Cache/MetaCache.Eqdp.cs delete mode 100644 Penumbra/Collections/Cache/MetaCache.Eqp.cs diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs new file mode 100644 index 00000000..bb87aa88 --- /dev/null +++ b/Penumbra/Collections/Cache/CmpCache.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using OtterGui.Filesystem; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public struct CmpCache : IDisposable +{ + private CmpFile? _cmpFile = null; + private readonly List< RspManipulation > _cmpManipulations = new(); + + public CmpCache() + {} + + public void SetFiles(CollectionCacheManager manager) + => manager.SetFile( _cmpFile, MetaIndex.HumanCmp ); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) + => manager.TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp ); + + public bool ApplyMod( CollectionCacheManager manager, RspManipulation manip ) + { + _cmpManipulations.AddOrReplace( manip ); + _cmpFile ??= new CmpFile(); + return manip.Apply( _cmpFile ); + } + + public bool RevertMod( CollectionCacheManager manager, RspManipulation manip ) + { + if (!_cmpManipulations.Remove(manip)) + return false; + + var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute ); + manip = new RspManipulation( manip.SubRace, manip.Attribute, def ); + return manip.Apply( _cmpFile! ); + + } + + public void Dispose() + { + _cmpFile?.Dispose(); + _cmpFile = null; + _cmpManipulations.Clear(); + } +} \ No newline at end of file diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index aa24d208..326b5918 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -17,15 +17,17 @@ namespace Penumbra.Collections.Cache; public class CollectionCacheManager : IDisposable { - private readonly FrameworkManager _framework; - private readonly ActiveCollections _active; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _characterUtility; - private readonly TempModManager _tempMods; - private readonly ModStorage _modStorage; - private readonly ModCacheManager _modCaches; - private readonly Configuration _config; - private readonly ResidentResourceManager _resources; + private readonly FrameworkManager _framework; + private readonly ActiveCollections _active; + private readonly CommunicatorService _communicator; + private readonly TempModManager _tempMods; + private readonly ModStorage _modStorage; + private readonly ModCacheManager _modCaches; + private readonly Configuration _config; + + internal readonly ValidityChecker ValidityChecker; + internal readonly CharacterUtility CharacterUtility; + internal readonly ResidentResourceManager ResidentResources; private readonly Dictionary _caches = new(); @@ -37,17 +39,18 @@ public class CollectionCacheManager : IDisposable public CollectionCacheManager(FrameworkManager framework, ActiveCollections active, CommunicatorService communicator, CharacterUtility characterUtility, TempModManager tempMods, ModStorage modStorage, Configuration config, - ResidentResourceManager resources, ModCacheManager modCaches) + ResidentResourceManager residentResources, ModCacheManager modCaches, ValidityChecker validityChecker) { _framework = framework; _active = active; _communicator = communicator; - _characterUtility = characterUtility; + CharacterUtility = characterUtility; _tempMods = tempMods; _modStorage = modStorage; _config = config; - _resources = resources; + ResidentResources = residentResources; _modCaches = modCaches; + ValidityChecker = validityChecker; _communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); @@ -58,8 +61,8 @@ public class CollectionCacheManager : IDisposable _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); CreateNecessaryCaches(); - if (!_characterUtility.Ready) - _characterUtility.LoadingFinished += IncrementCounters; + if (!CharacterUtility.Ready) + CharacterUtility.LoadingFinished += IncrementCounters; } public void Dispose() @@ -71,7 +74,7 @@ public class CollectionCacheManager : IDisposable _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); - _characterUtility.LoadingFinished -= IncrementCounters; + CharacterUtility.LoadingFinished -= IncrementCounters; } /// Only creates a new cache, does not update an existing one. @@ -80,7 +83,7 @@ public class CollectionCacheManager : IDisposable if (_caches.ContainsKey(collection) || collection.Index == ModCollection.Empty.Index) return false; - var cache = new CollectionCache(collection); + var cache = new CollectionCache(this, collection); _caches.Add(collection, cache); collection._cache = cache; Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); @@ -95,6 +98,9 @@ public class CollectionCacheManager : IDisposable => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, () => CalculateEffectiveFileListInternal(collection)); + public bool IsDefault(ModCollection collection) + => _active.Default == collection; + private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. @@ -133,10 +139,10 @@ public class CollectionCacheManager : IDisposable ++collection.ChangeCounter; - if (_active.Default != collection || !_characterUtility.Ready || !_config.EnableMods) + if (_active.Default != collection || !CharacterUtility.Ready || !_config.EnableMods) return; - _resources.Reload(); + ResidentResources.Reload(); cache.MetaManipulations.SetFiles(); } @@ -232,7 +238,7 @@ public class CollectionCacheManager : IDisposable { foreach (var (collection, _) in _caches) ++collection.ChangeCounter; - _characterUtility.LoadingFinished -= IncrementCounters; + CharacterUtility.LoadingFinished -= IncrementCounters; } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs new file mode 100644 index 00000000..aa882ac4 --- /dev/null +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public readonly struct EqdpCache : IDisposable +{ + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar + private readonly List _eqdpManipulations = new(); + + public EqdpCache() + { } + + public void SetFiles(CollectionCacheManager manager) + { + for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) + manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); + } + + public CharacterUtility.MetaList.MetaReverter? TemporarilySetEqdpFile(CollectionCacheManager manager, GenderRace genderRace, bool accessory) + { + var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); + if ((int)idx == -1) + return null; + + var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); + return i != -1 ? manager.TemporarilySetFile(_eqdpFiles[i], idx) : null; + } + + public void Reset(CollectionCacheManager manager) + { + foreach (var file in _eqdpFiles.OfType()) + { + var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; + file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (int)m.SetId)); + } + + _eqdpManipulations.Clear(); + } + + public bool ApplyMod(CollectionCacheManager manager, EqdpManipulation manip) + { + _eqdpManipulations.AddOrReplace(manip); + var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= + new ExpandedEqdpFile(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar + return manip.Apply(file); + } + + public bool RevertMod(CollectionCacheManager manager, EqdpManipulation manip) + { + if (!_eqdpManipulations.Remove(manip)) + return false; + + var def = ExpandedEqdpFile.GetDefault(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); + var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; + manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId); + return manip.Apply(file); + } + + public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) + => _eqdpFiles + [Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar + + public void Dispose() + { + for (var i = 0; i < _eqdpFiles.Length; ++i) + { + _eqdpFiles[i]?.Dispose(); + _eqdpFiles[i] = null; + } + + _eqdpManipulations.Clear(); + } +} diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs new file mode 100644 index 00000000..8cf19386 --- /dev/null +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using OtterGui.Filesystem; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public struct EqpCache : IDisposable +{ + private ExpandedEqpFile? _eqpFile = null; + private readonly List< EqpManipulation > _eqpManipulations = new(); + + public EqpCache() + {} + + public void SetFiles(CollectionCacheManager manager) + => manager.SetFile( _eqpFile, MetaIndex.Eqp ); + + public static void ResetFiles(CollectionCacheManager manager) + => manager.SetFile( null, MetaIndex.Eqp ); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) + => manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); + + public bool ApplyMod( CollectionCacheManager manager, EqpManipulation manip ) + { + _eqpManipulations.AddOrReplace( manip ); + _eqpFile ??= new ExpandedEqpFile(); + return manip.Apply( _eqpFile ); + } + + public bool RevertMod( CollectionCacheManager manager, EqpManipulation manip ) + { + var idx = _eqpManipulations.FindIndex( manip.Equals ); + if (idx < 0) + return false; + + var def = ExpandedEqpFile.GetDefault( manip.SetId ); + manip = new EqpManipulation( def, manip.Slot, manip.SetId ); + return manip.Apply( _eqpFile! ); + + } + + public void Dispose() + { + _eqpFile?.Dispose(); + _eqpFile = null; + _eqpManipulations.Clear(); + } +} \ No newline at end of file diff --git a/Penumbra/Collections/Cache/MetaCache.Est.cs b/Penumbra/Collections/Cache/EstCache.cs similarity index 60% rename from Penumbra/Collections/Cache/MetaCache.Est.cs rename to Penumbra/Collections/Cache/EstCache.cs index 6815be30..76470d87 100644 --- a/Penumbra/Collections/Cache/MetaCache.Est.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -9,32 +9,27 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public partial class MetaCache +public struct EstCache : IDisposable { private EstFile? _estFaceFile = null; private EstFile? _estHairFile = null; private EstFile? _estBodyFile = null; private EstFile? _estHeadFile = null; - private readonly List< EstManipulation > _estManipulations = new(); + private readonly List< EstManipulation > _estManipulations = new(); + + public EstCache() + {} - public void SetEstFiles() + public void SetFiles(CollectionCacheManager manager) { - SetFile( _estFaceFile, MetaIndex.FaceEst ); - SetFile( _estHairFile, MetaIndex.HairEst ); - SetFile( _estBodyFile, MetaIndex.BodyEst ); - SetFile( _estHeadFile, MetaIndex.HeadEst ); + manager.SetFile( _estFaceFile, MetaIndex.FaceEst ); + manager.SetFile( _estHairFile, MetaIndex.HairEst ); + manager.SetFile( _estBodyFile, MetaIndex.BodyEst ); + manager.SetFile( _estHeadFile, MetaIndex.HeadEst ); } - public static void ResetEstFiles() - { - SetFile( null, MetaIndex.FaceEst ); - SetFile( null, MetaIndex.HairEst ); - SetFile( null, MetaIndex.BodyEst ); - SetFile( null, MetaIndex.HeadEst ); - } - - public CharacterUtility.MetaList.MetaReverter? TemporarilySetEstFile(EstManipulation.EstType type) + public CharacterUtility.MetaList.MetaReverter? TemporarilySetFiles(CollectionCacheManager manager, EstManipulation.EstType type) { var (file, idx) = type switch { @@ -45,10 +40,10 @@ public partial class MetaCache _ => ( null, 0 ), }; - return idx != 0 ? TemporarilySetFile( file, idx ) : null; + return idx != 0 ? manager.TemporarilySetFile( file, idx ) : null; } - public void ResetEst() + public void Reset() { _estFaceFile?.Reset(); _estHairFile?.Reset(); @@ -57,7 +52,7 @@ public partial class MetaCache _estManipulations.Clear(); } - public bool ApplyMod( EstManipulation m ) + public bool ApplyMod( CollectionCacheManager manager, EstManipulation m ) { _estManipulations.AddOrReplace( m ); var file = m.Slot switch @@ -71,27 +66,26 @@ public partial class MetaCache return m.Apply( file ); } - public bool RevertMod( EstManipulation m ) + public bool RevertMod( CollectionCacheManager manager, EstManipulation m ) { - if( _estManipulations.Remove( m ) ) - { - var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); - var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); - var file = m.Slot switch - { - EstManipulation.EstType.Hair => _estHairFile!, - EstManipulation.EstType.Face => _estFaceFile!, - EstManipulation.EstType.Body => _estBodyFile!, - EstManipulation.EstType.Head => _estHeadFile!, - _ => throw new ArgumentOutOfRangeException(), - }; - return manip.Apply( file ); - } + if (!_estManipulations.Remove(m)) + return false; + + var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); + var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); + var file = m.Slot switch + { + EstManipulation.EstType.Hair => _estHairFile!, + EstManipulation.EstType.Face => _estFaceFile!, + EstManipulation.EstType.Body => _estBodyFile!, + EstManipulation.EstType.Head => _estHeadFile!, + _ => throw new ArgumentOutOfRangeException(), + }; + return manip.Apply( file ); - return false; } - public void DisposeEst() + public void Dispose() { _estFaceFile?.Dispose(); _estHairFile?.Dispose(); diff --git a/Penumbra/Collections/Cache/MetaCache.Gmp.cs b/Penumbra/Collections/Cache/GmpCache.cs similarity index 53% rename from Penumbra/Collections/Cache/MetaCache.Gmp.cs rename to Penumbra/Collections/Cache/GmpCache.cs index f83efe77..436dfdbf 100644 --- a/Penumbra/Collections/Cache/MetaCache.Gmp.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using OtterGui.Filesystem; @@ -8,51 +9,47 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public partial class MetaCache +public struct GmpCache : IDisposable { private ExpandedGmpFile? _gmpFile = null; private readonly List< GmpManipulation > _gmpManipulations = new(); + + public GmpCache() + {} - public void SetGmpFiles() - => SetFile( _gmpFile, MetaIndex.Gmp ); + public void SetFiles(CollectionCacheManager manager) + => manager.SetFile( _gmpFile, MetaIndex.Gmp ); - public static void ResetGmpFiles() - => SetFile( null, MetaIndex.Gmp ); + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) + => manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); - public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() - => TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); - - public void ResetGmp() + public void ResetGmp(CollectionCacheManager manager) { if( _gmpFile == null ) - { return; - } _gmpFile.Reset( _gmpManipulations.Select( m => ( int )m.SetId ) ); _gmpManipulations.Clear(); } - public bool ApplyMod( GmpManipulation manip ) + public bool ApplyMod( CollectionCacheManager manager, GmpManipulation manip ) { _gmpManipulations.AddOrReplace( manip ); _gmpFile ??= new ExpandedGmpFile(); return manip.Apply( _gmpFile ); } - public bool RevertMod( GmpManipulation manip ) + public bool RevertMod( CollectionCacheManager manager, GmpManipulation manip ) { - if( _gmpManipulations.Remove( manip ) ) - { - var def = ExpandedGmpFile.GetDefault( manip.SetId ); - manip = new GmpManipulation( def, manip.SetId ); - return manip.Apply( _gmpFile! ); - } + if (!_gmpManipulations.Remove(manip)) + return false; - return false; + var def = ExpandedGmpFile.GetDefault( manip.SetId ); + manip = new GmpManipulation( def, manip.SetId ); + return manip.Apply( _gmpFile! ); } - public void DisposeGmp() + public void Dispose() { _gmpFile?.Dispose(); _gmpFile = null; diff --git a/Penumbra/Collections/Cache/MetaCache.Imc.cs b/Penumbra/Collections/Cache/ImcCache.cs similarity index 61% rename from Penumbra/Collections/Cache/MetaCache.Imc.cs rename to Penumbra/Collections/Cache/ImcCache.cs index 89e55b1b..43dc73e5 100644 --- a/Penumbra/Collections/Cache/MetaCache.Imc.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -8,46 +8,32 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public partial class MetaCache +public readonly struct ImcCache : IDisposable { private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); private readonly List< ImcManipulation > _imcManipulations = new(); - public void SetImcFiles() - { - if( !_collection.HasCache ) - { - return; - } + public ImcCache() + { } + public void SetFiles(CollectionCacheManager manager, ModCollection collection) + { foreach( var path in _imcFiles.Keys ) - { - _collection.ForceFile( path, CreateImcPath( path ) ); - } + collection._cache!.ForceFile( path, CreateImcPath( collection, path ) ); } - public void ResetImc() + public void Reset(CollectionCacheManager manager, ModCollection collection) { - if( _collection.HasCache ) + foreach( var (path, file) in _imcFiles ) { - foreach( var (path, file) in _imcFiles ) - { - _collection.RemoveFile( path ); - file.Reset(); - } - } - else - { - foreach( var (_, file) in _imcFiles ) - { - file.Reset(); - } + collection._cache!.RemoveFile( path ); + file.Reset(); } _imcManipulations.Clear(); } - public bool ApplyMod( ImcManipulation manip ) + public bool ApplyMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation manip ) { if( !manip.Valid ) { @@ -69,17 +55,14 @@ public partial class MetaCache } _imcFiles[ path ] = file; - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } + var fullPath = CreateImcPath( collection, path ); + collection._cache!.ForceFile( path, fullPath ); return true; } catch( ImcException e ) { - Penumbra.ValidityChecker.ImcExceptions.Add( e ); + manager.ValidityChecker.ImcExceptions.Add( e ); Penumbra.Log.Error( e.ToString() ); } catch( Exception e ) @@ -90,7 +73,7 @@ public partial class MetaCache return false; } - public bool RevertMod( ImcManipulation m ) + public bool RevertMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation m ) { if( !m.Valid || !_imcManipulations.Remove( m ) ) { @@ -106,32 +89,25 @@ public partial class MetaCache var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); var manip = m.Copy( def ); if( !manip.Apply( file ) ) - { return false; - } - var fullPath = CreateImcPath( path ); - if( _collection.HasCache ) - { - _collection.ForceFile( path, fullPath ); - } + var fullPath = CreateImcPath( collection, path ); + collection._cache!.ForceFile( path, fullPath ); return true; } - public void DisposeImc() + public void Dispose() { foreach( var file in _imcFiles.Values ) - { file.Dispose(); - } _imcFiles.Clear(); _imcManipulations.Clear(); } - private FullPath CreateImcPath( Utf8GamePath path ) - => new($"|{_collection.Name}_{_collection.ChangeCounter}|{path}"); + private static FullPath CreateImcPath( ModCollection collection, Utf8GamePath path ) + => new($"|{collection.Name}_{collection.ChangeCounter}|{path}"); public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); diff --git a/Penumbra/Collections/Cache/MetaCache.Cmp.cs b/Penumbra/Collections/Cache/MetaCache.Cmp.cs deleted file mode 100644 index 35d97eee..00000000 --- a/Penumbra/Collections/Cache/MetaCache.Cmp.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OtterGui.Filesystem; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections.Cache; - -public partial class MetaCache -{ - private CmpFile? _cmpFile = null; - private readonly List< RspManipulation > _cmpManipulations = new(); - - public void SetCmpFiles() - => SetFile( _cmpFile, MetaIndex.HumanCmp ); - - public static void ResetCmpFiles() - => SetFile( null, MetaIndex.HumanCmp ); - - public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() - => TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp ); - - public void ResetCmp() - { - if( _cmpFile == null ) - { - return; - } - - _cmpFile.Reset( _cmpManipulations.Select( m => ( m.SubRace, m.Attribute ) ) ); - _cmpManipulations.Clear(); - } - - public bool ApplyMod( RspManipulation manip ) - { - _cmpManipulations.AddOrReplace( manip ); - _cmpFile ??= new CmpFile(); - return manip.Apply( _cmpFile ); - } - - public bool RevertMod( RspManipulation manip ) - { - if( _cmpManipulations.Remove( manip ) ) - { - var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute ); - manip = new RspManipulation( manip.SubRace, manip.Attribute, def ); - return manip.Apply( _cmpFile! ); - } - - return false; - } - - public void DisposeCmp() - { - _cmpFile?.Dispose(); - _cmpFile = null; - _cmpManipulations.Clear(); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/Cache/MetaCache.Eqdp.cs b/Penumbra/Collections/Cache/MetaCache.Eqdp.cs deleted file mode 100644 index 5bac9169..00000000 --- a/Penumbra/Collections/Cache/MetaCache.Eqdp.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections.Cache; - -public partial class MetaCache -{ - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - - private readonly List< EqdpManipulation > _eqdpManipulations = new(); - - public void SetEqdpFiles() - { - for( var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i ) - { - SetFile( _eqdpFiles[ i ], CharacterUtilityData.EqdpIndices[ i ] ); - } - } - - public CharacterUtility.MetaList.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) - { - var idx = CharacterUtilityData.EqdpIdx( genderRace, accessory ); - if( ( int )idx != -1 ) - { - var i = CharacterUtilityData.EqdpIndices.IndexOf( idx ); - if( i != -1 ) - { - return TemporarilySetFile( _eqdpFiles[ i ], idx ); - } - } - - return null; - } - - public static void ResetEqdpFiles() - { - foreach( var idx in CharacterUtilityData.EqdpIndices ) - { - SetFile( null, idx ); - } - } - - public void ResetEqdp() - { - foreach( var file in _eqdpFiles.OfType< ExpandedEqdpFile >() ) - { - var relevant = CharacterUtility.RelevantIndices[ file.Index.Value ]; - file.Reset( _eqdpManipulations.Where( m => m.FileIndex() == relevant ).Select( m => ( int )m.SetId ) ); - } - - _eqdpManipulations.Clear(); - } - - public bool ApplyMod( EqdpManipulation manip ) - { - _eqdpManipulations.AddOrReplace( manip ); - var file = _eqdpFiles[ Array.IndexOf( CharacterUtilityData.EqdpIndices, manip.FileIndex() ) ] ??= - new ExpandedEqdpFile( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory() ); // TODO: female Hrothgar - return manip.Apply( file ); - } - - public bool RevertMod( EqdpManipulation manip ) - { - if( _eqdpManipulations.Remove( manip ) ) - { - var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( manip.Gender, manip.Race ), manip.Slot.IsAccessory(), manip.SetId ); - var file = _eqdpFiles[ Array.IndexOf( CharacterUtilityData.EqdpIndices, manip.FileIndex() ) ]!; - manip = new EqdpManipulation( def, manip.Slot, manip.Gender, manip.Race, manip.SetId ); - return manip.Apply( file ); - } - - return false; - } - - public ExpandedEqdpFile? EqdpFile( GenderRace race, bool accessory ) - => _eqdpFiles - [ Array.IndexOf( CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar - - public void DisposeEqdp() - { - for( var i = 0; i < _eqdpFiles.Length; ++i ) - { - _eqdpFiles[ i ]?.Dispose(); - _eqdpFiles[ i ] = null; - } - - _eqdpManipulations.Clear(); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/Cache/MetaCache.Eqp.cs b/Penumbra/Collections/Cache/MetaCache.Eqp.cs deleted file mode 100644 index ce0829a0..00000000 --- a/Penumbra/Collections/Cache/MetaCache.Eqp.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OtterGui.Filesystem; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections.Cache; - -public partial class MetaCache -{ - private ExpandedEqpFile? _eqpFile = null; - private readonly List< EqpManipulation > _eqpManipulations = new(); - - public void SetEqpFiles() - => SetFile( _eqpFile, MetaIndex.Eqp ); - - public static void ResetEqpFiles() - => SetFile( null, MetaIndex.Eqp ); - - public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() - => TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); - - public void ResetEqp() - { - if( _eqpFile == null ) - { - return; - } - - _eqpFile.Reset( _eqpManipulations.Select( m => ( int )m.SetId ) ); - _eqpManipulations.Clear(); - } - - public bool ApplyMod( EqpManipulation manip ) - { - _eqpManipulations.AddOrReplace( manip ); - _eqpFile ??= new ExpandedEqpFile(); - return manip.Apply( _eqpFile ); - } - - public bool RevertMod( EqpManipulation manip ) - { - var idx = _eqpManipulations.FindIndex( manip.Equals ); - if( idx >= 0 ) - { - var def = ExpandedEqpFile.GetDefault( manip.SetId ); - manip = new EqpManipulation( def, manip.Slot, manip.SetId ); - return manip.Apply( _eqpFile! ); - } - - return false; - } - - public void DisposeEqp() - { - _eqpFile?.Dispose(); - _eqpFile = null; - _eqpManipulations.Clear(); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 0b6fa942..bee01714 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -12,115 +12,115 @@ using Penumbra.Mods; namespace Penumbra.Collections.Cache; -public partial class MetaCache : IDisposable, IEnumerable< KeyValuePair< MetaManipulation, IMod > > +public struct MetaCache : IDisposable, IEnumerable> { - private readonly Dictionary< MetaManipulation, IMod > _manipulations = new(); - private readonly ModCollection _collection; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + private readonly Dictionary _manipulations = new(); + private EqpCache _eqpCache = new(); + private readonly EqdpCache _eqdpCache = new(); + private EstCache _estCache = new(); + private GmpCache _gmpCache = new(); + private CmpCache _cmpCache = new(); + private readonly ImcCache _imcCache; - public bool TryGetValue( MetaManipulation manip, [NotNullWhen( true )] out IMod? mod ) - => _manipulations.TryGetValue( manip, out mod ); + public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) + => _manipulations.TryGetValue(manip, out mod); public int Count => _manipulations.Count; - public IReadOnlyCollection< MetaManipulation > Manipulations + public IReadOnlyCollection Manipulations => _manipulations.Keys; - public IEnumerator< KeyValuePair< MetaManipulation, IMod > > GetEnumerator() + public IEnumerator> GetEnumerator() => _manipulations.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public MetaCache( ModCollection collection ) + public MetaCache(CollectionCacheManager manager, ModCollection collection) { - _collection = collection; - if( !Penumbra.CharacterUtility.Ready ) - { - Penumbra.CharacterUtility.LoadingFinished += ApplyStoredManipulations; - } + _manager = manager; + _imcCache = new ImcCache(collection); + if (!_manager.CharacterUtility.Ready) + _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations; } public void SetFiles() { - SetEqpFiles(); - SetEqdpFiles(); - SetGmpFiles(); - SetEstFiles(); - SetCmpFiles(); - SetImcFiles(); + _eqpCache.SetFiles(_manager); + _eqdpCache.SetFiles(_manager); + _estCache.SetFiles(_manager); + _gmpCache.SetFiles(_manager); + _cmpCache.SetFiles(_manager); + _imcCache.SetFiles(_manager, _collection); } public void Reset() { - ResetEqp(); - ResetEqdp(); - ResetGmp(); - ResetEst(); - ResetCmp(); - ResetImc(); + _eqpCache.Reset(_manager); + _eqdpCache.Reset(_manager); + _estCache.Reset(_manager); + _gmpCache.Reset(_manager); + _cmpCache.Reset(_manager); + _imcCache.Reset(_manager, _collection); _manipulations.Clear(); } public void Dispose() { + _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; + _eqpCache.Dispose(); + _eqdpCache.Dispose(); + _estCache.Dispose(); + _gmpCache.Dispose(); + _cmpCache.Dispose(); + _imcCache.Dispose(); _manipulations.Clear(); - Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - DisposeEqp(); - DisposeEqdp(); - DisposeCmp(); - DisposeGmp(); - DisposeEst(); - DisposeImc(); } - public bool ApplyMod( MetaManipulation manip, IMod mod ) + public bool ApplyMod(MetaManipulation manip, IMod mod) { - if( _manipulations.ContainsKey( manip ) ) - { - _manipulations.Remove( manip ); - } + if (_manipulations.ContainsKey(manip)) + _manipulations.Remove(manip); - _manipulations[ manip ] = mod; + _manipulations[manip] = mod; - if( !Penumbra.CharacterUtility.Ready ) - { + if (!_manager.CharacterUtility.Ready) return true; - } // Imc manipulations do not require character utility, // but they do require the file space to be ready. return manip.ManipulationType switch { - MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), - MetaManipulation.Type.Gmp => ApplyMod( manip.Gmp ), - MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), - MetaManipulation.Type.Est => ApplyMod( manip.Est ), - MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), - MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), + MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), MetaManipulation.Type.Unknown => false, _ => false, }; } - public bool RevertMod( MetaManipulation manip ) + public bool RevertMod(MetaManipulation manip) { - var ret = _manipulations.Remove( manip ); - if( !Penumbra.CharacterUtility.Ready ) - { + var ret = _manipulations.Remove(manip); + if (!Penumbra.CharacterUtility.Ready) return ret; - } // Imc manipulations do not require character utility, // but they do require the file space to be ready. return manip.ManipulationType switch { - MetaManipulation.Type.Eqp => RevertMod( (MetaManipulation)manip.Eqp ), - MetaManipulation.Type.Gmp => RevertMod( (MetaManipulation)manip.Gmp ), - MetaManipulation.Type.Eqdp => RevertMod( (MetaManipulation)manip.Eqdp ), - MetaManipulation.Type.Est => RevertMod( (MetaManipulation)manip.Est ), - MetaManipulation.Type.Rsp => RevertMod( (MetaManipulation)manip.Rsp ), - MetaManipulation.Type.Imc => RevertMod( (MetaManipulation)manip.Imc ), + MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -129,22 +129,20 @@ public partial class MetaCache : IDisposable, IEnumerable< KeyValuePair< MetaMan // Use this when CharacterUtility becomes ready. private void ApplyStoredManipulations() { - if( !Penumbra.CharacterUtility.Ready ) - { + if (!Penumbra.CharacterUtility.Ready) return; - } var loaded = 0; - foreach( var manip in Manipulations ) + foreach (var manip in Manipulations) { loaded += manip.ManipulationType switch { - MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), - MetaManipulation.Type.Gmp => ApplyMod( manip.Gmp ), - MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), - MetaManipulation.Type.Est => ApplyMod( manip.Est ), - MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), - MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), + MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), MetaManipulation.Type.Unknown => false, _ => false, } @@ -152,68 +150,28 @@ public partial class MetaCache : IDisposable, IEnumerable< KeyValuePair< MetaMan : 0; } - if( Penumbra.CollectionManager.Active.Default == _collection ) + if (_manager.IsDefault(_collection)) { SetFiles(); - Penumbra.ResidentResources.Reload(); + _manager.ResidentResources.Reload(); } - Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - Penumbra.Log.Debug( $"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations." ); + _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; + Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations."); } - public void SetFile( MetaIndex metaIndex ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public unsafe void SetFile(MetaBaseFile? file, MetaIndex metaIndex) { - switch( metaIndex ) - { - case MetaIndex.Eqp: - SetFile( _eqpFile, metaIndex ); - break; - case MetaIndex.Gmp: - SetFile( _gmpFile, metaIndex ); - break; - case MetaIndex.HumanCmp: - SetFile( _cmpFile, metaIndex ); - break; - case MetaIndex.FaceEst: - SetFile( _estFaceFile, metaIndex ); - break; - case MetaIndex.HairEst: - SetFile( _estHairFile, metaIndex ); - break; - case MetaIndex.HeadEst: - SetFile( _estHeadFile, metaIndex ); - break; - case MetaIndex.BodyEst: - SetFile( _estBodyFile, metaIndex ); - break; - default: - var i = CharacterUtilityData.EqdpIndices.IndexOf( metaIndex ); - if( i != -1 ) - { - SetFile( _eqdpFiles[ i ], metaIndex ); - } - - break; - } - } - - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe void SetFile( MetaBaseFile? file, MetaIndex metaIndex ) - { - if( file == null ) - { - Penumbra.CharacterUtility.ResetResource( metaIndex ); - } + if (file == null) + _manager.CharacterUtility.ResetResource(metaIndex); else - { - Penumbra.CharacterUtility.SetResource( metaIndex, ( IntPtr )file.Data, file.Length ); - } + _manager.CharacterUtility.SetResource(metaIndex, (IntPtr)file.Data, file.Length); } - [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private static unsafe CharacterUtility.MetaList.MetaReverter TemporarilySetFile( MetaBaseFile? file, MetaIndex metaIndex ) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public unsafe CharacterUtility.MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) => file == null - ? Penumbra.CharacterUtility.TemporarilyResetResource( metaIndex ) - : Penumbra.CharacterUtility.TemporarilySetResource( metaIndex, ( IntPtr )file.Data, file.Length ); + ? _manager.CharacterUtility.TemporarilyResetResource(metaIndex) + : _manager.CharacterUtility.TemporarilySetResource(metaIndex, (IntPtr)file.Data, file.Length); } \ No newline at end of file From 1d82e882ed1d48807eeaaeb0949744faecc23927 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Apr 2023 13:18:43 +0200 Subject: [PATCH 0881/2451] Meta stuff is terrible. --- Penumbra/Collections/Cache/CmpCache.cs | 46 +- Penumbra/Collections/Cache/CollectionCache.cs | 29 +- .../Cache/CollectionCacheManager.cs | 48 +- Penumbra/Collections/Cache/EqdpCache.cs | 34 +- Penumbra/Collections/Cache/EqpCache.cs | 25 +- Penumbra/Collections/Cache/EstCache.cs | 79 ++-- Penumbra/Collections/Cache/GmpCache.cs | 15 +- Penumbra/Collections/Cache/ImcCache.cs | 13 +- Penumbra/Collections/Cache/MetaCache.cs | 115 +++-- .../Collections/ModCollection.Cache.Access.cs | 26 +- .../Import/TexToolsMeta.Deserialization.cs | 136 +++--- Penumbra/Import/TexToolsMeta.Export.cs | 19 +- Penumbra/Import/TexToolsMeta.Rgsp.cs | 7 +- Penumbra/Import/TexToolsMeta.cs | 42 +- Penumbra/Interop/Services/MetaFileManager.cs | 37 -- Penumbra/Meta/Files/CmpFile.cs | 42 +- Penumbra/Meta/Files/EqdpFile.cs | 125 +++-- Penumbra/Meta/Files/EqpGmpFile.cs | 141 +++--- Penumbra/Meta/Files/EstFile.cs | 186 ++++---- Penumbra/Meta/Files/EvpFile.cs | 55 +-- Penumbra/Meta/Files/ImcFile.cs | 157 +++---- Penumbra/Meta/Files/MetaBaseFile.cs | 69 ++- Penumbra/Meta/MetaFileManager.cs | 85 ++++ Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 23 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 433 +++++++++--------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 45 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 15 +- Penumbra/Mods/ItemSwap/Swaps.cs | 82 ++-- Penumbra/Mods/Mod.Files.cs | 7 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 202 ++++---- Penumbra/Penumbra.cs | 3 +- Penumbra/PenumbraNew.cs | 9 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 38 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 105 ++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 19 +- 35 files changed, 1265 insertions(+), 1247 deletions(-) delete mode 100644 Penumbra/Interop/Services/MetaFileManager.cs create mode 100644 Penumbra/Meta/MetaFileManager.cs diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs index bb87aa88..47d6a441 100644 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ b/Penumbra/Collections/Cache/CmpCache.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -10,34 +12,42 @@ namespace Penumbra.Collections.Cache; public struct CmpCache : IDisposable { - private CmpFile? _cmpFile = null; - private readonly List< RspManipulation > _cmpManipulations = new(); - + private CmpFile? _cmpFile = null; + private readonly List _cmpManipulations = new(); + public CmpCache() - {} + { } - public void SetFiles(CollectionCacheManager manager) - => manager.SetFile( _cmpFile, MetaIndex.HumanCmp ); + public void SetFiles(MetaFileManager manager) + => manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) - => manager.TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp ); + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) + => manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); - public bool ApplyMod( CollectionCacheManager manager, RspManipulation manip ) + public void Reset() { - _cmpManipulations.AddOrReplace( manip ); - _cmpFile ??= new CmpFile(); - return manip.Apply( _cmpFile ); + if (_cmpFile == null) + return; + + _cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute))); + _cmpManipulations.Clear(); } - public bool RevertMod( CollectionCacheManager manager, RspManipulation manip ) + public bool ApplyMod(MetaFileManager manager, RspManipulation manip) + { + _cmpManipulations.AddOrReplace(manip); + _cmpFile ??= new CmpFile(manager); + return manip.Apply(_cmpFile); + } + + public bool RevertMod(MetaFileManager manager, RspManipulation manip) { if (!_cmpManipulations.Remove(manip)) return false; - var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute ); - manip = new RspManipulation( manip.SubRace, manip.Attribute, def ); - return manip.Apply( _cmpFile! ); - + var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute); + manip = new RspManipulation(manip.SubRace, manip.Attribute, def); + return manip.Apply(_cmpFile!); } public void Dispose() @@ -46,4 +56,4 @@ public struct CmpCache : IDisposable _cmpFile = null; _cmpManipulations.Clear(); } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 7c58e5bf..d630ad31 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using Penumbra.Api.Enums; using Penumbra.String.Classes; using Penumbra.Mods.Manager; @@ -51,7 +52,7 @@ public class CollectionCache : IDisposable { _manager = manager; _collection = collection; - MetaManipulations = new MetaCache(_collection); + MetaManipulations = new MetaCache(manager.MetaFileManager, _collection); } public void Dispose() @@ -59,6 +60,9 @@ public class CollectionCache : IDisposable MetaManipulations.Dispose(); } + ~CollectionCache() + => MetaManipulations.Dispose(); + // Resolve a given game path according to this collection. public FullPath? ResolvePath(Utf8GamePath gameResourcePath) { @@ -115,6 +119,17 @@ public class CollectionCache : IDisposable return ret; } + /// Force a file to be resolved to a specific path regardless of conflicts. + internal void ForceFile(Utf8GamePath path, FullPath fullPath) + { + if (CheckFullPath(path, fullPath)) + ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath); + } + + /// Force a file resolve to be removed. + internal void RemoveFile(Utf8GamePath path) + => ResolvedFiles.Remove(path); + public void ReloadMod(IMod mod, bool addMetaChanges) { RemoveMod(mod, addMetaChanges); @@ -234,7 +249,7 @@ public class CollectionCache : IDisposable // Inside the same mod, conflicts are not recorded. private void AddFile(Utf8GamePath path, FullPath file, IMod mod) { - if (!ModCollection.CheckFullPath(path, file)) + if (!CheckFullPath(path, file)) return; if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) @@ -387,4 +402,14 @@ public class CollectionCache : IDisposable Penumbra.Log.Error($"Unknown Error:\n{e}"); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) + { + if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) + return true; + + Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}"); + return false; + } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 326b5918..467d0617 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,7 +8,7 @@ using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; -using Penumbra.Interop.Services; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -18,16 +18,12 @@ namespace Penumbra.Collections.Cache; public class CollectionCacheManager : IDisposable { private readonly FrameworkManager _framework; - private readonly ActiveCollections _active; private readonly CommunicatorService _communicator; private readonly TempModManager _tempMods; private readonly ModStorage _modStorage; - private readonly ModCacheManager _modCaches; - private readonly Configuration _config; + private readonly ActiveCollections _active; - internal readonly ValidityChecker ValidityChecker; - internal readonly CharacterUtility CharacterUtility; - internal readonly ResidentResourceManager ResidentResources; + internal readonly MetaFileManager MetaFileManager; private readonly Dictionary _caches = new(); @@ -37,20 +33,15 @@ public class CollectionCacheManager : IDisposable public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active => _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value)); - public CollectionCacheManager(FrameworkManager framework, ActiveCollections active, CommunicatorService communicator, - CharacterUtility characterUtility, TempModManager tempMods, ModStorage modStorage, Configuration config, - ResidentResourceManager residentResources, ModCacheManager modCaches, ValidityChecker validityChecker) + public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, + TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active) { - _framework = framework; - _active = active; - _communicator = communicator; - CharacterUtility = characterUtility; - _tempMods = tempMods; - _modStorage = modStorage; - _config = config; - ResidentResources = residentResources; - _modCaches = modCaches; - ValidityChecker = validityChecker; + _framework = framework; + _communicator = communicator; + _tempMods = tempMods; + _modStorage = modStorage; + MetaFileManager = metaFileManager; + _active = active; _communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); @@ -61,8 +52,8 @@ public class CollectionCacheManager : IDisposable _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); CreateNecessaryCaches(); - if (!CharacterUtility.Ready) - CharacterUtility.LoadingFinished += IncrementCounters; + if (!MetaFileManager.CharacterUtility.Ready) + MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; } public void Dispose() @@ -74,7 +65,7 @@ public class CollectionCacheManager : IDisposable _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); - CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } /// Only creates a new cache, does not update an existing one. @@ -98,9 +89,6 @@ public class CollectionCacheManager : IDisposable => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, () => CalculateEffectiveFileListInternal(collection)); - public bool IsDefault(ModCollection collection) - => _active.Default == collection; - private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. @@ -139,11 +127,7 @@ public class CollectionCacheManager : IDisposable ++collection.ChangeCounter; - if (_active.Default != collection || !CharacterUtility.Ready || !_config.EnableMods) - return; - - ResidentResources.Reload(); - cache.MetaManipulations.SetFiles(); + MetaFileManager.ApplyDefaultFiles(collection); } private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) @@ -238,7 +222,7 @@ public class CollectionCacheManager : IDisposable { foreach (var (collection, _) in _caches) ++collection.ChangeCounter; - CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index aa882ac4..4a1325ed 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -19,23 +21,29 @@ public readonly struct EqdpCache : IDisposable public EqdpCache() { } - public void SetFiles(CollectionCacheManager manager) + public void SetFiles(MetaFileManager manager) { for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); } - public CharacterUtility.MetaList.MetaReverter? TemporarilySetEqdpFile(CollectionCacheManager manager, GenderRace genderRace, bool accessory) + public void SetFile(MetaFileManager manager, MetaIndex index) { - var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - if ((int)idx == -1) - return null; - - var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); - return i != -1 ? manager.TemporarilySetFile(_eqdpFiles[i], idx) : null; + var i = CharacterUtilityData.EqdpIndices.IndexOf(index); + if (i != -1) + manager.SetFile(_eqdpFiles[i], index); } - public void Reset(CollectionCacheManager manager) + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) + { + var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); + Debug.Assert(idx >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); + Debug.Assert(i >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + return manager.TemporarilySetFile(_eqdpFiles[i], idx); + } + + public void Reset() { foreach (var file in _eqdpFiles.OfType()) { @@ -46,20 +54,20 @@ public readonly struct EqdpCache : IDisposable _eqdpManipulations.Clear(); } - public bool ApplyMod(CollectionCacheManager manager, EqdpManipulation manip) + public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip) { _eqdpManipulations.AddOrReplace(manip); var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= - new ExpandedEqdpFile(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar + new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar return manip.Apply(file); } - public bool RevertMod(CollectionCacheManager manager, EqdpManipulation manip) + public bool RevertMod(MetaFileManager manager, EqdpManipulation manip) { if (!_eqdpManipulations.Remove(manip)) return false; - var def = ExpandedEqdpFile.GetDefault(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); + var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId); return manip.Apply(file); diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 8cf19386..f3b7e8f1 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -16,29 +18,38 @@ public struct EqpCache : IDisposable public EqpCache() {} - public void SetFiles(CollectionCacheManager manager) + public void SetFiles(MetaFileManager manager) => manager.SetFile( _eqpFile, MetaIndex.Eqp ); - public static void ResetFiles(CollectionCacheManager manager) + public static void ResetFiles(MetaFileManager manager) => manager.SetFile( null, MetaIndex.Eqp ); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) => manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); - public bool ApplyMod( CollectionCacheManager manager, EqpManipulation manip ) + public void Reset() + { + if (_eqpFile == null) + return; + + _eqpFile.Reset(_eqpManipulations.Select(m => (int)m.SetId)); + _eqpManipulations.Clear(); + } + + public bool ApplyMod( MetaFileManager manager, EqpManipulation manip ) { _eqpManipulations.AddOrReplace( manip ); - _eqpFile ??= new ExpandedEqpFile(); + _eqpFile ??= new ExpandedEqpFile(manager); return manip.Apply( _eqpFile ); } - public bool RevertMod( CollectionCacheManager manager, EqpManipulation manip ) + public bool RevertMod( MetaFileManager manager, EqpManipulation manip ) { var idx = _eqpManipulations.FindIndex( manip.Equals ); if (idx < 0) return false; - var def = ExpandedEqpFile.GetDefault( manip.SetId ); + var def = ExpandedEqpFile.GetDefault( manager, manip.SetId ); manip = new EqpManipulation( def, manip.Slot, manip.SetId ); return manip.Apply( _eqpFile! ); diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 76470d87..ac30e0d6 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -4,6 +4,7 @@ using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -16,31 +17,50 @@ public struct EstCache : IDisposable private EstFile? _estBodyFile = null; private EstFile? _estHeadFile = null; - private readonly List< EstManipulation > _estManipulations = new(); - - public EstCache() - {} + private readonly List _estManipulations = new(); - public void SetFiles(CollectionCacheManager manager) + public EstCache() + { } + + public void SetFiles(MetaFileManager manager) { - manager.SetFile( _estFaceFile, MetaIndex.FaceEst ); - manager.SetFile( _estHairFile, MetaIndex.HairEst ); - manager.SetFile( _estBodyFile, MetaIndex.BodyEst ); - manager.SetFile( _estHeadFile, MetaIndex.HeadEst ); + manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + manager.SetFile(_estHairFile, MetaIndex.HairEst); + manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + manager.SetFile(_estHeadFile, MetaIndex.HeadEst); } - public CharacterUtility.MetaList.MetaReverter? TemporarilySetFiles(CollectionCacheManager manager, EstManipulation.EstType type) + public void SetFile(MetaFileManager manager, MetaIndex index) + { + switch (index) + { + case MetaIndex.FaceEst: + manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + break; + case MetaIndex.HairEst: + manager.SetFile(_estHairFile, MetaIndex.HairEst); + break; + case MetaIndex.BodyEst: + manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + break; + case MetaIndex.HeadEst: + manager.SetFile(_estHeadFile, MetaIndex.HeadEst); + break; + } + } + + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) { var (file, idx) = type switch { - EstManipulation.EstType.Face => ( _estFaceFile, MetaIndex.FaceEst ), - EstManipulation.EstType.Hair => ( _estHairFile, MetaIndex.HairEst ), - EstManipulation.EstType.Body => ( _estBodyFile, MetaIndex.BodyEst ), - EstManipulation.EstType.Head => ( _estHeadFile, MetaIndex.HeadEst ), - _ => ( null, 0 ), + EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst), + EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst), + EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst), + EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst), + _ => (null, 0), }; - - return idx != 0 ? manager.TemporarilySetFile( file, idx ) : null; + + return manager.TemporarilySetFile(file, idx); } public void Reset() @@ -52,27 +72,27 @@ public struct EstCache : IDisposable _estManipulations.Clear(); } - public bool ApplyMod( CollectionCacheManager manager, EstManipulation m ) + public bool ApplyMod(MetaFileManager manager, EstManipulation m) { - _estManipulations.AddOrReplace( m ); + _estManipulations.AddOrReplace(m); var file = m.Slot switch { - EstManipulation.EstType.Hair => _estHairFile ??= new EstFile( EstManipulation.EstType.Hair ), - EstManipulation.EstType.Face => _estFaceFile ??= new EstFile( EstManipulation.EstType.Face ), - EstManipulation.EstType.Body => _estBodyFile ??= new EstFile( EstManipulation.EstType.Body ), - EstManipulation.EstType.Head => _estHeadFile ??= new EstFile( EstManipulation.EstType.Head ), + EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair), + EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face), + EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body), + EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head), _ => throw new ArgumentOutOfRangeException(), }; - return m.Apply( file ); + return m.Apply(file); } - public bool RevertMod( CollectionCacheManager manager, EstManipulation m ) + public bool RevertMod(MetaFileManager manager, EstManipulation m) { if (!_estManipulations.Remove(m)) return false; - var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); - var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); + var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId); + var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); var file = m.Slot switch { EstManipulation.EstType.Hair => _estHairFile!, @@ -81,8 +101,7 @@ public struct EstCache : IDisposable EstManipulation.EstType.Head => _estHeadFile!, _ => throw new ArgumentOutOfRangeException(), }; - return manip.Apply( file ); - + return manip.Apply(file); } public void Dispose() @@ -97,4 +116,4 @@ public struct EstCache : IDisposable _estHeadFile = null; _estManipulations.Clear(); } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 436dfdbf..2698af7b 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -4,6 +4,7 @@ using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -17,13 +18,13 @@ public struct GmpCache : IDisposable public GmpCache() {} - public void SetFiles(CollectionCacheManager manager) + public void SetFiles(MetaFileManager manager) => manager.SetFile( _gmpFile, MetaIndex.Gmp ); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) + public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) => manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); - public void ResetGmp(CollectionCacheManager manager) + public void Reset() { if( _gmpFile == null ) return; @@ -32,19 +33,19 @@ public struct GmpCache : IDisposable _gmpManipulations.Clear(); } - public bool ApplyMod( CollectionCacheManager manager, GmpManipulation manip ) + public bool ApplyMod( MetaFileManager manager, GmpManipulation manip ) { _gmpManipulations.AddOrReplace( manip ); - _gmpFile ??= new ExpandedGmpFile(); + _gmpFile ??= new ExpandedGmpFile(manager); return manip.Apply( _gmpFile ); } - public bool RevertMod( CollectionCacheManager manager, GmpManipulation manip ) + public bool RevertMod( MetaFileManager manager, GmpManipulation manip ) { if (!_gmpManipulations.Remove(manip)) return false; - var def = ExpandedGmpFile.GetDefault( manip.SetId ); + var def = ExpandedGmpFile.GetDefault( manager, manip.SetId ); manip = new GmpManipulation( def, manip.SetId ); return manip.Apply( _gmpFile! ); } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 43dc73e5..2335fadd 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using OtterGui.Filesystem; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -16,13 +17,13 @@ public readonly struct ImcCache : IDisposable public ImcCache() { } - public void SetFiles(CollectionCacheManager manager, ModCollection collection) + public void SetFiles(ModCollection collection) { foreach( var path in _imcFiles.Keys ) collection._cache!.ForceFile( path, CreateImcPath( collection, path ) ); } - public void Reset(CollectionCacheManager manager, ModCollection collection) + public void Reset(ModCollection collection) { foreach( var (path, file) in _imcFiles ) { @@ -33,7 +34,7 @@ public readonly struct ImcCache : IDisposable _imcManipulations.Clear(); } - public bool ApplyMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation manip ) + public bool ApplyMod( MetaFileManager manager, ModCollection collection, ImcManipulation manip ) { if( !manip.Valid ) { @@ -46,7 +47,7 @@ public readonly struct ImcCache : IDisposable { if( !_imcFiles.TryGetValue( path, out var file ) ) { - file = new ImcFile( manip ); + file = new ImcFile( manager, manip ); } if( !manip.Apply( file ) ) @@ -73,7 +74,7 @@ public readonly struct ImcCache : IDisposable return false; } - public bool RevertMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation m ) + public bool RevertMod( MetaFileManager manager, ModCollection collection, ImcManipulation m ) { if( !m.Valid || !_imcManipulations.Remove( m ) ) { @@ -86,7 +87,7 @@ public readonly struct ImcCache : IDisposable return false; } - var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); + var def = ImcFile.GetDefault( manager, path, m.EquipSlot, m.Variant, out _ ); var manip = m.Copy( def ); if( !manip.Apply( file ) ) return false; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index bee01714..abb77f9d 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -2,19 +2,19 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using OtterGui; +using Penumbra.GameData.Enums; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public struct MetaCache : IDisposable, IEnumerable> +public class MetaCache : IDisposable, IEnumerable> { - private readonly CollectionCacheManager _manager; + private readonly MetaFileManager _manager; private readonly ModCollection _collection; private readonly Dictionary _manipulations = new(); private EqpCache _eqpCache = new(); @@ -22,7 +22,7 @@ public struct MetaCache : IDisposable, IEnumerable _manipulations.TryGetValue(manip, out mod); @@ -39,10 +39,10 @@ public struct MetaCache : IDisposable, IEnumerable GetEnumerator(); - public MetaCache(CollectionCacheManager manager, ModCollection collection) + public MetaCache(MetaFileManager manager, ModCollection collection) { - _manager = manager; - _imcCache = new ImcCache(collection); + _manager = manager; + _collection = collection; if (!_manager.CharacterUtility.Ready) _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations; } @@ -54,17 +54,17 @@ public struct MetaCache : IDisposable, IEnumerable Dispose(); + public bool ApplyMod(MetaManipulation manip, IMod mod) { if (_manipulations.ContainsKey(manip)) @@ -125,11 +128,61 @@ public struct MetaCache : IDisposable, IEnumerable false, }; } + + /// Set a single file. + public void SetFile(MetaIndex metaIndex) + { + switch (metaIndex) + { + case MetaIndex.Eqp: + _eqpCache.SetFiles(_manager); + break; + case MetaIndex.Gmp: + _gmpCache.SetFiles(_manager); + break; + case MetaIndex.HumanCmp: + _cmpCache.SetFiles(_manager); + break; + case MetaIndex.FaceEst: + case MetaIndex.HairEst: + case MetaIndex.HeadEst: + case MetaIndex.BodyEst: + _estCache.SetFile(_manager, metaIndex); + break; + default: + _eqdpCache.SetFile(_manager, metaIndex); + break; + } + } + + /// Set the currently relevant IMC files for the collection cache. + public void SetImcFiles() + => _imcCache.SetFiles(_collection); - // Use this when CharacterUtility becomes ready. + public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() + => _eqpCache.TemporarilySetFiles(_manager); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() + => _gmpCache.TemporarilySetFiles(_manager); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() + => _cmpCache.TemporarilySetFiles(_manager); + + public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + => _estCache.TemporarilySetFiles(_manager, type); + + + /// Try to obtain a manipulated IMC file. + public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) + => _imcCache.GetImcFile(path, out file); + + /// Use this when CharacterUtility becomes ready. private void ApplyStoredManipulations() { - if (!Penumbra.CharacterUtility.Ready) + if (!_manager.CharacterUtility.Ready) return; var loaded = 0; @@ -149,29 +202,9 @@ public struct MetaCache : IDisposable, IEnumerable file == null - ? _manager.CharacterUtility.TemporarilyResetResource(metaIndex) - : _manager.CharacterUtility.TemporarilySetResource(metaIndex, (IntPtr)file.Data, file.Length); -} \ No newline at end of file +} diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index f37d9163..f50e2472 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -47,27 +47,6 @@ public partial class ModCollection public FullPath? ResolvePath(Utf8GamePath path) => _cache?.ResolvePath(path); - // Force a file to be resolved to a specific path regardless of conflicts. - internal void ForceFile(Utf8GamePath path, FullPath fullPath) - { - if (CheckFullPath(path, fullPath)) - _cache!.ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) - { - if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) - return true; - - Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}"); - return false; - } - - // Force a file resolve to be removed. - internal void RemoveFile(Utf8GamePath path) - => _cache!.ResolvedFiles.Remove(path); - // Obtain data from the cache. internal MetaCache? MetaCache => _cache?.MetaManipulations; @@ -135,3 +114,8 @@ public partial class ModCollection => _cache?.MetaManipulations.TemporarilySetEstFile(type) ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); } + + +public static class CollectionCacheExtensions +{ +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index e97312b0..dc2047b2 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -3,7 +3,7 @@ using System.IO; using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -12,148 +12,128 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Deserialize and check Eqp Entries and add them to the list if they are non-default. - private void DeserializeEqpEntry( MetaFileInfo metaFileInfo, byte[]? data ) + private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data) { // Eqp can only be valid for equipment. - if( data == null || !metaFileInfo.EquipSlot.IsEquipment() ) - { + if (data == null || !metaFileInfo.EquipSlot.IsEquipment()) return; - } - var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data ); - var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); - var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); - if( _keepDefault || def.Entry != manip.Entry ) - { - MetaManipulations.Add( manip ); - } + var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data); + var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot, + metaFileInfo.PrimaryId); + var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId); + if (_keepDefault || def.Entry != manip.Entry) + MetaManipulations.Add(manip); } // Deserialize and check Eqdp Entries and add them to the list if they are non-default. - private void DeserializeEqdpEntries( MetaFileInfo metaFileInfo, byte[]? data ) + private void DeserializeEqdpEntries(MetaFileInfo metaFileInfo, byte[]? data) { - if( data == null ) - { + if (data == null) return; - } var num = data.Length / 5; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) + using var reader = new BinaryReader(new MemoryStream(data)); + for (var i = 0; i < num; ++i) { // Use the SE gender/race code. - var gr = ( GenderRace )reader.ReadUInt32(); + var gr = (GenderRace)reader.ReadUInt32(); var byteValue = reader.ReadByte(); - if( !gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory() ) - { + if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory()) continue; - } - var value = Eqdp.FromSlotAndBits( metaFileInfo.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); - var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId ), + var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2); + var def = new EqdpManipulation( + ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId), metaFileInfo.EquipSlot, - gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); - var manip = new EqdpManipulation( value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); - if( _keepDefault || def.Entry != manip.Entry ) - { - MetaManipulations.Add( manip ); - } + gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); + var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); + if (_keepDefault || def.Entry != manip.Entry) + MetaManipulations.Add(manip); } } // Deserialize and check Gmp Entries and add them to the list if they are non-default. - private void DeserializeGmpEntry( MetaFileInfo metaFileInfo, byte[]? data ) + private void DeserializeGmpEntry(MetaFileInfo metaFileInfo, byte[]? data) { - if( data == null ) - { + if (data == null) return; - } - using var reader = new BinaryReader( new MemoryStream( data ) ); - var value = ( GmpEntry )reader.ReadUInt32(); + using var reader = new BinaryReader(new MemoryStream(data)); + var value = (GmpEntry)reader.ReadUInt32(); value.UnknownTotal = reader.ReadByte(); - var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId ); - if( _keepDefault || value != def ) - { - MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) ); - } + var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); + if (_keepDefault || value != def) + MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId)); } // Deserialize and check Est Entries and add them to the list if they are non-default. - private void DeserializeEstEntries( MetaFileInfo metaFileInfo, byte[]? data ) + private void DeserializeEstEntries(MetaFileInfo metaFileInfo, byte[]? data) { - if( data == null ) - { + if (data == null) return; - } var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - for( var i = 0; i < num; ++i ) + using var reader = new BinaryReader(new MemoryStream(data)); + for (var i = 0; i < num; ++i) { - var gr = ( GenderRace )reader.ReadUInt16(); + var gr = (GenderRace)reader.ReadUInt16(); var id = reader.ReadUInt16(); var value = reader.ReadUInt16(); - var type = ( metaFileInfo.SecondaryType, metaFileInfo.EquipSlot ) switch + var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch { (BodySlot.Face, _) => EstManipulation.EstType.Face, (BodySlot.Hair, _) => EstManipulation.EstType.Hair, (_, EquipSlot.Head) => EstManipulation.EstType.Head, (_, EquipSlot.Body) => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, + _ => (EstManipulation.EstType)0, }; - if( !gr.IsValid() || type == 0 ) - { + if (!gr.IsValid() || type == 0) continue; - } - var def = EstFile.GetDefault( type, gr, id ); - if( _keepDefault || def != value ) - { - MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) ); - } + var def = EstFile.GetDefault(_metaFileManager, type, gr, id); + if (_keepDefault || def != value) + MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value)); } } // Deserialize and check IMC Entries and add them to the list if they are non-default. // This requires requesting a file from Lumina, which may fail due to TexTools corruption or just not existing. // TexTools creates IMC files for off-hand weapon models which may not exist in the game files. - private void DeserializeImcEntries( MetaFileInfo metaFileInfo, byte[]? data ) + private void DeserializeImcEntries(MetaFileInfo metaFileInfo, byte[]? data) { - if( data == null ) - { + if (data == null) return; - } var num = data.Length / 6; - using var reader = new BinaryReader( new MemoryStream( data ) ); - var values = reader.ReadStructures< ImcEntry >( num ); + using var reader = new BinaryReader(new MemoryStream(data)); + var values = reader.ReadStructures(num); ushort i = 0; try { - var manip = new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, - new ImcEntry() ); - var def = new ImcFile( manip ); - var partIdx = ImcFile.PartIndex( manip.EquipSlot ); // Gets turned to unknown for things without equip, and unknown turns to 0. - foreach( var value in values ) + var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, + metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, + new ImcEntry()); + var def = new ImcFile(_metaFileManager, manip); + var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. + foreach (var value in values) { - if( _keepDefault || !value.Equals( def.GetEntry( partIdx, i ) ) ) + if (_keepDefault || !value.Equals(def.GetEntry(partIdx, i))) { - var imc = new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ); - if( imc.Valid ) - { - MetaManipulations.Add( imc ); - } + var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, + value); + if (imc.Valid) + MetaManipulations.Add(imc); } ++i; } } - catch( Exception e ) + catch (Exception e) { Penumbra.Log.Warning( $"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" - + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); + + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}"); } } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 03aae64a..759474e9 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -12,7 +13,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { - public static Dictionary< string, byte[] > ConvertToTexTools( IEnumerable< MetaManipulation > manips ) + public static Dictionary< string, byte[] > ConvertToTexTools( MetaFileManager manager, IEnumerable< MetaManipulation > manips ) { var ret = new Dictionary< string, byte[] >(); foreach( var group in manips.GroupBy( ManipToPath ) ) @@ -23,8 +24,8 @@ public partial class TexToolsMeta } var bytes = group.Key.EndsWith( ".rgsp" ) - ? WriteRgspFile( group.Key, group ) - : WriteMetaFile( group.Key, group ); + ? WriteRgspFile( manager, group.Key, group ) + : WriteMetaFile( manager, group.Key, group ); if( bytes.Length == 0 ) { continue; @@ -36,7 +37,7 @@ public partial class TexToolsMeta return ret; } - private static byte[] WriteRgspFile( string path, IEnumerable< MetaManipulation > manips ) + private static byte[] WriteRgspFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips ) { var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp ); using var m = new MemoryStream( 45 ); @@ -54,7 +55,7 @@ public partial class TexToolsMeta { foreach( var attribute in attributes ) { - var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( race, attribute ); + var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( manager, race, attribute ); b.Write( value ); } } @@ -72,7 +73,7 @@ public partial class TexToolsMeta return m.GetBuffer(); } - private static byte[] WriteMetaFile( string path, IEnumerable< MetaManipulation > manips ) + private static byte[] WriteMetaFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips ) { var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) ); @@ -103,7 +104,7 @@ public partial class TexToolsMeta b.Write( ( uint )header ); b.Write( offset ); - var size = WriteData( b, offset, header, data ); + var size = WriteData( manager, b, offset, header, data ); b.Write( size ); offset += size; } @@ -111,7 +112,7 @@ public partial class TexToolsMeta return m.ToArray(); } - private static uint WriteData( BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) + private static uint WriteData( MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) { var oldPos = b.BaseStream.Position; b.Seek( ( int )offset, SeekOrigin.Begin ); @@ -120,7 +121,7 @@ public partial class TexToolsMeta { case MetaManipulation.Type.Imc: var allManips = manips.ToList(); - var baseFile = new ImcFile( allManips[ 0 ].Imc ); + var baseFile = new ImcFile( manager, allManips[ 0 ].Imc ); foreach( var manip in allManips ) { manip.Imc.Apply( baseFile ); diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 8eb0c49a..7bb837ce 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Penumbra.GameData.Enums; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -9,7 +10,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Parse a single rgsp file. - public static TexToolsMeta FromRgspFile( string filePath, byte[] data, bool keepDefault ) + public static TexToolsMeta FromRgspFile( MetaFileManager manager, string filePath, byte[] data, bool keepDefault ) { if( data.Length != 45 && data.Length != 42 ) { @@ -25,7 +26,7 @@ public partial class TexToolsMeta var flag = br.ReadByte(); var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); - var ret = new TexToolsMeta( filePath, version ); + var ret = new TexToolsMeta( manager, filePath, version ); // SubRace is offset by one due to Unknown. var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); @@ -46,7 +47,7 @@ public partial class TexToolsMeta // Add the given values to the manipulations if they are not default. void Add( RspAttribute attribute, float value ) { - var def = CmpFile.GetDefault( subRace, attribute ); + var def = CmpFile.GetDefault( manager, subRace, attribute ); if( keepDefault || value != def ) { ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) ); diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index cbf1e9fa..b07ac8ab 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -4,34 +4,37 @@ using System.IO; using System.Text; using Penumbra.GameData; using Penumbra.Import.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; -// TexTools provices custom generated *.meta files for its modpacks, that contain changes to -// - imc files -// - eqp files -// - gmp files -// - est files -// - eqdp files -// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. -// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. -// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. -// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. +/// TexTools provices custom generated *.meta files for its modpacks, that contain changes to +/// - imc files +/// - eqp files +/// - gmp files +/// - est files +/// - eqdp files +/// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. +/// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. +/// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. +/// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. public partial class TexToolsMeta -{ - // An empty TexToolsMeta. - public static readonly TexToolsMeta Invalid = new(string.Empty, 0); +{ + /// An empty TexToolsMeta. + public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. - public readonly uint Version; public readonly string FilePath; public readonly List< MetaManipulation > MetaManipulations = new(); private readonly bool _keepDefault = false; + + private readonly MetaFileManager _metaFileManager; - public TexToolsMeta( IGamePathParser parser, byte[] data, bool keepDefault ) - { + public TexToolsMeta( MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault ) + { + _metaFileManager = metaFileManager; _keepDefault = keepDefault; try { @@ -80,10 +83,11 @@ public partial class TexToolsMeta } } - private TexToolsMeta( string filePath, uint version ) + private TexToolsMeta( MetaFileManager metaFileManager, string filePath, uint version ) { - FilePath = filePath; - Version = version; + _metaFileManager = metaFileManager; + FilePath = filePath; + Version = version; } // Read a null terminated string from a binary reader. diff --git a/Penumbra/Interop/Services/MetaFileManager.cs b/Penumbra/Interop/Services/MetaFileManager.cs deleted file mode 100644 index 382a6c84..00000000 --- a/Penumbra/Interop/Services/MetaFileManager.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Memory; -using Penumbra.GameData; - -namespace Penumbra.Interop.Services; - -public unsafe class MetaFileManager -{ - public MetaFileManager() - { - SignatureHelper.Initialise(this); - } - - // Allocate in the games space for file storage. - // We only need this if using any meta file. - [Signature(Sigs.GetFileSpace)] - private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero; - - public IMemorySpace* GetFileSpace() - => ((delegate* unmanaged)_getFileSpaceAddress)(); - - public void* AllocateFileMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateFileMemory(int length, int alignment = 0) - => AllocateFileMemory((ulong)length, (ulong)alignment); - - public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateDefaultMemory(int length, int alignment = 0) - => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); - - public void Free(IntPtr ptr, int length) - => IMemorySpace.Free((void*)ptr, (ulong)length); -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 1696abf6..e67c5efd 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -8,46 +8,46 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -// The human.cmp file contains many character-relevant parameters like color sets. -// We only support manipulating the racial scaling parameters at the moment. +/// +/// The human.cmp file contains many character-relevant parameters like color sets. +/// We only support manipulating the racial scaling parameters at the moment. +/// public sealed unsafe class CmpFile : MetaBaseFile { public static readonly CharacterUtility.InternalIndex InternalIndex = - CharacterUtility.ReverseIndices[ ( int )MetaIndex.HumanCmp ]; + CharacterUtility.ReverseIndices[(int)MetaIndex.HumanCmp]; private const int RacialScalingStart = 0x2A800; - public float this[ SubRace subRace, RspAttribute attribute ] + public float this[SubRace subRace, RspAttribute attribute] { - get => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); - set => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ) = value; + get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); + set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4) = value; } public override void Reset() - => MemoryUtility.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); + => MemoryUtility.MemCpyUnchecked(Data, (byte*)DefaultData.Data, DefaultData.Length); - public void Reset( IEnumerable< (SubRace, RspAttribute) > entries ) + public void Reset(IEnumerable<(SubRace, RspAttribute)> entries) { - foreach( var (r, a) in entries ) - { - this[ r, a ] = GetDefault( r, a ); - } + foreach (var (r, a) in entries) + this[r, a] = GetDefault(Manager, r, a); } - public CmpFile() - : base( MetaIndex.HumanCmp ) + public CmpFile(MetaFileManager manager) + : base(manager, MetaIndex.HumanCmp) { - AllocateData( DefaultData.Length ); + AllocateData(DefaultData.Length); Reset(); } - public static float GetDefault( SubRace subRace, RspAttribute attribute ) + public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( InternalIndex ).Address; - return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); + var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; + return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); } - private static int ToRspIndex( SubRace subRace ) + private static int ToRspIndex(SubRace subRace) => subRace switch { SubRace.Midlander => 0, @@ -67,6 +67,6 @@ public sealed unsafe class CmpFile : MetaBaseFile SubRace.Rava => 70, SubRace.Veena => 71, SubRace.Unknown => 0, - _ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), + _ => throw new ArgumentOutOfRangeException(nameof(subRace), subRace, null), }; -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 0122fe5a..14275467 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -8,14 +8,15 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -// EQDP file structure: -// [Identifier][BlockSize:ushort][BlockCount:ushort] -// BlockCount x [BlockHeader:ushort] -// Containing offsets for blocks, ushort.Max means collapsed. -// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2. -// ExpandedBlockCount x [Entry] - -// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it. +/// +/// EQDP file structure: +/// [Identifier][BlockSize:ushort][BlockCount:ushort] +/// BlockCount x [BlockHeader:ushort] +/// Containing offsets for blocks, ushort.Max means collapsed. +/// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2. +/// ExpandedBlockCount x [Entry] +/// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it. +/// public sealed unsafe class ExpandedEqdpFile : MetaBaseFile { private const ushort BlockHeaderSize = 2; @@ -28,117 +29,103 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public readonly int DataOffset; public ushort Identifier - => *( ushort* )Data; + => *(ushort*)Data; public ushort BlockSize - => *( ushort* )( Data + 2 ); + => *(ushort*)(Data + 2); public ushort BlockCount - => *( ushort* )( Data + 4 ); + => *(ushort*)(Data + 4); public int Count - => ( Length - DataOffset ) / EqdpEntrySize; + => (Length - DataOffset) / EqdpEntrySize; - public EqdpEntry this[ int idx ] + public EqdpEntry this[int idx] { get { - if( idx >= Count || idx < 0 ) - { + if (idx >= Count || idx < 0) throw new IndexOutOfRangeException(); - } - return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) ); + return (EqdpEntry)(*(ushort*)(Data + DataOffset + EqdpEntrySize * idx)); } set { - if( idx >= Count || idx < 0 ) - { + if (idx >= Count || idx < 0) throw new IndexOutOfRangeException(); - } - *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) = ( ushort )value; + *(ushort*)(Data + DataOffset + EqdpEntrySize * idx) = (ushort)value; } } public override void Reset() { - var def = ( byte* )DefaultData.Data; - MemoryUtility.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); + var def = (byte*)DefaultData.Data; + MemoryUtility.MemCpyUnchecked(Data, def, IdentifierSize + PreambleSize); - var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); + var controlPtr = (ushort*)(def + IdentifierSize + PreambleSize); var dataBasePtr = controlPtr + BlockCount; - var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); - var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize ); - for( var i = 0; i < BlockCount; ++i ) + var myDataPtr = (ushort*)(Data + IdentifierSize + PreambleSize + 2 * BlockCount); + var myControlPtr = (ushort*)(Data + IdentifierSize + PreambleSize); + for (var i = 0; i < BlockCount; ++i) { - if( controlPtr[ i ] == CollapsedBlock ) - { - MemoryUtility.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize ); - } + if (controlPtr[i] == CollapsedBlock) + MemoryUtility.MemSet(myDataPtr, 0, BlockSize * EqdpEntrySize); else - { - MemoryUtility.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); - } + MemoryUtility.MemCpyUnchecked(myDataPtr, dataBasePtr + controlPtr[i], BlockSize * EqdpEntrySize); - myControlPtr[ i ] = ( ushort )( i * BlockSize ); - myDataPtr += BlockSize; + myControlPtr[i] = (ushort)(i * BlockSize); + myDataPtr += BlockSize; } - MemoryUtility.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); + MemoryUtility.MemSet(myDataPtr, 0, Length - (int)((byte*)myDataPtr - Data)); } - public void Reset( IEnumerable< int > entries ) + public void Reset(IEnumerable entries) { - foreach( var entry in entries ) - { - this[ entry ] = GetDefault( entry ); - } + foreach (var entry in entries) + this[entry] = GetDefault(entry); } - public ExpandedEqdpFile( GenderRace raceCode, bool accessory ) - : base( CharacterUtilityData.EqdpIdx( raceCode, accessory ) ) + public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory) + : base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory)) { - var def = ( byte* )DefaultData.Data; - var blockSize = *( ushort* )( def + IdentifierSize ); - var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 ); + var def = (byte*)DefaultData.Data; + var blockSize = *(ushort*)(def + IdentifierSize); + var totalBlockCount = *(ushort*)(def + IdentifierSize + 2); var totalBlockSize = blockSize * EqdpEntrySize; DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize; - var fullLength = DataOffset + totalBlockCount * totalBlockSize; - fullLength += ( FileAlignment - ( fullLength & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); - AllocateData( fullLength ); + var fullLength = DataOffset + totalBlockCount * totalBlockSize; + fullLength += (FileAlignment - (fullLength & (FileAlignment - 1))) & (FileAlignment - 1); + AllocateData(fullLength); Reset(); } - public EqdpEntry GetDefault( int setIdx ) - => GetDefault( Index, setIdx ); + public EqdpEntry GetDefault(int setIdx) + => GetDefault(Manager, Index, setIdx); - public static EqdpEntry GetDefault( CharacterUtility.InternalIndex idx, int setIdx ) - => GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( idx ).Address, setIdx ); + public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, int setIdx) + => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, setIdx); - public static EqdpEntry GetDefault( byte* data, int setIdx ) + public static EqdpEntry GetDefault(byte* data, int setIdx) { - var blockSize = *( ushort* )( data + IdentifierSize ); - var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 ); + var blockSize = *(ushort*)(data + IdentifierSize); + var totalBlockCount = *(ushort*)(data + IdentifierSize + 2); var blockIdx = setIdx / blockSize; - if( blockIdx >= totalBlockCount ) - { + if (blockIdx >= totalBlockCount) return 0; - } - var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ]; - if( block == CollapsedBlock ) - { + var block = ((ushort*)(data + IdentifierSize + PreambleSize))[blockIdx]; + if (block == CollapsedBlock) return 0; - } - var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 ); - return ( EqdpEntry )( *( blockData + setIdx % blockSize ) ); + var blockData = (ushort*)(data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2); + return (EqdpEntry)(*(blockData + setIdx % blockSize)); } - public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) - => GetDefault( CharacterUtility.ReverseIndices[ ( int )CharacterUtilityData.EqdpIdx( raceCode, accessory ) ], setIdx ); -} \ No newline at end of file + public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, int setIdx) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setIdx); +} diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 8ad3e95f..e21e18c7 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -9,11 +9,13 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -// EQP/GMP Structure: -// 64 x [Block collapsed or not bit] -// 159 x [EquipmentParameter:ulong] -// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong] -// Item 0 does not exist and is sent to Item 1 instead. +/// +/// EQP/GMP Structure: +/// 64 x [Block collapsed or not bit] +/// 159 x [EquipmentParameter:ulong] +/// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong] +/// Item 0 does not exist and is sent to Item 1 instead. +/// public unsafe class ExpandedEqpGmpBase : MetaBaseFile { protected const int BlockSize = 160; @@ -24,19 +26,19 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public const int Count = BlockSize * NumBlocks; public ulong ControlBlock - => *( ulong* )Data; + => *(ulong*)Data; - protected ulong GetInternal( int idx ) + protected ulong GetInternal(int idx) { return idx switch { >= Count => throw new IndexOutOfRangeException(), - <= 1 => *( ( ulong* )Data + 1 ), - _ => *( ( ulong* )Data + idx ), + <= 1 => *((ulong*)Data + 1), + _ => *((ulong*)Data + idx), }; } - protected void SetInternal( int idx, ulong value ) + protected void SetInternal(int idx, ulong value) { idx = idx switch { @@ -45,67 +47,62 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile _ => idx, }; - *( ( ulong* )Data + idx ) = value; + *((ulong*)Data + idx) = value; } - protected virtual void SetEmptyBlock( int idx ) + protected virtual void SetEmptyBlock(int idx) { - MemoryUtility.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); + MemoryUtility.MemSet(Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize); } public sealed override void Reset() { - var ptr = ( byte* )DefaultData.Data; - var controlBlock = *( ulong* )ptr; + var ptr = (byte*)DefaultData.Data; + var controlBlock = *(ulong*)ptr; var expandedBlocks = 0; - for( var i = 0; i < NumBlocks; ++i ) + for (var i = 0; i < NumBlocks; ++i) { - var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; - if( !collapsed ) + var collapsed = ((controlBlock >> i) & 1) == 0; + if (!collapsed) { - MemoryUtility.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); + MemoryUtility.MemCpyUnchecked(Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, + BlockSize * EntrySize); expandedBlocks++; } else { - SetEmptyBlock( i ); + SetEmptyBlock(i); } } - *( ulong* )Data = ulong.MaxValue; + *(ulong*)Data = ulong.MaxValue; } - public ExpandedEqpGmpBase( bool gmp ) - : base( gmp ? MetaIndex.Gmp : MetaIndex.Eqp ) + public ExpandedEqpGmpBase(MetaFileManager manager, bool gmp) + : base(manager, gmp ? MetaIndex.Gmp : MetaIndex.Eqp) { - AllocateData( MaxSize ); + AllocateData(MaxSize); Reset(); } - protected static ulong GetDefaultInternal( CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def ) + protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource(fileIndex).Address; - if( setIdx == 0 ) - { + var data = (byte*)manager.CharacterUtility.DefaultResource(fileIndex).Address; + if (setIdx == 0) setIdx = 1; - } var blockIdx = setIdx / BlockSize; - if( blockIdx >= NumBlocks ) - { + if (blockIdx >= NumBlocks) return def; - } - var control = *( ulong* )data; + var control = *(ulong*)data; var blockBit = 1ul << blockIdx; - if( ( control & blockBit ) == 0 ) - { + if ((control & blockBit) == 0) return def; - } - var count = BitOperations.PopCount( control & ( blockBit - 1 ) ); + var count = BitOperations.PopCount(control & (blockBit - 1)); var idx = setIdx % BlockSize; - var ptr = ( ulong* )data + BlockSize * count + idx; + var ptr = (ulong*)data + BlockSize * count + idx; return *ptr; } } @@ -113,44 +110,40 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = - CharacterUtility.ReverseIndices[ (int) MetaIndex.Eqp ]; + CharacterUtility.ReverseIndices[(int)MetaIndex.Eqp]; - public ExpandedEqpFile() - : base( false ) + public ExpandedEqpFile(MetaFileManager manager) + : base(manager, false) { } - public EqpEntry this[ int idx ] + public EqpEntry this[int idx] { - get => ( EqpEntry )GetInternal( idx ); - set => SetInternal( idx, ( ulong )value ); + get => (EqpEntry)GetInternal(idx); + set => SetInternal(idx, (ulong)value); } - public static EqpEntry GetDefault( int setIdx ) - => ( EqpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )Eqp.DefaultEntry ); + public static EqpEntry GetDefault(MetaFileManager manager, int setIdx) + => (EqpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)Eqp.DefaultEntry); - protected override unsafe void SetEmptyBlock( int idx ) + protected override unsafe void SetEmptyBlock(int idx) { - var blockPtr = ( ulong* )( Data + idx * BlockSize * EntrySize ); + var blockPtr = (ulong*)(Data + idx * BlockSize * EntrySize); var endPtr = blockPtr + BlockSize; - for( var ptr = blockPtr; ptr < endPtr; ++ptr ) - { - *ptr = ( ulong )Eqp.DefaultEntry; - } + for (var ptr = blockPtr; ptr < endPtr; ++ptr) + *ptr = (ulong)Eqp.DefaultEntry; } - public void Reset( IEnumerable< int > entries ) + public void Reset(IEnumerable entries) { - foreach( var entry in entries ) - { - this[ entry ] = GetDefault( entry ); - } + foreach (var entry in entries) + this[entry] = GetDefault(Manager, entry); } - public IEnumerator< EqpEntry > GetEnumerator() + public IEnumerator GetEnumerator() { - for( var idx = 1; idx < Count; ++idx ) - yield return this[ idx ]; + for (var idx = 1; idx < Count; ++idx) + yield return this[idx]; } IEnumerator IEnumerable.GetEnumerator() @@ -160,35 +153,33 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = - CharacterUtility.ReverseIndices[( int )MetaIndex.Gmp]; + CharacterUtility.ReverseIndices[(int)MetaIndex.Gmp]; - public ExpandedGmpFile() - : base( true ) + public ExpandedGmpFile(MetaFileManager manager) + : base(manager, true) { } - public GmpEntry this[ int idx ] + public GmpEntry this[int idx] { - get => ( GmpEntry )GetInternal( idx ); - set => SetInternal( idx, ( ulong )value ); + get => (GmpEntry)GetInternal(idx); + set => SetInternal(idx, (ulong)value); } - public static GmpEntry GetDefault( int setIdx ) - => ( GmpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )GmpEntry.Default ); + public static GmpEntry GetDefault(MetaFileManager manager, int setIdx) + => (GmpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)GmpEntry.Default); - public void Reset( IEnumerable< int > entries ) + public void Reset(IEnumerable entries) { - foreach( var entry in entries ) - { - this[ entry ] = GetDefault( entry ); - } + foreach (var entry in entries) + this[entry] = GetDefault(Manager, entry); } public IEnumerator GetEnumerator() { - for( var idx = 1; idx < Count; ++idx ) + for (var idx = 1; idx < Count; ++idx) yield return this[idx]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index d2d36f28..72fae443 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -8,11 +8,13 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -// EST Structure: -// 1x [NumEntries : UInt32] -// Apparently entries need to be sorted. -// #NumEntries x [SetId : UInt16] [RaceId : UInt16] -// #NumEntries x [SkeletonId : UInt16] +/// +/// EST Structure: +/// 1x [NumEntries : UInt32] +/// Apparently entries need to be sorted. +/// #NumEntries x [SetId : UInt16] [RaceId : UInt16] +/// #NumEntries x [SkeletonId : UInt16] +/// public sealed unsafe class EstFile : MetaBaseFile { private const ushort EntryDescSize = 4; @@ -20,10 +22,10 @@ public sealed unsafe class EstFile : MetaBaseFile private const int IncreaseSize = 512; public int Count - => *( int* )Data; + => *(int*)Data; private int Size - => 4 + Count * ( EntryDescSize + EntrySize ); + => 4 + Count * (EntryDescSize + EntrySize); public enum EstEntryChange { @@ -33,176 +35,154 @@ public sealed unsafe class EstFile : MetaBaseFile Removed, } - public ushort this[ GenderRace genderRace, ushort setId ] + public ushort this[GenderRace genderRace, ushort setId] { get { - var (idx, exists) = FindEntry( genderRace, setId ); - if( !exists ) - { + var (idx, exists) = FindEntry(genderRace, setId); + if (!exists) return 0; - } - return *( ushort* )( Data + EntryDescSize * ( Count + 1 ) + EntrySize * idx ); + return *(ushort*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); } - set => SetEntry( genderRace, setId, value ); + set => SetEntry(genderRace, setId, value); } - private void InsertEntry( int idx, GenderRace genderRace, ushort setId, ushort skeletonId ) + private void InsertEntry(int idx, GenderRace genderRace, ushort setId, ushort skeletonId) { - if( Length < Size + EntryDescSize + EntrySize ) - { - ResizeResources( Length + IncreaseSize ); - } + if (Length < Size + EntryDescSize + EntrySize) + ResizeResources(Length + IncreaseSize); - var control = ( Info* )( Data + 4 ); - var entries = ( ushort* )( control + Count ); + var control = (Info*)(Data + 4); + var entries = (ushort*)(control + Count); - for( var i = Count - 1; i >= idx; --i ) - { - entries[ i + 3 ] = entries[ i ]; - } + for (var i = Count - 1; i >= idx; --i) + entries[i + 3] = entries[i]; - entries[ idx + 2 ] = skeletonId; + entries[idx + 2] = skeletonId; - for( var i = idx - 1; i >= 0; --i ) - { - entries[ i + 2 ] = entries[ i ]; - } + for (var i = idx - 1; i >= 0; --i) + entries[i + 2] = entries[i]; - for( var i = Count - 1; i >= idx; --i ) - { - control[ i + 1 ] = control[ i ]; - } + for (var i = Count - 1; i >= idx; --i) + control[i + 1] = control[i]; - control[ idx ] = new Info( genderRace, setId ); + control[idx] = new Info(genderRace, setId); - *( int* )Data = Count + 1; + *(int*)Data = Count + 1; } - private void RemoveEntry( int idx ) + private void RemoveEntry(int idx) { - var control = ( Info* )( Data + 4 ); - var entries = ( ushort* )( control + Count ); + var control = (Info*)(Data + 4); + var entries = (ushort*)(control + Count); - for( var i = idx; i < Count; ++i ) - { - control[ i ] = control[ i + 1 ]; - } + for (var i = idx; i < Count; ++i) + control[i] = control[i + 1]; - for( var i = 0; i < idx; ++i ) - { - entries[ i - 2 ] = entries[ i ]; - } + for (var i = 0; i < idx; ++i) + entries[i - 2] = entries[i]; - for( var i = idx; i < Count - 1; ++i ) - { - entries[ i - 2 ] = entries[ i + 1 ]; - } + for (var i = idx; i < Count - 1; ++i) + entries[i - 2] = entries[i + 1]; - entries[ Count - 3 ] = 0; - entries[ Count - 2 ] = 0; - entries[ Count - 1 ] = 0; - *( int* )Data = Count - 1; + entries[Count - 3] = 0; + entries[Count - 2] = 0; + entries[Count - 1] = 0; + *(int*)Data = Count - 1; } - [StructLayout( LayoutKind.Sequential, Size = 4 )] - private struct Info : IComparable< Info > + [StructLayout(LayoutKind.Sequential, Size = 4)] + private struct Info : IComparable { public readonly ushort SetId; public readonly GenderRace GenderRace; - public Info( GenderRace gr, ushort setId ) + public Info(GenderRace gr, ushort setId) { GenderRace = gr; SetId = setId; } - public int CompareTo( Info other ) + public int CompareTo(Info other) { - var genderRaceComparison = GenderRace.CompareTo( other.GenderRace ); - return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo( other.SetId ); + var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); + return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo(other.SetId); } } - private static (int, bool) FindEntry( ReadOnlySpan< Info > data, GenderRace genderRace, ushort setId ) + private static (int, bool) FindEntry(ReadOnlySpan data, GenderRace genderRace, ushort setId) { - var idx = data.BinarySearch( new Info( genderRace, setId ) ); - return idx < 0 ? ( ~idx, false ) : ( idx, true ); + var idx = data.BinarySearch(new Info(genderRace, setId)); + return idx < 0 ? (~idx, false) : (idx, true); } - private (int, bool) FindEntry( GenderRace genderRace, ushort setId ) + private (int, bool) FindEntry(GenderRace genderRace, ushort setId) { - var span = new ReadOnlySpan< Info >( Data + 4, Count ); - return FindEntry( span, genderRace, setId ); + var span = new ReadOnlySpan(Data + 4, Count); + return FindEntry(span, genderRace, setId); } - public EstEntryChange SetEntry( GenderRace genderRace, ushort setId, ushort skeletonId ) + public EstEntryChange SetEntry(GenderRace genderRace, ushort setId, ushort skeletonId) { - var (idx, exists) = FindEntry( genderRace, setId ); - if( exists ) + var (idx, exists) = FindEntry(genderRace, setId); + if (exists) { - var value = *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ); - if( value == skeletonId ) - { + var value = *(ushort*)(Data + 4 * (Count + 1) + 2 * idx); + if (value == skeletonId) return EstEntryChange.Unchanged; - } - if( skeletonId == 0 ) + if (skeletonId == 0) { - RemoveEntry( idx ); + RemoveEntry(idx); return EstEntryChange.Removed; } - *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ) = skeletonId; + *(ushort*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; return EstEntryChange.Changed; } - if( skeletonId == 0 ) - { + if (skeletonId == 0) return EstEntryChange.Unchanged; - } - InsertEntry( idx, genderRace, setId, skeletonId ); + InsertEntry(idx, genderRace, setId, skeletonId); return EstEntryChange.Added; } public override void Reset() { var (d, length) = DefaultData; - var data = ( byte* )d; - MemoryUtility.MemCpyUnchecked( Data, data, length ); - MemoryUtility.MemSet( Data + length, 0, Length - length ); + var data = (byte*)d; + MemoryUtility.MemCpyUnchecked(Data, data, length); + MemoryUtility.MemSet(Data + length, 0, Length - length); } - public EstFile( EstManipulation.EstType estType ) - : base( ( MetaIndex )estType ) + public EstFile(MetaFileManager manager, EstManipulation.EstType estType) + : base(manager, (MetaIndex)estType) { var length = DefaultData.Length; - AllocateData( length + IncreaseSize ); + AllocateData(length + IncreaseSize); Reset(); } - public ushort GetDefault( GenderRace genderRace, ushort setId ) - => GetDefault( Index, genderRace, setId ); + public ushort GetDefault(GenderRace genderRace, ushort setId) + => GetDefault(Manager, Index, genderRace, setId); - public static ushort GetDefault( CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId ) + public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId) { - var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( index ).Address; - var count = *( int* )data; - var span = new ReadOnlySpan< Info >( data + 4, count ); - var (idx, found) = FindEntry( span, genderRace, setId ); - if( !found ) - { + var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; + var count = *(int*)data; + var span = new ReadOnlySpan(data + 4, count); + var (idx, found) = FindEntry(span, genderRace, setId); + if (!found) return 0; - } - return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize ); + return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); } - public static ushort GetDefault( MetaIndex metaIndex, GenderRace genderRace, ushort setId ) - => GetDefault( CharacterUtility.ReverseIndices[ ( int )metaIndex ], genderRace, setId ); + public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, ushort setId) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, setId); - public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId ) - => GetDefault( ( MetaIndex )estType, genderRace, setId ); -} \ No newline at end of file + public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, ushort setId) + => GetDefault(manager, (MetaIndex)estType, genderRace, setId); +} diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index c475011a..0b64e1e8 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -3,15 +3,16 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; - -// EVP file structure: -// [Identifier:3 bytes, EVP] -// [NumModels:ushort] -// NumModels x [ModelId:ushort] -// Containing the relevant model IDs. Seems to be sorted. -// NumModels x [DataArray]:512 Byte] -// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. -// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. +/// +/// EVP file structure: +/// [Identifier:3 bytes, EVP] +/// [NumModels:ushort] +/// NumModels x [ModelId:ushort] +/// Containing the relevant model IDs. Seems to be sorted. +/// NumModels x [DataArray]:512 Byte] +/// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. +/// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. +/// public unsafe class EvpFile : MetaBaseFile { public const int FlagArraySize = 512; @@ -26,45 +27,39 @@ public unsafe class EvpFile : MetaBaseFile } public int NumModels - => Data[ 3 ]; + => Data[3]; - public ReadOnlySpan< ushort > ModelSetIds + public ReadOnlySpan ModelSetIds => new(Data + 4, NumModels); - public ushort ModelSetId( int idx ) - => idx >= 0 && idx < NumModels ? ( ( ushort* )( Data + 4 ) )[ idx ] : ushort.MaxValue; + public ushort ModelSetId(int idx) + => idx >= 0 && idx < NumModels ? ((ushort*)(Data + 4))[idx] : ushort.MaxValue; - public ReadOnlySpan< EvpFlag > Flags( int idx ) + public ReadOnlySpan Flags(int idx) => new(Data + 4 + idx * FlagArraySize, FlagArraySize); - public EvpFlag Flag( ushort modelSet, int arrayIndex ) + public EvpFlag Flag(ushort modelSet, int arrayIndex) { - if( arrayIndex is >= FlagArraySize or < 0 ) - { + if (arrayIndex is >= FlagArraySize or < 0) return EvpFlag.None; - } var ids = ModelSetIds; - for( var i = 0; i < ids.Length; ++i ) + for (var i = 0; i < ids.Length; ++i) { - var model = ids[ i ]; - if( model < modelSet ) - { + var model = ids[i]; + if (model < modelSet) continue; - } - if( model > modelSet ) - { + if (model > modelSet) break; - } - return Flags( i )[ arrayIndex ]; + return Flags(i)[arrayIndex]; } return EvpFlag.None; } - public EvpFile() - : base( ( MetaIndex )1 ) // TODO: Name + public EvpFile(MetaFileManager manager) + : base(manager, (MetaIndex)1) // TODO: Name { } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 32a9c925..2dc60120 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,11 +1,10 @@ using System; using System.Numerics; -using Newtonsoft.Json; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -16,7 +15,7 @@ public class ImcException : Exception public readonly ImcManipulation Manipulation; public readonly string GamePath; - public ImcException( ImcManipulation manip, Utf8GamePath path ) + public ImcException(ImcManipulation manip, Utf8GamePath path) { Manipulation = manip; GamePath = path.ToString(); @@ -34,44 +33,42 @@ public unsafe class ImcFile : MetaBaseFile private const int PreambleSize = 4; public int ActualLength - => NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize; + => NumParts * sizeof(ImcEntry) * (Count + 1) + PreambleSize; public int Count - => CountInternal( Data ); + => CountInternal(Data); public readonly Utf8GamePath Path; public readonly int NumParts; - public ReadOnlySpan< ImcEntry > Span - => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); + public ReadOnlySpan Span + => new((ImcEntry*)(Data + PreambleSize), (Length - PreambleSize) / sizeof(ImcEntry)); - private static int CountInternal( byte* data ) - => *( ushort* )data; + private static int CountInternal(byte* data) + => *(ushort*)data; - private static ushort PartMask( byte* data ) - => *( ushort* )( data + 2 ); + private static ushort PartMask(byte* data) + => *(ushort*)(data + 2); - private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) + private static ImcEntry* VariantPtr(byte* data, int partIdx, int variantIdx) { var flag = 1 << partIdx; - if( ( PartMask( data ) & flag ) == 0 || variantIdx > CountInternal( data ) ) - { + if ((PartMask(data) & flag) == 0 || variantIdx > CountInternal(data)) return null; - } - var numParts = BitOperations.PopCount( PartMask( data ) ); - var ptr = ( ImcEntry* )( data + PreambleSize ); + var numParts = BitOperations.PopCount(PartMask(data)); + var ptr = (ImcEntry*)(data + PreambleSize); ptr += variantIdx * numParts + partIdx; return ptr; } - public ImcEntry GetEntry( int partIdx, int variantIdx ) + public ImcEntry GetEntry(int partIdx, int variantIdx) { - var ptr = VariantPtr( Data, partIdx, variantIdx ); + var ptr = VariantPtr(Data, partIdx, variantIdx); return ptr == null ? new ImcEntry() : *ptr; } - public static int PartIndex( EquipSlot slot ) + public static int PartIndex(EquipSlot slot) => slot switch { EquipSlot.Head => 0, @@ -87,52 +84,44 @@ public unsafe class ImcFile : MetaBaseFile _ => 0, }; - public bool EnsureVariantCount( int numVariants ) + public bool EnsureVariantCount(int numVariants) { - if( numVariants <= Count ) - { + if (numVariants <= Count) return true; - } var oldCount = Count; - *( ushort* )Data = ( ushort )numVariants; - if( ActualLength > Length ) + *(ushort*)Data = (ushort)numVariants; + if (ActualLength > Length) { - var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; - Penumbra.Log.Verbose( $"Resized IMC {Path} from {Length} to {newLength}." ); - ResizeResources( newLength ); + var newLength = (((ActualLength - 1) >> 7) + 1) << 7; + Penumbra.Log.Verbose($"Resized IMC {Path} from {Length} to {newLength}."); + ResizeResources(newLength); } - var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); - for( var i = oldCount + 1; i < numVariants + 1; ++i ) - { - MemoryUtility.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); - } + var defaultPtr = (ImcEntry*)(Data + PreambleSize); + for (var i = oldCount + 1; i < numVariants + 1; ++i) + MemoryUtility.MemCpyUnchecked(defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof(ImcEntry)); - Penumbra.Log.Verbose( $"Expanded IMC {Path} from {oldCount} to {numVariants} variants." ); + Penumbra.Log.Verbose($"Expanded IMC {Path} from {oldCount} to {numVariants} variants."); return true; } - public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry ) + public bool SetEntry(int partIdx, int variantIdx, ImcEntry entry) { - if( partIdx >= NumParts ) + if (partIdx >= NumParts) + return false; + + EnsureVariantCount(variantIdx); + + var variantPtr = VariantPtr(Data, partIdx, variantIdx); + if (variantPtr == null) { + Penumbra.Log.Error("Error during expansion of imc file."); return false; } - EnsureVariantCount( variantIdx ); - - var variantPtr = VariantPtr( Data, partIdx, variantIdx ); - if( variantPtr == null ) - { - Penumbra.Log.Error( "Error during expansion of imc file." ); + if (variantPtr->Equals(entry)) return false; - } - - if( variantPtr->Equals( entry ) ) - { - return false; - } *variantPtr = entry; return true; @@ -141,70 +130,64 @@ public unsafe class ImcFile : MetaBaseFile public override void Reset() { - var file = DalamudServices.SGameData.GetFile( Path.ToString() ); - fixed( byte* ptr = file!.Data ) + var file = DalamudServices.SGameData.GetFile(Path.ToString()); + fixed (byte* ptr = file!.Data) { - MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); - MemoryUtility.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); + MemoryUtility.MemCpyUnchecked(Data, ptr, file.Data.Length); + MemoryUtility.MemSet(Data + file.Data.Length, 0, Length - file.Data.Length); } } - public ImcFile( ImcManipulation manip ) - : base( 0 ) + public ImcFile(MetaFileManager manager, ImcManipulation manip) + : base(manager, 0) { Path = manip.GamePath(); - var file = DalamudServices.SGameData.GetFile( Path.ToString() ); - if( file == null ) - { - throw new ImcException( manip, Path ); - } + var file = manager.GameData.GetFile(Path.ToString()); + if (file == null) + throw new ImcException(manip, Path); - fixed( byte* ptr = file.Data ) + fixed (byte* ptr = file.Data) { - NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); - AllocateData( file.Data.Length ); - MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); + NumParts = BitOperations.PopCount(*(ushort*)(ptr + 2)); + AllocateData(file.Data.Length); + MemoryUtility.MemCpyUnchecked(Data, ptr, file.Data.Length); } } - public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists ) - => GetDefault( path.ToString(), slot, variantIdx, out exists ); + public static ImcEntry GetDefault(MetaFileManager manager, Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists) + => GetDefault(manager, path.ToString(), slot, variantIdx, out exists); - public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) + public static ImcEntry GetDefault(MetaFileManager manager, string path, EquipSlot slot, int variantIdx, out bool exists) { - var file = DalamudServices.SGameData.GetFile( path ); + var file = manager.GameData.GetFile(path); exists = false; - if( file == null ) - { + if (file == null) throw new Exception(); - } - fixed( byte* ptr = file.Data ) + fixed (byte* ptr = file.Data) { - var entry = VariantPtr( ptr, PartIndex( slot ), variantIdx ); - if( entry != null ) - { - exists = true; - return *entry; - } + var entry = VariantPtr(ptr, PartIndex(slot), variantIdx); + if (entry == null) + return new ImcEntry(); - return new ImcEntry(); + exists = true; + return *entry; } } - public void Replace( ResourceHandle* resource ) + public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( ActualLength, 8 ); - if( newData == null ) + var newData = Penumbra.MetaFileManager.AllocateDefaultMemory(ActualLength, 8); + if (newData == null) { - Penumbra.Log.Error( $"Could not replace loaded IMC data at 0x{( ulong )resource:X}, allocation failed." ); + Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - MemoryUtility.MemCpyUnchecked( newData, Data, ActualLength ); + MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); - Penumbra.MetaFileManager.Free( data, length ); - resource->SetData( ( IntPtr )newData, ActualLength ); + Penumbra.MetaFileManager.Free(data, length); + resource->SetData((IntPtr)newData, ActualLength); } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 4307a78d..46e87567 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -8,79 +8,78 @@ namespace Penumbra.Meta.Files; public unsafe class MetaBaseFile : IDisposable { - public byte* Data { get; private set; } - public int Length { get; private set; } - public CharacterUtility.InternalIndex Index { get; } + protected readonly MetaFileManager Manager; - public MetaBaseFile( MetaIndex idx ) - => Index = CharacterUtility.ReverseIndices[ ( int )idx ]; + public byte* Data { get; private set; } + public int Length { get; private set; } + public CharacterUtility.InternalIndex Index { get; } + + public MetaBaseFile(MetaFileManager manager, MetaIndex idx) + { + Manager = manager; + Index = CharacterUtility.ReverseIndices[(int)idx]; + } protected (IntPtr Data, int Length) DefaultData - => Penumbra.CharacterUtility.DefaultResource( Index ); + => Manager.CharacterUtility.DefaultResource(Index); - // Reset to default values. + /// Reset to default values. public virtual void Reset() { } - // Obtain memory. - protected void AllocateData( int length ) + /// Obtain memory. + protected void AllocateData(int length) { Length = length; - Data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( length ); - if( length > 0 ) - { - GC.AddMemoryPressure( length ); - } + Data = (byte*)Manager.AllocateFileMemory(length); + if (length > 0) + GC.AddMemoryPressure(length); } - // Free memory. + /// Free memory. protected void ReleaseUnmanagedResources() { - var ptr = ( IntPtr )Data; - MemoryHelper.GameFree( ref ptr, ( ulong )Length ); - if( Length > 0 ) - { - GC.RemoveMemoryPressure( Length ); - } + var ptr = (IntPtr)Data; + MemoryHelper.GameFree(ref ptr, (ulong)Length); + if (Length > 0) + GC.RemoveMemoryPressure(Length); Length = 0; Data = null; } - // Resize memory while retaining data. - protected void ResizeResources( int newLength ) + /// Resize memory while retaining data. + protected void ResizeResources(int newLength) { - if( newLength == Length ) - { + if (newLength == Length) return; - } - var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); - if( newLength > Length ) + var data = (byte*)Manager.AllocateFileMemory((ulong)newLength); + if (newLength > Length) { - MemoryUtility.MemCpyUnchecked( data, Data, Length ); - MemoryUtility.MemSet( data + Length, 0, newLength - Length ); + MemoryUtility.MemCpyUnchecked(data, Data, Length); + MemoryUtility.MemSet(data + Length, 0, newLength - Length); } else { - MemoryUtility.MemCpyUnchecked( data, Data, newLength ); + MemoryUtility.MemCpyUnchecked(data, Data, newLength); } ReleaseUnmanagedResources(); - GC.AddMemoryPressure( newLength ); + GC.AddMemoryPressure(newLength); Data = data; Length = newLength; } - // Manually free memory. + /// Manually free memory. public void Dispose() { ReleaseUnmanagedResources(); - GC.SuppressFinalize( this ); + GC.SuppressFinalize(this); } ~MetaBaseFile() { ReleaseUnmanagedResources(); } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs new file mode 100644 index 00000000..654760ca --- /dev/null +++ b/Penumbra/Meta/MetaFileManager.cs @@ -0,0 +1,85 @@ +using System; +using System.Runtime.CompilerServices; +using Dalamud.Data; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; + +namespace Penumbra.Meta; + +public unsafe class MetaFileManager +{ + internal readonly Configuration Config; + internal readonly CharacterUtility CharacterUtility; + internal readonly ResidentResourceManager ResidentResources; + internal readonly DataManager GameData; + internal readonly ActiveCollections ActiveCollections; + internal readonly ValidityChecker ValidityChecker; + + public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData, + ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker) + { + CharacterUtility = characterUtility; + ResidentResources = residentResources; + GameData = gameData; + ActiveCollections = activeCollections; + Config = config; + ValidityChecker = validityChecker; + SignatureHelper.Initialise(this); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) + { + if (file == null) + CharacterUtility.ResetResource(metaIndex); + else + CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public CharacterUtility.MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) + => file == null + ? CharacterUtility.TemporarilyResetResource(metaIndex) + : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length); + + public void ApplyDefaultFiles(ModCollection collection) + { + if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods) + return; + + ResidentResources.Reload(); + collection._cache?.MetaManipulations.SetFiles(); + } + + /// + /// Allocate in the games space for file storage. + /// We only need this if using any meta file. + /// + [Signature(Sigs.GetFileSpace)] + private readonly nint _getFileSpaceAddress = nint.Zero; + + public IMemorySpace* GetFileSpace() + => ((delegate* unmanaged)_getFileSpaceAddress)(); + + public void* AllocateFileMemory(ulong length, ulong alignment = 0) + => GetFileSpace()->Malloc(length, alignment); + + public void* AllocateFileMemory(int length, int alignment = 0) + => AllocateFileMemory((ulong)length, (ulong)alignment); + + public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) + => GetFileSpace()->Malloc(length, alignment); + + public void* AllocateDefaultMemory(int length, int alignment = 0) + => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); + + public void Free(nint ptr, int length) + => IMemorySpace.Free((void*)ptr, (ulong)length); +} diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index 9b346c00..a8aec374 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.String.Classes; namespace Penumbra.Mods.ItemSwap; @@ -12,7 +13,7 @@ namespace Penumbra.Mods.ItemSwap; public static class CustomizationSwap { /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. - public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) + public static FileSwap CreateMdl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) { if( idFrom.Value > byte.MaxValue ) { @@ -22,7 +23,7 @@ public static class CustomizationSwap var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() ); var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() ); - var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); + var mdl = FileSwap.CreateSwap( manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1; foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() ) @@ -31,7 +32,7 @@ public static class CustomizationSwap foreach( var variant in Enumerable.Range( 1, range ) ) { name = materialFileName; - var mtrl = CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged ); + var mtrl = CreateMtrl( manager, redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged ); mdl.ChildSwaps.Add( mtrl ); } @@ -41,7 +42,7 @@ public static class CustomizationSwap return mdl; } - public static FileSwap CreateMtrl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, + public static FileSwap CreateMtrl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, ref string fileName, ref bool dataWasChanged ) { variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; @@ -62,20 +63,20 @@ public static class CustomizationSwap dataWasChanged = true; } - var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath ); - var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); + var mtrl = FileSwap.CreateSwap( manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath ); + var shpk = CreateShader( manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( shpk ); foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) { - var tex = CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged ); + var tex = CreateTex( manager, redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged ); mtrl.ChildSwaps.Add( tex ); } return mtrl; } - public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, + public static FileSwap CreateTex( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, ref bool dataWasChanged ) { var path = texture.Path; @@ -99,13 +100,13 @@ public static class CustomizationSwap dataWasChanged = true; } - return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); + return FileSwap.CreateSwap( manager, ResourceType.Tex, redirections, newPath, path, path ); } - public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) + public static FileSwap CreateShader( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) { var path = $"shader/sm5/shpk/{shaderName}"; - return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); + return FileSwap.CreateSwap( manager, ResourceType.Shpk, redirections, path, path ); } } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 1f1cecfb..443376a6 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -18,41 +19,51 @@ namespace Penumbra.Mods.ItemSwap; public static class EquipmentSwap { - private static EquipSlot[] ConvertSlots( EquipSlot slot, bool rFinger, bool lFinger ) + private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger) { - if( slot != EquipSlot.RFinger ) - { - return new[] { slot }; - } + if (slot != EquipSlot.RFinger) + return new[] + { + slot, + }; return rFinger ? lFinger - ? new[] { EquipSlot.RFinger, EquipSlot.LFinger } - : new[] { EquipSlot.RFinger } + ? new[] + { + EquipSlot.RFinger, + EquipSlot.LFinger, + } + : new[] + { + EquipSlot.RFinger, + } : lFinger - ? new[] { EquipSlot.LFinger } - : Array.Empty< EquipSlot >(); + ? new[] + { + EquipSlot.LFinger, + } + : Array.Empty(); } - public static Item[] CreateTypeSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, - EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo ) + public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + Func redirections, Func manips, + EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo) { - LookupItem( itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom ); - LookupItem( itemTo, out var actualSlotTo, out var idTo, out var variantTo ); - if( actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot() ) - { + LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); - } - var ( imcFileFrom, variants, affectedItems ) = GetVariants( identifier, slotFrom, idFrom, idTo, variantFrom ); - var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default ); - var imcFileTo = new ImcFile( imcManip ); + var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); + var imcManip = new ImcManipulation(slotTo, variantTo, idTo.Value, default); + var imcFileTo = new ImcFile(manager, imcManip); var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ) ) ).Imc.Entry.MaterialId; - foreach( var gr in Enum.GetValues< GenderRace >() ) + var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo))).Imc.Entry.MaterialId; + foreach (var gr in Enum.GetValues()) { - switch( gr.Split().Item1 ) + switch (gr.Split().Item1) { case Gender.Male when skipMale: continue; case Gender.Female when skipFemale: continue; @@ -60,22 +71,18 @@ public static class EquipmentSwap case Gender.FemaleNpc when skipFemale: continue; } - if( CharacterUtilityData.EqdpIdx( gr, true ) < 0 ) - { + if (CharacterUtilityData.EqdpIdx(gr, true) < 0) continue; - } try { - var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo ); - if( eqdp != null ) - { - swaps.Add( eqdp ); - } + var eqdp = CreateEqdp(manager, redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo); + if (eqdp != null) + swaps.Add(eqdp); } - catch( ItemSwap.MissingFileException e ) + catch (ItemSwap.MissingFileException e) { - switch( gr ) + switch (gr) { case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: skipMale = true; @@ -88,59 +95,54 @@ public static class EquipmentSwap } } - foreach( var variant in variants ) + foreach (var variant in variants) { - var imc = CreateImc( redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); - swaps.Add( imc ); + var imc = CreateImc(manager, redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo); + swaps.Add(imc); } return affectedItems; } - public static Item[] CreateItemSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, - Item itemTo, bool rFinger = true, bool lFinger = true ) + public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + Func redirections, Func manips, Item itemFrom, + Item itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. - LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom ); - LookupItem( itemTo, out var slotTo, out var idTo, out var variantTo ); - if( slotFrom != slotTo ) - { + LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); - } - var eqp = CreateEqp( manips, slotFrom, idFrom, idTo ); - if( eqp != null ) - { - swaps.Add( eqp ); - } + var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo); + if (eqp != null) + swaps.Add(eqp); - var gmp = CreateGmp( manips, slotFrom, idFrom, idTo ); - if( gmp != null ) - { - swaps.Add( gmp ); - } + var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo); + if (gmp != null) + swaps.Add(gmp); - var affectedItems = Array.Empty< Item >(); - foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) + var affectedItems = Array.Empty(); + foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { - ( var imcFileFrom, var variants, affectedItems ) = GetVariants( identifier, slot, idFrom, idTo, variantFrom ); - var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); - var imcFileTo = new ImcFile( imcManip ); + (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); + var imcManip = new ImcManipulation(slot, variantTo, idTo.Value, default); + var imcFileTo = new ImcFile(manager, imcManip); var isAccessory = slot.IsAccessory(); var estType = slot switch { EquipSlot.Head => EstManipulation.EstType.Head, EquipSlot.Body => EstManipulation.EstType.Body, - _ => ( EstManipulation.EstType )0, + _ => (EstManipulation.EstType)0, }; var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ) ) ).Imc.Entry.MaterialId; - foreach( var gr in Enum.GetValues< GenderRace >() ) + var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId; + foreach (var gr in Enum.GetValues()) { - switch( gr.Split().Item1 ) + switch (gr.Split().Item1) { case Gender.Male when skipMale: continue; case Gender.Female when skipFemale: continue; @@ -148,30 +150,24 @@ public static class EquipmentSwap case Gender.FemaleNpc when skipFemale: continue; } - if( CharacterUtilityData.EqdpIdx( gr, isAccessory ) < 0 ) - { + if (CharacterUtilityData.EqdpIdx(gr, isAccessory) < 0) continue; - } try { - var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); - if( eqdp != null ) - { - swaps.Add( eqdp ); - } + var eqdp = CreateEqdp(manager, redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo); + if (eqdp != null) + swaps.Add(eqdp); - var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false; - var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); - if( est != null ) - { - swaps.Add( est ); - } + var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false; + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + if (est != null) + swaps.Add(est); } - catch( ItemSwap.MissingFileException e ) + catch (ItemSwap.MissingFileException e) { - switch( gr ) + switch (gr) { case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: skipMale = true; @@ -184,33 +180,38 @@ public static class EquipmentSwap } } - foreach( var variant in variants ) + foreach (var variant in variants) { - var imc = CreateImc( redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); - swaps.Add( imc ); + var imc = CreateImc(manager, redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo); + swaps.Add(imc); } } return affectedItems; } - public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, - SetId idTo, byte mtrlTo ) - => CreateEqdp( redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo ); - public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, - SetId idTo, byte mtrlTo ) + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + Func manips, EquipSlot slot, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo) + => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); + + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, + SetId idTo, byte mtrlTo) { var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotFrom.IsAccessory(), idFrom.Value ), slotFrom, gender, race, idFrom.Value ); - var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotTo.IsAccessory(), idTo.Value ), slotTo, gender, race, idTo.Value ); - var meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slotFrom ); - if( ownMdl ) + var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom.Value), slotFrom, gender, + race, idFrom.Value); + var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo.Value), slotTo, gender, race, + idTo.Value); + var meta = new MetaSwap(manips, eqdpFrom, eqdpTo); + var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom); + if (ownMdl) { - var mdl = CreateMdl( redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo ); - meta.ChildSwaps.Add( mdl ); + var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo); + meta.ChildSwaps.Add(mdl); } - else if( !ownMtrl && meta.SwapAppliedIsDefault ) + else if (!ownMtrl && meta.SwapAppliedIsDefault) { meta = null; } @@ -218,97 +219,98 @@ public static class EquipmentSwap return meta; } - public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) - => CreateMdl( redirections, slot, slot, gr, idFrom, idTo, mtrlTo ); + public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slot, GenderRace gr, + SetId idFrom, SetId idTo, byte mtrlTo) + => CreateMdl(manager, redirections, slot, slot, gr, idFrom, idTo, mtrlTo); - public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) + public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo) { - var mdlPathFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slotFrom ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slotFrom ); - var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idTo, gr, slotTo ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slotTo ); - var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); + var mdlPathFrom = slotFrom.IsAccessory() + ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) + : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); + var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); - foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) + foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { - var mtrl = CreateMtrl( redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); - if( mtrl != null ) - { - mdl.ChildSwaps.Add( mtrl ); - } + var mtrl = CreateMtrl(manager, redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged); + if (mtrl != null) + mdl.ChildSwaps.Add(mtrl); } return mdl; } - private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) + private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant) { - slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - if( !slot.IsEquipmentPiece() ) - { + slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); + if (!slot.IsEquipmentPiece()) throw new ItemSwap.InvalidItemTypeException(); - } - modelId = ( ( Quad )i.ModelMain ).A; - variant = ( byte )( ( Quad )i.ModelMain ).B; + modelId = ((Quad)i.ModelMain).A; + variant = (byte)((Quad)i.ModelMain).B; } - private static (ImcFile, byte[], Item[]) GetVariants( IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom ) + private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, + SetId idFrom, SetId idTo, byte variantFrom) { - var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default ); - var imc = new ImcFile( entry ); + var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default); + var imc = new ImcFile(manager, entry); Item[] items; byte[] variants; - if( idFrom.Value == idTo.Value ) + if (idFrom.Value == idTo.Value) { - items = identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); - variants = new[] { variantFrom }; + items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray(); + variants = new[] + { + variantFrom, + }; } else { - items = identifier.Identify( slotFrom.IsEquipment() - ? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) - : GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); - variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); + items = identifier.Identify(slotFrom.IsEquipment() + ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); + variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray(); } - return ( imc, variants, items ); + return (imc, variants, items); } - public static MetaSwap? CreateGmp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) + public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, SetId idFrom, + SetId idTo) { - if( slot is not EquipSlot.Head ) - { + if (slot is not EquipSlot.Head) return null; - } - var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value ); - var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value ); - return new MetaSwap( manips, manipFrom, manipTo ); + var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom.Value), idFrom.Value); + var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo.Value), idTo.Value); + return new MetaSwap(manips, manipFrom, manipTo); } - public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, - byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) - => CreateImc( redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo ); + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, EquipSlot slot, + SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); - public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, - byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, + EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, + byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { - var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slotFrom ), variantFrom ); - var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ); - var manipulationFrom = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, entryFrom ); - var manipulationTo = new ImcManipulation( slotTo, variantTo, idTo.Value, entryTo ); - var imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); + var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipulationFrom = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, entryFrom); + var manipulationTo = new ImcManipulation(slotTo, variantTo, idTo.Value, entryTo); + var imc = new MetaSwap(manips, manipulationFrom, manipulationTo); - var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId ); - if( decal != null ) - { - imc.ChildSwaps.Add( decal ); - } + var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId); + if (decal != null) + imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId ); - if( avfx != null ) - { - imc.ChildSwaps.Add( avfx ); - } + var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId); + if (avfx != null) + imc.ChildSwaps.Add(avfx); // IMC also controls sound, Example: Dodore Doublet, but unknown what it does? // IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. @@ -316,134 +318,135 @@ public static class EquipmentSwap } // Example: Crimson Standard Bracelet - public static FileSwap? CreateDecal( Func< Utf8GamePath, FullPath > redirections, byte decalId ) + public static FileSwap? CreateDecal(MetaFileManager manager, Func redirections, byte decalId) { - if( decalId == 0 ) - { + if (decalId == 0) return null; - } - var decalPath = GamePaths.Equipment.Decal.Path( decalId ); - return FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath ); + var decalPath = GamePaths.Equipment.Decal.Path(decalId); + return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath); } // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx( Func< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId ) + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, SetId idFrom, SetId idTo, byte vfxId) { - if( vfxId == 0 ) - { + if (vfxId == 0) return null; - } - var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId ); - var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId ); - var avfx = FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo ); + var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom.Value, vfxId); + var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo.Value, vfxId); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); - foreach( ref var filePath in avfx.AsAvfx()!.Textures.AsSpan() ) + foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { - var atex = CreateAtex( redirections, ref filePath, ref avfx.DataWasChanged ); - avfx.ChildSwaps.Add( atex ); + var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged); + avfx.ChildSwaps.Add(atex); } return avfx; } - public static MetaSwap? CreateEqp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) + public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, SetId idFrom, + SetId idTo) { - if( slot.IsAccessory() ) - { + if (slot.IsAccessory()) return null; - } - var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value ); - var eqpValueTo = ExpandedEqpFile.GetDefault( idTo.Value ); - var eqpFrom = new EqpManipulation( eqpValueFrom, slot, idFrom.Value ); - var eqpTo = new EqpManipulation( eqpValueTo, slot, idFrom.Value ); - return new MetaSwap( manips, eqpFrom, eqpTo ); + var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom.Value); + var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo.Value); + var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom.Value); + var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom.Value); + return new MetaSwap(manips, eqpFrom, eqpTo); } - public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, - ref bool dataWasChanged ) - => CreateMtrl( redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged ); + public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, SetId idFrom, + SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged) + => CreateMtrl(manager, redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged); - public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, - ref bool dataWasChanged ) + public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + ref bool dataWasChanged) { var prefix = slotTo.IsAccessory() ? 'a' : 'e'; - if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) - { + if (!fileName.Contains($"{prefix}{idTo.Value:D4}")) return null; - } - var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); - var pathTo = $"{folderTo}{fileName}"; + var folderTo = slotTo.IsAccessory() + ? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo) + : GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo); + var pathTo = $"{folderTo}{fileName}"; - var folderFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); - var newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom ); - newFileName = ItemSwap.ReplaceSlot( newFileName, slotTo, slotFrom, slotTo != slotFrom ); - var pathFrom = $"{folderFrom}{newFileName}"; + var folderFrom = slotFrom.IsAccessory() + ? GamePaths.Accessory.Mtrl.FolderPath(idFrom, variantTo) + : GamePaths.Equipment.Mtrl.FolderPath(idFrom, variantTo); + var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); + newFileName = ItemSwap.ReplaceSlot(newFileName, slotTo, slotFrom, slotTo != slotFrom); + var pathFrom = $"{folderFrom}{newFileName}"; - if( newFileName != fileName ) + if (newFileName != fileName) { fileName = newFileName; dataWasChanged = true; } - var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo ); - var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); - mtrl.ChildSwaps.Add( shpk ); + var mtrl = FileSwap.CreateSwap(manager, ResourceType.Mtrl, redirections, pathFrom, pathTo); + var shpk = CreateShader(manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged); + mtrl.ChildSwaps.Add(shpk); - foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) + foreach (ref var texture in mtrl.AsMtrl()!.Textures.AsSpan()) { - var tex = CreateTex( redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); - mtrl.ChildSwaps.Add( tex ); + var tex = CreateTex(manager, redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged); + mtrl.ChildSwaps.Add(tex); } return mtrl; } - public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) - => CreateTex( redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged ); + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, SetId idFrom, SetId idTo, + ref MtrlFile.Texture texture, ref bool dataWasChanged) + => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); - public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, + SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var path = texture.Path; var addedDashes = false; - if( texture.DX11 ) + if (texture.DX11) { - var fileName = Path.GetFileName( path ); - if( !fileName.StartsWith( "--" ) ) + var fileName = Path.GetFileName(path); + if (!fileName.StartsWith("--")) { - path = path.Replace( fileName, $"--{fileName}" ); + path = path.Replace(fileName, $"--{fileName}"); addedDashes = true; } } - var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); - newPath = ItemSwap.ReplaceSlot( newPath, slotTo, slotFrom, slotTo != slotFrom ); - newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" ); - if( newPath != path ) + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); + newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); + if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } - return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); + return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, newPath, path, path); } - public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) + public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, ref bool dataWasChanged) { var path = $"shader/sm5/shpk/{shaderName}"; - return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); + return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } - public static FileSwap CreateAtex( Func< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged ) + public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}" ); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); dataWasChanged = true; - return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, oldPath ); + return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index daefee5c..30159591 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -6,6 +6,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Services; @@ -27,7 +28,7 @@ public static class ItemSwap => Type = type; } - private static bool LoadFile( FullPath path, out byte[] data ) + private static bool LoadFile( MetaFileManager manager, FullPath path, out byte[] data ) { if( path.FullName.Length > 0 ) { @@ -39,7 +40,7 @@ public static class ItemSwap return true; } - var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() ); + var file = manager.GameData.GetFile( path.InternalName.ToString() ); if( file != null ) { data = file.Data; @@ -61,18 +62,18 @@ public static class ItemSwap public readonly byte[] Data; public bool Valid { get; } - public GenericFile( FullPath path ) - => Valid = LoadFile( path, out Data ); + public GenericFile( MetaFileManager manager, FullPath path ) + => Valid = LoadFile( manager, path, out Data ); public byte[] Write() => Data; - public static readonly GenericFile Invalid = new(FullPath.Empty); + public static readonly GenericFile Invalid = new(null!, FullPath.Empty); } - public static bool LoadFile( FullPath path, [NotNullWhen( true )] out GenericFile? file ) + public static bool LoadFile( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out GenericFile? file ) { - file = new GenericFile( path ); + file = new GenericFile( manager, path ); if( file.Valid ) { return true; @@ -82,11 +83,11 @@ public static class ItemSwap return false; } - public static bool LoadMdl( FullPath path, [NotNullWhen( true )] out MdlFile? file ) + public static bool LoadMdl( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MdlFile? file ) { try { - if( LoadFile( path, out byte[] data ) ) + if( LoadFile( manager, path, out byte[] data ) ) { file = new MdlFile( data ); return true; @@ -101,11 +102,11 @@ public static class ItemSwap return false; } - public static bool LoadMtrl( FullPath path, [NotNullWhen( true )] out MtrlFile? file ) + public static bool LoadMtrl(MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MtrlFile? file ) { try { - if( LoadFile( path, out byte[] data ) ) + if( LoadFile( manager, path, out byte[] data ) ) { file = new MtrlFile( data ); return true; @@ -120,11 +121,11 @@ public static class ItemSwap return false; } - public static bool LoadAvfx( FullPath path, [NotNullWhen( true )] out AvfxFile? file ) + public static bool LoadAvfx( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out AvfxFile? file ) { try { - if( LoadFile( path, out byte[] data ) ) + if( LoadFile( manager, path, out byte[] data ) ) { file = new AvfxFile( data ); return true; @@ -140,20 +141,20 @@ public static class ItemSwap } - public static FileSwap CreatePhyb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) + public static FileSwap CreatePhyb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) { var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath ); + return FileSwap.CreateSwap( manager, ResourceType.Phyb, redirections, phybPath, phybPath ); } - public static FileSwap CreateSklb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) + public static FileSwap CreateSklb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) { var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath ); + return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath ); } /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static MetaSwap? CreateEst( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, + public static MetaSwap? CreateEst( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl ) { if( type == 0 ) @@ -162,15 +163,15 @@ public static class ItemSwap } var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) ); - var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) ); + var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( manager, type, genderRace, idFrom.Value ) ); + var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( manager, type, genderRace, idTo.Value ) ); var est = new MetaSwap( manips, fromDefault, toDefault ); if( ownMdl && est.SwapApplied.Est.Entry >= 2 ) { - var phyb = CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); + var phyb = CreatePhyb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry ); est.ChildSwaps.Add( phyb ); - var sklb = CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); + var sklb = CreateSklb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry ); est.ChildSwaps.Add( sklb ); } else if( est.SwapAppliedIsDefault ) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 2ca70cda..09283dc7 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -9,12 +9,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Penumbra.GameData; +using Penumbra.Meta; using Penumbra.Mods.Manager; namespace Penumbra.Mods.ItemSwap; public class ItemSwapContainer { + private readonly MetaFileManager _manager; private readonly IObjectIdentifier _identifier; private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); @@ -112,8 +114,9 @@ public class ItemSwapContainer } } - public ItemSwapContainer(IObjectIdentifier identifier) + public ItemSwapContainer(MetaFileManager manager, IObjectIdentifier identifier) { + _manager = manager; _identifier = identifier; LoadMod( null, null ); } @@ -133,7 +136,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); + var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); Loaded = true; return ret; } @@ -142,15 +145,15 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateTypeSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); Loaded = true; return ret; } - public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) + public bool LoadCustomization( MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) { var pathResolver = PathResolver( collection ); - var mdl = CustomizationSwap.CreateMdl( pathResolver, slot, race, from, to ); + var mdl = CustomizationSwap.CreateMdl( manager, pathResolver, slot, race, from, to ); var type = slot switch { BodySlot.Hair => EstManipulation.EstType.Hair, @@ -159,7 +162,7 @@ public class ItemSwapContainer }; var metaResolver = MetaResolver( collection ); - var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to, true ); + var est = ItemSwap.CreateEst( manager, pathResolver, metaResolver, type, race, from, to, true ); Swaps.Add( mdl ); if( est != null ) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 8249c189..d689c2cf 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -7,18 +7,19 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using Penumbra.GameData.Enums; +using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; -using Penumbra.Services; - +using Penumbra.Services; + namespace Penumbra.Mods.ItemSwap; public class Swap { /// Any further swaps belonging specifically to this tree of changes. - public readonly List< Swap > ChildSwaps = new(); + public readonly List ChildSwaps = new(); - public IEnumerable< Swap > WithChildren() - => ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this ); + public IEnumerable WithChildren() + => ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this); } public sealed class MetaSwap : Swap @@ -47,15 +48,15 @@ public sealed class MetaSwap : Swap /// A function that converts the given manipulation to the modded one. /// The original meta identifier with its default value. /// The target meta identifier with its default value. - public MetaSwap( Func< MetaManipulation, MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo ) + public MetaSwap(Func manipulations, MetaManipulation manipFrom, MetaManipulation manipTo) { SwapFrom = manipFrom; SwapToDefault = manipTo; - SwapToModded = manipulations( manipTo ); - SwapToIsDefault = manipTo.EntryEquals( SwapToModded ); - SwapApplied = SwapFrom.WithEntryOf( SwapToModded ); - SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom ); + SwapToModded = manipulations(manipTo); + SwapToIsDefault = manipTo.EntryEquals(SwapToModded); + SwapApplied = SwapFrom.WithEntryOf(SwapToModded); + SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom); } } @@ -95,8 +96,8 @@ public sealed class FileSwap : Swap /// Whether SwapFromPreChangePath equals SwapFromRequest. public bool SwapFromChanged; - public string GetNewPath( string newMod ) - => Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() ); + public string GetNewPath(string newMod) + => Path.Combine(newMod, new Utf8RelPath(SwapFromRequestPath).ToString()); public MdlFile? AsMdl() => FileData as MdlFile; @@ -116,8 +117,9 @@ public sealed class FileSwap : Swap /// The unmodded path to the file the game is supposed to load instead. /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. - public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, - string? swapFromPreChange = null ) + public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func redirections, + string swapFromRequest, string swapToRequest, + string? swapFromPreChange = null) { var swap = new FileSwap { @@ -131,49 +133,25 @@ public sealed class FileSwap : Swap SwapToModded = FullPath.Empty, }; - if( swapFromRequest.Length == 0 - || swapToRequest.Length == 0 - || !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath ) - || !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) ) - { - throw new Exception( $"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"." ); - } + if (swapFromRequest.Length == 0 + || swapToRequest.Length == 0 + || !Utf8GamePath.FromString(swapToRequest, out swap.SwapToRequestPath) + || !Utf8GamePath.FromString(swapFromRequest, out swap.SwapFromRequestPath)) + throw new Exception($"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"."); - swap.SwapToModded = redirections( swap.SwapToRequestPath ); - swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() ); - swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); + swap.SwapToModded = redirections(swap.SwapToRequestPath); + swap.SwapToModdedExistsInGame = + !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists(swap.SwapToModded.InternalName.ToString()); + swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals(swap.SwapFromRequestPath.Path); swap.FileData = type switch { - ResourceType.Mdl => LoadMdl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), - ResourceType.Mtrl => LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), - ResourceType.Avfx => LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), - _ => LoadFile( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), + ResourceType.Mdl => LoadMdl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded), + ResourceType.Mtrl => LoadMtrl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded), + ResourceType.Avfx => LoadAvfx(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded), + _ => LoadFile(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded), }; return swap; } - - - /// - /// Convert a single file redirection to use the file name and extension given by type and the files SHA256 hash, if possible. - /// - /// The set of redirections that need to be considered. - /// The in- and output path for a file - /// Will be set to true if was changed. - /// Will be updated. - public static bool CreateShaRedirection( Func< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap ) - { - var oldFilename = Path.GetFileName( path ); - var hash = SHA256.HashData( swap.FileData.Write() ); - var name = - $"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}"; - var newPath = path.Replace( oldFilename, name ); - var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString() ); - - path = newPath; - dataWasChanged = true; - swap = newSwap; - return true; - } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 8326b4fb..8820dd7b 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -5,6 +5,7 @@ using System.Linq; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -109,11 +110,11 @@ public partial class Mod } } - public void WriteAllTexToolsMeta() + public void WriteAllTexToolsMeta(MetaFileManager manager) { try { - _default.WriteTexToolsMeta(ModPath); + _default.WriteTexToolsMeta(manager, ModPath); foreach (var group in Groups) { var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); @@ -126,7 +127,7 @@ public partial class Mod if (!optionDir.Exists) optionDir.Create(); - option.WriteTexToolsMeta(optionDir); + option.WriteTexToolsMeta(manager, optionDir); } } } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 8b5ba641..459d5ffb 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -5,12 +5,13 @@ using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using Penumbra.Import; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; namespace Penumbra.Mods; -/// +/// /// A sub mod is a collection of /// - file replacements /// - file swaps @@ -18,14 +19,14 @@ namespace Penumbra.Mods; /// that can be used either as an option or as the default data for a mod. /// It can be loaded and reloaded from Json. /// Nothing is checked for existence or validity when loading. -/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. +/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// - public sealed class SubMod : ISubMod +public sealed class SubMod : ISubMod { public string Name { get; set; } = "Default"; public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}"; public string Description { get; set; } = string.Empty; @@ -36,180 +37,165 @@ namespace Penumbra.Mods; public bool IsDefault => GroupIdx < 0; - public Dictionary< Utf8GamePath, FullPath > FileData = new(); - public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - public HashSet< MetaManipulation > ManipulationData = new(); + public Dictionary FileData = new(); + public Dictionary FileSwapData = new(); + public HashSet ManipulationData = new(); - public SubMod( IMod parentMod ) + public SubMod(IMod parentMod) => ParentMod = parentMod; - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + public IReadOnlyDictionary Files => FileData; - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + public IReadOnlyDictionary FileSwaps => FileSwapData; - public IReadOnlySet< MetaManipulation > Manipulations + public IReadOnlySet Manipulations => ManipulationData; - public void SetPosition( int groupIdx, int optionIdx ) + public void SetPosition(int groupIdx, int optionIdx) { GroupIdx = groupIdx; OptionIdx = optionIdx; } - public void Load( DirectoryInfo basePath, JToken json, out int priority ) + public void Load(DirectoryInfo basePath, JToken json, out int priority) { FileData.Clear(); FileSwapData.Clear(); ManipulationData.Clear(); // Every option has a name, but priorities are only relevant for multi group options. - Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; - Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; - priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; + Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; + Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; + priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? 0; - var files = ( JObject? )json[ nameof( Files ) ]; - if( files != null ) - { - foreach( var property in files.Properties() ) + var files = (JObject?)json[nameof(Files)]; + if (files != null) + foreach (var property in files.Properties()) { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); - } + if (Utf8GamePath.FromString(property.Name, out var p, true)) + FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } - } - var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; - if( swaps != null ) - { - foreach( var property in swaps.Properties() ) + var swaps = (JObject?)json[nameof(FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); - } + if (Utf8GamePath.FromString(property.Name, out var p, true)) + FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); } - } - var manips = json[ nameof( Manipulations ) ]; - if( manips != null ) - { - foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) - { - ManipulationData.Add( s ); - } - } + var manips = json[nameof(Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) + ManipulationData.Add(s); } // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. // If delete is true, the files are deleted afterwards. - public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + public (bool Changes, List DeleteList) IncorporateMetaChanges(DirectoryInfo basePath, bool delete) { - var deleteList = new List< string >(); + var deleteList = new List(); var oldSize = ManipulationData.Count; var deleteString = delete ? "with deletion." : "without deletion."; - foreach( var (key, file) in Files.ToList() ) + foreach (var (key, file) in Files.ToList()) { var ext1 = key.Extension().AsciiToLower().ToString(); var ext2 = file.Extension.ToLowerInvariant(); try { - if( ext1 == ".meta" || ext2 == ".meta" ) + if (ext1 == ".meta" || ext2 == ".meta") { - FileData.Remove( key ); - if( !file.Exists ) - { + FileData.Remove(key); + if (!file.Exists) continue; - } - var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); - ManipulationData.UnionWith( meta.MetaManipulations ); + var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName), + Penumbra.Config.KeepDefaultMetaChanges); + Penumbra.Log.Verbose( + $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); + deleteList.Add(file.FullName); + ManipulationData.UnionWith(meta.MetaManipulations); } - else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) + else if (ext1 == ".rgsp" || ext2 == ".rgsp") { - FileData.Remove( key ); - if( !file.Exists ) - { + FileData.Remove(key); + if (!file.Exists) continue; - } - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); + var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName), + Penumbra.Config.KeepDefaultMetaChanges); + Penumbra.Log.Verbose( + $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); + deleteList.Add(file.FullName); - ManipulationData.UnionWith( rgsp.MetaManipulations ); + ManipulationData.UnionWith(rgsp.MetaManipulations); } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}"); } } - DeleteDeleteList( deleteList, delete ); - return ( oldSize < ManipulationData.Count, deleteList ); + DeleteDeleteList(deleteList, delete); + return (oldSize < ManipulationData.Count, deleteList); } - internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) { - if( !delete ) - { + if (!delete) return; - } - foreach( var file in deleteList ) + foreach (var file in deleteList) { try { - File.Delete( file ); + File.Delete(file); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); } } } - public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false) { - var files = TexToolsMeta.ConvertToTexTools( Manipulations ); + var files = TexToolsMeta.ConvertToTexTools(manager, Manipulations); - foreach( var (file, data) in files ) + foreach (var (file, data) in files) { - var path = Path.Combine( basePath.FullName, file ); + var path = Path.Combine(basePath.FullName, file); try { - Directory.CreateDirectory( Path.GetDirectoryName( path )! ); - File.WriteAllBytes( path, data ); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllBytes(path, data); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); + Penumbra.Log.Error($"Could not write meta file {path}:\n{e}"); } } - if( test ) - { - TestMetaWriting( files ); - } + if (test) + TestMetaWriting(manager, files); } - [Conditional("DEBUG" )] - private void TestMetaWriting( Dictionary< string, byte[] > files ) + [Conditional("DEBUG")] + private void TestMetaWriting(MetaFileManager manager, Dictionary files) { - var meta = new HashSet< MetaManipulation >( Manipulations.Count ); - foreach( var (file, data) in files ) + var meta = new HashSet(Manipulations.Count); + foreach (var (file, data) in files) { try { - var x = file.EndsWith( "rgsp" ) - ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) - : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); - meta.UnionWith( x.MetaManipulations ); + var x = file.EndsWith("rgsp") + ? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges) + : new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges); + meta.UnionWith(x.MetaManipulations); } catch { @@ -217,27 +203,21 @@ namespace Penumbra.Mods; } } - if( !Manipulations.SetEquals( meta ) ) + if (!Manipulations.SetEquals(meta)) { - Penumbra.Log.Information( "Meta Sets do not equal." ); - foreach( var (m1, m2) in Manipulations.Zip( meta ) ) - { - Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); - } + Penumbra.Log.Information("Meta Sets do not equal."); + foreach (var (m1, m2) in Manipulations.Zip(meta)) + Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}"); - foreach( var m in Manipulations.Skip( meta.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } + foreach (var m in Manipulations.Skip(meta.Count)) + Penumbra.Log.Information($"{m} {m.EntryToString()} "); - foreach( var m in meta.Skip( Manipulations.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } + foreach (var m in meta.Skip(Manipulations.Count)) + Penumbra.Log.Information($"{m} {m.EntryToString()} "); } else { - Penumbra.Log.Information( "Meta Sets are equal." ); + Penumbra.Log.Information("Meta Sets are equal."); } } -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 24f13749..150fd9c9 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -28,7 +28,8 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods; - +using Penumbra.Meta; + namespace Penumbra; public class Penumbra : IDalamudPlugin diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index d1227472..94305567 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -23,6 +23,7 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using Penumbra.Mods.Manager; using Penumbra.Collections.Cache; +using Penumbra.Meta; namespace Penumbra; @@ -66,7 +67,6 @@ public class PenumbraNew // Add Game Services services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -78,10 +78,6 @@ public class PenumbraNew .AddSingleton() .AddSingleton(); - // Add PathResolver - services.AddSingleton() - .AddSingleton(); - // Add Configuration services.AddTransient() .AddSingleton(); @@ -109,7 +105,8 @@ public class PenumbraNew // Add Resource services services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Add Path Resolver services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 1721480b..7f2966ff 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -12,9 +12,10 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Collections.Manager; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; @@ -25,21 +26,23 @@ namespace Penumbra.UI.AdvancedWindow; public class ItemSwapTab : IDisposable, ITab { - private readonly CommunicatorService _communicator; - private readonly ItemService _itemService; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; - private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly ItemService _itemService; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; + private readonly Configuration _config; + private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, Configuration config, IdentifierService identifier) + ModManager modManager, Configuration config, IdentifierService identifier, MetaFileManager metaFileManager) { _communicator = communicator; _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; _config = config; - _swapData = new ItemSwapContainer(identifier.AwaitedService); + _metaFileManager = metaFileManager; + _swapData = new ItemSwapContainer(metaFileManager, identifier.AwaitedService); _selectors = new Dictionary { @@ -215,22 +218,22 @@ public class ItemSwapTab : IDisposable, ITab _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; @@ -312,7 +315,8 @@ public class ItemSwapTab : IDisposable, ITab optionCreated = true; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, + if (!_swapData.WriteMod(_modManager, _mod, + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) throw new Exception("Failure writing files for mod swap."); @@ -751,7 +755,6 @@ public class ItemSwapTab : IDisposable, ITab private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) { - if (collection != _collectionManager.Active.Current || mod != _mod) return; @@ -762,7 +765,7 @@ public class ItemSwapTab : IDisposable, ITab private void OnInheritanceChange(ModCollection collection, bool _) { if (collection != _collectionManager.Active.Current || _mod == null) - return; + return; UpdateMod(_mod, collection[_mod.Index].Settings); _swapData.LoadMod(_mod, _modSettings); @@ -772,8 +775,9 @@ public class ItemSwapTab : IDisposable, ITab private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) { if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod) - return; - _swapData.LoadMod(_mod, _modSettings); + return; + + _swapData.LoadMod(_mod, _modSettings); UpdateOption(); _dirty = true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 242c49dc..1b92463e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -9,6 +9,7 @@ using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -62,7 +63,7 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) - _mod!.WriteAllTexToolsMeta(); + _mod!.WriteAllTexToolsMeta(_metaFileManager); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) @@ -77,9 +78,9 @@ public partial class ModEditWindow } - // The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, Action draw, - Action drawNew) + /// The headers for the different meta changes all have basically the same structure for different types. + private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, Action draw, + Action drawNew) { const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; if (!ImGui.CollapsingHeader($"{items.Count} {label}")) @@ -89,11 +90,11 @@ public partial class ModEditWindow { if (table) { - drawNew(_editor, _iconSize); + drawNew(_metaFileManager, _editor, _iconSize); foreach (var (item, index) in items.ToArray().WithIndex()) { using var id = ImRaii.PushId(index); - draw(item, _editor, _iconSize); + draw(_metaFileManager, item, _editor, _iconSize); } } } @@ -108,7 +109,7 @@ public partial class ModEditWindow private static float IdWidth => 100 * UiHelpers.Scale; - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, @@ -116,20 +117,20 @@ public partial class ModEditWindow ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedEqpFile.GetDefault(_new.SetId); + var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); // Identifier ImGui.TableNextColumn(); if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), _new.Slot, setId); + _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId); ImGuiUtil.HoverTooltip(ModelSetIdTooltip); ImGui.TableNextColumn(); if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), slot, _new.SetId); + _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -148,7 +149,7 @@ public partial class ModEditWindow ImGui.NewLine(); } - public static void Draw(EqpManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -157,7 +158,7 @@ public partial class ModEditWindow ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.TextUnformatted(meta.SetId.ToString()); ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId); + var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId); ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); @@ -192,7 +193,7 @@ public partial class ModEditWindow private static float IdWidth => 100 * UiHelpers.Scale; - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, @@ -204,7 +205,7 @@ public partial class ModEditWindow var tt = canAdd ? "Stage this edit." : validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; var defaultEntry = validRaceCode - ? ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) + ? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) : 0; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -213,7 +214,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); } @@ -222,7 +223,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Race("##eqdpRace", _new.Race, out var race)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); } @@ -231,7 +232,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); } @@ -240,7 +241,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); } @@ -255,7 +256,7 @@ public partial class ModEditWindow Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); } - public static void Draw(EqdpManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -278,7 +279,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(EquipSlotTooltip); // Values - var defaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); + var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); ImGui.TableNextColumn(); @@ -301,12 +302,12 @@ public partial class ModEditWindow private static float SmallIdWidth => 45 * UiHelpers.Scale; - // Convert throwing to null-return if the file does not exist. - private static ImcEntry? GetDefault(ImcManipulation imc) + /// Convert throwing to null-return if the file does not exist. + private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc) { try { - return ImcFile.GetDefault(imc.GamePath(), imc.EquipSlot, imc.Variant, out _); + return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _); } catch { @@ -314,13 +315,13 @@ public partial class ModEditWindow } } - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(_new); + var defaultEntry = GetDefault(metaFileManager, _new); var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; defaultEntry ??= new ImcEntry(); @@ -347,7 +348,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry) - .Copy(GetDefault(_new) + .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGuiUtil.HoverTooltip(PrimaryIdTooltip); @@ -361,7 +362,7 @@ public partial class ModEditWindow { if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) - .Copy(GetDefault(_new) + .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -370,7 +371,7 @@ public partial class ModEditWindow { if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) - .Copy(GetDefault(_new) + .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -379,7 +380,7 @@ public partial class ModEditWindow { if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry) - .Copy(GetDefault(_new) + .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGuiUtil.HoverTooltip(SecondaryIdTooltip); @@ -388,7 +389,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, - _new.Entry).Copy(GetDefault(_new) + _new.Entry).Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGui.TableNextColumn(); @@ -396,7 +397,7 @@ public partial class ModEditWindow { if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) - .Copy(GetDefault(_new) + .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -438,7 +439,7 @@ public partial class ModEditWindow ImGui.NewLine(); } - public static void Draw(ImcManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -479,7 +480,7 @@ public partial class ModEditWindow using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(meta) ?? new ImcEntry(); + var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f)) editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId })); @@ -530,7 +531,7 @@ public partial class ModEditWindow private static float IdWidth => 100 * UiHelpers.Scale; - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, @@ -538,7 +539,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); + var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -546,7 +547,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) { - var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); + var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); } @@ -555,7 +556,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Race("##estRace", _new.Race, out var race)) { - var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); + var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); } @@ -564,7 +565,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Gender("##estGender", _new.Gender, out var gender)) { - var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); + var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); } @@ -573,7 +574,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) { - var newDefaultEntry = EstFile.GetDefault(slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); + var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); } @@ -585,7 +586,7 @@ public partial class ModEditWindow IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); } - public static void Draw(EstManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -608,7 +609,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(EstTypeTooltip); // Values - var defaultEntry = EstFile.GetDefault(meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); + var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); ImGui.TableNextColumn(); if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, out var entry, 0, ushort.MaxValue, 0.05f)) @@ -629,7 +630,7 @@ public partial class ModEditWindow private static float IdWidth => 100 * UiHelpers.Scale; - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, @@ -637,14 +638,14 @@ public partial class ModEditWindow ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedGmpFile.GetDefault(_new.SetId); + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); // Identifier ImGui.TableNextColumn(); if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new GmpManipulation(ExpandedGmpFile.GetDefault(setId), setId); + _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId); ImGuiUtil.HoverTooltip(ModelSetIdTooltip); @@ -669,7 +670,7 @@ public partial class ModEditWindow IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); } - public static void Draw(GmpManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -680,7 +681,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); // Values - var defaultEntry = ExpandedGmpFile.GetDefault(meta.SetId); + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId); ImGui.TableNextColumn(); if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); @@ -723,7 +724,7 @@ public partial class ModEditWindow private static float FloatWidth => 150 * UiHelpers.Scale; - public static void DrawNew(ModEditor editor, Vector2 iconSize) + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, @@ -731,20 +732,20 @@ public partial class ModEditWindow ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = CmpFile.GetDefault(_new.SubRace, _new.Attribute); + var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); // Identifier ImGui.TableNextColumn(); if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) - _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(subRace, _new.Attribute)); + _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute)); ImGuiUtil.HoverTooltip(RacialTribeTooltip); ImGui.TableNextColumn(); if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) - _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(subRace, attribute)); + _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute)); ImGuiUtil.HoverTooltip(ScalingTypeTooltip); @@ -755,7 +756,7 @@ public partial class ModEditWindow ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); } - public static void Draw(RspManipulation meta, ModEditor editor, Vector2 iconSize) + public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) { DrawMetaButtons(meta, editor, iconSize); @@ -771,7 +772,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Values - var def = CmpFile.GetDefault(meta.SubRace, meta.Attribute); + var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute); var value = meta.Entry; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 68265a43..31fc2a49 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.String.Classes; @@ -31,6 +32,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly Configuration _config; private readonly ItemSwapTab _itemSwapTab; private readonly DataManager _gameData; + private readonly MetaFileManager _metaFileManager; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -493,16 +495,17 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _modCaches = modCaches; - _gameData = gameData; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _modCaches = modCaches; + _metaFileManager = metaFileManager; + _gameData = gameData; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); From 4972dd1c9fef4155d4769c47d7bd33bb93254d9c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Apr 2023 09:35:54 +0200 Subject: [PATCH 0882/2451] Untangling the mods. --- Penumbra/Api/HttpApi.cs | 4 +- Penumbra/Api/IpcTester.cs | 39 +- Penumbra/Api/PenumbraIpcProviders.cs | 8 +- Penumbra/Collections/Cache/CollectionCache.cs | 46 +- .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 5 +- .../Manager/IndividualCollections.Access.cs | 137 +++-- .../Manager/IndividualCollections.cs | 17 +- .../Manager/TempCollectionManager.cs | 4 +- .../Collections/ModCollection.Cache.Access.cs | 18 +- Penumbra/CommandHandler.cs | 6 +- Penumbra/Import/Structs/TexToolsStructs.cs | 12 +- Penumbra/Import/TexToolsImporter.Archives.cs | 14 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 10 +- Penumbra/Import/TexToolsMeta.Export.cs | 21 +- Penumbra/Meta/MetaFileManager.cs | 7 +- Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 4 +- Penumbra/Mods/Manager/ModDataEditor.cs | 4 +- Penumbra/Mods/Manager/ModFileSystem.cs | 14 +- Penumbra/Mods/Manager/ModManager.cs | 15 +- Penumbra/Mods/Manager/ModMigration.cs | 42 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 46 +- Penumbra/Mods/Mod.BasePath.cs | 72 +-- Penumbra/Mods/Mod.Creator.cs | 283 ----------- Penumbra/Mods/Mod.Files.cs | 139 ----- Penumbra/Mods/Mod.cs | 59 +++ Penumbra/Mods/ModCreator.cs | 476 ++++++++++++++++++ Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 110 ---- Penumbra/Mods/TemporaryMod.cs | 27 +- Penumbra/Penumbra.cs | 84 ++-- Penumbra/PenumbraNew.cs | 1 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 19 +- .../ModEditWindow.Materials.ColorSet.cs | 24 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 23 +- Penumbra/UI/ConfigWindow.cs | 8 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 12 +- 39 files changed, 883 insertions(+), 935 deletions(-) delete mode 100644 Penumbra/Mods/Mod.Creator.cs delete mode 100644 Penumbra/Mods/Mod.Files.cs create mode 100644 Penumbra/Mods/ModCreator.cs diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index e6d31104..0d9ef997 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -26,10 +26,10 @@ public class HttpApi : IDisposable private readonly IPenumbraApi _api; private WebServer? _server; - public HttpApi(IPenumbraApi api) + public HttpApi(Configuration config, IPenumbraApi api) { _api = api; - if (Penumbra.Config.EnableHttpApi) + if (config.EnableHttpApi) CreateWebServer(); } diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index eb9dbe07..32366bed 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -19,6 +19,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; +using Penumbra.Util; namespace Penumbra.Api; @@ -39,7 +40,8 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Temporary _temporary; - public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager) + public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, + TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) { _ipcProviders = ipcProviders; _pluginState = new PluginState(pi); @@ -52,7 +54,7 @@ public class IpcTester : IDisposable _meta = new Meta(pi); _mods = new Mods(pi); _modSettings = new ModSettings(pi); - _temporary = new Temporary(pi, modManager); + _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService); UnsubscribeEvents(); } @@ -1151,11 +1153,20 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; private readonly ModManager _modManager; + private readonly CollectionManager _collections; + private readonly TempModManager _tempMods; + private readonly TempCollectionManager _tempCollections; + private readonly SaveService _saveService; - public Temporary(DalamudPluginInterface pi, ModManager modManager) + public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, + TempCollectionManager tempCollections, SaveService saveService) { - _pi = pi; - _modManager = modManager; + _pi = pi; + _modManager = modManager; + _collections = collections; + _tempMods = tempMods; + _tempCollections = tempCollections; + _saveService = saveService; } public string LastCreatedCollectionName = string.Empty; @@ -1223,7 +1234,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...", - !Penumbra.CollectionManager.Storage.ByName(_tempModName, out var copyCollection)) + !_collections.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); @@ -1249,7 +1260,7 @@ public class IpcTester : IDisposable public void DrawCollections() { - using var collTree = ImRaii.TreeNode("Collections##TempCollections"); + using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); if (!collTree) return; @@ -1257,26 +1268,26 @@ public class IpcTester : IDisposable if (!table) return; - foreach (var collection in Penumbra.TempCollections.Values) + foreach (var collection in _tempCollections.Values) { ImGui.TableNextColumn(); - var character = Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) + var character = _tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) .FirstOrDefault() ?? "Unknown"; if (ImGui.Button($"Save##{collection.Name}")) - TemporaryMod.SaveTempCollection(_modManager, collection, character); + TemporaryMod.SaveTempCollection(_saveService, _modManager, collection, character); ImGuiUtil.DrawTableColumn(collection.Name); ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); ImGuiUtil.DrawTableColumn(string.Join(", ", - Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); + _tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); } } public void DrawMods() { - using var modTree = ImRaii.TreeNode("Mods##TempMods"); + using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods"); if (!modTree) return; @@ -1314,8 +1325,8 @@ public class IpcTester : IDisposable if (table) { - PrintList("All", Penumbra.TempMods.ModsForAllCollections); - foreach (var (collection, list) in Penumbra.TempMods.Mods) + PrintList("All", _tempMods.ModsForAllCollections); + foreach (var (collection, list) in _tempMods.Mods) PrintList(collection.Name, list); } } diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 1457b3a6..36245110 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -5,8 +5,9 @@ using System; using System.Collections.Generic; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; -using Penumbra.Mods; +using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; +using Penumbra.Util; namespace Penumbra.Api; @@ -114,7 +115,8 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryModAll; internal readonly FuncProvider RemoveTemporaryMod; - public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager) + public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, + TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) { Api = api; @@ -226,7 +228,7 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - Tester = new IpcTester(pi, this, modManager); + Tester = new IpcTester(pi, this, modManager, collections, tempMods, tempCollections, saveService); Initialized.Invoke(); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index d630ad31..e62fd1b9 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -26,7 +26,7 @@ public class CollectionCache : IDisposable private readonly ModCollection _collection; public readonly SortedList, object?)> _changedItems = new(); public readonly Dictionary ResolvedFiles = new(); - public readonly MetaCache MetaManipulations; + public readonly MetaCache Meta; public readonly Dictionary> _conflicts = new(); public IEnumerable> AllConflicts @@ -50,18 +50,18 @@ public class CollectionCache : IDisposable // The cache reacts through events on its collection changing. public CollectionCache(CollectionCacheManager manager, ModCollection collection) { - _manager = manager; - _collection = collection; - MetaManipulations = new MetaCache(manager.MetaFileManager, _collection); + _manager = manager; + _collection = collection; + Meta = new MetaCache(manager.MetaFileManager, _collection); } public void Dispose() { - MetaManipulations.Dispose(); + Meta.Dispose(); } ~CollectionCache() - => MetaManipulations.Dispose(); + => Meta.Dispose(); // Resolve a given game path according to this collection. public FullPath? ResolvePath(Utf8GamePath gameResourcePath) @@ -119,16 +119,16 @@ public class CollectionCache : IDisposable return ret; } - /// Force a file to be resolved to a specific path regardless of conflicts. + /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile(Utf8GamePath path, FullPath fullPath) { if (CheckFullPath(path, fullPath)) ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath); } - /// Force a file resolve to be removed. + /// Force a file resolve to be removed. internal void RemoveFile(Utf8GamePath path) - => ResolvedFiles.Remove(path); + => ResolvedFiles.Remove(path); public void ReloadMod(IMod mod, bool addMetaChanges) { @@ -151,8 +151,8 @@ public class CollectionCache : IDisposable foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations)) { - if (MetaManipulations.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod) - MetaManipulations.RevertMod(manipulation); + if (Meta.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod) + Meta.RevertMod(manipulation); } _conflicts.Remove(mod); @@ -175,11 +175,7 @@ public class CollectionCache : IDisposable if (addMetaChanges) { ++_collection.ChangeCounter; - if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } + _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -225,11 +221,7 @@ public class CollectionCache : IDisposable if ((mod is TemporaryMod temp ? temp.TotalManipulations : Penumbra.ModCaches[mod.Index].TotalManipulations) > 0) AddMetaFiles(); - if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } + _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -335,9 +327,9 @@ public class CollectionCache : IDisposable // Inside the same mod, conflicts are not recorded. private void AddManipulation(MetaManipulation manip, IMod mod) { - if (!MetaManipulations.TryGetValue(manip, out var existingMod)) + if (!Meta.TryGetValue(manip, out var existingMod)) { - MetaManipulations.ApplyMod(manip, mod); + Meta.ApplyMod(manip, mod); return; } @@ -346,13 +338,13 @@ public class CollectionCache : IDisposable return; if (AddConflict(manip, mod, existingMod)) - MetaManipulations.ApplyMod(manip, mod); + Meta.ApplyMod(manip, mod); } // Add all necessary meta file redirects. public void AddMetaFiles() - => MetaManipulations.SetImcFiles(); + => Meta.SetImcFiles(); // Identify and record all manipulated objects for this entire collection. @@ -367,7 +359,7 @@ public class CollectionCache : IDisposable _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. - var identifier = Penumbra.Identifier; + var identifier = _manager.MetaFileManager.Identifier.AwaitedService; var items = new SortedList(512); void AddItems(IMod mod) @@ -391,7 +383,7 @@ public class CollectionCache : IDisposable AddItems(modPath.Mod); } - foreach (var (manip, mod) in MetaManipulations) + foreach (var (manip, mod) in Meta) { ModCacheManager.ComputeChangedItems(identifier, items, manip); AddItems(mod); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 467d0617..4e3bb0cf 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -112,7 +112,7 @@ public class CollectionCacheManager : IDisposable private void FullRecalculation(ModCollection collection, CollectionCache cache) { cache.ResolvedFiles.Clear(); - cache.MetaManipulations.Reset(); + cache.Meta.Reset(); cache._conflicts.Clear(); // Add all forced redirects. diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 8fdeaa08..1e083d90 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.UI; using Penumbra.Util; -using static OtterGui.Raii.ImRaii; namespace Penumbra.Collections.Manager; @@ -22,7 +21,7 @@ public class ActiveCollections : ISavable, IDisposable private readonly CommunicatorService _communicator; private readonly SaveService _saveService; - public ActiveCollections(CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) + public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) { _storage = storage; _communicator = communicator; @@ -30,7 +29,7 @@ public class ActiveCollections : ISavable, IDisposable Current = storage.DefaultNamed; Default = storage.DefaultNamed; Interface = storage.DefaultNamed; - Individuals = new IndividualCollections(actors.AwaitedService); + Individuals = new IndividualCollections(actors.AwaitedService, config); _communicator.CollectionChange.Subscribe(OnCollectionChange); LoadCollections(); UpdateCurrentCollectionInUse(); diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 32e7fd17..b81e72c1 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -9,10 +9,10 @@ using Penumbra.String; namespace Penumbra.Collections.Manager; -public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +public sealed partial class IndividualCollections : IReadOnlyList<(string DisplayName, ModCollection Collection)> { - public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() - => _assignments.Select( t => ( t.DisplayName, t.Collection ) ).GetEnumerator(); + public IEnumerator<(string DisplayName, ModCollection Collection)> GetEnumerator() + => _assignments.Select(t => (t.DisplayName, t.Collection)).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -20,59 +20,51 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public int Count => _assignments.Count; - public (string DisplayName, ModCollection Collection) this[ int index ] - => ( _assignments[ index ].DisplayName, _assignments[ index ].Collection ); + public (string DisplayName, ModCollection Collection) this[int index] + => (_assignments[index].DisplayName, _assignments[index].Collection); - public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) + public bool TryGetCollection(ActorIdentifier identifier, [NotNullWhen(true)] out ModCollection? collection) { - if( Count == 0 ) + if (Count == 0) { collection = null; return false; } - switch( identifier.Type ) + switch (identifier.Type) { - case IdentifierType.Player: return CheckWorlds( identifier, out collection ); + case IdentifierType.Player: return CheckWorlds(identifier, out collection); case IdentifierType.Retainer: { - if( _individuals.TryGetValue( identifier, out collection ) ) - { + if (_individuals.TryGetValue(identifier, out collection)) return true; - } - if( identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && Penumbra.Config.UseOwnerNameForCharacterCollection ) - { - return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); - } + if (identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && _config.UseOwnerNameForCharacterCollection) + return CheckWorlds(_actorManager.GetCurrentPlayer(), out collection); break; } case IdentifierType.Owned: { - if( CheckWorlds( identifier, out collection! ) ) - { + if (CheckWorlds(identifier, out collection!)) return true; - } // Handle generic NPC - var npcIdentifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId ); - if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) ) - { + var npcIdentifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, + identifier.Kind, identifier.DataId); + if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) return true; - } // Handle Ownership. - if( Penumbra.Config.UseOwnerNameForCharacterCollection ) - { - identifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); - return CheckWorlds( identifier, out collection ); - } + if (!_config.UseOwnerNameForCharacterCollection) + return false; - return false; + identifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, + ObjectKind.None, uint.MaxValue); + return CheckWorlds(identifier, out collection); } - case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); - case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ).Item1, out collection ); + case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection); + case IdentifierType.Special: return CheckWorlds(ConvertSpecialIdentifier(identifier).Item1, out collection); } collection = null; @@ -94,80 +86,71 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ Invalid, } - public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier( ActorIdentifier identifier ) + public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier(ActorIdentifier identifier) { - if( identifier.Type != IdentifierType.Special ) - { - return ( identifier, SpecialResult.Invalid ); - } + if (identifier.Type != IdentifierType.Special) + return (identifier, SpecialResult.Invalid); - if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) ) - { - return ( id, SpecialResult.PartyBanner ); - } + if (_actorManager.ResolvePartyBannerPlayer(identifier.Special, out var id)) + return (id, SpecialResult.PartyBanner); - if( _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) ) - { - return ( id, SpecialResult.PvPBanner ); - } + if (_actorManager.ResolvePvPBannerPlayer(identifier.Special, out id)) + return (id, SpecialResult.PvPBanner); - if( _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) ) - { - return ( id, SpecialResult.Mahjong ); - } + if (_actorManager.ResolveMahjongPlayer(identifier.Special, out id)) + return (id, SpecialResult.Mahjong); - switch( identifier.Special ) + switch (identifier.Special) { - case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: return ( _actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen ); - case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom ); - case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.DyePreview ); - case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: return ( _actorManager.GetCurrentPlayer(), SpecialResult.Portrait ); + case ScreenActor.CharacterScreen when _config.UseCharacterCollectionInMainWindow: + return (_actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen); + case ScreenActor.FittingRoom when _config.UseCharacterCollectionInTryOn: + return (_actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom); + case ScreenActor.DyePreview when _config.UseCharacterCollectionInTryOn: + return (_actorManager.GetCurrentPlayer(), SpecialResult.DyePreview); + case ScreenActor.Portrait when _config.UseCharacterCollectionsInCards: + return (_actorManager.GetCurrentPlayer(), SpecialResult.Portrait); case ScreenActor.ExamineScreen: { identifier = _actorManager.GetInspectPlayer(); - if( identifier.IsValid ) - { - return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect ); - } + if (identifier.IsValid) + return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect); identifier = _actorManager.GetCardPlayer(); - if( identifier.IsValid ) - { - return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card ); - } + if (identifier.IsValid) + return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card); - return Penumbra.Config.UseCharacterCollectionInTryOn ? ( _actorManager.GetGlamourPlayer(), SpecialResult.Glamour ) : ( identifier, SpecialResult.Invalid ); + return _config.UseCharacterCollectionInTryOn + ? (_actorManager.GetGlamourPlayer(), SpecialResult.Glamour) + : (identifier, SpecialResult.Invalid); } - default: return ( identifier, SpecialResult.Invalid ); + default: return (identifier, SpecialResult.Invalid); } } - public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, true, false, false ), out collection ); + public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection) + => TryGetCollection(_actorManager.FromObject(gameObject, true, false, false), out collection); - public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false, false ), out collection ); + public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection) + => TryGetCollection(_actorManager.FromObject(gameObject, out _, true, false, false), out collection); - private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) + private bool CheckWorlds(ActorIdentifier identifier, out ModCollection? collection) { - if( !identifier.IsValid ) + if (!identifier.IsValid) { collection = null; return false; } - if( _individuals.TryGetValue( identifier, out collection ) ) - { + if (_individuals.TryGetValue(identifier, out collection)) return true; - } - identifier = _actorManager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); - if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) - { + identifier = _actorManager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, + identifier.DataId); + if (identifier.IsValid && _individuals.TryGetValue(identifier, out collection)) return true; - } collection = null; return false; } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 28059ecf..80fa6089 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -12,6 +12,7 @@ namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections { + private readonly Configuration _config; private readonly ActorManager _actorManager; private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); private readonly Dictionary _individuals = new(); @@ -20,11 +21,17 @@ public sealed partial class IndividualCollections => _assignments; // TODO - public IndividualCollections(ActorService actorManager) - => _actorManager = actorManager.AwaitedService; + public IndividualCollections(ActorService actorManager, Configuration config) + { + _config = config; + _actorManager = actorManager.AwaitedService; + } - public IndividualCollections(ActorManager actorManager) - => _actorManager = actorManager; + public IndividualCollections(ActorManager actorManager, Configuration config) + { + _actorManager = actorManager; + _config = config; + } public enum AddResult { @@ -234,7 +241,7 @@ public sealed partial class IndividualCollections => identifier.IsValid ? Index(DisplayString(identifier)) : -1; private string DisplayString(ActorIdentifier identifier) - { + { return identifier.Type switch { IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 29733382..34a8db3c 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -19,11 +19,11 @@ public class TempCollectionManager : IDisposable private readonly CollectionStorage _storage; private readonly Dictionary _customCollections = new(); - public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage) + public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorService actors, CollectionStorage storage) { _communicator = communicator; _storage = storage; - Collections = new IndividualCollections(actors); + Collections = new IndividualCollections(actors, config); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index f50e2472..2ccdf3c3 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -49,12 +49,12 @@ public partial class ModCollection // Obtain data from the cache. internal MetaCache? MetaCache - => _cache?.MetaManipulations; + => _cache?.Meta; public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) { if (_cache != null) - return _cache.MetaManipulations.GetImcFile(path, out file); + return _cache.Meta.GetImcFile(path, out file); file = null; return false; @@ -80,7 +80,7 @@ public partial class ModCollection } else { - _cache.MetaManipulations.SetFiles(); + _cache.Meta.SetFiles(); Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}."); } } @@ -90,28 +90,28 @@ public partial class ModCollection if (_cache == null) Penumbra.CharacterUtility.ResetResource(idx); else - _cache.MetaManipulations.SetFile(idx); + _cache.Meta.SetFile(idx); } // Used for short periods of changed files. public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => _cache?.MetaManipulations.TemporarilySetEqdpFile(genderRace, accessory) + => _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory) ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() - => _cache?.MetaManipulations.TemporarilySetEqpFile() + => _cache?.Meta.TemporarilySetEqpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Eqp); public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() - => _cache?.MetaManipulations.TemporarilySetGmpFile() + => _cache?.Meta.TemporarilySetGmpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Gmp); public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() - => _cache?.MetaManipulations.TemporarilySetCmpFile() + => _cache?.Meta.TemporarilySetCmpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.HumanCmp); public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) - => _cache?.MetaManipulations.TemporarilySetEstFile(type) + => _cache?.Meta.TemporarilySetEstFile(type) ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index bdc16d75..bda1a8e5 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -550,19 +550,19 @@ public class CommandHandler : IDisposable private void Print(string text) { - if (Penumbra.Config.PrintSuccessfulCommandsToChat) + if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text); } private void Print(DefaultInterpolatedStringHandler text) { - if (Penumbra.Config.PrintSuccessfulCommandsToChat) + if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text.ToStringAndClear()); } private void Print(Func text) { - if (Penumbra.Config.PrintSuccessfulCommandsToChat) + if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text()); } } diff --git a/Penumbra/Import/Structs/TexToolsStructs.cs b/Penumbra/Import/Structs/TexToolsStructs.cs index cdd70c53..2a160c62 100644 --- a/Penumbra/Import/Structs/TexToolsStructs.cs +++ b/Penumbra/Import/Structs/TexToolsStructs.cs @@ -12,7 +12,7 @@ internal static class DefaultTexToolsData } [Serializable] -internal class SimpleMod +public class SimpleMod { public string Name = string.Empty; public string Category = string.Empty; @@ -24,14 +24,14 @@ internal class SimpleMod } [Serializable] -internal class ModPackPage +public class ModPackPage { public int PageIndex = 0; public ModGroup[] ModGroups = Array.Empty(); } [Serializable] -internal class ModGroup +public class ModGroup { public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; @@ -40,7 +40,7 @@ internal class ModGroup } [Serializable] -internal class OptionList +public class OptionList { public string Name = string.Empty; public string Description = string.Empty; @@ -52,7 +52,7 @@ internal class OptionList } [Serializable] -internal class ExtendedModPack +public class ExtendedModPack { public string PackVersion = string.Empty; public string Name = DefaultTexToolsData.Name; @@ -65,7 +65,7 @@ internal class ExtendedModPack } [Serializable] -internal class SimpleModPack +public class SimpleModPack { public string TtmpVersion = string.Empty; public string Name = DefaultTexToolsData.Name; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 6234e9ca..13200a9c 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -18,11 +18,13 @@ namespace Penumbra.Import; public partial class TexToolsImporter { - // Extract regular compressed archives that are folders containing penumbra-formatted mods. - // The mod has to either contain a meta.json at top level, or one folder deep. - // If the meta.json is one folder deep, all other files have to be in the same folder. - // The extracted folder gets its name either from that one top-level folder or from the mod name. - // All data is extracted without manipulation of the files or metadata. + /// + /// Extract regular compressed archives that are folders containing penumbra-formatted mods. + /// The mod has to either contain a meta.json at top level, or one folder deep. + /// If the meta.json is one folder deep, all other files have to be in the same folder. + /// The extracted folder gets its name either from that one top-level folder or from the mod name. + /// All data is extracted without manipulation of the files or metadata. + /// private DirectoryInfo HandleRegularArchive( FileInfo modPackFile ) { using var zfs = modPackFile.OpenRead(); @@ -111,7 +113,7 @@ public partial class TexToolsImporter } _currentModDirectory.Refresh(); - ModCreator.SplitMultiGroups( _currentModDirectory ); + _modManager.Creator.SplitMultiGroups( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 5c06dcdc..6de16612 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -40,7 +40,7 @@ public partial class TexToolsImporter // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList ); - ModCreator.CreateDefaultFiles( _currentModDirectory ); + _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -97,7 +97,7 @@ public partial class TexToolsImporter // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); - ModCreator.CreateDefaultFiles( _currentModDirectory ); + _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -185,7 +185,7 @@ public partial class TexToolsImporter var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name ) ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); + options.Add( _modManager.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); if( option.IsChecked ) { defaultSettings = group.SelectionType == GroupType.Multi @@ -211,7 +211,7 @@ public partial class TexToolsImporter } } - ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + _modManager.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, defaultSettings ?? 0, group.Description, options ); ++groupPriority; } @@ -219,7 +219,7 @@ public partial class TexToolsImporter } ResetStreamDisposer(); - ModCreator.CreateDefaultFiles( _currentModDirectory ); + _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 759474e9..bee31cb2 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -12,7 +12,26 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Import; public partial class TexToolsMeta -{ +{ + public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable manipulations, DirectoryInfo basePath) + { + var files = ConvertToTexTools(manager, manipulations); + + foreach (var (file, data) in files) + { + var path = Path.Combine(basePath.FullName, file); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllBytes(path, data); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not write meta file {path}:\n{e}"); + } + } + } + public static Dictionary< string, byte[] > ConvertToTexTools( MetaFileManager manager, IEnumerable< MetaManipulation > manips ) { var ret = new Dictionary< string, byte[] >(); diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 654760ca..72ebac34 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -9,6 +9,7 @@ using Penumbra.GameData; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.Services; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.Meta; @@ -21,9 +22,10 @@ public unsafe class MetaFileManager internal readonly DataManager GameData; internal readonly ActiveCollections ActiveCollections; internal readonly ValidityChecker ValidityChecker; + internal readonly IdentifierService Identifier; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData, - ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker) + ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -31,6 +33,7 @@ public unsafe class MetaFileManager ActiveCollections = activeCollections; Config = config; ValidityChecker = validityChecker; + Identifier = identifier; SignatureHelper.Initialise(this); } @@ -55,7 +58,7 @@ public unsafe class MetaFileManager return; ResidentResources.Reload(); - collection._cache?.MetaManipulations.SetFiles(); + collection._cache?.Meta.SetFiles(); } /// diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 20b6e019..b2ec750f 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -244,7 +244,7 @@ public class DuplicateManager try { var mod = new Mod(modDirectory); - mod.Reload(_modManager, true, out _); + _modManager.Creator.ReloadMod(mod, true, out _); Finished = false; _files.UpdateAll(mod, mod.Default); diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 658922cf..5f32122c 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -13,5 +13,5 @@ public interface IMod public ISubMod Default { get; } public IReadOnlyList< IModGroup > Groups { get; } - public IEnumerable< ISubMod > AllSubMods { get; } + public IEnumerable< SubMod > AllSubMods { get; } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index cf1ff027..17505550 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -160,7 +160,7 @@ public class ModFileEditor if (deletions <= 0) return; - mod.Reload(_modManager, false, out _); + _modManager.Creator.ReloadMod(mod, false, out _); _files.UpdateAll(mod, option); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 6ef43454..c25a6872 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -221,10 +221,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList if (!_identifier.Valid) return; - foreach (var gamePath in mod.AllRedirects) + foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) _identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString()); - foreach (var manip in mod.AllManipulations) + foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip); cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 86bd826e..15fc5e92 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -116,7 +116,7 @@ public class ModDataEditor return changes; } - public ModDataChangeType LoadMeta(Mod mod) + public ModDataChangeType LoadMeta(ModCreator creator, Mod mod) { var metaFile = _saveService.FileNames.ModMetaPath(mod); if (!File.Exists(metaFile)) @@ -171,7 +171,7 @@ public class ModDataEditor } if (newFileVersion != ModMeta.FileVersion) - if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion)) + if (ModMigration.Migrate(creator, _saveService, mod, json, ref newFileVersion)) { changes |= ModDataChangeType.Migration; _saveService.ImmediateSave(new ModMeta(mod)); diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 2d5201ad..76f4e1d6 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -15,14 +15,14 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { private readonly ModManager _modManager; private readonly CommunicatorService _communicator; - private readonly FilenameService _files; + private readonly SaveService _saveService; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files) + public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService) { _modManager = modManager; _communicator = communicator; - _files = files; + _saveService = saveService; Reload(); Changed += OnChange; _communicator.ModDiscoveryFinished.Subscribe(Reload); @@ -66,8 +66,8 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable private void Reload() { // TODO - if (Load(new FileInfo(_files.FilesystemFile), _modManager, ModToIdentifier, ModToName)) - Penumbra.SaveService.ImmediateSave(this); + if (Load(new FileInfo(_saveService.FileNames.FilesystemFile), _modManager, ModToIdentifier, ModToName)) + _saveService.ImmediateSave(this); Penumbra.Log.Debug("Reloaded mod filesystem."); } @@ -76,7 +76,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) { if (type != FileSystemChangeType.Reload) - Penumbra.SaveService.QueueSave(this); + _saveService.QueueSave(this); } // Update sort order when defaulted mod names change. @@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable break; case ModPathChangeType.Moved: - Penumbra.SaveService.QueueSave(this); + _saveService.QueueSave(this); break; case ModPathChangeType.Reloaded: // Nothing diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 7acb2417..ae22c5e9 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -24,18 +24,21 @@ public sealed class ModManager : ModStorage private readonly Configuration _config; private readonly CommunicatorService _communicator; + public readonly ModCreator Creator; public readonly ModDataEditor DataEditor; public readonly ModOptionEditor OptionEditor; public DirectoryInfo BasePath { get; private set; } = null!; public bool Valid { get; private set; } - public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor) + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, + ModCreator creator) { _config = config; _communicator = communicator; DataEditor = dataEditor; OptionEditor = optionEditor; + Creator = creator; SetBaseDirectory(config.ModDirectory, true); DiscoverMods(); } @@ -73,8 +76,8 @@ public sealed class ModManager : ModStorage if (this.Any(m => m.ModPath.Name == modFolder.Name)) return; - ModCreator.SplitMultiGroups(modFolder); - var mod = Mod.LoadMod(this, modFolder, true); + Creator.SplitMultiGroups(modFolder); + var mod = Creator.LoadMod(modFolder, true); if (mod == null) return; @@ -119,7 +122,7 @@ public sealed class ModManager : ModStorage var oldName = mod.Name; _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!mod.Reload(Penumbra.ModManager, true, out var metaChange)) + if (!Creator.ReloadMod(mod, true, out var metaChange)) { Penumbra.Log.Warning(mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." @@ -185,7 +188,7 @@ public sealed class ModManager : ModStorage dir.Refresh(); mod.ModPath = dir; - if (!mod.Reload(Penumbra.ModManager, false, out var metaChange)) + if (!Creator.ReloadMod(mod, false, out var metaChange)) { Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); return; @@ -307,7 +310,7 @@ public sealed class ModManager : ModStorage var queue = new ConcurrentQueue(); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => { - var mod = Mod.LoadMod(this, dir, false); + var mod = Creator.LoadMod(dir, false); if (mod != null) queue.Enqueue(mod); }); diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 196c7ed5..c41c40dc 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -20,8 +20,8 @@ public static partial class ModMigration [GeneratedRegex("^group_", RegexOptions.Compiled)] private static partial Regex GroupStartRegex(); - public static bool Migrate(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) - => MigrateV0ToV1(saveService, mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); + public static bool Migrate(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + => MigrateV0ToV1(creator, saveService, mod, json, ref fileVersion) || MigrateV1ToV2(saveService, mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) { @@ -33,13 +33,13 @@ public static partial class ModMigration return true; } - private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) + private static bool MigrateV1ToV2(SaveService saveService, Mod mod, ref uint fileVersion) { if (fileVersion > 1) return false; - if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) - foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) + if (!saveService.FileNames.GetOptionGroupFiles(mod).All(g => GroupRegex().IsMatch(g.Name))) + foreach (var (group, index) in saveService.FileNames.GetOptionGroupFiles(mod).WithIndex().ToArray()) { var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); try @@ -58,7 +58,7 @@ public static partial class ModMigration return true; } - private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + private static bool MigrateV0ToV1(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion) { if (fileVersion > 0) return false; @@ -69,21 +69,21 @@ public static partial class ModMigration var priority = 1; var seenMetaFiles = new HashSet(); foreach (var group in groups.Values) - ConvertGroup(mod, group, ref priority, seenMetaFiles); + ConvertGroup(creator, mod, group, ref priority, seenMetaFiles); foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) { if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod._default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); + && !mod.Default.FileData.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}."); } - mod._default.FileSwapData.Clear(); - mod._default.FileSwapData.EnsureCapacity(swaps.Count); + mod.Default.FileSwapData.Clear(); + mod.Default.FileSwapData.EnsureCapacity(swaps.Count); foreach (var (gamePath, swapPath) in swaps) - mod._default.FileSwapData.Add(gamePath, swapPath); + mod.Default.FileSwapData.Add(gamePath, swapPath); - mod._default.IncorporateMetaChanges(mod.ModPath, true); + creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); foreach (var (_, index) in mod.Groups.WithIndex()) saveService.ImmediateSave(new ModSaveGroup(mod, index)); @@ -118,7 +118,7 @@ public static partial class ModMigration return true; } - private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) + private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) { if (group.Options.Count == 0) return; @@ -134,15 +134,15 @@ public static partial class ModMigration Priority = priority++, Description = string.Empty, }; - mod._groups.Add(newMultiGroup); + mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++)); break; case GroupType.Single: if (group.Options.Count == 1) { - AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); + AddFilesToSubMod(mod.Default, mod.ModPath, group.Options[0], seenMetaFiles); return; } @@ -152,9 +152,9 @@ public static partial class ModMigration Priority = priority++, Description = string.Empty, }; - mod._groups.Add(newSingleGroup); + mod.Groups.Add(newSingleGroup); foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); + newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles)); break; } @@ -173,11 +173,11 @@ public static partial class ModMigration } } - private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) + private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet seenMetaFiles) { var subMod = new SubMod(mod) { Name = option.OptionName }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - subMod.IncorporateMetaChanges(mod.ModPath, false); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index b1029822..ed978000 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -46,11 +46,11 @@ public class ModOptionEditor /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (group.Type == type) return; - mod._groups[groupIdx] = group.Convert(type); + mod.Groups[groupIdx] = group.Convert(type); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } @@ -58,7 +58,7 @@ public class ModOptionEditor /// Change the settings stored as default options in a mod. public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (group.DefaultSettings == defaultOption) return; @@ -70,7 +70,7 @@ public class ModOptionEditor /// Rename an option group if possible. public void RenameModGroup(Mod mod, int groupIdx, string newName) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; var oldName = group.Name; if (oldName == newName || !VerifyFileName(mod, group, newName, true)) return; @@ -93,9 +93,9 @@ public class ModOptionEditor if (!VerifyFileName(mod, null, newName, true)) return; - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; + var maxPriority = mod.Groups.Count == 0 ? 0 : mod.Groups.Max(o => o.Priority) + 1; - mod._groups.Add(type == GroupType.Multi + mod.Groups.Add(type == GroupType.Multi ? new MultiModGroup { Name = newName, @@ -106,16 +106,16 @@ public class ModOptionEditor Name = newName, Priority = maxPriority, }); - _saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + _saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } /// Delete a given option group. Fires an event to prepare before actually deleting. public void DeleteModGroup(Mod mod, int groupIdx) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod._groups.RemoveAt(groupIdx); + mod.Groups.RemoveAt(groupIdx); UpdateSubModPositions(mod, groupIdx); _saveService.SaveAllOptionGroups(mod); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); @@ -124,7 +124,7 @@ public class ModOptionEditor /// Move the index of a given option group. public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) { - if (!mod._groups.Move(groupIdxFrom, groupIdxTo)) + if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) return; UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); @@ -135,7 +135,7 @@ public class ModOptionEditor /// Change the description of the given option group. public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (group.Description == newDescription) return; @@ -152,7 +152,7 @@ public class ModOptionEditor /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; var option = group[optionIdx]; if (option.Description == newDescription || option is not SubMod s) return; @@ -165,7 +165,7 @@ public class ModOptionEditor /// Change the internal priority of the given option group. public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (group.Priority == newPriority) return; @@ -182,7 +182,7 @@ public class ModOptionEditor /// Change the internal priority of the given option. public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) { - switch (mod._groups[groupIdx]) + switch (mod.Groups[groupIdx]) { case SingleModGroup: ChangeGroupPriority(mod, groupIdx, newPriority); @@ -201,7 +201,7 @@ public class ModOptionEditor /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch (mod._groups[groupIdx]) + switch (mod.Groups[groupIdx]) { case SingleModGroup s: if (s.OptionData[optionIdx].Name == newName) @@ -225,7 +225,7 @@ public class ModOptionEditor /// Add a new empty option of the given name for the given group. public void AddOption(Mod mod, int groupIdx, string newName) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; var subMod = new SubMod(mod) { Name = newName }; subMod.SetPosition(groupIdx, group.Count); switch (group) @@ -248,7 +248,7 @@ public class ModOptionEditor if (option is not SubMod o) return; - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions) { Penumbra.Log.Error( @@ -276,7 +276,7 @@ public class ModOptionEditor /// Delete the given option from the given group. public void DeleteOption(Mod mod, int groupIdx, int optionIdx) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); switch (group) { @@ -297,7 +297,7 @@ public class ModOptionEditor /// Move an option inside the given option group. public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) { - var group = mod._groups[groupIdx]; + var group = mod.Groups[groupIdx]; if (!group.MoveOption(optionIdxFrom, optionIdxTo)) return; @@ -379,7 +379,7 @@ public class ModOptionEditor /// Update the indices stored in options from a given group on. private static void UpdateSubModPositions(Mod mod, int fromGroup) { - foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup)) { foreach (var (o, optionIdx) in group.OfType().WithIndex()) o.SetPosition(groupIdx, optionIdx); @@ -390,9 +390,9 @@ public class ModOptionEditor private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { if (groupIdx == -1 && optionIdx == 0) - return mod._default; + return mod.Default; - return mod._groups[groupIdx] switch + return mod.Groups[groupIdx] switch { SingleModGroup s => s.OptionData[optionIdx], MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index eb9571c2..28258ded 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -31,76 +31,6 @@ public partial class Mod internal Mod( DirectoryInfo modPath ) { ModPath = modPath; - _default = new SubMod( this ); - } - - public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) - { - modPath.Refresh(); - if( !modPath.Exists ) - { - Penumbra.Log.Error( $"Supplied mod directory {modPath} does not exist." ); - return null; - } - - var mod = new Mod(modPath); - if (mod.Reload(modManager, incorporateMetaChanges, out _)) - return mod; - - // Can not be base path not existing because that is checked before. - Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." ); - return null; - - } - - internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) - { - modDataChange = ModDataChangeType.Deletion; - ModPath.Refresh(); - if( !ModPath.Exists ) - { - return false; - } - - modDataChange = modManager.DataEditor.LoadMeta(this); - if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 ) - { - return false; - } - - modManager.DataEditor.LoadLocalData(this); - - LoadDefaultOption(); - LoadAllGroups(); - if( incorporateMetaChanges ) - { - IncorporateAllMetaChanges(true); - } - - return true; - } - - // Convert all .meta and .rgsp files to their respective meta changes and add them to their options. - // Deletes the source files if delete is true. - private void IncorporateAllMetaChanges( bool delete ) - { - var changes = false; - List< string > deleteList = new(); - foreach( var subMod in AllSubMods.OfType< SubMod >() ) - { - var (localChanges, localDeleteList) = subMod.IncorporateMetaChanges( ModPath, false ); - changes |= localChanges; - if( delete ) - { - deleteList.AddRange( localDeleteList ); - } - } - - SubMod.DeleteDeleteList( deleteList, delete ); - - if( changes ) - { - Penumbra.SaveService.SaveAllOptionGroups(this); - } + Default = new SubMod( this ); } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs deleted file mode 100644 index 6233ece9..00000000 --- a/Penumbra/Mods/Mod.Creator.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Import.Structs; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -internal static partial class ModCreator -{ - /// - /// Create and return a new directory based on the given directory and name, that is
- /// - Not Empty.
- /// - Unique, by appending (digit) for duplicates.
- /// - Containing no symbols invalid for FFXIV or windows paths.
- ///
- /// - /// - /// - /// - /// - public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) - { - var name = modListName; - if( name.Length == 0 ) - { - name = "_"; - } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - if( newModFolder.Length == 0 ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - if( create ) - { - Directory.CreateDirectory( newModFolder ); - } - - return new DirectoryInfo( newModFolder ); - } - - /// - /// Create the name for a group or option subfolder based on its parent folder and given name. - /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. - /// - public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) - { - var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); - } - - /// Create a file for an option group from given data. - public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) - { - switch( type ) - { - case GroupType.Multi: - { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - case GroupType.Single: - { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange( subMods.OfType< SubMod >() ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - } - } - - /// Create the data for a given sub mod from its data and the folder it is based on. - public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) - { - var list = optionFolder.EnumerateNonHiddenFiles() - .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) - .Where( t => t.Item1 ); - - var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; - foreach( var (_, gamePath, file) in list ) - { - mod.FileData.TryAdd( gamePath, file ); - } - - mod.IncorporateMetaChanges( baseFolder, true ); - return mod; - } - - /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - - /// - /// Create the default data file from all unused files that were not handled before - /// and are used in sub mods. - /// - internal static void CreateDefaultFiles( DirectoryInfo directory ) - { - var mod = new Mod( directory ); - mod.Reload( Penumbra.ModManager, false, out _ ); - foreach( var file in mod.FindUnusedFiles() ) - { - if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - mod._default.FileData.TryAdd( gamePath, file ); - } - - mod._default.IncorporateMetaChanges( directory, true ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); - } - - /// Return the name of a new valid directory based on the base directory and the given name. - public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); - - /// Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) - { - switch( s ) - { - case ".": return replacement; - case "..": return replacement + replacement; - } - - StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) - { - if( c.IsInvalidInPath() ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } - - public static void SplitMultiGroups( DirectoryInfo baseDir ) - { - var mod = new Mod( baseDir ); - - var files = mod.GroupFiles.ToList(); - var idx = 0; - var reorder = false; - foreach( var groupFile in files ) - { - ++idx; - try - { - if( reorder ) - { - var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; - Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); - groupFile.MoveTo( newName, false ); - } - } - catch( Exception ex ) - { - throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); - } - - try - { - var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); - if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) - { - continue; - } - - var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; - if( name.Length == 0 ) - { - continue; - } - - - var options = json[ "Options" ]?.Children().ToList(); - if( options == null ) - { - continue; - } - - if( options.Count <= IModGroup.MaxMultiOptions ) - { - continue; - } - - Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); - var clone = json.DeepClone(); - reorder = true; - foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var newOptions = clone[ "Options" ]!.Children().ToList(); - foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var match = DuplicateNumber().Match( name ); - var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; - name = match.Success ? name[ ..4 ] : name; - var oldName = $"{name}, Part {startNumber}"; - var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - var newName = $"{name}, Part {startNumber + 1}"; - var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - json[ nameof( IModGroup.Name ) ] = oldName; - clone[ nameof( IModGroup.Name ) ] = newName; - - clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; - - Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); - using( var oldFile = File.CreateText( oldPath ) ) - { - using var j = new JsonTextWriter( oldFile ) - { - Formatting = Formatting.Indented, - }; - json.WriteTo( j ); - } - - Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); - using( var newFile = File.CreateText( newPath ) ) - { - using var j = new JsonTextWriter( newFile ) - { - Formatting = Formatting.Indented, - }; - clone.WriteTo( j ); - } - - Penumbra.Log.Debug( - $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); - groupFile.Delete(); - } - catch( Exception ex ) - { - throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); - } - } - } - - [GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking )] - private static partial Regex DuplicateNumber(); -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs deleted file mode 100644 index 8820dd7b..00000000 --- a/Penumbra/Mods/Mod.Files.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Api.Enums; -using Penumbra.Meta; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public ISubMod Default - => _default; - - public IReadOnlyList Groups - => _groups; - - internal readonly SubMod _default; - internal readonly List _groups = new(); - - public IEnumerable AllSubMods - => _groups.SelectMany(o => o).Prepend(_default); - - public IEnumerable AllManipulations - => AllSubMods.SelectMany(s => s.Manipulations); - - public IEnumerable AllRedirects - => AllSubMods.SelectMany(s => s.Files.Keys.Concat(s.FileSwaps.Keys)); - - public IEnumerable AllFiles - => AllSubMods.SelectMany(o => o.Files) - .Select(p => p.Value); - - public IEnumerable GroupFiles - => ModPath.EnumerateFiles("group_*.json"); - - public List FindUnusedFiles() - { - var modFiles = AllFiles.ToHashSet(); - return ModPath.EnumerateDirectories() - .Where(d => !d.IsHidden()) - .SelectMany(FileExtensions.EnumerateNonHiddenFiles) - .Select(f => new FullPath(f)) - .Where(f => !modFiles.Contains(f)) - .ToList(); - } - - private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) - { - if (!File.Exists(file.FullName)) - return null; - - try - { - var json = JObject.Parse(File.ReadAllText(file.FullName)); - switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) - { - case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx); - case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx); - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}"); - } - - return null; - } - - private void LoadAllGroups() - { - _groups.Clear(); - var changes = false; - foreach (var file in GroupFiles) - { - var group = LoadModGroup(this, file, _groups.Count); - if (group != null && _groups.All(g => g.Name != group.Name)) - { - changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName; - _groups.Add(group); - } - else - { - changes = true; - } - } - - if (changes) - Penumbra.SaveService.SaveAllOptionGroups(this); - } - - private void LoadDefaultOption() - { - var defaultFile = Penumbra.Filenames.OptionGroupFile(this, -1); - _default.SetPosition(-1, 0); - try - { - if (!File.Exists(defaultFile)) - _default.Load(ModPath, new JObject(), out _); - else - _default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not parse default file for {Name}:\n{e}"); - } - } - - public void WriteAllTexToolsMeta(MetaFileManager manager) - { - try - { - _default.WriteTexToolsMeta(manager, ModPath); - foreach (var group in Groups) - { - var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); - if (!dir.Exists) - dir.Create(); - - foreach (var option in group.OfType()) - { - var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); - if (!optionDir.Exists) - optionDir.Create(); - - option.WriteTexToolsMeta(manager, optionDir); - } - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); - } - } -} diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index bc7a2c4d..24a78370 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using OtterGui; using OtterGui.Classes; +using Penumbra.Import; +using Penumbra.Meta; +using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -29,7 +34,61 @@ public sealed partial class Mod : IMod public bool Favorite { get; internal set; } = false; + // Options + public readonly SubMod Default; + public readonly List Groups = new(); + + ISubMod IMod.Default + => Default; + + IReadOnlyList IMod.Groups + => Groups; + + public IEnumerable AllSubMods + => Groups.SelectMany(o => o).OfType().Prepend(Default); + + public List FindUnusedFiles() + { + var modFiles = AllSubMods.SelectMany(o => o.Files) + .Select(p => p.Value) + .ToHashSet(); + return ModPath.EnumerateDirectories() + .Where(d => !d.IsHidden()) + .SelectMany(FileExtensions.EnumerateNonHiddenFiles) + .Select(f => new FullPath(f)) + .Where(f => !modFiles.Contains(f)) + .ToList(); + } + // Access public override string ToString() => Name.Text; + + public void WriteAllTexToolsMeta(MetaFileManager manager) + { + try + { + TexToolsMeta.WriteTexToolsMeta(manager, Default.Manipulations, ModPath); + foreach (var group in Groups) + { + var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); + if (!dir.Exists) + dir.Create(); + + foreach (var option in group.OfType()) + { + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); + if (!optionDir.Exists) + optionDir.Create(); + + TexToolsMeta.WriteTexToolsMeta(manager, option.Manipulations, optionDir); + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); + } + } + } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs new file mode 100644 index 00000000..b0add38f --- /dev/null +++ b/Penumbra/Mods/ModCreator.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.Import; +using Penumbra.Import.Structs; +using Penumbra.Meta; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class ModCreator +{ + private readonly Configuration _config; + private readonly SaveService _saveService; + private readonly ModDataEditor _dataEditor; + private readonly MetaFileManager _metaFileManager; + private readonly IGamePathParser _gamePathParser; + + public ModCreator(SaveService saveService, Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager, + IGamePathParser gamePathParser) + { + _saveService = saveService; + _config = config; + _dataEditor = dataEditor; + _metaFileManager = metaFileManager; + _gamePathParser = gamePathParser; + } + + /// Creates directory and files necessary for a new mod without adding it to the manager. + public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "") + { + try + { + var newDir = CreateModFolder(basePath, newName); + _dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty); + CreateDefaultFiles(newDir); + return newDir; + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure", + NotificationType.Error); + return null; + } + } + + /// Load a mod by its directory. + public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges) + { + modPath.Refresh(); + if (!modPath.Exists) + { + Penumbra.Log.Error($"Supplied mod directory {modPath} does not exist."); + return null; + } + + var mod = new Mod(modPath); + if (ReloadMod(mod, incorporateMetaChanges, out _)) + return mod; + + // Can not be base path not existing because that is checked before. + Penumbra.Log.Warning($"Mod at {modPath} without name is not supported."); + return null; + } + + /// Reload a mod from its mod path. + public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange) + { + modDataChange = ModDataChangeType.Deletion; + if (!Directory.Exists(mod.ModPath.FullName)) + return false; + + modDataChange = _dataEditor.LoadMeta(this, mod); + if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) + return false; + + _dataEditor.LoadLocalData(mod); + LoadDefaultOption(mod); + LoadAllGroups(mod); + if (incorporateMetaChanges) + IncorporateAllMetaChanges(mod, true); + + return true; + } + + /// Load all option groups for a given mod. + public void LoadAllGroups(Mod mod) + { + mod.Groups.Clear(); + var changes = false; + foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) + { + var group = LoadModGroup(mod, file, mod.Groups.Count); + if (group != null && mod.Groups.All(g => g.Name != group.Name)) + { + changes = changes + || _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name) != file.FullName; + mod.Groups.Add(group); + } + else + { + changes = true; + } + } + + if (changes) + _saveService.SaveAllOptionGroups(mod); + } + + /// Load the default option for a given mod. + public void LoadDefaultOption(Mod mod) + { + var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1); + mod.Default.SetPosition(-1, 0); + try + { + if (!File.Exists(defaultFile)) + mod.Default.Load(mod.ModPath, new JObject(), out _); + else + mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not parse default file for {mod.Name}:\n{e}"); + } + } + + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool create = true) + { + var name = modListName; + if (name.Length == 0) + name = "_"; + + var newModFolderBase = NewOptionDirectory(outDirectory, name); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + if (newModFolder.Length == 0) + throw new IOException("Could not create mod folder: too many folders of the same name exist."); + + if (create) + Directory.CreateDirectory(newModFolder); + + return new DirectoryInfo(newModFolder); + } + + /// + /// Convert all .meta and .rgsp files to their respective meta changes and add them to their options. + /// Deletes the source files if delete is true. + /// + public void IncorporateAllMetaChanges(Mod mod, bool delete) + { + var changes = false; + List deleteList = new(); + foreach (var subMod in mod.AllSubMods) + { + var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); + changes |= localChanges; + if (delete) + deleteList.AddRange(localDeleteList); + } + + SubMod.DeleteDeleteList(deleteList, delete); + + if (!changes) + return; + + _saveService.SaveAllOptionGroups(mod); + _saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default)); + } + + + /// + /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. + /// If delete is true, the files are deleted afterwards. + /// + public (bool Changes, List DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete) + { + var deleteList = new List(); + var oldSize = option.ManipulationData.Count; + var deleteString = delete ? "with deletion." : "without deletion."; + foreach (var (key, file) in option.Files.ToList()) + { + var ext1 = key.Extension().AsciiToLower().ToString(); + var ext2 = file.Extension.ToLowerInvariant(); + try + { + if (ext1 == ".meta" || ext2 == ".meta") + { + option.FileData.Remove(key); + if (!file.Exists) + continue; + + var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName), + _config.KeepDefaultMetaChanges); + Penumbra.Log.Verbose( + $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); + deleteList.Add(file.FullName); + option.ManipulationData.UnionWith(meta.MetaManipulations); + } + else if (ext1 == ".rgsp" || ext2 == ".rgsp") + { + option.FileData.Remove(key); + if (!file.Exists) + continue; + + var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName), + _config.KeepDefaultMetaChanges); + Penumbra.Log.Verbose( + $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); + deleteList.Add(file.FullName); + + option.ManipulationData.UnionWith(rgsp.MetaManipulations); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}"); + } + } + + SubMod.DeleteDeleteList(deleteList, delete); + return (oldSize < option.ManipulationData.Count, deleteList); + } + + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName) + { + var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + return newModFolder.Length == 0 ? null : new DirectoryInfo(newModFolder); + } + + /// Create a file for an option group from given data. + public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, + int priority, int index, uint defaultSettings, string desc, IEnumerable subMods) + { + switch (type) + { + case GroupType.Multi: + { + var group = new MultiModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, idx))); + _saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + case GroupType.Single: + { + var group = new SingleModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.OptionData.AddRange(subMods.OfType()); + _saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + } + } + + /// Create the data for a given sub mod from its data and the folder it is based on. + public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + { + var list = optionFolder.EnumerateNonHiddenFiles() + .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) + .Where(t => t.Item1); + + var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. + { + Name = option.Name, + Description = option.Description, + }; + foreach (var (_, gamePath, file) in list) + mod.FileData.TryAdd(gamePath, file); + + IncorporateMetaChanges(mod, baseFolder, true); + return mod; + } + + /// Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod(string name) + => new SubMod(null!) // Mod is irrelevant here, only used for saving. + { + Name = name, + }; + + /// + /// Create the default data file from all unused files that were not handled before + /// and are used in sub mods. + /// + internal void CreateDefaultFiles(DirectoryInfo directory) + { + var mod = new Mod(directory); + ReloadMod(mod, false, out _); + foreach (var file in mod.FindUnusedFiles()) + { + if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) + mod.Default.FileData.TryAdd(gamePath, file); + } + + IncorporateMetaChanges(mod.Default, directory, true); + _saveService.ImmediateSave(new ModSaveGroup(mod, -1)); + } + + /// Return the name of a new valid directory based on the base directory and the given name. + public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName) + => new(Path.Combine(baseDir.FullName, ReplaceBadXivSymbols(optionName))); + + /// Normalize for nicer names, and remove invalid symbols or invalid paths. + public static string ReplaceBadXivSymbols(string s, string replacement = "_") + { + switch (s) + { + case ".": return replacement; + case "..": return replacement + replacement; + } + + StringBuilder sb = new(s.Length); + foreach (var c in s.Normalize(NormalizationForm.FormKC)) + { + if (c.IsInvalidInPath()) + sb.Append(replacement); + else + sb.Append(c); + } + + return sb.ToString(); + } + + public void SplitMultiGroups(DirectoryInfo baseDir) + { + var mod = new Mod(baseDir); + + var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList(); + var idx = 0; + var reorder = false; + foreach (var groupFile in files) + { + ++idx; + try + { + if (reorder) + { + var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[9..]}"; + Penumbra.Log.Debug($"Moving {groupFile.Name} to {Path.GetFileName(newName)} due to reordering after multi group split."); + groupFile.MoveTo(newName, false); + } + } + catch (Exception ex) + { + throw new Exception("Could not reorder group file after splitting multi group on .pmp import.", ex); + } + + try + { + var json = JObject.Parse(File.ReadAllText(groupFile.FullName)); + if (json[nameof(IModGroup.Type)]?.ToObject() is not GroupType.Multi) + continue; + + var name = json[nameof(IModGroup.Name)]?.ToObject() ?? string.Empty; + if (name.Length == 0) + continue; + + + var options = json["Options"]?.Children().ToList(); + if (options is not { Count: > IModGroup.MaxMultiOptions }) + continue; + + Penumbra.Log.Information($"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options."); + var clone = json.DeepClone(); + reorder = true; + foreach (var o in options.Skip(IModGroup.MaxMultiOptions)) + o.Remove(); + + var newOptions = clone["Options"]!.Children().ToList(); + foreach (var o in newOptions.Take(IModGroup.MaxMultiOptions)) + o.Remove(); + + var match = DuplicateNumber().Match(name); + var startNumber = match.Success ? int.Parse(match.Groups[0].Value) : 1; + name = match.Success ? name[..4] : name; + var oldName = $"{name}, Part {startNumber}"; + var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + var newName = $"{name}, Part {startNumber + 1}"; + var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + json[nameof(IModGroup.Name)] = oldName; + clone[nameof(IModGroup.Name)] = newName; + + clone[nameof(IModGroup.DefaultSettings)] = 0u; + + Penumbra.Log.Debug($"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName(oldPath)} after split."); + using (var oldFile = File.CreateText(oldPath)) + { + using var j = new JsonTextWriter(oldFile) + { + Formatting = Formatting.Indented, + }; + json.WriteTo(j); + } + + Penumbra.Log.Debug( + $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName(newPath)} after split."); + using (var newFile = File.CreateText(newPath)) + { + using var j = new JsonTextWriter(newFile) + { + Formatting = Formatting.Indented, + }; + clone.WriteTo(j); + } + + Penumbra.Log.Debug( + $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName(oldPath)} and {Path.GetFileName(newPath)}."); + groupFile.Delete(); + } + catch (Exception ex) + { + throw new Exception($"Could not split multi group file {groupFile.Name} on .pmp import.", ex); + } + } + } + + [GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking)] + private static partial Regex DuplicateNumber(); + + + /// Load an option group for a specific mod by its file and index. + private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) + { + if (!File.Exists(file.FullName)) + return null; + + try + { + var json = JObject.Parse(File.ReadAllText(file.FullName)); + switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) + { + case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx); + case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}"); + } + + return null; + } +} diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 459d5ffb..89b3d5bf 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -93,57 +93,6 @@ public sealed class SubMod : ISubMod ManipulationData.Add(s); } - // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. - // If delete is true, the files are deleted afterwards. - public (bool Changes, List DeleteList) IncorporateMetaChanges(DirectoryInfo basePath, bool delete) - { - var deleteList = new List(); - var oldSize = ManipulationData.Count; - var deleteString = delete ? "with deletion." : "without deletion."; - foreach (var (key, file) in Files.ToList()) - { - var ext1 = key.Extension().AsciiToLower().ToString(); - var ext2 = file.Extension.ToLowerInvariant(); - try - { - if (ext1 == ".meta" || ext2 == ".meta") - { - FileData.Remove(key); - if (!file.Exists) - continue; - - var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName), - Penumbra.Config.KeepDefaultMetaChanges); - Penumbra.Log.Verbose( - $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); - deleteList.Add(file.FullName); - ManipulationData.UnionWith(meta.MetaManipulations); - } - else if (ext1 == ".rgsp" || ext2 == ".rgsp") - { - FileData.Remove(key); - if (!file.Exists) - continue; - - var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName), - Penumbra.Config.KeepDefaultMetaChanges); - Penumbra.Log.Verbose( - $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); - deleteList.Add(file.FullName); - - ManipulationData.UnionWith(rgsp.MetaManipulations); - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}"); - } - } - - DeleteDeleteList(deleteList, delete); - return (oldSize < ManipulationData.Count, deleteList); - } - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) { if (!delete) @@ -161,63 +110,4 @@ public sealed class SubMod : ISubMod } } } - - public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false) - { - var files = TexToolsMeta.ConvertToTexTools(manager, Manipulations); - - foreach (var (file, data) in files) - { - var path = Path.Combine(basePath.FullName, file); - try - { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllBytes(path, data); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not write meta file {path}:\n{e}"); - } - } - - if (test) - TestMetaWriting(manager, files); - } - - [Conditional("DEBUG")] - private void TestMetaWriting(MetaFileManager manager, Dictionary files) - { - var meta = new HashSet(Manipulations.Count); - foreach (var (file, data) in files) - { - try - { - var x = file.EndsWith("rgsp") - ? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges) - : new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges); - meta.UnionWith(x.MetaManipulations); - } - catch - { - // ignored - } - } - - if (!Manipulations.SetEquals(meta)) - { - Penumbra.Log.Information("Meta Sets do not equal."); - foreach (var (m1, m2) in Manipulations.Zip(meta)) - Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}"); - - foreach (var m in Manipulations.Skip(meta.Count)) - Penumbra.Log.Information($"{m} {m.EntryToString()} "); - - foreach (var m in meta.Skip(Manipulations.Count)) - Penumbra.Log.Information($"{m} {m.EntryToString()} "); - } - else - { - Penumbra.Log.Information("Meta Sets are equal."); - } - } } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 3f0664cb..9019b468 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -7,6 +7,7 @@ using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Mods; @@ -19,33 +20,33 @@ public class TemporaryMod : IMod public int TotalManipulations => Default.Manipulations.Count; - public ISubMod Default - => _default; + public readonly SubMod Default; + + ISubMod IMod.Default + => Default; public IReadOnlyList< IModGroup > Groups => Array.Empty< IModGroup >(); - public IEnumerable< ISubMod > AllSubMods + public IEnumerable< SubMod > AllSubMods => new[] { Default }; - private readonly SubMod _default; - public TemporaryMod() - => _default = new SubMod( this ); + => Default = new SubMod( this ); public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) - => _default.FileData[ gamePath ] = fullPath; + => Default.FileData[ gamePath ] = fullPath; public bool SetManipulation( MetaManipulation manip ) - => _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip ); + => Default.ManipulationData.Remove( manip ) | Default.ManipulationData.Add( manip ); public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) { - _default.FileData = dict; - _default.ManipulationData = manips; + Default.FileData = dict; + Default.ManipulationData = manips; } - public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null ) + public static void SaveTempCollection( SaveService saveService, ModManager modManager, ModCollection collection, string? character = null ) { DirectoryInfo? dir = null; try @@ -55,7 +56,7 @@ public class TemporaryMod : IMod modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); - var defaultMod = (SubMod) mod.Default; + var defaultMod = mod.Default; foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) { if( gamePath.Path.EndsWith( ".imc"u8 ) ) @@ -84,7 +85,7 @@ public class TemporaryMod : IMod foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) defaultMod.ManipulationData.Add( manip ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(dir, defaultMod)); + saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod)); modManager.AddMod( dir ); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 150fd9c9..2ff848d8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -8,7 +8,6 @@ using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.DependencyInjection; using OtterGui; -using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; using Penumbra.Api.Enums; @@ -28,8 +27,8 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods; -using Penumbra.Meta; - +using Penumbra.Meta; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -39,32 +38,26 @@ public class Penumbra : IDalamudPlugin public static Logger Log { get; private set; } = null!; public static ChatService ChatService { get; private set; } = null!; - public static FilenameService Filenames { get; private set; } = null!; - public static SaveService SaveService { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; - public static ResidentResourceManager ResidentResources { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static ModManager ModManager { get; private set; } = null!; - public static ModCacheManager ModCaches { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; - public static TempCollectionManager TempCollections { get; private set; } = null!; - public static TempModManager TempMods { get; private set; } = null!; - public static ActorManager Actors { get; private set; } = null!; - public static IObjectIdentifier Identifier { get; private set; } = null!; - public static IGamePathParser GamePathParser { get; private set; } = null!; - public static StainService StainService { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaFileManager MetaFileManager { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + public static ModCacheManager ModCaches { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; - // TODO - public static ValidityChecker ValidityChecker { get; private set; } = null!; + public readonly RedrawService RedrawService; + public readonly ModFileSystem ModFileSystem; + public HttpApi HttpApi = null!; + internal ConfigWindow? ConfigWindow { get; private set; } - public readonly RedrawService RedrawService; - public readonly ModFileSystem ModFileSystem; - public HttpApi HttpApi = null!; - internal ConfigWindow? ConfigWindow { get; private set; } - private PenumbraWindowSystem? _windowSystem; - private bool _disposed; + private readonly ValidityChecker _validityChecker; + private readonly ResidentResourceManager _residentResources; + private readonly TempModManager _tempMods; + private readonly TempCollectionManager _tempCollections; + private PenumbraWindowSystem? _windowSystem; + private bool _disposed; private readonly PenumbraNew _tmp; @@ -73,29 +66,24 @@ public class Penumbra : IDalamudPlugin Log = PenumbraNew.Log; try { - _tmp = new PenumbraNew(this, pluginInterface); - ChatService = _tmp.Services.GetRequiredService(); - Filenames = _tmp.Services.GetRequiredService(); - SaveService = _tmp.Services.GetRequiredService(); - ValidityChecker = _tmp.Services.GetRequiredService(); + _tmp = new PenumbraNew(this, pluginInterface); + ChatService = _tmp.Services.GetRequiredService(); + _validityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); Config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); MetaFileManager = _tmp.Services.GetRequiredService(); Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); - ResidentResources = _tmp.Services.GetRequiredService(); + _tempMods = _tmp.Services.GetRequiredService(); + _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); ModManager = _tmp.Services.GetRequiredService(); CollectionManager = _tmp.Services.GetRequiredService(); - TempCollections = _tmp.Services.GetRequiredService(); + _tempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModCaches = _tmp.Services.GetRequiredService(); + ModCaches = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { _tmp.Services.GetRequiredService(); @@ -104,14 +92,14 @@ public class Penumbra : IDalamudPlugin SetupInterface(); SetupApi(); - ValidityChecker.LogExceptions(); + _validityChecker.LogExceptions(); Log.Information( - $"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + $"Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); if (CharacterUtility.Ready) - ResidentResources.Reload(); + _residentResources.Reload(); } catch { @@ -173,7 +161,7 @@ public class Penumbra : IDalamudPlugin if (CharacterUtility.Ready) { CollectionManager.Active.Default.SetFiles(); - ResidentResources.Reload(); + _residentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } } @@ -182,7 +170,7 @@ public class Penumbra : IDalamudPlugin if (CharacterUtility.Ready) { CharacterUtility.ResetAll(); - ResidentResources.Reload(); + _residentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } } @@ -211,8 +199,8 @@ public class Penumbra : IDalamudPlugin var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); - sb.Append($"> **`Plugin Version: `** {ValidityChecker.Version}\n"); - sb.Append($"> **`Commit Hash: `** {ValidityChecker.CommitHash}\n"); + sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); + sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n"); sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); @@ -235,9 +223,9 @@ public class Penumbra : IDalamudPlugin $"> **`Mods with FileSwaps: `** {ModCaches.Count(m => m.TotalSwapCount > 0)}, Total: {ModCaches.Sum(m => m.TotalSwapCount)}\n"); sb.Append( $"> **`Mods with Meta Manipulations:`** {ModCaches.Count(m => m.TotalManipulations > 0)}, Total {ModCaches.Sum(m => m.TotalManipulations)}\n"); - sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n"); + sb.Append($"> **`IMC Exceptions Thrown: `** {_validityChecker.ImcExceptions.Count}\n"); sb.Append( - $"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n"); + $"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n"); string CharacterName(ActorIdentifier id, string name) { @@ -259,8 +247,8 @@ public class Penumbra : IDalamudPlugin sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n"); - sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - TempCollections.Count}\n"); + sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - _tempCollections.Count}\n"); sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n"); sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n"); sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n"); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 94305567..d32bed09 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -95,6 +95,7 @@ public class PenumbraNew services.AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 7f2966ff..d602ed3c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -218,22 +218,26 @@ public class ItemSwapTab : IDisposable, ITab _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), + (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), + (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), + (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, + _swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), + (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; @@ -276,9 +280,10 @@ public class ItemSwapTab : IDisposable, ITab private void CreateMod() { - var newDir = ModCreator.CreateModFolder(_modManager.BasePath, _newModName); - _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); - ModCreator.CreateDefaultFiles(newDir); + var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription()); + if (newDir == null) + return; + _modManager.AddMod(newDir); if (!_swapData.WriteMod(_modManager, _modManager[^1], _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 5a338b9c..26c7c2ae 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,7 +13,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) { if( !file.ColorSets.Any( c => c.HasRows ) ) { @@ -95,9 +95,9 @@ public partial class ModEditWindow } } - private static bool DrawPreviewDye( MtrlFile file, bool disabled ) + private bool DrawPreviewDye( MtrlFile file, bool disabled ) { - var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection; + var (dyeId, (name, dyeColor, _)) = _stainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { @@ -106,7 +106,7 @@ public partial class ModEditWindow { for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId ); + ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); } } @@ -115,7 +115,7 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true ); + _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true ); return false; } @@ -217,7 +217,7 @@ public partial class ModEditWindow return false; } - private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) { static bool FixFloat( ref float val, float current ) { @@ -355,10 +355,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( hasDye ) { - if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection; + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; ret = true; } @@ -376,10 +376,10 @@ public partial class ModEditWindow return ret; } - private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) { - var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key; - if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + var stain = _stainService.StainCombo.CurrentSelection.Key; + if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) { return false; } @@ -390,7 +390,7 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Apply the selected dye to this row.", disabled, true ); - ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); ImGui.SameLine(); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 31fc2a49..4edb2701 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -15,7 +15,8 @@ using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; @@ -33,6 +34,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ItemSwapTab _itemSwapTab; private readonly DataManager _gameData; private readonly MetaFileManager _metaFileManager; + private readonly StainService _stainService; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -495,17 +497,18 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager, StainService stainService) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _modCaches = modCaches; - _metaFileManager = metaFileManager; - _gameData = gameData; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _modCaches = modCaches; + _metaFileManager = metaFileManager; + _stainService = stainService; + _gameData = gameData; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 9d4883b1..7484a344 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -66,7 +66,7 @@ public sealed class ConfigWindow : Window UiHelpers.SetupCommonSizes(); try { - if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) + if (_validityChecker.ImcExceptions.Count > 0) { DrawProblemWindow( $"There were {_validityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" @@ -75,14 +75,14 @@ public sealed class ConfigWindow : Window + "Please use the Launcher's Repair Game Files function to repair your client installation."); DrawImcExceptions(); } - else if (!Penumbra.ValidityChecker.IsValidSourceRepo) + else if (!_validityChecker.IsValidSourceRepo) { DrawProblemWindow( $"You are loading a release version of Penumbra from the repository \"{_pluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } - else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) + else if (_validityChecker.IsNotInstalledPenumbra) { DrawProblemWindow( $"You are loading a release version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" @@ -90,7 +90,7 @@ public sealed class ConfigWindow : Window + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } - else if (Penumbra.ValidityChecker.DevPenumbraExists) + else if (_validityChecker.DevPenumbraExists) { DrawProblemWindow( $"You are loading a installed version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 448ddd00..32844294 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -119,18 +119,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Tue, 18 Apr 2023 15:22:35 +0200 Subject: [PATCH 0883/2451] Add hook for parasol animation loading. --- .../PathResolving/AnimationHookService.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 787f13d0..e3b06de3 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -46,6 +46,7 @@ public unsafe class AnimationHookService : IDisposable _loadAreaVfxHook.Enable(); _scheduleClipUpdateHook.Enable(); _unkMountAnimationHook.Enable(); + _unkParasolAnimationHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -101,6 +102,7 @@ public unsafe class AnimationHookService : IDisposable _loadAreaVfxHook.Dispose(); _scheduleClipUpdateHook.Dispose(); _unkMountAnimationHook.Dispose(); + _unkParasolAnimationHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -317,9 +319,23 @@ public unsafe class AnimationHookService : IDisposable private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) { - var last = _animationLoadData.Value; + var last = _animationLoadData.Value; _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); _unkMountAnimationHook.Original(drawObject, unk1, unk2, unk3); _animationLoadData.Value = last; } + + + private delegate void UnkParasolAnimationDelegate(DrawObject* drawObject, int unk1); + + [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(UnkParasolAnimationDetour))] + private readonly Hook _unkParasolAnimationHook = null!; + + private void UnkParasolAnimationDetour(DrawObject* drawObject, int unk1) + { + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); + _unkParasolAnimationHook!.Original(drawObject, unk1); + _animationLoadData.Value = last; + } } From e9fc57022e3c95ac0f5f47e9d5ee0afcbe4ad9bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Apr 2023 18:42:33 +0200 Subject: [PATCH 0884/2451] Add new Mod Collections tab. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.cs | 10 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 107 ++++++++++++++++++ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 13 ++- 5 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs diff --git a/OtterGui b/OtterGui index 8ebcbf3e..51c350b5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8ebcbf3e78ed498be35fa2b9a13d9765d109c428 +Subproject commit 51c350b5f129b53afda3a51b057c228e152a6b88 diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 7484a344..7e465a82 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -9,8 +9,7 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; -using Penumbra.Util; - +using Penumbra.Util; namespace Penumbra.UI; public sealed class ConfigWindow : Window @@ -45,7 +44,7 @@ public sealed class ConfigWindow : Window RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(800, 600), + MinimumSize = new Vector2(900, 600), MaximumSize = new Vector2(4096, 2160), }; tutorial.UpdateTutorialStep(); @@ -112,9 +111,12 @@ public sealed class ConfigWindow : Window var text = e.ToString(); if (text == _lastException) return; - _lastException = text; } + else + { + _lastException = e.ToString(); + } Penumbra.Log.Error($"Exception thrown during UI Render:\n{_lastException}"); } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs new file mode 100644 index 00000000..069c981d --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Data.Parsing.Layer; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public class ModPanelCollectionsTab : ITab +{ + private readonly Configuration _config; + private readonly ModFileSystemSelector _selector; + private readonly CollectionStorage _collections; + + private readonly List<(ModCollection, ModCollection, uint, string)> _cache = new(); + + public ModPanelCollectionsTab(Configuration config, CollectionStorage storage, ModFileSystemSelector selector) + { + _config = config; + _collections = storage; + _selector = selector; + } + + public ReadOnlySpan Label + => "Collections"u8; + + public void DrawContent() + { + var (direct, inherited) = CountUsage(_selector.Selected!); + ImGui.NewLine(); + if (direct == 1) + ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); + else if (direct == 0) + ImGuiUtil.TextColored(Colors.RegexWarningBorder, "This mod is entirely unused."); + else + ImGui.TextUnformatted($"This Mod is directly configured in {direct} collections."); + if (inherited > 0) + { + ImGui.TextUnformatted($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); + } + + ImGui.NewLine(); + ImGui.Separator(); + ImGui.NewLine(); + using var table = ImRaii.Table("##modCollections", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + var size = ImGui.CalcTextSize("Unconfigured").X + 20 * ImGuiHelpers.GlobalScale; + var collectionSize = 200 * ImGuiHelpers.GlobalScale; + ImGui.TableSetupColumn("Collection", ImGuiTableColumnFlags.WidthFixed, collectionSize); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, size); + ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, collectionSize); + + ImGui.TableHeadersRow(); + foreach (var (collection, parent, color, text) in _cache) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collection.Name); + + ImGui.TableNextColumn(); + using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + { + ImGui.TextUnformatted(text); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(parent == collection ? string.Empty : parent.Name); + } + } + + private (int Direct, int Inherited) CountUsage(Mod mod) + { + _cache.Clear(); + var undefined = ColorId.UndefinedMod.Value(_config); + var enabled = ColorId.EnabledMod.Value(_config); + var inherited = ColorId.InheritedMod.Value(_config); + var disabled = ColorId.DisabledMod.Value(_config); + var disInherited = ColorId.InheritedDisabledMod.Value(_config); + var directCount = 0; + var inheritedCount = 0; + foreach (var collection in _collections) + { + var (settings, parent) = collection[mod.Index]; + var (color, text) = settings == null + ? (undefined, "Unconfigured") + : settings.Enabled + ? (parent == collection ? enabled : inherited, "Enabled") + : (parent == collection ? disabled : disInherited, "Disabled"); + _cache.Add((collection, parent, color, text)); + + if (color == enabled) + ++directCount; + else if (color == inherited) + ++inheritedCount; + } + + return (directCount, inheritedCount); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 9ecbdc41..68f9707d 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -198,13 +198,13 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description)); ImGui.SameLine(); - var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod)); + var fileExists = File.Exists(_filenames.ModMetaPath(_mod)); var tt = fileExists ? "Open the metadata json file in the text editor of your choice." : "The metadata json file does not exist."; if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(_modManager.DataEditor.MetaFile(_mod)) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(_filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } /// Do some edits outside of iterations. diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 59dce714..503e471f 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -19,16 +19,18 @@ public class ModPanelTabBar Settings, ChangedItems, Conflicts, + Collections, Edit, }; public readonly ModPanelSettingsTab Settings; public readonly ModPanelDescriptionTab Description; + public readonly ModPanelCollectionsTab Collections; public readonly ModPanelConflictsTab Conflicts; public readonly ModPanelChangedItemsTab ChangedItems; public readonly ModPanelEditTab Edit; private readonly ModEditWindow _modEditWindow; - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly TutorialService _tutorial; public readonly ITab[] Tabs; @@ -37,7 +39,7 @@ public class ModPanelTabBar public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, - TutorialService tutorial) + TutorialService tutorial, ModPanelCollectionsTab collections) { _modEditWindow = modEditWindow; Settings = settings; @@ -47,6 +49,7 @@ public class ModPanelTabBar Edit = edit; _modManager = modManager; _tutorial = tutorial; + Collections = collections; Tabs = new ITab[] { @@ -54,6 +57,7 @@ public class ModPanelTabBar Description, Conflicts, ChangedItems, + Collections, Edit, }; } @@ -83,6 +87,7 @@ public class ModPanelTabBar ModPanelTabType.Settings => Settings.Label, ModPanelTabType.ChangedItems => ChangedItems.Label, ModPanelTabType.Conflicts => Conflicts.Label, + ModPanelTabType.Collections => Collections.Label, ModPanelTabType.Edit => Edit.Label, _ => ReadOnlySpan.Empty, }; @@ -97,6 +102,8 @@ public class ModPanelTabBar return ModPanelTabType.ChangedItems; if (label == Conflicts.Label) return ModPanelTabType.Conflicts; + if (label == Collections.Label) + return ModPanelTabType.Collections; if (label == Edit.Label) return ModPanelTabType.Edit; @@ -108,7 +115,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((SubMod) mod.Default); + _modEditWindow.ChangeOption((SubMod)mod.Default); _modEditWindow.IsOpen = true; } From fba5bc68209d7c2614ca48a13e3cc9dcf36d6ad8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Apr 2023 18:44:53 +0200 Subject: [PATCH 0885/2451] Fix some bugs and start work on new collections tab. --- Penumbra.GameData/Enums/Race.cs | 10 + .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 123 ++- .../Collections/Manager/CollectionStorage.cs | 4 +- .../Collections/Manager/CollectionType.cs | 566 ++++++----- Penumbra/Collections/ModCollection.cs | 6 +- Penumbra/Interop/Services/FontReloader.cs | 37 +- Penumbra/Meta/Files/ImcFile.cs | 4 +- Penumbra/Meta/MetaFileManager.cs | 4 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 10 +- Penumbra/Mods/Manager/ModDataEditor.cs | 6 - Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Penumbra.cs | 4 - Penumbra/PenumbraNew.cs | 2 + Penumbra/Services/ConfigMigrationService.cs | 6 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 +- Penumbra/UI/Classes/Colors.cs | 2 + ...llectionSelector.cs => CollectionCombo.cs} | 4 +- ...lectionUi.cs => IndividualCollectionUi.cs} | 4 +- ...ions.InheritanceUi.cs => InheritanceUi.cs} | 0 .../{Collections.NpcCombo.cs => NpcCombo.cs} | 0 ...ctions.SpecialCombo.cs => SpecialCombo.cs} | 1 - ...ollections.WorldCombo.cs => WorldCombo.cs} | 0 Penumbra/UI/Tabs/CollectionsTab.cs | 891 ++++++++++++------ Penumbra/UI/Tabs/CollectionsTabOld.cs | 299 ++++++ Penumbra/UI/Tabs/ModsTab.cs | 70 +- 26 files changed, 1346 insertions(+), 717 deletions(-) rename Penumbra/UI/CollectionTab/{Collections.CollectionSelector.cs => CollectionCombo.cs} (85%) rename Penumbra/UI/CollectionTab/{Collections.IndividualCollectionUi.cs => IndividualCollectionUi.cs} (96%) rename Penumbra/UI/CollectionTab/{Collections.InheritanceUi.cs => InheritanceUi.cs} (100%) rename Penumbra/UI/CollectionTab/{Collections.NpcCombo.cs => NpcCombo.cs} (100%) rename Penumbra/UI/CollectionTab/{Collections.SpecialCombo.cs => SpecialCombo.cs} (95%) rename Penumbra/UI/CollectionTab/{Collections.WorldCombo.cs => WorldCombo.cs} (100%) create mode 100644 Penumbra/UI/Tabs/CollectionsTabOld.cs diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index 7f86cb6c..d1d859b7 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -221,6 +221,16 @@ public static class RaceEnumExtensions }; } + public static string ToShortName(this SubRace subRace) + { + return subRace switch + { + SubRace.SeekerOfTheSun => "Sunseeker", + SubRace.KeeperOfTheMoon => "Moonkeeper", + _ => subRace.ToName(), + }; + } + public static bool FitsRace(this SubRace subRace, Race race) => subRace.ToRace() == race; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 4e3bb0cf..a14facb4 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -43,7 +43,7 @@ public class CollectionCacheManager : IDisposable MetaFileManager = metaFileManager; _active = active; - _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 1e083d90..616640cf 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -13,24 +13,35 @@ using Penumbra.Util; namespace Penumbra.Collections.Manager; +public class ActiveCollectionData +{ + public ModCollection Current { get; internal set; } = ModCollection.Empty; + public ModCollection Default { get; internal set; } = ModCollection.Empty; + public ModCollection Interface { get; internal set; } = ModCollection.Empty; + + public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues().Length - 3]; +} + public class ActiveCollections : ISavable, IDisposable { public const int Version = 1; - private readonly CollectionStorage _storage; - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; + private readonly CollectionStorage _storage; + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + private readonly ActiveCollectionData _data; - public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) + public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService, ActiveCollectionData data) { _storage = storage; _communicator = communicator; _saveService = saveService; + _data = data; Current = storage.DefaultNamed; Default = storage.DefaultNamed; Interface = storage.DefaultNamed; Individuals = new IndividualCollections(actors.AwaitedService, config); - _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); LoadCollections(); UpdateCurrentCollectionInUse(); } @@ -39,16 +50,28 @@ public class ActiveCollections : ISavable, IDisposable => _communicator.CollectionChange.Unsubscribe(OnCollectionChange); /// The collection currently selected for changing settings. - public ModCollection Current { get; private set; } + public ModCollection Current + { + get => _data.Current; + private set => _data.Current = value; + } /// Whether the currently selected collection is used either directly via assignment or via inheritance. public bool CurrentCollectionInUse { get; private set; } /// The collection used for general file redirections and all characters not specifically named. - public ModCollection Default { get; private set; } + public ModCollection Default + { + get => _data.Default; + private set => _data.Default = value; + } /// The collection used for all files categorized as UI files. - public ModCollection Interface { get; private set; } + public ModCollection Interface + { + get => _data.Interface; + private set => _data.Interface = value; + } /// The list of individual assignments. public readonly IndividualCollections Individuals; @@ -58,16 +81,17 @@ public class ActiveCollections : ISavable, IDisposable => Individuals.TryGetCollection(identifier, out var c) ? c : Default; /// The list of group assignments. - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; + private ModCollection?[] SpecialCollections + => _data.SpecialCollections; /// Return all actually assigned group assignments. public IEnumerable> SpecialAssignments { get { - for (var i = 0; i < _specialCollections.Length; ++i) + for (var i = 0; i < SpecialCollections.Length; ++i) { - var collection = _specialCollections[i]; + var collection = SpecialCollections[i]; if (collection != null) yield return new KeyValuePair((CollectionType)i, collection); } @@ -82,7 +106,7 @@ public class ActiveCollections : ISavable, IDisposable public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) { if (type.IsSpecial()) - return _specialCollections[(int)type]; + return SpecialCollections[(int)type]; return type switch { @@ -94,13 +118,13 @@ public class ActiveCollections : ISavable, IDisposable }; } - /// Create a special collection if it does not exist and set it to Empty. + /// Create a special collection if it does not exist and set it to the current default. public bool CreateSpecialCollection(CollectionType collectionType) { - if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) + if (!collectionType.IsSpecial() || SpecialCollections[(int)collectionType] != null) return false; - _specialCollections[(int)collectionType] = Default; + SpecialCollections[(int)collectionType] = Default; _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); return true; } @@ -111,11 +135,11 @@ public class ActiveCollections : ISavable, IDisposable if (!collectionType.IsSpecial()) return; - var old = _specialCollections[(int)collectionType]; + var old = SpecialCollections[(int)collectionType]; if (old == null) return; - _specialCollections[(int)collectionType] = null; + SpecialCollections[(int)collectionType] = null; _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); } @@ -144,7 +168,38 @@ public class ActiveCollections : ISavable, IDisposable _saveService.QueueSave(this); } - /// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + /// Set and create an active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + public void SetCollection(ModCollection? collection, CollectionType collectionType, ActorIdentifier[] identifiers) + { + if (collectionType is CollectionType.Individual && identifiers.Length > 0 && identifiers[0].IsValid) + { + var idx = Individuals.Index(identifiers[0]); + if (idx >= 0) + { + if (collection == null) + RemoveIndividualCollection(idx); + else + SetCollection(collection, collectionType, idx); + } + else if (collection != null) + { + CreateIndividualCollection(identifiers); + SetCollection(collection, CollectionType.Individual, Individuals.Count - 1); + } + } + else + { + if (collection == null) + RemoveSpecialCollection(collectionType); + else + { + CreateSpecialCollection(collectionType); + SetCollection(collection, collectionType); + } + } + } + + /// Set an active collection, can be used to set Default, Current, Interface, Special, or Individual collections. public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) { var oldCollection = collectionType switch @@ -154,7 +209,7 @@ public class ActiveCollections : ISavable, IDisposable CollectionType.Current => Current, CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection, CollectionType.Individual => null, - _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType] ?? Default, + _ when collectionType.IsSpecial() => SpecialCollections[(int)collectionType] ?? Default, _ => null, }; @@ -178,7 +233,7 @@ public class ActiveCollections : ISavable, IDisposable break; default: - _specialCollections[(int)collectionType] = collection; + SpecialCollections[(int)collectionType] = collection; break; } @@ -205,7 +260,7 @@ public class ActiveCollections : ISavable, IDisposable { nameof(Interface), Interface.Name }, { nameof(Current), Current.Name }, }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null) .Select(p => ((CollectionType)p.Index, p.Value!))) jObj.Add(type.ToString(), collection.Name); @@ -215,7 +270,7 @@ public class ActiveCollections : ISavable, IDisposable } private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = _specialCollections + => CurrentCollectionInUse = SpecialCollections .OfType() .Prepend(Interface) .Prepend(Default) @@ -240,9 +295,9 @@ public class ActiveCollections : ISavable, IDisposable if (oldCollection == Current) SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); - for (var i = 0; i < _specialCollections.Length; ++i) + for (var i = 0; i < SpecialCollections.Length; ++i) { - if (oldCollection == _specialCollections[i]) + if (oldCollection == SpecialCollections[i]) SetCollection(ModCollection.Empty, (CollectionType)i); } @@ -329,7 +384,7 @@ public class ActiveCollections : ISavable, IDisposable } else { - _specialCollections[(int)type] = typeCollection; + SpecialCollections[(int)type] = typeCollection; } } } @@ -398,24 +453,6 @@ public class ActiveCollections : ISavable, IDisposable : string.Empty; } - break; - // The group of all Characters is redundant if they are all equal to Default or unassigned. - case CollectionType.MalePlayerCharacter: - case CollectionType.MaleNonPlayerCharacter: - case CollectionType.FemalePlayerCharacter: - case CollectionType.FemaleNonPlayerCharacter: - var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; - var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; - var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; - var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; - if (first.Index == second.Index - && first.Index == third.Index - && first.Index == fourth.Index - && first.Index == Default.Index) - return - "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" - + "You can keep just the Default Assignment."; - break; // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. case CollectionType.NonPlayerChild: diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index be8db099..bf273e1c 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -113,7 +113,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } - var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count); + var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); @@ -200,7 +200,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// Does not check for uniqueness. ///
private static bool IsValidName(string name) - => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); + => name.Length is > 0 and < 32 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); /// /// Read all collection files in the Collection Directory. diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index 52b48d9b..f08cf380 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -10,96 +10,96 @@ public enum CollectionType : byte // Special Collections Yourself = Api.Enums.ApiCollectionType.Yourself, - MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, - FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, - MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, + MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, + FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, + MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, - NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, - NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, + NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, + NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, - MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, - FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, - MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, + MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, + FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, + MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, - MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, - FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, - MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, + MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, + FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, + MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, - MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, + MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, - MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, - FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, + MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, + FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, - MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, - FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, - MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, + MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, + FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, - MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, - FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, - MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, + MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, + FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, + MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, - MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, - FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, - MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, + MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, + FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, + MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, - MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, + MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, - MaleLost = Api.Enums.ApiCollectionType.MaleLost, - FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, + MaleLost = Api.Enums.ApiCollectionType.MaleLost, + FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, - MaleRava = Api.Enums.ApiCollectionType.MaleRava, - FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, - MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, + MaleRava = Api.Enums.ApiCollectionType.MaleRava, + FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, + MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, - MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, - FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, - MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, + MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, + FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, + MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, - MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, - FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, - MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, + MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, + FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, + MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, - MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, + MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, - MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, - FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, + MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, + FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, - MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, - FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, - MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, + MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, - MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, - FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, - MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, + MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, + FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, + MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, - MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, - FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, - MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, + MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, + FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, + MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, - MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, + MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, - MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, - FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, + MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, + FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, - MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, - FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, - MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, + MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, + FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, + MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, - Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed + Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed - Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed + Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed Individual, // An individual collection was changed Inactive, // A collection was added or removed Temporary, // A temporary collections was set or deleted via IPC @@ -111,202 +111,200 @@ public static class CollectionTypeExtensions => collectionType < CollectionType.Default; public static readonly (CollectionType, string, string)[] Special = Enum.GetValues() - .Where(IsSpecial) - .Select(s => (s, s.ToName(), s.ToDescription())) - .ToArray(); + .Where(IsSpecial) + .Select(s => (s, s.ToName(), s.ToDescription())) + .ToArray(); public static CollectionType FromParts(Gender gender, bool npc) { gender = gender switch { - Gender.MaleNpc => Gender.Male, + Gender.MaleNpc => Gender.Male, Gender.FemaleNpc => Gender.Female, - _ => gender, + _ => gender, }; return (gender, npc) switch { - (Gender.Male, false) => CollectionType.MalePlayerCharacter, + (Gender.Male, false) => CollectionType.MalePlayerCharacter, (Gender.Female, false) => CollectionType.FemalePlayerCharacter, - (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, - (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, - _ => CollectionType.Inactive, + (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, + (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, + _ => CollectionType.Inactive, }; } - - // @formatter:off - private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; - private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; + + // @formatter:off + private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; + private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; // @formatter:on - - /// A list of definite redundancy possibilities. + + /// A list of definite redundancy possibilities. public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) => collectionType switch { - CollectionType.Yourself => DefaultList, - CollectionType.MalePlayerCharacter => DefaultList, - CollectionType.FemalePlayerCharacter => DefaultList, - CollectionType.MaleNonPlayerCharacter => DefaultList, + CollectionType.Yourself => DefaultList, + CollectionType.MalePlayerCharacter => DefaultList, + CollectionType.FemalePlayerCharacter => DefaultList, + CollectionType.MaleNonPlayerCharacter => DefaultList, CollectionType.FemaleNonPlayerCharacter => DefaultList, - CollectionType.MaleMidlander => MalePlayerList, - CollectionType.FemaleMidlander => FemalePlayerList, - CollectionType.MaleHighlander => MalePlayerList, - CollectionType.FemaleHighlander => FemalePlayerList, - CollectionType.MaleWildwood => MalePlayerList, - CollectionType.FemaleWildwood => FemalePlayerList, - CollectionType.MaleDuskwight => MalePlayerList, - CollectionType.FemaleDuskwight => FemalePlayerList, - CollectionType.MalePlainsfolk => MalePlayerList, - CollectionType.FemalePlainsfolk => FemalePlayerList, - CollectionType.MaleDunesfolk => MalePlayerList, - CollectionType.FemaleDunesfolk => FemalePlayerList, - CollectionType.MaleSeekerOfTheSun => MalePlayerList, - CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, - CollectionType.MaleKeeperOfTheMoon => MalePlayerList, - CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, - CollectionType.MaleSeawolf => MalePlayerList, - CollectionType.FemaleSeawolf => FemalePlayerList, - CollectionType.MaleHellsguard => MalePlayerList, - CollectionType.FemaleHellsguard => FemalePlayerList, - CollectionType.MaleRaen => MalePlayerList, - CollectionType.FemaleRaen => FemalePlayerList, - CollectionType.MaleXaela => MalePlayerList, - CollectionType.FemaleXaela => FemalePlayerList, - CollectionType.MaleHelion => MalePlayerList, - CollectionType.FemaleHelion => FemalePlayerList, - CollectionType.MaleLost => MalePlayerList, - CollectionType.FemaleLost => FemalePlayerList, - CollectionType.MaleRava => MalePlayerList, - CollectionType.FemaleRava => FemalePlayerList, - CollectionType.MaleVeena => MalePlayerList, - CollectionType.FemaleVeena => FemalePlayerList, - CollectionType.MaleMidlanderNpc => MaleNpcList, - CollectionType.FemaleMidlanderNpc => FemaleNpcList, - CollectionType.MaleHighlanderNpc => MaleNpcList, - CollectionType.FemaleHighlanderNpc => FemaleNpcList, - CollectionType.MaleWildwoodNpc => MaleNpcList, - CollectionType.FemaleWildwoodNpc => FemaleNpcList, - CollectionType.MaleDuskwightNpc => MaleNpcList, - CollectionType.FemaleDuskwightNpc => FemaleNpcList, - CollectionType.MalePlainsfolkNpc => MaleNpcList, - CollectionType.FemalePlainsfolkNpc => FemaleNpcList, - CollectionType.MaleDunesfolkNpc => MaleNpcList, - CollectionType.FemaleDunesfolkNpc => FemaleNpcList, - CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, - CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, - CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, + CollectionType.MaleMidlander => MalePlayerList, + CollectionType.FemaleMidlander => FemalePlayerList, + CollectionType.MaleHighlander => MalePlayerList, + CollectionType.FemaleHighlander => FemalePlayerList, + CollectionType.MaleWildwood => MalePlayerList, + CollectionType.FemaleWildwood => FemalePlayerList, + CollectionType.MaleDuskwight => MalePlayerList, + CollectionType.FemaleDuskwight => FemalePlayerList, + CollectionType.MalePlainsfolk => MalePlayerList, + CollectionType.FemalePlainsfolk => FemalePlayerList, + CollectionType.MaleDunesfolk => MalePlayerList, + CollectionType.FemaleDunesfolk => FemalePlayerList, + CollectionType.MaleSeekerOfTheSun => MalePlayerList, + CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, + CollectionType.MaleKeeperOfTheMoon => MalePlayerList, + CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, + CollectionType.MaleSeawolf => MalePlayerList, + CollectionType.FemaleSeawolf => FemalePlayerList, + CollectionType.MaleHellsguard => MalePlayerList, + CollectionType.FemaleHellsguard => FemalePlayerList, + CollectionType.MaleRaen => MalePlayerList, + CollectionType.FemaleRaen => FemalePlayerList, + CollectionType.MaleXaela => MalePlayerList, + CollectionType.FemaleXaela => FemalePlayerList, + CollectionType.MaleHelion => MalePlayerList, + CollectionType.FemaleHelion => FemalePlayerList, + CollectionType.MaleLost => MalePlayerList, + CollectionType.FemaleLost => FemalePlayerList, + CollectionType.MaleRava => MalePlayerList, + CollectionType.FemaleRava => FemalePlayerList, + CollectionType.MaleVeena => MalePlayerList, + CollectionType.FemaleVeena => FemalePlayerList, + CollectionType.MaleMidlanderNpc => MaleNpcList, + CollectionType.FemaleMidlanderNpc => FemaleNpcList, + CollectionType.MaleHighlanderNpc => MaleNpcList, + CollectionType.FemaleHighlanderNpc => FemaleNpcList, + CollectionType.MaleWildwoodNpc => MaleNpcList, + CollectionType.FemaleWildwoodNpc => FemaleNpcList, + CollectionType.MaleDuskwightNpc => MaleNpcList, + CollectionType.FemaleDuskwightNpc => FemaleNpcList, + CollectionType.MalePlainsfolkNpc => MaleNpcList, + CollectionType.FemalePlainsfolkNpc => FemaleNpcList, + CollectionType.MaleDunesfolkNpc => MaleNpcList, + CollectionType.FemaleDunesfolkNpc => FemaleNpcList, + CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, + CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, + CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, - CollectionType.MaleSeawolfNpc => MaleNpcList, - CollectionType.FemaleSeawolfNpc => FemaleNpcList, - CollectionType.MaleHellsguardNpc => MaleNpcList, - CollectionType.FemaleHellsguardNpc => FemaleNpcList, - CollectionType.MaleRaenNpc => MaleNpcList, - CollectionType.FemaleRaenNpc => FemaleNpcList, - CollectionType.MaleXaelaNpc => MaleNpcList, - CollectionType.FemaleXaelaNpc => FemaleNpcList, - CollectionType.MaleHelionNpc => MaleNpcList, - CollectionType.FemaleHelionNpc => FemaleNpcList, - CollectionType.MaleLostNpc => MaleNpcList, - CollectionType.FemaleLostNpc => FemaleNpcList, - CollectionType.MaleRavaNpc => MaleNpcList, - CollectionType.FemaleRavaNpc => FemaleNpcList, - CollectionType.MaleVeenaNpc => MaleNpcList, - CollectionType.FemaleVeenaNpc => FemaleNpcList, - CollectionType.Individual => DefaultList, - _ => Array.Empty(), + CollectionType.MaleSeawolfNpc => MaleNpcList, + CollectionType.FemaleSeawolfNpc => FemaleNpcList, + CollectionType.MaleHellsguardNpc => MaleNpcList, + CollectionType.FemaleHellsguardNpc => FemaleNpcList, + CollectionType.MaleRaenNpc => MaleNpcList, + CollectionType.FemaleRaenNpc => FemaleNpcList, + CollectionType.MaleXaelaNpc => MaleNpcList, + CollectionType.FemaleXaelaNpc => FemaleNpcList, + CollectionType.MaleHelionNpc => MaleNpcList, + CollectionType.FemaleHelionNpc => FemaleNpcList, + CollectionType.MaleLostNpc => MaleNpcList, + CollectionType.FemaleLostNpc => FemaleNpcList, + CollectionType.MaleRavaNpc => MaleNpcList, + CollectionType.FemaleRavaNpc => FemaleNpcList, + CollectionType.MaleVeenaNpc => MaleNpcList, + CollectionType.FemaleVeenaNpc => FemaleNpcList, + CollectionType.Individual => DefaultList, + _ => Array.Empty(), }; public static CollectionType FromParts(SubRace race, Gender gender, bool npc) { gender = gender switch { - Gender.MaleNpc => Gender.Male, + Gender.MaleNpc => Gender.Male, Gender.FemaleNpc => Gender.Female, - _ => gender, + _ => gender, }; return (race, gender, npc) switch { - (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, - (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, - (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, - (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, - (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, - (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, + (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, + (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, + (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, + (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, + (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, + (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, - (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, - (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, - (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, - (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, - (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, - (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, - (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, + (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, + (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, + (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, + (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, + (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, + (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, + (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, + (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, - (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, - (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, - (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, - (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, - (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, - (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, + (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, + (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, + (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, + (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, + (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, + (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, - (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, - (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, - (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, - (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, - (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, - (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, - (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, + (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, + (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, + (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, + (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, + (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, + (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, + (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, + (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, - (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, - (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, - (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, - (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, + (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, + (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, + (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, + (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, - (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, - (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, - (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, - (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, - (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, - (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, + (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, + (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, + (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, + (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, + (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, + (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, + (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, - (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, - (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, - (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, - (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, + (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, + (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, + (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, + (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, - (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, - (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, - (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, - (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, - (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, - (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, - _ => CollectionType.Inactive, + (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, + (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, + (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, + (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, + (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, + (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, + (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, + _ => CollectionType.Inactive, }; } public static bool TryParse(string text, out CollectionType type) { if (Enum.TryParse(text, true, out type)) - { return type is not CollectionType.Inactive and not CollectionType.Temporary; - } if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase)) { @@ -335,9 +333,7 @@ public static class CollectionTypeExtensions foreach (var t in Enum.GetValues()) { if (t is CollectionType.Inactive or CollectionType.Temporary) - { continue; - } if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase)) { @@ -352,83 +348,83 @@ public static class CollectionTypeExtensions public static string ToName(this CollectionType collectionType) => collectionType switch { - CollectionType.Yourself => "Your Character", - CollectionType.NonPlayerChild => "Non-Player Children", - CollectionType.NonPlayerElderly => "Non-Player Elderly", - CollectionType.MalePlayerCharacter => "Male Player Characters", - CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", - CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", - CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", - CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", - CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", - CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", - CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", - CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", - CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", - CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", - CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", - CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", - CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", - CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", - CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", - CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", - CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", - CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", - CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", - CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", - CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", - CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", - CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", - CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", - CollectionType.FemalePlayerCharacter => "Female Player Characters", + CollectionType.Yourself => "Your Character", + CollectionType.NonPlayerChild => "Non-Player Children", + CollectionType.NonPlayerElderly => "Non-Player Elderly", + CollectionType.MalePlayerCharacter => "Male Player Characters", + CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", + CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", + CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", + CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", + CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", + CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", + CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", + CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", + CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", + CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", + CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", + CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", + CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", + CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", + CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", + CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", + CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", + CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", + CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", + CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", + CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", + CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", + CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", + CollectionType.FemalePlayerCharacter => "Female Player Characters", CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", - CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", - CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", - CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", - CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", - CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", - CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", - CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", - CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", - CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", - CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", - CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", - CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", - CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", - CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", - CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", - CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", - CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", + CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", + CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", + CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", + CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", + CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", + CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", + CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", + CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", + CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", + CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", + CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", + CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", + CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", + CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", + CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", + CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", - CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", - CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", - CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", - CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", - CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", - CollectionType.Inactive => "Collection", - CollectionType.Default => "Default", - CollectionType.Interface => "Interface", - CollectionType.Individual => "Individual", - CollectionType.Current => "Current", - _ => string.Empty, + CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", + CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", + CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", + CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", + CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", + CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", + CollectionType.Inactive => "Collection", + CollectionType.Default => "Base", + CollectionType.Interface => "Interface", + CollectionType.Individual => "Individual", + CollectionType.Current => "Current", + _ => string.Empty, }; public static string ToDescription(this CollectionType collectionType) @@ -580,4 +576,4 @@ public static class CollectionTypeExtensions "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", _ => string.Empty, }; -} \ No newline at end of file +} diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 84e47897..a2b201cd 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -28,7 +28,7 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0); + public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); /// The name of a collection can not contain characters invalid in a path. public string Name { get; internal init; } @@ -133,10 +133,10 @@ public partial class ModCollection } /// Constructor for empty collections. - public static ModCollection CreateEmpty(string name, int index) + public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(name, index, 0, CurrentVersion, new List(), new List(), + return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?) null, modCount).ToList(), new List(), new Dictionary()); } diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 51f8abc2..76a205dc 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -1,12 +1,12 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.GameData; - + namespace Penumbra.Interop.Services; -/// +/// /// Handle font reloading via game functions. -/// May cause a interface flicker while reloading. +/// May cause a interface flicker while reloading. /// public unsafe class FontReloader { @@ -21,24 +21,27 @@ public unsafe class FontReloader Penumbra.Log.Error("Could not reload fonts, function could not be found."); } - private readonly AtkModule* _atkModule = null!; - private readonly delegate* unmanaged _reloadFontsFunc = null!; + private AtkModule* _atkModule = null!; + private delegate* unmanaged _reloadFontsFunc = null!; - public FontReloader() + public FontReloader(Dalamud.Game.Framework dFramework) { - var framework = Framework.Instance(); - if (framework == null) - return; + dFramework.RunOnFrameworkThread(() => + { + var framework = Framework.Instance(); + if (framework == null) + return; - var uiModule = framework->GetUiModule(); - if (uiModule == null) - return; + var uiModule = framework->GetUiModule(); + if (uiModule == null) + return; - var atkModule = uiModule->GetRaptureAtkModule(); - if (atkModule == null) - return; + var atkModule = uiModule->GetRaptureAtkModule(); + if (atkModule == null) + return; - _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; + _atkModule = &atkModule->AtkModule; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; + }); } } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 2dc60120..79313a6f 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -178,7 +178,7 @@ public unsafe class ImcFile : MetaBaseFile public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - var newData = Penumbra.MetaFileManager.AllocateDefaultMemory(ActualLength, 8); + var newData = Manager.AllocateDefaultMemory(ActualLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); @@ -187,7 +187,7 @@ public unsafe class ImcFile : MetaBaseFile MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); - Penumbra.MetaFileManager.Free(data, length); + Manager.Free(data, length); resource->SetData((IntPtr)newData, ActualLength); } } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 72ebac34..53b35559 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -20,12 +20,12 @@ public unsafe class MetaFileManager internal readonly CharacterUtility CharacterUtility; internal readonly ResidentResourceManager ResidentResources; internal readonly DataManager GameData; - internal readonly ActiveCollections ActiveCollections; + internal readonly ActiveCollectionData ActiveCollections; internal readonly ValidityChecker ValidityChecker; internal readonly IdentifierService Identifier; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData, - ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) + ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) { CharacterUtility = characterUtility; ResidentResources = residentResources; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 09283dc7..c793c68f 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -8,16 +8,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Penumbra.GameData; using Penumbra.Meta; using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; public class ItemSwapContainer { private readonly MetaFileManager _manager; - private readonly IObjectIdentifier _identifier; + private readonly IdentifierService _identifier; private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); private HashSet< MetaManipulation > _modManipulations = new(); @@ -114,7 +114,7 @@ public class ItemSwapContainer } } - public ItemSwapContainer(MetaFileManager manager, IObjectIdentifier identifier) + public ItemSwapContainer(MetaFileManager manager, IdentifierService identifier) { _manager = manager; _identifier = identifier; @@ -136,7 +136,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); + var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); Loaded = true; return ret; } @@ -145,7 +145,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); Loaded = true; return ret; } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 15fc5e92..d8116998 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -38,12 +38,6 @@ public class ModDataEditor _communicatorService = communicatorService; } - public string MetaFile(Mod mod) - => _saveService.FileNames.ModMetaPath(mod); - - public string DataFile(Mod mod) - => _saveService.FileNames.LocalDataFile(mod); - /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, string? website) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index b0add38f..806d1a1b 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -220,7 +220,7 @@ public partial class ModCreator if (!file.Exists) continue; - var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName), + var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), _config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2ff848d8..8a936998 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -41,7 +41,6 @@ public class Penumbra : IDalamudPlugin public static Configuration Config { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static MetaFileManager MetaFileManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; public static ModCacheManager ModCaches { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; @@ -72,7 +71,6 @@ public class Penumbra : IDalamudPlugin _tmp.Services.GetRequiredService(); Config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); Actors = _tmp.Services.GetRequiredService().AwaitedService; _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); @@ -114,8 +112,6 @@ public class Penumbra : IDalamudPlugin var api = _tmp.Services.GetRequiredService(); HttpApi = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - if (Config.EnableHttpApi) - HttpApi.CreateWebServer(); api.ChangedItemTooltip += it => { if (it is Item) diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index d32bed09..5cd501af 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -84,6 +84,7 @@ public class PenumbraNew // Add Collection Services services.AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -139,6 +140,7 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index fde8ea62..2d525c00 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -252,11 +252,11 @@ public class ConfigMigrationService using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); - j.WritePropertyName(nameof(ActiveCollections.Default)); + j.WritePropertyName(nameof(ActiveCollectionData.Default)); j.WriteValue(def); - j.WritePropertyName(nameof(ActiveCollections.Interface)); + j.WritePropertyName(nameof(ActiveCollectionData.Interface)); j.WriteValue(ui); - j.WritePropertyName(nameof(ActiveCollections.Current)); + j.WritePropertyName(nameof(ActiveCollectionData.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index d602ed3c..22bca756 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -30,19 +30,17 @@ public class ItemSwapTab : IDisposable, ITab private readonly ItemService _itemService; private readonly CollectionManager _collectionManager; private readonly ModManager _modManager; - private readonly Configuration _config; private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, Configuration config, IdentifierService identifier, MetaFileManager metaFileManager) + ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager) { _communicator = communicator; _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; - _config = config; _metaFileManager = metaFileManager; - _swapData = new ItemSwapContainer(metaFileManager, identifier.AwaitedService); + _swapData = new ItemSwapContainer(metaFileManager, identifier); _selectors = new Dictionary { diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 3edc0e9c..b2c65c45 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -27,6 +27,8 @@ public static class Colors public const uint MetaInfoText = 0xAAFFFFFF; public const uint RedTableBgTint = 0x40000080; public const uint DiscordColor = 0xFFDA8972; + public const uint SelectedColor = 0x6069C056; + public const uint RedundantColor = 0x6050D0D0; public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF; diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs similarity index 85% rename from Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs rename to Penumbra/UI/CollectionTab/CollectionCombo.cs index 41aa1437..b7d379cf 100644 --- a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -8,11 +8,11 @@ using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; -public sealed class CollectionSelector : FilterComboCache +public sealed class CollectionCombo : FilterComboCache { private readonly CollectionManager _collectionManager; - public CollectionSelector(CollectionManager manager, Func> items) + public CollectionCombo(CollectionManager manager, Func> items) : base(items) => _collectionManager = manager; diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/IndividualCollectionUi.cs similarity index 96% rename from Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs rename to Penumbra/UI/CollectionTab/IndividualCollectionUi.cs index 7867f2b3..226c9c8b 100644 --- a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualCollectionUi.cs @@ -18,9 +18,9 @@ public class IndividualCollectionUi { private readonly ActorService _actorService; private readonly CollectionManager _collectionManager; - private readonly CollectionSelector _withEmpty; + private readonly CollectionCombo _withEmpty; - public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty) + public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionCombo withEmpty) { _actorService = actors; _collectionManager = collectionManager; diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs similarity index 100% rename from Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs rename to Penumbra/UI/CollectionTab/InheritanceUi.cs diff --git a/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs b/Penumbra/UI/CollectionTab/NpcCombo.cs similarity index 100% rename from Penumbra/UI/CollectionTab/Collections.NpcCombo.cs rename to Penumbra/UI/CollectionTab/NpcCombo.cs diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/SpecialCombo.cs similarity index 95% rename from Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs rename to Penumbra/UI/CollectionTab/SpecialCombo.cs index 91b7f491..ab332399 100644 --- a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs +++ b/Penumbra/UI/CollectionTab/SpecialCombo.cs @@ -1,7 +1,6 @@ using ImGuiNET; using OtterGui.Classes; using OtterGui.Widgets; -using Penumbra.Collections; using Penumbra.Collections.Manager; namespace Penumbra.UI.CollectionTab; diff --git a/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs b/Penumbra/UI/CollectionTab/WorldCombo.cs similarity index 100% rename from Penumbra/UI/CollectionTab/Collections.WorldCombo.cs rename to Penumbra/UI/CollectionTab/WorldCombo.cs diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 7f46ee07..5370e16e 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,299 +1,592 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Services; -using Penumbra.UI.CollectionTab; - -namespace Penumbra.UI.Tabs; - -public class CollectionsTab : IDisposable, ITab -{ - private readonly CommunicatorService _communicator; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TutorialService _tutorial; - private readonly SpecialCombo _specialCollectionCombo; - - private readonly CollectionSelector _collectionsWithEmpty; - private readonly CollectionSelector _collectionSelector; - private readonly InheritanceUi _inheritance; - private readonly IndividualCollectionUi _individualCollections; - - public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager, - TutorialService tutorial, Configuration config) - { - _communicator = communicator; - _collectionManager = collectionManager; - _tutorial = tutorial; - _config = config; - _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); - _collectionsWithEmpty = new CollectionSelector(_collectionManager, - () => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); - _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); - _inheritance = new InheritanceUi(_collectionManager); - _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); - - _communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); - } - - public ReadOnlySpan Label - => "Collections"u8; - - /// Draw a collection selector of a certain width for a certain type. - public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) - => (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); - - public void Dispose() - => _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); - - /// Draw a tutorial step regardless of tab selection. - public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); - - public void DrawContent() - { - using var child = ImRaii.Child("##collections", -Vector2.One); - if (child) - { - DrawActiveCollectionSelectors(); - DrawMainSelectors(); - } - } - - #region New Collections - - // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection; - - /// - /// Create a new collection that is either empty or a duplicate of the current collection. - /// Resets the new collection name. - /// - private void CreateNewCollection(bool duplicate) - { - if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null)) - _newCollectionName = string.Empty; - } - - /// Draw the Clean Unused Settings button if there are any. - private void DrawCleanCollectionButton(Vector2 width) - { - if (_collectionManager.Active.Current.UnusedSettings.Count == 0) - return; - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton( - $"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width - , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." - , false)) - _collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current); - } - - /// Draw the new collection input as well as its buttons. - private void DrawNewCollectionInput(Vector2 width) - { - // Input for new collection name. Also checks for validity when changed. - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) - _canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of enabled mods."); - - // Creation buttons. - var tt = _canAddCollection - ? string.Empty - : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; - if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) - CreateNewCollection(false); - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection)) - CreateNewCollection(true); - } - - #endregion - - #region Collection Selection - - /// Draw all collection assignment selections. - private void DrawActiveCollectionSelectors() - { - UiHelpers.DefaultLineSpace(); - var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); - _tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); - if (!open) - return; - - UiHelpers.DefaultLineSpace(); - - DrawDefaultCollectionSelector(); - _tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); - DrawInterfaceCollectionSelector(); - _tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); - UiHelpers.DefaultLineSpace(); - - DrawSpecialAssignments(); - _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); - UiHelpers.DefaultLineSpace(); - - _individualCollections.Draw(); - _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); - UiHelpers.DefaultLineSpace(); - } - - private void DrawCurrentCollectionSelector(Vector2 width) - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection, - "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); - - // Deletion conditions. - var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; - var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = deleteCondition - ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollectionName}."; - - if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) - _collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); - - DrawCleanCollectionButton(width); - } - - /// Draw the selector for the default collection assignment. - private void DrawDefaultCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, - $"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," - + "as well as any character for whom no more specific conditions from below apply."); - } - - /// Draw the selector for the interface collection assignment. - private void DrawInterfaceCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, - $"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); - } - - /// Description for character groups used in multiple help markers. - private const string CharacterGroupDescription = - $"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {TutorialService.DefaultCollection},\n" - + $"but all {TutorialService.IndividualAssignments} take precedence before them."; - - /// Draw the entire group assignment section. - private void DrawSpecialAssignments() - { - using var _ = ImRaii.Group(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(TutorialService.CharacterGroups); - ImGuiComponents.HelpMarker(CharacterGroupDescription); - ImGui.Separator(); - DrawSpecialCollections(); - ImGui.Dummy(Vector2.Zero); - DrawNewSpecialCollection(); - } - - /// Draw a new combo to select special collections as well as button to create it. - private void DrawNewSpecialCollection() - { - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - if (_specialCollectionCombo.CurrentIdx == -1 - || _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) - { - _specialCollectionCombo.ResetFilter(); - _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); - } - - if (_specialCollectionCombo.CurrentType == null) - return; - - _specialCollectionCombo.Draw(); - ImGui.SameLine(); - var disabled = _specialCollectionCombo.CurrentType == null; - var tt = disabled - ? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" - + CharacterGroupDescription - : CharacterGroupDescription; - if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) - return; - - _collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); - _specialCollectionCombo.CurrentIdx = -1; - } - - #endregion - - #region Current Collection Editing - - /// Draw the current collection selection, the creation of new collections and the inheritance block. - private void DrawMainSelectors() - { - UiHelpers.DefaultLineSpace(); - var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen); - _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); - if (!open) - return; - - var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); - UiHelpers.DefaultLineSpace(); - - DrawCurrentCollectionSelector(width); - _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); - UiHelpers.DefaultLineSpace(); - - DrawNewCollectionInput(width); - UiHelpers.DefaultLineSpace(); - - _inheritance.Draw(); - _tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); - } - - /// Draw all currently set special collections. - private void DrawSpecialCollections() - { - foreach (var (type, name, desc) in CollectionTypeExtensions.Special) - { - var collection = _collectionManager.Active.ByType(type); - if (collection == null) - continue; - - using var id = ImRaii.PushId((int)type); - DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, - false, true)) - { - _collectionManager.Active.RemoveSpecialCollection(type); - _specialCollectionCombo.ResetFilter(); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker(name, desc); - } - } - - #endregion -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; +using Penumbra.UI.CollectionTab; + +namespace Penumbra.UI.Tabs; + +public sealed class CollectionTree +{ + private readonly CollectionStorage _collections; + private readonly ActiveCollections _active; + private readonly CollectionSelector2 _selector; + private readonly ActorService _actors; + private readonly TargetManager _targets; + + private static readonly IReadOnlyList<(string Name, uint Border)> Buttons = CreateButtons(); + private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); + + public CollectionTree(CollectionManager manager, CollectionSelector2 selector, ActorService actors, + TargetManager targets) + { + _collections = manager.Storage; + _active = manager.Active; + _selector = selector; + _actors = actors; + _targets = targets; + } + + public void DrawSimple() + { + var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + DrawSimpleCollectionButton(CollectionType.Default, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); + + var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale }; + var player = _actors.AwaitedService.GetCurrentPlayer(); + DrawButton($"Current Character ({(player.IsValid ? player.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, + player); + ImGui.SameLine(); + + var target = _actors.AwaitedService.FromObject(_targets.Target, false, true, true); + DrawButton($"Current Target ({(target.IsValid ? target.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, target); + if (_active.Individuals.Count > 0) + { + ImGui.TextUnformatted("Currently Active Individual Assignments"); + for (var i = 0; i < _active.Individuals.Count; ++i) + { + var (name, ids, coll) = _active.Individuals.Assignments[i]; + DrawButton(name, CollectionType.Individual, buttonWidth, 0, ids[0], coll); + + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + && i < _active.Individuals.Count - 1) + ImGui.NewLine(); + } + + ImGui.NewLine(); + } + + var first = true; + + void Button(CollectionType type) + { + var (name, border) = Buttons[(int)type]; + var collection = _active.ByType(type); + if (collection == null) + return; + + if (first) + { + ImGui.Separator(); + ImGui.TextUnformatted("Currently Active Advanced Assignments"); + first = false; + } + DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, collection); + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) + ImGui.NewLine(); + } + + Button(CollectionType.NonPlayerChild); + Button(CollectionType.NonPlayerElderly); + foreach (var race in Enum.GetValues().Skip(1)) + { + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); + } + } + + public void DrawAdvanced() + { + using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + + var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + var dummy = new Vector2(1, 0); + + foreach (var (type, pre, post, name, border) in AdvancedTree) + { + ImGui.TableNextColumn(); + if (type is CollectionType.Inactive) + continue; + + if (pre) + ImGui.Dummy(dummy); + DrawAssignmentButton(type, buttonWidth, name, border); + if (post) + ImGui.Dummy(dummy); + } + } + + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix = 'i') + { + var label = $"{type}{identifier}{suffix}"; + if (open) + ImGui.OpenPopup(label); + + using var context = ImRaii.Popup(label); + if (context) + { + using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor)) + { + if (ImGui.MenuItem("Use no mods.")) + _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); + } + + if (collection != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + if (ImGui.MenuItem("Remove this assignment.")) + _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); + } + + foreach (var coll in _collections) + { + if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) + _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); + } + } + } + + private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, ModCollection? collection = null) + { + using var group = ImRaii.Group(); + var invalid = type == CollectionType.Individual && !id.IsValid; + var redundancy = _active.RedundancyCheck(type, id); + collection ??= _active.ByType(type, id); + using var color = ImRaii.PushColor(ImGuiCol.Button, + collection == null + ? 0 + : redundancy.Length > 0 + ? Colors.RedundantColor + : collection == _active.Current + ? Colors.SelectedColor + : collection == ModCollection.Empty + ? Colors.RedTableBgTint + : ImGui.GetColorU32(ImGuiCol.Button), !invalid) + .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); + using var disabled = ImRaii.Disabled(invalid); + var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); + var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); + if (!invalid) + { + _selector.DragTarget(type, id); + var name = collection == ModCollection.Empty ? "Use No Mods" : collection?.Name ?? "Unassigned"; + var size = ImGui.CalcTextSize(name); + var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; + ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); + DrawContext(button, collection, type, id); + } + + if (hovered) + ImGui.SetTooltip(redundancy); + + return button; + } + + private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) + { + DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid); + ImGui.SameLine(); + var secondLine = string.Empty; + foreach (var parent in type.InheritanceOrder()) + { + var coll = _active.ByType(parent); + if (coll == null) + continue; + + secondLine = $"\nWill behave as {parent.ToName()} ({coll.Name}) while unassigned."; + break; + } + + ImGui.TextUnformatted(type.ToDescription() + secondLine); + ImGui.Separator(); + } + + private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color) + => DrawButton(name, type, width, color, ActorIdentifier.Invalid, _active.ByType(type)); + + private static IReadOnlyList<(string Name, uint Border)> CreateButtons() + { + var ret = Enum.GetValues().Select(t => (t.ToName(), 0u)).ToArray(); + + foreach (var race in Enum.GetValues().Skip(1)) + { + var color = race switch + { + SubRace.Midlander => 0xAA5C9FE4u, + SubRace.Highlander => 0xAA5C9FE4u, + SubRace.Wildwood => 0xAA5C9F49u, + SubRace.Duskwight => 0xAA5C9F49u, + SubRace.Plainsfolk => 0xAAEF8CB6u, + SubRace.Dunesfolk => 0xAAEF8CB6u, + SubRace.SeekerOfTheSun => 0xAA8CEFECu, + SubRace.KeeperOfTheMoon => 0xAA8CEFECu, + SubRace.Seawolf => 0xAAEFE68Cu, + SubRace.Hellsguard => 0xAAEFE68Cu, + SubRace.Raen => 0xAAB5EF8Cu, + SubRace.Xaela => 0xAAB5EF8Cu, + SubRace.Helion => 0xAAFFFFFFu, + SubRace.Lost => 0xAAFFFFFFu, + SubRace.Rava => 0xAA607FA7u, + SubRace.Veena => 0xAA607FA7u, + _ => 0u, + }; + + ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color); + ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color); + ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color); + ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color); + } + + ret[(int)CollectionType.MalePlayerCharacter] = ("♂ Player", 0); + ret[(int)CollectionType.FemalePlayerCharacter] = ("♀ Player", 0); + ret[(int)CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0); + ret[(int)CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0); + return ret; + } + + private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree() + { + var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count); + + void Add(CollectionType type, bool pre, bool post) + { + var (name, border) = (int)type >= Buttons.Count ? (type.ToName(), 0) : Buttons[(int)type]; + ret.Add((type, pre, post, name, border)); + } + + Add(CollectionType.Default, false, false); + Add(CollectionType.Interface, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Yourself, false, true); + Add(CollectionType.Inactive, false, true); + Add(CollectionType.NonPlayerChild, false, true); + Add(CollectionType.NonPlayerElderly, false, true); + Add(CollectionType.MalePlayerCharacter, true, true); + Add(CollectionType.FemalePlayerCharacter, true, true); + Add(CollectionType.MaleNonPlayerCharacter, true, true); + Add(CollectionType.FemaleNonPlayerCharacter, true, true); + var pre = true; + foreach (var race in Enum.GetValues().Skip(1)) + { + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre); + pre = !pre; + } + + return ret; + } +} + +public sealed class CollectionPanel +{ + private readonly CollectionManager _manager; + private readonly ModStorage _modStorage; + private readonly InheritanceUi _inheritanceUi; + + public CollectionPanel(CollectionManager manager, ModStorage modStorage) + { + _manager = manager; + _modStorage = modStorage; + _inheritanceUi = new InheritanceUi(_manager); + } + + public void Draw() + { + var collection = _manager.Active.Current; + DrawName(collection); + DrawStatistics(collection); + _inheritanceUi.Draw(); + DrawSettingsList(collection); + DrawInactiveSettingsList(collection); + } + + private void DrawName(ModCollection collection) + { + ImGui.TextUnformatted($"{collection.Name} ({collection.AnonymizedName})"); + } + + private void DrawStatistics(ModCollection collection) + { + ImGui.TextUnformatted("Used for:"); + var sb = new StringBuilder(128); + if (_manager.Active.Default == collection) + sb.Append(CollectionType.Default.ToName()).Append(", "); + if (_manager.Active.Interface == collection) + sb.Append(CollectionType.Interface.ToName()).Append(", "); + foreach (var (type, _) in _manager.Active.SpecialAssignments.Where(p => p.Value == collection)) + sb.Append(type.ToName()).Append(", "); + foreach (var (name, _) in _manager.Active.Individuals.Where(p => p.Collection == collection)) + sb.Append(name).Append(", "); + + ImGui.SameLine(); + ImGuiUtil.TextWrapped(sb.Length == 0 ? "Nothing" : sb.ToString(0, sb.Length - 2)); + + if (collection.DirectParentOf.Count > 0) + { + ImGui.TextUnformatted("Inherited by:"); + ImGui.SameLine(); + ImGuiUtil.TextWrapped(string.Join(", ", collection.DirectParentOf.Select(c => c.Name))); + } + } + + private void DrawSettingsList(ModCollection collection) + { + using var box = ImRaii.ListBox("##activeSettings"); + if (!box) + return; + + foreach (var (mod, (settings, parent)) in _modStorage.Select(m => (m, collection[m.Index])).Where(t => t.Item2.Settings != null) + .OrderBy(t => t.m.Name)) + ImGui.TextUnformatted($"{mod}{(parent != collection ? $" (inherited from {parent.Name})" : string.Empty)}"); + } + + private void DrawInactiveSettingsList(ModCollection collection) + { + if (collection.UnusedSettings.Count == 0) + return; + + if (ImGui.Button("Clear Unused Settings")) + _manager.Storage.CleanUnavailableSettings(collection); + + using var box = ImRaii.ListBox("##inactiveSettings"); + if (!box) + return; + + foreach (var name in collection.UnusedSettings.Keys) + ImGui.TextUnformatted(name); + } +} + +public sealed class CollectionSelector2 : ItemSelector, IDisposable +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; + private readonly ActiveCollections _active; + + private ModCollection? _dragging; + + public CollectionSelector2(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active) + : base(new List(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) + { + _config = config; + _communicator = communicator; + _storage = storage; + _active = active; + + _communicator.CollectionChange.Subscribe(OnCollectionChange); + // Set items. + OnCollectionChange(CollectionType.Inactive, null, null, string.Empty); + // Set selection. + OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty); + } + + protected override bool OnDelete(int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + return _storage.RemoveCollection(Items[idx]); + } + + protected override bool DeleteButtonEnabled() + => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); + + protected override string DeleteButtonTooltip() + => _storage.DefaultNamed == Current + ? $"The selected collection {Current.Name} can not be deleted." + : $"Delete the currently selected collection {Current?.Name}. Hold {_config.DeleteModModifier} to delete."; + + protected override bool OnAdd(string name) + => _storage.AddCollection(name, null); + + protected override bool OnDuplicate(string name, int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + return _storage.AddCollection(name, Items[idx]); + } + + protected override bool Filtered(int idx) + => !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); + + protected override bool OnDraw(int idx) + { + using var color = ImRaii.PushColor(ImGuiCol.Header, Colors.SelectedColor); + var ret = ImGui.Selectable(Items[idx].Name, idx == CurrentIdx); + using var source = ImRaii.DragDropSource(); + if (source) + { + _dragging = Items[idx]; + ImGui.SetDragDropPayload("Assignment", nint.Zero, 0); + ImGui.TextUnformatted($"Assigning {_dragging.Name} to..."); + } + + if (ret) + _active.SetCollection(Items[idx], CollectionType.Current); + + return ret; + } + + public void DragTarget(CollectionType type, ActorIdentifier identifier) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping("Assignment")) + return; + + _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); + _dragging = null; + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + } + + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) + { + switch (type) + { + case CollectionType.Temporary: return; + case CollectionType.Current: + if (@new != null) + SetCurrent(@new); + SetFilterDirty(); + return; + case CollectionType.Inactive: + Items.Clear(); + foreach (var c in _storage.OrderBy(c => c.Name)) + Items.Add(c); + + if (old == Current) + ClearCurrentSelection(); + else + TryRestoreCurrent(); + SetFilterDirty(); + return; + default: + SetFilterDirty(); + return; + } + } +} + +public class CollectionsTab : IDisposable, ITab +{ + private readonly CommunicatorService _communicator; + private readonly Configuration _configuration; + private readonly CollectionManager _collectionManager; + private readonly CollectionSelector2 _selector; + private readonly CollectionPanel _panel; + private readonly CollectionTree _tree; + + public enum PanelMode + { + SimpleAssignment, + ComplexAssignment, + Details, + }; + + public PanelMode Mode = PanelMode.SimpleAssignment; + + public CollectionsTab(CommunicatorService communicator, Configuration configuration, CollectionManager collectionManager, + ModStorage modStorage, ActorService actors, TargetManager targets) + { + _communicator = communicator; + _configuration = configuration; + _collectionManager = collectionManager; + _selector = new CollectionSelector2(_configuration, _communicator, _collectionManager.Storage, _collectionManager.Active); + _panel = new CollectionPanel(_collectionManager, modStorage); + _tree = new CollectionTree(collectionManager, _selector, actors, targets); + } + + public void Dispose() + { + _selector.Dispose(); + } + + public ReadOnlySpan Label + => "Collections"u8; + + public void DrawContent() + { + var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnn").X; + _selector.Draw(width); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawHeaderLine(); + DrawPanel(); + } + + private void DrawHeaderLine() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 3f, 0); + + using var _ = ImRaii.Group(); + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.SimpleAssignment); + if (ImGui.Button("Simple Assignments", buttonSize)) + Mode = PanelMode.SimpleAssignment; + + ImGui.SameLine(); + color.Pop(); + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.Details); + if (ImGui.Button("Collection Details", buttonSize)) + Mode = PanelMode.Details; + + ImGui.SameLine(); + color.Pop(); + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.ComplexAssignment); + if (ImGui.Button("Advanced Assignments", buttonSize)) + Mode = PanelMode.ComplexAssignment; + } + + private void DrawPanel() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + using var child = ImRaii.Child("##CollectionSettings", new Vector2(-1, 0), true, ImGuiWindowFlags.HorizontalScrollbar); + if (!child) + return; + + style.Pop(); + switch (Mode) + { + case PanelMode.SimpleAssignment: + _tree.DrawSimple(); + break; + case PanelMode.ComplexAssignment: + _tree.DrawAdvanced(); + break; + case PanelMode.Details: + _panel.Draw(); + break; + } + + style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTabOld.cs b/Penumbra/UI/Tabs/CollectionsTabOld.cs new file mode 100644 index 00000000..15655e2a --- /dev/null +++ b/Penumbra/UI/Tabs/CollectionsTabOld.cs @@ -0,0 +1,299 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Services; +using Penumbra.UI.CollectionTab; + +namespace Penumbra.UI.Tabs; + +public class CollectionsTabOld : IDisposable, ITab +{ + private readonly CommunicatorService _communicator; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly TutorialService _tutorial; + private readonly SpecialCombo _specialCollectionCombo; + + private readonly CollectionCombo _collectionsWithEmpty; + private readonly CollectionCombo _collectionCombo; + private readonly InheritanceUi _inheritance; + private readonly IndividualCollectionUi _individualCollections; + + public CollectionsTabOld(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager, + TutorialService tutorial, Configuration config) + { + _communicator = communicator; + _collectionManager = collectionManager; + _tutorial = tutorial; + _config = config; + _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); + _collectionsWithEmpty = new CollectionCombo(_collectionManager, + () => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); + _collectionCombo = new CollectionCombo(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); + _inheritance = new InheritanceUi(_collectionManager); + _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); + + _communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); + } + + public ReadOnlySpan Label + => "Collections"u8; + + /// Draw a collection selector of a certain width for a certain type. + public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) + => (withEmpty ? _collectionsWithEmpty : _collectionCombo).Draw(label, width, collectionType); + + public void Dispose() + => _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); + + /// Draw a tutorial step regardless of tab selection. + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); + + public void DrawContent() + { + using var child = ImRaii.Child("##collections", -Vector2.One); + if (child) + { + DrawActiveCollectionSelectors(); + DrawMainSelectors(); + } + } + + #region New Collections + + // Input text fields. + private string _newCollectionName = string.Empty; + private bool _canAddCollection; + + /// + /// Create a new collection that is either empty or a duplicate of the current collection. + /// Resets the new collection name. + /// + private void CreateNewCollection(bool duplicate) + { + if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null)) + _newCollectionName = string.Empty; + } + + /// Draw the Clean Unused Settings button if there are any. + private void DrawCleanCollectionButton(Vector2 width) + { + if (_collectionManager.Active.Current.UnusedSettings.Count == 0) + return; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton( + $"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width + , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." + , false)) + _collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current); + } + + /// Draw the new collection input as well as its buttons. + private void DrawNewCollectionInput(Vector2 width) + { + // Input for new collection name. Also checks for validity when changed. + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) + _canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of enabled mods."); + + // Creation buttons. + var tt = _canAddCollection + ? string.Empty + : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; + if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) + CreateNewCollection(false); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection)) + CreateNewCollection(true); + } + + #endregion + + #region Collection Selection + + /// Draw all collection assignment selections. + private void DrawActiveCollectionSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); + if (!open) + return; + + UiHelpers.DefaultLineSpace(); + + DrawDefaultCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); + DrawInterfaceCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); + UiHelpers.DefaultLineSpace(); + + DrawSpecialAssignments(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); + UiHelpers.DefaultLineSpace(); + + _individualCollections.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); + UiHelpers.DefaultLineSpace(); + } + + private void DrawCurrentCollectionSelector(Vector2 width) + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection, + "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); + + // Deletion conditions. + var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; + var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); + var tt = deleteCondition + ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." + : $"You can not delete the collection {ModCollection.DefaultCollectionName}."; + + if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) + _collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); + + DrawCleanCollectionButton(width); + } + + /// Draw the selector for the default collection assignment. + private void DrawDefaultCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, + $"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," + + "as well as any character for whom no more specific conditions from below apply."); + } + + /// Draw the selector for the interface collection assignment. + private void DrawInterfaceCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, + $"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); + } + + /// Description for character groups used in multiple help markers. + private const string CharacterGroupDescription = + $"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {TutorialService.DefaultCollection},\n" + + $"but all {TutorialService.IndividualAssignments} take precedence before them."; + + /// Draw the entire group assignment section. + private void DrawSpecialAssignments() + { + using var _ = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(TutorialService.CharacterGroups); + ImGuiComponents.HelpMarker(CharacterGroupDescription); + ImGui.Separator(); + DrawSpecialCollections(); + ImGui.Dummy(Vector2.Zero); + DrawNewSpecialCollection(); + } + + /// Draw a new combo to select special collections as well as button to create it. + private void DrawNewSpecialCollection() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (_specialCollectionCombo.CurrentIdx == -1 + || _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) + { + _specialCollectionCombo.ResetFilter(); + _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special + .IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); + } + + if (_specialCollectionCombo.CurrentType == null) + return; + + _specialCollectionCombo.Draw(); + ImGui.SameLine(); + var disabled = _specialCollectionCombo.CurrentType == null; + var tt = disabled + ? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" + + CharacterGroupDescription + : CharacterGroupDescription; + if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) + return; + + _collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); + _specialCollectionCombo.CurrentIdx = -1; + } + + #endregion + + #region Current Collection Editing + + /// Draw the current collection selection, the creation of new collections and the inheritance block. + private void DrawMainSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); + if (!open) + return; + + var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + UiHelpers.DefaultLineSpace(); + + DrawCurrentCollectionSelector(width); + _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + UiHelpers.DefaultLineSpace(); + + DrawNewCollectionInput(width); + UiHelpers.DefaultLineSpace(); + + _inheritance.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); + } + + /// Draw all currently set special collections. + private void DrawSpecialCollections() + { + foreach (var (type, name, desc) in CollectionTypeExtensions.Special) + { + var collection = _collectionManager.Active.ByType(type); + if (collection == null) + continue; + + using var id = ImRaii.PushId((int)type); + DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, + false, true)) + { + _collectionManager.Active.RemoveSpecialCollection(type); + _specialCollectionCombo.ResetFilter(); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker(name, desc); + } + } + + #endregion +} diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 2437f96e..18db156c 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -9,15 +9,15 @@ using System.Numerics; using Dalamud.Interface; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.ModsTab; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; -using Penumbra.Collections.Manager; - +using Penumbra.Collections.Manager; +using Penumbra.UI.CollectionTab; + namespace Penumbra.UI.Tabs; public class ModsTab : ITab @@ -25,23 +25,23 @@ public class ModsTab : ITab private readonly ModFileSystemSelector _selector; private readonly ModPanel _panel; private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; + private readonly ActiveCollections _activeCollections; private readonly RedrawService _redrawService; private readonly Configuration _config; - private readonly CollectionsTab _collectionsTab; + private readonly CollectionCombo _collectionCombo; public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, - TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) + TutorialService tutorial, RedrawService redrawService, Configuration config) { _modManager = modManager; - _collectionManager = collectionManager; + _activeCollections = collectionManager.Active; _selector = selector; _panel = panel; _tutorial = tutorial; _redrawService = redrawService; _config = config; - _collectionsTab = collectionsTab; + _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); } public bool IsVisible @@ -62,14 +62,14 @@ public class ModsTab : ITab { try { - _selector.Draw(GetModSelectorSize()); + _selector.Draw(GetModSelectorSize(_config)); ImGui.SameLine(); using var group = ImRaii.Group(); DrawHeaderLine(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, _config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), true, ImGuiWindowFlags.HorizontalScrollbar)) { style.Pop(); @@ -86,16 +86,29 @@ public class ModsTab : ITab { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"{_modManager.Count} Mods\n" - + $"{_collectionManager.Active.Current.AnonymizedName} Current Collection\n" - + $"{_collectionManager.Active.Current.Settings.Count} Settings\n" + + $"{_activeCollections.Current.AnonymizedName} Current Collection\n" + + $"{_activeCollections.Current.Settings.Count} Settings\n" + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _collectionManager.Active.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); } } + /// Get the correct size for the mod selector based on current config. + public static float GetModSelectorSize(Configuration config) + { + var absoluteSize = Math.Clamp(config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, + Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); + var relativeSize = config.ScaleModSelector + ? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) + : 0; + return !config.ScaleModSelector + ? absoluteSize + : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + } + private void DrawRedrawLine() { if (Penumbra.Config.HideRedrawBar) @@ -159,32 +172,32 @@ public class ModsTab : ITab ImGui.SameLine(); DrawInheritedCollectionButton(3 * buttonSize); ImGui.SameLine(); - _collectionsTab.DrawCollectionSelector("##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false); + _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, CollectionType.Current); } _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); - if (!_collectionManager.Active.CurrentCollectionInUse) + if (!_activeCollections.CurrentCollectionInUse) ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } private void DrawDefaultCollectionButton(Vector2 width) { - var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Active.Default.Name})"; - var isCurrent = _collectionManager.Active.Default == _collectionManager.Active.Current; - var isEmpty = _collectionManager.Active.Default == ModCollection.Empty; + var name = $"{TutorialService.DefaultCollection} ({_activeCollections.Default.Name})"; + var isCurrent = _activeCollections.Default == _activeCollections.Current; + var isEmpty = _activeCollections.Default == ModCollection.Empty; var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) - _collectionManager.Active.SetCollection(_collectionManager.Active.Default, CollectionType.Current); + _activeCollections.SetCollection(_activeCollections.Default, CollectionType.Current); } private void DrawInheritedCollectionButton(Vector2 width) { var noModSelected = _selector.Selected == null; var collection = _selector.SelectedSettingCollection; - var modInherited = collection != _collectionManager.Active.Current; + var modInherited = collection != _activeCollections.Current; var (name, tt) = (noModSelected, modInherited) switch { (true, _) => ("Inherited Collection", "No mod selected."), @@ -193,19 +206,6 @@ public class ModsTab : ITab (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), }; if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) - _collectionManager.Active.SetCollection(collection, CollectionType.Current); - } - - /// Get the correct size for the mod selector based on current config. - private float GetModSelectorSize() - { - var absoluteSize = Math.Clamp(_config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); - var relativeSize = _config.ScaleModSelector - ? Math.Clamp(_config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) - : 0; - return !_config.ScaleModSelector - ? absoluteSize - : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + _activeCollections.SetCollection(collection, CollectionType.Current); } } From 49ba771b260b422c426d63ad11ac8fb03620bd82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Apr 2023 18:45:21 +0200 Subject: [PATCH 0886/2451] Add unused debug hook for finding functions faster. --- Penumbra/Interop/Services/GameEventManager.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index dcd5585f..1e0561b4 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData; using System; +using System.Diagnostics; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Interop.Structs; @@ -29,7 +30,8 @@ public unsafe class GameEventManager : IDisposable _resourceHandleDestructorHook.Enable(); _characterBaseCreateHook.Enable(); _characterBaseDestructorHook.Enable(); - _weaponReloadHook.Enable(); + _weaponReloadHook.Enable(); + EnableDebugHook(); Penumbra.Log.Verbose($"{Prefix} Created."); } @@ -41,6 +43,7 @@ public unsafe class GameEventManager : IDisposable _characterBaseCreateHook.Dispose(); _characterBaseDestructorHook.Dispose(); _weaponReloadHook.Dispose(); + DisposeDebugHook(); Penumbra.Log.Verbose($"{Prefix} Disposed."); } @@ -256,4 +259,27 @@ public unsafe class GameEventManager : IDisposable public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject); #endregion + + #region Testing + +#if DEBUG + //[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(TestDetour))] + private readonly Hook? _testHook = null; + + private delegate void TestDelegate(nint a1, int a2); + private void TestDetour(nint a1, int a2) + { + Penumbra.Log.Information($"Test: {a1:X} {a2}"); + _testHook!.Original(a1, a2); + } + #endif + [Conditional("DEBUG")] + private void EnableDebugHook() + => _testHook?.Enable(); + + [Conditional("DEBUG")] + private void DisposeDebugHook() + => _testHook?.Dispose(); + + #endregion } From e3c333dd223e94f21240afa77805b868807b3c32 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Apr 2023 20:45:57 +0200 Subject: [PATCH 0887/2451] Use filtered combos in file selectors. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 117 +++++++++++++---------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 313d0f26..2da62772 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Data; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.Mods; using Penumbra.String.Classes; @@ -17,30 +20,27 @@ namespace Penumbra.UI.AdvancedWindow; public class FileEditor where T : class, IWritable { - private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly DataManager _gameData; - private readonly ModEditWindow _owner; + private readonly ModEditWindow _owner; - public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, - Func> getFiles, Func drawEdit, Func getInitialPath, + public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, + string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, Func? parseFile) { - _owner = owner; + _owner = owner; _gameData = gameData; - _config = config; _fileDialog = fileDialog; _tabName = tabName; _fileType = fileType; - _getFiles = getFiles; _drawEdit = drawEdit; _getInitialPath = getInitialPath; _parseFile = parseFile ?? DefaultParseFile; + _combo = new Combo(config, getFiles); } public void Draw() { - _list = _getFiles(); using var tab = ImRaii.TabItem(_tabName); if (!tab) { @@ -60,12 +60,11 @@ public class FileEditor where T : class, IWritable DrawFilePanel(); } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func> _getFiles; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; + private readonly string _tabName; + private readonly string _fileType; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private FileRegistry? _currentPath; private T? _currentFile; @@ -79,7 +78,7 @@ public class FileEditor where T : class, IWritable private T? _defaultFile; private Exception? _defaultException; - private IReadOnlyList _list = null!; + private readonly Combo _combo; private ModEditWindow.QuickImportAction? _quickImport; @@ -134,9 +133,11 @@ public class FileEditor where T : class, IWritable } }, _getInitialPath(), false); - _quickImport ??= ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); + _quickImport ??= + ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) { try { @@ -146,6 +147,7 @@ public class FileEditor where T : class, IWritable { Penumbra.Log.Error($"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}"); } + _quickImport = null; } @@ -162,39 +164,10 @@ public class FileEditor where T : class, IWritable private void DrawFileSelectCombo() { - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..."); - if (!combo) - return; - - foreach (var file in _list) - { - if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath))) - UpdateCurrentFile(file); - - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted("All Game Paths"); - ImGui.Separator(); - using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); - foreach (var (option, gamePath) in file.SubModUsage) - { - ImGui.TableNextColumn(); - UiHelpers.Text(gamePath.Path); - ImGui.TableNextColumn(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); - ImGui.TextUnformatted(option.FullName); - } - } - - if (file.SubModUsage.Count > 0) - { - ImGui.SameLine(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); - ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); - } - } + if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...", string.Empty, + ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight()) + && _combo.CurrentSelection != null) + UpdateCurrentFile(_combo.CurrentSelection); } private static T? DefaultParseFile(byte[] bytes) @@ -291,4 +264,48 @@ public class FileEditor where T : class, IWritable } } } + + private class Combo : FilterComboCache + { + private readonly Configuration _config; + + public Combo(Configuration config, Func> generator) + : base(generator) + => _config = config; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var file = Items[globalIdx]; + var ret = ImGui.Selectable(file.RelPath.ToString(), selected); + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted("All Game Paths"); + ImGui.Separator(); + using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); + foreach (var (option, gamePath) in file.SubModUsage) + { + ImGui.TableNextColumn(); + UiHelpers.Text(gamePath.Path); + ImGui.TableNextColumn(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + ImGui.TextUnformatted(option.FullName); + } + } + + if (file.SubModUsage.Count > 0) + { + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); + } + + return ret; + } + + protected override bool IsVisible(int globalIndex, LowerString filter) + => filter.IsContained(Items[globalIndex].File.FullName) + || Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString())); + } } From 1364b39f65822b01e2d69a0a5b1693edd4e2596b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 19 Apr 2023 11:00:53 +0200 Subject: [PATCH 0888/2451] Bugfix for service creation stalling itself. --- Penumbra.GameData/Data/ObjectIdentification.cs | 1 - Penumbra/Services/ServiceWrapper.cs | 13 +++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 53c71730..111a652d 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -29,7 +29,6 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier public readonly IReadOnlyDictionary> Actions; private readonly ActorManager.ActorManagerData _actorData; - private readonly EquipmentIdentificationList _equipment; private readonly WeaponIdentificationList _weapons; private readonly ModelIdentificationList _modelIdentifierToModelChara; diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 1adec97f..5da7cd07 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -78,9 +78,14 @@ public abstract class AsyncServiceWrapper : IServiceWrapper { Service = service; Penumbra.Log.Verbose($"[{Name}] Created."); - FinishedCreation?.Invoke(); + _task = null; } }); + _task.ContinueWith((t, x) => + { + if (!_isDisposed) + FinishedCreation?.Invoke(); + }, null); } protected AsyncServiceWrapper(string name, Func factory) @@ -99,9 +104,13 @@ public abstract class AsyncServiceWrapper : IServiceWrapper Service = service; Penumbra.Log.Verbose($"[{Name}] Created."); _task = null; - FinishedCreation?.Invoke(); } }); + _task.ContinueWith((t, x) => + { + if (!_isDisposed) + FinishedCreation?.Invoke(); + }, null); } public void Dispose() From 25cb46525a65e5527cea44f4c48ad5a1ec7f1626 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Apr 2023 09:35:23 +0200 Subject: [PATCH 0889/2451] Fix bug with shpk editing. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 7 ++----- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 2da62772..401d96a6 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -26,7 +26,7 @@ public class FileEditor where T : class, IWritable public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func? parseFile) + Func parseFile) { _owner = owner; _gameData = gameData; @@ -35,7 +35,7 @@ public class FileEditor where T : class, IWritable _fileType = fileType; _drawEdit = drawEdit; _getInitialPath = getInitialPath; - _parseFile = parseFile ?? DefaultParseFile; + _parseFile = parseFile; _combo = new Combo(config, getFiles); } @@ -170,9 +170,6 @@ public class FileEditor where T : class, IWritable UpdateCurrentFile(_combo.CurrentSelection); } - private static T? DefaultParseFile(byte[] bytes) - => Activator.CreateInstance(typeof(T), bytes) as T; - private void UpdateCurrentFile(FileRegistry path) { if (ReferenceEquals(_currentPath, path)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 4edb2701..a922c477 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -513,9 +513,9 @@ public partial class ModEditWindow : Window, IDisposable () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", - () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", - () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null); + () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); } From 9c4f7b7562dbbd5fbd0434bd11f92128e4627c14 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 21 Apr 2023 18:42:54 +0200 Subject: [PATCH 0890/2451] Finish CollectionTab rework. --- OtterGui | 2 +- Penumbra.GameData/Actors/ActorIdentifier.cs | 12 + .../Collections/Manager/CollectionStorage.cs | 17 +- .../Collections/Manager/CollectionType.cs | 151 +--- Penumbra/Configuration.cs | 24 +- Penumbra/Penumbra.cs | 14 +- Penumbra/UI/Classes/Colors.cs | 40 +- Penumbra/UI/CollectionTab/CollectionCombo.cs | 18 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 671 ++++++++++++++++++ .../UI/CollectionTab/CollectionSelector.cs | 142 ++++ .../CollectionTab/IndividualAssignmentUi.cs | 206 ++++++ .../CollectionTab/IndividualCollectionUi.cs | 358 ---------- Penumbra/UI/CollectionTab/InheritanceUi.cs | 186 ++--- Penumbra/UI/CollectionTab/NpcCombo.cs | 25 +- Penumbra/UI/CollectionTab/SpecialCombo.cs | 42 -- Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 602 +++------------- Penumbra/UI/Tabs/CollectionsTabOld.cs | 299 -------- Penumbra/UI/Tabs/ConfigTabBar.cs | 2 +- Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 3 +- Penumbra/UI/TutorialService.cs | 75 +- 22 files changed, 1350 insertions(+), 1543 deletions(-) create mode 100644 Penumbra/UI/CollectionTab/CollectionPanel.cs create mode 100644 Penumbra/UI/CollectionTab/CollectionSelector.cs create mode 100644 Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs delete mode 100644 Penumbra/UI/CollectionTab/IndividualCollectionUi.cs delete mode 100644 Penumbra/UI/CollectionTab/SpecialCombo.cs delete mode 100644 Penumbra/UI/Tabs/CollectionsTabOld.cs diff --git a/OtterGui b/OtterGui index 51c350b5..ee7815a4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 51c350b5f129b53afda3a51b057c228e152a6b88 +Subproject commit ee7815a4f4c91ec04a240c9e52733f2f5ffa15d0 diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 81199fc6..e0a653d8 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Enums; using Newtonsoft.Json.Linq; @@ -65,6 +66,17 @@ public readonly struct ActorIdentifier : IEquatable public bool IsValid => Type is not (IdentifierType.UnkObject or IdentifierType.Invalid); + public string Incognito(string? name) + { + name ??= ToString(); + if (Type is not (IdentifierType.Player or IdentifierType.Owned)) + return name; + + var parts = name.Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); + } + public override string ToString() => Manager?.ToString(this) ?? Type switch diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index bf273e1c..658e2e9b 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -113,7 +113,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } - var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); + var newCollection = duplicate?.Duplicate(name, _collections.Count) + ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); @@ -195,6 +196,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } + /// Remove a specific setting for not currently-installed mods from the given collection. + public void CleanUnavailableSetting(ModCollection collection, string? setting) + { + if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + /// /// Check if a name is valid to use for a collection. /// Does not check for uniqueness. @@ -214,7 +222,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var file in _saveService.FileNames.CollectionFiles) { - if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance) || !IsValidName(name)) + if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance) + || !IsValidName(name)) continue; if (ByName(name, out _)) @@ -224,7 +233,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable continue; } - var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings); + var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", @@ -305,4 +314,4 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index f08cf380..40f0f488 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -430,150 +430,13 @@ public static class CollectionTypeExtensions public static string ToDescription(this CollectionType collectionType) => collectionType switch { - CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerChild => - "This collection applies to all non-player characters with a child body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerElderly => - "This collection applies to all non-player characters with an elderly body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.MalePlayerCharacter => - "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", - CollectionType.MaleNonPlayerCharacter => - "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.MaleMidlander => - "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlander => - "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwood => - "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwight => - "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolk => - "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolk => - "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSun => - "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoon => - "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolf => - "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguard => - "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaen => - "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaela => - "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelion => - "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLost => - "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRava => - "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeena => - "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.MaleMidlanderNpc => - "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlanderNpc => - "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwoodNpc => - "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwightNpc => - "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolkNpc => - "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolkNpc => - "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSunNpc => - "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoonNpc => - "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolfNpc => - "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguardNpc => - "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaenNpc => - "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaelaNpc => - "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelionNpc => - "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLostNpc => - "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRavaNpc => - "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeenaNpc => - "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemalePlayerCharacter => - "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", - CollectionType.FemaleNonPlayerCharacter => - "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.FemaleMidlander => - "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlander => - "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwood => - "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwight => - "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolk => - "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolk => - "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSun => - "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoon => - "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolf => - "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguard => - "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaen => - "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaela => - "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelion => - "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLost => - "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRava => - "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeena => - "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemaleMidlanderNpc => - "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlanderNpc => - "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwoodNpc => - "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwightNpc => - "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolkNpc => - "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolkNpc => - "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSunNpc => - "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoonNpc => - "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolfNpc => - "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguardNpc => - "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaenNpc => - "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaelaNpc => - "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelionNpc => - "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLostNpc => - "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRavaNpc => - "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeenaNpc => - "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.Default => "World, Music, Furniture, baseline for characters and monsters not specialized.", + CollectionType.Interface => "User Interface, Icons, Maps, Styles.", + CollectionType.Yourself => "Your characters, regardless of name, race or gender. Applies in the login screen.", + CollectionType.MalePlayerCharacter => "Baseline for male player characters.", + CollectionType.FemalePlayerCharacter => "Baseline for female player characters.", + CollectionType.MaleNonPlayerCharacter => "Baseline for humanoid male non-player characters.", + CollectionType.FemaleNonPlayerCharacter => "Baseline for humanoid female non-player characters.", _ => string.Empty, }; } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 89b29978..764d3269 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,10 +11,11 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.Tabs; using Penumbra.Util; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -72,16 +73,17 @@ public class Configuration : IPluginConfiguration, ISavable [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; - public bool OpenFoldersByDefault { get; set; } = false; - public int SingleGroupRadioMax { get; set; } = 2; - public string DefaultImportFolder { get; set; } = string.Empty; - public string QuickMoveFolder1 { get; set; } = string.Empty; - public string QuickMoveFolder2 { get; set; } = string.Empty; - public string QuickMoveFolder3 { get; set; } = string.Empty; - public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public bool ScaleModSelector { get; set; } = false; + public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; + public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool OpenFoldersByDefault { get; set; } = false; + public int SingleGroupRadioMax { get; set; } = 2; + public string DefaultImportFolder { get; set; } = string.Empty; + public string QuickMoveFolder1 { get; set; } = string.Empty; + public string QuickMoveFolder2 { get; set; } = string.Empty; + public string QuickMoveFolder3 { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8a936998..259048f0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -223,18 +223,6 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n"); - string CharacterName(ActorIdentifier id, string name) - { - if (id.Type is IdentifierType.Player or IdentifierType.Owned) - { - var parts = name.Split(' ', 3); - return string.Join(" ", - parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); - } - - return name + ':'; - } - void PrintCollection(ModCollection c, CollectionCache _) => sb.Append($"**Collection {c.AnonymizedName}**\n" + $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n" @@ -256,7 +244,7 @@ public class Penumbra : IDalamudPlugin } foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments) - sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n"); foreach (var (collection, cache) in CollectionManager.Caches.Active) PrintCollection(collection, cache); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index b2c65c45..d69c3bdb 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -17,7 +17,11 @@ public enum ColorId FolderLine, ItemId, IncreasedMetaValue, - DecreasedMetaValue, + DecreasedMetaValue, + SelectedCollection, + RedundantAssignment, + NoModsAssignment, + NoAssignment, } public static class Colors @@ -27,8 +31,6 @@ public static class Colors public const uint MetaInfoText = 0xAAFFFFFF; public const uint RedTableBgTint = 0x40000080; public const uint DiscordColor = 0xFFDA8972; - public const uint SelectedColor = 0x6069C056; - public const uint RedundantColor = 0x6050D0D0; public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF; @@ -40,20 +42,24 @@ public static class Colors => color switch { // @formatter:off - ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), - ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), - ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), - ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), - ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), - ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), - ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), - ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), - ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), - ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), - ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), - ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), - ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), - ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), + ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), + ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."), + ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), + ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), + ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index b7d379cf..94e6f6d2 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using ImGuiNET; +using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -11,23 +12,26 @@ namespace Penumbra.UI.CollectionTab; public sealed class CollectionCombo : FilterComboCache { private readonly CollectionManager _collectionManager; + private readonly ImRaii.Color _color = new(); public CollectionCombo(CollectionManager manager, Func> items) : base(items) => _collectionManager = manager; - public void Draw(string label, float width, int individualIdx) + protected override void DrawFilter(int currentSelected, float width) { - var (_, collection) = _collectionManager.Active.Individuals[individualIdx]; - if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); + _color.Dispose(); + base.DrawFilter(currentSelected, width); } - public void Draw(string label, float width, CollectionType type) + public void Draw(string label, float width, uint color) { - var current = _collectionManager.Active.ByType(type, ActorIdentifier.Invalid); + var current = _collectionManager.Active.ByType(CollectionType.Current, ActorIdentifier.Invalid); + _color.Push(ImGuiCol.FrameBg, color).Push(ImGuiCol.FrameBgHovered, color); + if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.Active.SetCollection(CurrentSelection, type); + _collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Current); + _color.Dispose(); } protected override string ToString(ModCollection obj) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs new file mode 100644 index 00000000..ae21fde5 --- /dev/null +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -0,0 +1,671 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionPanel : IDisposable +{ + private readonly Configuration _config; + private readonly CollectionStorage _collections; + private readonly ActiveCollections _active; + private readonly CollectionSelector _selector; + private readonly ActorService _actors; + private readonly TargetManager _targets; + private readonly IndividualAssignmentUi _individualAssignmentUi; + private readonly InheritanceUi _inheritanceUi; + private readonly ModStorage _mods; + + private readonly GameFontHandle _nameFont; + + private static readonly IReadOnlyDictionary Buttons = CreateButtons(); + private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); + private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new(); + + public CollectionPanel(DalamudPluginInterface pi, Configuration config, CommunicatorService communicator, CollectionManager manager, + CollectionSelector selector, ActorService actors, TargetManager targets, ModStorage mods) + { + _config = config; + _collections = manager.Storage; + _active = manager.Active; + _selector = selector; + _actors = actors; + _targets = targets; + _mods = mods; + _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); + _inheritanceUi = new InheritanceUi(_config, manager, _selector); + _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + } + + public void Dispose() + { + _individualAssignmentUi.Dispose(); + _nameFont.Dispose(); + } + + /// Draw the panel containing beginners information and simple assignments. + public void DrawSimple() + { + ImGuiUtil.TextWrapped("A collection is a set of mod configurations. You can have as many collections as you desire.\n" + + "The collection you are currently editing in the mod tab can be selected here and is highlighted.\n"); + ImGuiUtil.TextWrapped( + "There are functions you can assign these collections to, so different mod configurations apply for different things.\n" + + "You can assign an existing collection to such a function by clicking the function or dragging the collection over."); + ImGui.Separator(); + + var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + DrawSimpleCollectionButton(CollectionType.Default, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); + + ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value(_config)), + ("Assignments take precedence before anything else and only apply to one specific character or monster.", 0)); + ImGui.Dummy(Vector2.UnitX); + + var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale }; + DrawCurrentCharacter(specialWidth); + ImGui.SameLine(); + DrawCurrentTarget(specialWidth); + DrawIndividualCollections(buttonWidth); + + var first = true; + + void Button(CollectionType type) + { + var (name, border) = Buttons[type]; + var collection = _active.ByType(type); + if (collection == null) + return; + + if (first) + { + ImGui.Separator(); + ImGui.TextUnformatted("Currently Active Advanced Assignments"); + first = false; + } + + DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, 's', collection); + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) + ImGui.NewLine(); + } + + Button(CollectionType.NonPlayerChild); + Button(CollectionType.NonPlayerElderly); + foreach (var race in Enum.GetValues().Skip(1)) + { + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); + } + } + + /// Draw the panel containing new and existing individual assignments. + public void DrawIndividualPanel() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + var width = new Vector2(300 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + + ImGui.Dummy(Vector2.One); + DrawCurrentCharacter(width); + ImGui.SameLine(); + DrawCurrentTarget(width); + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + style.Pop(); + _individualAssignmentUi.DrawWorldCombo(width.X / 2); + ImGui.SameLine(); + _individualAssignmentUi.DrawNewPlayerCollection(width.X); + + _individualAssignmentUi.DrawObjectKindCombo(width.X / 2); + ImGui.SameLine(); + _individualAssignmentUi.DrawNewNpcCollection(width.X); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + style.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + + DrawNewPlayer(width); + ImGui.SameLine(); + ImGuiUtil.TextWrapped("Also check General Settings for UI characters and inheritance through ownership."); + ImGui.Separator(); + + DrawNewRetainer(width); + ImGui.SameLine(); + ImGuiUtil.TextWrapped("Bell Retainers apply to Mannequins, but not to outdoor retainers, since those only carry their owners name."); + ImGui.Separator(); + + DrawNewNpc(width); + ImGui.SameLine(); + ImGuiUtil.TextWrapped("Some NPCs are available as Battle - and Event NPCs and need to be setup for both if desired."); + ImGui.Separator(); + + DrawNewOwned(width); + ImGui.SameLine(); + ImGuiUtil.TextWrapped("Owned NPCs take precedence before unowned NPCs of the same type."); + ImGui.Separator(); + + DrawIndividualCollections(width with { X = 200 * ImGuiHelpers.GlobalScale }); + } + + /// Draw the panel containing all special group assignments. + public void DrawGroupPanel() + { + ImGui.Dummy(Vector2.One); + using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + + var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + var dummy = new Vector2(1, 0); + + foreach (var (type, pre, post, name, border) in AdvancedTree) + { + ImGui.TableNextColumn(); + if (type is CollectionType.Inactive) + continue; + + if (pre) + ImGui.Dummy(dummy); + DrawAssignmentButton(type, buttonWidth, name, border); + if (post) + ImGui.Dummy(dummy); + } + } + + /// Draw the collection detail panel with inheritance, visible mod settings and statistics. + public void DrawDetailsPanel() + { + var collection = _active.Current; + DrawCollectionName(collection); + DrawStatistics(collection); + _inheritanceUi.Draw(); + ImGui.Separator(); + DrawInactiveSettingsList(collection); + DrawSettingsList(collection); + } + + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix) + { + var label = $"{type}{identifier}{suffix}"; + if (open) + ImGui.OpenPopup(label); + + using var context = ImRaii.Popup(label); + if (!context) + return; + + using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor)) + { + if (ImGui.MenuItem("Use no mods.")) + _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); + } + + if (collection != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + if (ImGui.MenuItem("Remove this assignment.")) + _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); + } + + foreach (var coll in _collections) + { + if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) + _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); + } + } + + private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, char suffix, + ModCollection? collection = null) + { + using var group = ImRaii.Group(); + var invalid = type == CollectionType.Individual && !id.IsValid; + var redundancy = _active.RedundancyCheck(type, id); + collection ??= _active.ByType(type, id); + using var color = ImRaii.PushColor(ImGuiCol.Button, + collection == null + ? ColorId.NoAssignment.Value(_config) + : redundancy.Length > 0 + ? ColorId.RedundantAssignment.Value(_config) + : collection == _active.Current + ? ColorId.SelectedCollection.Value(_config) + : collection == ModCollection.Empty + ? ColorId.NoModsAssignment.Value(_config) + : ImGui.GetColorU32(ImGuiCol.Button), !invalid) + .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); + using var disabled = ImRaii.Disabled(invalid); + var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); + var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); + if (!invalid) + { + _selector.DragTargetAssignment(type, id); + var name = Name(collection); + var size = ImGui.CalcTextSize(name); + var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; + ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); + DrawContext(button, collection, type, id, suffix); + } + + if (hovered) + ImGui.SetTooltip(redundancy); + + return button; + } + + private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) + { + DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid, 's'); + ImGui.SameLine(); + using (var group = ImRaii.Group()) + { + ImGuiUtil.TextWrapped(type.ToDescription()); + switch (type) + { + case CollectionType.Default: + ImGui.TextUnformatted("Overruled by any other Assignment."); + break; + case CollectionType.Yourself: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + break; + case CollectionType.MalePlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial Player", Colors.DiscordColor), (", ", 0), + ("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0), + ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + break; + case CollectionType.FemalePlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial Player", Colors.ReniColorActive), (", ", 0), + ("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0), + ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + break; + case CollectionType.MaleNonPlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial NPC", Colors.DiscordColor), (", ", 0), + ("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + break; + case CollectionType.FemaleNonPlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial NPC", Colors.ReniColorActive), (", ", 0), + ("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + break; + } + } + + ImGui.Separator(); + } + + private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color) + => DrawButton(name, type, width, color, ActorIdentifier.Invalid, 's', _active.ByType(type)); + + /// Respect incognito mode for names of identifiers. + private string Name(ActorIdentifier id, string? name) + => _selector.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + ? id.Incognito(name) + : name ?? id.ToString(); + + /// Respect incognito mode for names of collections. + private string Name(ModCollection? collection) + => collection == null ? "Unassigned" : + collection == ModCollection.Empty ? "Use No Mods" : + _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + + private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) + { + if (identifiers.Length > 0 && identifiers[0].IsValid) + { + DrawButton($"{intro} ({Name(identifiers[0], null)})", CollectionType.Individual, width, 0, identifiers[0], suffix); + } + else + { + if (tooltip.Length == 0 && identifiers.Length > 0) + tooltip = $"The current target {identifiers[0].PlayerName} is not valid for an assignment."; + DrawButton($"{intro} (Unavailable)", CollectionType.Individual, width, 0, ActorIdentifier.Invalid, suffix); + } + + ImGuiUtil.HoverTooltip(tooltip); + } + + private void DrawCurrentCharacter(Vector2 width) + => DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.AwaitedService.GetCurrentPlayer()); + + private void DrawCurrentTarget(Vector2 width) + => DrawIndividualButton("Current Target", width, string.Empty, 't', + _actors.AwaitedService.FromObject(_targets.Target, false, true, true)); + + private void DrawNewPlayer(Vector2 width) + => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', + _individualAssignmentUi.PlayerIdentifiers.FirstOrDefault()); + + private void DrawNewRetainer(Vector2 width) + => DrawIndividualButton("New Bell Retainer", width, _individualAssignmentUi.RetainerTooltip, 'r', + _individualAssignmentUi.RetainerIdentifiers.FirstOrDefault()); + + private void DrawNewNpc(Vector2 width) + => DrawIndividualButton("New NPC", width, _individualAssignmentUi.NpcTooltip, 'n', + _individualAssignmentUi.NpcIdentifiers.FirstOrDefault()); + + private void DrawNewOwned(Vector2 width) + => DrawIndividualButton("New Owned NPC", width, _individualAssignmentUi.OwnedTooltip, 'o', + _individualAssignmentUi.OwnedIdentifiers.FirstOrDefault()); + + private void DrawIndividualCollections(Vector2 width) + { + for (var i = 0; i < _active.Individuals.Count; ++i) + { + var (name, ids, coll) = _active.Individuals.Assignments[i]; + DrawButton(Name(ids[0], name), CollectionType.Individual, width, 0, ids[0], 'i', coll); + + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < width.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + && i < _active.Individuals.Count - 1) + ImGui.NewLine(); + } + + if (_active.Individuals.Count > 0) + ImGui.NewLine(); + } + + private void DrawCollectionName(ModCollection collection) + { + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + var name = Name(collection); + var size = ImGui.CalcTextSize(name).X; + var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2; + if (pos > 0) + ImGui.SetCursorPosX(pos / 2); + ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); + ImGui.Dummy(Vector2.One); + } + + private void DrawStatistics(ModCollection collection) + { + GatherInUse(collection); + ImGui.Separator(); + + var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); + if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) + { + ImGui.Dummy(Vector2.One); + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight), + Colors.PressEnterWarningBg); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + else + { + var buttonWidth = new Vector2(175 * ImGuiHelpers.GlobalScale, buttonHeight); + DrawInUseStatistics(collection, buttonWidth); + DrawInheritanceStatistics(collection, buttonWidth); + } + } + + private void GatherInUse(ModCollection collection) + { + _inUseCache.Clear(); + foreach (var special in CollectionTypeExtensions.Special.Select(t => t.Item1) + .Prepend(CollectionType.Default) + .Prepend(CollectionType.Interface) + .Where(t => _active.ByType(t) == collection)) + _inUseCache.Add((special, ActorIdentifier.Invalid)); + + foreach (var (_, id, coll) in _active.Individuals.Assignments.Where(t + => t.Collection == collection && t.Identifiers.FirstOrDefault().IsValid)) + _inUseCache.Add((CollectionType.Individual, id[0])); + } + + private void DrawInUseStatistics(ModCollection collection, Vector2 buttonWidth) + { + if (_inUseCache.Count <= 0) + return; + + using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) + { + ImGuiUtil.DrawTextButton("In Use By", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); + } + + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale) + .Push(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero); + + foreach (var ((type, id), idx) in _inUseCache.WithIndex()) + { + var name = type == CollectionType.Individual ? Name(id, null) : Buttons[type].Name; + var color = Buttons.TryGetValue(type, out var p) ? p.Border : 0; + DrawButton(name, type, buttonWidth, color, id, 's', collection); + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + && idx != _inUseCache.Count - 1) + ImGui.NewLine(); + } + + ImGui.NewLine(); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + + private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) + { + if (collection.DirectParentOf.Count <= 0) + return; + + using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) + { + ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); + } + + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); + var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 + + ImGui.GetStyle().ItemSpacing.X + + ImGui.GetStyle().WindowPadding.X; + foreach (var parent in collection.DirectParentOf.Skip(1)) + { + var name = Name(parent); + var size = ImGui.CalcTextSize(name).X; + ImGui.SameLine(); + if (constOffset + size >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); + } + + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + + private void DrawSettingsList(ModCollection collection) + { + ImGui.Dummy(Vector2.One); + var size = new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetFrameHeightWithSpacing()); + using var table = ImRaii.Table("##activeSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); + if (!table) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, 5f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + foreach (var (mod, (settings, parent)) in _mods.Select(m => (m, collection[m.Index])) + .Where(t => t.Item2.Settings != null) + .OrderBy(t => t.m.Name)) + { + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(mod.Name); + ImGui.TableNextColumn(); + if (parent != collection) + ImGui.TextUnformatted(Name(parent)); + ImGui.TableNextColumn(); + var enabled = settings!.Enabled; + using (var dis = ImRaii.Disabled()) + { + ImGui.Checkbox("##check", ref enabled); + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); + } + } + + private void DrawInactiveSettingsList(ModCollection collection) + { + if (collection.UnusedSettings.Count == 0) + return; + + ImGui.Dummy(Vector2.One); + var text = collection.UnusedSettings.Count > 1 + ? $"Clear all {collection.UnusedSettings.Count} unused settings from deleted mods." + : "Clear the currently unused setting from a deleted mods."; + if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) + _collections.CleanUnavailableSettings(collection); + + ImGui.Dummy(Vector2.One); + + var size = new Vector2(ImGui.GetContentRegionAvail().X, + Math.Min(10, collection.UnusedSettings.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); + if (!table) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("Unused Mod Identifier", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + string? delete = null; + foreach (var (name, settings) in collection.UnusedSettings.OrderBy(n => n.Key)) + { + using var id = ImRaii.PushId(name); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this unused setting.", false, true)) + delete = name; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(name); + ImGui.TableNextColumn(); + var enabled = settings.Enabled; + using (var dis = ImRaii.Disabled()) + { + ImGui.Checkbox("##check", ref enabled); + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); + } + + _collections.CleanUnavailableSetting(collection, delete); + ImGui.Separator(); + } + + /// Create names and border colors for special assignments. + private static IReadOnlyDictionary CreateButtons() + { + var ret = Enum.GetValues().ToDictionary(t => t, t => (t.ToName(), 0u)); + + foreach (var race in Enum.GetValues().Skip(1)) + { + var color = race switch + { + SubRace.Midlander => 0xAA5C9FE4u, + SubRace.Highlander => 0xAA5C9FE4u, + SubRace.Wildwood => 0xAA5C9F49u, + SubRace.Duskwight => 0xAA5C9F49u, + SubRace.Plainsfolk => 0xAAEF8CB6u, + SubRace.Dunesfolk => 0xAAEF8CB6u, + SubRace.SeekerOfTheSun => 0xAA8CEFECu, + SubRace.KeeperOfTheMoon => 0xAA8CEFECu, + SubRace.Seawolf => 0xAAEFE68Cu, + SubRace.Hellsguard => 0xAAEFE68Cu, + SubRace.Raen => 0xAAB5EF8Cu, + SubRace.Xaela => 0xAAB5EF8Cu, + SubRace.Helion => 0xAAFFFFFFu, + SubRace.Lost => 0xAAFFFFFFu, + SubRace.Rava => 0xAA607FA7u, + SubRace.Veena => 0xAA607FA7u, + _ => 0u, + }; + + ret[CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color); + } + + ret[CollectionType.MalePlayerCharacter] = ("♂ Player", 0); + ret[CollectionType.FemalePlayerCharacter] = ("♀ Player", 0); + ret[CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0); + ret[CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0); + return ret; + } + + /// Create the special assignment tree in order and with free spaces. + private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree() + { + var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count); + + void Add(CollectionType type, bool pre, bool post) + { + var (name, border) = Buttons[type]; + ret.Add((type, pre, post, name, border)); + } + + Add(CollectionType.Default, false, false); + Add(CollectionType.Interface, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Yourself, false, true); + Add(CollectionType.Inactive, false, true); + Add(CollectionType.NonPlayerChild, false, true); + Add(CollectionType.NonPlayerElderly, false, true); + Add(CollectionType.MalePlayerCharacter, true, true); + Add(CollectionType.FemalePlayerCharacter, true, true); + Add(CollectionType.MaleNonPlayerCharacter, true, true); + Add(CollectionType.FemaleNonPlayerCharacter, true, true); + var pre = true; + foreach (var race in Enum.GetValues().Skip(1)) + { + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre); + pre = !pre; + } + + return ret; + } +} diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs new file mode 100644 index 00000000..5cbb3a80 --- /dev/null +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionSelector : ItemSelector, IDisposable +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; + private readonly ActiveCollections _active; + private readonly TutorialService _tutorial; + + private ModCollection? _dragging; + + public bool IncognitoMode; + + public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, + TutorialService tutorial) + : base(new List(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) + { + _config = config; + _communicator = communicator; + _storage = storage; + _active = active; + _tutorial = tutorial; + + _communicator.CollectionChange.Subscribe(OnCollectionChange); + // Set items. + OnCollectionChange(CollectionType.Inactive, null, null, string.Empty); + // Set selection. + OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty); + } + + protected override bool OnDelete(int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + return _storage.RemoveCollection(Items[idx]); + } + + protected override bool DeleteButtonEnabled() + => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); + + protected override string DeleteButtonTooltip() + => _storage.DefaultNamed == Current + ? $"The selected collection {Name(Current)} can not be deleted." + : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete."; + + protected override bool OnAdd(string name) + => _storage.AddCollection(name, null); + + protected override bool OnDuplicate(string name, int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + return _storage.AddCollection(name, Items[idx]); + } + + protected override bool Filtered(int idx) + => !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); + + private const string PayloadString = "Collection"; + + protected override bool OnDraw(int idx) + { + using var color = ImRaii.PushColor(ImGuiCol.Header, ColorId.SelectedCollection.Value(_config)); + var ret = ImGui.Selectable(Name(Items[idx]), idx == CurrentIdx); + using var source = ImRaii.DragDropSource(); + + if (idx == CurrentIdx) + _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + + if (source) + { + _dragging = Items[idx]; + ImGui.SetDragDropPayload(PayloadString, nint.Zero, 0); + ImGui.TextUnformatted($"Assigning {Name(_dragging)} to..."); + } + + if (ret) + _active.SetCollection(Items[idx], CollectionType.Current); + + return ret; + } + + public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping(PayloadString)) + return; + + _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); + _dragging = null; + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + } + + private string Name(ModCollection collection) + => IncognitoMode ? collection.AnonymizedName : collection.Name; + + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) + { + switch (type) + { + case CollectionType.Temporary: return; + case CollectionType.Current: + if (@new != null) + SetCurrent(@new); + SetFilterDirty(); + return; + case CollectionType.Inactive: + Items.Clear(); + foreach (var c in _storage.OrderBy(c => c.Name)) + Items.Add(c); + + if (old == Current) + ClearCurrentSelection(); + else + TryRestoreCurrent(); + SetFilterDirty(); + return; + default: + SetFilterDirty(); + return; + } + } +} diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs new file mode 100644 index 00000000..98fcc0d6 --- /dev/null +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.Enums; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.Services; + +namespace Penumbra.UI.CollectionTab; + +public class IndividualAssignmentUi : IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ActorService _actorService; + private readonly CollectionManager _collectionManager; + + private WorldCombo _worldCombo = null!; + private NpcCombo _mountCombo = null!; + private NpcCombo _companionCombo = null!; + private NpcCombo _ornamentCombo = null!; + private NpcCombo _bnpcCombo = null!; + private NpcCombo _enpcCombo = null!; + + private bool _ready; + + public IndividualAssignmentUi(CommunicatorService communicator, ActorService actors, CollectionManager collectionManager) + { + _communicator = communicator; + _actorService = actors; + _collectionManager = collectionManager; + _communicator.CollectionChange.Subscribe(UpdateIdentifiers); + if (_actorService.Valid) + SetupCombos(); + else + _actorService.FinishedCreation += SetupCombos; + } + + public string PlayerTooltip { get; private set; } = NewPlayerTooltipEmpty; + public string RetainerTooltip { get; private set; } = NewRetainerTooltipEmpty; + public string NpcTooltip { get; private set; } = NewNpcTooltipEmpty; + public string OwnedTooltip { get; private set; } = NewPlayerTooltipEmpty; + + public ActorIdentifier[] PlayerIdentifiers + => _playerIdentifiers; + + public ActorIdentifier[] RetainerIdentifiers + => _retainerIdentifiers; + + public ActorIdentifier[] NpcIdentifiers + => _npcIdentifiers; + + public ActorIdentifier[] OwnedIdentifiers + => _ownedIdentifiers; + + public void DrawWorldCombo(float width) + { + if (_ready && _worldCombo.Draw(width)) + UpdateIdentifiers(); + } + + public void DrawObjectKindCombo(float width) + { + if (!_ready) + return; + + ImGui.SetNextItemWidth(width); + using var combo = ImRaii.Combo("##newKind", _newKind.ToName()); + if (!combo) + return; + + foreach (var kind in ObjectKinds) + { + if (!ImGui.Selectable(kind.ToName(), _newKind == kind)) + continue; + + _newKind = kind; + UpdateIdentifiers(); + } + } + + public void DrawNewPlayerCollection(float width) + { + if (!_ready) + return; + + ImGui.SetNextItemWidth(width); + if (ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32)) + UpdateIdentifiers(); + } + + public void DrawNewNpcCollection(float width) + { + if (!_ready) + return; + + var combo = GetNpcCombo(_newKind); + if (combo.Draw(width)) + UpdateIdentifiers(); + } + + public void Dispose() + => _communicator.CollectionChange.Unsubscribe(UpdateIdentifiers); + + // Input Selections. + private string _newCharacterName = string.Empty; + private ObjectKind _newKind = ObjectKind.BattleNpc; + private ActorIdentifier[] _playerIdentifiers = Array.Empty(); + private ActorIdentifier[] _retainerIdentifiers = Array.Empty(); + private ActorIdentifier[] _npcIdentifiers = Array.Empty(); + private ActorIdentifier[] _ownedIdentifiers = Array.Empty(); + + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + + private static readonly IReadOnlyList ObjectKinds = new[] + { + ObjectKind.BattleNpc, + ObjectKind.EventNpc, + ObjectKind.Companion, + ObjectKind.MountType, + ObjectKind.Ornament, + }; + + private NpcCombo GetNpcCombo(ObjectKind kind) + => kind switch + { + ObjectKind.BattleNpc => _bnpcCombo, + ObjectKind.EventNpc => _enpcCombo, + ObjectKind.MountType => _mountCombo, + ObjectKind.Companion => _companionCombo, + ObjectKind.Ornament => _ornamentCombo, + _ => throw new NotImplementedException(), + }; + + /// Create combos when ready. + private void SetupCombos() + { + _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds); + _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts); + _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions); + _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments); + _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs); + _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs); + _ready = true; + _actorService.FinishedCreation -= SetupCombos; + } + + private void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) + { + if (type == CollectionType.Individual) + UpdateIdentifiers(); + } + + private void UpdateIdentifiers() + { + var combo = GetNpcCombo(_newKind); + PlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, + _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty(), out _playerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + RetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, + Array.Empty(), out _retainerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + if (combo.CurrentSelection.Ids != null) + { + NpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + combo.CurrentSelection.Ids, out _npcIdentifiers) switch + { + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + OwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, + _worldCombo.CurrentSelection.Key, _newKind, + combo.CurrentSelection.Ids, out _ownedIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + } + else + { + NpcTooltip = NewNpcTooltipEmpty; + OwnedTooltip = NewNpcTooltipEmpty; + _npcIdentifiers = Array.Empty(); + _ownedIdentifiers = Array.Empty(); + } + } +} diff --git a/Penumbra/UI/CollectionTab/IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/IndividualCollectionUi.cs deleted file mode 100644 index 226c9c8b..00000000 --- a/Penumbra/UI/CollectionTab/IndividualCollectionUi.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; -using Penumbra.Services; - -namespace Penumbra.UI.CollectionTab; - -public class IndividualCollectionUi -{ - private readonly ActorService _actorService; - private readonly CollectionManager _collectionManager; - private readonly CollectionCombo _withEmpty; - - public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionCombo withEmpty) - { - _actorService = actors; - _collectionManager = collectionManager; - _withEmpty = withEmpty; - if (_actorService.Valid) - SetupCombos(); - else - _actorService.FinishedCreation += SetupCombos; - } - - /// Draw all individual assignments as well as the options to create a new one. - public void Draw() - { - if (!_ready) - return; - - using var _ = ImRaii.Group(); - using var mainId = ImRaii.PushId("Individual"); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Individual {TutorialService.ConditionalIndividual}s"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker("Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" - + $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n" - + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections."); - ImGui.Separator(); - for (var i = 0; i < _collectionManager.Active.Individuals.Count; ++i) - { - DrawIndividualAssignment(i); - } - - UiHelpers.DefaultLineSpace(); - DrawNewIndividualCollection(); - } - - public void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) - { - if (type == CollectionType.Individual) - UpdateIdentifiers(); - } - - // Input Selections. - private string _newCharacterName = string.Empty; - private ObjectKind _newKind = ObjectKind.BattleNpc; - - private WorldCombo _worldCombo = null!; - private NpcCombo _mountCombo = null!; - private NpcCombo _companionCombo = null!; - private NpcCombo _ornamentCombo = null!; - private NpcCombo _bnpcCombo = null!; - private NpcCombo _enpcCombo = null!; - - private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; - private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; - private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; - private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; - private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; - private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; - - private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty(); - private string _newPlayerTooltip = NewPlayerTooltipEmpty; - private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty(); - private string _newRetainerTooltip = NewRetainerTooltipEmpty; - private ActorIdentifier[] _newNpcIdentifiers = Array.Empty(); - private string _newNpcTooltip = NewNpcTooltipEmpty; - private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty(); - private string _newOwnedTooltip = NewPlayerTooltipEmpty; - - private bool _ready; - - /// Create combos when ready. - private void SetupCombos() - { - _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds); - _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts); - _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions); - _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments); - _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs); - _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs); - _ready = true; - _actorService.FinishedCreation -= SetupCombos; - } - - - private static readonly IReadOnlyList ObjectKinds = new[] - { - ObjectKind.BattleNpc, - ObjectKind.EventNpc, - ObjectKind.Companion, - ObjectKind.MountType, - ObjectKind.Ornament, - }; - - /// Draw the Object Kind Selector. - private bool DrawNewObjectKindOptions(float width) - { - ImGui.SetNextItemWidth(width); - using var combo = ImRaii.Combo("##newKind", _newKind.ToName()); - if (!combo) - return false; - - var ret = false; - foreach (var kind in ObjectKinds) - { - if (!ImGui.Selectable(kind.ToName(), _newKind == kind)) - continue; - - _newKind = kind; - ret = true; - } - - return ret; - } - - private int _individualDragDropIdx = -1; - - /// Draw a single individual assignment. - private void DrawIndividualAssignment(int idx) - { - var (name, _) = _collectionManager.Active.Individuals[idx]; - using var id = ImRaii.PushId(idx); - _withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, - false, true)) - _collectionManager.Active.RemoveIndividualCollection(idx); - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable(name); - using (var source = ImRaii.DragDropSource()) - { - if (source) - { - ImGui.SetDragDropPayload("Individual", nint.Zero, 0); - _individualDragDropIdx = idx; - } - } - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !ImGuiUtil.IsDropping("Individual")) - return; - - if (_individualDragDropIdx >= 0) - _collectionManager.Active.MoveIndividualCollection(_individualDragDropIdx, idx); - - _individualDragDropIdx = -1; - } - - private bool DrawNewPlayerCollection(Vector2 buttonWidth, float width) - { - var change = _worldCombo.Draw(width); - ImGui.SameLine(); - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width); - change |= ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip, - _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0)) - { - _collectionManager.Active.CreateIndividualCollection(_newPlayerIdentifiers); - change = true; - } - - return change; - } - - private bool DrawNewNpcCollection(NpcCombo combo, Vector2 buttonWidth, float width) - { - var comboWidth = UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; - var change = DrawNewObjectKindOptions(width); - ImGui.SameLine(); - change |= combo.Draw(comboWidth); - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip, - _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0)) - { - _collectionManager.Active.CreateIndividualCollection(_newNpcIdentifiers); - change = true; - } - - return change; - } - - private bool DrawNewOwnedCollection(Vector2 buttonWidth) - { - if (!ImGuiUtil.DrawDisabledButton("Assign Owned NPC", buttonWidth, _newOwnedTooltip, - _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0)) - return false; - - _collectionManager.Active.CreateIndividualCollection(_newOwnedIdentifiers); - return true; - - } - - private bool DrawNewRetainerCollection(Vector2 buttonWidth) - { - if (!ImGuiUtil.DrawDisabledButton("Assign Bell Retainer", buttonWidth, _newRetainerTooltip, - _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0)) - return false; - - _collectionManager.Active.CreateIndividualCollection(_newRetainerIdentifiers); - return true; - - } - - private NpcCombo GetNpcCombo(ObjectKind kind) - => kind switch - { - ObjectKind.BattleNpc => _bnpcCombo, - ObjectKind.EventNpc => _enpcCombo, - ObjectKind.MountType => _mountCombo, - ObjectKind.Companion => _companionCombo, - ObjectKind.Ornament => _ornamentCombo, - _ => throw new NotImplementedException(), - }; - - private void DrawNewIndividualCollection() - { - var width = (UiHelpers.InputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X) / 3; - - var buttonWidth1 = new Vector2(90 * UiHelpers.Scale, 0); - var buttonWidth2 = new Vector2(120 * UiHelpers.Scale, 0); - - var assignWidth = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); - var change = DrawNewCurrentPlayerCollection(assignWidth); - ImGui.SameLine(); - change |= DrawNewTargetCollection(assignWidth); - - change |= DrawNewPlayerCollection(buttonWidth1, width); - ImGui.SameLine(); - change |= DrawNewRetainerCollection(buttonWidth2); - - var combo = GetNpcCombo(_newKind); - change |= DrawNewNpcCollection(combo, buttonWidth1, width); - ImGui.SameLine(); - change |= DrawNewOwnedCollection(buttonWidth2); - - if (change) - UpdateIdentifiers(); - } - - private bool DrawNewCurrentPlayerCollection(Vector2 width) - { - var player = _actorService.AwaitedService.GetCurrentPlayer(); - var result = _collectionManager.Active.Individuals.CanAdd(player); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No logged-in character detected.", - _ => string.Empty, - }; - - - if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid)) - return false; - - _collectionManager.Active.CreateIndividualCollection(player); - return true; - - } - - private bool DrawNewTargetCollection(Vector2 width) - { - var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true); - var result = _collectionManager.Active.Individuals.CanAdd(target); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No valid character in target detected.", - _ => string.Empty, - }; - if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid)) - { - _collectionManager.Active.CreateIndividualCollection(_collectionManager.Active.Individuals.GetGroup(target)); - return true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" - + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" - + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); - - return false; - } - - private void UpdateIdentifiers() - { - var combo = GetNpcCombo(_newKind); - _newPlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, - _worldCombo.CurrentSelection.Key, ObjectKind.None, - Array.Empty(), out _newPlayerIdentifiers) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newRetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, - Array.Empty(), out _newRetainerIdentifiers) switch - { - _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - if (combo.CurrentSelection.Ids != null) - { - _newNpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, - combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch - { - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newOwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, - _worldCombo.CurrentSelection.Key, _newKind, - combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - } - else - { - _newNpcTooltip = NewNpcTooltipEmpty; - _newOwnedTooltip = NewNpcTooltipEmpty; - _newNpcIdentifiers = Array.Empty(); - _newOwnedIdentifiers = Array.Empty(); - } - } -} diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 6a43f5b2..6a503f3d 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -7,32 +7,53 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; -using Penumbra.Collections.Manager; +using Penumbra.Collections.Manager; using Penumbra.UI.Classes; - + namespace Penumbra.UI.CollectionTab; public class InheritanceUi { - private const int InheritedCollectionHeight = 9; - private const string InheritanceDragDropLabel = "##InheritanceMove"; + private const int InheritedCollectionHeight = 9; + private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly CollectionManager _collectionManager; + private readonly Configuration _config; + private readonly CollectionStorage _collections; + private readonly ActiveCollections _active; + private readonly InheritanceManager _inheritance; + private readonly CollectionSelector _selector; + + public InheritanceUi(Configuration config, CollectionManager collectionManager, CollectionSelector selector) + { + _config = config; + _selector = selector; + _collections = collectionManager.Storage; + _active = collectionManager.Active; + _inheritance = collectionManager.Inheritances; + } - public InheritanceUi(CollectionManager collectionManager) - => _collectionManager = collectionManager; /// Draw the whole inheritance block. public void Draw() { - using var group = ImRaii.Group(); - using var id = ImRaii.PushId("##Inheritance"); - ImGui.TextUnformatted($"The {TutorialService.SelectedCollection} inherits from:"); - DrawCurrentCollectionInheritance(); + using var id = ImRaii.PushId("##Inheritance"); + ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), (Name(_active.Current), ColorId.SelectedCollection.Value(_config) | 0xFF000000), (" inherits from:", 0)); + ImGui.Dummy(Vector2.One); + + DrawCurrentCollectionInheritance(); + ImGui.SameLine(); DrawInheritanceTrashButton(); + ImGui.SameLine(); + DrawRightText(); + DrawNewInheritanceSelection(); - DelayedActions(); - } + ImGui.SameLine(); + if (ImGui.Button("More Information about Inheritance", new Vector2(ImGui.GetContentRegionAvail().X, 0))) + ImGui.OpenPopup("InheritanceHelp"); + + DrawHelpPopup(); + DelayedActions(); + } // Keep for reuse. private readonly HashSet _seenInheritedCollections = new(32); @@ -40,8 +61,45 @@ public class InheritanceUi // Execute changes only outside of loops. private ModCollection? _newInheritance; private ModCollection? _movedInheritance; - private (int, int)? _inheritanceAction; - private ModCollection? _newCurrentCollection; + private (int, int)? _inheritanceAction; + private ModCollection? _newCurrentCollection; + + private void DrawRightText() + { + using var group = ImRaii.Group(); + ImGuiUtil.TextWrapped( + "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod."); + ImGuiUtil.TextWrapped( + "You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can."); + } + + private void DrawHelpPopup() + => ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.NewLine(); + ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); + ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); + ImGui.BulletText( + "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); + ImGui.BulletText( + "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); + ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); + ImGui.BulletText( + "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); + ImGui.NewLine(); + ImGui.TextUnformatted("Example"); + ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); + ImGui.BulletText( + "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); + ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); + ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); + ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); + ImGui.BulletText( + "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); + }); + /// /// If an inherited collection is expanded, @@ -49,15 +107,15 @@ public class InheritanceUi /// private void DrawInheritedChildren(ModCollection collection) { - using var id = ImRaii.PushId(collection.Index); + using var id = ImRaii.PushId(collection.Index); using var indent = ImRaii.PushIndent(); // Get start point for the lines (top of the selector). // Tree line stuff. var lineStart = ImGui.GetCursorScreenPos(); - var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; - var drawList = ImGui.GetWindowDrawList(); - var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale); + var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; + var drawList = ImGui.GetWindowDrawList(); + var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale); lineStart.X += offsetX; lineStart.Y -= 2 * UiHelpers.Scale; var lineEnd = lineStart; @@ -70,7 +128,7 @@ public class InheritanceUi _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode(inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); @@ -95,7 +153,7 @@ public class InheritanceUi using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), _seenInheritedCollections.Contains(collection)); _seenInheritedCollections.Add(collection); - using var tree = ImRaii.TreeNode(collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen); + using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); color.Pop(); DrawInheritanceTreeClicks(collection, true); DrawInheritanceDropSource(collection); @@ -117,16 +175,15 @@ public class InheritanceUi return; _seenInheritedCollections.Clear(); - _seenInheritedCollections.Add(_collectionManager.Active.Current); - foreach (var collection in _collectionManager.Active.Current.DirectlyInheritsFrom.ToList()) + _seenInheritedCollections.Add(_active.Current); + foreach (var collection in _active.Current.DirectlyInheritsFrom.ToList()) DrawInheritance(collection); } /// Draw a drag and drop button to delete. private void DrawInheritanceTrashButton() { - ImGui.SameLine(); - var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight }; + var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight }; var buttonColor = ImGui.GetColorU32(ImGuiCol.Button); // Prevent hovering from highlighting the button. using var color = ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor) @@ -136,7 +193,7 @@ public class InheritanceUi using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); } /// @@ -147,7 +204,7 @@ public class InheritanceUi { if (_newCurrentCollection != null) { - _collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current); + _active.SetCollection(_newCurrentCollection, CollectionType.Current); _newCurrentCollection = null; } @@ -157,9 +214,9 @@ public class InheritanceUi if (_inheritanceAction.Value.Item1 >= 0) { if (_inheritanceAction.Value.Item2 == -1) - _collectionManager.Inheritances.RemoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1); + _inheritance.RemoveInheritance(_active.Current, _inheritanceAction.Value.Item1); else - _collectionManager.Inheritances.MoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + _inheritance.MoveInheritance(_active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); } _inheritanceAction = null; @@ -173,57 +230,23 @@ public class InheritanceUi { DrawNewInheritanceCombo(); ImGui.SameLine(); - var inheritance = InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, _newInheritance); + var inheritance = InheritanceManager.CheckValidInheritance(_active.Current, _newInheritance); var tt = inheritance switch { - InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.", - InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", - InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.", + InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.", + InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", + InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.", InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.", - InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", - _ => string.Empty, + InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", + _ => string.Empty, }; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, inheritance != InheritanceManager.ValidInheritance.Valid, true) - && _collectionManager.Inheritances.AddInheritance(_collectionManager.Active.Current, _newInheritance!)) + && _inheritance.AddInheritance(_active.Current, _newInheritance!)) _newInheritance = null; if (inheritance != InheritanceManager.ValidInheritance.Valid) _newInheritance = null; - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), UiHelpers.IconButtonSize, "What is Inheritance?", - false, true)) - ImGui.OpenPopup("InheritanceHelp"); - - ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 21 * ImGui.GetTextLineHeightWithSpacing()), () => - { - ImGui.NewLine(); - ImGui.TextWrapped( - "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod."); - ImGui.NewLine(); - ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); - ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); - ImGui.BulletText( - "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); - ImGui.BulletText( - "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); - ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); - ImGui.BulletText( - "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); - ImGui.NewLine(); - ImGui.TextUnformatted("Example"); - ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); - ImGui.BulletText( - "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); - ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); - ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); - ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); - ImGui.BulletText( - "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); - }); } /// @@ -233,18 +256,18 @@ public class InheritanceUi private void DrawNewInheritanceCombo() { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); - _newInheritance ??= _collectionManager.Storage.FirstOrDefault(c - => c != _collectionManager.Active.Current && !_collectionManager.Active.Current.DirectlyInheritsFrom.Contains(c)) + _newInheritance ??= _collections.FirstOrDefault(c + => c != _active.Current && !_active.Current.DirectlyInheritsFrom.Contains(c)) ?? ModCollection.Empty; - using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); + using var combo = ImRaii.Combo("##newInheritance", Name(_newInheritance)); if (!combo) return; - foreach (var collection in _collectionManager.Storage - .Where(c => InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, c) == InheritanceManager.ValidInheritance.Valid) + foreach (var collection in _collections + .Where(c => InheritanceManager.CheckValidInheritance(_active.Current, c) == InheritanceManager.ValidInheritance.Valid) .OrderBy(c => c.Name)) { - if (ImGui.Selectable(collection.Name, _newInheritance == collection)) + if (ImGui.Selectable(Name(collection), _newInheritance == collection)) _newInheritance = collection; } } @@ -261,8 +284,8 @@ public class InheritanceUi if (_movedInheritance != null) { - var idx1 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); - var idx2 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection); + var idx1 = _active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); + var idx2 = _active.Current.DirectlyInheritsFrom.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -279,7 +302,7 @@ public class InheritanceUi ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0); _movedInheritance = collection; - ImGui.TextUnformatted($"Moving {_movedInheritance?.Name ?? "Unknown"}..."); + ImGui.TextUnformatted($"Moving {(_movedInheritance != null ? Name(_movedInheritance) : "Unknown")}..."); } /// @@ -292,7 +315,7 @@ public class InheritanceUi if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); + _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); else _newCurrentCollection = collection; } @@ -300,4 +323,7 @@ public class InheritanceUi ImGuiUtil.HoverTooltip($"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one." + (withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty)); } + + private string Name(ModCollection collection) + => _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; } diff --git a/Penumbra/UI/CollectionTab/NpcCombo.cs b/Penumbra/UI/CollectionTab/NpcCombo.cs index 9e0ffe14..7095a78b 100644 --- a/Penumbra/UI/CollectionTab/NpcCombo.cs +++ b/Penumbra/UI/CollectionTab/NpcCombo.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Utility; using ImGuiNET; using OtterGui.Widgets; @@ -10,7 +12,8 @@ public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)> private readonly string _label; public NpcCombo(string label, IReadOnlyDictionary names) - : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key).ToList()) + : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key, Comparer) + .ToList()) => _label = label; protected override string ToString((string Name, uint[] Ids) obj) @@ -28,4 +31,24 @@ public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)> public bool Draw(float width) => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + + + /// Compare strings in a way that letters and numbers are sorted before any special symbols. + private class NameComparer : IComparer + { + public int Compare(string? x, string? y) + { + if (x.IsNullOrEmpty() || y.IsNullOrEmpty()) + return StringComparer.OrdinalIgnoreCase.Compare(x, y); + + return (char.IsAsciiLetterOrDigit(x[0]), char.IsAsciiLetterOrDigit(y[0])) switch + { + (true, false) => -1, + (false, true) => 1, + _ => StringComparer.OrdinalIgnoreCase.Compare(x, y), + }; + } + } + + private static readonly NameComparer Comparer = new(); } diff --git a/Penumbra/UI/CollectionTab/SpecialCombo.cs b/Penumbra/UI/CollectionTab/SpecialCombo.cs deleted file mode 100644 index ab332399..00000000 --- a/Penumbra/UI/CollectionTab/SpecialCombo.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ImGuiNET; -using OtterGui.Classes; -using OtterGui.Widgets; -using Penumbra.Collections.Manager; - -namespace Penumbra.UI.CollectionTab; - -public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> -{ - private readonly CollectionManager _collectionManager; - - public (CollectionType, string, string)? CurrentType - => CollectionTypeExtensions.Special[CurrentIdx]; - - public int CurrentIdx; - private readonly float _unscaledWidth; - private readonly string _label; - - public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth) - : base(CollectionTypeExtensions.Special, false) - { - _collectionManager = collectionManager; - _label = label; - _unscaledWidth = unscaledWidth; - } - - public void Draw() - { - var preview = CurrentIdx >= 0 ? Items[CurrentIdx].Item2 : string.Empty; - Draw(_label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * UiHelpers.Scale, - ImGui.GetTextLineHeightWithSpacing()); - } - - protected override string ToString((CollectionType, string, string) obj) - => obj.Item2; - - protected override bool IsVisible(int globalIdx, LowerString filter) - { - var obj = Items[globalIdx]; - return filter.IsContained(obj.Item2) && _collectionManager.Active.ByType(obj.Item1) == null; - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 7e465a82..1a02284e 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -44,7 +44,7 @@ public sealed class ConfigWindow : Window RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(900, 600), + MinimumSize = new Vector2(900, 675), MaximumSize = new Vector2(4096, 2160), }; tutorial.UpdateTutorialStep(); diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 5370e16e..ca6b6932 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,18 +1,13 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Numerics; -using System.Text; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; +using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; @@ -20,514 +15,44 @@ using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; -public sealed class CollectionTree -{ - private readonly CollectionStorage _collections; - private readonly ActiveCollections _active; - private readonly CollectionSelector2 _selector; - private readonly ActorService _actors; - private readonly TargetManager _targets; - - private static readonly IReadOnlyList<(string Name, uint Border)> Buttons = CreateButtons(); - private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); - - public CollectionTree(CollectionManager manager, CollectionSelector2 selector, ActorService actors, - TargetManager targets) - { - _collections = manager.Storage; - _active = manager.Active; - _selector = selector; - _actors = actors; - _targets = targets; - } - - public void DrawSimple() - { - var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) - .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); - DrawSimpleCollectionButton(CollectionType.Default, buttonWidth); - DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth); - DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth); - DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); - - var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale }; - var player = _actors.AwaitedService.GetCurrentPlayer(); - DrawButton($"Current Character ({(player.IsValid ? player.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, - player); - ImGui.SameLine(); - - var target = _actors.AwaitedService.FromObject(_targets.Target, false, true, true); - DrawButton($"Current Target ({(target.IsValid ? target.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, target); - if (_active.Individuals.Count > 0) - { - ImGui.TextUnformatted("Currently Active Individual Assignments"); - for (var i = 0; i < _active.Individuals.Count; ++i) - { - var (name, ids, coll) = _active.Individuals.Assignments[i]; - DrawButton(name, CollectionType.Individual, buttonWidth, 0, ids[0], coll); - - ImGui.SameLine(); - if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X - && i < _active.Individuals.Count - 1) - ImGui.NewLine(); - } - - ImGui.NewLine(); - } - - var first = true; - - void Button(CollectionType type) - { - var (name, border) = Buttons[(int)type]; - var collection = _active.ByType(type); - if (collection == null) - return; - - if (first) - { - ImGui.Separator(); - ImGui.TextUnformatted("Currently Active Advanced Assignments"); - first = false; - } - DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, collection); - ImGui.SameLine(); - if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) - ImGui.NewLine(); - } - - Button(CollectionType.NonPlayerChild); - Button(CollectionType.NonPlayerElderly); - foreach (var race in Enum.GetValues().Skip(1)) - { - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); - } - } - - public void DrawAdvanced() - { - using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) - .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); - - var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); - var dummy = new Vector2(1, 0); - - foreach (var (type, pre, post, name, border) in AdvancedTree) - { - ImGui.TableNextColumn(); - if (type is CollectionType.Inactive) - continue; - - if (pre) - ImGui.Dummy(dummy); - DrawAssignmentButton(type, buttonWidth, name, border); - if (post) - ImGui.Dummy(dummy); - } - } - - private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix = 'i') - { - var label = $"{type}{identifier}{suffix}"; - if (open) - ImGui.OpenPopup(label); - - using var context = ImRaii.Popup(label); - if (context) - { - using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor)) - { - if (ImGui.MenuItem("Use no mods.")) - _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); - } - - if (collection != null) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - if (ImGui.MenuItem("Remove this assignment.")) - _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); - } - - foreach (var coll in _collections) - { - if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) - _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); - } - } - } - - private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, ModCollection? collection = null) - { - using var group = ImRaii.Group(); - var invalid = type == CollectionType.Individual && !id.IsValid; - var redundancy = _active.RedundancyCheck(type, id); - collection ??= _active.ByType(type, id); - using var color = ImRaii.PushColor(ImGuiCol.Button, - collection == null - ? 0 - : redundancy.Length > 0 - ? Colors.RedundantColor - : collection == _active.Current - ? Colors.SelectedColor - : collection == ModCollection.Empty - ? Colors.RedTableBgTint - : ImGui.GetColorU32(ImGuiCol.Button), !invalid) - .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); - using var disabled = ImRaii.Disabled(invalid); - var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); - var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); - if (!invalid) - { - _selector.DragTarget(type, id); - var name = collection == ModCollection.Empty ? "Use No Mods" : collection?.Name ?? "Unassigned"; - var size = ImGui.CalcTextSize(name); - var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; - ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); - DrawContext(button, collection, type, id); - } - - if (hovered) - ImGui.SetTooltip(redundancy); - - return button; - } - - private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) - { - DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid); - ImGui.SameLine(); - var secondLine = string.Empty; - foreach (var parent in type.InheritanceOrder()) - { - var coll = _active.ByType(parent); - if (coll == null) - continue; - - secondLine = $"\nWill behave as {parent.ToName()} ({coll.Name}) while unassigned."; - break; - } - - ImGui.TextUnformatted(type.ToDescription() + secondLine); - ImGui.Separator(); - } - - private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color) - => DrawButton(name, type, width, color, ActorIdentifier.Invalid, _active.ByType(type)); - - private static IReadOnlyList<(string Name, uint Border)> CreateButtons() - { - var ret = Enum.GetValues().Select(t => (t.ToName(), 0u)).ToArray(); - - foreach (var race in Enum.GetValues().Skip(1)) - { - var color = race switch - { - SubRace.Midlander => 0xAA5C9FE4u, - SubRace.Highlander => 0xAA5C9FE4u, - SubRace.Wildwood => 0xAA5C9F49u, - SubRace.Duskwight => 0xAA5C9F49u, - SubRace.Plainsfolk => 0xAAEF8CB6u, - SubRace.Dunesfolk => 0xAAEF8CB6u, - SubRace.SeekerOfTheSun => 0xAA8CEFECu, - SubRace.KeeperOfTheMoon => 0xAA8CEFECu, - SubRace.Seawolf => 0xAAEFE68Cu, - SubRace.Hellsguard => 0xAAEFE68Cu, - SubRace.Raen => 0xAAB5EF8Cu, - SubRace.Xaela => 0xAAB5EF8Cu, - SubRace.Helion => 0xAAFFFFFFu, - SubRace.Lost => 0xAAFFFFFFu, - SubRace.Rava => 0xAA607FA7u, - SubRace.Veena => 0xAA607FA7u, - _ => 0u, - }; - - ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color); - ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color); - ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color); - ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color); - } - - ret[(int)CollectionType.MalePlayerCharacter] = ("♂ Player", 0); - ret[(int)CollectionType.FemalePlayerCharacter] = ("♀ Player", 0); - ret[(int)CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0); - ret[(int)CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0); - return ret; - } - - private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree() - { - var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count); - - void Add(CollectionType type, bool pre, bool post) - { - var (name, border) = (int)type >= Buttons.Count ? (type.ToName(), 0) : Buttons[(int)type]; - ret.Add((type, pre, post, name, border)); - } - - Add(CollectionType.Default, false, false); - Add(CollectionType.Interface, false, false); - Add(CollectionType.Inactive, false, false); - Add(CollectionType.Inactive, false, false); - Add(CollectionType.Yourself, false, true); - Add(CollectionType.Inactive, false, true); - Add(CollectionType.NonPlayerChild, false, true); - Add(CollectionType.NonPlayerElderly, false, true); - Add(CollectionType.MalePlayerCharacter, true, true); - Add(CollectionType.FemalePlayerCharacter, true, true); - Add(CollectionType.MaleNonPlayerCharacter, true, true); - Add(CollectionType.FemaleNonPlayerCharacter, true, true); - var pre = true; - foreach (var race in Enum.GetValues().Skip(1)) - { - Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre); - pre = !pre; - } - - return ret; - } -} - -public sealed class CollectionPanel -{ - private readonly CollectionManager _manager; - private readonly ModStorage _modStorage; - private readonly InheritanceUi _inheritanceUi; - - public CollectionPanel(CollectionManager manager, ModStorage modStorage) - { - _manager = manager; - _modStorage = modStorage; - _inheritanceUi = new InheritanceUi(_manager); - } - - public void Draw() - { - var collection = _manager.Active.Current; - DrawName(collection); - DrawStatistics(collection); - _inheritanceUi.Draw(); - DrawSettingsList(collection); - DrawInactiveSettingsList(collection); - } - - private void DrawName(ModCollection collection) - { - ImGui.TextUnformatted($"{collection.Name} ({collection.AnonymizedName})"); - } - - private void DrawStatistics(ModCollection collection) - { - ImGui.TextUnformatted("Used for:"); - var sb = new StringBuilder(128); - if (_manager.Active.Default == collection) - sb.Append(CollectionType.Default.ToName()).Append(", "); - if (_manager.Active.Interface == collection) - sb.Append(CollectionType.Interface.ToName()).Append(", "); - foreach (var (type, _) in _manager.Active.SpecialAssignments.Where(p => p.Value == collection)) - sb.Append(type.ToName()).Append(", "); - foreach (var (name, _) in _manager.Active.Individuals.Where(p => p.Collection == collection)) - sb.Append(name).Append(", "); - - ImGui.SameLine(); - ImGuiUtil.TextWrapped(sb.Length == 0 ? "Nothing" : sb.ToString(0, sb.Length - 2)); - - if (collection.DirectParentOf.Count > 0) - { - ImGui.TextUnformatted("Inherited by:"); - ImGui.SameLine(); - ImGuiUtil.TextWrapped(string.Join(", ", collection.DirectParentOf.Select(c => c.Name))); - } - } - - private void DrawSettingsList(ModCollection collection) - { - using var box = ImRaii.ListBox("##activeSettings"); - if (!box) - return; - - foreach (var (mod, (settings, parent)) in _modStorage.Select(m => (m, collection[m.Index])).Where(t => t.Item2.Settings != null) - .OrderBy(t => t.m.Name)) - ImGui.TextUnformatted($"{mod}{(parent != collection ? $" (inherited from {parent.Name})" : string.Empty)}"); - } - - private void DrawInactiveSettingsList(ModCollection collection) - { - if (collection.UnusedSettings.Count == 0) - return; - - if (ImGui.Button("Clear Unused Settings")) - _manager.Storage.CleanUnavailableSettings(collection); - - using var box = ImRaii.ListBox("##inactiveSettings"); - if (!box) - return; - - foreach (var name in collection.UnusedSettings.Keys) - ImGui.TextUnformatted(name); - } -} - -public sealed class CollectionSelector2 : ItemSelector, IDisposable -{ - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionStorage _storage; - private readonly ActiveCollections _active; - - private ModCollection? _dragging; - - public CollectionSelector2(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active) - : base(new List(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) - { - _config = config; - _communicator = communicator; - _storage = storage; - _active = active; - - _communicator.CollectionChange.Subscribe(OnCollectionChange); - // Set items. - OnCollectionChange(CollectionType.Inactive, null, null, string.Empty); - // Set selection. - OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty); - } - - protected override bool OnDelete(int idx) - { - if (idx < 0 || idx >= Items.Count) - return false; - - return _storage.RemoveCollection(Items[idx]); - } - - protected override bool DeleteButtonEnabled() - => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); - - protected override string DeleteButtonTooltip() - => _storage.DefaultNamed == Current - ? $"The selected collection {Current.Name} can not be deleted." - : $"Delete the currently selected collection {Current?.Name}. Hold {_config.DeleteModModifier} to delete."; - - protected override bool OnAdd(string name) - => _storage.AddCollection(name, null); - - protected override bool OnDuplicate(string name, int idx) - { - if (idx < 0 || idx >= Items.Count) - return false; - - return _storage.AddCollection(name, Items[idx]); - } - - protected override bool Filtered(int idx) - => !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); - - protected override bool OnDraw(int idx) - { - using var color = ImRaii.PushColor(ImGuiCol.Header, Colors.SelectedColor); - var ret = ImGui.Selectable(Items[idx].Name, idx == CurrentIdx); - using var source = ImRaii.DragDropSource(); - if (source) - { - _dragging = Items[idx]; - ImGui.SetDragDropPayload("Assignment", nint.Zero, 0); - ImGui.TextUnformatted($"Assigning {_dragging.Name} to..."); - } - - if (ret) - _active.SetCollection(Items[idx], CollectionType.Current); - - return ret; - } - - public void DragTarget(CollectionType type, ActorIdentifier identifier) - { - using var target = ImRaii.DragDropTarget(); - if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping("Assignment")) - return; - - _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); - _dragging = null; - } - - public void Dispose() - { - _communicator.CollectionChange.Unsubscribe(OnCollectionChange); - } - - private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) - { - switch (type) - { - case CollectionType.Temporary: return; - case CollectionType.Current: - if (@new != null) - SetCurrent(@new); - SetFilterDirty(); - return; - case CollectionType.Inactive: - Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Name)) - Items.Add(c); - - if (old == Current) - ClearCurrentSelection(); - else - TryRestoreCurrent(); - SetFilterDirty(); - return; - default: - SetFilterDirty(); - return; - } - } -} - public class CollectionsTab : IDisposable, ITab { - private readonly CommunicatorService _communicator; - private readonly Configuration _configuration; - private readonly CollectionManager _collectionManager; - private readonly CollectionSelector2 _selector; - private readonly CollectionPanel _panel; - private readonly CollectionTree _tree; + private readonly Configuration _config; + private readonly CollectionSelector _selector; + private readonly CollectionPanel _panel; + private readonly TutorialService _tutorial; public enum PanelMode { SimpleAssignment, - ComplexAssignment, + IndividualAssignment, + GroupAssignment, Details, }; - public PanelMode Mode = PanelMode.SimpleAssignment; - - public CollectionsTab(CommunicatorService communicator, Configuration configuration, CollectionManager collectionManager, - ModStorage modStorage, ActorService actors, TargetManager targets) + public PanelMode Mode { - _communicator = communicator; - _configuration = configuration; - _collectionManager = collectionManager; - _selector = new CollectionSelector2(_configuration, _communicator, _collectionManager.Storage, _collectionManager.Active); - _panel = new CollectionPanel(_collectionManager, modStorage); - _tree = new CollectionTree(collectionManager, _selector, actors, targets); + get => _config.CollectionPanel; + set + { + _config.CollectionPanel = value; + _config.Save(); + } + } + + public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, + CollectionManager collectionManager, ModStorage modStorage, ActorService actors, TargetManager targets, TutorialService tutorial) + { + _config = configuration; + _tutorial = tutorial; + _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); + _panel = new CollectionPanel(pi, configuration, communicator, collectionManager, _selector, actors, targets, modStorage); } public void Dispose() { _selector.Dispose(); + _panel.Dispose(); } public ReadOnlySpan Label @@ -535,41 +60,79 @@ public class CollectionsTab : IDisposable, ITab public void DrawContent() { - var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnn").X; - _selector.Draw(width); + var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnnnn").X; + using (var group = ImRaii.Group()) + { + _selector.Draw(width); + } + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); + ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawHeaderLine(); - DrawPanel(); + using (var group = ImRaii.Group()) + { + DrawHeaderLine(); + DrawPanel(); + } + } + + public void DrawHeader() + { + _tutorial.OpenTutorial(BasicTutorialSteps.Collections); } private void DrawHeaderLine() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 3f, 0); + var withSpacing = ImGui.GetFrameHeightWithSpacing(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - withSpacing) / 4f, ImGui.GetFrameHeight()); using var _ = ImRaii.Group(); using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.SimpleAssignment); if (ImGui.Button("Simple Assignments", buttonSize)) Mode = PanelMode.SimpleAssignment; - - ImGui.SameLine(); color.Pop(); + _tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments); + ImGui.SameLine(); + + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.IndividualAssignment); + if (ImGui.Button("Individual Assignments", buttonSize)) + Mode = PanelMode.IndividualAssignment; + color.Pop(); + _tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments); + ImGui.SameLine(); + + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.GroupAssignment); + if (ImGui.Button("Group Assignments", buttonSize)) + Mode = PanelMode.GroupAssignment; + color.Pop(); + _tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments); + ImGui.SameLine(); + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.Details); if (ImGui.Button("Collection Details", buttonSize)) Mode = PanelMode.Details; - - ImGui.SameLine(); color.Pop(); - color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.ComplexAssignment); - if (ImGui.Button("Advanced Assignments", buttonSize)) - Mode = PanelMode.ComplexAssignment; + _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); + ImGui.SameLine(); + + style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value(_config)) + .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value(_config)); + if (ImGuiUtil.DrawDisabledButton( + $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", + buttonSize with { X = withSpacing }, string.Empty, false, true)) + _selector.IncognitoMode = !_selector.IncognitoMode; + var hovered = ImGui.IsItemHovered(); + _tutorial.OpenTutorial(BasicTutorialSteps.Incognito); + color.Pop(2); + if (hovered) + ImGui.SetTooltip(_selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); } private void DrawPanel() { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - using var child = ImRaii.Child("##CollectionSettings", new Vector2(-1, 0), true, ImGuiWindowFlags.HorizontalScrollbar); + using var child = ImRaii.Child("##CollectionSettings", new Vector2(ImGui.GetContentRegionAvail().X, 0), true); if (!child) return; @@ -577,13 +140,16 @@ public class CollectionsTab : IDisposable, ITab switch (Mode) { case PanelMode.SimpleAssignment: - _tree.DrawSimple(); + _panel.DrawSimple(); break; - case PanelMode.ComplexAssignment: - _tree.DrawAdvanced(); + case PanelMode.IndividualAssignment: + _panel.DrawIndividualPanel(); + break; + case PanelMode.GroupAssignment: + _panel.DrawGroupPanel(); break; case PanelMode.Details: - _panel.Draw(); + _panel.DrawDetailsPanel(); break; } diff --git a/Penumbra/UI/Tabs/CollectionsTabOld.cs b/Penumbra/UI/Tabs/CollectionsTabOld.cs deleted file mode 100644 index 15655e2a..00000000 --- a/Penumbra/UI/Tabs/CollectionsTabOld.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Services; -using Penumbra.UI.CollectionTab; - -namespace Penumbra.UI.Tabs; - -public class CollectionsTabOld : IDisposable, ITab -{ - private readonly CommunicatorService _communicator; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TutorialService _tutorial; - private readonly SpecialCombo _specialCollectionCombo; - - private readonly CollectionCombo _collectionsWithEmpty; - private readonly CollectionCombo _collectionCombo; - private readonly InheritanceUi _inheritance; - private readonly IndividualCollectionUi _individualCollections; - - public CollectionsTabOld(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager, - TutorialService tutorial, Configuration config) - { - _communicator = communicator; - _collectionManager = collectionManager; - _tutorial = tutorial; - _config = config; - _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); - _collectionsWithEmpty = new CollectionCombo(_collectionManager, - () => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); - _collectionCombo = new CollectionCombo(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); - _inheritance = new InheritanceUi(_collectionManager); - _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); - - _communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); - } - - public ReadOnlySpan Label - => "Collections"u8; - - /// Draw a collection selector of a certain width for a certain type. - public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) - => (withEmpty ? _collectionsWithEmpty : _collectionCombo).Draw(label, width, collectionType); - - public void Dispose() - => _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); - - /// Draw a tutorial step regardless of tab selection. - public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); - - public void DrawContent() - { - using var child = ImRaii.Child("##collections", -Vector2.One); - if (child) - { - DrawActiveCollectionSelectors(); - DrawMainSelectors(); - } - } - - #region New Collections - - // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection; - - /// - /// Create a new collection that is either empty or a duplicate of the current collection. - /// Resets the new collection name. - /// - private void CreateNewCollection(bool duplicate) - { - if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null)) - _newCollectionName = string.Empty; - } - - /// Draw the Clean Unused Settings button if there are any. - private void DrawCleanCollectionButton(Vector2 width) - { - if (_collectionManager.Active.Current.UnusedSettings.Count == 0) - return; - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton( - $"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width - , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." - , false)) - _collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current); - } - - /// Draw the new collection input as well as its buttons. - private void DrawNewCollectionInput(Vector2 width) - { - // Input for new collection name. Also checks for validity when changed. - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) - _canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of enabled mods."); - - // Creation buttons. - var tt = _canAddCollection - ? string.Empty - : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; - if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) - CreateNewCollection(false); - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection)) - CreateNewCollection(true); - } - - #endregion - - #region Collection Selection - - /// Draw all collection assignment selections. - private void DrawActiveCollectionSelectors() - { - UiHelpers.DefaultLineSpace(); - var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); - _tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); - if (!open) - return; - - UiHelpers.DefaultLineSpace(); - - DrawDefaultCollectionSelector(); - _tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); - DrawInterfaceCollectionSelector(); - _tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); - UiHelpers.DefaultLineSpace(); - - DrawSpecialAssignments(); - _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); - UiHelpers.DefaultLineSpace(); - - _individualCollections.Draw(); - _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); - UiHelpers.DefaultLineSpace(); - } - - private void DrawCurrentCollectionSelector(Vector2 width) - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection, - "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); - - // Deletion conditions. - var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; - var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = deleteCondition - ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollectionName}."; - - if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) - _collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); - - DrawCleanCollectionButton(width); - } - - /// Draw the selector for the default collection assignment. - private void DrawDefaultCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, - $"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," - + "as well as any character for whom no more specific conditions from below apply."); - } - - /// Draw the selector for the interface collection assignment. - private void DrawInterfaceCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, - $"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); - } - - /// Description for character groups used in multiple help markers. - private const string CharacterGroupDescription = - $"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {TutorialService.DefaultCollection},\n" - + $"but all {TutorialService.IndividualAssignments} take precedence before them."; - - /// Draw the entire group assignment section. - private void DrawSpecialAssignments() - { - using var _ = ImRaii.Group(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(TutorialService.CharacterGroups); - ImGuiComponents.HelpMarker(CharacterGroupDescription); - ImGui.Separator(); - DrawSpecialCollections(); - ImGui.Dummy(Vector2.Zero); - DrawNewSpecialCollection(); - } - - /// Draw a new combo to select special collections as well as button to create it. - private void DrawNewSpecialCollection() - { - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - if (_specialCollectionCombo.CurrentIdx == -1 - || _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) - { - _specialCollectionCombo.ResetFilter(); - _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); - } - - if (_specialCollectionCombo.CurrentType == null) - return; - - _specialCollectionCombo.Draw(); - ImGui.SameLine(); - var disabled = _specialCollectionCombo.CurrentType == null; - var tt = disabled - ? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" - + CharacterGroupDescription - : CharacterGroupDescription; - if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) - return; - - _collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); - _specialCollectionCombo.CurrentIdx = -1; - } - - #endregion - - #region Current Collection Editing - - /// Draw the current collection selection, the creation of new collections and the inheritance block. - private void DrawMainSelectors() - { - UiHelpers.DefaultLineSpace(); - var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen); - _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); - if (!open) - return; - - var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); - UiHelpers.DefaultLineSpace(); - - DrawCurrentCollectionSelector(width); - _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); - UiHelpers.DefaultLineSpace(); - - DrawNewCollectionInput(width); - UiHelpers.DefaultLineSpace(); - - _inheritance.Draw(); - _tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); - } - - /// Draw all currently set special collections. - private void DrawSpecialCollections() - { - foreach (var (type, name, desc) in CollectionTypeExtensions.Special) - { - var collection = _collectionManager.Active.ByType(type); - if (collection == null) - continue; - - using var id = ImRaii.PushId((int)type); - DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, - false, true)) - { - _collectionManager.Active.RemoveSpecialCollection(type); - _specialCollectionCombo.ResetFilter(); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker(name, desc); - } - } - - #endregion -} diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index a69b6cd7..f778daea 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -37,8 +37,8 @@ public class ConfigTabBar Tabs = new ITab[] { Settings, - Mods, Collections, + Mods, ChangedItems, Effective, OnScreenTab, diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 18db156c..f63cdf2a 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -172,7 +172,7 @@ public class ModsTab : ITab ImGui.SameLine(); DrawInheritedCollectionButton(3 * buttonSize); ImGui.SameLine(); - _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, CollectionType.Current); + _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value(_config)); } _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index d29ce49a..80a181a4 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -60,7 +60,6 @@ public class SettingsTab : ITab _tutorial.OpenTutorial(BasicTutorialSteps.Fin); _tutorial.OpenTutorial(BasicTutorialSteps.Faq1); _tutorial.OpenTutorial(BasicTutorialSteps.Faq2); - _tutorial.OpenTutorial(BasicTutorialSteps.Faq3); } public void DrawContent() @@ -633,7 +632,7 @@ public class SettingsTab : ITab private void DrawAdvancedSettings() { var header = ImGui.CollapsingHeader("Advanced"); - _tutorial.OpenTutorial(BasicTutorialSteps.AdvancedSettings); + _tutorial.OpenTutorial(BasicTutorialSteps.Deprecated1); if (!header) return; diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 2426160a..9671abde 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.CompilerServices; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -12,17 +13,17 @@ public enum BasicTutorialSteps GeneralTooltips, ModDirectory, EnableMods, - AdvancedSettings, + Deprecated1, GeneralSettings, Collections, EditingCollections, CurrentCollection, - Inheritance, - ActiveCollections, - DefaultCollection, - InterfaceCollection, - SpecialCollections1, - SpecialCollections2, + SimpleAssignments, + IndividualAssignments, + GroupAssignments, + CollectionDetails, + Incognito, + Deprecated2, Mods, ModImport, AdvancedHelp, @@ -33,9 +34,9 @@ public enum BasicTutorialSteps Priority, ModOptions, Fin, + Deprecated3, Faq1, Faq2, - Faq3, Favorites, Tags, } @@ -49,9 +50,6 @@ public class TutorialService public const string ActiveCollections = "Active Collections"; public const string AssignedCollections = "Assigned Collections"; public const string GroupAssignment = "Group Assignment"; - public const string CharacterGroups = "Character Groups"; - public const string ConditionalGroup = "Group"; - public const string ConditionalIndividual = "Character"; public const string IndividualAssignments = "Individual Assignments"; public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" @@ -86,35 +84,26 @@ public class TutorialService .Register("Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + "This is our next stop!\n\n" + "Go here after setting up your root folder to continue the tutorial!") - .Register("Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n" - + "In here, we can create new collections, delete collections, or make them inherit from each other.") + .Register("Initial Setup, Step 4: Managing Collections", "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" + + $"There will always be one collection called {ModCollection.DefaultCollectionName} that can not be deleted.") .Register($"Initial Setup, Step 5: {SelectedCollection}", - $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." - + $"We should already have a collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") - .Register("Inheritance", - "This is a more advanced feature. Click the help button for more information, but we will ignore this for now.") - .Register($"Initial Setup, Step 6: {ActiveCollections}", - $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" - + "Any collection assigned here will apply to the game under certain conditions.\n\n" - + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" - + "Open this now to continue.") - .Register($"Initial Setup, Step 7: {DefaultCollection}", - $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollectionName} - is the main one.\n\n" - + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" - + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.") - .Register("Interface Collection", - $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" - + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") - .Register(GroupAssignment + 's', - "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" - + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" - + $"{IndividualAssignments} always take precedence before groups.") - .Register(IndividualAssignments, - "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" - + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n") - .Register("Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" + $"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n" + + $"We should already have the collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") + .Register("Initial Setup, Step 6: Simple Assignments", "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" + + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n" + + $"If you are just starting, you can see that the {ModCollection.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.") + .Register("Individual Assignments", "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") + .Register("Group Assignments", "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age.") + .Register("Collection Details", "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n" + + "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself.") + .Register("Incognito Mode", "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n" + + "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n" + + "It is strongly recommended to not show your characters name in public screenshots when using Penumbra.") + .Deprecated() + .Register("Initial Setup, Step 7: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking.") - .Register("Initial Setup, Step 9: Mod Import", + .Register("Initial Setup, Step 8: Mod Import", "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") // TODO @@ -129,10 +118,10 @@ public class TutorialService "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too.") - .Register("Initial Setup, Step 11: Enabling Mods", + .Register("Initial Setup, Step 9: Enabling Mods", "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance.") - .Register("Initial Setup, Step 12: Priority", + .Register("Initial Setup, Step 10: Priority", "If two enabled mods in one collection change the same files, there is a conflict.\n\n" + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible.") @@ -140,10 +129,10 @@ public class TutorialService + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately.") .Register("Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n" + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page.") - .Register("FAQ 1", "Penumbra can not easily change which items a mod applies to.") - .Register("FAQ 2", + .Deprecated() + .Register("FAQ 1", "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices.") - .Register("FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing.") + .Register("FAQ 2", "Penumbra can change the skin material a mod uses. This is under advanced editing.") .Register("Favorites", "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections.") .Register("Tags", From 10c0117402e6461ff09469d299edf39a73019f01 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 21 Apr 2023 18:47:34 +0200 Subject: [PATCH 0891/2451] Why does this not work, stupid Conditional. --- Penumbra/Interop/Services/GameEventManager.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index 1e0561b4..dc810f04 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -30,7 +30,7 @@ public unsafe class GameEventManager : IDisposable _resourceHandleDestructorHook.Enable(); _characterBaseCreateHook.Enable(); _characterBaseDestructorHook.Enable(); - _weaponReloadHook.Enable(); + _weaponReloadHook.Enable(); EnableDebugHook(); Penumbra.Log.Verbose($"{Prefix} Created."); } @@ -268,18 +268,22 @@ public unsafe class GameEventManager : IDisposable private delegate void TestDelegate(nint a1, int a2); private void TestDetour(nint a1, int a2) - { + { Penumbra.Log.Information($"Test: {a1:X} {a2}"); _testHook!.Original(a1, a2); } - #endif - [Conditional("DEBUG")] private void EnableDebugHook() => _testHook?.Enable(); - [Conditional("DEBUG")] private void DisposeDebugHook() => _testHook?.Dispose(); +#else + private void EnableDebugHook() + { } + + private void DisposeDebugHook() + { } +#endif #endregion } From 49c8afb72ab3a58cee02c113533461cda4d39064 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 21 Apr 2023 23:12:26 +0200 Subject: [PATCH 0892/2451] Remove remaining static ModManager. --- Penumbra/Api/IpcTester.cs | 8 +++++--- Penumbra/Mods/TemporaryMod.cs | 6 +++--- Penumbra/Penumbra.cs | 8 +++----- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 32366bed..8c859bc8 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -54,7 +54,7 @@ public class IpcTester : IDisposable _meta = new Meta(pi); _mods = new Mods(pi); _modSettings = new ModSettings(pi); - _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService); + _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, _configuration); UnsubscribeEvents(); } @@ -1157,9 +1157,10 @@ public class IpcTester : IDisposable private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly SaveService _saveService; + private readonly Configuration _config; public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, - TempCollectionManager tempCollections, SaveService saveService) + TempCollectionManager tempCollections, SaveService saveService, Configuration config) { _pi = pi; _modManager = modManager; @@ -1167,6 +1168,7 @@ public class IpcTester : IDisposable _tempMods = tempMods; _tempCollections = tempCollections; _saveService = saveService; + _config = config; } public string LastCreatedCollectionName = string.Empty; @@ -1275,7 +1277,7 @@ public class IpcTester : IDisposable .FirstOrDefault() ?? "Unknown"; if (ImGui.Button($"Save##{collection.Name}")) - TemporaryMod.SaveTempCollection(_saveService, _modManager, collection, character); + TemporaryMod.SaveTempCollection(_config, _saveService, _modManager, collection, character); ImGuiUtil.DrawTableColumn(collection.Name); ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 9019b468..1f408b15 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -46,14 +46,14 @@ public class TemporaryMod : IMod Default.ManipulationData = manips; } - public static void SaveTempCollection( SaveService saveService, ModManager modManager, ModCollection collection, string? character = null ) + public static void SaveTempCollection( Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, string? character = null ) { DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + dir = ModCreator.CreateModFolder( modManager.BasePath, collection.Name ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); - modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, + modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); var defaultMod = mod.Default; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 259048f0..c46f23b4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -15,7 +15,6 @@ using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.Collections.Cache; -using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; @@ -27,7 +26,6 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods; -using Penumbra.Meta; namespace Penumbra; @@ -41,7 +39,6 @@ public class Penumbra : IDalamudPlugin public static Configuration Config { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static ModManager ModManager { get; private set; } = null!; public static ModCacheManager ModCaches { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; @@ -55,6 +52,7 @@ public class Penumbra : IDalamudPlugin private readonly ResidentResourceManager _residentResources; private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; + private readonly ModManager _modManager; private PenumbraWindowSystem? _windowSystem; private bool _disposed; @@ -75,7 +73,7 @@ public class Penumbra : IDalamudPlugin _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModManager = _tmp.Services.GetRequiredService(); + _modManager = _tmp.Services.GetRequiredService(); CollectionManager = _tmp.Services.GetRequiredService(); _tempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); @@ -211,7 +209,7 @@ public class Penumbra : IDalamudPlugin $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); sb.AppendLine("**Mods**"); - sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n"); + sb.Append($"> **`Installed Mods: `** {_modManager.Count}\n"); sb.Append($"> **`Mods with Config: `** {ModCaches.Count(m => m.HasOptions)}\n"); sb.Append( $"> **`Mods with File Redirections: `** {ModCaches.Count(m => m.TotalFileCount > 0)}, Total: {ModCaches.Sum(m => m.TotalFileCount)}\n"); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 32844294..8a78b6fb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -211,7 +211,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 21 Apr 2023 23:17:05 +0200 Subject: [PATCH 0893/2451] Remove Mod.BasePath --- Penumbra/Api/IpcTester.cs | 61 ++++++++++++++-------------- Penumbra/Api/PenumbraIpcProviders.cs | 4 +- Penumbra/Mods/Manager/ModManager.cs | 10 +++++ Penumbra/Mods/Mod.BasePath.cs | 36 ---------------- Penumbra/Mods/Mod.cs | 20 +++++++++ Penumbra/Mods/ModCache.cs | 3 ++ Penumbra/Penumbra.cs | 24 +++++------ 7 files changed, 78 insertions(+), 80 deletions(-) delete mode 100644 Penumbra/Mods/Mod.BasePath.cs diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 8c859bc8..1044581f 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -28,33 +28,34 @@ public class IpcTester : IDisposable private readonly PenumbraIpcProviders _ipcProviders; private bool _subscribed = true; - private readonly PluginState _pluginState; - private readonly Configuration _configuration; - private readonly Ui _ui; - private readonly Redrawing _redrawing; - private readonly GameState _gameState; - private readonly Resolve _resolve; - private readonly Collections _collections; - private readonly Meta _meta; - private readonly Mods _mods; - private readonly ModSettings _modSettings; - private readonly Temporary _temporary; + private readonly PluginState _pluginState; + private readonly IpcConfiguration _ipcConfiguration; + private readonly Ui _ui; + private readonly Redrawing _redrawing; + private readonly GameState _gameState; + private readonly Resolve _resolve; + private readonly Collections _collections; + private readonly Meta _meta; + private readonly Mods _mods; + private readonly ModSettings _modSettings; + private readonly Temporary _temporary; - public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, + public IpcTester(Configuration config, DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager, + CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) { - _ipcProviders = ipcProviders; - _pluginState = new PluginState(pi); - _configuration = new Configuration(pi); - _ui = new Ui(pi); - _redrawing = new Redrawing(pi); - _gameState = new GameState(pi); - _resolve = new Resolve(pi); - _collections = new Collections(pi); - _meta = new Meta(pi); - _mods = new Mods(pi); - _modSettings = new ModSettings(pi); - _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, _configuration); + _ipcProviders = ipcProviders; + _pluginState = new PluginState(pi); + _ipcConfiguration = new IpcConfiguration(pi); + _ui = new Ui(pi); + _redrawing = new Redrawing(pi); + _gameState = new GameState(pi); + _resolve = new Resolve(pi); + _collections = new Collections(pi); + _meta = new Meta(pi); + _mods = new Mods(pi); + _modSettings = new ModSettings(pi); + _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config); UnsubscribeEvents(); } @@ -65,7 +66,7 @@ public class IpcTester : IDisposable SubscribeEvents(); ImGui.TextUnformatted($"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}"); _pluginState.Draw(); - _configuration.Draw(); + _ipcConfiguration.Draw(); _ui.Draw(); _redrawing.Draw(); _gameState.Draw(); @@ -97,7 +98,7 @@ public class IpcTester : IDisposable _modSettings.SettingChanged.Enable(); _gameState.CharacterBaseCreating.Enable(); _gameState.CharacterBaseCreated.Enable(); - _configuration.ModDirectoryChanged.Enable(); + _ipcConfiguration.ModDirectoryChanged.Enable(); _gameState.GameObjectResourcePathResolved.Enable(); _mods.DeleteSubscriber.Enable(); _mods.AddSubscriber.Enable(); @@ -121,7 +122,7 @@ public class IpcTester : IDisposable _modSettings.SettingChanged.Disable(); _gameState.CharacterBaseCreating.Disable(); _gameState.CharacterBaseCreated.Disable(); - _configuration.ModDirectoryChanged.Disable(); + _ipcConfiguration.ModDirectoryChanged.Disable(); _gameState.GameObjectResourcePathResolved.Disable(); _mods.DeleteSubscriber.Disable(); _mods.AddSubscriber.Disable(); @@ -143,7 +144,7 @@ public class IpcTester : IDisposable _modSettings.SettingChanged.Dispose(); _gameState.CharacterBaseCreating.Dispose(); _gameState.CharacterBaseCreated.Dispose(); - _configuration.ModDirectoryChanged.Dispose(); + _ipcConfiguration.ModDirectoryChanged.Dispose(); _gameState.GameObjectResourcePathResolved.Dispose(); _mods.DeleteSubscriber.Dispose(); _mods.AddSubscriber.Dispose(); @@ -229,7 +230,7 @@ public class IpcTester : IDisposable => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); } - private class Configuration + private class IpcConfiguration { private readonly DalamudPluginInterface _pi; public readonly EventSubscriber ModDirectoryChanged; @@ -239,7 +240,7 @@ public class IpcTester : IDisposable private bool _lastModDirectoryValid; private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; - public Configuration(DalamudPluginInterface pi) + public IpcConfiguration(DalamudPluginInterface pi) { _pi = pi; ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 36245110..e8139453 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -116,7 +116,7 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryMod; public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, - TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) + TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { Api = api; @@ -228,7 +228,7 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - Tester = new IpcTester(pi, this, modManager, collections, tempMods, tempCollections, saveService); + Tester = new IpcTester(config, pi, this, modManager, collections, tempMods, tempCollections, saveService); Initialized.Invoke(); } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index ae22c5e9..746cb645 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -18,6 +18,16 @@ public enum NewDirectoryState Identical, Empty, } + +/// Describes the state of a changed mod event. +public enum ModPathChangeType +{ + Added, + Deleted, + Moved, + Reloaded, + StartingReload, +} public sealed class ModManager : ModStorage { diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs deleted file mode 100644 index 28258ded..00000000 --- a/Penumbra/Mods/Mod.BasePath.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.Mods.Manager; - -namespace Penumbra.Mods; - -public enum ModPathChangeType -{ - Added, - Deleted, - Moved, - Reloaded, - StartingReload, -} - -public partial class Mod -{ - public DirectoryInfo ModPath { get; internal set; } - public string Identifier - => Index >= 0 ? ModPath.Name : Name; - public int Index { get; internal set; } = -1; - - public bool IsTemporary - => Index < 0; - - // Unused if Index < 0 but used for special temporary mods. - public int Priority - => 0; - - internal Mod( DirectoryInfo modPath ) - { - ModPath = modPath; - Default = new SubMod( this ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 24a78370..fb011b2b 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using OtterGui; using OtterGui.Classes; @@ -18,6 +19,25 @@ public sealed partial class Mod : IMod Priority = int.MaxValue, }; + // Main Data + public DirectoryInfo ModPath { get; internal set; } + public string Identifier + => Index >= 0 ? ModPath.Name : Name; + public int Index { get; internal set; } = -1; + + public bool IsTemporary + => Index < 0; + + /// Unused if Index < 0 but used for special temporary mods. + public int Priority + => 0; + + public Mod(DirectoryInfo modPath) + { + ModPath = modPath; + Default = new SubMod(this); + } + // Meta Data public LowerString Name { get; internal set; } = "New Mod"; public LowerString Author { get; internal set; } = LowerString.Empty; diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs index 76e01bf6..5872587d 100644 --- a/Penumbra/Mods/ModCache.cs +++ b/Penumbra/Mods/ModCache.cs @@ -13,6 +13,9 @@ public class ModCache public string LowerChangedItemsString = string.Empty; public string AllTagsLower = string.Empty; + public ModCache() + {} + public void Reset() { TotalFileCount = 0; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c46f23b4..ca0a06ae 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,14 +34,14 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static Logger Log { get; private set; } = null!; - public static ChatService ChatService { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; + public static Configuration Config { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static ModCacheManager ModCaches { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; - public static ActorManager Actors { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static ModCacheManager ModCaches { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; + public static ActorManager Actors { get; private set; } = null!; public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; @@ -67,15 +67,15 @@ public class Penumbra : IDalamudPlugin ChatService = _tmp.Services.GetRequiredService(); _validityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - _modManager = _tmp.Services.GetRequiredService(); + _modManager = _tmp.Services.GetRequiredService(); CollectionManager = _tmp.Services.GetRequiredService(); - _tempCollections = _tmp.Services.GetRequiredService(); + _tempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); From be3c1c85aa8e7950cd27caca860457b1ba4a3252 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 21 Apr 2023 23:52:31 +0200 Subject: [PATCH 0894/2451] Remove static Config. --- Penumbra/Collections/Cache/CollectionCache.cs | 2 +- Penumbra/Configuration.cs | 2 +- Penumbra/Import/TexToolsImporter.Gui.cs | 4 +- Penumbra/Import/Textures/Texture.cs | 215 ++++++++---------- Penumbra/Meta/MetaFileManager.cs | 31 +++ Penumbra/Mods/Editor/IMod.cs | 5 +- Penumbra/Mods/Editor/ModNormalizer.cs | 10 +- Penumbra/Mods/Manager/ModCacheManager.cs | 137 ++++------- Penumbra/Mods/Mod.cs | 45 ++-- Penumbra/Mods/ModCache.cs | 29 --- Penumbra/Penumbra.cs | 41 ++-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 8 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 26 +-- Penumbra/UI/Classes/Colors.cs | 12 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 34 ++- .../UI/CollectionTab/CollectionSelector.cs | 2 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 10 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 55 +++-- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 13 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 24 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 +- .../ResourceWatcher/ResourceWatcher.Table.cs | 56 +++-- Penumbra/UI/Tabs/ChangedItemsTab.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 6 +- Penumbra/UI/Tabs/ModsTab.cs | 4 +- Penumbra/UI/Tabs/SettingsTab.cs | 16 +- Penumbra/UI/UiHelpers.cs | 2 +- 30 files changed, 363 insertions(+), 440 deletions(-) delete mode 100644 Penumbra/Mods/ModCache.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e62fd1b9..1d2ff39c 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -218,7 +218,7 @@ public class CollectionCache : IDisposable if (addMetaChanges) { ++_collection.ChangeCounter; - if ((mod is TemporaryMod temp ? temp.TotalManipulations : Penumbra.ModCaches[mod.Index].TotalManipulations) > 0) + if (mod.TotalManipulations > 0) AddMetaFiles(); _manager.MetaFileManager.ApplyDefaultFiles(_collection); diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 764d3269..568b2fc5 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,7 +11,6 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; @@ -106,6 +105,7 @@ public class Configuration : IPluginConfiguration, ISavable { _saveService = saveService; Load(fileNames, migrator); + UI.Classes.Colors.SetColors(this); } public void Load(FilenameService fileNames, ConfigMigrationService migrator) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 8c5ad81b..79cd728b 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -94,12 +94,12 @@ public partial class TexToolsImporter ImGui.TableNextColumn(); if( ex == null ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config) ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); ImGui.TextUnformatted( dir?.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ?? "Unknown Directory" ); } else { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value(Penumbra.Config) ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); ImGui.TextUnformatted( ex.Message ); ImGuiUtil.HoverTooltip( ex.ToString() ); } diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index d37c8967..3a576028 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -37,7 +37,7 @@ public sealed class Texture : IDisposable // The pixels of the main image in RGBA order. // Empty if LoadError != null or Path is empty. - public byte[] RGBAPixels = Array.Empty< byte >(); + public byte[] RGBAPixels = Array.Empty(); // The ImGui wrapper to load the image. // null if LoadError != null or Path is empty. @@ -53,101 +53,97 @@ public sealed class Texture : IDisposable public bool IsLoaded => TextureWrap != null; - public void Draw( Vector2 size ) + public void Draw(Vector2 size) { - if( TextureWrap != null ) + if (TextureWrap != null) { size = size.X < TextureWrap.Width ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } - : new Vector2( TextureWrap.Width, TextureWrap.Height ); + : new Vector2(TextureWrap.Width, TextureWrap.Height); - ImGui.Image( TextureWrap.ImGuiHandle, size ); + ImGui.Image(TextureWrap.ImGuiHandle, size); DrawData(); } - else if( LoadError != null ) + else if (LoadError != null) { - ImGui.TextUnformatted( "Could not load file:" ); - ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); + ImGui.TextUnformatted("Could not load file:"); + ImGuiUtil.TextColored(Colors.RegexWarningBorder, LoadError.ToString()); } } public void DrawData() { - using var table = ImRaii.Table( "##data", 2, ImGuiTableFlags.SizingFixedFit ); - ImGuiUtil.DrawTableColumn( "Width" ); - ImGuiUtil.DrawTableColumn( TextureWrap!.Width.ToString() ); - ImGuiUtil.DrawTableColumn( "Height" ); - ImGuiUtil.DrawTableColumn( TextureWrap!.Height.ToString() ); - ImGuiUtil.DrawTableColumn( "File Type" ); - ImGuiUtil.DrawTableColumn( Type.ToString() ); - ImGuiUtil.DrawTableColumn( "Bitmap Size" ); - ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( RGBAPixels.Length )} ({RGBAPixels.Length} Bytes)" ); - switch( BaseImage ) + using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); + ImGuiUtil.DrawTableColumn("Width"); + ImGuiUtil.DrawTableColumn(TextureWrap!.Width.ToString()); + ImGuiUtil.DrawTableColumn("Height"); + ImGuiUtil.DrawTableColumn(TextureWrap!.Height.ToString()); + ImGuiUtil.DrawTableColumn("File Type"); + ImGuiUtil.DrawTableColumn(Type.ToString()); + ImGuiUtil.DrawTableColumn("Bitmap Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(RGBAPixels.Length)} ({RGBAPixels.Length} Bytes)"); + switch (BaseImage) { case ScratchImage s: - ImGuiUtil.DrawTableColumn( "Format" ); - ImGuiUtil.DrawTableColumn( s.Meta.Format.ToString() ); - ImGuiUtil.DrawTableColumn( "Mip Levels" ); - ImGuiUtil.DrawTableColumn( s.Meta.MipLevels.ToString() ); - ImGuiUtil.DrawTableColumn( "Data Size" ); - ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( s.Pixels.Length )} ({s.Pixels.Length} Bytes)" ); - ImGuiUtil.DrawTableColumn( "Number of Images" ); - ImGuiUtil.DrawTableColumn( s.Images.Length.ToString() ); + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); + ImGuiUtil.DrawTableColumn("Number of Images"); + ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); break; case TexFile t: - ImGuiUtil.DrawTableColumn( "Format" ); - ImGuiUtil.DrawTableColumn( t.Header.Format.ToString() ); - ImGuiUtil.DrawTableColumn( "Mip Levels" ); - ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString() ); - ImGuiUtil.DrawTableColumn( "Data Size" ); - ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( t.ImageData.Length )} ({t.ImageData.Length} Bytes)" ); + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); break; } } private void Clean() { - RGBAPixels = Array.Empty< byte >(); + RGBAPixels = Array.Empty(); TextureWrap?.Dispose(); TextureWrap = null; - ( BaseImage as IDisposable )?.Dispose(); + (BaseImage as IDisposable)?.Dispose(); BaseImage = null; Type = FileType.Unknown; - Loaded?.Invoke( false ); + Loaded?.Invoke(false); } public void Dispose() => Clean(); - public event Action< bool >? Loaded; + public event Action? Loaded; - private void Load( string path ) + private void Load(string path) { _tmpPath = null; - if( path == Path ) - { + if (path == Path) return; - } Path = path; Clean(); - if( path.Length == 0 ) - { + if (path.Length == 0) return; - } try { - var _ = System.IO.Path.GetExtension( Path ).ToLowerInvariant() switch + var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch { ".dds" => LoadDds(), ".png" => LoadPng(), ".tex" => LoadTex(), - _ => throw new Exception( $"Extension {System.IO.Path.GetExtension( Path )} unknown." ), + _ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."), }; - Loaded?.Invoke( true ); + Loaded?.Invoke(true); } - catch( Exception e ) + catch (Exception e) { LoadError = e; Clean(); @@ -157,11 +153,11 @@ public sealed class Texture : IDisposable private bool LoadDds() { Type = FileType.Dds; - var scratch = ScratchImage.LoadDDS( Path ); + var scratch = ScratchImage.LoadDDS(Path); BaseImage = scratch; - var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); - RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); - CreateTextureWrap( f.Meta.Width, f.Meta.Height ); + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); + CreateTextureWrap(f.Meta.Width, f.Meta.Height); return true; } @@ -169,11 +165,11 @@ public sealed class Texture : IDisposable { Type = FileType.Png; BaseImage = null; - using var stream = File.OpenRead( Path ); - using var png = Image.Load< Rgba32 >( stream ); + using var stream = File.OpenRead(Path); + using var png = Image.Load(stream); RGBAPixels = new byte[png.Height * png.Width * 4]; - png.CopyPixelDataTo( RGBAPixels ); - CreateTextureWrap( png.Width, png.Height ); + png.CopyPixelDataTo(RGBAPixels); + CreateTextureWrap(png.Width, png.Height); return true; } @@ -181,113 +177,100 @@ public sealed class Texture : IDisposable { Type = FileType.Tex; using var stream = OpenTexStream(); - var scratch = TexFileParser.Parse( stream ); + var scratch = TexFileParser.Parse(stream); BaseImage = scratch; - var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); - RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); - CreateTextureWrap( scratch.Meta.Width, scratch.Meta.Height ); + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); + CreateTextureWrap(scratch.Meta.Width, scratch.Meta.Height); return true; } private Stream OpenTexStream() { - if( System.IO.Path.IsPathRooted( Path ) ) - { - return File.OpenRead( Path ); - } + if (System.IO.Path.IsPathRooted(Path)) + return File.OpenRead(Path); - var file = DalamudServices.SGameData.GetFile( Path ); - return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); + var file = DalamudServices.SGameData.GetFile(Path); + return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{Path}\" from game files."); } - private void CreateTextureWrap( int width, int height ) - => TextureWrap = DalamudServices.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); + private void CreateTextureWrap(int width, int height) + => TextureWrap = DalamudServices.PluginInterface.UiBuilder.LoadImageRaw(RGBAPixels, width, height, 4); private string? _tmpPath; - public void PathSelectBox( string label, string tooltip, IEnumerable< (string, bool) > paths, int skipPrefix ) + public void PathSelectBox(string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix) { - ImGui.SetNextItemWidth( -0.0001f ); + ImGui.SetNextItemWidth(-0.0001f); var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here..."; - using var combo = ImRaii.Combo( label, startPath ); - if( combo ) - { - foreach( var ((path, game), idx) in paths.WithIndex() ) + using var combo = ImRaii.Combo(label, startPath); + if (combo) + foreach (var ((path, game), idx) in paths.WithIndex()) { - if( game ) + if (game) { - if( !DalamudServices.SGameData.FileExists( path ) ) - { + if (!DalamudServices.SGameData.FileExists(path)) continue; - } } - else if( !File.Exists( path ) ) + else if (!File.Exists(path)) { continue; } - using var id = ImRaii.PushId( idx ); - using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config), game ) ) + using var id = ImRaii.PushId(idx); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) { - var p = game ? $"--> {path}" : path[ skipPrefix.. ]; - if( ImGui.Selectable( p, path == startPath ) && path != startPath ) - { - Load( path ); - } + var p = game ? $"--> {path}" : path[skipPrefix..]; + if (ImGui.Selectable(p, path == startPath) && path != startPath) + Load(path); } - ImGuiUtil.HoverTooltip( game + ImGuiUtil.HoverTooltip(game ? "This is a game path and refers to an unmanipulated file from your game data." - : "This is a path to a modded file on your file system." ); + : "This is a path to a modded file on your file system."); } - } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } - public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogService fileDialog ) + public void PathInputBox(string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, + string defaultModImportPath) { _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y ) ); - ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale ); - ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Load( _tmpPath ); - } + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); + ImGui.InputTextWithHint(label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength); + if (ImGui.IsItemDeactivatedAfterEdit()) + Load(_tmpPath); - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, - true ) ) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, + true)) { - if( Penumbra.Config.DefaultModImportPath.Length > 0 ) - { - startPath = Penumbra.Config.DefaultModImportPath; - } + if (defaultModImportPath.Length > 0) + startPath = defaultModImportPath; var texture = this; - void UpdatePath( bool success, List< string > paths ) + void UpdatePath(bool success, List paths) { - if( success && paths.Count > 0 ) - { - texture.Load( paths[ 0 ] ); - } + if (success && paths.Count > 0) + texture.Load(paths[0]); } - fileDialog.OpenFilePicker( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false ); + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Reload the currently selected path.", false, - true ) ) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Reload the currently selected path.", false, + true)) { var path = Path; Path = string.Empty; - Load( path ); + Load(path); } } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 53b35559..033fc4ea 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,14 +1,18 @@ using System; +using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; +using Penumbra.Import; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; +using Penumbra.Mods; using Penumbra.Services; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; @@ -37,6 +41,33 @@ public unsafe class MetaFileManager SignatureHelper.Initialise(this); } + public void WriteAllTexToolsMeta(Mod mod) + { + try + { + TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); + foreach (var group in mod.Groups) + { + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name); + if (!dir.Exists) + dir.Create(); + + foreach (var option in group.OfType()) + { + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); + if (!optionDir.Exists) + optionDir.Create(); + + TexToolsMeta.WriteTexToolsMeta(this, option.Manipulations, optionDir); + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) { diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 5f32122c..7741cf6a 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -13,5 +13,8 @@ public interface IMod public ISubMod Default { get; } public IReadOnlyList< IModGroup > Groups { get; } - public IEnumerable< SubMod > AllSubMods { get; } + public IEnumerable< SubMod > AllSubMods { get; } + + // Cache + public int TotalManipulations { get; } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index d920adc4..aaf304db 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -13,7 +13,6 @@ namespace Penumbra.Mods; public class ModNormalizer { private readonly ModManager _modManager; - private readonly ModCacheManager _modCacheManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -26,11 +25,8 @@ public class ModNormalizer public bool Running => Step < TotalSteps; - public ModNormalizer(ModManager modManager, ModCacheManager modCacheManager) - { - _modManager = modManager; - _modCacheManager = modCacheManager; - } + public ModNormalizer(ModManager modManager) + => _modManager = modManager; public void Normalize(Mod mod) { @@ -41,7 +37,7 @@ public class ModNormalizer _normalizationDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalization"); _oldDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalizationOld"); Step = 0; - TotalSteps = _modCacheManager[mod].TotalFileCount + 5; + TotalSteps = mod.TotalFileCount + 5; Task.Run(NormalizeSync); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index c25a6872..86a26960 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,14 +11,12 @@ using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModCacheManager : IDisposable, IReadOnlyList +public class ModCacheManager : IDisposable { private readonly CommunicatorService _communicator; private readonly IdentifierService _identifier; private readonly ModStorage _modManager; - private readonly List _cache = new(); - public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModStorage modStorage) { _communicator = communicator; @@ -35,20 +32,6 @@ public class ModCacheManager : IDisposable, IReadOnlyList OnModDiscoveryFinished(); } - public IEnumerator GetEnumerator() - => _cache.Take(Count).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count { get; private set; } - - public ModCache this[int index] - => _cache[index]; - - public ModCache this[Mod mod] - => _cache[mod.Index]; - public void Dispose() { _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); @@ -127,35 +110,30 @@ public class ModCacheManager : IDisposable, IReadOnlyList private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) { - ModCache cache; switch (type) { case ModOptionChangeType.GroupAdded: case ModOptionChangeType.GroupDeleted: case ModOptionChangeType.OptionAdded: case ModOptionChangeType.OptionDeleted: - cache = EnsureCount(mod); - UpdateChangedItems(cache, mod); - UpdateCounts(cache, mod); + UpdateChangedItems(mod); + UpdateCounts(mod); break; case ModOptionChangeType.GroupTypeChanged: - UpdateHasOptions(EnsureCount(mod), mod); + UpdateHasOptions(mod); break; case ModOptionChangeType.OptionFilesChanged: case ModOptionChangeType.OptionFilesAdded: - cache = EnsureCount(mod); - UpdateChangedItems(cache, mod); - UpdateFileCount(cache, mod); + UpdateChangedItems(mod); + UpdateFileCount(mod); break; case ModOptionChangeType.OptionSwapsChanged: - cache = EnsureCount(mod); - UpdateChangedItems(cache, mod); - UpdateSwapCount(cache, mod); + UpdateChangedItems(mod); + UpdateSwapCount(mod); break; case ModOptionChangeType.OptionMetaChanged: - cache = EnsureCount(mod); - UpdateChangedItems(cache, mod); - UpdateMetaCount(cache, mod); + UpdateChangedItems(mod); + UpdateMetaCount(mod); break; } } @@ -166,106 +144,79 @@ public class ModCacheManager : IDisposable, IReadOnlyList { case ModPathChangeType.Added: case ModPathChangeType.Reloaded: - Refresh(EnsureCount(mod), mod); - break; - case ModPathChangeType.Deleted: - --Count; - var oldCache = _cache[mod.Index]; - oldCache.Reset(); - for (var i = mod.Index; i < Count; ++i) - _cache[i] = _cache[i + 1]; - _cache[Count] = oldCache; + Refresh(mod); break; } } - private void OnModDataChange(ModDataChangeType type, Mod mod, string? _) + private static void OnModDataChange(ModDataChangeType type, Mod mod, string? _) { if ((type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) != 0) - UpdateTags(EnsureCount(mod), mod); + UpdateTags(mod); } private void OnModDiscoveryFinished() - { - if (_modManager.Count > _cache.Count) - _cache.AddRange(Enumerable.Range(0, _modManager.Count - _cache.Count).Select(_ => new ModCache())); - - Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { Refresh(_cache[idx], _modManager[idx]); }); - Count = _modManager.Count; - } + => Parallel.ForEach(_modManager, Refresh); private void OnIdentifierCreation() { - Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { UpdateChangedItems(_cache[idx], _modManager[idx]); }); + Parallel.ForEach(_modManager, UpdateChangedItems); _identifier.FinishedCreation -= OnIdentifierCreation; } - private static void UpdateFileCount(ModCache cache, Mod mod) - => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); + private static void UpdateFileCount(Mod mod) + => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); - private static void UpdateSwapCount(ModCache cache, Mod mod) - => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + private static void UpdateSwapCount(Mod mod) + => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); - private static void UpdateMetaCount(ModCache cache, Mod mod) - => cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Manipulations.Count); + private static void UpdateMetaCount(Mod mod) + => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Manipulations.Count); - private static void UpdateHasOptions(ModCache cache, Mod mod) - => cache.HasOptions = mod.Groups.Any(o => o.IsOption); + private static void UpdateHasOptions(Mod mod) + => mod.HasOptions = mod.Groups.Any(o => o.IsOption); - private static void UpdateTags(ModCache cache, Mod mod) - => cache.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant())); + private static void UpdateTags(Mod mod) + => mod.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant())); - private void UpdateChangedItems(ModCache cache, Mod mod) + private void UpdateChangedItems(Mod mod) { - cache.ChangedItems.Clear(); + var changedItems = (SortedList)mod.ChangedItems; + changedItems.Clear(); if (!_identifier.Valid) return; foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) - _identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString()); + _identifier.AwaitedService.Identify(changedItems, gamePath.ToString()); foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) - ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip); + ComputeChangedItems(_identifier.AwaitedService, changedItems, manip); - cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } - private static void UpdateCounts(ModCache cache, Mod mod) + private static void UpdateCounts(Mod mod) { - cache.TotalFileCount = mod.Default.Files.Count; - cache.TotalSwapCount = mod.Default.FileSwaps.Count; - cache.TotalManipulations = mod.Default.Manipulations.Count; - cache.HasOptions = false; + mod.TotalFileCount = mod.Default.Files.Count; + mod.TotalSwapCount = mod.Default.FileSwaps.Count; + mod.TotalManipulations = mod.Default.Manipulations.Count; + mod.HasOptions = false; foreach (var group in mod.Groups) { - cache.HasOptions |= group.IsOption; + mod.HasOptions |= group.IsOption; foreach (var s in group) { - cache.TotalFileCount += s.Files.Count; - cache.TotalSwapCount += s.FileSwaps.Count; - cache.TotalManipulations += s.Manipulations.Count; + mod.TotalFileCount += s.Files.Count; + mod.TotalSwapCount += s.FileSwaps.Count; + mod.TotalManipulations += s.Manipulations.Count; } } } - private void Refresh(ModCache cache, Mod mod) + private void Refresh(Mod mod) { - UpdateTags(cache, mod); - UpdateCounts(cache, mod); - UpdateChangedItems(cache, mod); - } - - private ModCache EnsureCount(Mod mod) - { - if (mod.Index < Count) - return _cache[mod.Index]; - - - if (mod.Index >= _cache.Count) - _cache.AddRange(Enumerable.Range(0, mod.Index + 1 - _cache.Count).Select(_ => new ModCache())); - for (var i = Count; i < mod.Index; ++i) - Refresh(_cache[i], _modManager[i]); - Count = mod.Index + 1; - return _cache[mod.Index]; + UpdateTags(mod); + UpdateCounts(mod); + UpdateChangedItems(mod); } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index fb011b2b..242ec260 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using OtterGui; using OtterGui.Classes; +using Penumbra.Collections.Cache; using Penumbra.Import; using Penumbra.Meta; using Penumbra.String.Classes; @@ -21,8 +22,10 @@ public sealed partial class Mod : IMod // Main Data public DirectoryInfo ModPath { get; internal set; } + public string Identifier => Index >= 0 ? ModPath.Name : Name; + public int Index { get; internal set; } = -1; public bool IsTemporary @@ -32,12 +35,15 @@ public sealed partial class Mod : IMod public int Priority => 0; - public Mod(DirectoryInfo modPath) + internal Mod(DirectoryInfo modPath) { ModPath = modPath; Default = new SubMod(this); } + public override string ToString() + => Name.Text; + // Meta Data public LowerString Name { get; internal set; } = "New Mod"; public LowerString Author { get; internal set; } = LowerString.Empty; @@ -80,35 +86,14 @@ public sealed partial class Mod : IMod .ToList(); } - // Access - public override string ToString() - => Name.Text; + // Cache + public readonly IReadOnlyDictionary ChangedItems = new SortedList(); - public void WriteAllTexToolsMeta(MetaFileManager manager) - { - try - { - TexToolsMeta.WriteTexToolsMeta(manager, Default.Manipulations, ModPath); - foreach (var group in Groups) - { - var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); - if (!dir.Exists) - dir.Create(); - - foreach (var option in group.OfType()) - { - var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); - if (!optionDir.Exists) - optionDir.Create(); - - TexToolsMeta.WriteTexToolsMeta(manager, option.Manipulations, optionDir); - } - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); - } - } + public string LowerChangedItemsString { get; internal set; } = string.Empty; + public string AllTagsLower { get; internal set; } = string.Empty; + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public bool HasOptions { get; internal set; } } diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs deleted file mode 100644 index 5872587d..00000000 --- a/Penumbra/Mods/ModCache.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; - -namespace Penumbra.Mods; - -public class ModCache -{ - public int TotalFileCount; - public int TotalSwapCount; - public int TotalManipulations; - public bool HasOptions; - - public readonly SortedList ChangedItems = new(); - public string LowerChangedItemsString = string.Empty; - public string AllTagsLower = string.Empty; - - public ModCache() - {} - - public void Reset() - { - TotalFileCount = 0; - TotalSwapCount = 0; - TotalManipulations = 0; - HasOptions = false; - ChangedItems.Clear(); - LowerChangedItemsString = string.Empty; - AllTagsLower = string.Empty; - } -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ca0a06ae..5dd865a2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -36,10 +36,8 @@ public class Penumbra : IDalamudPlugin public static Logger Log { get; private set; } = null!; public static ChatService ChatService { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static ModCacheManager ModCaches { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!; @@ -53,6 +51,7 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; + private readonly Configuration _config; private PenumbraWindowSystem? _windowSystem; private bool _disposed; @@ -67,7 +66,7 @@ public class Penumbra : IDalamudPlugin ChatService = _tmp.Services.GetRequiredService(); _validityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); + _config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); Actors = _tmp.Services.GetRequiredService().AwaitedService; _tempMods = _tmp.Services.GetRequiredService(); @@ -78,8 +77,8 @@ public class Penumbra : IDalamudPlugin _tempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); - ModCaches = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. + _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { _tmp.Services.GetRequiredService(); @@ -146,10 +145,10 @@ public class Penumbra : IDalamudPlugin public bool SetEnabled(bool enabled) { - if (enabled == Config.EnableMods) + if (enabled == _config.EnableMods) return false; - Config.EnableMods = enabled; + _config.EnableMods = enabled; if (enabled) { if (CharacterUtility.Ready) @@ -169,7 +168,7 @@ public class Penumbra : IDalamudPlugin } } - Config.Save(); + _config.Save(); EnabledChange?.Invoke(enabled); return true; @@ -190,33 +189,33 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { var sb = new StringBuilder(10240); - var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); - var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null; + var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); + var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); - sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n"); - sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); + sb.Append($"> **`Enable Mods: `** {_config.EnableMods}\n"); + sb.Append($"> **`Enable HTTP API: `** {_config.EnableHttpApi}\n"); sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); - sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); + sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); - sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n"); - sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n"); + sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); + sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); sb.Append( - $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n"); - sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); + $"> **`Logging: `** Log: {_config.EnableResourceLogging}, Watcher: {_config.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); + sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); sb.AppendLine("**Mods**"); sb.Append($"> **`Installed Mods: `** {_modManager.Count}\n"); - sb.Append($"> **`Mods with Config: `** {ModCaches.Count(m => m.HasOptions)}\n"); + sb.Append($"> **`Mods with Config: `** {_modManager.Count(m => m.HasOptions)}\n"); sb.Append( - $"> **`Mods with File Redirections: `** {ModCaches.Count(m => m.TotalFileCount > 0)}, Total: {ModCaches.Sum(m => m.TotalFileCount)}\n"); + $"> **`Mods with File Redirections: `** {_modManager.Count(m => m.TotalFileCount > 0)}, Total: {_modManager.Sum(m => m.TotalFileCount)}\n"); sb.Append( - $"> **`Mods with FileSwaps: `** {ModCaches.Count(m => m.TotalSwapCount > 0)}, Total: {ModCaches.Sum(m => m.TotalSwapCount)}\n"); + $"> **`Mods with FileSwaps: `** {_modManager.Count(m => m.TotalSwapCount > 0)}, Total: {_modManager.Sum(m => m.TotalSwapCount)}\n"); sb.Append( - $"> **`Mods with Meta Manipulations:`** {ModCaches.Count(m => m.TotalManipulations > 0)}, Total {ModCaches.Sum(m => m.TotalManipulations)}\n"); + $"> **`Mods with Meta Manipulations:`** {_modManager.Count(m => m.TotalManipulations > 0)}, Total {_modManager.Sum(m => m.TotalManipulations)}\n"); sb.Append($"> **`IMC Exceptions Thrown: `** {_validityChecker.ImcExceptions.Count}\n"); sb.Append( $"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n"); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 401d96a6..e42f9724 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -286,7 +286,7 @@ public class FileEditor where T : class, IWritable ImGui.TableNextColumn(); UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); ImGui.TextUnformatted(option.FullName); } } @@ -294,7 +294,7 @@ public class FileEditor where T : class, IWritable if (file.SubModUsage.Count > 0) { ImGui.SameLine(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config)); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 8cc1060b..ee93ad29 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -82,7 +82,7 @@ public partial class ModEditWindow return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, - _editor.Option! == s.Item1 && _modCaches[_mod!].HasOptions ? 0x40008000u : 0u)); + _editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); }); void DrawLine((string, string, string, uint) data) @@ -179,7 +179,7 @@ public partial class ModEditWindow var selected = _selectedFiles.Contains(registry); var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; - using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value(Penumbra.Config)); + using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value()); if (UiHelpers.Selectable(registry.RelPath.Path, selected)) { if (selected) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 1b92463e..709b6cf5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -63,7 +63,7 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) - _mod!.WriteAllTexToolsMeta(_metaFileManager); + _metaFileManager.WriteAllTexToolsMeta(_mod!); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) @@ -776,7 +776,7 @@ public partial class ModEditWindow var value = meta.Entry; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config), + def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def != value); if (ImGui.DragFloat("##rspValue", ref value, 0.001f, 0.01f, 8f) && value is >= 0.01f and <= 8f) editor.MetaEditor.Change(meta.Copy(value)); @@ -805,7 +805,7 @@ public partial class ModEditWindow private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue) { using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue); newValue = currentValue; ImGui.Checkbox(label, ref newValue); @@ -820,7 +820,7 @@ public partial class ModEditWindow { newValue = currentValue; using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue); ImGui.SetNextItemWidth(width); if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 2ddd3ded..6ec0d3b6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -42,7 +42,7 @@ public partial class ModEditWindow ImGui.NewLine(); tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, - _fileDialog); + _fileDialog, _config.DefaultModImportPath); var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a922c477..46aedb33 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -29,7 +29,6 @@ public partial class ModEditWindow : Window, IDisposable private readonly PerformanceTracker _performance; private readonly ModEditor _editor; - private readonly ModCacheManager _modCaches; private readonly Configuration _config; private readonly ItemSwapTab _itemSwapTab; private readonly DataManager _gameData; @@ -277,7 +276,7 @@ public partial class ModEditWindow : Window, IDisposable var modifier = _config.DeleteModModifier.IsActive(); var tt = _allowReduplicate ? desc : - modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; + modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {_config.DeleteModModifier} to force normalization anyway."; if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { @@ -497,25 +496,26 @@ public partial class ModEditWindow : Window, IDisposable } public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, - Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager, StainService stainService) + Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, + StainService stainService) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _modCaches = modCaches; - _metaFileManager = metaFileManager; - _stainService = stainService; - _gameData = gameData; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _gameData = gameData; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", - () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new ShpkTab(_fileDialog, bytes)); + () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, + bytes => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); } diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index d69c3bdb..67dc17e2 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Penumbra.UI.Classes; @@ -64,6 +65,13 @@ public static class Colors // @formatter:on }; - public static uint Value(this ColorId color, Configuration config) - => config.Colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; + private static IReadOnlyDictionary _colors = new Dictionary(); + + /// Obtain the configured value for a color. + public static uint Value(this ColorId color) + => _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; + + /// Set the configurable colors dictionary to a value. + public static void SetColors(Configuration config) + => _colors = config.Colors; } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index ae21fde5..8993a554 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -23,7 +23,6 @@ namespace Penumbra.UI.CollectionTab; public sealed class CollectionPanel : IDisposable { - private readonly Configuration _config; private readonly CollectionStorage _collections; private readonly ActiveCollections _active; private readonly CollectionSelector _selector; @@ -39,10 +38,9 @@ public sealed class CollectionPanel : IDisposable private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new(); - public CollectionPanel(DalamudPluginInterface pi, Configuration config, CommunicatorService communicator, CollectionManager manager, + public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, CollectionSelector selector, ActorService actors, TargetManager targets, ModStorage mods) { - _config = config; _collections = manager.Storage; _active = manager.Active; _selector = selector; @@ -50,7 +48,7 @@ public sealed class CollectionPanel : IDisposable _targets = targets; _mods = mods; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); - _inheritanceUi = new InheritanceUi(_config, manager, _selector); + _inheritanceUi = new InheritanceUi(manager, _selector); _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); } @@ -81,7 +79,7 @@ public sealed class CollectionPanel : IDisposable DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); - ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value(_config)), + ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value()), ("Assignments take precedence before anything else and only apply to one specific character or monster.", 0)); ImGui.Dummy(Vector2.UnitX); @@ -254,13 +252,13 @@ public sealed class CollectionPanel : IDisposable collection ??= _active.ByType(type, id); using var color = ImRaii.PushColor(ImGuiCol.Button, collection == null - ? ColorId.NoAssignment.Value(_config) + ? ColorId.NoAssignment.Value() : redundancy.Length > 0 - ? ColorId.RedundantAssignment.Value(_config) + ? ColorId.RedundantAssignment.Value() : collection == _active.Current - ? ColorId.SelectedCollection.Value(_config) + ? ColorId.SelectedCollection.Value() : collection == ModCollection.Empty - ? ColorId.NoModsAssignment.Value(_config) + ? ColorId.NoModsAssignment.Value() : ImGui.GetColorU32(ImGuiCol.Button), !invalid) .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); using var disabled = ImRaii.Disabled(invalid); @@ -295,27 +293,27 @@ public sealed class CollectionPanel : IDisposable ImGui.TextUnformatted("Overruled by any other Assignment."); break; case CollectionType.Yourself: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; case CollectionType.MalePlayerCharacter: ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial Player", Colors.DiscordColor), (", ", 0), - ("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0), - ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + ("Your Character", ColorId.HandledConflictMod.Value()), (", or ", 0), + ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; case CollectionType.FemalePlayerCharacter: ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial Player", Colors.ReniColorActive), (", ", 0), - ("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0), - ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + ("Your Character", ColorId.HandledConflictMod.Value()), (", or ", 0), + ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; case CollectionType.MaleNonPlayerCharacter: ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial NPC", Colors.DiscordColor), (", ", 0), - ("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), - ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + ("Children", ColorId.FolderLine.Value()), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; case CollectionType.FemaleNonPlayerCharacter: ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial NPC", Colors.ReniColorActive), (", ", 0), - ("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), - ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0)); + ("Children", ColorId.FolderLine.Value()), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; } } diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 5cbb3a80..14611d59 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -75,7 +75,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl protected override bool OnDraw(int idx) { - using var color = ImRaii.PushColor(ImGuiCol.Header, ColorId.SelectedCollection.Value(_config)); + using var color = ImRaii.PushColor(ImGuiCol.Header, ColorId.SelectedCollection.Value()); var ret = ImGui.Selectable(Name(Items[idx]), idx == CurrentIdx); using var source = ImRaii.DragDropSource(); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 6a503f3d..87c09917 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -17,15 +17,13 @@ public class InheritanceUi private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly Configuration _config; private readonly CollectionStorage _collections; private readonly ActiveCollections _active; private readonly InheritanceManager _inheritance; private readonly CollectionSelector _selector; - public InheritanceUi(Configuration config, CollectionManager collectionManager, CollectionSelector selector) + public InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) { - _config = config; _selector = selector; _collections = collectionManager.Storage; _active = collectionManager.Active; @@ -37,7 +35,7 @@ public class InheritanceUi public void Draw() { using var id = ImRaii.PushId("##Inheritance"); - ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), (Name(_active.Current), ColorId.SelectedCollection.Value(_config) | 0xFF000000), (" inherits from:", 0)); + ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), (Name(_active.Current), ColorId.SelectedCollection.Value() | 0xFF000000), (" inherits from:", 0)); ImGui.Dummy(Vector2.One); DrawCurrentCollectionInheritance(); @@ -124,7 +122,7 @@ public class InheritanceUi foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) { // Draw the child, already seen collections are colored as conflicts. - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); @@ -150,7 +148,7 @@ public class InheritanceUi /// Draw a single primary inherited collection. private void DrawInheritance(ModCollection collection) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), _seenInheritedCollections.Contains(collection)); _seenInheritedCollections.Add(collection); using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8a78b6fb..2b4ce7e4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -28,7 +28,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector _config.SortMode; protected override uint ExpandedFolderColor - => ColorId.FolderExpanded.Value(_config); + => ColorId.FolderExpanded.Value(); protected override uint CollapsedFolderColor - => ColorId.FolderCollapsed.Value(_config); + => ColorId.FolderCollapsed.Value(); protected override uint FolderLineColor - => ColorId.FolderLine.Value(_config); + => ColorId.FolderLine.Value(); protected override bool FoldersDefaultOpen => _config.OpenFoldersByDefault; @@ -138,7 +136,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value(_config)) + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); using var id = ImRaii.PushId(leaf.Value.Index); ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); @@ -285,7 +283,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), 1 => !mod.Name.Contains(_modFilter), 2 => !mod.Author.Contains(_modFilter), - 3 => !_modCaches[mod].LowerChangedItemsString.Contains(_modFilter.Lower), - 4 => !_modCaches[mod].AllTagsLower.Contains(_modFilter.Lower), + 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), + 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), _ => false, // Should never happen }; } @@ -554,13 +552,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector Label => "Changed Items"u8; - public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector, ModCacheManager modCaches) + public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector) { - _api = api; - _selector = selector; - _modCaches = modCaches; + _api = api; + _selector = selector; } public bool IsVisible - => _modCaches[_selector.Selected!].ChangedItems.Count > 0; + => _selector.Selected!.ChangedItems.Count > 0; public void DrawContent() { @@ -35,7 +34,7 @@ public class ModPanelChangedItemsTab : ITab if (!list) return; - var zipList = ZipList.FromSortedList(_modCaches[_selector.Selected!].ChangedItems); + var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); var height = ImGui.GetTextLineHeight(); ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_api, kvp.Item1, kvp.Item2, true), height); } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 069c981d..1bdb32c0 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using Dalamud.Interface; using ImGuiNET; -using Lumina.Data.Parsing.Layer; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -15,15 +14,13 @@ namespace Penumbra.UI.ModsTab; public class ModPanelCollectionsTab : ITab { - private readonly Configuration _config; private readonly ModFileSystemSelector _selector; private readonly CollectionStorage _collections; private readonly List<(ModCollection, ModCollection, uint, string)> _cache = new(); - public ModPanelCollectionsTab(Configuration config, CollectionStorage storage, ModFileSystemSelector selector) + public ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) { - _config = config; _collections = storage; _selector = selector; } @@ -42,9 +39,8 @@ public class ModPanelCollectionsTab : ITab else ImGui.TextUnformatted($"This Mod is directly configured in {direct} collections."); if (inherited > 0) - { - ImGui.TextUnformatted($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); - } + ImGui.TextUnformatted( + $"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); ImGui.NewLine(); ImGui.Separator(); @@ -79,13 +75,13 @@ public class ModPanelCollectionsTab : ITab private (int Direct, int Inherited) CountUsage(Mod mod) { _cache.Clear(); - var undefined = ColorId.UndefinedMod.Value(_config); - var enabled = ColorId.EnabledMod.Value(_config); - var inherited = ColorId.InheritedMod.Value(_config); - var disabled = ColorId.DisabledMod.Value(_config); - var disInherited = ColorId.InheritedDisabledMod.Value(_config); - var directCount = 0; - var inheritedCount = 0; + var undefined = ColorId.UndefinedMod.Value(); + var enabled = ColorId.EnabledMod.Value(); + var inherited = ColorId.InheritedMod.Value(); + var disabled = ColorId.DisabledMod.Value(); + var disInherited = ColorId.InheritedDisabledMod.Value(); + var directCount = 0; + var inheritedCount = 0; foreach (var collection in _collections) { var (settings, parent) = collection[mod.Index]; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 4dd77b55..68a50123 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -43,7 +43,7 @@ public class ModPanelConflictsTab : ITab ImGui.SameLine(); using (var color = ImRaii.PushColor(ImGuiCol.Text, - conflict.HasPriority ? ColorId.HandledConflictMod.Value(Penumbra.Config) : ColorId.ConflictingMod.Value(Penumbra.Config))) + conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value())) { var priority = conflict.Mod2.Index < 0 ? conflict.Mod2.Priority diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs index d5ff7d7f..f3d66725 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -18,18 +18,18 @@ internal sealed class ResourceWatcherTable : Table public ResourceWatcherTable(Configuration config, ICollection records) : base("##records", records, - new PathColumn { Label = "Path" }, - new RecordTypeColumn(config) { Label = "Record" }, - new CollectionColumn { Label = "Collection" }, - new ObjectColumn { Label = "Game Object" }, - new CustomLoadColumn { Label = "Custom" }, - new SynchronousLoadColumn { Label = "Sync" }, - new OriginalPathColumn { Label = "Original Path" }, - new ResourceCategoryColumn { Label = "Category" }, - new ResourceTypeColumn { Label = "Type" }, - new HandleColumn { Label = "Resource" }, - new RefCountColumn { Label = "#Ref" }, - new DateColumn { Label = "Time" } + new PathColumn { Label = "Path" }, + new RecordTypeColumn(config) { Label = "Record" }, + new CollectionColumn { Label = "Collection" }, + new ObjectColumn { Label = "Game Object" }, + new CustomLoadColumn { Label = "Custom" }, + new SynchronousLoadColumn { Label = "Sync" }, + new OriginalPathColumn { Label = "Original Path" }, + new ResourceCategoryColumn(config) { Label = "Category" }, + new ResourceTypeColumn(config) { Label = "Type" }, + new HandleColumn { Label = "Resource" }, + new RefCountColumn { Label = "#Ref" }, + new DateColumn { Label = "Time" } ) { } @@ -111,7 +111,7 @@ internal sealed class ResourceWatcherTable : Table else _config.ResourceWatcherRecordTypes &= ~value; - Penumbra.Config.Save(); + _config.Save(); } public override void DrawColumn(Record item, int idx) @@ -175,8 +175,13 @@ internal sealed class ResourceWatcherTable : Table private sealed class ResourceCategoryColumn : ColumnFlags { - public ResourceCategoryColumn() - => AllFlags = ResourceExtensions.AllResourceCategories; + private readonly Configuration _config; + + public ResourceCategoryColumn(Configuration config) + { + _config = config; + AllFlags = ResourceExtensions.AllResourceCategories; + } public override float Width => 80 * UiHelpers.Scale; @@ -185,16 +190,16 @@ internal sealed class ResourceWatcherTable : Table => FilterValue.HasFlag(item.Category); public override ResourceCategoryFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceCategories; + => _config.ResourceWatcherResourceCategories; protected override void SetValue(ResourceCategoryFlag value, bool enable) { if (enable) - Penumbra.Config.ResourceWatcherResourceCategories |= value; + _config.ResourceWatcherResourceCategories |= value; else - Penumbra.Config.ResourceWatcherResourceCategories &= ~value; + _config.ResourceWatcherResourceCategories &= ~value; - Penumbra.Config.Save(); + _config.Save(); } public override void DrawColumn(Record item, int idx) @@ -205,8 +210,11 @@ internal sealed class ResourceWatcherTable : Table private sealed class ResourceTypeColumn : ColumnFlags { - public ResourceTypeColumn() + private readonly Configuration _config; + + public ResourceTypeColumn(Configuration config) { + _config = config; AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); for (var i = 0; i < Names.Length; ++i) Names[i] = Names[i].ToLowerInvariant(); @@ -219,16 +227,16 @@ internal sealed class ResourceWatcherTable : Table => FilterValue.HasFlag(item.ResourceType); public override ResourceTypeFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceTypes; + => _config.ResourceWatcherResourceTypes; protected override void SetValue(ResourceTypeFlag value, bool enable) { if (enable) - Penumbra.Config.ResourceWatcherResourceTypes |= value; + _config.ResourceWatcherResourceTypes |= value; else - Penumbra.Config.ResourceWatcherResourceTypes &= ~value; + _config.ResourceWatcherResourceTypes &= ~value; - Penumbra.Config.Save(); + _config.Save(); } public override void DrawColumn(Record item, int idx) diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 7f517634..6f36af56 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -94,7 +94,7 @@ public class ChangedItemsTab : ITab if (!UiHelpers.GetChangedItemObject(item.Value.Item2, out var text)) return; - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); ImGuiUtil.RightAlign(text); } } diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index ca6b6932..d6941ba0 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -46,7 +46,7 @@ public class CollectionsTab : IDisposable, ITab _config = configuration; _tutorial = tutorial; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); - _panel = new CollectionPanel(pi, configuration, communicator, collectionManager, _selector, actors, targets, modStorage); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage); } public void Dispose() @@ -116,8 +116,8 @@ public class CollectionsTab : IDisposable, ITab ImGui.SameLine(); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value(_config)) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value(_config)); + color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value()) + .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); if (ImGuiUtil.DrawDisabledButton( $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", buttonSize with { X = withSpacing }, string.Empty, false, true)) diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index f63cdf2a..47d6dbdc 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -111,7 +111,7 @@ public class ModsTab : ITab private void DrawRedrawLine() { - if (Penumbra.Config.HideRedrawBar) + if (_config.HideRedrawBar) { _tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); return; @@ -172,7 +172,7 @@ public class ModsTab : ITab ImGui.SameLine(); DrawInheritedCollectionButton(3 * buttonSize); ImGui.SameLine(); - _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value(_config)); + _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value()); } _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 80a181a4..0da6a407 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -69,8 +69,8 @@ public class SettingsTab : ITab return; DrawEnabledBox(); - Checkbox("Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, - v => Penumbra.Config.FixMainWindow = v); + Checkbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.FixMainWindow, + v => _config.FixMainWindow = v); ImGui.NewLine(); DrawRootFolder(); @@ -220,7 +220,7 @@ public class SettingsTab : ITab if (_config.ModDirectory != _newModDirectory && _newModDirectory.Length != 0 - && DrawPressEnterWarning(_newModDirectory, Penumbra.Config.ModDirectory, pos, save, selected)) + && DrawPressEnterWarning(_newModDirectory, _config.ModDirectory, pos, save, selected)) _modManager.DiscoverMods(_newModDirectory); } @@ -344,14 +344,14 @@ public class SettingsTab : ITab _dalamud.UiBuilder.DisableUserUiHide = !v; }); Checkbox("Hide Config Window when in Cutscenes", - "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, + "Hide the penumbra main window when you are currently watching a cutscene.", _config.HideUiInCutscenes, v => { _config.HideUiInCutscenes = v; _dalamud.UiBuilder.DisableCutsceneUiHide = !v; }); Checkbox("Hide Config Window when in GPose", - "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, + "Hide the penumbra main window when you are currently in GPose mode.", _config.HideUiInGPose, v => { _config.HideUiInGPose = v; @@ -481,7 +481,7 @@ public class SettingsTab : ITab Widget.DoubleModifierSelector("Mod Deletion Modifier", "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", UiHelpers.InputTextWidth.X, - Penumbra.Config.DeleteModModifier, + _config.DeleteModModifier, v => { _config.DeleteModModifier = v; @@ -742,8 +742,8 @@ public class SettingsTab : ITab ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) { - Penumbra.Config.TutorialStep = 0; - Penumbra.Config.Save(); + _config.TutorialStep = 0; + _config.Save(); } ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 11d06428..7cd1c120 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -86,7 +86,7 @@ public static class UiHelpers return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); - ImGuiUtil.RightJustify(text, ColorId.ItemId.Value(Penumbra.Config)); + ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); } /// Return more detailed object information in text, if it exists. From 2c55701cbfba4a370b1460bbea368e779bb12f69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 21 Apr 2023 23:56:20 +0200 Subject: [PATCH 0895/2451] Remove static ActorService. --- Penumbra/Collections/Cache/MetaCache.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 18 ++++++++++++------ .../Manager/TempCollectionManager.cs | 7 ++++--- Penumbra/Penumbra.cs | 2 -- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index abb77f9d..514e2589 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -111,7 +111,7 @@ public class MetaCache : IDisposable, IEnumerable Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection, - CollectionType.Individual => null, + CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex] + .Collection, + CollectionType.Individual => null, _ when collectionType.IsSpecial() => SpecialCollections[(int)collectionType] ?? Default, _ => null, }; @@ -433,7 +439,7 @@ public class ActiveCollections : ISavable, IDisposable { case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: { - var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + var global = ByType(CollectionType.Individual, _actors.AwaitedService.CreatePlayer(id.PlayerName, ushort.MaxValue)); return global?.Index == checkAssignment.Index ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." : string.Empty; @@ -442,12 +448,12 @@ public class ActiveCollections : ISavable, IDisposable if (id.HomeWorld != ushort.MaxValue) { var global = ByType(CollectionType.Individual, - Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + _actors.AwaitedService.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); if (global?.Index == checkAssignment.Index) return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; } - var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + var unowned = ByType(CollectionType.Individual, _actors.AwaitedService.CreateNpc(id.Kind, id.DataId)); return unowned?.Index == checkAssignment.Index ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." : string.Empty; diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 34a8db3c..0416b4b7 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -17,11 +17,13 @@ public class TempCollectionManager : IDisposable private readonly CommunicatorService _communicator; private readonly CollectionStorage _storage; + private readonly ActorService _actors; private readonly Dictionary _customCollections = new(); public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorService actors, CollectionStorage storage) { _communicator = communicator; + _actors = actors; _storage = storage; Collections = new IndividualCollections(actors, config); @@ -97,7 +99,6 @@ public class TempCollectionManager : IDisposable Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}."); _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); return true; - } public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) @@ -113,7 +114,7 @@ public class TempCollectionManager : IDisposable if (!ByteString.FromString(characterName, out var byteString, false)) return false; - var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + var identifier = _actors.AwaitedService.CreatePlayer(byteString, worldId); if (!identifier.IsValid) return false; @@ -125,7 +126,7 @@ public class TempCollectionManager : IDisposable if (!ByteString.FromString(characterName, out var byteString, false)) return false; - var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId); + var identifier = _actors.AwaitedService.CreatePlayer(byteString, worldId); return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5dd865a2..3eb83f65 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -39,7 +39,6 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!; - public static ActorManager Actors { get; private set; } = null!; public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; @@ -68,7 +67,6 @@ public class Penumbra : IDalamudPlugin _tmp.Services.GetRequiredService(); _config = _tmp.Services.GetRequiredService(); CharacterUtility = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); From c49454fc25a8a2a78c7bfcc3a37380a68f0b6b6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 00:11:47 +0200 Subject: [PATCH 0896/2451] Move MetaList out of CharacterUtility and remove static CollectionManager. --- Penumbra/Collections/Cache/CmpCache.cs | 2 +- Penumbra/Collections/Cache/EqdpCache.cs | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 2 +- Penumbra/Collections/Cache/EstCache.cs | 2 +- Penumbra/Collections/Cache/GmpCache.cs | 2 +- Penumbra/Collections/Cache/MetaCache.cs | 10 +- .../Collections/ModCollection.Cache.Access.cs | 22 +-- Penumbra/Configuration.cs | 9 +- .../Interop/Services/CharacterUtility.List.cs | 166 ------------------ Penumbra/Interop/Services/CharacterUtility.cs | 14 +- Penumbra/Interop/Services/MetaList.cs | 157 +++++++++++++++++ Penumbra/Meta/MetaFileManager.cs | 2 +- Penumbra/Penumbra.cs | 56 +++--- Penumbra/Services/ConfigMigrationService.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 26 +-- 15 files changed, 237 insertions(+), 247 deletions(-) delete mode 100644 Penumbra/Interop/Services/CharacterUtility.List.cs create mode 100644 Penumbra/Interop/Services/MetaList.cs diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs index 47d6a441..9333501a 100644 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ b/Penumbra/Collections/Cache/CmpCache.cs @@ -21,7 +21,7 @@ public struct CmpCache : IDisposable public void SetFiles(MetaFileManager manager) => manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) => manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); public void Reset() diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 4a1325ed..db40fcb6 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -34,7 +34,7 @@ public readonly struct EqdpCache : IDisposable manager.SetFile(_eqdpFiles[i], index); } - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) { var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); Debug.Assert(idx >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index f3b7e8f1..5fe40426 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -24,7 +24,7 @@ public struct EqpCache : IDisposable public static void ResetFiles(MetaFileManager manager) => manager.SetFile( null, MetaIndex.Eqp ); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) => manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); public void Reset() diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index ac30e0d6..d079a532 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -49,7 +49,7 @@ public struct EstCache : IDisposable } } - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) { var (file, idx) = type switch { diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 2698af7b..4e485036 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -21,7 +21,7 @@ public struct GmpCache : IDisposable public void SetFiles(MetaFileManager manager) => manager.SetFile( _gmpFile, MetaIndex.Gmp ); - public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) => manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); public void Reset() diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 514e2589..559fb3cd 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -159,19 +159,19 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.SetFiles(_collection); - public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() + public MetaList.MetaReverter TemporarilySetEqpFile() => _eqpCache.TemporarilySetFiles(_manager); - public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + public MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); - public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() + public MetaList.MetaReverter TemporarilySetGmpFile() => _gmpCache.TemporarilySetFiles(_manager); - public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() + public MetaList.MetaReverter TemporarilySetCmpFile() => _cmpCache.TemporarilySetFiles(_manager); - public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) => _estCache.TemporarilySetFiles(_manager, type); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 2ccdf3c3..7597f7ca 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Runtime.CompilerServices; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using Penumbra.Collections.Cache; - +using Penumbra.Collections.Cache; +using Penumbra.Interop.Services; + namespace Penumbra.Collections; public partial class ModCollection @@ -94,28 +93,23 @@ public partial class ModCollection } // Used for short periods of changed files. - public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + public MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory) ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); - public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile() + public MetaList.MetaReverter TemporarilySetEqpFile() => _cache?.Meta.TemporarilySetEqpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Eqp); - public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile() + public MetaList.MetaReverter TemporarilySetGmpFile() => _cache?.Meta.TemporarilySetGmpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Gmp); - public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile() + public MetaList.MetaReverter TemporarilySetCmpFile() => _cache?.Meta.TemporarilySetCmpFile() ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.HumanCmp); - public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); -} - - -public static class CollectionCacheExtensions -{ } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 568b2fc5..3799d9db 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -10,6 +10,7 @@ using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; @@ -101,14 +102,14 @@ public class Configuration : IPluginConfiguration, ISavable /// Load the current configuration. /// Includes adding new colors and migrating from old versions. /// - public Configuration(FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService) + public Configuration(CharacterUtility utility, FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService) { _saveService = saveService; - Load(fileNames, migrator); + Load(utility, fileNames, migrator); UI.Classes.Colors.SetColors(this); } - public void Load(FilenameService fileNames, ConfigMigrationService migrator) + public void Load(CharacterUtility utility, FilenameService fileNames, ConfigMigrationService migrator) { static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { @@ -126,7 +127,7 @@ public class Configuration : IPluginConfiguration, ISavable }); } - migrator.Migrate(this); + migrator.Migrate(utility, this); } /// Save the current configuration. diff --git a/Penumbra/Interop/Services/CharacterUtility.List.cs b/Penumbra/Interop/Services/CharacterUtility.List.cs deleted file mode 100644 index abe024af..00000000 --- a/Penumbra/Interop/Services/CharacterUtility.List.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using Penumbra.Interop.Structs; - -namespace Penumbra.Interop.Services; - -public unsafe partial class CharacterUtility -{ - public class MetaList : IDisposable - { - private readonly LinkedList _entries = new(); - public readonly InternalIndex Index; - public readonly MetaIndex GlobalMetaIndex; - - public IReadOnlyCollection Entries - => _entries; - - private nint _defaultResourceData = nint.Zero; - private int _defaultResourceSize = 0; - public bool Ready { get; private set; } = false; - - public MetaList(InternalIndex index) - { - Index = index; - GlobalMetaIndex = RelevantIndices[index.Value]; - } - - public void SetDefaultResource(nint data, int size) - { - if (Ready) - return; - - _defaultResourceData = data; - _defaultResourceSize = size; - Ready = _defaultResourceData != nint.Zero && size != 0; - if (_entries.Count <= 0) - return; - - var first = _entries.First!.Value; - SetResource(first.Data, first.Length); - } - - public (nint Address, int Size) DefaultResource - => (_defaultResourceData, _defaultResourceSize); - - public MetaReverter TemporarilySetResource(nint data, int length) - { -#if false - Penumbra.Log.Verbose($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); -#endif - var reverter = new MetaReverter(this, data, length); - _entries.AddFirst(reverter); - SetResourceInternal(data, length); - return reverter; - } - - public MetaReverter TemporarilyResetResource() - { -#if false - Penumbra.Log.Verbose( - $"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); -#endif - var reverter = new MetaReverter(this); - _entries.AddFirst(reverter); - ResetResourceInternal(); - return reverter; - } - - public void SetResource(nint data, int length) - { -#if false - Penumbra.Log.Verbose($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); -#endif - SetResourceInternal(data, length); - } - - public void ResetResource() - { -#if false - Penumbra.Log.Verbose($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); -#endif - ResetResourceInternal(); - } - - /// Set the currently stored data of this resource to new values. - private void SetResourceInternal(nint data, int length) - { - if (!Ready) - return; - - var resource = Penumbra.CharacterUtility.Address->Resource(GlobalMetaIndex); - resource->SetData(data, length); - } - - /// Reset the currently stored data of this resource to its default values. - private void ResetResourceInternal() - => SetResourceInternal(_defaultResourceData, _defaultResourceSize); - - private void SetResourceToDefaultCollection() - => Penumbra.CollectionManager.Active.Default.SetMetaFile(GlobalMetaIndex); - - public void Dispose() - { - if (_entries.Count > 0) - { - foreach (var entry in _entries) - entry.Disposed = true; - - _entries.Clear(); - } - - ResetResourceInternal(); - } - - public sealed class MetaReverter : IDisposable - { - public readonly MetaList MetaList; - public readonly nint Data; - public readonly int Length; - public readonly bool Resetter; - public bool Disposed; - - public MetaReverter(MetaList metaList, nint data, int length) - { - MetaList = metaList; - Data = data; - Length = length; - } - - public MetaReverter(MetaList metaList) - { - MetaList = metaList; - Data = nint.Zero; - Length = 0; - Resetter = true; - } - - public void Dispose() - { - if (Disposed) - return; - - var list = MetaList._entries; - var wasCurrent = ReferenceEquals(this, list.First?.Value); - list.Remove(this); - if (!wasCurrent) - return; - - if (list.Count == 0) - { - MetaList.SetResourceToDefaultCollection(); - } - else - { - var next = list.First!.Value; - if (next.Resetter) - MetaList.ResetResourceInternal(); - else - MetaList.SetResourceInternal(next.Data, next.Length); - } - - Disposed = true; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 17052101..3ac83e50 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game; using Dalamud.Utility.Signatures; +using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -45,9 +46,7 @@ public unsafe partial class CharacterUtility : IDisposable .Select(i => new InternalIndex(Array.IndexOf(RelevantIndices, (MetaIndex)i))) .ToArray(); - private readonly MetaList[] _lists = Enumerable.Range(0, RelevantIndices.Length) - .Select(idx => new MetaList(new InternalIndex(idx))) - .ToArray(); + private readonly MetaList[] _lists; public IReadOnlyList Lists => _lists; @@ -55,12 +54,17 @@ public unsafe partial class CharacterUtility : IDisposable public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; - private readonly Framework _framework; + private readonly Framework _framework; + public readonly ActiveCollectionData Active; - public CharacterUtility(Framework framework) + public CharacterUtility(Framework framework, ActiveCollectionData active) { SignatureHelper.Initialise(this); + _lists = Enumerable.Range(0, RelevantIndices.Length) + .Select(idx => new MetaList(this, new InternalIndex(idx))) + .ToArray(); _framework = framework; + Active = active; LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); LoadDefaultResources(null!); if (!Ready) diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs new file mode 100644 index 00000000..c0c38ff4 --- /dev/null +++ b/Penumbra/Interop/Services/MetaList.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Services; + +public unsafe class MetaList : IDisposable +{ + private readonly CharacterUtility _utility; + private readonly LinkedList _entries = new(); + public readonly CharacterUtility.InternalIndex Index; + public readonly MetaIndex GlobalMetaIndex; + + public IReadOnlyCollection Entries + => _entries; + + private nint _defaultResourceData = nint.Zero; + private int _defaultResourceSize = 0; + public bool Ready { get; private set; } = false; + + public MetaList(CharacterUtility utility, CharacterUtility.InternalIndex index) + { + _utility = utility; + Index = index; + GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value]; + } + + public void SetDefaultResource(nint data, int size) + { + if (Ready) + return; + + _defaultResourceData = data; + _defaultResourceSize = size; + Ready = _defaultResourceData != nint.Zero && size != 0; + if (_entries.Count <= 0) + return; + + var first = _entries.First!.Value; + SetResource(first.Data, first.Length); + } + + public (nint Address, int Size) DefaultResource + => (_defaultResourceData, _defaultResourceSize); + + public MetaReverter TemporarilySetResource(nint data, int length) + { + Penumbra.Log.Excessive($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); + var reverter = new MetaReverter(this, data, length); + _entries.AddFirst(reverter); + SetResourceInternal(data, length); + return reverter; + } + + public MetaReverter TemporarilyResetResource() + { + Penumbra.Log.Excessive( + $"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); + var reverter = new MetaReverter(this); + _entries.AddFirst(reverter); + ResetResourceInternal(); + return reverter; + } + + public void SetResource(nint data, int length) + { + Penumbra.Log.Excessive($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); + SetResourceInternal(data, length); + } + + public void ResetResource() + { + Penumbra.Log.Excessive($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); + ResetResourceInternal(); + } + + /// Set the currently stored data of this resource to new values. + private void SetResourceInternal(nint data, int length) + { + if (!Ready) + return; + + var resource = _utility.Address->Resource(GlobalMetaIndex); + resource->SetData(data, length); + } + + /// Reset the currently stored data of this resource to its default values. + private void ResetResourceInternal() + => SetResourceInternal(_defaultResourceData, _defaultResourceSize); + + private void SetResourceToDefaultCollection() + => _utility.Active.Default.SetMetaFile(GlobalMetaIndex); + + public void Dispose() + { + if (_entries.Count > 0) + { + foreach (var entry in _entries) + entry.Disposed = true; + + _entries.Clear(); + } + + ResetResourceInternal(); + } + + public sealed class MetaReverter : IDisposable + { + public readonly MetaList MetaList; + public readonly nint Data; + public readonly int Length; + public readonly bool Resetter; + public bool Disposed; + + public MetaReverter(MetaList metaList, nint data, int length) + { + MetaList = metaList; + Data = data; + Length = length; + } + + public MetaReverter(MetaList metaList) + { + MetaList = metaList; + Data = nint.Zero; + Length = 0; + Resetter = true; + } + + public void Dispose() + { + if (Disposed) + return; + + var list = MetaList._entries; + var wasCurrent = ReferenceEquals(this, list.First?.Value); + list.Remove(this); + if (!wasCurrent) + return; + + if (list.Count == 0) + { + MetaList.SetResourceToDefaultCollection(); + } + else + { + var next = list.First!.Value; + if (next.Resetter) + MetaList.ResetResourceInternal(); + else + MetaList.SetResourceInternal(next.Data, next.Length); + } + + Disposed = true; + } + } +} diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 033fc4ea..30186768 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -78,7 +78,7 @@ public unsafe class MetaFileManager } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public CharacterUtility.MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) + public MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) => file == null ? CharacterUtility.TemporarilyResetResource(metaIndex) : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3eb83f65..4892f5d9 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -15,12 +15,8 @@ using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.Collections.Cache; -using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using DalamudUtil = Dalamud.Utility.Util; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; @@ -34,11 +30,10 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static Logger Log { get; private set; } = null!; - public static ChatService ChatService { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; @@ -50,7 +45,9 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; private readonly Configuration _config; + private readonly CharacterUtility _characterUtility; private PenumbraWindowSystem? _windowSystem; private bool _disposed; @@ -65,16 +62,17 @@ public class Penumbra : IDalamudPlugin ChatService = _tmp.Services.GetRequiredService(); _validityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - _config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); + _config = _tmp.Services.GetRequiredService(); + _characterUtility = _tmp.Services.GetRequiredService(); + CharacterUtility = _characterUtility; _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - _modManager = _tmp.Services.GetRequiredService(); - CollectionManager = _tmp.Services.GetRequiredService(); - _tempCollections = _tmp.Services.GetRequiredService(); - ModFileSystem = _tmp.Services.GetRequiredService(); - RedrawService = _tmp.Services.GetRequiredService(); + _modManager = _tmp.Services.GetRequiredService(); + _collectionManager = _tmp.Services.GetRequiredService(); + _tempCollections = _tmp.Services.GetRequiredService(); + ModFileSystem = _tmp.Services.GetRequiredService(); + RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) @@ -91,7 +89,7 @@ public class Penumbra : IDalamudPlugin OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); - if (CharacterUtility.Ready) + if (_characterUtility.Ready) _residentResources.Reload(); } catch @@ -149,18 +147,18 @@ public class Penumbra : IDalamudPlugin _config.EnableMods = enabled; if (enabled) { - if (CharacterUtility.Ready) + if (_characterUtility.Ready) { - CollectionManager.Active.Default.SetFiles(); + _collectionManager.Active.Default.SetFiles(); _residentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } } else { - if (CharacterUtility.Ready) + if (_characterUtility.Ready) { - CharacterUtility.ResetAll(); + _characterUtility.ResetAll(); _residentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } @@ -194,7 +192,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); sb.Append($"> **`Enable Mods: `** {_config.EnableMods}\n"); sb.Append($"> **`Enable HTTP API: `** {_config.EnableHttpApi}\n"); - sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); + sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); @@ -225,23 +223,23 @@ public class Penumbra : IDalamudPlugin + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); sb.AppendLine("**Collections**"); - sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n"); + sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - _tempCollections.Count}\n"); - sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n"); + sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count - _tempCollections.Count}\n"); + sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var collection = CollectionManager.Active.ByType(type); + var collection = _collectionManager.Active.ByType(type); if (collection != null) sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); } - foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments) + foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n"); - foreach (var (collection, cache) in CollectionManager.Caches.Active) + foreach (var (collection, cache) in _collectionManager.Caches.Active) PrintCollection(collection, cache); return sb.ToString(); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 2d525c00..33f41deb 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -2,17 +2,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Plugin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; using Penumbra.Util; -using SixLabors.ImageSharp; namespace Penumbra.Services; @@ -50,7 +49,7 @@ public class ConfigMigrationService config.Save(); } - public void Migrate(Configuration config) + public void Migrate(CharacterUtility utility, Configuration config) { _config = config; // Do this on every migration from now on for a while @@ -67,7 +66,7 @@ public class ConfigMigrationService CreateBackup(); Version0To1(); - Version1To2(); + Version1To2(utility); Version2To3(); Version3To4(); Version4To5(); @@ -149,14 +148,15 @@ public class ConfigMigrationService // Sort Order was moved to a separate file and may contain empty folders. // Active collections in general were moved to their own file. // Delete the penumbrametatmp folder if it exists. - private void Version1To2() + private void Version1To2(CharacterUtility utility) { if (_config.Version != 1) return; // Ensure the right meta files are loaded. DeleteMetaTmp(); - Penumbra.CharacterUtility.LoadCharacterResources(); + if (utility.Ready) + utility.LoadCharacterResources(); ResettleSortOrder(); ResettleCollectionSettings(); ResettleForcedCollection(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 46aedb33..517118c9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -9,13 +9,13 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; -using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -33,6 +33,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ItemSwapTab _itemSwapTab; private readonly DataManager _gameData; private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; private readonly StainService _stainService; private Mod? _mod; @@ -56,7 +57,7 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Active.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); } public void ChangeOption(SubMod? subMod) @@ -479,7 +480,7 @@ public partial class ModEditWindow : Window, IDisposable /// private FullPath FindBestMatch(Utf8GamePath path) { - var currentFile = Penumbra.CollectionManager.Active.Current.ResolvePath(path); + var currentFile = _activeCollections.Current.ResolvePath(path); if (currentFile != null) return currentFile.Value; @@ -497,17 +498,18 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService) + StainService stainService, ActiveCollections activeCollections) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _gameData = gameData; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _gameData = gameData; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); From 826777b7eefd62a85826acde7d6eb9302a8b39d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 00:28:05 +0200 Subject: [PATCH 0897/2451] Remove static Dalamud Services. --- Penumbra/Api/IpcTester.cs | 57 +++++++++---------- Penumbra/Api/PenumbraIpcProviders.cs | 6 +- Penumbra/Import/Textures/CombinedTexture.cs | 5 +- Penumbra/Import/Textures/Texture.cs | 45 ++++++++------- Penumbra/Interop/Services/RedrawService.cs | 6 +- Penumbra/Meta/Files/ImcFile.cs | 2 +- Penumbra/Mods/ItemSwap/Swaps.cs | 2 +- Penumbra/Services/DalamudServices.cs | 42 ++++++-------- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../ModEditWindow.QuickImport.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 20 +++---- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 5 +- Penumbra/UI/Tabs/DebugTab.cs | 2 +- Penumbra/UI/Tabs/ModsTab.cs | 11 ++-- Penumbra/UI/Tabs/SettingsTab.cs | 12 ++-- 16 files changed, 111 insertions(+), 114 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 1044581f..d1124847 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -40,22 +40,21 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Temporary _temporary; - public IpcTester(Configuration config, DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager, - CollectionManager collections, - TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) + public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, + CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) { _ipcProviders = ipcProviders; - _pluginState = new PluginState(pi); - _ipcConfiguration = new IpcConfiguration(pi); - _ui = new Ui(pi); - _redrawing = new Redrawing(pi); - _gameState = new GameState(pi); - _resolve = new Resolve(pi); - _collections = new Collections(pi); - _meta = new Meta(pi); - _mods = new Mods(pi); - _modSettings = new ModSettings(pi); - _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config); + _pluginState = new PluginState(dalamud.PluginInterface); + _ipcConfiguration = new IpcConfiguration(dalamud.PluginInterface); + _ui = new Ui(dalamud.PluginInterface); + _redrawing = new Redrawing(dalamud); + _gameState = new GameState(dalamud.PluginInterface); + _resolve = new Resolve(dalamud.PluginInterface); + _collections = new Collections(dalamud.PluginInterface); + _meta = new Meta(dalamud.PluginInterface); + _mods = new Mods(dalamud.PluginInterface); + _modSettings = new ModSettings(dalamud.PluginInterface); + _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); UnsubscribeEvents(); } @@ -395,17 +394,17 @@ public class IpcTester : IDisposable private class Redrawing { - private readonly DalamudPluginInterface _pi; + private readonly DalamudServices _dalamud; public readonly EventSubscriber Redrawn; private string _redrawName = string.Empty; private int _redrawIndex = 0; private string _lastRedrawnString = "None"; - public Redrawing(DalamudPluginInterface pi) - { - _pi = pi; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(pi, SetLastRedrawn); + public Redrawing(DalamudServices dalamud) + { + _dalamud = dalamud; + Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); } public void Draw() @@ -423,25 +422,25 @@ public class IpcTester : IDisposable ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32); ImGui.SameLine(); if (ImGui.Button("Redraw##Name")) - Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw); + Ipc.RedrawObjectByName.Subscriber(_dalamud.PluginInterface).Invoke(_redrawName, RedrawType.Redraw); DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character"); - if (ImGui.Button("Redraw##pc") && DalamudServices.SClientState.LocalPlayer != null) - Ipc.RedrawObject.Subscriber(_pi).Invoke(DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw); + if (ImGui.Button("Redraw##pc") && _dalamud.ClientState.LocalPlayer != null) + Ipc.RedrawObject.Subscriber(_dalamud.PluginInterface).Invoke(_dalamud.ClientState.LocalPlayer, RedrawType.Redraw); DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); var tmp = _redrawIndex; ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length)) - _redrawIndex = Math.Clamp(tmp, 0, DalamudServices.SObjects.Length); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _dalamud.Objects.Length)) + _redrawIndex = Math.Clamp(tmp, 0, _dalamud.Objects.Length); ImGui.SameLine(); if (ImGui.Button("Redraw##Index")) - Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw); + Ipc.RedrawObjectByIndex.Subscriber(_dalamud.PluginInterface).Invoke(_redrawIndex, RedrawType.Redraw); DrawIntro(Ipc.RedrawAll.Label, "Redraw All"); if (ImGui.Button("Redraw##All")) - Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw); + Ipc.RedrawAll.Subscriber(_dalamud.PluginInterface).Invoke(RedrawType.Redraw); DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:"); ImGui.TextUnformatted(_lastRedrawnString); @@ -450,12 +449,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn(IntPtr address, int index) { if (index < 0 - || index > DalamudServices.SObjects.Length + || index > _dalamud.Objects.Length || address == IntPtr.Zero - || DalamudServices.SObjects[index]?.Address != address) + || _dalamud.Objects[index]?.Address != address) _lastRedrawnString = "Invalid"; - _lastRedrawnString = $"{DalamudServices.SObjects[index]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{_dalamud.Objects[index]!.Name} (0x{address:X}, {index})"; } } diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index e8139453..7ccd7e20 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -7,6 +7,7 @@ using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Api; @@ -115,9 +116,10 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryModAll; internal readonly FuncProvider RemoveTemporaryMod; - public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, + public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { + var pi = dalamud.PluginInterface; Api = api; // Plugin State @@ -228,7 +230,7 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - Tester = new IpcTester(config, pi, this, modManager, collections, tempMods, tempCollections, saveService); + Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); Initialized.Invoke(); } diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index bd9154a0..fcfbc3ee 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Numerics; +using Dalamud.Interface; using Lumina.Data.Files; using OtterTex; using Penumbra.Services; @@ -43,13 +44,13 @@ public partial class CombinedTexture : IDisposable public Exception? SaveException { get; private set; } = null; - public void Draw( Vector2 size ) + public void Draw( UiBuilder builder, Vector2 size ) { if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) { var (width, height) = CombineImage(); _centerStorage.TextureWrap = - DalamudServices.PluginInterface.UiBuilder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); } _current?.Draw( size ); diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 3a576028..22096b37 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Numerics; +using Dalamud.Data; using Dalamud.Interface; using ImGuiNET; using ImGuiScene; @@ -121,7 +122,7 @@ public sealed class Texture : IDisposable public event Action? Loaded; - private void Load(string path) + private void Load(DalamudServices dalamud, string path) { _tmpPath = null; if (path == Path) @@ -136,9 +137,9 @@ public sealed class Texture : IDisposable { var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch { - ".dds" => LoadDds(), - ".png" => LoadPng(), - ".tex" => LoadTex(), + ".dds" => LoadDds(dalamud), + ".png" => LoadPng(dalamud), + ".tex" => LoadTex(dalamud), _ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."), }; Loaded?.Invoke(true); @@ -150,18 +151,18 @@ public sealed class Texture : IDisposable } } - private bool LoadDds() + private bool LoadDds(DalamudServices dalamud) { Type = FileType.Dds; var scratch = ScratchImage.LoadDDS(Path); BaseImage = scratch; var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); - CreateTextureWrap(f.Meta.Width, f.Meta.Height); + CreateTextureWrap(dalamud.UiBuilder, f.Meta.Width, f.Meta.Height); return true; } - private bool LoadPng() + private bool LoadPng(DalamudServices dalamud) { Type = FileType.Png; BaseImage = null; @@ -169,37 +170,37 @@ public sealed class Texture : IDisposable using var png = Image.Load(stream); RGBAPixels = new byte[png.Height * png.Width * 4]; png.CopyPixelDataTo(RGBAPixels); - CreateTextureWrap(png.Width, png.Height); + CreateTextureWrap(dalamud.UiBuilder, png.Width, png.Height); return true; } - private bool LoadTex() + private bool LoadTex(DalamudServices dalamud) { Type = FileType.Tex; - using var stream = OpenTexStream(); + using var stream = OpenTexStream(dalamud.GameData); var scratch = TexFileParser.Parse(stream); BaseImage = scratch; var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); - CreateTextureWrap(scratch.Meta.Width, scratch.Meta.Height); + CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height); return true; } - private Stream OpenTexStream() + private Stream OpenTexStream(DataManager gameData) { if (System.IO.Path.IsPathRooted(Path)) return File.OpenRead(Path); - var file = DalamudServices.SGameData.GetFile(Path); + var file = gameData.GetFile(Path); return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{Path}\" from game files."); } - private void CreateTextureWrap(int width, int height) - => TextureWrap = DalamudServices.PluginInterface.UiBuilder.LoadImageRaw(RGBAPixels, width, height, 4); + private void CreateTextureWrap(UiBuilder builder, int width, int height) + => TextureWrap = builder.LoadImageRaw(RGBAPixels, width, height, 4); private string? _tmpPath; - public void PathSelectBox(string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix) + public void PathSelectBox(DalamudServices dalamud, string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix) { ImGui.SetNextItemWidth(-0.0001f); var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here..."; @@ -209,7 +210,7 @@ public sealed class Texture : IDisposable { if (game) { - if (!DalamudServices.SGameData.FileExists(path)) + if (!dalamud.GameData.FileExists(path)) continue; } else if (!File.Exists(path)) @@ -222,7 +223,7 @@ public sealed class Texture : IDisposable { var p = game ? $"--> {path}" : path[skipPrefix..]; if (ImGui.Selectable(p, path == startPath) && path != startPath) - Load(path); + Load(dalamud, path); } ImGuiUtil.HoverTooltip(game @@ -233,7 +234,7 @@ public sealed class Texture : IDisposable ImGuiUtil.HoverTooltip(tooltip); } - public void PathInputBox(string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, + public void PathInputBox(DalamudServices dalamud, string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, string defaultModImportPath) { _tmpPath ??= Path; @@ -242,7 +243,7 @@ public sealed class Texture : IDisposable ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); ImGui.InputTextWithHint(label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength); if (ImGui.IsItemDeactivatedAfterEdit()) - Load(_tmpPath); + Load(dalamud, _tmpPath); ImGuiUtil.HoverTooltip(tooltip); ImGui.SameLine(); @@ -257,7 +258,7 @@ public sealed class Texture : IDisposable void UpdatePath(bool success, List paths) { if (success && paths.Count > 0) - texture.Load(paths[0]); + texture.Load(dalamud, paths[0]); } fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); @@ -270,7 +271,7 @@ public sealed class Texture : IDisposable { var path = Path; Path = string.Empty; - Load(path); + Load(dalamud, path); } } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index d85204b1..f78d6dca 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -293,10 +293,10 @@ public sealed unsafe partial class RedrawService : IDisposable } } - private static GameObject? GetLocalPlayer() + private GameObject? GetLocalPlayer() { - var gPosePlayer = DalamudServices.SObjects[GPosePlayerIdx]; - return gPosePlayer ?? DalamudServices.SObjects[0]; + var gPosePlayer = _objects[GPosePlayerIdx]; + return gPosePlayer ?? _objects[0]; } public bool GetName(string lowerName, out GameObject? actor) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 79313a6f..d47bf387 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -130,7 +130,7 @@ public unsafe class ImcFile : MetaBaseFile public override void Reset() { - var file = DalamudServices.SGameData.GetFile(Path.ToString()); + var file = Manager.GameData.GetFile(Path.ToString()); fixed (byte* ptr = file!.Data) { MemoryUtility.MemCpyUnchecked(Data, ptr, file.Data.Length); diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index d689c2cf..36ca77cf 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -141,7 +141,7 @@ public sealed class FileSwap : Swap swap.SwapToModded = redirections(swap.SwapToRequestPath); swap.SwapToModdedExistsInGame = - !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists(swap.SwapToModded.InternalName.ToString()); + !swap.SwapToModded.IsRooted && manager.GameData.FileExists(swap.SwapToModded.InternalName.ToString()); swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals(swap.SwapFromRequestPath.Path); swap.FileData = type switch diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 7320822d..a75f3e4a 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -63,49 +63,41 @@ public class DalamudServices { services.AddSingleton(PluginInterface); services.AddSingleton(Commands); - services.AddSingleton(SGameData); - services.AddSingleton(SClientState); + services.AddSingleton(GameData); + services.AddSingleton(ClientState); services.AddSingleton(Chat); services.AddSingleton(Framework); services.AddSingleton(Conditions); services.AddSingleton(Targets); - services.AddSingleton(SObjects); + services.AddSingleton(Objects); services.AddSingleton(TitleScreenMenu); services.AddSingleton(GameGui); services.AddSingleton(KeyState); services.AddSingleton(SigScanner); services.AddSingleton(this); + services.AddSingleton(UiBuilder); } // TODO remove static // @formatter:off - [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager SGameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState SClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable SObjects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; // @formatter:on public UiBuilder UiBuilder => PluginInterface.UiBuilder; - public ObjectTable Objects - => SObjects; - - public ClientState ClientState - => SClientState; - - public DataManager GameData - => SGameData; - public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; private readonly object? _dalamudConfig; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index a0028a6e..2b607280 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : DalamudServices.SGameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + : _edit._dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); LoadedShpkPathName = path.ToPath(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index b90d607e..29647c53 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -50,7 +50,7 @@ public partial class ModEditWindow } else { - var file = _gameData.GetFile(path); + var file = _dalamud.GameData.GetFile(path); writable = file == null ? null : new RawGameFileWritable(file); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 6ec0d3b6..514dcb02 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -41,11 +41,11 @@ public partial class ModEditWindow ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, + tex.PathInputBox(_dalamud, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); - tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", + tex.PathSelectBox(_dalamud, "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, _mod.ModPath.FullName.Length + 1); if (tex == _left) @@ -139,7 +139,7 @@ public partial class ModEditWindow using var child2 = ImRaii.Child("image"); if (child2) - _center.Draw(imageSize); + _center.Draw(_dalamud.UiBuilder, imageSize); } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 517118c9..b8a2590c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -31,7 +31,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ModEditor _editor; private readonly Configuration _config; private readonly ItemSwapTab _itemSwapTab; - private readonly DataManager _gameData; + private readonly DalamudServices _dalamud; private readonly MetaFileManager _metaFileManager; private readonly ActiveCollections _activeCollections; private readonly StainService _stainService; @@ -498,18 +498,18 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections) + StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; _activeCollections = activeCollections; - _gameData = gameData; - _fileDialog = fileDialog; + _dalamud = dalamud; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2b4ce7e4..e151faa8 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; @@ -34,10 +35,10 @@ public sealed class ModFileSystemSelector : FileSystemSelectorObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 47d6dbdc..b926c2c5 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -6,6 +6,7 @@ using Penumbra.UI.Classes; using System; using System.Linq; using System.Numerics; +using Dalamud.Game.ClientState; using Dalamud.Interface; using OtterGui.Widgets; using Penumbra.Api.Enums; @@ -29,10 +30,11 @@ public class ModsTab : ITab private readonly ActiveCollections _activeCollections; private readonly RedrawService _redrawService; private readonly Configuration _config; - private readonly CollectionCombo _collectionCombo; + private readonly CollectionCombo _collectionCombo; + private readonly ClientState _clientState; public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, - TutorialService tutorial, RedrawService redrawService, Configuration config) + TutorialService tutorial, RedrawService redrawService, Configuration config, ClientState clientState) { _modManager = modManager; _activeCollections = collectionManager.Active; @@ -41,6 +43,7 @@ public class ModsTab : ITab _tutorial = tutorial; _redrawService = redrawService; _config = config; + _clientState = clientState; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); } @@ -56,7 +59,7 @@ public class ModsTab : ITab public Mod SelectMod { set => _selector.SelectByValue(value); - } + } public void DrawContent() { @@ -148,7 +151,7 @@ public class ModsTab : ITab ImGuiUtil.HoverTooltip(lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'."); } - using var disabled = ImRaii.Disabled(DalamudServices.SClientState.LocalPlayer == null); + using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; DrawButton(buttonWidth, "All", string.Empty); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 0da6a407..029e03cd 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -10,12 +10,10 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Interop.Services; -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; +using Penumbra.UI.ModsTab; namespace Penumbra.UI.Tabs; @@ -104,7 +102,7 @@ public class SettingsTab : ITab /// 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 newName, string old, float width, bool saved, bool selected) + private bool DrawPressEnterWarning(string newName, string old, float width, bool saved, bool selected) { using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var w = new Vector2(width, 0); @@ -114,7 +112,7 @@ public class SettingsTab : ITab } /// Check a potential new root directory for validity and return the button text and whether it is valid. - private static (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) + private (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) { static bool IsSubPathOf(string basePath, string subPath) { @@ -140,14 +138,14 @@ public class SettingsTab : ITab if (IsSubPathOf(programFiles, newName) || IsSubPathOf(programFilesX86, newName)) return ("Path is not allowed to be in ProgramFiles.", false); - var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; + var dalamud = _dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; if (IsSubPathOf(dalamud.FullName, newName)) return ("Path is not allowed to be inside your Dalamud directories.", false); if (Functions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName)) return ("Path is not allowed to be inside your Downloads folder.", false); - var gameDir = DalamudServices.SGameData.GameData.DataPath.Parent!.Parent!.FullName; + var gameDir = _dalamud.GameData.GameData.DataPath.Parent!.Parent!.FullName; if (IsSubPathOf(gameDir, newName)) return ("Path is not allowed to be inside your game folder.", false); From e66d666d4dade9d70046ed994895163afd69baff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 00:35:48 +0200 Subject: [PATCH 0898/2451] Get rid of last statics. --- .../Collections/ModCollection.Cache.Access.cs | 28 +++++++-------- Penumbra/Interop/PathResolving/MetaState.cs | 36 +++++++++---------- Penumbra/Interop/PathResolving/PathState.cs | 9 +++-- .../Interop/PathResolving/ResolvePathHooks.cs | 10 +++--- Penumbra/Interop/Services/MetaList.cs | 2 +- Penumbra/Penumbra.cs | 5 +-- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 7597f7ca..a3e43afd 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -71,11 +71,11 @@ public partial class ModCollection internal SingleArray Conflicts(Mod mod) => _cache?.Conflicts(mod) ?? new SingleArray(); - public void SetFiles() + public void SetFiles(CharacterUtility utility) { if (_cache == null) { - Penumbra.CharacterUtility.ResetAll(); + utility.ResetAll(); } else { @@ -84,32 +84,32 @@ public partial class ModCollection } } - public void SetMetaFile(MetaIndex idx) + public void SetMetaFile(CharacterUtility utility, MetaIndex idx) { if (_cache == null) - Penumbra.CharacterUtility.ResetResource(idx); + utility.ResetResource(idx); else _cache.Meta.SetFile(idx); } // Used for short periods of changed files. - public MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + public MetaList.MetaReverter TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) => _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory) - ?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); + ?? utility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); - public MetaList.MetaReverter TemporarilySetEqpFile() + public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetEqpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Eqp); + ?? utility.TemporarilyResetResource(MetaIndex.Eqp); - public MetaList.MetaReverter TemporarilySetGmpFile() + public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetGmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Gmp); + ?? utility.TemporarilyResetResource(MetaIndex.Gmp); - public MetaList.MetaReverter TemporarilySetCmpFile() + public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetCmpFile() - ?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.HumanCmp); + ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); - public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) => _cache?.Meta.TemporarilySetEstFile(type) - ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); + ?? utility.TemporarilyResetResource((MetaIndex)type); } \ No newline at end of file diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 82dc3914..75985a31 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -45,18 +45,18 @@ public unsafe class MetaState : IDisposable [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; - private readonly CommunicatorService _communicator; - private readonly PerformanceTracker _performance; - private readonly CollectionResolver _collectionResolver; - private readonly ResourceService _resources; - private readonly GameEventManager _gameEventManager; - private readonly Services.CharacterUtility _characterUtility; + private readonly CommunicatorService _communicator; + private readonly PerformanceTracker _performance; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceService _resources; + private readonly GameEventManager _gameEventManager; + private readonly CharacterUtility _characterUtility; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceService resources, GameEventManager gameEventManager, Services.CharacterUtility characterUtility) + ResourceService resources, GameEventManager gameEventManager, CharacterUtility characterUtility) { _performance = performance; _communicator = communicator; @@ -90,17 +90,17 @@ public unsafe class MetaState : IDisposable return false; } - public static DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) + public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) { var races = race.Dependencies(); if (races.Length == 0) return DisposableContainer.Empty; var equipmentEnumerable = equipment - ? races.Select(r => collection.TemporarilySetEqdpFile(r, false)) + ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false)) : Array.Empty().AsEnumerable(); var accessoryEnumerable = accessory - ? races.Select(r => collection.TemporarilySetEqdpFile(r, true)) + ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true)) : Array.Empty().AsEnumerable(); return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable)); } @@ -128,7 +128,7 @@ public unsafe class MetaState : IDisposable _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); var decal = new DecalReverter(_characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); - var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); + var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); } @@ -149,7 +149,7 @@ public unsafe class MetaState : IDisposable private void OnModelLoadCompleteDetour(nint drawObject) { var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); _onModelLoadCompleteHook.Original.Invoke(drawObject); } @@ -169,7 +169,7 @@ public unsafe class MetaState : IDisposable using var performance = _performance.Measure(PerformanceType.UpdateModels); var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); _updateModelsHook.Original.Invoke(drawObject); } @@ -198,7 +198,7 @@ public unsafe class MetaState : IDisposable using var performance = _performance.Measure(PerformanceType.GetEqp); var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); + using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(_characterUtility); _getEqpIndirectHook.Original(drawObject); } @@ -214,7 +214,7 @@ public unsafe class MetaState : IDisposable { using var performance = _performance.Measure(PerformanceType.SetupVisor); var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(); + using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(_characterUtility); return _setupVisorHook.Original(drawObject, modelId, visorState); } @@ -234,7 +234,7 @@ public unsafe class MetaState : IDisposable { using var performance = _performance.Measure(PerformanceType.SetupCharacter); var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); } } @@ -252,11 +252,11 @@ public unsafe class MetaState : IDisposable using var performance = _performance.Measure(PerformanceType.ChangeCustomize); _inChangeCustomize = true; var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); using var decals = new DecalReverter(_characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); var ret = _changeCustomize.Original(human, data, skipEquipment); - _inChangeCustomize = false; + _inChangeCustomize = false; return ret; } diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index 440aed32..de9d65ee 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -5,6 +5,7 @@ using System.Threading; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData; +using Penumbra.Interop.Services; using Penumbra.String; namespace Penumbra.Interop.PathResolving; @@ -24,6 +25,8 @@ public unsafe class PathState : IDisposable private readonly nint* _monsterVTable = null!; public readonly CollectionResolver CollectionResolver; + public readonly MetaState MetaState; + public readonly CharacterUtility CharacterUtility; private readonly ResolvePathHooks _human; private readonly ResolvePathHooks _weapon; @@ -35,10 +38,12 @@ public unsafe class PathState : IDisposable public IList CurrentData => _resolveData.Values; - public PathState(CollectionResolver collectionResolver) + public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility) { SignatureHelper.Initialise(this); CollectionResolver = collectionResolver; + MetaState = metaState; + CharacterUtility = characterUtility; _human = new ResolvePathHooks(this, _humanVTable, ResolvePathHooks.Type.Human); _weapon = new ResolvePathHooks(this, _weaponVTable, ResolvePathHooks.Type.Weapon); _demiHuman = new ResolvePathHooks(this, _demiHumanVTable, ResolvePathHooks.Type.Other); @@ -59,7 +64,7 @@ public unsafe class PathState : IDisposable _monster.Dispose(); } - public bool Consume(ByteString path, out ResolveData collection) + public bool Consume(ByteString _, out ResolveData collection) { if (_resolveData.IsValueCreated) { diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index fab9fd92..609c131d 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -144,7 +144,7 @@ public unsafe class ResolvePathHooks : IDisposable var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); using var eqdp = modelType > 9 ? DisposableContainer.Empty - : MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), modelType < 5, modelType > 4); + : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), modelType < 5, modelType > 4); return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); } @@ -175,10 +175,10 @@ public unsafe class ResolvePathHooks : IDisposable private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) { data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Face), - data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Body), - data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Hair), - data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Head)); + return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Head)); } private nint ResolveDecalWeapon(nint drawObject, nint path, nint unk3, uint unk4) diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index c0c38ff4..98471c5f 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -89,7 +89,7 @@ public unsafe class MetaList : IDisposable => SetResourceInternal(_defaultResourceData, _defaultResourceSize); private void SetResourceToDefaultCollection() - => _utility.Active.Default.SetMetaFile(GlobalMetaIndex); + => _utility.Active.Default.SetMetaFile(_utility, GlobalMetaIndex); public void Dispose() { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4892f5d9..d4c867ab 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -33,8 +33,6 @@ public class Penumbra : IDalamudPlugin public static Logger Log { get; private set; } = null!; public static ChatService ChatService { get; private set; } = null!; - public static CharacterUtility CharacterUtility { get; private set; } = null!; - public readonly RedrawService RedrawService; public readonly ModFileSystem ModFileSystem; public HttpApi HttpApi = null!; @@ -64,7 +62,6 @@ public class Penumbra : IDalamudPlugin _tmp.Services.GetRequiredService(); _config = _tmp.Services.GetRequiredService(); _characterUtility = _tmp.Services.GetRequiredService(); - CharacterUtility = _characterUtility; _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); @@ -149,7 +146,7 @@ public class Penumbra : IDalamudPlugin { if (_characterUtility.Ready) { - _collectionManager.Active.Default.SetFiles(); + _collectionManager.Active.Default.SetFiles(_characterUtility); _residentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } From a94c5ae7aff342224d52f29fe6f9e8b2f32229ac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 13:30:14 +0200 Subject: [PATCH 0899/2451] Some more reworking. --- Penumbra/Api/PenumbraApi.cs | 128 ++++++++++-------- Penumbra/Penumbra.cs | 49 +++---- .../{PenumbraNew.cs => ServiceManager.cs} | 14 +- Penumbra/Services/CommunicatorService.cs | 105 +++++++++++++- Penumbra/UI/ConfigWindow.cs | 25 ++-- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 12 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 12 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 12 +- Penumbra/UI/Tabs/SettingsTab.cs | 10 +- Penumbra/UI/TutorialService.cs | 3 - Penumbra/UI/UiHelpers.cs | 9 +- Penumbra/Util/EventWrapper.cs | 3 + 12 files changed, 245 insertions(+), 137 deletions(-) rename Penumbra/{PenumbraNew.cs => ServiceManager.cs} (94%) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8d1f7a10..103178e4 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,6 +23,8 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; using Penumbra.Collections.Manager; +using Penumbra.Interop.Services; +using Penumbra.UI; namespace Penumbra.Api; @@ -31,20 +33,29 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (int, int) ApiVersion => (4, 19); - public event Action? PreSettingsPanelDraw; - public event Action? PostSettingsPanelDraw; + public event Action? PreSettingsPanelDraw + { + add => _communicator.PreSettingsPanelDraw.Subscribe(value!); + remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); + } + + public event Action? PostSettingsPanelDraw + { + add => _communicator.PostSettingsPanelDraw.Subscribe(value!); + remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); + } public event GameObjectRedrawnDelegate? GameObjectRedrawn { add { CheckInitialized(); - _penumbra!.RedrawService.GameObjectRedrawn += value; + _redrawService.GameObjectRedrawn += value; } remove { CheckInitialized(); - _penumbra!.RedrawService.GameObjectRedrawn -= value; + _redrawService.GameObjectRedrawn -= value; } } @@ -91,10 +102,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public bool Valid - => _penumbra != null; + => _lumina != null; private CommunicatorService _communicator; - private Penumbra _penumbra; private Lumina.GameData? _lumina; private ModManager _modManager; @@ -109,32 +119,39 @@ public class PenumbraApi : IDisposable, IPenumbraApi private CutsceneService _cutsceneService; private ModImportManager _modImportManager; private CollectionEditor _collectionEditor; + private RedrawService _redrawService; + private ModFileSystem _modFileSystem; + private ConfigWindow _configWindow; - public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader, + public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor) + TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, + ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, + ConfigWindow configWindow) { - _communicator = communicator; - _penumbra = penumbra; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; - _modImportManager = modImportManager; - _collectionEditor = collectionEditor; + _communicator = communicator; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _modImportManager = modImportManager; + _collectionEditor = collectionEditor; + _redrawService = redrawService; + _modFileSystem = modFileSystem; + _configWindow = configWindow; _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) ?.GetValue(_dalamud.GameData); _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); + _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, -1000); } @@ -144,11 +161,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); + _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _lumina = null; _communicator = null!; - _penumbra = null!; _modManager = null!; _resourceLoader = null!; _config = null!; @@ -161,9 +177,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi _cutsceneService = null!; _modImportManager = null!; _collectionEditor = null!; + _redrawService = null!; + _modFileSystem = null!; + _configWindow = null!; } - public event ChangedItemClick? ChangedItemClicked; + public event ChangedItemClick? ChangedItemClicked + { + add => _communicator.ChangedItemClick.Subscribe(new Action(value!)); + remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!)); + } public string GetModDirectory() { @@ -201,12 +224,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _penumbra!.EnabledChange += value; + _communicator.EnabledChanged.Subscribe(value!, int.MinValue); } remove { CheckInitialized(); - _penumbra!.EnabledChange -= value; + _communicator.EnabledChanged.Unsubscribe(value!); } } @@ -216,27 +239,32 @@ public class PenumbraApi : IDisposable, IPenumbraApi return JsonConvert.SerializeObject(_config, Formatting.Indented); } - public event ChangedItemHover? ChangedItemTooltip; + public event ChangedItemHover? ChangedItemTooltip + { + add => _communicator.ChangedItemHover.Subscribe(new Action(value!)); + remove => _communicator.ChangedItemHover.Unsubscribe(new Action(value!)); + } + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) { CheckInitialized(); - if (_penumbra!.ConfigWindow == null) + if (_configWindow == null) return PenumbraApiEc.SystemDisposed; - _penumbra!.ConfigWindow.IsOpen = true; + _configWindow.IsOpen = true; if (!Enum.IsDefined(tab)) return PenumbraApiEc.InvalidArgument; if (tab != TabType.None) - _penumbra!.ConfigWindow.SelectTab(tab); + _configWindow.SelectTab(tab); if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { if (_modManager.TryGetMod(modDirectory, modName, out var mod)) - _penumbra!.ConfigWindow.SelectMod(mod); + _configWindow.SelectMod(mod); else return PenumbraApiEc.ModMissing; } @@ -247,34 +275,34 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void CloseMainWindow() { CheckInitialized(); - if (_penumbra!.ConfigWindow == null) + if (_configWindow == null) return; - _penumbra!.ConfigWindow.IsOpen = false; + _configWindow.IsOpen = false; } public void RedrawObject(int tableIndex, RedrawType setting) { CheckInitialized(); - _penumbra!.RedrawService.RedrawObject(tableIndex, setting); + _redrawService.RedrawObject(tableIndex, setting); } public void RedrawObject(string name, RedrawType setting) { CheckInitialized(); - _penumbra!.RedrawService.RedrawObject(name, setting); + _redrawService.RedrawObject(name, setting); } public void RedrawObject(GameObject? gameObject, RedrawType setting) { CheckInitialized(); - _penumbra!.RedrawService.RedrawObject(gameObject, setting); + _redrawService.RedrawObject(gameObject, setting); } public void RedrawAll(RedrawType setting) { CheckInitialized(); - _penumbra!.RedrawService.RedrawAll(setting); + _redrawService.RedrawAll(setting); } public string ResolveDefaultPath(string path) @@ -657,7 +685,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.FindLeaf(mod, out var leaf)) return (PenumbraApiEc.ModMissing, string.Empty, false); var fullPath = leaf.FullName(); @@ -672,12 +700,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidArgument; if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.FindLeaf(mod, out var leaf)) return PenumbraApiEc.ModMissing; try { - _penumbra.ModFileSystem.RenameAndMove(leaf, newPath); + _modFileSystem.RenameAndMove(leaf, newPath); return PenumbraApiEc.Success; } catch @@ -986,16 +1014,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } - internal bool HasTooltip - => ChangedItemTooltip != null; - - internal void InvokeTooltip(object? it) - => ChangedItemTooltip?.Invoke(it); - - internal void InvokeClick(MouseButton button, object? it) - => ChangedItemClicked?.Invoke(button, it); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void CheckInitialized() { @@ -1117,12 +1135,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi return true; } - public void InvokePreSettingsPanel(string modDirectory) - => PreSettingsPanelDraw?.Invoke(modDirectory); - - public void InvokePostSettingsPanel(string modDirectory) - => PostSettingsPanelDraw?.Invoke(modDirectory); - // TODO: replace all usages with ActorIdentifier stuff when incrementing API private ActorIdentifier NameToIdentifier(string name, ushort worldId) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d4c867ab..96e3b937 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using System.Text; @@ -11,7 +10,6 @@ using OtterGui; using OtterGui.Log; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -21,7 +19,7 @@ using Penumbra.Services; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; -using Penumbra.Mods; +using Penumbra.UI.Tabs; namespace Penumbra; @@ -30,13 +28,8 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static Logger Log { get; private set; } = null!; - public static ChatService ChatService { get; private set; } = null!; - - public readonly RedrawService RedrawService; - public readonly ModFileSystem ModFileSystem; - public HttpApi HttpApi = null!; - internal ConfigWindow? ConfigWindow { get; private set; } + public static readonly Logger Log = new(); + public static ChatService ChatService { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -46,30 +39,31 @@ public class Penumbra : IDalamudPlugin private readonly CollectionManager _collectionManager; private readonly Configuration _config; private readonly CharacterUtility _characterUtility; + private readonly RedrawService _redrawService; + private readonly CommunicatorService _communicatorService; private PenumbraWindowSystem? _windowSystem; private bool _disposed; - private readonly PenumbraNew _tmp; + private readonly ServiceManager _tmp; public Penumbra(DalamudPluginInterface pluginInterface) { - Log = PenumbraNew.Log; try { - _tmp = new PenumbraNew(this, pluginInterface); + _tmp = new ServiceManager(this, pluginInterface, Log); ChatService = _tmp.Services.GetRequiredService(); _validityChecker = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. _config = _tmp.Services.GetRequiredService(); _characterUtility = _tmp.Services.GetRequiredService(); _tempMods = _tmp.Services.GetRequiredService(); _residentResources = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); - _modManager = _tmp.Services.GetRequiredService(); - _collectionManager = _tmp.Services.GetRequiredService(); - _tempCollections = _tmp.Services.GetRequiredService(); - ModFileSystem = _tmp.Services.GetRequiredService(); - RedrawService = _tmp.Services.GetRequiredService(); + _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. + _modManager = _tmp.Services.GetRequiredService(); + _collectionManager = _tmp.Services.GetRequiredService(); + _tempCollections = _tmp.Services.GetRequiredService(); + _redrawService = _tmp.Services.GetRequiredService(); + _communicatorService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) @@ -100,7 +94,6 @@ public class Penumbra : IDalamudPlugin { using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); var api = _tmp.Services.GetRequiredService(); - HttpApi = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); api.ChangedItemTooltip += it => { @@ -120,22 +113,16 @@ public class Penumbra : IDalamudPlugin { using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); var system = _tmp.Services.GetRequiredService(); + system.Window.Setup(this, _tmp.Services.GetRequiredService()); _tmp.Services.GetRequiredService(); if (!_disposed) - { _windowSystem = system; - ConfigWindow = system.Window; - } else - { system.Dispose(); - } } ); } - public event Action? EnabledChange; - public bool SetEnabled(bool enabled) { if (enabled == _config.EnableMods) @@ -148,7 +135,7 @@ public class Penumbra : IDalamudPlugin { _collectionManager.Active.Default.SetFiles(_characterUtility); _residentResources.Reload(); - RedrawService.RedrawAll(RedrawType.Redraw); + _redrawService.RedrawAll(RedrawType.Redraw); } } else @@ -157,12 +144,12 @@ public class Penumbra : IDalamudPlugin { _characterUtility.ResetAll(); _residentResources.Reload(); - RedrawService.RedrawAll(RedrawType.Redraw); + _redrawService.RedrawAll(RedrawType.Redraw); } } _config.Save(); - EnabledChange?.Invoke(enabled); + _communicatorService.EnabledChanged.Invoke(enabled); return true; } diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/ServiceManager.cs similarity index 94% rename from Penumbra/PenumbraNew.cs rename to Penumbra/ServiceManager.cs index 5cd501af..af2cec75 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/ServiceManager.cs @@ -1,10 +1,8 @@ -using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; -using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; @@ -27,22 +25,18 @@ using Penumbra.Meta; namespace Penumbra; -public class PenumbraNew +public class ServiceManager { - public string Name - => "Penumbra"; + public readonly ServiceProvider Services; - public static readonly Logger Log = new(); - public readonly ServiceProvider Services; - - public PenumbraNew(Penumbra penumbra, DalamudPluginInterface pi) + public ServiceManager(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { var startTimer = new StartTracker(); using var time = startTimer.Measure(StartTimeType.Total); var services = new ServiceCollection(); // Add meta services. - services.AddSingleton(Log) + services.AddSingleton(log) .AddSingleton(startTimer) .AddSingleton() .AddSingleton() diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index f3a9a389..a45548c7 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.IO; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -211,6 +210,90 @@ public sealed class CollectionInheritanceChanged : EventWrapper Invoke(this, collection, inherited); } +/// +/// Triggered when the general Enabled state of Penumbra is changed. +/// +/// Parameter is whether Penumbra is now Enabled (true) or Disabled (false). +/// +/// +public sealed class EnabledChanged : EventWrapper> +{ + public EnabledChanged() + : base(nameof(EnabledChanged)) + { } + + public void Invoke(bool enabled) + => Invoke(this, enabled); +} + +/// +/// Triggered before the settings panel is drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// +/// +public sealed class PreSettingsPanelDraw : EventWrapper> +{ + public PreSettingsPanelDraw() + : base(nameof(PreSettingsPanelDraw)) + { } + + public void Invoke(string modDirectory) + => Invoke(this, modDirectory); +} + +/// +/// Triggered after the settings panel is drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// +/// +public sealed class PostSettingsPanelDraw : EventWrapper> +{ + public PostSettingsPanelDraw() + : base(nameof(PostSettingsPanelDraw)) + { } + + public void Invoke(string modDirectory) + => Invoke(this, modDirectory); +} + +/// +/// Triggered when a Changed Item in Penumbra is hovered. +/// +/// Parameter is the hovered object data if any. +/// +/// +public sealed class ChangedItemHover : EventWrapper> +{ + public ChangedItemHover() + : base(nameof(ChangedItemHover)) + { } + + public void Invoke(object? data) + => Invoke(this, data); + + public bool HasTooltip + => HasSubscribers; +} + +/// +/// Triggered when a Changed Item in Penumbra is clicked. +/// +/// Parameter is the clicked mouse button. +/// Parameter is the clicked object data if any.. +/// +/// +public sealed class ChangedItemClick : EventWrapper> +{ + public ChangedItemClick() + : base(nameof(ChangedItemClick)) + { } + + public void Invoke(MouseButton button, object? data) + => Invoke(this, button, data); +} + public class CommunicatorService : IDisposable { /// @@ -249,6 +332,21 @@ public class CommunicatorService : IDisposable /// public readonly CollectionInheritanceChanged CollectionInheritanceChanged = new(); + /// + public readonly EnabledChanged EnabledChanged = new(); + + /// + public readonly PreSettingsPanelDraw PreSettingsPanelDraw = new(); + + /// + public readonly PostSettingsPanelDraw PostSettingsPanelDraw = new(); + + /// + public readonly ChangedItemHover ChangedItemHover = new(); + + /// + public readonly ChangedItemClick ChangedItemClick = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -263,5 +361,10 @@ public class CommunicatorService : IDisposable ModPathChanged.Dispose(); ModSettingChanged.Dispose(); CollectionInheritanceChanged.Dispose(); + EnabledChanged.Dispose(); + PreSettingsPanelDraw.Dispose(); + PostSettingsPanelDraw.Dispose(); + ChangedItemHover.Dispose(); + ChangedItemClick.Dispose(); } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 1a02284e..d676cf62 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -9,7 +9,8 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; -using Penumbra.Util; +using Penumbra.Util; + namespace Penumbra.UI; public sealed class ConfigWindow : Window @@ -18,8 +19,8 @@ public sealed class ConfigWindow : Window private readonly Configuration _config; private readonly PerformanceTracker _tracker; private readonly ValidityChecker _validityChecker; - private readonly Penumbra _penumbra; - private readonly ConfigTabBar _configTabs; + private Penumbra? _penumbra; + private ConfigTabBar _configTabs = null!; private string? _lastException; public void SelectTab(TabType tab) @@ -31,15 +32,13 @@ public sealed class ConfigWindow : Window public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, - TutorialService tutorial, Penumbra penumbra, ConfigTabBar configTabs) + TutorialService tutorial) : base(GetLabel(checker)) { _pluginInterface = pi; _config = config; _tracker = tracker; _validityChecker = checker; - _penumbra = penumbra; - _configTabs = configTabs; RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() @@ -51,6 +50,15 @@ public sealed class ConfigWindow : Window IsOpen = _config.DebugMode; } + public void Setup(Penumbra penumbra, ConfigTabBar configTabs) + { + _penumbra = penumbra; + _configTabs = configTabs; + } + + public override bool DrawConditions() + => _penumbra != null; + public override void PreDraw() { if (_config.FixMainWindow) @@ -61,7 +69,7 @@ public sealed class ConfigWindow : Window public override void Draw() { - using var timer = _tracker.Measure(PerformanceType.UiMainWindow); + using var timer = _tracker.Measure(PerformanceType.UiMainWindow); UiHelpers.SetupCommonSizes(); try { @@ -111,6 +119,7 @@ public sealed class ConfigWindow : Window var text = e.ToString(); if (text == _lastException) return; + _lastException = text; } else @@ -139,7 +148,7 @@ public sealed class ConfigWindow : Window ImGui.NewLine(); UiHelpers.DrawDiscordButton(0); ImGui.SameLine(); - UiHelpers.DrawSupportButton(_penumbra); + UiHelpers.DrawSupportButton(_penumbra!); ImGui.NewLine(); ImGui.NewLine(); } diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 66d53f6c..ae500b1d 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -7,22 +7,22 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.UI.ModsTab; public class ModPanelChangedItemsTab : ITab { private readonly ModFileSystemSelector _selector; - private readonly PenumbraApi _api; + private readonly CommunicatorService _communicator; public ReadOnlySpan Label => "Changed Items"u8; - public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector) + public ModPanelChangedItemsTab(ModFileSystemSelector selector, CommunicatorService communicator) { - _api = api; - _selector = selector; + _selector = selector; + _communicator = communicator; } public bool IsVisible @@ -36,6 +36,6 @@ public class ModPanelChangedItemsTab : ITab var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_api, kvp.Item1, kvp.Item2, true), height); + ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_communicator, kvp.Item1, kvp.Item2, true), height); } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index e6c66fbd..8d831279 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -5,7 +5,6 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Widgets; -using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; @@ -14,16 +13,17 @@ using Dalamud.Interface.Components; using Dalamud.Interface; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab : ITab { private readonly Configuration _config; + private readonly CommunicatorService _communicator; private readonly CollectionManager _collectionManager; private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; - private readonly PenumbraApi _api; private readonly ModManager _modManager; private bool _inherited; @@ -33,13 +33,13 @@ public class ModPanelSettingsTab : ITab private int? _currentPriority = null; public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, - TutorialService tutorial, PenumbraApi api, Configuration config) + TutorialService tutorial, CommunicatorService communicator, Configuration config) { _collectionManager = collectionManager; + _communicator = communicator; _modManager = modManager; _selector = selector; _tutorial = tutorial; - _api = api; _config = config; } @@ -65,7 +65,7 @@ public class ModPanelSettingsTab : ITab DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - _api.InvokePreSettingsPanel(_selector.Selected!.ModPath.Name); + _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.ModPath.Name); DrawEnabledInput(); _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); @@ -102,7 +102,7 @@ public class ModPanelSettingsTab : ITab } UiHelpers.DefaultLineSpace(); - _api.InvokePostSettingsPanel(_selector.Selected!.ModPath.Name); + _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.ModPath.Name); } /// Draw a big red bar if the current setting is inherited. diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 6f36af56..92ce322d 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -7,22 +7,22 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Api; using Penumbra.Collections.Manager; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { - private readonly CollectionManager _collectionManager; - private readonly PenumbraApi _api; + private readonly CollectionManager _collectionManager; + private readonly CommunicatorService _communicator; - public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api) + public ChangedItemsTab(CollectionManager collectionManager, CommunicatorService communicator) { _collectionManager = collectionManager; - _api = api; + _communicator = communicator; } public ReadOnlySpan Label @@ -81,7 +81,7 @@ public class ChangedItemsTab : ITab private void DrawChangedItemColumn(KeyValuePair, object?)> item) { ImGui.TableNextColumn(); - UiHelpers.DrawChangedItem(_api, item.Key, item.Value.Item2, false); + UiHelpers.DrawChangedItem(_communicator, item.Key, item.Value.Item2, false); ImGui.TableNextColumn(); if (item.Value.Item1.Count > 0) { diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 029e03cd..6983afb7 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -9,6 +9,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Api; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -35,10 +36,11 @@ public class SettingsTab : ITab private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly DalamudServices _dalamud; + private readonly HttpApi _httpApi; public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager) + ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi) { _config = config; _fontReloader = fontReloader; @@ -51,6 +53,7 @@ public class SettingsTab : ITab _residentResources = residentResources; _dalamud = dalamud; _modExportManager = modExportManager; + _httpApi = httpApi; } public void DrawHeader() @@ -630,7 +633,6 @@ public class SettingsTab : ITab private void DrawAdvancedSettings() { var header = ImGui.CollapsingHeader("Advanced"); - _tutorial.OpenTutorial(BasicTutorialSteps.Deprecated1); if (!header) return; @@ -657,9 +659,9 @@ public class SettingsTab : ITab if (ImGui.Checkbox("##http", ref http)) { if (http) - _penumbra.HttpApi.CreateWebServer(); + _httpApi.CreateWebServer(); else - _penumbra.HttpApi.ShutdownWebServer(); + _httpApi.ShutdownWebServer(); _config.EnableHttpApi = http; _config.Save(); diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 9671abde..7f9b2ce3 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -47,10 +47,7 @@ public class TutorialService public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; public const string InterfaceCollection = "Interface Collection"; - public const string ActiveCollections = "Active Collections"; public const string AssignedCollections = "Assigned Collections"; - public const string GroupAssignment = "Group Assignment"; - public const string IndividualAssignments = "Individual Assignments"; public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" + " - 'self' or '': your own character\n" diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 7cd1c120..944223f0 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -13,6 +13,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; @@ -60,7 +61,7 @@ public static class UiHelpers /// Draw a changed item, invoking the Api-Events for clicks and tooltips. /// Also draw the item Id in grey if requested. /// - public static void DrawChangedItem(PenumbraApi api, string name, object? data, bool drawId) + public static void DrawChangedItem(CommunicatorService communicator, string name, object? data, bool drawId) { name = ChangedItemName(name, data); var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; @@ -68,15 +69,15 @@ public static class UiHelpers ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - api.InvokeClick(ret, data); + communicator.ChangedItemClick.Invoke(ret, data); - if (api.HasTooltip && ImGui.IsItemHovered()) + if (communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) { // We can not be sure that any subscriber actually prints something in any case. // Circumvent ugly blank tooltip with less-ugly useless tooltip. using var tt = ImRaii.Tooltip(); using var group = ImRaii.Group(); - api.InvokeTooltip(data); + communicator.ChangedItemHover.Invoke(data); group.Dispose(); if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs index cc374df1..2d4bb318 100644 --- a/Penumbra/Util/EventWrapper.cs +++ b/Penumbra/Util/EventWrapper.cs @@ -9,6 +9,9 @@ public abstract class EventWrapper : IDisposable where T : Delegate private readonly string _name; private readonly List<(object Subscriber, int Priority)> _event = new(); + public bool HasSubscribers + => _event.Count > 0; + protected EventWrapper(string name) => _name = name; From ce03fb59c82a8288a9ded61ca2a6ea4b10569e2d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 14:40:56 +0200 Subject: [PATCH 0900/2451] Some more shuffling around. --- Penumbra/Api/PenumbraApi.cs | 5 +- .../Collections/Manager/ActiveCollections.cs | 4 +- .../Manager/IndividualCollections.Access.cs | 32 ++--- .../Manager/IndividualCollections.Files.cs | 36 ++++-- .../Manager/IndividualCollections.cs | 34 +++--- Penumbra/Penumbra.cs | 58 ++++----- Penumbra/{ => Services}/ServiceManager.cs | 110 +++++++++--------- Penumbra/Services/ValidityChecker.cs | 5 +- Penumbra/UI/ConfigWindow.cs | 1 + 9 files changed, 147 insertions(+), 138 deletions(-) rename Penumbra/{ => Services}/ServiceManager.cs (65%) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 103178e4..9f7910ee 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -145,10 +145,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _redrawService = redrawService; _modFileSystem = modFileSystem; _configWindow = configWindow; - - _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() - .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) - ?.GetValue(_dalamud.GameData); + _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index d8bbae4f..a78cc48b 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -43,7 +43,7 @@ public class ActiveCollections : ISavable, IDisposable Current = storage.DefaultNamed; Default = storage.DefaultNamed; Interface = storage.DefaultNamed; - Individuals = new IndividualCollections(actors.AwaitedService, config); + Individuals = new IndividualCollections(actors, config); _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); LoadCollections(); UpdateCurrentCollectionInUse(); @@ -396,7 +396,7 @@ public class ActiveCollections : ISavable, IDisposable } configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); - configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, _storage); + configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage); // Save any changes. if (configChanged) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index b81e72c1..93c555b3 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -40,7 +40,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return true; if (identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && _config.UseOwnerNameForCharacterCollection) - return CheckWorlds(_actorManager.GetCurrentPlayer(), out collection); + return CheckWorlds(_actorService.AwaitedService.GetCurrentPlayer(), out collection); break; } @@ -50,7 +50,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return true; // Handle generic NPC - var npcIdentifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, + var npcIdentifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId); if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) return true; @@ -59,7 +59,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (!_config.UseOwnerNameForCharacterCollection) return false; - identifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, + identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); } @@ -91,37 +91,37 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (identifier.Type != IdentifierType.Special) return (identifier, SpecialResult.Invalid); - if (_actorManager.ResolvePartyBannerPlayer(identifier.Special, out var id)) + if (_actorService.AwaitedService.ResolvePartyBannerPlayer(identifier.Special, out var id)) return (id, SpecialResult.PartyBanner); - if (_actorManager.ResolvePvPBannerPlayer(identifier.Special, out id)) + if (_actorService.AwaitedService.ResolvePvPBannerPlayer(identifier.Special, out id)) return (id, SpecialResult.PvPBanner); - if (_actorManager.ResolveMahjongPlayer(identifier.Special, out id)) + if (_actorService.AwaitedService.ResolveMahjongPlayer(identifier.Special, out id)) return (id, SpecialResult.Mahjong); switch (identifier.Special) { case ScreenActor.CharacterScreen when _config.UseCharacterCollectionInMainWindow: - return (_actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen); + return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.CharacterScreen); case ScreenActor.FittingRoom when _config.UseCharacterCollectionInTryOn: - return (_actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom); + return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.FittingRoom); case ScreenActor.DyePreview when _config.UseCharacterCollectionInTryOn: - return (_actorManager.GetCurrentPlayer(), SpecialResult.DyePreview); + return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.DyePreview); case ScreenActor.Portrait when _config.UseCharacterCollectionsInCards: - return (_actorManager.GetCurrentPlayer(), SpecialResult.Portrait); + return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.Portrait); case ScreenActor.ExamineScreen: { - identifier = _actorManager.GetInspectPlayer(); + identifier = _actorService.AwaitedService.GetInspectPlayer(); if (identifier.IsValid) return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect); - identifier = _actorManager.GetCardPlayer(); + identifier = _actorService.AwaitedService.GetCardPlayer(); if (identifier.IsValid) return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card); return _config.UseCharacterCollectionInTryOn - ? (_actorManager.GetGlamourPlayer(), SpecialResult.Glamour) + ? (_actorService.AwaitedService.GetGlamourPlayer(), SpecialResult.Glamour) : (identifier, SpecialResult.Invalid); } default: return (identifier, SpecialResult.Invalid); @@ -129,10 +129,10 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa } public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection) - => TryGetCollection(_actorManager.FromObject(gameObject, true, false, false), out collection); + => TryGetCollection(_actorService.AwaitedService.FromObject(gameObject, true, false, false), out collection); public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection) - => TryGetCollection(_actorManager.FromObject(gameObject, out _, true, false, false), out collection); + => TryGetCollection(_actorService.AwaitedService.FromObject(gameObject, out _, true, false, false), out collection); private bool CheckWorlds(ActorIdentifier identifier, out ModCollection? collection) { @@ -145,7 +145,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (_individuals.TryGetValue(identifier, out collection)) return true; - identifier = _actorManager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, + identifier = _actorService.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId); if (identifier.IsValid && _individuals.TryGetValue(identifier, out collection)) return true; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 8adf03fa..b720c35c 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; using Penumbra.String; +using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -25,7 +26,21 @@ public partial class IndividualCollections return ret; } - public bool ReadJObject(JArray? obj, CollectionStorage storage) + public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage) + { + if (_actorService.Valid) + return ReadJObjectInternal(obj, storage); + void Func() + { + if (ReadJObjectInternal(obj, storage)) + saver.ImmediateSave(parent); + _actorService.FinishedCreation -= Func; + } + _actorService.FinishedCreation += Func; + return false; + } + + private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage) { if (obj == null) return true; @@ -35,7 +50,7 @@ public partial class IndividualCollections { try { - var identifier = _actorManager.FromJson(data as JObject); + var identifier = _actorService.AwaitedService.FromJson(data as JObject); var group = GetGroup(identifier); if (group.Length == 0 || group.Any(i => !i.IsValid)) { @@ -71,7 +86,6 @@ public partial class IndividualCollections NotificationType.Error); } } - return changes; } @@ -90,22 +104,22 @@ public partial class IndividualCollections var kind = ObjectKind.None; var lowerName = name.ToLowerInvariant(); // Prefer matching NPC names, fewer false positives than preferring players. - if (FindDataId(lowerName, _actorManager.Data.Companions, out var dataId)) + if (FindDataId(lowerName, _actorService.AwaitedService.Data.Companions, out var dataId)) kind = ObjectKind.Companion; - else if (FindDataId(lowerName, _actorManager.Data.Mounts, out dataId)) + else if (FindDataId(lowerName, _actorService.AwaitedService.Data.Mounts, out dataId)) kind = ObjectKind.MountType; - else if (FindDataId(lowerName, _actorManager.Data.BNpcs, out dataId)) + else if (FindDataId(lowerName, _actorService.AwaitedService.Data.BNpcs, out dataId)) kind = ObjectKind.BattleNpc; - else if (FindDataId(lowerName, _actorManager.Data.ENpcs, out dataId)) + else if (FindDataId(lowerName, _actorService.AwaitedService.Data.ENpcs, out dataId)) kind = ObjectKind.EventNpc; - var identifier = _actorManager.CreateNpc(kind, dataId); + var identifier = _actorService.AwaitedService.CreateNpc(kind, dataId); if (identifier.IsValid) { // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. var group = GetGroup(identifier); var ids = string.Join(", ", group.Select(i => i.DataId.ToString())); - if (Add($"{_actorManager.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) + if (Add($"{_actorService.AwaitedService.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else Penumbra.ChatService.NotificationMessage( @@ -115,10 +129,10 @@ public partial class IndividualCollections // If it is not a valid NPC name, check if it can be a player name. else if (ActorManager.VerifyPlayerName(name)) { - identifier = _actorManager.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); + identifier = _actorService.AwaitedService.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); // Try to migrate the player name without logging full names. - if (Add($"{name} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", new[] + if (Add($"{name} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})", new[] { identifier, }, collection)) diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 80fa6089..f333ed81 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -13,24 +13,17 @@ namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections { private readonly Configuration _config; - private readonly ActorManager _actorManager; + private readonly ActorService _actorService; private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); private readonly Dictionary _individuals = new(); public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments => _assignments; - // TODO - public IndividualCollections(ActorService actorManager, Configuration config) + public IndividualCollections(ActorService actorService, Configuration config) { _config = config; - _actorManager = actorManager.AwaitedService; - } - - public IndividualCollections(ActorManager actorManager, Configuration config) - { - _actorManager = actorManager; - _config = config; + _actorService = actorService; } public enum AddResult @@ -78,6 +71,7 @@ public sealed partial class IndividualCollections { identifiers = Array.Empty(); + var manager = _actorService.AwaitedService; switch (type) { case IdentifierType.Player: @@ -86,7 +80,7 @@ public sealed partial class IndividualCollections identifiers = new[] { - _actorManager.CreatePlayer(playerName, homeWorld), + manager.CreatePlayer(playerName, homeWorld), }; break; case IdentifierType.Retainer: @@ -95,18 +89,18 @@ public sealed partial class IndividualCollections identifiers = new[] { - _actorManager.CreateRetainer(retainerName, 0), + manager.CreateRetainer(retainerName, 0), }; break; case IdentifierType.Owned: if (!ByteString.FromString(name, out var ownerName)) return AddResult.Invalid; - - identifiers = dataIds.Select(id => _actorManager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); + + identifiers = dataIds.Select(id => manager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); break; case IdentifierType.Npc: identifiers = dataIds - .Select(id => _actorManager.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray(); + .Select(id => manager.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray(); break; default: identifiers = Array.Empty(); @@ -152,8 +146,8 @@ public sealed partial class IndividualCollections { identifier.CreatePermanent(), }, - IdentifierType.Owned => CreateNpcs(_actorManager, identifier.CreatePermanent()), - IdentifierType.Npc => CreateNpcs(_actorManager, identifier), + IdentifierType.Owned => CreateNpcs(_actorService.AwaitedService, identifier.CreatePermanent()), + IdentifierType.Npc => CreateNpcs(_actorService.AwaitedService, identifier), _ => Array.Empty(), }; } @@ -244,11 +238,11 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => - $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})'s {_actorManager.Data.ToName(identifier.Kind, identifier.DataId)}", - IdentifierType.Npc => $"{_actorManager.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", + $"{identifier.PlayerName} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})'s {_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)}", + IdentifierType.Npc => $"{_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", _ => string.Empty, }; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 96e3b937..997ff784 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -44,31 +44,33 @@ public class Penumbra : IDalamudPlugin private PenumbraWindowSystem? _windowSystem; private bool _disposed; - private readonly ServiceManager _tmp; + private readonly ServiceProvider _services; public Penumbra(DalamudPluginInterface pluginInterface) { try { - _tmp = new ServiceManager(this, pluginInterface, Log); - ChatService = _tmp.Services.GetRequiredService(); - _validityChecker = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. - _config = _tmp.Services.GetRequiredService(); - _characterUtility = _tmp.Services.GetRequiredService(); - _tempMods = _tmp.Services.GetRequiredService(); - _residentResources = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. - _modManager = _tmp.Services.GetRequiredService(); - _collectionManager = _tmp.Services.GetRequiredService(); - _tempCollections = _tmp.Services.GetRequiredService(); - _redrawService = _tmp.Services.GetRequiredService(); - _communicatorService = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. - _tmp.Services.GetRequiredService(); // Initialize because not required anywhere else. - using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) + var startTimer = new StartTracker(); + using var timer = startTimer.Measure(StartTimeType.Total); + _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); + ChatService = _services.GetRequiredService(); + _validityChecker = _services.GetRequiredService(); + _services.GetRequiredService(); // Initialize because not required anywhere else. + _config = _services.GetRequiredService(); + _characterUtility = _services.GetRequiredService(); + _tempMods = _services.GetRequiredService(); + _residentResources = _services.GetRequiredService(); + _services.GetRequiredService(); // Initialize because not required anywhere else. + _modManager = _services.GetRequiredService(); + _collectionManager = _services.GetRequiredService(); + _tempCollections = _services.GetRequiredService(); + _redrawService = _services.GetRequiredService(); + _communicatorService = _services.GetRequiredService(); + _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. + using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { - _tmp.Services.GetRequiredService(); + _services.GetRequiredService(); } SetupInterface(); @@ -92,9 +94,9 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { - using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); - var api = _tmp.Services.GetRequiredService(); - _tmp.Services.GetRequiredService(); + using var timer = _services.GetRequiredService().Measure(StartTimeType.Api); + var api = _services.GetRequiredService(); + _services.GetRequiredService(); api.ChangedItemTooltip += it => { if (it is Item) @@ -111,10 +113,10 @@ public class Penumbra : IDalamudPlugin { Task.Run(() => { - using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); - var system = _tmp.Services.GetRequiredService(); - system.Window.Setup(this, _tmp.Services.GetRequiredService()); - _tmp.Services.GetRequiredService(); + using var tInterface = _services.GetRequiredService().Measure(StartTimeType.Interface); + var system = _services.GetRequiredService(); + system.Window.Setup(this, _services.GetRequiredService()); + _services.GetRequiredService(); if (!_disposed) _windowSystem = system; else @@ -162,7 +164,7 @@ public class Penumbra : IDalamudPlugin if (_disposed) return; - _tmp?.Dispose(); + _services?.Dispose(); _disposed = true; } @@ -183,7 +185,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append( - $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); + $"> **`Synchronous Load (Dalamud): `** {(_services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); sb.Append( $"> **`Logging: `** Log: {_config.EnableResourceLogging}, Watcher: {_config.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); diff --git a/Penumbra/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs similarity index 65% rename from Penumbra/ServiceManager.cs rename to Penumbra/Services/ServiceManager.cs index af2cec75..e973c784 100644 --- a/Penumbra/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -3,42 +3,58 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; +using Penumbra.Meta; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; using Penumbra.Util; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; -using Penumbra.Mods.Manager; -using Penumbra.Collections.Cache; -using Penumbra.Meta; - -namespace Penumbra; -public class ServiceManager +namespace Penumbra.Services; + +public static class ServiceManager { - public readonly ServiceProvider Services; - - public ServiceManager(Penumbra penumbra, DalamudPluginInterface pi, Logger log) + public static ServiceProvider CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log, StartTracker startTimer) { - var startTimer = new StartTracker(); - using var time = startTimer.Measure(StartTimeType.Total); - - var services = new ServiceCollection(); - // Add meta services. - services.AddSingleton(log) + var services = new ServiceCollection() + .AddSingleton(log) .AddSingleton(startTimer) - .AddSingleton() + .AddSingleton(penumbra) + .AddDalamud(pi) + .AddMeta() + .AddGameData() + .AddInterop() + .AddConfiguration() + .AddCollections() + .AddMods() + .AddResources() + .AddResolvers() + .AddInterface() + .AddModEditor() + .AddApi(); + + return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + } + + private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi) + { + var dalamud = new DalamudServices(pi); + dalamud.AddServices(services); + return services; + } + + private static IServiceCollection AddMeta(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -46,20 +62,16 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add Dalamud services - var dalamud = new DalamudServices(pi); - dalamud.AddServices(services); - services.AddSingleton(penumbra); - // Add Game Data - services.AddSingleton() + private static IServiceCollection AddGameData(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); - // Add Game Services - services.AddSingleton() + private static IServiceCollection AddInterop(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -72,12 +84,12 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add Configuration - services.AddTransient() + private static IServiceCollection AddConfiguration(this IServiceCollection services) + => services.AddTransient() .AddSingleton(); - // Add Collection Services - services.AddSingleton() + private static IServiceCollection AddCollections(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -86,8 +98,8 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add Mod Services - services.AddSingleton() + private static IServiceCollection AddMods(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -96,16 +108,16 @@ public class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(s => (ModStorage) s.GetRequiredService()); + .AddSingleton(s => (ModStorage)s.GetRequiredService()); - // Add Resource services - services.AddSingleton() + private static IServiceCollection AddResources(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); - // Add Path Resolver - services.AddSingleton() + private static IServiceCollection AddResolvers(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -115,8 +127,8 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add Interface - services.AddSingleton() + private static IServiceCollection AddInterface(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -147,8 +159,8 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add Mod Editor - services.AddSingleton() + private static IServiceCollection AddModEditor(this IServiceCollection services) + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -157,17 +169,9 @@ public class ServiceManager .AddSingleton() .AddSingleton(); - // Add API - services.AddSingleton() + private static IServiceCollection AddApi(this IServiceCollection services) + => services.AddSingleton() .AddSingleton(x => x.GetRequiredService()) .AddSingleton() .AddSingleton(); - - Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); - } - - public void Dispose() - { - Services.Dispose(); - } } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index a5bc3f3c..7ce8c803 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; -using Penumbra.Util; -namespace Penumbra; +namespace Penumbra.Services; public class ValidityChecker { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index d676cf62..12144957 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -7,6 +7,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; using Penumbra.Util; From fd3a066aee423a43b5b16fba92640ae26010c635 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 21:55:31 +0200 Subject: [PATCH 0901/2451] Some more touches. --- OtterGui | 2 +- .../Manager/IndividualCollections.Files.cs | 1 + .../Manager/ModCollectionMigration.cs | 1 + Penumbra/Collections/ModCollection.cs | 5 +- Penumbra/Configuration.cs | 1 - .../PathResolving/AnimationHookService.cs | 11 +- Penumbra/Mods/Editor/DuplicateManager.cs | 1 + Penumbra/Mods/Manager/ModMigration.cs | 1 + Penumbra/Mods/ModCreator.cs | 1 + Penumbra/Mods/TemporaryMod.cs | 1 + Penumbra/Services/ChatService.cs | 47 ++++++++ Penumbra/{Util => Services}/SaveService.cs | 3 +- Penumbra/Services/ServiceManager.cs | 1 - Penumbra/Services/ServiceWrapper.cs | 11 +- Penumbra/Services/StainService.cs | 1 - Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 - Penumbra/Util/ChatService.cs | 109 ------------------ 18 files changed, 64 insertions(+), 136 deletions(-) create mode 100644 Penumbra/Services/ChatService.cs rename Penumbra/{Util => Services}/SaveService.cs (95%) delete mode 100644 Penumbra/Util/ChatService.cs diff --git a/OtterGui b/OtterGui index ee7815a4..99bd0a88 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ee7815a4f4c91ec04a240c9e52733f2f5ffa15d0 +Subproject commit 99bd0a889806f0560109686ca3b29a4edfa48f76 diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index b720c35c..97e22bb2 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; +using Penumbra.Services; using Penumbra.String; using Penumbra.Util; diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index c0ddd0e4..c1f158ea 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -2,6 +2,7 @@ using Penumbra.Mods; using System.Collections.Generic; using System.Linq; using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index a2b201cd..32e7b0b3 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -5,8 +5,9 @@ using System.Diagnostics; using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Util; -using Penumbra.Collections.Manager; - +using Penumbra.Collections.Manager; +using Penumbra.Services; + namespace Penumbra.Collections; /// diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 3799d9db..cc10e3a7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -16,7 +16,6 @@ using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; -using Penumbra.Util; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index e3b06de3..2add5771 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -225,12 +225,9 @@ public unsafe class AnimationHookService : IDisposable } var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); -#if DEBUG - var path = new ByteString(vfxPath); - Penumbra.Log.Verbose( - $"Load Character VFX: {path} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> " + Penumbra.Log.Excessive( + $"Load Character VFX: {new ByteString(vfxPath)} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> " + $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); -#endif _animationLoadData.Value = last; return ret; } @@ -250,11 +247,9 @@ public unsafe class AnimationHookService : IDisposable : ResolveData.Invalid; var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3); -#if DEBUG - Penumbra.Log.Verbose( + Penumbra.Log.Excessive( $"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}" + $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); -#endif _animationLoadData.Value = last; return ret; } diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index b2ec750f..62edb9fb 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index c41c40dc..28c37175 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 806d1a1b..8801e433 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -15,6 +15,7 @@ using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 1f408b15..6959194f 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Services/ChatService.cs b/Penumbra/Services/ChatService.cs new file mode 100644 index 00000000..f4d4ead0 --- /dev/null +++ b/Penumbra/Services/ChatService.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Log; + +namespace Penumbra.Services; + +public class ChatService : OtterGui.Classes.ChatService +{ + private readonly ChatGui _chat; + + public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat, UiBuilder uiBuilder) + : base(log, pi) + => _chat = chat; + + public void LinkItem(Item item) + { + // @formatter:off + var payloadList = new List + { + new UIForegroundPayload((ushort)(0x223 + item.Rarity * 2)), + new UIGlowPayload((ushort)(0x224 + item.Rarity * 2)), + new ItemPayload(item.RowId, false), + new UIForegroundPayload(500), + new UIGlowPayload(501), + new TextPayload($"{(char)SeIconChar.LinkMarker}"), + new UIForegroundPayload(0), + new UIGlowPayload(0), + new TextPayload(item.Name), + new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }), + new RawPayload(new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 }), + }; + // @formatter:on + + var payload = new SeString(payloadList); + + _chat.PrintChat(new XivChatEntry + { + Message = payload, + }); + } +} diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Services/SaveService.cs similarity index 95% rename from Penumbra/Util/SaveService.cs rename to Penumbra/Services/SaveService.cs index f8bdd848..ff4ec151 100644 --- a/Penumbra/Util/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -4,9 +4,8 @@ using System.Text; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Mods; -using Penumbra.Services; -namespace Penumbra.Util; +namespace Penumbra.Services; /// /// Any file type that we want to save via SaveService. diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index e973c784..b55a820f 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -18,7 +18,6 @@ using Penumbra.UI; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; -using Penumbra.Util; namespace Penumbra.Services; diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 5da7cd07..cb4c86e8 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -4,14 +4,7 @@ using Penumbra.Util; namespace Penumbra.Services; -public interface IServiceWrapper : IDisposable -{ - public string Name { get; } - public T? Service { get; } - public bool Valid { get; } -} - -public abstract class SyncServiceWrapper : IServiceWrapper +public abstract class SyncServiceWrapper { public string Name { get; } public T Service { get; } @@ -40,7 +33,7 @@ public abstract class SyncServiceWrapper : IServiceWrapper } } -public abstract class AsyncServiceWrapper : IServiceWrapper +public abstract class AsyncServiceWrapper { public string Name { get; } public T? Service { get; private set; } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index cf493716..d795062c 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Data; using Dalamud.Plugin; -using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index e151faa8..2b5b50ba 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -18,7 +18,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; -using Penumbra.Util; +using ChatService = Penumbra.Services.ChatService; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 68f9707d..294f8041 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -15,7 +15,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; -using Penumbra.Util; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/Util/ChatService.cs b/Penumbra/Util/ChatService.cs deleted file mode 100644 index 92cf0560..00000000 --- a/Penumbra/Util/ChatService.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.Gui; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Log; - -namespace Penumbra.Util; - -public class ChatService -{ - private readonly Logger _log; - private readonly UiBuilder _uiBuilder; - private readonly ChatGui _chat; - - public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat) - { - _log = log; - _uiBuilder = pi.UiBuilder; - _chat = chat; - } - - public void LinkItem(Item item) - { - // @formatter:off - var payloadList = new List - { - new UIForegroundPayload((ushort)(0x223 + item.Rarity * 2)), - new UIGlowPayload((ushort)(0x224 + item.Rarity * 2)), - new ItemPayload(item.RowId, false), - new UIForegroundPayload(500), - new UIGlowPayload(501), - new TextPayload($"{(char)SeIconChar.LinkMarker}"), - new UIForegroundPayload(0), - new UIGlowPayload(0), - new TextPayload(item.Name), - new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }), - new RawPayload(new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 }), - }; - // @formatter:on - - var payload = new SeString(payloadList); - - _chat.PrintChat(new XivChatEntry - { - Message = payload, - }); - } - - public void NotificationMessage(string content, string? title = null, NotificationType type = NotificationType.None) - { - var logLevel = type switch - { - NotificationType.None => Logger.LogLevel.Information, - NotificationType.Success => Logger.LogLevel.Information, - NotificationType.Warning => Logger.LogLevel.Warning, - NotificationType.Error => Logger.LogLevel.Error, - NotificationType.Info => Logger.LogLevel.Information, - _ => Logger.LogLevel.Debug, - }; - _uiBuilder.AddNotification(content, title, type); - _log.Message(logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}"); - } -} - -public static class SeStringBuilderExtensions -{ - public const ushort Green = 504; - public const ushort Yellow = 31; - public const ushort Red = 534; - public const ushort Blue = 517; - public const ushort White = 1; - public const ushort Purple = 541; - - public static SeStringBuilder AddText(this SeStringBuilder sb, string text, int color, bool brackets = false) - => sb.AddUiForeground((ushort)color).AddText(brackets ? $"[{text}]" : text).AddUiForegroundOff(); - - public static SeStringBuilder AddGreen(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, Green, brackets); - - public static SeStringBuilder AddYellow(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, Yellow, brackets); - - public static SeStringBuilder AddRed(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, Red, brackets); - - public static SeStringBuilder AddBlue(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, Blue, brackets); - - public static SeStringBuilder AddWhite(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, White, brackets); - - public static SeStringBuilder AddPurple(this SeStringBuilder sb, string text, bool brackets = false) - => AddText(sb, text, Purple, brackets); - - public static SeStringBuilder AddCommand(this SeStringBuilder sb, string command, string description) - => sb.AddText(" 》 ") - .AddBlue(command) - .AddText($" - {description}"); - - public static SeStringBuilder AddInitialPurple(this SeStringBuilder sb, string word, bool withComma = true) - => sb.AddPurple($"[{word[0]}]") - .AddText(withComma ? $"{word[1..]}, " : word[1..]); -} From 3bd6b0ccea772a0695c124e28f2a7f2d914e870f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 21:58:51 +0200 Subject: [PATCH 0902/2451] Stupid. --- Penumbra/CommandHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index bda1a8e5..73b5cd04 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; +using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; From 5ba455fe71d87fe33c914549677dcd896d5b3215 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Apr 2023 22:01:39 +0200 Subject: [PATCH 0903/2451] More stupid. --- Penumbra/Services/ValidityChecker.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 7ce8c803..3f55750b 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; From a293e7dfeaf1f4feb0affe7c6add2af3f8ac782d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Apr 2023 11:59:26 +0200 Subject: [PATCH 0904/2451] Add Trim in String Constructors for byte strings. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 81f384cf..41956a51 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 81f384cf96a9257b1ee2c7019772f30df78ba417 +Subproject commit 41956a51afcc4991dbf3aec5ed98ebcf739062f5 From 7d1d6ac82961dc0aa6b95ad2e3c9c2055d0ef4bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 12:25:24 +0200 Subject: [PATCH 0905/2451] Some Glamourer stuff --- Penumbra.GameData/Structs/CharacterArmor.cs | 39 +++++++++++++------- Penumbra.GameData/Structs/CharacterEquip.cs | 3 ++ Penumbra.GameData/Structs/CharacterWeapon.cs | 6 +++ Penumbra.GameData/Structs/StainId.cs | 9 ++++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index f8239c0e..eda75810 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -3,22 +3,24 @@ using System.Runtime.InteropServices; namespace Penumbra.GameData.Structs; -[StructLayout( LayoutKind.Explicit, Pack = 1 )] -public struct CharacterArmor : IEquatable< CharacterArmor > +[StructLayout(LayoutKind.Explicit, Pack = 1)] +public struct CharacterArmor : IEquatable { - [FieldOffset( 0 )] + public const int Size = 4; + + [FieldOffset(0)] public uint Value; - [FieldOffset( 0 )] + [FieldOffset(0)] public SetId Set; - [FieldOffset( 2 )] + [FieldOffset(2)] public byte Variant; - [FieldOffset( 3 )] + [FieldOffset(3)] public StainId Stain; - public CharacterArmor( SetId set, byte variant, StainId stain ) + public CharacterArmor(SetId set, byte variant, StainId stain) { Value = 0; Set = set; @@ -26,23 +28,32 @@ public struct CharacterArmor : IEquatable< CharacterArmor > Stain = stain; } + public CharacterArmor With(StainId stain) + => new(Set, Variant, stain); + + public CharacterWeapon ToWeapon() + => new(Set, 0, Variant, Stain); + + public CharacterWeapon ToWeapon(StainId stain) + => new(Set, 0, Variant, stain); + public override string ToString() => $"{Set},{Variant},{Stain}"; public static readonly CharacterArmor Empty; - public bool Equals( CharacterArmor other ) + public bool Equals(CharacterArmor other) => Value == other.Value; - public override bool Equals( object? obj ) - => obj is CharacterArmor other && Equals( other ); + public override bool Equals(object? obj) + => obj is CharacterArmor other && Equals(other); public override int GetHashCode() - => ( int )Value; + => (int)Value; - public static bool operator ==( CharacterArmor left, CharacterArmor right ) + public static bool operator ==(CharacterArmor left, CharacterArmor right) => left.Value == right.Value; - public static bool operator !=( CharacterArmor left, CharacterArmor right ) + public static bool operator !=(CharacterArmor left, CharacterArmor right) => left.Value != right.Value; -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs index dc8801d8..d045bd58 100644 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ b/Penumbra.GameData/Structs/CharacterEquip.cs @@ -6,6 +6,9 @@ namespace Penumbra.GameData.Structs; public readonly unsafe struct CharacterEquip { + public const int Slots = 10; + public const int Size = CharacterArmor.Size * Slots; + public static readonly CharacterEquip Null = new(null); private readonly CharacterArmor* _armor; diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index b94e0b05..188a163d 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -40,6 +40,12 @@ public struct CharacterWeapon : IEquatable Stain = (StainId)(value >> 48); } + public CharacterArmor ToArmor() + => new(Set, (byte)Variant, Stain); + + public CharacterArmor ToArmor(StainId stain) + => new(Set, (byte)Variant, stain); + public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); public bool Equals(CharacterWeapon other) diff --git a/Penumbra.GameData/Structs/StainId.cs b/Penumbra.GameData/Structs/StainId.cs index d97b5a94..6767a052 100644 --- a/Penumbra.GameData/Structs/StainId.cs +++ b/Penumbra.GameData/Structs/StainId.cs @@ -1,8 +1,9 @@ using System; +using System.Numerics; namespace Penumbra.GameData.Structs; -public readonly struct StainId : IEquatable< StainId > +public readonly struct StainId : IEquatable< StainId >, IEqualityOperators { public readonly byte Value; @@ -26,4 +27,10 @@ public readonly struct StainId : IEquatable< StainId > public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(StainId left, StainId right) + => left.Value == right.Value; + + public static bool operator !=(StainId left, StainId right) + => left.Value != right.Value; } \ No newline at end of file From 31338e43d67576324ea4dafea0cc285a3def079f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 12:25:43 +0200 Subject: [PATCH 0906/2451] Improve mod editor a bit --- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 51 ++++++++++++++++++- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 34 +++++++++---- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 33 +++++++----- 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 0b41d7c3..6ab0da69 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -42,7 +42,7 @@ public class ModEditor : IDisposable LoadOption(groupIdx, optionIdx, true); Files.UpdateAll(mod, Option!); SwapEditor.Revert(Option!); - MetaEditor.Load(Option!); + MetaEditor.Load(Mod!, Option!); Duplicates.Clear(); } @@ -51,7 +51,7 @@ public class ModEditor : IDisposable LoadOption(groupIdx, optionIdx, true); SwapEditor.Revert(Option!); Files.UpdatePaths(Mod!, Option!); - MetaEditor.Load(Option!); + MetaEditor.Load(Mod!, Option!); FileEditor.Clear(); Duplicates.Clear(); } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index dbd42aa3..4d845a7c 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Penumbra.Meta.Manipulations; @@ -16,6 +17,14 @@ public class ModMetaEditor private readonly HashSet _est = new(); private readonly HashSet _rsp = new(); + public int OtherImcCount { get; private set; } + public int OtherEqpCount { get; private set; } + public int OtherEqdpCount { get; private set; } + public int OtherGmpCount { get; private set; } + public int OtherEstCount { get; private set; } + public int OtherRspCount { get; private set; } + + public ModMetaEditor(ModManager modManager) => _modManager = modManager; @@ -102,8 +111,46 @@ public class ModMetaEditor Changes = true; } - public void Load(ISubMod mod) - => Split(mod.Manipulations); + public void Load(Mod mod, ISubMod currentOption) + { + OtherImcCount = 0; + OtherEqpCount = 0; + OtherEqdpCount = 0; + OtherGmpCount = 0; + OtherEstCount = 0; + OtherRspCount = 0; + foreach (var option in mod.AllSubMods) + { + if (option == currentOption) + continue; + + foreach (var manip in option.Manipulations) + { + switch (manip.ManipulationType) + { + case MetaManipulation.Type.Imc: + ++OtherImcCount; + break; + case MetaManipulation.Type.Eqdp: + ++OtherEqdpCount; + break; + case MetaManipulation.Type.Eqp: + ++OtherEqpCount; + break; + case MetaManipulation.Type.Est: + ++OtherEstCount; + break; + case MetaManipulation.Type.Gmp: + ++OtherGmpCount; + break; + case MetaManipulation.Type.Rsp: + ++OtherRspCount; + break; + } + } + } + Split(currentOption.Manipulations); + } public void Apply(Mod mod, int groupIdx, int optionIdx) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 709b6cf5..e6d8d40c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -53,7 +53,7 @@ public partial class ModEditWindow ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Load(_editor.Option!); + _editor.MetaEditor.Load(_editor.Mod!, _editor.Option!); ImGui.SameLine(); AddFromClipboardButton(); @@ -69,21 +69,33 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew); - DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew); - DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew); - DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew); - DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew); + DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew , _editor.MetaEditor.OtherEqpCount); + DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, _editor.MetaEditor.OtherEqdpCount); + DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew , _editor.MetaEditor.OtherImcCount); + DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew , _editor.MetaEditor.OtherEstCount); + DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew , _editor.MetaEditor.OtherGmpCount); + DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew , _editor.MetaEditor.OtherRspCount); } /// The headers for the different meta changes all have basically the same structure for different types. private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, Action draw, - Action drawNew) - { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - if (!ImGui.CollapsingHeader($"{items.Count} {label}")) + Action drawNew, int otherCount) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + + var oldPos = ImGui.GetCursorPosY(); + var header = ImGui.CollapsingHeader($"{items.Count} {label}"); + var newPos = ImGui.GetCursorPos(); + if (otherCount > 0) + { + var text = $"{otherCount} Edits in other Options"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + ImGui.SetCursorPos(newPos); + } + if (!header) return; using (var table = ImRaii.Table(label, numColumns, flags)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index b8a2590c..22fb385e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -367,9 +367,9 @@ public partial class ModEditWindow : Window, IDisposable { const string defaultOption = "Default Option"; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); - var width = new Vector2(ImGui.GetWindowWidth() / 3, 0); + var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor!.Option!.IsDefault)) + _editor.Option!.IsDefault)) _editor.LoadOption(-1, 0); ImGui.SameLine(); @@ -377,12 +377,14 @@ public partial class ModEditWindow : Window, IDisposable _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); ImGui.SameLine(); - - using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName, ImGuiComboFlags.NoArrowButton); + ImGui.SetNextItemWidth(width.X); + style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); + using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); if (!combo) return; - foreach (var option in _mod!.AllSubMods.Cast()) + foreach (var option in _mod!.AllSubMods) { if (ImGui.Selectable(option.FullName, option == _editor.Option)) _editor.LoadOption(option.GroupIdx, option.OptionIdx); @@ -411,6 +413,13 @@ public partial class ModEditWindow : Window, IDisposable if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) _editor.SwapEditor.Revert(_editor.Option!); + var otherSwaps = _editor.Mod!.TotalSwapCount - _editor.SwapEditor.Swaps.Count; + if (otherSwaps > 0) + { + ImGui.SameLine(); + ImGuiUtil.DrawTextButton($"There are {otherSwaps} file swaps configured in other options.", Vector2.Zero, ColorId.RedundantAssignment.Value()); + } + using var child = ImRaii.Child("##swaps", -Vector2.One, true); if (!child) return; @@ -434,18 +443,18 @@ public partial class ModEditWindow : Window, IDisposable _editor.SwapEditor.Remove(gamePath); ImGui.TableNextColumn(); - var tmp = gamePath.Path.ToString(); + var tmp = file.FullName; + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) + _editor.SwapEditor.Change(gamePath, new FullPath(tmp)); + + ImGui.TableNextColumn(); + tmp = gamePath.Path.ToString(); ImGui.SetNextItemWidth(-1); if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength) && Utf8GamePath.FromString(tmp, out var path) && !_editor.SwapEditor.Swaps.ContainsKey(path)) _editor.SwapEditor.Change(gamePath, path); - - ImGui.TableNextColumn(); - tmp = file.FullName; - ImGui.SetNextItemWidth(-1); - if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) - _editor.SwapEditor.Change(gamePath, new FullPath(tmp)); } ImGui.TableNextColumn(); From 2402d0aa6f142152da05ebec6d3dcca32425ecf7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 12:26:07 +0200 Subject: [PATCH 0907/2451] Fix bug with deleting mods --- Penumbra/Mods/Manager/ModManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 746cb645..27b4eeb8 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -116,9 +116,9 @@ public sealed class ModManager : ModStorage } _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - Mods.RemoveAt(mod.Index); - foreach (var remainingMod in this.Skip(mod.Index)) + foreach (var remainingMod in Mods.Skip(mod.Index)) --remainingMod.Index; + Mods.RemoveAt(mod.Index); Penumbra.Log.Debug($"Deleted mod {mod.Name}."); } From 290912e7cd2101bac5f1abae7c6a44c55948acdf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 13:12:58 +0200 Subject: [PATCH 0908/2451] Add collection select headers to Changed Items and Effective Changes. --- Penumbra/Services/ServiceManager.cs | 2 + Penumbra/UI/Classes/CollectionSelectHeader.cs | 76 +++++++++++++++++++ Penumbra/UI/Tabs/ChangedItemsTab.cs | 11 ++- Penumbra/UI/Tabs/EffectiveTab.cs | 44 ++++++----- Penumbra/UI/Tabs/ModsTab.cs | 74 ++++-------------- 5 files changed, 123 insertions(+), 84 deletions(-) create mode 100644 Penumbra/UI/Classes/CollectionSelectHeader.cs diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index b55a820f..6d1bf710 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -16,6 +16,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; @@ -139,6 +140,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs new file mode 100644 index 00000000..5c4570bf --- /dev/null +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -0,0 +1,76 @@ +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.UI.CollectionTab; +using Penumbra.UI.ModsTab; + +namespace Penumbra.UI.Classes; + +public class CollectionSelectHeader +{ + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModFileSystemSelector _selector; + + public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector) + { + _tutorial = tutorial; + _selector = selector; + _activeCollections = collectionManager.Active; + _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); + } + + /// Draw the header line that can quick switch between collections. + public void Draw(bool spacing) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); + var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 8f, 0); + + using (var _ = ImRaii.Group()) + { + DrawDefaultCollectionButton(3 * buttonSize); + ImGui.SameLine(); + DrawInheritedCollectionButton(3 * buttonSize); + ImGui.SameLine(); + _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value()); + } + + _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); + + if (!_activeCollections.CurrentCollectionInUse) + ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); + } + + private void DrawDefaultCollectionButton(Vector2 width) + { + var name = $"{TutorialService.DefaultCollection} ({_activeCollections.Default.Name})"; + var isCurrent = _activeCollections.Default == _activeCollections.Current; + var isEmpty = _activeCollections.Default == ModCollection.Empty; + var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." + : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." + : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) + _activeCollections.SetCollection(_activeCollections.Default, CollectionType.Current); + } + + private void DrawInheritedCollectionButton(Vector2 width) + { + var noModSelected = _selector.Selected == null; + var collection = _selector.SelectedSettingCollection; + var modInherited = collection != _activeCollections.Current; + var (name, tt) = (noModSelected, modInherited) switch + { + (true, _) => ("Inherited Collection", "No mod selected."), + (false, true) => ($"Inherited Collection ({collection.Name})", + "Set the current collection to the collection the selected mod inherits its settings from."), + (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), + }; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) + _activeCollections.SetCollection(collection, CollectionType.Current); + } +} diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 92ce322d..b7880d0c 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -16,13 +16,15 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { - private readonly CollectionManager _collectionManager; - private readonly CommunicatorService _communicator; + private readonly CollectionManager _collectionManager; + private readonly CommunicatorService _communicator; + private readonly CollectionSelectHeader _collectionHeader; - public ChangedItemsTab(CollectionManager collectionManager, CommunicatorService communicator) + public ChangedItemsTab(CollectionManager collectionManager, CommunicatorService communicator, CollectionSelectHeader collectionHeader) { _collectionManager = collectionManager; _communicator = communicator; + _collectionHeader = collectionHeader; } public ReadOnlySpan Label @@ -32,7 +34,8 @@ public class ChangedItemsTab : ITab private LowerString _changedItemModFilter = LowerString.Empty; public void DrawContent() - { + { + _collectionHeader.Draw(true); var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); if (!child) diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index d66c680e..de9ad706 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,41 +9,47 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; -using Penumbra.Collections.Cache; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.String.Classes; - +using Penumbra.UI.Classes; + namespace Penumbra.UI.Tabs; public class EffectiveTab : ITab { - private readonly CollectionManager _collectionManager; - - public EffectiveTab(CollectionManager collectionManager) - => _collectionManager = collectionManager; + private readonly CollectionManager _collectionManager; + private readonly CollectionSelectHeader _collectionHeader; + + public EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) + { + _collectionManager = collectionManager; + _collectionHeader = collectionHeader; + } public ReadOnlySpan Label => "Effective Changes"u8; public void DrawContent() { - SetupEffectiveSizes(); + SetupEffectiveSizes(); + _collectionHeader.Draw(true); DrawFilters(); using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); if (!child) return; - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - using var table = ImRaii.Table("##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg); + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips(height); + using var table = ImRaii.Table("##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg); if (!table) return; ImGui.TableSetupColumn("##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength); ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); - ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); + ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); DrawEffectiveRows(_collectionManager.Active.Current, skips, height, _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); @@ -69,8 +75,8 @@ public class EffectiveTab : ITab ImGui.CalcTextSize(FontAwesomeIcon.LongArrowAltLeft.ToIconString()).X / UiHelpers.Scale; } - _effectiveArrowLength = _effectiveUnscaledArrowLength * UiHelpers.Scale; - _effectiveLeftTextLength = 450 * UiHelpers.Scale; + _effectiveArrowLength = _effectiveUnscaledArrowLength * UiHelpers.Scale; + _effectiveLeftTextLength = 450 * UiHelpers.Scale; _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; } @@ -89,7 +95,7 @@ public class EffectiveTab : ITab _effectiveFilePathFilter = tmp; } - /// Draw all rows for one collection respecting filters and using clipping. + /// Draw all rows for one collection respecting filters and using clipping. private void DrawEffectiveRows(ModCollection active, int skips, float height, bool hasFilters) { // We can use the known counts if no filters are active. @@ -134,7 +140,7 @@ public class EffectiveTab : ITab } } - /// Draw a line for a game path and its redirected file. + /// Draw a line for a game path and its redirected file. private static void DrawLine(KeyValuePair pair) { var (path, name) = pair; @@ -148,7 +154,7 @@ public class EffectiveTab : ITab ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); } - /// Draw a line for a path and its name. + /// Draw a line for a path and its name. private static void DrawLine((string, LowerString) pair) { var (path, name) = pair; @@ -161,7 +167,7 @@ public class EffectiveTab : ITab ImGuiUtil.CopyOnClickSelectable(name); } - /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. + /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. private static void DrawLine(KeyValuePair pair) { var (manipulation, mod) = pair; @@ -174,7 +180,7 @@ public class EffectiveTab : ITab ImGuiUtil.CopyOnClickSelectable(mod.Name); } - /// Check filters for file replacements. + /// Check filters for file replacements. private bool CheckFilters(KeyValuePair kvp) { var (gamePath, fullPath) = kvp; @@ -184,7 +190,7 @@ public class EffectiveTab : ITab return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains(_effectiveFilePathFilter.Lower); } - /// Check filters for meta manipulations. + /// Check filters for meta manipulations. private bool CheckFilters((string, LowerString) kvp) { var (name, path) = kvp; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index b926c2c5..dba4c535 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -23,18 +23,19 @@ namespace Penumbra.UI.Tabs; public class ModsTab : ITab { - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _panel; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private readonly ActiveCollections _activeCollections; - private readonly RedrawService _redrawService; - private readonly Configuration _config; - private readonly CollectionCombo _collectionCombo; - private readonly ClientState _clientState; + private readonly ModFileSystemSelector _selector; + private readonly ModPanel _panel; + private readonly TutorialService _tutorial; + private readonly ModManager _modManager; + private readonly ActiveCollections _activeCollections; + private readonly RedrawService _redrawService; + private readonly Configuration _config; + private readonly ClientState _clientState; + private readonly CollectionSelectHeader _collectionHeader; public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, - TutorialService tutorial, RedrawService redrawService, Configuration config, ClientState clientState) + TutorialService tutorial, RedrawService redrawService, Configuration config, ClientState clientState, + CollectionSelectHeader collectionHeader) { _modManager = modManager; _activeCollections = collectionManager.Active; @@ -44,7 +45,7 @@ public class ModsTab : ITab _redrawService = redrawService; _config = config; _clientState = clientState; - _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); + _collectionHeader = collectionHeader; } public bool IsVisible @@ -68,7 +69,7 @@ public class ModsTab : ITab _selector.Draw(GetModSelectorSize(_config)); ImGui.SameLine(); using var group = ImRaii.Group(); - DrawHeaderLine(); + _collectionHeader.Draw(false); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -162,53 +163,4 @@ public class ModsTab : ITab ImGui.SameLine(); DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus"); } - - /// Draw the header line that can quick switch between collections. - private void DrawHeaderLine() - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 8f, 0); - - using (var _ = ImRaii.Group()) - { - DrawDefaultCollectionButton(3 * buttonSize); - ImGui.SameLine(); - DrawInheritedCollectionButton(3 * buttonSize); - ImGui.SameLine(); - _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value()); - } - - _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); - - if (!_activeCollections.CurrentCollectionInUse) - ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); - } - - private void DrawDefaultCollectionButton(Vector2 width) - { - var name = $"{TutorialService.DefaultCollection} ({_activeCollections.Default.Name})"; - var isCurrent = _activeCollections.Default == _activeCollections.Current; - var isEmpty = _activeCollections.Default == ModCollection.Empty; - var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." - : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." - : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; - if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) - _activeCollections.SetCollection(_activeCollections.Default, CollectionType.Current); - } - - private void DrawInheritedCollectionButton(Vector2 width) - { - var noModSelected = _selector.Selected == null; - var collection = _selector.SelectedSettingCollection; - var modInherited = collection != _activeCollections.Current; - var (name, tt) = (noModSelected, modInherited) switch - { - (true, _) => ("Inherited Collection", "No mod selected."), - (false, true) => ($"Inherited Collection ({collection.Name})", - "Set the current collection to the collection the selected mod inherits its settings from."), - (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), - }; - if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) - _activeCollections.SetCollection(collection, CollectionType.Current); - } } From d649a3b1a74bcc020d95abb8345c219d17d336ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 13:17:41 +0200 Subject: [PATCH 0909/2451] Increment submodules and API. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- Penumbra/Api/PenumbraApi.cs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index d7e8c8c4..983c98f7 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit d7e8c8c44d92bd50764394af51ac24cb07f362dc +Subproject commit 983c98f74e7cd052b21f6ca35ef0ceaa9b388964 diff --git a/Penumbra.String b/Penumbra.String index 41956a51..8ed213d0 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 41956a51afcc4991dbf3aec5ed98ebcf739062f5 +Subproject commit 8ed213d0127c7e17742cc29894b1b81a6dd5adf6 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9f7910ee..2d907faa 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -12,7 +12,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; @@ -31,7 +30,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 19); + => (4, 20); public event Action? PreSettingsPanelDraw { From c2fe0d6ed1c33b63048588a649af78af8d3ec512 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 13:19:06 +0200 Subject: [PATCH 0910/2451] Add Changelog. --- Penumbra/UI/Changelog.cs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 3119e48c..b0159306 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -34,10 +34,47 @@ public class PenumbraChangelog Add6_5_2(Changelog); Add6_6_0(Changelog); Add6_6_1(Changelog); + Add7_0_0(Changelog); } #region Changelogs + private static void Add7_0_0(Changelog log) + => log.NextVersion("Version 0.7.0.0") + .RegisterHighlight("The entire backend was reworked (this is still in progress). While this does not come with a lot of functionality changes, basically every file and functionality was touched.") + .RegisterEntry("This may have (re-)introduced some bugs that have not yet been noticed despite a long testing period - there are not many users of the testing branch.", 1) + .RegisterEntry("If you encounter any - but especially breaking or lossy - bugs, please report them immediately.", 1) + .RegisterEntry("This also fixed or improved numerous bugs and issues that will not be listed here.", 1) + .RegisterEntry("GitHub currently reports 321 changed files with 34541 additions and 28464 deletions.", 1) + .RegisterEntry("Added Notifications on many failures that previously only wrote to log.") + .RegisterEntry("Reworked the Collections Tab to hopefully be much more intuitive. It should be self-explanatory now.") + .RegisterEntry("The tutorial was adapted to the new window, if you are unsure, maybe try restarting it.", 1) + .RegisterEntry("You can now toggle an incognito mode in the collection window so it shows shortened names of collections and players.", 1) + .RegisterEntry("You can get an overview about the current usage of a selected collection and its active and unused mod settings in the Collection Details panel.", 1) + .RegisterEntry("The currently selected collection is now highlighted in green (default, configurable) in multiple places.", 1) + .RegisterEntry("Mods now have a 'Collections' panel in the Mod Panel containing an overview about usage of the mod in all collections.") + .RegisterEntry("The 'Changed Items' and 'Effective Changes' tab now contain a collection selector.") + .RegisterEntry("Added the On-Screen tab to find what files a specific character is actually using (by Ny).") + .RegisterEntry("Added 3 Quick Move folders in the mod selector that can be setup in context menus for easier cleanup.") + .RegisterEntry("Added handling for certain animation files for mounts and fashion accessories to correctly associate them to players.") + .RegisterEntry("The file selectors in the Advanced Mod Editing Window now use filterable combos.") + .RegisterEntry("The Advanced Mod Editing Window now shows the number of meta edits and file swaps in unselected options and highlights the option selector.") + .RegisterEntry("Added API/IPC to start unpacking and installing mods from external tools (by Sebastina).") + .RegisterEntry("Hidden files and folders are now ignored for unused files in Advanced Mod Editing (by myr)") + .RegisterEntry("Paths in mods are now automatically trimmed of whitespace on loading.") + .RegisterEntry("Fixed double 'by' in mod author display (by Caraxi).") + .RegisterEntry("Fixed a crash when trying to obtain names from the game data.") + .RegisterEntry("Fixed some issues with tutorial windows.") + .RegisterEntry("Fixed some bugs in the Resource Logger.") + .RegisterEntry("Fixed Button Sizing for collapsible groups and several related bugs.") + .RegisterEntry("Fixed issue with mods with default settings other than 0.") + .RegisterEntry("Fixed issue with commands not registering on startup. (0.6.6.5)") + .RegisterEntry("Improved Startup Times and Time Tracking. (0.6.6.4)") + .RegisterEntry("Add Item Swapping between different types of Accessories and Hats. (0.6.6.4)") + .RegisterEntry("Fixed bugs with assignment of temporary collections and their deletion. (0.6.6.4)") + .RegisterEntry("Fixed bugs with new file loading mechanism. (0.6.6.2, 0.6.6.3)") + .RegisterEntry("Added API/IPC to open and close the main window and select specific tabs and mods. (0.6.6.2)"); + private static void Add6_6_1(Changelog log) => log.NextVersion("Version 0.6.6.1") .RegisterEntry("Added an option to make successful chat commands not print their success confirmations to chat.") From b8ad456ed3880f0e9990039f31c17d9e8d20903d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Apr 2023 11:21:37 +0000 Subject: [PATCH 0911/2451] [CI] Updating repo.json for refs/tags/0.7.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8b41b9cc..afe1365d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.6.6.5", - "TestingAssemblyVersion": "0.6.6.5", + "AssemblyVersion": "0.7.0.0", + "TestingAssemblyVersion": "0.7.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.6.6.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From d831b61c0246ef9621e6695f3273a6f6f8548c42 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 14:21:08 +0200 Subject: [PATCH 0912/2451] Fix redundancy check for Your Character. --- .../Collections/Manager/ActiveCollections.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index a78cc48b..3a7d1fb4 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -433,6 +433,33 @@ public class ActiveCollections : ISavable, IDisposable switch (type) { + // Yourself is redundant if + case CollectionType.Yourself: + var yourself = ByType(CollectionType.Yourself); + if (yourself == null) + return string.Empty; + + var @base = ByType(CollectionType.Default); + var male = ByType(CollectionType.MalePlayerCharacter); + var female = ByType(CollectionType.FemalePlayerCharacter); + if (male == yourself && female == yourself) + return + "Assignment is redundant due to overwriting Male Players and Female Players with an identical collection.\nYou can remove it."; + + if (male == null) + { + if (female == null && @base == yourself) + return "Assignment is redundant due to overwriting Base with an identical collection.\nYou can remove it."; + if (female == yourself && @base == yourself) + return + "Assignment is redundant due to overwriting Base and Female Players with an identical collection.\nYou can remove it."; + } + else if (male == yourself && female == null && @base == yourself) + { + return "Assignment is redundant due to overwriting Base and Male Players with an identical collection.\nYou can remove it."; + } + + break; // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. case CollectionType.Individual: switch (id.Type) From cd94c73d9356b3a67c9bc3d08539fe9449aec89e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 14:49:06 +0200 Subject: [PATCH 0913/2451] Redundancy is stupid. --- .../Collections/Manager/ActiveCollections.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 3a7d1fb4..d97973ab 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.Services; using Penumbra.UI; using Penumbra.Util; @@ -433,32 +434,45 @@ public class ActiveCollections : ISavable, IDisposable switch (type) { - // Yourself is redundant if case CollectionType.Yourself: var yourself = ByType(CollectionType.Yourself); if (yourself == null) return string.Empty; + var racial = false; + foreach (var race in Enum.GetValues().Skip(1)) + { + var m = ByType(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + if (m != null && m != yourself) + return string.Empty; + var f = ByType(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + if (f != null && f != yourself) + return string.Empty; + + racial |= m != null || f != null; + } + + var racialString = racial ? " and Racial Assignments" : string.Empty; var @base = ByType(CollectionType.Default); var male = ByType(CollectionType.MalePlayerCharacter); var female = ByType(CollectionType.FemalePlayerCharacter); if (male == yourself && female == yourself) return - "Assignment is redundant due to overwriting Male Players and Female Players with an identical collection.\nYou can remove it."; - + $"Assignment is redundant due to overwriting Male Players and Female Players{racialString} with an identical collection.\nYou can remove it."; + if (male == null) { if (female == null && @base == yourself) - return "Assignment is redundant due to overwriting Base with an identical collection.\nYou can remove it."; + return $"Assignment is redundant due to overwriting Base{racialString} with an identical collection.\nYou can remove it."; if (female == yourself && @base == yourself) return - "Assignment is redundant due to overwriting Base and Female Players with an identical collection.\nYou can remove it."; + $"Assignment is redundant due to overwriting Base and Female Players{racialString} with an identical collection.\nYou can remove it."; } else if (male == yourself && female == null && @base == yourself) { - return "Assignment is redundant due to overwriting Base and Male Players with an identical collection.\nYou can remove it."; + return $"Assignment is redundant due to overwriting Base and Male Players{racialString} with an identical collection.\nYou can remove it."; } - + break; // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. case CollectionType.Individual: From 648286a9232b59a64bdb4faada48a55397ca43de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 15:04:30 +0200 Subject: [PATCH 0914/2451] Fix wrong Collection Unused Banner for individual assignments. --- Penumbra/Collections/Manager/ActiveCollections.cs | 3 +-- Penumbra/Collections/Manager/IndividualCollections.Files.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index d97973ab..99d3c651 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Services; using Penumbra.UI; -using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -276,7 +275,7 @@ public class ActiveCollections : ISavable, IDisposable jObj.WriteTo(j); } - private void UpdateCurrentCollectionInUse() + public void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = SpecialCollections .OfType() .Prepend(Interface) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 97e22bb2..54b845f7 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; -using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -35,6 +34,7 @@ public partial class IndividualCollections { if (ReadJObjectInternal(obj, storage)) saver.ImmediateSave(parent); + parent.UpdateCurrentCollectionInUse(); _actorService.FinishedCreation -= Func; } _actorService.FinishedCreation += Func; From 73b4787a556b76dd8c03a3125b259a38df8ec37e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 15:15:55 +0200 Subject: [PATCH 0915/2451] Add re-ordering of individual collections back. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8993a554..d18defa2 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -14,7 +14,6 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; @@ -38,6 +37,8 @@ public sealed class CollectionPanel : IDisposable private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new(); + private int _draggedIndividualAssignment = -1; + public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, CollectionSelector selector, ActorService actors, TargetManager targets, ModStorage mods) { @@ -264,6 +265,8 @@ public sealed class CollectionPanel : IDisposable using var disabled = ImRaii.Disabled(invalid); var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); + DrawIndividualDragSource(text, id); + DrawIndividualDragTarget(text, id); if (!invalid) { _selector.DragTargetAssignment(type, id); @@ -280,6 +283,35 @@ public sealed class CollectionPanel : IDisposable return button; } + private void DrawIndividualDragSource(string text, ActorIdentifier id) + { + if (!id.IsValid) + return; + + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + ImGui.SetDragDropPayload("DragIndividual", nint.Zero, 0); + ImGui.TextUnformatted($"Re-ordering {text}..."); + _draggedIndividualAssignment = _active.Individuals.Index(id); + } + + private void DrawIndividualDragTarget(string text, ActorIdentifier id) + { + if (!id.IsValid) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target || !ImGuiUtil.IsDropping("DragIndividual")) + return; + + var currentIdx = _active.Individuals.Index(id); + if (_draggedIndividualAssignment != -1 && currentIdx != -1) + _active.MoveIndividualCollection(_draggedIndividualAssignment, currentIdx); + _draggedIndividualAssignment = -1; + } + private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) { DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid, 's'); From 3d5765796e83d2a4d40e93184faf50286ce7e3c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 15:49:33 +0200 Subject: [PATCH 0916/2451] Hopefully fix issue with missing caches. --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 1 + Penumbra/Collections/Manager/ActiveCollections.cs | 3 ++- .../Collections/Manager/IndividualCollections.Files.cs | 2 +- Penumbra/Collections/Manager/IndividualCollections.cs | 9 ++++++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index a14facb4..c1d42258 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -51,6 +51,7 @@ public class CollectionCacheManager : IDisposable _communicator.ModSettingChanged.Subscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); CreateNecessaryCaches(); + _active.Individuals.Loaded += CreateNecessaryCaches; if (!MetaFileManager.CharacterUtility.Ready) MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 99d3c651..19aa27cd 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -47,6 +47,7 @@ public class ActiveCollections : ISavable, IDisposable _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); LoadCollections(); UpdateCurrentCollectionInUse(); + Individuals.Loaded += UpdateCurrentCollectionInUse; } public void Dispose() @@ -275,7 +276,7 @@ public class ActiveCollections : ISavable, IDisposable jObj.WriteTo(j); } - public void UpdateCurrentCollectionInUse() + private void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = SpecialCollections .OfType() .Prepend(Interface) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 54b845f7..6fe12a9d 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -34,7 +34,7 @@ public partial class IndividualCollections { if (ReadJObjectInternal(obj, storage)) saver.ImmediateSave(parent); - parent.UpdateCurrentCollectionInUse(); + Loaded?.Invoke(); _actorService.FinishedCreation -= Func; } _actorService.FinishedCreation += Func; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index f333ed81..dfecec67 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -17,6 +17,8 @@ public sealed partial class IndividualCollections private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); private readonly Dictionary _individuals = new(); + public event Action? Loaded; + public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments => _assignments; @@ -95,7 +97,7 @@ public sealed partial class IndividualCollections case IdentifierType.Owned: if (!ByteString.FromString(name, out var ownerName)) return AddResult.Invalid; - + identifiers = dataIds.Select(id => manager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); break; case IdentifierType.Npc: @@ -242,8 +244,9 @@ public sealed partial class IndividualCollections IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => $"{identifier.PlayerName} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})'s {_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)}", - IdentifierType.Npc => $"{_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", - _ => string.Empty, + IdentifierType.Npc => + $"{_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", + _ => string.Empty, }; } } From 89b587744370b71624bef1ff617901a67dc1b1fe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 15:50:08 +0200 Subject: [PATCH 0917/2451] Allow 64 characters for collection names instead of 32. --- Penumbra/Collections/Manager/CollectionStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 658e2e9b..0f21cd6e 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -208,7 +208,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// Does not check for uniqueness. /// private static bool IsValidName(string name) - => name.Length is > 0 and < 32 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); + => name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); /// /// Read all collection files in the Collection Directory. From a9ff6135b33323aee801eabf2864c75723fb262f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 15:51:09 +0200 Subject: [PATCH 0918/2451] Add changelog. --- Penumbra/UI/Changelog.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index b0159306..79634957 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -35,10 +35,18 @@ public class PenumbraChangelog Add6_6_0(Changelog); Add6_6_1(Changelog); Add7_0_0(Changelog); + Add7_0_1(Changelog); } #region Changelogs + private static void Add7_0_1(Changelog log) + => log.NextVersion("Version 0.7.0.1") + .RegisterEntry("Individual assignments can again be re-ordered by drag-and-dropping them.") + .RegisterEntry("Relax the restriction of a maximum of 32 characters for collection names to 64 characters.") + .RegisterEntry("Fixed a bug that showed the Your Character collection as redundant even if it was not.") + .RegisterEntry("Fixed a bug that caused some required collection caches to not be built on startup and thus mods not to apply.") + .RegisterEntry("Fixed a bug that showed the current collection as unused even if it was used."); private static void Add7_0_0(Changelog log) => log.NextVersion("Version 0.7.0.0") .RegisterHighlight("The entire backend was reworked (this is still in progress). While this does not come with a lot of functionality changes, basically every file and functionality was touched.") From ef5cf14b2be8a8084eab8692004e828e8d776d1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 16:08:16 +0200 Subject: [PATCH 0919/2451] Add some tracking of cached collections. --- .../Manager/IndividualCollections.Files.cs | 3 +- .../Manager/IndividualCollections.cs | 7 +-- Penumbra/Services/SaveService.cs | 20 ++++---- Penumbra/UI/Tabs/DebugTab.cs | 47 ++++++++++++------- 4 files changed, 45 insertions(+), 32 deletions(-) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 6fe12a9d..94bf9b46 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; @@ -34,7 +35,7 @@ public partial class IndividualCollections { if (ReadJObjectInternal(obj, storage)) saver.ImmediateSave(parent); - Loaded?.Invoke(); + saver.DalamudFramework.RunOnFrameworkThread(() => Loaded.Invoke()); _actorService.FinishedCreation -= Func; } _actorService.FinishedCreation += Func; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index dfecec67..8708df11 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -17,15 +17,16 @@ public sealed partial class IndividualCollections private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); private readonly Dictionary _individuals = new(); - public event Action? Loaded; + public event Action Loaded; public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments => _assignments; public IndividualCollections(ActorService actorService, Configuration config) { - _config = config; - _actorService = actorService; + _config = config; + _actorService = actorService; + Loaded += () => Penumbra.Log.Information($"{_assignments.Count} Individual Assignments loaded after delay."); } public enum AddResult diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index ff4ec151..2445392e 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text; +using Dalamud.Game; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Mods; @@ -32,22 +33,21 @@ public class SaveService private readonly FrameworkManager _framework; public readonly FilenameService FileNames; + public readonly Framework DalamudFramework; - public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) + public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames, Framework dalamudFramework) { - _log = log; - _framework = framework; - FileNames = fileNames; + _log = log; + _framework = framework; + FileNames = fileNames; + DalamudFramework = dalamudFramework; } /// Queue a save for the next framework tick. public void QueueSave(ISavable value) { var file = value.ToFilename(FileNames); - _framework.RegisterDelayed(value.GetType().Name + file, () => - { - ImmediateSave(value); - }); + _framework.RegisterDelayed(value.GetType().Name + file, () => { ImmediateSave(value); }); } /// Immediately trigger a save. @@ -57,9 +57,7 @@ public class SaveService try { if (name.Length == 0) - { throw new Exception("Invalid object returned empty filename."); - } _log.Debug($"Saving {value.TypeName} {value.LogName(name)}..."); var file = new FileInfo(name); @@ -80,9 +78,7 @@ public class SaveService try { if (name.Length == 0) - { throw new Exception("Invalid object returned empty filename."); - } if (!File.Exists(name)) return; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 2d5dee5a..267937b9 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.Interop; using ImGuiNET; using OtterGui; +using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -128,23 +129,37 @@ public class DebugTab : ITab if (!ImGui.CollapsingHeader("General")) return; - using var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2(-1, ImGui.GetTextLineHeightWithSpacing() * 1)); - if (!table) - return; + using (var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit)) + { + if (table) + { + PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); + PrintValue("Git Commit Hash", _validityChecker.CommitHash); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); + PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); + PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); + PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); + PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); + PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); + PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); + PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); + PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); + } + } - PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); - PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); - PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); - PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); - PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); - PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); - PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); - PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); - PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); - PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); + using (var tree = TreeNode("Collections")) + { + if (!tree) + return; + + using var table = Table("##DebugCollectionsTable", 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var collection in _collectionManager.Storage) + PrintValue(collection.Name, collection.HasCache.ToString()); + } } private void DrawPerformanceTab() From 183b59305a186de05ce71054218dc27bcad41eb5 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Apr 2023 14:10:43 +0000 Subject: [PATCH 0920/2451] [CI] Updating repo.json for refs/tags/0.7.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index afe1365d..68f86838 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.0", - "TestingAssemblyVersion": "0.7.0.0", + "AssemblyVersion": "0.7.0.1", + "TestingAssemblyVersion": "0.7.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 46bf3d739171addf4d867893b52852d9d71bd7f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 18:34:21 +0200 Subject: [PATCH 0921/2451] Notification on invalid collection names. --- Penumbra/Collections/Manager/CollectionStorage.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 0f21cd6e..d065bbcf 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -222,10 +222,17 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var file in _saveService.FileNames.CollectionFiles) { - if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance) - || !IsValidName(name)) + if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)) continue; + if (!IsValidName(name)) + { + // TODO: handle better. + Penumbra.ChatService.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + "Warning", NotificationType.Warning); + continue; + } + if (ByName(name, out _)) { Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", From 777c0cc69e0a712b6340062a242294d288bae541 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 18:34:31 +0200 Subject: [PATCH 0922/2451] Fix invalid state after mod deletion. --- Penumbra/Mods/Manager/ModManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 27b4eeb8..2ef1c890 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -116,7 +116,7 @@ public sealed class ModManager : ModStorage } _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - foreach (var remainingMod in Mods.Skip(mod.Index)) + foreach (var remainingMod in Mods.Skip(mod.Index + 1)) --remainingMod.Index; Mods.RemoveAt(mod.Index); From b1ab7e1cd013587756cec20a03b942c27306c801 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 18:34:39 +0200 Subject: [PATCH 0923/2451] Add mod state debugging. --- Penumbra/UI/Tabs/DebugTab.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 267937b9..20cb1a19 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -160,6 +160,27 @@ public class DebugTab : ITab foreach (var collection in _collectionManager.Storage) PrintValue(collection.Name, collection.HasCache.ToString()); } + + using (var tree = TreeNode("Mods")) + { + if (!tree) + return; + + using var table = Table("##DebugModsTable", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + var lastIndex = -1; + foreach (var mod in _modManager) + { + PrintValue(mod.Name, mod.Index.ToString("D5")); + ImGui.TableNextColumn(); + var index = mod.Index; + if (index != lastIndex + 1) + ImGui.TextUnformatted("!!!"); + lastIndex = index; + } + } } private void DrawPerformanceTab() From d8597009a8d89e29c3e06dcc06da5a51ef25b1c7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 18:35:02 +0200 Subject: [PATCH 0924/2451] Trim created options. --- Penumbra/Mods/ModCreator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 8801e433..755caeb3 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -352,7 +352,7 @@ public partial class ModCreator sb.Append(c); } - return sb.ToString(); + return sb.ToString().Trim(); } public void SplitMultiGroups(DirectoryInfo baseDir) From 42ef951b8250455ed8b118afefd1690598caf1e9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Apr 2023 16:37:45 +0000 Subject: [PATCH 0925/2451] [CI] Updating repo.json for refs/tags/0.7.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 68f86838..78de6881 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.1", - "TestingAssemblyVersion": "0.7.0.1", + "AssemblyVersion": "0.7.0.2", + "TestingAssemblyVersion": "0.7.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 127bbcb485ce1ea275b8471e70174a8acff49aa5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 19:12:19 +0200 Subject: [PATCH 0926/2451] Prevent integer overflowing on 8k x 8k textures. --- Penumbra/Import/Textures/Texture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 22096b37..77412e92 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -180,8 +180,8 @@ public sealed class Texture : IDisposable using var stream = OpenTexStream(dalamud.GameData); var scratch = TexFileParser.Parse(stream); BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); - RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(); CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height); return true; } From 8d37c5ff06f5a08d479286c1c5f377c7d5d1dedd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Apr 2023 19:18:11 +0200 Subject: [PATCH 0927/2451] Only Trim End on RelPath. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 8ed213d0..5d9f36c5 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 8ed213d0127c7e17742cc29894b1b81a6dd5adf6 +Subproject commit 5d9f36c5b57685b07354460e225e65759ef9996e From ca0caebe844cbb7134daccb060f25b4e97631141 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Apr 2023 17:30:18 +0000 Subject: [PATCH 0928/2451] [CI] Updating repo.json for refs/tags/0.7.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 78de6881..5c7c4b46 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.2", - "TestingAssemblyVersion": "0.7.0.2", + "AssemblyVersion": "0.7.0.3", + "TestingAssemblyVersion": "0.7.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 60754012c220057eb6854e3193c788d925b25954 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Apr 2023 10:46:05 +0200 Subject: [PATCH 0929/2451] Make Material Reassignment work again. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 22fb385e..d9bebeaf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -58,6 +58,7 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Reset(); _shaderPackageTab.Reset(); _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + UpdateModels(); } public void ChangeOption(SubMod? subMod) From d9dc37c994b032cb6d68830b830b011e8ee89eb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Apr 2023 10:48:07 +0200 Subject: [PATCH 0930/2451] Disable Meta Edits when mods are disabled. --- Penumbra/Interop/PathResolving/MetaState.cs | 8 +++++--- Penumbra/Interop/Services/DecalReverter.cs | 5 ++++- Penumbra/Interop/Services/MetaList.cs | 2 ++ Penumbra/Meta/MetaFileManager.cs | 13 +++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 75985a31..656b00b6 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -45,6 +45,7 @@ public unsafe class MetaState : IDisposable [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; @@ -56,7 +57,7 @@ public unsafe class MetaState : IDisposable private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceService resources, GameEventManager gameEventManager, CharacterUtility characterUtility) + ResourceService resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config) { _performance = performance; _communicator = communicator; @@ -64,6 +65,7 @@ public unsafe class MetaState : IDisposable _resources = resources; _gameEventManager = gameEventManager; _characterUtility = characterUtility; + _config = config; SignatureHelper.Initialise(this); _onModelLoadCompleteHook = Hook.FromAddress(_humanVTable[58], OnModelLoadCompleteDetour); _getEqpIndirectHook.Enable(); @@ -127,7 +129,7 @@ public unsafe class MetaState : IDisposable _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); - var decal = new DecalReverter(_characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); + var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); @@ -254,7 +256,7 @@ public unsafe class MetaState : IDisposable var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); using var decals = - new DecalReverter(_characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); + new DecalReverter(_config, _characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); var ret = _changeCustomize.Original(human, data, skipEquipment); _inChangeCustomize = false; return ret; diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index cc9c6403..a0a8c84d 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -19,12 +19,15 @@ public sealed unsafe class DecalReverter : IDisposable private readonly Structs.TextureResourceHandle* _decal; private readonly Structs.TextureResourceHandle* _transparent; - public DecalReverter(CharacterUtility utility, ResourceService resources, ModCollection? collection, bool doDecal) + public DecalReverter(Configuration config, CharacterUtility utility, ResourceService resources, ModCollection? collection, bool doDecal) { _utility = utility; var ptr = _utility.Address; _decal = null; _transparent = null; + if (!config.EnableMods) + return; + if (doDecal) { var decalPath = collection?.ResolvePath(DecalPath)?.InternalName ?? DecalPath.Path; diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index 98471c5f..e2603f79 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -106,6 +106,8 @@ public unsafe class MetaList : IDisposable public sealed class MetaReverter : IDisposable { + public static readonly MetaReverter Disabled = new(null!) { Disposed = true }; + public readonly MetaList MetaList; public readonly nint Data; public readonly int Length; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 30186768..8e764b14 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -5,7 +5,6 @@ using Dalamud.Data; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; using Penumbra.Collections; -using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.Import; @@ -66,12 +65,12 @@ public unsafe class MetaFileManager { Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}"); } - } + } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) { - if (file == null) + if (file == null || !Config.EnableMods) CharacterUtility.ResetResource(metaIndex); else CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length); @@ -79,9 +78,11 @@ public unsafe class MetaFileManager [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) - => file == null - ? CharacterUtility.TemporarilyResetResource(metaIndex) - : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length); + => Config.EnableMods + ? file == null + ? CharacterUtility.TemporarilyResetResource(metaIndex) + : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length) + : MetaList.MetaReverter.Disabled; public void ApplyDefaultFiles(ModCollection collection) { From 23e553c88e61fcc94d2681fc95f23a91385603f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Apr 2023 11:09:22 +0200 Subject: [PATCH 0931/2451] Add options to BulkTag to not only check local tags. --- Penumbra/CommandHandler.cs | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 73b5cd04..03647d70 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -395,6 +395,13 @@ public class CommandHandler : IDisposable return false; } + private enum TagType + { + Local, + Mod, + Both, + } + private bool SetTag(string arguments) { if (arguments.Length == 0) @@ -402,8 +409,17 @@ public class CommandHandler : IDisposable var seString = new SeStringBuilder() .AddText("Use with /penumbra bulktag ").AddBlue("[enable|disable|toggle|inherit]").AddText(" ").AddYellow("[Collection Name]") .AddText(" | ") - .AddPurple("[Local Tag]"); + .AddPurple("[Tag]"); _chat.Print(seString.BuiltString); + var tagString = new SeStringBuilder() + .AddText(" 》 ") + .AddPurple("[Tag]") + .AddText(" is only Local tags by default, but can be prefixed with '") + .AddWhite("b:") + .AddText("' for both types of tags or '") + .AddWhite("m:") + .AddText("' for only Mod tags."); + _chat.Print(tagString.BuiltString); return true; } @@ -428,11 +444,26 @@ public class CommandHandler : IDisposable if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; - var mods = _modManager.Where(m => m.LocalTags.Contains(nameSplit[1], StringComparer.OrdinalIgnoreCase)).ToList(); + var tagType = nameSplit[1].Length < 3 || nameSplit[1][1] != ':' + ? TagType.Local + : nameSplit[1][0] switch + { + 'b' => TagType.Both, + 'm' => TagType.Mod, + _ => TagType.Local, + }; + var tag = tagType is TagType.Local ? nameSplit[1] : nameSplit[1][2..]; + + var mods = tagType switch + { + TagType.Local => _modManager.Where(m => m.LocalTags.Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), + TagType.Mod => _modManager.Where(m => m.ModTags.Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), + _ => _modManager.Where(m => m.LocalTags.Concat(m.ModTags).Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), + }; if (mods.Count == 0) { - _chat.Print(new SeStringBuilder().AddText("The tag ").AddRed(nameSplit[1], true).AddText(" does not match any mods.") + _chat.Print(new SeStringBuilder().AddText("The tag ").AddRed(tag, true).AddText(" does not match any mods.") .BuiltString); return false; } @@ -543,7 +574,6 @@ public class CommandHandler : IDisposable .AddYellow(collection.Name, true) .AddText(" to inherit.").BuiltString); return true; - } return false; From 1ccf3a42560822dbd1f5d145cd87da4ed902a5f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Apr 2023 12:36:48 +0200 Subject: [PATCH 0932/2451] Improve delayed individual collection loading. --- OtterGui | 2 +- Penumbra/Collections/Cache/CollectionCacheManager.cs | 4 ++-- .../Manager/IndividualCollections.Files.cs | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 99bd0a88..4ef03980 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 99bd0a889806f0560109686ca3b29a4edfa48f76 +Subproject commit 4ef03980803cdf5a253c041d6ed1ef26d9a9b938 diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c1d42258..336d52a9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -296,8 +296,8 @@ public class CollectionCacheManager : IDisposable .Prepend(_active.Current) .Prepend(_active.Default) .Prepend(_active.Interface) - .Distinct() - .Select(c => CreateCache(c) ? Task.Run(() => CalculateEffectiveFileListInternal(c)) : Task.CompletedTask) + .Where(CreateCache) + .Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))) .ToArray(); Task.WaitAll(tasks); diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 94bf9b46..0ab9cbfb 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -33,10 +33,13 @@ public partial class IndividualCollections return ReadJObjectInternal(obj, storage); void Func() { - if (ReadJObjectInternal(obj, storage)) - saver.ImmediateSave(parent); - saver.DalamudFramework.RunOnFrameworkThread(() => Loaded.Invoke()); - _actorService.FinishedCreation -= Func; + saver.DalamudFramework.RunOnFrameworkThread(() => + { + if (ReadJObjectInternal(obj, storage)) + saver.ImmediateSave(parent); + Loaded.Invoke(); + _actorService.FinishedCreation -= Func; + }); } _actorService.FinishedCreation += Func; return false; From 340a35918c680a0c91623dff27ee8dd265d429c2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Apr 2023 12:57:59 +0200 Subject: [PATCH 0933/2451] Add Changelog. --- Penumbra/UI/Changelog.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 79634957..9f493a8e 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -36,10 +36,22 @@ public class PenumbraChangelog Add6_6_1(Changelog); Add7_0_0(Changelog); Add7_0_1(Changelog); - } - + Add7_0_4(Changelog); + } + #region Changelogs + private static void Add7_0_4(Changelog log) + => log.NextVersion("Version 0.7.0.4") + .RegisterEntry("Added options to the bulktag slash command to check all/local/mod tags specifically.") + .RegisterEntry("Possibly improved handling of the delayed loading of individual assignments.") + .RegisterEntry("Fixed a bug that caused metadata edits to apply even though mods were disabled.") + .RegisterEntry("Fixed a bug that prevented material reassignments from working.") + .RegisterEntry("Reverted trimming of whitespace for relative paths to only trim the end, not the start. (0.7.0.3)") + .RegisterEntry("Fixed a bug that caused an integer overflow on textures of high dimensions. (0.7.0.3)") + .RegisterEntry("Fixed a bug that caused Penumbra to enter invalid state when deleting mods. (0.7.0.2)") + .RegisterEntry("Added Notification on invalid collection names. (0.7.0.2)"); + private static void Add7_0_1(Changelog log) => log.NextVersion("Version 0.7.0.1") .RegisterEntry("Individual assignments can again be re-ordered by drag-and-dropping them.") From f9b1e85c8f8f296f9825e924efcabe06f7e32047 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 30 Apr 2023 11:00:51 +0000 Subject: [PATCH 0934/2451] [CI] Updating repo.json for refs/tags/0.7.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5c7c4b46..77d284bb 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.3", - "TestingAssemblyVersion": "0.7.0.3", + "AssemblyVersion": "0.7.0.4", + "TestingAssemblyVersion": "0.7.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 101a1b739292bfd38cfb95e5eb6681d80cc8349e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 May 2023 18:42:57 +0200 Subject: [PATCH 0935/2451] Fix non-populating models for Update Bibo. --- Penumbra/Mods/Editor/ModEditor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 6ab0da69..c19b9962 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -44,6 +44,7 @@ public class ModEditor : IDisposable SwapEditor.Revert(Option!); MetaEditor.Load(Mod!, Option!); Duplicates.Clear(); + MdlMaterialEditor.ScanModels(Mod!); } public void LoadOption(int groupIdx, int optionIdx) From 7ab5c7311c5e88175f672546d3f2c0717e913d1d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 May 2023 18:43:27 +0200 Subject: [PATCH 0936/2451] Fix multiple context menus for collections with identical identifier names. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index d18defa2..86742b3d 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -214,9 +214,9 @@ public sealed class CollectionPanel : IDisposable DrawSettingsList(collection); } - private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix) + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix) { - var label = $"{type}{identifier}{suffix}"; + var label = $"{type}{text}{suffix}"; if (open) ImGui.OpenPopup(label); @@ -274,7 +274,7 @@ public sealed class CollectionPanel : IDisposable var size = ImGui.CalcTextSize(name); var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); - DrawContext(button, collection, type, id, suffix); + DrawContext(button, collection, type, id, text, suffix); } if (hovered) From c2fb18ab5380c4cfdf05562e5de42310cf42fe1a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 May 2023 18:43:49 +0200 Subject: [PATCH 0937/2451] Change CollectionCache handling. --- .../Cache/CollectionCacheManager.cs | 65 ++++++++++--------- Penumbra/Penumbra.cs | 4 +- Penumbra/UI/ImportPopup.cs | 3 +- Penumbra/UI/Tabs/DebugTab.cs | 53 +++++++-------- 4 files changed, 63 insertions(+), 62 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 336d52a9..5a4ab2dd 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -21,20 +21,18 @@ public class CollectionCacheManager : IDisposable private readonly CommunicatorService _communicator; private readonly TempModManager _tempMods; private readonly ModStorage _modStorage; + private readonly CollectionStorage _storage; private readonly ActiveCollections _active; internal readonly MetaFileManager MetaFileManager; - private readonly Dictionary _caches = new(); + public int Count { get; private set; } - public int Count - => _caches.Count; - - public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active - => _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value)); + public IEnumerable Active + => _storage.Where(c => c.HasCache); public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, - TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active) + TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) { _framework = framework; _communicator = communicator; @@ -42,6 +40,7 @@ public class CollectionCacheManager : IDisposable _modStorage = modStorage; MetaFileManager = metaFileManager; _active = active; + _storage = storage; _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); @@ -72,12 +71,11 @@ public class CollectionCacheManager : IDisposable /// Only creates a new cache, does not update an existing one. public bool CreateCache(ModCollection collection) { - if (_caches.ContainsKey(collection) || collection.Index == ModCollection.Empty.Index) + if (collection.HasCache || collection.Index == ModCollection.Empty.Index) return false; - var cache = new CollectionCache(this, collection); - _caches.Add(collection, cache); - collection._cache = cache; + collection._cache = new CollectionCache(this, collection); + ++Count; Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); return true; } @@ -97,28 +95,34 @@ public class CollectionCacheManager : IDisposable return; Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); - if (!_caches.TryGetValue(collection, out var cache)) + if (!collection.HasCache) { Penumbra.Log.Error( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); return; } - FullRecalculation(collection, cache); + FullRecalculation(collection); Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); } - private void FullRecalculation(ModCollection collection, CollectionCache cache) + private void FullRecalculation(ModCollection collection) { + var cache = collection._cache; + if (cache == null) + return; + cache.ResolvedFiles.Clear(); cache.Meta.Reset(); cache._conflicts.Clear(); // Add all forced redirects. - foreach (var tempMod in _tempMods.ModsForAllCollections.Concat( - _tempMods.Mods.TryGetValue(collection, out var list) ? list : Array.Empty())) + foreach (var tempMod in _tempMods.ModsForAllCollections + .Concat(_tempMods.Mods.TryGetValue(collection, out var list) + ? list + : Array.Empty())) cache.AddMod(tempMod, false); foreach (var mod in _modStorage) @@ -157,11 +161,11 @@ public class CollectionCacheManager : IDisposable { case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: - foreach (var collection in _caches.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.RemoveMod(mod, true); break; case ModPathChangeType.Moved: - foreach (var collection in _caches.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); break; } @@ -172,13 +176,13 @@ public class CollectionCacheManager : IDisposable if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) return; - foreach (var collection in _caches.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.AddMod(mod, true); } /// Apply a mod change to all collections with a cache. private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_caches.Keys, mod, created, removed); + => TempModManager.OnGlobalModChange(_storage.Where(c => c.HasCache), mod, created, removed); /// Remove a cache from a collection if it is active. private void RemoveCache(ModCollection? collection) @@ -198,7 +202,7 @@ public class CollectionCacheManager : IDisposable { if (type is ModOptionChangeType.PrepareChange) { - foreach (var collection in _caches.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) collection._cache!.RemoveMod(mod, false); return; @@ -209,7 +213,7 @@ public class CollectionCacheManager : IDisposable if (!recomputeList) return; - foreach (var collection in _caches.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) { if (reload) collection._cache!.ReloadMod(mod, true); @@ -221,16 +225,17 @@ public class CollectionCacheManager : IDisposable /// Increment the counter to ensure new files are loaded after applying meta changes. private void IncrementCounters() { - foreach (var (collection, _) in _caches) + foreach (var collection in _storage.Where(c => c.HasCache)) ++collection.ChangeCounter; MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) { - if (!_caches.TryGetValue(collection, out var cache)) + if (!collection.HasCache) return; + var cache = collection._cache!; switch (type) { case ModSettingChange.Inheritance: @@ -259,7 +264,7 @@ public class CollectionCacheManager : IDisposable break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - FullRecalculation(collection, cache); + FullRecalculation(collection); break; } } @@ -269,19 +274,17 @@ public class CollectionCacheManager : IDisposable /// just recompute everything. /// private void OnCollectionInheritanceChange(ModCollection collection, bool _) - { - if (_caches.TryGetValue(collection, out var cache)) - FullRecalculation(collection, cache); - } + => FullRecalculation(collection); /// Clear the current cache of a collection. private void ClearCache(ModCollection collection) { - if (!_caches.Remove(collection, out var cache)) + if (!collection.HasCache) return; - cache.Dispose(); + collection._cache!.Dispose(); collection._cache = null; + --Count; Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}."); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 997ff784..ffd32df8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -225,8 +225,8 @@ public class Penumbra : IDalamudPlugin foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n"); - foreach (var (collection, cache) in _collectionManager.Caches.Active) - PrintCollection(collection, cache); + foreach (var collection in _collectionManager.Caches.Active) + PrintCollection(collection, collection._cache!); return sb.ToString(); } diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 0a5f160e..17a21dc7 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -5,6 +5,7 @@ using ImGuiNET; using OtterGui.Raii; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.UI; @@ -36,7 +37,7 @@ public sealed class ImportPopup : Window if (!_modImportManager.IsImporting(out var import)) return; - const string importPopup = "##importPopup"; + const string importPopup = "##PenumbraImportPopup"; if (!ImGui.IsPopupOpen(importPopup)) ImGui.OpenPopup(importPopup); diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 20cb1a19..0e821ab8 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -6,10 +6,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.Interop; using ImGuiNET; using OtterGui; -using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -148,37 +146,36 @@ public class DebugTab : ITab } } - using (var tree = TreeNode("Collections")) + using (var tree = TreeNode($"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1})###Collections")) { - if (!tree) - return; - - using var table = Table("##DebugCollectionsTable", 2, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - foreach (var collection in _collectionManager.Storage) - PrintValue(collection.Name, collection.HasCache.ToString()); + if (tree) + { + using var table = Table("##DebugCollectionsTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var collection in _collectionManager.Storage) + PrintValue(collection.Name, collection.HasCache.ToString()); + } } - using (var tree = TreeNode("Mods")) + var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index); + using (var tree = TreeNode($"Mods ({issues} Issues)###Mods")) { - if (!tree) - return; - - using var table = Table("##DebugModsTable", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - var lastIndex = -1; - foreach (var mod in _modManager) + if (tree) { - PrintValue(mod.Name, mod.Index.ToString("D5")); - ImGui.TableNextColumn(); - var index = mod.Index; - if (index != lastIndex + 1) - ImGui.TextUnformatted("!!!"); - lastIndex = index; + using var table = Table("##DebugModsTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + { + var lastIndex = -1; + foreach (var mod in _modManager) + { + PrintValue(mod.Name, mod.Index.ToString("D5")); + ImGui.TableNextColumn(); + var index = mod.Index; + if (index != lastIndex + 1) + ImGui.TextUnformatted("!!!"); + lastIndex = index; + } + } } } } From 2167ddf9d9a5c9f80b1b0394b2aecd291e8937fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 May 2023 18:47:52 +0200 Subject: [PATCH 0938/2451] Maybe fix window issues? Dunno wtf is going on. --- Penumbra/UI/ImportPopup.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 17a21dc7..85b4f650 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -5,7 +5,6 @@ using ImGuiNET; using OtterGui.Raii; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; -using Penumbra.Services; namespace Penumbra.UI; @@ -31,9 +30,15 @@ public sealed class ImportPopup : Window }; } - public override void Draw() + public override void PreDraw() { _modImportManager.TryUnpacking(); + ImGui.SetNextWindowCollapsed(false, ImGuiCond.Always); + IsOpen = true; + } + + public override void Draw() + { if (!_modImportManager.IsImporting(out var import)) return; From a38a989fe789e4bca09ad4c3f2d344e05a1908ed Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 1 May 2023 16:51:25 +0000 Subject: [PATCH 0939/2451] [CI] Updating repo.json for refs/tags/0.7.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 77d284bb..fe3e6109 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.4", - "TestingAssemblyVersion": "0.7.0.4", + "AssemblyVersion": "0.7.0.5", + "TestingAssemblyVersion": "0.7.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ee50994b3984c93392fce8a5b73779a91bb77fd0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 May 2023 22:50:34 +0200 Subject: [PATCH 0940/2451] Maybe sort race condition. --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 7 +++++-- Penumbra/Collections/Manager/ActiveCollections.cs | 2 +- .../Collections/Manager/IndividualCollections.Files.cs | 2 +- Penumbra/Collections/Manager/IndividualCollections.cs | 4 +++- Penumbra/Collections/Manager/TempCollectionManager.cs | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5a4ab2dd..001ea952 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -42,6 +42,8 @@ public class CollectionCacheManager : IDisposable _active = active; _storage = storage; + if (!_active.Individuals.IsLoaded) + _active.Individuals.Loaded += CreateNecessaryCaches; _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); @@ -49,11 +51,11 @@ public class CollectionCacheManager : IDisposable _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); _communicator.ModSettingChanged.Subscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); - CreateNecessaryCaches(); - _active.Individuals.Loaded += CreateNecessaryCaches; if (!MetaFileManager.CharacterUtility.Ready) MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; + + CreateNecessaryCaches(); } public void Dispose() @@ -303,6 +305,7 @@ public class CollectionCacheManager : IDisposable .Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))) .ToArray(); + Penumbra.Log.Debug($"Creating {tasks.Length} necessary caches."); Task.WaitAll(tasks); } } diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 19aa27cd..1034227e 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -43,7 +43,7 @@ public class ActiveCollections : ISavable, IDisposable Current = storage.DefaultNamed; Default = storage.DefaultNamed; Interface = storage.DefaultNamed; - Individuals = new IndividualCollections(actors, config); + Individuals = new IndividualCollections(actors, config, false); _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); LoadCollections(); UpdateCurrentCollectionInUse(); diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 0ab9cbfb..4e238722 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Dalamud.Game; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; @@ -37,6 +36,7 @@ public partial class IndividualCollections { if (ReadJObjectInternal(obj, storage)) saver.ImmediateSave(parent); + IsLoaded = true; Loaded.Invoke(); _actorService.FinishedCreation -= Func; }); diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 8708df11..a3005f07 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -18,14 +18,16 @@ public sealed partial class IndividualCollections private readonly Dictionary _individuals = new(); public event Action Loaded; + public bool IsLoaded { get; private set; } public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments => _assignments; - public IndividualCollections(ActorService actorService, Configuration config) + public IndividualCollections(ActorService actorService, Configuration config, bool temporary) { _config = config; _actorService = actorService; + IsLoaded = temporary; Loaded += () => Penumbra.Log.Information($"{_assignments.Count} Individual Assignments loaded after delay."); } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 0416b4b7..7f1f03b8 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -25,7 +25,7 @@ public class TempCollectionManager : IDisposable _communicator = communicator; _actors = actors; _storage = storage; - Collections = new IndividualCollections(actors, config); + Collections = new IndividualCollections(actors, config, true); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); } From 94a086455668551d77190c7222a1252cc9c0dcdf Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 1 May 2023 20:55:50 +0000 Subject: [PATCH 0941/2451] [CI] Updating repo.json for refs/tags/0.7.0.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index fe3e6109..f650984b 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.5", - "TestingAssemblyVersion": "0.7.0.5", + "AssemblyVersion": "0.7.0.6", + "TestingAssemblyVersion": "0.7.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9c0406ec9d467593a649a09519f12c66eac4bc1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 15:40:37 +0200 Subject: [PATCH 0942/2451] Fix some caching issues. --- .../Cache/CollectionCacheManager.cs | 18 +++++++++++++++ Penumbra/Mods/Manager/ModCacheManager.cs | 4 ++-- Penumbra/Mods/Manager/ModManager.cs | 15 +++++++------ Penumbra/Mods/Manager/ModOptionEditor.cs | 6 ++--- Penumbra/Mods/Manager/ModStorage.cs | 11 ++++++---- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/Util/DictionaryExtensions.cs | 22 +++++++++++++------ 7 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 001ea952..fc43f654 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -51,6 +51,8 @@ public class CollectionCacheManager : IDisposable _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); _communicator.ModSettingChanged.Subscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, -100); if (!MetaFileManager.CharacterUtility.Ready) MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; @@ -308,4 +310,20 @@ public class CollectionCacheManager : IDisposable Penumbra.Log.Debug($"Creating {tasks.Length} necessary caches."); Task.WaitAll(tasks); } + + private void OnModDiscoveryStarted() + { + foreach (var collection in Active) + { + collection._cache!.ResolvedFiles.Clear(); + collection._cache!.Meta.Reset(); + collection._cache!._conflicts.Clear(); + } + } + + private void OnModDiscoveryFinished() + { + var tasks = Active.Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))).ToArray(); + Task.WaitAll(tasks); + } } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 86a26960..1015e4ed 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -168,10 +168,10 @@ public class ModCacheManager : IDisposable => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); private static void UpdateSwapCount(Mod mod) - => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + => mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); private static void UpdateMetaCount(Mod mod) - => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Manipulations.Count); + => mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count); private static void UpdateHasOptions(Mod mod) => mod.HasOptions = mod.Groups.Any(o => o.IsOption); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 2ef1c890..8b25f763 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -29,7 +29,7 @@ public enum ModPathChangeType StartingReload, } -public sealed class ModManager : ModStorage +public sealed class ModManager : ModStorage, IDisposable { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -49,7 +49,8 @@ public sealed class ModManager : ModStorage DataEditor = dataEditor; OptionEditor = optionEditor; Creator = creator; - SetBaseDirectory(config.ModDirectory, true); + SetBaseDirectory(config.ModDirectory, true); + _communicator.ModPathChanged.Subscribe(OnModPathChange); DiscoverMods(); } @@ -66,7 +67,7 @@ public sealed class ModManager : ModStorage public void DiscoverMods() { _communicator.ModDiscoveryStarted.Invoke(); - NewMods.Clear(); + ClearNewMods(); Mods.Clear(); BasePath.Refresh(); @@ -243,11 +244,11 @@ public sealed class ModManager : ModStorage { switch (type) { - case ModPathChangeType.Added: - NewMods.Add(mod); + case ModPathChangeType.Added: + SetNew(mod); break; case ModPathChangeType.Deleted: - NewMods.Remove(mod); + SetKnown(mod); break; case ModPathChangeType.Moved: if (oldDirectory != null && newDirectory != null) @@ -258,7 +259,7 @@ public sealed class ModManager : ModStorage } public void Dispose() - { } + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); /// /// Set the mod base directory. diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index ed978000..c30d90c3 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -314,7 +314,7 @@ public class ModOptionEditor return; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData = manipulations; + subMod.ManipulationData.SetTo(manipulations); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } @@ -327,7 +327,7 @@ public class ModOptionEditor return; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData = replacements; + subMod.FileData.SetTo(replacements); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } @@ -353,7 +353,7 @@ public class ModOptionEditor return; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData = swaps; + subMod.FileSwapData.SetTo(swaps); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 3aa6d31f..5e8999d7 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -53,14 +53,17 @@ public class ModStorage : IReadOnlyList /// Mods are removed when they are deleted or when they are toggled in any collection. /// Also gets cleared on mod rediscovery. /// - protected readonly HashSet NewMods = new(); + private readonly HashSet _newMods = new(); public bool IsNew(Mod mod) - => NewMods.Contains(mod); + => _newMods.Contains(mod); public void SetNew(Mod mod) - => NewMods.Add(mod); + => _newMods.Add(mod); public void SetKnown(Mod mod) - => NewMods.Remove(mod); + => _newMods.Remove(mod); + + public void ClearNewMods() + => _newMods.Clear(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d9bebeaf..211acc85 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) _editor.SwapEditor.Revert(_editor.Option!); - var otherSwaps = _editor.Mod!.TotalSwapCount - _editor.SwapEditor.Swaps.Count; + var otherSwaps = _editor.Mod!.TotalSwapCount - _editor.Option!.FileSwaps.Count; if (otherSwaps > 0) { ImGui.SameLine(); diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index ad832457..31931fe7 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -6,7 +6,7 @@ namespace Penumbra.Util; public static class DictionaryExtensions { - // Returns whether two dictionaries contain equal keys and values. + /// Returns whether two dictionaries contain equal keys and values. public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) { if( ReferenceEquals( lhs, rhs ) ) @@ -46,24 +46,32 @@ public static class DictionaryExtensions return true; } - // Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand. + /// Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand. public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) where TKey : notnull { if( ReferenceEquals( lhs, rhs ) ) - { return; - } lhs.Clear(); lhs.EnsureCapacity( rhs.Count ); foreach( var (key, value) in rhs ) - { lhs.Add( key, value ); - } } - // Add all entries from the other dictionary that would not overwrite current keys. + /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. + public static void SetTo(this HashSet lhs, IReadOnlySet rhs) + { + if (ReferenceEquals(lhs, rhs)) + return; + + lhs.Clear(); + lhs.EnsureCapacity(rhs.Count); + foreach (var value in rhs) + lhs.Add(value); + } + + /// Add all entries from the other dictionary that would not overwrite current keys. public static void AddFrom< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) where TKey : notnull { From 314a1e0e8c044cfd3bd65bd08ee7cd71fc7bb8ee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 16:31:18 +0200 Subject: [PATCH 0943/2451] Fix file selector not opening at right location. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2b5b50ba..758b6aad 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -215,9 +215,11 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0 ? _config.DefaultModImportPath - : _config.ModDirectory.Length > 0 ? _config.ModDirectory : null; + var modPath = _config.DefaultModImportPath.Length > 0 + ? _config.DefaultModImportPath + : _config.ModDirectory.Length > 0 + ? _config.ModDirectory + : null; _fileDialog.OpenFilePicker("Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => From f46daf0f54497573a210643899d4de00bb7773f6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 2 May 2023 14:36:08 +0000 Subject: [PATCH 0944/2451] [CI] Updating repo.json for refs/tags/0.7.0.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f650984b..c2595932 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.6", - "TestingAssemblyVersion": "0.7.0.6", + "AssemblyVersion": "0.7.0.7", + "TestingAssemblyVersion": "0.7.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From fb84b43d697c78c1a0e639f252e1571cb7737961 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 17:46:13 +0200 Subject: [PATCH 0945/2451] Use explicit priorities for all internal communication events. --- Penumbra/Api/PenumbraApi.cs | 21 +- Penumbra/Api/TempModManager.cs | 7 +- .../Cache/CollectionCacheManager.cs | 19 +- .../Collections/Manager/ActiveCollections.cs | 3 +- .../Collections/Manager/CollectionStorage.cs | 9 +- .../Collections/Manager/InheritanceManager.cs | 3 +- .../Manager/TempCollectionManager.cs | 3 +- Penumbra/Communication/ChangedItemClick.cs | 28 ++ Penumbra/Communication/ChangedItemHover.cs | 29 ++ Penumbra/Communication/CollectionChange.cs | 54 +++ .../CollectionInheritanceChanged.cs | 34 ++ .../Communication/CreatedCharacterBase.cs | 25 ++ .../Communication/CreatingCharacterBase.cs | 29 ++ Penumbra/Communication/EnabledChanged.cs | 26 ++ Penumbra/Communication/ModDataChanged.cs | 35 ++ Penumbra/Communication/ModDirectoryChanged.cs | 30 ++ .../Communication/ModDiscoveryFinished.cs | 33 ++ Penumbra/Communication/ModDiscoveryStarted.cs | 26 ++ Penumbra/Communication/ModOptionChanged.cs | 40 +++ Penumbra/Communication/ModPathChanged.cs | 54 +++ Penumbra/Communication/ModSettingChanged.cs | 43 +++ .../Communication/PostSettingsPanelDraw.cs | 26 ++ .../Communication/PreSettingsPanelDraw.cs | 26 ++ .../Communication/TemporaryGlobalModChange.cs | 31 ++ .../IdentifiedCollectionCache.cs | 5 +- Penumbra/Mods/Manager/ModCacheManager.cs | 9 +- Penumbra/Mods/Manager/ModExportManager.cs | 3 +- Penumbra/Mods/Manager/ModFileSystem.cs | 7 +- Penumbra/Mods/Manager/ModManager.cs | 3 +- Penumbra/Services/CommunicatorService.cs | 327 +----------------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 9 +- .../UI/CollectionTab/CollectionSelector.cs | 3 +- .../CollectionTab/IndividualAssignmentUi.cs | 15 +- Penumbra/UI/FileDialogService.cs | 7 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 13 +- Penumbra/Util/EventWrapper.cs | 30 +- 36 files changed, 681 insertions(+), 384 deletions(-) create mode 100644 Penumbra/Communication/ChangedItemClick.cs create mode 100644 Penumbra/Communication/ChangedItemHover.cs create mode 100644 Penumbra/Communication/CollectionChange.cs create mode 100644 Penumbra/Communication/CollectionInheritanceChanged.cs create mode 100644 Penumbra/Communication/CreatedCharacterBase.cs create mode 100644 Penumbra/Communication/CreatingCharacterBase.cs create mode 100644 Penumbra/Communication/EnabledChanged.cs create mode 100644 Penumbra/Communication/ModDataChanged.cs create mode 100644 Penumbra/Communication/ModDirectoryChanged.cs create mode 100644 Penumbra/Communication/ModDiscoveryFinished.cs create mode 100644 Penumbra/Communication/ModDiscoveryStarted.cs create mode 100644 Penumbra/Communication/ModOptionChanged.cs create mode 100644 Penumbra/Communication/ModPathChanged.cs create mode 100644 Penumbra/Communication/ModSettingChanged.cs create mode 100644 Penumbra/Communication/PostSettingsPanelDraw.cs create mode 100644 Penumbra/Communication/PreSettingsPanelDraw.cs create mode 100644 Penumbra/Communication/TemporaryGlobalModChange.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2d907faa..d88d069e 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -22,6 +22,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.Interop.Services; using Penumbra.UI; @@ -34,13 +35,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event Action? PreSettingsPanelDraw { - add => _communicator.PreSettingsPanelDraw.Subscribe(value!); + add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); } public event Action? PostSettingsPanelDraw { - add => _communicator.PostSettingsPanelDraw.Subscribe(value!); + add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default); remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); } @@ -68,7 +69,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Subscribe(new Action(value)); + _communicator.CreatingCharacterBase.Subscribe(new Action(value), Communication.CreatingCharacterBase.Priority.Api); } remove { @@ -88,7 +89,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value)); + _communicator.CreatedCharacterBase.Subscribe(new Action(value), Communication.CreatedCharacterBase.Priority.Api); } remove { @@ -147,8 +148,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); - _communicator.ModSettingChanged.Subscribe(OnModSettingChange, -1000); + _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); } public unsafe void Dispose() @@ -180,7 +181,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemClick? ChangedItemClicked { - add => _communicator.ChangedItemClick.Subscribe(new Action(value!)); + add => _communicator.ChangedItemClick.Subscribe(new Action(value!), Communication.ChangedItemClick.Priority.Default); remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!)); } @@ -203,7 +204,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _communicator.ModDirectoryChanged.Subscribe(value!); + _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); } remove { @@ -220,7 +221,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _communicator.EnabledChanged.Subscribe(value!, int.MinValue); + _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); } remove { @@ -237,7 +238,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemHover? ChangedItemTooltip { - add => _communicator.ChangedItemHover.Subscribe(new Action(value!)); + add => _communicator.ChangedItemHover.Subscribe(new Action(value!), Communication.ChangedItemHover.Priority.Default); remove => _communicator.ChangedItemHover.Unsubscribe(new Action(value!)); } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index c28a10f7..1dd8331f 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -5,8 +5,9 @@ using Penumbra.Mods; using System.Collections.Generic; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Collections.Manager; - +using Penumbra.Collections.Manager; +using Penumbra.Communication; + namespace Penumbra.Api; public enum RedirectResult @@ -27,7 +28,7 @@ public class TempModManager : IDisposable public TempModManager(CommunicatorService communicator) { _communicator = communicator; - _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.TempModManager); } public void Dispose() diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index fc43f654..088c3a90 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -44,15 +45,15 @@ public class CollectionCacheManager : IDisposable if (!_active.Individuals.IsLoaded) _active.Individuals.Loaded += CreateNecessaryCaches; - _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); - _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); - _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); - _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); - _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); - _communicator.ModSettingChanged.Subscribe(OnModSettingChange); - _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); - _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); - _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, -100); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionCacheManager); + _communicator.ModPathChanged.Subscribe(OnModChangeAddition, ModPathChanged.Priority.CollectionCacheManagerAddition); + _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, ModPathChanged.Priority.CollectionCacheManagerRemoval); + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange, TemporaryGlobalModChange.Priority.CollectionCacheManager); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionCacheManager); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange, ModSettingChanged.Priority.CollectionCacheManager); + _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange, CollectionInheritanceChanged.Priority.CollectionCacheManager); + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionCacheManager); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager); if (!MetaFileManager.CharacterUtility.Ready) MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 1034227e..86d36abd 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Services; @@ -44,7 +45,7 @@ public class ActiveCollections : ISavable, IDisposable Default = storage.DefaultNamed; Interface = storage.DefaultNamed; Individuals = new IndividualCollections(actors, config, false); - _communicator.CollectionChange.Subscribe(OnCollectionChange, -100); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ActiveCollections); LoadCollections(); UpdateCurrentCollectionInUse(); Individuals.Loaded += UpdateCurrentCollectionInUse; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index d065bbcf..4980ea71 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -7,6 +7,7 @@ using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -56,10 +57,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator = communicator; _saveService = saveService; _modStorage = modStorage; - _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); - _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); - _communicator.ModPathChanged.Subscribe(OnModPathChange, 10); - _communicator.ModOptionChanged.Subscribe(OnModOptionChange, 100); + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); ReadCollections(out DefaultNamed); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 1378ae56..93dee89f 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; @@ -47,7 +48,7 @@ public class InheritanceManager : IDisposable _modStorage = modStorage; ApplyInheritances(); - _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.InheritanceManager); } public void Dispose() diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 7f1f03b8..400bd2cf 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Penumbra.Api; +using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.Mods; using Penumbra.Services; @@ -27,7 +28,7 @@ public class TempCollectionManager : IDisposable _storage = storage; Collections = new IndividualCollections(actors, config, true); - _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange, TemporaryGlobalModChange.Priority.TempCollectionManager); } public void Dispose() diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs new file mode 100644 index 00000000..fd879280 --- /dev/null +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -0,0 +1,28 @@ +using System; +using Penumbra.Api.Enums; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered when a Changed Item in Penumbra is clicked. +/// +/// Parameter is the clicked mouse button. +/// Parameter is the clicked object data if any.. +/// +/// +public sealed class ChangedItemClick : EventWrapper, ChangedItemClick.Priority> +{ + public enum Priority + { + /// + Default = 0, + } + + public ChangedItemClick() + : base(nameof(ChangedItemClick)) + { } + + public void Invoke(MouseButton button, object? data) + => Invoke(this, button, data); +} diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs new file mode 100644 index 00000000..9a658769 --- /dev/null +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -0,0 +1,29 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered when a Changed Item in Penumbra is hovered. +/// +/// Parameter is the hovered object data if any. +/// +/// +public sealed class ChangedItemHover : EventWrapper, ChangedItemHover.Priority> +{ + public enum Priority + { + /// + Default = 0, + } + + public ChangedItemHover() + : base(nameof(ChangedItemHover)) + { } + + public void Invoke(object? data) + => Invoke(this, data); + + public bool HasTooltip + => HasSubscribers; +} diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs new file mode 100644 index 00000000..c5a0b93f --- /dev/null +++ b/Penumbra/Communication/CollectionChange.cs @@ -0,0 +1,54 @@ +using System; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever collection setup is changed. +/// +/// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) +/// Parameter is the old collection, or null on additions. +/// Parameter is the new collection, or null on deletions. +/// Parameter is the display name for Individual collections or an empty string otherwise. +/// +public sealed class CollectionChange : EventWrapper, CollectionChange.Priority> +{ + public enum Priority + { + /// + CollectionCacheManager = -2, + + /// + ActiveCollections = -1, + + /// + TempModManager = 0, + + /// + InheritanceManager = 0, + + /// + IdentifiedCollectionCache = 0, + + /// + ItemSwapTab = 0, + + /// + CollectionSelector = 0, + + /// + IndividualAssignmentUi = 0, + + /// + ModFileSystemSelector = 0, + } + + public CollectionChange() + : base(nameof(CollectionChange)) + { } + + public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) + => Invoke(this, collectionType, oldCollection, newCollection, displayName); +} diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs new file mode 100644 index 00000000..3562f457 --- /dev/null +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -0,0 +1,34 @@ +using System; +using Penumbra.Collections; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a collections inheritances change. +/// +/// Parameter is the collection whose ancestors were changed. +/// Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). +/// +/// +public sealed class CollectionInheritanceChanged : EventWrapper, CollectionInheritanceChanged.Priority> +{ + public enum Priority + { + /// + CollectionCacheManager = 0, + + /// + ItemSwapTab = 0, + + /// + ModFileSystemSelector = 0, + } + + public CollectionInheritanceChanged() + : base(nameof(CollectionInheritanceChanged)) + { } + + public void Invoke(ModCollection collection, bool inherited) + => Invoke(this, collection, inherited); +} diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs new file mode 100644 index 00000000..dacd51dd --- /dev/null +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -0,0 +1,25 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is the created draw object. +/// +public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +{ + public enum Priority + { + /// + Api = 0, + } + + public CreatedCharacterBase() + : base(nameof(CreatedCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) + => Invoke(this, gameObject, appliedCollectionName, drawObject); +} diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs new file mode 100644 index 00000000..357bc066 --- /dev/null +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -0,0 +1,29 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a character base draw object is being created by the game. +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is a pointer to the model id (an uint). +/// Parameter is a pointer to the customize array. +/// Parameter is a pointer to the equip data array. +/// +public sealed class CreatingCharacterBase : EventWrapper, CreatingCharacterBase.Priority> +{ + public enum Priority + { + /// + Api = 0, + } + + public CreatingCharacterBase() + : base(nameof(CreatingCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) + => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); +} diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs new file mode 100644 index 00000000..ec63337f --- /dev/null +++ b/Penumbra/Communication/EnabledChanged.cs @@ -0,0 +1,26 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered when the general Enabled state of Penumbra is changed. +/// +/// Parameter is whether Penumbra is now Enabled (true) or Disabled (false). +/// +/// +public sealed class EnabledChanged : EventWrapper, EnabledChanged.Priority> +{ + public enum Priority + { + /// + Api = int.MinValue, + } + + public EnabledChanged() + : base(nameof(EnabledChanged)) + { } + + public void Invoke(bool enabled) + => Invoke(this, enabled); +} diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs new file mode 100644 index 00000000..8f16dcfe --- /dev/null +++ b/Penumbra/Communication/ModDataChanged.cs @@ -0,0 +1,35 @@ +using System; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever mod meta data or local data is changed. +/// +/// Parameter is the type of data change for the mod, which can be multiple flags. +/// Parameter is the changed mod. +/// Parameter is the old name of the mod in case of a name change, and null otherwise. +/// +public sealed class ModDataChanged : EventWrapper, ModDataChanged.Priority> +{ + public enum Priority + { + /// + ModFileSystemSelector = -10, + + /// + ModCacheManager = 0, + + /// + ModFileSystem = 0, + } + + public ModDataChanged() + : base(nameof(ModDataChanged)) + { } + + public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) + => Invoke(this, changeType, mod, oldName); +} diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs new file mode 100644 index 00000000..102ddec4 --- /dev/null +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -0,0 +1,30 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever the mod root directory changes. +/// +/// Parameter is the full path of the new directory. +/// Parameter is whether the new directory is valid. +/// +/// +public sealed class ModDirectoryChanged : EventWrapper, ModDirectoryChanged.Priority> +{ + public enum Priority + { + /// + Api = 0, + + /// + FileDialogService = 0, + } + + public ModDirectoryChanged() + : base(nameof(ModDirectoryChanged)) + { } + + public void Invoke(string newModDirectory, bool newDirectoryValid) + => Invoke(this, newModDirectory, newDirectoryValid); +} diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs new file mode 100644 index 00000000..1471d7d7 --- /dev/null +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -0,0 +1,33 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// Triggered whenever a new mod discovery has finished. +public sealed class ModDiscoveryFinished : EventWrapper +{ + public enum Priority + { + /// + ModFileSystemSelector = -200, + + /// + CollectionCacheManager = -100, + + /// + CollectionStorage = 0, + + /// + ModCacheManager = 0, + + /// + ModFileSystem = 0, + } + + public ModDiscoveryFinished() + : base(nameof(ModDiscoveryFinished)) + { } + + public void Invoke() + => Invoke(this); +} diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs new file mode 100644 index 00000000..4e98a3e0 --- /dev/null +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -0,0 +1,26 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// Triggered whenever mods are prepared to be rediscovered. +public sealed class ModDiscoveryStarted : EventWrapper +{ + public enum Priority + { + /// + CollectionCacheManager = 0, + + /// + CollectionStorage = 0, + + /// + ModFileSystemSelector = 200, + } + public ModDiscoveryStarted() + : base(nameof(ModDiscoveryStarted)) + { } + + public void Invoke() + => Invoke(this); +} diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs new file mode 100644 index 00000000..cacc7a89 --- /dev/null +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -0,0 +1,40 @@ +using System; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever an option of a mod is changed inside the mod. +/// +/// Parameter is the type option change. +/// Parameter is the changed mod. +/// Parameter is the index of the changed group inside the mod. +/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. +/// Parameter is the index of the group an option was moved to. +/// +public sealed class ModOptionChanged : EventWrapper, ModOptionChanged.Priority> +{ + public enum Priority + { + /// + CollectionCacheManager = -100, + + /// + ModCacheManager = 0, + + /// + ItemSwapTab = 0, + + /// + CollectionStorage = 100, + } + + public ModOptionChanged() + : base(nameof(ModOptionChanged)) + { } + + public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) + => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); +} diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs new file mode 100644 index 00000000..608c10eb --- /dev/null +++ b/Penumbra/Communication/ModPathChanged.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a mod is added, deleted, moved or reloaded. +/// +/// Parameter is the type of change. +/// Parameter is the changed mod. +/// Parameter is the old directory on deletion, move or reload and null on addition. +/// Parameter is the new directory on addition, move or reload and null on deletion. +/// +/// +public sealed class ModPathChanged : EventWrapper, ModPathChanged.Priority> +{ + public enum Priority + { + /// + CollectionCacheManagerAddition = -100, + + /// + Api = 0, + + /// + ModCacheManager = 0, + + /// + ModExportManager = 0, + + /// + ModFileSystem = 0, + + /// + ModManager = 0, + + /// + CollectionStorage = 10, + + /// + CollectionCacheManagerRemoval = 100, + + + } + public ModPathChanged() + : base(nameof(ModPathChanged)) + { } + + public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) + => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); +} diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs new file mode 100644 index 00000000..e32a84c2 --- /dev/null +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -0,0 +1,43 @@ +using System; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a mod setting is changed. +/// +/// Parameter is the collection in which the setting was changed. +/// Parameter is the type of change. +/// Parameter is the mod the setting was changed for, unless it was a multi-change. +/// Parameter is the old value of the setting before the change as int. +/// Parameter is the index of the changed group if the change type is Setting. +/// Parameter is whether the change was inherited from another collection. +/// +/// +public sealed class ModSettingChanged : EventWrapper, ModSettingChanged.Priority> +{ + public enum Priority + { + /// + Api = int.MinValue, + + /// + CollectionCacheManager = 0, + + /// + ItemSwapTab = 0, + + /// + ModFileSystemSelector = 0, + } + + public ModSettingChanged() + : base(nameof(ModSettingChanged)) + { } + + public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) + => Invoke(this, collection, type, mod, oldValue, groupIdx, inherited); +} diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs new file mode 100644 index 00000000..e460e369 --- /dev/null +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -0,0 +1,26 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered after the settings panel is drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// +/// +public sealed class PostSettingsPanelDraw : EventWrapper, PostSettingsPanelDraw.Priority> +{ + public enum Priority + { + /// + Default = 0, + } + + public PostSettingsPanelDraw() + : base(nameof(PostSettingsPanelDraw)) + { } + + public void Invoke(string modDirectory) + => Invoke(this, modDirectory); +} diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs new file mode 100644 index 00000000..c3e182b0 --- /dev/null +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -0,0 +1,26 @@ +using System; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered before the settings panel is drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// +/// +public sealed class PreSettingsPanelDraw : EventWrapper, PreSettingsPanelDraw.Priority> +{ + public enum Priority + { + /// + Default = 0, + } + + public PreSettingsPanelDraw() + : base(nameof(PreSettingsPanelDraw)) + { } + + public void Invoke(string modDirectory) + => Invoke(this, modDirectory); +} diff --git a/Penumbra/Communication/TemporaryGlobalModChange.cs b/Penumbra/Communication/TemporaryGlobalModChange.cs new file mode 100644 index 00000000..1f0352a2 --- /dev/null +++ b/Penumbra/Communication/TemporaryGlobalModChange.cs @@ -0,0 +1,31 @@ +using System; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a temporary mod for all collections is changed. +/// +/// Parameter added, deleted or edited temporary mod. +/// Parameter is whether the mod was newly created. +/// Parameter is whether the mod was deleted. +/// +public sealed class TemporaryGlobalModChange : EventWrapper, TemporaryGlobalModChange.Priority> +{ + public enum Priority + { + /// + CollectionCacheManager = 0, + + /// + TempCollectionManager = 0, + } + + public TemporaryGlobalModChange() + : base(nameof(TemporaryGlobalModChange)) + { } + + public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) + => Invoke(this, temporaryMod, newlyCreated, deleted); +} diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 58ae0d92..67ab584e 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,7 +5,8 @@ using Dalamud.Game.ClientState; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; -using Penumbra.Collections.Manager; +using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Services; @@ -26,7 +27,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A _communicator = communicator; _events = events; - _communicator.CollectionChange.Subscribe(CollectionChangeClear); + _communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache); _clientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 1015e4ed..1ace9536 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -23,10 +24,10 @@ public class ModCacheManager : IDisposable _identifier = identifier; _modManager = modStorage; - _communicator.ModOptionChanged.Subscribe(OnModOptionChange); - _communicator.ModPathChanged.Subscribe(OnModPathChange); - _communicator.ModDataChanged.Subscribe(OnModDataChange); - _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager); + _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager); if (!identifier.Valid) identifier.FinishedCreation += OnIdentifierCreation; OnModDiscoveryFinished(); diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 6396e1f9..6bb919fc 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -21,7 +22,7 @@ public class ModExportManager : IDisposable _communicator = communicator; _modManager = modManager; UpdateExportDirectory(_config.ExportDirectory, false); - _communicator.ModPathChanged.Subscribe(OnModPathChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModExportManager); } /// diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 76f4e1d6..62daa9fb 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; +using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; @@ -25,9 +26,9 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable _saveService = saveService; Reload(); Changed += OnChange; - _communicator.ModDiscoveryFinished.Subscribe(Reload); - _communicator.ModDataChanged.Subscribe(OnDataChange); - _communicator.ModPathChanged.Subscribe(OnModPathChange); + _communicator.ModDiscoveryFinished.Subscribe(Reload, ModDiscoveryFinished.Priority.ModFileSystem); + _communicator.ModDataChanged.Subscribe(OnDataChange, ModDataChanged.Priority.ModFileSystem); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModFileSystem); } public void Dispose() diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 8b25f763..7e7599f0 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading.Tasks; +using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -50,7 +51,7 @@ public sealed class ModManager : ModStorage, IDisposable OptionEditor = optionEditor; Creator = creator; SetBaseDirectory(config.ModDirectory, true); - _communicator.ModPathChanged.Subscribe(OnModPathChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); DiscoverMods(); } diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index a45548c7..371722b2 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,350 +1,59 @@ using System; -using System.IO; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Mods; -using Penumbra.Mods.Manager; -using Penumbra.Util; +using Penumbra.Communication; namespace Penumbra.Services; -/// -/// Triggered whenever collection setup is changed. -/// -/// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) -/// Parameter is the old collection, or null on additions. -/// Parameter is the new collection, or null on deletions. -/// Parameter is the display name for Individual collections or an empty string otherwise. -/// -public sealed class CollectionChange : EventWrapper> -{ - public CollectionChange() - : base(nameof(CollectionChange)) - { } - - public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) - => Invoke(this, collectionType, oldCollection, newCollection, displayName); -} - -/// -/// Triggered whenever a temporary mod for all collections is changed. -/// -/// Parameter added, deleted or edited temporary mod. -/// Parameter is whether the mod was newly created. -/// Parameter is whether the mod was deleted. -/// -public sealed class TemporaryGlobalModChange : EventWrapper> -{ - public TemporaryGlobalModChange() - : base(nameof(TemporaryGlobalModChange)) - { } - - public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) - => Invoke(this, temporaryMod, newlyCreated, deleted); -} - -/// -/// Triggered whenever a character base draw object is being created by the game. -/// -/// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. -/// Parameter is a pointer to the model id (an uint). -/// Parameter is a pointer to the customize array. -/// Parameter is a pointer to the equip data array. -/// -public sealed class CreatingCharacterBase : EventWrapper> -{ - public CreatingCharacterBase() - : base(nameof(CreatingCharacterBase)) - { } - - public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) - => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); -} - -/// -/// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. -/// Parameter is the created draw object. -/// -public sealed class CreatedCharacterBase : EventWrapper> -{ - public CreatedCharacterBase() - : base(nameof(CreatedCharacterBase)) - { } - - public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) - => Invoke(this, gameObject, appliedCollectionName, drawObject); -} - -/// -/// Triggered whenever mod meta data or local data is changed. -/// -/// Parameter is the type of data change for the mod, which can be multiple flags. -/// Parameter is the changed mod. -/// Parameter is the old name of the mod in case of a name change, and null otherwise. -/// -public sealed class ModDataChanged : EventWrapper> -{ - public ModDataChanged() - : base(nameof(ModDataChanged)) - { } - - public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) - => Invoke(this, changeType, mod, oldName); -} - -/// -/// Triggered whenever an option of a mod is changed inside the mod. -/// -/// Parameter is the type option change. -/// Parameter is the changed mod. -/// Parameter is the index of the changed group inside the mod. -/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. -/// Parameter is the index of the group an option was moved to. -/// -public sealed class ModOptionChanged : EventWrapper> -{ - public ModOptionChanged() - : base(nameof(ModOptionChanged)) - { } - - public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) - => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); -} - -/// Triggered whenever mods are prepared to be rediscovered. -public sealed class ModDiscoveryStarted : EventWrapper -{ - public ModDiscoveryStarted() - : base(nameof(ModDiscoveryStarted)) - { } - - public void Invoke() - => EventWrapper.Invoke(this); -} - -/// Triggered whenever a new mod discovery has finished. -public sealed class ModDiscoveryFinished : EventWrapper -{ - public ModDiscoveryFinished() - : base(nameof(ModDiscoveryFinished)) - { } - - public void Invoke() - => Invoke(this); -} - -/// -/// Triggered whenever the mod root directory changes. -/// -/// Parameter is the full path of the new directory. -/// Parameter is whether the new directory is valid. -/// -/// -public sealed class ModDirectoryChanged : EventWrapper> -{ - public ModDirectoryChanged() - : base(nameof(ModDirectoryChanged)) - { } - - public void Invoke(string newModDirectory, bool newDirectoryValid) - => Invoke(this, newModDirectory, newDirectoryValid); -} - -/// -/// Triggered whenever a mod is added, deleted, moved or reloaded. -/// -/// Parameter is the type of change. -/// Parameter is the changed mod. -/// Parameter is the old directory on deletion, move or reload and null on addition. -/// Parameter is the new directory on addition, move or reload and null on deletion. -/// -/// -public sealed class ModPathChanged : EventWrapper> -{ - public ModPathChanged() - : base(nameof(ModPathChanged)) - { } - - public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) - => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); -} - -/// -/// Triggered whenever a mod setting is changed. -/// -/// Parameter is the collection in which the setting was changed. -/// Parameter is the type of change. -/// Parameter is the mod the setting was changed for, unless it was a multi-change. -/// Parameter is the old value of the setting before the change as int. -/// Parameter is the index of the changed group if the change type is Setting. -/// Parameter is whether the change was inherited from another collection. -/// -/// -public sealed class ModSettingChanged : EventWrapper> -{ - public ModSettingChanged() - : base(nameof(ModSettingChanged)) - { } - - public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) - => Invoke(this, collection, type, mod, oldValue, groupIdx, inherited); -} - -/// -/// Triggered whenever a collections inheritances change. -/// -/// Parameter is the collection whose ancestors were changed. -/// Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). -/// -/// -public sealed class CollectionInheritanceChanged : EventWrapper> -{ - public CollectionInheritanceChanged() - : base(nameof(CollectionInheritanceChanged)) - { } - - public void Invoke(ModCollection collection, bool inherited) - => Invoke(this, collection, inherited); -} - -/// -/// Triggered when the general Enabled state of Penumbra is changed. -/// -/// Parameter is whether Penumbra is now Enabled (true) or Disabled (false). -/// -/// -public sealed class EnabledChanged : EventWrapper> -{ - public EnabledChanged() - : base(nameof(EnabledChanged)) - { } - - public void Invoke(bool enabled) - => Invoke(this, enabled); -} - -/// -/// Triggered before the settings panel is drawn. -/// -/// Parameter is the identifier (directory name) of the currently selected mod. -/// -/// -public sealed class PreSettingsPanelDraw : EventWrapper> -{ - public PreSettingsPanelDraw() - : base(nameof(PreSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); -} - -/// -/// Triggered after the settings panel is drawn. -/// -/// Parameter is the identifier (directory name) of the currently selected mod. -/// -/// -public sealed class PostSettingsPanelDraw : EventWrapper> -{ - public PostSettingsPanelDraw() - : base(nameof(PostSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); -} - -/// -/// Triggered when a Changed Item in Penumbra is hovered. -/// -/// Parameter is the hovered object data if any. -/// -/// -public sealed class ChangedItemHover : EventWrapper> -{ - public ChangedItemHover() - : base(nameof(ChangedItemHover)) - { } - - public void Invoke(object? data) - => Invoke(this, data); - - public bool HasTooltip - => HasSubscribers; -} - -/// -/// Triggered when a Changed Item in Penumbra is clicked. -/// -/// Parameter is the clicked mouse button. -/// Parameter is the clicked object data if any.. -/// -/// -public sealed class ChangedItemClick : EventWrapper> -{ - public ChangedItemClick() - : base(nameof(ChangedItemClick)) - { } - - public void Invoke(MouseButton button, object? data) - => Invoke(this, button, data); -} - public class CommunicatorService : IDisposable { - /// + /// public readonly CollectionChange CollectionChange = new(); - /// + /// public readonly TemporaryGlobalModChange TemporaryGlobalModChange = new(); - /// + /// public readonly CreatingCharacterBase CreatingCharacterBase = new(); - /// + /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// + /// public readonly ModDataChanged ModDataChanged = new(); - /// + /// public readonly ModOptionChanged ModOptionChanged = new(); - /// + /// public readonly ModDiscoveryStarted ModDiscoveryStarted = new(); - /// + /// public readonly ModDiscoveryFinished ModDiscoveryFinished = new(); - /// + /// public readonly ModDirectoryChanged ModDirectoryChanged = new(); - /// + /// public readonly ModPathChanged ModPathChanged = new(); - /// + /// public readonly ModSettingChanged ModSettingChanged = new(); - /// + /// public readonly CollectionInheritanceChanged CollectionInheritanceChanged = new(); - /// + /// public readonly EnabledChanged EnabledChanged = new(); - /// + /// public readonly PreSettingsPanelDraw PreSettingsPanelDraw = new(); - /// + /// public readonly PostSettingsPanelDraw PostSettingsPanelDraw = new(); - /// + /// public readonly ChangedItemHover ChangedItemHover = new(); - /// + /// public readonly ChangedItemClick ChangedItemClick = new(); public void Dispose() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 22bca756..4903158d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -13,6 +13,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -57,10 +58,10 @@ public class ItemSwapTab : IDisposable, ITab // @formatter:on }; - _communicator.CollectionChange.Subscribe(OnCollectionChange); - _communicator.ModSettingChanged.Subscribe(OnSettingChange); - _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange); - _communicator.ModOptionChanged.Subscribe(OnModOptionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ItemSwapTab); + _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ItemSwapTab); + _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ItemSwapTab); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ItemSwapTab); } /// Update the currently selected mod or its settings. diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 14611d59..746c2d5f 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.UI.Classes; @@ -34,7 +35,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl _active = active; _tutorial = tutorial; - _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); // Set items. OnCollectionChange(CollectionType.Inactive, null, null, string.Empty); // Set selection. diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index 98fcc0d6..81e0a862 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -5,6 +5,7 @@ using ImGuiNET; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.Services; @@ -30,7 +31,7 @@ public class IndividualAssignmentUi : IDisposable _communicator = communicator; _actorService = actors; _collectionManager = collectionManager; - _communicator.CollectionChange.Subscribe(UpdateIdentifiers); + _communicator.CollectionChange.Subscribe(UpdateIdentifiers, CollectionChange.Priority.IndividualAssignmentUi); if (_actorService.Valid) SetupCombos(); else @@ -57,7 +58,7 @@ public class IndividualAssignmentUi : IDisposable public void DrawWorldCombo(float width) { if (_ready && _worldCombo.Draw(width)) - UpdateIdentifiers(); + UpdateIdentifiersInternal(); } public void DrawObjectKindCombo(float width) @@ -76,7 +77,7 @@ public class IndividualAssignmentUi : IDisposable continue; _newKind = kind; - UpdateIdentifiers(); + UpdateIdentifiersInternal(); } } @@ -87,7 +88,7 @@ public class IndividualAssignmentUi : IDisposable ImGui.SetNextItemWidth(width); if (ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32)) - UpdateIdentifiers(); + UpdateIdentifiersInternal(); } public void DrawNewNpcCollection(float width) @@ -97,7 +98,7 @@ public class IndividualAssignmentUi : IDisposable var combo = GetNpcCombo(_newKind); if (combo.Draw(width)) - UpdateIdentifiers(); + UpdateIdentifiersInternal(); } public void Dispose() @@ -154,10 +155,10 @@ public class IndividualAssignmentUi : IDisposable private void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) { if (type == CollectionType.Individual) - UpdateIdentifiers(); + UpdateIdentifiersInternal(); } - private void UpdateIdentifiers() + private void UpdateIdentifiersInternal() { var combo = GetNpcCombo(_newKind); PlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index b1956796..c483c3b1 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.UI; @@ -22,9 +23,9 @@ public class FileDialogService : IDisposable public FileDialogService(CommunicatorService communicator, Configuration config) { - _communicator = communicator; - _manager = SetupFileManager(config.ModDirectory); - _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange); + _communicator = communicator; + _manager = SetupFileManager(config.ModDirectory); + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange, ModDirectoryChanged.Priority.FileDialogService); } public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 758b6aad..5a53712d 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,6 +14,7 @@ using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -72,12 +73,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector : IDisposable where T : Delegate +public abstract class EventWrapper : IDisposable + where T : Delegate + where TPriority : struct, Enum { - private readonly string _name; - private readonly List<(object Subscriber, int Priority)> _event = new(); + private readonly string _name; + private readonly List<(object Subscriber, TPriority Priority)> _event = new(); public bool HasSubscribers => _event.Count > 0; @@ -23,12 +25,12 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - public void Subscribe(T subscriber, int priority = 0) + public void Subscribe(T subscriber, TPriority priority) { lock (_event) { - var existingIdx = _event.FindIndex(p => (T) p.Subscriber == subscriber); - var idx = _event.FindIndex(p => p.Priority > priority); + var existingIdx = _event.FindIndex(p => (T)p.Subscriber == subscriber); + var idx = _event.FindIndex(p => p.Priority.CompareTo(priority) > 0); if (idx == existingIdx) { if (idx < 0) @@ -53,14 +55,14 @@ public abstract class EventWrapper : IDisposable where T : Delegate { lock (_event) { - var idx = _event.FindIndex(p => (T) p.Subscriber == subscriber); + var idx = _event.FindIndex(p => (T)p.Subscriber == subscriber); if (idx >= 0) _event.RemoveAt(idx); } } - protected static void Invoke(EventWrapper wrapper) + protected static void Invoke(EventWrapper wrapper) { lock (wrapper._event) { @@ -78,7 +80,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a) + protected static void Invoke(EventWrapper wrapper, T1 a) { lock (wrapper._event) { @@ -96,7 +98,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b) + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b) { lock (wrapper._event) { @@ -114,7 +116,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c) + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c) { lock (wrapper._event) { @@ -132,7 +134,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d) + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d) { lock (wrapper._event) { @@ -150,7 +152,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e) + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e) { lock (wrapper._event) { @@ -168,7 +170,7 @@ public abstract class EventWrapper : IDisposable where T : Delegate } } - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) { lock (wrapper._event) { From 58bd223a80f116b51500b8950b9d6fd989970b51 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 18:02:32 +0200 Subject: [PATCH 0946/2451] Make cache calculation thread safe(r) --- .../Cache/CollectionCacheManager.cs | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 088c3a90..d5df7d7d 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -27,7 +27,8 @@ public class CollectionCacheManager : IDisposable internal readonly MetaFileManager MetaFileManager; - public int Count { get; private set; } + public int Count { get; private set; } + public bool Calculating { get; private set; } public IEnumerable Active => _storage.Where(c => c.HasCache); @@ -116,28 +117,36 @@ public class CollectionCacheManager : IDisposable private void FullRecalculation(ModCollection collection) { var cache = collection._cache; - if (cache == null) + if (cache == null || Calculating) return; - cache.ResolvedFiles.Clear(); - cache.Meta.Reset(); - cache._conflicts.Clear(); + Calculating = true; + try + { + cache.ResolvedFiles.Clear(); + cache.Meta.Reset(); + cache._conflicts.Clear(); - // Add all forced redirects. - foreach (var tempMod in _tempMods.ModsForAllCollections - .Concat(_tempMods.Mods.TryGetValue(collection, out var list) - ? list - : Array.Empty())) - cache.AddMod(tempMod, false); + // Add all forced redirects. + foreach (var tempMod in _tempMods.ModsForAllCollections + .Concat(_tempMods.Mods.TryGetValue(collection, out var list) + ? list + : Array.Empty())) + cache.AddMod(tempMod, false); - foreach (var mod in _modStorage) - cache.AddMod(mod, false); + foreach (var mod in _modStorage) + cache.AddMod(mod, false); - cache.AddMetaFiles(); + cache.AddMetaFiles(); - ++collection.ChangeCounter; + ++collection.ChangeCounter; - MetaFileManager.ApplyDefaultFiles(collection); + MetaFileManager.ApplyDefaultFiles(collection); + } + finally + { + Calculating = false; + } } private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) From 4cd03f2198cb93a3f1205b30f466232380660aa9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 20:50:10 +0200 Subject: [PATCH 0947/2451] Work around some file picker stuff for textures. --- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 14 ++++++++++---- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 514dcb02..d235c417 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -45,7 +45,8 @@ public partial class ModEditWindow _fileDialog, _config.DefaultModImportPath); var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); - tex.PathSelectBox(_dalamud, "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", + tex.PathSelectBox(_dalamud, "##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, _mod.ModPath.FullName.Length + 1); if (tex == _left) @@ -84,6 +85,8 @@ public partial class ModEditWindow "Add the appropriate number of MipMaps to the file."); } + private bool _forceTextureStartPath = true; + private void DrawOutputChild(Vector2 size, Vector2 imageSize) { using var child = ImRaii.Child("Output", size, true); @@ -102,7 +105,8 @@ public partial class ModEditWindow { if (a) _center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, false); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; } if (ImGui.Button("Save as DDS", -Vector2.UnitX)) @@ -112,7 +116,8 @@ public partial class ModEditWindow { if (a) _center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, false); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; } ImGui.NewLine(); @@ -124,7 +129,8 @@ public partial class ModEditWindow { if (a) _center.SaveAsPng(b); - }, _mod!.ModPath.FullName, false); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; } ImGui.NewLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 211acc85..ba6cc0aa 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -59,6 +59,7 @@ public partial class ModEditWindow : Window, IDisposable _shaderPackageTab.Reset(); _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); UpdateModels(); + _forceTextureStartPath = true; } public void ChangeOption(SubMod? subMod) From c911977b5ea1d0db68d7ee5390dedc925aef6956 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 2 May 2023 18:52:24 +0000 Subject: [PATCH 0948/2451] [CI] Updating repo.json for refs/tags/0.7.0.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c2595932..5428b5f5 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.7", - "TestingAssemblyVersion": "0.7.0.7", + "AssemblyVersion": "0.7.0.8", + "TestingAssemblyVersion": "0.7.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 4b1443ec930c54bd830ab89eba38a7927163c928 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 May 2023 21:13:01 +0200 Subject: [PATCH 0949/2451] Fuck --- Penumbra/Collections/Cache/CollectionCache.cs | 2 ++ Penumbra/Collections/Cache/CollectionCacheManager.cs | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 1d2ff39c..af037ce5 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -29,6 +29,8 @@ public class CollectionCache : IDisposable public readonly MetaCache Meta; public readonly Dictionary> _conflicts = new(); + public bool Calculating; + public IEnumerable> AllConflicts => _conflicts.Values; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index d5df7d7d..e415dc62 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -28,7 +28,6 @@ public class CollectionCacheManager : IDisposable internal readonly MetaFileManager MetaFileManager; public int Count { get; private set; } - public bool Calculating { get; private set; } public IEnumerable Active => _storage.Where(c => c.HasCache); @@ -117,10 +116,10 @@ public class CollectionCacheManager : IDisposable private void FullRecalculation(ModCollection collection) { var cache = collection._cache; - if (cache == null || Calculating) + if (cache == null || cache.Calculating) return; - Calculating = true; + cache.Calculating = true; try { cache.ResolvedFiles.Clear(); @@ -145,7 +144,7 @@ public class CollectionCacheManager : IDisposable } finally { - Calculating = false; + cache.Calculating = false; } } From 6316458613f15209b244a98c4156778b64bdcbb9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 2 May 2023 19:15:34 +0000 Subject: [PATCH 0950/2451] [CI] Updating repo.json for refs/tags/0.7.0.9 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5428b5f5..9e991904 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.8", - "TestingAssemblyVersion": "0.7.0.8", + "AssemblyVersion": "0.7.0.9", + "TestingAssemblyVersion": "0.7.0.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From beb777e3cdc0d52aa8963beb6dcd23c9fd043768 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 3 May 2023 16:56:38 +0200 Subject: [PATCH 0951/2451] Add some more debugging fuckery for the import popup fuckery. --- Penumbra/Configuration.cs | 1 + Penumbra/Mods/Manager/ModImportManager.cs | 18 +++++- Penumbra/UI/ImportPopup.cs | 23 ++++--- Penumbra/UI/Tabs/DebugTab.cs | 74 ++++++++++++++++++++++- Penumbra/UI/WindowSystem.cs | 14 +++-- 5 files changed, 110 insertions(+), 20 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index cc10e3a7..ff5df1ab 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -49,6 +49,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool DebugSeparateWindow = false; #if DEBUG public bool DebugMode { get; set; } = true; #else diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 84aa2c7f..5a9fa319 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -23,6 +23,14 @@ public class ModImportManager : IDisposable private TexToolsImporter? _import; + + internal IEnumerable ModBatches + => _modsToUnpack; + + internal IEnumerable AddableMods + => _modsToAdd; + + public ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) { _modManager = modManager; @@ -43,9 +51,9 @@ public class ModImportManager : IDisposable Penumbra.ChatService.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", "Warning", NotificationType.Warning); return false; - }).Select(s => new FileInfo(s)).ToArray(); + Penumbra.Log.Debug($"Unpacking mods: {string.Join("\n\t", files.Select(f => f.FullName))}."); if (files.Length == 0) return; @@ -62,10 +70,13 @@ public class ModImportManager : IDisposable } public void AddUnpack(IEnumerable paths) - => _modsToUnpack.Enqueue(paths.ToArray()); + => AddUnpack(paths.ToArray()); public void AddUnpack(params string[] paths) - => _modsToUnpack.Enqueue(paths); + { + Penumbra.Log.Debug($"Adding mods to install: {string.Join("\n\t", paths)}"); + _modsToUnpack.Enqueue(paths); + } public void ClearImport() { @@ -117,6 +128,7 @@ public class ModImportManager : IDisposable } else if (dir != null) { + Penumbra.Log.Debug($"Adding newly installed mod to queue: {dir.FullName}"); _modsToAdd.Enqueue(dir); } } diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 85b4f650..064e1255 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -11,10 +11,15 @@ namespace Penumbra.UI; /// Draw the progress information for import. public sealed class ImportPopup : Window { + public const string WindowLabel = "Penumbra Import Status"; + private readonly ModImportManager _modImportManager; + public bool WasDrawn { get; private set; } + public bool PopupWasDrawn { get; private set; } + public ImportPopup(ModImportManager modImportManager) - : base("Penumbra Import Status", + : base(WindowLabel, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground @@ -22,7 +27,7 @@ public sealed class ImportPopup : Window | ImGuiWindowFlags.NoInputs, true) { _modImportManager = modImportManager; - IsOpen = true; + IsOpen = true; SizeConstraints = new WindowSizeConstraints { MinimumSize = Vector2.Zero, @@ -30,15 +35,16 @@ public sealed class ImportPopup : Window }; } - public override void PreDraw() + public override void PreOpenCheck() { + WasDrawn = false; + PopupWasDrawn = false; _modImportManager.TryUnpacking(); - ImGui.SetNextWindowCollapsed(false, ImGuiCond.Always); - IsOpen = true; } public override void Draw() { + WasDrawn = true; if (!_modImportManager.IsImporting(out var import)) return; @@ -47,12 +53,13 @@ public sealed class ImportPopup : Window ImGui.OpenPopup(importPopup); var display = ImGui.GetIO().DisplaySize; - var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 8; - var size = new Vector2(width * 2, height); + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 8; + var size = new Vector2(width * 2, height); ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); ImGui.SetNextWindowSize(size); using var popup = ImRaii.Popup(importPopup, ImGuiWindowFlags.Modal); + PopupWasDrawn = true; using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { if (child) diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 0e821ab8..ae8b047d 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Linq; using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -13,6 +15,7 @@ using Penumbra.Api; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; +using Penumbra.Import.Structs; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; @@ -28,7 +31,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage namespace Penumbra.UI.Tabs; -public class DebugTab : ITab +public class DebugTab : Window, ITab { private readonly StartTracker _timer; private readonly PerformanceTracker _performance; @@ -50,14 +53,23 @@ public class DebugTab : ITab private readonly SubfileHelper _subfileHelper; private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly CutsceneService _cutsceneService; + private readonly ModImportManager _modImporter; + private readonly ImportPopup _importPopup; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, - CutsceneService cutsceneService) + CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup) + : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { + IsOpen = true; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(200, 200), + MaximumSize = new Vector2(2000, 2000), + }; _timer = timer; _performance = performance; _config = config; @@ -78,13 +90,15 @@ public class DebugTab : ITab _subfileHelper = subfileHelper; _identifiedCollectionCache = identifiedCollectionCache; _cutsceneService = cutsceneService; + _modImporter = modImporter; + _importPopup = importPopup; } public ReadOnlySpan Label => "Debug"u8; public bool IsVisible - => _config.DebugMode; + => _config.DebugMode && !_config.DebugSeparateWindow; #if DEBUG private const string DebugVersionString = "(Debug)"; @@ -127,6 +141,14 @@ public class DebugTab : ITab if (!ImGui.CollapsingHeader("General")) return; + var separateWindow = _config.DebugSeparateWindow; + if (ImGui.Checkbox("Draw as Separate Window", ref separateWindow)) + { + IsOpen = true; + _config.DebugSeparateWindow = separateWindow; + _config.Save(); + } + using (var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit)) { if (table) @@ -178,6 +200,40 @@ public class DebugTab : ITab } } } + + using (var tree = TreeNode("Mod Import")) + { + if (tree) + { + using var table = Table("##DebugModImport", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + var importing = _modImporter.IsImporting(out var importer); + PrintValue("Is Importing", importing.ToString()); + PrintValue("Importer State", (importer?.State ?? ImporterState.None).ToString()); + PrintValue("Import Window Was Drawn", _importPopup.WasDrawn.ToString()); + PrintValue("Import Popup Was Drawn", _importPopup.PopupWasDrawn.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Import Batches"); + ImGui.TableNextColumn(); + foreach (var (batch, index) in _modImporter.ModBatches.WithIndex()) + { + foreach (var mod in batch) + PrintValue(index.ToString(), mod); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Addable Mods"); + ImGui.TableNextColumn(); + foreach (var mod in _modImporter.AddableMods) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Name); + } + } + } + } } private void DrawPerformanceTab() @@ -655,4 +711,16 @@ public class DebugTab : ITab ImGui.TableNextColumn(); ImGui.TextUnformatted(value); } + + public override void Draw() + => DrawContent(); + + public override bool DrawConditions() + => _config.DebugMode && _config.DebugSeparateWindow; + + public override void OnClose() + { + _config.DebugSeparateWindow = false; + _config.Save(); + } } diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 4756accf..bde27bfc 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Tabs; namespace Penumbra; @@ -16,17 +17,18 @@ public class PenumbraWindowSystem : IDisposable public readonly PenumbraChangelog Changelog; public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, - LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup) + LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); _windowSystem.AddWindow(importPopup); + _windowSystem.AddWindow(debugTab); _uiBuilder.OpenConfigUi += Window.Toggle; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; From 4d9c5bdb8dfafcea05e8a0687a8a5b50aa76bfe9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 4 May 2023 10:00:48 +0200 Subject: [PATCH 0952/2451] Add some more flags to the popup window. --- Penumbra/UI/ImportPopup.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 064e1255..f97213c1 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -24,10 +24,17 @@ public sealed class ImportPopup : Window | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoInputs, true) + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoNavFocus + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoDocking + | ImGuiWindowFlags.NoTitleBar, true) { - _modImportManager = modImportManager; - IsOpen = true; + _modImportManager = modImportManager; + IsOpen = true; + RespectCloseHotkey = false; + Collapsed = false; SizeConstraints = new WindowSizeConstraints { MinimumSize = Vector2.Zero, @@ -40,6 +47,7 @@ public sealed class ImportPopup : Window WasDrawn = false; PopupWasDrawn = false; _modImportManager.TryUnpacking(); + IsOpen = true; } public override void Draw() From e5c474337413b454dc11ed50371c0bcd5c1dd627 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 4 May 2023 10:21:37 +0000 Subject: [PATCH 0953/2451] [CI] Updating repo.json for refs/tags/0.7.0.10 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9e991904..d81eb71e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.9", - "TestingAssemblyVersion": "0.7.0.9", + "AssemblyVersion": "0.7.0.10", + "TestingAssemblyVersion": "0.7.0.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.9/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6f6b72e7aa4d1774b1e0c142cc99c01647d38dc4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 May 2023 16:17:25 +0200 Subject: [PATCH 0954/2451] Move Creation of Caches to constructor thread. --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 4 +--- Penumbra/Penumbra.cs | 5 +++-- .../ResourceWatcher/{ResourceWatcher.Record.cs => Record.cs} | 0 .../{ResourceWatcher.Table.cs => ResourceWatcherTable.cs} | 0 4 files changed, 4 insertions(+), 5 deletions(-) rename Penumbra/UI/ResourceWatcher/{ResourceWatcher.Record.cs => Record.cs} (100%) rename Penumbra/UI/ResourceWatcher/{ResourceWatcher.Table.cs => ResourceWatcherTable.cs} (100%) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index e415dc62..7850cdce 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -57,8 +57,6 @@ public class CollectionCacheManager : IDisposable if (!MetaFileManager.CharacterUtility.Ready) MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; - - CreateNecessaryCaches(); } public void Dispose() @@ -305,7 +303,7 @@ public class CollectionCacheManager : IDisposable /// Cache handling. Usually recreate caches on the next framework tick, /// but at launch create all of them at once. /// - private void CreateNecessaryCaches() + public void CreateNecessaryCaches() { var tasks = _active.SpecialAssignments.Select(p => p.Value) .Concat(_active.Individuals.Select(p => p.Collection)) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ffd32df8..c70d688a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -49,7 +49,7 @@ public class Penumbra : IDalamudPlugin public Penumbra(DalamudPluginInterface pluginInterface) { try - { + { var startTimer = new StartTracker(); using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); @@ -67,7 +67,8 @@ public class Penumbra : IDalamudPlugin _redrawService = _services.GetRequiredService(); _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. + _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { _services.GetRequiredService(); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs similarity index 100% rename from Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs rename to Penumbra/UI/ResourceWatcher/Record.cs diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs similarity index 100% rename from Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs rename to Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs From 8e5ed60c7994f1ef091048572fec5ccd36578374 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 May 2023 16:17:53 +0200 Subject: [PATCH 0955/2451] Add hook for dismount sounds..? --- .../PathResolving/AnimationHookService.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 2add5771..b9250502 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -47,6 +47,7 @@ public unsafe class AnimationHookService : IDisposable _scheduleClipUpdateHook.Enable(); _unkMountAnimationHook.Enable(); _unkParasolAnimationHook.Enable(); + _dismountHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -103,6 +104,7 @@ public unsafe class AnimationHookService : IDisposable _scheduleClipUpdateHook.Dispose(); _unkMountAnimationHook.Dispose(); _unkParasolAnimationHook.Dispose(); + _dismountHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -333,4 +335,30 @@ public unsafe class AnimationHookService : IDisposable _unkParasolAnimationHook!.Original(drawObject, unk1); _animationLoadData.Value = last; } + + [Signature("E8 ?? ?? ?? ?? F6 43 ?? ?? 74 ?? 48 8B CB", DetourName = nameof(DismountDetour))] + private readonly Hook _dismountHook = null; + + private delegate void DismountDelegate(nint a1, nint a2); + + private void DismountDetour(nint a1, nint a2) + { + if (a1 == nint.Zero) + { + _dismountHook!.Original(a1, a2); + return; + } + + var gameObject = *(GameObject**)(a1 + 8); + if (gameObject == null) + { + _dismountHook!.Original(a1, a2); + return; + } + + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); + _dismountHook!.Original(a1, a2); + _animationLoadData.Value = last; + } } From d403f4425627b6dfaf8285656eecd35430fde0d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 May 2023 16:19:08 +0200 Subject: [PATCH 0956/2451] Add start of mod merger. --- Penumbra/Communication/ModPathChanged.cs | 5 +- Penumbra/Mods/Editor/DuplicateManager.cs | 20 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 1 + Penumbra/Mods/Editor/ModEditor.cs | 1 + Penumbra/Mods/Editor/ModFileCollection.cs | 3 +- Penumbra/Mods/Editor/ModFileEditor.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 361 ++++++++++++++++++++ Penumbra/Mods/Manager/ModOptionEditor.cs | 31 +- Penumbra/Mods/Manager/ModStorage.cs | 15 + Penumbra/Services/ServiceManager.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 10 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 104 ++++++ 12 files changed, 538 insertions(+), 20 deletions(-) create mode 100644 Penumbra/Mods/Editor/ModMerger.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModMergeTab.cs diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 608c10eb..0a362267 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -37,13 +37,14 @@ public sealed class ModPathChanged : EventWrapper ModManager = 0, + /// + ModMerger = 0, + /// CollectionStorage = 10, /// CollectionCacheManagerRemoval = 100, - - } public ModPathChanged() : base(nameof(ModPathChanged)) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 62edb9fb..03321cc3 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -8,22 +8,19 @@ using System.Threading.Tasks; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class DuplicateManager { private readonly SaveService _saveService; private readonly ModManager _modManager; private readonly SHA256 _hasher = SHA256.Create(); - private readonly ModFileCollection _files; private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - public DuplicateManager(ModFileCollection files, ModManager modManager, SaveService saveService) + public DuplicateManager(ModManager modManager, SaveService saveService) { - _files = files; - _modManager = modManager; + _modManager = modManager; _saveService = saveService; } @@ -43,7 +40,7 @@ public class DuplicateManager Task.Run(() => CheckDuplicates(filesTmp)); } - public void DeleteDuplicates(Mod mod, ISubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) { if (!Finished || _duplicates.Count == 0) return; @@ -60,7 +57,7 @@ public class DuplicateManager _duplicates.Clear(); DeleteEmptyDirectories(mod.ModPath); - _files.UpdateAll(mod, option); + files.UpdateAll(mod, option); } public void Clear() @@ -248,9 +245,10 @@ public class DuplicateManager _modManager.Creator.ReloadMod(mod, true, out _); Finished = false; - _files.UpdateAll(mod, mod.Default); - CheckDuplicates(_files.Available.OrderByDescending(f => f.FileSize).ToArray()); - DeleteDuplicates(mod, mod.Default, false); + var files = new ModFileCollection(); + files.UpdateAll(mod, mod.Default); + CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray()); + DeleteDuplicates(files, mod, mod.Default, false); } catch (Exception e) { diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index dc32869f..f616d128 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.Mods.Editor; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index c19b9962..a874f629 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,6 +1,7 @@ using System; using System.IO; using OtterGui; +using Penumbra.Mods.Editor; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index fa3d5614..5ef290dc 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -3,11 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; -using Microsoft.Win32; using OtterGui; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class ModFileCollection : IDisposable { diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 17505550..4f9d22b3 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs new file mode 100644 index 00000000..29ed210e --- /dev/null +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; +using OtterGui; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.UI.ModsTab; + +namespace Penumbra.Mods.Editor; + +public class ModMerger : IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ModOptionEditor _editor; + private readonly ModFileSystemSelector _selector; + private readonly DuplicateManager _duplicates; + private readonly ModManager _mods; + private readonly ModCreator _creator; + + public Mod? MergeFromMod { get; private set; } + public Mod? MergeToMod; + public string OptionGroupName = "Merges"; + public string OptionName = string.Empty; + + + private readonly Dictionary _fileToFile = new(); + private readonly HashSet _createdDirectories = new(); + public readonly HashSet SelectedOptions = new(); + + private int _createdGroup = -1; + private SubMod? _createdOption; + public Exception? Error { get; private set; } + + public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + CommunicatorService communicator, ModCreator creator) + { + _editor = editor; + _selector = selector; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _mods = mods; + _selector.SelectionChanged += OnSelectionChange; + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.Api); + } + + public void Dispose() + { + _selector.SelectionChanged -= OnSelectionChange; + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + } + + public IEnumerable ModsWithoutCurrent + => _mods.Where(m => m != MergeFromMod); + + public bool CanMerge + => MergeToMod != null && MergeToMod != MergeFromMod && !MergeFromMod!.HasOptions; + + public void Merge() + { + if (MergeFromMod == null || MergeToMod == null || MergeFromMod == MergeToMod) + return; + + try + { + Error = null; + DataCleanup(); + if (MergeFromMod.HasOptions) + MergeWithOptions(); + else + MergeIntoOption(OptionGroupName, OptionName); + _duplicates.DeduplicateMod(MergeToMod.ModPath); + } + catch (Exception ex) + { + Error = ex; + Penumbra.ChatService.NotificationMessage( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.:\n{ex}", "Failure", + NotificationType.Error); + FailureCleanup(); + DataCleanup(); + } + } + + private void MergeWithOptions() + { + // Not supported + } + + private void MergeIntoOption(string groupName, string optionName) + { + if (groupName.Length == 0 && optionName.Length == 0) + { + CopyFiles(MergeToMod!.ModPath); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default); + } + else if (groupName.Length * optionName.Length == 0) + { + return; + } + + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName); + if (groupCreated) + _createdGroup = groupIdx; + var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); + if (optionCreated) + _createdOption = option; + var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName); + if (!dir.Exists) + _createdDirectories.Add(dir.FullName); + dir = ModCreator.NewOptionDirectory(dir, optionName); + if (!dir.Exists) + _createdDirectories.Add(dir.FullName); + CopyFiles(dir); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option); + } + + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option) + { + var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var manips = option.ManipulationData.ToHashSet(); + foreach (var originalOption in mergeOptions) + { + foreach (var manip in originalOption.Manipulations) + { + if (!manips.Add(manip)) + throw new Exception( + $"Could not add meta manipulation {manip} from {originalOption.FullName} to {option.FullName} because another manipulation of the same data already exists in this option."); + } + + foreach (var (swapA, swapB) in originalOption.FileSwaps) + { + if (!swaps.TryAdd(swapA, swapB)) + throw new Exception( + $"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); + } + + foreach (var (gamePath, relPath) in originalOption.Files) + { + if (!_fileToFile.TryGetValue(relPath.FullName, out var newFile)) + throw new Exception( + $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + if (!redirections.TryAdd(gamePath, new FullPath(newFile))) + throw new Exception( + $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + } + } + + _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections); + _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps); + _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips); + } + + private void CopyFiles(DirectoryInfo directory) + { + directory = Directory.CreateDirectory(directory.FullName); + foreach (var file in MergeFromMod!.ModPath.EnumerateDirectories() + .Where(d => !d.IsHidden()) + .SelectMany(FileExtensions.EnumerateNonHiddenFiles)) + { + var path = Path.GetRelativePath(MergeFromMod.ModPath.FullName, file.FullName); + path = Path.Combine(directory.FullName, path); + var finalDir = Path.GetDirectoryName(path)!; + var dir = finalDir; + while (!dir.IsNullOrEmpty()) + { + if (!Directory.Exists(dir)) + _createdDirectories.Add(dir); + else + break; + + dir = Path.GetDirectoryName(dir); + } + + Directory.CreateDirectory(finalDir); + file.CopyTo(path); + Penumbra.Log.Verbose($"[Merger] Copied file {file.FullName} to {path}."); + _fileToFile.Add(file.FullName, path); + } + } + + public void SplitIntoMod(string modName) + { + var mods = SelectedOptions.ToList(); + if (mods.Count == 0) + return; + + Error = null; + DirectoryInfo? dir = null; + Mod? result = null; + try + { + dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); + if (dir == null) + throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); + + _mods.AddMod(dir); + result = _mods[^1]; + if (mods.Count == 1) + { + var files = CopySubModFiles(mods[0], dir); + _editor.OptionSetFiles(result, -1, 0, files); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); + _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + } + else + { + foreach (var originalOption in mods) + { + var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx]; + if (originalOption.IsDefault) + { + var files = CopySubModFiles(mods[0], dir); + _editor.OptionSetFiles(result, -1, 0, files); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); + _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + } + else + { + var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var folder = Path.Combine(dir.FullName, group.Name, option.Name); + var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); + _editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); + _editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); + _editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); + } + } + } + } + catch (Exception e) + { + Error = e; + if (result != null) + _mods.DeleteMod(result); + else if (dir != null) + try + { + Directory.Delete(dir.FullName); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not clean up after failure to split options into new mod {modName}:\n{ex}"); + } + } + } + + private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) + { + var ret = new Dictionary(option.FileData.Count); + var parentPath = ((Mod)option.ParentMod).ModPath.FullName; + foreach (var (path, file) in option.FileData) + { + var target = Path.GetRelativePath(parentPath, file.FullName); + target = Path.Combine(newMod.FullName, target); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file.FullName, target); + Penumbra.Log.Verbose($"[Splitter] Copied file {file.FullName} to {target}."); + ret.Add(path, new FullPath(target)); + } + + return ret; + } + + private void DataCleanup() + { + _fileToFile.Clear(); + _createdDirectories.Clear(); + _createdOption = null; + _createdGroup = -1; + } + + private void FailureCleanup() + { + if (_createdGroup >= 0 && _createdGroup < MergeToMod!.Groups.Count) + _editor.DeleteModGroup(MergeToMod!, _createdGroup); + else if (_createdOption != null) + _editor.DeleteOption(MergeToMod!, _createdOption.GroupIdx, _createdOption.OptionIdx); + + foreach (var dir in _createdDirectories) + { + if (!Directory.Exists(dir)) + continue; + + try + { + Directory.Delete(dir, true); + Penumbra.Log.Verbose($"[Merger] Deleted {dir}."); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"Could not clean up after failing to merge {MergeFromMod!.Name} into {MergeToMod!.Name}, unable to delete {dir}:\n{ex}"); + } + } + + foreach (var (_, file) in _fileToFile) + { + if (!File.Exists(file)) + continue; + + try + { + File.Delete(file); + Penumbra.Log.Verbose($"[Merger] Deleted {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"Could not clean up after failing to merge {MergeFromMod!.Name} into {MergeToMod!.Name}, unable to delete {file}:\n{ex}"); + } + } + } + + private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state) + { + if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text) + OptionName = newSelection?.Name.Text ?? string.Empty; + + if (MergeToMod == newSelection) + MergeToMod = null; + + SelectedOptions.Clear(); + MergeFromMod = newSelection; + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + { + switch (type) + { + case ModPathChangeType.Deleted: + { + if (mod == MergeFromMod) + { + SelectedOptions.Clear(); + MergeFromMod = null; + } + + if (mod == MergeToMod) + MergeToMod = null; + break; + } + case ModPathChangeType.StartingReload: + SelectedOptions.Clear(); + MergeFromMod = null; + MergeToMod = null; + break; + case ModPathChangeType.Reloaded: + MergeFromMod = _selector.Selected; + break; + } + } +} diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index c30d90c3..a5e77c37 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -87,7 +87,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } - /// Add a new mod, empty option group of the given type and name. + /// Add a new, empty option group of the given type and name. public void AddModGroup(Mod mod, GroupType type, string newName) { if (!VerifyFileName(mod, null, newName, true)) @@ -110,6 +110,20 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } + /// Add a new mod, empty option group of the given type and name if it does not exist already. + public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName) + { + var idx = mod.Groups.IndexOf(g => g.Name == newName); + if (idx >= 0) + return (mod.Groups[idx], idx, false); + + AddModGroup(mod, type, newName); + if (mod.Groups[^1].Name != newName) + throw new Exception($"Could not create new mod group with name {newName}."); + + return (mod.Groups[^1], mod.Groups.Count - 1, true); + } + /// Delete a given option group. Fires an event to prepare before actually deleting. public void DeleteModGroup(Mod mod, int groupIdx) { @@ -242,6 +256,21 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } + /// Add a new empty option of the given name for the given group if it does not exist already. + public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName) + { + var group = mod.Groups[groupIdx]; + var idx = group.IndexOf(o => o.Name == newName); + if (idx >= 0) + return ((SubMod)group[idx], false); + + AddOption(mod, groupIdx, newName); + if (group[^1].Name != newName) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + + return ((SubMod)group[^1], true); + } + /// Add an existing option to a given group with a given priority. public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) { diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 5e8999d7..8421d6e2 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -2,9 +2,24 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using OtterGui.Classes; +using OtterGui.Widgets; namespace Penumbra.Mods.Manager; +public class ModCombo : FilterComboCache +{ + protected override bool IsVisible(int globalIndex, LowerString filter) + => Items[globalIndex].Name.Contains(filter); + + protected override string ToString(Mod obj) + => obj.Name.Text; + + public ModCombo(Func> generator) + : base(generator) + { } +} + public class ModStorage : IReadOnlyList { /// The actual list of mods. diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 6d1bf710..3b9e3e31 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; @@ -13,6 +14,7 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; @@ -158,7 +160,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddModEditor(this IServiceCollection services) => services.AddSingleton() @@ -168,6 +171,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index ba6cc0aa..47d8f770 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -35,6 +35,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly MetaFileManager _metaFileManager; private readonly ActiveCollections _activeCollections; private readonly StainService _stainService; + private readonly ModMergeTab _modMergeTab; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -144,6 +145,7 @@ public partial class ModEditWindow : Window, IDisposable DrawFileTab(); DrawMetaTab(); DrawSwapTab(); + _modMergeTab.Draw(); DrawMissingFilesTab(); DrawDuplicatesTab(); DrawQuickImportTab(); @@ -311,7 +313,7 @@ public partial class ModEditWindow : Window, IDisposable } if (ImGui.Button("Delete and Redirect Duplicates")) - _editor.Duplicates.DeleteDuplicates(_editor.Mod!, _editor.Option!, true); + _editor.Duplicates.DeleteDuplicates(_editor.Files, _editor.Mod!, _editor.Option!, true); if (_editor.Duplicates.SavedSpace > 0) { @@ -419,7 +421,8 @@ public partial class ModEditWindow : Window, IDisposable if (otherSwaps > 0) { ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"There are {otherSwaps} file swaps configured in other options.", Vector2.Zero, ColorId.RedundantAssignment.Value()); + ImGuiUtil.DrawTextButton($"There are {otherSwaps} file swaps configured in other options.", Vector2.Zero, + ColorId.RedundantAssignment.Value()); } using var child = ImRaii.Child("##swaps", -Vector2.One, true); @@ -509,7 +512,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud) + StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab) : base(WindowBaseLabel) { _performance = performance; @@ -520,6 +523,7 @@ public partial class ModEditWindow : Window, IDisposable _stainService = stainService; _activeCollections = activeCollections; _dalamud = dalamud; + _modMergeTab = modMergeTab; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs new file mode 100644 index 00000000..c4cf1fb4 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp.ColorSpaces; + +namespace Penumbra.UI.AdvancedWindow; + +public class ModMergeTab +{ + private readonly ModMerger _modMerger; + private readonly ModCombo _modCombo; + + private string _newModName = string.Empty; + + public ModMergeTab(ModMerger modMerger) + { + _modMerger = modMerger; + _modCombo = new ModCombo(() => _modMerger.ModsWithoutCurrent.ToList()); + } + + public void Draw() + { + if (_modMerger.MergeFromMod == null) + return; + + using var tab = ImRaii.TabItem("Merge Mods"); + if (!tab) + return; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod.Name} into "); + ImGui.SameLine(); + DrawCombo(); + + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGui.SameLine(); + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + + if (ImGuiUtil.DrawDisabledButton("Merge", Vector2.Zero, string.Empty, !_modMerger.CanMerge)) + _modMerger.Merge(); + + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing()})) + { + foreach (var (option, idx) in _modMerger.MergeFromMod.AllSubMods.WithIndex()) + { + using var id = ImRaii.PushId(idx); + var selected = _modMerger.SelectedOptions.Contains(option); + ImGui.TableNextColumn(); + if (ImGui.Checkbox("##check", ref selected)) + { + if (selected) + _modMerger.SelectedOptions.Add(option); + else + _modMerger.SelectedOptions.Remove(option); + } + + if (option.IsDefault) + { + ImGuiUtil.DrawTableColumn(option.FullName); + ImGui.TableNextColumn(); + } + else + { + ImGuiUtil.DrawTableColumn(option.ParentMod.Groups[option.GroupIdx].Name); + ImGuiUtil.DrawTableColumn(option.Name); + } + + ImGuiUtil.DrawTableColumn(option.FileData.Count.ToString()); + ImGuiUtil.DrawTableColumn(option.FileSwapData.Count.ToString()); + ImGuiUtil.DrawTableColumn(option.Manipulations.Count.ToString()); + + } + } + ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); + if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) + _modMerger.SplitIntoMod(_newModName); + + if (_modMerger.Error != null) + { + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); + } + } + + private void DrawCombo() + { + _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? string.Empty, string.Empty, + 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + _modMerger.MergeToMod = _modCombo.CurrentSelection; + } +} From f9cc88cbb0645113ad24f6e25e086ba665d40085 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 6 May 2023 12:46:19 +0200 Subject: [PATCH 0957/2451] Add option to configure minimum window size. --- Penumbra/CommandHandler.cs | 12 +++++++ Penumbra/Configuration.cs | 7 ++++- Penumbra/UI/ConfigWindow.cs | 10 +++--- Penumbra/UI/Tabs/SettingsTab.cs | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 03647d70..9555f772 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -79,6 +79,7 @@ public class CommandHandler : IDisposable "reload" => Reload(arguments), "redraw" => Redraw(arguments), "lockui" => SetUiLockState(arguments), + "size" => SetUiMinimumSize(arguments), "debug" => SetDebug(arguments), "collection" => SetCollection(arguments), "mod" => SetMod(arguments), @@ -110,6 +111,7 @@ public class CommandHandler : IDisposable _chat.Print(new SeStringBuilder() .AddCommand("lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state.") .BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("size", "Reset the minimum config window size to its default values.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state.").BuiltString); _chat.Print(new SeStringBuilder() @@ -203,6 +205,16 @@ public class CommandHandler : IDisposable return true; } + private bool SetUiMinimumSize(string _) + { + if (_config.MinimumSize.X == Configuration.Constants.MinimumSizeX && _config.MinimumSize.Y == Configuration.Constants.MinimumSizeY) + return false; + _config.MinimumSize.X = Configuration.Constants.MinimumSizeX; + _config.MinimumSize.Y = Configuration.Constants.MinimumSizeY; + _config.Save(); + return true; + } + private bool SetCollection(string arguments) { if (arguments.Length == 0) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ff5df1ab..697d7a7d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Configuration; using Newtonsoft.Json; using OtterGui; @@ -49,7 +50,9 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; - public bool DebugSeparateWindow = false; + public bool DebugSeparateWindow = false; + public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); + #if DEBUG public bool DebugMode { get; set; } = true; #else @@ -144,6 +147,8 @@ public class Configuration : IPluginConfiguration, ISavable public const int MaxScaledSize = 80; public const int DefaultScaledSize = 20; public const int MinScaledSize = 5; + public const int MinimumSizeX = 900; + public const int MinimumSizeY = 675; public static readonly ISortMode[] ValidSortModes = { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 12144957..0007f53a 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -42,11 +42,6 @@ public sealed class ConfigWindow : Window _validityChecker = checker; RespectCloseHotkey = true; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(900, 675), - MaximumSize = new Vector2(4096, 2160), - }; tutorial.UpdateTutorialStep(); IsOpen = _config.DebugMode; } @@ -66,6 +61,11 @@ public sealed class ConfigWindow : Window Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; else Flags &= ~(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = _config.MinimumSize, + MaximumSize = new Vector2(4096, 2160), + }; } public override void Draw() diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 6983afb7..16443239 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -38,6 +38,9 @@ public class SettingsTab : ITab private readonly DalamudServices _dalamud; private readonly HttpApi _httpApi; + private int _minimumX = int.MaxValue; + private int _minimumY = int.MaxValue; + public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi) @@ -637,6 +640,7 @@ public class SettingsTab : ITab if (!header) return; + DrawMinimumDimensionConfig(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); @@ -652,6 +656,58 @@ public class SettingsTab : ITab ImGui.NewLine(); } + /// Draw two integral inputs for minimum dimensions of this window. + private void DrawMinimumDimensionConfig() + { + var x = _minimumX == int.MaxValue ? (int)_config.MinimumSize.X : _minimumX; + var y = _minimumY == int.MaxValue ? (int)_config.MinimumSize.Y : _minimumY; + + var warning = x < Configuration.Constants.MinimumSizeX + ? y < Configuration.Constants.MinimumSizeY + ? "Size is smaller than default: This may look undesirable." + : "Width is smaller than default: This may look undesirable." + : y < Configuration.Constants.MinimumSizeY + ? "Height is smaller than default: This may look undesirable." + : string.Empty; + var buttonWidth = UiHelpers.InputTextWidth.X / 2.5f; + ImGui.SetNextItemWidth(buttonWidth); + if (ImGui.DragInt("##xMinSize", ref x, 0.1f, 500, 1500)) + _minimumX = x; + var edited = ImGui.IsItemDeactivatedAfterEdit(); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(buttonWidth); + if (ImGui.DragInt("##yMinSize", ref y, 0.1f, 300, 1500)) + _minimumY = y; + edited |= ImGui.IsItemDeactivatedAfterEdit(); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Reset##resetMinSize", new Vector2(buttonWidth / 2 - ImGui.GetStyle().ItemSpacing.X * 2, 0), + $"Reset minimum dimensions to ({Configuration.Constants.MinimumSizeX}, {Configuration.Constants.MinimumSizeY}).", + x == Configuration.Constants.MinimumSizeX && y == Configuration.Constants.MinimumSizeY)) + { + x = Configuration.Constants.MinimumSizeX; + y = Configuration.Constants.MinimumSizeY; + edited = true; + } + + ImGuiUtil.LabeledHelpMarker("Minimum Window Dimensions", + "Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended."); + + if (warning.Length > 0) + ImGuiUtil.DrawTextButton(warning, UiHelpers.InputTextWidth, Colors.PressEnterWarningBg); + else + ImGui.NewLine(); + + if (!edited) + return; + + _config.MinimumSize = new Vector2(x, y); + _minimumX = int.MaxValue; + _minimumY = int.MaxValue; + _config.Save(); + } + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { From e8eff51d84a82c6642634c64ff9f04abb3b34b7a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 6 May 2023 12:47:03 +0200 Subject: [PATCH 0958/2451] Add hook for sounds loaded by weapons or something. Please cease this nonsense, modders! --- .../PathResolving/AnimationHookService.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index b9250502..8447dfa7 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -48,6 +48,7 @@ public unsafe class AnimationHookService : IDisposable _unkMountAnimationHook.Enable(); _unkParasolAnimationHook.Enable(); _dismountHook.Enable(); + _vfxWeaponHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -105,6 +106,7 @@ public unsafe class AnimationHookService : IDisposable _unkMountAnimationHook.Dispose(); _unkParasolAnimationHook.Dispose(); _dismountHook.Dispose(); + _vfxWeaponHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -337,7 +339,7 @@ public unsafe class AnimationHookService : IDisposable } [Signature("E8 ?? ?? ?? ?? F6 43 ?? ?? 74 ?? 48 8B CB", DetourName = nameof(DismountDetour))] - private readonly Hook _dismountHook = null; + private readonly Hook _dismountHook = null!; private delegate void DismountDelegate(nint a1, nint a2); @@ -345,20 +347,41 @@ public unsafe class AnimationHookService : IDisposable { if (a1 == nint.Zero) { - _dismountHook!.Original(a1, a2); + _dismountHook.Original(a1, a2); return; } var gameObject = *(GameObject**)(a1 + 8); if (gameObject == null) { - _dismountHook!.Original(a1, a2); + _dismountHook.Original(a1, a2); return; } var last = _animationLoadData.Value; _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - _dismountHook!.Original(a1, a2); + _dismountHook.Original(a1, a2); _animationLoadData.Value = last; } + + [Signature("48 89 6C 24 ?? 41 54 41 56 41 57 48 81 EC", DetourName = nameof(VfxWeaponDetour))] + private readonly Hook _vfxWeaponHook = null!; + + private delegate nint VfxWeaponDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + + private nint VfxWeaponDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + { + if (a6 == nint.Zero) + return _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + + var drawObject = ((DrawObject**)a6)[1]; + if (drawObject == null) + return _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); + var ret = _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + _animationLoadData.Value = last; + return ret; + } } From 5ba43c1b19d073faa26eaea7e3be75b754b46baf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 17:15:19 +0200 Subject: [PATCH 0959/2451] Add some delayed saves and UI for that. --- OtterGui | 2 +- .../Collections/Manager/ActiveCollections.cs | 4 +-- .../Collections/Manager/CollectionEditor.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 10 +++--- .../Collections/Manager/InheritanceManager.cs | 6 ++-- Penumbra/Configuration.cs | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 4 +-- Penumbra/Services/SaveService.cs | 15 ++++++++- Penumbra/UI/Tabs/DebugTab.cs | 33 ++++++++++++++++--- 9 files changed, 58 insertions(+), 20 deletions(-) diff --git a/OtterGui b/OtterGui index 4ef03980..6969f4c0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4ef03980803cdf5a253c041d6ed1ef26d9a9b938 +Subproject commit 6969f4c05b2fab57e0dc30ae249be7c23cd81fa4 diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 86d36abd..be46f02b 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -170,7 +170,7 @@ public class ActiveCollections : ISavable, IDisposable public void MoveIndividualCollection(int from, int to) { if (Individuals.Move(from, to)) - _saveService.QueueSave(this); + _saveService.DelaySave(this); } /// Set and create an active collection, can be used to set Default, Current, Interface, Special, or Individual collections. @@ -318,7 +318,7 @@ public class ActiveCollections : ISavable, IDisposable } else if (collectionType is not CollectionType.Temporary) { - _saveService.QueueSave(this); + _saveService.DelaySave(this); } } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 4000f504..c2b84256 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -205,7 +205,7 @@ public class CollectionEditor [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, changedCollection)); _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 4980ea71..7a60aaec 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -194,14 +194,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable var any = collection.UnusedSettings.Count > 0; ((Dictionary)collection.UnusedSettings).Clear(); if (any) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); } /// Remove a specific setting for not currently-installed mods from the given collection. public void CleanUnavailableSetting(ModCollection collection, string? setting) { if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); } /// @@ -292,7 +292,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - switch (type) + switch (type) { case ModPathChangeType.Added: foreach (var collection in this) @@ -304,7 +304,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable break; case ModPathChangeType.Moved: foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); break; } } @@ -319,7 +319,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); } } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 93dee89f..0d8bfe3c 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -88,7 +88,7 @@ public class InheritanceManager : IDisposable var parent = inheritor.DirectlyInheritsFrom[idx]; ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); ((List)parent.DirectParentOf).Remove(inheritor); - _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); @@ -100,7 +100,7 @@ public class InheritanceManager : IDisposable if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) return; - _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); @@ -116,7 +116,7 @@ public class InheritanceManager : IDisposable ((List)parent.DirectParentOf).Add(inheritor); if (invokeEvent) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 697d7a7d..f71e4d32 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -135,7 +135,7 @@ public class Configuration : IPluginConfiguration, ISavable /// Save the current configuration. public void Save() - => _saveService.QueueSave(this); + => _saveService.DelaySave(this); /// Contains some default values or boundaries for config values. public static class Constants diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 62daa9fb..79554797 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -77,7 +77,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) { if (type != FileSystemChangeType.Reload) - _saveService.QueueSave(this); + _saveService.DelaySave(this); } // Update sort order when defaulted mod names change. @@ -112,7 +112,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable break; case ModPathChangeType.Moved: - _saveService.QueueSave(this); + _saveService.DelaySave(this); break; case ModPathChangeType.Reloaded: // Nothing diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 2445392e..557cc4fc 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -29,6 +29,8 @@ public interface ISavable public class SaveService { + private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(30); + private readonly Logger _log; private readonly FrameworkManager _framework; @@ -47,7 +49,18 @@ public class SaveService public void QueueSave(ISavable value) { var file = value.ToFilename(FileNames); - _framework.RegisterDelayed(value.GetType().Name + file, () => { ImmediateSave(value); }); + _framework.RegisterOnTick($"{value.GetType().Name} ## {file}", () => { ImmediateSave(value); }); + } + + /// Queue a delayed save with the standard delay for after the delay is over. + public void DelaySave(ISavable value) + => DelaySave(value, StandardDelay); + + /// Queue a delayed save for after the delay is over. + public void DelaySave(ISavable value, TimeSpan delay) + { + var file = value.ToFilename(FileNames); + _framework.RegisterDelayed($"{value.GetType().Name} ## {file}", () => { ImmediateSave(value); }, delay); } /// Immediately trigger a save. diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index ae8b047d..75b90d29 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -10,6 +10,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -55,13 +56,14 @@ public class DebugTab : Window, ITab private readonly CutsceneService _cutsceneService; private readonly ModImportManager _modImporter; private readonly ImportPopup _importPopup; + private readonly FrameworkManager _framework; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, - CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup) + CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -92,6 +94,7 @@ public class DebugTab : Window, ITab _cutsceneService = cutsceneService; _modImporter = modImporter; _importPopup = importPopup; + _framework = framework; } public ReadOnlySpan Label @@ -209,10 +212,10 @@ public class DebugTab : Window, ITab if (table) { var importing = _modImporter.IsImporting(out var importer); - PrintValue("Is Importing", importing.ToString()); - PrintValue("Importer State", (importer?.State ?? ImporterState.None).ToString()); + PrintValue("Is Importing", importing.ToString()); + PrintValue("Importer State", (importer?.State ?? ImporterState.None).ToString()); PrintValue("Import Window Was Drawn", _importPopup.WasDrawn.ToString()); - PrintValue("Import Popup Was Drawn", _importPopup.PopupWasDrawn.ToString()); + PrintValue("Import Popup Was Drawn", _importPopup.PopupWasDrawn.ToString()); ImGui.TableNextColumn(); ImGui.TextUnformatted("Import Batches"); ImGui.TableNextColumn(); @@ -234,6 +237,28 @@ public class DebugTab : Window, ITab } } } + + using (var tree = TreeNode("Framework")) + { + if (tree) + { + using var table = Table("##DebugFramework", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + foreach(var important in _framework.Important) + PrintValue(important, "Immediate"); + + foreach (var (onTick, idx) in _framework.OnTick.WithIndex()) + PrintValue(onTick, $"{idx + 1} Tick(s) From Now"); + + foreach (var (time, name) in _framework.Delayed) + { + var span = time - DateTime.UtcNow; + PrintValue(name, $"After {span.Minutes:D2}:{span.Seconds:D2}.{span.Milliseconds / 10:D2} (+ Ticks)"); + } + } + } + } } private void DrawPerformanceTab() From e4e74376fc868f558a8f1fa9c150b845e155eeaa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 17:30:04 +0200 Subject: [PATCH 0960/2451] Order right-click context collections by name. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 86742b3d..1774cf1d 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -237,7 +237,7 @@ public sealed class CollectionPanel : IDisposable _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); } - foreach (var coll in _collections) + foreach (var coll in _collections.OrderBy(c => c.Name)) { if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); From f01b2f8754ad26dafac483c96304235b99d7a7bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 18:33:43 +0200 Subject: [PATCH 0961/2451] Reorganize advanced editing tabs a bit. --- Penumbra/Communication/ModPathChanged.cs | 3 ++ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 +-- .../AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 49 +++++++++++++------ 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 0a362267..e5b1e20c 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -40,6 +40,9 @@ public sealed class ModPathChanged : EventWrapper ModMerger = 0, + /// + ModEditWindow = 0, + /// CollectionStorage = 10, diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 4903158d..72283304 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -82,7 +82,7 @@ public class ItemSwapTab : IDisposable, ITab } public ReadOnlySpan Label - => "Item Swap (WIP)"u8; + => "Item Swap"u8; public void DrawContent() { @@ -416,10 +416,10 @@ public class ItemSwapTab : IDisposable, ITab DrawEquipmentSwap(SwapType.Ring); DrawAccessorySwap(); DrawHairSwap(); - DrawFaceSwap(); + //DrawFaceSwap(); DrawEarSwap(); DrawTailSwap(); - DrawWeaponSwap(); + //DrawWeaponSwap(); } private ImRaii.IEndObject DrawTab(SwapType newTab) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index d235c417..24ab9b90 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -162,7 +162,7 @@ public partial class ModEditWindow private void DrawTextureTab() { - using var tab = ImRaii.TabItem("Texture Import/Export"); + using var tab = ImRaii.TabItem("Textures"); if (!tab) return; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 47d8f770..9f5b1ed3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Numerics; using System.Text; @@ -10,12 +11,14 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections.Manager; +using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -27,15 +30,16 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly DalamudServices _dalamud; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly DalamudServices _dalamud; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly StainService _stainService; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -146,17 +150,20 @@ public partial class ModEditWindow : Window, IDisposable DrawMetaTab(); DrawSwapTab(); _modMergeTab.Draw(); - DrawMissingFilesTab(); DrawDuplicatesTab(); - DrawQuickImportTab(); DrawMaterialReassignmentTab(); + DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - using var tab = ImRaii.TabItem("Item Swap (WIP)"); - if (tab) - _itemSwapTab.DrawContent(); + using (var tab = ImRaii.TabItem("Item Swap")) + { + if (tab) + _itemSwapTab.DrawContent(); + } + + DrawMissingFilesTab(); } /// A row of three buttonSizes and a help marker that can be used for material suffix changing. @@ -512,7 +519,8 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab) + StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, + CommunicatorService communicator) : base(WindowBaseLabel) { _performance = performance; @@ -524,24 +532,33 @@ public partial class ModEditWindow : Window, IDisposable _activeCollections = activeCollections; _dalamud = dalamud; _modMergeTab = modMergeTab; + _communicator = communicator; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); - _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); + _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } public void Dispose() { + _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); _editor?.Dispose(); _left.Dispose(); _right.Dispose(); _center.Dispose(); } + + private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + { + if (type is ModPathChangeType.Reloaded) + ChangeMod(mod); + } } From 654978dd6450de6b40ea390863263d0776f1eab8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 20:04:23 +0200 Subject: [PATCH 0962/2451] Store last tab selected. --- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/ConfigWindow.cs | 10 ++++++++-- Penumbra/UI/Tabs/ConfigTabBar.cs | 22 ++++++++++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f71e4d32..ba8ca1cc 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; @@ -87,6 +88,7 @@ public class Configuration : IPluginConfiguration, ISavable public string QuickMoveFolder3 { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 0007f53a..8d22fe01 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -43,13 +43,14 @@ public sealed class ConfigWindow : Window RespectCloseHotkey = true; tutorial.UpdateTutorialStep(); - IsOpen = _config.DebugMode; + IsOpen = _config.DebugMode; } public void Setup(Penumbra penumbra, ConfigTabBar configTabs) { _penumbra = penumbra; _configTabs = configTabs; + SelectTab(_config.SelectedTab); } public override bool DrawConditions() @@ -108,7 +109,12 @@ public sealed class ConfigWindow : Window } else { - _configTabs.Draw(); + var type = _configTabs.Draw(); + if (type != _config.SelectedTab) + { + _config.SelectedTab = type; + _config.Save(); + } } _lastException = null; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index f778daea..b221224d 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -48,10 +48,12 @@ public class ConfigTabBar }; } - public void Draw() + public TabType Draw() { - if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out _, () => { }, Tabs)) + if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out var currentLabel, () => { }, Tabs)) SelectTab = TabType.None; + + return FromLabel(currentLabel); } private ReadOnlySpan ToLabel(TabType type) @@ -68,4 +70,20 @@ public class ConfigTabBar TabType.ResourceManager => Resource.Label, _ => ReadOnlySpan.Empty, }; + + private TabType FromLabel(ReadOnlySpan label) + { + // @formatter:off + if (label == Mods.Label) return TabType.Mods; + if (label == Collections.Label) return TabType.Collections; + if (label == Settings.Label) return TabType.Settings; + if (label == ChangedItems.Label) return TabType.ChangedItems; + if (label == Effective.Label) return TabType.EffectiveChanges; + if (label == OnScreenTab.Label) return TabType.OnScreen; + if (label == Watcher.Label) return TabType.ResourceWatcher; + if (label == Debug.Label) return TabType.Debug; + if (label == Resource.Label) return TabType.ResourceManager; + // @formatter:on + return TabType.None; + } } From b50564f7413ea52aae0cc8f301f90c299c4723ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 20:18:38 +0200 Subject: [PATCH 0963/2451] Use collection prefix for TMBs. --- Penumbra/Interop/PathResolving/SubfileHelper.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 83393d40..05f42220 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -85,7 +85,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection Materials and AVFX need to be set per collection so they can load their textures independently from each other. + /// Materials, TMB, and AVFX need to be set per collection so they can load their sub files independently from each other. public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, ResolveData) data) { @@ -94,6 +94,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollectionFileType) { case ResourceType.Mtrl: - case ResourceType.Avfx: + case ResourceType.Avfx: if (handle->FileSize == 0) _subFileCollection[(nint)handle] = resolveData; From 5d96f789fecac72cd016d31dace449240f0749bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 May 2023 20:19:12 +0200 Subject: [PATCH 0964/2451] Some more work on mod merging/splitting, still WIP. --- Penumbra/Mods/Editor/DuplicateManager.cs | 5 +- Penumbra/Mods/Editor/ModMerger.cs | 130 ++++++++++++++++------ Penumbra/Mods/Manager/ModOptionEditor.cs | 6 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 34 ++++-- 4 files changed, 126 insertions(+), 49 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 03321cc3..98eda9e4 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -15,7 +15,7 @@ public class DuplicateManager { private readonly SaveService _saveService; private readonly ModManager _modManager; - private readonly SHA256 _hasher = SHA256.Create(); + private readonly SHA256 _hasher = SHA256.Create(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); public DuplicateManager(ModManager modManager, SaveService saveService) @@ -175,7 +175,8 @@ public class DuplicateManager } } - private static unsafe bool CompareFilesDirectly(FullPath f1, FullPath f2) + /// Check if two files are identical on a binary level. Returns true if they are identical. + public static unsafe bool CompareFilesDirectly(FullPath f1, FullPath f2) { if (!f1.Exists || !f2.Exists) return false; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 29ed210e..2d90a6dd 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using OtterGui; using Penumbra.Api.Enums; using Penumbra.Communication; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String.Classes; @@ -24,7 +24,9 @@ public class ModMerger : IDisposable private readonly ModManager _mods; private readonly ModCreator _creator; - public Mod? MergeFromMod { get; private set; } + public Mod? MergeFromMod + => _selector.Selected; + public Mod? MergeToMod; public string OptionGroupName = "Merges"; public string OptionName = string.Empty; @@ -32,11 +34,13 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = new(); private readonly HashSet _createdDirectories = new(); - public readonly HashSet SelectedOptions = new(); + private readonly HashSet _createdGroups = new(); + private readonly HashSet _createdOptions = new(); - private int _createdGroup = -1; - private SubMod? _createdOption; - public Exception? Error { get; private set; } + public readonly HashSet SelectedOptions = new(); + + public readonly IReadOnlyList Warnings = new List(); + public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator) @@ -48,7 +52,7 @@ public class ModMerger : IDisposable _creator = creator; _mods = mods; _selector.SelectionChanged += OnSelectionChange; - _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.Api); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); } public void Dispose() @@ -61,7 +65,7 @@ public class ModMerger : IDisposable => _mods.Where(m => m != MergeFromMod); public bool CanMerge - => MergeToMod != null && MergeToMod != MergeFromMod && !MergeFromMod!.HasOptions; + => MergeToMod != null && MergeToMod != MergeFromMod; public void Merge() { @@ -91,7 +95,34 @@ public class ModMerger : IDisposable private void MergeWithOptions() { - // Not supported + MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false); + + foreach (var originalGroup in MergeFromMod!.Groups) + { + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); + if (groupCreated) + _createdGroups.Add(groupIdx); + if (group.Type != originalGroup.Type) + ((List)Warnings).Add( + $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); + + foreach (var originalOption in originalGroup) + { + var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + if (optionCreated) + { + _createdOptions.Add(option); + MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false); + } + else + { + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option.FullName} already existed."); + } + } + } + + CopyFiles(MergeToMod!.ModPath); } private void MergeIntoOption(string groupName, string optionName) @@ -99,7 +130,7 @@ public class ModMerger : IDisposable if (groupName.Length == 0 && optionName.Length == 0) { CopyFiles(MergeToMod!.ModPath); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true); } else if (groupName.Length * optionName.Length == 0) { @@ -108,10 +139,10 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName); if (groupCreated) - _createdGroup = groupIdx; + _createdGroups.Add(groupIdx); var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); if (optionCreated) - _createdOption = option; + _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -119,14 +150,36 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option) + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) { var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var manips = option.ManipulationData.ToHashSet(); + + bool GetFullPath(FullPath input, out FullPath ret) + { + if (fromFileToFile) + { + if (!_fileToFile.TryGetValue(input.FullName, out var s)) + { + ret = input; + return false; + } + + ret = new FullPath(s); + return true; + } + + if (!Utf8RelPath.FromFile(input, MergeFromMod!.ModPath, out var relPath)) + throw new Exception($"Could not create relative path from {input} and {MergeFromMod!.ModPath}."); + + ret = new FullPath(MergeToMod!.ModPath, relPath); + return true; + } + foreach (var originalOption in mergeOptions) { foreach (var manip in originalOption.Manipulations) @@ -143,14 +196,14 @@ public class ModMerger : IDisposable $"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); } - foreach (var (gamePath, relPath) in originalOption.Files) + foreach (var (gamePath, path) in originalOption.Files) { - if (!_fileToFile.TryGetValue(relPath.FullName, out var newFile)) + if (!GetFullPath(path, out var newFile)) throw new Exception( - $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); - if (!redirections.TryAdd(gamePath, new FullPath(newFile))) + $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + if (!redirections.TryAdd(gamePath, newFile)) throw new Exception( - $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); } } @@ -193,6 +246,7 @@ public class ModMerger : IDisposable if (mods.Count == 0) return; + ((List)Warnings).Clear(); Error = null; DirectoryInfo? dir = null; Mod? result = null; @@ -261,10 +315,14 @@ public class ModMerger : IDisposable { var target = Path.GetRelativePath(parentPath, file.FullName); target = Path.Combine(newMod.FullName, target); + var targetPath = new FullPath(target); Directory.CreateDirectory(Path.GetDirectoryName(target)!); - File.Copy(file.FullName, target); + // Copy throws if the file exists, which we want. + // This copies if the target does not exist, throws if it exists and is different, or does nothing if it exists and is identical. + if (!File.Exists(target) || !DuplicateManager.CompareFilesDirectly(targetPath, file)) + File.Copy(file.FullName, target); Penumbra.Log.Verbose($"[Splitter] Copied file {file.FullName} to {target}."); - ret.Add(path, new FullPath(target)); + ret.Add(path, targetPath); } return ret; @@ -274,16 +332,24 @@ public class ModMerger : IDisposable { _fileToFile.Clear(); _createdDirectories.Clear(); - _createdOption = null; - _createdGroup = -1; + _createdGroups.Clear(); + _createdOptions.Clear(); } private void FailureCleanup() { - if (_createdGroup >= 0 && _createdGroup < MergeToMod!.Groups.Count) - _editor.DeleteModGroup(MergeToMod!, _createdGroup); - else if (_createdOption != null) - _editor.DeleteOption(MergeToMod!, _createdOption.GroupIdx, _createdOption.OptionIdx); + foreach (var option in _createdOptions) + { + _editor.DeleteOption(MergeToMod!, option.GroupIdx, option.OptionIdx); + Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); + } + + foreach (var group in _createdGroups) + { + var groupName = MergeToMod!.Groups[group]; + _editor.DeleteModGroup(MergeToMod!, group); + Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}."); + } foreach (var dir in _createdDirectories) { @@ -329,7 +395,6 @@ public class ModMerger : IDisposable MergeToMod = null; SelectedOptions.Clear(); - MergeFromMod = newSelection; } private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) @@ -339,10 +404,7 @@ public class ModMerger : IDisposable case ModPathChangeType.Deleted: { if (mod == MergeFromMod) - { SelectedOptions.Clear(); - MergeFromMod = null; - } if (mod == MergeToMod) MergeToMod = null; @@ -350,11 +412,7 @@ public class ModMerger : IDisposable } case ModPathChangeType.StartingReload: SelectedOptions.Clear(); - MergeFromMod = null; - MergeToMod = null; - break; - case ModPathChangeType.Reloaded: - MergeFromMod = _selector.Selected; + MergeToMod = null; break; } } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index a5e77c37..b4306877 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -349,7 +349,7 @@ public class ModOptionEditor } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileData.SetEquals(replacements)) @@ -362,7 +362,7 @@ public class ModOptionEditor } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary additions) { var subMod = GetSubMod(mod, groupIdx, optionIdx); var oldCount = subMod.FileData.Count; @@ -375,7 +375,7 @@ public class ModOptionEditor } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileSwapData.SetEquals(swaps)) diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index c4cf1fb4..d1a5ec95 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -38,11 +38,14 @@ public class ModMergeTab ImGui.SameLine(); DrawCombo(); - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); - ImGui.SameLine(); - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + using (var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions)) + { + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGui.SameLine(); + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + } if (ImGuiUtil.DrawDisabledButton("Merge", Vector2.Zero, string.Empty, !_modMerger.CanMerge)) _modMerger.Merge(); @@ -50,7 +53,8 @@ public class ModMergeTab ImGui.Dummy(Vector2.One); ImGui.Separator(); ImGui.Dummy(Vector2.One); - using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing()})) + using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, + ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing() })) { foreach (var (option, idx) in _modMerger.MergeFromMod.AllSubMods.WithIndex()) { @@ -79,13 +83,27 @@ public class ModMergeTab ImGuiUtil.DrawTableColumn(option.FileData.Count.ToString()); ImGuiUtil.DrawTableColumn(option.FileSwapData.Count.ToString()); ImGuiUtil.DrawTableColumn(option.Manipulations.Count.ToString()); - } } + ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); - if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) + if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, + _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) _modMerger.SplitIntoMod(_newModName); + if (_modMerger.Warnings.Count > 0) + { + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); + foreach (var warning in _modMerger.Warnings.SkipLast(1)) + { + ImGuiUtil.TextWrapped(warning); + ImGui.Separator(); + } + ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); + } + if (_modMerger.Error != null) { ImGui.Separator(); From c86d2eded5151621722fc2d4532f550d07ef9b1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 May 2023 16:15:11 +0200 Subject: [PATCH 0965/2451] Expand and name vfxWeaponHook to deal with more files. --- .../Collections/Manager/CollectionEditor.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 8 ++-- .../Collections/Manager/InheritanceManager.cs | 6 +-- .../PathResolving/AnimationHookService.cs | 37 ++++++++++++------- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index c2b84256..4000f504 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -205,7 +205,7 @@ public class CollectionEditor [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) { - _saveService.DelaySave(new ModCollectionSave(_modStorage, changedCollection)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection)); _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 7a60aaec..2c2b9f24 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -194,14 +194,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable var any = collection.UnusedSettings.Count > 0; ((Dictionary)collection.UnusedSettings).Clear(); if (any) - _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } /// Remove a specific setting for not currently-installed mods from the given collection. public void CleanUnavailableSetting(ModCollection collection, string? setting) { if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) - _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } /// @@ -304,7 +304,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable break; case ModPathChangeType.Moved: foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; } } @@ -319,7 +319,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - _saveService.DelaySave(new ModCollectionSave(_modStorage, collection)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 0d8bfe3c..93dee89f 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -88,7 +88,7 @@ public class InheritanceManager : IDisposable var parent = inheritor.DirectlyInheritsFrom[idx]; ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); ((List)parent.DirectParentOf).Remove(inheritor); - _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); @@ -100,7 +100,7 @@ public class InheritanceManager : IDisposable if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) return; - _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); @@ -116,7 +116,7 @@ public class InheritanceManager : IDisposable ((List)parent.DirectParentOf).Add(inheritor); if (invokeEvent) { - _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); + _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); } diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 8447dfa7..19d1ede8 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -48,7 +48,7 @@ public unsafe class AnimationHookService : IDisposable _unkMountAnimationHook.Enable(); _unkParasolAnimationHook.Enable(); _dismountHook.Enable(); - _vfxWeaponHook.Enable(); + _apricotListenerSoundPlayHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -106,7 +106,7 @@ public unsafe class AnimationHookService : IDisposable _unkMountAnimationHook.Dispose(); _unkParasolAnimationHook.Dispose(); _dismountHook.Dispose(); - _vfxWeaponHook.Dispose(); + _apricotListenerSoundPlayHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -364,23 +364,34 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = last; } - [Signature("48 89 6C 24 ?? 41 54 41 56 41 57 48 81 EC", DetourName = nameof(VfxWeaponDetour))] - private readonly Hook _vfxWeaponHook = null!; + [Signature("48 89 6C 24 ?? 41 54 41 56 41 57 48 81 EC", DetourName = nameof(ApricotListenerSoundPlayDetour))] + private readonly Hook _apricotListenerSoundPlayHook = null!; - private delegate nint VfxWeaponDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + private delegate nint ApricotListenerSoundPlayDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); - private nint VfxWeaponDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + private nint ApricotListenerSoundPlayDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) { if (a6 == nint.Zero) - return _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + return _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - var drawObject = ((DrawObject**)a6)[1]; - if (drawObject == null) - return _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + var last = _animationLoadData.Value; + // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. + var gameObject = (*(delegate* unmanaged**)a6)[1](a6); + if (gameObject != null) + { + _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); + } + else + { + // for VfxListenner we can obtain the associated draw object as its first member, + // if the object has different type, drawObject will contain other values or garbage, + // but only be used in a dictionary pointer lookup, so this does not hurt. + var drawObject = ((DrawObject**)a6)[1]; + if (drawObject != null) + _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); + } - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - var ret = _vfxWeaponHook!.Original(a1, a2, a3, a4, a5, a6); + var ret = _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); _animationLoadData.Value = last; return ret; } From cbda4614a96505fa7df8a05199922de57fc9e5f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 May 2023 21:17:04 +0200 Subject: [PATCH 0966/2451] Make Merge Prettier --- Penumbra/Mods/Editor/ModMerger.cs | 1 - Penumbra/Services/SaveService.cs | 6 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 284 ++++++++++++++++------ 3 files changed, 215 insertions(+), 76 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 2d90a6dd..1ed590cf 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -31,7 +31,6 @@ public class ModMerger : IDisposable public string OptionGroupName = "Merges"; public string OptionName = string.Empty; - private readonly Dictionary _fileToFile = new(); private readonly HashSet _createdDirectories = new(); private readonly HashSet _createdGroups = new(); diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 557cc4fc..66695b40 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -29,7 +29,11 @@ public interface ISavable public class SaveService { - private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(30); +#if DEBUG + private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(2); +#else + private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(10); +#endif private readonly Logger _log; private readonly FrameworkManager _framework; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index d1a5ec95..f7490c93 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,13 +1,14 @@ +using System; using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; -using SixLabors.ImageSharp.ColorSpaces; namespace Penumbra.UI.AdvancedWindow; @@ -33,90 +34,225 @@ public class ModMergeTab if (!tab) return; - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod.Name} into "); + ImGui.Dummy(Vector2.One); + var size = 550 * ImGuiHelpers.GlobalScale; + DrawMergeInto(size); ImGui.SameLine(); - DrawCombo(); - - using (var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions)) - { - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); - ImGui.SameLine(); - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); - } - - if (ImGuiUtil.DrawDisabledButton("Merge", Vector2.Zero, string.Empty, !_modMerger.CanMerge)) - _modMerger.Merge(); + DrawMergeIntoDesc(); ImGui.Dummy(Vector2.One); ImGui.Separator(); ImGui.Dummy(Vector2.One); - using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, - ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing() })) - { - foreach (var (option, idx) in _modMerger.MergeFromMod.AllSubMods.WithIndex()) - { - using var id = ImRaii.PushId(idx); - var selected = _modMerger.SelectedOptions.Contains(option); - ImGui.TableNextColumn(); - if (ImGui.Checkbox("##check", ref selected)) - { - if (selected) - _modMerger.SelectedOptions.Add(option); - else - _modMerger.SelectedOptions.Remove(option); - } - if (option.IsDefault) - { - ImGuiUtil.DrawTableColumn(option.FullName); - ImGui.TableNextColumn(); - } - else - { - ImGuiUtil.DrawTableColumn(option.ParentMod.Groups[option.GroupIdx].Name); - ImGuiUtil.DrawTableColumn(option.Name); - } + DrawSplitOff(size); + ImGui.SameLine(); + DrawSplitOffDesc(); - ImGuiUtil.DrawTableColumn(option.FileData.Count.ToString()); - ImGuiUtil.DrawTableColumn(option.FileSwapData.Count.ToString()); - ImGuiUtil.DrawTableColumn(option.Manipulations.Count.ToString()); - } - } - ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); - if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, - _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) - _modMerger.SplitIntoMod(_newModName); - - if (_modMerger.Warnings.Count > 0) - { - ImGui.Separator(); - ImGui.Dummy(Vector2.One); - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); - foreach (var warning in _modMerger.Warnings.SkipLast(1)) - { - ImGuiUtil.TextWrapped(warning); - ImGui.Separator(); - } - ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); - } - - if (_modMerger.Error != null) - { - ImGui.Separator(); - ImGui.Dummy(Vector2.One); - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); - } + DrawError(); + DrawWarnings(); } - private void DrawCombo() + private void DrawMergeInto(float size) { - _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? string.Empty, string.Empty, - 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + using var bigGroup = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod!.Name} into "); + ImGui.SameLine(); + DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); + + var width = ImGui.GetItemRectSize(); + using (var g = ImRaii.Group()) + { + using var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions); + var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1); + var group = _modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == _modMerger.OptionGroupName); + var color = group != null || _modMerger.OptionGroupName.Length == 0 && _modMerger.OptionName.Length == 0 + ? Colors.PressEnterWarningBg + : Colors.DiscordColor; + using var c = ImRaii.PushColor(ImGuiCol.Border, color); + ImGui.SetNextItemWidth(buttonWidth); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGuiUtil.HoverTooltip( + "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n" + + "A red border indicates an existing option group, a blue border indicates a new one."); + ImGui.SameLine(); + + + color = color == Colors.DiscordColor + ? Colors.DiscordColor + : group == null || group.Any(o => o.Name == _modMerger.OptionName) + ? Colors.PressEnterWarningBg + : Colors.DiscordColor; + c.Push(ImGuiCol.Border, color); + ImGui.SetNextItemWidth(buttonWidth); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + ImGuiUtil.HoverTooltip( + "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n" + + "A red border indicates an existing option, a blue border indicates a new one."); + } + + if (_modMerger.MergeFromMod.HasOptions) + ImGuiUtil.HoverTooltip("You can only specify a target option if the source mod has no true options itself.", + ImGuiHoveredFlags.AllowWhenDisabled); + + if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0), + _modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !_modMerger.CanMerge)) + _modMerger.Merge(); + } + + private void DrawMergeIntoDesc() + { + ImGuiUtil.TextWrapped(_modMerger.MergeFromMod!.HasOptions + ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break." + : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."); + } + + private void DrawCombo(float width) + { + _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? "Select the target Mod...", string.Empty, width, + ImGui.GetTextLineHeight()); _modMerger.MergeToMod = _modCombo.CurrentSelection; } + + private void DrawSplitOff(float size) + { + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth(size); + ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); + ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique."); + var tt = _newModName.Length == 0 + ? "Please enter a name for the newly created mod first." + : _modMerger.SelectedOptions.Count == 0 + ? "Please select at least one option to split off." + : string.Empty; + var buttonText = + $"Split Off {_modMerger.SelectedOptions.Count} Option{(_modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; + if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0)) + _modMerger.SplitIntoMod(_newModName); + + ImGui.Dummy(Vector2.One); + var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); + if (ImGui.Button("Select All", buttonSize)) + _modMerger.SelectedOptions.UnionWith(_modMerger.MergeFromMod!.AllSubMods); + ImGui.SameLine(); + if (ImGui.Button("Unselect All", buttonSize)) + _modMerger.SelectedOptions.Clear(); + ImGui.SameLine(); + if (ImGui.Button("Invert Selection", buttonSize)) + _modMerger.SelectedOptions.SymmetricExceptWith(_modMerger.MergeFromMod!.AllSubMods); + DrawOptionTable(size); + } + + private void DrawSplitOffDesc() + { + ImGuiUtil.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n" + + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n" + + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n" + + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."); + } + + private void DrawOptionTable(float size) + { + var options = _modMerger.MergeFromMod!.AllSubMods.ToList(); + var height = _modMerger.Warnings.Count == 0 && _modMerger.Error == null + ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() + : 8 * ImGui.GetFrameHeightWithSpacing(); + height = Math.Min(height, (options.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + var tableSize = new Vector2(size, height); + using var table = ImRaii.Table("##options", 6, + ImGuiTableFlags.RowBg + | ImGuiTableFlags.SizingFixedFit + | ImGuiTableFlags.ScrollY + | ImGuiTableFlags.BordersOuterV + | ImGuiTableFlags.BordersOuterH, + tableSize); + if (!table) + return; + + ImGui.TableSetupColumn("##Selected", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Option", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Option Group", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Files", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Swaps", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Manips", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); + foreach (var (option, idx) in options.WithIndex()) + { + using var id = ImRaii.PushId(idx); + var selected = _modMerger.SelectedOptions.Contains(option); + + void Handle(SubMod option2, bool selected2) + { + if (selected2) + _modMerger.SelectedOptions.Add(option2); + else + _modMerger.SelectedOptions.Remove(option2); + } + + ImGui.TableNextColumn(); + if (ImGui.Checkbox("##check", ref selected)) + Handle(option, selected); + + if (option.IsDefault) + { + ImGuiUtil.DrawTableColumn(option.FullName); + ImGui.TableNextColumn(); + } + else + { + ImGuiUtil.DrawTableColumn(option.Name); + var group = option.ParentMod.Groups[option.GroupIdx]; + ImGui.TableNextColumn(); + ImGui.Selectable(group.Name, false); + if (ImGui.BeginPopupContextItem("##groupContext")) + { + if (ImGui.MenuItem("Select All")) + foreach (var opt in group) + Handle((SubMod)opt, true); + + if (ImGui.MenuItem("Unselect All")) + foreach (var opt in group) + Handle((SubMod)opt, false); + ImGui.EndPopup(); + } + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + } + } + + private void DrawWarnings() + { + if (_modMerger.Warnings.Count == 0) + return; + + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); + foreach (var warning in _modMerger.Warnings.SkipLast(1)) + { + ImGuiUtil.TextWrapped(warning); + ImGui.Separator(); + } + + ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); + } + + private void DrawError() + { + if (_modMerger.Error == null) + return; + + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); + } } From 3f03712e24c71afa8c6f91787f46254dd24c0a7e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 May 2023 17:55:48 +0200 Subject: [PATCH 0967/2451] Fix some issues with removing mods from collection caches. --- Penumbra/Collections/Cache/CollectionCache.cs | 48 +++++++++----- .../Collections/Cache/CollectionModData.cs | 62 +++++++++++++++++++ Penumbra/Collections/Cache/ImcCache.cs | 2 +- Penumbra/Collections/Cache/MetaCache.cs | 4 +- Penumbra/UI/Tabs/DebugTab.cs | 57 +++++++++++++---- 5 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 Penumbra/Collections/Cache/CollectionModData.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index af037ce5..97205990 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -24,6 +24,7 @@ public class CollectionCache : IDisposable { private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); public readonly SortedList, object?)> _changedItems = new(); public readonly Dictionary ResolvedFiles = new(); public readonly MetaCache Meta; @@ -124,13 +125,21 @@ public class CollectionCache : IDisposable /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile(Utf8GamePath path, FullPath fullPath) { - if (CheckFullPath(path, fullPath)) - ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath); + if (!CheckFullPath(path, fullPath)) + return; + + if (ResolvedFiles.Remove(path, out var modPath)) + ModData.RemovePath(modPath.Mod, path); + ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + ModData.AddPath(Mod.ForcedFiles, path); } /// Force a file resolve to be removed. - internal void RemoveFile(Utf8GamePath path) - => ResolvedFiles.Remove(path); + internal void RemovePath(Utf8GamePath path) + { + if (ResolvedFiles.Remove(path, out var modPath)) + ModData.RemovePath(modPath.Mod, path); + } public void ReloadMod(IMod mod, bool addMetaChanges) { @@ -141,20 +150,19 @@ public class CollectionCache : IDisposable public void RemoveMod(IMod mod, bool addMetaChanges) { var conflicts = Conflicts(mod); - - foreach (var (path, _) in mod.AllSubMods.SelectMany(s => s.Files.Concat(s.FileSwaps))) + var (paths, manipulations) = ModData.RemoveMod(mod); + foreach (var path in paths) { - if (!ResolvedFiles.TryGetValue(path, out var modPath)) - continue; - - if (modPath.Mod == mod) - ResolvedFiles.Remove(path); + if (ResolvedFiles.Remove(path, out var mp) && mp.Mod != mod) + Penumbra.Log.Warning( + $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); } - foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations)) + foreach (var manipulation in manipulations) { - if (Meta.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod) - Meta.RevertMod(manipulation); + if (Meta.RevertMod(manipulation, out var mp) && mp != mod) + Penumbra.Log.Warning( + $"Invalid mod state, removing {mod.Name} and associated manipulation {manipulation} returned current mod {mp.Name}."); } _conflicts.Remove(mod); @@ -247,7 +255,10 @@ public class CollectionCache : IDisposable return; if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) + { + ModData.AddPath(mod, path); return; + } var modPath = ResolvedFiles[path]; // Lower prioritized option in the same mod. @@ -255,7 +266,11 @@ public class CollectionCache : IDisposable return; if (AddConflict(path, mod, modPath.Mod)) + { + ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); + ModData.AddPath(mod, path); + } } @@ -332,6 +347,7 @@ public class CollectionCache : IDisposable if (!Meta.TryGetValue(manip, out var existingMod)) { Meta.ApplyMod(manip, mod); + ModData.AddManip(mod, manip); return; } @@ -340,7 +356,11 @@ public class CollectionCache : IDisposable return; if (AddConflict(manip, mod, existingMod)) + { + ModData.RemoveManip(existingMod, manip); Meta.ApplyMod(manip, mod); + ModData.AddManip(mod, manip); + } } diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs new file mode 100644 index 00000000..24f8f297 --- /dev/null +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.Collections.Cache; + +public class CollectionModData +{ + private readonly Dictionary, HashSet)> _data = new(); + + public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data + => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); + + public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) + { + if (_data.Remove(mod, out var data)) + return data; + + return (Array.Empty(), Array.Empty()); + } + + public void AddPath(IMod mod, Utf8GamePath path) + { + if (_data.TryGetValue(mod, out var data)) + { + data.Item1.Add(path); + } + else + { + data = (new HashSet { path }, new HashSet()); + _data.Add(mod, data); + } + } + + public void AddManip(IMod mod, MetaManipulation manipulation) + { + if (_data.TryGetValue(mod, out var data)) + { + data.Item2.Add(manipulation); + } + else + { + data = (new HashSet(), new HashSet { manipulation }); + _data.Add(mod, data); + } + } + + public void RemovePath(IMod mod, Utf8GamePath path) + { + if (_data.TryGetValue(mod, out var data) && data.Item1.Remove(path) && data.Item1.Count == 0 && data.Item2.Count == 0) + _data.Remove(mod); + } + + public void RemoveManip(IMod mod, MetaManipulation manip) + { + if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0) + _data.Remove(mod); + } +} diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 2335fadd..7689e5b2 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -27,7 +27,7 @@ public readonly struct ImcCache : IDisposable { foreach( var (path, file) in _imcFiles ) { - collection._cache!.RemoveFile( path ); + collection._cache!.RemovePath( path ); file.Reset(); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 559fb3cd..e2bff762 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -108,9 +108,9 @@ public class MetaCache : IDisposable, IEnumerable t.Item1.Name)) + { + using var id = mod is TemporaryMod t ? PushId(t.Priority) : PushId(((Mod)mod).ModPath.Name); + using var node2 = TreeNode(mod.Name.Text); + if (!node2) + continue; + + foreach (var path in paths) + + TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + + foreach (var manip in manips) + TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } + } + else + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); + TreeNode(collection.AnonymizedName, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } + } + } + /// Draw general information about mod and collection state. private void DrawDebugTabGeneral() { @@ -171,17 +215,6 @@ public class DebugTab : Window, ITab } } - using (var tree = TreeNode($"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1})###Collections")) - { - if (tree) - { - using var table = Table("##DebugCollectionsTable", 2, ImGuiTableFlags.SizingFixedFit); - if (table) - foreach (var collection in _collectionManager.Storage) - PrintValue(collection.Name, collection.HasCache.ToString()); - } - } - var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index); using (var tree = TreeNode($"Mods ({issues} Issues)###Mods")) { @@ -245,7 +278,7 @@ public class DebugTab : Window, ITab using var table = Table("##DebugFramework", 2, ImGuiTableFlags.SizingFixedFit); if (table) { - foreach(var important in _framework.Important) + foreach (var important in _framework.Important) PrintValue(important, "Immediate"); foreach (var (onTick, idx) in _framework.OnTick.WithIndex()) From 0aa74692a8388a64c0fc4c74628e07c403b43eec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 May 2023 00:54:47 +0200 Subject: [PATCH 0968/2451] Fix issue with reverted IMC edits and change counter on disabling mods. --- Penumbra/Collections/Cache/CollectionCache.cs | 7 ++++--- Penumbra/Collections/Cache/CollectionCacheManager.cs | 11 ++++++++--- Penumbra/Collections/Manager/CollectionEditor.cs | 1 - Penumbra/UI/Tabs/DebugTab.cs | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 97205990..56539526 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -151,6 +151,10 @@ public class CollectionCache : IDisposable { var conflicts = Conflicts(mod); var (paths, manipulations) = ModData.RemoveMod(mod); + + if (addMetaChanges) + ++_collection.ChangeCounter; + foreach (var path in paths) { if (ResolvedFiles.Remove(path, out var mp) && mp.Mod != mod) @@ -183,10 +187,7 @@ public class CollectionCache : IDisposable } if (addMetaChanges) - { - ++_collection.ChangeCounter; _manager.MetaFileManager.ApplyDefaultFiles(_collection); - } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 7850cdce..5417aa19 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -27,7 +27,10 @@ public class CollectionCacheManager : IDisposable internal readonly MetaFileManager MetaFileManager; - public int Count { get; private set; } + private int _count; + + public int Count + => _count; public IEnumerable Active => _storage.Where(c => c.HasCache); @@ -78,7 +81,8 @@ public class CollectionCacheManager : IDisposable return false; collection._cache = new CollectionCache(this, collection); - ++Count; + if (collection.Index > 0) + Interlocked.Increment(ref _count); Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); return true; } @@ -295,7 +299,8 @@ public class CollectionCacheManager : IDisposable collection._cache!.Dispose(); collection._cache = null; - --Count; + if (collection.Index > 0) + Interlocked.Decrement(ref _count); Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}."); } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 4000f504..5aad1595 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -7,7 +7,6 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 671ead3b..9acecce9 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -154,7 +154,7 @@ public class DebugTab : Window, ITab if (collection.HasCache) { using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode(collection.AnonymizedName); + using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})"); if (!node) continue; @@ -177,7 +177,7 @@ public class DebugTab : Window, ITab else { using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode(collection.AnonymizedName, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } } From 4298b4613083a7df2af5b41d32e500463c92c797 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 May 2023 21:49:29 +0200 Subject: [PATCH 0969/2451] Remove soon unrestricted gear. --- Penumbra.GameData/Data/RestrictedGear.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs index 6f59ae2d..7bfb2360 100644 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -405,10 +405,6 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood AddItem(m2f, f2m, 36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat AddItem(m2f, f2m, 36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes - AddItem(m2f, f2m, 38253, 38257); // Valentione Emissary's Hat <-> Valentione Emissary's Dress Hat - AddItem(m2f, f2m, 38254, 38258); // Valentione Emissary's Jacket <-> Valentione Emissary's Ruffled Dress - AddItem(m2f, f2m, 38255, 38259); // Valentione Emissary's Bottoms <-> Valentione Emissary's Culottes - AddItem(m2f, f2m, 38256, 38260); // Valentione Emissary's Boots <-> Valentione Emissary's Boots AddItem(m2f, f2m, 32393, 39302, false); // Edenmete Gown of Casting <- Gaia's Attire } From 5567134a568782b683313f68e3b25f3fd2a3fe37 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 May 2023 21:50:01 +0200 Subject: [PATCH 0970/2451] Fix inheritance save issues and sort mod settings on collection save. --- .../Collections/Manager/CollectionStorage.cs | 43 +------------------ .../Collections/Manager/InheritanceManager.cs | 35 ++++++++++----- Penumbra/Collections/ModCollection.cs | 11 +++-- Penumbra/Collections/ModCollectionSave.cs | 15 ++++--- Penumbra/Services/ConfigMigrationService.cs | 2 +- 5 files changed, 43 insertions(+), 63 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2c2b9f24..0a6b95a8 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -11,7 +11,6 @@ using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -153,41 +152,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable return true; } - /// Stored after loading to be consumed and passed to the inheritance manager later. - private List>? _inheritancesByName = new(); - - /// Return an enumerable of collections and the collections they should inherit. - public IEnumerable<(ModCollection Collection, IReadOnlyList Inheritance, bool LoadChanges)> ConsumeInheritanceNames() - { - if (_inheritancesByName == null) - throw new Exception("Inheritances were already consumed. This method can not be called twice."); - - var inheritances = _inheritancesByName; - _inheritancesByName = null; - var list = new List(); - foreach (var (collection, inheritance) in _collections.Zip(inheritances)) - { - list.Clear(); - var changes = false; - foreach (var subCollectionName in inheritance) - { - if (ByName(subCollectionName, out var subCollection)) - { - list.Add(subCollection); - } - else - { - Penumbra.ChatService.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", - NotificationType.Warning); - changes = true; - } - } - - yield return (collection, list, changes); - } - } - /// Remove all settings for not currently-installed mods from the given collection. public void CleanUnavailableSettings(ModCollection collection) { @@ -218,9 +182,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// private void ReadCollections(out ModCollection defaultNamedCollection) { - _inheritancesByName?.Clear(); - _inheritancesByName?.Add(Array.Empty()); // None. - foreach (var file in _saveService.FileNames.CollectionFiles) { if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)) @@ -241,13 +202,11 @@ public class CollectionStorage : IReadOnlyList, IDisposable continue; } - var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings); + var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", NotificationType.Warning); - - _inheritancesByName?.Add(inheritance); _collections.Add(collection); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 93dee89f..ca17a87d 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -7,6 +7,7 @@ using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.UI.CollectionTab; using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -131,20 +132,34 @@ public class InheritanceManager : IDisposable /// private void ApplyInheritances() { - foreach (var (collection, directParents, changes) in _storage.ConsumeInheritanceNames()) + foreach (var collection in _storage) { - var localChanges = changes; - foreach (var parent in directParents) - { - if (AddInheritance(collection, parent, false)) - continue; + if (collection.InheritanceByName == null) + continue; - localChanges = true; - Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {parent.Name}, removed.", "Warning", - NotificationType.Warning); + var changes = false; + foreach (var subCollectionName in collection.InheritanceByName) + { + if (_storage.ByName(subCollectionName, out var subCollection)) + { + if (AddInheritance(collection, subCollection, false)) + continue; + + changes = true; + Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", + NotificationType.Warning); + } + else + { + Penumbra.ChatService.NotificationMessage( + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", + NotificationType.Warning); + changes = true; + } } - if (localChanges) + collection.InheritanceByName = null; + if (changes) _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 32e7b0b3..fc333747 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Penumbra.Mods.Manager; -using Penumbra.Util; using Penumbra.Collections.Manager; using Penumbra.Services; @@ -58,6 +57,9 @@ public partial class ModCollection /// Settings for deleted mods will be kept via the mods identifier (directory name). public readonly IReadOnlyDictionary UnusedSettings; + + /// Inheritances stored before they can be applied. + public IReadOnlyList? InheritanceByName; /// Contains all direct parent collections this collection inherits settings from. public readonly IReadOnlyList DirectlyInheritsFrom; @@ -115,10 +117,13 @@ public partial class ModCollection /// Constructor for reading from files. public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index, - Dictionary allSettings) + Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings); + var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings) + { + InheritanceByName = inheritances, + }; ret.ApplyModSettings(saver, mods); ModCollectionMigration.Migrate(saver, mods, version, ret); return ret; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 6bb1a5af..72b2e94c 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -48,17 +48,18 @@ internal readonly struct ModCollectionSave : ISavable // Write all used and unused settings by mod directory name. j.WriteStartObject(); + var list = new List<(string, ModSettings.SavedSettings)>(_modCollection.Settings.Count + _modCollection.UnusedSettings.Count); for (var i = 0; i < _modCollection.Settings.Count; ++i) { var settings = _modCollection.Settings[i]; if (settings != null) - { - j.WritePropertyName(_modStorage[i].ModPath.Name); - x.Serialize(j, new ModSettings.SavedSettings(settings, _modStorage[i])); - } + list.Add((_modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, _modStorage[i]))); } - foreach (var (modDir, settings) in _modCollection.UnusedSettings) + list.AddRange(_modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); + list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); + + foreach (var (modDir, settings) in list) { j.WritePropertyName(modDir); x.Serialize(j, settings); @@ -67,8 +68,8 @@ internal readonly struct ModCollectionSave : ISavable j.WriteEndObject(); // Inherit by collection name. - j.WritePropertyName("Inheritance"); - x.Serialize(j, _modCollection.DirectlyInheritsFrom.Select(c => c.Name)); + j.WritePropertyName("Inheritance"); + x.Serialize(j, _modCollection.InheritanceByName ?? _modCollection.DirectlyInheritsFrom.Select(c => c.Name)); j.WriteEndObject(); } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 33f41deb..8c0a60f1 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -333,7 +333,7 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict); + var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); _saveService.ImmediateSave(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) From e14fedf59e22813917451fb4117a78bb1ea28e30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 May 2023 23:00:44 +0200 Subject: [PATCH 0971/2451] Add some Metadata validation. --- Penumbra/Api/PenumbraApi.cs | 22 +-- Penumbra/Collections/Cache/ImcCache.cs | 4 +- .../Communication/CreatedCharacterBase.cs | 3 +- .../Communication/CreatingCharacterBase.cs | 3 +- Penumbra/Communication/EnabledChanged.cs | 3 +- Penumbra/Communication/ModDirectoryChanged.cs | 3 +- Penumbra/Communication/ModPathChanged.cs | 3 +- Penumbra/Communication/ModSettingChanged.cs | 3 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- .../Meta/Manipulations/EqdpManipulation.cs | 87 +++++----- .../Meta/Manipulations/EqpManipulation.cs | 13 ++ .../Meta/Manipulations/EstManipulation.cs | 10 ++ .../Meta/Manipulations/GmpManipulation.cs | 6 + .../Meta/Manipulations/ImcManipulation.cs | 45 ++++-- .../Meta/Manipulations/MetaManipulation.cs | 151 ++++++++++-------- .../Meta/Manipulations/RspManipulation.cs | 52 +++--- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 2 +- 17 files changed, 254 insertions(+), 158 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index d88d069e..369374c6 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -69,7 +69,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Subscribe(new Action(value), Communication.CreatingCharacterBase.Priority.Api); + _communicator.CreatingCharacterBase.Subscribe(new Action(value), + Communication.CreatingCharacterBase.Priority.Api); } remove { @@ -89,7 +90,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value), Communication.CreatedCharacterBase.Priority.Api); + _communicator.CreatedCharacterBase.Subscribe(new Action(value), + Communication.CreatedCharacterBase.Priority.Api); } remove { @@ -181,7 +183,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemClick? ChangedItemClicked { - add => _communicator.ChangedItemClick.Subscribe(new Action(value!), Communication.ChangedItemClick.Priority.Default); + add => _communicator.ChangedItemClick.Subscribe(new Action(value!), + Communication.ChangedItemClick.Priority.Default); remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!)); } @@ -1120,13 +1123,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi } manips = new HashSet(manipArray!.Length); - foreach (var manip in manipArray.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) + foreach (var manip in manipArray.Where(m => m.Validate())) { - if (!manips.Add(manip)) - { - manips = null; - return false; - } + if (manips.Add(manip)) + continue; + + Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); + manips = null; + return false; } return true; diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 7689e5b2..97f2aa8a 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -36,7 +36,7 @@ public readonly struct ImcCache : IDisposable public bool ApplyMod( MetaFileManager manager, ModCollection collection, ImcManipulation manip ) { - if( !manip.Valid ) + if( !manip.Validate() ) { return false; } @@ -76,7 +76,7 @@ public readonly struct ImcCache : IDisposable public bool RevertMod( MetaFileManager manager, ModCollection collection, ImcManipulation m ) { - if( !m.Valid || !_imcManipulations.Remove( m ) ) + if( !m.Validate() || !_imcManipulations.Remove( m ) ) { return false; } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index dacd51dd..adb92bfe 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api; using Penumbra.Util; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class CreatedCharacterBase : EventWrapper + /// Api = 0, } diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 357bc066..8dc1c634 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api; using Penumbra.Util; namespace Penumbra.Communication; @@ -16,7 +17,7 @@ public sealed class CreatingCharacterBase : EventWrapper + /// Api = 0, } diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index ec63337f..793663b9 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api; using Penumbra.Util; namespace Penumbra.Communication; @@ -13,7 +14,7 @@ public sealed class EnabledChanged : EventWrapper, EnabledChanged.P { public enum Priority { - /// + /// Api = int.MinValue, } diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 102ddec4..0f30b71b 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api; using Penumbra.Util; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ public sealed class ModDirectoryChanged : EventWrapper, Mod { public enum Priority { - /// + /// Api = 0, /// diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index e5b1e20c..e35a0ff4 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Penumbra.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Util; @@ -22,7 +23,7 @@ public sealed class ModPathChanged : EventWrapper CollectionCacheManagerAddition = -100, - /// + /// Api = 0, /// diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index e32a84c2..6445b956 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; @@ -21,7 +22,7 @@ public sealed class ModSettingChanged : EventWrapper + /// Api = int.MinValue, /// diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index dc2047b2..92d2aa5a 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -122,7 +122,7 @@ public partial class TexToolsMeta { var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value); - if (imc.Valid) + if (imc.Validate()) MetaManipulations.Add(imc); } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 3e927407..526abdd4 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -9,91 +9,102 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct EqdpManipulation : IMetaManipulation { public EqdpEntry Entry { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public Gender Gender { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public ModelRace Race { get; private init; } public ushort SetId { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) + public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId) { Gender = gender; Race = race; SetId = setId; Slot = slot; - Entry = Eqdp.Mask( Slot ) & entry; + Entry = Eqdp.Mask(Slot) & entry; } - public EqdpManipulation Copy( EqdpManipulation entry ) + public EqdpManipulation Copy(EqdpManipulation entry) { - if( entry.Slot != Slot ) + if (entry.Slot != Slot) { - var (bit1, bit2) = entry.Entry.ToBits( entry.Slot ); - return new EqdpManipulation(Eqdp.FromSlotAndBits( Slot, bit1, bit2 ), Slot, Gender, Race, SetId); + var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); + return new EqdpManipulation(Eqdp.FromSlotAndBits(Slot, bit1, bit2), Slot, Gender, Race, SetId); } + return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); } - public EqdpManipulation Copy( EqdpEntry entry ) + public EqdpManipulation Copy(EqdpEntry entry) => new(entry, Slot, Gender, Race, SetId); public override string ToString() => $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}"; - public bool Equals( EqdpManipulation other ) + public bool Equals(EqdpManipulation other) => Gender == other.Gender - && Race == other.Race + && Race == other.Race && SetId == other.SetId - && Slot == other.Slot; + && Slot == other.Slot; - public override bool Equals( object? obj ) - => obj is EqdpManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is EqdpManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot ); + => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - public int CompareTo( EqdpManipulation other ) + public int CompareTo(EqdpManipulation other) { - var r = Race.CompareTo( other.Race ); - if( r != 0 ) - { + var r = Race.CompareTo(other.Race); + if (r != 0) return r; - } - var g = Gender.CompareTo( other.Gender ); - if( g != 0 ) - { + var g = Gender.CompareTo(other.Gender); + if (g != 0) return g; - } - var set = SetId.CompareTo( other.SetId ); - return set != 0 ? set : Slot.CompareTo( other.Slot ); + var set = SetId.CompareTo(other.SetId); + return set != 0 ? set : Slot.CompareTo(other.Slot); } public MetaIndex FileIndex() - => CharacterUtilityData.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() ); + => CharacterUtilityData.EqdpIdx(Names.CombinedRace(Gender, Race), Slot.IsAccessory()); - public bool Apply( ExpandedEqdpFile file ) + public bool Apply(ExpandedEqdpFile file) { - var entry = file[ SetId ]; - var mask = Eqdp.Mask( Slot ); - if( ( entry & mask ) == Entry ) - { + var entry = file[SetId]; + var mask = Eqdp.Mask(Slot); + if ((entry & mask) == Entry) return false; - } - file[ SetId ] = ( entry & ~mask ) | Entry; + file[SetId] = (entry & ~mask) | Entry; return true; } -} \ No newline at end of file + + public bool Validate() + { + var mask = Eqdp.Mask(Slot); + if (mask == 0) + return false; + + if ((mask & Entry) != Entry) + return false; + + if (FileIndex() == (MetaIndex)(-1)) + return false; + + // No check for set id. + return true; + } +} diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 9d9010d2..b7a17c19 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -67,4 +67,17 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > file[ SetId ] = ( entry & ~mask ) | Entry; return true; } + + public bool Validate() + { + var mask = Eqp.Mask(Slot); + if (mask == 0) + return false; + if ((Entry & mask) != Entry) + return false; + + // No check for set id. + + return true; + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 24c22024..497c9219 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -103,4 +103,14 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > _ => throw new ArgumentOutOfRangeException(), }; } + + public bool Validate() + { + if (!Enum.IsDefined(Slot)) + return false; + if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown) + return false; + // No known check for set id or entry. + return true; + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index b8464abf..38669d12 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -51,4 +51,10 @@ public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > file[ SetId ] = Entry; return true; } + + public bool Validate() + { + // No known conditions. + return true; + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 2db291bd..9046f85e 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -72,15 +73,6 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > } } - public bool Valid - => ObjectType switch - { - ObjectType.Accessory => BodySlot == BodySlot.Unknown, - ObjectType.Equipment => BodySlot == BodySlot.Unknown, - ObjectType.DemiHuman => BodySlot == BodySlot.Unknown, - _ => EquipSlot == EquipSlot.Unknown, - }; - public ImcManipulation Copy( ImcEntry entry ) => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant, EquipSlot, entry); @@ -160,4 +152,39 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > public bool Apply( ImcFile file ) => file.SetEntry( ImcFile.PartIndex( EquipSlot ), Variant, Entry ); + + public bool Validate() + { + switch (ObjectType) + { + case ObjectType.Accessory: + case ObjectType.Equipment: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipmentPiece()) + return false; + if (SecondaryId != 0) + return false; + break; + case ObjectType.DemiHuman: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipmentPiece()) + return false; + break; + default: + if (!Enum.IsDefined(BodySlot)) + return false; + if (EquipSlot is not EquipSlot.Unknown) + return false; + if (!Enum.IsDefined(ObjectType)) + return false; + break; + } + + if (Entry.MaterialId == 0) + return false; + + return true; + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 98d138dd..1e0760f1 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -12,12 +12,12 @@ public interface IMetaManipulation public MetaIndex FileIndex(); } -public interface IMetaManipulation< T > - : IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct +public interface IMetaManipulation + : IMetaManipulation, IComparable, IEquatable where T : struct { } -[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] -public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > +[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 16)] +public readonly struct MetaManipulation : IEquatable, IComparable { public const int CurrentVersion = 0; @@ -32,33 +32,33 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Rsp = 6, } - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly EqpManipulation Eqp = default; - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly GmpManipulation Gmp = default; - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly EqdpManipulation Eqdp = default; - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly EstManipulation Est = default; - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly RspManipulation Rsp = default; - [FieldOffset( 0 )] + [FieldOffset(0)] [JsonIgnore] public readonly ImcManipulation Imc = default; - [FieldOffset( 15 )] - [JsonConverter( typeof( StringEnumConverter ) )] - [JsonProperty( "Type" )] + [FieldOffset(15)] + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("Type")] public readonly Type ManipulationType; public object? Manipulation @@ -76,149 +76,157 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa }; init { - switch( value ) + switch (value) { case EqpManipulation m: Eqp = m; - ManipulationType = Type.Eqp; + ManipulationType = m.Validate() ? Type.Eqp : Type.Unknown; return; case EqdpManipulation m: Eqdp = m; - ManipulationType = Type.Eqdp; + ManipulationType = m.Validate() ? Type.Eqdp : Type.Unknown; return; case GmpManipulation m: Gmp = m; - ManipulationType = Type.Gmp; + ManipulationType = m.Validate() ? Type.Gmp : Type.Unknown; return; case EstManipulation m: Est = m; - ManipulationType = Type.Est; + ManipulationType = m.Validate() ? Type.Est : Type.Unknown; return; case RspManipulation m: Rsp = m; - ManipulationType = Type.Rsp; + ManipulationType = m.Validate() ? Type.Rsp : Type.Unknown; return; case ImcManipulation m: Imc = m; - ManipulationType = m.Valid ? Type.Imc : Type.Unknown; + ManipulationType = m.Validate() ? Type.Imc : Type.Unknown; return; } } } - public MetaManipulation( EqpManipulation eqp ) + public bool Validate() + { + return ManipulationType switch + { + Type.Imc => Imc.Validate(), + Type.Eqdp => Eqdp.Validate(), + Type.Eqp => Eqp.Validate(), + Type.Est => Est.Validate(), + Type.Gmp => Gmp.Validate(), + Type.Rsp => Rsp.Validate(), + _ => false, + }; + } + + public MetaManipulation(EqpManipulation eqp) { Eqp = eqp; ManipulationType = Type.Eqp; } - public MetaManipulation( GmpManipulation gmp ) + public MetaManipulation(GmpManipulation gmp) { Gmp = gmp; ManipulationType = Type.Gmp; } - public MetaManipulation( EqdpManipulation eqdp ) + public MetaManipulation(EqdpManipulation eqdp) { Eqdp = eqdp; ManipulationType = Type.Eqdp; } - public MetaManipulation( EstManipulation est ) + public MetaManipulation(EstManipulation est) { Est = est; ManipulationType = Type.Est; } - public MetaManipulation( RspManipulation rsp ) + public MetaManipulation(RspManipulation rsp) { Rsp = rsp; ManipulationType = Type.Rsp; } - public MetaManipulation( ImcManipulation imc ) + public MetaManipulation(ImcManipulation imc) { Imc = imc; ManipulationType = Type.Imc; } - public static implicit operator MetaManipulation( EqpManipulation eqp ) + public static implicit operator MetaManipulation(EqpManipulation eqp) => new(eqp); - public static implicit operator MetaManipulation( GmpManipulation gmp ) + public static implicit operator MetaManipulation(GmpManipulation gmp) => new(gmp); - public static implicit operator MetaManipulation( EqdpManipulation eqdp ) + public static implicit operator MetaManipulation(EqdpManipulation eqdp) => new(eqdp); - public static implicit operator MetaManipulation( EstManipulation est ) + public static implicit operator MetaManipulation(EstManipulation est) => new(est); - public static implicit operator MetaManipulation( RspManipulation rsp ) + public static implicit operator MetaManipulation(RspManipulation rsp) => new(rsp); - public static implicit operator MetaManipulation( ImcManipulation imc ) + public static implicit operator MetaManipulation(ImcManipulation imc) => new(imc); - public bool EntryEquals( MetaManipulation other ) + public bool EntryEquals(MetaManipulation other) { - if( ManipulationType != other.ManipulationType ) - { + if (ManipulationType != other.ManipulationType) return false; - } return ManipulationType switch { - Type.Eqp => Eqp.Entry.Equals( other.Eqp.Entry ), - Type.Gmp => Gmp.Entry.Equals( other.Gmp.Entry ), - Type.Eqdp => Eqdp.Entry.Equals( other.Eqdp.Entry ), - Type.Est => Est.Entry.Equals( other.Est.Entry ), - Type.Rsp => Rsp.Entry.Equals( other.Rsp.Entry ), - Type.Imc => Imc.Entry.Equals( other.Imc.Entry ), + Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), + Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), + Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), + Type.Est => Est.Entry.Equals(other.Est.Entry), + Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), + Type.Imc => Imc.Entry.Equals(other.Imc.Entry), _ => throw new ArgumentOutOfRangeException(), }; } - public bool Equals( MetaManipulation other ) + public bool Equals(MetaManipulation other) { - if( ManipulationType != other.ManipulationType ) - { + if (ManipulationType != other.ManipulationType) return false; - } return ManipulationType switch { - Type.Eqp => Eqp.Equals( other.Eqp ), - Type.Gmp => Gmp.Equals( other.Gmp ), - Type.Eqdp => Eqdp.Equals( other.Eqdp ), - Type.Est => Est.Equals( other.Est ), - Type.Rsp => Rsp.Equals( other.Rsp ), - Type.Imc => Imc.Equals( other.Imc ), + Type.Eqp => Eqp.Equals(other.Eqp), + Type.Gmp => Gmp.Equals(other.Gmp), + Type.Eqdp => Eqdp.Equals(other.Eqdp), + Type.Est => Est.Equals(other.Est), + Type.Rsp => Rsp.Equals(other.Rsp), + Type.Imc => Imc.Equals(other.Imc), _ => false, }; } - public MetaManipulation WithEntryOf( MetaManipulation other ) + public MetaManipulation WithEntryOf(MetaManipulation other) { - if( ManipulationType != other.ManipulationType ) - { + if (ManipulationType != other.ManipulationType) return this; - } return ManipulationType switch { - Type.Eqp => Eqp.Copy( other.Eqp.Entry ), - Type.Gmp => Gmp.Copy( other.Gmp.Entry ), - Type.Eqdp => Eqdp.Copy( other.Eqdp ), - Type.Est => Est.Copy( other.Est.Entry ), - Type.Rsp => Rsp.Copy( other.Rsp.Entry ), - Type.Imc => Imc.Copy( other.Imc.Entry ), + Type.Eqp => Eqp.Copy(other.Eqp.Entry), + Type.Gmp => Gmp.Copy(other.Gmp.Entry), + Type.Eqdp => Eqdp.Copy(other.Eqdp), + Type.Est => Est.Copy(other.Est.Entry), + Type.Rsp => Rsp.Copy(other.Rsp.Entry), + Type.Imc => Imc.Copy(other.Imc.Entry), _ => throw new ArgumentOutOfRangeException(), }; } - public override bool Equals( object? obj ) - => obj is MetaManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is MetaManipulation other && Equals(other); public override int GetHashCode() => ManipulationType switch @@ -232,11 +240,11 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa _ => 0, }; - public unsafe int CompareTo( MetaManipulation other ) + public unsafe int CompareTo(MetaManipulation other) { - fixed( MetaManipulation* lhs = &this ) + fixed (MetaManipulation* lhs = &this) { - return MemoryUtility.MemCmpUnchecked( lhs, &other, sizeof( MetaManipulation ) ); + return MemoryUtility.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); } } @@ -255,12 +263,13 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa public string EntryToString() => ManipulationType switch { - Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{( ushort )Eqdp.Entry:X}", - Type.Eqp => $"{( ulong )Eqp.Entry:X}", + Type.Imc => + $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", + Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", + Type.Eqp => $"{(ulong)Eqp.Entry:X}", Type.Est => $"{Est.Entry}", Type.Gmp => $"{Gmp.Entry.Value}", Type.Rsp => $"{Rsp.Entry}", _ => string.Empty, }; -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 397f80dc..734488ae 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -8,59 +8,69 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct RspManipulation : IMetaManipulation< RspManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct RspManipulation : IMetaManipulation { public float Entry { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public SubRace SubRace { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public RspAttribute Attribute { get; private init; } [JsonConstructor] - public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) + public RspManipulation(SubRace subRace, RspAttribute attribute, float entry) { Entry = entry; SubRace = subRace; Attribute = attribute; } - public RspManipulation Copy( float entry ) + public RspManipulation Copy(float entry) => new(SubRace, Attribute, entry); public override string ToString() => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; - public bool Equals( RspManipulation other ) - => SubRace == other.SubRace + public bool Equals(RspManipulation other) + => SubRace == other.SubRace && Attribute == other.Attribute; - public override bool Equals( object? obj ) - => obj is RspManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is RspManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine( ( int )SubRace, ( int )Attribute ); + => HashCode.Combine((int)SubRace, (int)Attribute); - public int CompareTo( RspManipulation other ) + public int CompareTo(RspManipulation other) { - var s = SubRace.CompareTo( other.SubRace ); - return s != 0 ? s : Attribute.CompareTo( other.Attribute ); + var s = SubRace.CompareTo(other.SubRace); + return s != 0 ? s : Attribute.CompareTo(other.Attribute); } public MetaIndex FileIndex() => MetaIndex.HumanCmp; - public bool Apply( CmpFile file ) + public bool Apply(CmpFile file) { - var value = file[ SubRace, Attribute ]; - if( value == Entry ) - { + var value = file[SubRace, Attribute]; + if (value == Entry) return false; - } - file[ SubRace, Attribute ] = Entry; + file[SubRace, Attribute] = Entry; return true; } -} \ No newline at end of file + + public bool Validate() + { + if (SubRace is SubRace.Unknown || !Enum.IsDefined(SubRace)) + return false; + if (!Enum.IsDefined(Attribute)) + return false; + if (Entry is <= 1e-2f or > 8f) + return false; + + return true; + } +} diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 89b3d5bf..24e9aafe 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -89,7 +89,7 @@ public sealed class SubMod : ISubMod var manips = json[nameof(Manipulations)]; if (manips != null) foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) + .Where(m => m.Validate())) ManipulationData.Add(s); } From 1a36b7455788243db03a569e68c7a3e446431f8f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 May 2023 15:24:28 +0200 Subject: [PATCH 0972/2451] Fix 6.4 sigs. --- Penumbra.GameData/Signatures.cs | 4 +- Penumbra/Collections/Cache/MetaCache.cs | 54 ++++++++++++++++--------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs index 5b7f139b..3bdb22ba 100644 --- a/Penumbra.GameData/Signatures.cs +++ b/Penumbra.GameData/Signatures.cs @@ -52,12 +52,12 @@ public static class Sigs public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86"; // PathResolver.PathState - public const string HumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1"; + public const string HumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 89 8B"; public const string WeaponVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B"; - public const string DemiHumanVTable = "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA"; + public const string DemiHumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8B C3 89 8B"; public const string MonsterVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83"; // PathResolver.Subfiles diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index e2bff762..6906f6dc 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -25,7 +25,12 @@ public class MetaCache : IDisposable, IEnumerable _manipulations.TryGetValue(manip, out mod); + { + lock (_manipulations) + { + return _manipulations.TryGetValue(manip, out mod); + } + } public int Count => _manipulations.Count; @@ -85,10 +90,13 @@ public class MetaCache : IDisposable, IEnumerable _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, + loaded += manip.ManipulationType switch + { + MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), + MetaManipulation.Type.Unknown => false, + _ => false, + } + ? 1 + : 0; } - ? 1 - : 0; } - + _manager.ApplyDefaultFiles(_collection); _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations."); From b51ced8cfbf409df0583ae708006ce7b1c588614 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 May 2023 16:33:05 +0200 Subject: [PATCH 0973/2451] Fix Stain change. --- Penumbra.GameData/Structs/Stain.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData/Structs/Stain.cs b/Penumbra.GameData/Structs/Stain.cs index c8b47b42..3b0323d3 100644 --- a/Penumbra.GameData/Structs/Stain.cs +++ b/Penumbra.GameData/Structs/Stain.cs @@ -30,7 +30,7 @@ public readonly struct Stain => ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000; public Stain(Lumina.Excel.GeneratedSheets.Stain stain) - : this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown4) + : this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown5) { } internal Stain(string name, uint dye, byte index, bool gloss) From 78aff2b9dcf6b24ea3766555a71e7dd3cdf7fe2b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 00:56:15 +0200 Subject: [PATCH 0974/2451] Fix some warnings. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 6 +++--- Penumbra/Interop/PathResolving/CollectionResolver.cs | 8 ++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 51e1811a..e4e5ae57 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -250,7 +250,7 @@ public sealed partial class ActorManager : IDisposable private unsafe bool SearchPlayerCustomize(Character* character, int idx, out ActorIdentifier id) { var other = (Character*)_objects.GetObjectAddress(idx); - if (other == null || !CustomizeData.ScreenActorEquals((CustomizeData*)character->CustomizeData, (CustomizeData*)other->CustomizeData)) + if (other == null || !CustomizeData.ScreenActorEquals((CustomizeData*)character->DrawData.CustomizeData.Data, (CustomizeData*)other->DrawData.CustomizeData.Data)) { id = ActorIdentifier.Invalid; return false; @@ -271,8 +271,8 @@ public sealed partial class ActorManager : IDisposable { static bool Compare(Character* a, Character* b) { - var data1 = (CustomizeData*)a->CustomizeData; - var data2 = (CustomizeData*)b->CustomizeData; + var data1 = (CustomizeData*)a->DrawData.CustomizeData.Data; + var data2 = (CustomizeData*)b->DrawData.CustomizeData.Data; var equals = CustomizeData.ScreenActorEquals(data1, data2); return equals; } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 6b314ca2..4e1f6963 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -216,13 +216,13 @@ public unsafe class CollectionResolver if (!IsModelHuman((uint)character->ModelCharaId)) return null; - if (character->CustomizeData[0] == 0) + if (character->DrawData.CustomizeData[0] == 0) { notYetReady = true; return null; } - var bodyType = character->CustomizeData[2]; + var bodyType = character->DrawData.CustomizeData[2]; var collection = bodyType switch { 3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly), @@ -232,8 +232,8 @@ public unsafe class CollectionResolver if (collection != null) return collection; - var race = (SubRace)character->CustomizeData[4]; - var gender = (Gender)(character->CustomizeData[1] + 1); + var race = (SubRace)character->DrawData.CustomizeData[4]; + var gender = (Gender)(character->DrawData.CustomizeData[1] + 1); var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 2cec991d..80e56231 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -35,7 +35,7 @@ public class ResourceTree { var character = (Character*)SourceAddress; var model = (CharacterBase*)character->GameObject.GetDrawObject(); - var equipment = new ReadOnlySpan(character->EquipSlotData, 10); + var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); ModelId = character->ModelCharaId; CustomizeData = character->DrawData.CustomizeData; From f303b9e443d5d74b573bd135e0d97c7c69f08331 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 01:14:40 +0200 Subject: [PATCH 0975/2451] Add 0.7.1.0 Changelog. --- Penumbra/UI/Changelog.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 9f493a8e..227eaf24 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -37,10 +37,35 @@ public class PenumbraChangelog Add7_0_0(Changelog); Add7_0_1(Changelog); Add7_0_4(Changelog); + Add7_1_0(Changelog); } #region Changelogs + private static void Add7_1_0(Changelog log) + => log.NextVersion("Version 0.7.1.0") + .RegisterEntry("Updated for patch 6.4 - there may be some oversights on edge cases, but I could not find any issues myself.") + .RegisterHighlight("This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is be detectable by the server.", 1) + .RegisterEntry("Added a Mod Merging tab in the Advanced Editing Window. This can help you merge multiple mods to one, or split off specific options from an existing mod into a new mod.") + .RegisterEntry("Added advanced options to configure the minimum allowed window size for the main window (to reduce it). This is not quite supported and may look bad, so only use it if you really need smaller windows.") + .RegisterEntry("The last tab selected in the main window is now saved and re-used when relaunching Penumbra.") + .RegisterEntry("Added a hook to correctly associate some sounds that are played while weapons are drawn.") + .RegisterEntry("Added a hook to correctly associate sounds that are played while dismounting.") + .RegisterEntry("A hook to associate weapon-associated VFX was expanded to work in more cases.") + .RegisterEntry("TMB resources now use a collection prefix to prevent retained state in some cases.") + .RegisterEntry("Improved startup times a bit.") + .RegisterEntry("Right-Click context menus for collections are now also ordered by name.") + .RegisterEntry("Advanced Editing tabs have been reordered and renamed slightly.") + .RegisterEntry("Added some validation of metadata changes to prevent stalling on load of bad IMC edits.") + .RegisterEntry("Fixed an issue where collections could lose their configured inheritances during startup in some cases.") + .RegisterEntry("Fixed some bugs when mods were removed from collection caches.") + .RegisterEntry("Fixed some bugs with IMC files not correctly reverting to default values in some cases.") + .RegisterEntry("Fixed an issue with the mod import popup not appearing (0.7.0.10)") + .RegisterEntry("Fixed an issue with the file selectors not always opening at the expected locations. (0.7.0.7)") + .RegisterEntry("Fixed some cache handling issues. (0.7.0.5 - 0.7.0.10)") + .RegisterEntry("Fixed an issue with multiple collection context menus appearing for some identifiers (0.7.0.5)") + .RegisterEntry("Fixed an issue where the Update Bibo button did only work if the Advanced Editing window was opened before. (0.7.0.5)"); + private static void Add7_0_4(Changelog log) => log.NextVersion("Version 0.7.0.4") .RegisterEntry("Added options to the bulktag slash command to check all/local/mod tags specifically.") From 575c1e21183529a5fb941fe0850dcc5fd798a587 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 24 May 2023 23:17:40 +0000 Subject: [PATCH 0976/2451] [CI] Updating repo.json for refs/tags/0.7.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index d81eb71e..8724755f 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.0.10", - "TestingAssemblyVersion": "0.7.0.10", + "AssemblyVersion": "0.7.1.0", + "TestingAssemblyVersion": "0.7.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.0.10/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From edcfea5701e614c158fe4383ebf9814655d52776 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 21:25:45 +0200 Subject: [PATCH 0977/2451] Allow giantess fetish rsp scaling values. --- Penumbra/Meta/Manipulations/RspManipulation.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 734488ae..e61fafea 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -11,7 +11,9 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct RspManipulation : IMetaManipulation { - public float Entry { get; private init; } + public const float MinValue = 0.01f; + public const float MaxValue = 512f; + public float Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public SubRace SubRace { get; private init; } @@ -68,7 +70,7 @@ public readonly struct RspManipulation : IMetaManipulation return false; if (!Enum.IsDefined(Attribute)) return false; - if (Entry is <= 1e-2f or > 8f) + if (Entry is < MinValue or > MaxValue) return false; return true; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index e6d8d40c..9607d2ac 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -790,7 +790,7 @@ public partial class ModEditWindow using var color = ImRaii.PushColor(ImGuiCol.FrameBg, def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, 0.01f, 8f) && value is >= 0.01f and <= 8f) + if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) && value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) editor.MetaEditor.Change(meta.Copy(value)); ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); From 99506048679f2677002ddb38be2638928b8a6fc4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 May 2023 01:24:10 +0200 Subject: [PATCH 0978/2451] Fix LFinger not being valid for IMC files. --- Penumbra/Meta/Manipulations/ImcManipulation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 9046f85e..d933f73a 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -161,7 +161,7 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > case ObjectType.Equipment: if (BodySlot is not BodySlot.Unknown) return false; - if (!EquipSlot.IsEquipmentPiece()) + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) return false; if (SecondaryId != 0) return false; @@ -169,7 +169,7 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > case ObjectType.DemiHuman: if (BodySlot is not BodySlot.Unknown) return false; - if (!EquipSlot.IsEquipmentPiece()) + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) return false; break; default: From 7244d63e1e1f51ee83ba14c86bf6ef68b3309420 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 May 2023 02:11:06 +0200 Subject: [PATCH 0979/2451] Fix some bugs? --- OtterGui | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 15 +++------ .../Cache/CollectionCacheManager.cs | 33 +++++++++++++++++-- Penumbra/Meta/MetaFileManager.cs | 7 ++-- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/OtterGui b/OtterGui index 6969f4c0..d59cf504 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6969f4c05b2fab57e0dc30ae249be7c23cd81fa4 +Subproject commit d59cf50483fc81af5deb871bac76f70d42d7b8c1 diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 56539526..e753c8c8 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -3,6 +3,7 @@ using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -127,19 +128,13 @@ public class CollectionCache : IDisposable { if (!CheckFullPath(path, fullPath)) return; - - if (ResolvedFiles.Remove(path, out var modPath)) - ModData.RemovePath(modPath.Mod, path); - ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); - ModData.AddPath(Mod.ForcedFiles, path); + + _manager.ForceFile(this, path, fullPath); } /// Force a file resolve to be removed. internal void RemovePath(Utf8GamePath path) - { - if (ResolvedFiles.Remove(path, out var modPath)) - ModData.RemovePath(modPath.Mod, path); - } + => _manager.ForceFile(this, path, FullPath.Empty); public void ReloadMod(IMod mod, bool addMetaChanges) { @@ -153,7 +148,7 @@ public class CollectionCache : IDisposable var (paths, manipulations) = ModData.RemoveMod(mod); if (addMetaChanges) - ++_collection.ChangeCounter; + ++_collection.ChangeCounter; foreach (var path in paths) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5417aa19..d7408e9b 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Dalamud.Game; using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; @@ -13,6 +15,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; @@ -27,6 +30,8 @@ public class CollectionCacheManager : IDisposable internal readonly MetaFileManager MetaFileManager; + private readonly ConcurrentQueue<(CollectionCache, Utf8GamePath, FullPath)> _forcedFileQueue = new(); + private int _count; public int Count @@ -48,13 +53,15 @@ public class CollectionCacheManager : IDisposable if (!_active.Individuals.IsLoaded) _active.Individuals.Loaded += CreateNecessaryCaches; + _framework.Framework.Update += OnFramework; _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionCacheManager); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, ModPathChanged.Priority.CollectionCacheManagerAddition); _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, ModPathChanged.Priority.CollectionCacheManagerRemoval); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange, TemporaryGlobalModChange.Priority.CollectionCacheManager); _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionCacheManager); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, ModSettingChanged.Priority.CollectionCacheManager); - _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange, CollectionInheritanceChanged.Priority.CollectionCacheManager); + _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange, + CollectionInheritanceChanged.Priority.CollectionCacheManager); _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionCacheManager); _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager); @@ -95,6 +102,9 @@ public class CollectionCacheManager : IDisposable => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, () => CalculateEffectiveFileListInternal(collection)); + public void ForceFile(CollectionCache cache, Utf8GamePath path, FullPath fullPath) + => _forcedFileQueue.Enqueue((cache, path, fullPath)); + private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. @@ -163,9 +173,14 @@ public class CollectionCacheManager : IDisposable else { RemoveCache(old); - if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection)) CalculateEffectiveFileList(newCollection); + + if (type is CollectionType.Default) + if (newCollection != null) + MetaFileManager.ApplyDefaultFiles(newCollection); + else + MetaFileManager.CharacterUtility.ResetAll(); } } @@ -338,4 +353,18 @@ public class CollectionCacheManager : IDisposable var tasks = Active.Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))).ToArray(); Task.WaitAll(tasks); } + + /// + /// Update forced files only on framework. + /// + private void OnFramework(Framework _) + { + while (_forcedFileQueue.TryDequeue(out var tuple)) + { + if (tuple.Item1.ResolvedFiles.Remove(tuple.Item2, out var modPath)) + tuple.Item1.ModData.RemovePath(modPath.Mod, tuple.Item2); + if (tuple.Item3.FullName.Length > 0) + tuple.Item1.ResolvedFiles.Add(tuple.Item2, new ModPath(Mod.ForcedFiles, tuple.Item3)); + } + } } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 8e764b14..1cb3319e 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -84,13 +84,16 @@ public unsafe class MetaFileManager : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length) : MetaList.MetaReverter.Disabled; - public void ApplyDefaultFiles(ModCollection collection) + public void ApplyDefaultFiles(ModCollection? collection) { if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods) return; ResidentResources.Reload(); - collection._cache?.Meta.SetFiles(); + if (collection?._cache == null) + CharacterUtility.ResetAll(); + else + collection._cache.Meta.SetFiles(); } /// From 96aaefd3e26503a02ae22a77a855b8e179532e9a Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 26 May 2023 00:16:06 +0000 Subject: [PATCH 0980/2451] [CI] Updating repo.json for refs/tags/0.7.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8724755f..f342936a 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.0", - "TestingAssemblyVersion": "0.7.1.0", + "AssemblyVersion": "0.7.1.1", + "TestingAssemblyVersion": "0.7.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f938531e21b42a8da8abbedbfd4c0ea3f76baea8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 May 2023 13:56:43 +0200 Subject: [PATCH 0981/2451] Some small fixes/improvements. --- Penumbra.GameData/Data/DataSharer.cs | 1 - Penumbra/Collections/Cache/CollectionCache.cs | 1 - .../Collections/Cache/CollectionCacheManager.cs | 14 +++----------- Penumbra/CommandHandler.cs | 2 ++ Penumbra/Interop/Services/CharacterUtility.cs | 5 ++++- Penumbra/Penumbra.cs | 6 ++++-- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs index ce5fc0c3..3608a4a0 100644 --- a/Penumbra.GameData/Data/DataSharer.cs +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Dalamud; using Dalamud.Logging; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e753c8c8..298e683c 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -3,7 +3,6 @@ using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index d7408e9b..a14c6ac2 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -325,17 +325,12 @@ public class CollectionCacheManager : IDisposable /// public void CreateNecessaryCaches() { - var tasks = _active.SpecialAssignments.Select(p => p.Value) + Parallel.ForEach(_active.SpecialAssignments.Select(p => p.Value) .Concat(_active.Individuals.Select(p => p.Collection)) .Prepend(_active.Current) .Prepend(_active.Default) .Prepend(_active.Interface) - .Where(CreateCache) - .Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))) - .ToArray(); - - Penumbra.Log.Debug($"Creating {tasks.Length} necessary caches."); - Task.WaitAll(tasks); + .Where(CreateCache), CalculateEffectiveFileListInternal); } private void OnModDiscoveryStarted() @@ -349,10 +344,7 @@ public class CollectionCacheManager : IDisposable } private void OnModDiscoveryFinished() - { - var tasks = Active.Select(c => Task.Run(() => CalculateEffectiveFileListInternal(c))).ToArray(); - Task.WaitAll(tasks); - } + => Parallel.ForEach(Active, CalculateEffectiveFileListInternal); /// /// Update forced files only on framework. diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 9555f772..2e38448b 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -51,11 +51,13 @@ public class CommandHandler : IDisposable _collectionEditor = collectionEditor; framework.RunOnFrameworkThread(() => { + _commandManager.RemoveHandler(CommandName); _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", ShowInHelp = true, }); + Penumbra.Log.Information($"Registered {CommandName} with Dalamud."); }); } diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 3ac83e50..ef706f6d 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -140,7 +140,10 @@ public unsafe partial class CharacterUtility : IDisposable /// Return all relevant resources to the default resource. public void ResetAll() - { + { + if (!Ready) + return; + foreach (var list in _lists) list.Dispose(); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c70d688a..4876d530 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Text; @@ -86,8 +87,9 @@ public class Penumbra : IDalamudPlugin if (_characterUtility.Ready) _residentResources.Reload(); } - catch - { + catch(Exception ex) + { + Log.Error($"Error constructing Penumbra, Disposing again:\n{ex}"); Dispose(); throw; } From 0243e7a633f32fcee3ebbd126fad0a81f4a40d55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 May 2023 13:57:01 +0200 Subject: [PATCH 0982/2451] Improve deduplicator and normalizer. --- Penumbra/Mods/Editor/DuplicateManager.cs | 39 +++++++++++---------- Penumbra/Mods/Editor/ModNormalizer.cs | 12 ++++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 36 +++++++++---------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 98eda9e4..f4583f70 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Threading; using System.Threading.Tasks; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,21 +29,23 @@ public class DuplicateManager => _duplicates; public long SavedSpace { get; private set; } = 0; - public bool Finished { get; private set; } = true; + public Task Worker { get; private set; } = Task.CompletedTask; + + private CancellationTokenSource _cancellationTokenSource = new(); public void StartDuplicateCheck(IEnumerable files) { - if (!Finished) + if (!Worker.IsCompleted) return; - Finished = false; var filesTmp = files.OrderByDescending(f => f.FileSize).ToArray(); - Task.Run(() => CheckDuplicates(filesTmp)); + _cancellationTokenSource = new CancellationTokenSource(); + Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) { - if (!Finished || _duplicates.Count == 0) + if (!Worker.IsCompleted || _duplicates.Count == 0) return; foreach (var (set, _, _) in _duplicates) @@ -62,7 +65,9 @@ public class DuplicateManager public void Clear() { - Finished = true; + _cancellationTokenSource.Cancel(); + Worker = Task.CompletedTask; + _duplicates.Clear(); SavedSpace = 0; } @@ -110,7 +115,7 @@ public class DuplicateManager return to; } - private void CheckDuplicates(IReadOnlyList files) + private void CheckDuplicates(IReadOnlyList files, CancellationToken token) { _duplicates.Clear(); SavedSpace = 0; @@ -122,8 +127,7 @@ public class DuplicateManager if (file.SubModUsage.Any(f => f.Item2.Path.StartsWith("ui/"u8))) continue; - if (Finished) - return; + token.ThrowIfCancellationRequested(); if (file.FileSize == lastSize) { @@ -132,7 +136,7 @@ public class DuplicateManager } if (list.Count >= 2) - CheckMultiDuplicates(list, lastSize); + CheckMultiDuplicates(list, lastSize, token); lastSize = file.FileSize; @@ -141,26 +145,23 @@ public class DuplicateManager } if (list.Count >= 2) - CheckMultiDuplicates(list, lastSize); + CheckMultiDuplicates(list, lastSize, token); _duplicates.Sort((a, b) => a.Size != b.Size ? b.Size.CompareTo(a.Size) : a.Paths[0].CompareTo(b.Paths[0])); - Finished = true; } - private void CheckMultiDuplicates(IReadOnlyList list, long size) + private void CheckMultiDuplicates(IReadOnlyList list, long size, CancellationToken token) { var hashes = list.Select(f => (f, ComputeHash(f))).ToList(); while (hashes.Count > 0) { - if (Finished) - return; + token.ThrowIfCancellationRequested(); var set = new HashSet { hashes[0].Item1 }; var hash = hashes[0]; for (var j = 1; j < hashes.Count; ++j) { - if (Finished) - return; + token.ThrowIfCancellationRequested(); if (CompareHashes(hash.Item2, hashes[j].Item2) && CompareFilesDirectly(hashes[0].Item1, hashes[j].Item1)) set.Add(hashes[j].Item1); @@ -245,10 +246,10 @@ public class DuplicateManager var mod = new Mod(modDirectory); _modManager.Creator.ReloadMod(mod, true, out _); - Finished = false; + Clear(); var files = new ModFileCollection(); files.UpdateAll(mod, mod.Default); - CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray()); + CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray(), CancellationToken.None); DeleteDuplicates(files, mod, mod.Default, false); } catch (Exception e) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index aaf304db..8a918ec6 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -19,11 +19,13 @@ public class ModNormalizer private string _normalizationDirName = null!; private string _oldDirName = null!; - public int Step { get; private set; } - public int TotalSteps { get; private set; } + public int Step { get; private set; } + public int TotalSteps { get; private set; } + public Task Worker { get; private set; } = Task.CompletedTask; + public bool Running - => Step < TotalSteps; + => !Worker.IsCompleted; public ModNormalizer(ModManager modManager) => _modManager = modManager; @@ -39,7 +41,7 @@ public class ModNormalizer Step = 0; TotalSteps = mod.TotalFileCount + 5; - Task.Run(NormalizeSync); + Worker = Task.Run(NormalizeSync); } private void NormalizeSync() @@ -280,7 +282,7 @@ public class ModNormalizer private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods.OfType()) + foreach (var option in Mod.AllSubMods) _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 9f5b1ed3..f5fbaec3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -126,7 +126,7 @@ public partial class ModEditWindow : Window, IDisposable if (swaps > 0) sb.Append($" | {swaps} Swaps"); - _allowReduplicate = redirections != _editor.Files.Available.Count || _editor.Files.Available.Count > 0; + _allowReduplicate = redirections != _editor.Files.Available.Count || _editor.Files.Missing.Count > 0 || unused > 0; sb.Append(WindowBaseLabel); WindowName = sb.ToString(); } @@ -275,10 +275,17 @@ public partial class ModEditWindow : Window, IDisposable if (!tab) return; - var buttonText = _editor.Duplicates.Finished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; - if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", - !_editor.Duplicates.Finished)) - _editor.Duplicates.StartDuplicateCheck(_editor.Files.Available); + if (_editor.Duplicates.Worker.IsCompleted) + { + if (ImGuiUtil.DrawDisabledButton("Scan for Duplicates", Vector2.Zero, + "Search for identical files in this mod. This may take a while.", false)) + _editor.Duplicates.StartDuplicateCheck(_editor.Files.Available); + } + else + { + if (ImGuiUtil.DrawDisabledButton("Cancel Scanning for Duplicates", Vector2.Zero, "Cancel the current scanning operation...", false)) + _editor.Duplicates.Clear(); + } const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" @@ -290,28 +297,21 @@ public partial class ModEditWindow : Window, IDisposable var tt = _allowReduplicate ? desc : modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {_config.DeleteModModifier} to force normalization anyway."; - if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) - { - _editor.ModNormalizer.Normalize(_mod!); - _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx); - } - if (_editor.ModNormalizer.Running) { - using var popup = ImRaii.Popup("Normalization", ImGuiWindowFlags.Modal); ImGui.ProgressBar((float)_editor.ModNormalizer.Step / _editor.ModNormalizer.TotalSteps, new Vector2(300 * UiHelpers.Scale, ImGui.GetFrameHeight()), $"{_editor.ModNormalizer.Step} / {_editor.ModNormalizer.TotalSteps}"); } - - if (!_editor.Duplicates.Finished) + else if(ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - _editor.Duplicates.Clear(); - return; + _editor.ModNormalizer.Normalize(_mod!); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx)); } + if (!_editor.Duplicates.Worker.IsCompleted) + return; + if (_editor.Duplicates.Duplicates.Count == 0) { ImGui.NewLine(); From e98003eb09dac2d105d1597f8153ad27b7257ab3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 May 2023 15:43:37 +0200 Subject: [PATCH 0983/2451] Added some other task handling for collection caches. --- OtterGui | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 93 ++++++++++++++++--- .../Cache/CollectionCacheManager.cs | 27 +++--- Penumbra/Mods/Editor/ModBackup.cs | 5 +- Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Manager/ModExportManager.cs | 1 + Penumbra/Mods/Manager/ModManager.cs | 1 + Penumbra/Penumbra.cs | 8 +- Penumbra/Services/ConfigMigrationService.cs | 1 + Penumbra/Services/ServiceWrapper.cs | 5 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + 11 files changed, 110 insertions(+), 36 deletions(-) diff --git a/OtterGui b/OtterGui index d59cf504..73f6b14d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d59cf50483fc81af5deb871bac76f70d42d7b8c1 +Subproject commit 73f6b14d16920a94b7b98fe85973b9b2b959ada5 diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 298e683c..1196a110 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -122,26 +122,40 @@ public class CollectionCache : IDisposable return ret; } + public void ForceFile(Utf8GamePath path, FullPath fullPath) + => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath)); + + public void RemovePath(Utf8GamePath path) + => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); + + public void ReloadMod(IMod mod, bool addMetaChanges) + => _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges)); + + public void AddMod(IMod mod, bool addMetaChanges) + => _manager.AddChange(ChangeData.ModAddition(this, mod, addMetaChanges)); + + public void RemoveMod(IMod mod, bool addMetaChanges) + => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); + /// Force a file to be resolved to a specific path regardless of conflicts. - internal void ForceFile(Utf8GamePath path, FullPath fullPath) + private void ForceFileSync(Utf8GamePath path, FullPath fullPath) { if (!CheckFullPath(path, fullPath)) return; - - _manager.ForceFile(this, path, fullPath); + + if (ResolvedFiles.Remove(path, out var modPath)) + ModData.RemovePath(modPath.Mod, path); + if (fullPath.FullName.Length > 0) + ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); } - /// Force a file resolve to be removed. - internal void RemovePath(Utf8GamePath path) - => _manager.ForceFile(this, path, FullPath.Empty); - - public void ReloadMod(IMod mod, bool addMetaChanges) + internal void ReloadModSync(IMod mod, bool addMetaChanges) { - RemoveMod(mod, addMetaChanges); - AddMod(mod, addMetaChanges); + RemoveModSync(mod, addMetaChanges); + AddModSync(mod, addMetaChanges); } - public void RemoveMod(IMod mod, bool addMetaChanges) + internal void RemoveModSync(IMod mod, bool addMetaChanges) { var conflicts = Conflicts(mod); var (paths, manipulations) = ModData.RemoveMod(mod); @@ -168,7 +182,7 @@ public class CollectionCache : IDisposable { if (conflict.HasPriority) { - ReloadMod(conflict.Mod2, false); + ReloadModSync(conflict.Mod2, false); } else { @@ -185,8 +199,8 @@ public class CollectionCache : IDisposable } - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod(IMod mod, bool addMetaChanges) + /// Add all files and possibly manipulations of a given mod according to its settings in this collection. + internal void AddModSync(IMod mod, bool addMetaChanges) { if (mod.Index >= 0) { @@ -421,4 +435,55 @@ public class CollectionCache : IDisposable Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}"); return false; } + + public readonly record struct ChangeData + { + public readonly CollectionCache Cache; + public readonly Utf8GamePath Path; + public readonly FullPath FullPath; + public readonly IMod Mod; + public readonly byte Type; + public readonly bool AddMetaChanges; + + private ChangeData(CollectionCache cache, Utf8GamePath p, FullPath fp, IMod m, byte t, bool a) + { + Cache = cache; + Path = p; + FullPath = fp; + Mod = m; + Type = t; + AddMetaChanges = a; + } + + public static ChangeData ModRemoval(CollectionCache cache, IMod mod, bool addMetaChanges) + => new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 0, addMetaChanges); + + public static ChangeData ModAddition(CollectionCache cache, IMod mod, bool addMetaChanges) + => new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 1, addMetaChanges); + + public static ChangeData ModReload(CollectionCache cache, IMod mod, bool addMetaChanges) + => new(cache, Utf8GamePath.Empty, FullPath.Empty, mod, 2, addMetaChanges); + + public static ChangeData ForcedFile(CollectionCache cache, Utf8GamePath p, FullPath fp) + => new(cache, p, fp, Mods.Mod.ForcedFiles, 3, false); + + public void Apply() + { + switch (Type) + { + case 0: + Cache.RemoveModSync(Mod, AddMetaChanges); + break; + case 1: + Cache.AddModSync(Mod, AddMetaChanges); + break; + case 2: + Cache.ReloadModSync(Mod, AddMetaChanges); + break; + case 3: + Cache.ForceFileSync(Path, FullPath); + break; + } + } + } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index a14c6ac2..0c084c5c 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -15,7 +15,6 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; @@ -30,7 +29,7 @@ public class CollectionCacheManager : IDisposable internal readonly MetaFileManager MetaFileManager; - private readonly ConcurrentQueue<(CollectionCache, Utf8GamePath, FullPath)> _forcedFileQueue = new(); + private readonly ConcurrentQueue _changeQueue = new(); private int _count; @@ -81,6 +80,14 @@ public class CollectionCacheManager : IDisposable MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } + public void AddChange(CollectionCache.ChangeData data) + { + if (_framework.Framework.IsInFrameworkUpdateThread) + data.Apply(); + else + _changeQueue.Enqueue(data); + } + /// Only creates a new cache, does not update an existing one. public bool CreateCache(ModCollection collection) { @@ -102,9 +109,6 @@ public class CollectionCacheManager : IDisposable => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, () => CalculateEffectiveFileListInternal(collection)); - public void ForceFile(CollectionCache cache, Utf8GamePath path, FullPath fullPath) - => _forcedFileQueue.Enqueue((cache, path, fullPath)); - private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. @@ -143,10 +147,10 @@ public class CollectionCacheManager : IDisposable .Concat(_tempMods.Mods.TryGetValue(collection, out var list) ? list : Array.Empty())) - cache.AddMod(tempMod, false); + cache.AddModSync(tempMod, false); foreach (var mod in _modStorage) - cache.AddMod(mod, false); + cache.AddModSync(mod, false); cache.AddMetaFiles(); @@ -351,12 +355,7 @@ public class CollectionCacheManager : IDisposable /// private void OnFramework(Framework _) { - while (_forcedFileQueue.TryDequeue(out var tuple)) - { - if (tuple.Item1.ResolvedFiles.Remove(tuple.Item2, out var modPath)) - tuple.Item1.ModData.RemovePath(modPath.Mod, tuple.Item2); - if (tuple.Item3.FullName.Length > 0) - tuple.Item1.ResolvedFiles.Add(tuple.Item2, new ModPath(Mod.ForcedFiles, tuple.Item3)); - } + while (_changeQueue.TryDequeue(out var changeData)) + changeData.Apply(); } } diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 72680091..cbb98a38 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -3,9 +3,10 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Threading.Tasks; +using OtterGui; using Penumbra.Mods.Manager; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; /// Utility to create and apply a zipped backup of a mod. public class ModBackup @@ -80,7 +81,7 @@ public class ModBackup return; CreatingBackup = true; - await Task.Run(Create); + await AsyncTask.Run(Create); CreatingBackup = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 8a918ec6..0019b5f6 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -41,7 +41,7 @@ public class ModNormalizer Step = 0; TotalSteps = mod.TotalFileCount + 5; - Worker = Task.Run(NormalizeSync); + Worker = TrackedTask.Run(NormalizeSync); } private void NormalizeSync() diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 6bb919fc..f2bfb9bc 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Penumbra.Communication; +using Penumbra.Mods.Editor; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 7e7599f0..6c1ff9ab 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Penumbra.Communication; +using Penumbra.Mods.Editor; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4876d530..bd530ccd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -52,10 +52,14 @@ public class Penumbra : IDalamudPlugin try { var startTimer = new StartTracker(); - using var timer = startTimer.Measure(StartTimeType.Total); + using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); ChatService = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); + var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) + ? s.ToString() + : "Unknown"; + Log.Information($"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); _services.GetRequiredService(); // Initialize because not required anywhere else. _config = _services.GetRequiredService(); _characterUtility = _services.GetRequiredService(); @@ -114,7 +118,7 @@ public class Penumbra : IDalamudPlugin private void SetupInterface() { - Task.Run(() => + AsyncTask.Run(() => { using var tInterface = _services.GetRequiredService().Measure(StartTimeType.Interface); var system = _services.GetRequiredService(); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 8c0a60f1..5d19dbe5 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.Services; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; using Penumbra.Util; diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index cb4c86e8..5a43cf9a 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using OtterGui; using Penumbra.Util; namespace Penumbra.Services; @@ -58,7 +59,7 @@ public abstract class AsyncServiceWrapper protected AsyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) { Name = name; - _task = Task.Run(() => + _task = TrackedTask.Run(() => { using var timer = tracker.Measure(type); var service = factory(); @@ -84,7 +85,7 @@ public abstract class AsyncServiceWrapper protected AsyncServiceWrapper(string name, Func factory) { Name = name; - _task = Task.Run(() => + _task = TrackedTask.Run(() => { var service = factory(); if (_isDisposed) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 294f8041..f5b03659 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; From e28483d1ae6a7eb6c656c33834bf2295efd4e8d9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 May 2023 16:04:45 +0200 Subject: [PATCH 0984/2451] Stop failure to load on broken configuration files. --- OtterGui | 2 +- Penumbra/Configuration.cs | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 73f6b14d..7d0fcdba 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 73f6b14d16920a94b7b98fe85973b9b2b959ada5 +Subproject commit 7d0fcdbadf70f59675e8666de014e5dcb67da35d diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ba8ca1cc..83fd2e51 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Numerics; using Dalamud.Configuration; +using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; @@ -124,13 +125,20 @@ public class Configuration : IPluginConfiguration, ISavable } if (File.Exists(fileNames.ConfigFile)) - { - var text = File.ReadAllText(fileNames.ConfigFile); - JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + try { - Error = HandleDeserializationError, - }); - } + var text = File.ReadAllText(fileNames.ConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Penumbra.ChatService.NotificationMessage(ex, + "Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Penumbra directory.", + "Error reading Configuration", "Error", NotificationType.Error); + } migrator.Migrate(utility, this); } From 312cb236151c232f21f92328e68e51bd274f97e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 May 2023 16:13:51 +0200 Subject: [PATCH 0985/2451] Add Changelog. --- Penumbra/UI/Changelog.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 227eaf24..3a51fb28 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -38,14 +38,26 @@ public class PenumbraChangelog Add7_0_1(Changelog); Add7_0_4(Changelog); Add7_1_0(Changelog); + Add7_1_2(Changelog); } #region Changelogs + + private static void Add7_1_2(Changelog log) + => log.NextVersion("Version 0.7.1.2") + .RegisterEntry("Changed threaded handling of collection caches. Maybe this fixes the startup problems some people are experiencing.") + .RegisterEntry("This is just testing and may not be the solution, or may even make things worse. Sorry if I have to put out multiple small patches again to get this right.", 1) + .RegisterEntry("Fixed Penumbra failing to load if the main configuration file is corrupted.") + .RegisterEntry("Some miscellaneous small bug fixes.") + .RegisterEntry("Slight changes in behaviour for deduplicator/normalizer, mostly backend.") + .RegisterEntry("A typo in the 0.7.1.0 Changelog was been fixed.") + .RegisterEntry("Fixed left rings not being valid for IMC entries after validation. (7.1.1)") + .RegisterEntry("Relaxed the scaling restrictions for RSP scaling values to go from 0.01 to 512.0 instead of the prior upper limit of 8.0, in interface as well as validation, to better support the fetish community. (7.1.1)"); private static void Add7_1_0(Changelog log) => log.NextVersion("Version 0.7.1.0") .RegisterEntry("Updated for patch 6.4 - there may be some oversights on edge cases, but I could not find any issues myself.") - .RegisterHighlight("This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is be detectable by the server.", 1) + .RegisterHighlight("This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is detectable by the server.", 1) .RegisterEntry("Added a Mod Merging tab in the Advanced Editing Window. This can help you merge multiple mods to one, or split off specific options from an existing mod into a new mod.") .RegisterEntry("Added advanced options to configure the minimum allowed window size for the main window (to reduce it). This is not quite supported and may look bad, so only use it if you really need smaller windows.") .RegisterEntry("The last tab selected in the main window is now saved and re-used when relaunching Penumbra.") From f8d1fcf4e29b75cb6e2fdada9e4f6c5c1be3c170 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 May 2023 14:23:42 +0000 Subject: [PATCH 0986/2451] [CI] Updating repo.json for refs/tags/0.7.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f342936a..f3ab08ba 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.1", - "TestingAssemblyVersion": "0.7.1.1", + "AssemblyVersion": "0.7.1.2", + "TestingAssemblyVersion": "0.7.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 02fe5a4fb3e7005dc0554450eef1f41507ae1ae7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 May 2023 18:48:46 +0200 Subject: [PATCH 0987/2451] Maybe fix the race condition and add more logging. --- Penumbra/Collections/Cache/CollectionCache.cs | 49 ++++++++------ .../Cache/CollectionCacheManager.cs | 65 +++++++++++++------ Penumbra/Collections/Cache/ImcCache.cs | 14 +++- Penumbra/Collections/Cache/MetaCache.cs | 6 +- .../Manager/IndividualCollections.Files.cs | 13 ++-- Penumbra/Mods/Manager/ModManager.cs | 2 +- 6 files changed, 96 insertions(+), 53 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 1196a110..6daa78f3 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using Penumbra.Api.Enums; using Penumbra.String.Classes; using Penumbra.Mods.Manager; @@ -30,7 +31,10 @@ public class CollectionCache : IDisposable public readonly MetaCache Meta; public readonly Dictionary> _conflicts = new(); - public bool Calculating; + public int Calculating = -1; + + public string AnonymizedName + => _collection.AnonymizedName; public IEnumerable> AllConflicts => _conflicts.Values; @@ -138,7 +142,7 @@ public class CollectionCache : IDisposable => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); /// Force a file to be resolved to a specific path regardless of conflicts. - private void ForceFileSync(Utf8GamePath path, FullPath fullPath) + internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { if (!CheckFullPath(path, fullPath)) return; @@ -149,7 +153,7 @@ public class CollectionCache : IDisposable ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); } - internal void ReloadModSync(IMod mod, bool addMetaChanges) + private void ReloadModSync(IMod mod, bool addMetaChanges) { RemoveModSync(mod, addMetaChanges); AddModSync(mod, addMetaChanges); @@ -238,7 +242,7 @@ public class CollectionCache : IDisposable { ++_collection.ChangeCounter; if (mod.TotalManipulations > 0) - AddMetaFiles(); + AddMetaFiles(false); _manager.MetaFileManager.ApplyDefaultFiles(_collection); } @@ -263,22 +267,29 @@ public class CollectionCache : IDisposable if (!CheckFullPath(path, file)) return; - if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) + try { - ModData.AddPath(mod, path); - return; + if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) + { + ModData.AddPath(mod, path); + return; + } + + var modPath = ResolvedFiles[path]; + // Lower prioritized option in the same mod. + if (mod == modPath.Mod) + return; + + if (AddConflict(path, mod, modPath.Mod)) + { + ModData.RemovePath(modPath.Mod, path); + ResolvedFiles[path] = new ModPath(mod, file); + ModData.AddPath(mod, path); + } } - - var modPath = ResolvedFiles[path]; - // Lower prioritized option in the same mod. - if (mod == modPath.Mod) - return; - - if (AddConflict(path, mod, modPath.Mod)) + catch (Exception ex) { - ModData.RemovePath(modPath.Mod, path); - ResolvedFiles[path] = new ModPath(mod, file); - ModData.AddPath(mod, path); + Penumbra.Log.Error($"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); } } @@ -374,8 +385,8 @@ public class CollectionCache : IDisposable // Add all necessary meta file redirects. - public void AddMetaFiles() - => Meta.SetImcFiles(); + public void AddMetaFiles(bool fromFullCompute) + => Meta.SetImcFiles(fromFullCompute); // Identify and record all manipulated objects for this entire collection. diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 0c084c5c..5ac1ab7f 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -82,16 +82,30 @@ public class CollectionCacheManager : IDisposable public void AddChange(CollectionCache.ChangeData data) { - if (_framework.Framework.IsInFrameworkUpdateThread) + if (data.Cache.Calculating == -1) + { + if (_framework.Framework.IsInFrameworkUpdateThread) + data.Apply(); + else + _changeQueue.Enqueue(data); + } + else if (data.Cache.Calculating == Environment.CurrentManagedThreadId) + { data.Apply(); + } else + { _changeQueue.Enqueue(data); + } } /// Only creates a new cache, does not update an existing one. public bool CreateCache(ModCollection collection) { - if (collection.HasCache || collection.Index == ModCollection.Empty.Index) + if (collection.Index == ModCollection.Empty.Index) + return false; + + if (collection._cache != null) return false; collection._cache = new CollectionCache(this, collection); @@ -115,27 +129,33 @@ public class CollectionCacheManager : IDisposable if (collection.Index == 0) return; - Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); + Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); if (!collection.HasCache) { Penumbra.Log.Error( - $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); - return; + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); } + else if (collection._cache!.Calculating != -1) + { + Penumbra.Log.Error( + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); + } + else + { + FullRecalculation(collection); - FullRecalculation(collection); - - Penumbra.Log.Debug( - $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); + Penumbra.Log.Debug( + $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); + } } private void FullRecalculation(ModCollection collection) { var cache = collection._cache; - if (cache == null || cache.Calculating) + if (cache is not { Calculating: -1 }) return; - cache.Calculating = true; + cache.Calculating = Environment.CurrentManagedThreadId; try { cache.ResolvedFiles.Clear(); @@ -152,7 +172,7 @@ public class CollectionCacheManager : IDisposable foreach (var mod in _modStorage) cache.AddModSync(mod, false); - cache.AddMetaFiles(); + cache.AddMetaFiles(true); ++collection.ChangeCounter; @@ -160,7 +180,7 @@ public class CollectionCacheManager : IDisposable } finally { - cache.Calculating = false; + cache.Calculating = -1; } } @@ -329,12 +349,19 @@ public class CollectionCacheManager : IDisposable /// public void CreateNecessaryCaches() { - Parallel.ForEach(_active.SpecialAssignments.Select(p => p.Value) - .Concat(_active.Individuals.Select(p => p.Collection)) - .Prepend(_active.Current) - .Prepend(_active.Default) - .Prepend(_active.Interface) - .Where(CreateCache), CalculateEffectiveFileListInternal); + ModCollection[] caches; + // Lock to make sure no race conditions during CreateCache happen. + lock (this) + { + caches = _active.SpecialAssignments.Select(p => p.Value) + .Concat(_active.Individuals.Select(p => p.Collection)) + .Prepend(_active.Current) + .Prepend(_active.Default) + .Prepend(_active.Interface) + .Where(CreateCache).ToArray(); + } + + Parallel.ForEach(caches, CalculateEffectiveFileListInternal); } private void OnModDiscoveryStarted() diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 97f2aa8a..896e3078 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -17,10 +17,18 @@ public readonly struct ImcCache : IDisposable public ImcCache() { } - public void SetFiles(ModCollection collection) + public void SetFiles(ModCollection collection, bool fromFullCompute) { - foreach( var path in _imcFiles.Keys ) - collection._cache!.ForceFile( path, CreateImcPath( collection, path ) ); + if (fromFullCompute) + { + foreach (var path in _imcFiles.Keys) + collection._cache!.ForceFileSync(path, CreateImcPath(collection, path)); + } + else + { + foreach (var path in _imcFiles.Keys) + collection._cache!.ForceFile(path, CreateImcPath(collection, path)); + } } public void Reset(ModCollection collection) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 6906f6dc..ee589d1b 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -59,7 +59,7 @@ public class MetaCache : IDisposable, IEnumerable Set the currently relevant IMC files for the collection cache.
- public void SetImcFiles() - => _imcCache.SetFiles(_collection); + public void SetImcFiles(bool fromFullCompute) + => _imcCache.SetFiles(_collection, fromFullCompute); public MetaList.MetaReverter TemporarilySetEqpFile() => _eqpCache.TemporarilySetFiles(_manager); diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 4e238722..0225927e 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -32,14 +32,11 @@ public partial class IndividualCollections return ReadJObjectInternal(obj, storage); void Func() { - saver.DalamudFramework.RunOnFrameworkThread(() => - { - if (ReadJObjectInternal(obj, storage)) - saver.ImmediateSave(parent); - IsLoaded = true; - Loaded.Invoke(); - _actorService.FinishedCreation -= Func; - }); + if (ReadJObjectInternal(obj, storage)) + saver.ImmediateSave(parent); + IsLoaded = true; + Loaded.Invoke(); + _actorService.FinishedCreation -= Func; } _actorService.FinishedCreation += Func; return false; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 6c1ff9ab..38eff57b 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -77,7 +77,7 @@ public sealed class ModManager : ModStorage, IDisposable ScanMods(); _communicator.ModDiscoveryFinished.Invoke(); - Penumbra.Log.Information("Rediscovered mods."); + Penumbra.Log.Information($"Rediscovered {Mods.Count} mods."); if (ModBackup.MigrateModBackups) ModBackup.MigrateZipToPmp(this); From 9fb5ac65d185dbca650c83849cec9cb3fb5b8632 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 29 May 2023 16:51:14 +0000 Subject: [PATCH 0988/2451] [CI] Updating repo.json for refs/tags/0.7.1.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f3ab08ba..61f85a41 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.2", - "TestingAssemblyVersion": "0.7.1.2", + "AssemblyVersion": "0.7.1.3", + "TestingAssemblyVersion": "0.7.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 81891cfe09aec97ed27d284ee62af9dc8f347a22 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 May 2023 23:37:04 +0200 Subject: [PATCH 0989/2451] Add Sea of Stars as accepted repo. --- Penumbra/Services/ValidityChecker.cs | 11 ++++++----- Penumbra/UI/ConfigWindow.cs | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 3f55750b..44df07e3 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -10,9 +10,10 @@ namespace Penumbra.Services; public class ValidityChecker { - public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; - public const string RepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/master/repo.json"; - public const string TestRepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/test/repo.json"; + public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public const string SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json"; + public const string RepositoryLower = "https://raw.githubusercontent.com/xivdev/penumbra/master/repo.json"; + public const string SeaOfStarsLower = "https://raw.githubusercontent.com/ottermandias/seaofstars/main/repo.json"; public readonly bool DevPenumbraExists; public readonly bool IsNotInstalledPenumbra; @@ -82,11 +83,11 @@ public class ValidityChecker private static bool CheckSourceRepo( DalamudPluginInterface pi ) { #if !DEBUG - return pi.SourceRepository.Trim().ToLowerInvariant() switch + return pi.SourceRepository?.Trim().ToLowerInvariant() switch { null => false, RepositoryLower => true, - TestRepositoryLower => true, + SeaOfStarsLower => true, _ => false, }; #else diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 8d22fe01..7f484518 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -88,7 +88,7 @@ public sealed class ConfigWindow : Window { DrawProblemWindow( $"You are loading a release version of Penumbra from the repository \"{_pluginInterface.SourceRepository}\" instead of the official repository.\n" - + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + + $"Please use the official repository at {ValidityChecker.Repository} or the suite repository at {ValidityChecker.SeaOfStars}.\n\n" + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } else if (_validityChecker.IsNotInstalledPenumbra) From 381ab4befe012278c2d1cc81948d6d16f67a93df Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 30 May 2023 21:43:18 +0000 Subject: [PATCH 0990/2451] [CI] Updating repo.json for refs/tags/0.7.1.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 61f85a41..b34b8a98 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.3", - "TestingAssemblyVersion": "0.7.1.3", + "AssemblyVersion": "0.7.1.4", + "TestingAssemblyVersion": "0.7.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 2bc8092cca9ae9a08761abf90388aff6d4624350 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 May 2023 14:26:38 +0200 Subject: [PATCH 0991/2451] Try revamping release actions. --- .github/workflows/release.yml | 37 +++++++++++++++--------------- .github/workflows/test_release.yml | 37 +++++++++++++++--------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89ea2b56..267bf2dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,14 +24,15 @@ jobs: Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | - $ver = '${{ github.ref }}' -replace 'refs/tags/','' + $ver = '${{ github.ref_name }}' invoke-expression 'dotnet build --no-restore --configuration Release --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver' - - name: write version into json + - name: write version into jsons run: | - $ver = '${{ github.ref }}' -replace 'refs/tags/','' + $ver = '${{ github.ref_name }}' $path = './Penumbra/bin/Release/Penumbra.json' - $content = get-content -path $path - $content = $content -replace '1.0.0.0',$ver + $json = Get-Content -Raw $path | ConvertFrom-Json + $json.AssemblyVersion = $ver + $content = $json | ConvertTo-Json set-content -Path $path -Value $content - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip @@ -63,26 +64,24 @@ jobs: - name: Write out repo.json run: | - $ver = '${{ 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',$ver - set-content -Path $new_path -Value $content + $ver = '${{ github.ref_name }}' + $path = './repo.json' + $json = Get-Content -Raw $path | ConvertFrom-Json + $json[0].AssemblyVersion = $ver + $json[0].TestingAssemblyVersion = $ver + $json[0].DownloadLinkInstall = $json.DownloadLinkInstall -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" + $json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" + $json[0].DownloadLinkUpdate = $json.DownloadLinkUpdate -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" + $content = $json | ConvertTo-Json + set-content -Path $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 branch -f test ${{ github.sha }} + git branch -f master ${{ github.sha }} git checkout master git add repo.json - git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true - + git commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true git push origin master - git branch -f test origin/master - git checkout test - git push origin test -f || true diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 6ae471be..6175f3c1 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -24,14 +24,15 @@ jobs: Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | - $ver = '${{ github.ref }}' -replace 'refs/tags/t','' + $ver = '${{ github.ref_name }}' -replace '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','' + $ver = '${{ github.ref_name }}' -replace 't' $path = './Penumbra/bin/Debug/Penumbra.json' - $content = get-content -path $path - $content = $content -replace '1.0.0.0',$ver + $json = Get-Content -Raw $path | ConvertFrom-Json + $json.AssemblyVersion = $ver + $content = $json | ConvertTo-Json set-content -Path $path -Value $content - name: Archive run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip @@ -63,24 +64,22 @@ jobs: - 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 + $verT = '${{ github.ref_name }}' + $ver = $verT -replace 't' + $path = './repo.json' + $json = Get-Content -Raw $path | ConvertFrom-Json + $json[0].TestingAssemblyVersion = $ver + $json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$verT/Penumbra.zip" + $content = $json | ConvertTo-Json + set-content -Path $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 test - git branch -f test ${{ github.sha }} - git checkout test + git fetch origin master + git branch -f master ${{ github.sha }} + git checkout master git add repo.json - git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true - - git push origin test -f || true + git commit -m "[CI] Updating repo.json for ${{ github.ref_name }}" || true + git push origin master From 071317e168f5d593c2cb00ae312af75f3170a209 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 31 May 2023 12:41:53 +0000 Subject: [PATCH 0992/2451] [CI] Updating repo.json for t0.7.1.5 --- repo.json | 46 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/repo.json b/repo.json index b34b8a98..039e79cb 100644 --- a/repo.json +++ b/repo.json @@ -1,24 +1,22 @@ -[ - { - "Author": "Ottermandias, Adam, Wintermute", - "Name": "Penumbra", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.4", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 8, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" - } -] +{ + "Author": "Ottermandias, Adam, Wintermute", + "Name": "Penumbra", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "0.7.1.4", + "TestingAssemblyVersion": "0.7.1.5", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 8, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" +} From 77e76dd8a20b69bd04ba99998e69c30c2c47de4d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 May 2023 14:45:44 +0200 Subject: [PATCH 0993/2451] Remove now unneeded base-repo. --- base_repo.json | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 base_repo.json diff --git a/base_repo.json b/base_repo.json deleted file mode 100644 index 202d59de..00000000 --- a/base_repo.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "Author": "Ottermandias, Adam, Wintermute", - "Name": "Penumbra", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.0", - "TestingAssemblyVersion": "1.0.0.0", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 8, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", - "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" - } -] \ No newline at end of file From 5320f43491ed931e9071b2cd17c1fb0645fa2ab8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 May 2023 15:11:13 +0200 Subject: [PATCH 0994/2451] Make repo write out an array. --- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 267bf2dc..9337defb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: $json[0].DownloadLinkInstall = $json.DownloadLinkInstall -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" $json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" $json[0].DownloadLinkUpdate = $json.DownloadLinkUpdate -replace '[^/]+/Penumbra.zip',"$ver/Penumbra.zip" - $content = $json | ConvertTo-Json + $content = $json | ConvertTo-Json -AsArray set-content -Path $path -Value $content - name: Commit repo.json diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 6175f3c1..3c0b5a63 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -70,7 +70,7 @@ jobs: $json = Get-Content -Raw $path | ConvertFrom-Json $json[0].TestingAssemblyVersion = $ver $json[0].DownloadLinkTesting = $json.DownloadLinkTesting -replace '[^/]+/Penumbra.zip',"$verT/Penumbra.zip" - $content = $json | ConvertTo-Json + $content = $json | ConvertTo-Json -AsArray set-content -Path $path -Value $content - name: Commit repo.json From 3dc1553429437119f83d9fd3941b7392d4207b3b Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 31 May 2023 13:14:07 +0000 Subject: [PATCH 0995/2451] [CI] Updating repo.json for t0.7.1.5 --- repo.json | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/repo.json b/repo.json index 039e79cb..720c5ee9 100644 --- a/repo.json +++ b/repo.json @@ -1,22 +1,24 @@ -{ - "Author": "Ottermandias, Adam, Wintermute", - "Name": "Penumbra", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.5", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 8, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" -} +[ + { + "Author": "Ottermandias, Adam, Wintermute", + "Name": "Penumbra", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "0.7.1.4", + "TestingAssemblyVersion": "0.7.1.5", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 8, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" + } +] From 768016a897212e00052cd8a4d15e071a2e1073f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Jun 2023 15:58:53 +0200 Subject: [PATCH 0996/2451] Allow resetting text mod filter with right-click on arrow. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index 7d0fcdba..dc4ad8a5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 7d0fcdbadf70f59675e8666de014e5dcb67da35d +Subproject commit dc4ad8a5fd0347642d3fdae5f2dc17a7fbfacaa1 diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 5a53712d..0665c68a 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -652,12 +652,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector Add the state filter combo-button to the right of the filter box. - protected override float CustomFilters(float width) + protected override (float, bool) CustomFilters(float width) { var pos = ImGui.GetCursorPos(); var remainingWidth = width - ImGui.GetFrameHeight(); @@ -693,17 +696,17 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 2 Jun 2023 16:06:15 +0200 Subject: [PATCH 0997/2451] Add key-check for file deletion in advanced editing. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index ee93ad29..09578a69 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -287,14 +287,20 @@ public partial class ModEditWindow ImGui.SameLine(); - if (ImGui.Button("Delete Selected Files")) + var active = _config.DeleteModModifier.IsActive(); + var tt = + "Delete all selected files entirely from your filesystem, but not their file associations in the mod.\n!!!This can not be reverted!!!"; + if (_selectedFiles.Count == 0) + tt += "\n\nNo files selected."; + else if (!active) + tt += $"\n\nHold {_config.DeleteModModifier.ToString()} to delete."; + + if (ImGuiUtil.DrawDisabledButton("Delete Selected Files", Vector2.Zero, tt, _selectedFiles.Count == 0 || !active)) _editor.FileEditor.DeleteFiles(_editor.Mod!, _editor.Option!, _editor.Files.Available.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!!!"); ImGui.SameLine(); var changes = _editor.FileEditor.Changes; - var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; + tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); From 9255f2bb2bc5bca2092ef8c953f08acfc44ee834 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Jun 2023 00:04:30 +0200 Subject: [PATCH 0998/2451] Some readonlys. --- Penumbra.GameData/Structs/CharacterArmor.cs | 6 +++--- Penumbra.GameData/Structs/CharacterWeapon.cs | 4 ++-- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index eda75810..f44ae5e6 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -28,13 +28,13 @@ public struct CharacterArmor : IEquatable Stain = stain; } - public CharacterArmor With(StainId stain) + public readonly CharacterArmor With(StainId stain) => new(Set, Variant, stain); - public CharacterWeapon ToWeapon() + public readonly CharacterWeapon ToWeapon() => new(Set, 0, Variant, Stain); - public CharacterWeapon ToWeapon(StainId stain) + public readonly CharacterWeapon ToWeapon(StainId stain) => new(Set, 0, Variant, stain); public override string ToString() diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index 188a163d..7b40f55f 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -40,10 +40,10 @@ public struct CharacterWeapon : IEquatable Stain = (StainId)(value >> 48); } - public CharacterArmor ToArmor() + public readonly CharacterArmor ToArmor() => new(Set, (byte)Variant, Stain); - public CharacterArmor ToArmor(StainId stain) + public readonly CharacterArmor ToArmor(StainId stain) => new(Set, (byte)Variant, stain); public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 09578a69..fb1e59aa 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -293,7 +293,7 @@ public partial class ModEditWindow if (_selectedFiles.Count == 0) tt += "\n\nNo files selected."; else if (!active) - tt += $"\n\nHold {_config.DeleteModModifier.ToString()} to delete."; + tt += $"\n\nHold {_config.DeleteModModifier} to delete."; if (ImGuiUtil.DrawDisabledButton("Delete Selected Files", Vector2.Zero, tt, _selectedFiles.Count == 0 || !active)) _editor.FileEditor.DeleteFiles(_editor.Mod!, _editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains)); From 0404ea6109f5496d2a07e662b0d4ced48662a9c8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Jun 2023 15:10:48 +0200 Subject: [PATCH 0999/2451] Resolve common/font paths from Interface Assignment --- Penumbra/Interop/PathResolving/PathResolver.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index a05497be..6833e352 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -16,7 +16,7 @@ public class PathResolver : IDisposable { private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly CollectionManager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly ResourceLoader _loader; @@ -57,8 +57,7 @@ public class PathResolver : IDisposable return category switch { // Only Interface collection. - ResourceCategory.Ui => (_collectionManager.Active.Interface.ResolvePath(path), - _collectionManager.Active.Interface.ToResolveData()), + ResourceCategory.Ui => ResolveUi(path), // Never allow changing scripts. ResourceCategory.UiScript => (null, ResolveData.Invalid), ResourceCategory.GameScript => (null, ResolveData.Invalid), @@ -68,8 +67,11 @@ public class PathResolver : IDisposable ResourceCategory.Vfx => Resolve(path, resourceType), ResourceCategory.Sound => Resolve(path, resourceType), // None of these files are ever associated with specific characters, - // always use the default resolver for now. - ResourceCategory.Common => DefaultResolver(path), + // always use the default resolver for now, + // except that common/font is conceptually more UI. + ResourceCategory.Common => path.Path.StartsWith("common/font"u8) + ? ResolveUi(path) + : DefaultResolver(path), ResourceCategory.BgCommon => DefaultResolver(path), ResourceCategory.Bg => DefaultResolver(path), ResourceCategory.Cut => DefaultResolver(path), @@ -137,4 +139,9 @@ public class PathResolver : IDisposable $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); } } + + /// Resolve a path from the interface collection. + private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) + => (_collectionManager.Active.Interface.ResolvePath(path), + _collectionManager.Active.Interface.ToResolveData()); } From c991eead896788390459566ecaa016bb0960fce1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Jun 2023 15:50:20 +0200 Subject: [PATCH 1000/2451] More readonlys. --- Penumbra.GameData/Structs/CharacterArmor.cs | 10 +++++----- Penumbra.GameData/Structs/CharacterWeapon.cs | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index f44ae5e6..9027a5cf 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -9,7 +9,7 @@ public struct CharacterArmor : IEquatable public const int Size = 4; [FieldOffset(0)] - public uint Value; + public readonly uint Value; [FieldOffset(0)] public SetId Set; @@ -37,18 +37,18 @@ public struct CharacterArmor : IEquatable public readonly CharacterWeapon ToWeapon(StainId stain) => new(Set, 0, Variant, stain); - public override string ToString() + public override readonly string ToString() => $"{Set},{Variant},{Stain}"; public static readonly CharacterArmor Empty; - public bool Equals(CharacterArmor other) + public readonly bool Equals(CharacterArmor other) => Value == other.Value; - public override bool Equals(object? obj) + public override readonly bool Equals(object? obj) => obj is CharacterArmor other && Equals(other); - public override int GetHashCode() + public override readonly int GetHashCode() => (int)Value; public static bool operator ==(CharacterArmor left, CharacterArmor right) diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index 7b40f55f..a1f2dd11 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -16,12 +16,12 @@ public struct CharacterWeapon : IEquatable public ushort Variant; [FieldOffset(6)] - public StainId Stain; - - public ulong Value + public StainId Stain; + + public readonly ulong Value => (ulong)Set | ((ulong)Type << 16) | ((ulong)Variant << 32) | ((ulong)Stain << 48); - public override string ToString() + public override readonly string ToString() => $"{Set},{Type},{Variant},{Stain}"; public CharacterWeapon(SetId set, WeaponType type, ushort variant, StainId stain) @@ -48,13 +48,13 @@ public struct CharacterWeapon : IEquatable public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); - public bool Equals(CharacterWeapon other) + public readonly bool Equals(CharacterWeapon other) => Value == other.Value; - public override bool Equals(object? obj) + public override readonly bool Equals(object? obj) => obj is CharacterWeapon other && Equals(other); - public override int GetHashCode() + public override readonly int GetHashCode() => Value.GetHashCode(); public static bool operator ==(CharacterWeapon left, CharacterWeapon right) From d24e1576d31bd53ab0f9c7661656f423e4cb5f99 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Jun 2023 15:41:02 +0200 Subject: [PATCH 1001/2451] Move EventWrapper, some Glamourer changes. --- OtterGui | 2 +- Penumbra.GameData/Structs/CustomizeData.cs | 2 +- Penumbra/Communication/ChangedItemClick.cs | 2 +- Penumbra/Communication/ChangedItemHover.cs | 2 +- Penumbra/Communication/CollectionChange.cs | 2 +- .../CollectionInheritanceChanged.cs | 2 +- .../Communication/CreatedCharacterBase.cs | 2 +- .../Communication/CreatingCharacterBase.cs | 2 +- Penumbra/Communication/EnabledChanged.cs | 2 +- Penumbra/Communication/ModDataChanged.cs | 2 +- Penumbra/Communication/ModDirectoryChanged.cs | 2 +- .../Communication/ModDiscoveryFinished.cs | 2 +- Penumbra/Communication/ModDiscoveryStarted.cs | 2 +- Penumbra/Communication/ModOptionChanged.cs | 2 +- Penumbra/Communication/ModPathChanged.cs | 2 +- Penumbra/Communication/ModSettingChanged.cs | 2 +- .../Communication/PostSettingsPanelDraw.cs | 2 +- .../Communication/PreSettingsPanelDraw.cs | 2 +- .../Communication/TemporaryGlobalModChange.cs | 2 +- Penumbra/Services/CommunicatorService.cs | 7 + Penumbra/Util/EventWrapper.cs | 190 ------------------ 21 files changed, 26 insertions(+), 209 deletions(-) delete mode 100644 Penumbra/Util/EventWrapper.cs diff --git a/OtterGui b/OtterGui index dc4ad8a5..c69f49e0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit dc4ad8a5fd0347642d3fdae5f2dc17a7fbfacaa1 +Subproject commit c69f49e026e17e81df546aa0621f1f575a22534d diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index c60ee746..37660c08 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -74,7 +74,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } - public string WriteBytes() + public override string ToString() { var sb = new StringBuilder(Size * 3); for (var i = 0; i < Size - 1; ++i) diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index fd879280..80ff8e75 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Api.Enums; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 9a658769..afedf8fd 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.Util; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index c5a0b93f..7c4946d2 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -1,7 +1,7 @@ using System; +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index 3562f457..00e90546 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index adb92bfe..cbb86fc2 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Api; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 8dc1c634..6161a984 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Api; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index 793663b9..dee5e50f 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Api; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index 8f16dcfe..941ed4d5 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -1,7 +1,7 @@ using System; +using OtterGui.Classes; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 0f30b71b..5a3fb473 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Api; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs index 1471d7d7..b8e4e460 100644 --- a/Penumbra/Communication/ModDiscoveryFinished.cs +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.Util; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs index 4e98a3e0..ee193681 100644 --- a/Penumbra/Communication/ModDiscoveryStarted.cs +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.Util; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index cacc7a89..a86826a3 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,7 +1,7 @@ using System; +using OtterGui.Classes; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index e35a0ff4..675759dd 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -1,9 +1,9 @@ using System; using System.IO; +using OtterGui.Classes; using Penumbra.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 6445b956..c3c9f671 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,9 +1,9 @@ using System; +using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index e460e369..f653bd0b 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.Util; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index c3e182b0..1167e92f 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -1,5 +1,5 @@ using System; -using Penumbra.Util; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/TemporaryGlobalModChange.cs b/Penumbra/Communication/TemporaryGlobalModChange.cs index 1f0352a2..8906627b 100644 --- a/Penumbra/Communication/TemporaryGlobalModChange.cs +++ b/Penumbra/Communication/TemporaryGlobalModChange.cs @@ -1,6 +1,6 @@ using System; +using OtterGui.Classes; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Communication; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 371722b2..3e499317 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,10 +1,17 @@ using System; +using OtterGui.Classes; +using OtterGui.Log; using Penumbra.Communication; namespace Penumbra.Services; public class CommunicatorService : IDisposable { + public CommunicatorService(Logger logger) + { + EventWrapper.ChangeLogger(logger); + } + /// public readonly CollectionChange CollectionChange = new(); diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs deleted file mode 100644 index d8d86d5a..00000000 --- a/Penumbra/Util/EventWrapper.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.Util; - -public abstract class EventWrapper : IDisposable - where T : Delegate - where TPriority : struct, Enum -{ - private readonly string _name; - private readonly List<(object Subscriber, TPriority Priority)> _event = new(); - - public bool HasSubscribers - => _event.Count > 0; - - protected EventWrapper(string name) - => _name = name; - - public void Dispose() - { - lock (_event) - { - _event.Clear(); - } - } - - public void Subscribe(T subscriber, TPriority priority) - { - lock (_event) - { - var existingIdx = _event.FindIndex(p => (T)p.Subscriber == subscriber); - var idx = _event.FindIndex(p => p.Priority.CompareTo(priority) > 0); - if (idx == existingIdx) - { - if (idx < 0) - _event.Add((subscriber, priority)); - else - _event[idx] = (subscriber, priority); - } - else - { - if (idx < 0) - _event.Add((subscriber, priority)); - else - _event.Insert(idx, (subscriber, priority)); - - if (existingIdx >= 0) - _event.RemoveAt(existingIdx < idx ? existingIdx : existingIdx + 1); - } - } - } - - public void Unsubscribe(T subscriber) - { - lock (_event) - { - var idx = _event.FindIndex(p => (T)p.Subscriber == subscriber); - if (idx >= 0) - _event.RemoveAt(idx); - } - } - - - protected static void Invoke(EventWrapper wrapper) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a, b); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a, b, c); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a, b, c, d); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a, b, c, d, e); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - - protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) - { - lock (wrapper._event) - { - foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) - { - try - { - ((Action)action).Invoke(a, b, c, d, e, f); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } -} From 52efacacd7b6dace838eb15ef9013d6202917fc4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Jun 2023 01:31:08 +0200 Subject: [PATCH 1002/2451] Fix issue with trimmed folder names being empty. --- Penumbra/Mods/ModCreator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 755caeb3..5d37e99e 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -332,7 +332,10 @@ public partial class ModCreator /// Return the name of a new valid directory based on the base directory and the given name. public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName) - => new(Path.Combine(baseDir.FullName, ReplaceBadXivSymbols(optionName))); + { + var option = ReplaceBadXivSymbols(optionName); + return new DirectoryInfo(Path.Combine(baseDir.FullName, option.Length > 0 ? option : "_")); + } /// Normalize for nicer names, and remove invalid symbols or invalid paths. public static string ReplaceBadXivSymbols(string s, string replacement = "_") From b5f20c0ec8b0fe85997b0b412d2378aea2011c41 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Jun 2023 01:31:20 +0200 Subject: [PATCH 1003/2451] Fix issue with collection count being wrong. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index bd530ccd..545ae83c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -218,7 +218,7 @@ public class Penumbra : IDalamudPlugin sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count - _tempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); From b748e349170b3b08bbfaaa1badc36e7ed93edc62 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Jun 2023 01:31:51 +0200 Subject: [PATCH 1004/2451] Add ChangedItemDrawer and move it. --- Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/ChangedItemDrawer.cs | 255 ++++++++++++++++++ .../UI/ModsTab/ModPanelChangedItemsTab.cs | 11 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 19 +- Penumbra/UI/UiHelpers.cs | 62 ----- 5 files changed, 271 insertions(+), 79 deletions(-) create mode 100644 Penumbra/UI/ChangedItemDrawer.cs diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 3b9e3e31..1ab1f313 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -161,7 +161,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddModEditor(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs new file mode 100644 index 00000000..69c2bc51 --- /dev/null +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Data; +using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using ImGuiScene; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public class ChangedItemDrawer : IDisposable +{ + private const EquipSlot MonsterSlot = (EquipSlot)100; + private const EquipSlot DemihumanSlot = (EquipSlot)101; + private const EquipSlot CustomizationSlot = (EquipSlot)102; + private const EquipSlot ActionSlot = (EquipSlot)103; + + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons; + + public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator) + { + _icons = CreateEquipSlotIcons(uiBuilder, gameData); + _communicator = communicator; + } + + public void Dispose() + { + foreach (var wrap in _icons.Values.Distinct()) + wrap.Dispose(); + _icons.Clear(); + } + + /// Apply Changed Item Counters to the Name if necessary. + public static string ChangedItemName(string name, object? data) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + + /// Add filterable information to the string. + public static string ChangedItemFilterName(string name, object? data) + => data switch + { + int counter => $"{counter} Files Manipulating {name}s", + Item it => $"{name}\0{((EquipSlot)it.EquipSlotCategory.Row).ToName()}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", + ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", + _ => name, + }; + + /// + /// Draw a changed item, invoking the Api-Events for clicks and tooltips. + /// Also draw the item Id in grey if requested. + /// + public void DrawChangedItem(string name, object? data, bool drawId) + { + name = ChangedItemName(name, data); + DrawCategoryIcon(name, data); + ImGui.SameLine(); + var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(ret, data); + + if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) + { + // We can not be sure that any subscriber actually prints something in any case. + // Circumvent ugly blank tooltip with less-ugly useless tooltip. + using var tt = ImRaii.Tooltip(); + using var group = ImRaii.Group(); + _communicator.ChangedItemHover.Invoke(data); + group.Dispose(); + if (ImGui.GetItemRectSize() == Vector2.Zero) + ImGui.TextUnformatted("No actions available."); + } + + if (!drawId || !GetChangedItemObject(data, out var text)) + return; + + ImGui.SameLine(ImGui.GetContentRegionAvail().X); + ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); + } + + private void DrawCategoryIcon(string name, object? obj) + { + var height = ImGui.GetTextLineHeight(); + var slot = EquipSlot.Unknown; + var desc = string.Empty; + if (obj is Item it) + { + slot = (EquipSlot)it.EquipSlotCategory.Row; + desc = slot.ToName(); + } + else if (obj is ModelChara m) + { + (slot, desc) = (CharacterBase.ModelType)m.Type switch + { + CharacterBase.ModelType.DemiHuman => (DemihumanSlot, "Demi-Human"), + CharacterBase.ModelType.Monster => (MonsterSlot, "Monster"), + _ => (EquipSlot.Unknown, string.Empty), + }; + } + else if (name.StartsWith("Action: ")) + { + (slot, desc) = (ActionSlot, "Action"); + } + else if (name.StartsWith("Customization: ")) + { + (slot, desc) = (CustomizationSlot, "Customization"); + } + + if (!_icons.TryGetValue(slot, out var icon)) + { + ImGui.Dummy(new Vector2(height)); + return; + } + + ImGui.Image(icon.ImGuiHandle, new Vector2(height)); + if (ImGui.IsItemHovered() && icon.Height > height) + { + using var tt = ImRaii.Tooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); + ImGui.SameLine(); + ImGuiUtil.DrawTextButton(desc, new Vector2(0, icon.Height), 0); + } + } + + /// Return more detailed object information in text, if it exists. + public static bool GetChangedItemObject(object? obj, out string text) + { + switch (obj) + { + case Item it: + var quad = (Quad)it.ModelMain; + text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + return true; + case ModelChara m: + text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; + return true; + default: + text = string.Empty; + return false; + } + } + + private static Dictionary CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) + { + using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); + + if (!equipTypeIcons.Valid) + return new Dictionary(); + + var dict = new Dictionary(12); + + // Weapon + var tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0); + if (tex != null) + { + dict.Add(EquipSlot.MainHand, tex); + dict.Add(EquipSlot.BothHand, tex); + } + + // Hat + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1); + if (tex != null) + dict.Add(EquipSlot.Head, tex); + + // Body + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2); + if (tex != null) + { + dict.Add(EquipSlot.Body, tex); + dict.Add(EquipSlot.BodyHands, tex); + dict.Add(EquipSlot.BodyHandsLegsFeet, tex); + dict.Add(EquipSlot.BodyLegsFeet, tex); + dict.Add(EquipSlot.ChestHands, tex); + dict.Add(EquipSlot.FullBody, tex); + dict.Add(EquipSlot.HeadBody, tex); + } + + // Hands + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3); + if (tex != null) + dict.Add(EquipSlot.Hands, tex); + + // Pants + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5); + if (tex != null) + { + dict.Add(EquipSlot.Legs, tex); + dict.Add(EquipSlot.LegsFeet, tex); + } + + // Shoes + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6); + if (tex != null) + dict.Add(EquipSlot.Feet, tex); + + // Offhand + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7); + if (tex != null) + dict.Add(EquipSlot.OffHand, tex); + + // Earrings + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8); + if (tex != null) + dict.Add(EquipSlot.Ears, tex); + + // Necklace + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9); + if (tex != null) + dict.Add(EquipSlot.Neck, tex); + + // Bracelet + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10); + if (tex != null) + dict.Add(EquipSlot.Wrists, tex); + + // Ring + tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11); + if (tex != null) + dict.Add(EquipSlot.RFinger, tex); + + // Monster + tex = gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex"); + if (tex != null) + dict.Add(MonsterSlot, tex); + + // Demihuman + tex = gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex"); + if (tex != null) + dict.Add(DemihumanSlot, tex); + + // Customization + tex = gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex"); + if (tex != null) + dict.Add(CustomizationSlot, tex); + + // Action + tex = gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex"); + if (tex != null) + dict.Add(ActionSlot, tex); + + return dict; + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index ae500b1d..b2b75d8b 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -6,7 +6,6 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Api; using Penumbra.Services; namespace Penumbra.UI.ModsTab; @@ -14,15 +13,15 @@ namespace Penumbra.UI.ModsTab; public class ModPanelChangedItemsTab : ITab { private readonly ModFileSystemSelector _selector; - private readonly CommunicatorService _communicator; + private readonly ChangedItemDrawer _drawer; public ReadOnlySpan Label => "Changed Items"u8; - public ModPanelChangedItemsTab(ModFileSystemSelector selector, CommunicatorService communicator) + public ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) { - _selector = selector; - _communicator = communicator; + _selector = selector; + _drawer = drawer; } public bool IsVisible @@ -36,6 +35,6 @@ public class ModPanelChangedItemsTab : ITab var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_communicator, kvp.Item1, kvp.Item2, true), height); + ImGuiClip.ClippedDraw(zipList, kvp => _drawer.DrawChangedItem(kvp.Item1, kvp.Item2, true), height); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index b7880d0c..bc371010 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -9,7 +9,6 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.Mods; -using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; @@ -17,14 +16,14 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { private readonly CollectionManager _collectionManager; - private readonly CommunicatorService _communicator; + private readonly ChangedItemDrawer _drawer; private readonly CollectionSelectHeader _collectionHeader; - public ChangedItemsTab(CollectionManager collectionManager, CommunicatorService communicator, CollectionSelectHeader collectionHeader) + public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer) { _collectionManager = collectionManager; - _communicator = communicator; _collectionHeader = collectionHeader; + _drawer = drawer; } public ReadOnlySpan Label @@ -34,7 +33,7 @@ public class ChangedItemsTab : ITab private LowerString _changedItemModFilter = LowerString.Empty; public void DrawContent() - { + { _collectionHeader.Draw(true); var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); @@ -49,8 +48,8 @@ public class ChangedItemsTab : ITab const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; ImGui.TableSetupColumn("items", flags, 400 * UiHelpers.Scale); - ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale); - ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale); + ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); + ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); var items = _collectionManager.Active.Current.ChangedItems; var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty @@ -76,7 +75,7 @@ public class ChangedItemsTab : ITab /// Apply the current filters. private bool FilterChangedItem(KeyValuePair, object?)> item) => (_changedItemFilter.IsEmpty - || UiHelpers.ChangedItemName(item.Key, item.Value.Item2) + || ChangedItemDrawer.ChangedItemFilterName(item.Key, item.Value.Item2) .Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase)) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); @@ -84,7 +83,7 @@ public class ChangedItemsTab : ITab private void DrawChangedItemColumn(KeyValuePair, object?)> item) { ImGui.TableNextColumn(); - UiHelpers.DrawChangedItem(_communicator, item.Key, item.Value.Item2, false); + _drawer.DrawChangedItem(item.Key, item.Value.Item2, false); ImGui.TableNextColumn(); if (item.Value.Item1.Count > 0) { @@ -94,7 +93,7 @@ public class ChangedItemsTab : ITab } ImGui.TableNextColumn(); - if (!UiHelpers.GetChangedItemObject(item.Value.Item2, out var text)) + if (!ChangedItemDrawer.GetChangedItemObject(item.Value.Item2, out var text)) return; using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 944223f0..f46eac35 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -3,17 +3,10 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; -using Penumbra.Api; -using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; @@ -53,61 +46,6 @@ public static class UiHelpers ImGui.SetTooltip("Click to copy to clipboard."); } - /// Apply Changed Item Counters to the Name if necessary. - public static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - /// - /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item Id in grey if requested. - /// - public static void DrawChangedItem(CommunicatorService communicator, string name, object? data, bool drawId) - { - name = ChangedItemName(name, data); - var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - - if (ret != MouseButton.None) - communicator.ChangedItemClick.Invoke(ret, data); - - if (communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - communicator.ChangedItemHover.Invoke(data); - group.Dispose(); - if (ImGui.GetItemRectSize() == Vector2.Zero) - ImGui.TextUnformatted("No actions available."); - } - - if (!drawId || !GetChangedItemObject(data, out var text)) - return; - - ImGui.SameLine(ImGui.GetContentRegionAvail().X); - ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); - } - - /// Return more detailed object information in text, if it exists. - public static bool GetChangedItemObject(object? obj, out string text) - { - switch (obj) - { - case Item it: - var quad = (Quad)it.ModelMain; - text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; - return true; - case ModelChara m: - text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; - } - } - /// Draw a button to open the official discord server. /// The desired width of the button. public static void DrawDiscordButton(float width) From 6ec60c9150fe812af05af99bc6ec45d8c7f2ee5b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Jun 2023 13:58:44 +0200 Subject: [PATCH 1005/2451] Improve changed items somewhat. --- OtterGui | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 76 ++++++++++--------- Penumbra/UI/ConfigWindow.cs | 2 - .../UI/ModsTab/ModPanelChangedItemsTab.cs | 3 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 46 ++++++++--- Penumbra/UI/Tabs/ConfigTabBar.cs | 1 + 6 files changed, 78 insertions(+), 52 deletions(-) diff --git a/OtterGui b/OtterGui index c69f49e0..1eda5406 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c69f49e026e17e81df546aa0621f1f575a22534d +Subproject commit 1eda5406db266e49ffb04a73bd5217ce6281f489 diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 69c2bc51..c52bd5b3 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -26,11 +26,11 @@ public class ChangedItemDrawer : IDisposable private const EquipSlot ActionSlot = (EquipSlot)103; private readonly CommunicatorService _communicator; - private readonly Dictionary _icons; + private readonly Dictionary _icons = new(16); public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator) { - _icons = CreateEquipSlotIcons(uiBuilder, gameData); + uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true); _communicator = communicator; } @@ -64,12 +64,15 @@ public class ChangedItemDrawer : IDisposable name = ChangedItemName(name, data); DrawCategoryIcon(name, data); ImGui.SameLine(); - var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - - if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); + using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) + .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) + { + var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(ret, data); + } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) { @@ -87,12 +90,13 @@ public class ChangedItemDrawer : IDisposable return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); + ImGui.AlignTextToFramePadding(); ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); } private void DrawCategoryIcon(string name, object? obj) { - var height = ImGui.GetTextLineHeight(); + var height = ImGui.GetFrameHeight(); var slot = EquipSlot.Unknown; var desc = string.Empty; if (obj is Item it) @@ -152,104 +156,102 @@ public class ChangedItemDrawer : IDisposable } } - private static Dictionary CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) + private bool CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); if (!equipTypeIcons.Valid) - return new Dictionary(); - - var dict = new Dictionary(12); + return false; // Weapon var tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0); if (tex != null) { - dict.Add(EquipSlot.MainHand, tex); - dict.Add(EquipSlot.BothHand, tex); + _icons.Add(EquipSlot.MainHand, tex); + _icons.Add(EquipSlot.BothHand, tex); } // Hat tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1); if (tex != null) - dict.Add(EquipSlot.Head, tex); + _icons.Add(EquipSlot.Head, tex); // Body tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2); if (tex != null) { - dict.Add(EquipSlot.Body, tex); - dict.Add(EquipSlot.BodyHands, tex); - dict.Add(EquipSlot.BodyHandsLegsFeet, tex); - dict.Add(EquipSlot.BodyLegsFeet, tex); - dict.Add(EquipSlot.ChestHands, tex); - dict.Add(EquipSlot.FullBody, tex); - dict.Add(EquipSlot.HeadBody, tex); + _icons.Add(EquipSlot.Body, tex); + _icons.Add(EquipSlot.BodyHands, tex); + _icons.Add(EquipSlot.BodyHandsLegsFeet, tex); + _icons.Add(EquipSlot.BodyLegsFeet, tex); + _icons.Add(EquipSlot.ChestHands, tex); + _icons.Add(EquipSlot.FullBody, tex); + _icons.Add(EquipSlot.HeadBody, tex); } // Hands tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3); if (tex != null) - dict.Add(EquipSlot.Hands, tex); + _icons.Add(EquipSlot.Hands, tex); // Pants tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5); if (tex != null) { - dict.Add(EquipSlot.Legs, tex); - dict.Add(EquipSlot.LegsFeet, tex); + _icons.Add(EquipSlot.Legs, tex); + _icons.Add(EquipSlot.LegsFeet, tex); } // Shoes tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6); if (tex != null) - dict.Add(EquipSlot.Feet, tex); + _icons.Add(EquipSlot.Feet, tex); // Offhand tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7); if (tex != null) - dict.Add(EquipSlot.OffHand, tex); + _icons.Add(EquipSlot.OffHand, tex); // Earrings tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8); if (tex != null) - dict.Add(EquipSlot.Ears, tex); + _icons.Add(EquipSlot.Ears, tex); // Necklace tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9); if (tex != null) - dict.Add(EquipSlot.Neck, tex); + _icons.Add(EquipSlot.Neck, tex); // Bracelet tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10); if (tex != null) - dict.Add(EquipSlot.Wrists, tex); + _icons.Add(EquipSlot.Wrists, tex); // Ring tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11); if (tex != null) - dict.Add(EquipSlot.RFinger, tex); + _icons.Add(EquipSlot.RFinger, tex); // Monster tex = gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex"); if (tex != null) - dict.Add(MonsterSlot, tex); + _icons.Add(MonsterSlot, tex); // Demihuman tex = gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex"); if (tex != null) - dict.Add(DemihumanSlot, tex); + _icons.Add(DemihumanSlot, tex); // Customization tex = gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex"); if (tex != null) - dict.Add(CustomizationSlot, tex); + _icons.Add(CustomizationSlot, tex); // Action tex = gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex"); if (tex != null) - dict.Add(ActionSlot, tex); + _icons.Add(ActionSlot, tex); - return dict; + return true; } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 7f484518..ebd04ef6 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -27,11 +27,9 @@ public sealed class ConfigWindow : Window public void SelectTab(TabType tab) => _configTabs.SelectTab = tab; - public void SelectMod(Mod mod) => _configTabs.Mods.SelectMod = mod; - public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) : base(GetLabel(checker)) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index b2b75d8b..46d9f3bc 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -6,7 +6,6 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.Services; namespace Penumbra.UI.ModsTab; @@ -34,7 +33,7 @@ public class ModPanelChangedItemsTab : ITab return; var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); - var height = ImGui.GetTextLineHeight(); + var height = ImGui.GetFrameHeight(); ImGuiClip.ClippedDraw(zipList, kvp => _drawer.DrawChangedItem(kvp.Item1, kvp.Item2, true), height); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index bc371010..7e725e18 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -7,6 +7,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -18,6 +19,7 @@ public class ChangedItemsTab : ITab private readonly CollectionManager _collectionManager; private readonly ChangedItemDrawer _drawer; private readonly CollectionSelectHeader _collectionHeader; + private ConfigTabBar? _tabBar = null; public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer) { @@ -26,6 +28,9 @@ public class ChangedItemsTab : ITab _drawer = drawer; } + public void SetTabBar(ConfigTabBar tabBar) + => _tabBar = tabBar; + public ReadOnlySpan Label => "Changed Items"u8; @@ -40,14 +45,14 @@ public class ChangedItemsTab : ITab if (!child) return; - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var height = ImGui.GetFrameHeight() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips(height); using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) return; const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn("items", flags, 400 * UiHelpers.Scale); + ImGui.TableSetupColumn("items", flags, 450 * UiHelpers.Scale); ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); @@ -62,9 +67,9 @@ public class ChangedItemsTab : ITab private float DrawFilters() { var varWidth = ImGui.GetContentRegionAvail().X - - 400 * UiHelpers.Scale + - 450 * UiHelpers.Scale - ImGui.GetStyle().ItemSpacing.X; - ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + ImGui.SetNextItemWidth(450 * UiHelpers.Scale); LowerString.InputWithHint("##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128); ImGui.SameLine(); ImGui.SetNextItemWidth(varWidth); @@ -85,18 +90,39 @@ public class ChangedItemsTab : ITab ImGui.TableNextColumn(); _drawer.DrawChangedItem(item.Key, item.Value.Item2, false); ImGui.TableNextColumn(); - if (item.Value.Item1.Count > 0) - { - ImGui.TextUnformatted(item.Value.Item1[0].Name); - if (item.Value.Item1.Count > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", item.Value.Item1.Skip(1).Select(m => m.Name))); - } + DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); if (!ChangedItemDrawer.GetChangedItemObject(item.Value.Item2, out var text)) return; using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.AlignTextToFramePadding(); ImGuiUtil.RightAlign(text); } + + private void DrawModColumn(SingleArray mods) + { + if (mods.Count <= 0) + return; + + var first = mods[0]; + using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); + if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + && ImGui.GetIO().KeyCtrl + && _tabBar != null + && first is Mod mod) + { + _tabBar.SelectTab = TabType.Mods; + _tabBar.Mods.SelectMod = mod; + } + + if (ImGui.IsItemHovered()) + { + using var _ = ImRaii.Tooltip(); + ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); + if (mods.Count > 1) + ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); + } + } } diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index b221224d..4105f2e3 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -46,6 +46,7 @@ public class ConfigTabBar Resource, Watcher, }; + ChangedItems.SetTabBar(this); } public TabType Draw() From 878395e164a8b043b9ef74c9655de5e8c03e5b68 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 5 Jun 2023 12:01:28 +0000 Subject: [PATCH 1006/2451] [CI] Updating repo.json for t0.7.1.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 720c5ee9..3f8ab838 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.5", + "TestingAssemblyVersion": "0.7.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 78e772dad920ce097acd18d96eff4f1d9532ea45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Jun 2023 18:24:58 +0200 Subject: [PATCH 1007/2451] Fix unknown animations not counting for changed items. --- .../Data/ObjectIdentification.cs | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 111a652d..e992ce43 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -50,13 +50,12 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)) { - IdentifyVfx(set, path); - } - else - { - var info = GamePathParser.GetFileInfo(path); - IdentifyParsed(set, info); - } + if (IdentifyVfx(set, path)) + return; + } + + var info = GamePathParser.GetFileInfo(path); + IdentifyParsed(set, info); } public Dictionary Identify(string path) @@ -161,24 +160,22 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private void IdentifyParsed(IDictionary set, GameObjectInfo info) { - switch (info.ObjectType) + switch (info.FileType) { - case ObjectType.Unknown: - switch (info.FileType) - { - case FileType.Sound: - AddCounterString(set, FileType.Sound.ToString()); - break; - case FileType.Animation: - case FileType.Pap: - AddCounterString(set, FileType.Animation.ToString()); - break; - case FileType.Shader: - AddCounterString(set, FileType.Shader.ToString()); - break; - } + case FileType.Sound: + AddCounterString(set, FileType.Sound.ToString()); + return; + case FileType.Animation: + case FileType.Pap: + AddCounterString(set, FileType.Animation.ToString()); + return; + case FileType.Shader: + AddCounterString(set, FileType.Shader.ToString()); + return; + } - break; + switch (info.ObjectType) + { case ObjectType.LoadingScreen: case ObjectType.Map: case ObjectType.Interface: @@ -235,19 +232,18 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier } break; - - default: throw new InvalidEnumArgumentException(); } } - private void IdentifyVfx(IDictionary set, string path) + private bool IdentifyVfx(IDictionary set, string path) { var key = GamePathParser.VfxToKey(path); - if (key.Length == 0 || !Actions.TryGetValue(key, out var actions)) - return; + if (key.Length == 0 || !Actions.TryGetValue(key, out var actions) || actions.Count == 0) + return false; foreach (var action in actions) set[$"Action: {action.Name}"] = action; + return true; } private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, From 5fcb07487e0c1166477a37a39c6d47249fced718 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 7 Jun 2023 18:29:05 +0200 Subject: [PATCH 1008/2451] Add SelectTab event, update new clientstructs. --- Penumbra.GameData/Structs/CharacterEquip.cs | 118 ------------------ Penumbra/Api/PenumbraApi.cs | 11 +- Penumbra/Communication/SelectTab.cs | 29 +++++ Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 1 - Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/UI/ConfigWindow.cs | 16 +-- Penumbra/UI/Tabs/ChangedItemsTab.cs | 30 ++--- Penumbra/UI/Tabs/ConfigTabBar.cs | 25 +++- Penumbra/UI/Tabs/DebugTab.cs | 2 +- 10 files changed, 79 insertions(+), 159 deletions(-) delete mode 100644 Penumbra.GameData/Structs/CharacterEquip.cs create mode 100644 Penumbra/Communication/SelectTab.cs diff --git a/Penumbra.GameData/Structs/CharacterEquip.cs b/Penumbra.GameData/Structs/CharacterEquip.cs deleted file mode 100644 index d045bd58..00000000 --- a/Penumbra.GameData/Structs/CharacterEquip.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using Penumbra.GameData.Enums; -using Penumbra.String.Functions; - -namespace Penumbra.GameData.Structs; - -public readonly unsafe struct CharacterEquip -{ - public const int Slots = 10; - public const int Size = CharacterArmor.Size * Slots; - - public static readonly CharacterEquip Null = new(null); - - private readonly CharacterArmor* _armor; - - public IntPtr Address - => ( IntPtr )_armor; - - public ref CharacterArmor this[ int idx ] - => ref _armor[ idx ]; - - public ref CharacterArmor this[ uint idx ] - => ref _armor[ idx ]; - - public ref CharacterArmor this[ EquipSlot slot ] - => ref _armor[ IndexOf( slot ) ]; - - public ref CharacterArmor Head - => ref _armor[ 0 ]; - - public ref CharacterArmor Body - => ref _armor[ 1 ]; - - public ref CharacterArmor Hands - => ref _armor[ 2 ]; - - public ref CharacterArmor Legs - => ref _armor[ 3 ]; - - public ref CharacterArmor Feet - => ref _armor[ 4 ]; - - public ref CharacterArmor Ears - => ref _armor[ 5 ]; - - public ref CharacterArmor Neck - => ref _armor[ 6 ]; - - public ref CharacterArmor Wrists - => ref _armor[ 7 ]; - - public ref CharacterArmor RFinger - => ref _armor[ 8 ]; - - public ref CharacterArmor LFinger - => ref _armor[ 9 ]; - - public CharacterEquip( CharacterArmor* val ) - => _armor = val; - - public static implicit operator CharacterEquip( CharacterArmor* val ) - => new(val); - - public static implicit operator CharacterEquip( IntPtr val ) - => new(( CharacterArmor* )val); - - public static implicit operator CharacterEquip( ReadOnlySpan< CharacterArmor > val ) - { - if( val.Length != 10 ) - { - throw new ArgumentException( "Invalid number of equipment pieces in span." ); - } - - fixed( CharacterArmor* ptr = val ) - { - return new CharacterEquip( ptr ); - } - } - - public static implicit operator bool( CharacterEquip equip ) - => equip._armor != null; - - public static bool operator true( CharacterEquip equip ) - => equip._armor != null; - - public static bool operator false( CharacterEquip equip ) - => equip._armor == null; - - public static bool operator !( CharacterEquip equip ) - => equip._armor == null; - - private static int IndexOf( EquipSlot slot ) - { - return slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 1, - EquipSlot.Hands => 2, - EquipSlot.Legs => 3, - EquipSlot.Feet => 4, - EquipSlot.Ears => 5, - EquipSlot.Neck => 6, - EquipSlot.Wrists => 7, - EquipSlot.RFinger => 8, - EquipSlot.LFinger => 9, - _ => throw new ArgumentOutOfRangeException( nameof( slot ), slot, null ), - }; - } - - - public void Load( CharacterEquip source ) - { - MemoryUtility.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 ); - } - - public bool Equals( CharacterEquip other ) - => MemoryUtility.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0; -} \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 369374c6..9b7e6410 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -258,16 +258,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(tab)) return PenumbraApiEc.InvalidArgument; - if (tab != TabType.None) - _configWindow.SelectTab(tab); - if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { if (_modManager.TryGetMod(modDirectory, modName, out var mod)) - _configWindow.SelectMod(mod); + _communicator.SelectTab.Invoke(tab, mod); else return PenumbraApiEc.ModMissing; } + else if (tab != TabType.None) + { + _communicator.SelectTab.Invoke(tab); + } return PenumbraApiEc.Success; } @@ -1127,7 +1128,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if (manips.Add(manip)) continue; - + Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); manips = null; return false; diff --git a/Penumbra/Communication/SelectTab.cs b/Penumbra/Communication/SelectTab.cs new file mode 100644 index 00000000..3dcf6d1b --- /dev/null +++ b/Penumbra/Communication/SelectTab.cs @@ -0,0 +1,29 @@ +using System; +using OtterGui.Classes; +using Penumbra.Api.Enums; +using Penumbra.Mods; + +namespace Penumbra.Communication; + +/// +/// Trigger to select a tab and mod in the Config Window. +/// +/// Parameter is the selected tab. +/// Parameter is the selected mod, if any. +/// +/// +public sealed class SelectTab : EventWrapper, SelectTab.Priority> +{ + public enum Priority + { + /// + ConfigTabBar = 0, + } + + public SelectTab() + : base(nameof(SelectTab)) + { } + + public void Invoke(TabType tab = TabType.None, Mod? mod = null) + => Invoke(this, tab, mod); +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 80e56231..e7de1a4a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -89,7 +89,7 @@ public class ResourceTree ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") : imcNode); - var mdl = (RenderModel*)subObject->ModelArray[i]; + var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) subObjectNodes.Add(globalContext.WithNames diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index d8116998..b6a0036f 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -267,7 +267,6 @@ public class ModDataEditor _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } - private void ChangeTag(Mod mod, int tagIdx, string newTag, bool local) { var which = local ? mod.LocalTags : mod.ModTags; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 3e499317..784b7f89 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -63,6 +63,9 @@ public class CommunicatorService : IDisposable /// public readonly ChangedItemClick ChangedItemClick = new(); + /// + public readonly SelectTab SelectTab = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -82,5 +85,6 @@ public class CommunicatorService : IDisposable PostSettingsPanelDraw.Dispose(); ChangedItemHover.Dispose(); ChangedItemClick.Dispose(); + SelectTab.Dispose(); } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ebd04ef6..2ce27052 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -5,8 +5,6 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.Api.Enums; -using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; @@ -24,12 +22,6 @@ public sealed class ConfigWindow : Window private ConfigTabBar _configTabs = null!; private string? _lastException; - public void SelectTab(TabType tab) - => _configTabs.SelectTab = tab; - - public void SelectMod(Mod mod) - => _configTabs.Mods.SelectMod = mod; - public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) : base(GetLabel(checker)) @@ -41,14 +33,14 @@ public sealed class ConfigWindow : Window RespectCloseHotkey = true; tutorial.UpdateTutorialStep(); - IsOpen = _config.DebugMode; + IsOpen = _config.DebugMode; } public void Setup(Penumbra penumbra, ConfigTabBar configTabs) { - _penumbra = penumbra; - _configTabs = configTabs; - SelectTab(_config.SelectedTab); + _penumbra = penumbra; + _configTabs = configTabs; + _configTabs.SelectTab = _config.SelectedTab; } public override bool DrawConditions() diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 7e725e18..9b929c60 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -10,6 +10,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; @@ -19,18 +20,17 @@ public class ChangedItemsTab : ITab private readonly CollectionManager _collectionManager; private readonly ChangedItemDrawer _drawer; private readonly CollectionSelectHeader _collectionHeader; - private ConfigTabBar? _tabBar = null; + private readonly CommunicatorService _communicator; - public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer) + public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, + CommunicatorService communicator) { _collectionManager = collectionManager; _collectionHeader = collectionHeader; _drawer = drawer; + _communicator = communicator; } - public void SetTabBar(ConfigTabBar tabBar) - => _tabBar = tabBar; - public ReadOnlySpan Label => "Changed Items"u8; @@ -106,22 +106,18 @@ public class ChangedItemsTab : ITab if (mods.Count <= 0) return; - var first = mods[0]; + var first = mods[0]; using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); - if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) - && ImGui.GetIO().KeyCtrl - && _tabBar != null - && first is Mod mod) - { - _tabBar.SelectTab = TabType.Mods; - _tabBar.Mods.SelectMod = mod; - } + if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + && ImGui.GetIO().KeyCtrl + && first is Mod mod) + _communicator.SelectTab.Invoke(TabType.Mods, mod); if (ImGui.IsItemHovered()) { - using var _ = ImRaii.Tooltip(); - ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); - if (mods.Count > 1) + using var _ = ImRaii.Tooltip(); + ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); + if (mods.Count > 1) ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); } } diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 4105f2e3..49b6348a 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -2,11 +2,15 @@ using System; using ImGuiNET; using OtterGui.Widgets; using Penumbra.Api.Enums; +using Penumbra.Mods; +using Penumbra.Services; namespace Penumbra.UI.Tabs; -public class ConfigTabBar +public class ConfigTabBar : IDisposable { + private readonly CommunicatorService _communicator; + public readonly SettingsTab Settings; public readonly ModsTab Mods; public readonly CollectionsTab Collections; @@ -22,9 +26,12 @@ public class ConfigTabBar /// The tab to select on the next Draw call, if any. public TabType SelectTab = TabType.None; - public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, - DebugTab debug, ResourceTab resource, ResourceWatcher watcher, OnScreenTab onScreenTab) + public ConfigTabBar(CommunicatorService communicator, SettingsTab settings, ModsTab mods, CollectionsTab collections, + ChangedItemsTab changedItems, EffectiveTab effective, DebugTab debug, ResourceTab resource, ResourceWatcher watcher, + OnScreenTab onScreenTab) { + _communicator = communicator; + Settings = settings; Mods = mods; Collections = collections; @@ -46,9 +53,12 @@ public class ConfigTabBar Resource, Watcher, }; - ChangedItems.SetTabBar(this); + _communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar); } + public void Dispose() + => _communicator.SelectTab.Unsubscribe(OnSelectTab); + public TabType Draw() { if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out var currentLabel, () => { }, Tabs)) @@ -87,4 +97,11 @@ public class ConfigTabBar // @formatter:on return TabType.None; } + + private void OnSelectTab(TabType tab, Mod? mod) + { + SelectTab = tab; + if (mod != null) + Mods.SelectMod = mod; + } } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 9acecce9..2fd21c6f 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -696,7 +696,7 @@ public class DebugTab : Window, ITab if (imc != null) UiHelpers.Text(imc); - var mdl = (RenderModel*)model->ModelArray[i]; + var mdl = (RenderModel*)model->Models[i]; ImGui.TableNextColumn(); ImGui.TextUnformatted(mdl == null ? "NULL" : $"0x{(ulong)mdl:X}"); if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) From d9c5c053cfcdcb3bd3be40884563a955b9748407 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Jun 2023 16:10:02 +0200 Subject: [PATCH 1009/2451] Use EquipItem in item management and add filters to changed item types. --- .../Data/EquipmentIdentificationList.cs | 25 +- Penumbra.GameData/Data/ItemData.cs | 41 +- .../Data/ObjectIdentification.cs | 2 +- .../Data/WeaponIdentificationList.cs | 47 +- .../Enums/ChangedItemExtensions.cs | 28 +- Penumbra.GameData/Enums/FullEquipType.cs | 200 +++++---- Penumbra.GameData/GameData.cs | 4 +- Penumbra.GameData/Structs/EquipItem.cs | 88 ++++ Penumbra/Communication/ChangedItemClick.cs | 3 + Penumbra/Communication/ChangedItemHover.cs | 3 + Penumbra/Configuration.cs | 2 + Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 30 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/Penumbra.cs | 25 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 39 +- Penumbra/UI/ChangedItemDrawer.cs | 418 ++++++++++++------ .../UI/ModsTab/ModPanelChangedItemsTab.cs | 29 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 26 +- 19 files changed, 641 insertions(+), 375 deletions(-) create mode 100644 Penumbra.GameData/Structs/EquipItem.cs diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index f26cfc1a..e4ba59d1 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -9,15 +9,15 @@ using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -internal sealed class EquipmentIdentificationList : KeyList +internal sealed class EquipmentIdentificationList : KeyList { - private const string Tag = "EquipmentIdentification"; + private const string Tag = "EquipmentIdentification"; public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language)) { } - public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) + public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) { if (slot == EquipSlot.Unknown) return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)); @@ -33,15 +33,10 @@ internal sealed class EquipmentIdentificationList : KeyList public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant) => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; - public static ulong ToKey(Item i) - { - var model = (SetId)((Lumina.Data.Parsing.Quad)i.ModelMain).A; - var slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - var variant = (byte)((Lumina.Data.Parsing.Quad)i.ModelMain).B; - return ToKey(model, slot, variant); - } + public static ulong ToKey(EquipItem i) + => ToKey(i.ModelId, i.Slot, i.Variant); - protected override IEnumerable ToKeys(Item i) + protected override IEnumerable ToKeys(EquipItem i) { yield return ToKey(i); } @@ -49,12 +44,12 @@ internal sealed class EquipmentIdentificationList : KeyList protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(Item data) - => (int)data.RowId; + protected override int ValueKeySelector(EquipItem data) + => (int)data.Id; - private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) { var items = gameData.GetExcelSheet(language)!; - return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()); + return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(EquipItem.FromArmor); } } diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index 96e305ad..7cbd819c 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -5,32 +5,41 @@ using System.Linq; using Dalamud; using Dalamud.Data; using Dalamud.Plugin; -using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -public sealed class ItemData : DataSharer, IReadOnlyDictionary> +public sealed class ItemData : DataSharer, IReadOnlyDictionary> { - private readonly IReadOnlyList> _items; + private readonly IReadOnlyList> _items; - private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) + private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) { - var tmp = Enum.GetValues().Select(t => new List(1024)).ToArray(); + var tmp = Enum.GetValues().Select(_ => new List(1024)).ToArray(); var itemSheet = dataManager.GetExcelSheet(language)!; - foreach (var item in itemSheet) + foreach (var item in itemSheet.Where(i => i.Name.RawData.Length > 1)) { var type = item.ToEquipType(); - if (type != FullEquipType.Unknown && item.Name.RawData.Length > 1) - tmp[(int)type].Add(item); + if (type.IsWeapon()) + { + if (item.ModelMain != 0) + tmp[(int)type].Add(EquipItem.FromMainhand(item)); + if (item.ModelSub != 0) + tmp[(int)type].Add(EquipItem.FromOffhand(item)); + } + else if (type != FullEquipType.Unknown) + { + tmp[(int)type].Add(EquipItem.FromArmor(item)); + } } - var ret = new IReadOnlyList[tmp.Length]; - ret[0] = Array.Empty(); + var ret = new IReadOnlyList[tmp.Length]; + ret[0] = Array.Empty(); for (var i = 1; i < tmp.Length; ++i) - ret[i] = tmp[i].OrderBy(item => item.Name.ToDalamudString().TextValue).ToArray(); + ret[i] = tmp[i].OrderBy(item => item.Name).ToArray(); return ret; } @@ -44,10 +53,10 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary DisposeTag("ItemList"); - public IEnumerator>> GetEnumerator() + public IEnumerator>> GetEnumerator() { for (var i = 1; i < _items.Count; ++i) - yield return new KeyValuePair>((FullEquipType)i, _items[i]); + yield return new KeyValuePair>((FullEquipType)i, _items[i]); } IEnumerator IEnumerable.GetEnumerator() @@ -59,7 +68,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary (int)key < _items.Count && key != FullEquipType.Unknown; - public bool TryGetValue(FullEquipType key, out IReadOnlyList value) + public bool TryGetValue(FullEquipType key, out IReadOnlyList value) { if (ContainsKey(key)) { @@ -71,12 +80,12 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary this[FullEquipType key] + public IReadOnlyList this[FullEquipType key] => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); public IEnumerable Keys => Enum.GetValues().Skip(1); - public IEnumerable> Values + public IEnumerable> Values => _items.Skip(1); } diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index e992ce43..8e789339 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -65,7 +65,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return ret; } - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) => slot switch { EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs index 1b58d39b..a5e4ddf8 100644 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -9,7 +9,7 @@ using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -internal sealed class WeaponIdentificationList : KeyList +internal sealed class WeaponIdentificationList : KeyList { private const string Tag = "WeaponIdentification"; private const int Version = 1; @@ -18,10 +18,10 @@ internal sealed class WeaponIdentificationList : KeyList : base(pi, Tag, language, Version, CreateWeaponList(gameData, language)) { } - public IEnumerable Between(SetId modelId) + public IEnumerable Between(SetId modelId) => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); - public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) + public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) { if (type == 0) return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); @@ -37,38 +37,31 @@ internal sealed class WeaponIdentificationList : KeyList public static ulong ToKey(SetId modelId, WeaponType type, byte variant) => ((ulong)modelId << 32) | ((ulong)type << 16) | variant; - public static ulong ToKey(Item i, bool offhand) - { - var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; - return ToKey(quad.A, quad.B, (byte)quad.C); - } + public static ulong ToKey(EquipItem i) + => ToKey(i.ModelId, i.WeaponType, i.Variant); - protected override IEnumerable ToKeys(Item i) + protected override IEnumerable ToKeys(EquipItem data) { - var key1 = 0ul; - if (i.ModelMain != 0) - { - key1 = ToKey(i, false); - yield return key1; - } - - if (i.ModelSub != 0) - { - var key2 = ToKey(i, true); - if (key1 != key2) - yield return key2; - } + yield return ToKey(data); } protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(Item data) - => (int)data.RowId; + protected override int ValueKeySelector(EquipItem data) + => (int)data.Id; - private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + => gameData.GetExcelSheet(language)!.SelectMany(ToEquipItems); + + private static IEnumerable ToEquipItems(Item item) { - var items = gameData.GetExcelSheet(language)!; - return items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand); + if ((EquipSlot)item.EquipSlotCategory.Row is not (EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) + yield break; + + if (item.ModelMain != 0) + yield return EquipItem.FromMainhand(item); + if (item.ModelSub != 0) + yield return EquipItem.FromOffhand(item); } } diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs index 85fb078e..8dbd5bb6 100644 --- a/Penumbra.GameData/Enums/ChangedItemExtensions.cs +++ b/Penumbra.GameData/Enums/ChangedItemExtensions.cs @@ -1,33 +1,19 @@ -using System; -using Dalamud.Data; -using Lumina.Excel.GeneratedSheets; using Penumbra.Api.Enums; +using Penumbra.GameData.Structs; using Action = Lumina.Excel.GeneratedSheets.Action; namespace Penumbra.GameData.Enums; public static class ChangedItemExtensions { - public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item ) + public static (ChangedItemType, uint) ChangedItemToTypeAndId(object? item) { return item switch { - null => ( ChangedItemType.None, 0 ), - Item i => ( ChangedItemType.Item, i.RowId ), - Action a => ( ChangedItemType.Action, a.RowId ), - _ => ( ChangedItemType.Customization, 0 ), + null => (ChangedItemType.None, 0), + EquipItem i => (ChangedItemType.Item, i.Id), + Action a => (ChangedItemType.Action, a.RowId), + _ => (ChangedItemType.Customization, 0), }; } - - public static object? GetObject( this ChangedItemType type, DataManager manager, uint id ) - { - return type switch - { - ChangedItemType.None => null, - ChangedItemType.Item => manager.GetExcelSheet< Item >()?.GetRow( id ), - ChangedItemType.Action => manager.GetExcelSheet< Action >()?.GetRow( id ), - ChangedItemType.Customization => null, - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index 27c046ea..c3956ae2 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -20,26 +20,34 @@ public enum FullEquipType : byte Wrists, Finger, - Fists, // PGL, MNK - Sword, // GLA, PLD Main - Axe, // MRD, WAR - Bow, // ARC, BRD - Lance, // LNC, DRG, - Staff, // THM, BLM, CNJ, WHM - Wand, // THM, BLM, CNJ, WHM Main - Book, // ACN, SMN, SCH - Daggers, // ROG, NIN + Fists, // PGL, MNK + FistsOff, + Sword, // GLA, PLD Main + Axe, // MRD, WAR + Bow, // ARC, BRD + BowOff, + Lance, // LNC, DRG, + Staff, // THM, BLM, CNJ, WHM + Wand, // THM, BLM, CNJ, WHM Main + Book, // ACN, SMN, SCH + Daggers, // ROG, NIN + DaggersOff, Broadsword, // DRK, Gun, // MCH, - Orrery, // AST, - Katana, // SAM - Rapier, // RDM - Cane, // BLU - Gunblade, // GNB, - Glaives, // DNC, - Scythe, // RPR, - Nouliths, // SGE - Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off + GunOff, + Orrery, // AST, + OrreryOff, + Katana, // SAM + KatanaOff, + Rapier, // RDM + RapierOff, + Cane, // BLU + Gunblade, // GNB, + Glaives, // DNC, + GlaivesOff, + Scythe, // RPR, + Nouliths, // SGE + Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off Saw, // CRP CrossPeinHammer, // BSM @@ -68,7 +76,7 @@ public enum FullEquipType : byte public static class FullEquipTypeExtensions { - public static FullEquipType ToEquipType(this Item item) + internal static FullEquipType ToEquipType(this Item item) { var slot = (EquipSlot)item.EquipSlotCategory.Row; var weapon = (WeaponCategory)item.ItemUICategory.Row; @@ -152,22 +160,30 @@ public static class FullEquipTypeExtensions FullEquipType.Wrists => EquipSlot.Wrists.ToName(), FullEquipType.Finger => "Ring", FullEquipType.Fists => "Fist Weapon", + FullEquipType.FistsOff => "Fist Weapon (Offhand)", FullEquipType.Sword => "Sword", FullEquipType.Axe => "Axe", FullEquipType.Bow => "Bow", + FullEquipType.BowOff => "Quiver", FullEquipType.Lance => "Lance", FullEquipType.Staff => "Staff", FullEquipType.Wand => "Mace", FullEquipType.Book => "Book", FullEquipType.Daggers => "Dagger", + FullEquipType.DaggersOff => "Dagger (Offhand)", FullEquipType.Broadsword => "Broadsword", FullEquipType.Gun => "Gun", + FullEquipType.GunOff => "Aetherotransformer", FullEquipType.Orrery => "Orrery", + FullEquipType.OrreryOff => "Card Holder", FullEquipType.Katana => "Katana", + FullEquipType.KatanaOff => "Sheathe", FullEquipType.Rapier => "Rapier", + FullEquipType.RapierOff => "Focus", FullEquipType.Cane => "Cane", FullEquipType.Gunblade => "Gunblade", FullEquipType.Glaives => "Glaive", + FullEquipType.GlaivesOff => "Glaive (Offhand)", FullEquipType.Scythe => "Scythe", FullEquipType.Nouliths => "Nouliths", FullEquipType.Shield => "Shield", @@ -209,22 +225,30 @@ public static class FullEquipTypeExtensions FullEquipType.Wrists => EquipSlot.Wrists, FullEquipType.Finger => EquipSlot.RFinger, FullEquipType.Fists => EquipSlot.MainHand, + FullEquipType.FistsOff => EquipSlot.OffHand, FullEquipType.Sword => EquipSlot.MainHand, FullEquipType.Axe => EquipSlot.MainHand, FullEquipType.Bow => EquipSlot.MainHand, + FullEquipType.BowOff => EquipSlot.OffHand, FullEquipType.Lance => EquipSlot.MainHand, FullEquipType.Staff => EquipSlot.MainHand, FullEquipType.Wand => EquipSlot.MainHand, FullEquipType.Book => EquipSlot.MainHand, FullEquipType.Daggers => EquipSlot.MainHand, + FullEquipType.DaggersOff => EquipSlot.OffHand, FullEquipType.Broadsword => EquipSlot.MainHand, FullEquipType.Gun => EquipSlot.MainHand, + FullEquipType.GunOff => EquipSlot.OffHand, FullEquipType.Orrery => EquipSlot.MainHand, + FullEquipType.OrreryOff => EquipSlot.OffHand, FullEquipType.Katana => EquipSlot.MainHand, + FullEquipType.KatanaOff => EquipSlot.OffHand, FullEquipType.Rapier => EquipSlot.MainHand, + FullEquipType.RapierOff => EquipSlot.OffHand, FullEquipType.Cane => EquipSlot.MainHand, FullEquipType.Gunblade => EquipSlot.MainHand, FullEquipType.Glaives => EquipSlot.MainHand, + FullEquipType.GlaivesOff => EquipSlot.OffHand, FullEquipType.Scythe => EquipSlot.MainHand, FullEquipType.Nouliths => EquipSlot.MainHand, FullEquipType.Shield => EquipSlot.OffHand, @@ -253,7 +277,7 @@ public static class FullEquipTypeExtensions _ => EquipSlot.Unknown, }; - public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown) + public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown, bool mainhand = true) => slot switch { EquipSlot.Head => FullEquipType.Head, @@ -273,77 +297,101 @@ public static class FullEquipTypeExtensions EquipSlot.BodyHands => FullEquipType.Body, EquipSlot.BodyLegsFeet => FullEquipType.Body, EquipSlot.ChestHands => FullEquipType.Body, - EquipSlot.MainHand => category.ToEquipType(), - EquipSlot.OffHand => category.ToEquipType(), - EquipSlot.BothHand => category.ToEquipType(), + EquipSlot.MainHand => category.ToEquipType(mainhand), + EquipSlot.OffHand => category.ToEquipType(mainhand), + EquipSlot.BothHand => category.ToEquipType(mainhand), _ => FullEquipType.Unknown, }; - public static FullEquipType ToEquipType(this WeaponCategory category) + public static FullEquipType ToEquipType(this WeaponCategory category, bool mainhand = true) => category switch { - WeaponCategory.Pugilist => FullEquipType.Fists, - WeaponCategory.Gladiator => FullEquipType.Sword, - WeaponCategory.Marauder => FullEquipType.Axe, - WeaponCategory.Archer => FullEquipType.Bow, - WeaponCategory.Lancer => FullEquipType.Lance, - WeaponCategory.Thaumaturge1 => FullEquipType.Wand, - WeaponCategory.Thaumaturge2 => FullEquipType.Staff, - WeaponCategory.Conjurer1 => FullEquipType.Wand, - WeaponCategory.Conjurer2 => FullEquipType.Staff, - WeaponCategory.Arcanist => FullEquipType.Book, - WeaponCategory.Shield => FullEquipType.Shield, - WeaponCategory.CarpenterMain => FullEquipType.Saw, - WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, - WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, - WeaponCategory.BlacksmithOff => FullEquipType.File, - WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, - WeaponCategory.ArmorerOff => FullEquipType.Pliers, - WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, - WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, - WeaponCategory.LeatherworkerMain => FullEquipType.Knife, - WeaponCategory.LeatherworkerOff => FullEquipType.Awl, - WeaponCategory.WeaverMain => FullEquipType.Needle, - WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, - WeaponCategory.AlchemistMain => FullEquipType.Alembic, - WeaponCategory.AlchemistOff => FullEquipType.Mortar, - WeaponCategory.CulinarianMain => FullEquipType.Frypan, - WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, - WeaponCategory.MinerMain => FullEquipType.Pickaxe, - WeaponCategory.MinerOff => FullEquipType.Sledgehammer, - WeaponCategory.BotanistMain => FullEquipType.Hatchet, - WeaponCategory.BotanistOff => FullEquipType.GardenScythe, - WeaponCategory.FisherMain => FullEquipType.FishingRod, - WeaponCategory.Rogue => FullEquipType.Gig, - WeaponCategory.DarkKnight => FullEquipType.Broadsword, - WeaponCategory.Machinist => FullEquipType.Gun, - WeaponCategory.Astrologian => FullEquipType.Orrery, - WeaponCategory.Samurai => FullEquipType.Katana, - WeaponCategory.RedMage => FullEquipType.Rapier, - WeaponCategory.Scholar => FullEquipType.Book, - WeaponCategory.FisherOff => FullEquipType.Gig, - WeaponCategory.BlueMage => FullEquipType.Cane, - WeaponCategory.Gunbreaker => FullEquipType.Gunblade, - WeaponCategory.Dancer => FullEquipType.Glaives, - WeaponCategory.Reaper => FullEquipType.Scythe, - WeaponCategory.Sage => FullEquipType.Nouliths, - _ => FullEquipType.Unknown, + WeaponCategory.Pugilist when mainhand => FullEquipType.Fists, + WeaponCategory.Pugilist => FullEquipType.FistsOff, + WeaponCategory.Gladiator => FullEquipType.Sword, + WeaponCategory.Marauder => FullEquipType.Axe, + WeaponCategory.Archer when mainhand => FullEquipType.Bow, + WeaponCategory.Archer => FullEquipType.BowOff, + WeaponCategory.Lancer => FullEquipType.Lance, + WeaponCategory.Thaumaturge1 => FullEquipType.Wand, + WeaponCategory.Thaumaturge2 => FullEquipType.Staff, + WeaponCategory.Conjurer1 => FullEquipType.Wand, + WeaponCategory.Conjurer2 => FullEquipType.Staff, + WeaponCategory.Arcanist => FullEquipType.Book, + WeaponCategory.Shield => FullEquipType.Shield, + WeaponCategory.CarpenterMain => FullEquipType.Saw, + WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, + WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, + WeaponCategory.BlacksmithOff => FullEquipType.File, + WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, + WeaponCategory.ArmorerOff => FullEquipType.Pliers, + WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, + WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, + WeaponCategory.LeatherworkerMain => FullEquipType.Knife, + WeaponCategory.LeatherworkerOff => FullEquipType.Awl, + WeaponCategory.WeaverMain => FullEquipType.Needle, + WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, + WeaponCategory.AlchemistMain => FullEquipType.Alembic, + WeaponCategory.AlchemistOff => FullEquipType.Mortar, + WeaponCategory.CulinarianMain => FullEquipType.Frypan, + WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, + WeaponCategory.MinerMain => FullEquipType.Pickaxe, + WeaponCategory.MinerOff => FullEquipType.Sledgehammer, + WeaponCategory.BotanistMain => FullEquipType.Hatchet, + WeaponCategory.BotanistOff => FullEquipType.GardenScythe, + WeaponCategory.FisherMain => FullEquipType.FishingRod, + WeaponCategory.FisherOff => FullEquipType.Gig, + WeaponCategory.Rogue when mainhand => FullEquipType.DaggersOff, + WeaponCategory.Rogue => FullEquipType.Daggers, + WeaponCategory.DarkKnight => FullEquipType.Broadsword, + WeaponCategory.Machinist when mainhand => FullEquipType.Gun, + WeaponCategory.Machinist => FullEquipType.GunOff, + WeaponCategory.Astrologian when mainhand => FullEquipType.Orrery, + WeaponCategory.Astrologian => FullEquipType.OrreryOff, + WeaponCategory.Samurai when mainhand => FullEquipType.Katana, + WeaponCategory.Samurai => FullEquipType.KatanaOff, + WeaponCategory.RedMage when mainhand => FullEquipType.Rapier, + WeaponCategory.RedMage => FullEquipType.RapierOff, + WeaponCategory.Scholar => FullEquipType.Book, + WeaponCategory.BlueMage => FullEquipType.Cane, + WeaponCategory.Gunbreaker => FullEquipType.Gunblade, + WeaponCategory.Dancer when mainhand => FullEquipType.Glaives, + WeaponCategory.Dancer => FullEquipType.GlaivesOff, + WeaponCategory.Reaper => FullEquipType.Scythe, + WeaponCategory.Sage => FullEquipType.Nouliths, + _ => FullEquipType.Unknown, }; public static FullEquipType Offhand(this FullEquipType type) => type switch { - FullEquipType.Fists => FullEquipType.Fists, + FullEquipType.Fists => FullEquipType.FistsOff, FullEquipType.Sword => FullEquipType.Shield, FullEquipType.Wand => FullEquipType.Shield, - FullEquipType.Daggers => FullEquipType.Daggers, - FullEquipType.Gun => FullEquipType.Gun, - FullEquipType.Orrery => FullEquipType.Orrery, - FullEquipType.Rapier => FullEquipType.Rapier, - FullEquipType.Glaives => FullEquipType.Glaives, + FullEquipType.Daggers => FullEquipType.DaggersOff, + FullEquipType.Gun => FullEquipType.GunOff, + FullEquipType.Orrery => FullEquipType.OrreryOff, + FullEquipType.Rapier => FullEquipType.RapierOff, + FullEquipType.Glaives => FullEquipType.GlaivesOff, + FullEquipType.Bow => FullEquipType.BowOff, + FullEquipType.Katana => FullEquipType.KatanaOff, _ => FullEquipType.Unknown, }; + internal static string OffhandTypeSuffix(this FullEquipType type) + => type switch + { + FullEquipType.FistsOff => " (Offhand)", + FullEquipType.DaggersOff => " (Offhand)", + FullEquipType.GunOff => " (Aetherotransformer)", + FullEquipType.OrreryOff => " (Card Holder)", + FullEquipType.RapierOff => " (Focus)", + FullEquipType.GlaivesOff => " (Offhand)", + FullEquipType.BowOff => " (Quiver)", + FullEquipType.KatanaOff => " (Sheathe)", + _ => string.Empty, + }; + public static readonly IReadOnlyList WeaponTypes = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index f5fc1d05..02f7d33b 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -58,10 +58,10 @@ public interface IObjectIdentifier : IDisposable /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. /// The variant ID of the model. /// The equipment slot the piece of equipment uses. - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); /// - public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) + public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); } diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs new file mode 100644 index 00000000..9e26b27c --- /dev/null +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -0,0 +1,88 @@ +using System.Runtime.InteropServices; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; + +namespace Penumbra.GameData.Structs; + +[StructLayout(LayoutKind.Sequential)] +public readonly struct EquipItem +{ + public readonly string Name; + public readonly uint Id; + public readonly ushort IconId; + public readonly SetId ModelId; + public readonly WeaponType WeaponType; + public readonly byte Variant; + public readonly FullEquipType Type; + public readonly EquipSlot Slot; + + public bool Valid + => Type != FullEquipType.Unknown; + + public CharacterArmor Armor() + => new(ModelId, Variant, 0); + + public CharacterArmor Armor(StainId stain) + => new(ModelId, Variant, stain); + + public CharacterWeapon Weapon() + => new(ModelId, WeaponType, Variant, 0); + + public CharacterWeapon Weapon(StainId stain) + => new(ModelId, WeaponType, Variant, stain); + + public EquipItem() + => Name = string.Empty; + + public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type, + EquipSlot slot) + { + Name = name; + Id = id; + IconId = iconId; + ModelId = modelId; + WeaponType = weaponType; + Variant = variant; + Type = type; + Slot = slot; + } + + + public static EquipItem FromArmor(Item item) + { + var type = item.ToEquipType(); + var slot = type.ToSlot(); + var name = string.Intern(item.Name.ToDalamudString().TextValue); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelMain; + var weapon = (WeaponType)0; + var variant = (byte)(item.ModelMain >> 16); + return new EquipItem(name, id, icon, model, weapon, variant, type, slot); + } + + public static EquipItem FromMainhand(Item item) + { + var type = item.ToEquipType(); + var name = string.Intern(item.Name.ToDalamudString().TextValue); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelMain; + var weapon = (WeaponType)(item.ModelMain >> 16); + var variant = (byte)(item.ModelMain >> 32); + return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.MainHand); + } + + public static EquipItem FromOffhand(Item item) + { + var type = item.ToEquipType().Offhand(); + var name = string.Intern(item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix()); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelSub; + var weapon = (WeaponType)(item.ModelSub >> 16); + var variant = (byte)(item.ModelSub >> 32); + return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.OffHand); + } +} diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 80ff8e75..01a5fd56 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -17,6 +17,9 @@ public sealed class ChangedItemClick : EventWrapper { /// Default = 0, + + /// + Link = 1, } public ChangedItemClick() diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index afedf8fd..c0736256 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -15,6 +15,9 @@ public sealed class ChangedItemHover : EventWrapper, ChangedItem { /// Default = 0, + + /// + Link = 1, } public ChangedItemHover() diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 83fd2e51..12c24033 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -91,6 +91,8 @@ public class Configuration : IPluginConfiguration, ISavable public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; + public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; public bool AutoDeduplicateOnImport { get; set; } = true; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index e7de1a4a..57fdf20e 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -53,7 +53,7 @@ public class ResourceTree if (imcNode != null) Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); - var mdl = (RenderModel*)model->ModelArray[i]; + var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 443376a6..902f5e08 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -46,9 +44,9 @@ public static class EquipmentSwap : Array.Empty(); } - public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + public static EquipItem[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, Func redirections, Func manips, - EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo) + EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); @@ -104,9 +102,9 @@ public static class EquipmentSwap return affectedItems; } - public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, - Func redirections, Func manips, Item itemFrom, - Item itemTo, bool rFinger = true, bool lFinger = true) + public static EquipItem[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + Func redirections, Func manips, EquipItem itemFrom, + EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); @@ -122,7 +120,7 @@ public static class EquipmentSwap if (gmp != null) swaps.Add(gmp); - var affectedItems = Array.Empty(); + var affectedItems = Array.Empty(); foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); @@ -242,22 +240,22 @@ public static class EquipmentSwap return mdl; } - private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant) + private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out byte variant) { - slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - if (!slot.IsEquipmentPiece()) + if (!i.Slot.IsEquipmentPiece()) throw new ItemSwap.InvalidItemTypeException(); - modelId = ((Quad)i.ModelMain).A; - variant = (byte)((Quad)i.ModelMain).B; + slot = i.Slot; + modelId = i.ModelId; + variant = i.Variant; } - private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, + private static (ImcFile, byte[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default); var imc = new ImcFile(manager, entry); - Item[] items; + EquipItem[] items; byte[] variants; if (idFrom.Value == idTo.Value) { @@ -271,7 +269,7 @@ public static class EquipmentSwap { items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray(); } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index c793c68f..00bbaac8 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -132,7 +132,7 @@ public class ItemSwapContainer return m => set.TryGetValue( m, out var a ) ? a : m; } - public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) + public EquipItem[] LoadEquipment( EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) { Swaps.Clear(); Loaded = false; @@ -141,7 +141,7 @@ public class ItemSwapContainer return ret; } - public Item[] LoadTypeSwap( EquipSlot slotFrom, Item from, EquipSlot slotTo, Item to, ModCollection? collection = null ) + public EquipItem[] LoadTypeSwap( EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null ) { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 545ae83c..ba64ef79 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; using Dalamud.Plugin; using ImGuiNET; using Lumina.Excel.GeneratedSheets; @@ -21,6 +20,8 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; +using ChangedItemClick = Penumbra.Communication.ChangedItemClick; +using ChangedItemHover = Penumbra.Communication.ChangedItemHover; namespace Penumbra; @@ -50,16 +51,17 @@ public class Penumbra : IDalamudPlugin public Penumbra(DalamudPluginInterface pluginInterface) { try - { + { var startTimer = new StartTracker(); - using var timer = startTimer.Measure(StartTimeType.Total); + using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); ChatService = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() : "Unknown"; - Log.Information($"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); + Log.Information( + $"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); _services.GetRequiredService(); // Initialize because not required anywhere else. _config = _services.GetRequiredService(); _characterUtility = _services.GetRequiredService(); @@ -72,7 +74,7 @@ public class Penumbra : IDalamudPlugin _redrawService = _services.GetRequiredService(); _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { @@ -91,8 +93,8 @@ public class Penumbra : IDalamudPlugin if (_characterUtility.Ready) _residentResources.Reload(); } - catch(Exception ex) - { + catch (Exception ex) + { Log.Error($"Error constructing Penumbra, Disposing again:\n{ex}"); Dispose(); throw; @@ -104,16 +106,17 @@ public class Penumbra : IDalamudPlugin using var timer = _services.GetRequiredService().Measure(StartTimeType.Api); var api = _services.GetRequiredService(); _services.GetRequiredService(); - api.ChangedItemTooltip += it => + _communicatorService.ChangedItemHover.Subscribe(it => { if (it is Item) ImGui.TextUnformatted("Left Click to create an item link in chat."); - }; - api.ChangedItemClicked += (button, it) => + }, ChangedItemHover.Priority.Link); + + _communicatorService.ChangedItemClick.Subscribe((button, it) => { if (button == MouseButton.Left && it is Item item) ChatService.LinkItem(item); - }; + }, ChangedItemClick.Priority.Link); } private void SetupInterface() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 72283304..7ca83ec8 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -129,14 +129,14 @@ public class ItemSwapTab : IDisposable, ITab Weapon, } - private class ItemSelector : FilterComboCache<(string, Item)> + private class ItemSelector : FilterComboCache { public ItemSelector(ItemService data, FullEquipType type) - : base(() => data.AwaitedService[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) + : base(() => data.AwaitedService[type]) { } - protected override string ToString((string, Item) obj) - => obj.Item1; + protected override string ToString(EquipItem obj) + => obj.Name; } private class WeaponSelector : FilterComboCache @@ -179,7 +179,7 @@ public class ItemSwapTab : IDisposable, ITab private bool _useLeftRing = true; private bool _useRightRing = true; - private Item[]? _affectedItems; + private EquipItem[]? _affectedItems; private void UpdateState() { @@ -203,17 +203,16 @@ public class ItemSwapTab : IDisposable, ITab case SwapType.Bracelet: case SwapType.Ring: var values = _selectors[_lastTab]; - if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null) - _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, + if (values.Source.CurrentSelection.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Type != FullEquipType.Unknown) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); - if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, - selectorFrom.CurrentSelection.Item2, + if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid) + _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection, _slotFrom, selectorFrom.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -468,7 +467,7 @@ public class ItemSwapTab : IDisposable, ITab } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -493,7 +492,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (_affectedItems is not { Length: > 1 }) return; @@ -502,8 +501,8 @@ public class ItemSwapTab : IDisposable, ITab ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)) + .Select(i => i.Name))); } private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) @@ -534,7 +533,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) @@ -547,7 +546,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) { @@ -562,8 +561,8 @@ public class ItemSwapTab : IDisposable, ITab ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)) + .Select(i => i.Name))); } private void DrawHairSwap() @@ -647,14 +646,14 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("and put this variant of it"); ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("onto this one"); ImGui.TableNextColumn(); - _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index c52bd5b3..1c84d6b2 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -4,15 +4,19 @@ using System.Linq; using System.Numerics; using Dalamud.Data; using Dalamud.Interface; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using ImGuiScene; -using Lumina.Data.Parsing; +using Lumina.Data.Files; +using Lumina.Excel; using Lumina.Excel.GeneratedSheets; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Services; using Penumbra.UI.Classes; @@ -20,18 +24,42 @@ namespace Penumbra.UI; public class ChangedItemDrawer : IDisposable { - private const EquipSlot MonsterSlot = (EquipSlot)100; - private const EquipSlot DemihumanSlot = (EquipSlot)101; - private const EquipSlot CustomizationSlot = (EquipSlot)102; - private const EquipSlot ActionSlot = (EquipSlot)103; - - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - - public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator) + [Flags] + public enum ChangedItemIcon : uint { + Head = 0x0001, + Body = 0x0002, + Hands = 0x0004, + Legs = 0x0008, + Feet = 0x0010, + Ears = 0x0020, + Neck = 0x0040, + Wrists = 0x0080, + Finger = 0x0100, + Monster = 0x0200, + Demihuman = 0x0400, + Customization = 0x0800, + Action = 0x1000, + Mainhand = 0x2000, + Offhand = 0x4000, + Unknown = 0x8000, + } + + public const ChangedItemIcon AllFlags = (ChangedItemIcon)0xFFFF; + public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; + + private readonly Configuration _config; + private readonly ExcelSheet _items; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth = 0; + + public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator, Configuration config) + { + _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true); _communicator = communicator; + _config = config; } public void Dispose() @@ -41,37 +69,49 @@ public class ChangedItemDrawer : IDisposable _icons.Clear(); } - /// Apply Changed Item Counters to the Name if necessary. - public static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + /// Check if a changed item should be drawn based on its category. + public bool FilterChangedItem(string name, object? data, LowerString filter) + => (_config.ChangedItemFilter == AllFlags || _config.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) + && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); - /// Add filterable information to the string. - public static string ChangedItemFilterName(string name, object? data) - => data switch + /// Draw the icon corresponding to the category of a changed item. + public void DrawCategoryIcon(string name, object? data) + { + var height = ImGui.GetFrameHeight(); + var iconType = GetCategoryIcon(name, data); + if (!_icons.TryGetValue(iconType, out var icon)) { - int counter => $"{counter} Files Manipulating {name}s", - Item it => $"{name}\0{((EquipSlot)it.EquipSlotCategory.Row).ToName()}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", - ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", - _ => name, - }; + ImGui.Dummy(new Vector2(height)); + return; + } + + ImGui.Image(icon.ImGuiHandle, new Vector2(height)); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.SameLine(); + ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0); + } + } /// /// Draw a changed item, invoking the Api-Events for clicks and tooltips. /// Also draw the item Id in grey if requested. /// - public void DrawChangedItem(string name, object? data, bool drawId) + public void DrawChangedItem(string name, object? data) { name = ChangedItemName(name, data); - DrawCategoryIcon(name, data); - ImGui.SameLine(); using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) { - var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) ? MouseButton.Left : MouseButton.None; + var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + ? MouseButton.Left + : MouseButton.None; ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); + _communicator.ChangedItemClick.Invoke(ret, Convert(data)); } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) @@ -80,13 +120,17 @@ public class ChangedItemDrawer : IDisposable // Circumvent ugly blank tooltip with less-ugly useless tooltip. using var tt = ImRaii.Tooltip(); using var group = ImRaii.Group(); - _communicator.ChangedItemHover.Invoke(data); + _communicator.ChangedItemHover.Invoke(Convert(data)); group.Dispose(); if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); } + } - if (!drawId || !GetChangedItemObject(data, out var text)) + /// Draw the model information, right-justified. + public void DrawModelData(object? data) + { + if (!GetChangedItemObject(data, out var text)) return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); @@ -94,58 +138,147 @@ public class ChangedItemDrawer : IDisposable ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); } - private void DrawCategoryIcon(string name, object? obj) + /// Draw a header line with the different icon types to filter them. + public void DrawTypeFilter() { - var height = ImGui.GetFrameHeight(); - var slot = EquipSlot.Unknown; - var desc = string.Empty; - if (obj is Item it) - { - slot = (EquipSlot)it.EquipSlotCategory.Row; - desc = slot.ToName(); - } - else if (obj is ModelChara m) - { - (slot, desc) = (CharacterBase.ModelType)m.Type switch + using var _ = ImRaii.PushId("ChangedItemIconFilter"); + var available = ImGui.GetContentRegionAvail().X; + var (numLines, size) = available / _icons.Count > ImGui.GetTextLineHeight() * 2 + ? (1, new Vector2(Math.Min(_smallestIconWidth, available / _icons.Count))) + : (2, new Vector2(Math.Min(_smallestIconWidth, 2 * available / _icons.Count))); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var lines = numLines == 2 + ? new[] { - CharacterBase.ModelType.DemiHuman => (DemihumanSlot, "Demi-Human"), - CharacterBase.ModelType.Monster => (MonsterSlot, "Monster"), - _ => (EquipSlot.Unknown, string.Empty), + new[] + { + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Unknown, + }, + new[] + { + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + }, + } + : new[] + { + new[] + { + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + ChangedItemIcon.Unknown, + }, }; - } - else if (name.StartsWith("Action: ")) + + void DrawIcon(ChangedItemIcon type) { - (slot, desc) = (ActionSlot, "Action"); - } - else if (name.StartsWith("Customization: ")) - { - (slot, desc) = (CustomizationSlot, "Customization"); + var icon = _icons[type]; + var flag = _config.ChangedItemFilter.HasFlag(type); + ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); + if (ImGui.IsItemClicked()) + { + _config.ChangedItemFilter = flag ? _config.ChangedItemFilter & ~type : _config.ChangedItemFilter | type; + _config.Save(); + } + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.SameLine(); + ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); + } } - if (!_icons.TryGetValue(slot, out var icon)) + foreach (var line in lines) { - ImGui.Dummy(new Vector2(height)); - return; - } + foreach (var iconType in line.SkipLast(1)) + { + DrawIcon(iconType); + ImGui.SameLine(); + } - ImGui.Image(icon.ImGuiHandle, new Vector2(height)); - if (ImGui.IsItemHovered() && icon.Height > height) - { - using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); - ImGui.SameLine(); - ImGuiUtil.DrawTextButton(desc, new Vector2(0, icon.Height), 0); + DrawIcon(line.Last()); } } + /// Obtain the icon category corresponding to a changed item. + private static ChangedItemIcon GetCategoryIcon(string name, object? obj) + { + var iconType = ChangedItemIcon.Unknown; + switch (obj) + { + case EquipItem it: + iconType = it.Slot switch + { + EquipSlot.MainHand => ChangedItemIcon.Mainhand, + EquipSlot.OffHand => ChangedItemIcon.Offhand, + EquipSlot.Head => ChangedItemIcon.Head, + EquipSlot.Body => ChangedItemIcon.Body, + EquipSlot.Hands => ChangedItemIcon.Hands, + EquipSlot.Legs => ChangedItemIcon.Legs, + EquipSlot.Feet => ChangedItemIcon.Feet, + EquipSlot.Ears => ChangedItemIcon.Ears, + EquipSlot.Neck => ChangedItemIcon.Neck, + EquipSlot.Wrists => ChangedItemIcon.Wrists, + EquipSlot.RFinger => ChangedItemIcon.Finger, + _ => ChangedItemIcon.Unknown, + }; + break; + case ModelChara m: + iconType = (CharacterBase.ModelType)m.Type switch + { + CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman, + CharacterBase.ModelType.Monster => ChangedItemIcon.Monster, + _ => ChangedItemIcon.Unknown, + }; + break; + default: + { + if (name.StartsWith("Action: ")) + iconType = ChangedItemIcon.Action; + else if (name.StartsWith("Customization: ")) + iconType = ChangedItemIcon.Customization; + break; + } + } + + return iconType; + } + /// Return more detailed object information in text, if it exists. - public static bool GetChangedItemObject(object? obj, out string text) + private static bool GetChangedItemObject(object? obj, out string text) { switch (obj) { - case Item it: - var quad = (Quad)it.ModelMain; - text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + case EquipItem it: + text = it.WeaponType == 0 ? $"({it.ModelId.Value}-{it.Variant})" : $"({it.ModelId.Value}-{it.WeaponType.Value}-{it.Variant})"; return true; case ModelChara m: text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; @@ -156,6 +289,51 @@ public class ChangedItemDrawer : IDisposable } } + /// We need to transform the internal EquipItem type to the Lumina Item type for API-events. + private object? Convert(object? data) + { + if (data is EquipItem it) + return _items.GetRow(it.Id); + + return data; + } + + private static string ToDescription(ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Head => EquipSlot.Head.ToName(), + ChangedItemIcon.Body => EquipSlot.Body.ToName(), + ChangedItemIcon.Hands => EquipSlot.Hands.ToName(), + ChangedItemIcon.Legs => EquipSlot.Legs.ToName(), + ChangedItemIcon.Feet => EquipSlot.Feet.ToName(), + ChangedItemIcon.Ears => EquipSlot.Ears.ToName(), + ChangedItemIcon.Neck => EquipSlot.Neck.ToName(), + ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(), + ChangedItemIcon.Finger => "Ring", + ChangedItemIcon.Monster => "Monster", + ChangedItemIcon.Demihuman => "Demi-Human", + ChangedItemIcon.Customization => "Customization", + ChangedItemIcon.Action => "Action", + ChangedItemIcon.Mainhand => "Weapon (Mainhand)", + ChangedItemIcon.Offhand => "Weapon (Offhand)", + _ => "Other", + }; + + /// Apply Changed Item Counters to the Name if necessary. + private static string ChangedItemName(string name, object? data) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + + /// Add filterable information to the string. + private static string ChangedItemFilterName(string name, object? data) + => data switch + { + int counter => $"{counter} Files Manipulating {name}s", + EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", + ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", + _ => name, + }; + + /// Initialize the icons. private bool CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); @@ -163,94 +341,40 @@ public class ChangedItemDrawer : IDisposable if (!equipTypeIcons.Valid) return false; - // Weapon - var tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0); - if (tex != null) + void Add(ChangedItemIcon icon, TextureWrap? tex) { - _icons.Add(EquipSlot.MainHand, tex); - _icons.Add(EquipSlot.BothHand, tex); + if (tex != null) + _icons.Add(icon, tex); } - // Hat - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1); - if (tex != null) - _icons.Add(EquipSlot.Head, tex); + Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIcon.Monster, gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex")); + Add(ChangedItemIcon.Demihuman, gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex")); + Add(ChangedItemIcon.Customization, gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex")); + Add(ChangedItemIcon.Action, gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex")); - // Body - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2); - if (tex != null) - { - _icons.Add(EquipSlot.Body, tex); - _icons.Add(EquipSlot.BodyHands, tex); - _icons.Add(EquipSlot.BodyHandsLegsFeet, tex); - _icons.Add(EquipSlot.BodyLegsFeet, tex); - _icons.Add(EquipSlot.ChestHands, tex); - _icons.Add(EquipSlot.FullBody, tex); - _icons.Add(EquipSlot.HeadBody, tex); - } + var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); + if (unk == null) + return true; - // Hands - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3); - if (tex != null) - _icons.Add(EquipSlot.Hands, tex); + var image = unk.GetRgbaImageData(); + var bytes = new byte[unk.Header.Height * unk.Header.Height * 4]; + var diff = 2 * (unk.Header.Height - unk.Header.Width); + for (var y = 0; y < unk.Header.Height; ++y) + image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); + Add(ChangedItemIcon.Unknown, uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4)); - // Pants - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5); - if (tex != null) - { - _icons.Add(EquipSlot.Legs, tex); - _icons.Add(EquipSlot.LegsFeet, tex); - } - - // Shoes - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6); - if (tex != null) - _icons.Add(EquipSlot.Feet, tex); - - // Offhand - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7); - if (tex != null) - _icons.Add(EquipSlot.OffHand, tex); - - // Earrings - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8); - if (tex != null) - _icons.Add(EquipSlot.Ears, tex); - - // Necklace - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9); - if (tex != null) - _icons.Add(EquipSlot.Neck, tex); - - // Bracelet - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10); - if (tex != null) - _icons.Add(EquipSlot.Wrists, tex); - - // Ring - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11); - if (tex != null) - _icons.Add(EquipSlot.RFinger, tex); - - // Monster - tex = gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex"); - if (tex != null) - _icons.Add(MonsterSlot, tex); - - // Demihuman - tex = gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex"); - if (tex != null) - _icons.Add(DemihumanSlot, tex); - - // Customization - tex = gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex"); - if (tex != null) - _icons.Add(CustomizationSlot, tex); - - // Action - tex = gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex"); - if (tex != null) - _icons.Add(ActionSlot, tex); + _smallestIconWidth = _icons.Values.Min(i => i.Width); return true; } diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 46d9f3bc..df2c905e 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; using ImGuiNET; using OtterGui; @@ -14,6 +15,8 @@ public class ModPanelChangedItemsTab : ITab private readonly ModFileSystemSelector _selector; private readonly ChangedItemDrawer _drawer; + private ChangedItemDrawer.ChangedItemIcon _filter = Enum.GetValues().Aggregate((a, b) => a | b); + public ReadOnlySpan Label => "Changed Items"u8; @@ -28,12 +31,30 @@ public class ModPanelChangedItemsTab : ITab public void DrawContent() { - using var list = ImRaii.ListBox("##changedItems", -Vector2.One); - if (!list) + _drawer.DrawTypeFilter(); + ImGui.Separator(); + using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, + new Vector2(ImGui.GetContentRegionAvail().X, -1)); + if (!table) return; var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); - var height = ImGui.GetFrameHeight(); - ImGuiClip.ClippedDraw(zipList, kvp => _drawer.DrawChangedItem(kvp.Item1, kvp.Item2, true), height); + var height = ImGui.GetFrameHeightWithSpacing(); + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(height); + var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); + ImGuiClip.DrawEndDummy(remainder, height); + } + + private bool CheckFilter((string Name, object? Data) kvp) + => _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + + private void DrawChangedItem((string Name, object? Data) kvp) + { + ImGui.TableNextColumn(); + _drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + ImGui.SameLine(); + _drawer.DrawChangedItem(kvp.Name, kvp.Data); + _drawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 9b929c60..f85387f8 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -39,8 +39,9 @@ public class ChangedItemsTab : ITab public void DrawContent() { - _collectionHeader.Draw(true); - var varWidth = DrawFilters(); + _collectionHeader.Draw(true); + _drawer.DrawTypeFilter(); + var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); if (!child) return; @@ -57,9 +58,7 @@ public class ChangedItemsTab : ITab ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); var items = _collectionManager.Active.Current.ChangedItems; - var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty - ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) - : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); + var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); ImGuiClip.DrawEndDummy(rest, height); } @@ -79,26 +78,21 @@ public class ChangedItemsTab : ITab /// Apply the current filters. private bool FilterChangedItem(KeyValuePair, object?)> item) - => (_changedItemFilter.IsEmpty - || ChangedItemDrawer.ChangedItemFilterName(item.Key, item.Value.Item2) - .Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase)) + => _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. private void DrawChangedItemColumn(KeyValuePair, object?)> item) { ImGui.TableNextColumn(); - _drawer.DrawChangedItem(item.Key, item.Value.Item2, false); + _drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + ImGui.SameLine(); + _drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - if (!ChangedItemDrawer.GetChangedItemObject(item.Value.Item2, out var text)) - return; - - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightAlign(text); + _drawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) @@ -110,7 +104,7 @@ public class ChangedItemsTab : ITab using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) && ImGui.GetIO().KeyCtrl - && first is Mod mod) + && first is Mod mod) _communicator.SelectTab.Invoke(TabType.Mods, mod); if (ImGui.IsItemHovered()) From 5aec508616bfc64521d87ebbe60d9117e1c113ca Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 9 Jun 2023 14:13:14 +0000 Subject: [PATCH 1010/2451] [CI] Updating repo.json for t0.7.1.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3f8ab838..39e81185 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.6", + "TestingAssemblyVersion": "0.7.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From e72479c0467046c725506716bd0f2991d84e2ce2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Jun 2023 17:49:09 +0200 Subject: [PATCH 1011/2451] Make filter icons non-scaling. --- Penumbra/UI/ChangedItemDrawer.cs | 87 ++++++++++---------------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 1c84d6b2..09f31c99 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -142,59 +142,27 @@ public class ChangedItemDrawer : IDisposable public void DrawTypeFilter() { using var _ = ImRaii.PushId("ChangedItemIconFilter"); - var available = ImGui.GetContentRegionAvail().X; - var (numLines, size) = available / _icons.Count > ImGui.GetTextLineHeight() * 2 - ? (1, new Vector2(Math.Min(_smallestIconWidth, available / _icons.Count))) - : (2, new Vector2(Math.Min(_smallestIconWidth, 2 * available / _icons.Count))); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - var lines = numLines == 2 - ? new[] - { - new[] - { - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Unknown, - }, - new[] - { - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - }, - } - : new[] - { - new[] - { - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - ChangedItemIcon.Unknown, - }, - }; + var size = new Vector2(2 * ImGui.GetTextLineHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var order = new[] + { + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + ChangedItemIcon.Unknown, + }; void DrawIcon(ChangedItemIcon type) { @@ -216,16 +184,13 @@ public class ChangedItemDrawer : IDisposable } } - foreach (var line in lines) + foreach (var iconType in order.SkipLast(1)) { - foreach (var iconType in line.SkipLast(1)) - { - DrawIcon(iconType); - ImGui.SameLine(); - } - - DrawIcon(line.Last()); + DrawIcon(iconType); + ImGui.SameLine(); } + + DrawIcon(order.Last()); } /// Obtain the icon category corresponding to a changed item. From 387b6da4d5860f317676d912648f11579cb8df5e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Jun 2023 18:13:52 +0200 Subject: [PATCH 1012/2451] Fix changed item event. --- .../Enums/ChangedItemExtensions.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs index 8dbd5bb6..f2b531d6 100644 --- a/Penumbra.GameData/Enums/ChangedItemExtensions.cs +++ b/Penumbra.GameData/Enums/ChangedItemExtensions.cs @@ -1,5 +1,7 @@ +using System; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; using Penumbra.Api.Enums; -using Penumbra.GameData.Structs; using Action = Lumina.Excel.GeneratedSheets.Action; namespace Penumbra.GameData.Enums; @@ -10,10 +12,22 @@ public static class ChangedItemExtensions { return item switch { - null => (ChangedItemType.None, 0), - EquipItem i => (ChangedItemType.Item, i.Id), - Action a => (ChangedItemType.Action, a.RowId), - _ => (ChangedItemType.Customization, 0), + null => (ChangedItemType.None, 0), + Item i => (ChangedItemType.Item, i.RowId), + Action a => (ChangedItemType.Action, a.RowId), + _ => (ChangedItemType.Customization, 0), + }; + } + + public static object? GetObject(this ChangedItemType type, DataManager manager, uint id) + { + return type switch + { + ChangedItemType.None => null, + ChangedItemType.Item => manager.GetExcelSheet()?.GetRow(id), + ChangedItemType.Action => manager.GetExcelSheet()?.GetRow(id), + ChangedItemType.Customization => null, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } } From 6b4e60e42ee166c0225ab1f677c29ba814620145 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 9 Jun 2023 16:16:26 +0000 Subject: [PATCH 1013/2451] [CI] Updating repo.json for t0.7.1.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 39e81185..776bb618 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.7", + "TestingAssemblyVersion": "0.7.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4f63e32df34d525e81dca63745ef6f01490016c1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 00:52:41 +0200 Subject: [PATCH 1014/2451] Move Dissolve Folder to the bottom. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1eda5406..57f84013 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1eda5406db266e49ffb04a73bd5217ce6281f489 +Subproject commit 57f84013b42be29e4a17e5dcf6b2f59c15c2586d From 0999ab804a11e840d188c09ab13207c27282a2e0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 00:55:59 +0200 Subject: [PATCH 1015/2451] Fix ninja weapons. --- Penumbra.GameData/Enums/FullEquipType.cs | 4 ++-- Penumbra.GameData/Structs/EquipItem.cs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index c3956ae2..a45d0800 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -341,8 +341,8 @@ public static class FullEquipTypeExtensions WeaponCategory.BotanistOff => FullEquipType.GardenScythe, WeaponCategory.FisherMain => FullEquipType.FishingRod, WeaponCategory.FisherOff => FullEquipType.Gig, - WeaponCategory.Rogue when mainhand => FullEquipType.DaggersOff, - WeaponCategory.Rogue => FullEquipType.Daggers, + WeaponCategory.Rogue when mainhand => FullEquipType.Daggers, + WeaponCategory.Rogue => FullEquipType.DaggersOff, WeaponCategory.DarkKnight => FullEquipType.Broadsword, WeaponCategory.Machinist when mainhand => FullEquipType.Gun, WeaponCategory.Machinist => FullEquipType.GunOff, diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs index 9e26b27c..78d73870 100644 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -38,7 +38,7 @@ public readonly struct EquipItem public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type, EquipSlot slot) { - Name = name; + Name = string.Intern(name); Id = id; IconId = iconId; ModelId = modelId; @@ -48,12 +48,11 @@ public readonly struct EquipItem Slot = slot; } - public static EquipItem FromArmor(Item item) { var type = item.ToEquipType(); var slot = type.ToSlot(); - var name = string.Intern(item.Name.ToDalamudString().TextValue); + var name = item.Name.ToDalamudString().TextValue; var id = item.RowId; var icon = item.Icon; var model = (SetId)item.ModelMain; @@ -65,7 +64,7 @@ public readonly struct EquipItem public static EquipItem FromMainhand(Item item) { var type = item.ToEquipType(); - var name = string.Intern(item.Name.ToDalamudString().TextValue); + var name = item.Name.ToDalamudString().TextValue; var id = item.RowId; var icon = item.Icon; var model = (SetId)item.ModelMain; @@ -77,7 +76,7 @@ public readonly struct EquipItem public static EquipItem FromOffhand(Item item) { var type = item.ToEquipType().Offhand(); - var name = string.Intern(item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix()); + var name = item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix(); var id = item.RowId; var icon = item.Icon; var model = (SetId)item.ModelSub; From 5a9f1385a25c74e60d9581f90fb4899cba911f7e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 00:56:07 +0200 Subject: [PATCH 1016/2451] Add All toggle for changed items. --- Penumbra/UI/ChangedItemDrawer.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 09f31c99..114b05e8 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -141,9 +141,9 @@ public class ChangedItemDrawer : IDisposable /// Draw a header line with the different icon types to filter them. public void DrawTypeFilter() { - using var _ = ImRaii.PushId("ChangedItemIconFilter"); - var size = new Vector2(2 * ImGui.GetTextLineHeight()); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + using var _ = ImRaii.PushId("ChangedItemIconFilter"); + var size = new Vector2(2 * ImGui.GetTextLineHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); var order = new[] { ChangedItemIcon.Head, @@ -184,13 +184,21 @@ public class ChangedItemDrawer : IDisposable } } - foreach (var iconType in order.SkipLast(1)) + foreach (var iconType in order) { DrawIcon(iconType); ImGui.SameLine(); } - DrawIcon(order.Last()); + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); + ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + _config.ChangedItemFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : + _config.ChangedItemFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); + if (ImGui.IsItemClicked()) + { + _config.ChangedItemFilter = _config.ChangedItemFilter == AllFlags ? 0 : AllFlags; + _config.Save(); + } } /// Obtain the icon category corresponding to a changed item. @@ -327,6 +335,7 @@ public class ChangedItemDrawer : IDisposable Add(ChangedItemIcon.Demihuman, gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex")); Add(ChangedItemIcon.Customization, gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex")); Add(ChangedItemIcon.Action, gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex")); + Add(AllFlags, gameData.GetImGuiTexture("ui/icon/114000/114052_hr1.tex")); var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) From 712dcf578200ec24551854d0d54bb6a02d573311 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 13:27:17 +0200 Subject: [PATCH 1017/2451] Fix not being able to update option descriptions to empty. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f5b03659..786a6130 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -195,7 +195,7 @@ public class ModPanelEditTab : ITab var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); ImGui.SameLine(); var fileExists = File.Exists(_filenames.ModMetaPath(_mod)); @@ -304,16 +304,15 @@ public class ModPanelEditTab : ITab /// Open a popup to edit a multi-line mod or option description. private static class DescriptionEdit { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; - private static FilenameService? _fileNames; + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static string _oldDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDescriptionOptionIdx = -1; + private static Mod? _mod; - public static void OpenPopup(FilenameService filenames, Mod mod, int groupIdx, int optionIdx = -1) + public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) { - _fileNames = filenames; _newDescriptionIdx = groupIdx; _newDescriptionOptionIdx = optionIdx; _newDescription = groupIdx < 0 @@ -321,6 +320,7 @@ public class ModPanelEditTab : ITab : optionIdx < 0 ? mod.Groups[groupIdx].Description : mod.Groups[groupIdx][optionIdx].Description; + _oldDescription = _newDescription; _mod = mod; ImGui.OpenPopup(PopupName); @@ -347,11 +347,7 @@ public class ModPanelEditTab : ITab + ImGui.GetStyle().ItemSpacing.X; ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); - var oldDescription = _newDescriptionIdx == Input.Description - ? _mod.Description - : _mod.Groups[_newDescriptionIdx].Description; - - var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; + var tooltip = _newDescription != _oldDescription ? string.Empty : "No changes made yet."; if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) { @@ -429,7 +425,7 @@ public class ModPanelEditTab : ITab if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, groupIdx)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); ImGui.SameLine(); var fileName = _filenames.OptionGroupFile(_mod, groupIdx); @@ -523,7 +519,7 @@ public class ModPanelEditTab : ITab ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._filenames, panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, From 37798d93ba8c8cc4dec32cba84a117700458f6ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 15:30:11 +0200 Subject: [PATCH 1018/2451] make testing tags testing_ instead of t. --- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9337defb..44f9fd2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Create Release on: push: tags-ignore: - - t* + - testing_* jobs: build: diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 3c0b5a63..80c0ce8f 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -3,7 +3,7 @@ name: Create Test Release on: push: tags: - - t* + - testing_* jobs: build: @@ -24,11 +24,11 @@ jobs: Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | - $ver = '${{ github.ref_name }}' -replace 't' + $ver = '${{ github.ref_name }}' -replace 'testing_' 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_name }}' -replace 't' + $ver = '${{ github.ref_name }}' -replace 'testing_' $path = './Penumbra/bin/Debug/Penumbra.json' $json = Get-Content -Raw $path | ConvertFrom-Json $json.AssemblyVersion = $ver @@ -65,7 +65,7 @@ jobs: - name: Write out repo.json run: | $verT = '${{ github.ref_name }}' - $ver = $verT -replace 't' + $ver = $verT -replace 'testing_' $path = './repo.json' $json = Get-Content -Raw $path | ConvertFrom-Json $json[0].TestingAssemblyVersion = $ver From 3b68eca212faff00def1d2f4814feafcfa859781 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 12 Jun 2023 13:32:38 +0000 Subject: [PATCH 1019/2451] [CI] Updating repo.json for testing_0.7.1.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 776bb618..f7db2722 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.8", + "TestingAssemblyVersion": "0.7.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/t0.7.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 03cb88be10dabb0fd0bac2d8304f341663d50169 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Jun 2023 16:17:29 +0200 Subject: [PATCH 1020/2451] Add right-click to select only this current filter to changed items. --- OtterGui | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 57f84013..029676a3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 57f84013b42be29e4a17e5dcf6b2f59c15c2586d +Subproject commit 029676a3e25112e5db6457c4d5cfe9e4d38511a6 diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 114b05e8..40c4e72a 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -169,12 +169,23 @@ public class ChangedItemDrawer : IDisposable var icon = _icons[type]; var flag = _config.ChangedItemFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); - if (ImGui.IsItemClicked()) + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { _config.ChangedItemFilter = flag ? _config.ChangedItemFilter & ~type : _config.ChangedItemFilter | type; _config.Save(); } + using var popup = ImRaii.ContextPopupItem(type.ToString()); + if (popup) + { + if (ImGui.MenuItem("Enable Only This")) + { + _config.ChangedItemFilter = type; + _config.Save(); + ImGui.CloseCurrentPopup(); + } + } + if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); From 636f14a06d45020ad6fa5305da9e989a16da5656 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 16:01:38 +0200 Subject: [PATCH 1021/2451] Change imc handling in caches slightly. --- Penumbra/Collections/Cache/CollectionCache.cs | 4 +- .../Cache/CollectionCacheManager.cs | 4 +- Penumbra/Collections/Cache/ImcCache.cs | 102 +++++++++--------- Penumbra/Collections/ModCollection.cs | 6 +- 4 files changed, 62 insertions(+), 54 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 6daa78f3..3477fdf0 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -165,7 +165,7 @@ public class CollectionCache : IDisposable var (paths, manipulations) = ModData.RemoveMod(mod); if (addMetaChanges) - ++_collection.ChangeCounter; + _collection.IncrementCounter(); foreach (var path in paths) { @@ -240,7 +240,7 @@ public class CollectionCache : IDisposable if (addMetaChanges) { - ++_collection.ChangeCounter; + _collection.IncrementCounter(); if (mod.TotalManipulations > 0) AddMetaFiles(false); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5ac1ab7f..f2223849 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -174,7 +174,7 @@ public class CollectionCacheManager : IDisposable cache.AddMetaFiles(true); - ++collection.ChangeCounter; + collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); } @@ -280,7 +280,7 @@ public class CollectionCacheManager : IDisposable private void IncrementCounters() { foreach (var collection in _storage.Where(c => c.HasCache)) - ++collection.ChangeCounter; + collection.IncrementCounter(); MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 896e3078..3d097b7b 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using OtterGui.Filesystem; +using System.Linq; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -11,113 +11,117 @@ namespace Penumbra.Collections.Cache; public readonly struct ImcCache : IDisposable { - private readonly Dictionary< Utf8GamePath, ImcFile > _imcFiles = new(); - private readonly List< ImcManipulation > _imcManipulations = new(); + private readonly Dictionary _imcFiles = new(); + private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new(); public ImcCache() - { } + { } public void SetFiles(ModCollection collection, bool fromFullCompute) { if (fromFullCompute) - { foreach (var path in _imcFiles.Keys) collection._cache!.ForceFileSync(path, CreateImcPath(collection, path)); - } else - { foreach (var path in _imcFiles.Keys) collection._cache!.ForceFile(path, CreateImcPath(collection, path)); - } } public void Reset(ModCollection collection) { - foreach( var (path, file) in _imcFiles ) + foreach (var (path, file) in _imcFiles) { - collection._cache!.RemovePath( path ); + collection._cache!.RemovePath(path); file.Reset(); } _imcManipulations.Clear(); } - public bool ApplyMod( MetaFileManager manager, ModCollection collection, ImcManipulation manip ) + public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) { - if( !manip.Validate() ) - { + if (!manip.Validate()) return false; - } - _imcManipulations.AddOrReplace( manip ); + var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); + if (idx < 0) + { + idx = _imcManipulations.Count; + _imcManipulations.Add((manip, null!)); + } + var path = manip.GamePath(); try { - if( !_imcFiles.TryGetValue( path, out var file ) ) - { - file = new ImcFile( manager, manip ); - } + if (!_imcFiles.TryGetValue(path, out var file)) + file = new ImcFile(manager, manip); - if( !manip.Apply( file ) ) - { + _imcManipulations[idx] = (manip, file); + if (!manip.Apply(file)) return false; - } - _imcFiles[ path ] = file; - var fullPath = CreateImcPath( collection, path ); - collection._cache!.ForceFile( path, fullPath ); + _imcFiles[path] = file; + var fullPath = CreateImcPath(collection, path); + collection._cache!.ForceFile(path, fullPath); return true; } - catch( ImcException e ) + catch (ImcException e) { - manager.ValidityChecker.ImcExceptions.Add( e ); - Penumbra.Log.Error( e.ToString() ); + manager.ValidityChecker.ImcExceptions.Add(e); + Penumbra.Log.Error(e.ToString()); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not apply IMC Manipulation {manip}:\n{e}" ); + Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}"); } return false; } - public bool RevertMod( MetaFileManager manager, ModCollection collection, ImcManipulation m ) + public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) { - if( !m.Validate() || !_imcManipulations.Remove( m ) ) + if (!m.Validate()) + return false; + + var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); + if (idx < 0) + return false; + + var (_, file) = _imcManipulations[idx]; + _imcManipulations.RemoveAt(idx); + + if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file))) { - return false; - } - - var path = m.GamePath(); - if( !_imcFiles.TryGetValue( path, out var file ) ) - { - return false; - } - - var def = ImcFile.GetDefault( manager, path, m.EquipSlot, m.Variant, out _ ); - var manip = m.Copy( def ); - if( !manip.Apply( file ) ) + _imcFiles.Remove(file.Path); + collection._cache!.ForceFile(file.Path, FullPath.Empty); + file.Dispose(); + return true; + } + + var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant, out _); + var manip = m.Copy(def); + if (!manip.Apply(file)) return false; - var fullPath = CreateImcPath( collection, path ); - collection._cache!.ForceFile( path, fullPath ); + var fullPath = CreateImcPath(collection, file.Path); + collection._cache!.ForceFile(file.Path, fullPath); return true; } public void Dispose() { - foreach( var file in _imcFiles.Values ) + foreach (var file in _imcFiles.Values) file.Dispose(); _imcFiles.Clear(); _imcManipulations.Clear(); } - private static FullPath CreateImcPath( ModCollection collection, Utf8GamePath path ) + private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path) => new($"|{collection.Name}_{collection.ChangeCounter}|{path}"); public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); -} \ No newline at end of file +} diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index fc333747..ca20f371 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -47,7 +47,11 @@ public partial class ModCollection /// Count the number of changes of the effective file list. /// This is used for material and imc changes. /// - public int ChangeCounter { get; internal set; } + public int ChangeCounter { get; private set; } + + /// Increment the number of changes in the effective file list. + public int IncrementCounter() + => ++ChangeCounter; /// /// If a ModSetting is null, it can be inherited from other collections. From b3a1a979ebf99d505cdef65656003aa0434a9a1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 16:01:48 +0200 Subject: [PATCH 1022/2451] Update BNPC Data. --- Penumbra.GameData/Data/BNpcNames.cs | 795 +++++++++++++++++----------- 1 file changed, 499 insertions(+), 296 deletions(-) diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs index 6c044e25..f2ab962f 100644 --- a/Penumbra.GameData/Data/BNpcNames.cs +++ b/Penumbra.GameData/Data/BNpcNames.cs @@ -5,7 +5,10 @@ namespace Penumbra.GameData.Data; public static class NpcNames { - /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2023-01-17. + /// + /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2023-01-17 + /// and https://raw.githubusercontent.com/ffxiv-teamcraft/ffxiv-teamcraft/staging/libs/data/src/lib/json/gubal-bnpcs-index.json on 2023-06-13. + /// public static IReadOnlyList> CreateNames() => new IReadOnlyList[] { @@ -3154,7 +3157,7 @@ public static class NpcNames new uint[]{2627}, new uint[]{2610, 2613}, new uint[]{2611}, - Array.Empty(), + new uint[]{1478}, new uint[]{2611, 2614, 2615, 2616}, new uint[]{2612}, new uint[]{2628}, @@ -5436,7 +5439,7 @@ public static class NpcNames new uint[]{4816}, new uint[]{4708}, Array.Empty(), - Array.Empty(), + new uint[]{4820}, new uint[]{4773}, new uint[]{4819}, new uint[]{4703, 4705, 4706, 4707, 4708, 4764}, @@ -7135,7 +7138,7 @@ public static class NpcNames new uint[]{6204}, new uint[]{6205}, new uint[]{6205}, - Array.Empty(), + new uint[]{6206}, new uint[]{6206}, new uint[]{6207}, new uint[]{6170, 6171}, @@ -8535,7 +8538,7 @@ public static class NpcNames new uint[]{7122}, new uint[]{7123}, new uint[]{7120}, - Array.Empty(), + new uint[]{7073}, new uint[]{7127}, new uint[]{7131}, new uint[]{7131}, @@ -14615,13 +14618,13 @@ public static class NpcNames new uint[]{11281}, new uint[]{11282}, Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{108}, + new uint[]{11310}, + new uint[]{11309}, + new uint[]{9344}, + new uint[]{9344}, + new uint[]{9344}, + new uint[]{9344}, new uint[]{11284}, new uint[]{108}, new uint[]{11419}, @@ -14678,7 +14681,7 @@ public static class NpcNames new uint[]{11421}, new uint[]{11233}, new uint[]{297}, - Array.Empty(), + new uint[]{7636}, Array.Empty(), new uint[]{108}, new uint[]{108}, @@ -14945,11 +14948,11 @@ public static class NpcNames new uint[]{11460}, new uint[]{353}, new uint[]{12313}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12456}, + new uint[]{12457}, + new uint[]{12458}, + new uint[]{12459}, + new uint[]{12460}, Array.Empty(), Array.Empty(), new uint[]{3330}, @@ -15218,7 +15221,7 @@ public static class NpcNames new uint[]{11637}, new uint[]{11638}, new uint[]{11639}, - Array.Empty(), + new uint[]{11640}, new uint[]{11641}, new uint[]{11642}, new uint[]{11643}, @@ -15311,7 +15314,7 @@ public static class NpcNames new uint[]{11730}, new uint[]{11731}, new uint[]{11732}, - Array.Empty(), + new uint[]{11733}, new uint[]{11734}, new uint[]{11735}, new uint[]{11736}, @@ -15621,8 +15624,8 @@ public static class NpcNames new uint[]{11431}, new uint[]{11431}, new uint[]{11431}, - Array.Empty(), - Array.Empty(), + new uint[]{12100}, + new uint[]{12101}, new uint[]{11992}, new uint[]{108}, new uint[]{108}, @@ -15632,16 +15635,16 @@ public static class NpcNames new uint[]{108}, new uint[]{9363}, new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12045}, + new uint[]{12043}, + new uint[]{12046}, + new uint[]{12047}, + new uint[]{12048}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{12269}, + new uint[]{12052}, new uint[]{12250}, new uint[]{12251}, new uint[]{12252}, @@ -15649,21 +15652,21 @@ public static class NpcNames new uint[]{12254}, new uint[]{12255}, new uint[]{4954}, + new uint[]{12247}, + new uint[]{12248}, Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12246}, + new uint[]{12316}, + new uint[]{12263}, + new uint[]{12264}, new uint[]{12079}, new uint[]{12080}, new uint[]{108}, new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12102}, + new uint[]{12103}, + new uint[]{12104}, + new uint[]{12105}, new uint[]{11997}, new uint[]{11998}, new uint[]{11999}, @@ -15709,34 +15712,34 @@ public static class NpcNames new uint[]{12038}, new uint[]{12039}, new uint[]{12040}, - Array.Empty(), - Array.Empty(), + new uint[]{12240}, + new uint[]{12241}, new uint[]{12078}, + new uint[]{12097}, + new uint[]{12098}, + new uint[]{12099}, + new uint[]{12097}, + new uint[]{108}, + new uint[]{7695}, + new uint[]{7696}, + new uint[]{7697}, Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{7635}, + new uint[]{7635}, + new uint[]{7633}, + new uint[]{7634}, + new uint[]{7640}, + new uint[]{7636}, + new uint[]{7637}, + new uint[]{7638}, + new uint[]{12257}, + new uint[]{12257}, + new uint[]{12258}, + new uint[]{7639}, + new uint[]{7695}, + new uint[]{12256}, + new uint[]{12259}, + new uint[]{12260}, new uint[]{12063}, new uint[]{12066}, new uint[]{12067}, @@ -15779,15 +15782,15 @@ public static class NpcNames new uint[]{12058}, new uint[]{108}, Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12242}, + new uint[]{12243}, + new uint[]{12265}, + new uint[]{12266}, new uint[]{6091}, new uint[]{12278}, new uint[]{12279}, - Array.Empty(), - Array.Empty(), + new uint[]{12267}, + new uint[]{12268}, new uint[]{3822}, new uint[]{2143}, new uint[]{12292}, @@ -15815,223 +15818,223 @@ public static class NpcNames new uint[]{713}, new uint[]{713}, new uint[]{11262}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{11262}, + new uint[]{12236}, + new uint[]{12237}, + new uint[]{12238}, new uint[]{11418, 12053}, Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12106}, + new uint[]{12107}, + new uint[]{12108}, + new uint[]{12109}, + new uint[]{12110}, + new uint[]{12111}, + new uint[]{12112}, + new uint[]{12113}, + new uint[]{12114}, + new uint[]{12115}, + new uint[]{12116}, + new uint[]{12117}, + new uint[]{12118}, + new uint[]{12122}, + new uint[]{12121}, + new uint[]{12120}, + new uint[]{12119}, + new uint[]{12123}, + new uint[]{12124}, + new uint[]{12125}, + new uint[]{12126}, + new uint[]{12127}, + new uint[]{12128}, + new uint[]{12129}, + new uint[]{12130}, + new uint[]{12131}, + new uint[]{12132}, + new uint[]{12133}, + new uint[]{12134}, + new uint[]{12135}, + new uint[]{12136}, + new uint[]{12137}, + new uint[]{12138}, + new uint[]{12139}, + new uint[]{12140}, + new uint[]{12141}, + new uint[]{12142}, + new uint[]{12143}, + new uint[]{12144}, + new uint[]{12145}, + new uint[]{12146}, + new uint[]{12147}, + new uint[]{12148}, + new uint[]{12149}, + new uint[]{12150}, + new uint[]{12151}, + new uint[]{12152}, + new uint[]{12153}, + new uint[]{12154}, + new uint[]{12155}, + new uint[]{12156}, + new uint[]{12157}, + new uint[]{12158}, + new uint[]{12159}, + new uint[]{12160}, + new uint[]{12161}, + new uint[]{12162}, + new uint[]{12163}, + new uint[]{12164}, + new uint[]{12165}, + new uint[]{12166}, + new uint[]{12167}, + new uint[]{12168}, + new uint[]{12169}, + new uint[]{12170}, + new uint[]{12171}, + new uint[]{12172}, + new uint[]{12173}, + new uint[]{12174}, + new uint[]{12175}, + new uint[]{12176}, + new uint[]{12177}, + new uint[]{12178}, + new uint[]{12179}, + new uint[]{12180}, + new uint[]{12181}, + new uint[]{12182}, + new uint[]{12183}, new uint[]{5199}, new uint[]{5200}, new uint[]{5201}, new uint[]{5202}, new uint[]{5203}, new uint[]{5204}, - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12321}, + new uint[]{12320}, + new uint[]{12319}, new uint[]{12244}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12324}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12323}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12322}, + new uint[]{12184}, + new uint[]{12185}, + new uint[]{12186}, + new uint[]{12187}, + new uint[]{12188}, + new uint[]{12189}, + new uint[]{12190}, + new uint[]{12191}, + new uint[]{12192}, + new uint[]{12193}, + new uint[]{12194}, + new uint[]{12195}, + new uint[]{12196}, + new uint[]{12197}, + new uint[]{12198}, + new uint[]{12209}, + new uint[]{12199}, + new uint[]{12200}, + new uint[]{12208}, + new uint[]{12201}, + new uint[]{12202}, + new uint[]{12207}, + new uint[]{12203}, + new uint[]{12206}, + new uint[]{12204}, + new uint[]{12205}, + new uint[]{12212}, + new uint[]{12213}, + new uint[]{12210}, + new uint[]{12211}, + new uint[]{12214}, + new uint[]{12215}, + new uint[]{12216}, + new uint[]{12217}, + new uint[]{12218}, + new uint[]{12219}, + new uint[]{12220}, + new uint[]{12221}, + new uint[]{12222}, + new uint[]{12223}, + new uint[]{12224}, + new uint[]{12225}, + new uint[]{12229}, + new uint[]{12230}, + new uint[]{12226}, + new uint[]{12227}, + new uint[]{12232}, + new uint[]{12231}, + new uint[]{12233}, + new uint[]{12228}, + new uint[]{12234}, + new uint[]{12235}, new uint[]{108}, Array.Empty(), Array.Empty(), new uint[]{108}, new uint[]{11296}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{7142}, + new uint[]{12258}, + new uint[]{12261}, + new uint[]{12262}, new uint[]{11295}, new uint[]{5199}, new uint[]{5199, 5204}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{2566}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, + new uint[]{10309}, new uint[]{12031}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12270}, + new uint[]{12271}, + new uint[]{12272}, + new uint[]{12273}, + new uint[]{12274}, + new uint[]{12275}, new uint[]{108}, new uint[]{750}, new uint[]{750}, @@ -16051,22 +16054,22 @@ public static class NpcNames new uint[]{12311}, Array.Empty(), Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12334}, + new uint[]{12335}, + new uint[]{108}, Array.Empty(), Array.Empty(), new uint[]{12281}, new uint[]{12282}, - Array.Empty(), + new uint[]{12283}, new uint[]{12284}, - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12285}, + new uint[]{12286}, + new uint[]{12287}, new uint[]{12288}, + new uint[]{12289}, Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12291}, new uint[]{12298}, Array.Empty(), new uint[]{12300}, @@ -16079,23 +16082,23 @@ public static class NpcNames new uint[]{12307}, new uint[]{12314}, new uint[]{12315}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12338}, + new uint[]{12339}, + new uint[]{12340}, + new uint[]{12341}, + new uint[]{12342}, + new uint[]{12343}, + new uint[]{12344}, + new uint[]{12345}, + new uint[]{12346}, + new uint[]{12347}, + new uint[]{12348}, + new uint[]{12349}, + new uint[]{12350}, + new uint[]{12318}, new uint[]{11297}, - Array.Empty(), - Array.Empty(), + new uint[]{12369}, + new uint[]{12372}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16111,6 +16114,18 @@ public static class NpcNames Array.Empty(), new uint[]{12311}, new uint[]{12308}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{108}, + new uint[]{12337}, + new uint[]{12336}, + new uint[]{12388}, + new uint[]{12389}, + new uint[]{12390}, + new uint[]{12390}, + Array.Empty(), + new uint[]{12391}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16150,6 +16165,24 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12354}, + new uint[]{12355}, + new uint[]{12426}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12377}, + new uint[]{12378}, + new uint[]{12379}, + new uint[]{12380}, + new uint[]{12378}, + new uint[]{12377}, + new uint[]{12377}, + new uint[]{12377}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16166,6 +16199,176 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12392}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12365}, + new uint[]{12367}, + new uint[]{12368}, + new uint[]{12365}, + new uint[]{12367}, + new uint[]{12368}, + new uint[]{12366}, + new uint[]{108}, + Array.Empty(), + Array.Empty(), + new uint[]{12339}, + new uint[]{12339}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{5569}, + new uint[]{5570}, + new uint[]{5571}, + new uint[]{5572}, + Array.Empty(), + new uint[]{12463}, + new uint[]{5970}, + new uint[]{5239}, + new uint[]{4130}, + new uint[]{4392}, + Array.Empty(), + Array.Empty(), + new uint[]{4130}, + new uint[]{9350}, + new uint[]{12464}, + new uint[]{6148}, + new uint[]{12465}, + new uint[]{12466}, + new uint[]{12467}, + new uint[]{12468}, + new uint[]{12469}, + Array.Empty(), + new uint[]{5964}, + new uint[]{4133}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{108}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12437}, + Array.Empty(), + new uint[]{12439}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12445}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12455}, + new uint[]{12449}, + new uint[]{12450}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{12461}, + new uint[]{12462}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new uint[]{10902}, + new uint[]{10903}, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), From 323b4d6f21479ae64634a366bf6244124d209753 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 13 Jun 2023 14:03:57 +0000 Subject: [PATCH 1023/2451] [CI] Updating repo.json for testing_0.7.1.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f7db2722..58582f2b 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.9", + "TestingAssemblyVersion": "0.7.1.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 84364559363b7b8dbae0b1c701aaff1a40c35472 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 21:04:19 +0200 Subject: [PATCH 1024/2451] Make equipitems sharable again. --- .../Data/EquipmentIdentificationList.cs | 21 ++-- Penumbra.GameData/Data/GamePaths.cs | 2 - Penumbra.GameData/Data/ItemData.cs | 118 +++++++++++++++--- .../Data/WeaponIdentificationList.cs | 25 ++-- Penumbra.GameData/Enums/FullEquipType.cs | 3 + Penumbra.GameData/Structs/EquipItem.cs | 19 +-- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 4 +- Penumbra/Services/ServiceWrapper.cs | 4 +- Penumbra/UI/ChangedItemDrawer.cs | 2 +- 9 files changed, 146 insertions(+), 52 deletions(-) diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index e4ba59d1..d8b946a4 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -6,10 +6,11 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; -internal sealed class EquipmentIdentificationList : KeyList +internal sealed class EquipmentIdentificationList : KeyList { private const string Tag = "EquipmentIdentification"; @@ -20,11 +21,11 @@ internal sealed class EquipmentIdentificationList : KeyList public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) { if (slot == EquipSlot.Unknown) - return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)); + return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)).Select(e => (EquipItem)e); if (variant == 0) - return Between(ToKey(modelId, slot, 0), ToKey(modelId, slot, 0xFF)); + return Between(ToKey(modelId, slot, 0), ToKey(modelId, slot, 0xFF)).Select(e => (EquipItem)e); - return Between(ToKey(modelId, slot, variant), ToKey(modelId, slot, variant)); + return Between(ToKey(modelId, slot, variant), ToKey(modelId, slot, variant)).Select(e => (EquipItem)e); } public void Dispose(DalamudPluginInterface pi, ClientLanguage language) @@ -34,9 +35,9 @@ internal sealed class EquipmentIdentificationList : KeyList => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; public static ulong ToKey(EquipItem i) - => ToKey(i.ModelId, i.Slot, i.Variant); + => ToKey(i.ModelId, i.Type.ToSlot(), i.Variant); - protected override IEnumerable ToKeys(EquipItem i) + protected override IEnumerable ToKeys(PseudoEquipItem i) { yield return ToKey(i); } @@ -44,12 +45,12 @@ internal sealed class EquipmentIdentificationList : KeyList protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(EquipItem data) - => (int)data.Id; + protected override int ValueKeySelector(PseudoEquipItem data) + => (int)data.Item2; - private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) { var items = gameData.GetExcelSheet(language)!; - return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(EquipItem.FromArmor); + return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(i => (PseudoEquipItem)EquipItem.FromArmor(i)); } } diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs index ed6078c7..5df91600 100644 --- a/Penumbra.GameData/Data/GamePaths.cs +++ b/Penumbra.GameData/Data/GamePaths.cs @@ -1,5 +1,4 @@ using System.Text.RegularExpressions; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -18,7 +17,6 @@ public static partial class GamePaths : GenderRace.Unknown; } - public static partial class Monster { public static partial class Imc diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index 7cbd819c..cbde9ede 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -8,14 +8,17 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; public sealed class ItemData : DataSharer, IReadOnlyDictionary> { - private readonly IReadOnlyList> _items; + private readonly IReadOnlyDictionary _mainItems; + private readonly IReadOnlyDictionary _offItems; + private readonly IReadOnlyList> _byType; - private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) + private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) { var tmp = Enum.GetValues().Select(_ => new List(1024)).ToArray(); @@ -28,7 +31,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary[tmp.Length]; - ret[0] = Array.Empty(); + var ret = new IReadOnlyList[tmp.Length]; + ret[0] = Array.Empty(); for (var i = 1; i < tmp.Length; ++i) - ret[i] = tmp[i].OrderBy(item => item.Name).ToArray(); + ret[i] = tmp[i].OrderBy(item => item.Name).Select(s => (PseudoEquipItem)s).ToArray(); return ret; } + private static IReadOnlyDictionary CreateMainItems(IReadOnlyList> items) + { + var dict = new Dictionary(1024 * 4); + foreach (var type in Enum.GetValues().Where(v => !FullEquipTypeExtensions.OffhandTypes.Contains(v))) + { + var list = items[(int)type]; + foreach (var item in list) + dict.TryAdd(item.Item2, item); + } + + dict.TrimExcess(); + return dict; + } + + private static IReadOnlyDictionary CreateOffItems(IReadOnlyList> items) + { + var dict = new Dictionary(128); + foreach (var type in FullEquipTypeExtensions.OffhandTypes) + { + var list = items[(int)type]; + foreach (var item in list) + dict.TryAdd(item.Item2, item); + } + + dict.TrimExcess(); + return dict; + } + public ItemData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) : base(pluginInterface, language, 1) { - _items = TryCatchData("ItemList", () => CreateItems(dataManager, language)); + _byType = TryCatchData("ItemList", () => CreateItems(dataManager, language)); + _mainItems = TryCatchData("ItemDictMain", () => CreateMainItems(_byType)); + _offItems = TryCatchData("ItemDictOff", () => CreateOffItems(_byType)); } protected override void DisposeInternal() - => DisposeTag("ItemList"); + { + DisposeTag("ItemList"); + DisposeTag("ItemDictMain"); + DisposeTag("ItemDictOff"); + } public IEnumerator>> GetEnumerator() { - for (var i = 1; i < _items.Count; ++i) - yield return new KeyValuePair>((FullEquipType)i, _items[i]); + for (var i = 1; i < _byType.Count; ++i) + yield return new KeyValuePair>((FullEquipType)i, new EquipItemList(_byType[i])); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count - => _items.Count - 1; + => _byType.Count - 1; public bool ContainsKey(FullEquipType key) - => (int)key < _items.Count && key != FullEquipType.Unknown; + => (int)key < _byType.Count && key != FullEquipType.Unknown; public bool TryGetValue(FullEquipType key, out IReadOnlyList value) { if (ContainsKey(key)) { - value = _items[(int)key]; + value = new EquipItemList(_byType[(int)key]); return true; } - value = _items[0]; + value = Array.Empty(); return false; } public IReadOnlyList this[FullEquipType key] => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); + public bool ContainsKey(uint key, bool main = true) + => main ? _mainItems.ContainsKey(key) : _offItems.ContainsKey(key); + + public bool TryGetValue(uint key, out EquipItem value) + { + if (_mainItems.TryGetValue(key, out var v)) + { + value = v; + return true; + } + + value = default; + return false; + } + + public IEnumerable<(uint, EquipItem)> AllItems(bool main) + => (main ? _mainItems : _offItems).Select(i => (i.Key, (EquipItem)i.Value)); + + public bool TryGetValue(uint key, bool main, out EquipItem value) + { + var dict = main ? _mainItems : _offItems; + if (dict.TryGetValue(key, out var v)) + { + value = v; + return true; + } + + value = default; + return false; + } + public IEnumerable Keys => Enum.GetValues().Skip(1); public IEnumerable> Values - => _items.Skip(1); + => _byType.Skip(1).Select(l => (IReadOnlyList)new EquipItemList(l)); + + private readonly struct EquipItemList : IReadOnlyList + { + private readonly IReadOnlyList _items; + + public EquipItemList(IReadOnlyList items) + => _items = items; + + public IEnumerator GetEnumerator() + => _items.Select(i => (EquipItem)i).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _items.Count; + + public EquipItem this[int index] + => _items[index]; + } } diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs index a5e4ddf8..8f7bb131 100644 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -6,10 +6,11 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; -internal sealed class WeaponIdentificationList : KeyList +internal sealed class WeaponIdentificationList : KeyList { private const string Tag = "WeaponIdentification"; private const int Version = 1; @@ -19,16 +20,16 @@ internal sealed class WeaponIdentificationList : KeyList { } public IEnumerable Between(SetId modelId) - => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); + => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e); public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) { if (type == 0) - return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); + return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e); if (variant == 0) - return Between(ToKey(modelId, type, 0), ToKey(modelId, type, 0xFF)); + return Between(ToKey(modelId, type, 0), ToKey(modelId, type, 0xFF)).Select(e => (EquipItem)e); - return Between(ToKey(modelId, type, variant), ToKey(modelId, type, variant)); + return Between(ToKey(modelId, type, variant), ToKey(modelId, type, variant)).Select(e => (EquipItem)e); } public void Dispose(DalamudPluginInterface pi, ClientLanguage language) @@ -40,7 +41,7 @@ internal sealed class WeaponIdentificationList : KeyList public static ulong ToKey(EquipItem i) => ToKey(i.ModelId, i.WeaponType, i.Variant); - protected override IEnumerable ToKeys(EquipItem data) + protected override IEnumerable ToKeys(PseudoEquipItem data) { yield return ToKey(data); } @@ -48,20 +49,20 @@ internal sealed class WeaponIdentificationList : KeyList protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(EquipItem data) - => (int)data.Id; + protected override int ValueKeySelector(PseudoEquipItem data) + => (int)data.Item2; - private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) => gameData.GetExcelSheet(language)!.SelectMany(ToEquipItems); - private static IEnumerable ToEquipItems(Item item) + private static IEnumerable ToEquipItems(Item item) { if ((EquipSlot)item.EquipSlotCategory.Row is not (EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) yield break; if (item.ModelMain != 0) - yield return EquipItem.FromMainhand(item); + yield return (PseudoEquipItem)EquipItem.FromMainhand(item); if (item.ModelSub != 0) - yield return EquipItem.FromOffhand(item); + yield return (PseudoEquipItem)EquipItem.FromOffhand(item); } } diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index a45d0800..6220b1b8 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -403,4 +403,7 @@ public static class FullEquipTypeExtensions public static readonly IReadOnlyList AccessoryTypes = Enum.GetValues().Where(v => v.IsAccessory()).ToArray(); + + public static readonly IReadOnlyList OffhandTypes + = Enum.GetValues().Where(v => v.OffhandTypeSuffix().Length > 0).ToArray(); } diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs index 78d73870..718ea2ad 100644 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -2,6 +2,7 @@ using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Structs; @@ -15,7 +16,6 @@ public readonly struct EquipItem public readonly WeaponType WeaponType; public readonly byte Variant; public readonly FullEquipType Type; - public readonly EquipSlot Slot; public bool Valid => Type != FullEquipType.Unknown; @@ -35,8 +35,7 @@ public readonly struct EquipItem public EquipItem() => Name = string.Empty; - public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type, - EquipSlot slot) + public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type) { Name = string.Intern(name); Id = id; @@ -45,20 +44,24 @@ public readonly struct EquipItem WeaponType = weaponType; Variant = variant; Type = type; - Slot = slot; } + public static implicit operator EquipItem(PseudoEquipItem it) + => new(it.Item1, it.Item2, it.Item3, it.Item4, it.Item5, it.Item6, (FullEquipType)it.Item7); + + public static explicit operator PseudoEquipItem(EquipItem it) + => (it.Name, it.Id, it.IconId, (ushort)it.ModelId, (ushort)it.WeaponType, it.Variant, (byte)it.Type); + public static EquipItem FromArmor(Item item) { var type = item.ToEquipType(); - var slot = type.ToSlot(); var name = item.Name.ToDalamudString().TextValue; var id = item.RowId; var icon = item.Icon; var model = (SetId)item.ModelMain; var weapon = (WeaponType)0; var variant = (byte)(item.ModelMain >> 16); - return new EquipItem(name, id, icon, model, weapon, variant, type, slot); + return new EquipItem(name, id, icon, model, weapon, variant, type); } public static EquipItem FromMainhand(Item item) @@ -70,7 +73,7 @@ public readonly struct EquipItem var model = (SetId)item.ModelMain; var weapon = (WeaponType)(item.ModelMain >> 16); var variant = (byte)(item.ModelMain >> 32); - return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.MainHand); + return new EquipItem(name, id, icon, model, weapon, variant, type); } public static EquipItem FromOffhand(Item item) @@ -82,6 +85,6 @@ public readonly struct EquipItem var model = (SetId)item.ModelSub; var weapon = (WeaponType)(item.ModelSub >> 16); var variant = (byte)(item.ModelSub >> 32); - return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.OffHand); + return new EquipItem(name, id, icon, model, weapon, variant, type); } } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 902f5e08..3d444e1f 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -242,10 +242,10 @@ public static class EquipmentSwap private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out byte variant) { - if (!i.Slot.IsEquipmentPiece()) + slot = i.Type.ToSlot(); + if (!slot.IsEquipmentPiece()) throw new ItemSwap.InvalidItemTypeException(); - slot = i.Slot; modelId = i.ModelId; variant = i.Variant; } diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 5a43cf9a..783acc49 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -5,7 +5,7 @@ using Penumbra.Util; namespace Penumbra.Services; -public abstract class SyncServiceWrapper +public abstract class SyncServiceWrapper : IDisposable { public string Name { get; } public T Service { get; } @@ -34,7 +34,7 @@ public abstract class SyncServiceWrapper } } -public abstract class AsyncServiceWrapper +public abstract class AsyncServiceWrapper : IDisposable { public string Name { get; } public T? Service { get; private set; } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 40c4e72a..11d044b9 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -219,7 +219,7 @@ public class ChangedItemDrawer : IDisposable switch (obj) { case EquipItem it: - iconType = it.Slot switch + iconType = it.Type.ToSlot() switch { EquipSlot.MainHand => ChangedItemIcon.Mainhand, EquipSlot.OffHand => ChangedItemIcon.Offhand, From d42a10568720dbf3bbb9582c3534b6aa4c65711f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Jun 2023 01:20:59 +0200 Subject: [PATCH 1025/2451] Add some quick convert buttons to texture editing. --- Penumbra/Import/Textures/CombinedTexture.cs | 4 ++- Penumbra/Import/Textures/Texture.cs | 31 +++++++++++++---- .../AdvancedWindow/ModEditWindow.Textures.cs | 34 +++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index fcfbc3ee..bf017048 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -4,7 +4,6 @@ using System.Numerics; using Dalamud.Interface; using Lumina.Data.Files; using OtterTex; -using Penumbra.Services; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; @@ -41,6 +40,9 @@ public partial class CombinedTexture : IDisposable public bool IsLoaded => _mode != Mode.Empty; + + public bool IsLeftCopy + => _mode == Mode.LeftCopy; public Exception? SaveException { get; private set; } = null; diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 77412e92..ef8e16fc 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -54,6 +54,22 @@ public sealed class Texture : IDisposable public bool IsLoaded => TextureWrap != null; + public DXGIFormat Format + => BaseImage switch + { + ScratchImage s => s.Meta.Format, + TexFile t => t.Header.Format.ToDXGI(), + _ => DXGIFormat.Unknown, + }; + + public int MipMaps + => BaseImage switch + { + ScratchImage s => s.Meta.MipLevels, + TexFile t => t.Header.MipLevels, + _ => 1, + }; + public void Draw(Vector2 size) { if (TextureWrap != null) @@ -151,6 +167,13 @@ public sealed class Texture : IDisposable } } + public void Reload(DalamudServices dalamud) + { + var path = Path; + Path = string.Empty; + Load(dalamud, path); + } + private bool LoadDds(DalamudServices dalamud) { Type = FileType.Dds; @@ -180,7 +203,7 @@ public sealed class Texture : IDisposable using var stream = OpenTexStream(dalamud.GameData); var scratch = TexFileParser.Parse(stream); BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(); CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height); return true; @@ -268,10 +291,6 @@ public sealed class Texture : IDisposable if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Reload the currently selected path.", false, true)) - { - var path = Path; - Path = string.Empty; - Load(dalamud, path); - } + Reload(dalamud); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 24ab9b90..f96ab4da 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -5,6 +5,7 @@ using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterTex; using Penumbra.Import.Textures; namespace Penumbra.UI.AdvancedWindow; @@ -133,6 +134,39 @@ public partial class ModEditWindow _forceTextureStartPath = false; } + if (_left.Type is Texture.FileType.Tex && _center.IsLeftCopy) + { + var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); + if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, + "This converts the texture to BC7 format in place. This is not revertible.", + _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) + { + _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + _left.Reload(_dalamud); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize, + "This converts the texture to BC3 format in place. This is not revertible.", + _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) + { + _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + _left.Reload(_dalamud); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize, + "This converts the texture to RGBA format in place. This is not revertible.", + _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) + { + _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + _left.Reload(_dalamud); + } + } + else + { + ImGui.NewLine(); + } ImGui.NewLine(); } From 208d8a11ff278147d9474e2b15e3061b58b0e80c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Jun 2023 01:21:40 +0200 Subject: [PATCH 1026/2451] Move SaveService to OtterGui. --- OtterGui | 2 +- Penumbra/Services/SaveService.cs | 108 +++---------------------------- 2 files changed, 9 insertions(+), 101 deletions(-) diff --git a/OtterGui b/OtterGui index 029676a3..ced7068b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 029676a3e25112e5db6457c4d5cfe9e4d38511a6 +Subproject commit ced7068b8bf9729018c9f03f9a8e9354b9e7ca3e diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 66695b40..ce0957f0 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -1,7 +1,4 @@ using System; -using System.IO; -using System.Text; -using Dalamud.Game; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Mods; @@ -11,103 +8,14 @@ namespace Penumbra.Services; /// /// Any file type that we want to save via SaveService. /// -public interface ISavable +public interface ISavable : ISavable +{ } + +public sealed class SaveService : SaveServiceBase { - /// The full file name of a given object. - public string ToFilename(FilenameService fileNames); - - /// Write the objects data to the given stream writer. - public void Save(StreamWriter writer); - - /// An arbitrary message printed to Debug before saving. - public string LogName(string fileName) - => fileName; - - public string TypeName - => GetType().Name; -} - -public class SaveService -{ -#if DEBUG - private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(2); -#else - private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(10); -#endif - - private readonly Logger _log; - private readonly FrameworkManager _framework; - - public readonly FilenameService FileNames; - public readonly Framework DalamudFramework; - - public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames, Framework dalamudFramework) - { - _log = log; - _framework = framework; - FileNames = fileNames; - DalamudFramework = dalamudFramework; - } - - /// Queue a save for the next framework tick. - public void QueueSave(ISavable value) - { - var file = value.ToFilename(FileNames); - _framework.RegisterOnTick($"{value.GetType().Name} ## {file}", () => { ImmediateSave(value); }); - } - - /// Queue a delayed save with the standard delay for after the delay is over. - public void DelaySave(ISavable value) - => DelaySave(value, StandardDelay); - - /// Queue a delayed save for after the delay is over. - public void DelaySave(ISavable value, TimeSpan delay) - { - var file = value.ToFilename(FileNames); - _framework.RegisterDelayed($"{value.GetType().Name} ## {file}", () => { ImmediateSave(value); }, delay); - } - - /// Immediately trigger a save. - public void ImmediateSave(ISavable value) - { - var name = value.ToFilename(FileNames); - try - { - if (name.Length == 0) - throw new Exception("Invalid object returned empty filename."); - - _log.Debug($"Saving {value.TypeName} {value.LogName(name)}..."); - var file = new FileInfo(name); - file.Directory?.Create(); - using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew); - using var w = new StreamWriter(s, Encoding.UTF8); - value.Save(w); - } - catch (Exception ex) - { - _log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}"); - } - } - - public void ImmediateDelete(ISavable value) - { - var name = value.ToFilename(FileNames); - try - { - if (name.Length == 0) - throw new Exception("Invalid object returned empty filename."); - - if (!File.Exists(name)) - return; - - _log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}..."); - File.Delete(name); - } - catch (Exception ex) - { - _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); - } - } + public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) + : base(log, framework, fileNames) + { } /// Immediately delete all existing option group files for a mod and save them anew. public void SaveAllOptionGroups(Mod mod) @@ -121,7 +29,7 @@ public class SaveService } catch (Exception e) { - Penumbra.Log.Error($"Could not delete outdated group file {file}:\n{e}"); + Log.Error($"Could not delete outdated group file {file}:\n{e}"); } } From 306c2ffd10cda6e62e0d5a7510ded45445b6b35d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Jun 2023 01:21:59 +0200 Subject: [PATCH 1027/2451] Some glamourer related changes. --- Penumbra.GameData/Data/StainData.cs | 4 ++- Penumbra.GameData/Enums/FullEquipType.cs | 33 ++++++++++++++------- Penumbra.GameData/Structs/CustomizeData.cs | 2 +- Penumbra/Configuration.cs | 11 ++++--- Penumbra/Services/ConfigMigrationService.cs | 3 +- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Penumbra.GameData/Data/StainData.cs b/Penumbra.GameData/Data/StainData.cs index f4c4080e..8ba69d89 100644 --- a/Penumbra.GameData/Data/StainData.cs +++ b/Penumbra.GameData/Data/StainData.cs @@ -34,7 +34,9 @@ public sealed class StainData : DataSharer, IReadOnlyDictionary } public IEnumerator> GetEnumerator() - => Data.Select(kvp => new KeyValuePair(new StainId(kvp.Key), new Stain())).GetEnumerator(); + => Data.Select(kvp + => new KeyValuePair(new StainId(kvp.Key), new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss))) + .GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index 6220b1b8..d2054a7b 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -365,17 +365,28 @@ public static class FullEquipTypeExtensions public static FullEquipType Offhand(this FullEquipType type) => type switch { - FullEquipType.Fists => FullEquipType.FistsOff, - FullEquipType.Sword => FullEquipType.Shield, - FullEquipType.Wand => FullEquipType.Shield, - FullEquipType.Daggers => FullEquipType.DaggersOff, - FullEquipType.Gun => FullEquipType.GunOff, - FullEquipType.Orrery => FullEquipType.OrreryOff, - FullEquipType.Rapier => FullEquipType.RapierOff, - FullEquipType.Glaives => FullEquipType.GlaivesOff, - FullEquipType.Bow => FullEquipType.BowOff, - FullEquipType.Katana => FullEquipType.KatanaOff, - _ => FullEquipType.Unknown, + FullEquipType.Fists => FullEquipType.FistsOff, + FullEquipType.Sword => FullEquipType.Shield, + FullEquipType.Wand => FullEquipType.Shield, + FullEquipType.Daggers => FullEquipType.DaggersOff, + FullEquipType.Gun => FullEquipType.GunOff, + FullEquipType.Orrery => FullEquipType.OrreryOff, + FullEquipType.Rapier => FullEquipType.RapierOff, + FullEquipType.Glaives => FullEquipType.GlaivesOff, + FullEquipType.Bow => FullEquipType.BowOff, + FullEquipType.Katana => FullEquipType.KatanaOff, + FullEquipType.Saw => FullEquipType.ClawHammer, + FullEquipType.CrossPeinHammer => FullEquipType.File, + FullEquipType.RaisingHammer => FullEquipType.Pliers, + FullEquipType.LapidaryHammer => FullEquipType.GrindingWheel, + FullEquipType.Knife => FullEquipType.Awl, + FullEquipType.Needle => FullEquipType.SpinningWheel, + FullEquipType.Alembic => FullEquipType.Mortar, + FullEquipType.Frypan => FullEquipType.CulinaryKnife, + FullEquipType.Pickaxe => FullEquipType.Sledgehammer, + FullEquipType.Hatchet => FullEquipType.GardenScythe, + FullEquipType.FishingRod => FullEquipType.Gig, + _ => FullEquipType.Unknown, }; internal static string OffhandTypeSuffix(this FullEquipType type) diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 37660c08..b7a92103 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -65,7 +65,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > } } - public string WriteBase64() + public readonly string WriteBase64() { fixed( byte* ptr = Data ) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 12c24033..7266892a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -110,14 +110,13 @@ public class Configuration : IPluginConfiguration, ISavable /// Load the current configuration. /// Includes adding new colors and migrating from old versions. /// - public Configuration(CharacterUtility utility, FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService) + public Configuration(CharacterUtility utility, ConfigMigrationService migrator, SaveService saveService) { _saveService = saveService; - Load(utility, fileNames, migrator); - UI.Classes.Colors.SetColors(this); + Load(utility, migrator); } - public void Load(CharacterUtility utility, FilenameService fileNames, ConfigMigrationService migrator) + public void Load(CharacterUtility utility, ConfigMigrationService migrator) { static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { @@ -126,10 +125,10 @@ public class Configuration : IPluginConfiguration, ISavable errorArgs.ErrorContext.Handled = true; } - if (File.Exists(fileNames.ConfigFile)) + if (File.Exists(_saveService.FileNames.ConfigFile)) try { - var text = File.ReadAllText(fileNames.ConfigFile); + var text = File.ReadAllText(_saveService.FileNames.ConfigFile); JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 5d19dbe5..b9e58deb 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -12,7 +12,6 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; -using Penumbra.Util; namespace Penumbra.Services; @@ -48,6 +47,8 @@ public class ConfigMigrationService if (save || forceSave) config.Save(); + + Colors.SetColors(config); } public void Migrate(CharacterUtility utility, Configuration config) From 3f1d84343aa24abcf39d6cd4dedb1a61885810ed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Jun 2023 01:24:49 +0200 Subject: [PATCH 1028/2451] Do not associate timeline resources with characters in cutscenes. --- .../PathResolving/AnimationHookService.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 19d1ede8..5b502d99 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Hooking; using Dalamud.Utility.Signatures; @@ -22,18 +23,20 @@ public unsafe class AnimationHookService : IDisposable private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly CollectionResolver _resolver; + private readonly Condition _conditions; private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); public AnimationHookService(PerformanceTracker performance, ObjectTable objects, CollectionResolver collectionResolver, - DrawObjectState drawObjectState, CollectionResolver resolver) + DrawObjectState drawObjectState, CollectionResolver resolver, Condition conditions) { _performance = performance; _objects = objects; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _resolver = resolver; + _conditions = conditions; SignatureHelper.Initialise(this); @@ -137,13 +140,17 @@ public unsafe class AnimationHookService : IDisposable private ulong LoadTimelineResourcesDetour(IntPtr timeline) { using var performance = _performance.Measure(PerformanceType.TimelineResources); - var last = _animationLoadData.Value; + // Do not check timeline loading in cutscenes. + if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene] || _conditions[ConditionFlag.WatchingCutscene78]) + return _loadTimelineResourcesHook.Original(timeline); + + var last = _animationLoadData.Value; _animationLoadData.Value = GetDataFromTimeline(timeline); var ret = _loadTimelineResourcesHook.Original(timeline); _animationLoadData.Value = last; return ret; } - + /// /// Probably used when the base idle animation gets loaded. /// Make it aware of the correct collection to load the correct pap files. @@ -287,17 +294,17 @@ public unsafe class AnimationHookService : IDisposable } /// Use timelines vfuncs to obtain the associated game object. - private ResolveData GetDataFromTimeline(IntPtr timeline) + private ResolveData GetDataFromTimeline(nint timeline) { try { if (timeline != IntPtr.Zero) - { - var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; + { + var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; var idx = getGameObjectIdx(timeline); if (idx >= 0 && idx < _objects.Length) { - var obj = (GameObject*)_objects.GetObjectAddress(idx); + var obj = (GameObject*)_objects.GetObjectAddress(idx); return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid; } } @@ -310,7 +317,6 @@ public unsafe class AnimationHookService : IDisposable return ResolveData.Invalid; } - private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); [Signature("48 89 5C 24 ?? 48 89 6C 24 ?? 89 54 24", DetourName = nameof(UnkMountAnimationDetour))] @@ -334,7 +340,7 @@ public unsafe class AnimationHookService : IDisposable { var last = _animationLoadData.Value; _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - _unkParasolAnimationHook!.Original(drawObject, unk1); + _unkParasolAnimationHook.Original(drawObject, unk1); _animationLoadData.Value = last; } From 5f916efb13b7170e6da0c04ec664120b8478b63a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Jun 2023 13:24:13 +0200 Subject: [PATCH 1029/2451] Rename ChatService and move some support buttons to OtterGui. --- OtterGui | 2 +- .../Manager/ActiveCollectionMigration.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 8 +-- .../Collections/Manager/CollectionStorage.cs | 18 +++---- .../Manager/IndividualCollections.Files.cs | 14 ++--- .../Collections/Manager/InheritanceManager.cs | 4 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 12 ++--- Penumbra/Mods/Manager/ModImportManager.cs | 2 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 2 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Mods/Subclasses/MultiModGroup.cs | 2 +- Penumbra/Penumbra.cs | 6 +-- Penumbra/Services/ValidityChecker.cs | 2 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../ModEditWindow.ShaderPackages.cs | 8 +-- Penumbra/UI/Classes/Colors.cs | 9 ++-- Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 5 +- Penumbra/UI/UiHelpers.cs | 51 ------------------- 22 files changed, 56 insertions(+), 104 deletions(-) diff --git a/OtterGui b/OtterGui index ced7068b..a79abe20 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ced7068b8bf9729018c9f03f9a8e9354b9e7ca3e +Subproject commit a79abe203da2673f71a4e31422c6058375fb8dec diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 7126d0e2..7ea52eb6 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -50,7 +50,7 @@ public static class ActiveCollectionMigration { if (!storage.ByName(collectionName, out var collection)) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", NotificationType.Warning); dict.Add(player, ModCollection.Empty); diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index be46f02b..69cd1239 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -335,7 +335,7 @@ public class ActiveCollections : ISavable, IDisposable ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); if (!_storage.ByName(defaultName, out var defaultCollection)) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", NotificationType.Warning); @@ -351,7 +351,7 @@ public class ActiveCollections : ISavable, IDisposable var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; if (!_storage.ByName(interfaceName, out var interfaceCollection)) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", NotificationType.Warning); Interface = ModCollection.Empty; @@ -366,7 +366,7 @@ public class ActiveCollections : ISavable, IDisposable var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; if (!_storage.ByName(currentName, out var currentCollection)) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", "Load Failure", NotificationType.Warning); Current = _storage.DefaultNamed; @@ -385,7 +385,7 @@ public class ActiveCollections : ISavable, IDisposable { if (!_storage.ByName(typeName, out var typeCollection)) { - Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + Penumbra.Chat.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning); configChanged = true; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 0a6b95a8..0bee38cf 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -107,7 +107,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (!CanAddCollection(name, out var fixedName)) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", "Warning", NotificationType.Warning); return false; @@ -118,7 +118,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); - Penumbra.ChatService.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", + Penumbra.Chat.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", NotificationType.Success); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); return true; @@ -131,13 +131,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) { - Penumbra.ChatService.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error); + Penumbra.Chat.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error); return false; } if (collection.Index == DefaultNamed.Index) { - Penumbra.ChatService.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error); + Penumbra.Chat.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error); return false; } @@ -147,7 +147,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable for (var i = collection.Index; i < Count; ++i) _collections[i].Index = i; - Penumbra.ChatService.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success); + Penumbra.Chat.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } @@ -190,14 +190,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!IsValidName(name)) { // TODO: handle better. - Penumbra.ChatService.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + Penumbra.Chat.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", "Warning", NotificationType.Warning); continue; } if (ByName(name, out _)) { - Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", + Penumbra.Chat.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", "Warning", NotificationType.Warning); continue; } @@ -205,7 +205,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", + Penumbra.Chat.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", NotificationType.Warning); _collections.Add(collection); } @@ -226,7 +226,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (AddCollection(ModCollection.DefaultCollectionName, null)) return _collections[^1]; - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", "Error", NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 0225927e..d670fc42 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -57,7 +57,7 @@ public partial class IndividualCollections if (group.Length == 0 || group.Any(i => !i.IsValid)) { changes = true; - Penumbra.ChatService.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure", + Penumbra.Chat.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning); continue; } @@ -66,7 +66,7 @@ public partial class IndividualCollections if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) { changes = true; - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", NotificationType.Warning); @@ -76,7 +76,7 @@ public partial class IndividualCollections if (!Add(group, collection)) { changes = true; - Penumbra.ChatService.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + Penumbra.Chat.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", "Load Failure", NotificationType.Warning); } @@ -84,7 +84,7 @@ public partial class IndividualCollections catch (Exception e) { changes = true; - Penumbra.ChatService.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", + Penumbra.Chat.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error); } } @@ -124,7 +124,7 @@ public partial class IndividualCollections if (Add($"{_actorService.AwaitedService.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", "Migration Failure", NotificationType.Error); } @@ -140,13 +140,13 @@ public partial class IndividualCollections }, collection)) Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); else - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", "Migration Failure", NotificationType.Error); } else { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", "Migration Failure", NotificationType.Error); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index ca17a87d..fef7bdbc 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -146,12 +146,12 @@ public class InheritanceManager : IDisposable continue; changes = true; - Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", + Penumbra.Chat.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", NotificationType.Warning); } else { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", NotificationType.Warning); changes = true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 1ed590cf..3b5babcc 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -84,7 +84,7 @@ public class ModMerger : IDisposable catch (Exception ex) { Error = ex; - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.:\n{ex}", "Failure", NotificationType.Error); FailureCleanup(); diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 0019b5f6..1726eab2 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -77,7 +77,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + Penumbra.Chat.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); } finally { @@ -90,7 +90,7 @@ public class ModNormalizer { if (Directory.Exists(_normalizationDirName)) { - Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n" + Penumbra.Chat.NotificationMessage("Could not normalize mod:\n" + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", NotificationType.Error); return false; @@ -98,7 +98,7 @@ public class ModNormalizer if (Directory.Exists(_oldDirName)) { - Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n" + Penumbra.Chat.NotificationMessage("Could not normalize mod:\n" + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", NotificationType.Error); return false; @@ -204,7 +204,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + Penumbra.Chat.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); } return false; @@ -232,7 +232,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", + Penumbra.Chat.NotificationMessage($"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error); } @@ -256,7 +256,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", + Penumbra.Chat.NotificationMessage($"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error); foreach (var dir in Mod.ModPath.EnumerateDirectories()) { diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 5a9fa319..bbe881b0 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -48,7 +48,7 @@ public class ModImportManager : IDisposable if (File.Exists(s)) return true; - Penumbra.ChatService.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", "Warning", + Penumbra.Chat.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", "Warning", NotificationType.Warning); return false; }).Select(s => new FileInfo(s)).ToArray(); diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index b4306877..ab6e839a 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -398,7 +398,7 @@ public class ModOptionEditor return true; if (message) - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Could not name option {newName} because option with same filename {path} already exists.", "Warning", NotificationType.Warning); diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 5d37e99e..527c658d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -51,7 +51,7 @@ public partial class ModCreator } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure", + Penumbra.Chat.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure", NotificationType.Error); return null; } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 0383f763..40f3d37e 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -59,7 +59,7 @@ public sealed class MultiModGroup : IModGroup { if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - Penumbra.ChatService.NotificationMessage( + Penumbra.Chat.NotificationMessage( $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", NotificationType.Warning); break; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ba64ef79..fbbaab51 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -31,7 +31,7 @@ public class Penumbra : IDalamudPlugin => "Penumbra"; public static readonly Logger Log = new(); - public static ChatService ChatService { get; private set; } = null!; + public static ChatService Chat { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -55,7 +55,7 @@ public class Penumbra : IDalamudPlugin var startTimer = new StartTracker(); using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); - ChatService = _services.GetRequiredService(); + Chat = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() @@ -115,7 +115,7 @@ public class Penumbra : IDalamudPlugin _communicatorService.ChangedItemClick.Subscribe((button, it) => { if (button == MouseButton.Left && it is Item item) - ChatService.LinkItem(item); + Chat.LinkItem(item); }, ChangedItemClick.Priority.Link); } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 44df07e3..3d09f097 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -38,7 +38,7 @@ public class ValidityChecker public void LogExceptions() { if( ImcExceptions.Count > 0 ) - Penumbra.ChatService.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); + Penumbra.Chat.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index e42f9724..0f163994 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -129,7 +129,7 @@ public class FileEditor where T : class, IWritable } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); + Penumbra.Chat.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); } }, _getInitialPath(), false); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 7ca83ec8..9331fb32 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -327,7 +327,7 @@ public class ItemSwapTab : IDisposable, ITab } catch (Exception e) { - Penumbra.ChatService.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); + Penumbra.Chat.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); try { if (optionCreated && _selectedGroup != null) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 2b607280..753ad8e9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -90,7 +90,7 @@ public partial class ModEditWindow LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - Penumbra.ChatService.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } Update(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index b6af9dd9..4e8a4f45 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -80,12 +80,12 @@ public partial class ModEditWindow } catch( Exception e ) { - Penumbra.ChatService.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } - Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", + Penumbra.Chat.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); }, null, false ); } @@ -110,7 +110,7 @@ public partial class ModEditWindow } catch( Exception e ) { - Penumbra.ChatService.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } @@ -122,7 +122,7 @@ public partial class ModEditWindow catch( Exception e ) { tab.Shpk.SetInvalid(); - Penumbra.ChatService.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 67dc17e2..42577034 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using OtterGui.Custom; namespace Penumbra.UI.Classes; @@ -31,13 +32,13 @@ public static class Colors public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; public const uint RedTableBgTint = 0x40000080; - public const uint DiscordColor = 0xFFDA8972; + public const uint DiscordColor = CustomGui.DiscordColor; public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF; - public const uint ReniColorButton = 0xFFCC648D; - public const uint ReniColorHovered = 0xFFB070B0; - public const uint ReniColorActive = 0xFF9070E0; + public const uint ReniColorButton = CustomGui.ReniColorButton; + public const uint ReniColorHovered = CustomGui.ReniColorHovered; + public const uint ReniColorActive = CustomGui.ReniColorActive ; public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 2ce27052..05c70ff8 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Custom; using OtterGui.Raii; using Penumbra.Services; using Penumbra.UI.Classes; @@ -143,7 +144,7 @@ public sealed class ConfigWindow : Window ImGui.NewLine(); ImGui.NewLine(); - UiHelpers.DrawDiscordButton(0); + CustomGui.DrawDiscordButton(Penumbra.ChatService, 0); ImGui.SameLine(); UiHelpers.DrawSupportButton(_penumbra!); ImGui.NewLine(); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 16443239..4f243026 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Components; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; @@ -790,10 +791,10 @@ public class SettingsTab : ITab UiHelpers.DrawSupportButton(_penumbra); ImGui.SetCursorPos(new Vector2(xPos, 0)); - UiHelpers.DrawDiscordButton(width); + CustomGui.DrawDiscordButton(Penumbra.Chat, width); ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); - UiHelpers.DrawGuideButton(width); + CustomGui.DrawGuideButton(Penumbra.Chat, width); ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index f46eac35..7d517afc 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -8,7 +8,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Interop.Structs; using Penumbra.String; -using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -46,29 +45,6 @@ public static class UiHelpers ImGui.SetTooltip("Click to copy to clipboard."); } - /// Draw a button to open the official discord server. - /// The desired width of the button. - public static void DrawDiscordButton(float width) - { - const string address = @"https://discord.gg/kVva7DHV4r"; - using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.DiscordColor); - if (ImGui.Button("Join Discord for Support", new Vector2(width, 0))) - try - { - var process = new ProcessStartInfo(address) - { - UseShellExecute = true, - }; - Process.Start(process); - } - catch - { - Penumbra.ChatService.NotificationMessage($"Unable to open Discord at {address}.", "Error", NotificationType.Error); - } - - ImGuiUtil.HoverTooltip($"Open {address}"); - } - /// The longest support button text. public const string SupportInfoButtonText = "Copy Support Info to Clipboard"; @@ -101,33 +77,6 @@ public static class UiHelpers }); } - /// Draw the button that opens the ReniGuide. - public static void DrawGuideButton(float width) - { - const string address = @"https://reniguide.info/"; - using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.ReniColorButton) - .Push(ImGuiCol.ButtonHovered, Colors.ReniColorHovered) - .Push(ImGuiCol.ButtonActive, Colors.ReniColorActive); - if (ImGui.Button("Beginner's Guides", new Vector2(width, 0))) - try - { - var process = new ProcessStartInfo(address) - { - UseShellExecute = true, - }; - Process.Start(process); - } - catch - { - Penumbra.ChatService.NotificationMessage($"Could not open guide at {address} in external browser.", "Error", - NotificationType.Error); - } - - ImGuiUtil.HoverTooltip( - $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" - + "Not directly affiliated and potentially, but not usually out of date."); - } - /// Draw default vertical space. public static void DefaultLineSpace() => ImGui.Dummy(DefaultSpace); From fbd8a12f3aa2dcdf427cb71ed9f0e7db1d463b7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Jun 2023 13:33:26 +0200 Subject: [PATCH 1030/2451] Add stuff that wasn't saved after rename for some reason... --- Penumbra/Configuration.cs | 2 +- Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/UiHelpers.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 7266892a..4fd3cc69 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -136,7 +136,7 @@ public class Configuration : IPluginConfiguration, ISavable } catch (Exception ex) { - Penumbra.ChatService.NotificationMessage(ex, + Penumbra.Chat.NotificationMessage(ex, "Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Penumbra directory.", "Error reading Configuration", "Error", NotificationType.Error); } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 05c70ff8..6259ffe2 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -144,7 +144,7 @@ public sealed class ConfigWindow : Window ImGui.NewLine(); ImGui.NewLine(); - CustomGui.DrawDiscordButton(Penumbra.ChatService, 0); + CustomGui.DrawDiscordButton(Penumbra.Chat, 0); ImGui.SameLine(); UiHelpers.DrawSupportButton(_penumbra!); ImGui.NewLine(); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0665c68a..842616bf 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -352,7 +352,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Penumbra.ChatService.NotificationMessage(e.Message, "Failure", NotificationType.Warning); + => Penumbra.Chat.NotificationMessage(e.Message, "Failure", NotificationType.Warning); #endregion diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 7d517afc..8ae27dd6 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -59,7 +59,7 @@ public static class UiHelpers var text = penumbra.GatherSupportInformation(); ImGui.SetClipboardText(text); - Penumbra.ChatService.NotificationMessage($"Copied Support Info to Clipboard.", "Success", NotificationType.Success); + Penumbra.Chat.NotificationMessage($"Copied Support Info to Clipboard.", "Success", NotificationType.Success); } /// Draw a button to open a specific directory in a file explorer. From 5805d5c798d17bd2f4a9772592dc1a610e577abe Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 18 Jun 2023 21:51:31 +0000 Subject: [PATCH 1031/2451] [CI] Updating repo.json for testing_0.7.1.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 58582f2b..bd7dd25a 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.10", + "TestingAssemblyVersion": "0.7.1.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 895e70555d7e8f95c687553f4592518f2fa3019b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Jun 2023 17:11:40 +0200 Subject: [PATCH 1032/2451] Fix parameters of EnableDraw --- Penumbra.GameData/Enums/FullEquipType.cs | 3 +++ Penumbra/Interop/PathResolving/DrawObjectState.cs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index d2054a7b..7db76496 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -403,6 +403,9 @@ public static class FullEquipTypeExtensions _ => string.Empty, }; + public static bool IsOffhandType(this FullEquipType type) + => type.OffhandTypeSuffix().Length > 0; + public static readonly IReadOnlyList WeaponTypes = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 8273aed3..88e600b8 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -132,15 +132,15 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary - private delegate void EnableDrawDelegate(nint gameObject, nint b, nint c, nint d); + private delegate void EnableDrawDelegate(nint gameObject); [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] private readonly Hook _enableDrawHook = null!; - private void EnableDrawDetour(nint gameObject, nint b, nint c, nint d) + private void EnableDrawDetour(nint gameObject) { _lastGameObject.Value!.Enqueue(gameObject); - _enableDrawHook.Original.Invoke(gameObject, b, c, d); + _enableDrawHook.Original.Invoke(gameObject); _lastGameObject.Value!.TryDequeue(out _); } } From 22cb33e49eb8f632fdec49f6177951e89b56beba Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 20 Jun 2023 19:03:48 +0200 Subject: [PATCH 1033/2451] Fix -- texture prefixing in ResourceTree --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 5b93a726..8184b31f 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -48,8 +48,9 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide prefixed[lastDirectorySeparator + 1] = (byte)'-'; prefixed[lastDirectorySeparator + 2] = (byte)'-'; gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + prefixed[^1] = 0; - if (!Utf8GamePath.FromSpan(prefixed, out var tmp)) + if (!Utf8GamePath.FromSpan(prefixed[..^1], out var tmp)) return null; gamePath = tmp.Path.Clone(); From f88b5761ba2a9bfa0b789aef4feb73cb717e1a63 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 21 Jun 2023 00:41:41 +0200 Subject: [PATCH 1034/2451] Remove null-terminator that was actually useless --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8184b31f..54940f3c 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -43,14 +43,13 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-') { - Span prefixed = stackalloc byte[gamePath.Length + 3]; + Span prefixed = stackalloc byte[gamePath.Length + 2]; gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); prefixed[lastDirectorySeparator + 1] = (byte)'-'; prefixed[lastDirectorySeparator + 2] = (byte)'-'; gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); - prefixed[^1] = 0; - if (!Utf8GamePath.FromSpan(prefixed[..^1], out var tmp)) + if (!Utf8GamePath.FromSpan(prefixed, out var tmp)) return null; gamePath = tmp.Path.Clone(); From 0690c0c53c0a401414f76dad527fc7768505c528 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 Jun 2023 18:02:55 +0200 Subject: [PATCH 1035/2451] Small Glamourer stuff. --- OtterGui | 2 +- Penumbra.GameData/Structs/CharacterArmor.cs | 8 ++++---- Penumbra.GameData/Structs/CharacterWeapon.cs | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index a79abe20..201dd38e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a79abe203da2673f71a4e31422c6058375fb8dec +Subproject commit 201dd38e88ebc1762070b8e691d2600375f5c8a4 diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs index 9027a5cf..7351bc48 100644 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ b/Penumbra.GameData/Structs/CharacterArmor.cs @@ -31,11 +31,11 @@ public struct CharacterArmor : IEquatable public readonly CharacterArmor With(StainId stain) => new(Set, Variant, stain); - public readonly CharacterWeapon ToWeapon() - => new(Set, 0, Variant, Stain); + public readonly CharacterWeapon ToWeapon(WeaponType type) + => new(Set, type, Variant, Stain); - public readonly CharacterWeapon ToWeapon(StainId stain) - => new(Set, 0, Variant, stain); + public readonly CharacterWeapon ToWeapon(WeaponType type, StainId stain) + => new(Set, type, Variant, stain); public override readonly string ToString() => $"{Set},{Variant},{Stain}"; diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs index a1f2dd11..c86dd467 100644 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ b/Penumbra.GameData/Structs/CharacterWeapon.cs @@ -40,6 +40,9 @@ public struct CharacterWeapon : IEquatable Stain = (StainId)(value >> 48); } + public readonly CharacterWeapon With(StainId stain) + => new(Set, Type, Variant, stain); + public readonly CharacterArmor ToArmor() => new(Set, (byte)Variant, Stain); From 0d343c3bab572e8fe86eb0c8119c872a2a0230ba Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Jun 2023 18:11:07 +0200 Subject: [PATCH 1036/2451] Fix crashes when drawing folders containing %. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 201dd38e..39e11eaf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 201dd38e88ebc1762070b8e691d2600375f5c8a4 +Subproject commit 39e11eafa4c019bdec6a937c1be80e3a53a5cb4a From 0ed94676ed7337f06c2103af33695f9a1034be5d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Jun 2023 01:10:45 +0200 Subject: [PATCH 1037/2451] Update BNPC names. --- Penumbra.GameData/Data/BNpcNames.cs | 91 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs index f2ab962f..c28396f1 100644 --- a/Penumbra.GameData/Data/BNpcNames.cs +++ b/Penumbra.GameData/Data/BNpcNames.cs @@ -5,10 +5,7 @@ namespace Penumbra.GameData.Data; public static class NpcNames { - /// - /// Generated from https://gubal.hasura.app/api/rest/bnpc on 2023-01-17 - /// and https://raw.githubusercontent.com/ffxiv-teamcraft/ffxiv-teamcraft/staging/libs/data/src/lib/json/gubal-bnpcs-index.json on 2023-06-13. - /// + /// Generated from https://api.ffxivteamcraft.com/gubal on 2023-06-23. public static IReadOnlyList> CreateNames() => new IReadOnlyList[] { @@ -16099,10 +16096,10 @@ public static class NpcNames new uint[]{11297}, new uint[]{12369}, new uint[]{12372}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12369}, + new uint[]{12370}, + new uint[]{12371}, + new uint[]{12372}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16126,14 +16123,14 @@ public static class NpcNames new uint[]{12390}, Array.Empty(), new uint[]{12391}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12388}, + new uint[]{12389}, + new uint[]{12389}, + new uint[]{12390}, + new uint[]{12390}, + new uint[]{12390}, + new uint[]{12390}, + new uint[]{12391}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16169,12 +16166,12 @@ public static class NpcNames new uint[]{12355}, new uint[]{12426}, new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12354}, + new uint[]{12355}, + new uint[]{12426}, + new uint[]{12427}, + new uint[]{108}, + new uint[]{12356}, new uint[]{12377}, new uint[]{12378}, new uint[]{12379}, @@ -16183,22 +16180,22 @@ public static class NpcNames new uint[]{12377}, new uint[]{12377}, new uint[]{12377}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12377}, + new uint[]{12378}, + new uint[]{12379}, + new uint[]{12380}, + new uint[]{12381}, + new uint[]{12377}, + new uint[]{12377}, + new uint[]{12377}, + new uint[]{12377}, + new uint[]{12377}, + new uint[]{12382}, + new uint[]{12383}, + new uint[]{12384}, + new uint[]{12385}, + new uint[]{12386}, + new uint[]{12387}, new uint[]{12392}, Array.Empty(), Array.Empty(), @@ -16255,8 +16252,8 @@ public static class NpcNames new uint[]{5239}, new uint[]{4130}, new uint[]{4392}, - Array.Empty(), - Array.Empty(), + new uint[]{4130}, + new uint[]{4392}, new uint[]{4130}, new uint[]{9350}, new uint[]{12464}, @@ -16266,7 +16263,7 @@ public static class NpcNames new uint[]{12467}, new uint[]{12468}, new uint[]{12469}, - Array.Empty(), + new uint[]{12470}, new uint[]{5964}, new uint[]{4133}, Array.Empty(), @@ -16313,6 +16310,7 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), + new uint[]{12382}, Array.Empty(), Array.Empty(), Array.Empty(), @@ -16330,18 +16328,17 @@ public static class NpcNames Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12436}, new uint[]{12437}, Array.Empty(), new uint[]{12439}, + new uint[]{12440}, + new uint[]{12441}, Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), + new uint[]{12443}, Array.Empty(), new uint[]{12445}, - Array.Empty(), + new uint[]{12446}, Array.Empty(), Array.Empty(), new uint[]{12455}, From 8ea6893fc3482e66b96bbbd197c20d000b50fdfd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jul 2023 16:39:34 +0200 Subject: [PATCH 1038/2451] Fix some ToDos, parallelization problems. --- OtterGui | 2 +- Penumbra.GameData/Actors/ActorIdentifier.cs | 4 +- Penumbra.GameData/Actors/ActorManager.Data.cs | 2 +- .../Actors/ActorManager.Identifiers.cs | 13 +++--- Penumbra.GameData/Data/ItemData.cs | 3 ++ .../Data/ObjectIdentification.cs | 2 +- Penumbra.GameData/Enums/FullEquipType.cs | 42 +++++++++---------- Penumbra.GameData/Structs/EquipItem.cs | 3 ++ .../PathResolving/CollectionResolver.cs | 4 +- .../Interop/PathResolving/PathResolver.cs | 6 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 2 +- Penumbra/Interop/Services/RedrawService.cs | 2 +- Penumbra/Mods/Manager/ModManager.cs | 2 +- .../ResourceWatcher/ResourceWatcherTable.cs | 2 +- 15 files changed, 50 insertions(+), 41 deletions(-) diff --git a/OtterGui b/OtterGui index 39e11eaf..adce3030 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 39e11eafa4c019bdec6a937c1be80e3a53a5cb4a +Subproject commit adce3030c9dc125f2ebbaefbef6c756977c047c3 diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index e0a653d8..00f54ea6 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -171,7 +171,7 @@ public static class ActorManagerExtensions { ObjectKind.MountType => manager.Data.Mounts, ObjectKind.Companion => manager.Data.Companions, - (ObjectKind)15 => manager.Data.Ornaments, // TODO: CS Update + ObjectKind.Ornament => manager.Data.Ornaments, ObjectKind.BattleNpc => manager.Data.BNpcs, ObjectKind.EventNpc => manager.Data.ENpcs, _ => new Dictionary(), @@ -190,7 +190,7 @@ public static class ActorManagerExtensions ObjectKind.EventNpc => "Event NPC", ObjectKind.MountType => "Mount", ObjectKind.Companion => "Companion", - (ObjectKind)15 => "Accessory", // TODO: CS update + ObjectKind.Ornament => "Accessory", _ => kind.ToString(), }; diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index e4e5ae57..b23fabf9 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -98,7 +98,7 @@ public sealed partial class ActorManager : IDisposable { ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), ObjectKind.Companion => Companions.TryGetValue(dataId, out name), - (ObjectKind)15 => Ornaments.TryGetValue(dataId, out name), // TODO: CS Update + ObjectKind.Ornament => Ornaments.TryGetValue(dataId, out name), ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), _ => false, diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 9f5f246f..2a71081d 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -179,9 +179,8 @@ public partial class ActorManager case "o": case "accessory": case "ornament": - // TODO: Objectkind ornament. return FindDataId(split[1], Data.Ornaments, out id) - ? ((ObjectKind)15, id) + ? (ObjectKind.Ornament, id) : throw new IdentifierParseError($"Could not identify an Accessory named {split[1]}."); case "e": case "enpc": @@ -334,7 +333,7 @@ public partial class ActorManager } case ObjectKind.MountType: case ObjectKind.Companion: - case (ObjectKind)15: // TODO: CS Update + case ObjectKind.Ornament: { owner = HandleCutscene( (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(actor->ObjectIndex - 1)); @@ -369,12 +368,12 @@ public partial class ActorManager /// Obtain the current companion ID for an object by its actor and owner. /// private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - Character* owner) // TODO: CS Update + Character* owner) { return (ObjectKind)actor->ObjectKind switch { ObjectKind.MountType => owner->Mount.MountId, - (ObjectKind)15 => owner->Ornament.OrnamentId, + ObjectKind.Ornament => owner->Ornament.OrnamentId, ObjectKind.Companion => actor->DataID, _ => actor->DataID, }; @@ -571,7 +570,7 @@ public partial class ActorManager { ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), ObjectKind.Companion => Data.Companions.ContainsKey(dataId), - (ObjectKind)15 => Data.Ornaments.ContainsKey(dataId), // TODO: CS Update + ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId), ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), _ => false, }; @@ -582,7 +581,7 @@ public partial class ActorManager { ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), ObjectKind.Companion => Data.Companions.ContainsKey(dataId), - (ObjectKind)15 => Data.Ornaments.ContainsKey(dataId), // TODO: CS Update + ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId), ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), ObjectKind.EventNpc => Data.ENpcs.ContainsKey(dataId), _ => false, diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index cbde9ede..edd5e3ba 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -138,6 +138,9 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary AllItems(bool main) => (main ? _mainItems : _offItems).Select(i => (i.Key, (EquipItem)i.Value)); + public int TotalItemCount(bool main) + => main ? _mainItems.Count : _offItems.Count; + public bool TryGetValue(uint key, bool main, out EquipItem value) { var dict = main ? _mainItems : _offItems; diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 8e789339..de71be3a 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -290,7 +290,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier var options = new ParallelOptions() { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), }; Parallel.ForEach(gameData.GetExcelSheet(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc => diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index 7db76496..6d5b015e 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -187,28 +187,28 @@ public static class FullEquipTypeExtensions FullEquipType.Scythe => "Scythe", FullEquipType.Nouliths => "Nouliths", FullEquipType.Shield => "Shield", - FullEquipType.Saw => "Saw (Carpenter)", - FullEquipType.CrossPeinHammer => "Hammer (Blacksmith)", - FullEquipType.RaisingHammer => "Hammer (Armorsmith)", - FullEquipType.LapidaryHammer => "Hammer (Goldsmith)", - FullEquipType.Knife => "Knife (Leatherworker)", - FullEquipType.Needle => "Needle (Weaver)", - FullEquipType.Alembic => "Alembic (Alchemist)", - FullEquipType.Frypan => "Frypan (Culinarian)", - FullEquipType.Pickaxe => "Pickaxe (Miner)", - FullEquipType.Hatchet => "Hatchet (Botanist)", + FullEquipType.Saw => "Saw", + FullEquipType.CrossPeinHammer => "Cross Pein Hammer", + FullEquipType.RaisingHammer => "Raising Hammer", + FullEquipType.LapidaryHammer => "Lapidary Hammer", + FullEquipType.Knife => "Round Knife", + FullEquipType.Needle => "Needle", + FullEquipType.Alembic => "Alembic", + FullEquipType.Frypan => "Frypan", + FullEquipType.Pickaxe => "Pickaxe", + FullEquipType.Hatchet => "Hatchet", FullEquipType.FishingRod => "Fishing Rod", - FullEquipType.ClawHammer => "Clawhammer (Carpenter)", - FullEquipType.File => "File (Blacksmith)", - FullEquipType.Pliers => "Pliers (Armorsmith)", - FullEquipType.GrindingWheel => "Grinding Wheel (Goldsmith)", - FullEquipType.Awl => "Awl (Leatherworker)", - FullEquipType.SpinningWheel => "Spinning Wheel (Weaver)", - FullEquipType.Mortar => "Mortar (Alchemist)", - FullEquipType.CulinaryKnife => "Knife (Culinarian)", - FullEquipType.Sledgehammer => "Sledgehammer (Miner)", - FullEquipType.GardenScythe => "Garden Scythe (Botanist)", - FullEquipType.Gig => "Gig (Fisher)", + FullEquipType.ClawHammer => "Clawhammer", + FullEquipType.File => "File", + FullEquipType.Pliers => "Pliers", + FullEquipType.GrindingWheel => "Grinding Wheel", + FullEquipType.Awl => "Awl", + FullEquipType.SpinningWheel => "Spinning Wheel", + FullEquipType.Mortar => "Mortar", + FullEquipType.CulinaryKnife => "Culinary Knife", + FullEquipType.Sledgehammer => "Sledgehammer", + FullEquipType.GardenScythe => "Garden Scythe", + FullEquipType.Gig => "Gig", _ => "Unknown", }; diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs index 718ea2ad..ccd9b691 100644 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -46,6 +46,9 @@ public readonly struct EquipItem Type = type; } + public string ModelString + => WeaponType == 0 ? $"{ModelId.Value}-{Variant}" : $"{ModelId.Value}-{WeaponType.Value}-{Variant}"; + public static implicit operator EquipItem(PseudoEquipItem it) => new(it.Item1, it.Item2, it.Item3, it.Item4, it.Item5, it.Item6, (FullEquipType)it.Item7); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 4e1f6963..c5b797fd 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -119,7 +119,7 @@ public unsafe class CollectionResolver /// Return whether the given character has a human model. public bool IsModelHuman(Character* character) - => character != null && IsModelHuman((uint)character->ModelCharaId); + => character != null && IsModelHuman((uint)character->CharacterData.ModelCharaId); /// /// Used if on the Login screen. Names are populated after actors are drawn, @@ -213,7 +213,7 @@ public unsafe class CollectionResolver // Only handle human models. var character = (Character*)actor; - if (!IsModelHuman((uint)character->ModelCharaId)) + if (!IsModelHuman((uint)character->CharacterData.ModelCharaId)) return null; if (character->DrawData.CustomizeData[0] == 0) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 6833e352..cc513a92 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -66,6 +66,11 @@ public class PathResolver : IDisposable ResourceCategory.Shader => Resolve(path, resourceType), ResourceCategory.Vfx => Resolve(path, resourceType), ResourceCategory.Sound => Resolve(path, resourceType), + // EXD Modding in general should probably be prohibited but is currently used for fan translations. + // We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code. + ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) + ? (null, ResolveData.Invalid) + : DefaultResolver(path), // None of these files are ever associated with specific characters, // always use the default resolver for now, // except that common/font is conceptually more UI. @@ -75,7 +80,6 @@ public class PathResolver : IDisposable ResourceCategory.BgCommon => DefaultResolver(path), ResourceCategory.Bg => DefaultResolver(path), ResourceCategory.Cut => DefaultResolver(path), - ResourceCategory.Exd => DefaultResolver(path), ResourceCategory.Music => DefaultResolver(path), _ => DefaultResolver(path), }; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 57fdf20e..76d0c3f2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -37,7 +37,7 @@ public class ResourceTree var model = (CharacterBase*)character->GameObject.GetDrawObject(); var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); - ModelId = character->ModelCharaId; + ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace) ((Human*)model)->RaceSexId : GenderRace.Unknown; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 161f896f..2408bf67 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -74,7 +74,7 @@ public class ResourceTreeFactory var (name, related) = GetCharacterName(character, cache); var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->ModelCharaId, withNames); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); tree.LoadResources(globalContext); return tree; } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index f78d6dca..825eda5c 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -209,7 +209,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (actor == null || _targets.Target != null) return; - _targets.SetTarget(actor); + _targets.Target = actor; _target = -1; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 38eff57b..ac973be4 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -318,7 +318,7 @@ public sealed class ModManager : ModStorage, IDisposable { var options = new ParallelOptions() { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), }; var queue = new ConcurrentQueue(); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index f3d66725..0b2f2ae5 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -15,7 +15,7 @@ namespace Penumbra.UI; internal sealed class ResourceWatcherTable : Table { - public ResourceWatcherTable(Configuration config, ICollection records) + public ResourceWatcherTable(Configuration config, IReadOnlyCollection records) : base("##records", records, new PathColumn { Label = "Path" }, From 00bc17c57a38c9153331b0cb85ec63962fc4fdb5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jul 2023 16:13:11 +0200 Subject: [PATCH 1039/2451] Move some stuff to shared things, improve some filesystem rename handling. --- OtterGui | 2 +- Penumbra.GameData/Data/HumanModelList.cs | 44 +++++++++++++++ .../Manager/IndividualCollections.Files.cs | 1 + .../Manager/IndividualCollections.cs | 1 + .../PathResolving/CollectionResolver.cs | 31 +++-------- .../ResourceTree/ResourceTreeFactory.cs | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 20 +++---- Penumbra/Services/ServiceManager.cs | 6 +-- .../CollectionTab/IndividualAssignmentUi.cs | 18 +------ Penumbra/UI/CollectionTab/NpcCombo.cs | 54 ------------------- Penumbra/UI/CollectionTab/WorldCombo.cs | 24 --------- 11 files changed, 67 insertions(+), 136 deletions(-) create mode 100644 Penumbra.GameData/Data/HumanModelList.cs delete mode 100644 Penumbra/UI/CollectionTab/NpcCombo.cs delete mode 100644 Penumbra/UI/CollectionTab/WorldCombo.cs diff --git a/OtterGui b/OtterGui index adce3030..d43be328 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit adce3030c9dc125f2ebbaefbef6c756977c047c3 +Subproject commit d43be3287a4782be091635e81ef2ec64849ba462 diff --git a/Penumbra.GameData/Data/HumanModelList.cs b/Penumbra.GameData/Data/HumanModelList.cs new file mode 100644 index 00000000..d4177e51 --- /dev/null +++ b/Penumbra.GameData/Data/HumanModelList.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.GameData.Data; + +public sealed class HumanModelList : DataSharer +{ + public const string Tag = "HumanModels"; + public const int CurrentVersion = 1; + + private readonly BitArray _humanModels; + + public HumanModelList(DalamudPluginInterface pluginInterface, DataManager gameData) + : base(pluginInterface, ClientLanguage.English, CurrentVersion) + { + _humanModels = TryCatchData(Tag, () => GetValidHumanModels(gameData)); + } + + public bool IsHuman(uint modelId) + => modelId < _humanModels.Count && _humanModels[(int)modelId]; + + protected override void DisposeInternal() + { + DisposeTag(Tag); + } + + /// + /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. + /// + private static BitArray GetValidHumanModels(DataManager gameData) + { + var sheet = gameData.GetExcelSheet()!; + var ret = new BitArray((int)sheet.RowCount, false); + foreach (var (_, idx) in sheet.Select((m, i) => (m, i)).Where(p => p.m.Type == (byte)CharacterBase.ModelType.Human)) + ret[idx] = true; + + return ret; + } +} diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index d670fc42..c719891e 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; +using OtterGui.Custom; using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index a3005f07..91ab49c3 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; +using OtterGui.Custom; using OtterGui.Filesystem; using Penumbra.GameData.Actors; using Penumbra.Services; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index c5b797fd..8220c629 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,15 +1,11 @@ using System; -using System.Collections; -using System.Linq; -using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Lumina.Excel.GeneratedSheets; -using OtterGui; using Penumbra.Collections; -using Penumbra.Collections.Manager; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Services; using Penumbra.Util; @@ -23,7 +19,7 @@ public unsafe class CollectionResolver { private readonly PerformanceTracker _performance; private readonly IdentifiedCollectionCache _cache; - private readonly BitArray _validHumanModels; + private readonly HumanModelList _humanModels; private readonly ClientState _clientState; private readonly GameGui _gameGui; @@ -36,8 +32,8 @@ public unsafe class CollectionResolver private readonly DrawObjectState _drawObjectState; public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, - DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, DrawObjectState drawObjectState) + ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, + TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels) { _performance = performance; _cache = cache; @@ -49,7 +45,7 @@ public unsafe class CollectionResolver _collectionManager = collectionManager; _tempCollections = tempCollections; _drawObjectState = drawObjectState; - _validHumanModels = GetValidHumanModels(gameData); + _humanModels = humanModels; } /// @@ -115,7 +111,7 @@ public unsafe class CollectionResolver /// Return whether the given ModelChara id refers to a human-type model. public bool IsModelHuman(uint modelCharaId) - => modelCharaId < _validHumanModels.Length && _validHumanModels[(int)modelCharaId]; + => _humanModels.IsHuman(modelCharaId); /// Return whether the given character has a human model. public bool IsModelHuman(Character* character) @@ -254,17 +250,4 @@ public unsafe class CollectionResolver return CheckYourself(id, owner) ?? CollectionByAttributes(owner, ref notYetReady); } - - /// - /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. - /// - private static BitArray GetValidHumanModels(DataManager gameData) - { - var sheet = gameData.GetExcelSheet()!; - var ret = new BitArray((int)sheet.RowCount, false); - foreach (var (_, idx) in sheet.WithIndex().Where(p => p.Value.Type == (byte)CharacterBase.ModelType.Human)) - ret[idx] = true; - - return ret; - } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 2408bf67..9189327c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -4,7 +4,7 @@ using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; +using OtterGui.Custom; using Penumbra.GameData.Actors; using Penumbra.Interop.PathResolving; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 79554797..8e6e729c 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -83,12 +83,12 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable // Update sort order when defaulted mod names change. private void OnDataChange(ModDataChangeType type, Mod mod, string? oldName) { - if (type.HasFlag(ModDataChangeType.Name) && oldName != null) - { - var old = oldName.FixName(); - if (Find(old, out var child) && child is not Folder) - Rename(child, mod.Name.Text); - } + if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf)) + return; + + var old = oldName.FixName(); + if (old == leaf.Name || leaf.Name.IsDuplicateName(out var baseName, out _) && baseName == old) + RenameWithDuplicates(leaf, mod.Name.Text); } // Update the filesystem if a mod has been added or removed. @@ -98,13 +98,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable switch (type) { case ModPathChangeType.Added: - var originalName = mod.Name.Text.FixName(); - var name = originalName; - var counter = 1; - while (Find(name, out _)) - name = $"{originalName} ({++counter})"; - - CreateLeaf(Root, name, mod); + CreateDuplicateLeaf(Root, mod.Name.Text, mod); break; case ModPathChangeType.Deleted: if (FindLeaf(mod, out var leaf)) diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 1ab1f313..782d40a0 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; @@ -18,7 +17,7 @@ using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; -using Penumbra.UI.Classes; +using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; @@ -70,7 +69,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index 81e0a862..376b7ad8 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Enums; using ImGuiNET; -using OtterGui.Raii; +using OtterGui.Custom; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -63,22 +63,8 @@ public class IndividualAssignmentUi : IDisposable public void DrawObjectKindCombo(float width) { - if (!_ready) - return; - - ImGui.SetNextItemWidth(width); - using var combo = ImRaii.Combo("##newKind", _newKind.ToName()); - if (!combo) - return; - - foreach (var kind in ObjectKinds) - { - if (!ImGui.Selectable(kind.ToName(), _newKind == kind)) - continue; - - _newKind = kind; + if (_ready && IndividualHelpers.DrawObjectKindCombo(width, _newKind, out _newKind, ObjectKinds)) UpdateIdentifiersInternal(); - } } public void DrawNewPlayerCollection(float width) diff --git a/Penumbra/UI/CollectionTab/NpcCombo.cs b/Penumbra/UI/CollectionTab/NpcCombo.cs deleted file mode 100644 index 7095a78b..00000000 --- a/Penumbra/UI/CollectionTab/NpcCombo.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Utility; -using ImGuiNET; -using OtterGui.Widgets; - -namespace Penumbra.UI.CollectionTab; - -public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)> -{ - private readonly string _label; - - public NpcCombo(string label, IReadOnlyDictionary names) - : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key, Comparer) - .ToList()) - => _label = label; - - protected override string ToString((string Name, uint[] Ids) obj) - => obj.Name; - - protected override bool DrawSelectable(int globalIdx, bool selected) - { - var (name, ids) = Items[globalIdx]; - var ret = ImGui.Selectable(name, selected); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', ids.Select(i => i.ToString()))); - - return ret; - } - - public bool Draw(float width) - => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); - - - /// Compare strings in a way that letters and numbers are sorted before any special symbols. - private class NameComparer : IComparer - { - public int Compare(string? x, string? y) - { - if (x.IsNullOrEmpty() || y.IsNullOrEmpty()) - return StringComparer.OrdinalIgnoreCase.Compare(x, y); - - return (char.IsAsciiLetterOrDigit(x[0]), char.IsAsciiLetterOrDigit(y[0])) switch - { - (true, false) => -1, - (false, true) => 1, - _ => StringComparer.OrdinalIgnoreCase.Compare(x, y), - }; - } - } - - private static readonly NameComparer Comparer = new(); -} diff --git a/Penumbra/UI/CollectionTab/WorldCombo.cs b/Penumbra/UI/CollectionTab/WorldCombo.cs deleted file mode 100644 index 5441dbaa..00000000 --- a/Penumbra/UI/CollectionTab/WorldCombo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ImGuiNET; -using OtterGui.Widgets; - -namespace Penumbra.UI.CollectionTab; - -public sealed class WorldCombo : FilterComboCache> -{ - private static readonly KeyValuePair AllWorldPair = new(ushort.MaxValue, "Any World"); - - public WorldCombo(IReadOnlyDictionary worlds) - : base(worlds.OrderBy(kvp => kvp.Value).Prepend(AllWorldPair)) - { - CurrentSelection = AllWorldPair; - CurrentSelectionIdx = 0; - } - - protected override string ToString(KeyValuePair obj) - => obj.Value; - - public bool Draw(float width) - => Draw("##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); -} From 823b195cb18959bcfcc4be3988452e69738db899 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jul 2023 16:14:53 +0200 Subject: [PATCH 1040/2451] Update ONE new BNPC Name. --- Penumbra.GameData/Data/BNpcNames.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs index c28396f1..b268e827 100644 --- a/Penumbra.GameData/Data/BNpcNames.cs +++ b/Penumbra.GameData/Data/BNpcNames.cs @@ -5,7 +5,7 @@ namespace Penumbra.GameData.Data; public static class NpcNames { - /// Generated from https://api.ffxivteamcraft.com/gubal on 2023-06-23. + /// Generated from https://api.ffxivteamcraft.com/gubal on 2023-07-05. public static IReadOnlyList> CreateNames() => new IReadOnlyList[] { @@ -16065,7 +16065,7 @@ public static class NpcNames new uint[]{12287}, new uint[]{12288}, new uint[]{12289}, - Array.Empty(), + new uint[]{12290}, new uint[]{12291}, new uint[]{12298}, Array.Empty(), From 81dae2293650dbab69b9e47221f956818ad658db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jul 2023 16:18:49 +0200 Subject: [PATCH 1041/2451] Fix some stupidly introduced ambiguities. --- .../Manager/IndividualCollections.Files.cs | 1 - .../Collections/Manager/IndividualCollections.cs | 15 ++++++++------- .../Interop/ResourceTree/ResourceTreeFactory.cs | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index c719891e..d670fc42 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -4,7 +4,6 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; -using OtterGui.Custom; using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 91ab49c3..8c58722d 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; -using OtterGui.Custom; using OtterGui.Filesystem; using Penumbra.GameData.Actors; using Penumbra.Services; @@ -13,15 +12,17 @@ namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections { - private readonly Configuration _config; - private readonly ActorService _actorService; - private readonly List<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> _assignments = new(); - private readonly Dictionary _individuals = new(); + public record struct IndividualAssignment(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection); + + private readonly Configuration _config; + private readonly ActorService _actorService; + private readonly Dictionary _individuals = new(); + private readonly List _assignments = new(); public event Action Loaded; public bool IsLoaded { get; private set; } - public IReadOnlyList<(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection)> Assignments + public IReadOnlyList Assignments => _assignments; public IndividualCollections(ActorService actorService, Configuration config, bool temporary) @@ -183,7 +184,7 @@ public sealed partial class IndividualCollections } } - _assignments.Add((displayName, identifiers, collection)); + _assignments.Add(new IndividualAssignment(displayName, identifiers, collection)); return true; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 9189327c..965769a7 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -4,7 +4,6 @@ using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using OtterGui.Custom; using Penumbra.GameData.Actors; using Penumbra.Interop.PathResolving; using Penumbra.Services; From 93e1b7acb93804ef5e0507cb875c5b30501fce53 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jul 2023 16:40:57 +0200 Subject: [PATCH 1042/2451] Add Changelog. --- Penumbra/UI/Changelog.cs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 3a51fb28..cecd1380 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using OtterGui.Widgets; namespace Penumbra.UI; @@ -39,9 +40,38 @@ public class PenumbraChangelog Add7_0_4(Changelog); Add7_1_0(Changelog); Add7_1_2(Changelog); + Add7_2_0(Changelog); } #region Changelogs + + private static void Add7_2_0(Changelog log) + => log.NextVersion("Version 0.7.2.0") + .RegisterEntry("Added Changed Item Categories and icons that can filter for specific types of Changed Items, in the Changed Items Tab as well as in the Changed Items panel for specific mods..") + .RegisterEntry("Icons at the top can be clicked to filter, as well as right-clicked to open a context menu with the option to inverse-filter for them", 1) + .RegisterEntry("There is also an ALL button that can be toggled.", 1) + .RegisterEntry("Modded files in the Font category now resolve from the Interface assignment instead of the base assignment, despite not technically being in the UI category.") + .RegisterEntry("Timeline files will no longer be associated with specific characters in cutscenes, since there is no way to correctly do this, and it could cause crashes if IVCS-requiring animations were used on characters without IVCS.") + .RegisterEntry("File deletion in the Advanced Editing Window now also checks for your configured deletion key combo.") + .RegisterEntry("The Texture tab in the Advanced Editing Window now has some quick convert buttons to just convert the selected texture to a different format in-place.") + .RegisterEntry("These buttons only appear if only one texture is selected on the left side, it is not otherwise manipulated, and the texture is a .tex file.", 1) + .RegisterEntry("The text part of the mod filter in the mod selector now also resets when right-clicking the drop-down arrow.") + .RegisterEntry("The Dissolve Folder option in the mod selector context menu has been moved to the bottom.") + .RegisterEntry("Somewhat improved IMC handling to prevent some issues.") + .RegisterEntry("Improved the handling of mod renames on mods with default-search names to correctly rename their search-name in (hopefully) all cases too.") + .RegisterEntry("A lot of backend improvements and changes related to the pending Glamourer rework.") + .RegisterEntry("Fixed an issue where the displayed active collection count in the support info was wrong.") + .RegisterEntry("Fixed an issue with created directories dealing badly with non-standard whitespace characters like half-width or non-breaking spaces.") + .RegisterEntry("Fixed an issue with unknown animation and vfx edits not being recognized correctly.") + .RegisterEntry("Fixed an issue where changing option descriptions to be empty was not working correctly.") + .RegisterEntry("Fixed an issue with texture names in the resource tree of the On-Screen views.") + .RegisterEntry("Fixed a bug where the game would crash when drawing folders in the mod selector that contained a '%' symbol.") + .RegisterEntry("Fixed an issue with parallel algorithms obtaining the wrong number of available cores.") + .RegisterEntry("Updated the available selection of Battle NPC names.") + .RegisterEntry("A typo in the 0.7.1.2 Changlog has been fixed.") + .RegisterEntry("Added the Sea of Stars as accepted repository. (0.7.1.4)") + .RegisterEntry("Fixed an issue with collections sometimes not loading correctly, and IMC files not applying correctly. (0.7.1.3)"); + private static void Add7_1_2(Changelog log) => log.NextVersion("Version 0.7.1.2") @@ -50,7 +80,7 @@ public class PenumbraChangelog .RegisterEntry("Fixed Penumbra failing to load if the main configuration file is corrupted.") .RegisterEntry("Some miscellaneous small bug fixes.") .RegisterEntry("Slight changes in behaviour for deduplicator/normalizer, mostly backend.") - .RegisterEntry("A typo in the 0.7.1.0 Changelog was been fixed.") + .RegisterEntry("A typo in the 0.7.1.0 Changelog has been fixed.") .RegisterEntry("Fixed left rings not being valid for IMC entries after validation. (7.1.1)") .RegisterEntry("Relaxed the scaling restrictions for RSP scaling values to go from 0.01 to 512.0 instead of the prior upper limit of 8.0, in interface as well as validation, to better support the fetish community. (7.1.1)"); From 869be0cb95af51f1cc2f7f17fbb3e2fc343b981d Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 5 Jul 2023 14:43:34 +0000 Subject: [PATCH 1043/2451] [CI] Updating repo.json for 0.7.2.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index bd7dd25a..d417e1eb 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.1.4", - "TestingAssemblyVersion": "0.7.1.11", + "AssemblyVersion": "0.7.2.0", + "TestingAssemblyVersion": "0.7.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.1.11/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.1.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a6b929c207aeb2adeaf6a5458d708d1ca51db28a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jul 2023 00:28:54 +0200 Subject: [PATCH 1044/2451] Remove GPose condition from timeline loading restrictions. --- .../Interop/PathResolving/AnimationHookService.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 5b502d99..130c0926 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -4,6 +4,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.GameData; @@ -59,13 +60,13 @@ public unsafe class AnimationHookService : IDisposable switch (type) { case ResourceType.Scd: - if (_characterSoundData.IsValueCreated && _characterSoundData.Value.Valid) + if (_characterSoundData is { IsValueCreated: true, Value.Valid: true }) { resolveData = _characterSoundData.Value; return true; } - if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid) + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) { resolveData = _animationLoadData.Value; return true; @@ -76,7 +77,7 @@ public unsafe class AnimationHookService : IDisposable case ResourceType.Pap: case ResourceType.Avfx: case ResourceType.Atex: - if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid) + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) { resolveData = _animationLoadData.Value; return true; @@ -124,7 +125,7 @@ public unsafe class AnimationHookService : IDisposable var last = _characterSoundData.Value; _characterSoundData.Value = _collectionResolver.IdentifyCollection((GameObject*)character, true); var ret = _loadCharacterSoundHook.Original(character, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - _characterSoundData.Value = last; + _characterSoundData.Value = last; return ret; } @@ -141,7 +142,7 @@ public unsafe class AnimationHookService : IDisposable { using var performance = _performance.Measure(PerformanceType.TimelineResources); // Do not check timeline loading in cutscenes. - if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene] || _conditions[ConditionFlag.WatchingCutscene78]) + if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) return _loadTimelineResourcesHook.Original(timeline); var last = _animationLoadData.Value; From e3a608fe0e487ef21df31fae3eff56817b14f529 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jul 2023 00:29:25 +0200 Subject: [PATCH 1045/2451] Fix DecalReverter using wrong variables. --- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 656b00b6..bac81c89 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -129,7 +129,7 @@ public unsafe class MetaState : IDisposable _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData)); + var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); From 6e7805d58f5e1a842311d7c6f051ddba0fd2bf14 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jul 2023 00:51:08 +0200 Subject: [PATCH 1046/2451] Fix handling of decals overall. --- Penumbra/Interop/PathResolving/MetaState.cs | 12 +++++----- .../Interop/ResourceLoading/ResourceLoader.cs | 23 ++++++++++++++++--- .../ResourceLoading/ResourceService.cs | 4 +++- Penumbra/Interop/Services/DecalReverter.cs | 8 +++---- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index bac81c89..4d95b94a 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,7 +49,7 @@ public unsafe class MetaState : IDisposable private readonly CommunicatorService _communicator; private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; - private readonly ResourceService _resources; + private readonly ResourceLoader _resources; private readonly GameEventManager _gameEventManager; private readonly CharacterUtility _characterUtility; @@ -57,7 +57,7 @@ public unsafe class MetaState : IDisposable private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceService resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config) + ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config) { _performance = performance; _communicator = communicator; @@ -129,7 +129,7 @@ public unsafe class MetaState : IDisposable _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, customize)); + var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(modelCharaId, customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); @@ -255,9 +255,9 @@ public unsafe class MetaState : IDisposable _inChangeCustomize = true; var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - using var decals = - new DecalReverter(_config, _characterUtility, _resources, resolveData.ModCollection, UsesDecal(0, data)); - var ret = _changeCustomize.Original(human, data, skipEquipment); + using var decals = new DecalReverter(_config, _characterUtility, _resources, resolveData, true); + using var decal2 = new DecalReverter(_config, _characterUtility, _resources, resolveData, false); + var ret = _changeCustomize.Original(human, data, skipEquipment); _inChangeCustomize = false; return ret; } diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 5d7ba16a..f96ad99b 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -16,6 +16,8 @@ public unsafe class ResourceLoader : IDisposable private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; + private ResolveData _resolvedData = ResolveData.Invalid; + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, CreateFileWHook _) { @@ -30,6 +32,15 @@ public unsafe class ResourceLoader : IDisposable _fileReadService.ReadSqPack += ReadSqPackDetour; } + /// Load a resource for a given path and a specific collection. + public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + { + _resolvedData = resolveData; + var ret = _resources.GetResource(category, type, path); + _resolvedData = ResolveData.Invalid; + return ret; + } + /// The function to use to resolve a given path. public Func ResolvePath = null!; @@ -66,7 +77,8 @@ public unsafe class ResourceLoader : IDisposable _fileReadService.ReadSqPack -= ReadSqPackDetour; } - private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, + private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (returnValue != null) @@ -75,7 +87,12 @@ public unsafe class ResourceLoader : IDisposable CompareHash(ComputeHash(path.Path, parameters), hash, path); // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : ResolvePath(path, category, type); + var (resolvedPath, data) = _incMode.Value + ? (null, ResolveData.Invalid) + : _resolvedData.Valid + ? (_resolvedData.ModCollection.ResolvePath(path), _resolvedData) + : ResolvePath(path, category, type); + if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); @@ -85,7 +102,7 @@ public unsafe class ResourceLoader : IDisposable _texMdlService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. - hash = ComputeHash(resolvedPath.Value.InternalName, parameters); + hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index bced539c..645596a3 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -2,6 +2,7 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -60,7 +61,8 @@ public unsafe class ResourceService : IDisposable /// Mainly used for SCD streaming, can be null. /// Whether to request the resource synchronously or asynchronously. /// The returned resource handle. If this is not null, calling original will be skipped. - public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index a0a8c84d..a8aa3fa2 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -19,7 +19,7 @@ public sealed unsafe class DecalReverter : IDisposable private readonly Structs.TextureResourceHandle* _decal; private readonly Structs.TextureResourceHandle* _transparent; - public DecalReverter(Configuration config, CharacterUtility utility, ResourceService resources, ModCollection? collection, bool doDecal) + public DecalReverter(Configuration config, CharacterUtility utility, ResourceLoader resources, ResolveData resolveData, bool doDecal) { _utility = utility; var ptr = _utility.Address; @@ -30,16 +30,14 @@ public sealed unsafe class DecalReverter : IDisposable if (doDecal) { - var decalPath = collection?.ResolvePath(DecalPath)?.InternalName ?? DecalPath.Path; - var decalHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, decalPath); + var decalHandle = resources.LoadResolvedResource(ResourceCategory.Chara, ResourceType.Tex, DecalPath.Path, resolveData); _decal = (Structs.TextureResourceHandle*)decalHandle; if (_decal != null) ptr->DecalTexResource = _decal; } else { - var transparentPath = collection?.ResolvePath(TransparentPath)?.InternalName ?? TransparentPath.Path; - var transparentHandle = resources.GetResource(ResourceCategory.Chara, ResourceType.Tex, transparentPath); + var transparentHandle = resources.LoadResolvedResource(ResourceCategory.Chara, ResourceType.Tex, TransparentPath.Path, resolveData); _transparent = (Structs.TextureResourceHandle*)transparentHandle; if (_transparent != null) ptr->TransparentTexResource = _transparent; From 0521cf0d18752fbe0f0adf520236e5e6b34a9504 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 5 Jul 2023 22:53:38 +0000 Subject: [PATCH 1047/2451] [CI] Updating repo.json for 0.7.2.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index d417e1eb..87900e5c 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.2.0", - "TestingAssemblyVersion": "0.7.2.0", + "AssemblyVersion": "0.7.2.1", + "TestingAssemblyVersion": "0.7.2.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 2bc7eb165ef531b82bd4909192a4f8bbd22905dc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jul 2023 00:45:56 +0200 Subject: [PATCH 1048/2451] Add toggle for the Changed Item category filter. --- Penumbra/Configuration.cs | 1 + Penumbra/Mods/Manager/ModDataEditor.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 3 +++ Penumbra/UI/Tabs/SettingsTab.cs | 19 +++++++++++++------ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 4fd3cc69..da2fd935 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -48,6 +48,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index b6a0036f..43175815 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -183,7 +183,7 @@ public class ModDataEditor } catch (Exception e) { - Penumbra.Log.Error($"Could not load mod meta:\n{e}"); + Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}"); return ModDataChangeType.Deletion; } } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 11d044b9..c6a94542 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -141,6 +141,9 @@ public class ChangedItemDrawer : IDisposable /// Draw a header line with the different icon types to filter them. public void DrawTypeFilter() { + if (_config.HideChangedItemFilters) + return; + using var _ = ImRaii.PushId("ChangedItemIconFilter"); var size = new Vector2(2 * ImGui.GetTextLineHeight()); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4f243026..84432ce6 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -7,7 +7,7 @@ using Dalamud.Interface.Components; using Dalamud.Utility; using ImGuiNET; using OtterGui; -using OtterGui.Custom; +using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; @@ -372,6 +372,13 @@ public class SettingsTab : ITab _config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v); Checkbox("Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", _config.HideRedrawBar, v => _config.HideRedrawBar = v); + Checkbox("Hide Changed Item Filters", "Hides the category filter line in the Changed Items tab and the Changed Items mod panel.", + _config.HideChangedItemFilters, v => + { + _config.HideChangedItemFilters = v; + if (v) + _config.ChangedItemFilter = ChangedItemDrawer.AllFlags; + }); DrawSingleSelectRadioMax(); DrawCollapsibleGroupMin(); } @@ -687,17 +694,17 @@ public class SettingsTab : ITab $"Reset minimum dimensions to ({Configuration.Constants.MinimumSizeX}, {Configuration.Constants.MinimumSizeY}).", x == Configuration.Constants.MinimumSizeX && y == Configuration.Constants.MinimumSizeY)) { - x = Configuration.Constants.MinimumSizeX; - y = Configuration.Constants.MinimumSizeY; - edited = true; + x = Configuration.Constants.MinimumSizeX; + y = Configuration.Constants.MinimumSizeY; + edited = true; } ImGuiUtil.LabeledHelpMarker("Minimum Window Dimensions", - "Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended."); + "Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended."); if (warning.Length > 0) ImGuiUtil.DrawTextButton(warning, UiHelpers.InputTextWidth, Colors.PressEnterWarningBg); - else + else ImGui.NewLine(); if (!edited) From 4626c2176f58481a303c9e54ec49e40d6a343e5a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jul 2023 00:46:22 +0200 Subject: [PATCH 1049/2451] Fix multiple options with same label. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f5fbaec3..b6051136 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -395,8 +395,9 @@ public partial class ModEditWindow : Window, IDisposable if (!combo) return; - foreach (var option in _mod!.AllSubMods) + foreach (var (option, idx) in _mod!.AllSubMods.WithIndex()) { + using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) _editor.LoadOption(option.GroupIdx, option.OptionIdx); } From 64668e0e03de1b992e9731e6f894b8d2c5fc27da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jul 2023 00:46:32 +0200 Subject: [PATCH 1050/2451] Improve save service. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index d43be328..e47c15c0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d43be3287a4782be091635e81ef2ec64849ba462 +Subproject commit e47c15c0496d1afad41cc7b2854dfc43d9b82eee From a7ace8a8c85930685ef8dbc581fb418d997a884c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jul 2023 00:51:57 +0200 Subject: [PATCH 1051/2451] Oops. --- Penumbra/Services/BackupService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index 23fb20cc..1acb5bdc 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -13,7 +13,7 @@ public class BackupService { using var t = timer.Measure(StartTimeType.Backup); var files = PenumbraFiles(fileNames); - Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); } // Collect all relevant files for penumbra configuration. From 9e0c38169fbc02baa8d5cb0dc58ff2ce8e31fa6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Jul 2023 02:45:40 +0200 Subject: [PATCH 1052/2451] Glamourer-related changes. --- OtterGui | 2 +- Penumbra.GameData/Actors/ActorIdentifier.cs | 15 ++-- .../Actors/ActorManager.Identifiers.cs | 19 ++++- .../Data/EquipmentIdentificationList.cs | 12 ++- Penumbra.GameData/Data/HumanModelList.cs | 5 +- Penumbra.GameData/Data/ItemData.cs | 6 +- .../Data/ObjectIdentification.cs | 62 +++++++------- .../Data/WeaponIdentificationList.cs | 2 +- Penumbra.GameData/Enums/EquipSlot.cs | 8 ++ Penumbra.GameData/GameData.cs | 10 ++- Penumbra.GameData/Structs/CustomizeData.cs | 83 +++++++++++-------- Penumbra.GameData/Structs/EquipItem.cs | 35 +++++++- .../Manager/IndividualCollections.cs | 4 +- Penumbra/Interop/PathResolving/MetaState.cs | 6 +- Penumbra/Interop/Services/GameEventManager.cs | 5 +- 15 files changed, 184 insertions(+), 90 deletions(-) diff --git a/OtterGui b/OtterGui index e47c15c0..e43a0f00 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e47c15c0496d1afad41cc7b2854dfc43d9b82eee +Subproject commit e43a0f00661f319be27f606b1aa841d3234dfb0f diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 00f54ea6..f097e578 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Enums; using Newtonsoft.Json.Linq; @@ -17,10 +17,10 @@ public readonly struct ActorIdentifier : IEquatable public enum RetainerType : ushort { - Both = 0, - Bell = 1, + Both = 0, + Bell = 1, Mannequin = 2, - } + } // @formatter:off [FieldOffset( 0 )] public readonly IdentifierType Type; // All @@ -75,7 +75,7 @@ public readonly struct ActorIdentifier : IEquatable var parts = name.Split(' ', 3); return string.Join(" ", parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); - } + } public override string ToString() => Manager?.ToString(this) @@ -95,6 +95,9 @@ public readonly struct ActorIdentifier : IEquatable _ => "Invalid", }; + public string ToName() + => Manager?.ToName(this) ?? "Unknown Object"; + public override int GetHashCode() => Type switch { @@ -217,6 +220,6 @@ public static class ActorManagerExtensions ScreenActor.FittingRoom => "Fitting Room Actor", ScreenActor.DyePreview => "Dye Preview Actor", ScreenActor.Portrait => "Portrait Actor", - _ => "Invalid", + _ => "Invalid", }; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 2a71081d..8584a32b 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -91,6 +91,23 @@ public partial class ActorManager }; } + /// + /// Use stored data to convert an ActorIdentifier to a name only. + /// + public string ToName(ActorIdentifier id) + { + return id.Type switch + { + IdentifierType.Player => id.PlayerName.ToString(), + IdentifierType.Retainer => id.PlayerName.ToString(), + IdentifierType.Owned => $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", + IdentifierType.Special => id.Special.ToName(), + IdentifierType.Npc => Data.ToName(id.Kind, id.DataId), + IdentifierType.UnkObject => id.PlayerName.IsEmpty ? id.PlayerName.ToString() : "Unknown Object", + _ => "Invalid", + }; + } + private unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* HandleCutscene( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* main) { @@ -382,7 +399,7 @@ public partial class ActorManager public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check, bool withoutIndex) => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, - check, withoutIndex); + check, withoutIndex); public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check, bool withoutIndex) => FromObject(actor, out _, allowPlayerNpc, check, withoutIndex); diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index d8b946a4..5ce5e521 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -6,7 +6,7 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; @@ -51,6 +51,14 @@ internal sealed class EquipmentIdentificationList : KeyList private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) { var items = gameData.GetExcelSheet(language)!; - return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(i => (PseudoEquipItem)EquipItem.FromArmor(i)); + return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()) + .Select(i => (PseudoEquipItem)EquipItem.FromArmor(i)) + .Concat(CustomList); } + + private static IEnumerable CustomList + => new[] + { + (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), + }; } diff --git a/Penumbra.GameData/Data/HumanModelList.cs b/Penumbra.GameData/Data/HumanModelList.cs index d4177e51..5ade3616 100644 --- a/Penumbra.GameData/Data/HumanModelList.cs +++ b/Penumbra.GameData/Data/HumanModelList.cs @@ -10,7 +10,7 @@ namespace Penumbra.GameData.Data; public sealed class HumanModelList : DataSharer { - public const string Tag = "HumanModels"; + public const string Tag = "HumanModels"; public const int CurrentVersion = 1; private readonly BitArray _humanModels; @@ -24,6 +24,9 @@ public sealed class HumanModelList : DataSharer public bool IsHuman(uint modelId) => modelId < _humanModels.Count && _humanModels[(int)modelId]; + public int Count + => _humanModels.Count; + protected override void DisposeInternal() { DisposeTag(Tag); diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index edd5e3ba..47f7b327 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -8,7 +8,7 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; @@ -54,7 +54,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary> BnpcNames; - public readonly IReadOnlyList> ModelCharaToObjects; - public readonly IReadOnlyDictionary> Actions; - private readonly ActorManager.ActorManagerData _actorData; + public IGamePathParser GamePathParser { get; } = new GamePathParser(); + public readonly IReadOnlyList> BnpcNames; + public readonly IReadOnlyList> ModelCharaToObjects; + public readonly IReadOnlyDictionary> Actions; + private readonly ActorManager.ActorManagerData _actorData; private readonly EquipmentIdentificationList _equipment; private readonly WeaponIdentificationList _weapons; @@ -49,10 +48,8 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier public void Identify(IDictionary set, string path) { if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)) - { if (IdentifyVfx(set, path)) return; - } var info = GamePathParser.GetFileInfo(path); IdentifyParsed(set, info); @@ -73,6 +70,15 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier _ => _equipment.Between(setId, slot, (byte)variant), }; + public IReadOnlyList GetBnpcNames(uint bNpcId) + => bNpcId >= BnpcNames.Count ? Array.Empty() : BnpcNames[(int)bNpcId]; + + public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId) + => modelId >= ModelCharaToObjects.Count ? Array.Empty<(string Name, ObjectKind Kind, uint Id)>() : ModelCharaToObjects[(int)modelId]; + + public int NumModelChara + => ModelCharaToObjects.Count; + protected override void DisposeInternal() { _actorData.Dispose(); @@ -125,14 +131,14 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant); foreach (var item in items) - set[item.Name.ToString()] = item; + set[item.Name] = item; } private void FindWeapon(IDictionary set, GameObjectInfo info) { var items = _weapons.Between(info.PrimaryId, info.SecondaryId, info.Variant); foreach (var item in items) - set[item.Name.ToString()] = item; + set[item.Name] = item; } private void FindModel(IDictionary set, GameObjectInfo info) @@ -142,10 +148,10 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return; var models = _modelIdentifierToModelChara.Between(type, info.PrimaryId, (byte)info.SecondaryId, info.Variant); - foreach (var model in models.Where(m => m.RowId < ModelCharaToObjects.Count)) + foreach (var model in models.Where(m => m.RowId != 0 && m.RowId < ModelCharaToObjects.Count)) { var objectList = ModelCharaToObjects[(int)model.RowId]; - foreach (var (name, kind) in objectList) + foreach (var (name, kind, _) in objectList) set[$"{name} ({kind.ToName()})"] = model; } } @@ -172,10 +178,10 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier case FileType.Shader: AddCounterString(set, FileType.Shader.ToString()); return; - } + } switch (info.ObjectType) - { + { case ObjectType.LoadingScreen: case ObjectType.Map: case ObjectType.Interface: @@ -246,46 +252,46 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return true; } - private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, + private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, DataManager gameData, ClientLanguage language) { var modelSheet = gameData.GetExcelSheet(language)!; - var ret = new List>((int)modelSheet.RowCount); - + var ret = new List>((int)modelSheet.RowCount); + for (var i = -1; i < modelSheet.Last().RowId; ++i) - ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind)>()); + ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind, uint Id)>()); - void AddChara(int modelChara, ObjectKind kind, uint dataId) + void AddChara(int modelChara, ObjectKind kind, uint dataId, uint displayId) { - if (modelChara == 0 || modelChara >= ret.Count) + if (modelChara >= ret.Count) return; if (actors.TryGetName(kind, dataId, out var name)) - ret[modelChara].Add((name, kind)); + ret[modelChara].Add((name, kind, displayId)); } var oTask = Task.Run(() => { foreach (var ornament in gameData.GetExcelSheet(language)!) - AddChara(ornament.Model, (ObjectKind)15, ornament.RowId); + AddChara(ornament.Model, ObjectKind.Ornament, ornament.RowId, ornament.RowId); }); var mTask = Task.Run(() => { foreach (var mount in gameData.GetExcelSheet(language)!) - AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId); + AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId, mount.RowId); }); var cTask = Task.Run(() => { foreach (var companion in gameData.GetExcelSheet(language)!) - AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId); + AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId, companion.RowId); }); var eTask = Task.Run(() => { foreach (var eNpc in gameData.GetExcelSheet(language)!) - AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId); + AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId, eNpc.RowId); }); var options = new ParallelOptions() @@ -296,14 +302,14 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier Parallel.ForEach(gameData.GetExcelSheet(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc => { foreach (var name in BnpcNames[(int)bNpc.RowId]) - AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name); + AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name, bNpc.RowId); }); Task.WaitAll(oTask, mTask, cTask, eTask); - return ret.Select(s => s.Count > 0 + return ret.Select(s => !s.IsEmpty ? s.ToArray() - : Array.Empty<(string Name, ObjectKind Kind)>()).ToArray(); + : Array.Empty<(string Name, ObjectKind Kind, uint Id)>()).ToArray(); } public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs index 8f7bb131..90ef46c7 100644 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -6,7 +6,7 @@ using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Data; diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index b23ae22d..27e5a92c 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -204,6 +204,14 @@ public static class EquipSlotExtensions public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues().Where(e => e.IsEquipment()).ToArray(); public static readonly EquipSlot[] AccessorySlots = Enum.GetValues().Where(e => e.IsAccessory()).ToArray(); public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat(AccessorySlots).ToArray(); + + public static readonly EquipSlot[] WeaponSlots = + { + EquipSlot.MainHand, + EquipSlot.OffHand, + }; + + public static readonly EquipSlot[] FullSlots = WeaponSlots.Concat(EqdpSlots).ToArray(); } public static partial class Names diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 02f7d33b..81657bff 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using Dalamud; using Dalamud.Data; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -63,6 +63,14 @@ public interface IObjectIdentifier : IDisposable /// public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); + + /// Obtain a list of BNPC Name Ids associated with a BNPC Id. + public IReadOnlyList GetBnpcNames(uint bNpcId); + + /// Obtain a list of Names and Object Kinds associated with a ModelChara ID. + public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId); + + public int NumModelChara { get; } } public interface IGamePathParser diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index b7a92103..c9946bb0 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -1,76 +1,94 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using Penumbra.String.Functions; namespace Penumbra.GameData.Structs; - + [StructLayout(LayoutKind.Sequential, Size = Size)] -public unsafe struct CustomizeData : IEquatable< CustomizeData > +public unsafe struct CustomizeData : IEquatable, IReadOnlyCollection { public const int Size = 26; public fixed byte Data[Size]; - public void Read( void* source ) + public int Count + => Size; + + public IEnumerator GetEnumerator() { - fixed( byte* ptr = Data ) + for (var i = 0; i < Size; ++i) + yield return At(i); + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + + private unsafe byte At(int index) + => Data[index]; + + public void Read(void* source) + { + fixed (byte* ptr = Data) { - MemoryUtility.MemCpyUnchecked( ptr, source, Size ); + MemoryUtility.MemCpyUnchecked(ptr, source, Size); } } - public readonly void Write( void* target ) + public readonly void Write(void* target) { - fixed( byte* ptr = Data ) + fixed (byte* ptr = Data) { - MemoryUtility.MemCpyUnchecked( target, ptr, Size ); + MemoryUtility.MemCpyUnchecked(target, ptr, Size); } } public readonly CustomizeData Clone() { var ret = new CustomizeData(); - Write( ret.Data ); + Write(ret.Data); return ret; } - public readonly bool Equals( CustomizeData other ) + public readonly bool Equals(CustomizeData other) { - fixed( byte* ptr = Data ) + fixed (byte* ptr = Data) { - return MemoryUtility.MemCmpUnchecked( ptr, other.Data, Size ) == 0; + return MemoryUtility.MemCmpUnchecked(ptr, other.Data, Size) == 0; } } - public override bool Equals( object? obj ) - => obj is CustomizeData other && Equals( other ); + public override bool Equals(object? obj) + => obj is CustomizeData other && Equals(other); public static bool Equals(CustomizeData* lhs, CustomizeData* rhs) - => MemoryUtility.MemCmpUnchecked(lhs, rhs, Size) == 0; - + => MemoryUtility.MemCmpUnchecked(lhs, rhs, Size) == 0; + /// Compare Gender and then only from Height onwards, because all screen actors are set to Height 50, /// the Race is implicitly included in the subrace (after height), - /// and the body type is irrelevant for players.> + /// and the body type is irrelevant for players.> public static bool ScreenActorEquals(CustomizeData* lhs, CustomizeData* rhs) - => lhs->Data[1] == rhs->Data[1] && MemoryUtility.MemCmpUnchecked(lhs->Data + 4, rhs->Data + 4, Size - 4) == 0; + => lhs->Data[1] == rhs->Data[1] && MemoryUtility.MemCmpUnchecked(lhs->Data + 4, rhs->Data + 4, Size - 4) == 0; public override int GetHashCode() { - fixed( byte* ptr = Data ) + fixed (byte* ptr = Data) { - var p = ( int* )ptr; - var u = *( ushort* )( p + 6 ); - return HashCode.Combine( *p, p[ 1 ], p[ 2 ], p[ 3 ], p[ 4 ], p[ 5 ], u ); + var p = (int*)ptr; + var u = *(ushort*)(p + 6); + return HashCode.Combine(*p, p[1], p[2], p[3], p[4], p[5], u); } } public readonly string WriteBase64() { - fixed( byte* ptr = Data ) + fixed (byte* ptr = Data) { - var data = new ReadOnlySpan< byte >( ptr, Size ); - return Convert.ToBase64String( data ); + var data = new ReadOnlySpan(ptr, Size); + return Convert.ToBase64String(data); } } @@ -78,23 +96,20 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > { var sb = new StringBuilder(Size * 3); for (var i = 0; i < Size - 1; ++i) - { sb.Append($"{Data[i]:X2} "); - } sb.Append($"{Data[Size - 1]:X2}"); return sb.ToString(); } - public bool LoadBase64( string base64 ) + + public bool LoadBase64(string base64) { var buffer = stackalloc byte[Size]; - var span = new Span< byte >( buffer, Size ); - if( !Convert.TryFromBase64String( base64, span, out var written ) || written != Size ) - { + var span = new Span(buffer, Size); + if (!Convert.TryFromBase64String(base64, span, out var written) || written != Size) return false; - } - Read( buffer ); + Read(buffer); return true; } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs index ccd9b691..8a7a7fae 100644 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -2,7 +2,7 @@ using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; -using PseudoEquipItem = System.ValueTuple; +using PseudoEquipItem = System.ValueTuple; namespace Penumbra.GameData.Structs; @@ -10,13 +10,16 @@ namespace Penumbra.GameData.Structs; public readonly struct EquipItem { public readonly string Name; - public readonly uint Id; + public readonly ulong Id; public readonly ushort IconId; public readonly SetId ModelId; public readonly WeaponType WeaponType; public readonly byte Variant; public readonly FullEquipType Type; + public uint ItemId + => (uint)Id; + public bool Valid => Type != FullEquipType.Unknown; @@ -35,7 +38,7 @@ public readonly struct EquipItem public EquipItem() => Name = string.Empty; - public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type) + public EquipItem(string name, ulong id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type) { Name = string.Intern(name); Id = id; @@ -53,7 +56,7 @@ public readonly struct EquipItem => new(it.Item1, it.Item2, it.Item3, it.Item4, it.Item5, it.Item6, (FullEquipType)it.Item7); public static explicit operator PseudoEquipItem(EquipItem it) - => (it.Name, it.Id, it.IconId, (ushort)it.ModelId, (ushort)it.WeaponType, it.Variant, (byte)it.Type); + => (it.Name, it.ItemId, it.IconId, (ushort)it.ModelId, (ushort)it.WeaponType, it.Variant, (byte)it.Type); public static EquipItem FromArmor(Item item) { @@ -90,4 +93,28 @@ public readonly struct EquipItem var variant = (byte)(item.ModelSub >> 32); return new EquipItem(name, id, icon, model, weapon, variant, type); } + + public static EquipItem FromIds(uint itemId, ushort iconId, SetId modelId, WeaponType type, byte variant, + FullEquipType equipType = FullEquipType.Unknown, string? name = null) + { + name ??= $"Unknown ({modelId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})"; + var fullId = itemId == 0 + ? modelId.Value | ((ulong)type.Value << 16) | ((ulong)variant << 32) | ((ulong)equipType << 40) | (1ul << 48) + : itemId; + return new EquipItem(name, fullId, iconId, modelId, type, variant, equipType); + } + + + public static EquipItem FromId(ulong id) + { + var setId = (SetId)id; + var type = (WeaponType)(id >> 16); + var variant = (byte)(id >> 32); + var equipType = (FullEquipType)(id >> 40); + return new EquipItem($"Unknown ({setId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})", id, 0, setId, type, + variant, equipType); + } + + public override string ToString() + => Name; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 8c58722d..065d6a79 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -96,7 +96,7 @@ public sealed partial class IndividualCollections identifiers = new[] { - manager.CreateRetainer(retainerName, 0), + manager.CreateRetainer(retainerName, ActorIdentifier.RetainerType.Both), }; break; case IdentifierType.Owned: @@ -131,7 +131,7 @@ public sealed partial class IndividualCollections ObjectKind.EventNpc => manager.Data.ENpcs, ObjectKind.Companion => manager.Data.Companions, ObjectKind.MountType => manager.Data.Mounts, - (ObjectKind)15 => manager.Data.Ornaments, + ObjectKind.Ornament => manager.Data.Ornaments, _ => throw new NotImplementedException(), }; return table.Where(kvp => kvp.Value == name) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 4d95b94a..2f6260a9 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -122,14 +122,14 @@ public unsafe class MetaState : IDisposable _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; } - private void OnCreatingCharacterBase(uint modelCharaId, nint customize, nint equipData) + private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData) { _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData); + _lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(modelCharaId, customize)); + var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index dc810f04..ea8d32d1 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -2,7 +2,6 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData; using System; -using System.Diagnostics; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Interop.Structs; @@ -154,7 +153,7 @@ public unsafe class GameEventManager : IDisposable { try { - ((CreatingCharacterBaseEvent)subscriber).Invoke(a, b, c); + ((CreatingCharacterBaseEvent)subscriber).Invoke((nint) (&a), b, c); } catch (Exception ex) { @@ -181,7 +180,7 @@ public unsafe class GameEventManager : IDisposable return ret; } - public delegate void CreatingCharacterBaseEvent(uint modelCharaId, nint customize, nint equipment); + public delegate void CreatingCharacterBaseEvent(nint modelCharaId, nint customize, nint equipment); public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject); #endregion From 0fb9e77c3cff40d55dcd6a5dc297a4194d1c1b46 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Jul 2023 13:11:18 +0200 Subject: [PATCH 1053/2451] Fix gloss and bug. --- OtterGui | 2 +- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs | 4 ++-- Penumbra/UI/ChangedItemDrawer.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index e43a0f00..f94bb054 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e43a0f00661f319be27f606b1aa841d3234dfb0f +Subproject commit f94bb0541e422a3e61a7117e8ad5bbba2f8ed6c0 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 26c7c2ae..cd599f11 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -97,7 +97,7 @@ public partial class ModEditWindow private bool DrawPreviewDye( MtrlFile file, bool disabled ) { - var (dyeId, (name, dyeColor, _)) = _stainService.StainCombo.CurrentSelection; + var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { @@ -115,7 +115,7 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true ); + _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true, gloss); return false; } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index c6a94542..41b5f420 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -280,7 +280,7 @@ public class ChangedItemDrawer : IDisposable private object? Convert(object? data) { if (data is EquipItem it) - return _items.GetRow(it.Id); + return _items.GetRow(it.ItemId); return data; } From 62f71df28cb57919ebe719b3a01f424423888139 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 13 Jul 2023 00:43:41 +0200 Subject: [PATCH 1054/2451] Some further small Glamourer changes, increment versioning of gamedata stuff. --- OtterGui | 2 +- Penumbra.GameData/Actors/ActorManager.Data.cs | 2 +- Penumbra.GameData/Data/HumanModelList.cs | 2 +- Penumbra.GameData/Data/ItemData.cs | 6 ++--- .../Data/ObjectIdentification.cs | 2 +- Penumbra.GameData/Data/RestrictedGear.cs | 2 +- Penumbra.GameData/Data/StainData.cs | 2 +- Penumbra.GameData/Enums/FullEquipType.cs | 27 +++++++++++++++++++ Penumbra.GameData/Structs/EquipItem.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 ++- 10 files changed, 39 insertions(+), 11 deletions(-) diff --git a/OtterGui b/OtterGui index f94bb054..b18fdb0d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f94bb0541e422a3e61a7117e8ad5bbba2f8ed6c0 +Subproject commit b18fdb0d39016e783923d5956ae6c07a89e53f30 diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index b23fabf9..c2a4a8cd 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -48,7 +48,7 @@ public sealed partial class ActorManager : IDisposable public IReadOnlyDictionary ENpcs { get; } public ActorManagerData(DalamudPluginInterface pluginInterface, DataManager gameData, ClientLanguage language) - : base(pluginInterface, language, 1) + : base(pluginInterface, language, 2) { var worldTask = TryCatchDataAsync("Worlds", CreateWorldData(gameData)); var mountsTask = TryCatchDataAsync("Mounts", CreateMountData(gameData)); diff --git a/Penumbra.GameData/Data/HumanModelList.cs b/Penumbra.GameData/Data/HumanModelList.cs index 5ade3616..719c7bbb 100644 --- a/Penumbra.GameData/Data/HumanModelList.cs +++ b/Penumbra.GameData/Data/HumanModelList.cs @@ -11,7 +11,7 @@ namespace Penumbra.GameData.Data; public sealed class HumanModelList : DataSharer { public const string Tag = "HumanModels"; - public const int CurrentVersion = 1; + public const int CurrentVersion = 2; private readonly BitArray _humanModels; diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index 47f7b327..cece8732 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -26,12 +26,12 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary i.Name.RawData.Length > 1)) { var type = item.ToEquipType(); - if (type.IsWeapon()) + if (type.IsWeapon() || type.IsTool()) { if (item.ModelMain != 0) tmp[(int)type].Add(EquipItem.FromMainhand(item)); if (item.ModelSub != 0) - tmp[(int)type.Offhand()].Add(EquipItem.FromOffhand(item)); + tmp[(int)type.ValidOffhand()].Add(EquipItem.FromOffhand(item)); } else if (type != FullEquipType.Unknown) { @@ -76,7 +76,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary CreateItems(dataManager, language)); _mainItems = TryCatchData("ItemDictMain", () => CreateMainItems(_byType)); diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 81968012..25b01f81 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -20,7 +20,7 @@ namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { - public const int IdentificationVersion = 1; + public const int IdentificationVersion = 2; public IGamePathParser GamePathParser { get; } = new GamePathParser(); public readonly IReadOnlyList> BnpcNames; diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs index 7bfb2360..5e1fa5a0 100644 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -30,7 +30,7 @@ public sealed class RestrictedGear : DataSharer public readonly IReadOnlyDictionary FemaleToMale; public RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) - : base(pi, language, 1) + : base(pi, language, 2) { _items = gameData.GetExcelSheet()!; _categories = gameData.GetExcelSheet()!; diff --git a/Penumbra.GameData/Data/StainData.cs b/Penumbra.GameData/Data/StainData.cs index 8ba69d89..0e602307 100644 --- a/Penumbra.GameData/Data/StainData.cs +++ b/Penumbra.GameData/Data/StainData.cs @@ -14,7 +14,7 @@ public sealed class StainData : DataSharer, IReadOnlyDictionary public readonly IReadOnlyDictionary Data; public StainData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) - : base(pluginInterface, language, 1) + : base(pluginInterface, language, 2) { Data = TryCatchData("Stains", () => CreateStainData(dataManager)); } diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index 6d5b015e..7d7ae512 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -123,6 +123,17 @@ public static class FullEquipTypeExtensions FullEquipType.Pickaxe => true, FullEquipType.Hatchet => true, FullEquipType.FishingRod => true, + FullEquipType.ClawHammer => true, + FullEquipType.File => true, + FullEquipType.Pliers => true, + FullEquipType.GrindingWheel => true, + FullEquipType.Awl => true, + FullEquipType.SpinningWheel => true, + FullEquipType.Mortar => true, + FullEquipType.CulinaryKnife => true, + FullEquipType.Sledgehammer => true, + FullEquipType.GardenScythe => true, + FullEquipType.Gig => true, _ => false, }; @@ -362,6 +373,22 @@ public static class FullEquipTypeExtensions _ => FullEquipType.Unknown, }; + public static FullEquipType ValidOffhand(this FullEquipType type) + => type switch + { + FullEquipType.Fists => FullEquipType.FistsOff, + FullEquipType.Sword => FullEquipType.Shield, + FullEquipType.Wand => FullEquipType.Shield, + FullEquipType.Daggers => FullEquipType.DaggersOff, + FullEquipType.Gun => FullEquipType.GunOff, + FullEquipType.Orrery => FullEquipType.OrreryOff, + FullEquipType.Rapier => FullEquipType.RapierOff, + FullEquipType.Glaives => FullEquipType.GlaivesOff, + FullEquipType.Bow => FullEquipType.BowOff, + FullEquipType.Katana => FullEquipType.KatanaOff, + _ => FullEquipType.Unknown, + }; + public static FullEquipType Offhand(this FullEquipType type) => type switch { diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs index 8a7a7fae..59ea94b4 100644 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -84,7 +84,7 @@ public readonly struct EquipItem public static EquipItem FromOffhand(Item item) { - var type = item.ToEquipType().Offhand(); + var type = item.ToEquipType().ValidOffhand(); var name = item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix(); var id = item.RowId; var icon = item.Icon; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 9331fb32..a705393d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -203,7 +203,8 @@ public class ItemSwapTab : IDisposable, ITab case SwapType.Bracelet: case SwapType.Ring: var values = _selectors[_lastTab]; - if (values.Source.CurrentSelection.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Type != FullEquipType.Unknown) + if (values.Source.CurrentSelection.Type != FullEquipType.Unknown + && values.Target.CurrentSelection.Type != FullEquipType.Unknown) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); From c3b106e359acb9f88e8c69bf9f7a93c9e9bdb972 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 13 Jul 2023 00:45:01 +0200 Subject: [PATCH 1055/2451] Remove outdated file. --- Penumbra.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra.sln b/Penumbra.sln index bccc56d8..5c11aaea 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,7 +8,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - base_repo.json = base_repo.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" From 65fc0292180c9f169039e9be9f450036837fb477 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 12 Jul 2023 23:07:33 +0000 Subject: [PATCH 1056/2451] [CI] Updating repo.json for 0.7.2.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 87900e5c..094e506e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.2.1", - "TestingAssemblyVersion": "0.7.2.1", + "AssemblyVersion": "0.7.2.2", + "TestingAssemblyVersion": "0.7.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 344defca8ed92e43f5b0b8ee981bec3b1c87d00f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jul 2023 14:59:52 +0200 Subject: [PATCH 1057/2451] Typo. --- OtterGui | 2 +- Penumbra.GameData/Data/RestrictedGear.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index b18fdb0d..0d512d15 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b18fdb0d39016e783923d5956ae6c07a89e53f30 +Subproject commit 0d512d15089298b93fa3f70e8eb8819955143600 diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs index 5e1fa5a0..74fcd975 100644 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ b/Penumbra.GameData/Data/RestrictedGear.cs @@ -387,7 +387,7 @@ public sealed class RestrictedGear : DataSharer AddItem(m2f, f2m, 22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress AddItem(m2f, f2m, 22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves AddItem(m2f, f2m, 22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches - AddItem(m2f, f2m, 22456, 20636); // Scuffed Manderville Gaiters <-> lackbosom Boots + AddItem(m2f, f2m, 22456, 20636); // Scuffed Manderville Gaiters <-> Blackbosom Boots AddItem(m2f, f2m, 23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket AddItem(m2f, f2m, 23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves AddItem(m2f, f2m, 23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes From 808dabf600958f3d56d9158d6d17b86134b63198 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Jul 2023 14:12:33 +0200 Subject: [PATCH 1058/2451] Add DragDropManager. --- Penumbra/Services/DalamudServices.cs | 3 ++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 36 +++++++++++++++++--- Penumbra/UI/Tabs/ModsTab.cs | 3 -- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index a75f3e4a..c2cf0c75 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -12,6 +12,7 @@ using Dalamud.IoC; using Dalamud.Plugin; using System.Linq; using System.Reflection; +using Dalamud.Interface.DragDrop; using Microsoft.Extensions.DependencyInjection; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local @@ -76,6 +77,7 @@ public class DalamudServices services.AddSingleton(SigScanner); services.AddSingleton(this); services.AddSingleton(UiBuilder); + services.AddSingleton(DragDropManager); } // TODO remove static @@ -93,6 +95,7 @@ public class DalamudServices [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; // @formatter:on public UiBuilder UiBuilder diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 842616bf..2b943f82 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; +using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using OtterGui; @@ -33,12 +35,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector m.Extensions.Any(e => ValidModExtensions.Contains(e.ToLowerInvariant())), m => + { + ImGui.TextUnformatted($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); + return true; + }); + base.Draw(width); + if (_dragDrop.CreateImGuiTarget("ModDragDrop", out var files, out _)) + _modImportManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f.ToLowerInvariant())))); + } + public override void Dispose() { base.Dispose(); @@ -655,7 +681,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Wed, 19 Jul 2023 00:37:39 +0200 Subject: [PATCH 1059/2451] Small Changes. --- OtterGui | 2 +- Penumbra.GameData/Structs/CustomizeData.cs | 5 ++++- Penumbra/CommandHandler.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 0d512d15..e3d26f16 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0d512d15089298b93fa3f70e8eb8819955143600 +Subproject commit e3d26f16234a4295bf3c7802d87ce43293c6ffe0 diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index c9946bb0..209574ec 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -27,9 +27,12 @@ public unsafe struct CustomizeData : IEquatable, IReadOnlyCollect => GetEnumerator(); - private unsafe byte At(int index) + public byte At(int index) => Data[index]; + public void Set(int index, byte value) + => Data[index] = value; + public void Read(void* source) { fixed (byte* ptr = Data) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 2e38448b..4ef04e77 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -92,7 +92,7 @@ public class CommandHandler : IDisposable private bool PrintHelp(string arguments) { - if (!string.Equals(arguments, "help", StringComparison.OrdinalIgnoreCase) && arguments == "?") + if (!string.Equals(arguments, "help", StringComparison.OrdinalIgnoreCase) && arguments != "?") _chat.Print(new SeStringBuilder().AddText("The given argument ").AddRed(arguments, true) .AddText(" is not valid. Valid arguments are:").BuiltString); else From 774f93f96203aa3787fe913e458a5f54ca1d1adf Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 18 Jul 2023 22:48:42 +0000 Subject: [PATCH 1060/2451] [CI] Updating repo.json for testing_0.7.2.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 094e506e..2631d285 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.2", + "TestingAssemblyVersion": "0.7.2.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4f2a14c9eed4640c9512fa5a73277c3110592a66 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 23 Jul 2023 16:13:21 +0200 Subject: [PATCH 1061/2451] Change some item data. --- .../Data/EquipmentIdentificationList.cs | 17 +++-- Penumbra.GameData/Data/ItemData.cs | 63 ++++++++++--------- .../Data/ObjectIdentification.cs | 8 +-- .../Data/WeaponIdentificationList.cs | 25 +++----- Penumbra.GameData/GameData.cs | 8 +-- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index 5ce5e521..25e2e095 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud; -using Dalamud.Data; using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using PseudoEquipItem = System.ValueTuple; @@ -14,8 +12,8 @@ internal sealed class EquipmentIdentificationList : KeyList { private const string Tag = "EquipmentIdentification"; - public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) - : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language)) + public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data) + : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(data)) { } public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) @@ -48,7 +46,7 @@ internal sealed class EquipmentIdentificationList : KeyList protected override int ValueKeySelector(PseudoEquipItem data) => (int)data.Item2; - private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateEquipmentList(ItemData data) { var items = gameData.GetExcelSheet(language)!; return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()) @@ -59,6 +57,13 @@ internal sealed class EquipmentIdentificationList : KeyList private static IEnumerable CustomList => new[] { - (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Head, "Cid's Bandana (9041)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Body, "Cid's Body (9041)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Head, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Body, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Hands, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Legs, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Feet, "Smallclothes (NPC, 9903)"), }; } diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index cece8732..c9f69694 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -16,6 +16,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary _mainItems; private readonly IReadOnlyDictionary _offItems; + private readonly IReadOnlyDictionary _gauntlets; private readonly IReadOnlyList> _byType; private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) @@ -28,10 +29,23 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary> 16), FullEquipType.Hands)); + tmp[(int)FullEquipType.FistsOff].Add(new EquipItem(mh.Name + FullEquipType.FistsOff.OffhandTypeSuffix(), mh.Id, + mh.IconId, (SetId)(mh.ModelId.Value + 50), mh.WeaponType, mh.Variant, FullEquipType.FistsOff)); + } + else + { + tmp[(int)type.ValidOffhand()].Add(EquipItem.FromOffhand(item)); + } + } } else if (type != FullEquipType.Unknown) { @@ -47,18 +61,26 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary CreateMainItems(IReadOnlyList> items) + private static Tuple, IReadOnlyDictionary> CreateMainItems( + IReadOnlyList> items) { var dict = new Dictionary(1024 * 4); + foreach (var fistWeapon in items[(int)FullEquipType.Fists]) + dict.TryAdd((uint)fistWeapon.Item2, fistWeapon); + + var gauntlets = items[(int)FullEquipType.Hands].Where(g => dict.ContainsKey((uint)g.Item2)).ToDictionary(g => (uint)g.Item2, g => g); + gauntlets.TrimExcess(); + foreach (var type in Enum.GetValues().Where(v => !FullEquipTypeExtensions.OffhandTypes.Contains(v))) { var list = items[(int)type]; foreach (var item in list) - dict.TryAdd((uint) item.Item2, item); + dict.TryAdd((uint)item.Item2, item); } dict.TrimExcess(); - return dict; + return new Tuple, + IReadOnlyDictionary>(dict, gauntlets); } private static IReadOnlyDictionary CreateOffItems(IReadOnlyList> items) @@ -68,7 +90,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary CreateItems(dataManager, language)); - _mainItems = TryCatchData("ItemDictMain", () => CreateMainItems(_byType)); - _offItems = TryCatchData("ItemDictOff", () => CreateOffItems(_byType)); + _byType = TryCatchData("ItemList", () => CreateItems(dataManager, language)); + (_mainItems, _gauntlets) = TryCatchData("ItemDictMain", () => CreateMainItems(_byType)); + _offItems = TryCatchData("ItemDictOff", () => CreateOffItems(_byType)); } protected override void DisposeInternal() @@ -120,31 +142,16 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary this[FullEquipType key] => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); - public bool ContainsKey(uint key, bool main = true) - => main ? _mainItems.ContainsKey(key) : _offItems.ContainsKey(key); - - public bool TryGetValue(uint key, out EquipItem value) - { - if (_mainItems.TryGetValue(key, out var v)) - { - value = v; - return true; - } - - value = default; - return false; - } - public IEnumerable<(uint, EquipItem)> AllItems(bool main) => (main ? _mainItems : _offItems).Select(i => (i.Key, (EquipItem)i.Value)); public int TotalItemCount(bool main) => main ? _mainItems.Count : _offItems.Count; - public bool TryGetValue(uint key, bool main, out EquipItem value) + public bool TryGetValue(uint key, EquipSlot slot, out EquipItem value) { - var dict = main ? _mainItems : _offItems; - if (dict.TryGetValue(key, out var v)) + var dict = slot is EquipSlot.OffHand ? _offItems : _mainItems; + if (slot is EquipSlot.Hands && _gauntlets.TryGetValue(key, out var v) || dict.TryGetValue(key, out v)) { value = v; return true; diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index 25b01f81..4aa766a1 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -20,7 +20,7 @@ namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { - public const int IdentificationVersion = 2; + public const int IdentificationVersion = 3; public IGamePathParser GamePathParser { get; } = new GamePathParser(); public readonly IReadOnlyList> BnpcNames; @@ -32,12 +32,12 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier private readonly WeaponIdentificationList _weapons; private readonly ModelIdentificationList _modelIdentifierToModelChara; - public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language) : base(pluginInterface, language, IdentificationVersion) { _actorData = new ActorManager.ActorManagerData(pluginInterface, dataManager, language); - _equipment = new EquipmentIdentificationList(pluginInterface, language, dataManager); - _weapons = new WeaponIdentificationList(pluginInterface, language, dataManager); + _equipment = new EquipmentIdentificationList(pluginInterface, language, itemData); + _weapons = new WeaponIdentificationList(pluginInterface, language, itemData); Actions = TryCatchData("Actions", () => CreateActionList(dataManager)); _modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager); diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs index 90ef46c7..e4566769 100644 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud; -using Dalamud.Data; using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using PseudoEquipItem = System.ValueTuple; @@ -13,10 +11,10 @@ namespace Penumbra.GameData.Data; internal sealed class WeaponIdentificationList : KeyList { private const string Tag = "WeaponIdentification"; - private const int Version = 1; + private const int Version = 2; - public WeaponIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) - : base(pi, Tag, language, Version, CreateWeaponList(gameData, language)) + public WeaponIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data) + : base(pi, Tag, language, Version, CreateWeaponList(data)) { } public IEnumerable Between(SetId modelId) @@ -52,17 +50,8 @@ internal sealed class WeaponIdentificationList : KeyList protected override int ValueKeySelector(PseudoEquipItem data) => (int)data.Item2; - private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) - => gameData.GetExcelSheet(language)!.SelectMany(ToEquipItems); - - private static IEnumerable ToEquipItems(Item item) - { - if ((EquipSlot)item.EquipSlotCategory.Row is not (EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) - yield break; - - if (item.ModelMain != 0) - yield return (PseudoEquipItem)EquipItem.FromMainhand(item); - if (item.ModelSub != 0) - yield return (PseudoEquipItem)EquipItem.FromOffhand(item); - } + private static IEnumerable CreateWeaponList(ItemData data) + => data.Where(kvp => !kvp.Key.IsEquipment() && !kvp.Key.IsAccessory()) + .SelectMany(kvp => kvp.Value) + .Select(i => (PseudoEquipItem)i); } diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 81657bff..5f8c2fe4 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -15,14 +15,14 @@ public static class GameData /// /// Obtain an object identifier that can link a game path to game objects that use it, using your client language. /// - public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager) - => new ObjectIdentification(pluginInterface, dataManager, dataManager.Language); + public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData) + => new ObjectIdentification(pluginInterface, dataManager, itemData, dataManager.Language); /// /// Obtain an object identifier that can link a game path to game objects that use it using the given language. /// - public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) - => new ObjectIdentification(pluginInterface, dataManager, language); + public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language) + => new ObjectIdentification(pluginInterface, dataManager, itemData, language); /// /// Obtain a parser for game paths. From 96e6ff0fbfb053204bdea5f6bdab341f864f7b8e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 23 Jul 2023 16:13:34 +0200 Subject: [PATCH 1062/2451] Fix potential crash in decref. --- .../Data/EquipmentIdentificationList.cs | 8 ++++---- Penumbra/Interop/ResourceLoading/ResourceLoader.cs | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index 25e2e095..ec2e914a 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -48,16 +48,16 @@ internal sealed class EquipmentIdentificationList : KeyList private static IEnumerable CreateEquipmentList(ItemData data) { - var items = gameData.GetExcelSheet(language)!; - return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()) - .Select(i => (PseudoEquipItem)EquipItem.FromArmor(i)) + return data.Where(kvp => kvp.Key.IsEquipment() || kvp.Key.IsAccessory()) + .SelectMany(kvp => kvp.Value) + .Select(i => (PseudoEquipItem)i) .Concat(CustomList); } private static IEnumerable CustomList => new[] { - (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Head, "Cid's Bandana (9041)"), (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Body, "Cid's Body (9041)"), (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Head, "Smallclothes (NPC, 9903)"), diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index f96ad99b..da50f26e 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -212,8 +212,16 @@ public unsafe class ResourceLoader : IDisposable if (handle->RefCount != 0) return; - Penumbra.Log.Error( - $"[ResourceLoader] Caught decrease of Reference Counter for {handle->FileName()} at 0x{(ulong)handle} below 0."); + try + { + Penumbra.Log.Error( + $"[ResourceLoader] Caught decrease of Reference Counter for {handle->FileName()} at 0x{(ulong)handle} below 0."); + } + catch + { + // ignored + } + returnValue = 1; } From a6d68ddd5abff74548633aba1436001fe3513bd6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 24 Jul 2023 01:34:02 +0200 Subject: [PATCH 1063/2451] Add some known models to identification. --- .../Data/EquipmentIdentificationList.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index ec2e914a..571867c4 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -57,13 +57,16 @@ internal sealed class EquipmentIdentificationList : KeyList private static IEnumerable CustomList => new[] { - (PseudoEquipItem)EquipItem.FromIds(0, 0, 8100, 0, 1, FullEquipType.Body, "Reaper Shroud"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Head, "Cid's Bandana (9041)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9041, 0, 1, FullEquipType.Body, "Cid's Body (9041)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Head, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Body, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Hands, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Legs, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, 9903, 0, 1, FullEquipType.Feet, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)8100, (WeaponType)0, 01, FullEquipType.Body, "Reaper Shroud"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Head, "Cid's Bandana (9041)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Body, "Cid's Body (9041)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Head, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Body, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Hands, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Legs, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Feet, "Smallclothes (NPC, 9903)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 12, FullEquipType.Body, "Ancient Robes (Lahabrea)"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Legs, "Ancient Legs"), + (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Feet, "Ancient Shoes"), }; } From 8d7c7794398da23110120cfc5cd24903cf7ddef5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jul 2023 15:41:38 +0200 Subject: [PATCH 1064/2451] Fix dumb. --- Penumbra/Services/Wrappers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index 8b88fb09..c0ffec79 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -14,8 +14,8 @@ namespace Penumbra.Services; public sealed class IdentifierService : AsyncServiceWrapper { - public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data) - : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data)) + public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data, ItemService items) + : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService)) { } } From b8c9a98ba2e0a803c139ff3837d6051eadd90ba6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jul 2023 15:52:39 +0200 Subject: [PATCH 1065/2451] Remove local GameData --- Penumbra.GameData/.editorconfig | 3623 ---- Penumbra.GameData/Actors/ActorIdentifier.cs | 225 - Penumbra.GameData/Actors/ActorManager.Data.cs | 406 - .../Actors/ActorManager.Identifiers.cs | 606 - Penumbra.GameData/Actors/AgentBannerParty.cs | 91 - Penumbra.GameData/Actors/IdentifierType.cs | 12 - Penumbra.GameData/Actors/ScreenActor.cs | 17 - Penumbra.GameData/Data/BNpcNames.cs | 16414 ---------------- Penumbra.GameData/Data/DataSharer.cs | 92 - Penumbra.GameData/Data/DisassembledShader.cs | 463 - .../Data/EquipmentIdentificationList.cs | 72 - Penumbra.GameData/Data/GamePathParser.cs | 306 - Penumbra.GameData/Data/GamePaths.cs | 408 - Penumbra.GameData/Data/HumanModelList.cs | 47 - Penumbra.GameData/Data/ItemData.cs | 189 - Penumbra.GameData/Data/KeyList.cs | 101 - Penumbra.GameData/Data/MaterialHandling.cs | 32 - .../Data/ModelIdentificationList.cs | 52 - .../Data/ObjectIdentification.cs | 331 - Penumbra.GameData/Data/RestrictedGear.cs | 433 - Penumbra.GameData/Data/StainData.cs | 70 - .../Data/WeaponIdentificationList.cs | 57 - Penumbra.GameData/Enums/BodySlot.cs | 63 - .../Enums/ChangedItemExtensions.cs | 33 - Penumbra.GameData/Enums/CustomizationType.cs | 55 - Penumbra.GameData/Enums/EquipSlot.cs | 232 - Penumbra.GameData/Enums/FileType.cs | 44 - Penumbra.GameData/Enums/FullEquipType.cs | 450 - .../Enums/ModelTypeExtensions.cs | 26 - Penumbra.GameData/Enums/ObjectType.cs | 53 - Penumbra.GameData/Enums/Race.cs | 510 - Penumbra.GameData/Enums/ResourceType.cs | 319 - Penumbra.GameData/Enums/RspAttribute.cs | 91 - Penumbra.GameData/Enums/WeaponCategory.cs | 51 - Penumbra.GameData/Files/AvfxFile.cs | 283 - Penumbra.GameData/Files/AvfxMagic.cs | 149 - Penumbra.GameData/Files/IWritable.cs | 7 - Penumbra.GameData/Files/MdlFile.Write.cs | 285 - Penumbra.GameData/Files/MdlFile.cs | 249 - .../Files/MtrlFile.ColorDyeSet.cs | 90 - Penumbra.GameData/Files/MtrlFile.ColorSet.cs | 135 - Penumbra.GameData/Files/MtrlFile.Write.cs | 116 - Penumbra.GameData/Files/MtrlFile.cs | 243 - Penumbra.GameData/Files/ShpkFile.Shader.cs | 220 - .../Files/ShpkFile.StringPool.cs | 79 - Penumbra.GameData/Files/ShpkFile.Write.cs | 165 - Penumbra.GameData/Files/ShpkFile.cs | 486 - .../Files/StmFile.StainingTemplateEntry.cs | 174 - Penumbra.GameData/Files/StmFile.cs | 98 - Penumbra.GameData/GameData.cs | 81 - Penumbra.GameData/Interop/D3DCompiler.cs | 64 - Penumbra.GameData/Offsets.cs | 35 - Penumbra.GameData/Penumbra.GameData.csproj | 64 - Penumbra.GameData/Signatures.cs | 90 - Penumbra.GameData/Structs/CharacterArmor.cs | 59 - Penumbra.GameData/Structs/CharacterWeapon.cs | 68 - Penumbra.GameData/Structs/CustomizeData.cs | 118 - Penumbra.GameData/Structs/EqdpEntry.cs | 120 - Penumbra.GameData/Structs/EqpEntry.cs | 316 - Penumbra.GameData/Structs/EquipItem.cs | 120 - Penumbra.GameData/Structs/GameObjectInfo.cs | 159 - Penumbra.GameData/Structs/GmpEntry.cs | 103 - Penumbra.GameData/Structs/ImcEntry.cs | 50 - Penumbra.GameData/Structs/RspEntry.cs | 57 - Penumbra.GameData/Structs/SetId.cs | 29 - Penumbra.GameData/Structs/Stain.cs | 52 - Penumbra.GameData/Structs/StainId.cs | 36 - Penumbra.GameData/Structs/WeaponType.cs | 35 - Penumbra.GameData/UtilityFunctions.cs | 35 - 69 files changed, 30444 deletions(-) delete mode 100644 Penumbra.GameData/.editorconfig delete mode 100644 Penumbra.GameData/Actors/ActorIdentifier.cs delete mode 100644 Penumbra.GameData/Actors/ActorManager.Data.cs delete mode 100644 Penumbra.GameData/Actors/ActorManager.Identifiers.cs delete mode 100644 Penumbra.GameData/Actors/AgentBannerParty.cs delete mode 100644 Penumbra.GameData/Actors/IdentifierType.cs delete mode 100644 Penumbra.GameData/Actors/ScreenActor.cs delete mode 100644 Penumbra.GameData/Data/BNpcNames.cs delete mode 100644 Penumbra.GameData/Data/DataSharer.cs delete mode 100644 Penumbra.GameData/Data/DisassembledShader.cs delete mode 100644 Penumbra.GameData/Data/EquipmentIdentificationList.cs delete mode 100644 Penumbra.GameData/Data/GamePathParser.cs delete mode 100644 Penumbra.GameData/Data/GamePaths.cs delete mode 100644 Penumbra.GameData/Data/HumanModelList.cs delete mode 100644 Penumbra.GameData/Data/ItemData.cs delete mode 100644 Penumbra.GameData/Data/KeyList.cs delete mode 100644 Penumbra.GameData/Data/MaterialHandling.cs delete mode 100644 Penumbra.GameData/Data/ModelIdentificationList.cs delete mode 100644 Penumbra.GameData/Data/ObjectIdentification.cs delete mode 100644 Penumbra.GameData/Data/RestrictedGear.cs delete mode 100644 Penumbra.GameData/Data/StainData.cs delete mode 100644 Penumbra.GameData/Data/WeaponIdentificationList.cs delete mode 100644 Penumbra.GameData/Enums/BodySlot.cs delete mode 100644 Penumbra.GameData/Enums/ChangedItemExtensions.cs delete mode 100644 Penumbra.GameData/Enums/CustomizationType.cs delete mode 100644 Penumbra.GameData/Enums/EquipSlot.cs delete mode 100644 Penumbra.GameData/Enums/FileType.cs delete mode 100644 Penumbra.GameData/Enums/FullEquipType.cs delete mode 100644 Penumbra.GameData/Enums/ModelTypeExtensions.cs delete mode 100644 Penumbra.GameData/Enums/ObjectType.cs delete mode 100644 Penumbra.GameData/Enums/Race.cs delete mode 100644 Penumbra.GameData/Enums/ResourceType.cs delete mode 100644 Penumbra.GameData/Enums/RspAttribute.cs delete mode 100644 Penumbra.GameData/Enums/WeaponCategory.cs delete mode 100644 Penumbra.GameData/Files/AvfxFile.cs delete mode 100644 Penumbra.GameData/Files/AvfxMagic.cs delete mode 100644 Penumbra.GameData/Files/IWritable.cs delete mode 100644 Penumbra.GameData/Files/MdlFile.Write.cs delete mode 100644 Penumbra.GameData/Files/MdlFile.cs delete mode 100644 Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs delete mode 100644 Penumbra.GameData/Files/MtrlFile.ColorSet.cs delete mode 100644 Penumbra.GameData/Files/MtrlFile.Write.cs delete mode 100644 Penumbra.GameData/Files/MtrlFile.cs delete mode 100644 Penumbra.GameData/Files/ShpkFile.Shader.cs delete mode 100644 Penumbra.GameData/Files/ShpkFile.StringPool.cs delete mode 100644 Penumbra.GameData/Files/ShpkFile.Write.cs delete mode 100644 Penumbra.GameData/Files/ShpkFile.cs delete mode 100644 Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs delete mode 100644 Penumbra.GameData/Files/StmFile.cs delete mode 100644 Penumbra.GameData/GameData.cs delete mode 100644 Penumbra.GameData/Interop/D3DCompiler.cs delete mode 100644 Penumbra.GameData/Offsets.cs delete mode 100644 Penumbra.GameData/Penumbra.GameData.csproj delete mode 100644 Penumbra.GameData/Signatures.cs delete mode 100644 Penumbra.GameData/Structs/CharacterArmor.cs delete mode 100644 Penumbra.GameData/Structs/CharacterWeapon.cs delete mode 100644 Penumbra.GameData/Structs/CustomizeData.cs delete mode 100644 Penumbra.GameData/Structs/EqdpEntry.cs delete mode 100644 Penumbra.GameData/Structs/EqpEntry.cs delete mode 100644 Penumbra.GameData/Structs/EquipItem.cs delete mode 100644 Penumbra.GameData/Structs/GameObjectInfo.cs delete mode 100644 Penumbra.GameData/Structs/GmpEntry.cs delete mode 100644 Penumbra.GameData/Structs/ImcEntry.cs delete mode 100644 Penumbra.GameData/Structs/RspEntry.cs delete mode 100644 Penumbra.GameData/Structs/SetId.cs delete mode 100644 Penumbra.GameData/Structs/Stain.cs delete mode 100644 Penumbra.GameData/Structs/StainId.cs delete mode 100644 Penumbra.GameData/Structs/WeaponType.cs delete mode 100644 Penumbra.GameData/UtilityFunctions.cs diff --git a/Penumbra.GameData/.editorconfig b/Penumbra.GameData/.editorconfig deleted file mode 100644 index 0bbaa114..00000000 --- a/Penumbra.GameData/.editorconfig +++ /dev/null @@ -1,3623 +0,0 @@ - -[*.proto] -indent_style=tab -indent_size=tab -tab_width=4 - -[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] -indent_style=space -indent_size=4 -tab_width=4 - -[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] -indent_style=space -indent_size=2 -tab_width=2 - -[*] - -# Microsoft .NET properties -csharp_indent_braces=false -csharp_indent_switch_labels=true -csharp_new_line_before_catch=true -csharp_new_line_before_else=true -csharp_new_line_before_finally=true -csharp_new_line_before_members_in_object_initializers=true -csharp_new_line_before_open_brace=all -csharp_new_line_between_query_expression_clauses=true -csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_preserve_single_line_blocks=true -csharp_space_after_cast=false -csharp_space_after_colon_in_inheritance_clause=true -csharp_space_after_comma=true -csharp_space_after_dot=false -csharp_space_after_keywords_in_control_flow_statements=true -csharp_space_after_semicolon_in_for_statement=true -csharp_space_around_binary_operators=before_and_after -csharp_space_before_colon_in_inheritance_clause=true -csharp_space_before_comma=false -csharp_space_before_dot=false -csharp_space_before_open_square_brackets=false -csharp_space_before_semicolon_in_for_statement=false -csharp_space_between_empty_square_brackets=false -csharp_space_between_method_call_empty_parameter_list_parentheses=false -csharp_space_between_method_call_name_and_opening_parenthesis=false -csharp_space_between_method_call_parameter_list_parentheses=false -csharp_space_between_method_declaration_empty_parameter_list_parentheses=false -csharp_space_between_method_declaration_name_and_open_parenthesis=false -csharp_space_between_method_declaration_parameter_list_parentheses=false -csharp_space_between_parentheses=false -csharp_space_between_square_brackets=false -csharp_style_namespace_declarations= file_scoped:suggestion -csharp_style_var_elsewhere=true:suggestion -csharp_style_var_for_built_in_types=true:suggestion -csharp_style_var_when_type_is_apparent=true:suggestion -csharp_using_directive_placement= outside_namespace:silent -dotnet_diagnostic.bc40000.severity=warning -dotnet_diagnostic.bc400005.severity=warning -dotnet_diagnostic.bc40008.severity=warning -dotnet_diagnostic.bc40056.severity=warning -dotnet_diagnostic.bc42016.severity=warning -dotnet_diagnostic.bc42024.severity=warning -dotnet_diagnostic.bc42025.severity=warning -dotnet_diagnostic.bc42104.severity=warning -dotnet_diagnostic.bc42105.severity=warning -dotnet_diagnostic.bc42106.severity=warning -dotnet_diagnostic.bc42107.severity=warning -dotnet_diagnostic.bc42304.severity=warning -dotnet_diagnostic.bc42309.severity=warning -dotnet_diagnostic.bc42322.severity=warning -dotnet_diagnostic.bc42349.severity=warning -dotnet_diagnostic.bc42353.severity=warning -dotnet_diagnostic.bc42354.severity=warning -dotnet_diagnostic.bc42355.severity=warning -dotnet_diagnostic.bc42356.severity=warning -dotnet_diagnostic.bc42358.severity=warning -dotnet_diagnostic.bc42504.severity=warning -dotnet_diagnostic.bc42505.severity=warning -dotnet_diagnostic.cs0067.severity=warning -dotnet_diagnostic.cs0078.severity=warning -dotnet_diagnostic.cs0108.severity=warning -dotnet_diagnostic.cs0109.severity=warning -dotnet_diagnostic.cs0114.severity=warning -dotnet_diagnostic.cs0162.severity=warning -dotnet_diagnostic.cs0164.severity=warning -dotnet_diagnostic.cs0168.severity=warning -dotnet_diagnostic.cs0169.severity=warning -dotnet_diagnostic.cs0183.severity=warning -dotnet_diagnostic.cs0184.severity=warning -dotnet_diagnostic.cs0197.severity=warning -dotnet_diagnostic.cs0219.severity=warning -dotnet_diagnostic.cs0252.severity=warning -dotnet_diagnostic.cs0253.severity=warning -dotnet_diagnostic.cs0414.severity=warning -dotnet_diagnostic.cs0420.severity=warning -dotnet_diagnostic.cs0465.severity=warning -dotnet_diagnostic.cs0469.severity=warning -dotnet_diagnostic.cs0612.severity=warning -dotnet_diagnostic.cs0618.severity=warning -dotnet_diagnostic.cs0628.severity=warning -dotnet_diagnostic.cs0642.severity=warning -dotnet_diagnostic.cs0649.severity=warning -dotnet_diagnostic.cs0652.severity=warning -dotnet_diagnostic.cs0657.severity=warning -dotnet_diagnostic.cs0658.severity=warning -dotnet_diagnostic.cs0659.severity=warning -dotnet_diagnostic.cs0660.severity=warning -dotnet_diagnostic.cs0661.severity=warning -dotnet_diagnostic.cs0665.severity=warning -dotnet_diagnostic.cs0672.severity=warning -dotnet_diagnostic.cs0675.severity=warning -dotnet_diagnostic.cs0693.severity=warning -dotnet_diagnostic.cs1030.severity=warning -dotnet_diagnostic.cs1058.severity=warning -dotnet_diagnostic.cs1066.severity=warning -dotnet_diagnostic.cs1522.severity=warning -dotnet_diagnostic.cs1570.severity=warning -dotnet_diagnostic.cs1571.severity=warning -dotnet_diagnostic.cs1572.severity=warning -dotnet_diagnostic.cs1573.severity=warning -dotnet_diagnostic.cs1574.severity=warning -dotnet_diagnostic.cs1580.severity=warning -dotnet_diagnostic.cs1581.severity=warning -dotnet_diagnostic.cs1584.severity=warning -dotnet_diagnostic.cs1587.severity=warning -dotnet_diagnostic.cs1589.severity=warning -dotnet_diagnostic.cs1590.severity=warning -dotnet_diagnostic.cs1591.severity=warning -dotnet_diagnostic.cs1592.severity=warning -dotnet_diagnostic.cs1710.severity=warning -dotnet_diagnostic.cs1711.severity=warning -dotnet_diagnostic.cs1712.severity=warning -dotnet_diagnostic.cs1717.severity=warning -dotnet_diagnostic.cs1723.severity=warning -dotnet_diagnostic.cs1911.severity=warning -dotnet_diagnostic.cs1957.severity=warning -dotnet_diagnostic.cs1981.severity=warning -dotnet_diagnostic.cs1998.severity=warning -dotnet_diagnostic.cs4014.severity=warning -dotnet_diagnostic.cs7022.severity=warning -dotnet_diagnostic.cs7023.severity=warning -dotnet_diagnostic.cs7095.severity=warning -dotnet_diagnostic.cs8094.severity=warning -dotnet_diagnostic.cs8123.severity=warning -dotnet_diagnostic.cs8321.severity=warning -dotnet_diagnostic.cs8383.severity=warning -dotnet_diagnostic.cs8416.severity=warning -dotnet_diagnostic.cs8417.severity=warning -dotnet_diagnostic.cs8424.severity=warning -dotnet_diagnostic.cs8425.severity=warning -dotnet_diagnostic.cs8509.severity=warning -dotnet_diagnostic.cs8524.severity=warning -dotnet_diagnostic.cs8597.severity=warning -dotnet_diagnostic.cs8600.severity=warning -dotnet_diagnostic.cs8601.severity=warning -dotnet_diagnostic.cs8602.severity=warning -dotnet_diagnostic.cs8603.severity=warning -dotnet_diagnostic.cs8604.severity=warning -dotnet_diagnostic.cs8605.severity=warning -dotnet_diagnostic.cs8607.severity=warning -dotnet_diagnostic.cs8608.severity=warning -dotnet_diagnostic.cs8609.severity=warning -dotnet_diagnostic.cs8610.severity=warning -dotnet_diagnostic.cs8611.severity=warning -dotnet_diagnostic.cs8612.severity=warning -dotnet_diagnostic.cs8613.severity=warning -dotnet_diagnostic.cs8614.severity=warning -dotnet_diagnostic.cs8615.severity=warning -dotnet_diagnostic.cs8616.severity=warning -dotnet_diagnostic.cs8617.severity=warning -dotnet_diagnostic.cs8618.severity=warning -dotnet_diagnostic.cs8619.severity=warning -dotnet_diagnostic.cs8620.severity=warning -dotnet_diagnostic.cs8621.severity=warning -dotnet_diagnostic.cs8622.severity=warning -dotnet_diagnostic.cs8624.severity=warning -dotnet_diagnostic.cs8625.severity=warning -dotnet_diagnostic.cs8629.severity=warning -dotnet_diagnostic.cs8631.severity=warning -dotnet_diagnostic.cs8632.severity=none -dotnet_diagnostic.cs8633.severity=warning -dotnet_diagnostic.cs8634.severity=warning -dotnet_diagnostic.cs8643.severity=warning -dotnet_diagnostic.cs8644.severity=warning -dotnet_diagnostic.cs8645.severity=warning -dotnet_diagnostic.cs8655.severity=warning -dotnet_diagnostic.cs8656.severity=warning -dotnet_diagnostic.cs8667.severity=warning -dotnet_diagnostic.cs8669.severity=none -dotnet_diagnostic.cs8670.severity=warning -dotnet_diagnostic.cs8714.severity=warning -dotnet_diagnostic.cs8762.severity=warning -dotnet_diagnostic.cs8763.severity=warning -dotnet_diagnostic.cs8764.severity=warning -dotnet_diagnostic.cs8765.severity=warning -dotnet_diagnostic.cs8766.severity=warning -dotnet_diagnostic.cs8767.severity=warning -dotnet_diagnostic.cs8768.severity=warning -dotnet_diagnostic.cs8769.severity=warning -dotnet_diagnostic.cs8770.severity=warning -dotnet_diagnostic.cs8774.severity=warning -dotnet_diagnostic.cs8775.severity=warning -dotnet_diagnostic.cs8776.severity=warning -dotnet_diagnostic.cs8777.severity=warning -dotnet_diagnostic.cs8794.severity=warning -dotnet_diagnostic.cs8819.severity=warning -dotnet_diagnostic.cs8824.severity=warning -dotnet_diagnostic.cs8825.severity=warning -dotnet_diagnostic.cs8846.severity=warning -dotnet_diagnostic.cs8847.severity=warning -dotnet_diagnostic.cs8851.severity=warning -dotnet_diagnostic.cs8860.severity=warning -dotnet_diagnostic.cs8892.severity=warning -dotnet_diagnostic.cs8907.severity=warning -dotnet_diagnostic.cs8947.severity=warning -dotnet_diagnostic.cs8960.severity=warning -dotnet_diagnostic.cs8961.severity=warning -dotnet_diagnostic.cs8962.severity=warning -dotnet_diagnostic.cs8963.severity=warning -dotnet_diagnostic.cs8965.severity=warning -dotnet_diagnostic.cs8966.severity=warning -dotnet_diagnostic.cs8971.severity=warning -dotnet_diagnostic.wme006.severity=warning -dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.constants_rule.severity = warning -dotnet_naming_rule.constants_rule.style = upper_camel_case_style -dotnet_naming_rule.constants_rule.symbols=constants_symbols -dotnet_naming_rule.event_rule.import_to_resharper=as_predefined -dotnet_naming_rule.event_rule.severity = warning -dotnet_naming_rule.event_rule.style = upper_camel_case_style -dotnet_naming_rule.event_rule.symbols=event_symbols -dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined -dotnet_naming_rule.interfaces_rule.severity = warning -dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style -dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols -dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined -dotnet_naming_rule.locals_rule.severity = warning -dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 -dotnet_naming_rule.locals_rule.symbols=locals_symbols -dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.local_constants_rule.severity = warning -dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 -dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols -dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined -dotnet_naming_rule.local_functions_rule.severity = warning -dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style -dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols -dotnet_naming_rule.method_rule.import_to_resharper=as_predefined -dotnet_naming_rule.method_rule.severity = warning -dotnet_naming_rule.method_rule.style = upper_camel_case_style -dotnet_naming_rule.method_rule.symbols=method_symbols -dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined -dotnet_naming_rule.parameters_rule.severity = warning -dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 -dotnet_naming_rule.parameters_rule.symbols=parameters_symbols -dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_constants_rule.severity = warning -dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style -dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols -dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_instance_fields_rule.severity = warning -dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols -dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_static_fields_rule.severity = warning -dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols -dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined -dotnet_naming_rule.private_static_readonly_rule.severity = warning -dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style -dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols -dotnet_naming_rule.property_rule.import_to_resharper=as_predefined -dotnet_naming_rule.property_rule.severity = warning -dotnet_naming_rule.property_rule.style = upper_camel_case_style -dotnet_naming_rule.property_rule.symbols=property_symbols -dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined -dotnet_naming_rule.public_fields_rule.severity = warning -dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style -dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols -dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined -dotnet_naming_rule.static_readonly_rule.severity = warning -dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style -dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols -dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined -dotnet_naming_rule.types_and_namespaces_rule.severity = warning -dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style -dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols -dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined -dotnet_naming_rule.type_parameters_rule.severity = warning -dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style -dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols -dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case -dotnet_naming_style.i_upper_camel_case_style.required_prefix=I -dotnet_naming_style.lower_camel_case_style.capitalization=camel_case -dotnet_naming_style.lower_camel_case_style.required_prefix=_ -dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case -dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case -dotnet_naming_style.t_upper_camel_case_style.required_prefix=T -dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case -dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.constants_symbols.applicable_kinds=field -dotnet_naming_symbols.constants_symbols.required_modifiers=const -dotnet_naming_symbols.event_symbols.applicable_accessibilities=* -dotnet_naming_symbols.event_symbols.applicable_kinds=event -dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* -dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface -dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* -dotnet_naming_symbols.locals_symbols.applicable_kinds=local -dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* -dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local -dotnet_naming_symbols.local_constants_symbols.required_modifiers=const -dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* -dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function -dotnet_naming_symbols.method_symbols.applicable_accessibilities=* -dotnet_naming_symbols.method_symbols.applicable_kinds=method -dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* -dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter -dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field -dotnet_naming_symbols.private_constants_symbols.required_modifiers=const -dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static -dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private -dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field -dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly -dotnet_naming_symbols.property_symbols.applicable_accessibilities=* -dotnet_naming_symbols.property_symbols.applicable_kinds=property -dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field -dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field -dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly -dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* -dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate -dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* -dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter -dotnet_separate_import_directive_groups=false -dotnet_sort_system_directives_first=true -dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion -dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion -dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion -dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion -dotnet_style_predefined_type_for_member_access=true:suggestion -dotnet_style_qualification_for_event=false:suggestion -dotnet_style_qualification_for_field=false:suggestion -dotnet_style_qualification_for_method=false:suggestion -dotnet_style_qualification_for_property=false:suggestion -dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion -file_header_template= - -# ReSharper properties -resharper_accessor_owner_body=expression_body -resharper_alignment_tab_fill_style=use_spaces -resharper_align_first_arg_by_paren=false -resharper_align_linq_query=false -resharper_align_multiline_argument=true -resharper_align_multiline_array_and_object_initializer=false -resharper_align_multiline_array_initializer=true -resharper_align_multiline_binary_expressions_chain=false -resharper_align_multiline_binary_patterns=false -resharper_align_multiline_ctor_init=true -resharper_align_multiline_expression_braces=false -resharper_align_multiline_implements_list=true -resharper_align_multiline_property_pattern=false -resharper_align_multiline_statement_conditions=true -resharper_align_multiline_switch_expression=false -resharper_align_multiline_type_argument=true -resharper_align_multiline_type_parameter=true -resharper_align_multline_type_parameter_constrains=true -resharper_align_multline_type_parameter_list=false -resharper_align_tuple_components=false -resharper_align_union_type_usage=true -resharper_allow_alias=true -resharper_allow_comment_after_lbrace=false -resharper_allow_far_alignment=false -resharper_always_use_end_of_line_brace_style=false -resharper_apply_auto_detected_rules=false -resharper_apply_on_completion=false -resharper_arguments_anonymous_function=positional -resharper_arguments_literal=positional -resharper_arguments_named=positional -resharper_arguments_other=positional -resharper_arguments_skip_single=false -resharper_arguments_string_literal=positional -resharper_attribute_style=do_not_touch -resharper_autodetect_indent_settings=false -resharper_blank_lines_after_block_statements=1 -resharper_blank_lines_after_case=0 -resharper_blank_lines_after_control_transfer_statements=1 -resharper_blank_lines_after_file_scoped_namespace_directive=1 -resharper_blank_lines_after_imports=1 -resharper_blank_lines_after_multiline_statements=0 -resharper_blank_lines_after_options=1 -resharper_blank_lines_after_start_comment=1 -resharper_blank_lines_after_using_list=1 -resharper_blank_lines_around_accessor=0 -resharper_blank_lines_around_auto_property=1 -resharper_blank_lines_around_block_case_section=0 -resharper_blank_lines_around_class_definition=1 -resharper_blank_lines_around_field=1 -resharper_blank_lines_around_function_declaration=0 -resharper_blank_lines_around_function_definition=1 -resharper_blank_lines_around_global_attribute=0 -resharper_blank_lines_around_invocable=1 -resharper_blank_lines_around_local_method=1 -resharper_blank_lines_around_multiline_case_section=0 -resharper_blank_lines_around_namespace=1 -resharper_blank_lines_around_other_declaration=0 -resharper_blank_lines_around_property=1 -resharper_blank_lines_around_razor_functions=1 -resharper_blank_lines_around_razor_helpers=1 -resharper_blank_lines_around_razor_sections=1 -resharper_blank_lines_around_region=1 -resharper_blank_lines_around_single_line_accessor=0 -resharper_blank_lines_around_single_line_auto_property=0 -resharper_blank_lines_around_single_line_field=0 -resharper_blank_lines_around_single_line_function_definition=0 -resharper_blank_lines_around_single_line_invocable=0 -resharper_blank_lines_around_single_line_local_method=0 -resharper_blank_lines_around_single_line_property=0 -resharper_blank_lines_around_single_line_type=0 -resharper_blank_lines_around_type=1 -resharper_blank_lines_before_block_statements=0 -resharper_blank_lines_before_case=0 -resharper_blank_lines_before_control_transfer_statements=0 -resharper_blank_lines_before_multiline_statements=0 -resharper_blank_lines_before_single_line_comment=0 -resharper_blank_lines_inside_namespace=0 -resharper_blank_lines_inside_region=1 -resharper_blank_lines_inside_type=0 -resharper_blank_line_after_pi=true -resharper_braces_for_dowhile=required -resharper_braces_for_fixed=required -resharper_braces_for_for=required_for_multiline -resharper_braces_for_foreach=required_for_multiline -resharper_braces_for_ifelse=not_required_for_both -resharper_braces_for_lock=required -resharper_braces_for_using=required -resharper_braces_for_while=required_for_multiline -resharper_braces_redundant=true -resharper_break_template_declaration=line_break -resharper_can_use_global_alias=true -resharper_configure_await_analysis_mode=disabled -resharper_constructor_or_destructor_body=expression_body -resharper_continuous_indent_multiplier=1 -resharper_continuous_line_indent=single -resharper_cpp_align_multiline_argument=true -resharper_cpp_align_multiline_calls_chain=true -resharper_cpp_align_multiline_extends_list=true -resharper_cpp_align_multiline_for_stmt=true -resharper_cpp_align_multiline_parameter=true -resharper_cpp_align_multiple_declaration=true -resharper_cpp_align_ternary=align_not_nested -resharper_cpp_anonymous_method_declaration_braces=next_line -resharper_cpp_case_block_braces=next_line_shifted_2 -resharper_cpp_empty_block_style=multiline -resharper_cpp_indent_switch_labels=false -resharper_cpp_insert_final_newline=false -resharper_cpp_int_align_comments=false -resharper_cpp_invocable_declaration_braces=next_line -resharper_cpp_max_line_length=120 -resharper_cpp_new_line_before_catch=true -resharper_cpp_new_line_before_else=true -resharper_cpp_new_line_before_while=true -resharper_cpp_other_braces=next_line -resharper_cpp_space_around_binary_operator=true -resharper_cpp_type_declaration_braces=next_line -resharper_cpp_wrap_arguments_style=wrap_if_long -resharper_cpp_wrap_lines=true -resharper_cpp_wrap_parameters_style=wrap_if_long -resharper_csharp_align_multiline_argument=false -resharper_csharp_align_multiline_calls_chain=false -resharper_csharp_align_multiline_expression=false -resharper_csharp_align_multiline_extends_list=false -resharper_csharp_align_multiline_for_stmt=false -resharper_csharp_align_multiline_parameter=false -resharper_csharp_align_multiple_declaration=true -resharper_csharp_empty_block_style=together -resharper_csharp_insert_final_newline=true -resharper_csharp_int_align_comments=true -resharper_csharp_max_line_length=144 -resharper_csharp_naming_rule.enum_member=AaBb -resharper_csharp_naming_rule.method_property_event=AaBb -resharper_csharp_naming_rule.other=AaBb -resharper_csharp_new_line_before_while=false -resharper_csharp_prefer_qualified_reference=false -resharper_csharp_space_after_unary_operator=false -resharper_csharp_wrap_arguments_style=wrap_if_long -resharper_csharp_wrap_before_binary_opsign=true -resharper_csharp_wrap_for_stmt_header_style=wrap_if_long -resharper_csharp_wrap_lines=true -resharper_csharp_wrap_parameters_style=wrap_if_long -resharper_css_brace_style=end_of_line -resharper_css_insert_final_newline=false -resharper_css_keep_blank_lines_between_declarations=1 -resharper_css_max_line_length=120 -resharper_css_wrap_lines=true -resharper_cxxcli_property_declaration_braces=next_line -resharper_declarations_style=separate_lines -resharper_default_exception_variable_name=e -resharper_default_value_when_type_evident=default_literal -resharper_default_value_when_type_not_evident=default_literal -resharper_delete_quotes_from_solid_values=false -resharper_disable_blank_line_changes=false -resharper_disable_formatter=false -resharper_disable_indenter=false -resharper_disable_int_align=false -resharper_disable_line_break_changes=false -resharper_disable_line_break_removal=false -resharper_disable_space_changes=false -resharper_disable_space_changes_before_trailing_comment=false -resharper_dont_remove_extra_blank_lines=false -resharper_enable_wrapping=false -resharper_enforce_line_ending_style=false -resharper_event_handler_pattern_long=$object$On$event$ -resharper_event_handler_pattern_short=On$event$ -resharper_expression_braces=inside -resharper_expression_pars=inside -resharper_extra_spaces=remove_all -resharper_force_attribute_style=separate -resharper_force_chop_compound_do_expression=false -resharper_force_chop_compound_if_expression=false -resharper_force_chop_compound_while_expression=false -resharper_force_control_statements_braces=do_not_change -resharper_force_linebreaks_inside_complex_literals=true -resharper_force_variable_declarations_on_new_line=false -resharper_format_leading_spaces_decl=false -resharper_free_block_braces=next_line -resharper_function_declaration_return_type_style=do_not_change -resharper_function_definition_return_type_style=do_not_change -resharper_generator_mode=false -resharper_html_attribute_indent=align_by_first_attribute -resharper_html_insert_final_newline=false -resharper_html_linebreak_before_elements=body,div,p,form,h1,h2,h3 -resharper_html_max_blank_lines_between_tags=2 -resharper_html_max_line_length=120 -resharper_html_pi_attribute_style=on_single_line -resharper_html_space_before_self_closing=false -resharper_html_wrap_lines=true -resharper_ignore_space_preservation=false -resharper_include_prefix_comment_in_indent=false -resharper_indent_access_specifiers_from_class=false -resharper_indent_aligned_ternary=true -resharper_indent_anonymous_method_block=false -resharper_indent_braces_inside_statement_conditions=true -resharper_indent_case_from_select=true -resharper_indent_child_elements=OneIndent -resharper_indent_class_members_from_access_specifiers=false -resharper_indent_comment=true -resharper_indent_inside_namespace=true -resharper_indent_invocation_pars=inside -resharper_indent_left_par_inside_expression=false -resharper_indent_method_decl_pars=inside -resharper_indent_nested_fixed_stmt=false -resharper_indent_nested_foreach_stmt=true -resharper_indent_nested_for_stmt=true -resharper_indent_nested_lock_stmt=false -resharper_indent_nested_usings_stmt=false -resharper_indent_nested_while_stmt=true -resharper_indent_pars=inside -resharper_indent_preprocessor_directives=none -resharper_indent_preprocessor_if=no_indent -resharper_indent_preprocessor_other=no_indent -resharper_indent_preprocessor_region=usual_indent -resharper_indent_statement_pars=inside -resharper_indent_text=OneIndent -resharper_indent_typearg_angles=inside -resharper_indent_typeparam_angles=inside -resharper_indent_type_constraints=true -resharper_indent_wrapped_function_names=false -resharper_instance_members_qualify_declared_in=this_class, base_class -resharper_int_align=true -resharper_int_align_assignments=true -resharper_int_align_binary_expressions=false -resharper_int_align_declaration_names=false -resharper_int_align_eq=false -resharper_int_align_fields=true -resharper_int_align_fix_in_adjacent=true -resharper_int_align_invocations=true -resharper_int_align_methods=true -resharper_int_align_nested_ternary=true -resharper_int_align_parameters=false -resharper_int_align_properties=true -resharper_int_align_property_patterns=true -resharper_int_align_switch_expressions=true -resharper_int_align_switch_sections=true -resharper_int_align_variables=true -resharper_js_align_multiline_parameter=false -resharper_js_align_multiple_declaration=false -resharper_js_align_ternary=none -resharper_js_brace_style=end_of_line -resharper_js_empty_block_style=multiline -resharper_js_indent_switch_labels=false -resharper_js_insert_final_newline=false -resharper_js_keep_blank_lines_between_declarations=2 -resharper_js_max_line_length=120 -resharper_js_new_line_before_catch=false -resharper_js_new_line_before_else=false -resharper_js_new_line_before_finally=false -resharper_js_new_line_before_while=false -resharper_js_space_around_binary_operator=true -resharper_js_wrap_arguments_style=chop_if_long -resharper_js_wrap_before_binary_opsign=false -resharper_js_wrap_for_stmt_header_style=chop_if_long -resharper_js_wrap_lines=true -resharper_js_wrap_parameters_style=chop_if_long -resharper_keep_blank_lines_in_code=2 -resharper_keep_blank_lines_in_declarations=2 -resharper_keep_existing_attribute_arrangement=false -resharper_keep_existing_declaration_block_arrangement=false -resharper_keep_existing_declaration_parens_arrangement=true -resharper_keep_existing_embedded_arrangement=false -resharper_keep_existing_embedded_block_arrangement=false -resharper_keep_existing_enum_arrangement=false -resharper_keep_existing_expr_member_arrangement=false -resharper_keep_existing_initializer_arrangement=false -resharper_keep_existing_invocation_parens_arrangement=true -resharper_keep_existing_property_patterns_arrangement=true -resharper_keep_existing_switch_expression_arrangement=false -resharper_keep_nontrivial_alias=true -resharper_keep_user_linebreaks=true -resharper_keep_user_wrapping=true -resharper_linebreaks_around_razor_statements=true -resharper_linebreaks_inside_tags_for_elements_longer_than=2147483647 -resharper_linebreaks_inside_tags_for_elements_with_child_elements=true -resharper_linebreaks_inside_tags_for_multiline_elements=true -resharper_linebreak_before_all_elements=false -resharper_linebreak_before_multiline_elements=true -resharper_linebreak_before_singleline_elements=false -resharper_line_break_after_colon_in_member_initializer_lists=do_not_change -resharper_line_break_after_comma_in_member_initializer_lists=false -resharper_line_break_before_comma_in_member_initializer_lists=false -resharper_line_break_before_requires_clause=do_not_change -resharper_linkage_specification_braces=end_of_line -resharper_linkage_specification_indentation=none -resharper_local_function_body=expression_body -resharper_macro_block_begin= -resharper_macro_block_end= -resharper_max_array_initializer_elements_on_line=10000 -resharper_max_attribute_length_for_same_line=38 -resharper_max_enum_members_on_line=1 -resharper_max_formal_parameters_on_line=10000 -resharper_max_initializer_elements_on_line=1 -resharper_max_invocation_arguments_on_line=10000 -resharper_media_query_style=same_line -resharper_member_initializer_list_style=do_not_change -resharper_method_or_operator_body=expression_body -resharper_min_blank_lines_after_imports=0 -resharper_min_blank_lines_around_fields=0 -resharper_min_blank_lines_around_functions=1 -resharper_min_blank_lines_around_types=1 -resharper_min_blank_lines_between_declarations=1 -resharper_namespace_declaration_braces=next_line -resharper_namespace_indentation=all -resharper_nested_ternary_style=autodetect -resharper_new_line_before_enumerators=true -resharper_normalize_tag_names=false -resharper_no_indent_inside_elements=html,body,thead,tbody,tfoot -resharper_no_indent_inside_if_element_longer_than=200 -resharper_object_creation_when_type_evident=target_typed -resharper_object_creation_when_type_not_evident=explicitly_typed -resharper_old_engine=false -resharper_options_braces_pointy=false -resharper_outdent_binary_ops=true -resharper_outdent_binary_pattern_ops=false -resharper_outdent_commas=false -resharper_outdent_dots=false -resharper_outdent_namespace_member=false -resharper_outdent_statement_labels=false -resharper_outdent_ternary_ops=false -resharper_parentheses_non_obvious_operations=none, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, bitwise_and -resharper_parentheses_redundancy_style=remove_if_not_clarifies_precedence -resharper_parentheses_same_type_operations=false -resharper_pi_attributes_indent=align_by_first_attribute -resharper_place_attribute_on_same_line=false -resharper_place_class_decorator_on_the_same_line=false -resharper_place_comments_at_first_column=false -resharper_place_constructor_initializer_on_same_line=false -resharper_place_each_decorator_on_new_line=false -resharper_place_event_attribute_on_same_line=false -resharper_place_expr_accessor_on_single_line=true -resharper_place_expr_method_on_single_line=false -resharper_place_expr_property_on_single_line=false -resharper_place_field_decorator_on_the_same_line=false -resharper_place_linq_into_on_new_line=true -resharper_place_method_decorator_on_the_same_line=false -resharper_place_namespace_definitions_on_same_line=false -resharper_place_property_attribute_on_same_line=false -resharper_place_property_decorator_on_the_same_line=false -resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line -resharper_place_simple_embedded_statement_on_same_line=false -resharper_place_simple_enum_on_single_line=true -resharper_place_simple_initializer_on_single_line=true -resharper_place_simple_property_pattern_on_single_line=true -resharper_place_simple_switch_expression_on_single_line=true -resharper_place_template_args_on_new_line=false -resharper_place_type_constraints_on_same_line=true -resharper_prefer_explicit_discard_declaration=false -resharper_prefer_separate_deconstructed_variables_declaration=false -resharper_preserve_spaces_inside_tags=pre,textarea -resharper_properties_style=separate_lines_for_nonsingle -resharper_protobuf_brace_style=end_of_line -resharper_protobuf_empty_block_style=together_same_line -resharper_protobuf_insert_final_newline=false -resharper_protobuf_max_line_length=120 -resharper_protobuf_wrap_lines=true -resharper_qualified_using_at_nested_scope=false -resharper_quote_style=doublequoted -resharper_razor_prefer_qualified_reference=true -resharper_remove_blank_lines_near_braces=false -resharper_remove_blank_lines_near_braces_in_code=true -resharper_remove_blank_lines_near_braces_in_declarations=true -resharper_remove_this_qualifier=true -resharper_requires_expression_braces=next_line -resharper_resx_attribute_indent=single_indent -resharper_resx_insert_final_newline=false -resharper_resx_linebreak_before_elements= -resharper_resx_max_blank_lines_between_tags=0 -resharper_resx_max_line_length=2147483647 -resharper_resx_pi_attribute_style=do_not_touch -resharper_resx_space_before_self_closing=false -resharper_resx_wrap_lines=false -resharper_resx_wrap_tags_and_pi=false -resharper_resx_wrap_text=false -resharper_selector_style=same_line -resharper_show_autodetect_configure_formatting_tip=true -resharper_simple_blocks=do_not_change -resharper_simple_block_style=do_not_change -resharper_simple_case_statement_style=do_not_change -resharper_simple_embedded_statement_style=do_not_change -resharper_single_statement_function_style=do_not_change -resharper_sort_attributes=false -resharper_sort_class_selectors=false -resharper_sort_usings=true -resharper_sort_usings_lowercase_first=false -resharper_spaces_around_eq_in_attribute=false -resharper_spaces_around_eq_in_pi_attribute=false -resharper_spaces_inside_tags=false -resharper_space_after_arrow=true -resharper_space_after_attributes=true -resharper_space_after_attribute_target_colon=true -resharper_space_after_cast=false -resharper_space_after_colon=true -resharper_space_after_colon_in_case=true -resharper_space_after_colon_in_inheritance_clause=true -resharper_space_after_colon_in_type_annotation=true -resharper_space_after_comma=true -resharper_space_after_for_colon=true -resharper_space_after_function_comma=true -resharper_space_after_keywords_in_control_flow_statements=true -resharper_space_after_last_attribute=false -resharper_space_after_last_pi_attribute=false -resharper_space_after_media_colon=true -resharper_space_after_media_comma=true -resharper_space_after_operator_keyword=true -resharper_space_after_property_colon=true -resharper_space_after_property_semicolon=true -resharper_space_after_ptr_in_data_member=true -resharper_space_after_ptr_in_data_members=false -resharper_space_after_ptr_in_method=true -resharper_space_after_ref_in_data_member=true -resharper_space_after_ref_in_data_members=false -resharper_space_after_ref_in_method=true -resharper_space_after_selector_comma=true -resharper_space_after_semicolon_in_for_statement=true -resharper_space_after_separator=false -resharper_space_after_ternary_colon=true -resharper_space_after_ternary_quest=true -resharper_space_after_triple_slash=true -resharper_space_after_type_parameter_constraint_colon=true -resharper_space_around_additive_op=true -resharper_space_around_alias_eq=true -resharper_space_around_assignment_op=true -resharper_space_around_assignment_operator=true -resharper_space_around_attribute_match_operator=false -resharper_space_around_deref_in_trailing_return_type=true -resharper_space_around_lambda_arrow=true -resharper_space_around_member_access_operator=false -resharper_space_around_operator=true -resharper_space_around_pipe_or_amper_in_type_usage=true -resharper_space_around_relational_op=true -resharper_space_around_selector_operator=true -resharper_space_around_shift_op=true -resharper_space_around_stmt_colon=true -resharper_space_around_ternary_operator=true -resharper_space_before_array_rank_parentheses=false -resharper_space_before_arrow=true -resharper_space_before_attribute_target_colon=false -resharper_space_before_checked_parentheses=false -resharper_space_before_colon=false -resharper_space_before_colon_in_case=false -resharper_space_before_colon_in_inheritance_clause=true -resharper_space_before_colon_in_type_annotation=false -resharper_space_before_comma=false -resharper_space_before_default_parentheses=false -resharper_space_before_empty_invocation_parentheses=false -resharper_space_before_empty_method_parentheses=false -resharper_space_before_for_colon=true -resharper_space_before_function_comma=false -resharper_space_before_initializer_braces=false -resharper_space_before_invocation_parentheses=false -resharper_space_before_label_colon=false -resharper_space_before_lambda_parentheses=false -resharper_space_before_media_colon=false -resharper_space_before_media_comma=false -resharper_space_before_method_parentheses=false -resharper_space_before_nameof_parentheses=false -resharper_space_before_new_parentheses=false -resharper_space_before_nullable_mark=false -resharper_space_before_open_square_brackets=false -resharper_space_before_pointer_asterik_declaration=false -resharper_space_before_property_colon=false -resharper_space_before_property_semicolon=false -resharper_space_before_ptr_in_abstract_decl=false -resharper_space_before_ptr_in_data_member=false -resharper_space_before_ptr_in_data_members=true -resharper_space_before_ptr_in_method=false -resharper_space_before_ref_in_abstract_decl=false -resharper_space_before_ref_in_data_member=false -resharper_space_before_ref_in_data_members=true -resharper_space_before_ref_in_method=false -resharper_space_before_selector_comma=false -resharper_space_before_semicolon=false -resharper_space_before_semicolon_in_for_statement=false -resharper_space_before_separator=false -resharper_space_before_singleline_accessorholder=true -resharper_space_before_sizeof_parentheses=false -resharper_space_before_template_args=false -resharper_space_before_template_params=true -resharper_space_before_ternary_colon=true -resharper_space_before_ternary_quest=true -resharper_space_before_trailing_comment=true -resharper_space_before_typeof_parentheses=false -resharper_space_before_type_argument_angle=false -resharper_space_before_type_parameters_brackets=false -resharper_space_before_type_parameter_angle=false -resharper_space_before_type_parameter_constraint_colon=true -resharper_space_before_type_parameter_parentheses=true -resharper_space_between_accessors_in_singleline_property=true -resharper_space_between_attribute_sections=true -resharper_space_between_closing_angle_brackets_in_template_args=false -resharper_space_between_empty_square_brackets=false -resharper_space_between_keyword_and_expression=true -resharper_space_between_keyword_and_type=true -resharper_space_between_method_call_empty_parameter_list_parentheses=false -resharper_space_between_method_call_name_and_opening_parenthesis=false -resharper_space_between_method_call_parameter_list_parentheses=false -resharper_space_between_method_declaration_empty_parameter_list_parentheses=false -resharper_space_between_method_declaration_name_and_open_parenthesis=false -resharper_space_between_method_declaration_parameter_list_parentheses=false -resharper_space_between_parentheses_of_control_flow_statements=false -resharper_space_between_square_brackets=false -resharper_space_between_typecast_parentheses=false -resharper_space_colon_after=true -resharper_space_colon_before=false -resharper_space_comma=true -resharper_space_equals=true -resharper_space_inside_braces=true -resharper_space_in_singleline_accessorholder=true -resharper_space_in_singleline_anonymous_method=true -resharper_space_in_singleline_method=true -resharper_space_near_postfix_and_prefix_op=false -resharper_space_within_array_initialization_braces=false -resharper_space_within_array_rank_empty_parentheses=false -resharper_space_within_array_rank_parentheses=false -resharper_space_within_attribute_angles=false -resharper_space_within_attribute_match_brackets=false -resharper_space_within_checked_parentheses=false -resharper_space_within_default_parentheses=false -resharper_space_within_empty_braces=true -resharper_space_within_empty_initializer_braces=false -resharper_space_within_empty_invocation_parentheses=false -resharper_space_within_empty_method_parentheses=false -resharper_space_within_empty_object_literal_braces=false -resharper_space_within_empty_template_params=false -resharper_space_within_expression_parentheses=false -resharper_space_within_function_parentheses=false -resharper_space_within_import_braces=true -resharper_space_within_initializer_braces=false -resharper_space_within_invocation_parentheses=false -resharper_space_within_media_block=true -resharper_space_within_media_parentheses=false -resharper_space_within_method_parentheses=false -resharper_space_within_nameof_parentheses=false -resharper_space_within_new_parentheses=false -resharper_space_within_object_literal_braces=true -resharper_space_within_parentheses=false -resharper_space_within_property_block=true -resharper_space_within_single_line_array_initializer_braces=true -resharper_space_within_sizeof_parentheses=false -resharper_space_within_template_args=false -resharper_space_within_template_argument=false -resharper_space_within_template_params=false -resharper_space_within_tuple_parentheses=false -resharper_space_within_typeof_parentheses=false -resharper_space_within_type_argument_angles=false -resharper_space_within_type_parameters_brackets=false -resharper_space_within_type_parameter_angles=false -resharper_space_within_type_parameter_parentheses=false -resharper_special_else_if_treatment=true -resharper_static_members_qualify_members=none -resharper_static_members_qualify_with=declared_type -resharper_stick_comment=true -resharper_support_vs_event_naming_pattern=true -resharper_termination_style=ensure_semicolon -resharper_toplevel_function_declaration_return_type_style=do_not_change -resharper_toplevel_function_definition_return_type_style=do_not_change -resharper_trailing_comma_in_multiline_lists=true -resharper_trailing_comma_in_singleline_lists=false -resharper_types_braces=end_of_line -resharper_use_continuous_indent_inside_initializer_braces=true -resharper_use_continuous_indent_inside_parens=true -resharper_use_continuous_line_indent_in_expression_braces=false -resharper_use_continuous_line_indent_in_method_pars=false -resharper_use_heuristics_for_body_style=true -resharper_use_indents_from_main_language_in_file=true -resharper_use_indent_from_previous_element=true -resharper_use_indent_from_vs=false -resharper_use_roslyn_logic_for_evident_types=false -resharper_vb_align_multiline_argument=true -resharper_vb_align_multiline_expression=true -resharper_vb_align_multiline_parameter=true -resharper_vb_align_multiple_declaration=true -resharper_vb_insert_final_newline=false -resharper_vb_max_line_length=120 -resharper_vb_place_field_attribute_on_same_line=true -resharper_vb_place_method_attribute_on_same_line=false -resharper_vb_place_type_attribute_on_same_line=false -resharper_vb_prefer_qualified_reference=false -resharper_vb_space_after_unary_operator=true -resharper_vb_space_around_multiplicative_op=false -resharper_vb_wrap_arguments_style=wrap_if_long -resharper_vb_wrap_before_binary_opsign=false -resharper_vb_wrap_lines=true -resharper_vb_wrap_parameters_style=wrap_if_long -resharper_wrap_after_binary_opsign=true -resharper_wrap_after_declaration_lpar=false -resharper_wrap_after_dot=false -resharper_wrap_after_dot_in_method_calls=false -resharper_wrap_after_expression_lbrace=true -resharper_wrap_after_invocation_lpar=false -resharper_wrap_around_elements=true -resharper_wrap_array_initializer_style=chop_always -resharper_wrap_array_literals=chop_if_long -resharper_wrap_base_clause_style=wrap_if_long -resharper_wrap_before_arrow_with_expressions=true -resharper_wrap_before_binary_pattern_op=true -resharper_wrap_before_colon=false -resharper_wrap_before_comma=false -resharper_wrap_before_comma_in_base_clause=false -resharper_wrap_before_declaration_lpar=false -resharper_wrap_before_declaration_rpar=false -resharper_wrap_before_dot=true -resharper_wrap_before_eq=false -resharper_wrap_before_expression_rbrace=true -resharper_wrap_before_extends_colon=false -resharper_wrap_before_first_type_parameter_constraint=false -resharper_wrap_before_invocation_lpar=false -resharper_wrap_before_invocation_rpar=false -resharper_wrap_before_linq_expression=false -resharper_wrap_before_ternary_opsigns=true -resharper_wrap_before_type_parameter_langle=false -resharper_wrap_braced_init_list_style=wrap_if_long -resharper_wrap_chained_binary_expressions=chop_if_long -resharper_wrap_chained_binary_patterns=wrap_if_long -resharper_wrap_chained_method_calls=wrap_if_long -resharper_wrap_ctor_initializer_style=wrap_if_long -resharper_wrap_enumeration_style=chop_if_long -resharper_wrap_enum_declaration=chop_always -resharper_wrap_enum_style=do_not_change -resharper_wrap_extends_list_style=wrap_if_long -resharper_wrap_imports=chop_if_long -resharper_wrap_multiple_declaration_style=chop_if_long -resharper_wrap_multiple_type_parameter_constraints_style=chop_if_long -resharper_wrap_object_literals=chop_if_long -resharper_wrap_property_pattern=chop_if_long -resharper_wrap_switch_expression=chop_always -resharper_wrap_ternary_expr_style=chop_if_long -resharper_wrap_union_type_usage=chop_if_long -resharper_wrap_verbatim_interpolated_strings=no_wrap -resharper_xmldoc_attribute_indent=single_indent -resharper_xmldoc_insert_final_newline=false -resharper_xmldoc_linebreak_before_elements=summary,remarks,example,returns,param,typeparam,value,para -resharper_xmldoc_max_blank_lines_between_tags=0 -resharper_xmldoc_max_line_length=120 -resharper_xmldoc_pi_attribute_style=do_not_touch -resharper_xmldoc_space_before_self_closing=true -resharper_xmldoc_wrap_lines=true -resharper_xmldoc_wrap_tags_and_pi=true -resharper_xmldoc_wrap_text=true -resharper_xml_attribute_indent=align_by_first_attribute -resharper_xml_insert_final_newline=false -resharper_xml_linebreak_before_elements= -resharper_xml_max_blank_lines_between_tags=2 -resharper_xml_max_line_length=120 -resharper_xml_pi_attribute_style=do_not_touch -resharper_xml_space_before_self_closing=true -resharper_xml_wrap_lines=true -resharper_xml_wrap_tags_and_pi=true -resharper_xml_wrap_text=false - -# ReSharper inspection severities -resharper_abstract_class_constructor_can_be_made_protected_highlighting=hint -resharper_access_rights_in_text_highlighting=warning -resharper_access_to_disposed_closure_highlighting=warning -resharper_access_to_for_each_variable_in_closure_highlighting=warning -resharper_access_to_modified_closure_highlighting=warning -resharper_access_to_static_member_via_derived_type_highlighting=warning -resharper_address_of_marshal_by_ref_object_highlighting=warning -resharper_amd_dependency_path_problem_highlighting=none -resharper_amd_external_module_highlighting=suggestion -resharper_angular_html_banana_highlighting=warning -resharper_annotate_can_be_null_parameter_highlighting=none -resharper_annotate_can_be_null_type_member_highlighting=none -resharper_annotate_not_null_parameter_highlighting=none -resharper_annotate_not_null_type_member_highlighting=none -resharper_annotation_conflict_in_hierarchy_highlighting=warning -resharper_annotation_redundancy_at_value_type_highlighting=warning -resharper_annotation_redundancy_in_hierarchy_highlighting=warning -resharper_arguments_style_anonymous_function_highlighting=hint -resharper_arguments_style_literal_highlighting=hint -resharper_arguments_style_named_expression_highlighting=hint -resharper_arguments_style_other_highlighting=hint -resharper_arguments_style_string_literal_highlighting=hint -resharper_arrange_accessor_owner_body_highlighting=suggestion -resharper_arrange_attributes_highlighting=none -resharper_arrange_constructor_or_destructor_body_highlighting=hint -resharper_arrange_default_value_when_type_evident_highlighting=suggestion -resharper_arrange_default_value_when_type_not_evident_highlighting=hint -resharper_arrange_local_function_body_highlighting=hint -resharper_arrange_method_or_operator_body_highlighting=hint -resharper_arrange_missing_parentheses_highlighting=hint -resharper_arrange_namespace_body_highlighting=hint -resharper_arrange_object_creation_when_type_evident_highlighting=suggestion -resharper_arrange_object_creation_when_type_not_evident_highlighting=hint -resharper_arrange_redundant_parentheses_highlighting=hint -resharper_arrange_static_member_qualifier_highlighting=hint -resharper_arrange_this_qualifier_highlighting=hint -resharper_arrange_trailing_comma_in_multiline_lists_highlighting=hint -resharper_arrange_trailing_comma_in_singleline_lists_highlighting=hint -resharper_arrange_type_member_modifiers_highlighting=hint -resharper_arrange_type_modifiers_highlighting=hint -resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting=suggestion -resharper_asp_content_placeholder_not_resolved_highlighting=error -resharper_asp_custom_page_parser_filter_type_highlighting=warning -resharper_asp_dead_code_highlighting=warning -resharper_asp_entity_highlighting=warning -resharper_asp_image_highlighting=warning -resharper_asp_invalid_control_type_highlighting=error -resharper_asp_not_resolved_highlighting=error -resharper_asp_ods_method_reference_resolve_error_highlighting=error -resharper_asp_resolve_warning_highlighting=warning -resharper_asp_skin_not_resolved_highlighting=error -resharper_asp_tag_attribute_with_optional_value_highlighting=warning -resharper_asp_theme_not_resolved_highlighting=error -resharper_asp_unused_register_directive_highlighting_highlighting=warning -resharper_asp_warning_highlighting=warning -resharper_assigned_value_is_never_used_highlighting=warning -resharper_assigned_value_wont_be_assigned_to_corresponding_field_highlighting=warning -resharper_assignment_in_conditional_expression_highlighting=warning -resharper_assignment_in_condition_expression_highlighting=warning -resharper_assignment_is_fully_discarded_highlighting=warning -resharper_assign_null_to_not_null_attribute_highlighting=warning -resharper_assign_to_constant_highlighting=error -resharper_assign_to_implicit_global_in_function_scope_highlighting=warning -resharper_asxx_path_error_highlighting=warning -resharper_async_iterator_invocation_without_await_foreach_highlighting=warning -resharper_async_void_lambda_highlighting=warning -resharper_async_void_method_highlighting=none -resharper_auto_property_can_be_made_get_only_global_highlighting=suggestion -resharper_auto_property_can_be_made_get_only_local_highlighting=suggestion -resharper_bad_attribute_brackets_spaces_highlighting=none -resharper_bad_braces_spaces_highlighting=none -resharper_bad_child_statement_indent_highlighting=warning -resharper_bad_colon_spaces_highlighting=none -resharper_bad_comma_spaces_highlighting=none -resharper_bad_control_braces_indent_highlighting=suggestion -resharper_bad_control_braces_line_breaks_highlighting=none -resharper_bad_declaration_braces_indent_highlighting=none -resharper_bad_declaration_braces_line_breaks_highlighting=none -resharper_bad_empty_braces_line_breaks_highlighting=none -resharper_bad_expression_braces_indent_highlighting=none -resharper_bad_expression_braces_line_breaks_highlighting=none -resharper_bad_generic_brackets_spaces_highlighting=none -resharper_bad_indent_highlighting=none -resharper_bad_linq_line_breaks_highlighting=none -resharper_bad_list_line_breaks_highlighting=none -resharper_bad_member_access_spaces_highlighting=none -resharper_bad_namespace_braces_indent_highlighting=none -resharper_bad_parens_line_breaks_highlighting=none -resharper_bad_parens_spaces_highlighting=none -resharper_bad_preprocessor_indent_highlighting=none -resharper_bad_semicolon_spaces_highlighting=none -resharper_bad_spaces_after_keyword_highlighting=none -resharper_bad_square_brackets_spaces_highlighting=none -resharper_bad_switch_braces_indent_highlighting=none -resharper_bad_symbol_spaces_highlighting=none -resharper_base_member_has_params_highlighting=warning -resharper_base_method_call_with_default_parameter_highlighting=warning -resharper_base_object_equals_is_object_equals_highlighting=warning -resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting=warning -resharper_bitwise_operator_on_enum_without_flags_highlighting=warning -resharper_block_scope_redeclaration_highlighting=error -resharper_built_in_type_reference_style_for_member_access_highlighting=hint -resharper_built_in_type_reference_style_highlighting=hint -resharper_by_ref_argument_is_volatile_field_highlighting=warning -resharper_caller_callee_using_error_highlighting=error -resharper_caller_callee_using_highlighting=warning -resharper_cannot_apply_equality_operator_to_type_highlighting=warning -resharper_center_tag_is_obsolete_highlighting=warning -resharper_check_for_reference_equality_instead_1_highlighting=suggestion -resharper_check_for_reference_equality_instead_2_highlighting=suggestion -resharper_check_for_reference_equality_instead_3_highlighting=suggestion -resharper_check_for_reference_equality_instead_4_highlighting=suggestion -resharper_check_namespace_highlighting=warning -resharper_class_cannot_be_instantiated_highlighting=warning -resharper_class_can_be_sealed_global_highlighting=none -resharper_class_can_be_sealed_local_highlighting=none -resharper_class_highlighting=suggestion -resharper_class_never_instantiated_global_highlighting=suggestion -resharper_class_never_instantiated_local_highlighting=suggestion -resharper_class_with_virtual_members_never_inherited_global_highlighting=suggestion -resharper_class_with_virtual_members_never_inherited_local_highlighting=suggestion -resharper_clear_attribute_is_obsolete_all_highlighting=warning -resharper_clear_attribute_is_obsolete_highlighting=warning -resharper_closure_on_modified_variable_highlighting=warning -resharper_coerced_equals_using_highlighting=warning -resharper_coerced_equals_using_with_null_undefined_highlighting=none -resharper_collection_never_queried_global_highlighting=warning -resharper_collection_never_queried_local_highlighting=warning -resharper_collection_never_updated_global_highlighting=warning -resharper_collection_never_updated_local_highlighting=warning -resharper_comma_not_valid_here_highlighting=error -resharper_comment_typo_highlighting=suggestion -resharper_common_js_external_module_highlighting=suggestion -resharper_compare_non_constrained_generic_with_null_highlighting=none -resharper_compare_of_floats_by_equality_operator_highlighting=none -resharper_conditional_ternary_equal_branch_highlighting=warning -resharper_condition_is_always_const_highlighting=warning -resharper_condition_is_always_true_or_false_highlighting=warning -resharper_confusing_char_as_integer_in_constructor_highlighting=warning -resharper_constant_conditional_access_qualifier_highlighting=warning -resharper_constant_null_coalescing_condition_highlighting=warning -resharper_constructor_call_not_used_highlighting=warning -resharper_constructor_initializer_loop_highlighting=warning -resharper_container_annotation_redundancy_highlighting=warning -resharper_context_value_is_provided_highlighting=none -resharper_contract_annotation_not_parsed_highlighting=warning -resharper_convert_closure_to_method_group_highlighting=suggestion -resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting=hint -resharper_convert_if_do_to_while_highlighting=suggestion -resharper_convert_if_statement_to_conditional_ternary_expression_highlighting=suggestion -resharper_convert_if_statement_to_null_coalescing_assignment_highlighting=suggestion -resharper_convert_if_statement_to_null_coalescing_expression_highlighting=suggestion -resharper_convert_if_statement_to_return_statement_highlighting=hint -resharper_convert_if_statement_to_switch_expression_highlighting=hint -resharper_convert_if_statement_to_switch_statement_highlighting=hint -resharper_convert_if_to_or_expression_highlighting=suggestion -resharper_convert_nullable_to_short_form_highlighting=suggestion -resharper_convert_switch_statement_to_switch_expression_highlighting=hint -resharper_convert_to_auto_property_highlighting=suggestion -resharper_convert_to_auto_property_when_possible_highlighting=hint -resharper_convert_to_auto_property_with_private_setter_highlighting=hint -resharper_convert_to_compound_assignment_highlighting=hint -resharper_convert_to_constant_global_highlighting=hint -resharper_convert_to_constant_local_highlighting=hint -resharper_convert_to_lambda_expression_highlighting=suggestion -resharper_convert_to_lambda_expression_when_possible_highlighting=none -resharper_convert_to_local_function_highlighting=suggestion -resharper_convert_to_null_coalescing_compound_assignment_highlighting=suggestion -resharper_convert_to_primary_constructor_highlighting=suggestion -resharper_convert_to_static_class_highlighting=suggestion -resharper_convert_to_using_declaration_highlighting=suggestion -resharper_convert_to_vb_auto_property_highlighting=suggestion -resharper_convert_to_vb_auto_property_when_possible_highlighting=hint -resharper_convert_to_vb_auto_property_with_private_setter_highlighting=hint -resharper_convert_type_check_pattern_to_null_check_highlighting=warning -resharper_convert_type_check_to_null_check_highlighting=warning -resharper_co_variant_array_conversion_highlighting=warning -resharper_cpp_abstract_class_without_specifier_highlighting=warning -resharper_cpp_abstract_final_class_highlighting=warning -resharper_cpp_abstract_virtual_function_call_in_ctor_highlighting=error -resharper_cpp_access_specifier_with_no_declarations_highlighting=suggestion -resharper_cpp_assigned_value_is_never_used_highlighting=warning -resharper_cpp_awaiter_type_is_not_class_highlighting=warning -resharper_cpp_bad_angle_brackets_spaces_highlighting=none -resharper_cpp_bad_braces_spaces_highlighting=none -resharper_cpp_bad_child_statement_indent_highlighting=none -resharper_cpp_bad_colon_spaces_highlighting=none -resharper_cpp_bad_comma_spaces_highlighting=none -resharper_cpp_bad_control_braces_indent_highlighting=none -resharper_cpp_bad_control_braces_line_breaks_highlighting=none -resharper_cpp_bad_declaration_braces_indent_highlighting=none -resharper_cpp_bad_declaration_braces_line_breaks_highlighting=none -resharper_cpp_bad_empty_braces_line_breaks_highlighting=none -resharper_cpp_bad_expression_braces_indent_highlighting=none -resharper_cpp_bad_expression_braces_line_breaks_highlighting=none -resharper_cpp_bad_indent_highlighting=none -resharper_cpp_bad_list_line_breaks_highlighting=none -resharper_cpp_bad_member_access_spaces_highlighting=none -resharper_cpp_bad_namespace_braces_indent_highlighting=none -resharper_cpp_bad_parens_line_breaks_highlighting=none -resharper_cpp_bad_parens_spaces_highlighting=none -resharper_cpp_bad_semicolon_spaces_highlighting=none -resharper_cpp_bad_spaces_after_keyword_highlighting=none -resharper_cpp_bad_square_brackets_spaces_highlighting=none -resharper_cpp_bad_switch_braces_indent_highlighting=none -resharper_cpp_bad_symbol_spaces_highlighting=none -resharper_cpp_boolean_increment_expression_highlighting=warning -resharper_cpp_boost_format_bad_code_highlighting=warning -resharper_cpp_boost_format_legacy_code_highlighting=suggestion -resharper_cpp_boost_format_mixed_args_highlighting=error -resharper_cpp_boost_format_too_few_args_highlighting=error -resharper_cpp_boost_format_too_many_args_highlighting=warning -resharper_cpp_clang_tidy_abseil_duration_addition_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_comparison_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_conversion_cast_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_division_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_factory_float_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_factory_scale_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_subtraction_highlighting=none -resharper_cpp_clang_tidy_abseil_duration_unnecessary_conversion_highlighting=none -resharper_cpp_clang_tidy_abseil_faster_strsplit_delimiter_highlighting=none -resharper_cpp_clang_tidy_abseil_no_internal_dependencies_highlighting=none -resharper_cpp_clang_tidy_abseil_no_namespace_highlighting=none -resharper_cpp_clang_tidy_abseil_redundant_strcat_calls_highlighting=none -resharper_cpp_clang_tidy_abseil_string_find_startswith_highlighting=none -resharper_cpp_clang_tidy_abseil_string_find_str_contains_highlighting=none -resharper_cpp_clang_tidy_abseil_str_cat_append_highlighting=none -resharper_cpp_clang_tidy_abseil_time_comparison_highlighting=none -resharper_cpp_clang_tidy_abseil_time_subtraction_highlighting=none -resharper_cpp_clang_tidy_abseil_upgrade_duration_conversions_highlighting=none -resharper_cpp_clang_tidy_altera_id_dependent_backward_branch_highlighting=none -resharper_cpp_clang_tidy_altera_kernel_name_restriction_highlighting=none -resharper_cpp_clang_tidy_altera_single_work_item_barrier_highlighting=none -resharper_cpp_clang_tidy_altera_struct_pack_align_highlighting=none -resharper_cpp_clang_tidy_altera_unroll_loops_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_accept4_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_accept_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_creat_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_dup_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_epoll_create1_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_epoll_create_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_fopen_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_inotify_init1_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_inotify_init_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_memfd_create_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_open_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_pipe2_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_pipe_highlighting=none -resharper_cpp_clang_tidy_android_cloexec_socket_highlighting=none -resharper_cpp_clang_tidy_android_comparison_in_temp_failure_retry_highlighting=none -resharper_cpp_clang_tidy_boost_use_to_string_highlighting=suggestion -resharper_cpp_clang_tidy_bugprone_argument_comment_highlighting=suggestion -resharper_cpp_clang_tidy_bugprone_assert_side_effect_highlighting=warning -resharper_cpp_clang_tidy_bugprone_bad_signal_to_kill_thread_highlighting=warning -resharper_cpp_clang_tidy_bugprone_bool_pointer_implicit_conversion_highlighting=none -resharper_cpp_clang_tidy_bugprone_branch_clone_highlighting=warning -resharper_cpp_clang_tidy_bugprone_copy_constructor_init_highlighting=warning -resharper_cpp_clang_tidy_bugprone_dangling_handle_highlighting=warning -resharper_cpp_clang_tidy_bugprone_dynamic_static_initializers_highlighting=warning -resharper_cpp_clang_tidy_bugprone_easily_swappable_parameters_highlighting=none -resharper_cpp_clang_tidy_bugprone_exception_escape_highlighting=none -resharper_cpp_clang_tidy_bugprone_fold_init_type_highlighting=warning -resharper_cpp_clang_tidy_bugprone_forwarding_reference_overload_highlighting=warning -resharper_cpp_clang_tidy_bugprone_forward_declaration_namespace_highlighting=warning -resharper_cpp_clang_tidy_bugprone_implicit_widening_of_multiplication_result_highlighting=warning -resharper_cpp_clang_tidy_bugprone_inaccurate_erase_highlighting=warning -resharper_cpp_clang_tidy_bugprone_incorrect_roundings_highlighting=warning -resharper_cpp_clang_tidy_bugprone_infinite_loop_highlighting=warning -resharper_cpp_clang_tidy_bugprone_integer_division_highlighting=warning -resharper_cpp_clang_tidy_bugprone_lambda_function_name_highlighting=warning -resharper_cpp_clang_tidy_bugprone_macro_parentheses_highlighting=warning -resharper_cpp_clang_tidy_bugprone_macro_repeated_side_effects_highlighting=warning -resharper_cpp_clang_tidy_bugprone_misplaced_operator_in_strlen_in_alloc_highlighting=warning -resharper_cpp_clang_tidy_bugprone_misplaced_pointer_arithmetic_in_alloc_highlighting=warning -resharper_cpp_clang_tidy_bugprone_misplaced_widening_cast_highlighting=warning -resharper_cpp_clang_tidy_bugprone_move_forwarding_reference_highlighting=warning -resharper_cpp_clang_tidy_bugprone_multiple_statement_macro_highlighting=warning -resharper_cpp_clang_tidy_bugprone_narrowing_conversions_highlighting=warning -resharper_cpp_clang_tidy_bugprone_not_null_terminated_result_highlighting=warning -resharper_cpp_clang_tidy_bugprone_no_escape_highlighting=warning -resharper_cpp_clang_tidy_bugprone_parent_virtual_call_highlighting=warning -resharper_cpp_clang_tidy_bugprone_posix_return_highlighting=warning -resharper_cpp_clang_tidy_bugprone_redundant_branch_condition_highlighting=warning -resharper_cpp_clang_tidy_bugprone_reserved_identifier_highlighting=warning -resharper_cpp_clang_tidy_bugprone_signal_handler_highlighting=warning -resharper_cpp_clang_tidy_bugprone_signed_char_misuse_highlighting=warning -resharper_cpp_clang_tidy_bugprone_sizeof_container_highlighting=warning -resharper_cpp_clang_tidy_bugprone_sizeof_expression_highlighting=warning -resharper_cpp_clang_tidy_bugprone_spuriously_wake_up_functions_highlighting=warning -resharper_cpp_clang_tidy_bugprone_string_constructor_highlighting=warning -resharper_cpp_clang_tidy_bugprone_string_integer_assignment_highlighting=warning -resharper_cpp_clang_tidy_bugprone_string_literal_with_embedded_nul_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_enum_usage_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_include_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_memset_usage_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_missing_comma_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_semicolon_highlighting=warning -resharper_cpp_clang_tidy_bugprone_suspicious_string_compare_highlighting=warning -resharper_cpp_clang_tidy_bugprone_swapped_arguments_highlighting=warning -resharper_cpp_clang_tidy_bugprone_terminating_continue_highlighting=warning -resharper_cpp_clang_tidy_bugprone_throw_keyword_missing_highlighting=warning -resharper_cpp_clang_tidy_bugprone_too_small_loop_variable_highlighting=warning -resharper_cpp_clang_tidy_bugprone_undefined_memory_manipulation_highlighting=warning -resharper_cpp_clang_tidy_bugprone_undelegated_constructor_highlighting=warning -resharper_cpp_clang_tidy_bugprone_unhandled_exception_at_new_highlighting=none -resharper_cpp_clang_tidy_bugprone_unhandled_self_assignment_highlighting=warning -resharper_cpp_clang_tidy_bugprone_unused_raii_highlighting=warning -resharper_cpp_clang_tidy_bugprone_unused_return_value_highlighting=warning -resharper_cpp_clang_tidy_bugprone_use_after_move_highlighting=warning -resharper_cpp_clang_tidy_bugprone_virtual_near_miss_highlighting=suggestion -resharper_cpp_clang_tidy_cert_con36_c_highlighting=none -resharper_cpp_clang_tidy_cert_con54_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_dcl03_c_highlighting=none -resharper_cpp_clang_tidy_cert_dcl16_c_highlighting=none -resharper_cpp_clang_tidy_cert_dcl21_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_dcl37_c_highlighting=none -resharper_cpp_clang_tidy_cert_dcl50_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_dcl51_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_dcl54_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_dcl58_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_dcl59_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_env33_c_highlighting=none -resharper_cpp_clang_tidy_cert_err09_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_err34_c_highlighting=suggestion -resharper_cpp_clang_tidy_cert_err52_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_err58_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_err60_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_err61_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_fio38_c_highlighting=none -resharper_cpp_clang_tidy_cert_flp30_c_highlighting=warning -resharper_cpp_clang_tidy_cert_mem57_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_msc30_c_highlighting=none -resharper_cpp_clang_tidy_cert_msc32_c_highlighting=none -resharper_cpp_clang_tidy_cert_msc50_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_msc51_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_oop11_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_oop54_cpp_highlighting=none -resharper_cpp_clang_tidy_cert_oop57_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_oop58_cpp_highlighting=warning -resharper_cpp_clang_tidy_cert_pos44_c_highlighting=none -resharper_cpp_clang_tidy_cert_pos47_c_highlighting=none -resharper_cpp_clang_tidy_cert_sig30_c_highlighting=none -resharper_cpp_clang_tidy_cert_str34_c_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_api_modeling_google_g_test_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_cast_value_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_return_value_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_api_modeling_std_c_library_functions_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_api_modeling_trust_nonnull_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_builtin_builtin_functions_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_builtin_no_return_functions_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_modeling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_divide_zero_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_dynamic_type_propagation_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_nonnil_string_constants_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_non_null_param_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_null_dereference_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_stack_address_escape_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_stack_addr_escape_base_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_undefined_binary_operator_result_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_array_subscript_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_assign_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_branch_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_captured_block_variable_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_undef_return_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_core_vla_size_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_inner_pointer_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_move_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_leaks_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_placement_new_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_pure_virtual_call_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_self_assignment_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_smart_ptr_modeling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_cplusplus_virtual_call_modeling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_deadcode_dead_stores_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_fuchsia_handle_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_nullability_base_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_dereferenced_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_passed_to_nonnull_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_returned_from_nonnull_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_null_passed_to_nonnull_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_nullability_null_returned_from_nonnull_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_uninitialized_object_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_virtual_call_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_mpi_mpi_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_empty_localization_context_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_non_localized_string_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_osx_os_object_c_style_cast_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_performance_gcd_antipattern_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_performance_padding_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_optin_portability_unix_api_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_api_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_at_sync_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_autorelease_write_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_class_release_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_dealloc_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_incompatible_method_types_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_loops_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_missing_super_call_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_nil_arg_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_non_nil_return_value_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_autorelease_pool_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_error_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_obj_c_generics_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_base_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_run_loop_autorelease_leak_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_self_init_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_super_dealloc_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_unused_ivars_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_variadic_method_types_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_error_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_number_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_retain_release_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_out_of_bounds_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_pointer_sized_values_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_mig_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_ns_or_cf_error_deref_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_number_object_conversion_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_obj_c_property_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_os_object_retain_count_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_osx_sec_keychain_api_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_float_loop_counter_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcmp_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcopy_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bzero_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_decode_value_of_obj_c_type_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_deprecated_or_unsafe_buffer_handling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_getpw_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_gets_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mkstemp_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mktemp_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_rand_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_security_syntax_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_strcpy_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_unchecked_return_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_vfork_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_api_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_bad_size_arg_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_c_string_modeling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_null_arg_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_dynamic_memory_modeling_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_sizeof_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_mismatched_deallocator_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_unix_vfork_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_valist_copy_to_self_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_valist_uninitialized_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_valist_unterminated_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_valist_valist_base_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_webkit_no_uncounted_member_checker_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_webkit_ref_cntbl_base_virtual_dtor_highlighting=none -resharper_cpp_clang_tidy_clang_analyzer_webkit_uncounted_lambda_captures_checker_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_absolute_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_abstract_final_class_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_abstract_vbase_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_address_of_packed_member_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_address_of_temporary_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_aix_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_align_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_alloca_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_alloca_with_align_alignof_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_delete_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_ellipsis_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_macro_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_member_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_reversed_operator_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_analyzer_incompatible_plugin_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_anonymous_pack_parens_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_anon_enum_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_bridge_casts_disallowed_in_nonarc_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_maybe_repeated_use_of_weak_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_non_pod_memaccess_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_perform_selector_leaks_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_repeated_use_of_weak_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_retain_cycles_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_arc_unsafe_retained_assign_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_argument_outside_range_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_pointer_arithmetic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_asm_operand_widths_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_assign_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_assume_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_atimport_in_framework_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_atomic_alignment_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_atomic_implicit_seq_cst_highlighting=suggestion -resharper_cpp_clang_tidy_clang_diagnostic_atomic_memory_ordering_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_atomic_property_with_user_defined_accessor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_attribute_packed_for_bitfield_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_at_protocol_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_auto_disable_vptr_sanitizer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_auto_import_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_auto_storage_class_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_auto_var_id_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_availability_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_avr_rtlib_linking_quirks_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_backslash_newline_escape_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bad_function_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_binding_in_condition_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bind_to_temporary_copy_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bitfield_constant_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bitfield_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bitfield_width_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bitwise_conditional_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bitwise_op_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_block_capture_autoreleasing_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bool_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bool_operation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_braced_scalar_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_bridge_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_builtin_assume_aligned_alignment_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_builtin_macro_redefined_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_builtin_memcpy_chk_size_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_builtin_requires_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_c11_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_c2x_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_c99_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_c99_designator_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_c99_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_called_once_parameter_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_call_to_pure_virtual_from_ctor_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_align_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_calling_convention_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_function_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_of_sel_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_unrelated_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cf_string_literal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_char_subscripts_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_clang_cl_pch_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_class_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_class_varargs_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cmse_union_leak_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_comma_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_comment_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_compare_distinct_pointer_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_completion_handler_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_complex_component_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_macro_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_space_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_concepts_ts_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_conditional_type_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_conditional_uninitialized_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_config_macros_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_constant_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_constant_evaluated_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_constant_logical_operand_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_constexpr_not_const_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_consumed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_coroutine_missing_unhandled_exception_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_covered_switch_default_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_deprecated_writable_strings_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_reserved_user_defined_literal_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extra_semi_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_inline_namespace_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_long_long_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp11_narrowing_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp14_binary_literal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp14_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_mangling_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp17_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp20_designator_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp20_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp2b_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_bind_to_temporary_copy_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_extra_semi_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_local_type_template_args_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_unnamed_type_template_args_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_binary_literal_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cpp_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cstring_format_directive_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ctad_maybe_unsupported_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_ctu_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_cuda_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_custom_atomic_properties_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_cxx_attribute_extension_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dangling_else_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dangling_field_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dangling_gsl_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dangling_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dangling_initializer_list_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_darwin_sdk_settings_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_date_time_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dealloc_in_category_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_debug_compression_unavailable_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_declaration_after_statement_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_defaulted_function_deleted_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_delegating_ctor_cycles_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_delete_abstract_non_virtual_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_delete_incomplete_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_delete_non_abstract_non_virtual_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_delete_non_virtual_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_altivec_src_compat_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_anon_enum_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_array_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_attributes_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_comma_subscript_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_copy_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_declarations_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_dynamic_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_conditional_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_implementations_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_increment_bool_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_isa_usage_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_perform_selector_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_register_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_this_capture_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_deprecated_volatile_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_direct_ivar_access_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_disabled_macro_expansion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_distributed_object_modifiers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_division_by_zero_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dllexport_explicit_instantiation_decl_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dllimport_static_field_def_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dll_attribute_on_redeclaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_documentation_deprecated_sync_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_documentation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_documentation_html_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_documentation_pedantic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_documentation_unknown_command_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_dollar_in_identifier_extension_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_double_promotion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dtor_name_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dtor_typedef_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_duplicate_decl_specifier_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_duplicate_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_arg_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_match_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_duplicate_protocol_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dynamic_class_memaccess_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_dynamic_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_embedded_directive_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_empty_body_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_empty_decomposition_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_empty_init_stmt_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_empty_translation_unit_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_encode_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_conditional_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_switch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_enum_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_enum_too_large_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_error_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_exceptions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_excess_initializers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_exit_time_destructors_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_expansion_to_defined_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_explicit_initialize_call_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_explicit_ownership_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_export_unnamed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_export_using_directive_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_extern_c_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_extern_initializer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_extra_qualification_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_stmt_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_extra_tokens_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_final_dtor_non_final_class_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_fixed_enum_extension_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_fixed_point_overflow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_flag_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_flexible_array_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_float_equal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_float_overflow_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_float_zero_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_extra_args_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_insufficient_args_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_invalid_specifier_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_nonliteral_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_non_iso_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_pedantic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_security_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_type_confusion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_format_zero_length_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_fortify_source_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_for_loop_analysis_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_four_char_constants_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_framework_include_private_from_public_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_frame_address_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_frame_larger_than_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_free_nonheap_object_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_function_def_in_objc_container_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_function_multiversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gcc_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_global_constructors_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_global_isel_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_alignof_expression_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_anonymous_struct_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_gnu_array_member_paren_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_auto_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_binary_literal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_case_range_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_complex_integer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_compound_literal_initializer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_conditional_omitted_operand_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_designator_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_initializer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_struct_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_initializer_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_union_member_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_folding_constant_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_imaginary_constant_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_include_next_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_inline_cpp_without_extern_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_label_as_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_redeclared_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_statement_expression_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_static_float_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_string_literal_operator_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_union_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_variable_sized_type_not_at_end_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_gnu_zero_variadic_macro_arguments_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_header_guard_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_header_hygiene_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_hip_only_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_idiomatic_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ignored_attributes_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_ignored_availability_without_sdk_settings_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_ignored_optimization_argument_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragmas_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_intrinsic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_optimize_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_ignored_qualifiers_highlighting=suggestion -resharper_cpp_clang_tidy_clang_diagnostic_implicitly_unsigned_literal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_atomic_properties_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_const_int_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_conversion_floating_point_to_bool_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_exception_spec_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_per_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_fixed_point_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_function_declaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_float_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_implicit_retain_self_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_import_preprocessor_directive_pedantic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inaccessible_base_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_include_next_absolute_path_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_include_next_outside_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_function_pointer_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_library_redeclaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_ms_struct_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_discards_qualifiers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_property_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incompatible_sysroot_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incomplete_framework_module_declaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incomplete_implementation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incomplete_module_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incomplete_setjmp_declaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_incomplete_umbrella_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_dllimport_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_destructor_override_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_override_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_increment_bool_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_independent_class_attribute_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_infinite_recursion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_initializer_overrides_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_injected_class_name_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inline_asm_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inline_namespace_reopened_noninline_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_inline_new_delete_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_instantiation_after_specialization_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_integer_overflow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_interrupt_service_routine_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_int_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_int_in_bool_context_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_int_to_pointer_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_int_to_void_pointer_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_constexpr_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_iboutlet_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_initializer_from_system_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_ios_deployment_target_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_noreturn_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_no_builtin_names_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_offsetof_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_or_nonexistent_directory_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_partial_specialization_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_pp_token_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_source_encoding_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_invalid_token_paste_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_jump_seh_finally_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_keyword_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_keyword_macro_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_knr_promoted_parameter_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_language_extension_token_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_large_by_value_copy_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_literal_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_literal_range_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_local_type_template_args_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_logical_not_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_logical_op_parentheses_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_long_long_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_macro_redefined_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_main_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_main_return_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_malformed_warning_check_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_many_braces_around_scalar_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_max_tokens_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_max_unsigned_zero_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_memset_transposed_args_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_memsize_comparison_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_method_signatures_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_abstract_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_anon_tag_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_charize_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_comment_paste_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_const_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cpp_macro_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_default_arg_redefinition_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_drectve_section_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_end_of_file_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_forward_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exists_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_explicit_constructor_call_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_extra_qualification_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_fixed_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_flexible_array_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_goto_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_inaccessible_base_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_include_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_mutable_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_pure_definition_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_redeclare_static_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_sealed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_static_assert_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_shadow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_union_member_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_unqualified_friend_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_using_decl_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_microsoft_void_pseudo_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_misleading_indentation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_mismatched_new_delete_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_mismatched_parameter_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_mismatched_return_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_mismatched_tags_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_braces_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_constinit_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_declarations_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_field_initializers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_method_return_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_noescape_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_noreturn_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_prototypes_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_missing_prototype_for_cc_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_selector_name_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_sysroot_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_missing_variable_declarations_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_misspelled_assumption_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_modules_ambiguous_internal_linkage_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_modules_import_nested_redundant_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_module_conflict_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_module_file_config_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_module_file_extension_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_module_import_in_extern_c_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_msvc_not_found_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_multichar_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_multiple_move_vbase_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nested_anon_types_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_newline_eof_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_new_returns_null_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_noderef_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nonnull_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nonportable_include_path_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nonportable_system_include_path_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nonportable_vector_initialization_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nontrivial_memaccess_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_c_typedef_for_linkage_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_literal_null_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_framework_module_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_module_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_pod_varargs_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_power_of_two_alignment_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_non_virtual_dtor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nsconsumed_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nsreturns_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ns_object_attribute_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_on_arrays_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_declspec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_extension_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullability_inferred_on_nested_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_nullable_to_nonnull_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_arithmetic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_character_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_dereference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_arithmetic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_subtraction_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_odr_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_old_style_cast_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_opencl_unsupported_rgba_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_openmp51_extensions_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_openmp_clauses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_openmp_loop_form_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_openmp_mapping_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_openmp_target_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_option_ignored_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_ordered_compare_function_pointers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_out_of_line_declaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_out_of_scope_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_overlength_strings_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_overloaded_shift_op_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_overloaded_virtual_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_override_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_override_module_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_overriding_method_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_overriding_t_option_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_over_aligned_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_packed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_padded_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_parentheses_equality_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pass_failed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pch_date_time_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pedantic_core_features_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pedantic_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pessimizing_move_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_arith_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_bool_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_integer_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_sign_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_enum_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_int_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pointer_type_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_poison_system_directories_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_potentially_evaluated_expression_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragmas_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_clang_attribute_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_messages_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_once_outside_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_suspicious_include_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pragma_system_header_outside_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_predefined_identifier_outside_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_pedantic_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_pre_openmp51_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_private_extern_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_private_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_private_module_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_missing_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_out_of_date_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_unprofiled_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_property_access_dot_syntax_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_property_attribute_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_protocol_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_protocol_property_synthesis_ambiguity_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_psabi_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_qualified_void_return_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_quoted_include_in_framework_header_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_range_loop_analysis_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_range_loop_bind_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_range_loop_construct_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_readonly_iboutlet_property_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_receiver_expr_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_receiver_forward_class_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_redeclared_class_member_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_redundant_move_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_redundant_parens_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_register_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reinterpret_base_class_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reorder_ctor_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reorder_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reorder_init_list_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_requires_expression_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_requires_super_attribute_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reserved_identifier_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reserved_id_macro_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reserved_macro_identifier_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_reserved_user_defined_literal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_retained_language_linkage_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_return_stack_address_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_return_std_move_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_return_type_c_linkage_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_return_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_rewrite_not_bool_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_section_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_selector_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_selector_type_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_self_assign_field_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_self_assign_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_self_assign_overloaded_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_self_move_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_semicolon_before_method_body_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sentinel_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_serialized_diagnostics_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_modified_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_shadow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shadow_ivar_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shadow_uncaptured_local_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_shift_count_negative_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shift_count_overflow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shift_negative_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shift_op_parentheses_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shift_overflow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shift_sign_overflow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_shorten64_to32_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_signed_enum_bitfield_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_signed_unsigned_wchar_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sign_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sign_conversion_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_argument_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_decay_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_div_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_div_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_memaccess_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_slash_u_filename_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_slh_asm_goto_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_sometimes_uninitialized_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_source_uses_openmp_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_spir_compat_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_static_float_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_static_inline_explicit_instantiation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_static_in_inline_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_static_local_in_inline_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_static_self_init_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_stdlibcxx_not_found_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_strict_prototypes_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_strict_selector_match_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_string_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_string_concatenation_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_string_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_string_plus_char_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_string_plus_int_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_strlcpy_strlcat_size_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_strncat_size_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_suggest_destructor_override_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_suggest_override_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_super_class_method_mismatch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_suspicious_bzero_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_switch_bool_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_switch_enum_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_switch_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_sync_fetch_and_nand_semantics_changed_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_bitwise_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_in_range_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_out_of_range_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_objc_bool_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_overlap_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_pointer_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_type_limit_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_undefined_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_char_zero_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_enum_zero_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_zero_compare_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_tautological_value_range_compare_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_tentative_definition_incomplete_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_analysis_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_attributes_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_beta_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_negative_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_precise_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_verbose_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_trigraphs_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_typedef_redefinition_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_typename_missing_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_type_safety_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unable_to_open_stats_file_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unavailable_declarations_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undeclared_selector_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_bool_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_func_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_inline_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_type_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_reinterpret_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undefined_var_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undef_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_undef_prefix_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_underaligned_exception_object_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unevaluated_expression_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_new_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unicode_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unicode_homoglyph_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unicode_whitespace_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unicode_zero_width_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_const_reference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unknown_argument_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unknown_attributes_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unknown_cuda_version_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unknown_escape_sequence_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unknown_pragmas_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unknown_sanitizers_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unknown_warning_option_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unnamed_type_template_args_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unneeded_internal_declaration_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unneeded_member_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_break_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_loop_increment_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_return_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsequenced_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_abs_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_availability_guard_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_cb_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_dll_base_class_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_friend_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_gpopt_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_nan_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_target_opt_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unsupported_visibility_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unusable_partial_specialization_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_parameter_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_variable_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unused_comparison_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_const_variable_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_exception_parameter_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_getter_return_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_label_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_lambda_capture_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unused_local_typedef_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_macros_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_member_function_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_parameter_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unused_private_field_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_property_ivar_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_result_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_template_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_value_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_unused_variable_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_unused_volatile_lvalue_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_used_but_marked_unused_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_user_defined_literals_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_user_defined_warnings_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_varargs_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_variadic_macros_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_vector_conversion_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_vec_elem_size_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_vexing_parse_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_visibility_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_vla_extension_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_vla_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_enum_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_int_cast_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_void_ptr_dereference_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_warnings_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_wasm_exception_spec_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_weak_template_vtables_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_weak_vtables_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_writable_strings_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_xor_used_as_pow_highlighting=warning -resharper_cpp_clang_tidy_clang_diagnostic_zero_as_null_pointer_constant_highlighting=none -resharper_cpp_clang_tidy_clang_diagnostic_zero_length_array_highlighting=warning -resharper_cpp_clang_tidy_concurrency_mt_unsafe_highlighting=warning -resharper_cpp_clang_tidy_concurrency_thread_canceltype_asynchronous_highlighting=warning -resharper_cpp_clang_tidy_cppcoreguidelines_avoid_c_arrays_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_avoid_goto_highlighting=warning -resharper_cpp_clang_tidy_cppcoreguidelines_avoid_magic_numbers_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_avoid_non_const_global_variables_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_c_copy_assignment_signature_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_explicit_virtual_functions_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_init_variables_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_interfaces_global_init_highlighting=warning -resharper_cpp_clang_tidy_cppcoreguidelines_macro_usage_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_narrowing_conversions_highlighting=warning -resharper_cpp_clang_tidy_cppcoreguidelines_non_private_member_variables_in_classes_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_no_malloc_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_owning_memory_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_prefer_member_initializer_highlighting=suggestion -resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_array_to_pointer_decay_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_constant_array_index_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_pointer_arithmetic_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_const_cast_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_cstyle_cast_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_member_init_highlighting=warning -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_reinterpret_cast_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_static_cast_downcast_highlighting=suggestion -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_union_access_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_vararg_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_slicing_highlighting=none -resharper_cpp_clang_tidy_cppcoreguidelines_special_member_functions_highlighting=suggestion -resharper_cpp_clang_tidy_darwin_avoid_spinlock_highlighting=none -resharper_cpp_clang_tidy_darwin_dispatch_once_nonstatic_highlighting=none -resharper_cpp_clang_tidy_fuchsia_default_arguments_calls_highlighting=none -resharper_cpp_clang_tidy_fuchsia_default_arguments_declarations_highlighting=none -resharper_cpp_clang_tidy_fuchsia_header_anon_namespaces_highlighting=none -resharper_cpp_clang_tidy_fuchsia_multiple_inheritance_highlighting=none -resharper_cpp_clang_tidy_fuchsia_overloaded_operator_highlighting=none -resharper_cpp_clang_tidy_fuchsia_statically_constructed_objects_highlighting=none -resharper_cpp_clang_tidy_fuchsia_trailing_return_highlighting=none -resharper_cpp_clang_tidy_fuchsia_virtual_inheritance_highlighting=none -resharper_cpp_clang_tidy_google_build_explicit_make_pair_highlighting=none -resharper_cpp_clang_tidy_google_build_namespaces_highlighting=none -resharper_cpp_clang_tidy_google_build_using_namespace_highlighting=none -resharper_cpp_clang_tidy_google_default_arguments_highlighting=none -resharper_cpp_clang_tidy_google_explicit_constructor_highlighting=none -resharper_cpp_clang_tidy_google_global_names_in_headers_highlighting=none -resharper_cpp_clang_tidy_google_objc_avoid_nsobject_new_highlighting=none -resharper_cpp_clang_tidy_google_objc_avoid_throwing_exception_highlighting=none -resharper_cpp_clang_tidy_google_objc_function_naming_highlighting=none -resharper_cpp_clang_tidy_google_objc_global_variable_declaration_highlighting=none -resharper_cpp_clang_tidy_google_readability_avoid_underscore_in_googletest_name_highlighting=none -resharper_cpp_clang_tidy_google_readability_braces_around_statements_highlighting=none -resharper_cpp_clang_tidy_google_readability_casting_highlighting=none -resharper_cpp_clang_tidy_google_readability_function_size_highlighting=none -resharper_cpp_clang_tidy_google_readability_namespace_comments_highlighting=none -resharper_cpp_clang_tidy_google_readability_todo_highlighting=none -resharper_cpp_clang_tidy_google_runtime_int_highlighting=none -resharper_cpp_clang_tidy_google_runtime_operator_highlighting=warning -resharper_cpp_clang_tidy_google_upgrade_googletest_case_highlighting=suggestion -resharper_cpp_clang_tidy_hicpp_avoid_c_arrays_highlighting=none -resharper_cpp_clang_tidy_hicpp_avoid_goto_highlighting=warning -resharper_cpp_clang_tidy_hicpp_braces_around_statements_highlighting=none -resharper_cpp_clang_tidy_hicpp_deprecated_headers_highlighting=none -resharper_cpp_clang_tidy_hicpp_exception_baseclass_highlighting=suggestion -resharper_cpp_clang_tidy_hicpp_explicit_conversions_highlighting=none -resharper_cpp_clang_tidy_hicpp_function_size_highlighting=none -resharper_cpp_clang_tidy_hicpp_invalid_access_moved_highlighting=none -resharper_cpp_clang_tidy_hicpp_member_init_highlighting=none -resharper_cpp_clang_tidy_hicpp_move_const_arg_highlighting=none -resharper_cpp_clang_tidy_hicpp_multiway_paths_covered_highlighting=warning -resharper_cpp_clang_tidy_hicpp_named_parameter_highlighting=none -resharper_cpp_clang_tidy_hicpp_new_delete_operators_highlighting=none -resharper_cpp_clang_tidy_hicpp_noexcept_move_highlighting=none -resharper_cpp_clang_tidy_hicpp_no_array_decay_highlighting=none -resharper_cpp_clang_tidy_hicpp_no_assembler_highlighting=none -resharper_cpp_clang_tidy_hicpp_no_malloc_highlighting=none -resharper_cpp_clang_tidy_hicpp_signed_bitwise_highlighting=none -resharper_cpp_clang_tidy_hicpp_special_member_functions_highlighting=none -resharper_cpp_clang_tidy_hicpp_static_assert_highlighting=none -resharper_cpp_clang_tidy_hicpp_undelegated_constructor_highlighting=none -resharper_cpp_clang_tidy_hicpp_uppercase_literal_suffix_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_auto_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_emplace_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_equals_default_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_equals_delete_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_noexcept_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_nullptr_highlighting=none -resharper_cpp_clang_tidy_hicpp_use_override_highlighting=none -resharper_cpp_clang_tidy_hicpp_vararg_highlighting=none -resharper_cpp_clang_tidy_highlighting_highlighting=suggestion -resharper_cpp_clang_tidy_linuxkernel_must_check_errs_highlighting=warning -resharper_cpp_clang_tidy_llvmlibc_callee_namespace_highlighting=none -resharper_cpp_clang_tidy_llvmlibc_implementation_in_namespace_highlighting=none -resharper_cpp_clang_tidy_llvmlibc_restrict_system_libc_headers_highlighting=none -resharper_cpp_clang_tidy_llvm_else_after_return_highlighting=none -resharper_cpp_clang_tidy_llvm_header_guard_highlighting=none -resharper_cpp_clang_tidy_llvm_include_order_highlighting=none -resharper_cpp_clang_tidy_llvm_namespace_comment_highlighting=none -resharper_cpp_clang_tidy_llvm_prefer_isa_or_dyn_cast_in_conditionals_highlighting=none -resharper_cpp_clang_tidy_llvm_prefer_register_over_unsigned_highlighting=suggestion -resharper_cpp_clang_tidy_llvm_qualified_auto_highlighting=none -resharper_cpp_clang_tidy_llvm_twine_local_highlighting=none -resharper_cpp_clang_tidy_misc_definitions_in_headers_highlighting=none -resharper_cpp_clang_tidy_misc_misplaced_const_highlighting=warning -resharper_cpp_clang_tidy_misc_new_delete_overloads_highlighting=warning -resharper_cpp_clang_tidy_misc_non_copyable_objects_highlighting=warning -resharper_cpp_clang_tidy_misc_non_private_member_variables_in_classes_highlighting=none -resharper_cpp_clang_tidy_misc_no_recursion_highlighting=none -resharper_cpp_clang_tidy_misc_redundant_expression_highlighting=warning -resharper_cpp_clang_tidy_misc_static_assert_highlighting=suggestion -resharper_cpp_clang_tidy_misc_throw_by_value_catch_by_reference_highlighting=warning -resharper_cpp_clang_tidy_misc_unconventional_assign_operator_highlighting=warning -resharper_cpp_clang_tidy_misc_uniqueptr_reset_release_highlighting=suggestion -resharper_cpp_clang_tidy_misc_unused_alias_decls_highlighting=suggestion -resharper_cpp_clang_tidy_misc_unused_parameters_highlighting=none -resharper_cpp_clang_tidy_misc_unused_using_decls_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_avoid_bind_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_avoid_c_arrays_highlighting=none -resharper_cpp_clang_tidy_modernize_concat_nested_namespaces_highlighting=none -resharper_cpp_clang_tidy_modernize_deprecated_headers_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_deprecated_ios_base_aliases_highlighting=warning -resharper_cpp_clang_tidy_modernize_loop_convert_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_make_shared_highlighting=none -resharper_cpp_clang_tidy_modernize_make_unique_highlighting=none -resharper_cpp_clang_tidy_modernize_pass_by_value_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_raw_string_literal_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_redundant_void_arg_highlighting=none -resharper_cpp_clang_tidy_modernize_replace_auto_ptr_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_replace_disallow_copy_and_assign_macro_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_replace_random_shuffle_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_return_braced_init_list_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_shrink_to_fit_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_unary_static_assert_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_auto_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_bool_literals_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_default_member_init_highlighting=none -resharper_cpp_clang_tidy_modernize_use_emplace_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_equals_default_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_equals_delete_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_nodiscard_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_noexcept_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_nullptr_highlighting=none -resharper_cpp_clang_tidy_modernize_use_override_highlighting=none -resharper_cpp_clang_tidy_modernize_use_trailing_return_type_highlighting=none -resharper_cpp_clang_tidy_modernize_use_transparent_functors_highlighting=suggestion -resharper_cpp_clang_tidy_modernize_use_uncaught_exceptions_highlighting=warning -resharper_cpp_clang_tidy_modernize_use_using_highlighting=none -resharper_cpp_clang_tidy_mpi_buffer_deref_highlighting=warning -resharper_cpp_clang_tidy_mpi_type_mismatch_highlighting=warning -resharper_cpp_clang_tidy_objc_avoid_nserror_init_highlighting=warning -resharper_cpp_clang_tidy_objc_dealloc_in_category_highlighting=warning -resharper_cpp_clang_tidy_objc_forbidden_subclassing_highlighting=warning -resharper_cpp_clang_tidy_objc_missing_hash_highlighting=warning -resharper_cpp_clang_tidy_objc_nsinvocation_argument_lifetime_highlighting=warning -resharper_cpp_clang_tidy_objc_property_declaration_highlighting=warning -resharper_cpp_clang_tidy_objc_super_self_highlighting=warning -resharper_cpp_clang_tidy_openmp_exception_escape_highlighting=warning -resharper_cpp_clang_tidy_openmp_use_default_none_highlighting=warning -resharper_cpp_clang_tidy_performance_faster_string_find_highlighting=suggestion -resharper_cpp_clang_tidy_performance_for_range_copy_highlighting=suggestion -resharper_cpp_clang_tidy_performance_implicit_conversion_in_loop_highlighting=suggestion -resharper_cpp_clang_tidy_performance_inefficient_algorithm_highlighting=suggestion -resharper_cpp_clang_tidy_performance_inefficient_string_concatenation_highlighting=suggestion -resharper_cpp_clang_tidy_performance_inefficient_vector_operation_highlighting=suggestion -resharper_cpp_clang_tidy_performance_move_constructor_init_highlighting=warning -resharper_cpp_clang_tidy_performance_move_const_arg_highlighting=suggestion -resharper_cpp_clang_tidy_performance_noexcept_move_constructor_highlighting=none -resharper_cpp_clang_tidy_performance_no_automatic_move_highlighting=warning -resharper_cpp_clang_tidy_performance_no_int_to_ptr_highlighting=warning -resharper_cpp_clang_tidy_performance_trivially_destructible_highlighting=suggestion -resharper_cpp_clang_tidy_performance_type_promotion_in_math_fn_highlighting=suggestion -resharper_cpp_clang_tidy_performance_unnecessary_copy_initialization_highlighting=suggestion -resharper_cpp_clang_tidy_performance_unnecessary_value_param_highlighting=suggestion -resharper_cpp_clang_tidy_portability_restrict_system_includes_highlighting=none -resharper_cpp_clang_tidy_portability_simd_intrinsics_highlighting=none -resharper_cpp_clang_tidy_readability_avoid_const_params_in_decls_highlighting=none -resharper_cpp_clang_tidy_readability_braces_around_statements_highlighting=none -resharper_cpp_clang_tidy_readability_const_return_type_highlighting=none -resharper_cpp_clang_tidy_readability_container_size_empty_highlighting=suggestion -resharper_cpp_clang_tidy_readability_convert_member_functions_to_static_highlighting=none -resharper_cpp_clang_tidy_readability_delete_null_pointer_highlighting=suggestion -resharper_cpp_clang_tidy_readability_else_after_return_highlighting=none -resharper_cpp_clang_tidy_readability_function_cognitive_complexity_highlighting=none -resharper_cpp_clang_tidy_readability_function_size_highlighting=none -resharper_cpp_clang_tidy_readability_identifier_naming_highlighting=none -resharper_cpp_clang_tidy_readability_implicit_bool_conversion_highlighting=none -resharper_cpp_clang_tidy_readability_inconsistent_declaration_parameter_name_highlighting=suggestion -resharper_cpp_clang_tidy_readability_isolate_declaration_highlighting=none -resharper_cpp_clang_tidy_readability_magic_numbers_highlighting=none -resharper_cpp_clang_tidy_readability_make_member_function_const_highlighting=none -resharper_cpp_clang_tidy_readability_misleading_indentation_highlighting=none -resharper_cpp_clang_tidy_readability_misplaced_array_index_highlighting=suggestion -resharper_cpp_clang_tidy_readability_named_parameter_highlighting=none -resharper_cpp_clang_tidy_readability_non_const_parameter_highlighting=none -resharper_cpp_clang_tidy_readability_qualified_auto_highlighting=none -resharper_cpp_clang_tidy_readability_redundant_access_specifiers_highlighting=none -resharper_cpp_clang_tidy_readability_redundant_control_flow_highlighting=none -resharper_cpp_clang_tidy_readability_redundant_declaration_highlighting=suggestion -resharper_cpp_clang_tidy_readability_redundant_function_ptr_dereference_highlighting=suggestion -resharper_cpp_clang_tidy_readability_redundant_member_init_highlighting=none -resharper_cpp_clang_tidy_readability_redundant_preprocessor_highlighting=warning -resharper_cpp_clang_tidy_readability_redundant_smartptr_get_highlighting=suggestion -resharper_cpp_clang_tidy_readability_redundant_string_cstr_highlighting=suggestion -resharper_cpp_clang_tidy_readability_redundant_string_init_highlighting=suggestion -resharper_cpp_clang_tidy_readability_simplify_boolean_expr_highlighting=none -resharper_cpp_clang_tidy_readability_simplify_subscript_expr_highlighting=warning -resharper_cpp_clang_tidy_readability_static_accessed_through_instance_highlighting=suggestion -resharper_cpp_clang_tidy_readability_static_definition_in_anonymous_namespace_highlighting=none -resharper_cpp_clang_tidy_readability_string_compare_highlighting=warning -resharper_cpp_clang_tidy_readability_suspicious_call_argument_highlighting=warning -resharper_cpp_clang_tidy_readability_uniqueptr_delete_release_highlighting=suggestion -resharper_cpp_clang_tidy_readability_uppercase_literal_suffix_highlighting=none -resharper_cpp_clang_tidy_readability_use_anyofallof_highlighting=suggestion -resharper_cpp_clang_tidy_zircon_temporary_objects_highlighting=none -resharper_cpp_class_can_be_final_highlighting=hint -resharper_cpp_class_disallow_lazy_merging_highlighting=warning -resharper_cpp_class_is_incomplete_highlighting=warning -resharper_cpp_class_needs_constructor_because_of_uninitialized_member_highlighting=warning -resharper_cpp_class_never_used_highlighting=warning -resharper_cpp_compile_time_constant_can_be_replaced_with_boolean_constant_highlighting=suggestion -resharper_cpp_const_parameter_in_declaration_highlighting=suggestion -resharper_cpp_const_value_function_return_type_highlighting=suggestion -resharper_cpp_coroutine_call_resolve_error_highlighting=warning -resharper_cpp_cv_qualifier_can_not_be_applied_to_reference_highlighting=warning -resharper_cpp_c_style_cast_highlighting=suggestion -resharper_cpp_declaration_hides_local_highlighting=warning -resharper_cpp_declaration_hides_uncaptured_local_highlighting=hint -resharper_cpp_declaration_specifier_without_declarators_highlighting=warning -resharper_cpp_declarator_disambiguated_as_function_highlighting=warning -resharper_cpp_declarator_never_used_highlighting=warning -resharper_cpp_declarator_used_before_initialization_highlighting=error -resharper_cpp_defaulted_special_member_function_is_implicitly_deleted_highlighting=warning -resharper_cpp_default_case_not_handled_in_switch_statement_highlighting=warning -resharper_cpp_default_initialization_with_no_user_constructor_highlighting=warning -resharper_cpp_default_is_used_as_identifier_highlighting=warning -resharper_cpp_deleting_void_pointer_highlighting=warning -resharper_cpp_dependent_template_without_template_keyword_highlighting=warning -resharper_cpp_dependent_type_without_typename_keyword_highlighting=warning -resharper_cpp_deprecated_entity_highlighting=warning -resharper_cpp_deprecated_register_storage_class_specifier_highlighting=warning -resharper_cpp_dereference_operator_limit_exceeded_highlighting=warning -resharper_cpp_discarded_postfix_operator_result_highlighting=suggestion -resharper_cpp_doxygen_syntax_error_highlighting=warning -resharper_cpp_doxygen_undocumented_parameter_highlighting=suggestion -resharper_cpp_doxygen_unresolved_reference_highlighting=warning -resharper_cpp_empty_declaration_highlighting=warning -resharper_cpp_enforce_cv_qualifiers_order_highlighting=none -resharper_cpp_enforce_cv_qualifiers_placement_highlighting=none -resharper_cpp_enforce_do_statement_braces_highlighting=none -resharper_cpp_enforce_for_statement_braces_highlighting=none -resharper_cpp_enforce_function_declaration_style_highlighting=none -resharper_cpp_enforce_if_statement_braces_highlighting=none -resharper_cpp_enforce_nested_namespaces_style_highlighting=hint -resharper_cpp_enforce_overriding_destructor_style_highlighting=suggestion -resharper_cpp_enforce_overriding_function_style_highlighting=suggestion -resharper_cpp_enforce_type_alias_code_style_highlighting=none -resharper_cpp_enforce_while_statement_braces_highlighting=none -resharper_cpp_entity_assigned_but_no_read_highlighting=warning -resharper_cpp_entity_used_only_in_unevaluated_context_highlighting=warning -resharper_cpp_enumerator_never_used_highlighting=warning -resharper_cpp_equal_operands_in_binary_expression_highlighting=warning -resharper_cpp_explicit_specialization_in_non_namespace_scope_highlighting=warning -resharper_cpp_expression_without_side_effects_highlighting=warning -resharper_cpp_final_function_in_final_class_highlighting=suggestion -resharper_cpp_final_non_overriding_virtual_function_highlighting=suggestion -resharper_cpp_for_loop_can_be_replaced_with_while_highlighting=suggestion -resharper_cpp_functional_style_cast_highlighting=suggestion -resharper_cpp_function_doesnt_return_value_highlighting=warning -resharper_cpp_function_is_not_implemented_highlighting=warning -resharper_cpp_header_has_been_already_included_highlighting=hint -resharper_cpp_hidden_function_highlighting=warning -resharper_cpp_hiding_function_highlighting=warning -resharper_cpp_identical_operands_in_binary_expression_highlighting=warning -resharper_cpp_if_can_be_replaced_by_constexpr_if_highlighting=suggestion -resharper_cpp_implicit_default_constructor_not_available_highlighting=warning -resharper_cpp_incompatible_pointer_conversion_highlighting=warning -resharper_cpp_incomplete_switch_statement_highlighting=warning -resharper_cpp_inconsistent_naming_highlighting=hint -resharper_cpp_incorrect_blank_lines_near_braces_highlighting=none -resharper_cpp_initialized_value_is_always_rewritten_highlighting=warning -resharper_cpp_integral_to_pointer_conversion_highlighting=warning -resharper_cpp_invalid_line_continuation_highlighting=warning -resharper_cpp_join_declaration_and_assignment_highlighting=suggestion -resharper_cpp_lambda_capture_never_used_highlighting=warning -resharper_cpp_local_variable_may_be_const_highlighting=suggestion -resharper_cpp_local_variable_might_not_be_initialized_highlighting=warning -resharper_cpp_local_variable_with_non_trivial_dtor_is_never_used_highlighting=none -resharper_cpp_long_float_highlighting=warning -resharper_cpp_member_function_may_be_const_highlighting=suggestion -resharper_cpp_member_function_may_be_static_highlighting=suggestion -resharper_cpp_member_initializers_order_highlighting=suggestion -resharper_cpp_mismatched_class_tags_highlighting=warning -resharper_cpp_missing_blank_lines_highlighting=none -resharper_cpp_missing_include_guard_highlighting=warning -resharper_cpp_missing_indent_highlighting=none -resharper_cpp_missing_keyword_throw_highlighting=warning -resharper_cpp_missing_linebreak_highlighting=none -resharper_cpp_missing_space_highlighting=none -resharper_cpp_ms_ext_address_of_class_r_value_highlighting=warning -resharper_cpp_ms_ext_binding_r_value_to_lvalue_reference_highlighting=warning -resharper_cpp_ms_ext_copy_elision_in_copy_init_declarator_highlighting=warning -resharper_cpp_ms_ext_double_user_conversion_in_copy_init_highlighting=warning -resharper_cpp_ms_ext_not_initialized_static_const_local_var_highlighting=warning -resharper_cpp_ms_ext_reinterpret_cast_from_nullptr_highlighting=warning -resharper_cpp_multiple_spaces_highlighting=none -resharper_cpp_must_be_public_virtual_to_implement_interface_highlighting=warning -resharper_cpp_mutable_specifier_on_reference_member_highlighting=warning -resharper_cpp_nodiscard_function_without_return_value_highlighting=warning -resharper_cpp_non_exception_safe_resource_acquisition_highlighting=hint -resharper_cpp_non_explicit_conversion_operator_highlighting=hint -resharper_cpp_non_explicit_converting_constructor_highlighting=hint -resharper_cpp_non_inline_function_definition_in_header_file_highlighting=warning -resharper_cpp_non_inline_variable_definition_in_header_file_highlighting=warning -resharper_cpp_not_all_paths_return_value_highlighting=warning -resharper_cpp_no_discard_expression_highlighting=warning -resharper_cpp_object_member_might_not_be_initialized_highlighting=warning -resharper_cpp_outdent_is_off_prev_level_highlighting=none -resharper_cpp_out_parameter_must_be_written_highlighting=warning -resharper_cpp_parameter_may_be_const_highlighting=hint -resharper_cpp_parameter_may_be_const_ptr_or_ref_highlighting=suggestion -resharper_cpp_parameter_names_mismatch_highlighting=hint -resharper_cpp_parameter_never_used_highlighting=hint -resharper_cpp_parameter_value_is_reassigned_highlighting=warning -resharper_cpp_pointer_conversion_drops_qualifiers_highlighting=warning -resharper_cpp_pointer_to_integral_conversion_highlighting=warning -resharper_cpp_polymorphic_class_with_non_virtual_public_destructor_highlighting=warning -resharper_cpp_possibly_erroneous_empty_statements_highlighting=warning -resharper_cpp_possibly_uninitialized_member_highlighting=warning -resharper_cpp_possibly_unintended_object_slicing_highlighting=warning -resharper_cpp_precompiled_header_is_not_included_highlighting=error -resharper_cpp_precompiled_header_not_found_highlighting=error -resharper_cpp_printf_bad_format_highlighting=warning -resharper_cpp_printf_extra_arg_highlighting=warning -resharper_cpp_printf_missed_arg_highlighting=error -resharper_cpp_printf_risky_format_highlighting=warning -resharper_cpp_private_special_member_function_is_not_implemented_highlighting=warning -resharper_cpp_range_based_for_incompatible_reference_highlighting=warning -resharper_cpp_redefinition_of_default_argument_in_override_function_highlighting=warning -resharper_cpp_redundant_access_specifier_highlighting=hint -resharper_cpp_redundant_base_class_access_specifier_highlighting=hint -resharper_cpp_redundant_blank_lines_highlighting=none -resharper_cpp_redundant_boolean_expression_argument_highlighting=warning -resharper_cpp_redundant_cast_expression_highlighting=hint -resharper_cpp_redundant_const_specifier_highlighting=hint -resharper_cpp_redundant_control_flow_jump_highlighting=hint -resharper_cpp_redundant_elaborated_type_specifier_highlighting=hint -resharper_cpp_redundant_else_keyword_highlighting=hint -resharper_cpp_redundant_else_keyword_inside_compound_statement_highlighting=hint -resharper_cpp_redundant_empty_declaration_highlighting=hint -resharper_cpp_redundant_empty_statement_highlighting=hint -resharper_cpp_redundant_explicit_template_arguments_highlighting=hint -resharper_cpp_redundant_inline_specifier_highlighting=hint -resharper_cpp_redundant_lambda_parameter_list_highlighting=hint -resharper_cpp_redundant_linebreak_highlighting=none -resharper_cpp_redundant_member_initializer_highlighting=suggestion -resharper_cpp_redundant_namespace_definition_highlighting=suggestion -resharper_cpp_redundant_parentheses_highlighting=hint -resharper_cpp_redundant_qualifier_highlighting=hint -resharper_cpp_redundant_space_highlighting=none -resharper_cpp_redundant_static_specifier_on_member_allocation_function_highlighting=hint -resharper_cpp_redundant_template_keyword_highlighting=warning -resharper_cpp_redundant_typename_keyword_highlighting=warning -resharper_cpp_redundant_void_argument_list_highlighting=suggestion -resharper_cpp_reinterpret_cast_from_void_ptr_highlighting=suggestion -resharper_cpp_remove_redundant_braces_highlighting=none -resharper_cpp_replace_memset_with_zero_initialization_highlighting=suggestion -resharper_cpp_replace_tie_with_structured_binding_highlighting=suggestion -resharper_cpp_return_no_value_in_non_void_function_highlighting=warning -resharper_cpp_smart_pointer_vs_make_function_highlighting=suggestion -resharper_cpp_some_object_members_might_not_be_initialized_highlighting=warning -resharper_cpp_special_function_without_noexcept_specification_highlighting=warning -resharper_cpp_static_data_member_in_unnamed_struct_highlighting=warning -resharper_cpp_static_specifier_on_anonymous_namespace_member_highlighting=suggestion -resharper_cpp_string_literal_to_char_pointer_conversion_highlighting=warning -resharper_cpp_syntax_warning_highlighting=warning -resharper_cpp_tabs_and_spaces_mismatch_highlighting=none -resharper_cpp_tabs_are_disallowed_highlighting=none -resharper_cpp_tabs_outside_indent_highlighting=none -resharper_cpp_template_parameter_shadowing_highlighting=warning -resharper_cpp_this_arg_member_func_delegate_ctor_is_unsuported_by_dot_net_core_highlighting=none -resharper_cpp_throw_expression_can_be_replaced_with_rethrow_highlighting=warning -resharper_cpp_too_wide_scope_highlighting=suggestion -resharper_cpp_too_wide_scope_init_statement_highlighting=hint -resharper_cpp_type_alias_never_used_highlighting=warning -resharper_cpp_ue4_blueprint_callable_function_may_be_const_highlighting=hint -resharper_cpp_ue4_blueprint_callable_function_may_be_static_highlighting=hint -resharper_cpp_ue4_coding_standard_naming_violation_warning_highlighting=hint -resharper_cpp_ue4_coding_standard_u_class_naming_violation_error_highlighting=error -resharper_cpp_ue4_probable_memory_issues_with_u_objects_in_container_highlighting=warning -resharper_cpp_ue4_probable_memory_issues_with_u_object_highlighting=warning -resharper_cpp_ue_blueprint_callable_function_unused_highlighting=warning -resharper_cpp_ue_blueprint_implementable_event_not_implemented_highlighting=warning -resharper_cpp_ue_incorrect_engine_directory_highlighting=error -resharper_cpp_ue_non_existent_input_action_highlighting=warning -resharper_cpp_ue_non_existent_input_axis_highlighting=warning -resharper_cpp_ue_source_file_without_predefined_macros_highlighting=warning -resharper_cpp_ue_source_file_without_standard_library_highlighting=error -resharper_cpp_ue_version_file_doesnt_exist_highlighting=error -resharper_cpp_uninitialized_dependent_base_class_highlighting=warning -resharper_cpp_uninitialized_non_static_data_member_highlighting=warning -resharper_cpp_union_member_of_reference_type_highlighting=warning -resharper_cpp_unnamed_namespace_in_header_file_highlighting=warning -resharper_cpp_unnecessary_whitespace_highlighting=none -resharper_cpp_unreachable_code_highlighting=warning -resharper_cpp_unsigned_zero_comparison_highlighting=warning -resharper_cpp_unused_include_directive_highlighting=warning -resharper_cpp_user_defined_literal_suffix_does_not_start_with_underscore_highlighting=warning -resharper_cpp_use_algorithm_with_count_highlighting=suggestion -resharper_cpp_use_associative_contains_highlighting=suggestion -resharper_cpp_use_auto_for_numeric_highlighting=hint -resharper_cpp_use_auto_highlighting=hint -resharper_cpp_use_elements_view_highlighting=suggestion -resharper_cpp_use_erase_algorithm_highlighting=suggestion -resharper_cpp_use_familiar_template_syntax_for_generic_lambdas_highlighting=suggestion -resharper_cpp_use_range_algorithm_highlighting=suggestion -resharper_cpp_use_std_size_highlighting=suggestion -resharper_cpp_use_structured_binding_highlighting=hint -resharper_cpp_use_type_trait_alias_highlighting=suggestion -resharper_cpp_using_result_of_assignment_as_condition_highlighting=warning -resharper_cpp_u_function_macro_call_has_no_effect_highlighting=warning -resharper_cpp_u_property_macro_call_has_no_effect_highlighting=warning -resharper_cpp_variable_can_be_made_constexpr_highlighting=suggestion -resharper_cpp_virtual_function_call_inside_ctor_highlighting=warning -resharper_cpp_virtual_function_in_final_class_highlighting=warning -resharper_cpp_volatile_parameter_in_declaration_highlighting=suggestion -resharper_cpp_wrong_includes_order_highlighting=hint -resharper_cpp_wrong_indent_size_highlighting=none -resharper_cpp_wrong_slashes_in_include_directive_highlighting=hint -resharper_cpp_zero_constant_can_be_replaced_with_nullptr_highlighting=suggestion -resharper_cpp_zero_valued_expression_used_as_null_pointer_highlighting=warning -resharper_create_specialized_overload_highlighting=hint -resharper_css_browser_compatibility_highlighting=warning -resharper_css_caniuse_feature_requires_prefix_highlighting=hint -resharper_css_caniuse_unsupported_feature_highlighting=hint -resharper_css_not_resolved_highlighting=error -resharper_css_obsolete_highlighting=hint -resharper_css_property_does_not_override_vendor_property_highlighting=warning -resharper_cyclic_reference_comment_highlighting=none -resharper_c_declaration_with_implicit_int_type_highlighting=warning -resharper_c_sharp_build_cs_invalid_module_name_highlighting=warning -resharper_c_sharp_missing_plugin_dependency_highlighting=warning -resharper_declaration_hides_highlighting=hint -resharper_declaration_is_empty_highlighting=warning -resharper_declaration_visibility_error_highlighting=error -resharper_default_value_attribute_for_optional_parameter_highlighting=warning -resharper_deleting_non_qualified_reference_highlighting=error -resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting=hint -resharper_double_colons_expected_highlighting=error -resharper_double_colons_preferred_highlighting=suggestion -resharper_double_negation_in_pattern_highlighting=suggestion -resharper_double_negation_of_boolean_highlighting=warning -resharper_double_negation_operator_highlighting=suggestion -resharper_duplicate_identifier_error_highlighting=error -resharper_duplicate_reference_comment_highlighting=warning -resharper_duplicate_resource_highlighting=warning -resharper_duplicating_local_declaration_highlighting=warning -resharper_duplicating_parameter_declaration_error_highlighting=error -resharper_duplicating_property_declaration_error_highlighting=error -resharper_duplicating_property_declaration_highlighting=warning -resharper_duplicating_switch_label_highlighting=warning -resharper_dynamic_shift_right_op_is_not_int_highlighting=warning -resharper_elided_trailing_element_highlighting=warning -resharper_empty_constructor_highlighting=warning -resharper_empty_destructor_highlighting=warning -resharper_empty_embedded_statement_highlighting=warning -resharper_empty_for_statement_highlighting=warning -resharper_empty_general_catch_clause_highlighting=warning -resharper_empty_namespace_highlighting=warning -resharper_empty_object_property_declaration_highlighting=error -resharper_empty_return_value_for_type_annotated_function_highlighting=warning -resharper_empty_statement_highlighting=warning -resharper_empty_title_tag_highlighting=hint -resharper_enforce_do_while_statement_braces_highlighting=none -resharper_enforce_fixed_statement_braces_highlighting=none -resharper_enforce_foreach_statement_braces_highlighting=none -resharper_enforce_for_statement_braces_highlighting=none -resharper_enforce_if_statement_braces_highlighting=none -resharper_enforce_lock_statement_braces_highlighting=none -resharper_enforce_using_statement_braces_highlighting=none -resharper_enforce_while_statement_braces_highlighting=none -resharper_entity_name_captured_only_global_highlighting=warning -resharper_entity_name_captured_only_local_highlighting=warning -resharper_enumerable_sum_in_explicit_unchecked_context_highlighting=warning -resharper_enum_underlying_type_is_int_highlighting=warning -resharper_equal_expression_comparison_highlighting=warning -resharper_error_in_xml_doc_reference_highlighting=error -resharper_es6_feature_highlighting=error -resharper_es7_feature_highlighting=error -resharper_eval_arguments_name_error_highlighting=error -resharper_event_never_invoked_global_highlighting=suggestion -resharper_event_never_subscribed_to_global_highlighting=suggestion -resharper_event_never_subscribed_to_local_highlighting=suggestion -resharper_event_unsubscription_via_anonymous_delegate_highlighting=warning -resharper_experimental_feature_highlighting=error -resharper_explicit_caller_info_argument_highlighting=warning -resharper_expression_is_always_const_highlighting=warning -resharper_expression_is_always_null_highlighting=warning -resharper_field_can_be_made_read_only_global_highlighting=suggestion -resharper_field_can_be_made_read_only_local_highlighting=suggestion -resharper_field_hides_interface_property_with_default_implementation_highlighting=warning -resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting=hint -resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting=hint -resharper_format_string_placeholders_mismatch_highlighting=warning -resharper_format_string_problem_highlighting=warning -resharper_for_can_be_converted_to_foreach_highlighting=suggestion -resharper_for_statement_condition_is_true_highlighting=warning -resharper_functions_used_before_declared_highlighting=none -resharper_function_complexity_overflow_highlighting=none -resharper_function_never_returns_highlighting=warning -resharper_function_parameter_named_arguments_highlighting=warning -resharper_function_recursive_on_all_paths_highlighting=warning -resharper_function_used_out_of_scope_highlighting=warning -resharper_gc_suppress_finalize_for_type_without_destructor_highlighting=warning -resharper_generic_enumerator_not_disposed_highlighting=warning -resharper_heuristically_unreachable_code_highlighting=warning -resharper_heuristic_unreachable_code_highlighting=warning -resharper_hex_color_value_with_alpha_highlighting=error -resharper_html_attributes_quotes_highlighting=hint -resharper_html_attribute_not_resolved_highlighting=warning -resharper_html_attribute_value_not_resolved_highlighting=warning -resharper_html_dead_code_highlighting=warning -resharper_html_event_not_resolved_highlighting=warning -resharper_html_id_duplication_highlighting=warning -resharper_html_id_not_resolved_highlighting=warning -resharper_html_obsolete_highlighting=warning -resharper_html_path_error_highlighting=warning -resharper_html_tag_not_closed_highlighting=error -resharper_html_tag_not_resolved_highlighting=warning -resharper_html_tag_should_be_self_closed_highlighting=warning -resharper_html_tag_should_not_be_self_closed_highlighting=warning -resharper_html_warning_highlighting=warning -resharper_identifier_typo_highlighting=suggestion -resharper_implicit_any_error_highlighting=error -resharper_implicit_any_type_warning_highlighting=warning -resharper_import_keyword_not_with_invocation_highlighting=error -resharper_inactive_preprocessor_branch_highlighting=warning -resharper_inconsistently_synchronized_field_highlighting=warning -resharper_inconsistent_function_returns_highlighting=warning -resharper_inconsistent_naming_highlighting=warning -resharper_inconsistent_order_of_locks_highlighting=warning -resharper_incorrect_blank_lines_near_braces_highlighting=none -resharper_incorrect_operand_in_type_of_comparison_highlighting=warning -resharper_incorrect_triple_slash_location_highlighting=warning -resharper_indexing_by_invalid_range_highlighting=warning -resharper_inheritdoc_consider_usage_highlighting=none -resharper_inheritdoc_invalid_usage_highlighting=warning -resharper_inline_out_variable_declaration_highlighting=suggestion -resharper_inline_temporary_variable_highlighting=hint -resharper_internal_module_highlighting=suggestion -resharper_internal_or_private_member_not_documented_highlighting=none -resharper_interpolated_string_expression_is_not_i_formattable_highlighting=warning -resharper_introduce_optional_parameters_global_highlighting=suggestion -resharper_introduce_optional_parameters_local_highlighting=suggestion -resharper_introduce_variable_to_apply_guard_highlighting=hint -resharper_int_division_by_zero_highlighting=warning -resharper_int_variable_overflow_highlighting=warning -resharper_int_variable_overflow_in_checked_context_highlighting=warning -resharper_int_variable_overflow_in_unchecked_context_highlighting=warning -resharper_invalid_attribute_value_highlighting=warning -resharper_invalid_json_syntax_highlighting=error -resharper_invalid_task_element_highlighting=none -resharper_invalid_value_highlighting=error -resharper_invalid_value_type_highlighting=warning -resharper_invalid_xml_doc_comment_highlighting=warning -resharper_invert_condition_1_highlighting=hint -resharper_invert_if_highlighting=hint -resharper_invocation_is_skipped_highlighting=hint -resharper_invocation_of_non_function_highlighting=warning -resharper_invoked_expression_maybe_non_function_highlighting=warning -resharper_invoke_as_extension_method_highlighting=suggestion -resharper_is_expression_always_false_highlighting=warning -resharper_is_expression_always_true_highlighting=warning -resharper_iterator_method_result_is_ignored_highlighting=warning -resharper_iterator_never_returns_highlighting=warning -resharper_join_declaration_and_initializer_highlighting=suggestion -resharper_join_declaration_and_initializer_js_highlighting=suggestion -resharper_join_null_check_with_usage_highlighting=suggestion -resharper_join_null_check_with_usage_when_possible_highlighting=none -resharper_json_validation_failed_highlighting=error -resharper_js_path_not_found_highlighting=error -resharper_js_unreachable_code_highlighting=warning -resharper_jump_must_be_in_loop_highlighting=warning -resharper_label_or_semicolon_expected_highlighting=error -resharper_lambda_expression_can_be_made_static_highlighting=none -resharper_lambda_expression_must_be_static_highlighting=suggestion -resharper_lambda_highlighting=suggestion -resharper_lambda_should_not_capture_context_highlighting=warning -resharper_less_specific_overload_than_main_signature_highlighting=warning -resharper_lexical_declaration_needs_block_highlighting=error -resharper_localizable_element_highlighting=warning -resharper_local_function_can_be_made_static_highlighting=none -resharper_local_function_hides_method_highlighting=warning -resharper_local_function_redefined_later_highlighting=warning -resharper_local_variable_hides_member_highlighting=warning -resharper_long_literal_ending_lower_l_highlighting=warning -resharper_loop_can_be_converted_to_query_highlighting=hint -resharper_loop_can_be_partly_converted_to_query_highlighting=none -resharper_loop_variable_is_never_changed_inside_loop_highlighting=warning -resharper_l_value_is_expected_highlighting=error -resharper_markup_attribute_typo_highlighting=suggestion -resharper_markup_text_typo_highlighting=suggestion -resharper_math_abs_method_is_redundant_highlighting=warning -resharper_math_clamp_min_greater_than_max_highlighting=warning -resharper_meaningless_default_parameter_value_highlighting=warning -resharper_member_can_be_internal_highlighting=none -resharper_member_can_be_made_static_global_highlighting=hint -resharper_member_can_be_made_static_local_highlighting=hint -resharper_member_can_be_private_global_highlighting=suggestion -resharper_member_can_be_private_local_highlighting=suggestion -resharper_member_can_be_protected_global_highlighting=suggestion -resharper_member_can_be_protected_local_highlighting=suggestion -resharper_member_hides_interface_member_with_default_implementation_highlighting=warning -resharper_member_hides_static_from_outer_class_highlighting=warning -resharper_member_initializer_value_ignored_highlighting=warning -resharper_merge_and_pattern_highlighting=suggestion -resharper_merge_cast_with_type_check_highlighting=suggestion -resharper_merge_conditional_expression_highlighting=suggestion -resharper_merge_conditional_expression_when_possible_highlighting=none -resharper_merge_into_logical_pattern_highlighting=hint -resharper_merge_into_negated_pattern_highlighting=hint -resharper_merge_into_pattern_highlighting=suggestion -resharper_merge_nested_property_patterns_highlighting=suggestion -resharper_merge_sequential_checks_highlighting=hint -resharper_merge_sequential_checks_when_possible_highlighting=none -resharper_method_has_async_overload_highlighting=suggestion -resharper_method_has_async_overload_with_cancellation_highlighting=suggestion -resharper_method_overload_with_optional_parameter_highlighting=warning -resharper_method_safe_this_highlighting=suggestion -resharper_method_supports_cancellation_highlighting=suggestion -resharper_missing_alt_attribute_in_img_tag_highlighting=hint -resharper_missing_attribute_highlighting=warning -resharper_missing_blank_lines_highlighting=none -resharper_missing_body_tag_highlighting=warning -resharper_missing_has_own_property_in_foreach_highlighting=warning -resharper_missing_head_and_body_tags_highlighting=warning -resharper_missing_head_tag_highlighting=warning -resharper_missing_indent_highlighting=none -resharper_missing_linebreak_highlighting=none -resharper_missing_space_highlighting=none -resharper_missing_title_tag_highlighting=hint -resharper_misuse_of_owner_function_this_highlighting=warning -resharper_more_specific_foreach_variable_type_available_highlighting=suggestion -resharper_more_specific_signature_after_less_specific_highlighting=warning -resharper_move_to_existing_positional_deconstruction_pattern_highlighting=hint -resharper_multiple_declarations_in_foreach_highlighting=error -resharper_multiple_nullable_attributes_usage_highlighting=warning -resharper_multiple_order_by_highlighting=warning -resharper_multiple_output_tags_highlighting=warning -resharper_multiple_resolve_candidates_in_text_highlighting=warning -resharper_multiple_spaces_highlighting=none -resharper_multiple_statements_on_one_line_highlighting=none -resharper_multiple_type_members_on_one_line_highlighting=none -resharper_must_use_return_value_highlighting=warning -resharper_mvc_action_not_resolved_highlighting=error -resharper_mvc_area_not_resolved_highlighting=error -resharper_mvc_controller_not_resolved_highlighting=error -resharper_mvc_invalid_model_type_highlighting=error -resharper_mvc_masterpage_not_resolved_highlighting=error -resharper_mvc_partial_view_not_resolved_highlighting=error -resharper_mvc_template_not_resolved_highlighting=error -resharper_mvc_view_component_not_resolved_highlighting=error -resharper_mvc_view_component_view_not_resolved_highlighting=error -resharper_mvc_view_not_resolved_highlighting=error -resharper_native_type_prototype_extending_highlighting=warning -resharper_native_type_prototype_overwriting_highlighting=warning -resharper_negation_of_relational_pattern_highlighting=suggestion -resharper_negative_equality_expression_highlighting=suggestion -resharper_negative_index_highlighting=warning -resharper_nested_string_interpolation_highlighting=suggestion -resharper_non_assigned_constant_highlighting=error -resharper_non_atomic_compound_operator_highlighting=warning -resharper_non_constant_equality_expression_has_constant_result_highlighting=warning -resharper_non_parsable_element_highlighting=warning -resharper_non_readonly_member_in_get_hash_code_highlighting=warning -resharper_non_volatile_field_in_double_check_locking_highlighting=warning -resharper_not_accessed_field_global_highlighting=suggestion -resharper_not_accessed_field_local_highlighting=warning -resharper_not_accessed_positional_property_global_highlighting=warning -resharper_not_accessed_positional_property_local_highlighting=warning -resharper_not_accessed_variable_highlighting=warning -resharper_not_all_paths_return_value_highlighting=warning -resharper_not_assigned_out_parameter_highlighting=warning -resharper_not_declared_in_parent_culture_highlighting=warning -resharper_not_null_member_is_not_initialized_highlighting=warning -resharper_not_observable_annotation_redundancy_highlighting=warning -resharper_not_overridden_in_specific_culture_highlighting=warning -resharper_not_resolved_highlighting=warning -resharper_not_resolved_in_text_highlighting=warning -resharper_nullable_warning_suppression_is_used_highlighting=none -resharper_n_unit_async_method_must_be_task_highlighting=warning -resharper_n_unit_attribute_produces_too_many_tests_highlighting=none -resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting=warning -resharper_n_unit_auto_fixture_missed_test_attribute_highlighting=warning -resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting=warning -resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting=warning -resharper_n_unit_duplicate_values_highlighting=warning -resharper_n_unit_ignored_parameter_attribute_highlighting=warning -resharper_n_unit_implicit_unspecified_null_values_highlighting=warning -resharper_n_unit_incorrect_argument_type_highlighting=warning -resharper_n_unit_incorrect_expected_result_type_highlighting=warning -resharper_n_unit_incorrect_range_bounds_highlighting=warning -resharper_n_unit_method_with_parameters_and_test_attribute_highlighting=warning -resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting=warning -resharper_n_unit_non_public_method_with_test_attribute_highlighting=warning -resharper_n_unit_no_values_provided_highlighting=warning -resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting=warning -resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting=warning -resharper_n_unit_range_step_sign_mismatch_highlighting=warning -resharper_n_unit_range_step_value_must_not_be_zero_highlighting=warning -resharper_n_unit_range_to_value_is_not_reachable_highlighting=warning -resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting=warning -resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting=warning -resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting=warning -resharper_n_unit_test_case_attribute_requires_expected_result_highlighting=warning -resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting=warning -resharper_n_unit_test_case_result_property_is_obsolete_highlighting=warning -resharper_n_unit_test_case_source_cannot_be_resolved_highlighting=warning -resharper_n_unit_test_case_source_must_be_field_property_method_highlighting=warning -resharper_n_unit_test_case_source_must_be_static_highlighting=warning -resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting=warning -resharper_object_creation_as_statement_highlighting=warning -resharper_object_destructuring_without_parentheses_highlighting=error -resharper_object_literals_are_not_comma_free_highlighting=error -resharper_obsolete_element_error_highlighting=error -resharper_obsolete_element_highlighting=warning -resharper_octal_literals_not_allowed_error_highlighting=error -resharper_ol_tag_contains_non_li_elements_highlighting=hint -resharper_one_way_operation_contract_with_return_type_highlighting=warning -resharper_operation_contract_without_service_contract_highlighting=warning -resharper_operator_is_can_be_used_highlighting=warning -resharper_optional_parameter_hierarchy_mismatch_highlighting=warning -resharper_optional_parameter_ref_out_highlighting=warning -resharper_other_tags_inside_script1_highlighting=error -resharper_other_tags_inside_script2_highlighting=error -resharper_other_tags_inside_unclosed_script_highlighting=error -resharper_outdent_is_off_prev_level_highlighting=none -resharper_output_tag_required_highlighting=warning -resharper_out_parameter_value_is_always_discarded_global_highlighting=suggestion -resharper_out_parameter_value_is_always_discarded_local_highlighting=warning -resharper_overload_signature_inferring_highlighting=hint -resharper_overridden_with_empty_value_highlighting=warning -resharper_overridden_with_same_value_highlighting=suggestion -resharper_parameter_doesnt_make_any_sense_highlighting=warning -resharper_parameter_hides_member_highlighting=warning -resharper_parameter_only_used_for_precondition_check_global_highlighting=suggestion -resharper_parameter_only_used_for_precondition_check_local_highlighting=warning -resharper_parameter_type_can_be_enumerable_global_highlighting=hint -resharper_parameter_type_can_be_enumerable_local_highlighting=hint -resharper_parameter_value_is_not_used_highlighting=warning -resharper_partial_method_parameter_name_mismatch_highlighting=warning -resharper_partial_method_with_single_part_highlighting=warning -resharper_partial_type_with_single_part_highlighting=warning -resharper_pass_string_interpolation_highlighting=hint -resharper_path_not_resolved_highlighting=error -resharper_pattern_always_matches_highlighting=warning -resharper_pattern_is_always_true_or_false_highlighting=warning -resharper_pattern_never_matches_highlighting=warning -resharper_polymorphic_field_like_event_invocation_highlighting=warning -resharper_possible_infinite_inheritance_highlighting=warning -resharper_possible_intended_rethrow_highlighting=warning -resharper_possible_interface_member_ambiguity_highlighting=warning -resharper_possible_invalid_cast_exception_highlighting=warning -resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting=warning -resharper_possible_invalid_operation_exception_highlighting=warning -resharper_possible_loss_of_fraction_highlighting=warning -resharper_possible_mistaken_argument_highlighting=warning -resharper_possible_mistaken_call_to_get_type_1_highlighting=warning -resharper_possible_mistaken_call_to_get_type_2_highlighting=warning -resharper_possible_multiple_enumeration_highlighting=warning -resharper_possible_multiple_write_access_in_double_check_locking_highlighting=warning -resharper_possible_null_reference_exception_highlighting=warning -resharper_possible_struct_member_modification_of_non_variable_struct_highlighting=warning -resharper_possible_unintended_linear_search_in_set_highlighting=warning -resharper_possible_unintended_queryable_as_enumerable_highlighting=suggestion -resharper_possible_unintended_reference_comparison_highlighting=warning -resharper_possible_write_to_me_highlighting=warning -resharper_possibly_impure_method_call_on_readonly_variable_highlighting=warning -resharper_possibly_incorrectly_broken_statement_highlighting=warning -resharper_possibly_missing_indexer_initializer_comma_highlighting=warning -resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting=warning -resharper_possibly_mistaken_use_of_params_method_highlighting=warning -resharper_possibly_unassigned_property_highlighting=hint -resharper_private_field_can_be_converted_to_local_variable_highlighting=warning -resharper_private_variable_can_be_made_readonly_highlighting=hint -resharper_property_can_be_made_init_only_global_highlighting=suggestion -resharper_property_can_be_made_init_only_local_highlighting=suggestion -resharper_property_getter_cannot_have_parameters_highlighting=error -resharper_property_not_resolved_highlighting=error -resharper_property_setter_must_have_single_parameter_highlighting=error -resharper_public_constructor_in_abstract_class_highlighting=suggestion -resharper_pure_attribute_on_void_method_highlighting=warning -resharper_qualified_expression_is_null_highlighting=warning -resharper_qualified_expression_maybe_null_highlighting=warning -resharper_razor_layout_not_resolved_highlighting=error -resharper_razor_section_not_resolved_highlighting=error -resharper_read_access_in_double_check_locking_highlighting=warning -resharper_redundant_abstract_modifier_highlighting=warning -resharper_redundant_always_match_subpattern_highlighting=suggestion -resharper_redundant_anonymous_type_property_name_highlighting=warning -resharper_redundant_argument_default_value_highlighting=warning -resharper_redundant_array_creation_expression_highlighting=hint -resharper_redundant_array_lower_bound_specification_highlighting=warning -resharper_redundant_assignment_highlighting=warning -resharper_redundant_attribute_parentheses_highlighting=hint -resharper_redundant_attribute_usage_property_highlighting=suggestion -resharper_redundant_base_constructor_call_highlighting=warning -resharper_redundant_base_qualifier_highlighting=warning -resharper_redundant_blank_lines_highlighting=none -resharper_redundant_block_highlighting=warning -resharper_redundant_bool_compare_highlighting=warning -resharper_redundant_case_label_highlighting=warning -resharper_redundant_cast_highlighting=warning -resharper_redundant_catch_clause_highlighting=warning -resharper_redundant_check_before_assignment_highlighting=warning -resharper_redundant_collection_initializer_element_braces_highlighting=hint -resharper_redundant_comparison_with_boolean_highlighting=warning -resharper_redundant_configure_await_highlighting=suggestion -resharper_redundant_css_hack_highlighting=warning -resharper_redundant_declaration_semicolon_highlighting=hint -resharper_redundant_default_member_initializer_highlighting=warning -resharper_redundant_delegate_creation_highlighting=warning -resharper_redundant_disable_warning_comment_highlighting=warning -resharper_redundant_discard_designation_highlighting=suggestion -resharper_redundant_else_block_highlighting=warning -resharper_redundant_empty_case_else_highlighting=warning -resharper_redundant_empty_constructor_highlighting=warning -resharper_redundant_empty_finally_block_highlighting=warning -resharper_redundant_empty_object_creation_argument_list_highlighting=hint -resharper_redundant_empty_object_or_collection_initializer_highlighting=warning -resharper_redundant_empty_switch_section_highlighting=warning -resharper_redundant_enumerable_cast_call_highlighting=warning -resharper_redundant_enum_case_label_for_default_section_highlighting=none -resharper_redundant_explicit_array_creation_highlighting=warning -resharper_redundant_explicit_array_size_highlighting=warning -resharper_redundant_explicit_nullable_creation_highlighting=warning -resharper_redundant_explicit_params_array_creation_highlighting=suggestion -resharper_redundant_explicit_positional_property_declaration_highlighting=warning -resharper_redundant_explicit_tuple_component_name_highlighting=warning -resharper_redundant_extends_list_entry_highlighting=warning -resharper_redundant_fixed_pointer_declaration_highlighting=suggestion -resharper_redundant_highlighting=warning -resharper_redundant_if_else_block_highlighting=hint -resharper_redundant_if_statement_then_keyword_highlighting=none -resharper_redundant_immediate_delegate_invocation_highlighting=suggestion -resharper_redundant_intermediate_variable_highlighting=hint -resharper_redundant_is_before_relational_pattern_highlighting=suggestion -resharper_redundant_iterator_keyword_highlighting=warning -resharper_redundant_jump_statement_highlighting=warning -resharper_redundant_lambda_parameter_type_highlighting=warning -resharper_redundant_lambda_signature_parentheses_highlighting=hint -resharper_redundant_linebreak_highlighting=none -resharper_redundant_local_class_name_highlighting=hint -resharper_redundant_local_function_name_highlighting=hint -resharper_redundant_logical_conditional_expression_operand_highlighting=warning -resharper_redundant_me_qualifier_highlighting=warning -resharper_redundant_my_base_qualifier_highlighting=warning -resharper_redundant_my_class_qualifier_highlighting=warning -resharper_redundant_name_qualifier_highlighting=warning -resharper_redundant_not_null_constraint_highlighting=warning -resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting=warning -resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting=warning -resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting=warning -resharper_redundant_nullable_flow_attribute_highlighting=warning -resharper_redundant_nullable_type_mark_highlighting=warning -resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting=warning -resharper_redundant_overflow_checking_context_highlighting=warning -resharper_redundant_overload_global_highlighting=suggestion -resharper_redundant_overload_local_highlighting=suggestion -resharper_redundant_overridden_member_highlighting=warning -resharper_redundant_params_highlighting=warning -resharper_redundant_parentheses_highlighting=none -resharper_redundant_parent_type_declaration_highlighting=warning -resharper_redundant_pattern_parentheses_highlighting=hint -resharper_redundant_property_parentheses_highlighting=hint -resharper_redundant_property_pattern_clause_highlighting=suggestion -resharper_redundant_qualifier_highlighting=warning -resharper_redundant_query_order_by_ascending_keyword_highlighting=hint -resharper_redundant_range_bound_highlighting=suggestion -resharper_redundant_readonly_modifier_highlighting=suggestion -resharper_redundant_record_body_highlighting=warning -resharper_redundant_record_class_keyword_highlighting=warning -resharper_redundant_setter_value_parameter_declaration_highlighting=hint -resharper_redundant_space_highlighting=none -resharper_redundant_string_format_call_highlighting=warning -resharper_redundant_string_interpolation_highlighting=suggestion -resharper_redundant_string_to_char_array_call_highlighting=warning -resharper_redundant_string_type_highlighting=suggestion -resharper_redundant_suppress_nullable_warning_expression_highlighting=warning -resharper_redundant_ternary_expression_highlighting=warning -resharper_redundant_to_string_call_for_value_type_highlighting=hint -resharper_redundant_to_string_call_highlighting=warning -resharper_redundant_type_arguments_of_method_highlighting=warning -resharper_redundant_type_cast_highlighting=warning -resharper_redundant_type_cast_structural_highlighting=warning -resharper_redundant_type_check_in_pattern_highlighting=warning -resharper_redundant_units_highlighting=warning -resharper_redundant_unsafe_context_highlighting=warning -resharper_redundant_using_directive_global_highlighting=warning -resharper_redundant_using_directive_highlighting=warning -resharper_redundant_variable_type_specification_highlighting=hint -resharper_redundant_verbatim_prefix_highlighting=suggestion -resharper_redundant_verbatim_string_prefix_highlighting=suggestion -resharper_redundant_with_expression_highlighting=suggestion -resharper_reference_equals_with_value_type_highlighting=warning -resharper_reg_exp_inspections_highlighting=warning -resharper_remove_constructor_invocation_highlighting=none -resharper_remove_redundant_braces_highlighting=none -resharper_remove_redundant_or_statement_false_highlighting=suggestion -resharper_remove_redundant_or_statement_true_highlighting=suggestion -resharper_remove_to_list_1_highlighting=suggestion -resharper_remove_to_list_2_highlighting=suggestion -resharper_replace_auto_property_with_computed_property_highlighting=hint -resharper_replace_indicing_with_array_destructuring_highlighting=hint -resharper_replace_indicing_with_short_hand_properties_after_destructuring_highlighting=hint -resharper_replace_object_pattern_with_var_pattern_highlighting=suggestion -resharper_replace_slice_with_range_indexer_highlighting=hint -resharper_replace_substring_with_range_indexer_highlighting=hint -resharper_replace_undefined_checking_series_with_object_destructuring_highlighting=hint -resharper_replace_with_destructuring_swap_highlighting=hint -resharper_replace_with_first_or_default_1_highlighting=suggestion -resharper_replace_with_first_or_default_2_highlighting=suggestion -resharper_replace_with_first_or_default_3_highlighting=suggestion -resharper_replace_with_first_or_default_4_highlighting=suggestion -resharper_replace_with_last_or_default_1_highlighting=suggestion -resharper_replace_with_last_or_default_2_highlighting=suggestion -resharper_replace_with_last_or_default_3_highlighting=suggestion -resharper_replace_with_last_or_default_4_highlighting=suggestion -resharper_replace_with_of_type_1_highlighting=suggestion -resharper_replace_with_of_type_2_highlighting=suggestion -resharper_replace_with_of_type_3_highlighting=suggestion -resharper_replace_with_of_type_any_1_highlighting=suggestion -resharper_replace_with_of_type_any_2_highlighting=suggestion -resharper_replace_with_of_type_count_1_highlighting=suggestion -resharper_replace_with_of_type_count_2_highlighting=suggestion -resharper_replace_with_of_type_first_1_highlighting=suggestion -resharper_replace_with_of_type_first_2_highlighting=suggestion -resharper_replace_with_of_type_first_or_default_1_highlighting=suggestion -resharper_replace_with_of_type_first_or_default_2_highlighting=suggestion -resharper_replace_with_of_type_last_1_highlighting=suggestion -resharper_replace_with_of_type_last_2_highlighting=suggestion -resharper_replace_with_of_type_last_or_default_1_highlighting=suggestion -resharper_replace_with_of_type_last_or_default_2_highlighting=suggestion -resharper_replace_with_of_type_long_count_highlighting=suggestion -resharper_replace_with_of_type_single_1_highlighting=suggestion -resharper_replace_with_of_type_single_2_highlighting=suggestion -resharper_replace_with_of_type_single_or_default_1_highlighting=suggestion -resharper_replace_with_of_type_single_or_default_2_highlighting=suggestion -resharper_replace_with_of_type_where_highlighting=suggestion -resharper_replace_with_simple_assignment_false_highlighting=suggestion -resharper_replace_with_simple_assignment_true_highlighting=suggestion -resharper_replace_with_single_assignment_false_highlighting=suggestion -resharper_replace_with_single_assignment_true_highlighting=suggestion -resharper_replace_with_single_call_to_any_highlighting=suggestion -resharper_replace_with_single_call_to_count_highlighting=suggestion -resharper_replace_with_single_call_to_first_highlighting=suggestion -resharper_replace_with_single_call_to_first_or_default_highlighting=suggestion -resharper_replace_with_single_call_to_last_highlighting=suggestion -resharper_replace_with_single_call_to_last_or_default_highlighting=suggestion -resharper_replace_with_single_call_to_single_highlighting=suggestion -resharper_replace_with_single_call_to_single_or_default_highlighting=suggestion -resharper_replace_with_single_or_default_1_highlighting=suggestion -resharper_replace_with_single_or_default_2_highlighting=suggestion -resharper_replace_with_single_or_default_3_highlighting=suggestion -resharper_replace_with_single_or_default_4_highlighting=suggestion -resharper_replace_with_string_is_null_or_empty_highlighting=suggestion -resharper_required_base_types_conflict_highlighting=warning -resharper_required_base_types_direct_conflict_highlighting=warning -resharper_required_base_types_is_not_inherited_highlighting=warning -resharper_requires_fallback_color_highlighting=warning -resharper_resource_item_not_resolved_highlighting=error -resharper_resource_not_resolved_highlighting=error -resharper_resx_not_resolved_highlighting=warning -resharper_return_from_global_scopet_with_value_highlighting=warning -resharper_return_type_can_be_enumerable_global_highlighting=hint -resharper_return_type_can_be_enumerable_local_highlighting=hint -resharper_return_type_can_be_not_nullable_highlighting=warning -resharper_return_value_of_pure_method_is_not_used_highlighting=warning -resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting=hint -resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting=warning -resharper_route_templates_ambiguous_route_match_highlighting=warning -resharper_route_templates_constraint_argument_cannot_be_converted_highlighting=warning -resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting=hint -resharper_route_templates_duplicated_parameter_highlighting=warning -resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting=warning -resharper_route_templates_method_missing_route_parameters_highlighting=hint -resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting=warning -resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting=warning -resharper_route_templates_parameter_constraint_can_be_specified_highlighting=hint -resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting=warning -resharper_route_templates_parameter_type_can_be_made_stricter_highlighting=suggestion -resharper_route_templates_route_parameter_constraint_not_resolved_highlighting=warning -resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting=hint -resharper_route_templates_route_token_not_resolved_highlighting=warning -resharper_route_templates_symbol_not_resolved_highlighting=warning -resharper_route_templates_syntax_error_highlighting=warning -resharper_safe_cast_is_used_as_type_check_highlighting=suggestion -resharper_same_imports_with_different_name_highlighting=warning -resharper_same_variable_assignment_highlighting=warning -resharper_script_tag_has_both_src_and_content_attributes_highlighting=error -resharper_script_tag_with_content_before_includes_highlighting=hint -resharper_sealed_member_in_sealed_class_highlighting=warning -resharper_separate_control_transfer_statement_highlighting=none -resharper_service_contract_without_operations_highlighting=warning -resharper_shift_expression_real_shift_count_is_zero_highlighting=warning -resharper_shift_expression_result_equals_zero_highlighting=warning -resharper_shift_expression_right_operand_not_equal_real_count_highlighting=warning -resharper_shift_expression_zero_left_operand_highlighting=warning -resharper_similar_anonymous_type_nearby_highlighting=hint -resharper_similar_expressions_comparison_highlighting=warning -resharper_simplify_conditional_operator_highlighting=suggestion -resharper_simplify_conditional_ternary_expression_highlighting=suggestion -resharper_simplify_i_if_highlighting=suggestion -resharper_simplify_linq_expression_use_all_highlighting=suggestion -resharper_simplify_linq_expression_use_any_highlighting=suggestion -resharper_simplify_string_interpolation_highlighting=suggestion -resharper_specify_a_culture_in_string_conversion_explicitly_highlighting=warning -resharper_specify_string_comparison_highlighting=hint -resharper_specify_variable_type_explicitly_highlighting=hint -resharper_spin_lock_in_readonly_field_highlighting=warning -resharper_stack_alloc_inside_loop_highlighting=warning -resharper_statement_termination_highlighting=warning -resharper_static_member_initializer_referes_to_member_below_highlighting=warning -resharper_static_member_in_generic_type_highlighting=none -resharper_static_problem_in_text_highlighting=warning -resharper_string_compare_is_culture_specific_1_highlighting=warning -resharper_string_compare_is_culture_specific_2_highlighting=warning -resharper_string_compare_is_culture_specific_3_highlighting=warning -resharper_string_compare_is_culture_specific_4_highlighting=warning -resharper_string_compare_is_culture_specific_5_highlighting=warning -resharper_string_compare_is_culture_specific_6_highlighting=warning -resharper_string_compare_to_is_culture_specific_highlighting=warning -resharper_string_concatenation_to_template_string_highlighting=hint -resharper_string_ends_with_is_culture_specific_highlighting=none -resharper_string_index_of_is_culture_specific_1_highlighting=warning -resharper_string_index_of_is_culture_specific_2_highlighting=warning -resharper_string_index_of_is_culture_specific_3_highlighting=warning -resharper_string_last_index_of_is_culture_specific_1_highlighting=warning -resharper_string_last_index_of_is_culture_specific_2_highlighting=warning -resharper_string_last_index_of_is_culture_specific_3_highlighting=warning -resharper_string_literal_as_interpolation_argument_highlighting=suggestion -resharper_string_literal_typo_highlighting=suggestion -resharper_string_literal_wrong_quotes_highlighting=hint -resharper_string_starts_with_is_culture_specific_highlighting=none -resharper_structured_message_template_problem_highlighting=warning -resharper_struct_can_be_made_read_only_highlighting=suggestion -resharper_struct_member_can_be_made_read_only_highlighting=none -resharper_suggest_base_type_for_parameter_highlighting=hint -resharper_suggest_base_type_for_parameter_in_constructor_highlighting=hint -resharper_suggest_discard_declaration_var_style_highlighting=hint -resharper_suggest_var_or_type_built_in_types_highlighting=hint -resharper_suggest_var_or_type_deconstruction_declarations_highlighting=hint -resharper_suggest_var_or_type_elsewhere_highlighting=hint -resharper_suggest_var_or_type_simple_types_highlighting=hint -resharper_super_call_highlighting=suggestion -resharper_super_call_prohibits_this_highlighting=error -resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting=warning -resharper_suspicious_instanceof_check_highlighting=warning -resharper_suspicious_lambda_block_highlighting=warning -resharper_suspicious_lock_over_synchronization_primitive_highlighting=warning -resharper_suspicious_math_sign_method_highlighting=warning -resharper_suspicious_parameter_name_in_argument_null_exception_highlighting=warning -resharper_suspicious_this_usage_highlighting=warning -resharper_suspicious_typeof_check_highlighting=warning -resharper_suspicious_type_conversion_global_highlighting=warning -resharper_swap_via_deconstruction_highlighting=suggestion -resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting=hint -resharper_switch_statement_for_enum_misses_default_section_highlighting=hint -resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting=hint -resharper_switch_statement_missing_some_enum_cases_no_default_highlighting=none -resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting=warning -resharper_syntax_is_not_allowed_highlighting=warning -resharper_tabs_and_spaces_mismatch_highlighting=none -resharper_tabs_are_disallowed_highlighting=none -resharper_tabs_outside_indent_highlighting=none -resharper_tail_recursive_call_highlighting=hint -resharper_tasks_not_loaded_highlighting=warning -resharper_ternary_can_be_replaced_by_its_condition_highlighting=warning -resharper_this_in_global_context_highlighting=warning -resharper_thread_static_at_instance_field_highlighting=warning -resharper_thread_static_field_has_initializer_highlighting=warning -resharper_throw_must_be_followed_by_expression_highlighting=error -resharper_too_wide_local_variable_scope_highlighting=suggestion -resharper_try_cast_always_succeeds_highlighting=suggestion -resharper_try_statements_can_be_merged_highlighting=hint -resharper_ts_not_resolved_highlighting=error -resharper_ts_resolved_from_inaccessible_module_highlighting=error -resharper_type_guard_doesnt_affect_anything_highlighting=warning -resharper_type_guard_produces_never_type_highlighting=warning -resharper_type_parameter_can_be_variant_highlighting=suggestion -resharper_type_parameter_hides_type_param_from_outer_scope_highlighting=warning -resharper_ul_tag_contains_non_li_elements_highlighting=hint -resharper_unassigned_field_global_highlighting=suggestion -resharper_unassigned_field_local_highlighting=warning -resharper_unassigned_get_only_auto_property_highlighting=warning -resharper_unassigned_readonly_field_highlighting=warning -resharper_unclosed_script_highlighting=error -resharper_undeclared_global_variable_using_highlighting=warning -resharper_unexpected_value_highlighting=error -resharper_unknown_css_class_highlighting=warning -resharper_unknown_css_variable_highlighting=warning -resharper_unknown_css_vendor_extension_highlighting=hint -resharper_unknown_item_group_highlighting=warning -resharper_unknown_metadata_highlighting=warning -resharper_unknown_output_parameter_highlighting=warning -resharper_unknown_property_highlighting=warning -resharper_unknown_target_highlighting=warning -resharper_unknown_task_attribute_highlighting=warning -resharper_unknown_task_highlighting=warning -resharper_unnecessary_whitespace_highlighting=none -resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting=warning -resharper_unreachable_switch_case_due_to_integer_analysis_highlighting=warning -resharper_unreal_header_tool_error_highlighting=error -resharper_unreal_header_tool_parser_error_highlighting=error -resharper_unreal_header_tool_warning_highlighting=warning -resharper_unsafe_comma_in_object_properties_list_highlighting=warning -resharper_unsupported_required_base_type_highlighting=warning -resharper_unused_anonymous_method_signature_highlighting=warning -resharper_unused_auto_property_accessor_global_highlighting=warning -resharper_unused_auto_property_accessor_local_highlighting=warning -resharper_unused_import_clause_highlighting=warning -resharper_unused_inherited_parameter_highlighting=hint -resharper_unused_locals_highlighting=warning -resharper_unused_local_function_highlighting=warning -resharper_unused_local_function_parameter_highlighting=warning -resharper_unused_local_function_return_value_highlighting=warning -resharper_unused_local_import_highlighting=warning -resharper_unused_member_global_highlighting=suggestion -resharper_unused_member_hierarchy_global_highlighting=suggestion -resharper_unused_member_hierarchy_local_highlighting=warning -resharper_unused_member_in_super_global_highlighting=suggestion -resharper_unused_member_in_super_local_highlighting=warning -resharper_unused_member_local_highlighting=warning -resharper_unused_method_return_value_global_highlighting=suggestion -resharper_unused_method_return_value_local_highlighting=warning -resharper_unused_parameter_global_highlighting=suggestion -resharper_unused_parameter_highlighting=warning -resharper_unused_parameter_in_partial_method_highlighting=warning -resharper_unused_parameter_local_highlighting=warning -resharper_unused_property_highlighting=warning -resharper_unused_tuple_component_in_return_value_highlighting=warning -resharper_unused_type_global_highlighting=suggestion -resharper_unused_type_local_highlighting=warning -resharper_unused_type_parameter_highlighting=warning -resharper_unused_variable_highlighting=warning -resharper_usage_of_definitely_unassigned_value_highlighting=warning -resharper_usage_of_possibly_unassigned_value_highlighting=warning -resharper_useless_binary_operation_highlighting=warning -resharper_useless_comparison_to_integral_constant_highlighting=warning -resharper_use_array_creation_expression_1_highlighting=suggestion -resharper_use_array_creation_expression_2_highlighting=suggestion -resharper_use_array_empty_method_highlighting=suggestion -resharper_use_as_instead_of_type_cast_highlighting=hint -resharper_use_await_using_highlighting=suggestion -resharper_use_cancellation_token_for_i_async_enumerable_highlighting=suggestion -resharper_use_collection_count_property_highlighting=suggestion -resharper_use_configure_await_false_for_async_disposable_highlighting=none -resharper_use_configure_await_false_highlighting=suggestion -resharper_use_deconstruction_highlighting=hint -resharper_use_deconstruction_on_parameter_highlighting=hint -resharper_use_empty_types_field_highlighting=suggestion -resharper_use_event_args_empty_field_highlighting=suggestion -resharper_use_format_specifier_in_format_string_highlighting=suggestion -resharper_use_implicitly_typed_variable_evident_highlighting=hint -resharper_use_implicitly_typed_variable_highlighting=none -resharper_use_implicit_by_val_modifier_highlighting=hint -resharper_use_indexed_property_highlighting=suggestion -resharper_use_index_from_end_expression_highlighting=suggestion -resharper_use_is_operator_1_highlighting=suggestion -resharper_use_is_operator_2_highlighting=suggestion -resharper_use_method_any_0_highlighting=suggestion -resharper_use_method_any_1_highlighting=suggestion -resharper_use_method_any_2_highlighting=suggestion -resharper_use_method_any_3_highlighting=suggestion -resharper_use_method_any_4_highlighting=suggestion -resharper_use_method_is_instance_of_type_highlighting=suggestion -resharper_use_nameof_expression_for_part_of_the_string_highlighting=none -resharper_use_nameof_expression_highlighting=suggestion -resharper_use_name_of_instead_of_type_of_highlighting=suggestion -resharper_use_negated_pattern_in_is_expression_highlighting=hint -resharper_use_negated_pattern_matching_highlighting=hint -resharper_use_nullable_annotation_instead_of_attribute_highlighting=suggestion -resharper_use_nullable_attributes_supported_by_compiler_highlighting=suggestion -resharper_use_nullable_reference_types_annotation_syntax_highlighting=warning -resharper_use_null_propagation_highlighting=hint -resharper_use_null_propagation_when_possible_highlighting=none -resharper_use_object_or_collection_initializer_highlighting=suggestion -resharper_use_of_implicit_global_in_function_scope_highlighting=warning -resharper_use_of_possibly_unassigned_property_highlighting=warning -resharper_use_pattern_matching_highlighting=suggestion -resharper_use_positional_deconstruction_pattern_highlighting=none -resharper_use_string_interpolation_highlighting=suggestion -resharper_use_switch_case_pattern_variable_highlighting=suggestion -resharper_use_throw_if_null_method_highlighting=none -resharper_use_verbatim_string_highlighting=hint -resharper_using_of_reserved_word_error_highlighting=error -resharper_using_of_reserved_word_highlighting=warning -resharper_value_parameter_not_used_highlighting=warning -resharper_value_range_attribute_violation_highlighting=warning -resharper_value_should_have_units_highlighting=error -resharper_variable_can_be_made_const_highlighting=hint -resharper_variable_can_be_made_let_highlighting=hint -resharper_variable_can_be_moved_to_inner_block_highlighting=hint -resharper_variable_can_be_not_nullable_highlighting=warning -resharper_variable_hides_outer_variable_highlighting=warning -resharper_variable_used_before_declared_highlighting=warning -resharper_variable_used_in_inner_scope_before_declared_highlighting=warning -resharper_variable_used_out_of_scope_highlighting=warning -resharper_vb_check_for_reference_equality_instead_1_highlighting=suggestion -resharper_vb_check_for_reference_equality_instead_2_highlighting=suggestion -resharper_vb_possible_mistaken_argument_highlighting=warning -resharper_vb_possible_mistaken_call_to_get_type_1_highlighting=warning -resharper_vb_possible_mistaken_call_to_get_type_2_highlighting=warning -resharper_vb_remove_to_list_1_highlighting=suggestion -resharper_vb_remove_to_list_2_highlighting=suggestion -resharper_vb_replace_with_first_or_default_highlighting=suggestion -resharper_vb_replace_with_last_or_default_highlighting=suggestion -resharper_vb_replace_with_of_type_1_highlighting=suggestion -resharper_vb_replace_with_of_type_2_highlighting=suggestion -resharper_vb_replace_with_of_type_any_1_highlighting=suggestion -resharper_vb_replace_with_of_type_any_2_highlighting=suggestion -resharper_vb_replace_with_of_type_count_1_highlighting=suggestion -resharper_vb_replace_with_of_type_count_2_highlighting=suggestion -resharper_vb_replace_with_of_type_first_1_highlighting=suggestion -resharper_vb_replace_with_of_type_first_2_highlighting=suggestion -resharper_vb_replace_with_of_type_first_or_default_1_highlighting=suggestion -resharper_vb_replace_with_of_type_first_or_default_2_highlighting=suggestion -resharper_vb_replace_with_of_type_last_1_highlighting=suggestion -resharper_vb_replace_with_of_type_last_2_highlighting=suggestion -resharper_vb_replace_with_of_type_last_or_default_1_highlighting=suggestion -resharper_vb_replace_with_of_type_last_or_default_2_highlighting=suggestion -resharper_vb_replace_with_of_type_single_1_highlighting=suggestion -resharper_vb_replace_with_of_type_single_2_highlighting=suggestion -resharper_vb_replace_with_of_type_single_or_default_1_highlighting=suggestion -resharper_vb_replace_with_of_type_single_or_default_2_highlighting=suggestion -resharper_vb_replace_with_of_type_where_highlighting=suggestion -resharper_vb_replace_with_single_assignment_1_highlighting=suggestion -resharper_vb_replace_with_single_assignment_2_highlighting=suggestion -resharper_vb_replace_with_single_call_to_any_highlighting=suggestion -resharper_vb_replace_with_single_call_to_count_highlighting=suggestion -resharper_vb_replace_with_single_call_to_first_highlighting=suggestion -resharper_vb_replace_with_single_call_to_first_or_default_highlighting=suggestion -resharper_vb_replace_with_single_call_to_last_highlighting=suggestion -resharper_vb_replace_with_single_call_to_last_or_default_highlighting=suggestion -resharper_vb_replace_with_single_call_to_single_highlighting=suggestion -resharper_vb_replace_with_single_call_to_single_or_default_highlighting=suggestion -resharper_vb_replace_with_single_or_default_highlighting=suggestion -resharper_vb_simplify_linq_expression_10_highlighting=hint -resharper_vb_simplify_linq_expression_1_highlighting=suggestion -resharper_vb_simplify_linq_expression_2_highlighting=suggestion -resharper_vb_simplify_linq_expression_3_highlighting=suggestion -resharper_vb_simplify_linq_expression_4_highlighting=suggestion -resharper_vb_simplify_linq_expression_5_highlighting=suggestion -resharper_vb_simplify_linq_expression_6_highlighting=suggestion -resharper_vb_simplify_linq_expression_7_highlighting=hint -resharper_vb_simplify_linq_expression_8_highlighting=hint -resharper_vb_simplify_linq_expression_9_highlighting=hint -resharper_vb_string_compare_is_culture_specific_1_highlighting=warning -resharper_vb_string_compare_is_culture_specific_2_highlighting=warning -resharper_vb_string_compare_is_culture_specific_3_highlighting=warning -resharper_vb_string_compare_is_culture_specific_4_highlighting=warning -resharper_vb_string_compare_is_culture_specific_5_highlighting=warning -resharper_vb_string_compare_is_culture_specific_6_highlighting=warning -resharper_vb_string_compare_to_is_culture_specific_highlighting=warning -resharper_vb_string_ends_with_is_culture_specific_highlighting=none -resharper_vb_string_index_of_is_culture_specific_1_highlighting=warning -resharper_vb_string_index_of_is_culture_specific_2_highlighting=warning -resharper_vb_string_index_of_is_culture_specific_3_highlighting=warning -resharper_vb_string_last_index_of_is_culture_specific_1_highlighting=warning -resharper_vb_string_last_index_of_is_culture_specific_2_highlighting=warning -resharper_vb_string_last_index_of_is_culture_specific_3_highlighting=warning -resharper_vb_string_starts_with_is_culture_specific_highlighting=none -resharper_vb_unreachable_code_highlighting=warning -resharper_vb_use_array_creation_expression_1_highlighting=suggestion -resharper_vb_use_array_creation_expression_2_highlighting=suggestion -resharper_vb_use_first_instead_highlighting=warning -resharper_vb_use_method_any_1_highlighting=suggestion -resharper_vb_use_method_any_2_highlighting=suggestion -resharper_vb_use_method_any_3_highlighting=suggestion -resharper_vb_use_method_any_4_highlighting=suggestion -resharper_vb_use_method_any_5_highlighting=suggestion -resharper_vb_use_method_is_instance_of_type_highlighting=suggestion -resharper_vb_use_type_of_is_operator_1_highlighting=suggestion -resharper_vb_use_type_of_is_operator_2_highlighting=suggestion -resharper_virtual_member_call_in_constructor_highlighting=warning -resharper_virtual_member_never_overridden_global_highlighting=suggestion -resharper_virtual_member_never_overridden_local_highlighting=suggestion -resharper_void_method_with_must_use_return_value_attribute_highlighting=warning -resharper_web_config_module_not_resolved_highlighting=error -resharper_web_config_module_qualification_resolve_highlighting=warning -resharper_web_config_redundant_add_namespace_tag_highlighting=warning -resharper_web_config_redundant_location_tag_highlighting=warning -resharper_web_config_tag_prefix_redundand_highlighting=warning -resharper_web_config_type_not_resolved_highlighting=error -resharper_web_config_unused_add_tag_highlighting=warning -resharper_web_config_unused_element_due_to_config_source_attribute_highlighting=warning -resharper_web_config_unused_remove_or_clear_tag_highlighting=warning -resharper_web_config_web_config_path_warning_highlighting=warning -resharper_web_config_wrong_module_highlighting=error -resharper_web_ignored_path_highlighting=none -resharper_web_mapped_path_highlighting=hint -resharper_with_expression_instead_of_initializer_highlighting=suggestion -resharper_with_statement_using_error_highlighting=error -resharper_wrong_expression_statement_highlighting=warning -resharper_wrong_indent_size_highlighting=none -resharper_wrong_metadata_use_highlighting=none -resharper_wrong_public_modifier_specification_highlighting=hint -resharper_wrong_require_relative_path_highlighting=hint -resharper_xaml_assign_null_to_not_null_attribute_highlighting=warning -resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting=warning -resharper_xaml_binding_without_context_not_resolved_highlighting=hint -resharper_xaml_binding_with_context_not_resolved_highlighting=warning -resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting=error -resharper_xaml_constructor_warning_highlighting=warning -resharper_xaml_decimal_parsing_is_culture_dependent_highlighting=warning -resharper_xaml_dependency_property_resolve_error_highlighting=warning -resharper_xaml_duplicate_style_setter_highlighting=warning -resharper_xaml_dynamic_resource_error_highlighting=error -resharper_xaml_element_name_reference_not_resolved_highlighting=error -resharper_xaml_empty_grid_length_definition_highlighting=error -resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting=hint -resharper_xaml_ignored_path_highlighting_highlighting=none -resharper_xaml_index_out_of_grid_definition_highlighting=warning -resharper_xaml_invalid_member_type_highlighting=error -resharper_xaml_invalid_resource_target_type_highlighting=error -resharper_xaml_invalid_resource_type_highlighting=error -resharper_xaml_invalid_type_highlighting=error -resharper_xaml_language_level_highlighting=error -resharper_xaml_mapped_path_highlighting_highlighting=hint -resharper_xaml_method_arguments_will_be_ignored_highlighting=warning -resharper_xaml_missing_grid_index_highlighting=warning -resharper_xaml_overloads_collision_highlighting=warning -resharper_xaml_parent_is_out_of_current_component_tree_highlighting=warning -resharper_xaml_path_error_highlighting=warning -resharper_xaml_possible_null_reference_exception_highlighting=suggestion -resharper_xaml_redundant_attached_property_highlighting=warning -resharper_xaml_redundant_binding_mode_attribute_highlighting=warning -resharper_xaml_redundant_collection_property_highlighting=warning -resharper_xaml_redundant_freeze_attribute_highlighting=warning -resharper_xaml_redundant_grid_definitions_highlighting=warning -resharper_xaml_redundant_grid_span_highlighting=warning -resharper_xaml_redundant_modifiers_attribute_highlighting=warning -resharper_xaml_redundant_namespace_alias_highlighting=warning -resharper_xaml_redundant_name_attribute_highlighting=warning -resharper_xaml_redundant_property_type_qualifier_highlighting=warning -resharper_xaml_redundant_resource_highlighting=warning -resharper_xaml_redundant_styled_value_highlighting=warning -resharper_xaml_redundant_update_source_trigger_attribute_highlighting=warning -resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting=warning -resharper_xaml_resource_file_path_case_mismatch_highlighting=warning -resharper_xaml_routed_event_resolve_error_highlighting=warning -resharper_xaml_static_resource_not_resolved_highlighting=warning -resharper_xaml_style_class_not_found_highlighting=warning -resharper_xaml_style_invalid_target_type_highlighting=error -resharper_xaml_unexpected_text_token_highlighting=error -resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting=error -resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting=warning -resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting=warning -resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting=warning -resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting=warning -resharper_xaml_x_key_attribute_disallowed_highlighting=error -resharper_xml_doc_comment_syntax_problem_highlighting=warning -resharper_xunit_xunit_test_with_console_output_highlighting=warning - -# Standard properties -end_of_line= crlf -csharp_indent_labels = one_less_than_current -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -[*.{cshtml,htm,html,proto,razor}] -indent_style=tab -indent_size=tab -tab_width=4 - -[*.{asax,ascx,aspx,axaml,c,c++,cc,cginc,compute,cp,cpp,cs,css,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,js,jsx,master,mpp,mq4,mq5,mqh,paml,skin,tpp,ts,tsx,usf,ush,vb,xaml,xamlx,xoml}] -indent_style=space -indent_size=4 -tab_width=4 - -[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] -indent_style=space -indent_size= 4 -tab_width= 4 -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs deleted file mode 100644 index f097e578..00000000 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Objects.Enums; -using Newtonsoft.Json.Linq; -using Penumbra.String; - -namespace Penumbra.GameData.Actors; - -[StructLayout(LayoutKind.Explicit)] -public readonly struct ActorIdentifier : IEquatable -{ - public static ActorManager? Manager; - - public static readonly ActorIdentifier Invalid = new(IdentifierType.Invalid, 0, 0, 0, ByteString.Empty); - - public enum RetainerType : ushort - { - Both = 0, - Bell = 1, - Mannequin = 2, - } - - // @formatter:off - [FieldOffset( 0 )] public readonly IdentifierType Type; // All - [FieldOffset( 1 )] public readonly ObjectKind Kind; // Npc, Owned - [FieldOffset( 2 )] public readonly ushort HomeWorld; // Player, Owned - [FieldOffset( 2 )] public readonly ushort Index; // NPC - [FieldOffset( 2 )] public readonly RetainerType Retainer; // Retainer - [FieldOffset( 2 )] public readonly ScreenActor Special; // Special - [FieldOffset( 4 )] public readonly uint DataId; // Owned, NPC - [FieldOffset( 8 )] public readonly ByteString PlayerName; // Player, Owned - // @formatter:on - - public ActorIdentifier CreatePermanent() - => new(Type, Kind, Index, DataId, PlayerName.IsEmpty || PlayerName.IsOwned ? PlayerName : PlayerName.Clone()); - - public bool Equals(ActorIdentifier other) - { - if (Type != other.Type) - return false; - - return Type switch - { - IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), - IdentifierType.Retainer => PlayerName.EqualsCi(other.PlayerName), - IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), - IdentifierType.Special => Special == other.Special, - IdentifierType.Npc => Manager.DataIdEquals(this, other) - && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), - IdentifierType.UnkObject => PlayerName.EqualsCi(other.PlayerName) && Index == other.Index, - _ => false, - }; - } - - public override bool Equals(object? obj) - => obj is ActorIdentifier other && Equals(other); - - public static bool operator ==(ActorIdentifier lhs, ActorIdentifier rhs) - => lhs.Equals(rhs); - - public static bool operator !=(ActorIdentifier lhs, ActorIdentifier rhs) - => !lhs.Equals(rhs); - - public bool IsValid - => Type is not (IdentifierType.UnkObject or IdentifierType.Invalid); - - public string Incognito(string? name) - { - name ??= ToString(); - if (Type is not (IdentifierType.Player or IdentifierType.Owned)) - return name; - - var parts = name.Split(' ', 3); - return string.Join(" ", - parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); - } - - public override string ToString() - => Manager?.ToString(this) - ?? Type switch - { - IdentifierType.Player => $"{PlayerName} ({HomeWorld})", - IdentifierType.Retainer => $"{PlayerName} (Retainer)", - IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", - IdentifierType.Special => Special.ToName(), - IdentifierType.Npc => - Index == ushort.MaxValue - ? $"{Kind.ToName()} #{DataId}" - : $"{Kind.ToName()} #{DataId} at {Index}", - IdentifierType.UnkObject => PlayerName.IsEmpty - ? $"Unknown Object at {Index}" - : $"{PlayerName} at {Index}", - _ => "Invalid", - }; - - public string ToName() - => Manager?.ToName(this) ?? "Unknown Object"; - - public override int GetHashCode() - => Type switch - { - IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), - IdentifierType.Retainer => HashCode.Combine(IdentifierType.Player, PlayerName), - IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), - IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), - IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), - IdentifierType.UnkObject => HashCode.Combine(IdentifierType.UnkObject, PlayerName, Index), - _ => 0, - }; - - internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName) - { - Type = type; - Kind = kind; - Special = (ScreenActor)index; - HomeWorld = Index = index; - DataId = data; - PlayerName = playerName; - } - - public JObject ToJson() - { - var ret = new JObject { { nameof(Type), Type.ToString() } }; - switch (Type) - { - case IdentifierType.Player: - ret.Add(nameof(PlayerName), PlayerName.ToString()); - ret.Add(nameof(HomeWorld), HomeWorld); - return ret; - case IdentifierType.Retainer: - ret.Add(nameof(PlayerName), PlayerName.ToString()); - return ret; - case IdentifierType.Owned: - ret.Add(nameof(PlayerName), PlayerName.ToString()); - ret.Add(nameof(HomeWorld), HomeWorld); - ret.Add(nameof(Kind), Kind.ToString()); - ret.Add(nameof(DataId), DataId); - return ret; - case IdentifierType.Special: - ret.Add(nameof(Special), Special.ToString()); - return ret; - case IdentifierType.Npc: - ret.Add(nameof(Kind), Kind.ToString()); - if (Index != ushort.MaxValue) - ret.Add(nameof(Index), Index); - ret.Add(nameof(DataId), DataId); - return ret; - case IdentifierType.UnkObject: - ret.Add(nameof(PlayerName), PlayerName.ToString()); - ret.Add(nameof(Index), Index); - return ret; - } - - return ret; - } -} - -public static class ActorManagerExtensions -{ - public static bool DataIdEquals(this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs) - { - if (lhs.Kind != rhs.Kind) - return false; - - if (lhs.DataId == rhs.DataId) - return true; - - if (manager == null) - return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; - - var dict = lhs.Kind switch - { - ObjectKind.MountType => manager.Data.Mounts, - ObjectKind.Companion => manager.Data.Companions, - ObjectKind.Ornament => manager.Data.Ornaments, - ObjectKind.BattleNpc => manager.Data.BNpcs, - ObjectKind.EventNpc => manager.Data.ENpcs, - _ => new Dictionary(), - }; - - return dict.TryGetValue(lhs.DataId, out var lhsName) - && dict.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase); - } - - public static string ToName(this ObjectKind kind) - => kind switch - { - ObjectKind.None => "Unknown", - ObjectKind.BattleNpc => "Battle NPC", - ObjectKind.EventNpc => "Event NPC", - ObjectKind.MountType => "Mount", - ObjectKind.Companion => "Companion", - ObjectKind.Ornament => "Accessory", - _ => kind.ToString(), - }; - - public static string ToName(this IdentifierType type) - => type switch - { - IdentifierType.Player => "Player", - IdentifierType.Retainer => "Retainer (Bell)", - IdentifierType.Owned => "Owned NPC", - IdentifierType.Special => "Special Actor", - IdentifierType.Npc => "NPC", - IdentifierType.UnkObject => "Unknown Object", - _ => "Invalid", - }; - - /// - /// Fixed names for special actors. - /// - public static string ToName(this ScreenActor actor) - => actor switch - { - ScreenActor.CharacterScreen => "Character Screen Actor", - ScreenActor.ExamineScreen => "Examine Screen Actor", - ScreenActor.FittingRoom => "Fitting Room Actor", - ScreenActor.DyePreview => "Dye Preview Actor", - ScreenActor.Portrait => "Portrait Actor", - _ => "Invalid", - }; -} diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs deleted file mode 100644 index c2a4a8cd..00000000 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ /dev/null @@ -1,406 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using Dalamud; -using Dalamud.Data; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Gui; -using Dalamud.Plugin; -using Dalamud.Utility; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.System.Framework; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Excel.GeneratedSheets; -using Lumina.Text; -using Penumbra.GameData.Data; -using Penumbra.GameData.Structs; -using Penumbra.String; -using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; - -namespace Penumbra.GameData.Actors; - -public sealed partial class ActorManager : IDisposable -{ - public sealed class ActorManagerData : DataSharer - { - /// Worlds available for players. - public IReadOnlyDictionary Worlds { get; } - - /// Valid Mount names in title case by mount id. - public IReadOnlyDictionary Mounts { get; } - - /// Valid Companion names in title case by companion id. - public IReadOnlyDictionary Companions { get; } - - /// Valid ornament names by id. - public IReadOnlyDictionary Ornaments { get; } - - /// Valid BNPC names in title case by BNPC Name id. - public IReadOnlyDictionary BNpcs { get; } - - /// Valid ENPC names in title case by ENPC id. - public IReadOnlyDictionary ENpcs { get; } - - public ActorManagerData(DalamudPluginInterface pluginInterface, DataManager gameData, ClientLanguage language) - : base(pluginInterface, language, 2) - { - var worldTask = TryCatchDataAsync("Worlds", CreateWorldData(gameData)); - var mountsTask = TryCatchDataAsync("Mounts", CreateMountData(gameData)); - var companionsTask = TryCatchDataAsync("Companions", CreateCompanionData(gameData)); - var ornamentsTask = TryCatchDataAsync("Ornaments", CreateOrnamentData(gameData)); - var bNpcsTask = TryCatchDataAsync("BNpcs", CreateBNpcData(gameData)); - var eNpcsTask = TryCatchDataAsync("ENpcs", CreateENpcData(gameData)); - - Worlds = worldTask.Result; - Mounts = mountsTask.Result; - Companions = companionsTask.Result; - Ornaments = ornamentsTask.Result; - BNpcs = bNpcsTask.Result; - ENpcs = eNpcsTask.Result; - } - - /// - /// Return the world name including the Any World option. - /// - public string ToWorldName(ushort worldId) - => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; - - /// - /// Return the world id corresponding to the given name. - /// - /// ushort.MaxValue if the name is empty, 0 if it is not a valid world, or the worlds id. - public ushort ToWorldId(string worldName) - => worldName.Length != 0 - ? Worlds.FirstOrDefault(kvp => string.Equals(kvp.Value, worldName, StringComparison.OrdinalIgnoreCase), default).Key - : ushort.MaxValue; - - /// - /// Convert a given ID for a certain ObjectKind to a name. - /// - /// Invalid or a valid name. - public string ToName(ObjectKind kind, uint dataId) - => TryGetName(kind, dataId, out var ret) ? ret : "Invalid"; - - - /// - /// Convert a given ID for a certain ObjectKind to a name. - /// - public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name) - { - name = null; - return kind switch - { - ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), - ObjectKind.Companion => Companions.TryGetValue(dataId, out name), - ObjectKind.Ornament => Ornaments.TryGetValue(dataId, out name), - ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), - ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), - _ => false, - }; - } - - protected override void DisposeInternal() - { - DisposeTag("Worlds"); - DisposeTag("Mounts"); - DisposeTag("Companions"); - DisposeTag("Ornaments"); - DisposeTag("BNpcs"); - DisposeTag("ENpcs"); - } - - private Action> CreateWorldData(DataManager gameData) - => d => - { - foreach (var w in gameData.GetExcelSheet(Language)!.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)) - d.TryAdd((ushort)w.RowId, string.Intern(w.Name.ToDalamudString().TextValue)); - }; - - private Action> CreateMountData(DataManager gameData) - => d => - { - foreach (var m in gameData.GetExcelSheet(Language)!.Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0)) - d.TryAdd(m.RowId, ToTitleCaseExtended(m.Singular, m.Article)); - }; - - private Action> CreateCompanionData(DataManager gameData) - => d => - { - foreach (var c in gameData.GetExcelSheet(Language)!.Where(c - => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue)) - d.TryAdd(c.RowId, ToTitleCaseExtended(c.Singular, c.Article)); - }; - - private Action> CreateOrnamentData(DataManager gameData) - => d => - { - foreach (var o in gameData.GetExcelSheet(Language)!.Where(o => o.Singular.RawData.Length > 0)) - d.TryAdd(o.RowId, ToTitleCaseExtended(o.Singular, o.Article)); - }; - - private Action> CreateBNpcData(DataManager gameData) - => d => - { - foreach (var n in gameData.GetExcelSheet(Language)!.Where(n => n.Singular.RawData.Length > 0)) - d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article)); - }; - - private Action> CreateENpcData(DataManager gameData) - => d => - { - foreach (var n in gameData.GetExcelSheet(Language)!.Where(e => e.Singular.RawData.Length > 0)) - d.TryAdd(n.RowId, ToTitleCaseExtended(n.Singular, n.Article)); - }; - - private static string ToTitleCaseExtended(SeString s, sbyte article) - { - if (article == 1) - return string.Intern(s.ToDalamudString().ToString()); - - var sb = new StringBuilder(s.ToDalamudString().ToString()); - var lastSpace = true; - for (var i = 0; i < sb.Length; ++i) - { - if (sb[i] == ' ') - { - lastSpace = true; - } - else if (lastSpace) - { - lastSpace = false; - sb[i] = char.ToUpperInvariant(sb[i]); - } - } - - return string.Intern(sb.ToString()); - } - } - - public readonly ActorManagerData Data; - - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, - DataManager gameData, GameGui gameGui, - Func toParentIdx) - : this(pluginInterface, objects, state, framework, gameData, gameGui, gameData.Language, toParentIdx) - { } - - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, Dalamud.Game.Framework framework, - DataManager gameData, GameGui gameGui, - ClientLanguage language, Func toParentIdx) - { - _framework = framework; - _objects = objects; - _gameGui = gameGui; - _clientState = state; - _toParentIdx = toParentIdx; - Data = new ActorManagerData(pluginInterface, gameData, language); - - ActorIdentifier.Manager = this; - - SignatureHelper.Initialise(this); - } - - public unsafe ActorIdentifier GetCurrentPlayer() - { - var address = (Character*)_objects.GetObjectAddress(0); - return address == null - ? ActorIdentifier.Invalid - : CreateIndividualUnchecked(IdentifierType.Player, new ByteString(address->GameObject.Name), address->HomeWorld, - ObjectKind.None, uint.MaxValue); - } - - public ActorIdentifier GetInspectPlayer() - { - var addon = _gameGui.GetAddonByName("CharacterInspect", 1); - if (addon == IntPtr.Zero) - return ActorIdentifier.Invalid; - - return CreatePlayer(InspectName, InspectWorldId); - } - - public unsafe bool ResolvePartyBannerPlayer(ScreenActor type, out ActorIdentifier id) - { - id = ActorIdentifier.Invalid; - var module = Framework.Instance()->GetUiModule()->GetAgentModule(); - if (module == null) - return false; - - var agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerParty); - if (agent == null || !agent->AgentInterface.IsAgentActive()) - agent = (AgentBannerInterface*)module->GetAgentByInternalId(AgentId.BannerMIP); - if (agent == null || !agent->AgentInterface.IsAgentActive()) - return false; - - var idx = (ushort)type - (ushort)ScreenActor.CharacterScreen; - var character = agent->Character(idx); - if (character == null) - return true; - - var name = new ByteString(character->Name1.StringPtr); - id = CreatePlayer(name, (ushort)character->WorldId); - return true; - } - - private unsafe bool SearchPlayerCustomize(Character* character, int idx, out ActorIdentifier id) - { - var other = (Character*)_objects.GetObjectAddress(idx); - if (other == null || !CustomizeData.ScreenActorEquals((CustomizeData*)character->DrawData.CustomizeData.Data, (CustomizeData*)other->DrawData.CustomizeData.Data)) - { - id = ActorIdentifier.Invalid; - return false; - } - - id = FromObject(&other->GameObject, out _, false, true, false); - return true; - } - - private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject, int idx1, int idx2, int idx3) - => SearchPlayerCustomize(gameObject, idx1, out var ret) - || SearchPlayerCustomize(gameObject, idx2, out ret) - || SearchPlayerCustomize(gameObject, idx3, out ret) - ? ret - : ActorIdentifier.Invalid; - - private unsafe ActorIdentifier SearchPlayersCustomize(Character* gameObject) - { - static bool Compare(Character* a, Character* b) - { - var data1 = (CustomizeData*)a->DrawData.CustomizeData.Data; - var data2 = (CustomizeData*)b->DrawData.CustomizeData.Data; - var equals = CustomizeData.ScreenActorEquals(data1, data2); - return equals; - } - - for (var i = 0; i < (int)ScreenActor.CutsceneStart; i += 2) - { - var obj = (GameObject*)_objects.GetObjectAddress(i); - if (obj != null - && obj->ObjectKind is (byte)ObjectKind.Player - && Compare(gameObject, (Character*)obj)) - return FromObject(obj, out _, false, true, false); - } - - return ActorIdentifier.Invalid; - } - - public unsafe bool ResolveMahjongPlayer(ScreenActor type, out ActorIdentifier id) - { - id = ActorIdentifier.Invalid; - if (_clientState.TerritoryType != 831 && _gameGui.GetAddonByName("EmjIntro") == IntPtr.Zero) - return false; - - var obj = (Character*)_objects.GetObjectAddress((int)type); - if (obj == null) - return false; - - id = type switch - { - ScreenActor.CharacterScreen => GetCurrentPlayer(), - ScreenActor.ExamineScreen => SearchPlayersCustomize(obj, 2, 4, 6), - ScreenActor.FittingRoom => SearchPlayersCustomize(obj, 4, 2, 6), - ScreenActor.DyePreview => SearchPlayersCustomize(obj, 6, 2, 4), - _ => ActorIdentifier.Invalid, - }; - return true; - } - - public unsafe bool ResolvePvPBannerPlayer(ScreenActor type, out ActorIdentifier id) - { - id = ActorIdentifier.Invalid; - if (!_clientState.IsPvPExcludingDen) - return false; - - var addon = (AtkUnitBase*)_gameGui.GetAddonByName("PvPMap"); - if (addon == null || addon->IsVisible) - return false; - - var obj = (Character*)_objects.GetObjectAddress((int)type); - if (obj == null) - return false; - - id = type switch - { - ScreenActor.CharacterScreen => SearchPlayersCustomize(obj), - ScreenActor.ExamineScreen => SearchPlayersCustomize(obj), - ScreenActor.FittingRoom => SearchPlayersCustomize(obj), - ScreenActor.DyePreview => SearchPlayersCustomize(obj), - ScreenActor.Portrait => SearchPlayersCustomize(obj), - _ => ActorIdentifier.Invalid, - }; - return true; - } - - public unsafe ActorIdentifier GetCardPlayer() - { - var agent = AgentCharaCard.Instance(); - if (agent == null || agent->Data == null) - return ActorIdentifier.Invalid; - - var worldId = *(ushort*)((byte*)agent->Data + Offsets.AgentCharaCardDataWorldId); - return CreatePlayer(new ByteString(agent->Data->Name.StringPtr), worldId); - } - - public ActorIdentifier GetGlamourPlayer() - { - var addon = _gameGui.GetAddonByName("MiragePrismMiragePlate", 1); - return addon == IntPtr.Zero ? ActorIdentifier.Invalid : GetCurrentPlayer(); - } - - public void Dispose() - { - Data.Dispose(); - if (ActorIdentifier.Manager == this) - ActorIdentifier.Manager = null; - } - - ~ActorManager() - => Dispose(); - - private readonly Dalamud.Game.Framework _framework; - private readonly ObjectTable _objects; - private readonly ClientState _clientState; - private readonly GameGui _gameGui; - - private readonly Func _toParentIdx; - - [Signature(Sigs.InspectTitleId, ScanType = ScanType.StaticAddress)] - private static unsafe ushort* _inspectTitleId = null!; - - [Signature(Sigs.InspectWorldId, ScanType = ScanType.StaticAddress)] - private static unsafe ushort* _inspectWorldId = null!; - - private static unsafe ushort InspectTitleId - => *_inspectTitleId; - - private static unsafe ByteString InspectName - => new((byte*)(_inspectWorldId + 1)); - - private static unsafe ushort InspectWorldId - => *_inspectWorldId; - - public static readonly IReadOnlySet MannequinIds = new HashSet() - { - 1026228u, - 1026229u, - 1026986u, - 1026987u, - 1026988u, - 1026989u, - 1032291u, - 1032292u, - 1032293u, - 1032294u, - 1033046u, - 1033047u, - 1033658u, - 1033659u, - 1007137u, - // TODO: Female Hrothgar - }; -} diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs deleted file mode 100644 index 8584a32b..00000000 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ /dev/null @@ -1,606 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Newtonsoft.Json.Linq; -using Penumbra.String; -using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; - -namespace Penumbra.GameData.Actors; - -public partial class ActorManager -{ - /// - /// Try to create an ActorIdentifier from a already parsed JObject . - /// - /// A parsed JObject - /// ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise. - public ActorIdentifier FromJson(JObject? data) - { - if (data == null) - return ActorIdentifier.Invalid; - - var type = data[nameof(ActorIdentifier.Type)]?.ToObject() ?? IdentifierType.Invalid; - switch (type) - { - case IdentifierType.Player: - { - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); - var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; - return CreatePlayer(name, homeWorld); - } - case IdentifierType.Retainer: - { - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); - return CreateRetainer(name, 0); - } - case IdentifierType.Owned: - { - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); - var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; - var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject() ?? ObjectKind.CardStand; - var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; - return CreateOwned(name, homeWorld, kind, dataId); - } - case IdentifierType.Special: - { - var special = data[nameof(ActorIdentifier.Special)]?.ToObject() ?? 0; - return CreateSpecial(special); - } - case IdentifierType.Npc: - { - var index = data[nameof(ActorIdentifier.Index)]?.ToObject() ?? ushort.MaxValue; - var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject() ?? ObjectKind.CardStand; - var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; - return CreateNpc(kind, dataId, index); - } - case IdentifierType.UnkObject: - { - var index = data[nameof(ActorIdentifier.Index)]?.ToObject() ?? ushort.MaxValue; - var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); - return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); - } - default: return ActorIdentifier.Invalid; - } - } - - /// - /// Use stored data to convert an ActorIdentifier to a string. - /// - public string ToString(ActorIdentifier id) - { - return id.Type switch - { - IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})" - : id.PlayerName.ToString(), - IdentifierType.Retainer => id.PlayerName.ToString(), - IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Data.ToWorldName(id.HomeWorld)})'s {Data.ToName(id.Kind, id.DataId)}" - : $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", - IdentifierType.Special => id.Special.ToName(), - IdentifierType.Npc => - id.Index == ushort.MaxValue - ? Data.ToName(id.Kind, id.DataId) - : $"{Data.ToName(id.Kind, id.DataId)} at {id.Index}", - IdentifierType.UnkObject => id.PlayerName.IsEmpty - ? $"Unknown Object at {id.Index}" - : $"{id.PlayerName} at {id.Index}", - _ => "Invalid", - }; - } - - /// - /// Use stored data to convert an ActorIdentifier to a name only. - /// - public string ToName(ActorIdentifier id) - { - return id.Type switch - { - IdentifierType.Player => id.PlayerName.ToString(), - IdentifierType.Retainer => id.PlayerName.ToString(), - IdentifierType.Owned => $"{id.PlayerName}s {Data.ToName(id.Kind, id.DataId)}", - IdentifierType.Special => id.Special.ToName(), - IdentifierType.Npc => Data.ToName(id.Kind, id.DataId), - IdentifierType.UnkObject => id.PlayerName.IsEmpty ? id.PlayerName.ToString() : "Unknown Object", - _ => "Invalid", - }; - } - - private unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* HandleCutscene( - FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* main) - { - if (main == null) - return null; - - if (main->ObjectIndex is >= (ushort)ScreenActor.CutsceneStart and < (ushort)ScreenActor.CutsceneEnd) - { - var parentIdx = _toParentIdx(main->ObjectIndex); - if (parentIdx >= 0) - return (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx); - } - - return main; - } - - public class IdentifierParseError : Exception - { - public IdentifierParseError(string reason) - : base(reason) - { } - } - - public ActorIdentifier FromUserString(string userString) - { - if (userString.Length == 0) - throw new IdentifierParseError("The identifier string was empty."); - - var split = userString.Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (split.Length < 2) - throw new IdentifierParseError($"The identifier string {userString} does not contain a type and a value."); - - IdentifierType type; - var playerName = ByteString.Empty; - ushort worldId = 0; - var kind = ObjectKind.Player; - var objectId = 0u; - - (ByteString, ushort) ParsePlayer(string player) - { - var parts = player.Split('@', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (!VerifyPlayerName(parts[0])) - throw new IdentifierParseError($"{parts[0]} is not a valid player name."); - if (!ByteString.FromString(parts[0], out var p)) - throw new IdentifierParseError($"The player string {parts[0]} contains invalid symbols."); - - var world = parts.Length == 2 - ? Data.ToWorldId(parts[1]) - : ushort.MaxValue; - - if (!VerifyWorld(world)) - throw new IdentifierParseError($"{parts[1]} is not a valid world name."); - - return (p, world); - } - - (ObjectKind, uint) ParseNpc(string npc) - { - var split = npc.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (split.Length != 2) - throw new IdentifierParseError("NPCs need to be specified by '[Object Type]:[NPC Name]'."); - - static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) - { - var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), - new KeyValuePair(uint.MaxValue, string.Empty)); - dataId = kvp.Key; - return kvp.Value.Length > 0; - } - - switch (split[0].ToLowerInvariant()) - { - case "m": - case "mount": - return FindDataId(split[1], Data.Mounts, out var id) - ? (ObjectKind.MountType, id) - : throw new IdentifierParseError($"Could not identify a Mount named {split[1]}."); - case "c": - case "companion": - case "minion": - case "mini": - return FindDataId(split[1], Data.Companions, out id) - ? (ObjectKind.Companion, id) - : throw new IdentifierParseError($"Could not identify a Minion named {split[1]}."); - case "a": - case "o": - case "accessory": - case "ornament": - return FindDataId(split[1], Data.Ornaments, out id) - ? (ObjectKind.Ornament, id) - : throw new IdentifierParseError($"Could not identify an Accessory named {split[1]}."); - case "e": - case "enpc": - case "eventnpc": - case "event npc": - return FindDataId(split[1], Data.ENpcs, out id) - ? (ObjectKind.EventNpc, id) - : throw new IdentifierParseError($"Could not identify an Event NPC named {split[1]}."); - case "b": - case "bnpc": - case "battlenpc": - case "battle npc": - return FindDataId(split[1], Data.BNpcs, out id) - ? (ObjectKind.BattleNpc, id) - : throw new IdentifierParseError($"Could not identify a Battle NPC named {split[1]}."); - default: throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type."); - } - } - - switch (split[0].ToLowerInvariant()) - { - case "p": - case "player": - type = IdentifierType.Player; - (playerName, worldId) = ParsePlayer(split[1]); - break; - case "r": - case "retainer": - type = IdentifierType.Retainer; - if (!VerifyRetainerName(split[1])) - throw new IdentifierParseError($"{split[1]} is not a valid player name."); - if (!ByteString.FromString(split[1], out playerName)) - throw new IdentifierParseError($"The retainer string {split[1]} contains invalid symbols."); - - break; - case "n": - case "npc": - type = IdentifierType.Npc; - (kind, objectId) = ParseNpc(split[1]); - break; - case "o": - case "owned": - if (split.Length < 3) - throw new IdentifierParseError( - "Owned NPCs need a NPC and a player, separated by '|', but only one was provided."); - - type = IdentifierType.Owned; - (kind, objectId) = ParseNpc(split[1]); - (playerName, worldId) = ParsePlayer(split[2]); - break; - default: - throw new IdentifierParseError( - $"{split[0]} is not a valid identifier type. Valid types are [P]layer, [R]etainer, [N]PC, or [O]wned"); - } - - return CreateIndividualUnchecked(type, playerName, worldId, kind, objectId); - } - - /// - /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. - /// - public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check, bool withoutIndex) - { - owner = null; - if (actor == null) - return ActorIdentifier.Invalid; - - actor = HandleCutscene(actor); - var idx = actor->ObjectIndex; - if (idx is >= (ushort)ScreenActor.CharacterScreen and <= (ushort)ScreenActor.Card8) - return CreateIndividualUnchecked(IdentifierType.Special, ByteString.Empty, idx, ObjectKind.None, uint.MaxValue); - - var kind = (ObjectKind)actor->ObjectKind; - switch (kind) - { - case ObjectKind.Player: - { - var name = new ByteString(actor->Name); - var homeWorld = ((Character*)actor)->HomeWorld; - return check - ? CreatePlayer(name, homeWorld) - : CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue); - } - case ObjectKind.BattleNpc: - { - var ownerId = actor->OwnerID; - // 952 -> 780 is a special case for chocobos because they have NameId == 0 otherwise. - var nameId = actor->DataID == 952 ? 780 : ((Character*)actor)->NameID; - if (ownerId != 0xE0000000) - { - owner = HandleCutscene( - (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero)); - if (owner == null) - return ActorIdentifier.Invalid; - - var name = new ByteString(owner->Name); - var homeWorld = ((Character*)owner)->HomeWorld; - return check - ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) - : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); - } - - // Hack to support Anamnesis changing ObjectKind for NPC faces. - if (nameId == 0 && allowPlayerNpc) - { - var name = new ByteString(actor->Name); - if (!name.IsEmpty) - { - var homeWorld = ((Character*)actor)->HomeWorld; - return check - ? CreatePlayer(name, homeWorld) - : CreateIndividualUnchecked(IdentifierType.Player, name, homeWorld, ObjectKind.None, uint.MaxValue); - } - } - - var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; - return check - ? CreateNpc(ObjectKind.BattleNpc, nameId, index) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.BattleNpc, nameId); - } - case ObjectKind.EventNpc: - { - var dataId = actor->DataID; - // Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF - if (dataId == 0xF845D) - dataId = actor->GetNpcID(); - if (MannequinIds.Contains(dataId)) - { - static ByteString Get(byte* ptr) - => ptr == null ? ByteString.Empty : new ByteString(ptr); - - var retainerName = Get(actor->Name); - var actualName = _framework.IsInFrameworkUpdateThread ? Get(actor->GetName()) : ByteString.Empty; - if (!actualName.Equals(retainerName)) - { - var ident = check - ? CreateRetainer(retainerName, ActorIdentifier.RetainerType.Mannequin) - : CreateIndividualUnchecked(IdentifierType.Retainer, retainerName, (ushort)ActorIdentifier.RetainerType.Mannequin, - ObjectKind.EventNpc, dataId); - if (ident.IsValid) - return ident; - } - } - - var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; - return check - ? CreateNpc(ObjectKind.EventNpc, dataId, index) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, index, ObjectKind.EventNpc, dataId); - } - case ObjectKind.MountType: - case ObjectKind.Companion: - case ObjectKind.Ornament: - { - owner = HandleCutscene( - (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(actor->ObjectIndex - 1)); - if (owner == null) - return ActorIdentifier.Invalid; - - var dataId = GetCompanionId(actor, (Character*)owner); - var name = new ByteString(owner->Name); - var homeWorld = ((Character*)owner)->HomeWorld; - return check - ? CreateOwned(name, homeWorld, kind, dataId) - : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, kind, dataId); - } - case ObjectKind.Retainer: - { - var name = new ByteString(actor->Name); - return check - ? CreateRetainer(name, ActorIdentifier.RetainerType.Bell) - : CreateIndividualUnchecked(IdentifierType.Retainer, name, (ushort)ActorIdentifier.RetainerType.Bell, ObjectKind.None, - uint.MaxValue); - } - default: - { - var name = new ByteString(actor->Name); - var index = withoutIndex ? ushort.MaxValue : actor->ObjectIndex; - return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); - } - } - } - - /// - /// Obtain the current companion ID for an object by its actor and owner. - /// - private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - Character* owner) - { - return (ObjectKind)actor->ObjectKind switch - { - ObjectKind.MountType => owner->Mount.MountId, - ObjectKind.Ornament => owner->Ornament.OrnamentId, - ObjectKind.Companion => actor->DataID, - _ => actor->DataID, - }; - } - - public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, - bool allowPlayerNpc, bool check, bool withoutIndex) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, - check, withoutIndex); - - public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check, bool withoutIndex) - => FromObject(actor, out _, allowPlayerNpc, check, withoutIndex); - - public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) - => type switch - { - IdentifierType.Player => CreatePlayer(name, homeWorld), - IdentifierType.Retainer => CreateRetainer(name, (ActorIdentifier.RetainerType)homeWorld), - IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), - IdentifierType.Special => CreateSpecial((ScreenActor)homeWorld), - IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), - IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0), - _ => ActorIdentifier.Invalid, - }; - - /// - /// Only use this if you are sure the input is valid. - /// - public ActorIdentifier CreateIndividualUnchecked(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) - => new(type, kind, homeWorld, dataId, name); - - public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) - { - if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name.Span)) - return ActorIdentifier.Invalid; - - return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); - } - - public ActorIdentifier CreateRetainer(ByteString name, ActorIdentifier.RetainerType type) - { - if (!VerifyRetainerName(name.Span)) - return ActorIdentifier.Invalid; - - return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, (ushort)type, 0, name); - } - - public ActorIdentifier CreateSpecial(ScreenActor actor) - { - if (!VerifySpecial(actor)) - return ActorIdentifier.Invalid; - - return new ActorIdentifier(IdentifierType.Special, ObjectKind.Player, (ushort)actor, 0, ByteString.Empty); - } - - public ActorIdentifier CreateNpc(ObjectKind kind, uint data, ushort index = ushort.MaxValue) - { - if (!VerifyIndex(index) || !VerifyNpcData(kind, data)) - return ActorIdentifier.Invalid; - - return new ActorIdentifier(IdentifierType.Npc, kind, index, data, ByteString.Empty); - } - - public ActorIdentifier CreateOwned(ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId) - { - if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName.Span) || !VerifyOwnedData(kind, dataId)) - return ActorIdentifier.Invalid; - - return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName); - } - - /// Checks SE naming rules. - public static bool VerifyPlayerName(ReadOnlySpan name) - { - // Total no more than 20 characters + space. - if (name.Length is < 5 or > 21) - return false; - - // Forename and surname, no more spaces. - var splitIndex = name.IndexOf((byte)' '); - if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0) - return false; - - return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); - } - - /// Checks SE naming rules. - public static bool VerifyPlayerName(ReadOnlySpan name) - { - // Total no more than 20 characters + space. - if (name.Length is < 5 or > 21) - return false; - - // Forename and surname, no more spaces. - var splitIndex = name.IndexOf(' '); - if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0) - return false; - - return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); - } - - /// Checks SE naming rules. - public static bool VerifyRetainerName(ReadOnlySpan name) - => CheckNamePart(name, 3, 20); - - /// Checks SE naming rules. - public static bool VerifyRetainerName(ReadOnlySpan name) - => CheckNamePart(name, 3, 20); - - private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) - { - // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. - if (part.Length < minLength || part.Length > maxLength) - return false; - - // Each part starting with capitalized letter. - if (part[0] is < 'A' or > 'Z') - return false; - - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = '\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is '\'' && current is '-') - return false; - if (last is '-' && current is '-' or '\'') - return false; - - last = current; - } - - return true; - } - - private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) - { - // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. - if (part.Length < minLength || part.Length > maxLength) - return false; - - // Each part starting with capitalized letter. - if (part[0] is < (byte)'A' or > (byte)'Z') - return false; - - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = (byte)'\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is (byte)'\'' && current is (byte)'-') - return false; - if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') - return false; - - last = current; - } - - return true; - } - - /// Checks if the world is a valid public world or ushort.MaxValue (any world). - public bool VerifyWorld(ushort worldId) - => worldId == ushort.MaxValue || Data.Worlds.ContainsKey(worldId); - - /// Verify that the enum value is a specific actor and return the name if it is. - public static bool VerifySpecial(ScreenActor actor) - => actor is >= ScreenActor.CharacterScreen and <= ScreenActor.Card8; - - /// Verify that the object index is a valid index for an NPC. - public bool VerifyIndex(ushort index) - { - return index switch - { - ushort.MaxValue => true, - < 200 => index % 2 == 0, - > (ushort)ScreenActor.Card8 => index < _objects.Length, - _ => false, - }; - } - - /// Verify that the object kind is a valid owned object, and the corresponding data Id. - public bool VerifyOwnedData(ObjectKind kind, uint dataId) - { - return kind switch - { - ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), - ObjectKind.Companion => Data.Companions.ContainsKey(dataId), - ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId), - ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), - _ => false, - }; - } - - public bool VerifyNpcData(ObjectKind kind, uint dataId) - => kind switch - { - ObjectKind.MountType => Data.Mounts.ContainsKey(dataId), - ObjectKind.Companion => Data.Companions.ContainsKey(dataId), - ObjectKind.Ornament => Data.Ornaments.ContainsKey(dataId), - ObjectKind.BattleNpc => Data.BNpcs.ContainsKey(dataId), - ObjectKind.EventNpc => Data.ENpcs.ContainsKey(dataId), - _ => false, - }; -} diff --git a/Penumbra.GameData/Actors/AgentBannerParty.cs b/Penumbra.GameData/Actors/AgentBannerParty.cs deleted file mode 100644 index 2de95c8c..00000000 --- a/Penumbra.GameData/Actors/AgentBannerParty.cs +++ /dev/null @@ -1,91 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.System.String; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.System.Framework; - -namespace Penumbra; - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct AgentBannerInterface -{ - [FieldOffset( 0x0 )] public AgentInterface AgentInterface; - [FieldOffset( 0x28 )] public BannerInterfaceStorage* Data; - - public BannerInterfaceStorage.CharacterData* Character( int idx ) - => idx switch - { - _ when Data == null => null, - 0 => &Data->Character1, - 1 => &Data->Character2, - 2 => &Data->Character3, - 3 => &Data->Character4, - 4 => &Data->Character5, - 5 => &Data->Character6, - 6 => &Data->Character7, - 7 => &Data->Character8, - _ => null, - }; -} - -[StructLayout(LayoutKind.Explicit)] -public unsafe struct AgentBannerParty -{ - public static AgentBannerParty* Instance() => ( AgentBannerParty* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerParty ); - - [FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface; -} - -[StructLayout( LayoutKind.Explicit )] -public unsafe struct AgentBannerMIP -{ - public static AgentBannerMIP* Instance() => ( AgentBannerMIP* )Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId( AgentId.BannerMIP ); - [FieldOffset( 0x0 )] public AgentBannerInterface AgentBannerInterface; -} - -// Client::UI::Agent::AgentBannerInterface::Storage -// destructed in Client::UI::Agent::AgentBannerInterface::dtor -[StructLayout( LayoutKind.Explicit, Size = 0x3B30 )] -public unsafe struct BannerInterfaceStorage -{ - // vtable: 48 8D 05 ?? ?? ?? ?? 48 89 01 48 8B F9 7E - // dtor: E8 ?? ?? ?? ?? 48 83 EF ?? 75 ?? BA ?? ?? ?? ?? 48 8B CE E8 ?? ?? ?? ?? 48 89 7D - [StructLayout( LayoutKind.Explicit, Size = 0x760 )] - public struct CharacterData - { - [FieldOffset( 0x000 )] public void** VTable; - - [FieldOffset( 0x018 )] public Utf8String Name1; - [FieldOffset( 0x080 )] public Utf8String Name2; - [FieldOffset( 0x0E8 )] public Utf8String UnkString1; - [FieldOffset( 0x150 )] public Utf8String UnkString2; - [FieldOffset( 0x1C0 )] public Utf8String Job; - [FieldOffset( 0x238 )] public uint WorldId; - [FieldOffset( 0x240 )] public Utf8String UnkString3; - - [FieldOffset( 0x2B0 )] public void* CharaView; - [FieldOffset( 0x5D0 )] public AtkTexture AtkTexture; - - [FieldOffset( 0x6E0 )] public Utf8String Title; - [FieldOffset( 0x750 )] public void* SomePointer; - - } - - [FieldOffset( 0x0000 )] public void* Agent; // AgentBannerParty, maybe other Banner agents - [FieldOffset( 0x0008 )] public UIModule* UiModule; - [FieldOffset( 0x0010 )] public uint Unk1; // Maybe count or bitfield, but probably not - [FieldOffset( 0x0014 )] public uint Unk2; - - [FieldOffset( 0x0020 )] public CharacterData Character1; - [FieldOffset( 0x0780 )] public CharacterData Character2; - [FieldOffset( 0x0EE0 )] public CharacterData Character3; - [FieldOffset( 0x1640 )] public CharacterData Character4; - [FieldOffset( 0x1DA0 )] public CharacterData Character5; - [FieldOffset( 0x2500 )] public CharacterData Character6; - [FieldOffset( 0x2C60 )] public CharacterData Character7; - [FieldOffset( 0x33C0 )] public CharacterData Character8; - - [FieldOffset( 0x3B20 )] public long Unk3; - [FieldOffset( 0x3B28 )] public long Unk4; -} \ No newline at end of file diff --git a/Penumbra.GameData/Actors/IdentifierType.cs b/Penumbra.GameData/Actors/IdentifierType.cs deleted file mode 100644 index 8fe1ee4f..00000000 --- a/Penumbra.GameData/Actors/IdentifierType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Penumbra.GameData.Actors; - -public enum IdentifierType : byte -{ - Invalid, - Player, - Owned, - Special, - Npc, - Retainer, - UnkObject, -}; \ No newline at end of file diff --git a/Penumbra.GameData/Actors/ScreenActor.cs b/Penumbra.GameData/Actors/ScreenActor.cs deleted file mode 100644 index 00cf66fc..00000000 --- a/Penumbra.GameData/Actors/ScreenActor.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Penumbra.GameData.Actors; - -public enum ScreenActor : ushort -{ - CutsceneStart = 200, - GPosePlayer = 201, - CutsceneEnd = 240, - CharacterScreen = CutsceneEnd, - ExamineScreen = 241, - FittingRoom = 242, - DyePreview = 243, - Portrait = 244, - Card6 = 245, - Card7 = 246, - Card8 = 247, - ScreenEnd = Card8 + 1, -} diff --git a/Penumbra.GameData/Data/BNpcNames.cs b/Penumbra.GameData/Data/BNpcNames.cs deleted file mode 100644 index b268e827..00000000 --- a/Penumbra.GameData/Data/BNpcNames.cs +++ /dev/null @@ -1,16414 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Penumbra.GameData.Data; - -public static class NpcNames -{ - /// Generated from https://api.ffxivteamcraft.com/gubal on 2023-07-05. - public static IReadOnlyList> CreateNames() - => new IReadOnlyList[] - { - Array.Empty(), - new uint[]{6373}, - new uint[]{411, 412, 965, 1064, 1863, 2012}, - new uint[]{3, 176}, - new uint[]{4, 460}, - new uint[]{5, 184, 408}, - new uint[]{6, 183, 407, 2020}, - new uint[]{7, 125, 1121, 2157}, - new uint[]{8, 481}, - new uint[]{9, 571}, - new uint[]{10, 572, 589}, - new uint[]{11, 2033}, - new uint[]{12, 13, 299, 301, 590, 1039, 1122, 1216, 1315, 1743, 2011, 2757}, - new uint[]{397, 398, 566, 1067}, - new uint[]{14, 195, 202}, - new uint[]{16, 502}, - new uint[]{15}, - new uint[]{17, 132, 270, 316, 963, 1034, 1120, 1742, 2688, 2707, 2743, 2745}, - new uint[]{182, 319, 320, 321, 493, 1026, 1218, 2018}, - new uint[]{19, 322, 323, 324, 1025, 1219, 2019}, - new uint[]{20, 110, 494, 1854}, - new uint[]{21, 492, 606, 1028, 1029, 1086, 1198, 1349, 2039, 2169, 2223, 3687}, - new uint[]{22, 180, 400, 573, 1085, 4064}, - new uint[]{23, 162, 175, 1085, 3341}, - new uint[]{24, 163, 232, 233, 508, 607, 1136, 3343}, - new uint[]{25}, - new uint[]{26, 216, 217, 500, 592, 1040}, - new uint[]{27, 164, 1757}, - new uint[]{28, 966, 1070, 1071}, - new uint[]{29}, - new uint[]{30, 131, 280, 365, 552, 2996, 3621, 4108, 4286}, - Array.Empty(), - new uint[]{32, 203}, - new uint[]{33, 165, 214, 215, 497}, - new uint[]{34, 506}, - Array.Empty(), - new uint[]{36, 296, 485, 1051}, - new uint[]{37, 198, 1101}, - new uint[]{38, 279, 363, 364, 612, 1078, 1146}, - new uint[]{39, 177, 188, 962, 1350, 4046}, - new uint[]{40, 1145}, - new uint[]{41, 168, 201, 486, 1199}, - new uint[]{227, 284, 285, 489, 603, 1043, 1084}, - new uint[]{43, 181}, - new uint[]{44, 587, 1147, 2225, 4069}, - new uint[]{45, 491, 2918}, - new uint[]{46, 1703, 2056}, - new uint[]{47, 141, 199, 220, 699}, - new uint[]{48, 166}, - new uint[]{49, 200, 1046, 1222}, - Array.Empty(), - Array.Empty(), - new uint[]{50, 283, 464, 551, 1148}, - new uint[]{130, 206, 462}, - Array.Empty(), - new uint[]{53, 57, 1113, 1364, 1809, 1815, 1821, 2234, 2983, 3110, 3176}, - Array.Empty(), - new uint[]{54, 179, 201, 395, 564, 696, 1142}, - new uint[]{55, 2032}, - new uint[]{56, 7134}, - Array.Empty(), - new uint[]{58, 1810, 1816, 1822, 2173, 2351, 3181}, - new uint[]{59, 1811, 1817, 1823, 2120, 2173, 2238, 3177}, - new uint[]{60, 1812, 1818, 1824, 2235, 3178}, - new uint[]{61, 1365, 1813, 1819, 1825, 2173, 2236, 3180, 3334}, - new uint[]{62}, - new uint[]{1021}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{67, 229, 538, 1139}, - new uint[]{68, 230}, - new uint[]{69, 231, 2313}, - new uint[]{1138}, - Array.Empty(), - Array.Empty(), - new uint[]{73}, - new uint[]{74}, - Array.Empty(), - new uint[]{140, 239, 2362}, - new uint[]{2363}, - new uint[]{139, 240, 675, 2361, 4067, 4851}, - new uint[]{79}, - new uint[]{79}, - new uint[]{52, 80, 540}, - new uint[]{81}, - new uint[]{82, 172, 540}, - new uint[]{83}, - new uint[]{84}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{91}, - new uint[]{91}, - new uint[]{91, 317}, - new uint[]{91, 317}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{548}, - Array.Empty(), - new uint[]{310}, - new uint[]{314}, - new uint[]{312}, - new uint[]{101}, - new uint[]{102, 190, 209, 660, 1777}, - new uint[]{103, 208, 662, 1783}, - new uint[]{104, 210, 661, 1782}, - new uint[]{663, 1784}, - new uint[]{106, 352, 1066}, - new uint[]{107, 405, 1092, 1093, 2182}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{456}, - new uint[]{110}, - new uint[]{111}, - Array.Empty(), - Array.Empty(), - new uint[]{112, 171, 3342}, - new uint[]{113}, - new uint[]{114}, - new uint[]{115}, - new uint[]{116, 912}, - new uint[]{117}, - new uint[]{118, 298, 396, 490, 696, 1042}, - new uint[]{119}, - new uint[]{302, 399, 1033, 1088, 1089, 2756}, - new uint[]{391, 645, 950, 1134, 1758, 1831}, - new uint[]{237}, - new uint[]{174, 1366, 1846, 3994}, - new uint[]{120, 605}, - new uint[]{170, 1763, 2156}, - new uint[]{121}, - new uint[]{83}, - new uint[]{122}, - new uint[]{84}, - new uint[]{84}, - new uint[]{123}, - Array.Empty(), - new uint[]{125}, - new uint[]{185}, - new uint[]{186}, - new uint[]{186}, - new uint[]{189}, - Array.Empty(), - Array.Empty(), - new uint[]{139}, - new uint[]{197, 226, 381, 382}, - new uint[]{196, 289}, - new uint[]{228, 1758}, - Array.Empty(), - new uint[]{191}, - Array.Empty(), - new uint[]{140}, - new uint[]{245, 259, 1177, 1353, 2155, 2300}, - new uint[]{253, 256}, - new uint[]{249, 251, 252}, - new uint[]{259, 260, 1725}, - new uint[]{219, 268, 351, 570, 1072, 1115, 1170, 1244, 2987, 2989}, - new uint[]{1114, 1244}, - new uint[]{305, 1037, 1119}, - new uint[]{304, 1736}, - new uint[]{303}, - new uint[]{287, 288, 1045}, - new uint[]{286}, - new uint[]{1019}, - new uint[]{221, 222, 366, 1065, 1869}, - new uint[]{223, 224, 1316}, - new uint[]{282, 1036}, - new uint[]{281, 1116, 1181}, - new uint[]{207, 242, 467, 1738, 1768}, - new uint[]{242, 1739, 1759}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{276, 277, 278, 575, 901, 1041}, - new uint[]{275, 1741, 2008}, - new uint[]{1671, 1836}, - new uint[]{264, 1024, 1744, 1841, 3622, 4285}, - new uint[]{788, 1761, 3627, 3699}, - new uint[]{1344}, - new uint[]{273, 1030, 1125, 2049}, - new uint[]{274, 2050}, - Array.Empty(), - new uint[]{6733}, - new uint[]{292, 293, 1022}, - Array.Empty(), - Array.Empty(), - new uint[]{269, 1865}, - new uint[]{1814, 1820, 1826, 3179, 3545}, - Array.Empty(), - new uint[]{272, 1023}, - new uint[]{271}, - new uint[]{318, 409, 410, 574, 577, 958, 1049, 1082, 1083}, - new uint[]{306, 403, 1047, 1317}, - new uint[]{1185}, - new uint[]{1186}, - new uint[]{1185}, - new uint[]{1186}, - new uint[]{1185}, - new uint[]{1186}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{718}, - new uint[]{719}, - new uint[]{720}, - new uint[]{721}, - new uint[]{722}, - new uint[]{723}, - new uint[]{724}, - new uint[]{725}, - new uint[]{718}, - new uint[]{719}, - new uint[]{720}, - new uint[]{721}, - new uint[]{722}, - new uint[]{723}, - new uint[]{724}, - new uint[]{2752}, - new uint[]{1649}, - new uint[]{1647}, - new uint[]{1644}, - new uint[]{1649}, - new uint[]{1647}, - new uint[]{1644}, - new uint[]{1649}, - new uint[]{1647}, - new uint[]{1644}, - new uint[]{1801}, - new uint[]{1801}, - new uint[]{1801}, - new uint[]{1375}, - new uint[]{1376}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{422}, - new uint[]{424}, - new uint[]{425}, - new uint[]{428}, - new uint[]{444}, - new uint[]{443}, - new uint[]{437}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{440}, - new uint[]{988}, - Array.Empty(), - new uint[]{1208}, - new uint[]{442}, - new uint[]{441}, - new uint[]{423}, - new uint[]{426}, - new uint[]{427}, - new uint[]{428}, - new uint[]{633}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{134}, - new uint[]{553}, - Array.Empty(), - new uint[]{136}, - new uint[]{142}, - new uint[]{135}, - Array.Empty(), - new uint[]{596}, - new uint[]{234, 235, 308, 1135}, - new uint[]{1180, 1735}, - new uint[]{238, 505}, - new uint[]{236, 504, 1123, 1785, 2038, 3344}, - new uint[]{129, 211, 591, 1027, 1740}, - Array.Empty(), - new uint[]{104}, - Array.Empty(), - Array.Empty(), - new uint[]{77, 169, 241}, - new uint[]{360, 361, 1069, 1161}, - new uint[]{265, 349, 1100}, - new uint[]{341, 1068}, - new uint[]{1075, 1168, 2733}, - new uint[]{344}, - new uint[]{342, 979, 1107, 2732}, - new uint[]{978, 1107}, - Array.Empty(), - new uint[]{353, 687, 1091, 1167, 1748}, - new uint[]{355, 4114}, - new uint[]{357, 1076, 1313}, - new uint[]{358, 1077}, - new uint[]{563, 1069, 1102, 1103}, - new uint[]{560, 1081}, - new uint[]{561, 825, 1079, 1080, 1164}, - new uint[]{270, 1073, 1742, 1749}, - new uint[]{378, 379, 380, 1158}, - new uint[]{368, 369, 370, 1157}, - new uint[]{376, 1156, 1235}, - new uint[]{372, 373, 1155}, - Array.Empty(), - new uint[]{2165, 2167}, - new uint[]{1163, 2165, 2166}, - new uint[]{1162, 2166}, - new uint[]{2166}, - Array.Empty(), - new uint[]{392, 1098}, - new uint[]{393, 1096}, - new uint[]{394, 1087}, - new uint[]{401, 402, 1097}, - new uint[]{309, 404, 1035, 1123, 1791}, - new uint[]{414}, - new uint[]{413}, - new uint[]{416, 676}, - new uint[]{415}, - new uint[]{417, 1236}, - new uint[]{420, 421, 674, 1075, 1719}, - new uint[]{418, 420, 1719}, - new uint[]{419, 1173}, - new uint[]{262, 1050, 1101}, - new uint[]{326, 1309}, - new uint[]{329, 330, 1311}, - new uint[]{1310}, - new uint[]{328}, - new uint[]{234, 307, 503}, - new uint[]{290}, - new uint[]{243, 2511}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1115}, - new uint[]{331, 1997, 3332}, - new uint[]{333, 3332}, - new uint[]{332}, - Array.Empty(), - new uint[]{337}, - new uint[]{338}, - new uint[]{339}, - Array.Empty(), - Array.Empty(), - new uint[]{3332}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1769}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{636, 638, 1032, 1153}, - new uint[]{640, 642, 2021, 2533}, - new uint[]{684}, - new uint[]{632, 641, 893, 953, 1140}, - new uint[]{639, 1063, 3604}, - new uint[]{637, 685, 1638, 3338, 3572, 3589}, - new uint[]{658, 1017, 1141, 1780, 2232}, - new uint[]{635, 643}, - new uint[]{1182, 1760}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1850}, - new uint[]{784, 1755}, - new uint[]{1852}, - new uint[]{644, 2755}, - new uint[]{657, 1756, 1849, 3336, 3567, 3963}, - new uint[]{634, 3533}, - Array.Empty(), - new uint[]{1185}, - Array.Empty(), - new uint[]{567, 1183, 1789, 1853}, - new uint[]{1753}, - new uint[]{1021}, - new uint[]{480}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108, 1486, 1694, 2136, 2137, 2564, 2655, 2667, 2824, 3234, 3725, 3726, 3734, 3740, 3741, 4687, 4696, 4805, 5186, 5187, 5199, 5278, 5279, 5280, 5515, 5517, 5526, 5529, 5530, 5553, 5559, 5561, 5562, 5570, 5588, 6126, 6155, 6173, 6193, 6197, 6198, 6199}, - new uint[]{1204}, - new uint[]{342}, - new uint[]{1382}, - new uint[]{1205}, - new uint[]{1205}, - new uint[]{1382}, - new uint[]{1206}, - new uint[]{1207}, - Array.Empty(), - new uint[]{1298}, - new uint[]{1299}, - new uint[]{1300}, - new uint[]{1280}, - new uint[]{1281}, - new uint[]{1282}, - new uint[]{1283}, - new uint[]{1286}, - new uint[]{1279}, - new uint[]{1279}, - Array.Empty(), - new uint[]{282, 1036, 1038}, - new uint[]{294}, - new uint[]{1262}, - new uint[]{108, 148, 157, 444, 510, 686, 1185, 1459, 1466, 1644, 1645, 1646, 1680, 1801, 2137, 2143, 2154, 2160, 2193, 2265, 2294, 2345, 2547, 2549, 2595, 2598, 2605, 2633, 2814, 2815, 2821, 2846, 2903, 2907, 3091, 3093, 3227, 3231, 3234, 3240, 3252, 3287, 3376, 3380, 3381, 3382, 3423, 3764, 3791, 3798, 3818, 3821, 3822, 3823, 4613, 4624, 4631, 4657, 4658, 4698, 4888, 4896, 4897, 4943, 4951, 4952, 4954, 4956, 5199, 5204, 5218, 5219, 5220, 5281, 5282}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{453}, - new uint[]{453}, - new uint[]{524}, - Array.Empty(), - Array.Empty(), - new uint[]{527}, - Array.Empty(), - new uint[]{526}, - Array.Empty(), - new uint[]{520}, - Array.Empty(), - new uint[]{533}, - Array.Empty(), - new uint[]{526}, - new uint[]{453}, - new uint[]{453}, - Array.Empty(), - Array.Empty(), - new uint[]{453}, - new uint[]{453}, - new uint[]{453}, - new uint[]{526}, - new uint[]{529}, - new uint[]{453}, - new uint[]{453}, - new uint[]{453}, - new uint[]{526}, - Array.Empty(), - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{491}, - new uint[]{203}, - Array.Empty(), - Array.Empty(), - new uint[]{230}, - new uint[]{526}, - new uint[]{51}, - new uint[]{512}, - new uint[]{446}, - new uint[]{517}, - new uint[]{518}, - new uint[]{445}, - new uint[]{447}, - new uint[]{448}, - new uint[]{513}, - new uint[]{449}, - new uint[]{514}, - Array.Empty(), - new uint[]{515}, - new uint[]{450}, - new uint[]{451}, - new uint[]{516}, - new uint[]{452}, - new uint[]{498}, - new uint[]{507}, - new uint[]{479}, - new uint[]{471}, - new uint[]{472}, - new uint[]{473}, - new uint[]{474}, - new uint[]{475}, - new uint[]{476}, - new uint[]{477}, - new uint[]{478}, - new uint[]{1131}, - new uint[]{1130}, - Array.Empty(), - Array.Empty(), - new uint[]{501, 1029, 1117, 1788}, - new uint[]{509}, - new uint[]{72}, - new uint[]{20}, - new uint[]{19}, - new uint[]{14}, - new uint[]{52}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{464}, - new uint[]{464}, - Array.Empty(), - new uint[]{465}, - new uint[]{466}, - new uint[]{467}, - new uint[]{468}, - Array.Empty(), - new uint[]{470}, - new uint[]{598}, - new uint[]{600}, - new uint[]{598}, - new uint[]{599}, - new uint[]{608}, - new uint[]{609}, - new uint[]{610}, - new uint[]{611}, - new uint[]{614}, - new uint[]{939}, - new uint[]{30}, - new uint[]{621}, - new uint[]{622}, - new uint[]{621}, - new uint[]{622}, - new uint[]{193}, - new uint[]{194}, - new uint[]{192}, - new uint[]{947}, - new uint[]{628}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{613}, - new uint[]{455}, - Array.Empty(), - Array.Empty(), - new uint[]{655}, - new uint[]{541}, - new uint[]{437}, - new uint[]{1536}, - new uint[]{6947, 6948}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{453}, - new uint[]{453}, - new uint[]{453}, - new uint[]{453}, - new uint[]{454}, - new uint[]{454}, - new uint[]{454}, - new uint[]{511}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{553}, - new uint[]{543}, - new uint[]{169}, - new uint[]{84}, - new uint[]{1385}, - Array.Empty(), - new uint[]{627}, - new uint[]{439}, - Array.Empty(), - new uint[]{448}, - new uint[]{448}, - new uint[]{521, 902, 905, 1859, 4396}, - new uint[]{522}, - new uint[]{550}, - new uint[]{542}, - new uint[]{536}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{523}, - new uint[]{535}, - new uint[]{528}, - new uint[]{531, 1728}, - new uint[]{532}, - new uint[]{4}, - new uint[]{523}, - Array.Empty(), - Array.Empty(), - new uint[]{10}, - new uint[]{5}, - Array.Empty(), - new uint[]{140}, - new uint[]{139}, - new uint[]{924}, - new uint[]{923}, - new uint[]{923}, - new uint[]{944}, - new uint[]{690}, - new uint[]{925}, - new uint[]{943}, - new uint[]{925}, - new uint[]{929}, - new uint[]{1306}, - new uint[]{1304}, - new uint[]{928}, - new uint[]{1307}, - new uint[]{689}, - Array.Empty(), - new uint[]{698}, - Array.Empty(), - Array.Empty(), - new uint[]{1694}, - new uint[]{789, 790, 1762, 1774, 1790}, - new uint[]{789, 1448, 1746, 1774, 1775}, - new uint[]{682, 794, 1111, 1737, 1754, 1770, 1781}, - new uint[]{793, 1770, 6725}, - Array.Empty(), - Array.Empty(), - new uint[]{1625}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1301}, - new uint[]{690}, - new uint[]{929}, - new uint[]{928}, - new uint[]{1305}, - new uint[]{1306}, - new uint[]{1307}, - new uint[]{929}, - new uint[]{1305}, - new uint[]{689}, - new uint[]{934}, - new uint[]{934}, - new uint[]{709}, - new uint[]{925}, - new uint[]{925}, - new uint[]{781}, - new uint[]{1306}, - new uint[]{758}, - new uint[]{756}, - new uint[]{757}, - new uint[]{765}, - new uint[]{766}, - new uint[]{767}, - new uint[]{782}, - new uint[]{783}, - Array.Empty(), - new uint[]{841}, - new uint[]{842}, - new uint[]{843}, - new uint[]{844}, - new uint[]{845}, - new uint[]{708}, - new uint[]{1647}, - Array.Empty(), - new uint[]{262}, - new uint[]{932}, - new uint[]{1305}, - new uint[]{1305}, - new uint[]{823}, - new uint[]{751}, - new uint[]{751}, - new uint[]{751}, - new uint[]{828}, - new uint[]{823}, - new uint[]{829}, - new uint[]{830}, - new uint[]{831}, - new uint[]{823}, - new uint[]{828}, - new uint[]{751}, - new uint[]{907}, - new uint[]{756}, - new uint[]{932}, - new uint[]{1306}, - new uint[]{932}, - new uint[]{757}, - new uint[]{1307}, - new uint[]{824}, - new uint[]{825}, - new uint[]{826}, - new uint[]{262}, - new uint[]{937}, - Array.Empty(), - new uint[]{795, 1750, 1751}, - new uint[]{1611, 1612, 1747}, - new uint[]{785, 1447, 1630}, - new uint[]{786, 1631}, - new uint[]{787}, - new uint[]{650, 1989, 2230, 2990, 3182, 3894, 5214}, - new uint[]{651, 1989, 2228, 2990, 3182, 3989, 5215}, - new uint[]{652, 1989, 2229, 3182, 4296}, - new uint[]{646, 647, 1767}, - new uint[]{646, 647, 1766}, - new uint[]{648, 1771}, - new uint[]{649, 1772}, - new uint[]{653, 659, 1745, 3473}, - new uint[]{3576, 4054}, - new uint[]{655}, - Array.Empty(), - new uint[]{246, 247}, - new uint[]{254}, - new uint[]{250}, - Array.Empty(), - new uint[]{245, 1837, 2297, 2300, 2301, 2304}, - new uint[]{253, 1388, 1731, 1839, 1880, 2305, 2369}, - new uint[]{249, 1389, 1838, 2296}, - new uint[]{258, 1840, 1879}, - new uint[]{192}, - new uint[]{193}, - new uint[]{194}, - Array.Empty(), - new uint[]{436, 1842, 2986}, - new uint[]{103, 662, 1843}, - new uint[]{662, 1844, 2985}, - new uint[]{663, 1845}, - new uint[]{378}, - new uint[]{368}, - Array.Empty(), - new uint[]{372}, - new uint[]{377, 1832, 2518, 2702, 2711}, - new uint[]{562, 1833, 2519, 2694}, - new uint[]{375, 1834, 2521, 2704}, - new uint[]{371, 1835, 2520, 2703}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{64, 67, 2319, 2321}, - new uint[]{65, 68, 2317, 2320}, - new uint[]{66, 69, 2313, 2318}, - new uint[]{386, 1827, 2525, 2534, 2543}, - new uint[]{384, 1828, 2524, 2540, 2679}, - new uint[]{389, 1829, 2523, 2537, 2721}, - new uint[]{565, 1830, 2526, 2541}, - new uint[]{225, 283, 367, 1360, 3099}, - Array.Empty(), - new uint[]{218, 266, 350}, - Array.Empty(), - new uint[]{347, 1168, 1705, 2539}, - new uint[]{345, 1705}, - new uint[]{979, 1866, 2536, 2542}, - new uint[]{559, 1705, 2535}, - new uint[]{1851, 2239, 4094}, - new uint[]{914}, - new uint[]{914}, - new uint[]{914}, - Array.Empty(), - new uint[]{1329}, - new uint[]{1272}, - new uint[]{1272}, - new uint[]{1272}, - new uint[]{1273}, - new uint[]{1273}, - new uint[]{1273}, - new uint[]{1274}, - new uint[]{1274}, - new uint[]{1275}, - new uint[]{1275}, - new uint[]{1275}, - new uint[]{1276}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1255}, - new uint[]{1252}, - new uint[]{1253}, - new uint[]{1254}, - new uint[]{1243}, - new uint[]{1242}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1240}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1239}, - new uint[]{1240}, - new uint[]{1240}, - new uint[]{1238}, - new uint[]{1372}, - new uint[]{1234}, - Array.Empty(), - new uint[]{1263}, - Array.Empty(), - new uint[]{405}, - new uint[]{1261}, - new uint[]{1260}, - new uint[]{1259}, - new uint[]{6}, - new uint[]{1043}, - Array.Empty(), - Array.Empty(), - new uint[]{402}, - new uint[]{1258}, - Array.Empty(), - new uint[]{1257}, - new uint[]{289}, - new uint[]{1008}, - new uint[]{1009}, - new uint[]{1010}, - new uint[]{1011}, - new uint[]{1012}, - new uint[]{1013}, - new uint[]{1015}, - new uint[]{1014}, - new uint[]{479}, - new uint[]{1016}, - new uint[]{1024}, - new uint[]{1039, 1122}, - new uint[]{1047}, - new uint[]{1052}, - new uint[]{1053}, - new uint[]{1054}, - new uint[]{1055}, - new uint[]{1056}, - new uint[]{1057}, - new uint[]{1058}, - new uint[]{1059}, - new uint[]{1061}, - new uint[]{1062}, - new uint[]{479}, - new uint[]{1060}, - new uint[]{1092}, - new uint[]{1079}, - new uint[]{1075}, - new uint[]{117}, - new uint[]{1175}, - new uint[]{1174}, - new uint[]{1099}, - new uint[]{934}, - new uint[]{933}, - new uint[]{933}, - new uint[]{933}, - new uint[]{1304}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{531}, - new uint[]{539}, - new uint[]{621}, - new uint[]{751}, - new uint[]{751}, - new uint[]{751}, - new uint[]{887}, - new uint[]{1377}, - new uint[]{1547}, - new uint[]{942}, - new uint[]{938}, - new uint[]{936}, - new uint[]{935}, - new uint[]{713}, - new uint[]{35}, - new uint[]{949}, - new uint[]{974}, - new uint[]{973}, - new uint[]{970}, - new uint[]{970}, - new uint[]{972}, - new uint[]{971}, - new uint[]{945}, - new uint[]{980}, - new uint[]{617}, - new uint[]{541}, - new uint[]{941}, - new uint[]{940}, - new uint[]{1374}, - new uint[]{404}, - new uint[]{1373}, - new uint[]{978}, - new uint[]{977}, - new uint[]{836}, - new uint[]{982}, - new uint[]{981}, - new uint[]{981}, - new uint[]{729}, - new uint[]{975}, - new uint[]{976}, - new uint[]{945}, - new uint[]{980}, - new uint[]{617}, - new uint[]{1556}, - new uint[]{619}, - new uint[]{620}, - new uint[]{948}, - new uint[]{517}, - new uint[]{946}, - new uint[]{193}, - new uint[]{194}, - new uint[]{947}, - new uint[]{945}, - new uint[]{980}, - new uint[]{617}, - new uint[]{1550}, - new uint[]{752}, - new uint[]{945}, - new uint[]{811}, - new uint[]{812}, - new uint[]{813}, - new uint[]{814}, - new uint[]{815}, - new uint[]{816}, - new uint[]{817}, - new uint[]{818}, - new uint[]{1209}, - new uint[]{1532}, - new uint[]{716}, - new uint[]{983}, - new uint[]{985}, - new uint[]{555}, - new uint[]{554}, - new uint[]{554}, - new uint[]{554}, - new uint[]{554}, - new uint[]{780}, - new uint[]{1378}, - new uint[]{1381}, - new uint[]{1380}, - new uint[]{1277}, - new uint[]{827}, - Array.Empty(), - new uint[]{1306}, - new uint[]{991}, - new uint[]{989}, - new uint[]{990}, - new uint[]{993}, - new uint[]{994}, - new uint[]{1007}, - new uint[]{1005}, - new uint[]{992}, - new uint[]{256}, - new uint[]{251}, - new uint[]{248}, - new uint[]{1187}, - new uint[]{1195}, - new uint[]{1194}, - new uint[]{117}, - new uint[]{1196}, - new uint[]{1187}, - new uint[]{1195}, - new uint[]{116}, - new uint[]{1197}, - new uint[]{1193}, - new uint[]{346}, - new uint[]{344}, - new uint[]{342}, - new uint[]{348}, - new uint[]{346}, - Array.Empty(), - new uint[]{342}, - new uint[]{348}, - new uint[]{1279}, - new uint[]{1284}, - new uint[]{1285}, - new uint[]{824}, - new uint[]{826}, - new uint[]{906}, - new uint[]{833}, - new uint[]{834}, - new uint[]{824}, - new uint[]{835}, - new uint[]{824}, - new uint[]{838}, - new uint[]{839}, - new uint[]{1314}, - new uint[]{837}, - new uint[]{1221}, - new uint[]{1343}, - new uint[]{342}, - new uint[]{968}, - new uint[]{1398}, - new uint[]{1399}, - new uint[]{8141}, - Array.Empty(), - new uint[]{1401}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{269}, - new uint[]{1001}, - new uint[]{1176}, - new uint[]{999}, - new uint[]{996}, - new uint[]{998}, - new uint[]{997}, - new uint[]{995}, - new uint[]{994}, - new uint[]{1006}, - new uint[]{1005}, - new uint[]{1267}, - new uint[]{1268}, - Array.Empty(), - new uint[]{1003}, - new uint[]{1002}, - new uint[]{1004}, - new uint[]{346}, - new uint[]{344}, - new uint[]{342}, - new uint[]{348}, - new uint[]{38}, - new uint[]{1210}, - new uint[]{1211}, - new uint[]{1212}, - new uint[]{1287}, - new uint[]{1288}, - new uint[]{1289}, - new uint[]{1290}, - new uint[]{1292}, - new uint[]{1291}, - new uint[]{1293}, - new uint[]{1294}, - new uint[]{1295}, - new uint[]{1296}, - new uint[]{1297}, - new uint[]{1548}, - new uint[]{1549}, - new uint[]{1551}, - new uint[]{1552}, - new uint[]{1553}, - new uint[]{1554}, - new uint[]{1555}, - Array.Empty(), - new uint[]{1566}, - new uint[]{1557}, - new uint[]{1558}, - new uint[]{1559}, - new uint[]{1383}, - new uint[]{1205}, - new uint[]{1205}, - Array.Empty(), - Array.Empty(), - new uint[]{346}, - new uint[]{1560}, - new uint[]{1561}, - new uint[]{1562}, - new uint[]{1563}, - Array.Empty(), - new uint[]{1565}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{891}, - new uint[]{888}, - new uint[]{889}, - new uint[]{890}, - new uint[]{1154}, - new uint[]{1169}, - new uint[]{1165}, - new uint[]{1160}, - new uint[]{1143}, - new uint[]{1133}, - new uint[]{1159}, - new uint[]{1073}, - new uint[]{1137}, - new uint[]{1127}, - new uint[]{1126}, - new uint[]{1109}, - new uint[]{1108}, - new uint[]{1110}, - new uint[]{1105}, - new uint[]{1166}, - new uint[]{1033, 1088}, - new uint[]{1167}, - new uint[]{1151}, - new uint[]{1150}, - new uint[]{542}, - new uint[]{1144}, - new uint[]{1136}, - new uint[]{497}, - new uint[]{1129}, - new uint[]{1128}, - new uint[]{1125}, - new uint[]{1124}, - new uint[]{1107}, - new uint[]{1106}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{200}, - new uint[]{1037}, - new uint[]{1264}, - new uint[]{1262}, - new uint[]{1262}, - new uint[]{1265}, - new uint[]{1266}, - new uint[]{1269}, - new uint[]{1270}, - new uint[]{116}, - Array.Empty(), - Array.Empty(), - new uint[]{1367, 1584}, - Array.Empty(), - Array.Empty(), - new uint[]{1584}, - new uint[]{1584}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1584}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{621}, - new uint[]{1861}, - new uint[]{981, 1337}, - new uint[]{981}, - new uint[]{1273, 1331, 1332, 1868}, - new uint[]{892, 1332, 1868}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - Array.Empty(), - new uint[]{1275, 1343, 1371, 1711, 1733}, - new uint[]{1275, 1343, 1371}, - new uint[]{1275, 1371}, - Array.Empty(), - new uint[]{1584}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1584, 1856}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1341, 1584}, - new uint[]{1711}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{526}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1729}, - Array.Empty(), - new uint[]{904, 981}, - Array.Empty(), - new uint[]{981, 1338}, - Array.Empty(), - new uint[]{1359}, - new uint[]{903, 1273}, - Array.Empty(), - new uint[]{1273}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - new uint[]{1277}, - Array.Empty(), - Array.Empty(), - new uint[]{1275, 1343}, - new uint[]{1275, 1371}, - new uint[]{1275, 1371}, - new uint[]{1275, 1371}, - Array.Empty(), - Array.Empty(), - new uint[]{1320, 1328, 1334, 1355, 1358, 1659}, - new uint[]{519}, - new uint[]{1352, 1356}, - new uint[]{1324}, - new uint[]{1336, 1339, 1340, 1361, 1867}, - new uint[]{1321, 1335, 1363, 1370}, - new uint[]{1322, 1323}, - new uint[]{1322, 1351, 1711}, - Array.Empty(), - new uint[]{2175}, - new uint[]{849}, - new uint[]{850}, - new uint[]{851}, - new uint[]{852}, - new uint[]{853}, - new uint[]{854}, - new uint[]{855}, - new uint[]{856}, - new uint[]{857}, - new uint[]{858}, - new uint[]{859}, - new uint[]{860}, - new uint[]{861}, - new uint[]{862}, - Array.Empty(), - Array.Empty(), - new uint[]{1177}, - new uint[]{1213}, - new uint[]{1304}, - new uint[]{929}, - new uint[]{974}, - new uint[]{1305}, - new uint[]{1177}, - new uint[]{923}, - new uint[]{1179}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{2971}, - new uint[]{895}, - Array.Empty(), - new uint[]{1319}, - Array.Empty(), - new uint[]{896}, - new uint[]{897}, - Array.Empty(), - new uint[]{898}, - new uint[]{899}, - new uint[]{865}, - new uint[]{866}, - new uint[]{867}, - new uint[]{868}, - new uint[]{869}, - new uint[]{870}, - new uint[]{871}, - new uint[]{872}, - new uint[]{873}, - new uint[]{874}, - new uint[]{875}, - new uint[]{876}, - new uint[]{877}, - new uint[]{878}, - new uint[]{879}, - new uint[]{880}, - new uint[]{881}, - new uint[]{882}, - new uint[]{883}, - new uint[]{884}, - new uint[]{885}, - new uint[]{886}, - new uint[]{1333, 1369, 1660, 1720, 1723, 1867}, - new uint[]{520, 1711}, - Array.Empty(), - Array.Empty(), - new uint[]{900, 1730}, - new uint[]{1347}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1325}, - new uint[]{1348}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1172}, - new uint[]{1171}, - Array.Empty(), - new uint[]{1237}, - new uint[]{1241}, - new uint[]{1256}, - new uint[]{1244}, - new uint[]{1251}, - new uint[]{1245}, - new uint[]{1246}, - new uint[]{1250}, - new uint[]{1249}, - new uint[]{1248}, - new uint[]{1247}, - new uint[]{346}, - new uint[]{344}, - new uint[]{346}, - new uint[]{344}, - new uint[]{344}, - new uint[]{344}, - new uint[]{969}, - new uint[]{1232}, - new uint[]{1231}, - new uint[]{1225}, - new uint[]{1230}, - new uint[]{1229}, - new uint[]{1228}, - new uint[]{1227}, - new uint[]{1226}, - new uint[]{1225}, - new uint[]{1589}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{1345}, - new uint[]{1278}, - new uint[]{1245}, - new uint[]{1330}, - new uint[]{109}, - new uint[]{143}, - new uint[]{144}, - new uint[]{145}, - new uint[]{146}, - new uint[]{111}, - new uint[]{284}, - new uint[]{147}, - new uint[]{151}, - new uint[]{153}, - new uint[]{441}, - new uint[]{158}, - new uint[]{438}, - new uint[]{117}, - new uint[]{154}, - new uint[]{155}, - new uint[]{157}, - new uint[]{430}, - new uint[]{431}, - new uint[]{432}, - new uint[]{433}, - new uint[]{434}, - new uint[]{435}, - new uint[]{625}, - new uint[]{1202}, - new uint[]{1203}, - new uint[]{1200}, - new uint[]{1201}, - new uint[]{918}, - new uint[]{919}, - new uint[]{920}, - new uint[]{921}, - new uint[]{922}, - new uint[]{1326}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1271}, - new uint[]{1331}, - new uint[]{908, 1178}, - new uint[]{833}, - Array.Empty(), - new uint[]{497}, - new uint[]{967}, - new uint[]{629}, - new uint[]{631}, - new uint[]{117}, - new uint[]{97}, - new uint[]{984}, - new uint[]{1303}, - new uint[]{987}, - new uint[]{986}, - new uint[]{1302}, - new uint[]{1357}, - new uint[]{1362}, - new uint[]{1223}, - new uint[]{1224}, - new uint[]{894}, - Array.Empty(), - new uint[]{1188}, - new uint[]{1189}, - new uint[]{1190}, - new uint[]{1191}, - new uint[]{1192}, - new uint[]{1193}, - new uint[]{117}, - new uint[]{1276}, - new uint[]{1346}, - new uint[]{1312}, - new uint[]{1608}, - new uint[]{1342}, - new uint[]{621}, - new uint[]{622}, - new uint[]{1354}, - new uint[]{1704}, - new uint[]{193}, - new uint[]{194}, - new uint[]{602}, - new uint[]{602}, - new uint[]{601}, - new uint[]{139}, - new uint[]{140}, - new uint[]{212}, - new uint[]{2163}, - new uint[]{130}, - new uint[]{108}, - new uint[]{2135}, - new uint[]{2118}, - new uint[]{2160}, - Array.Empty(), - new uint[]{2136}, - new uint[]{1471}, - new uint[]{1803}, - new uint[]{1804}, - new uint[]{244}, - new uint[]{128}, - new uint[]{354}, - new uint[]{1907}, - new uint[]{139}, - new uint[]{599}, - new uint[]{597}, - new uint[]{35}, - new uint[]{192}, - new uint[]{517}, - new uint[]{1680}, - new uint[]{1680}, - new uint[]{1678}, - new uint[]{1678}, - new uint[]{108}, - new uint[]{1676}, - new uint[]{1677}, - new uint[]{1672}, - new uint[]{1673}, - new uint[]{1674}, - new uint[]{114}, - new uint[]{1675}, - new uint[]{1535}, - new uint[]{1536}, - Array.Empty(), - new uint[]{1533}, - new uint[]{1534}, - new uint[]{1538}, - new uint[]{1535}, - new uint[]{1539}, - new uint[]{1540}, - new uint[]{1541}, - new uint[]{1542}, - new uint[]{1543}, - new uint[]{1544}, - new uint[]{1545}, - new uint[]{1546}, - new uint[]{108}, - Array.Empty(), - new uint[]{1696}, - new uint[]{1697}, - new uint[]{1698}, - Array.Empty(), - new uint[]{1689}, - new uint[]{1690}, - new uint[]{1691}, - new uint[]{1692}, - new uint[]{1693}, - new uint[]{1681}, - new uint[]{1681}, - new uint[]{1681}, - new uint[]{1681}, - new uint[]{1682}, - new uint[]{1683}, - new uint[]{1684}, - new uint[]{1685}, - new uint[]{1686}, - new uint[]{1687}, - new uint[]{29}, - new uint[]{1803}, - new uint[]{1804}, - new uint[]{1804}, - new uint[]{1802}, - new uint[]{1802}, - new uint[]{1802}, - new uint[]{1566}, - new uint[]{1566}, - new uint[]{1566}, - new uint[]{1402}, - new uint[]{1640}, - new uint[]{1970}, - new uint[]{2104}, - new uint[]{1883}, - new uint[]{1884}, - new uint[]{1885}, - new uint[]{1531, 2147}, - new uint[]{1886}, - new uint[]{1650}, - new uint[]{1613}, - new uint[]{1887}, - new uint[]{1888}, - new uint[]{2147}, - new uint[]{1889}, - new uint[]{1650}, - new uint[]{1650}, - new uint[]{2146}, - new uint[]{1890}, - new uint[]{1890}, - new uint[]{108}, - Array.Empty(), - new uint[]{1648}, - new uint[]{2091}, - new uint[]{1648}, - Array.Empty(), - new uint[]{108}, - new uint[]{1397}, - new uint[]{1397}, - new uint[]{1497}, - new uint[]{1498}, - new uint[]{1415}, - new uint[]{1499}, - new uint[]{1396}, - new uint[]{2154}, - new uint[]{2154}, - new uint[]{1657}, - new uint[]{2152}, - new uint[]{1654}, - new uint[]{1655}, - new uint[]{1656}, - new uint[]{1652}, - new uint[]{1449}, - new uint[]{1653}, - new uint[]{1808}, - new uint[]{1421}, - new uint[]{1892}, - new uint[]{1893}, - new uint[]{1894}, - new uint[]{1895}, - Array.Empty(), - new uint[]{1897}, - new uint[]{1805}, - new uint[]{1646}, - new uint[]{1645}, - new uint[]{1646}, - new uint[]{1645}, - new uint[]{1644}, - new uint[]{1644}, - new uint[]{1644}, - new uint[]{1644}, - new uint[]{1644}, - new uint[]{2100}, - new uint[]{2098}, - new uint[]{2099}, - new uint[]{1453}, - new uint[]{2101}, - new uint[]{2102}, - new uint[]{2103}, - new uint[]{583}, - new uint[]{68}, - new uint[]{69}, - new uint[]{2063}, - new uint[]{619}, - new uint[]{620}, - new uint[]{1391}, - new uint[]{1392}, - new uint[]{1393}, - new uint[]{1394}, - new uint[]{1639}, - new uint[]{750}, - new uint[]{1395}, - new uint[]{436}, - new uint[]{103}, - new uint[]{104}, - new uint[]{1901}, - new uint[]{653}, - new uint[]{1416}, - new uint[]{2204}, - new uint[]{1903}, - new uint[]{1904}, - new uint[]{1905}, - new uint[]{1906}, - new uint[]{1585}, - new uint[]{1586}, - new uint[]{1587}, - new uint[]{1588}, - new uint[]{1589}, - new uint[]{1589}, - new uint[]{1590}, - new uint[]{1848}, - Array.Empty(), - new uint[]{1592}, - new uint[]{1591}, - new uint[]{1593}, - new uint[]{1799}, - new uint[]{1596}, - new uint[]{1597}, - new uint[]{1595}, - new uint[]{1594}, - new uint[]{1599}, - new uint[]{1598}, - new uint[]{1600}, - new uint[]{1601}, - Array.Empty(), - new uint[]{1806}, - new uint[]{1805}, - new uint[]{1805}, - new uint[]{1858}, - new uint[]{1417}, - new uint[]{2201}, - new uint[]{2201}, - new uint[]{598}, - new uint[]{1907}, - new uint[]{550}, - new uint[]{614}, - new uint[]{2064}, - new uint[]{122}, - new uint[]{139}, - new uint[]{546}, - new uint[]{2065}, - new uint[]{2066}, - new uint[]{57}, - new uint[]{80}, - new uint[]{2200}, - new uint[]{2199}, - new uint[]{2198}, - new uint[]{619}, - new uint[]{729}, - new uint[]{2087}, - new uint[]{2088}, - new uint[]{1810}, - new uint[]{2089}, - new uint[]{1418}, - new uint[]{269}, - new uint[]{67}, - new uint[]{115}, - new uint[]{1910}, - new uint[]{1419}, - new uint[]{1420}, - new uint[]{1421}, - new uint[]{1911}, - new uint[]{1912}, - new uint[]{1582, 1605, 1847}, - Array.Empty(), - new uint[]{1581}, - new uint[]{1603}, - new uint[]{1582}, - Array.Empty(), - new uint[]{1581}, - new uint[]{1603}, - new uint[]{11}, - new uint[]{1422}, - new uint[]{1423}, - new uint[]{40}, - new uint[]{130}, - new uint[]{56}, - new uint[]{201}, - new uint[]{56}, - new uint[]{1424}, - new uint[]{1919}, - new uint[]{1920}, - new uint[]{1921}, - new uint[]{113}, - new uint[]{1923}, - new uint[]{1419}, - new uint[]{1425}, - new uint[]{1426}, - new uint[]{115}, - new uint[]{117}, - new uint[]{56}, - new uint[]{1924}, - new uint[]{1925}, - new uint[]{1926}, - new uint[]{2161}, - new uint[]{656}, - new uint[]{2162}, - new uint[]{1848}, - new uint[]{1927}, - new uint[]{1450}, - new uint[]{213}, - new uint[]{1451}, - new uint[]{1929}, - new uint[]{1422}, - new uint[]{1423}, - new uint[]{1452}, - new uint[]{1930}, - new uint[]{1424}, - new uint[]{1453}, - new uint[]{1931}, - new uint[]{2096}, - new uint[]{1932}, - new uint[]{1933}, - new uint[]{2153}, - new uint[]{1907}, - new uint[]{1935}, - new uint[]{1936}, - new uint[]{1937}, - new uint[]{361}, - new uint[]{1939}, - new uint[]{824}, - new uint[]{1391}, - new uint[]{1863}, - new uint[]{2186}, - new uint[]{2186}, - new uint[]{1941}, - new uint[]{2068}, - new uint[]{2202}, - new uint[]{2076}, - new uint[]{2077}, - new uint[]{2078}, - new uint[]{2079}, - new uint[]{1454}, - new uint[]{2080}, - new uint[]{656}, - new uint[]{2082}, - new uint[]{2081}, - new uint[]{1688}, - new uint[]{1942}, - new uint[]{15}, - new uint[]{56}, - new uint[]{116}, - new uint[]{614}, - new uint[]{2083}, - new uint[]{2084}, - new uint[]{1293}, - new uint[]{656}, - new uint[]{2085}, - new uint[]{2086}, - new uint[]{2086}, - new uint[]{1567}, - new uint[]{1568}, - new uint[]{1569}, - new uint[]{1570}, - new uint[]{1798}, - new uint[]{1572}, - new uint[]{1573}, - Array.Empty(), - new uint[]{1571}, - new uint[]{1574}, - new uint[]{1575}, - new uint[]{1576}, - new uint[]{1577}, - new uint[]{1578}, - new uint[]{1579}, - new uint[]{1580}, - new uint[]{2073}, - new uint[]{2074}, - new uint[]{2075}, - new uint[]{1385}, - new uint[]{1581}, - new uint[]{1582}, - new uint[]{1583}, - new uint[]{1584}, - new uint[]{1584}, - new uint[]{1584}, - new uint[]{1607}, - new uint[]{1427}, - new uint[]{1428}, - new uint[]{1429}, - new uint[]{1430}, - new uint[]{1431}, - new uint[]{1432}, - new uint[]{1433}, - new uint[]{1434}, - new uint[]{1435}, - new uint[]{1436}, - new uint[]{1437}, - new uint[]{1438}, - new uint[]{1439}, - new uint[]{1440}, - new uint[]{1445}, - new uint[]{1441}, - new uint[]{1442}, - new uint[]{1443}, - new uint[]{1444}, - new uint[]{1610}, - new uint[]{1446}, - new uint[]{1614}, - new uint[]{1615}, - new uint[]{1616}, - new uint[]{1617}, - new uint[]{1618}, - new uint[]{1619}, - new uint[]{1620}, - new uint[]{1621}, - new uint[]{1622}, - new uint[]{1623}, - new uint[]{1624}, - new uint[]{1625}, - new uint[]{646}, - new uint[]{647}, - new uint[]{648}, - new uint[]{1629}, - new uint[]{1630}, - new uint[]{1631}, - new uint[]{1632}, - new uint[]{1633}, - new uint[]{1634}, - new uint[]{1635}, - new uint[]{1636}, - new uint[]{1637}, - new uint[]{1638}, - new uint[]{1796}, - new uint[]{1797}, - new uint[]{1734}, - new uint[]{1734}, - new uint[]{1734}, - new uint[]{1747}, - new uint[]{1750}, - new uint[]{1752}, - new uint[]{1763}, - new uint[]{1764}, - new uint[]{1765}, - new uint[]{1767}, - new uint[]{1766}, - new uint[]{1773}, - new uint[]{1776}, - new uint[]{1778}, - new uint[]{114}, - new uint[]{1777}, - new uint[]{1783}, - new uint[]{491}, - new uint[]{1789}, - new uint[]{1638}, - new uint[]{1631}, - new uint[]{1787}, - new uint[]{117}, - new uint[]{1762}, - new uint[]{1770}, - new uint[]{1779}, - new uint[]{1786}, - new uint[]{1793}, - new uint[]{1792}, - new uint[]{1794}, - new uint[]{1232}, - new uint[]{1795}, - new uint[]{1170}, - new uint[]{115}, - new uint[]{1148}, - new uint[]{113}, - new uint[]{1609}, - new uint[]{2105}, - new uint[]{2106}, - new uint[]{2107, 2120}, - new uint[]{2108}, - new uint[]{2109}, - new uint[]{2110}, - new uint[]{2111}, - new uint[]{2159}, - new uint[]{1810}, - new uint[]{1811}, - new uint[]{1812}, - new uint[]{1486}, - new uint[]{2092}, - new uint[]{2113}, - new uint[]{269}, - Array.Empty(), - new uint[]{2114}, - new uint[]{557}, - new uint[]{2116}, - new uint[]{2117}, - new uint[]{2206}, - new uint[]{1975}, - new uint[]{1976}, - new uint[]{34}, - new uint[]{1977}, - new uint[]{1978}, - new uint[]{1979}, - new uint[]{1980}, - Array.Empty(), - new uint[]{1982}, - new uint[]{58}, - new uint[]{59}, - new uint[]{60}, - new uint[]{61}, - new uint[]{1985}, - new uint[]{1986}, - Array.Empty(), - new uint[]{1988}, - Array.Empty(), - new uint[]{1991}, - new uint[]{331}, - new uint[]{333}, - new uint[]{332}, - new uint[]{1996}, - new uint[]{2001}, - new uint[]{2002}, - new uint[]{2003}, - new uint[]{2004}, - new uint[]{2005}, - new uint[]{2006}, - new uint[]{2007}, - new uint[]{236}, - new uint[]{2010}, - new uint[]{2013}, - new uint[]{2014}, - new uint[]{2015}, - new uint[]{2016}, - new uint[]{2017}, - new uint[]{2022}, - new uint[]{2023}, - new uint[]{2024}, - new uint[]{2025}, - new uint[]{2026}, - new uint[]{2029}, - new uint[]{2030}, - new uint[]{2031}, - new uint[]{2034}, - new uint[]{2035}, - new uint[]{2036}, - new uint[]{2037}, - new uint[]{2041}, - new uint[]{2042}, - new uint[]{2043}, - new uint[]{2043, 2044}, - new uint[]{2045}, - new uint[]{2046}, - new uint[]{2047}, - new uint[]{2048}, - new uint[]{2051}, - new uint[]{2051}, - new uint[]{2051}, - new uint[]{2052}, - new uint[]{2053}, - new uint[]{2054}, - new uint[]{2055}, - new uint[]{2115}, - new uint[]{2123}, - new uint[]{2106, 2124}, - new uint[]{2125}, - new uint[]{2126}, - new uint[]{2109, 2127}, - new uint[]{2128}, - new uint[]{2129}, - new uint[]{2089}, - new uint[]{2113}, - new uint[]{2205}, - new uint[]{1727}, - new uint[]{1946}, - new uint[]{2149}, - new uint[]{104}, - new uint[]{105}, - new uint[]{2089}, - new uint[]{2130}, - Array.Empty(), - new uint[]{2131}, - new uint[]{2132}, - new uint[]{2133}, - new uint[]{2134}, - new uint[]{2121}, - new uint[]{2137}, - new uint[]{2137}, - new uint[]{2143}, - new uint[]{2067}, - new uint[]{253}, - Array.Empty(), - new uint[]{245}, - new uint[]{1126}, - new uint[]{2069}, - new uint[]{2070}, - new uint[]{2071}, - new uint[]{2072}, - new uint[]{2072}, - new uint[]{1486}, - new uint[]{2092}, - new uint[]{2088}, - new uint[]{1811}, - new uint[]{1809}, - new uint[]{2090}, - Array.Empty(), - Array.Empty(), - new uint[]{1486}, - new uint[]{2208, 2209}, - new uint[]{1464}, - new uint[]{1461}, - Array.Empty(), - new uint[]{2174}, - new uint[]{1479}, - new uint[]{1481}, - new uint[]{1463}, - new uint[]{1480}, - new uint[]{1462}, - new uint[]{1465}, - new uint[]{1466}, - new uint[]{1467}, - new uint[]{1459}, - new uint[]{1468}, - new uint[]{1469}, - new uint[]{1470}, - new uint[]{1471}, - new uint[]{1472}, - new uint[]{1473}, - new uint[]{1474}, - new uint[]{1475}, - new uint[]{1477}, - new uint[]{1476}, - new uint[]{1478}, - new uint[]{1482}, - new uint[]{2171}, - new uint[]{2176}, - new uint[]{1483}, - new uint[]{1484}, - new uint[]{1485}, - new uint[]{2118}, - new uint[]{2119}, - new uint[]{2120}, - new uint[]{2158}, - new uint[]{297}, - new uint[]{2136}, - new uint[]{108}, - new uint[]{2170}, - new uint[]{2210}, - new uint[]{2138}, - new uint[]{2142}, - new uint[]{2139}, - new uint[]{2140}, - new uint[]{2141}, - new uint[]{108}, - new uint[]{1490}, - new uint[]{108}, - new uint[]{620}, - new uint[]{1492}, - new uint[]{1949}, - new uint[]{1950}, - new uint[]{1493}, - new uint[]{1400}, - new uint[]{1951}, - new uint[]{1952}, - new uint[]{1953}, - new uint[]{1494}, - new uint[]{1495}, - new uint[]{1493}, - new uint[]{1400}, - new uint[]{1954}, - new uint[]{1955}, - new uint[]{1956}, - new uint[]{1957}, - new uint[]{1496}, - new uint[]{1400}, - new uint[]{1494}, - new uint[]{1495}, - new uint[]{1958}, - new uint[]{1958}, - new uint[]{1960}, - new uint[]{1961}, - new uint[]{1962}, - new uint[]{1963}, - new uint[]{1951}, - new uint[]{1952}, - new uint[]{1954}, - new uint[]{1967}, - new uint[]{1493}, - new uint[]{1400}, - new uint[]{1501}, - new uint[]{1968}, - new uint[]{1969}, - new uint[]{1500}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1994}, - new uint[]{417}, - new uint[]{678}, - new uint[]{270}, - new uint[]{2009}, - new uint[]{637}, - new uint[]{346}, - new uint[]{342}, - Array.Empty(), - new uint[]{347}, - Array.Empty(), - Array.Empty(), - new uint[]{345}, - new uint[]{2181}, - new uint[]{1862}, - new uint[]{1714}, - new uint[]{1716}, - new uint[]{1718}, - new uint[]{1721}, - new uint[]{1722}, - new uint[]{1604}, - new uint[]{1724}, - Array.Empty(), - new uint[]{1707}, - new uint[]{248}, - new uint[]{260}, - new uint[]{1710}, - new uint[]{343}, - new uint[]{1661}, - new uint[]{1662}, - new uint[]{1663}, - new uint[]{1664}, - new uint[]{1665}, - new uint[]{1666}, - new uint[]{1667}, - new uint[]{1668}, - new uint[]{1669}, - new uint[]{1670}, - Array.Empty(), - new uint[]{1503}, - new uint[]{1504}, - new uint[]{1505}, - new uint[]{257}, - new uint[]{1506}, - new uint[]{1699}, - new uint[]{1507}, - new uint[]{1508}, - new uint[]{1509}, - Array.Empty(), - new uint[]{1511}, - new uint[]{1512}, - new uint[]{1513}, - new uint[]{1514}, - new uint[]{1515}, - new uint[]{1516}, - new uint[]{1517}, - new uint[]{1700}, - new uint[]{1701}, - new uint[]{1518}, - new uint[]{1519}, - new uint[]{1520}, - new uint[]{1521}, - new uint[]{1522}, - new uint[]{1523}, - new uint[]{1524}, - new uint[]{1702}, - new uint[]{1525}, - new uint[]{1526}, - new uint[]{1527}, - Array.Empty(), - new uint[]{1529}, - new uint[]{1530}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{1717, 2172}, - new uint[]{1713}, - new uint[]{1715}, - new uint[]{1712}, - new uint[]{1726}, - new uint[]{2093}, - new uint[]{1811}, - new uint[]{1813}, - new uint[]{1418}, - new uint[]{1502}, - new uint[]{2095}, - new uint[]{2090}, - new uint[]{1809}, - new uint[]{2089}, - new uint[]{297}, - new uint[]{2094}, - new uint[]{2093}, - Array.Empty(), - Array.Empty(), - new uint[]{1459}, - new uint[]{1460}, - new uint[]{1469}, - new uint[]{1472}, - new uint[]{1992}, - new uint[]{1993}, - new uint[]{1995}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2057}, - Array.Empty(), - new uint[]{2059}, - new uint[]{2060}, - new uint[]{2061}, - new uint[]{2062}, - Array.Empty(), - new uint[]{2197}, - new uint[]{108}, - new uint[]{2196}, - new uint[]{1403}, - new uint[]{1882}, - new uint[]{1971}, - new uint[]{2211}, - new uint[]{1881}, - new uint[]{1640}, - new uint[]{1404}, - new uint[]{2203}, - new uint[]{1640}, - new uint[]{1972}, - new uint[]{1973}, - new uint[]{1681, 1870}, - new uint[]{1681, 1870}, - new uint[]{1681, 1870}, - new uint[]{1681, 1690, 1870}, - new uint[]{1651}, - new uint[]{2148}, - new uint[]{1640}, - new uint[]{1732}, - new uint[]{1848}, - new uint[]{399}, - new uint[]{67}, - new uint[]{68}, - new uint[]{69}, - Array.Empty(), - new uint[]{2144}, - new uint[]{2145}, - new uint[]{2168}, - new uint[]{1870}, - Array.Empty(), - Array.Empty(), - new uint[]{2183}, - new uint[]{1855}, - new uint[]{1974}, - new uint[]{1860}, - new uint[]{331}, - new uint[]{417}, - new uint[]{678}, - new uint[]{43}, - new uint[]{680}, - new uint[]{681}, - new uint[]{630}, - new uint[]{2097}, - new uint[]{2164}, - new uint[]{2185}, - new uint[]{2092}, - new uint[]{297}, - new uint[]{2121}, - new uint[]{2092}, - new uint[]{297}, - new uint[]{2121}, - new uint[]{2092}, - new uint[]{2089}, - new uint[]{2121}, - new uint[]{1984}, - new uint[]{1984}, - new uint[]{1998}, - new uint[]{1999}, - new uint[]{2180}, - Array.Empty(), - Array.Empty(), - new uint[]{2187}, - new uint[]{2188}, - new uint[]{2189}, - new uint[]{2190}, - new uint[]{2191}, - new uint[]{2192}, - new uint[]{1468}, - new uint[]{1470}, - new uint[]{1472}, - new uint[]{1473}, - new uint[]{1459}, - new uint[]{1459}, - new uint[]{2105}, - new uint[]{2109}, - new uint[]{2110}, - new uint[]{2106}, - new uint[]{2111}, - new uint[]{1382}, - new uint[]{1474}, - new uint[]{1482}, - new uint[]{1472}, - new uint[]{2212}, - new uint[]{108, 1482, 3234, 3240}, - new uint[]{2265}, - new uint[]{2266}, - new uint[]{2513}, - new uint[]{2325}, - new uint[]{108, 750, 9380}, - new uint[]{2261}, - new uint[]{2262}, - new uint[]{2263}, - new uint[]{2267}, - new uint[]{2267}, - new uint[]{2261}, - new uint[]{108}, - new uint[]{2259}, - new uint[]{2260}, - new uint[]{2256}, - new uint[]{2252}, - new uint[]{2251}, - new uint[]{2245}, - new uint[]{2249}, - new uint[]{2257}, - new uint[]{3984, 6290}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{1185}, - new uint[]{2246}, - new uint[]{2247}, - new uint[]{2248}, - new uint[]{2250}, - new uint[]{2255}, - new uint[]{2253}, - new uint[]{2137}, - new uint[]{2139}, - new uint[]{2140}, - new uint[]{2141}, - new uint[]{2138}, - new uint[]{2324}, - new uint[]{2142}, - Array.Empty(), - new uint[]{2254}, - new uint[]{2264}, - new uint[]{2258}, - new uint[]{2256}, - new uint[]{1186}, - new uint[]{2268}, - new uint[]{2269}, - new uint[]{2270}, - new uint[]{2271}, - new uint[]{2272}, - new uint[]{2273}, - new uint[]{2274}, - new uint[]{2275}, - new uint[]{2276}, - new uint[]{2277}, - new uint[]{2278}, - new uint[]{2279}, - new uint[]{2280}, - new uint[]{2281}, - Array.Empty(), - new uint[]{706}, - new uint[]{706}, - new uint[]{707}, - new uint[]{710}, - new uint[]{710}, - new uint[]{711}, - new uint[]{712}, - new uint[]{727}, - new uint[]{727}, - new uint[]{728}, - new uint[]{730}, - new uint[]{731}, - new uint[]{2510}, - new uint[]{732}, - new uint[]{732}, - new uint[]{733}, - new uint[]{108}, - new uint[]{2370}, - new uint[]{2371}, - new uint[]{2371}, - new uint[]{2333}, - new uint[]{2334}, - new uint[]{2335}, - new uint[]{428}, - Array.Empty(), - new uint[]{2337}, - new uint[]{2338}, - new uint[]{2339}, - new uint[]{2282}, - new uint[]{2283}, - new uint[]{2283}, - new uint[]{2283}, - new uint[]{983}, - new uint[]{2284}, - new uint[]{2285}, - new uint[]{2286}, - new uint[]{1303}, - new uint[]{2287}, - new uint[]{2288}, - new uint[]{2289}, - new uint[]{2290}, - new uint[]{2290}, - new uint[]{2281}, - new uint[]{2332}, - new uint[]{2332}, - new uint[]{736}, - new uint[]{737}, - new uint[]{738}, - new uint[]{739}, - new uint[]{740}, - new uint[]{741}, - new uint[]{820}, - new uint[]{821}, - new uint[]{822}, - new uint[]{1864}, - new uint[]{1871}, - new uint[]{1872}, - new uint[]{820}, - new uint[]{822}, - new uint[]{1864}, - new uint[]{1873}, - new uint[]{1874}, - new uint[]{1875}, - new uint[]{1876}, - Array.Empty(), - new uint[]{426}, - new uint[]{2340}, - new uint[]{427}, - new uint[]{2341}, - new uint[]{2342}, - new uint[]{2343}, - new uint[]{2344}, - new uint[]{2346}, - new uint[]{2917}, - new uint[]{633}, - new uint[]{428}, - new uint[]{2347}, - new uint[]{108, 2348}, - new uint[]{2349}, - new uint[]{2291}, - new uint[]{2292}, - new uint[]{2292}, - new uint[]{2292}, - new uint[]{2293}, - new uint[]{2293}, - new uint[]{2293}, - new uint[]{2286}, - new uint[]{913}, - new uint[]{2000}, - new uint[]{730}, - new uint[]{2375}, - new uint[]{2376}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{727}, - new uint[]{730}, - new uint[]{8143}, - new uint[]{1205}, - new uint[]{909}, - new uint[]{910}, - new uint[]{911}, - new uint[]{2360}, - new uint[]{1390, 1877}, - new uint[]{1878}, - new uint[]{258, 2298}, - new uint[]{2300, 2309}, - new uint[]{2302}, - new uint[]{2303}, - new uint[]{2306}, - new uint[]{2307}, - Array.Empty(), - new uint[]{2309}, - new uint[]{2306}, - new uint[]{2310}, - new uint[]{2315}, - new uint[]{2316}, - new uint[]{2312}, - Array.Empty(), - Array.Empty(), - new uint[]{2508}, - new uint[]{2509}, - new uint[]{2364}, - new uint[]{2365}, - new uint[]{2366}, - new uint[]{2326}, - new uint[]{2299}, - Array.Empty(), - new uint[]{692}, - new uint[]{693}, - new uint[]{694}, - new uint[]{695}, - new uint[]{697}, - new uint[]{2359}, - new uint[]{700}, - new uint[]{701}, - new uint[]{702}, - new uint[]{703}, - new uint[]{704}, - new uint[]{705}, - new uint[]{2224}, - new uint[]{2226}, - new uint[]{2227}, - new uint[]{2233}, - new uint[]{2241}, - new uint[]{2242}, - new uint[]{2243}, - new uint[]{2244}, - new uint[]{1756}, - new uint[]{2237}, - new uint[]{2240}, - new uint[]{2231}, - new uint[]{2368}, - new uint[]{2350}, - new uint[]{64}, - new uint[]{2367}, - new uint[]{2353}, - new uint[]{2354}, - new uint[]{2355}, - new uint[]{2356}, - new uint[]{2357}, - new uint[]{2377}, - new uint[]{2378}, - new uint[]{2379}, - new uint[]{2380}, - new uint[]{2381}, - new uint[]{2382}, - new uint[]{2383}, - new uint[]{2384}, - new uint[]{2385}, - new uint[]{2386}, - new uint[]{2387}, - new uint[]{2388}, - new uint[]{2389}, - new uint[]{2390}, - new uint[]{2390}, - new uint[]{2391}, - new uint[]{2392}, - new uint[]{2393}, - new uint[]{2394}, - new uint[]{2395}, - new uint[]{2396}, - new uint[]{2397}, - new uint[]{2379}, - new uint[]{2398}, - new uint[]{2399}, - new uint[]{2400}, - new uint[]{2400}, - new uint[]{2401}, - new uint[]{2402}, - new uint[]{2403}, - new uint[]{2404}, - new uint[]{2405}, - new uint[]{2406}, - new uint[]{2407}, - new uint[]{2408}, - new uint[]{2409}, - new uint[]{2410}, - new uint[]{2411}, - new uint[]{2411}, - new uint[]{2412}, - new uint[]{2413}, - new uint[]{2414}, - new uint[]{2415}, - new uint[]{2416}, - new uint[]{2417}, - new uint[]{2418}, - new uint[]{2419}, - new uint[]{2420}, - new uint[]{2421}, - new uint[]{2422}, - new uint[]{2423}, - new uint[]{2424}, - new uint[]{2425}, - new uint[]{2426}, - new uint[]{2427}, - new uint[]{2428}, - new uint[]{2429}, - new uint[]{2430}, - new uint[]{2431}, - new uint[]{2432}, - new uint[]{2406}, - new uint[]{2433}, - new uint[]{2434}, - new uint[]{2435}, - new uint[]{2436}, - new uint[]{2437}, - new uint[]{2438}, - new uint[]{2439}, - new uint[]{2440}, - new uint[]{2439}, - new uint[]{2440}, - new uint[]{2441}, - new uint[]{2442}, - new uint[]{2443}, - new uint[]{2444}, - new uint[]{2445}, - new uint[]{2407}, - new uint[]{2446}, - new uint[]{2447}, - new uint[]{2448}, - new uint[]{2380}, - new uint[]{2449}, - new uint[]{2450}, - new uint[]{2451}, - new uint[]{2452}, - new uint[]{2414}, - new uint[]{2453}, - new uint[]{2454}, - new uint[]{2455}, - new uint[]{2405}, - new uint[]{2456}, - new uint[]{2457}, - new uint[]{2458}, - new uint[]{2459}, - new uint[]{2460}, - new uint[]{2461}, - new uint[]{2462}, - new uint[]{2463}, - new uint[]{2394}, - new uint[]{2464}, - new uint[]{2465}, - new uint[]{2466}, - new uint[]{2411}, - new uint[]{2429}, - new uint[]{2467}, - new uint[]{2468}, - new uint[]{2469}, - new uint[]{2470}, - new uint[]{2404}, - new uint[]{2471}, - new uint[]{2472}, - new uint[]{2473}, - new uint[]{2474}, - new uint[]{2475}, - new uint[]{2476}, - new uint[]{2477}, - new uint[]{2404}, - new uint[]{2471}, - new uint[]{2478}, - new uint[]{2479}, - new uint[]{2480}, - new uint[]{2481}, - new uint[]{2482}, - new uint[]{2482}, - new uint[]{2483}, - new uint[]{2484}, - new uint[]{2485}, - new uint[]{2486}, - new uint[]{2388}, - new uint[]{2460}, - new uint[]{2488}, - new uint[]{2489}, - new uint[]{2487}, - new uint[]{2490}, - new uint[]{2491}, - new uint[]{2492}, - new uint[]{2493}, - new uint[]{2494}, - new uint[]{2452}, - new uint[]{2495}, - new uint[]{2496}, - new uint[]{2497}, - new uint[]{2498}, - new uint[]{2499}, - new uint[]{2500}, - new uint[]{2501}, - new uint[]{2502}, - new uint[]{2503}, - new uint[]{2504}, - new uint[]{2422}, - new uint[]{2314}, - new uint[]{2374}, - Array.Empty(), - new uint[]{2550}, - Array.Empty(), - new uint[]{2334}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2291}, - new uint[]{2292}, - new uint[]{2292}, - new uint[]{2292}, - Array.Empty(), - new uint[]{2273}, - new uint[]{2295}, - new uint[]{2564}, - new uint[]{2309}, - new uint[]{2307}, - new uint[]{2313}, - new uint[]{2556}, - new uint[]{2557}, - Array.Empty(), - new uint[]{2552}, - new uint[]{2553}, - new uint[]{2554}, - new uint[]{2551}, - new uint[]{2550}, - new uint[]{2550}, - Array.Empty(), - Array.Empty(), - new uint[]{2567}, - Array.Empty(), - new uint[]{2560}, - new uint[]{2561}, - Array.Empty(), - Array.Empty(), - new uint[]{2572}, - new uint[]{2573}, - new uint[]{2574}, - new uint[]{2575}, - new uint[]{2576}, - new uint[]{2577}, - new uint[]{2578}, - new uint[]{2579}, - new uint[]{2580}, - new uint[]{2581}, - new uint[]{2582}, - new uint[]{2583}, - new uint[]{2566}, - new uint[]{2594}, - new uint[]{2596}, - new uint[]{2595}, - new uint[]{2604}, - new uint[]{2605}, - new uint[]{2606}, - new uint[]{2602}, - new uint[]{2607}, - new uint[]{2609}, - new uint[]{2619}, - new uint[]{2620}, - new uint[]{2621}, - new uint[]{2622}, - new uint[]{2609, 2621}, - new uint[]{2610}, - new uint[]{2624}, - new uint[]{2625}, - new uint[]{2626}, - new uint[]{2627}, - new uint[]{2610, 2613}, - new uint[]{2611}, - new uint[]{1478}, - new uint[]{2660}, - new uint[]{2611, 2614, 2615, 2616}, - new uint[]{2612}, - new uint[]{2628}, - new uint[]{2629}, - new uint[]{2630}, - new uint[]{2631}, - new uint[]{2632}, - new uint[]{2634}, - Array.Empty(), - Array.Empty(), - new uint[]{2636}, - new uint[]{2637}, - new uint[]{2638}, - Array.Empty(), - new uint[]{2612}, - new uint[]{2623}, - new uint[]{2597}, - Array.Empty(), - Array.Empty(), - new uint[]{2590}, - new uint[]{2598}, - Array.Empty(), - Array.Empty(), - new uint[]{2603}, - new uint[]{2505}, - Array.Empty(), - new uint[]{2599}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2660}, - new uint[]{2659}, - new uint[]{1289}, - new uint[]{2547}, - new uint[]{1288}, - new uint[]{1287}, - new uint[]{1300}, - new uint[]{108}, - new uint[]{2661}, - new uint[]{2656}, - new uint[]{1297}, - new uint[]{2658}, - new uint[]{2662}, - new uint[]{2663}, - new uint[]{1300}, - new uint[]{2653}, - new uint[]{2548}, - new uint[]{2654}, - new uint[]{2549}, - new uint[]{2656}, - new uint[]{2660}, - new uint[]{2547}, - new uint[]{2654}, - new uint[]{1385}, - new uint[]{2650}, - new uint[]{2655}, - Array.Empty(), - new uint[]{2550}, - new uint[]{2551}, - new uint[]{2550}, - new uint[]{2550}, - Array.Empty(), - new uint[]{2552}, - new uint[]{2553}, - new uint[]{2554}, - new uint[]{2555}, - new uint[]{2652}, - new uint[]{2651}, - new uint[]{2591}, - new uint[]{2589}, - Array.Empty(), - new uint[]{2584}, - new uint[]{2585}, - new uint[]{2750}, - new uint[]{2593}, - new uint[]{2586}, - new uint[]{2587}, - new uint[]{2588}, - new uint[]{2665}, - new uint[]{2666}, - new uint[]{2667}, - new uint[]{2668}, - new uint[]{2669}, - new uint[]{2516}, - new uint[]{1528}, - new uint[]{374}, - Array.Empty(), - new uint[]{2527, 2538}, - new uint[]{2528}, - new uint[]{2529}, - new uint[]{2530}, - new uint[]{2531}, - new uint[]{2532}, - new uint[]{2522}, - new uint[]{2721, 2725}, - new uint[]{2698}, - new uint[]{2706}, - new uint[]{2722}, - new uint[]{2728, 3101, 3894}, - new uint[]{2729, 2737, 3333}, - new uint[]{2746}, - new uint[]{2709}, - new uint[]{2725}, - new uint[]{2672}, - new uint[]{2700, 2710}, - Array.Empty(), - Array.Empty(), - new uint[]{2865, 2868}, - new uint[]{2670, 3021}, - new uint[]{2684, 2702}, - new uint[]{2685, 2702}, - new uint[]{2702, 2712}, - Array.Empty(), - new uint[]{2680}, - new uint[]{2681}, - Array.Empty(), - new uint[]{2677, 3106}, - new uint[]{2677}, - new uint[]{2677, 3022, 3101}, - new uint[]{2686, 3020, 3185}, - new uint[]{2686, 3020, 3185}, - new uint[]{2686, 3020, 3185}, - new uint[]{2716}, - new uint[]{2717}, - new uint[]{2718}, - new uint[]{2689, 2715}, - new uint[]{2691, 2692, 2693, 2702, 2705}, - new uint[]{2749}, - Array.Empty(), - new uint[]{2723}, - Array.Empty(), - Array.Empty(), - new uint[]{2670}, - new uint[]{2738}, - new uint[]{2739}, - new uint[]{2740}, - new uint[]{2687}, - new uint[]{2708}, - new uint[]{2699}, - new uint[]{2713}, - new uint[]{2714}, - new uint[]{2724, 2735}, - new uint[]{2734}, - new uint[]{2736}, - new uint[]{2727}, - new uint[]{2678}, - new uint[]{2679}, - new uint[]{2680}, - new uint[]{2678}, - new uint[]{2679}, - new uint[]{2681}, - new uint[]{2680}, - new uint[]{2681}, - new uint[]{2682, 3894}, - new uint[]{2695}, - new uint[]{2695}, - new uint[]{2696}, - new uint[]{2719}, - new uint[]{2726}, - new uint[]{2681}, - new uint[]{2679}, - new uint[]{2680}, - new uint[]{2678}, - new uint[]{2679}, - new uint[]{2680}, - new uint[]{2741}, - new uint[]{539}, - new uint[]{2640}, - new uint[]{2742}, - new uint[]{2742}, - new uint[]{2641}, - new uint[]{2642}, - new uint[]{2643}, - new uint[]{2174}, - new uint[]{2644}, - new uint[]{2645}, - new uint[]{2646}, - new uint[]{1474}, - new uint[]{2647}, - new uint[]{2648}, - new uint[]{2649}, - new uint[]{2570}, - new uint[]{2569}, - new uint[]{2571, 3379}, - new uint[]{2505}, - new uint[]{2665}, - Array.Empty(), - new uint[]{108}, - new uint[]{2617, 2618, 2632, 2634}, - new uint[]{2592}, - new uint[]{2751}, - new uint[]{2754}, - new uint[]{2753}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{720, 721, 722, 725}, - new uint[]{108}, - new uint[]{108, 2568, 2608, 2891}, - new uint[]{2730, 4050}, - new uint[]{2827}, - new uint[]{2828}, - new uint[]{2829}, - new uint[]{2830}, - new uint[]{2831}, - new uint[]{2887}, - new uint[]{2888}, - new uint[]{2889}, - new uint[]{2890}, - new uint[]{2892}, - Array.Empty(), - new uint[]{2894}, - new uint[]{2895}, - new uint[]{2896}, - new uint[]{2897}, - new uint[]{2898}, - new uint[]{2899}, - new uint[]{2900}, - new uint[]{2901}, - new uint[]{2902}, - new uint[]{2904}, - new uint[]{2906}, - new uint[]{2905}, - new uint[]{2993}, - new uint[]{2790}, - new uint[]{2788}, - new uint[]{2789}, - new uint[]{2787}, - new uint[]{2786}, - new uint[]{2786}, - new uint[]{2782}, - new uint[]{2783}, - new uint[]{2784}, - new uint[]{2785}, - new uint[]{2781}, - new uint[]{2780}, - new uint[]{108}, - new uint[]{2832}, - new uint[]{2832}, - new uint[]{2833}, - new uint[]{2832}, - new uint[]{2832}, - new uint[]{2833}, - new uint[]{2891}, - new uint[]{2903}, - new uint[]{2916}, - new uint[]{2168}, - new uint[]{2833}, - new uint[]{2778}, - new uint[]{2779}, - new uint[]{2775}, - new uint[]{2776}, - new uint[]{2777}, - new uint[]{2086}, - new uint[]{2086}, - new uint[]{2086}, - new uint[]{2086}, - new uint[]{2774}, - new uint[]{2774}, - new uint[]{2809}, - new uint[]{2808}, - new uint[]{2801}, - new uint[]{2806}, - new uint[]{2805}, - new uint[]{2804}, - new uint[]{2800}, - new uint[]{2803}, - new uint[]{2802}, - new uint[]{2801}, - new uint[]{2800}, - new uint[]{2799}, - new uint[]{2796}, - new uint[]{2795}, - new uint[]{2798}, - new uint[]{2794}, - new uint[]{2797}, - new uint[]{2797}, - new uint[]{2792}, - new uint[]{2807}, - new uint[]{2791}, - new uint[]{2793}, - new uint[]{2815}, - new uint[]{2814}, - new uint[]{2813}, - new uint[]{2812}, - new uint[]{2824}, - new uint[]{2823}, - new uint[]{2822}, - new uint[]{108}, - new uint[]{2825}, - new uint[]{2821}, - new uint[]{2820}, - new uint[]{2819}, - new uint[]{2818}, - new uint[]{2817}, - new uint[]{2816}, - new uint[]{2826}, - new uint[]{2886}, - new uint[]{2851, 2970}, - new uint[]{2851, 2970}, - new uint[]{2852}, - new uint[]{2854}, - new uint[]{2853}, - new uint[]{2855}, - new uint[]{2856}, - Array.Empty(), - new uint[]{2857}, - new uint[]{2858}, - new uint[]{2859}, - new uint[]{2860}, - new uint[]{2861}, - Array.Empty(), - new uint[]{2758}, - new uint[]{2759}, - new uint[]{2760}, - new uint[]{2761}, - new uint[]{2762}, - new uint[]{2763}, - new uint[]{2764}, - new uint[]{2765}, - new uint[]{2766}, - new uint[]{2767}, - new uint[]{2768}, - new uint[]{2769}, - new uint[]{2770}, - new uint[]{2772}, - new uint[]{2771}, - new uint[]{2773}, - new uint[]{2810}, - new uint[]{2834}, - new uint[]{2836}, - new uint[]{2835}, - new uint[]{2837}, - Array.Empty(), - new uint[]{2839}, - new uint[]{2840}, - new uint[]{2841}, - new uint[]{2842}, - new uint[]{2843}, - new uint[]{2844}, - new uint[]{2845}, - new uint[]{2846}, - new uint[]{2847}, - new uint[]{2848}, - new uint[]{2849}, - new uint[]{2850}, - new uint[]{2884}, - new uint[]{2885}, - Array.Empty(), - new uint[]{2168}, - new uint[]{2994}, - new uint[]{2872}, - new uint[]{2873}, - new uint[]{3218}, - new uint[]{2995}, - new uint[]{2994}, - new uint[]{2994}, - new uint[]{2994}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2868}, - new uint[]{2868}, - new uint[]{2869}, - new uint[]{2870}, - Array.Empty(), - new uint[]{2864, 2875, 3894}, - new uint[]{2863, 2875, 3021}, - new uint[]{2862}, - new uint[]{3182}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2914}, - new uint[]{2915}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2609}, - new uint[]{2619}, - new uint[]{2620}, - new uint[]{2621}, - new uint[]{2622}, - new uint[]{2623}, - new uint[]{2609, 2621}, - new uint[]{2610}, - new uint[]{2624}, - new uint[]{2625}, - new uint[]{2626}, - new uint[]{2627}, - new uint[]{2610, 2613}, - new uint[]{2611}, - new uint[]{1478}, - new uint[]{2611, 2614, 2615, 2616}, - new uint[]{2612}, - new uint[]{2628}, - new uint[]{2629}, - new uint[]{2630}, - new uint[]{2631}, - new uint[]{2632}, - new uint[]{2634}, - new uint[]{2635}, - new uint[]{2636}, - new uint[]{2637}, - new uint[]{2638}, - new uint[]{2612}, - new uint[]{2617, 2618, 2632, 2634}, - new uint[]{539}, - new uint[]{2640}, - new uint[]{2742}, - new uint[]{2742}, - new uint[]{2641}, - new uint[]{2642}, - new uint[]{2643}, - new uint[]{2174}, - new uint[]{2644}, - new uint[]{2645}, - new uint[]{2646}, - new uint[]{1474}, - new uint[]{2647}, - new uint[]{2648}, - new uint[]{2649}, - new uint[]{2919}, - new uint[]{2920}, - new uint[]{2921}, - new uint[]{2922}, - new uint[]{2923}, - new uint[]{2924}, - new uint[]{2925}, - new uint[]{2926}, - new uint[]{2927}, - new uint[]{2928}, - new uint[]{2929}, - new uint[]{2930}, - new uint[]{2931}, - new uint[]{2932}, - new uint[]{2933}, - new uint[]{2934}, - new uint[]{2935}, - new uint[]{2936}, - new uint[]{2937}, - new uint[]{2938}, - new uint[]{2939}, - new uint[]{2940}, - new uint[]{2941}, - new uint[]{2942}, - new uint[]{2943}, - new uint[]{2944}, - new uint[]{2945}, - new uint[]{2946}, - new uint[]{2947}, - new uint[]{2948}, - new uint[]{2949}, - new uint[]{2950}, - new uint[]{2951}, - new uint[]{2952}, - new uint[]{2953}, - new uint[]{2954}, - new uint[]{2955}, - new uint[]{2956}, - new uint[]{2957}, - new uint[]{2958}, - new uint[]{2959}, - new uint[]{2960}, - new uint[]{2961}, - new uint[]{2962}, - new uint[]{2963}, - new uint[]{2964}, - new uint[]{2965}, - new uint[]{2966}, - new uint[]{2967}, - new uint[]{2968}, - new uint[]{2969}, - new uint[]{3330}, - new uint[]{2866}, - new uint[]{2972}, - new uint[]{2973}, - new uint[]{2974}, - new uint[]{2975}, - new uint[]{2976}, - new uint[]{2977}, - new uint[]{2978}, - new uint[]{2979}, - new uint[]{2980}, - new uint[]{2981}, - Array.Empty(), - new uint[]{2984}, - new uint[]{2988}, - new uint[]{2992}, - new uint[]{3050}, - new uint[]{3051}, - new uint[]{3052}, - new uint[]{3053}, - new uint[]{3054}, - new uint[]{3055}, - new uint[]{3330}, - new uint[]{5763}, - new uint[]{3056}, - new uint[]{3057}, - new uint[]{3058}, - new uint[]{3059}, - new uint[]{3060}, - new uint[]{3061}, - new uint[]{3666}, - Array.Empty(), - new uint[]{3014}, - new uint[]{3015}, - new uint[]{3016}, - new uint[]{3017}, - new uint[]{3018}, - new uint[]{3019}, - new uint[]{2904}, - new uint[]{2906}, - new uint[]{2905}, - new uint[]{2997}, - new uint[]{2998}, - new uint[]{2999}, - new uint[]{3000}, - new uint[]{3001}, - new uint[]{3002}, - new uint[]{3003}, - new uint[]{3004}, - new uint[]{3005}, - new uint[]{3006}, - new uint[]{3007}, - new uint[]{3008}, - new uint[]{3009}, - new uint[]{3010}, - new uint[]{3011}, - new uint[]{3012}, - new uint[]{3013}, - new uint[]{3065}, - new uint[]{3192}, - new uint[]{3193}, - new uint[]{3194}, - new uint[]{3192, 3193}, - new uint[]{3197}, - new uint[]{3198}, - new uint[]{3199}, - new uint[]{3200}, - new uint[]{3201}, - new uint[]{3197, 3199, 3200, 3201}, - new uint[]{3204}, - new uint[]{3205}, - new uint[]{3206}, - new uint[]{3207}, - new uint[]{3208}, - new uint[]{3209}, - new uint[]{3209}, - new uint[]{3209}, - new uint[]{3204}, - new uint[]{3210}, - new uint[]{3211}, - new uint[]{3212}, - new uint[]{3213}, - Array.Empty(), - new uint[]{3214}, - new uint[]{3215}, - new uint[]{3216}, - new uint[]{3217}, - new uint[]{3210}, - new uint[]{261}, - new uint[]{267}, - new uint[]{3190}, - new uint[]{3191}, - new uint[]{1467}, - new uint[]{3196}, - new uint[]{3195}, - new uint[]{2810}, - new uint[]{3240}, - new uint[]{3242}, - new uint[]{3069}, - new uint[]{3070}, - new uint[]{3062}, - new uint[]{3063}, - new uint[]{3064}, - new uint[]{2994}, - new uint[]{3218}, - new uint[]{2995}, - new uint[]{2994}, - new uint[]{2994}, - new uint[]{2994}, - new uint[]{3038}, - new uint[]{3014}, - new uint[]{3039}, - new uint[]{3040}, - new uint[]{3041}, - new uint[]{3043}, - new uint[]{3042}, - new uint[]{3044}, - new uint[]{3044}, - new uint[]{3045}, - new uint[]{3072}, - new uint[]{3073}, - new uint[]{3074}, - new uint[]{3071}, - new uint[]{3075}, - new uint[]{3066}, - new uint[]{3067}, - new uint[]{3068}, - new uint[]{3028}, - new uint[]{3288}, - new uint[]{3030}, - new uint[]{3032}, - new uint[]{3037}, - new uint[]{114}, - new uint[]{3031}, - new uint[]{3027}, - new uint[]{3034}, - new uint[]{3035}, - new uint[]{3219}, - new uint[]{3029}, - new uint[]{108, 1644, 2775, 3271, 3272, 3408, 3428, 3434, 3437, 3634, 3639, 3642, 3744, 3851, 3852, 4555, 4567, 4568, 4571, 4739, 4745, 4747, 5259, 5273}, - new uint[]{3046}, - new uint[]{3047}, - new uint[]{3046}, - new uint[]{3047}, - new uint[]{3048}, - new uint[]{3046}, - new uint[]{3046}, - new uint[]{3047}, - new uint[]{3038}, - new uint[]{3049}, - new uint[]{3033}, - new uint[]{3220}, - Array.Empty(), - new uint[]{3022}, - new uint[]{3021}, - new uint[]{887}, - new uint[]{1858}, - new uint[]{1858}, - new uint[]{887}, - new uint[]{3026}, - new uint[]{3076}, - new uint[]{3164}, - new uint[]{3169}, - new uint[]{3172}, - new uint[]{3168}, - new uint[]{3163}, - new uint[]{3162}, - new uint[]{3164}, - new uint[]{3129}, - new uint[]{3130}, - new uint[]{3131}, - new uint[]{3045}, - new uint[]{3133}, - new uint[]{3134}, - new uint[]{3135}, - new uint[]{3136}, - new uint[]{3137}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3139}, - new uint[]{3140}, - new uint[]{3141}, - new uint[]{3133}, - new uint[]{3142}, - new uint[]{3138}, - new uint[]{3129}, - new uint[]{3143}, - new uint[]{3144}, - new uint[]{3145}, - new uint[]{3146}, - new uint[]{3133}, - new uint[]{3129}, - new uint[]{3148}, - new uint[]{3147}, - new uint[]{3149}, - new uint[]{3272}, - new uint[]{3273}, - new uint[]{3274}, - new uint[]{3275}, - new uint[]{3276}, - new uint[]{3277}, - Array.Empty(), - new uint[]{3279}, - new uint[]{3280}, - new uint[]{3281}, - new uint[]{3282}, - new uint[]{3283}, - new uint[]{3284}, - new uint[]{3285}, - new uint[]{3286}, - new uint[]{1695}, - Array.Empty(), - new uint[]{3255}, - new uint[]{3256}, - new uint[]{3257}, - new uint[]{3258}, - new uint[]{3259}, - new uint[]{3260}, - new uint[]{3261}, - new uint[]{3262}, - new uint[]{3263}, - new uint[]{3264}, - new uint[]{3265}, - new uint[]{3266}, - new uint[]{3267}, - new uint[]{3268}, - new uint[]{3269}, - new uint[]{3270}, - new uint[]{3271}, - new uint[]{3150}, - new uint[]{3151}, - new uint[]{3152}, - new uint[]{3133}, - new uint[]{3153}, - new uint[]{3154}, - new uint[]{3155}, - new uint[]{3156}, - new uint[]{3159}, - new uint[]{3157}, - new uint[]{3158}, - new uint[]{3189}, - new uint[]{3160}, - new uint[]{3133}, - new uint[]{3138}, - new uint[]{3129}, - new uint[]{3165}, - new uint[]{3164}, - new uint[]{3166}, - new uint[]{3167}, - new uint[]{3170}, - new uint[]{3166}, - new uint[]{3167}, - new uint[]{3165}, - new uint[]{3164}, - new uint[]{3169}, - new uint[]{3243}, - new uint[]{3244}, - new uint[]{3246}, - new uint[]{3245}, - new uint[]{3247}, - new uint[]{3248}, - new uint[]{3249}, - new uint[]{3250}, - new uint[]{3380, 3381, 3382}, - new uint[]{3252}, - new uint[]{3119}, - new uint[]{3120}, - new uint[]{3121}, - new uint[]{3122}, - new uint[]{3123}, - new uint[]{3124}, - new uint[]{3125}, - new uint[]{3126}, - new uint[]{3110}, - new uint[]{2120}, - new uint[]{3114}, - new uint[]{3111}, - new uint[]{3112}, - new uint[]{3113}, - new uint[]{3118}, - new uint[]{3115}, - new uint[]{3116}, - new uint[]{3117}, - new uint[]{3127}, - new uint[]{3128}, - new uint[]{3132}, - new uint[]{3221}, - new uint[]{3152}, - new uint[]{3161}, - new uint[]{3165}, - new uint[]{3167}, - new uint[]{3164}, - new uint[]{3170}, - new uint[]{3172}, - new uint[]{3172}, - new uint[]{3173, 3174, 3175}, - new uint[]{3210}, - new uint[]{3213}, - new uint[]{3172}, - new uint[]{3172}, - new uint[]{3171}, - new uint[]{3091}, - new uint[]{3092}, - new uint[]{3093}, - Array.Empty(), - new uint[]{3095}, - new uint[]{3096}, - new uint[]{3097}, - new uint[]{3098}, - new uint[]{3077}, - new uint[]{3078}, - new uint[]{3079}, - new uint[]{3080}, - new uint[]{3081}, - new uint[]{108, 3082}, - new uint[]{3083}, - new uint[]{3084}, - new uint[]{3085}, - new uint[]{3086}, - new uint[]{3087}, - new uint[]{3088}, - new uint[]{3089}, - new uint[]{3090}, - new uint[]{3100}, - new uint[]{3100}, - new uint[]{3104}, - new uint[]{3104}, - new uint[]{3102}, - new uint[]{3103}, - new uint[]{3101}, - new uint[]{3107}, - new uint[]{3108}, - Array.Empty(), - new uint[]{3106}, - new uint[]{3105}, - Array.Empty(), - new uint[]{3188}, - new uint[]{3118, 3179}, - new uint[]{3183}, - new uint[]{3184}, - new uint[]{3153}, - new uint[]{3153}, - new uint[]{3154}, - new uint[]{3166}, - new uint[]{3166}, - new uint[]{3234}, - new uint[]{3235}, - new uint[]{3236}, - new uint[]{3237}, - new uint[]{3238}, - new uint[]{3239}, - new uint[]{3227}, - new uint[]{3228}, - new uint[]{3229}, - new uint[]{3230}, - new uint[]{3231}, - new uint[]{3232}, - new uint[]{3233}, - new uint[]{3241}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{108, 749, 4571, 8395}, - new uint[]{2993}, - new uint[]{108, 8395}, - new uint[]{3077}, - new uint[]{3222}, - new uint[]{3223}, - new uint[]{3224}, - new uint[]{3225}, - new uint[]{3301}, - new uint[]{3345}, - new uint[]{3345}, - new uint[]{3345}, - new uint[]{3082}, - new uint[]{3302}, - new uint[]{3303}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3078}, - new uint[]{2665}, - new uint[]{2665}, - new uint[]{3304}, - new uint[]{3307}, - new uint[]{2665, 3305}, - new uint[]{3304}, - new uint[]{3305}, - new uint[]{3306}, - new uint[]{3321}, - new uint[]{3322}, - new uint[]{3323}, - new uint[]{3324}, - Array.Empty(), - new uint[]{3325}, - new uint[]{3326}, - Array.Empty(), - new uint[]{3329}, - new uint[]{2168}, - new uint[]{3331}, - new uint[]{3314}, - Array.Empty(), - new uint[]{3315}, - new uint[]{3316}, - new uint[]{3317}, - new uint[]{3318}, - new uint[]{3319}, - new uint[]{3320}, - new uint[]{3309, 3310, 3311}, - Array.Empty(), - new uint[]{3287}, - new uint[]{3386}, - new uint[]{3289}, - new uint[]{3290}, - new uint[]{3291}, - new uint[]{3292}, - new uint[]{3294}, - new uint[]{3293}, - new uint[]{3335}, - new uint[]{3337}, - new uint[]{3339}, - new uint[]{3340}, - Array.Empty(), - new uint[]{3300}, - new uint[]{3046}, - new uint[]{108}, - new uint[]{108}, - new uint[]{3355}, - new uint[]{3354}, - new uint[]{3353}, - new uint[]{3368}, - new uint[]{3367}, - new uint[]{3366}, - new uint[]{3365}, - new uint[]{3358}, - new uint[]{3357}, - Array.Empty(), - new uint[]{3363}, - new uint[]{3362}, - new uint[]{3361}, - new uint[]{3360}, - new uint[]{3359}, - Array.Empty(), - new uint[]{3352}, - new uint[]{3351}, - new uint[]{3350}, - new uint[]{3349}, - new uint[]{3369}, - new uint[]{3370}, - new uint[]{3373}, - new uint[]{3357}, - new uint[]{3374}, - new uint[]{3375}, - new uint[]{3375}, - Array.Empty(), - new uint[]{3251}, - new uint[]{3362}, - new uint[]{3361}, - new uint[]{3360}, - new uint[]{3359}, - new uint[]{3298}, - new uint[]{3298}, - new uint[]{3378}, - new uint[]{3372}, - new uint[]{3371}, - new uint[]{3095}, - new uint[]{3, 5, 9, 11, 19, 20, 52, 83, 90, 131, 159, 165, 242, 312, 451, 471, 540, 567, 584, 588, 593, 598, 599, 606, 653, 658, 665, 668, 719, 737, 744, 750, 834, 839, 906, 939, 959, 962, 964, 993, 1023, 1033, 1043, 1052, 1056, 1058, 1102, 1103, 1105, 1106, 1114, 1126, 1133, 1142, 1143, 1144, 1215, 1227, 1244, 1248, 1276, 1279, 1280, 1373, 1374, 1375, 1376, 1377, 1380, 1381, 1382, 1384, 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, 1395, 1396, 1397, 1399, 1402, 1403, 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, 1413, 1414, 1417, 1418, 1420, 1565, 1566, 1567, 1568, 1569, 1570, 1571, 1573, 1574, 1575, 1577, 1578, 1589, 1603, 1604, 1605, 1607}, - new uint[]{108}, - new uint[]{108, 3373, 3374, 3375, 3387}, - new uint[]{3370}, - new uint[]{2665}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{3923}, - new uint[]{3925}, - new uint[]{3930}, - new uint[]{3931}, - new uint[]{3932}, - new uint[]{3933}, - new uint[]{3789}, - new uint[]{3405}, - new uint[]{3406}, - new uint[]{3407}, - new uint[]{3408}, - new uint[]{3409}, - new uint[]{3410}, - new uint[]{3791}, - Array.Empty(), - new uint[]{3793}, - new uint[]{3794}, - Array.Empty(), - new uint[]{3796}, - new uint[]{3797}, - new uint[]{3798}, - new uint[]{3818}, - new uint[]{3819}, - new uint[]{3820}, - new uint[]{3821}, - new uint[]{2143}, - new uint[]{3822}, - new uint[]{4383}, - new uint[]{4384}, - new uint[]{3823}, - new uint[]{4382}, - new uint[]{4383}, - new uint[]{4384}, - new uint[]{3293}, - new uint[]{3930}, - new uint[]{2343}, - new uint[]{3452}, - new uint[]{3453}, - new uint[]{3454}, - new uint[]{3455}, - new uint[]{3456}, - new uint[]{3457}, - new uint[]{3458}, - new uint[]{3459}, - new uint[]{3460}, - new uint[]{3461}, - new uint[]{3462}, - new uint[]{3463}, - new uint[]{3464}, - new uint[]{3465}, - new uint[]{3660}, - new uint[]{3660}, - new uint[]{3661}, - new uint[]{3661}, - new uint[]{3662}, - new uint[]{3662}, - new uint[]{3663}, - new uint[]{3663}, - new uint[]{3664}, - new uint[]{3664}, - new uint[]{3745}, - new uint[]{3746}, - new uint[]{3747}, - new uint[]{3748}, - new uint[]{3749}, - new uint[]{3747, 3748, 3750}, - new uint[]{4492}, - new uint[]{4420}, - new uint[]{1385}, - new uint[]{4133}, - new uint[]{3735}, - new uint[]{3921}, - new uint[]{3918}, - new uint[]{3910}, - new uint[]{3922}, - new uint[]{3913}, - new uint[]{3912}, - new uint[]{3915}, - new uint[]{3916}, - new uint[]{3911}, - new uint[]{3917}, - new uint[]{3920}, - new uint[]{3909}, - new uint[]{3914}, - new uint[]{3923}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3388}, - new uint[]{3389}, - new uint[]{3390}, - new uint[]{3391}, - new uint[]{3392}, - new uint[]{3393}, - new uint[]{3394}, - new uint[]{3395}, - new uint[]{3396}, - new uint[]{3397}, - new uint[]{3398}, - new uint[]{3399}, - new uint[]{3400}, - new uint[]{3401}, - new uint[]{3402}, - new uint[]{3403}, - new uint[]{3404}, - new uint[]{3409}, - new uint[]{3649}, - new uint[]{3649, 3658}, - new uint[]{4606}, - new uint[]{108}, - new uint[]{3650}, - new uint[]{3651}, - new uint[]{3652}, - new uint[]{3653}, - new uint[]{3654}, - new uint[]{3655}, - new uint[]{3658}, - Array.Empty(), - new uint[]{3649}, - new uint[]{108, 3649}, - new uint[]{3650}, - new uint[]{8144}, - new uint[]{3652}, - new uint[]{3653}, - new uint[]{3654}, - new uint[]{3658}, - new uint[]{3655}, - new uint[]{3754}, - new uint[]{3755}, - new uint[]{4142}, - new uint[]{4490}, - new uint[]{1300}, - new uint[]{3753}, - new uint[]{3757}, - new uint[]{8145}, - new uint[]{3758}, - new uint[]{3759}, - new uint[]{3760}, - new uint[]{3761}, - new uint[]{2667}, - Array.Empty(), - new uint[]{3758}, - new uint[]{3754}, - new uint[]{3755}, - new uint[]{3753}, - new uint[]{3757}, - new uint[]{8146}, - new uint[]{3758}, - new uint[]{3759}, - new uint[]{3760}, - new uint[]{3761}, - new uint[]{2667}, - Array.Empty(), - Array.Empty(), - new uint[]{3818}, - new uint[]{3799}, - new uint[]{3800}, - new uint[]{3801}, - new uint[]{3802}, - new uint[]{3803}, - new uint[]{3804}, - Array.Empty(), - new uint[]{3805}, - new uint[]{3807}, - new uint[]{3808}, - new uint[]{3809, 3815}, - new uint[]{3809}, - new uint[]{3810, 3816}, - new uint[]{3810}, - new uint[]{3811}, - new uint[]{3812}, - new uint[]{3026}, - new uint[]{3813}, - new uint[]{3814}, - new uint[]{3806}, - new uint[]{3815}, - new uint[]{3816}, - Array.Empty(), - new uint[]{3772}, - new uint[]{3772}, - new uint[]{3773}, - new uint[]{3778}, - new uint[]{3779}, - new uint[]{3774}, - new uint[]{3775}, - new uint[]{3759}, - new uint[]{3776}, - new uint[]{3777}, - new uint[]{3772}, - new uint[]{3772}, - new uint[]{3773}, - new uint[]{3778}, - new uint[]{3774}, - new uint[]{3759}, - new uint[]{6305}, - new uint[]{4259}, - Array.Empty(), - new uint[]{3824}, - new uint[]{3825}, - new uint[]{3826}, - new uint[]{3827}, - new uint[]{3828}, - new uint[]{3829}, - new uint[]{3830}, - new uint[]{3831}, - new uint[]{3832}, - new uint[]{3833}, - new uint[]{3834}, - new uint[]{4336}, - new uint[]{3835}, - new uint[]{3836}, - new uint[]{3837}, - new uint[]{3838}, - new uint[]{3839}, - new uint[]{3840}, - new uint[]{4340}, - new uint[]{4339}, - new uint[]{3818}, - new uint[]{3825}, - new uint[]{4489}, - new uint[]{3765}, - Array.Empty(), - new uint[]{3378}, - new uint[]{3766}, - new uint[]{3766}, - new uint[]{8958}, - new uint[]{3767}, - new uint[]{3770}, - new uint[]{3769}, - new uint[]{3771}, - new uint[]{3768}, - new uint[]{3765}, - new uint[]{4141}, - new uint[]{3726}, - new uint[]{3372}, - Array.Empty(), - new uint[]{3119}, - new uint[]{3120}, - new uint[]{4135}, - new uint[]{4136}, - new uint[]{4137}, - new uint[]{4138}, - new uint[]{3721}, - new uint[]{3723}, - new uint[]{3724}, - new uint[]{3722}, - new uint[]{3731}, - new uint[]{3732}, - new uint[]{3733}, - new uint[]{3736}, - new uint[]{3737}, - new uint[]{108}, - new uint[]{3739}, - new uint[]{3278}, - new uint[]{3727}, - new uint[]{3728}, - new uint[]{3734}, - new uint[]{3740}, - new uint[]{3741}, - new uint[]{3742}, - new uint[]{4130}, - new uint[]{3127}, - new uint[]{4139}, - new uint[]{4140}, - new uint[]{2064}, - new uint[]{4116}, - new uint[]{4125}, - new uint[]{4117, 4506}, - new uint[]{4126}, - new uint[]{4130}, - new uint[]{1394}, - new uint[]{4116}, - new uint[]{3481, 3489}, - Array.Empty(), - new uint[]{3479}, - Array.Empty(), - new uint[]{3488}, - new uint[]{3492}, - new uint[]{3483, 3490}, - new uint[]{3485}, - Array.Empty(), - Array.Empty(), - new uint[]{3484}, - new uint[]{3487}, - new uint[]{3486}, - new uint[]{3582, 3620, 3670, 4289}, - new uint[]{3574, 3584, 3591, 3900}, - new uint[]{3573, 3583, 3590, 3902, 4049}, - new uint[]{3575, 3585, 3592, 3901}, - new uint[]{4337}, - new uint[]{4338}, - new uint[]{4390}, - new uint[]{3581}, - new uint[]{3577}, - new uint[]{3565, 3588, 3688}, - new uint[]{3578, 3691}, - Array.Empty(), - new uint[]{3564}, - new uint[]{3571, 3696}, - new uint[]{3579, 3693}, - new uint[]{3580}, - new uint[]{3568, 3668, 4052, 4284}, - new uint[]{3608, 4070}, - new uint[]{3597}, - new uint[]{3593, 3715}, - new uint[]{3598, 3706, 3945}, - new uint[]{3595, 3712}, - new uint[]{3611}, - Array.Empty(), - new uint[]{3612}, - new uint[]{3603, 3714}, - new uint[]{3607, 3710}, - new uint[]{3610}, - new uint[]{3605, 3713}, - new uint[]{3596, 3711}, - new uint[]{3566, 3606}, - new uint[]{3599}, - new uint[]{3600, 3707}, - new uint[]{3601, 3708}, - new uint[]{3602, 3709}, - new uint[]{56}, - new uint[]{3609}, - new uint[]{3626, 3977}, - new uint[]{3624, 4045}, - new uint[]{3613}, - new uint[]{3614, 4277}, - new uint[]{3559}, - new uint[]{3615, 3700, 4042}, - new uint[]{3617, 3701}, - Array.Empty(), - new uint[]{3629, 4294}, - new uint[]{3625, 4291}, - new uint[]{3542}, - new uint[]{117}, - new uint[]{3526}, - new uint[]{3507, 3703}, - new uint[]{3512}, - new uint[]{3501, 3515, 3527}, - new uint[]{3502, 3516, 3528, 4061}, - new uint[]{3503, 3517, 3529}, - new uint[]{3511, 3705}, - new uint[]{3671, 4059}, - new uint[]{3704, 4071}, - new uint[]{3506, 3674}, - new uint[]{3500, 3514, 3531, 3672}, - new uint[]{3509}, - new uint[]{3510, 4288}, - new uint[]{3594, 4283, 4301}, - new uint[]{3611}, - new uint[]{3504, 3673}, - new uint[]{3524, 4293}, - Array.Empty(), - new uint[]{3523, 3702, 4060}, - Array.Empty(), - new uint[]{3513, 3530}, - new uint[]{3505, 3675}, - new uint[]{3525}, - new uint[]{3544}, - new uint[]{3541}, - Array.Empty(), - Array.Empty(), - new uint[]{3554}, - new uint[]{3619, 3698, 4053, 4278, 4290}, - Array.Empty(), - new uint[]{3623}, - new uint[]{3555}, - new uint[]{3556}, - new uint[]{3537}, - Array.Empty(), - new uint[]{3552}, - new uint[]{4399}, - new uint[]{3538}, - new uint[]{3539}, - new uint[]{3543}, - new uint[]{3561}, - new uint[]{3551}, - new uint[]{3534}, - new uint[]{3535}, - new uint[]{3536}, - new uint[]{3532}, - Array.Empty(), - new uint[]{3557}, - new uint[]{4128}, - new uint[]{3502}, - new uint[]{3503}, - new uint[]{4129}, - new uint[]{2082}, - new uint[]{1990}, - new uint[]{108}, - new uint[]{4346}, - new uint[]{4347}, - new uint[]{4348}, - new uint[]{4349}, - new uint[]{4346}, - new uint[]{4347}, - new uint[]{4348}, - new uint[]{4349}, - new uint[]{4130}, - new uint[]{3850}, - new uint[]{4131}, - new uint[]{3428}, - new uint[]{3429}, - new uint[]{3430}, - new uint[]{3431}, - new uint[]{3432}, - new uint[]{3433}, - new uint[]{3434}, - new uint[]{3435}, - new uint[]{3436}, - new uint[]{3411}, - new uint[]{3412}, - new uint[]{3413}, - new uint[]{3414}, - new uint[]{3415}, - new uint[]{3416}, - new uint[]{3417}, - new uint[]{3418}, - new uint[]{3419}, - new uint[]{3420}, - new uint[]{3421}, - new uint[]{3422}, - new uint[]{3423}, - new uint[]{3424}, - new uint[]{3425}, - new uint[]{3426}, - new uint[]{3427}, - new uint[]{4132}, - new uint[]{3540}, - new uint[]{3586}, - Array.Empty(), - new uint[]{3817}, - new uint[]{4154}, - new uint[]{4154}, - new uint[]{1391}, - new uint[]{4155}, - new uint[]{4156}, - new uint[]{4157}, - new uint[]{4145}, - new uint[]{4158}, - new uint[]{4159}, - new uint[]{4160}, - new uint[]{4161}, - new uint[]{4162}, - new uint[]{1392}, - new uint[]{4150}, - new uint[]{4151}, - new uint[]{4152}, - new uint[]{4153}, - new uint[]{4130}, - new uint[]{1394}, - new uint[]{4130}, - new uint[]{4179}, - new uint[]{3563, 3690}, - new uint[]{4130}, - new uint[]{2082}, - new uint[]{4144}, - new uint[]{2077}, - new uint[]{4143}, - new uint[]{2080}, - new uint[]{2076}, - new uint[]{3849}, - new uint[]{3841}, - new uint[]{3843}, - new uint[]{3634}, - new uint[]{4385}, - new uint[]{3850}, - new uint[]{3639}, - new uint[]{3293}, - new uint[]{3642}, - new uint[]{3851}, - new uint[]{3852}, - new uint[]{4400}, - new uint[]{3841}, - new uint[]{3841}, - new uint[]{3319}, - new uint[]{3319}, - new uint[]{4401}, - new uint[]{3841}, - new uint[]{3841}, - new uint[]{3842}, - new uint[]{3842}, - new uint[]{3843}, - new uint[]{3843}, - new uint[]{3844}, - new uint[]{3845}, - new uint[]{3846}, - new uint[]{3847}, - new uint[]{3848}, - new uint[]{2234}, - new uint[]{4148}, - new uint[]{4147}, - new uint[]{4146}, - new uint[]{2098}, - new uint[]{2099}, - new uint[]{1453}, - new uint[]{4427}, - new uint[]{4178}, - new uint[]{3632}, - new uint[]{3633}, - new uint[]{3634}, - new uint[]{3635}, - new uint[]{3636}, - new uint[]{3637}, - new uint[]{3638}, - new uint[]{3639}, - new uint[]{3640}, - new uint[]{3641}, - new uint[]{3642}, - new uint[]{3643}, - new uint[]{3644}, - new uint[]{3645}, - new uint[]{4385}, - new uint[]{4386}, - new uint[]{4387}, - new uint[]{3632, 3634, 3635, 3639, 3640, 3641, 3642, 3643, 3645}, - new uint[]{3223}, - new uint[]{3224}, - new uint[]{1848}, - new uint[]{4173}, - new uint[]{4174}, - new uint[]{2347}, - new uint[]{4180}, - new uint[]{4185}, - new uint[]{4184}, - new uint[]{4186}, - new uint[]{4187}, - new uint[]{3438}, - new uint[]{3439}, - new uint[]{3440}, - new uint[]{3441}, - new uint[]{3442}, - new uint[]{3443}, - new uint[]{3445, 11994}, - new uint[]{3446, 11993}, - new uint[]{3447}, - new uint[]{3448}, - new uint[]{3449}, - new uint[]{3450}, - new uint[]{3451}, - new uint[]{3445, 11994}, - new uint[]{4188}, - new uint[]{1422}, - new uint[]{1423}, - new uint[]{3665}, - new uint[]{3665}, - new uint[]{4381}, - new uint[]{4163}, - new uint[]{4164}, - new uint[]{4165}, - new uint[]{4166}, - new uint[]{4167}, - new uint[]{4168}, - new uint[]{4109}, - new uint[]{4169}, - new uint[]{4170}, - new uint[]{4171}, - new uint[]{4172}, - new uint[]{4426}, - new uint[]{4342}, - new uint[]{4343}, - new uint[]{4344}, - new uint[]{4345}, - new uint[]{4388}, - new uint[]{4389}, - new uint[]{3919}, - new uint[]{3915}, - Array.Empty(), - new uint[]{4190}, - new uint[]{4190}, - new uint[]{4191}, - new uint[]{3045}, - new uint[]{4192}, - new uint[]{4193}, - new uint[]{365}, - new uint[]{398}, - new uint[]{106}, - new uint[]{117}, - new uint[]{4194}, - new uint[]{3929}, - new uint[]{3928}, - new uint[]{4195}, - new uint[]{4196}, - new uint[]{407}, - new uint[]{171}, - new uint[]{45}, - new uint[]{170}, - new uint[]{24}, - new uint[]{3854}, - new uint[]{3855}, - new uint[]{3856}, - new uint[]{3857}, - new uint[]{4424}, - new uint[]{3859}, - new uint[]{3860}, - new uint[]{3861}, - new uint[]{3862}, - new uint[]{3863}, - new uint[]{114}, - new uint[]{3476}, - new uint[]{3475}, - new uint[]{3478}, - new uint[]{3743}, - new uint[]{4116}, - new uint[]{4423}, - new uint[]{4126}, - new uint[]{4127}, - new uint[]{4197, 4198, 4199}, - new uint[]{4200}, - new uint[]{4201}, - new uint[]{4202}, - new uint[]{1440}, - new uint[]{4203}, - new uint[]{3888}, - new uint[]{4193}, - new uint[]{4204}, - Array.Empty(), - new uint[]{2147}, - new uint[]{4107}, - new uint[]{4173}, - new uint[]{4174}, - new uint[]{2096}, - new uint[]{4175}, - new uint[]{4176}, - new uint[]{1391}, - new uint[]{1392}, - Array.Empty(), - new uint[]{1393}, - new uint[]{1990}, - new uint[]{4184}, - new uint[]{4181}, - new uint[]{4182}, - new uint[]{4183}, - new uint[]{445}, - new uint[]{4189}, - new uint[]{3338}, - new uint[]{3326}, - new uint[]{3339}, - new uint[]{4193}, - new uint[]{4205}, - new uint[]{4206}, - new uint[]{4207}, - new uint[]{4208}, - new uint[]{4209}, - new uint[]{4220}, - new uint[]{4221}, - new uint[]{4222}, - new uint[]{4079, 4097, 4117}, - new uint[]{4101}, - new uint[]{4102}, - new uint[]{4035}, - new uint[]{3900, 4048, 4393}, - new uint[]{3902}, - new uint[]{3901}, - new uint[]{3900}, - new uint[]{3901}, - new uint[]{3902}, - new uint[]{3901}, - new uint[]{4109}, - Array.Empty(), - new uint[]{4068, 4100}, - new uint[]{4112}, - new uint[]{3908}, - new uint[]{3893}, - new uint[]{3894, 4123}, - new uint[]{3894, 4123}, - new uint[]{3894}, - new uint[]{3896}, - new uint[]{3895}, - new uint[]{3897}, - new uint[]{3898}, - new uint[]{3899}, - new uint[]{3900}, - new uint[]{3901}, - new uint[]{3901}, - new uint[]{3900}, - new uint[]{3900}, - new uint[]{3902}, - new uint[]{4030}, - new uint[]{4031}, - new uint[]{4032}, - new uint[]{4030}, - new uint[]{4033}, - new uint[]{4031}, - new uint[]{4034}, - new uint[]{4035}, - new uint[]{4107}, - new uint[]{4110}, - new uint[]{4111}, - new uint[]{4103}, - new uint[]{4104}, - new uint[]{4105}, - new uint[]{4106}, - new uint[]{3729}, - new uint[]{3730}, - new uint[]{3562}, - new uint[]{1273, 1275, 5660, 6159, 6399, 6402, 6403, 6533, 6544, 6560}, - new uint[]{1273, 6159, 6402, 6405, 6533, 6544, 8475, 8478}, - new uint[]{1275, 6398, 6399, 6402, 6404, 6535, 6560, 8478}, - new uint[]{1273, 1275, 6159, 6399, 6403, 6533, 6544}, - new uint[]{3995, 6159, 6399, 6405, 6533, 6535, 6560, 8477}, - new uint[]{108, 1275, 3889, 4005, 5660, 5797, 6156, 6406, 6478, 6479, 6534, 6541, 6547, 6556, 6654, 8675, 8676}, - new uint[]{3991, 4254, 6150, 6409, 6410, 6452, 6484, 6486, 6502, 6520, 8470, 8472}, - new uint[]{3976, 4410, 5660, 5791, 6156, 6399, 6412, 6481, 6484, 6520, 6537, 6543, 6559, 6655, 8470, 8471, 8473, 8479, 8480, 8481, 8675, 8676}, - new uint[]{5660, 6411}, - new uint[]{3891, 5660, 5791, 6398, 6399, 6417, 6421, 6520, 6557, 6558, 6653, 8470, 8471, 8473, 8479, 8480, 8482, 8483}, - new uint[]{4402}, - new uint[]{108, 3452}, - new uint[]{3656}, - new uint[]{3657}, - new uint[]{3656}, - new uint[]{3657}, - new uint[]{2085}, - new uint[]{2086}, - new uint[]{4269}, - new uint[]{4270}, - new uint[]{4271}, - new uint[]{4272}, - new uint[]{2086}, - new uint[]{3763}, - new uint[]{3762}, - new uint[]{3764}, - new uint[]{4193}, - new uint[]{4192}, - new uint[]{4204}, - new uint[]{4209}, - new uint[]{4212}, - new uint[]{4223}, - new uint[]{4224}, - new uint[]{3339}, - new uint[]{4225}, - new uint[]{4220}, - new uint[]{4149}, - new uint[]{4392}, - new uint[]{108, 9374, 9379, 9380, 9381, 9382, 9383, 9386, 9387, 9388, 9543, 10187}, - new uint[]{3745}, - new uint[]{3746}, - new uint[]{3747}, - new uint[]{3748}, - new uint[]{3749}, - new uint[]{3747, 3748, 3750}, - new uint[]{3493}, - new uint[]{3761}, - new uint[]{2667}, - Array.Empty(), - new uint[]{3602}, - new uint[]{3599}, - new uint[]{3600}, - new uint[]{3601}, - new uint[]{729}, - new uint[]{4130}, - new uint[]{3482, 4408}, - new uint[]{3569, 3694}, - new uint[]{3695}, - new uint[]{3570}, - new uint[]{657, 3587, 3631, 4044}, - new uint[]{3630}, - new uint[]{3616, 4043}, - new uint[]{3894}, - new uint[]{3178}, - new uint[]{3177}, - Array.Empty(), - Array.Empty(), - new uint[]{4113}, - new uint[]{4113}, - new uint[]{4085}, - new uint[]{3907}, - new uint[]{4287}, - new uint[]{3906}, - new uint[]{4089}, - new uint[]{3903, 4078}, - new uint[]{4057, 4062, 4334}, - new uint[]{4280}, - new uint[]{4087}, - new uint[]{3904, 4076}, - new uint[]{3904}, - new uint[]{3617}, - new uint[]{3905}, - new uint[]{3905}, - new uint[]{4047}, - new uint[]{4073, 4292}, - new uint[]{3623}, - new uint[]{3623}, - new uint[]{3623}, - new uint[]{4302}, - new uint[]{4279, 4297}, - new uint[]{4281}, - new uint[]{4058, 4063}, - new uint[]{4304}, - new uint[]{4303}, - new uint[]{4072}, - new uint[]{4086}, - new uint[]{4295}, - new uint[]{4298}, - new uint[]{4299}, - new uint[]{4236}, - new uint[]{4237}, - new uint[]{4238}, - new uint[]{4239}, - new uint[]{4324}, - new uint[]{4325}, - new uint[]{4326}, - new uint[]{4327}, - new uint[]{4328}, - new uint[]{4329}, - new uint[]{4330}, - new uint[]{4331}, - new uint[]{1808}, - new uint[]{4233}, - new uint[]{4234}, - new uint[]{3508}, - new uint[]{3558}, - new uint[]{3751, 3758}, - new uint[]{3469}, - new uint[]{4350}, - new uint[]{4351}, - new uint[]{4352}, - new uint[]{4353}, - new uint[]{4354}, - new uint[]{4355}, - new uint[]{4356}, - new uint[]{4357}, - new uint[]{4358}, - new uint[]{4359}, - new uint[]{4360}, - new uint[]{4361}, - new uint[]{4362}, - new uint[]{4363}, - new uint[]{4364}, - new uint[]{4365}, - new uint[]{4366}, - new uint[]{4367}, - new uint[]{4368}, - new uint[]{4369}, - new uint[]{4370}, - new uint[]{4371}, - new uint[]{4372}, - new uint[]{4373}, - new uint[]{4374}, - new uint[]{4375}, - new uint[]{4376}, - new uint[]{4377}, - new uint[]{4378}, - new uint[]{4380}, - new uint[]{729}, - new uint[]{4231}, - new uint[]{1893}, - new uint[]{1895}, - new uint[]{4232}, - new uint[]{2145}, - new uint[]{4099}, - new uint[]{1416}, - new uint[]{4226}, - new uint[]{4227}, - new uint[]{2204}, - new uint[]{4235}, - new uint[]{4236}, - new uint[]{4237}, - new uint[]{108}, - new uint[]{4316}, - new uint[]{4317}, - new uint[]{4318}, - new uint[]{4319}, - Array.Empty(), - new uint[]{3164}, - new uint[]{4241}, - new uint[]{4243}, - new uint[]{3171}, - new uint[]{3170}, - new uint[]{4493}, - new uint[]{3860}, - new uint[]{3861}, - new uint[]{4247}, - new uint[]{4247}, - new uint[]{4248}, - new uint[]{3863}, - new uint[]{4254}, - new uint[]{3860}, - new uint[]{3861}, - new uint[]{4253}, - new uint[]{4253}, - new uint[]{4253}, - new uint[]{3858}, - new uint[]{4255}, - new uint[]{3860}, - new uint[]{3861}, - new uint[]{3870}, - new uint[]{4249}, - new uint[]{4250}, - new uint[]{4251, 4421}, - new uint[]{794}, - new uint[]{1849}, - new uint[]{108}, - new uint[]{4491}, - new uint[]{4250}, - new uint[]{4251}, - new uint[]{3660}, - new uint[]{3660}, - new uint[]{4251}, - Array.Empty(), - Array.Empty(), - new uint[]{4029}, - new uint[]{4093}, - new uint[]{4090}, - new uint[]{4040, 4335}, - new uint[]{4041}, - new uint[]{1240, 1893, 3186, 4091, 4092, 4116, 4123}, - new uint[]{4038}, - new uint[]{4056}, - Array.Empty(), - new uint[]{4029}, - new uint[]{4036}, - new uint[]{4037}, - new uint[]{4039}, - new uint[]{4095}, - new uint[]{108}, - new uint[]{3725}, - new uint[]{3553}, - new uint[]{3628, 3669}, - Array.Empty(), - new uint[]{3164}, - new uint[]{4251}, - new uint[]{4242}, - new uint[]{4243}, - new uint[]{4244}, - new uint[]{4091}, - new uint[]{4245}, - new uint[]{4246}, - new uint[]{3171}, - new uint[]{3170}, - new uint[]{4256}, - new uint[]{4257}, - new uint[]{2077}, - new uint[]{2080}, - new uint[]{2076}, - new uint[]{4258}, - new uint[]{4259}, - new uint[]{4228}, - new uint[]{2204}, - new uint[]{4229}, - new uint[]{4230}, - new uint[]{1416}, - new uint[]{4226}, - new uint[]{4227}, - new uint[]{4379}, - new uint[]{4193}, - new uint[]{4204}, - new uint[]{4209}, - new uint[]{4210}, - new uint[]{4211}, - new uint[]{4212}, - new uint[]{4212}, - new uint[]{3888}, - new uint[]{1990}, - new uint[]{4203}, - new uint[]{3560, 3618, 3697}, - new uint[]{3518, 3546, 4018}, - new uint[]{3519, 3547}, - new uint[]{3520, 3548}, - new uint[]{3521, 3549}, - new uint[]{3522, 3550}, - new uint[]{4228}, - new uint[]{4312}, - new uint[]{4118, 4582}, - new uint[]{4081}, - new uint[]{4394}, - new uint[]{4395}, - new uint[]{4075}, - new uint[]{4077}, - new uint[]{4077}, - new uint[]{4080}, - new uint[]{4074}, - new uint[]{4311}, - new uint[]{4096}, - new uint[]{4098}, - new uint[]{4099}, - new uint[]{4081}, - new uint[]{4082}, - new uint[]{4083}, - new uint[]{4084}, - new uint[]{4119}, - new uint[]{4120}, - new uint[]{4121}, - new uint[]{787}, - new uint[]{2086}, - new uint[]{4218}, - new uint[]{4214}, - new uint[]{4213}, - new uint[]{4216}, - new uint[]{4219}, - new uint[]{4215}, - new uint[]{4217}, - new uint[]{3860}, - new uint[]{3861}, - new uint[]{3870}, - new uint[]{3871}, - new uint[]{3872}, - new uint[]{3873}, - new uint[]{3874}, - new uint[]{3875}, - new uint[]{3876}, - new uint[]{3877}, - new uint[]{3878}, - new uint[]{3879}, - new uint[]{3880}, - new uint[]{1724}, - new uint[]{4260}, - new uint[]{4261}, - new uint[]{4262}, - new uint[]{4263, 4264}, - new uint[]{4259}, - new uint[]{108}, - new uint[]{4265}, - new uint[]{4266}, - new uint[]{4267}, - new uint[]{4259}, - new uint[]{4322}, - new uint[]{3318}, - new uint[]{3317}, - new uint[]{4323}, - new uint[]{4321}, - new uint[]{108}, - new uint[]{4322}, - new uint[]{4322}, - new uint[]{4322}, - new uint[]{4322}, - new uint[]{4333}, - new uint[]{4333}, - new uint[]{3319}, - new uint[]{3319}, - new uint[]{3317}, - new uint[]{3317}, - new uint[]{4323}, - new uint[]{4332}, - new uint[]{4331}, - new uint[]{4321}, - new uint[]{3888}, - new uint[]{3773}, - new uint[]{3958, 3959, 4407}, - new uint[]{3960}, - Array.Empty(), - Array.Empty(), - new uint[]{3957}, - new uint[]{3956}, - new uint[]{3955}, - new uint[]{4412}, - new uint[]{3949, 3954}, - new uint[]{3953}, - Array.Empty(), - new uint[]{4417}, - new uint[]{3950}, - new uint[]{3948}, - new uint[]{3951, 3952}, - new uint[]{3861}, - new uint[]{3860}, - new uint[]{3864}, - new uint[]{4425}, - new uint[]{1400}, - new uint[]{3865}, - new uint[]{3129}, - new uint[]{3866}, - new uint[]{3867}, - new uint[]{3868}, - new uint[]{3869}, - new uint[]{4313}, - new uint[]{4313}, - new uint[]{4300}, - new uint[]{5533}, - new uint[]{4314}, - new uint[]{4055}, - new uint[]{4066}, - new uint[]{4065}, - new uint[]{4051}, - new uint[]{4315}, - new uint[]{4124}, - new uint[]{1402}, - new uint[]{3887}, - new uint[]{3892}, - new uint[]{3888, 3889, 3890}, - new uint[]{3891}, - new uint[]{3965}, - new uint[]{3964}, - new uint[]{3983}, - new uint[]{3962}, - new uint[]{3946}, - new uint[]{3783}, - Array.Empty(), - Array.Empty(), - new uint[]{114}, - new uint[]{3474}, - new uint[]{3471, 3684}, - new uint[]{3475, 3488}, - new uint[]{3478, 3685}, - new uint[]{3470}, - new uint[]{3476}, - new uint[]{3477, 3680}, - new uint[]{3472}, - new uint[]{3473, 3682}, - new uint[]{3499, 4406}, - new uint[]{3495, 4011}, - new uint[]{3494}, - new uint[]{3497}, - new uint[]{3496}, - new uint[]{3498}, - new uint[]{56}, - Array.Empty(), - new uint[]{3937, 4012, 4416}, - new uint[]{3944}, - new uint[]{3943}, - Array.Empty(), - new uint[]{3942}, - new uint[]{3941}, - new uint[]{3940}, - new uint[]{3939}, - Array.Empty(), - new uint[]{3938}, - new uint[]{3936}, - new uint[]{3935}, - new uint[]{3934}, - new uint[]{1640}, - new uint[]{3881}, - new uint[]{3882}, - new uint[]{3883}, - new uint[]{3884}, - new uint[]{3885}, - new uint[]{3886}, - new uint[]{4192}, - new uint[]{3477}, - new uint[]{3474}, - new uint[]{3952}, - new uint[]{3659}, - Array.Empty(), - new uint[]{3781}, - new uint[]{3410}, - new uint[]{3410}, - Array.Empty(), - Array.Empty(), - new uint[]{3982}, - new uint[]{3981}, - new uint[]{3980}, - new uint[]{3984}, - new uint[]{3984}, - new uint[]{3979}, - new uint[]{3978}, - new uint[]{3975}, - new uint[]{3974}, - Array.Empty(), - new uint[]{3973}, - new uint[]{3972}, - new uint[]{3971}, - new uint[]{3970}, - new uint[]{3969}, - new uint[]{3968}, - new uint[]{3967}, - new uint[]{108}, - new uint[]{4419}, - new uint[]{3968}, - new uint[]{538}, - new uint[]{4009}, - new uint[]{4008}, - new uint[]{4006}, - Array.Empty(), - new uint[]{4002, 4004}, - new uint[]{4003}, - new uint[]{4001, 4418}, - new uint[]{4000}, - new uint[]{3999}, - new uint[]{3998}, - new uint[]{3997}, - new uint[]{4405}, - new uint[]{4404}, - new uint[]{4403}, - new uint[]{3996}, - new uint[]{3564}, - new uint[]{3993}, - new uint[]{3992}, - new uint[]{3990}, - new uint[]{3988}, - new uint[]{3988}, - new uint[]{3987}, - new uint[]{3986}, - new uint[]{3985}, - new uint[]{1640}, - new uint[]{3881}, - new uint[]{3882}, - new uint[]{1402}, - new uint[]{1403}, - new uint[]{1404}, - new uint[]{1402}, - new uint[]{1403}, - new uint[]{1404}, - new uint[]{3882}, - new uint[]{3789}, - new uint[]{3780}, - new uint[]{108}, - new uint[]{3780}, - new uint[]{3765}, - new uint[]{3766}, - new uint[]{3770}, - new uint[]{3769}, - new uint[]{3768}, - new uint[]{3765}, - new uint[]{108}, - new uint[]{108}, - new uint[]{3660}, - new uint[]{3660}, - new uint[]{3753}, - new uint[]{3752}, - new uint[]{3758}, - new uint[]{115}, - Array.Empty(), - Array.Empty(), - new uint[]{3716}, - new uint[]{3717}, - new uint[]{3718}, - new uint[]{3719}, - new uint[]{3720}, - new uint[]{3679}, - new uint[]{3679}, - new uint[]{3686}, - new uint[]{3686}, - new uint[]{3689}, - new uint[]{3687}, - new uint[]{3692}, - new uint[]{3692}, - new uint[]{3701}, - new uint[]{3709}, - new uint[]{2838}, - new uint[]{3773}, - new uint[]{3853}, - new uint[]{3853}, - new uint[]{4341}, - new uint[]{4398}, - new uint[]{4428}, - new uint[]{4429}, - new uint[]{4430}, - new uint[]{4431}, - new uint[]{4432}, - new uint[]{4433}, - new uint[]{4434}, - new uint[]{4435}, - new uint[]{4436}, - new uint[]{4437}, - new uint[]{4438}, - new uint[]{4439}, - new uint[]{4440}, - new uint[]{4441}, - new uint[]{4442}, - new uint[]{4443}, - new uint[]{4444}, - new uint[]{4445}, - new uint[]{4446}, - new uint[]{4447}, - new uint[]{4448}, - new uint[]{4449}, - new uint[]{4450}, - new uint[]{4451}, - new uint[]{4452}, - new uint[]{4453}, - new uint[]{4454}, - new uint[]{4455}, - new uint[]{4456}, - new uint[]{4457}, - new uint[]{4458}, - new uint[]{4459}, - new uint[]{4460}, - new uint[]{4461}, - new uint[]{4462}, - new uint[]{4463}, - new uint[]{4464}, - new uint[]{4465}, - new uint[]{4466}, - new uint[]{4467}, - new uint[]{4468}, - new uint[]{4469}, - new uint[]{4470}, - new uint[]{4471}, - new uint[]{4472}, - new uint[]{4473}, - new uint[]{4474}, - new uint[]{4475}, - new uint[]{4476}, - new uint[]{4477}, - new uint[]{4478}, - new uint[]{4479}, - new uint[]{4480}, - new uint[]{4481}, - new uint[]{4482}, - new uint[]{4483}, - new uint[]{4484}, - new uint[]{4485}, - new uint[]{4486}, - new uint[]{4487}, - new uint[]{4488}, - new uint[]{4897}, - new uint[]{5046}, - new uint[]{5047}, - new uint[]{4026}, - new uint[]{4025}, - new uint[]{4024}, - new uint[]{4023}, - new uint[]{4022}, - new uint[]{4021}, - new uint[]{4020}, - new uint[]{3545, 4019}, - new uint[]{4017}, - new uint[]{4016}, - new uint[]{4015}, - new uint[]{4014}, - new uint[]{4013}, - new uint[]{4243}, - new uint[]{4243}, - new uint[]{4243}, - new uint[]{4243}, - new uint[]{4010}, - new uint[]{4397}, - new uint[]{114}, - new uint[]{795}, - new uint[]{3410}, - new uint[]{3410}, - new uint[]{3410}, - new uint[]{4239}, - new uint[]{108}, - new uint[]{108, 3789}, - new uint[]{731}, - new uint[]{3790}, - new uint[]{3180, 4034}, - new uint[]{3181}, - new uint[]{4115}, - new uint[]{1239, 4091, 4092, 4305, 4309, 4310}, - new uint[]{1895, 4422}, - new uint[]{3187, 4123}, - new uint[]{4123, 4310}, - new uint[]{1239, 4067, 4850}, - new uint[]{4307, 4422}, - new uint[]{1240, 1893, 4306}, - new uint[]{4079, 4088, 4097, 4308}, - Array.Empty(), - new uint[]{4067, 4851}, - new uint[]{4282}, - new uint[]{4027}, - new uint[]{4028}, - new uint[]{3738}, - new uint[]{3948, 4415}, - Array.Empty(), - new uint[]{3444}, - new uint[]{3654, 3655}, - new uint[]{4414}, - new uint[]{4413}, - Array.Empty(), - new uint[]{3784}, - Array.Empty(), - new uint[]{4411}, - new uint[]{4007}, - Array.Empty(), - Array.Empty(), - new uint[]{3676}, - new uint[]{3677}, - new uint[]{3678}, - new uint[]{3681}, - new uint[]{3683}, - new uint[]{3660}, - new uint[]{3660}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2901}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3983}, - new uint[]{116}, - new uint[]{113}, - new uint[]{114}, - new uint[]{115, 3961}, - new uint[]{3480, 3491, 4409}, - new uint[]{4624}, - new uint[]{4624}, - new uint[]{4623}, - new uint[]{4625}, - new uint[]{4631}, - new uint[]{4632}, - new uint[]{4633}, - new uint[]{4635}, - new uint[]{4613}, - new uint[]{4609}, - new uint[]{4610}, - new uint[]{4612}, - new uint[]{4613}, - Array.Empty(), - new uint[]{4607}, - new uint[]{4608}, - new uint[]{4609}, - new uint[]{4611}, - new uint[]{108}, - new uint[]{4551}, - new uint[]{4552}, - new uint[]{4553}, - new uint[]{4554}, - new uint[]{4568}, - new uint[]{4555}, - new uint[]{4556}, - new uint[]{4557}, - new uint[]{4558}, - new uint[]{4559}, - new uint[]{4560}, - new uint[]{4561}, - new uint[]{4562}, - new uint[]{4563}, - new uint[]{4564}, - new uint[]{4565}, - new uint[]{4566}, - new uint[]{4626}, - new uint[]{4627}, - new uint[]{4630}, - new uint[]{4628}, - new uint[]{4629}, - new uint[]{4626}, - new uint[]{2564}, - new uint[]{4622}, - new uint[]{4620}, - new uint[]{4621}, - Array.Empty(), - new uint[]{4636}, - new uint[]{4637}, - new uint[]{4638}, - new uint[]{4639}, - new uint[]{4640}, - new uint[]{4641}, - new uint[]{4642}, - new uint[]{4643}, - new uint[]{4644}, - new uint[]{4645}, - new uint[]{4646}, - new uint[]{4646}, - new uint[]{4647}, - new uint[]{4648}, - new uint[]{4649}, - new uint[]{4650}, - new uint[]{4651}, - new uint[]{4653}, - new uint[]{4654}, - new uint[]{4655}, - new uint[]{4656}, - new uint[]{4657}, - new uint[]{4658}, - new uint[]{4659}, - new uint[]{4660}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4568}, - new uint[]{4568}, - new uint[]{4567}, - new uint[]{4568}, - new uint[]{4568}, - new uint[]{4569}, - new uint[]{4570}, - new uint[]{4571}, - Array.Empty(), - new uint[]{4572}, - new uint[]{4573}, - new uint[]{4574}, - new uint[]{4570}, - new uint[]{4575}, - new uint[]{4576}, - new uint[]{4577}, - new uint[]{4578}, - new uint[]{4579}, - new uint[]{4580}, - new uint[]{108}, - new uint[]{4614}, - new uint[]{4616}, - new uint[]{4619}, - new uint[]{4617}, - new uint[]{4618}, - new uint[]{3632}, - new uint[]{3633}, - new uint[]{3634}, - new uint[]{3635}, - new uint[]{3636}, - new uint[]{3637}, - new uint[]{3638}, - new uint[]{3639}, - new uint[]{3640}, - new uint[]{3641}, - new uint[]{3642}, - new uint[]{3643}, - new uint[]{3644}, - new uint[]{3645}, - new uint[]{4386}, - new uint[]{4387}, - new uint[]{3632, 3639, 3640, 3641, 3642, 3644, 3645}, - new uint[]{4634}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4652}, - new uint[]{108, 6291, 6292, 6431, 6432, 6439, 6442, 6444, 6448, 6451, 6476, 6494, 6504, 6508, 6510, 6512, 6526, 6527, 6547, 8479, 9401, 10808, 10809, 10829}, - new uint[]{4334, 4583}, - new uint[]{4335, 4584}, - new uint[]{4585}, - new uint[]{4587}, - new uint[]{4586}, - new uint[]{4603}, - new uint[]{4588}, - new uint[]{4589}, - new uint[]{4590}, - new uint[]{4591}, - new uint[]{4592, 4682}, - new uint[]{4593}, - new uint[]{4594}, - new uint[]{4562}, - new uint[]{4581}, - new uint[]{4563}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4666}, - new uint[]{4671}, - new uint[]{4672}, - new uint[]{4673}, - new uint[]{4597}, - new uint[]{3841}, - new uint[]{3841}, - new uint[]{4401}, - new uint[]{3843}, - new uint[]{4401}, - new uint[]{3842}, - new uint[]{3842}, - new uint[]{3843}, - new uint[]{3843}, - new uint[]{4598}, - new uint[]{4599}, - new uint[]{4130}, - new uint[]{729}, - new uint[]{713}, - new uint[]{4595}, - new uint[]{4596}, - new uint[]{4670}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4600}, - new uint[]{4602}, - new uint[]{4602}, - new uint[]{4600}, - new uint[]{4601}, - new uint[]{4600}, - Array.Empty(), - new uint[]{4653}, - new uint[]{4654}, - new uint[]{3842}, - new uint[]{108}, - new uint[]{4578}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4744}, - new uint[]{4839}, - new uint[]{4745}, - new uint[]{4746}, - new uint[]{4747}, - new uint[]{4748}, - new uint[]{4749}, - new uint[]{4750}, - new uint[]{4751}, - new uint[]{4752}, - new uint[]{4753}, - new uint[]{4754}, - new uint[]{4755}, - new uint[]{4756}, - new uint[]{4757}, - new uint[]{4758}, - new uint[]{4759}, - new uint[]{4760}, - new uint[]{4761}, - new uint[]{4776}, - new uint[]{4776}, - new uint[]{4777}, - new uint[]{4778}, - new uint[]{4779}, - new uint[]{4780}, - new uint[]{4776}, - new uint[]{4687}, - new uint[]{3646}, - new uint[]{4791}, - new uint[]{4791}, - new uint[]{4688}, - Array.Empty(), - new uint[]{4690}, - new uint[]{4762}, - new uint[]{4794}, - new uint[]{4798}, - new uint[]{4799}, - new uint[]{4800}, - new uint[]{4795}, - new uint[]{4801}, - new uint[]{736}, - new uint[]{4802}, - new uint[]{4803}, - new uint[]{4804}, - new uint[]{4796}, - new uint[]{4805}, - new uint[]{4806}, - new uint[]{4807}, - new uint[]{4808}, - new uint[]{4810}, - new uint[]{4809}, - new uint[]{4813}, - new uint[]{4811}, - new uint[]{4812}, - new uint[]{3376}, - new uint[]{4692}, - new uint[]{4775}, - new uint[]{4694}, - new uint[]{3749}, - new uint[]{3774}, - new uint[]{3746}, - new uint[]{4698}, - new uint[]{4695}, - new uint[]{3745}, - new uint[]{4709}, - new uint[]{4838}, - new uint[]{4699}, - new uint[]{4700}, - new uint[]{4700}, - new uint[]{4700}, - new uint[]{4773}, - new uint[]{4703}, - new uint[]{4704}, - new uint[]{4818}, - new uint[]{4817}, - new uint[]{4816}, - new uint[]{4705}, - new uint[]{4769}, - new uint[]{4770}, - new uint[]{4772}, - new uint[]{4691}, - new uint[]{4771}, - new uint[]{4706}, - new uint[]{4768}, - new uint[]{4820}, - new uint[]{4699, 4703, 4704, 4705, 4706, 4764}, - new uint[]{4707}, - new uint[]{3778}, - new uint[]{3776}, - new uint[]{3777}, - new uint[]{4699}, - new uint[]{4700}, - new uint[]{4706}, - new uint[]{4705}, - new uint[]{4703}, - new uint[]{4704}, - new uint[]{4817}, - new uint[]{4816}, - new uint[]{4708}, - Array.Empty(), - new uint[]{4820}, - new uint[]{4773}, - new uint[]{4819}, - new uint[]{4703, 4705, 4706, 4707, 4708, 4764}, - new uint[]{4813}, - new uint[]{4730}, - new uint[]{4729}, - new uint[]{4733}, - new uint[]{4734}, - new uint[]{4732}, - new uint[]{4732}, - new uint[]{4729}, - new uint[]{4733}, - new uint[]{4731}, - new uint[]{4728}, - new uint[]{4727}, - new uint[]{4725, 4726}, - new uint[]{4725}, - new uint[]{4728}, - new uint[]{4725}, - new uint[]{108}, - new uint[]{108}, - new uint[]{4735}, - new uint[]{4736}, - new uint[]{4737}, - new uint[]{4738}, - new uint[]{4739}, - new uint[]{4739}, - new uint[]{4740}, - new uint[]{4741}, - new uint[]{4742}, - new uint[]{4743}, - new uint[]{541}, - new uint[]{4765}, - new uint[]{4766}, - new uint[]{4767}, - new uint[]{4784}, - new uint[]{4785}, - new uint[]{4957}, - new uint[]{4956}, - new uint[]{4955}, - new uint[]{4952}, - new uint[]{4953}, - new uint[]{5043}, - new uint[]{4815}, - new uint[]{4896}, - new uint[]{4954}, - new uint[]{2667}, - new uint[]{4782}, - new uint[]{541}, - new uint[]{4784}, - new uint[]{4782}, - new uint[]{4814}, - new uint[]{4784}, - new uint[]{4785}, - new uint[]{4786}, - new uint[]{4784}, - new uint[]{4786}, - new uint[]{4782}, - new uint[]{4784}, - new uint[]{4781}, - new uint[]{4782}, - new uint[]{4784}, - new uint[]{4785}, - new uint[]{4786}, - new uint[]{4781}, - new uint[]{4782}, - new uint[]{4784}, - new uint[]{4785}, - new uint[]{4781}, - new uint[]{4782}, - new uint[]{4784}, - Array.Empty(), - new uint[]{4786}, - new uint[]{4781}, - new uint[]{4782}, - new uint[]{4784}, - Array.Empty(), - new uint[]{4814}, - new uint[]{4781}, - new uint[]{4782}, - new uint[]{4784}, - new uint[]{4781}, - new uint[]{4784}, - new uint[]{4786}, - new uint[]{4781}, - new uint[]{4783}, - new uint[]{4784}, - new uint[]{4785}, - new uint[]{4781}, - new uint[]{4783}, - new uint[]{4781}, - new uint[]{4783}, - new uint[]{4782}, - new uint[]{4787}, - new uint[]{4788}, - new uint[]{4789}, - Array.Empty(), - new uint[]{2667}, - new uint[]{4687}, - new uint[]{3646}, - new uint[]{4688}, - Array.Empty(), - new uint[]{4690}, - new uint[]{4797}, - new uint[]{4701}, - new uint[]{4702}, - new uint[]{4697}, - Array.Empty(), - new uint[]{4710}, - new uint[]{4711}, - new uint[]{4712}, - new uint[]{4712}, - new uint[]{4713}, - new uint[]{4714}, - new uint[]{4715}, - new uint[]{4716}, - new uint[]{4717}, - new uint[]{4718}, - new uint[]{4719}, - new uint[]{4721}, - new uint[]{4720}, - new uint[]{4724}, - new uint[]{4713}, - new uint[]{4723}, - new uint[]{4722}, - new uint[]{3745}, - new uint[]{4709}, - new uint[]{4776}, - new uint[]{4776}, - new uint[]{4777}, - new uint[]{4778}, - new uint[]{4779}, - new uint[]{4780}, - new uint[]{4776}, - new uint[]{4782}, - Array.Empty(), - new uint[]{3376}, - new uint[]{4692}, - new uint[]{4775}, - new uint[]{4694}, - new uint[]{3749}, - new uint[]{3774}, - new uint[]{3746}, - new uint[]{4698}, - new uint[]{4695}, - new uint[]{2667}, - new uint[]{4691}, - new uint[]{4693}, - new uint[]{3758}, - new uint[]{4772}, - new uint[]{4774}, - new uint[]{4699}, - new uint[]{4700}, - new uint[]{4773}, - new uint[]{4703}, - new uint[]{4705}, - new uint[]{4706}, - new uint[]{4768}, - new uint[]{4820}, - new uint[]{4699, 4703, 4705, 4706, 4764}, - new uint[]{4707}, - new uint[]{3778}, - new uint[]{3776}, - new uint[]{3777}, - new uint[]{4699}, - new uint[]{4700}, - new uint[]{4706}, - new uint[]{4705}, - new uint[]{4703}, - new uint[]{4708}, - new uint[]{4773}, - new uint[]{4705, 4706, 4707, 4708, 4764}, - new uint[]{4811}, - new uint[]{4812}, - new uint[]{4811}, - new uint[]{4812}, - new uint[]{4787}, - new uint[]{4788}, - new uint[]{4821}, - new uint[]{4822, 4823, 4824, 4825}, - new uint[]{4826, 4827, 4828, 4829, 4830, 4831, 4832, 4833, 4834, 4835, 4836, 4837, 4840, 4841, 4842, 4843, 4844, 4845}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3458}, - new uint[]{3458}, - Array.Empty(), - new uint[]{4963}, - new uint[]{4962}, - new uint[]{3460}, - new uint[]{3459}, - new uint[]{4960}, - new uint[]{4961}, - new uint[]{4959}, - new uint[]{3458, 3459, 4962, 4963}, - new uint[]{4943}, - new uint[]{4944}, - new uint[]{4945}, - new uint[]{4946}, - new uint[]{4947}, - new uint[]{4948}, - new uint[]{4949}, - new uint[]{4950}, - new uint[]{4951}, - new uint[]{4928}, - new uint[]{4929}, - new uint[]{4932}, - new uint[]{4933}, - new uint[]{4931}, - new uint[]{4930}, - new uint[]{4936}, - new uint[]{4935}, - new uint[]{4937}, - new uint[]{4934}, - new uint[]{4939}, - new uint[]{4938}, - new uint[]{4940}, - new uint[]{4942}, - new uint[]{4954}, - new uint[]{4941}, - new uint[]{4855}, - new uint[]{4856}, - new uint[]{4857}, - new uint[]{4858}, - new uint[]{4859}, - new uint[]{4860}, - new uint[]{4861}, - new uint[]{4854}, - new uint[]{4853}, - new uint[]{4852}, - new uint[]{3818}, - new uint[]{3819}, - new uint[]{3820}, - new uint[]{4489}, - new uint[]{5045}, - new uint[]{1492}, - new uint[]{729}, - new uint[]{4846}, - new uint[]{4847}, - new uint[]{4878}, - new uint[]{4879}, - new uint[]{4880}, - new uint[]{4881}, - new uint[]{4882}, - Array.Empty(), - new uint[]{4884}, - new uint[]{4885}, - new uint[]{4886}, - new uint[]{4887}, - new uint[]{4878}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{4942}, - new uint[]{4871}, - new uint[]{4848}, - new uint[]{4907}, - new uint[]{5589}, - new uint[]{4908}, - Array.Empty(), - Array.Empty(), - new uint[]{4909}, - new uint[]{5048}, - new uint[]{5049}, - new uint[]{4910}, - new uint[]{4849}, - new uint[]{4911}, - new uint[]{4911}, - new uint[]{4912}, - new uint[]{4913}, - new uint[]{4914}, - new uint[]{4915}, - new uint[]{4916}, - new uint[]{4888}, - new uint[]{4889}, - new uint[]{4890}, - new uint[]{4891}, - new uint[]{4892}, - new uint[]{4893}, - new uint[]{4894}, - new uint[]{4958}, - new uint[]{2095}, - new uint[]{4895}, - new uint[]{5050}, - new uint[]{5051}, - new uint[]{108, 4965, 5055}, - new uint[]{5052}, - new uint[]{5053}, - new uint[]{4966}, - new uint[]{4967}, - new uint[]{4968}, - new uint[]{4969}, - new uint[]{4970}, - new uint[]{4971}, - new uint[]{4972}, - new uint[]{5044}, - new uint[]{4897}, - new uint[]{4898}, - new uint[]{4899}, - new uint[]{4900}, - new uint[]{4901}, - new uint[]{4902}, - new uint[]{4903}, - new uint[]{4904}, - new uint[]{4862}, - new uint[]{4863}, - new uint[]{4864}, - new uint[]{4865}, - new uint[]{4866}, - new uint[]{4867}, - new uint[]{4868}, - new uint[]{4869}, - new uint[]{4870}, - new uint[]{4872}, - new uint[]{4873}, - new uint[]{4874}, - new uint[]{4875}, - new uint[]{4877}, - new uint[]{4871}, - new uint[]{5056}, - new uint[]{4911}, - new uint[]{5057}, - new uint[]{4876}, - new uint[]{4973}, - new uint[]{4974}, - new uint[]{4905}, - new uint[]{4905}, - new uint[]{4905}, - new uint[]{4906}, - new uint[]{4901}, - new uint[]{108}, - new uint[]{5054}, - Array.Empty(), - new uint[]{5479}, - new uint[]{4975}, - new uint[]{4976}, - new uint[]{4977}, - new uint[]{4978}, - new uint[]{4979}, - new uint[]{4980}, - new uint[]{4981}, - new uint[]{4982}, - new uint[]{4983}, - new uint[]{4984}, - new uint[]{4985}, - new uint[]{4986}, - new uint[]{4987}, - new uint[]{4988}, - new uint[]{4989}, - new uint[]{4990}, - new uint[]{4991}, - new uint[]{4992}, - new uint[]{4993}, - new uint[]{4994}, - new uint[]{4995}, - new uint[]{4996}, - new uint[]{4997}, - new uint[]{4998}, - new uint[]{4999}, - new uint[]{5000}, - new uint[]{5001}, - new uint[]{5002}, - new uint[]{5003}, - new uint[]{5004}, - new uint[]{5005}, - new uint[]{5006}, - new uint[]{5007}, - new uint[]{5008}, - new uint[]{5009}, - new uint[]{5010}, - new uint[]{5011}, - new uint[]{5012}, - new uint[]{5013}, - new uint[]{5014}, - new uint[]{5015}, - new uint[]{5016}, - new uint[]{5017}, - new uint[]{5018}, - new uint[]{5019}, - new uint[]{5020}, - new uint[]{5021}, - new uint[]{5022}, - new uint[]{5023}, - new uint[]{5024}, - new uint[]{5025}, - new uint[]{5026}, - new uint[]{5027}, - new uint[]{5028}, - new uint[]{5029}, - new uint[]{5030}, - new uint[]{5031}, - new uint[]{5032}, - new uint[]{5033}, - new uint[]{5034}, - new uint[]{5035}, - new uint[]{5036}, - new uint[]{5037}, - new uint[]{5038}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - Array.Empty(), - new uint[]{5041}, - new uint[]{5041}, - new uint[]{5041}, - new uint[]{5041}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3458}, - new uint[]{3458}, - Array.Empty(), - Array.Empty(), - new uint[]{3459}, - new uint[]{4960}, - new uint[]{4961}, - new uint[]{4959}, - new uint[]{3458, 3459}, - new uint[]{5039}, - new uint[]{5040}, - new uint[]{5058}, - new uint[]{5059}, - new uint[]{5060}, - new uint[]{5060}, - new uint[]{5060}, - new uint[]{5060}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5062}, - new uint[]{5063}, - new uint[]{5064}, - new uint[]{5065}, - new uint[]{5066}, - new uint[]{5066}, - new uint[]{5066}, - new uint[]{5066}, - new uint[]{5067}, - new uint[]{5068}, - new uint[]{5069}, - new uint[]{5070}, - Array.Empty(), - new uint[]{5072}, - new uint[]{5073}, - new uint[]{5074}, - new uint[]{5075}, - new uint[]{5076}, - new uint[]{5077}, - new uint[]{5078}, - new uint[]{5079}, - new uint[]{5080}, - new uint[]{5081}, - new uint[]{5082}, - new uint[]{5083}, - new uint[]{5084}, - new uint[]{5085}, - new uint[]{5086}, - new uint[]{5087}, - new uint[]{5088}, - new uint[]{5089}, - new uint[]{5090}, - new uint[]{5091}, - new uint[]{5092}, - new uint[]{5093}, - new uint[]{5094}, - new uint[]{5095}, - new uint[]{5096}, - new uint[]{5097}, - new uint[]{5098}, - new uint[]{5099}, - new uint[]{5100}, - new uint[]{5101}, - new uint[]{5102}, - new uint[]{5103}, - new uint[]{5104}, - new uint[]{5105}, - new uint[]{5106}, - new uint[]{5107}, - new uint[]{5108}, - new uint[]{5109}, - new uint[]{5110}, - new uint[]{5111}, - new uint[]{5112}, - new uint[]{5113}, - new uint[]{5114}, - new uint[]{5115}, - new uint[]{5116}, - new uint[]{5117}, - new uint[]{5118}, - new uint[]{5119}, - new uint[]{5120}, - new uint[]{5121}, - new uint[]{5122}, - new uint[]{5123}, - new uint[]{5124}, - new uint[]{5125}, - new uint[]{5126}, - new uint[]{5127}, - new uint[]{5128}, - new uint[]{5129}, - new uint[]{5130}, - new uint[]{5131}, - new uint[]{5132}, - new uint[]{5133}, - new uint[]{5134}, - new uint[]{5135}, - new uint[]{5136}, - new uint[]{5137}, - new uint[]{5138}, - new uint[]{5139}, - new uint[]{5140}, - new uint[]{5141}, - new uint[]{5142}, - new uint[]{5143}, - new uint[]{5144}, - new uint[]{5145}, - new uint[]{5146}, - new uint[]{5147}, - new uint[]{5148}, - new uint[]{5149}, - new uint[]{5150}, - new uint[]{5151}, - new uint[]{5152}, - new uint[]{5153}, - new uint[]{5154}, - new uint[]{5155}, - new uint[]{5156}, - new uint[]{5157}, - new uint[]{5158}, - new uint[]{5159}, - new uint[]{5160}, - new uint[]{5161}, - new uint[]{5162}, - new uint[]{5163}, - new uint[]{5164}, - new uint[]{5165}, - new uint[]{5166}, - new uint[]{5167}, - new uint[]{4981}, - new uint[]{5030}, - Array.Empty(), - new uint[]{108}, - new uint[]{5216}, - new uint[]{5278}, - new uint[]{5279}, - new uint[]{5280}, - new uint[]{5218}, - Array.Empty(), - Array.Empty(), - new uint[]{5219}, - new uint[]{5220}, - new uint[]{4895}, - new uint[]{5221}, - new uint[]{5222}, - new uint[]{5223}, - new uint[]{5224}, - new uint[]{5225}, - new uint[]{5226}, - new uint[]{5227}, - new uint[]{5228}, - new uint[]{5229}, - new uint[]{5230}, - new uint[]{5231}, - new uint[]{5232}, - new uint[]{5233}, - new uint[]{5234}, - new uint[]{5235}, - new uint[]{5236}, - new uint[]{5237}, - new uint[]{5238}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5061}, - new uint[]{5168}, - new uint[]{5085}, - new uint[]{5085}, - new uint[]{5085}, - new uint[]{5199}, - new uint[]{5201}, - new uint[]{5200}, - new uint[]{5202}, - new uint[]{5203}, - new uint[]{5204}, - new uint[]{4914}, - new uint[]{5199}, - new uint[]{5201}, - new uint[]{5202}, - new uint[]{5203}, - new uint[]{5204}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5265}, - new uint[]{5266}, - new uint[]{5267}, - new uint[]{5268}, - new uint[]{5269}, - new uint[]{5270}, - Array.Empty(), - new uint[]{5272}, - new uint[]{5259}, - new uint[]{1644}, - new uint[]{5186}, - new uint[]{5186}, - new uint[]{5187}, - new uint[]{5188}, - new uint[]{5189}, - new uint[]{5190}, - new uint[]{5246}, - new uint[]{5247}, - new uint[]{5251}, - new uint[]{5252}, - new uint[]{5253}, - Array.Empty(), - new uint[]{5257}, - new uint[]{5255}, - new uint[]{5256}, - new uint[]{5258}, - new uint[]{5254}, - new uint[]{5260}, - new uint[]{5259}, - new uint[]{5264}, - new uint[]{5262}, - new uint[]{5261}, - new uint[]{5263}, - new uint[]{5193}, - new uint[]{5194}, - new uint[]{5195}, - new uint[]{5196}, - new uint[]{5197}, - new uint[]{5193}, - new uint[]{108}, - new uint[]{5195}, - new uint[]{5196}, - new uint[]{5197}, - new uint[]{5198}, - new uint[]{5174, 5193}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{5239}, - new uint[]{713}, - new uint[]{1492}, - new uint[]{5240}, - new uint[]{5241}, - new uint[]{5244}, - new uint[]{5242}, - new uint[]{5243}, - Array.Empty(), - new uint[]{3306}, - new uint[]{5274}, - new uint[]{3647}, - new uint[]{3648}, - Array.Empty(), - new uint[]{5169}, - new uint[]{5170}, - new uint[]{5171}, - new uint[]{5172}, - new uint[]{5173}, - new uint[]{1385}, - new uint[]{3749}, - new uint[]{5176}, - new uint[]{5277}, - new uint[]{5178}, - new uint[]{5170}, - new uint[]{5180}, - new uint[]{5181}, - new uint[]{5182}, - new uint[]{5183}, - new uint[]{5184}, - new uint[]{5185}, - new uint[]{4698, 5180, 5182, 5209}, - new uint[]{5186}, - new uint[]{5186}, - new uint[]{5187}, - new uint[]{5188}, - new uint[]{5189}, - Array.Empty(), - new uint[]{5192}, - Array.Empty(), - Array.Empty(), - new uint[]{5179}, - new uint[]{5191}, - new uint[]{541}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{5181}, - Array.Empty(), - new uint[]{5207}, - new uint[]{5208}, - new uint[]{4691}, - new uint[]{5206}, - new uint[]{5175}, - new uint[]{3468}, - new uint[]{5205}, - new uint[]{5180}, - new uint[]{5182}, - new uint[]{5185}, - Array.Empty(), - new uint[]{5184}, - new uint[]{4698, 5180, 5182, 5209}, - new uint[]{5356}, - new uint[]{5357}, - new uint[]{5358}, - new uint[]{5359}, - Array.Empty(), - Array.Empty(), - new uint[]{5193}, - new uint[]{5194}, - new uint[]{5195}, - new uint[]{5197}, - new uint[]{5194}, - new uint[]{5174, 5193}, - new uint[]{5309}, - new uint[]{5321}, - new uint[]{5333}, - new uint[]{5345}, - new uint[]{5371}, - new uint[]{5384}, - new uint[]{5397}, - new uint[]{5410}, - new uint[]{5424}, - new uint[]{5438}, - new uint[]{5449}, - new uint[]{5461}, - new uint[]{5471}, - new uint[]{4996}, - new uint[]{5299}, - new uint[]{5300}, - new uint[]{5301}, - new uint[]{5302}, - new uint[]{5303}, - new uint[]{5304}, - new uint[]{5305}, - new uint[]{5306}, - new uint[]{5307}, - new uint[]{5308}, - new uint[]{5311}, - new uint[]{5312}, - new uint[]{5313}, - new uint[]{5004}, - new uint[]{5314}, - new uint[]{5315}, - new uint[]{5316}, - new uint[]{5317}, - new uint[]{5318}, - new uint[]{5319}, - new uint[]{5320}, - new uint[]{5322}, - new uint[]{5323}, - new uint[]{5324}, - new uint[]{5325}, - new uint[]{5326}, - new uint[]{5327}, - new uint[]{5328}, - new uint[]{5329}, - new uint[]{5330}, - new uint[]{5331}, - new uint[]{5332}, - new uint[]{5334}, - new uint[]{5335}, - new uint[]{5336}, - new uint[]{5337}, - new uint[]{5338}, - new uint[]{5339}, - new uint[]{5340}, - new uint[]{5341}, - new uint[]{5342}, - new uint[]{5343}, - new uint[]{5344}, - new uint[]{5346}, - new uint[]{5347}, - new uint[]{5348}, - new uint[]{5349}, - new uint[]{5350}, - new uint[]{5351}, - new uint[]{4979}, - new uint[]{5352}, - new uint[]{5353}, - new uint[]{5354}, - new uint[]{5355}, - new uint[]{5480}, - new uint[]{5360}, - new uint[]{5361}, - new uint[]{5362}, - new uint[]{5363}, - new uint[]{5364}, - new uint[]{5365}, - new uint[]{5366}, - new uint[]{5367}, - new uint[]{5368}, - new uint[]{5369}, - new uint[]{5370}, - new uint[]{5372}, - new uint[]{5373}, - new uint[]{5374}, - new uint[]{5375}, - new uint[]{5376}, - new uint[]{5377}, - new uint[]{5378}, - new uint[]{5379}, - new uint[]{5380}, - new uint[]{5381}, - new uint[]{5382}, - new uint[]{5383}, - new uint[]{5385}, - new uint[]{5386}, - new uint[]{5387}, - new uint[]{5388}, - new uint[]{5389}, - new uint[]{5390}, - new uint[]{5391}, - new uint[]{5392}, - new uint[]{5393}, - new uint[]{5394}, - new uint[]{5395}, - new uint[]{5396}, - new uint[]{5398}, - new uint[]{5399}, - new uint[]{5400}, - new uint[]{5401}, - new uint[]{5402}, - new uint[]{5403}, - new uint[]{5404}, - new uint[]{5405}, - new uint[]{5406}, - new uint[]{5407}, - new uint[]{5408}, - new uint[]{5409}, - new uint[]{5412}, - new uint[]{5413}, - new uint[]{5414}, - new uint[]{5415}, - new uint[]{5416}, - new uint[]{5417}, - new uint[]{5418}, - new uint[]{5419}, - new uint[]{5420}, - new uint[]{5421}, - new uint[]{5422}, - new uint[]{5423}, - new uint[]{5381}, - new uint[]{5429}, - new uint[]{5430}, - new uint[]{5431}, - new uint[]{5432}, - new uint[]{5433}, - new uint[]{5406}, - new uint[]{5434}, - new uint[]{5435}, - new uint[]{5436}, - new uint[]{5437}, - new uint[]{5439}, - new uint[]{5440}, - new uint[]{5441}, - new uint[]{5389}, - new uint[]{5442}, - new uint[]{5443}, - new uint[]{5444}, - new uint[]{5445}, - new uint[]{5446}, - new uint[]{5447}, - new uint[]{5448}, - new uint[]{5450}, - new uint[]{5451}, - new uint[]{5452}, - new uint[]{5453}, - new uint[]{5454}, - new uint[]{5455}, - new uint[]{5456}, - new uint[]{5457}, - new uint[]{5458}, - new uint[]{5459}, - new uint[]{5460}, - new uint[]{5462}, - new uint[]{5463}, - new uint[]{5440}, - new uint[]{5464}, - new uint[]{5465}, - new uint[]{5466}, - new uint[]{5467}, - new uint[]{5480}, - new uint[]{5468}, - new uint[]{5469}, - new uint[]{5470}, - new uint[]{5401}, - new uint[]{5474}, - new uint[]{5420}, - new uint[]{5415}, - new uint[]{5472}, - new uint[]{5473}, - new uint[]{5364}, - new uint[]{5409}, - new uint[]{5475}, - new uint[]{5422}, - new uint[]{5423}, - new uint[]{3467}, - new uint[]{5169}, - new uint[]{5170}, - new uint[]{5171}, - new uint[]{5172}, - new uint[]{5173}, - new uint[]{5176}, - new uint[]{5277}, - new uint[]{5179}, - Array.Empty(), - new uint[]{5217}, - new uint[]{108, 510, 1482, 2210, 2564, 2618, 2632, 3210, 3305, 3308, 5629, 5631, 5633, 5640, 5641, 5642, 6037, 6038, 6039, 6052, 6070, 6071, 6072, 6087, 6088, 6089, 6090, 6104, 6115, 6117, 6118, 6119, 6120, 6133, 6153, 6177, 6216, 6237, 6241, 6243, 6263, 6266, 6267, 6268, 6274, 6275, 6307, 6308, 6309, 6311, 6321, 6333, 6338, 6345, 6347, 6352, 6385, 6795, 6907, 6908, 6910, 6922, 6924, 6925, 6929, 6941, 6950, 6970, 6994, 6995, 6996, 7055, 7056, 7057, 7058, 7070, 7092, 7093, 7096, 7097, 7101, 7102, 7103, 7104, 7107, 7117, 7118, 7126, 7127, 7131, 7132, 7133, 7135, 7179, 7180, 7181, 7202, 7203, 7206, 7221, 7223, 7225, 7237, 7244, 7245, 7400, 7640, 7689, 7863, 7958, 9225, 9650, 9958, 9990, 10315, 11072, 11143}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{5310}, - Array.Empty(), - new uint[]{4578}, - new uint[]{4580}, - new uint[]{5477}, - new uint[]{5425}, - new uint[]{5426}, - new uint[]{5427}, - new uint[]{5428}, - new uint[]{4170}, - new uint[]{5333, 5461}, - new uint[]{4579}, - new uint[]{4580}, - new uint[]{5477}, - new uint[]{5042, 5625, 7395}, - new uint[]{5292}, - new uint[]{5293}, - new uint[]{5294}, - new uint[]{5297}, - new uint[]{5290}, - new uint[]{5291}, - new uint[]{5295}, - new uint[]{5283}, - new uint[]{5296}, - new uint[]{5298}, - new uint[]{5289}, - new uint[]{5288}, - new uint[]{5286}, - new uint[]{5285}, - new uint[]{5287}, - new uint[]{5284}, - new uint[]{5366}, - new uint[]{5411}, - new uint[]{2564}, - new uint[]{5526}, - new uint[]{2564}, - new uint[]{5523}, - new uint[]{2568}, - new uint[]{5522}, - new uint[]{5524}, - new uint[]{5525}, - new uint[]{5507}, - new uint[]{5508}, - new uint[]{5507}, - new uint[]{3725}, - new uint[]{5529}, - new uint[]{5585}, - new uint[]{5586}, - new uint[]{5530}, - new uint[]{5587}, - new uint[]{5531}, - new uint[]{5532}, - new uint[]{5534}, - new uint[]{5535}, - new uint[]{5536}, - new uint[]{5537}, - new uint[]{5538}, - new uint[]{5539}, - new uint[]{3805}, - new uint[]{5540}, - new uint[]{5541}, - new uint[]{5542}, - new uint[]{5543}, - new uint[]{3797}, - new uint[]{5544}, - new uint[]{5545}, - new uint[]{5509}, - new uint[]{5510}, - new uint[]{5511}, - new uint[]{5512}, - new uint[]{5513}, - new uint[]{5514}, - new uint[]{5509}, - new uint[]{2096}, - new uint[]{5567}, - new uint[]{5567}, - new uint[]{5568}, - new uint[]{5569}, - new uint[]{5570}, - new uint[]{5571}, - new uint[]{5572}, - new uint[]{5567}, - new uint[]{5560}, - new uint[]{5561}, - new uint[]{5562}, - new uint[]{5563}, - new uint[]{5554}, - new uint[]{5564}, - new uint[]{5565}, - new uint[]{5566}, - new uint[]{5547}, - new uint[]{5548}, - Array.Empty(), - new uint[]{5550}, - new uint[]{5551}, - new uint[]{5552}, - new uint[]{5546}, - new uint[]{5553}, - new uint[]{5557}, - new uint[]{5554}, - new uint[]{5555}, - new uint[]{5556}, - new uint[]{5558}, - new uint[]{5559}, - new uint[]{3281}, - new uint[]{5515}, - new uint[]{5516}, - Array.Empty(), - new uint[]{5517}, - new uint[]{5519}, - new uint[]{5518}, - new uint[]{5521}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4686}, - new uint[]{5476}, - new uint[]{5573}, - new uint[]{5574}, - new uint[]{5575}, - new uint[]{5576}, - new uint[]{5576}, - new uint[]{5577}, - new uint[]{5578}, - new uint[]{5579}, - new uint[]{5580}, - new uint[]{5581}, - new uint[]{5582}, - new uint[]{5583}, - new uint[]{3780}, - new uint[]{3782}, - new uint[]{3781}, - new uint[]{3780}, - new uint[]{5276}, - new uint[]{3780}, - new uint[]{5515}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5640}, - new uint[]{5549}, - new uint[]{5550}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5502}, - new uint[]{5503}, - new uint[]{5504}, - new uint[]{5505}, - Array.Empty(), - Array.Empty(), - new uint[]{5567}, - new uint[]{5567}, - new uint[]{5568}, - new uint[]{5569}, - new uint[]{5570}, - Array.Empty(), - new uint[]{5572}, - new uint[]{5567}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{2096}, - Array.Empty(), - Array.Empty(), - new uint[]{628}, - new uint[]{6014}, - new uint[]{6015}, - new uint[]{6017}, - new uint[]{6018}, - new uint[]{6019}, - new uint[]{108}, - new uint[]{5520}, - new uint[]{108}, - new uint[]{5689, 6353}, - new uint[]{5690}, - new uint[]{5691}, - new uint[]{6145}, - new uint[]{5718, 5767}, - new uint[]{5696}, - new uint[]{5698, 5731}, - new uint[]{5699, 5706, 6429}, - new uint[]{5704, 5709}, - Array.Empty(), - Array.Empty(), - new uint[]{5716}, - new uint[]{5717}, - new uint[]{5681, 5694}, - Array.Empty(), - Array.Empty(), - new uint[]{5700, 5710}, - Array.Empty(), - new uint[]{5672, 5692}, - Array.Empty(), - new uint[]{5684}, - new uint[]{5787, 6485}, - new uint[]{5719, 6797}, - new uint[]{5683}, - new uint[]{5674}, - new uint[]{5711}, - new uint[]{5705}, - new uint[]{5701}, - new uint[]{5712}, - new uint[]{5721}, - new uint[]{5720}, - new uint[]{5688}, - new uint[]{5707}, - new uint[]{5722}, - new uint[]{113}, - new uint[]{115}, - new uint[]{56}, - new uint[]{117}, - new uint[]{116}, - new uint[]{5733}, - new uint[]{108}, - new uint[]{5743, 5832, 6477, 6483}, - new uint[]{5744, 6477, 6483, 6489}, - new uint[]{5745, 6483, 6489}, - new uint[]{5747, 5766, 5849}, - new uint[]{5739, 5897}, - Array.Empty(), - new uint[]{5768, 6528}, - new uint[]{5755, 6518}, - new uint[]{5776}, - new uint[]{5769, 6509, 6525, 6731}, - Array.Empty(), - new uint[]{5782}, - Array.Empty(), - new uint[]{5756, 5770, 6529}, - new uint[]{5773, 6799}, - new uint[]{6141}, - new uint[]{5752, 6519}, - new uint[]{5753, 6519}, - new uint[]{5754, 6519}, - new uint[]{5734}, - new uint[]{5735, 5899}, - new uint[]{5775}, - Array.Empty(), - new uint[]{5758, 5786, 6288}, - Array.Empty(), - new uint[]{5783, 6521}, - new uint[]{5777}, - new uint[]{5736}, - new uint[]{5737, 5900}, - new uint[]{5738, 6798}, - new uint[]{5746, 6729}, - new uint[]{5749, 5774, 6550}, - new uint[]{5778}, - new uint[]{5728}, - new uint[]{5714}, - new uint[]{5715}, - new uint[]{5685}, - new uint[]{5693, 6796}, - Array.Empty(), - Array.Empty(), - new uint[]{5686}, - new uint[]{6775}, - Array.Empty(), - new uint[]{5726}, - new uint[]{5732, 6741}, - new uint[]{5680}, - new uint[]{5697, 6280}, - new uint[]{5687}, - new uint[]{5713}, - new uint[]{5708}, - new uint[]{5702}, - new uint[]{5723}, - new uint[]{5724}, - new uint[]{5725}, - new uint[]{5729}, - new uint[]{5730}, - new uint[]{5751, 6730}, - new uint[]{5750}, - new uint[]{5740, 6505, 6722}, - new uint[]{5741, 5784, 5887}, - Array.Empty(), - new uint[]{5748, 5759, 5771}, - new uint[]{5760, 5772}, - new uint[]{5761, 6728}, - new uint[]{5762}, - Array.Empty(), - new uint[]{5764}, - new uint[]{5836, 6507}, - new uint[]{5765}, - new uint[]{5779, 5941}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5785}, - new uint[]{5780}, - new uint[]{5781}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6426}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5727}, - new uint[]{5675, 6776}, - Array.Empty(), - Array.Empty(), - new uint[]{5954}, - new uint[]{6378}, - new uint[]{6381}, - new uint[]{6382}, - new uint[]{6383}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5590}, - new uint[]{5591}, - new uint[]{5592}, - new uint[]{5650}, - new uint[]{5576}, - new uint[]{5651}, - new uint[]{5652}, - new uint[]{5670}, - new uint[]{108}, - new uint[]{6154}, - new uint[]{4133}, - new uint[]{5239}, - new uint[]{5656}, - new uint[]{5657}, - new uint[]{5658}, - new uint[]{5659}, - new uint[]{5660}, - new uint[]{5661}, - new uint[]{5662}, - new uint[]{5239}, - new uint[]{4130}, - new uint[]{6146}, - new uint[]{6143}, - new uint[]{6144}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5540}, - new uint[]{6144}, - new uint[]{6147}, - Array.Empty(), - new uint[]{5643}, - new uint[]{5629}, - new uint[]{5629}, - new uint[]{5630}, - new uint[]{108}, - new uint[]{6142}, - new uint[]{6085}, - new uint[]{6086}, - new uint[]{6087}, - new uint[]{6088}, - new uint[]{6089}, - new uint[]{6090}, - new uint[]{6675}, - new uint[]{6676}, - new uint[]{6691, 6787}, - new uint[]{6691, 6787}, - new uint[]{6786}, - new uint[]{108}, - new uint[]{3237}, - new uint[]{6677}, - new uint[]{5970}, - new uint[]{5659}, - new uint[]{5964}, - new uint[]{5965}, - new uint[]{5966}, - new uint[]{5967}, - new uint[]{5953}, - new uint[]{5968}, - new uint[]{5969}, - new uint[]{5950}, - new uint[]{5951}, - new uint[]{1768}, - new uint[]{6342}, - new uint[]{6091}, - new uint[]{3305}, - new uint[]{6093}, - new uint[]{6075}, - new uint[]{6075}, - new uint[]{6076}, - new uint[]{6077}, - new uint[]{6078}, - new uint[]{6079}, - new uint[]{6080}, - new uint[]{6081}, - new uint[]{6082}, - new uint[]{6083}, - new uint[]{6084}, - new uint[]{5630}, - new uint[]{6173}, - new uint[]{6174}, - new uint[]{6175}, - new uint[]{6175}, - new uint[]{6176}, - new uint[]{6177}, - new uint[]{6178}, - new uint[]{6180}, - new uint[]{6155}, - new uint[]{6181}, - new uint[]{6182}, - new uint[]{6263}, - new uint[]{6264}, - new uint[]{6265}, - new uint[]{6266}, - new uint[]{6267}, - new uint[]{6268}, - new uint[]{6269}, - new uint[]{6270}, - new uint[]{5984}, - new uint[]{5985}, - new uint[]{5986}, - new uint[]{5987}, - new uint[]{5988}, - new uint[]{5989}, - new uint[]{5990}, - new uint[]{5991}, - new uint[]{5992}, - new uint[]{5993}, - new uint[]{5994}, - new uint[]{5995}, - new uint[]{5996}, - new uint[]{5997}, - new uint[]{5998}, - new uint[]{5999}, - new uint[]{6000}, - new uint[]{6001}, - new uint[]{6002}, - new uint[]{6003}, - new uint[]{6004}, - new uint[]{6005}, - new uint[]{6006}, - new uint[]{6007}, - new uint[]{6008}, - new uint[]{6009}, - new uint[]{6010}, - new uint[]{6011}, - new uint[]{6012}, - new uint[]{6013}, - new uint[]{6674}, - new uint[]{9083, 9088, 9090, 9095, 9096, 9099, 9103, 9107, 9109, 9113, 9117}, - new uint[]{9070, 9074, 9083, 9089, 9092, 9094, 9104, 9107, 9109, 9114, 9119, 9123, 9125}, - new uint[]{9070, 9079, 9087, 9088, 9089, 9093, 9099, 9100, 9105, 9106, 9107, 9111, 9114, 9117, 9120, 9122, 9124}, - new uint[]{9068, 9079, 9085, 9086, 9091, 9098, 9102, 9105, 9110, 9122}, - new uint[]{9072, 9077, 9088, 9090, 9097, 9113, 9118}, - new uint[]{9082, 9093, 9094, 9100, 9103, 9104, 9111, 9125}, - new uint[]{9069, 9080, 9084, 9089, 9093, 9095, 9101, 9108, 9111, 9115, 9119, 9123}, - new uint[]{9075, 9076, 9081, 9087, 9088, 9099, 9102, 9103, 9112, 9121}, - new uint[]{9080, 9081, 9084, 9087, 9088, 9093, 9102, 9103, 9105, 9107, 9113, 9114, 9115, 9120, 9125}, - new uint[]{6352}, - new uint[]{6049}, - new uint[]{6050}, - new uint[]{6051}, - new uint[]{5645}, - new uint[]{6045}, - new uint[]{6342}, - new uint[]{6341}, - new uint[]{6672}, - new uint[]{6673}, - new uint[]{1854}, - new uint[]{6691, 6787}, - new uint[]{108, 3649}, - new uint[]{6346}, - new uint[]{6341}, - new uint[]{5648}, - new uint[]{6662}, - new uint[]{6658}, - new uint[]{6659}, - new uint[]{6660}, - new uint[]{1501}, - new uint[]{108}, - new uint[]{4865}, - new uint[]{4866}, - new uint[]{4869}, - new uint[]{6391}, - new uint[]{6341}, - new uint[]{6669}, - new uint[]{6670}, - new uint[]{5972}, - new uint[]{6221}, - new uint[]{6221}, - new uint[]{108}, - new uint[]{6671}, - new uint[]{6071}, - new uint[]{6072}, - new uint[]{6073}, - new uint[]{6074}, - new uint[]{6058}, - new uint[]{6059}, - new uint[]{6060}, - new uint[]{6061}, - new uint[]{6062}, - new uint[]{6063}, - new uint[]{6064}, - new uint[]{6065}, - new uint[]{6066}, - new uint[]{6067}, - new uint[]{6068}, - new uint[]{6058}, - new uint[]{6069}, - new uint[]{6237}, - new uint[]{6238}, - new uint[]{6241}, - new uint[]{6243}, - new uint[]{6244}, - new uint[]{6246}, - new uint[]{5789}, - new uint[]{6275}, - new uint[]{6272}, - new uint[]{6279}, - new uint[]{6278}, - new uint[]{6277}, - Array.Empty(), - new uint[]{5641}, - new uint[]{5642}, - new uint[]{6226}, - new uint[]{6227}, - new uint[]{6228}, - new uint[]{6229}, - new uint[]{6384}, - new uint[]{6230}, - new uint[]{6231}, - new uint[]{6231}, - new uint[]{6232}, - new uint[]{6231}, - new uint[]{6233}, - new uint[]{6234}, - new uint[]{6235}, - new uint[]{6236}, - new uint[]{6335}, - new uint[]{6239}, - new uint[]{6146}, - new uint[]{5575}, - new uint[]{6148}, - new uint[]{6149}, - new uint[]{6156, 6158, 6790}, - new uint[]{6156, 6159}, - new uint[]{6157, 6158, 6160}, - new uint[]{6157, 6160}, - new uint[]{6161}, - new uint[]{6162, 6165}, - new uint[]{6163, 6165, 6791}, - new uint[]{6164}, - new uint[]{6163}, - new uint[]{6153}, - new uint[]{6152}, - new uint[]{5576}, - new uint[]{5562}, - new uint[]{6170}, - new uint[]{6208}, - new uint[]{6172}, - new uint[]{6336}, - new uint[]{6336}, - new uint[]{6153}, - new uint[]{6152}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6566}, - Array.Empty(), - new uint[]{6166}, - new uint[]{6151}, - new uint[]{5973}, - Array.Empty(), - new uint[]{6307}, - new uint[]{6308}, - new uint[]{6309}, - new uint[]{4385}, - new uint[]{6310}, - new uint[]{6311}, - new uint[]{1420}, - new uint[]{6119}, - new uint[]{6120}, - new uint[]{6118}, - new uint[]{6117}, - new uint[]{6116}, - new uint[]{6115}, - new uint[]{6665}, - new uint[]{6242}, - new uint[]{6185}, - new uint[]{6186}, - new uint[]{6187}, - new uint[]{6188}, - new uint[]{6189}, - Array.Empty(), - new uint[]{6190}, - new uint[]{6191}, - new uint[]{6192}, - new uint[]{6193}, - Array.Empty(), - new uint[]{6194}, - new uint[]{6195}, - new uint[]{6196}, - new uint[]{713}, - new uint[]{4392}, - new uint[]{6146}, - new uint[]{4130}, - new uint[]{6102}, - new uint[]{6103}, - new uint[]{6097, 6250}, - new uint[]{6101}, - new uint[]{6098}, - new uint[]{6099}, - new uint[]{6100}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6094}, - new uint[]{6094}, - new uint[]{6094}, - new uint[]{6097}, - new uint[]{6101}, - new uint[]{6098}, - new uint[]{6104}, - new uint[]{6108}, - Array.Empty(), - new uint[]{6140}, - new uint[]{6138}, - new uint[]{6139}, - new uint[]{6136}, - new uint[]{6137}, - new uint[]{6134}, - new uint[]{6135}, - new uint[]{6132}, - new uint[]{6131}, - new uint[]{6130}, - new uint[]{6129}, - new uint[]{6127}, - new uint[]{6128}, - new uint[]{6125}, - new uint[]{6124}, - new uint[]{6123}, - new uint[]{6122}, - new uint[]{6121}, - new uint[]{3920}, - new uint[]{3921}, - new uint[]{3922}, - new uint[]{3913}, - new uint[]{3912}, - new uint[]{6342}, - new uint[]{6668}, - new uint[]{3930}, - Array.Empty(), - new uint[]{6222}, - new uint[]{6221}, - new uint[]{6221}, - Array.Empty(), - new uint[]{6222}, - new uint[]{6223}, - new uint[]{6221, 6224}, - new uint[]{108}, - new uint[]{4420}, - new uint[]{6037}, - new uint[]{108}, - new uint[]{6038}, - new uint[]{6389}, - new uint[]{6389}, - new uint[]{6390}, - new uint[]{6039}, - new uint[]{6039}, - new uint[]{6041}, - new uint[]{6042}, - new uint[]{6040}, - new uint[]{6666}, - new uint[]{6114}, - new uint[]{331}, - new uint[]{6021}, - new uint[]{332}, - new uint[]{5666}, - Array.Empty(), - new uint[]{6342}, - new uint[]{6343}, - new uint[]{6248}, - new uint[]{6249}, - new uint[]{6250}, - new uint[]{6251}, - new uint[]{6252}, - Array.Empty(), - new uint[]{6254}, - new uint[]{6255}, - new uint[]{6256}, - new uint[]{6258}, - new uint[]{6259}, - new uint[]{6260}, - new uint[]{6261}, - new uint[]{6262}, - new uint[]{6257}, - new uint[]{5980}, - new uint[]{6088}, - new uint[]{3165}, - new uint[]{108, 6104}, - new uint[]{5667}, - new uint[]{6200}, - new uint[]{6200}, - new uint[]{6202}, - new uint[]{6201}, - new uint[]{6203}, - new uint[]{6203}, - new uint[]{6204}, - new uint[]{6204}, - new uint[]{6205}, - new uint[]{6205}, - new uint[]{6206}, - new uint[]{6206}, - new uint[]{6207}, - new uint[]{6170, 6171}, - new uint[]{6208}, - new uint[]{6209}, - new uint[]{6210}, - new uint[]{6211}, - new uint[]{6212}, - new uint[]{6213}, - new uint[]{6214}, - new uint[]{6215}, - new uint[]{6216}, - new uint[]{6170}, - new uint[]{6217}, - new uint[]{6218}, - new uint[]{6219}, - new uint[]{6220}, - new uint[]{6112}, - new uint[]{5764}, - new uint[]{6113}, - new uint[]{6113}, - new uint[]{6111}, - new uint[]{6289}, - new uint[]{6111}, - new uint[]{6111}, - new uint[]{6111}, - new uint[]{6710}, - new uint[]{6095}, - new uint[]{6095}, - new uint[]{6110}, - new uint[]{6105}, - new uint[]{6106}, - new uint[]{6107}, - new uint[]{6096}, - new uint[]{6096}, - new uint[]{6110}, - new uint[]{6105}, - new uint[]{6107}, - new uint[]{6114}, - new uint[]{331}, - new uint[]{6021}, - new uint[]{332}, - new uint[]{5666}, - new uint[]{6022}, - new uint[]{1416}, - new uint[]{4226}, - new uint[]{4227}, - new uint[]{823}, - new uint[]{828}, - new uint[]{751}, - new uint[]{108, 8395}, - new uint[]{6183}, - new uint[]{6184}, - new uint[]{331}, - new uint[]{6021}, - new uint[]{6328}, - new uint[]{6329}, - new uint[]{6330}, - new uint[]{6331}, - new uint[]{6324}, - new uint[]{6332}, - new uint[]{6333}, - new uint[]{6334}, - new uint[]{6332}, - new uint[]{6321}, - new uint[]{6322}, - new uint[]{6323}, - new uint[]{6324}, - new uint[]{6325}, - new uint[]{6326}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6667}, - new uint[]{6224}, - new uint[]{6224}, - new uint[]{5790, 5797, 5798, 6355}, - new uint[]{5791, 6793, 6794}, - new uint[]{5793, 5794, 5795, 6792}, - new uint[]{5796, 6732}, - new uint[]{5799}, - new uint[]{5800}, - new uint[]{5801}, - new uint[]{5802}, - new uint[]{5806}, - new uint[]{5807}, - new uint[]{5808}, - new uint[]{5809}, - new uint[]{5810}, - new uint[]{5811}, - new uint[]{5812}, - new uint[]{2235, 5813}, - new uint[]{5814, 6785}, - new uint[]{5814, 6785}, - Array.Empty(), - new uint[]{2234}, - new uint[]{5815}, - new uint[]{5814, 6785}, - new uint[]{5814, 5872, 5946, 6785}, - new uint[]{5816}, - new uint[]{5819}, - new uint[]{5216}, - new uint[]{1402}, - new uint[]{1402}, - new uint[]{1403}, - new uint[]{3825}, - new uint[]{5822}, - new uint[]{5823}, - new uint[]{5824}, - new uint[]{5825}, - new uint[]{5826}, - new uint[]{1255, 5827, 5855, 5872, 6739}, - new uint[]{5828}, - Array.Empty(), - new uint[]{5975}, - new uint[]{5976}, - new uint[]{5977}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{6565}, - Array.Empty(), - new uint[]{108}, - new uint[]{6700}, - new uint[]{6701}, - new uint[]{1640}, - new uint[]{3881}, - new uint[]{6694}, - new uint[]{6702}, - new uint[]{6703}, - new uint[]{6704}, - new uint[]{6705}, - new uint[]{108}, - new uint[]{5979}, - new uint[]{5979}, - new uint[]{4331}, - new uint[]{4321}, - new uint[]{6300}, - new uint[]{4332, 5240, 6300, 6303, 6320}, - new uint[]{6301}, - new uint[]{6302}, - new uint[]{6303}, - new uint[]{6306}, - new uint[]{4332}, - new uint[]{6304}, - new uint[]{6304}, - new uint[]{6304}, - new uint[]{6304}, - new uint[]{5241}, - new uint[]{5243}, - new uint[]{5242}, - new uint[]{5244}, - new uint[]{6313}, - new uint[]{3849}, - new uint[]{4142}, - new uint[]{6314}, - new uint[]{6315}, - new uint[]{6316}, - new uint[]{4131}, - new uint[]{6317}, - new uint[]{3850}, - new uint[]{6318}, - new uint[]{6319}, - new uint[]{6320}, - new uint[]{5240}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{5239}, - new uint[]{6146}, - new uint[]{3204}, - new uint[]{5649}, - new uint[]{6306}, - new uint[]{6661}, - new uint[]{6656}, - new uint[]{6273}, - new uint[]{6225}, - new uint[]{6225}, - new uint[]{6276}, - new uint[]{6245}, - new uint[]{6706}, - new uint[]{5631}, - new uint[]{5632}, - new uint[]{6711}, - new uint[]{6712}, - Array.Empty(), - new uint[]{6385}, - new uint[]{6690}, - new uint[]{6386}, - Array.Empty(), - new uint[]{6388}, - new uint[]{6387}, - Array.Empty(), - new uint[]{6705}, - new uint[]{6705}, - new uint[]{6707}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{5239}, - new uint[]{4846}, - new uint[]{4740}, - new uint[]{5947}, - new uint[]{5948}, - new uint[]{5949}, - new uint[]{5950}, - new uint[]{5951}, - new uint[]{5952}, - new uint[]{5953}, - new uint[]{5954}, - new uint[]{5955}, - new uint[]{5956}, - new uint[]{5957}, - new uint[]{5958}, - new uint[]{5959}, - new uint[]{5960}, - new uint[]{5961}, - new uint[]{108}, - new uint[]{2147}, - new uint[]{6344}, - new uint[]{6345}, - new uint[]{4759}, - new uint[]{4760}, - new uint[]{4761}, - new uint[]{4762}, - new uint[]{6346}, - new uint[]{6408}, - new uint[]{6709}, - new uint[]{1640}, - new uint[]{3881}, - new uint[]{6694}, - new uint[]{6695}, - new uint[]{5682, 5695}, - Array.Empty(), - new uint[]{4236}, - new uint[]{4237}, - new uint[]{6686}, - new uint[]{6687}, - new uint[]{6688}, - new uint[]{6407}, - Array.Empty(), - new uint[]{6413}, - new uint[]{6414}, - new uint[]{6415}, - new uint[]{6416}, - new uint[]{6418}, - new uint[]{6422}, - new uint[]{6424}, - new uint[]{4427}, - new uint[]{5648}, - new uint[]{6419, 6420, 6423}, - Array.Empty(), - Array.Empty(), - new uint[]{6425}, - new uint[]{6696}, - new uint[]{5788, 5919}, - new uint[]{5742}, - new uint[]{5757}, - new uint[]{6697}, - new uint[]{6698}, - new uint[]{108}, - new uint[]{6699}, - new uint[]{6693}, - new uint[]{5962}, - new uint[]{5963}, - new uint[]{6347}, - new uint[]{6347}, - new uint[]{6351}, - new uint[]{6348}, - new uint[]{108}, - new uint[]{541}, - new uint[]{541}, - new uint[]{108}, - new uint[]{5633}, - new uint[]{5634}, - new uint[]{5635}, - new uint[]{5626}, - new uint[]{6724}, - new uint[]{6056}, - new uint[]{5636}, - new uint[]{5637}, - new uint[]{5633, 6056}, - new uint[]{6379}, - new uint[]{5954}, - new uint[]{5664}, - new uint[]{2096}, - new uint[]{628}, - new uint[]{5663}, - new uint[]{5665}, - new uint[]{5663}, - new uint[]{6385}, - new uint[]{6261}, - new uint[]{6392}, - new uint[]{6393}, - new uint[]{2096}, - new uint[]{6020}, - new uint[]{6016}, - new uint[]{6394}, - new uint[]{6395}, - new uint[]{6290}, - new uint[]{6290}, - new uint[]{6298}, - new uint[]{6293, 6294}, - new uint[]{2343}, - new uint[]{108, 6325, 6327, 8395}, - new uint[]{6453}, - new uint[]{6349}, - new uint[]{6350}, - new uint[]{6231}, - new uint[]{6231}, - new uint[]{6231}, - new uint[]{6231}, - new uint[]{5671}, - new uint[]{5678}, - new uint[]{5672}, - new uint[]{5673}, - new uint[]{5674}, - new uint[]{5675}, - new uint[]{5676}, - new uint[]{5677}, - new uint[]{5679}, - new uint[]{5680}, - new uint[]{5698}, - new uint[]{5699}, - new uint[]{5700}, - new uint[]{5701}, - new uint[]{5702}, - new uint[]{5703}, - new uint[]{113}, - new uint[]{115}, - new uint[]{56}, - Array.Empty(), - new uint[]{6052}, - new uint[]{6052}, - new uint[]{6053}, - new uint[]{6054}, - new uint[]{6055}, - new uint[]{6055}, - new uint[]{4815}, - new uint[]{6052, 6055}, - new uint[]{6385}, - Array.Empty(), - new uint[]{6686}, - new uint[]{6687}, - new uint[]{6688}, - new uint[]{6685}, - new uint[]{108}, - new uint[]{6689}, - new uint[]{5655}, - new uint[]{5653}, - new uint[]{5654}, - new uint[]{6337}, - new uint[]{6338}, - new uint[]{6339}, - new uint[]{6340}, - new uint[]{6340}, - new uint[]{6340}, - new uint[]{4237}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{5850}, - new uint[]{5851}, - new uint[]{5853}, - new uint[]{5854}, - new uint[]{5855}, - new uint[]{5856}, - new uint[]{3102}, - new uint[]{104, 105}, - new uint[]{5842, 5855, 5862, 5880, 5894, 6713, 6720, 6789}, - new uint[]{5860}, - new uint[]{5861, 6354}, - new uint[]{5861, 5913, 5938}, - new uint[]{5861, 5915}, - new uint[]{5842, 5863, 6714, 6740}, - new uint[]{3107}, - new uint[]{5865}, - new uint[]{3107}, - new uint[]{5865}, - new uint[]{5866}, - new uint[]{5866}, - new uint[]{5867}, - new uint[]{5868}, - new uint[]{5869}, - new uint[]{5838, 5870}, - new uint[]{5871}, - new uint[]{5777}, - new uint[]{5872, 5946}, - new uint[]{5873}, - new uint[]{5829, 5895, 5934}, - new uint[]{5874}, - new uint[]{5875}, - new uint[]{5800}, - new uint[]{5876}, - new uint[]{5805}, - new uint[]{5878}, - new uint[]{5806}, - Array.Empty(), - new uint[]{5882}, - new uint[]{5883}, - new uint[]{5883}, - new uint[]{5884}, - new uint[]{5885}, - new uint[]{3930}, - new uint[]{5839, 5886}, - new uint[]{5888}, - new uint[]{5889}, - new uint[]{5890}, - new uint[]{5891}, - new uint[]{5892}, - new uint[]{5916}, - new uint[]{5893}, - new uint[]{5896}, - Array.Empty(), - new uint[]{5901}, - new uint[]{5902}, - new uint[]{5903}, - new uint[]{5904}, - new uint[]{5905}, - new uint[]{5906}, - new uint[]{5907}, - new uint[]{5909}, - new uint[]{5841, 5981}, - Array.Empty(), - new uint[]{5910}, - new uint[]{5911}, - new uint[]{5912}, - new uint[]{5877}, - new uint[]{2234}, - new uint[]{6736}, - new uint[]{5830}, - Array.Empty(), - new uint[]{5833}, - new uint[]{5832}, - new uint[]{5834}, - new uint[]{5835}, - new uint[]{5837}, - new uint[]{5838}, - new uint[]{5804}, - Array.Empty(), - new uint[]{5841, 5893, 5946}, - new uint[]{5982}, - new uint[]{5843, 5983}, - new uint[]{5847}, - new uint[]{5848}, - new uint[]{108}, - new uint[]{6023}, - new uint[]{6024}, - new uint[]{6026}, - new uint[]{6025}, - new uint[]{6027}, - new uint[]{6028}, - new uint[]{6029}, - new uint[]{6030}, - new uint[]{6031}, - new uint[]{6032}, - new uint[]{6033}, - new uint[]{6034}, - new uint[]{6035}, - new uint[]{6035}, - new uint[]{6036}, - new uint[]{6737}, - new uint[]{6738}, - new uint[]{5660}, - new uint[]{6735}, - new uint[]{6471}, - new uint[]{6472}, - new uint[]{6474}, - new uint[]{6475}, - new uint[]{6480}, - new uint[]{6482}, - new uint[]{6492}, - new uint[]{6497}, - new uint[]{6455}, - new uint[]{6454}, - new uint[]{5644}, - new uint[]{6050}, - new uint[]{6051}, - new uint[]{5645}, - new uint[]{6045}, - new uint[]{5643}, - new uint[]{3860}, - new uint[]{5646}, - new uint[]{5647}, - new uint[]{5971}, - new uint[]{6043}, - new uint[]{6044}, - new uint[]{6146}, - new uint[]{5575}, - new uint[]{6148}, - new uint[]{6149}, - new uint[]{6159, 6162}, - new uint[]{6156}, - new uint[]{6046}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6493}, - new uint[]{6556, 6557, 6558, 6559, 6653, 6654, 6655}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6157}, - new uint[]{6163}, - new uint[]{6743}, - new uint[]{6744}, - new uint[]{6745}, - new uint[]{6746}, - new uint[]{6747}, - new uint[]{6748}, - new uint[]{6749}, - new uint[]{6750}, - new uint[]{6751}, - new uint[]{6752}, - new uint[]{6753}, - new uint[]{6754}, - new uint[]{6755}, - new uint[]{6756}, - new uint[]{6757}, - new uint[]{6758}, - new uint[]{6759}, - new uint[]{6760}, - new uint[]{6761}, - new uint[]{6762}, - new uint[]{6763}, - new uint[]{6764}, - new uint[]{6765}, - new uint[]{6766}, - new uint[]{6767}, - new uint[]{6768}, - new uint[]{6769}, - new uint[]{6770}, - new uint[]{6771}, - new uint[]{6772}, - new uint[]{6773}, - new uint[]{6774}, - new uint[]{6433}, - new uint[]{6434}, - new uint[]{6435}, - Array.Empty(), - new uint[]{6438}, - new uint[]{6440}, - new uint[]{6441}, - new uint[]{6443}, - new uint[]{6445}, - new uint[]{6446}, - new uint[]{6447}, - new uint[]{6449}, - new uint[]{6450}, - new uint[]{6503}, - new uint[]{6506}, - Array.Empty(), - new uint[]{6513}, - new uint[]{6514}, - new uint[]{6515}, - new uint[]{6516}, - new uint[]{6517}, - new uint[]{6522}, - new uint[]{6523}, - new uint[]{6524}, - new uint[]{6047}, - new uint[]{5633}, - new uint[]{5634}, - new uint[]{6724}, - new uint[]{3293}, - new uint[]{6048}, - new uint[]{5644}, - new uint[]{6377}, - new uint[]{6364}, - new uint[]{6365}, - new uint[]{6366}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6385}, - new uint[]{6690}, - new uint[]{6386}, - new uint[]{6385}, - new uint[]{108}, - new uint[]{6473}, - new uint[]{4144}, - new uint[]{4192}, - new uint[]{6367}, - new uint[]{6368}, - new uint[]{6369}, - new uint[]{6156, 6790}, - new uint[]{6159}, - new uint[]{6163, 6791}, - new uint[]{6715}, - new uint[]{2147}, - new uint[]{4760}, - new uint[]{5978}, - new uint[]{6716}, - new uint[]{3257}, - new uint[]{3258}, - new uint[]{3259}, - new uint[]{3262}, - new uint[]{3264}, - new uint[]{1486}, - new uint[]{1418}, - new uint[]{1502}, - new uint[]{6057}, - new uint[]{3374}, - new uint[]{5625}, - new uint[]{5625}, - new uint[]{5626}, - new uint[]{5627}, - Array.Empty(), - new uint[]{108}, - new uint[]{6718}, - new uint[]{108}, - new uint[]{6719}, - new uint[]{6530}, - new uint[]{6531}, - new uint[]{6536}, - new uint[]{6538}, - new uint[]{6539}, - new uint[]{6542}, - new uint[]{6545}, - new uint[]{6546}, - new uint[]{6548}, - new uint[]{6551}, - new uint[]{6552}, - new uint[]{6553}, - new uint[]{6555}, - new uint[]{6549}, - new uint[]{5777}, - Array.Empty(), - new uint[]{6499}, - new uint[]{5753}, - new uint[]{6500}, - new uint[]{6495}, - new uint[]{5748}, - Array.Empty(), - Array.Empty(), - new uint[]{5631}, - new uint[]{6711}, - new uint[]{6712}, - new uint[]{3164}, - new uint[]{3133}, - new uint[]{6678}, - new uint[]{3138}, - new uint[]{3129}, - new uint[]{6679}, - new uint[]{6680}, - new uint[]{6680}, - new uint[]{6680}, - new uint[]{3165}, - new uint[]{4144}, - new uint[]{4192}, - new uint[]{4204}, - new uint[]{6375}, - new uint[]{6370, 6376}, - new uint[]{6371}, - new uint[]{6372}, - new uint[]{6372}, - new uint[]{6373}, - new uint[]{6374}, - new uint[]{6681}, - new uint[]{1501}, - new uint[]{108}, - new uint[]{6052}, - new uint[]{6052}, - new uint[]{6053}, - new uint[]{6054}, - new uint[]{6566}, - new uint[]{5970}, - new uint[]{4130}, - new uint[]{713}, - new uint[]{6357}, - new uint[]{6358}, - new uint[]{6470}, - new uint[]{6356}, - new uint[]{5721}, - new uint[]{6359}, - new uint[]{6281}, - new uint[]{5841}, - new uint[]{6283}, - new uint[]{6284}, - new uint[]{6285}, - new uint[]{6286}, - new uint[]{6287}, - new uint[]{5792}, - new uint[]{5914}, - new uint[]{5818}, - new uint[]{5916}, - new uint[]{5918}, - new uint[]{5920}, - new uint[]{5921}, - new uint[]{5922}, - new uint[]{5923}, - new uint[]{5924}, - new uint[]{5925}, - new uint[]{5926}, - new uint[]{5927}, - new uint[]{5798}, - new uint[]{5929}, - new uint[]{5930}, - new uint[]{5931}, - new uint[]{5932}, - new uint[]{5933}, - new uint[]{5934, 6282}, - new uint[]{5935}, - Array.Empty(), - new uint[]{5937}, - new uint[]{5939}, - new uint[]{5940}, - new uint[]{5942}, - new uint[]{5943}, - Array.Empty(), - new uint[]{5944}, - new uint[]{5945}, - Array.Empty(), - new uint[]{6111}, - new uint[]{6683}, - new uint[]{6684}, - new uint[]{6684}, - new uint[]{108}, - new uint[]{6360}, - new uint[]{6361}, - new uint[]{6362}, - new uint[]{6363}, - new uint[]{6657}, - new uint[]{6102}, - new uint[]{6097, 6250}, - new uint[]{6098}, - new uint[]{6101}, - new uint[]{5719}, - new uint[]{5723}, - new uint[]{4392}, - new uint[]{5954}, - new uint[]{108}, - new uint[]{6567}, - new uint[]{6568}, - new uint[]{6569}, - new uint[]{6570}, - new uint[]{6571}, - new uint[]{6572}, - new uint[]{6573}, - new uint[]{6574}, - new uint[]{6575}, - new uint[]{6576}, - new uint[]{6577}, - new uint[]{6578}, - new uint[]{6579}, - new uint[]{6580}, - new uint[]{6581}, - new uint[]{6582}, - new uint[]{6583}, - new uint[]{6584}, - new uint[]{6585}, - new uint[]{6586}, - new uint[]{6587}, - new uint[]{6588}, - new uint[]{6589}, - new uint[]{6590}, - new uint[]{6591}, - new uint[]{6592}, - new uint[]{6593}, - new uint[]{6594}, - new uint[]{6595}, - new uint[]{6596}, - new uint[]{6597}, - new uint[]{6598}, - new uint[]{6599}, - new uint[]{6600}, - new uint[]{6601}, - new uint[]{6602}, - new uint[]{6582}, - new uint[]{6603}, - new uint[]{6604}, - new uint[]{6605}, - new uint[]{6606}, - new uint[]{6607}, - new uint[]{6608}, - new uint[]{6609}, - new uint[]{6610}, - new uint[]{6611}, - new uint[]{6612}, - new uint[]{6585}, - new uint[]{6613}, - new uint[]{6614}, - new uint[]{6615}, - new uint[]{6616}, - new uint[]{6617}, - new uint[]{6618}, - new uint[]{6619}, - new uint[]{6620}, - new uint[]{6621}, - new uint[]{6622}, - new uint[]{6623}, - new uint[]{6624}, - new uint[]{6625}, - new uint[]{6626}, - new uint[]{6627}, - new uint[]{6628}, - new uint[]{6629}, - new uint[]{6630}, - new uint[]{6631}, - new uint[]{6632}, - new uint[]{6633}, - new uint[]{6634}, - new uint[]{6635}, - new uint[]{6636}, - new uint[]{6637}, - new uint[]{6638}, - new uint[]{6639}, - new uint[]{6640}, - new uint[]{6641}, - new uint[]{6642}, - new uint[]{6585}, - new uint[]{6643}, - new uint[]{6644}, - new uint[]{6645}, - new uint[]{6586}, - new uint[]{6646}, - new uint[]{6647}, - new uint[]{6648}, - new uint[]{6649}, - new uint[]{6650}, - new uint[]{6651}, - new uint[]{6652}, - new uint[]{6562}, - new uint[]{6563}, - new uint[]{6554}, - new uint[]{6540}, - new uint[]{6532}, - new uint[]{6456}, - new uint[]{6457}, - new uint[]{6458}, - new uint[]{6459}, - new uint[]{6460}, - new uint[]{6461}, - new uint[]{6462}, - new uint[]{6463}, - new uint[]{6464}, - new uint[]{6465}, - new uint[]{6466}, - new uint[]{6468}, - new uint[]{6469}, - new uint[]{6734}, - new uint[]{6467}, - new uint[]{6727}, - new uint[]{6427}, - new uint[]{6430}, - new uint[]{6428}, - new uint[]{6511}, - new uint[]{6487}, - Array.Empty(), - new uint[]{6495}, - new uint[]{6491}, - new uint[]{6493}, - new uint[]{6493}, - new uint[]{6402, 6404, 6405}, - new uint[]{6396}, - new uint[]{6397}, - new uint[]{6400}, - new uint[]{6401}, - new uint[]{6564}, - new uint[]{6564}, - new uint[]{6501}, - new uint[]{6501}, - new uint[]{6501}, - new uint[]{108}, - new uint[]{6111}, - new uint[]{108}, - new uint[]{6680}, - new uint[]{6680}, - new uint[]{6682}, - Array.Empty(), - new uint[]{108, 6095}, - new uint[]{6664}, - new uint[]{6663}, - Array.Empty(), - new uint[]{5632}, - new uint[]{6692}, - new uint[]{6437}, - new uint[]{6498}, - new uint[]{541}, - new uint[]{3306}, - new uint[]{6296}, - new uint[]{4953}, - new uint[]{6395}, - new uint[]{108}, - new uint[]{6708}, - new uint[]{5628}, - new uint[]{108}, - new uint[]{108}, - new uint[]{5640}, - Array.Empty(), - new uint[]{6529}, - new uint[]{6721, 6723}, - new uint[]{5923}, - new uint[]{5846}, - new uint[]{5845}, - new uint[]{5844}, - new uint[]{5898}, - new uint[]{5936}, - new uint[]{2234}, - new uint[]{5801}, - new uint[]{6285}, - new uint[]{5817}, - new uint[]{2234}, - new uint[]{5832}, - new uint[]{5833}, - new uint[]{5834}, - new uint[]{2236}, - new uint[]{5578}, - new uint[]{5852}, - new uint[]{5829}, - new uint[]{2236}, - new uint[]{5578}, - new uint[]{5578}, - new uint[]{6284}, - new uint[]{5970}, - new uint[]{5659}, - new uint[]{5964}, - new uint[]{5660}, - new uint[]{5660}, - new uint[]{5967}, - Array.Empty(), - new uint[]{6726}, - new uint[]{6742}, - Array.Empty(), - new uint[]{12317}, - new uint[]{6857, 6858}, - new uint[]{6859, 6860, 6861, 6862}, - new uint[]{541}, - Array.Empty(), - new uint[]{6922}, - new uint[]{3069}, - new uint[]{6923}, - new uint[]{6950}, - new uint[]{5789}, - new uint[]{6275}, - new uint[]{6272}, - Array.Empty(), - new uint[]{6278}, - new uint[]{6277}, - new uint[]{6271}, - new uint[]{5641}, - new uint[]{5642}, - new uint[]{6273}, - new uint[]{6276}, - new uint[]{6856}, - new uint[]{6869}, - new uint[]{6870}, - new uint[]{6871}, - new uint[]{6872}, - new uint[]{6925}, - new uint[]{6926}, - new uint[]{6927}, - new uint[]{6928}, - new uint[]{6929}, - new uint[]{6930}, - new uint[]{6931}, - new uint[]{6932}, - new uint[]{6933}, - new uint[]{6934}, - new uint[]{6935}, - new uint[]{6936}, - new uint[]{6937}, - new uint[]{6938}, - new uint[]{6939}, - new uint[]{6940}, - new uint[]{5640, 5641, 5642, 6275}, - new uint[]{5640, 5641, 5642, 6856}, - new uint[]{5641}, - new uint[]{5642}, - new uint[]{6907}, - new uint[]{6908}, - new uint[]{6909}, - new uint[]{6910}, - new uint[]{6941}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6897}, - new uint[]{6896}, - new uint[]{6898}, - new uint[]{6899}, - new uint[]{6900}, - new uint[]{6901}, - new uint[]{6902}, - new uint[]{6903}, - new uint[]{6905}, - new uint[]{6904}, - new uint[]{6906}, - new uint[]{6945}, - new uint[]{6953}, - new uint[]{6952}, - new uint[]{6951}, - new uint[]{6942}, - new uint[]{108, 6943}, - new uint[]{6944}, - new uint[]{6945}, - new uint[]{6946}, - new uint[]{6853}, - new uint[]{6847}, - new uint[]{6848}, - new uint[]{6849}, - new uint[]{6850}, - new uint[]{6851}, - new uint[]{6385}, - new uint[]{6911}, - new uint[]{4130}, - new uint[]{5978}, - new uint[]{4133}, - new uint[]{6912}, - new uint[]{6971}, - new uint[]{5970}, - Array.Empty(), - new uint[]{6913}, - new uint[]{5964}, - new uint[]{6386}, - new uint[]{6914}, - new uint[]{1482}, - new uint[]{2210}, - new uint[]{2612}, - new uint[]{2628}, - new uint[]{2630}, - new uint[]{2631}, - new uint[]{2632}, - new uint[]{6958}, - new uint[]{6957}, - new uint[]{3210}, - new uint[]{3204}, - Array.Empty(), - new uint[]{6854}, - new uint[]{6855}, - Array.Empty(), - new uint[]{6817}, - new uint[]{6808}, - new uint[]{6811}, - new uint[]{6810}, - new uint[]{6807}, - new uint[]{6812}, - new uint[]{6813}, - new uint[]{6814}, - new uint[]{6815}, - new uint[]{6809}, - new uint[]{6816}, - new uint[]{6818}, - new uint[]{6819}, - new uint[]{6820}, - new uint[]{6821}, - new uint[]{6822}, - new uint[]{6823}, - new uint[]{6824}, - new uint[]{6825}, - new uint[]{6826}, - new uint[]{6827}, - new uint[]{6828}, - new uint[]{6829}, - new uint[]{6830}, - new uint[]{6831}, - new uint[]{6832}, - new uint[]{6835}, - new uint[]{6834}, - new uint[]{6833}, - new uint[]{6836}, - new uint[]{6837}, - new uint[]{6838}, - new uint[]{6839}, - new uint[]{6840}, - new uint[]{6841}, - new uint[]{6842}, - new uint[]{6843}, - new uint[]{6844}, - new uint[]{6845}, - new uint[]{6846}, - new uint[]{6863}, - new uint[]{6864}, - Array.Empty(), - new uint[]{6915}, - new uint[]{6916}, - new uint[]{6917}, - new uint[]{6918}, - new uint[]{6919}, - new uint[]{6920}, - new uint[]{6921}, - new uint[]{7147}, - new uint[]{7167}, - new uint[]{7168}, - Array.Empty(), - new uint[]{6865}, - new uint[]{6866}, - new uint[]{6941}, - Array.Empty(), - new uint[]{6873}, - new uint[]{6874}, - new uint[]{6875}, - new uint[]{6876, 6894}, - new uint[]{6876}, - new uint[]{6877}, - new uint[]{6878}, - new uint[]{6878}, - new uint[]{6879}, - new uint[]{6880}, - new uint[]{6881}, - new uint[]{6881, 6895}, - new uint[]{6882, 6889}, - new uint[]{6884}, - new uint[]{6883, 6890}, - new uint[]{6882, 6886}, - new uint[]{6884, 6888}, - new uint[]{6883, 6887}, - new uint[]{6885}, - new uint[]{6889}, - new uint[]{6890}, - new uint[]{6891}, - new uint[]{6961}, - new uint[]{6962}, - new uint[]{6963}, - new uint[]{6964}, - new uint[]{6965}, - new uint[]{6966}, - new uint[]{7184}, - new uint[]{7147}, - new uint[]{5746}, - new uint[]{5743}, - new uint[]{5744}, - new uint[]{5745}, - new uint[]{6967}, - new uint[]{5746}, - new uint[]{5743}, - new uint[]{5744}, - new uint[]{7036}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6949}, - new uint[]{6954}, - new uint[]{6955}, - new uint[]{6956}, - new uint[]{6961}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6967}, - new uint[]{7143}, - new uint[]{7160}, - new uint[]{7144}, - new uint[]{6959}, - new uint[]{6567}, - new uint[]{6568}, - new uint[]{6569}, - new uint[]{6570}, - new uint[]{6571}, - new uint[]{6572}, - new uint[]{6573}, - new uint[]{6574}, - new uint[]{6575}, - new uint[]{6576}, - new uint[]{6577}, - new uint[]{6578}, - new uint[]{6579}, - new uint[]{6580}, - new uint[]{6581}, - new uint[]{6582}, - new uint[]{6583}, - new uint[]{6584}, - new uint[]{6585}, - new uint[]{6586}, - new uint[]{6587}, - new uint[]{6588}, - new uint[]{6589}, - new uint[]{6590}, - new uint[]{6591}, - new uint[]{6592}, - new uint[]{6593}, - new uint[]{6594}, - new uint[]{6595}, - new uint[]{6596}, - new uint[]{6597}, - new uint[]{6598}, - new uint[]{6599}, - new uint[]{6600}, - new uint[]{6601}, - new uint[]{6602}, - new uint[]{6582}, - new uint[]{6603}, - new uint[]{6604}, - new uint[]{6605}, - new uint[]{6606}, - new uint[]{6607}, - new uint[]{6608}, - new uint[]{6609}, - new uint[]{6610}, - new uint[]{6611}, - new uint[]{6612}, - new uint[]{6585}, - new uint[]{6613}, - new uint[]{6614}, - new uint[]{6615}, - new uint[]{6616}, - new uint[]{6617}, - new uint[]{6618}, - new uint[]{6619}, - new uint[]{6620}, - new uint[]{6621}, - new uint[]{6622}, - new uint[]{6623}, - new uint[]{6624}, - new uint[]{6625}, - new uint[]{6626}, - new uint[]{6627}, - new uint[]{6628}, - new uint[]{6629}, - new uint[]{6630}, - new uint[]{6631}, - new uint[]{6632}, - new uint[]{6633}, - new uint[]{6634}, - new uint[]{6635}, - new uint[]{6636}, - new uint[]{6637}, - new uint[]{6638}, - new uint[]{6639}, - new uint[]{6640}, - new uint[]{6641}, - new uint[]{6642}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6960}, - new uint[]{6968}, - new uint[]{6969}, - new uint[]{7114}, - new uint[]{7115}, - new uint[]{6994}, - new uint[]{6995}, - new uint[]{7016}, - new uint[]{6996}, - new uint[]{7001}, - new uint[]{6999}, - new uint[]{7015}, - new uint[]{7074}, - new uint[]{7055}, - new uint[]{108}, - new uint[]{7056}, - new uint[]{7057}, - new uint[]{7058}, - new uint[]{7058}, - new uint[]{7144}, - new uint[]{7107}, - new uint[]{7108}, - new uint[]{7109}, - new uint[]{7110}, - new uint[]{7111}, - new uint[]{7112}, - new uint[]{7107}, - new uint[]{7108}, - new uint[]{7109}, - new uint[]{7110}, - new uint[]{7111}, - new uint[]{7112}, - new uint[]{7116}, - new uint[]{7059}, - new uint[]{7060}, - new uint[]{7061}, - new uint[]{7062}, - new uint[]{7063}, - new uint[]{7064}, - new uint[]{7065}, - new uint[]{7066}, - new uint[]{7067}, - new uint[]{7068}, - new uint[]{7069}, - new uint[]{2642}, - new uint[]{7071}, - new uint[]{6972}, - new uint[]{6973}, - new uint[]{6974}, - new uint[]{6975}, - new uint[]{6976}, - new uint[]{6977}, - new uint[]{6978}, - new uint[]{6979}, - new uint[]{6980}, - new uint[]{6981}, - new uint[]{7113}, - new uint[]{7113}, - new uint[]{7182}, - new uint[]{7181}, - new uint[]{7092}, - new uint[]{7093}, - new uint[]{7094}, - new uint[]{7095}, - new uint[]{7092}, - new uint[]{7093}, - new uint[]{7094}, - Array.Empty(), - new uint[]{7000}, - new uint[]{6998}, - new uint[]{7002}, - new uint[]{7007}, - new uint[]{7005}, - new uint[]{7004}, - new uint[]{7003}, - new uint[]{7006}, - new uint[]{7009}, - new uint[]{7008}, - new uint[]{7010}, - new uint[]{7011}, - new uint[]{7012}, - new uint[]{7013}, - new uint[]{7014}, - new uint[]{7096}, - new uint[]{7097}, - new uint[]{7098}, - new uint[]{7183}, - new uint[]{7105}, - new uint[]{7099}, - Array.Empty(), - new uint[]{7106}, - new uint[]{108}, - new uint[]{108}, - new uint[]{6200}, - new uint[]{6203}, - new uint[]{6997}, - new uint[]{6982}, - new uint[]{6982}, - new uint[]{6982}, - new uint[]{6983}, - new uint[]{6983}, - new uint[]{6983}, - new uint[]{6986}, - new uint[]{6984}, - new uint[]{6985}, - new uint[]{6987}, - new uint[]{6988}, - new uint[]{6989}, - new uint[]{7148}, - new uint[]{7149}, - new uint[]{7150}, - new uint[]{7166}, - new uint[]{7151}, - new uint[]{6990}, - new uint[]{7221}, - new uint[]{6173}, - new uint[]{6174}, - new uint[]{6175}, - new uint[]{6175}, - new uint[]{6176}, - new uint[]{7221}, - new uint[]{6173}, - new uint[]{6174}, - new uint[]{6175}, - new uint[]{6175}, - new uint[]{6176}, - new uint[]{6991}, - new uint[]{5574}, - new uint[]{5239}, - new uint[]{6992}, - new uint[]{108}, - new uint[]{6891}, - new uint[]{5834}, - new uint[]{5834}, - new uint[]{5834}, - new uint[]{7127}, - new uint[]{7126}, - new uint[]{7125}, - new uint[]{7127}, - new uint[]{7126}, - new uint[]{7125}, - new uint[]{7124}, - new uint[]{7122}, - new uint[]{7123}, - Array.Empty(), - new uint[]{7126}, - new uint[]{7126}, - new uint[]{7124}, - new uint[]{7122}, - new uint[]{7123}, - new uint[]{7120}, - new uint[]{7073}, - new uint[]{7127}, - new uint[]{7131}, - new uint[]{7131}, - new uint[]{7130}, - new uint[]{7131}, - new uint[]{7080}, - new uint[]{7081}, - new uint[]{7082}, - new uint[]{7083}, - new uint[]{7084}, - new uint[]{7085}, - new uint[]{7086}, - new uint[]{7087}, - new uint[]{7088}, - new uint[]{7089}, - new uint[]{7090}, - new uint[]{6993}, - new uint[]{7119}, - new uint[]{7093}, - new uint[]{7093}, - new uint[]{7169}, - new uint[]{7170}, - new uint[]{7171}, - new uint[]{7172}, - new uint[]{7149}, - new uint[]{7149}, - new uint[]{7149}, - new uint[]{7177}, - new uint[]{7178}, - new uint[]{7149}, - new uint[]{7149}, - new uint[]{7149}, - new uint[]{7145}, - new uint[]{7146}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7129}, - new uint[]{7129}, - new uint[]{7091}, - new uint[]{7152}, - new uint[]{7153}, - new uint[]{7154}, - new uint[]{7155}, - Array.Empty(), - new uint[]{7156}, - new uint[]{7158}, - new uint[]{7160}, - new uint[]{7161}, - new uint[]{7157}, - new uint[]{7017}, - new uint[]{7018}, - new uint[]{7019}, - new uint[]{7020}, - new uint[]{7021}, - new uint[]{7022}, - new uint[]{7023}, - new uint[]{7024}, - new uint[]{7025}, - new uint[]{7026}, - new uint[]{7027}, - new uint[]{7028}, - new uint[]{7029}, - new uint[]{7030}, - new uint[]{7031}, - new uint[]{7032}, - new uint[]{7033}, - new uint[]{7034}, - new uint[]{7035}, - new uint[]{7152}, - new uint[]{7173}, - new uint[]{7174}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{7176}, - new uint[]{7150}, - new uint[]{7142}, - new uint[]{7141}, - new uint[]{6057}, - new uint[]{1486}, - new uint[]{3374}, - new uint[]{5625}, - new uint[]{5625}, - new uint[]{7110}, - new uint[]{7140}, - new uint[]{7139}, - Array.Empty(), - new uint[]{7087}, - new uint[]{108}, - new uint[]{7128}, - new uint[]{4130}, - new uint[]{5978}, - new uint[]{6381}, - new uint[]{7036}, - new uint[]{7054}, - new uint[]{4148}, - new uint[]{5579}, - new uint[]{5577}, - new uint[]{5581}, - new uint[]{7137}, - new uint[]{7138}, - new uint[]{6203}, - new uint[]{7036}, - new uint[]{7136}, - new uint[]{7126}, - Array.Empty(), - new uint[]{6984}, - new uint[]{7037}, - new uint[]{7038}, - new uint[]{7039}, - new uint[]{7040}, - new uint[]{6993}, - new uint[]{7041}, - new uint[]{7042}, - new uint[]{6993}, - new uint[]{7043}, - new uint[]{6985}, - new uint[]{7044}, - new uint[]{7045}, - new uint[]{7046}, - new uint[]{7047}, - new uint[]{108}, - new uint[]{108}, - new uint[]{7052}, - new uint[]{7048}, - new uint[]{17}, - new uint[]{7096}, - new uint[]{7097}, - new uint[]{7098}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7200}, - new uint[]{7201}, - new uint[]{7202}, - new uint[]{7203}, - new uint[]{7204}, - new uint[]{7050}, - new uint[]{7049}, - new uint[]{6993}, - new uint[]{7222}, - new uint[]{7051}, - new uint[]{7222}, - new uint[]{6982}, - new uint[]{7053}, - new uint[]{6983}, - new uint[]{7186}, - new uint[]{6982}, - new uint[]{6983}, - new uint[]{7126}, - new uint[]{7126}, - new uint[]{7053}, - new uint[]{7187}, - new uint[]{6982}, - new uint[]{7162}, - new uint[]{7161}, - new uint[]{7161}, - new uint[]{6983}, - new uint[]{7188}, - new uint[]{7163}, - new uint[]{7164}, - new uint[]{7165}, - new uint[]{7189}, - new uint[]{6982}, - new uint[]{7185}, - new uint[]{7159}, - new uint[]{7190}, - new uint[]{6982}, - new uint[]{7185}, - new uint[]{7191}, - new uint[]{7185}, - new uint[]{7192}, - new uint[]{6988}, - new uint[]{6983}, - new uint[]{6988}, - new uint[]{7193}, - new uint[]{7194}, - new uint[]{7195}, - new uint[]{6993}, - new uint[]{7196}, - new uint[]{7197}, - new uint[]{6983}, - new uint[]{7198}, - new uint[]{7199}, - new uint[]{6982}, - new uint[]{6983}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{7225}, - new uint[]{7225}, - new uint[]{1644}, - new uint[]{1645, 1646}, - new uint[]{1647}, - new uint[]{1648}, - new uint[]{2091}, - new uint[]{1801}, - new uint[]{1803}, - new uint[]{1804}, - new uint[]{1185}, - new uint[]{1186}, - new uint[]{2143}, - new uint[]{5563}, - new uint[]{2137}, - new uint[]{2138}, - new uint[]{2324}, - new uint[]{7206}, - new uint[]{7207}, - new uint[]{7208}, - new uint[]{7209}, - new uint[]{7210}, - new uint[]{7211}, - new uint[]{108}, - new uint[]{7212}, - new uint[]{7213}, - new uint[]{7214}, - new uint[]{7215}, - new uint[]{7216}, - new uint[]{7217}, - new uint[]{7218}, - new uint[]{7219}, - new uint[]{7220}, - Array.Empty(), - new uint[]{7205}, - new uint[]{7229}, - Array.Empty(), - new uint[]{7231}, - new uint[]{7232}, - new uint[]{7227}, - new uint[]{7228}, - new uint[]{7476}, - new uint[]{7537}, - new uint[]{7477}, - new uint[]{7226}, - new uint[]{7233}, - new uint[]{7233}, - new uint[]{7234}, - new uint[]{7234}, - new uint[]{7229}, - new uint[]{7230}, - Array.Empty(), - new uint[]{7227}, - new uint[]{7228}, - new uint[]{7476}, - new uint[]{7537}, - new uint[]{7477}, - new uint[]{7226}, - new uint[]{7233}, - new uint[]{7234}, - new uint[]{7206}, - new uint[]{1185}, - new uint[]{1801}, - new uint[]{1644}, - new uint[]{887}, - new uint[]{7478}, - new uint[]{7479}, - new uint[]{7070}, - new uint[]{7100}, - new uint[]{7235}, - new uint[]{108}, - new uint[]{6945}, - new uint[]{7494}, - new uint[]{7236}, - new uint[]{7245}, - new uint[]{7246}, - new uint[]{7247}, - new uint[]{7237}, - new uint[]{7238}, - new uint[]{7239}, - new uint[]{7244}, - new uint[]{6943}, - new uint[]{7403}, - new uint[]{7404}, - new uint[]{7405}, - new uint[]{7406}, - new uint[]{7407}, - new uint[]{7408}, - new uint[]{7409}, - new uint[]{7410}, - new uint[]{7411}, - new uint[]{7412}, - new uint[]{7413}, - new uint[]{7414}, - new uint[]{7415}, - new uint[]{7413}, - new uint[]{7416}, - new uint[]{7417}, - new uint[]{7418}, - new uint[]{7419}, - new uint[]{7420}, - new uint[]{7421}, - new uint[]{7422}, - new uint[]{7423}, - new uint[]{7231}, - new uint[]{7424}, - new uint[]{7425}, - new uint[]{7426}, - new uint[]{7427}, - new uint[]{7428}, - new uint[]{7429}, - new uint[]{7430}, - new uint[]{7431}, - new uint[]{7432}, - new uint[]{7433}, - new uint[]{7434}, - new uint[]{7435}, - new uint[]{7436}, - new uint[]{7437}, - new uint[]{7438}, - new uint[]{7439}, - new uint[]{7440}, - new uint[]{7441}, - new uint[]{7442}, - new uint[]{7443}, - new uint[]{7444}, - new uint[]{7445}, - new uint[]{7446}, - new uint[]{7447}, - new uint[]{7448}, - new uint[]{7449}, - new uint[]{7450}, - new uint[]{7451}, - new uint[]{7452}, - new uint[]{7453}, - new uint[]{7454}, - new uint[]{7455}, - new uint[]{7456}, - new uint[]{7457}, - new uint[]{7458}, - new uint[]{7459}, - new uint[]{7460}, - new uint[]{7461}, - new uint[]{7462}, - new uint[]{7463}, - new uint[]{7582}, - new uint[]{7244}, - new uint[]{7244}, - new uint[]{7223}, - new uint[]{7224}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{7262}, - new uint[]{7263}, - new uint[]{7264}, - new uint[]{7265}, - new uint[]{7266}, - new uint[]{7267}, - new uint[]{7268}, - new uint[]{7269}, - new uint[]{7270}, - new uint[]{7271}, - new uint[]{7272}, - new uint[]{7273}, - new uint[]{7274}, - new uint[]{7275}, - new uint[]{7276}, - new uint[]{7277}, - new uint[]{7278}, - new uint[]{7279}, - new uint[]{7280}, - new uint[]{7281}, - new uint[]{7282}, - new uint[]{7283}, - new uint[]{7284}, - new uint[]{7285}, - new uint[]{7286}, - new uint[]{7287}, - new uint[]{7288}, - new uint[]{7289}, - new uint[]{7290}, - new uint[]{7291}, - new uint[]{7292}, - new uint[]{7293}, - new uint[]{7294}, - new uint[]{7295}, - new uint[]{7296}, - new uint[]{7297}, - new uint[]{7298}, - new uint[]{7299}, - new uint[]{7300}, - new uint[]{7301}, - new uint[]{7302}, - new uint[]{7303}, - new uint[]{7304}, - new uint[]{7305}, - new uint[]{7306}, - new uint[]{7307}, - new uint[]{7308}, - new uint[]{7309}, - new uint[]{7310}, - new uint[]{7305}, - new uint[]{7312}, - new uint[]{7313}, - new uint[]{7314}, - new uint[]{7315}, - new uint[]{7316}, - new uint[]{7317}, - new uint[]{7318}, - new uint[]{7319}, - new uint[]{7320}, - new uint[]{7321}, - new uint[]{7322}, - new uint[]{7323}, - new uint[]{7324}, - new uint[]{7325}, - new uint[]{7326}, - new uint[]{7327}, - new uint[]{7328}, - new uint[]{7329}, - new uint[]{7330}, - new uint[]{7331}, - new uint[]{7332}, - new uint[]{7333}, - new uint[]{7334}, - new uint[]{7335}, - new uint[]{7336}, - new uint[]{7337}, - new uint[]{7338}, - new uint[]{7339}, - new uint[]{7340}, - new uint[]{7341}, - new uint[]{7265}, - new uint[]{7342}, - new uint[]{7343}, - new uint[]{7344}, - new uint[]{7345}, - new uint[]{7346}, - new uint[]{7347}, - new uint[]{7348}, - new uint[]{7349}, - new uint[]{7350}, - new uint[]{7351}, - new uint[]{7352}, - new uint[]{7353}, - new uint[]{7354}, - new uint[]{7355}, - new uint[]{7356}, - new uint[]{7357}, - new uint[]{7358}, - new uint[]{7359}, - new uint[]{7360}, - new uint[]{7361}, - new uint[]{7362}, - new uint[]{7363}, - new uint[]{7364}, - new uint[]{7365}, - new uint[]{7366}, - new uint[]{7367}, - new uint[]{7368}, - new uint[]{7369}, - new uint[]{7370}, - new uint[]{7371}, - new uint[]{7372}, - new uint[]{7373}, - new uint[]{7374}, - new uint[]{7375}, - new uint[]{7376}, - new uint[]{7377}, - new uint[]{7378}, - new uint[]{7379}, - new uint[]{7380}, - new uint[]{7381}, - new uint[]{7382}, - new uint[]{7584}, - new uint[]{7384}, - new uint[]{7385}, - new uint[]{7386}, - new uint[]{7387}, - new uint[]{7388}, - new uint[]{7389}, - new uint[]{7390}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7258}, - new uint[]{7259}, - new uint[]{7260}, - new uint[]{7260}, - new uint[]{7261}, - Array.Empty(), - new uint[]{7240}, - new uint[]{6996}, - new uint[]{7241}, - new uint[]{101, 108, 510, 548, 557, 718, 1279, 1680, 2116, 2118, 2134, 2135, 2136, 2137, 2143, 2160, 3040, 3042, 3044, 3046, 3047, 3330, 3374, 3455, 3458, 3642, 4739, 6039, 6148, 6153, 6649, 6650, 6651, 6652, 6853, 7570, 7585, 7586, 7587, 7588, 7589, 7590, 7591, 7593, 7594, 7595, 7597, 7599, 7600, 7601, 7627, 7628, 7629, 7641, 7657, 7659, 7660, 7662, 7667, 7672, 7691, 7695, 7699, 7702, 7855, 7856, 7857, 7858, 7885, 7886, 7888, 7899, 7900, 7906, 7911, 7912, 7914, 7919, 7922, 7931, 7939, 7947, 7950, 8084, 8087, 8099, 8102, 8107, 8109, 8113, 8117, 8121, 8123, 8124, 8125, 8128, 8129, 8141, 8146, 8162, 8165, 8167, 8201, 8202, 8210, 8211, 8231, 8232, 8233, 8235, 8236, 8250, 8252, 8258, 8260, 8261, 8262, 8267, 8270, 8272, 8273, 8300, 8301, 8338, 8339, 8345, 8350, 8352, 8353, 8361, 8363, 8374, 8379, 8381, 8382, 8393, 8397, 8486, 8826, 8953, 8955, 9029, 9041, 9044, 9046, 9063, 9138, 9140, 9141, 9142, 9143, 9147, 9152, 9153, 9155, 9161, 9162, 9189, 9208, 9220, 9230, 9231, 9233, 9239, 9241, 9245, 9250, 9260, 9261, 9263, 9264, 9265, 9270, 9281, 9287, 9288, 9289, 9296, 9298, 9300, 9331, 9340, 9341, 9353, 9355, 9364, 9384, 9390, 9391, 9394, 9396, 9398, 9400, 9405, 9407, 9408, 9409, 9411, 9416, 9417, 9419, 9422, 9424, 9426, 9427, 9436, 9439, 9442, 9458, 9461, 9462, 9505, 9508, 9511, 9617, 9618, 9642, 9644, 9646, 9648, 9650, 9652, 9678, 9693, 9694, 9695, 9696, 9707, 9709, 9735, 9738, 9741, 9751, 9755, 9759, 9764, 9768, 9769, 9776, 9778, 9780, 9782, 9784, 9786, 9788, 9790, 9793, 9795, 9797, 9806, 9807, 9808, 9811, 9812, 9813, 9834, 9838, 9853, 9854, 9855, 9856, 9857, 9858, 9859, 9860, 9861, 9862, 9863, 9881, 9902, 9908, 9909, 9918, 9921, 9922, 9925, 9929, 9930, 9931, 9935, 9937, 9941, 9942, 9945, 9946, 9948, 9949, 9950, 9953, 9955, 9958, 9961, 9964, 9965, 9966, 9969, 9973, 9988, 9989, 9992, 10004, 10006, 10007, 10013, 10037, 10041, 10059, 10064, 10067, 10075, 10077, 10095, 10189, 10192, 10205, 10207, 10212, 10246, 10247, 10256, 10259, 10279, 10282, 10285, 10288, 10290, 10292, 10293, 10298, 10313, 10314, 10315, 10316, 10317, 10331, 10333, 10336, 10337, 10341, 10345, 10348, 10393, 10394, 10395, 10396, 10399, 10401, 10403, 10404, 10438, 10445, 10446, 10448, 10453, 10456, 10489, 10559, 10572, 10581, 10586, 10647, 10717, 10718, 10719, 10720, 10730, 10731, 10732, 10733, 10742, 10744, 10831, 10832, 10905, 10932, 10933, 10935, 11070, 11195, 11218, 11227, 11238, 11239, 11241, 11253, 11254, 11274, 11277, 11278, 11280, 11281, 11286, 11288, 11289, 11292, 11293, 11302, 11322, 11352, 11369, 11372, 11374, 11378, 11381, 11384, 11387, 11393, 11399, 11402, 11404, 11405, 11407, 11413, 11419, 11440, 11442, 11517}, - new uint[]{7396}, - new uint[]{7397}, - new uint[]{7398}, - new uint[]{7250}, - new uint[]{7251}, - new uint[]{7252}, - new uint[]{5978}, - new uint[]{7253}, - new uint[]{7254}, - new uint[]{7254}, - new uint[]{7255}, - new uint[]{7256}, - new uint[]{7402}, - new uint[]{7402}, - new uint[]{7257}, - new uint[]{7401}, - new uint[]{7248}, - new uint[]{7250}, - new uint[]{7242}, - new uint[]{7243}, - new uint[]{7240}, - new uint[]{7392}, - new uint[]{7392}, - new uint[]{7392}, - new uint[]{7393}, - new uint[]{7393}, - new uint[]{7393}, - new uint[]{7394}, - new uint[]{7394}, - new uint[]{7394}, - new uint[]{7394}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - new uint[]{7610}, - Array.Empty(), - new uint[]{7526}, - new uint[]{7527}, - new uint[]{7528}, - new uint[]{7512}, - new uint[]{7513}, - new uint[]{7512}, - new uint[]{7464}, - new uint[]{7464}, - new uint[]{7464}, - new uint[]{7464}, - new uint[]{7465}, - new uint[]{7465}, - new uint[]{7465}, - new uint[]{7465}, - new uint[]{7466}, - new uint[]{7466}, - new uint[]{7466}, - new uint[]{7466}, - new uint[]{7467}, - new uint[]{7467}, - new uint[]{7467}, - new uint[]{7467}, - new uint[]{7468}, - new uint[]{7468}, - new uint[]{7468}, - new uint[]{7468}, - new uint[]{7468}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7471}, - new uint[]{7472}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7471}, - new uint[]{7472}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7471}, - new uint[]{7472}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7471}, - new uint[]{7472}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7471}, - new uint[]{7472}, - new uint[]{7469}, - new uint[]{7520}, - new uint[]{7521}, - new uint[]{7391}, - new uint[]{7516}, - new uint[]{7517}, - new uint[]{7517}, - new uint[]{7399}, - new uint[]{7497}, - new uint[]{7498}, - new uint[]{7514}, - new uint[]{7515}, - new uint[]{7473}, - new uint[]{7508}, - new uint[]{7509}, - new uint[]{7510}, - new uint[]{7511}, - new uint[]{7508}, - new uint[]{7508}, - new uint[]{7495}, - new uint[]{7669}, - new uint[]{7670}, - new uint[]{7671}, - new uint[]{7701}, - new uint[]{7496}, - new uint[]{7495}, - new uint[]{7524}, - new uint[]{7525}, - new uint[]{7524}, - new uint[]{7518}, - new uint[]{7519}, - new uint[]{7518}, - new uint[]{7518}, - new uint[]{7518}, - new uint[]{7503}, - new uint[]{7504}, - new uint[]{7503}, - new uint[]{7523}, - Array.Empty(), - new uint[]{7523}, - new uint[]{7523}, - new uint[]{7524}, - new uint[]{7475}, - new uint[]{7522}, - new uint[]{7249}, - new uint[]{7499}, - new uint[]{7501}, - new uint[]{7502}, - new uint[]{7499}, - new uint[]{7505}, - new uint[]{7505}, - new uint[]{7505}, - new uint[]{7505}, - new uint[]{7533}, - new uint[]{7647}, - new uint[]{7648}, - new uint[]{7649}, - new uint[]{7650}, - new uint[]{7651}, - new uint[]{7652}, - new uint[]{7653}, - new uint[]{7654}, - new uint[]{7655}, - new uint[]{7656}, - new uint[]{7658}, - new uint[]{7506}, - new uint[]{7507}, - new uint[]{7475}, - new uint[]{7567}, - new uint[]{7248}, - new uint[]{7534}, - new uint[]{7248}, - new uint[]{7532}, - new uint[]{7531}, - new uint[]{7536}, - new uint[]{7480}, - new uint[]{7483}, - new uint[]{7484}, - new uint[]{7487}, - new uint[]{7488}, - new uint[]{7474}, - new uint[]{7481}, - new uint[]{7482}, - new uint[]{7485}, - new uint[]{7486}, - new uint[]{7489}, - new uint[]{7490}, - new uint[]{7491}, - new uint[]{7492}, - new uint[]{7493}, - new uint[]{7535}, - new uint[]{7529}, - new uint[]{7530}, - new uint[]{7568}, - new uint[]{7569}, - new uint[]{3045}, - new uint[]{7529}, - new uint[]{7529}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{7583}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7667}, - new uint[]{7668}, - new uint[]{7675}, - new uint[]{7676}, - new uint[]{7677}, - new uint[]{7678}, - new uint[]{7679}, - new uint[]{7680}, - new uint[]{7681}, - new uint[]{7682}, - new uint[]{7683}, - new uint[]{7684}, - new uint[]{7685}, - new uint[]{7686}, - new uint[]{7687}, - new uint[]{7688}, - new uint[]{7660}, - new uint[]{7661}, - new uint[]{7662}, - new uint[]{7665}, - new uint[]{8099}, - new uint[]{7663}, - new uint[]{7659}, - new uint[]{7650}, - new uint[]{7672}, - new uint[]{7673}, - new uint[]{7674}, - new uint[]{7672}, - new uint[]{7691}, - new uint[]{7694}, - new uint[]{7692}, - new uint[]{7693}, - new uint[]{7691}, - new uint[]{7694}, - new uint[]{7692}, - new uint[]{7693}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7889}, - new uint[]{7890}, - Array.Empty(), - new uint[]{7641}, - new uint[]{7641}, - new uint[]{7643}, - Array.Empty(), - new uint[]{7645}, - new uint[]{7646}, - new uint[]{7641}, - new uint[]{7641}, - new uint[]{7643}, - Array.Empty(), - new uint[]{7645}, - new uint[]{7646}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7702}, - new uint[]{7725}, - new uint[]{7703}, - new uint[]{7705}, - Array.Empty(), - Array.Empty(), - new uint[]{7702}, - new uint[]{7707}, - new uint[]{7708}, - new uint[]{7709}, - new uint[]{7710}, - new uint[]{7702}, - new uint[]{7725}, - new uint[]{7703}, - new uint[]{7704}, - new uint[]{7705}, - Array.Empty(), - Array.Empty(), - new uint[]{7702}, - new uint[]{7711}, - new uint[]{7714}, - new uint[]{7713}, - new uint[]{7712}, - Array.Empty(), - new uint[]{7695}, - new uint[]{7696}, - new uint[]{7697}, - new uint[]{7698}, - Array.Empty(), - new uint[]{7700}, - new uint[]{7695}, - Array.Empty(), - new uint[]{7699}, - Array.Empty(), - new uint[]{7633}, - new uint[]{7635}, - new uint[]{7633}, - new uint[]{7635}, - new uint[]{7636}, - new uint[]{7637}, - new uint[]{7638}, - new uint[]{7639}, - new uint[]{7726}, - new uint[]{7727}, - new uint[]{7729}, - new uint[]{7731}, - new uint[]{7736}, - new uint[]{7739}, - new uint[]{7740}, - new uint[]{7742}, - new uint[]{7746}, - new uint[]{7748}, - new uint[]{7750}, - new uint[]{7753}, - new uint[]{7754}, - new uint[]{7756}, - new uint[]{7759}, - new uint[]{7760}, - new uint[]{7763}, - Array.Empty(), - new uint[]{8345}, - new uint[]{8344}, - new uint[]{8343}, - new uint[]{8342}, - new uint[]{8345}, - new uint[]{8344}, - new uint[]{8343}, - new uint[]{8341}, - new uint[]{8341}, - new uint[]{8272}, - new uint[]{7718}, - new uint[]{6152}, - new uint[]{6153}, - new uint[]{7716}, - new uint[]{6148}, - new uint[]{7771}, - new uint[]{7772}, - new uint[]{7772}, - new uint[]{7773}, - new uint[]{7773}, - new uint[]{7771}, - new uint[]{7772}, - new uint[]{7771}, - new uint[]{7773}, - new uint[]{7773}, - new uint[]{7771}, - new uint[]{7772}, - new uint[]{7774}, - new uint[]{7771}, - new uint[]{7771}, - new uint[]{7774}, - new uint[]{7772}, - new uint[]{7774}, - new uint[]{7773}, - new uint[]{7772}, - new uint[]{7771}, - new uint[]{7775}, - new uint[]{7775}, - new uint[]{7775}, - new uint[]{7776}, - new uint[]{7776}, - new uint[]{7776}, - new uint[]{7777}, - new uint[]{7778}, - new uint[]{7777}, - new uint[]{7779}, - new uint[]{7779}, - new uint[]{7779}, - new uint[]{7780}, - new uint[]{7780}, - new uint[]{7781}, - new uint[]{7783}, - new uint[]{7782}, - new uint[]{7782}, - new uint[]{7785}, - new uint[]{7784}, - new uint[]{7784}, - new uint[]{7786}, - new uint[]{7787}, - new uint[]{7788}, - new uint[]{7789}, - new uint[]{7790}, - new uint[]{7791}, - new uint[]{7792}, - new uint[]{7793}, - new uint[]{7794}, - new uint[]{7795}, - new uint[]{7796}, - new uint[]{7797}, - new uint[]{7798}, - new uint[]{7799}, - new uint[]{7800}, - new uint[]{7801}, - new uint[]{7802}, - new uint[]{7803}, - new uint[]{7804}, - new uint[]{7805}, - new uint[]{7806}, - new uint[]{7807}, - new uint[]{7808}, - new uint[]{7809}, - new uint[]{7810}, - new uint[]{7811}, - new uint[]{7812}, - new uint[]{7813}, - new uint[]{7814}, - new uint[]{7815}, - new uint[]{7816}, - new uint[]{7817}, - new uint[]{7818}, - new uint[]{7819}, - new uint[]{7820}, - new uint[]{7821}, - new uint[]{7822}, - new uint[]{7823}, - new uint[]{7824}, - new uint[]{7825}, - new uint[]{7826}, - new uint[]{7827}, - new uint[]{7828}, - new uint[]{7829}, - new uint[]{7830}, - new uint[]{7831}, - new uint[]{7832}, - new uint[]{7833}, - new uint[]{7834}, - new uint[]{7835}, - new uint[]{7836}, - new uint[]{7837}, - new uint[]{7838}, - new uint[]{7839}, - new uint[]{7840}, - new uint[]{7841}, - new uint[]{7842}, - new uint[]{7843}, - new uint[]{7844}, - new uint[]{7845}, - new uint[]{7846}, - new uint[]{7847}, - new uint[]{7848}, - new uint[]{7849}, - new uint[]{7658}, - new uint[]{7578}, - new uint[]{7579}, - new uint[]{7580}, - new uint[]{7581}, - new uint[]{7939}, - Array.Empty(), - new uint[]{7856}, - new uint[]{108}, - new uint[]{7871}, - new uint[]{7872}, - new uint[]{7873}, - new uint[]{7874}, - new uint[]{8264}, - new uint[]{7876}, - new uint[]{7877}, - new uint[]{7878}, - new uint[]{7879}, - new uint[]{7880}, - new uint[]{7881}, - new uint[]{7882}, - Array.Empty(), - new uint[]{7884}, - new uint[]{7885}, - new uint[]{7886}, - new uint[]{7887}, - new uint[]{7888}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{6154}, - new uint[]{7715}, - new uint[]{6196}, - new uint[]{6336}, - new uint[]{2823}, - new uint[]{7718}, - new uint[]{7585}, - new uint[]{7586}, - new uint[]{7587}, - new uint[]{7588}, - new uint[]{7589}, - new uint[]{7590}, - new uint[]{7591}, - new uint[]{7627}, - new uint[]{7593}, - new uint[]{7622}, - new uint[]{7595}, - new uint[]{7596}, - new uint[]{7597}, - new uint[]{7598}, - new uint[]{7599}, - new uint[]{7600}, - new uint[]{7601}, - new uint[]{7602}, - new uint[]{7603}, - new uint[]{7604}, - new uint[]{7605}, - new uint[]{7606}, - new uint[]{7607}, - new uint[]{7608}, - new uint[]{7609}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7759}, - new uint[]{8265}, - new uint[]{7732}, - new uint[]{7733}, - Array.Empty(), - Array.Empty(), - new uint[]{7731}, - new uint[]{7731}, - new uint[]{9147}, - new uint[]{7628}, - new uint[]{7727}, - new uint[]{7728}, - new uint[]{7620}, - new uint[]{7592}, - new uint[]{7594}, - new uint[]{7621}, - new uint[]{7743}, - new uint[]{7744}, - new uint[]{7629}, - new uint[]{7742}, - new uint[]{7630}, - new uint[]{7747}, - new uint[]{7623}, - new uint[]{7631}, - new uint[]{7719}, - new uint[]{7745}, - new uint[]{7720}, - new uint[]{7721}, - new uint[]{7730}, - new uint[]{7756}, - new uint[]{7757}, - new uint[]{7758}, - new uint[]{7722}, - new uint[]{7702}, - new uint[]{7723}, - new uint[]{7598}, - new uint[]{7598}, - new uint[]{7598}, - new uint[]{7764}, - new uint[]{7724}, - new uint[]{7248}, - new uint[]{7765}, - new uint[]{7766}, - Array.Empty(), - new uint[]{7751}, - new uint[]{7752}, - new uint[]{7248}, - new uint[]{7768}, - new uint[]{7769}, - new uint[]{7770}, - new uint[]{7767}, - Array.Empty(), - new uint[]{7750}, - new uint[]{7750}, - new uint[]{7750}, - new uint[]{7748}, - new uint[]{7749}, - Array.Empty(), - Array.Empty(), - new uint[]{7248}, - Array.Empty(), - new uint[]{7737}, - new uint[]{7738}, - Array.Empty(), - new uint[]{7761}, - new uint[]{7762}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{7741}, - new uint[]{7696}, - new uint[]{7696}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{7755}, - new uint[]{7718}, - new uint[]{7642}, - new uint[]{7642}, - new uint[]{7726}, - Array.Empty(), - new uint[]{7850}, - new uint[]{7851}, - new uint[]{7739}, - new uint[]{7739}, - new uint[]{7852}, - new uint[]{7739}, - new uint[]{7769}, - new uint[]{7909}, - new uint[]{7853}, - new uint[]{9066}, - new uint[]{108}, - new uint[]{108}, - new uint[]{7664}, - new uint[]{7857}, - new uint[]{7858}, - new uint[]{7861}, - new uint[]{7862}, - Array.Empty(), - Array.Empty(), - new uint[]{7861}, - new uint[]{7664}, - new uint[]{7919}, - new uint[]{7920}, - new uint[]{7921}, - Array.Empty(), - new uint[]{8336}, - new uint[]{7919}, - new uint[]{8076}, - new uint[]{8077}, - Array.Empty(), - new uint[]{8078}, - new uint[]{8079}, - new uint[]{8080}, - new uint[]{8081}, - new uint[]{8082}, - new uint[]{8083}, - Array.Empty(), - Array.Empty(), - new uint[]{8086}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{8089}, - new uint[]{8090}, - new uint[]{8092}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{8085}, - Array.Empty(), - new uint[]{8084}, - new uint[]{8087}, - new uint[]{8088}, - new uint[]{7899}, - new uint[]{7900}, - new uint[]{7901}, - new uint[]{7855}, - new uint[]{7946}, - new uint[]{7976}, - new uint[]{7977}, - new uint[]{7978}, - new uint[]{7979}, - new uint[]{7980}, - new uint[]{7947}, - new uint[]{7948}, - new uint[]{7950}, - new uint[]{7949}, - new uint[]{7951}, - new uint[]{7952}, - new uint[]{7953}, - new uint[]{7981}, - new uint[]{7982}, - new uint[]{7983}, - new uint[]{7984}, - new uint[]{7922}, - new uint[]{7930}, - new uint[]{7923}, - new uint[]{7924}, - new uint[]{7925}, - new uint[]{7927}, - new uint[]{7928}, - new uint[]{7929}, - new uint[]{7922}, - new uint[]{7930}, - new uint[]{7927}, - new uint[]{7928}, - new uint[]{7926}, - new uint[]{7929}, - new uint[]{7906}, - new uint[]{7891}, - new uint[]{7892}, - new uint[]{7570}, - new uint[]{7571}, - new uint[]{7572}, - new uint[]{7573}, - new uint[]{7574}, - new uint[]{7575}, - new uint[]{7576}, - new uint[]{7657}, - new uint[]{7973}, - new uint[]{7974}, - new uint[]{7975}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{7879}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{7875}, - new uint[]{8922}, - new uint[]{7871}, - new uint[]{7872}, - Array.Empty(), - new uint[]{7874}, - new uint[]{7875}, - new uint[]{7879}, - new uint[]{7880}, - new uint[]{108}, - new uint[]{7973}, - new uint[]{10216}, - new uint[]{8922}, - new uint[]{750}, - new uint[]{7931}, - new uint[]{7932}, - new uint[]{7933}, - new uint[]{7934}, - new uint[]{7935}, - new uint[]{7936}, - new uint[]{7937}, - new uint[]{8133}, - Array.Empty(), - new uint[]{7976}, - new uint[]{7981, 7982}, - new uint[]{7915, 7919}, - new uint[]{108, 7916}, - new uint[]{108, 7917}, - new uint[]{7865}, - new uint[]{7866}, - new uint[]{7867}, - new uint[]{7868}, - new uint[]{7869}, - new uint[]{7870}, - new uint[]{8922}, - new uint[]{7912}, - new uint[]{7913}, - new uint[]{7914}, - new uint[]{7911}, - new uint[]{7918}, - new uint[]{7910}, - new uint[]{7908}, - new uint[]{7985}, - new uint[]{7986}, - new uint[]{7987}, - new uint[]{7988}, - new uint[]{7989}, - new uint[]{7990}, - new uint[]{7991}, - new uint[]{8922}, - new uint[]{7992}, - new uint[]{7993}, - new uint[]{7994}, - new uint[]{7995}, - new uint[]{7996}, - new uint[]{7997}, - new uint[]{7998}, - new uint[]{7999}, - new uint[]{8000}, - new uint[]{8001}, - new uint[]{8002}, - new uint[]{8003}, - new uint[]{8004}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{8922}, - new uint[]{7930}, - new uint[]{7930}, - new uint[]{7930}, - new uint[]{7930}, - new uint[]{7968}, - new uint[]{7969}, - new uint[]{7968}, - new uint[]{7970}, - new uint[]{7971}, - new uint[]{7970}, - new uint[]{7970}, - new uint[]{7968}, - new uint[]{7970}, - new uint[]{7871}, - new uint[]{7872}, - new uint[]{7873}, - new uint[]{7874}, - new uint[]{7875}, - new uint[]{7879}, - new uint[]{8922}, - new uint[]{8922}, - new uint[]{7972}, - new uint[]{7902}, - new uint[]{7903}, - new uint[]{5915}, - new uint[]{5915}, - new uint[]{7904}, - new uint[]{7970}, - new uint[]{7930}, - Array.Empty(), - Array.Empty(), - new uint[]{7985}, - new uint[]{7986}, - new uint[]{7991}, - new uint[]{8129}, - new uint[]{8252}, - new uint[]{8250}, - new uint[]{8249}, - new uint[]{8248}, - new uint[]{7915, 7916, 7917}, - new uint[]{8251}, - new uint[]{8129}, - new uint[]{8129}, - new uint[]{8132}, - new uint[]{8130}, - new uint[]{9040}, - Array.Empty(), - new uint[]{8060}, - new uint[]{8061}, - new uint[]{8061}, - new uint[]{8063}, - new uint[]{7664}, - new uint[]{6039}, - new uint[]{7537}, - new uint[]{6039}, - new uint[]{7537}, - Array.Empty(), - new uint[]{6042}, - new uint[]{6040}, - Array.Empty(), - new uint[]{6040}, - new uint[]{3293}, - new uint[]{3211}, - new uint[]{5574}, - new uint[]{7036}, - new uint[]{7867}, - new uint[]{6094}, - new uint[]{6094}, - new uint[]{7941}, - new uint[]{8015}, - new uint[]{8011}, - new uint[]{8014}, - new uint[]{8008}, - new uint[]{8012}, - new uint[]{8013}, - new uint[]{8009}, - new uint[]{8010}, - new uint[]{8016}, - new uint[]{8017}, - new uint[]{8018}, - new uint[]{8019}, - new uint[]{8020}, - new uint[]{8021}, - new uint[]{8022}, - new uint[]{8023}, - new uint[]{8024}, - new uint[]{8025}, - new uint[]{8026}, - new uint[]{8027}, - new uint[]{8028}, - new uint[]{8029}, - new uint[]{8030}, - new uint[]{8031}, - new uint[]{8032}, - new uint[]{8033}, - new uint[]{8034}, - new uint[]{8035}, - new uint[]{8036}, - new uint[]{8037}, - new uint[]{8038}, - new uint[]{8039}, - new uint[]{8040}, - new uint[]{8041}, - new uint[]{8042}, - new uint[]{8043}, - new uint[]{8044}, - new uint[]{8045}, - new uint[]{8046}, - new uint[]{8047}, - new uint[]{8048}, - new uint[]{8049}, - new uint[]{8050}, - new uint[]{8051}, - new uint[]{5945}, - new uint[]{8052}, - new uint[]{8053}, - new uint[]{8054}, - new uint[]{7176}, - new uint[]{7471}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7176}, - new uint[]{7471}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7176}, - new uint[]{7471}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{7176}, - new uint[]{7471}, - new uint[]{7469}, - new uint[]{7470}, - new uint[]{8055}, - new uint[]{8055}, - new uint[]{8055}, - new uint[]{8056}, - new uint[]{8056}, - new uint[]{8056}, - new uint[]{8057}, - new uint[]{8057}, - new uint[]{8057}, - new uint[]{8058}, - new uint[]{8058}, - new uint[]{8058}, - new uint[]{8059}, - new uint[]{8059}, - new uint[]{8059}, - new uint[]{8059}, - new uint[]{7871}, - new uint[]{7872}, - new uint[]{7873}, - new uint[]{7874}, - new uint[]{8487}, - new uint[]{8064}, - new uint[]{8065}, - new uint[]{8112}, - new uint[]{8112}, - new uint[]{8113}, - new uint[]{8104}, - new uint[]{8105}, - new uint[]{8106}, - new uint[]{8107}, - new uint[]{8109}, - new uint[]{8110}, - new uint[]{8111}, - new uint[]{8122}, - new uint[]{8101}, - new uint[]{8123}, - new uint[]{8090}, - new uint[]{8091}, - new uint[]{8091}, - new uint[]{8093}, - new uint[]{8094}, - new uint[]{8093}, - new uint[]{8094}, - new uint[]{8095}, - new uint[]{8094}, - new uint[]{8140}, - new uint[]{8096}, - new uint[]{8097}, - new uint[]{8098}, - new uint[]{8140}, - new uint[]{8097}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{8078}, - new uint[]{8079}, - new uint[]{8080}, - new uint[]{8081}, - new uint[]{8082}, - new uint[]{8083}, - new uint[]{8100}, - new uint[]{8101}, - new uint[]{8101}, - new uint[]{8103}, - new uint[]{8102}, - new uint[]{8103}, - new uint[]{8108}, - new uint[]{8108}, - new uint[]{8114}, - new uint[]{8115}, - new uint[]{8087}, - new uint[]{8114}, - new uint[]{8115}, - new uint[]{8116}, - new uint[]{8116}, - new uint[]{4916}, - new uint[]{8117}, - new uint[]{8117}, - new uint[]{8118}, - new uint[]{3046}, - new uint[]{3047}, - new uint[]{3046}, - new uint[]{3047}, - new uint[]{8119}, - new uint[]{8120}, - new uint[]{8121}, - new uint[]{8120}, - new uint[]{8124}, - new uint[]{8132}, - new uint[]{8126}, - new uint[]{8127}, - new uint[]{8128}, - new uint[]{8126}, - new uint[]{8127}, - new uint[]{8125}, - new uint[]{8127}, - new uint[]{7155}, - new uint[]{8068}, - Array.Empty(), - new uint[]{7954}, - new uint[]{108}, - new uint[]{8066}, - new uint[]{8067}, - new uint[]{8069}, - new uint[]{8069}, - new uint[]{7967}, - new uint[]{7966}, - new uint[]{7965}, - new uint[]{7248}, - new uint[]{8005}, - new uint[]{8005}, - new uint[]{8006}, - new uint[]{8007}, - new uint[]{7248}, - Array.Empty(), - Array.Empty(), - new uint[]{8778}, - new uint[]{7248}, - new uint[]{7248}, - new uint[]{4130, 11264}, - new uint[]{5239, 11265}, - new uint[]{713, 11266}, - new uint[]{8917}, - new uint[]{1492, 11267}, - new uint[]{8378, 11268}, - new uint[]{8889, 11269}, - new uint[]{8919}, - new uint[]{8650}, - new uint[]{8650}, - new uint[]{8650}, - new uint[]{8070}, - new uint[]{8071}, - new uint[]{6041}, - new uint[]{6042}, - new uint[]{8072}, - new uint[]{8073}, - new uint[]{8074}, - new uint[]{8075}, - new uint[]{7955}, - new uint[]{7956}, - new uint[]{7957}, - new uint[]{8061}, - new uint[]{8062}, - new uint[]{7967}, - new uint[]{7967}, - new uint[]{7964}, - new uint[]{7964}, - new uint[]{7964}, - new uint[]{7966}, - new uint[]{7961}, - new uint[]{7962}, - new uint[]{7963}, - new uint[]{7965}, - new uint[]{7960}, - new uint[]{7959}, - new uint[]{7965}, - new uint[]{7926}, - new uint[]{108}, - new uint[]{8925}, - Array.Empty(), - new uint[]{8061}, - new uint[]{8131}, - Array.Empty(), - new uint[]{8061}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{7956}, - new uint[]{7956}, - new uint[]{7923}, - new uint[]{7924}, - new uint[]{7925}, - new uint[]{7965}, - new uint[]{8299}, - new uint[]{8300}, - new uint[]{8301}, - new uint[]{7176}, - new uint[]{7471}, - new uint[]{7469}, - new uint[]{8135}, - new uint[]{8134}, - new uint[]{8136}, - new uint[]{8137}, - new uint[]{8139}, - new uint[]{8138}, - new uint[]{8183}, - new uint[]{8184}, - new uint[]{8185}, - new uint[]{8186}, - new uint[]{8187}, - new uint[]{8188}, - new uint[]{8189}, - new uint[]{8190}, - new uint[]{8191}, - new uint[]{8192}, - new uint[]{8193}, - new uint[]{8194}, - new uint[]{8195}, - new uint[]{8196}, - new uint[]{8197}, - new uint[]{8198}, - new uint[]{8199}, - new uint[]{8200}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108, 8210}, - new uint[]{8235}, - new uint[]{8236}, - new uint[]{8234}, - new uint[]{8826}, - new uint[]{8231}, - new uint[]{8232}, - new uint[]{8233}, - new uint[]{8935}, - new uint[]{8981}, - new uint[]{8455}, - Array.Empty(), - new uint[]{8201}, - new uint[]{8826}, - new uint[]{8202}, - new uint[]{8203}, - new uint[]{8204}, - new uint[]{8205}, - new uint[]{8206}, - new uint[]{8207}, - new uint[]{8208}, - new uint[]{8209}, - new uint[]{8456}, - new uint[]{8210}, - new uint[]{8479, 8480, 8483}, - new uint[]{8211}, - new uint[]{8484}, - new uint[]{8325}, - new uint[]{8469}, - new uint[]{541}, - new uint[]{5978}, - new uint[]{8141}, - new uint[]{8260}, - new uint[]{108}, - new uint[]{8261}, - new uint[]{7864}, - new uint[]{8262}, - new uint[]{8262}, - new uint[]{108}, - new uint[]{8162}, - new uint[]{8163}, - new uint[]{8164}, - new uint[]{8147}, - new uint[]{8148}, - new uint[]{8149}, - new uint[]{8150}, - new uint[]{8151}, - new uint[]{8152}, - new uint[]{8153}, - new uint[]{8154}, - new uint[]{8856}, - new uint[]{8156}, - new uint[]{8157}, - new uint[]{8158}, - new uint[]{8159}, - new uint[]{8160}, - new uint[]{8161}, - new uint[]{8361}, - new uint[]{8357}, - new uint[]{8356}, - new uint[]{8355}, - new uint[]{8359}, - new uint[]{8358}, - new uint[]{8357}, - new uint[]{8356}, - new uint[]{8360}, - new uint[]{8354}, - new uint[]{8361}, - new uint[]{8357}, - new uint[]{8356}, - new uint[]{8360}, - new uint[]{8359}, - new uint[]{8357}, - new uint[]{8356}, - new uint[]{8360}, - new uint[]{8169}, - new uint[]{8170}, - new uint[]{7062}, - new uint[]{8172}, - new uint[]{8173}, - new uint[]{8174}, - new uint[]{8175}, - new uint[]{8176}, - new uint[]{8177}, - new uint[]{8178}, - new uint[]{8179}, - new uint[]{8180}, - new uint[]{8181}, - new uint[]{8182}, - new uint[]{8165}, - new uint[]{8166}, - new uint[]{8167}, - new uint[]{8569}, - new uint[]{8155}, - new uint[]{8571}, - new uint[]{8572}, - new uint[]{8573}, - new uint[]{8574}, - new uint[]{8575}, - new uint[]{8576}, - new uint[]{8577}, - new uint[]{8578}, - new uint[]{8579}, - new uint[]{8788}, - new uint[]{8581}, - new uint[]{8582}, - new uint[]{8583}, - new uint[]{8584}, - new uint[]{8585}, - new uint[]{8586}, - new uint[]{8587}, - new uint[]{8588}, - new uint[]{8589}, - new uint[]{8590}, - new uint[]{8459}, - new uint[]{8592}, - new uint[]{8653}, - new uint[]{8654}, - new uint[]{8655}, - new uint[]{8656}, - new uint[]{8596}, - new uint[]{8597}, - new uint[]{8598}, - new uint[]{8599}, - new uint[]{8600}, - new uint[]{8601}, - new uint[]{8789}, - new uint[]{8603}, - new uint[]{8604}, - new uint[]{8605}, - new uint[]{8606}, - new uint[]{8607}, - new uint[]{8608}, - new uint[]{8609}, - new uint[]{8610}, - new uint[]{8611}, - new uint[]{8612}, - new uint[]{8613}, - new uint[]{8614}, - new uint[]{8615}, - new uint[]{8616}, - new uint[]{8591}, - new uint[]{8890}, - new uint[]{8891}, - new uint[]{8892}, - new uint[]{8893}, - new uint[]{8894}, - new uint[]{8618}, - new uint[]{8619}, - new uint[]{8620}, - new uint[]{8621}, - new uint[]{8622}, - new uint[]{8638}, - new uint[]{8623}, - new uint[]{8624}, - new uint[]{8625}, - new uint[]{8626}, - new uint[]{8627}, - new uint[]{8628}, - new uint[]{8629}, - Array.Empty(), - new uint[]{8630}, - new uint[]{8631}, - new uint[]{8632}, - new uint[]{8633}, - new uint[]{8634}, - new uint[]{8635}, - Array.Empty(), - new uint[]{8895}, - new uint[]{8896}, - new uint[]{8897}, - new uint[]{8898}, - new uint[]{8899}, - new uint[]{8657}, - new uint[]{8543}, - new uint[]{8544}, - Array.Empty(), - new uint[]{8545}, - new uint[]{8546}, - new uint[]{8547}, - new uint[]{8548}, - new uint[]{8549}, - new uint[]{8550}, - new uint[]{8551}, - new uint[]{8552}, - new uint[]{8553}, - new uint[]{8554}, - new uint[]{8555}, - new uint[]{8556}, - new uint[]{8557}, - new uint[]{8558}, - new uint[]{8559}, - new uint[]{8560}, - new uint[]{8561}, - new uint[]{8562}, - new uint[]{8563}, - new uint[]{8564}, - new uint[]{8565}, - new uint[]{8566}, - new uint[]{8567}, - new uint[]{8568}, - new uint[]{8900}, - new uint[]{8901}, - new uint[]{8902}, - new uint[]{8903}, - new uint[]{8904}, - new uint[]{8358}, - new uint[]{8213}, - new uint[]{8498}, - new uint[]{8499}, - new uint[]{8500}, - new uint[]{8501}, - Array.Empty(), - new uint[]{8502}, - new uint[]{8503}, - new uint[]{8504}, - new uint[]{8505}, - new uint[]{8506}, - new uint[]{8507}, - new uint[]{8508}, - new uint[]{8509}, - new uint[]{8786}, - new uint[]{8511}, - new uint[]{8512}, - new uint[]{8513}, - new uint[]{8514}, - new uint[]{8515}, - new uint[]{8516}, - new uint[]{8905}, - new uint[]{8906}, - new uint[]{8907}, - new uint[]{8908}, - new uint[]{8909}, - Array.Empty(), - Array.Empty(), - new uint[]{8570}, - new uint[]{8299}, - new uint[]{8517}, - new uint[]{8518}, - new uint[]{8519}, - new uint[]{8520}, - new uint[]{8521}, - new uint[]{8522}, - new uint[]{8523}, - new uint[]{8524}, - new uint[]{8525}, - new uint[]{8791}, - new uint[]{8526}, - new uint[]{8527}, - new uint[]{8528}, - new uint[]{8529}, - new uint[]{8787}, - new uint[]{8531}, - new uint[]{8532}, - new uint[]{8533}, - new uint[]{8534}, - new uint[]{8535}, - new uint[]{8536}, - new uint[]{8537}, - new uint[]{8538}, - new uint[]{8539}, - new uint[]{8540}, - new uint[]{8541}, - new uint[]{8542}, - new uint[]{8913}, - new uint[]{8914}, - new uint[]{8911}, - new uint[]{8912}, - new uint[]{8915}, - new uint[]{8310}, - new uint[]{8264}, - new uint[]{8388}, - new uint[]{8308}, - new uint[]{8303}, - new uint[]{8302}, - new uint[]{8306}, - new uint[]{4130}, - new uint[]{5978}, - new uint[]{5239}, - new uint[]{729}, - new uint[]{1492}, - new uint[]{713}, - new uint[]{8917}, - new uint[]{8918}, - new uint[]{8279}, - new uint[]{8275}, - new uint[]{8278}, - new uint[]{8274}, - new uint[]{8276}, - new uint[]{8277}, - new uint[]{8288}, - new uint[]{8280}, - new uint[]{8281}, - new uint[]{8287}, - new uint[]{8285}, - new uint[]{8283}, - new uint[]{8284}, - new uint[]{8282}, - new uint[]{8286}, - new uint[]{8292}, - new uint[]{8291}, - new uint[]{8289}, - new uint[]{8293}, - new uint[]{8290}, - new uint[]{8273}, - new uint[]{108}, - new uint[]{8214}, - new uint[]{8215}, - new uint[]{8216}, - new uint[]{8217}, - new uint[]{8218}, - new uint[]{8219}, - Array.Empty(), - new uint[]{8221}, - new uint[]{8222}, - new uint[]{8223}, - new uint[]{8224}, - new uint[]{8225}, - new uint[]{8226}, - new uint[]{8154}, - new uint[]{8263}, - new uint[]{8264}, - new uint[]{8265}, - new uint[]{8262}, - new uint[]{8271}, - new uint[]{8266}, - new uint[]{8267}, - new uint[]{8268}, - new uint[]{8269}, - new uint[]{8270}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{8227}, - new uint[]{8228}, - new uint[]{8229}, - new uint[]{8230}, - new uint[]{8353}, - new uint[]{8394}, - new uint[]{8268}, - new uint[]{8353}, - Array.Empty(), - new uint[]{8391}, - new uint[]{8390}, - new uint[]{8389}, - new uint[]{8353}, - Array.Empty(), - new uint[]{8391}, - new uint[]{8389}, - new uint[]{8389}, - new uint[]{8394}, - new uint[]{8268}, - new uint[]{8353}, - new uint[]{8379}, - new uint[]{8382}, - new uint[]{8381}, - new uint[]{8380}, - new uint[]{8382}, - new uint[]{8381}, - new uint[]{8380}, - new uint[]{8382}, - new uint[]{8382}, - new uint[]{8258}, - new uint[]{8256}, - new uint[]{8255}, - new uint[]{8254}, - new uint[]{8253}, - new uint[]{8339}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8338}, - new uint[]{8337}, - new uint[]{8922}, - new uint[]{8923}, - new uint[]{8924}, - Array.Empty(), - new uint[]{8238}, - new uint[]{8239}, - Array.Empty(), - new uint[]{8241}, - Array.Empty(), - new uint[]{8243}, - new uint[]{8244}, - new uint[]{8245}, - new uint[]{8246}, - new uint[]{8247}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8302}, - new uint[]{8303}, - new uint[]{8304}, - Array.Empty(), - new uint[]{8306}, - new uint[]{8307}, - new uint[]{8308}, - new uint[]{8309}, - new uint[]{8310}, - new uint[]{8311}, - new uint[]{108}, - new uint[]{7864}, - new uint[]{8304}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8351}, - new uint[]{8352}, - new uint[]{8822}, - new uint[]{8378}, - new uint[]{8377}, - new uint[]{8376}, - new uint[]{8375}, - new uint[]{8374}, - new uint[]{8373}, - new uint[]{8372}, - new uint[]{8371}, - new uint[]{8645}, - new uint[]{8154}, - new uint[]{8649}, - new uint[]{8648}, - new uint[]{8374}, - new uint[]{8647}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8399}, - new uint[]{8398}, - new uint[]{729}, - new uint[]{713}, - new uint[]{8889}, - new uint[]{1492}, - new uint[]{4130}, - new uint[]{5239}, - new uint[]{8645}, - new uint[]{8645}, - new uint[]{8925}, - new uint[]{8778}, - new uint[]{8778}, - new uint[]{8486}, - new uint[]{8486}, - new uint[]{8264}, - new uint[]{8258}, - new uint[]{8312}, - new uint[]{8254}, - new uint[]{8369}, - new uint[]{8368}, - Array.Empty(), - Array.Empty(), - new uint[]{8365}, - new uint[]{8364}, - new uint[]{8363}, - new uint[]{8362}, - new uint[]{8643}, - new uint[]{8644}, - new uint[]{8643}, - new uint[]{8644}, - new uint[]{8643}, - new uint[]{8644}, - new uint[]{8318}, - new uint[]{8314}, - new uint[]{8330}, - new uint[]{8316}, - new uint[]{8329}, - new uint[]{8315}, - new uint[]{8328}, - new uint[]{8317}, - new uint[]{8331}, - new uint[]{8332}, - new uint[]{8333}, - new uint[]{8910}, - new uint[]{8370}, - new uint[]{8350}, - new uint[]{8348}, - new uint[]{8347}, - new uint[]{8350}, - new uint[]{8349}, - new uint[]{8348}, - new uint[]{8347}, - new uint[]{8346}, - new uint[]{8379}, - new uint[]{8234}, - new uint[]{8778}, - new uint[]{8374}, - new uint[]{108}, - new uint[]{8919}, - new uint[]{4130}, - new uint[]{5239}, - new uint[]{8917}, - new uint[]{8374}, - new uint[]{8399}, - new uint[]{5978}, - new uint[]{5978}, - new uint[]{8651}, - new uint[]{8650}, - new uint[]{8308}, - new uint[]{8264}, - new uint[]{8310}, - new uint[]{8219}, - new uint[]{8269}, - new uint[]{8311}, - new uint[]{8396}, - new uint[]{8395}, - new uint[]{8394}, - new uint[]{8307}, - new uint[]{8258}, - new uint[]{8255}, - new uint[]{8312}, - new uint[]{8393}, - new uint[]{8214}, - new uint[]{8476, 8478, 8593, 8959}, - new uint[]{8594, 8960}, - new uint[]{8595, 8961}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - Array.Empty(), - new uint[]{8645}, - new uint[]{8921}, - new uint[]{8645}, - new uint[]{8645}, - new uint[]{8920}, - new uint[]{8921}, - new uint[]{8645, 8920, 8921}, - new uint[]{8646}, - new uint[]{8929}, - new uint[]{8930}, - new uint[]{8931}, - new uint[]{8932}, - Array.Empty(), - new uint[]{8933}, - new uint[]{8486}, - new uint[]{8486}, - new uint[]{8486}, - new uint[]{8486}, - new uint[]{8776}, - new uint[]{8777}, - new uint[]{8778}, - new uint[]{8777}, - new uint[]{8488}, - new uint[]{8489}, - new uint[]{8490}, - new uint[]{8491}, - new uint[]{8492}, - new uint[]{8489}, - new uint[]{8776}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8748}, - new uint[]{8781, 8782, 8784}, - new uint[]{8780}, - new uint[]{8780}, - new uint[]{8781}, - new uint[]{8782}, - new uint[]{8783}, - new uint[]{8784}, - new uint[]{8785}, - new uint[]{108}, - new uint[]{8374}, - new uint[]{8931}, - new uint[]{8652}, - new uint[]{8258}, - new uint[]{8257}, - new uint[]{8256}, - new uint[]{8254}, - new uint[]{8312}, - new uint[]{8397}, - new uint[]{8636}, - new uint[]{8637}, - new uint[]{8858}, - new uint[]{8859}, - new uint[]{8860}, - new uint[]{8861}, - new uint[]{8862}, - new uint[]{8863}, - new uint[]{8864}, - new uint[]{8865}, - new uint[]{8866}, - new uint[]{8867}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8374}, - new uint[]{8916}, - new uint[]{8682}, - new uint[]{8683}, - new uint[]{8684}, - new uint[]{8685}, - new uint[]{8686}, - new uint[]{8687}, - new uint[]{8688}, - new uint[]{8489}, - new uint[]{8395, 8777}, - Array.Empty(), - new uint[]{108}, - new uint[]{8803}, - new uint[]{8868}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8310}, - new uint[]{8308}, - new uint[]{8306}, - new uint[]{8869}, - new uint[]{8870}, - new uint[]{8871}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8640}, - new uint[]{8641}, - new uint[]{8642}, - new uint[]{8689}, - new uint[]{8690}, - new uint[]{8691}, - new uint[]{8692}, - new uint[]{8693}, - new uint[]{8694}, - new uint[]{8695}, - new uint[]{8696}, - new uint[]{8697}, - new uint[]{8698}, - new uint[]{8699}, - new uint[]{8700}, - new uint[]{8701}, - new uint[]{8702}, - new uint[]{8703}, - new uint[]{8704}, - new uint[]{8705}, - new uint[]{8706}, - new uint[]{8707}, - new uint[]{8708}, - new uint[]{8709}, - new uint[]{8710}, - new uint[]{8711}, - new uint[]{8712}, - Array.Empty(), - new uint[]{8713}, - new uint[]{8714}, - new uint[]{8715}, - new uint[]{8785}, - new uint[]{8639}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8872}, - new uint[]{8872}, - new uint[]{8874}, - new uint[]{8875}, - new uint[]{8864}, - new uint[]{8876}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8858}, - new uint[]{8346}, - new uint[]{8400}, - new uint[]{8401}, - new uint[]{8402}, - new uint[]{8403}, - new uint[]{8404}, - new uint[]{8405}, - new uint[]{8406}, - new uint[]{8407}, - new uint[]{8408}, - new uint[]{8409}, - new uint[]{8410}, - new uint[]{8411}, - new uint[]{8412}, - new uint[]{8413}, - new uint[]{8414}, - new uint[]{8414}, - new uint[]{8414}, - new uint[]{8417}, - new uint[]{8418}, - new uint[]{8419}, - new uint[]{8420}, - new uint[]{8421}, - new uint[]{8422}, - new uint[]{8423}, - new uint[]{8424}, - new uint[]{8425}, - new uint[]{8426}, - new uint[]{8427}, - new uint[]{8428}, - new uint[]{8429}, - new uint[]{8430}, - new uint[]{8431}, - new uint[]{8432}, - new uint[]{8433}, - new uint[]{8434}, - new uint[]{8435}, - new uint[]{8823}, - new uint[]{8436}, - new uint[]{8437}, - new uint[]{8438}, - new uint[]{8439}, - new uint[]{8440}, - new uint[]{8441}, - new uint[]{8442}, - new uint[]{8443}, - new uint[]{8444}, - new uint[]{8445}, - new uint[]{8446}, - new uint[]{8447}, - new uint[]{8448}, - new uint[]{8449}, - new uint[]{8450}, - new uint[]{8451}, - new uint[]{8452}, - new uint[]{8323}, - new uint[]{8324}, - new uint[]{8485}, - new uint[]{8319}, - new uint[]{8817}, - new uint[]{8818}, - new uint[]{8816}, - new uint[]{8320}, - new uint[]{8934}, - new uint[]{8322}, - new uint[]{108}, - new uint[]{8313}, - new uint[]{8815}, - new uint[]{8814}, - new uint[]{8813}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8302}, - new uint[]{8821}, - new uint[]{8819}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{8234}, - Array.Empty(), - new uint[]{8352}, - new uint[]{8453}, - new uint[]{8454}, - new uint[]{8457}, - new uint[]{8458}, - new uint[]{8463}, - new uint[]{8467}, - new uint[]{8662, 8663, 8664}, - new uint[]{8662}, - new uint[]{8663}, - new uint[]{9029}, - new uint[]{9029}, - new uint[]{8670}, - new uint[]{8673}, - new uint[]{8680}, - new uint[]{8234}, - new uint[]{8922}, - new uint[]{8922}, - new uint[]{8327}, - new uint[]{8334}, - new uint[]{8335}, - new uint[]{8842, 8843, 8844, 8845}, - new uint[]{8842, 8843, 8845}, - new uint[]{8842, 8843, 8844, 8845}, - new uint[]{8842, 8843, 8844, 8845}, - new uint[]{8842, 8843, 8845}, - new uint[]{8842, 8845}, - new uint[]{8848}, - new uint[]{8849}, - new uint[]{8850}, - new uint[]{8851}, - new uint[]{8854}, - new uint[]{8852}, - new uint[]{8853}, - new uint[]{8855}, - new uint[]{108}, - new uint[]{8434}, - new uint[]{8493}, - new uint[]{8494}, - new uint[]{8495}, - new uint[]{8496}, - new uint[]{8497}, - new uint[]{8488}, - new uint[]{8489}, - new uint[]{108, 8493}, - new uint[]{8493}, - new uint[]{8795}, - new uint[]{8782}, - Array.Empty(), - new uint[]{8872}, - new uint[]{8872}, - new uint[]{8872}, - new uint[]{8872}, - new uint[]{8872}, - new uint[]{8872}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{8795}, - new uint[]{108}, - new uint[]{8796}, - new uint[]{108, 8796}, - new uint[]{8825}, - new uint[]{8824}, - new uint[]{108, 2186}, - new uint[]{8488}, - new uint[]{8846}, - new uint[]{8838}, - new uint[]{8839}, - new uint[]{8840}, - new uint[]{8841}, - new uint[]{8305}, - new uint[]{8847}, - new uint[]{8493}, - new uint[]{8485}, - new uint[]{8799}, - new uint[]{8798, 8823}, - new uint[]{108}, - new uint[]{8374}, - new uint[]{8779}, - new uint[]{8796}, - new uint[]{8797}, - new uint[]{8951}, - new uint[]{8952}, - new uint[]{8964}, - new uint[]{8830}, - new uint[]{8834}, - new uint[]{8835}, - new uint[]{108}, - new uint[]{8323}, - new uint[]{8812}, - new uint[]{8810}, - new uint[]{8809}, - new uint[]{8808}, - new uint[]{8807}, - new uint[]{8806}, - new uint[]{8800}, - new uint[]{8805}, - new uint[]{108, 8395}, - new uint[]{8927}, - new uint[]{8926}, - new uint[]{8716}, - new uint[]{8717}, - new uint[]{8718}, - new uint[]{8719}, - new uint[]{8720}, - new uint[]{8721}, - new uint[]{8722}, - new uint[]{8723}, - new uint[]{8724}, - new uint[]{8725}, - new uint[]{8726}, - new uint[]{8727}, - new uint[]{8728}, - new uint[]{8729}, - new uint[]{8730}, - new uint[]{8731}, - new uint[]{8732}, - new uint[]{8733}, - new uint[]{8734}, - new uint[]{8735}, - new uint[]{8736}, - new uint[]{8737}, - new uint[]{8738}, - new uint[]{8739}, - new uint[]{8740}, - new uint[]{8741}, - new uint[]{8742}, - new uint[]{8743}, - new uint[]{8744}, - new uint[]{8745}, - new uint[]{8746}, - new uint[]{8747}, - new uint[]{8965}, - new uint[]{8749}, - new uint[]{8750}, - new uint[]{8751}, - new uint[]{8752}, - new uint[]{8753}, - new uint[]{8754}, - new uint[]{8755}, - new uint[]{8756}, - new uint[]{8757}, - new uint[]{8758}, - new uint[]{8759}, - new uint[]{8760}, - new uint[]{8761}, - new uint[]{8762}, - new uint[]{8763}, - new uint[]{8764}, - new uint[]{8765}, - new uint[]{8766}, - new uint[]{8767}, - new uint[]{8768}, - new uint[]{8769}, - new uint[]{8770}, - new uint[]{8771}, - new uint[]{8772}, - new uint[]{8773}, - new uint[]{8774}, - new uint[]{8775}, - new uint[]{8228}, - new uint[]{8227}, - new uint[]{8827}, - new uint[]{8828}, - new uint[]{8829}, - new uint[]{8831}, - new uint[]{8832}, - new uint[]{8833}, - new uint[]{8836}, - new uint[]{8837}, - new uint[]{8826}, - new uint[]{8224}, - new uint[]{8966}, - new uint[]{8967}, - new uint[]{8968}, - new uint[]{9034}, - new uint[]{9033}, - new uint[]{9035}, - new uint[]{8969}, - new uint[]{8970}, - new uint[]{8971}, - new uint[]{8972}, - new uint[]{8973}, - new uint[]{8974}, - new uint[]{8975}, - new uint[]{8976}, - new uint[]{8977, 9026}, - new uint[]{8978}, - new uint[]{8979}, - new uint[]{8980}, - new uint[]{8460}, - new uint[]{8982}, - new uint[]{8983}, - new uint[]{8984}, - new uint[]{8985}, - new uint[]{8986}, - new uint[]{8987, 9025}, - new uint[]{8988}, - new uint[]{9039}, - new uint[]{8990}, - new uint[]{8991}, - new uint[]{8992}, - new uint[]{8993}, - new uint[]{8994}, - new uint[]{8995}, - new uint[]{8996}, - new uint[]{8997}, - new uint[]{9036}, - new uint[]{8998}, - new uint[]{8999}, - new uint[]{8877}, - new uint[]{8878}, - new uint[]{8879}, - new uint[]{8880}, - new uint[]{8881}, - new uint[]{8882}, - new uint[]{8883}, - new uint[]{8884}, - new uint[]{8885}, - new uint[]{8886}, - new uint[]{8887}, - new uint[]{8888}, - new uint[]{8947}, - new uint[]{8948}, - new uint[]{9000}, - new uint[]{9001}, - new uint[]{9002}, - new uint[]{9003, 9012}, - new uint[]{9004}, - new uint[]{9005}, - new uint[]{9006}, - new uint[]{9007}, - new uint[]{9008}, - new uint[]{8427}, - new uint[]{9027}, - new uint[]{9010}, - new uint[]{9011}, - new uint[]{9013}, - new uint[]{9037}, - new uint[]{9014}, - new uint[]{9015}, - new uint[]{8936, 8937, 8940, 8943, 8946}, - new uint[]{8937, 8938, 8944}, - new uint[]{8939, 8941, 8942, 8945}, - new uint[]{9016}, - new uint[]{9017}, - new uint[]{9018}, - new uint[]{9019}, - new uint[]{9020}, - new uint[]{9021}, - new uint[]{9022}, - new uint[]{9023}, - new uint[]{9024}, - new uint[]{9027}, - new uint[]{9027}, - new uint[]{9027}, - new uint[]{9038}, - new uint[]{9028}, - new uint[]{8953}, - new uint[]{8954}, - new uint[]{8955}, - new uint[]{8956}, - new uint[]{8957}, - new uint[]{2201}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8822}, - new uint[]{8822}, - new uint[]{8154}, - Array.Empty(), - new uint[]{8470, 8474}, - new uint[]{8461}, - new uint[]{8462}, - new uint[]{8464}, - new uint[]{8465}, - new uint[]{8466}, - new uint[]{8468}, - new uint[]{8666}, - new uint[]{8669}, - new uint[]{8671}, - new uint[]{8672}, - new uint[]{8674}, - new uint[]{8677}, - new uint[]{8678}, - new uint[]{8679}, - Array.Empty(), - new uint[]{8659, 8660, 8661}, - new uint[]{8662}, - new uint[]{8663}, - new uint[]{8665}, - new uint[]{8667}, - new uint[]{8668}, - new uint[]{8668}, - new uint[]{108}, - new uint[]{8848}, - new uint[]{8645}, - new uint[]{8918}, - new uint[]{8645, 8921}, - new uint[]{8313}, - new uint[]{8872}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8918}, - new uint[]{8921}, - new uint[]{8645}, - new uint[]{8645}, - new uint[]{8820}, - new uint[]{108}, - new uint[]{8323}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{8819}, - new uint[]{8485}, - new uint[]{8294}, - new uint[]{8295}, - new uint[]{8294}, - new uint[]{8295}, - new uint[]{8296}, - new uint[]{8296}, - new uint[]{8949}, - new uint[]{8950}, - new uint[]{8646}, - new uint[]{8962}, - new uint[]{8962}, - new uint[]{8962}, - new uint[]{8962}, - new uint[]{8962}, - new uint[]{8963}, - new uint[]{8353}, - new uint[]{8394}, - new uint[]{8268}, - new uint[]{8952}, - new uint[]{8838}, - new uint[]{8918}, - new uint[]{8790}, - new uint[]{8234}, - new uint[]{8485}, - new uint[]{8485}, - new uint[]{8485}, - new uint[]{8324}, - new uint[]{8811}, - new uint[]{8804}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9044}, - new uint[]{9045}, - new uint[]{9064}, - new uint[]{9148}, - new uint[]{9149}, - new uint[]{9150}, - new uint[]{9151}, - new uint[]{9152}, - new uint[]{9152}, - new uint[]{9041}, - new uint[]{9049}, - new uint[]{9047}, - new uint[]{9048}, - new uint[]{9050}, - new uint[]{9065}, - new uint[]{9046}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9041}, - new uint[]{9063}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8352}, - new uint[]{8826}, - new uint[]{9180}, - new uint[]{9181}, - new uint[]{9182}, - new uint[]{9183}, - new uint[]{8351}, - new uint[]{9184}, - new uint[]{9185}, - new uint[]{9143}, - new uint[]{9143}, - new uint[]{9143}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9051}, - new uint[]{9051}, - new uint[]{9051}, - new uint[]{9052}, - new uint[]{9063}, - new uint[]{9053}, - new uint[]{9053}, - new uint[]{9053}, - new uint[]{9054}, - new uint[]{9055}, - new uint[]{9056}, - new uint[]{9062}, - new uint[]{9057}, - new uint[]{9179}, - new uint[]{9058}, - new uint[]{9059}, - new uint[]{9127}, - new uint[]{9128}, - new uint[]{9129}, - new uint[]{9060}, - new uint[]{9130}, - new uint[]{9061}, - new uint[]{9211}, - new uint[]{9212}, - new uint[]{9213}, - new uint[]{9214}, - new uint[]{9215}, - new uint[]{9216}, - new uint[]{9217}, - new uint[]{9218}, - new uint[]{8658}, - new uint[]{9221}, - new uint[]{9222}, - new uint[]{9223}, - new uint[]{9220}, - new uint[]{9224}, - new uint[]{9042}, - new uint[]{108}, - new uint[]{9186}, - new uint[]{9231}, - new uint[]{9232}, - Array.Empty(), - new uint[]{9239}, - new uint[]{9240}, - new uint[]{9241}, - new uint[]{9242}, - new uint[]{9243}, - new uint[]{9244}, - new uint[]{9141}, - new uint[]{9146}, - new uint[]{9142}, - Array.Empty(), - new uint[]{9142}, - new uint[]{9131}, - new uint[]{9245}, - new uint[]{2667}, - Array.Empty(), - new uint[]{9245}, - Array.Empty(), - new uint[]{9245}, - new uint[]{9245}, - new uint[]{9245}, - new uint[]{9245}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9143}, - new uint[]{9153}, - new uint[]{9154}, - new uint[]{9155}, - new uint[]{9156}, - new uint[]{9157}, - new uint[]{9136}, - new uint[]{9134}, - new uint[]{9135}, - new uint[]{9136}, - new uint[]{9137}, - new uint[]{9138}, - new uint[]{9139}, - new uint[]{9140}, - new uint[]{9219}, - new uint[]{9132}, - new uint[]{9133}, - new uint[]{9230}, - new uint[]{9189}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9190}, - new uint[]{9190}, - new uint[]{9191}, - new uint[]{9192}, - new uint[]{9193}, - new uint[]{9194}, - new uint[]{9195}, - new uint[]{9196}, - new uint[]{9197}, - Array.Empty(), - new uint[]{9199}, - new uint[]{9200}, - new uint[]{9201}, - new uint[]{7857}, - new uint[]{7860}, - new uint[]{7857, 9189}, - new uint[]{9202}, - new uint[]{9203}, - new uint[]{9204}, - new uint[]{9205}, - new uint[]{9218}, - new uint[]{9220}, - new uint[]{9042}, - new uint[]{9178}, - new uint[]{9177}, - new uint[]{9176}, - new uint[]{9160}, - new uint[]{9159}, - new uint[]{9162}, - new uint[]{9161}, - new uint[]{9207}, - new uint[]{9208}, - new uint[]{9209}, - new uint[]{9210}, - new uint[]{9158}, - new uint[]{9158}, - new uint[]{9159}, - new uint[]{9160}, - new uint[]{9161}, - new uint[]{9162}, - new uint[]{8126}, - new uint[]{8105}, - new uint[]{9163}, - new uint[]{9164}, - new uint[]{9165}, - new uint[]{9166}, - new uint[]{9167}, - new uint[]{9168}, - new uint[]{9169}, - new uint[]{9170}, - new uint[]{9171}, - new uint[]{9172}, - new uint[]{9172}, - new uint[]{9172}, - new uint[]{9173}, - new uint[]{9173}, - new uint[]{9174}, - new uint[]{9175}, - Array.Empty(), - new uint[]{9131}, - new uint[]{9229}, - new uint[]{9247}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{8845}, - new uint[]{8845}, - new uint[]{8845}, - new uint[]{9131}, - new uint[]{108, 9147}, - Array.Empty(), - Array.Empty(), - new uint[]{9233}, - new uint[]{9234}, - new uint[]{9235}, - new uint[]{9236}, - new uint[]{9237}, - new uint[]{9238}, - Array.Empty(), - new uint[]{9260}, - new uint[]{9261}, - new uint[]{9262}, - new uint[]{9250}, - Array.Empty(), - new uint[]{731}, - new uint[]{9250}, - new uint[]{9254}, - new uint[]{9254}, - new uint[]{9250}, - new uint[]{9259}, - new uint[]{9253}, - new uint[]{731}, - new uint[]{9250}, - new uint[]{9254}, - new uint[]{9254}, - new uint[]{9255}, - new uint[]{9256}, - Array.Empty(), - new uint[]{9246}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{9829}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{3046}, - new uint[]{2667}, - Array.Empty(), - new uint[]{9248}, - new uint[]{9249}, - Array.Empty(), - new uint[]{9263}, - new uint[]{9264}, - new uint[]{9265}, - new uint[]{9266}, - new uint[]{9281}, - new uint[]{9282}, - new uint[]{9283}, - new uint[]{9284}, - new uint[]{9285}, - Array.Empty(), - new uint[]{9281}, - new uint[]{9282}, - new uint[]{9283}, - new uint[]{9284}, - new uint[]{9285}, - new uint[]{9286}, - new uint[]{9287}, - new uint[]{9288}, - new uint[]{9289}, - new uint[]{9290}, - new uint[]{9291}, - new uint[]{9287}, - new uint[]{9288}, - new uint[]{9289}, - new uint[]{9290}, - new uint[]{9291}, - new uint[]{9292}, - new uint[]{9267}, - new uint[]{9268}, - new uint[]{9269}, - new uint[]{9271}, - new uint[]{108}, - new uint[]{9272}, - new uint[]{9273}, - new uint[]{9274}, - new uint[]{9275}, - new uint[]{9276}, - new uint[]{9277}, - new uint[]{9278}, - new uint[]{9316}, - new uint[]{9830}, - new uint[]{9288}, - new uint[]{9289}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9279}, - new uint[]{9280}, - new uint[]{9298}, - new uint[]{9299}, - Array.Empty(), - new uint[]{9301}, - new uint[]{9301}, - new uint[]{9298}, - new uint[]{9299}, - new uint[]{9300}, - new uint[]{9301}, - new uint[]{9301}, - Array.Empty(), - new uint[]{9302}, - new uint[]{9303}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9296}, - new uint[]{8378}, - new uint[]{8377}, - new uint[]{8376}, - new uint[]{8375}, - new uint[]{9293}, - new uint[]{9294}, - new uint[]{9295}, - new uint[]{9297}, - new uint[]{9341}, - new uint[]{9365}, - Array.Empty(), - new uint[]{9360}, - new uint[]{9361}, - new uint[]{9362}, - new uint[]{9355}, - new uint[]{3819}, - new uint[]{3820}, - new uint[]{9342}, - new uint[]{9356}, - new uint[]{9331}, - new uint[]{9331}, - Array.Empty(), - new uint[]{9329}, - new uint[]{9328}, - new uint[]{9332}, - new uint[]{9332}, - new uint[]{9332}, - new uint[]{9332}, - new uint[]{9332}, - new uint[]{9333}, - new uint[]{9333}, - new uint[]{9333}, - new uint[]{9334}, - new uint[]{9335}, - new uint[]{9336}, - new uint[]{9337}, - new uint[]{9338}, - new uint[]{108, 9339}, - new uint[]{9340}, - new uint[]{9341}, - new uint[]{9365}, - new uint[]{9360}, - new uint[]{9361}, - new uint[]{9362}, - new uint[]{9355}, - new uint[]{9342}, - new uint[]{9353}, - Array.Empty(), - new uint[]{9318}, - new uint[]{9319}, - new uint[]{9320}, - new uint[]{9321}, - new uint[]{9322}, - new uint[]{9323}, - new uint[]{9353}, - Array.Empty(), - Array.Empty(), - new uint[]{9319}, - new uint[]{9320}, - new uint[]{9321}, - new uint[]{9324}, - new uint[]{9346}, - new uint[]{5239}, - new uint[]{9348}, - new uint[]{9347}, - new uint[]{9349}, - new uint[]{8378}, - Array.Empty(), - new uint[]{9333}, - Array.Empty(), - new uint[]{9358}, - new uint[]{9357}, - new uint[]{9278}, - new uint[]{9325}, - new uint[]{9491}, - new uint[]{9490}, - new uint[]{9502}, - new uint[]{9503}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9326}, - new uint[]{9299}, - new uint[]{9299}, - Array.Empty(), - new uint[]{731}, - Array.Empty(), - Array.Empty(), - new uint[]{10119}, - Array.Empty(), - new uint[]{9317}, - new uint[]{9317}, - new uint[]{9288}, - new uint[]{9289}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9400}, - new uint[]{9513}, - new uint[]{9390}, - new uint[]{9402}, - new uint[]{9350}, - new uint[]{9359}, - new uint[]{9254}, - new uint[]{9254}, - new uint[]{9329}, - new uint[]{9327}, - new uint[]{9278}, - new uint[]{9333}, - new uint[]{9334}, - new uint[]{9339}, - new uint[]{9424}, - new uint[]{9425}, - new uint[]{9398}, - new uint[]{9399}, - new uint[]{9403}, - new uint[]{9404}, - new uint[]{9405}, - new uint[]{9406}, - new uint[]{9396}, - new uint[]{9475}, - new uint[]{9475}, - new uint[]{9476, 9501}, - new uint[]{9477}, - new uint[]{9477}, - new uint[]{9478, 9641}, - new uint[]{9478, 9641}, - new uint[]{9479}, - new uint[]{9480}, - new uint[]{9481}, - new uint[]{9482}, - new uint[]{9482}, - new uint[]{108}, - new uint[]{9427}, - new uint[]{9428}, - new uint[]{9462}, - new uint[]{9829}, - new uint[]{9830}, - new uint[]{9702}, - new uint[]{9702}, - new uint[]{9693}, - Array.Empty(), - new uint[]{9465}, - new uint[]{9466}, - new uint[]{9467}, - new uint[]{9468}, - new uint[]{9469}, - new uint[]{9470}, - new uint[]{9471}, - new uint[]{9472}, - new uint[]{9473}, - new uint[]{9462}, - new uint[]{9462}, - new uint[]{9822, 9823, 9824}, - new uint[]{541}, - Array.Empty(), - Array.Empty(), - new uint[]{9465}, - new uint[]{9466}, - new uint[]{9467}, - new uint[]{9468}, - new uint[]{9469}, - new uint[]{9470}, - new uint[]{9471}, - new uint[]{9472}, - new uint[]{9473}, - new uint[]{9505}, - new uint[]{9506}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{9508}, - new uint[]{9510}, - new uint[]{9510}, - new uint[]{9510}, - new uint[]{9510}, - new uint[]{9509}, - new uint[]{9509}, - new uint[]{9509}, - new uint[]{9509}, - new uint[]{9458}, - new uint[]{9459}, - new uint[]{9460}, - new uint[]{5045}, - new uint[]{9461}, - new uint[]{9461}, - new uint[]{9461}, - new uint[]{9442}, - new uint[]{9443}, - new uint[]{9444}, - new uint[]{9445}, - new uint[]{9446}, - new uint[]{9447}, - new uint[]{9448}, - new uint[]{9449}, - new uint[]{9450}, - new uint[]{9299}, - new uint[]{9299}, - Array.Empty(), - Array.Empty(), - new uint[]{9451}, - new uint[]{108}, - new uint[]{9364}, - new uint[]{9617}, - new uint[]{9618}, - new uint[]{9322}, - new uint[]{9417}, - new uint[]{9515}, - new uint[]{9516}, - new uint[]{9517}, - new uint[]{9518}, - new uint[]{9519}, - new uint[]{9520}, - new uint[]{9521}, - new uint[]{9522}, - new uint[]{9523}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11320}, - Array.Empty(), - new uint[]{9530}, - new uint[]{9531}, - new uint[]{9532}, - new uint[]{9533}, - new uint[]{9534}, - new uint[]{9535}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{10015}, - new uint[]{9391}, - new uint[]{9392}, - new uint[]{9393}, - new uint[]{9536}, - new uint[]{9537}, - new uint[]{9538}, - new uint[]{9931}, - new uint[]{9539}, - new uint[]{9540}, - new uint[]{9541}, - new uint[]{9542}, - new uint[]{9408}, - new uint[]{9544}, - new uint[]{9533}, - new uint[]{9545}, - new uint[]{9546}, - new uint[]{9547}, - new uint[]{9536}, - new uint[]{9548}, - new uint[]{9549}, - new uint[]{9550}, - new uint[]{9551}, - new uint[]{9537}, - new uint[]{10107}, - new uint[]{10116}, - new uint[]{108}, - new uint[]{9538}, - new uint[]{9555}, - new uint[]{9556}, - new uint[]{9557}, - new uint[]{9558}, - new uint[]{9559}, - new uint[]{9560}, - new uint[]{9561}, - new uint[]{9562}, - new uint[]{9563}, - new uint[]{9564}, - new uint[]{9536}, - new uint[]{9565}, - new uint[]{9430}, - new uint[]{9567}, - new uint[]{9533}, - new uint[]{9568}, - new uint[]{9569}, - new uint[]{9570}, - new uint[]{9571}, - new uint[]{9538}, - new uint[]{9572}, - new uint[]{9573}, - new uint[]{9679}, - new uint[]{9537}, - new uint[]{9575}, - new uint[]{9576}, - new uint[]{9577}, - new uint[]{9578}, - new uint[]{9394}, - new uint[]{9395}, - new uint[]{9407}, - new uint[]{9408}, - new uint[]{9650}, - Array.Empty(), - new uint[]{9664}, - new uint[]{9651}, - new uint[]{9384}, - new uint[]{9422}, - new uint[]{9423}, - new uint[]{9507}, - new uint[]{9411}, - new uint[]{9411}, - new uint[]{9412}, - new uint[]{9413}, - new uint[]{9414}, - new uint[]{9415}, - new uint[]{9416}, - new uint[]{108, 9489}, - new uint[]{9492}, - new uint[]{9493}, - new uint[]{9494}, - new uint[]{9495}, - new uint[]{9496}, - new uint[]{9497}, - new uint[]{9498}, - new uint[]{9499}, - new uint[]{9500}, - new uint[]{9656}, - new uint[]{9664}, - new uint[]{9657}, - new uint[]{9366}, - new uint[]{9367}, - new uint[]{9369}, - new uint[]{9371}, - new uint[]{9372}, - new uint[]{9373}, - new uint[]{9426}, - new uint[]{9409}, - new uint[]{9390}, - new uint[]{9410}, - new uint[]{9375}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9389}, - new uint[]{9386}, - new uint[]{9386}, - new uint[]{9386}, - new uint[]{9387}, - new uint[]{9387}, - new uint[]{9388}, - new uint[]{9388}, - new uint[]{9384}, - new uint[]{108, 9384}, - new uint[]{9642}, - new uint[]{9381}, - new uint[]{9588}, - new uint[]{9382}, - new uint[]{9383}, - new uint[]{9390}, - new uint[]{9390}, - Array.Empty(), - new uint[]{9390}, - new uint[]{9390}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{9403}, - new uint[]{9404}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9409}, - Array.Empty(), - new uint[]{9452}, - new uint[]{9453}, - new uint[]{9454}, - new uint[]{9455}, - new uint[]{9456}, - new uint[]{9457}, - new uint[]{9513}, - new uint[]{9646}, - new uint[]{9647}, - new uint[]{9644}, - new uint[]{9645}, - new uint[]{9648}, - new uint[]{9368}, - new uint[]{9370}, - new uint[]{9140}, - new uint[]{9436}, - new uint[]{9437}, - new uint[]{9438}, - new uint[]{9419}, - new uint[]{9420}, - new uint[]{9421}, - new uint[]{108}, - new uint[]{9439}, - new uint[]{9439}, - new uint[]{9439}, - new uint[]{9436}, - new uint[]{9440}, - Array.Empty(), - new uint[]{9441}, - new uint[]{9366}, - new uint[]{9367}, - new uint[]{9374}, - new uint[]{9543}, - new uint[]{9429}, - new uint[]{9430}, - Array.Empty(), - new uint[]{9431}, - new uint[]{9543}, - new uint[]{9432}, - new uint[]{9652}, - new uint[]{9655}, - new uint[]{9653}, - new uint[]{9654}, - new uint[]{510, 9384, 9398, 9400, 9419, 9426, 9929, 9931, 9967}, - new uint[]{9132}, - new uint[]{9133}, - new uint[]{9649}, - new uint[]{9140}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9608}, - new uint[]{9607}, - new uint[]{9368}, - new uint[]{9369}, - new uint[]{9370}, - new uint[]{9371}, - new uint[]{9372}, - new uint[]{9373}, - new uint[]{9384}, - new uint[]{9659}, - new uint[]{9662}, - new uint[]{9660}, - new uint[]{9661}, - new uint[]{9663}, - new uint[]{9511}, - new uint[]{9512}, - new uint[]{9363, 11271}, - new uint[]{9363, 11271}, - new uint[]{9363, 11271}, - new uint[]{9418}, - new uint[]{108, 9485, 9486, 9487, 9488}, - new uint[]{108, 9483, 9484}, - new uint[]{9477}, - new uint[]{9433}, - new uint[]{9680}, - new uint[]{9433}, - new uint[]{9681}, - new uint[]{9434}, - Array.Empty(), - new uint[]{9595}, - new uint[]{9668}, - new uint[]{9348}, - new uint[]{8378}, - new uint[]{5573}, - new uint[]{9666}, - new uint[]{2118}, - new uint[]{2160}, - new uint[]{2135}, - new uint[]{2136}, - new uint[]{9667}, - new uint[]{9671}, - new uint[]{9672}, - new uint[]{9674}, - new uint[]{3639}, - new uint[]{3642}, - new uint[]{3633}, - new uint[]{3632}, - new uint[]{4739}, - new uint[]{7869}, - new uint[]{9675}, - new uint[]{3458}, - new uint[]{3458}, - new uint[]{5576}, - new uint[]{9676}, - new uint[]{9677}, - new uint[]{9678}, - new uint[]{6148}, - new uint[]{6039}, - new uint[]{7537}, - new uint[]{6041}, - new uint[]{6042}, - new uint[]{6040}, - new uint[]{9673}, - new uint[]{8258}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9669}, - new uint[]{8826}, - new uint[]{9476}, - new uint[]{9479}, - new uint[]{9479}, - new uint[]{9479}, - new uint[]{9475}, - new uint[]{9479}, - new uint[]{9480}, - new uint[]{8826}, - new uint[]{9670}, - new uint[]{9586}, - new uint[]{9587}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9514}, - new uint[]{9432}, - new uint[]{9596}, - Array.Empty(), - new uint[]{9597}, - Array.Empty(), - new uint[]{9377}, - new uint[]{9378}, - new uint[]{108, 9379}, - new uint[]{9385}, - new uint[]{9425}, - new uint[]{9378}, - new uint[]{9589}, - new uint[]{9589}, - new uint[]{9589}, - new uint[]{9589}, - new uint[]{9590}, - new uint[]{9591}, - new uint[]{9592}, - new uint[]{9593}, - new uint[]{9594}, - Array.Empty(), - Array.Empty(), - new uint[]{9388}, - new uint[]{9606}, - new uint[]{9604}, - new uint[]{9375}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9605}, - new uint[]{9598}, - new uint[]{9599}, - new uint[]{9600}, - new uint[]{9386}, - new uint[]{9386}, - new uint[]{9386}, - new uint[]{9387}, - new uint[]{9388}, - new uint[]{9376}, - new uint[]{9390}, - new uint[]{9603}, - new uint[]{9602}, - new uint[]{9579}, - new uint[]{9580}, - new uint[]{9581}, - new uint[]{9579}, - new uint[]{9580}, - new uint[]{9582}, - new uint[]{9582}, - new uint[]{9582}, - Array.Empty(), - new uint[]{9582}, - new uint[]{9582}, - new uint[]{9582}, - Array.Empty(), - new uint[]{9543}, - new uint[]{9609}, - new uint[]{9559}, - new uint[]{9375}, - new uint[]{9639}, - new uint[]{9543}, - new uint[]{9566}, - Array.Empty(), - Array.Empty(), - new uint[]{9612}, - Array.Empty(), - new uint[]{9589}, - new uint[]{9589}, - new uint[]{9408}, - new uint[]{9407}, - new uint[]{9632}, - new uint[]{9601}, - new uint[]{9629}, - new uint[]{9630}, - new uint[]{9631}, - new uint[]{9374}, - new uint[]{9386}, - new uint[]{9636}, - new uint[]{9602}, - new uint[]{9635}, - new uint[]{9377}, - new uint[]{9647}, - new uint[]{9647}, - new uint[]{9385}, - new uint[]{9377}, - new uint[]{9637}, - new uint[]{9604}, - new uint[]{9385}, - new uint[]{9390}, - new uint[]{9633}, - new uint[]{9384}, - new uint[]{9634}, - Array.Empty(), - new uint[]{7974}, - new uint[]{9388}, - new uint[]{9386}, - new uint[]{9543}, - new uint[]{9638}, - new uint[]{9376}, - new uint[]{9457}, - new uint[]{9636}, - new uint[]{9602}, - new uint[]{9390}, - new uint[]{9366}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9640}, - new uint[]{9599}, - new uint[]{9636}, - new uint[]{9602}, - new uint[]{9564}, - new uint[]{9366}, - new uint[]{9367}, - new uint[]{6945}, - new uint[]{9425}, - new uint[]{9378}, - new uint[]{9607}, - new uint[]{9608}, - new uint[]{9368}, - new uint[]{108}, - new uint[]{9390}, - new uint[]{9636}, - new uint[]{9602}, - new uint[]{9585}, - new uint[]{9714}, - new uint[]{9715}, - new uint[]{9716}, - new uint[]{9717}, - new uint[]{9718}, - new uint[]{9719}, - new uint[]{9720}, - new uint[]{9721}, - new uint[]{9722}, - new uint[]{9723}, - new uint[]{9724}, - new uint[]{9725}, - new uint[]{9726}, - new uint[]{9727}, - new uint[]{9728}, - new uint[]{9729}, - new uint[]{9730}, - new uint[]{9731}, - new uint[]{9732}, - new uint[]{9733}, - new uint[]{9734}, - new uint[]{9665}, - Array.Empty(), - Array.Empty(), - new uint[]{9371}, - new uint[]{9372}, - new uint[]{9373}, - new uint[]{9389}, - new uint[]{9386}, - new uint[]{9386}, - new uint[]{9387}, - new uint[]{9388}, - new uint[]{10096}, - new uint[]{10099}, - new uint[]{9682}, - new uint[]{9683}, - new uint[]{9684}, - new uint[]{9603}, - new uint[]{9685}, - new uint[]{9686}, - new uint[]{9390}, - new uint[]{9390}, - new uint[]{9411}, - new uint[]{9503}, - new uint[]{9916}, - new uint[]{9439}, - new uint[]{8826}, - new uint[]{8826}, - new uint[]{541}, - new uint[]{9737}, - new uint[]{9688}, - new uint[]{9741}, - new uint[]{9735}, - new uint[]{9736}, - new uint[]{9737}, - new uint[]{9788}, - new uint[]{9789}, - new uint[]{9790}, - new uint[]{9791}, - new uint[]{9792}, - new uint[]{9795}, - new uint[]{9796}, - new uint[]{9793}, - new uint[]{9794}, - new uint[]{9797}, - new uint[]{9799}, - new uint[]{9808}, - new uint[]{9810}, - new uint[]{9800}, - new uint[]{9801}, - new uint[]{9802}, - new uint[]{9803}, - new uint[]{9804}, - new uint[]{9805}, - new uint[]{9806}, - new uint[]{9807}, - new uint[]{9809}, - new uint[]{9776}, - new uint[]{9777}, - new uint[]{9778}, - new uint[]{9779}, - new uint[]{9780}, - new uint[]{9781}, - new uint[]{9782}, - new uint[]{9783}, - new uint[]{9784}, - new uint[]{9785}, - new uint[]{9786}, - new uint[]{9808}, - new uint[]{9787}, - new uint[]{9808}, - new uint[]{9774}, - new uint[]{9773}, - new uint[]{9811}, - new uint[]{9775}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9696}, - new uint[]{9697}, - new uint[]{9698}, - new uint[]{9699}, - new uint[]{9697}, - new uint[]{9701}, - new uint[]{9702}, - new uint[]{9696}, - new uint[]{9697}, - Array.Empty(), - new uint[]{9699}, - new uint[]{9697}, - new uint[]{9701}, - new uint[]{9702}, - new uint[]{9703}, - new uint[]{9704}, - new uint[]{9705}, - new uint[]{9706}, - new uint[]{9798}, - new uint[]{9798}, - Array.Empty(), - new uint[]{9812}, - new uint[]{9808}, - new uint[]{9808}, - new uint[]{9742}, - new uint[]{9743}, - new uint[]{9744}, - new uint[]{9745}, - new uint[]{9764}, - new uint[]{9765}, - new uint[]{9766}, - new uint[]{9767}, - new uint[]{9768}, - new uint[]{9764}, - new uint[]{9766}, - new uint[]{9767}, - new uint[]{9765}, - new uint[]{9768}, - Array.Empty(), - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - Array.Empty(), - Array.Empty(), - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9769}, - new uint[]{9772}, - new uint[]{9770}, - new uint[]{9771}, - new uint[]{9707}, - new uint[]{9708}, - Array.Empty(), - new uint[]{9709}, - new uint[]{9710}, - new uint[]{9711}, - new uint[]{9708}, - new uint[]{9707}, - new uint[]{9708}, - Array.Empty(), - new uint[]{9709}, - new uint[]{9710}, - new uint[]{9711}, - new uint[]{9712}, - new uint[]{9713}, - new uint[]{9738}, - new uint[]{9739}, - new uint[]{9740}, - new uint[]{9808}, - new uint[]{9619, 9885}, - new uint[]{9886}, - new uint[]{9887}, - new uint[]{9888}, - new uint[]{9889}, - new uint[]{9890}, - new uint[]{9891}, - new uint[]{9892}, - new uint[]{9893}, - new uint[]{9894}, - new uint[]{9813}, - new uint[]{9813}, - new uint[]{9815}, - new uint[]{9816}, - new uint[]{9817}, - new uint[]{9818}, - new uint[]{9819}, - new uint[]{9829}, - new uint[]{9830}, - new uint[]{9826}, - new uint[]{9827}, - new uint[]{9828}, - new uint[]{9813}, - new uint[]{9814}, - new uint[]{9815}, - new uint[]{9821}, - new uint[]{9818}, - new uint[]{9819}, - new uint[]{9820}, - new uint[]{9830}, - new uint[]{9831}, - new uint[]{9832}, - new uint[]{9747}, - new uint[]{9748}, - new uint[]{9746}, - new uint[]{9751}, - new uint[]{9752}, - new uint[]{9751}, - new uint[]{9752}, - new uint[]{9753}, - new uint[]{9754}, - new uint[]{9755}, - new uint[]{9756}, - new uint[]{9755}, - new uint[]{9756}, - new uint[]{9757}, - new uint[]{9758}, - new uint[]{9759}, - new uint[]{9760}, - new uint[]{9761}, - Array.Empty(), - new uint[]{9763}, - new uint[]{9328}, - Array.Empty(), - new uint[]{9750}, - new uint[]{9838}, - new uint[]{9839}, - new uint[]{9840}, - new uint[]{9841}, - new uint[]{9842}, - new uint[]{9843}, - new uint[]{9844}, - new uint[]{9849}, - new uint[]{9847}, - new uint[]{9838}, - new uint[]{9839}, - new uint[]{9851}, - new uint[]{9852}, - new uint[]{9840}, - new uint[]{9841}, - new uint[]{9842}, - new uint[]{9850}, - new uint[]{9843}, - new uint[]{9844}, - new uint[]{9845}, - new uint[]{9846}, - new uint[]{9849}, - new uint[]{9847}, - new uint[]{9848}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9853}, - new uint[]{9855}, - new uint[]{9856}, - new uint[]{9857}, - new uint[]{9858}, - new uint[]{9859}, - new uint[]{9860}, - new uint[]{9861}, - new uint[]{9862}, - new uint[]{9854}, - new uint[]{9853}, - new uint[]{9855}, - new uint[]{9856}, - new uint[]{9857}, - new uint[]{9858}, - new uint[]{9859}, - new uint[]{9860}, - new uint[]{9861}, - new uint[]{9862}, - new uint[]{9854}, - new uint[]{9796}, - new uint[]{9796}, - new uint[]{9796}, - new uint[]{9796}, - new uint[]{9796}, - new uint[]{9908}, - new uint[]{9909}, - new uint[]{9908}, - Array.Empty(), - new uint[]{9910}, - new uint[]{9869}, - new uint[]{9870}, - new uint[]{9871}, - new uint[]{9872}, - new uint[]{9346}, - new uint[]{9873}, - new uint[]{9875}, - new uint[]{9876}, - new uint[]{9874}, - new uint[]{9877}, - new uint[]{9878}, - new uint[]{316}, - new uint[]{9879}, - new uint[]{9881}, - new uint[]{9880}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9863}, - new uint[]{9838}, - new uint[]{9840}, - new uint[]{9841}, - new uint[]{9842}, - new uint[]{9843}, - new uint[]{9844}, - new uint[]{9849}, - new uint[]{9847}, - new uint[]{9863}, - new uint[]{9838}, - Array.Empty(), - new uint[]{9851}, - new uint[]{9840}, - new uint[]{9841}, - new uint[]{9842}, - Array.Empty(), - new uint[]{9843}, - new uint[]{9844}, - new uint[]{9849}, - new uint[]{9847}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9895}, - new uint[]{9896}, - new uint[]{9897}, - new uint[]{9332, 9898}, - new uint[]{9332, 9898}, - new uint[]{9898}, - new uint[]{9332}, - new uint[]{9398}, - new uint[]{9902}, - new uint[]{9903}, - new uint[]{9904}, - new uint[]{9905}, - new uint[]{11321}, - new uint[]{3633}, - new uint[]{9834}, - new uint[]{9836}, - Array.Empty(), - Array.Empty(), - new uint[]{9834}, - new uint[]{9835}, - new uint[]{9836}, - Array.Empty(), - new uint[]{3634}, - new uint[]{3639}, - new uint[]{3642}, - new uint[]{3632}, - new uint[]{3458}, - new uint[]{3458}, - new uint[]{11315}, - new uint[]{11316}, - new uint[]{108, 11317}, - new uint[]{11318}, - new uint[]{3632}, - new uint[]{3458}, - new uint[]{4954}, - Array.Empty(), - new uint[]{3458}, - new uint[]{11319}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9696}, - new uint[]{9696}, - new uint[]{9868}, - new uint[]{3983}, - new uint[]{11314}, - new uint[]{3635}, - new uint[]{3636}, - new uint[]{3637}, - new uint[]{3638}, - new uint[]{3640}, - new uint[]{3641}, - new uint[]{3643}, - new uint[]{3644}, - new uint[]{9696}, - new uint[]{9696}, - new uint[]{9700}, - new uint[]{9700}, - new uint[]{9700}, - new uint[]{9700}, - new uint[]{3984}, - new uint[]{3983}, - new uint[]{11314}, - new uint[]{10075}, - new uint[]{10077}, - new uint[]{10074}, - new uint[]{10013, 11270}, - new uint[]{9696}, - new uint[]{9696}, - Array.Empty(), - Array.Empty(), - new uint[]{9696}, - Array.Empty(), - new uint[]{9696}, - new uint[]{9696}, - new uint[]{4093}, - new uint[]{9864}, - new uint[]{9865}, - new uint[]{9866}, - new uint[]{9867}, - new uint[]{9871}, - new uint[]{9911}, - new uint[]{9912}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9879}, - new uint[]{9917}, - new uint[]{10103}, - new uint[]{108}, - new uint[]{9902}, - new uint[]{9879}, - new uint[]{9948}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9949}, - new uint[]{9950}, - new uint[]{9951}, - new uint[]{9918}, - new uint[]{9919}, - new uint[]{9921}, - new uint[]{9922}, - new uint[]{9664}, - new uint[]{9136}, - new uint[]{9923}, - new uint[]{10064}, - new uint[]{10063}, - new uint[]{9941}, - new uint[]{9942}, - new uint[]{9943}, - new uint[]{9944}, - new uint[]{9945}, - new uint[]{9946}, - new uint[]{10065}, - new uint[]{10066}, - new uint[]{9989}, - new uint[]{9988}, - new uint[]{9992}, - new uint[]{9993}, - new uint[]{9649}, - new uint[]{10059}, - new uint[]{9133}, - new uint[]{10004}, - new uint[]{10076}, - new uint[]{10068}, - new uint[]{10069}, - new uint[]{10070}, - new uint[]{10071}, - new uint[]{10072}, - new uint[]{10073}, - new uint[]{10007}, - new uint[]{10008}, - new uint[]{10009}, - new uint[]{108}, - Array.Empty(), - new uint[]{9955}, - new uint[]{9938}, - new uint[]{9947}, - new uint[]{9514}, - new uint[]{9429}, - new uint[]{9939}, - new uint[]{9940}, - new uint[]{10095}, - Array.Empty(), - new uint[]{9409}, - new uint[]{9390}, - new uint[]{9694}, - new uint[]{9695}, - new uint[]{9924}, - new uint[]{9928}, - new uint[]{9929}, - new uint[]{9930}, - new uint[]{9925}, - new uint[]{9926}, - new uint[]{9927}, - new uint[]{9969}, - new uint[]{9970}, - new uint[]{9971}, - new uint[]{9972}, - new uint[]{9973}, - new uint[]{9974}, - new uint[]{10057}, - new uint[]{10100}, - new uint[]{108}, - new uint[]{10097}, - new uint[]{10098}, - new uint[]{10109}, - new uint[]{10113}, - new uint[]{10108}, - new uint[]{10101}, - new uint[]{10107}, - new uint[]{108}, - new uint[]{10192}, - new uint[]{10006}, - new uint[]{9409}, - new uint[]{9694}, - new uint[]{9453}, - new uint[]{10005}, - new uint[]{9346}, - new uint[]{10010}, - new uint[]{9869}, - new uint[]{9870}, - new uint[]{10011}, - new uint[]{10013}, - new uint[]{10014}, - new uint[]{9349}, - new uint[]{9348}, - new uint[]{8378}, - new uint[]{10016}, - new uint[]{10017}, - new uint[]{10018}, - new uint[]{10019}, - new uint[]{10020}, - new uint[]{10021}, - new uint[]{10022}, - new uint[]{10024}, - new uint[]{10026}, - new uint[]{10025}, - new uint[]{10026}, - Array.Empty(), - new uint[]{10028}, - new uint[]{10029}, - new uint[]{10030}, - new uint[]{3573}, - new uint[]{10031}, - new uint[]{10032}, - new uint[]{10033}, - new uint[]{10034}, - new uint[]{10037}, - new uint[]{10041}, - new uint[]{10041}, - new uint[]{10042}, - new uint[]{10043}, - new uint[]{10044}, - new uint[]{108, 7941, 10028, 10045, 10046, 10051, 10052}, - new uint[]{10024}, - new uint[]{10026}, - new uint[]{10030}, - new uint[]{3573}, - new uint[]{10047}, - new uint[]{10048}, - new uint[]{10049}, - new uint[]{10050}, - new uint[]{10053}, - new uint[]{10054}, - new uint[]{10055}, - new uint[]{10056}, - new uint[]{10021}, - new uint[]{10016}, - new uint[]{10017}, - new uint[]{10018}, - new uint[]{10036}, - new uint[]{10038}, - new uint[]{10039}, - new uint[]{10040}, - new uint[]{10035}, - new uint[]{10012}, - new uint[]{10377}, - new uint[]{10378}, - new uint[]{10379}, - new uint[]{10380}, - new uint[]{10381}, - new uint[]{10382}, - new uint[]{10383}, - new uint[]{10384}, - new uint[]{10939}, - new uint[]{10386}, - new uint[]{10387}, - new uint[]{10388}, - new uint[]{10389}, - new uint[]{10390}, - new uint[]{10058}, - new uint[]{9956}, - new uint[]{9664}, - Array.Empty(), - new uint[]{10191}, - Array.Empty(), - new uint[]{9932}, - new uint[]{9933}, - new uint[]{9934}, - new uint[]{9935}, - new uint[]{9936}, - new uint[]{9937}, - new uint[]{108}, - new uint[]{10057}, - new uint[]{10057}, - new uint[]{9961}, - new uint[]{9962}, - new uint[]{9963}, - new uint[]{9963}, - new uint[]{9961}, - new uint[]{9963}, - new uint[]{9962}, - new uint[]{9964}, - new uint[]{9965}, - new uint[]{9966}, - new uint[]{9967}, - new uint[]{10079}, - new uint[]{10080}, - new uint[]{10081}, - new uint[]{10082}, - new uint[]{10086}, - new uint[]{11214}, - new uint[]{10456}, - new uint[]{10456}, - Array.Empty(), - Array.Empty(), - new uint[]{9384}, - new uint[]{9423}, - new uint[]{9384}, - new uint[]{9361}, - new uint[]{9388}, - new uint[]{9682}, - new uint[]{9388}, - new uint[]{10160}, - new uint[]{9429}, - new uint[]{9432}, - new uint[]{10001}, - new uint[]{9559}, - new uint[]{2142}, - new uint[]{10002}, - new uint[]{10000}, - new uint[]{10003}, - Array.Empty(), - new uint[]{9958}, - new uint[]{9959}, - new uint[]{9960}, - new uint[]{9366}, - new uint[]{9367}, - new uint[]{7954}, - new uint[]{3164}, - new uint[]{3169}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10214}, - new uint[]{9682}, - new uint[]{9564}, - new uint[]{9366}, - new uint[]{9367}, - new uint[]{7954}, - new uint[]{10177}, - new uint[]{10120}, - new uint[]{10121}, - new uint[]{10122}, - new uint[]{10123}, - new uint[]{10124}, - new uint[]{10125}, - new uint[]{10126}, - new uint[]{10130}, - new uint[]{10131}, - new uint[]{10132}, - new uint[]{10127}, - new uint[]{10133}, - new uint[]{10128}, - new uint[]{10129}, - new uint[]{10134}, - new uint[]{10135}, - new uint[]{10136}, - new uint[]{10137}, - new uint[]{10138}, - new uint[]{10139}, - new uint[]{10140}, - new uint[]{10141}, - new uint[]{10142}, - new uint[]{10143}, - new uint[]{10129}, - new uint[]{10144}, - new uint[]{10145}, - new uint[]{10146}, - new uint[]{10128}, - new uint[]{10147}, - new uint[]{10148}, - new uint[]{10149}, - new uint[]{10150}, - new uint[]{10126}, - new uint[]{10127}, - new uint[]{10151}, - new uint[]{10152}, - new uint[]{10153}, - new uint[]{10154}, - new uint[]{10155}, - new uint[]{10156}, - new uint[]{10157}, - new uint[]{10158}, - new uint[]{10159}, - new uint[]{10160}, - new uint[]{10127}, - new uint[]{10161}, - new uint[]{10162}, - new uint[]{10163}, - new uint[]{10126}, - new uint[]{10164}, - new uint[]{10165}, - new uint[]{10166}, - new uint[]{10167}, - new uint[]{10129}, - new uint[]{10168}, - new uint[]{10169}, - new uint[]{10170}, - new uint[]{10128}, - new uint[]{10171}, - new uint[]{10172}, - new uint[]{10173}, - new uint[]{10174}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{9950}, - new uint[]{9920}, - new uint[]{9950}, - new uint[]{9950}, - new uint[]{10212}, - new uint[]{10211}, - new uint[]{9947}, - Array.Empty(), - Array.Empty(), - new uint[]{9380}, - new uint[]{10210}, - new uint[]{10209}, - new uint[]{10208}, - new uint[]{10207}, - Array.Empty(), - new uint[]{10205}, - new uint[]{10204}, - new uint[]{10057}, - new uint[]{10057}, - new uint[]{10213}, - new uint[]{10203}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{9975}, - new uint[]{9976}, - new uint[]{9977}, - new uint[]{9978}, - new uint[]{9979}, - new uint[]{9980}, - new uint[]{9957}, - new uint[]{9953}, - new uint[]{9954}, - new uint[]{9954}, - new uint[]{9953}, - new uint[]{9954}, - new uint[]{9954}, - new uint[]{9432}, - new uint[]{9981}, - new uint[]{9982}, - new uint[]{9983}, - new uint[]{108}, - new uint[]{9984}, - new uint[]{9983}, - new uint[]{9985}, - new uint[]{9985}, - new uint[]{9986}, - new uint[]{9987}, - new uint[]{9366, 9896}, - new uint[]{9367, 9897}, - new uint[]{3164}, - new uint[]{3169}, - new uint[]{10175}, - new uint[]{10176}, - new uint[]{10177}, - new uint[]{9404, 9936}, - new uint[]{9375, 10178}, - new uint[]{9374, 9898}, - new uint[]{9374}, - new uint[]{9374}, - new uint[]{9374, 9901}, - new uint[]{9374, 9898, 9900}, - new uint[]{9374, 9898, 9901}, - new uint[]{9374, 9901}, - new uint[]{9946}, - new uint[]{10067}, - new uint[]{10182}, - new uint[]{10215}, - new uint[]{6529}, - new uint[]{10087}, - Array.Empty(), - new uint[]{10089}, - new uint[]{10090}, - Array.Empty(), - new uint[]{10092}, - new uint[]{10093}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{4385}, - new uint[]{3293}, - new uint[]{9975}, - new uint[]{9976}, - new uint[]{9978}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{9380}, - new uint[]{9397}, - new uint[]{10252}, - new uint[]{9425}, - new uint[]{10892}, - new uint[]{10062}, - Array.Empty(), - new uint[]{10396}, - new uint[]{10397}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10083}, - new uint[]{10084}, - new uint[]{10085}, - new uint[]{10219}, - new uint[]{10224}, - new uint[]{10242}, - new uint[]{9503}, - new uint[]{9503}, - Array.Empty(), - new uint[]{9378}, - new uint[]{10251}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{1455}, - new uint[]{11313}, - new uint[]{11312}, - new uint[]{10099}, - new uint[]{10099}, - new uint[]{10099}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10104}, - new uint[]{10105}, - new uint[]{10106}, - new uint[]{10240}, - new uint[]{10177}, - new uint[]{10184}, - new uint[]{10222}, - new uint[]{10223}, - new uint[]{10110}, - new uint[]{10111}, - new uint[]{10112}, - new uint[]{10116}, - new uint[]{108}, - new uint[]{10114}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10115}, - new uint[]{10102}, - new uint[]{10117}, - new uint[]{10118}, - new uint[]{10240}, - new uint[]{10179}, - new uint[]{10180}, - new uint[]{10183}, - new uint[]{10186}, - new uint[]{10183}, - new uint[]{10239}, - new uint[]{10229}, - new uint[]{10220}, - new uint[]{10393}, - new uint[]{10393}, - new uint[]{10394}, - Array.Empty(), - new uint[]{9425}, - new uint[]{9665}, - new uint[]{10024}, - new uint[]{10026}, - new uint[]{10030}, - new uint[]{3573}, - new uint[]{10049}, - new uint[]{10050}, - new uint[]{10055}, - new uint[]{10056}, - new uint[]{10019}, - new uint[]{10020}, - new uint[]{10186}, - new uint[]{10221}, - new uint[]{10189}, - Array.Empty(), - new uint[]{10356}, - new uint[]{10357}, - new uint[]{108}, - Array.Empty(), - new uint[]{10184}, - new uint[]{10222}, - new uint[]{10223}, - new uint[]{10234}, - new uint[]{10235}, - new uint[]{10236}, - new uint[]{10237}, - new uint[]{10243}, - new uint[]{10244}, - new uint[]{10185}, - new uint[]{9386}, - new uint[]{9967}, - new uint[]{10225}, - new uint[]{10226}, - new uint[]{10227}, - new uint[]{10228}, - new uint[]{9595}, - new uint[]{10245}, - new uint[]{10241}, - new uint[]{9936}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{10184}, - new uint[]{10182}, - new uint[]{9386}, - new uint[]{10182}, - new uint[]{9377}, - new uint[]{10187}, - new uint[]{10187}, - new uint[]{10185}, - new uint[]{10217}, - new uint[]{10181}, - new uint[]{10181}, - new uint[]{10181}, - new uint[]{10177}, - new uint[]{3164}, - new uint[]{3169}, - new uint[]{9939}, - new uint[]{10124}, - new uint[]{10216}, - new uint[]{10218}, - new uint[]{10183}, - new uint[]{10239}, - new uint[]{9519}, - new uint[]{9902}, - new uint[]{10186}, - new uint[]{6945}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9633}, - new uint[]{5851}, - new uint[]{10230}, - new uint[]{10231}, - new uint[]{10232}, - new uint[]{3459}, - new uint[]{9388}, - new uint[]{9386}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{9388}, - new uint[]{9387}, - new uint[]{9380, 9595}, - new uint[]{10216}, - new uint[]{10252}, - new uint[]{9425}, - new uint[]{9939}, - new uint[]{10238}, - new uint[]{108}, - new uint[]{4130, 11264}, - new uint[]{5239, 11265}, - new uint[]{713, 11266}, - new uint[]{1492, 11267}, - new uint[]{8378, 11268}, - new uint[]{9363, 11271}, - new uint[]{9363, 11271}, - new uint[]{9363, 11271}, - new uint[]{10586}, - new uint[]{10586}, - new uint[]{10586}, - new uint[]{10898}, - new uint[]{10898}, - new uint[]{10899}, - new uint[]{10243}, - new uint[]{10244}, - new uint[]{9695}, - new uint[]{9408}, - new uint[]{9695}, - new uint[]{108}, - new uint[]{10231}, - new uint[]{108}, - new uint[]{10207}, - new uint[]{10250}, - new uint[]{10249}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{10717}, - new uint[]{10718}, - Array.Empty(), - new uint[]{10719}, - Array.Empty(), - new uint[]{9425}, - new uint[]{10256}, - new uint[]{10257}, - new uint[]{10258}, - new uint[]{10259}, - new uint[]{10257}, - new uint[]{10256}, - new uint[]{108}, - new uint[]{10249}, - new uint[]{9543}, - new uint[]{10279}, - new uint[]{10280}, - new uint[]{10281}, - new uint[]{10282}, - new uint[]{10283}, - new uint[]{10284}, - new uint[]{10285}, - new uint[]{10286}, - new uint[]{10287}, - new uint[]{10288}, - new uint[]{10289}, - new uint[]{10419}, - new uint[]{10420}, - new uint[]{10421}, - new uint[]{10422}, - new uint[]{10423}, - new uint[]{10424}, - new uint[]{10425}, - new uint[]{10426}, - new uint[]{10427}, - new uint[]{10428}, - new uint[]{10429}, - new uint[]{10430}, - new uint[]{10431}, - new uint[]{10432}, - new uint[]{10433}, - new uint[]{10434}, - new uint[]{10435}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10403}, - new uint[]{10401}, - new uint[]{10402}, - new uint[]{10364}, - new uint[]{10365}, - new uint[]{10366}, - new uint[]{10367}, - Array.Empty(), - new uint[]{10369}, - new uint[]{11166}, - new uint[]{10371}, - new uint[]{10372}, - new uint[]{10373}, - new uint[]{10374}, - new uint[]{10375}, - new uint[]{10398}, - new uint[]{10395}, - Array.Empty(), - new uint[]{10404}, - new uint[]{10405}, - new uint[]{10406}, - new uint[]{10407}, - new uint[]{10586}, - new uint[]{10587}, - new uint[]{10588}, - new uint[]{10589}, - new uint[]{1492}, - new uint[]{10457}, - new uint[]{10458}, - new uint[]{10459}, - new uint[]{10460}, - new uint[]{10461}, - new uint[]{10462}, - new uint[]{10463}, - Array.Empty(), - new uint[]{10464}, - new uint[]{10465}, - Array.Empty(), - new uint[]{10467}, - new uint[]{10468}, - new uint[]{10469}, - new uint[]{10470}, - new uint[]{10471}, - Array.Empty(), - new uint[]{10473}, - new uint[]{10474}, - new uint[]{10475}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{10668}, - new uint[]{10669}, - new uint[]{10670}, - new uint[]{10671}, - new uint[]{10672}, - new uint[]{10673}, - new uint[]{10674}, - new uint[]{10675}, - new uint[]{10676}, - new uint[]{10677}, - new uint[]{10678}, - new uint[]{10679}, - new uint[]{108}, - new uint[]{10680}, - new uint[]{10681}, - new uint[]{10682}, - new uint[]{108}, - new uint[]{10683}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10684}, - new uint[]{10685}, - new uint[]{10686}, - new uint[]{10687}, - new uint[]{10688}, - new uint[]{10689}, - new uint[]{10690}, - new uint[]{10290}, - new uint[]{10291}, - new uint[]{10292}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10293}, - new uint[]{10762}, - new uint[]{108}, - new uint[]{10590}, - new uint[]{10591}, - new uint[]{10592}, - Array.Empty(), - new uint[]{10594}, - new uint[]{10595}, - new uint[]{10596}, - new uint[]{10597}, - new uint[]{10598}, - new uint[]{10599}, - new uint[]{10600}, - new uint[]{10601}, - new uint[]{10602}, - new uint[]{10603}, - new uint[]{10604}, - new uint[]{10605}, - new uint[]{10606}, - new uint[]{10607}, - new uint[]{10608}, - new uint[]{10609}, - new uint[]{10610}, - new uint[]{10611}, - new uint[]{10612}, - new uint[]{10613}, - new uint[]{10614}, - new uint[]{10399}, - new uint[]{10494}, - new uint[]{10495}, - new uint[]{108}, - new uint[]{10408}, - new uint[]{10409}, - new uint[]{10410}, - new uint[]{10411}, - new uint[]{10412}, - new uint[]{10413}, - new uint[]{10414}, - new uint[]{10415}, - new uint[]{10407}, - new uint[]{10406}, - new uint[]{10648}, - new uint[]{10649}, - new uint[]{10650}, - new uint[]{10651}, - new uint[]{10652}, - new uint[]{10653}, - new uint[]{10654}, - new uint[]{10655}, - new uint[]{10656}, - new uint[]{10657}, - new uint[]{10658}, - new uint[]{10659}, - new uint[]{10942}, - new uint[]{10660}, - new uint[]{10661}, - new uint[]{10662}, - new uint[]{10663}, - new uint[]{10664}, - new uint[]{10665}, - new uint[]{10666}, - new uint[]{9349}, - new uint[]{10012}, - new uint[]{8378}, - new uint[]{4846}, - new uint[]{10259}, - new uint[]{10257}, - new uint[]{10256}, - new uint[]{10409}, - new uint[]{10412}, - new uint[]{10411}, - new uint[]{10413}, - new uint[]{10416}, - new uint[]{10417}, - new uint[]{10376}, - new uint[]{10261}, - new uint[]{4149}, - new uint[]{1400}, - new uint[]{1401}, - new uint[]{1402}, - new uint[]{1403}, - new uint[]{1404}, - new uint[]{10262}, - new uint[]{10263}, - new uint[]{10264}, - new uint[]{10313}, - new uint[]{10314}, - new uint[]{10315}, - new uint[]{10316}, - new uint[]{10317}, - new uint[]{10450}, - new uint[]{10450}, - new uint[]{10400}, - new uint[]{10400}, - new uint[]{10400}, - Array.Empty(), - new uint[]{10331}, - new uint[]{10332}, - new uint[]{10333}, - new uint[]{10335}, - new uint[]{10336}, - new uint[]{10697}, - new uint[]{11217}, - new uint[]{10698}, - new uint[]{10699}, - new uint[]{10700}, - new uint[]{10701}, - new uint[]{10702}, - new uint[]{10703}, - new uint[]{10704}, - new uint[]{10705}, - new uint[]{10706}, - new uint[]{10707}, - new uint[]{10708}, - new uint[]{10709}, - new uint[]{10710}, - new uint[]{10711}, - new uint[]{10712}, - new uint[]{10713}, - new uint[]{10714}, - new uint[]{10715}, - new uint[]{10716}, - new uint[]{10334}, - new uint[]{10453}, - new uint[]{10454}, - new uint[]{10452}, - new uint[]{10453}, - new uint[]{10454}, - Array.Empty(), - new uint[]{10452}, - new uint[]{10451}, - new uint[]{10451}, - new uint[]{10448}, - new uint[]{5640}, - new uint[]{10448}, - new uint[]{10448}, - new uint[]{10447}, - new uint[]{11195}, - new uint[]{10561}, - new uint[]{10443}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10449}, - new uint[]{10449}, - new uint[]{10400}, - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10906}, - new uint[]{10907}, - new uint[]{10908}, - new uint[]{10909}, - Array.Empty(), - new uint[]{10911}, - new uint[]{10912}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10905}, - new uint[]{10267}, - new uint[]{10267}, - new uint[]{10913}, - new uint[]{10914}, - new uint[]{10915}, - new uint[]{10916}, - new uint[]{10268}, - new uint[]{10918}, - new uint[]{11070}, - new uint[]{10345}, - new uint[]{10345}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10920}, - new uint[]{10910}, - new uint[]{10497}, - new uint[]{10921}, - new uint[]{10922}, - new uint[]{10500}, - new uint[]{10501}, - new uint[]{10502}, - new uint[]{10503}, - new uint[]{10504}, - new uint[]{10505}, - new uint[]{10506}, - new uint[]{10507}, - new uint[]{10508}, - new uint[]{10509}, - new uint[]{10510}, - new uint[]{10511}, - new uint[]{10518, 11201}, - new uint[]{10497}, - new uint[]{10920}, - new uint[]{10496}, - new uint[]{10720}, - new uint[]{10721}, - Array.Empty(), - new uint[]{10722}, - new uint[]{10720}, - new uint[]{10721}, - new uint[]{10724}, - Array.Empty(), - new uint[]{10722}, - new uint[]{10725}, - new uint[]{10726}, - new uint[]{10727}, - new uint[]{10728}, - new uint[]{10318}, - new uint[]{10319}, - new uint[]{10320}, - new uint[]{10321}, - new uint[]{10322}, - new uint[]{10323}, - new uint[]{10324}, - new uint[]{10325}, - new uint[]{10326}, - new uint[]{10327}, - new uint[]{10328}, - new uint[]{10329}, - new uint[]{10330}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10871}, - new uint[]{10869}, - new uint[]{10870}, - new uint[]{10873}, - new uint[]{10874}, - new uint[]{10875, 10901}, - new uint[]{10876}, - new uint[]{10877}, - new uint[]{10872}, - new uint[]{108}, - new uint[]{10878}, - new uint[]{10879}, - new uint[]{10881}, - new uint[]{10880}, - new uint[]{10882}, - new uint[]{10883}, - new uint[]{10884}, - new uint[]{10885}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10269}, - new uint[]{10926}, - new uint[]{10271}, - new uint[]{10271}, - new uint[]{10272}, - new uint[]{10272}, - new uint[]{10269}, - new uint[]{10269}, - new uint[]{10269}, - new uint[]{10479}, - new uint[]{10480}, - new uint[]{10481}, - new uint[]{10482}, - new uint[]{10483}, - new uint[]{10484}, - new uint[]{10485}, - new uint[]{10486}, - new uint[]{10919}, - new uint[]{10488}, - new uint[]{10490}, - new uint[]{10489}, - new uint[]{10492}, - new uint[]{10491}, - new uint[]{10479}, - new uint[]{11215}, - new uint[]{10731}, - new uint[]{10732}, - new uint[]{10730}, - new uint[]{10729}, - new uint[]{4636}, - new uint[]{4637}, - new uint[]{3799}, - new uint[]{10734}, - new uint[]{10735}, - new uint[]{10455}, - new uint[]{10455}, - new uint[]{10455}, - new uint[]{10455}, - new uint[]{10455}, - new uint[]{10455}, - new uint[]{10800}, - new uint[]{10341}, - new uint[]{10343}, - new uint[]{10344}, - new uint[]{10348}, - new uint[]{10348}, - new uint[]{10348}, - new uint[]{10348}, - new uint[]{10342}, - new uint[]{10266}, - new uint[]{10295}, - new uint[]{541}, - new uint[]{10438}, - Array.Empty(), - new uint[]{10441}, - new uint[]{10439}, - new uint[]{10436}, - new uint[]{10437}, - new uint[]{10294}, - new uint[]{10295}, - new uint[]{10296}, - new uint[]{10297}, - new uint[]{10908}, - new uint[]{10299}, - new uint[]{10911}, - new uint[]{10301}, - new uint[]{10302}, - new uint[]{10303}, - new uint[]{10304}, - new uint[]{10305}, - new uint[]{10306}, - new uint[]{10307}, - new uint[]{11196}, - new uint[]{11197}, - new uint[]{10525, 11204}, - new uint[]{11198}, - new uint[]{10312}, - new uint[]{10354}, - new uint[]{108}, - new uint[]{10278}, - new uint[]{10620}, - new uint[]{10619}, - new uint[]{10625}, - new uint[]{10634}, - new uint[]{10633}, - new uint[]{10729}, - new uint[]{10730}, - new uint[]{10731}, - new uint[]{10732}, - new uint[]{10733}, - new uint[]{10734}, - new uint[]{10735}, - new uint[]{4130}, - new uint[]{10337}, - new uint[]{10337}, - new uint[]{10338}, - new uint[]{10339}, - new uint[]{10340}, - new uint[]{10615}, - new uint[]{10273}, - new uint[]{10274}, - new uint[]{10270}, - new uint[]{10274}, - new uint[]{10270}, - new uint[]{10270}, - new uint[]{10274}, - Array.Empty(), - new uint[]{10277}, - new uint[]{10275}, - new uint[]{108}, - new uint[]{10622}, - new uint[]{10621}, - new uint[]{10624}, - new uint[]{10623}, - new uint[]{10629}, - new uint[]{10923}, - new uint[]{10924}, - new uint[]{10925}, - new uint[]{10926}, - new uint[]{10927}, - new uint[]{10928}, - new uint[]{10929}, - new uint[]{10930}, - new uint[]{10931}, - new uint[]{10932}, - new uint[]{10931}, - new uint[]{10928}, - new uint[]{10933}, - new uint[]{10934}, - new uint[]{10933}, - new uint[]{10935}, - new uint[]{10936}, - new uint[]{10904, 10937}, - new uint[]{10937}, - new uint[]{10937}, - new uint[]{10938}, - new uint[]{10939}, - new uint[]{10940}, - new uint[]{10940}, - new uint[]{10940}, - new uint[]{10941}, - new uint[]{10942}, - new uint[]{10632}, - new uint[]{10626}, - new uint[]{10742}, - new uint[]{108}, - new uint[]{10743}, - new uint[]{10744}, - new uint[]{10745}, - new uint[]{10742}, - new uint[]{10743}, - new uint[]{10943}, - new uint[]{10944}, - new uint[]{10418}, - new uint[]{10414}, - new uint[]{10415}, - new uint[]{10627}, - new uint[]{10617}, - new uint[]{10630}, - new uint[]{10647}, - new uint[]{6148}, - new uint[]{9678}, - new uint[]{10772}, - new uint[]{10259}, - new uint[]{2667}, - new uint[]{2667}, - new uint[]{108}, - new uint[]{10886}, - new uint[]{10887}, - new uint[]{10888}, - new uint[]{10889}, - new uint[]{10890}, - new uint[]{10891}, - new uint[]{10945}, - new uint[]{10631}, - new uint[]{10731}, - new uint[]{10732}, - new uint[]{10400}, - new uint[]{10513}, - new uint[]{10519, 10520}, - new uint[]{10523}, - new uint[]{10528, 10529}, - new uint[]{10530, 10531, 10532}, - new uint[]{11208, 11209}, - new uint[]{10537, 10538, 10539, 10540, 10541, 10543, 10544, 10545, 10546, 11200}, - new uint[]{10548}, - new uint[]{10549, 10550, 11211}, - new uint[]{10550, 11212}, - new uint[]{10551, 11213}, - new uint[]{10552}, - new uint[]{10553}, - new uint[]{10549}, - new uint[]{10550}, - new uint[]{11213}, - new uint[]{10552}, - new uint[]{5239}, - new uint[]{4130}, - new uint[]{9363}, - new uint[]{10557}, - new uint[]{10558, 11202}, - new uint[]{10559}, - new uint[]{10549}, - new uint[]{10550, 11212}, - new uint[]{10551, 11213}, - new uint[]{10552}, - new uint[]{10560}, - new uint[]{10561}, - new uint[]{10562}, - new uint[]{6146}, - new uint[]{4740}, - new uint[]{6153}, - new uint[]{6152}, - new uint[]{6149}, - new uint[]{11210}, - new uint[]{10013}, - new uint[]{8378}, - new uint[]{1492}, - new uint[]{10572}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - new uint[]{10575}, - new uint[]{108}, - new uint[]{10400}, - new uint[]{10400}, - new uint[]{10498}, - new uint[]{4736}, - new uint[]{10555}, - new uint[]{10554}, - new uint[]{10011}, - new uint[]{10013}, - new uint[]{10571}, - new uint[]{9348}, - new uint[]{10570}, - new uint[]{10278}, - new uint[]{10278}, - new uint[]{655}, - new uint[]{11165}, - new uint[]{10526, 11205}, - new uint[]{11206}, - new uint[]{11207}, - new uint[]{10444}, - new uint[]{10512}, - new uint[]{4386}, - new uint[]{10578}, - new uint[]{10579}, - new uint[]{10580}, - new uint[]{10581}, - new uint[]{10582}, - new uint[]{10584}, - new uint[]{10585}, - new uint[]{10893}, - new uint[]{10894}, - new uint[]{10895}, - new uint[]{10896}, - new uint[]{10897}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10618}, - new uint[]{10628}, - new uint[]{10616}, - new uint[]{10476}, - new uint[]{10477}, - new uint[]{10478}, - new uint[]{10937}, - new uint[]{10277}, - new uint[]{10273}, - new uint[]{10350}, - Array.Empty(), - new uint[]{10350}, - new uint[]{10667}, - new uint[]{10691}, - new uint[]{10692}, - new uint[]{10693}, - new uint[]{10694}, - new uint[]{10695}, - new uint[]{10696}, - new uint[]{10351}, - new uint[]{10352}, - new uint[]{10353}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10349}, - new uint[]{10355}, - new uint[]{108}, - new uint[]{10359}, - new uint[]{10360}, - new uint[]{10361}, - new uint[]{10358}, - new uint[]{10635}, - new uint[]{10636}, - new uint[]{10637}, - new uint[]{10638}, - new uint[]{10639}, - new uint[]{10640}, - Array.Empty(), - new uint[]{10642}, - new uint[]{10643}, - new uint[]{10644}, - new uint[]{10645}, - new uint[]{10646}, - new uint[]{11056}, - new uint[]{11057}, - new uint[]{11056}, - new uint[]{11055}, - new uint[]{11049}, - new uint[]{11050}, - new uint[]{11052}, - new uint[]{11053}, - new uint[]{11051}, - new uint[]{11054}, - new uint[]{11053}, - new uint[]{11052}, - new uint[]{11051}, - new uint[]{11050}, - new uint[]{11049}, - new uint[]{11048}, - new uint[]{11048}, - new uint[]{11048}, - new uint[]{11047}, - new uint[]{11046}, - new uint[]{11045}, - new uint[]{11044}, - new uint[]{11043}, - new uint[]{11042}, - new uint[]{11042}, - new uint[]{11042}, - new uint[]{11041}, - new uint[]{11041}, - new uint[]{11041}, - new uint[]{11040}, - new uint[]{11039}, - new uint[]{11038}, - new uint[]{11037}, - new uint[]{11036}, - new uint[]{11036}, - new uint[]{11036}, - new uint[]{11035}, - new uint[]{11238}, - new uint[]{11033}, - new uint[]{11033}, - new uint[]{11032}, - new uint[]{11032}, - new uint[]{11031}, - new uint[]{11030}, - new uint[]{11030}, - new uint[]{11029}, - new uint[]{11028}, - new uint[]{11027}, - new uint[]{11026}, - new uint[]{11025}, - new uint[]{11024}, - new uint[]{11023}, - new uint[]{10408}, - new uint[]{10409}, - new uint[]{10408}, - new uint[]{10409}, - new uint[]{10412}, - new uint[]{10409}, - new uint[]{541}, - new uint[]{11022}, - new uint[]{11021}, - new uint[]{11020}, - new uint[]{11019}, - new uint[]{11018}, - new uint[]{11017}, - new uint[]{11016}, - new uint[]{11015}, - new uint[]{11014}, - new uint[]{11013}, - new uint[]{11012}, - new uint[]{11011}, - new uint[]{11010}, - new uint[]{11009}, - new uint[]{11008}, - new uint[]{11007}, - new uint[]{11006}, - new uint[]{11005}, - new uint[]{11004}, - new uint[]{11003}, - new uint[]{11002}, - new uint[]{11001}, - new uint[]{11000}, - new uint[]{10999}, - new uint[]{10998}, - new uint[]{10997}, - new uint[]{10996}, - new uint[]{10995}, - new uint[]{10994}, - new uint[]{10947}, - new uint[]{10993}, - new uint[]{10992}, - new uint[]{10990}, - new uint[]{10991}, - new uint[]{10990}, - new uint[]{10989}, - new uint[]{10989}, - new uint[]{10988}, - new uint[]{10988}, - new uint[]{10987}, - new uint[]{10986}, - new uint[]{10986}, - new uint[]{10985}, - new uint[]{10984}, - new uint[]{10984}, - new uint[]{10984}, - new uint[]{10983}, - new uint[]{10983}, - new uint[]{10982}, - new uint[]{10981}, - new uint[]{10980}, - new uint[]{10979}, - new uint[]{10978}, - new uint[]{10977}, - new uint[]{11047}, - new uint[]{10976}, - new uint[]{10975}, - new uint[]{10974}, - new uint[]{10973}, - new uint[]{10972}, - new uint[]{10971}, - new uint[]{10970}, - new uint[]{10969}, - new uint[]{10968}, - new uint[]{10967}, - new uint[]{10966}, - new uint[]{10965}, - new uint[]{10964}, - new uint[]{10963}, - new uint[]{10962}, - new uint[]{10961}, - new uint[]{10960}, - new uint[]{10959}, - new uint[]{10958}, - new uint[]{10957}, - new uint[]{10956}, - new uint[]{10955}, - new uint[]{10954}, - new uint[]{10953}, - new uint[]{10952}, - new uint[]{10951}, - new uint[]{10950}, - new uint[]{10949}, - new uint[]{10948}, - new uint[]{10947}, - new uint[]{10946}, - new uint[]{108}, - new uint[]{108}, - Array.Empty(), - new uint[]{2095}, - new uint[]{1383}, - Array.Empty(), - new uint[]{10276}, - new uint[]{10276}, - new uint[]{10273}, - new uint[]{11193}, - new uint[]{10270, 10274}, - new uint[]{10276}, - new uint[]{10385}, - new uint[]{10900}, - new uint[]{10391}, - new uint[]{10370}, - new uint[]{10368}, - new uint[]{10793}, - new uint[]{10794}, - new uint[]{10804}, - new uint[]{11216}, - new uint[]{10749}, - new uint[]{10750}, - new uint[]{10756}, - new uint[]{10757}, - new uint[]{10760}, - new uint[]{10758}, - new uint[]{10754}, - new uint[]{10759}, - new uint[]{10755}, - new uint[]{10761}, - new uint[]{10796}, - new uint[]{10795}, - new uint[]{10798}, - Array.Empty(), - new uint[]{10805}, - new uint[]{10807}, - new uint[]{10801}, - new uint[]{10802}, - new uint[]{10830}, - new uint[]{10803}, - new uint[]{10797}, - new uint[]{10806}, - new uint[]{10641}, - new uint[]{10923}, - new uint[]{10924}, - new uint[]{10927}, - new uint[]{10926}, - new uint[]{10490}, - new uint[]{10746}, - new uint[]{10747}, - new uint[]{10748}, - new uint[]{10751}, - new uint[]{10752}, - new uint[]{10274}, - new uint[]{10274}, - new uint[]{11144}, - new uint[]{11188}, - new uint[]{11145}, - new uint[]{11189}, - new uint[]{11189}, - new uint[]{11146}, - new uint[]{11147}, - new uint[]{11148}, - new uint[]{10819}, - new uint[]{10818}, - new uint[]{10811}, - new uint[]{10816}, - new uint[]{10812}, - new uint[]{10817}, - new uint[]{10814}, - new uint[]{10815}, - new uint[]{10810}, - new uint[]{10823}, - new uint[]{10820}, - new uint[]{10821}, - new uint[]{10822}, - new uint[]{11149}, - new uint[]{10824}, - new uint[]{11168}, - new uint[]{11169}, - new uint[]{10753}, - new uint[]{10831}, - new uint[]{10832}, - new uint[]{10773}, - new uint[]{10774}, - new uint[]{10775}, - new uint[]{10776}, - new uint[]{10777}, - new uint[]{10778}, - new uint[]{10779}, - new uint[]{10780}, - new uint[]{10781}, - new uint[]{10782}, - new uint[]{10783}, - new uint[]{10784}, - new uint[]{10736}, - new uint[]{10737}, - new uint[]{10738}, - new uint[]{10739}, - new uint[]{10740}, - new uint[]{10741}, - new uint[]{11153}, - new uint[]{11181}, - new uint[]{11180}, - new uint[]{11179}, - new uint[]{11154}, - new uint[]{11182}, - new uint[]{11183}, - new uint[]{11155}, - new uint[]{11156}, - new uint[]{11157}, - new uint[]{11158}, - new uint[]{11159}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{10785}, - new uint[]{11093}, - new uint[]{11094}, - new uint[]{11095}, - new uint[]{11096}, - new uint[]{11097}, - new uint[]{11098}, - new uint[]{11099}, - new uint[]{11100}, - new uint[]{11101}, - new uint[]{11102}, - new uint[]{11103}, - new uint[]{11104}, - new uint[]{11105}, - new uint[]{11106}, - new uint[]{11107}, - new uint[]{11108}, - new uint[]{11109}, - new uint[]{11110}, - new uint[]{11111}, - new uint[]{11112}, - new uint[]{11113}, - new uint[]{11114}, - new uint[]{11115}, - new uint[]{11116}, - new uint[]{11117}, - new uint[]{11118}, - new uint[]{11119}, - new uint[]{11120}, - new uint[]{11121}, - new uint[]{11122}, - new uint[]{11123}, - new uint[]{11124}, - new uint[]{11125}, - new uint[]{11126}, - new uint[]{11127}, - new uint[]{11128}, - new uint[]{11129}, - new uint[]{11130}, - new uint[]{11131}, - new uint[]{11132}, - new uint[]{11133}, - new uint[]{11134}, - new uint[]{11135}, - new uint[]{11136}, - new uint[]{11137}, - new uint[]{11138}, - new uint[]{11139}, - new uint[]{11140}, - new uint[]{11141}, - new uint[]{11142}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{11150}, - new uint[]{11151}, - new uint[]{11152}, - Array.Empty(), - Array.Empty(), - new uint[]{10813}, - new uint[]{10771}, - new uint[]{10790}, - new uint[]{10791}, - new uint[]{10786}, - new uint[]{10787}, - new uint[]{10788}, - new uint[]{10789}, - new uint[]{10813}, - new uint[]{10376}, - new uint[]{11077}, - new uint[]{11184}, - new uint[]{11078}, - new uint[]{11185}, - new uint[]{11186}, - new uint[]{11079}, - new uint[]{11080}, - new uint[]{11081}, - new uint[]{11082}, - new uint[]{11083}, - new uint[]{11084}, - new uint[]{11085}, - new uint[]{11187}, - new uint[]{11086}, - new uint[]{11087}, - new uint[]{11088}, - new uint[]{11089}, - new uint[]{11090}, - new uint[]{11091}, - new uint[]{11092}, - new uint[]{10825}, - new uint[]{10826}, - new uint[]{10721}, - new uint[]{10721}, - new uint[]{10827, 10828}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10833}, - new uint[]{10834}, - new uint[]{10835}, - new uint[]{10836}, - new uint[]{10837}, - new uint[]{10838}, - new uint[]{10839}, - new uint[]{10840}, - new uint[]{10841}, - new uint[]{10842}, - new uint[]{10843}, - new uint[]{10844}, - new uint[]{10845}, - new uint[]{10846}, - new uint[]{10847}, - new uint[]{10848}, - new uint[]{10849}, - new uint[]{10850}, - new uint[]{10851}, - new uint[]{10852}, - new uint[]{10853}, - new uint[]{10854}, - new uint[]{10855}, - new uint[]{10856}, - new uint[]{10857}, - new uint[]{10858}, - new uint[]{10859}, - new uint[]{10860}, - new uint[]{10861}, - new uint[]{10862}, - new uint[]{10863}, - new uint[]{10864}, - new uint[]{10865}, - new uint[]{10866}, - new uint[]{10867}, - new uint[]{10868}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{10347}, - new uint[]{11176}, - new uint[]{11177}, - new uint[]{11178}, - new uint[]{11174}, - new uint[]{11175}, - new uint[]{655}, - new uint[]{11165}, - new uint[]{8273}, - Array.Empty(), - new uint[]{10577}, - new uint[]{11073}, - new uint[]{11074}, - new uint[]{11075}, - new uint[]{11076}, - new uint[]{11072}, - new uint[]{11167}, - new uint[]{10365}, - new uint[]{11143}, - new uint[]{11160}, - new uint[]{11143}, - new uint[]{11161}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11172}, - new uint[]{11173}, - new uint[]{10542, 10547, 11203}, - new uint[]{11191}, - new uint[]{5964}, - new uint[]{108}, - new uint[]{108}, - new uint[]{10310}, - new uint[]{10536}, - new uint[]{11195}, - new uint[]{108}, - new uint[]{10315}, - new uint[]{10908}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11192}, - new uint[]{11192}, - new uint[]{11192}, - new uint[]{108}, - Array.Empty(), - new uint[]{10726}, - new uint[]{11194}, - new uint[]{10908}, - new uint[]{2137}, - new uint[]{2139}, - new uint[]{2140}, - new uint[]{2141}, - new uint[]{2138}, - new uint[]{2324}, - new uint[]{2142}, - Array.Empty(), - new uint[]{10270, 10274}, - new uint[]{10499}, - new uint[]{11199}, - new uint[]{10559}, - new uint[]{510, 2137}, - new uint[]{108}, - new uint[]{10348}, - new uint[]{10348}, - new uint[]{10904}, - new uint[]{11239}, - new uint[]{11240}, - new uint[]{11171}, - new uint[]{11170}, - new uint[]{554}, - new uint[]{11307}, - new uint[]{2134}, - new uint[]{2135}, - new uint[]{2121}, - new uint[]{2136}, - new uint[]{11285}, - new uint[]{11285}, - new uint[]{9947}, - new uint[]{11241}, - new uint[]{11242}, - new uint[]{11243}, - new uint[]{548}, - new uint[]{108}, - new uint[]{10893}, - new uint[]{10894}, - new uint[]{10895}, - new uint[]{10896}, - new uint[]{10893}, - new uint[]{10894}, - new uint[]{10895}, - new uint[]{10896}, - new uint[]{11350}, - new uint[]{11351}, - new uint[]{11353}, - new uint[]{11244}, - new uint[]{11245}, - new uint[]{11246}, - new uint[]{11247}, - new uint[]{11248}, - new uint[]{11245}, - new uint[]{11246}, - new uint[]{11249}, - new uint[]{11248}, - new uint[]{11250}, - new uint[]{11251}, - new uint[]{11252}, - new uint[]{11253}, - new uint[]{11254}, - new uint[]{11255}, - new uint[]{11256}, - new uint[]{11257}, - new uint[]{11258}, - new uint[]{11259}, - new uint[]{11260}, - new uint[]{11261}, - new uint[]{11262}, - new uint[]{11272}, - new uint[]{11286}, - new uint[]{11290}, - new uint[]{11244}, - new uint[]{11245}, - new uint[]{11246}, - new uint[]{11247}, - new uint[]{11248}, - new uint[]{11308}, - new uint[]{422}, - new uint[]{428}, - new uint[]{424}, - new uint[]{11218}, - new uint[]{11219}, - new uint[]{11220}, - new uint[]{11221}, - new uint[]{11222}, - new uint[]{11223}, - new uint[]{11224}, - new uint[]{11225}, - new uint[]{11226}, - new uint[]{11227}, - new uint[]{11228}, - new uint[]{11229}, - new uint[]{11230}, - new uint[]{11231}, - new uint[]{11232}, - new uint[]{11233}, - new uint[]{11234}, - new uint[]{11235}, - new uint[]{11236}, - new uint[]{11237}, - new uint[]{10448}, - new uint[]{5640}, - new uint[]{10448}, - new uint[]{10446}, - new uint[]{10445}, - new uint[]{10444}, - new uint[]{1279}, - new uint[]{11352}, - new uint[]{1678}, - new uint[]{9910}, - new uint[]{101}, - new uint[]{557}, - new uint[]{2106}, - new uint[]{2109}, - new uint[]{2116}, - new uint[]{2118}, - new uint[]{11216}, - new uint[]{9910}, - new uint[]{11277}, - new uint[]{11278}, - new uint[]{11322}, - new uint[]{11279}, - new uint[]{11280}, - new uint[]{11302}, - new uint[]{11303}, - new uint[]{11304}, - new uint[]{11305}, - new uint[]{11292}, - new uint[]{11293}, - new uint[]{11294}, - new uint[]{2124}, - new uint[]{2125}, - new uint[]{2126}, - new uint[]{2128}, - new uint[]{2130}, - new uint[]{2092}, - new uint[]{2121}, - new uint[]{2089}, - new uint[]{297}, - new uint[]{2133}, - new uint[]{1486}, - new uint[]{2105}, - new uint[]{2106}, - new uint[]{2107}, - new uint[]{2108}, - new uint[]{2109}, - new uint[]{2110}, - new uint[]{2111}, - new uint[]{2112}, - new uint[]{2113}, - new uint[]{2121}, - new uint[]{269}, - Array.Empty(), - new uint[]{1486}, - new uint[]{2160}, - new uint[]{11275}, - new uint[]{11276}, - new uint[]{11273}, - new uint[]{11274}, - new uint[]{11274}, - new uint[]{2137}, - new uint[]{2141}, - new uint[]{2140}, - new uint[]{1804}, - new uint[]{2139}, - new uint[]{2137}, - new uint[]{2142}, - new uint[]{2138}, - new uint[]{11286}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{11290}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{11281}, - new uint[]{11282}, - Array.Empty(), - new uint[]{108}, - new uint[]{11310}, - new uint[]{11309}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{9344}, - new uint[]{11284}, - new uint[]{108}, - new uint[]{11419}, - new uint[]{2132}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11446}, - new uint[]{718}, - new uint[]{719}, - new uint[]{720}, - new uint[]{721}, - new uint[]{722}, - new uint[]{723}, - new uint[]{724}, - new uint[]{725}, - new uint[]{11258}, - new uint[]{11288}, - new uint[]{11289}, - new uint[]{3369}, - new uint[]{3370}, - new uint[]{3371}, - new uint[]{3374}, - new uint[]{3375}, - new uint[]{3375}, - new uint[]{3375}, - new uint[]{3375}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{2143}, - new uint[]{4384}, - new uint[]{2183}, - new uint[]{11326}, - new uint[]{11327}, - new uint[]{11328}, - new uint[]{11329}, - new uint[]{11330}, - new uint[]{11331}, - new uint[]{11332}, - new uint[]{11333}, - new uint[]{11330}, - new uint[]{11331}, - new uint[]{11332}, - new uint[]{11333}, - new uint[]{11334}, - new uint[]{11331, 11335}, - new uint[]{11332, 11336}, - new uint[]{11333, 11337}, - Array.Empty(), - new uint[]{269}, - new uint[]{297}, - new uint[]{11420}, - new uint[]{11421}, - new uint[]{11233}, - new uint[]{297}, - new uint[]{7636}, - Array.Empty(), - new uint[]{108}, - new uint[]{108}, - new uint[]{3667}, - new uint[]{11338}, - new uint[]{11339}, - new uint[]{11340}, - new uint[]{11341}, - new uint[]{10961}, - new uint[]{11342}, - new uint[]{11343}, - new uint[]{11344}, - new uint[]{11345}, - new uint[]{11346}, - new uint[]{11346}, - new uint[]{11346}, - new uint[]{11347}, - new uint[]{11348}, - new uint[]{11349}, - new uint[]{11323}, - new uint[]{11324}, - new uint[]{11325}, - new uint[]{11277}, - new uint[]{11302}, - new uint[]{11382}, - new uint[]{11382}, - new uint[]{11382}, - new uint[]{11354}, - new uint[]{428}, - new uint[]{11355}, - new uint[]{11355}, - new uint[]{11384}, - Array.Empty(), - new uint[]{11355}, - new uint[]{3455}, - new uint[]{10308}, - new uint[]{11386}, - new uint[]{108}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - new uint[]{3040}, - new uint[]{3042}, - new uint[]{3044}, - new uint[]{3045}, - Array.Empty(), - new uint[]{11357}, - new uint[]{11358}, - new uint[]{11359}, - new uint[]{11360}, - new uint[]{11361}, - new uint[]{11362}, - new uint[]{11363}, - new uint[]{11364}, - new uint[]{11365}, - new uint[]{11366}, - new uint[]{11367}, - new uint[]{11368}, - new uint[]{11382}, - new uint[]{11422}, - Array.Empty(), - new uint[]{11442}, - new uint[]{11443}, - new uint[]{11330}, - new uint[]{11313}, - new uint[]{11313}, - new uint[]{10013}, - new uint[]{4130}, - new uint[]{4130}, - new uint[]{11431}, - new uint[]{11433}, - new uint[]{2077}, - new uint[]{1455}, - new uint[]{1455}, - new uint[]{10261}, - new uint[]{10261}, - new uint[]{4130}, - new uint[]{4130}, - new uint[]{4149}, - new uint[]{4149}, - new uint[]{11447}, - new uint[]{11387}, - new uint[]{11389}, - new uint[]{11390}, - new uint[]{11391}, - new uint[]{11392}, - Array.Empty(), - new uint[]{11387}, - new uint[]{11388}, - new uint[]{11389}, - new uint[]{11390}, - new uint[]{11393}, - new uint[]{11393}, - new uint[]{11394}, - new uint[]{11394}, - new uint[]{11395}, - new uint[]{11395}, - new uint[]{11396}, - new uint[]{11396}, - new uint[]{11397}, - new uint[]{11397}, - new uint[]{11407}, - new uint[]{11408}, - new uint[]{11409}, - new uint[]{11410}, - new uint[]{11407}, - new uint[]{11411}, - Array.Empty(), - new uint[]{11413}, - new uint[]{11414}, - new uint[]{11415}, - new uint[]{11415}, - new uint[]{11416}, - new uint[]{11417}, - new uint[]{11418}, - new uint[]{11418}, - Array.Empty(), - new uint[]{108}, - Array.Empty(), - new uint[]{11372}, - new uint[]{11373}, - new uint[]{11373}, - new uint[]{108}, - new uint[]{11443}, - new uint[]{3458}, - new uint[]{3460}, - new uint[]{3462}, - new uint[]{3464}, - new uint[]{10298}, - new uint[]{11398}, - new uint[]{10300}, - new uint[]{10298}, - new uint[]{11398}, - new uint[]{10300}, - new uint[]{108}, - new uint[]{8378}, - new uint[]{11416}, - new uint[]{11416}, - Array.Empty(), - Array.Empty(), - new uint[]{4776}, - new uint[]{4776}, - new uint[]{4777}, - new uint[]{4778}, - new uint[]{4779}, - new uint[]{4780}, - new uint[]{4776}, - new uint[]{11393}, - new uint[]{11394}, - new uint[]{11395}, - new uint[]{11396}, - new uint[]{11397}, - new uint[]{11440}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11441}, - new uint[]{11440}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11441}, - Array.Empty(), - new uint[]{11369}, - new uint[]{11370}, - new uint[]{11371}, - new uint[]{11369}, - new uint[]{11370}, - new uint[]{11371}, - new uint[]{11369}, - new uint[]{11370}, - new uint[]{11371}, - new uint[]{11469}, - new uint[]{11470}, - new uint[]{11471}, - new uint[]{11472}, - new uint[]{11473}, - Array.Empty(), - new uint[]{11474}, - Array.Empty(), - new uint[]{11476}, - new uint[]{11477}, - new uint[]{11478}, - new uint[]{11479}, - new uint[]{11480}, - new uint[]{11481}, - Array.Empty(), - Array.Empty(), - new uint[]{11484}, - new uint[]{11485}, - new uint[]{11486}, - new uint[]{11487}, - new uint[]{11488}, - new uint[]{11490}, - new uint[]{11491}, - new uint[]{11492}, - new uint[]{11387}, - new uint[]{11388}, - new uint[]{11389}, - new uint[]{11390}, - new uint[]{11493}, - new uint[]{11494}, - new uint[]{11495}, - Array.Empty(), - new uint[]{11496}, - new uint[]{11497}, - new uint[]{11498}, - new uint[]{11499}, - new uint[]{11500}, - new uint[]{11501}, - new uint[]{11502}, - new uint[]{11503}, - new uint[]{11504}, - new uint[]{11506}, - new uint[]{11507}, - new uint[]{11508}, - new uint[]{11509}, - new uint[]{11489}, - new uint[]{11505}, - Array.Empty(), - new uint[]{11374}, - new uint[]{11375}, - new uint[]{11376}, - new uint[]{11378}, - new uint[]{11379}, - new uint[]{11374}, - new uint[]{11375}, - new uint[]{11376}, - new uint[]{11377}, - new uint[]{11378}, - new uint[]{11379}, - new uint[]{11380}, - new uint[]{108}, - new uint[]{11381}, - new uint[]{11462}, - new uint[]{11381}, - new uint[]{11462}, - new uint[]{11462}, - new uint[]{392}, - new uint[]{795}, - new uint[]{5}, - new uint[]{6}, - new uint[]{341}, - new uint[]{11448}, - new uint[]{37}, - new uint[]{262}, - new uint[]{11449}, - new uint[]{11450}, - new uint[]{393}, - new uint[]{11451}, - new uint[]{11452}, - new uint[]{11453}, - new uint[]{780}, - new uint[]{6942}, - new uint[]{11454}, - new uint[]{10173}, - new uint[]{357}, - new uint[]{11455}, - new uint[]{11456}, - new uint[]{11457}, - new uint[]{1281}, - new uint[]{2769}, - new uint[]{11458}, - new uint[]{11459}, - new uint[]{3499}, - new uint[]{11460}, - new uint[]{353}, - new uint[]{12313}, - new uint[]{12456}, - new uint[]{12457}, - new uint[]{12458}, - new uint[]{12459}, - new uint[]{12460}, - Array.Empty(), - Array.Empty(), - new uint[]{3330}, - new uint[]{11423}, - new uint[]{11424}, - new uint[]{11425}, - new uint[]{11426}, - new uint[]{11427}, - new uint[]{11428}, - new uint[]{11429}, - new uint[]{11430}, - new uint[]{11987}, - new uint[]{11988}, - new uint[]{11989}, - new uint[]{11990}, - new uint[]{11991}, - new uint[]{11439}, - new uint[]{11439}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11438}, - new uint[]{11436}, - new uint[]{11431}, - new uint[]{11432}, - Array.Empty(), - Array.Empty(), - new uint[]{11435}, - new uint[]{11436}, - Array.Empty(), - Array.Empty(), - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{2168}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11438}, - new uint[]{11436}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{3323}, - new uint[]{3323}, - new uint[]{3323}, - new uint[]{3323}, - new uint[]{3323}, - new uint[]{3321}, - new uint[]{3323}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3326}, - new uint[]{3329}, - new uint[]{3321}, - new uint[]{3329}, - new uint[]{3323}, - new uint[]{3321}, - new uint[]{3323}, - new uint[]{3326}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{3324}, - new uint[]{3323}, - new uint[]{3323}, - new uint[]{3327}, - new uint[]{3326}, - new uint[]{3321}, - new uint[]{3326}, - new uint[]{3322}, - new uint[]{3329}, - new uint[]{11431}, - new uint[]{11436}, - new uint[]{11399}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11517}, - new uint[]{11399}, - new uint[]{108}, - new uint[]{108}, - new uint[]{11517}, - new uint[]{11405}, - new uint[]{11405}, - new uint[]{11404}, - new uint[]{11404}, - new uint[]{11513}, - new uint[]{11481}, - new uint[]{11511}, - new uint[]{11512}, - new uint[]{11514}, - new uint[]{11510}, - new uint[]{11506}, - new uint[]{11515}, - new uint[]{108}, - new uint[]{11513}, - new uint[]{11481}, - new uint[]{11511}, - new uint[]{11512}, - new uint[]{11514}, - new uint[]{11510}, - new uint[]{11506}, - new uint[]{11515}, - new uint[]{11995}, - new uint[]{11996}, - new uint[]{108}, - new uint[]{11402}, - new uint[]{11406}, - new uint[]{11406}, - Array.Empty(), - Array.Empty(), - new uint[]{11462}, - new uint[]{11462}, - new uint[]{11391}, - new uint[]{11468}, - new uint[]{12054}, - new uint[]{11405}, - Array.Empty(), - Array.Empty(), - new uint[]{11521}, - new uint[]{11522}, - new uint[]{11523}, - new uint[]{11524}, - new uint[]{11525}, - new uint[]{11526}, - new uint[]{11527}, - new uint[]{11528}, - new uint[]{11529}, - new uint[]{11530}, - new uint[]{11531}, - new uint[]{11532}, - new uint[]{11533}, - new uint[]{11534}, - new uint[]{11535}, - new uint[]{11536}, - new uint[]{11537}, - new uint[]{11538}, - new uint[]{11539}, - new uint[]{11540}, - new uint[]{11541}, - new uint[]{11542}, - new uint[]{11543}, - new uint[]{11544}, - new uint[]{11545}, - new uint[]{11546}, - new uint[]{11547}, - new uint[]{11548}, - new uint[]{11549}, - new uint[]{11550}, - new uint[]{11551}, - new uint[]{11552}, - new uint[]{11553}, - new uint[]{11554}, - new uint[]{11555}, - new uint[]{11556}, - new uint[]{11557}, - new uint[]{11558}, - new uint[]{11559}, - new uint[]{11560}, - new uint[]{11561}, - new uint[]{11562}, - new uint[]{11563}, - new uint[]{11564}, - new uint[]{11565}, - new uint[]{11566}, - new uint[]{11567}, - new uint[]{11568}, - new uint[]{11569}, - new uint[]{11570}, - new uint[]{11571}, - new uint[]{11572}, - new uint[]{11573}, - new uint[]{11574}, - new uint[]{11575}, - new uint[]{11576}, - new uint[]{11577}, - new uint[]{11578}, - new uint[]{11579}, - new uint[]{11580}, - new uint[]{11581}, - new uint[]{11582}, - new uint[]{11583}, - new uint[]{11584}, - new uint[]{11585}, - new uint[]{11586}, - Array.Empty(), - new uint[]{11588}, - new uint[]{11589}, - new uint[]{11590}, - Array.Empty(), - new uint[]{11592}, - new uint[]{11593}, - new uint[]{11594}, - new uint[]{11595}, - new uint[]{11596}, - new uint[]{11597}, - new uint[]{11598}, - new uint[]{11599}, - new uint[]{11600}, - new uint[]{11601}, - new uint[]{11602}, - new uint[]{11603}, - new uint[]{11604}, - new uint[]{11605}, - new uint[]{11606}, - new uint[]{11607}, - new uint[]{11608}, - new uint[]{11609}, - new uint[]{11610}, - new uint[]{11611}, - new uint[]{11612}, - new uint[]{11613}, - new uint[]{11614}, - new uint[]{11615}, - new uint[]{11616}, - new uint[]{11617}, - new uint[]{11618}, - new uint[]{11619}, - new uint[]{11620}, - new uint[]{11621}, - new uint[]{11622}, - new uint[]{11623}, - new uint[]{11624}, - new uint[]{11625}, - new uint[]{11626}, - new uint[]{11627}, - new uint[]{11628}, - new uint[]{11629}, - new uint[]{11630}, - new uint[]{11631}, - new uint[]{11632}, - new uint[]{11633}, - new uint[]{11634}, - new uint[]{11635}, - new uint[]{11636}, - new uint[]{11637}, - new uint[]{11638}, - new uint[]{11639}, - new uint[]{11640}, - new uint[]{11641}, - new uint[]{11642}, - new uint[]{11643}, - new uint[]{11644}, - new uint[]{11645}, - new uint[]{11646}, - new uint[]{11647}, - new uint[]{11648}, - new uint[]{11649}, - new uint[]{11650}, - new uint[]{11651}, - new uint[]{11652}, - new uint[]{11653}, - new uint[]{11654}, - new uint[]{11655}, - new uint[]{11656}, - new uint[]{11657}, - new uint[]{11658}, - new uint[]{11659}, - new uint[]{11660}, - new uint[]{11661}, - new uint[]{11662}, - new uint[]{11663}, - new uint[]{11664}, - new uint[]{11665}, - new uint[]{11666}, - new uint[]{11667}, - new uint[]{11668}, - new uint[]{11669}, - new uint[]{11670}, - new uint[]{11671}, - new uint[]{11672}, - Array.Empty(), - new uint[]{11674}, - new uint[]{11675}, - new uint[]{11676}, - new uint[]{11677}, - new uint[]{11678}, - new uint[]{11679}, - new uint[]{11680}, - new uint[]{11681}, - new uint[]{11682}, - new uint[]{11683}, - new uint[]{11684}, - new uint[]{11685}, - new uint[]{11686}, - new uint[]{11687}, - new uint[]{11688}, - new uint[]{11689}, - new uint[]{11690}, - new uint[]{11691}, - new uint[]{11692}, - new uint[]{11693}, - new uint[]{11694}, - new uint[]{11695}, - new uint[]{11696}, - new uint[]{11697}, - new uint[]{11698}, - new uint[]{11699}, - new uint[]{11700}, - new uint[]{11701}, - new uint[]{11702}, - new uint[]{11703}, - new uint[]{11704}, - new uint[]{11705}, - new uint[]{11706}, - new uint[]{11707}, - new uint[]{11708}, - new uint[]{11709}, - new uint[]{11710}, - new uint[]{11711}, - new uint[]{11712}, - new uint[]{11713}, - new uint[]{11714}, - new uint[]{11715}, - new uint[]{11716}, - new uint[]{11717}, - new uint[]{11718}, - new uint[]{11719}, - new uint[]{11720}, - new uint[]{11721}, - new uint[]{11722}, - new uint[]{11723}, - new uint[]{11724}, - new uint[]{11725}, - new uint[]{11726}, - new uint[]{11727}, - new uint[]{11728}, - new uint[]{11729}, - new uint[]{11730}, - new uint[]{11731}, - new uint[]{11732}, - new uint[]{11733}, - new uint[]{11734}, - new uint[]{11735}, - new uint[]{11736}, - new uint[]{11737}, - new uint[]{11738}, - new uint[]{11739}, - new uint[]{11740}, - new uint[]{11741}, - new uint[]{11742}, - Array.Empty(), - new uint[]{11744}, - new uint[]{11745}, - new uint[]{11746}, - new uint[]{11747}, - new uint[]{11748}, - new uint[]{11749}, - new uint[]{11750}, - new uint[]{11751}, - new uint[]{11752}, - new uint[]{11753}, - new uint[]{11754}, - new uint[]{11755}, - new uint[]{11756}, - new uint[]{11757}, - new uint[]{11758}, - new uint[]{11759}, - new uint[]{11760}, - new uint[]{11761}, - new uint[]{11762}, - new uint[]{11763}, - new uint[]{11764}, - new uint[]{11765}, - new uint[]{11766}, - new uint[]{11767}, - new uint[]{11768}, - new uint[]{11769}, - new uint[]{11770}, - new uint[]{11771}, - new uint[]{11772}, - new uint[]{11773}, - new uint[]{11774}, - new uint[]{11775}, - new uint[]{11776}, - new uint[]{11777}, - new uint[]{11778}, - new uint[]{11779}, - new uint[]{11780}, - new uint[]{11781}, - new uint[]{11782}, - new uint[]{11783}, - new uint[]{11784}, - new uint[]{11785}, - new uint[]{11786}, - new uint[]{11787}, - new uint[]{11788}, - new uint[]{11789}, - new uint[]{11790}, - new uint[]{11791}, - new uint[]{11792}, - new uint[]{11793}, - new uint[]{11794}, - new uint[]{11795}, - new uint[]{11796}, - new uint[]{11797}, - new uint[]{11798}, - new uint[]{11799}, - new uint[]{11800}, - new uint[]{11801}, - new uint[]{11802}, - new uint[]{11803}, - new uint[]{11804}, - new uint[]{11805}, - new uint[]{11806}, - new uint[]{11807}, - new uint[]{11808}, - new uint[]{11809}, - new uint[]{11810}, - new uint[]{11811}, - new uint[]{11812}, - new uint[]{11813}, - new uint[]{11814}, - new uint[]{11815}, - new uint[]{11816}, - new uint[]{11817}, - new uint[]{11818}, - new uint[]{11819}, - new uint[]{11820}, - new uint[]{11821}, - new uint[]{11822}, - new uint[]{11823}, - new uint[]{11824}, - new uint[]{11825}, - new uint[]{11826}, - new uint[]{11827}, - new uint[]{11828}, - new uint[]{11829}, - new uint[]{11830}, - new uint[]{11831}, - new uint[]{11832}, - new uint[]{11833}, - new uint[]{11834}, - new uint[]{11835}, - new uint[]{11836}, - new uint[]{11837}, - new uint[]{11838}, - new uint[]{11839}, - new uint[]{11840}, - new uint[]{11841}, - new uint[]{11842}, - new uint[]{11843}, - new uint[]{11844}, - new uint[]{11845}, - new uint[]{11846}, - new uint[]{11847}, - new uint[]{11848}, - new uint[]{11849}, - new uint[]{11850}, - new uint[]{11851}, - new uint[]{11852}, - new uint[]{11853}, - new uint[]{11854}, - new uint[]{11855}, - new uint[]{11856}, - new uint[]{11857}, - new uint[]{11858}, - new uint[]{11859}, - new uint[]{11860}, - new uint[]{11861}, - new uint[]{11862}, - new uint[]{11863}, - new uint[]{11864}, - new uint[]{11865}, - new uint[]{11866}, - new uint[]{11867}, - new uint[]{11868}, - new uint[]{11869}, - new uint[]{11870}, - new uint[]{11871}, - new uint[]{11872}, - new uint[]{11873}, - new uint[]{11874}, - new uint[]{11875}, - new uint[]{11876}, - new uint[]{11877}, - new uint[]{11878}, - new uint[]{11879}, - new uint[]{11880}, - new uint[]{11881}, - new uint[]{11882}, - new uint[]{11883}, - new uint[]{11884}, - new uint[]{11885}, - new uint[]{11886}, - new uint[]{11887}, - new uint[]{11888}, - new uint[]{11889}, - new uint[]{11890}, - new uint[]{11891}, - new uint[]{11892}, - new uint[]{11893}, - new uint[]{11894}, - new uint[]{11895}, - new uint[]{11896}, - new uint[]{11897}, - new uint[]{11898}, - new uint[]{11899}, - new uint[]{11900}, - new uint[]{11901}, - new uint[]{11902}, - new uint[]{11903}, - new uint[]{11904}, - new uint[]{11905}, - new uint[]{11906}, - new uint[]{11907}, - new uint[]{11908}, - new uint[]{11909}, - new uint[]{11910}, - new uint[]{11911}, - new uint[]{11912}, - new uint[]{11913}, - new uint[]{11914}, - new uint[]{11915}, - new uint[]{11916}, - new uint[]{11917}, - new uint[]{11918}, - new uint[]{11919}, - new uint[]{11920}, - new uint[]{11921}, - new uint[]{11922}, - new uint[]{11923}, - new uint[]{11924}, - new uint[]{11925}, - new uint[]{11926}, - new uint[]{11927}, - new uint[]{11928}, - new uint[]{11929}, - new uint[]{11930}, - new uint[]{11931}, - new uint[]{11932}, - new uint[]{11933}, - new uint[]{11934}, - new uint[]{11935}, - new uint[]{11936}, - new uint[]{11937}, - new uint[]{11938}, - new uint[]{11939}, - new uint[]{11940}, - new uint[]{11941}, - new uint[]{11942}, - new uint[]{11943}, - new uint[]{11944}, - new uint[]{11945}, - new uint[]{11946}, - new uint[]{11947}, - new uint[]{11948}, - new uint[]{11949}, - new uint[]{11950}, - new uint[]{11951}, - new uint[]{11952}, - new uint[]{11953}, - new uint[]{11954}, - new uint[]{11955}, - new uint[]{11956}, - new uint[]{11957}, - new uint[]{11958}, - new uint[]{11959}, - new uint[]{11960}, - new uint[]{11961}, - new uint[]{11962}, - new uint[]{11963}, - new uint[]{11964}, - new uint[]{11965}, - new uint[]{11966}, - new uint[]{11967}, - new uint[]{11968}, - new uint[]{11969}, - new uint[]{11970}, - new uint[]{11971}, - new uint[]{11972}, - new uint[]{11973}, - Array.Empty(), - new uint[]{11975}, - new uint[]{11976}, - new uint[]{11977}, - new uint[]{11978}, - new uint[]{11979}, - new uint[]{11980}, - Array.Empty(), - new uint[]{11982}, - new uint[]{11983}, - new uint[]{11984}, - new uint[]{11985}, - new uint[]{11986}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - Array.Empty(), - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{11433}, - new uint[]{3321}, - new uint[]{3321}, - new uint[]{3326}, - new uint[]{3326}, - new uint[]{11463}, - new uint[]{11464}, - new uint[]{11465}, - new uint[]{11466}, - new uint[]{11467}, - new uint[]{11519}, - new uint[]{11520}, - new uint[]{12062}, - new uint[]{12061}, - new uint[]{12094}, - new uint[]{12081}, - Array.Empty(), - new uint[]{12095}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12082}, - new uint[]{12088}, - new uint[]{12087}, - new uint[]{12086}, - new uint[]{12084}, - new uint[]{12085}, - new uint[]{12083}, - new uint[]{3329}, - new uint[]{3329}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{11431}, - new uint[]{12100}, - new uint[]{12101}, - new uint[]{11992}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{12060}, - new uint[]{108}, - new uint[]{9363}, - new uint[]{108}, - new uint[]{12045}, - new uint[]{12043}, - new uint[]{12046}, - new uint[]{12047}, - new uint[]{12048}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{12269}, - new uint[]{12052}, - new uint[]{12250}, - new uint[]{12251}, - new uint[]{12252}, - new uint[]{12253}, - new uint[]{12254}, - new uint[]{12255}, - new uint[]{4954}, - new uint[]{12247}, - new uint[]{12248}, - Array.Empty(), - new uint[]{12246}, - new uint[]{12316}, - new uint[]{12263}, - new uint[]{12264}, - new uint[]{12079}, - new uint[]{12080}, - new uint[]{108}, - new uint[]{108}, - new uint[]{12102}, - new uint[]{12103}, - new uint[]{12104}, - new uint[]{12105}, - new uint[]{11997}, - new uint[]{11998}, - new uint[]{11999}, - new uint[]{12000}, - new uint[]{12001}, - new uint[]{12002}, - new uint[]{12003}, - new uint[]{12004}, - new uint[]{12005}, - new uint[]{12006}, - new uint[]{12007}, - new uint[]{12008}, - new uint[]{12009}, - Array.Empty(), - new uint[]{12010}, - new uint[]{12011}, - new uint[]{12012}, - new uint[]{12013}, - new uint[]{12014}, - new uint[]{12015}, - new uint[]{12016}, - new uint[]{12017}, - new uint[]{12018}, - new uint[]{12019}, - new uint[]{12020}, - new uint[]{12021}, - new uint[]{12022}, - new uint[]{12023}, - new uint[]{12024}, - new uint[]{12025}, - new uint[]{12026}, - new uint[]{12027}, - new uint[]{12028}, - new uint[]{12029}, - new uint[]{12030}, - new uint[]{12031}, - new uint[]{12032}, - new uint[]{12033}, - new uint[]{12034}, - new uint[]{12035}, - new uint[]{12036}, - new uint[]{12037}, - new uint[]{12038}, - new uint[]{12039}, - new uint[]{12040}, - new uint[]{12240}, - new uint[]{12241}, - new uint[]{12078}, - new uint[]{12097}, - new uint[]{12098}, - new uint[]{12099}, - new uint[]{12097}, - new uint[]{108}, - new uint[]{7695}, - new uint[]{7696}, - new uint[]{7697}, - Array.Empty(), - new uint[]{7635}, - new uint[]{7635}, - new uint[]{7633}, - new uint[]{7634}, - new uint[]{7640}, - new uint[]{7636}, - new uint[]{7637}, - new uint[]{7638}, - new uint[]{12257}, - new uint[]{12257}, - new uint[]{12258}, - new uint[]{7639}, - new uint[]{7695}, - new uint[]{12256}, - new uint[]{12259}, - new uint[]{12260}, - new uint[]{12063}, - new uint[]{12066}, - new uint[]{12067}, - Array.Empty(), - new uint[]{12069}, - new uint[]{12070}, - new uint[]{12071}, - new uint[]{12067}, - new uint[]{12067}, - new uint[]{12072}, - Array.Empty(), - Array.Empty(), - new uint[]{12073}, - new uint[]{12074}, - new uint[]{12075}, - new uint[]{12076}, - new uint[]{12077}, - new uint[]{12065}, - new uint[]{4808}, - new uint[]{4810}, - Array.Empty(), - new uint[]{12057}, - new uint[]{12059}, - Array.Empty(), - new uint[]{12280}, - new uint[]{12056}, - new uint[]{12056}, - new uint[]{12056}, - new uint[]{12054}, - Array.Empty(), - new uint[]{12057}, - new uint[]{12059}, - new uint[]{12059}, - new uint[]{12280}, - new uint[]{12056}, - new uint[]{12056}, - new uint[]{12056}, - new uint[]{12058}, - new uint[]{12058}, - new uint[]{12058}, - new uint[]{108}, - Array.Empty(), - new uint[]{12242}, - new uint[]{12243}, - new uint[]{12265}, - new uint[]{12266}, - new uint[]{6091}, - new uint[]{12278}, - new uint[]{12279}, - new uint[]{12267}, - new uint[]{12268}, - new uint[]{3822}, - new uint[]{2143}, - new uint[]{12292}, - new uint[]{12293}, - new uint[]{3823}, - new uint[]{12292}, - new uint[]{12293}, - new uint[]{12296}, - new uint[]{12295}, - new uint[]{12294}, - new uint[]{12064}, - new uint[]{12297}, - new uint[]{12297}, - new uint[]{12312}, - new uint[]{12312}, - new uint[]{8378}, - new uint[]{4392}, - new uint[]{4392}, - new uint[]{4130}, - new uint[]{4130}, - new uint[]{11330}, - new uint[]{11331}, - new uint[]{11332}, - new uint[]{11333}, - new uint[]{713}, - new uint[]{713}, - new uint[]{11262}, - new uint[]{11262}, - new uint[]{12236}, - new uint[]{12237}, - new uint[]{12238}, - new uint[]{11418, 12053}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12106}, - new uint[]{12107}, - new uint[]{12108}, - new uint[]{12109}, - new uint[]{12110}, - new uint[]{12111}, - new uint[]{12112}, - new uint[]{12113}, - new uint[]{12114}, - new uint[]{12115}, - new uint[]{12116}, - new uint[]{12117}, - new uint[]{12118}, - new uint[]{12122}, - new uint[]{12121}, - new uint[]{12120}, - new uint[]{12119}, - new uint[]{12123}, - new uint[]{12124}, - new uint[]{12125}, - new uint[]{12126}, - new uint[]{12127}, - new uint[]{12128}, - new uint[]{12129}, - new uint[]{12130}, - new uint[]{12131}, - new uint[]{12132}, - new uint[]{12133}, - new uint[]{12134}, - new uint[]{12135}, - new uint[]{12136}, - new uint[]{12137}, - new uint[]{12138}, - new uint[]{12139}, - new uint[]{12140}, - new uint[]{12141}, - new uint[]{12142}, - new uint[]{12143}, - new uint[]{12144}, - new uint[]{12145}, - new uint[]{12146}, - new uint[]{12147}, - new uint[]{12148}, - new uint[]{12149}, - new uint[]{12150}, - new uint[]{12151}, - new uint[]{12152}, - new uint[]{12153}, - new uint[]{12154}, - new uint[]{12155}, - new uint[]{12156}, - new uint[]{12157}, - new uint[]{12158}, - new uint[]{12159}, - new uint[]{12160}, - new uint[]{12161}, - new uint[]{12162}, - new uint[]{12163}, - new uint[]{12164}, - new uint[]{12165}, - new uint[]{12166}, - new uint[]{12167}, - new uint[]{12168}, - new uint[]{12169}, - new uint[]{12170}, - new uint[]{12171}, - new uint[]{12172}, - new uint[]{12173}, - new uint[]{12174}, - new uint[]{12175}, - new uint[]{12176}, - new uint[]{12177}, - new uint[]{12178}, - new uint[]{12179}, - new uint[]{12180}, - new uint[]{12181}, - new uint[]{12182}, - new uint[]{12183}, - new uint[]{5199}, - new uint[]{5200}, - new uint[]{5201}, - new uint[]{5202}, - new uint[]{5203}, - new uint[]{5204}, - new uint[]{12321}, - new uint[]{12320}, - new uint[]{12319}, - new uint[]{12244}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12324}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12323}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12322}, - new uint[]{12184}, - new uint[]{12185}, - new uint[]{12186}, - new uint[]{12187}, - new uint[]{12188}, - new uint[]{12189}, - new uint[]{12190}, - new uint[]{12191}, - new uint[]{12192}, - new uint[]{12193}, - new uint[]{12194}, - new uint[]{12195}, - new uint[]{12196}, - new uint[]{12197}, - new uint[]{12198}, - new uint[]{12209}, - new uint[]{12199}, - new uint[]{12200}, - new uint[]{12208}, - new uint[]{12201}, - new uint[]{12202}, - new uint[]{12207}, - new uint[]{12203}, - new uint[]{12206}, - new uint[]{12204}, - new uint[]{12205}, - new uint[]{12212}, - new uint[]{12213}, - new uint[]{12210}, - new uint[]{12211}, - new uint[]{12214}, - new uint[]{12215}, - new uint[]{12216}, - new uint[]{12217}, - new uint[]{12218}, - new uint[]{12219}, - new uint[]{12220}, - new uint[]{12221}, - new uint[]{12222}, - new uint[]{12223}, - new uint[]{12224}, - new uint[]{12225}, - new uint[]{12229}, - new uint[]{12230}, - new uint[]{12226}, - new uint[]{12227}, - new uint[]{12232}, - new uint[]{12231}, - new uint[]{12233}, - new uint[]{12228}, - new uint[]{12234}, - new uint[]{12235}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{108}, - new uint[]{11296}, - new uint[]{7142}, - new uint[]{12258}, - new uint[]{12261}, - new uint[]{12262}, - new uint[]{11295}, - new uint[]{5199}, - new uint[]{5199, 5204}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{2566}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{10309}, - new uint[]{12031}, - new uint[]{12270}, - new uint[]{12271}, - new uint[]{12272}, - new uint[]{12273}, - new uint[]{12274}, - new uint[]{12275}, - new uint[]{108}, - new uint[]{750}, - new uint[]{750}, - new uint[]{750}, - new uint[]{750}, - new uint[]{12276}, - new uint[]{12276}, - new uint[]{12277}, - new uint[]{12245}, - Array.Empty(), - Array.Empty(), - new uint[]{12308}, - new uint[]{12309}, - Array.Empty(), - new uint[]{12308}, - new uint[]{12072}, - new uint[]{12311}, - Array.Empty(), - Array.Empty(), - new uint[]{12334}, - new uint[]{12335}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{12281}, - new uint[]{12282}, - new uint[]{12283}, - new uint[]{12284}, - new uint[]{12285}, - new uint[]{12286}, - new uint[]{12287}, - new uint[]{12288}, - new uint[]{12289}, - new uint[]{12290}, - new uint[]{12291}, - new uint[]{12298}, - Array.Empty(), - new uint[]{12300}, - Array.Empty(), - new uint[]{12302}, - new uint[]{12303}, - new uint[]{12304}, - new uint[]{12305}, - new uint[]{12306}, - new uint[]{12307}, - new uint[]{12314}, - new uint[]{12315}, - new uint[]{12338}, - new uint[]{12339}, - new uint[]{12340}, - new uint[]{12341}, - new uint[]{12342}, - new uint[]{12343}, - new uint[]{12344}, - new uint[]{12345}, - new uint[]{12346}, - new uint[]{12347}, - new uint[]{12348}, - new uint[]{12349}, - new uint[]{12350}, - new uint[]{12318}, - new uint[]{11297}, - new uint[]{12369}, - new uint[]{12372}, - new uint[]{12369}, - new uint[]{12370}, - new uint[]{12371}, - new uint[]{12372}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12311}, - new uint[]{12308}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{108}, - new uint[]{12337}, - new uint[]{12336}, - new uint[]{12388}, - new uint[]{12389}, - new uint[]{12390}, - new uint[]{12390}, - Array.Empty(), - new uint[]{12391}, - new uint[]{12388}, - new uint[]{12389}, - new uint[]{12389}, - new uint[]{12390}, - new uint[]{12390}, - new uint[]{12390}, - new uint[]{12390}, - new uint[]{12391}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12354}, - new uint[]{12355}, - new uint[]{12426}, - new uint[]{108}, - new uint[]{12354}, - new uint[]{12355}, - new uint[]{12426}, - new uint[]{12427}, - new uint[]{108}, - new uint[]{12356}, - new uint[]{12377}, - new uint[]{12378}, - new uint[]{12379}, - new uint[]{12380}, - new uint[]{12378}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12378}, - new uint[]{12379}, - new uint[]{12380}, - new uint[]{12381}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12377}, - new uint[]{12382}, - new uint[]{12383}, - new uint[]{12384}, - new uint[]{12385}, - new uint[]{12386}, - new uint[]{12387}, - new uint[]{12392}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12365}, - new uint[]{12367}, - new uint[]{12368}, - new uint[]{12365}, - new uint[]{12367}, - new uint[]{12368}, - new uint[]{12366}, - new uint[]{108}, - Array.Empty(), - Array.Empty(), - new uint[]{12339}, - new uint[]{12339}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{5569}, - new uint[]{5570}, - new uint[]{5571}, - new uint[]{5572}, - Array.Empty(), - new uint[]{12463}, - new uint[]{5970}, - new uint[]{5239}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{4130}, - new uint[]{4392}, - new uint[]{4130}, - new uint[]{9350}, - new uint[]{12464}, - new uint[]{6148}, - new uint[]{12465}, - new uint[]{12466}, - new uint[]{12467}, - new uint[]{12468}, - new uint[]{12469}, - new uint[]{12470}, - new uint[]{5964}, - new uint[]{4133}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{108}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12382}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12436}, - new uint[]{12437}, - Array.Empty(), - new uint[]{12439}, - new uint[]{12440}, - new uint[]{12441}, - Array.Empty(), - new uint[]{12443}, - Array.Empty(), - new uint[]{12445}, - new uint[]{12446}, - Array.Empty(), - Array.Empty(), - new uint[]{12455}, - new uint[]{12449}, - new uint[]{12450}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{12461}, - new uint[]{12462}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - new uint[]{10902}, - new uint[]{10903}, - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - }; -} diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs deleted file mode 100644 index 3608a4a0..00000000 --- a/Penumbra.GameData/Data/DataSharer.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Threading.Tasks; -using Dalamud; -using Dalamud.Logging; -using Dalamud.Plugin; - -namespace Penumbra.GameData.Data; - -/// -/// A container base class that shares data through Dalamud but cares about the used language and version. -/// Inheritors should dispose their Dalamud Shares in DisposeInternal via DisposeTag and add them in their constructor via TryCatchData. -/// -public abstract class DataSharer : IDisposable -{ - protected readonly DalamudPluginInterface PluginInterface; - protected readonly int Version; - protected readonly ClientLanguage Language; - private bool _disposed; - - protected DataSharer(DalamudPluginInterface pluginInterface, ClientLanguage language, int version) - { - PluginInterface = pluginInterface; - Language = language; - Version = version; - } - - protected virtual void DisposeInternal() - { } - - public void Dispose() - { - if (_disposed) - return; - - DisposeInternal(); - GC.SuppressFinalize(this); - _disposed = true; - } - - ~DataSharer() - => Dispose(); - - protected void DisposeTag(string tag) - => PluginInterface.RelinquishData(GetVersionedTag(tag, Language, Version)); - - protected T TryCatchData(string tag, Func func) where T : class - { - try - { - return PluginInterface.GetOrCreateData(GetVersionedTag(tag, Language, Version), func); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared data for {tag}:\n{ex}"); - return func(); - } - } - - protected Task TryCatchDataAsync(string tag, Action fill) where T : class, new() - { - tag = GetVersionedTag(tag, Language, Version); - if (PluginInterface.TryGetData(tag, out var data)) - return Task.FromResult(data); - - T ret = new(); - return Task.Run(() => - { - fill(ret); - return ret; - }); - } - - public static void DisposeTag(DalamudPluginInterface pi, string tag, ClientLanguage language, int version) - => pi.RelinquishData(GetVersionedTag(tag, language, version)); - - public static T TryCatchData(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, Func func) - where T : class - { - try - { - return pi.GetOrCreateData(GetVersionedTag(tag, language, version), func); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); - return func(); - } - } - - private static string GetVersionedTag(string tag, ClientLanguage language, int version) - => $"Penumbra.GameData.{tag}.{language}.V{version}"; -} diff --git a/Penumbra.GameData/Data/DisassembledShader.cs b/Penumbra.GameData/Data/DisassembledShader.cs deleted file mode 100644 index f611982c..00000000 --- a/Penumbra.GameData/Data/DisassembledShader.cs +++ /dev/null @@ -1,463 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Penumbra.GameData.Interop; -using Penumbra.String; - -namespace Penumbra.GameData.Data; - -public partial class DisassembledShader -{ - public struct ResourceBinding - { - public string Name; - public ResourceType Type; - public Format Format; - public ResourceDimension Dimension; - public uint Slot; - public uint Elements; - public uint RegisterCount; - public VectorComponents[] Used; - public VectorComponents UsedDynamically; - } - - // Abbreviated using the uppercased first char of their name - public enum ResourceType : byte - { - Unspecified = 0, - ConstantBuffer = 0x43, // 'C' - Sampler = 0x53, // 'S' - Texture = 0x54, // 'T' - Uav = 0x55, // 'U' - } - - // Abbreviated using the uppercased first and last char of their name - public enum Format : ushort - { - Unspecified = 0, - NotApplicable = 0x4E41, // 'NA' - Int = 0x4954, // 'IT' - Int4 = 0x4934, // 'I4' - Float = 0x4654, // 'FT' - Float4 = 0x4634, // 'F4' - } - - // Abbreviated using the uppercased first and last char of their name - public enum ResourceDimension : ushort - { - Unspecified = 0, - NotApplicable = 0x4E41, // 'NA' - TwoD = 0x3244, // '2D' - ThreeD = 0x3344, // '3D' - Cube = 0x4345, // 'CE' - } - - public struct InputOutput - { - public string Name; - public uint Index; - public VectorComponents Mask; - public uint Register; - public string SystemValue; - public Format Format; - public VectorComponents Used; - } - - [Flags] - public enum VectorComponents : byte - { - X = 1, - Y = 2, - Z = 4, - W = 8, - All = 15, - } - - public enum ShaderStage : byte - { - Unspecified = 0, - Pixel = 0x50, // 'P' - Vertex = 0x56, // 'V' - } - - [GeneratedRegex(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking)] - private static partial Regex ResourceBindingSizeRegex(); - - [GeneratedRegex(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking)] - private static partial Regex Sm3ConstantBufferUsageRegex(); - - [GeneratedRegex(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking)] - private static partial Regex Sm3TextureUsageRegex(); - - [GeneratedRegex(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking)] - private static partial Regex Sm5ConstantBufferUsageRegex(); - - [GeneratedRegex(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking)] - private static partial Regex Sm5TextureUsageRegex(); - - private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char)('0' + c)).ToArray(); - - public readonly ByteString RawDisassembly; - public readonly uint ShaderModel; - public readonly ShaderStage Stage; - public readonly string BufferDefinitions; - public readonly ResourceBinding[] ResourceBindings; - public readonly InputOutput[] InputSignature; - public readonly InputOutput[] OutputSignature; - public readonly IReadOnlyList Instructions; - - public DisassembledShader(ByteString rawDisassembly) - { - RawDisassembly = rawDisassembly; - var lines = rawDisassembly.Split((byte) '\n'); - Instructions = lines.FindAll(ln => !ln.StartsWith("//"u8) && ln.Length > 0); - var shaderModel = Instructions[0].Trim().Split((byte) '_'); - Stage = (ShaderStage)(byte)char.ToUpper((char) shaderModel[0][0]); - ShaderModel = (uint.Parse(shaderModel[1].ToString()) << 8) | uint.Parse(shaderModel[2].ToString()); - var header = PreParseHeader(lines.Take(lines.IndexOf(Instructions[0])).Select(l => l.ToString()).ToArray()); - switch (ShaderModel >> 8) - { - case 3: - ParseSm3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); - ParseSm3ResourceUsage(Instructions, ResourceBindings); - break; - case 5: - ParseSm5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature); - ParseSm5ResourceUsage(Instructions, ResourceBindings); - break; - default: throw new NotImplementedException(); - } - } - - public ResourceBinding? GetResourceBindingByName(ResourceType type, string name) - => ResourceBindings.FirstOrNull(b => b.Type == type && b.Name == name); - - public ResourceBinding? GetResourceBindingBySlot(ResourceType type, uint slot) - => ResourceBindings.FirstOrNull(b => b.Type == type && b.Slot == slot); - - public static DisassembledShader Disassemble(ReadOnlySpan shaderBlob) - => new(D3DCompiler.Disassemble(shaderBlob)); - - private static void ParseSm3Header(Dictionary header, out string bufferDefinitions, - out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) - { - bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) - ? string.Join('\n', rawParameters) - : string.Empty; - if (header.TryGetValue("Registers", out var rawRegisters)) - { - var (_, registers) = ParseTable(rawRegisters); - resourceBindings = Array.ConvertAll(registers, register => - { - var type = (ResourceType)(byte)char.ToUpper(register[1][0]); - if (type == ResourceType.Sampler) - type = ResourceType.Texture; - var size = uint.Parse(register[2]); - return new ResourceBinding - { - Name = register[0], - Type = type, - Format = Format.Unspecified, - Dimension = ResourceDimension.Unspecified, - Slot = uint.Parse(register[1][1..]), - Elements = 1, - RegisterCount = size, - Used = new VectorComponents[size], - }; - }); - } - else - { - resourceBindings = Array.Empty(); - } - - inputSignature = Array.Empty(); - outputSignature = Array.Empty(); - } - - private static void ParseSm3ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) - { - var cbIndices = new Dictionary(); - var tIndices = new Dictionary(); - { - var i = 0; - foreach (var binding in resourceBindings) - { - switch (binding.Type) - { - case ResourceType.ConstantBuffer: - for (var j = 0u; j < binding.RegisterCount; j++) - cbIndices[binding.Slot + j] = i; - break; - case ResourceType.Texture: - tIndices[binding.Slot] = i; - break; - } - - ++i; - } - } - foreach (var instruction in instructions) - { - var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) - continue; - - var instructionString = instruction.ToString(); - foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instructionString)) - { - var buffer = uint.Parse(cbMatch.Groups[1].Value); - if (cbIndices.TryGetValue(buffer, out var i)) - { - var swizzle = cbMatch.Groups[3].Success ? ParseVectorComponents(cbMatch.Groups[3].Value) : VectorComponents.All; - if (cbMatch.Groups[2].Success) - resourceBindings[i].UsedDynamically |= swizzle; - else - resourceBindings[i].Used[buffer - resourceBindings[i].Slot] |= swizzle; - } - } - - var tMatch = Sm3TextureUsageRegex().Match(instructionString); - if (tMatch.Success) - { - var texture = uint.Parse(tMatch.Groups[1].Value); - if (tIndices.TryGetValue(texture, out var i)) - resourceBindings[i].Used[0] = VectorComponents.All; - } - } - } - - private static void ParseSm5Header(Dictionary header, out string bufferDefinitions, - out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) - { - if (header.TryGetValue("Resource Bindings", out var rawResBindings)) - { - var (head, resBindings) = ParseTable(rawResBindings); - resourceBindings = Array.ConvertAll(resBindings, binding => - { - var type = (ResourceType)(byte)char.ToUpper(binding[1][0]); - return new ResourceBinding - { - Name = binding[0], - Type = type, - Format = (Format)(((byte)char.ToUpper(binding[2][0]) << 8) | (byte)char.ToUpper(binding[2][^1])), - Dimension = (ResourceDimension)(((byte)char.ToUpper(binding[3][0]) << 8) | (byte)char.ToUpper(binding[3][^1])), - Slot = uint.Parse(binding[4][binding[4].IndexOfAny(Digits)..]), - Elements = uint.Parse(binding[5]), - RegisterCount = type == ResourceType.Texture ? 1u : 0u, - Used = type == ResourceType.Texture ? new VectorComponents[1] : Array.Empty(), - }; - }); - } - else - { - resourceBindings = Array.Empty(); - } - - if (header.TryGetValue("Buffer Definitions", out var rawBufferDefs)) - { - bufferDefinitions = string.Join('\n', rawBufferDefs); - foreach (Match match in ResourceBindingSizeRegex().Matches(bufferDefinitions)) - { - var name = match.Groups[1].Value; - var bytesSize = uint.Parse(match.Groups[2].Value); - var pos = Array.FindIndex(resourceBindings, binding => binding.Type == ResourceType.ConstantBuffer && binding.Name == name); - if (pos >= 0) - { - resourceBindings[pos].RegisterCount = (bytesSize + 0xF) >> 4; - resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount]; - } - } - } - else - { - bufferDefinitions = string.Empty; - } - - static InputOutput ParseInputOutput(string[] inOut) - => new() - { - Name = inOut[0], - Index = uint.Parse(inOut[1]), - Mask = ParseVectorComponents(inOut[2]), - Register = uint.Parse(inOut[3]), - SystemValue = string.Intern(inOut[4]), - Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])), - Used = ParseVectorComponents(inOut[6]), - }; - - if (header.TryGetValue("Input signature", out var rawInputSig)) - { - var (_, inputSig) = ParseTable(rawInputSig); - inputSignature = Array.ConvertAll(inputSig, ParseInputOutput); - } - else - { - inputSignature = Array.Empty(); - } - - if (header.TryGetValue("Output signature", out var rawOutputSig)) - { - var (_, outputSig) = ParseTable(rawOutputSig); - outputSignature = Array.ConvertAll(outputSig, ParseInputOutput); - } - else - { - outputSignature = Array.Empty(); - } - } - - private static void ParseSm5ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) - { - var cbIndices = new Dictionary(); - var tIndices = new Dictionary(); - { - var i = 0; - foreach (var binding in resourceBindings) - { - switch (binding.Type) - { - case ResourceType.ConstantBuffer: - cbIndices[binding.Slot] = i; - break; - case ResourceType.Texture: - tIndices[binding.Slot] = i; - break; - } - - ++i; - } - } - foreach (var instruction in instructions) - { - var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) - continue; - - var instructionString = instruction.ToString(); - foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instructionString)) - { - var buffer = uint.Parse(cbMatch.Groups[1].Value); - if (cbIndices.TryGetValue(buffer, out var i)) - { - var swizzle = ParseVectorComponents(cbMatch.Groups[3].Value); - if (int.TryParse(cbMatch.Groups[2].Value, out var vector)) - { - if (vector < resourceBindings[i].Used.Length) - resourceBindings[i].Used[vector] |= swizzle; - } - else - { - resourceBindings[i].UsedDynamically |= swizzle; - } - } - } - - var tMatch = Sm5TextureUsageRegex().Match(instructionString); - if (tMatch.Success) - { - var texture = uint.Parse(tMatch.Groups[2].Value); - if (tIndices.TryGetValue(texture, out var i)) - { - var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value); - var rawInSwizzle = tMatch.Groups[3].Value; - var inSwizzle = new StringBuilder(4); - if ((outSwizzle & VectorComponents.X) != 0) - inSwizzle.Append(rawInSwizzle[0]); - if ((outSwizzle & VectorComponents.Y) != 0) - inSwizzle.Append(rawInSwizzle[1]); - if ((outSwizzle & VectorComponents.Z) != 0) - inSwizzle.Append(rawInSwizzle[2]); - if ((outSwizzle & VectorComponents.W) != 0) - inSwizzle.Append(rawInSwizzle[3]); - resourceBindings[i].Used[0] |= ParseVectorComponents(inSwizzle.ToString()); - } - } - } - } - - private static VectorComponents ParseVectorComponents(string components) - { - components = components.ToUpperInvariant(); - return (components.Contains('X') ? VectorComponents.X : 0) - | (components.Contains('Y') ? VectorComponents.Y : 0) - | (components.Contains('Z') ? VectorComponents.Z : 0) - | (components.Contains('W') ? VectorComponents.W : 0); - } - - private static Dictionary PreParseHeader(ReadOnlySpan header) - { - var sections = new Dictionary(); - - void AddSection(string name, ReadOnlySpan section) - { - while (section.Length > 0 && section[0].Length <= 3) - section = section[1..]; - while (section.Length > 0 && section[^1].Length <= 3) - section = section[..^1]; - sections.Add(name, Array.ConvertAll(section.ToArray(), ln => ln.Length <= 3 ? string.Empty : ln[3..])); - } - - var lastSectionName = ""; - var lastSectionStart = 0; - for (var i = 1; i < header.Length - 1; ++i) - { - string current; - if (header[i - 1].Length <= 3 && header[i + 1].Length <= 3 && (current = header[i].TrimEnd()).EndsWith(':')) - { - AddSection(lastSectionName, header[lastSectionStart..(i - 1)]); - lastSectionName = current[3..^1]; - lastSectionStart = i + 2; - ++i; // The next line cannot match - } - } - - AddSection(lastSectionName, header[lastSectionStart..]); - - return sections; - } - - private static (string[], string[][]) ParseTable(ReadOnlySpan lines) - { - var columns = new List(); - { - var dashLine = lines[1]; - for (var i = 0; true; /* this part intentionally left blank */) - { - var start = dashLine.IndexOf('-', i); - if (start < 0) - break; - - var end = dashLine.IndexOf(' ', start + 1); - if (end < 0) - { - columns.Add(start..dashLine.Length); - break; - } - else - { - columns.Add(start..end); - i = end + 1; - } - } - } - var headers = new string[columns.Count]; - { - var headerLine = lines[0]; - for (var i = 0; i < columns.Count; ++i) - headers[i] = headerLine[columns[i]].Trim(); - } - var data = new List(); - foreach (var line in lines[2..]) - { - var row = new string[columns.Count]; - for (var i = 0; i < columns.Count; ++i) - row[i] = line[columns[i]].Trim(); - data.Add(row); - } - - return (headers, data.ToArray()); - } -} diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs deleted file mode 100644 index 571867c4..00000000 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Plugin; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; - -namespace Penumbra.GameData.Data; - -internal sealed class EquipmentIdentificationList : KeyList -{ - private const string Tag = "EquipmentIdentification"; - - public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data) - : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(data)) - { } - - public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) - { - if (slot == EquipSlot.Unknown) - return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)).Select(e => (EquipItem)e); - if (variant == 0) - return Between(ToKey(modelId, slot, 0), ToKey(modelId, slot, 0xFF)).Select(e => (EquipItem)e); - - return Between(ToKey(modelId, slot, variant), ToKey(modelId, slot, variant)).Select(e => (EquipItem)e); - } - - public void Dispose(DalamudPluginInterface pi, ClientLanguage language) - => DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion); - - public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant) - => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; - - public static ulong ToKey(EquipItem i) - => ToKey(i.ModelId, i.Type.ToSlot(), i.Variant); - - protected override IEnumerable ToKeys(PseudoEquipItem i) - { - yield return ToKey(i); - } - - protected override bool ValidKey(ulong key) - => key != 0; - - protected override int ValueKeySelector(PseudoEquipItem data) - => (int)data.Item2; - - private static IEnumerable CreateEquipmentList(ItemData data) - { - return data.Where(kvp => kvp.Key.IsEquipment() || kvp.Key.IsAccessory()) - .SelectMany(kvp => kvp.Value) - .Select(i => (PseudoEquipItem)i) - .Concat(CustomList); - } - - private static IEnumerable CustomList - => new[] - { - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)8100, (WeaponType)0, 01, FullEquipType.Body, "Reaper Shroud"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Head, "Cid's Bandana (9041)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9041, (WeaponType)0, 01, FullEquipType.Body, "Cid's Body (9041)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Head, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Body, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Hands, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Legs, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9903, (WeaponType)0, 01, FullEquipType.Feet, "Smallclothes (NPC, 9903)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 12, FullEquipType.Body, "Ancient Robes (Lahabrea)"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Legs, "Ancient Legs"), - (PseudoEquipItem)EquipItem.FromIds(0, 0, (SetId)9212, (WeaponType)0, 01, FullEquipType.Feet, "Ancient Shoes"), - }; -} diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs deleted file mode 100644 index 3f2f54a1..00000000 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Data; - -public class GamePathParser : IGamePathParser -{ - /// Obtain basic information about a file path. - public GameObjectInfo GetFileInfo(string path) - { - path = path.ToLowerInvariant().Replace('\\', '/'); - - var (fileType, objectType, match) = ParseGamePath(path); - if (match is not { Success: true }) - return new GameObjectInfo - { - FileType = fileType, - ObjectType = objectType, - }; - - try - { - var groups = match.Groups; - switch (objectType) - { - case ObjectType.Accessory: return HandleEquipment(fileType, groups); - case ObjectType.Equipment: return HandleEquipment(fileType, groups); - case ObjectType.Weapon: return HandleWeapon(fileType, groups); - case ObjectType.Map: return HandleMap(fileType, groups); - case ObjectType.Monster: return HandleMonster(fileType, groups); - case ObjectType.DemiHuman: return HandleDemiHuman(fileType, groups); - case ObjectType.Character: return HandleCustomization(fileType, groups); - case ObjectType.Icon: return HandleIcon(fileType, groups); - } - } - catch (Exception e) - { - PluginLog.Error($"Could not parse {path}:\n{e}"); - } - - return new GameObjectInfo - { - FileType = fileType, - ObjectType = objectType, - }; - } - - /// Get the key of a VFX symbol. - /// The lower-case key or an empty string if no match is found. - public string VfxToKey(string path) - { - var match = GamePaths.Vfx.Tmb().Match(path); - if (match.Success) - return match.Groups["key"].Value.ToLowerInvariant(); - - match = GamePaths.Vfx.Pap().Match(path); - return match.Success ? match.Groups["key"].Value.ToLowerInvariant() : string.Empty; - } - - /// Obtain the ObjectType from a given path. - public ObjectType PathToObjectType(string path) - { - if (path.Length == 0) - return ObjectType.Unknown; - - var folders = path.Split('/'); - if (folders.Length < 2) - return ObjectType.Unknown; - - return folders[0] switch - { - CharacterFolder => folders[1] switch - { - EquipmentFolder => ObjectType.Equipment, - AccessoryFolder => ObjectType.Accessory, - WeaponFolder => ObjectType.Weapon, - PlayerFolder => ObjectType.Character, - DemiHumanFolder => ObjectType.DemiHuman, - MonsterFolder => ObjectType.Monster, - CommonFolder => ObjectType.Character, - _ => ObjectType.Unknown, - }, - UiFolder => folders[1] switch - { - IconFolder => ObjectType.Icon, - LoadingFolder => ObjectType.LoadingScreen, - MapFolder => ObjectType.Map, - InterfaceFolder => ObjectType.Interface, - _ => ObjectType.Unknown, - }, - CommonFolder => folders[1] switch - { - FontFolder => ObjectType.Font, - _ => ObjectType.Unknown, - }, - HousingFolder => ObjectType.Housing, - WorldFolder1 => folders[1] switch - { - HousingFolder => ObjectType.Housing, - _ => ObjectType.World, - }, - WorldFolder2 => ObjectType.World, - VfxFolder => ObjectType.Vfx, - _ => ObjectType.Unknown, - }; - } - - private const string CharacterFolder = "chara"; - private const string EquipmentFolder = "equipment"; - private const string PlayerFolder = "human"; - private const string WeaponFolder = "weapon"; - private const string AccessoryFolder = "accessory"; - private const string DemiHumanFolder = "demihuman"; - private const string MonsterFolder = "monster"; - private const string CommonFolder = "common"; - private const string UiFolder = "ui"; - private const string IconFolder = "icon"; - private const string LoadingFolder = "loadingimage"; - private const string MapFolder = "map"; - private const string InterfaceFolder = "uld"; - private const string FontFolder = "font"; - private const string HousingFolder = "hou"; - private const string VfxFolder = "vfx"; - private const string WorldFolder1 = "bgcommon"; - private const string WorldFolder2 = "bg"; - - private (FileType, ObjectType, Match?) ParseGamePath(string path) - { - if (!Names.ExtensionToFileType.TryGetValue(Path.GetExtension(path), out var fileType)) - fileType = FileType.Unknown; - - var objectType = PathToObjectType(path); - - static Match TestCharacterTextures(string path) - { - var regexes = new Regex[] - { - GamePaths.Character.Tex.Regex(), - GamePaths.Character.Tex.FolderRegex(), - GamePaths.Character.Tex.SkinRegex(), - GamePaths.Character.Tex.CatchlightRegex(), - GamePaths.Character.Tex.DecalRegex(), - }; - foreach (var regex in regexes) - { - var match = regex.Match(path); - if (match.Success) - return match; - } - - return Match.Empty; - } - - var match = (fileType, objectType) switch - { - (FileType.Font, ObjectType.Font) => GamePaths.Font.Regex().Match(path), - (FileType.Imc, ObjectType.Weapon) => GamePaths.Weapon.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Monster) => GamePaths.Monster.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.DemiHuman) => GamePaths.DemiHuman.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Equipment) => GamePaths.Equipment.Imc.Regex().Match(path), - (FileType.Imc, ObjectType.Accessory) => GamePaths.Accessory.Imc.Regex().Match(path), - (FileType.Model, ObjectType.Weapon) => GamePaths.Weapon.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Monster) => GamePaths.Monster.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Equipment) => GamePaths.Equipment.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Accessory) => GamePaths.Accessory.Mdl.Regex().Match(path), - (FileType.Model, ObjectType.Character) => GamePaths.Character.Mdl.Regex().Match(path), - (FileType.Material, ObjectType.Weapon) => GamePaths.Weapon.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Monster) => GamePaths.Monster.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.DemiHuman) => GamePaths.DemiHuman.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Equipment) => GamePaths.Equipment.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Accessory) => GamePaths.Accessory.Mtrl.Regex().Match(path), - (FileType.Material, ObjectType.Character) => GamePaths.Character.Mtrl.Regex().Match(path), - (FileType.Texture, ObjectType.Weapon) => GamePaths.Weapon.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Monster) => GamePaths.Monster.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.DemiHuman) => GamePaths.DemiHuman.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Equipment) => GamePaths.Equipment.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Accessory) => GamePaths.Accessory.Tex.Regex().Match(path), - (FileType.Texture, ObjectType.Character) => TestCharacterTextures(path), - (FileType.Texture, ObjectType.Icon) => GamePaths.Icon.Regex().Match(path), - (FileType.Texture, ObjectType.Map) => GamePaths.Map.Regex().Match(path), - _ => Match.Empty, - }; - - return (fileType, objectType, match.Success ? match : null); - } - - private static GameObjectInfo HandleEquipment(FileType fileType, GroupCollection groups) - { - var setId = ushort.Parse(groups["id"].Value); - if (fileType == FileType.Imc) - return GameObjectInfo.Equipment(fileType, setId); - - var gr = Names.GenderRaceFromCode(groups["race"].Value); - var slot = Names.SuffixToEquipSlot[groups["slot"].Value]; - if (fileType == FileType.Model) - return GameObjectInfo.Equipment(fileType, setId, gr, slot); - - var variant = byte.Parse(groups["variant"].Value); - return GameObjectInfo.Equipment(fileType, setId, gr, slot, variant); - } - - private static GameObjectInfo HandleWeapon(FileType fileType, GroupCollection groups) - { - var weaponId = ushort.Parse(groups["weapon"].Value); - var setId = ushort.Parse(groups["id"].Value); - if (fileType is FileType.Imc or FileType.Model) - return GameObjectInfo.Weapon(fileType, setId, weaponId); - - var variant = byte.Parse(groups["variant"].Value); - return GameObjectInfo.Weapon(fileType, setId, weaponId, variant); - } - - private static GameObjectInfo HandleMonster(FileType fileType, GroupCollection groups) - { - var monsterId = ushort.Parse(groups["monster"].Value); - var bodyId = ushort.Parse(groups["id"].Value); - if (fileType is FileType.Imc or FileType.Model) - return GameObjectInfo.Monster(fileType, monsterId, bodyId); - - var variant = byte.Parse(groups["variant"].Value); - return GameObjectInfo.Monster(fileType, monsterId, bodyId, variant); - } - - private static GameObjectInfo HandleDemiHuman(FileType fileType, GroupCollection groups) - { - var demiHumanId = ushort.Parse(groups["id"].Value); - var equipId = ushort.Parse(groups["equip"].Value); - if (fileType == FileType.Imc) - return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId); - - var slot = Names.SuffixToEquipSlot[groups["slot"].Value]; - if (fileType == FileType.Model) - return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot); - - var variant = byte.Parse(groups["variant"].Value); - return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot, variant); - } - - 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); - - var id = ushort.Parse(groups["id"].Value); - if (groups["location"].Success) - { - var tmpType = groups["location"].Value == "face" ? CustomizationType.DecalFace - : groups["location"].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; - return GameObjectInfo.Customization(fileType, tmpType, id); - } - - var gr = Names.GenderRaceFromCode(groups["race"].Value); - var bodySlot = Names.StringToBodySlot[groups["type"].Value]; - var type = groups["slot"].Success - ? Names.SuffixToCustomizationType[groups["slot"].Value] - : CustomizationType.Skin; - if (fileType == FileType.Material) - { - var variant = groups["variant"].Success ? byte.Parse(groups["variant"].Value) : (byte)0; - return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot, variant); - } - - return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot); - } - - private static GameObjectInfo HandleIcon(FileType fileType, GroupCollection groups) - { - var hq = groups["hq"].Success; - var hr = groups["hr"].Success; - var id = uint.Parse(groups["id"].Value); - if (!groups["lang"].Success) - return GameObjectInfo.Icon(fileType, id, hq, hr); - - var language = groups["lang"].Value switch - { - "en" => Dalamud.ClientLanguage.English, - "ja" => Dalamud.ClientLanguage.Japanese, - "de" => Dalamud.ClientLanguage.German, - "fr" => Dalamud.ClientLanguage.French, - _ => Dalamud.ClientLanguage.English, - }; - return GameObjectInfo.Icon(fileType, id, hq, hr, language); - } - - private static GameObjectInfo HandleMap(FileType fileType, GroupCollection groups) - { - var map = Encoding.ASCII.GetBytes(groups["id"].Value); - var variant = byte.Parse(groups["variant"].Value); - if (groups["suffix"].Success) - { - var suffix = Encoding.ASCII.GetBytes(groups["suffix"].Value)[0]; - return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant, suffix); - } - - return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant); - } -} diff --git a/Penumbra.GameData/Data/GamePaths.cs b/Penumbra.GameData/Data/GamePaths.cs deleted file mode 100644 index 5df91600..00000000 --- a/Penumbra.GameData/Data/GamePaths.cs +++ /dev/null @@ -1,408 +0,0 @@ -using System.Text.RegularExpressions; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Data; - -public static partial class GamePaths -{ - [GeneratedRegex(@"c(?'racecode'\d{4})")] - public static partial Regex RaceCodeParser(); - - public static GenderRace ParseRaceCode(string path) - { - var match = RaceCodeParser().Match(path); - return match.Success - ? Names.GenderRaceFromCode(match.Groups["racecode"].Value) - : GenderRace.Unknown; - } - - public static partial class Monster - { - public static partial class Imc - { - [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc")] - public static partial Regex Regex(); - - public static string Path(SetId monsterId, SetId bodyId) - => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; - } - - public static partial class Mdl - { - [GeneratedRegex(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl")] - public static partial Regex Regex(); - - public static string Path(SetId monsterId, SetId bodyId) - => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/m{monsterId.Value:D4}b{bodyId.Value:D4}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string Path(SetId monsterId, SetId bodyId, byte variant, string suffix) - => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_m{monsterId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; - } - - public static partial class Tex - { - [GeneratedRegex( - @"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex")] - public static partial Regex Regex(); - - public static string Path(SetId monsterId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/monster/m{monsterId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_m{monsterId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - } - - public static partial class Sklb - { - public static string Path(SetId monsterId) - => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.sklb"; - } - - public static partial class Skp - { - public static string Path(SetId monsterId) - => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/skl_m{monsterId.Value:D4}b0001.skp"; - } - - public static partial class Eid - { - public static string Path(SetId monsterId) - => $"chara/monster/m{monsterId.Value:D4}/skeleton/base/b0001/eid_m{monsterId.Value:D4}b0001.eid"; - } - } - - public static partial class Weapon - { - public static partial class Imc - { - [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc")] - public static partial Regex Regex(); - - public static string Path(SetId weaponId, SetId bodyId) - => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/b{bodyId.Value:D4}.imc"; - } - - public static partial class Mdl - { - [GeneratedRegex(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl")] - public static partial Regex Regex(); - - public static string Path(SetId weaponId, SetId bodyId) - => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/model/w{weaponId.Value:D4}b{bodyId.Value:D4}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string Path(SetId weaponId, SetId bodyId, byte variant, string suffix) - => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/material/v{variant:D4}/mt_w{weaponId.Value:D4}b{bodyId.Value:D4}_{suffix}.mtrl"; - } - - public static partial class Tex - { - [GeneratedRegex( - @"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[a-z])?_[a-z]\.tex")] - public static partial Regex Regex(); - - public static string Path(SetId weaponId, SetId bodyId, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/weapon/w{weaponId.Value:D4}/obj/body/b{bodyId.Value:D4}/texture/v{variant:D2}_w{weaponId.Value:D4}b{bodyId.Value:D4}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - } - } - - public static partial class DemiHuman - { - public static partial class Imc - { - [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc")] - public static partial Regex Regex(); - - public static string Path(SetId demiId, SetId equipId) - => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; - } - - public static partial class Mdl - { - [GeneratedRegex(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl")] - public static partial Regex Regex(); - - public static string Path(SetId demiId, SetId equipId, EquipSlot slot) - => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/model/d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, string suffix) - => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/material/v{variant:D4}/mt_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; - } - - public static partial class Tex - { - [GeneratedRegex( - @"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - public static partial Regex Regex(); - - public static string Path(SetId demiId, SetId equipId, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/demihuman/d{demiId.Value:D4}/obj/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_d{demiId.Value:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - } - } - - public static partial class Equipment - { - public static partial class Imc - { - [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc")] - public static partial Regex Regex(); - - public static string Path(SetId equipId) - => $"chara/equipment/e{equipId.Value:D4}/e{equipId.Value:D4}.imc"; - } - - public static partial class Mdl - { - [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl")] - public static partial Regex Regex(); - - public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot) - => $"chara/equipment/e{equipId.Value:D4}/model/c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) - => $"{FolderPath(equipId, variant)}/mt_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; - - public static string FolderPath(SetId equipId, byte variant) - => $"chara/equipment/e{equipId.Value:D4}/material/v{variant:D4}"; - } - - public static partial class Tex - { - [GeneratedRegex( - @"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - public static partial Regex Regex(); - - public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - } - - public static partial class Avfx - { - [GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")] - public static partial Regex Regex(); - - public static string Path(SetId equipId, byte effectId) - => $"chara/equipment/e{equipId.Value:D4}/vfx/eff/ve{effectId:D4}.avfx"; - } - - public static partial class Decal - { - [GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")] - public static partial Regex Regex(); - - public static string Path(byte decalId) - => $"chara/common/texture/decal_equip/-decal_{decalId:D3}.tex"; - } - } - - public static partial class Accessory - { - public static partial class Imc - { - [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc")] - public static partial Regex Regex(); - - public static string Path(SetId accessoryId) - => $"chara/accessory/a{accessoryId.Value:D4}/a{accessoryId.Value:D4}.imc"; - } - - public static partial class Mdl - { - [GeneratedRegex(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl")] - public static partial Regex Regex(); - - public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot) - => $"chara/accessory/a{accessoryId.Value:D4}/model/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix) - => $"{FolderPath(accessoryId, variant)}/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl"; - - public static string FolderPath(SetId accessoryId, byte variant) - => $"chara/accessory/a{accessoryId.Value:D4}/material/v{variant:D4}"; - } - - public static partial class Tex - { - [GeneratedRegex( - @"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex")] - public static partial Regex Regex(); - - public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0') - => $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - } - } - - public static partial class Skeleton - { - public static partial class Phyb - { - public static string Path(GenderRace raceCode, string slot, SetId slotId) - => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.phyb"; - } - - public static partial class Sklb - { - public static string Path(GenderRace raceCode, string slot, SetId slotId) - => $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot}/{slot[0]}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot[0]}{slotId.Value:D4}.sklb"; - } - } - - public static partial class Character - { - public static partial class Mdl - { - [GeneratedRegex( - @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl")] - public static partial Regex Regex(); - - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type) - => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl"; - } - - public static partial class Mtrl - { - [GeneratedRegex( - @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")] - public static partial Regex Regex(); - - public static string FolderPath(GenderRace raceCode, BodySlot slot, SetId slotId, byte variant = byte.MaxValue) - => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material{(variant != byte.MaxValue ? $"/v{variant:D4}" : string.Empty)}"; - - public static string HairPath(GenderRace raceCode, SetId slotId, string fileName, out GenderRace actualGr) - { - actualGr = MaterialHandling.GetGameGenderRace(raceCode, slotId); - var folder = FolderPath(actualGr, BodySlot.Hair, slotId, 1); - return actualGr == raceCode - ? $"{folder}{fileName}" - : $"{folder}/mt_c{actualGr.ToRaceCode()}{fileName[9..]}"; - } - - public static string TailPath(GenderRace raceCode, SetId slotId, string fileName, byte variant, out SetId actualSlotId) - { - switch (raceCode) - { - case GenderRace.HrothgarMale: - case GenderRace.HrothgarFemale: - case GenderRace.HrothgarMaleNpc: - case GenderRace.HrothgarFemaleNpc: - var folder = FolderPath(raceCode, BodySlot.Tail, 1, variant == byte.MaxValue ? (byte)1 : variant); - actualSlotId = 1; - return $"{folder}{fileName}"; - default: - actualSlotId = slotId; - return $"{FolderPath(raceCode, BodySlot.Tail, slotId, variant)}{fileName}"; - } - } - - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string fileName, - out GenderRace actualGr, out SetId actualSlotId, byte variant = byte.MaxValue) - { - switch (slot) - { - case BodySlot.Hair: - actualSlotId = slotId; - return HairPath(raceCode, slotId, fileName, out actualGr); - case BodySlot.Tail: - actualGr = raceCode; - return TailPath(raceCode, slotId, fileName, variant, out actualSlotId); - default: - actualSlotId = slotId; - actualGr = raceCode; - return $"{FolderPath(raceCode, slot, slotId, variant)}{fileName}"; - } - } - } - - public static partial class Tex - { - [GeneratedRegex( - @"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")] - public static partial Regex Regex(); - - public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false, - CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0') - => $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/" - + (minus ? "--" : string.Empty) - + (variant != byte.MaxValue ? $"v{variant:D2}_" : string.Empty) - + $"c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex"; - - - [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")] - public static partial Regex CatchlightRegex(); - - [GeneratedRegex(@"chara/common/texture/skin(?'skin'.*)\.tex")] - public static partial Regex SkinRegex(); - - [GeneratedRegex(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex")] - public static partial Regex DecalRegex(); - - [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")] - public static partial Regex FolderRegex(); - } - } - - public static partial class Icon - { - [GeneratedRegex(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex")] - public static partial Regex Regex(); - } - - public static partial class Map - { - [GeneratedRegex(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex")] - public static partial Regex Regex(); - } - - public static partial class Font - { - [GeneratedRegex(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt")] - public static partial Regex Regex(); - } - - public static partial class Vfx - { - [GeneratedRegex(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.IgnoreCase)] - public static partial Regex Tmb(); - - [GeneratedRegex(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", RegexOptions.IgnoreCase)] - public static partial Regex Pap(); - } - - public static partial class Shader - { - public static string ShpkPath(string name) - => $"shader/sm5/shpk/{name}"; - } -} diff --git a/Penumbra.GameData/Data/HumanModelList.cs b/Penumbra.GameData/Data/HumanModelList.cs deleted file mode 100644 index 719c7bbb..00000000 --- a/Penumbra.GameData/Data/HumanModelList.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections; -using System.Linq; -using Dalamud; -using Dalamud.Data; -using Dalamud.Plugin; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Lumina.Excel.GeneratedSheets; - -namespace Penumbra.GameData.Data; - -public sealed class HumanModelList : DataSharer -{ - public const string Tag = "HumanModels"; - public const int CurrentVersion = 2; - - private readonly BitArray _humanModels; - - public HumanModelList(DalamudPluginInterface pluginInterface, DataManager gameData) - : base(pluginInterface, ClientLanguage.English, CurrentVersion) - { - _humanModels = TryCatchData(Tag, () => GetValidHumanModels(gameData)); - } - - public bool IsHuman(uint modelId) - => modelId < _humanModels.Count && _humanModels[(int)modelId]; - - public int Count - => _humanModels.Count; - - protected override void DisposeInternal() - { - DisposeTag(Tag); - } - - /// - /// Go through all ModelChara rows and return a bitfield of those that resolve to human models. - /// - private static BitArray GetValidHumanModels(DataManager gameData) - { - var sheet = gameData.GetExcelSheet()!; - var ret = new BitArray((int)sheet.RowCount, false); - foreach (var (_, idx) in sheet.Select((m, i) => (m, i)).Where(p => p.m.Type == (byte)CharacterBase.ModelType.Human)) - ret[idx] = true; - - return ret; - } -} diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs deleted file mode 100644 index c9f69694..00000000 --- a/Penumbra.GameData/Data/ItemData.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Data; -using Dalamud.Plugin; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; - -namespace Penumbra.GameData.Data; - -public sealed class ItemData : DataSharer, IReadOnlyDictionary> -{ - private readonly IReadOnlyDictionary _mainItems; - private readonly IReadOnlyDictionary _offItems; - private readonly IReadOnlyDictionary _gauntlets; - private readonly IReadOnlyList> _byType; - - private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) - { - var tmp = Enum.GetValues().Select(_ => new List(1024)).ToArray(); - - var itemSheet = dataManager.GetExcelSheet(language)!; - foreach (var item in itemSheet.Where(i => i.Name.RawData.Length > 1)) - { - var type = item.ToEquipType(); - if (type.IsWeapon() || type.IsTool()) - { - var mh = EquipItem.FromMainhand(item); - if (item.ModelMain != 0) - tmp[(int)type].Add(mh); - if (item.ModelSub != 0) - { - if (type is FullEquipType.Fists && item.ModelSub < 0x100000000) - { - tmp[(int)FullEquipType.Hands].Add(new EquipItem(mh.Name + $" (Gauntlets)", mh.Id, mh.IconId, (SetId)item.ModelSub, 0, - (byte)(item.ModelSub >> 16), FullEquipType.Hands)); - tmp[(int)FullEquipType.FistsOff].Add(new EquipItem(mh.Name + FullEquipType.FistsOff.OffhandTypeSuffix(), mh.Id, - mh.IconId, (SetId)(mh.ModelId.Value + 50), mh.WeaponType, mh.Variant, FullEquipType.FistsOff)); - } - else - { - tmp[(int)type.ValidOffhand()].Add(EquipItem.FromOffhand(item)); - } - } - } - else if (type != FullEquipType.Unknown) - { - tmp[(int)type].Add(EquipItem.FromArmor(item)); - } - } - - var ret = new IReadOnlyList[tmp.Length]; - ret[0] = Array.Empty(); - for (var i = 1; i < tmp.Length; ++i) - ret[i] = tmp[i].OrderBy(item => item.Name).Select(s => (PseudoEquipItem)s).ToArray(); - - return ret; - } - - private static Tuple, IReadOnlyDictionary> CreateMainItems( - IReadOnlyList> items) - { - var dict = new Dictionary(1024 * 4); - foreach (var fistWeapon in items[(int)FullEquipType.Fists]) - dict.TryAdd((uint)fistWeapon.Item2, fistWeapon); - - var gauntlets = items[(int)FullEquipType.Hands].Where(g => dict.ContainsKey((uint)g.Item2)).ToDictionary(g => (uint)g.Item2, g => g); - gauntlets.TrimExcess(); - - foreach (var type in Enum.GetValues().Where(v => !FullEquipTypeExtensions.OffhandTypes.Contains(v))) - { - var list = items[(int)type]; - foreach (var item in list) - dict.TryAdd((uint)item.Item2, item); - } - - dict.TrimExcess(); - return new Tuple, - IReadOnlyDictionary>(dict, gauntlets); - } - - private static IReadOnlyDictionary CreateOffItems(IReadOnlyList> items) - { - var dict = new Dictionary(128); - foreach (var type in FullEquipTypeExtensions.OffhandTypes) - { - var list = items[(int)type]; - foreach (var item in list) - dict.TryAdd((uint)item.Item2, item); - } - - dict.TrimExcess(); - return dict; - } - - public ItemData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) - : base(pluginInterface, language, 4) - { - _byType = TryCatchData("ItemList", () => CreateItems(dataManager, language)); - (_mainItems, _gauntlets) = TryCatchData("ItemDictMain", () => CreateMainItems(_byType)); - _offItems = TryCatchData("ItemDictOff", () => CreateOffItems(_byType)); - } - - protected override void DisposeInternal() - { - DisposeTag("ItemList"); - DisposeTag("ItemDictMain"); - DisposeTag("ItemDictOff"); - } - - public IEnumerator>> GetEnumerator() - { - for (var i = 1; i < _byType.Count; ++i) - yield return new KeyValuePair>((FullEquipType)i, new EquipItemList(_byType[i])); - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _byType.Count - 1; - - public bool ContainsKey(FullEquipType key) - => (int)key < _byType.Count && key != FullEquipType.Unknown; - - public bool TryGetValue(FullEquipType key, out IReadOnlyList value) - { - if (ContainsKey(key)) - { - value = new EquipItemList(_byType[(int)key]); - return true; - } - - value = Array.Empty(); - return false; - } - - public IReadOnlyList this[FullEquipType key] - => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); - - public IEnumerable<(uint, EquipItem)> AllItems(bool main) - => (main ? _mainItems : _offItems).Select(i => (i.Key, (EquipItem)i.Value)); - - public int TotalItemCount(bool main) - => main ? _mainItems.Count : _offItems.Count; - - public bool TryGetValue(uint key, EquipSlot slot, out EquipItem value) - { - var dict = slot is EquipSlot.OffHand ? _offItems : _mainItems; - if (slot is EquipSlot.Hands && _gauntlets.TryGetValue(key, out var v) || dict.TryGetValue(key, out v)) - { - value = v; - return true; - } - - value = default; - return false; - } - - public IEnumerable Keys - => Enum.GetValues().Skip(1); - - public IEnumerable> Values - => _byType.Skip(1).Select(l => (IReadOnlyList)new EquipItemList(l)); - - private readonly struct EquipItemList : IReadOnlyList - { - private readonly IReadOnlyList _items; - - public EquipItemList(IReadOnlyList items) - => _items = items; - - public IEnumerator GetEnumerator() - => _items.Select(i => (EquipItem)i).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _items.Count; - - public EquipItem this[int index] - => _items[index]; - } -} diff --git a/Penumbra.GameData/Data/KeyList.cs b/Penumbra.GameData/Data/KeyList.cs deleted file mode 100644 index a6109674..00000000 --- a/Penumbra.GameData/Data/KeyList.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Plugin; - -namespace Penumbra.GameData.Data; - -/// -/// A list sorting objects based on a key which then allows efficiently finding all objects between a pair of keys via binary search. -/// -public abstract class KeyList -{ - private readonly List<(ulong Key, T Data)> _list; - - public IReadOnlyList<(ulong Key, T Data)> List - => _list; - - /// - /// Iterate over all objects between the given minimal and maximal keys (inclusive). - /// - protected IEnumerable Between(ulong minKey, ulong maxKey) - { - var (minIdx, maxIdx) = GetMinMax(minKey, maxKey); - if (minIdx < 0) - yield break; - - for (var i = minIdx; i <= maxIdx; ++i) - yield return _list[i].Data; - } - - private (int MinIdx, int MaxIdx) GetMinMax(ulong minKey, ulong maxKey) - { - var idx = _list.BinarySearch((minKey, default!), ListComparer); - var minIdx = idx; - if (minIdx < 0) - { - minIdx = ~minIdx; - if (minIdx == _list.Count || _list[minIdx].Key > maxKey) - return (-1, -1); - - idx = minIdx; - } - else - { - while (minIdx > 0 && _list[minIdx - 1].Key >= minKey) - --minIdx; - } - - if (_list[minIdx].Key < minKey || _list[minIdx].Key > maxKey) - return (-1, -1); - - - var maxIdx = _list.BinarySearch(idx, _list.Count - idx, (maxKey, default!), ListComparer); - if (maxIdx < 0) - { - maxIdx = ~maxIdx; - return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); - } - - while (maxIdx < _list.Count - 1 && _list[maxIdx + 1].Key <= maxKey) - ++maxIdx; - - if (_list[maxIdx].Key < minKey || _list[maxIdx].Key > maxKey) - return (-1, -1); - - return (minIdx, maxIdx); - } - - /// - /// The function turning an object to (potentially multiple) keys. Only used during construction. - /// - protected abstract IEnumerable ToKeys(T data); - - /// - /// Whether a returned key is valid. Only used during construction. - /// - protected abstract bool ValidKey(ulong key); - - /// - /// How multiple items with the same key should be sorted. - /// - protected abstract int ValueKeySelector(T data); - - protected KeyList(DalamudPluginInterface pi, string tag, ClientLanguage language, int version, IEnumerable data) - { - _list = DataSharer.TryCatchData(pi, tag, language, version, - () => data.SelectMany(d => ToKeys(d).Select(k => (k, d))) - .Where(p => ValidKey(p.k)) - .OrderBy(p => p.k) - .ThenBy(p => ValueKeySelector(p.d)) - .ToList()); - } - - private class Comparer : IComparer<(ulong, T)> - { - public int Compare((ulong, T) x, (ulong, T) y) - => x.Item1.CompareTo(y.Item1); - } - - private static readonly Comparer ListComparer = new(); -} diff --git a/Penumbra.GameData/Data/MaterialHandling.cs b/Penumbra.GameData/Data/MaterialHandling.cs deleted file mode 100644 index ef336a9d..00000000 --- a/Penumbra.GameData/Data/MaterialHandling.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Data; - -public static class MaterialHandling -{ - public static GenderRace GetGameGenderRace(GenderRace actualGr, SetId hairId) - { - // Hrothgar do not share hairstyles. - if (actualGr is GenderRace.HrothgarFemale or GenderRace.HrothgarMale) - return actualGr; - - // Some hairstyles are miqo'te specific but otherwise shared. - if (hairId.Value is >= 101 and <= 115) - { - if (actualGr is GenderRace.MiqoteFemale or GenderRace.MiqoteMale) - return actualGr; - - return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale; - } - - // All hairstyles above 116 are shared except for Hrothgar - if (hairId.Value is >= 116 and <= 200) - return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale; - - return actualGr; - } - - public static bool IsSpecialCase(GenderRace gr, SetId hairId) - => gr is GenderRace.MidlanderMale or GenderRace.MidlanderFemale && hairId.Value is >= 101 and <= 200; -} diff --git a/Penumbra.GameData/Data/ModelIdentificationList.cs b/Penumbra.GameData/Data/ModelIdentificationList.cs deleted file mode 100644 index e1179898..00000000 --- a/Penumbra.GameData/Data/ModelIdentificationList.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using Dalamud; -using Dalamud.Data; -using Dalamud.Plugin; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Data; - -internal sealed class ModelIdentificationList : KeyList -{ - private const string Tag = "ModelIdentification"; - - public ModelIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) - : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateModelList(gameData, language)) - { } - - public IEnumerable Between(CharacterBase.ModelType type, SetId modelId, byte modelBase = 0, byte variant = 0) - { - if (modelBase == 0) - return Between(ToKey(type, modelId, 0, 0), ToKey(type, modelId, 0xFF, 0xFF)); - if (variant == 0) - return Between(ToKey(type, modelId, modelBase, 0), ToKey(type, modelId, modelBase, 0xFF)); - - return Between(ToKey(type, modelId, modelBase, variant), ToKey(type, modelId, modelBase, variant)); - } - - public void Dispose(DalamudPluginInterface pi, ClientLanguage language) - => DataSharer.DisposeTag(pi, Tag, language, ObjectIdentification.IdentificationVersion); - - - public static ulong ToKey(CharacterBase.ModelType type, SetId model, byte modelBase, byte variant) - => ((ulong)type << 32) | ((ulong)model << 16) | ((ulong)modelBase << 8) | variant; - - private static ulong ToKey(ModelChara row) - => ToKey((CharacterBase.ModelType)row.Type, row.Model, row.Base, row.Variant); - - protected override IEnumerable ToKeys(ModelChara row) - { - yield return ToKey(row); - } - - protected override bool ValidKey(ulong key) - => key != 0; - - protected override int ValueKeySelector(ModelChara data) - => (int)data.RowId; - - private static IEnumerable CreateModelList(DataManager gameData, ClientLanguage language) - => gameData.GetExcelSheet(language)!; -} diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs deleted file mode 100644 index 4aa766a1..00000000 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ /dev/null @@ -1,331 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Dalamud; -using Dalamud.Data; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Plugin; -using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.Actors; -using Action = Lumina.Excel.GeneratedSheets.Action; -using ObjectType = Penumbra.GameData.Enums.ObjectType; - -namespace Penumbra.GameData.Data; - -internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier -{ - public const int IdentificationVersion = 3; - - public IGamePathParser GamePathParser { get; } = new GamePathParser(); - public readonly IReadOnlyList> BnpcNames; - public readonly IReadOnlyList> ModelCharaToObjects; - public readonly IReadOnlyDictionary> Actions; - private readonly ActorManager.ActorManagerData _actorData; - - private readonly EquipmentIdentificationList _equipment; - private readonly WeaponIdentificationList _weapons; - private readonly ModelIdentificationList _modelIdentifierToModelChara; - - public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language) - : base(pluginInterface, language, IdentificationVersion) - { - _actorData = new ActorManager.ActorManagerData(pluginInterface, dataManager, language); - _equipment = new EquipmentIdentificationList(pluginInterface, language, itemData); - _weapons = new WeaponIdentificationList(pluginInterface, language, itemData); - Actions = TryCatchData("Actions", () => CreateActionList(dataManager)); - - _modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager); - BnpcNames = TryCatchData("BNpcNames", NpcNames.CreateNames); - ModelCharaToObjects = TryCatchData("ModelObjects", () => CreateModelObjects(_actorData, dataManager, language)); - } - - public void Identify(IDictionary set, string path) - { - if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)) - if (IdentifyVfx(set, path)) - return; - - var info = GamePathParser.GetFileInfo(path); - IdentifyParsed(set, info); - } - - public Dictionary Identify(string path) - { - Dictionary ret = new(); - Identify(ret, path); - return ret; - } - - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), - EquipSlot.OffHand => _weapons.Between(setId, weaponType, (byte)variant), - _ => _equipment.Between(setId, slot, (byte)variant), - }; - - public IReadOnlyList GetBnpcNames(uint bNpcId) - => bNpcId >= BnpcNames.Count ? Array.Empty() : BnpcNames[(int)bNpcId]; - - public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId) - => modelId >= ModelCharaToObjects.Count ? Array.Empty<(string Name, ObjectKind Kind, uint Id)>() : ModelCharaToObjects[(int)modelId]; - - public int NumModelChara - => ModelCharaToObjects.Count; - - protected override void DisposeInternal() - { - _actorData.Dispose(); - _weapons.Dispose(PluginInterface, Language); - _equipment.Dispose(PluginInterface, Language); - DisposeTag("Actions"); - DisposeTag("Models"); - - _modelIdentifierToModelChara.Dispose(PluginInterface, Language); - DisposeTag("BNpcNames"); - DisposeTag("ModelObjects"); - } - - private IReadOnlyDictionary> CreateActionList(DataManager gameData) - { - var sheet = gameData.GetExcelSheet(Language)!; - var storage = new ConcurrentDictionary>(); - - void AddAction(string? key, Action action) - { - if (key.IsNullOrEmpty()) - return; - - key = key.ToLowerInvariant(); - if (storage.TryGetValue(key, out var actions)) - actions.Add(action); - else - storage[key] = new ConcurrentBag { action }; - } - - var options = new ParallelOptions - { - MaxDegreeOfParallelism = Environment.ProcessorCount, - }; - - Parallel.ForEach(sheet.Where(a => !a.Name.RawData.IsEmpty), options, action => - { - var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); - var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); - var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); - AddAction(startKey, action); - AddAction(endKey, action); - AddAction(hitKey, action); - }); - - return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); - } - - private void FindEquipment(IDictionary set, GameObjectInfo info) - { - var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant); - foreach (var item in items) - set[item.Name] = item; - } - - private void FindWeapon(IDictionary set, GameObjectInfo info) - { - var items = _weapons.Between(info.PrimaryId, info.SecondaryId, info.Variant); - foreach (var item in items) - set[item.Name] = item; - } - - private void FindModel(IDictionary set, GameObjectInfo info) - { - var type = info.ObjectType.ToModelType(); - if (type is 0 or CharacterBase.ModelType.Weapon) - return; - - var models = _modelIdentifierToModelChara.Between(type, info.PrimaryId, (byte)info.SecondaryId, info.Variant); - foreach (var model in models.Where(m => m.RowId != 0 && m.RowId < ModelCharaToObjects.Count)) - { - var objectList = ModelCharaToObjects[(int)model.RowId]; - foreach (var (name, kind, _) in objectList) - set[$"{name} ({kind.ToName()})"] = model; - } - } - - private static void AddCounterString(IDictionary set, string data) - { - if (set.TryGetValue(data, out var obj) && obj is int counter) - set[data] = counter + 1; - else - set[data] = 1; - } - - private void IdentifyParsed(IDictionary set, GameObjectInfo info) - { - switch (info.FileType) - { - case FileType.Sound: - AddCounterString(set, FileType.Sound.ToString()); - return; - case FileType.Animation: - case FileType.Pap: - AddCounterString(set, FileType.Animation.ToString()); - return; - case FileType.Shader: - AddCounterString(set, FileType.Shader.ToString()); - return; - } - - switch (info.ObjectType) - { - case ObjectType.LoadingScreen: - case ObjectType.Map: - case ObjectType.Interface: - case ObjectType.Vfx: - case ObjectType.World: - case ObjectType.Housing: - case ObjectType.Font: - AddCounterString(set, info.ObjectType.ToString()); - break; - case ObjectType.DemiHuman: - FindModel(set, info); - break; - case ObjectType.Monster: - FindModel(set, info); - break; - case ObjectType.Icon: - set[$"Icon: {info.IconId}"] = null; - break; - case ObjectType.Accessory: - case ObjectType.Equipment: - FindEquipment(set, info); - break; - case ObjectType.Weapon: - FindWeapon(set, info); - break; - case ObjectType.Character: - var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; - var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; - switch (info.CustomizationType) - { - case CustomizationType.Skin: - set[$"Customization: {raceString}{genderString}Skin Textures"] = null; - break; - 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; - case CustomizationType.DecalEquip: - set[$"Equipment Decal {info.PrimaryId}"] = null; - break; - default: - { - 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; - } - } - - break; - } - } - - private bool IdentifyVfx(IDictionary set, string path) - { - var key = GamePathParser.VfxToKey(path); - if (key.Length == 0 || !Actions.TryGetValue(key, out var actions) || actions.Count == 0) - return false; - - foreach (var action in actions) - set[$"Action: {action.Name}"] = action; - return true; - } - - private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, - DataManager gameData, ClientLanguage language) - { - var modelSheet = gameData.GetExcelSheet(language)!; - var ret = new List>((int)modelSheet.RowCount); - - for (var i = -1; i < modelSheet.Last().RowId; ++i) - ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind, uint Id)>()); - - void AddChara(int modelChara, ObjectKind kind, uint dataId, uint displayId) - { - if (modelChara >= ret.Count) - return; - - if (actors.TryGetName(kind, dataId, out var name)) - ret[modelChara].Add((name, kind, displayId)); - } - - var oTask = Task.Run(() => - { - foreach (var ornament in gameData.GetExcelSheet(language)!) - AddChara(ornament.Model, ObjectKind.Ornament, ornament.RowId, ornament.RowId); - }); - - var mTask = Task.Run(() => - { - foreach (var mount in gameData.GetExcelSheet(language)!) - AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId, mount.RowId); - }); - - var cTask = Task.Run(() => - { - foreach (var companion in gameData.GetExcelSheet(language)!) - AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId, companion.RowId); - }); - - var eTask = Task.Run(() => - { - foreach (var eNpc in gameData.GetExcelSheet(language)!) - AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId, eNpc.RowId); - }); - - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), - }; - - Parallel.ForEach(gameData.GetExcelSheet(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc => - { - foreach (var name in BnpcNames[(int)bNpc.RowId]) - AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name, bNpc.RowId); - }); - - Task.WaitAll(oTask, mTask, cTask, eTask); - - return ret.Select(s => !s.IsEmpty - ? s.ToArray() - : Array.Empty<(string Name, ObjectKind Kind, uint Id)>()).ToArray(); - } - - public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) - { - var type = (*(delegate* unmanaged**)drawObject)[Offsets.DrawObjectGetModelTypeVfunc](drawObject); - var unk = (ulong)*((byte*)drawObject + Offsets.DrawObjectModelUnk1) << 8; - return type switch - { - 1 => type | unk, - 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 16), - 3 => type - | unk - | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk2) << 16) - | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk4) << 32) - | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 40), - _ => 0u, - }; - } -} diff --git a/Penumbra.GameData/Data/RestrictedGear.cs b/Penumbra.GameData/Data/RestrictedGear.cs deleted file mode 100644 index 74fcd975..00000000 --- a/Penumbra.GameData/Data/RestrictedGear.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Data; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Utility; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Race = Penumbra.GameData.Enums.Race; - -namespace Penumbra.GameData.Data; - -/// -/// Handle gender- or race-locked gear in the draw model itself. -/// Racial gear gets swapped to the correct current race and gender (it is one set each). -/// Gender-locked gear gets swapped to the equivalent set if it exists (most of them do), -/// with some items getting send to emperor's new clothes and a few funny entries. -/// -public sealed class RestrictedGear : DataSharer -{ - private readonly ExcelSheet _items; - private readonly ExcelSheet _categories; - - public readonly IReadOnlySet RaceGenderSet; - public readonly IReadOnlyDictionary MaleToFemale; - public readonly IReadOnlyDictionary FemaleToMale; - - public RestrictedGear(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) - : base(pi, language, 2) - { - _items = gameData.GetExcelSheet()!; - _categories = gameData.GetExcelSheet()!; - (RaceGenderSet, MaleToFemale, FemaleToMale) = TryCatchData("RestrictedGear", CreateRestrictedGear); - } - - protected override void DisposeInternal() - => DisposeTag("RestrictedGear"); - - /// - /// Resolve a model given by its model id, variant and slot for your current race and gender. - /// - /// The equipment piece. - /// The equipment slot. - /// The intended race. - /// The intended gender. - /// True and the changed-to piece of gear or false and the same piece of gear. - public (bool Replaced, CharacterArmor Armor) ResolveRestricted(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) - { - var quad = armor.Set.Value | ((uint)armor.Variant << 16); - // Check racial gear, this does not need slots. - if (RaceGenderGroup.Contains(quad)) - { - var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0); - var value = RaceGenderGroup[idx]; - return (value != quad, new CharacterArmor((ushort)value, (byte)(value >> 16), armor.Stain)); - } - - // Check gender slots. If current gender is female, check if anything needs to be changed from male to female, - // and vice versa. - // Some items lead to the exact same model- and variant id just gender specified, - // so check for actual difference in the Replaced bool. - var needle = quad | ((uint)slot.ToSlot() << 24); - if (gender is Gender.Female or Gender.FemaleNpc && MaleToFemale.TryGetValue(needle, out var newValue) - || gender is Gender.Male or Gender.MaleNpc && FemaleToMale.TryGetValue(needle, out newValue)) - return (quad != newValue, new CharacterArmor((ushort)newValue, (byte)(newValue >> 16), armor.Stain)); - - // The gear is not restricted. - return (false, armor); - } - - private Tuple, IReadOnlyDictionary, IReadOnlyDictionary> CreateRestrictedGear() - { - var m2f = new Dictionary(); - var f2m = new Dictionary(); - var rg = RaceGenderGroup.Where(c => c is not 0 and not uint.MaxValue).ToHashSet(); - AddKnown(m2f, f2m); - UnhandledRestrictedGear(rg, m2f, f2m, false); // Set this to true to create a print of unassigned gear on launch. - return new Tuple, IReadOnlyDictionary, IReadOnlyDictionary>(rg, m2f, f2m); - } - - - // Add all unknown restricted gear and pair it with emperor's new gear on start up. - // Can also print unhandled items. - private void UnhandledRestrictedGear(IReadOnlySet rg, Dictionary m2f, Dictionary f2m, bool print) - { - if (print) - PluginLog.Information("#### MALE ONLY ######"); - - void AddEmperor(Item item, bool male, bool female) - { - var slot = ((EquipSlot)item.EquipSlotCategory.Row).ToSlot(); - var emperor = slot switch - { - EquipSlot.Head => 10032u, - EquipSlot.Body => 10033u, - EquipSlot.Hands => 10034u, - EquipSlot.Legs => 10035u, - EquipSlot.Feet => 10036u, - EquipSlot.Ears => 09293u, - EquipSlot.Neck => 09292u, - EquipSlot.Wrists => 09294u, - EquipSlot.RFinger => 09295u, - EquipSlot.LFinger => 09295u, - _ => 0u, - }; - if (emperor == 0) - return; - - if (male) - AddItem(m2f, f2m, item.RowId, emperor, true, false); - if (female) - AddItem(m2f, f2m, emperor, item.RowId, false, true); - } - - var unhandled = 0; - foreach (var item in _items.Where(i => i.EquipRestriction == 2)) - { - if (m2f.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24))) - continue; - - ++unhandled; - AddEmperor(item, true, false); - - if (print) - PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}"); - } - - if (print) - PluginLog.Information("#### FEMALE ONLY ####"); - foreach (var item in _items.Where(i => i.EquipRestriction == 3)) - { - if (f2m.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24))) - continue; - - ++unhandled; - AddEmperor(item, false, true); - - if (print) - PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}"); - } - - if (print) - PluginLog.Information("#### OTHER #########"); - - foreach (var item in _items.Where(i => i.EquipRestriction > 3)) - { - if (rg.Contains((uint)item.ModelMain)) - continue; - - ++unhandled; - if (print) - PluginLog.Information( - $"{item.RowId:D5} {item.Name.ToDalamudString().TextValue} RestrictionGroup {_categories.GetRow(item.EquipRestriction)!.RowId:D2}"); - } - - if (unhandled > 0) - PluginLog.Warning($"There were {unhandled} restricted items not handled and directed to Emperor's New Set."); - } - - // Add a item redirection by its item - NOT MODEL - id. - // This uses the items model as well as its slot. - // Creates a <-> redirection by default but can add -> or <- redirections by setting the corresponding bools to false. - // Prints warnings if anything does not make sense. - private void AddItem(Dictionary m2f, Dictionary f2m, uint itemIdMale, uint itemIdFemale, bool addMale = true, - bool addFemale = true) - { - if (!addMale && !addFemale) - return; - - var mItem = _items.GetRow(itemIdMale); - var fItem = _items.GetRow(itemIdFemale); - if (mItem == null || fItem == null) - { - PluginLog.Warning($"Could not add item {itemIdMale} or {itemIdFemale} to restricted items."); - return; - } - - if (mItem.EquipRestriction != 2 && addMale) - { - PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not restricted anymore."); - return; - } - - if (fItem.EquipRestriction != 3 && addFemale) - { - PluginLog.Warning($"{fItem.Name.ToDalamudString().TextValue} is not restricted anymore."); - return; - } - - var mSlot = ((EquipSlot)mItem.EquipSlotCategory.Row).ToSlot(); - var fSlot = ((EquipSlot)fItem.EquipSlotCategory.Row).ToSlot(); - if (!mSlot.IsAccessory() && !mSlot.IsEquipment()) - { - PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not equippable to a known slot."); - return; - } - - if (mSlot != fSlot) - { - PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} and {fItem.Name.ToDalamudString().TextValue} are not compatible."); - return; - } - - var mModelIdSlot = (uint)mItem.ModelMain | ((uint)mSlot << 24); - var fModelIdSlot = (uint)fItem.ModelMain | ((uint)fSlot << 24); - - if (addMale) - m2f.TryAdd(mModelIdSlot, fModelIdSlot); - if (addFemale) - f2m.TryAdd(fModelIdSlot, mModelIdSlot); - } - - // @formatter:off - // Add all currently existing and known gender restricted items. - private void AddKnown(Dictionary m2f, Dictionary f2m) - { - AddItem(m2f, f2m, 02967, 02970); // Lord's Yukata (Blue) <-> Lady's Yukata (Red) - AddItem(m2f, f2m, 02968, 02971); // Lord's Yukata (Green) <-> Lady's Yukata (Blue) - AddItem(m2f, f2m, 02969, 02972); // Lord's Yukata (Grey) <-> Lady's Yukata (Black) - AddItem(m2f, f2m, 02973, 02978); // Red Summer Top <-> Red Summer Halter - AddItem(m2f, f2m, 02974, 02979); // Green Summer Top <-> Green Summer Halter - AddItem(m2f, f2m, 02975, 02980); // Blue Summer Top <-> Blue Summer Halter - AddItem(m2f, f2m, 02976, 02981); // Solar Summer Top <-> Solar Summer Halter - AddItem(m2f, f2m, 02977, 02982); // Lunar Summer Top <-> Lunar Summer Halter - AddItem(m2f, f2m, 02996, 02997); // Hempen Undershirt <-> Hempen Camise - AddItem(m2f, f2m, 03280, 03283); // Lord's Drawers (Black) <-> Lady's Knickers (Black) - AddItem(m2f, f2m, 03281, 03284); // Lord's Drawers (White) <-> Lady's Knickers (White) - AddItem(m2f, f2m, 03282, 03285); // Lord's Drawers (Gold) <-> Lady's Knickers (Gold) - AddItem(m2f, f2m, 03286, 03291); // Red Summer Trunks <-> Red Summer Tanga - AddItem(m2f, f2m, 03287, 03292); // Green Summer Trunks <-> Green Summer Tanga - AddItem(m2f, f2m, 03288, 03293); // Blue Summer Trunks <-> Blue Summer Tanga - AddItem(m2f, f2m, 03289, 03294); // Solar Summer Trunks <-> Solar Summer Tanga - AddItem(m2f, f2m, 03290, 03295); // Lunar Summer Trunks <-> Lunar Summer Tanga - AddItem(m2f, f2m, 03307, 03308); // Hempen Underpants <-> Hempen Pantalettes - AddItem(m2f, f2m, 03748, 03749); // Lord's Clogs <-> Lady's Clogs - AddItem(m2f, f2m, 06045, 06041); // Bohemian's Coat <-> Guardian Corps Coat - AddItem(m2f, f2m, 06046, 06042); // Bohemian's Gloves <-> Guardian Corps Gauntlets - AddItem(m2f, f2m, 06047, 06043); // Bohemian's Trousers <-> Guardian Corps Skirt - AddItem(m2f, f2m, 06048, 06044); // Bohemian's Boots <-> Guardian Corps Boots - AddItem(m2f, f2m, 06094, 06098); // Summer Evening Top <-> Summer Morning Halter - AddItem(m2f, f2m, 06095, 06099); // Summer Evening Trunks <-> Summer Morning Tanga - AddItem(m2f, f2m, 06096, 06100); // Striped Summer Top <-> Striped Summer Halter - AddItem(m2f, f2m, 06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga - AddItem(m2f, f2m, 06102, 06104); // Black Summer Top <-> Black Summer Halter - AddItem(m2f, f2m, 06103, 06105); // Black Summer Trunks <-> Black Summer Tanga - AddItem(m2f, f2m, 08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly) - AddItem(m2f, f2m, 08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly) - AddItem(m2f, f2m, 08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly) - AddItem(m2f, f2m, 08542, 08549); // Ti Leaf Lei <-> Coronal Summer Halter - AddItem(m2f, f2m, 08543, 08550); // Red Summer Maro <-> Red Summer Pareo - AddItem(m2f, f2m, 08544, 08551); // South Seas Talisman <-> Sea Breeze Summer Halter - AddItem(m2f, f2m, 08545, 08552); // Blue Summer Maro <-> Sea Breeze Summer Pareo - AddItem(m2f, f2m, 08546, 08553); // Coeurl Talisman <-> Coeurl Beach Halter - AddItem(m2f, f2m, 08547, 08554); // Coeurl Beach Maro <-> Coeurl Beach Pareo - AddItem(m2f, f2m, 08548, 08555); // Coeurl Beach Briefs <-> Coeurl Beach Tanga - AddItem(m2f, f2m, 10316, 10317); // Southern Seas Vest <-> Southern Seas Swimsuit - AddItem(m2f, f2m, 10318, 10319); // Southern Seas Trunks <-> Southern Seas Tanga - AddItem(m2f, f2m, 10320, 10321); // Striped Southern Seas Vest <-> Striped Southern Seas Swimsuit - AddItem(m2f, f2m, 13298, 13567); // Black-feathered Flat Hat <-> Red-feathered Flat Hat - AddItem(m2f, f2m, 13300, 13639); // Lord's Suikan <-> Lady's Suikan - AddItem(m2f, f2m, 13724, 13725); // Little Lord's Clogs <-> Little Lady's Clogs - AddItem(m2f, f2m, 14854, 14857); // Eastern Lord's Togi <-> Eastern Lady's Togi - AddItem(m2f, f2m, 14855, 14858); // Eastern Lord's Trousers <-> Eastern Lady's Loincloth - AddItem(m2f, f2m, 14856, 14859); // Eastern Lord's Crakows <-> Eastern Lady's Crakows - AddItem(m2f, f2m, 15639, 15642); // Far Eastern Patriarch's Hat <-> Far Eastern Matriarch's Sun Hat - AddItem(m2f, f2m, 15640, 15643); // Far Eastern Patriarch's Tunic <-> Far Eastern Matriarch's Dress - AddItem(m2f, f2m, 15641, 15644); // Far Eastern Patriarch's Longboots <-> Far Eastern Matriarch's Boots - AddItem(m2f, f2m, 15922, 15925); // Moonfire Vest <-> Moonfire Halter - AddItem(m2f, f2m, 15923, 15926); // Moonfire Trunks <-> Moonfire Tanga - AddItem(m2f, f2m, 15924, 15927); // Moonfire Caligae <-> Moonfire Sandals - AddItem(m2f, f2m, 16106, 16111); // Makai Mauler's Facemask <-> Makai Manhandler's Facemask - AddItem(m2f, f2m, 16107, 16112); // Makai Mauler's Oilskin <-> Makai Manhandler's Jerkin - AddItem(m2f, f2m, 16108, 16113); // Makai Mauler's Fingerless Gloves <-> Makai Manhandler's Fingerless Gloves - AddItem(m2f, f2m, 16109, 16114); // Makai Mauler's Leggings <-> Makai Manhandler's Quartertights - AddItem(m2f, f2m, 16110, 16115); // Makai Mauler's Boots <-> Makai Manhandler's Longboots - AddItem(m2f, f2m, 16116, 16121); // Makai Marksman's Eyepatch <-> Makai Markswoman's Ribbon - AddItem(m2f, f2m, 16117, 16122); // Makai Marksman's Battlegarb <-> Makai Markswoman's Battledress - AddItem(m2f, f2m, 16118, 16123); // Makai Marksman's Fingerless Gloves <-> Makai Markswoman's Fingerless Gloves - AddItem(m2f, f2m, 16119, 16124); // Makai Marksman's Slops <-> Makai Markswoman's Quartertights - AddItem(m2f, f2m, 16120, 16125); // Makai Marksman's Boots <-> Makai Markswoman's Longboots - AddItem(m2f, f2m, 16126, 16131); // Makai Sun Guide's Circlet <-> Makai Moon Guide's Circlet - AddItem(m2f, f2m, 16127, 16132); // Makai Sun Guide's Oilskin <-> Makai Moon Guide's Gown - AddItem(m2f, f2m, 16128, 16133); // Makai Sun Guide's Fingerless Gloves <-> Makai Moon Guide's Fingerless Gloves - AddItem(m2f, f2m, 16129, 16134); // Makai Sun Guide's Slops <-> Makai Moon Guide's Quartertights - AddItem(m2f, f2m, 16130, 16135); // Makai Sun Guide's Boots <-> Makai Moon Guide's Longboots - AddItem(m2f, f2m, 16136, 16141); // Makai Priest's Coronet <-> Makai Priestess's Headdress - AddItem(m2f, f2m, 16137, 16142); // Makai Priest's Doublet Robe <-> Makai Priestess's Jerkin - AddItem(m2f, f2m, 16138, 16143); // Makai Priest's Fingerless Gloves <-> Makai Priestess's Fingerless Gloves - AddItem(m2f, f2m, 16139, 16144); // Makai Priest's Slops <-> Makai Priestess's Skirt - AddItem(m2f, f2m, 16140, 16145); // Makai Priest's Boots <-> Makai Priestess's Longboots - AddItem(m2f, f2m, 16588, 16592); // Far Eastern Gentleman's Hat <-> Far Eastern Beauty's Hairpin - AddItem(m2f, f2m, 16589, 16593); // Far Eastern Gentleman's Robe <-> Far Eastern Beauty's Robe - AddItem(m2f, f2m, 16590, 16594); // Far Eastern Gentleman's Haidate <-> Far Eastern Beauty's Koshita - AddItem(m2f, f2m, 16591, 16595); // Far Eastern Gentleman's Boots <-> Far Eastern Beauty's Boots - AddItem(m2f, f2m, 17204, 17209); // Common Makai Mauler's Facemask <-> Common Makai Manhandler's Facemask - AddItem(m2f, f2m, 17205, 17210); // Common Makai Mauler's Oilskin <-> Common Makai Manhandler's Jerkin - AddItem(m2f, f2m, 17206, 17211); // Common Makai Mauler's Fingerless Gloves <-> Common Makai Manhandler's Fingerless Glove - AddItem(m2f, f2m, 17207, 17212); // Common Makai Mauler's Leggings <-> Common Makai Manhandler's Quartertights - AddItem(m2f, f2m, 17208, 17213); // Common Makai Mauler's Boots <-> Common Makai Manhandler's Longboots - AddItem(m2f, f2m, 17214, 17219); // Common Makai Marksman's Eyepatch <-> Common Makai Markswoman's Ribbon - AddItem(m2f, f2m, 17215, 17220); // Common Makai Marksman's Battlegarb <-> Common Makai Markswoman's Battledress - AddItem(m2f, f2m, 17216, 17221); // Common Makai Marksman's Fingerless Gloves <-> Common Makai Markswoman's Fingerless Glove - AddItem(m2f, f2m, 17217, 17222); // Common Makai Marksman's Slops <-> Common Makai Markswoman's Quartertights - AddItem(m2f, f2m, 17218, 17223); // Common Makai Marksman's Boots <-> Common Makai Markswoman's Longboots - AddItem(m2f, f2m, 17224, 17229); // Common Makai Sun Guide's Circlet <-> Common Makai Moon Guide's Circlet - AddItem(m2f, f2m, 17225, 17230); // Common Makai Sun Guide's Oilskin <-> Common Makai Moon Guide's Gown - AddItem(m2f, f2m, 17226, 17231); // Common Makai Sun Guide's Fingerless Gloves <-> Common Makai Moon Guide's Fingerless Glove - AddItem(m2f, f2m, 17227, 17232); // Common Makai Sun Guide's Slops <-> Common Makai Moon Guide's Quartertights - AddItem(m2f, f2m, 17228, 17233); // Common Makai Sun Guide's Boots <-> Common Makai Moon Guide's Longboots - AddItem(m2f, f2m, 17234, 17239); // Common Makai Priest's Coronet <-> Common Makai Priestess's Headdress - AddItem(m2f, f2m, 17235, 17240); // Common Makai Priest's Doublet Robe <-> Common Makai Priestess's Jerkin - AddItem(m2f, f2m, 17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves - AddItem(m2f, f2m, 17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt - AddItem(m2f, f2m, 17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots - AddItem(m2f, f2m, 20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady - AddItem(m2f, f2m, 20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi - AddItem(m2f, f2m, 20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves - AddItem(m2f, f2m, 20482, 20487); // Nezha Lord's Slops <-> Nezha Lady's Slops - AddItem(m2f, f2m, 20483, 20488); // Nezha Lord's Boots <-> Nezha Lady's Kneeboots - AddItem(m2f, f2m, 22367, 22372); // Faerie Tale Prince's Circlet <-> Faerie Tale Princess's Tiara - AddItem(m2f, f2m, 22368, 22373); // Faerie Tale Prince's Vest <-> Faerie Tale Princess's Dress - AddItem(m2f, f2m, 22369, 22374); // Faerie Tale Prince's Gloves <-> Faerie Tale Princess's Gloves - AddItem(m2f, f2m, 22370, 22375); // Faerie Tale Prince's Slops <-> Faerie Tale Princess's Long Skirt - AddItem(m2f, f2m, 22371, 22376); // Faerie Tale Prince's Boots <-> Faerie Tale Princess's Heels - AddItem(m2f, f2m, 24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon - AddItem(m2f, f2m, 24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama - AddItem(m2f, f2m, 24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots - AddItem(m2f, f2m, 28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat - AddItem(m2f, f2m, 28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat - AddItem(m2f, f2m, 28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves - AddItem(m2f, f2m, 28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt - AddItem(m2f, f2m, 28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots - AddItem(m2f, f2m, 36336, 36337); // Omega-M Attire <-> Omega-F Attire - AddItem(m2f, f2m, 36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings - AddItem(m2f, f2m, 37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon - AddItem(m2f, f2m, 37443, 37448); // Makai Vanguard's Battlegarb <-> Makai Vanbreaker's Battledress - AddItem(m2f, f2m, 37444, 37449); // Makai Vanguard's Fingerless Gloves <-> Makai Vanbreaker's Fingerless Gloves - AddItem(m2f, f2m, 37445, 37450); // Makai Vanguard's Leggings <-> Makai Vanbreaker's Quartertights - AddItem(m2f, f2m, 37446, 37451); // Makai Vanguard's Boots <-> Makai Vanbreaker's Longboots - AddItem(m2f, f2m, 37452, 37457); // Makai Harbinger's Facemask <-> Makai Harrower's Facemask - AddItem(m2f, f2m, 37453, 37458); // Makai Harbinger's Battlegarb <-> Makai Harrower's Jerkin - AddItem(m2f, f2m, 37454, 37459); // Makai Harbinger's Fingerless Gloves <-> Makai Harrower's Fingerless Gloves - AddItem(m2f, f2m, 37455, 37460); // Makai Harbinger's Leggings <-> Makai Harrower's Quartertights - AddItem(m2f, f2m, 37456, 37461); // Makai Harbinger's Boots <-> Makai Harrower's Longboots - AddItem(m2f, f2m, 37462, 37467); // Common Makai Vanguard's Monocle <-> Common Makai Vanbreaker's Ribbon - AddItem(m2f, f2m, 37463, 37468); // Common Makai Vanguard's Battlegarb <-> Common Makai Vanbreaker's Battledress - AddItem(m2f, f2m, 37464, 37469); // Common Makai Vanguard's Fingerless Gloves <-> Common Makai Vanbreaker's Fingerless Gloves - AddItem(m2f, f2m, 37465, 37470); // Common Makai Vanguard's Leggings <-> Common Makai Vanbreaker's Quartertights - AddItem(m2f, f2m, 37466, 37471); // Common Makai Vanguard's Boots <-> Common Makai Vanbreaker's Longboots - AddItem(m2f, f2m, 37472, 37477); // Common Makai Harbinger's Facemask <-> Common Makai Harrower's Facemask - AddItem(m2f, f2m, 37473, 37478); // Common Makai Harbinger's Battlegarb <-> Common Makai Harrower's Jerkin - AddItem(m2f, f2m, 37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves - AddItem(m2f, f2m, 37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights - AddItem(m2f, f2m, 37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots - AddItem(m2f, f2m, 13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica - AddItem(m2f, f2m, 13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves - AddItem(m2f, f2m, 13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses - AddItem(m2f, f2m, 13695, 13692); // Scion Thief's Armored Caligae <-> Scion Conjurer's Pattens - AddItem(m2f, f2m, 13326, 30063); // Scion Thaumaturge's Robe <-> Scion Sorceress's Headdress - AddItem(m2f, f2m, 13696, 30062); // Scion Thaumaturge's Monocle <-> Scion Sorceress's Robe - AddItem(m2f, f2m, 13697, 30064); // Scion Thaumaturge's Gauntlets <-> Scion Sorceress's Shadowtalons - AddItem(m2f, f2m, 13698, 10035, true, false); // Scion Thaumaturge's Gaskins -> The Emperor's New Breeches - AddItem(m2f, f2m, 13699, 30065); // Scion Thaumaturge's Moccasins <-> Scion Sorceress's High Boots - AddItem(m2f, f2m, 13327, 15942); // Scion Chronocler's Cowl <-> Scion Healer's Robe - AddItem(m2f, f2m, 13700, 10034, true, false); // Scion Chronocler's Ringbands -> The Emperor's New Gloves - AddItem(m2f, f2m, 13701, 15943); // Scion Chronocler's Tights <-> Scion Healer's Halftights - AddItem(m2f, f2m, 13702, 15944); // Scion Chronocler's Caligae <-> Scion Healer's Highboots - AddItem(m2f, f2m, 14861, 13324); // Head Engineer's Goggles <-> Scion Striker's Visor - AddItem(m2f, f2m, 14862, 13325); // Head Engineer's Attire <-> Scion Striker's Attire - AddItem(m2f, f2m, 15938, 33751); // Scion Rogue's Jacket <-> Oracle Top - AddItem(m2f, f2m, 15939, 10034, true, false); // Scion Rogue's Armguards -> The Emperor's New Gloves - AddItem(m2f, f2m, 15940, 33752); // Scion Rogue's Gaskins <-> Oracle Leggings - AddItem(m2f, f2m, 15941, 33753); // Scion Rogue's Boots <-> Oracle Pantalettes - AddItem(m2f, f2m, 16042, 16046); // Abes Jacket <-> High Summoner's Dress - AddItem(m2f, f2m, 16043, 16047); // Abes Gloves <-> High Summoner's Armlets - AddItem(m2f, f2m, 16044, 10035, true, false); // Abes Halfslops -> The Emperor's New Breeches - AddItem(m2f, f2m, 16045, 16048); // Abes Boots <-> High Summoner's Boots - AddItem(m2f, f2m, 17473, 28553); // Lord Commander's Coat <-> Majestic Dress - AddItem(m2f, f2m, 17474, 28554); // Lord Commander's Gloves <-> Majestic Wristdresses - AddItem(m2f, f2m, 10036, 28555, false); // Emperor's New Boots <- Majestic Boots - AddItem(m2f, f2m, 21021, 21026); // Werewolf Feet <-> Werewolf Legs - AddItem(m2f, f2m, 22452, 20633); // Cracked Manderville Monocle <-> Blackbosom Hat - AddItem(m2f, f2m, 22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress - AddItem(m2f, f2m, 22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves - AddItem(m2f, f2m, 22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches - AddItem(m2f, f2m, 22456, 20636); // Scuffed Manderville Gaiters <-> Blackbosom Boots - AddItem(m2f, f2m, 23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket - AddItem(m2f, f2m, 23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves - AddItem(m2f, f2m, 23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes - AddItem(m2f, f2m, 23016, 21305); // Doman Liege's Kyahan <-> Scion Liberator's Sabatons - AddItem(m2f, f2m, 09293, 21306, false); // The Emperor's New Earrings <- Scion Liberator's Earrings - AddItem(m2f, f2m, 24158, 23008, true, false); // Leal Samurai's Kasa -> Eastern Socialite's Hat - AddItem(m2f, f2m, 24159, 23009, true, false); // Leal Samurai's Dogi -> Eastern Socialite's Cheongsam - AddItem(m2f, f2m, 24160, 23010, true, false); // Leal Samurai's Tekko -> Eastern Socialite's Gloves - AddItem(m2f, f2m, 24161, 23011, true, false); // Leal Samurai's Tsutsu-hakama -> Eastern Socialite's Skirt - AddItem(m2f, f2m, 24162, 23012, true, false); // Leal Samurai's Geta -> Eastern Socialite's Boots - AddItem(m2f, f2m, 02966, 13321, false); // Reindeer Suit <- Antecedent's Attire - AddItem(m2f, f2m, 15479, 36843, false); // Swine Body <- Lyse's Leadership Attire - AddItem(m2f, f2m, 21941, 24999, false); // Ala Mhigan Gown <- Gown of Light - AddItem(m2f, f2m, 30757, 25000, false); // Southern Seas Skirt <- Skirt of Light - AddItem(m2f, f2m, 36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood - AddItem(m2f, f2m, 36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat - AddItem(m2f, f2m, 36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes - AddItem(m2f, f2m, 32393, 39302, false); // Edenmete Gown of Casting <- Gaia's Attire - } - - // The racial starter sets are available for all 4 slots each, - // but have no associated accessories or hats. - private static readonly uint[] RaceGenderGroup = - { - 0x020054, - 0x020055, - 0x020056, - 0x020057, - 0x02005C, - 0x02005D, - 0x020058, - 0x020059, - 0x02005A, - 0x02005B, - 0x020101, - 0x020102, - 0x010255, - uint.MaxValue, // TODO: Female Hrothgar - 0x0102E8, - 0x010245, - }; - // @Formatter:on -} diff --git a/Penumbra.GameData/Data/StainData.cs b/Penumbra.GameData/Data/StainData.cs deleted file mode 100644 index 0e602307..00000000 --- a/Penumbra.GameData/Data/StainData.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Data; -using Dalamud.Plugin; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Data; - -public sealed class StainData : DataSharer, IReadOnlyDictionary -{ - public readonly IReadOnlyDictionary Data; - - public StainData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) - : base(pluginInterface, language, 2) - { - Data = TryCatchData("Stains", () => CreateStainData(dataManager)); - } - - protected override void DisposeInternal() - => DisposeTag("Stains"); - - private IReadOnlyDictionary CreateStainData(DataManager dataManager) - { - var stainSheet = dataManager.GetExcelSheet(Language)!; - return stainSheet.Where(s => s.Color != 0 && s.Name.RawData.Length > 0) - .ToDictionary(s => (byte)s.RowId, s => - { - var stain = new Stain(s); - return (stain.Name, stain.RgbaColor, stain.Gloss); - }); - } - - public IEnumerator> GetEnumerator() - => Data.Select(kvp - => new KeyValuePair(new StainId(kvp.Key), new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss))) - .GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => Data.Count; - - public bool ContainsKey(StainId key) - => Data.ContainsKey(key.Value); - - public bool TryGetValue(StainId key, out Stain value) - { - if (!Data.TryGetValue(key.Value, out var data)) - { - value = default; - return false; - } - - value = new Stain(data.Name, data.Dye, key.Value, data.Gloss); - return true; - } - - public Stain this[StainId key] - => TryGetValue(key, out var data) ? data : throw new ArgumentOutOfRangeException(nameof(key)); - - public IEnumerable Keys - => Data.Keys.Select(k => new StainId(k)); - - public IEnumerable Values - => Data.Select(kvp => new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss)); -} diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs deleted file mode 100644 index e4566769..00000000 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Plugin; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using PseudoEquipItem = System.ValueTuple; - -namespace Penumbra.GameData.Data; - -internal sealed class WeaponIdentificationList : KeyList -{ - private const string Tag = "WeaponIdentification"; - private const int Version = 2; - - public WeaponIdentificationList(DalamudPluginInterface pi, ClientLanguage language, ItemData data) - : base(pi, Tag, language, Version, CreateWeaponList(data)) - { } - - public IEnumerable Between(SetId modelId) - => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e); - - public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) - { - if (type == 0) - return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)).Select(e => (EquipItem)e); - if (variant == 0) - return Between(ToKey(modelId, type, 0), ToKey(modelId, type, 0xFF)).Select(e => (EquipItem)e); - - return Between(ToKey(modelId, type, variant), ToKey(modelId, type, variant)).Select(e => (EquipItem)e); - } - - public void Dispose(DalamudPluginInterface pi, ClientLanguage language) - => DataSharer.DisposeTag(pi, Tag, language, Version); - - public static ulong ToKey(SetId modelId, WeaponType type, byte variant) - => ((ulong)modelId << 32) | ((ulong)type << 16) | variant; - - public static ulong ToKey(EquipItem i) - => ToKey(i.ModelId, i.WeaponType, i.Variant); - - protected override IEnumerable ToKeys(PseudoEquipItem data) - { - yield return ToKey(data); - } - - protected override bool ValidKey(ulong key) - => key != 0; - - protected override int ValueKeySelector(PseudoEquipItem data) - => (int)data.Item2; - - private static IEnumerable CreateWeaponList(ItemData data) - => data.Where(kvp => !kvp.Key.IsEquipment() && !kvp.Key.IsAccessory()) - .SelectMany(kvp => kvp.Value) - .Select(i => (PseudoEquipItem)i); -} diff --git a/Penumbra.GameData/Enums/BodySlot.cs b/Penumbra.GameData/Enums/BodySlot.cs deleted file mode 100644 index 92b4c6ce..00000000 --- a/Penumbra.GameData/Enums/BodySlot.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace Penumbra.GameData.Enums; - -public enum BodySlot : byte -{ - Unknown, - Hair, - Face, - Tail, - Body, - Zear, -} - -public static class BodySlotEnumExtension -{ - public static string ToSuffix( this BodySlot value ) - => value switch - { - BodySlot.Zear => "zear", - BodySlot.Face => "face", - BodySlot.Hair => "hair", - BodySlot.Body => "body", - BodySlot.Tail => "tail", - _ => throw new InvalidEnumArgumentException(), - }; - - public static char ToAbbreviation(this BodySlot value) - => value switch - { - BodySlot.Hair => 'h', - BodySlot.Face => 'f', - BodySlot.Tail => 't', - BodySlot.Body => 'b', - BodySlot.Zear => 'z', - _ => throw new InvalidEnumArgumentException(), - }; - - public static CustomizationType ToCustomizationType(this BodySlot value) - => value switch - { - BodySlot.Hair => CustomizationType.Hair, - BodySlot.Face => CustomizationType.Face, - BodySlot.Tail => CustomizationType.Tail, - BodySlot.Body => CustomizationType.Body, - BodySlot.Zear => CustomizationType.Zear, - _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) - }; -} - -public static partial class Names -{ - public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() - { - { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, - { BodySlot.Face.ToSuffix(), BodySlot.Face }, - { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, - { BodySlot.Body.ToSuffix(), BodySlot.Body }, - { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs deleted file mode 100644 index f2b531d6..00000000 --- a/Penumbra.GameData/Enums/ChangedItemExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Dalamud.Data; -using Lumina.Excel.GeneratedSheets; -using Penumbra.Api.Enums; -using Action = Lumina.Excel.GeneratedSheets.Action; - -namespace Penumbra.GameData.Enums; - -public static class ChangedItemExtensions -{ - public static (ChangedItemType, uint) ChangedItemToTypeAndId(object? item) - { - return item switch - { - null => (ChangedItemType.None, 0), - Item i => (ChangedItemType.Item, i.RowId), - Action a => (ChangedItemType.Action, a.RowId), - _ => (ChangedItemType.Customization, 0), - }; - } - - public static object? GetObject(this ChangedItemType type, DataManager manager, uint id) - { - return type switch - { - ChangedItemType.None => null, - ChangedItemType.Item => manager.GetExcelSheet()?.GetRow(id), - ChangedItemType.Action => manager.GetExcelSheet()?.GetRow(id), - ChangedItemType.Customization => null, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - } -} diff --git a/Penumbra.GameData/Enums/CustomizationType.cs b/Penumbra.GameData/Enums/CustomizationType.cs deleted file mode 100644 index 60cf23dd..00000000 --- a/Penumbra.GameData/Enums/CustomizationType.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; - -namespace Penumbra.GameData.Enums -{ - public enum CustomizationType : byte - { - Unknown, - Body, - Tail, - Face, - Iris, - Accessory, - Hair, - Zear, - DecalFace, - DecalEquip, - Skin, - Etc, - } - - public static class CustomizationTypeEnumExtension - { - public static string ToSuffix( this CustomizationType value ) - { - return value switch - { - CustomizationType.Body => "top", - CustomizationType.Face => "fac", - CustomizationType.Iris => "iri", - CustomizationType.Accessory => "acc", - CustomizationType.Hair => "hir", - CustomizationType.Tail => "til", - CustomizationType.Zear => "zer", - CustomizationType.Etc => "etc", - _ => throw new InvalidEnumArgumentException(), - }; - } - } - - public static partial class Names - { - public static readonly Dictionary< string, CustomizationType > SuffixToCustomizationType = new() - { - { CustomizationType.Body.ToSuffix(), CustomizationType.Body }, - { CustomizationType.Face.ToSuffix(), CustomizationType.Face }, - { CustomizationType.Iris.ToSuffix(), CustomizationType.Iris }, - { CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory }, - { CustomizationType.Hair.ToSuffix(), CustomizationType.Hair }, - { CustomizationType.Tail.ToSuffix(), CustomizationType.Tail }, - { CustomizationType.Zear.ToSuffix(), CustomizationType.Zear }, - { CustomizationType.Etc.ToSuffix(), CustomizationType.Etc }, - }; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs deleted file mode 100644 index 27e5a92c..00000000 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.GameData.Enums; - -public enum EquipSlot : byte -{ - Unknown = 0, - MainHand = 1, - OffHand = 2, - Head = 3, - Body = 4, - Hands = 5, - Belt = 6, - Legs = 7, - Feet = 8, - Ears = 9, - Neck = 10, - Wrists = 11, - RFinger = 12, - BothHand = 13, - LFinger = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game. - HeadBody = 15, - BodyHandsLegsFeet = 16, - SoulCrystal = 17, - LegsFeet = 18, - FullBody = 19, - BodyHands = 20, - BodyLegsFeet = 21, - ChestHands = 22, - Nothing = 23, - All = 24, // Not officially existing -} - -public static class EquipSlotExtensions -{ - public static EquipSlot ToEquipSlot(this uint value) - => value switch - { - 0 => EquipSlot.Head, - 1 => EquipSlot.Body, - 2 => EquipSlot.Hands, - 3 => EquipSlot.Legs, - 4 => EquipSlot.Feet, - 5 => EquipSlot.Ears, - 6 => EquipSlot.Neck, - 7 => EquipSlot.Wrists, - 8 => EquipSlot.RFinger, - 9 => EquipSlot.LFinger, - 10 => EquipSlot.MainHand, - 11 => EquipSlot.OffHand, - _ => EquipSlot.Unknown, - }; - - public static uint ToIndex(this EquipSlot slot) - => slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 1, - EquipSlot.Hands => 2, - EquipSlot.Legs => 3, - EquipSlot.Feet => 4, - EquipSlot.Ears => 5, - EquipSlot.Neck => 6, - EquipSlot.Wrists => 7, - EquipSlot.RFinger => 8, - EquipSlot.LFinger => 9, - EquipSlot.MainHand => 10, - EquipSlot.OffHand => 11, - _ => uint.MaxValue, - }; - - public static string ToSuffix(this EquipSlot value) - { - return value switch - { - EquipSlot.Head => "met", - EquipSlot.Hands => "glv", - EquipSlot.Legs => "dwn", - EquipSlot.Feet => "sho", - EquipSlot.Body => "top", - EquipSlot.Ears => "ear", - EquipSlot.Neck => "nek", - EquipSlot.RFinger => "rir", - EquipSlot.LFinger => "ril", - EquipSlot.Wrists => "wrs", - _ => "unk", - }; - } - - public static EquipSlot ToSlot(this EquipSlot value) - { - return value switch - { - EquipSlot.MainHand => EquipSlot.MainHand, - EquipSlot.OffHand => EquipSlot.OffHand, - EquipSlot.Head => EquipSlot.Head, - EquipSlot.Body => EquipSlot.Body, - EquipSlot.Hands => EquipSlot.Hands, - EquipSlot.Belt => EquipSlot.Belt, - EquipSlot.Legs => EquipSlot.Legs, - EquipSlot.Feet => EquipSlot.Feet, - EquipSlot.Ears => EquipSlot.Ears, - EquipSlot.Neck => EquipSlot.Neck, - EquipSlot.Wrists => EquipSlot.Wrists, - EquipSlot.RFinger => EquipSlot.RFinger, - EquipSlot.BothHand => EquipSlot.MainHand, - EquipSlot.LFinger => EquipSlot.RFinger, - EquipSlot.HeadBody => EquipSlot.Body, - EquipSlot.BodyHandsLegsFeet => EquipSlot.Body, - EquipSlot.SoulCrystal => EquipSlot.SoulCrystal, - EquipSlot.LegsFeet => EquipSlot.Legs, - EquipSlot.FullBody => EquipSlot.Body, - EquipSlot.BodyHands => EquipSlot.Body, - EquipSlot.BodyLegsFeet => EquipSlot.Body, - EquipSlot.ChestHands => EquipSlot.Body, - _ => EquipSlot.Unknown, - }; - } - - public static string ToName(this EquipSlot value) - { - return value switch - { - EquipSlot.Head => "Head", - EquipSlot.Hands => "Hands", - EquipSlot.Legs => "Legs", - EquipSlot.Feet => "Feet", - EquipSlot.Body => "Body", - EquipSlot.Ears => "Earrings", - EquipSlot.Neck => "Necklace", - EquipSlot.RFinger => "Right Ring", - EquipSlot.LFinger => "Left Ring", - EquipSlot.Wrists => "Bracelets", - EquipSlot.MainHand => "Primary Weapon", - EquipSlot.OffHand => "Secondary Weapon", - EquipSlot.Belt => "Belt", - EquipSlot.BothHand => "Primary Weapon", - EquipSlot.HeadBody => "Head and Body", - EquipSlot.BodyHandsLegsFeet => "Costume", - EquipSlot.SoulCrystal => "Soul Crystal", - EquipSlot.LegsFeet => "Bottom", - EquipSlot.FullBody => "Costume", - EquipSlot.BodyHands => "Top", - EquipSlot.BodyLegsFeet => "Costume", - EquipSlot.All => "Costume", - _ => "Unknown", - }; - } - - public static bool IsEquipment(this EquipSlot value) - { - return value switch - { - EquipSlot.Head => true, - EquipSlot.Hands => true, - EquipSlot.Legs => true, - EquipSlot.Feet => true, - EquipSlot.Body => true, - _ => false, - }; - } - - public static bool IsAccessory(this EquipSlot value) - { - return value switch - { - EquipSlot.Ears => true, - EquipSlot.Neck => true, - EquipSlot.RFinger => true, - EquipSlot.LFinger => true, - EquipSlot.Wrists => true, - _ => false, - }; - } - - public static bool IsEquipmentPiece(this EquipSlot value) - { - return value switch - { - // Accessories - EquipSlot.RFinger => true, - EquipSlot.Wrists => true, - EquipSlot.Ears => true, - EquipSlot.Neck => true, - // Equipment - EquipSlot.Head => true, - EquipSlot.Body => true, - EquipSlot.Hands => true, - EquipSlot.Legs => true, - EquipSlot.Feet => true, - EquipSlot.BodyHands => true, - EquipSlot.BodyHandsLegsFeet => true, - EquipSlot.BodyLegsFeet => true, - EquipSlot.FullBody => true, - EquipSlot.HeadBody => true, - EquipSlot.LegsFeet => true, - EquipSlot.ChestHands => true, - _ => false, - }; - } - - public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues().Where(e => e.IsEquipment()).ToArray(); - public static readonly EquipSlot[] AccessorySlots = Enum.GetValues().Where(e => e.IsAccessory()).ToArray(); - public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat(AccessorySlots).ToArray(); - - public static readonly EquipSlot[] WeaponSlots = - { - EquipSlot.MainHand, - EquipSlot.OffHand, - }; - - public static readonly EquipSlot[] FullSlots = WeaponSlots.Concat(EqdpSlots).ToArray(); -} - -public static partial class Names -{ - public static readonly Dictionary SuffixToEquipSlot = new() - { - { EquipSlot.Head.ToSuffix(), EquipSlot.Head }, - { EquipSlot.Hands.ToSuffix(), EquipSlot.Hands }, - { EquipSlot.Legs.ToSuffix(), EquipSlot.Legs }, - { EquipSlot.Feet.ToSuffix(), EquipSlot.Feet }, - { EquipSlot.Body.ToSuffix(), EquipSlot.Body }, - { EquipSlot.Ears.ToSuffix(), EquipSlot.Ears }, - { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, - { EquipSlot.RFinger.ToSuffix(), EquipSlot.RFinger }, - { EquipSlot.LFinger.ToSuffix(), EquipSlot.LFinger }, - { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists }, - }; -} diff --git a/Penumbra.GameData/Enums/FileType.cs b/Penumbra.GameData/Enums/FileType.cs deleted file mode 100644 index 14c077b8..00000000 --- a/Penumbra.GameData/Enums/FileType.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace Penumbra.GameData.Enums; - -public enum FileType : byte -{ - Unknown, - Sound, - Imc, - Vfx, - Animation, - Pap, - MetaInfo, - Material, - Texture, - Model, - Shader, - Font, - Environment, -} - -public static partial class Names -{ - public static readonly Dictionary< string, FileType > ExtensionToFileType = new() - { - { ".mdl", FileType.Model }, - { ".tex", FileType.Texture }, - { ".mtrl", FileType.Material }, - { ".atex", FileType.Animation }, - { ".avfx", FileType.Vfx }, - { ".scd", FileType.Sound }, - { ".imc", FileType.Imc }, - { ".pap", FileType.Pap }, - { ".eqp", FileType.MetaInfo }, - { ".eqdp", FileType.MetaInfo }, - { ".est", FileType.MetaInfo }, - { ".exd", FileType.MetaInfo }, - { ".exh", FileType.MetaInfo }, - { ".shpk", FileType.Shader }, - { ".shcd", FileType.Shader }, - { ".fdt", FileType.Font }, - { ".envb", FileType.Environment }, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs deleted file mode 100644 index 7d7ae512..00000000 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ /dev/null @@ -1,450 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lumina.Excel.GeneratedSheets; - -namespace Penumbra.GameData.Enums; - -public enum FullEquipType : byte -{ - Unknown, - - Head, - Body, - Hands, - Legs, - Feet, - - Ears, - Neck, - Wrists, - Finger, - - Fists, // PGL, MNK - FistsOff, - Sword, // GLA, PLD Main - Axe, // MRD, WAR - Bow, // ARC, BRD - BowOff, - Lance, // LNC, DRG, - Staff, // THM, BLM, CNJ, WHM - Wand, // THM, BLM, CNJ, WHM Main - Book, // ACN, SMN, SCH - Daggers, // ROG, NIN - DaggersOff, - Broadsword, // DRK, - Gun, // MCH, - GunOff, - Orrery, // AST, - OrreryOff, - Katana, // SAM - KatanaOff, - Rapier, // RDM - RapierOff, - Cane, // BLU - Gunblade, // GNB, - Glaives, // DNC, - GlaivesOff, - Scythe, // RPR, - Nouliths, // SGE - Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off - - Saw, // CRP - CrossPeinHammer, // BSM - RaisingHammer, // ARM - LapidaryHammer, // GSM - Knife, // LTW - Needle, // WVR - Alembic, // ALC - Frypan, // CUL - Pickaxe, // MIN - Hatchet, // BTN - FishingRod, // FSH - - ClawHammer, // CRP Off - File, // BSM Off - Pliers, // ARM Off - GrindingWheel, // GSM Off - Awl, // LTW Off - SpinningWheel, // WVR Off - Mortar, // ALC Off - CulinaryKnife, // CUL Off - Sledgehammer, // MIN Off - GardenScythe, // BTN Off - Gig, // FSH Off -} - -public static class FullEquipTypeExtensions -{ - internal static FullEquipType ToEquipType(this Item item) - { - var slot = (EquipSlot)item.EquipSlotCategory.Row; - var weapon = (WeaponCategory)item.ItemUICategory.Row; - return slot.ToEquipType(weapon); - } - - public static bool IsWeapon(this FullEquipType type) - => type switch - { - FullEquipType.Fists => true, - FullEquipType.Sword => true, - FullEquipType.Axe => true, - FullEquipType.Bow => true, - FullEquipType.Lance => true, - FullEquipType.Staff => true, - FullEquipType.Wand => true, - FullEquipType.Book => true, - FullEquipType.Daggers => true, - FullEquipType.Broadsword => true, - FullEquipType.Gun => true, - FullEquipType.Orrery => true, - FullEquipType.Katana => true, - FullEquipType.Rapier => true, - FullEquipType.Cane => true, - FullEquipType.Gunblade => true, - FullEquipType.Glaives => true, - FullEquipType.Scythe => true, - FullEquipType.Nouliths => true, - FullEquipType.Shield => true, - _ => false, - }; - - public static bool IsTool(this FullEquipType type) - => type switch - { - FullEquipType.Saw => true, - FullEquipType.CrossPeinHammer => true, - FullEquipType.RaisingHammer => true, - FullEquipType.LapidaryHammer => true, - FullEquipType.Knife => true, - FullEquipType.Needle => true, - FullEquipType.Alembic => true, - FullEquipType.Frypan => true, - FullEquipType.Pickaxe => true, - FullEquipType.Hatchet => true, - FullEquipType.FishingRod => true, - FullEquipType.ClawHammer => true, - FullEquipType.File => true, - FullEquipType.Pliers => true, - FullEquipType.GrindingWheel => true, - FullEquipType.Awl => true, - FullEquipType.SpinningWheel => true, - FullEquipType.Mortar => true, - FullEquipType.CulinaryKnife => true, - FullEquipType.Sledgehammer => true, - FullEquipType.GardenScythe => true, - FullEquipType.Gig => true, - _ => false, - }; - - public static bool IsEquipment(this FullEquipType type) - => type switch - { - FullEquipType.Head => true, - FullEquipType.Body => true, - FullEquipType.Hands => true, - FullEquipType.Legs => true, - FullEquipType.Feet => true, - _ => false, - }; - - public static bool IsAccessory(this FullEquipType type) - => type switch - { - FullEquipType.Ears => true, - FullEquipType.Neck => true, - FullEquipType.Wrists => true, - FullEquipType.Finger => true, - _ => false, - }; - - public static string ToName(this FullEquipType type) - => type switch - { - FullEquipType.Head => EquipSlot.Head.ToName(), - FullEquipType.Body => EquipSlot.Body.ToName(), - FullEquipType.Hands => EquipSlot.Hands.ToName(), - FullEquipType.Legs => EquipSlot.Legs.ToName(), - FullEquipType.Feet => EquipSlot.Feet.ToName(), - FullEquipType.Ears => EquipSlot.Ears.ToName(), - FullEquipType.Neck => EquipSlot.Neck.ToName(), - FullEquipType.Wrists => EquipSlot.Wrists.ToName(), - FullEquipType.Finger => "Ring", - FullEquipType.Fists => "Fist Weapon", - FullEquipType.FistsOff => "Fist Weapon (Offhand)", - FullEquipType.Sword => "Sword", - FullEquipType.Axe => "Axe", - FullEquipType.Bow => "Bow", - FullEquipType.BowOff => "Quiver", - FullEquipType.Lance => "Lance", - FullEquipType.Staff => "Staff", - FullEquipType.Wand => "Mace", - FullEquipType.Book => "Book", - FullEquipType.Daggers => "Dagger", - FullEquipType.DaggersOff => "Dagger (Offhand)", - FullEquipType.Broadsword => "Broadsword", - FullEquipType.Gun => "Gun", - FullEquipType.GunOff => "Aetherotransformer", - FullEquipType.Orrery => "Orrery", - FullEquipType.OrreryOff => "Card Holder", - FullEquipType.Katana => "Katana", - FullEquipType.KatanaOff => "Sheathe", - FullEquipType.Rapier => "Rapier", - FullEquipType.RapierOff => "Focus", - FullEquipType.Cane => "Cane", - FullEquipType.Gunblade => "Gunblade", - FullEquipType.Glaives => "Glaive", - FullEquipType.GlaivesOff => "Glaive (Offhand)", - FullEquipType.Scythe => "Scythe", - FullEquipType.Nouliths => "Nouliths", - FullEquipType.Shield => "Shield", - FullEquipType.Saw => "Saw", - FullEquipType.CrossPeinHammer => "Cross Pein Hammer", - FullEquipType.RaisingHammer => "Raising Hammer", - FullEquipType.LapidaryHammer => "Lapidary Hammer", - FullEquipType.Knife => "Round Knife", - FullEquipType.Needle => "Needle", - FullEquipType.Alembic => "Alembic", - FullEquipType.Frypan => "Frypan", - FullEquipType.Pickaxe => "Pickaxe", - FullEquipType.Hatchet => "Hatchet", - FullEquipType.FishingRod => "Fishing Rod", - FullEquipType.ClawHammer => "Clawhammer", - FullEquipType.File => "File", - FullEquipType.Pliers => "Pliers", - FullEquipType.GrindingWheel => "Grinding Wheel", - FullEquipType.Awl => "Awl", - FullEquipType.SpinningWheel => "Spinning Wheel", - FullEquipType.Mortar => "Mortar", - FullEquipType.CulinaryKnife => "Culinary Knife", - FullEquipType.Sledgehammer => "Sledgehammer", - FullEquipType.GardenScythe => "Garden Scythe", - FullEquipType.Gig => "Gig", - _ => "Unknown", - }; - - public static EquipSlot ToSlot(this FullEquipType type) - => type switch - { - FullEquipType.Head => EquipSlot.Head, - FullEquipType.Body => EquipSlot.Body, - FullEquipType.Hands => EquipSlot.Hands, - FullEquipType.Legs => EquipSlot.Legs, - FullEquipType.Feet => EquipSlot.Feet, - FullEquipType.Ears => EquipSlot.Ears, - FullEquipType.Neck => EquipSlot.Neck, - FullEquipType.Wrists => EquipSlot.Wrists, - FullEquipType.Finger => EquipSlot.RFinger, - FullEquipType.Fists => EquipSlot.MainHand, - FullEquipType.FistsOff => EquipSlot.OffHand, - FullEquipType.Sword => EquipSlot.MainHand, - FullEquipType.Axe => EquipSlot.MainHand, - FullEquipType.Bow => EquipSlot.MainHand, - FullEquipType.BowOff => EquipSlot.OffHand, - FullEquipType.Lance => EquipSlot.MainHand, - FullEquipType.Staff => EquipSlot.MainHand, - FullEquipType.Wand => EquipSlot.MainHand, - FullEquipType.Book => EquipSlot.MainHand, - FullEquipType.Daggers => EquipSlot.MainHand, - FullEquipType.DaggersOff => EquipSlot.OffHand, - FullEquipType.Broadsword => EquipSlot.MainHand, - FullEquipType.Gun => EquipSlot.MainHand, - FullEquipType.GunOff => EquipSlot.OffHand, - FullEquipType.Orrery => EquipSlot.MainHand, - FullEquipType.OrreryOff => EquipSlot.OffHand, - FullEquipType.Katana => EquipSlot.MainHand, - FullEquipType.KatanaOff => EquipSlot.OffHand, - FullEquipType.Rapier => EquipSlot.MainHand, - FullEquipType.RapierOff => EquipSlot.OffHand, - FullEquipType.Cane => EquipSlot.MainHand, - FullEquipType.Gunblade => EquipSlot.MainHand, - FullEquipType.Glaives => EquipSlot.MainHand, - FullEquipType.GlaivesOff => EquipSlot.OffHand, - FullEquipType.Scythe => EquipSlot.MainHand, - FullEquipType.Nouliths => EquipSlot.MainHand, - FullEquipType.Shield => EquipSlot.OffHand, - FullEquipType.Saw => EquipSlot.MainHand, - FullEquipType.CrossPeinHammer => EquipSlot.MainHand, - FullEquipType.RaisingHammer => EquipSlot.MainHand, - FullEquipType.LapidaryHammer => EquipSlot.MainHand, - FullEquipType.Knife => EquipSlot.MainHand, - FullEquipType.Needle => EquipSlot.MainHand, - FullEquipType.Alembic => EquipSlot.MainHand, - FullEquipType.Frypan => EquipSlot.MainHand, - FullEquipType.Pickaxe => EquipSlot.MainHand, - FullEquipType.Hatchet => EquipSlot.MainHand, - FullEquipType.FishingRod => EquipSlot.MainHand, - FullEquipType.ClawHammer => EquipSlot.OffHand, - FullEquipType.File => EquipSlot.OffHand, - FullEquipType.Pliers => EquipSlot.OffHand, - FullEquipType.GrindingWheel => EquipSlot.OffHand, - FullEquipType.Awl => EquipSlot.OffHand, - FullEquipType.SpinningWheel => EquipSlot.OffHand, - FullEquipType.Mortar => EquipSlot.OffHand, - FullEquipType.CulinaryKnife => EquipSlot.OffHand, - FullEquipType.Sledgehammer => EquipSlot.OffHand, - FullEquipType.GardenScythe => EquipSlot.OffHand, - FullEquipType.Gig => EquipSlot.OffHand, - _ => EquipSlot.Unknown, - }; - - public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown, bool mainhand = true) - => slot switch - { - EquipSlot.Head => FullEquipType.Head, - EquipSlot.Body => FullEquipType.Body, - EquipSlot.Hands => FullEquipType.Hands, - EquipSlot.Legs => FullEquipType.Legs, - EquipSlot.Feet => FullEquipType.Feet, - EquipSlot.Ears => FullEquipType.Ears, - EquipSlot.Neck => FullEquipType.Neck, - EquipSlot.Wrists => FullEquipType.Wrists, - EquipSlot.RFinger => FullEquipType.Finger, - EquipSlot.LFinger => FullEquipType.Finger, - EquipSlot.HeadBody => FullEquipType.Body, - EquipSlot.BodyHandsLegsFeet => FullEquipType.Body, - EquipSlot.LegsFeet => FullEquipType.Legs, - EquipSlot.FullBody => FullEquipType.Body, - EquipSlot.BodyHands => FullEquipType.Body, - EquipSlot.BodyLegsFeet => FullEquipType.Body, - EquipSlot.ChestHands => FullEquipType.Body, - EquipSlot.MainHand => category.ToEquipType(mainhand), - EquipSlot.OffHand => category.ToEquipType(mainhand), - EquipSlot.BothHand => category.ToEquipType(mainhand), - _ => FullEquipType.Unknown, - }; - - public static FullEquipType ToEquipType(this WeaponCategory category, bool mainhand = true) - => category switch - { - WeaponCategory.Pugilist when mainhand => FullEquipType.Fists, - WeaponCategory.Pugilist => FullEquipType.FistsOff, - WeaponCategory.Gladiator => FullEquipType.Sword, - WeaponCategory.Marauder => FullEquipType.Axe, - WeaponCategory.Archer when mainhand => FullEquipType.Bow, - WeaponCategory.Archer => FullEquipType.BowOff, - WeaponCategory.Lancer => FullEquipType.Lance, - WeaponCategory.Thaumaturge1 => FullEquipType.Wand, - WeaponCategory.Thaumaturge2 => FullEquipType.Staff, - WeaponCategory.Conjurer1 => FullEquipType.Wand, - WeaponCategory.Conjurer2 => FullEquipType.Staff, - WeaponCategory.Arcanist => FullEquipType.Book, - WeaponCategory.Shield => FullEquipType.Shield, - WeaponCategory.CarpenterMain => FullEquipType.Saw, - WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, - WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, - WeaponCategory.BlacksmithOff => FullEquipType.File, - WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, - WeaponCategory.ArmorerOff => FullEquipType.Pliers, - WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, - WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, - WeaponCategory.LeatherworkerMain => FullEquipType.Knife, - WeaponCategory.LeatherworkerOff => FullEquipType.Awl, - WeaponCategory.WeaverMain => FullEquipType.Needle, - WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, - WeaponCategory.AlchemistMain => FullEquipType.Alembic, - WeaponCategory.AlchemistOff => FullEquipType.Mortar, - WeaponCategory.CulinarianMain => FullEquipType.Frypan, - WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, - WeaponCategory.MinerMain => FullEquipType.Pickaxe, - WeaponCategory.MinerOff => FullEquipType.Sledgehammer, - WeaponCategory.BotanistMain => FullEquipType.Hatchet, - WeaponCategory.BotanistOff => FullEquipType.GardenScythe, - WeaponCategory.FisherMain => FullEquipType.FishingRod, - WeaponCategory.FisherOff => FullEquipType.Gig, - WeaponCategory.Rogue when mainhand => FullEquipType.Daggers, - WeaponCategory.Rogue => FullEquipType.DaggersOff, - WeaponCategory.DarkKnight => FullEquipType.Broadsword, - WeaponCategory.Machinist when mainhand => FullEquipType.Gun, - WeaponCategory.Machinist => FullEquipType.GunOff, - WeaponCategory.Astrologian when mainhand => FullEquipType.Orrery, - WeaponCategory.Astrologian => FullEquipType.OrreryOff, - WeaponCategory.Samurai when mainhand => FullEquipType.Katana, - WeaponCategory.Samurai => FullEquipType.KatanaOff, - WeaponCategory.RedMage when mainhand => FullEquipType.Rapier, - WeaponCategory.RedMage => FullEquipType.RapierOff, - WeaponCategory.Scholar => FullEquipType.Book, - WeaponCategory.BlueMage => FullEquipType.Cane, - WeaponCategory.Gunbreaker => FullEquipType.Gunblade, - WeaponCategory.Dancer when mainhand => FullEquipType.Glaives, - WeaponCategory.Dancer => FullEquipType.GlaivesOff, - WeaponCategory.Reaper => FullEquipType.Scythe, - WeaponCategory.Sage => FullEquipType.Nouliths, - _ => FullEquipType.Unknown, - }; - - public static FullEquipType ValidOffhand(this FullEquipType type) - => type switch - { - FullEquipType.Fists => FullEquipType.FistsOff, - FullEquipType.Sword => FullEquipType.Shield, - FullEquipType.Wand => FullEquipType.Shield, - FullEquipType.Daggers => FullEquipType.DaggersOff, - FullEquipType.Gun => FullEquipType.GunOff, - FullEquipType.Orrery => FullEquipType.OrreryOff, - FullEquipType.Rapier => FullEquipType.RapierOff, - FullEquipType.Glaives => FullEquipType.GlaivesOff, - FullEquipType.Bow => FullEquipType.BowOff, - FullEquipType.Katana => FullEquipType.KatanaOff, - _ => FullEquipType.Unknown, - }; - - public static FullEquipType Offhand(this FullEquipType type) - => type switch - { - FullEquipType.Fists => FullEquipType.FistsOff, - FullEquipType.Sword => FullEquipType.Shield, - FullEquipType.Wand => FullEquipType.Shield, - FullEquipType.Daggers => FullEquipType.DaggersOff, - FullEquipType.Gun => FullEquipType.GunOff, - FullEquipType.Orrery => FullEquipType.OrreryOff, - FullEquipType.Rapier => FullEquipType.RapierOff, - FullEquipType.Glaives => FullEquipType.GlaivesOff, - FullEquipType.Bow => FullEquipType.BowOff, - FullEquipType.Katana => FullEquipType.KatanaOff, - FullEquipType.Saw => FullEquipType.ClawHammer, - FullEquipType.CrossPeinHammer => FullEquipType.File, - FullEquipType.RaisingHammer => FullEquipType.Pliers, - FullEquipType.LapidaryHammer => FullEquipType.GrindingWheel, - FullEquipType.Knife => FullEquipType.Awl, - FullEquipType.Needle => FullEquipType.SpinningWheel, - FullEquipType.Alembic => FullEquipType.Mortar, - FullEquipType.Frypan => FullEquipType.CulinaryKnife, - FullEquipType.Pickaxe => FullEquipType.Sledgehammer, - FullEquipType.Hatchet => FullEquipType.GardenScythe, - FullEquipType.FishingRod => FullEquipType.Gig, - _ => FullEquipType.Unknown, - }; - - internal static string OffhandTypeSuffix(this FullEquipType type) - => type switch - { - FullEquipType.FistsOff => " (Offhand)", - FullEquipType.DaggersOff => " (Offhand)", - FullEquipType.GunOff => " (Aetherotransformer)", - FullEquipType.OrreryOff => " (Card Holder)", - FullEquipType.RapierOff => " (Focus)", - FullEquipType.GlaivesOff => " (Offhand)", - FullEquipType.BowOff => " (Quiver)", - FullEquipType.KatanaOff => " (Sheathe)", - _ => string.Empty, - }; - - public static bool IsOffhandType(this FullEquipType type) - => type.OffhandTypeSuffix().Length > 0; - - public static readonly IReadOnlyList WeaponTypes - = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); - - public static readonly IReadOnlyList ToolTypes - = Enum.GetValues().Where(v => v.IsTool()).ToArray(); - - public static readonly IReadOnlyList EquipmentTypes - = Enum.GetValues().Where(v => v.IsEquipment()).ToArray(); - - public static readonly IReadOnlyList AccessoryTypes - = Enum.GetValues().Where(v => v.IsAccessory()).ToArray(); - - public static readonly IReadOnlyList OffhandTypes - = Enum.GetValues().Where(v => v.OffhandTypeSuffix().Length > 0).ToArray(); -} diff --git a/Penumbra.GameData/Enums/ModelTypeExtensions.cs b/Penumbra.GameData/Enums/ModelTypeExtensions.cs deleted file mode 100644 index e872aef8..00000000 --- a/Penumbra.GameData/Enums/ModelTypeExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.GameData.Enums; - -public static class ModelTypeExtensions -{ - public static string ToName(this CharacterBase.ModelType type) - => type switch - { - CharacterBase.ModelType.DemiHuman => "Demihuman", - CharacterBase.ModelType.Monster => "Monster", - CharacterBase.ModelType.Human => "Human", - CharacterBase.ModelType.Weapon => "Weapon", - _ => string.Empty, - }; - - public static CharacterBase.ModelType ToModelType(this ObjectType type) - => type switch - { - ObjectType.DemiHuman => CharacterBase.ModelType.DemiHuman, - ObjectType.Monster => CharacterBase.ModelType.Monster, - ObjectType.Character => CharacterBase.ModelType.Human, - ObjectType.Weapon => CharacterBase.ModelType.Weapon, - _ => 0, - }; -} diff --git a/Penumbra.GameData/Enums/ObjectType.cs b/Penumbra.GameData/Enums/ObjectType.cs deleted file mode 100644 index d081e6a6..00000000 --- a/Penumbra.GameData/Enums/ObjectType.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Penumbra.GameData.Enums; - -public enum ObjectType : byte -{ - Unknown, - Vfx, - DemiHuman, - Accessory, - World, - Housing, - Monster, - Icon, - LoadingScreen, - Map, - Interface, - Equipment, - Character, - Weapon, - Font, -} - -public static class ObjectTypeExtensions -{ - public static string ToName( this ObjectType type ) - => type switch - { - ObjectType.Vfx => "Visual Effect", - ObjectType.DemiHuman => "Demi Human", - ObjectType.Accessory => "Accessory", - ObjectType.World => "Doodad", - ObjectType.Housing => "Housing Object", - ObjectType.Monster => "Monster", - ObjectType.Icon => "Icon", - ObjectType.LoadingScreen => "Loading Screen", - ObjectType.Map => "Map", - ObjectType.Interface => "UI Element", - ObjectType.Equipment => "Equipment", - ObjectType.Character => "Character", - ObjectType.Weapon => "Weapon", - ObjectType.Font => "Font", - _ => "Unknown", - }; - - - public static readonly ObjectType[] ValidImcTypes = - { - ObjectType.Equipment, - ObjectType.Accessory, - ObjectType.DemiHuman, - ObjectType.Monster, - ObjectType.Weapon, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs deleted file mode 100644 index d1d859b7..00000000 --- a/Penumbra.GameData/Enums/Race.cs +++ /dev/null @@ -1,510 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using static Penumbra.GameData.Enums.GenderRace; - -namespace Penumbra.GameData.Enums; - -public enum Race : byte -{ - Unknown, - Hyur, - Elezen, - Lalafell, - Miqote, - Roegadyn, - AuRa, - Hrothgar, - Viera, -} - -public enum Gender : byte -{ - Unknown, - Male, - Female, - MaleNpc, - FemaleNpc, -} - -public enum ModelRace : byte -{ - Unknown, - Midlander, - Highlander, - Elezen, - Lalafell, - Miqote, - Roegadyn, - AuRa, - Hrothgar, - Viera, -} - -public enum SubRace : byte -{ - Unknown, - Midlander, - Highlander, - Wildwood, - Duskwight, - Plainsfolk, - Dunesfolk, - SeekerOfTheSun, - KeeperOfTheMoon, - Seawolf, - Hellsguard, - Raen, - Xaela, - Helion, - Lost, - Rava, - Veena, -} - -// The combined gender-race-npc numerical code as used by the game. -public enum GenderRace : ushort -{ - Unknown = 0, - MidlanderMale = 0101, - MidlanderMaleNpc = 0104, - MidlanderFemale = 0201, - MidlanderFemaleNpc = 0204, - HighlanderMale = 0301, - HighlanderMaleNpc = 0304, - HighlanderFemale = 0401, - HighlanderFemaleNpc = 0404, - ElezenMale = 0501, - ElezenMaleNpc = 0504, - ElezenFemale = 0601, - ElezenFemaleNpc = 0604, - MiqoteMale = 0701, - MiqoteMaleNpc = 0704, - MiqoteFemale = 0801, - MiqoteFemaleNpc = 0804, - RoegadynMale = 0901, - RoegadynMaleNpc = 0904, - RoegadynFemale = 1001, - RoegadynFemaleNpc = 1004, - LalafellMale = 1101, - LalafellMaleNpc = 1104, - LalafellFemale = 1201, - LalafellFemaleNpc = 1204, - AuRaMale = 1301, - AuRaMaleNpc = 1304, - AuRaFemale = 1401, - AuRaFemaleNpc = 1404, - HrothgarMale = 1501, - HrothgarMaleNpc = 1504, - HrothgarFemale = 1601, - HrothgarFemaleNpc = 1604, - VieraMale = 1701, - VieraMaleNpc = 1704, - VieraFemale = 1801, - VieraFemaleNpc = 1804, - UnknownMaleNpc = 9104, - UnknownFemaleNpc = 9204, -} - -public static class RaceEnumExtensions -{ - public static Race ToRace(this ModelRace race) - { - return race switch - { - ModelRace.Unknown => Race.Unknown, - ModelRace.Midlander => Race.Hyur, - ModelRace.Highlander => Race.Hyur, - ModelRace.Elezen => Race.Elezen, - ModelRace.Lalafell => Race.Lalafell, - ModelRace.Miqote => Race.Miqote, - ModelRace.Roegadyn => Race.Roegadyn, - ModelRace.AuRa => Race.AuRa, - ModelRace.Hrothgar => Race.Hrothgar, - ModelRace.Viera => Race.Viera, - _ => throw new ArgumentOutOfRangeException(nameof(race), race, null), - }; - } - - public static Race ToRace(this SubRace subRace) - { - return subRace switch - { - SubRace.Unknown => Race.Unknown, - SubRace.Midlander => Race.Hyur, - SubRace.Highlander => Race.Hyur, - SubRace.Wildwood => Race.Elezen, - SubRace.Duskwight => Race.Elezen, - SubRace.Plainsfolk => Race.Lalafell, - SubRace.Dunesfolk => Race.Lalafell, - SubRace.SeekerOfTheSun => Race.Miqote, - SubRace.KeeperOfTheMoon => Race.Miqote, - SubRace.Seawolf => Race.Roegadyn, - SubRace.Hellsguard => Race.Roegadyn, - SubRace.Raen => Race.AuRa, - SubRace.Xaela => Race.AuRa, - SubRace.Helion => Race.Hrothgar, - SubRace.Lost => Race.Hrothgar, - SubRace.Rava => Race.Viera, - SubRace.Veena => Race.Viera, - _ => throw new ArgumentOutOfRangeException(nameof(subRace), subRace, null), - }; - } - - public static string ToName(this ModelRace modelRace) - { - return modelRace switch - { - ModelRace.Midlander => SubRace.Midlander.ToName(), - ModelRace.Highlander => SubRace.Highlander.ToName(), - ModelRace.Elezen => Race.Elezen.ToName(), - ModelRace.Lalafell => Race.Lalafell.ToName(), - ModelRace.Miqote => Race.Miqote.ToName(), - ModelRace.Roegadyn => Race.Roegadyn.ToName(), - ModelRace.AuRa => Race.AuRa.ToName(), - ModelRace.Hrothgar => Race.Hrothgar.ToName(), - ModelRace.Viera => Race.Viera.ToName(), - _ => Race.Unknown.ToName(), - }; - } - - public static string ToName(this Race race) - { - return race switch - { - Race.Hyur => "Hyur", - Race.Elezen => "Elezen", - Race.Lalafell => "Lalafell", - Race.Miqote => "Miqo'te", - Race.Roegadyn => "Roegadyn", - Race.AuRa => "Au Ra", - Race.Hrothgar => "Hrothgar", - Race.Viera => "Viera", - _ => "Unknown", - }; - } - - public static string ToName(this Gender gender) - { - return gender switch - { - Gender.Male => "Male", - Gender.Female => "Female", - Gender.MaleNpc => "Male (NPC)", - Gender.FemaleNpc => "Female (NPC)", - _ => "Unknown", - }; - } - - public static string ToName(this SubRace subRace) - { - return subRace switch - { - SubRace.Midlander => "Midlander", - SubRace.Highlander => "Highlander", - SubRace.Wildwood => "Wildwood", - SubRace.Duskwight => "Duskwight", - SubRace.Plainsfolk => "Plainsfolk", - SubRace.Dunesfolk => "Dunesfolk", - SubRace.SeekerOfTheSun => "Seeker Of The Sun", - SubRace.KeeperOfTheMoon => "Keeper Of The Moon", - SubRace.Seawolf => "Seawolf", - SubRace.Hellsguard => "Hellsguard", - SubRace.Raen => "Raen", - SubRace.Xaela => "Xaela", - SubRace.Helion => "Hellion", - SubRace.Lost => "Lost", - SubRace.Rava => "Rava", - SubRace.Veena => "Veena", - _ => "Unknown", - }; - } - - public static string ToShortName(this SubRace subRace) - { - return subRace switch - { - SubRace.SeekerOfTheSun => "Sunseeker", - SubRace.KeeperOfTheMoon => "Moonkeeper", - _ => subRace.ToName(), - }; - } - - public static bool FitsRace(this SubRace subRace, Race race) - => subRace.ToRace() == race; - - public static byte ToByte(this Gender gender, ModelRace modelRace) - => (byte)((int)gender | ((int)modelRace << 3)); - - public static byte ToByte(this ModelRace modelRace, Gender gender) - => gender.ToByte(modelRace); - - public static byte ToByte(this GenderRace value) - { - var (gender, race) = value.Split(); - return gender.ToByte(race); - } - - public static (Gender, ModelRace) Split(this GenderRace value) - { - return value switch - { - Unknown => (Gender.Unknown, ModelRace.Unknown), - MidlanderMale => (Gender.Male, ModelRace.Midlander), - MidlanderMaleNpc => (Gender.MaleNpc, ModelRace.Midlander), - MidlanderFemale => (Gender.Female, ModelRace.Midlander), - MidlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Midlander), - HighlanderMale => (Gender.Male, ModelRace.Highlander), - HighlanderMaleNpc => (Gender.MaleNpc, ModelRace.Highlander), - HighlanderFemale => (Gender.Female, ModelRace.Highlander), - HighlanderFemaleNpc => (Gender.FemaleNpc, ModelRace.Highlander), - ElezenMale => (Gender.Male, ModelRace.Elezen), - ElezenMaleNpc => (Gender.MaleNpc, ModelRace.Elezen), - ElezenFemale => (Gender.Female, ModelRace.Elezen), - ElezenFemaleNpc => (Gender.FemaleNpc, ModelRace.Elezen), - LalafellMale => (Gender.Male, ModelRace.Lalafell), - LalafellMaleNpc => (Gender.MaleNpc, ModelRace.Lalafell), - LalafellFemale => (Gender.Female, ModelRace.Lalafell), - LalafellFemaleNpc => (Gender.FemaleNpc, ModelRace.Lalafell), - MiqoteMale => (Gender.Male, ModelRace.Miqote), - MiqoteMaleNpc => (Gender.MaleNpc, ModelRace.Miqote), - MiqoteFemale => (Gender.Female, ModelRace.Miqote), - MiqoteFemaleNpc => (Gender.FemaleNpc, ModelRace.Miqote), - RoegadynMale => (Gender.Male, ModelRace.Roegadyn), - RoegadynMaleNpc => (Gender.MaleNpc, ModelRace.Roegadyn), - RoegadynFemale => (Gender.Female, ModelRace.Roegadyn), - RoegadynFemaleNpc => (Gender.FemaleNpc, ModelRace.Roegadyn), - AuRaMale => (Gender.Male, ModelRace.AuRa), - AuRaMaleNpc => (Gender.MaleNpc, ModelRace.AuRa), - AuRaFemale => (Gender.Female, ModelRace.AuRa), - AuRaFemaleNpc => (Gender.FemaleNpc, ModelRace.AuRa), - HrothgarMale => (Gender.Male, ModelRace.Hrothgar), - HrothgarMaleNpc => (Gender.MaleNpc, ModelRace.Hrothgar), - HrothgarFemale => (Gender.Female, ModelRace.Hrothgar), - HrothgarFemaleNpc => (Gender.FemaleNpc, ModelRace.Hrothgar), - VieraMale => (Gender.Male, ModelRace.Viera), - VieraMaleNpc => (Gender.Male, ModelRace.Viera), - VieraFemale => (Gender.Female, ModelRace.Viera), - VieraFemaleNpc => (Gender.FemaleNpc, ModelRace.Viera), - UnknownMaleNpc => (Gender.MaleNpc, ModelRace.Unknown), - UnknownFemaleNpc => (Gender.FemaleNpc, ModelRace.Unknown), - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static bool IsValid(this GenderRace value) - => value != Unknown && Enum.IsDefined(typeof(GenderRace), value); - - public static string ToRaceCode(this GenderRace value) - { - return value switch - { - MidlanderMale => "0101", - MidlanderMaleNpc => "0104", - MidlanderFemale => "0201", - MidlanderFemaleNpc => "0204", - HighlanderMale => "0301", - HighlanderMaleNpc => "0304", - HighlanderFemale => "0401", - HighlanderFemaleNpc => "0404", - ElezenMale => "0501", - ElezenMaleNpc => "0504", - ElezenFemale => "0601", - ElezenFemaleNpc => "0604", - MiqoteMale => "0701", - MiqoteMaleNpc => "0704", - MiqoteFemale => "0801", - MiqoteFemaleNpc => "0804", - RoegadynMale => "0901", - RoegadynMaleNpc => "0904", - RoegadynFemale => "1001", - RoegadynFemaleNpc => "1004", - LalafellMale => "1101", - LalafellMaleNpc => "1104", - LalafellFemale => "1201", - LalafellFemaleNpc => "1204", - AuRaMale => "1301", - AuRaMaleNpc => "1304", - AuRaFemale => "1401", - AuRaFemaleNpc => "1404", - HrothgarMale => "1501", - HrothgarMaleNpc => "1504", - HrothgarFemale => "1601", - HrothgarFemaleNpc => "1604", - VieraMale => "1701", - VieraMaleNpc => "1704", - VieraFemale => "1801", - VieraFemaleNpc => "1804", - UnknownMaleNpc => "9104", - UnknownFemaleNpc => "9204", - _ => string.Empty, - }; - } - - public static GenderRace[] Dependencies(this GenderRace raceCode) - => DependencyList.TryGetValue(raceCode, out var dep) ? dep : Array.Empty(); - - public static IEnumerable OnlyDependencies(this GenderRace raceCode) - => DependencyList.TryGetValue(raceCode, out var dep) ? dep.Skip(1) : Array.Empty(); - - private static readonly Dictionary DependencyList = new() - { - // @formatter:off - [MidlanderMale] = new[]{ MidlanderMale }, - [HighlanderMale] = new[]{ HighlanderMale, MidlanderMale }, - [ElezenMale] = new[]{ ElezenMale, MidlanderMale }, - [MiqoteMale] = new[]{ MiqoteMale, MidlanderMale }, - [RoegadynMale] = new[]{ RoegadynMale, MidlanderMale }, - [LalafellMale] = new[]{ LalafellMale, MidlanderMale }, - [AuRaMale] = new[]{ AuRaMale, MidlanderMale }, - [HrothgarMale] = new[]{ HrothgarMale, RoegadynMale, MidlanderMale }, - [VieraMale] = new[]{ VieraMale, MidlanderMale }, - [MidlanderFemale] = new[]{ MidlanderFemale, MidlanderMale }, - [HighlanderFemale] = new[]{ HighlanderFemale, MidlanderFemale, MidlanderMale }, - [ElezenFemale] = new[]{ ElezenFemale, MidlanderFemale, MidlanderMale }, - [MiqoteFemale] = new[]{ MiqoteFemale, MidlanderFemale, MidlanderMale }, - [RoegadynFemale] = new[]{ RoegadynFemale, MidlanderFemale, MidlanderMale }, - [LalafellFemale] = new[]{ LalafellFemale, LalafellMale, MidlanderMale }, - [AuRaFemale] = new[]{ AuRaFemale, MidlanderFemale, MidlanderMale }, - [HrothgarFemale] = new[]{ HrothgarFemale, RoegadynFemale, MidlanderFemale, MidlanderMale }, - [VieraFemale] = new[]{ VieraFemale, MidlanderFemale, MidlanderMale }, - [MidlanderMaleNpc] = new[]{ MidlanderMaleNpc, MidlanderMale }, - [HighlanderMaleNpc] = new[]{ HighlanderMaleNpc, HighlanderMale, MidlanderMaleNpc, MidlanderMale }, - [ElezenMaleNpc] = new[]{ ElezenMaleNpc, ElezenMale, MidlanderMaleNpc, MidlanderMale }, - [MiqoteMaleNpc] = new[]{ MiqoteMaleNpc, MiqoteMale, MidlanderMaleNpc, MidlanderMale }, - [RoegadynMaleNpc] = new[]{ RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale }, - [LalafellMaleNpc] = new[]{ LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale }, - [AuRaMaleNpc] = new[]{ AuRaMaleNpc, AuRaMale, MidlanderMaleNpc, MidlanderMale }, - [HrothgarMaleNpc] = new[]{ HrothgarMaleNpc, HrothgarMale, RoegadynMaleNpc, RoegadynMale, MidlanderMaleNpc, MidlanderMale }, - [VieraMaleNpc] = new[]{ VieraMaleNpc, VieraMale, MidlanderMaleNpc, MidlanderMale }, - [MidlanderFemaleNpc] = new[]{ MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [HighlanderFemaleNpc] = new[]{ HighlanderFemaleNpc, HighlanderFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [ElezenFemaleNpc] = new[]{ ElezenFemaleNpc, ElezenFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [MiqoteFemaleNpc] = new[]{ MiqoteFemaleNpc, MiqoteFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [RoegadynFemaleNpc] = new[]{ RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [LalafellFemaleNpc] = new[]{ LalafellFemaleNpc, LalafellFemale, LalafellMaleNpc, LalafellMale, MidlanderMaleNpc, MidlanderMale }, - [AuRaFemaleNpc] = new[]{ AuRaFemaleNpc, AuRaFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [HrothgarFemaleNpc] = new[]{ HrothgarFemaleNpc, HrothgarFemale, RoegadynFemaleNpc, RoegadynFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [VieraFemaleNpc] = new[]{ VieraFemaleNpc, VieraFemale, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - [UnknownMaleNpc] = new[]{ UnknownMaleNpc, MidlanderMaleNpc, MidlanderMale }, - [UnknownFemaleNpc] = new[]{ UnknownFemaleNpc, MidlanderFemaleNpc, MidlanderFemale, MidlanderMaleNpc, MidlanderMale }, - // @formatter:on - }; -} - -public static partial class Names -{ - public static GenderRace GenderRaceFromCode(string code) - { - return code switch - { - "0101" => MidlanderMale, - "0104" => MidlanderMaleNpc, - "0201" => MidlanderFemale, - "0204" => MidlanderFemaleNpc, - "0301" => HighlanderMale, - "0304" => HighlanderMaleNpc, - "0401" => HighlanderFemale, - "0404" => HighlanderFemaleNpc, - "0501" => ElezenMale, - "0504" => ElezenMaleNpc, - "0601" => ElezenFemale, - "0604" => ElezenFemaleNpc, - "0701" => MiqoteMale, - "0704" => MiqoteMaleNpc, - "0801" => MiqoteFemale, - "0804" => MiqoteFemaleNpc, - "0901" => RoegadynMale, - "0904" => RoegadynMaleNpc, - "1001" => RoegadynFemale, - "1004" => RoegadynFemaleNpc, - "1101" => LalafellMale, - "1104" => LalafellMaleNpc, - "1201" => LalafellFemale, - "1204" => LalafellFemaleNpc, - "1301" => AuRaMale, - "1304" => AuRaMaleNpc, - "1401" => AuRaFemale, - "1404" => AuRaFemaleNpc, - "1501" => HrothgarMale, - "1504" => HrothgarMaleNpc, - "1601" => HrothgarFemale, - "1604" => HrothgarFemaleNpc, - "1701" => VieraMale, - "1704" => VieraMaleNpc, - "1801" => VieraFemale, - "1804" => VieraFemaleNpc, - "9104" => UnknownMaleNpc, - "9204" => UnknownFemaleNpc, - _ => Unknown, - }; - } - - public static GenderRace GenderRaceFromByte(byte value) - { - var gender = (Gender)(value & 0b111); - var race = (ModelRace)(value >> 3); - return CombinedRace(gender, race); - } - - public static GenderRace CombinedRace(Gender gender, ModelRace modelRace) - { - return gender switch - { - Gender.Male => modelRace switch - { - ModelRace.Midlander => MidlanderMale, - ModelRace.Highlander => HighlanderMale, - ModelRace.Elezen => ElezenMale, - ModelRace.Lalafell => LalafellMale, - ModelRace.Miqote => MiqoteMale, - ModelRace.Roegadyn => RoegadynMale, - ModelRace.AuRa => AuRaMale, - ModelRace.Hrothgar => HrothgarMale, - ModelRace.Viera => VieraMale, - _ => Unknown, - }, - Gender.MaleNpc => modelRace switch - { - ModelRace.Midlander => MidlanderMaleNpc, - ModelRace.Highlander => HighlanderMaleNpc, - ModelRace.Elezen => ElezenMaleNpc, - ModelRace.Lalafell => LalafellMaleNpc, - ModelRace.Miqote => MiqoteMaleNpc, - ModelRace.Roegadyn => RoegadynMaleNpc, - ModelRace.AuRa => AuRaMaleNpc, - ModelRace.Hrothgar => HrothgarMaleNpc, - ModelRace.Viera => VieraMaleNpc, - _ => Unknown, - }, - Gender.Female => modelRace switch - { - ModelRace.Midlander => MidlanderFemale, - ModelRace.Highlander => HighlanderFemale, - ModelRace.Elezen => ElezenFemale, - ModelRace.Lalafell => LalafellFemale, - ModelRace.Miqote => MiqoteFemale, - ModelRace.Roegadyn => RoegadynFemale, - ModelRace.AuRa => AuRaFemale, - ModelRace.Hrothgar => HrothgarFemale, - ModelRace.Viera => VieraFemale, - _ => Unknown, - }, - Gender.FemaleNpc => modelRace switch - { - ModelRace.Midlander => MidlanderFemaleNpc, - ModelRace.Highlander => HighlanderFemaleNpc, - ModelRace.Elezen => ElezenFemaleNpc, - ModelRace.Lalafell => LalafellFemaleNpc, - ModelRace.Miqote => MiqoteFemaleNpc, - ModelRace.Roegadyn => RoegadynFemaleNpc, - ModelRace.AuRa => AuRaFemaleNpc, - ModelRace.Hrothgar => HrothgarFemaleNpc, - ModelRace.Viera => VieraFemaleNpc, - _ => Unknown, - }, - _ => Unknown, - }; - } -} diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs deleted file mode 100644 index 80ba03e9..00000000 --- a/Penumbra.GameData/Enums/ResourceType.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.String; -using Penumbra.String.Functions; - -namespace Penumbra.GameData.Enums; - -public enum ResourceType : uint -{ - Unknown = 0, - Aet = 0x00616574, - Amb = 0x00616D62, - Atch = 0x61746368, - Atex = 0x61746578, - Avfx = 0x61766678, - Awt = 0x00617774, - Cmp = 0x00636D70, - Dic = 0x00646963, - Eid = 0x00656964, - Envb = 0x656E7662, - Eqdp = 0x65716470, - Eqp = 0x00657170, - Essb = 0x65737362, - Est = 0x00657374, - Evp = 0x00657670, - Exd = 0x00657864, - Exh = 0x00657868, - Exl = 0x0065786C, - Fdt = 0x00666474, - Gfd = 0x00676664, - Ggd = 0x00676764, - Gmp = 0x00676D70, - Gzd = 0x00677A64, - Imc = 0x00696D63, - Lcb = 0x006C6362, - Lgb = 0x006C6762, - Luab = 0x6C756162, - Lvb = 0x006C7662, - Mdl = 0x006D646C, - Mlt = 0x006D6C74, - Mtrl = 0x6D74726C, - Obsb = 0x6F627362, - Pap = 0x00706170, - Pbd = 0x00706264, - Pcb = 0x00706362, - Phyb = 0x70687962, - Plt = 0x00706C74, - Scd = 0x00736364, - Sgb = 0x00736762, - Shcd = 0x73686364, - Shpk = 0x7368706B, - Sklb = 0x736B6C62, - Skp = 0x00736B70, - Stm = 0x0073746D, - Svb = 0x00737662, - Tera = 0x74657261, - Tex = 0x00746578, - Tmb = 0x00746D62, - Ugd = 0x00756764, - Uld = 0x00756C64, - Waoe = 0x77616F65, - Wtd = 0x00777464, -} - -[Flags] -public enum ResourceTypeFlag : ulong -{ - Aet = 0x0000_0000_0000_0001, - Amb = 0x0000_0000_0000_0002, - Atch = 0x0000_0000_0000_0004, - Atex = 0x0000_0000_0000_0008, - Avfx = 0x0000_0000_0000_0010, - Awt = 0x0000_0000_0000_0020, - Cmp = 0x0000_0000_0000_0040, - Dic = 0x0000_0000_0000_0080, - Eid = 0x0000_0000_0000_0100, - Envb = 0x0000_0000_0000_0200, - Eqdp = 0x0000_0000_0000_0400, - Eqp = 0x0000_0000_0000_0800, - Essb = 0x0000_0000_0000_1000, - Est = 0x0000_0000_0000_2000, - Evp = 0x0000_0000_0000_4000, - Exd = 0x0000_0000_0000_8000, - Exh = 0x0000_0000_0001_0000, - Exl = 0x0000_0000_0002_0000, - Fdt = 0x0000_0000_0004_0000, - Gfd = 0x0000_0000_0008_0000, - Ggd = 0x0000_0000_0010_0000, - Gmp = 0x0000_0000_0020_0000, - Gzd = 0x0000_0000_0040_0000, - Imc = 0x0000_0000_0080_0000, - Lcb = 0x0000_0000_0100_0000, - Lgb = 0x0000_0000_0200_0000, - Luab = 0x0000_0000_0400_0000, - Lvb = 0x0000_0000_0800_0000, - Mdl = 0x0000_0000_1000_0000, - Mlt = 0x0000_0000_2000_0000, - Mtrl = 0x0000_0000_4000_0000, - Obsb = 0x0000_0000_8000_0000, - Pap = 0x0000_0001_0000_0000, - Pbd = 0x0000_0002_0000_0000, - Pcb = 0x0000_0004_0000_0000, - Phyb = 0x0000_0008_0000_0000, - Plt = 0x0000_0010_0000_0000, - Scd = 0x0000_0020_0000_0000, - Sgb = 0x0000_0040_0000_0000, - Shcd = 0x0000_0080_0000_0000, - Shpk = 0x0000_0100_0000_0000, - Sklb = 0x0000_0200_0000_0000, - Skp = 0x0000_0400_0000_0000, - Stm = 0x0000_0800_0000_0000, - Svb = 0x0000_1000_0000_0000, - Tera = 0x0000_2000_0000_0000, - Tex = 0x0000_4000_0000_0000, - Tmb = 0x0000_8000_0000_0000, - Ugd = 0x0001_0000_0000_0000, - Uld = 0x0002_0000_0000_0000, - Waoe = 0x0004_0000_0000_0000, - Wtd = 0x0008_0000_0000_0000, -} - -[Flags] -public enum ResourceCategoryFlag : ushort -{ - Common = 0x0001, - BgCommon = 0x0002, - Bg = 0x0004, - Cut = 0x0008, - Chara = 0x0010, - Shader = 0x0020, - Ui = 0x0040, - Sound = 0x0080, - Vfx = 0x0100, - UiScript = 0x0200, - Exd = 0x0400, - GameScript = 0x0800, - Music = 0x1000, - SqpackTest = 0x2000, -} - -public static class ResourceExtensions -{ - public static readonly ResourceTypeFlag AllResourceTypes = Enum.GetValues().Aggregate((v, f) => v | f); - public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues().Aggregate((v, f) => v | f); - - public static ResourceTypeFlag ToFlag(this ResourceType type) - => type switch - { - ResourceType.Aet => ResourceTypeFlag.Aet, - ResourceType.Amb => ResourceTypeFlag.Amb, - ResourceType.Atch => ResourceTypeFlag.Atch, - ResourceType.Atex => ResourceTypeFlag.Atex, - ResourceType.Avfx => ResourceTypeFlag.Avfx, - ResourceType.Awt => ResourceTypeFlag.Awt, - ResourceType.Cmp => ResourceTypeFlag.Cmp, - ResourceType.Dic => ResourceTypeFlag.Dic, - ResourceType.Eid => ResourceTypeFlag.Eid, - ResourceType.Envb => ResourceTypeFlag.Envb, - ResourceType.Eqdp => ResourceTypeFlag.Eqdp, - ResourceType.Eqp => ResourceTypeFlag.Eqp, - ResourceType.Essb => ResourceTypeFlag.Essb, - ResourceType.Est => ResourceTypeFlag.Est, - ResourceType.Evp => ResourceTypeFlag.Evp, - ResourceType.Exd => ResourceTypeFlag.Exd, - ResourceType.Exh => ResourceTypeFlag.Exh, - ResourceType.Exl => ResourceTypeFlag.Exl, - ResourceType.Fdt => ResourceTypeFlag.Fdt, - ResourceType.Gfd => ResourceTypeFlag.Gfd, - ResourceType.Ggd => ResourceTypeFlag.Ggd, - ResourceType.Gmp => ResourceTypeFlag.Gmp, - ResourceType.Gzd => ResourceTypeFlag.Gzd, - ResourceType.Imc => ResourceTypeFlag.Imc, - ResourceType.Lcb => ResourceTypeFlag.Lcb, - ResourceType.Lgb => ResourceTypeFlag.Lgb, - ResourceType.Luab => ResourceTypeFlag.Luab, - ResourceType.Lvb => ResourceTypeFlag.Lvb, - ResourceType.Mdl => ResourceTypeFlag.Mdl, - ResourceType.Mlt => ResourceTypeFlag.Mlt, - ResourceType.Mtrl => ResourceTypeFlag.Mtrl, - ResourceType.Obsb => ResourceTypeFlag.Obsb, - ResourceType.Pap => ResourceTypeFlag.Pap, - ResourceType.Pbd => ResourceTypeFlag.Pbd, - ResourceType.Pcb => ResourceTypeFlag.Pcb, - ResourceType.Phyb => ResourceTypeFlag.Phyb, - ResourceType.Plt => ResourceTypeFlag.Plt, - ResourceType.Scd => ResourceTypeFlag.Scd, - ResourceType.Sgb => ResourceTypeFlag.Sgb, - ResourceType.Shcd => ResourceTypeFlag.Shcd, - ResourceType.Shpk => ResourceTypeFlag.Shpk, - ResourceType.Sklb => ResourceTypeFlag.Sklb, - ResourceType.Skp => ResourceTypeFlag.Skp, - ResourceType.Stm => ResourceTypeFlag.Stm, - ResourceType.Svb => ResourceTypeFlag.Svb, - ResourceType.Tera => ResourceTypeFlag.Tera, - ResourceType.Tex => ResourceTypeFlag.Tex, - ResourceType.Tmb => ResourceTypeFlag.Tmb, - ResourceType.Ugd => ResourceTypeFlag.Ugd, - ResourceType.Uld => ResourceTypeFlag.Uld, - ResourceType.Waoe => ResourceTypeFlag.Waoe, - ResourceType.Wtd => ResourceTypeFlag.Wtd, - _ => 0, - }; - - public static bool FitsFlag(this ResourceType type, ResourceTypeFlag flags) - => (type.ToFlag() & flags) != 0; - - public static ResourceCategoryFlag ToFlag(this ResourceCategory type) - => type switch - { - ResourceCategory.Common => ResourceCategoryFlag.Common, - ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, - ResourceCategory.Bg => ResourceCategoryFlag.Bg, - ResourceCategory.Cut => ResourceCategoryFlag.Cut, - ResourceCategory.Chara => ResourceCategoryFlag.Chara, - ResourceCategory.Shader => ResourceCategoryFlag.Shader, - ResourceCategory.Ui => ResourceCategoryFlag.Ui, - ResourceCategory.Sound => ResourceCategoryFlag.Sound, - ResourceCategory.Vfx => ResourceCategoryFlag.Vfx, - ResourceCategory.UiScript => ResourceCategoryFlag.UiScript, - ResourceCategory.Exd => ResourceCategoryFlag.Exd, - ResourceCategory.GameScript => ResourceCategoryFlag.GameScript, - ResourceCategory.Music => ResourceCategoryFlag.Music, - ResourceCategory.SqpackTest => ResourceCategoryFlag.SqpackTest, - _ => 0, - }; - - public static bool FitsFlag(this ResourceCategory type, ResourceCategoryFlag flags) - => (type.ToFlag() & flags) != 0; - - public static ResourceType FromBytes(byte a1, byte a2, byte a3) - => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 16) - | ((uint)ByteStringFunctions.AsciiToLower(a2) << 8) - | ByteStringFunctions.AsciiToLower(a3)); - - public static ResourceType FromBytes(byte a1, byte a2, byte a3, byte a4) - => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 24) - | ((uint)ByteStringFunctions.AsciiToLower(a2) << 16) - | ((uint)ByteStringFunctions.AsciiToLower(a3) << 8) - | ByteStringFunctions.AsciiToLower(a4)); - - public static ResourceType FromBytes(char a1, char a2, char a3) - => FromBytes((byte)a1, (byte)a2, (byte)a3); - - public static ResourceType FromBytes(char a1, char a2, char a3, char a4) - => FromBytes((byte)a1, (byte)a2, (byte)a3, (byte)a4); - - public static ResourceType Type(string path) - { - var ext = Path.GetExtension(path.AsSpan()); - ext = ext.Length == 0 ? path.AsSpan() : ext[1..]; - - return ext.Length switch - { - 0 => 0, - 1 => (ResourceType)ext[^1], - 2 => FromBytes('\0', ext[^2], ext[^1]), - 3 => FromBytes(ext[^3], ext[^2], ext[^1]), - _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), - }; - } - - public static ResourceType Type(ByteString path) - { - var extIdx = path.LastIndexOf((byte)'.'); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); - - return ext.Length switch - { - 0 => 0, - 1 => (ResourceType)ext[^1], - 2 => FromBytes(0, ext[^2], ext[^1]), - 3 => FromBytes(ext[^3], ext[^2], ext[^1]), - _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), - }; - } - - public static ResourceCategory Category(ByteString path) - { - if (path.Length < 3) - return ResourceCategory.Debug; - - return ByteStringFunctions.AsciiToUpper(path[0]) switch - { - (byte)'C' => ByteStringFunctions.AsciiToUpper(path[1]) switch - { - (byte)'O' => ResourceCategory.Common, - (byte)'U' => ResourceCategory.Cut, - (byte)'H' => ResourceCategory.Chara, - _ => ResourceCategory.Debug, - }, - (byte)'B' => ByteStringFunctions.AsciiToUpper(path[2]) switch - { - (byte)'C' => ResourceCategory.BgCommon, - (byte)'/' => ResourceCategory.Bg, - _ => ResourceCategory.Debug, - }, - (byte)'S' => ByteStringFunctions.AsciiToUpper(path[1]) switch - { - (byte)'H' => ResourceCategory.Shader, - (byte)'O' => ResourceCategory.Sound, - (byte)'Q' => ResourceCategory.SqpackTest, - _ => ResourceCategory.Debug, - }, - (byte)'U' => ByteStringFunctions.AsciiToUpper(path[2]) switch - { - (byte)'/' => ResourceCategory.Ui, - (byte)'S' => ResourceCategory.UiScript, - _ => ResourceCategory.Debug, - }, - (byte)'V' => ResourceCategory.Vfx, - (byte)'E' => ResourceCategory.Exd, - (byte)'G' => ResourceCategory.GameScript, - (byte)'M' => ResourceCategory.Music, - _ => ResourceCategory.Debug, - }; - } -} diff --git a/Penumbra.GameData/Enums/RspAttribute.cs b/Penumbra.GameData/Enums/RspAttribute.cs deleted file mode 100644 index c4016bb6..00000000 --- a/Penumbra.GameData/Enums/RspAttribute.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.ComponentModel; - -namespace Penumbra.GameData.Enums; - -public enum RspAttribute : byte -{ - MaleMinSize, - MaleMaxSize, - MaleMinTail, - MaleMaxTail, - FemaleMinSize, - FemaleMaxSize, - FemaleMinTail, - FemaleMaxTail, - BustMinX, - BustMinY, - BustMinZ, - BustMaxX, - BustMaxY, - BustMaxZ, - NumAttributes, -} - -public static class RspAttributeExtensions -{ - public static Gender ToGender( this RspAttribute attribute ) - { - return attribute switch - { - RspAttribute.MaleMinSize => Gender.Male, - RspAttribute.MaleMaxSize => Gender.Male, - RspAttribute.MaleMinTail => Gender.Male, - RspAttribute.MaleMaxTail => Gender.Male, - RspAttribute.FemaleMinSize => Gender.Female, - RspAttribute.FemaleMaxSize => Gender.Female, - RspAttribute.FemaleMinTail => Gender.Female, - RspAttribute.FemaleMaxTail => Gender.Female, - RspAttribute.BustMinX => Gender.Female, - RspAttribute.BustMinY => Gender.Female, - RspAttribute.BustMinZ => Gender.Female, - RspAttribute.BustMaxX => Gender.Female, - RspAttribute.BustMaxY => Gender.Female, - RspAttribute.BustMaxZ => Gender.Female, - _ => Gender.Unknown, - }; - } - - public static string ToUngenderedString( this RspAttribute attribute ) - { - return attribute switch - { - RspAttribute.MaleMinSize => "MinSize", - RspAttribute.MaleMaxSize => "MaxSize", - RspAttribute.MaleMinTail => "MinTail", - RspAttribute.MaleMaxTail => "MaxTail", - RspAttribute.FemaleMinSize => "MinSize", - RspAttribute.FemaleMaxSize => "MaxSize", - RspAttribute.FemaleMinTail => "MinTail", - RspAttribute.FemaleMaxTail => "MaxTail", - RspAttribute.BustMinX => "BustMinX", - RspAttribute.BustMinY => "BustMinY", - RspAttribute.BustMinZ => "BustMinZ", - RspAttribute.BustMaxX => "BustMaxX", - RspAttribute.BustMaxY => "BustMaxY", - RspAttribute.BustMaxZ => "BustMaxZ", - _ => "", - }; - } - - public static string ToFullString( this RspAttribute attribute ) - { - return attribute switch - { - RspAttribute.MaleMinSize => "Male Minimum Size", - RspAttribute.MaleMaxSize => "Male Maximum Size", - RspAttribute.FemaleMinSize => "Female Minimum Size", - RspAttribute.FemaleMaxSize => "Female Maximum Size", - RspAttribute.BustMinX => "Bust Minimum X-Axis", - RspAttribute.BustMaxX => "Bust Maximum X-Axis", - RspAttribute.BustMinY => "Bust Minimum Y-Axis", - RspAttribute.BustMaxY => "Bust Maximum Y-Axis", - RspAttribute.BustMinZ => "Bust Minimum Z-Axis", - RspAttribute.BustMaxZ => "Bust Maximum Z-Axis", - RspAttribute.MaleMinTail => "Male Minimum Tail Length", - RspAttribute.MaleMaxTail => "Male Maximum Tail Length", - RspAttribute.FemaleMinTail => "Female Minimum Tail Length", - RspAttribute.FemaleMaxTail => "Female Maximum Tail Length", - _ => throw new InvalidEnumArgumentException(), - }; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/WeaponCategory.cs b/Penumbra.GameData/Enums/WeaponCategory.cs deleted file mode 100644 index 4128361f..00000000 --- a/Penumbra.GameData/Enums/WeaponCategory.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Penumbra.GameData.Enums; - -public enum WeaponCategory : byte -{ - Unknown = 0, - Pugilist, - Gladiator, - Marauder, - Archer, - Lancer, - Thaumaturge1, - Thaumaturge2, - Conjurer1, - Conjurer2, - Arcanist, - Shield, - CarpenterMain, - CarpenterOff, - BlacksmithMain, - BlacksmithOff, - ArmorerMain, - ArmorerOff, - GoldsmithMain, - GoldsmithOff, - LeatherworkerMain, - LeatherworkerOff, - WeaverMain, - WeaverOff, - AlchemistMain, - AlchemistOff, - CulinarianMain, - CulinarianOff, - MinerMain, - MinerOff, - BotanistMain, - BotanistOff, - FisherMain, - Rogue = 84, - DarkKnight = 87, - Machinist = 88, - Astrologian = 89, - Samurai = 96, - RedMage = 97, - Scholar = 98, - FisherOff = 99, - BlueMage = 105, - Gunbreaker = 106, - Dancer = 107, - Reaper = 108, - Sage = 109, -} \ No newline at end of file diff --git a/Penumbra.GameData/Files/AvfxFile.cs b/Penumbra.GameData/Files/AvfxFile.cs deleted file mode 100644 index 330a8416..00000000 --- a/Penumbra.GameData/Files/AvfxFile.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.IO; -using System.Numerics; -using System.Text; - -namespace Penumbra.GameData.Files; - -public class AvfxFile : IWritable -{ - public struct Block - { - public uint Name; - public uint Size; - public byte[] Data; - - public Block(BinaryReader r) - { - Name = r.ReadUInt32(); - Size = r.ReadUInt32(); - Data = r.ReadBytes((int)Size.RoundTo4()); - } - - public byte ToBool() - => BitConverter.ToBoolean(Data) ? (byte)1 : (byte)0; - - public uint ToUint() - => BitConverter.ToUInt32(Data); - - public float ToFloat() - => BitConverter.ToSingle(Data); - - public new string ToString() - { - var span = Data.AsSpan(0, (int)Size - 1); - return Encoding.UTF8.GetString(span); - } - } - - public static readonly Vector3 BadVector = new(float.NaN); - - public Vector3 ClipBox = BadVector; - public Vector3 ClipBoxSize = BadVector; - public Vector3 RevisedValuesPos = BadVector; - public Vector3 RevisedValuesRot = BadVector; - public Vector3 RevisedValuesScale = BadVector; - public Vector3 RevisedValuesColor = BadVector; - - public uint Version = uint.MaxValue; - public uint DrawLayerType = uint.MaxValue; - public uint DrawOrderType = uint.MaxValue; - public uint DirectionalLightSourceType = uint.MaxValue; - public uint PointLightsType1 = uint.MaxValue; - public uint PointLightsType2 = uint.MaxValue; - - public float BiasZmaxScale = float.NaN; - public float BiasZmaxDistance = float.NaN; - public float NearClipBegin = float.NaN; - public float NearClipEnd = float.NaN; - public float FadeInnerX = float.NaN; - public float FadeOuterX = float.NaN; - public float FadeInnerY = float.NaN; - public float FadeOuterY = float.NaN; - public float FadeInnerZ = float.NaN; - public float FadeOuterZ = float.NaN; - public float FarClipBegin = float.NaN; - public float FarClipEnd = float.NaN; - public float SoftParticleFadeRange = float.NaN; - public float SoftKeyOffset = float.NaN; - public float GlobalFogInfluence = float.NaN; - - public byte IsDelayFastParticle = byte.MaxValue; - public byte IsFitGround = byte.MaxValue; - public byte IsTransformSkip = byte.MaxValue; - public byte IsAllStopOnHide = byte.MaxValue; - public byte CanBeClippedOut = byte.MaxValue; - public byte ClipBoxEnabled = byte.MaxValue; - public byte IsCameraSpace = byte.MaxValue; - public byte IsFullEnvLight = byte.MaxValue; - public byte IsClipOwnSetting = byte.MaxValue; - public byte FadeEnabledX = byte.MaxValue; - public byte FadeEnabledY = byte.MaxValue; - public byte FadeEnabledZ = byte.MaxValue; - public byte GlobalFogEnabled = byte.MaxValue; - public byte LtsEnabled = byte.MaxValue; - - public Block[] Schedulers = Array.Empty(); - public Block[] Timelines = Array.Empty(); - public Block[] Emitters = Array.Empty(); - public Block[] Particles = Array.Empty(); - public Block[] Effectors = Array.Empty(); - public Block[] Binders = Array.Empty(); - public string[] Textures = Array.Empty(); - public Block[] Models = Array.Empty(); - - public bool Valid - => true; - - public AvfxFile(byte[] data) - { - using var stream = new MemoryStream(data); - using var r = new BinaryReader(stream); - - var name = r.ReadUInt32(); - var size = r.ReadUInt32(); - var schedulerCount = 0; - var timelineCount = 0; - var emitterCount = 0; - var particleCount = 0; - var effectorCount = 0; - var binderCount = 0; - var textureCount = 0; - var modelCount = 0; - while (r.BaseStream.Position < size) - { - var block = new Block(r); - switch (block.Name) - { - // @formatter:off - case AvfxMagic.Version: Version = block.ToUint(); break; - case AvfxMagic.IsDelayFastParticle: IsDelayFastParticle = block.ToBool(); break; - case AvfxMagic.IsFitGround: IsFitGround = block.ToBool(); break; - case AvfxMagic.IsTransformSkip: IsTransformSkip = block.ToBool(); break; - case AvfxMagic.IsAllStopOnHide: IsAllStopOnHide = block.ToBool(); break; - case AvfxMagic.CanBeClippedOut: CanBeClippedOut = block.ToBool(); break; - case AvfxMagic.ClipBoxEnabled: ClipBoxEnabled = block.ToBool(); break; - case AvfxMagic.ClipBoxX: ClipBox.X = block.ToFloat(); break; - case AvfxMagic.ClipBoxY: ClipBox.Y = block.ToFloat(); break; - case AvfxMagic.ClipBoxZ: ClipBox.Z = block.ToFloat(); break; - case AvfxMagic.ClipBoxSizeX: ClipBoxSize.X = block.ToFloat(); break; - case AvfxMagic.ClipBoxSizeY: ClipBoxSize.Y = block.ToFloat(); break; - case AvfxMagic.ClipBoxSizeZ: ClipBoxSize.Z = block.ToFloat(); break; - case AvfxMagic.BiasZmaxScale: BiasZmaxScale = block.ToFloat(); break; - case AvfxMagic.BiasZmaxDistance: BiasZmaxDistance = block.ToFloat(); break; - case AvfxMagic.IsCameraSpace: IsCameraSpace = block.ToBool(); break; - case AvfxMagic.IsFullEnvLight: IsFullEnvLight = block.ToBool(); break; - case AvfxMagic.IsClipOwnSetting: IsClipOwnSetting = block.ToBool(); break; - case AvfxMagic.NearClipBegin: NearClipBegin = block.ToFloat(); break; - case AvfxMagic.NearClipEnd: NearClipEnd = block.ToFloat(); break; - case AvfxMagic.FarClipBegin: FarClipBegin = block.ToFloat(); break; - case AvfxMagic.FarClipEnd: FarClipEnd = block.ToFloat(); break; - case AvfxMagic.SoftParticleFadeRange: SoftParticleFadeRange = block.ToFloat(); break; - case AvfxMagic.SoftKeyOffset: SoftKeyOffset = block.ToFloat(); break; - case AvfxMagic.DrawLayerType: DrawLayerType = block.ToUint(); break; - case AvfxMagic.DrawOrderType: DrawOrderType = block.ToUint(); break; - case AvfxMagic.DirectionalLightSourceType: DirectionalLightSourceType = block.ToUint(); break; - case AvfxMagic.PointLightsType1: PointLightsType1 = block.ToUint(); break; - case AvfxMagic.PointLightsType2: PointLightsType2 = block.ToUint(); break; - case AvfxMagic.RevisedValuesPosX: RevisedValuesPos.X = block.ToFloat(); break; - case AvfxMagic.RevisedValuesPosY: RevisedValuesPos.Y = block.ToFloat(); break; - case AvfxMagic.RevisedValuesPosZ: RevisedValuesPos.Z = block.ToFloat(); break; - case AvfxMagic.RevisedValuesRotX: RevisedValuesRot.X = block.ToFloat(); break; - case AvfxMagic.RevisedValuesRotY: RevisedValuesRot.Y = block.ToFloat(); break; - case AvfxMagic.RevisedValuesRotZ: RevisedValuesRot.Z = block.ToFloat(); break; - case AvfxMagic.RevisedValuesScaleX: RevisedValuesScale.X = block.ToFloat(); break; - case AvfxMagic.RevisedValuesScaleY: RevisedValuesScale.Y = block.ToFloat(); break; - case AvfxMagic.RevisedValuesScaleZ: RevisedValuesScale.Z = block.ToFloat(); break; - case AvfxMagic.RevisedValuesColorR: RevisedValuesColor.X = block.ToFloat(); break; - case AvfxMagic.RevisedValuesColorG: RevisedValuesColor.Y = block.ToFloat(); break; - case AvfxMagic.RevisedValuesColorB: RevisedValuesColor.Z = block.ToFloat(); break; - case AvfxMagic.FadeEnabledX: FadeEnabledX = block.ToBool(); break; - case AvfxMagic.FadeInnerX: FadeInnerX = block.ToFloat(); break; - case AvfxMagic.FadeOuterX: FadeOuterX = block.ToFloat(); break; - case AvfxMagic.FadeEnabledY: FadeEnabledY = block.ToBool(); break; - case AvfxMagic.FadeInnerY: FadeInnerY = block.ToFloat(); break; - case AvfxMagic.FadeOuterY: FadeOuterY = block.ToFloat(); break; - case AvfxMagic.FadeEnabledZ: FadeEnabledZ = block.ToBool(); break; - case AvfxMagic.FadeInnerZ: FadeInnerZ = block.ToFloat(); break; - case AvfxMagic.FadeOuterZ: FadeOuterZ = block.ToFloat(); break; - case AvfxMagic.GlobalFogEnabled: GlobalFogEnabled = block.ToBool(); break; - case AvfxMagic.GlobalFogInfluence: GlobalFogInfluence = block.ToFloat(); break; - case AvfxMagic.LtsEnabled: LtsEnabled = block.ToBool(); break; - case AvfxMagic.NumSchedulers: Schedulers = new Block[block.ToUint()]; break; - case AvfxMagic.NumTimelines: Timelines = new Block[block.ToUint()]; break; - case AvfxMagic.NumEmitters: Emitters = new Block[block.ToUint()]; break; - case AvfxMagic.NumParticles: Particles = new Block[block.ToUint()]; break; - case AvfxMagic.NumEffectors: Effectors = new Block[block.ToUint()]; break; - case AvfxMagic.NumBinders: Binders = new Block[block.ToUint()]; break; - case AvfxMagic.NumTextures: Textures = new string[block.ToUint()]; break; - case AvfxMagic.NumModels: Models = new Block[block.ToUint()]; break; - case AvfxMagic.Scheduler: Schedulers[schedulerCount++] = block; break; - case AvfxMagic.Timeline: Timelines[timelineCount++] = block; break; - case AvfxMagic.Emitter: Emitters[emitterCount++] = block; break; - case AvfxMagic.Particle: Particles[particleCount++] = block; break; - case AvfxMagic.Effector: Effectors[effectorCount++] = block; break; - case AvfxMagic.Binder: Binders[binderCount++] = block; break; - case AvfxMagic.Texture: Textures[textureCount++] = block.ToString(); break; - case AvfxMagic.Model: Models[modelCount++] = block; break; - // @formatter:on - } - } - } - - - public byte[] Write() - { - using var m = new MemoryStream(512 * 1024); - using var w = new BinaryWriter(m); - - w.Write(AvfxMagic.AvfxBase); - var sizePos = w.BaseStream.Position; - w.Write(0u); - w.WriteBlock(AvfxMagic.Version, Version) - .WriteBlock(AvfxMagic.IsDelayFastParticle, IsDelayFastParticle) - .WriteBlock(AvfxMagic.IsFitGround, IsFitGround) - .WriteBlock(AvfxMagic.IsTransformSkip, IsTransformSkip) - .WriteBlock(AvfxMagic.IsAllStopOnHide, IsAllStopOnHide) - .WriteBlock(AvfxMagic.CanBeClippedOut, CanBeClippedOut) - .WriteBlock(AvfxMagic.ClipBoxEnabled, ClipBoxEnabled) - .WriteBlock(AvfxMagic.ClipBoxX, ClipBox.X) - .WriteBlock(AvfxMagic.ClipBoxY, ClipBox.Y) - .WriteBlock(AvfxMagic.ClipBoxZ, ClipBox.Z) - .WriteBlock(AvfxMagic.ClipBoxSizeX, ClipBoxSize.X) - .WriteBlock(AvfxMagic.ClipBoxSizeY, ClipBoxSize.Y) - .WriteBlock(AvfxMagic.ClipBoxSizeZ, ClipBoxSize.Z) - .WriteBlock(AvfxMagic.BiasZmaxScale, BiasZmaxScale) - .WriteBlock(AvfxMagic.BiasZmaxDistance, BiasZmaxDistance) - .WriteBlock(AvfxMagic.IsCameraSpace, IsCameraSpace) - .WriteBlock(AvfxMagic.IsFullEnvLight, IsFullEnvLight) - .WriteBlock(AvfxMagic.IsClipOwnSetting, IsClipOwnSetting) - .WriteBlock(AvfxMagic.NearClipBegin, NearClipBegin) - .WriteBlock(AvfxMagic.NearClipEnd, NearClipEnd) - .WriteBlock(AvfxMagic.FarClipBegin, FarClipBegin) - .WriteBlock(AvfxMagic.FarClipEnd, FarClipEnd) - .WriteBlock(AvfxMagic.SoftParticleFadeRange, SoftParticleFadeRange) - .WriteBlock(AvfxMagic.SoftKeyOffset, SoftKeyOffset) - .WriteBlock(AvfxMagic.DrawLayerType, DrawLayerType) - .WriteBlock(AvfxMagic.DrawOrderType, DrawOrderType) - .WriteBlock(AvfxMagic.DirectionalLightSourceType, DirectionalLightSourceType) - .WriteBlock(AvfxMagic.PointLightsType1, PointLightsType1) - .WriteBlock(AvfxMagic.PointLightsType2, PointLightsType2) - .WriteBlock(AvfxMagic.RevisedValuesPosX, RevisedValuesPos.X) - .WriteBlock(AvfxMagic.RevisedValuesPosY, RevisedValuesPos.Y) - .WriteBlock(AvfxMagic.RevisedValuesPosZ, RevisedValuesPos.Z) - .WriteBlock(AvfxMagic.RevisedValuesRotX, RevisedValuesRot.X) - .WriteBlock(AvfxMagic.RevisedValuesRotY, RevisedValuesRot.Y) - .WriteBlock(AvfxMagic.RevisedValuesRotZ, RevisedValuesRot.Z) - .WriteBlock(AvfxMagic.RevisedValuesScaleX, RevisedValuesScale.X) - .WriteBlock(AvfxMagic.RevisedValuesScaleY, RevisedValuesScale.Y) - .WriteBlock(AvfxMagic.RevisedValuesScaleZ, RevisedValuesScale.Z) - .WriteBlock(AvfxMagic.RevisedValuesColorR, RevisedValuesColor.X) - .WriteBlock(AvfxMagic.RevisedValuesColorG, RevisedValuesColor.Y) - .WriteBlock(AvfxMagic.RevisedValuesColorB, RevisedValuesColor.Z) - .WriteBlock(AvfxMagic.FadeEnabledX, FadeEnabledX) - .WriteBlock(AvfxMagic.FadeInnerX, FadeInnerX) - .WriteBlock(AvfxMagic.FadeOuterX, FadeOuterX) - .WriteBlock(AvfxMagic.FadeEnabledY, FadeEnabledY) - .WriteBlock(AvfxMagic.FadeInnerY, FadeInnerY) - .WriteBlock(AvfxMagic.FadeOuterY, FadeOuterY) - .WriteBlock(AvfxMagic.FadeEnabledZ, FadeEnabledZ) - .WriteBlock(AvfxMagic.FadeInnerZ, FadeInnerZ) - .WriteBlock(AvfxMagic.FadeOuterZ, FadeOuterZ) - .WriteBlock(AvfxMagic.GlobalFogEnabled, GlobalFogEnabled) - .WriteBlock(AvfxMagic.GlobalFogInfluence, GlobalFogInfluence) - .WriteBlock(AvfxMagic.LtsEnabled, LtsEnabled) - .WriteBlock(AvfxMagic.NumSchedulers, (uint)Schedulers.Length) - .WriteBlock(AvfxMagic.NumTimelines, (uint)Timelines.Length) - .WriteBlock(AvfxMagic.NumEmitters, (uint)Emitters.Length) - .WriteBlock(AvfxMagic.NumParticles, (uint)Particles.Length) - .WriteBlock(AvfxMagic.NumEffectors, (uint)Effectors.Length) - .WriteBlock(AvfxMagic.NumBinders, (uint)Binders.Length) - .WriteBlock(AvfxMagic.NumTextures, (uint)Textures.Length) - .WriteBlock(AvfxMagic.NumModels, (uint)Models.Length); - foreach (var block in Schedulers) - w.WriteBlock(block); - foreach (var block in Timelines) - w.WriteBlock(block); - foreach (var block in Emitters) - w.WriteBlock(block); - foreach (var block in Particles) - w.WriteBlock(block); - foreach (var block in Effectors) - w.WriteBlock(block); - foreach (var block in Binders) - w.WriteBlock(block); - foreach (var texture in Textures) - w.WriteTextureBlock(texture); - foreach (var block in Models) - w.WriteBlock(block); - w.Seek((int)sizePos, SeekOrigin.Begin); - w.Write((uint)w.BaseStream.Length - 8u); - return m.ToArray(); - } -} diff --git a/Penumbra.GameData/Files/AvfxMagic.cs b/Penumbra.GameData/Files/AvfxMagic.cs deleted file mode 100644 index 0a78b4fb..00000000 --- a/Penumbra.GameData/Files/AvfxMagic.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.IO; -using System.Numerics; -using System.Text; - -// ReSharper disable ShiftExpressionZeroLeftOperand - -namespace Penumbra.GameData.Files; - -public static class AvfxMagic -{ - public const uint AvfxBase = ('A' << 24) | ('V' << 16) | ('F' << 8) | (uint)'X'; - public const uint Version = (000 << 24) | ('V' << 16) | ('e' << 8) | (uint)'r'; - public const uint IsDelayFastParticle = ('b' << 24) | ('D' << 16) | ('F' << 8) | (uint)'P'; - public const uint IsFitGround = (000 << 24) | ('b' << 16) | ('F' << 8) | (uint)'G'; - public const uint IsTransformSkip = (000 << 24) | ('b' << 16) | ('T' << 8) | (uint)'S'; - public const uint IsAllStopOnHide = ('b' << 24) | ('A' << 16) | ('S' << 8) | (uint)'H'; - public const uint CanBeClippedOut = ('b' << 24) | ('C' << 16) | ('B' << 8) | (uint)'C'; - public const uint ClipBoxEnabled = ('b' << 24) | ('C' << 16) | ('u' << 8) | (uint)'l'; - public const uint ClipBoxX = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'x'; - public const uint ClipBoxY = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'y'; - public const uint ClipBoxZ = ('C' << 24) | ('B' << 16) | ('P' << 8) | (uint)'z'; - public const uint ClipBoxSizeX = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'x'; - public const uint ClipBoxSizeY = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'y'; - public const uint ClipBoxSizeZ = ('C' << 24) | ('B' << 16) | ('S' << 8) | (uint)'z'; - public const uint BiasZmaxScale = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'s'; - public const uint BiasZmaxDistance = ('Z' << 24) | ('B' << 16) | ('M' << 8) | (uint)'d'; - public const uint IsCameraSpace = ('b' << 24) | ('C' << 16) | ('m' << 8) | (uint)'S'; - public const uint IsFullEnvLight = ('b' << 24) | ('F' << 16) | ('E' << 8) | (uint)'L'; - public const uint IsClipOwnSetting = ('b' << 24) | ('O' << 16) | ('S' << 8) | (uint)'t'; - public const uint NearClipBegin = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'B'; - public const uint NearClipEnd = (000 << 24) | ('N' << 16) | ('C' << 8) | (uint)'E'; - public const uint FarClipBegin = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'B'; - public const uint FarClipEnd = (000 << 24) | ('F' << 16) | ('C' << 8) | (uint)'E'; - public const uint SoftParticleFadeRange = ('S' << 24) | ('P' << 16) | ('F' << 8) | (uint)'R'; - public const uint SoftKeyOffset = (000 << 24) | ('S' << 16) | ('K' << 8) | (uint)'O'; - public const uint DrawLayerType = ('D' << 24) | ('w' << 16) | ('L' << 8) | (uint)'y'; - public const uint DrawOrderType = ('D' << 24) | ('w' << 16) | ('O' << 8) | (uint)'T'; - public const uint DirectionalLightSourceType = ('D' << 24) | ('L' << 16) | ('S' << 8) | (uint)'T'; - public const uint PointLightsType1 = ('P' << 24) | ('L' << 16) | ('1' << 8) | (uint)'S'; - public const uint PointLightsType2 = ('P' << 24) | ('L' << 16) | ('2' << 8) | (uint)'S'; - public const uint RevisedValuesPosX = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'x'; - public const uint RevisedValuesPosY = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'y'; - public const uint RevisedValuesPosZ = ('R' << 24) | ('v' << 16) | ('P' << 8) | (uint)'z'; - public const uint RevisedValuesRotX = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'x'; - public const uint RevisedValuesRotY = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'y'; - public const uint RevisedValuesRotZ = ('R' << 24) | ('v' << 16) | ('R' << 8) | (uint)'z'; - public const uint RevisedValuesScaleX = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'x'; - public const uint RevisedValuesScaleY = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'y'; - public const uint RevisedValuesScaleZ = ('R' << 24) | ('v' << 16) | ('S' << 8) | (uint)'z'; - public const uint RevisedValuesColorR = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'R'; - public const uint RevisedValuesColorG = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'G'; - public const uint RevisedValuesColorB = (000 << 24) | ('R' << 16) | ('v' << 8) | (uint)'B'; - public const uint FadeEnabledX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'e'; - public const uint FadeInnerX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'i'; - public const uint FadeOuterX = ('A' << 24) | ('F' << 16) | ('X' << 8) | (uint)'o'; - public const uint FadeEnabledY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'e'; - public const uint FadeInnerY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'i'; - public const uint FadeOuterY = ('A' << 24) | ('F' << 16) | ('Y' << 8) | (uint)'o'; - public const uint FadeEnabledZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'e'; - public const uint FadeInnerZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'i'; - public const uint FadeOuterZ = ('A' << 24) | ('F' << 16) | ('Z' << 8) | (uint)'o'; - public const uint GlobalFogEnabled = ('b' << 24) | ('G' << 16) | ('F' << 8) | (uint)'E'; - public const uint GlobalFogInfluence = ('G' << 24) | ('F' << 16) | ('I' << 8) | (uint)'M'; - public const uint LtsEnabled = ('b' << 24) | ('L' << 16) | ('T' << 8) | (uint)'S'; - public const uint NumSchedulers = ('S' << 24) | ('c' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumTimelines = ('T' << 24) | ('l' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumEmitters = ('E' << 24) | ('m' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumParticles = ('P' << 24) | ('r' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumEffectors = ('E' << 24) | ('f' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumBinders = ('B' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumTextures = ('T' << 24) | ('x' << 16) | ('C' << 8) | (uint)'n'; - public const uint NumModels = ('M' << 24) | ('d' << 16) | ('C' << 8) | (uint)'n'; - public const uint Scheduler = ('S' << 24) | ('c' << 16) | ('h' << 8) | (uint)'d'; - public const uint Timeline = ('T' << 24) | ('m' << 16) | ('L' << 8) | (uint)'n'; - public const uint Emitter = ('E' << 24) | ('m' << 16) | ('i' << 8) | (uint)'t'; - public const uint Particle = ('P' << 24) | ('t' << 16) | ('c' << 8) | (uint)'l'; - public const uint Effector = ('E' << 24) | ('f' << 16) | ('c' << 8) | (uint)'t'; - public const uint Binder = ('B' << 24) | ('i' << 16) | ('n' << 8) | (uint)'d'; - public const uint Texture = (000 << 24) | ('T' << 16) | ('e' << 8) | (uint)'x'; - public const uint Model = ('M' << 24) | ('o' << 16) | ('d' << 8) | (uint)'l'; - - internal static uint RoundTo4(this uint size) - { - var rest = size & 0b11u; - return rest > 0 ? (size & ~0b11u) + 4u : size; - } - - internal static BinaryWriter WriteTextureBlock(this BinaryWriter bw, string texture) - { - bw.Write(Texture); - var bytes = Encoding.UTF8.GetBytes(texture); - var size = (uint)bytes.Length + 1u; - bw.Write(size); - bw.Write(bytes); - bw.Write((byte)0); - for (var end = size.RoundTo4(); size < end; ++size) - bw.Write((byte)0); - return bw; - } - - internal static BinaryWriter WriteBlock(this BinaryWriter bw, AvfxFile.Block block) - { - bw.Write(block.Name); - bw.Write(block.Size); - bw.Write(block.Data); - return bw; - } - - internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, uint value) - { - if (value != uint.MaxValue) - { - bw.Write(magic); - bw.Write(4u); - bw.Write(value); - } - - return bw; - } - - internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, byte value) - { - if (value != byte.MaxValue) - { - bw.Write(magic); - bw.Write(4u); - bw.Write(value == 1 ? 1u : 0u); - } - - return bw; - } - - internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magic, float value) - { - if (!float.IsNaN(value)) - { - bw.Write(magic); - bw.Write(4u); - bw.Write(value); - } - - return bw; - } - - internal static BinaryWriter WriteBlock(this BinaryWriter bw, uint magicX, uint magicY, uint magicZ, Vector3 value) - => bw.WriteBlock(magicX, value.X) - .WriteBlock(magicY, value.Y) - .WriteBlock(magicZ, value.Z); -} diff --git a/Penumbra.GameData/Files/IWritable.cs b/Penumbra.GameData/Files/IWritable.cs deleted file mode 100644 index 0a170af9..00000000 --- a/Penumbra.GameData/Files/IWritable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Penumbra.GameData.Files; - -public interface IWritable -{ - public bool Valid { get; } - public byte[] Write(); -} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.Write.cs b/Penumbra.GameData/Files/MdlFile.Write.cs deleted file mode 100644 index 7db29954..00000000 --- a/Penumbra.GameData/Files/MdlFile.Write.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Lumina.Data.Parsing; - -namespace Penumbra.GameData.Files; - -public partial class MdlFile -{ - private static uint Write(BinaryWriter w, string s, long basePos) - { - var currentPos = w.BaseStream.Position; - w.Write(Encoding.UTF8.GetBytes(s)); - w.Write((byte)0); - return (uint)(currentPos - basePos); - } - - private List WriteStrings(BinaryWriter w) - { - var startPos = (int)w.BaseStream.Position; - var basePos = startPos + 8; - var count = (ushort)(Attributes.Length + Bones.Length + Materials.Length + Shapes.Length); - - w.Write(count); - w.Seek(basePos, SeekOrigin.Begin); - var ret = Attributes.Concat(Bones) - .Concat(Materials) - .Concat(Shapes.Select(s => s.ShapeName)) - .Select(attribute => Write(w, attribute, basePos)).ToList(); - - var padding = (w.BaseStream.Position & 0b111) > 0 ? (w.BaseStream.Position & ~0b111) + 8 : w.BaseStream.Position; - for (var i = w.BaseStream.Position; i < padding; ++i) - w.Write((byte)0); - var size = (int)w.BaseStream.Position - basePos; - w.Seek(startPos + 4, SeekOrigin.Begin); - w.Write((uint)size); - w.Seek(basePos + size, SeekOrigin.Begin); - return ret; - } - - private void WriteModelFileHeader(BinaryWriter w, uint runtimeSize) - { - w.Write(Version); - w.Write(StackSize); - w.Write(runtimeSize); - w.Write((ushort)VertexDeclarations.Length); - w.Write((ushort)Materials.Length); - w.Write(VertexOffset[0] > 0 ? VertexOffset[0] + runtimeSize : 0u); - w.Write(VertexOffset[1] > 0 ? VertexOffset[1] + runtimeSize : 0u); - w.Write(VertexOffset[2] > 0 ? VertexOffset[2] + runtimeSize : 0u); - w.Write(IndexOffset[0] > 0 ? IndexOffset[0] + runtimeSize : 0u); - w.Write(IndexOffset[1] > 0 ? IndexOffset[1] + runtimeSize : 0u); - w.Write(IndexOffset[2] > 0 ? IndexOffset[2] + runtimeSize : 0u); - w.Write(VertexBufferSize[0]); - w.Write(VertexBufferSize[1]); - w.Write(VertexBufferSize[2]); - w.Write(IndexBufferSize[0]); - w.Write(IndexBufferSize[1]); - w.Write(IndexBufferSize[2]); - w.Write(LodCount); - w.Write(EnableIndexBufferStreaming); - w.Write(EnableEdgeGeometry); - w.Write((byte)0); // Padding - } - - private void WriteModelHeader(BinaryWriter w) - { - w.Write(Radius); - w.Write((ushort)Meshes.Length); - w.Write((ushort)Attributes.Length); - w.Write((ushort)SubMeshes.Length); - w.Write((ushort)Materials.Length); - w.Write((ushort)Bones.Length); - w.Write((ushort)BoneTables.Length); - w.Write((ushort)Shapes.Length); - w.Write((ushort)ShapeMeshes.Length); - w.Write((ushort)ShapeValues.Length); - w.Write(LodCount); - w.Write((byte)Flags1); - w.Write((ushort)ElementIds.Length); - w.Write((byte)TerrainShadowMeshes.Length); - w.Write((byte)Flags2); - w.Write(ModelClipOutDistance); - w.Write(ShadowClipOutDistance); - w.Write(Unknown4); - w.Write((ushort)TerrainShadowSubMeshes.Length); - w.Write(Unknown5); - w.Write(BgChangeMaterialIndex); - w.Write(BgCrestChangeMaterialIndex); - w.Write(Unknown6); - w.Write(Unknown7); - w.Write(Unknown8); - w.Write(Unknown9); - w.Write((uint)0); // 6 byte padding - w.Write((ushort)0); - } - - - private static void Write(BinaryWriter w, in MdlStructs.VertexElement vertex) - { - w.Write(vertex.Stream); - w.Write(vertex.Offset); - w.Write(vertex.Type); - w.Write(vertex.Usage); - w.Write(vertex.UsageIndex); - w.Write((ushort)0); // 3 byte padding - w.Write((byte)0); - } - - private static void Write(BinaryWriter w, in MdlStructs.VertexDeclarationStruct vertexDecl) - { - foreach (var vertex in vertexDecl.VertexElements) - Write(w, vertex); - - Write(w, new MdlStructs.VertexElement() { Stream = 255 }); - w.Seek((int)(NumVertices - 1 - vertexDecl.VertexElements.Length) * 8, SeekOrigin.Current); - } - - private static void Write(BinaryWriter w, in MdlStructs.ElementIdStruct elementId) - { - w.Write(elementId.ElementId); - w.Write(elementId.ParentBoneName); - w.Write(elementId.Translate[0]); - w.Write(elementId.Translate[1]); - w.Write(elementId.Translate[2]); - w.Write(elementId.Rotate[0]); - w.Write(elementId.Rotate[1]); - w.Write(elementId.Rotate[2]); - } - - private static unsafe void Write(BinaryWriter w, in T data) where T : unmanaged - { - fixed (T* ptr = &data) - { - var bytePtr = (byte*)ptr; - var size = sizeof(T); - var span = new ReadOnlySpan(bytePtr, size); - w.Write(span); - } - } - - private static void Write(BinaryWriter w, MdlStructs.MeshStruct mesh) - { - w.Write(mesh.VertexCount); - w.Write((ushort)0); // padding - w.Write(mesh.IndexCount); - w.Write(mesh.MaterialIndex); - w.Write(mesh.SubMeshIndex); - w.Write(mesh.SubMeshCount); - w.Write(mesh.BoneTableIndex); - w.Write(mesh.StartIndex); - w.Write(mesh.VertexBufferOffset[0]); - w.Write(mesh.VertexBufferOffset[1]); - w.Write(mesh.VertexBufferOffset[2]); - w.Write(mesh.VertexBufferStride[0]); - w.Write(mesh.VertexBufferStride[1]); - w.Write(mesh.VertexBufferStride[2]); - w.Write(mesh.VertexStreamCount); - } - - private static void Write(BinaryWriter w, MdlStructs.BoneTableStruct bone) - { - foreach (var index in bone.BoneIndex) - w.Write(index); - - w.Write(bone.BoneCount); - w.Write((ushort)0); // 3 bytes padding - w.Write((byte)0); - } - - private void Write(BinaryWriter w, int shapeIdx, IReadOnlyList offsets) - { - var shape = Shapes[shapeIdx]; - var offset = offsets[Attributes.Length + Bones.Length + Materials.Length + shapeIdx]; - w.Write(offset); - w.Write(shape.ShapeMeshStartIndex[0]); - w.Write(shape.ShapeMeshStartIndex[1]); - w.Write(shape.ShapeMeshStartIndex[2]); - w.Write(shape.ShapeMeshCount[0]); - w.Write(shape.ShapeMeshCount[1]); - w.Write(shape.ShapeMeshCount[2]); - } - - private static void Write(BinaryWriter w, MdlStructs.BoundingBoxStruct box) - { - w.Write(box.Min[0]); - w.Write(box.Min[1]); - w.Write(box.Min[2]); - w.Write(box.Min[3]); - w.Write(box.Max[0]); - w.Write(box.Max[1]); - w.Write(box.Max[2]); - w.Write(box.Max[3]); - } - - public byte[] Write() - { - using var stream = new MemoryStream(); - using (var w = new BinaryWriter(stream)) - { - // Skip and write this later when we actually know it. - w.Seek((int)FileHeaderSize, SeekOrigin.Begin); - - foreach (var vertexDecl in VertexDeclarations) - Write(w, vertexDecl); - - var offsets = WriteStrings(w); - WriteModelHeader(w); - - foreach (var elementId in ElementIds) - Write(w, elementId); - - foreach (var lod in Lods) - Write(w, lod); - - if (Flags2.HasFlag(MdlStructs.ModelFlags2.ExtraLodEnabled)) - foreach (var extraLod in ExtraLods) - Write(w, extraLod); - - foreach (var mesh in Meshes) - Write(w, mesh); - - for (var i = 0; i < Attributes.Length; ++i) - w.Write(offsets[i]); - - foreach (var terrainShadowMesh in TerrainShadowMeshes) - Write(w, terrainShadowMesh); - - foreach (var subMesh in SubMeshes) - Write(w, subMesh); - - foreach (var terrainShadowSubMesh in TerrainShadowSubMeshes) - Write(w, terrainShadowSubMesh); - - for (var i = 0; i < Materials.Length; ++i) - w.Write(offsets[Attributes.Length + Bones.Length + i]); - - for (var i = 0; i < Bones.Length; ++i) - w.Write(offsets[Attributes.Length + i]); - - foreach (var boneTable in BoneTables) - Write(w, boneTable); - - for (var i = 0; i < Shapes.Length; ++i) - Write(w, i, offsets); - - foreach (var shapeMesh in ShapeMeshes) - Write(w, shapeMesh); - - foreach (var shapeValue in ShapeValues) - Write(w, shapeValue); - - w.Write(SubMeshBoneMap.Length * 2); - foreach (var bone in SubMeshBoneMap) - w.Write(bone); - - var pos = w.BaseStream.Position + 1; - var padding = (byte) (pos & 0b111); - if (padding > 0) - padding = (byte) (8 - padding); - w.Write(padding); - for (var i = 0; i < padding; ++i) - w.Write((byte) (0xDEADBEEFF00DCAFEu >> (8 * (7 - i)))); - - Write(w, BoundingBoxes); - Write(w, ModelBoundingBoxes); - Write(w, WaterBoundingBoxes); - Write(w, VerticalFogBoundingBoxes); - foreach (var box in BoneBoundingBoxes) - Write(w, box); - - var totalSize = w.BaseStream.Position; - var runtimeSize = (uint)(totalSize - StackSize - FileHeaderSize); - w.Write(RemainingData); - - // Write header data. - w.Seek(0, SeekOrigin.Begin); - WriteModelFileHeader(w, runtimeSize); - } - - return stream.ToArray(); - } -} diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs deleted file mode 100644 index 6cde07f5..00000000 --- a/Penumbra.GameData/Files/MdlFile.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Text; -using Lumina.Data; -using Lumina.Data.Parsing; -using Lumina.Extensions; - -namespace Penumbra.GameData.Files; - -public partial class MdlFile : IWritable -{ - public const uint NumVertices = 17; - public const uint FileHeaderSize = 0x44; - - // Refers to string, thus not Lumina struct. - public struct Shape - { - public string ShapeName = string.Empty; - public ushort[] ShapeMeshStartIndex; - public ushort[] ShapeMeshCount; - - public Shape(MdlStructs.ShapeStruct data, uint[] offsets, string[] strings) - { - var idx = offsets.AsSpan().IndexOf(data.StringOffset); - ShapeName = idx >= 0 ? strings[idx] : string.Empty; - ShapeMeshStartIndex = data.ShapeMeshStartIndex; - ShapeMeshCount = data.ShapeMeshCount; - } - } - - // Raw data to write back. - public uint Version; - public float Radius; - public float ModelClipOutDistance; - public float ShadowClipOutDistance; - public byte BgChangeMaterialIndex; - public byte BgCrestChangeMaterialIndex; - public ushort Unknown4; - public byte Unknown5; - public byte Unknown6; - public ushort Unknown7; - public ushort Unknown8; - public ushort Unknown9; - - // Offsets are stored relative to RuntimeSize instead of file start. - public uint[] VertexOffset; - public uint[] IndexOffset; - - public uint[] VertexBufferSize; - public uint[] IndexBufferSize; - public byte LodCount; - public bool EnableIndexBufferStreaming; - public bool EnableEdgeGeometry; - - - public MdlStructs.ModelFlags1 Flags1; - public MdlStructs.ModelFlags2 Flags2; - - public MdlStructs.BoundingBoxStruct BoundingBoxes; - public MdlStructs.BoundingBoxStruct ModelBoundingBoxes; - public MdlStructs.BoundingBoxStruct WaterBoundingBoxes; - public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes; - - public MdlStructs.VertexDeclarationStruct[] VertexDeclarations; - public MdlStructs.ElementIdStruct[] ElementIds; - public MdlStructs.MeshStruct[] Meshes; - public MdlStructs.BoneTableStruct[] BoneTables; - public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes; - public MdlStructs.SubmeshStruct[] SubMeshes; - public MdlStructs.ShapeMeshStruct[] ShapeMeshes; - public MdlStructs.ShapeValueStruct[] ShapeValues; - public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes; - public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubMeshes; - public MdlStructs.LodStruct[] Lods; - public MdlStructs.ExtraLodStruct[] ExtraLods; - public ushort[] SubMeshBoneMap; - - // Strings are written in order - public string[] Attributes; - public string[] Bones; - public string[] Materials; - public Shape[] Shapes; - - // Raw, unparsed data. - public byte[] RemainingData; - - public bool Valid { get; } - - public MdlFile(byte[] data) - { - using var stream = new MemoryStream(data); - using var r = new LuminaBinaryReader(stream); - - var header = LoadModelFileHeader(r); - LodCount = header.LodCount; - VertexBufferSize = header.VertexBufferSize; - IndexBufferSize = header.IndexBufferSize; - VertexOffset = header.VertexOffset; - IndexOffset = header.IndexOffset; - for (var i = 0; i < 3; ++i) - { - if (VertexOffset[i] > 0) - VertexOffset[i] -= header.RuntimeSize; - - if (IndexOffset[i] > 0) - IndexOffset[i] -= header.RuntimeSize; - } - - VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount]; - for (var i = 0; i < header.VertexDeclarationCount; ++i) - VertexDeclarations[i] = MdlStructs.VertexDeclarationStruct.Read(r); - - var (offsets, strings) = LoadStrings(r); - - var modelHeader = LoadModelHeader(r); - ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount]; - for (var i = 0; i < modelHeader.ElementIdCount; i++) - ElementIds[i] = MdlStructs.ElementIdStruct.Read(r); - - Lods = r.ReadStructuresAsArray(3); - ExtraLods = modelHeader.ExtraLodEnabled - ? r.ReadStructuresAsArray(3) - : Array.Empty(); - - Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount]; - for (var i = 0; i < modelHeader.MeshCount; i++) - Meshes[i] = MdlStructs.MeshStruct.Read(r); - - Attributes = new string[modelHeader.AttributeCount]; - for (var i = 0; i < modelHeader.AttributeCount; ++i) - { - var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf(offset); - Attributes[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; - } - - TerrainShadowMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowMeshCount); - SubMeshes = r.ReadStructuresAsArray(modelHeader.SubmeshCount); - TerrainShadowSubMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowSubmeshCount); - - Materials = new string[modelHeader.MaterialCount]; - for (var i = 0; i < modelHeader.MaterialCount; ++i) - { - var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf(offset); - Materials[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; - } - - Bones = new string[modelHeader.BoneCount]; - for (var i = 0; i < modelHeader.BoneCount; ++i) - { - var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf(offset); - Bones[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; - } - - BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount]; - for (var i = 0; i < modelHeader.BoneTableCount; i++) - BoneTables[i] = MdlStructs.BoneTableStruct.Read(r); - - Shapes = new Shape[modelHeader.ShapeCount]; - for (var i = 0; i < modelHeader.ShapeCount; i++) - Shapes[i] = new Shape(MdlStructs.ShapeStruct.Read(r), offsets, strings); - - ShapeMeshes = r.ReadStructuresAsArray(modelHeader.ShapeMeshCount); - ShapeValues = r.ReadStructuresAsArray(modelHeader.ShapeValueCount); - - var submeshBoneMapSize = r.ReadUInt32(); - SubMeshBoneMap = r.ReadStructures((int)submeshBoneMapSize / 2).ToArray(); - - var paddingAmount = r.ReadByte(); - r.Seek(r.BaseStream.Position + paddingAmount); - - // Dunno what this first one is for? - BoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); - ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); - WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); - VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); - BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount]; - for (var i = 0; i < modelHeader.BoneCount; i++) - BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r); - - var runtimePadding = header.RuntimeSize + FileHeaderSize + header.StackSize - r.BaseStream.Position; - if (runtimePadding > 0) - r.ReadBytes((int)runtimePadding); - RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position)); - Valid = true; - } - - private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) - { - var header = MdlStructs.ModelFileHeader.Read(r); - Version = header.Version; - EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; - EnableEdgeGeometry = header.EnableEdgeGeometry; - return header; - } - - private MdlStructs.ModelHeader LoadModelHeader(BinaryReader r) - { - var modelHeader = r.ReadStructure(); - Radius = modelHeader.Radius; - Flags1 = (MdlStructs.ModelFlags1)(modelHeader.GetType() - .GetField("Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) - ?? 0); - Flags2 = (MdlStructs.ModelFlags2)(modelHeader.GetType() - .GetField("Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) - ?? 0); - ModelClipOutDistance = modelHeader.ModelClipOutDistance; - ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; - Unknown4 = modelHeader.Unknown4; - Unknown5 = (byte)(modelHeader.GetType() - .GetField("Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) - ?? 0); - Unknown6 = modelHeader.Unknown6; - Unknown7 = modelHeader.Unknown7; - Unknown8 = modelHeader.Unknown8; - Unknown9 = modelHeader.Unknown9; - BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; - BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; - - return modelHeader; - } - - private static (uint[], string[]) LoadStrings(BinaryReader r) - { - var stringCount = r.ReadUInt16(); - r.ReadUInt16(); - var stringSize = (int)r.ReadUInt32(); - var stringData = r.ReadBytes(stringSize); - var start = 0; - var strings = new string[stringCount]; - var offsets = new uint[stringCount]; - for (var i = 0; i < stringCount; ++i) - { - var span = stringData.AsSpan(start); - var idx = span.IndexOf((byte)'\0'); - strings[i] = Encoding.UTF8.GetString(span[..idx]); - offsets[i] = (uint)start; - start = start + idx + 1; - } - - return (offsets, strings); - } - - public unsafe uint StackSize - => (uint)(VertexDeclarations.Length * NumVertices * sizeof(MdlStructs.VertexElement)); -} diff --git a/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs b/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs deleted file mode 100644 index 4cd2ff28..00000000 --- a/Penumbra.GameData/Files/MtrlFile.ColorDyeSet.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Penumbra.GameData.Files; - -public partial class MtrlFile -{ - public unsafe struct ColorDyeSet - { - public struct Row - { - private ushort _data; - - public ushort Template - { - get => (ushort)(_data >> 5); - set => _data = (ushort)((_data & 0x1F) | (value << 5)); - } - - public bool Diffuse - { - get => (_data & 0x01) != 0; - set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); - } - - public bool Specular - { - get => (_data & 0x02) != 0; - set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); - } - - public bool Emissive - { - get => (_data & 0x04) != 0; - set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); - } - - public bool Gloss - { - get => (_data & 0x08) != 0; - set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); - } - - public bool SpecularStrength - { - get => (_data & 0x10) != 0; - set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); - } - } - - public struct RowArray : IEnumerable - { - public const int NumRows = 16; - private fixed ushort _rowData[NumRows]; - - public ref Row this[int i] - { - get - { - fixed (ushort* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ReadOnlySpan AsBytes() - { - fixed (ushort* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); - } - } - } - - public RowArray Rows; - public string Name; - public ushort Index; - } -} diff --git a/Penumbra.GameData/Files/MtrlFile.ColorSet.cs b/Penumbra.GameData/Files/MtrlFile.ColorSet.cs deleted file mode 100644 index 61647d79..00000000 --- a/Penumbra.GameData/Files/MtrlFile.ColorSet.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Numerics; - -namespace Penumbra.GameData.Files; - -public partial class MtrlFile -{ - public unsafe struct ColorSet - { - public struct Row - { - public const int Size = 32; - - private fixed ushort _data[16]; - - public Vector3 Diffuse - { - get => new(ToFloat(0), ToFloat(1), ToFloat(2)); - set - { - _data[0] = FromFloat(value.X); - _data[1] = FromFloat(value.Y); - _data[2] = FromFloat(value.Z); - } - } - - public Vector3 Specular - { - get => new(ToFloat(4), ToFloat(5), ToFloat(6)); - set - { - _data[4] = FromFloat(value.X); - _data[5] = FromFloat(value.Y); - _data[6] = FromFloat(value.Z); - } - } - - public Vector3 Emissive - { - get => new(ToFloat(8), ToFloat(9), ToFloat(10)); - set - { - _data[8] = FromFloat(value.X); - _data[9] = FromFloat(value.Y); - _data[10] = FromFloat(value.Z); - } - } - - public Vector2 MaterialRepeat - { - get => new(ToFloat(12), ToFloat(15)); - set - { - _data[12] = FromFloat(value.X); - _data[15] = FromFloat(value.Y); - } - } - - public Vector2 MaterialSkew - { - get => new(ToFloat(13), ToFloat(14)); - set - { - _data[13] = FromFloat(value.X); - _data[14] = FromFloat(value.Y); - } - } - - public float SpecularStrength - { - get => ToFloat(3); - set => _data[3] = FromFloat(value); - } - - public float GlossStrength - { - get => ToFloat(7); - set => _data[7] = FromFloat(value); - } - - public ushort TileSet - { - get => (ushort)(ToFloat(11) * 64f); - set => _data[11] = FromFloat(value / 64f); - } - - private float ToFloat(int idx) - => (float)BitConverter.UInt16BitsToHalf(_data[idx]); - - private static ushort FromFloat(float x) - => BitConverter.HalfToUInt16Bits((Half)x); - } - - public struct RowArray : IEnumerable - { - public const int NumRows = 16; - private fixed byte _rowData[NumRows * Row.Size]; - - public ref Row this[int i] - { - get - { - fixed (byte* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ReadOnlySpan AsBytes() - { - fixed (byte* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * Row.Size); - } - } - } - - public RowArray Rows; - public string Name; - public ushort Index; - public bool HasRows; - } -} diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs deleted file mode 100644 index 9bc5a2ce..00000000 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.IO; -using System.Linq; -using System.Text; - -namespace Penumbra.GameData.Files; - -public partial class MtrlFile -{ - public byte[] Write() - { - using var stream = new MemoryStream(); - using( var w = new BinaryWriter( stream ) ) - { - const int materialHeaderSize = 4 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1; - - w.BaseStream.Seek( materialHeaderSize, SeekOrigin.Begin ); - ushort cumulativeStringOffset = 0; - foreach( var texture in Textures ) - { - w.Write( cumulativeStringOffset ); - w.Write( texture.Flags ); - cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 ); - } - - foreach( var set in UvSets ) - { - w.Write( cumulativeStringOffset ); - w.Write( set.Index ); - cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); - } - - foreach( var set in ColorSets ) - { - w.Write( cumulativeStringOffset ); - w.Write( set.Index ); - cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); - } - - foreach( var text in Textures.Select( t => t.Path ) - .Concat( UvSets.Select( c => c.Name ) ) - .Concat( ColorSets.Select( c => c.Name ) ) - .Append( ShaderPackage.Name ) ) - { - w.Write( Encoding.UTF8.GetBytes( text ) ); - w.Write( ( byte )'\0' ); - } - - w.Write( AdditionalData ); - var dataSetSize = 0; - foreach( var row in ColorSets.Where( c => c.HasRows ).Select( c => c.Rows ) ) - { - var span = row.AsBytes(); - w.Write( span ); - dataSetSize += span.Length; - } - - foreach( var row in ColorDyeSets.Select( c => c.Rows ) ) - { - var span = row.AsBytes(); - w.Write( span ); - dataSetSize += span.Length; - } - - w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); - w.Write( ( ushort )ShaderPackage.ShaderKeys.Length ); - w.Write( ( ushort )ShaderPackage.Constants.Length ); - w.Write( ( ushort )ShaderPackage.Samplers.Length ); - w.Write( ShaderPackage.Flags ); - - foreach( var key in ShaderPackage.ShaderKeys ) - { - w.Write( key.Category ); - w.Write( key.Value ); - } - - foreach( var constant in ShaderPackage.Constants ) - { - w.Write( constant.Id ); - w.Write( constant.ByteOffset ); - w.Write( constant.ByteSize ); - } - - foreach( var sampler in ShaderPackage.Samplers ) - { - w.Write( sampler.SamplerId ); - w.Write( sampler.Flags ); - w.Write( sampler.TextureIndex ); - w.Write( ( ushort )0 ); - w.Write( ( byte )0 ); - } - - foreach( var value in ShaderPackage.ShaderValues ) - { - w.Write( value ); - } - - WriteHeader( w, ( ushort )w.BaseStream.Position, dataSetSize, cumulativeStringOffset ); - } - - return stream.ToArray(); - } - - private void WriteHeader( BinaryWriter w, ushort fileSize, int dataSetSize, ushort shaderPackageNameOffset ) - { - w.BaseStream.Seek( 0, SeekOrigin.Begin ); - w.Write( Version ); - w.Write( fileSize ); - w.Write( ( ushort )dataSetSize ); - w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); - w.Write( shaderPackageNameOffset ); - w.Write( ( byte )Textures.Length ); - w.Write( ( byte )UvSets.Length ); - w.Write( ( byte )ColorSets.Length ); - w.Write( ( byte )AdditionalData.Length ); - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs deleted file mode 100644 index c1c683e7..00000000 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Lumina.Data.Parsing; -using Lumina.Extensions; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Files; - -public partial class MtrlFile : IWritable -{ - public readonly uint Version; - - public bool Valid - => CheckTextures(); - - public Texture[] Textures; - public UvSet[] UvSets; - public ColorSet[] ColorSets; - public ColorDyeSet[] ColorDyeSets; - public ShaderPackageData ShaderPackage; - public byte[] AdditionalData; - - public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) - { - if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) - return false; - - var dyeSet = ColorDyeSets[colorSetIdx].Rows[rowIdx]; - if (!stm.TryGetValue(dyeSet.Template, stainId, out var dyes)) - return false; - - var ret = false; - if (dyeSet.Diffuse && ColorSets[colorSetIdx].Rows[rowIdx].Diffuse != dyes.Diffuse) - { - ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = dyes.Diffuse; - ret = true; - } - - if (dyeSet.Specular && ColorSets[colorSetIdx].Rows[rowIdx].Specular != dyes.Specular) - { - ColorSets[colorSetIdx].Rows[rowIdx].Specular = dyes.Specular; - ret = true; - } - - if (dyeSet.SpecularStrength && ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength != dyes.SpecularPower) - { - ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = dyes.SpecularPower; - ret = true; - } - - if (dyeSet.Emissive && ColorSets[colorSetIdx].Rows[rowIdx].Emissive != dyes.Emissive) - { - ColorSets[colorSetIdx].Rows[rowIdx].Emissive = dyes.Emissive; - ret = true; - } - - if (dyeSet.Gloss && ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength != dyes.Gloss) - { - ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = dyes.Gloss; - ret = true; - } - - return ret; - } - - public Span GetConstantValues(Constant constant) - { - if ((constant.ByteOffset & 0x3) != 0 - || (constant.ByteSize & 0x3) != 0 - || (constant.ByteOffset + constant.ByteSize) >> 2 > ShaderPackage.ShaderValues.Length) - return null; - - return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2); - - } - - public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture(ShpkFile? shpk) - { - var samplers = new List<(Sampler?, ShpkFile.Resource?)>(); - for (var i = 0; i < Textures.Length; ++i) - { - samplers.Add((null, null)); - } - foreach (var sampler in ShaderPackage.Samplers) - { - samplers[sampler.TextureIndex] = (sampler, shpk?.GetSamplerById(sampler.SamplerId)); - } - - return samplers; - } - - public MtrlFile(byte[] data) - { - using var stream = new MemoryStream(data); - using var r = new BinaryReader(stream); - - Version = r.ReadUInt32(); - r.ReadUInt16(); // file size - var dataSetSize = r.ReadUInt16(); - var stringTableSize = r.ReadUInt16(); - var shaderPackageNameOffset = r.ReadUInt16(); - var textureCount = r.ReadByte(); - var uvSetCount = r.ReadByte(); - var colorSetCount = r.ReadByte(); - var additionalDataSize = r.ReadByte(); - - Textures = ReadTextureOffsets(r, textureCount, out var textureOffsets); - UvSets = ReadUvSetOffsets(r, uvSetCount, out var uvOffsets); - ColorSets = ReadColorSetOffsets(r, colorSetCount, out var colorOffsets); - - var strings = r.ReadBytes(stringTableSize); - for (var i = 0; i < textureCount; ++i) - Textures[i].Path = UseOffset(strings, textureOffsets[i]); - - for (var i = 0; i < uvSetCount; ++i) - UvSets[i].Name = UseOffset(strings, uvOffsets[i]); - - for (var i = 0; i < colorSetCount; ++i) - ColorSets[i].Name = UseOffset(strings, colorOffsets[i]); - - ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize - ? ColorSets.Select(c => new ColorDyeSet - { - Index = c.Index, - Name = c.Name, - }).ToArray() - : Array.Empty(); - - ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset); - - AdditionalData = r.ReadBytes(additionalDataSize); - for (var i = 0; i < ColorSets.Length; ++i) - { - if (stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length) - { - ColorSets[i].Rows = r.ReadStructure(); - ColorSets[i].HasRows = true; - } - else - { - ColorSets[i].HasRows = false; - } - } - - for (var i = 0; i < ColorDyeSets.Length; ++i) - ColorDyeSets[i].Rows = r.ReadStructure(); - - var shaderValueListSize = r.ReadUInt16(); - var shaderKeyCount = r.ReadUInt16(); - var constantCount = r.ReadUInt16(); - var samplerCount = r.ReadUInt16(); - ShaderPackage.Flags = r.ReadUInt32(); - - ShaderPackage.ShaderKeys = r.ReadStructuresAsArray(shaderKeyCount); - ShaderPackage.Constants = r.ReadStructuresAsArray(constantCount); - ShaderPackage.Samplers = r.ReadStructuresAsArray(samplerCount); - ShaderPackage.ShaderValues = r.ReadStructuresAsArray(shaderValueListSize / 4); - } - - private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets) - { - var ret = new Texture[count]; - offsets = new ushort[count]; - for (var i = 0; i < count; ++i) - { - offsets[i] = r.ReadUInt16(); - ret[i].Flags = r.ReadUInt16(); - } - - return ret; - } - - private static UvSet[] ReadUvSetOffsets(BinaryReader r, int count, out ushort[] offsets) - { - var ret = new UvSet[count]; - offsets = new ushort[count]; - for (var i = 0; i < count; ++i) - { - offsets[i] = r.ReadUInt16(); - ret[i].Index = r.ReadUInt16(); - } - - return ret; - } - - private static ColorSet[] ReadColorSetOffsets(BinaryReader r, int count, out ushort[] offsets) - { - var ret = new ColorSet[count]; - offsets = new ushort[count]; - for (var i = 0; i < count; ++i) - { - offsets[i] = r.ReadUInt16(); - ret[i].Index = r.ReadUInt16(); - } - - return ret; - } - - private static string UseOffset(ReadOnlySpan strings, ushort offset) - { - strings = strings[offset..]; - var end = strings.IndexOf((byte)'\0'); - return Encoding.UTF8.GetString(end == -1 ? strings : strings[..end]); - } - - private bool CheckTextures() - => Textures.All(texture => texture.Path.Contains('/')); - - public struct UvSet - { - public string Name; - public ushort Index; - } - - public struct Texture - { - public string Path; - public ushort Flags; - - public bool DX11 - => (Flags & 0x8000) != 0; - } - - public struct Constant - { - public uint Id; - public ushort ByteOffset; - public ushort ByteSize; - } - - public struct ShaderPackageData - { - public string Name; - public ShaderKey[] ShaderKeys; - public Constant[] Constants; - public Sampler[] Samplers; - public float[] ShaderValues; - public uint Flags; - } -} diff --git a/Penumbra.GameData/Files/ShpkFile.Shader.cs b/Penumbra.GameData/Files/ShpkFile.Shader.cs deleted file mode 100644 index 3d94dbb4..00000000 --- a/Penumbra.GameData/Files/ShpkFile.Shader.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using Lumina.Misc; -using Penumbra.GameData.Data; - -namespace Penumbra.GameData.Files; - -public partial class ShpkFile -{ - public struct Shader - { - public DisassembledShader.ShaderStage Stage; - public DxVersion DirectXVersion; - public Resource[] Constants; - public Resource[] Samplers; - public Resource[] Uavs; - public byte[] AdditionalHeader; - private byte[] _byteData; - private DisassembledShader? _disassembly; - - public byte[] Blob - { - get => _byteData; - set - { - if (_byteData == value) - return; - - if (Stage != DisassembledShader.ShaderStage.Unspecified) - { - // Reject the blob entirely if we can't disassemble it or if we find inconsistencies. - var disasm = DisassembledShader.Disassemble(value); - if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion) - throw new ArgumentException( - $"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.", - nameof(value)); - - if (disasm.ShaderModel >= 0x0500) - { - var samplers = new Dictionary(); - var textures = new Dictionary(); - foreach (var binding in disasm.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.Texture: - textures[binding.Slot] = NormalizeResourceName(binding.Name); - break; - case DisassembledShader.ResourceType.Sampler: - samplers[binding.Slot] = NormalizeResourceName(binding.Name); - break; - } - } - - if (samplers.Count != textures.Count - || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName)) - throw new ArgumentException($"The supplied blob has inconsistent sampler and texture allocation."); - } - - _byteData = value; - _disassembly = disasm; - } - else - { - _byteData = value; - _disassembly = null; - } - - UpdateUsed(); - } - } - - public DisassembledShader? Disassembly - => _disassembly; - - public Resource? GetConstantById(uint id) - => Constants.FirstOrNull(res => res.Id == id); - - public Resource? GetConstantByName(string name) - => Constants.FirstOrNull(res => res.Name == name); - - public Resource? GetSamplerById(uint id) - => Samplers.FirstOrNull(s => s.Id == id); - - public Resource? GetSamplerByName(string name) - => Samplers.FirstOrNull(s => s.Name == name); - - public Resource? GetUavById(uint id) - => Uavs.FirstOrNull(u => u.Id == id); - - public Resource? GetUavByName(string name) - => Uavs.FirstOrNull(u => u.Name == name); - - public void UpdateResources(ShpkFile file) - { - if (_disassembly == null) - throw new InvalidOperationException(); - - var constants = new List(); - var samplers = new List(); - var uavs = new List(); - foreach (var binding in _disassembly.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.ConstantBuffer: - var name = NormalizeResourceName(binding.Name); - // We want to preserve IDs as much as possible, and to deterministically generate new ones in a way that's most compliant with the native ones, to maximize compatibility. - var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - constants.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.RegisterCount, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - case DisassembledShader.ResourceType.Texture: - name = NormalizeResourceName(binding.Name); - id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - samplers.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.Slot, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - case DisassembledShader.ResourceType.Uav: - name = NormalizeResourceName(binding.Name); - id = GetUavByName(name)?.Id ?? file.GetUavByName(name)?.Id ?? Crc32.Get(name, 0xFFFFFFFFu); - uavs.Add(new Resource - { - Id = id, - Name = name, - Slot = (ushort)binding.Slot, - Size = (ushort)binding.Slot, - Used = binding.Used, - UsedDynamically = binding.UsedDynamically, - }); - break; - } - } - - Constants = constants.ToArray(); - Samplers = samplers.ToArray(); - Uavs = uavs.ToArray(); - } - - private void UpdateUsed() - { - if (_disassembly != null) - { - var cbUsage = new Dictionary(); - var tUsage = new Dictionary(); - var uUsage = new Dictionary(); - foreach (var binding in _disassembly.ResourceBindings) - { - switch (binding.Type) - { - case DisassembledShader.ResourceType.ConstantBuffer: - cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - case DisassembledShader.ResourceType.Texture: - tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - case DisassembledShader.ResourceType.Uav: - uUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically); - break; - } - } - - static void CopyUsed(Resource[] resources, - Dictionary used) - { - for (var i = 0; i < resources.Length; ++i) - { - if (used.TryGetValue(resources[i].Name, out var usage)) - { - resources[i].Used = usage.Item1; - resources[i].UsedDynamically = usage.Item2; - } - else - { - resources[i].Used = null; - resources[i].UsedDynamically = null; - } - } - } - - CopyUsed(Constants, cbUsage); - CopyUsed(Samplers, tUsage); - CopyUsed(Uavs, uUsage); - } - else - { - ClearUsed(Constants); - ClearUsed(Samplers); - ClearUsed(Uavs); - } - } - - private static string NormalizeResourceName(string resourceName) - { - var dot = resourceName.IndexOf('.'); - if (dot >= 0) - return resourceName[..dot]; - if (resourceName.Length > 1 && resourceName[^2] is '_' && resourceName[^1] is 'S' or 'T') - return resourceName[..^2]; - - return resourceName; - } - } -} diff --git a/Penumbra.GameData/Files/ShpkFile.StringPool.cs b/Penumbra.GameData/Files/ShpkFile.StringPool.cs deleted file mode 100644 index bad56d20..00000000 --- a/Penumbra.GameData/Files/ShpkFile.StringPool.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Penumbra.GameData.Files; - -public partial class ShpkFile -{ - public class StringPool - { - public MemoryStream Data; - public List StartingOffsets; - - public StringPool(ReadOnlySpan bytes) - { - Data = new MemoryStream(); - Data.Write(bytes); - StartingOffsets = new List - { - 0, - }; - for (var i = 0; i < bytes.Length; ++i) - { - if (bytes[i] == 0) - StartingOffsets.Add(i + 1); - } - - if (StartingOffsets[^1] == bytes.Length) - StartingOffsets.RemoveAt(StartingOffsets.Count - 1); - else - Data.WriteByte(0); - } - - public string GetString(int offset, int size) - => Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size)); - - public string GetNullTerminatedString(int offset) - { - var str = Data.GetBuffer().AsSpan()[offset..]; - var size = str.IndexOf((byte)0); - if (size >= 0) - str = str[..size]; - return Encoding.UTF8.GetString(str); - } - - public (int, int) FindOrAddString(string str) - { - var dataSpan = Data.GetBuffer().AsSpan(); - var bytes = Encoding.UTF8.GetBytes(str); - foreach (var offset in StartingOffsets) - { - if (offset + bytes.Length > Data.Length) - break; - - var strSpan = dataSpan[offset..]; - var match = true; - for (var i = 0; i < bytes.Length; ++i) - { - if (strSpan[i] != bytes[i]) - { - match = false; - break; - } - } - - if (match && strSpan[bytes.Length] == 0) - return (offset, bytes.Length); - } - - Data.Seek(0L, SeekOrigin.End); - var newOffset = (int)Data.Position; - StartingOffsets.Add(newOffset); - Data.Write(bytes); - Data.WriteByte(0); - return (newOffset, bytes.Length); - } - } -} diff --git a/Penumbra.GameData/Files/ShpkFile.Write.cs b/Penumbra.GameData/Files/ShpkFile.Write.cs deleted file mode 100644 index 117ea5e5..00000000 --- a/Penumbra.GameData/Files/ShpkFile.Write.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.IO; - -namespace Penumbra.GameData.Files; - -public partial class ShpkFile -{ - public byte[] Write() - { - if (SubViewKeys.Length != 2) - throw new InvalidDataException(); - - using var stream = new MemoryStream(); - using var blobs = new MemoryStream(); - var strings = new StringPool(ReadOnlySpan.Empty); - using (var w = new BinaryWriter(stream)) - { - w.Write(ShPkMagic); - w.Write(Version); - w.Write(DirectXVersion switch - { - DxVersion.DirectX9 => Dx9Magic, - DxVersion.DirectX11 => Dx11Magic, - _ => throw new NotImplementedException(), - }); - var offsetsPosition = stream.Position; - w.Write(0u); // Placeholder for file size - w.Write(0u); // Placeholder for blobs offset - w.Write(0u); // Placeholder for strings offset - w.Write((uint)VertexShaders.Length); - w.Write((uint)PixelShaders.Length); - w.Write(MaterialParamsSize); - w.Write((uint)MaterialParams.Length); - w.Write((uint)Constants.Length); - w.Write((uint)Samplers.Length); - w.Write((uint)Uavs.Length); - w.Write((uint)SystemKeys.Length); - w.Write((uint)SceneKeys.Length); - w.Write((uint)MaterialKeys.Length); - w.Write((uint)Nodes.Length); - w.Write((uint)Items.Length); - - WriteShaderArray(w, VertexShaders, blobs, strings); - WriteShaderArray(w, PixelShaders, blobs, strings); - - foreach (var materialParam in MaterialParams) - { - w.Write(materialParam.Id); - w.Write(materialParam.ByteOffset); - w.Write(materialParam.ByteSize); - } - - WriteResourceArray(w, Constants, strings); - WriteResourceArray(w, Samplers, strings); - WriteResourceArray(w, Uavs, strings); - - foreach (var key in SystemKeys) - { - w.Write(key.Id); - w.Write(key.DefaultValue); - } - - foreach (var key in SceneKeys) - { - w.Write(key.Id); - w.Write(key.DefaultValue); - } - - foreach (var key in MaterialKeys) - { - w.Write(key.Id); - w.Write(key.DefaultValue); - } - - foreach (var key in SubViewKeys) - w.Write(key.DefaultValue); - - foreach (var node in Nodes) - { - if (node.PassIndices.Length != 16 - || node.SystemKeys.Length != SystemKeys.Length - || node.SceneKeys.Length != SceneKeys.Length - || node.MaterialKeys.Length != MaterialKeys.Length - || node.SubViewKeys.Length != SubViewKeys.Length) - throw new InvalidDataException(); - - w.Write(node.Id); - w.Write(node.Passes.Length); - w.Write(node.PassIndices); - foreach (var key in node.SystemKeys) - w.Write(key); - foreach (var key in node.SceneKeys) - w.Write(key); - foreach (var key in node.MaterialKeys) - w.Write(key); - foreach (var key in node.SubViewKeys) - w.Write(key); - foreach (var pass in node.Passes) - { - w.Write(pass.Id); - w.Write(pass.VertexShader); - w.Write(pass.PixelShader); - } - } - - foreach (var item in Items) - { - w.Write(item.Id); - w.Write(item.Node); - } - - w.Write(AdditionalData); - - var blobsOffset = (int)stream.Position; - blobs.WriteTo(stream); - - var stringsOffset = (int)stream.Position; - strings.Data.WriteTo(stream); - - var fileSize = (int)stream.Position; - - stream.Seek(offsetsPosition, SeekOrigin.Begin); - w.Write(fileSize); - w.Write(blobsOffset); - w.Write(stringsOffset); - } - - return stream.ToArray(); - } - - private static void WriteResourceArray(BinaryWriter w, Resource[] array, StringPool strings) - { - foreach (var buf in array) - { - var (strOffset, strSize) = strings.FindOrAddString(buf.Name); - w.Write(buf.Id); - w.Write(strOffset); - w.Write(strSize); - w.Write(buf.Slot); - w.Write(buf.Size); - } - } - - private static void WriteShaderArray(BinaryWriter w, Shader[] array, MemoryStream blobs, StringPool strings) - { - foreach (var shader in array) - { - var blobOffset = (int)blobs.Position; - blobs.Write(shader.AdditionalHeader); - blobs.Write(shader.Blob); - var blobSize = (int)blobs.Position - blobOffset; - - w.Write(blobOffset); - w.Write(blobSize); - w.Write((ushort)shader.Constants.Length); - w.Write((ushort)shader.Samplers.Length); - w.Write((ushort)shader.Uavs.Length); - w.Write((ushort)0); - - WriteResourceArray(w, shader.Constants, strings); - WriteResourceArray(w, shader.Samplers, strings); - WriteResourceArray(w, shader.Uavs, strings); - } - } -} diff --git a/Penumbra.GameData/Files/ShpkFile.cs b/Penumbra.GameData/Files/ShpkFile.cs deleted file mode 100644 index 5b88bb00..00000000 --- a/Penumbra.GameData/Files/ShpkFile.cs +++ /dev/null @@ -1,486 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Lumina.Extensions; -using Penumbra.GameData.Data; - -namespace Penumbra.GameData.Files; - -public partial class ShpkFile : IWritable -{ - private const uint ShPkMagic = 0x6B506853u; // bytes of ShPk - private const uint Dx9Magic = 0x00395844u; // bytes of DX9\0 - private const uint Dx11Magic = 0x31315844u; // bytes of DX11 - - public const uint MaterialParamsConstantId = 0x64D12851u; - - public uint Version; - public DxVersion DirectXVersion; - public Shader[] VertexShaders; - public Shader[] PixelShaders; - public uint MaterialParamsSize; - public MaterialParam[] MaterialParams; - public Resource[] Constants; - public Resource[] Samplers; - public Resource[] Uavs; - public Key[] SystemKeys; - public Key[] SceneKeys; - public Key[] MaterialKeys; - public Key[] SubViewKeys; - public Node[] Nodes; - public Item[] Items; - public byte[] AdditionalData; - - public bool Valid { get; private set; } - private bool _changed; - - public MaterialParam? GetMaterialParamById(uint id) - => MaterialParams.FirstOrNull(m => m.Id == id); - - public Resource? GetConstantById(uint id) - => Constants.FirstOrNull(c => c.Id == id); - - public Resource? GetConstantByName(string name) - => Constants.FirstOrNull(c => c.Name == name); - - public Resource? GetSamplerById(uint id) - => Samplers.FirstOrNull(s => s.Id == id); - - public Resource? GetSamplerByName(string name) - => Samplers.FirstOrNull(s => s.Name == name); - - public Resource? GetUavById(uint id) - => Uavs.FirstOrNull(u => u.Id == id); - - public Resource? GetUavByName(string name) - => Uavs.FirstOrNull(u => u.Name == name); - - public Key? GetSystemKeyById(uint id) - => SystemKeys.FirstOrNull(k => k.Id == id); - - public Key? GetSceneKeyById(uint id) - => SceneKeys.FirstOrNull(k => k.Id == id); - - public Key? GetMaterialKeyById(uint id) - => MaterialKeys.FirstOrNull(k => k.Id == id); - - public Node? GetNodeById(uint id) - => Nodes.FirstOrNull(n => n.Id == id); - - public Item? GetItemById(uint id) - => Items.FirstOrNull(i => i.Id == id); - - public ShpkFile(byte[] data, bool disassemble = false) - { - using var stream = new MemoryStream(data); - using var r = new BinaryReader(stream); - - if (r.ReadUInt32() != ShPkMagic) - throw new InvalidDataException(); - - Version = r.ReadUInt32(); - DirectXVersion = r.ReadUInt32() switch - { - Dx9Magic => DxVersion.DirectX9, - Dx11Magic => DxVersion.DirectX11, - _ => throw new InvalidDataException(), - }; - if (r.ReadUInt32() != data.Length) - throw new InvalidDataException(); - - var blobsOffset = r.ReadUInt32(); - var stringsOffset = r.ReadUInt32(); - var vertexShaderCount = r.ReadUInt32(); - var pixelShaderCount = r.ReadUInt32(); - MaterialParamsSize = r.ReadUInt32(); - var materialParamCount = r.ReadUInt32(); - var constantCount = r.ReadUInt32(); - var samplerCount = r.ReadUInt32(); - var uavCount = r.ReadUInt32(); - var systemKeyCount = r.ReadUInt32(); - var sceneKeyCount = r.ReadUInt32(); - var materialKeyCount = r.ReadUInt32(); - var nodeCount = r.ReadUInt32(); - var itemCount = r.ReadUInt32(); - - var blobs = new ReadOnlySpan(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset)); - var strings = new StringPool(new ReadOnlySpan(data, (int)stringsOffset, (int)(data.Length - stringsOffset))); - - VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, - strings); - PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, - strings); - - MaterialParams = r.ReadStructuresAsArray((int)materialParamCount); - - Constants = ReadResourceArray(r, (int)constantCount, strings); - Samplers = ReadResourceArray(r, (int)samplerCount, strings); - Uavs = ReadResourceArray(r, (int)uavCount, strings); - - SystemKeys = ReadKeyArray(r, (int)systemKeyCount); - SceneKeys = ReadKeyArray(r, (int)sceneKeyCount); - MaterialKeys = ReadKeyArray(r, (int)materialKeyCount); - - var subViewKey1Default = r.ReadUInt32(); - var subViewKey2Default = r.ReadUInt32(); - - SubViewKeys = new Key[] - { - new() - { - Id = 1, - DefaultValue = subViewKey1Default, - Values = Array.Empty(), - }, - new() - { - Id = 2, - DefaultValue = subViewKey2Default, - Values = Array.Empty(), - }, - }; - - Nodes = ReadNodeArray(r, (int)nodeCount, SystemKeys.Length, SceneKeys.Length, MaterialKeys.Length, SubViewKeys.Length); - Items = r.ReadStructuresAsArray((int)itemCount); - - AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position)); // This should be empty, but just in case. - - if (disassemble) - UpdateUsed(); - - UpdateKeyValues(); - - Valid = true; - _changed = false; - } - - public void UpdateResources() - { - var constants = new Dictionary(); - var samplers = new Dictionary(); - var uavs = new Dictionary(); - - static void CollectResources(Dictionary resources, Resource[] shaderResources, Func getExistingById, - DisassembledShader.ResourceType type) - { - foreach (var resource in shaderResources) - { - if (resources.TryGetValue(resource.Id, out var carry) && type != DisassembledShader.ResourceType.ConstantBuffer) - continue; - - var existing = getExistingById(resource.Id); - resources[resource.Id] = new Resource - { - Id = resource.Id, - Name = resource.Name, - Slot = existing?.Slot ?? (type == DisassembledShader.ResourceType.ConstantBuffer ? (ushort)65535 : (ushort)2), - Size = type == DisassembledShader.ResourceType.ConstantBuffer ? Math.Max(carry.Size, resource.Size) : existing?.Size ?? 0, - Used = null, - UsedDynamically = null, - }; - } - } - - foreach (var shader in VertexShaders) - { - CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); - CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); - CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav); - } - - foreach (var shader in PixelShaders) - { - CollectResources(constants, shader.Constants, GetConstantById, DisassembledShader.ResourceType.ConstantBuffer); - CollectResources(samplers, shader.Samplers, GetSamplerById, DisassembledShader.ResourceType.Sampler); - CollectResources(uavs, shader.Uavs, GetUavById, DisassembledShader.ResourceType.Uav); - } - - Constants = constants.Values.ToArray(); - Samplers = samplers.Values.ToArray(); - Uavs = uavs.Values.ToArray(); - UpdateUsed(); - - // Ceil required size to a multiple of 16 bytes. - // Offsets can be skipped, MaterialParamsConstantId's size is the count. - MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4; - foreach (var param in MaterialParams) - MaterialParamsSize = Math.Max(MaterialParamsSize, (uint)param.ByteOffset + param.ByteSize); - MaterialParamsSize = (MaterialParamsSize + 0xFu) & ~0xFu; - } - - private void UpdateUsed() - { - var cUsage = new Dictionary(); - var sUsage = new Dictionary(); - var uUsage = new Dictionary(); - - static void CollectUsed(Dictionary usage, - Resource[] resources) - { - foreach (var resource in resources) - { - if (resource.Used == null) - continue; - - usage.TryGetValue(resource.Id, out var carry); - carry.Item1 ??= Array.Empty(); - var combined = new DisassembledShader.VectorComponents[Math.Max(carry.Item1.Length, resource.Used.Length)]; - for (var i = 0; i < combined.Length; ++i) - combined[i] = (i < carry.Item1.Length ? carry.Item1[i] : 0) | (i < resource.Used.Length ? resource.Used[i] : 0); - usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0)); - } - } - - static void CopyUsed(Resource[] resources, - Dictionary used) - { - for (var i = 0; i < resources.Length; ++i) - { - if (used.TryGetValue(resources[i].Id, out var usage)) - { - resources[i].Used = usage.Item1; - resources[i].UsedDynamically = usage.Item2; - } - else - { - resources[i].Used = null; - resources[i].UsedDynamically = null; - } - } - } - - foreach (var shader in VertexShaders) - { - CollectUsed(cUsage, shader.Constants); - CollectUsed(sUsage, shader.Samplers); - CollectUsed(uUsage, shader.Uavs); - } - - foreach (var shader in PixelShaders) - { - CollectUsed(cUsage, shader.Constants); - CollectUsed(sUsage, shader.Samplers); - CollectUsed(uUsage, shader.Uavs); - } - - CopyUsed(Constants, cUsage); - CopyUsed(Samplers, sUsage); - CopyUsed(Uavs, uUsage); - } - - public void UpdateKeyValues() - { - static HashSet[] InitializeValueSet(Key[] keys) - => Array.ConvertAll(keys, key => new HashSet() - { - key.DefaultValue, - }); - - static void CollectValues(HashSet[] valueSets, uint[] values) - { - for (var i = 0; i < valueSets.Length; ++i) - valueSets[i].Add(values[i]); - } - - static void CopyValues(Key[] keys, HashSet[] valueSets) - { - for (var i = 0; i < keys.Length; ++i) - keys[i].Values = valueSets[i].ToArray(); - } - - var systemKeyValues = InitializeValueSet(SystemKeys); - var sceneKeyValues = InitializeValueSet(SceneKeys); - var materialKeyValues = InitializeValueSet(MaterialKeys); - var subViewKeyValues = InitializeValueSet(SubViewKeys); - foreach (var node in Nodes) - { - CollectValues(systemKeyValues, node.SystemKeys); - CollectValues(sceneKeyValues, node.SceneKeys); - CollectValues(materialKeyValues, node.MaterialKeys); - CollectValues(subViewKeyValues, node.SubViewKeys); - } - - CopyValues(SystemKeys, systemKeyValues); - CopyValues(SceneKeys, sceneKeyValues); - CopyValues(MaterialKeys, materialKeyValues); - CopyValues(SubViewKeys, subViewKeyValues); - } - - public void SetInvalid() - => Valid = false; - - public void SetChanged() - => _changed = true; - - public bool IsChanged() - { - var changed = _changed; - _changed = false; - return changed; - } - - private static void ClearUsed(Resource[] resources) - { - for (var i = 0; i < resources.Length; ++i) - { - resources[i].Used = null; - resources[i].UsedDynamically = null; - } - } - - private static Resource[] ReadResourceArray(BinaryReader r, int count, StringPool strings) - { - var ret = new Resource[count]; - for (var i = 0; i < count; ++i) - { - var id = r.ReadUInt32(); - var strOffset = r.ReadUInt32(); - var strSize = r.ReadUInt32(); - ret[i] = new Resource - { - Id = id, - Name = strings.GetString((int)strOffset, (int)strSize), - Slot = r.ReadUInt16(), - Size = r.ReadUInt16(), - }; - } - - return ret; - } - - private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DxVersion directX, - bool disassemble, ReadOnlySpan blobs, StringPool strings) - { - var extraHeaderSize = stage switch - { - DisassembledShader.ShaderStage.Vertex => directX switch - { - DxVersion.DirectX9 => 4, - DxVersion.DirectX11 => 8, - _ => throw new NotImplementedException(), - }, - _ => 0, - }; - - var ret = new Shader[count]; - for (var i = 0; i < count; ++i) - { - var blobOffset = r.ReadUInt32(); - var blobSize = r.ReadUInt32(); - var constantCount = r.ReadUInt16(); - var samplerCount = r.ReadUInt16(); - var uavCount = r.ReadUInt16(); - if (r.ReadUInt16() != 0) - throw new NotImplementedException(); - - var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize); - - ret[i] = new Shader - { - Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified, - DirectXVersion = directX, - Constants = ReadResourceArray(r, constantCount, strings), - Samplers = ReadResourceArray(r, samplerCount, strings), - Uavs = ReadResourceArray(r, uavCount, strings), - AdditionalHeader = rawBlob[..extraHeaderSize].ToArray(), - Blob = rawBlob[extraHeaderSize..].ToArray(), - }; - } - - return ret; - } - - private static Key[] ReadKeyArray(BinaryReader r, int count) - { - var ret = new Key[count]; - for (var i = 0; i < count; ++i) - { - ret[i] = new Key - { - Id = r.ReadUInt32(), - DefaultValue = r.ReadUInt32(), - Values = Array.Empty(), - }; - } - - return ret; - } - - private static Node[] ReadNodeArray(BinaryReader r, int count, int systemKeyCount, int sceneKeyCount, int materialKeyCount, - int subViewKeyCount) - { - var ret = new Node[count]; - for (var i = 0; i < count; ++i) - { - var id = r.ReadUInt32(); - var passCount = r.ReadUInt32(); - ret[i] = new Node - { - Id = id, - PassIndices = r.ReadBytes(16), - SystemKeys = r.ReadStructuresAsArray(systemKeyCount), - SceneKeys = r.ReadStructuresAsArray(sceneKeyCount), - MaterialKeys = r.ReadStructuresAsArray(materialKeyCount), - SubViewKeys = r.ReadStructuresAsArray(subViewKeyCount), - Passes = r.ReadStructuresAsArray((int)passCount), - }; - } - - return ret; - } - - public enum DxVersion : uint - { - DirectX9 = 9, - DirectX11 = 11, - } - - public struct Resource - { - public uint Id; - public string Name; - public ushort Slot; - public ushort Size; - public DisassembledShader.VectorComponents[]? Used; - public DisassembledShader.VectorComponents? UsedDynamically; - } - - public struct MaterialParam - { - public uint Id; - public ushort ByteOffset; - public ushort ByteSize; - } - - public struct Pass - { - public uint Id; - public uint VertexShader; - public uint PixelShader; - } - - public struct Key - { - public uint Id; - public uint DefaultValue; - public uint[] Values; - } - - public struct Node - { - public uint Id; - public byte[] PassIndices; - public uint[] SystemKeys; - public uint[] SceneKeys; - public uint[] MaterialKeys; - public uint[] SubViewKeys; - public Pass[] Passes; - } - - public struct Item - { - public uint Id; - public uint Node; - } -} diff --git a/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs b/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs deleted file mode 100644 index 6da0ab2e..00000000 --- a/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Lumina.Extensions; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Files; - -public partial class StmFile -{ - public readonly struct StainingTemplateEntry - { - /// - /// The number of stains is capped at 128 at the moment - /// - public const int NumElements = 128; - - // ColorSet row information for each stain. - public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries; - public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries; - public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries; - public readonly IReadOnlyList GlossEntries; - public readonly IReadOnlyList SpecularPowerEntries; - - public DyePack this[StainId idx] - => this[(int)idx.Value]; - - public DyePack this[int idx] - { - get - { - // The 0th index is skipped. - if (idx is <= 0 or > NumElements) - return default; - - --idx; - var (dr, dg, db) = DiffuseEntries[idx]; - var (sr, sg, sb) = SpecularEntries[idx]; - var (er, eg, eb) = EmissiveEntries[idx]; - var g = GlossEntries[idx]; - var sp = SpecularPowerEntries[idx]; - // Convert to DyePack using floats. - return new DyePack - { - Diffuse = new Vector3((float)dr, (float)dg, (float)db), - Specular = new Vector3((float)sr, (float)sg, (float)sb), - Emissive = new Vector3((float)er, (float)eg, (float)eb), - Gloss = (float)g, - SpecularPower = (float)sp, - }; - } - } - - private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) - { - br.Seek(offset); - var arraySize = size / entrySize; - // The actual amount of entries informs which type of list we use. - switch (arraySize) - { - case 0: return new RepeatingList(default!, NumElements); // All default - case 1: return new RepeatingList(read(br), NumElements); // All single entry - case NumElements: // 1-to-1 entries - var ret = new T[NumElements]; - for (var i = 0; i < NumElements; ++i) - ret[i] = read(br); - return ret; - // Indexed access. - case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize, NumElements, read); - // Should not happen. - case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); - } - } - - // Read functions - private static (Half, Half, Half) ReadTriple(BinaryReader br) - => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); - - private static Half ReadSingle(BinaryReader br) - => br.ReadHalf(); - - // Actually parse an entry. - public unsafe StainingTemplateEntry(BinaryReader br, int offset) - { - br.Seek(offset); - // 5 different lists of values. - Span ends = stackalloc ushort[5]; - for (var i = 0; i < ends.Length; ++i) - ends[i] = (ushort)(br.ReadUInt16() * 2); // because the ends are in terms of ushort. - offset += ends.Length * 2; - - DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 6); - SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 6); - EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 6); - GlossEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 2); - SpecularPowerEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 2); - } - - /// - /// Used if a single value is used for all entries of a list. - /// - private sealed class RepeatingList : IReadOnlyList - { - private readonly T _value; - public int Count { get; } - - public RepeatingList(T value, int size) - { - _value = value; - Count = size; - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < Count; ++i) - yield return _value; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public T this[int index] - => index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException(); - } - - /// - /// Used if there is a small set of values for a bigger list, accessed via index information. - /// - private sealed class IndexedList : IReadOnlyList - { - private readonly T[] _values; - private readonly byte[] _indices; - - /// - /// Reads values from via , then reads byte indices. - /// - public IndexedList(BinaryReader br, int count, int indexCount, Func read) - { - _values = new T[count + 1]; - _indices = new byte[indexCount]; - _values[0] = default!; - for (var i = 1; i < count + 1; ++i) - _values[i] = read(br); - - // Seems to be an unused 0xFF byte marker. - // Necessary for correct offsets. - br.ReadByte(); - for (var i = 0; i < indexCount; ++i) - { - _indices[i] = br.ReadByte(); - if (_indices[i] > count) - _indices[i] = 0; - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumElements; ++i) - yield return _values[_indices[i]]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _indices.Length; - - public T this[int index] - => index >= 0 && index < Count ? _values[_indices[index]] : default!; - } - } -} diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs deleted file mode 100644 index 7ee4f0d3..00000000 --- a/Penumbra.GameData/Files/StmFile.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Dalamud.Data; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData.Files; - -public partial class StmFile -{ - public const string Path = "chara/base_material/stainingtemplate.stm"; - - /// - /// All dye-able color set information for a row. - /// - public record struct DyePack - { - public Vector3 Diffuse; - public Vector3 Specular; - public Vector3 Emissive; - public float Gloss; - public float SpecularPower; - } - - /// - /// All currently available dyeing templates with their IDs. - /// - public readonly IReadOnlyDictionary Entries; - - /// - /// Access a specific dye pack. - /// - /// The ID of the accessed template. - /// The ID of the Stain. - /// The corresponding color set information or a defaulted DyePack of 0-entries. - public DyePack this[ushort template, int idx] - => Entries.TryGetValue(template, out var entry) ? entry[idx] : default; - - /// - public DyePack this[ushort template, StainId idx] - => this[template, (int)idx.Value]; - - /// - /// Try to access a specific dye pack. - /// - /// The ID of the accessed template. - /// The ID of the Stain. - /// On success, the corresponding color set information, otherwise a defaulted DyePack. - /// True on success, false otherwise. - public bool TryGetValue(ushort template, StainId idx, out DyePack dyes) - { - if (idx.Value is > 0 and <= StainingTemplateEntry.NumElements && Entries.TryGetValue(template, out var entry)) - { - dyes = entry[idx]; - return true; - } - - dyes = default; - return false; - } - - /// - /// Create a STM file from the given data array. - /// - public StmFile(byte[] data) - { - using var stream = new MemoryStream(data); - using var br = new BinaryReader(stream); - br.ReadUInt32(); - var numEntries = br.ReadInt32(); - - var keys = new ushort[numEntries]; - var offsets = new ushort[numEntries]; - - for (var i = 0; i < numEntries; ++i) - keys[i] = br.ReadUInt16(); - - for (var i = 0; i < numEntries; ++i) - offsets[i] = br.ReadUInt16(); - - var entries = new Dictionary(numEntries); - Entries = entries; - - for (var i = 0; i < numEntries; ++i) - { - var offset = offsets[i] * 2 + 8 + 4 * numEntries; - entries.Add(keys[i], new StainingTemplateEntry(br, offset)); - } - } - - /// - /// Try to read and parse the default STM file given by Lumina. - /// - public StmFile(DataManager gameData) - : this(gameData.GetFile(Path)?.Data ?? Array.Empty()) - { } -} diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs deleted file mode 100644 index 5f8c2fe4..00000000 --- a/Penumbra.GameData/GameData.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud; -using Dalamud.Data; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Plugin; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Penumbra.GameData; - -public static class GameData -{ - /// - /// Obtain an object identifier that can link a game path to game objects that use it, using your client language. - /// - public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData) - => new ObjectIdentification(pluginInterface, dataManager, itemData, dataManager.Language); - - /// - /// Obtain an object identifier that can link a game path to game objects that use it using the given language. - /// - public static IObjectIdentifier GetIdentifier(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language) - => new ObjectIdentification(pluginInterface, dataManager, itemData, language); - - /// - /// Obtain a parser for game paths. - /// - public static IGamePathParser GetGamePathParser() - => new GamePathParser(); -} - -public interface IObjectIdentifier : IDisposable -{ - /// - /// An accessible parser for game paths. - /// - public IGamePathParser GamePathParser { get; } - - /// - /// Add all known game objects using the given game path to the dictionary. - /// - /// A pre-existing dictionary to which names (and optional linked objects) can be added. - /// The game path to identify. - public void Identify(IDictionary set, string path); - - /// - /// Return named information and possibly linked objects for all known game objects using the given path. - /// - /// The game path to identify. - public Dictionary Identify(string path); - - /// - /// Identify an equippable item by its model values. - /// - /// The primary model ID for the piece of equipment. - /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. - /// The variant ID of the model. - /// The equipment slot the piece of equipment uses. - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); - - /// - public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) - => Identify(setId, 0, variant, slot); - - /// Obtain a list of BNPC Name Ids associated with a BNPC Id. - public IReadOnlyList GetBnpcNames(uint bNpcId); - - /// Obtain a list of Names and Object Kinds associated with a ModelChara ID. - public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId); - - public int NumModelChara { get; } -} - -public interface IGamePathParser -{ - public ObjectType PathToObjectType(string path); - public GameObjectInfo GetFileInfo(string path); - public string VfxToKey(string path); -} diff --git a/Penumbra.GameData/Interop/D3DCompiler.cs b/Penumbra.GameData/Interop/D3DCompiler.cs deleted file mode 100644 index 04bf1ba7..00000000 --- a/Penumbra.GameData/Interop/D3DCompiler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.String; - -namespace Penumbra.GameData.Interop; - -internal static class D3DCompiler -{ - [Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface ID3DBlob - { - [PreserveSig] - public unsafe void* GetBufferPointer(); - - [PreserveSig] - public UIntPtr GetBufferSize(); - } - - [Flags] - public enum DisassembleFlags : uint - { - EnableColorCode = 1, - EnableDefaultValuePrints = 2, - EnableInstructionNumbering = 4, - EnableInstructionCycle = 8, - DisableDebugInfo = 16, - EnableInstructionOffset = 32, - InstructionOnly = 64, - PrintHexLiterals = 128, - } - - public static unsafe ByteString Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") - { - ID3DBlob? disassembly = null; - try - { - fixed (byte* pSrcData = blob) - { - var hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); - Marshal.ThrowExceptionForHR(hr); - } - - return disassembly == null - ? ByteString.Empty - : new ByteString((byte*)disassembly.GetBufferPointer()).Clone(); - } - finally - { - if (disassembly != null) - Marshal.FinalReleaseComObject(disassembly); - } - } - - [PreserveSig] - [DllImport("D3DCompiler_47.dll")] - private static extern unsafe int D3DDisassemble( - [In] byte* pSrcData, - [In] UIntPtr srcDataSize, - uint flags, - [MarshalAs(UnmanagedType.LPStr)] string szComments, - out ID3DBlob? ppDisassembly); -} diff --git a/Penumbra.GameData/Offsets.cs b/Penumbra.GameData/Offsets.cs deleted file mode 100644 index 00c1c8e2..00000000 --- a/Penumbra.GameData/Offsets.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Penumbra.GameData; - -public static class Offsets -{ - // ActorManager.Data - public const int AgentCharaCardDataWorldId = 0xC0; - - // ObjectIdentification - public const int DrawObjectGetModelTypeVfunc = 50; - private const int DrawObjectModelBase = 0x8E8; - public const int DrawObjectModelUnk1 = DrawObjectModelBase; - public const int DrawObjectModelUnk2 = DrawObjectModelBase + 0x08; - public const int DrawObjectModelUnk3 = DrawObjectModelBase + 0x20; - public const int DrawObjectModelUnk4 = DrawObjectModelBase + 0x28; - - // PathResolver.AnimationState - public const int GetGameObjectIdxVfunc = 28; - public const int TimeLinePtr = 0x50; - - // PathResolver.Meta - public const int UpdateModelSkip = 0x90c; - public const int GetEqpIndirectSkip1 = 0xA30; - public const int GetEqpIndirectSkip2 = 0xA28; - - // FontReloader - public const int ReloadFontsVfunc = 43; - - // ObjectReloader - public const int EnableDrawVfunc = 16; - public const int DisableDrawVfunc = 17; - - // ResourceHandle - public const int ResourceHandleGetDataVfunc = 23; - public const int ResourceHandleGetLengthVfunc = 17; -} diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj deleted file mode 100644 index 68fcb147..00000000 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ /dev/null @@ -1,64 +0,0 @@ - - - net7.0-windows - preview - x64 - Penumbra.GameData - absolute gangstas - Penumbra - Copyright © 2022 - 1.0.0.0 - 1.0.0.0 - bin\$(Configuration)\ - true - enable - true - false - false - - - - full - DEBUG;TRACE - - - - pdbonly - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - - - - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs deleted file mode 100644 index 3bdb22ba..00000000 --- a/Penumbra.GameData/Signatures.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Penumbra.GameData; - -public static class Sigs -{ - // ResourceLoader.Debug - public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; - - // ResourceLoader.Replacement - public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00"; - public const string GetResourceAsync = "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00"; - public const string ReadFile = "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05"; - public const string ReadSqPack = "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3"; - - // ResourceLoader.TexMdl - public const string CheckFileState = "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24"; - public const string LoadTexFileLocal = "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20"; - public const string LoadMdlFileLocal = "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00"; - public const string LoadTexFileExtern = "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8"; - public const string LoadMdlFileExtern = "E8 ?? ?? ?? ?? EB 02 B0 F1"; - - // GameEventManager - public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; - public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; - - public const string CharacterDestructor = - "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05"; - - // PathResolver.AnimationState - public const string LoadCharacterSound = "4C 89 4C 24 ?? 55 57 41 56"; - public const string LoadTimelineResources = "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87"; - public const string CharacterBaseLoadAnimation = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05"; - public const string LoadSomePap = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51"; - public const string LoadSomeAction = "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E"; - public const string LoadCharacterVfx = "E8 ?? ?? ?? ?? 48 8B F8 48 8D 93"; - public const string LoadAreaVfx = "48 8B C4 53 55 56 57 41 56 48 81 EC"; - public const string ScheduleClipUpdate = "40 53 55 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B F9"; - - // PathResolver.DrawObjectState - public const string CharacterBaseCreate = "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40"; - - public const string CharacterBaseDestructor = - "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30"; - - public const string EnableDraw = "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0"; - public const string WeaponReload = "E8 ?? ?? ?? ?? 33 DB BE"; - - // PathResolver.Meta - public const string UpdateModel = "48 8B ?? 56 48 83 ?? ?? ?? B9"; - public const string GetEqpIndirect = "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C"; - public const string SetupVisor = "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B"; - public const string RspSetupCharacter = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56"; - public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86"; - - // PathResolver.PathState - public const string HumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 89 8B"; - - public const string WeaponVTable = - "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B"; - - public const string DemiHumanVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8B C3 89 8B"; - public const string MonsterVTable = "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83"; - - // PathResolver.Subfiles - public const string LoadMtrlTex = "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55"; - - public const string LoadMtrlShpk = - "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89"; - - public const string ApricotResourceLoad = "48 89 74 24 ?? 57 48 83 EC ?? 41 0F B6 F0 48 8B F9"; - - // CharacterUtility - public const string CharacterUtility = "48 8B 05 ?? ?? ?? ?? 83 B9"; - public const string LoadCharacterResources = "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0"; - - // MetaFileManager - public const string GetFileSpace = "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0"; - - // ResidentResourceManager - public const string ResidentResourceManager = "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05"; - - public const string LoadPlayerResources = - "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A"; - - public const string UnloadPlayerResources = - "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08"; - - // ActorManager - public const string InspectTitleId = "0F B7 0D ?? ?? ?? ?? C7 85"; - public const string InspectWorldId = "0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0"; -} diff --git a/Penumbra.GameData/Structs/CharacterArmor.cs b/Penumbra.GameData/Structs/CharacterArmor.cs deleted file mode 100644 index 7351bc48..00000000 --- a/Penumbra.GameData/Structs/CharacterArmor.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Penumbra.GameData.Structs; - -[StructLayout(LayoutKind.Explicit, Pack = 1)] -public struct CharacterArmor : IEquatable -{ - public const int Size = 4; - - [FieldOffset(0)] - public readonly uint Value; - - [FieldOffset(0)] - public SetId Set; - - [FieldOffset(2)] - public byte Variant; - - [FieldOffset(3)] - public StainId Stain; - - public CharacterArmor(SetId set, byte variant, StainId stain) - { - Value = 0; - Set = set; - Variant = variant; - Stain = stain; - } - - public readonly CharacterArmor With(StainId stain) - => new(Set, Variant, stain); - - public readonly CharacterWeapon ToWeapon(WeaponType type) - => new(Set, type, Variant, Stain); - - public readonly CharacterWeapon ToWeapon(WeaponType type, StainId stain) - => new(Set, type, Variant, stain); - - public override readonly string ToString() - => $"{Set},{Variant},{Stain}"; - - public static readonly CharacterArmor Empty; - - public readonly bool Equals(CharacterArmor other) - => Value == other.Value; - - public override readonly bool Equals(object? obj) - => obj is CharacterArmor other && Equals(other); - - public override readonly int GetHashCode() - => (int)Value; - - public static bool operator ==(CharacterArmor left, CharacterArmor right) - => left.Value == right.Value; - - public static bool operator !=(CharacterArmor left, CharacterArmor right) - => left.Value != right.Value; -} diff --git a/Penumbra.GameData/Structs/CharacterWeapon.cs b/Penumbra.GameData/Structs/CharacterWeapon.cs deleted file mode 100644 index c86dd467..00000000 --- a/Penumbra.GameData/Structs/CharacterWeapon.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Penumbra.GameData.Structs; - -[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 7)] -public struct CharacterWeapon : IEquatable -{ - [FieldOffset(0)] - public SetId Set; - - [FieldOffset(2)] - public WeaponType Type; - - [FieldOffset(4)] - public ushort Variant; - - [FieldOffset(6)] - public StainId Stain; - - public readonly ulong Value - => (ulong)Set | ((ulong)Type << 16) | ((ulong)Variant << 32) | ((ulong)Stain << 48); - - public override readonly string ToString() - => $"{Set},{Type},{Variant},{Stain}"; - - public CharacterWeapon(SetId set, WeaponType type, ushort variant, StainId stain) - { - Set = set; - Type = type; - Variant = variant; - Stain = stain; - } - - public CharacterWeapon(ulong value) - { - Set = (SetId)value; - Type = (WeaponType)(value >> 16); - Variant = (ushort)(value >> 32); - Stain = (StainId)(value >> 48); - } - - public readonly CharacterWeapon With(StainId stain) - => new(Set, Type, Variant, stain); - - public readonly CharacterArmor ToArmor() - => new(Set, (byte)Variant, Stain); - - public readonly CharacterArmor ToArmor(StainId stain) - => new(Set, (byte)Variant, stain); - - public static readonly CharacterWeapon Empty = new(0, 0, 0, 0); - - public readonly bool Equals(CharacterWeapon other) - => Value == other.Value; - - public override readonly bool Equals(object? obj) - => obj is CharacterWeapon other && Equals(other); - - public override readonly int GetHashCode() - => Value.GetHashCode(); - - public static bool operator ==(CharacterWeapon left, CharacterWeapon right) - => left.Value == right.Value; - - public static bool operator !=(CharacterWeapon left, CharacterWeapon right) - => left.Value != right.Value; -} diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs deleted file mode 100644 index 209574ec..00000000 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using Penumbra.String.Functions; - -namespace Penumbra.GameData.Structs; - -[StructLayout(LayoutKind.Sequential, Size = Size)] -public unsafe struct CustomizeData : IEquatable, IReadOnlyCollection -{ - public const int Size = 26; - - public fixed byte Data[Size]; - - public int Count - => Size; - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < Size; ++i) - yield return At(i); - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - - public byte At(int index) - => Data[index]; - - public void Set(int index, byte value) - => Data[index] = value; - - public void Read(void* source) - { - fixed (byte* ptr = Data) - { - MemoryUtility.MemCpyUnchecked(ptr, source, Size); - } - } - - public readonly void Write(void* target) - { - fixed (byte* ptr = Data) - { - MemoryUtility.MemCpyUnchecked(target, ptr, Size); - } - } - - public readonly CustomizeData Clone() - { - var ret = new CustomizeData(); - Write(ret.Data); - return ret; - } - - public readonly bool Equals(CustomizeData other) - { - fixed (byte* ptr = Data) - { - return MemoryUtility.MemCmpUnchecked(ptr, other.Data, Size) == 0; - } - } - - public override bool Equals(object? obj) - => obj is CustomizeData other && Equals(other); - - public static bool Equals(CustomizeData* lhs, CustomizeData* rhs) - => MemoryUtility.MemCmpUnchecked(lhs, rhs, Size) == 0; - - /// Compare Gender and then only from Height onwards, because all screen actors are set to Height 50, - /// the Race is implicitly included in the subrace (after height), - /// and the body type is irrelevant for players.> - public static bool ScreenActorEquals(CustomizeData* lhs, CustomizeData* rhs) - => lhs->Data[1] == rhs->Data[1] && MemoryUtility.MemCmpUnchecked(lhs->Data + 4, rhs->Data + 4, Size - 4) == 0; - - public override int GetHashCode() - { - fixed (byte* ptr = Data) - { - var p = (int*)ptr; - var u = *(ushort*)(p + 6); - return HashCode.Combine(*p, p[1], p[2], p[3], p[4], p[5], u); - } - } - - public readonly string WriteBase64() - { - fixed (byte* ptr = Data) - { - var data = new ReadOnlySpan(ptr, Size); - return Convert.ToBase64String(data); - } - } - - public override string ToString() - { - var sb = new StringBuilder(Size * 3); - for (var i = 0; i < Size - 1; ++i) - sb.Append($"{Data[i]:X2} "); - sb.Append($"{Data[Size - 1]:X2}"); - return sb.ToString(); - } - - - public bool LoadBase64(string base64) - { - var buffer = stackalloc byte[Size]; - var span = new Span(buffer, Size); - if (!Convert.TryFromBase64String(base64, span, out var written) || written != Size) - return false; - - Read(buffer); - return true; - } -} diff --git a/Penumbra.GameData/Structs/EqdpEntry.cs b/Penumbra.GameData/Structs/EqdpEntry.cs deleted file mode 100644 index 00d05bc5..00000000 --- a/Penumbra.GameData/Structs/EqdpEntry.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.ComponentModel; -using Penumbra.GameData.Enums; - -namespace Penumbra.GameData.Structs; - -[Flags] -public enum EqdpEntry : ushort -{ - Invalid = 0, - Head1 = 0b0000000001, - Head2 = 0b0000000010, - HeadMask = 0b0000000011, - - Body1 = 0b0000000100, - Body2 = 0b0000001000, - BodyMask = 0b0000001100, - - Hands1 = 0b0000010000, - Hands2 = 0b0000100000, - HandsMask = 0b0000110000, - - Legs1 = 0b0001000000, - Legs2 = 0b0010000000, - LegsMask = 0b0011000000, - - Feet1 = 0b0100000000, - Feet2 = 0b1000000000, - FeetMask = 0b1100000000, - - Ears1 = 0b0000000001, - Ears2 = 0b0000000010, - EarsMask = 0b0000000011, - - Neck1 = 0b0000000100, - Neck2 = 0b0000001000, - NeckMask = 0b0000001100, - - Wrists1 = 0b0000010000, - Wrists2 = 0b0000100000, - WristsMask = 0b0000110000, - - RingR1 = 0b0001000000, - RingR2 = 0b0010000000, - RingRMask = 0b0011000000, - - RingL1 = 0b0100000000, - RingL2 = 0b1000000000, - RingLMask = 0b1100000000, -} - -public static class Eqdp -{ - public static int Offset( EquipSlot slot ) - => slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 2, - EquipSlot.Hands => 4, - EquipSlot.Legs => 6, - EquipSlot.Feet => 8, - EquipSlot.Ears => 0, - EquipSlot.Neck => 2, - EquipSlot.Wrists => 4, - EquipSlot.RFinger => 6, - EquipSlot.LFinger => 8, - _ => throw new InvalidEnumArgumentException(), - }; - - public static (bool, bool) ToBits( this EqdpEntry entry, EquipSlot slot ) - => slot switch - { - EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), - EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), - EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), - EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), - EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), - EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), - EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), - EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), - EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), - EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), - _ => throw new InvalidEnumArgumentException(), - }; - - public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 ) - { - EqdpEntry ret = 0; - var offset = Offset( slot ); - if( bit1 ) - { - ret |= ( EqdpEntry )( 1 << offset ); - } - - if( bit2 ) - { - ret |= ( EqdpEntry )( 1 << ( offset + 1 ) ); - } - - return ret; - } - - public static EqdpEntry Mask( EquipSlot slot ) - { - return slot switch - { - EquipSlot.Head => EqdpEntry.HeadMask, - EquipSlot.Body => EqdpEntry.BodyMask, - EquipSlot.Hands => EqdpEntry.HandsMask, - EquipSlot.Legs => EqdpEntry.LegsMask, - EquipSlot.Feet => EqdpEntry.FeetMask, - EquipSlot.Ears => EqdpEntry.EarsMask, - EquipSlot.Neck => EqdpEntry.NeckMask, - EquipSlot.Wrists => EqdpEntry.WristsMask, - EquipSlot.RFinger => EqdpEntry.RingRMask, - EquipSlot.LFinger => EqdpEntry.RingLMask, - _ => 0, - }; - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs deleted file mode 100644 index 49b2f66f..00000000 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.ComponentModel; -using Penumbra.GameData.Enums; - -namespace Penumbra.GameData.Structs; - -[Flags] -public enum EqpEntry : ulong -{ - BodyEnabled = 0x00_01ul, - BodyHideWaist = 0x00_02ul, - BodyHideThighs = 0x00_04ul, - BodyHideGlovesS = 0x00_08ul, - _4 = 0x00_10ul, - BodyHideGlovesM = 0x00_20ul, - BodyHideGlovesL = 0x00_40ul, - BodyHideGorget = 0x00_80ul, - BodyShowLeg = 0x01_00ul, - BodyShowHand = 0x02_00ul, - BodyShowHead = 0x04_00ul, - BodyShowNecklace = 0x08_00ul, - BodyShowBracelet = 0x10_00ul, - BodyShowTail = 0x20_00ul, - BodyDisableBreastPhysics = 0x40_00ul, - BodyUsesEvpTable = 0x80_00ul, - BodyMask = 0xFF_FFul, - - LegsEnabled = 0x01ul << 16, - LegsHideKneePads = 0x02ul << 16, - LegsHideBootsS = 0x04ul << 16, - LegsHideBootsM = 0x08ul << 16, - _20 = 0x10ul << 16, - LegsShowFoot = 0x20ul << 16, - LegsShowTail = 0x40ul << 16, - _23 = 0x80ul << 16, - LegsMask = 0xFFul << 16, - - HandsEnabled = 0x01ul << 24, - HandsHideElbow = 0x02ul << 24, - HandsHideForearm = 0x04ul << 24, - _27 = 0x08ul << 24, - HandShowBracelet = 0x10ul << 24, - HandShowRingL = 0x20ul << 24, - HandShowRingR = 0x40ul << 24, - _31 = 0x80ul << 24, - HandsMask = 0xFFul << 24, - - FeetEnabled = 0x01ul << 32, - FeetHideKnee = 0x02ul << 32, - FeetHideCalf = 0x04ul << 32, - FeetHideAnkle = 0x08ul << 32, - _36 = 0x10ul << 32, - _37 = 0x20ul << 32, - _38 = 0x40ul << 32, - _39 = 0x80ul << 32, - FeetMask = 0xFFul << 32, - - HeadEnabled = 0x00_00_01ul << 40, - HeadHideScalp = 0x00_00_02ul << 40, - HeadHideHair = 0x00_00_04ul << 40, - HeadShowHairOverride = 0x00_00_08ul << 40, - HeadHideNeck = 0x00_00_10ul << 40, - HeadShowNecklace = 0x00_00_20ul << 40, - _46 = 0x00_00_40ul << 40, - HeadShowEarrings = 0x00_00_80ul << 40, - HeadShowEarringsHuman = 0x00_01_00ul << 40, - HeadShowEarringsAura = 0x00_02_00ul << 40, - HeadShowEarHuman = 0x00_04_00ul << 40, - HeadShowEarMiqote = 0x00_08_00ul << 40, - HeadShowEarAuRa = 0x00_10_00ul << 40, - HeadShowEarViera = 0x00_20_00ul << 40, - _54 = 0x00_40_00ul << 40, - _55 = 0x00_80_00ul << 40, - HeadShowHrothgarHat = 0x01_00_00ul << 40, - HeadShowVieraHat = 0x02_00_00ul << 40, - HeadUsesEvpTable = 0x04_00_00ul << 40, - _59 = 0x08_00_00ul << 40, - _60 = 0x10_00_00ul << 40, - _61 = 0x20_00_00ul << 40, - _62 = 0x40_00_00ul << 40, - _63 = 0x80_00_00ul << 40, - HeadMask = 0xFF_FF_FFul << 40, -} - -public static class Eqp -{ - // cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags - public const EqpEntry DefaultEntry = (EqpEntry)0x3fe00070603f00; - - public static (int, int) BytesAndOffset(EquipSlot slot) - { - return slot switch - { - EquipSlot.Body => (2, 0), - EquipSlot.Legs => (1, 2), - EquipSlot.Hands => (1, 3), - EquipSlot.Feet => (1, 4), - EquipSlot.Head => (3, 5), - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static EqpEntry ShiftAndMask(this EqpEntry entry, EquipSlot slot) - { - var (_, offset) = BytesAndOffset(slot); - var mask = Mask(slot); - return (EqpEntry)((ulong)(entry & mask) >> (offset * 8)); - } - - public static EqpEntry FromSlotAndBytes(EquipSlot slot, byte[] value) - { - EqpEntry ret = 0; - var (bytes, offset) = BytesAndOffset(slot); - if (bytes != value.Length) - throw new ArgumentException(); - - for (var i = 0; i < bytes; ++i) - ret |= (EqpEntry)((ulong)value[i] << ((offset + i) * 8)); - - return ret; - } - - public static EqpEntry Mask(EquipSlot slot) - { - return slot switch - { - EquipSlot.Body => EqpEntry.BodyMask, - EquipSlot.Head => EqpEntry.HeadMask, - EquipSlot.Legs => EqpEntry.LegsMask, - EquipSlot.Feet => EqpEntry.FeetMask, - EquipSlot.Hands => EqpEntry.HandsMask, - _ => 0, - }; - } - - public static EquipSlot ToEquipSlot(this EqpEntry entry) - { - return entry switch - { - EqpEntry.BodyEnabled => EquipSlot.Body, - EqpEntry.BodyHideWaist => EquipSlot.Body, - EqpEntry.BodyHideThighs => EquipSlot.Body, - EqpEntry.BodyHideGlovesS => EquipSlot.Body, - EqpEntry._4 => EquipSlot.Body, - EqpEntry.BodyHideGlovesM => EquipSlot.Body, - EqpEntry.BodyHideGlovesL => EquipSlot.Body, - EqpEntry.BodyHideGorget => EquipSlot.Body, - EqpEntry.BodyShowLeg => EquipSlot.Body, - EqpEntry.BodyShowHand => EquipSlot.Body, - EqpEntry.BodyShowHead => EquipSlot.Body, - EqpEntry.BodyShowNecklace => EquipSlot.Body, - EqpEntry.BodyShowBracelet => EquipSlot.Body, - EqpEntry.BodyShowTail => EquipSlot.Body, - EqpEntry.BodyDisableBreastPhysics => EquipSlot.Body, - EqpEntry.BodyUsesEvpTable => EquipSlot.Body, - - EqpEntry.LegsEnabled => EquipSlot.Legs, - EqpEntry.LegsHideKneePads => EquipSlot.Legs, - EqpEntry.LegsHideBootsS => EquipSlot.Legs, - EqpEntry.LegsHideBootsM => EquipSlot.Legs, - EqpEntry._20 => EquipSlot.Legs, - EqpEntry.LegsShowFoot => EquipSlot.Legs, - EqpEntry.LegsShowTail => EquipSlot.Legs, - EqpEntry._23 => EquipSlot.Legs, - - EqpEntry.HandsEnabled => EquipSlot.Hands, - EqpEntry.HandsHideElbow => EquipSlot.Hands, - EqpEntry.HandsHideForearm => EquipSlot.Hands, - EqpEntry._27 => EquipSlot.Hands, - EqpEntry.HandShowBracelet => EquipSlot.Hands, - EqpEntry.HandShowRingL => EquipSlot.Hands, - EqpEntry.HandShowRingR => EquipSlot.Hands, - EqpEntry._31 => EquipSlot.Hands, - - EqpEntry.FeetEnabled => EquipSlot.Feet, - EqpEntry.FeetHideKnee => EquipSlot.Feet, - EqpEntry.FeetHideCalf => EquipSlot.Feet, - EqpEntry.FeetHideAnkle => EquipSlot.Feet, - EqpEntry._36 => EquipSlot.Feet, - EqpEntry._37 => EquipSlot.Feet, - EqpEntry._38 => EquipSlot.Feet, - EqpEntry._39 => EquipSlot.Feet, - - EqpEntry.HeadEnabled => EquipSlot.Head, - EqpEntry.HeadHideScalp => EquipSlot.Head, - EqpEntry.HeadHideHair => EquipSlot.Head, - EqpEntry.HeadShowHairOverride => EquipSlot.Head, - EqpEntry.HeadHideNeck => EquipSlot.Head, - EqpEntry.HeadShowNecklace => EquipSlot.Head, - EqpEntry._46 => EquipSlot.Head, - EqpEntry.HeadShowEarrings => EquipSlot.Head, - EqpEntry.HeadShowEarringsHuman => EquipSlot.Head, - EqpEntry.HeadShowEarringsAura => EquipSlot.Head, - EqpEntry.HeadShowEarHuman => EquipSlot.Head, - EqpEntry.HeadShowEarMiqote => EquipSlot.Head, - EqpEntry.HeadShowEarAuRa => EquipSlot.Head, - EqpEntry.HeadShowEarViera => EquipSlot.Head, - EqpEntry._54 => EquipSlot.Head, - EqpEntry._55 => EquipSlot.Head, - EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, - EqpEntry.HeadShowVieraHat => EquipSlot.Head, - EqpEntry.HeadUsesEvpTable => EquipSlot.Head, - - // currently unused - EqpEntry._59 => EquipSlot.Unknown, - EqpEntry._60 => EquipSlot.Unknown, - EqpEntry._61 => EquipSlot.Unknown, - EqpEntry._62 => EquipSlot.Unknown, - EqpEntry._63 => EquipSlot.Unknown, - - _ => EquipSlot.Unknown, - }; - } - - public static string ToLocalName(this EqpEntry entry) - { - return entry switch - { - EqpEntry.BodyEnabled => "Enabled", - EqpEntry.BodyHideWaist => "Hide Waist", - EqpEntry.BodyHideThighs => "Hide Thigh Pads", - EqpEntry.BodyHideGlovesS => "Hide Small Gloves", - EqpEntry._4 => "Unknown 4", - EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", - EqpEntry.BodyHideGlovesL => "Hide Large Gloves", - EqpEntry.BodyHideGorget => "Hide Gorget", - EqpEntry.BodyShowLeg => "Show Legs", - EqpEntry.BodyShowHand => "Show Hands", - EqpEntry.BodyShowHead => "Show Head", - EqpEntry.BodyShowNecklace => "Show Necklace", - EqpEntry.BodyShowBracelet => "Show Bracelet", - EqpEntry.BodyShowTail => "Show Tail", - EqpEntry.BodyDisableBreastPhysics => "Disable Breast Physics", - EqpEntry.BodyUsesEvpTable => "Uses EVP Table", - - EqpEntry.LegsEnabled => "Enabled", - EqpEntry.LegsHideKneePads => "Hide Knee Pads", - EqpEntry.LegsHideBootsS => "Hide Small Boots", - EqpEntry.LegsHideBootsM => "Hide Medium Boots", - EqpEntry._20 => "Unknown 20", - EqpEntry.LegsShowFoot => "Show Foot", - EqpEntry.LegsShowTail => "Show Tail", - EqpEntry._23 => "Unknown 23", - - EqpEntry.HandsEnabled => "Enabled", - EqpEntry.HandsHideElbow => "Hide Elbow", - EqpEntry.HandsHideForearm => "Hide Forearm", - EqpEntry._27 => "Unknown 27", - EqpEntry.HandShowBracelet => "Show Bracelet", - EqpEntry.HandShowRingL => "Show Left Ring", - EqpEntry.HandShowRingR => "Show Right Ring", - EqpEntry._31 => "Unknown 31", - - EqpEntry.FeetEnabled => "Enabled", - EqpEntry.FeetHideKnee => "Hide Knees", - EqpEntry.FeetHideCalf => "Hide Calves", - EqpEntry.FeetHideAnkle => "Hide Ankles", - EqpEntry._36 => "Unknown 36", - EqpEntry._37 => "Unknown 37", - EqpEntry._38 => "Unknown 38", - EqpEntry._39 => "Unknown 39", - - EqpEntry.HeadEnabled => "Enabled", - EqpEntry.HeadHideScalp => "Hide Scalp", - EqpEntry.HeadHideHair => "Hide Hair", - EqpEntry.HeadShowHairOverride => "Show Hair Override", - EqpEntry.HeadHideNeck => "Hide Neck", - EqpEntry.HeadShowNecklace => "Show Necklace", - EqpEntry._46 => "Unknown 46", - EqpEntry.HeadShowEarrings => "Show Earrings", - EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)", - EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)", - EqpEntry.HeadShowEarHuman => "Show Ears (Human)", - EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)", - EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)", - EqpEntry.HeadShowEarViera => "Show Ears (Viera)", - EqpEntry._54 => "Unknown 54", - EqpEntry._55 => "Unknown 55", - EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar", - EqpEntry.HeadShowVieraHat => "Show on Viera", - EqpEntry.HeadUsesEvpTable => "Uses EVP Table", - - EqpEntry._59 => "Unknown 59", - EqpEntry._60 => "Unknown 60", - EqpEntry._61 => "Unknown 61", - EqpEntry._62 => "Unknown 62", - EqpEntry._63 => "Unknown 63", - - _ => throw new InvalidEnumArgumentException(), - }; - } - - private static EqpEntry[] GetEntriesForSlot(EquipSlot slot) - { - return ((EqpEntry[])Enum.GetValues(typeof(EqpEntry))) - .Where(e => e.ToEquipSlot() == slot) - .ToArray(); - } - - public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot(EquipSlot.Body); - public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot(EquipSlot.Legs); - public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot(EquipSlot.Hands); - public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot(EquipSlot.Feet); - public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot(EquipSlot.Head); - - public static readonly IReadOnlyDictionary EqpAttributes = new Dictionary() - { - [EquipSlot.Body] = EqpAttributesBody, - [EquipSlot.Legs] = EqpAttributesLegs, - [EquipSlot.Hands] = EqpAttributesHands, - [EquipSlot.Feet] = EqpAttributesFeet, - [EquipSlot.Head] = EqpAttributesHead, - }; -} diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs deleted file mode 100644 index 59ea94b4..00000000 --- a/Penumbra.GameData/Structs/EquipItem.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Runtime.InteropServices; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Enums; -using PseudoEquipItem = System.ValueTuple; - -namespace Penumbra.GameData.Structs; - -[StructLayout(LayoutKind.Sequential)] -public readonly struct EquipItem -{ - public readonly string Name; - public readonly ulong Id; - public readonly ushort IconId; - public readonly SetId ModelId; - public readonly WeaponType WeaponType; - public readonly byte Variant; - public readonly FullEquipType Type; - - public uint ItemId - => (uint)Id; - - public bool Valid - => Type != FullEquipType.Unknown; - - public CharacterArmor Armor() - => new(ModelId, Variant, 0); - - public CharacterArmor Armor(StainId stain) - => new(ModelId, Variant, stain); - - public CharacterWeapon Weapon() - => new(ModelId, WeaponType, Variant, 0); - - public CharacterWeapon Weapon(StainId stain) - => new(ModelId, WeaponType, Variant, stain); - - public EquipItem() - => Name = string.Empty; - - public EquipItem(string name, ulong id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type) - { - Name = string.Intern(name); - Id = id; - IconId = iconId; - ModelId = modelId; - WeaponType = weaponType; - Variant = variant; - Type = type; - } - - public string ModelString - => WeaponType == 0 ? $"{ModelId.Value}-{Variant}" : $"{ModelId.Value}-{WeaponType.Value}-{Variant}"; - - public static implicit operator EquipItem(PseudoEquipItem it) - => new(it.Item1, it.Item2, it.Item3, it.Item4, it.Item5, it.Item6, (FullEquipType)it.Item7); - - public static explicit operator PseudoEquipItem(EquipItem it) - => (it.Name, it.ItemId, it.IconId, (ushort)it.ModelId, (ushort)it.WeaponType, it.Variant, (byte)it.Type); - - public static EquipItem FromArmor(Item item) - { - var type = item.ToEquipType(); - var name = item.Name.ToDalamudString().TextValue; - var id = item.RowId; - var icon = item.Icon; - var model = (SetId)item.ModelMain; - var weapon = (WeaponType)0; - var variant = (byte)(item.ModelMain >> 16); - return new EquipItem(name, id, icon, model, weapon, variant, type); - } - - public static EquipItem FromMainhand(Item item) - { - var type = item.ToEquipType(); - var name = item.Name.ToDalamudString().TextValue; - var id = item.RowId; - var icon = item.Icon; - var model = (SetId)item.ModelMain; - var weapon = (WeaponType)(item.ModelMain >> 16); - var variant = (byte)(item.ModelMain >> 32); - return new EquipItem(name, id, icon, model, weapon, variant, type); - } - - public static EquipItem FromOffhand(Item item) - { - var type = item.ToEquipType().ValidOffhand(); - var name = item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix(); - var id = item.RowId; - var icon = item.Icon; - var model = (SetId)item.ModelSub; - var weapon = (WeaponType)(item.ModelSub >> 16); - var variant = (byte)(item.ModelSub >> 32); - return new EquipItem(name, id, icon, model, weapon, variant, type); - } - - public static EquipItem FromIds(uint itemId, ushort iconId, SetId modelId, WeaponType type, byte variant, - FullEquipType equipType = FullEquipType.Unknown, string? name = null) - { - name ??= $"Unknown ({modelId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})"; - var fullId = itemId == 0 - ? modelId.Value | ((ulong)type.Value << 16) | ((ulong)variant << 32) | ((ulong)equipType << 40) | (1ul << 48) - : itemId; - return new EquipItem(name, fullId, iconId, modelId, type, variant, equipType); - } - - - public static EquipItem FromId(ulong id) - { - var setId = (SetId)id; - var type = (WeaponType)(id >> 16); - var variant = (byte)(id >> 32); - var equipType = (FullEquipType)(id >> 40); - return new EquipItem($"Unknown ({setId.Value}-{(type.Value != 0 ? $"{type.Value}-" : string.Empty)}{variant})", id, 0, setId, type, - variant, equipType); - } - - public override string ToString() - => Name; -} diff --git a/Penumbra.GameData/Structs/GameObjectInfo.cs b/Penumbra.GameData/Structs/GameObjectInfo.cs deleted file mode 100644 index fae17494..00000000 --- a/Penumbra.GameData/Structs/GameObjectInfo.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud; -using Penumbra.GameData.Enums; - -namespace Penumbra.GameData.Structs; - -[StructLayout( LayoutKind.Explicit )] -public struct GameObjectInfo : IComparable -{ - public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown - , EquipSlot slot = EquipSlot.Unknown, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, - PrimaryId = setId, - GenderRace = gr, - Variant = variant, - EquipSlot = slot, - }; - - public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Weapon, - PrimaryId = setId, - SecondaryId = weaponId, - Variant = variant, - }; - - public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 - , GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Character, - PrimaryId = id, - GenderRace = gr, - BodySlot = bodySlot, - Variant = variant, - CustomizationType = customizationType, - }; - - public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Monster, - PrimaryId = monsterId, - SecondaryId = bodyId, - Variant = variant, - }; - - public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown, - byte variant = 0 - ) - => new() - { - FileType = type, - ObjectType = ObjectType.DemiHuman, - PrimaryId = demiHumanId, - SecondaryId = bodyId, - Variant = variant, - EquipSlot = slot, - }; - - public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) - => new() - { - FileType = type, - ObjectType = ObjectType.Map, - MapC1 = c1, - MapC2 = c2, - MapC3 = c3, - MapC4 = c4, - MapSuffix = suffix, - Variant = variant, - }; - - public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, bool hr, ClientLanguage lang = ClientLanguage.English ) - => new() - { - FileType = type, - ObjectType = ObjectType.Icon, - IconId = iconId, - IconHqHr = ( byte )( hq ? hr ? 3 : 1 : hr ? 2 : 0 ), - Language = lang, - }; - - - [FieldOffset( 0 )] - public readonly ulong Identifier; - - [FieldOffset( 0 )] - public FileType FileType; - - [FieldOffset( 1 )] - public ObjectType ObjectType; - - - [FieldOffset( 2 )] - public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman - - [FieldOffset( 2 )] - public uint IconId; // Icon - - [FieldOffset( 2 )] - public byte MapC1; // Map - - [FieldOffset( 3 )] - public byte MapC2; // Map - - [FieldOffset( 4 )] - public ushort SecondaryId; // Weapon, Monster, Demihuman - - [FieldOffset( 4 )] - public byte MapC3; // Map - - [FieldOffset( 4 )] - private byte _genderRaceByte; // Equipment, Customization - - public GenderRace GenderRace - { - get => Names.GenderRaceFromByte( _genderRaceByte ); - set => _genderRaceByte = value.ToByte(); - } - - [FieldOffset( 5 )] - public BodySlot BodySlot; // Customization - - [FieldOffset( 5 )] - public byte MapC4; // Map - - [FieldOffset( 6 )] - public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman - - [FieldOffset( 6 )] - public byte IconHqHr; // Icon - - [FieldOffset( 7 )] - public EquipSlot EquipSlot; // Equipment, Demihuman - - [FieldOffset( 7 )] - public CustomizationType CustomizationType; // Customization - - [FieldOffset( 7 )] - public ClientLanguage Language; // Icon - - [FieldOffset( 7 )] - public byte MapSuffix; - - public override int GetHashCode() - => Identifier.GetHashCode(); - - public int CompareTo( object? r ) - => Identifier.CompareTo( r ); -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/GmpEntry.cs b/Penumbra.GameData/Structs/GmpEntry.cs deleted file mode 100644 index 8ad571ed..00000000 --- a/Penumbra.GameData/Structs/GmpEntry.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; - -namespace Penumbra.GameData.Structs; - -public struct GmpEntry : IEquatable< GmpEntry > -{ - public static readonly GmpEntry Default = new(); - - public bool Enabled - { - get => ( Value & 1 ) == 1; - set - { - if( value ) - { - Value |= 1ul; - } - else - { - Value &= ~1ul; - } - } - } - - public bool Animated - { - get => ( Value & 2 ) == 2; - set - { - if( value ) - { - Value |= 2ul; - } - else - { - Value &= ~2ul; - } - } - } - - public ushort RotationA - { - get => ( ushort )( ( Value >> 2 ) & 0x3FF ); - set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 ); - } - - public ushort RotationB - { - get => ( ushort )( ( Value >> 12 ) & 0x3FF ); - set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 ); - } - - public ushort RotationC - { - get => ( ushort )( ( Value >> 22 ) & 0x3FF ); - set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 ); - } - - public byte UnknownA - { - get => ( byte )( ( Value >> 32 ) & 0x0F ); - set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 ); - } - - public byte UnknownB - { - get => ( byte )( ( Value >> 36 ) & 0x0F ); - set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 ); - } - - public byte UnknownTotal - { - get => ( byte )( ( Value >> 32 ) & 0xFF ); - set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 ); - } - - public ulong Value { get; set; } - - public static GmpEntry FromTexToolsMeta( byte[] data ) - { - GmpEntry ret = new(); - using var reader = new BinaryReader( new MemoryStream( data ) ); - ret.Value = reader.ReadUInt32(); - ret.UnknownTotal = data[ 4 ]; - return ret; - } - - public static implicit operator ulong( GmpEntry entry ) - => entry.Value; - - public static explicit operator GmpEntry( ulong entry ) - => new() { Value = entry }; - - public bool Equals( GmpEntry other ) - => Value == other.Value; - - public override bool Equals( object? obj ) - => obj is GmpEntry other && Equals( other ); - - public override int GetHashCode() - => Value.GetHashCode(); -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/ImcEntry.cs b/Penumbra.GameData/Structs/ImcEntry.cs deleted file mode 100644 index 9cabe54f..00000000 --- a/Penumbra.GameData/Structs/ImcEntry.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Penumbra.GameData.Structs; - -public readonly struct ImcEntry : IEquatable -{ - public byte MaterialId { get; init; } - public byte DecalId { get; init; } - public readonly ushort AttributeAndSound; - public byte VfxId { get; init; } - public byte MaterialAnimationId { get; init; } - - public ushort AttributeMask - { - get => (ushort)(AttributeAndSound & 0x3FF); - init => AttributeAndSound = (ushort)((AttributeAndSound & ~0x3FF) | (value & 0x3FF)); - } - - public byte SoundId - { - get => (byte)(AttributeAndSound >> 10); - init => AttributeAndSound = (ushort)(AttributeMask | (value << 10)); - } - - public bool Equals(ImcEntry other) - => MaterialId == other.MaterialId - && DecalId == other.DecalId - && AttributeAndSound == other.AttributeAndSound - && VfxId == other.VfxId - && MaterialAnimationId == other.MaterialAnimationId; - - public override bool Equals(object? obj) - => obj is ImcEntry other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine(MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId); - - [JsonConstructor] - public ImcEntry(byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId) - { - MaterialId = materialId; - DecalId = decalId; - AttributeAndSound = 0; - VfxId = vfxId; - MaterialAnimationId = materialAnimationId; - AttributeMask = attributeMask; - SoundId = soundId; - } -} diff --git a/Penumbra.GameData/Structs/RspEntry.cs b/Penumbra.GameData/Structs/RspEntry.cs deleted file mode 100644 index 98f85da3..00000000 --- a/Penumbra.GameData/Structs/RspEntry.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; -using Penumbra.GameData.Enums; - -namespace Penumbra.GameData.Structs; - -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct RspEntry -{ - public const int ByteSize = ( int )RspAttribute.NumAttributes * 4; - - private readonly float[] Attributes; - - public RspEntry( RspEntry copy ) - => Attributes = ( float[] )copy.Attributes.Clone(); - - public RspEntry( byte[] bytes, int offset ) - { - if( offset < 0 || offset + ByteSize > bytes.Length ) - { - throw new ArgumentOutOfRangeException(); - } - - Attributes = new float[( int )RspAttribute.NumAttributes]; - using MemoryStream s = new(bytes) { Position = offset }; - using BinaryReader br = new(s); - for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i ) - { - Attributes[ i ] = br.ReadSingle(); - } - } - - private static int ToIndex( RspAttribute attribute ) - => attribute < RspAttribute.NumAttributes && attribute >= 0 - ? ( int )attribute - : throw new InvalidEnumArgumentException(); - - public float this[ RspAttribute attribute ] - { - get => Attributes[ ToIndex( attribute ) ]; - set => Attributes[ ToIndex( attribute ) ] = value; - } - - public byte[] ToBytes() - { - using var s = new MemoryStream( ByteSize ); - using var bw = new BinaryWriter( s ); - foreach( var attribute in Attributes ) - { - bw.Write( attribute ); - } - - return s.ToArray(); - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/SetId.cs b/Penumbra.GameData/Structs/SetId.cs deleted file mode 100644 index 5de82c68..00000000 --- a/Penumbra.GameData/Structs/SetId.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Penumbra.GameData.Structs; - -public readonly struct SetId : IComparable< SetId >, IEquatable, IEquatable -{ - public readonly ushort Value; - - public SetId( ushort value ) - => Value = value; - - public static implicit operator SetId( ushort id ) - => new(id); - - public static explicit operator ushort( SetId id ) - => id.Value; - - public bool Equals(SetId other) - => Value == other.Value; - - public bool Equals(ushort other) - => Value == other; - - public override string ToString() - => Value.ToString(); - - public int CompareTo( SetId other ) - => Value.CompareTo( other.Value ); -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/Stain.cs b/Penumbra.GameData/Structs/Stain.cs deleted file mode 100644 index 3b0323d3..00000000 --- a/Penumbra.GameData/Structs/Stain.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Dalamud.Utility; - -namespace Penumbra.GameData.Structs; - -// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name. -public readonly struct Stain -{ - // An empty stain with transparent color. - public static readonly Stain None = new("None"); - - public readonly string Name; - public readonly uint RgbaColor; - public readonly byte RowIndex; - public readonly bool Gloss; - - public byte R - => (byte)(RgbaColor & 0xFF); - - public byte G - => (byte)((RgbaColor >> 8) & 0xFF); - - public byte B - => (byte)((RgbaColor >> 16) & 0xFF); - - public byte Intensity - => (byte)((1 + R + G + B) / 3); - - // R and B need to be shuffled and Alpha set to max. - private static uint SeColorToRgba(uint color) - => ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000; - - public Stain(Lumina.Excel.GeneratedSheets.Stain stain) - : this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown5) - { } - - internal Stain(string name, uint dye, byte index, bool gloss) - { - Name = name; - RowIndex = index; - Gloss = gloss; - RgbaColor = dye; - } - - // Only used by None. - private Stain(string name) - { - Name = name; - RowIndex = 0; - RgbaColor = 0; - Gloss = false; - } -} diff --git a/Penumbra.GameData/Structs/StainId.cs b/Penumbra.GameData/Structs/StainId.cs deleted file mode 100644 index 6767a052..00000000 --- a/Penumbra.GameData/Structs/StainId.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Numerics; - -namespace Penumbra.GameData.Structs; - -public readonly struct StainId : IEquatable< StainId >, IEqualityOperators -{ - public readonly byte Value; - - public StainId( byte value ) - => Value = value; - - public static implicit operator StainId( byte id ) - => new(id); - - public static explicit operator byte( StainId id ) - => id.Value; - - public override string ToString() - => Value.ToString(); - - public bool Equals( StainId other ) - => Value == other.Value; - - public override bool Equals( object? obj ) - => obj is StainId other && Equals( other ); - - public override int GetHashCode() - => Value.GetHashCode(); - - public static bool operator ==(StainId left, StainId right) - => left.Value == right.Value; - - public static bool operator !=(StainId left, StainId right) - => left.Value != right.Value; -} \ No newline at end of file diff --git a/Penumbra.GameData/Structs/WeaponType.cs b/Penumbra.GameData/Structs/WeaponType.cs deleted file mode 100644 index ea310bd7..00000000 --- a/Penumbra.GameData/Structs/WeaponType.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Penumbra.GameData.Structs; - -public readonly struct WeaponType : IEquatable< WeaponType > -{ - public readonly ushort Value; - - public WeaponType( ushort value ) - => Value = value; - - public static implicit operator WeaponType( ushort id ) - => new(id); - - public static explicit operator ushort( WeaponType id ) - => id.Value; - - public override string ToString() - => Value.ToString(); - - public bool Equals( WeaponType other ) - => Value == other.Value; - - public override bool Equals( object? obj ) - => obj is WeaponType other && Equals( other ); - - public override int GetHashCode() - => Value.GetHashCode(); - - public static bool operator ==( WeaponType lhs, WeaponType rhs ) - => lhs.Value == rhs.Value; - - public static bool operator !=( WeaponType lhs, WeaponType rhs ) - => lhs.Value != rhs.Value; -} \ No newline at end of file diff --git a/Penumbra.GameData/UtilityFunctions.cs b/Penumbra.GameData/UtilityFunctions.cs deleted file mode 100644 index 704d7a0c..00000000 --- a/Penumbra.GameData/UtilityFunctions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Penumbra.GameData; - -public static class UtilityFunctions -{ - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T? FirstOrNull(this IEnumerable values, Func predicate) where T : struct - => values.Cast().FirstOrDefault(v => predicate(v!.Value)); - - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T[] AddItem(this T[] array, T element, int count = 1) - { - var length = array.Length; - var newArray = new T[array.Length + count]; - Array.Copy(array, newArray, length); - for (var i = length; i < newArray.Length; ++i) - newArray[i] = element; - - return newArray; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T[] RemoveItems(this T[] array, int offset, int count = 1) - { - var newArray = new T[array.Length - count]; - Array.Copy(array, newArray, offset); - Array.Copy(array, offset + count, newArray, offset, newArray.Length - offset); - return newArray; - } -} From ec1dee8871d111f3145e5f5057300ed6f61e9002 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jul 2023 15:56:38 +0200 Subject: [PATCH 1066/2451] Add GameData as submodule. --- .gitmodules | 4 ++++ Penumbra.GameData | 1 + 2 files changed, 5 insertions(+) create mode 160000 Penumbra.GameData diff --git a/.gitmodules b/.gitmodules index 94049366..c03525eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,7 @@ path = Penumbra.String url = git@github.com:Ottermandias/Penumbra.String.git branch = main +[submodule "Penumbra.GameData"] + path = Penumbra.GameData + url = git@github.com:Ottermandias/Penumbra.GameData.git + branch = main diff --git a/Penumbra.GameData b/Penumbra.GameData new file mode 160000 index 00000000..eeb55916 --- /dev/null +++ b/Penumbra.GameData @@ -0,0 +1 @@ +Subproject commit eeb55916372432aa0b5936cc8648657fd4b3447a From dccd347432abc15d0812f323afd02baf3d9a5064 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jul 2023 17:24:00 +0200 Subject: [PATCH 1067/2451] Fix some EquipItem stuff. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index eeb55916..98bd4e99 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit eeb55916372432aa0b5936cc8648657fd4b3447a +Subproject commit 98bd4e9946ded20cb5d54182883e73f344fe2d26 From 18b6b87e6bb0fee05f1b461af3cc8ff1dadb8142 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Jul 2023 02:22:31 +0200 Subject: [PATCH 1068/2451] Use strongly typed ids in most places. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqdpCache.cs | 3 +- Penumbra/Collections/Cache/EqpCache.cs | 2 +- Penumbra/Collections/Cache/GmpCache.cs | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 2 +- .../Manager/IndividualCollections.Access.cs | 2 +- .../Manager/IndividualCollections.cs | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- Penumbra/Import/TexToolsMeta.Export.cs | 4 +- .../PathResolving/CollectionResolver.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 8 +- Penumbra/Meta/Files/EqdpFile.cs | 30 ++--- Penumbra/Meta/Files/EqpGmpFile.cs | 38 +++--- Penumbra/Meta/Files/EstFile.cs | 9 +- Penumbra/Meta/Files/ImcFile.cs | 17 ++- .../Meta/Manipulations/EqdpManipulation.cs | 6 +- .../Meta/Manipulations/EqpManipulation.cs | 6 +- .../Meta/Manipulations/EstManipulation.cs | 9 +- .../Meta/Manipulations/GmpManipulation.cs | 6 +- .../Meta/Manipulations/ImcManipulation.cs | 124 +++++++++--------- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 6 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 55 ++++---- Penumbra/Mods/ItemSwap/ItemSwap.cs | 9 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 26 ++-- Penumbra/UI/ChangedItemDrawer.cs | 6 +- Penumbra/UI/Tabs/DebugTab.cs | 7 +- 26 files changed, 192 insertions(+), 193 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 98bd4e99..263bfb49 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 98bd4e9946ded20cb5d54182883e73f344fe2d26 +Subproject commit 263bfb49c998700197a18ad99fa1daadc8736f5d diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index db40fcb6..1b9c8156 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -5,6 +5,7 @@ using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -48,7 +49,7 @@ public readonly struct EqdpCache : IDisposable foreach (var file in _eqdpFiles.OfType()) { var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (int)m.SetId)); + file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (SetId) m.SetId)); } _eqdpManipulations.Clear(); diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 5fe40426..4e87a34a 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -32,7 +32,7 @@ public struct EqpCache : IDisposable if (_eqpFile == null) return; - _eqpFile.Reset(_eqpManipulations.Select(m => (int)m.SetId)); + _eqpFile.Reset(_eqpManipulations.Select(m => m.SetId)); _eqpManipulations.Clear(); } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 4e485036..1c25b9e5 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -29,7 +29,7 @@ public struct GmpCache : IDisposable if( _gmpFile == null ) return; - _gmpFile.Reset( _gmpManipulations.Select( m => ( int )m.SetId ) ); + _gmpFile.Reset( _gmpManipulations.Select( m => m.SetId ) ); _gmpManipulations.Clear(); } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 3d097b7b..28680d11 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -99,7 +99,7 @@ public readonly struct ImcCache : IDisposable return true; } - var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant, out _); + var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _); var manip = m.Copy(def); if (!manip.Apply(file)) return false; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 93c555b3..b1b0698a 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -59,7 +59,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (!_config.UseOwnerNameForCharacterCollection) return false; - identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, + identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); } diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 065d6a79..c4d9c516 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -135,7 +135,7 @@ public sealed partial class IndividualCollections _ => throw new NotImplementedException(), }; return table.Where(kvp => kvp.Value == name) - .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, + .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, identifier.Kind, kvp.Key)).ToArray(); } diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 92d2aa5a..49501d91 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -118,7 +118,7 @@ public partial class TexToolsMeta var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { - if (_keepDefault || !value.Equals(def.GetEntry(partIdx, i))) + if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant) i))) { var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value); diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index bee31cb2..7f455ab0 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -152,7 +152,7 @@ public partial class TexToolsMeta for( var i = 0; i <= baseFile.Count; ++i ) { - var entry = baseFile.GetEntry( partIdx, i ); + var entry = baseFile.GetEntry( partIdx, (Variant)i ); b.Write( entry.MaterialId ); b.Write( entry.DecalId ); b.Write( entry.AttributeAndSound ); @@ -184,7 +184,7 @@ public partial class TexToolsMeta foreach( var manip in manips ) { b.Write( ( ushort )Names.CombinedRace( manip.Est.Gender, manip.Est.Race ) ); - b.Write( manip.Est.SetId ); + b.Write( manip.Est.SetId.Id ); b.Write( manip.Est.Entry ); } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 8220c629..67d9e96d 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -244,7 +244,7 @@ public unsafe class CollectionResolver if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) return null; - var id = _actors.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, + var id = _actors.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckYourself(id, owner) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 54940f3c..90ee1a16 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -225,10 +225,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide { "chara" => SafeGet(path, 1) switch { - "accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Value:D4}"), - "equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Value:D4}"), + "accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Id:D4}"), + "equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Id:D4}"), "monster" => SafeGet(path, 2) == $"m{Skeleton:D4}", - "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Value:D4}"), + "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"), _ => null, }, _ => null, @@ -238,7 +238,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide => SafeGet(path, 0) == equipmentDir ? SafeGet(path, 1) switch { - "material" => SafeGet(path, 2) == $"v{Equipment.Variant:D4}", + "material" => SafeGet(path, 2) == $"v{Equipment.Variant.Id:D4}", _ => null, } : false; diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 14275467..6d1b5476 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -40,21 +40,21 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public int Count => (Length - DataOffset) / EqdpEntrySize; - public EqdpEntry this[int idx] + public EqdpEntry this[SetId id] { get { - if (idx >= Count || idx < 0) + if (id.Id >= Count) throw new IndexOutOfRangeException(); - return (EqdpEntry)(*(ushort*)(Data + DataOffset + EqdpEntrySize * idx)); + return (EqdpEntry)(*(ushort*)(Data + DataOffset + EqdpEntrySize * id.Id)); } set { - if (idx >= Count || idx < 0) + if (id.Id >= Count) throw new IndexOutOfRangeException(); - *(ushort*)(Data + DataOffset + EqdpEntrySize * idx) = (ushort)value; + *(ushort*)(Data + DataOffset + EqdpEntrySize * id.Id) = (ushort)value; } } @@ -81,7 +81,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile MemoryUtility.MemSet(myDataPtr, 0, Length - (int)((byte*)myDataPtr - Data)); } - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(entry); @@ -103,18 +103,18 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile Reset(); } - public EqdpEntry GetDefault(int setIdx) - => GetDefault(Manager, Index, setIdx); + public EqdpEntry GetDefault(SetId setId) + => GetDefault(Manager, Index, setId); - public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, int setIdx) - => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, setIdx); + public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, SetId setId) + => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, setId); - public static EqdpEntry GetDefault(byte* data, int setIdx) + public static EqdpEntry GetDefault(byte* data, SetId setId) { var blockSize = *(ushort*)(data + IdentifierSize); var totalBlockCount = *(ushort*)(data + IdentifierSize + 2); - var blockIdx = setIdx / blockSize; + var blockIdx = setId.Id / blockSize; if (blockIdx >= totalBlockCount) return 0; @@ -123,9 +123,9 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile return 0; var blockData = (ushort*)(data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2); - return (EqdpEntry)(*(blockData + setIdx % blockSize)); + return (EqdpEntry)(*(blockData + setId.Id % blockSize)); } - public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, int setIdx) - => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setIdx); + public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, SetId setId) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setId); } diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index e21e18c7..71563ef9 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -28,26 +28,26 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public ulong ControlBlock => *(ulong*)Data; - protected ulong GetInternal(int idx) + protected ulong GetInternal(SetId idx) { - return idx switch + return idx.Id switch { >= Count => throw new IndexOutOfRangeException(), <= 1 => *((ulong*)Data + 1), - _ => *((ulong*)Data + idx), + _ => *((ulong*)Data + idx.Id), }; } - protected void SetInternal(int idx, ulong value) + protected void SetInternal(SetId idx, ulong value) { - idx = idx switch + idx = idx.Id switch { >= Count => throw new IndexOutOfRangeException(), <= 0 => 1, _ => idx, }; - *((ulong*)Data + idx) = value; + *((ulong*)Data + idx.Id) = value; } protected virtual void SetEmptyBlock(int idx) @@ -85,13 +85,13 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile Reset(); } - protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def) + protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, SetId setId, ulong def) { var data = (byte*)manager.CharacterUtility.DefaultResource(fileIndex).Address; - if (setIdx == 0) - setIdx = 1; + if (setId == 0) + setId = 1; - var blockIdx = setIdx / BlockSize; + var blockIdx = setId.Id / BlockSize; if (blockIdx >= NumBlocks) return def; @@ -101,7 +101,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile return def; var count = BitOperations.PopCount(control & (blockBit - 1)); - var idx = setIdx % BlockSize; + var idx = setId.Id % BlockSize; var ptr = (ulong*)data + BlockSize * count + idx; return *ptr; } @@ -116,14 +116,14 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable : base(manager, false) { } - public EqpEntry this[int idx] + public EqpEntry this[SetId idx] { get => (EqpEntry)GetInternal(idx); set => SetInternal(idx, (ulong)value); } - public static EqpEntry GetDefault(MetaFileManager manager, int setIdx) + public static EqpEntry GetDefault(MetaFileManager manager, SetId setIdx) => (EqpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)Eqp.DefaultEntry); protected override unsafe void SetEmptyBlock(int idx) @@ -134,7 +134,7 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable *ptr = (ulong)Eqp.DefaultEntry; } - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(Manager, entry); @@ -142,7 +142,7 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable public IEnumerator GetEnumerator() { - for (var idx = 1; idx < Count; ++idx) + for (ushort idx = 1; idx < Count; ++idx) yield return this[idx]; } @@ -159,16 +159,16 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable : base(manager, true) { } - public GmpEntry this[int idx] + public GmpEntry this[SetId idx] { get => (GmpEntry)GetInternal(idx); set => SetInternal(idx, (ulong)value); } - public static GmpEntry GetDefault(MetaFileManager manager, int setIdx) + public static GmpEntry GetDefault(MetaFileManager manager, SetId setIdx) => (GmpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)GmpEntry.Default); - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(Manager, entry); @@ -176,7 +176,7 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable public IEnumerator GetEnumerator() { - for (var idx = 1; idx < Count; ++idx) + for (ushort idx = 1; idx < Count; ++idx) yield return this[idx]; } diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 72fae443..81749d46 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -168,21 +169,21 @@ public sealed unsafe class EstFile : MetaBaseFile public ushort GetDefault(GenderRace genderRace, ushort setId) => GetDefault(Manager, Index, genderRace, setId); - public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId) + public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, SetId setId) { var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; var count = *(int*)data; var span = new ReadOnlySpan(data + 4, count); - var (idx, found) = FindEntry(span, genderRace, setId); + var (idx, found) = FindEntry(span, genderRace, setId.Id); if (!found) return 0; return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); } - public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, ushort setId) + public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, SetId setId) => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, setId); - public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, ushort setId) + public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, SetId setId) => GetDefault(manager, (MetaIndex)estType, genderRace, setId); } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index d47bf387..8c957bcc 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -4,7 +4,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; -using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -50,19 +49,19 @@ public unsafe class ImcFile : MetaBaseFile private static ushort PartMask(byte* data) => *(ushort*)(data + 2); - private static ImcEntry* VariantPtr(byte* data, int partIdx, int variantIdx) + private static ImcEntry* VariantPtr(byte* data, int partIdx, Variant variantIdx) { var flag = 1 << partIdx; - if ((PartMask(data) & flag) == 0 || variantIdx > CountInternal(data)) + if ((PartMask(data) & flag) == 0 || variantIdx.Id > CountInternal(data)) return null; var numParts = BitOperations.PopCount(PartMask(data)); var ptr = (ImcEntry*)(data + PreambleSize); - ptr += variantIdx * numParts + partIdx; + ptr += variantIdx.Id * numParts + partIdx; return ptr; } - public ImcEntry GetEntry(int partIdx, int variantIdx) + public ImcEntry GetEntry(int partIdx, Variant variantIdx) { var ptr = VariantPtr(Data, partIdx, variantIdx); return ptr == null ? new ImcEntry() : *ptr; @@ -106,12 +105,12 @@ public unsafe class ImcFile : MetaBaseFile return true; } - public bool SetEntry(int partIdx, int variantIdx, ImcEntry entry) + public bool SetEntry(int partIdx, Variant variantIdx, ImcEntry entry) { if (partIdx >= NumParts) return false; - EnsureVariantCount(variantIdx); + EnsureVariantCount(variantIdx.Id); var variantPtr = VariantPtr(Data, partIdx, variantIdx); if (variantPtr == null) @@ -154,10 +153,10 @@ public unsafe class ImcFile : MetaBaseFile } } - public static ImcEntry GetDefault(MetaFileManager manager, Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists) + public static ImcEntry GetDefault(MetaFileManager manager, Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists) => GetDefault(manager, path.ToString(), slot, variantIdx, out exists); - public static ImcEntry GetDefault(MetaFileManager manager, string path, EquipSlot slot, int variantIdx, out bool exists) + public static ImcEntry GetDefault(MetaFileManager manager, string path, EquipSlot slot, Variant variantIdx, out bool exists) { var file = manager.GameData.GetFile(path); exists = false; diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 526abdd4..8524ab8c 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -20,13 +20,13 @@ public readonly struct EqdpManipulation : IMetaManipulation [JsonConverter(typeof(StringEnumConverter))] public ModelRace Race { get; private init; } - public ushort SetId { get; private init; } + public SetId SetId { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId) + public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, SetId setId) { Gender = gender; Race = race; @@ -74,7 +74,7 @@ public readonly struct EqdpManipulation : IMetaManipulation if (g != 0) return g; - var set = SetId.CompareTo(other.SetId); + var set = SetId.Id.CompareTo(other.SetId.Id); return set != 0 ? set : Slot.CompareTo(other.Slot); } diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index b7a17c19..91949ae4 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -17,13 +17,13 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] public EqpEntry Entry { get; private init; } - public ushort SetId { get; private init; } + public SetId SetId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) + public EqpManipulation( EqpEntry entry, EquipSlot slot, SetId setId ) { Slot = slot; SetId = setId; @@ -48,7 +48,7 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > public int CompareTo( EqpManipulation other ) { - var set = SetId.CompareTo( other.SetId ); + var set = SetId.Id.CompareTo( other.SetId.Id ); return set != 0 ? set : Slot.CompareTo( other.Slot ); } diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 497c9219..3496c56c 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; @@ -37,13 +38,13 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > [JsonConverter( typeof( StringEnumConverter ) )] public ModelRace Race { get; private init; } - public ushort SetId { get; private init; } + public SetId SetId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] public EstType Slot { get; private init; } [JsonConstructor] - public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) + public EstManipulation( Gender gender, ModelRace race, EstType slot, SetId setId, ushort entry ) { Entry = entry; Gender = gender; @@ -86,7 +87,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > } var s = Slot.CompareTo( other.Slot ); - return s != 0 ? s : SetId.CompareTo( other.SetId ); + return s != 0 ? s : SetId.Id.CompareTo( other.SetId.Id ); } public MetaIndex FileIndex() @@ -94,7 +95,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public bool Apply( EstFile file ) { - return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, Entry ) switch + return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId.Id, Entry ) switch { EstFile.EstEntryChange.Unchanged => false, EstFile.EstEntryChange.Changed => true, diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 38669d12..6ce954fb 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -10,10 +10,10 @@ namespace Penumbra.Meta.Manipulations; public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > { public GmpEntry Entry { get; private init; } - public ushort SetId { get; private init; } + public SetId SetId { get; private init; } [JsonConstructor] - public GmpManipulation( GmpEntry entry, ushort setId ) + public GmpManipulation( GmpEntry entry, SetId setId ) { Entry = entry; SetId = setId; @@ -35,7 +35,7 @@ public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > => SetId.GetHashCode(); public int CompareTo( GmpManipulation other ) - => SetId.CompareTo( other.SetId ); + => SetId.Id.CompareTo( other.SetId.Id ); public MetaIndex FileIndex() => MetaIndex.Gmp; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index d933f73a..51ecd0fb 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,5 +1,4 @@ using System; -using System.Reflection.Metadata; using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -12,28 +11,28 @@ using Penumbra.String.Classes; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct ImcManipulation : IMetaManipulation { - public ImcEntry Entry { get; private init; } - public ushort PrimaryId { get; private init; } - public ushort SecondaryId { get; private init; } - public byte Variant { get; private init; } + public ImcEntry Entry { get; private init; } + public SetId PrimaryId { get; private init; } + public SetId SecondaryId { get; private init; } + public Variant Variant { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public ObjectType ObjectType { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public EquipSlot EquipSlot { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public BodySlot BodySlot { get; private init; } - public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) + public ImcManipulation(EquipSlot equipSlot, ushort variant, SetId primaryId, ImcEntry entry) { Entry = entry; PrimaryId = primaryId; - Variant = ( byte )Math.Clamp( variant, ( ushort )0, byte.MaxValue ); + Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); SecondaryId = 0; ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; EquipSlot = equipSlot; @@ -45,21 +44,21 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > // so we change the unused value to something nonsensical in that case, just so they do not compare equal, // and clamp the variant to 255. [JsonConstructor] - internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, - EquipSlot equipSlot, ImcEntry entry ) + internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, SetId primaryId, SetId secondaryId, ushort variant, + EquipSlot equipSlot, ImcEntry entry) { Entry = entry; ObjectType = objectType; PrimaryId = primaryId; - Variant = ( byte )Math.Clamp( variant, ( ushort )0, byte.MaxValue ); + Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - if( objectType is ObjectType.Accessory or ObjectType.Equipment ) + if (objectType is ObjectType.Accessory or ObjectType.Equipment) { BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; SecondaryId = 0; EquipSlot = equipSlot; } - else if( objectType is ObjectType.DemiHuman ) + else if (objectType is ObjectType.DemiHuman) { BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; SecondaryId = secondaryId; @@ -73,85 +72,81 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > } } - public ImcManipulation Copy( ImcEntry entry ) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant, EquipSlot, entry); + public ImcManipulation Copy(ImcEntry entry) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant.Id, EquipSlot, entry); public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" : $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}"; - public bool Equals( ImcManipulation other ) - => PrimaryId == other.PrimaryId - && Variant == other.Variant + public bool Equals(ImcManipulation other) + => PrimaryId == other.PrimaryId + && Variant == other.Variant && SecondaryId == other.SecondaryId - && ObjectType == other.ObjectType - && EquipSlot == other.EquipSlot - && BodySlot == other.BodySlot; + && ObjectType == other.ObjectType + && EquipSlot == other.EquipSlot + && BodySlot == other.BodySlot; - public override bool Equals( object? obj ) - => obj is ImcManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is ImcManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot ); + => HashCode.Combine(PrimaryId, Variant, SecondaryId, (int)ObjectType, (int)EquipSlot, (int)BodySlot); - public int CompareTo( ImcManipulation other ) + public int CompareTo(ImcManipulation other) { - var o = ObjectType.CompareTo( other.ObjectType ); - if( o != 0 ) - { + var o = ObjectType.CompareTo(other.ObjectType); + if (o != 0) return o; - } - var i = PrimaryId.CompareTo( other.PrimaryId ); - if( i != 0 ) - { + var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); + if (i != 0) return i; + + if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + var e = EquipSlot.CompareTo(other.EquipSlot); + return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); } - if( ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + if (ObjectType is ObjectType.DemiHuman) { - var e = EquipSlot.CompareTo( other.EquipSlot ); - return e != 0 ? e : Variant.CompareTo( other.Variant ); - } - - if( ObjectType is ObjectType.DemiHuman ) - { - var e = EquipSlot.CompareTo( other.EquipSlot ); - if( e != 0 ) - { + var e = EquipSlot.CompareTo(other.EquipSlot); + if (e != 0) return e; - } } - var s = SecondaryId.CompareTo( other.SecondaryId ); - if( s != 0 ) - { + var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); + if (s != 0) return s; - } - var b = BodySlot.CompareTo( other.BodySlot ); - return b != 0 ? b : Variant.CompareTo( other.Variant ); + var b = BodySlot.CompareTo(other.BodySlot); + return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); } public MetaIndex FileIndex() - => ( MetaIndex )( -1 ); + => (MetaIndex)(-1); public Utf8GamePath GamePath() { return ObjectType switch { - ObjectType.Accessory => Utf8GamePath.FromString( GamePaths.Accessory.Imc.Path( PrimaryId ), out var p ) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString( GamePaths.Equipment.Imc.Path( PrimaryId ), out var p ) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString( GamePaths.DemiHuman.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString( GamePaths.Monster.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString( GamePaths.Weapon.Imc.Path( PrimaryId, SecondaryId ), out var p ) ? p : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), + ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId), out var p) ? p : Utf8GamePath.Empty, + _ => throw new NotImplementedException(), }; } - public bool Apply( ImcFile file ) - => file.SetEntry( ImcFile.PartIndex( EquipSlot ), Variant, Entry ); + public bool Apply(ImcFile file) + => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); public bool Validate() { @@ -165,12 +160,14 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return false; if (SecondaryId != 0) return false; + break; case ObjectType.DemiHuman: if (BodySlot is not BodySlot.Unknown) return false; if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) return false; + break; default: if (!Enum.IsDefined(BodySlot)) @@ -179,6 +176,7 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return false; if (!Enum.IsDefined(ObjectType)) return false; + break; } @@ -187,4 +185,4 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > return true; } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index a8aec374..7fd77199 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -15,7 +15,7 @@ public static class CustomizationSwap /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. public static FileSwap CreateMdl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) { - if( idFrom.Value > byte.MaxValue ) + if( idFrom.Id > byte.MaxValue ) { throw new Exception( $"The Customization ID {idFrom} is too large for {slot}." ); } @@ -51,9 +51,9 @@ public static class CustomizationSwap var newFileName = fileName; newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); - newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value ); + newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom != idTo ); newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race || MaterialHandling.IsSpecialCase( race, idFrom ) ); - newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value ); + newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Id:D4}", gameSetIdFrom != idFrom ); var actualMtrlFromPath = mtrlFromPath; if( newFileName != fileName ) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3d444e1f..72cb9a03 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -54,11 +54,11 @@ public static class EquipmentSwap throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slotTo, variantTo, idTo.Value, default); + var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); var imcFileTo = new ImcFile(manager, imcManip); var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo))).Imc.Entry.MaterialId; + var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -124,7 +124,7 @@ public static class EquipmentSwap foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slot, variantTo, idTo.Value, default); + var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); var imcFileTo = new ImcFile(manager, imcManip); var isAccessory = slot.IsAccessory(); @@ -198,10 +198,10 @@ public static class EquipmentSwap SetId idTo, byte mtrlTo) { var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom.Value), slotFrom, gender, - race, idFrom.Value); - var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo.Value), slotTo, gender, race, - idTo.Value); + var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom), slotFrom, gender, + race, idFrom); + var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo), slotTo, gender, race, + idTo); var meta = new MetaSwap(manips, eqdpFrom, eqdpTo); var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom); if (ownMdl) @@ -240,7 +240,7 @@ public static class EquipmentSwap return mdl; } - private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out byte variant) + private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out Variant variant) { slot = i.Type.ToSlot(); if (!slot.IsEquipmentPiece()) @@ -250,14 +250,14 @@ public static class EquipmentSwap variant = i.Variant; } - private static (ImcFile, byte[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, - SetId idFrom, SetId idTo, byte variantFrom) + private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, + SetId idFrom, SetId idTo, Variant variantFrom) { - var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default); + var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); var imc = new ImcFile(manager, entry); EquipItem[] items; - byte[] variants; - if (idFrom.Value == idTo.Value) + Variant[] variants; + if (idFrom == idTo) { items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray(); variants = new[] @@ -270,7 +270,7 @@ public static class EquipmentSwap items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); - variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray(); + variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } return (imc, variants, items); @@ -282,24 +282,23 @@ public static class EquipmentSwap if (slot is not EquipSlot.Head) return null; - var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom.Value), idFrom.Value); - var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo.Value), idTo.Value); + var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom), idFrom); + var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo), idTo); return new MetaSwap(manips, manipFrom, manipTo); } public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, EquipSlot slot, - SetId idFrom, SetId idTo, - byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + SetId idFrom, SetId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, - byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var manipulationFrom = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, entryFrom); - var manipulationTo = new ImcManipulation(slotTo, variantTo, idTo.Value, entryTo); + var manipulationFrom = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, entryFrom); + var manipulationTo = new ImcManipulation(slotTo, variantTo.Id, idTo, entryTo); var imc = new MetaSwap(manips, manipulationFrom, manipulationTo); var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId); @@ -332,8 +331,8 @@ public static class EquipmentSwap if (vfxId == 0) return null; - var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom.Value, vfxId); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo.Value, vfxId); + var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); + var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) @@ -351,10 +350,10 @@ public static class EquipmentSwap if (slot.IsAccessory()) return null; - var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom.Value); - var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo.Value); - var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom.Value); - var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom.Value); + var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom); + var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo); + var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom); + var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom); return new MetaSwap(manips, eqpFrom, eqpTo); } @@ -368,7 +367,7 @@ public static class EquipmentSwap ref bool dataWasChanged) { var prefix = slotTo.IsAccessory() ? 'a' : 'e'; - if (!fileName.Contains($"{prefix}{idTo.Value:D4}")) + if (!fileName.Contains($"{prefix}{idTo.Id:D4}")) return null; var folderTo = slotTo.IsAccessory() diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 30159591..6ed2112a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -9,7 +9,6 @@ using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.ItemSwap; @@ -163,8 +162,8 @@ public static class ItemSwap } var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( manager, type, genderRace, idFrom.Value ) ); - var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( manager, type, genderRace, idTo.Value ) ); + var fromDefault = new EstManipulation( gender, race, type, idFrom, EstFile.GetDefault( manager, type, genderRace, idFrom ) ); + var toDefault = new EstManipulation( gender, race, type, idTo, EstFile.GetDefault( manager, type, genderRace, idTo ) ); var est = new MetaSwap( manips, fromDefault, toDefault ); if( ownMdl && est.SwapApplied.Est.Entry >= 2 ) @@ -206,7 +205,7 @@ public static class ItemSwap public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true ) => condition - ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" ) + ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Id:D4}" ) : path; public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true ) @@ -217,7 +216,7 @@ public static class ItemSwap public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true ) => condition - ? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" ) + ? path.Replace( $"{type}{idFrom.Id:D4}", $"{type}{idTo.Id:D4}" ) : path; public static string ReplaceSlot( string path, EquipSlot from, EquipSlot to, bool condition = true ) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 9607d2ac..3e81901b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -135,7 +135,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + if (IdInput("##eqpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId); ImGuiUtil.HoverTooltip(ModelSetIdTooltip); @@ -224,7 +224,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) { var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); @@ -352,14 +352,14 @@ public partial class ModEditWindow _ => EquipSlot.Unknown, }; _new = new ImcManipulation(type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? (ushort)1 : _new.SecondaryId, - _new.Variant, equipSlot, _new.Entry); + _new.Variant.Id, equipSlot, _new.Entry); } ImGuiUtil.HoverTooltip(ObjectTypeTooltip); ImGui.TableNextColumn(); - if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry) + if (IdInput("##imcId", IdWidth, _new.PrimaryId.Id, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant.Id, _new.EquipSlot, _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -373,7 +373,7 @@ public partial class ModEditWindow if (_new.ObjectType is ObjectType.Equipment) { if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -382,7 +382,7 @@ public partial class ModEditWindow else if (_new.ObjectType is ObjectType.Accessory) { if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -390,8 +390,8 @@ public partial class ModEditWindow } else { - if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry) + if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId.Id, out var setId2, 0, ushort.MaxValue, false)) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant.Id, _new.EquipSlot, _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -399,7 +399,7 @@ public partial class ModEditWindow } ImGui.TableNextColumn(); - if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false)) + if (IdInput("##imcVariant", SmallIdWidth, _new.Variant.Id, out var variant, 0, byte.MaxValue, false)) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new.Entry).Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -408,7 +408,7 @@ public partial class ModEditWindow if (_new.ObjectType is ObjectType.DemiHuman) { if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -557,7 +557,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + if (IdInput("##estId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) { var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); @@ -656,7 +656,7 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) + if (IdInput("##gmpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId); ImGuiUtil.HoverTooltip(ModelSetIdTooltip); diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 41b5f420..b2f13e51 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -52,7 +52,7 @@ public class ChangedItemDrawer : IDisposable private readonly ExcelSheet _items; private readonly CommunicatorService _communicator; private readonly Dictionary _icons = new(16); - private float _smallestIconWidth = 0; + private float _smallestIconWidth; public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator, Configuration config) { @@ -265,7 +265,7 @@ public class ChangedItemDrawer : IDisposable switch (obj) { case EquipItem it: - text = it.WeaponType == 0 ? $"({it.ModelId.Value}-{it.Variant})" : $"({it.ModelId.Value}-{it.WeaponType.Value}-{it.Variant})"; + text = it.ModelString; return true; case ModelChara m: text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; @@ -280,7 +280,7 @@ public class ChangedItemDrawer : IDisposable private object? Convert(object? data) { if (data is EquipItem it) - return _items.GetRow(it.ItemId); + return _items.GetRow(it.ItemId.Id); return data; } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 2fd21c6f..9d5caa0c 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -503,10 +504,10 @@ public class DebugTab : Window, ITab if (table) for (var i = 0; i < 8; ++i) { - var c = agent->Character(i); + ref var c = ref agent->Data->CharacterArraySpan[i]; ImGuiUtil.DrawTableColumn($"Character {i}"); - var name = c->Name1.ToString(); - ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})"); + var name = c.Name1.ToString(); + ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})"); } } else From ef916fc93cadbefb27ef8d6e3463e6a3a83372e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Jul 2023 02:46:30 +0200 Subject: [PATCH 1069/2451] Add backup option for failure to load option groups. --- Penumbra/Services/SaveService.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index ce0957f0..8429863b 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -2,6 +2,7 @@ using System; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; namespace Penumbra.Services; @@ -18,18 +19,21 @@ public sealed class SaveService : SaveServiceBase { } /// Immediately delete all existing option group files for a mod and save them anew. - public void SaveAllOptionGroups(Mod mod) + public void SaveAllOptionGroups(Mod mod, bool backup) { foreach (var file in FileNames.GetOptionGroupFiles(mod)) { try { if (file.Exists) - file.Delete(); + if (backup) + file.MoveTo(file.FullName + ".bak", true); + else + file.Delete(); } catch (Exception e) { - Log.Error($"Could not delete outdated group file {file}:\n{e}"); + Log.Error($"Could not {(backup ? "move" : "delete")} outdated group file {file}:\n{e}"); } } From 1d5e050de609bdfee8b079e05553e769e7877ad4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Jul 2023 02:46:43 +0200 Subject: [PATCH 1070/2451] Move some classes. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 3 ++- Penumbra/Mods/Editor/DuplicateManager.cs | 1 + Penumbra/Mods/Editor/IMod.cs | 1 + Penumbra/Mods/Editor/ModEditor.cs | 1 + Penumbra/Mods/Manager/ModMigration.cs | 1 + Penumbra/Mods/Manager/ModOptionEditor.cs | 6 +++--- Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/ModCreator.cs | 7 +++++-- Penumbra/Mods/Subclasses/IModGroup.cs | 3 +-- Penumbra/Mods/Subclasses/ISubMod.cs | 1 + Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 1 + Penumbra/Mods/Subclasses/ModSettings.cs | 1 + Penumbra/Mods/Subclasses/MultiModGroup.cs | 1 + Penumbra/Mods/Subclasses/SingleModGroup.cs | 1 + Penumbra/Mods/TemporaryMod.cs | 1 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 1 + 18 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 6de16612..0776faea 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,8 +4,9 @@ using System.IO; using System.Linq; using Newtonsoft.Json; using Penumbra.Api.Enums; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; using Penumbra.Util; using SharpCompress.Archives.Zip; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index f4583f70..3a58d91a 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 7741cf6a..88f04ef3 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using OtterGui.Classes; +using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index a874f629..bd774607 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -2,6 +2,7 @@ using System; using System.IO; using OtterGui; using Penumbra.Mods.Editor; +using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 28c37175..d6c5e63f 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index ab6e839a..4bf774fb 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; @@ -127,11 +128,10 @@ public class ModOptionEditor /// Delete a given option group. Fires an event to prepare before actually deleting. public void DeleteModGroup(Mod mod, int groupIdx) { - var group = mod.Groups[groupIdx]; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); UpdateSubModPositions(mod, groupIdx); - _saveService.SaveAllOptionGroups(mod); + _saveService.SaveAllOptionGroups(mod, false); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } @@ -142,7 +142,7 @@ public class ModOptionEditor return; UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - _saveService.SaveAllOptionGroups(mod); + _saveService.SaveAllOptionGroups(mod, false); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 242ec260..6aac7e1e 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -7,6 +7,7 @@ using OtterGui.Classes; using Penumbra.Collections.Cache; using Penumbra.Import; using Penumbra.Meta; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 527c658d..ce63fd42 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -15,6 +15,7 @@ using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; @@ -117,7 +118,9 @@ public partial class ModCreator } if (changes) - _saveService.SaveAllOptionGroups(mod); + { + _saveService.SaveAllOptionGroups(mod, true); + } } /// Load the default option for a given mod. @@ -182,7 +185,7 @@ public partial class ModCreator if (!changes) return; - _saveService.SaveAllOptionGroups(mod); + _saveService.SaveAllOptionGroups(mod, false); _saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default)); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index c5087711..f66f29ea 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -4,9 +4,8 @@ using System.IO; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Services; -using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Subclasses; public interface IModGroup : IEnumerable { diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 2693fcad..bf11527f 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 24e9aafe..aad74a13 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json.Linq; using Penumbra.Import; using Penumbra.Meta; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 441ffea9..ae06a082 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -7,6 +7,7 @@ using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 40f3d37e..facbacdc 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index b330c00d..a67ee1e5 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6959194f..ef721414 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index a705393d..726a83e1 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -20,6 +20,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 786a6130..c629de5b 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -14,6 +14,7 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 8d831279..0378d620 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.UI.ModsTab; From 4df616e4c0cdb604271b01309fcba54fba9ab8bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 30 Jul 2023 13:23:03 +0200 Subject: [PATCH 1071/2451] Fix CustomItem recognition. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 263bfb49..1e65d3fd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 263bfb49c998700197a18ad99fa1daadc8736f5d +Subproject commit 1e65d3fd028d3ac58090a8c886f351acbd9f3a2a From 3738b5f8f052d91624b9f143b2431722a1775001 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 30 Jul 2023 11:27:53 +0000 Subject: [PATCH 1072/2451] [CI] Updating repo.json for testing_0.7.2.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2631d285..0a8fb60c 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.3", + "TestingAssemblyVersion": "0.7.2.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a95877b9e45d7b77a1188df03ec99c65a7fa71c2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 1 Aug 2023 13:11:33 +0200 Subject: [PATCH 1073/2451] Add priority display to mod selector. --- Penumbra/Configuration.cs | 1 + Penumbra/Mods/ModCreator.cs | 1 - Penumbra/UI/Classes/Colors.cs | 4 ++- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 28 +++++++++++++++++++- Penumbra/UI/Tabs/SettingsTab.cs | 3 +++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index da2fd935..beaf1960 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -50,6 +50,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ce63fd42..d63627f5 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -18,7 +18,6 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 42577034..450f3787 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -23,7 +23,8 @@ public enum ColorId SelectedCollection, RedundantAssignment, NoModsAssignment, - NoAssignment, + NoAssignment, + SelectorPriority, } public static class Colors @@ -62,6 +63,7 @@ public static class Colors ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), + ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2b943f82..f3ea815a 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -168,6 +168,27 @@ public sealed class ModFileSystemSelector : FileSystemSelector ImGui.GetStyle().ItemSpacing.X) + { + c.Push(ImGuiCol.Text, ColorId.SelectorPriority.Value()); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + ImGui.TextUnformatted(priorityString); + } + else + { + ImGui.NewLine(); + } + } } @@ -468,6 +489,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Combined wrapper for handling all filters and setting state. private bool ApplyFiltersAndState(ModFileSystem.Leaf leaf, out ModState state) { - state = new ModState { Color = ColorId.EnabledMod }; var mod = leaf.Value; var (settings, collection) = _collectionManager.Active.Current[mod.Index]; + state = new ModState + { + Color = ColorId.EnabledMod, + Priority = settings?.Priority ?? 0, + }; if (ApplyStringFilters(leaf, mod)) return true; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 84432ce6..375ada2d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -379,6 +379,9 @@ public class SettingsTab : ITab if (v) _config.ChangedItemFilter = ChangedItemDrawer.AllFlags; }); + Checkbox("Hide Priority Numbers in Mod Selector", + "Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them.", + _config.HidePrioritiesInSelector, v => _config.HidePrioritiesInSelector = v); DrawSingleSelectRadioMax(); DrawCollapsibleGroupMin(); } From 2da6a33a62bcfe844ba28eea72fa93988bdb922e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 1 Aug 2023 13:11:57 +0200 Subject: [PATCH 1074/2451] Some texfile formatting. --- Penumbra/Import/Textures/TexFileParser.cs | 143 ++++++++++------------ 1 file changed, 63 insertions(+), 80 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 6b77bd0e..f0c3beca 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -8,36 +8,30 @@ namespace Penumbra.Import.Textures; public static class TexFileParser { - public static ScratchImage Parse( Stream data ) + public static ScratchImage Parse(Stream data) { - using var r = new BinaryReader( data ); - var header = r.ReadStructure< TexFile.TexHeader >(); + using var r = new BinaryReader(data); + var header = r.ReadStructure(); var meta = header.ToTexMeta(); - if( meta.Format == DXGIFormat.Unknown ) - { - throw new Exception( $"Could not convert format {header.Format} to DXGI Format." ); - } + if (meta.Format == DXGIFormat.Unknown) + throw new Exception($"Could not convert format {header.Format} to DXGI Format."); - if( meta.Dimension == TexDimension.Unknown ) - { - throw new Exception( $"Could not obtain dimensionality from {header.Type}." ); - } + if (meta.Dimension == TexDimension.Unknown) + throw new Exception($"Could not obtain dimensionality from {header.Type}."); - meta.MipLevels = CountMipLevels( data, in meta, in header ); - if( meta.MipLevels == 0 ) - { - throw new Exception( "Could not load file. Image is corrupted and does not contain enough data for its size." ); - } + meta.MipLevels = CountMipLevels(data, in meta, in header); + if (meta.MipLevels == 0) + throw new Exception("Could not load file. Image is corrupted and does not contain enough data for its size."); - var scratch = ScratchImage.Initialize( meta ); + var scratch = ScratchImage.Initialize(meta); - CopyData( scratch, r ); + CopyData(scratch, r); return scratch; } - private static unsafe int CountMipLevels( Stream data, in TexMeta meta, in TexFile.TexHeader header ) + private static unsafe int CountMipLevels(Stream data, in TexMeta meta, in TexFile.TexHeader header) { var width = meta.Width; var height = meta.Height; @@ -46,28 +40,22 @@ public static class TexFileParser var lastOffset = 0L; var lastSize = 80L; var minSize = meta.Format.IsCompressed() ? 4 : 1; - for( var i = 0; i < 13; ++i ) + for (var i = 0; i < 13; ++i) { - var offset = header.OffsetToSurface[ i ]; - if( offset == 0 ) - { + var offset = header.OffsetToSurface[i]; + if (offset == 0) return i; - } var requiredSize = width * height * bits / 8; - if( offset + requiredSize > data.Length ) - { + if (offset + requiredSize > data.Length) return i; - } var diff = offset - lastOffset; - if( diff != lastSize ) - { + if (diff != lastSize) return i; - } - width = Math.Max( width / 2, minSize ); - height = Math.Max( height / 2, minSize ); + width = Math.Max(width / 2, minSize); + height = Math.Max(height / 2, minSize); lastOffset = offset; lastSize = requiredSize; } @@ -75,48 +63,45 @@ public static class TexFileParser return 13; } - private static unsafe void CopyData( ScratchImage image, BinaryReader r ) + private static unsafe void CopyData(ScratchImage image, BinaryReader r) { - fixed( byte* ptr = image.Pixels ) + fixed (byte* ptr = image.Pixels) { - var span = new Span< byte >( ptr, image.Pixels.Length ); - var readBytes = r.Read( span ); - if( readBytes < image.Pixels.Length ) - { - throw new Exception( $"Invalid data length {readBytes} < {image.Pixels.Length}." ); - } + var span = new Span(ptr, image.Pixels.Length); + var readBytes = r.Read(span); + if (readBytes < image.Pixels.Length) + throw new Exception($"Invalid data length {readBytes} < {image.Pixels.Length}."); } } - public static void Write( this TexFile.TexHeader header, BinaryWriter w ) + public static void Write(this TexFile.TexHeader header, BinaryWriter w) { - w.Write( ( uint )header.Type ); - w.Write( ( uint )header.Format ); - w.Write( header.Width ); - w.Write( header.Height ); - w.Write( header.Depth ); - w.Write( header.MipLevels ); + w.Write((uint)header.Type); + w.Write((uint)header.Format); + w.Write(header.Width); + w.Write(header.Height); + w.Write(header.Depth); + w.Write((byte) header.MipLevels); + w.Write((byte) 0); // TODO Lumina Update unsafe { - w.Write( header.LodOffset[ 0 ] ); - w.Write( header.LodOffset[ 1 ] ); - w.Write( header.LodOffset[ 2 ] ); - for( var i = 0; i < 13; ++i ) - { - w.Write( header.OffsetToSurface[ i ] ); - } + w.Write(header.LodOffset[0]); + w.Write(header.LodOffset[1]); + w.Write(header.LodOffset[2]); + for (var i = 0; i < 13; ++i) + w.Write(header.OffsetToSurface[i]); } } - public static TexFile.TexHeader ToTexHeader( this ScratchImage scratch ) + public static TexFile.TexHeader ToTexHeader(this ScratchImage scratch) { var meta = scratch.Meta; var ret = new TexFile.TexHeader() { - Height = ( ushort )meta.Height, - Width = ( ushort )meta.Width, - Depth = ( ushort )Math.Max( meta.Depth, 1 ), - MipLevels = ( ushort )Math.Min( meta.MipLevels, 12 ), + Height = (ushort)meta.Height, + Width = (ushort)meta.Width, + Depth = (ushort)Math.Max(meta.Depth, 1), + MipLevels = (byte)Math.Min(meta.MipLevels, 12), Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { @@ -128,50 +113,48 @@ public static class TexFileParser }, }; - ret.FillSurfaceOffsets( scratch ); + ret.FillSurfaceOffsets(scratch); return ret; } - private static unsafe void FillSurfaceOffsets( this ref TexFile.TexHeader header, ScratchImage scratch ) + private static unsafe void FillSurfaceOffsets(this ref TexFile.TexHeader header, ScratchImage scratch) { var idx = 0; - fixed( byte* ptr = scratch.Pixels ) + fixed (byte* ptr = scratch.Pixels) { - foreach( var image in scratch.Images ) + foreach (var image in scratch.Images) { - var offset = ( byte* )image.Pixels - ptr; - header.OffsetToSurface[ idx++ ] = ( uint )( 80 + offset ); + var offset = (byte*)image.Pixels - ptr; + header.OffsetToSurface[idx++] = (uint)(80 + offset); } } - for( ; idx < 13; ++idx ) - { - header.OffsetToSurface[ idx ] = 0; - } + for (; idx < 13; ++idx) + header.OffsetToSurface[idx] = 0; - header.LodOffset[ 0 ] = 0; - header.LodOffset[ 1 ] = 1; - header.LodOffset[ 2 ] = 2; + header.LodOffset[0] = 0; + header.LodOffset[1] = 1; + header.LodOffset[2] = 2; } - public static TexMeta ToTexMeta( this TexFile.TexHeader header ) + public static TexMeta ToTexMeta(this TexFile.TexHeader header) => new() { Height = header.Height, Width = header.Width, - Depth = Math.Max( header.Depth, ( ushort )1 ), + Depth = Math.Max(header.Depth, (ushort)1), MipLevels = header.MipLevels, ArraySize = 1, Format = header.Format.ToDXGI(), Dimension = header.Type.ToDimension(), - MiscFlags = header.Type.HasFlag( TexFile.Attribute.TextureTypeCube ) ? D3DResourceMiscFlags.TextureCube : 0, + MiscFlags = header.Type.HasFlag(TexFile.Attribute.TextureTypeCube) ? D3DResourceMiscFlags.TextureCube : 0, MiscFlags2 = 0, }; - private static TexDimension ToDimension( this TexFile.Attribute attribute ) - => ( attribute & TexFile.Attribute.TextureTypeMask ) switch + private static TexDimension ToDimension(this TexFile.Attribute attribute) + => (attribute & TexFile.Attribute.TextureTypeMask) switch { TexFile.Attribute.TextureType1D => TexDimension.Tex1D, TexFile.Attribute.TextureType2D => TexDimension.Tex2D, @@ -179,7 +162,7 @@ public static class TexFileParser _ => TexDimension.Unknown, }; - public static TexFile.TextureFormat ToTexFormat( this DXGIFormat format ) + public static TexFile.TextureFormat ToTexFormat(this DXGIFormat format) => format switch { DXGIFormat.R8UNorm => TexFile.TextureFormat.L8, @@ -204,7 +187,7 @@ public static class TexFileParser _ => TexFile.TextureFormat.Unknown, }; - public static DXGIFormat ToDXGI( this TexFile.TextureFormat format ) + public static DXGIFormat ToDXGI(this TexFile.TextureFormat format) => format switch { TexFile.TextureFormat.L8 => DXGIFormat.R8UNorm, @@ -229,4 +212,4 @@ public static class TexFileParser TexFile.TextureFormat.Shadow24 => DXGIFormat.R24G8Typeless, _ => DXGIFormat.Unknown, }; -} \ No newline at end of file +} From 930931a8463e27a8530fd1e7d9b4627b347afa54 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 1 Aug 2023 17:39:41 +0200 Subject: [PATCH 1075/2451] Fix ChangeCustomize not loading decals from collections. --- Penumbra/Interop/PathResolving/MetaState.cs | 29 +++++++++------------ 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 2f6260a9..a4cbc967 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -54,6 +53,7 @@ public unsafe class MetaState : IDisposable private readonly CharacterUtility _characterUtility; private ResolveData _lastCreatedCollection = ResolveData.Invalid; + private ResolveData _customizeChangeCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, @@ -81,10 +81,10 @@ public unsafe class MetaState : IDisposable public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) { if (type == ResourceType.Tex - && _lastCreatedCollection.Valid + && (_lastCreatedCollection.Valid || _customizeChangeCollection.Valid) && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) { - resolveData = _lastCreatedCollection; + resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : _customizeChangeCollection; return true; } @@ -129,8 +129,9 @@ public unsafe class MetaState : IDisposable _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, customize)); - var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); + var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, + UsesDecal(*(uint*)modelCharaId, customize)); + var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); } @@ -228,7 +229,7 @@ public unsafe class MetaState : IDisposable private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5) { - if (_inChangeCustomize) + if (_customizeChangeCollection.Valid) { _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); } @@ -241,9 +242,6 @@ public unsafe class MetaState : IDisposable } } - /// ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. - private bool _inChangeCustomize; - private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment); [Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))] @@ -252,13 +250,12 @@ public unsafe class MetaState : IDisposable private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment) { using var performance = _performance.Measure(PerformanceType.ChangeCustomize); - _inChangeCustomize = true; - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - using var decals = new DecalReverter(_config, _characterUtility, _resources, resolveData, true); - using var decal2 = new DecalReverter(_config, _characterUtility, _resources, resolveData, false); - var ret = _changeCustomize.Original(human, data, skipEquipment); - _inChangeCustomize = false; + _customizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + using var cmp = _customizeChangeCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); + using var decals = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, true); + using var decal2 = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, false); + var ret = _changeCustomize.Original(human, data, skipEquipment); + _customizeChangeCollection = ResolveData.Invalid; return ret; } From 622af4e7e9f2abe45b0b8a9bcbd46f095fcdc67f Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 1 Aug 2023 15:45:07 +0000 Subject: [PATCH 1076/2451] [CI] Updating repo.json for testing_0.7.2.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0a8fb60c..a3ea22c3 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.4", + "TestingAssemblyVersion": "0.7.2.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 01b88950bf2fa9b9cbf4cdfb6d9108d946ddbe8c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 1 Aug 2023 20:37:57 +0200 Subject: [PATCH 1077/2451] Fix shit. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 24 ++++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index f3ea815a..6eecf36a 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -170,24 +170,18 @@ public sealed class ModFileSystemSelector : FileSystemSelector ImGui.GetStyle().ItemSpacing.X) - { - c.Push(ImGuiCol.Text, ColorId.SelectorPriority.Value()); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); - ImGui.TextUnformatted(priorityString); - } - else - { - ImGui.NewLine(); - } + ImGui.GetWindowDrawList().AddText(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value(), priorityString); } } From af0edf30029b4c0de1abed888e5834f1b7c90856 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 1 Aug 2023 18:40:28 +0000 Subject: [PATCH 1078/2451] [CI] Updating repo.json for testing_0.7.2.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a3ea22c3..fb7afb7e 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.5", + "TestingAssemblyVersion": "0.7.2.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0e252d489ae721a5ff842df31c016f16f54c8dc4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Aug 2023 02:14:47 +0200 Subject: [PATCH 1079/2451] Update SharpCompress. --- Penumbra/Penumbra.csproj | 2 +- Penumbra/packages.lock.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index cddc5812..ec433113 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -70,7 +70,7 @@ - + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 26a67367..eed5d7c8 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -22,9 +22,9 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.32.1, )", - "resolved": "0.32.1", - "contentHash": "9Cwj3lK/p7wkiBaQPCvaKINuHYuZ0ACDldA4M3o5ISSq7cjbbq3yqigTDBUoKjtyyXpqmQHUkw6fhLnjNF30ow==" + "requested": "[0.33.0, )", + "resolved": "0.33.0", + "contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw==" }, "SixLabors.ImageSharp": { "type": "Direct", @@ -81,8 +81,8 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.7, )", - "Penumbra.String": "[1.0.3, )" + "Penumbra.Api": "[1.0.8, )", + "Penumbra.String": "[1.0.4, )" } }, "penumbra.string": { From 2a7ccb952de46595428c86f109592e56735951b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Aug 2023 02:33:25 +0200 Subject: [PATCH 1080/2451] Fix missing scaling for item combos. --- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 51 +++-------------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 726a83e1..6e810000 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using ImGuiNET; @@ -469,7 +470,7 @@ public class ItemSwapTab : IDisposable, ITab } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -494,7 +495,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (_affectedItems is not { Length: > 1 }) return; @@ -535,7 +536,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) @@ -548,7 +549,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) { @@ -617,48 +618,6 @@ public class ItemSwapTab : IDisposable, ITab DrawGenderInput("for all Viera", 0); } - - private void DrawWeaponSwap() - { - using var disabled = ImRaii.Disabled(); - using var tab = DrawTab(SwapType.Weapon); - if (!tab) - return; - - using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Select the weapon or tool you want"); - ImGui.TableNextColumn(); - if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, - ImGui.GetTextLineHeightWithSpacing())) - { - _dirty = true; - _weaponSource = new ItemSelector(_itemService, _slotSelector.CurrentSelection); - _weaponTarget = new ItemSelector(_itemService, _slotSelector.CurrentSelection); - } - else - { - _dirty = _weaponSource == null || _weaponTarget == null; - _weaponSource ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection); - _weaponTarget ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection); - } - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("and put this variant of it"); - ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Name, string.Empty, InputWidth * 2, - ImGui.GetTextLineHeightWithSpacing()); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("onto this one"); - ImGui.TableNextColumn(); - _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Name, string.Empty, InputWidth * 2, - ImGui.GetTextLineHeightWithSpacing()); - } - private const float InputWidth = 120; private void DrawTargetIdInput(string text = "Take this ID") From 2f836426d6520ff5d74544617ed195b1acb34a1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Aug 2023 17:01:53 +0200 Subject: [PATCH 1081/2451] Temporary fix for broken CS offset. --- Penumbra.GameData | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 +----- Penumbra/UI/Tabs/DebugTab.cs | 6 ++++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1e65d3fd..9eb60aa0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1e65d3fd028d3ac58090a8c886f351acbd9f3a2a +Subproject commit 9eb60aa0fdaad4a10af2edd77b154a20c7647ce4 diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6e810000..c7c09de2 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -152,11 +152,7 @@ public class ItemSwapTab : IDisposable, ITab } private readonly Dictionary _selectors; - - private ItemSelector? _weaponSource; - private ItemSelector? _weaponTarget; - private readonly WeaponSelector _slotSelector = new(); - private readonly ItemSwapContainer _swapData; + private readonly ItemSwapContainer _swapData; private Mod? _mod; private ModSettings? _modSettings; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 9d5caa0c..ee12e8a2 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -500,11 +500,13 @@ public class DebugTab : Window, ITab if (agent->Data != null) { - using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); + // TODO fix when updated in CS. + var characterData = (AgentBannerInterface.Storage.CharacterData*)((byte*)agent->Data + 0x20); + using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) for (var i = 0; i < 8; ++i) { - ref var c = ref agent->Data->CharacterArraySpan[i]; + ref var c = ref *(characterData + i); ImGuiUtil.DrawTableColumn($"Character {i}"); var name = c.Name1.ToString(); ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})"); From e24a535a93fe85bd8b308d1e9ef769f35901ff90 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 8 Aug 2023 01:10:57 +0200 Subject: [PATCH 1082/2451] Initial Texture rework. --- OtterGui | 2 +- Penumbra/Import/Textures/BaseImage.cs | 111 +++++ .../Textures/CombinedTexture.Manipulation.cs | 38 +- Penumbra/Import/Textures/CombinedTexture.cs | 182 ++------ Penumbra/Import/Textures/TexFileParser.cs | 2 +- Penumbra/Import/Textures/Texture.cs | 256 ++-------- Penumbra/Import/Textures/TextureDrawer.cs | 139 ++++++ Penumbra/Import/Textures/TextureImporter.cs | 61 --- Penumbra/Import/Textures/TextureManager.cs | 438 ++++++++++++++++++ Penumbra/Mods/Editor/ModBackup.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 1 + Penumbra/Penumbra.cs | 3 +- Penumbra/Services/ServiceManager.cs | 4 +- Penumbra/Services/ServiceWrapper.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 76 ++- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 3 +- 16 files changed, 831 insertions(+), 489 deletions(-) create mode 100644 Penumbra/Import/Textures/BaseImage.cs create mode 100644 Penumbra/Import/Textures/TextureDrawer.cs delete mode 100644 Penumbra/Import/Textures/TextureImporter.cs create mode 100644 Penumbra/Import/Textures/TextureManager.cs diff --git a/OtterGui b/OtterGui index e3d26f16..8d61845c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e3d26f16234a4295bf3c7802d87ce43293c6ffe0 +Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4 diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs new file mode 100644 index 00000000..f0f6a47e --- /dev/null +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -0,0 +1,111 @@ +using System; +using System.Numerics; +using Lumina.Data.Files; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Textures; + +public readonly struct BaseImage : IDisposable +{ + public readonly object? Image; + + public BaseImage(ScratchImage scratch) + => Image = scratch; + + public BaseImage(Image image) + => Image = image; + + public static implicit operator BaseImage(ScratchImage scratch) + => new(scratch); + + public static implicit operator BaseImage(Image img) + => new(img); + + public ScratchImage? AsDds + => Image as ScratchImage; + + public Image? AsPng + => Image as Image; + + public TexFile? AsTex + => Image as TexFile; + + public TextureType Type + => Image switch + { + null => TextureType.Unknown, + ScratchImage => TextureType.Dds, + Image => TextureType.Png, + _ => TextureType.Unknown, + }; + + public void Dispose() + => (Image as IDisposable)?.Dispose(); + + /// Obtain RGBA pixel data for the given image (not including any mip maps.) + public (byte[] Rgba, int Width, int Height) GetPixelData() + { + switch (Image) + { + case null: return (Array.Empty(), 0, 0); + case ScratchImage scratch: + { + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + return (rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(), f.Meta.Width, + f.Meta.Height); + } + case Image img: + { + var ret = new byte[img.Height * img.Width * 4]; + img.CopyPixelDataTo(ret); + return (ret, img.Width, img.Height); + } + default: return (Array.Empty(), 0, 0); + } + } + + public (int Width, int Height) Dimensions + => Image switch + { + null => (0, 0), + ScratchImage scratch => (scratch.Meta.Width, scratch.Meta.Height), + Image img => (img.Width, img.Height), + _ => (0, 0), + }; + + public int Width + => Dimensions.Width; + + public int Height + => Dimensions.Height; + + public Vector2 ImageSize + { + get + { + var (width, height) = Dimensions; + return new Vector2(width, height); + } + } + + public DXGIFormat Format + => Image switch + { + null => DXGIFormat.Unknown, + ScratchImage s => s.Meta.Format, + TexFile t => t.Header.Format.ToDXGI(), + Image => DXGIFormat.B8G8R8X8UNorm, + _ => DXGIFormat.Unknown, + }; + + public int MipMaps + => Image switch + { + null => 0, + ScratchImage s => s.Meta.MipLevels, + TexFile t => t.Header.MipLevels, + _ => 1, + }; +} diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 16bd6dfe..a32b9578 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -16,14 +16,13 @@ public partial class CombinedTexture private bool _invertLeft = false; private bool _invertRight = false; private int _offsetX = 0; - private int _offsetY = 0; - + private int _offsetY = 0; private Vector4 DataLeft( int offset ) - => CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft ); + => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft ); private Vector4 DataRight( int offset ) - => CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); private Vector4 DataRight( int x, int y ) { @@ -35,7 +34,7 @@ public partial class CombinedTexture } var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); } private void AddPixelsMultiplied( int y, ParallelLoopState _ ) @@ -49,10 +48,10 @@ public partial class CombinedTexture var rgba = alpha == 0 ? new Rgba32() : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -63,10 +62,10 @@ public partial class CombinedTexture var offset = ( _left.TextureWrap!.Width * y + x ) * 4; var left = DataLeft( offset ); var rgba = new Rgba32( left ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -77,10 +76,10 @@ public partial class CombinedTexture var offset = ( _right.TextureWrap!.Width * y + x ) * 4; var left = DataRight( offset ); var rgba = new Rgba32( left ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -90,8 +89,8 @@ public partial class CombinedTexture var (width, height) = _left.IsLoaded ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); - _centerStorage.RGBAPixels = new byte[width * height * 4]; - _centerStorage.Type = Texture.FileType.Bitmap; + _centerStorage.RgbaPixels = new byte[width * height * 4]; + _centerStorage.Type = TextureType.Bitmap; if( _left.IsLoaded ) { Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); @@ -103,7 +102,6 @@ public partial class CombinedTexture return ( width, height ); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) { if( bytes.Count == 0 ) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index bf017048..99303234 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,14 +1,5 @@ using System; -using System.IO; using System.Numerics; -using Dalamud.Interface; -using Lumina.Data.Files; -using OtterTex; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using DalamudUtil = Dalamud.Utility.Util; -using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; @@ -38,170 +29,58 @@ public partial class CombinedTexture : IDisposable private readonly Texture _centerStorage = new(); + public Guid SaveGuid { get; private set; } = Guid.Empty; + public bool IsLoaded => _mode != Mode.Empty; - - public bool IsLeftCopy - => _mode == Mode.LeftCopy; - public Exception? SaveException { get; private set; } = null; + public bool IsLeftCopy + => _mode == Mode.LeftCopy; - public void Draw( UiBuilder builder, Vector2 size ) + public void Draw(TextureManager textures, Vector2 size) { - if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) + if (_mode == Mode.Custom && !_centerStorage.IsLoaded) { - var (width, height) = CombineImage(); - _centerStorage.TextureWrap = - builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + var (width, height) = CombineImage(); + _centerStorage.TextureWrap = textures.LoadTextureWrap(_centerStorage.RgbaPixels, width, height); } - _current?.Draw( size ); + if (_current != null) + TextureDrawer.Draw(_current, size); } - public void SaveAsPng( string path ) + public void SaveAsPng(TextureManager textures, string path) { - if( !IsLoaded || _current == null ) - { + if (!IsLoaded || _current == null) return; - } - try - { - var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height ); - image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } + SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } - private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex ) + private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) { - if( _current == null || _mode == Mode.Empty ) - { + if (!IsLoaded || _current == null) return; - } - try - { - if( _current.BaseImage is not ScratchImage s ) - { - s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); - } - - var tex = type switch - { - TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s, - TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), - TextureSaveType.BC3 => CreateCompressed( s, mipMaps, false ), - TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - - if( !writeTex ) - { - tex.SaveDDS( path ); - } - else - { - SaveTex( path, tex ); - } - - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } + SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); } - private static void SaveTex( string path, ScratchImage input ) - { - var header = input.ToTexHeader(); - if( header.Format == TexFile.TextureFormat.Unknown ) - { - throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." ); - } + public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps) + => SaveAs(textures, path, type, mipMaps, true); - using var stream = File.Open( path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); - using var w = new BinaryWriter( stream ); - header.Write( w ); - w.Write( input.Pixels ); - } - - private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) - { - if( !mipMaps ) - { - return input; - } - - var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ); - var ec = input.GenerateMipMaps( out var ret, numMips, ( DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0 ) | FilterFlags.SeparateAlpha ); - if (ec != ErrorCode.Ok) - { - throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" ); - } - - return ret; - } - - private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) - { - if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm ) - { - return AddMipMaps( input, mipMaps ); - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - else - { - input = input.Convert( DXGIFormat.B8G8R8A8UNorm ); - } - - return AddMipMaps( input, mipMaps ); - } - - private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 ) - { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; - if( input.Meta.Format == format ) - { - return input; - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - - input = AddMipMaps( input, mipMaps ); - - return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel ); - } - - public void SaveAsTex( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, true ); - - public void SaveAsDds( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, false ); + public void SaveAsDds(TextureManager textures, string path, TextureSaveType type, bool mipMaps) + => SaveAs(textures, path, type, mipMaps, false); - public CombinedTexture( Texture left, Texture right ) + public CombinedTexture(Texture left, Texture right) { _left = left; _right = right; _left.Loaded += OnLoaded; _right.Loaded += OnLoaded; - OnLoaded( false ); + OnLoaded(false); } public void Dispose() @@ -211,20 +90,20 @@ public partial class CombinedTexture : IDisposable _right.Loaded -= OnLoaded; } - private void OnLoaded( bool _ ) + private void OnLoaded(bool _) => Update(); public void Update() { Clean(); - if( _left.IsLoaded ) + if (_left.IsLoaded) { - if( _right.IsLoaded ) + if (_right.IsLoaded) { _current = _centerStorage; _mode = Mode.Custom; } - else if( !_invertLeft && _multiplierLeft.IsIdentity ) + else if (!_invertLeft && _multiplierLeft.IsIdentity) { _mode = Mode.LeftCopy; _current = _left; @@ -235,9 +114,9 @@ public partial class CombinedTexture : IDisposable _mode = Mode.Custom; } } - else if( _right.IsLoaded ) + else if (_right.IsLoaded) { - if( !_invertRight && _multiplierRight.IsIdentity ) + if (!_invertRight && _multiplierRight.IsIdentity) { _current = _right; _mode = Mode.RightCopy; @@ -254,6 +133,7 @@ public partial class CombinedTexture : IDisposable { _centerStorage.Dispose(); _current = null; + SaveGuid = Guid.Empty; _mode = Mode.Empty; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index f0c3beca..f84442c1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -101,7 +101,7 @@ public static class TexFileParser Height = (ushort)meta.Height, Width = (ushort)meta.Width, Depth = (ushort)Math.Max(meta.Depth, 1), - MipLevels = (byte)Math.Min(meta.MipLevels, 12), + MipLevels = (byte)Math.Min(meta.MipLevels, 13), Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index ef8e16fc..aefe72b4 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,135 +1,61 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Dalamud.Data; -using Dalamud.Interface; -using ImGuiNET; using ImGuiScene; -using Lumina.Data.Files; -using OtterGui; -using OtterGui.Raii; using OtterTex; -using Penumbra.Services; -using Penumbra.String.Classes; -using Penumbra.UI; -using Penumbra.UI.Classes; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; +public enum TextureType +{ + Unknown, + Dds, + Tex, + Png, + Bitmap, +} + public sealed class Texture : IDisposable { - public enum FileType - { - Unknown, - Dds, - Tex, - Png, - Bitmap, - } - // Path to the file we tried to load. public string Path = string.Empty; + // Path for changing paths. + internal string? TmpPath; + // If the load failed, an exception is stored. public Exception? LoadError = null; // The pixels of the main image in RGBA order. // Empty if LoadError != null or Path is empty. - public byte[] RGBAPixels = Array.Empty(); + public byte[] RgbaPixels = Array.Empty(); // The ImGui wrapper to load the image. // null if LoadError != null or Path is empty. public TextureWrap? TextureWrap = null; // The base image in whatever format it has. - public object? BaseImage = null; + public BaseImage BaseImage; // Original File Type. - public FileType Type = FileType.Unknown; + public TextureType Type = TextureType.Unknown; // Whether the file is successfully loaded and drawable. public bool IsLoaded => TextureWrap != null; public DXGIFormat Format - => BaseImage switch - { - ScratchImage s => s.Meta.Format, - TexFile t => t.Header.Format.ToDXGI(), - _ => DXGIFormat.Unknown, - }; + => BaseImage.Format; public int MipMaps - => BaseImage switch - { - ScratchImage s => s.Meta.MipLevels, - TexFile t => t.Header.MipLevels, - _ => 1, - }; - - public void Draw(Vector2 size) - { - if (TextureWrap != null) - { - size = size.X < TextureWrap.Width - ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } - : new Vector2(TextureWrap.Width, TextureWrap.Height); - - ImGui.Image(TextureWrap.ImGuiHandle, size); - DrawData(); - } - else if (LoadError != null) - { - ImGui.TextUnformatted("Could not load file:"); - ImGuiUtil.TextColored(Colors.RegexWarningBorder, LoadError.ToString()); - } - } - - public void DrawData() - { - using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); - ImGuiUtil.DrawTableColumn("Width"); - ImGuiUtil.DrawTableColumn(TextureWrap!.Width.ToString()); - ImGuiUtil.DrawTableColumn("Height"); - ImGuiUtil.DrawTableColumn(TextureWrap!.Height.ToString()); - ImGuiUtil.DrawTableColumn("File Type"); - ImGuiUtil.DrawTableColumn(Type.ToString()); - ImGuiUtil.DrawTableColumn("Bitmap Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(RGBAPixels.Length)} ({RGBAPixels.Length} Bytes)"); - switch (BaseImage) - { - case ScratchImage s: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); - ImGuiUtil.DrawTableColumn("Number of Images"); - ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); - break; - case TexFile t: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); - break; - } - } + => BaseImage.MipMaps; private void Clean() { - RGBAPixels = Array.Empty(); + RgbaPixels = Array.Empty(); TextureWrap?.Dispose(); TextureWrap = null; - (BaseImage as IDisposable)?.Dispose(); - BaseImage = null; - Type = FileType.Unknown; + BaseImage.Dispose(); + BaseImage = new BaseImage(); + Type = TextureType.Unknown; Loaded?.Invoke(false); } @@ -138,9 +64,9 @@ public sealed class Texture : IDisposable public event Action? Loaded; - private void Load(DalamudServices dalamud, string path) + public void Load(TextureManager textures, string path) { - _tmpPath = null; + TmpPath = null; if (path == Path) return; @@ -151,13 +77,9 @@ public sealed class Texture : IDisposable try { - var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch - { - ".dds" => LoadDds(dalamud), - ".png" => LoadPng(dalamud), - ".tex" => LoadTex(dalamud), - _ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."), - }; + (BaseImage, Type) = textures.Load(path); + (RgbaPixels, var width, var height) = BaseImage.GetPixelData(); + TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels); Loaded?.Invoke(true); } catch (Exception e) @@ -167,130 +89,10 @@ public sealed class Texture : IDisposable } } - public void Reload(DalamudServices dalamud) + public void Reload(TextureManager textures) { var path = Path; - Path = string.Empty; - Load(dalamud, path); - } - - private bool LoadDds(DalamudServices dalamud) - { - Type = FileType.Dds; - var scratch = ScratchImage.LoadDDS(Path); - BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); - RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); - CreateTextureWrap(dalamud.UiBuilder, f.Meta.Width, f.Meta.Height); - return true; - } - - private bool LoadPng(DalamudServices dalamud) - { - Type = FileType.Png; - BaseImage = null; - using var stream = File.OpenRead(Path); - using var png = Image.Load(stream); - RGBAPixels = new byte[png.Height * png.Width * 4]; - png.CopyPixelDataTo(RGBAPixels); - CreateTextureWrap(dalamud.UiBuilder, png.Width, png.Height); - return true; - } - - private bool LoadTex(DalamudServices dalamud) - { - Type = FileType.Tex; - using var stream = OpenTexStream(dalamud.GameData); - var scratch = TexFileParser.Parse(stream); - BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); - RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(); - CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height); - return true; - } - - private Stream OpenTexStream(DataManager gameData) - { - if (System.IO.Path.IsPathRooted(Path)) - return File.OpenRead(Path); - - var file = gameData.GetFile(Path); - return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{Path}\" from game files."); - } - - private void CreateTextureWrap(UiBuilder builder, int width, int height) - => TextureWrap = builder.LoadImageRaw(RGBAPixels, width, height, 4); - - private string? _tmpPath; - - public void PathSelectBox(DalamudServices dalamud, string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix) - { - ImGui.SetNextItemWidth(-0.0001f); - var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here..."; - using var combo = ImRaii.Combo(label, startPath); - if (combo) - foreach (var ((path, game), idx) in paths.WithIndex()) - { - if (game) - { - if (!dalamud.GameData.FileExists(path)) - continue; - } - else if (!File.Exists(path)) - { - continue; - } - - using var id = ImRaii.PushId(idx); - using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) - { - var p = game ? $"--> {path}" : path[skipPrefix..]; - if (ImGui.Selectable(p, path == startPath) && path != startPath) - Load(dalamud, path); - } - - ImGuiUtil.HoverTooltip(game - ? "This is a game path and refers to an unmanipulated file from your game data." - : "This is a path to a modded file on your file system."); - } - - ImGuiUtil.HoverTooltip(tooltip); - } - - public void PathInputBox(DalamudServices dalamud, string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, - string defaultModImportPath) - { - _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); - ImGui.InputTextWithHint(label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength); - if (ImGui.IsItemDeactivatedAfterEdit()) - Load(dalamud, _tmpPath); - - ImGuiUtil.HoverTooltip(tooltip); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, - true)) - { - if (defaultModImportPath.Length > 0) - startPath = defaultModImportPath; - - var texture = this; - - void UpdatePath(bool success, List paths) - { - if (success && paths.Count > 0) - texture.Load(dalamud, paths[0]); - } - - fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Reload the currently selected path.", false, - true)) - Reload(dalamud); + Path = string.Empty; + Load(textures, path); } } diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs new file mode 100644 index 00000000..b077f6fd --- /dev/null +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Raii; +using OtterTex; +using Penumbra.String.Classes; +using Penumbra.UI; +using Penumbra.UI.Classes; + +namespace Penumbra.Import.Textures; + +public static class TextureDrawer +{ + public static void Draw(Texture texture, Vector2 size) + { + if (texture.TextureWrap != null) + { + size = size.X < texture.TextureWrap.Width + ? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width } + : new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height); + + ImGui.Image(texture.TextureWrap.ImGuiHandle, size); + DrawData(texture); + } + else if (texture.LoadError != null) + { + ImGui.TextUnformatted("Could not load file:"); + ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString()); + } + } + + public static void PathSelectBox(TextureManager textures, Texture current, string label, string tooltip, IEnumerable<(string, bool)> paths, + int skipPrefix) + { + ImGui.SetNextItemWidth(-0.0001f); + var startPath = current.Path.Length > 0 ? current.Path : "Choose a modded texture from this mod here..."; + using var combo = ImRaii.Combo(label, startPath); + if (combo) + foreach (var ((path, game), idx) in paths.WithIndex()) + { + if (game) + { + if (!textures.GameFileExists(path)) + continue; + } + else if (!File.Exists(path)) + { + continue; + } + + using var id = ImRaii.PushId(idx); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) + { + var p = game ? $"--> {path}" : path[skipPrefix..]; + if (ImGui.Selectable(p, path == startPath) && path != startPath) + current.Load(textures, path); + } + + ImGuiUtil.HoverTooltip(game + ? "This is a game path and refers to an unmanipulated file from your game data." + : "This is a path to a modded file on your file system."); + } + + ImGuiUtil.HoverTooltip(tooltip); + } + + public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip, + string startPath, FileDialogService fileDialog, string defaultModImportPath) + { + tmpPath ??= current.Path; + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); + ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength); + if (ImGui.IsItemDeactivatedAfterEdit()) + current.Load(textures, tmpPath); + + ImGuiUtil.HoverTooltip(tooltip); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, + true)) + { + if (defaultModImportPath.Length > 0) + startPath = defaultModImportPath; + + void UpdatePath(bool success, List paths) + { + if (success && paths.Count > 0) + current.Load(textures, paths[0]); + } + + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Reload the currently selected path.", false, + true)) + current.Reload(textures); + } + + private static void DrawData(Texture texture) + { + using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); + ImGuiUtil.DrawTableColumn("Width"); + ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString()); + ImGuiUtil.DrawTableColumn("Height"); + ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString()); + ImGuiUtil.DrawTableColumn("File Type"); + ImGuiUtil.DrawTableColumn(texture.Type.ToString()); + ImGuiUtil.DrawTableColumn("Bitmap Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); + switch (texture.BaseImage.Image) + { + case ScratchImage s: + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); + ImGuiUtil.DrawTableColumn("Number of Images"); + ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); + break; + case TexFile t: + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); + break; + } + } +} diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs deleted file mode 100644 index 74bef485..00000000 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.IO; -using Lumina.Data.Files; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Penumbra.Import.Textures; - -public static 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.B8G8R8A8 ); - 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 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.CopyTo( texData.AsSpan( 80 ) ); - for( var i = 80; i < texData.Length; i += 4 ) - (texData[ i ], texData[i + 2]) = (texData[ i + 2], texData[i]); - 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; - } -} \ No newline at end of file diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs new file mode 100644 index 00000000..9ac503df --- /dev/null +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -0,0 +1,438 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading; +using Dalamud.Data; +using Dalamud.Interface; +using ImGuiScene; +using Lumina.Data.Files; +using OtterGui.Log; +using OtterGui.Tasks; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public sealed class TextureManager : AsyncTaskManager +{ + private readonly UiBuilder _uiBuilder; + private readonly DataManager _gameData; + + public TextureManager(Logger logger, UiBuilder uiBuilder, DataManager gameData) + : base(logger) + { + _uiBuilder = uiBuilder; + _gameData = gameData; + } + + public Guid SavePng(string input, string output) + => Enqueue(new SavePngAction(this, input, output)); + + public Guid SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); + + public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); + + public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, + int width = 0, int height = 0) + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); + + private readonly struct ImageInputData + { + private readonly string? _inputPath; + + private readonly BaseImage _image; + private readonly byte[]? _rgba; + private readonly int _width; + private readonly int _height; + + public ImageInputData(string inputPath) + { + _inputPath = inputPath; + _image = new BaseImage(); + _rgba = null; + _width = 0; + _height = 0; + } + + public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + { + _inputPath = null; + _image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image; + _rgba = rgba?.ToArray(); + _width = width; + _height = height; + } + + public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures) + { + if (_inputPath == null) + return (_image, _rgba, _width, _height); + + if (!File.Exists(_inputPath)) + throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath); + + var (image, _) = textures.Load(_inputPath); + return (image, null, 0, 0); + } + + public bool Equals(ImageInputData rhs) + { + if (_inputPath != null) + return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase); + + if (rhs._inputPath != null) + return false; + + if (_image.Image != null) + return ReferenceEquals(_image.Image, rhs._image.Image); + + return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba); + } + + public override string ToString() + => _inputPath + ?? _image.Type switch + { + TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image", + TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Png => $"Custom {_width} x {_height} .png Image", + TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", + _ => "Unknown Image", + }; + } + + private class SavePngAction : IAction + { + private readonly TextureManager _textures; + private readonly string _outputPath; + private readonly ImageInputData _input; + + public SavePngAction(TextureManager textures, string input, string output) + { + _textures = textures; + _input = new ImageInputData(input); + _outputPath = output; + } + + public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + { + _textures = textures; + _input = new ImageInputData(image, rgba, width, height); + _outputPath = path; + } + + public void Execute(CancellationToken cancel) + { + _textures.Logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + var (image, rgba, width, height) = _input.GetData(_textures); + cancel.ThrowIfCancellationRequested(); + Image? png = null; + if (image.Type is TextureType.Unknown) + { + if (rgba != null && width > 0 && height > 0) + png = ConvertToPng(rgba, width, height).AsPng!; + } + else + { + png = ConvertToPng(image, cancel, rgba).AsPng!; + } + + cancel.ThrowIfCancellationRequested(); + png?.SaveAsync(_outputPath, cancel).Wait(cancel); + } + + public bool Equals(IAction? other) + { + if (other is not SavePngAction rhs) + return false; + + return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); + } + } + + private class SaveAsAction : IAction + { + private readonly TextureManager _textures; + private readonly string _outputPath; + private readonly ImageInputData _input; + private readonly CombinedTexture.TextureSaveType _type; + private readonly bool _mipMaps; + private readonly bool _asTex; + + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, + string output) + { + _textures = textures; + _input = new ImageInputData(input); + _outputPath = output; + _type = type; + _mipMaps = mipMaps; + _asTex = asTex; + } + + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, + string path, + byte[]? rgba = null, int width = 0, int height = 0) + { + _textures = textures; + _input = new ImageInputData(image, rgba, width, height); + _outputPath = path; + _type = type; + _mipMaps = mipMaps; + _asTex = asTex; + } + + public void Execute(CancellationToken cancel) + { + _textures.Logger.Information( + $"[{nameof(TextureManager)}] Saving {_input} as {_type} {(_asTex ? ".tex" : ".dds")} file{(_mipMaps ? " with mip maps" : string.Empty)} to {_outputPath}..."); + var (image, rgba, width, height) = _input.GetData(_textures); + if (image.Type is TextureType.Unknown) + { + if (rgba != null && width > 0 && height > 0) + image = ConvertToDds(rgba, width, height); + else + return; + } + + var dds = _type switch + { + CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), + CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => + ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + _ => throw new Exception("Wrong save type."), + }; + + cancel.ThrowIfCancellationRequested(); + if (_asTex) + SaveTex(_outputPath, dds.AsDds!); + else + dds.AsDds!.SaveDDS(_outputPath); + } + + public bool Equals(IAction? other) + { + if (other is not SaveAsAction rhs) + return false; + + return _type == rhs._type + && _mipMaps == rhs._mipMaps + && _asTex == rhs._asTex + && string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) + && _input.Equals(rhs._input); + } + } + + /// Load a texture wrap for a given image. + public TextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + { + (rgba, width, height) = GetData(image, rgba, width, height); + return LoadTextureWrap(rgba, width, height); + } + + /// Load a texture wrap for a given image. + public TextureWrap LoadTextureWrap(byte[] rgba, int width, int height) + => _uiBuilder.LoadImageRaw(rgba, width, height, 4); + + /// Load any supported file from game data or drive depending on extension and if the path is rooted. + public (BaseImage, TextureType) Load(string path) + => Path.GetExtension(path).ToLowerInvariant() switch + { + ".dds" => (LoadDds(path), TextureType.Dds), + ".png" => (LoadPng(path), TextureType.Png), + ".tex" => (LoadTex(path), TextureType.Tex), + _ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."), + }; + + /// Load a .tex file from game data or drive depending on if the path is rooted. + public BaseImage LoadTex(string path) + { + using var stream = OpenTexStream(path); + return TexFileParser.Parse(stream); + } + + /// Load a .dds file from drive using OtterTex. + public BaseImage LoadDds(string path) + => ScratchImage.LoadDDS(path); + + /// Load a .png file from drive using ImageSharp. + public BaseImage LoadPng(string path) + { + using var stream = File.OpenRead(path); + return Image.Load(stream); + } + + /// Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. + public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) + { + switch (input.Type) + { + case TextureType.Png: return input; + case TextureType.Dds: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + return ConvertToPng(rgba, width, height); + } + default: return new BaseImage(); + } + } + + /// Convert an existing image to a RGBA32 .dds. Does not create a deep copy of an existing RGBA32 dds and just returns the existing one. + public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0, + int height = 0) + { + switch (input.Type) + { + case TextureType.Png: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(dds, mipMaps); + } + case TextureType.Dds: + { + var scratch = input.AsDds!; + if (rgba == null) + return CreateUncompressed(scratch, mipMaps, cancel); + + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(dds, mipMaps); + } + default: return new BaseImage(); + } + } + + /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. + public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + int width = 0, int height = 0) + { + switch (input.Type) + { + case TextureType.Png: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return CreateCompressed(dds, mipMaps, bc7, cancel); + } + case TextureType.Dds: + { + var scratch = input.AsDds!; + return CreateCompressed(scratch, mipMaps, bc7, cancel); + } + default: return new BaseImage(); + } + } + + public static BaseImage ConvertToPng(byte[] rgba, int width, int height) + => Image.LoadPixelData(rgba, width, height); + + public static BaseImage ConvertToDds(byte[] rgba, int width, int height) + { + var scratch = ScratchImage.FromRGBA(rgba, width, height, out var i).ThrowIfError(i); + return scratch.Convert(DXGIFormat.B8G8R8A8UNorm); + } + + public bool GameFileExists(string path) + => _gameData.FileExists(path); + + /// Add up to 13 mip maps to the input if mip maps is true, otherwise return input. + public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) + { + var numMips = mipMaps ? Math.Min(13, 1 + BitOperations.Log2((uint)Math.Max(input.Meta.Width, input.Meta.Height))) : 1; + if (numMips == input.Meta.MipLevels) + return input; + + var ec = input.GenerateMipMaps(out var ret, numMips, + (Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha); + if (ec != ErrorCode.Ok) + throw new Exception($"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}"); + + return ret; + } + + /// Create an uncompressed .dds (optionally with mip maps) from the input. Returns input (+ mipmaps) if it is already uncompressed. + public static ScratchImage CreateUncompressed(ScratchImage input, bool mipMaps, CancellationToken cancel) + { + if (input.Meta.Format == DXGIFormat.B8G8R8A8UNorm) + return AddMipMaps(input, mipMaps); + + input = input.Meta.Format.IsCompressed() + ? input.Decompress(DXGIFormat.B8G8R8A8UNorm) + : input.Convert(DXGIFormat.B8G8R8A8UNorm); + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(input, mipMaps); + } + + /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. + public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + { + var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; + if (input.Meta.Format == format) + return input; + + if (input.Meta.Format.IsCompressed()) + { + input = input.Decompress(DXGIFormat.B8G8R8A8UNorm); + cancel.ThrowIfCancellationRequested(); + } + + input = AddMipMaps(input, mipMaps); + cancel.ThrowIfCancellationRequested(); + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); + } + + + /// Load a tex file either from game data if the path is not rooted, or from drive if it is rooted. + private Stream OpenTexStream(string path) + { + if (Path.IsPathRooted(path)) + return File.OpenRead(path); + + var file = _gameData.GetFile(path); + return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files."); + } + + /// Obtain the checked rgba data, width and height for an image. + private static (byte[], int, int) GetData(BaseImage input, byte[]? rgba, int width, int height) + { + if (rgba == null) + return input.GetPixelData(); + + if (width == 0 || height == 0) + (width, height) = input.Dimensions; + return width * height * 4 != rgba.Length + ? input.GetPixelData() + : (rgba, width, height); + } + + /// Save a .dds file as .tex file with appropriately changed header. + public static void SaveTex(string path, ScratchImage input) + { + var header = input.ToTexHeader(); + if (header.Format == TexFile.TextureFormat.Unknown) + throw new Exception($"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex format."); + + using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); + using var w = new BinaryWriter(stream); + header.Write(w); + w.Write(input.Pixels); + } +} diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index cbb98a38..eb15de87 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Threading.Tasks; -using OtterGui; +using OtterGui.Tasks; using Penumbra.Mods.Manager; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 1726eab2..62d87815 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Tasks; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fbbaab51..b291e392 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,7 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 782d40a0..f0864b97 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -7,6 +7,7 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceTree; @@ -173,7 +174,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 783acc49..ca1e3624 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using OtterGui; +using OtterGui.Tasks; using Penumbra.Util; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index f96ab4da..6fd7b130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -5,6 +5,7 @@ using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Tasks; using OtterTex; using Penumbra.Import.Textures; @@ -12,13 +13,14 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly TextureManager _textures; + private readonly Texture _left = new(); private readonly Texture _right = new(); private readonly CombinedTexture _center; private bool _overlayCollapsed = true; - - private bool _addMipMaps = true; + private bool _addMipMaps = true; private int _currentSaveAs; private static readonly (string, string)[] SaveAsStrings = @@ -42,13 +44,13 @@ public partial class ModEditWindow ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - tex.PathInputBox(_dalamud, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, - _fileDialog, _config.DefaultModImportPath); + TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", + "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); - tex.PathSelectBox(_dalamud, "##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", - files, _mod.ModPath.FullName.Length + 1); + TextureDrawer.PathSelectBox(_textures, tex, "##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, + _mod.ModPath.FullName.Length + 1); if (tex == _left) _center.DrawMatrixInputLeft(size.X); @@ -58,7 +60,7 @@ public partial class ModEditWindow ImGui.NewLine(); using var child2 = ImRaii.Child("image"); if (child2) - tex.Draw(imageSize); + TextureDrawer.Draw(tex, imageSize); } private void SaveAsCombo() @@ -105,7 +107,7 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => { if (a) - _center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -116,7 +118,7 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => { if (a) - _center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -129,20 +131,20 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => { if (a) - _center.SaveAsPng(b); + _center.SaveAsPng(_textures, b); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } - if (_left.Type is Texture.FileType.Tex && _center.IsLeftCopy) + if (_left.Type is TextureType.Tex && _center.IsLeftCopy) { var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, "This converts the texture to BC7 format in place. This is not revertible.", _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } ImGui.SameLine(); @@ -150,8 +152,8 @@ public partial class ModEditWindow "This converts the texture to BC3 format in place. This is not revertible.", _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } ImGui.SameLine(); @@ -159,27 +161,55 @@ public partial class ModEditWindow "This converts the texture to RGBA format in place. This is not revertible.", _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } } else { ImGui.NewLine(); } + ImGui.NewLine(); } - if (_center.SaveException != null) + if (_center.SaveGuid != Guid.Empty) { - ImGui.TextUnformatted("Could not save file:"); - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); - ImGuiUtil.TextWrapped(_center.SaveException.ToString()); + var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _); + if (saveException != null) + { + ImGui.TextUnformatted("Could not save file:"); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); + ImGuiUtil.TextWrapped(saveException.ToString()); + } + else if (state == ActionState.Running) + { + ImGui.TextUnformatted("Computing..."); + } } using var child2 = ImRaii.Child("image"); if (child2) - _center.Draw(_dalamud.UiBuilder, imageSize); + _center.Draw(_textures, imageSize); + } + + + private void ReloadConvertedSubscribe(string path, Guid guid) + { + void Reload(Guid eventGuid, ActionState state, Exception? ex) + { + if (guid != eventGuid) + return; + + if (_left.Path != path) + return; + + if (state is ActionState.Succeeded) + _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + _textures.Finished -= Reload; + } + + _textures.Finished += Reload; } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index b6051136..93d28b85 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -521,7 +521,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator) + CommunicatorService communicator, TextureManager textures) : base(WindowBaseLabel) { _performance = performance; @@ -534,6 +534,7 @@ public partial class ModEditWindow : Window, IDisposable _dalamud = dalamud; _modMergeTab = modMergeTab; _communicator = communicator; + _textures = textures; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, From af93c2aca9bfa7fa2679231c3a690b83ff09b2b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Aug 2023 14:32:02 +0200 Subject: [PATCH 1083/2451] Revert CS change. --- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/DebugTab.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9eb60aa0..e5b733c6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9eb60aa0fdaad4a10af2edd77b154a20c7647ce4 +Subproject commit e5b733c6fcc5436c8f767dd99a37e5e8c16b4fc8 diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index ee12e8a2..066aec6c 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -500,13 +500,11 @@ public class DebugTab : Window, ITab if (agent->Data != null) { - // TODO fix when updated in CS. - var characterData = (AgentBannerInterface.Storage.CharacterData*)((byte*)agent->Data + 0x20); using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) for (var i = 0; i < 8; ++i) { - ref var c = ref *(characterData + i); + ref var c = ref agent->Data->CharacterArraySpan[i]; ImGuiUtil.DrawTableColumn($"Character {i}"); var name = c.Name1.ToString(); ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})"); From 6e11b36401c7cc248fc627d044d42dc78515971c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Aug 2023 16:55:43 +0200 Subject: [PATCH 1084/2451] Add Texture Conversion IPC and use texture tasks. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 76 ++++++- Penumbra/Api/PenumbraApi.cs | 44 +++- Penumbra/Api/PenumbraIpcProviders.cs | 13 ++ Penumbra/Import/Textures/CombinedTexture.cs | 9 +- Penumbra/Import/Textures/TextureManager.cs | 196 +++++++++++------- .../AdvancedWindow/ModEditWindow.Textures.cs | 40 ++-- Penumbra/UI/Tabs/DebugTab.cs | 32 ++- 9 files changed, 305 insertions(+), 109 deletions(-) diff --git a/OtterGui b/OtterGui index 8d61845c..9dad9558 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4 +Subproject commit 9dad955808831a5d154d778d1123acbe648c42ac diff --git a/Penumbra.Api b/Penumbra.Api index 983c98f7..623e802b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 983c98f74e7cd052b21f6ca35ef0ceaa9b388964 +Subproject commit 623e802bbc18496aab4030b444154a5b015093c2 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index d1124847..fea91b0e 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -19,7 +21,6 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; -using Penumbra.Util; namespace Penumbra.Api; @@ -38,6 +39,7 @@ public class IpcTester : IDisposable private readonly Meta _meta; private readonly Mods _mods; private readonly ModSettings _modSettings; + private readonly Editing _editing; private readonly Temporary _temporary; public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, @@ -54,6 +56,7 @@ public class IpcTester : IDisposable _meta = new Meta(dalamud.PluginInterface); _mods = new Mods(dalamud.PluginInterface); _modSettings = new ModSettings(dalamud.PluginInterface); + _editing = new Editing(dalamud.PluginInterface); _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); UnsubscribeEvents(); } @@ -74,6 +77,7 @@ public class IpcTester : IDisposable _meta.Draw(); _mods.Draw(); _modSettings.Draw(); + _editing.Draw(); _temporary.Draw(); _temporary.DrawCollections(); _temporary.DrawMods(); @@ -402,9 +406,9 @@ public class IpcTester : IDisposable private string _lastRedrawnString = "None"; public Redrawing(DalamudServices dalamud) - { + { _dalamud = dalamud; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); + Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); } public void Draw() @@ -1149,6 +1153,72 @@ public class IpcTester : IDisposable } } + private class Editing + { + private readonly DalamudPluginInterface _pi; + + private string _inputPath = string.Empty; + private string _inputPath2 = string.Empty; + private string _outputPath = string.Empty; + private string _outputPath2 = string.Empty; + + private TextureType _typeSelector; + private bool _mipMaps = true; + + private Task? _task1; + private Task? _task2; + + public Editing(DalamudPluginInterface pi) + => _pi = pi; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Editing"); + if (!_) + return; + + ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); + ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); + ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); + ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); + TypeCombo(); + ImGui.Checkbox("Add MipMaps", ref _mipMaps); + + using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 1"); + if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) + _task1 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); + if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task1.Exception?.ToString()); + + DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 2"); + if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) + _task2 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); + if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task2.Exception?.ToString()); + } + + private void TypeCombo() + { + using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); + if (!combo) + return; + + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), _typeSelector == value)) + _typeSelector = value; + } + } + } + private class Temporary { private readonly DalamudPluginInterface _pi; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9b7e6410..140a928b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,11 +8,13 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; @@ -23,15 +25,17 @@ using Penumbra.String.Classes; using Penumbra.Services; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.UI; +using TextureType = Penumbra.Api.Enums.TextureType; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 20); + => (4, 21); public event Action? PreSettingsPanelDraw { @@ -124,12 +128,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi private RedrawService _redrawService; private ModFileSystem _modFileSystem; private ConfigWindow _configWindow; + private TextureManager _textureManager; public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, - ConfigWindow configWindow) + ConfigWindow configWindow, TextureManager textureManager) { _communicator = communicator; _modManager = modManager; @@ -147,6 +152,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _redrawService = redrawService; _modFileSystem = modFileSystem; _configWindow = configWindow; + _textureManager = textureManager; _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; @@ -179,6 +185,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _redrawService = null!; _modFileSystem = null!; _configWindow = null!; + _textureManager = null!; } public event ChangedItemClick? ChangedItemClicked @@ -992,6 +999,39 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } + public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => _textureManager.SavePng(inputFile, outputFile), + TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), + TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), + TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), + TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), + TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), + TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), + TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), + TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + + // @formatter:off + public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => _textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + // @formatter:on + + // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) => GetMetaManipulations(characterName, ushort.MaxValue); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 7ccd7e20..73f87b94 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using Penumbra.GameData.Enums; using System; using System.Collections.Generic; +using System.Threading.Tasks; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Collections.Manager; @@ -105,6 +106,10 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider ModSettingChanged; internal readonly FuncProvider CopyModSettings; + // Editing + internal readonly FuncProvider ConvertTextureFile; + internal readonly FuncProvider ConvertTextureData; + // Temporary internal readonly FuncProvider CreateTemporaryCollection; internal readonly FuncProvider RemoveTemporaryCollection; @@ -219,6 +224,10 @@ public class PenumbraIpcProviders : IDisposable () => Api.ModSettingChanged -= ModSettingChangedEvent); CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings); + // Editing + ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile); + ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData); + // Temporary CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection); RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection); @@ -335,6 +344,10 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll.Dispose(); RemoveTemporaryMod.Dispose(); + // Editing + ConvertTextureFile.Dispose(); + ConvertTextureData.Dispose(); + Disposed.Invoke(); Disposed.Dispose(); } diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 99303234..c26cb900 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Threading.Tasks; namespace Penumbra.Import.Textures; @@ -29,7 +30,7 @@ public partial class CombinedTexture : IDisposable private readonly Texture _centerStorage = new(); - public Guid SaveGuid { get; private set; } = Guid.Empty; + public Task SaveTask { get; private set; } = Task.CompletedTask; public bool IsLoaded => _mode != Mode.Empty; @@ -55,7 +56,7 @@ public partial class CombinedTexture : IDisposable if (!IsLoaded || _current == null) return; - SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); + SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) @@ -63,7 +64,7 @@ public partial class CombinedTexture : IDisposable if (!IsLoaded || _current == null) return; - SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, + SaveTask = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } @@ -133,7 +134,7 @@ public partial class CombinedTexture : IDisposable { _centerStorage.Dispose(); _current = null; - SaveGuid = Guid.Empty; + SaveTask = Task.CompletedTask; _mode = Mode.Empty; } } diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 9ac503df..76604e84 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Threading; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Interface; using ImGuiScene; @@ -11,100 +14,71 @@ using OtterGui.Log; using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; +using Swan; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager : AsyncTaskManager +public sealed class TextureManager : SingleTaskQueue, IDisposable { + private readonly Logger _logger; private readonly UiBuilder _uiBuilder; private readonly DataManager _gameData; - public TextureManager(Logger logger, UiBuilder uiBuilder, DataManager gameData) - : base(logger) + private readonly ConcurrentDictionary _tasks = new(); + private bool _disposed = false; + + public TextureManager(UiBuilder uiBuilder, DataManager gameData, Logger logger) { _uiBuilder = uiBuilder; _gameData = gameData; + _logger = logger; } - public Guid SavePng(string input, string output) + public IReadOnlyDictionary Tasks + => _tasks; + + public void Dispose() + { + _disposed = true; + foreach (var (_, cancel) in _tasks.Values.ToArray()) + cancel.Cancel(); + _tasks.Clear(); + } + + public Task SavePng(string input, string output) => Enqueue(new SavePngAction(this, input, output)); - public Guid SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); - public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); - public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); - private readonly struct ImageInputData + private Task Enqueue(IAction action) { - private readonly string? _inputPath; + if (_disposed) + return Task.FromException(new ObjectDisposedException(nameof(TextureManager))); - private readonly BaseImage _image; - private readonly byte[]? _rgba; - private readonly int _width; - private readonly int _height; - - public ImageInputData(string inputPath) + Task t; + lock (_tasks) { - _inputPath = inputPath; - _image = new BaseImage(); - _rgba = null; - _width = 0; - _height = 0; + t = _tasks.GetOrAdd(action, a => + { + var token = new CancellationTokenSource(); + var task = Enqueue(a, token.Token); + task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None); + return (task, token); + }).Item1; } - public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) - { - _inputPath = null; - _image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image; - _rgba = rgba?.ToArray(); - _width = width; - _height = height; - } - - public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures) - { - if (_inputPath == null) - return (_image, _rgba, _width, _height); - - if (!File.Exists(_inputPath)) - throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath); - - var (image, _) = textures.Load(_inputPath); - return (image, null, 0, 0); - } - - public bool Equals(ImageInputData rhs) - { - if (_inputPath != null) - return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase); - - if (rhs._inputPath != null) - return false; - - if (_image.Image != null) - return ReferenceEquals(_image.Image, rhs._image.Image); - - return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba); - } - - public override string ToString() - => _inputPath - ?? _image.Type switch - { - TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image", - TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", - TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", - TextureType.Png => $"Custom {_width} x {_height} .png Image", - TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", - _ => "Unknown Image", - }; + return t; } private class SavePngAction : IAction @@ -129,7 +103,7 @@ public sealed class TextureManager : AsyncTaskManager public void Execute(CancellationToken cancel) { - _textures.Logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); cancel.ThrowIfCancellationRequested(); Image? png = null; @@ -144,9 +118,12 @@ public sealed class TextureManager : AsyncTaskManager } cancel.ThrowIfCancellationRequested(); - png?.SaveAsync(_outputPath, cancel).Wait(cancel); + png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel); } + public override string ToString() + => $"{_input} to {_outputPath} PNG"; + public bool Equals(IAction? other) { if (other is not SavePngAction rhs) @@ -154,6 +131,9 @@ public sealed class TextureManager : AsyncTaskManager return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); } + + public override int GetHashCode() + => HashCode.Combine(_outputPath.ToLowerInvariant(), _input); } private class SaveAsAction : IAction @@ -177,8 +157,7 @@ public sealed class TextureManager : AsyncTaskManager } public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, - string path, - byte[]? rgba = null, int width = 0, int height = 0) + string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); @@ -190,7 +169,7 @@ public sealed class TextureManager : AsyncTaskManager public void Execute(CancellationToken cancel) { - _textures.Logger.Information( + _textures._logger.Information( $"[{nameof(TextureManager)}] Saving {_input} as {_type} {(_asTex ? ".tex" : ".dds")} file{(_mipMaps ? " with mip maps" : string.Empty)} to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); if (image.Type is TextureType.Unknown) @@ -220,6 +199,9 @@ public sealed class TextureManager : AsyncTaskManager dds.AsDds!.SaveDDS(_outputPath); } + public override string ToString() + => $"{_input} to {_outputPath} {_type} {(_asTex ? "TEX" : "DDS")}{(_mipMaps ? " with MipMaps" : string.Empty)}"; + public bool Equals(IAction? other) { if (other is not SaveAsAction rhs) @@ -231,6 +213,9 @@ public sealed class TextureManager : AsyncTaskManager && string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); } + + public override int GetHashCode() + => HashCode.Combine(_outputPath.ToLowerInvariant(), _type, _mipMaps, _asTex, _input); } /// Load a texture wrap for a given image. @@ -435,4 +420,73 @@ public sealed class TextureManager : AsyncTaskManager header.Write(w); w.Write(input.Pixels); } + + private readonly struct ImageInputData + { + private readonly string? _inputPath; + + private readonly BaseImage _image; + private readonly byte[]? _rgba; + private readonly int _width; + private readonly int _height; + + public ImageInputData(string inputPath) + { + _inputPath = inputPath; + _image = new BaseImage(); + _rgba = null; + _width = 0; + _height = 0; + } + + public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + { + _inputPath = null; + _image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image; + _rgba = rgba?.ToArray(); + _width = width; + _height = height; + } + + public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures) + { + if (_inputPath == null) + return (_image, _rgba, _width, _height); + + if (!File.Exists(_inputPath)) + throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath); + + var (image, _) = textures.Load(_inputPath); + return (image, null, 0, 0); + } + + public bool Equals(ImageInputData rhs) + { + if (_inputPath != null) + return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase); + + if (rhs._inputPath != null) + return false; + + if (_image.Image != null) + return ReferenceEquals(_image.Image, rhs._image.Image); + + return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba); + } + + public override string ToString() + => _inputPath + ?? _image.Type switch + { + TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image", + TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Png => $"Custom {_width} x {_height} .png Image", + TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", + _ => "Unknown Image", + }; + + public override int GetHashCode() + => _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height); + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 6fd7b130..07607d11 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -144,7 +145,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } ImGui.SameLine(); @@ -153,7 +154,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } ImGui.SameLine(); @@ -162,7 +163,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } } else @@ -173,18 +174,20 @@ public partial class ModEditWindow ImGui.NewLine(); } - if (_center.SaveGuid != Guid.Empty) + switch (_center.SaveTask.Status) { - var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _); - if (saveException != null) + case TaskStatus.WaitingForActivation: + case TaskStatus.WaitingToRun: + case TaskStatus.Running: + ImGui.TextUnformatted("Computing..."); + break; + case TaskStatus.Canceled: + case TaskStatus.Faulted: { ImGui.TextUnformatted("Could not save file:"); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); - ImGuiUtil.TextWrapped(saveException.ToString()); - } - else if (state == ActionState.Running) - { - ImGui.TextUnformatted("Computing..."); + ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); + break; } } @@ -193,23 +196,18 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } - - private void ReloadConvertedSubscribe(string path, Guid guid) + private void AddReloadTask(string path) { - void Reload(Guid eventGuid, ActionState state, Exception? ex) + _center.SaveTask.ContinueWith(t => { - if (guid != eventGuid) + if (!t.IsCompletedSuccessfully) return; if (_left.Path != path) return; - if (state is ActionState.Succeeded) - _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); - _textures.Finished -= Reload; - } - - _textures.Finished += Reload; + _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + }); } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 066aec6c..a48fd714 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -19,6 +19,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Import.Structs; +using Penumbra.Import.Textures; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; @@ -61,13 +62,15 @@ public class DebugTab : Window, ITab private readonly ModImportManager _modImporter; private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; + private readonly TextureManager _textureManager; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, - CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework) + CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, + TextureManager textureManager) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -99,6 +102,7 @@ public class DebugTab : Window, ITab _modImporter = modImporter; _importPopup = importPopup; _framework = framework; + _textureManager = textureManager; } public ReadOnlySpan Label @@ -147,14 +151,15 @@ public class DebugTab : Window, ITab private void DrawCollectionCaches() { - if (!ImGui.CollapsingHeader($"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) + if (!ImGui.CollapsingHeader( + $"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) return; foreach (var collection in _collectionManager.Storage) { if (collection.HasCache) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); + using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})"); if (!node) continue; @@ -177,8 +182,9 @@ public class DebugTab : Window, ITab } else { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); + TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } } @@ -293,6 +299,20 @@ public class DebugTab : Window, ITab } } } + + using (var tree = TreeNode($"Texture Manager {_textureManager.Tasks.Count}###Texture Manager")) + { + if (tree) + { + using var table = Table("##Tasks", 2, ImGuiTableFlags.RowBg); + if (table) + foreach (var task in _textureManager.Tasks) + { + ImGuiUtil.DrawTableColumn(task.Key.ToString()!); + ImGuiUtil.DrawTableColumn(task.Value.Item1.Status.ToString()); + } + } + } } private void DrawPerformanceTab() @@ -500,7 +520,7 @@ public class DebugTab : Window, ITab if (agent->Data != null) { - using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) for (var i = 0; i < 8; ++i) { From e615ffcf3def127f458c5633b8ff9a0f5c599362 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Aug 2023 17:01:55 +0200 Subject: [PATCH 1085/2451] Increment API --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 623e802b..97610e1c 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 623e802bbc18496aab4030b444154a5b015093c2 +Subproject commit 97610e1c9d27d863ae8563bb9c8525cc6f8a3709 From df808187e25bcea93f913c9fb295c0a8f77b1b55 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 10 Aug 2023 15:07:04 +0000 Subject: [PATCH 1086/2451] [CI] Updating repo.json for testing_0.7.2.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index fb7afb7e..760f1ad1 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.6", + "TestingAssemblyVersion": "0.7.2.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 09ca32f33dd084bbe9e8e8c702efe5e89c6776f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Aug 2023 17:38:35 +0200 Subject: [PATCH 1087/2451] Add DalamudSubstitutionProvider --- Penumbra/Api/DalamudSubstitutionProvider.cs | 46 +++++++++++++++++++++ Penumbra/Services/DalamudServices.cs | 19 +++++---- 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Penumbra/Api/DalamudSubstitutionProvider.cs diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs new file mode 100644 index 00000000..faa6710a --- /dev/null +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -0,0 +1,46 @@ +using System; +using Dalamud.Plugin.Services; +using Penumbra.Collections.Manager; +using Penumbra.String.Classes; + +namespace Penumbra.Api; + +public class DalamudSubstitutionProvider : IDisposable +{ + private readonly ITextureSubstitutionProvider _substitution; + private readonly ActiveCollectionData _activeCollectionData; + + public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData) + { + _substitution = substitution; + _activeCollectionData = activeCollectionData; + _substitution.InterceptTexDataLoad += Substitute; + } + + public void Dispose() + => _substitution.InterceptTexDataLoad -= Substitute; + + private void Substitute(string path, ref string? replacementPath) + { + // Let other plugins prioritize replacement paths. + if (replacementPath != null) + return; + + // Only replace interface textures. + if (!path.StartsWith("ui/") && !path.StartsWith("common/font/")) + return; + + try + { + if (!Utf8GamePath.FromString(path, out var utf8Path, true)) + return; + + var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path); + replacementPath = resolved?.FullName; + } + catch + { + // ignored + } + } +} \ No newline at end of file diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index c2cf0c75..491e2161 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -13,6 +13,7 @@ using Dalamud.Plugin; using System.Linq; using System.Reflection; using Dalamud.Interface.DragDrop; +using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local @@ -78,24 +79,28 @@ public class DalamudServices services.AddSingleton(this); services.AddSingleton(UiBuilder); services.AddSingleton(DragDropManager); + services.AddSingleton(TextureProvider); + services.AddSingleton(TextureSubstitutionProvider); } // TODO remove static // @formatter:off - [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ITextureProvider TextureProvider { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ITextureSubstitutionProvider TextureSubstitutionProvider { get; private set; } = null!; // @formatter:on public UiBuilder UiBuilder From af536b34236f73d62c4894b6f0d2bc7df1665f3f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Aug 2023 18:10:26 +0200 Subject: [PATCH 1088/2451] Update some Dalamud Services. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/CommandHandler.cs | 6 +++--- Penumbra/Import/Textures/TextureManager.cs | 11 +++++------ .../PathResolving/AnimationHookService.cs | 7 +++---- .../PathResolving/CollectionResolver.cs | 9 ++++----- .../Interop/PathResolving/CutsceneService.cs | 6 +++--- .../Interop/PathResolving/DrawObjectState.cs | 6 +++--- .../PathResolving/IdentifiedCollectionCache.cs | 6 +++--- .../ResourceTree/ResourceTreeFactory.cs | 9 ++++----- .../Interop/ResourceTree/TreeBuildCache.cs | 9 ++++----- Penumbra/Interop/Services/RedrawService.cs | 8 ++++---- Penumbra/Meta/MetaFileManager.cs | 6 +++--- Penumbra/Services/ChatService.cs | 3 +-- Penumbra/Services/DalamudServices.cs | 17 +++++++---------- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/Services/Wrappers.cs | 13 +++++-------- Penumbra/UI/AdvancedWindow/FileEditor.cs | 6 +++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 ++-- Penumbra/UI/ChangedItemDrawer.cs | 18 +++++++++--------- Penumbra/UI/CollectionTab/CollectionPanel.cs | 4 ++-- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/ModsTab.cs | 6 +++--- Penumbra/UI/Tabs/ResourceTab.cs | 5 ++--- 24 files changed, 78 insertions(+), 91 deletions(-) diff --git a/OtterGui b/OtterGui index 9dad9558..863d08bd 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9dad955808831a5d154d778d1123acbe648c42ac +Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f diff --git a/Penumbra.GameData b/Penumbra.GameData index e5b733c6..d84508ea 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e5b733c6fcc5436c8f767dd99a37e5e8c16b4fc8 +Subproject commit d84508ea1a607976525265e8f75a329667eec0e5 diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4ef04e77..a78c4da7 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -5,6 +5,7 @@ using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -16,7 +17,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; -using Penumbra.Util; namespace Penumbra; @@ -24,7 +24,7 @@ public class CommandHandler : IDisposable { private const string CommandName = "/penumbra"; - private readonly CommandManager _commandManager; + private readonly ICommandManager _commandManager; private readonly RedrawService _redrawService; private readonly ChatGui _chat; private readonly Configuration _config; @@ -35,7 +35,7 @@ public class CommandHandler : IDisposable private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; - public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, + public CommandHandler(Framework framework, ICommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra, CollectionEditor collectionEditor) { diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 76604e84..3b4c7f67 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -6,8 +6,8 @@ using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; -using Dalamud.Data; using Dalamud.Interface; +using Dalamud.Plugin.Services; using ImGuiScene; using Lumina.Data.Files; using OtterGui.Log; @@ -16,21 +16,20 @@ using OtterTex; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; -using Swan; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; public sealed class TextureManager : SingleTaskQueue, IDisposable { - private readonly Logger _logger; - private readonly UiBuilder _uiBuilder; - private readonly DataManager _gameData; + private readonly Logger _logger; + private readonly UiBuilder _uiBuilder; + private readonly IDataManager _gameData; private readonly ConcurrentDictionary _tasks = new(); private bool _disposed = false; - public TextureManager(UiBuilder uiBuilder, DataManager gameData, Logger logger) + public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) { _uiBuilder = uiBuilder; _gameData = gameData; diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 130c0926..b1efb1b9 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -1,10 +1,9 @@ using System; using System.Threading; using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects; using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.GameData; @@ -20,7 +19,7 @@ namespace Penumbra.Interop.PathResolving; public unsafe class AnimationHookService : IDisposable { private readonly PerformanceTracker _performance; - private readonly ObjectTable _objects; + private readonly IObjectTable _objects; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly CollectionResolver _resolver; @@ -29,7 +28,7 @@ public unsafe class AnimationHookService : IDisposable private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); - public AnimationHookService(PerformanceTracker performance, ObjectTable objects, CollectionResolver collectionResolver, + public AnimationHookService(PerformanceTracker performance, IObjectTable objects, CollectionResolver collectionResolver, DrawObjectState drawObjectState, CollectionResolver resolver, Condition conditions) { _performance = performance; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 67d9e96d..a795b46b 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,6 +1,5 @@ using System; -using Dalamud.Game.ClientState; -using Dalamud.Game.Gui; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -21,8 +20,8 @@ public unsafe class CollectionResolver private readonly IdentifiedCollectionCache _cache; private readonly HumanModelList _humanModels; - private readonly ClientState _clientState; - private readonly GameGui _gameGui; + private readonly IClientState _clientState; + private readonly IGameGui _gameGui; private readonly ActorService _actors; private readonly CutsceneService _cutscenes; @@ -31,7 +30,7 @@ public unsafe class CollectionResolver private readonly TempCollectionManager _tempCollections; private readonly DrawObjectState _drawObjectState; - public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, + public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, IGameGui gameGui, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels) { diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 2c59a086..4273ae01 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Dalamud.Game.ClientState.Objects; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; @@ -16,7 +16,7 @@ public class CutsceneService : IDisposable public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; private readonly GameEventManager _events; - private readonly ObjectTable _objects; + private readonly IObjectTable _objects; private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors @@ -24,7 +24,7 @@ public class CutsceneService : IDisposable .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public unsafe CutsceneService(ObjectTable objects, GameEventManager events) + public unsafe CutsceneService(IObjectTable objects, GameEventManager events) { _objects = objects; _events = events; diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 88e600b8..12f867b4 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -4,7 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Threading; -using Dalamud.Game.ClientState.Objects; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData; using Penumbra.Interop.Services; @@ -14,7 +14,7 @@ namespace Penumbra.Interop.PathResolving; public class DrawObjectState : IDisposable, IReadOnlyDictionary { - private readonly ObjectTable _objects; + private readonly IObjectTable _objects; private readonly GameEventManager _gameEvents; private readonly Dictionary _drawObjectToGameObject = new(); @@ -24,7 +24,7 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; - public DrawObjectState(ObjectTable objects, GameEventManager gameEvents) + public DrawObjectState(IObjectTable objects, GameEventManager gameEvents) { SignatureHelper.Initialise(this); _enableDrawHook.Enable(); diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 67ab584e..2bf165c1 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using Dalamud.Game.ClientState; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; @@ -17,11 +17,11 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A { private readonly CommunicatorService _communicator; private readonly GameEventManager _events; - private readonly ClientState _clientState; + private readonly IClientState _clientState; private readonly Dictionary _cache = new(317); private bool _dirty; - public IdentifiedCollectionCache(ClientState clientState, CommunicatorService communicator, GameEventManager events) + public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, GameEventManager events) { _clientState = clientState; _communicator = communicator; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 965769a7..98c1b305 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Dalamud.Data; -using Dalamud.Game.ClientState.Objects; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData.Actors; @@ -12,14 +11,14 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTreeFactory { - private readonly DataManager _gameData; - private readonly ObjectTable _objects; + private readonly IDataManager _gameData; + private readonly IObjectTable _objects; private readonly CollectionResolver _collectionResolver; private readonly IdentifierService _identifier; private readonly Configuration _config; private readonly ActorService _actors; - public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier, + public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier, Configuration config, ActorService actors) { _gameData = gameData; diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 4e432dd4..e9939496 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -2,9 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Data; -using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -12,13 +11,13 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { - private readonly DataManager _dataManager; + private readonly IDataManager _dataManager; private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); public readonly List Characters; public readonly Dictionary CharactersById; - public TreeBuildCache(ObjectTable objects, DataManager dataManager) + public TreeBuildCache(IObjectTable objects, IDataManager dataManager) { _dataManager = dataManager; Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast().ToList(); @@ -36,7 +35,7 @@ internal class TreeBuildCache public ShpkFile? ReadShaderPackage(FullPath path) => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); - private static T? ReadFile(DataManager dataManager, FullPath path, Dictionary cache, Func parseFile) + private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func parseFile) where T : class { if (path.FullName.Length == 0) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 825eda5c..38b7de1c 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -7,12 +7,12 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; -using Penumbra.Services; namespace Penumbra.Interop.Services; @@ -104,8 +104,8 @@ public unsafe partial class RedrawService public sealed unsafe partial class RedrawService : IDisposable { private readonly Framework _framework; - private readonly ObjectTable _objects; - private readonly TargetManager _targets; + private readonly IObjectTable _objects; + private readonly ITargetManager _targets; private readonly Condition _conditions; private readonly List _queue = new(100); @@ -114,7 +114,7 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(Framework framework, ObjectTable objects, TargetManager targets, Condition conditions) + public RedrawService(Framework framework, IObjectTable objects, ITargetManager targets, Condition conditions) { _framework = framework; _objects = objects; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 1cb3319e..7287204c 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Runtime.CompilerServices; -using Dalamud.Data; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; using Penumbra.Collections; @@ -22,12 +22,12 @@ public unsafe class MetaFileManager internal readonly Configuration Config; internal readonly CharacterUtility CharacterUtility; internal readonly ResidentResourceManager ResidentResources; - internal readonly DataManager GameData; + internal readonly IDataManager GameData; internal readonly ActiveCollectionData ActiveCollections; internal readonly ValidityChecker ValidityChecker; internal readonly IdentifierService Identifier; - public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData, + public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) { CharacterUtility = characterUtility; diff --git a/Penumbra/Services/ChatService.cs b/Penumbra/Services/ChatService.cs index f4d4ead0..9ea52cac 100644 --- a/Penumbra/Services/ChatService.cs +++ b/Penumbra/Services/ChatService.cs @@ -3,7 +3,6 @@ using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface; using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; @@ -14,7 +13,7 @@ public class ChatService : OtterGui.Classes.ChatService { private readonly ChatGui _chat; - public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat, UiBuilder uiBuilder) + public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat) : base(log, pi) => _chat = chat; diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 491e2161..03978566 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -1,11 +1,8 @@ using System; -using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; @@ -86,18 +83,18 @@ public class DalamudServices // TODO remove static // @formatter:off [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ICommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IDataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IClientState ClientState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ITargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IGameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ISigScanner SigScanner { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ITextureProvider TextureProvider { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ITextureSubstitutionProvider TextureSubstitutionProvider { get; private set; } = null!; diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index d795062c..56893d70 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Dalamud.Data; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using OtterGui.Widgets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; @@ -24,7 +24,7 @@ public class StainService : IDisposable public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, DataManager dataManager) + public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, IDataManager dataManager) { using var t = timer.Measure(StartTimeType.Stains); StainData = new StainData(pluginInterface, dataManager, dataManager.Language); diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index c0ffec79..d9bef172 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -1,9 +1,6 @@ -using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Gui; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; @@ -14,22 +11,22 @@ namespace Penumbra.Services; public sealed class IdentifierService : AsyncServiceWrapper { - public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data, ItemService items) + public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, IDataManager data, ItemService items) : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService)) { } } public sealed class ItemService : AsyncServiceWrapper { - public ItemService(StartTracker tracker, DalamudPluginInterface pi, DataManager gameData) + public ItemService(StartTracker tracker, DalamudPluginInterface pi, IDataManager gameData) : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language)) { } } public sealed class ActorService : AsyncServiceWrapper { - public ActorService(StartTracker tracker, DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, - Framework framework, DataManager gameData, GameGui gui, CutsceneService cutscene) + public ActorService(StartTracker tracker, DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, + Framework framework, IDataManager gameData, IGameGui gui, CutsceneService cutscene) : base(nameof(ActorService), tracker, StartTimeType.Actors, () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) { } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 0f163994..acead332 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Data; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -21,10 +21,10 @@ namespace Penumbra.UI.AdvancedWindow; public class FileEditor where T : class, IWritable { private readonly FileDialogService _fileDialog; - private readonly DataManager _gameData; + private readonly IDataManager _gameData; private readonly ModEditWindow _owner; - public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, + public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, Func parseFile) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 93d28b85..a36dcf14 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -3,10 +3,10 @@ using System.IO; using System.Linq; using System.Numerics; using System.Text; -using Dalamud.Data; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -518,7 +518,7 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } - public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index b2f13e51..7a81ec60 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using Dalamud.Data; using Dalamud.Interface; +using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; @@ -54,10 +54,10 @@ public class ChangedItemDrawer : IDisposable private readonly Dictionary _icons = new(16); private float _smallestIconWidth; - public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator, Configuration config) + public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { _items = gameData.GetExcelSheet()!; - uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true); + uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); _communicator = communicator; _config = config; } @@ -321,7 +321,7 @@ public class ChangedItemDrawer : IDisposable }; /// Initialize the icons. - private bool CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) + private bool CreateEquipSlotIcons(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); @@ -345,11 +345,11 @@ public class ChangedItemDrawer : IDisposable Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex")); - Add(ChangedItemIcon.Demihuman, gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex")); - Add(ChangedItemIcon.Customization, gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex")); - Add(ChangedItemIcon.Action, gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex")); - Add(AllFlags, gameData.GetImGuiTexture("ui/icon/114000/114052_hr1.tex")); + Add(ChangedItemIcon.Monster, textureProvider.GetTextureFromGame("ui/icon/062000/062042_hr1.tex", true)); + Add(ChangedItemIcon.Demihuman, textureProvider.GetTextureFromGame("ui/icon/062000/062041_hr1.tex", true)); + Add(ChangedItemIcon.Customization, textureProvider.GetTextureFromGame("ui/icon/062000/062043_hr1.tex", true)); + Add(ChangedItemIcon.Action, textureProvider.GetTextureFromGame("ui/icon/062000/062001_hr1.tex", true)); + Add(AllFlags, textureProvider.GetTextureFromGame("ui/icon/114000/114052_hr1.tex", true)); var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 1774cf1d..b5e60380 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -26,7 +26,7 @@ public sealed class CollectionPanel : IDisposable private readonly ActiveCollections _active; private readonly CollectionSelector _selector; private readonly ActorService _actors; - private readonly TargetManager _targets; + private readonly ITargetManager _targets; private readonly IndividualAssignmentUi _individualAssignmentUi; private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; @@ -40,7 +40,7 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorService actors, TargetManager targets, ModStorage mods) + CollectionSelector selector, ActorService actors, ITargetManager targets, ModStorage mods) { _collections = manager.Storage; _active = manager.Active; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index d6941ba0..d0f227c8 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -41,7 +41,7 @@ public class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, - CollectionManager collectionManager, ModStorage modStorage, ActorService actors, TargetManager targets, TutorialService tutorial) + CollectionManager collectionManager, ModStorage modStorage, ActorService actors, ITargetManager targets, TutorialService tutorial) { _config = configuration; _tutorial = tutorial; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 36bf9c7d..10e4be1d 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -5,8 +5,8 @@ using Penumbra.UI.Classes; using System; using System.Linq; using System.Numerics; -using Dalamud.Game.ClientState; using Dalamud.Interface; +using Dalamud.Plugin.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop.Services; @@ -27,11 +27,11 @@ public class ModsTab : ITab private readonly ActiveCollections _activeCollections; private readonly RedrawService _redrawService; private readonly Configuration _config; - private readonly ClientState _clientState; + private readonly IClientState _clientState; private readonly CollectionSelectHeader _collectionHeader; public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, - TutorialService tutorial, RedrawService redrawService, Configuration config, ClientState clientState, + TutorialService tutorial, RedrawService redrawService, Configuration config, IClientState clientState, CollectionSelectHeader collectionHeader) { _modManager = modManager; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 95ff0d2a..db1236bb 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Numerics; using Dalamud.Game; -using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; @@ -20,9 +19,9 @@ public class ResourceTab : ITab { private readonly Configuration _config; private readonly ResourceManagerService _resourceManager; - private readonly SigScanner _sigScanner; + private readonly ISigScanner _sigScanner; - public ResourceTab(Configuration config, ResourceManagerService resourceManager, SigScanner sigScanner) + public ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) { _config = config; _resourceManager = resourceManager; From cf3810a1b8840745d2b0f984b0e4e2c3c3882aca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Aug 2023 14:30:06 +0200 Subject: [PATCH 1089/2451] Add filter to texturedrawer. --- Penumbra/Import/Textures/TextureDrawer.cs | 88 +++++++++++-------- .../AdvancedWindow/ModEditWindow.Textures.cs | 17 ++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 31 +++---- 3 files changed, 77 insertions(+), 59 deletions(-) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index b077f6fd..fead989e 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -1,12 +1,16 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using OtterTex; +using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI; using Penumbra.UI.Classes; @@ -33,41 +37,6 @@ public static class TextureDrawer } } - public static void PathSelectBox(TextureManager textures, Texture current, string label, string tooltip, IEnumerable<(string, bool)> paths, - int skipPrefix) - { - ImGui.SetNextItemWidth(-0.0001f); - var startPath = current.Path.Length > 0 ? current.Path : "Choose a modded texture from this mod here..."; - using var combo = ImRaii.Combo(label, startPath); - if (combo) - foreach (var ((path, game), idx) in paths.WithIndex()) - { - if (game) - { - if (!textures.GameFileExists(path)) - continue; - } - else if (!File.Exists(path)) - { - continue; - } - - using var id = ImRaii.PushId(idx); - using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) - { - var p = game ? $"--> {path}" : path[skipPrefix..]; - if (ImGui.Selectable(p, path == startPath) && path != startPath) - current.Load(textures, path); - } - - ImGuiUtil.HoverTooltip(game - ? "This is a game path and refers to an unmanipulated file from your game data." - : "This is a path to a modded file on your file system."); - } - - ImGuiUtil.HoverTooltip(tooltip); - } - public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, string defaultModImportPath) { @@ -136,4 +105,53 @@ public static class TextureDrawer break; } } + + public sealed class PathSelectCombo : FilterComboCache<(string, bool)> + { + private int _skipPrefix = 0; + + public PathSelectCombo(TextureManager textures, ModEditor editor) + : base(() => CreateFiles(textures, editor)) + { } + + protected override string ToString((string, bool) obj) + => obj.Item1; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (path, game) = Items[globalIdx]; + bool ret; + using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) + { + var equals = string.Equals(CurrentSelection.Item1, path, StringComparison.OrdinalIgnoreCase); + var p = game ? $"--> {path}" : path[_skipPrefix..]; + ret = ImGui.Selectable(p, selected) && !equals; + } + + ImGuiUtil.HoverTooltip(game + ? "This is a game path and refers to an unmanipulated file from your game data." + : "This is a path to a modded file on your file system."); + return ret; + } + + private static IReadOnlyList<(string, bool)> CreateFiles(TextureManager textures, ModEditor editor) + => editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + .Prepend((f.File.FullName, false))) + .Where(p => p.Item2 ? textures.GameFileExists(p.Item1) : File.Exists(p.Item1)) + .ToList(); + + public bool Draw(string label, string tooltip, string current, int skipPrefix, out string newPath) + { + _skipPrefix = skipPrefix; + var startPath = current.Length > 0 ? current : "Choose a modded texture from this mod here..."; + if (!Draw(label, startPath, tooltip, -0.0001f, ImGui.GetTextLineHeightWithSpacing())) + { + newPath = current; + return false; + } + + newPath = CurrentSelection.Item1; + return true; + } + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 07607d11..32cd400f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Tasks; using OtterTex; using Penumbra.Import.Textures; @@ -16,9 +15,10 @@ public partial class ModEditWindow { private readonly TextureManager _textures; - private readonly Texture _left = new(); - private readonly Texture _right = new(); - private readonly CombinedTexture _center; + private readonly Texture _left = new(); + private readonly Texture _right = new(); + private readonly CombinedTexture _center; + private readonly TextureDrawer.PathSelectCombo _textureSelectCombo; private bool _overlayCollapsed = true; private bool _addMipMaps = true; @@ -47,11 +47,10 @@ public partial class ModEditWindow TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); - var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) - .Prepend((f.File.FullName, false))); - TextureDrawer.PathSelectBox(_textures, tex, "##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, - _mod.ModPath.FullName.Length + 1); + if (_textureSelectCombo.Draw("##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, + _mod.ModPath.FullName.Length + 1, out var newPath) && newPath != tex.Path) + tex.Load(_textures, newPath); if (tex == _left) _center.DrawMatrixInputLeft(size.X); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a36dcf14..8b870937 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -520,22 +520,22 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, + StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _dalamud = dalamud; - _modMergeTab = modMergeTab; - _communicator = communicator; - _textures = textures; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _dalamud = dalamud; + _modMergeTab = modMergeTab; + _communicator = communicator; + _textures = textures; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); @@ -544,8 +544,9 @@ public partial class ModEditWindow : Window, IDisposable _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new ShpkTab(_fileDialog, bytes)); - _center = new CombinedTexture(_left, _right); - _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); + _center = new CombinedTexture(_left, _right); + _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } From 04b76ddee1e36664cb2ba118a520e401b2ed06f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Aug 2023 17:25:06 +0200 Subject: [PATCH 1090/2451] Add support for the DalamudSubstitutionProvider for textures. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 127 +++++++++++++++++- Penumbra/Collections/Cache/CollectionCache.cs | 62 ++++++--- .../Cache/CollectionCacheManager.cs | 39 +++--- Penumbra/Communication/CollectionChange.cs | 4 + Penumbra/Communication/EnabledChanged.cs | 3 + Penumbra/Communication/ResolvedFileChanged.cs | 43 ++++++ Penumbra/Configuration.cs | 7 +- Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 59 ++++---- 10 files changed, 285 insertions(+), 66 deletions(-) create mode 100644 Penumbra/Communication/ResolvedFileChanged.cs diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index faa6710a..42abd1cd 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,7 +1,14 @@ using System; +using System.Collections.Generic; +using System.Linq; using Dalamud.Plugin.Services; +using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Services; using Penumbra.String.Classes; +using static Penumbra.Api.Ipc; namespace Penumbra.Api; @@ -9,19 +16,109 @@ public class DalamudSubstitutionProvider : IDisposable { private readonly ITextureSubstitutionProvider _substitution; private readonly ActiveCollectionData _activeCollectionData; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; - public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData) + public bool Enabled + => _config.UseDalamudUiTextureRedirection; + + public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData, + Configuration config, CommunicatorService communicator) { - _substitution = substitution; - _activeCollectionData = activeCollectionData; - _substitution.InterceptTexDataLoad += Substitute; + _substitution = substitution; + _activeCollectionData = activeCollectionData; + _config = config; + _communicator = communicator; + if (Enabled) + Subscribe(); + } + + public void Set(bool value) + { + if (value) + Enable(); + else + Disable(); + } + + public void ResetSubstitutions(IEnumerable paths) + { + var transformed = paths + .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) + .Select(p => p.ToString()); + _substitution.InvalidatePaths(transformed); + } + + public void Enable() + { + if (Enabled) + return; + + _config.UseDalamudUiTextureRedirection = true; + _config.Save(); + Subscribe(); + } + + public void Disable() + { + if (!Enabled) + return; + + Unsubscribe(); + _config.UseDalamudUiTextureRedirection = false; + _config.Save(); } public void Dispose() - => _substitution.InterceptTexDataLoad -= Substitute; + => Unsubscribe(); + + private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) + { + if (type is not CollectionType.Interface) + return; + + var enumerable = oldCollection?.ResolvedFiles.Keys ?? Array.Empty().AsEnumerable(); + enumerable = enumerable.Concat(newCollection?.ResolvedFiles.Keys ?? Array.Empty().AsEnumerable()); + ResetSubstitutions(enumerable); + } + + private void OnResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath _1, FullPath _2, + IMod? _3) + { + if (_activeCollectionData.Interface != collection) + return; + + switch (type) + { + case ResolvedFileChanged.Type.Added: + case ResolvedFileChanged.Type.Removed: + case ResolvedFileChanged.Type.Replaced: + ResetSubstitutions(new[] + { + key, + }); + break; + case ResolvedFileChanged.Type.FullRecomputeStart: + case ResolvedFileChanged.Type.FullRecomputeFinished: + ResetSubstitutions(collection.ResolvedFiles.Keys); + break; + } + } + + private void OnEnabledChange(bool state) + { + if (state) + OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty); + else + OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty); + } private void Substitute(string path, ref string? replacementPath) { + // Do not replace when not enabled. + if (!_config.EnableMods) + return; + // Let other plugins prioritize replacement paths. if (replacementPath != null) return; @@ -43,4 +140,22 @@ public class DalamudSubstitutionProvider : IDisposable // ignored } } -} \ No newline at end of file + + private void Subscribe() + { + _substitution.InterceptTexDataLoad += Substitute; + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.DalamudSubstitutionProvider); + _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.DalamudSubstitutionProvider); + _communicator.EnabledChanged.Subscribe(OnEnabledChange, EnabledChanged.Priority.DalamudSubstitutionProvider); + OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty); + } + + private void Unsubscribe() + { + _substitution.InterceptTexDataLoad -= Substitute; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange); + _communicator.EnabledChanged.Unsubscribe(OnEnabledChange); + OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty); + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 3477fdf0..1f712124 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Api.Enums; +using Penumbra.Communication; using Penumbra.String.Classes; using Penumbra.Mods.Manager; @@ -34,7 +35,7 @@ public class CollectionCache : IDisposable public int Calculating = -1; public string AnonymizedName - => _collection.AnonymizedName; + => _collection.AnonymizedName; public IEnumerable> AllConflicts => _conflicts.Values; @@ -63,9 +64,7 @@ public class CollectionCache : IDisposable } public void Dispose() - { - Meta.Dispose(); - } + => Meta.Dispose(); ~CollectionCache() => Meta.Dispose(); @@ -130,8 +129,8 @@ public class CollectionCache : IDisposable => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath)); public void RemovePath(Utf8GamePath path) - => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); - + => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); + public void ReloadMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges)); @@ -139,8 +138,8 @@ public class CollectionCache : IDisposable => _manager.AddChange(ChangeData.ModAddition(this, mod, addMetaChanges)); public void RemoveMod(IMod mod, bool addMetaChanges) - => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); - + => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); + /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -148,9 +147,24 @@ public class CollectionCache : IDisposable return; if (ResolvedFiles.Remove(path, out var modPath)) + { ModData.RemovePath(modPath.Mod, path); - if (fullPath.FullName.Length > 0) + if (fullPath.FullName.Length > 0) + { + ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, + Mod.ForcedFiles); + } + else + { + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); + } + } + else if (fullPath.FullName.Length > 0) + { ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); + } } private void ReloadModSync(IMod mod, bool addMetaChanges) @@ -169,9 +183,14 @@ public class CollectionCache : IDisposable foreach (var path in paths) { - if (ResolvedFiles.Remove(path, out var mp) && mp.Mod != mod) - Penumbra.Log.Warning( - $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); + if (ResolvedFiles.Remove(path, out var mp)) + { + if (mp.Mod != mod) + Penumbra.Log.Warning( + $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); + else + _manager.ResolvedFileChanged.Invoke(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, mp.Path, mp.Mod); + } } foreach (var manipulation in manipulations) @@ -203,7 +222,7 @@ public class CollectionCache : IDisposable } - /// Add all files and possibly manipulations of a given mod according to its settings in this collection. + /// Add all files and possibly manipulations of a given mod according to its settings in this collection. internal void AddModSync(IMod mod, bool addMetaChanges) { if (mod.Index >= 0) @@ -257,6 +276,14 @@ public class CollectionCache : IDisposable foreach (var manip in subMod.Manipulations) AddManipulation(manip, parentMod); } + + /// Invoke only if not in a full recalculation. + private void InvokeResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath value, + FullPath old, IMod? mod) + { + if (Calculating == -1) + _manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod); + } // Add a specific file redirection, handling potential conflicts. // For different mods, higher mod priority takes precedence before option group priority, @@ -271,7 +298,8 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { - ModData.AddPath(mod, path); + ModData.AddPath(mod, path); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -285,11 +313,13 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } catch (Exception ex) { - Penumbra.Log.Error($"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); + Penumbra.Log.Error( + $"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); } } @@ -491,7 +521,7 @@ public class CollectionCache : IDisposable case 2: Cache.ReloadModSync(Mod, AddMetaChanges); break; - case 3: + case 3: Cache.ForceFileSync(Path, FullPath); break; } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index f2223849..abe5bfca 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -15,17 +15,19 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; public class CollectionCacheManager : IDisposable { - private readonly FrameworkManager _framework; - private readonly CommunicatorService _communicator; - private readonly TempModManager _tempMods; - private readonly ModStorage _modStorage; - private readonly CollectionStorage _storage; - private readonly ActiveCollections _active; + private readonly FrameworkManager _framework; + private readonly CommunicatorService _communicator; + private readonly TempModManager _tempMods; + private readonly ModStorage _modStorage; + private readonly CollectionStorage _storage; + private readonly ActiveCollections _active; + internal readonly ResolvedFileChanged ResolvedFileChanged; internal readonly MetaFileManager MetaFileManager; @@ -39,16 +41,17 @@ public class CollectionCacheManager : IDisposable public IEnumerable Active => _storage.Where(c => c.HasCache); - public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, - TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) + public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) { - _framework = framework; - _communicator = communicator; - _tempMods = tempMods; - _modStorage = modStorage; - MetaFileManager = metaFileManager; - _active = active; - _storage = storage; + _framework = framework; + _communicator = communicator; + _tempMods = tempMods; + _modStorage = modStorage; + MetaFileManager = metaFileManager; + _active = active; + _storage = storage; + ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) _active.Individuals.Loaded += CreateNecessaryCaches; @@ -158,6 +161,9 @@ public class CollectionCacheManager : IDisposable cache.Calculating = Environment.CurrentManagedThreadId; try { + ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty, + FullPath.Empty, + null); cache.ResolvedFiles.Clear(); cache.Meta.Reset(); cache._conflicts.Clear(); @@ -177,6 +183,9 @@ public class CollectionCacheManager : IDisposable collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); + ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty, + FullPath.Empty, + null); } finally { diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 7c4946d2..96dd61ab 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -17,6 +17,9 @@ public sealed class CollectionChange : EventWrapper + DalamudSubstitutionProvider = -3, + /// CollectionCacheManager = -2, @@ -43,6 +46,7 @@ public sealed class CollectionChange : EventWrapper ModFileSystemSelector = 0, + } public CollectionChange() diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index dee5e50f..38c6b387 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -16,6 +16,9 @@ public sealed class EnabledChanged : EventWrapper, EnabledChanged.P { /// Api = int.MinValue, + + /// + DalamudSubstitutionProvider = 0, } public EnabledChanged() diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs new file mode 100644 index 00000000..99cec829 --- /dev/null +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -0,0 +1,43 @@ +using System; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a redirection in a mod collection cache is manipulated. +/// +/// Parameter is collection with a changed cache. +/// Parameter is the type of change. +/// Parameter is the game path to be redirected or empty for FullRecompute. +/// Parameter is the new redirection path or empty for Removed or FullRecompute +/// Parameter is the old redirection path for Replaced, or empty. +/// Parameter is the mod responsible for the new redirection if any. +/// +public sealed class ResolvedFileChanged : EventWrapper, + ResolvedFileChanged.Priority> +{ + public enum Type + { + Added, + Removed, + Replaced, + FullRecomputeStart, + FullRecomputeFinished, + } + + public enum Priority + { + /// + DalamudSubstitutionProvider = 0, + } + + public ResolvedFileChanged() + : base(nameof(ResolvedFileChanged)) + { } + + public void Invoke(ModCollection collection, Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod) + => Invoke(this, collection, type, key, value, old, mod); +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index beaf1960..cc7cc026 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -38,9 +38,10 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; - public bool HideUiWhenUiHidden { get; set; } = false; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiWhenUiHidden { get; set; } = false; + public bool UseDalamudUiTextureRedirection { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 784b7f89..728b391c 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -66,6 +66,9 @@ public class CommunicatorService : IDisposable /// public readonly SelectTab SelectTab = new(); + /// + public readonly ResolvedFileChanged ResolvedFileChanged = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -86,5 +89,6 @@ public class CommunicatorService : IDisposable ChangedItemHover.Dispose(); ChangedItemClick.Dispose(); SelectTab.Dispose(); + ResolvedFileChanged.Dispose(); } } diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index f0864b97..728585ae 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -181,5 +181,6 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton(x => x.GetRequiredService()) .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 375ada2d..fa4c23f9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -26,38 +26,41 @@ public class SettingsTab : ITab public ReadOnlySpan Label => "Settings"u8; - private readonly Configuration _config; - private readonly FontReloader _fontReloader; - private readonly TutorialService _tutorial; - private readonly Penumbra _penumbra; - private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; - private readonly ModExportManager _modExportManager; - private readonly ModFileSystemSelector _selector; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly DalamudServices _dalamud; - private readonly HttpApi _httpApi; + private readonly Configuration _config; + private readonly FontReloader _fontReloader; + private readonly TutorialService _tutorial; + private readonly Penumbra _penumbra; + private readonly FileDialogService _fileDialog; + private readonly ModManager _modManager; + private readonly ModExportManager _modExportManager; + private readonly ModFileSystemSelector _selector; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly DalamudServices _dalamud; + private readonly HttpApi _httpApi; + private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi) + ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi, + DalamudSubstitutionProvider dalamudSubstitutionProvider) { - _config = config; - _fontReloader = fontReloader; - _tutorial = tutorial; - _penumbra = penumbra; - _fileDialog = fileDialog; - _modManager = modManager; - _selector = selector; - _characterUtility = characterUtility; - _residentResources = residentResources; - _dalamud = dalamud; - _modExportManager = modExportManager; - _httpApi = httpApi; + _config = config; + _fontReloader = fontReloader; + _tutorial = tutorial; + _penumbra = penumbra; + _fileDialog = fileDialog; + _modManager = modManager; + _selector = selector; + _characterUtility = characterUtility; + _residentResources = residentResources; + _dalamud = dalamud; + _modExportManager = modExportManager; + _httpApi = httpApi; + _dalamudSubstitutionProvider = dalamudSubstitutionProvider; } public void DrawHeader() @@ -389,6 +392,12 @@ public class SettingsTab : ITab /// Draw all settings pertaining to actor identification for collections. private void DrawIdentificationSettings() { + Checkbox("Use Interface Collection for other Plugin UIs", + "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", + _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); + var icon = _dalamud.TextureProvider.GetIcon(60026); + if (icon != null) + ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); From 0c07d4bec677182156b56f4f3c649e556e8f3fcf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Aug 2023 17:39:40 +0200 Subject: [PATCH 1091/2451] Cleanup. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 1 - Penumbra/UI/Tabs/SettingsTab.cs | 3 --- 2 files changed, 4 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 42abd1cd..ef6555bb 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -8,7 +8,6 @@ using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Services; using Penumbra.String.Classes; -using static Penumbra.Api.Ipc; namespace Penumbra.Api; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index fa4c23f9..ac834e7c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -395,9 +395,6 @@ public class SettingsTab : ITab Checkbox("Use Interface Collection for other Plugin UIs", "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); - var icon = _dalamud.TextureProvider.GetIcon(60026); - if (icon != null) - ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); From 53b36f2597696ffce4de6048d190ce6b7eb60c0f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Aug 2023 17:16:38 +0200 Subject: [PATCH 1092/2451] Add drag & drop to texture import. --- Penumbra/Api/PenumbraApi.cs | 1 - .../AdvancedWindow/ModEditWindow.Textures.cs | 70 +++++++++++++------ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 35 +++++----- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 140a928b..01078450 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,7 +8,6 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 32cd400f..fcbb054d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Numerics; @@ -37,30 +39,36 @@ public partial class ModEditWindow private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize) { - using var child = ImRaii.Child(label, size, true); - if (!child) - return; + using (var child = ImRaii.Child(label, size, true)) + { + if (!child) + return; - using var id = ImRaii.PushId(label); - ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); - ImGui.NewLine(); + using var id = ImRaii.PushId(label); + ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); + ImGui.NewLine(); - TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", - "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); - if (_textureSelectCombo.Draw("##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, - _mod.ModPath.FullName.Length + 1, out var newPath) && newPath != tex.Path) - tex.Load(_textures, newPath); + TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", + "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); + if (_textureSelectCombo.Draw("##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, + _mod.ModPath.FullName.Length + 1, out var newPath) + && newPath != tex.Path) + tex.Load(_textures, newPath); - if (tex == _left) - _center.DrawMatrixInputLeft(size.X); - else - _center.DrawMatrixInputRight(size.X); + if (tex == _left) + _center.DrawMatrixInputLeft(size.X); + else + _center.DrawMatrixInputRight(size.X); - ImGui.NewLine(); - using var child2 = ImRaii.Child("image"); - if (child2) - TextureDrawer.Draw(tex, imageSize); + ImGui.NewLine(); + using var child2 = ImRaii.Child("image"); + if (child2) + TextureDrawer.Draw(tex, imageSize); + } + + if (_dragDropManager.CreateImGuiTarget("TextureDragDrop", out var files, out _) && GetFirstTexture(files, out var file)) + tex.Load(_textures, file); } private void SaveAsCombo() @@ -229,6 +237,15 @@ public partial class ModEditWindow try { + _dragDropManager.CreateImGuiSource("TextureDragDrop", + m => m.Extensions.Any(e => ValidTextureExtensions.Contains(e.ToLowerInvariant())), m => + { + if (!GetFirstTexture(m.Files, out var file)) + return false; + + ImGui.TextUnformatted($"Dragging texture for editing: {Path.GetFileName(file)}"); + return true; + }); var childWidth = GetChildWidth(); var imageSize = new Vector2(childWidth.X - ImGui.GetStyle().FramePadding.X * 2); DrawInputChild("Input Texture", _left, childWidth, imageSize); @@ -259,4 +276,17 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(tooltip); } + + private static bool GetFirstTexture(IEnumerable files, [NotNullWhen(true)] out string? file) + { + file = files.FirstOrDefault(f => ValidTextureExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())); + return file != null; + } + + private static readonly string[] ValidTextureExtensions = + { + ".png", + ".dds", + ".tex", + }; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 8b870937..59cf8b80 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Text; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.DragDrop; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; @@ -40,6 +41,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly StainService _stainService; private readonly ModMergeTab _modMergeTab; private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -283,7 +285,7 @@ public partial class ModEditWindow : Window, IDisposable } else { - if (ImGuiUtil.DrawDisabledButton("Cancel Scanning for Duplicates", Vector2.Zero, "Cancel the current scanning operation...", false)) + if (ImGuiUtil.DrawDisabledButton("Cancel Scanning for Duplicates", Vector2.Zero, "Cancel the current scanning operation...", false)) _editor.Duplicates.Clear(); } @@ -303,14 +305,14 @@ public partial class ModEditWindow : Window, IDisposable new Vector2(300 * UiHelpers.Scale, ImGui.GetFrameHeight()), $"{_editor.ModNormalizer.Step} / {_editor.ModNormalizer.TotalSteps}"); } - else if(ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) + else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { _editor.ModNormalizer.Normalize(_mod!); _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx)); } if (!_editor.Duplicates.Worker.IsCompleted) - return; + return; if (_editor.Duplicates.Duplicates.Count == 0) { @@ -521,21 +523,22 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _dalamud = dalamud; - _modMergeTab = modMergeTab; - _communicator = communicator; - _textures = textures; - _fileDialog = fileDialog; + _performance = performance; + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _dalamud = dalamud; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); From 635d5e05ce2ad4dc923280e615d14b5196a1aa52 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Aug 2023 20:05:15 +0200 Subject: [PATCH 1093/2451] 0.7.3.0 --- Penumbra.GameData | 2 +- Penumbra/UI/Changelog.cs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d84508ea..ad67dfcd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d84508ea1a607976525265e8f75a329667eec0e5 +Subproject commit ad67dfcd95912d171f2c9576d70d328bdd68b1ca diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index cecd1380..a91c2150 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -41,10 +41,30 @@ public class PenumbraChangelog Add7_1_0(Changelog); Add7_1_2(Changelog); Add7_2_0(Changelog); + Add7_3_0(Changelog); } #region Changelogs + private static void Add7_3_0(Changelog log) + => log.NextVersion("Version 0.7.3.0") + .RegisterEntry("Added the ability to drag and drop mod files from external sources (like a file explorer or browser) into Penumbras mod selector to import them.") + .RegisterEntry("You can also drag and drop texture files into the textures tab of the Advanced Editing Window.", 1) + .RegisterEntry("Added a priority display to the mod selector using the currently selected collections priorities. This can be hidden in settings.") + .RegisterEntry("Added IPC for texture conversion, improved texture handling backend and threading.") + .RegisterEntry("Added Dalamud Substitution so that other plugins can more easily use replaced icons from Penumbras Interface collection when using Dalamuds new Texture Provider.") + .RegisterEntry("Added a filter to texture selection combos in the textures tab of the Advanced Editing Window.") + .RegisterEntry("Changed behaviour when failing to load group JSON files for mods - the pre-existing but failing files are now backed up before being deleted or overwritten.") + .RegisterEntry("Further backend changes, mostly relating to the Glamourer rework.") + .RegisterEntry("Fixed an issue with modded decals not loading correctly when used with the Glamourer rework.") + .RegisterEntry("Fixed missing scaling with UI Scale for some combos.") + .RegisterEntry("Updated the used version of SharpCompress to deal with Zip64 correctly.") + .RegisterEntry("Added a toggle to not display the Changed Item categories in settings (0.7.2.2).") + .RegisterEntry("Many backend changes relating to the Glamourer rework (0.7.2.2).") + .RegisterEntry("Fixed an issue when multiple options in the same option group had the same label (0.7.2.2).") + .RegisterEntry("Fixed an issue with a GPose condition breaking animation and vfx modding in GPose (0.7.2.1).") + .RegisterEntry("Fixed some handling of decals (0.7.2.1)."); + private static void Add7_2_0(Changelog log) => log.NextVersion("Version 0.7.2.0") .RegisterEntry("Added Changed Item Categories and icons that can filter for specific types of Changed Items, in the Changed Items Tab as well as in the Changed Items panel for specific mods..") From 82ba2cd16a45c4cb9dd544db27e09e0a1c51148c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 18 Aug 2023 18:07:12 +0000 Subject: [PATCH 1094/2451] [CI] Updating repo.json for 0.7.3.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 760f1ad1..0e4bba68 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.2.2", - "TestingAssemblyVersion": "0.7.2.7", + "AssemblyVersion": "0.7.3.0", + "TestingAssemblyVersion": "0.7.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.2.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.2.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 4c611530f397aa6463bd01a7fdc728d9b3fd29b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Aug 2023 20:34:16 +0200 Subject: [PATCH 1095/2451] Temporarily not use dalamud function because it is not available in release yet. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index ef6555bb..e608b854 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -42,10 +42,10 @@ public class DalamudSubstitutionProvider : IDisposable public void ResetSubstitutions(IEnumerable paths) { - var transformed = paths - .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) - .Select(p => p.ToString()); - _substitution.InvalidatePaths(transformed); + //var transformed = paths + // .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) + // .Select(p => p.ToString()); + //_substitution.InvalidatePaths(transformed); } public void Enable() From ebaa42f3111341218272e1b3fa083c48561e5cf9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 18 Aug 2023 18:37:27 +0000 Subject: [PATCH 1096/2451] [CI] Updating repo.json for 0.7.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 0e4bba68..23e4cc96 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.3.0", - "TestingAssemblyVersion": "0.7.3.0", + "AssemblyVersion": "0.7.3.1", + "TestingAssemblyVersion": "0.7.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ad830dc56e19b664d99adac284edb26079120436 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Aug 2023 14:18:29 +0200 Subject: [PATCH 1097/2451] Disable UI for textures when converting. --- Penumbra.GameData | 2 +- Penumbra/Api/DalamudSubstitutionProvider.cs | 1 + Penumbra/Import/Textures/TextureManager.cs | 10 ++--- .../AdvancedWindow/ModEditWindow.Textures.cs | 37 ++++++++++++------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ad67dfcd..36df7a87 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ad67dfcd95912d171f2c9576d70d328bdd68b1ca +Subproject commit 36df7a87680eecde48801a38271f0ed8696233ed diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index e608b854..07da761f 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -42,6 +42,7 @@ public class DalamudSubstitutionProvider : IDisposable public void ResetSubstitutions(IEnumerable paths) { + // TODO fix //var transformed = paths // .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) // .Select(p => p.ToString()); diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 3b4c7f67..8c7ec609 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -186,8 +186,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => - ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -344,10 +343,11 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable if (numMips == input.Meta.MipLevels) return input; - var ec = input.GenerateMipMaps(out var ret, numMips, - (Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha); + var flags = (Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha; + var ec = input.GenerateMipMaps(out var ret, numMips, flags); if (ec != ErrorCode.Ok) - throw new Exception($"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}"); + throw new Exception( + $"Could not create the requested {numMips} mip maps (input has {input.Meta.MipLevels}) with flags [{flags}], maybe retry with the top-right checkbox unchecked:\n{ec}"); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index fcbb054d..4d36ff8a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -5,11 +5,13 @@ using System.IO; using System.Linq; using System.Numerics; using System.Threading.Tasks; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterTex; using Penumbra.Import.Textures; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -48,18 +50,21 @@ public partial class ModEditWindow ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", - "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); - if (_textureSelectCombo.Draw("##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, - _mod.ModPath.FullName.Length + 1, out var newPath) - && newPath != tex.Path) - tex.Load(_textures, newPath); + using (var disabled = ImRaii.Disabled(!_center.SaveTask.IsCompleted)) + { + TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", + "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); + if (_textureSelectCombo.Draw("##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, + _mod.ModPath.FullName.Length + 1, out var newPath) + && newPath != tex.Path) + tex.Load(_textures, newPath); - if (tex == _left) - _center.DrawMatrixInputLeft(size.X); - else - _center.DrawMatrixInputRight(size.X); + if (tex == _left) + _center.DrawMatrixInputLeft(size.X); + else + _center.DrawMatrixInputRight(size.X); + } ImGui.NewLine(); using var child2 = ImRaii.Child("image"); @@ -177,8 +182,6 @@ public partial class ModEditWindow { ImGui.NewLine(); } - - ImGui.NewLine(); } switch (_center.SaveTask.Status) @@ -186,7 +189,8 @@ public partial class ModEditWindow case TaskStatus.WaitingForActivation: case TaskStatus.WaitingToRun: case TaskStatus.Running: - ImGui.TextUnformatted("Computing..."); + ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg); + break; case TaskStatus.Canceled: case TaskStatus.Faulted: @@ -196,8 +200,13 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); break; } + default: + ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); + break; } + ImGui.NewLine(); + using var child2 = ImRaii.Child("image"); if (child2) _center.Draw(_textures, imageSize); From bc6e9d1d84516408d04ceee12cb1ffc61538ada0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 22 Aug 2023 15:18:30 +0200 Subject: [PATCH 1098/2451] Update DirectXTex/OtterTex --- Penumbra/lib/DirectXTexC.dll | Bin 585728 -> 806400 bytes Penumbra/lib/OtterTex.dll | Bin 32256 -> 32256 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Penumbra/lib/DirectXTexC.dll b/Penumbra/lib/DirectXTexC.dll index 083c20e9efd442251c355fa32f721295f3f6ef4d..5e0aa0c43150ac1a01cfb8e0e016517559f6328a 100644 GIT binary patch delta 239984 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 jzIx8;^3|2At5($#)xQ4^>>8Ct delta 124823 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 ahU>!hVTp2`ky$nuZ(g*yb~Bx~_kRF@j05EW diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 32744d0dc101a9eac49f9111cdffddd8f23ffaf4..d4819ff2a44a62390ff6e861cf98b902676fd91e 100644 GIT binary patch literal 32256 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 Date: Tue, 22 Aug 2023 15:18:59 +0200 Subject: [PATCH 1099/2451] Keep the texture alive during write. --- Penumbra/Import/Textures/TextureManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 8c7ec609..f67dabb0 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -418,6 +418,8 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable using var w = new BinaryWriter(stream); header.Write(w); w.Write(input.Pixels); + // No idea why this is necessary, but it is. + GC.KeepAlive(input); } private readonly struct ImageInputData From 3a8bf5dfa15ad819b041cb35939c576f68842375 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 22 Aug 2023 13:21:39 +0000 Subject: [PATCH 1100/2451] [CI] Updating repo.json for 0.7.3.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 23e4cc96..630dab0d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.3.1", - "TestingAssemblyVersion": "0.7.3.1", + "AssemblyVersion": "0.7.3.2", + "TestingAssemblyVersion": "0.7.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a6ae580b9f23f7f43acf3637b866435616929396 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 23 Aug 2023 18:42:12 +0200 Subject: [PATCH 1101/2451] Explain comment. --- Penumbra/Import/Textures/TextureManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index f67dabb0..9e4890f2 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -418,7 +418,8 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable using var w = new BinaryWriter(stream); header.Write(w); w.Write(input.Pixels); - // No idea why this is necessary, but it is. + // Necessary due to the GC being allowed to collect after the last invocation of an object, + // thus invalidating the ReadOnlySpan. GC.KeepAlive(input); } From 3530e139d1d4a3cd1c39a02dcdf5eec15b6de178 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 23 Aug 2023 18:47:22 +0200 Subject: [PATCH 1102/2451] Add some unnamed mounts to actor identification. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 36df7a87..97643cad 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 36df7a87680eecde48801a38271f0ed8696233ed +Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf From ccca2f14340899c8ce3e4eac6323766f970db90d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 19 Aug 2023 03:16:45 +0200 Subject: [PATCH 1103/2451] Material editor: improve color accuracy --- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index cd599f11..c03272ef 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -412,12 +412,13 @@ public partial class ModEditWindow private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; - var tmp = input; + var inputSqrt = Vector3.SquareRoot( input ); + var tmp = inputSqrt; if( ImGui.ColorEdit3( label, ref tmp, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) - && tmp != input ) + && tmp != inputSqrt ) { - setter( tmp ); + setter( tmp * tmp ); ret = true; } From f64fdd2b26d53bae3a1c24b256637e71ae53ba40 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 19 Aug 2023 05:42:26 +0200 Subject: [PATCH 1104/2451] Material editor: live-preview changes --- .../Interop/ResourceTree/ResolveContext.cs | 26 +- Penumbra/Interop/Structs/ConstantBuffer.cs | 31 ++ Penumbra/Interop/Structs/Material.cs | 35 +- Penumbra/Interop/Structs/MtrlResource.cs | 22 +- Penumbra/Interop/Structs/ResourceHandle.cs | 14 +- .../Interop/Structs/ShaderPackageUtility.cs | 19 + Penumbra/Interop/Structs/TextureUtility.cs | 36 ++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 41 +- .../ModEditWindow.Materials.ColorSet.cs | 136 +++-- .../ModEditWindow.Materials.LivePreview.cs | 484 ++++++++++++++++++ .../ModEditWindow.Materials.MtrlTab.cs | 269 +++++++++- .../ModEditWindow.Materials.Shpk.cs | 20 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 32 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 12 +- 14 files changed, 1067 insertions(+), 110 deletions(-) create mode 100644 Penumbra/Interop/Structs/ConstantBuffer.cs create mode 100644 Penumbra/Interop/Structs/ShaderPackageUtility.cs create mode 100644 Penumbra/Interop/Structs/TextureUtility.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 90ee1a16..6dc005ee 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -65,26 +65,34 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, - bool withName) + public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle) { - if (handle == null) - return null; - var name = handle->FileName(); if (name.IsEmpty) - return null; + return FullPath.Empty; if (name[0] == (byte)'|') { var pos = name.IndexOf((byte)'|', 1); if (pos < 0) - return null; + return FullPath.Empty; name = name.Substring(pos + 1); } - var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + return new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + } + + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + bool withName) + { + if (handle == null) + return null; + + var fullPath = GetResourceHandlePath(handle); + if (fullPath.InternalName.IsEmpty) + return null; + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); fullPath = FilterFullPath(fullPath); @@ -161,7 +169,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (mtrl == null) return null; - var resource = (MtrlResource*)mtrl->ResourceHandle; + var resource = mtrl->ResourceHandle; var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); if (node == null) return null; diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs new file mode 100644 index 00000000..52df6477 --- /dev/null +++ b/Penumbra/Interop/Structs/ConstantBuffer.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit, Size = 0x70)] +public unsafe struct ConstantBuffer +{ + [FieldOffset(0x20)] + public int Size; + + [FieldOffset(0x24)] + public int Flags; + + [FieldOffset(0x28)] + private void* _maybeSourcePointer; + + public bool TryGetBuffer(out Span buffer) + { + if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null) + { + buffer = new Span(_maybeSourcePointer, Size >> 2); + return true; + } + else + { + buffer = null; + return false; + } + } +} diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 7cee271e..7b66531c 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -3,17 +3,42 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout( LayoutKind.Explicit, Size = 0x40 )] public unsafe struct Material { [FieldOffset( 0x10 )] - public ResourceHandle* ResourceHandle; + public MtrlResource* ResourceHandle; + + [FieldOffset( 0x18 )] + public uint ShaderPackageFlags; + + [FieldOffset( 0x20 )] + public uint* ShaderKeys; + + public int ShaderKeyCount + => (int)((uint*)Textures - ShaderKeys); [FieldOffset( 0x28 )] - public void* MaterialData; + public ConstantBuffer* MaterialParameter; [FieldOffset( 0x30 )] - public void** Textures; + public TextureEntry* Textures; - public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1]; + [FieldOffset( 0x38 )] + public ushort TextureCount; + + public Texture* Texture( int index ) => Textures[index].ResourceHandle->KernelTexture; + + [StructLayout( LayoutKind.Explicit, Size = 0x18 )] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public uint Id; + + [FieldOffset( 0x08 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x10 )] + public uint SamplerFlags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 28756877..424adfe4 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -8,8 +8,11 @@ public unsafe struct MtrlResource [FieldOffset( 0x00 )] public ResourceHandle Handle; + [FieldOffset( 0xC8 )] + public ShaderPackageResourceHandle* ShpkResourceHandle; + [FieldOffset( 0xD0 )] - public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. [FieldOffset( 0xE0 )] public byte* StringList; @@ -24,8 +27,21 @@ public unsafe struct MtrlResource => StringList + ShpkOffset; public byte* TexString( int idx ) - => StringList + *( TexSpace + 4 + idx * 8 ); + => StringList + TexSpace[idx].PathOffset; public bool TexIsDX11( int idx ) - => *(TexSpace + 5 + idx * 8) >= 0x8000; + => TexSpace[idx].Flags >= 0x8000; + + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x08 )] + public ushort PathOffset; + + [FieldOffset( 0x0A )] + public ushort Flags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 4de81903..5db0f8e1 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle public IntPtr Unk; [FieldOffset( 0x118 )] - public IntPtr KernelTexture; + public Texture* KernelTexture; [FieldOffset( 0x20 )] public IntPtr NewKernelTexture; } +[StructLayout(LayoutKind.Explicit)] +public unsafe struct ShaderPackageResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0xB0 )] + public ShaderPackage* ShaderPackage; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs new file mode 100644 index 00000000..5bf95f5b --- /dev/null +++ b/Penumbra/Interop/Structs/ShaderPackageUtility.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +public static class ShaderPackageUtility +{ + [StructLayout(LayoutKind.Explicit, Size = 0xC)] + public unsafe struct Sampler + { + [FieldOffset(0x0)] + public uint Crc; + + [FieldOffset(0x4)] + public uint Id; + + [FieldOffset(0xA)] + public ushort Slot; + } +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs new file mode 100644 index 00000000..ec9c4b71 --- /dev/null +++ b/Penumbra/Interop/Structs/TextureUtility.cs @@ -0,0 +1,36 @@ +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +public unsafe static class TextureUtility +{ + private static readonly Functions Funcs = new(); + + public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, flags, unk); + + public static bool InitializeContents(Texture* texture, void* contents) + => ((delegate* unmanaged)Funcs.TextureInitializeContents)(texture, contents); + + public static void IncRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[2])(texture); + + public static void DecRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[3])(texture); + + private sealed class Functions + { + [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] + public nint TextureCreate2D = nint.Zero; + + [Signature("E9 ?? ?? ?? ?? 8B 02 25")] + public nint TextureInitializeContents = nint.Zero; + + public Functions() + { + SignatureHelper.Initialise(this); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index acead332..ac873ce2 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -18,7 +18,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor where T : class, IWritable +public class FileEditor : IDisposable where T : class, IWritable { private readonly FileDialogService _fileDialog; private readonly IDataManager _gameData; @@ -26,7 +26,7 @@ public class FileEditor where T : class, IWritable public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) + Func parseFile) { _owner = owner; _gameData = gameData; @@ -39,6 +39,11 @@ public class FileEditor where T : class, IWritable _combo = new Combo(config, getFiles); } + ~FileEditor() + { + DoDispose(); + } + public void Draw() { using var tab = ImRaii.TabItem(_tabName); @@ -60,11 +65,23 @@ public class FileEditor where T : class, IWritable DrawFilePanel(); } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; + public void Dispose() + { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + } + + private readonly string _tabName; + private readonly string _fileType; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private FileRegistry? _currentPath; private T? _currentFile; @@ -99,7 +116,9 @@ public class FileEditor where T : class, IWritable if (file != null) { _defaultException = null; - _defaultFile = _parseFile(file.Data); + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _defaultFile = _parseFile(file.Data, _defaultPath, false); } else { @@ -158,6 +177,7 @@ public class FileEditor where T : class, IWritable { _currentException = null; _currentPath = null; + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _changed = false; } @@ -181,10 +201,13 @@ public class FileEditor where T : class, IWritable try { var bytes = File.ReadAllBytes(_currentPath.File.FullName); - _currentFile = _parseFile(bytes); + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _currentException = e; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index c03272ef..1d6c480a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,20 +13,20 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) { - if( !file.ColorSets.Any( c => c.HasRows ) ) + if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) { return false; } - ColorSetCopyAllClipboardButton( file, 0 ); + ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( file, 0 ); + var ret = ColorSetPasteAllClipboardButton( tab, 0 ); ImGui.SameLine(); ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); ImGui.SameLine(); - ret |= DrawPreviewDye( file, disabled ); + ret |= DrawPreviewDye( tab, disabled ); using var table = ImRaii.Table( "##ColorSets", 11, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); @@ -58,12 +58,12 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableHeader( "Dye Preview" ); - for( var j = 0; j < file.ColorSets.Length; ++j ) + for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) { using var _ = ImRaii.PushId( j ); for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= DrawColorSetRow( file, j, i, disabled ); + ret |= DrawColorSetRow( tab, j, i, disabled ); ImGui.TableNextRow(); } } @@ -95,33 +95,36 @@ public partial class ModEditWindow } } - private bool DrawPreviewDye( MtrlFile file, bool disabled ) + private bool DrawPreviewDye( MtrlTab tab, bool disabled ) { var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) { var ret = false; - for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + for( var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j ) { for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); + ret |= tab.Mtrl.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); } } + tab.UpdateColorSetPreview(); + return ret; } ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true, gloss); + if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) + tab.UpdateColorSetPreview(); return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) + if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } @@ -135,14 +138,14 @@ public partial class ModEditWindow return false; } - ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; + ref var rows = ref tab.Mtrl.ColorSets[ colorSetIdx ].Rows; fixed( void* ptr = data, output = &rows ) { MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx ) + && tab.Mtrl.ColorDyeSets.Length > colorSetIdx ) { - ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; + ref var dyeRows = ref tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows; fixed( void* output2 = &dyeRows ) { MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); @@ -150,6 +153,8 @@ public partial class ModEditWindow } } + tab.UpdateColorSetPreview(); + return true; } catch @@ -182,7 +187,7 @@ public partial class ModEditWindow } } - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Import an exported row from your clipboard onto this row.", disabled, true ) ) @@ -192,20 +197,22 @@ public partial class ModEditWindow var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String( text ); if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || file.ColorSets.Length <= colorSetIdx ) + || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } fixed( byte* ptr = data ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < file.ColorDyeSets.Length ) + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( colorSetIdx < tab.Mtrl.ColorDyeSets.Length ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); } } + tab.UpdateColorSetRowPreview(rowIdx); + return true; } catch @@ -217,7 +224,18 @@ public partial class ModEditWindow return false; } - private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static void ColorSetHighlightButton( MtrlTab tab, int rowIdx, bool disabled ) + { + ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true ); + + if( ImGui.IsItemHovered() ) + tab.HighlightColorSetRow( rowIdx ); + else if( tab.HighlightedColorSetRow == rowIdx ) + tab.CancelColorSetHighlight(); + } + + private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { static bool FixFloat( ref float val, float current ) { @@ -226,38 +244,41 @@ public partial class ModEditWindow } using var id = ImRaii.PushId( rowIdx ); - var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = file.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); ColorSetCopyClipboardButton( row, dye ); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + var ret = ColorSetPasteFromClipboardButton( tab, colorSetIdx, rowIdx, disabled ); + ImGui.SameLine(); + ColorSetHighlightButton( tab, rowIdx, disabled ); ImGui.TableNextColumn(); ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); ImGui.TableNextColumn(); using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); + ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c; tab.UpdateColorSetRowPreview(rowIdx); } ); if( hasDye ) { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c; tab.UpdateColorSetRowPreview(rowIdx); } ); ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -266,19 +287,19 @@ public partial class ModEditWindow { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c; tab.UpdateColorSetRowPreview(rowIdx); } ); if( hasDye ) { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); @@ -286,8 +307,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -295,7 +317,7 @@ public partial class ModEditWindow { ImGui.SameLine(); ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); } ImGui.TableNextColumn(); @@ -303,8 +325,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( intSize ); if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -314,8 +337,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -324,8 +348,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -335,8 +360,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -346,8 +372,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth( floatSize ); if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); @@ -358,14 +385,15 @@ public partial class ModEditWindow if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; + tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; + ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.TableNextColumn(); - ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); + ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); } else { @@ -376,7 +404,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + private bool DrawDyePreview( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) { var stain = _stainService.StainCombo.CurrentSelection.Key; if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) @@ -390,7 +418,9 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Apply the selected dye to this row.", disabled, true ); - ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + if (ret) + tab.UpdateColorSetRowPreview(rowIdx); ImGui.SameLine(); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs new file mode 100644 index 00000000..376e656f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Files; +using Penumbra.Interop.ResourceTree; +using Structs = Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private static unsafe Character* FindLocalPlayer(IObjectTable objects) + { + var localPlayer = objects[0]; + if (localPlayer is not Dalamud.Game.ClientState.Objects.Types.Character) + return null; + + return (Character*)localPlayer.Address; + } + + private static unsafe Character* FindSubActor(Character* character, int subActorType) + { + if (character == null) + return null; + + switch (subActorType) + { + case -1: + return character; + case 0: + return character->Mount.MountObject; + case 1: + var companion = character->Companion.CompanionObject; + if (companion == null) + return null; + return &companion->Character; + case 2: + var ornament = character->Ornament.OrnamentObject; + if (ornament == null) + return null; + return &ornament->Character; + default: + return null; + } + } + + private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath) + { + static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, string materialPath) + { + for (var i = 0; i < drawObject->SlotCount; ++i) + { + var model = drawObject->Models[i]; + if (model == null) + continue; + + for (var j = 0; j < model->MaterialCount; ++j) + { + var material = model->Materials[j]; + if (material == null) + continue; + + var mtrlHandle = material->MaterialResourceHandle; + if (mtrlHandle == null) + continue; + + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path.ToString() == materialPath) + result.Add((subActorType, childObjectIndex, i, j)); + } + } + } + + var result = new List<(int, int, int, int)>(); + + if (drawObject == null) + return result; + + materialPath = materialPath.Replace('/', '\\').ToLowerInvariant(); + + CollectMaterials(result, subActorType, -1, drawObject, materialPath); + + var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; + if (firstChildObject != null) + { + var childObject = firstChildObject; + var childObjectIndex = 0; + do + { + CollectMaterials(result, subActorType, childObjectIndex, childObject, materialPath); + + childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject; + ++childObjectIndex; + } + while (childObject != null && childObject != firstChildObject); + } + + return result; + } + + private static unsafe CharacterBase* GetChildObject(CharacterBase* drawObject, int index) + { + if (drawObject == null) + return null; + + if (index >= 0) + { + drawObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; + if (drawObject == null) + return null; + } + + var first = drawObject; + while (index-- > 0) + { + drawObject = (CharacterBase*)drawObject->DrawObject.Object.NextSiblingObject; + if (drawObject == null || drawObject == first) + return null; + } + + return drawObject; + } + + private static unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject, int modelSlot, int materialSlot) + { + if (drawObject == null) + return null; + + if (modelSlot < 0 || modelSlot >= drawObject->SlotCount) + return null; + + var model = drawObject->Models[modelSlot]; + if (model == null) + return null; + + if (materialSlot < 0 || materialSlot >= model->MaterialCount) + return null; + + return model->Materials[materialSlot]; + } + + private abstract unsafe class LiveMaterialPreviewerBase : IDisposable + { + private readonly IObjectTable _objects; + + protected readonly int SubActorType; + protected readonly int ChildObjectIndex; + protected readonly int ModelSlot; + protected readonly int MaterialSlot; + + protected readonly CharacterBase* DrawObject; + protected readonly Material* Material; + + protected bool Valid; + + public LiveMaterialPreviewerBase(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) + { + _objects = objects; + + SubActorType = subActorType; + ChildObjectIndex = childObjectIndex; + ModelSlot = modelSlot; + MaterialSlot = materialSlot; + + var localPlayer = FindLocalPlayer(objects); + if (localPlayer == null) + throw new InvalidOperationException("Cannot retrieve local player object"); + + var subActor = FindSubActor(localPlayer, subActorType); + if (subActor == null) + throw new InvalidOperationException("Cannot retrieve sub-actor (mount, companion or ornament)"); + + DrawObject = GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), childObjectIndex); + if (DrawObject == null) + throw new InvalidOperationException("Cannot retrieve draw object"); + + Material = GetDrawObjectMaterial(DrawObject, modelSlot, materialSlot); + if (Material == null) + throw new InvalidOperationException("Cannot retrieve material"); + + Valid = true; + } + + ~LiveMaterialPreviewerBase() + { + if (Valid) + Dispose(false, IsStillValid()); + } + + public void Dispose() + { + if (Valid) + Dispose(true, IsStillValid()); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing, bool reset) + { + Valid = false; + } + + public bool CheckValidity() + { + if (Valid && !IsStillValid()) + Dispose(false, false); + + return Valid; + } + + protected virtual bool IsStillValid() + { + var localPlayer = FindLocalPlayer(_objects); + if (localPlayer == null) + return false; + + var subActor = FindSubActor(localPlayer, SubActorType); + if (subActor == null) + return false; + + if (DrawObject != GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), ChildObjectIndex)) + return false; + + if (Material != GetDrawObjectMaterial(DrawObject, ModelSlot, MaterialSlot)) + return false; + + return true; + } + } + + private sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase + { + private readonly ShaderPackage* _shaderPackage; + + private readonly uint _originalShPkFlags; + private readonly float[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; + + public LiveMaterialPreviewer(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) + { + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); + + _shaderPackage = shpkHandle->ShaderPackage; + if (_shaderPackage == null) + throw new InvalidOperationException("Material doesn't have a shader package"); + + var material = (Structs.Material*)Material; + + _originalShPkFlags = material->ShaderPackageFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter = materialParameter.ToArray(); + else + _originalMaterialParameter = Array.Empty(); + + _originalSamplerFlags = new uint[material->TextureCount]; + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; + } + + protected override void Dispose(bool disposing, bool reset) + { + base.Dispose(disposing, reset); + + if (reset) + { + var material = (Structs.Material*)Material; + + material->ShaderPackageFlags = _originalShPkFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter.AsSpan().CopyTo(materialParameter); + + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + if (!CheckValidity()) + return; + + ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + if (!CheckValidity()) + return; + + var cbuffer = ((Structs.Material*)Material)->MaterialParameter; + if (cbuffer == null) + return; + + if (!cbuffer->TryGetBuffer(out var buffer)) + return; + + for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) + { + ref var parameter = ref _shaderPackage->MaterialElements[i]; + if (parameter.CRC == parameterCrc) + { + if ((parameter.Offset & 0x3) != 0 || (parameter.Size & 0x3) != 0 || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + return; + + value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + return; + } + } + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + if (!CheckValidity()) + return; + + var id = 0u; + var found = false; + + var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; + for (var i = 0; i < _shaderPackage->SamplerCount; ++i) + { + if (samplers[i].Crc == samplerCrc) + { + id = samplers[i].Id; + found = true; + break; + } + } + + if (!found) + return; + + var material = (Structs.Material*)Material; + for (var i = 0; i < material->TextureCount; ++i) + { + if (material->Textures[i].Id == id) + { + material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; + break; + } + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + return false; + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + return false; + + if (_shaderPackage != shpkHandle->ShaderPackage) + return false; + + return true; + } + } + + private sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase + { + public const int TextureWidth = 4; + public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; + public const int TextureLength = TextureWidth * TextureHeight * 4; + + private readonly Framework _framework; + + private readonly Texture** _colorSetTexture; + private readonly Texture* _originalColorSetTexture; + + private Half[] _colorSet; + private bool _updatePending; + + public Half[] ColorSet => _colorSet; + + public LiveColorSetPreviewer(IObjectTable objects, Framework framework, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) + { + _framework = framework; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + if (colorSetTextures == null) + throw new InvalidOperationException("Draw object doesn't have color set textures"); + + _colorSetTexture = colorSetTextures + (modelSlot * 4 + materialSlot); + + _originalColorSetTexture = *_colorSetTexture; + if (_originalColorSetTexture == null) + throw new InvalidOperationException("Material doesn't have a color set"); + Structs.TextureUtility.IncRef(_originalColorSetTexture); + + _colorSet = new Half[TextureLength]; + _updatePending = true; + + framework.Update += OnFrameworkUpdate; + } + + protected override void Dispose(bool disposing, bool reset) + { + _framework.Update -= OnFrameworkUpdate; + + base.Dispose(disposing, reset); + + if (reset) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); + Structs.TextureUtility.DecRef(oldTexture); + } + else + Structs.TextureUtility.DecRef(_originalColorSetTexture); + } + + public void ScheduleUpdate() + { + _updatePending = true; + } + + private void OnFrameworkUpdate(Framework _) + { + if (!_updatePending) + return; + _updatePending = false; + + if (!CheckValidity()) + return; + + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); + if (newTexture == null) + return; + + bool success; + lock (_colorSet) + fixed (Half* colorSet = _colorSet) + success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); + + if (success) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); + Structs.TextureUtility.DecRef(oldTexture); + } + else + Structs.TextureUtility.DecRef(newTexture); + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + if (colorSetTextures == null) + return false; + + if (_colorSetTexture != colorSetTextures + (ModelSlot * 4 + MaterialSlot)) + return false; + + return true; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 753ad8e9..9cff681a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -2,15 +2,20 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; -using Penumbra.Services; +using Penumbra.GameData.Structs; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -19,10 +24,12 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private sealed class MtrlTab : IWritable + private sealed class MtrlTab : IWritable, IDisposable { private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; public uint NewKeyId; public uint NewKeyDefault; @@ -57,11 +64,17 @@ public partial class ModEditWindow public bool HasMalformedMaterialConstants; // Samplers - public readonly List< (string Label, string FileName) > Samplers = new(4); - public readonly List< (string Name, uint Id) > MissingSamplers = new(4); - public readonly HashSet< uint > DefinedSamplers = new(4); - public IndexSet OrphanedSamplers = new(0, false); - public int AliasedSamplerCount; + public readonly List< (string Label, string FileName, uint Id) > Samplers = new(4); + public readonly List< (string Name, uint Id) > MissingSamplers = new(4); + public readonly HashSet< uint > DefinedSamplers = new(4); + public IndexSet OrphanedSamplers = new(0, false); + public int AliasedSamplerCount; + + // Live-Previewers + public readonly List MaterialPreviewers = new(4); + public readonly List ColorSetPreviewers = new(4); + public int HighlightedColorSetRow = -1; + public int HighlightTime = -1; public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { @@ -243,7 +256,7 @@ public partial class ModEditWindow ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; - Samplers.Add( ( label, fileName ) ); + Samplers.Add( ( label, fileName, sampler.SamplerId ) ); } MissingSamplers.Clear(); @@ -269,6 +282,220 @@ public partial class ModEditWindow } } + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var localPlayer = FindLocalPlayer(_edit._dalamud.Objects); + if (null == localPlayer) + return; + + var drawObject = (CharacterBase*)localPlayer->GameObject.GetDrawObject(); + if (null == drawObject) + return; + + var instances = FindMaterial(drawObject, -1, FilePath); + + var drawObjects = stackalloc CharacterBase*[4]; + drawObjects[0] = drawObject; + + for (var i = 0; i < 3; ++i) + { + var subActor = FindSubActor(localPlayer, i); + if (null == subActor) + continue; + + var subDrawObject = (CharacterBase*)subActor->GameObject.GetDrawObject(); + if (null == subDrawObject) + continue; + + instances.AddRange(FindMaterial(subDrawObject, i, FilePath)); + drawObjects[i + 1] = subDrawObject; + } + + var foundMaterials = new HashSet(); + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + { + var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot); + if (foundMaterials.Contains((nint)material)) + continue; + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, materialSlot)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + + if (colorSet.HasValue) + { + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + { + try + { + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, childObjectIndex, modelSlot, materialSlot)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + UpdateColorSetPreview(); + } + } + + public void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorSetPreviewers) + previewer.Dispose(); + ColorSetPreviewers.Clear(); + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + public void HighlightColorSetRow(int rowIdx) + { + var oldRowIdx = HighlightedColorSetRow; + + HighlightedColorSetRow = rowIdx; + HighlightTime = (HighlightTime + 1) % 32; + + if (oldRowIdx >= 0) + UpdateColorSetRowPreview(oldRowIdx); + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public void CancelColorSetHighlight() + { + var rowIdx = HighlightedColorSetRow; + + HighlightedColorSetRow = -1; + HighlightTime = -1; + + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public unsafe void UpdateColorSetRowPreview(int rowIdx) + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var row = colorSet.Rows[rowIdx]; + if (maybeColorDyeSet.HasValue) + { + var stm = _edit._stainService.StmFile; + var dye = maybeColorDyeSet.Value.Rows[rowIdx]; + if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) + ApplyDye(ref row, dye, dyes); + } + + if (HighlightedColorSetRow == rowIdx) + ApplyHighlight(ref row, HighlightTime); + + foreach (var previewer in ColorSetPreviewers) + { + fixed (Half* pDest = previewer.ColorSet) + Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row)); + previewer.ScheduleUpdate(); + } + } + + public unsafe void UpdateColorSetPreview() + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var rows = colorSet.Rows; + if (maybeColorDyeSet.HasValue) + { + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + var colorDyeSet = maybeColorDyeSet.Value; + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + { + ref var row = ref rows[i]; + var dye = colorDyeSet.Rows[i]; + if (stm.TryGetValue(dye.Template, stainId, out var dyes)) + ApplyDye(ref row, dye, dyes); + } + } + + if (HighlightedColorSetRow >= 0) + ApplyHighlight(ref rows[HighlightedColorSetRow], HighlightTime); + + foreach (var previewer in ColorSetPreviewers) + { + fixed (Half* pDest = previewer.ColorSet) + Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray)); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes) + { + if (dye.Diffuse) + row.Diffuse = dyes.Diffuse; + if (dye.Specular) + row.Specular = dyes.Specular; + if (dye.SpecularStrength) + row.SpecularStrength = dyes.SpecularPower; + if (dye.Emissive) + row.Emissive = dyes.Emissive; + if (dye.Gloss) + row.GlossStrength = dyes.Gloss; + } + + private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) + { + var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; + var levelSq = (float)(level * level); + + row.Diffuse = Vector3.Zero; + row.Specular = Vector3.Zero; + row.Emissive = new Vector3(levelSq); + } + public void Update() { UpdateTextureLabels(); @@ -277,11 +504,31 @@ public partial class ModEditWindow UpdateSamplers(); } - public MtrlTab( ModEditWindow edit, MtrlFile file ) + public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) { - _edit = edit; - Mtrl = file; + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; LoadShpk( FindAssociatedShpk( out _, out _ ) ); + if (writable) + BindToMaterialInstances(); + } + + ~MtrlTab() + { + DoDispose(); + } + + public void Dispose() + { + DoDispose(); + GC.SuppressFinalize(this); + } + + private void DoDispose() + { + UnbindFromMaterialInstances(); } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 16ad708c..2d1859bd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -37,16 +37,17 @@ public partial class ModEditWindow return ret; } - private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled) + private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { var ret = false; - var shpkFlags = (int)file.ShaderPackage.Flags; + var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - file.ShaderPackage.Flags = (uint)shpkFlags; - ret = true; + tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + ret = true; + tab.SetShaderPackageFlags((uint)shpkFlags); } return ret; @@ -221,6 +222,7 @@ public partial class ModEditWindow { ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); } } } @@ -247,6 +249,7 @@ public partial class ModEditWindow ret = true; tab.UpdateConstantLabels(); + tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); } return ret; @@ -336,7 +339,7 @@ public partial class ModEditWindow private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) { - var (label, filename) = tab.Samplers[idx]; + var (label, filename, samplerCrc) = tab.Samplers[idx]; using var tree = ImRaii.TreeNode(label); if (!tree) return false; @@ -366,6 +369,7 @@ public partial class ModEditWindow { tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; ret = true; + tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); } if (!disabled @@ -410,9 +414,10 @@ public partial class ModEditWindow if (!ImGui.Button("Add Sampler")) return false; + var newSamplerId = tab.NewSamplerId; tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler { - SamplerId = tab.NewSamplerId, + SamplerId = newSamplerId, TextureIndex = (byte)tab.Mtrl.Textures.Length, Flags = 0, }); @@ -423,6 +428,7 @@ public partial class ModEditWindow }); tab.UpdateSamplers(); tab.UpdateTextureLabels(); + tab.SetSamplerFlags(newSamplerId, 0); return true; } @@ -467,7 +473,7 @@ public partial class ModEditWindow return ret; ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab.Mtrl, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); DrawCustomAssociations(tab); ret |= DrawMaterialShaderKeys(tab, disabled); DrawMaterialShaders(tab); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index d7e23ac3..e4de66a8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -15,13 +15,16 @@ public partial class ModEditWindow private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) { + DrawMaterialLivePreviewRebind( tab, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); var ret = DrawMaterialTextureChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled ); + ret |= DrawBackFaceAndTransparency( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled ); + ret |= DrawMaterialColorSetChange( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawMaterialShaderResources( tab, disabled ); @@ -32,6 +35,15 @@ public partial class ModEditWindow return !disabled && ret; } + private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) + { + if (disabled) + return; + + if (ImGui.Button("Reload live-preview")) + tab.BindToMaterialInstances(); + } + private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { var ret = false; @@ -62,7 +74,7 @@ public partial class ModEditWindow return ret; } - private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled ) + private static bool DrawBackFaceAndTransparency( MtrlTab tab, bool disabled ) { const uint transparencyBit = 0x10; const uint backfaceBit = 0x01; @@ -71,19 +83,21 @@ public partial class ModEditWindow using var dis = ImRaii.Disabled( disabled ); - var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0; + var tmp = ( tab.Mtrl.ShaderPackage.Flags & transparencyBit ) != 0; if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); - tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; + tmp = ( tab.Mtrl.ShaderPackage.Flags & backfaceBit ) != 0; if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 59cf8b80..a37363e3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -137,6 +137,9 @@ public partial class ModEditWindow : Window, IDisposable { _left.Dispose(); _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); } public override void Draw() @@ -541,12 +544,12 @@ public partial class ModEditWindow : Window, IDisposable _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlTab(this, new MtrlFile(bytes))); + (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", - () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkTab(_fileDialog, bytes)); + (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); @@ -557,6 +560,9 @@ public partial class ModEditWindow : Window, IDisposable { _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); _editor?.Dispose(); + _materialTab.Dispose(); + _modelTab.Dispose(); + _shaderPackageTab.Dispose(); _left.Dispose(); _right.Dispose(); _center.Dispose(); From b8d09ab6602d141b99b37e019da6ed50b54734dc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 24 Aug 2023 05:51:21 +0200 Subject: [PATCH 1105/2451] Material editor 2099 --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/Structs/CharacterBaseExt.cs | 15 + Penumbra/Interop/Structs/HumanExt.cs | 3 + .../ModEditWindow.Materials.ColorSet.cs | 101 ++- .../ModEditWindow.Materials.ConstantEditor.cs | 235 +++++++ .../ModEditWindow.Materials.LivePreview.cs | 10 +- .../ModEditWindow.Materials.MtrlTab.cs | 622 +++++++++++------ .../ModEditWindow.Materials.Shpk.cs | 654 ++++++++---------- .../AdvancedWindow/ModEditWindow.Materials.cs | 90 ++- .../ModEditWindow.ShaderPackages.cs | 88 ++- .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 20 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 16 +- 15 files changed, 1221 insertions(+), 651 deletions(-) create mode 100644 Penumbra/Interop/Structs/CharacterBaseExt.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs diff --git a/OtterGui b/OtterGui index 863d08bd..1e172ee9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f +Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f diff --git a/Penumbra.GameData b/Penumbra.GameData index 97643cad..07c001c5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf +Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 6dc005ee..d14bd68b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -190,7 +190,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (WithNames) { - var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null; + var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs new file mode 100644 index 00000000..3bbbeca9 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterBaseExt +{ + [FieldOffset( 0x0 )] + public CharacterBase CharacterBase; + + [FieldOffset( 0x258 )] + public Texture** ColorSetTextures; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 7af5cee4..33d83b06 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -9,6 +9,9 @@ public unsafe struct HumanExt [FieldOffset( 0x0 )] public Human Human; + [FieldOffset( 0x0 )] + public CharacterBaseExt CharacterBase; + [FieldOffset( 0x9E8 )] public ResourceHandle* Decal; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 1d6c480a..798939ca 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,22 +13,44 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) { - if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) + if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) { return false; } + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + return false; + } + + var hasAnyDye = tab.UseColorDyeSet; + ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( tab, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( tab, disabled ); + var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled ); + if( !disabled ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye ); + } + if( hasAnyDye ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= DrawPreviewDye( tab, disabled ); + } - using var table = ImRaii.Table( "##ColorSets", 11, + using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); if( !table ) { @@ -53,17 +75,20 @@ public partial class ModEditWindow ImGui.TableHeader( "Repeat" ); ImGui.TableNextColumn(); ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); + if( hasAnyDye ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye Preview"); + } for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) { using var _ = ImRaii.PushId( j ); for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= DrawColorSetRow( tab, j, i, disabled ); + ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye ); ImGui.TableNextRow(); } } @@ -122,9 +147,9 @@ public partial class ModEditWindow return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled ) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) + if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } @@ -187,6 +212,21 @@ public partial class ModEditWindow } } + private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) + { + var ret = ImGui.Checkbox( "Dyeable", ref dyeable ); + + if( ret ) + { + tab.UseColorDyeSet = dyeable; + if( dyeable ) + tab.Mtrl.FindOrAddColorDyeSet(); + tab.UpdateColorSetPreview(); + } + + return ret; + } + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -235,7 +275,7 @@ public partial class ModEditWindow tab.CancelColorSetHighlight(); } - private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) + private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye ) { static bool FixFloat( ref float val, float current ) { @@ -245,7 +285,7 @@ public partial class ModEditWindow using var id = ImRaii.PushId( rowIdx ); var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; @@ -274,7 +314,7 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -305,9 +345,9 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -323,9 +363,9 @@ public partial class ModEditWindow ImGui.TableNextColumn(); int tmpInt = row.TileSet; ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -335,7 +375,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; @@ -346,7 +386,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; @@ -358,7 +398,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; @@ -370,7 +410,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; @@ -379,10 +419,10 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.TableNextColumn(); if( hasDye ) { - if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.TableNextColumn(); + if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; @@ -395,9 +435,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); } - else + else if ( hasAnyDye ) { ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } @@ -431,10 +472,10 @@ public partial class ModEditWindow ImGui.SameLine(); using var dis = ImRaii.Disabled(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" ); ImGui.SameLine(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" ); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs new file mode 100644 index 00000000..5616425c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.GameData; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private interface IConstantEditor + { + bool Draw(Span values, bool disabled, float editorWidth); + } + + private sealed class FloatConstantEditor : IConstantEditor + { + public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); + + private readonly float? _minimum; + private readonly float? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = $"%.{Math.Min(precision, (byte)9)}f"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (values[valueIdx] - _bias) / _factor; + if (disabled) + ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private float Clamp(float value) + => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); + } + + private sealed class IntConstantEditor : IConstantEditor + { + private readonly int? _minimum; + private readonly int? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = "%d"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); + if (disabled) + ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private int Clamp(int value) + => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); + } + + private sealed class ColorConstantEditor : IConstantEditor + { + private readonly bool _squaredRgb; + private readonly bool _clamped; + + public ColorConstantEditor(bool squaredRgb, bool clamped) + { + _squaredRgb = squaredRgb; + _clamped = clamped; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + if (values.Length == 3) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = Vector3.SquareRoot(value); + if (ImGui.ColorEdit3("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= value; + if (_clamped) + value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); + value.CopyTo(values); + return true; + } + + return false; + } + else if (values.Length == 4) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); + if (ImGui.ColorEdit4("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= new Vector4(value.X, value.Y, value.Z, 1.0f); + if (_clamped) + value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); + value.CopyTo(values); + return true; + } + + return false; + } + else + return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); + } + } + + private sealed class EnumConstantEditor : IConstantEditor + { + private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; + + public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) + { + _values = values; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var currentValue = values[valueIdx]; + var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty); + if (disabled) + ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly); + else + { + using var c = ImRaii.Combo($"##{valueIdx}", currentLabel); + { + if (c) + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + values[valueIdx] = value; + ret = true; + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + } + } + + return ret; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 376e656f..7d400a71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -398,7 +398,7 @@ public partial class ModEditWindow if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color set textures"); @@ -424,7 +424,8 @@ public partial class ModEditWindow if (reset) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(_originalColorSetTexture); @@ -460,7 +461,8 @@ public partial class ModEditWindow if (success) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(newTexture); @@ -471,7 +473,7 @@ public partial class ModEditWindow if (!base.IsStillValid()) return false; - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9cff681a..17f34b64 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -16,6 +17,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -26,49 +28,47 @@ public partial class ModEditWindow { private sealed class MtrlTab : IWritable, IDisposable { + private const int ShpkPrefixLength = 16; + + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; public readonly string FilePath; public readonly bool Writable; - public uint NewKeyId; - public uint NewKeyDefault; - public uint NewConstantId; - public int NewConstantIdx; - public uint NewSamplerId; - public int NewSamplerIdx; + private string[]? _shpkNames; + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public JObject? AssociatedShpkDevkit; - public ShpkFile? AssociatedShpk; - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public float TextureLabelWidth; + public readonly string LoadedBaseDevkitPathName = string.Empty; + public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< string > ShaderKeyLabels = new(16); - public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); - public readonly List< int > MissingShaderKeyIndices = new(16); - public readonly List< uint > AvailableKeyValues = new(16); - public string VertexShaders = "Vertex Shaders: ???"; - public string PixelShaders = "Pixel Shaders: ???"; + public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16); + + public readonly HashSet< int > VertexShaders = new(16); + public readonly HashSet< int > PixelShaders = new(16); + public bool ShadersKnown = false; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; + + // Textures & Samplers + public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4); + + public readonly HashSet< int > UnfoldedTextures = new(4); + public readonly HashSet< uint > SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); - public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); - public readonly HashSet< uint > DefinedMaterialConstants = new(16); - - public string MaterialConstantLabel = "Constants###Constants"; - public IndexSet OrphanedMaterialValues = new(0, false); - public int AliasedMaterialValueCount; - public bool HasMalformedMaterialConstants; - - // Samplers - public readonly List< (string Label, string FileName, uint Id) > Samplers = new(4); - public readonly List< (string Name, uint Id) > MissingSamplers = new(4); - public readonly HashSet< uint > DefinedSamplers = new(4); - public IndexSet OrphanedSamplers = new(0, false); - public int AliasedSamplerCount; + public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16); // Live-Previewers public readonly List MaterialPreviewers = new(4); @@ -87,8 +87,24 @@ public partial class ModEditWindow return _edit.FindBestMatch( defaultGamePath ); } + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + public void LoadShpk( FullPath path ) { + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + try { LoadedShpkPath = path; @@ -106,180 +122,314 @@ public partial class ModEditWindow Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } + if( LoadedShpkPath.InternalName.IsEmpty ) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName ); + + UpdateShaderKeys(); Update(); } - public void UpdateTextureLabels() + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) { - var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); - TextureLabels.Clear(); - TextureLabelWidth = 50f * UiHelpers.Scale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + try { - for( var i = 0; i < Mtrl.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - TextureLabels.Add( name ); - TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); - } - } + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } } - public void UpdateShaderKeyLabels() + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class { - ShaderKeyLabels.Clear(); - DefinedShaderKeys.Clear(); - foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) - { - ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); - DefinedShaderKeys.Add( key.Category, key.Value ); - } + return TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + } - MissingShaderKeyIndices.Clear(); - AvailableKeyValues.Clear(); - var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); - var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); - if( AssociatedShpk != null ) - { - MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; - if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) ) + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) { - var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; - NewKeyId = key.Id; - NewKeyDefault = key.DefaultValue; + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; } - AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); - foreach( var node in AssociatedShpk.Nodes ) + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + public void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + { + foreach (var key in AssociatedShpk.MaterialKeys) { - if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => { - foreach( var pass in node.Passes ) + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); + else + return ($"0x{value:X8}", value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + return x.Label.CompareTo(y.Label); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); + } + } + else + { + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); + } + } + + public void UpdateShaders() + { + VertexShaders.Clear(); + PixelShaders.Clear(); + if (AssociatedShpk == null) + ShadersKnown = false; + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + { + foreach (var pass in node.Value.Passes) + { + VertexShaders.Add((int)pass.VertexShader); + PixelShaders.Add((int)pass.PixelShader); + } + } + else + ShadersKnown = false; } } } } - VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; - PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; + var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); + var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); + + VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } - public void UpdateConstantLabels() + public void UpdateTextures() { - var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; - MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; - - DefinedMaterialConstants.Clear(); - MaterialConstants.Clear(); - HasMalformedMaterialConstants = false; - AliasedMaterialValueCount = 0; - OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true ); - foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() ) + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) { - DefinedMaterialConstants.Add( constant.Id ); - var values = Mtrl.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) { - var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam?.ByteOffset ?? -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - - var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - AliasedMaterialValueCount += values.Length - unique; + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); } - else + foreach (var samplerId in SamplerIds) { - HasMalformedMaterialConstants = true; + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); } + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.FindOrAddColorSet(); + } + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); - var label = name == null - ? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}" - : $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}"; + TextureLabelWidth = 50f * UiHelpers.Scale; - MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) ); + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + + foreach (var (label, _, _, description, monoFont) in Textures) + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); } - MissingMaterialConstants.Clear(); - if( AssociatedShpk != null ) - { - var setIdx = false; - foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) ) - { - var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); - var label = name == null - ? $"(ID: 0x{param.Id:X8})" - : $"{name} (ID: 0x{param.Id:X8})"; - if( NewConstantId == param.Id ) - { - setIdx = true; - NewConstantIdx = MissingMaterialConstants.Count; - } - - MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); - } - - if( !setIdx && MissingMaterialConstants.Count > 0 ) - { - NewConstantIdx = 0; - NewConstantId = MissingMaterialConstants[ 0 ].Id; - } - } + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateSamplers() + public void UpdateConstants() { - Samplers.Clear(); - DefinedSamplers.Clear(); - OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true ); - foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() ) + static List FindOrAddGroup(List<(string, List)> groups, string name) { - DefinedSamplers.Add( sampler.SamplerId ); - if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) - { - ++AliasedSamplerCount; - } + foreach (var (groupName, group) in groups) + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; - var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - var label = shpk.HasValue - ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" - : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; - var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; - Samplers.Add( ( label, fileName, sampler.SamplerId ) ); + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; } - MissingSamplers.Clear(); - if( AssociatedShpk != null ) + Constants.Clear(); + if (AssociatedShpk == null) { - var setSampler = false; - foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) { - if( sampler.Id == NewSamplerId ) + var values = Mtrl.GetConstantValues(constant); + for (var i = 0; i < values.Length; i += 4) + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + if ((shpkConstant.ByteSize & 0x3) != 0) + continue; + + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) { - setSampler = true; - NewSamplerIdx = MissingSamplers.Count; + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.Offset; + var length = values.Length - offset; + if (dkConstant.Length.HasValue) + length = Math.Min(length, (int)dkConstant.Length.Value); + if (length <= 0) + continue; + var editor = dkConstant.CreateEditor(); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } } - MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); - } - - if( !setSampler && MissingSamplers.Count > 0 ) - { - NewSamplerIdx = 0; - NewSamplerId = MissingSamplers[ 0 ].Id; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(true)) + { + if ((shpkConstant.ByteOffset & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset >> 2; + for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 4, end); + if (rangeEnd > rangeStart) + fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + for (var i = start; i < end; i += 4) + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default)); + } + } } } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme + foreach (var (_, group) in Constants) + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); } public unsafe void BindToMaterialInstances() @@ -329,6 +479,7 @@ public partial class ModEditWindow // Carry on without that previewer. } } + UpdateMaterialPreview(); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); @@ -378,6 +529,19 @@ public partial class ModEditWindow previewer.SetSamplerFlags(samplerCrc, samplerFlags); } + public void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValues(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + public void HighlightColorSetRow(int rowIdx) { var oldRowIdx = HighlightedColorSetRow; @@ -402,7 +566,7 @@ public partial class ModEditWindow UpdateColorSetRowPreview(rowIdx); } - public unsafe void UpdateColorSetRowPreview(int rowIdx) + public void UpdateColorSetRowPreview(int rowIdx) { if (ColorSetPreviewers.Count == 0) return; @@ -415,12 +579,12 @@ public partial class ModEditWindow var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var row = colorSet.Rows[rowIdx]; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { var stm = _edit._stainService.StmFile; var dye = maybeColorDyeSet.Value.Rows[rowIdx]; if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - ApplyDye(ref row, dye, dyes); + row.ApplyDyeTemplate(dye, dyes); } if (HighlightedColorSetRow == rowIdx) @@ -428,13 +592,12 @@ public partial class ModEditWindow foreach (var previewer in ColorSetPreviewers) { - fixed (Half* pDest = previewer.ColorSet) - Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row)); + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } - public unsafe void UpdateColorSetPreview() + public void UpdateColorSetPreview() { if (ColorSetPreviewers.Count == 0) return; @@ -447,7 +610,7 @@ public partial class ModEditWindow var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var rows = colorSet.Rows; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; @@ -457,7 +620,7 @@ public partial class ModEditWindow ref var row = ref rows[i]; var dye = colorDyeSet.Rows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - ApplyDye(ref row, dye, dyes); + row.ApplyDyeTemplate(dye, dyes); } } @@ -466,26 +629,11 @@ public partial class ModEditWindow foreach (var previewer in ColorSetPreviewers) { - fixed (Half* pDest = previewer.ColorSet) - Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray)); + rows.AsHalves().CopyTo(previewer.ColorSet); previewer.ScheduleUpdate(); } } - private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes) - { - if (dye.Diffuse) - row.Diffuse = dyes.Diffuse; - if (dye.Specular) - row.Specular = dyes.Specular; - if (dye.SpecularStrength) - row.SpecularStrength = dyes.SpecularPower; - if (dye.Emissive) - row.Emissive = dyes.Emissive; - if (dye.Gloss) - row.GlossStrength = dyes.Gloss; - } - private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) { var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; @@ -498,18 +646,19 @@ public partial class ModEditWindow public void Update() { - UpdateTextureLabels(); - UpdateShaderKeyLabels(); - UpdateConstantLabels(); - UpdateSamplers(); + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); } public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); LoadShpk( FindAssociatedShpk( out _, out _ ) ); if (writable) BindToMaterialInstances(); @@ -532,9 +681,96 @@ public partial class ModEditWindow } public bool Valid - => Mtrl.Valid; + => ShadersKnown && Mtrl.Valid; public byte[] Write() - => Mtrl.Write(); + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet); + + return output.Write(); + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = new(); + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + Integer = 1, + Color = 2, + Enum = 3, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public float Value = 0.0f; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = Array.Empty(); + + public IConstantEditor? CreateEditor() + { + switch (Type) + { + case DevkitConstantType.Hidden: + return null; + case DevkitConstantType.Float: + return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit); + case DevkitConstantType.Integer: + return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit); + case DevkitConstantType.Color: + return new ColorConstantEditor(SquaredRgb, Clamped); + case DevkitConstantType.Enum: + return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description))); + default: + return FloatConstantEditor.Default; + } + } + + private int? ToInteger(float? value) + => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; + } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 2d1859bd..d3bc826a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +8,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; @@ -19,20 +21,92 @@ public partial class ModEditWindow { private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput(MtrlTab tab, bool disabled) + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + private static readonly IReadOnlyList StandardShaderPackages = new string[] { - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bg.shpk", + "bguvscroll.shpk", + "channeling.shpk", + "characterglass.shpk", + "character.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "grass.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private enum TextureAddressMode : uint + { + Wrap = 0, + Mirror = 1, + Clamp = 2, + Border = 3, + } + + private static readonly IReadOnlyList TextureAddressModeTooltips = new string[] + { + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", + "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", + }; + + private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) + { + if (disabled) { - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); + return false; } - if (ImGui.IsItemDeactivatedAfterEdit()) - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in tab.GetShpkNames()) + { + if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) + { + tab.Mtrl.ShaderPackage.Name = value; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + } + } return ret; } @@ -41,8 +115,8 @@ public partial class ModEditWindow { var ret = false; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; @@ -62,6 +136,12 @@ public partial class ModEditWindow var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; + var devkitText = tab.AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; + var baseDevkitText = tab.AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -70,6 +150,16 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Selectable(devkitText)) + ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + + if (ImGui.Selectable(baseDevkitText)) + ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { @@ -94,94 +184,50 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - - private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) - { - var ret = false; - using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); - if (!t2 || disabled) - return ret; - - var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - if (shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); - if (c) - foreach (var value in shpkKey.Value.Values) - { - if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) - { - tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - if (ImGui.Button("Remove Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - - private static bool DrawNewShaderKey(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - var ret = false; - using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) - { - if (c) - foreach (var idx in tab.MissingShaderKeyIndices) - { - var key = tab.AssociatedShpk!.MaterialKeys[idx]; - - if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) - { - tab.NewKeyDefault = key.DefaultValue; - tab.NewKeyId = key.Id; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey - { - Category = tab.NewKeyId, - Value = tab.NewKeyDefault, - }); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) - return false; - - using var t = ImRaii.TreeNode("Shader Keys"); - if (!t) + if (tab.ShaderKeys.Count == 0) return false; var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) - ret |= DrawShaderKey(tab, disabled, ref idx); + foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + { + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + tab.Update(); + } - if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) - ret |= DrawNewShaderKey(tab); + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription); + else + ImGui.TextUnformatted($"{label}: {currentLabel}"); + } return ret; } @@ -191,162 +237,64 @@ public partial class ModEditWindow if (tab.AssociatedShpk == null) return; - ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - } + ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) - { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name); - if (!t2) - return false; - - font.Dispose(); - - var constant = tab.Mtrl.ShaderPackage.Constants[idx]; - var ret = false; - var values = tab.Mtrl.GetConstantValues(constant); - if (values.Length > 0) + if (tab.ShaderComment.Length > 0) { - var valueOffset = constant.ByteOffset >> 2; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); - } - } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(tab.ShaderComment); } - else - { - ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (!disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button("Remove Constant")) - { - tab.Mtrl.ShaderPackage.ShaderValues = - tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) - tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); - } - - return ret; - } - - private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) - { - using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); - if (!t2) - return false; - - var ret = false; - foreach (var idx in tab.OrphanedMaterialValues) - { - ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); - if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - } - } - - return ret; - } - - private static bool DrawNewMaterialParam(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - { - using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); - if (c) - foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) - { - if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) - { - tab.NewConstantIdx = idx; - tab.NewConstantId = constant.Id; - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Constant")) - { - var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant - { - Id = tab.NewConstantId, - ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), - ByteSize = byteSize, - }); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); - tab.UpdateConstantLabels(); - return true; - } - - return false; } private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) + if (tab.Constants.Count == 0) return false; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); - if (!t) + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) return false; - font.Dispose(); + using var _ = ImRaii.PushId("MaterialConstants"); + var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) - ret |= DrawMaterialConstantValues(tab, disabled, ref idx); + foreach (var (header, group) in tab.Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; - if (tab.OrphanedMaterialValues.Count > 0) - ret |= DrawMaterialOrphans(tab, disabled); - else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) - ret |= DrawNewMaterialParam(tab); + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = tab.Mtrl.GetConstantValues(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + if (editor.Draw(buffer[slice], disabled, 250.0f)) + { + ret = true; + tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } return ret; } - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) { - var (label, filename, samplerCrc) = tab.Samplers[idx]; - using var tree = ImRaii.TreeNode(label); - if (!tree) - return false; - - ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); - var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; + var ret = false; + ref var texture = ref tab.Mtrl.Textures[textureIdx]; + ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; // FIXME this probably doesn't belong here static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) @@ -357,128 +305,123 @@ public partial class ModEditWindow } } - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) + { + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == current)) + { + samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); + ret = true; + } + + ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); + } + return ret; + } + + var dx11 = texture.DX11; + if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = (int)((sampler.Flags >> 20) & 0xF); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + + using var t = ImRaii.TreeNode("Advanced Settings"); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (InputHexUInt16("Texture Flags", ref texture.Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); - } - - if (!disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button("Remove Sampler")) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; - } - - ret = true; - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); + sampler.Flags = (uint)samplerFlags; + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); } return ret; } - private static bool DrawMaterialNewSampler(MtrlTab tab) - { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) - { - if (c) - foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) - { - if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) - { - tab.NewSamplerIdx = idx; - tab.NewSamplerId = sampler.Id; - } - } - } - - ImGui.SameLine(); - if (!ImGui.Button("Add Sampler")) - return false; - - var newSamplerId = tab.NewSamplerId; - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler - { - SamplerId = newSamplerId, - TextureIndex = (byte)tab.Mtrl.Textures.Length, - Flags = 0, - }); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - }); - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); - tab.SetSamplerFlags(newSamplerId, 0); - return true; - } - - private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) - { - if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) - return false; - - using var t = ImRaii.TreeNode("Samplers"); - if (!t) - return false; - - var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) - ret |= DrawMaterialSampler(tab, disabled, ref idx); - - if (tab.OrphanedSamplers.Count > 0) - { - using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); - if (t2) - foreach (var idx in tab.OrphanedSamplers) - { - ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", - ImGuiTreeNodeFlags.Leaf) - .Dispose(); - } - } - else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) - { - ret |= DrawMaterialNewSampler(tab); - } - - return ret; - } - - private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) + private bool DrawMaterialShader(MtrlTab tab, bool disabled) { var ret = false; - if (!ImGui.CollapsingHeader("Advanced Shader Resources")) - return ret; + if (ImGui.CollapsingHeader(tab.ShaderHeader)) + { + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); + } + + if (tab.AssociatedShpkDevkit == null) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + GC.KeepAlive(tab); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted(tab.AssociatedShpk == null + ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." + : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); + } - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - ret |= DrawMaterialConstants(tab, disabled); - ret |= DrawMaterialSamplers(tab, disabled); return ret; } @@ -500,26 +443,25 @@ public partial class ModEditWindow _ => null, }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz ", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw ", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - if (valueLength == 0 || valueOffset < 0) return (null, false); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index e4de66a8..b89bab01 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -18,16 +18,14 @@ public partial class ModEditWindow DrawMaterialLivePreviewRebind( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - var ret = DrawMaterialTextureChange( tab, disabled ); + var ret = DrawBackFaceAndTransparency( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab, disabled ); + ret |= DrawMaterialShader( tab, disabled ); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawMaterialTextureChange( tab, disabled ); ret |= DrawMaterialColorSetChange( tab, disabled ); - - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( tab, disabled ); + ret |= DrawMaterialConstants( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); DrawOtherMaterialDetails( tab.Mtrl, disabled ); @@ -40,35 +38,87 @@ public partial class ModEditWindow if (disabled) return; - if (ImGui.Button("Reload live-preview")) + if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); + + if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0) + { + ImGui.SameLine(); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information."); + } } private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { - var ret = false; - using var table = ImRaii.Table( "##Textures", 2 ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) + if( tab.Textures.Count == 0 ) { - using var _ = ImRaii.PushId( i ); - var tmp = tab.Mtrl.Textures[ i ].Path; + return false; + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + return false; + } + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table( "##Textures", 3 ); + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight ); + ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); + for( var i = 0; i < tab.Textures.Count; ++i ) + { + var (label, textureI, samplerI, description, monoFont) = tab.Textures[i]; + + using var _ = ImRaii.PushId( samplerI ); + var tmp = tab.Mtrl.Textures[ textureI ].Path; + var unfolded = tab.UnfoldedTextures.Contains( samplerI ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ), + "Settings for this texture and the associated sampler", false, true ) ) + { + unfolded = !unfolded; + if( unfolded ) + tab.UnfoldedTextures.Add( samplerI ); + else + tab.UnfoldedTextures.Remove( samplerI ); + } ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ i ].Path ) + && tmp != tab.Mtrl.Textures[ textureI ].Path ) { - ret = true; - tab.Mtrl.Textures[ i ].Path = tmp; + ret = true; + tab.Mtrl.Textures[ textureI ].Path = tmp; } ImGui.TableNextColumn(); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( tab.TextureLabels[ i ] ); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) ) + { + ImGui.AlignTextToFramePadding(); + if( description.Length > 0 ) + ImGuiUtil.LabeledHelpMarker( label, description ); + else + ImGui.TextUnformatted( label ); + } + + if( unfolded ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI ); + ImGui.TableNextColumn(); + } } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 4e8a4f45..1b159efc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -14,7 +14,6 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String; -using Penumbra.UI.AdvancedWindow; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -40,7 +39,13 @@ public partial class ModEditWindow ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherShaderPackageDetails( file, disabled ); + ret |= DrawShaderPackageResources( file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawShaderPackageSelection( file ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawOtherShaderPackageDetails( file ); file.FileDialog.Draw(); @@ -50,7 +55,18 @@ public partial class ModEditWindow } private static void DrawShaderPackageSummary( ShpkTab tab ) - => ImGui.TextUnformatted( tab.Header ); + { + ImGui.TextUnformatted( tab.Header ); + if( !tab.Shpk.Disassembled ) + { + var textColor = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + + using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning ); + + ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." ); + } + } private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) { @@ -163,7 +179,7 @@ public partial class ModEditWindow } DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled ) + if( !disabled && tab.Shpk.Disassembled ) { ImGui.SameLine(); DrawShaderImportButton( tab, objectName, shaders, idx ); @@ -182,7 +198,8 @@ public partial class ModEditWindow } } - DrawRawDisassembly( shader ); + if( tab.Shpk.Disassembled ) + DrawRawDisassembly( shader ); } return ret; @@ -276,7 +293,9 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) { - ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + ImGui.TextUnformatted( tab.Shpk.Disassembled + ? "Parameter positions (continuations are grayed out, unused values are red):" + : "Parameter positions (continuations are grayed out):" ); using var table = ImRaii.Table( "##MaterialParamLayout", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); @@ -471,6 +490,22 @@ public partial class ModEditWindow return ret; } + private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled ) + { + var ret = false; + + if( !ImGui.CollapsingHeader( "Shader Resources" ) ) + { + return false; + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); + + return ret; + } + private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) { if( keys.Count == 0 ) @@ -513,7 +548,7 @@ public partial class ModEditWindow foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) { using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); + using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" ); if( !t2 ) { continue; @@ -549,39 +584,38 @@ public partial class ModEditWindow } } - private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) + private static void DrawShaderPackageSelection( ShpkTab tab ) { - var ret = false; - - if( !ImGui.CollapsingHeader( "Further Content" ) ) + if( !ImGui.CollapsingHeader( "Shader Selection" ) ) { - return false; + return; } - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); DrawShaderPackageNodes( tab ); - if( tab.Shpk.Items.Length > 0 ) + using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" ); + if( t ) { - using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); - if( t ) + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var selector in tab.Shpk.NodeSelectors ) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } } + } + + private static void DrawOtherShaderPackageDetails( ShpkTab tab ) + { + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return; + } + + ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); if( tab.Shpk.AdditionalData.Length > 0 ) { @@ -591,8 +625,6 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } - - return ret; } private static string UsedComponentString( bool withSize, in Resource resource ) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 1720ec8c..2df52130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -27,8 +27,16 @@ public partial class ModEditWindow public ShpkTab(FileDialogService fileDialog, byte[] bytes) { FileDialog = fileDialog; - Shpk = new ShpkFile(bytes, true); - Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; + try + { + Shpk = new ShpkFile(bytes, true); + } + catch (NotImplementedException) + { + Shpk = new ShpkFile(bytes, false); + } + + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a37363e3..f1c78bf7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -21,6 +23,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; @@ -523,6 +526,23 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } + private HashSet FindPathsStartingWith(ByteString prefix) + { + var ret = new HashSet(); + + foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + if (_mod != null) + foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + foreach (var path in option.Files.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + return ret; + } + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 0378d620..ad0f2e40 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); - ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); - } - - if (hovered) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted(option.Description); - } - } + ImGuiUtil.SelectableHelpMarker(option.Description); id.Pop(); } From 9364ecccd22673bd54b3c63574992bdb81dba88b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 06:17:54 +0200 Subject: [PATCH 1106/2451] Material editor: better color constants --- OtterGui | 2 +- .../AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 1e172ee9..c8394607 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f +Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 5616425c..f7ea317d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -148,7 +148,7 @@ public partial class ModEditWindow var value = new Vector3(values); if (_squaredRgb) value = Vector3.SquareRoot(value); - if (ImGui.ColorEdit3("##0", ref value) && !disabled) + if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) value *= value; @@ -166,7 +166,7 @@ public partial class ModEditWindow var value = new Vector4(values); if (_squaredRgb) value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); - if (ImGui.ColorEdit4("##0", ref value) && !disabled) + if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) value *= new Vector4(value.X, value.Y, value.Z, 1.0f); From 42b874413df85787af1b2acf8834b1dc6caa5153 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 06:28:09 +0200 Subject: [PATCH 1107/2451] Add a few texture manipulation tools. --- OtterGui | 2 +- .../Textures/CombinedTexture.Manipulation.cs | 319 ++++++++++++++++-- Penumbra/Import/Textures/CombinedTexture.cs | 69 ++-- .../AdvancedWindow/ModEditWindow.Textures.cs | 100 +++--- 4 files changed, 381 insertions(+), 109 deletions(-) diff --git a/OtterGui b/OtterGui index 863d08bd..c8394607 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f +Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index a32b9578..f8608071 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -6,23 +6,110 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; +using Dalamud.Interface; +using Penumbra.UI; namespace Penumbra.Import.Textures; public partial class CombinedTexture { + private enum CombineOp + { + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + Over = 0, + Under = 1, + LeftMultiply = 2, + RightMultiply = 3, + CopyChannels = 4, + } + + [Flags] + private enum Channels + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; + private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; - private bool _invertLeft = false; - private bool _invertRight = false; + private Vector4 _constantRight = Vector4.Zero; private int _offsetX = 0; - private int _offsetY = 0; + private int _offsetY = 0; + private CombineOp _combineOp = CombineOp.Over; + private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; + + private static readonly IReadOnlyList CombineOpLabels = new string[] + { + "Overlay over Input", + "Input over Overlay", + "Ignore Overlay", + "Replace Input", + "Copy Channels", + }; + + private static readonly IReadOnlyList CombineOpTooltips = new string[] + { + "Standard composition.\nApply the overlay over the input.", + "Standard composition, reversed.\nApply the input over the overlay.", + "Use only the input, and ignore the overlay.", + "Completely replace the input with the overlay.", + "Replace some input channels with those from the overlay.\nUseful for Multi maps.", + }; + + private const float OneThird = 1.0f / 3.0f; + private const float RWeight = 0.2126f; + private const float GWeight = 0.7152f; + private const float BWeight = 0.0722f; + + private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = new (string, Matrix4x4, Vector4)[] + { + ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero), + ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), + ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), + ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), + ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), + ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply => (_multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero) ? CombineOp.LeftCopy : CombineOp.LeftMultiply, + CombineOp.RightMultiply => (_multiplierRight.IsIdentity && _constantRight == Vector4.Zero) ? CombineOp.RightCopy : CombineOp.RightMultiply, + _ => combineOp, + }; + } private Vector4 DataLeft( int offset ) - => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft ); + => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _constantLeft ); private Vector4 DataRight( int offset ) - => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); + => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); private Vector4 DataRight( int x, int y ) { @@ -34,7 +121,7 @@ public partial class CombinedTexture } var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); + return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); } private void AddPixelsMultiplied( int y, ParallelLoopState _ ) @@ -55,6 +142,43 @@ public partial class CombinedTexture } } + private void ReverseAddPixelsMultiplied( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); + var alpha = left.W + right.W * ( 1 - left.W ); + var rgba = alpha == 0 + ? new Rgba32() + : new Rgba32( ( ( left * left.W + right * right.W * ( 1 - left.W ) ) / alpha ) with { W = alpha } ); + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + } + } + + private void ChannelMergePixelsMultiplied( int y, ParallelLoopState _ ) + { + var channels = _copyChannels; + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); + var rgba = new Rgba32( ( channels & Channels.Red ) != 0 ? right.X : left.X, + ( channels & Channels.Green ) != 0 ? right.Y : left.Y, + ( channels & Channels.Blue ) != 0 ? right.Z : left.Z, + ( channels & Channels.Alpha ) != 0 ? right.W : left.W ); + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + } + } + private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) { for( var x = 0; x < _left.TextureWrap!.Width; ++x ) @@ -74,8 +198,8 @@ public partial class CombinedTexture for( var x = 0; x < _right.TextureWrap!.Width; ++x ) { var offset = ( _right.TextureWrap!.Width * y + x ) * 4; - var left = DataRight( offset ); - var rgba = new Rgba32( left ); + var right = DataRight( offset ); + var rgba = new Rgba32( right ); _centerStorage.RgbaPixels[ offset ] = rgba.R; _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; @@ -86,23 +210,25 @@ public partial class CombinedTexture private (int Width, int Height) CombineImage() { - var (width, height) = _left.IsLoaded + var combineOp = GetActualCombineOp(); + var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); _centerStorage.RgbaPixels = new byte[width * height * 4]; _centerStorage.Type = TextureType.Bitmap; - if( _left.IsLoaded ) + Parallel.For( 0, height, combineOp switch { - Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); - } - else - { - Parallel.For( 0, height, MultiplyPixelsRight ); - } + CombineOp.Over => AddPixelsMultiplied, + CombineOp.Under => ReverseAddPixelsMultiplied, + CombineOp.LeftMultiply => MultiplyPixelsLeft, + CombineOp.RightMultiply => MultiplyPixelsRight, + CombineOp.CopyChannels => ChannelMergePixelsMultiplied, + _ => throw new InvalidOperationException( $"Cannot combine images with operation {combineOp}" ), + } ); return ( width, height ); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) + private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, Vector4 constant ) { if( bytes.Count == 0 ) { @@ -110,11 +236,7 @@ public partial class CombinedTexture } 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 ); - } + var transformed = Vector4.Transform( rgba.ToVector4(), transform ) + constant; transformed.X = Math.Clamp( transformed.X, 0, 1 ); transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); @@ -138,8 +260,8 @@ public partial class CombinedTexture public void DrawMatrixInputLeft( float width ) { - var ret = DrawMatrixInput( ref _multiplierLeft, width ); - ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft ); + var ret = DrawMatrixInput( ref _multiplierLeft, ref _constantLeft, width ); + ret |= DrawMatrixTools( ref _multiplierLeft, ref _constantLeft ); if( ret ) { Update(); @@ -148,23 +270,56 @@ public partial class CombinedTexture public void DrawMatrixInputRight( float width ) { - var ret = DrawMatrixInput( ref _multiplierRight, width ); - ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); + var ret = DrawMatrixInput( ref _multiplierRight, ref _constantRight, width ); + ret |= DrawMatrixTools( ref _multiplierRight, ref _constantRight ); + ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); ret |= ImGui.IsItemDeactivatedAfterEdit(); ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); + ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SetNextItemWidth( 200.0f * UiHelpers.Scale ); + using( var c = ImRaii.Combo( "Combine Operation", CombineOpLabels[ (int)_combineOp ] ) ) + { + if( c ) + { + foreach( var op in Enum.GetValues() ) + { + if ( (int)op < 0 ) // Negative codes are for internal use only. + continue; + + if( ImGui.Selectable( CombineOpLabels[ (int)op ], op == _combineOp ) ) + { + _combineOp = op; + ret = true; + } + + ImGuiUtil.SelectableHelpMarker( CombineOpTooltips[ (int)op ] ); + } + } + } + using( var dis = ImRaii.Disabled( _combineOp != CombineOp.CopyChannels )) + { + ImGui.TextUnformatted( "Copy" ); + foreach( var channel in Enum.GetValues() ) + { + ImGui.SameLine(); + var copy = ( _copyChannels & channel ) != 0; + if( ImGui.Checkbox( channel.ToString(), ref copy ) ) + { + _copyChannels = copy ? ( _copyChannels | channel ) : ( _copyChannels & ~channel ); + ret = true; + } + } + } if( ret ) { Update(); } } - private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width ) + private static bool DrawMatrixInput( ref Matrix4x4 multiplier, ref Vector4 constant, float width ) { using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); if( !table ) @@ -217,6 +372,110 @@ public partial class CombinedTexture changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "1 " ); + changes |= DragFloat( "##1R", inputWidth, ref constant.X ); + changes |= DragFloat( "##1G", inputWidth, ref constant.Y ); + changes |= DragFloat( "##1B", inputWidth, ref constant.Z ); + changes |= DragFloat( "##1A", inputWidth, ref constant.W ); + return changes; } + + private static bool DrawMatrixTools( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + var changes = false; + + using( var combo = ImRaii.Combo( "Presets", string.Empty, ImGuiComboFlags.NoPreview ) ) + { + if( combo ) + { + foreach( var (label, preMultiplier, preConstant) in PredefinedColorTransforms ) + { + if( ImGui.Selectable( label, multiplier == preMultiplier && constant == preConstant ) ) + { + multiplier = preMultiplier; + constant = preConstant; + changes = true; + } + } + } + } + + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ImGui.TextUnformatted( "Invert" ); + ImGui.SameLine(); + if( ImGui.Button( "Colors" ) ) + { + InvertRed( ref multiplier, ref constant ); + InvertGreen( ref multiplier, ref constant ); + InvertBlue( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "R" ) ) + { + InvertRed( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "G" ) ) + { + InvertGreen( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "B" ) ) + { + InvertBlue( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "A" ) ) + { + InvertAlpha( ref multiplier, ref constant ); + changes = true; + } + + return changes; + } + + private static void InvertRed( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } } \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c26cb900..14a8a41c 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Numerics; using System.Threading.Tasks; @@ -68,6 +69,38 @@ public partial class CombinedTexture : IDisposable _current.TextureWrap!.Height); } + public void SaveAs(TextureType? texType, TextureManager textures, string path, TextureSaveType type, bool mipMaps) + { + TextureType finalTexType; + if (texType.HasValue) + finalTexType = texType.Value; + else + { + finalTexType = Path.GetExtension(path).ToLowerInvariant() switch + { + ".tex" => TextureType.Tex, + ".dds" => TextureType.Dds, + ".png" => TextureType.Png, + _ => TextureType.Unknown, + }; + } + + switch (finalTexType) + { + case TextureType.Tex: + SaveAsTex(textures, path, type, mipMaps); + break; + case TextureType.Dds: + SaveAsDds(textures, path, type, mipMaps); + break; + case TextureType.Png: + SaveAsPng(textures, path); + break; + default: + throw new ArgumentException($"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); + } + } + public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps) => SaveAs(textures, path, type, mipMaps, true); @@ -97,36 +130,22 @@ public partial class CombinedTexture : IDisposable public void Update() { Clean(); - if (_left.IsLoaded) + switch (GetActualCombineOp()) { - if (_right.IsLoaded) - { - _current = _centerStorage; - _mode = Mode.Custom; - } - else if (!_invertLeft && _multiplierLeft.IsIdentity) - { + case CombineOp.Invalid: + break; + case CombineOp.LeftCopy: _mode = Mode.LeftCopy; _current = _left; - } - else - { - _current = _centerStorage; - _mode = Mode.Custom; - } - } - else if (_right.IsLoaded) - { - if (!_invertRight && _multiplierRight.IsIdentity) - { - _current = _right; + break; + case CombineOp.RightCopy: _mode = Mode.RightCopy; - } - else - { - _current = _centerStorage; + _current = _right; + break; + default: _mode = Mode.Custom; - } + _current = _centerStorage; + break; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4d36ff8a..4cf3731d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -90,7 +90,7 @@ public partial class ModEditWindow if (ImGui.Selectable(newText, idx == _currentSaveAs)) _currentSaveAs = idx; - ImGuiUtil.HoverTooltip(newDesc); + ImGuiUtil.SelectableHelpMarker(newDesc); } } @@ -114,73 +114,65 @@ public partial class ModEditWindow SaveAsCombo(); ImGui.SameLine(); MipMapInput(); - if (ImGui.Button("Save as TEX", -Vector2.UnitX)) + + var canSaveInPlace = Path.IsPathRooted(_left.Path) && _left.Type is TextureType.Tex or TextureType.Dds or TextureType.Png; + + var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, + "This saves the texture in place. This is not revertible.", + !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { - var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => - { - if (a) - _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + AddReloadTask(_left.Path, false); } - if (ImGui.Button("Save as DDS", -Vector2.UnitX)) + ImGui.SameLine(); + if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => { if (a) - _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } ImGui.NewLine(); - if (ImGui.Button("Save as PNG", -Vector2.UnitX)) + var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy; + + var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); + if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3, + "This converts the texture to BC7 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => - { - if (a) - _center.SaveAsPng(_textures, b); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - if (_left.Type is TextureType.Tex && _center.IsLeftCopy) + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize3, + "This converts the texture to BC3 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { - var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); - if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, - "This converts the texture to BC7 format in place. This is not revertible.", - _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize, - "This converts the texture to BC3 format in place. This is not revertible.", - _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize, - "This converts the texture to RGBA format in place. This is not revertible.", - _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - else + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3, + "This converts the texture to RGBA format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { - ImGui.NewLine(); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } } @@ -212,17 +204,19 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } - private void AddReloadTask(string path) + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => { if (!t.IsCompletedSuccessfully) return; - if (_left.Path != path) + var tex = right ? _right : _left; + + if (tex.Path != path) return; - _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + _dalamud.Framework.RunOnFrameworkThread(() => tex.Reload(_textures)); }); } From afd7aab37dcb3a2b0e8b4991367c26ad38890ce3 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 17:46:53 +0200 Subject: [PATCH 1108/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07c001c5..1c68fd5e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616 +Subproject commit 1c68fd5efb23798d13154c1de0ad010db319abe2 From 87c5164367443dc43a9b1dc7ede61ca5b2c93f84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Aug 2023 17:56:48 +0200 Subject: [PATCH 1109/2451] Small cleanup, auto-formatting. --- .../Textures/CombinedTexture.Manipulation.cs | 380 +++++++++--------- Penumbra/Import/Textures/CombinedTexture.cs | 15 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 24 +- 3 files changed, 205 insertions(+), 214 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index f8608071..2af2a8e4 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -66,25 +66,28 @@ public partial class CombinedTexture private const float GWeight = 0.7152f; private const float BWeight = 0.0722f; - private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = new (string, Matrix4x4, Vector4)[] - { - ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero), - ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), - ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), - ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), - ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), - ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW), - }; + // @formatter:off + private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = + new[] + { + ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero ), + ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), + }; + // @formatter:on private CombineOp GetActualCombineOp() { var combineOp = (_left.IsLoaded, _right.IsLoaded) switch { - (true, true) => _combineOp, - (true, false) => CombineOp.LeftMultiply, + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, (false, true) => CombineOp.RightMultiply, (false, false) => CombineOp.Invalid, }; @@ -99,111 +102,109 @@ public partial class CombinedTexture return combineOp switch { - CombineOp.LeftMultiply => (_multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero) ? CombineOp.LeftCopy : CombineOp.LeftMultiply, - CombineOp.RightMultiply => (_multiplierRight.IsIdentity && _constantRight == Vector4.Zero) ? CombineOp.RightCopy : CombineOp.RightMultiply, - _ => combineOp, + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, }; } - private Vector4 DataLeft( int offset ) - => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _constantLeft ); + private Vector4 DataLeft(int offset) + => CappedVector(_left.RgbaPixels, offset, _multiplierLeft, _constantLeft); - private Vector4 DataRight( int offset ) - => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); + private Vector4 DataRight(int offset) + => CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); - private Vector4 DataRight( int x, int y ) + private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if( x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height ) - { + if (x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height) return Vector4.Zero; - } - var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); + var offset = (y * _right.TextureWrap!.Width + x) * 4; + return CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); } - private void AddPixelsMultiplied( int y, ParallelLoopState _ ) + private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = right.W + left.W * ( 1 - right.W ); + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = right.W + left.W * (1 - right.W); var rgba = alpha == 0 ? new Rgba32() - : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + : new Rgba32(((right * right.W + left * left.W * (1 - right.W)) / alpha) with { W = alpha }); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void ReverseAddPixelsMultiplied( int y, ParallelLoopState _ ) + private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = left.W + right.W * ( 1 - left.W ); + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = left.W + right.W * (1 - left.W); var rgba = alpha == 0 ? new Rgba32() - : new Rgba32( ( ( left * left.W + right * right.W * ( 1 - left.W ) ) / alpha ) with { W = alpha } ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + : new Rgba32(((left * left.W + right * right.W * (1 - left.W)) / alpha) with { W = alpha }); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void ChannelMergePixelsMultiplied( int y, ParallelLoopState _ ) + private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var rgba = new Rgba32( ( channels & Channels.Red ) != 0 ? right.X : left.X, - ( channels & Channels.Green ) != 0 ? right.Y : left.Y, - ( channels & Channels.Blue ) != 0 ? right.Z : left.Z, - ( channels & Channels.Alpha ) != 0 ? right.W : left.W ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, + (channels & Channels.Green) != 0 ? right.Y : left.Y, + (channels & Channels.Blue) != 0 ? right.Z : left.Z, + (channels & Channels.Alpha) != 0 ? right.W : left.W); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) + private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var rgba = new Rgba32( left ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var rgba = new Rgba32(left); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void MultiplyPixelsRight( int y, ParallelLoopState _ ) + private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for( var x = 0; x < _right.TextureWrap!.Width; ++x ) + for (var x = 0; x < _right.TextureWrap!.Width; ++x) { - var offset = ( _right.TextureWrap!.Width * y + x ) * 4; - var right = DataRight( offset ); - var rgba = new Rgba32( right ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_right.TextureWrap!.Width * y + x) * 4; + var right = DataRight(offset); + var rgba = new Rgba32(right); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } @@ -212,238 +213,231 @@ public partial class CombinedTexture { var combineOp = GetActualCombineOp(); var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply - ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) - : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); + ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) + : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); _centerStorage.RgbaPixels = new byte[width * height * 4]; _centerStorage.Type = TextureType.Bitmap; - Parallel.For( 0, height, combineOp switch + Parallel.For(0, height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, CombineOp.LeftMultiply => MultiplyPixelsLeft, CombineOp.RightMultiply => MultiplyPixelsRight, CombineOp.CopyChannels => ChannelMergePixelsMultiplied, - _ => throw new InvalidOperationException( $"Cannot combine images with operation {combineOp}" ), - } ); + _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), + }); - return ( width, height ); + return (width, height); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, Vector4 constant ) + + private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) { - if( bytes.Count == 0 ) - { + if (bytes.Count == 0) 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 ) + constant; + var rgba = new Rgba32(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]); + var transformed = Vector4.Transform(rgba.ToVector4(), transform) + constant; - 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 ); + 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 static bool DragFloat( string label, float width, ref float value ) + 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 ) ) - { + ImGui.SetNextItemWidth(width); + if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f)) value = tmp; - } return ImGui.IsItemDeactivatedAfterEdit(); } - public void DrawMatrixInputLeft( float width ) + public void DrawMatrixInputLeft(float width) { - var ret = DrawMatrixInput( ref _multiplierLeft, ref _constantLeft, width ); - ret |= DrawMatrixTools( ref _multiplierLeft, ref _constantLeft ); - if( ret ) - { + var ret = DrawMatrixInput(ref _multiplierLeft, ref _constantLeft, width); + ret |= DrawMatrixTools(ref _multiplierLeft, ref _constantLeft); + if (ret) Update(); - } } - public void DrawMatrixInputRight( float width ) + public void DrawMatrixInputRight(float width) { - var ret = DrawMatrixInput( ref _multiplierRight, ref _constantRight, width ); - ret |= DrawMatrixTools( ref _multiplierRight, ref _constantRight ); - ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); - ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); + var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); + ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); ImGui.SameLine(); - ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); - ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); - ImGui.SetNextItemWidth( 200.0f * UiHelpers.Scale ); - using( var c = ImRaii.Combo( "Combine Operation", CombineOpLabels[ (int)_combineOp ] ) ) + ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); + using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) { - if( c ) - { - foreach( var op in Enum.GetValues() ) + if (c) + foreach (var op in Enum.GetValues()) { - if ( (int)op < 0 ) // Negative codes are for internal use only. + if ((int)op < 0) // Negative codes are for internal use only. continue; - if( ImGui.Selectable( CombineOpLabels[ (int)op ], op == _combineOp ) ) + if (ImGui.Selectable(CombineOpLabels[(int)op], op == _combineOp)) { _combineOp = op; ret = true; } - ImGuiUtil.SelectableHelpMarker( CombineOpTooltips[ (int)op ] ); + ImGuiUtil.SelectableHelpMarker(CombineOpTooltips[(int)op]); } - } } - using( var dis = ImRaii.Disabled( _combineOp != CombineOp.CopyChannels )) + + using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) { - ImGui.TextUnformatted( "Copy" ); - foreach( var channel in Enum.GetValues() ) + ImGui.TextUnformatted("Copy"); + foreach (var channel in Enum.GetValues()) { ImGui.SameLine(); - var copy = ( _copyChannels & channel ) != 0; - if( ImGui.Checkbox( channel.ToString(), ref copy ) ) + var copy = (_copyChannels & channel) != 0; + if (ImGui.Checkbox(channel.ToString(), ref copy)) { - _copyChannels = copy ? ( _copyChannels | channel ) : ( _copyChannels & ~channel ); - ret = true; + _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; + ret = true; } } } - if( ret ) - { + + if (ret) Update(); - } } - private static bool DrawMatrixInput( ref Matrix4x4 multiplier, ref Vector4 constant, float width ) + private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + 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" ); + ImGuiUtil.Center("R"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "G" ); + ImGuiUtil.Center("G"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "B" ); + ImGuiUtil.Center("B"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "A" ); + ImGuiUtil.Center("A"); var inputWidth = width / 6; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "R " ); - changes |= DragFloat( "##RR", inputWidth, ref multiplier.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref multiplier.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref multiplier.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref multiplier.M14 ); + ImGui.Text("R "); + changes |= DragFloat("##RR", inputWidth, ref multiplier.M11); + changes |= DragFloat("##RG", inputWidth, ref multiplier.M12); + changes |= DragFloat("##RB", inputWidth, ref multiplier.M13); + changes |= DragFloat("##RA", inputWidth, ref multiplier.M14); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref multiplier.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref multiplier.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref multiplier.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref multiplier.M24 ); + ImGui.Text("G "); + changes |= DragFloat("##GR", inputWidth, ref multiplier.M21); + changes |= DragFloat("##GG", inputWidth, ref multiplier.M22); + changes |= DragFloat("##GB", inputWidth, ref multiplier.M23); + changes |= DragFloat("##GA", inputWidth, ref multiplier.M24); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref multiplier.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref multiplier.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref multiplier.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref multiplier.M34 ); + ImGui.Text("B "); + changes |= DragFloat("##BR", inputWidth, ref multiplier.M31); + changes |= DragFloat("##BG", inputWidth, ref multiplier.M32); + changes |= DragFloat("##BB", inputWidth, ref multiplier.M33); + changes |= DragFloat("##BA", inputWidth, ref multiplier.M34); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref multiplier.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref multiplier.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + ImGui.Text("A "); + changes |= DragFloat("##AR", inputWidth, ref multiplier.M41); + changes |= DragFloat("##AG", inputWidth, ref multiplier.M42); + changes |= DragFloat("##AB", inputWidth, ref multiplier.M43); + changes |= DragFloat("##AA", inputWidth, ref multiplier.M44); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "1 " ); - changes |= DragFloat( "##1R", inputWidth, ref constant.X ); - changes |= DragFloat( "##1G", inputWidth, ref constant.Y ); - changes |= DragFloat( "##1B", inputWidth, ref constant.Z ); - changes |= DragFloat( "##1A", inputWidth, ref constant.W ); + ImGui.Text("1 "); + changes |= DragFloat("##1R", inputWidth, ref constant.X); + changes |= DragFloat("##1G", inputWidth, ref constant.Y); + changes |= DragFloat("##1B", inputWidth, ref constant.Z); + changes |= DragFloat("##1A", inputWidth, ref constant.W); return changes; } - private static bool DrawMatrixTools( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static bool DrawMatrixTools(ref Matrix4x4 multiplier, ref Vector4 constant) { var changes = false; - using( var combo = ImRaii.Combo( "Presets", string.Empty, ImGuiComboFlags.NoPreview ) ) + using (var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview)) { - if( combo ) - { - foreach( var (label, preMultiplier, preConstant) in PredefinedColorTransforms ) + if (combo) + foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) { - if( ImGui.Selectable( label, multiplier == preMultiplier && constant == preConstant ) ) + if (ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) { multiplier = preMultiplier; constant = preConstant; changes = true; } } - } } ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted( "Invert" ); + ImGui.TextUnformatted("Invert"); ImGui.SameLine(); - if( ImGui.Button( "Colors" ) ) + if (ImGui.Button("Colors")) { - InvertRed( ref multiplier, ref constant ); - InvertGreen( ref multiplier, ref constant ); - InvertBlue( ref multiplier, ref constant ); + InvertRed(ref multiplier, ref constant); + InvertGreen(ref multiplier, ref constant); + InvertBlue(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "R" ) ) + if (ImGui.Button("R")) { - InvertRed( ref multiplier, ref constant ); + InvertRed(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "G" ) ) + if (ImGui.Button("G")) { - InvertGreen( ref multiplier, ref constant ); + InvertGreen(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "B" ) ) + if (ImGui.Button("B")) { - InvertBlue( ref multiplier, ref constant ); + InvertBlue(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "A" ) ) + if (ImGui.Button("A")) { - InvertAlpha( ref multiplier, ref constant ); + InvertAlpha(ref multiplier, ref constant); changes = true; } return changes; } - private static void InvertRed( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M11 = -multiplier.M11; multiplier.M21 = -multiplier.M21; @@ -452,7 +446,7 @@ public partial class CombinedTexture constant.X = 1.0f - constant.X; } - private static void InvertGreen( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M12 = -multiplier.M12; multiplier.M22 = -multiplier.M22; @@ -461,7 +455,7 @@ public partial class CombinedTexture constant.Y = 1.0f - constant.Y; } - private static void InvertBlue( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M13 = -multiplier.M13; multiplier.M23 = -multiplier.M23; @@ -470,7 +464,7 @@ public partial class CombinedTexture constant.Z = 1.0f - constant.Z; } - private static void InvertAlpha( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M14 = -multiplier.M14; multiplier.M24 = -multiplier.M24; @@ -478,4 +472,4 @@ public partial class CombinedTexture multiplier.M44 = -multiplier.M44; constant.W = 1.0f - constant.W; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 14a8a41c..b7e2a90a 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -71,19 +71,14 @@ public partial class CombinedTexture : IDisposable public void SaveAs(TextureType? texType, TextureManager textures, string path, TextureSaveType type, bool mipMaps) { - TextureType finalTexType; - if (texType.HasValue) - finalTexType = texType.Value; - else - { - finalTexType = Path.GetExtension(path).ToLowerInvariant() switch + var finalTexType = texType + ?? Path.GetExtension(path).ToLowerInvariant() switch { ".tex" => TextureType.Tex, ".dds" => TextureType.Dds, ".png" => TextureType.Png, _ => TextureType.Unknown, }; - } switch (finalTexType) { @@ -97,7 +92,8 @@ public partial class CombinedTexture : IDisposable SaveAsPng(textures, path); break; default: - throw new ArgumentException($"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); + throw new ArgumentException( + $"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); } } @@ -132,8 +128,7 @@ public partial class CombinedTexture : IDisposable Clean(); switch (GetActualCombineOp()) { - case CombineOp.Invalid: - break; + case CombineOp.Invalid: break; case CombineOp.LeftCopy: _mode = Mode.LeftCopy; _current = _left; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4cf3731d..af256f5c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -130,17 +130,18 @@ public partial class ModEditWindow if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => - { - if (a) + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", + (a, b) => { - _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - if (b == _left.Path) - AddReloadTask(_left.Path, false); - else if (b == _right.Path) - AddReloadTask(_right.Path, true); - } - }, _mod!.ModPath.FullName, _forceTextureStartPath); + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -169,7 +170,8 @@ public partial class ModEditWindow ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3, "This converts the texture to RGBA format in place. This is not revertible.", - !canConvertInPlace || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) + !canConvertInPlace + || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); AddReloadTask(_left.Path, false); From 781bbb3d26ebaa0400b92d7996a8c0563a1d8ccd Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 17:59:18 +0200 Subject: [PATCH 1110/2451] Textures: Un-merge save buttons, make ignore unselectable --- .../Textures/CombinedTexture.Manipulation.cs | 8 ++-- .../AdvancedWindow/ModEditWindow.Textures.cs | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index f8608071..3256836d 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -15,12 +15,12 @@ public partial class CombinedTexture { private enum CombineOp { + LeftMultiply = -4, LeftCopy = -3, RightCopy = -2, Invalid = -1, Over = 0, Under = 1, - LeftMultiply = 2, RightMultiply = 3, CopyChannels = 4, } @@ -47,7 +47,6 @@ public partial class CombinedTexture { "Overlay over Input", "Input over Overlay", - "Ignore Overlay", "Replace Input", "Copy Channels", }; @@ -55,9 +54,8 @@ public partial class CombinedTexture private static readonly IReadOnlyList CombineOpTooltips = new string[] { "Standard composition.\nApply the overlay over the input.", - "Standard composition, reversed.\nApply the input over the overlay.", - "Use only the input, and ignore the overlay.", - "Completely replace the input with the overlay.", + "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", + "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4cf3731d..3db2d407 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -127,22 +127,14 @@ public partial class ModEditWindow } ImGui.SameLine(); - if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) - { - var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => - { - if (a) - { - _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - if (b == _left.Path) - AddReloadTask(_left.Path, false); - else if (b == _right.Path) - AddReloadTask(_right.Path, true); - } - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; - } + if (ImGui.Button("Save as TEX", buttonSize2)) + OpenSaveAsDialog(".tex"); + + if (ImGui.Button("Export as PNG", buttonSize2)) + OpenSaveAsDialog(".png"); + ImGui.SameLine(); + if (ImGui.Button("Export as DDS", buttonSize2)) + OpenSaveAsDialog(".dds"); ImGui.NewLine(); @@ -204,6 +196,23 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } + private void OpenSaveAsDialog(string defaultExtension) + { + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, (a, b) => + { + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => From 792707a6e3237e55d909ca05e0e58a7c4d736743 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 18:24:21 +0200 Subject: [PATCH 1111/2451] Textures: Renumber CombineOps. Positive values in this enum also double as indices into the labels and tooltip arrays. (confirmed skill issue moment) --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index fed269da..057d0234 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -21,8 +21,8 @@ public partial class CombinedTexture Invalid = -1, Over = 0, Under = 1, - RightMultiply = 3, - CopyChannels = 4, + RightMultiply = 2, + CopyChannels = 3, } [Flags] From 99b43bf577d58744525264b9dcd3957cfb76dd5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 20:09:16 +0200 Subject: [PATCH 1112/2451] Textures: Automatic resizing --- .../Textures/CombinedTexture.Manipulation.cs | 131 ++++++++++++++---- 1 file changed, 105 insertions(+), 26 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 057d0234..b02ba7b7 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -8,6 +8,8 @@ using OtterGui; using SixLabors.ImageSharp.PixelFormats; using Dalamud.Interface; using Penumbra.UI; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace Penumbra.Import.Textures; @@ -25,6 +27,13 @@ public partial class CombinedTexture CopyChannels = 3, } + private enum ResizeOp + { + None = 0, + ToLeft = 1, + ToRight = 2, + } + [Flags] private enum Channels { @@ -41,8 +50,16 @@ public partial class CombinedTexture private int _offsetX = 0; private int _offsetY = 0; private CombineOp _combineOp = CombineOp.Over; + private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; + private int _rightWidth = 0; + private int _rightHeight = 0; + private int _targetWidth = 0; + private int _targetHeight = 0; + private byte[] _leftPixels = Array.Empty(); + private byte[] _rightPixels = Array.Empty(); + private static readonly IReadOnlyList CombineOpLabels = new string[] { "Overlay over Input", @@ -59,6 +76,26 @@ public partial class CombinedTexture "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; + private static (bool UsesLeft, bool UsesRight) GetCombineOpFlags(CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => (true, false), + CombineOp.LeftMultiply => (true, false), + CombineOp.RightCopy => (false, true), + CombineOp.RightMultiply => (false, true), + CombineOp.Over => (true, true), + CombineOp.Under => (true, true), + CombineOp.CopyChannels => (true, true), + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private static readonly IReadOnlyList ResizeOpLabels = new string[] + { + "No Resizing", + "Adjust Overlay to Input", + "Adjust Input to Overlay", + }; + private const float OneThird = 1.0f / 3.0f; private const float RWeight = 0.2126f; private const float GWeight = 0.7152f; @@ -107,27 +144,27 @@ public partial class CombinedTexture } private Vector4 DataLeft(int offset) - => CappedVector(_left.RgbaPixels, offset, _multiplierLeft, _constantLeft); + => CappedVector(_leftPixels, offset, _multiplierLeft, _constantLeft); private Vector4 DataRight(int offset) - => CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); + => CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if (x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height) + if (x < 0 || x >= _rightWidth || y < 0 || y >= _rightHeight) return Vector4.Zero; - var offset = (y * _right.TextureWrap!.Width + x) * 4; - return CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); + var offset = (y * _rightWidth + x) * 4; + return CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); } private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -143,9 +180,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -162,9 +199,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -180,9 +217,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -194,9 +231,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _right.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_right.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -206,26 +243,59 @@ public partial class CombinedTexture } } + private byte[] ResizePixels(byte[] rgbaPixels, int sourceWidth, int sourceHeight) + { + if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) + return rgbaPixels; + + byte[] resizedPixels; + using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) + { + image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight)); + + resizedPixels = new byte[_targetWidth * _targetHeight * 4]; + image.CopyPixelDataTo(resizedPixels); + } + + return resizedPixels; + } + private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply + var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); + var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; + (_targetWidth, _targetHeight) = usesLeft && resizeOp != ResizeOp.ToRight ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); - _centerStorage.RgbaPixels = new byte[width * height * 4]; + _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; _centerStorage.Type = TextureType.Bitmap; - Parallel.For(0, height, combineOp switch + try { - CombineOp.Over => AddPixelsMultiplied, - CombineOp.Under => ReverseAddPixelsMultiplied, - CombineOp.LeftMultiply => MultiplyPixelsLeft, - CombineOp.RightMultiply => MultiplyPixelsRight, - CombineOp.CopyChannels => ChannelMergePixelsMultiplied, - _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), - }); + if (usesLeft) + _leftPixels = (resizeOp == ResizeOp.ToRight) ? ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height) : _left.RgbaPixels; + if (usesRight) + (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) + ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) + : (_right.TextureWrap!.Width, _right.TextureWrap!.Height, _right.RgbaPixels); + Parallel.For(0, _targetHeight, combineOp switch + { + CombineOp.Over => AddPixelsMultiplied, + CombineOp.Under => ReverseAddPixelsMultiplied, + CombineOp.LeftMultiply => MultiplyPixelsLeft, + CombineOp.RightMultiply => MultiplyPixelsRight, + CombineOp.CopyChannels => ChannelMergePixelsMultiplied, + _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), + }); + } + finally + { + _leftPixels = Array.Empty(); + _rightPixels = Array.Empty(); + } - return (width, height); + return (_targetWidth, _targetHeight); } private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) @@ -266,6 +336,7 @@ public partial class CombinedTexture { var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); @@ -273,6 +344,7 @@ public partial class CombinedTexture ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) { @@ -292,6 +364,13 @@ public partial class CombinedTexture } } + var (usesLeft, usesRight) = GetCombineOpFlags(_combineOp); + using (var dis = ImRaii.Disabled(!usesLeft || !usesRight)) + { + ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, + Enum.GetValues(), op => ResizeOpLabels[(int)op]); + } + using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) { ImGui.TextUnformatted("Copy"); From ead88f9fa6313ea83082b9c51d9ad4d886f1fd61 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 27 Aug 2023 03:42:34 +0200 Subject: [PATCH 1113/2451] Skin Fixer (fixes modding of skin.shpk) --- Penumbra/Interop/Services/CharacterUtility.cs | 12 +- Penumbra/Interop/Services/SkinFixer.cs | 161 ++++++++++++++++++ .../Interop/Structs/CharacterUtilityData.cs | 8 +- Penumbra/Interop/Structs/MtrlResource.cs | 22 ++- Penumbra/Interop/Structs/ResourceHandle.cs | 14 +- Penumbra/Penumbra.cs | 5 +- Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/DebugTab.cs | 107 +++++++++--- 8 files changed, 302 insertions(+), 30 deletions(-) create mode 100644 Penumbra/Interop/Services/SkinFixer.cs diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index ef706f6d..00eab531 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable public event Action LoadingFinished; public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -102,6 +103,12 @@ public unsafe partial class CharacterUtility : IDisposable anyMissing |= DefaultDecalResource == nint.Zero; } + if (DefaultSkinShpkResource == nint.Zero) + { + DefaultSkinShpkResource = (nint)Address->SkinShpkResource; + anyMissing |= DefaultSkinShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable /// Return all relevant resources to the default resource. public void ResetAll() - { + { if (!Ready) - return; + return; foreach (var list in _lists) list.Dispose(); Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs new file mode 100644 index 00000000..5ce35f29 --- /dev/null +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public unsafe class SkinFixer : IDisposable +{ + public static readonly Utf8GamePath SkinShpkPath = + Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _onRenderMaterialHook; + + private readonly CollectionResolver _collectionResolver; + private readonly GameEventManager _gameEvents; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + private readonly ConcurrentDictionary _skinShpks = new(); + + private readonly object _lock = new(); + + private bool _enabled = true; + private int _moddedSkinShpkCount = 0; + private ulong _slowPathCallDelta = 0; + public bool Enabled + { + get => _enabled; + set => _enabled = value; + } + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public SkinFixer(CollectionResolver collectionResolver, GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, DrawObjectState _) + { + SignatureHelper.Initialise(this); + _collectionResolver = collectionResolver; + _gameEvents = gameEvents; + _resources = resources; + _utility = utility; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; // The dependency on DrawObjectState shall ensure that this handler is registered after its one. + _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _onRenderMaterialHook.Enable(); + } + + ~SkinFixer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + _onRenderMaterialHook.Dispose(); + _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; + _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; + foreach (var skinShpk in _skinShpks.Values) + if (skinShpk != nint.Zero) + ((ResourceHandle*)skinShpk)->DecRef(); + _skinShpks.Clear(); + _moddedSkinShpkCount = 0; + } + + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + { + if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) + return; + + nint skinShpk; + try + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + skinShpk = nint.Zero; + } + + if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + + private void OnCharacterBaseDestructor(nint characterBase) + { + if (_skinShpks.Remove(characterBase, out var skinShpk) && skinShpk != nint.Zero) + { + ((ResourceHandle*)skinShpk)->DecRef(); + if (skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); + } + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + if (!_enabled || // Can be toggled on the debug tab. + _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk == nint.Zero) + return _onRenderMaterialHook!.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; + if ((nint)shpkResource != skinShpk) + return _onRenderMaterialHook!.Original(human, param); + + Interlocked.Increment(ref _slowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_lock) + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk; + return _onRenderMaterialHook!.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index b273091b..765ad25f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -10,6 +10,7 @@ public unsafe struct CharacterUtilityData { public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; + public const int IndexSkinShpk = 76; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() .Zip( Enum.GetValues< MetaIndex >() ) @@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData .Select( n => n.Second ).ToArray(); public const int TotalNumResources = 87; - - /// Obtain the index for the eqdp file corresponding to the given race code and accessory. + + /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) => +( int )raceCode switch { @@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData [FieldOffset( 8 + IndexDecalTex * 8 )] public TextureResourceHandle* DecalTexResource; + [FieldOffset( 8 + IndexSkinShpk * 8 )] + public ResourceHandle* SkinShpkResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 28756877..424adfe4 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -8,8 +8,11 @@ public unsafe struct MtrlResource [FieldOffset( 0x00 )] public ResourceHandle Handle; + [FieldOffset( 0xC8 )] + public ShaderPackageResourceHandle* ShpkResourceHandle; + [FieldOffset( 0xD0 )] - public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. [FieldOffset( 0xE0 )] public byte* StringList; @@ -24,8 +27,21 @@ public unsafe struct MtrlResource => StringList + ShpkOffset; public byte* TexString( int idx ) - => StringList + *( TexSpace + 4 + idx * 8 ); + => StringList + TexSpace[idx].PathOffset; public bool TexIsDX11( int idx ) - => *(TexSpace + 5 + idx * 8) >= 0x8000; + => TexSpace[idx].Flags >= 0x8000; + + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x08 )] + public ushort PathOffset; + + [FieldOffset( 0x0A )] + public ushort Flags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 4de81903..5db0f8e1 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle public IntPtr Unk; [FieldOffset( 0x118 )] - public IntPtr KernelTexture; + public Texture* KernelTexture; [FieldOffset( 0x20 )] public IntPtr NewKernelTexture; } +[StructLayout(LayoutKind.Explicit)] +public unsafe struct ShaderPackageResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0xB0 )] + public ShaderPackage* ShaderPackage; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b291e392..eeee8fd0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; -using OtterGui.Tasks; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -81,6 +81,7 @@ public class Penumbra : IDalamudPlugin { _services.GetRequiredService(); } + _services.GetRequiredService(); SetupInterface(); SetupApi(); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 728585ae..8bea52e3 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -117,7 +117,8 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddResolvers(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index a48fd714..1ee62c35 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,6 +34,9 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using static Lumina.Data.Parsing.Layer.LayerCommon; namespace Penumbra.UI.Tabs; @@ -63,6 +66,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; + private readonly SkinFixer _skinFixer; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -70,7 +74,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager) + TextureManager textureManager, SkinFixer skinFixer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -103,6 +107,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; + _skinFixer = skinFixer; } public ReadOnlySpan Label @@ -144,6 +149,8 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawPlayerModelInfo(); ImGui.NewLine(); + DrawGlobalVariableInfo(); + ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } @@ -338,7 +345,7 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -350,6 +357,7 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(name); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); } @@ -363,6 +371,7 @@ public class DebugTab : Window, ITab { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + ImGuiUtil.DrawTableColumn((obj.Address == nint.Zero) ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); @@ -582,45 +591,76 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + var enableSkinFixer = _skinFixer.Enabled; + if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) + _skinFixer.Enabled = enableSkinFixer; + + if (enableSkinFixer) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + } + + using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) { - var idx = CharacterUtility.RelevantIndices[i]; - var intern = new CharacterUtility.InternalIndex(i); + var intern = CharacterUtility.ReverseIndices[idx]; var resource = _characterUtility.Address->Resource(idx); ImGui.TableNextColumn(); + ImGui.TextUnformatted($"[{idx}]"); + ImGui.TableNextColumn(); ImGui.TextUnformatted($"0x{(ulong)resource:X}"); ImGui.TableNextColumn(); + if (resource == null) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + continue; + } UiHelpers.Text(resource); ImGui.TableNextColumn(); - ImGui.Selectable($"0x{resource->GetData().Data:X}"); + var data = (nint)ResourceHandle.GetData(resource); + var length = ResourceHandle.GetLength(resource); + ImGui.Selectable($"0x{data:X}"); if (ImGui.IsItemClicked()) { - var (data, length) = resource->GetData(); if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); } ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{length}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{resource->GetData().Length}"); - ImGui.TableNextColumn(); - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + if (intern.Value != -1) + { + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + else + ImGui.TableNextColumn(); } } @@ -665,6 +705,18 @@ public class DebugTab : Window, ITab } } + private static void DrawCopyableAddress(string label, nint address) + { + using (var _ = PushFont(UiBuilder.MonoFont)) + if (ImGui.Selectable($"0x{address:X16} {label}")) + ImGui.SetClipboardText($"{address:X16}"); + + ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); + } + + private static unsafe void DrawCopyableAddress(string label, void* address) + => DrawCopyableAddress(label, (nint)address); + /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -673,10 +725,14 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; + DrawCopyableAddress("PlayerCharacter", player.Address); + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; + DrawCopyableAddress("CharacterBase", model); + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { if (t1) @@ -730,6 +786,19 @@ public class DebugTab : Window, ITab } } + /// Draw information about some game global variables. + private unsafe void DrawGlobalVariableInfo() + { + var header = ImGui.CollapsingHeader("Global Variables"); + ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); + if (!header) + return; + + DrawCopyableAddress("CharacterUtility", _characterUtility.Address); + DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); + DrawCopyableAddress("Device", Device.Instance()); + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From ec14efb789d28d2c8bdddf10e94ad474222d7c0d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 27 Aug 2023 04:04:14 +0200 Subject: [PATCH 1114/2451] Skin Fixer: Make resolving skin.shpk for new draw objects async --- Penumbra/Interop/Services/SkinFixer.cs | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 5ce35f29..479feafc 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; @@ -102,20 +103,23 @@ public unsafe class SkinFixer : IDisposable if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) return; - nint skinShpk; - try + Task.Run(delegate { - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; - } - catch (Exception e) - { - Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - skinShpk = nint.Zero; - } + nint skinShpk; + try + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + skinShpk = nint.Zero; + } - if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); + if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + }); } private void OnCharacterBaseDestructor(nint characterBase) From 6c0864c8b9ae942566cc0d2f2368270c3eaeb98d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 01:54:14 +0200 Subject: [PATCH 1115/2451] Textures: Add a matrix preset that drops alpha --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index b02ba7b7..c36c5065 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -110,6 +110,7 @@ public partial class CombinedTexture ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Make Opaque (Drop Alpha)", new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), From ffb8f0e8d3530e24a4406ebb44e459bb07cfc56e Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 03:06:18 +0200 Subject: [PATCH 1116/2451] =?UTF-8?q?Material=20editor:=20Allow=20negative?= =?UTF-8?q?s=20again=20with=20R=C2=B2G=C2=B2B=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There seems to be people using it. --- .../ModEditWindow.Materials.ColorSet.cs | 26 ++++++++++++++++--- .../ModEditWindow.Materials.ConstantEditor.cs | 8 +++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 798939ca..daca1098 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -483,13 +483,13 @@ public partial class ModEditWindow private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; - var inputSqrt = Vector3.SquareRoot( input ); + var inputSqrt = PseudoSqrtRgb( input ); var tmp = inputSqrt; if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.HDR ) && tmp != inputSqrt ) { - setter( tmp * tmp ); + setter( PseudoSquareRgb( tmp ) ); ret = true; } @@ -505,4 +505,24 @@ public partial class ModEditWindow return ret; } + + // Functions to deal with squared RGB values without making negatives useless. + + private static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : (x * x); + + private static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + private static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + private static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + private static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + private static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); } \ No newline at end of file diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index f7ea317d..8a104145 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -147,11 +147,11 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(editorWidth); var value = new Vector3(values); if (_squaredRgb) - value = Vector3.SquareRoot(value); + value = PseudoSqrtRgb(value); if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) - value *= value; + value = PseudoSquareRgb(value); if (_clamped) value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); value.CopyTo(values); @@ -165,11 +165,11 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(editorWidth); var value = new Vector4(values); if (_squaredRgb) - value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); + value = PseudoSqrtRgb(value); if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) { if (_squaredRgb) - value *= new Vector4(value.X, value.Y, value.Z, 1.0f); + value = PseudoSquareRgb(value); if (_clamped) value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); value.CopyTo(values); From f54146ada43d118c64392b83c77145659769574f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 03:30:21 +0200 Subject: [PATCH 1117/2451] Textures: PR #327 feedback --- .../Textures/CombinedTexture.Manipulation.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index c36c5065..57749471 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -249,12 +249,10 @@ public partial class CombinedTexture if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) return rgbaPixels; - byte[] resizedPixels; + var resizedPixels = new byte[_targetWidth * _targetHeight * 4]; using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) { - image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight)); - - resizedPixels = new byte[_targetWidth * _targetHeight * 4]; + image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight, KnownResamplers.Lanczos3)); image.CopyPixelDataTo(resizedPixels); } @@ -267,9 +265,13 @@ public partial class CombinedTexture var combineOp = GetActualCombineOp(); var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; - (_targetWidth, _targetHeight) = usesLeft && resizeOp != ResizeOp.ToRight - ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) - : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); + (_targetWidth, _targetHeight) = resizeOp switch + { + ResizeOp.ToLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), + ResizeOp.ToRight => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + ResizeOp.None when usesLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), + _ => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + }; _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; _centerStorage.Type = TextureType.Bitmap; try From 598f3db06aada150dad03ff61314538e8ef19646 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 29 Aug 2023 00:42:59 +0200 Subject: [PATCH 1118/2451] Textures: PR #327 feedback --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 57749471..eb8db121 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -277,7 +277,7 @@ public partial class CombinedTexture try { if (usesLeft) - _leftPixels = (resizeOp == ResizeOp.ToRight) ? ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height) : _left.RgbaPixels; + _leftPixels = ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height); if (usesRight) (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) From 848e4ff8a649292656be3cad613713a20d8b03ef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 29 Aug 2023 03:25:54 +0200 Subject: [PATCH 1119/2451] Textures: Refactor resizing code --- .../Textures/CombinedTexture.Manipulation.cs | 168 +++++++++++------- 1 file changed, 99 insertions(+), 69 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index eb8db121..3c0e8193 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -10,6 +10,7 @@ using Dalamud.Interface; using Penumbra.UI; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; +using System.Linq; namespace Penumbra.Import.Textures; @@ -29,9 +30,11 @@ public partial class CombinedTexture private enum ResizeOp { - None = 0, - ToLeft = 1, - ToRight = 2, + LeftOnly = -2, + RightOnly = -1, + None = 0, + ToLeft = 1, + ToRight = 2, } [Flags] @@ -43,6 +46,38 @@ public partial class CombinedTexture Alpha = 8, } + private readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) + { + public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); + + public (int Width, int Height) Size + => (Width, Height); + + public Image ToImage() + => Image.LoadPixelData(PixelData, Width, Height); + + public RgbaPixelData Resize((int Width, int Height) size) + { + if (Width == size.Width && Height == size.Height) + return this; + + var result = WithNewPixelData(size); + using (var image = ToImage()) + { + image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); + image.CopyPixelDataTo(result.PixelData); + } + + return result; + } + + public static RgbaPixelData WithNewPixelData((int Width, int Height) size) + => new(size.Width, size.Height, new byte[size.Width * size.Height * 4]); + + public static RgbaPixelData FromTexture(Texture texture) + => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); + } + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; @@ -53,12 +88,9 @@ public partial class CombinedTexture private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; - private int _rightWidth = 0; - private int _rightHeight = 0; - private int _targetWidth = 0; - private int _targetHeight = 0; - private byte[] _leftPixels = Array.Empty(); - private byte[] _rightPixels = Array.Empty(); + private RgbaPixelData _targetPixels = RgbaPixelData.Empty; + private RgbaPixelData _leftPixels = RgbaPixelData.Empty; + private RgbaPixelData _rightPixels = RgbaPixelData.Empty; private static readonly IReadOnlyList CombineOpLabels = new string[] { @@ -76,16 +108,16 @@ public partial class CombinedTexture "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; - private static (bool UsesLeft, bool UsesRight) GetCombineOpFlags(CombineOp combineOp) + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) => combineOp switch { - CombineOp.LeftCopy => (true, false), - CombineOp.LeftMultiply => (true, false), - CombineOp.RightCopy => (false, true), - CombineOp.RightMultiply => (false, true), - CombineOp.Over => (true, true), - CombineOp.Under => (true, true), - CombineOp.CopyChannels => (true, true), + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), }; @@ -145,27 +177,27 @@ public partial class CombinedTexture } private Vector4 DataLeft(int offset) - => CappedVector(_leftPixels, offset, _multiplierLeft, _constantLeft); + => CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft); private Vector4 DataRight(int offset) - => CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); + => CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if (x < 0 || x >= _rightWidth || y < 0 || y >= _rightHeight) + if (x < 0 || x >= _rightPixels.Width || y < 0 || y >= _rightPixels.Height) return Vector4.Zero; - var offset = (y * _rightWidth + x) * 4; - return CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); + var offset = (y * _rightPixels.Width + x) * 4; + return CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); } private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -181,9 +213,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -200,9 +232,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -218,9 +250,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -232,9 +264,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -244,45 +276,42 @@ public partial class CombinedTexture } } - private byte[] ResizePixels(byte[] rgbaPixels, int sourceWidth, int sourceHeight) - { - if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) - return rgbaPixels; - - var resizedPixels = new byte[_targetWidth * _targetHeight * 4]; - using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) - { - image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight, KnownResamplers.Lanczos3)); - image.CopyPixelDataTo(resizedPixels); - } - - return resizedPixels; - } - private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); - var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; - (_targetWidth, _targetHeight) = resizeOp switch + var resizeOp = GetActualResizeOp(_resizeOp, combineOp); + + var left = resizeOp != ResizeOp.RightOnly ? RgbaPixelData.FromTexture(_left) : RgbaPixelData.Empty; + var right = resizeOp != ResizeOp.LeftOnly ? RgbaPixelData.FromTexture(_right) : RgbaPixelData.Empty; + + var targetSize = resizeOp switch { - ResizeOp.ToLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), - ResizeOp.ToRight => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), - ResizeOp.None when usesLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), - _ => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + ResizeOp.RightOnly => right.Size, + ResizeOp.ToRight => right.Size, + _ => left.Size, }; - _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; - _centerStorage.Type = TextureType.Bitmap; + try { - if (usesLeft) - _leftPixels = ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height); - if (usesRight) - (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) - ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) - : (_right.TextureWrap!.Width, _right.TextureWrap!.Height, _right.RgbaPixels); - Parallel.For(0, _targetHeight, combineOp switch + _targetPixels = RgbaPixelData.WithNewPixelData(targetSize); + + _centerStorage.RgbaPixels = _targetPixels.PixelData; + _centerStorage.Type = TextureType.Bitmap; + + _leftPixels = resizeOp switch + { + ResizeOp.RightOnly => RgbaPixelData.Empty, + _ => left.Resize(targetSize), + }; + _rightPixels = resizeOp switch + { + ResizeOp.LeftOnly => RgbaPixelData.Empty, + ResizeOp.None => right, + _ => right.Resize(targetSize), + }; + + Parallel.For(0, _targetPixels.Height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, @@ -294,11 +323,12 @@ public partial class CombinedTexture } finally { - _leftPixels = Array.Empty(); - _rightPixels = Array.Empty(); + _leftPixels = RgbaPixelData.Empty; + _rightPixels = RgbaPixelData.Empty; + _targetPixels = RgbaPixelData.Empty; } - return (_targetWidth, _targetHeight); + return targetSize; } private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) @@ -367,11 +397,11 @@ public partial class CombinedTexture } } - var (usesLeft, usesRight) = GetCombineOpFlags(_combineOp); - using (var dis = ImRaii.Disabled(!usesLeft || !usesRight)) + var resizeOp = GetActualResizeOp(_resizeOp, _combineOp); + using (var dis = ImRaii.Disabled((int)resizeOp < 0)) { ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, - Enum.GetValues(), op => ResizeOpLabels[(int)op]); + Enum.GetValues().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]); } using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) From bb8d9441f49ff69e190ae4a0708b37097aefe330 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 01:14:20 +0200 Subject: [PATCH 1120/2451] Material editor: tweak colorset highlighting Make the frequency framerate-independent, set it to 1 Hz, and decrease the dynamic range. Thanks @StoiaCode for feedback! --- .../ModEditWindow.Materials.MtrlTab.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 17f34b64..9061cc98 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; @@ -74,7 +75,7 @@ public partial class ModEditWindow public readonly List MaterialPreviewers = new(4); public readonly List ColorSetPreviewers = new(4); public int HighlightedColorSetRow = -1; - public int HighlightTime = -1; + public readonly Stopwatch HighlightTime = new(); public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) { @@ -546,8 +547,11 @@ public partial class ModEditWindow { var oldRowIdx = HighlightedColorSetRow; - HighlightedColorSetRow = rowIdx; - HighlightTime = (HighlightTime + 1) % 32; + if (HighlightedColorSetRow != rowIdx) + { + HighlightedColorSetRow = rowIdx; + HighlightTime.Restart(); + } if (oldRowIdx >= 0) UpdateColorSetRowPreview(oldRowIdx); @@ -560,7 +564,7 @@ public partial class ModEditWindow var rowIdx = HighlightedColorSetRow; HighlightedColorSetRow = -1; - HighlightTime = -1; + HighlightTime.Reset(); if (rowIdx >= 0) UpdateColorSetRowPreview(rowIdx); @@ -588,7 +592,7 @@ public partial class ModEditWindow } if (HighlightedColorSetRow == rowIdx) - ApplyHighlight(ref row, HighlightTime); + ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); foreach (var previewer in ColorSetPreviewers) { @@ -625,7 +629,7 @@ public partial class ModEditWindow } if (HighlightedColorSetRow >= 0) - ApplyHighlight(ref rows[HighlightedColorSetRow], HighlightTime); + ApplyHighlight(ref rows[HighlightedColorSetRow], (float)HighlightTime.Elapsed.TotalSeconds); foreach (var previewer in ColorSetPreviewers) { @@ -634,9 +638,9 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) + private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) { - var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; + var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; var levelSq = (float)(level * level); row.Diffuse = Vector3.Zero; From 5346abaadf34171d81a0525f16e44d1437eb2a9b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 01:51:43 +0200 Subject: [PATCH 1121/2451] Material editor: tear down previewers bound to a CharacterBase that goes away --- .../ModEditWindow.Materials.LivePreview.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 28 +++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 7d400a71..76ac8915 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -154,7 +154,7 @@ public partial class ModEditWindow protected readonly int ModelSlot; protected readonly int MaterialSlot; - protected readonly CharacterBase* DrawObject; + public readonly CharacterBase* DrawObject; protected readonly Material* Material; protected bool Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9061cc98..6677db5b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -512,6 +512,29 @@ public partial class ModEditWindow ColorSetPreviewers.Clear(); } + public unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0; ) + { + var previewer = MaterialPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorSetPreviewers.Count; i-- > 0;) + { + var previewer = ColorSetPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorSetPreviewers.RemoveAt(i); + } + } + public void SetShaderPackageFlags(uint shPkFlags) { foreach (var previewer in MaterialPreviewers) @@ -665,7 +688,10 @@ public partial class ModEditWindow AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); LoadShpk( FindAssociatedShpk( out _, out _ ) ); if (writable) + { + _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; BindToMaterialInstances(); + } } ~MtrlTab() @@ -682,6 +708,8 @@ public partial class ModEditWindow private void DoDispose() { UnbindFromMaterialInstances(); + if (Writable) + _edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances; } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f1c78bf7..e40a7915 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -19,6 +19,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; +using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -45,6 +46,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ModMergeTab _modMergeTab; private readonly CommunicatorService _communicator; private readonly IDragDropManager _dragDropManager; + private readonly GameEventManager _gameEvents; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -546,7 +548,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) : base(WindowBaseLabel) { _performance = performance; @@ -562,6 +564,7 @@ public partial class ModEditWindow : Window, IDisposable _dragDropManager = dragDropManager; _textures = textures; _fileDialog = fileDialog; + _gameEvents = gameEvents; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); From 38a22c5298865b44180a9a5d2f77809de5ae30ac Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 02:51:36 +0200 Subject: [PATCH 1122/2451] Textures: Simplify away _targetPixels --- .../Textures/CombinedTexture.Manipulation.cs | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 3c0e8193..1633de62 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -53,6 +53,11 @@ public partial class CombinedTexture public (int Width, int Height) Size => (Width, Height); + public RgbaPixelData((int Width, int Height) size, byte[] pixelData) + : this(size.Width, size.Height, pixelData) + { + } + public Image ToImage() => Image.LoadPixelData(PixelData, Width, Height); @@ -61,7 +66,7 @@ public partial class CombinedTexture if (Width == size.Width && Height == size.Height) return this; - var result = WithNewPixelData(size); + var result = new RgbaPixelData(size, NewPixelData(size)); using (var image = ToImage()) { image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); @@ -71,8 +76,8 @@ public partial class CombinedTexture return result; } - public static RgbaPixelData WithNewPixelData((int Width, int Height) size) - => new(size.Width, size.Height, new byte[size.Width * size.Height * 4]); + public static byte[] NewPixelData((int Width, int Height) size) + => new byte[size.Width * size.Height * 4]; public static RgbaPixelData FromTexture(Texture texture) => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); @@ -88,9 +93,8 @@ public partial class CombinedTexture private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; - private RgbaPixelData _targetPixels = RgbaPixelData.Empty; - private RgbaPixelData _leftPixels = RgbaPixelData.Empty; - private RgbaPixelData _rightPixels = RgbaPixelData.Empty; + private RgbaPixelData _leftPixels = RgbaPixelData.Empty; + private RgbaPixelData _rightPixels = RgbaPixelData.Empty; private static readonly IReadOnlyList CombineOpLabels = new string[] { @@ -195,9 +199,9 @@ public partial class CombinedTexture private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -213,9 +217,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -232,9 +236,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -250,9 +254,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -264,9 +268,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _rightPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_rightPixels.Width * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -294,9 +298,7 @@ public partial class CombinedTexture try { - _targetPixels = RgbaPixelData.WithNewPixelData(targetSize); - - _centerStorage.RgbaPixels = _targetPixels.PixelData; + _centerStorage.RgbaPixels = RgbaPixelData.NewPixelData(targetSize); _centerStorage.Type = TextureType.Bitmap; _leftPixels = resizeOp switch @@ -311,7 +313,7 @@ public partial class CombinedTexture _ => right.Resize(targetSize), }; - Parallel.For(0, _targetPixels.Height, combineOp switch + Parallel.For(0, targetSize.Height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, @@ -325,7 +327,6 @@ public partial class CombinedTexture { _leftPixels = RgbaPixelData.Empty; _rightPixels = RgbaPixelData.Empty; - _targetPixels = RgbaPixelData.Empty; } return targetSize; From 600f5987cda4f9e345fdb17830d500acffd52f93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Aug 2023 17:25:26 +0200 Subject: [PATCH 1123/2451] Slight restructuring. --- .../Textures/CombinedTexture.Manipulation.cs | 230 ++---------------- .../Textures/CombinedTexture.Operations.cs | 150 ++++++++++++ Penumbra/Import/Textures/RgbaPixelData.cs | 43 ++++ 3 files changed, 219 insertions(+), 204 deletions(-) create mode 100644 Penumbra/Import/Textures/CombinedTexture.Operations.cs create mode 100644 Penumbra/Import/Textures/RgbaPixelData.cs diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 1633de62..9bc4a2a5 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -8,81 +8,12 @@ using OtterGui; using SixLabors.ImageSharp.PixelFormats; using Dalamud.Interface; using Penumbra.UI; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; using System.Linq; namespace Penumbra.Import.Textures; public partial class CombinedTexture { - private enum CombineOp - { - LeftMultiply = -4, - LeftCopy = -3, - RightCopy = -2, - Invalid = -1, - Over = 0, - Under = 1, - RightMultiply = 2, - CopyChannels = 3, - } - - private enum ResizeOp - { - LeftOnly = -2, - RightOnly = -1, - None = 0, - ToLeft = 1, - ToRight = 2, - } - - [Flags] - private enum Channels - { - Red = 1, - Green = 2, - Blue = 4, - Alpha = 8, - } - - private readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) - { - public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); - - public (int Width, int Height) Size - => (Width, Height); - - public RgbaPixelData((int Width, int Height) size, byte[] pixelData) - : this(size.Width, size.Height, pixelData) - { - } - - public Image ToImage() - => Image.LoadPixelData(PixelData, Width, Height); - - public RgbaPixelData Resize((int Width, int Height) size) - { - if (Width == size.Width && Height == size.Height) - return this; - - var result = new RgbaPixelData(size, NewPixelData(size)); - using (var image = ToImage()) - { - image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); - image.CopyPixelDataTo(result.PixelData); - } - - return result; - } - - public static byte[] NewPixelData((int Width, int Height) size) - => new byte[size.Width * size.Height * 4]; - - public static RgbaPixelData FromTexture(Texture texture) - => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); - } - private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; @@ -96,42 +27,6 @@ public partial class CombinedTexture private RgbaPixelData _leftPixels = RgbaPixelData.Empty; private RgbaPixelData _rightPixels = RgbaPixelData.Empty; - private static readonly IReadOnlyList CombineOpLabels = new string[] - { - "Overlay over Input", - "Input over Overlay", - "Replace Input", - "Copy Channels", - }; - - private static readonly IReadOnlyList CombineOpTooltips = new string[] - { - "Standard composition.\nApply the overlay over the input.", - "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", - "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", - "Replace some input channels with those from the overlay.\nUseful for Multi maps.", - }; - - private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) - => combineOp switch - { - CombineOp.LeftCopy => ResizeOp.LeftOnly, - CombineOp.LeftMultiply => ResizeOp.LeftOnly, - CombineOp.RightCopy => ResizeOp.RightOnly, - CombineOp.RightMultiply => ResizeOp.RightOnly, - CombineOp.Over => resizeOp, - CombineOp.Under => resizeOp, - CombineOp.CopyChannels => resizeOp, - _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), - }; - - private static readonly IReadOnlyList ResizeOpLabels = new string[] - { - "No Resizing", - "Adjust Overlay to Input", - "Adjust Input to Overlay", - }; - private const float OneThird = 1.0f / 3.0f; private const float RWeight = 0.2126f; private const float GWeight = 0.7152f; @@ -154,32 +49,6 @@ public partial class CombinedTexture }; // @formatter:on - private CombineOp GetActualCombineOp() - { - var combineOp = (_left.IsLoaded, _right.IsLoaded) switch - { - (true, true) => _combineOp, - (true, false) => CombineOp.LeftMultiply, - (false, true) => CombineOp.RightMultiply, - (false, false) => CombineOp.Invalid, - }; - - if (combineOp == CombineOp.CopyChannels) - { - if (_copyChannels == 0) - combineOp = CombineOp.LeftMultiply; - else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) - combineOp = CombineOp.RightMultiply; - } - - return combineOp switch - { - CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, - CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, - _ => combineOp, - }; - } - private Vector4 DataLeft(int offset) => CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft); @@ -280,11 +149,10 @@ public partial class CombinedTexture } } - private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var resizeOp = GetActualResizeOp(_resizeOp, combineOp); + var resizeOp = GetActualResizeOp(_resizeOp, combineOp); var left = resizeOp != ResizeOp.RightOnly ? RgbaPixelData.FromTexture(_left) : RgbaPixelData.Empty; var right = resizeOp != ResizeOp.LeftOnly ? RgbaPixelData.FromTexture(_right) : RgbaPixelData.Empty; @@ -325,8 +193,8 @@ public partial class CombinedTexture } finally { - _leftPixels = RgbaPixelData.Empty; - _rightPixels = RgbaPixelData.Empty; + _leftPixels = RgbaPixelData.Empty; + _rightPixels = RgbaPixelData.Empty; } return targetSize; @@ -488,99 +356,53 @@ public partial class CombinedTexture private static bool DrawMatrixTools(ref Matrix4x4 multiplier, ref Vector4 constant) { - var changes = false; - - using (var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview)) - { - if (combo) - foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) - { - if (ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) - { - multiplier = preMultiplier; - constant = preConstant; - changes = true; - } - } - } - + var changes = PresetCombo(ref multiplier, ref constant); ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); ImGui.TextUnformatted("Invert"); ImGui.SameLine(); - if (ImGui.Button("Colors")) - { - InvertRed(ref multiplier, ref constant); - InvertGreen(ref multiplier, ref constant); - InvertBlue(ref multiplier, ref constant); - changes = true; - } + Channels channels = 0; + if (ImGui.Button("Colors")) + channels |= Channels.Red | Channels.Green | Channels.Blue; ImGui.SameLine(); if (ImGui.Button("R")) - { - InvertRed(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Red; ImGui.SameLine(); if (ImGui.Button("G")) - { - InvertGreen(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Green; ImGui.SameLine(); if (ImGui.Button("B")) - { - InvertBlue(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Blue; ImGui.SameLine(); if (ImGui.Button("A")) - { - InvertAlpha(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Alpha; + changes |= InvertChannels(channels, ref multiplier, ref constant); return changes; } - private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant) { - multiplier.M11 = -multiplier.M11; - multiplier.M21 = -multiplier.M21; - multiplier.M31 = -multiplier.M31; - multiplier.M41 = -multiplier.M41; - constant.X = 1.0f - constant.X; - } + using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview); + if (!combo) + return false; - private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M12 = -multiplier.M12; - multiplier.M22 = -multiplier.M22; - multiplier.M32 = -multiplier.M32; - multiplier.M42 = -multiplier.M42; - constant.Y = 1.0f - constant.Y; - } + var ret = false; + foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) + { + if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) + continue; - private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M13 = -multiplier.M13; - multiplier.M23 = -multiplier.M23; - multiplier.M33 = -multiplier.M33; - multiplier.M43 = -multiplier.M43; - constant.Z = 1.0f - constant.Z; - } + multiplier = preMultiplier; + constant = preConstant; + ret = true; + } - private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M14 = -multiplier.M14; - multiplier.M24 = -multiplier.M24; - multiplier.M34 = -multiplier.M34; - multiplier.M44 = -multiplier.M44; - constant.W = 1.0f - constant.W; + return ret; } } diff --git a/Penumbra/Import/Textures/CombinedTexture.Operations.cs b/Penumbra/Import/Textures/CombinedTexture.Operations.cs new file mode 100644 index 00000000..441cd3f0 --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.Operations.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + private enum CombineOp + { + LeftMultiply = -4, + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + Over = 0, + Under = 1, + RightMultiply = 2, + CopyChannels = 3, + } + + private enum ResizeOp + { + LeftOnly = -2, + RightOnly = -1, + None = 0, + ToLeft = 1, + ToRight = 2, + } + + [Flags] + private enum Channels : byte + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + + private static readonly IReadOnlyList CombineOpLabels = new[] + { + "Overlay over Input", + "Input over Overlay", + "Replace Input", + "Copy Channels", + }; + + private static readonly IReadOnlyList CombineOpTooltips = new[] + { + "Standard composition.\nApply the overlay over the input.", + "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", + "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", + "Replace some input channels with those from the overlay.\nUseful for Multi maps.", + }; + + private static readonly IReadOnlyList ResizeOpLabels = new string[] + { + "No Resizing", + "Adjust Overlay to Input", + "Adjust Input to Overlay", + }; + + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, + }; + } + + + private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) + { + if (channels.HasFlag(Channels.Red)) + InvertRed(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Green)) + InvertGreen(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Blue)) + InvertBlue(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Alpha)) + InvertAlpha(ref multiplier, ref constant); + return channels != 0; + } + + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } +} diff --git a/Penumbra/Import/Textures/RgbaPixelData.cs b/Penumbra/Import/Textures/RgbaPixelData.cs new file mode 100644 index 00000000..0314b104 --- /dev/null +++ b/Penumbra/Import/Textures/RgbaPixelData.cs @@ -0,0 +1,43 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace Penumbra.Import.Textures; + +public readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) +{ + public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); + + public (int Width, int Height) Size + => (Width, Height); + + public RgbaPixelData((int Width, int Height) size, byte[] pixelData) + : this(size.Width, size.Height, pixelData) + { + } + + public Image ToImage() + => Image.LoadPixelData(PixelData, Width, Height); + + public RgbaPixelData Resize((int Width, int Height) size) + { + if (Width == size.Width && Height == size.Height) + return this; + + var result = new RgbaPixelData(size, NewPixelData(size)); + using (var image = ToImage()) + { + image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); + image.CopyPixelDataTo(result.PixelData); + } + + return result; + } + + public static byte[] NewPixelData((int Width, int Height) size) + => new byte[size.Width * size.Height * 4]; + + public static RgbaPixelData FromTexture(Texture texture) + => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); +} From f23804975004b9841e99a3f77aa8149425be9581 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 19:16:22 +0200 Subject: [PATCH 1124/2451] Skin Fixer: Fix potential ref leak + add SRH `SafeResourceHandle` wraps a `ResourceHandle*` with auto `IncRef` / `DecRef`, to further help prevent leaks. --- .../Interop/SafeHandles/SafeResourceHandle.cs | 34 ++++++++++++++++ Penumbra/Interop/Services/SkinFixer.cs | 40 ++++++++++++------- 2 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 Penumbra/Interop/SafeHandles/SafeResourceHandle.cs diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs new file mode 100644 index 00000000..7ec0f218 --- /dev/null +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.SafeHandles; + +public unsafe class SafeResourceHandle : SafeHandle +{ + public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; + + public override bool IsInvalid => handle == 0; + + public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public static SafeResourceHandle CreateInvalid() + => new(null, incRef: false); + + protected override bool ReleaseHandle() + { + var handle = Interlocked.Exchange(ref this.handle, 0); + if (handle != 0) + ((ResourceHandle*)handle)->DecRef(); + + return true; + } +} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 479feafc..d72cedfb 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -14,6 +14,7 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; @@ -44,7 +45,7 @@ public unsafe class SkinFixer : IDisposable private readonly ResourceLoader _resources; private readonly CharacterUtility _utility; - private readonly ConcurrentDictionary _skinShpks = new(); + private readonly ConcurrentDictionary _skinShpks = new(); private readonly object _lock = new(); @@ -89,8 +90,7 @@ public unsafe class SkinFixer : IDisposable _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; foreach (var skinShpk in _skinShpks.Values) - if (skinShpk != nint.Zero) - ((ResourceHandle*)skinShpk)->DecRef(); + skinShpk.Dispose(); _skinShpks.Clear(); _moddedSkinShpkCount = 0; } @@ -105,29 +105,41 @@ public unsafe class SkinFixer : IDisposable Task.Run(delegate { - nint skinShpk; + var skinShpk = SafeResourceHandle.CreateInvalid(); try { var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + if (data.Valid) + { + var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, incRef: false); + } } catch (Exception e) { Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - skinShpk = nint.Zero; } - if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); + if (!skinShpk.IsInvalid) + { + if (_skinShpks.TryAdd(drawObject, skinShpk)) + { + if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + else + skinShpk.Dispose(); + } }); } private void OnCharacterBaseDestructor(nint characterBase) { - if (_skinShpks.Remove(characterBase, out var skinShpk) && skinShpk != nint.Zero) + if (_skinShpks.Remove(characterBase, out var skinShpk)) { - ((ResourceHandle*)skinShpk)->DecRef(); - if (skinShpk != _utility.DefaultSkinShpkResource) + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) Interlocked.Decrement(ref _moddedSkinShpkCount); } } @@ -136,12 +148,12 @@ public unsafe class SkinFixer : IDisposable { if (!_enabled || // Can be toggled on the debug tab. _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk == nint.Zero) + !_skinShpks.TryGetValue(human, out var skinShpk)) return _onRenderMaterialHook!.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; - if ((nint)shpkResource != skinShpk) + if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) return _onRenderMaterialHook!.Original(human, param); Interlocked.Increment(ref _slowPathCallDelta); @@ -154,7 +166,7 @@ public unsafe class SkinFixer : IDisposable lock (_lock) try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk; + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; return _onRenderMaterialHook!.Original(human, param); } finally From 6d3e93044072addf83169dde3f2c73433c5e2141 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Aug 2023 20:52:21 +0200 Subject: [PATCH 1125/2451] Use better event in SkinFixer and some cleanup. --- Penumbra/Api/PenumbraApi.cs | 26 ++---- .../Communication/CreatedCharacterBase.cs | 14 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- Penumbra/Interop/Services/SkinFixer.cs | 83 +++++++++---------- Penumbra/UI/Tabs/DebugTab.cs | 11 +-- 5 files changed, 57 insertions(+), 79 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 01078450..9d578190 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -85,26 +85,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public event CreatedCharacterBaseDelegate? CreatedCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value), - Communication.CreatedCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); - } - } + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; public bool Valid => _lumina != null; @@ -157,6 +138,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() @@ -167,6 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded -= OnResourceLoaded; _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); _lumina = null; _communicator = null!; _modManager = null!; @@ -1189,4 +1172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index cbb86fc2..48ba86a5 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,26 +1,30 @@ using System; using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Collections; namespace Penumbra.Communication; /// /// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. +/// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> { public enum Priority { /// - Api = 0, + Api = int.MinValue, + + /// + SkinFixer = 0, } public CreatedCharacterBase() : base(nameof(CreatedCharacterBase)) { } - public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) - => Invoke(this, gameObject, appliedCollectionName, drawObject); + public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) + => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a4cbc967..1a257a96 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable _characterBaseCreateMetaChanges = DisposableContainer.Empty; if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, drawObject); + _lastCreatedCollection.ModCollection, drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index d72cedfb..be45708f 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -10,16 +10,18 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Collections; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; -public unsafe class SkinFixer : IDisposable +public sealed unsafe class SkinFixer : IDisposable { public static readonly Utf8GamePath SkinShpkPath = Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; @@ -34,60 +36,48 @@ public unsafe class SkinFixer : IDisposable { [FieldOffset(0x0)] public Model* Model; + [FieldOffset(0x8)] public uint MaterialIndex; } private readonly Hook _onRenderMaterialHook; - private readonly CollectionResolver _collectionResolver; - private readonly GameEventManager _gameEvents; - private readonly ResourceLoader _resources; - private readonly CharacterUtility _utility; - - private readonly ConcurrentDictionary _skinShpks = new(); + private readonly GameEventManager _gameEvents; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + // CharacterBase to ShpkHandle + private readonly ConcurrentDictionary _skinShpks = new(); private readonly object _lock = new(); - - private bool _enabled = true; + private int _moddedSkinShpkCount = 0; - private ulong _slowPathCallDelta = 0; - public bool Enabled - { - get => _enabled; - set => _enabled = value; - } + private ulong _slowPathCallDelta = 0; + + public bool Enabled { get; internal set; } = true; public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(CollectionResolver collectionResolver, GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, DrawObjectState _) + public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) { SignatureHelper.Initialise(this); - _collectionResolver = collectionResolver; _gameEvents = gameEvents; _resources = resources; _utility = utility; + _communicator = communicator; _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); - _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; // The dependency on DrawObjectState shall ensure that this handler is registered after its one. + _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; _onRenderMaterialHook.Enable(); } - ~SkinFixer() - { - Dispose(false); - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - protected virtual void Dispose(bool disposing) { _onRenderMaterialHook.Dispose(); - _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; + _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; foreach (var skinShpk in _skinShpks.Values) skinShpk.Dispose(); @@ -98,21 +88,21 @@ public unsafe class SkinFixer : IDisposable public ulong GetAndResetSlowPathCallDelta() => Interlocked.Exchange(ref _slowPathCallDelta, 0); - private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) { if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) return; - Task.Run(delegate + Task.Run(() => { var skinShpk = SafeResourceHandle.CreateInvalid(); try { - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + var data = collection.ToResolveData(gameObject); if (data.Valid) { var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); - skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, incRef: false); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); } } catch (Exception e) @@ -128,30 +118,31 @@ public unsafe class SkinFixer : IDisposable Interlocked.Increment(ref _moddedSkinShpkCount); } else + { skinShpk.Dispose(); + } } }); } private void OnCharacterBaseDestructor(nint characterBase) { - if (_skinShpks.Remove(characterBase, out var skinShpk)) - { - var handle = skinShpk.ResourceHandle; - skinShpk.Dispose(); - if ((nint)handle != _utility.DefaultSkinShpkResource) - Interlocked.Decrement(ref _moddedSkinShpkCount); - } + if (!_skinShpks.Remove(characterBase, out var skinShpk)) + return; + + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); } private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) { - if (!_enabled || // Can be toggled on the debug tab. - _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - !_skinShpks.TryGetValue(human, out var skinShpk)) + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) return _onRenderMaterialHook!.Original(human, param); - var material = param->Model->Materials[param->MaterialIndex]; + var material = param->Model->Materials[param->MaterialIndex]; var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) return _onRenderMaterialHook!.Original(human, param); @@ -164,6 +155,7 @@ public unsafe class SkinFixer : IDisposable // - Swapping path is taken up to hundreds of times a frame. // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. lock (_lock) + { try { _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; @@ -173,5 +165,6 @@ public unsafe class SkinFixer : IDisposable { _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; } + } } } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 1ee62c35..c24d64fa 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -36,7 +36,6 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Interop.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using static Lumina.Data.Parsing.Layer.LayerCommon; namespace Penumbra.UI.Tabs; @@ -623,18 +622,14 @@ public class DebugTab : Window, ITab ImGui.TableNextColumn(); if (resource == null) { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + ImGui.TableNextRow(); continue; } UiHelpers.Text(resource); ImGui.TableNextColumn(); var data = (nint)ResourceHandle.GetData(resource); var length = ResourceHandle.GetLength(resource); - ImGui.Selectable($"0x{data:X}"); - if (ImGui.IsItemClicked()) + if (ImGui.Selectable($"0x{data:X}")) { if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", @@ -643,7 +638,7 @@ public class DebugTab : Window, ITab ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{length}"); + ImGui.TextUnformatted(length.ToString()); ImGui.TableNextColumn(); if (intern.Value != -1) From 2ac997610d9b63366d5e964bfe971cd349b15ac3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 01:00:46 +0200 Subject: [PATCH 1126/2451] Remove Finalize from FileEditor. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index ac873ce2..d0e9504c 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -39,11 +39,6 @@ public class FileEditor : IDisposable where T : class, IWritable _combo = new Combo(config, getFiles); } - ~FileEditor() - { - DoDispose(); - } - public void Draw() { using var tab = ImRaii.TabItem(_tabName); @@ -66,15 +61,11 @@ public class FileEditor : IDisposable where T : class, IWritable } public void Dispose() - { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { (_currentFile as IDisposable)?.Dispose(); - _currentFile = null; + _currentFile = null; + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; } private readonly string _tabName; From 5023fafc19366ea5192f77831d00c05db61bee3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Aug 2023 18:45:13 +0200 Subject: [PATCH 1127/2451] Some formatting in Materials.Shpk. --- OtterGui | 2 +- .../ModEditWindow.Materials.Shpk.cs | 71 +++++++++++-------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/OtterGui b/OtterGui index c8394607..728dd8c3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 +Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index d3bc826a..0f13f47e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -101,9 +101,9 @@ public partial class ModEditWindow if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) { tab.Mtrl.ShaderPackage.Name = value; - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); } } @@ -133,6 +133,7 @@ public partial class ModEditWindow /// private void DrawCustomAssociations(MtrlTab tab) { + const string tooltip = "Click to copy file path to clipboard."; var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; @@ -145,20 +146,9 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (ImGui.Selectable(text)) - ImGui.SetClipboardText(tab.LoadedShpkPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - - if (ImGui.Selectable(devkitText)) - ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - - if (ImGui.Selectable(baseDevkitText)) - ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip); if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => @@ -192,11 +182,12 @@ public partial class ModEditWindow var ret = false; foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - var currentValue = key.Value; - var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = + values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); if (!disabled && shpkKey.HasValue) { ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); @@ -216,6 +207,7 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(valueDescription); } } + ImGui.SameLine(); if (description.Length > 0) ImGuiUtil.LabeledHelpMarker(label, description); @@ -223,10 +215,14 @@ public partial class ModEditWindow ImGui.TextUnformatted(label); } else if (description.Length > 0 || currentDescription.Length > 0) + { ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", - description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription); + description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); + } else + { ImGui.TextUnformatted($"{label}: {currentLabel}"); + } } return ret; @@ -268,7 +264,7 @@ public partial class ModEditWindow foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) { var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; - var buffer = tab.Mtrl.GetConstantValues(constant); + var buffer = tab.Mtrl.GetConstantValues(constant); if (buffer.Length > 0) { using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); @@ -277,6 +273,7 @@ public partial class ModEditWindow ret = true; tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); } + ImGui.SameLine(); using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); if (description.Length > 0) @@ -307,8 +304,8 @@ public partial class ModEditWindow static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) { - var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); - using var c = ImRaii.Combo(label, current.ToString()); + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); if (!c) return false; @@ -323,6 +320,7 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); } + return ret; } @@ -339,6 +337,7 @@ public partial class ModEditWindow ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); @@ -348,6 +347,7 @@ public partial class ModEditWindow ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); @@ -355,12 +355,15 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) { - sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10); - ret = true; + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) + | ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10)); + ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); var minLod = (int)((sampler.Flags >> 20) & 0xF); ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); @@ -370,8 +373,10 @@ public partial class ModEditWindow ret = true; tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); using var t = ImRaii.TreeNode("Advanced Settings"); if (!t) @@ -413,7 +418,10 @@ public partial class ModEditWindow GC.KeepAlive(tab); var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow + var textColorWarning = + (textColor & 0xFF000000u) + | ((textColor & 0x00FEFEFE) >> 1) + | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); @@ -443,6 +451,7 @@ public partial class ModEditWindow _ => null, }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) => (firstComponent, lastComponent) switch { From ff012768691892e06d9eda774a3306ba07d9145a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 01:11:57 +0200 Subject: [PATCH 1128/2451] Small cleanup in ResolveContext. --- .../Interop/ResourceTree/ResolveContext.cs | 46 +++++++++---------- .../ModEditWindow.Materials.LivePreview.cs | 17 +++---- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d14bd68b..0cb854f3 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -65,31 +65,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); - public static unsafe FullPath GetResourceHandlePath(ResourceHandle* handle) - { - var name = handle->FileName(); - if (name.IsEmpty) - return FullPath.Empty; - - if (name[0] == (byte)'|') - { - var pos = name.IndexOf((byte)'|', 1); - if (pos < 0) - return FullPath.Empty; - - name = name.Substring(pos + 1); - } - - return new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); - } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, bool withName) - { - if (handle == null) - return null; - - var fullPath = GetResourceHandlePath(handle); + { + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; if (fullPath.InternalName.IsEmpty) return null; @@ -294,4 +273,25 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var i = index.GetOffset(array.Length); return i >= 0 && i < array.Length ? array[i] : null; } + + internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) + { + if (handle == null) + return ByteString.Empty; + + var name = handle->FileName(); + if (name.IsEmpty) + return ByteString.Empty; + + if (name[0] == (byte)'|') + { + var pos = name.IndexOf((byte)'|', 1); + if (pos < 0) + return ByteString.Empty; + + name = name.Substring(pos + 1); + } + + return name; + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 76ac8915..d2ce8796 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; +using Penumbra.String; using Structs = Penumbra.Interop.Structs; namespace Penumbra.UI.AdvancedWindow; @@ -52,8 +53,8 @@ public partial class ModEditWindow private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath) { - static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, string materialPath) - { + static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, ByteString materialPath) + { for (var i = 0; i < drawObject->SlotCount; ++i) { var model = drawObject->Models[i]; @@ -67,11 +68,8 @@ public partial class ModEditWindow continue; var mtrlHandle = material->MaterialResourceHandle; - if (mtrlHandle == null) - continue; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); - if (path.ToString() == materialPath) + if (path == materialPath) result.Add((subActorType, childObjectIndex, i, j)); } } @@ -82,9 +80,8 @@ public partial class ModEditWindow if (drawObject == null) return result; - materialPath = materialPath.Replace('/', '\\').ToLowerInvariant(); - - CollectMaterials(result, subActorType, -1, drawObject, materialPath); + var path = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + CollectMaterials(result, subActorType, -1, drawObject, path); var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; if (firstChildObject != null) @@ -93,7 +90,7 @@ public partial class ModEditWindow var childObjectIndex = 0; do { - CollectMaterials(result, subActorType, childObjectIndex, childObject, materialPath); + CollectMaterials(result, subActorType, childObjectIndex, childObject, path); childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject; ++childObjectIndex; From e5e555b981a6afc2149c60cd581e1f8d41b68a93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 17:12:39 +0200 Subject: [PATCH 1129/2451] Auto-formatting and some cleanup. --- .../ModEditWindow.Materials.ColorSet.cs | 484 ++++++------ .../ModEditWindow.Materials.ConstantEditor.cs | 103 +-- .../ModEditWindow.Materials.MtrlTab.cs | 292 ++++---- .../ModEditWindow.Materials.Shpk.cs | 23 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 236 +++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - .../ModEditWindow.ShaderPackages.cs | 690 ++++++++---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 + 8 files changed, 876 insertions(+), 959 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index daca1098..e1ba045d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -17,65 +17,60 @@ public partial class ModEditWindow private static readonly float HalfMaxValue = (float)Half.MaxValue; private static readonly float HalfEpsilon = (float)Half.Epsilon; - private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) + private bool DrawMaterialColorSetChange(MtrlTab tab, bool disabled) { - if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) - { + if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.ColorSets.Any(c => c.HasRows)) return false; - } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) ) - { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Set", ImGuiTreeNodeFlags.DefaultOpen)) return false; - } var hasAnyDye = tab.UseColorDyeSet; - ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); + ColorSetCopyAllClipboardButton(tab.Mtrl, 0); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled ); - if( !disabled ) + var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled); + if (!disabled) { ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye ); - } - if( hasAnyDye ) - { - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( tab, disabled ); + ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye); } - using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); - if( !table ) + if (hasAnyDye) { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(tab, disabled); + } + + using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!table) return false; - } ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); + ImGui.TableHeader(string.Empty); ImGui.TableNextColumn(); - ImGui.TableHeader( "Row" ); + ImGui.TableHeader("Row"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Diffuse" ); + ImGui.TableHeader("Diffuse"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Specular" ); + ImGui.TableHeader("Specular"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Emissive" ); + ImGui.TableHeader("Emissive"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Gloss" ); + ImGui.TableHeader("Gloss"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Tile" ); + ImGui.TableHeader("Tile"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Repeat" ); + ImGui.TableHeader("Repeat"); ImGui.TableNextColumn(); - ImGui.TableHeader( "Skew" ); - if( hasAnyDye ) + ImGui.TableHeader("Skew"); + if (hasAnyDye) { ImGui.TableNextColumn(); ImGui.TableHeader("Dye"); @@ -83,12 +78,12 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) + for (var j = 0; j < tab.Mtrl.ColorSets.Length; ++j) { - using var _ = ImRaii.PushId( j ); - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + using var _ = ImRaii.PushId(j); + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) { - ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye ); + ret |= DrawColorSetRow(tab, j, i, disabled, hasAnyDye); ImGui.TableNextRow(); } } @@ -97,22 +92,20 @@ public partial class ModEditWindow } - private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx) { - if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) - { + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) return; - } try { - var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var data1 = file.ColorSets[colorSetIdx].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[colorSetIdx].Rows.AsBytes() : ReadOnlySpan.Empty; var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo( array ); - data2.TryCopyTo( array.AsSpan( data1.Length ) ); - var text = Convert.ToBase64String( array ); - ImGui.SetClipboardText( text ); + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); } catch { @@ -120,19 +113,19 @@ public partial class ModEditWindow } } - private bool DrawPreviewDye( MtrlTab tab, bool disabled ) + private bool DrawPreviewDye(MtrlTab tab, bool disabled) { var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + var tt = dyeId == 0 + ? "Select a preview dye first." + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) { var ret = false; - for( var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j ) + for (var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j) { - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= tab.Mtrl.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); - } + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, j, i, dyeId); } tab.UpdateColorSetPreview(); @@ -147,33 +140,31 @@ public partial class ModEditWindow return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled ) + private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled) { - if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) - { + if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) + || tab.Mtrl.ColorSets.Length <= colorSetIdx) return false; - } try { var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) - { + var data = Convert.FromBase64String(text); + if (data.Length < Marshal.SizeOf()) return false; - } - ref var rows = ref tab.Mtrl.ColorSets[ colorSetIdx ].Rows; - fixed( void* ptr = data, output = &rows ) + ref var rows = ref tab.Mtrl.ColorSets[colorSetIdx].Rows; + fixed (void* ptr = data, output = &rows) { - MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); - if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && tab.Mtrl.ColorDyeSets.Length > colorSetIdx ) + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + && tab.Mtrl.ColorDyeSets.Length > colorSetIdx) { - ref var dyeRows = ref tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows; - fixed( void* output2 = &dyeRows ) + ref var dyeRows = ref tab.Mtrl.ColorDyeSets[colorSetIdx].Rows; + fixed (void* output2 = &dyeRows) { - MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } @@ -188,38 +179,38 @@ public partial class ModEditWindow } } - private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true ) ) - { - try - { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; - fixed( byte* ptr = data ) - { - MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); - } + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true)) + return; - var text = Convert.ToBase64String( data ); - ImGui.SetClipboardText( text ); - } - catch + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed (byte* ptr = data) { - // ignored + MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorSet.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorSet.Row.Size, &dye, 2); } + + var text = Convert.ToBase64String(data); + ImGui.SetClipboardText(text); + } + catch + { + // ignored } } - private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) + private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable) { - var ret = ImGui.Checkbox( "Dyeable", ref dyeable ); + var ret = ImGui.Checkbox("Dyeable", ref dyeable); - if( ret ) + if (ret) { tab.UseColorDyeSet = dyeable; - if( dyeable ) + if (dyeable) tab.Mtrl.FindOrAddColorDyeSet(); tab.UpdateColorSetPreview(); } @@ -227,215 +218,244 @@ public partial class ModEditWindow return ret; } - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) + private static unsafe bool ColorSetPasteFromClipboardButton(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true ) ) + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true)) + return false; + + try { - try + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + if (data.Length != MtrlFile.ColorSet.Row.Size + 2 + || tab.Mtrl.ColorSets.Length <= colorSetIdx) + return false; + + fixed (byte* ptr = data) { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || tab.Mtrl.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - fixed( byte* ptr = data ) - { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < tab.Mtrl.ColorDyeSets.Length ) - { - tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); - } - } - - tab.UpdateColorSetRowPreview(rowIdx); - - return true; - } - catch - { - // ignored + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorSet.Row*)ptr; + if (colorSetIdx < tab.Mtrl.ColorDyeSets.Length) + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorDyeSet.Row*)(ptr + MtrlFile.ColorSet.Row.Size); } + + tab.UpdateColorSetRowPreview(rowIdx); + + return true; + } + catch + { + return false; } - - return false; } - private static void ColorSetHighlightButton( MtrlTab tab, int rowIdx, bool disabled ) + private static void ColorSetHighlightButton(MtrlTab tab, int rowIdx, bool disabled) { - ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true ); + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true); - if( ImGui.IsItemHovered() ) - tab.HighlightColorSetRow( rowIdx ); - else if( tab.HighlightedColorSetRow == rowIdx ) + if (ImGui.IsItemHovered()) + tab.HighlightColorSetRow(rowIdx); + else if (tab.HighlightedColorSetRow == rowIdx) tab.CancelColorSetHighlight(); } - private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye ) + private bool DrawColorSetRow(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye) { - static bool FixFloat( ref float val, float current ) + static bool FixFloat(ref float val, float current) { - val = ( float )( Half )val; + val = (float)(Half)val; return val != current; } - using var id = ImRaii.PushId( rowIdx ); - var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + using var id = ImRaii.PushId(rowIdx); + var row = tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx]; var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var dye = hasDye ? tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); - ColorSetCopyClipboardButton( row, dye ); + ColorSetCopyClipboardButton(row, dye); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( tab, colorSetIdx, rowIdx, disabled ); + var ret = ColorSetPasteFromClipboardButton(tab, colorSetIdx, rowIdx, disabled); ImGui.SameLine(); - ColorSetHighlightButton( tab, rowIdx, disabled ); + ColorSetHighlightButton(tab, rowIdx, disabled); ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c; tab.UpdateColorSetRowPreview(rowIdx); } ); - if( hasDye ) + using var dis = ImRaii.Disabled(disabled); + ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Diffuse = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c; tab.UpdateColorSetRowPreview(rowIdx); } ); + ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Specular = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); ImGui.SameLine(); var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = tmpFloat; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if( hasDye ) + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Specular = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].SpecularStrength = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c; tab.UpdateColorSetRowPreview(rowIdx); } ); - if( hasDye ) + ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Emissive = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Emissive = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f") + && FixFloat(ref tmpFloat, row.GlossStrength)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - if( hasDye ) + ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b; tab.UpdateColorSetRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Gloss = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); int tmpInt = row.TileSet; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + ImGui.SetNextItemWidth(intSize); + if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63); - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - if( hasDye ) + if (hasDye) { ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Template = _stainService.TemplateCombo.CurrentSelection; + ret = true; tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); - ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); + ret |= DrawDyePreview(tab, colorSetIdx, rowIdx, disabled, dye, floatSize); } - else if ( hasAnyDye ) + else if (hasAnyDye) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); @@ -445,63 +465,65 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + private bool DrawDyePreview(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; - if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { + if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) return false; - } - var values = entry[ ( int )stain ]; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + var values = entry[(int)stain]; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Apply the selected dye to this row.", disabled, true ); + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain); if (ret) tab.UpdateColorSetRowPreview(rowIdx); ImGui.SameLine(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); ImGui.SameLine(); using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); return ret; } - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) + private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") { - var ret = false; - var inputSqrt = PseudoSqrtRgb( input ); - var tmp = inputSqrt; - if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.HDR ) - && tmp != inputSqrt ) + var ret = false; + var inputSqrt = PseudoSqrtRgb(input); + var tmp = inputSqrt; + if (ImGui.ColorEdit3(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) { - setter( PseudoSquareRgb( tmp ) ); + setter(PseudoSquareRgb(tmp)); ret = true; } - if( letter.Length > 0 && ImGui.IsItemVisible() ) + if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImGui.CalcTextSize( letter ); - var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textSize = ImGui.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + ImGui.GetWindowDrawList().AddText(center, textColor, letter); } - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); return ret; } @@ -509,7 +531,7 @@ public partial class ModEditWindow // Functions to deal with squared RGB values without making negatives useless. private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : (x * x); + => x < 0.0f ? -(x * x) : x * x; private static Vector3 PseudoSquareRgb(Vector3 vec) => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); @@ -525,4 +547,4 @@ public partial class ModEditWindow private static Vector4 PseudoSqrtRgb(Vector4 vec) => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 8a104145..e5b16a47 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Numerics; using ImGuiNET; using OtterGui.Raii; @@ -27,7 +28,8 @@ public partial class ModEditWindow private readonly float _bias; private readonly string _format; - public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit) + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, + string unit) { _minimum = minimum; _maximum = maximum; @@ -55,10 +57,13 @@ public partial class ModEditWindow var value = (values[valueIdx] - _bias) / _factor; if (disabled) + { ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } else { - if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format)) + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, + _maximum ?? 0.0f, _format)) { values[valueIdx] = Clamp(value) * _factor + _bias; ret = true; @@ -111,10 +116,13 @@ public partial class ModEditWindow var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); if (disabled) + { ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } else { - if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format)) + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, + _format)) { values[valueIdx] = Clamp(value) * _factor + _bias; ret = true; @@ -142,14 +150,17 @@ public partial class ModEditWindow public bool Draw(Span values, bool disabled, float editorWidth) { - if (values.Length == 3) + switch (values.Length) { - ImGui.SetNextItemWidth(editorWidth); - var value = new Vector3(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) + case 3: { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled) + return false; + if (_squaredRgb) value = PseudoSquareRgb(value); if (_clamped) @@ -157,17 +168,17 @@ public partial class ModEditWindow value.CopyTo(values); return true; } - - return false; - } - else if (values.Length == 4) - { - ImGui.SetNextItemWidth(editorWidth); - var value = new Vector4(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (ImGui.ColorEdit4("##0", ref value, ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) && !disabled) + case 4: { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit4("##0", ref value, + ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) + || disabled) + return false; + if (_squaredRgb) value = PseudoSquareRgb(value); if (_clamped) @@ -175,11 +186,8 @@ public partial class ModEditWindow value.CopyTo(values); return true; } - - return false; + default: return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); } - else - return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); } } @@ -188,9 +196,7 @@ public partial class ModEditWindow private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) - { - _values = values; - } + => _values = values; public bool Draw(Span values, bool disabled, float editorWidth) { @@ -200,33 +206,40 @@ public partial class ModEditWindow for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { + using var id = ImRaii.PushId(valueIdx); if (valueIdx > 0) ImGui.SameLine(); ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); var currentValue = values[valueIdx]; - var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty); - if (disabled) - ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly); - else - { - using var c = ImRaii.Combo($"##{valueIdx}", currentLabel); - { - if (c) - foreach (var (valueLabel, value, valueDescription) in _values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - values[valueIdx] = value; - ret = true; - } + var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label + ?? currentValue.ToString(CultureInfo.CurrentCulture); + ret = disabled + ? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly) + : DrawCombo(currentLabel, ref values[valueIdx]); + } - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - } + return ret; + } + + private bool DrawCombo(string label, ref float currentValue) + { + using var c = ImRaii.Combo(string.Empty, label); + if (!c) + return false; + + var ret = false; + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + currentValue = value; + ret = true; } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 6677db5b..12f7acd7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; @@ -17,10 +16,8 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -48,28 +45,32 @@ public partial class ModEditWindow public ShpkFile? AssociatedShpk; public JObject? AssociatedShpkDevkit; - public readonly string LoadedBaseDevkitPathName = string.Empty; + public readonly string LoadedBaseDevkitPathName; public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16); + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); - public readonly HashSet< int > VertexShaders = new(16); - public readonly HashSet< int > PixelShaders = new(16); - public bool ShadersKnown = false; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; // Textures & Samplers - public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4); + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - public readonly HashSet< int > UnfoldedTextures = new(4); - public readonly HashSet< uint > SamplerIds = new(16); - public float TextureLabelWidth; - public bool UseColorDyeSet; + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16); + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> + Constants)> Constants = new(16); // Live-Previewers public readonly List MaterialPreviewers = new(4); @@ -77,15 +78,13 @@ public partial class ModEditWindow public int HighlightedColorSetRow = -1; public readonly Stopwatch HighlightTime = new(); - public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { - defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); - if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) - { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true)) return FullPath.Empty; - } - return _edit.FindBestMatch( defaultGamePath ); + return _edit.FindBestMatch(defaultGamePath); } public string[] GetShpkNames() @@ -102,7 +101,7 @@ public partial class ModEditWindow return _shpkNames; } - public void LoadShpk( FullPath path ) + public void LoadShpk(FullPath path) { ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; @@ -110,26 +109,30 @@ public partial class ModEditWindow { LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : _edit._dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); + ? File.ReadAllBytes(LoadedShpkPath.FullName) + : _edit._dalamud.GameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); LoadedShpkPathName = path.ToPath(); } - catch( Exception e ) + catch (Exception e) { LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.Chat.NotificationMessage($"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", + NotificationType.Error); } - if( LoadedShpkPath.InternalName.IsEmpty ) + if (LoadedShpkPath.InternalName.IsEmpty) { AssociatedShpkDevkit = null; LoadedShpkDevkitPathName = string.Empty; } else - AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName ); + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } UpdateShaderKeys(); Update(); @@ -157,10 +160,8 @@ public partial class ModEditWindow } private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - { - return TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - } + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class { @@ -175,7 +176,7 @@ public partial class ModEditWindow if (mayVary && (data as JObject)?["Vary"] != null) { - var selector = BuildSelector(data!["Vary"]! + var selector = BuildSelector(data["Vary"]! .Select(key => (uint)key) .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; @@ -192,14 +193,13 @@ public partial class ModEditWindow } } - public void UpdateShaderKeys() + private void UpdateShaderKeys() { ShaderKeys.Clear(); if (AssociatedShpk != null) - { foreach (var key in AssociatedShpk.MaterialKeys) { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var valueSet = new HashSet(key.Values); @@ -211,8 +211,8 @@ public partial class ModEditWindow { if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - else - return ($"0x{value:X8}", value, string.Empty); + + return ($"0x{value:X8}", value, string.Empty); }).ToArray(); Array.Sort(values, (x, y) => { @@ -220,31 +220,33 @@ public partial class ModEditWindow return -1; if (y.Value == key.DefaultValue) return 1; - return x.Label.CompareTo(y.Label); + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); } - } else - { foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } } - public void UpdateShaders() + private void UpdateShaders() { VertexShaders.Clear(); PixelShaders.Clear(); if (AssociatedShpk == null) + { ShadersKnown = false; + } else { ShadersKnown = true; var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); foreach (var systemKeySelector in systemKeySelectors) { foreach (var sceneKeySelector in sceneKeySelectors) @@ -252,15 +254,13 @@ public partial class ModEditWindow foreach (var subViewKeySelector in subViewKeySelectors) { var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); + var node = AssociatedShpk.GetNodeBySelector(selector); if (node.HasValue) - { foreach (var pass in node.Value.Passes) { VertexShaders.Add((int)pass.VertexShader); PixelShaders.Add((int)pass.PixelShader); } - } else ShadersKnown = false; } @@ -272,12 +272,12 @@ public partial class ModEditWindow var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } - public void UpdateTextures() + private void UpdateTextures() { Textures.Clear(); SamplerIds.Clear(); @@ -302,50 +302,63 @@ public partial class ModEditWindow if (Mtrl.ColorSets.Any(c => c.HasRows)) SamplerIds.Add(TableSamplerId); } + foreach (var samplerId in SamplerIds) { var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2) + if (shpkSampler is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); } + if (SamplerIds.Contains(TableSamplerId)) Mtrl.FindOrAddColorSet(); } + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); TextureLabelWidth = 50f * UiHelpers.Scale; float helpWidth; using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } foreach (var (label, _, _, description, monoFont) in Textures) + { if (!monoFont) TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { foreach (var (label, _, _, description, monoFont) in Textures) + { if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } } TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateConstants() + private void UpdateConstants() { static List FindOrAddGroup(List<(string, List)> groups, string name) { foreach (var (groupName, group) in groups) + { if (string.Equals(name, groupName, StringComparison.Ordinal)) return group; + } var newGroup = new List(16); groups.Add((name, newGroup)); @@ -360,7 +373,10 @@ public partial class ModEditWindow { var values = Mtrl.GetConstantValues(constant); for (var i = 0; i < values.Length; i += 4) - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default)); + { + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + FloatConstantEditor.Default)); + } } } else @@ -371,13 +387,12 @@ public partial class ModEditWindow if ((shpkConstant.ByteSize & 0x3) != 0) continue; - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); var handledElements = new IndexSet(values.Length, false); var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); if (dkData != null) - { foreach (var dkConstant in dkData) { var offset = (int)dkConstant.Offset; @@ -386,13 +401,13 @@ public partial class ModEditWindow length = Math.Min(length, (int)dkConstant.Length.Value); if (length <= 0) continue; + var editor = dkConstant.CreateEditor(); if (editor != null) FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); handledElements.AddRange(offset, length); } - } var fcGroup = FindOrAddGroup(Constants, "Further Constants"); foreach (var (start, end) in handledElements.Ranges(true)) @@ -403,15 +418,20 @@ public partial class ModEditWindow for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) { var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); + var rangeEnd = Math.Min(i + 4, end); if (rangeEnd > rangeStart) - fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + fcGroup.Add(( + $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); } } else { for (var i = start; i < end; i += 4) - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default)); + { + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, + FloatConstantEditor.Default)); + } } } } @@ -424,20 +444,23 @@ public partial class ModEditWindow return 1; if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) return -1; + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); }); // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme foreach (var (_, group) in Constants) + { group.Sort((x, y) => string.CompareOrdinal( x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); + } } public unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); - var localPlayer = FindLocalPlayer(_edit._dalamud.Objects); + var localPlayer = LocalPlayer(_edit._dalamud.Objects); if (null == localPlayer) return; @@ -449,7 +472,9 @@ public partial class ModEditWindow var drawObjects = stackalloc CharacterBase*[4]; drawObjects[0] = drawObject; - + drawObjects[1] = *((CharacterBase**)&localPlayer->DrawData.MainHand + 1); + drawObjects[2] = *((CharacterBase**)&localPlayer->DrawData.OffHand + 1); + drawObjects[3] = *((CharacterBase**)&localPlayer->DrawData.UnkF0 + 1); for (var i = 0; i < 3; ++i) { var subActor = FindSubActor(localPlayer, i); @@ -470,9 +495,11 @@ public partial class ModEditWindow var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot); if (foundMaterials.Contains((nint)material)) continue; + try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, materialSlot)); + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, + materialSlot)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -480,28 +507,31 @@ public partial class ModEditWindow // Carry on without that previewer. } } + UpdateMaterialPreview(); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); - if (colorSet.HasValue) + if (!colorSet.HasValue) + return; + + foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) { - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + try { - try - { - ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, childObjectIndex, modelSlot, materialSlot)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, + childObjectIndex, modelSlot, materialSlot)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. } - UpdateColorSetPreview(); } + + UpdateColorSetPreview(); } - public void UnbindFromMaterialInstances() + private void UnbindFromMaterialInstances() { foreach (var previewer in MaterialPreviewers) previewer.Dispose(); @@ -512,9 +542,9 @@ public partial class ModEditWindow ColorSetPreviewers.Clear(); } - public unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) { - for (var i = MaterialPreviewers.Count; i-- > 0; ) + for (var i = MaterialPreviewers.Count; i-- > 0;) { var previewer = MaterialPreviewers[i]; if ((nint)previewer.DrawObject != characterBase) @@ -553,7 +583,7 @@ public partial class ModEditWindow previewer.SetSamplerFlags(samplerCrc, samplerFlags); } - public void UpdateMaterialPreview() + private void UpdateMaterialPreview() { SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); foreach (var constant in Mtrl.ShaderPackage.Constants) @@ -562,6 +592,7 @@ public partial class ModEditWindow if (values != null) SetMaterialParameter(constant.Id, 0, values); } + foreach (var sampler in Mtrl.ShaderPackage.Samplers) SetSamplerFlags(sampler.SamplerId, sampler.Flags); } @@ -602,7 +633,7 @@ public partial class ModEditWindow if (!maybeColorSet.HasValue) return; - var colorSet = maybeColorSet.Value; + var colorSet = maybeColorSet.Value; var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var row = colorSet.Rows[rowIdx]; @@ -610,7 +641,7 @@ public partial class ModEditWindow { var stm = _edit._stainService.StmFile; var dye = maybeColorDyeSet.Value.Rows[rowIdx]; - if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) + if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -619,7 +650,8 @@ public partial class ModEditWindow foreach (var previewer in ColorSetPreviewers) { - row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan() + .Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } @@ -633,19 +665,19 @@ public partial class ModEditWindow if (!maybeColorSet.HasValue) return; - var colorSet = maybeColorSet.Value; + var colorSet = maybeColorSet.Value; var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var rows = colorSet.Rows; if (maybeColorDyeSet.HasValue && UseColorDyeSet) { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; var colorDyeSet = maybeColorDyeSet.Value; for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) { ref var row = ref rows[i]; - var dye = colorDyeSet.Rows[i]; + var dye = colorDyeSet.Rows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -663,10 +695,10 @@ public partial class ModEditWindow private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) { - var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; + var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; var levelSq = (float)(level * level); - row.Diffuse = Vector3.Zero; + row.Diffuse = Vector3.Zero; row.Specular = Vector3.Zero; row.Emissive = new Vector3(levelSq); } @@ -678,15 +710,15 @@ public partial class ModEditWindow UpdateConstants(); } - public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) + public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - UseColorDyeSet = file.ColorDyeSets.Length > 0; - AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); - LoadShpk( FindAssociatedShpk( out _, out _ ) ); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) { _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; @@ -694,18 +726,7 @@ public partial class ModEditWindow } } - ~MtrlTab() - { - DoDispose(); - } - public void Dispose() - { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { UnbindFromMaterialInstances(); if (Writable) @@ -723,11 +744,7 @@ public partial class ModEditWindow return output.Write(); } - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } + private sealed record DevkitShaderKeyValue(string Label = "", string Description = ""); private sealed class DevkitShaderKey { @@ -736,12 +753,7 @@ public partial class ModEditWindow public Dictionary Values = new(); } - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } + private sealed record DevkitSampler(string Label = "", string Description = "", string DefaultTexture = ""); private enum DevkitConstantType { @@ -752,12 +764,7 @@ public partial class ModEditWindow Enum = 3, } - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0.0f; - } + private sealed record DevkitConstantValue(string Label = "", string Description = "", float Value = 0); private sealed class DevkitConstant { @@ -783,25 +790,20 @@ public partial class ModEditWindow public DevkitConstantValue[] Values = Array.Empty(); public IConstantEditor? CreateEditor() - { - switch (Type) + => Type switch { - case DevkitConstantType.Hidden: - return null; - case DevkitConstantType.Float: - return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit); - case DevkitConstantType.Integer: - return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit); - case DevkitConstantType.Color: - return new ColorConstantEditor(SquaredRgb, Clamped); - case DevkitConstantType.Enum: - return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description))); - default: - return FloatConstantEditor.Default; - } - } + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, + Unit), + DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Factor, Bias, Unit), + DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), + DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, + value => (value.Label, value.Value, value.Description))), + _ => FloatConstantEditor.Default, + }; - private int? ToInteger(float? value) + private static int? ToInteger(float? value) => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 0f13f47e..8fca8aa6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,18 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Numerics; using System.Text; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; -using Penumbra.GameData.Files; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -25,7 +19,7 @@ public partial class ModEditWindow // Apricot shader packages are unlisted because // 1. they cause performance/memory issues when calculating the effective shader set // 2. they probably aren't intended for use with materials anyway - private static readonly IReadOnlyList StandardShaderPackages = new string[] + private static readonly IReadOnlyList StandardShaderPackages = new[] { "3dui.shpk", // "apricot_decal_dummy.shpk", @@ -76,7 +70,7 @@ public partial class ModEditWindow Border = 3, } - private static readonly IReadOnlyList TextureAddressModeTooltips = new string[] + private static readonly IReadOnlyList TextureAddressModeTooltips = new[] { "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", @@ -113,18 +107,15 @@ public partial class ModEditWindow private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { - var ret = false; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, + if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; - ret = true; - tab.SetShaderPackageFlags((uint)shpkFlags); - } + return false; - return ret; + tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + tab.SetShaderPackageFlags((uint)shpkFlags); + return true; } /// diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index b89bab01..102a6778 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,34 +6,35 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor< MtrlTab > _materialTab; + private readonly FileEditor _materialTab; - private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) + private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - DrawMaterialLivePreviewRebind( tab, disabled ); + DrawMaterialLivePreviewRebind(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - var ret = DrawBackFaceAndTransparency( tab, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShader( tab, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawMaterialShader(tab, disabled); - ret |= DrawMaterialTextureChange( tab, disabled ); - ret |= DrawMaterialColorSetChange( tab, disabled ); - ret |= DrawMaterialConstants( tab, disabled ); + ret |= DrawMaterialTextureChange(tab, disabled); + ret |= DrawMaterialColorSetChange(tab, disabled); + ret |= DrawMaterialConstants(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawOtherMaterialDetails( tab.Mtrl, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(tab.Mtrl, disabled); return !disabled && ret; } - private static void DrawMaterialLivePreviewRebind( MtrlTab tab, bool disabled ) + private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) { if (disabled) return; @@ -41,82 +42,74 @@ public partial class ModEditWindow if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); - if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0) - { - ImGui.SameLine(); + if (tab.MaterialPreviewers.Count != 0 || tab.ColorSetPreviewers.Count != 0) + return; - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information."); - } + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGui.TextUnformatted( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."); } - private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) + private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled) { - if( tab.Textures.Count == 0 ) - { + if (tab.Textures.Count == 0) return false; - } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) ) - { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) return false; - } var frameHeight = ImGui.GetFrameHeight(); var ret = false; - using var table = ImRaii.Table( "##Textures", 3 ); + using var table = ImRaii.Table("##Textures", 3); - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight ); - ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Textures.Count; ++i ) + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) { - var (label, textureI, samplerI, description, monoFont) = tab.Textures[i]; - - using var _ = ImRaii.PushId( samplerI ); - var tmp = tab.Mtrl.Textures[ textureI ].Path; - var unfolded = tab.UnfoldedTextures.Contains( samplerI ); + using var _ = ImRaii.PushId(samplerI); + var tmp = tab.Mtrl.Textures[textureI].Path; + var unfolded = tab.UnfoldedTextures.Contains(samplerI); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ), - "Settings for this texture and the associated sampler", false, true ) ) + if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), + new Vector2(frameHeight), + "Settings for this texture and the associated sampler", false, true)) { unfolded = !unfolded; - if( unfolded ) - tab.UnfoldedTextures.Add( samplerI ); + if (unfolded) + tab.UnfoldedTextures.Add(samplerI); else - tab.UnfoldedTextures.Remove( samplerI ); - } - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ textureI ].Path ) - { - ret = true; - tab.Mtrl.Textures[ textureI ].Path = tmp; + tab.UnfoldedTextures.Remove(samplerI); } ImGui.TableNextColumn(); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) ) + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != tab.Mtrl.Textures[textureI].Path) + { + ret = true; + tab.Mtrl.Textures[textureI].Path = tmp; + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) { ImGui.AlignTextToFramePadding(); - if( description.Length > 0 ) - ImGuiUtil.LabeledHelpMarker( label, description ); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); else - ImGui.TextUnformatted( label ); + ImGui.TextUnformatted(label); } - if( unfolded ) + if (unfolded) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI ); + ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI); ImGui.TableNextColumn(); } } @@ -124,26 +117,27 @@ public partial class ModEditWindow return ret; } - private static bool DrawBackFaceAndTransparency( MtrlTab tab, bool disabled ) + private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled) { const uint transparencyBit = 0x10; const uint backfaceBit = 0x01; var ret = false; - using var dis = ImRaii.Disabled( disabled ); + using var dis = ImRaii.Disabled(disabled); - var tmp = ( tab.Mtrl.ShaderPackage.Flags & transparencyBit ) != 0; - if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) + var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) { - tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = + tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; + ret = true; tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } - ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); - tmp = ( tab.Mtrl.ShaderPackage.Flags & backfaceBit ) != 0; - if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) { tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; ret = true; @@ -153,106 +147,80 @@ public partial class ModEditWindow return ret; } - private static void DrawOtherMaterialDetails( MtrlFile file, bool _ ) + private static void DrawOtherMaterialDetails(MtrlFile file, bool _) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return; + + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in file.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) - { - if( sets ) - { - foreach( var set in file.UvSets ) - { - ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - - if( file.AdditionalData.Length <= 0 ) - { + if (file.AdditionalData.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}"))); } private void DrawMaterialReassignmentTab() { - if( _editor.Files.Mdl.Count == 0 ) - { + if (_editor.Files.Mdl.Count == 0) return; - } - using var tab = ImRaii.TabItem( "Material Reassignment" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Material Reassignment"); + if (!tab) return; - } ImGui.NewLine(); - MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) return; - } var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex() ) + foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) { - using var id = ImRaii.PushId( idx ); + using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) info.Save(); - } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true)) info.Restore(); - } ImGui.TableNextColumn(); - ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - var tmp = info.CurrentMaterials[ 0 ]; - if( ImGui.InputText( "##0", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, 0 ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + var tmp = info.CurrentMaterials[0]; + if (ImGui.InputText("##0", ref tmp, 64)) + info.SetMaterial(tmp, 0); - for( var i = 1; i < info.Count; ++i ) + for (var i = 1; i < info.Count; ++i) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - tmp = info.CurrentMaterials[ i ]; - if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, i ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + tmp = info.CurrentMaterials[i]; + if (ImGui.InputText($"##{i}", ref tmp, 64)) + info.SetMaterial(tmp, i); } } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b212e791..518566f5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -5,7 +5,6 @@ using Penumbra.GameData.Files; using Penumbra.String.Classes; using System.Globalization; using System.Linq; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 1b159efc..c0868b71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -20,32 +20,32 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true ); + private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); - private readonly FileEditor< ShpkTab > _shaderPackageTab; + private readonly FileEditor _shaderPackageTab; - private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) + private static bool DrawShaderPackagePanel(ShpkTab file, bool disabled) { - DrawShaderPackageSummary( file ); + DrawShaderPackageSummary(file); var ret = false; - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Pixel Shader", file.Shpk.PixelShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageMaterialParamLayout(file, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageResources( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageResources(file, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawShaderPackageSelection( file ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageSelection(file); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawOtherShaderPackageDetails( file ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherShaderPackageDetails(file); file.FileDialog.Draw(); @@ -54,28 +54,26 @@ public partial class ModEditWindow return !disabled && ret; } - private static void DrawShaderPackageSummary( ShpkTab tab ) + private static void DrawShaderPackageSummary(ShpkTab tab) { - ImGui.TextUnformatted( tab.Header ); - if( !tab.Shpk.Disassembled ) + ImGui.TextUnformatted(tab.Header); + if (!tab.Shpk.Disassembled) { - var textColor = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning ); + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." ); + ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing."); } } - private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) + private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) { - if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) - { + if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) return; - } - var defaultName = objectName[ 0 ] switch + var defaultName = objectName[0] switch { 'V' => $"vs{idx}", 'P' => $"ps{idx}", @@ -83,247 +81,225 @@ public partial class ModEditWindow }; var blob = shader.Blob; - tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenSavePicker($"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, + (success, name) => { - return; - } + if (!success) + return; - try - { - File.WriteAllBytes( name, blob ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + File.WriteAllBytes(name, blob); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - Penumbra.Chat.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); - }, null, false ); + Penumbra.Chat.NotificationMessage( + $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}", + "Penumbra Advanced Editing", NotificationType.Success); + }, null, false); } - private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) + private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if( !ImGui.Button( "Replace Shader Program Blob" ) ) - { + if (!ImGui.Button("Replace Shader Program Blob")) return; - } - tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", + (success, name) => { - return; - } + if (!success) + return; - try - { - shaders[ idx ].Blob = File.ReadAllBytes(name[0] ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } + try + { + shaders[idx].Blob = File.ReadAllBytes(name[0]); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - try - { - shaders[ idx ].UpdateResources( tab.Shpk ); - tab.Shpk.UpdateResources(); - } - catch( Exception e ) - { - tab.Shpk.SetInvalid(); - Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + shaders[idx].UpdateResources(tab.Shpk); + tab.Shpk.UpdateResources(); + } + catch (Exception e) + { + tab.Shpk.SetInvalid(); + Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - tab.Shpk.SetChanged(); - }, 1, null, false ); + tab.Shpk.SetChanged(); + }, 1, null, false); } - private static unsafe void DrawRawDisassembly( Shader shader ) + private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ); - if( !t2 ) - { + using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); + if (!t2) return; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 ); - ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size, - ImGuiInputTextFlags.ReadOnly, null, null ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20); + ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, + (uint)shader.Disassembly!.RawDisassembly.Length + 1, size, + ImGuiInputTextFlags.ReadOnly, null, null); } - private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled ) + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) ) - { + if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) return false; - } var ret = false; - for( var idx = 0; idx < shaders.Length; ++idx ) + for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[ idx ]; - using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); - if( !t ) - { + var shader = shaders[idx]; + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + if (!t) continue; - } - DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled && tab.Shpk.Disassembled ) + DrawShaderExportButton(tab, objectName, shader, idx); + if (!disabled && tab.Shpk.Disassembled) { ImGui.SameLine(); - DrawShaderImportButton( tab, objectName, shaders, idx ); + DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); - if( shader.AdditionalHeader.Length > 0 ) + if (shader.AdditionalHeader.Length > 0) { - using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); - if( t2 ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); - } + using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + if (t2) + ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}"))); } - if( tab.Shpk.Disassembled ) - DrawRawDisassembly( shader ); + if (tab.Shpk.Disassembled) + DrawRawDisassembly(shader); } return ret; } - private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled ) + private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled) { var ret = false; - if( !disabled ) + if (!disabled) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f ); - if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) - { + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGuiUtil.InputUInt16($"{char.ToUpper(slotLabel[0])}{slotLabel[1..].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None)) ret = true; - } } - if( resource.Used == null ) - { + if (resource.Used == null) return ret; - } - var usedString = UsedComponentString( withSize, resource ); - if( usedString.Length > 0 ) - { - ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + var usedString = UsedComponentString(withSize, resource); + if (usedString.Length > 0) + ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled ) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled) { - if( resources.Length == 0 ) - { + if (resources.Length == 0) return false; - } - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { + using var t = ImRaii.TreeNode(arrayName); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < resources.Length; ++idx ) + for (var idx = 0; idx < resources.Length; ++idx) { - ref var buf = ref resources[ idx ]; + ref var buf = ref resources[idx]; var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" - + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + + (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); font.Dispose(); - if( t2 ) - { - ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled ); - } + if (t2) + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled); } return ret; } - private static bool DrawMaterialParamLayoutHeader( string label ) + private static bool DrawMaterialParamLayoutHeader(string label) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); var pos = ImGui.GetCursorScreenPos() - + new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y ); + + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), + ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader( label ); - ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" ); + var ret = ImGui.CollapsingHeader(label); + ImGui.GetWindowDrawList() + .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; } - private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams ) + private static bool DrawMaterialParamLayoutBufferSize(ShpkFile file, Resource? materialParams) { - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); - if( isSizeWellDefined ) - { + var isSizeWellDefined = (file.MaterialParamsSize & 0xF) == 0 + && (!materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4); + if (isSizeWellDefined) return true; - } - ImGui.TextUnformatted( materialParams.HasValue + ImGui.TextUnformatted(materialParams.HasValue ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" - : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16"); return false; } - private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted( tab.Shpk.Disassembled + ImGui.TextUnformatted(tab.Shpk.Disassembled ? "Parameter positions (continuations are grayed out, unused values are red):" - : "Parameter positions (continuations are grayed out):" ); + : "Parameter positions (continuations are grayed out):"); - using var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { + using var table = ImRaii.Table("##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) return false; - } - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); + var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity + var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red + var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1); var ret = false; - for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i ) + for (var i = 0; i < tab.Matrix.GetLength(0); ++i) { ImGui.TableNextColumn(); - ImGui.TableHeader( $" [{i}]" ); - for( var j = 0; j < 4; ++j ) + ImGui.TableHeader($" [{i}]"); + for (var j = 0; j < 4; ++j) { - var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ]; + var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; var color = colorType switch { ShpkTab.ColorType.Unused => textColorUnusedStart, @@ -332,367 +308,307 @@ public partial class ModEditWindow ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, _ => textColorStart, }; - using var _ = ImRaii.PushId( i * 4 + j ); + using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) ) + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) ) + using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable( name ); - if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) + ImGui.Selectable(name); + if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx ); + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); ret = true; tab.Update(); } } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } - if( deletable ) - { - ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." ); - } + if (deletable) + ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); } } return ret; } - private static void DrawShaderPackageMisalignedParameters( ShpkTab tab ) + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + if (!t) return; - } - using var _ = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var name in tab.MalformedParameters ) - { - ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var name in tab.MalformedParameters) + ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - private static void DrawShaderPackageStartCombo( ShpkTab tab ) + private static void DrawShaderPackageStartCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); - if( c ) - { - foreach( var (start, idx) in tab.Orphans.WithIndex() ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + if (c) + foreach (var (start, idx) in tab.Orphans.WithIndex()) { - if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) ) - { - tab.UpdateOrphanStart( idx ); - } + if (ImGui.Selectable(start.Name, idx == tab.NewMaterialParamStart)) + tab.UpdateOrphanStart(idx); } - } } ImGui.SameLine(); - ImGui.TextUnformatted( "Start" ); + ImGui.TextUnformatted("Start"); } - private static void DrawShaderPackageEndCombo( ShpkTab tab ) + private static void DrawShaderPackageEndCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); - if( c ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + if (c) { - var current = tab.Orphans[ tab.NewMaterialParamStart ].Index; - for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i ) + var current = tab.Orphans[tab.NewMaterialParamStart].Index; + for (var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i) { - var next = tab.Orphans[ i ]; - if( current++ != next.Index ) - { + var next = tab.Orphans[i]; + if (current++ != next.Index) break; - } - if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) ) - { + if (ImGui.Selectable(next.Name, i == tab.NewMaterialParamEnd)) tab.NewMaterialParamEnd = i; - } } } } ImGui.SameLine(); - ImGui.TextUnformatted( "End" ); + ImGui.TextUnformatted("End"); } - private static bool DrawShaderPackageNewParameter( ShpkTab tab ) + private static bool DrawShaderPackageNewParameter(ShpkTab tab) { - if( tab.Orphans.Count == 0 ) - { + if (tab.Orphans.Count == 0) return false; - } - DrawShaderPackageStartCombo( tab ); - DrawShaderPackageEndCombo( tab ); + DrawShaderPackageStartCombo(tab); + DrawShaderPackageEndCombo(tab); - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) - { - tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); - } + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) + tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); - var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) ? "The ID is already in use. Please choose a different name." : string.Empty; - if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip, - tooltip.Length > 0 ) ) - { + if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), + tooltip, + tooltip.Length > 0)) return false; - } - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { Id = tab.NewMaterialParamId, - ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ), - ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ), - } ); + ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), + ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), + }); tab.Update(); return true; } - private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialParamLayout(ShpkTab tab, bool disabled) { var ret = false; - var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId ); - if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) ) - { + var materialParams = tab.Shpk.GetConstantById(MaterialParamsConstantId); + if (!DrawMaterialParamLayoutHeader(materialParams?.Name ?? "Material Parameter")) return false; - } - var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams ); + var sizeWellDefined = DrawMaterialParamLayoutBufferSize(tab.Shpk, materialParams); - ret |= DrawShaderPackageMaterialMatrix( tab, disabled ); + ret |= DrawShaderPackageMaterialMatrix(tab, disabled); - if( tab.MalformedParameters.Count > 0 ) - { - DrawShaderPackageMisalignedParameters( tab ); - } - else if( !disabled && sizeWellDefined ) - { - ret |= DrawShaderPackageNewParameter( tab ); - } + if (tab.MalformedParameters.Count > 0) + DrawShaderPackageMisalignedParameters(tab); + else if (!disabled && sizeWellDefined) + ret |= DrawShaderPackageNewParameter(tab); return ret; } - private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageResources(ShpkTab tab, bool disabled) { var ret = false; - if( !ImGui.CollapsingHeader( "Shader Resources" ) ) - { + if (!ImGui.CollapsingHeader("Shader Resources")) return false; - } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled); return ret; } - private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) + private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) { - if( keys.Count == 0 ) - { + if (keys.Count == 0) return; - } - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { + using var t = ImRaii.TreeNode(arrayName); + if (!t) return; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (key, idx) in keys.WithIndex() ) + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); - if( t2 ) + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}"); + if (t2) { - ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - private static void DrawShaderPackageNodes( ShpkTab tab ) + private static void DrawShaderPackageNodes(ShpkTab tab) { - if( tab.Shpk.Nodes.Length <= 0 ) - { + if (tab.Shpk.Nodes.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" ); - if( !t ) - { + using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + if (!t) return; - } - foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) + foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex()) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" ); - if( !t2 ) - { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + if (!t2) continue; - } - foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) - { - ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) + ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) + ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) + ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) + ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ) - .Dispose(); + ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } - private static void DrawShaderPackageSelection( ShpkTab tab ) + private static void DrawShaderPackageSelection(ShpkTab tab) { - if( !ImGui.CollapsingHeader( "Shader Selection" ) ) - { + if (!ImGui.CollapsingHeader("Shader Selection")) return; - } - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); - DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); - DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); - DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); + DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys); - DrawShaderPackageNodes( tab ); - using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" ); - if( t ) + DrawShaderPackageNodes(tab); + using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + if (t) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var selector in tab.Shpk.NodeSelectors ) - { - ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var selector in tab.Shpk.NodeSelectors) + ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } - private static void DrawOtherShaderPackageDetails( ShpkTab tab ) + private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return; - } - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - if( tab.Shpk.AdditionalData.Length > 0 ) + if (tab.Shpk.AdditionalData.Length > 0) { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}"))); } } - private static string UsedComponentString( bool withSize, in Resource resource ) + private static string UsedComponentString(bool withSize, in Resource resource) { - var sb = new StringBuilder( 256 ); - if( withSize ) + var sb = new StringBuilder(256); + if (withSize) { - foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) + foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) { - switch( components ) + switch (components) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( $"[{i}], " ); + sb.Append($"[{i}], "); break; default: - sb.Append( $"[{i}]." ); - foreach( var c in components.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append($"[{i}]."); + foreach (var c in components.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } - switch( resource.UsedDynamically ?? 0 ) + switch (resource.UsedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( "[*], " ); + sb.Append("[*], "); break; default: - sb.Append( "[*]." ); - foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append("[*]."); + foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } else { - var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 ); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) - { - sb.Append( "Red, " ); - } + var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + if ((components & DisassembledShader.VectorComponents.X) != 0) + sb.Append("Red, "); - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) - { - sb.Append( "Green, " ); - } + if ((components & DisassembledShader.VectorComponents.Y) != 0) + sb.Append("Green, "); - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - sb.Append( "Blue, " ); - } + if ((components & DisassembledShader.VectorComponents.Z) != 0) + sb.Append("Blue, "); - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - sb.Append( "Alpha, " ); - } + if ((components & DisassembledShader.VectorComponents.W) != 0) + sb.Append("Alpha, "); } - return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 ); + return sb.Length == 0 ? string.Empty : sb.ToString(0, sb.Length - 2); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e40a7915..e90c148e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -533,14 +533,20 @@ public partial class ModEditWindow : Window, IDisposable var ret = new HashSet(); foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + { if (path.Path.StartsWith(prefix)) ret.Add(path); + } if (_mod != null) foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + { foreach (var path in option.Files.Keys) + { if (path.Path.StartsWith(prefix)) ret.Add(path); + } + } return ret; } From a768b039a8a7c0f98e54dc0872bca2615936aaea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 18:25:29 +0200 Subject: [PATCH 1130/2451] Restructure Live Preview. --- .../MaterialPreview/LiveColorSetPreviewer.cs | 131 +++++ .../MaterialPreview/LiveMaterialPreviewer.cs | 149 ++++++ .../LiveMaterialPreviewerBase.cs | 70 +++ .../Interop/MaterialPreview/MaterialInfo.cs | 120 +++++ .../ModEditWindow.Materials.LivePreview.cs | 483 ------------------ .../ModEditWindow.Materials.MtrlTab.cs | 43 +- 6 files changed, 478 insertions(+), 518 deletions(-) create mode 100644 Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs create mode 100644 Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs create mode 100644 Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs create mode 100644 Penumbra/Interop/MaterialPreview/MaterialInfo.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs diff --git a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs new file mode 100644 index 00000000..18afa949 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.MaterialPreview; + +public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase +{ + public const int TextureWidth = 4; + public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; + public const int TextureLength = TextureWidth * TextureHeight * 4; + + private readonly Framework _framework; + + private readonly Texture** _colorSetTexture; + private readonly Texture* _originalColorSetTexture; + + private Half[] _colorSet; + private bool _updatePending; + + public Half[] ColorSet + => _colorSet; + + public LiveColorSetPreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + _framework = framework; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + throw new InvalidOperationException("Draw object doesn't have color set textures"); + + _colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + + _originalColorSetTexture = *_colorSetTexture; + if (_originalColorSetTexture == null) + throw new InvalidOperationException("Material doesn't have a color set"); + + Structs.TextureUtility.IncRef(_originalColorSetTexture); + + _colorSet = new Half[TextureLength]; + _updatePending = true; + + framework.Update += OnFrameworkUpdate; + } + + protected override void Clear(bool disposing, bool reset) + { + _framework.Update -= OnFrameworkUpdate; + + base.Clear(disposing, reset); + + if (reset) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); + } + else + { + Structs.TextureUtility.DecRef(_originalColorSetTexture); + } + } + + public void ScheduleUpdate() + { + _updatePending = true; + } + + private void OnFrameworkUpdate(Framework _) + { + if (!_updatePending) + return; + + _updatePending = false; + + if (!CheckValidity()) + return; + + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); + if (newTexture == null) + return; + + bool success; + lock (_colorSet) + { + fixed (Half* colorSet = _colorSet) + { + success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); + } + } + + if (success) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); + } + else + { + Structs.TextureUtility.DecRef(newTexture); + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + return false; + + if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs new file mode 100644 index 00000000..1b280b20 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -0,0 +1,149 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Penumbra.Interop.MaterialPreview; + +public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase +{ + private readonly ShaderPackage* _shaderPackage; + + private readonly uint _originalShPkFlags; + private readonly float[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; + + public LiveMaterialPreviewer(IObjectTable objects, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); + + _shaderPackage = shpkHandle->ShaderPackage; + if (_shaderPackage == null) + throw new InvalidOperationException("Material doesn't have a shader package"); + + var material = (Structs.Material*)Material; + + _originalShPkFlags = material->ShaderPackageFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter = materialParameter.ToArray(); + else + _originalMaterialParameter = Array.Empty(); + + _originalSamplerFlags = new uint[material->TextureCount]; + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; + } + + protected override void Clear(bool disposing, bool reset) + { + base.Clear(disposing, reset); + + if (reset) + { + var material = (Structs.Material*)Material; + + material->ShaderPackageFlags = _originalShPkFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter.AsSpan().CopyTo(materialParameter); + + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + if (!CheckValidity()) + return; + + ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + if (!CheckValidity()) + return; + + var constantBuffer = ((Structs.Material*)Material)->MaterialParameter; + if (constantBuffer == null) + return; + + if (!constantBuffer->TryGetBuffer(out var buffer)) + return; + + for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) + { + ref var parameter = ref _shaderPackage->MaterialElements[i]; + if (parameter.CRC == parameterCrc) + { + if ((parameter.Offset & 0x3) != 0 + || (parameter.Size & 0x3) != 0 + || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + return; + + value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + return; + } + } + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + if (!CheckValidity()) + return; + + var id = 0u; + var found = false; + + var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; + for (var i = 0; i < _shaderPackage->SamplerCount; ++i) + { + if (samplers[i].Crc == samplerCrc) + { + id = samplers[i].Id; + found = true; + break; + } + } + + if (!found) + return; + + var material = (Structs.Material*)Material; + for (var i = 0; i < material->TextureCount; ++i) + { + if (material->Textures[i].Id == id) + { + material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; + break; + } + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + return false; + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + return false; + + if (_shaderPackage != shpkHandle->ShaderPackage) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs new file mode 100644 index 00000000..88369725 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -0,0 +1,70 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.MaterialPreview; + +public abstract unsafe class LiveMaterialPreviewerBase : IDisposable +{ + private readonly IObjectTable _objects; + + public readonly MaterialInfo MaterialInfo; + public readonly CharacterBase* DrawObject; + protected readonly Material* Material; + + protected bool Valid; + + public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo) + { + _objects = objects; + + MaterialInfo = materialInfo; + var gameObject = MaterialInfo.GetCharacter(objects); + if (gameObject == nint.Zero) + throw new InvalidOperationException("Cannot retrieve game object."); + + DrawObject = (CharacterBase*)MaterialInfo.GetDrawObject(gameObject); + if (DrawObject == null) + throw new InvalidOperationException("Cannot retrieve draw object."); + + Material = MaterialInfo.GetDrawObjectMaterial(DrawObject); + if (Material == null) + throw new InvalidOperationException("Cannot retrieve material."); + + Valid = true; + } + + public void Dispose() + { + if (Valid) + Clear(true, IsStillValid()); + } + + public bool CheckValidity() + { + if (Valid && !IsStillValid()) + Clear(false, false); + return Valid; + } + + protected virtual void Clear(bool disposing, bool reset) + { + Valid = false; + } + + protected virtual bool IsStillValid() + { + var gameObject = MaterialInfo.GetCharacter(_objects); + if (gameObject == nint.Zero) + return false; + + if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject)) + return false; + + if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs new file mode 100644 index 00000000..f1c9c10e --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Interop.ResourceTree; +using Penumbra.String; + +namespace Penumbra.Interop.MaterialPreview; + +public enum DrawObjectType +{ + PlayerCharacter, + PlayerMainhand, + PlayerOffhand, + PlayerVfx, + MinionCharacter, + MinionUnk1, + MinionUnk2, + MinionUnk3, +}; + +public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot) +{ + public nint GetCharacter(IObjectTable objects) + => GetCharacter(Type, objects); + + public static nint GetCharacter(DrawObjectType type, IObjectTable objects) + => type switch + { + DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0), + DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerVfx => objects.GetObjectAddress(0), + DrawObjectType.MinionCharacter => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1), + _ => nint.Zero, + }; + + public nint GetDrawObject(nint address) + => GetDrawObject(Type, address); + + public static nint GetDrawObject(DrawObjectType type, IObjectTable objects) + => GetDrawObject(type, GetCharacter(type, objects)); + + public static unsafe nint GetDrawObject(DrawObjectType type, nint address) + { + var gameObject = (Character*)address; + if (gameObject == null) + return nint.Zero; + + return type switch + { + DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), + DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1), + _ => nint.Zero, + }; + } + + public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject) + { + if (drawObject == null) + return null; + + if (ModelSlot < 0 || ModelSlot >= drawObject->SlotCount) + return null; + + var model = drawObject->Models[ModelSlot]; + if (model == null) + return null; + + if (MaterialSlot < 0 || MaterialSlot >= model->MaterialCount) + return null; + + return model->Materials[MaterialSlot]; + } + + public static unsafe List FindMaterials(IObjectTable objects, string materialPath) + { + var needle = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + + var result = new List(Enum.GetValues().Length); + foreach (var type in Enum.GetValues()) + { + var drawObject = (CharacterBase*)GetDrawObject(type, objects); + if (drawObject == null) + continue; + + for (var i = 0; i < drawObject->SlotCount; ++i) + { + var model = drawObject->Models[i]; + if (model == null) + continue; + + for (var j = 0; j < model->MaterialCount; ++j) + { + var material = model->Materials[j]; + if (material == null) + continue; + + var mtrlHandle = material->MaterialResourceHandle; + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path == needle) + result.Add(new MaterialInfo(type, i, j)); + } + } + } + + return result; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs deleted file mode 100644 index d2ce8796..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ /dev/null @@ -1,483 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Dalamud.Game; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.Files; -using Penumbra.Interop.ResourceTree; -using Penumbra.String; -using Structs = Penumbra.Interop.Structs; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private static unsafe Character* FindLocalPlayer(IObjectTable objects) - { - var localPlayer = objects[0]; - if (localPlayer is not Dalamud.Game.ClientState.Objects.Types.Character) - return null; - - return (Character*)localPlayer.Address; - } - - private static unsafe Character* FindSubActor(Character* character, int subActorType) - { - if (character == null) - return null; - - switch (subActorType) - { - case -1: - return character; - case 0: - return character->Mount.MountObject; - case 1: - var companion = character->Companion.CompanionObject; - if (companion == null) - return null; - return &companion->Character; - case 2: - var ornament = character->Ornament.OrnamentObject; - if (ornament == null) - return null; - return &ornament->Character; - default: - return null; - } - } - - private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath) - { - static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, ByteString materialPath) - { - for (var i = 0; i < drawObject->SlotCount; ++i) - { - var model = drawObject->Models[i]; - if (model == null) - continue; - - for (var j = 0; j < model->MaterialCount; ++j) - { - var material = model->Materials[j]; - if (material == null) - continue; - - var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); - if (path == materialPath) - result.Add((subActorType, childObjectIndex, i, j)); - } - } - } - - var result = new List<(int, int, int, int)>(); - - if (drawObject == null) - return result; - - var path = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; - CollectMaterials(result, subActorType, -1, drawObject, path); - - var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; - if (firstChildObject != null) - { - var childObject = firstChildObject; - var childObjectIndex = 0; - do - { - CollectMaterials(result, subActorType, childObjectIndex, childObject, path); - - childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject; - ++childObjectIndex; - } - while (childObject != null && childObject != firstChildObject); - } - - return result; - } - - private static unsafe CharacterBase* GetChildObject(CharacterBase* drawObject, int index) - { - if (drawObject == null) - return null; - - if (index >= 0) - { - drawObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject; - if (drawObject == null) - return null; - } - - var first = drawObject; - while (index-- > 0) - { - drawObject = (CharacterBase*)drawObject->DrawObject.Object.NextSiblingObject; - if (drawObject == null || drawObject == first) - return null; - } - - return drawObject; - } - - private static unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject, int modelSlot, int materialSlot) - { - if (drawObject == null) - return null; - - if (modelSlot < 0 || modelSlot >= drawObject->SlotCount) - return null; - - var model = drawObject->Models[modelSlot]; - if (model == null) - return null; - - if (materialSlot < 0 || materialSlot >= model->MaterialCount) - return null; - - return model->Materials[materialSlot]; - } - - private abstract unsafe class LiveMaterialPreviewerBase : IDisposable - { - private readonly IObjectTable _objects; - - protected readonly int SubActorType; - protected readonly int ChildObjectIndex; - protected readonly int ModelSlot; - protected readonly int MaterialSlot; - - public readonly CharacterBase* DrawObject; - protected readonly Material* Material; - - protected bool Valid; - - public LiveMaterialPreviewerBase(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) - { - _objects = objects; - - SubActorType = subActorType; - ChildObjectIndex = childObjectIndex; - ModelSlot = modelSlot; - MaterialSlot = materialSlot; - - var localPlayer = FindLocalPlayer(objects); - if (localPlayer == null) - throw new InvalidOperationException("Cannot retrieve local player object"); - - var subActor = FindSubActor(localPlayer, subActorType); - if (subActor == null) - throw new InvalidOperationException("Cannot retrieve sub-actor (mount, companion or ornament)"); - - DrawObject = GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), childObjectIndex); - if (DrawObject == null) - throw new InvalidOperationException("Cannot retrieve draw object"); - - Material = GetDrawObjectMaterial(DrawObject, modelSlot, materialSlot); - if (Material == null) - throw new InvalidOperationException("Cannot retrieve material"); - - Valid = true; - } - - ~LiveMaterialPreviewerBase() - { - if (Valid) - Dispose(false, IsStillValid()); - } - - public void Dispose() - { - if (Valid) - Dispose(true, IsStillValid()); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing, bool reset) - { - Valid = false; - } - - public bool CheckValidity() - { - if (Valid && !IsStillValid()) - Dispose(false, false); - - return Valid; - } - - protected virtual bool IsStillValid() - { - var localPlayer = FindLocalPlayer(_objects); - if (localPlayer == null) - return false; - - var subActor = FindSubActor(localPlayer, SubActorType); - if (subActor == null) - return false; - - if (DrawObject != GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), ChildObjectIndex)) - return false; - - if (Material != GetDrawObjectMaterial(DrawObject, ModelSlot, MaterialSlot)) - return false; - - return true; - } - } - - private sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase - { - private readonly ShaderPackage* _shaderPackage; - - private readonly uint _originalShPkFlags; - private readonly float[] _originalMaterialParameter; - private readonly uint[] _originalSamplerFlags; - - public LiveMaterialPreviewer(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) - { - var mtrlHandle = Material->MaterialResourceHandle; - if (mtrlHandle == null) - throw new InvalidOperationException("Material doesn't have a resource handle"); - - var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; - if (shpkHandle == null) - throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); - - _shaderPackage = shpkHandle->ShaderPackage; - if (_shaderPackage == null) - throw new InvalidOperationException("Material doesn't have a shader package"); - - var material = (Structs.Material*)Material; - - _originalShPkFlags = material->ShaderPackageFlags; - - if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) - _originalMaterialParameter = materialParameter.ToArray(); - else - _originalMaterialParameter = Array.Empty(); - - _originalSamplerFlags = new uint[material->TextureCount]; - for (var i = 0; i < _originalSamplerFlags.Length; ++i) - _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; - } - - protected override void Dispose(bool disposing, bool reset) - { - base.Dispose(disposing, reset); - - if (reset) - { - var material = (Structs.Material*)Material; - - material->ShaderPackageFlags = _originalShPkFlags; - - if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) - _originalMaterialParameter.AsSpan().CopyTo(materialParameter); - - for (var i = 0; i < _originalSamplerFlags.Length; ++i) - material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; - } - } - - public void SetShaderPackageFlags(uint shPkFlags) - { - if (!CheckValidity()) - return; - - ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; - } - - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) - { - if (!CheckValidity()) - return; - - var cbuffer = ((Structs.Material*)Material)->MaterialParameter; - if (cbuffer == null) - return; - - if (!cbuffer->TryGetBuffer(out var buffer)) - return; - - for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) - { - ref var parameter = ref _shaderPackage->MaterialElements[i]; - if (parameter.CRC == parameterCrc) - { - if ((parameter.Offset & 0x3) != 0 || (parameter.Size & 0x3) != 0 || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) - return; - - value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); - return; - } - } - } - - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) - { - if (!CheckValidity()) - return; - - var id = 0u; - var found = false; - - var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; - for (var i = 0; i < _shaderPackage->SamplerCount; ++i) - { - if (samplers[i].Crc == samplerCrc) - { - id = samplers[i].Id; - found = true; - break; - } - } - - if (!found) - return; - - var material = (Structs.Material*)Material; - for (var i = 0; i < material->TextureCount; ++i) - { - if (material->Textures[i].Id == id) - { - material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; - break; - } - } - } - - protected override bool IsStillValid() - { - if (!base.IsStillValid()) - return false; - - var mtrlHandle = Material->MaterialResourceHandle; - if (mtrlHandle == null) - return false; - - var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; - if (shpkHandle == null) - return false; - - if (_shaderPackage != shpkHandle->ShaderPackage) - return false; - - return true; - } - } - - private sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase - { - public const int TextureWidth = 4; - public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; - public const int TextureLength = TextureWidth * TextureHeight * 4; - - private readonly Framework _framework; - - private readonly Texture** _colorSetTexture; - private readonly Texture* _originalColorSetTexture; - - private Half[] _colorSet; - private bool _updatePending; - - public Half[] ColorSet => _colorSet; - - public LiveColorSetPreviewer(IObjectTable objects, Framework framework, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot) - { - _framework = framework; - - var mtrlHandle = Material->MaterialResourceHandle; - if (mtrlHandle == null) - throw new InvalidOperationException("Material doesn't have a resource handle"); - - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; - if (colorSetTextures == null) - throw new InvalidOperationException("Draw object doesn't have color set textures"); - - _colorSetTexture = colorSetTextures + (modelSlot * 4 + materialSlot); - - _originalColorSetTexture = *_colorSetTexture; - if (_originalColorSetTexture == null) - throw new InvalidOperationException("Material doesn't have a color set"); - Structs.TextureUtility.IncRef(_originalColorSetTexture); - - _colorSet = new Half[TextureLength]; - _updatePending = true; - - framework.Update += OnFrameworkUpdate; - } - - protected override void Dispose(bool disposing, bool reset) - { - _framework.Update -= OnFrameworkUpdate; - - base.Dispose(disposing, reset); - - if (reset) - { - var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); - if (oldTexture != null) - Structs.TextureUtility.DecRef(oldTexture); - } - else - Structs.TextureUtility.DecRef(_originalColorSetTexture); - } - - public void ScheduleUpdate() - { - _updatePending = true; - } - - private void OnFrameworkUpdate(Framework _) - { - if (!_updatePending) - return; - _updatePending = false; - - if (!CheckValidity()) - return; - - var textureSize = stackalloc int[2]; - textureSize[0] = TextureWidth; - textureSize[1] = TextureHeight; - - var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); - if (newTexture == null) - return; - - bool success; - lock (_colorSet) - fixed (Half* colorSet = _colorSet) - success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); - - if (success) - { - var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); - if (oldTexture != null) - Structs.TextureUtility.DecRef(oldTexture); - } - else - Structs.TextureUtility.DecRef(newTexture); - } - - protected override bool IsStillValid() - { - if (!base.IsStillValid()) - return false; - - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; - if (colorSetTextures == null) - return false; - - if (_colorSetTexture != colorSetTextures + (ModelSlot * 4 + MaterialSlot)) - return false; - - return true; - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 12f7acd7..6bb8b8c8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -16,6 +16,7 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.GameData.Files.ShpkFile; @@ -460,46 +461,19 @@ public partial class ModEditWindow { UnbindFromMaterialInstances(); - var localPlayer = LocalPlayer(_edit._dalamud.Objects); - if (null == localPlayer) - return; - - var drawObject = (CharacterBase*)localPlayer->GameObject.GetDrawObject(); - if (null == drawObject) - return; - - var instances = FindMaterial(drawObject, -1, FilePath); - - var drawObjects = stackalloc CharacterBase*[4]; - drawObjects[0] = drawObject; - drawObjects[1] = *((CharacterBase**)&localPlayer->DrawData.MainHand + 1); - drawObjects[2] = *((CharacterBase**)&localPlayer->DrawData.OffHand + 1); - drawObjects[3] = *((CharacterBase**)&localPlayer->DrawData.UnkF0 + 1); - for (var i = 0; i < 3; ++i) - { - var subActor = FindSubActor(localPlayer, i); - if (null == subActor) - continue; - - var subDrawObject = (CharacterBase*)subActor->GameObject.GetDrawObject(); - if (null == subDrawObject) - continue; - - instances.AddRange(FindMaterial(subDrawObject, i, FilePath)); - drawObjects[i + 1] = subDrawObject; - } + var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath); var foundMaterials = new HashSet(); - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + foreach (var materialInfo in instances) { - var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot); + var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects); + var material = materialInfo.GetDrawObjectMaterial(drawObject); if (foundMaterials.Contains((nint)material)) continue; try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot, - materialSlot)); + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -515,12 +489,11 @@ public partial class ModEditWindow if (!colorSet.HasValue) return; - foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances) + foreach (var materialInfo in instances) { try { - ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType, - childObjectIndex, modelSlot, materialSlot)); + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); } catch (InvalidOperationException) { From 616a4635d197d1e6eeb944c5bc892f1778722e38 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Aug 2023 18:32:18 +0200 Subject: [PATCH 1131/2451] Fix slash direction in material path. --- Penumbra/Interop/MaterialPreview/MaterialInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f1c9c10e..0146cf6f 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -86,7 +86,7 @@ public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, i public static unsafe List FindMaterials(IObjectTable objects, string materialPath) { - var needle = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty; + var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; var result = new List(Enum.GetValues().Length); foreach (var type in Enum.GetValues()) From af4373ce507b066a8c1ac7ab3767771e99766338 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 31 Aug 2023 16:36:03 +0000 Subject: [PATCH 1132/2451] [CI] Updating repo.json for testing_0.7.3.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 630dab0d..ccd0317b 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.2", + "TestingAssemblyVersion": "0.7.3.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6f760426c783c2cf7b6b56bc9f9331b8ac2a8989 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Sep 2023 17:27:28 +0200 Subject: [PATCH 1133/2451] Fix newtonsoft not playing well with records with strings. --- .../ModEditWindow.Materials.MtrlTab.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 6bb8b8c8..d230950a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -717,7 +717,11 @@ public partial class ModEditWindow return output.Write(); } - private sealed record DevkitShaderKeyValue(string Label = "", string Description = ""); + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } private sealed class DevkitShaderKey { @@ -726,7 +730,12 @@ public partial class ModEditWindow public Dictionary Values = new(); } - private sealed record DevkitSampler(string Label = "", string Description = "", string DefaultTexture = ""); + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } private enum DevkitConstantType { @@ -737,7 +746,12 @@ public partial class ModEditWindow Enum = 3, } - private sealed record DevkitConstantValue(string Label = "", string Description = "", float Value = 0); + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public float Value = 0; + } private sealed class DevkitConstant { From 0dbe9b59c20beb3a2bedd0ba10c1e4c765bd4b7a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Sep 2023 17:31:40 +0200 Subject: [PATCH 1134/2451] Cleanup --- Penumbra/Mods/Mod.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 6aac7e1e..4df41fb5 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -4,9 +4,6 @@ using System.IO; using System.Linq; using OtterGui; using OtterGui.Classes; -using Penumbra.Collections.Cache; -using Penumbra.Import; -using Penumbra.Meta; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; From 233a865c7837376ee5d558a2b3043bf1a2381f66 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 31 Aug 2023 20:05:39 +0200 Subject: [PATCH 1135/2451] Material editor: use a SafeHandle for texture swapping --- .../MaterialPreview/LiveColorSetPreviewer.cs | 37 +++++--------- .../Interop/SafeHandles/SafeTextureHandle.cs | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 Penumbra/Interop/SafeHandles/SafeTextureHandle.cs diff --git a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs index 18afa949..f927aa43 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs @@ -5,6 +5,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using Penumbra.GameData.Files; +using Penumbra.Interop.SafeHandles; namespace Penumbra.Interop.MaterialPreview; @@ -16,8 +17,8 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase private readonly Framework _framework; - private readonly Texture** _colorSetTexture; - private readonly Texture* _originalColorSetTexture; + private readonly Texture** _colorSetTexture; + private readonly SafeTextureHandle _originalColorSetTexture; private Half[] _colorSet; private bool _updatePending; @@ -40,12 +41,10 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase _colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); - _originalColorSetTexture = *_colorSetTexture; + _originalColorSetTexture = new SafeTextureHandle(*_colorSetTexture, true); if (_originalColorSetTexture == null) throw new InvalidOperationException("Material doesn't have a color set"); - Structs.TextureUtility.IncRef(_originalColorSetTexture); - _colorSet = new Half[TextureLength]; _updatePending = true; @@ -59,15 +58,9 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase base.Clear(disposing, reset); if (reset) - { - var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); - if (oldTexture != null) - Structs.TextureUtility.DecRef(oldTexture); - } - else - { - Structs.TextureUtility.DecRef(_originalColorSetTexture); - } + _originalColorSetTexture.Exchange(ref *(nint*)_colorSetTexture); + + _originalColorSetTexture.Dispose(); } public void ScheduleUpdate() @@ -89,8 +82,8 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase textureSize[0] = TextureWidth; textureSize[1] = TextureHeight; - var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); - if (newTexture == null) + using var texture = new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false); + if (texture.IsInvalid) return; bool success; @@ -98,20 +91,12 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase { fixed (Half* colorSet = _colorSet) { - success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); + success = Structs.TextureUtility.InitializeContents(texture.Texture, colorSet); } } if (success) - { - var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); - if (oldTexture != null) - Structs.TextureUtility.DecRef(oldTexture); - } - else - { - Structs.TextureUtility.DecRef(newTexture); - } + texture.Exchange(ref *(nint*)_colorSetTexture); } protected override bool IsStillValid() diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs new file mode 100644 index 00000000..36cd4612 --- /dev/null +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -0,0 +1,48 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.SafeHandles; + +public unsafe class SafeTextureHandle : SafeHandle +{ + public Texture* Texture => (Texture*)handle; + + public override bool IsInvalid => handle == 0; + + public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); + if (incRef && handle != null) + TextureUtility.IncRef(handle); + SetHandle((nint)handle); + } + + public void Exchange(ref nint ppTexture) + { + lock (this) + { + handle = Interlocked.Exchange(ref ppTexture, handle); + } + } + + public static SafeTextureHandle CreateInvalid() + => new(null, incRef: false); + + protected override bool ReleaseHandle() + { + nint handle; + lock (this) + { + handle = this.handle; + this.handle = 0; + } + if (handle != 0) + TextureUtility.DecRef((Texture*)handle); + + return true; + } +} From 686c53d919681ee64f00c16a182ea9cc54e1c02b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 18:46:31 +0200 Subject: [PATCH 1136/2451] Material editor: Customizable highlight color --- .../UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs | 8 +++++--- Penumbra/UI/Classes/Colors.cs | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index d230950a..f4ce3329 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -19,6 +19,7 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.Classes; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -668,12 +669,13 @@ public partial class ModEditWindow private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) { - var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; - var levelSq = (float)(level * level); + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = ColorId.InGameHighlight.Value(); + var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); row.Diffuse = Vector3.Zero; row.Specular = Vector3.Zero; - row.Emissive = new Vector3(levelSq); + row.Emissive = color * color; } public void Update() diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 450f3787..e2acc1a3 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,7 +24,8 @@ public enum ColorId RedundantAssignment, NoModsAssignment, NoAssignment, - SelectorPriority, + SelectorPriority, + InGameHighlight, } public static class Colors @@ -64,6 +65,7 @@ public static class Colors ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; From 5899a59e065e6534d03601b1636823c4dfe0fd38 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 18:46:40 +0200 Subject: [PATCH 1137/2451] Material editor: Vector field spacing --- .../ModEditWindow.Materials.ConstantEditor.cs | 31 ++++++++++--------- .../ModEditWindow.Materials.Shpk.cs | 3 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index e5b16a47..19e96539 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -13,7 +13,7 @@ public partial class ModEditWindow { private interface IConstantEditor { - bool Draw(Span values, bool disabled, float editorWidth); + bool Draw(Span values, bool disabled); } private sealed class FloatConstantEditor : IConstantEditor @@ -42,16 +42,18 @@ public partial class ModEditWindow _format = $"{_format} {unit.Replace("%", "%%")}"; } - public bool Draw(Span values, bool disabled, float editorWidth) + public bool Draw(Span values, bool disabled) { - var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; var ret = false; + // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { if (valueIdx > 0) - ImGui.SameLine(); + ImGui.SameLine(0.0f, spacing); ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); @@ -101,16 +103,18 @@ public partial class ModEditWindow _format = $"{_format} {unit.Replace("%", "%%")}"; } - public bool Draw(Span values, bool disabled, float editorWidth) + public bool Draw(Span values, bool disabled) { - var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; var ret = false; + // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { if (valueIdx > 0) - ImGui.SameLine(); + ImGui.SameLine(0.0f, spacing); ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); @@ -148,13 +152,12 @@ public partial class ModEditWindow _clamped = clamped; } - public bool Draw(Span values, bool disabled, float editorWidth) + public bool Draw(Span values, bool disabled) { switch (values.Length) { case 3: { - ImGui.SetNextItemWidth(editorWidth); var value = new Vector3(values); if (_squaredRgb) value = PseudoSqrtRgb(value); @@ -170,7 +173,6 @@ public partial class ModEditWindow } case 4: { - ImGui.SetNextItemWidth(editorWidth); var value = new Vector4(values); if (_squaredRgb) value = PseudoSqrtRgb(value); @@ -186,7 +188,7 @@ public partial class ModEditWindow value.CopyTo(values); return true; } - default: return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); + default: return FloatConstantEditor.Default.Draw(values, disabled); } } } @@ -198,9 +200,10 @@ public partial class ModEditWindow public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) => _values = values; - public bool Draw(Span values, bool disabled, float editorWidth) + public bool Draw(Span values, bool disabled) { - var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; var ret = false; @@ -208,7 +211,7 @@ public partial class ModEditWindow { using var id = ImRaii.PushId(valueIdx); if (valueIdx > 0) - ImGui.SameLine(); + ImGui.SameLine(0.0f, spacing); ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 8fca8aa6..f6f480e7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -259,7 +259,8 @@ public partial class ModEditWindow if (buffer.Length > 0) { using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); - if (editor.Draw(buffer[slice], disabled, 250.0f)) + ImGui.SetNextItemWidth(250.0f); + if (editor.Draw(buffer[slice], disabled)) { ret = true; tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); From 1b490510c7f3165956a9b09eee22c4e656a47995 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 19:09:38 +0200 Subject: [PATCH 1138/2451] Fix compiler warning --- Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index f4ce3329..aff30fb0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -178,7 +178,7 @@ public partial class ModEditWindow if (mayVary && (data as JObject)?["Vary"] != null) { - var selector = BuildSelector(data["Vary"]! + var selector = BuildSelector(data!["Vary"]! .Select(key => (uint)key) .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; From b985833aaa144d6e2593067529f9a0dad70cb07b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Sep 2023 15:54:11 +0200 Subject: [PATCH 1139/2451] Check for drawObject != null before invoking draw object created event. --- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 1a257a96..cdf28bbd 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -140,7 +140,7 @@ public unsafe class MetaState : IDisposable { _characterBaseCreateMetaChanges.Dispose(); _characterBaseCreateMetaChanges = DisposableContainer.Empty; - if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) + if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != nint.Zero) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, drawObject); _lastCreatedCollection = ResolveData.Invalid; From 0741ce0ce7179adba19055b599402ce10e30db65 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Sep 2023 15:55:06 +0200 Subject: [PATCH 1140/2451] Fix variant gamepath. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1c68fd5e..43f6737d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1c68fd5efb23798d13154c1de0ad010db319abe2 +Subproject commit 43f6737d4baa7988a5fe23096f20827bc54e6812 From ecfe88faa681fc96f75312e206f9b9b257d677b2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 2 Sep 2023 14:00:06 +0000 Subject: [PATCH 1141/2451] [CI] Updating repo.json for testing_0.7.3.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ccd0317b..84db423d 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.3", + "TestingAssemblyVersion": "0.7.3.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ccc0b51a999a6e3834d41620b827546875c784e1 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 01:53:32 +0200 Subject: [PATCH 1142/2451] Resource Tree: Improve mtrl and sklb support --- .../Interop/ResourceTree/ResolveContext.cs | 107 ++++++++++++++---- Penumbra/Interop/ResourceTree/ResourceNode.cs | 28 +++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 +++-- .../Interop/ResourceTree/TreeBuildCache.cs | 5 - .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 5 files changed, 129 insertions(+), 43 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0cb854f3..8a27f02b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData; @@ -23,17 +24,17 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal) + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal); } - private ResourceNode? CreateNodeFromTex(nint sourceAddress, ByteString gamePath, bool @internal, bool dx11) + private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) { if (dx11) { @@ -59,13 +60,19 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); } - private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) - => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); + private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal) + { + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + } + + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, bool withName) { var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; @@ -79,13 +86,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, sourceAddress, gamePaths[0], fullPath, @internal); + return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal); + return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { @@ -95,12 +103,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (!Utf8GamePath.FromString(path, out var gamePath)) return null; - return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false); + return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false); } public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { - var node = CreateNodeFromResourceHandle(ResourceType.Imc, (nint) imc, imc, true, false); + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); if (node == null) return null; @@ -113,8 +121,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return node; } - public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + => CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) { @@ -145,6 +153,38 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { + static ushort GetTextureIndex(ushort texFlags) + { + if ((texFlags & 0x001F) != 0x001F) + return (ushort)(texFlags & 0x001F); + else if ((texFlags & 0x03E0) != 0x03E0) + return (ushort)((texFlags >> 5) & 0x001F); + else + return (ushort)((texFlags >> 10) & 0x001F); + } + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) + { + var textures = mtrl->Textures; + for (var i = 0; i < mtrl->TextureCount; ++i) + { + if (textures[i].ResourceHandle == handle) + return textures[i].Id; + } + + return null; + } + static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) + { + var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers; + for (var i = 0; i < shpk->SamplerCount; ++i) + { + if (samplers[i].Id == id) + return samplers[i].Crc; + } + + return null; + } + if (mtrl == null) return null; @@ -153,23 +193,36 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; - - var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); + var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; + var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { - var texNode = CreateNodeFromTex(nint.Zero, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); if (texNode == null) continue; if (WithNames) { - var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; + string? name = null; + if (shpk != null) + { + var index = GetTextureIndex(resource->TexSpace[i].Flags); + uint? samplerId; + if (index != 0x001F) + samplerId = mtrl->Textures[index].Id; + else + samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); + if (samplerId.HasValue) + { + var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); + if (samplerCrc.HasValue) + name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + } + } node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else @@ -181,6 +234,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return node; } + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonResourceHandle == null) + return null; + + return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + } + private FullPath FilterFullPath(FullPath fullPath) { if (!fullPath.IsRooted) @@ -294,4 +355,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return name; } + + static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + { + if (handle == null) + return 0; + + return ResourceHandle.GetLength(handle); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dc0c5fcb..bceda36c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -9,37 +9,43 @@ public class ResourceNode { public readonly string? Name; public readonly ResourceType Type; - public readonly nint SourceAddress; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; public readonly Utf8GamePath GamePath; public readonly Utf8GamePath[] PossibleGamePaths; public readonly FullPath FullPath; + public readonly ulong Length; public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = gamePath; + Name = name; + Type = type; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; + GamePath = gamePath; PossibleGamePaths = new[] { gamePath, }; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, - bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + ulong length, bool @internal) { Name = name; Type = type; - SourceAddress = sourceAddress; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; PossibleGamePaths = possibleGamePaths; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } @@ -48,10 +54,12 @@ public class ResourceNode { Name = name; Type = originalResourceNode.Type; - SourceAddress = originalResourceNode.SourceAddress; + ObjectAddress = originalResourceNode.ObjectAddress; + ResourceHandle = originalResourceNode.ResourceHandle; GamePath = originalResourceNode.GamePath; PossibleGamePaths = originalResourceNode.PossibleGamePaths; FullPath = originalResourceNode.FullPath; + Length = originalResourceNode.Length; Internal = originalResourceNode.Internal; Children = originalResourceNode.Children; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 76d0c3f2..f14191c8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -57,7 +58,9 @@ public class ResourceTree var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); - } + } + + AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); @@ -95,7 +98,9 @@ public class ResourceTree subObjectNodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") : mdlNode); - } + } + + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; ++subObjectIndex; @@ -106,16 +111,25 @@ public class ResourceTree var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); - if (skeletonNode != null) - Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); - - var decalNode = context.CreateNodeFromTex(human->Decal); + var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); - var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); + var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + } + + private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + { + if (skeleton == null) + return; + + for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) + { + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (sklbNode != null) + nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index e9939496..d29916dd 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -12,7 +12,6 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { private readonly IDataManager _dataManager; - private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); public readonly List Characters; public readonly Dictionary CharactersById; @@ -27,10 +26,6 @@ internal class TreeBuildCache .ToDictionary(c => c.Key, c => c.First()); } - /// Try to read a material file from the given path and cache it on success. - public MtrlFile? ReadMaterial(FullPath path) - => ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes)); - /// Try to read a shpk file from the given path and cache it on success. public ShpkFile? ReadShaderPackage(FullPath path) => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 4d8c77a7..0d87215f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -128,7 +128,7 @@ public class ResourceTreeViewer if (debugMode) ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}"); } ImGui.TableNextColumn(); From db521dd21cc8438308c2e2d983d7fbfb704211f0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 04:52:54 +0200 Subject: [PATCH 1143/2451] Resource Tree: Deduplicate nodes, add skp --- .../Interop/ResourceTree/ResolveContext.cs | 76 +++++++++++++++++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 34 +++++---- .../ResourceTree/ResourceTreeFactory.cs | 6 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 32 +++++--- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8a27f02b..a97ff726 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,21 +11,27 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Interop.ResourceTree; internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) { + public readonly Dictionary Nodes = new(128); + public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, - CharacterArmor Equipment) +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, + Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + return cached; + if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) @@ -36,6 +42,9 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) { + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + return cached; + if (dx11) { var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); @@ -69,7 +78,11 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + if (resourceHandle != null) + Nodes.Add((nint)resourceHandle, node); + + return node; } private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, @@ -95,6 +108,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } + public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { var raceSexIdStr = gr.ToRaceCode(); @@ -108,6 +122,9 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { + if (Nodes.TryGetValue((nint)imc, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); if (node == null) return null; @@ -118,17 +135,31 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide node = node.WithName(name != null ? $"IMC: {name}" : null); } + Nodes.Add((nint)imc, node); + return node; } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + { + if (Nodes.TryGetValue((nint)tex, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + if (node != null) + Nodes.Add((nint)tex, node); + + return node; + } public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) return null; + if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); if (node == null) return null; @@ -148,6 +179,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide : mtrlNode); } + Nodes.Add((nint)mdl->ResourceHandle, node); + return node; } @@ -188,7 +221,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (mtrl == null) return null; - var resource = mtrl->ResourceHandle; + var resource = mtrl->ResourceHandle; + if (Nodes.TryGetValue((nint)resource, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); if (node == null) return null; @@ -231,6 +267,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide } } + Nodes.Add((nint)resource, node); + return node; } @@ -239,7 +277,29 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (sklb->SkeletonResourceHandle == null) return null; - return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + if (node != null) + Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + + return node; + } + + public unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonParameterResourceHandle == null) + return null; + + if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithNames); + if (node != null) + Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + + return node; } private FullPath FilterFullPath(FullPath fullPath) @@ -356,7 +416,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return name; } - static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + private static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) { if (handle == null) return 0; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index f14191c8..f3e6ca51 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -13,29 +13,33 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { - public readonly string Name; - public readonly nint SourceAddress; - public readonly bool PlayerRelated; - public readonly string CollectionName; - public readonly List Nodes; + public readonly string Name; + public readonly nint GameObjectAddress; + public readonly nint DrawObjectAddress; + public readonly bool PlayerRelated; + public readonly string CollectionName; + public readonly List Nodes; + public readonly HashSet FlatNodes; public int ModelId; public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName) + public ResourceTree(string name, nint gameObjectAddress, nint drawObjectAddress, bool playerRelated, string collectionName) { - Name = name; - SourceAddress = sourceAddress; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); + Name = name; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + PlayerRelated = playerRelated; + CollectionName = collectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } internal unsafe void LoadResources(GlobalResolveContext globalContext) { - var character = (Character*)SourceAddress; - var model = (CharacterBase*)character->GameObject.GetDrawObject(); + var character = (Character*)GameObjectAddress; + var model = (CharacterBase*)DrawObjectAddress; var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); ModelId = character->CharacterData.ModelCharaId; @@ -130,6 +134,10 @@ public class ResourceTree var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + + var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (skpNode != null) + nodes.Add(context.WithNames ? skpNode.WithName($"{prefix}Skeleton #{i} Parameters") : skpNode); } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 98c1b305..f416dc12 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -62,7 +62,8 @@ public class ResourceTreeFactory return null; var gameObjStruct = (GameObject*)character.Address; - if (gameObjStruct->GetDrawObject() == null) + var drawObjStruct = gameObjStruct->GetDrawObject(); + if (drawObjStruct == null) return null; var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true); @@ -70,10 +71,11 @@ public class ResourceTreeFactory return null; var (name, related) = GetCharacterName(character, cache); - var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name); + var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); tree.LoadResources(globalContext); + tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 0d87215f..8e996eb7 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -18,7 +18,7 @@ public class ResourceTreeViewer private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; - private readonly HashSet _unfolded; + private readonly HashSet _unfolded; private Task? _task; @@ -30,7 +30,7 @@ public class ResourceTreeViewer _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; - _unfolded = new HashSet(); + _unfolded = new HashSet(); } public void Draw() @@ -82,7 +82,7 @@ public class ResourceTreeViewer (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0); + DrawNodes(tree.Nodes, 0, 0); } } } @@ -101,7 +101,7 @@ public class ResourceTreeViewer } }); - private void DrawNodes(IEnumerable resourceNodes, int level) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); @@ -109,26 +109,36 @@ public class ResourceTreeViewer foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { if (resourceNode.Internal && !debugMode) - continue; + continue; + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity + + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + + var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); - var unfolded = _unfolded.Contains(resourceNode); + var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) { if (unfolded) - _unfolded.Remove(resourceNode); + _unfolded.Remove(nodePathHash); else - _unfolded.Add(resourceNode); + _unfolded.Add(nodePathHash); unfolded = !unfolded; } - if (debugMode) + if (debugMode) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X16}"); + } } ImGui.TableNextColumn(); @@ -171,7 +181,7 @@ public class ResourceTreeViewer } if (unfolded) - DrawNodes(resourceNode.Children, level + 1); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } } From 30c622c085d658ca69bf980a707ab1860b9993f8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 06:06:42 +0200 Subject: [PATCH 1144/2451] Resource Tree: Add ChangedItem-like icons, make UI prettier --- .../Interop/ResourceTree/ResolveContext.cs | 72 ++++++++++--------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 31 +++++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 20 +++--- .../ResourceTree/ResourceTreeFactory.cs | 17 ++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 44 ++++++++---- Penumbra/UI/ChangedItemDrawer.cs | 41 ++++++----- Penumbra/UI/Tabs/OnScreenTab.cs | 4 +- 8 files changed, 139 insertions(+), 95 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a97ff726..72a5be69 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,20 +11,21 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; -using static Penumbra.GameData.Files.ShpkFile; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, Nodes, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUIData, RedactExternalPaths, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, - Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) @@ -78,7 +79,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); @@ -99,14 +100,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], fullPath, GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) @@ -129,10 +130,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - if (WithNames) + if (WithUIData) { - var name = GuessModelName(node.GamePath); - node = node.WithName(name != null ? $"IMC: {name}" : null); + var uiData = GuessModelUIData(node.GamePath); + node = node.WithUIData(uiData.PrependName("IMC: ")); } Nodes.Add((nint)imc, node); @@ -145,7 +146,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUIData); if (node != null) Nodes.Add((nint)tex, node); @@ -164,8 +165,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - if (WithNames) - node = node.WithName(GuessModelName(node.GamePath)); + if (WithUIData) + node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -173,9 +174,9 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithNames - ? mtrlNode.WithName((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) - ?? $"Material #{i}") + node.Children.Add(WithUIData + ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) + ?? $"Material #{i}", mtrlNode.Icon) : mtrlNode); } @@ -225,15 +226,15 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithUIData); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); - var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + node.Children.Add(WithUIData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + var shpkFile = WithUIData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = WithUIData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { @@ -241,7 +242,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (texNode == null) continue; - if (WithNames) + if (WithUIData) { string? name = null; if (shpk != null) @@ -259,7 +260,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } } - node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); + node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); } else { @@ -280,7 +281,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonResourceHandle, node); @@ -295,7 +296,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); @@ -308,7 +309,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return fullPath; var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); - if (relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) + if (!RedactExternalPaths || relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) return fullPath.Exists ? fullPath : FullPath.Empty; return FullPath.Empty; @@ -351,7 +352,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide } : false; - private string? GuessModelName(Utf8GamePath gamePath) + private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); // Weapons intentionally left out. @@ -359,23 +360,24 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (isEquipment) foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) { - return Slot switch + var name = Slot switch { EquipSlot.RFinger => "R: ", EquipSlot.LFinger => "L: ", _ => string.Empty, } + item.Name.ToString(); + return new(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } - var nameFromPath = GuessNameFromPath(gamePath); - if (nameFromPath != null) - return nameFromPath; + var dataFromPath = GuessUIDataFromPath(gamePath); + if (dataFromPath.Name != null) + return dataFromPath; - return isEquipment ? Slot.ToName() : null; + return isEquipment ? new(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) : new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private string? GuessNameFromPath(Utf8GamePath gamePath) + private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -383,10 +385,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return name; + return new(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return null; + return new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index bceda36c..17584787 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.String.Classes; +using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; namespace Penumbra.Interop.ResourceTree; public class ResourceNode { public readonly string? Name; + public readonly ChangedItemIcon Icon; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -18,9 +20,11 @@ public class ResourceNode public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, + ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -35,10 +39,11 @@ public class ResourceNode Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -50,9 +55,10 @@ public class ResourceNode Children = new List(); } - private ResourceNode(string? name, ResourceNode originalResourceNode) + private ResourceNode(UIData uiData, ResourceNode originalResourceNode) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = originalResourceNode.Type; ObjectAddress = originalResourceNode.ObjectAddress; ResourceHandle = originalResourceNode.ResourceHandle; @@ -64,6 +70,15 @@ public class ResourceNode Children = originalResourceNode.Children; } - public ResourceNode WithName(string? name) - => string.Equals(Name, name, StringComparison.Ordinal) ? this : new ResourceNode(name, this); + public ResourceNode WithUIData(string? name, ChangedItemIcon icon) + => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new(name, icon), this); + + public ResourceNode WithUIData(UIData uiData) + => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); + + public readonly record struct UIData(string? Name, ChangedItemIcon Icon) + { + public readonly UIData PrependName(string prefix) + => Name == null ? this : new(prefix + Name, Icon); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index f3e6ca51..c6e90c6b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -56,12 +56,12 @@ public class ResourceTree var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); + Nodes.Add(globalContext.WithUIData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); + Nodes.Add(globalContext.WithUIData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -92,15 +92,15 @@ public class ResourceTree var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithNames - ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithNames - ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); } @@ -117,11 +117,11 @@ public class ResourceTree var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); + Nodes.Add(globalContext.WithUIData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + Nodes.Add(globalContext.WithUIData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -133,11 +133,11 @@ public class ResourceTree { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (skpNode != null) - nodes.Add(context.WithNames ? skpNode.WithName($"{prefix}Skeleton #{i} Parameters") : skpNode); + nodes.Add(context.WithUIData ? skpNode.WithUIData($"{prefix}Skeleton #{i} Parameters", skpNode.Icon) : skpNode); } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index f416dc12..8d5318f2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -29,34 +29,35 @@ public class ResourceTreeFactory _actors = actors; } - public ResourceTree[] FromObjectTable(bool withNames = true) + public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); return cache.Characters - .Select(c => FromCharacter(c, cache, withNames)) + .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) .OfType() .ToArray(); } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); foreach (var character in characters) { - var tree = FromCharacter(character, cache, withNames); + var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); if (tree != null) yield return (character, tree); } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) - => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); + public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, + bool redactExternalPaths = true) + => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { if (!character.IsValid()) return null; @@ -73,7 +74,7 @@ public class ResourceTreeFactory var (name, related) = GetCharacterName(character, cache); var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e90c148e..890abfed 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -554,7 +554,8 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, + ChangedItemDrawer changedItemDrawer) : base(WindowBaseLabel) { _performance = performance; @@ -581,7 +582,7 @@ public partial class ModEditWindow : Window, IDisposable (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); - _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 8e996eb7..528f19c1 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -14,7 +14,8 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; + private readonly ResourceTreeFactory _treeFactory; + private readonly ChangedItemDrawer _changedItemDrawer; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; @@ -22,15 +23,16 @@ public class ResourceTreeViewer private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh, - Action drawActions) + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, + int actionCapacity, Action onRefresh, Action drawActions) { - _config = config; - _treeFactory = treeFactory; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = new HashSet(); + _config = config; + _treeFactory = treeFactory; + _changedItemDrawer = changedItemDrawer; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new HashSet(); } public void Draw() @@ -122,8 +124,24 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) - { - ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); + { + if (resourceNode.Children.Count > 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); + var offset = (ImGui.GetFrameHeight() - ImGui.CalcTextSize(icon).X) / 2; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + ImGui.TextUnformatted(icon); + ImGui.SameLine(0f, offset + ImGui.GetStyle().ItemInnerSpacing.X); + } + else + { + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + } + _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) { if (unfolded) @@ -170,7 +188,9 @@ public class ResourceTreeViewer ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); - } + } + + mutedColor.Dispose(); if (_actionCapacity > 0) { diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 7a81ec60..da4faa43 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -76,9 +76,11 @@ public class ChangedItemDrawer : IDisposable /// Draw the icon corresponding to the category of a changed item. public void DrawCategoryIcon(string name, object? data) + => DrawCategoryIcon(GetCategoryIcon(name, data)); + + public void DrawCategoryIcon(ChangedItemIcon iconType) { - var height = ImGui.GetFrameHeight(); - var iconType = GetCategoryIcon(name, data); + var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -216,27 +218,13 @@ public class ChangedItemDrawer : IDisposable } /// Obtain the icon category corresponding to a changed item. - private static ChangedItemIcon GetCategoryIcon(string name, object? obj) + internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) { var iconType = ChangedItemIcon.Unknown; switch (obj) { case EquipItem it: - iconType = it.Type.ToSlot() switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; + iconType = GetCategoryIcon(it.Type.ToSlot()); break; case ModelChara m: iconType = (CharacterBase.ModelType)m.Type switch @@ -259,6 +247,23 @@ public class ChangedItemDrawer : IDisposable return iconType; } + internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => ChangedItemIcon.Mainhand, + EquipSlot.OffHand => ChangedItemIcon.Offhand, + EquipSlot.Head => ChangedItemIcon.Head, + EquipSlot.Body => ChangedItemIcon.Body, + EquipSlot.Hands => ChangedItemIcon.Hands, + EquipSlot.Legs => ChangedItemIcon.Legs, + EquipSlot.Feet => ChangedItemIcon.Feet, + EquipSlot.Ears => ChangedItemIcon.Ears, + EquipSlot.Neck => ChangedItemIcon.Neck, + EquipSlot.Wrists => ChangedItemIcon.Wrists, + EquipSlot.RFinger => ChangedItemIcon.Finger, + _ => ChangedItemIcon.Unknown, + }; + /// Return more detailed object information in text, if it exists. private static bool GetChangedItemObject(object? obj, out string text) { diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 0ebc7dbd..c8f86333 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -10,10 +10,10 @@ public class OnScreenTab : ITab private readonly Configuration _config; private ResourceTreeViewer _viewer; - public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) + public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) { _config = config; - _viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); + _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { }); } public ReadOnlySpan Label From a17a1e9576dca2c2e5fa4e699759bf73e1995d07 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 2 Sep 2023 17:16:33 +0200 Subject: [PATCH 1145/2451] Resource Tree: Make skp child of sklb --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 11 ++++++++++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 ---- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 72a5be69..8e553091 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -283,12 +283,17 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); if (node != null) + { + var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); + if (skpNode != null) + node.Children.Add(skpNode); Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + } return node; } - public unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) { if (sklb->SkeletonParameterResourceHandle == null) return null; @@ -298,7 +303,11 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); if (node != null) + { + if (WithUIData) + node = node.WithUIData("Skeleton Parameters", node.Icon); Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + } return node; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index c6e90c6b..db3b287f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -134,10 +134,6 @@ public class ResourceTree var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); - - var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); - if (skpNode != null) - nodes.Add(context.WithUIData ? skpNode.WithUIData($"{prefix}Skeleton #{i} Parameters", skpNode.Icon) : skpNode); } } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 528f19c1..f0cf6030 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -8,7 +8,8 @@ using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; - +using System.Linq; + namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer @@ -125,7 +126,10 @@ public class ResourceTreeViewer var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - if (resourceNode.Children.Count > 0) + var unfoldable = debugMode + ? resourceNode.Children.Count > 0 + : resourceNode.Children.Any(child => !child.Internal); + if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); @@ -142,7 +146,7 @@ public class ResourceTreeViewer _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); - if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) + if (ImGui.IsItemClicked() && unfoldable) { if (unfolded) _unfolded.Remove(nodePathHash); From cca626449d134ab1b459d7b2000cb88e3ac0843d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 3 Sep 2023 05:50:51 +0200 Subject: [PATCH 1146/2451] Resource Tree: Fix shared model fold state --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index f0cf6030..90fcd820 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -85,7 +85,7 @@ public class ResourceTreeViewer (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0, 0); + DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31)); } } } From 2a2fa3bf1d00ed84086040b7e05a72ead2ae5318 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 3 Sep 2023 13:13:35 +0200 Subject: [PATCH 1147/2451] Some auto-formatting and ROS iteration for lookups. --- OtterGui | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 118 +++++++++--------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 +-- Penumbra/Interop/Structs/Material.cs | 4 + 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/OtterGui b/OtterGui index 728dd8c3..8c7a309d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9 +Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8e553091..24eea690 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -15,19 +16,20 @@ using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, - bool RedactExternalPaths) +internal record GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, + ModCollection Collection, int Skeleton, bool WithUiData, bool RedactExternalPaths) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUIData, RedactExternalPaths, Nodes, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUiData, RedactExternalPaths, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, - bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, + int Skeleton, bool WithUiData, bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) @@ -73,26 +75,28 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); } - private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal) + private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + Utf8GamePath gamePath, bool @internal) { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), + GetResourceHandleLength(resourceHandle), @internal); if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); return node; } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, - bool withName) + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, + bool withName) { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; - if (fullPath.InternalName.IsEmpty) - return null; - + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + return null; + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); fullPath = FilterFullPath(fullPath); @@ -100,14 +104,16 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], + fullPath, GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), + @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) @@ -130,7 +136,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - if (WithUIData) + if (WithUiData) { var uiData = GuessModelUIData(node.GamePath); node = node.WithUIData(uiData.PrependName("IMC: ")); @@ -146,7 +152,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUiData); if (node != null) Nodes.Add((nint)tex, node); @@ -161,11 +167,11 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false, false); if (node == null) return null; - if (WithUIData) + if (WithUiData) node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) @@ -174,7 +180,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithUIData + node.Children.Add(WithUiData ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) ?? $"Material #{i}", mtrlNode.Icon) : mtrlNode); @@ -191,33 +197,21 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide { if ((texFlags & 0x001F) != 0x001F) return (ushort)(texFlags & 0x001F); - else if ((texFlags & 0x03E0) != 0x03E0) + if ((texFlags & 0x03E0) != 0x03E0) return (ushort)((texFlags >> 5) & 0x001F); - else - return (ushort)((texFlags >> 10) & 0x001F); + + return (ushort)((texFlags >> 10) & 0x001F); } + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) - { - var textures = mtrl->Textures; - for (var i = 0; i < mtrl->TextureCount; ++i) - { - if (textures[i].ResourceHandle == handle) - return textures[i].Id; - } + => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) + ? p.Id + : null; - return null; - } static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - { - var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers; - for (var i = 0; i < shpk->SamplerCount; ++i) - { - if (samplers[i].Id == id) - return samplers[i].Crc; - } - - return null; - } + => new ReadOnlySpan(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s) + ? s.Crc + : null; if (mtrl == null) return null; @@ -225,29 +219,30 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var resource = mtrl->ResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithUIData); + + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false, WithUiData); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithUIData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); - var shpkFile = WithUIData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithUIData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + node.Children.Add(WithUiData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { - var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, + resource->TexIsDX11(i)); if (texNode == null) continue; - if (WithUIData) + if (WithUiData) { string? name = null; if (shpk != null) { - var index = GetTextureIndex(resource->TexSpace[i].Flags); + var index = GetTextureIndex(resource->TexSpace[i].Flags); uint? samplerId; if (index != 0x001F) samplerId = mtrl->Textures[index].Id; @@ -259,7 +254,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (samplerCrc.HasValue) name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } - } + } + node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); } else @@ -281,7 +277,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, + WithUiData); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -301,10 +298,11 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, + WithUiData); if (node != null) { - if (WithUIData) + if (WithUiData) node = node.WithUIData("Skeleton Parameters", node.Icon); Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } @@ -376,14 +374,16 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide _ => string.Empty, } + item.Name.ToString(); - return new(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } var dataFromPath = GuessUIDataFromPath(gamePath); if (dataFromPath.Name != null) return dataFromPath; - return isEquipment ? new(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) : new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return isEquipment + ? new ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) + : new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) @@ -394,10 +394,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return new(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index db3b287f..755103d7 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -56,12 +56,12 @@ public class ResourceTree var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithUIData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); + Nodes.Add(globalContext.WithUiData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithUIData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); + Nodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -92,14 +92,14 @@ public class ResourceTree var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithUIData + subObjectNodes.Add(globalContext.WithUiData ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithUIData + subObjectNodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); } @@ -117,11 +117,11 @@ public class ResourceTree var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithUIData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); + Nodes.Add(globalContext.WithUiData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithUIData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); + Nodes.Add(globalContext.WithUiData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -133,7 +133,7 @@ public class ResourceTree { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); } } } diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 7b66531c..3a204c75 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; @@ -41,4 +42,7 @@ public unsafe struct Material [FieldOffset( 0x10 )] public uint SamplerFlags; } + + public ReadOnlySpan TextureSpan + => new(Textures, TextureCount); } \ No newline at end of file From 94a0a3902c8c6b7e71b57b80355fb52a7e3c8f4c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 3 Sep 2023 21:56:22 +0200 Subject: [PATCH 1148/2451] Resource Tree: Use `/`s for game actual paths --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 90fcd820..7da0275b 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -182,9 +182,9 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); if (resourceNode.FullPath.FullName.Length > 0) { - ImGui.Selectable(resourceNode.FullPath.ToString(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(resourceNode.FullPath.ToString()); + ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); ImGuiUtil.HoverTooltip($"{resourceNode.FullPath}\n\nClick to copy to clipboard."); } else From 32608ea45b1c51b8ebaf08d21fbbe2831a183022 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 5 Sep 2023 12:53:53 +0200 Subject: [PATCH 1149/2451] Skin Fixer: Switch to a passive approach. Do not load skin.shpk for ourselves as it causes a race condition. Instead, inspect the materials' ShPk names. --- .../Communication/CreatedCharacterBase.cs | 3 - Penumbra/Communication/MtrlShpkLoaded.cs | 24 +++ .../Interop/PathResolving/SubfileHelper.cs | 25 ++-- Penumbra/Interop/Services/SkinFixer.cs | 137 +++++++----------- Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/UI/Tabs/DebugTab.cs | 2 +- Penumbra/Util/Unit.cs | 26 ++++ 7 files changed, 126 insertions(+), 95 deletions(-) create mode 100644 Penumbra/Communication/MtrlShpkLoaded.cs create mode 100644 Penumbra/Util/Unit.cs diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index 48ba86a5..69d84ce2 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -16,9 +16,6 @@ public sealed class CreatedCharacterBase : EventWrapper Api = int.MinValue, - - /// - SkinFixer = 0, } public CreatedCharacterBase() diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs new file mode 100644 index 00000000..4b5600c9 --- /dev/null +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -0,0 +1,24 @@ +using System; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Parameter is the material resource handle for which the shader package has been loaded. +/// Parameter is the associated game object. +/// +public sealed class MtrlShpkLoaded : EventWrapper, MtrlShpkLoaded.Priority> +{ + public enum Priority + { + /// + SkinFixer = 0, + } + + public MtrlShpkLoaded() + : base(nameof(MtrlShpkLoaded)) + { } + + public void Invoke(nint mtrlResourceHandle, nint gameObject) + => Invoke(this, mtrlResourceHandle, gameObject); +} diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 05f42220..ffa3ac24 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -24,22 +25,24 @@ namespace Penumbra.Interop.PathResolving; /// public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> { - private readonly PerformanceTracker _performance; - private readonly ResourceLoader _loader; - private readonly GameEventManager _events; + private readonly PerformanceTracker _performance; + private readonly ResourceLoader _loader; + private readonly GameEventManager _events; + private readonly CommunicatorService _communicator; private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary _subFileCollection = new(); - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events) + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator) { SignatureHelper.Initialise(this); - _performance = performance; - _loader = loader; - _events = events; + _performance = performance; + _loader = loader; + _events = events; + _communicator = communicator; _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); @@ -150,10 +153,12 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection SkinShpkName + => "skin.shpk"u8; [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; @@ -48,11 +40,12 @@ public sealed unsafe class SkinFixer : IDisposable private readonly ResourceLoader _resources; private readonly CharacterUtility _utility; - // CharacterBase to ShpkHandle - private readonly ConcurrentDictionary _skinShpks = new(); + // MaterialResourceHandle set + private readonly ConcurrentDictionary _moddedSkinShpkMaterials = new(); private readonly object _lock = new(); - + + // ConcurrentDictionary.Count uses a lock in its current implementation. private int _moddedSkinShpkCount = 0; private ulong _slowPathCallDelta = 0; @@ -68,83 +61,65 @@ public sealed unsafe class SkinFixer : IDisposable _resources = resources; _utility = utility; _communicator = communicator; - _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); - _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); - _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); + _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; _onRenderMaterialHook.Enable(); - } - + } + public void Dispose() { - _onRenderMaterialHook.Dispose(); - _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); - _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; - foreach (var skinShpk in _skinShpks.Values) - skinShpk.Dispose(); - _skinShpks.Clear(); + _onRenderMaterialHook.Dispose(); + _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor; + _moddedSkinShpkMaterials.Clear(); _moddedSkinShpkCount = 0; - } - + } + public ulong GetAndResetSlowPathCallDelta() - => Interlocked.Exchange(ref _slowPathCallDelta, 0); - - private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) - { - if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) - return; - - Task.Run(() => - { - var skinShpk = SafeResourceHandle.CreateInvalid(); - try - { - var data = collection.ToResolveData(gameObject); - if (data.Valid) - { - var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); - skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - } - - if (!skinShpk.IsInvalid) - { - if (_skinShpks.TryAdd(drawObject, skinShpk)) - { - if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); - } - else - { - skinShpk.Dispose(); - } - } - }); - } - - private void OnCharacterBaseDestructor(nint characterBase) - { - if (!_skinShpks.Remove(characterBase, out var skinShpk)) - return; - - var handle = skinShpk.ResourceHandle; - skinShpk.Dispose(); - if ((nint)handle != _utility.DefaultSkinShpkResource) - Interlocked.Decrement(ref _moddedSkinShpkCount); + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource) + { + if (mtrlResource == null) + return false; + + var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString); + return SkinShpkName.SequenceEqual(shpkName); + } + + private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + { + var mtrl = (Structs.MtrlResource*)mtrlResourceHandle; + var shpk = mtrl->ShpkResourceHandle; + if (shpk == null) + return; + + if (!IsSkinMaterial(mtrl)) + return; + + if ((nint)shpk != _utility.DefaultSkinShpkResource) + { + if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle, Unit.Instance)) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + } + + private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) + { + if (_moddedSkinShpkMaterials.TryRemove((nint)handle, out _)) + Interlocked.Decrement(ref _moddedSkinShpkCount); } private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) + if (!Enabled || _moddedSkinShpkCount == 0) return _onRenderMaterialHook!.Original(human, param); - var material = param->Model->Materials[param->MaterialIndex]; - var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; - if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) + var material = param->Model->Materials[param->MaterialIndex]; + var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle; + if (!IsSkinMaterial(mtrlResource)) return _onRenderMaterialHook!.Original(human, param); Interlocked.Increment(ref _slowPathCallDelta); @@ -158,7 +133,7 @@ public sealed unsafe class SkinFixer : IDisposable { try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle; return _onRenderMaterialHook!.Original(human, param); } finally diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 728b391c..97340f6b 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -24,6 +24,9 @@ public class CommunicatorService : IDisposable /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); + /// + public readonly MtrlShpkLoaded MtrlShpkLoaded = new(); + /// public readonly ModDataChanged ModDataChanged = new(); @@ -75,6 +78,7 @@ public class CommunicatorService : IDisposable TemporaryGlobalModChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); + MtrlShpkLoaded.Dispose(); ModDataChanged.Dispose(); ModOptionChanged.Dispose(); ModDiscoveryStarted.Dispose(); diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index c24d64fa..d02da883 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -603,7 +603,7 @@ public class DebugTab : Window, ITab ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); } using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, diff --git a/Penumbra/Util/Unit.cs b/Penumbra/Util/Unit.cs new file mode 100644 index 00000000..9b8f4b1e --- /dev/null +++ b/Penumbra/Util/Unit.cs @@ -0,0 +1,26 @@ +using System; + +namespace Penumbra.Util; + +/// +/// An empty structure. Can be used as value of a concurrent dictionary, to use it as a set. +/// +public readonly struct Unit : IEquatable +{ + public static readonly Unit Instance = new(); + + public bool Equals(Unit other) + => true; + + public override bool Equals(object? obj) + => obj is Unit; + + public override int GetHashCode() + => 0; + + public static bool operator ==(Unit left, Unit right) + => true; + + public static bool operator !=(Unit left, Unit right) + => false; +} From 0e0733dab02b86db7e68cdec0612098fc3982df3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 5 Sep 2023 14:48:06 +0200 Subject: [PATCH 1150/2451] Some formatting, use ConcurrentSet explicitly for clarity. --- OtterGui | 2 +- .../Interop/PathResolving/SubfileHelper.cs | 14 +-- Penumbra/Interop/Services/SkinFixer.cs | 113 ++++++++---------- Penumbra/Util/Unit.cs | 26 ---- 4 files changed, 61 insertions(+), 94 deletions(-) delete mode 100644 Penumbra/Util/Unit.cs diff --git a/OtterGui b/OtterGui index 8c7a309d..86ec4d72 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428 +Subproject commit 86ec4d72c9c9ed57aa7be4a7d0c81069c5b94ad7 diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index ffa3ac24..c0b8c5e3 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -11,7 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -27,7 +27,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection _mtrlData = new(() => ResolveData.Invalid); @@ -41,7 +41,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollectionFileType) { case ResourceType.Mtrl: - case ResourceType.Avfx: + case ResourceType.Avfx: if (handle->FileSize == 0) _subFileCollection[(nint)handle] = resolveData; @@ -153,11 +153,11 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection SkinShpkName + public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] @@ -37,90 +35,85 @@ public sealed unsafe class SkinFixer : IDisposable private readonly GameEventManager _gameEvents; private readonly CommunicatorService _communicator; - private readonly ResourceLoader _resources; private readonly CharacterUtility _utility; - - // MaterialResourceHandle set - private readonly ConcurrentDictionary _moddedSkinShpkMaterials = new(); + + // MaterialResourceHandle set + private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); private readonly object _lock = new(); - + // ConcurrentDictionary.Count uses a lock in its current implementation. - private int _moddedSkinShpkCount = 0; - private ulong _slowPathCallDelta = 0; + private int _moddedSkinShpkCount; + private ulong _slowPathCallDelta; public bool Enabled { get; internal set; } = true; public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) + public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator) { SignatureHelper.Initialise(this); _gameEvents = gameEvents; - _resources = resources; _utility = utility; - _communicator = communicator; - _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator = communicator; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; _onRenderMaterialHook.Enable(); - } - + } + public void Dispose() { - _onRenderMaterialHook.Dispose(); - _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _onRenderMaterialHook.Dispose(); + _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); _gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor; _moddedSkinShpkMaterials.Clear(); _moddedSkinShpkCount = 0; - } - + } + public ulong GetAndResetSlowPathCallDelta() - => Interlocked.Exchange(ref _slowPathCallDelta, 0); - - private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource) - { - if (mtrlResource == null) - return false; - - var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString); - return SkinShpkName.SequenceEqual(shpkName); - } - - private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) - { - var mtrl = (Structs.MtrlResource*)mtrlResourceHandle; - var shpk = mtrl->ShpkResourceHandle; - if (shpk == null) - return; - - if (!IsSkinMaterial(mtrl)) - return; - - if ((nint)shpk != _utility.DefaultSkinShpkResource) - { - if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle, Unit.Instance)) - Interlocked.Increment(ref _moddedSkinShpkCount); - } - } - - private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) - { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle, out _)) - Interlocked.Decrement(ref _moddedSkinShpkCount); + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource) + { + if (mtrlResource == null) + return false; + + var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString); + return SkinShpkName.SequenceEqual(shpkName); + } + + private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + { + var mtrl = (Structs.MtrlResource*)mtrlResourceHandle; + var shpk = mtrl->ShpkResourceHandle; + if (shpk == null) + return; + + if (!IsSkinMaterial(mtrl) || (nint)shpk == _utility.DefaultSkinShpkResource) + return; + + if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + + private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) + { + if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _moddedSkinShpkCount); } private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. if (!Enabled || _moddedSkinShpkCount == 0) - return _onRenderMaterialHook!.Original(human, param); + return _onRenderMaterialHook.Original(human, param); - var material = param->Model->Materials[param->MaterialIndex]; - var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle; - if (!IsSkinMaterial(mtrlResource)) - return _onRenderMaterialHook!.Original(human, param); + var material = param->Model->Materials[param->MaterialIndex]; + var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle; + if (!IsSkinMaterial(mtrlResource)) + return _onRenderMaterialHook.Original(human, param); Interlocked.Increment(ref _slowPathCallDelta); @@ -134,7 +127,7 @@ public sealed unsafe class SkinFixer : IDisposable try { _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle; - return _onRenderMaterialHook!.Original(human, param); + return _onRenderMaterialHook.Original(human, param); } finally { diff --git a/Penumbra/Util/Unit.cs b/Penumbra/Util/Unit.cs deleted file mode 100644 index 9b8f4b1e..00000000 --- a/Penumbra/Util/Unit.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Penumbra.Util; - -/// -/// An empty structure. Can be used as value of a concurrent dictionary, to use it as a set. -/// -public readonly struct Unit : IEquatable -{ - public static readonly Unit Instance = new(); - - public bool Equals(Unit other) - => true; - - public override bool Equals(object? obj) - => obj is Unit; - - public override int GetHashCode() - => 0; - - public static bool operator ==(Unit left, Unit right) - => true; - - public static bool operator !=(Unit left, Unit right) - => false; -} From a890258cf5568f013fac1795dfa3af80a6efe0cc Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 5 Sep 2023 12:51:00 +0000 Subject: [PATCH 1151/2451] [CI] Updating repo.json for testing_0.7.3.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 84db423d..dd0b0d67 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.4", + "TestingAssemblyVersion": "0.7.3.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6cc89f3e7c33143f950a877d8ac38e41a9ec47e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 6 Sep 2023 21:15:05 +0200 Subject: [PATCH 1152/2451] Add Emotes to Changed Items. --- Penumbra.GameData | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 78 ++++++++++++++++++++++---------- Penumbra/UI/Tabs/DebugTab.cs | 72 ++++++++++++++++++++++++----- 3 files changed, 115 insertions(+), 37 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 43f6737d..bd7e7926 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 43f6737d4baa7988a5fe23096f20827bc54e6812 +Subproject commit bd7e79262a0db06e27e61b98ab0a31ebb881be2a diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index da4faa43..529a6246 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -27,22 +27,23 @@ public class ChangedItemDrawer : IDisposable [Flags] public enum ChangedItemIcon : uint { - Head = 0x0001, - Body = 0x0002, - Hands = 0x0004, - Legs = 0x0008, - Feet = 0x0010, - Ears = 0x0020, - Neck = 0x0040, - Wrists = 0x0080, - Finger = 0x0100, - Monster = 0x0200, - Demihuman = 0x0400, - Customization = 0x0800, - Action = 0x1000, - Mainhand = 0x2000, - Offhand = 0x4000, - Unknown = 0x8000, + Head = 0x00_00_01, + Body = 0x00_00_02, + Hands = 0x00_00_04, + Legs = 0x00_00_08, + Feet = 0x00_00_10, + Ears = 0x00_00_20, + Neck = 0x00_00_40, + Wrists = 0x00_00_80, + Finger = 0x00_01_00, + Monster = 0x00_02_00, + Demihuman = 0x00_04_00, + Customization = 0x00_08_00, + Action = 0x00_10_00, + Mainhand = 0x00_20_00, + Offhand = 0x00_40_00, + Unknown = 0x00_80_00, + Emote = 0x01_00_00, } public const ChangedItemIcon AllFlags = (ChangedItemIcon)0xFFFF; @@ -51,10 +52,11 @@ public class ChangedItemDrawer : IDisposable private readonly Configuration _config; private readonly ExcelSheet _items; private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); + private readonly Dictionary _icons = new(16); private float _smallestIconWidth; - public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) + public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, + Configuration config) { _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); @@ -164,6 +166,7 @@ public class ChangedItemDrawer : IDisposable ChangedItemIcon.Offhand, ChangedItemIcon.Customization, ChangedItemIcon.Action, + ChangedItemIcon.Emote, ChangedItemIcon.Monster, ChangedItemIcon.Demihuman, ChangedItemIcon.Unknown, @@ -182,14 +185,12 @@ public class ChangedItemDrawer : IDisposable using var popup = ImRaii.ContextPopupItem(type.ToString()); if (popup) - { if (ImGui.MenuItem("Enable Only This")) { _config.ChangedItemFilter = type; _config.Save(); ImGui.CloseCurrentPopup(); } - } if (ImGui.IsItemHovered()) { @@ -238,6 +239,8 @@ public class ChangedItemDrawer : IDisposable { if (name.StartsWith("Action: ")) iconType = ChangedItemIcon.Action; + else if (name.StartsWith("Emote: ")) + iconType = ChangedItemIcon.Emote; else if (name.StartsWith("Customization: ")) iconType = ChangedItemIcon.Customization; break; @@ -306,6 +309,7 @@ public class ChangedItemDrawer : IDisposable ChangedItemIcon.Demihuman => "Demi-Human", ChangedItemIcon.Customization => "Customization", ChangedItemIcon.Action => "Action", + ChangedItemIcon.Emote => "Emote", ChangedItemIcon.Mainhand => "Weapon (Mainhand)", ChangedItemIcon.Offhand => "Weapon (Offhand)", _ => "Other", @@ -354,21 +358,47 @@ public class ChangedItemDrawer : IDisposable Add(ChangedItemIcon.Demihuman, textureProvider.GetTextureFromGame("ui/icon/062000/062041_hr1.tex", true)); Add(ChangedItemIcon.Customization, textureProvider.GetTextureFromGame("ui/icon/062000/062043_hr1.tex", true)); Add(ChangedItemIcon.Action, textureProvider.GetTextureFromGame("ui/icon/062000/062001_hr1.tex", true)); + Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, uiBuilder)); + Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, uiBuilder)); Add(AllFlags, textureProvider.GetTextureFromGame("ui/icon/114000/114052_hr1.tex", true)); + _smallestIconWidth = _icons.Values.Min(i => i.Width); + + return true; + } + + private static unsafe TextureWrap? LoadUnknownTexture(IDataManager gameData, UiBuilder uiBuilder) + { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) - return true; + return null; var image = unk.GetRgbaImageData(); var bytes = new byte[unk.Header.Height * unk.Header.Height * 4]; var diff = 2 * (unk.Header.Height - unk.Header.Width); for (var y = 0; y < unk.Header.Height; ++y) image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); - Add(ChangedItemIcon.Unknown, uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4)); - _smallestIconWidth = _icons.Values.Min(i => i.Width); + return uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4); + } - return true; + private static unsafe TextureWrap? LoadEmoteTexture(IDataManager gameData, UiBuilder uiBuilder) + { + var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); + if (emote == null) + return null; + + var image2 = emote.GetRgbaImageData(); + fixed (byte* ptr = image2) + { + var color = (uint*)ptr; + for (var i = 0; i < image2.Length / 4; ++i) + { + if (color[i] == 0xFF000000) + image2[i * 4 + 3] = 0; + } + } + + return uiBuilder.LoadImageRaw(image2, emote.Header.Width, emote.Header.Height, 4); } } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index d02da883..72122722 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -66,6 +67,7 @@ public class DebugTab : Window, ITab private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; private readonly SkinFixer _skinFixer; + private readonly IdentifierService _identifier; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -73,7 +75,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer) + TextureManager textureManager, SkinFixer skinFixer, IdentifierService identifier) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -107,6 +109,7 @@ public class DebugTab : Window, ITab _framework = framework; _textureManager = textureManager; _skinFixer = skinFixer; + _identifier = identifier; } public ReadOnlySpan Label @@ -138,12 +141,10 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); - DrawStainTemplates(); + DrawData(); ImGui.NewLine(); DrawDebugTabMetaLists(); ImGui.NewLine(); - DrawDebugResidentResources(); - ImGui.NewLine(); DrawResourceProblems(); ImGui.NewLine(); DrawPlayerModelInfo(); @@ -370,7 +371,9 @@ public class DebugTab : Window, ITab { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); - ImGuiUtil.DrawTableColumn((obj.Address == nint.Zero) ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); + ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero + ? string.Empty + : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); @@ -546,9 +549,49 @@ public class DebugTab : Window, ITab } } + private void DrawData() + { + if (!ImGui.CollapsingHeader("Game Data")) + return; + + DrawEmotes(); + DrawStainTemplates(); + } + + private string _emoteSearchFile = string.Empty; + private string _emoteSearchName = string.Empty; + + private void DrawEmotes() + { + using var mainTree = TreeNode("Emotes"); + if (!mainTree) + return; + + ImGui.InputText("File Name", ref _emoteSearchFile, 256); + ImGui.InputText("Emote Name", ref _emoteSearchName, 256); + using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); + if (!table) + return; + + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + var dummy = ImGuiClip.FilteredClippedDraw(_identifier.AwaitedService.Emotes, skips, + p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase) + && (_emoteSearchName.Length == 0 + || p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))), + p => + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(p.Key); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(string.Join(", ", p.Value.Select(v => v.Name.ToDalamudString().TextValue))); + }); + ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); + } + private void DrawStainTemplates() { - if (!ImGui.CollapsingHeader("Staining Templates")) + using var mainTree = TreeNode("Staining Templates"); + if (!mainTree) return; foreach (var (key, data) in _stains.StmFile.Entries) @@ -625,16 +668,15 @@ public class DebugTab : Window, ITab ImGui.TableNextRow(); continue; } + UiHelpers.Text(resource); ImGui.TableNextColumn(); - var data = (nint)ResourceHandle.GetData(resource); + var data = (nint)ResourceHandle.GetData(resource); var length = ResourceHandle.GetLength(resource); if (ImGui.Selectable($"0x{data:X}")) - { if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - } ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); ImGui.TableNextColumn(); @@ -655,7 +697,9 @@ public class DebugTab : Window, ITab ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); } else + { ImGui.TableNextColumn(); + } } } @@ -679,7 +723,8 @@ public class DebugTab : Window, ITab /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { - if (!ImGui.CollapsingHeader("Resident Resources")) + using var tree = TreeNode("Resident Resources"); + if (!tree) return; if (_residentResources.Address == null || _residentResources.Address->NumResources == 0) @@ -703,8 +748,10 @@ public class DebugTab : Window, ITab private static void DrawCopyableAddress(string label, nint address) { using (var _ = PushFont(UiBuilder.MonoFont)) + { if (ImGui.Selectable($"0x{address:X16} {label}")) ImGui.SetClipboardText($"{address:X16}"); + } ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); } @@ -789,9 +836,10 @@ public class DebugTab : Window, ITab if (!header) return; - DrawCopyableAddress("CharacterUtility", _characterUtility.Address); + DrawCopyableAddress("CharacterUtility", _characterUtility.Address); DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); - DrawCopyableAddress("Device", Device.Instance()); + DrawCopyableAddress("Device", Device.Instance()); + DrawDebugResidentResources(); } /// Draw resources with unusual reference count. From 4bd3fd357f1026f3b568f1c62f230af487e33464 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 6 Sep 2023 19:19:57 +0000 Subject: [PATCH 1153/2451] [CI] Updating repo.json for testing_0.7.3.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index dd0b0d67..185481f9 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.5", + "TestingAssemblyVersion": "0.7.3.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ed243df4f34544f06444288b0d2d0309c5a2b77a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Sep 2023 13:59:36 +0200 Subject: [PATCH 1154/2451] Fix changed item flags for emotes. --- Penumbra/UI/ChangedItemDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 529a6246..902f6671 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -46,7 +46,7 @@ public class ChangedItemDrawer : IDisposable Emote = 0x01_00_00, } - public const ChangedItemIcon AllFlags = (ChangedItemIcon)0xFFFF; + public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; private readonly Configuration _config; From 40eb0c81b8a280287a1e4ab8ca938a65307e004a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Sep 2023 13:59:47 +0200 Subject: [PATCH 1155/2451] Update GameData for new parsing. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bd7e7926..635d4c72 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bd7e79262a0db06e27e61b98ab0a31ebb881be2a +Subproject commit 635d4c72e84da65a20a2d22d758dd8061bd8babf From 569fa06e183c299d3bc1c03636f5200a2d24eeee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Sep 2023 14:04:14 +0200 Subject: [PATCH 1156/2451] Fix CS update creating ambiguous reference. --- Penumbra/Interop/PathResolving/MetaState.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index cdf28bbd..f7a754fb 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -13,6 +13,7 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.Interop.PathResolving; From 8eaf14d93247298b61b38445a9d94afaf472e812 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Sep 2023 16:24:07 +0200 Subject: [PATCH 1157/2451] Add Player and Interface to quick select collections and rework their tooltips and names slightly. --- Penumbra/Api/IpcTester.cs | 1 - Penumbra/UI/Classes/CollectionSelectHeader.cs | 121 +++++++++++++----- 2 files changed, 92 insertions(+), 30 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index fea91b0e..cbd0cc8b 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; -using System.Reflection.Metadata.Ecma335; using System.Threading.Tasks; using Dalamud.Utility; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 5c4570bf..f71ae323 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Numerics; using ImGuiNET; @@ -5,6 +6,7 @@ using OtterGui.Raii; using OtterGui; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; using Penumbra.UI.CollectionTab; using Penumbra.UI.ModsTab; @@ -16,11 +18,14 @@ public class CollectionSelectHeader private readonly ActiveCollections _activeCollections; private readonly TutorialService _tutorial; private readonly ModFileSystemSelector _selector; + private readonly CollectionResolver _resolver; - public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector) + public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector, + CollectionResolver resolver) { _tutorial = tutorial; _selector = selector; + _resolver = resolver; _activeCollections = collectionManager.Active; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); } @@ -28,16 +33,18 @@ public class CollectionSelectHeader /// Draw the header line that can quick switch between collections. public void Draw(bool spacing) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); - var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 8f, 0); - + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); + var comboWidth = ImGui.GetContentRegionAvail().X / 4f; + var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) { - DrawDefaultCollectionButton(3 * buttonSize); - ImGui.SameLine(); - DrawInheritedCollectionButton(3 * buttonSize); - ImGui.SameLine(); - _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value()); + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo()); + DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo()); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo()); + DrawCollectionButton(buttonSize, GetInheritedCollectionInfo()); + + _collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value()); } _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); @@ -46,31 +53,87 @@ public class CollectionSelectHeader ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } - private void DrawDefaultCollectionButton(Vector2 width) + private enum CollectionState { - var name = $"{TutorialService.DefaultCollection} ({_activeCollections.Default.Name})"; - var isCurrent = _activeCollections.Default == _activeCollections.Current; - var isEmpty = _activeCollections.Default == ModCollection.Empty; - var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." - : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." - : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; - if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) - _activeCollections.SetCollection(_activeCollections.Default, CollectionType.Current); + Empty, + Selected, + Unavailable, + Available, } - private void DrawInheritedCollectionButton(Vector2 width) + private CollectionState CheckCollection(ModCollection? collection, bool inheritance = false) { - var noModSelected = _selector.Selected == null; - var collection = _selector.SelectedSettingCollection; - var modInherited = collection != _activeCollections.Current; - var (name, tt) = (noModSelected, modInherited) switch + if (collection == null) + return CollectionState.Unavailable; + if (collection == ModCollection.Empty) + return CollectionState.Empty; + if (collection == _activeCollections.Current) + return inheritance ? CollectionState.Unavailable : CollectionState.Selected; + + return CollectionState.Available; + } + + private (ModCollection?, string, string, bool) GetDefaultCollectionInfo() + { + var collection = _activeCollections.Default; + return CheckCollection(collection) switch { - (true, _) => ("Inherited Collection", "No mod selected."), - (false, true) => ($"Inherited Collection ({collection.Name})", - "Set the current collection to the collection the selected mod inherits its settings from."), - (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), + CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), + CollectionState.Selected => (collection, collection.Name, + "The configured base collection is already selected as the current collection.", true), + CollectionState.Available => (collection, collection.Name, + $"Select the configured base collection {collection.Name} as the current collection.", false), + _ => throw new Exception("Can not happen."), }; - if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) - _activeCollections.SetCollection(collection, CollectionType.Current); + } + + private (ModCollection?, string, string, bool) GetPlayerCollectionInfo() + { + var collection = _resolver.PlayerCollection(); + return CheckCollection(collection) switch + { + CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), + CollectionState.Selected => (collection, collection.Name, + "The collection configured to apply to the loaded player character is already selected as the current collection.", true), + CollectionState.Available => (collection, collection.Name, + $"Select the collection {collection.Name} that applies to the loaded player character as the current collection.", false), + _ => throw new Exception("Can not happen."), + }; + } + + private (ModCollection?, string, string, bool) GetInterfaceCollectionInfo() + { + var collection = _activeCollections.Interface; + return CheckCollection(collection) switch + { + CollectionState.Empty => (collection, "None", "The interface collection is configured to use no mods.", true), + CollectionState.Selected => (collection, collection.Name, + "The configured interface collection is already selected as the current collection.", true), + CollectionState.Available => (collection, collection.Name, + $"Select the configured interface collection {collection.Name} as the current collection.", false), + _ => throw new Exception("Can not happen."), + }; + } + + private (ModCollection?, string, string, bool) GetInheritedCollectionInfo() + { + var collection = _selector.Selected == null ? null : _selector.SelectedSettingCollection; + return CheckCollection(collection, true) switch + { + CollectionState.Unavailable => (null, "Not Inherited", + "The settings of the selected mod are not inherited from another collection.", true), + CollectionState.Available => (collection, collection!.Name, + $"Select the collection {collection!.Name} from which the selected mod inherits its settings as the current collection.", + false), + _ => throw new Exception("Can not happen."), + }; + } + + private void DrawCollectionButton(Vector2 buttonWidth, (ModCollection?, string, string, bool) tuple) + { + var (collection, name, tooltip, disabled) = tuple; + if (ImGuiUtil.DrawDisabledButton(name, buttonWidth, tooltip, disabled)) + _activeCollections.SetCollection(collection!, CollectionType.Current); + ImGui.SameLine(); } } From 4fdb89ce625ff7c48460884e74ec5e5dfe44edbf Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 11 Sep 2023 14:26:51 +0000 Subject: [PATCH 1158/2451] [CI] Updating repo.json for testing_0.7.3.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 185481f9..21ff7756 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.6", + "TestingAssemblyVersion": "0.7.3.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d21cba466933f2883148cf624ee2671604d074df Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Sep 2023 17:06:29 +0200 Subject: [PATCH 1159/2451] Allow drag & drop of multiple mods or folders with Control. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 12 +++-- Penumbra/UI/ModsTab/ModPanel.cs | 56 ++++++++++++++++++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index 86ec4d72..9c7a3147 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 86ec4d72c9c9ed57aa7be4a7d0c81069c5b94ad7 +Subproject commit 9c7a3147f6e64e93074bc318a52b0723efc6ffff diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 6eecf36a..8d978413 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -42,7 +42,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector ImGui.GetStyle().ItemSpacing.X) ImGui.GetWindowDrawList().AddText(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value(), priorityString); } @@ -341,7 +341,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 36.5f * ImGui.GetTextLineHeightWithSpacing()), () => { ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); ImGui.TextUnformatted("Mod Management"); @@ -380,6 +380,10 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Lexicographical) .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); - Select(leaf); + Select(leaf, AllowMultipleSelection); _lastSelectedDirectory = string.Empty; } diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index d85f77ff..f0d28dab 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,5 +1,11 @@ using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; using Penumbra.Mods; using Penumbra.UI.AdvancedWindow; @@ -7,10 +13,10 @@ namespace Penumbra.UI.ModsTab; public class ModPanel : IDisposable { - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) { @@ -24,7 +30,10 @@ public class ModPanel : IDisposable public void Draw() { if (!_valid) + { + DrawMultiSelection(); return; + } _header.Draw(); _tabs.Draw(_mod); @@ -36,6 +45,45 @@ public class ModPanel : IDisposable _header.Dispose(); } + private void DrawMultiSelection() + { + if (_selector.SelectedPaths.Count == 0) + return; + + var sizeType = ImGui.GetFrameHeight(); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; + var sizeMods = availableSizePercent * 35; + var sizeFolders = availableSizePercent * 65; + + ImGui.NewLine(); + ImGui.TextUnformatted("Currently Selected Objects"); + ImGui.Separator(); + using var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg); + ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); + ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); + + var i = 0; + foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) + .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) + { + using var id = ImRaii.PushId(i++); + ImGui.TableNextColumn(); + var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); + if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) + _selector.RemovePathFromMultiselection(path); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(fullName); + } + } + + private bool _valid; private Mod _mod = null!; From b352373a52196e295b0825d419fbf213c95c4f96 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 13 Sep 2023 15:09:13 +0000 Subject: [PATCH 1160/2451] [CI] Updating repo.json for testing_0.7.3.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 21ff7756..cceb2f29 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.7", + "TestingAssemblyVersion": "0.7.3.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 7431db2a0816d6e22c0aa9f4788e2572d3ff7364 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Sep 2023 20:09:00 +0200 Subject: [PATCH 1161/2451] Fix click check for selectables. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9c7a3147..51ae6032 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9c7a3147f6e64e93074bc318a52b0723efc6ffff +Subproject commit 51ae60322c22c9d9b49365ad0b9fd60dc3d50296 From e26873934b77ef5477b19c3e9c7abf5ab1a0e833 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 13 Sep 2023 18:11:56 +0000 Subject: [PATCH 1162/2451] [CI] Updating repo.json for testing_0.7.3.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cceb2f29..67cb1832 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.8", + "TestingAssemblyVersion": "0.7.3.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4e704770cb2a4d6a15ad50445a9ac5972ca9af62 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Sep 2023 17:23:54 +0200 Subject: [PATCH 1163/2451] Add Filesystem Compression as a toggle and button. Also some auto-formatting. --- OtterGui | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImport.cs | 146 ++++++------ Penumbra/Import/TexToolsImporter.Gui.cs | 1 - Penumbra/Import/TexToolsImporter.ModPack.cs | 215 +++++++++--------- Penumbra/Import/TexToolsMeta.Export.cs | 194 ++++++++-------- Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Meta/MetaFileManager.cs | 8 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 5 +- Penumbra/Mods/Editor/ModEditor.cs | 9 +- Penumbra/Mods/Editor/ModelMaterialInfo.cs | 7 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 3 +- Penumbra/Mods/Manager/ModImportManager.cs | 3 +- Penumbra/Services/ServiceManager.cs | 4 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 57 ++--- .../AdvancedWindow/ModEditWindow.Materials.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + .../ModEditWindow.QuickImport.cs | 5 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 13 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 49 +++- 21 files changed, 385 insertions(+), 344 deletions(-) diff --git a/OtterGui b/OtterGui index 51ae6032..e3e3f42f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 51ae60322c22c9d9b49365ad0b9fd60dc3d50296 +Subproject commit e3e3f42f093b53ad02694810398df5736174d711 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index cc7cc026..a80563c9 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -99,6 +99,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; public bool AutoDeduplicateOnImport { get; set; } = true; + public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index dff1c921..49b344da 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,9 +6,10 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; -using Penumbra.Api; +using OtterGui.Compression; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using FileMode = System.IO.FileMode; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; @@ -25,41 +25,41 @@ public partial class TexToolsImporter : IDisposable private readonly DirectoryInfo _baseDirectory; private readonly string _tmpFile; - private readonly IEnumerable< FileInfo > _modPackFiles; + private readonly IEnumerable _modPackFiles; private readonly int _modPackCount; private FileStream? _tmpFileStream; private StreamDisposer? _streamDisposer; private readonly CancellationTokenSource _cancellation = new(); private readonly CancellationToken _token; - public ImporterState State { get; private set; } - public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods; + public ImporterState State { get; private set; } + public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods; private readonly Configuration _config; private readonly ModEditor _editor; - private readonly ModManager _modManager; - - public TexToolsImporter( int count, IEnumerable< FileInfo > modPackFiles, - Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager) + private readonly ModManager _modManager; + private readonly FileCompactor _compactor; + + public TexToolsImporter(int count, IEnumerable modPackFiles, Action handler, + Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor) { _baseDirectory = modManager.BasePath; - _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); + _tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName); _modPackFiles = modPackFiles; _config = config; _editor = editor; - _modManager = modManager; + _modManager = modManager; + _compactor = compactor; _modPackCount = count; - ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count ); + ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); _token = _cancellation.Token; - Task.Run( ImportFiles, _token ) - .ContinueWith( _ => CloseStreams() ) - .ContinueWith( _ => + Task.Run(ImportFiles, _token) + .ContinueWith(_ => CloseStreams()) + .ContinueWith(_ => { - foreach( var (file, dir, error) in ExtractedMods ) - { - handler( file, dir, error ); - } - } ); + foreach (var (file, dir, error) in ExtractedMods) + handler(file, dir, error); + }); } private void CloseStreams() @@ -71,45 +71,43 @@ public partial class TexToolsImporter : IDisposable public void Dispose() { - _cancellation.Cancel( true ); - if( State != ImporterState.WritingPackToDisk ) + _cancellation.Cancel(true); + if (State != ImporterState.WritingPackToDisk) { _tmpFileStream?.Dispose(); _tmpFileStream = null; } - if( State != ImporterState.ExtractingModFiles ) - { + if (State != ImporterState.ExtractingModFiles) ResetStreamDisposer(); - } } private void ImportFiles() { - State = ImporterState.None; - _currentModPackIdx = 0; - foreach( var file in _modPackFiles ) + State = ImporterState.None; + _currentModPackIdx = 0; + foreach (var file in _modPackFiles) { _currentModDirectory = null; - if( _token.IsCancellationRequested ) + if (_token.IsCancellationRequested) { - ExtractedMods.Add( ( file, null, new TaskCanceledException( "Task canceled by user." ) ) ); + ExtractedMods.Add((file, null, new TaskCanceledException("Task canceled by user."))); continue; } try { - var directory = VerifyVersionAndImport( file ); - ExtractedMods.Add( ( file, directory, null ) ); - if( _config.AutoDeduplicateOnImport ) + var directory = VerifyVersionAndImport(file); + ExtractedMods.Add((file, directory, null)); + if (_config.AutoDeduplicateOnImport) { State = ImporterState.DeduplicatingFiles; - _editor.Duplicates.DeduplicateMod( directory ); + _editor.Duplicates.DeduplicateMod(directory); } } - catch( Exception e ) + catch (Exception e) { - ExtractedMods.Add( ( file, _currentModDirectory, e ) ); + ExtractedMods.Add((file, _currentModDirectory, e)); _currentNumOptions = 0; _currentOptionIdx = 0; _currentFileIdx = 0; @@ -124,87 +122,75 @@ public partial class TexToolsImporter : IDisposable // Rudimentary analysis of a TTMP file by extension and version. // Puts out warnings if extension does not correspond to data. - private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) + private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile) { - if( modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar" ) - { - return HandleRegularArchive( modPackFile ); - } + if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar") + return HandleRegularArchive(modPackFile); using var zfs = modPackFile.OpenRead(); - using var extractedModPack = ZipArchive.Open( zfs ); + using var extractedModPack = ZipArchive.Open(zfs); - var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); - if( mpl == null ) - { - throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); - } + var mpl = FindZipEntry(extractedModPack, "TTMPL.mpl"); + if (mpl == null) + throw new FileNotFoundException("ZIP does not contain a TTMPL.mpl file."); - var modRaw = GetStringFromZipEntry( mpl, Encoding.UTF8 ); + var modRaw = GetStringFromZipEntry(mpl, Encoding.UTF8); // At least a better validation than going by the extension. - if( modRaw.Contains( "\"TTMPVersion\":" ) ) + if (modRaw.Contains("\"TTMPVersion\":")) { - if( modPackFile.Extension != ".ttmp2" ) - { - Penumbra.Log.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); - } + if (modPackFile.Extension != ".ttmp2") + Penumbra.Log.Warning($"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension."); - return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); + return ImportV2ModPack(modPackFile, extractedModPack, modRaw); } - if( modPackFile.Extension != ".ttmp" ) - { - Penumbra.Log.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); - } + if (modPackFile.Extension != ".ttmp") + Penumbra.Log.Warning($"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension."); - return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); + return ImportV1ModPack(modPackFile, extractedModPack, modRaw); } // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry - private static ZipArchiveEntry? FindZipEntry( ZipArchive file, string fileName ) - => file.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.Contains( fileName ) ); + private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName) + => file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName)); - private static string GetStringFromZipEntry( ZipArchiveEntry entry, Encoding encoding ) + private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding) { using var ms = new MemoryStream(); using var s = entry.OpenEntryStream(); - s.CopyTo( ms ); - return encoding.GetString( ms.ToArray() ); + s.CopyTo(ms); + return encoding.GetString(ms.ToArray()); } - private void WriteZipEntryToTempFile( Stream s ) + private void WriteZipEntryToTempFile(Stream s) { _tmpFileStream?.Dispose(); // should not happen - _tmpFileStream = new FileStream( _tmpFile, FileMode.Create ); - if( _token.IsCancellationRequested ) - { + _tmpFileStream = new FileStream(_tmpFile, FileMode.Create); + if (_token.IsCancellationRequested) return; - } - s.CopyTo( _tmpFileStream ); + s.CopyTo(_tmpFileStream); _tmpFileStream.Dispose(); _tmpFileStream = null; } - private StreamDisposer GetSqPackStreamStream( ZipArchive file, string entryName ) + private StreamDisposer GetSqPackStreamStream(ZipArchive file, string entryName) { State = ImporterState.WritingPackToDisk; // write shitty zip garbage to disk - var entry = FindZipEntry( file, entryName ); - if( entry == null ) - { - throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); - } + var entry = FindZipEntry(file, entryName); + if (entry == null) + throw new FileNotFoundException($"ZIP does not contain a file named {entryName}."); using var s = entry.OpenEntryStream(); - WriteZipEntryToTempFile( s ); + WriteZipEntryToTempFile(s); _streamDisposer?.Dispose(); // Should not happen. - var fs = new FileStream( _tmpFile, FileMode.Open ); - return new StreamDisposer( fs ); + var fs = new FileStream(_tmpFile, FileMode.Open); + return new StreamDisposer(fs); } private void ResetStreamDisposer() @@ -212,4 +198,4 @@ public partial class TexToolsImporter : IDisposable _streamDisposer?.Dispose(); _streamDisposer = null; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 79cd728b..db818341 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -22,7 +22,6 @@ public partial class TexToolsImporter private string _currentOptionName = string.Empty; private string _currentFileName = string.Empty; - public void DrawProgressInfo( Vector2 size ) { if( _modPackCount == 0 ) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 0776faea..32c9c0e1 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -17,7 +17,7 @@ public partial class TexToolsImporter private DirectoryInfo? _currentModDirectory; // Version 1 mod packs are a simple collection of files without much information. - private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipArchive extractedModPack, string modRaw ) + private DirectoryInfo ImportV1ModPack(FileInfo modPackFile, ZipArchive extractedModPack, string modRaw) { _currentOptionIdx = 0; _currentNumOptions = 1; @@ -25,174 +25,171 @@ public partial class TexToolsImporter _currentGroupName = string.Empty; _currentOptionName = DefaultTexToolsData.DefaultOption; - Penumbra.Log.Information( " -> Importing V1 ModPack" ); + Penumbra.Log.Information(" -> Importing V1 ModPack"); var modListRaw = modRaw.Split( - new[] { "\r\n", "\r", "\n" }, + new[] + { + "\r\n", + "\r", + "\n", + }, StringSplitOptions.RemoveEmptyEntries ); - var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); + var modList = modListRaw.Select(m => JsonConvert.DeserializeObject(m, JsonSettings)!).ToList(); - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name)); // Create a new ModMeta from the TTMP mod list info - _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); + _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, + null, null); // Open the mod data file from the mod pack as a SqPackStream - _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); - ExtractSimpleModList( _currentModDirectory, modList ); - _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); + _streamDisposer = GetSqPackStreamStream(extractedModPack, "TTMPD.mpd"); + ExtractSimpleModList(_currentModDirectory, modList); + _modManager.Creator.CreateDefaultFiles(_currentModDirectory); ResetStreamDisposer(); return _currentModDirectory; } // Version 2 mod packs can either be simple or extended, import accordingly. - private DirectoryInfo ImportV2ModPack( FileInfo _, ZipArchive extractedModPack, string modRaw ) + private DirectoryInfo ImportV2ModPack(FileInfo _, ZipArchive extractedModPack, string modRaw) { - var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!; + var modList = JsonConvert.DeserializeObject(modRaw, JsonSettings)!; - if( modList.TtmpVersion.EndsWith( "s" ) ) - { - return ImportSimpleV2ModPack( extractedModPack, modList ); - } + if (modList.TtmpVersion.EndsWith("s")) + return ImportSimpleV2ModPack(extractedModPack, modList); - if( modList.TtmpVersion.EndsWith( "w" ) ) - { - return ImportExtendedV2ModPack( extractedModPack, modRaw ); - } + if (modList.TtmpVersion.EndsWith("w")) + return ImportExtendedV2ModPack(extractedModPack, modRaw); try { - Penumbra.Log.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." ); - return ImportSimpleV2ModPack( extractedModPack, modList ); + Penumbra.Log.Warning($"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack."); + return ImportSimpleV2ModPack(extractedModPack, modList); } - catch( Exception e1 ) + catch (Exception e1) { - Penumbra.Log.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" ); + Penumbra.Log.Warning($"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}"); try { - return ImportExtendedV2ModPack( extractedModPack, modRaw ); + return ImportExtendedV2ModPack(extractedModPack, modRaw); } - catch( Exception e2 ) + catch (Exception e2) { - throw new IOException( "Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2 ); + throw new IOException("Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2); } } } // Simple V2 mod packs are basically the same as V1 mod packs. - private DirectoryInfo ImportSimpleV2ModPack( ZipArchive extractedModPack, SimpleModPack modList ) + private DirectoryInfo ImportSimpleV2ModPack(ZipArchive extractedModPack, SimpleModPack modList) { _currentOptionIdx = 0; _currentNumOptions = 1; _currentModName = modList.Name; _currentGroupName = string.Empty; _currentOptionName = DefaultTexToolsData.DefaultOption; - Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); + Penumbra.Log.Information(" -> Importing Simple V2 ModPack"); - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); - _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty(modList.Description) ? "Mod imported from TexTools mod pack" - : modList.Description, modList.Version, modList.Url ); + : modList.Description, modList.Version, modList.Url); // Open the mod data file from the mod pack as a SqPackStream - _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); - ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); - _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); + _streamDisposer = GetSqPackStreamStream(extractedModPack, "TTMPD.mpd"); + ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList); + _modManager.Creator.CreateDefaultFiles(_currentModDirectory); ResetStreamDisposer(); return _currentModDirectory; } // Obtain the number of relevant options to extract. - private static int GetOptionCount( ExtendedModPack pack ) - => ( pack.SimpleModsList.Length > 0 ? 1 : 0 ) + private static int GetOptionCount(ExtendedModPack pack) + => (pack.SimpleModsList.Length > 0 ? 1 : 0) + pack.ModPackPages - .Sum( page => page.ModGroups - .Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 ) - .Sum( group => group.OptionList - .Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) - + ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) ); + .Sum(page => page.ModGroups + .Where(g => g.GroupName.Length > 0 && g.OptionList.Length > 0) + .Sum(group => group.OptionList + .Count(o => o.Name.Length > 0 && o.ModsJsons.Length > 0) + + (group.OptionList.Any(o => o.Name.Length > 0 && o.ModsJsons.Length == 0) ? 1 : 0))); - private static string GetGroupName( string groupName, ISet< string > names ) + private static string GetGroupName(string groupName, ISet names) { var baseName = groupName; var i = 2; - while( !names.Add( groupName ) ) - { + while (!names.Add(groupName)) groupName = $"{baseName} ({i++})"; - } return groupName; } // Extended V2 mod packs contain multiple options that need to be handled separately. - private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw ) + private DirectoryInfo ImportExtendedV2ModPack(ZipArchive extractedModPack, string modRaw) { _currentOptionIdx = 0; - Penumbra.Log.Information( " -> Importing Extended V2 ModPack" ); + Penumbra.Log.Information(" -> Importing Extended V2 ModPack"); - var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!; - _currentNumOptions = GetOptionCount( modList ); + var modList = JsonConvert.DeserializeObject(modRaw, JsonSettings)!; + _currentNumOptions = GetOptionCount(modList); _currentModName = modList.Name; - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); - _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, + modList.Url); - if( _currentNumOptions == 0 ) - { + if (_currentNumOptions == 0) return _currentModDirectory; - } // Open the mod data file from the mod pack as a SqPackStream - _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); + _streamDisposer = GetSqPackStreamStream(extractedModPack, "TTMPD.mpd"); // It can contain a simple list, still. - if( modList.SimpleModsList.Length > 0 ) + if (modList.SimpleModsList.Length > 0) { _currentGroupName = string.Empty; _currentOptionName = "Default"; - ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); + ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList); } // Iterate through all pages - var options = new List< ISubMod >(); + var options = new List(); var groupPriority = 0; - var groupNames = new HashSet< string >(); - foreach( var page in modList.ModPackPages ) + var groupNames = new HashSet(); + foreach (var page in modList.ModPackPages) { - foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) ) + foreach (var group in page.ModGroups.Where(group => group.GroupName.Length > 0 && group.OptionList.Length > 0)) { - var allOptions = group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ).ToList(); + var allOptions = group.OptionList.Where(option => option.Name.Length > 0 && option.ModsJsons.Length > 0).ToList(); var (numGroups, maxOptions) = group.SelectionType == GroupType.Single - ? ( 1, allOptions.Count ) - : ( 1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions ); - _currentGroupName = GetGroupName( group.GroupName, groupNames ); + ? (1, allOptions.Count) + : (1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions); + _currentGroupName = GetGroupName(group.GroupName, groupNames); - var optionIdx = 0; - for( var groupId = 0; groupId < numGroups; ++groupId ) + var optionIdx = 0; + for (var groupId = 0; groupId < numGroups; ++groupId) { - var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; + var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = ModCreator.NewSubFolderName( _currentModDirectory, name ) - ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, - numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); + var groupFolder = ModCreator.NewSubFolderName(_currentModDirectory, name) + ?? new DirectoryInfo(Path.Combine(_currentModDirectory.FullName, + numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}")); uint? defaultSettings = group.SelectionType == GroupType.Multi ? 0u : null; - for( var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i ) + for (var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i) { - var option = allOptions[ i + optionIdx ]; + var option = allOptions[i + optionIdx]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name ) - ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); - ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( _modManager.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); - if( option.IsChecked ) - { + var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name) + ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); + ExtractSimpleModList(optionFolder, option.ModsJsons); + options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); + if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi - ? ( defaultSettings!.Value | ( 1u << i ) ) - : ( uint )i; - } + ? defaultSettings!.Value | (1u << i) + : (uint)i; ++_currentOptionIdx; } @@ -201,30 +198,30 @@ public partial class TexToolsImporter // Handle empty options for single select groups without creating a folder for them. // We only want one of those at most, and it should usually be the first option. - if( group.SelectionType == GroupType.Single ) + if (group.SelectionType == GroupType.Single) { - var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); - if( empty != null ) + var empty = group.OptionList.FirstOrDefault(o => o.Name.Length > 0 && o.ModsJsons.Length == 0); + if (empty != null) { _currentOptionName = empty.Name; - options.Insert( 0, ModCreator.CreateEmptySubMod( empty.Name ) ); + options.Insert(0, ModCreator.CreateEmptySubMod(empty.Name)); defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; } } - _modManager.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - defaultSettings ?? 0, group.Description, options ); + _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + defaultSettings ?? 0, group.Description, options); ++groupPriority; } } } ResetStreamDisposer(); - _modManager.Creator.CreateDefaultFiles( _currentModDirectory ); + _modManager.Creator.CreateDefaultFiles(_currentModDirectory); return _currentModDirectory; } - private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods ) + private void ExtractSimpleModList(DirectoryInfo outDirectory, ICollection mods) { State = ImporterState.ExtractingModFiles; @@ -232,51 +229,47 @@ public partial class TexToolsImporter _currentNumFiles = mods.Count(m => m.FullPath.Length > 0); // Extract each SimpleMod into the new mod folder - foreach( var simpleMod in mods.Where(m => m.FullPath.Length > 0 ) ) + foreach (var simpleMod in mods.Where(m => m.FullPath.Length > 0)) { - ExtractMod( outDirectory, simpleMod ); + ExtractMod(outDirectory, simpleMod); ++_currentFileIdx; } } - private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod ) + private void ExtractMod(DirectoryInfo outDirectory, SimpleMod mod) { - if( _streamDisposer is not PenumbraSqPackStream stream ) - { + if (_streamDisposer is not PenumbraSqPackStream stream) return; - } - Penumbra.Log.Information( $" -> Extracting {mod.FullPath} at {mod.ModOffset:X}" ); + Penumbra.Log.Information($" -> Extracting {mod.FullPath} at {mod.ModOffset:X}"); _token.ThrowIfCancellationRequested(); - var data = stream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); + var data = stream.ReadFile(mod.ModOffset); _currentFileName = mod.FullPath; - var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) ); + var extractedFile = new FileInfo(Path.Combine(outDirectory.FullName, mod.FullPath)); extractedFile.Directory?.Create(); - if( extractedFile.FullName.EndsWith( ".mdl" ) ) - { - ProcessMdl( data.Data ); - } + if (extractedFile.FullName.EndsWith(".mdl")) + ProcessMdl(data.Data); - File.WriteAllBytes( extractedFile.FullName, data.Data ); + _compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token); } - private static void ProcessMdl( byte[] mdl ) + private static void ProcessMdl(byte[] mdl) { const int modelHeaderLodOffset = 22; // Model file header LOD num - mdl[ 64 ] = 1; + mdl[64] = 1; // Model header LOD num - var stackSize = BitConverter.ToUInt32( mdl, 4 ); - var runtimeBegin = stackSize + 0x44; + var stackSize = BitConverter.ToUInt32(mdl, 4); + var runtimeBegin = stackSize + 0x44; var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset ); + var stringsLength = BitConverter.ToUInt32(mdl, (int)stringsLengthOffset); var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1; + mdl[modelHeaderStart + modelHeaderLodOffset] = 1; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 7f455ab0..2eac8f59 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -12,7 +12,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Import; public partial class TexToolsMeta -{ +{ public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable manipulations, DirectoryInfo basePath) { var files = ConvertToTexTools(manager, manipulations); @@ -23,7 +23,7 @@ public partial class TexToolsMeta try { Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllBytes(path, data); + manager.Compactor.WriteAllBytes(path, data); } catch (Exception e) { @@ -32,191 +32,187 @@ public partial class TexToolsMeta } } - public static Dictionary< string, byte[] > ConvertToTexTools( MetaFileManager manager, IEnumerable< MetaManipulation > manips ) + public static Dictionary ConvertToTexTools(MetaFileManager manager, IEnumerable manips) { - var ret = new Dictionary< string, byte[] >(); - foreach( var group in manips.GroupBy( ManipToPath ) ) + var ret = new Dictionary(); + foreach (var group in manips.GroupBy(ManipToPath)) { - if( group.Key.Length == 0 ) - { + if (group.Key.Length == 0) continue; - } - var bytes = group.Key.EndsWith( ".rgsp" ) - ? WriteRgspFile( manager, group.Key, group ) - : WriteMetaFile( manager, group.Key, group ); - if( bytes.Length == 0 ) - { + var bytes = group.Key.EndsWith(".rgsp") + ? WriteRgspFile(manager, group.Key, group) + : WriteMetaFile(manager, group.Key, group); + if (bytes.Length == 0) continue; - } - ret.Add( group.Key, bytes ); + ret.Add(group.Key, bytes); } return ret; } - private static byte[] WriteRgspFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips ) + private static byte[] WriteRgspFile(MetaFileManager manager, string path, IEnumerable manips) { - var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp ); - using var m = new MemoryStream( 45 ); - using var b = new BinaryWriter( m ); + var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp); + using var m = new MemoryStream(45); + using var b = new BinaryWriter(m); // Version - b.Write( byte.MaxValue ); - b.Write( ( ushort )2 ); + b.Write(byte.MaxValue); + b.Write((ushort)2); var race = list.First().Value.SubRace; var gender = list.First().Value.Attribute.ToGender(); - b.Write( ( byte )(race - 1) ); // offset by one due to Unknown - b.Write( ( byte )(gender - 1) ); // offset by one due to Unknown + b.Write((byte)(race - 1)); // offset by one due to Unknown + b.Write((byte)(gender - 1)); // offset by one due to Unknown - void Add( params RspAttribute[] attributes ) + void Add(params RspAttribute[] attributes) { - foreach( var attribute in attributes ) + foreach (var attribute in attributes) { - var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( manager, race, attribute ); - b.Write( value ); + var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); + b.Write(value); } } - if( gender == Gender.Male ) + if (gender == Gender.Male) { - Add( RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail ); + Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail); } else { - Add( RspAttribute.FemaleMinSize, RspAttribute.FemaleMaxSize, RspAttribute.FemaleMinTail, RspAttribute.FemaleMaxTail ); - Add( RspAttribute.BustMinX, RspAttribute.BustMinY, RspAttribute.BustMinZ, RspAttribute.BustMaxX, RspAttribute.BustMaxY, RspAttribute.BustMaxZ ); + Add(RspAttribute.FemaleMinSize, RspAttribute.FemaleMaxSize, RspAttribute.FemaleMinTail, RspAttribute.FemaleMaxTail); + Add(RspAttribute.BustMinX, RspAttribute.BustMinY, RspAttribute.BustMinZ, RspAttribute.BustMaxX, RspAttribute.BustMaxY, + RspAttribute.BustMaxZ); } return m.GetBuffer(); } - private static byte[] WriteMetaFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips ) + private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnumerable manips) { - var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) ); + var filteredManips = manips.GroupBy(m => m.ManipulationType).ToDictionary(p => p.Key, p => p.Select(x => x)); using var m = new MemoryStream(); - using var b = new BinaryWriter( m ); + using var b = new BinaryWriter(m); // Header // Current TT Metadata version. - b.Write( 2u ); + b.Write(2u); // Null-terminated ASCII path. - var utf8Path = Encoding.ASCII.GetBytes( path ); - b.Write( utf8Path ); - b.Write( ( byte )0 ); + var utf8Path = Encoding.ASCII.GetBytes(path); + b.Write(utf8Path); + b.Write((byte)0); // Number of Headers - b.Write( ( uint )filteredManips.Count ); + b.Write((uint)filteredManips.Count); // Current TT Size of Headers - b.Write( ( uint )12 ); + b.Write((uint)12); // Start of Header Entries for some reason, which is absolutely useless. var headerStart = b.BaseStream.Position + 4; - b.Write( ( uint )headerStart ); + b.Write((uint)headerStart); - var offset = ( uint )( b.BaseStream.Position + 12 * filteredManips.Count ); - foreach( var (header, data) in filteredManips ) + var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count); + foreach (var (header, data) in filteredManips) { - b.Write( ( uint )header ); - b.Write( offset ); + b.Write((uint)header); + b.Write(offset); - var size = WriteData( manager, b, offset, header, data ); - b.Write( size ); + var size = WriteData(manager, b, offset, header, data); + b.Write(size); offset += size; } return m.ToArray(); } - private static uint WriteData( MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) + private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, + IEnumerable manips) { var oldPos = b.BaseStream.Position; - b.Seek( ( int )offset, SeekOrigin.Begin ); + b.Seek((int)offset, SeekOrigin.Begin); - switch( type ) + switch (type) { case MetaManipulation.Type.Imc: var allManips = manips.ToList(); - var baseFile = new ImcFile( manager, allManips[ 0 ].Imc ); - foreach( var manip in allManips ) - { - manip.Imc.Apply( baseFile ); - } + var baseFile = new ImcFile(manager, allManips[0].Imc); + foreach (var manip in allManips) + manip.Imc.Apply(baseFile); - var partIdx = allManips[ 0 ].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? ImcFile.PartIndex( allManips[ 0 ].Imc.EquipSlot ) + var partIdx = allManips[0].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? ImcFile.PartIndex(allManips[0].Imc.EquipSlot) : 0; - for( var i = 0; i <= baseFile.Count; ++i ) + for (var i = 0; i <= baseFile.Count; ++i) { - var entry = baseFile.GetEntry( partIdx, (Variant)i ); - b.Write( entry.MaterialId ); - b.Write( entry.DecalId ); - b.Write( entry.AttributeAndSound ); - b.Write( entry.VfxId ); - b.Write( entry.MaterialAnimationId ); + var entry = baseFile.GetEntry(partIdx, (Variant)i); + b.Write(entry.MaterialId); + b.Write(entry.DecalId); + b.Write(entry.AttributeAndSound); + b.Write(entry.VfxId); + b.Write(entry.MaterialAnimationId); } break; case MetaManipulation.Type.Eqdp: - foreach( var manip in manips ) + foreach (var manip in manips) { - b.Write( ( uint )Names.CombinedRace( manip.Eqdp.Gender, manip.Eqdp.Race ) ); - var entry = ( byte )(( ( uint )manip.Eqdp.Entry >> Eqdp.Offset( manip.Eqdp.Slot ) ) & 0x03); - b.Write( entry ); + b.Write((uint)Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race)); + var entry = (byte)(((uint)manip.Eqdp.Entry >> Eqdp.Offset(manip.Eqdp.Slot)) & 0x03); + b.Write(entry); } break; case MetaManipulation.Type.Eqp: - foreach( var manip in manips ) + foreach (var manip in manips) { - var bytes = BitConverter.GetBytes( (ulong) manip.Eqp.Entry ); - var (numBytes, byteOffset) = Eqp.BytesAndOffset( manip.Eqp.Slot ); - for( var i = byteOffset; i < numBytes + byteOffset; ++i ) - b.Write( bytes[ i ] ); + var bytes = BitConverter.GetBytes((ulong)manip.Eqp.Entry); + var (numBytes, byteOffset) = Eqp.BytesAndOffset(manip.Eqp.Slot); + for (var i = byteOffset; i < numBytes + byteOffset; ++i) + b.Write(bytes[i]); } break; case MetaManipulation.Type.Est: - foreach( var manip in manips ) + foreach (var manip in manips) { - b.Write( ( ushort )Names.CombinedRace( manip.Est.Gender, manip.Est.Race ) ); - b.Write( manip.Est.SetId.Id ); - b.Write( manip.Est.Entry ); + b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); + b.Write(manip.Est.SetId.Id); + b.Write(manip.Est.Entry); } break; case MetaManipulation.Type.Gmp: - foreach( var manip in manips ) + foreach (var manip in manips) { - b.Write( ( uint )manip.Gmp.Entry.Value ); - b.Write( manip.Gmp.Entry.UnknownTotal ); + b.Write((uint)manip.Gmp.Entry.Value); + b.Write(manip.Gmp.Entry.UnknownTotal); } break; } var size = b.BaseStream.Position - offset; - b.Seek( ( int )oldPos, SeekOrigin.Begin ); - return ( uint )size; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; } - private static string ManipToPath( MetaManipulation manip ) + private static string ManipToPath(MetaManipulation manip) => manip.ManipulationType switch { - MetaManipulation.Type.Imc => ManipToPath( manip.Imc ), - MetaManipulation.Type.Eqdp => ManipToPath( manip.Eqdp ), - MetaManipulation.Type.Eqp => ManipToPath( manip.Eqp ), - MetaManipulation.Type.Est => ManipToPath( manip.Est ), - MetaManipulation.Type.Gmp => ManipToPath( manip.Gmp ), - MetaManipulation.Type.Rsp => ManipToPath( manip.Rsp ), + MetaManipulation.Type.Imc => ManipToPath(manip.Imc), + MetaManipulation.Type.Eqdp => ManipToPath(manip.Eqdp), + MetaManipulation.Type.Eqp => ManipToPath(manip.Eqp), + MetaManipulation.Type.Est => ManipToPath(manip.Est), + MetaManipulation.Type.Gmp => ManipToPath(manip.Gmp), + MetaManipulation.Type.Rsp => ManipToPath(manip.Rsp), _ => string.Empty, }; - private static string ManipToPath( ImcManipulation manip ) + private static string ManipToPath(ImcManipulation manip) { var path = manip.GamePath().ToString(); var replacement = manip.ObjectType switch @@ -227,22 +223,22 @@ public partial class TexToolsMeta _ => ".meta", }; - return path.Replace( ".imc", replacement ); + return path.Replace(".imc", replacement); } - private static string ManipToPath( EqdpManipulation manip ) + private static string ManipToPath(EqdpManipulation manip) => manip.Slot.IsAccessory() ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath( EqpManipulation manip ) + private static string ManipToPath(EqpManipulation manip) => manip.Slot.IsAccessory() ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath( EstManipulation manip ) + private static string ManipToPath(EstManipulation manip) { - var raceCode = Names.CombinedRace( manip.Gender, manip.Race ).ToRaceCode(); + var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); return manip.Slot switch { EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", @@ -253,10 +249,10 @@ public partial class TexToolsMeta }; } - private static string ManipToPath( GmpManipulation manip ) + private static string ManipToPath(GmpManipulation manip) => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; - private static string ManipToPath( RspManipulation manip ) - => $"chara/xls/charamake/rgsp/{( int )manip.SubRace - 1}-{( int )manip.Attribute.ToGender() - 1}.rgsp"; -} \ No newline at end of file + private static string ManipToPath(RspManipulation manip) + => $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp"; +} diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index fead989e..d1b78268 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -10,7 +10,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterTex; -using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.UI; using Penumbra.UI.Classes; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 7287204c..171ea729 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; +using OtterGui.Compression; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData; @@ -26,9 +27,11 @@ public unsafe class MetaFileManager internal readonly ActiveCollectionData ActiveCollections; internal readonly ValidityChecker ValidityChecker; internal readonly IdentifierService Identifier; + internal readonly FileCompactor Compactor; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, - ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) + ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier, + FileCompactor compactor) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -37,6 +40,7 @@ public unsafe class MetaFileManager Config = config; ValidityChecker = validityChecker; Identifier = identifier; + Compactor = compactor; SignatureHelper.Initialise(this); } @@ -91,7 +95,7 @@ public unsafe class MetaFileManager ResidentResources.Reload(); if (collection?._cache == null) - CharacterUtility.ResetAll(); + CharacterUtility.ResetAll(); else collection._cache.Meta.SetFiles(); } diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index f616d128..0fe9ec46 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using OtterGui; +using OtterGui.Compression; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Mods.Editor; @@ -26,10 +27,10 @@ public partial class MdlMaterialEditor public MdlMaterialEditor(ModFileCollection files) => _files = files; - public void SaveAllModels() + public void SaveAllModels(FileCompactor compactor) { foreach (var info in _modelFiles) - info.Save(); + info.Save(compactor); } public void RestoreAllModels() diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index bd774607..f65ce280 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,10 +1,10 @@ using System; using System.IO; using OtterGui; -using Penumbra.Mods.Editor; +using OtterGui.Compression; using Penumbra.Mods.Subclasses; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class ModEditor : IDisposable { @@ -15,6 +15,7 @@ public class ModEditor : IDisposable public readonly ModFileCollection Files; public readonly ModSwapEditor SwapEditor; public readonly MdlMaterialEditor MdlMaterialEditor; + public readonly FileCompactor Compactor; public Mod? Mod { get; private set; } public int GroupIdx { get; private set; } @@ -24,7 +25,8 @@ public class ModEditor : IDisposable public ISubMod? Option { get; private set; } public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files, - ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor) + ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor, + FileCompactor compactor) { ModNormalizer = modNormalizer; MetaEditor = metaEditor; @@ -33,6 +35,7 @@ public class ModEditor : IDisposable Duplicates = duplicates; SwapEditor = swapEditor; MdlMaterialEditor = mdlMaterialEditor; + Compactor = compactor; } public void LoadMod(Mod mod) diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs index dc01ae7d..38e76deb 100644 --- a/Penumbra/Mods/Editor/ModelMaterialInfo.cs +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -2,10 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using OtterGui; +using OtterGui.Compression; using Penumbra.GameData.Files; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; /// A class that collects information about skin materials in a model file and handle changes on them. public class ModelMaterialInfo @@ -40,7 +41,7 @@ public class ModelMaterialInfo } // Save a changed .mdl file. - public void Save() + public void Save(FileCompactor compactor) { if (!Changed) return; @@ -50,7 +51,7 @@ public class ModelMaterialInfo try { - System.IO.File.WriteAllBytes(Path.FullName, File.Write()); + compactor.WriteAllBytes(Path.FullName, File.Write()); Changed = false; } catch (Exception e) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 00bbaac8..d1af1d09 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -63,7 +63,6 @@ public class ItemSwapContainer continue; } - if( writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged ) { convertedSwaps.TryAdd( file.SwapFromRequestPath, file.SwapToModded ); @@ -73,7 +72,7 @@ public class ItemSwapContainer var path = file.GetNewPath( directory.FullName ); var bytes = file.FileData.Write(); Directory.CreateDirectory( Path.GetDirectoryName( path )! ); - File.WriteAllBytes( path, bytes ); + _manager.Compactor.WriteAllBytes( path, bytes ); convertedFiles.TryAdd( file.SwapFromRequestPath, new FullPath( path ) ); } diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index bbe881b0..45b3f2ec 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using Dalamud.Interface.Internal.Notifications; using Penumbra.Import; +using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; @@ -57,7 +58,7 @@ public class ModImportManager : IDisposable if (files.Length == 0) return; - _import = new TexToolsImporter(files.Length, files, AddNewMod, _config, _modEditor, _modManager); + _import = new TexToolsImporter(files.Length, files, AddNewMod, _config, _modEditor, _modManager, _modEditor.Compactor); } public bool Importing diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 8bea52e3..07c7394c 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; +using OtterGui.Compression; using OtterGui.Log; using Penumbra.Api; using Penumbra.Collections.Cache; @@ -62,7 +63,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddGameData(this IServiceCollection services) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index d0e9504c..8ed59fed 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -9,6 +9,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Compression; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData.Files; @@ -18,15 +19,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor : IDisposable where T : class, IWritable +public class FileEditor : IDisposable where T : class, IWritable { private readonly FileDialogService _fileDialog; - private readonly IDataManager _gameData; + private readonly IDataManager _gameData; private readonly ModEditWindow _owner; + private readonly FileCompactor _compactor; - public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, - string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) + public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileCompactor compactor, FileDialogService fileDialog, + string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, + Func parseFile) { _owner = owner; _gameData = gameData; @@ -36,6 +38,7 @@ public class FileEditor : IDisposable where T : class, IWritable _drawEdit = drawEdit; _getInitialPath = getInitialPath; _parseFile = parseFile; + _compactor = compactor; _combo = new Combo(config, getFiles); } @@ -60,19 +63,19 @@ public class FileEditor : IDisposable where T : class, IWritable DrawFilePanel(); } - public void Dispose() - { - (_currentFile as IDisposable)?.Dispose(); + public void Dispose() + { + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; (_defaultFile as IDisposable)?.Dispose(); _defaultFile = null; - } - - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; + } + + private readonly string _tabName; + private readonly string _fileType; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private FileRegistry? _currentPath; private T? _currentFile; @@ -107,9 +110,9 @@ public class FileEditor : IDisposable where T : class, IWritable if (file != null) { _defaultException = null; - (_defaultFile as IDisposable)?.Dispose(); - _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _defaultFile = _parseFile(file.Data, _defaultPath, false); + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _defaultFile = _parseFile(file.Data, _defaultPath, false); } else { @@ -135,7 +138,7 @@ public class FileEditor : IDisposable where T : class, IWritable try { - File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + _compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); } catch (Exception e) { @@ -168,9 +171,9 @@ public class FileEditor : IDisposable where T : class, IWritable { _currentException = null; _currentPath = null; - (_currentFile as IDisposable)?.Dispose(); - _currentFile = null; - _changed = false; + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + _changed = false; } private void DrawFileSelectCombo() @@ -192,13 +195,13 @@ public class FileEditor : IDisposable where T : class, IWritable try { var bytes = File.ReadAllBytes(_currentPath.File.FullName); - (_currentFile as IDisposable)?.Dispose(); - _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { - (_currentFile as IDisposable)?.Dispose(); + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _currentException = e; } @@ -209,7 +212,7 @@ public class FileEditor : IDisposable where T : class, IWritable if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) { - File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + _compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); _changed = false; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 102a6778..84779570 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -195,7 +195,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) - info.Save(); + info.Save(_editor.Compactor); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3e81901b..fd4c082e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -13,6 +13,7 @@ using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 29647c53..88ed10df 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -72,7 +73,7 @@ public partial class ModEditWindow try { - File.WriteAllBytes(name, writable!.Write()); + _editor.Compactor.WriteAllBytes(name, writable!.Write()); } catch (Exception e) { @@ -194,7 +195,7 @@ public partial class ModEditWindow var directory = Path.GetDirectoryName(_targetPath); if (directory != null) Directory.CreateDirectory(directory); - File.WriteAllBytes(_targetPath!, _file!.Write()); + _editor.Compactor.WriteAllBytes(_targetPath!, _file!.Write()); _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); _editor.FileEditor.AddPathsToSelected(_editor.Option!, new[] diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 890abfed..6e1b1d24 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; +using OtterGui.Compression; using OtterGui.Raii; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -22,6 +23,7 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; @@ -236,7 +238,7 @@ public partial class ModEditWindow : Window, IDisposable var anyChanges = editor.MdlMaterialEditor.ModelFiles.Any(m => m.Changed); if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize, anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges)) - editor.MdlMaterialEditor.SaveAllModels(); + editor.MdlMaterialEditor.SaveAllModels(editor.Compactor); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize, @@ -572,17 +574,18 @@ public partial class ModEditWindow : Window, IDisposable _textures = textures; _fileDialog = fileDialog; _gameEvents = gameEvents; - _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", + _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); - _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", + _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); - _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", + _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); - _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = + new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index c629de5b..0e239b7f 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -138,7 +138,7 @@ public class ModPanelEditTab : ITab _editor.LoadMod(_mod); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); - _editor.MdlMaterialEditor.SaveAllModels(); + _editor.MdlMaterialEditor.SaveAllModels(_editor.Compactor); _editWindow.UpdateModels(); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ac834e7c..7f27e6ee 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Components; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Widgets; @@ -39,6 +40,7 @@ public class SettingsTab : ITab private readonly DalamudServices _dalamud; private readonly HttpApi _httpApi; private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; + private readonly FileCompactor _compactor; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -46,7 +48,7 @@ public class SettingsTab : ITab public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi, - DalamudSubstitutionProvider dalamudSubstitutionProvider) + DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor) { _config = config; _fontReloader = fontReloader; @@ -61,6 +63,9 @@ public class SettingsTab : ITab _modExportManager = modExportManager; _httpApi = httpApi; _dalamudSubstitutionProvider = dalamudSubstitutionProvider; + _compactor = compactor; + if (_compactor.CanCompact) + _compactor.Enabled = _config.UseFileSystemCompression; } public void DrawHeader() @@ -661,6 +666,7 @@ public class SettingsTab : ITab Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); + DrawCompressionBox(); Checkbox("Keep Default Metadata Changes on Import", "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", @@ -673,6 +679,47 @@ public class SettingsTab : ITab ImGui.NewLine(); } + private void DrawCompressionBox() + { + if (!_compactor.CanCompact) + return; + + Checkbox("Use Filesystem Compression", + "Use Windows functionality to transparently reduce storage size of mod files on your computer. This might cost performance, but seems to generally be beneficial to performance by shifting more responsibility to the underused CPU and away from the overused hard drives.", + _config.UseFileSystemCompression, + v => + { + _config.UseFileSystemCompression = v; + _compactor.Enabled = v; + }); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero, + "Try to compress all files in your root directory. This will take a while.", + _compactor.MassCompactRunning || !_modManager.Valid)) + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero, + "Try to decompress all files in your root directory. This will take a while.", + _compactor.MassCompactRunning || !_modManager.Valid)) + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None); + + if (_compactor.MassCompactRunning) + { + ImGui.ProgressBar((float)_compactor.CurrentIndex / _compactor.TotalFiles, + new Vector2(ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - UiHelpers.IconButtonSize.X, ImGui.GetFrameHeight()), + _compactor.CurrentFile?.FullName[(_modManager.BasePath.FullName.Length + 1)..] ?? "Gathering Files..."); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Ban.ToIconString(), UiHelpers.IconButtonSize, "Cancel the mass action.", + !_compactor.MassCompactRunning, true)) + _compactor.CancelMassCompact(); + } + else + { + ImGui.Dummy(UiHelpers.IconButtonSize); + } + } + /// Draw two integral inputs for minimum dimensions of this window. private void DrawMinimumDimensionConfig() { From 470c1317ed11d247a22db2ab77a4109b58aa5959 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 14 Sep 2023 15:26:36 +0000 Subject: [PATCH 1164/2451] [CI] Updating repo.json for testing_0.7.3.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 67cb1832..4eafa05f 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.9", + "TestingAssemblyVersion": "0.7.3.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c5ef7bf46cd9ceb6288912b8c4c2a83627c0982f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Sep 2023 01:06:38 +0200 Subject: [PATCH 1165/2451] Add Compacting to API AddMod. --- OtterGui | 2 +- Penumbra/Api/PenumbraApi.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index e3e3f42f..ee64ae2d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e3e3f42f093b53ad02694810398df5736174d711 +Subproject commit ee64ae2d2710aea45365dafa3c91a721e59ae8fc diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9d578190..5ac53210 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Compression; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; @@ -637,6 +638,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.FileMissing; _modManager.AddMod(dir); + if (_config.UseFileSystemCompression) + new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); return PenumbraApiEc.Success; } From 652b2e99d25239fe2fc0f6d0939531808d92cf8a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Sep 2023 01:06:53 +0200 Subject: [PATCH 1166/2451] Add key checks to restoring from backup or deleting backups. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 0e239b7f..bd5a62c4 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -30,6 +30,7 @@ public class ModPanelEditTab : ITab private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; private readonly ModEditor _editor; + private readonly Configuration _config; private readonly TagButtons _modTags = new(); @@ -39,7 +40,7 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config) { _modManager = modManager; _selector = selector; @@ -49,6 +50,7 @@ public class ModPanelEditTab : ITab _editor = editor; _filenames = filenames; _modExportManager = modExportManager; + _config = config; } public ReadOnlySpan Label @@ -162,17 +164,27 @@ public class ModPanelEditTab : ITab ImGui.SameLine(); tt = backup.Exists - ? $"Delete existing mod export \"{backup.Name}\"." + ? $"Delete existing mod export \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; - if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists)) + if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) backup.Delete(); tt = backup.Exists - ? $"Restore mod from exported file \"{backup.Name}\"." + ? $"Restore mod from exported file \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists)) + if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) backup.Restore(_modManager); + if (backup.Exists) + { + ImGui.SameLine(); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextUnformatted(FontAwesomeIcon.CheckCircle.ToIconString()); + } + + ImGuiUtil.HoverTooltip($"Export exists in \"{backup.Name}\"."); + } } /// Anything about editing the regular meta information about the mod. From 28c2af4266b90bab8d281b95d34fceb4d9682289 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 15 Sep 2023 13:38:47 +0200 Subject: [PATCH 1167/2451] Material Editor: Split ColorTable apart from ColorSet --- Penumbra.GameData | 2 +- ...reviewer.cs => LiveColorTablePreviewer.cs} | 47 ++-- Penumbra/Interop/Structs/CharacterBaseExt.cs | 2 +- ... => ModEditWindow.Materials.ColorTable.cs} | 226 +++++++++--------- .../ModEditWindow.Materials.MtrlTab.cs | 121 +++++----- .../AdvancedWindow/ModEditWindow.Materials.cs | 11 +- 6 files changed, 195 insertions(+), 214 deletions(-) rename Penumbra/Interop/MaterialPreview/{LiveColorSetPreviewer.cs => LiveColorTablePreviewer.cs} (62%) rename Penumbra/UI/AdvancedWindow/{ModEditWindow.Materials.ColorSet.cs => ModEditWindow.Materials.ColorTable.cs} (65%) diff --git a/Penumbra.GameData b/Penumbra.GameData index 635d4c72..862add38 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 635d4c72e84da65a20a2d22d758dd8061bd8babf +Subproject commit 862add38110116b9bb266b739489456a2dd699c0 diff --git a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs similarity index 62% rename from Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs rename to Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index f927aa43..c9505091 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using Dalamud.Game; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; @@ -9,24 +8,24 @@ using Penumbra.Interop.SafeHandles; namespace Penumbra.Interop.MaterialPreview; -public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase +public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; + public const int TextureHeight = MtrlFile.ColorTable.NumRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly Framework _framework; - private readonly Texture** _colorSetTexture; - private readonly SafeTextureHandle _originalColorSetTexture; + private readonly Texture** _colorTableTexture; + private readonly SafeTextureHandle _originalColorTableTexture; - private Half[] _colorSet; + private Half[] _colorTable; private bool _updatePending; - public Half[] ColorSet - => _colorSet; + public Half[] ColorTable + => _colorTable; - public LiveColorSetPreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo) + public LiveColorTablePreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo) : base(objects, materialInfo) { _framework = framework; @@ -35,17 +34,17 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures; if (colorSetTextures == null) - throw new InvalidOperationException("Draw object doesn't have color set textures"); + throw new InvalidOperationException("Draw object doesn't have color table textures"); - _colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); - _originalColorSetTexture = new SafeTextureHandle(*_colorSetTexture, true); - if (_originalColorSetTexture == null) - throw new InvalidOperationException("Material doesn't have a color set"); + _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); + if (_originalColorTableTexture == null) + throw new InvalidOperationException("Material doesn't have a color table"); - _colorSet = new Half[TextureLength]; + _colorTable = new Half[TextureLength]; _updatePending = true; framework.Update += OnFrameworkUpdate; @@ -58,9 +57,9 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase base.Clear(disposing, reset); if (reset) - _originalColorSetTexture.Exchange(ref *(nint*)_colorSetTexture); + _originalColorTableTexture.Exchange(ref *(nint*)_colorTableTexture); - _originalColorSetTexture.Dispose(); + _originalColorTableTexture.Dispose(); } public void ScheduleUpdate() @@ -87,16 +86,16 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase return; bool success; - lock (_colorSet) + lock (_colorTable) { - fixed (Half* colorSet = _colorSet) + fixed (Half* colorTable = _colorTable) { - success = Structs.TextureUtility.InitializeContents(texture.Texture, colorSet); + success = Structs.TextureUtility.InitializeContents(texture.Texture, colorTable); } } if (success) - texture.Exchange(ref *(nint*)_colorSetTexture); + texture.Exchange(ref *(nint*)_colorTableTexture); } protected override bool IsStillValid() @@ -104,11 +103,11 @@ public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase if (!base.IsStillValid()) return false; - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures; if (colorSetTextures == null) return false; - if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) + if (_colorTableTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) return false; return true; diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs index 3bbbeca9..1bc1c2f3 100644 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -11,5 +11,5 @@ public unsafe struct CharacterBaseExt public CharacterBase CharacterBase; [FieldOffset( 0x258 )] - public Texture** ColorSetTextures; + public Texture** ColorTableTextures; } \ No newline at end of file diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs similarity index 65% rename from Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index e1ba045d..2d868a42 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; @@ -17,29 +16,28 @@ public partial class ModEditWindow private static readonly float HalfMaxValue = (float)Half.MaxValue; private static readonly float HalfEpsilon = (float)Half.Epsilon; - private bool DrawMaterialColorSetChange(MtrlTab tab, bool disabled) + private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.ColorSets.Any(c => c.HasRows)) + if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Set", ImGuiTreeNodeFlags.DefaultOpen)) + if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) return false; - var hasAnyDye = tab.UseColorDyeSet; - - ColorSetCopyAllClipboardButton(tab.Mtrl, 0); + ColorTableCopyAllClipboardButton(tab.Mtrl); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled); + var ret = ColorTablePasteAllClipboardButton(tab, disabled); if (!disabled) { ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye); + ret |= ColorTableDyeableCheckbox(tab); } - - if (hasAnyDye) + + var hasDyeTable = tab.Mtrl.HasDyeTable; + if (hasDyeTable) { ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); @@ -47,7 +45,7 @@ public partial class ModEditWindow ret |= DrawPreviewDye(tab, disabled); } - using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9, + using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); if (!table) return false; @@ -70,7 +68,7 @@ public partial class ModEditWindow ImGui.TableHeader("Repeat"); ImGui.TableNextColumn(); ImGui.TableHeader("Skew"); - if (hasAnyDye) + if (hasDyeTable) { ImGui.TableNextColumn(); ImGui.TableHeader("Dye"); @@ -78,29 +76,25 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var j = 0; j < tab.Mtrl.ColorSets.Length; ++j) + for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { - using var _ = ImRaii.PushId(j); - for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) - { - ret |= DrawColorSetRow(tab, j, i, disabled, hasAnyDye); - ImGui.TableNextRow(); - } + ret |= DrawColorTableRow(tab, i, disabled); + ImGui.TableNextRow(); } return ret; } - private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx) + private static void ColorTableCopyAllClipboardButton(MtrlFile file) { if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) return; try { - var data1 = file.ColorSets[colorSetIdx].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[colorSetIdx].Rows.AsBytes() : ReadOnlySpan.Empty; + var data1 = file.Table.AsBytes(); + var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; var array = new byte[data1.Length + data2.Length]; data1.TryCopyTo(array); data2.TryCopyTo(array.AsSpan(data1.Length)); @@ -122,13 +116,13 @@ public partial class ModEditWindow if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) { var ret = false; - for (var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j) + if (tab.Mtrl.HasDyeTable) { - for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, j, i, dyeId); + for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); } - tab.UpdateColorSetPreview(); + tab.UpdateColorTablePreview(); return ret; } @@ -136,40 +130,40 @@ public partial class ModEditWindow ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorSetPreview(); + tab.UpdateColorTablePreview(); return false; } - private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled) + private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) { if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || tab.Mtrl.ColorSets.Length <= colorSetIdx) + || !tab.Mtrl.HasTable) return false; try { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) + if (data.Length < Marshal.SizeOf()) return false; - ref var rows = ref tab.Mtrl.ColorSets[colorSetIdx].Rows; + ref var rows = ref tab.Mtrl.Table; fixed (void* ptr = data, output = &rows) { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.ColorDyeSets.Length > colorSetIdx) + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + && tab.Mtrl.HasDyeTable) { - ref var dyeRows = ref tab.Mtrl.ColorDyeSets[colorSetIdx].Rows; + ref var dyeRows = ref tab.Mtrl.DyeTable; fixed (void* output2 = &dyeRows) { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } - tab.UpdateColorSetPreview(); + tab.UpdateColorTablePreview(); return true; } @@ -179,7 +173,7 @@ public partial class ModEditWindow } } - private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye) + private static unsafe void ColorTableCopyClipboardButton(MtrlFile.ColorTable.Row row, MtrlFile.ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -187,11 +181,11 @@ public partial class ModEditWindow try { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + var data = new byte[MtrlFile.ColorTable.Row.Size + 2]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorSet.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorSet.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorTable.Row.Size, &dye, 2); } var text = Convert.ToBase64String(data); @@ -203,22 +197,21 @@ public partial class ModEditWindow } } - private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable) - { - var ret = ImGui.Checkbox("Dyeable", ref dyeable); + private static bool ColorTableDyeableCheckbox(MtrlTab tab) + { + var dyeable = tab.Mtrl.HasDyeTable; + var ret = ImGui.Checkbox("Dyeable", ref dyeable); if (ret) { - tab.UseColorDyeSet = dyeable; - if (dyeable) - tab.Mtrl.FindOrAddColorDyeSet(); - tab.UpdateColorSetPreview(); + tab.Mtrl.HasDyeTable = dyeable; + tab.UpdateColorTablePreview(); } return ret; } - private static unsafe bool ColorSetPasteFromClipboardButton(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled) + private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Import an exported row from your clipboard onto this row.", disabled, true)) @@ -228,18 +221,18 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != MtrlFile.ColorSet.Row.Size + 2 - || tab.Mtrl.ColorSets.Length <= colorSetIdx) + if (data.Length != MtrlFile.ColorTable.Row.Size + 2 + || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorSet.Row*)ptr; - if (colorSetIdx < tab.Mtrl.ColorDyeSets.Length) - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorDyeSet.Row*)(ptr + MtrlFile.ColorSet.Row.Size); + tab.Mtrl.Table[rowIdx] = *(MtrlFile.ColorTable.Row*)ptr; + if (tab.Mtrl.HasDyeTable) + tab.Mtrl.DyeTable[rowIdx] = *(MtrlFile.ColorDyeTable.Row*)(ptr + MtrlFile.ColorTable.Row.Size); } - tab.UpdateColorSetRowPreview(rowIdx); + tab.UpdateColorTableRowPreview(rowIdx); return true; } @@ -249,18 +242,18 @@ public partial class ModEditWindow } } - private static void ColorSetHighlightButton(MtrlTab tab, int rowIdx, bool disabled) + private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) { ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true); + "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); if (ImGui.IsItemHovered()) - tab.HighlightColorSetRow(rowIdx); - else if (tab.HighlightedColorSetRow == rowIdx) - tab.CancelColorSetHighlight(); + tab.HighlightColorTableRow(rowIdx); + else if (tab.HighlightedColorTableRow == rowIdx) + tab.CancelColorTableHighlight(); } - private bool DrawColorSetRow(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye) + private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) { static bool FixFloat(ref float val, float current) { @@ -269,17 +262,17 @@ public partial class ModEditWindow } using var id = ImRaii.PushId(rowIdx); - var row = tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx]; - var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] : new MtrlFile.ColorDyeSet.Row(); + ref var row = ref tab.Mtrl.Table[rowIdx]; + var hasDye = tab.Mtrl.HasDyeTable; + ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); - ColorSetCopyClipboardButton(row, dye); + ColorTableCopyClipboardButton(row, dye); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton(tab, colorSetIdx, rowIdx, disabled); + var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); ImGui.SameLine(); - ColorSetHighlightButton(tab, rowIdx, disabled); + ColorTableHighlightButton(tab, rowIdx, disabled); ImGui.TableNextColumn(); ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); @@ -288,8 +281,8 @@ public partial class ModEditWindow using var dis = ImRaii.Disabled(disabled); ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = c; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.Table[rowIdx].Diffuse = c; + tab.UpdateColorTableRowPreview(rowIdx); }); if (hasDye) { @@ -297,25 +290,25 @@ public partial class ModEditWindow ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, b => { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Diffuse = b; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.DyeTable[rowIdx].Diffuse = b; + tab.UpdateColorTableRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Specular = c; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.Table[rowIdx].Specular = c; + tab.UpdateColorTableRowPreview(rowIdx); }); ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.SpecularStrength = tmpFloat; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); @@ -326,23 +319,23 @@ public partial class ModEditWindow ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, b => { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Specular = b; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.DyeTable[rowIdx].Specular = b; + tab.UpdateColorTableRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, b => { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].SpecularStrength = b; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; + tab.UpdateColorTableRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Emissive = c; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.Table[rowIdx].Emissive = c; + tab.UpdateColorTableRowPreview(rowIdx); }); if (hasDye) { @@ -350,8 +343,8 @@ public partial class ModEditWindow ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, b => { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Emissive = b; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.DyeTable[rowIdx].Emissive = b; + tab.UpdateColorTableRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled); } @@ -361,9 +354,9 @@ public partial class ModEditWindow if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f") && FixFloat(ref tmpFloat, row.GlossStrength)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.GlossStrength = Math.Max(tmpFloat, HalfEpsilon); + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); @@ -373,8 +366,8 @@ public partial class ModEditWindow ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, b => { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Gloss = b; - tab.UpdateColorSetRowPreview(rowIdx); + tab.Mtrl.DyeTable[rowIdx].Gloss = b; + tab.UpdateColorTableRowPreview(rowIdx); }, ImGuiHoveredFlags.AllowWhenDisabled); } @@ -383,9 +376,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(intSize); if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); @@ -396,9 +389,9 @@ public partial class ModEditWindow if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); @@ -408,9 +401,9 @@ public partial class ModEditWindow if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); @@ -420,9 +413,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(floatSize); if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); @@ -432,9 +425,9 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(floatSize); if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) { - tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); @@ -445,27 +438,22 @@ public partial class ModEditWindow if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorSetRowPreview(rowIdx); + dye.Template = _stainService.TemplateCombo.CurrentSelection; + ret = true; + tab.UpdateColorTableRowPreview(rowIdx); } ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, colorSetIdx, rowIdx, disabled, dye, floatSize); - } - else if (hasAnyDye) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); } return ret; } - private bool DrawDyePreview(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, MtrlFile.ColorDyeTable.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) @@ -477,9 +465,9 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain); if (ret) - tab.UpdateColorSetRowPreview(rowIdx); + tab.UpdateColorTableRowPreview(rowIdx); ImGui.SameLine(); ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index aff30fb0..12119742 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -67,7 +67,6 @@ public partial class ModEditWindow public readonly HashSet UnfoldedTextures = new(4); public readonly HashSet SamplerIds = new(16); public float TextureLabelWidth; - public bool UseColorDyeSet; // Material Constants public readonly @@ -75,10 +74,10 @@ public partial class ModEditWindow Constants)> Constants = new(16); // Live-Previewers - public readonly List MaterialPreviewers = new(4); - public readonly List ColorSetPreviewers = new(4); - public int HighlightedColorSetRow = -1; - public readonly Stopwatch HighlightTime = new(); + public readonly List MaterialPreviewers = new(4); + public readonly List ColorTablePreviewers = new(4); + public int HighlightedColorTableRow = -1; + public readonly Stopwatch HighlightTime = new(); public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { @@ -286,7 +285,7 @@ public partial class ModEditWindow if (AssociatedShpk == null) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.ColorSets.Any(c => c.HasRows)) + if (Mtrl.HasTable) SamplerIds.Add(TableSamplerId); foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) @@ -301,7 +300,7 @@ public partial class ModEditWindow if (!ShadersKnown) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.ColorSets.Any(c => c.HasRows)) + if (Mtrl.HasTable) SamplerIds.Add(TableSamplerId); } @@ -320,7 +319,7 @@ public partial class ModEditWindow } if (SamplerIds.Contains(TableSamplerId)) - Mtrl.FindOrAddColorSet(); + Mtrl.HasTable = true; } Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); @@ -483,18 +482,16 @@ public partial class ModEditWindow } } - UpdateMaterialPreview(); - - var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); - - if (!colorSet.HasValue) + UpdateMaterialPreview(); + + if (!Mtrl.HasTable) return; foreach (var materialInfo in instances) { try { - ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); } catch (InvalidOperationException) { @@ -502,7 +499,7 @@ public partial class ModEditWindow } } - UpdateColorSetPreview(); + UpdateColorTablePreview(); } private void UnbindFromMaterialInstances() @@ -511,9 +508,9 @@ public partial class ModEditWindow previewer.Dispose(); MaterialPreviewers.Clear(); - foreach (var previewer in ColorSetPreviewers) + foreach (var previewer in ColorTablePreviewers) previewer.Dispose(); - ColorSetPreviewers.Clear(); + ColorTablePreviewers.Clear(); } private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) @@ -528,14 +525,14 @@ public partial class ModEditWindow MaterialPreviewers.RemoveAt(i); } - for (var i = ColorSetPreviewers.Count; i-- > 0;) + for (var i = ColorTablePreviewers.Count; i-- > 0;) { - var previewer = ColorSetPreviewers[i]; + var previewer = ColorTablePreviewers[i]; if ((nint)previewer.DrawObject != characterBase) continue; previewer.Dispose(); - ColorSetPreviewers.RemoveAt(i); + ColorTablePreviewers.RemoveAt(i); } } @@ -571,103 +568,94 @@ public partial class ModEditWindow SetSamplerFlags(sampler.SamplerId, sampler.Flags); } - public void HighlightColorSetRow(int rowIdx) + public void HighlightColorTableRow(int rowIdx) { - var oldRowIdx = HighlightedColorSetRow; + var oldRowIdx = HighlightedColorTableRow; - if (HighlightedColorSetRow != rowIdx) + if (HighlightedColorTableRow != rowIdx) { - HighlightedColorSetRow = rowIdx; + HighlightedColorTableRow = rowIdx; HighlightTime.Restart(); } if (oldRowIdx >= 0) - UpdateColorSetRowPreview(oldRowIdx); + UpdateColorTableRowPreview(oldRowIdx); if (rowIdx >= 0) - UpdateColorSetRowPreview(rowIdx); + UpdateColorTableRowPreview(rowIdx); } - public void CancelColorSetHighlight() + public void CancelColorTableHighlight() { - var rowIdx = HighlightedColorSetRow; + var rowIdx = HighlightedColorTableRow; - HighlightedColorSetRow = -1; + HighlightedColorTableRow = -1; HighlightTime.Reset(); if (rowIdx >= 0) - UpdateColorSetRowPreview(rowIdx); + UpdateColorTableRowPreview(rowIdx); } - public void UpdateColorSetRowPreview(int rowIdx) + public void UpdateColorTableRowPreview(int rowIdx) { - if (ColorSetPreviewers.Count == 0) + if (ColorTablePreviewers.Count == 0) + return; + + if (!Mtrl.HasTable) return; - var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); - if (!maybeColorSet.HasValue) - return; - - var colorSet = maybeColorSet.Value; - var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); - - var row = colorSet.Rows[rowIdx]; - if (maybeColorDyeSet.HasValue && UseColorDyeSet) + var row = Mtrl.Table[rowIdx]; + if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; - var dye = maybeColorDyeSet.Value.Rows[rowIdx]; + var dye = Mtrl.DyeTable[rowIdx]; if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } - if (HighlightedColorSetRow == rowIdx) + if (HighlightedColorTableRow == rowIdx) ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - foreach (var previewer in ColorSetPreviewers) + foreach (var previewer in ColorTablePreviewers) { - row.AsHalves().CopyTo(previewer.ColorSet.AsSpan() - .Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); + row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() + .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } - public void UpdateColorSetPreview() + public void UpdateColorTablePreview() { - if (ColorSetPreviewers.Count == 0) + if (ColorTablePreviewers.Count == 0) + return; + + if (!Mtrl.HasTable) return; - var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); - if (!maybeColorSet.HasValue) - return; - - var colorSet = maybeColorSet.Value; - var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); - - var rows = colorSet.Rows; - if (maybeColorDyeSet.HasValue && UseColorDyeSet) + var rows = Mtrl.Table; + if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - var colorDyeSet = maybeColorDyeSet.Value; - for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { ref var row = ref rows[i]; - var dye = colorDyeSet.Rows[i]; + var dye = Mtrl.DyeTable[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } } - if (HighlightedColorSetRow >= 0) - ApplyHighlight(ref rows[HighlightedColorSetRow], (float)HighlightTime.Elapsed.TotalSeconds); + if (HighlightedColorTableRow >= 0) + ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - foreach (var previewer in ColorSetPreviewers) + foreach (var previewer in ColorTablePreviewers) { - rows.AsHalves().CopyTo(previewer.ColorSet); + rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); } } - private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) + private static void ApplyHighlight(ref MtrlFile.ColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); @@ -691,7 +679,6 @@ public partial class ModEditWindow Mtrl = file; FilePath = filePath; Writable = writable; - UseColorDyeSet = file.ColorDyeSets.Length > 0; AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) @@ -714,7 +701,7 @@ public partial class ModEditWindow public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet); + output.GarbageCollect(AssociatedShpk, SamplerIds); return output.Write(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 102a6778..02845362 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -25,7 +25,7 @@ public partial class ModEditWindow ret |= DrawMaterialShader(tab, disabled); ret |= DrawMaterialTextureChange(tab, disabled); - ret |= DrawMaterialColorSetChange(tab, disabled); + ret |= DrawMaterialColorTableChange(tab, disabled); ret |= DrawMaterialConstants(tab, disabled); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -42,7 +42,7 @@ public partial class ModEditWindow if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); - if (tab.MaterialPreviewers.Count != 0 || tab.ColorSetPreviewers.Count != 0) + if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0) return; ImGui.SameLine(); @@ -159,6 +159,13 @@ public partial class ModEditWindow ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } + using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in file.ColorSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + if (file.AdditionalData.Length <= 0) return; From 50d7619dde0641f08d52baf225b5bf37a533f518 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Sep 2023 13:57:39 +0200 Subject: [PATCH 1168/2451] GameData Commit. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 862add38..3aab17bd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 862add38110116b9bb266b739489456a2dd699c0 +Subproject commit 3aab17bd6a91146bb858cabee928ed214ec4e95c From 916ff0cbb234cb2d0fc88b1d7169aa1d6aba9a54 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Sep 2023 14:00:30 +0200 Subject: [PATCH 1169/2451] Auto Formatting. --- Penumbra/Interop/Structs/CharacterBaseExt.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs index 1bc1c2f3..cfd43a21 100644 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -4,12 +4,12 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterBaseExt { - [FieldOffset( 0x0 )] + [FieldOffset(0x0)] public CharacterBase CharacterBase; - [FieldOffset( 0x258 )] + [FieldOffset(0x258)] public Texture** ColorTableTextures; -} \ No newline at end of file +} From 53adb6fa548c7a687e602ca6fc5d382fac88636b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 15 Sep 2023 14:12:59 +0200 Subject: [PATCH 1170/2451] Use System global usings. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/DalamudSubstitutionProvider.cs | 3 --- Penumbra/Api/HttpApi.cs | 2 -- Penumbra/Api/IpcTester.cs | 5 ----- Penumbra/Api/PenumbraApi.cs | 6 ------ Penumbra/Api/PenumbraIpcProviders.cs | 3 --- Penumbra/Api/TempModManager.cs | 2 -- Penumbra/Collections/Cache/CmpCache.cs | 3 --- Penumbra/Collections/Cache/CollectionCache.cs | 6 ------ .../Collections/Cache/CollectionCacheManager.cs | 6 ------ Penumbra/Collections/Cache/CollectionModData.cs | 3 --- Penumbra/Collections/Cache/EqdpCache.cs | 4 ---- Penumbra/Collections/Cache/EqpCache.cs | 3 --- Penumbra/Collections/Cache/EstCache.cs | 2 -- Penumbra/Collections/Cache/GmpCache.cs | 3 --- Penumbra/Collections/Cache/ImcCache.cs | 3 --- Penumbra/Collections/Cache/MetaCache.cs | 3 --- .../Manager/ActiveCollectionMigration.cs | 4 ---- Penumbra/Collections/Manager/ActiveCollections.cs | 4 ---- Penumbra/Collections/Manager/CollectionEditor.cs | 4 ---- Penumbra/Collections/Manager/CollectionStorage.cs | 5 ----- Penumbra/Collections/Manager/CollectionType.cs | 3 --- .../Manager/IndividualCollections.Access.cs | 3 --- .../Manager/IndividualCollections.Files.cs | 3 --- .../Collections/Manager/IndividualCollections.cs | 3 --- Penumbra/Collections/Manager/InheritanceManager.cs | 3 --- .../Collections/Manager/ModCollectionMigration.cs | 2 -- .../Collections/Manager/TempCollectionManager.cs | 3 --- Penumbra/Collections/ModCollection.Cache.Access.cs | 3 --- Penumbra/Collections/ModCollection.cs | 4 ---- Penumbra/Collections/ModCollectionSave.cs | 4 ---- Penumbra/Collections/ResolveData.cs | 2 -- Penumbra/CommandHandler.cs | 3 --- Penumbra/Communication/ChangedItemClick.cs | 1 - Penumbra/Communication/ChangedItemHover.cs | 1 - Penumbra/Communication/CollectionChange.cs | 1 - .../Communication/CollectionInheritanceChanged.cs | 1 - Penumbra/Communication/CreatedCharacterBase.cs | 1 - Penumbra/Communication/CreatingCharacterBase.cs | 1 - Penumbra/Communication/EnabledChanged.cs | 1 - Penumbra/Communication/ModDataChanged.cs | 1 - Penumbra/Communication/ModDirectoryChanged.cs | 1 - Penumbra/Communication/ModDiscoveryFinished.cs | 1 - Penumbra/Communication/ModDiscoveryStarted.cs | 1 - Penumbra/Communication/ModOptionChanged.cs | 1 - Penumbra/Communication/ModPathChanged.cs | 2 -- Penumbra/Communication/ModSettingChanged.cs | 1 - Penumbra/Communication/MtrlShpkLoaded.cs | 3 +-- Penumbra/Communication/PostSettingsPanelDraw.cs | 1 - Penumbra/Communication/PreSettingsPanelDraw.cs | 1 - Penumbra/Communication/ResolvedFileChanged.cs | 1 - Penumbra/Communication/SelectTab.cs | 1 - Penumbra/Communication/TemporaryGlobalModChange.cs | 1 - Penumbra/Configuration.cs | 5 ----- Penumbra/GlobalUsings.cs | 14 ++++++++++++++ Penumbra/Import/Structs/StreamDisposer.cs | 2 -- Penumbra/Import/Structs/TexToolsStructs.cs | 1 - Penumbra/Import/TexToolsImport.cs | 6 ------ Penumbra/Import/TexToolsImporter.Archives.cs | 5 +---- Penumbra/Import/TexToolsImporter.Gui.cs | 2 -- Penumbra/Import/TexToolsImporter.ModPack.cs | 4 ---- Penumbra/Import/TexToolsMeta.Deserialization.cs | 2 -- Penumbra/Import/TexToolsMeta.Export.cs | 4 ---- Penumbra/Import/TexToolsMeta.Rgsp.cs | 2 -- Penumbra/Import/TexToolsMeta.cs | 3 --- Penumbra/Import/Textures/BaseImage.cs | 2 -- .../Textures/CombinedTexture.Manipulation.cs | 5 ----- .../Import/Textures/CombinedTexture.Operations.cs | 4 ---- Penumbra/Import/Textures/CombinedTexture.cs | 5 ----- Penumbra/Import/Textures/RgbaPixelData.cs | 1 - Penumbra/Import/Textures/TexFileParser.cs | 2 -- Penumbra/Import/Textures/Texture.cs | 1 - Penumbra/Import/Textures/TextureDrawer.cs | 5 ----- Penumbra/Import/Textures/TextureManager.cs | 7 ------- .../MaterialPreview/LiveColorTablePreviewer.cs | 1 - .../MaterialPreview/LiveMaterialPreviewer.cs | 1 - .../MaterialPreview/LiveMaterialPreviewerBase.cs | 1 - Penumbra/Interop/MaterialPreview/MaterialInfo.cs | 2 -- .../Interop/PathResolving/AnimationHookService.cs | 2 -- .../Interop/PathResolving/CollectionResolver.cs | 1 - Penumbra/Interop/PathResolving/CutsceneService.cs | 4 ---- Penumbra/Interop/PathResolving/DrawObjectState.cs | 4 ---- .../PathResolving/IdentifiedCollectionCache.cs | 3 --- Penumbra/Interop/PathResolving/MetaState.cs | 2 -- Penumbra/Interop/PathResolving/PathResolver.cs | 1 - Penumbra/Interop/PathResolving/PathState.cs | 4 ---- Penumbra/Interop/PathResolving/ResolvePathHooks.cs | 2 -- Penumbra/Interop/PathResolving/SubfileHelper.cs | 4 ---- .../Interop/ResourceLoading/CreateFileWHook.cs | 3 --- .../Interop/ResourceLoading/FileReadService.cs | 4 ---- Penumbra/Interop/ResourceLoading/ResourceLoader.cs | 4 +--- .../ResourceLoading/ResourceManagerService.cs | 2 -- .../Interop/ResourceLoading/ResourceService.cs | 1 - Penumbra/Interop/ResourceLoading/TexMdlService.cs | 2 -- Penumbra/Interop/ResourceTree/ResolveContext.cs | 4 ---- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 -- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 -- .../Interop/ResourceTree/ResourceTreeFactory.cs | 2 -- Penumbra/Interop/ResourceTree/TreeBuildCache.cs | 4 ---- Penumbra/Interop/SafeHandles/SafeResourceHandle.cs | 5 +---- Penumbra/Interop/SafeHandles/SafeTextureHandle.cs | 5 +---- Penumbra/Interop/Services/CharacterUtility.cs | 3 --- Penumbra/Interop/Services/DecalReverter.cs | 1 - Penumbra/Interop/Services/GameEventManager.cs | 1 - Penumbra/Interop/Services/MetaList.cs | 2 -- Penumbra/Interop/Services/RedrawService.cs | 3 --- Penumbra/Interop/Services/SkinFixer.cs | 3 --- Penumbra/Interop/Structs/CharacterBaseExt.cs | 1 - Penumbra/Interop/Structs/CharacterUtilityData.cs | 3 --- Penumbra/Interop/Structs/ClipScheduler.cs | 3 --- Penumbra/Interop/Structs/ConstantBuffer.cs | 3 --- Penumbra/Interop/Structs/DrawState.cs | 2 -- Penumbra/Interop/Structs/GetResourceParameters.cs | 2 -- Penumbra/Interop/Structs/HumanExt.cs | 1 - Penumbra/Interop/Structs/Material.cs | 2 -- Penumbra/Interop/Structs/MtrlResource.cs | 2 -- Penumbra/Interop/Structs/RenderModel.cs | 1 - .../Interop/Structs/ResidentResourceManager.cs | 2 -- Penumbra/Interop/Structs/ResourceHandle.cs | 2 -- Penumbra/Interop/Structs/SeFileDescriptor.cs | 2 -- Penumbra/Interop/Structs/ShaderPackageUtility.cs | 2 -- Penumbra/Interop/Structs/VfxParams.cs | 2 -- Penumbra/Meta/Files/CmpFile.cs | 2 -- Penumbra/Meta/Files/EqdpFile.cs | 2 -- Penumbra/Meta/Files/EqpGmpFile.cs | 4 ---- Penumbra/Meta/Files/EstFile.cs | 2 -- Penumbra/Meta/Files/EvpFile.cs | 1 - Penumbra/Meta/Files/ImcFile.cs | 2 -- Penumbra/Meta/Files/MetaBaseFile.cs | 1 - Penumbra/Meta/Manipulations/EqdpManipulation.cs | 2 -- Penumbra/Meta/Manipulations/EqpManipulation.cs | 2 -- Penumbra/Meta/Manipulations/EstManipulation.cs | 2 -- Penumbra/Meta/Manipulations/GmpManipulation.cs | 1 - Penumbra/Meta/Manipulations/ImcManipulation.cs | 2 -- Penumbra/Meta/Manipulations/MetaManipulation.cs | 2 -- Penumbra/Meta/Manipulations/RspManipulation.cs | 2 -- Penumbra/Meta/MetaFileManager.cs | 3 --- Penumbra/Mods/Editor/DuplicateManager.cs | 8 -------- Penumbra/Mods/Editor/FileRegistry.cs | 3 --- Penumbra/Mods/Editor/IMod.cs | 1 - Penumbra/Mods/Editor/MdlMaterialEditor.cs | 4 ---- Penumbra/Mods/Editor/ModBackup.cs | 4 ---- Penumbra/Mods/Editor/ModEditor.cs | 2 -- Penumbra/Mods/Editor/ModFileCollection.cs | 5 ----- Penumbra/Mods/Editor/ModFileEditor.cs | 4 ---- Penumbra/Mods/Editor/ModMerger.cs | 4 ---- Penumbra/Mods/Editor/ModMetaEditor.cs | 3 --- Penumbra/Mods/Editor/ModNormalizer.cs | 5 ----- Penumbra/Mods/Editor/ModSwapEditor.cs | 1 - Penumbra/Mods/Editor/ModelMaterialInfo.cs | 3 --- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 3 --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 4 ---- Penumbra/Mods/ItemSwap/ItemSwap.cs | 2 -- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 ---- Penumbra/Mods/ItemSwap/Swaps.cs | 5 ----- Penumbra/Mods/Manager/ModCacheManager.cs | 5 ----- Penumbra/Mods/Manager/ModDataEditor.cs | 3 --- Penumbra/Mods/Manager/ModExportManager.cs | 2 -- Penumbra/Mods/Manager/ModFileSystem.cs | 4 ---- Penumbra/Mods/Manager/ModImportManager.cs | 5 ----- Penumbra/Mods/Manager/ModManager.cs | 4 ---- Penumbra/Mods/Manager/ModMigration.cs | 4 ---- Penumbra/Mods/Manager/ModOptionEditor.cs | 3 --- Penumbra/Mods/Manager/ModStorage.cs | 3 --- Penumbra/Mods/Mod.cs | 4 ---- Penumbra/Mods/ModCreator.cs | 4 ---- Penumbra/Mods/ModLocalData.cs | 3 --- Penumbra/Mods/ModMeta.cs | 1 - Penumbra/Mods/Subclasses/IModGroup.cs | 3 --- Penumbra/Mods/Subclasses/ISubMod.cs | 2 -- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 5 ----- Penumbra/Mods/Subclasses/ModSettings.cs | 4 ---- Penumbra/Mods/Subclasses/MultiModGroup.cs | 5 ----- Penumbra/Mods/Subclasses/SingleModGroup.cs | 4 ---- Penumbra/Mods/TemporaryMod.cs | 4 ---- Penumbra/Penumbra.cs | 3 --- Penumbra/Services/BackupService.cs | 3 --- Penumbra/Services/ChatService.cs | 1 - Penumbra/Services/CommunicatorService.cs | 1 - Penumbra/Services/ConfigMigrationService.cs | 4 ---- Penumbra/Services/DalamudServices.cs | 2 -- Penumbra/Services/FilenameService.cs | 3 --- Penumbra/Services/SaveService.cs | 1 - Penumbra/Services/ServiceWrapper.cs | 2 -- Penumbra/Services/StainService.cs | 3 --- Penumbra/Services/ValidityChecker.cs | 4 ---- Penumbra/UI/AdvancedWindow/FileEditor.cs | 5 ----- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 5 ----- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 4 ---- .../ModEditWindow.Materials.ColorTable.cs | 3 --- .../ModEditWindow.Materials.ConstantEditor.cs | 3 --- .../ModEditWindow.Materials.MtrlTab.cs | 7 ------- .../AdvancedWindow/ModEditWindow.Materials.Shpk.cs | 3 --- .../UI/AdvancedWindow/ModEditWindow.Materials.cs | 2 -- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 4 ---- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - .../UI/AdvancedWindow/ModEditWindow.QuickImport.cs | 5 ----- .../AdvancedWindow/ModEditWindow.ShaderPackages.cs | 5 ----- .../UI/AdvancedWindow/ModEditWindow.ShpkTab.cs | 2 -- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 6 ------ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 ------ Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 3 --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 7 +------ Penumbra/UI/ChangedItemDrawer.cs | 4 ---- Penumbra/UI/Changelog.cs | 1 - Penumbra/UI/Classes/CollectionSelectHeader.cs | 3 --- Penumbra/UI/Classes/Colors.cs | 2 -- Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 -- Penumbra/UI/CollectionTab/CollectionPanel.cs | 4 ---- Penumbra/UI/CollectionTab/CollectionSelector.cs | 3 --- .../UI/CollectionTab/IndividualAssignmentUi.cs | 2 -- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 ---- Penumbra/UI/ConfigWindow.cs | 2 -- Penumbra/UI/FileDialogService.cs | 4 ---- Penumbra/UI/ImportPopup.cs | 2 -- Penumbra/UI/LaunchButton.cs | 2 -- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 5 ----- Penumbra/UI/ModsTab/ModFilter.cs | 2 -- Penumbra/UI/ModsTab/ModPanel.cs | 3 --- Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 4 ---- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 -- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 -- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 1 - Penumbra/UI/ModsTab/ModPanelEditTab.cs | 5 ----- Penumbra/UI/ModsTab/ModPanelHeader.cs | 3 --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 3 --- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 -- Penumbra/UI/ResourceWatcher/Record.cs | 1 - Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 3 --- .../UI/ResourceWatcher/ResourceWatcherTable.cs | 4 ---- Penumbra/UI/Tabs/ChangedItemsTab.cs | 4 ---- Penumbra/UI/Tabs/CollectionsTab.cs | 2 -- Penumbra/UI/Tabs/ConfigTabBar.cs | 1 - Penumbra/UI/Tabs/DebugTab.cs | 4 ---- Penumbra/UI/Tabs/EffectiveTab.cs | 4 ---- Penumbra/UI/Tabs/ModsTab.cs | 3 --- Penumbra/UI/Tabs/OnScreenTab.cs | 1 - Penumbra/UI/Tabs/ResourceTab.cs | 3 --- Penumbra/UI/Tabs/SettingsTab.cs | 4 ---- Penumbra/UI/TutorialService.cs | 2 -- Penumbra/UI/UiHelpers.cs | 3 --- Penumbra/UI/WindowSystem.cs | 1 - Penumbra/Util/DictionaryExtensions.cs | 4 ---- Penumbra/Util/FixedUlongStringEnumConverter.cs | 1 - Penumbra/Util/PenumbraSqPackStream.cs | 4 ---- 248 files changed, 24 insertions(+), 695 deletions(-) create mode 100644 Penumbra/GlobalUsings.cs diff --git a/OtterGui b/OtterGui index ee64ae2d..21333f3e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ee64ae2d2710aea45365dafa3c91a721e59ae8fc +Subproject commit 21333f3e2f3908d4f4c7dbb0b4bff5e5b7d1f64a diff --git a/Penumbra.Api b/Penumbra.Api index 97610e1c..316f3da4 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 97610e1c9d27d863ae8563bb9c8525cc6f8a3709 +Subproject commit 316f3da4a3ce246afe5b98c1568d73fcd7b6b22d diff --git a/Penumbra.GameData b/Penumbra.GameData index 3aab17bd..0507b1f0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3aab17bd6a91146bb858cabee928ed214ec4e95c +Subproject commit 0507b1f093f5382e03242e5da991752361b70c6e diff --git a/Penumbra.String b/Penumbra.String index 5d9f36c5..0e0c1e1e 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 5d9f36c5b57685b07354460e225e65759ef9996e +Subproject commit 0e0c1e1ee116c259abd00e1d5c3450ad40f92a98 diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 07da761f..fb966fe8 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Plugin.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 0d9ef997..e23f8b4f 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index cbd0cc8b..69c2fd3d 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -4,12 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5ac53210..1dead7e5 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -7,13 +7,7 @@ using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Compression; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 73f87b94..2d3fcf97 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -1,9 +1,6 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Penumbra.GameData.Enums; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Collections.Manager; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 1dd8331f..35aa1217 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,8 +1,6 @@ -using System; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using System.Collections.Generic; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs index 9333501a..470cadd4 100644 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ b/Penumbra/Collections/Cache/CmpCache.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 1f712124..6f3d59e9 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -2,12 +2,6 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index abe5bfca..3a94bc89 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,10 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Dalamud.Game; using OtterGui.Classes; using Penumbra.Api; diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index 24f8f297..fcbccc96 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 1b9c8156..a5232dbb 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.Enums; diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 4e87a34a..9d63479a 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index d079a532..43ebcf56 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Services; diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 1c25b9e5..3287eb2d 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 28680d11..e226b409 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index ee589d1b..fa60993a 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Penumbra.GameData.Enums; using Penumbra.Interop.Services; diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 7ea52eb6..5fcfd2f9 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 69cd1239..58eb7517 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 5aad1595..3d0ef60e 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 0bee38cf..c172aeff 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index 40f0f488..8e2d1aed 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -1,7 +1,4 @@ using Penumbra.GameData.Enums; -using System; -using System.Collections.Generic; -using System.Linq; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index b1b0698a..ac4acb8e 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -1,7 +1,4 @@ -using System.Collections; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index d670fc42..da403337 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index c4d9c516..e7547153 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index fef7bdbc..4c1bdc5a 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index c1f158ea..56135182 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,6 +1,4 @@ using Penumbra.Mods; -using System.Collections.Generic; -using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 400bd2cf..133a0990 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index a3e43afd..ae68a370 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,10 +1,7 @@ using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.Mods; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index ca20f371..cb4aecc6 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,8 +1,4 @@ using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Services; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 72b2e94c..05a8d9b0 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -1,10 +1,6 @@ using Newtonsoft.Json.Linq; using Penumbra.Mods; using Penumbra.Services; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; using Penumbra.Mods.Manager; using Penumbra.Util; diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index e0901e98..0c7bc967 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,5 +1,3 @@ -using System; - namespace Penumbra.Collections; public readonly struct ResolveData diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index a78c4da7..d1830617 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 01a5fd56..1e5bc863 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api.Enums; diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index c0736256..cf270ba0 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 96dd61ab..b815c48e 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index 00e90546..8288341d 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Collections; diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index 69d84ce2..b1903e5b 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api; using Penumbra.Collections; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 6161a984..090ca40b 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api; diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index 38c6b387..fa768235 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api; diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index 941ed4d5..cca546e0 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Mods; using Penumbra.Mods.Manager; diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 5a3fb473..9fdb261e 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api; diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs index b8e4e460..8f5d31d5 100644 --- a/Penumbra/Communication/ModDiscoveryFinished.cs +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs index ee193681..a2ff8633 100644 --- a/Penumbra/Communication/ModDiscoveryStarted.cs +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index a86826a3..416cc8df 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Mods; using Penumbra.Mods.Manager; diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 675759dd..99ec9109 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using OtterGui.Classes; using Penumbra.Api; using Penumbra.Mods; diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index c3c9f671..65bcf3ed 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 4b5600c9..868692cd 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -1,5 +1,4 @@ -using System; -using OtterGui.Classes; +using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index f653bd0b..b32b0dfa 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index 1167e92f..e5857474 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 99cec829..55e95320 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Mods; diff --git a/Penumbra/Communication/SelectTab.cs b/Penumbra/Communication/SelectTab.cs index 3dcf6d1b..aaa362f6 100644 --- a/Penumbra/Communication/SelectTab.cs +++ b/Penumbra/Communication/SelectTab.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Mods; diff --git a/Penumbra/Communication/TemporaryGlobalModChange.cs b/Penumbra/Communication/TemporaryGlobalModChange.cs index 8906627b..12d42e48 100644 --- a/Penumbra/Communication/TemporaryGlobalModChange.cs +++ b/Penumbra/Communication/TemporaryGlobalModChange.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Mods; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a80563c9..72f8bb95 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Configuration; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; diff --git a/Penumbra/GlobalUsings.cs b/Penumbra/GlobalUsings.cs new file mode 100644 index 00000000..0f6538bb --- /dev/null +++ b/Penumbra/GlobalUsings.cs @@ -0,0 +1,14 @@ +// Global using directives + +global using System; +global using System.Collections; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Numerics; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; +global using System.Security.Cryptography; +global using System.Threading; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/Penumbra/Import/Structs/StreamDisposer.cs b/Penumbra/Import/Structs/StreamDisposer.cs index 65c67585..7a755c40 100644 --- a/Penumbra/Import/Structs/StreamDisposer.cs +++ b/Penumbra/Import/Structs/StreamDisposer.cs @@ -1,6 +1,4 @@ using Penumbra.Util; -using System; -using System.IO; namespace Penumbra.Import.Structs; diff --git a/Penumbra/Import/Structs/TexToolsStructs.cs b/Penumbra/Import/Structs/TexToolsStructs.cs index 2a160c62..bf3bccd8 100644 --- a/Penumbra/Import/Structs/TexToolsStructs.cs +++ b/Penumbra/Import/Structs/TexToolsStructs.cs @@ -1,4 +1,3 @@ -using System; using Penumbra.Api.Enums; namespace Penumbra.Import.Structs; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 49b344da..ad61398f 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Newtonsoft.Json; using OtterGui.Compression; using Penumbra.Import.Structs; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 13200a9c..a41b9f24 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -2,7 +2,7 @@ using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; using SharpCompress.Archives.Rar; @@ -10,9 +10,6 @@ using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Readers; -using System; -using System.IO; -using System.Linq; namespace Penumbra.Import; diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index db818341..0c3e084d 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 32c9c0e1..73b5d976 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Import.Structs; diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 49501d91..eea1d811 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 2eac8f59..46636362 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 7bb837ce..0fd94fb4 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Penumbra.GameData.Enums; using Penumbra.Meta; using Penumbra.Meta.Files; diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index b07ac8ab..b44f99a8 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; using Penumbra.GameData; using Penumbra.Import.Structs; diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs index f0f6a47e..4f8c5305 100644 --- a/Penumbra/Import/Textures/BaseImage.cs +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Lumina.Data.Files; using OtterTex; using SixLabors.ImageSharp; diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 9bc4a2a5..8bac0a3b 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using System.Threading.Tasks; using ImGuiNET; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; using Dalamud.Interface; using Penumbra.UI; -using System.Linq; namespace Penumbra.Import.Textures; diff --git a/Penumbra/Import/Textures/CombinedTexture.Operations.cs b/Penumbra/Import/Textures/CombinedTexture.Operations.cs index 441cd3f0..8494f12b 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Operations.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Operations.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Numerics; - namespace Penumbra.Import.Textures; public partial class CombinedTexture diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index b7e2a90a..98b87ac3 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,8 +1,3 @@ -using System; -using System.IO; -using System.Numerics; -using System.Threading.Tasks; - namespace Penumbra.Import.Textures; public partial class CombinedTexture : IDisposable diff --git a/Penumbra/Import/Textures/RgbaPixelData.cs b/Penumbra/Import/Textures/RgbaPixelData.cs index 0314b104..32540f16 100644 --- a/Penumbra/Import/Textures/RgbaPixelData.cs +++ b/Penumbra/Import/Textures/RgbaPixelData.cs @@ -1,4 +1,3 @@ -using System; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index f84442c1..7f324601 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Lumina.Data.Files; using Lumina.Extensions; using OtterTex; diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index aefe72b4..fe1d3371 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,4 +1,3 @@ -using System; using ImGuiScene; using OtterTex; diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index d1b78268..b94fdbf8 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using Lumina.Data.Files; diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 9e4890f2..90dfa3d0 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,11 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Threading; -using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Plugin.Services; using ImGuiScene; diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index c9505091..f83b1531 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Game; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 1b280b20..9d2fc9eb 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs index 88369725..86fee976 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 0146cf6f..3f02e7e8 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index b1efb1b9..cc679bfd 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading; using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.Plugin.Services; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index a795b46b..3f73643b 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 4273ae01..add121b6 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Penumbra.GameData.Actors; diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 12f867b4..512d370a 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,9 +1,5 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 2bf165c1..546ffd92 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index f7a754fb..6afaf5d1 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index cc513a92..2176ebba 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index de9d65ee..0c6566a3 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData; diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index 609c131d..6f8f93dd 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index c0b8c5e3..b1bc806d 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index 3f8c8d27..6b751db7 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -1,7 +1,4 @@ -using System; -using System.Runtime.InteropServices; using System.Text; -using System.Threading; using Dalamud.Hooking; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index b09d568e..6ca0efb4 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -1,7 +1,3 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index da50f26e..5d5e4590 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,12 +1,10 @@ -using System; -using System.Diagnostics; -using System.Threading; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index 4458e699..0b29ee10 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 645596a3..8b4a5d15 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index e3d48cec..6dac0e6b 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 24eea690..0bbe8e66 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 17584787..cae89c09 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.String.Classes; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 755103d7..d2276291 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 8d5318f2..a2e29e48 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index d29916dd..43b25476 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Files; diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs index 7ec0f218..8dd1fb4a 100644 --- a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -1,7 +1,4 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; namespace Penumbra.Interop.SafeHandles; diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs index 36cd4612..88c97c54 100644 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -1,7 +1,4 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using Penumbra.Interop.Structs; namespace Penumbra.Interop.SafeHandles; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 00eab531..48824888 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Game; using Dalamud.Utility.Signatures; using Penumbra.Collections.Manager; diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index a8aa3fa2..21fa87a1 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -1,4 +1,3 @@ -using System; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData.Enums; diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index ea8d32d1..ca333ed4 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -1,7 +1,6 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.GameData; -using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index e2603f79..e956040b 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 38b7de1c..0b9c6c38 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index bb9e0983..211a062c 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -1,6 +1,3 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs index cfd43a21..7cdcc6fe 100644 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 765ad25f..87d9566c 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Runtime.InteropServices; using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index d968ffbe..c8fd082d 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -1,6 +1,3 @@ -using System; -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs index 52df6477..d61aaeea 100644 --- a/Penumbra/Interop/Structs/ConstantBuffer.cs +++ b/Penumbra/Interop/Structs/ConstantBuffer.cs @@ -1,6 +1,3 @@ -using System; -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit, Size = 0x70)] diff --git a/Penumbra/Interop/Structs/DrawState.cs b/Penumbra/Interop/Structs/DrawState.cs index b30d1a76..d0dfc22b 100644 --- a/Penumbra/Interop/Structs/DrawState.cs +++ b/Penumbra/Interop/Structs/DrawState.cs @@ -1,5 +1,3 @@ -using System; - namespace Penumbra.Interop.Structs; [Flags] diff --git a/Penumbra/Interop/Structs/GetResourceParameters.cs b/Penumbra/Interop/Structs/GetResourceParameters.cs index eb413ead..ef665b36 100644 --- a/Penumbra/Interop/Structs/GetResourceParameters.cs +++ b/Penumbra/Interop/Structs/GetResourceParameters.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 33d83b06..5eafa1f3 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 3a204c75..2bca832d 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 424adfe4..55c7f8d8 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs index 9c8581b0..25e928be 100644 --- a/Penumbra/Interop/Structs/RenderModel.cs +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/ResidentResourceManager.cs b/Penumbra/Interop/Structs/ResidentResourceManager.cs index d5dd1715..08461b03 100644 --- a/Penumbra/Interop/Structs/ResidentResourceManager.cs +++ b/Penumbra/Interop/Structs/ResidentResourceManager.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 5db0f8e1..b23cf62f 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs index 4e1a1f57..d4ca3afa 100644 --- a/Penumbra/Interop/Structs/SeFileDescriptor.cs +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs index 5bf95f5b..9f7ec1f5 100644 --- a/Penumbra/Interop/Structs/ShaderPackageUtility.cs +++ b/Penumbra/Interop/Structs/ShaderPackageUtility.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; public static class ShaderPackageUtility diff --git a/Penumbra/Interop/Structs/VfxParams.cs b/Penumbra/Interop/Structs/VfxParams.cs index 644d5a9a..76dbd3ed 100644 --- a/Penumbra/Interop/Structs/VfxParams.cs +++ b/Penumbra/Interop/Structs/VfxParams.cs @@ -1,5 +1,3 @@ -using System.Runtime.InteropServices; - namespace Penumbra.Interop.Structs; [StructLayout( LayoutKind.Explicit )] diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index e67c5efd..8a6040ec 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -1,8 +1,6 @@ -using System; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; -using System.Collections.Generic; using Penumbra.Interop.Services; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 6d1b5476..8a99225f 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 71563ef9..6e9fd010 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Numerics; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 81749d46..2c7409b4 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index 0b64e1e8..3d0b4dbe 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -1,4 +1,3 @@ -using System; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 8c957bcc..94bc2428 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 46e87567..ab08efc2 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Memory; using Penumbra.Interop.Structs; using Penumbra.String.Functions; diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 8524ab8c..1e0756f2 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 91949ae4..8e1a2ded 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 3496c56c..da834b35 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 6ce954fb..94f7f971 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using Newtonsoft.Json; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 51ecd0fb..391daacc 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Data; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 1e0760f1..94b45cdf 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index e61fafea..7e5e3fcb 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Penumbra.GameData.Enums; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 171ea729..2f19537b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Memory; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 3a58d91a..12fb4f35 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 2ce22ec1..2100526a 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 88f04ef3..6fa645a9 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using OtterGui.Classes; using Penumbra.Mods.Subclasses; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 0fe9ec46..1c1a10b4 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using OtterGui; diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index eb15de87..f4904271 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.IO.Compression; -using System.Threading.Tasks; using OtterGui.Tasks; using Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index f65ce280..2f39970d 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 5ef290dc..85a7a544 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using OtterGui; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 4f9d22b3..1ca3edd4 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 3b5babcc..5c9be2ac 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using OtterGui; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 4d845a7c..e389c86a 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 62d87815..9b17c390 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Tasks; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 58ef10a0..c00bb821 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs index 38e76deb..741c2388 100644 --- a/Penumbra/Mods/Editor/ModelMaterialInfo.cs +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OtterGui; using OtterGui.Compression; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index 7fd77199..da56d204 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; -using System.Linq; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 72cb9a03..188fc317 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 6ed2112a..02e02ccb 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,6 +1,4 @@ -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text.RegularExpressions; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index d1af1d09..8e6473cc 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -4,10 +4,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Penumbra.Meta; using Penumbra.Mods.Manager; using Penumbra.Services; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 36ca77cf..0fa81a52 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,11 +1,6 @@ -using System; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; using Penumbra.GameData.Enums; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 1ace9536..afd42f95 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Data; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 43175815..66101dcd 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; -using System.Linq; using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index f2bfb9bc..7d79d3d4 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 8e6e729c..f7006c4c 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; using Penumbra.Communication; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 45b3f2ec..6c49ccf8 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using Penumbra.Import; using Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index ac973be4..f8d293a2 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index d6c5e63f..3cfab943 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 4bf774fb..3eeb13c6 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 8421d6e2..377bb4a7 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using OtterGui.Classes; using OtterGui.Widgets; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 4df41fb5..4ce6da9a 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Mods.Subclasses; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index d63627f5..b5462eee 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Dalamud.Interface.Internal.Notifications; diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index 71aae013..56ee827b 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 66979345..49094aa0 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,4 +1,3 @@ -using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Services; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index f66f29ea..957fe21d 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Services; diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index bf11527f..ac92cd13 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.IO; using Newtonsoft.Json; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Subclasses; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index aad74a13..7c43de86 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; using Newtonsoft.Json.Linq; using Penumbra.Import; using Penumbra.Meta; diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index ae06a082..537c989f 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index facbacdc..d40a79be 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index a67ee1e5..a69238f5 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index ef721414..fcd4a5f7 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index eeee8fd0..1402ef89 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; -using System.Linq; using System.Text; using Dalamud.Plugin; using ImGuiNET; diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index 1acb5bdc..0c13217a 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Util; diff --git a/Penumbra/Services/ChatService.cs b/Penumbra/Services/ChatService.cs index 9ea52cac..adb24618 100644 --- a/Penumbra/Services/ChatService.cs +++ b/Penumbra/Services/ChatService.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 97340f6b..3e61e3c1 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Communication; diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index b9e58deb..89f1063e 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 03978566..6754d0bd 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; @@ -7,7 +6,6 @@ using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.Plugin; -using System.Linq; using System.Reflection; using Dalamud.Interface.DragDrop; using Dalamud.Plugin.Services; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index b5fa5487..c7ed6061 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using Dalamud.Plugin; using OtterGui.Filesystem; using Penumbra.Collections; diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 8429863b..0e61c565 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Mods; diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index ca1e3624..67ddb63d 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using OtterGui.Tasks; using Penumbra.Util; diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 56893d70..1c6b3ef1 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Dalamud.Plugin; using Dalamud.Plugin.Services; using OtterGui.Widgets; diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 3d09f097..e97a9a82 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 8ed59fed..2591145f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Services; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index c7c09de2..87598f5a 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index fb1e59aa..d14c7125 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 2d868a42..4f04150f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -1,6 +1,3 @@ -using System; -using System.Numerics; -using System.Runtime.InteropServices; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 19e96539..7b80920d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Numerics; using ImGuiNET; using OtterGui.Raii; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 12119742..187d86c7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -12,7 +6,6 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index f6f480e7..a5746ec7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Numerics; using System.Text; using Dalamud.Interface; using ImGuiNET; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index ad5c806c..73aad323 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index fd4c082e..a6a93a36 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 518566f5..c90eb2b4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -4,7 +4,6 @@ using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; using System.Globalization; -using System.Linq; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 88ed10df..43a1990e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using Lumina.Data; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index c0868b71..fb2b5128 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using System.Text; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 2df52130..58e2e0c1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Dalamud.Utility; using Lumina.Misc; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 12f98ccb..20aea21e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6e1b1d24..703329e6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; using System.Text; using Dalamud.Interface; using Dalamud.Interface.Components; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index f7490c93..75f75de0 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 7da0275b..f435cb98 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using System.Threading.Tasks; using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; -using System.Linq; - + namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 902f6671..91d81ea3 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Plugin.Services; using Dalamud.Utility; diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index a91c2150..d7845ab7 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using OtterGui.Widgets; namespace Penumbra.UI; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index f71ae323..f4fa1b68 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Numerics; using ImGuiNET; using OtterGui.Raii; using OtterGui; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index e2acc1a3..eb4dec4c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using OtterGui.Custom; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 94e6f6d2..fc37eaf2 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index b5e60380..fe248d99 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 746c2d5f..e568ecaf 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index 376b7ad8..da011bde 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Enums; using ImGuiNET; using OtterGui.Custom; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 87c09917..4bcff426 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 6259ffe2..5cedd824 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index c483c3b1..dc638ef5 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index f97213c1..228cf4e1 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui.Raii; diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 3aa470f5..5b9bf5a4 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Dalamud.Interface; using Dalamud.Plugin; using ImGuiScene; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8d978413..91da4da5 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,8 +1,3 @@ -using System; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; using Dalamud.Interface.DragDrop; diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs index 03fdc177..4b2798f7 100644 --- a/Penumbra/UI/ModsTab/ModFilter.cs +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -1,5 +1,3 @@ -using System; - namespace Penumbra.UI.ModsTab; [Flags] diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index f0d28dab..1866606a 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index df2c905e..e897f940 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 1bdb32c0..22f8022e 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 68a50123..83f79f56 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 9cb229d1..256be8d6 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index bd5a62c4..692cf8b7 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index e1da057f..2c71426f 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -1,6 +1,3 @@ -using System; -using System.Diagnostics; -using System.Numerics; using Dalamud.Interface.GameFonts; using Dalamud.Plugin; using ImGuiNET; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index ad0f2e40..acfdcf28 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Numerics; using ImGuiNET; using OtterGui.Raii; using OtterGui; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 503e471f..3f1b0f77 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index e438be27..ec66ae08 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index ec0274b6..e8031d4e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 0b2f2ae5..905307ba 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index f85387f8..4605527a 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index d0f227c8..4d6d2dd6 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,5 +1,3 @@ -using System; -using System.Numerics; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Plugin; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 49b6348a..60339893 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,3 @@ -using System; using ImGuiNET; using OtterGui.Widgets; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 72122722..fc0168a0 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Utility; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index de9ad706..3c1d0801 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 10e4be1d..16f0180e 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -2,9 +2,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; -using System; -using System.Linq; -using System.Numerics; using Dalamud.Interface; using Dalamud.Plugin.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index c8f86333..8d323baf 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Widgets; using Penumbra.Interop.ResourceTree; using Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index db1236bb..020493d1 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Numerics; using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 7f27e6ee..701d1ef6 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Utility; diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 7f9b2ce3..87e709c3 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -1,5 +1,3 @@ -using System; -using System.Runtime.CompilerServices; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 8ae27dd6..7f4fc7cb 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -1,6 +1,3 @@ -using System.Diagnostics; -using System.IO; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index bde27bfc..f08b6de9 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 31931fe7..74274c38 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - namespace Penumbra.Util; public static class DictionaryExtensions diff --git a/Penumbra/Util/FixedUlongStringEnumConverter.cs b/Penumbra/Util/FixedUlongStringEnumConverter.cs index 750422b4..85c61837 100644 --- a/Penumbra/Util/FixedUlongStringEnumConverter.cs +++ b/Penumbra/Util/FixedUlongStringEnumConverter.cs @@ -1,4 +1,3 @@ -using System; using Newtonsoft.Json; using Newtonsoft.Json.Converters; diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 0109ff35..d5b51433 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -1,8 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; using System.IO.Compression; -using System.Runtime.InteropServices; using Lumina.Data.Structs; using Lumina.Extensions; From 2b4a01df06fbc896789e12861891895b7a148f3d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 16:56:16 +0200 Subject: [PATCH 1171/2451] Make line endings explicit in editorconfig and share in sub projects, also apply editorconfig everywhere and move some namespaces. --- .editorconfig | 82 +++--- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/PenumbraApi.cs | 3 +- Penumbra/Api/TempModManager.cs | 6 +- Penumbra/Collections/Cache/CollectionCache.cs | 9 +- .../Cache/CollectionCacheManager.cs | 1 - Penumbra/Collections/Cache/EqdpCache.cs | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 45 ++- Penumbra/Collections/Cache/GmpCache.cs | 32 +-- Penumbra/Collections/Cache/ImcCache.cs | 16 +- Penumbra/Collections/Cache/MetaCache.cs | 8 +- .../Manager/ActiveCollectionMigration.cs | 11 +- .../Collections/Manager/ActiveCollections.cs | 17 +- .../Collections/Manager/CollectionEditor.cs | 1 + .../Collections/Manager/CollectionStorage.cs | 3 +- .../Collections/Manager/CollectionType.cs | 14 +- .../Manager/IndividualCollections.Access.cs | 9 +- .../Manager/IndividualCollections.Files.cs | 5 +- .../Manager/IndividualCollections.cs | 3 +- .../Manager/ModCollectionMigration.cs | 1 + .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Collections/ModCollection.cs | 18 +- Penumbra/Collections/ModCollectionSave.cs | 5 +- Penumbra/Collections/ResolveData.cs | 4 +- Penumbra/CommandHandler.cs | 3 +- Penumbra/Communication/CollectionChange.cs | 1 - Penumbra/Communication/ModDataChanged.cs | 2 +- .../Communication/ModDiscoveryFinished.cs | 3 +- Penumbra/Communication/ModDiscoveryStarted.cs | 1 + Penumbra/Communication/ModPathChanged.cs | 3 +- Penumbra/Configuration.cs | 3 +- Penumbra/GlobalUsings.cs | 4 +- Penumbra/Import/Structs/ImporterState.cs | 2 +- Penumbra/Import/Structs/StreamDisposer.cs | 2 +- Penumbra/Import/TexToolsImporter.Archives.cs | 99 ++++--- Penumbra/Import/TexToolsImporter.Gui.cs | 78 +++--- .../Import/TexToolsMeta.Deserialization.cs | 2 +- Penumbra/Import/TexToolsMeta.Rgsp.cs | 66 +++-- Penumbra/Import/TexToolsMeta.cs | 76 +++-- Penumbra/Import/Textures/RgbaPixelData.cs | 3 +- Penumbra/Import/Textures/TexFileParser.cs | 4 +- Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Import/Textures/TextureManager.cs | 1 - .../LiveColorTablePreviewer.cs | 3 +- .../PathResolving/AnimationHookService.cs | 26 +- .../PathResolving/CollectionResolver.cs | 2 +- .../Interop/PathResolving/DrawObjectState.cs | 6 +- .../IdentifiedCollectionCache.cs | 8 +- .../Interop/PathResolving/PathResolver.cs | 1 + .../Interop/PathResolving/SubfileHelper.cs | 1 - .../ResourceLoading/FileReadService.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceNode.cs | 7 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 48 ++-- .../Interop/SafeHandles/SafeResourceHandle.cs | 12 +- .../Interop/SafeHandles/SafeTextureHandle.cs | 15 +- Penumbra/Interop/Services/DecalReverter.cs | 6 +- Penumbra/Interop/Services/GameEventManager.cs | 4 +- Penumbra/Interop/Services/RedrawService.cs | 6 +- .../Services/ResidentResourceManager.cs | 4 +- .../Interop/Structs/CharacterUtilityData.cs | 54 ++-- Penumbra/Interop/Structs/ClipScheduler.cs | 8 +- Penumbra/Interop/Structs/DrawState.cs | 2 +- Penumbra/Interop/Structs/FileMode.cs | 2 +- Penumbra/Interop/Structs/HumanExt.cs | 12 +- Penumbra/Interop/Structs/Material.cs | 29 +- Penumbra/Interop/Structs/MtrlResource.cs | 26 +- Penumbra/Interop/Structs/RenderModel.cs | 26 +- .../Structs/ResidentResourceManager.cs | 10 +- Penumbra/Interop/Structs/ResourceHandle.cs | 92 +++--- Penumbra/Interop/Structs/SeFileDescriptor.cs | 19 +- Penumbra/Interop/Structs/TextureUtility.cs | 5 +- Penumbra/Interop/Structs/VfxParams.cs | 12 +- .../Meta/Manipulations/EqdpManipulation.cs | 4 +- .../Meta/Manipulations/EqpManipulation.cs | 50 ++-- .../Meta/Manipulations/EstManipulation.cs | 57 ++-- .../Meta/Manipulations/GmpManipulation.cs | 36 ++- Penumbra/Meta/MetaFileManager.cs | 1 + Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/Editor/FileRegistry.cs | 1 + Penumbra/Mods/Editor/IMod.cs | 14 +- Penumbra/Mods/Editor/ModBackup.cs | 19 +- Penumbra/Mods/Editor/ModFileCollection.cs | 1 + Penumbra/Mods/Editor/ModFileEditor.cs | 3 +- Penumbra/Mods/Editor/ModMerger.cs | 3 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 + Penumbra/Mods/Editor/ModNormalizer.cs | 1 + Penumbra/Mods/Editor/ModSwapEditor.cs | 3 +- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 94 ++++--- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 26 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 153 +++++----- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 114 ++++---- Penumbra/Mods/Manager/ModExportManager.cs | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 4 +- Penumbra/Mods/Manager/ModImportManager.cs | 1 - Penumbra/Mods/Manager/ModManager.cs | 11 +- Penumbra/Mods/Manager/ModMigration.cs | 17 +- Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/ModCreator.cs | 2 - Penumbra/Mods/ModLocalData.cs | 1 - Penumbra/Mods/ModMeta.cs | 1 - Penumbra/Mods/Subclasses/ISubMod.cs | 53 ++-- Penumbra/Mods/Subclasses/ModSettings.cs | 5 +- Penumbra/Mods/Subclasses/MultiModGroup.cs | 5 +- Penumbra/Mods/Subclasses/SingleModGroup.cs | 85 +++--- .../{Mod.Files.SubMod.cs => SubMod.cs} | 5 +- Penumbra/Mods/TemporaryMod.cs | 75 ++--- Penumbra/Penumbra.cs | 1 + Penumbra/Services/ConfigMigrationService.cs | 4 +- Penumbra/Services/DalamudServices.cs | 1 - Penumbra/Services/ServiceManager.cs | 1 + Penumbra/Services/StainService.cs | 13 +- Penumbra/Services/ValidityChecker.cs | 18 +- Penumbra/Services/Wrappers.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 7 +- .../ModEditWindow.Materials.ColorTable.cs | 13 +- .../ModEditWindow.Materials.MtrlTab.cs | 22 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 66 +++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 159 +++++------ .../ModEditWindow.QuickImport.cs | 1 + .../ModEditWindow.ShaderPackages.cs | 8 + .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 21 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 73 ++--- Penumbra/UI/Changelog.cs | 121 +++++--- Penumbra/UI/Classes/Colors.cs | 22 +- Penumbra/UI/Classes/Combos.cs | 53 ++-- Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 84 +++--- Penumbra/UI/FileDialogService.cs | 2 - Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 7 +- Penumbra/UI/ModsTab/ModFilter.cs | 80 +++--- Penumbra/UI/ModsTab/ModPanel.cs | 1 + Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 1 + Penumbra/UI/ResourceWatcher/Record.cs | 3 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 7 +- .../ResourceWatcher/ResourceWatcherTable.cs | 3 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 4 +- Penumbra/UI/Tabs/CollectionsTab.cs | 7 +- Penumbra/UI/Tabs/DebugTab.cs | 5 +- Penumbra/UI/Tabs/EffectiveTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 5 +- Penumbra/UI/TutorialService.cs | 28 +- Penumbra/UI/UiHelpers.cs | 4 +- Penumbra/UI/WindowSystem.cs | 3 +- Penumbra/Util/DictionaryExtensions.cs | 70 ++--- .../Util/FixedUlongStringEnumConverter.cs | 66 ++--- Penumbra/Util/PenumbraSqPackStream.cs | 262 ++++++++---------- Penumbra/Util/PerformanceType.cs | 2 +- 155 files changed, 1620 insertions(+), 1614 deletions(-) rename Penumbra/Mods/Subclasses/{Mod.Files.SubMod.cs => SubMod.cs} (97%) diff --git a/.editorconfig b/.editorconfig index 0bbaa114..c645b573 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,21 +1,31 @@ - -[*.proto] -indent_style=tab -indent_size=tab -tab_width=4 - -[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] -indent_style=space -indent_size=4 -tab_width=4 - -[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] -indent_style=space -indent_size=2 -tab_width=2 +# Standard properties +charset = utf-8 +end_of_line = lf +insert_final_newline = true +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_top_level_statements = true:silent [*] - # Microsoft .NET properties csharp_indent_braces=false csharp_indent_switch_labels=true @@ -3567,30 +3577,6 @@ resharper_xaml_x_key_attribute_disallowed_highlighting=error resharper_xml_doc_comment_syntax_problem_highlighting=warning resharper_xunit_xunit_test_with_console_output_highlighting=warning -# Standard properties -end_of_line= crlf -csharp_indent_labels = one_less_than_current -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - [*.{cshtml,htm,html,proto,razor}] indent_style=tab indent_size=tab @@ -3601,6 +3587,21 @@ indent_style=space indent_size=4 tab_width=4 +[ "*.proto" ] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + [*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] indent_style=space indent_size= 4 @@ -3621,3 +3622,4 @@ dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion dotnet_style_namespace_match_folder = true:suggestion +insert_final_newline = true diff --git a/Penumbra.Api b/Penumbra.Api index 316f3da4..22846625 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 316f3da4a3ce246afe5b98c1568d73fcd7b6b22d +Subproject commit 22846625192884c6e9f5ec4429fb579875b519e9 diff --git a/Penumbra.GameData b/Penumbra.GameData index 0507b1f0..f004e069 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0507b1f093f5382e03242e5da991752361b70c6e +Subproject commit f004e069824a1588244e06080b32bab170f78077 diff --git a/Penumbra.String b/Penumbra.String index 0e0c1e1e..620a7edf 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0e0c1e1ee116c259abd00e1d5c3450ad40f92a98 +Subproject commit 620a7edf009b92288257ce7d64fffb8fba44d8b5 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1dead7e5..73a87fab 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -633,7 +633,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _modManager.AddMod(dir); if (_config.UseFileSystemCompression) - new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), + CompressionAlgorithm.Xpress8K); return PenumbraApiEc.Success; } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 35aa1217..efbfd7f9 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -25,7 +25,7 @@ public class TempModManager : IDisposable public TempModManager(CommunicatorService communicator) { - _communicator = communicator; + _communicator = communicator; _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.TempModManager); } @@ -43,7 +43,7 @@ public class TempModManager : IDisposable public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, HashSet manips, int priority) { - var mod = GetOrCreateMod(tag, collection, priority, out var created); + var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); mod.SetAll(dict, manips); ApplyModChange(mod, collection, created, false); @@ -56,7 +56,7 @@ public class TempModManager : IDisposable var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; if (list == null) return RedirectResult.NotRegistered; - + var removed = list.RemoveAll(m => { if (m.Name != tag || priority != null && m.Priority != priority.Value) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 6f3d59e9..80539d96 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.String.Classes; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; namespace Penumbra.Collections.Cache; @@ -270,14 +271,14 @@ public class CollectionCache : IDisposable foreach (var manip in subMod.Manipulations) AddManipulation(manip, parentMod); } - - /// Invoke only if not in a full recalculation. + + /// Invoke only if not in a full recalculation. private void InvokeResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod) { if (Calculating == -1) _manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod); - } + } // Add a specific file redirection, handling potential conflicts. // For different mods, higher mod priority takes precedence before option group priority, @@ -292,7 +293,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { - ModData.AddPath(mod, path); + ModData.AddPath(mod, path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 3a94bc89..5daecaa9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Dalamud.Game; using OtterGui.Classes; using Penumbra.Api; diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index a5232dbb..3937fa72 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -45,7 +45,7 @@ public readonly struct EqdpCache : IDisposable foreach (var file in _eqdpFiles.OfType()) { var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (SetId) m.SetId)); + file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (SetId)m.SetId)); } _eqdpManipulations.Clear(); diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 9d63479a..972ee5a5 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -9,20 +9,20 @@ namespace Penumbra.Collections.Cache; public struct EqpCache : IDisposable { - private ExpandedEqpFile? _eqpFile = null; - private readonly List< EqpManipulation > _eqpManipulations = new(); - - public EqpCache() - {} + private ExpandedEqpFile? _eqpFile = null; + private readonly List _eqpManipulations = new(); - public void SetFiles(MetaFileManager manager) - => manager.SetFile( _eqpFile, MetaIndex.Eqp ); + public EqpCache() + { } + + public void SetFiles(MetaFileManager manager) + => manager.SetFile(_eqpFile, MetaIndex.Eqp); public static void ResetFiles(MetaFileManager manager) - => manager.SetFile( null, MetaIndex.Eqp ); + => manager.SetFile(null, MetaIndex.Eqp); public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); + => manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); public void Reset() { @@ -31,25 +31,24 @@ public struct EqpCache : IDisposable _eqpFile.Reset(_eqpManipulations.Select(m => m.SetId)); _eqpManipulations.Clear(); - } - - public bool ApplyMod( MetaFileManager manager, EqpManipulation manip ) - { - _eqpManipulations.AddOrReplace( manip ); - _eqpFile ??= new ExpandedEqpFile(manager); - return manip.Apply( _eqpFile ); } - public bool RevertMod( MetaFileManager manager, EqpManipulation manip ) + public bool ApplyMod(MetaFileManager manager, EqpManipulation manip) { - var idx = _eqpManipulations.FindIndex( manip.Equals ); + _eqpManipulations.AddOrReplace(manip); + _eqpFile ??= new ExpandedEqpFile(manager); + return manip.Apply(_eqpFile); + } + + public bool RevertMod(MetaFileManager manager, EqpManipulation manip) + { + var idx = _eqpManipulations.FindIndex(manip.Equals); if (idx < 0) return false; - var def = ExpandedEqpFile.GetDefault( manager, manip.SetId ); - manip = new EqpManipulation( def, manip.Slot, manip.SetId ); - return manip.Apply( _eqpFile! ); - + var def = ExpandedEqpFile.GetDefault(manager, manip.SetId); + manip = new EqpManipulation(def, manip.Slot, manip.SetId); + return manip.Apply(_eqpFile!); } public void Dispose() @@ -58,4 +57,4 @@ public struct EqpCache : IDisposable _eqpFile = null; _eqpManipulations.Clear(); } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 3287eb2d..0a713867 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -9,42 +9,42 @@ namespace Penumbra.Collections.Cache; public struct GmpCache : IDisposable { - private ExpandedGmpFile? _gmpFile = null; - private readonly List< GmpManipulation > _gmpManipulations = new(); - + private ExpandedGmpFile? _gmpFile = null; + private readonly List _gmpManipulations = new(); + public GmpCache() - {} + { } public void SetFiles(MetaFileManager manager) - => manager.SetFile( _gmpFile, MetaIndex.Gmp ); + => manager.SetFile(_gmpFile, MetaIndex.Gmp); public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); + => manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); public void Reset() { - if( _gmpFile == null ) + if (_gmpFile == null) return; - _gmpFile.Reset( _gmpManipulations.Select( m => m.SetId ) ); + _gmpFile.Reset(_gmpManipulations.Select(m => m.SetId)); _gmpManipulations.Clear(); } - public bool ApplyMod( MetaFileManager manager, GmpManipulation manip ) + public bool ApplyMod(MetaFileManager manager, GmpManipulation manip) { - _gmpManipulations.AddOrReplace( manip ); + _gmpManipulations.AddOrReplace(manip); _gmpFile ??= new ExpandedGmpFile(manager); - return manip.Apply( _gmpFile ); + return manip.Apply(_gmpFile); } - public bool RevertMod( MetaFileManager manager, GmpManipulation manip ) + public bool RevertMod(MetaFileManager manager, GmpManipulation manip) { if (!_gmpManipulations.Remove(manip)) return false; - var def = ExpandedGmpFile.GetDefault( manager, manip.SetId ); - manip = new GmpManipulation( def, manip.SetId ); - return manip.Apply( _gmpFile! ); + var def = ExpandedGmpFile.GetDefault(manager, manip.SetId); + manip = new GmpManipulation(def, manip.SetId); + return manip.Apply(_gmpFile!); } public void Dispose() @@ -53,4 +53,4 @@ public struct GmpCache : IDisposable _gmpFile = null; _gmpManipulations.Clear(); } -} \ No newline at end of file +} diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index e226b409..05756e12 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -44,9 +44,9 @@ public readonly struct ImcCache : IDisposable if (idx < 0) { idx = _imcManipulations.Count; - _imcManipulations.Add((manip, null!)); - } - + _imcManipulations.Add((manip, null!)); + } + var path = manip.GamePath(); try { @@ -79,13 +79,13 @@ public readonly struct ImcCache : IDisposable public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) { if (!m.Validate()) - return false; - + return false; + var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); if (idx < 0) return false; - var (_, file) = _imcManipulations[idx]; + var (_, file) = _imcManipulations[idx]; _imcManipulations.RemoveAt(idx); if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file))) @@ -94,8 +94,8 @@ public readonly struct ImcCache : IDisposable collection._cache!.ForceFile(file.Path, FullPath.Empty); file.Dispose(); return true; - } - + } + var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _); var manip = m.Copy(def); if (!manip.Apply(file)) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index fa60993a..d2dd48f8 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -136,7 +136,7 @@ public class MetaCache : IDisposable, IEnumerable false, }; } - + /// Set a single file. public void SetFile(MetaIndex metaIndex) { @@ -162,7 +162,7 @@ public class MetaCache : IDisposable, IEnumerable Set the currently relevant IMC files for the collection cache. public void SetImcFiles(bool fromFullCompute) => _imcCache.SetFiles(_collection, fromFullCompute); @@ -171,7 +171,7 @@ public class MetaCache : IDisposable, IEnumerable _eqpCache.TemporarilySetFiles(_manager); public MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); + => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); public MetaList.MetaReverter TemporarilySetGmpFile() => _gmpCache.TemporarilySetFiles(_manager); @@ -180,7 +180,7 @@ public class MetaCache : IDisposable, IEnumerable _cmpCache.TemporarilySetFiles(_manager); public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) - => _estCache.TemporarilySetFiles(_manager, type); + => _estCache.TemporarilySetFiles(_manager, type); /// Try to obtain a manipulated IMC file. diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 5fcfd2f9..5872dea1 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -16,18 +16,18 @@ public static class ActiveCollectionMigration foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) { var oldName = type.ToString()[4..]; - var value = jObject[oldName]; + var value = jObject[oldName]; if (value == null) continue; jObject.Remove(oldName); - jObject.Add("Male" + oldName, value); + jObject.Add("Male" + oldName, value); jObject.Add("Female" + oldName, value); } using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); + using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; jObject.WriteTo(j); } @@ -41,13 +41,14 @@ public static class ActiveCollectionMigration // Load character collections. If a player name comes up multiple times, the last one is applied. var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary(characters.Count); + var dict = new Dictionary(characters.Count); foreach (var (player, collectionName) in characters) { if (!storage.ByName(collectionName, out var collection)) { Penumbra.Chat.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", NotificationType.Warning); dict.Add(player, ModCollection.Empty); } diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 58eb7517..bae95885 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -442,6 +442,7 @@ public class ActiveCollections : ISavable, IDisposable var m = ByType(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); if (m != null && m != yourself) return string.Empty; + var f = ByType(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); if (f != null && f != yourself) return string.Empty; @@ -450,26 +451,28 @@ public class ActiveCollections : ISavable, IDisposable } var racialString = racial ? " and Racial Assignments" : string.Empty; - var @base = ByType(CollectionType.Default); - var male = ByType(CollectionType.MalePlayerCharacter); - var female = ByType(CollectionType.FemalePlayerCharacter); + var @base = ByType(CollectionType.Default); + var male = ByType(CollectionType.MalePlayerCharacter); + var female = ByType(CollectionType.FemalePlayerCharacter); if (male == yourself && female == yourself) return $"Assignment is redundant due to overwriting Male Players and Female Players{racialString} with an identical collection.\nYou can remove it."; - + if (male == null) { if (female == null && @base == yourself) - return $"Assignment is redundant due to overwriting Base{racialString} with an identical collection.\nYou can remove it."; + return + $"Assignment is redundant due to overwriting Base{racialString} with an identical collection.\nYou can remove it."; if (female == yourself && @base == yourself) return $"Assignment is redundant due to overwriting Base and Female Players{racialString} with an identical collection.\nYou can remove it."; } else if (male == yourself && female == null && @base == yourself) { - return $"Assignment is redundant due to overwriting Base and Male Players{racialString} with an identical collection.\nYou can remove it."; + return + $"Assignment is redundant due to overwriting Base and Male Players{racialString} with an identical collection.\nYou can remove it."; } - + break; // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. case CollectionType.Individual: diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 3d0ef60e..f0b4d509 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -2,6 +2,7 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index c172aeff..eb230e9e 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -5,6 +5,7 @@ using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Collections.Manager; @@ -246,7 +247,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - switch (type) + switch (type) { case ModPathChangeType.Added: foreach (var collection in this) diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index 8e2d1aed..8c51fd90 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -427,13 +427,13 @@ public static class CollectionTypeExtensions public static string ToDescription(this CollectionType collectionType) => collectionType switch { - CollectionType.Default => "World, Music, Furniture, baseline for characters and monsters not specialized.", - CollectionType.Interface => "User Interface, Icons, Maps, Styles.", - CollectionType.Yourself => "Your characters, regardless of name, race or gender. Applies in the login screen.", - CollectionType.MalePlayerCharacter => "Baseline for male player characters.", - CollectionType.FemalePlayerCharacter => "Baseline for female player characters.", - CollectionType.MaleNonPlayerCharacter => "Baseline for humanoid male non-player characters.", + CollectionType.Default => "World, Music, Furniture, baseline for characters and monsters not specialized.", + CollectionType.Interface => "User Interface, Icons, Maps, Styles.", + CollectionType.Yourself => "Your characters, regardless of name, race or gender. Applies in the login screen.", + CollectionType.MalePlayerCharacter => "Baseline for male player characters.", + CollectionType.FemalePlayerCharacter => "Baseline for female player characters.", + CollectionType.MaleNonPlayerCharacter => "Baseline for humanoid male non-player characters.", CollectionType.FemaleNonPlayerCharacter => "Baseline for humanoid female non-player characters.", - _ => string.Empty, + _ => string.Empty, }; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index ac4acb8e..489e2f72 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -47,7 +47,8 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return true; // Handle generic NPC - var npcIdentifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, + var npcIdentifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, + ushort.MaxValue, identifier.Kind, identifier.DataId); if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) return true; @@ -56,7 +57,8 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (!_config.UseOwnerNameForCharacterCollection) return false; - identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, + identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); } @@ -142,7 +144,8 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (_individuals.TryGetValue(identifier, out collection)) return true; - identifier = _actorService.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, + identifier = _actorService.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, + identifier.Kind, identifier.DataId); if (identifier.IsValid && _individuals.TryGetValue(identifier, out collection)) return true; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index da403337..21a0c730 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -27,6 +27,7 @@ public partial class IndividualCollections { if (_actorService.Valid) return ReadJObjectInternal(obj, storage); + void Func() { if (ReadJObjectInternal(obj, storage)) @@ -35,9 +36,10 @@ public partial class IndividualCollections Loaded.Invoke(); _actorService.FinishedCreation -= Func; } + _actorService.FinishedCreation += Func; return false; - } + } private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage) { @@ -85,6 +87,7 @@ public partial class IndividualCollections NotificationType.Error); } } + return changes; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index e7547153..ed3c3d4b 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -132,7 +132,8 @@ public sealed partial class IndividualCollections _ => throw new NotImplementedException(), }; return table.Where(kvp => kvp.Value == name) - .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, identifier.Kind, + .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, + identifier.Kind, kvp.Key)).ToArray(); } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 56135182..025df9ef 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,5 +1,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.Util; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index ae68a370..d788d0bd 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -92,7 +92,7 @@ public partial class ModCollection // Used for short periods of changed files. public MetaList.MetaReverter TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) => _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory) - ?? utility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory)); + ?? utility.TemporarilyResetResource(CharacterUtilityData.EqdpIdx(genderRace, accessory)); public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetEqpFile() @@ -109,4 +109,4 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); -} \ No newline at end of file +} diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index cb4aecc6..a9f565c6 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,6 +1,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Collections; @@ -44,10 +45,10 @@ public partial class ModCollection /// This is used for material and imc changes. /// public int ChangeCounter { get; private set; } - - /// Increment the number of changes in the effective file list. + + /// Increment the number of changes in the effective file list. public int IncrementCounter() - => ++ChangeCounter; + => ++ChangeCounter; /// /// If a ModSetting is null, it can be inherited from other collections. @@ -57,7 +58,7 @@ public partial class ModCollection /// Settings for deleted mods will be kept via the mods identifier (directory name). public readonly IReadOnlyDictionary UnusedSettings; - + /// Inheritances stored before they can be applied. public IReadOnlyList? InheritanceByName; @@ -118,7 +119,7 @@ public partial class ModCollection /// Constructor for reading from files. public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index, Dictionary allSettings, IReadOnlyList inheritances) - { + { Debug.Assert(index > 0, "Collection read with non-positive index."); var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings) { @@ -130,7 +131,7 @@ public partial class ModCollection } /// Constructor for temporary collections. - public static ModCollection CreateTemporary(string name, int index, int changeCounter) + public static ModCollection CreateTemporary(string name, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List(), new List(), @@ -142,9 +143,10 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?) null, modCount).ToList(), new List(), + return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), + new List(), new Dictionary()); - } + } /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. internal bool AddMod(Mod mod) diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 05a8d9b0..4cc7706e 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -3,6 +3,7 @@ using Penumbra.Mods; using Penumbra.Services; using Newtonsoft.Json; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Util; namespace Penumbra.Collections; @@ -53,7 +54,7 @@ internal readonly struct ModCollectionSave : ISavable } list.AddRange(_modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); - list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); + list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); foreach (var (modDir, settings) in list) { @@ -64,7 +65,7 @@ internal readonly struct ModCollectionSave : ISavable j.WriteEndObject(); // Inherit by collection name. - j.WritePropertyName("Inheritance"); + j.WritePropertyName("Inheritance"); x.Serialize(j, _modCollection.InheritanceByName ?? _modCollection.DirectlyInheritsFrom.Select(c => c.Name)); j.WriteEndObject(); } diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 0c7bc967..0f3a1155 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -14,8 +14,8 @@ public readonly struct ResolveData public bool Valid => _modCollection != null; - public ResolveData() - : this(null!, nint.Zero) + public ResolveData() + : this(null!, nint.Zero) { } public ResolveData(ModCollection collection, nint gameObject) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index d1830617..920e9cef 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -207,7 +207,8 @@ public class CommandHandler : IDisposable private bool SetUiMinimumSize(string _) { if (_config.MinimumSize.X == Configuration.Constants.MinimumSizeX && _config.MinimumSize.Y == Configuration.Constants.MinimumSizeY) - return false; + return false; + _config.MinimumSize.X = Configuration.Constants.MinimumSizeX; _config.MinimumSize.Y = Configuration.Constants.MinimumSizeY; _config.Save(); diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index b815c48e..b713cc72 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -45,7 +45,6 @@ public sealed class CollectionChange : EventWrapper ModFileSystemSelector = 0, - } public CollectionChange() diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index cca546e0..9ec60aa3 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -21,7 +21,7 @@ public sealed class ModDataChanged : EventWrapper ModCacheManager = 0, - /// + /// ModFileSystem = 0, } diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs index 8f5d31d5..04c13e95 100644 --- a/Penumbra/Communication/ModDiscoveryFinished.cs +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Mods.Manager; namespace Penumbra.Communication; @@ -19,7 +20,7 @@ public sealed class ModDiscoveryFinished : EventWrapper ModCacheManager = 0, - /// + /// ModFileSystem = 0, } diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs index a2ff8633..cf45528d 100644 --- a/Penumbra/Communication/ModDiscoveryStarted.cs +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -16,6 +16,7 @@ public sealed class ModDiscoveryStarted : EventWrapper ModFileSystemSelector = 200, } + public ModDiscoveryStarted() : base(nameof(ModDiscoveryStarted)) { } diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 99ec9109..83c3b5a5 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -30,7 +30,7 @@ public sealed class ModPathChanged : EventWrapper ModExportManager = 0, - /// + /// ModFileSystem = 0, /// @@ -48,6 +48,7 @@ public sealed class ModPathChanged : EventWrapper CollectionCacheManagerRemoval = 100, } + public ModPathChanged() : base(nameof(ModPathChanged)) { } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 72f8bb95..63d58a16 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -6,13 +6,14 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; diff --git a/Penumbra/GlobalUsings.cs b/Penumbra/GlobalUsings.cs index 0f6538bb..b1e6218c 100644 --- a/Penumbra/GlobalUsings.cs +++ b/Penumbra/GlobalUsings.cs @@ -2,13 +2,15 @@ global using System; global using System.Collections; +global using System.Collections.Concurrent; global using System.Collections.Generic; global using System.Diagnostics; global using System.IO; global using System.Linq; global using System.Numerics; +global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; global using System.Security.Cryptography; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; diff --git a/Penumbra/Import/Structs/ImporterState.cs b/Penumbra/Import/Structs/ImporterState.cs index 9ab2ab9a..8c0ddb4e 100644 --- a/Penumbra/Import/Structs/ImporterState.cs +++ b/Penumbra/Import/Structs/ImporterState.cs @@ -7,4 +7,4 @@ public enum ImporterState ExtractingModFiles, DeduplicatingFiles, Done, -} \ No newline at end of file +} diff --git a/Penumbra/Import/Structs/StreamDisposer.cs b/Penumbra/Import/Structs/StreamDisposer.cs index 7a755c40..84719331 100644 --- a/Penumbra/Import/Structs/StreamDisposer.cs +++ b/Penumbra/Import/Structs/StreamDisposer.cs @@ -20,4 +20,4 @@ public class StreamDisposer : PenumbraSqPackStream, IDisposable File.Delete(filePath); } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index a41b9f24..3b67ac50 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -15,19 +15,19 @@ namespace Penumbra.Import; public partial class TexToolsImporter { - /// + /// /// Extract regular compressed archives that are folders containing penumbra-formatted mods. /// The mod has to either contain a meta.json at top level, or one folder deep. /// If the meta.json is one folder deep, all other files have to be in the same folder. /// The extracted folder gets its name either from that one top-level folder or from the mod name. - /// All data is extracted without manipulation of the files or metadata. - /// - private DirectoryInfo HandleRegularArchive( FileInfo modPackFile ) + /// All data is extracted without manipulation of the files or metadata. + /// + private DirectoryInfo HandleRegularArchive(FileInfo modPackFile) { using var zfs = modPackFile.OpenRead(); - using var archive = ArchiveFactory.Open( zfs ); + using var archive = ArchiveFactory.Open(zfs); - var baseName = FindArchiveModMeta( archive, out var leadDir ); + var baseName = FindArchiveModMeta(archive, out var leadDir); var name = string.Empty; _currentOptionIdx = 0; _currentNumOptions = 1; @@ -42,9 +42,9 @@ public partial class TexToolsImporter SevenZipArchive s => s.Entries.Count, _ => archive.Entries.Count(), }; - Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." ); + Penumbra.Log.Information($" -> Importing {archive.Type} Archive."); - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName()); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -55,40 +55,38 @@ public partial class TexToolsImporter _currentFileIdx = 0; var reader = archive.ExtractAllEntries(); - while( reader.MoveToNextEntry() ) + while (reader.MoveToNextEntry()) { _token.ThrowIfCancellationRequested(); - if( reader.Entry.IsDirectory ) + if (reader.Entry.IsDirectory) { --_currentNumFiles; continue; } - Penumbra.Log.Information( $" -> Extracting {reader.Entry.Key}" ); + Penumbra.Log.Information($" -> Extracting {reader.Entry.Key}"); // Check that the mod has a valid name in the meta.json file. - if( Path.GetFileName( reader.Entry.Key ) == "meta.json" ) + if (Path.GetFileName(reader.Entry.Key) == "meta.json") { using var s = new MemoryStream(); using var e = reader.OpenEntryStream(); - e.CopyTo( s ); - s.Seek( 0, SeekOrigin.Begin ); - using var t = new StreamReader( s ); - using var j = new JsonTextReader( t ); - var obj = JObject.Load( j ); - name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; - if( name.Length == 0 ) - { - throw new Exception( "Invalid mod archive: mod meta has no name." ); - } + e.CopyTo(s); + s.Seek(0, SeekOrigin.Begin); + using var t = new StreamReader(s); + using var j = new JsonTextReader(t); + var obj = JObject.Load(j); + name = obj[nameof(Mod.Name)]?.Value()?.RemoveInvalidPathSymbols() ?? string.Empty; + if (name.Length == 0) + throw new Exception("Invalid mod archive: mod meta has no name."); - using var f = File.OpenWrite( Path.Combine( _currentModDirectory.FullName, reader.Entry.Key ) ); - s.Seek( 0, SeekOrigin.Begin ); - s.WriteTo( f ); + using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key)); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); } else { - reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); + reader.WriteEntryToDirectory(_currentModDirectory.FullName, options); } ++_currentFileIdx; @@ -97,60 +95,59 @@ public partial class TexToolsImporter _token.ThrowIfCancellationRequested(); var oldName = _currentModDirectory.FullName; // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. - if( leadDir ) + if (leadDir) { - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, baseName, false ); - Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); - Directory.Delete( oldName ); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, false); + Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); + Directory.Delete(oldName); } else { - _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, name, false ); - Directory.Move( oldName, _currentModDirectory.FullName ); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, false); + Directory.Move(oldName, _currentModDirectory.FullName); } _currentModDirectory.Refresh(); - _modManager.Creator.SplitMultiGroups( _currentModDirectory ); + _modManager.Creator.SplitMultiGroups(_currentModDirectory); return _currentModDirectory; } // Search the archive for the meta.json file which needs to exist. - private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) + private static string FindArchiveModMeta(IArchive archive, out bool leadDir) { - var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && Path.GetFileName( e.Key ) == "meta.json" ); + var entry = archive.Entries.FirstOrDefault(e => !e.IsDirectory && Path.GetFileName(e.Key) == "meta.json"); // None found. - if( entry == null ) - { - throw new Exception( "Invalid mod archive: No meta.json contained." ); - } + if (entry == null) + throw new Exception("Invalid mod archive: No meta.json contained."); var ret = string.Empty; leadDir = false; // If the file is not at top-level. - if( entry.Key != "meta.json" ) + if (entry.Key != "meta.json") { leadDir = true; - var directory = Path.GetDirectoryName( entry.Key ); + var directory = Path.GetDirectoryName(entry.Key); // Should not happen. - if( directory.IsNullOrEmpty() ) - { - throw new Exception( "Invalid mod archive: Unknown error fetching meta.json." ); - } + if (directory.IsNullOrEmpty()) + throw new Exception("Invalid mod archive: Unknown error fetching meta.json."); ret = directory; // Check that all other files are also contained in the top-level directory. - if( ret.IndexOfAny( new[] { '/', '\\' } ) >= 0 - || !archive.Entries.All( e => e.Key.StartsWith( ret ) && ( e.Key.Length == ret.Length || e.Key[ ret.Length ] is '/' or '\\' ) ) ) - { + if (ret.IndexOfAny(new[] + { + '/', + '\\', + }) + >= 0 + || !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\'))) throw new Exception( - "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too." ); - } + "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too."); } return ret; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 0c3e084d..e150d10d 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -1,7 +1,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.Import.Structs; +using Penumbra.Import.Structs; using Penumbra.UI.Classes; namespace Penumbra.Import; @@ -20,89 +20,79 @@ public partial class TexToolsImporter private string _currentOptionName = string.Empty; private string _currentFileName = string.Empty; - public void DrawProgressInfo( Vector2 size ) + public void DrawProgressInfo(Vector2 size) { - if( _modPackCount == 0 ) + if (_modPackCount == 0) { - ImGuiUtil.Center( "Nothing to extract." ); + ImGuiUtil.Center("Nothing to extract."); } - else if( _modPackCount == _currentModPackIdx ) + else if (_modPackCount == _currentModPackIdx) { DrawEndState(); } else { ImGui.NewLine(); - var percentage = _modPackCount / ( float )_currentModPackIdx; - ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" ); + var percentage = _modPackCount / (float)_currentModPackIdx; + ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); - if( State == ImporterState.DeduplicatingFiles ) - { - ImGui.TextUnformatted( $"Deduplicating {_currentModName}..." ); - } + if (State == ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); else - { - ImGui.TextUnformatted( $"Extracting {_currentModName}..." ); - } + ImGui.TextUnformatted($"Extracting {_currentModName}..."); - if( _currentNumOptions > 1 ) + if (_currentNumOptions > 1) { ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions; - ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" ); + percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; + ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); ImGui.NewLine(); - if( State != ImporterState.DeduplicatingFiles ) - { + if (State != ImporterState.DeduplicatingFiles) ImGui.TextUnformatted( - $"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." ); - } + $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles; - ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" ); + percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; + ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); ImGui.NewLine(); - if( State != ImporterState.DeduplicatingFiles ) - { - ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." ); - } + if (State != ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); } } private void DrawEndState() { - var success = ExtractedMods.Count( t => t.Error == null ); + var success = ExtractedMods.Count(t => t.Error == null); - ImGui.TextUnformatted( $"Successfully extracted {success} / {ExtractedMods.Count} files." ); + ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.NewLine(); - using var table = ImRaii.Table( "##files", 2 ); - if( !table ) - { + using var table = ImRaii.Table("##files", 2); + if (!table) return; - } - foreach( var (file, dir, ex) in ExtractedMods ) + foreach (var (file, dir, ex) in ExtractedMods) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.Name ); + ImGui.TextUnformatted(file.Name); ImGui.TableNextColumn(); - if( ex == null ) + if (ex == null) { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); - ImGui.TextUnformatted( dir?.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ?? "Unknown Directory" ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); + ImGui.TextUnformatted(dir?.FullName[(_baseDirectory.FullName.Length + 1)..] ?? "Unknown Directory"); } else { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); - ImGui.TextUnformatted( ex.Message ); - ImGuiUtil.HoverTooltip( ex.ToString() ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ConflictingMod.Value()); + ImGui.TextUnformatted(ex.Message); + ImGuiUtil.HoverTooltip(ex.ToString()); } } } - public bool DrawCancelButton( Vector2 size ) - => ImGuiUtil.DrawDisabledButton( "Cancel", size, string.Empty, _token.IsCancellationRequested ); -} \ No newline at end of file + public bool DrawCancelButton(Vector2 size) + => ImGuiUtil.DrawDisabledButton("Cancel", size, string.Empty, _token.IsCancellationRequested); +} diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index eea1d811..64eff8ba 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -116,7 +116,7 @@ public partial class TexToolsMeta var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { - if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant) i))) + if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant)i))) { var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value); diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 0fd94fb4..51faa175 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -8,72 +8,70 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Parse a single rgsp file. - public static TexToolsMeta FromRgspFile( MetaFileManager manager, string filePath, byte[] data, bool keepDefault ) + public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault) { - if( data.Length != 45 && data.Length != 42 ) + if (data.Length != 45 && data.Length != 42) { - Penumbra.Log.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); + Penumbra.Log.Error("Error while parsing .rgsp file:\n\tInvalid number of bytes."); return Invalid; } - using var s = new MemoryStream( data ); - using var br = new BinaryReader( s ); + using var s = new MemoryStream(data); + using var br = new BinaryReader(s); // The first value is a flag that signifies version. // If it is byte.max, the following two bytes are the version, // otherwise it is version 1 and signifies the sub race instead. var flag = br.ReadByte(); - var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); + var version = flag != 255 ? (uint)1 : br.ReadUInt16(); - var ret = new TexToolsMeta( manager, filePath, version ); + var ret = new TexToolsMeta(manager, filePath, version); // SubRace is offset by one due to Unknown. - var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); - if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) + var subRace = (SubRace)(version == 1 ? flag + 1 : br.ReadByte() + 1); + if (!Enum.IsDefined(typeof(SubRace), subRace) || subRace == SubRace.Unknown) { - Penumbra.Log.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); + Penumbra.Log.Error($"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace."); return Invalid; } // Next byte is Gender. 1 is Female, 0 is Male. var gender = br.ReadByte(); - if( gender != 1 && gender != 0 ) + if (gender != 1 && gender != 0) { - Penumbra.Log.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); + Penumbra.Log.Error($"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female."); return Invalid; } // Add the given values to the manipulations if they are not default. - void Add( RspAttribute attribute, float value ) + void Add(RspAttribute attribute, float value) { - var def = CmpFile.GetDefault( manager, subRace, attribute ); - if( keepDefault || value != def ) - { - ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) ); - } + var def = CmpFile.GetDefault(manager, subRace, attribute); + if (keepDefault || value != def) + ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, value)); } - if( gender == 1 ) + if (gender == 1) { - Add( RspAttribute.FemaleMinSize, br.ReadSingle() ); - Add( RspAttribute.FemaleMaxSize, br.ReadSingle() ); - Add( RspAttribute.FemaleMinTail, br.ReadSingle() ); - Add( RspAttribute.FemaleMaxTail, br.ReadSingle() ); + Add(RspAttribute.FemaleMinSize, br.ReadSingle()); + Add(RspAttribute.FemaleMaxSize, br.ReadSingle()); + Add(RspAttribute.FemaleMinTail, br.ReadSingle()); + Add(RspAttribute.FemaleMaxTail, br.ReadSingle()); - Add( RspAttribute.BustMinX, br.ReadSingle() ); - Add( RspAttribute.BustMinY, br.ReadSingle() ); - Add( RspAttribute.BustMinZ, br.ReadSingle() ); - Add( RspAttribute.BustMaxX, br.ReadSingle() ); - Add( RspAttribute.BustMaxY, br.ReadSingle() ); - Add( RspAttribute.BustMaxZ, br.ReadSingle() ); + Add(RspAttribute.BustMinX, br.ReadSingle()); + Add(RspAttribute.BustMinY, br.ReadSingle()); + Add(RspAttribute.BustMinZ, br.ReadSingle()); + Add(RspAttribute.BustMaxX, br.ReadSingle()); + Add(RspAttribute.BustMaxY, br.ReadSingle()); + Add(RspAttribute.BustMaxZ, br.ReadSingle()); } else { - Add( RspAttribute.MaleMinSize, br.ReadSingle() ); - Add( RspAttribute.MaleMaxSize, br.ReadSingle() ); - Add( RspAttribute.MaleMinTail, br.ReadSingle() ); - Add( RspAttribute.MaleMaxTail, br.ReadSingle() ); + Add(RspAttribute.MaleMinSize, br.ReadSingle()); + Add(RspAttribute.MaleMaxSize, br.ReadSingle()); + Add(RspAttribute.MaleMinTail, br.ReadSingle()); + Add(RspAttribute.MaleMaxTail, br.ReadSingle()); } return ret; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index b44f99a8..a188975c 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -17,70 +17,68 @@ namespace Penumbra.Import; /// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. /// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. public partial class TexToolsMeta -{ +{ /// An empty TexToolsMeta. public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. - public readonly uint Version; - public readonly string FilePath; - public readonly List< MetaManipulation > MetaManipulations = new(); - private readonly bool _keepDefault = false; - - private readonly MetaFileManager _metaFileManager; + public readonly uint Version; + public readonly string FilePath; + public readonly List MetaManipulations = new(); + private readonly bool _keepDefault = false; - public TexToolsMeta( MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault ) - { + private readonly MetaFileManager _metaFileManager; + + public TexToolsMeta(MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault) + { _metaFileManager = metaFileManager; - _keepDefault = keepDefault; + _keepDefault = keepDefault; try { - using var reader = new BinaryReader( new MemoryStream( data ) ); + using var reader = new BinaryReader(new MemoryStream(data)); Version = reader.ReadUInt32(); - FilePath = ReadNullTerminated( reader ); - var metaInfo = new MetaFileInfo( parser, FilePath ); + FilePath = ReadNullTerminated(reader); + var metaInfo = new MetaFileInfo(parser, FilePath); var numHeaders = reader.ReadUInt32(); var headerSize = reader.ReadUInt32(); var headerStart = reader.ReadUInt32(); - reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); + reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); - List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); - for( var i = 0; i < numHeaders; ++i ) + List<(MetaManipulation.Type type, uint offset, int size)> entries = new(); + for (var i = 0; i < numHeaders; ++i) { var currentOffset = reader.BaseStream.Position; - var type = ( MetaManipulation.Type )reader.ReadUInt32(); + var type = (MetaManipulation.Type)reader.ReadUInt32(); var offset = reader.ReadUInt32(); var size = reader.ReadInt32(); - entries.Add( ( type, offset, size ) ); - reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); + entries.Add((type, offset, size)); + reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin); } - byte[]? ReadEntry( MetaManipulation.Type type ) + byte[]? ReadEntry(MetaManipulation.Type type) { - var idx = entries.FindIndex( t => t.type == type ); - if( idx < 0 ) - { + var idx = entries.FindIndex(t => t.type == type); + if (idx < 0) return null; - } - reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); - return reader.ReadBytes( entries[ idx ].size ); + reader.BaseStream.Seek(entries[idx].offset, SeekOrigin.Begin); + return reader.ReadBytes(entries[idx].size); } - DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); - DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); - DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); - DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) ); - DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) ); + DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp)); + DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp)); + DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp)); + DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est)); + DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc)); } - catch( Exception e ) + catch (Exception e) { FilePath = ""; - Penumbra.Log.Error( $"Error while parsing .meta file:\n{e}" ); + Penumbra.Log.Error($"Error while parsing .meta file:\n{e}"); } } - private TexToolsMeta( MetaFileManager metaFileManager, string filePath, uint version ) + private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version) { _metaFileManager = metaFileManager; FilePath = filePath; @@ -88,14 +86,12 @@ public partial class TexToolsMeta } // Read a null terminated string from a binary reader. - private static string ReadNullTerminated( BinaryReader reader ) + private static string ReadNullTerminated(BinaryReader reader) { var builder = new StringBuilder(); - for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) - { - builder.Append( c ); - } + for (var c = reader.ReadChar(); c != 0; c = reader.ReadChar()) + builder.Append(c); return builder.ToString(); } -} \ No newline at end of file +} diff --git a/Penumbra/Import/Textures/RgbaPixelData.cs b/Penumbra/Import/Textures/RgbaPixelData.cs index 32540f16..7c28b72b 100644 --- a/Penumbra/Import/Textures/RgbaPixelData.cs +++ b/Penumbra/Import/Textures/RgbaPixelData.cs @@ -13,8 +13,7 @@ public readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelD public RgbaPixelData((int Width, int Height) size, byte[] pixelData) : this(size.Width, size.Height, pixelData) - { - } + { } public Image ToImage() => Image.LoadPixelData(PixelData, Width, Height); diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 7f324601..15d45be6 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,8 +79,8 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write((byte) header.MipLevels); - w.Write((byte) 0); // TODO Lumina Update + w.Write((byte)header.MipLevels); + w.Write((byte)0); // TODO Lumina Update unsafe { w.Write(header.LodOffset[0]); diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index b94fdbf8..a5692c23 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -103,7 +103,7 @@ public static class TextureDrawer public sealed class PathSelectCombo : FilterComboCache<(string, bool)> { - private int _skipPrefix = 0; + private int _skipPrefix = 0; public PathSelectCombo(TextureManager textures, ModEditor editor) : base(() => CreateFiles(textures, editor)) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 90dfa3d0..31c3275e 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Dalamud.Interface; using Dalamud.Plugin.Services; using ImGuiScene; diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index f83b1531..e89f0d10 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -80,7 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase textureSize[0] = TextureWidth; textureSize[1] = TextureHeight; - using var texture = new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false); + using var texture = + new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false); if (texture.IsInvalid) return; diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index cc679bfd..51612819 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -17,7 +17,7 @@ namespace Penumbra.Interop.PathResolving; public unsafe class AnimationHookService : IDisposable { private readonly PerformanceTracker _performance; - private readonly IObjectTable _objects; + private readonly IObjectTable _objects; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly CollectionResolver _resolver; @@ -34,7 +34,7 @@ public unsafe class AnimationHookService : IDisposable _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _resolver = resolver; - _conditions = conditions; + _conditions = conditions; SignatureHelper.Initialise(this); @@ -122,7 +122,7 @@ public unsafe class AnimationHookService : IDisposable var last = _characterSoundData.Value; _characterSoundData.Value = _collectionResolver.IdentifyCollection((GameObject*)character, true); var ret = _loadCharacterSoundHook.Original(character, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - _characterSoundData.Value = last; + _characterSoundData.Value = last; return ret; } @@ -140,15 +140,15 @@ public unsafe class AnimationHookService : IDisposable using var performance = _performance.Measure(PerformanceType.TimelineResources); // Do not check timeline loading in cutscenes. if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) - return _loadTimelineResourcesHook.Original(timeline); + return _loadTimelineResourcesHook.Original(timeline); - var last = _animationLoadData.Value; + var last = _animationLoadData.Value; _animationLoadData.Value = GetDataFromTimeline(timeline); var ret = _loadTimelineResourcesHook.Original(timeline); _animationLoadData.Value = last; return ret; } - + /// /// Probably used when the base idle animation gets loaded. /// Make it aware of the correct collection to load the correct pap files. @@ -297,12 +297,12 @@ public unsafe class AnimationHookService : IDisposable try { if (timeline != IntPtr.Zero) - { + { var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; var idx = getGameObjectIdx(timeline); if (idx >= 0 && idx < _objects.Length) { - var obj = (GameObject*)_objects.GetObjectAddress(idx); + var obj = (GameObject*)_objects.GetObjectAddress(idx); return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid; } } @@ -378,17 +378,17 @@ public unsafe class AnimationHookService : IDisposable if (a6 == nint.Zero) return _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - var last = _animationLoadData.Value; - // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. + var last = _animationLoadData.Value; + // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. var gameObject = (*(delegate* unmanaged**)a6)[1](a6); if (gameObject != null) { _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); } else - { - // for VfxListenner we can obtain the associated draw object as its first member, - // if the object has different type, drawObject will contain other values or garbage, + { + // for VfxListenner we can obtain the associated draw object as its first member, + // if the object has different type, drawObject will contain other values or garbage, // but only be used in a dictionary pointer lookup, so this does not hurt. var drawObject = ((DrawObject**)a6)[1]; if (drawObject != null) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 3f73643b..ecd4eb2e 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -20,7 +20,7 @@ public unsafe class CollectionResolver private readonly HumanModelList _humanModels; private readonly IClientState _clientState; - private readonly IGameGui _gameGui; + private readonly IGameGui _gameGui; private readonly ActorService _actors; private readonly CutsceneService _cutscenes; diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 512d370a..be1484db 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.PathResolving; public class DrawObjectState : IDisposable, IReadOnlyDictionary { - private readonly IObjectTable _objects; + private readonly IObjectTable _objects; private readonly GameEventManager _gameEvents; private readonly Dictionary _drawObjectToGameObject = new(); @@ -71,8 +71,8 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionaryDrawObject, gameObject, false, false); + _lastGameObject.Value!.Dequeue(); + IterateDrawObjectTree((Object*)((GameObject*)gameObject)->DrawObject, gameObject, false, false); } private void OnCharacterBaseDestructor(nint characterBase) diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 546ffd92..0b456b3c 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -25,8 +25,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A _events = events; _communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache); - _clientState.TerritoryChanged += TerritoryClear; - _events.CharacterDestructor += OnCharacterDestruct; + _clientState.TerritoryChanged += TerritoryClear; + _events.CharacterDestructor += OnCharacterDestruct; } public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) @@ -61,8 +61,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A public void Dispose() { _communicator.CollectionChange.Unsubscribe(CollectionChangeClear); - _clientState.TerritoryChanged -= TerritoryClear; - _events.CharacterDestructor -= OnCharacterDestruct; + _clientState.TerritoryChanged -= TerritoryClear; + _events.CharacterDestructor -= OnCharacterDestruct; } public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 2176ebba..4494dc77 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index b1bc806d..00b06963 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 6ca0efb4..9dc89ab2 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -81,6 +81,6 @@ public unsafe class FileReadService : IDisposable /// private nint GetResourceManager() => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero - ? (nint) _resourceManager.ResourceManager + ? (nint)_resourceManager.ResourceManager : _lastFileThreadResourceManager.Value; } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index cae89c09..c3327a9e 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -37,7 +37,8 @@ public class ResourceNode Children = new List(); } - public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, + FullPath fullPath, ulong length, bool @internal) { Name = uiData.Name; @@ -69,7 +70,7 @@ public class ResourceNode } public ResourceNode WithUIData(string? name, ChangedItemIcon icon) - => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new(name, icon), this); + => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new UIData(name, icon), this); public ResourceNode WithUIData(UIData uiData) => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); @@ -77,6 +78,6 @@ public class ResourceNode public readonly record struct UIData(string? Name, ChangedItemIcon Icon) { public readonly UIData PrependName(string prefix) - => Name == null ? this : new(prefix + Name, Icon); + => Name == null ? this : new UIData(prefix + Name, Icon); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index d2276291..a8ad9d4f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -16,7 +16,7 @@ public class ResourceTree public readonly nint DrawObjectAddress; public readonly bool PlayerRelated; public readonly string CollectionName; - public readonly List Nodes; + public readonly List Nodes; public readonly HashSet FlatNodes; public int ModelId; @@ -26,11 +26,11 @@ public class ResourceTree public ResourceTree(string name, nint gameObjectAddress, nint drawObjectAddress, bool playerRelated, string collectionName) { Name = name; - GameObjectAddress = gameObjectAddress; + GameObjectAddress = gameObjectAddress; DrawObjectAddress = drawObjectAddress; PlayerRelated = playerRelated; CollectionName = collectionName; - Nodes = new List(); + Nodes = new List(); FlatNodes = new HashSet(); } @@ -42,7 +42,7 @@ public class ResourceTree // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; - RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace) ((Human*)model)->RaceSexId : GenderRace.Unknown; + RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace)((Human*)model)->RaceSexId : GenderRace.Unknown; for (var i = 0; i < model->SlotCount; ++i) { @@ -60,8 +60,8 @@ public class ResourceTree var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); - } - + } + AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) @@ -100,8 +100,8 @@ public class ResourceTree subObjectNodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); - } - + } + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; @@ -119,19 +119,21 @@ public class ResourceTree var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithUiData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); - } - - private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") - { - if (skeleton == null) - return; - - for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) - { - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); - if (sklbNode != null) - nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); - } + Nodes.Add(globalContext.WithUiData + ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) + : legacyDecalNode); + } + + private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + { + if (skeleton == null) + return; + + for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) + { + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (sklbNode != null) + nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + } } } diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs index 8dd1fb4a..1f788a39 100644 --- a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -4,21 +4,25 @@ namespace Penumbra.Interop.SafeHandles; public unsafe class SafeResourceHandle : SafeHandle { - public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; + public ResourceHandle* ResourceHandle + => (ResourceHandle*)handle; - public override bool IsInvalid => handle == 0; + public override bool IsInvalid + => handle == 0; - public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) + : base(0, ownsHandle) { if (incRef && !ownsHandle) throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); + if (incRef && handle != null) handle->IncRef(); SetHandle((nint)handle); } public static SafeResourceHandle CreateInvalid() - => new(null, incRef: false); + => new(null, false); protected override bool ReleaseHandle() { diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs index 88c97c54..dee28797 100644 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -5,14 +5,18 @@ namespace Penumbra.Interop.SafeHandles; public unsafe class SafeTextureHandle : SafeHandle { - public Texture* Texture => (Texture*)handle; + public Texture* Texture + => (Texture*)handle; - public override bool IsInvalid => handle == 0; + public override bool IsInvalid + => handle == 0; - public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) + : base(0, ownsHandle) { if (incRef && !ownsHandle) throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); + if (incRef && handle != null) TextureUtility.IncRef(handle); SetHandle((nint)handle); @@ -27,16 +31,17 @@ public unsafe class SafeTextureHandle : SafeHandle } public static SafeTextureHandle CreateInvalid() - => new(null, incRef: false); + => new(null, false); protected override bool ReleaseHandle() { nint handle; lock (this) { - handle = this.handle; + handle = this.handle; this.handle = 0; } + if (handle != 0) TextureUtility.DecRef((Texture*)handle); diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 21fa87a1..18c88766 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -14,7 +14,7 @@ public sealed unsafe class DecalReverter : IDisposable public static readonly Utf8GamePath TransparentPath = Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; - private readonly CharacterUtility _utility; + private readonly CharacterUtility _utility; private readonly Structs.TextureResourceHandle* _decal; private readonly Structs.TextureResourceHandle* _transparent; @@ -22,10 +22,10 @@ public sealed unsafe class DecalReverter : IDisposable { _utility = utility; var ptr = _utility.Address; - _decal = null; + _decal = null; _transparent = null; if (!config.EnableMods) - return; + return; if (doDecal) { diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index ca333ed4..2e8a23f0 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -152,7 +152,7 @@ public unsafe class GameEventManager : IDisposable { try { - ((CreatingCharacterBaseEvent)subscriber).Invoke((nint) (&a), b, c); + ((CreatingCharacterBaseEvent)subscriber).Invoke((nint)(&a), b, c); } catch (Exception ex) { @@ -265,11 +265,13 @@ public unsafe class GameEventManager : IDisposable private readonly Hook? _testHook = null; private delegate void TestDelegate(nint a1, int a2); + private void TestDetour(nint a1, int a2) { Penumbra.Log.Information($"Test: {a1:X} {a2}"); _testHook!.Original(a1, a2); } + private void EnableDebugHook() => _testHook?.Enable(); diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 0b9c6c38..65d5b0c0 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -100,10 +100,10 @@ public unsafe partial class RedrawService public sealed unsafe partial class RedrawService : IDisposable { - private readonly Framework _framework; + private readonly Framework _framework; private readonly IObjectTable _objects; private readonly ITargetManager _targets; - private readonly Condition _conditions; + private readonly Condition _conditions; private readonly List _queue = new(100); private readonly List _afterGPoseQueue = new(GPoseSlots); @@ -207,7 +207,7 @@ public sealed unsafe partial class RedrawService : IDisposable return; _targets.Target = actor; - _target = -1; + _target = -1; } private void HandleRedraw() diff --git a/Penumbra/Interop/Services/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs index cd20b889..ff7f95a5 100644 --- a/Penumbra/Interop/Services/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -1,6 +1,6 @@ using Dalamud.Utility.Signatures; using Penumbra.GameData; - + namespace Penumbra.Interop.Services; public unsafe class ResidentResourceManager @@ -36,4 +36,4 @@ public unsafe class ResidentResourceManager LoadPlayerResources.Invoke(Address); } } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 87d9566c..08857292 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -2,23 +2,23 @@ using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; public const int IndexSkinShpk = 76; - public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() - .Zip( Enum.GetValues< MetaIndex >() ) - .Where( n => n.First.StartsWith( "Eqdp" ) ) - .Select( n => n.Second ).ToArray(); + public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() + .Zip(Enum.GetValues()) + .Where(n => n.First.StartsWith("Eqdp")) + .Select(n => n.Second).ToArray(); public const int TotalNumResources = 87; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. - public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) - => +( int )raceCode switch + public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) + => +(int)raceCode switch { 0101 => accessory ? MetaIndex.Eqdp0101Acc : MetaIndex.Eqdp0101, 0201 => accessory ? MetaIndex.Eqdp0201Acc : MetaIndex.Eqdp0201, @@ -48,53 +48,53 @@ public unsafe struct CharacterUtilityData 1404 => accessory ? MetaIndex.Eqdp1404Acc : MetaIndex.Eqdp1404, 9104 => accessory ? MetaIndex.Eqdp9104Acc : MetaIndex.Eqdp9104, 9204 => accessory ? MetaIndex.Eqdp9204Acc : MetaIndex.Eqdp9204, - _ => ( MetaIndex )( -1 ), + _ => (MetaIndex)(-1), }; - [FieldOffset( 0 )] + [FieldOffset(0)] public void* VTable; - [FieldOffset( 8 )] + [FieldOffset(8)] public fixed ulong Resources[TotalNumResources]; - [FieldOffset( 8 + ( int )MetaIndex.Eqp * 8 )] + [FieldOffset(8 + (int)MetaIndex.Eqp * 8)] public ResourceHandle* EqpResource; - [FieldOffset( 8 + ( int )MetaIndex.Gmp * 8 )] + [FieldOffset(8 + (int)MetaIndex.Gmp * 8)] public ResourceHandle* GmpResource; - public ResourceHandle* Resource( int idx ) - => ( ResourceHandle* )Resources[ idx ]; + public ResourceHandle* Resource(int idx) + => (ResourceHandle*)Resources[idx]; - public ResourceHandle* Resource( MetaIndex idx ) - => Resource( ( int )idx ); + public ResourceHandle* Resource(MetaIndex idx) + => Resource((int)idx); - public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory ) - => Resource( ( int )EqdpIdx( raceCode, accessory ) ); + public ResourceHandle* EqdpResource(GenderRace raceCode, bool accessory) + => Resource((int)EqdpIdx(raceCode, accessory)); - [FieldOffset( 8 + ( int )MetaIndex.HumanCmp * 8 )] + [FieldOffset(8 + (int)MetaIndex.HumanCmp * 8)] public ResourceHandle* HumanCmpResource; - [FieldOffset( 8 + ( int )MetaIndex.FaceEst * 8 )] + [FieldOffset(8 + (int)MetaIndex.FaceEst * 8)] public ResourceHandle* FaceEstResource; - [FieldOffset( 8 + ( int )MetaIndex.HairEst * 8 )] + [FieldOffset(8 + (int)MetaIndex.HairEst * 8)] public ResourceHandle* HairEstResource; - [FieldOffset( 8 + ( int )MetaIndex.BodyEst * 8 )] + [FieldOffset(8 + (int)MetaIndex.BodyEst * 8)] public ResourceHandle* BodyEstResource; - [FieldOffset( 8 + ( int )MetaIndex.HeadEst * 8 )] + [FieldOffset(8 + (int)MetaIndex.HeadEst * 8)] public ResourceHandle* HeadEstResource; - [FieldOffset( 8 + IndexTransparentTex * 8 )] + [FieldOffset(8 + IndexTransparentTex * 8)] public TextureResourceHandle* TransparentTexResource; - [FieldOffset( 8 + IndexDecalTex * 8 )] + [FieldOffset(8 + IndexDecalTex * 8)] public TextureResourceHandle* DecalTexResource; - [FieldOffset( 8 + IndexSkinShpk * 8 )] + [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; // not included resources have no known use case. -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index c8fd082d..3211c4f9 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -1,11 +1,11 @@ namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct ClipScheduler { - [FieldOffset( 0 )] + [FieldOffset(0)] public IntPtr* VTable; - [FieldOffset( 0x38 )] + [FieldOffset(0x38)] public IntPtr SchedulerTimeline; -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/DrawState.cs b/Penumbra/Interop/Structs/DrawState.cs index d0dfc22b..200e7952 100644 --- a/Penumbra/Interop/Structs/DrawState.cs +++ b/Penumbra/Interop/Structs/DrawState.cs @@ -9,4 +9,4 @@ public enum DrawState : uint MaybeCulled = 0x00_00_04_00, MaybeHiddenMinion = 0x00_00_80_00, MaybeHiddenSummon = 0x00_80_00_00, -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/FileMode.cs b/Penumbra/Interop/Structs/FileMode.cs index 1c1914b2..4e48b3c1 100644 --- a/Penumbra/Interop/Structs/FileMode.cs +++ b/Penumbra/Interop/Structs/FileMode.cs @@ -8,4 +8,4 @@ public enum FileMode : byte // Probably debug options only. LoadIndexResource = 0xA, // load index/index2 LoadSqPackResource = 0xB, -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 5eafa1f3..274b4fb2 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -2,18 +2,18 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct HumanExt { - [FieldOffset( 0x0 )] + [FieldOffset(0x0)] public Human Human; - [FieldOffset( 0x0 )] + [FieldOffset(0x0)] public CharacterBaseExt CharacterBase; - [FieldOffset( 0x9E8 )] + [FieldOffset(0x9E8)] public ResourceHandle* Decal; - [FieldOffset( 0x9F0 )] + [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 2bca832d..0165a8ff 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -2,45 +2,46 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit, Size = 0x40 )] +[StructLayout(LayoutKind.Explicit, Size = 0x40)] public unsafe struct Material { - [FieldOffset( 0x10 )] + [FieldOffset(0x10)] public MtrlResource* ResourceHandle; - [FieldOffset( 0x18 )] + [FieldOffset(0x18)] public uint ShaderPackageFlags; - [FieldOffset( 0x20 )] + [FieldOffset(0x20)] public uint* ShaderKeys; public int ShaderKeyCount => (int)((uint*)Textures - ShaderKeys); - [FieldOffset( 0x28 )] + [FieldOffset(0x28)] public ConstantBuffer* MaterialParameter; - [FieldOffset( 0x30 )] + [FieldOffset(0x30)] public TextureEntry* Textures; - [FieldOffset( 0x38 )] + [FieldOffset(0x38)] public ushort TextureCount; - public Texture* Texture( int index ) => Textures[index].ResourceHandle->KernelTexture; + public Texture* Texture(int index) + => Textures[index].ResourceHandle->KernelTexture; - [StructLayout( LayoutKind.Explicit, Size = 0x18 )] + [StructLayout(LayoutKind.Explicit, Size = 0x18)] public struct TextureEntry { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public uint Id; - [FieldOffset( 0x08 )] + [FieldOffset(0x08)] public TextureResourceHandle* ResourceHandle; - [FieldOffset( 0x10 )] + [FieldOffset(0x10)] public uint SamplerFlags; } public ReadOnlySpan TextureSpan - => new(Textures, TextureCount); -} \ No newline at end of file + => new(Textures, TextureCount); +} diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 55c7f8d8..c3b86e14 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -1,45 +1,45 @@ namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct MtrlResource { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public ResourceHandle Handle; - [FieldOffset( 0xC8 )] + [FieldOffset(0xC8)] public ShaderPackageResourceHandle* ShpkResourceHandle; - [FieldOffset( 0xD0 )] + [FieldOffset(0xD0)] public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. - [FieldOffset( 0xE0 )] + [FieldOffset(0xE0)] public byte* StringList; - [FieldOffset( 0xF8 )] + [FieldOffset(0xF8)] public ushort ShpkOffset; - [FieldOffset( 0xFA )] + [FieldOffset(0xFA)] public byte NumTex; public byte* ShpkString => StringList + ShpkOffset; - public byte* TexString( int idx ) + public byte* TexString(int idx) => StringList + TexSpace[idx].PathOffset; - public bool TexIsDX11( int idx ) + public bool TexIsDX11(int idx) => TexSpace[idx].Flags >= 0x8000; [StructLayout(LayoutKind.Explicit, Size = 0x10)] public struct TextureEntry { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public TextureResourceHandle* ResourceHandle; - [FieldOffset( 0x08 )] + [FieldOffset(0x08)] public ushort PathOffset; - [FieldOffset( 0x0A )] + [FieldOffset(0x0A)] public ushort Flags; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs index 25e928be..f9cb2d56 100644 --- a/Penumbra/Interop/Structs/RenderModel.cs +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -2,39 +2,39 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct RenderModel { - [FieldOffset( 0x18 )] + [FieldOffset(0x18)] public RenderModel* PreviousModel; - [FieldOffset( 0x20 )] + [FieldOffset(0x20)] public RenderModel* NextModel; - [FieldOffset( 0x30 )] + [FieldOffset(0x30)] public ResourceHandle* ResourceHandle; - [FieldOffset( 0x40 )] + [FieldOffset(0x40)] public Skeleton* Skeleton; - [FieldOffset( 0x58 )] + [FieldOffset(0x58)] public void** BoneList; - [FieldOffset( 0x60 )] + [FieldOffset(0x60)] public int BoneListCount; - [FieldOffset( 0x70 )] + [FieldOffset(0x70)] private void* UnkDXBuffer1; - [FieldOffset( 0x78 )] + [FieldOffset(0x78)] private void* UnkDXBuffer2; - [FieldOffset( 0x80 )] + [FieldOffset(0x80)] private void* UnkDXBuffer3; - [FieldOffset( 0x98 )] + [FieldOffset(0x98)] public void** Materials; - [FieldOffset( 0xA0 )] + [FieldOffset(0xA0)] public int MaterialCount; -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/ResidentResourceManager.cs b/Penumbra/Interop/Structs/ResidentResourceManager.cs index 08461b03..131f2884 100644 --- a/Penumbra/Interop/Structs/ResidentResourceManager.cs +++ b/Penumbra/Interop/Structs/ResidentResourceManager.cs @@ -3,15 +3,15 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct ResidentResourceManager { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public void** VTable; - [FieldOffset( 0x08 )] + [FieldOffset(0x08)] public void** ResourceListVTable; - [FieldOffset( 0x14 )] + [FieldOffset(0x14)] public uint NumResources; - [FieldOffset( 0x18 )] + [FieldOffset(0x18)] public ResourceHandle** ResourceList; -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index b23cf62f..dba113f3 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -8,45 +8,45 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct TextureResourceHandle { - [FieldOffset( 0x0 )] + [FieldOffset(0x0)] public ResourceHandle Handle; - [FieldOffset( 0x38 )] + [FieldOffset(0x38)] public IntPtr Unk; - [FieldOffset( 0x118 )] + [FieldOffset(0x118)] public Texture* KernelTexture; - [FieldOffset( 0x20 )] + [FieldOffset(0x20)] public IntPtr NewKernelTexture; } [StructLayout(LayoutKind.Explicit)] public unsafe struct ShaderPackageResourceHandle { - [FieldOffset( 0x0 )] + [FieldOffset(0x0)] public ResourceHandle Handle; - [FieldOffset( 0xB0 )] + [FieldOffset(0xB0)] public ShaderPackage* ShaderPackage; } -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct ResourceHandle { - [StructLayout( LayoutKind.Explicit )] + [StructLayout(LayoutKind.Explicit)] public struct DataIndirection { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public void** VTable; - [FieldOffset( 0x10 )] + [FieldOffset(0x10)] public byte* DataPtr; - [FieldOffset( 0x28 )] + [FieldOffset(0x28)] public ulong DataLength; } @@ -54,87 +54,83 @@ public unsafe struct ResourceHandle public byte* FileNamePtr() { - if( FileNameLength > SsoSize ) - { + if (FileNameLength > SsoSize) return FileNameData; - } - fixed( byte** name = &FileNameData ) + fixed (byte** name = &FileNameData) { - return ( byte* )name; + return (byte*)name; } } public ByteString FileName() - => ByteString.FromByteStringUnsafe( FileNamePtr(), FileNameLength, true ); + => ByteString.FromByteStringUnsafe(FileNamePtr(), FileNameLength, true); - public ReadOnlySpan< byte > FileNameAsSpan() - => new( FileNamePtr(), FileNameLength ); + public ReadOnlySpan FileNameAsSpan() + => new(FileNamePtr(), FileNameLength); - public bool GamePath( out Utf8GamePath path ) - => Utf8GamePath.FromSpan( FileNameAsSpan(), out path ); + public bool GamePath(out Utf8GamePath path) + => Utf8GamePath.FromSpan(FileNameAsSpan(), out path); - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public void** VTable; - [FieldOffset( 0x08 )] + [FieldOffset(0x08)] public ResourceCategory Category; - [FieldOffset( 0x0C )] + [FieldOffset(0x0C)] public ResourceType FileType; - [FieldOffset( 0x10 )] + [FieldOffset(0x10)] public uint Id; - [FieldOffset( 0x28 )] + [FieldOffset(0x28)] public uint FileSize; - [FieldOffset( 0x2C )] + [FieldOffset(0x2C)] public uint FileSize2; - [FieldOffset( 0x34 )] + [FieldOffset(0x34)] public uint FileSize3; - [FieldOffset( 0x48 )] + [FieldOffset(0x48)] public byte* FileNameData; - [FieldOffset( 0x58 )] + [FieldOffset(0x58)] public int FileNameLength; - [FieldOffset( 0xAC )] + [FieldOffset(0xAC)] public uint RefCount; // May return null. - public static byte* GetData( ResourceHandle* handle ) - => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ Offsets.ResourceHandleGetDataVfunc ] )( handle ); + public static byte* GetData(ResourceHandle* handle) + => ((delegate* unmanaged< ResourceHandle*, byte* >)handle->VTable[Offsets.ResourceHandleGetDataVfunc])(handle); - public static ulong GetLength( ResourceHandle* handle ) - => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ Offsets.ResourceHandleGetLengthVfunc ] )( handle ); + public static ulong GetLength(ResourceHandle* handle) + => ((delegate* unmanaged< ResourceHandle*, ulong >)handle->VTable[Offsets.ResourceHandleGetLengthVfunc])(handle); // Only use these if you know what you are doing. // Those are actually only sure to be accessible for DefaultResourceHandles. - [FieldOffset( 0xB0 )] + [FieldOffset(0xB0)] public DataIndirection* Data; - [FieldOffset( 0xB8 )] + [FieldOffset(0xB8)] public uint DataLength; public (IntPtr Data, int Length) GetData() => Data != null - ? ( ( IntPtr )Data->DataPtr, ( int )Data->DataLength ) - : ( IntPtr.Zero, 0 ); + ? ((IntPtr)Data->DataPtr, (int)Data->DataLength) + : (IntPtr.Zero, 0); - public bool SetData( IntPtr data, int length ) + public bool SetData(IntPtr data, int length) { - if( Data == null ) - { + if (Data == null) return false; - } - Data->DataPtr = length != 0 ? ( byte* )data : null; - Data->DataLength = ( ulong )length; - DataLength = ( uint )length; + Data->DataPtr = length != 0 ? (byte*)data : null; + Data->DataLength = (ulong)length; + DataLength = (uint)length; return true; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs index d4ca3afa..67730799 100644 --- a/Penumbra/Interop/Structs/SeFileDescriptor.cs +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -1,18 +1,17 @@ namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct SeFileDescriptor { - [FieldOffset( 0x00 )] + [FieldOffset(0x00)] public FileMode FileMode; - [FieldOffset( 0x30 )] - public void* FileDescriptor; // + [FieldOffset(0x30)] + public void* FileDescriptor; - [FieldOffset( 0x50 )] - public ResourceHandle* ResourceHandle; // + [FieldOffset(0x50)] + public ResourceHandle* ResourceHandle; - - [FieldOffset( 0x70 )] - public char Utf16FileName; // -} \ No newline at end of file + [FieldOffset(0x70)] + public char Utf16FileName; +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs index ec9c4b71..a81480fb 100644 --- a/Penumbra/Interop/Structs/TextureUtility.cs +++ b/Penumbra/Interop/Structs/TextureUtility.cs @@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -public unsafe static class TextureUtility +public static unsafe class TextureUtility { private static readonly Functions Funcs = new(); public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) - => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, flags, unk); + => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, + flags, unk); public static bool InitializeContents(Texture* texture, void* contents) => ((delegate* unmanaged)Funcs.TextureInitializeContents)(texture, contents); diff --git a/Penumbra/Interop/Structs/VfxParams.cs b/Penumbra/Interop/Structs/VfxParams.cs index 76dbd3ed..c3ae1751 100644 --- a/Penumbra/Interop/Structs/VfxParams.cs +++ b/Penumbra/Interop/Structs/VfxParams.cs @@ -1,17 +1,17 @@ namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout(LayoutKind.Explicit)] public unsafe struct VfxParams { - [FieldOffset( 0x118 )] + [FieldOffset(0x118)] public uint GameObjectId; - [FieldOffset( 0x11C )] + [FieldOffset(0x11C)] public byte GameObjectType; - [FieldOffset( 0xD0 )] + [FieldOffset(0xD0)] public ushort TargetCount; - [FieldOffset( 0x120 )] + [FieldOffset(0x120)] public fixed ulong Target[16]; -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 1e0756f2..df7ed2e4 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -101,8 +101,8 @@ public readonly struct EqdpManipulation : IMetaManipulation if (FileIndex() == (MetaIndex)(-1)) return false; - - // No check for set id. + + // No check for set id. return true; } } diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 8e1a2ded..4373e8e9 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -9,73 +9,71 @@ using SharpCompress.Common; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct EqpManipulation : IMetaManipulation { - [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] + [JsonConverter(typeof(ForceNumericFlagEnumConverter))] public EqpEntry Entry { get; private init; } public SetId SetId { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqpManipulation( EqpEntry entry, EquipSlot slot, SetId setId ) + public EqpManipulation(EqpEntry entry, EquipSlot slot, SetId setId) { Slot = slot; SetId = setId; - Entry = Eqp.Mask( slot ) & entry; + Entry = Eqp.Mask(slot) & entry; } - public EqpManipulation Copy( EqpEntry entry ) + public EqpManipulation Copy(EqpEntry entry) => new(entry, Slot, SetId); public override string ToString() => $"Eqp - {SetId} - {Slot}"; - public bool Equals( EqpManipulation other ) - => Slot == other.Slot + public bool Equals(EqpManipulation other) + => Slot == other.Slot && SetId == other.SetId; - public override bool Equals( object? obj ) - => obj is EqpManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is EqpManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine( ( int )Slot, SetId ); + => HashCode.Combine((int)Slot, SetId); - public int CompareTo( EqpManipulation other ) + public int CompareTo(EqpManipulation other) { - var set = SetId.Id.CompareTo( other.SetId.Id ); - return set != 0 ? set : Slot.CompareTo( other.Slot ); + var set = SetId.Id.CompareTo(other.SetId.Id); + return set != 0 ? set : Slot.CompareTo(other.Slot); } public MetaIndex FileIndex() => MetaIndex.Eqp; - public bool Apply( ExpandedEqpFile file ) + public bool Apply(ExpandedEqpFile file) { - var entry = file[ SetId ]; - var mask = Eqp.Mask( Slot ); - if( ( entry & mask ) == Entry ) - { + var entry = file[SetId]; + var mask = Eqp.Mask(Slot); + if ((entry & mask) == Entry) return false; - } - file[ SetId ] = ( entry & ~mask ) | Entry; + file[SetId] = (entry & ~mask) | Entry; return true; } public bool Validate() - { + { var mask = Eqp.Mask(Slot); if (mask == 0) return false; if ((Entry & mask) != Entry) return false; - - // No check for set id. + + // No check for set id. return true; } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index da834b35..455c39ff 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -7,8 +7,8 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct EstManipulation : IMetaManipulation< EstManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct EstManipulation : IMetaManipulation { public enum EstType : byte { @@ -18,7 +18,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = MetaIndex.HeadEst, } - public static string ToName( EstType type ) + public static string ToName(EstType type) => type switch { EstType.Hair => "hair", @@ -30,19 +30,19 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public ushort Entry { get; private init; } // SkeletonIdx. - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public Gender Gender { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public ModelRace Race { get; private init; } public SetId SetId { get; private init; } - [JsonConverter( typeof( StringEnumConverter ) )] + [JsonConverter(typeof(StringEnumConverter))] public EstType Slot { get; private init; } [JsonConstructor] - public EstManipulation( Gender gender, ModelRace race, EstType slot, SetId setId, ushort entry ) + public EstManipulation(Gender gender, ModelRace race, EstType slot, SetId setId, ushort entry) { Entry = entry; Gender = gender; @@ -51,49 +51,45 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Slot = slot; } - public EstManipulation Copy( ushort entry ) + public EstManipulation Copy(ushort entry) => new(Gender, Race, Slot, SetId, entry); public override string ToString() => $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}"; - public bool Equals( EstManipulation other ) + public bool Equals(EstManipulation other) => Gender == other.Gender - && Race == other.Race + && Race == other.Race && SetId == other.SetId - && Slot == other.Slot; + && Slot == other.Slot; - public override bool Equals( object? obj ) - => obj is EstManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is EstManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot ); + => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - public int CompareTo( EstManipulation other ) + public int CompareTo(EstManipulation other) { - var r = Race.CompareTo( other.Race ); - if( r != 0 ) - { + var r = Race.CompareTo(other.Race); + if (r != 0) return r; - } - var g = Gender.CompareTo( other.Gender ); - if( g != 0 ) - { + var g = Gender.CompareTo(other.Gender); + if (g != 0) return g; - } - var s = Slot.CompareTo( other.Slot ); - return s != 0 ? s : SetId.Id.CompareTo( other.SetId.Id ); + var s = Slot.CompareTo(other.Slot); + return s != 0 ? s : SetId.Id.CompareTo(other.SetId.Id); } public MetaIndex FileIndex() - => ( MetaIndex )Slot; + => (MetaIndex)Slot; - public bool Apply( EstFile file ) + public bool Apply(EstFile file) { - return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId.Id, Entry ) switch + return file.SetEntry(Names.CombinedRace(Gender, Race), SetId.Id, Entry) switch { EstFile.EstEntryChange.Unchanged => false, EstFile.EstEntryChange.Changed => true, @@ -109,7 +105,8 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > return false; if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown) return false; - // No known check for set id or entry. + + // No known check for set id or entry. return true; } -} \ No newline at end of file +} diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 94f7f971..928b6f55 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -5,55 +5,51 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; -[StructLayout( LayoutKind.Sequential, Pack = 1 )] -public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly struct GmpManipulation : IMetaManipulation { public GmpEntry Entry { get; private init; } - public SetId SetId { get; private init; } + public SetId SetId { get; private init; } [JsonConstructor] - public GmpManipulation( GmpEntry entry, SetId setId ) + public GmpManipulation(GmpEntry entry, SetId setId) { Entry = entry; SetId = setId; } - public GmpManipulation Copy( GmpEntry entry ) + public GmpManipulation Copy(GmpEntry entry) => new(entry, SetId); public override string ToString() => $"Gmp - {SetId}"; - public bool Equals( GmpManipulation other ) + public bool Equals(GmpManipulation other) => SetId == other.SetId; - public override bool Equals( object? obj ) - => obj is GmpManipulation other && Equals( other ); + public override bool Equals(object? obj) + => obj is GmpManipulation other && Equals(other); public override int GetHashCode() => SetId.GetHashCode(); - public int CompareTo( GmpManipulation other ) - => SetId.Id.CompareTo( other.SetId.Id ); + public int CompareTo(GmpManipulation other) + => SetId.Id.CompareTo(other.SetId.Id); public MetaIndex FileIndex() => MetaIndex.Gmp; - public bool Apply( ExpandedGmpFile file ) + public bool Apply(ExpandedGmpFile file) { - var entry = file[ SetId ]; - if( entry == Entry ) - { + var entry = file[SetId]; + if (entry == Entry) return false; - } - file[ SetId ] = Entry; + file[SetId] = Entry; return true; } public bool Validate() - { // No known conditions. - return true; - } -} \ No newline at end of file + => true; +} diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 2f19537b..3155e188 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -10,6 +10,7 @@ using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 12fb4f35..488c1c91 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -59,7 +59,7 @@ public class DuplicateManager public void Clear() { _cancellationTokenSource.Cancel(); - Worker = Task.CompletedTask; + Worker = Task.CompletedTask; _duplicates.Clear(); SavedSpace = 0; } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 2100526a..0aa70e61 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 6fa645a9..cd8a9594 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -7,14 +7,14 @@ public interface IMod { LowerString Name { get; } - public int Index { get; } + public int Index { get; } public int Priority { get; } - public ISubMod Default { get; } - public IReadOnlyList< IModGroup > Groups { get; } + public ISubMod Default { get; } + public IReadOnlyList Groups { get; } - public IEnumerable< SubMod > AllSubMods { get; } - - // Cache + public IEnumerable AllSubMods { get; } + + // Cache public int TotalManipulations { get; } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index f4904271..8de93bcc 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -1,25 +1,26 @@ using System.IO.Compression; -using OtterGui.Tasks; +using OtterGui.Tasks; using Penumbra.Mods.Manager; namespace Penumbra.Mods.Editor; /// Utility to create and apply a zipped backup of a mod. public class ModBackup -{ +{ /// Set when reading Config and migrating from v4 to v5. public static bool MigrateModBackups = false; + public static bool CreatingBackup { get; private set; } - private readonly Mod _mod; - public readonly string Name; - public readonly bool Exists; + private readonly Mod _mod; + public readonly string Name; + public readonly bool Exists; public ModBackup(ModExportManager modExportManager, Mod mod) - { - _mod = mod; - Name = Path.Combine(modExportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; - Exists = File.Exists(Name); + { + _mod = mod; + Name = Path.Combine(modExportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp"; + Exists = File.Exists(Name); } /// Migrate file extensions. diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 85a7a544..e3862c90 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,4 +1,5 @@ using OtterGui; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 1ca3edd4..82629971 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,5 +1,6 @@ using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; @@ -7,7 +8,7 @@ namespace Penumbra.Mods; public class ModFileEditor { private readonly ModFileCollection _files; - private readonly ModManager _modManager; + private readonly ModManager _modManager; public bool Changes { get; private set; } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 5c9be2ac..f90f6c0a 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -5,6 +5,7 @@ using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.ModsTab; @@ -174,7 +175,7 @@ public class ModMerger : IDisposable ret = new FullPath(MergeToMod!.ModPath, relPath); return true; } - + foreach (var originalOption in mergeOptions) { foreach (var manip in originalOption.Manipulations) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index e389c86a..09c5c77a 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,5 +1,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; @@ -146,6 +147,7 @@ public class ModMetaEditor } } } + Split(currentOption.Manipulations); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9b17c390..eebc8ab4 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Tasks; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index c00bb821..b9834da8 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,11 +1,12 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; using Penumbra.Util; public class ModSwapEditor { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly Dictionary _swaps = new(); public IReadOnlyDictionary Swaps diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index da56d204..b9200a24 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -10,27 +10,29 @@ namespace Penumbra.Mods.ItemSwap; public static class CustomizationSwap { /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. - public static FileSwap CreateMdl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) + public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, + SetId idFrom, SetId idTo) { - if( idFrom.Id > byte.MaxValue ) - { - throw new Exception( $"The Customization ID {idFrom} is too large for {slot}." ); - } + if (idFrom.Id > byte.MaxValue) + throw new Exception($"The Customization ID {idFrom} is too large for {slot}."); - var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() ); - var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() ); + var mdlPathFrom = GamePaths.Character.Mdl.Path(race, slot, idFrom, slot.ToCustomizationType()); + var mdlPathTo = GamePaths.Character.Mdl.Path(race, slot, idTo, slot.ToCustomizationType()); - var mdl = FileSwap.CreateSwap( manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); - var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1; + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var range = slot == BodySlot.Tail + && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc + ? 5 + : 1; - foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() ) + foreach (ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan()) { var name = materialFileName; - foreach( var variant in Enumerable.Range( 1, range ) ) + foreach (var variant in Enumerable.Range(1, range)) { name = materialFileName; - var mtrl = CreateMtrl( manager, redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged ); - mdl.ChildSwaps.Add( mtrl ); + var mtrl = CreateMtrl(manager, redirections, slot, race, idFrom, idTo, (byte)variant, ref name, ref mdl.DataWasChanged); + mdl.ChildSwaps.Add(mtrl); } materialFileName = name; @@ -39,71 +41,75 @@ public static class CustomizationSwap return mdl; } - public static FileSwap CreateMtrl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, - ref string fileName, ref bool dataWasChanged ) + public static FileSwap CreateMtrl(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, + SetId idFrom, SetId idTo, byte variant, + ref string fileName, ref bool dataWasChanged) { variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; - var mtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant ); - var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant ); + var mtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); + var mtrlToPath = GamePaths.Character.Mtrl.Path(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); var newFileName = fileName; - newFileName = ItemSwap.ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race ); - newFileName = ItemSwap.ReplaceBody( newFileName, slot, idTo, idFrom, idFrom != idTo ); - newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race || MaterialHandling.IsSpecialCase( race, idFrom ) ); - newFileName = ItemSwap.AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Id:D4}", gameSetIdFrom != idFrom ); + newFileName = ItemSwap.ReplaceRace(newFileName, gameRaceTo, race, gameRaceTo != race); + newFileName = ItemSwap.ReplaceBody(newFileName, slot, idTo, idFrom, idFrom != idTo); + newFileName = ItemSwap.AddSuffix(newFileName, ".mtrl", $"_c{race.ToRaceCode()}", + gameRaceFrom != race || MaterialHandling.IsSpecialCase(race, idFrom)); + newFileName = ItemSwap.AddSuffix(newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Id:D4}", gameSetIdFrom != idFrom); var actualMtrlFromPath = mtrlFromPath; - if( newFileName != fileName ) + if (newFileName != fileName) { - actualMtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, newFileName, out _, out _, variant ); + actualMtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, newFileName, out _, out _, variant); fileName = newFileName; dataWasChanged = true; } - var mtrl = FileSwap.CreateSwap( manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath ); - var shpk = CreateShader( manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); - mtrl.ChildSwaps.Add( shpk ); + var mtrl = FileSwap.CreateSwap(manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath); + var shpk = CreateShader(manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged); + mtrl.ChildSwaps.Add(shpk); - foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) + foreach (ref var texture in mtrl.AsMtrl()!.Textures.AsSpan()) { - var tex = CreateTex( manager, redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged ); - mtrl.ChildSwaps.Add( tex ); + var tex = CreateTex(manager, redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged); + mtrl.ChildSwaps.Add(tex); } return mtrl; } - public static FileSwap CreateTex( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, - ref bool dataWasChanged ) + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, + SetId idFrom, ref MtrlFile.Texture texture, + ref bool dataWasChanged) { var path = texture.Path; var addedDashes = false; - if( texture.DX11 ) + if (texture.DX11) { - var fileName = Path.GetFileName( path ); - if( !fileName.StartsWith( "--" ) ) + var fileName = Path.GetFileName(path); + if (!fileName.StartsWith("--")) { - path = path.Replace( fileName, $"--{fileName}" ); + path = path.Replace(fileName, $"--{fileName}"); addedDashes = true; } } - var newPath = ItemSwap.ReplaceAnyRace( path, race ); - newPath = ItemSwap.ReplaceAnyBody( newPath, slot, idFrom ); - newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}", true ); - if( newPath != path ) + var newPath = ItemSwap.ReplaceAnyRace(path, race); + newPath = ItemSwap.ReplaceAnyBody(newPath, slot, idFrom); + newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}", true); + if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } - return FileSwap.CreateSwap( manager, ResourceType.Tex, redirections, newPath, path, path ); + return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, newPath, path, path); } - public static FileSwap CreateShader( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) + public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, + ref bool dataWasChanged) { var path = $"shader/sm5/shpk/{shaderName}"; - return FileSwap.CreateSwap( manager, ResourceType.Shpk, redirections, path, path ); + return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 188fc317..d95c8796 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -249,10 +249,10 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, Variant variantFrom) { - var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry); + var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); + var imc = new ImcFile(manager, entry); EquipItem[] items; - Variant[] variants; + Variant[] variants; if (idFrom == idTo) { items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray(); @@ -264,8 +264,9 @@ public static class EquipmentSwap else { items = identifier.Identify(slotFrom.IsEquipment() - ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); + ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType() + .ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } @@ -283,11 +284,13 @@ public static class EquipmentSwap return new MetaSwap(manips, manipFrom, manipTo); } - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, EquipSlot slot, + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + Func manips, EquipSlot slot, SetId idFrom, SetId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + Func manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { @@ -401,7 +404,8 @@ public static class EquipmentSwap ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); - public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, + EquipSlot slotTo, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var path = texture.Path; @@ -428,13 +432,15 @@ public static class EquipmentSwap return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, newPath, path, path); } - public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, ref bool dataWasChanged) + public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, + ref bool dataWasChanged) { var path = $"shader/sm5/shpk/{shaderName}"; return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } - public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, ref string filePath, ref bool dataWasChanged) + public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, ref string filePath, + ref bool dataWasChanged) { var oldPath = filePath; filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 02e02ccb..0140d189 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -20,47 +20,45 @@ public static class ItemSwap { public readonly ResourceType Type; - public MissingFileException( ResourceType type, object path ) + public MissingFileException(ResourceType type, object path) : base($"Could not load {type} File Data for \"{path}\".") => Type = type; } - private static bool LoadFile( MetaFileManager manager, FullPath path, out byte[] data ) + private static bool LoadFile(MetaFileManager manager, FullPath path, out byte[] data) { - if( path.FullName.Length > 0 ) - { + if (path.FullName.Length > 0) try { - if( path.IsRooted ) + if (path.IsRooted) { - data = File.ReadAllBytes( path.FullName ); + data = File.ReadAllBytes(path.FullName); return true; } - var file = manager.GameData.GetFile( path.InternalName.ToString() ); - if( file != null ) + var file = manager.GameData.GetFile(path.InternalName.ToString()); + if (file != null) { data = file.Data; return true; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Debug( $"Could not load file {path}:\n{e}" ); + Penumbra.Log.Debug($"Could not load file {path}:\n{e}"); } - } - data = Array.Empty< byte >(); + data = Array.Empty(); return false; } public class GenericFile : IWritable { public readonly byte[] Data; - public bool Valid { get; } + public bool Valid { get; } - public GenericFile( MetaFileManager manager, FullPath path ) - => Valid = LoadFile( manager, path, out Data ); + public GenericFile(MetaFileManager manager, FullPath path) + => Valid = LoadFile(manager, path, out Data); public byte[] Write() => Data; @@ -68,69 +66,67 @@ public static class ItemSwap public static readonly GenericFile Invalid = new(null!, FullPath.Empty); } - public static bool LoadFile( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out GenericFile? file ) + public static bool LoadFile(MetaFileManager manager, FullPath path, [NotNullWhen(true)] out GenericFile? file) { - file = new GenericFile( manager, path ); - if( file.Valid ) - { + file = new GenericFile(manager, path); + if (file.Valid) return true; - } file = null; return false; } - public static bool LoadMdl( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MdlFile? file ) + public static bool LoadMdl(MetaFileManager manager, FullPath path, [NotNullWhen(true)] out MdlFile? file) { try { - if( LoadFile( manager, path, out byte[] data ) ) + if (LoadFile(manager, path, out byte[] data)) { - file = new MdlFile( data ); + file = new MdlFile(data); return true; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Debug( $"Could not parse file {path} to Mdl:\n{e}" ); + Penumbra.Log.Debug($"Could not parse file {path} to Mdl:\n{e}"); } file = null; return false; } - public static bool LoadMtrl(MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MtrlFile? file ) + public static bool LoadMtrl(MetaFileManager manager, FullPath path, [NotNullWhen(true)] out MtrlFile? file) { try { - if( LoadFile( manager, path, out byte[] data ) ) + if (LoadFile(manager, path, out byte[] data)) { - file = new MtrlFile( data ); + file = new MtrlFile(data); return true; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Debug( $"Could not parse file {path} to Mtrl:\n{e}" ); + Penumbra.Log.Debug($"Could not parse file {path} to Mtrl:\n{e}"); } file = null; return false; } - public static bool LoadAvfx( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out AvfxFile? file ) + public static bool LoadAvfx(MetaFileManager manager, FullPath path, [NotNullWhen(true)] out AvfxFile? file) { try { - if( LoadFile( manager, path, out byte[] data ) ) + if (LoadFile(manager, path, out byte[] data)) { - file = new AvfxFile( data ); + file = new AvfxFile(data); return true; } } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Debug( $"Could not parse file {path} to Avfx:\n{e}" ); + Penumbra.Log.Debug($"Could not parse file {path} to Avfx:\n{e}"); } file = null; @@ -138,40 +134,41 @@ public static class ItemSwap } - public static FileSwap CreatePhyb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) + public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, + GenderRace race, ushort estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap( manager, ResourceType.Phyb, redirections, phybPath, phybPath ); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry); + return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } - public static FileSwap CreateSklb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) + public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, + GenderRace race, ushort estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry ); - return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath ); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry); + return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static MetaSwap? CreateEst( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, - GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl ) + public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, + Func manips, EstManipulation.EstType type, + GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl) { - if( type == 0 ) - { + if (type == 0) return null; - } var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation( gender, race, type, idFrom, EstFile.GetDefault( manager, type, genderRace, idFrom ) ); - var toDefault = new EstManipulation( gender, race, type, idTo, EstFile.GetDefault( manager, type, genderRace, idTo ) ); - var est = new MetaSwap( manips, fromDefault, toDefault ); + var fromDefault = new EstManipulation(gender, race, type, idFrom, EstFile.GetDefault(manager, type, genderRace, idFrom)); + var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); + var est = new MetaSwap(manips, fromDefault, toDefault); - if( ownMdl && est.SwapApplied.Est.Entry >= 2 ) + if (ownMdl && est.SwapApplied.Est.Entry >= 2) { - var phyb = CreatePhyb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry ); - est.ChildSwaps.Add( phyb ); - var sklb = CreateSklb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry ); - est.ChildSwaps.Add( sklb ); + var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + est.ChildSwaps.Add(phyb); + var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + est.ChildSwaps.Add(sklb); } - else if( est.SwapAppliedIsDefault ) + else if (est.SwapAppliedIsDefault) { return null; } @@ -179,57 +176,55 @@ public static class ItemSwap return est; } - public static int GetStableHashCode( this string str ) + public static int GetStableHashCode(this string str) { unchecked { var hash1 = 5381; var hash2 = hash1; - for( var i = 0; i < str.Length && str[ i ] != '\0'; i += 2 ) + for (var i = 0; i < str.Length && str[i] != '\0'; i += 2) { - hash1 = ( ( hash1 << 5 ) + hash1 ) ^ str[ i ]; - if( i == str.Length - 1 || str[ i + 1 ] == '\0' ) - { + hash1 = ((hash1 << 5) + hash1) ^ str[i]; + if (i == str.Length - 1 || str[i + 1] == '\0') break; - } - hash2 = ( ( hash2 << 5 ) + hash2 ) ^ str[ i + 1 ]; + hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; } return hash1 + hash2 * 1566083941; } } - public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true ) + public static string ReplaceAnyId(string path, char idType, SetId id, bool condition = true) => condition - ? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Id:D4}" ) + ? Regex.Replace(path, $"{idType}\\d{{4}}", $"{idType}{id.Id:D4}") : path; - public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true ) - => ReplaceAnyId( path, 'c', ( ushort )to, condition ); + public static string ReplaceAnyRace(string path, GenderRace to, bool condition = true) + => ReplaceAnyId(path, 'c', (ushort)to, condition); - public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true ) - => ReplaceAnyId( path, slot.ToAbbreviation(), to, condition ); + public static string ReplaceAnyBody(string path, BodySlot slot, SetId to, bool condition = true) + => ReplaceAnyId(path, slot.ToAbbreviation(), to, condition); - public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true ) + public static string ReplaceId(string path, char type, SetId idFrom, SetId idTo, bool condition = true) => condition - ? path.Replace( $"{type}{idFrom.Id:D4}", $"{type}{idTo.Id:D4}" ) + ? path.Replace($"{type}{idFrom.Id:D4}", $"{type}{idTo.Id:D4}") : path; - public static string ReplaceSlot( string path, EquipSlot from, EquipSlot to, bool condition = true ) + public static string ReplaceSlot(string path, EquipSlot from, EquipSlot to, bool condition = true) => condition - ? path.Replace( $"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_" ) + ? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_") : path; - public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true ) - => ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition ); + public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true) + => ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); - public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true ) - => ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition ); + public static string ReplaceBody(string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true) + => ReplaceId(path, slot.ToAbbreviation(), idFrom, idTo, condition); - public static string AddSuffix( string path, string ext, string suffix, bool condition = true ) + public static string AddSuffix(string path, string ext, string suffix, bool condition = true) => condition - ? path.Replace( ext, suffix + ext ) + ? path.Replace(ext, suffix + ext) : path; -} \ No newline at end of file +} diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 8e6473cc..1db890ed 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -6,6 +6,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; @@ -13,18 +14,18 @@ namespace Penumbra.Mods.ItemSwap; public class ItemSwapContainer { private readonly MetaFileManager _manager; - private readonly IdentifierService _identifier; + private readonly IdentifierService _identifier; - private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); - private HashSet< MetaManipulation > _modManipulations = new(); + private Dictionary _modRedirections = new(); + private HashSet _modManipulations = new(); - public IReadOnlyDictionary< Utf8GamePath, FullPath > ModRedirections + public IReadOnlyDictionary ModRedirections => _modRedirections; - public IReadOnlySet< MetaManipulation > ModManipulations + public IReadOnlySet ModManipulations => _modManipulations; - public readonly List< Swap > Swaps = new(); + public readonly List Swaps = new(); public bool Loaded { get; private set; } @@ -40,72 +41,69 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod( ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) + public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, + int groupIndex = -1, int optionIndex = 0) { - var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); - var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); - var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); + var convertedManips = new HashSet(Swaps.Count); + var convertedFiles = new Dictionary(Swaps.Count); + var convertedSwaps = new Dictionary(Swaps.Count); directory ??= mod.ModPath; try { - foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) ) + foreach (var swap in Swaps.SelectMany(s => s.WithChildren())) { - switch( swap ) + switch (swap) { case FileSwap file: // Skip, nothing to do - if( file.SwapToModdedEqualsOriginal ) - { + if (file.SwapToModdedEqualsOriginal) continue; - } - if( writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged ) + if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) { - convertedSwaps.TryAdd( file.SwapFromRequestPath, file.SwapToModded ); + convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); } else { - var path = file.GetNewPath( directory.FullName ); + var path = file.GetNewPath(directory.FullName); var bytes = file.FileData.Write(); - Directory.CreateDirectory( Path.GetDirectoryName( path )! ); - _manager.Compactor.WriteAllBytes( path, bytes ); - convertedFiles.TryAdd( file.SwapFromRequestPath, new FullPath( path ) ); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _manager.Compactor.WriteAllBytes(path, bytes); + convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); } break; case MetaSwap meta: - if( !meta.SwapAppliedIsDefault ) - { - convertedManips.Add( meta.SwapApplied ); - } + if (!meta.SwapAppliedIsDefault) + convertedManips.Add(meta.SwapApplied); break; } } - manager.OptionEditor.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); - manager.OptionEditor.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); - manager.OptionEditor.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); + manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles); + manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps); + manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips); return true; } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not write FileSwapContainer to {mod.ModPath}:\n{e}" ); + Penumbra.Log.Error($"Could not write FileSwapContainer to {mod.ModPath}:\n{e}"); return false; } } - public void LoadMod( Mod? mod, ModSettings? settings ) + public void LoadMod(Mod? mod, ModSettings? settings) { Clear(); - if( mod == null ) + if (mod == null) { - _modRedirections = new Dictionary< Utf8GamePath, FullPath >(); - _modManipulations = new HashSet< MetaManipulation >(); + _modRedirections = new Dictionary(); + _modManipulations = new HashSet(); } else { - ( _modRedirections, _modManipulations ) = ModSettings.GetResolveData( mod, settings ); + (_modRedirections, _modManipulations) = ModSettings.GetResolveData(mod, settings); } } @@ -113,59 +111,61 @@ public class ItemSwapContainer { _manager = manager; _identifier = identifier; - LoadMod( null, null ); + LoadMod(null, null); } - private Func< Utf8GamePath, FullPath > PathResolver( ModCollection? collection ) + private Func PathResolver(ModCollection? collection) => collection != null - ? p => collection.ResolvePath( p ) ?? new FullPath( p ) - : p => ModRedirections.TryGetValue( p, out var path ) ? path : new FullPath( p ); + ? p => collection.ResolvePath(p) ?? new FullPath(p) + : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); - private Func< MetaManipulation, MetaManipulation > MetaResolver( ModCollection? collection ) + private Func MetaResolver(ModCollection? collection) { var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _modManipulations; - return m => set.TryGetValue( m, out var a ) ? a : m; + return m => set.TryGetValue(m, out var a) ? a : m; } - public EquipItem[] LoadEquipment( EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) + public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, + bool useLeftRing = true) { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); + var ret = EquipmentSwap.CreateItemSwap(_manager, _identifier.AwaitedService, Swaps, PathResolver(collection), MetaResolver(collection), + from, to, useRightRing, useLeftRing); Loaded = true; return ret; } - public EquipItem[] LoadTypeSwap( EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null ) + public EquipItem[] LoadTypeSwap(EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null) { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); + var ret = EquipmentSwap.CreateTypeSwap(_manager, _identifier.AwaitedService, Swaps, PathResolver(collection), MetaResolver(collection), + slotFrom, from, slotTo, to); Loaded = true; return ret; } - public bool LoadCustomization( MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) + public bool LoadCustomization(MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, + ModCollection? collection = null) { - var pathResolver = PathResolver( collection ); - var mdl = CustomizationSwap.CreateMdl( manager, pathResolver, slot, race, from, to ); + var pathResolver = PathResolver(collection); + var mdl = CustomizationSwap.CreateMdl(manager, pathResolver, slot, race, from, to); var type = slot switch { BodySlot.Hair => EstManipulation.EstType.Hair, BodySlot.Face => EstManipulation.EstType.Face, - _ => ( EstManipulation.EstType )0, + _ => (EstManipulation.EstType)0, }; - var metaResolver = MetaResolver( collection ); - var est = ItemSwap.CreateEst( manager, pathResolver, metaResolver, type, race, from, to, true ); + var metaResolver = MetaResolver(collection); + var est = ItemSwap.CreateEst(manager, pathResolver, metaResolver, type, race, from, to, true); - Swaps.Add( mdl ); - if( est != null ) - { - Swaps.Add( est ); - } + Swaps.Add(mdl); + if (est != null) + Swaps.Add(est); Loaded = true; return true; } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 7d79d3d4..676018be 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -89,4 +89,4 @@ public class ModExportManager : IDisposable new ModBackup(this, mod).Move(null, newDirectory.Name); mod.ModPath = newDirectory; } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index f7006c4c..013d0e40 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -2,11 +2,9 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using OtterGui.Filesystem; using Penumbra.Communication; -using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 6c49ccf8..cc91dfa6 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Dalamud.Interface.Internal.Notifications; using Penumbra.Import; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index f8d293a2..e258f996 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -16,7 +15,7 @@ public enum NewDirectoryState Identical, Empty, } - + /// Describes the state of a changed mod event. public enum ModPathChangeType { @@ -25,7 +24,7 @@ public enum ModPathChangeType Moved, Reloaded, StartingReload, -} +} public sealed class ModManager : ModStorage, IDisposable { @@ -46,8 +45,8 @@ public sealed class ModManager : ModStorage, IDisposable _communicator = communicator; DataEditor = dataEditor; OptionEditor = optionEditor; - Creator = creator; - SetBaseDirectory(config.ModDirectory, true); + Creator = creator; + SetBaseDirectory(config.ModDirectory, true); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); DiscoverMods(); } @@ -242,7 +241,7 @@ public sealed class ModManager : ModStorage, IDisposable { switch (type) { - case ModPathChangeType.Added: + case ModPathChangeType.Added: SetNew(mod); break; case ModPathChangeType.Deleted: diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 3cfab943..ddd88a72 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -6,7 +6,6 @@ using Penumbra.Api.Enums; using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods.Manager; @@ -19,7 +18,9 @@ public static partial class ModMigration private static partial Regex GroupStartRegex(); public static bool Migrate(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion) - => MigrateV0ToV1(creator, saveService, mod, json, ref fileVersion) || MigrateV1ToV2(saveService, mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); + => MigrateV0ToV1(creator, saveService, mod, json, ref fileVersion) + || MigrateV1ToV2(saveService, mod, ref fileVersion) + || MigrateV2ToV3(mod, ref fileVersion); private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) { @@ -63,8 +64,8 @@ public static partial class ModMigration var swaps = json["FileSwaps"]?.ToObject>() ?? new Dictionary(); - var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); - var priority = 1; + var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); + var priority = 1; var seenMetaFiles = new HashSet(); foreach (var group in groups.Values) ConvertGroup(creator, mod, group, ref priority, seenMetaFiles); @@ -128,8 +129,8 @@ public static partial class ModMigration var optionPriority = 0; var newMultiGroup = new MultiModGroup() { - Name = group.GroupName, - Priority = priority++, + Name = group.GroupName, + Priority = priority++, Description = string.Empty, }; mod.Groups.Add(newMultiGroup); @@ -146,8 +147,8 @@ public static partial class ModMigration var newSingleGroup = new SingleModGroup() { - Name = group.GroupName, - Priority = priority++, + Name = group.GroupName, + Priority = priority++, Description = string.Empty, }; mod.Groups.Add(newSingleGroup); diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 4ce6da9a..41cc023e 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -5,7 +5,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public sealed partial class Mod : IMod +public sealed class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index b5462eee..89d35cd2 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -113,9 +113,7 @@ public partial class ModCreator } if (changes) - { _saveService.SaveAllOptionGroups(mod, true); - } } /// Load the default option for a given mod. diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index 56ee827b..51fe8d58 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 49094aa0..d29cdb9c 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index ac92cd13..8c296f20 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -1,58 +1,57 @@ using Newtonsoft.Json; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Subclasses; public interface ISubMod { - public string Name { get; } - public string FullName { get; } + public string Name { get; } + public string FullName { get; } public string Description { get; } - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } - public IReadOnlySet< MetaManipulation > Manipulations { get; } + public IReadOnlyDictionary Files { get; } + public IReadOnlyDictionary FileSwaps { get; } + public IReadOnlySet Manipulations { get; } public bool IsDefault { get; } - public static void WriteSubMod( JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority ) + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority) { j.WriteStartObject(); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( mod.Name ); - j.WritePropertyName( nameof(Description) ); - j.WriteValue( mod.Description ); - if( priority != null ) + j.WritePropertyName(nameof(Name)); + j.WriteValue(mod.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(mod.Description); + if (priority != null) { - j.WritePropertyName( nameof( IModGroup.Priority ) ); - j.WriteValue( priority.Value ); + j.WritePropertyName(nameof(IModGroup.Priority)); + j.WriteValue(priority.Value); } - j.WritePropertyName( nameof( mod.Files ) ); + j.WritePropertyName(nameof(mod.Files)); j.WriteStartObject(); - foreach( var (gamePath, file) in mod.Files ) + foreach (var (gamePath, file) in mod.Files) { - if( file.ToRelPath( basePath, out var relPath ) ) + if (file.ToRelPath(basePath, out var relPath)) { - j.WritePropertyName( gamePath.ToString() ); - j.WriteValue( relPath.ToString() ); + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); } } j.WriteEndObject(); - j.WritePropertyName( nameof( mod.FileSwaps ) ); + j.WritePropertyName(nameof(mod.FileSwaps)); j.WriteStartObject(); - foreach( var (gamePath, file) in mod.FileSwaps ) + foreach (var (gamePath, file) in mod.FileSwaps) { - j.WritePropertyName( gamePath.ToString() ); - j.WriteValue( file.ToString() ); + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); } j.WriteEndObject(); - j.WritePropertyName( nameof( mod.Manipulations ) ); - serializer.Serialize( j, mod.Manipulations ); + j.WritePropertyName(nameof(mod.Manipulations)); + serializer.Serialize(j, mod.Manipulations); j.WriteEndObject(); } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 537c989f..122c6d29 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -3,10 +3,9 @@ using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Subclasses; /// Contains the settings for a given mod. public class ModSettings @@ -267,4 +266,4 @@ public class ModSettings return ( Enabled, Priority, dict ); } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index d40a79be..4d29c58d 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -4,10 +4,9 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Mods.Subclasses; -namespace Penumbra.Mods; - +namespace Penumbra.Mods.Subclasses; + /// Groups that allow all available options to be selected at once. public sealed class MultiModGroup : IModGroup { diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index a69238f5..1184b6ed 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -3,9 +3,8 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Mods.Subclasses; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. public sealed class SingleModGroup : IModGroup @@ -18,59 +17,55 @@ public sealed class SingleModGroup : IModGroup public int Priority { get; set; } public uint DefaultSettings { get; set; } - public readonly List< SubMod > OptionData = new(); + public readonly List OptionData = new(); - public int OptionPriority( Index _ ) + public int OptionPriority(Index _) => Priority; - public ISubMod this[ Index idx ] - => OptionData[ idx ]; + public ISubMod this[Index idx] + => OptionData[idx]; [JsonIgnore] public int Count => OptionData.Count; - public IEnumerator< ISubMod > GetEnumerator() + public IEnumerator GetEnumerator() => OptionData.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) + public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) { - var options = json[ "Options" ]; + var options = json["Options"]; var ret = new SingleModGroup { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, - DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0u, }; - if( ret.Name.Length == 0 ) - { + if (ret.Name.Length == 0) return null; - } - if( options != null ) - { - foreach( var child in options.Children() ) + if (options != null) + foreach (var child in options.Children()) { - var subMod = new SubMod( mod ); - subMod.SetPosition( groupIdx, ret.OptionData.Count ); - subMod.Load( mod.ModPath, child, out _ ); - ret.OptionData.Add( subMod ); + var subMod = new SubMod(mod); + subMod.SetPosition(groupIdx, ret.OptionData.Count); + subMod.Load(mod.ModPath, child, out _); + ret.OptionData.Add(subMod); } - } - if( ( int )ret.DefaultSettings >= ret.Count ) + if ((int)ret.DefaultSettings >= ret.Count) ret.DefaultSettings = 0; return ret; } - public IModGroup Convert( GroupType type ) + public IModGroup Convert(GroupType type) { - switch( type ) + switch (type) { case GroupType.Single: return this; case GroupType.Multi: @@ -79,47 +74,41 @@ public sealed class SingleModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = 1u << ( int )DefaultSettings, + DefaultSettings = 1u << (int)DefaultSettings, }; - multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); + multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, i))); return multi; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } - public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) - { + if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; - } // Update default settings with the move. - if( DefaultSettings == optionIdxFrom ) + if (DefaultSettings == optionIdxFrom) { - DefaultSettings = ( uint )optionIdxTo; + DefaultSettings = (uint)optionIdxTo; } - else if( optionIdxFrom < optionIdxTo ) + else if (optionIdxFrom < optionIdxTo) { - if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) - { + if (DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo) --DefaultSettings; - } } - else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) + else if (DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo) { ++DefaultSettings; } - UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions( int from = 0 ) + public void UpdatePositions(int from = 0) { - foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) - { - o.SetPosition( o.GroupIdx, i ); - } + foreach (var (o, i) in OptionData.WithIndex().Skip(from)) + o.SetPosition(o.GroupIdx, i); } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs similarity index 97% rename from Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs rename to Penumbra/Mods/Subclasses/SubMod.cs index 7c43de86..542b14d2 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,11 +1,8 @@ using Newtonsoft.Json.Linq; -using Penumbra.Import; -using Penumbra.Meta; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Subclasses; /// /// A sub mod is a collection of diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index fcd4a5f7..73273707 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -5,7 +5,6 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods; @@ -21,80 +20,84 @@ public class TemporaryMod : IMod public readonly SubMod Default; ISubMod IMod.Default - => Default; + => Default; - public IReadOnlyList< IModGroup > Groups - => Array.Empty< IModGroup >(); + public IReadOnlyList Groups + => Array.Empty(); - public IEnumerable< SubMod > AllSubMods - => new[] { Default }; + public IEnumerable AllSubMods + => new[] + { + Default, + }; public TemporaryMod() - => Default = new SubMod( this ); + => Default = new SubMod(this); - public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) - => Default.FileData[ gamePath ] = fullPath; + public void SetFile(Utf8GamePath gamePath, FullPath fullPath) + => Default.FileData[gamePath] = fullPath; - public bool SetManipulation( MetaManipulation manip ) - => Default.ManipulationData.Remove( manip ) | Default.ManipulationData.Add( manip ); + public bool SetManipulation(MetaManipulation manip) + => Default.ManipulationData.Remove(manip) | Default.ManipulationData.Add(manip); - public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) + public void SetAll(Dictionary dict, HashSet manips) { Default.FileData = dict; Default.ManipulationData = manips; } - public static void SaveTempCollection( Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, string? character = null ) + public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, + string? character = null) { DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder( modManager.BasePath, collection.Name ); - var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); - modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); - var mod = new Mod( dir ); + dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name); + var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); + modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, + $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null); + var mod = new Mod(dir); var defaultMod = mod.Default; - foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) + foreach (var (gamePath, fullPath) in collection.ResolvedFiles) { - if( gamePath.Path.EndsWith( ".imc"u8 ) ) + if (gamePath.Path.EndsWith(".imc"u8)) { continue; } var targetPath = fullPath.Path.FullName; - if( fullPath.Path.Name.StartsWith( '|' ) ) + if (fullPath.Path.Name.StartsWith('|')) { - targetPath = targetPath.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries ).Last(); + targetPath = targetPath.Split('|', 3, StringSplitOptions.RemoveEmptyEntries).Last(); } - if( Path.IsPathRooted(targetPath) ) + if (Path.IsPathRooted(targetPath)) { - var target = Path.Combine( fileDir.FullName, Path.GetFileName(targetPath) ); - File.Copy( targetPath, target, true ); - defaultMod.FileData[ gamePath ] = new FullPath( target ); + var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath)); + File.Copy(targetPath, target, true); + defaultMod.FileData[gamePath] = new FullPath(target); } else { - defaultMod.FileSwapData[ gamePath ] = new FullPath(targetPath); + defaultMod.FileSwapData[gamePath] = new FullPath(targetPath); } } - foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) - defaultMod.ManipulationData.Add( manip ); + foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) + defaultMod.ManipulationData.Add(manip); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod)); - modManager.AddMod( dir ); - Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); + modManager.AddMod(dir); + Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}."); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); - if( dir != null && Directory.Exists( dir.FullName ) ) + Penumbra.Log.Error($"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}"); + if (dir != null && Directory.Exists(dir.FullName)) { try { - Directory.Delete( dir.FullName, true ); + Directory.Delete(dir.FullName, true); } catch { @@ -103,4 +106,4 @@ public class TemporaryMod : IMod } } } -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 1402ef89..019355af 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -20,6 +20,7 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; +using Penumbra.UI; namespace Penumbra; diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 89f1063e..d896e526 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -7,6 +7,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.UI.Classes; namespace Penumbra.Services; @@ -331,7 +332,8 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); + var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, + Array.Empty()); _saveService.ImmediateSave(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 6754d0bd..0cd4c97a 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -6,7 +6,6 @@ using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.Plugin; -using System.Reflection; using Dalamud.Interface.DragDrop; using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 07c7394c..b76c39ef 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -21,6 +21,7 @@ using Penumbra.UI; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; +using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; namespace Penumbra.Services; diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 1c6b3ef1..30e22a7a 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,17 +16,18 @@ public class StainService : IDisposable { } } - public readonly StainData StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; + public readonly StainData StainData; + public readonly FilterComboColors StainCombo; + public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, IDataManager dataManager) { using var t = timer.Measure(StartTimeType.Stains); StainData = new StainData(pluginInterface, dataManager, dataManager.Language); - StainCombo = new FilterComboColors(140, StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); - StmFile = new StmFile(dataManager); + StainCombo = new FilterComboColors(140, + StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); } @@ -36,4 +37,4 @@ public class StainService : IDisposable StainData.Dispose(); Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed."); } -} \ No newline at end of file +} diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index e97a9a82..b8d9b30a 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; @@ -25,20 +24,21 @@ public class ValidityChecker DevPenumbraExists = CheckDevPluginPenumbra(pi); IsNotInstalledPenumbra = CheckIsNotInstalled(pi); IsValidSourceRepo = CheckSourceRepo(pi); - + var assembly = Assembly.GetExecutingAssembly(); - Version = assembly.GetName().Version?.ToString() ?? string.Empty; + Version = assembly.GetName().Version?.ToString() ?? string.Empty; CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; } public void LogExceptions() { - if( ImcExceptions.Count > 0 ) - Penumbra.Chat.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); + if (ImcExceptions.Count > 0) + Penumbra.Chat.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", + "Warning", NotificationType.Warning); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. - private static bool CheckDevPluginPenumbra( DalamudPluginInterface pi ) + private static bool CheckDevPluginPenumbra(DalamudPluginInterface pi) { #if !DEBUG var path = Path.Combine( pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); @@ -59,7 +59,7 @@ public class ValidityChecker } // Check if the loaded version of Penumbra itself is in devPlugins. - private static bool CheckIsNotInstalled( DalamudPluginInterface pi ) + private static bool CheckIsNotInstalled(DalamudPluginInterface pi) { #if !DEBUG var checkedDirectory = pi.AssemblyLocation.Directory?.Parent?.Parent?.Name; @@ -76,7 +76,7 @@ public class ValidityChecker } // Check if the loaded version of Penumbra is installed from a valid source repo. - private static bool CheckSourceRepo( DalamudPluginInterface pi ) + private static bool CheckSourceRepo(DalamudPluginInterface pi) { #if !DEBUG return pi.SourceRepository?.Trim().ToLowerInvariant() switch @@ -90,4 +90,4 @@ public class ValidityChecker return true; #endif } -} \ No newline at end of file +} diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index d9bef172..69dbbeab 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -12,7 +12,8 @@ namespace Penumbra.Services; public sealed class IdentifierService : AsyncServiceWrapper { public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, IDataManager data, ItemService items) - : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService)) + : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, + () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService)) { } } @@ -30,4 +31,4 @@ public sealed class ActorService : AsyncServiceWrapper : base(nameof(ActorService), tracker, StartTimeType.Actors, () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) { } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index d14c7125..4a193591 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -289,14 +290,14 @@ public partial class ModEditWindow if (_selectedFiles.Count == 0) tt += "\n\nNo files selected."; else if (!active) - tt += $"\n\nHold {_config.DeleteModModifier} to delete."; - + tt += $"\n\nHold {_config.DeleteModModifier} to delete."; + if (ImGuiUtil.DrawDisabledButton("Delete Selected Files", Vector2.Zero, tt, _selectedFiles.Count == 0 || !active)) _editor.FileEditor.DeleteFiles(_editor.Mod!, _editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains)); ImGui.SameLine(); var changes = _editor.FileEditor.Changes; - tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; + tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 4f04150f..cccc43ee 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -32,7 +32,7 @@ public partial class ModEditWindow ImGui.SameLine(); ret |= ColorTableDyeableCheckbox(tab); } - + var hasDyeTable = tab.Mtrl.HasDyeTable; if (hasDyeTable) { @@ -114,10 +114,8 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - { for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); - } tab.UpdateColorTablePreview(); @@ -195,7 +193,7 @@ public partial class ModEditWindow } private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { + { var dyeable = tab.Mtrl.HasDyeTable; var ret = ImGui.Checkbox("Dyeable", ref dyeable); @@ -259,9 +257,9 @@ public partial class ModEditWindow } using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; + ref var row = ref tab.Mtrl.Table[rowIdx]; var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; + ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); @@ -301,7 +299,8 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.SpecularStrength)) { row.SpecularStrength = tmpFloat; ret = true; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 187d86c7..ad83843d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -475,9 +475,9 @@ public partial class ModEditWindow } } - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) + UpdateMaterialPreview(); + + if (!Mtrl.HasTable) return; foreach (var materialInfo in instances) @@ -591,9 +591,9 @@ public partial class ModEditWindow public void UpdateColorTableRowPreview(int rowIdx) { if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) + return; + + if (!Mtrl.HasTable) return; var row = Mtrl.Table[rowIdx]; @@ -619,16 +619,16 @@ public partial class ModEditWindow public void UpdateColorTablePreview() { if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) + return; + + if (!Mtrl.HasTable) return; var rows = Mtrl.Table; if (Mtrl.HasDyeTable) { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { ref var row = ref rows[i]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index a6a93a36..821f4454 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -8,7 +8,6 @@ using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; @@ -66,20 +65,26 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew , _editor.MetaEditor.OtherEqpCount); - DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, _editor.MetaEditor.OtherEqdpCount); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew , _editor.MetaEditor.OtherImcCount); - DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew , _editor.MetaEditor.OtherEstCount); - DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew , _editor.MetaEditor.OtherGmpCount); - DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew , _editor.MetaEditor.OtherRspCount); + DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, + _editor.MetaEditor.OtherEqpCount); + DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, + _editor.MetaEditor.OtherEqdpCount); + DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, _editor.MetaEditor.OtherImcCount); + DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, + _editor.MetaEditor.OtherEstCount); + DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, + _editor.MetaEditor.OtherGmpCount); + DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, + _editor.MetaEditor.OtherRspCount); } - /// The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, Action draw, + /// The headers for the different meta changes all have basically the same structure for different types. + private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, + Action draw, Action drawNew, int otherCount) - { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; var oldPos = ImGui.GetCursorPosY(); var header = ImGui.CollapsingHeader($"{items.Count} {label}"); @@ -88,10 +93,11 @@ public partial class ModEditWindow { var text = $"{otherCount} Edits in other Options"; var size = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); - ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); ImGui.SetCursorPos(newPos); - } + } + if (!header) return; @@ -223,7 +229,8 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), + _new.Slot.IsAccessory(), setId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); } @@ -232,7 +239,8 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Race("##eqdpRace", _new.Race, out var race)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), + _new.Slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); } @@ -241,7 +249,8 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), + _new.Slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); } @@ -250,7 +259,8 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId); + var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), + slot.IsAccessory(), _new.SetId); _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); } @@ -288,7 +298,8 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(EquipSlotTooltip); // Values - var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); + var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), + meta.SetId); var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); ImGui.TableNextColumn(); @@ -311,7 +322,7 @@ public partial class ModEditWindow private static float SmallIdWidth => 45 * UiHelpers.Scale; - /// Convert throwing to null-return if the file does not exist. + /// Convert throwing to null-return if the file does not exist. private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc) { try @@ -370,7 +381,8 @@ public partial class ModEditWindow if (_new.ObjectType is ObjectType.Equipment) { if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, + _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -379,7 +391,8 @@ public partial class ModEditWindow else if (_new.ObjectType is ObjectType.Accessory) { if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, + _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -388,7 +401,8 @@ public partial class ModEditWindow else { if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId.Id, out var setId2, 0, ushort.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant.Id, _new.EquipSlot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant.Id, _new.EquipSlot, + _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -405,7 +419,8 @@ public partial class ModEditWindow if (_new.ObjectType is ObjectType.DemiHuman) { if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, _new.Entry) + _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, + _new.Entry) .Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); @@ -787,7 +802,8 @@ public partial class ModEditWindow using var color = ImRaii.PushColor(ImGuiCol.FrameBg, def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) && value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) + if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) + && value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) editor.MetaEditor.Change(meta.Copy(value)); ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index c90eb2b4..5d74dc33 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -9,125 +9,108 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor< MdlFile > _modelTab; + private readonly FileEditor _modelTab; - private static bool DrawModelPanel( MdlFile file, bool disabled ) + private static bool DrawModelPanel(MdlFile file, bool disabled) { var ret = false; - for( var i = 0; i < file.Materials.Length; ++i ) + for (var i = 0; i < file.Materials.Length; ++i) { - using var id = ImRaii.PushId( i ); - var tmp = file.Materials[ i ]; - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != file.Materials[ i ] ) + using var id = ImRaii.PushId(i); + var tmp = file.Materials[i]; + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != file.Materials[i]) { - file.Materials[ i ] = tmp; - ret = true; + file.Materials[i] = tmp; + ret = true; } } - ret |= DrawOtherModelDetails( file, disabled ); + ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } - private static bool DrawOtherModelDetails( MdlFile file, bool _ ) + private static bool DrawOtherModelDetails(MdlFile file, bool _) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return false; - } - using( var table = ImRaii.Table( "##data", 2, ImGuiTableFlags.SizingFixedFit ) ) + using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) { - if( table ) + if (table) { - ImGuiUtil.DrawTableColumn( "Version" ); - ImGuiUtil.DrawTableColumn( file.Version.ToString() ); - ImGuiUtil.DrawTableColumn( "Radius" ); - ImGuiUtil.DrawTableColumn( file.Radius.ToString( CultureInfo.InvariantCulture ) ); - ImGuiUtil.DrawTableColumn( "Model Clip Out Distance" ); - ImGuiUtil.DrawTableColumn( file.ModelClipOutDistance.ToString( CultureInfo.InvariantCulture ) ); - ImGuiUtil.DrawTableColumn( "Shadow Clip Out Distance" ); - ImGuiUtil.DrawTableColumn( file.ShadowClipOutDistance.ToString( CultureInfo.InvariantCulture ) ); - ImGuiUtil.DrawTableColumn( "LOD Count" ); - ImGuiUtil.DrawTableColumn( file.LodCount.ToString() ); - ImGuiUtil.DrawTableColumn( "Enable Index Buffer Streaming" ); - ImGuiUtil.DrawTableColumn( file.EnableIndexBufferStreaming.ToString() ); - ImGuiUtil.DrawTableColumn( "Enable Edge Geometry" ); - ImGuiUtil.DrawTableColumn( file.EnableEdgeGeometry.ToString() ); - ImGuiUtil.DrawTableColumn( "Flags 1" ); - ImGuiUtil.DrawTableColumn( file.Flags1.ToString() ); - ImGuiUtil.DrawTableColumn( "Flags 2" ); - ImGuiUtil.DrawTableColumn( file.Flags2.ToString() ); - ImGuiUtil.DrawTableColumn( "Vertex Declarations" ); - ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Bone Bounding Boxes" ); - ImGuiUtil.DrawTableColumn( file.BoneBoundingBoxes.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Bone Tables" ); - ImGuiUtil.DrawTableColumn( file.BoneTables.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Element IDs" ); - ImGuiUtil.DrawTableColumn( file.ElementIds.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Extra LoDs" ); - ImGuiUtil.DrawTableColumn( file.ExtraLods.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Meshes" ); - ImGuiUtil.DrawTableColumn( file.Meshes.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Shape Meshes" ); - ImGuiUtil.DrawTableColumn( file.ShapeMeshes.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "LoDs" ); - ImGuiUtil.DrawTableColumn( file.Lods.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Vertex Declarations" ); - ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() ); - ImGuiUtil.DrawTableColumn( "Stack Size" ); - ImGuiUtil.DrawTableColumn( file.StackSize.ToString() ); + ImGuiUtil.DrawTableColumn("Version"); + ImGuiUtil.DrawTableColumn(file.Version.ToString()); + ImGuiUtil.DrawTableColumn("Radius"); + ImGuiUtil.DrawTableColumn(file.Radius.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); + ImGuiUtil.DrawTableColumn(file.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn("Shadow Clip Out Distance"); + ImGuiUtil.DrawTableColumn(file.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn("LOD Count"); + ImGuiUtil.DrawTableColumn(file.LodCount.ToString()); + ImGuiUtil.DrawTableColumn("Enable Index Buffer Streaming"); + ImGuiUtil.DrawTableColumn(file.EnableIndexBufferStreaming.ToString()); + ImGuiUtil.DrawTableColumn("Enable Edge Geometry"); + ImGuiUtil.DrawTableColumn(file.EnableEdgeGeometry.ToString()); + ImGuiUtil.DrawTableColumn("Flags 1"); + ImGuiUtil.DrawTableColumn(file.Flags1.ToString()); + ImGuiUtil.DrawTableColumn("Flags 2"); + ImGuiUtil.DrawTableColumn(file.Flags2.ToString()); + ImGuiUtil.DrawTableColumn("Vertex Declarations"); + ImGuiUtil.DrawTableColumn(file.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn("Bone Bounding Boxes"); + ImGuiUtil.DrawTableColumn(file.BoneBoundingBoxes.Length.ToString()); + ImGuiUtil.DrawTableColumn("Bone Tables"); + ImGuiUtil.DrawTableColumn(file.BoneTables.Length.ToString()); + ImGuiUtil.DrawTableColumn("Element IDs"); + ImGuiUtil.DrawTableColumn(file.ElementIds.Length.ToString()); + ImGuiUtil.DrawTableColumn("Extra LoDs"); + ImGuiUtil.DrawTableColumn(file.ExtraLods.Length.ToString()); + ImGuiUtil.DrawTableColumn("Meshes"); + ImGuiUtil.DrawTableColumn(file.Meshes.Length.ToString()); + ImGuiUtil.DrawTableColumn("Shape Meshes"); + ImGuiUtil.DrawTableColumn(file.ShapeMeshes.Length.ToString()); + ImGuiUtil.DrawTableColumn("LoDs"); + ImGuiUtil.DrawTableColumn(file.Lods.Length.ToString()); + ImGuiUtil.DrawTableColumn("Vertex Declarations"); + ImGuiUtil.DrawTableColumn(file.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn("Stack Size"); + ImGuiUtil.DrawTableColumn(file.StackSize.ToString()); } } - using( var attributes = ImRaii.TreeNode( "Attributes", ImGuiTreeNodeFlags.DefaultOpen ) ) + using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { - if( attributes ) - { - foreach( var attribute in file.Attributes ) - { - ImRaii.TreeNode( attribute, ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } + if (attributes) + foreach (var attribute in file.Attributes) + ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); } - using( var bones = ImRaii.TreeNode( "Bones", ImGuiTreeNodeFlags.DefaultOpen ) ) + using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { - if( bones ) - { - foreach( var bone in file.Bones ) - { - ImRaii.TreeNode( bone, ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } + if (bones) + foreach (var bone in file.Bones) + ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); } - using( var shapes = ImRaii.TreeNode( "Shapes", ImGuiTreeNodeFlags.DefaultOpen ) ) + using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { - if( shapes ) - { - foreach( var shape in file.Shapes ) - { - ImRaii.TreeNode( shape.ShapeName, ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } + if (shapes) + foreach (var shape in file.Shapes) + ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); } - if( file.RemainingData.Length > 0 ) + if (file.RemainingData.Length > 0) { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.RemainingData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.RemainingData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {file.RemainingData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', file.RemainingData.Select(c => $"{c:X2}"))); } return false; } - -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 43a1990e..2f64f82a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -7,6 +7,7 @@ using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index fb2b5128..e475f47f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -489,16 +489,22 @@ public partial class ModEditWindow continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) + { ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) + { ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) + { ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); @@ -530,8 +536,10 @@ public partial class ModEditWindow { using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var selector in tab.Shpk.NodeSelectors) + { ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); + } } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 58e2e0c1..7f14165c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -34,7 +34,7 @@ public partial class ModEditWindow Shpk = new ShpkFile(bytes, false); } - Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 20aea21e..1ee0a128 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -193,17 +193,18 @@ public partial class ModEditWindow private void OpenSaveAsDialog(string defaultExtension) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, (a, b) => - { - if (a) + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, + (a, b) => { - _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - if (b == _left.Path) - AddReloadTask(_left.Path, false); - else if (b == _right.Path) - AddReloadTask(_right.Path, true); - } - }, _mod!.ModPath.FullName, _forceTextureStartPath); + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 703329e6..745b412b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -6,7 +6,6 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; -using OtterGui.Compression; using OtterGui.Raii; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -19,6 +18,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 75f75de0..60247d81 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -2,9 +2,9 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index f435cb98..5f09e584 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -10,20 +10,20 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; + private readonly ResourceTreeFactory _treeFactory; private readonly ChangedItemDrawer _changedItemDrawer; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; private readonly HashSet _unfolded; - private Task? _task; + private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, int actionCapacity, Action onRefresh, Action drawActions) { _config = config; - _treeFactory = treeFactory; + _treeFactory = treeFactory; _changedItemDrawer = changedItemDrawer; _actionCapacity = actionCapacity; _onRefresh = onRefresh; @@ -87,7 +87,7 @@ public class ResourceTreeViewer private Task RefreshCharacterList() => Task.Run(() => - { + { try { return _treeFactory.FromObjectTable(); @@ -107,38 +107,39 @@ public class ResourceTreeViewer foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { if (resourceNode.Internal && !debugMode) - continue; - - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity - - using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); - + continue; + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity + + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) - { - var unfoldable = debugMode - ? resourceNode.Children.Count > 0 - : resourceNode.Children.Any(child => !child.Internal); - if (unfoldable) - { - using var font = ImRaii.PushFont(UiBuilder.IconFont); - var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); - var offset = (ImGui.GetFrameHeight() - ImGui.CalcTextSize(icon).X) / 2; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); - ImGui.TextUnformatted(icon); - ImGui.SameLine(0f, offset + ImGui.GetStyle().ItemInnerSpacing.X); - } - else - { - ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); - ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); - } - _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + { + var unfoldable = debugMode + ? resourceNode.Children.Count > 0 + : resourceNode.Children.Any(child => !child.Internal); + if (unfoldable) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); + var offset = (ImGui.GetFrameHeight() - ImGui.CalcTextSize(icon).X) / 2; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + ImGui.TextUnformatted(icon); + ImGui.SameLine(0f, offset + ImGui.GetStyle().ItemInnerSpacing.X); + } + else + { + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + } + + _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) @@ -150,11 +151,11 @@ public class ResourceTreeViewer unfolded = !unfolded; } - if (debugMode) - { + if (debugMode) + { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X16}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X16}"); } } @@ -187,8 +188,8 @@ public class ResourceTreeViewer ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); - } - + } + mutedColor.Dispose(); if (_actionCapacity > 0) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index d7845ab7..ed62bf83 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -4,7 +4,7 @@ namespace Penumbra.UI; public class PenumbraChangelog { - public const int LastChangelogVersion = 0; + public const int LastChangelogVersion = 0; private readonly Configuration _config; public readonly Changelog Changelog; @@ -41,19 +41,23 @@ public class PenumbraChangelog Add7_1_2(Changelog); Add7_2_0(Changelog); Add7_3_0(Changelog); - } - + } + #region Changelogs private static void Add7_3_0(Changelog log) => log.NextVersion("Version 0.7.3.0") - .RegisterEntry("Added the ability to drag and drop mod files from external sources (like a file explorer or browser) into Penumbras mod selector to import them.") + .RegisterEntry( + "Added the ability to drag and drop mod files from external sources (like a file explorer or browser) into Penumbras mod selector to import them.") .RegisterEntry("You can also drag and drop texture files into the textures tab of the Advanced Editing Window.", 1) - .RegisterEntry("Added a priority display to the mod selector using the currently selected collections priorities. This can be hidden in settings.") + .RegisterEntry( + "Added a priority display to the mod selector using the currently selected collections priorities. This can be hidden in settings.") .RegisterEntry("Added IPC for texture conversion, improved texture handling backend and threading.") - .RegisterEntry("Added Dalamud Substitution so that other plugins can more easily use replaced icons from Penumbras Interface collection when using Dalamuds new Texture Provider.") + .RegisterEntry( + "Added Dalamud Substitution so that other plugins can more easily use replaced icons from Penumbras Interface collection when using Dalamuds new Texture Provider.") .RegisterEntry("Added a filter to texture selection combos in the textures tab of the Advanced Editing Window.") - .RegisterEntry("Changed behaviour when failing to load group JSON files for mods - the pre-existing but failing files are now backed up before being deleted or overwritten.") + .RegisterEntry( + "Changed behaviour when failing to load group JSON files for mods - the pre-existing but failing files are now backed up before being deleted or overwritten.") .RegisterEntry("Further backend changes, mostly relating to the Glamourer rework.") .RegisterEntry("Fixed an issue with modded decals not loading correctly when used with the Glamourer rework.") .RegisterEntry("Fixed missing scaling with UI Scale for some combos.") @@ -66,49 +70,67 @@ public class PenumbraChangelog private static void Add7_2_0(Changelog log) => log.NextVersion("Version 0.7.2.0") - .RegisterEntry("Added Changed Item Categories and icons that can filter for specific types of Changed Items, in the Changed Items Tab as well as in the Changed Items panel for specific mods..") - .RegisterEntry("Icons at the top can be clicked to filter, as well as right-clicked to open a context menu with the option to inverse-filter for them", 1) + .RegisterEntry( + "Added Changed Item Categories and icons that can filter for specific types of Changed Items, in the Changed Items Tab as well as in the Changed Items panel for specific mods..") + .RegisterEntry( + "Icons at the top can be clicked to filter, as well as right-clicked to open a context menu with the option to inverse-filter for them", + 1) .RegisterEntry("There is also an ALL button that can be toggled.", 1) - .RegisterEntry("Modded files in the Font category now resolve from the Interface assignment instead of the base assignment, despite not technically being in the UI category.") - .RegisterEntry("Timeline files will no longer be associated with specific characters in cutscenes, since there is no way to correctly do this, and it could cause crashes if IVCS-requiring animations were used on characters without IVCS.") + .RegisterEntry( + "Modded files in the Font category now resolve from the Interface assignment instead of the base assignment, despite not technically being in the UI category.") + .RegisterEntry( + "Timeline files will no longer be associated with specific characters in cutscenes, since there is no way to correctly do this, and it could cause crashes if IVCS-requiring animations were used on characters without IVCS.") .RegisterEntry("File deletion in the Advanced Editing Window now also checks for your configured deletion key combo.") - .RegisterEntry("The Texture tab in the Advanced Editing Window now has some quick convert buttons to just convert the selected texture to a different format in-place.") - .RegisterEntry("These buttons only appear if only one texture is selected on the left side, it is not otherwise manipulated, and the texture is a .tex file.", 1) + .RegisterEntry( + "The Texture tab in the Advanced Editing Window now has some quick convert buttons to just convert the selected texture to a different format in-place.") + .RegisterEntry( + "These buttons only appear if only one texture is selected on the left side, it is not otherwise manipulated, and the texture is a .tex file.", + 1) .RegisterEntry("The text part of the mod filter in the mod selector now also resets when right-clicking the drop-down arrow.") .RegisterEntry("The Dissolve Folder option in the mod selector context menu has been moved to the bottom.") .RegisterEntry("Somewhat improved IMC handling to prevent some issues.") - .RegisterEntry("Improved the handling of mod renames on mods with default-search names to correctly rename their search-name in (hopefully) all cases too.") + .RegisterEntry( + "Improved the handling of mod renames on mods with default-search names to correctly rename their search-name in (hopefully) all cases too.") .RegisterEntry("A lot of backend improvements and changes related to the pending Glamourer rework.") .RegisterEntry("Fixed an issue where the displayed active collection count in the support info was wrong.") - .RegisterEntry("Fixed an issue with created directories dealing badly with non-standard whitespace characters like half-width or non-breaking spaces.") + .RegisterEntry( + "Fixed an issue with created directories dealing badly with non-standard whitespace characters like half-width or non-breaking spaces.") .RegisterEntry("Fixed an issue with unknown animation and vfx edits not being recognized correctly.") .RegisterEntry("Fixed an issue where changing option descriptions to be empty was not working correctly.") .RegisterEntry("Fixed an issue with texture names in the resource tree of the On-Screen views.") .RegisterEntry("Fixed a bug where the game would crash when drawing folders in the mod selector that contained a '%' symbol.") .RegisterEntry("Fixed an issue with parallel algorithms obtaining the wrong number of available cores.") .RegisterEntry("Updated the available selection of Battle NPC names.") - .RegisterEntry("A typo in the 0.7.1.2 Changlog has been fixed.") - .RegisterEntry("Added the Sea of Stars as accepted repository. (0.7.1.4)") - .RegisterEntry("Fixed an issue with collections sometimes not loading correctly, and IMC files not applying correctly. (0.7.1.3)"); - - + .RegisterEntry("A typo in the 0.7.1.2 Changlog has been fixed.") + .RegisterEntry("Added the Sea of Stars as accepted repository. (0.7.1.4)") + .RegisterEntry("Fixed an issue with collections sometimes not loading correctly, and IMC files not applying correctly. (0.7.1.3)"); + + private static void Add7_1_2(Changelog log) => log.NextVersion("Version 0.7.1.2") - .RegisterEntry("Changed threaded handling of collection caches. Maybe this fixes the startup problems some people are experiencing.") - .RegisterEntry("This is just testing and may not be the solution, or may even make things worse. Sorry if I have to put out multiple small patches again to get this right.", 1) + .RegisterEntry( + "Changed threaded handling of collection caches. Maybe this fixes the startup problems some people are experiencing.") + .RegisterEntry( + "This is just testing and may not be the solution, or may even make things worse. Sorry if I have to put out multiple small patches again to get this right.", + 1) .RegisterEntry("Fixed Penumbra failing to load if the main configuration file is corrupted.") .RegisterEntry("Some miscellaneous small bug fixes.") .RegisterEntry("Slight changes in behaviour for deduplicator/normalizer, mostly backend.") .RegisterEntry("A typo in the 0.7.1.0 Changelog has been fixed.") .RegisterEntry("Fixed left rings not being valid for IMC entries after validation. (7.1.1)") - .RegisterEntry("Relaxed the scaling restrictions for RSP scaling values to go from 0.01 to 512.0 instead of the prior upper limit of 8.0, in interface as well as validation, to better support the fetish community. (7.1.1)"); + .RegisterEntry( + "Relaxed the scaling restrictions for RSP scaling values to go from 0.01 to 512.0 instead of the prior upper limit of 8.0, in interface as well as validation, to better support the fetish community. (7.1.1)"); private static void Add7_1_0(Changelog log) => log.NextVersion("Version 0.7.1.0") .RegisterEntry("Updated for patch 6.4 - there may be some oversights on edge cases, but I could not find any issues myself.") - .RegisterHighlight("This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is detectable by the server.", 1) - .RegisterEntry("Added a Mod Merging tab in the Advanced Editing Window. This can help you merge multiple mods to one, or split off specific options from an existing mod into a new mod.") - .RegisterEntry("Added advanced options to configure the minimum allowed window size for the main window (to reduce it). This is not quite supported and may look bad, so only use it if you really need smaller windows.") + .RegisterHighlight( + "This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is detectable by the server.", + 1) + .RegisterEntry( + "Added a Mod Merging tab in the Advanced Editing Window. This can help you merge multiple mods to one, or split off specific options from an existing mod into a new mod.") + .RegisterEntry( + "Added advanced options to configure the minimum allowed window size for the main window (to reduce it). This is not quite supported and may look bad, so only use it if you really need smaller windows.") .RegisterEntry("The last tab selected in the main window is now saved and re-used when relaunching Penumbra.") .RegisterEntry("Added a hook to correctly associate some sounds that are played while weapons are drawn.") .RegisterEntry("Added a hook to correctly associate sounds that are played while dismounting.") @@ -125,7 +147,8 @@ public class PenumbraChangelog .RegisterEntry("Fixed an issue with the file selectors not always opening at the expected locations. (0.7.0.7)") .RegisterEntry("Fixed some cache handling issues. (0.7.0.5 - 0.7.0.10)") .RegisterEntry("Fixed an issue with multiple collection context menus appearing for some identifiers (0.7.0.5)") - .RegisterEntry("Fixed an issue where the Update Bibo button did only work if the Advanced Editing window was opened before. (0.7.0.5)"); + .RegisterEntry( + "Fixed an issue where the Update Bibo button did only work if the Advanced Editing window was opened before. (0.7.0.5)"); private static void Add7_0_4(Changelog log) => log.NextVersion("Version 0.7.0.4") @@ -136,7 +159,7 @@ public class PenumbraChangelog .RegisterEntry("Reverted trimming of whitespace for relative paths to only trim the end, not the start. (0.7.0.3)") .RegisterEntry("Fixed a bug that caused an integer overflow on textures of high dimensions. (0.7.0.3)") .RegisterEntry("Fixed a bug that caused Penumbra to enter invalid state when deleting mods. (0.7.0.2)") - .RegisterEntry("Added Notification on invalid collection names. (0.7.0.2)"); + .RegisterEntry("Added Notification on invalid collection names. (0.7.0.2)"); private static void Add7_0_1(Changelog log) => log.NextVersion("Version 0.7.0.1") @@ -145,26 +168,36 @@ public class PenumbraChangelog .RegisterEntry("Fixed a bug that showed the Your Character collection as redundant even if it was not.") .RegisterEntry("Fixed a bug that caused some required collection caches to not be built on startup and thus mods not to apply.") .RegisterEntry("Fixed a bug that showed the current collection as unused even if it was used."); + private static void Add7_0_0(Changelog log) => log.NextVersion("Version 0.7.0.0") - .RegisterHighlight("The entire backend was reworked (this is still in progress). While this does not come with a lot of functionality changes, basically every file and functionality was touched.") - .RegisterEntry("This may have (re-)introduced some bugs that have not yet been noticed despite a long testing period - there are not many users of the testing branch.", 1) + .RegisterHighlight( + "The entire backend was reworked (this is still in progress). While this does not come with a lot of functionality changes, basically every file and functionality was touched.") + .RegisterEntry( + "This may have (re-)introduced some bugs that have not yet been noticed despite a long testing period - there are not many users of the testing branch.", + 1) .RegisterEntry("If you encounter any - but especially breaking or lossy - bugs, please report them immediately.", 1) - .RegisterEntry("This also fixed or improved numerous bugs and issues that will not be listed here.", 1) - .RegisterEntry("GitHub currently reports 321 changed files with 34541 additions and 28464 deletions.", 1) + .RegisterEntry("This also fixed or improved numerous bugs and issues that will not be listed here.", 1) + .RegisterEntry("GitHub currently reports 321 changed files with 34541 additions and 28464 deletions.", 1) .RegisterEntry("Added Notifications on many failures that previously only wrote to log.") .RegisterEntry("Reworked the Collections Tab to hopefully be much more intuitive. It should be self-explanatory now.") .RegisterEntry("The tutorial was adapted to the new window, if you are unsure, maybe try restarting it.", 1) - .RegisterEntry("You can now toggle an incognito mode in the collection window so it shows shortened names of collections and players.", 1) - .RegisterEntry("You can get an overview about the current usage of a selected collection and its active and unused mod settings in the Collection Details panel.", 1) + .RegisterEntry( + "You can now toggle an incognito mode in the collection window so it shows shortened names of collections and players.", 1) + .RegisterEntry( + "You can get an overview about the current usage of a selected collection and its active and unused mod settings in the Collection Details panel.", + 1) .RegisterEntry("The currently selected collection is now highlighted in green (default, configurable) in multiple places.", 1) - .RegisterEntry("Mods now have a 'Collections' panel in the Mod Panel containing an overview about usage of the mod in all collections.") + .RegisterEntry( + "Mods now have a 'Collections' panel in the Mod Panel containing an overview about usage of the mod in all collections.") .RegisterEntry("The 'Changed Items' and 'Effective Changes' tab now contain a collection selector.") .RegisterEntry("Added the On-Screen tab to find what files a specific character is actually using (by Ny).") .RegisterEntry("Added 3 Quick Move folders in the mod selector that can be setup in context menus for easier cleanup.") - .RegisterEntry("Added handling for certain animation files for mounts and fashion accessories to correctly associate them to players.") + .RegisterEntry( + "Added handling for certain animation files for mounts and fashion accessories to correctly associate them to players.") .RegisterEntry("The file selectors in the Advanced Mod Editing Window now use filterable combos.") - .RegisterEntry("The Advanced Mod Editing Window now shows the number of meta edits and file swaps in unselected options and highlights the option selector.") + .RegisterEntry( + "The Advanced Mod Editing Window now shows the number of meta edits and file swaps in unselected options and highlights the option selector.") .RegisterEntry("Added API/IPC to start unpacking and installing mods from external tools (by Sebastina).") .RegisterEntry("Hidden files and folders are now ignored for unused files in Advanced Mod Editing (by myr)") .RegisterEntry("Paths in mods are now automatically trimmed of whitespace on loading.") @@ -173,13 +206,13 @@ public class PenumbraChangelog .RegisterEntry("Fixed some issues with tutorial windows.") .RegisterEntry("Fixed some bugs in the Resource Logger.") .RegisterEntry("Fixed Button Sizing for collapsible groups and several related bugs.") - .RegisterEntry("Fixed issue with mods with default settings other than 0.") - .RegisterEntry("Fixed issue with commands not registering on startup. (0.6.6.5)") - .RegisterEntry("Improved Startup Times and Time Tracking. (0.6.6.4)") - .RegisterEntry("Add Item Swapping between different types of Accessories and Hats. (0.6.6.4)") - .RegisterEntry("Fixed bugs with assignment of temporary collections and their deletion. (0.6.6.4)") - .RegisterEntry("Fixed bugs with new file loading mechanism. (0.6.6.2, 0.6.6.3)") - .RegisterEntry("Added API/IPC to open and close the main window and select specific tabs and mods. (0.6.6.2)"); + .RegisterEntry("Fixed issue with mods with default settings other than 0.") + .RegisterEntry("Fixed issue with commands not registering on startup. (0.6.6.5)") + .RegisterEntry("Improved Startup Times and Time Tracking. (0.6.6.4)") + .RegisterEntry("Add Item Swapping between different types of Accessories and Hats. (0.6.6.4)") + .RegisterEntry("Fixed bugs with assignment of temporary collections and their deletion. (0.6.6.4)") + .RegisterEntry("Fixed bugs with new file loading mechanism. (0.6.6.2, 0.6.6.3)") + .RegisterEntry("Added API/IPC to open and close the main window and select specific tabs and mods. (0.6.6.2)"); private static void Add6_6_1(Changelog log) => log.NextVersion("Version 0.6.6.1") diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index eb4dec4c..ebcff821 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -17,12 +17,12 @@ public enum ColorId FolderLine, ItemId, IncreasedMetaValue, - DecreasedMetaValue, - SelectedCollection, - RedundantAssignment, - NoModsAssignment, - NoAssignment, - SelectorPriority, + DecreasedMetaValue, + SelectedCollection, + RedundantAssignment, + NoModsAssignment, + NoAssignment, + SelectorPriority, InGameHighlight, } @@ -38,7 +38,7 @@ public static class Colors public const uint TutorialBorder = 0xD00000FF; public const uint ReniColorButton = CustomGui.ReniColorButton; public const uint ReniColorHovered = CustomGui.ReniColorHovered; - public const uint ReniColorActive = CustomGui.ReniColorActive ; + public const uint ReniColorActive = CustomGui.ReniColorActive; public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch @@ -69,12 +69,12 @@ public static class Colors }; private static IReadOnlyDictionary _colors = new Dictionary(); - - /// Obtain the configured value for a color. + + /// Obtain the configured value for a color. public static uint Value(this ColorId color) => _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; - - /// Set the configurable colors dictionary to a value. + + /// Set the configurable colors dictionary to a value. public static void SetColors(Configuration config) => _colors = config.Colors; } diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 26f747b7..2cba7cf5 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -8,38 +8,41 @@ namespace Penumbra.UI.Classes; public static class Combos { // Different combos to use with enums. - public static bool Race( string label, ModelRace current, out ModelRace race ) - => Race( label, 100, current, out race ); + public static bool Race(string label, ModelRace current, out ModelRace race) + => Race(label, 100, current, out race); - public static bool Race( string label, float unscaledWidth, ModelRace current, out ModelRace race ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1 ); + public static bool Race(string label, float unscaledWidth, ModelRace current, out ModelRace race) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1); - public static bool Gender( string label, Gender current, out Gender gender ) - => Gender( label, 120, current, out gender ); + public static bool Gender(string label, Gender current, out Gender gender) + => Gender(label, 120, current, out gender); - public static bool Gender( string label, float unscaledWidth, Gender current, out Gender gender ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1 ); + public static bool Gender(string label, float unscaledWidth, Gender current, out Gender gender) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1); - public static bool EqdpEquipSlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); + public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot) + => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, + EquipSlotExtensions.ToName); - public static bool EqpEquipSlot( string label, float width, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); + public static bool EqpEquipSlot(string label, float width, EquipSlot current, out EquipSlot slot) + => ImGuiUtil.GenericEnumCombo(label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, + EquipSlotExtensions.ToName); - public static bool AccessorySlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); + public static bool AccessorySlot(string label, EquipSlot current, out EquipSlot slot) + => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, + EquipSlotExtensions.ToName); - public static bool SubRace( string label, SubRace current, out SubRace subRace ) - => ImGuiUtil.GenericEnumCombo( label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + public static bool SubRace(string label, SubRace current, out SubRace subRace) + => ImGuiUtil.GenericEnumCombo(label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1); - public static bool RspAttribute( string label, RspAttribute current, out RspAttribute attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute, - RspAttributeExtensions.ToFullString, 0, 1 ); + public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute) + => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute, + RspAttributeExtensions.ToFullString, 0, 1); - public static bool EstSlot( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute ); + public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute) + => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute); - public static bool ImcType( string label, ObjectType current, out ObjectType type ) - => ImGuiUtil.GenericEnumCombo( label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, - ObjectTypeExtensions.ToName ); -} \ No newline at end of file + public static bool ImcType(string label, ObjectType current, out ObjectType type) + => ImGuiUtil.GenericEnumCombo(label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, + ObjectTypeExtensions.ToName); +} diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index fe248d99..3ebd3252 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -22,7 +22,7 @@ public sealed class CollectionPanel : IDisposable private readonly ActiveCollections _active; private readonly CollectionSelector _selector; private readonly ActorService _actors; - private readonly ITargetManager _targets; + private readonly ITargetManager _targets; private readonly IndividualAssignmentUi _individualAssignmentUi; private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 4bcff426..88344e6a 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -30,23 +30,24 @@ public class InheritanceUi /// Draw the whole inheritance block. public void Draw() { - using var id = ImRaii.PushId("##Inheritance"); - ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), (Name(_active.Current), ColorId.SelectedCollection.Value() | 0xFF000000), (" inherits from:", 0)); - ImGui.Dummy(Vector2.One); + using var id = ImRaii.PushId("##Inheritance"); + ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), + (Name(_active.Current), ColorId.SelectedCollection.Value() | 0xFF000000), (" inherits from:", 0)); + ImGui.Dummy(Vector2.One); - DrawCurrentCollectionInheritance(); + DrawCurrentCollectionInheritance(); ImGui.SameLine(); DrawInheritanceTrashButton(); ImGui.SameLine(); - DrawRightText(); + DrawRightText(); DrawNewInheritanceSelection(); ImGui.SameLine(); if (ImGui.Button("More Information about Inheritance", new Vector2(ImGui.GetContentRegionAvail().X, 0))) - ImGui.OpenPopup("InheritanceHelp"); - + ImGui.OpenPopup("InheritanceHelp"); + DrawHelpPopup(); - DelayedActions(); + DelayedActions(); } // Keep for reuse. @@ -56,44 +57,44 @@ public class InheritanceUi private ModCollection? _newInheritance; private ModCollection? _movedInheritance; private (int, int)? _inheritanceAction; - private ModCollection? _newCurrentCollection; - + private ModCollection? _newCurrentCollection; + private void DrawRightText() { using var group = ImRaii.Group(); ImGuiUtil.TextWrapped( "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod."); ImGuiUtil.TextWrapped( - "You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can."); - } - - private void DrawHelpPopup() - => ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () => - { - ImGui.NewLine(); - ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); - ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); - ImGui.BulletText( - "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); - ImGui.BulletText( - "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); - ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); - ImGui.BulletText( - "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); - ImGui.NewLine(); - ImGui.TextUnformatted("Example"); - ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); - ImGui.BulletText( - "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); - ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); - ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); - ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); - ImGui.BulletText( - "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); - }); - + "You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can."); + } + + private void DrawHelpPopup() + => ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.NewLine(); + ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); + ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); + ImGui.BulletText( + "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); + ImGui.BulletText( + "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); + ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); + ImGui.BulletText( + "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); + ImGui.NewLine(); + ImGui.TextUnformatted("Example"); + ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); + ImGui.BulletText( + "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); + ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); + ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); + ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); + ImGui.BulletText( + "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); + }); + /// /// If an inherited collection is expanded, @@ -122,7 +123,8 @@ public class InheritanceUi _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", + ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index dc638ef5..e5b0fa19 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -1,5 +1,3 @@ -using System.Collections.Concurrent; -using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 91da4da5..3a7a7343 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,6 +14,7 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.Classes; using ChatService = Penumbra.Services.ChatService; @@ -376,8 +377,10 @@ public sealed class ModFileSystemSelector : FileSystemSelector filter switch { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.Favorite => "Favorite", - ModFilter.NotFavorite => "No Favorite", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", + ModFilter.Enabled => "Enabled", + ModFilter.Disabled => "Disabled", + ModFilter.Favorite => "Favorite", + ModFilter.NotFavorite => "No Favorite", + ModFilter.NoConflict => "No Conflicts", + ModFilter.SolvedConflict => "Solved Conflicts", + ModFilter.UnsolvedConflict => "Unsolved Conflicts", ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - ModFilter.Inherited => "Inherited Configuration", - ModFilter.Uninherited => "Own Configuration", - ModFilter.Undefined => "Not Configured", - _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), + ModFilter.HasMetaManipulations => "Meta Manipulations", + ModFilter.HasNoFileSwaps => "No File Swaps", + ModFilter.HasFileSwaps => "File Swaps", + ModFilter.HasNoConfig => "No Configuration", + ModFilter.HasConfig => "Configuration", + ModFilter.HasNoFiles => "No Files", + ModFilter.HasFiles => "Files", + ModFilter.IsNew => "Newly Imported", + ModFilter.NotNew => "Not Newly Imported", + ModFilter.Inherited => "Inherited Configuration", + ModFilter.Uninherited => "Own Configuration", + ModFilter.Undefined => "Not Configured", + _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), }; -} \ No newline at end of file +} diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 1866606a..59c9d279 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 83f79f56..38737274 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -12,7 +12,7 @@ namespace Penumbra.UI.ModsTab; public class ModPanelConflictsTab : ITab { private readonly ModFileSystemSelector _selector; - private readonly CollectionManager _collectionManager; + private readonly CollectionManager _collectionManager; public ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) { diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 256be8d6..790bc383 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -3,9 +3,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Widgets; -using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; @@ -13,7 +11,7 @@ public class ModPanelDescriptionTab : ITab { private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index acfdcf28..d63e42ef 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -4,10 +4,8 @@ using OtterGui; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Mods; using Penumbra.UI.Classes; using Dalamud.Interface.Components; -using Dalamud.Interface; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -192,7 +190,7 @@ public class ModPanelSettingsTab : ITab _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); + ImGuiUtil.SelectableHelpMarker(option.Description); id.Pop(); } diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 3f1b0f77..02ec9a32 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index ec66ae08..dea68955 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,10 +1,9 @@ using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; -namespace Penumbra.UI; +namespace Penumbra.UI.ResourceWatcher; [Flags] public enum RecordType : byte diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index e8031d4e..781c5fc1 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Text.RegularExpressions; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -15,12 +14,12 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; -namespace Penumbra.UI; +namespace Penumbra.UI.ResourceWatcher; public class ResourceWatcher : IDisposable, ITab { public const int DefaultMaxEntries = 1024; - public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; + public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; private readonly Configuration _config; private readonly ResourceService _resources; @@ -28,7 +27,7 @@ public class ResourceWatcher : IDisposable, ITab private readonly ActorService _actors; private readonly List _records = new(); private readonly ConcurrentQueue _newRecords = new(); - private readonly ResourceWatcherTable _table; + private readonly ResourceWatcherTable _table; private string _logFilter = string.Empty; private Regex? _logRegex; private int _newMaxEntries; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 905307ba..eb034459 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,10 +4,9 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; -using Penumbra.GameData.Enums; using Penumbra.String; -namespace Penumbra.UI; +namespace Penumbra.UI.ResourceWatcher; internal sealed class ResourceWatcherTable : Table { diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 4605527a..f7e64125 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -35,9 +35,9 @@ public class ChangedItemsTab : ITab public void DrawContent() { - _collectionHeader.Draw(true); + _collectionHeader.Draw(true); _drawer.DrawTypeFilter(); - var varWidth = DrawFilters(); + var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); if (!child) return; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 4d6d2dd6..5b51bd85 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -58,11 +58,12 @@ public class CollectionsTab : IDisposable, ITab public void DrawContent() { - var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnnnn").X; + var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnnnn").X; using (var group = ImRaii.Group()) { _selector.Draw(width); } + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); ImGui.SameLine(); @@ -91,7 +92,7 @@ public class CollectionsTab : IDisposable, ITab color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments); ImGui.SameLine(); - + color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.IndividualAssignment); if (ImGui.Button("Individual Assignments", buttonSize)) Mode = PanelMode.IndividualAssignment; @@ -112,7 +113,7 @@ public class CollectionsTab : IDisposable, ITab color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); ImGui.SameLine(); - + style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value()) .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index fc0168a0..8dae2caa 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -565,11 +565,12 @@ public class DebugTab : Window, ITab ImGui.InputText("File Name", ref _emoteSearchFile, 256); ImGui.InputText("Emote Name", ref _emoteSearchName, 256); - using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); + using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); if (!table) return; - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); var dummy = ImGuiClip.FilteredClippedDraw(_identifier.AwaitedService.Emotes, skips, p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase) && (_emoteSearchName.Length == 0 diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 3c1d0801..0cc2e5c1 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -30,7 +30,7 @@ public class EffectiveTab : ITab public void DrawContent() { - SetupEffectiveSizes(); + SetupEffectiveSizes(); _collectionHeader.Draw(true); DrawFilters(); using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 701d1ef6..7c350106 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -703,12 +703,13 @@ public class SettingsTab : ITab if (_compactor.MassCompactRunning) { ImGui.ProgressBar((float)_compactor.CurrentIndex / _compactor.TotalFiles, - new Vector2(ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - UiHelpers.IconButtonSize.X, ImGui.GetFrameHeight()), + new Vector2(ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - UiHelpers.IconButtonSize.X, + ImGui.GetFrameHeight()), _compactor.CurrentFile?.FullName[(_modManager.BasePath.FullName.Length + 1)..] ?? "Gathering Files..."); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Ban.ToIconString(), UiHelpers.IconButtonSize, "Cancel the mass action.", !_compactor.MassCompactRunning, true)) - _compactor.CancelMassCompact(); + _compactor.CancelMassCompact(); } else { diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 87e709c3..1a589c28 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -42,10 +42,10 @@ public enum BasicTutorialSteps /// Service for the in-game tutorial. public class TutorialService { - public const string SelectedCollection = "Selected Collection"; - public const string DefaultCollection = "Base Collection"; - public const string InterfaceCollection = "Interface Collection"; - public const string AssignedCollections = "Assigned Collections"; + public const string SelectedCollection = "Selected Collection"; + public const string DefaultCollection = "Base Collection"; + public const string InterfaceCollection = "Interface Collection"; + public const string AssignedCollections = "Assigned Collections"; public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" + " - 'self' or '': your own character\n" @@ -79,20 +79,26 @@ public class TutorialService .Register("Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + "This is our next stop!\n\n" + "Go here after setting up your root folder to continue the tutorial!") - .Register("Initial Setup, Step 4: Managing Collections", "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" + .Register("Initial Setup, Step 4: Managing Collections", + "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" + $"There will always be one collection called {ModCollection.DefaultCollectionName} that can not be deleted.") .Register($"Initial Setup, Step 5: {SelectedCollection}", $"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n" + $"We should already have the collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") - .Register("Initial Setup, Step 6: Simple Assignments", "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" + .Register("Initial Setup, Step 6: Simple Assignments", + "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n" + $"If you are just starting, you can see that the {ModCollection.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.") - .Register("Individual Assignments", "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") - .Register("Group Assignments", "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age.") - .Register("Collection Details", "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n" + .Register("Individual Assignments", + "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") + .Register("Group Assignments", + "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age.") + .Register("Collection Details", + "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n" + "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself.") - .Register("Incognito Mode", "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n" + .Register("Incognito Mode", + "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n" + "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n" + "It is strongly recommended to not show your characters name in public screenshots when using Penumbra.") .Deprecated() @@ -150,7 +156,7 @@ public class TutorialService _config.TutorialStep = v; _config.Save(); }); - + /// Update the current tutorial step if tutorials have changed since last update. public void UpdateTutorialStep() { diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 7f4fc7cb..132dce86 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -114,8 +114,8 @@ public static class UiHelpers ScaleX5 = Scale * 5; } - IconButtonSize = new Vector2(ImGui.GetFrameHeight()); + IconButtonSize = new Vector2(ImGui.GetFrameHeight()); InputTextMinusButton3 = InputTextWidth.X - IconButtonSize.X - ScaleX3; - InputTextMinusButton = InputTextWidth.X - IconButtonSize.X - ImGui.GetStyle().ItemSpacing.X; + InputTextMinusButton = InputTextWidth.X - IconButtonSize.X - ImGui.GetStyle().ItemSpacing.X; } } diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index f08b6de9..25d91644 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -1,11 +1,10 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using Penumbra.UI; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Tabs; -namespace Penumbra; +namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable { diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 74274c38..abf715e6 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -2,98 +2,84 @@ namespace Penumbra.Util; public static class DictionaryExtensions { - /// Returns whether two dictionaries contain equal keys and values. - public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + /// Returns whether two dictionaries contain equal keys and values. + public static bool SetEquals(this IReadOnlyDictionary lhs, IReadOnlyDictionary rhs) { - if( ReferenceEquals( lhs, rhs ) ) - { + if (ReferenceEquals(lhs, rhs)) return true; - } - if( lhs.Count != rhs.Count ) - { + if (lhs.Count != rhs.Count) return false; - } - foreach( var (key, value) in lhs ) + foreach (var (key, value) in lhs) { - if( !rhs.TryGetValue( key, out var rhsValue ) ) - { + if (!rhs.TryGetValue(key, out var rhsValue)) return false; - } - if( value == null ) + if (value == null) { - if( rhsValue != null ) - { + if (rhsValue != null) return false; - } continue; } - if( !value.Equals( rhsValue ) ) - { + if (!value.Equals(rhsValue)) return false; - } } return true; } - /// Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand. - public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + /// Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand. + public static void SetTo(this Dictionary lhs, IReadOnlyDictionary rhs) where TKey : notnull { - if( ReferenceEquals( lhs, rhs ) ) + if (ReferenceEquals(lhs, rhs)) return; lhs.Clear(); - lhs.EnsureCapacity( rhs.Count ); - foreach( var (key, value) in rhs ) - lhs.Add( key, value ); + lhs.EnsureCapacity(rhs.Count); + foreach (var (key, value) in rhs) + lhs.Add(key, value); } - /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. + /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. public static void SetTo(this HashSet lhs, IReadOnlySet rhs) { if (ReferenceEquals(lhs, rhs)) return; lhs.Clear(); - lhs.EnsureCapacity(rhs.Count); + lhs.EnsureCapacity(rhs.Count); foreach (var value in rhs) lhs.Add(value); } - /// Add all entries from the other dictionary that would not overwrite current keys. - public static void AddFrom< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs ) + /// Add all entries from the other dictionary that would not overwrite current keys. + public static void AddFrom(this Dictionary lhs, IReadOnlyDictionary rhs) where TKey : notnull { - if( ReferenceEquals( lhs, rhs ) ) - { + if (ReferenceEquals(lhs, rhs)) return; - } - lhs.EnsureCapacity( lhs.Count + rhs.Count ); - foreach( var (key, value) in rhs ) - { - lhs.Add( key, value ); - } + lhs.EnsureCapacity(lhs.Count + rhs.Count); + foreach (var (key, value) in rhs) + lhs.Add(key, value); } - public static int ReplaceValue< TKey, TValue >( this Dictionary< TKey, TValue > dict, TValue from, TValue to ) + public static int ReplaceValue(this Dictionary dict, TValue from, TValue to) where TKey : notnull - where TValue : IEquatable< TValue > + where TValue : IEquatable { var count = 0; - foreach( var (key, _) in dict.ToArray().Where( kvp => kvp.Value.Equals( from ) ) ) + foreach (var (key, _) in dict.ToArray().Where(kvp => kvp.Value.Equals(from))) { - dict[ key ] = to; + dict[key] = to; ++count; } return count; } -} \ No newline at end of file +} diff --git a/Penumbra/Util/FixedUlongStringEnumConverter.cs b/Penumbra/Util/FixedUlongStringEnumConverter.cs index 85c61837..857d951d 100644 --- a/Penumbra/Util/FixedUlongStringEnumConverter.cs +++ b/Penumbra/Util/FixedUlongStringEnumConverter.cs @@ -8,84 +8,70 @@ namespace Penumbra.Util; // These converters fix this, taken from https://stackoverflow.com/questions/61740964/json-net-unable-to-deserialize-ulong-flag-type-enum/ public class ForceNumericFlagEnumConverter : FixedUlongStringEnumConverter { - private static bool HasFlagsAttribute( Type? objectType ) - => objectType != null && Attribute.IsDefined( Nullable.GetUnderlyingType( objectType ) ?? objectType, typeof( System.FlagsAttribute ) ); + private static bool HasFlagsAttribute(Type? objectType) + => objectType != null && Attribute.IsDefined(Nullable.GetUnderlyingType(objectType) ?? objectType, typeof(FlagsAttribute)); - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { var enumType = value?.GetType(); - if( HasFlagsAttribute( enumType ) ) + if (HasFlagsAttribute(enumType)) { - var underlyingType = Enum.GetUnderlyingType( enumType! ); - var underlyingValue = Convert.ChangeType( value, underlyingType ); - writer.WriteValue( underlyingValue ); + var underlyingType = Enum.GetUnderlyingType(enumType!); + var underlyingValue = Convert.ChangeType(value, underlyingType); + writer.WriteValue(underlyingValue); } else { - base.WriteJson( writer, value, serializer ); + base.WriteJson(writer, value, serializer); } } } public class FixedUlongStringEnumConverter : StringEnumConverter { - public override object? ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - if( reader.MoveToContentAndAssert().TokenType != JsonToken.Integer || reader.ValueType != typeof( System.Numerics.BigInteger ) ) - { - return base.ReadJson( reader, objectType, existingValue, serializer ); - } + if (reader.MoveToContentAndAssert().TokenType != JsonToken.Integer || reader.ValueType != typeof(BigInteger)) + return base.ReadJson(reader, objectType, existingValue, serializer); // Todo: throw an exception if !this.AllowIntegerValues // https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Converters_StringEnumConverter_AllowIntegerValues.htm - var enumType = Nullable.GetUnderlyingType( objectType ) ?? objectType; - if( Enum.GetUnderlyingType( enumType ) == typeof( ulong ) ) + var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType; + if (Enum.GetUnderlyingType(enumType) == typeof(ulong)) { - var bigInteger = ( System.Numerics.BigInteger )reader.Value!; - if( bigInteger >= ulong.MinValue && bigInteger <= ulong.MaxValue ) - { - return Enum.ToObject( enumType, checked( ( ulong )bigInteger ) ); - } + var bigInteger = (BigInteger)reader.Value!; + if (bigInteger >= ulong.MinValue && bigInteger <= ulong.MaxValue) + return Enum.ToObject(enumType, checked((ulong)bigInteger)); } - return base.ReadJson( reader, objectType, existingValue, serializer ); + return base.ReadJson(reader, objectType, existingValue, serializer); } } public static partial class JsonExtensions { - public static JsonReader MoveToContentAndAssert( this JsonReader reader ) + public static JsonReader MoveToContentAndAssert(this JsonReader reader) { - if( reader == null ) - { + if (reader == null) throw new ArgumentNullException(); - } - if( reader.TokenType == JsonToken.None ) // Skip past beginning of stream. - { + if (reader.TokenType == JsonToken.None) // Skip past beginning of stream. reader.ReadAndAssert(); - } - while( reader.TokenType == JsonToken.Comment ) // Skip past comments. - { + while (reader.TokenType == JsonToken.Comment) // Skip past comments. reader.ReadAndAssert(); - } return reader; } - private static JsonReader ReadAndAssert( this JsonReader reader ) + private static JsonReader ReadAndAssert(this JsonReader reader) { - if( reader == null ) - { + if (reader == null) throw new ArgumentNullException(); - } - if( !reader.Read() ) - { - throw new JsonReaderException( "Unexpected end of JSON stream." ); - } + if (!reader.Read()) + throw new JsonReaderException("Unexpected end of JSON stream."); return reader; } -} \ No newline at end of file +} diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index d5b51433..d913a019 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -10,45 +10,45 @@ public class PenumbraSqPackStream : IDisposable protected BinaryReader Reader { get; set; } - public PenumbraSqPackStream( FileInfo file ) - : this( file.OpenRead() ) + public PenumbraSqPackStream(FileInfo file) + : this(file.OpenRead()) { } - public PenumbraSqPackStream( Stream stream ) + public PenumbraSqPackStream(Stream stream) { BaseStream = stream; - Reader = new BinaryReader( BaseStream ); + Reader = new BinaryReader(BaseStream); } public SqPackHeader GetSqPackHeader() { BaseStream.Position = 0; - return Reader.ReadStructure< SqPackHeader >(); + return Reader.ReadStructure(); } - public SqPackFileInfo GetFileMetadata( long offset ) + public SqPackFileInfo GetFileMetadata(long offset) { BaseStream.Position = offset; - return Reader.ReadStructure< SqPackFileInfo >(); + return Reader.ReadStructure(); } - public T ReadFile< T >( long offset ) where T : PenumbraFileResource + public T ReadFile(long offset) where T : PenumbraFileResource { using var ms = new MemoryStream(); BaseStream.Position = offset; - var fileInfo = Reader.ReadStructure< SqPackFileInfo >(); - var file = Activator.CreateInstance< T >(); + var fileInfo = Reader.ReadStructure(); + var file = Activator.CreateInstance(); // check if we need to read the extended model header or just default to the standard file header - if( fileInfo.Type == FileType.Model ) + if (fileInfo.Type == FileType.Model) { BaseStream.Position = offset; - var modelFileInfo = Reader.ReadStructure< ModelBlock >(); + var modelFileInfo = Reader.ReadStructure(); file.FileInfo = new PenumbraFileInfo { @@ -74,33 +74,31 @@ public class PenumbraSqPackStream : IDisposable }; } - switch( fileInfo.Type ) + switch (fileInfo.Type) { - case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." ); + case FileType.Empty: throw new FileNotFoundException($"The file located at 0x{offset:x} is empty."); case FileType.Standard: - ReadStandardFile( file, ms ); + ReadStandardFile(file, ms); break; case FileType.Model: - ReadModelFile( file, ms ); + ReadModelFile(file, ms); break; case FileType.Texture: - ReadTextureFile( file, ms ); + ReadTextureFile(file, ms); break; - default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." ); + default: throw new NotImplementedException($"File Type {(uint)fileInfo.Type} is not implemented."); } file.Data = ms.ToArray(); - if( file.Data.Length != file.FileInfo.RawFileSize ) - { - Debug.WriteLine( "Read data size does not match file size." ); - } + if (file.Data.Length != file.FileInfo.RawFileSize) + Debug.WriteLine("Read data size does not match file size."); - file.FileStream = new MemoryStream( file.Data, false ); - file.Reader = new BinaryReader( file.FileStream ); + file.FileStream = new MemoryStream(file.Data, false); + file.Reader = new BinaryReader(file.FileStream); file.FileStream.Position = 0; file.LoadFile(); @@ -108,20 +106,18 @@ public class PenumbraSqPackStream : IDisposable return file; } - private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms ) + private void ReadStandardFile(PenumbraFileResource resource, MemoryStream ms) { - var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount ); + var blocks = Reader.ReadStructures((int)resource.FileInfo!.BlockCount); - foreach( var block in blocks ) - { - ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms ); - } + foreach (var block in blocks) + ReadFileBlock(resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms); // reset position ready for reading ms.Position = 0; } - private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms ) + private unsafe void ReadModelFile(PenumbraFileResource resource, MemoryStream ms) { var mdlBlock = resource.FileInfo!.ModelBlock; var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; @@ -133,185 +129,165 @@ public class PenumbraSqPackStream : IDisposable // but it seems to work fine in explorer... int totalBlocks = mdlBlock.StackBlockNum; totalBlocks += mdlBlock.RuntimeBlockNum; - for( var i = 0; i < 3; i++ ) - { - totalBlocks += mdlBlock.VertexBufferBlockNum[ i ]; - } + for (var i = 0; i < 3; i++) + totalBlocks += mdlBlock.VertexBufferBlockNum[i]; - for( var i = 0; i < 3; i++ ) - { - totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; - } + for (var i = 0; i < 3; i++) + totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[i]; - for( var i = 0; i < 3; i++ ) - { - totalBlocks += mdlBlock.IndexBufferBlockNum[ i ]; - } + for (var i = 0; i < 3; i++) + totalBlocks += mdlBlock.IndexBufferBlockNum[i]; - var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks ); + var compressedBlockSizes = Reader.ReadStructures(totalBlocks); var currentBlock = 0; var vertexDataOffsets = new int[3]; var indexDataOffsets = new int[3]; var vertexBufferSizes = new int[3]; var indexBufferSizes = new int[3]; - ms.Seek( 0x44, SeekOrigin.Begin ); + ms.Seek(0x44, SeekOrigin.Begin); - Reader.Seek( baseOffset + mdlBlock.StackOffset ); + Reader.Seek(baseOffset + mdlBlock.StackOffset); var stackStart = ms.Position; - for( var i = 0; i < mdlBlock.StackBlockNum; i++ ) + for (var i = 0; i < mdlBlock.StackBlockNum; i++) { var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + ReadFileBlock(ms); + Reader.Seek(lastPos + compressedBlockSizes[currentBlock]); currentBlock++; } var stackEnd = ms.Position; - var stackSize = ( int )( stackEnd - stackStart ); + var stackSize = (int)(stackEnd - stackStart); - Reader.Seek( baseOffset + mdlBlock.RuntimeOffset ); + Reader.Seek(baseOffset + mdlBlock.RuntimeOffset); var runtimeStart = ms.Position; - for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ ) + for (var i = 0; i < mdlBlock.RuntimeBlockNum; i++) { var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + ReadFileBlock(ms); + Reader.Seek(lastPos + compressedBlockSizes[currentBlock]); currentBlock++; } var runtimeEnd = ms.Position; - var runtimeSize = ( int )( runtimeEnd - runtimeStart ); + var runtimeSize = (int)(runtimeEnd - runtimeStart); - for( var i = 0; i < 3; i++ ) + for (var i = 0; i < 3; i++) { - if( mdlBlock.VertexBufferBlockNum[ i ] != 0 ) + if (mdlBlock.VertexBufferBlockNum[i] != 0) { - var currentVertexOffset = ( int )ms.Position; - if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] ) - { - vertexDataOffsets[ i ] = currentVertexOffset; - } + var currentVertexOffset = (int)ms.Position; + if (i == 0 || currentVertexOffset != vertexDataOffsets[i - 1]) + vertexDataOffsets[i] = currentVertexOffset; else - { - vertexDataOffsets[ i ] = 0; - } + vertexDataOffsets[i] = 0; - Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] ); + Reader.Seek(baseOffset + mdlBlock.VertexBufferOffset[i]); - for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ ) + for (var j = 0; j < mdlBlock.VertexBufferBlockNum[i]; j++) { var lastPos = Reader.BaseStream.Position; - vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + vertexBufferSizes[i] += (int)ReadFileBlock(ms); + Reader.Seek(lastPos + compressedBlockSizes[currentBlock]); currentBlock++; } } - if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 ) - { - for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ ) + if (mdlBlock.EdgeGeometryVertexBufferBlockNum[i] != 0) + for (var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[i]; j++) { var lastPos = Reader.BaseStream.Position; - ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + ReadFileBlock(ms); + Reader.Seek(lastPos + compressedBlockSizes[currentBlock]); currentBlock++; } - } - if( mdlBlock.IndexBufferBlockNum[ i ] != 0 ) + if (mdlBlock.IndexBufferBlockNum[i] != 0) { - var currentIndexOffset = ( int )ms.Position; - if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] ) - { - indexDataOffsets[ i ] = currentIndexOffset; - } + var currentIndexOffset = (int)ms.Position; + if (i == 0 || currentIndexOffset != indexDataOffsets[i - 1]) + indexDataOffsets[i] = currentIndexOffset; else - { - indexDataOffsets[ i ] = 0; - } + indexDataOffsets[i] = 0; // i guess this is only needed in the vertex area, for i = 0 // Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] ); - for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ ) + for (var j = 0; j < mdlBlock.IndexBufferBlockNum[i]; j++) { var lastPos = Reader.BaseStream.Position; - indexBufferSizes[ i ] += ( int )ReadFileBlock( ms ); - Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] ); + indexBufferSizes[i] += (int)ReadFileBlock(ms); + Reader.Seek(lastPos + compressedBlockSizes[currentBlock]); currentBlock++; } } } - ms.Seek( 0, SeekOrigin.Begin ); - ms.Write( BitConverter.GetBytes( mdlBlock.Version ) ); - ms.Write( BitConverter.GetBytes( stackSize ) ); - ms.Write( BitConverter.GetBytes( runtimeSize ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) ); - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) ); - } + ms.Seek(0, SeekOrigin.Begin); + ms.Write(BitConverter.GetBytes(mdlBlock.Version)); + ms.Write(BitConverter.GetBytes(stackSize)); + ms.Write(BitConverter.GetBytes(runtimeSize)); + ms.Write(BitConverter.GetBytes(mdlBlock.VertexDeclarationNum)); + ms.Write(BitConverter.GetBytes(mdlBlock.MaterialNum)); + for (var i = 0; i < 3; i++) + ms.Write(BitConverter.GetBytes(vertexDataOffsets[i])); - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) ); - } + for (var i = 0; i < 3; i++) + ms.Write(BitConverter.GetBytes(indexDataOffsets[i])); - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) ); - } + for (var i = 0; i < 3; i++) + ms.Write(BitConverter.GetBytes(vertexBufferSizes[i])); - for( var i = 0; i < 3; i++ ) - { - ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) ); - } + for (var i = 0; i < 3; i++) + ms.Write(BitConverter.GetBytes(indexBufferSizes[i])); - ms.Write( new[] { mdlBlock.NumLods } ); - ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) ); - ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) ); - ms.Write( new byte[] { 0 } ); + ms.Write(new[] + { + mdlBlock.NumLods, + }); + ms.Write(BitConverter.GetBytes(mdlBlock.IndexBufferStreamingEnabled)); + ms.Write(BitConverter.GetBytes(mdlBlock.EdgeGeometryEnabled)); + ms.Write(new byte[] + { + 0, + }); } - private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) + private void ReadTextureFile(PenumbraFileResource resource, MemoryStream ms) { - if( resource.FileInfo!.BlockCount == 0 ) - { + if (resource.FileInfo!.BlockCount == 0) return; - } - var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount ); + var blocks = Reader.ReadStructures((int)resource.FileInfo!.BlockCount); // if there is a mipmap header, the comp_offset // will not be 0 - var mipMapSize = blocks[ 0 ].CompressedOffset; - if( mipMapSize != 0 ) + var mipMapSize = blocks[0].CompressedOffset; + if (mipMapSize != 0) { var originalPos = BaseStream.Position; BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; - ms.Write( Reader.ReadBytes( ( int )mipMapSize ) ); + ms.Write(Reader.ReadBytes((int)mipMapSize)); BaseStream.Position = originalPos; } // i is for texture blocks, j is 'data blocks'... - for( byte i = 0; i < blocks.Count; i++ ) + for (byte i = 0; i < blocks.Count; i++) { - if( blocks[ i ].CompressedSize == 0 ) + if (blocks[i].CompressedSize == 0) continue; // start from comp_offset - var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; - ReadFileBlock( runningBlockTotal, ms, true ); + var runningBlockTotal = blocks[i].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize; + ReadFileBlock(runningBlockTotal, ms, true); - for( var j = 1; j < blocks[ i ].BlockCount; j++ ) + for (var j = 1; j < blocks[i].BlockCount; j++) { - runningBlockTotal += ( uint )Reader.ReadInt16(); - ReadFileBlock( runningBlockTotal, ms, true ); + runningBlockTotal += (uint)Reader.ReadInt16(); + ReadFileBlock(runningBlockTotal, ms, true); } // unknown @@ -319,34 +295,32 @@ public class PenumbraSqPackStream : IDisposable } } - protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false ) - => ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition ); + protected uint ReadFileBlock(MemoryStream dest, bool resetPosition = false) + => ReadFileBlock(Reader.BaseStream.Position, dest, resetPosition); - protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false ) + protected uint ReadFileBlock(long offset, MemoryStream dest, bool resetPosition = false) { var originalPosition = BaseStream.Position; BaseStream.Position = offset; - var blockHeader = Reader.ReadStructure< DatBlockHeader >(); + var blockHeader = Reader.ReadStructure(); // uncompressed block - if( blockHeader.CompressedSize == 32000 ) + if (blockHeader.CompressedSize == 32000) { - dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) ); + dest.Write(Reader.ReadBytes((int)blockHeader.UncompressedSize)); } else { - var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize ); + var data = Reader.ReadBytes((int)blockHeader.CompressedSize); - using var compressedStream = new MemoryStream( data ); - using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress ); - zlibStream.CopyTo( dest ); + using var compressedStream = new MemoryStream(data); + using var zlibStream = new DeflateStream(compressedStream, CompressionMode.Decompress); + zlibStream.CopyTo(dest); } - if( resetPosition ) - { + if (resetPosition) BaseStream.Position = originalPosition; - } return blockHeader.UncompressedSize; } @@ -390,7 +364,7 @@ public class PenumbraSqPackStream : IDisposable } } - [StructLayout( LayoutKind.Sequential )] + [StructLayout(LayoutKind.Sequential)] private struct DatBlockHeader { public uint Size; @@ -399,7 +373,7 @@ public class PenumbraSqPackStream : IDisposable public uint UncompressedSize; }; - [StructLayout( LayoutKind.Sequential )] + [StructLayout(LayoutKind.Sequential)] private struct LodBlock { public uint CompressedOffset; @@ -408,4 +382,4 @@ public class PenumbraSqPackStream : IDisposable public uint BlockOffset; public uint BlockCount; } -} \ No newline at end of file +} diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index c0ad766d..932072f0 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,5 +1,5 @@ global using StartTracker = OtterGui.Classes.StartTimeTracker; -global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; +global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; namespace Penumbra.Util; From d7205344ebdde365f9c0c4990a1fbf1825775f49 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 17 Sep 2023 22:25:31 +0200 Subject: [PATCH 1172/2451] ResourceTree improvements + IPC - Moves ResourceType enum out of GameData as discussed on Discord ; - Adds new color coding for local player and non-networked objects on On-Screen ; - Adds ResourceTree-related IPC ; - Fixes #342. --- Penumbra/Api/IpcTester.cs | 223 +++++++++++++++ Penumbra/Api/PenumbraApi.cs | 115 +++++--- Penumbra/Api/PenumbraIpcProviders.cs | 18 ++ Penumbra/Configuration.cs | 1 + Penumbra/Enums/ResourceType.cs | 260 ++++++++++++++++++ .../PathResolving/AnimationHookService.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 1 + .../Interop/PathResolving/PathResolver.cs | 1 - .../Interop/PathResolving/SubfileHelper.cs | 2 +- .../Interop/ResourceLoading/ResourceLoader.cs | 2 +- .../ResourceLoading/ResourceManagerService.cs | 2 +- .../ResourceLoading/ResourceService.cs | 2 +- .../Interop/ResourceLoading/TexMdlService.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 1 + Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 24 +- .../ResourceTree/ResourceTreeApiHelper.cs | 104 +++++++ .../ResourceTree/ResourceTreeFactory.cs | 38 ++- .../Interop/ResourceTree/TreeBuildCache.cs | 28 +- Penumbra/Interop/Services/DecalReverter.cs | 2 +- Penumbra/Interop/Structs/ResourceHandle.cs | 2 +- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 1 + Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 1 + Penumbra/Mods/ItemSwap/ItemSwap.cs | 3 +- Penumbra/Mods/ItemSwap/Swaps.cs | 2 +- Penumbra/Penumbra.csproj | 4 + .../UI/AdvancedWindow/ResourceTreeViewer.cs | 25 +- Penumbra/UI/ChangedItemDrawer.cs | 24 ++ Penumbra/UI/Classes/Colors.cs | 10 + Penumbra/UI/ResourceWatcher/Record.cs | 1 + .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../ResourceWatcher/ResourceWatcherTable.cs | 1 + 32 files changed, 826 insertions(+), 80 deletions(-) create mode 100644 Penumbra/Enums/ResourceType.cs create mode 100644 Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 69c2fd3d..fb8719f4 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -15,6 +15,8 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; +using Dalamud.Plugin.Services; +using Penumbra.GameData.Enums; namespace Penumbra.Api; @@ -35,6 +37,7 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Editing _editing; private readonly Temporary _temporary; + private readonly ResourceTree _resourceTree; public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) @@ -52,6 +55,7 @@ public class IpcTester : IDisposable _modSettings = new ModSettings(dalamud.PluginInterface); _editing = new Editing(dalamud.PluginInterface); _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); + _resourceTree = new ResourceTree(dalamud.PluginInterface, dalamud.Objects); UnsubscribeEvents(); } @@ -75,6 +79,7 @@ public class IpcTester : IDisposable _temporary.Draw(); _temporary.DrawCollections(); _temporary.DrawMods(); + _resourceTree.Draw(); } catch (Exception e) { @@ -1397,4 +1402,222 @@ public class IpcTester : IDisposable } } } + + private class ResourceTree + { + private readonly DalamudPluginInterface _pi; + private readonly IObjectTable _objects; + + private string _gameObjectIndices = "0"; + private bool _mergeSameCollection = false; + private ResourceType _type = ResourceType.Mtrl; + private bool _withUIData = false; + + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + + public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) + { + _pi = pi; + _objects = objects; + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Resource Tree"); + if (!_) + return; + + ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); + ImGui.Checkbox("Merge entries that use the same collection", ref _mergeSameCollection); + ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); + ImGui.Checkbox("Also get names and icons", ref _withUIData); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); + if (ImGui.Button("Get##GameObjectResourcePaths")) + { + var gameObjects = GetSelectedGameObjects(); + var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects, _mergeSameCollection); + + _lastGameObjectResourcePaths = gameObjects + .Select(GameObjectToString) + .Zip(resourcePaths) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcePaths)); + } + + DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); + if (ImGui.Button("Get##PlayerResourcePaths")) + { + _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke(_mergeSameCollection) + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcePaths)); + } + + DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); + if (ImGui.Button("Get##GameObjectResourcesOfType")) + { + var gameObjects = GetSelectedGameObjects(); + var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(gameObjects, _type, _withUIData); + + _lastGameObjectResourcesOfType = gameObjects + .Select(GameObjectToString) + .Zip(resourcesOfType) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcesOfType)); + } + + DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); + if (ImGui.Button("Get##PlayerResourcesOfType")) + { + _lastPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData) + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); + } + + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths); + DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths); + + DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType); + DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType); + } + + private static void DrawPopup(string popupId, ref T? result, Action drawResult) where T : class + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); + using var popup = ImRaii.Popup(popupId); + if (!popup) + { + result = null; + return; + } + + if (result == null) + { + ImGui.CloseCurrentPopup(); + return; + } + + drawResult(result); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + { + result = null; + ImGui.CloseCurrentPopup(); + } + } + + private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class + { + var firstSeen = new Dictionary(); + foreach (var (label, item) in result) + { + if (item == null) + { + ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + if (firstSeen.TryGetValue(item, out var firstLabel)) + { + ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + firstSeen.Add(item, label); + + using var header = ImRaii.TreeNode(label); + if (!header) + continue; + + drawItem(item); + } + } + + private static void DrawResourcePaths((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, paths => + { + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (actualPath, gamePaths) in paths) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + ImGui.TableNextColumn(); + foreach (var gamePath in gamePaths) + ImGui.TextUnformatted(gamePath); + } + }); + } + + private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, resources => + { + using var table = ImRaii.Table(string.Empty, _withUIData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUIData ? 0.55f : 0.85f); + if (_withUIData) + ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var (resourceHandle, (actualPath, name, icon)) in resources) + { + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{resourceHandle:X}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + if (_withUIData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(icon.ToString()); + ImGui.SameLine(); + ImGui.TextUnformatted(name); + } + } + }); + } + + private static void TextUnformattedMono(string text) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(text); + } + + private ushort[] GetSelectedGameObjects() + => _gameObjectIndices.Split(',') + .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) + .ToArray(); + + private unsafe string GameObjectToString(ushort gameObjectIndex) + { + var gameObject = _objects[gameObjectIndex]; + + return gameObject != null + ? $"[{gameObjectIndex}] {gameObject.Name} ({gameObject.ObjectKind})" + : $"[{gameObjectIndex}] null"; + } + } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 73a87fab..1c56b9df 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -23,13 +23,15 @@ using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; +using Penumbra.Interop.ResourceTree; +using System.Collections.Immutable; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 21); + => (4, 22); public event Action? PreSettingsPanelDraw { @@ -104,31 +106,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi private ModFileSystem _modFileSystem; private ConfigWindow _configWindow; private TextureManager _textureManager; + private ResourceTreeFactory _resourceTreeFactory; public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, - ConfigWindow configWindow, TextureManager textureManager) + ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) { - _communicator = communicator; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _dalamud = dalamud; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; - _modImportManager = modImportManager; - _collectionEditor = collectionEditor; - _redrawService = redrawService; - _modFileSystem = modFileSystem; - _configWindow = configWindow; - _textureManager = textureManager; - _lumina = _dalamud.GameData.GameData; + _communicator = communicator; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _modImportManager = modImportManager; + _collectionEditor = collectionEditor; + _redrawService = redrawService; + _modFileSystem = modFileSystem; + _configWindow = configWindow; + _textureManager = textureManager; + _resourceTreeFactory = resourceTreeFactory; + _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); @@ -145,24 +149,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); - _lumina = null; - _communicator = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _dalamud = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; - _modImportManager = null!; - _collectionEditor = null!; - _redrawService = null!; - _modFileSystem = null!; - _configWindow = null!; - _textureManager = null!; + _lumina = null; + _communicator = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; + _collectionResolver = null!; + _cutsceneService = null!; + _modImportManager = null!; + _collectionEditor = null!; + _redrawService = null!; + _modFileSystem = null!; + _configWindow = null!; + _textureManager = null!; + _resourceTreeFactory = null!; } public event ChangedItemClick? ChangedItemClicked @@ -1011,6 +1016,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; // @formatter:on + public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects, bool mergeSameCollection) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, false, false); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + + return Array.ConvertAll(gameObjects, obj => pathDictionaries.TryGetValue(obj, out var pathDict) ? pathDict : null); + } + + public IReadOnlyDictionary> GetPlayerResourcePaths(bool mergeSameCollection) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(true, false, false); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + + return pathDictionaries.AsReadOnly(); + } + + public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ushort[] gameObjects, ResourceType type, bool withUIData) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData, false); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); + } + + public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, bool withUIData) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(true, withUIData, false); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return resDictionaries.AsReadOnly(); + } + // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 2d3fcf97..c05a7c47 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -118,6 +118,12 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryModAll; internal readonly FuncProvider RemoveTemporaryMod; + // Resource Tree + internal readonly FuncProvider?[]> GetGameObjectResourcePaths; + internal readonly FuncProvider>> GetPlayerResourcePaths; + internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; + internal readonly FuncProvider>> GetPlayerResourcesOfType; + public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { @@ -236,6 +242,12 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); + // ResourceTree + GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths); + GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); + GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); + GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); + Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); Initialized.Invoke(); @@ -345,6 +357,12 @@ public class PenumbraIpcProviders : IDisposable ConvertTextureFile.Dispose(); ConvertTextureData.Dispose(); + // Resource Tree + GetGameObjectResourcePaths.Dispose(); + GetPlayerResourcePaths.Dispose(); + GetGameObjectResourcesOfType.Dispose(); + GetPlayerResourcesOfType.Dispose(); + Disposed.Invoke(); Disposed.Dispose(); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63d58a16..0206d0ae 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; using Penumbra.Mods; diff --git a/Penumbra/Enums/ResourceType.cs b/Penumbra/Enums/ResourceType.cs new file mode 100644 index 00000000..0cfc5469 --- /dev/null +++ b/Penumbra/Enums/ResourceType.cs @@ -0,0 +1,260 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.String; +using Penumbra.String.Functions; + +namespace Penumbra.Enums; + +[Flags] +public enum ResourceTypeFlag : ulong +{ + Aet = 0x0000_0000_0000_0001, + Amb = 0x0000_0000_0000_0002, + Atch = 0x0000_0000_0000_0004, + Atex = 0x0000_0000_0000_0008, + Avfx = 0x0000_0000_0000_0010, + Awt = 0x0000_0000_0000_0020, + Cmp = 0x0000_0000_0000_0040, + Dic = 0x0000_0000_0000_0080, + Eid = 0x0000_0000_0000_0100, + Envb = 0x0000_0000_0000_0200, + Eqdp = 0x0000_0000_0000_0400, + Eqp = 0x0000_0000_0000_0800, + Essb = 0x0000_0000_0000_1000, + Est = 0x0000_0000_0000_2000, + Evp = 0x0000_0000_0000_4000, + Exd = 0x0000_0000_0000_8000, + Exh = 0x0000_0000_0001_0000, + Exl = 0x0000_0000_0002_0000, + Fdt = 0x0000_0000_0004_0000, + Gfd = 0x0000_0000_0008_0000, + Ggd = 0x0000_0000_0010_0000, + Gmp = 0x0000_0000_0020_0000, + Gzd = 0x0000_0000_0040_0000, + Imc = 0x0000_0000_0080_0000, + Lcb = 0x0000_0000_0100_0000, + Lgb = 0x0000_0000_0200_0000, + Luab = 0x0000_0000_0400_0000, + Lvb = 0x0000_0000_0800_0000, + Mdl = 0x0000_0000_1000_0000, + Mlt = 0x0000_0000_2000_0000, + Mtrl = 0x0000_0000_4000_0000, + Obsb = 0x0000_0000_8000_0000, + Pap = 0x0000_0001_0000_0000, + Pbd = 0x0000_0002_0000_0000, + Pcb = 0x0000_0004_0000_0000, + Phyb = 0x0000_0008_0000_0000, + Plt = 0x0000_0010_0000_0000, + Scd = 0x0000_0020_0000_0000, + Sgb = 0x0000_0040_0000_0000, + Shcd = 0x0000_0080_0000_0000, + Shpk = 0x0000_0100_0000_0000, + Sklb = 0x0000_0200_0000_0000, + Skp = 0x0000_0400_0000_0000, + Stm = 0x0000_0800_0000_0000, + Svb = 0x0000_1000_0000_0000, + Tera = 0x0000_2000_0000_0000, + Tex = 0x0000_4000_0000_0000, + Tmb = 0x0000_8000_0000_0000, + Ugd = 0x0001_0000_0000_0000, + Uld = 0x0002_0000_0000_0000, + Waoe = 0x0004_0000_0000_0000, + Wtd = 0x0008_0000_0000_0000, +} + +[Flags] +public enum ResourceCategoryFlag : ushort +{ + Common = 0x0001, + BgCommon = 0x0002, + Bg = 0x0004, + Cut = 0x0008, + Chara = 0x0010, + Shader = 0x0020, + Ui = 0x0040, + Sound = 0x0080, + Vfx = 0x0100, + UiScript = 0x0200, + Exd = 0x0400, + GameScript = 0x0800, + Music = 0x1000, + SqpackTest = 0x2000, +} + +public static class ResourceExtensions +{ + public static readonly ResourceTypeFlag AllResourceTypes = Enum.GetValues().Aggregate((v, f) => v | f); + public static readonly ResourceCategoryFlag AllResourceCategories = Enum.GetValues().Aggregate((v, f) => v | f); + + public static ResourceTypeFlag ToFlag(this ResourceType type) + => type switch + { + ResourceType.Aet => ResourceTypeFlag.Aet, + ResourceType.Amb => ResourceTypeFlag.Amb, + ResourceType.Atch => ResourceTypeFlag.Atch, + ResourceType.Atex => ResourceTypeFlag.Atex, + ResourceType.Avfx => ResourceTypeFlag.Avfx, + ResourceType.Awt => ResourceTypeFlag.Awt, + ResourceType.Cmp => ResourceTypeFlag.Cmp, + ResourceType.Dic => ResourceTypeFlag.Dic, + ResourceType.Eid => ResourceTypeFlag.Eid, + ResourceType.Envb => ResourceTypeFlag.Envb, + ResourceType.Eqdp => ResourceTypeFlag.Eqdp, + ResourceType.Eqp => ResourceTypeFlag.Eqp, + ResourceType.Essb => ResourceTypeFlag.Essb, + ResourceType.Est => ResourceTypeFlag.Est, + ResourceType.Evp => ResourceTypeFlag.Evp, + ResourceType.Exd => ResourceTypeFlag.Exd, + ResourceType.Exh => ResourceTypeFlag.Exh, + ResourceType.Exl => ResourceTypeFlag.Exl, + ResourceType.Fdt => ResourceTypeFlag.Fdt, + ResourceType.Gfd => ResourceTypeFlag.Gfd, + ResourceType.Ggd => ResourceTypeFlag.Ggd, + ResourceType.Gmp => ResourceTypeFlag.Gmp, + ResourceType.Gzd => ResourceTypeFlag.Gzd, + ResourceType.Imc => ResourceTypeFlag.Imc, + ResourceType.Lcb => ResourceTypeFlag.Lcb, + ResourceType.Lgb => ResourceTypeFlag.Lgb, + ResourceType.Luab => ResourceTypeFlag.Luab, + ResourceType.Lvb => ResourceTypeFlag.Lvb, + ResourceType.Mdl => ResourceTypeFlag.Mdl, + ResourceType.Mlt => ResourceTypeFlag.Mlt, + ResourceType.Mtrl => ResourceTypeFlag.Mtrl, + ResourceType.Obsb => ResourceTypeFlag.Obsb, + ResourceType.Pap => ResourceTypeFlag.Pap, + ResourceType.Pbd => ResourceTypeFlag.Pbd, + ResourceType.Pcb => ResourceTypeFlag.Pcb, + ResourceType.Phyb => ResourceTypeFlag.Phyb, + ResourceType.Plt => ResourceTypeFlag.Plt, + ResourceType.Scd => ResourceTypeFlag.Scd, + ResourceType.Sgb => ResourceTypeFlag.Sgb, + ResourceType.Shcd => ResourceTypeFlag.Shcd, + ResourceType.Shpk => ResourceTypeFlag.Shpk, + ResourceType.Sklb => ResourceTypeFlag.Sklb, + ResourceType.Skp => ResourceTypeFlag.Skp, + ResourceType.Stm => ResourceTypeFlag.Stm, + ResourceType.Svb => ResourceTypeFlag.Svb, + ResourceType.Tera => ResourceTypeFlag.Tera, + ResourceType.Tex => ResourceTypeFlag.Tex, + ResourceType.Tmb => ResourceTypeFlag.Tmb, + ResourceType.Ugd => ResourceTypeFlag.Ugd, + ResourceType.Uld => ResourceTypeFlag.Uld, + ResourceType.Waoe => ResourceTypeFlag.Waoe, + ResourceType.Wtd => ResourceTypeFlag.Wtd, + _ => 0, + }; + + public static bool FitsFlag(this ResourceType type, ResourceTypeFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceCategoryFlag ToFlag(this ResourceCategory type) + => type switch + { + ResourceCategory.Common => ResourceCategoryFlag.Common, + ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, + ResourceCategory.Bg => ResourceCategoryFlag.Bg, + ResourceCategory.Cut => ResourceCategoryFlag.Cut, + ResourceCategory.Chara => ResourceCategoryFlag.Chara, + ResourceCategory.Shader => ResourceCategoryFlag.Shader, + ResourceCategory.Ui => ResourceCategoryFlag.Ui, + ResourceCategory.Sound => ResourceCategoryFlag.Sound, + ResourceCategory.Vfx => ResourceCategoryFlag.Vfx, + ResourceCategory.UiScript => ResourceCategoryFlag.UiScript, + ResourceCategory.Exd => ResourceCategoryFlag.Exd, + ResourceCategory.GameScript => ResourceCategoryFlag.GameScript, + ResourceCategory.Music => ResourceCategoryFlag.Music, + ResourceCategory.SqpackTest => ResourceCategoryFlag.SqpackTest, + _ => 0, + }; + + public static bool FitsFlag(this ResourceCategory type, ResourceCategoryFlag flags) + => (type.ToFlag() & flags) != 0; + + public static ResourceType FromBytes(byte a1, byte a2, byte a3) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 8) + | ByteStringFunctions.AsciiToLower(a3)); + + public static ResourceType FromBytes(byte a1, byte a2, byte a3, byte a4) + => (ResourceType)(((uint)ByteStringFunctions.AsciiToLower(a1) << 24) + | ((uint)ByteStringFunctions.AsciiToLower(a2) << 16) + | ((uint)ByteStringFunctions.AsciiToLower(a3) << 8) + | ByteStringFunctions.AsciiToLower(a4)); + + public static ResourceType FromBytes(char a1, char a2, char a3) + => FromBytes((byte)a1, (byte)a2, (byte)a3); + + public static ResourceType FromBytes(char a1, char a2, char a3, char a4) + => FromBytes((byte)a1, (byte)a2, (byte)a3, (byte)a4); + + public static ResourceType Type(string path) + { + var ext = Path.GetExtension(path.AsSpan()); + ext = ext.Length == 0 ? path.AsSpan() : ext[1..]; + + return ext.Length switch + { + 0 => 0, + 1 => (ResourceType)ext[^1], + 2 => FromBytes('\0', ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), + }; + } + + public static ResourceType Type(ByteString path) + { + var extIdx = path.LastIndexOf((byte)'.'); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); + + return ext.Length switch + { + 0 => 0, + 1 => (ResourceType)ext[^1], + 2 => FromBytes(0, ext[^2], ext[^1]), + 3 => FromBytes(ext[^3], ext[^2], ext[^1]), + _ => FromBytes(ext[^4], ext[^3], ext[^2], ext[^1]), + }; + } + + public static ResourceCategory Category(ByteString path) + { + if (path.Length < 3) + return ResourceCategory.Debug; + + return ByteStringFunctions.AsciiToUpper(path[0]) switch + { + (byte)'C' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'O' => ResourceCategory.Common, + (byte)'U' => ResourceCategory.Cut, + (byte)'H' => ResourceCategory.Chara, + _ => ResourceCategory.Debug, + }, + (byte)'B' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'C' => ResourceCategory.BgCommon, + (byte)'/' => ResourceCategory.Bg, + _ => ResourceCategory.Debug, + }, + (byte)'S' => ByteStringFunctions.AsciiToUpper(path[1]) switch + { + (byte)'H' => ResourceCategory.Shader, + (byte)'O' => ResourceCategory.Sound, + (byte)'Q' => ResourceCategory.SqpackTest, + _ => ResourceCategory.Debug, + }, + (byte)'U' => ByteStringFunctions.AsciiToUpper(path[2]) switch + { + (byte)'/' => ResourceCategory.Ui, + (byte)'S' => ResourceCategory.UiScript, + _ => ResourceCategory.Debug, + }, + (byte)'V' => ResourceCategory.Vfx, + (byte)'E' => ResourceCategory.Exd, + (byte)'G' => ResourceCategory.GameScript, + (byte)'M' => ResourceCategory.Music, + _ => ResourceCategory.Debug, + }; + } +} diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 51612819..616cb2f6 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -4,8 +4,8 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6afaf5d1..40984c6a 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -3,6 +3,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 4494dc77..20713fe7 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 00b06963..a8fd816a 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,8 +1,8 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 5d5e4590..94fdcce4 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index 0b29ee10..ce7d3d4c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -3,8 +3,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; namespace Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 8b4a5d15..5d060d85 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -1,9 +1,9 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index 6dac0e6b..574da240 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,8 +1,8 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceLoading; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0bbe8e66..972e3c55 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index c3327a9e..2fffaedd 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,4 @@ -using Penumbra.GameData.Enums; +using Penumbra.Api.Enums; using Penumbra.String.Classes; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index a8ad9d4f..161e0368 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -12,9 +12,12 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { public readonly string Name; + public readonly int GameObjectIndex; public readonly nint GameObjectAddress; public readonly nint DrawObjectAddress; + public readonly bool LocalPlayerRelated; public readonly bool PlayerRelated; + public readonly bool Networked; public readonly string CollectionName; public readonly List Nodes; public readonly HashSet FlatNodes; @@ -23,15 +26,18 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, nint gameObjectAddress, nint drawObjectAddress, bool playerRelated, string collectionName) + public ResourceTree(string name, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName) { - Name = name; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Name = name; + GameObjectIndex = gameObjectIndex; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + LocalPlayerRelated = localPlayerRelated; + Networked = networked; + PlayerRelated = playerRelated; + CollectionName = collectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } internal unsafe void LoadResources(GlobalResolveContext globalContext) @@ -64,7 +70,7 @@ public class ResourceTree AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); - if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) + if (model->GetModelType() == CharacterBase.ModelType.Human) AddHumanResources(globalContext, (HumanExt*)model); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs new file mode 100644 index 00000000..e7d9abc2 --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -0,0 +1,104 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api.Enums; +using Penumbra.UI; + +namespace Penumbra.Interop.ResourceTree; + +internal static class ResourceTreeApiHelper +{ + public static Dictionary> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees, + bool mergeSameCollection) + => mergeSameCollection ? GetResourcePathDictionariesMerged(resourceTrees) : GetResourcePathDictionariesUnmerged(resourceTrees); + + private static Dictionary> GetResourcePathDictionariesMerged(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + var collections = new Dictionary(4); + var pathDictionaries = new Dictionary>>(4); + + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (collections.ContainsKey(gameObject.ObjectIndex)) + continue; + + collections.Add(gameObject.ObjectIndex, resourceTree.CollectionName); + if (!pathDictionaries.TryGetValue(resourceTree.CollectionName, out var pathDictionary)) + { + pathDictionary = new(); + pathDictionaries.Add(resourceTree.CollectionName, pathDictionary); + } + + CollectResourcePaths(pathDictionary, resourceTree); + } + + var pathRODictionaries = pathDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + + return collections.ToDictionary(pair => pair.Key, pair => pathRODictionaries[pair.Value]); + } + + private static Dictionary> GetResourcePathDictionariesUnmerged(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + var pathDictionaries = new Dictionary>>(4); + + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (pathDictionaries.ContainsKey(gameObject.ObjectIndex)) + continue; + + var pathDictionary = new Dictionary>(); + pathDictionaries.Add(gameObject.ObjectIndex, pathDictionary); + + CollectResourcePaths(pathDictionary, resourceTree); + } + + return pathDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + } + + private static void CollectResourcePaths(Dictionary> pathDictionary, ResourceTree resourceTree) + { + foreach (var node in resourceTree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0) + continue; + + var fullPath = node.FullPath.ToPath(); + if (!pathDictionary.TryGetValue(fullPath, out var gamePaths)) + { + gamePaths = new(); + pathDictionary.Add(fullPath, gamePaths); + } + + foreach (var gamePath in node.PossibleGamePaths) + gamePaths.Add(gamePath.ToString()); + } + } + + public static Dictionary> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + ResourceType type) + { + var resDictionaries = new Dictionary>(4); + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (resDictionaries.ContainsKey(gameObject.ObjectIndex)) + continue; + + var resDictionary = new Dictionary(); + resDictionaries.Add(gameObject.ObjectIndex, resDictionary); + + foreach (var node in resourceTree.FlatNodes) + { + if (node.Type != type) + continue; + if (resDictionary.ContainsKey(node.ResourceHandle)) + continue; + + var fullPath = node.FullPath.ToPath(); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, ChangedItemDrawer.ToApiIcon(node.Icon))); + } + } + + return resDictionaries.ToDictionary(pair => pair.Key, + pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); + } +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a2e29e48..e3418e5b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -27,21 +27,35 @@ public class ResourceTreeFactory _actors = actors; } - public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) - { - var cache = new TreeBuildCache(_objects, _gameData); + private TreeBuildCache CreateTreeBuildCache() + => new(_objects, _gameData, _actors); - return cache.Characters - .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) - .OfType() - .ToArray(); + public IEnumerable GetLocalPlayerRelatedCharacters() + { + var cache = CreateTreeBuildCache(); + + return cache.Characters.Where(cache.IsLocalPlayerRelated); + } + + public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( + bool localPlayerRelatedOnly = false, bool withUIData = true, bool redactExternalPaths = true) + { + var cache = CreateTreeBuildCache(); + var characters = localPlayerRelatedOnly ? cache.Characters.Where(cache.IsLocalPlayerRelated) : cache.Characters; + + foreach (var character in characters) + { + var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); + if (tree != null) + yield return (character, tree); + } } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, bool withUIData = true, bool redactExternalPaths = true) { - var cache = new TreeBuildCache(_objects, _gameData); + var cache = CreateTreeBuildCache(); foreach (var character in characters) { var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); @@ -52,7 +66,7 @@ public class ResourceTreeFactory public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, bool redactExternalPaths = true) - => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths); + => FromCharacter(character, CreateTreeBuildCache(), withUIData, redactExternalPaths); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, bool withUIData = true, bool redactExternalPaths = true) @@ -69,8 +83,10 @@ public class ResourceTreeFactory if (!collectionResolveData.Valid) return null; - var (name, related) = GetCharacterName(character, cache); - var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); + var localPlayerRelated = cache.IsLocalPlayerRelated(character); + var (name, related) = GetCharacterName(character, cache); + var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; + var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 43b25476..60714fbb 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,6 +1,8 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Files; +using Penumbra.Interop.Services; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -8,18 +10,38 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { private readonly IDataManager _dataManager; + private readonly ActorService _actors; private readonly Dictionary _shaderPackages = new(); + private readonly uint _localPlayerId; public readonly List Characters; public readonly Dictionary CharactersById; - public TreeBuildCache(IObjectTable objects, IDataManager dataManager) + public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors) { - _dataManager = dataManager; - Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast().ToList(); + _dataManager = dataManager; + _actors = actors; + Characters = objects.OfType().Where(ch => ch.IsValid()).ToList(); CharactersById = Characters .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) .GroupBy(c => c.ObjectId) .ToDictionary(c => c.Key, c => c.First()); + _localPlayerId = Characters.Count > 0 && Characters[0].ObjectIndex == 0 ? Characters[0].ObjectId : GameObject.InvalidGameObjectId; + } + + public unsafe bool IsLocalPlayerRelated(Character character) + { + if (_localPlayerId == GameObject.InvalidGameObjectId) + return false; + + // Index 0 is the local player, index 1 is the mount/minion/accessory. + if (character.ObjectIndex < 2 || character.ObjectIndex == RedrawService.GPosePlayerIdx) + return true; + + if (!_actors.AwaitedService.FromObject(character, out var owner, true, false, false).IsValid) + return false; + + // Check for SMN/SCH pet, chocobo and other owned NPCs. + return owner != null && owner->ObjectID == _localPlayerId; } /// Try to read a shpk file from the given path and cache it on success. diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 18c88766..17d8d2e0 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index dba113f3..1b78e857 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,8 +1,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index b9200a24..acd6eae9 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -1,3 +1,4 @@ +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index d95c8796..3d8ab1b6 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,3 +1,4 @@ +using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 0140d189..7c2f50c4 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 0fa81a52..27935ffb 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,7 +1,7 @@ +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; -using Penumbra.GameData.Enums; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; using Penumbra.Services; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ec433113..0cda8f59 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -90,6 +90,10 @@ + + + + diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 5f09e584..2d474a83 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -40,8 +40,6 @@ public class ResourceTreeViewer if (!child) return; - var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text); - var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green if (!_task.IsCompleted) { ImGui.NewLine(); @@ -55,17 +53,30 @@ public class ResourceTreeViewer } else if (_task.IsCompletedSuccessfully) { + var debugMode = _config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer)) + var headerColorId = + tree.LocalPlayerRelated ? ColorId.ResTreeLocalPlayer : + tree.PlayerRelated ? ColorId.ResTreePlayer : + tree.Networked ? ColorId.ResTreeNetworked : + ColorId.ResTreeNonNetworked; + using (var c = ImRaii.PushColor(ImGuiCol.Text, headerColorId.Value())) { - if (!ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0)) + var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + if (debugMode) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.HoverTooltip( + $"Object Index: {tree.GameObjectIndex}\nObject Address: 0x{tree.GameObjectAddress:X16}\nDraw Object Address: 0x{tree.DrawObjectAddress:X16}"); + } + if (!isOpen) continue; } using var id = ImRaii.PushId(index); - ImGui.Text($"Collection: {tree.CollectionName}"); + ImGui.TextUnformatted($"Collection: {tree.CollectionName}"); using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -90,7 +101,9 @@ public class ResourceTreeViewer { try { - return _treeFactory.FromObjectTable(); + return _treeFactory.FromObjectTable() + .Select(entry => entry.ResourceTree) + .ToArray(); } finally { diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 91d81ea3..13a5787e 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -15,6 +15,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Services; using Penumbra.UI.Classes; +using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; @@ -311,6 +312,29 @@ public class ChangedItemDrawer : IDisposable _ => "Other", }; + internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Head => ApiChangedItemIcon.Head, + ChangedItemIcon.Body => ApiChangedItemIcon.Body, + ChangedItemIcon.Hands => ApiChangedItemIcon.Hands, + ChangedItemIcon.Legs => ApiChangedItemIcon.Legs, + ChangedItemIcon.Feet => ApiChangedItemIcon.Feet, + ChangedItemIcon.Ears => ApiChangedItemIcon.Ears, + ChangedItemIcon.Neck => ApiChangedItemIcon.Neck, + ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists, + ChangedItemIcon.Finger => ApiChangedItemIcon.Finger, + ChangedItemIcon.Monster => ApiChangedItemIcon.Monster, + ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman, + ChangedItemIcon.Customization => ApiChangedItemIcon.Customization, + ChangedItemIcon.Action => ApiChangedItemIcon.Action, + ChangedItemIcon.Emote => ApiChangedItemIcon.Emote, + ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand, + ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown, + _ => ApiChangedItemIcon.None, + }; + /// Apply Changed Item Counters to the Name if necessary. private static string ChangedItemName(string name, object? data) => data is int counter ? $"{counter} Files Manipulating {name}s" : name; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index ebcff821..0e3b9377 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,10 +24,16 @@ public enum ColorId NoAssignment, SelectorPriority, InGameHighlight, + ResTreeLocalPlayer, + ResTreePlayer, + ResTreeNetworked, + ResTreeNonNetworked, } public static class Colors { + // These are written as 0xAABBGGRR. + public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; @@ -64,6 +70,10 @@ public static class Colors ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), + ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), + ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), + ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), + ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index dea68955..1a25d722 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 781c5fc1..de5a179d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -4,9 +4,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index eb034459..3789baf4 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; +using Penumbra.Enums; using Penumbra.String; namespace Penumbra.UI.ResourceWatcher; From 22966e648d84b26ce3ce39fefa2efde237044b04 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 18 Sep 2023 02:01:22 +0200 Subject: [PATCH 1173/2451] ResourceTree IPC: Remove mergeSameCollection. --- Penumbra/Api/IpcTester.cs | 12 +++---- Penumbra/Api/PenumbraApi.cs | 8 ++--- Penumbra/Api/PenumbraIpcProviders.cs | 4 +-- .../ResourceTree/ResourceTreeApiHelper.cs | 32 +------------------ 4 files changed, 12 insertions(+), 44 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index fb8719f4..86719160 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1408,10 +1408,9 @@ public class IpcTester : IDisposable private readonly DalamudPluginInterface _pi; private readonly IObjectTable _objects; - private string _gameObjectIndices = "0"; - private bool _mergeSameCollection = false; - private ResourceType _type = ResourceType.Mtrl; - private bool _withUIData = false; + private string _gameObjectIndices = "0"; + private ResourceType _type = ResourceType.Mtrl; + private bool _withUIData = false; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; @@ -1431,7 +1430,6 @@ public class IpcTester : IDisposable return; ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); - ImGui.Checkbox("Merge entries that use the same collection", ref _mergeSameCollection); ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); ImGui.Checkbox("Also get names and icons", ref _withUIData); @@ -1443,7 +1441,7 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcePaths")) { var gameObjects = GetSelectedGameObjects(); - var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects, _mergeSameCollection); + var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects); _lastGameObjectResourcePaths = gameObjects .Select(GameObjectToString) @@ -1456,7 +1454,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); if (ImGui.Button("Get##PlayerResourcePaths")) { - _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke(_mergeSameCollection) + _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke() .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1c56b9df..0572d868 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1016,19 +1016,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi }; // @formatter:on - public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects, bool mergeSameCollection) + public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, false, false); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); return Array.ConvertAll(gameObjects, obj => pathDictionaries.TryGetValue(obj, out var pathDict) ? pathDict : null); } - public IReadOnlyDictionary> GetPlayerResourcePaths(bool mergeSameCollection) + public IReadOnlyDictionary> GetPlayerResourcePaths() { var resourceTrees = _resourceTreeFactory.FromObjectTable(true, false, false); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees, mergeSameCollection); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); return pathDictionaries.AsReadOnly(); } diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index c05a7c47..aca57aac 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -119,8 +119,8 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryMod; // Resource Tree - internal readonly FuncProvider?[]> GetGameObjectResourcePaths; - internal readonly FuncProvider>> GetPlayerResourcePaths; + internal readonly FuncProvider?[]> GetGameObjectResourcePaths; + internal readonly FuncProvider>> GetPlayerResourcePaths; internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; internal readonly FuncProvider>> GetPlayerResourcesOfType; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index e7d9abc2..6c1e4d1e 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -6,37 +6,7 @@ namespace Penumbra.Interop.ResourceTree; internal static class ResourceTreeApiHelper { - public static Dictionary> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees, - bool mergeSameCollection) - => mergeSameCollection ? GetResourcePathDictionariesMerged(resourceTrees) : GetResourcePathDictionariesUnmerged(resourceTrees); - - private static Dictionary> GetResourcePathDictionariesMerged(IEnumerable<(Character, ResourceTree)> resourceTrees) - { - var collections = new Dictionary(4); - var pathDictionaries = new Dictionary>>(4); - - foreach (var (gameObject, resourceTree) in resourceTrees) - { - if (collections.ContainsKey(gameObject.ObjectIndex)) - continue; - - collections.Add(gameObject.ObjectIndex, resourceTree.CollectionName); - if (!pathDictionaries.TryGetValue(resourceTree.CollectionName, out var pathDictionary)) - { - pathDictionary = new(); - pathDictionaries.Add(resourceTree.CollectionName, pathDictionary); - } - - CollectResourcePaths(pathDictionary, resourceTree); - } - - var pathRODictionaries = pathDictionaries.ToDictionary(pair => pair.Key, - pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); - - return collections.ToDictionary(pair => pair.Key, pair => pathRODictionaries[pair.Value]); - } - - private static Dictionary> GetResourcePathDictionariesUnmerged(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees) { var pathDictionaries = new Dictionary>>(4); From a241b933ca75424d8d99943b8ccd663d70f15f0d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 18 Sep 2023 12:35:41 +0200 Subject: [PATCH 1174/2451] ResourceTree: Avoid enumerating the whole object table in some cases --- Penumbra/Api/PenumbraApi.cs | 8 ++-- .../ResourceTree/ResourceTreeFactory.cs | 40 +++++++++++-------- .../Interop/ResourceTree/TreeBuildCache.cs | 22 ++++++---- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 7 +++- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0572d868..4a31199c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1019,7 +1019,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, false, false); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); return Array.ConvertAll(gameObjects, obj => pathDictionaries.TryGetValue(obj, out var pathDict) ? pathDict : null); @@ -1027,7 +1027,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary> GetPlayerResourcePaths() { - var resourceTrees = _resourceTreeFactory.FromObjectTable(true, false, false); + var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); return pathDictionaries.AsReadOnly(); @@ -1036,7 +1036,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ushort[] gameObjects, ResourceType type, bool withUIData) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData, false); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUIData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); @@ -1044,7 +1044,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, bool withUIData) { - var resourceTrees = _resourceTreeFactory.FromObjectTable(true, withUIData, false); + var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly | (withUIData ? ResourceTreeFactory.Flags.WithUIData : 0)); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return resDictionaries.AsReadOnly(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index e3418e5b..1d91948d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -27,49 +27,46 @@ public class ResourceTreeFactory _actors = actors; } - private TreeBuildCache CreateTreeBuildCache() - => new(_objects, _gameData, _actors); + private TreeBuildCache CreateTreeBuildCache(bool withCharacters) + => new(_objects, _gameData, _actors, withCharacters); public IEnumerable GetLocalPlayerRelatedCharacters() { - var cache = CreateTreeBuildCache(); + var cache = CreateTreeBuildCache(true); return cache.Characters.Where(cache.IsLocalPlayerRelated); } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( - bool localPlayerRelatedOnly = false, bool withUIData = true, bool redactExternalPaths = true) + Flags flags) { - var cache = CreateTreeBuildCache(); - var characters = localPlayerRelatedOnly ? cache.Characters.Where(cache.IsLocalPlayerRelated) : cache.Characters; + var cache = CreateTreeBuildCache(true); + var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.Characters.Where(cache.IsLocalPlayerRelated) : cache.Characters; foreach (var character in characters) { - var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); + var tree = FromCharacter(character, cache, flags); if (tree != null) yield return (character, tree); } } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( - IEnumerable characters, - bool withUIData = true, bool redactExternalPaths = true) + IEnumerable characters, Flags flags) { - var cache = CreateTreeBuildCache(); + var cache = CreateTreeBuildCache((flags & Flags.WithOwnership) != 0); foreach (var character in characters) { - var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); + var tree = FromCharacter(character, cache, flags); if (tree != null) yield return (character, tree); } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, - bool redactExternalPaths = true) - => FromCharacter(character, CreateTreeBuildCache(), withUIData, redactExternalPaths); + public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, Flags flags) + => FromCharacter(character, CreateTreeBuildCache((flags & Flags.WithOwnership) != 0), flags); - private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, - bool withUIData = true, bool redactExternalPaths = true) + private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, Flags flags) { if (!character.IsValid()) return null; @@ -88,7 +85,7 @@ public class ResourceTreeFactory var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0, (flags & Flags.RedactExternalPaths) != 0); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; @@ -119,4 +116,13 @@ public class ResourceTreeFactory return (name, playerRelated); } + + [Flags] + public enum Flags + { + RedactExternalPaths = 1, + WithUIData = 2, + LocalPlayerRelatedOnly = 4, + WithOwnership = 8, + } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 60714fbb..d889cf5d 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -16,16 +16,24 @@ internal class TreeBuildCache public readonly List Characters; public readonly Dictionary CharactersById; - public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors) + public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors, bool withCharacters) { _dataManager = dataManager; _actors = actors; - Characters = objects.OfType().Where(ch => ch.IsValid()).ToList(); - CharactersById = Characters - .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) - .GroupBy(c => c.ObjectId) - .ToDictionary(c => c.Key, c => c.First()); - _localPlayerId = Characters.Count > 0 && Characters[0].ObjectIndex == 0 ? Characters[0].ObjectId : GameObject.InvalidGameObjectId; + _localPlayerId = objects[0]?.ObjectId ?? GameObject.InvalidGameObjectId; + if (withCharacters) + { + Characters = objects.OfType().Where(ch => ch.IsValid()).ToList(); + CharactersById = Characters + .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) + .GroupBy(c => c.ObjectId) + .ToDictionary(c => c.Key, c => c.First()); + } + else + { + Characters = new(); + CharactersById = new(); + } } public unsafe bool IsLocalPlayerRelated(Character character) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 2d474a83..3901b431 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -9,6 +9,11 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { + private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = + ResourceTreeFactory.Flags.RedactExternalPaths | + ResourceTreeFactory.Flags.WithUIData | + ResourceTreeFactory.Flags.WithOwnership; + private readonly Configuration _config; private readonly ResourceTreeFactory _treeFactory; private readonly ChangedItemDrawer _changedItemDrawer; @@ -101,7 +106,7 @@ public class ResourceTreeViewer { try { - return _treeFactory.FromObjectTable() + return _treeFactory.FromObjectTable(ResourceTreeFactoryFlags) .Select(entry => entry.ResourceTree) .ToArray(); } From ea79469abde85a4d8f8ebfeda3ae9c7334f19bec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 17:06:16 +0200 Subject: [PATCH 1175/2451] Move IPC Arguments around. --- Penumbra/Api/IpcTester.cs | 2 +- Penumbra/Api/PenumbraApi.cs | 10 ++++++---- Penumbra/Api/PenumbraIpcProviders.cs | 2 +- Penumbra/Configuration.cs | 2 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 5 +++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 86719160..0abf0cf5 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1465,7 +1465,7 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcesOfType")) { var gameObjects = GetSelectedGameObjects(); - var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(gameObjects, _type, _withUIData); + var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData, gameObjects); _lastGameObjectResourcesOfType = gameObjects .Select(GameObjectToString) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 4a31199c..49fa40db 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -24,7 +24,6 @@ using Penumbra.Interop.Services; using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; using Penumbra.Interop.ResourceTree; -using System.Collections.Immutable; namespace Penumbra.Api; @@ -1033,7 +1032,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi return pathDictionaries.AsReadOnly(); } - public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ushort[] gameObjects, ResourceType type, bool withUIData) + public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUIData, + params ushort[] gameObjects) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUIData : 0); @@ -1042,9 +1042,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); } - public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, bool withUIData) + public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, + bool withUIData) { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly | (withUIData ? ResourceTreeFactory.Flags.WithUIData : 0)); + var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUIData ? ResourceTreeFactory.Flags.WithUIData : 0)); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return resDictionaries.AsReadOnly(); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index aca57aac..87de6a0c 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -121,7 +121,7 @@ public class PenumbraIpcProviders : IDisposable // Resource Tree internal readonly FuncProvider?[]> GetGameObjectResourcePaths; internal readonly FuncProvider>> GetPlayerResourcePaths; - internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; + internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; internal readonly FuncProvider>> GetPlayerResourcesOfType; public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 0206d0ae..7c9ae665 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -6,7 +6,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; +using Penumbra.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; using Penumbra.Mods; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 60339893..ee66ca86 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -3,6 +3,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Services; +using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; namespace Penumbra.UI.Tabs; @@ -17,7 +18,7 @@ public class ConfigTabBar : IDisposable public readonly EffectiveTab Effective; public readonly DebugTab Debug; public readonly ResourceTab Resource; - public readonly ResourceWatcher Watcher; + public readonly Watcher Watcher; public readonly OnScreenTab OnScreenTab; public readonly ITab[] Tabs; @@ -26,7 +27,7 @@ public class ConfigTabBar : IDisposable public TabType SelectTab = TabType.None; public ConfigTabBar(CommunicatorService communicator, SettingsTab settings, ModsTab mods, CollectionsTab collections, - ChangedItemsTab changedItems, EffectiveTab effective, DebugTab debug, ResourceTab resource, ResourceWatcher watcher, + ChangedItemsTab changedItems, EffectiveTab effective, DebugTab debug, ResourceTab resource, Watcher watcher, OnScreenTab onScreenTab) { _communicator = communicator; From 3905d5b9763ed77ddf05c61bb5b4807964c23fd8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 17:14:16 +0200 Subject: [PATCH 1176/2451] Rename ResourceType file. --- Penumbra/Enums/{ResourceType.cs => ResourceTypeFlag.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Penumbra/Enums/{ResourceType.cs => ResourceTypeFlag.cs} (100%) diff --git a/Penumbra/Enums/ResourceType.cs b/Penumbra/Enums/ResourceTypeFlag.cs similarity index 100% rename from Penumbra/Enums/ResourceType.cs rename to Penumbra/Enums/ResourceTypeFlag.cs From 4ffb69ea31fa84f3b238cb38e96046c17257bb9d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 17:14:42 +0200 Subject: [PATCH 1177/2451] Remove enums folder from csproj?! --- Penumbra/Penumbra.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0cda8f59..ec433113 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -90,10 +90,6 @@ - - - - From fee99dd17e67a0b2a836d821eb96a1ce0ed71b63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 17:18:11 +0200 Subject: [PATCH 1178/2451] Fix params bug. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 22846625..2d34adfb 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 22846625192884c6e9f5ec4429fb579875b519e9 +Subproject commit 2d34adfbd105b10ed456d40aebdd54ad7fc73df7 From 5506dcc3f317ebb09c7486471dbdc560047e07a8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 17:20:19 +0200 Subject: [PATCH 1179/2451] Api nuget version. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 2d34adfb..6cc41dc1 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2d34adfbd105b10ed456d40aebdd54ad7fc73df7 +Subproject commit 6cc41dc15241417fd72dc46ebca48cf7d2092f53 From 5067ab2bb297f68aaaecff7e4255a60431eaeb04 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Sep 2023 18:18:23 +0200 Subject: [PATCH 1180/2451] Add load state to resource watcher. --- Penumbra/Interop/Structs/ResourceHandle.cs | 12 +++ Penumbra/UI/ResourceWatcher/Record.cs | 6 ++ .../ResourceWatcher/ResourceWatcherTable.cs | 89 +++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 1b78e857..3cceb949 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -34,6 +34,15 @@ public unsafe struct ShaderPackageResourceHandle public ShaderPackage* ShaderPackage; } +public enum LoadState : byte +{ + Success = 0x07, + Async = 0x03, + Failure = 0x09, + FailedSubResource = 0x0A, + None = 0xFF, +} + [StructLayout(LayoutKind.Explicit)] public unsafe struct ResourceHandle { @@ -99,6 +108,9 @@ public unsafe struct ResourceHandle [FieldOffset(0x58)] public int FileNameLength; + [FieldOffset(0xA9)] + public LoadState LoadState; + [FieldOffset(0xAC)] public uint RefCount; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 1a25d722..0fc51f26 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -30,6 +30,7 @@ internal unsafe struct Record public OptionalBool Synchronously; public OptionalBool ReturnValue; public OptionalBool CustomLoad; + public LoadState LoadState; public static Record CreateRequest(ByteString path, bool sync) => new() @@ -47,6 +48,7 @@ internal unsafe struct Record ReturnValue = OptionalBool.Null, CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, + LoadState = LoadState.None, }; public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) @@ -67,6 +69,7 @@ internal unsafe struct Record ReturnValue = OptionalBool.Null, CustomLoad = false, AssociatedGameObject = associatedGameObject, + LoadState = handle->LoadState, }; } @@ -87,6 +90,7 @@ internal unsafe struct Record ReturnValue = OptionalBool.Null, CustomLoad = true, AssociatedGameObject = associatedGameObject, + LoadState = handle->LoadState, }; public static Record CreateDestruction(ResourceHandle* handle) @@ -107,6 +111,7 @@ internal unsafe struct Record ReturnValue = OptionalBool.Null, CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, + LoadState = handle->LoadState, }; } @@ -126,5 +131,6 @@ internal unsafe struct Record ReturnValue = ret, CustomLoad = custom, AssociatedGameObject = string.Empty, + LoadState = handle->LoadState, }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 3789baf4..89dd42bb 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -5,7 +5,9 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; using Penumbra.Enums; +using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; @@ -24,6 +26,7 @@ internal sealed class ResourceWatcherTable : Table new ResourceCategoryColumn(config) { Label = "Category" }, new ResourceTypeColumn(config) { Label = "Type" }, new HandleColumn { Label = "Resource" }, + new LoadStateColumn { Label = "State" }, new RefCountColumn { Label = "#Ref" }, new DateColumn { Label = "Time" } ) @@ -241,6 +244,92 @@ internal sealed class ResourceWatcherTable : Table } } + private sealed class LoadStateColumn : ColumnFlags + { + public override float Width + => 50 * UiHelpers.Scale; + + [Flags] + public enum LoadStateFlag : byte + { + Success = 0x01, + Async = 0x02, + Failed = 0x04, + FailedSub = 0x08, + Unknown = 0x10, + None = 0xFF, + } + + protected override string[] Names + => new[] + { + "Loaded", + "Loading", + "Failed", + "Dependency Failed", + "Unknown", + "None", + }; + + public LoadStateColumn() + { + AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); + _filterValue = AllFlags; + } + + private LoadStateFlag _filterValue; + + public override LoadStateFlag FilterValue + => _filterValue; + + protected override void SetValue(LoadStateFlag value, bool enable) + { + if (enable) + _filterValue |= value; + else + _filterValue &= ~value; + } + + public override bool FilterFunc(Record item) + => item.LoadState switch + { + LoadState.None => FilterValue.HasFlag(LoadStateFlag.None), + LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success), + LoadState.Async => FilterValue.HasFlag(LoadStateFlag.Async), + LoadState.Failure => FilterValue.HasFlag(LoadStateFlag.Failed), + LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub), + _ => FilterValue.HasFlag(LoadStateFlag.Unknown), + }; + + public override void DrawColumn(Record item, int _) + { + if (item.LoadState == LoadState.None) + return; + + var (icon, color, tt) = item.LoadState switch + { + LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), + $"Successfully loaded ({(byte)item.LoadState})."), + LoadState.Async => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), + LoadState.Failure => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), + $"Failed to load ({(byte)item.LoadState})."), + LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), + $"Dependencies failed to load ({(byte)item.LoadState})."), + _ => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Unknown state ({(byte)item.LoadState})."), + }; + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + using var c = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(icon.ToIconString()); + } + + ImGuiUtil.HoverTooltip(tt); + } + + public override int Compare(Record lhs, Record rhs) + => lhs.LoadState.CompareTo(rhs.LoadState); + } + private sealed class HandleColumn : ColumnString { public override float Width From 69012e5ecda8fcfd195f32d8ebedc211ab5ea1a7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 18 Sep 2023 17:51:30 +0000 Subject: [PATCH 1181/2451] [CI] Updating repo.json for testing_0.7.3.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4eafa05f..386fb07a 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.10", + "TestingAssemblyVersion": "0.7.3.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From f02a37b939dd54d6dc62de544460d6f599204fff Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 19 Sep 2023 01:32:31 +0200 Subject: [PATCH 1182/2451] ResourceTree: Reverse-resolve in bulk --- Penumbra/Api/IpcTester.cs | 39 +++-- .../Interop/ResourceTree/ResolveContext.cs | 153 +++++++----------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 105 ++++++------ Penumbra/Interop/ResourceTree/ResourceTree.cs | 64 ++++++-- .../ResourceTree/ResourceTreeFactory.cs | 120 +++++++++++++- 5 files changed, 311 insertions(+), 170 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 0abf0cf5..de9ab5a7 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -17,6 +17,7 @@ using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; using Penumbra.GameData.Enums; +using System.Diagnostics; namespace Penumbra.Api; @@ -1407,6 +1408,7 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; private readonly IObjectTable _objects; + private readonly Stopwatch _stopwatch = new(); private string _gameObjectIndices = "0"; private ResourceType _type = ResourceType.Mtrl; @@ -1416,6 +1418,7 @@ public class IpcTester : IDisposable private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private TimeSpan _lastCallDuration; public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) { @@ -1441,8 +1444,11 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcePaths")) { var gameObjects = GetSelectedGameObjects(); - var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects); + var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(_pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(gameObjects); + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcePaths = gameObjects .Select(GameObjectToString) .Zip(resourcePaths) @@ -1454,7 +1460,12 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); if (ImGui.Button("Get##PlayerResourcePaths")) { - _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke() + var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(_pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcePaths = resourcePaths .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); @@ -1465,8 +1476,11 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcesOfType")) { var gameObjects = GetSelectedGameObjects(); - var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData, gameObjects); + var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUIData, gameObjects); + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcesOfType = gameObjects .Select(GameObjectToString) .Zip(resourcesOfType) @@ -1478,21 +1492,26 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); if (ImGui.Button("Get##PlayerResourcesOfType")) { - _lastPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData) + var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(_pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUIData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcesOfType = resourcesOfType .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); } - DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths); - DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths); + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); - DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType); - DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType); + DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); } - private static void DrawPopup(string popupId, ref T? result, Action drawResult) where T : class + private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class { ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); using var popup = ImRaii.Popup(popupId); @@ -1510,6 +1529,8 @@ public class IpcTester : IDisposable drawResult(result); + ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) { result = null; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 972e3c55..b6ce42d4 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,7 +2,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -13,17 +12,17 @@ using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, - ModCollection Collection, int Skeleton, bool WithUiData, bool RedactExternalPaths) +internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, + int Skeleton, bool WithUiData) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUiData, RedactExternalPaths, Nodes, slot, equipment); + => new(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment); } -internal record ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, - int Skeleton, bool WithUiData, bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData, + Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -76,52 +75,28 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie Utf8GamePath gamePath, bool @internal) { var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; - if (fullPath.InternalName.IsEmpty) - fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), - GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this) + { + GamePath = gamePath, + FullPath = fullPath, + }; if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); return node; } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, - bool withName) + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal) { var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; if (fullPath.InternalName.IsEmpty) return null; - var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); - fullPath = FilterFullPath(fullPath); - - if (gamePaths.Count > 1) - gamePaths = Filter(gamePaths); - - if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], - fullPath, - GetResourceHandleLength(handle), @internal); - - Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); - foreach (var gamePath in gamePaths) - Penumbra.Log.Information($"Game path: {gamePath}"); - - return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), - @internal); - } - - public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) - { - var raceSexIdStr = gr.ToRaceCode(); - var path = $"chara/human/c{raceSexIdStr}/skeleton/base/b0001/skl_c{raceSexIdStr}b0001.sklb"; - - if (!Utf8GamePath.FromString(path, out var gamePath)) - return null; - - return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false); + return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this) + { + FullPath = fullPath, + }; } public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) @@ -129,16 +104,10 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)imc, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true); if (node == null) return null; - if (WithUiData) - { - var uiData = GuessModelUIData(node.GamePath); - node = node.WithUIData(uiData.PrependName("IMC: ")); - } - Nodes.Add((nint)imc, node); return node; @@ -149,7 +118,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUiData); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false); if (node != null) Nodes.Add((nint)tex, node); @@ -164,23 +133,20 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false); if (node == null) return null; - if (WithUiData) - node = node.WithUIData(GuessModelUIData(node.GamePath)); - for (var i = 0; i < mdl->MaterialCount; i++) { var mtrl = (Material*)mdl->Materials[i]; var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) - // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithUiData - ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) - ?? $"Material #{i}", mtrlNode.Icon) - : mtrlNode); + { + if (WithUiData) + mtrlNode.FallbackName = $"Material #{i}"; + node.Children.Add(mtrlNode); + } } Nodes.Add((nint)mdl->ResourceHandle, node); @@ -190,18 +156,20 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { - static ushort GetTextureIndex(ushort texFlags) + static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet alreadyVisitedSamplerIds) { - if ((texFlags & 0x001F) != 0x001F) + if ((texFlags & 0x001F) != 0x001F && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[texFlags & 0x001F].Id)) return (ushort)(texFlags & 0x001F); - if ((texFlags & 0x03E0) != 0x03E0) + if ((texFlags & 0x03E0) != 0x03E0 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 5) & 0x001F].Id)) return (ushort)((texFlags >> 5) & 0x001F); + if ((texFlags & 0x7C00) != 0x7C00 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 10) & 0x001F].Id)) + return (ushort)((texFlags >> 10) & 0x001F); - return (ushort)((texFlags >> 10) & 0x001F); + return 0x001F; } - static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) - => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) + => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) ? p.Id : null; @@ -217,16 +185,21 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false, WithUiData); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithUiData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + { + if (WithUiData) + shpkNode.Name = "Shader Package"; + node.Children.Add(shpkNode); + } var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->NumTex; i++) { var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, @@ -239,26 +212,26 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie string? name = null; if (shpk != null) { - var index = GetTextureIndex(resource->TexSpace[i].Flags); + var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds); uint? samplerId; if (index != 0x001F) samplerId = mtrl->Textures[index].Id; else - samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); + samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds); if (samplerId.HasValue) { + alreadyProcessedSamplerIds.Add(samplerId.Value); var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); if (samplerCrc.HasValue) name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } } - node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); - } - else - { - node.Children.Add(texNode); + texNode = texNode.Clone(); + texNode.Name = name ?? $"Texture #{i}"; } + + node.Children.Add(texNode); } Nodes.Add((nint)resource, node); @@ -274,8 +247,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, - WithUiData); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -295,31 +267,18 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, - WithUiData); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true); if (node != null) { if (WithUiData) - node = node.WithUIData("Skeleton Parameters", node.Icon); + node.FallbackName = "Skeleton Parameters"; Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } return node; } - private FullPath FilterFullPath(FullPath fullPath) - { - if (!fullPath.IsRooted) - return fullPath; - - var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); - if (!RedactExternalPaths || relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) - return fullPath.Exists ? fullPath : FullPath.Empty; - - return FullPath.Empty; - } - - private List Filter(List gamePaths) + internal List FilterGamePaths(List gamePaths) { var filtered = new List(gamePaths.Count); foreach (var path in gamePaths) @@ -356,7 +315,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie } : false; - private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) + internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); // Weapons intentionally left out. @@ -371,7 +330,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie _ => string.Empty, } + item.Name.ToString(); - return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } var dataFromPath = GuessUIDataFromPath(gamePath); @@ -379,11 +338,11 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie return dataFromPath; return isEquipment - ? new ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) - : new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) + : new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) + internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -391,10 +350,10 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 2fffaedd..dfca5805 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -4,80 +4,89 @@ using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; namespace Penumbra.Interop.ResourceTree; -public class ResourceNode +public class ResourceNode : ICloneable { - public readonly string? Name; - public readonly ChangedItemIcon Icon; + public string? Name; + public string? FallbackName; + public ChangedItemIcon Icon; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; - public readonly Utf8GamePath GamePath; - public readonly Utf8GamePath[] PossibleGamePaths; - public readonly FullPath FullPath; + public Utf8GamePath[] PossibleGamePaths; + public FullPath FullPath; public readonly ulong Length; public readonly bool Internal; public readonly List Children; + internal ResolveContext? ResolveContext; - public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, - ulong length, bool @internal) + public Utf8GamePath GamePath { - Name = uiData.Name; - Icon = uiData.Icon; - Type = type; - ObjectAddress = objectAddress; - ResourceHandle = resourceHandle; - GamePath = gamePath; - PossibleGamePaths = new[] + get => PossibleGamePaths.Length == 1 ? PossibleGamePaths[0] : Utf8GamePath.Empty; + set { - gamePath, - }; - FullPath = fullPath; - Length = length; - Internal = @internal; - Children = new List(); + if (value.IsEmpty) + PossibleGamePaths = Array.Empty(); + else + PossibleGamePaths = new[] { value }; + } } - public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, - FullPath fullPath, - ulong length, bool @internal) + internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext) { - Name = uiData.Name; - Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; - GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; - PossibleGamePaths = possibleGamePaths; - FullPath = fullPath; + PossibleGamePaths = Array.Empty(); Length = length; Internal = @internal; Children = new List(); + ResolveContext = resolveContext; } - private ResourceNode(UIData uiData, ResourceNode originalResourceNode) + private ResourceNode(ResourceNode other) { - Name = uiData.Name; - Icon = uiData.Icon; - Type = originalResourceNode.Type; - ObjectAddress = originalResourceNode.ObjectAddress; - ResourceHandle = originalResourceNode.ResourceHandle; - GamePath = originalResourceNode.GamePath; - PossibleGamePaths = originalResourceNode.PossibleGamePaths; - FullPath = originalResourceNode.FullPath; - Length = originalResourceNode.Length; - Internal = originalResourceNode.Internal; - Children = originalResourceNode.Children; + Name = other.Name; + FallbackName = other.FallbackName; + Icon = other.Icon; + Type = other.Type; + ObjectAddress = other.ObjectAddress; + ResourceHandle = other.ResourceHandle; + PossibleGamePaths = other.PossibleGamePaths; + FullPath = other.FullPath; + Length = other.Length; + Internal = other.Internal; + Children = other.Children; + ResolveContext = other.ResolveContext; } - public ResourceNode WithUIData(string? name, ChangedItemIcon icon) - => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new UIData(name, icon), this); + public ResourceNode Clone() + => new(this); - public ResourceNode WithUIData(UIData uiData) - => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); + object ICloneable.Clone() + => Clone(); - public readonly record struct UIData(string? Name, ChangedItemIcon Icon) + public void ProcessPostfix(Action action, ResourceNode? parent) { - public readonly UIData PrependName(string prefix) - => Name == null ? this : new UIData(prefix + Name, Icon); + foreach (var child in Children) + child.ProcessPostfix(action, this); + action(this, parent); + } + + public void SetUiData(UiData uiData) + { + Name = uiData.Name; + Icon = uiData.Icon; + } + + public void PrependName(string prefix) + { + if (Name != null) + Name = prefix + Name; + } + + public readonly record struct UiData(string? Name, ChangedItemIcon Icon) + { + public readonly UiData PrependName(string prefix) + => Name == null ? this : new UiData(prefix + Name, Icon); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 161e0368..bc2cca26 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,10 +1,10 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; namespace Penumbra.Interop.ResourceTree; @@ -40,6 +40,12 @@ public class ResourceTree FlatNodes = new HashSet(); } + public void ProcessPostfix(Action action) + { + foreach (var node in Nodes) + node.ProcessPostfix(action, null); + } + internal unsafe void LoadResources(GlobalResolveContext globalContext) { var character = (Character*)GameObjectAddress; @@ -60,12 +66,20 @@ public class ResourceTree var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithUiData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); + { + if (globalContext.WithUiData) + imcNode.FallbackName = $"IMC #{i}"; + Nodes.Add(imcNode); + } var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"Model #{i}"; + Nodes.Add(mdlNode); + } } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -96,16 +110,20 @@ public class ResourceTree var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithUiData - ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) - : imcNode); + { + if (globalContext.WithUiData) + imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; + subObjectNodes.Add(imcNode); + } var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithUiData - ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) - : mdlNode); + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; + subObjectNodes.Add(mdlNode); + } } AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); @@ -121,13 +139,27 @@ public class ResourceTree var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithUiData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); + { + if (globalContext.WithUiData) + { + decalNode = decalNode.Clone(); + decalNode.FallbackName = "Face Decal"; + decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(decalNode); + } var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithUiData - ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) - : legacyDecalNode); + { + if (globalContext.WithUiData) + { + legacyDecalNode = legacyDecalNode.Clone(); + legacyDecalNode.FallbackName = "Legacy Body Decal"; + legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(legacyDecalNode); + } } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -139,7 +171,11 @@ public class ResourceTree { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + { + if (context.WithUiData) + sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; + nodes.Add(sklbNode); + } } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 1d91948d..33a8de0f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,9 +1,12 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop.PathResolving; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -84,13 +87,126 @@ public class ResourceTreeFactory var (name, related) = GetCharacterName(character, cache); var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); - var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0, (flags & Flags.RedactExternalPaths) != 0); + var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache, + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); + tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node)); + + ResolveGamePaths(tree, collectionResolveData.ModCollection); + if (globalContext.WithUiData) + ResolveUiData(tree); + FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null); + Cleanup(tree); + return tree; } + private static void ResolveGamePaths(ResourceTree tree, ModCollection collection) + { + var forwardSet = new HashSet(); + var reverseSet = new HashSet(); + foreach (var node in tree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) + reverseSet.Add(node.FullPath.ToPath()); + else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) + forwardSet.Add(node.GamePath); + } + + var forwardDictionary = forwardSet.ToDictionary(path => path, collection.ResolvePath); + var reverseArray = reverseSet.ToArray(); + var reverseResolvedArray = collection.ReverseResolvePaths(reverseArray); + var reverseDictionary = reverseArray.Zip(reverseResolvedArray).ToDictionary(pair => pair.First, pair => pair.Second); + + foreach (var node in tree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) + { + if (reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) + { + var resolvedList = resolvedSet.ToList(); + if (resolvedList.Count > 1) + { + var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList); + if (filteredList.Count > 0) + resolvedList = filteredList; + } + if (resolvedList.Count != 1) + { + Penumbra.Log.Information($"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); + foreach (var gamePath in resolvedList) + Penumbra.Log.Information($"Game path: {gamePath}"); + } + node.PossibleGamePaths = resolvedList.ToArray(); + } + } + else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) + { + if (forwardDictionary.TryGetValue(node.GamePath, out var resolved)) + node.FullPath = resolved ?? new FullPath(node.GamePath); + } + } + } + + private static void ResolveUiData(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + if (node.Name != null || node.PossibleGamePaths.Length == 0) + continue; + + var gamePath = node.PossibleGamePaths[0]; + node.SetUiData(node.Type switch + { + ResourceType.Imc => node.ResolveContext!.GuessModelUIData(gamePath).PrependName("IMC: "), + ResourceType.Mdl => node.ResolveContext!.GuessModelUIData(gamePath), + _ => node.ResolveContext!.GuessUIDataFromPath(gamePath), + }); + } + + tree.ProcessPostfix((node, parent) => + { + if (node.Name == parent?.Name) + node.Name = null; + }); + } + + private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) + { + static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) + { + if (!fullPath.IsRooted) + return true; + + if (onlyWithinPath != null) + { + var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); + if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + return false; + } + + return fullPath.Exists; + } + + foreach (var node in tree.FlatNodes) + { + if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPath = FullPath.Empty; + } + } + + private static void Cleanup(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + node.Name ??= node.FallbackName; + + node.FallbackName = null; + node.ResolveContext = null; + } + } + private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache) { From 808d7ab017f26fe0de1711572be508495558a40b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Sep 2023 20:18:53 +0200 Subject: [PATCH 1183/2451] Add CalculateHeight Hook --- Penumbra.GameData | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f004e069..7c483764 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f004e069824a1588244e06080b32bab170f78077 +Subproject commit 7c483764678c6edb5efd55f056aeaecae144d5fe diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 40984c6a..a68e376b 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,5 +1,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; @@ -36,7 +38,7 @@ namespace Penumbra.Interop.PathResolving; // RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05" // RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF" // they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, -// ChangeCustomize and RspSetupCharacter, which is hooked here. +// ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. public unsafe class MetaState : IDisposable @@ -74,6 +76,7 @@ public unsafe class MetaState : IDisposable _setupVisorHook.Enable(); _rspSetupCharacterHook.Enable(); _changeCustomize.Enable(); + _calculateHeightHook.Enable(); _gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase; _gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated; } @@ -118,6 +121,7 @@ public unsafe class MetaState : IDisposable _setupVisorHook.Dispose(); _rspSetupCharacterHook.Dispose(); _changeCustomize.Dispose(); + _calculateHeightHook.Dispose(); _gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase; _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; } @@ -242,6 +246,19 @@ public unsafe class MetaState : IDisposable } } + private delegate ulong CalculateHeightDelegate(Character* character); + + // TODO: use client structs + [Signature(Sigs.CalculateHeight, DetourName = nameof(CalculateHeightDetour))] + private readonly Hook _calculateHeightHook = null!; + + private ulong CalculateHeightDetour(Character* character) + { + var resolveData = _collectionResolver.IdentifyCollection((GameObject*)character, true); + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); + return _calculateHeightHook.Original(character); + } + private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment); [Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))] From c29d0a5a4c5bdc837a65ba4e0e24f6768ff37e57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Sep 2023 21:44:49 +0200 Subject: [PATCH 1184/2451] Remove some allocations from resource tree. --- Penumbra/Api/PenumbraApi.cs | 4 +- .../Collections/ModCollection.Cache.Access.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 62 ++++++++++--------- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 49fa40db..dabe4207 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1036,7 +1036,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi params ushort[] gameObjects) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUIData : 0); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); @@ -1046,7 +1046,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi bool withUIData) { var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUIData ? ResourceTreeFactory.Flags.WithUIData : 0)); + | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return resDictionaries.AsReadOnly(); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index d788d0bd..2d094970 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -37,7 +37,7 @@ public partial class ModCollection public IEnumerable ReverseResolvePath(FullPath path) => _cache?.ReverseResolvePath(path) ?? Array.Empty(); - public HashSet[] ReverseResolvePaths(string[] paths) + public HashSet[] ReverseResolvePaths(IReadOnlyCollection paths) => _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray(); public FullPath? ResolvePath(Utf8GamePath path) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b6ce42d4..55893cab 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -278,7 +278,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return node; } - internal List FilterGamePaths(List gamePaths) + internal List FilterGamePaths(IReadOnlyCollection gamePaths) { var filtered = new List(gamePaths.Count); foreach (var path in gamePaths) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 33a8de0f..bd0138c4 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -84,11 +84,12 @@ public class ResourceTreeFactory return null; var localPlayerRelated = cache.IsLocalPlayerRelated(character); - var (name, related) = GetCharacterName(character, cache); - var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; - var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); + var (name, related) = GetCharacterName(character, cache); + var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; + var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, + networked, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUiData) != 0); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node)); @@ -104,42 +105,47 @@ public class ResourceTreeFactory private static void ResolveGamePaths(ResourceTree tree, ModCollection collection) { - var forwardSet = new HashSet(); - var reverseSet = new HashSet(); + var forwardDictionary = new Dictionary(); + var reverseDictionary = new Dictionary>(); foreach (var node in tree.FlatNodes) { if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) - reverseSet.Add(node.FullPath.ToPath()); + reverseDictionary.TryAdd(node.FullPath.ToPath(), null!); else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) - forwardSet.Add(node.GamePath); + forwardDictionary.TryAdd(node.GamePath, null); } - var forwardDictionary = forwardSet.ToDictionary(path => path, collection.ResolvePath); - var reverseArray = reverseSet.ToArray(); - var reverseResolvedArray = collection.ReverseResolvePaths(reverseArray); - var reverseDictionary = reverseArray.Zip(reverseResolvedArray).ToDictionary(pair => pair.First, pair => pair.Second); + foreach (var key in forwardDictionary.Keys) + forwardDictionary[key] = collection.ResolvePath(key); + + var reverseResolvedArray = collection.ReverseResolvePaths(reverseDictionary.Keys); + foreach (var (key, set) in reverseDictionary.Keys.Zip(reverseResolvedArray)) + reverseDictionary[key] = set; foreach (var node in tree.FlatNodes) { if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) { - if (reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) + if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) + continue; + + IReadOnlyCollection resolvedList = resolvedSet; + if (resolvedList.Count > 1) { - var resolvedList = resolvedSet.ToList(); - if (resolvedList.Count > 1) - { - var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList); - if (filteredList.Count > 0) - resolvedList = filteredList; - } - if (resolvedList.Count != 1) - { - Penumbra.Log.Information($"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); - foreach (var gamePath in resolvedList) - Penumbra.Log.Information($"Game path: {gamePath}"); - } - node.PossibleGamePaths = resolvedList.ToArray(); + var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList); + if (filteredList.Count > 0) + resolvedList = filteredList; } + + if (resolvedList.Count != 1) + { + Penumbra.Log.Debug( + $"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); + foreach (var gamePath in resolvedList) + Penumbra.Log.Debug($"Game path: {gamePath}"); + } + + node.PossibleGamePaths = resolvedList.ToArray(); } else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) { @@ -237,7 +243,7 @@ public class ResourceTreeFactory public enum Flags { RedactExternalPaths = 1, - WithUIData = 2, + WithUiData = 2, LocalPlayerRelatedOnly = 4, WithOwnership = 8, } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3901b431..39728cd4 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -11,7 +11,7 @@ public class ResourceTreeViewer { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | - ResourceTreeFactory.Flags.WithUIData | + ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly Configuration _config; From 348480ed6846eb5e97e8b911c5244efd95bdddba Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Sep 2023 21:46:15 +0200 Subject: [PATCH 1185/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 21333f3e..4ea1eb7b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 21333f3e2f3908d4f4c7dbb0b4bff5e5b7d1f64a +Subproject commit 4ea1eb7b7d39a74465291164563ad5d29fa038e4 From 1760efb4775f1ee38af709056be2f3cd22166c4a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Sep 2023 21:50:43 +0200 Subject: [PATCH 1186/2451] Fix ambiguous reference for no fucking reason. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 4ea1eb7b..6eb6ab15 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4ea1eb7b7d39a74465291164563ad5d29fa038e4 +Subproject commit 6eb6ab156c1bc1cb61700f19768f3fa6c11e1e04 From 5a24d9155b1526e2c49d61e409dc7800df199a22 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 19 Sep 2023 19:52:53 +0000 Subject: [PATCH 1187/2451] [CI] Updating repo.json for testing_0.7.3.12 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 386fb07a..6eeaf412 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.11", + "TestingAssemblyVersion": "0.7.3.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.12/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 69388689ac20d92de722a328fc061d64a448e330 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 20 Sep 2023 01:53:10 +0200 Subject: [PATCH 1188/2451] Material Editor: Extend live preview. --- .../Interop/MaterialPreview/MaterialInfo.cs | 87 ++++++++----------- .../ModEditWindow.Materials.MtrlTab.cs | 5 +- .../ModEditWindow.QuickImport.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 7 +- 4 files changed, 44 insertions(+), 56 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 3f02e7e8..26e809f9 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -9,41 +9,20 @@ namespace Penumbra.Interop.MaterialPreview; public enum DrawObjectType { - PlayerCharacter, - PlayerMainhand, - PlayerOffhand, - PlayerVfx, - MinionCharacter, - MinionUnk1, - MinionUnk2, - MinionUnk3, + Character, + Mainhand, + Offhand, + Vfx, }; -public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot) +public readonly record struct MaterialInfo(ushort ObjectIndex, DrawObjectType Type, int ModelSlot, int MaterialSlot) { public nint GetCharacter(IObjectTable objects) - => GetCharacter(Type, objects); - - public static nint GetCharacter(DrawObjectType type, IObjectTable objects) - => type switch - { - DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0), - DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0), - DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0), - DrawObjectType.PlayerVfx => objects.GetObjectAddress(0), - DrawObjectType.MinionCharacter => objects.GetObjectAddress(1), - DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1), - DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1), - DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1), - _ => nint.Zero, - }; + => objects.GetObjectAddress(ObjectIndex); public nint GetDrawObject(nint address) => GetDrawObject(Type, address); - public static nint GetDrawObject(DrawObjectType type, IObjectTable objects) - => GetDrawObject(type, GetCharacter(type, objects)); - public static unsafe nint GetDrawObject(DrawObjectType type, nint address) { var gameObject = (Character*)address; @@ -52,18 +31,17 @@ public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, i return type switch { - DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(), - DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1), - DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1), - DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), - DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(), - DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1), - DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1), - DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1), - _ => nint.Zero, + DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.Mainhand => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.Offhand => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.Vfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), + _ => nint.Zero, }; } + public unsafe Material* GetDrawObjectMaterial(IObjectTable objects) + => GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects))); + public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject) { if (drawObject == null) @@ -82,33 +60,42 @@ public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, i return model->Materials[MaterialSlot]; } - public static unsafe List FindMaterials(IObjectTable objects, string materialPath) + public static unsafe List FindMaterials(IEnumerable gameObjects, string materialPath) { var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; var result = new List(Enum.GetValues().Length); - foreach (var type in Enum.GetValues()) + foreach (var objectPtr in gameObjects) { - var drawObject = (CharacterBase*)GetDrawObject(type, objects); - if (drawObject == null) + var gameObject = (Character*)objectPtr; + if (gameObject == null) continue; - for (var i = 0; i < drawObject->SlotCount; ++i) + var index = gameObject->GameObject.ObjectIndex; + + foreach (var type in Enum.GetValues()) { - var model = drawObject->Models[i]; - if (model == null) + var drawObject = (CharacterBase*)GetDrawObject(type, objectPtr); + if (drawObject == null) continue; - for (var j = 0; j < model->MaterialCount; ++j) + for (var i = 0; i < drawObject->SlotCount; ++i) { - var material = model->Materials[j]; - if (material == null) + var model = drawObject->Models[i]; + if (model == null) continue; - var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); - if (path == needle) - result.Add(new MaterialInfo(type, i, j)); + for (var j = 0; j < model->MaterialCount; ++j) + { + var material = model->Materials[j]; + if (material == null) + continue; + + var mtrlHandle = material->MaterialResourceHandle; + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path == needle) + result.Add(new MaterialInfo(index, type, i, j)); + } } } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index ad83843d..ebe980d7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -454,13 +454,12 @@ public partial class ModEditWindow { UnbindFromMaterialInstances(); - var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath); + var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), FilePath); var foundMaterials = new HashSet(); foreach (var materialInfo in instances) { - var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects); - var material = materialInfo.GetDrawObjectMaterial(drawObject); + var material = materialInfo.GetDrawObjectMaterial(_edit._dalamud.Objects); if (foundMaterials.Contains((nint)material)) continue; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 2f64f82a..48d617db 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -14,6 +14,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; private readonly Dictionary _quickImportWritables = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 745b412b..febb01cb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -576,9 +576,10 @@ public partial class ModEditWindow : Window, IDisposable _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); - _center = new CombinedTexture(_left, _right); - _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); - _quickImportViewer = + _center = new CombinedTexture(_left, _right); + _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); + _resourceTreeFactory = resourceTreeFactory; + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } From 40b6c6022a23bf1365cf58f75f9d887d1cad9c2b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Sep 2023 18:51:07 +0200 Subject: [PATCH 1189/2451] Add automatic restore from backup for sort_order and active_collections for now. --- OtterGui | 2 +- .../Collections/Manager/ActiveCollections.cs | 20 +++++++--------- Penumbra/Mods/Manager/ModFileSystem.cs | 6 +++-- Penumbra/Services/BackupService.cs | 23 +++++++++++++++++++ 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/OtterGui b/OtterGui index 6eb6ab15..0f8a8664 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6eb6ab156c1bc1cb61700f19768f3fa6c11e1e04 +Subproject commit 0f8a866491b246e819e0618a43078170686780ab diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index bae95885..7e6d691e 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -408,19 +408,15 @@ public class ActiveCollections : ISavable, IDisposable public static bool Load(FilenameService fileNames, out JObject ret) { var file = fileNames.ActiveCollectionsFile; - if (File.Exists(file)) - try - { - ret = JObject.Parse(File.ReadAllText(file)); - return true; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); - } + var jObj = BackupService.GetJObjectForFile(fileNames, file); + if (jObj == null) + { + ret = new JObject(); + return false; + } - ret = new JObject(); - return false; + ret = jObj; + return true; } public string RedundancyCheck(CollectionType type, ActorIdentifier id) diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 013d0e40..2d8b90ab 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Services; @@ -60,8 +62,8 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable // Used on construction and on mod rediscoveries. private void Reload() { - // TODO - if (Load(new FileInfo(_saveService.FileNames.FilesystemFile), _modManager, ModToIdentifier, ModToName)) + var jObj = BackupService.GetJObjectForFile(_saveService.FileNames, _saveService.FileNames.FilesystemFile); + if (Load(jObj, _modManager, ModToIdentifier, ModToName)) _saveService.ImmediateSave(this); Penumbra.Log.Debug("Reloaded mod filesystem."); diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index 0c13217a..f6e2c3e4 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Util; @@ -23,4 +24,26 @@ public class BackupService list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); return list; } + + /// Try to parse a file to JObject and check backups if this does not succeed. + public static JObject? GetJObjectForFile(FilenameService fileNames, string fileName) + { + JObject? ret = null; + if (!File.Exists(fileName)) + return ret; + + try + { + var text = File.ReadAllText(fileName); + ret = JObject.Parse(text); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Failed to load {fileName}, trying to restore from backup:\n{ex}"); + Backup.TryGetFile(new DirectoryInfo(fileNames.ConfigDirectory), fileName, out ret, out var messages, JObject.Parse); + Penumbra.Chat.NotificationMessage(messages); + } + + return ret; + } } From 11bf0d29987d2144c12aa740b9e75c5c6f69a24c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Sep 2023 02:05:52 +0200 Subject: [PATCH 1190/2451] Optimize ResourceTree somewhat. --- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester.cs | 9 +- .../Interop/MaterialPreview/MaterialInfo.cs | 7 +- .../ResourceTree/ResourceTreeFactory.cs | 46 ++++---- .../Interop/ResourceTree/TreeBuildCache.cs | 107 +++++++++++++----- 5 files changed, 108 insertions(+), 63 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 7c483764..ef403be9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 7c483764678c6edb5efd55f056aeaecae144d5fe +Subproject commit ef403be979bfac5ef805030ce76066151d36f112 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index de9ab5a7..7766b5af 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -18,6 +18,7 @@ using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; using Penumbra.GameData.Enums; using System.Diagnostics; +using Penumbra.GameData.Structs; namespace Penumbra.Api; @@ -1450,7 +1451,7 @@ public class IpcTester : IDisposable _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcePaths = gameObjects - .Select(GameObjectToString) + .Select(i => GameObjectToString(i)) .Zip(resourcePaths) .ToArray(); @@ -1482,7 +1483,7 @@ public class IpcTester : IDisposable _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcesOfType = gameObjects - .Select(GameObjectToString) + .Select(i => GameObjectToString(i)) .Zip(resourcesOfType) .ToArray(); @@ -1630,9 +1631,9 @@ public class IpcTester : IDisposable .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) .ToArray(); - private unsafe string GameObjectToString(ushort gameObjectIndex) + private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) { - var gameObject = _objects[gameObjectIndex]; + var gameObject = _objects[gameObjectIndex.Index]; return gameObject != null ? $"[{gameObjectIndex}] {gameObject.Name} ({gameObject.ObjectKind})" diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 26e809f9..7dd6f983 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.String; @@ -15,10 +16,10 @@ public enum DrawObjectType Vfx, }; -public readonly record struct MaterialInfo(ushort ObjectIndex, DrawObjectType Type, int ModelSlot, int MaterialSlot) +public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectType Type, int ModelSlot, int MaterialSlot) { public nint GetCharacter(IObjectTable objects) - => objects.GetObjectAddress(ObjectIndex); + => objects.GetObjectAddress(ObjectIndex.Index); public nint GetDrawObject(nint address) => GetDrawObject(Type, address); @@ -71,7 +72,7 @@ public readonly record struct MaterialInfo(ushort ObjectIndex, DrawObjectType Ty if (gameObject == null) continue; - var index = gameObject->GameObject.ObjectIndex; + var index = (ObjectIndex) gameObject->GameObject.ObjectIndex; foreach (var type in Enum.GetValues()) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index bd0138c4..6353d5b5 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -30,21 +30,20 @@ public class ResourceTreeFactory _actors = actors; } - private TreeBuildCache CreateTreeBuildCache(bool withCharacters) - => new(_objects, _gameData, _actors, withCharacters); + private TreeBuildCache CreateTreeBuildCache() + => new(_objects, _gameData, _actors); public IEnumerable GetLocalPlayerRelatedCharacters() { - var cache = CreateTreeBuildCache(true); - - return cache.Characters.Where(cache.IsLocalPlayerRelated); + var cache = CreateTreeBuildCache(); + return cache.GetLocalPlayerRelatedCharacters(); } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( Flags flags) { - var cache = CreateTreeBuildCache(true); - var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.Characters.Where(cache.IsLocalPlayerRelated) : cache.Characters; + var cache = CreateTreeBuildCache(); + var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters(); foreach (var character in characters) { @@ -57,7 +56,7 @@ public class ResourceTreeFactory public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, Flags flags) { - var cache = CreateTreeBuildCache((flags & Flags.WithOwnership) != 0); + var cache = CreateTreeBuildCache(); foreach (var character in characters) { var tree = FromCharacter(character, cache, flags); @@ -67,7 +66,7 @@ public class ResourceTreeFactory } public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, Flags flags) - => FromCharacter(character, CreateTreeBuildCache((flags & Flags.WithOwnership) != 0), flags); + => FromCharacter(character, CreateTreeBuildCache(), flags); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, Flags flags) { @@ -136,7 +135,7 @@ public class ResourceTreeFactory if (filteredList.Count > 0) resolvedList = filteredList; } - + if (resolvedList.Count != 1) { Penumbra.Log.Debug( @@ -216,27 +215,22 @@ public class ResourceTreeFactory private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache) { - var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false); - string name; - bool playerRelated; + var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false); switch (identifier.Type) { - case IdentifierType.Player: - name = identifier.PlayerName.ToString(); - playerRelated = true; - break; - case IdentifierType.Owned when cache.CharactersById.TryGetValue(owner->ObjectID, out var ownerChara): - var ownerName = GetCharacterName(ownerChara, cache); - name = $"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})"; - playerRelated = ownerName.PlayerRelated; - break; - default: - name = $"{character.Name} ({identifier.Kind.ToName()})"; - playerRelated = false; + case IdentifierType.Player: return (identifier.PlayerName.ToString(), true); + case IdentifierType.Owned: + var ownerChara = _objects.CreateObjectReference((nint)owner) as Dalamud.Game.ClientState.Objects.Types.Character; + if (ownerChara != null) + { + var ownerName = GetCharacterName(ownerChara, cache); + return ($"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})", ownerName.PlayerRelated); + } + break; } - return (name, playerRelated); + return ($"{character.Name} ({identifier.Kind.ToName()})", false); } [Flags] diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index d889cf5d..5f724b14 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,55 +1,104 @@ +using System.Diagnostics.CodeAnalysis; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; +using Penumbra.GameData.Actors; using Penumbra.GameData.Files; -using Penumbra.Interop.Services; +using Penumbra.GameData.Structs; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal class TreeBuildCache +internal readonly struct TreeBuildCache { private readonly IDataManager _dataManager; private readonly ActorService _actors; private readonly Dictionary _shaderPackages = new(); - private readonly uint _localPlayerId; - public readonly List Characters; - public readonly Dictionary CharactersById; + private readonly IObjectTable _objects; - public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors, bool withCharacters) + public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors) { - _dataManager = dataManager; - _actors = actors; - _localPlayerId = objects[0]?.ObjectId ?? GameObject.InvalidGameObjectId; - if (withCharacters) - { - Characters = objects.OfType().Where(ch => ch.IsValid()).ToList(); - CharactersById = Characters - .Where(c => c.ObjectId != GameObject.InvalidGameObjectId) - .GroupBy(c => c.ObjectId) - .ToDictionary(c => c.Key, c => c.First()); - } - else - { - Characters = new(); - CharactersById = new(); - } + _dataManager = dataManager; + _objects = objects; + _actors = actors; } public unsafe bool IsLocalPlayerRelated(Character character) { - if (_localPlayerId == GameObject.InvalidGameObjectId) + var player = _objects[0]; + if (player == null) return false; - // Index 0 is the local player, index 1 is the mount/minion/accessory. - if (character.ObjectIndex < 2 || character.ObjectIndex == RedrawService.GPosePlayerIdx) - return true; + var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)character.Address; + var parent = _actors.AwaitedService.ToCutsceneParent(gameObject->ObjectIndex); + var actualIndex = parent >= 0 ? (ushort)parent : gameObject->ObjectIndex; + return actualIndex switch + { + < 2 => true, + < (int)ScreenActor.CutsceneStart => gameObject->OwnerID == player.ObjectId, + _ => false, + }; + } - if (!_actors.AwaitedService.FromObject(character, out var owner, true, false, false).IsValid) + public IEnumerable GetCharacters() + => _objects.OfType(); + + public IEnumerable GetLocalPlayerRelatedCharacters() + { + var player = _objects[0]; + if (player == null) + yield break; + + yield return (Character)player; + + var minion = _objects[1]; + if (minion != null) + yield return (Character)minion; + + var playerId = player.ObjectId; + for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) + { + if (_objects[i] is Character owned && owned.OwnerId == playerId) + yield return owned; + } + + for (var i = ObjectIndex.CutsceneStart.Index; i < ObjectIndex.CharacterScreen.Index; ++i) + { + var character = _objects[i] as Character; + if (character == null) + continue; + + var parent = _actors.AwaitedService.ToCutsceneParent(i); + if (parent < 0) + continue; + + if (parent is 0 or 1 || _objects[parent]?.OwnerId == playerId) + yield return character; + } + } + + private unsafe ByteString GetPlayerName(GameObject player) + { + var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)player.Address; + return new ByteString(gameObject->Name); + } + + private unsafe bool GetOwnedId(ByteString playerName, uint playerId, int idx, [NotNullWhen(true)] out Character? character) + { + character = _objects[idx] as Character; + if (character == null) return false; - // Check for SMN/SCH pet, chocobo and other owned NPCs. - return owner != null && owner->ObjectID == _localPlayerId; + var actorId = _actors.AwaitedService.FromObject(character, out var owner, true, true, true); + if (!actorId.IsValid) + return false; + if (owner != null && owner->OwnerID != playerId) + return false; + if (actorId.Type is not IdentifierType.Player || !actorId.PlayerName.Equals(playerName)) + return false; + + return true; } /// Try to read a shpk file from the given path and cache it on success. From 3f439bacb2264a72b1388bd1960333ad3fc7bd78 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Sep 2023 02:15:23 +0200 Subject: [PATCH 1191/2451] Extract remaining global usings for System libs. --- Penumbra/Api/IpcTester.cs | 2 -- Penumbra/Api/PenumbraApi.cs | 1 - Penumbra/Collections/Cache/ImcCache.cs | 1 - Penumbra/Collections/Cache/MetaCache.cs | 1 - Penumbra/Collections/Manager/CollectionStorage.cs | 1 - Penumbra/Collections/Manager/IndividualCollections.Access.cs | 1 - Penumbra/Collections/Manager/IndividualCollections.cs | 1 - Penumbra/Collections/Manager/TempCollectionManager.cs | 1 - Penumbra/Collections/ModCollection.Cache.Access.cs | 1 - Penumbra/GlobalUsings.cs | 5 +++++ Penumbra/Import/Structs/MetaFileInfo.cs | 1 - Penumbra/Import/TexToolsImport.cs | 2 -- Penumbra/Import/TexToolsImporter.Archives.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Import/TexToolsMeta.Export.cs | 1 - Penumbra/Import/TexToolsMeta.cs | 1 - Penumbra/Interop/PathResolving/PathResolver.cs | 1 - Penumbra/Interop/ResourceLoading/CreateFileWHook.cs | 1 - Penumbra/Interop/ResourceTree/TreeBuildCache.cs | 1 - Penumbra/Mods/Editor/FileRegistry.cs | 1 - Penumbra/Mods/Editor/MdlMaterialEditor.cs | 2 -- Penumbra/Mods/Editor/ModBackup.cs | 1 - Penumbra/Mods/ItemSwap/ItemSwap.cs | 2 -- Penumbra/Mods/Manager/ModFileSystem.cs | 2 -- Penumbra/Mods/Manager/ModImportManager.cs | 1 - Penumbra/Mods/Manager/ModMigration.cs | 1 - Penumbra/Mods/Manager/ModStorage.cs | 1 - Penumbra/Mods/ModCreator.cs | 2 -- Penumbra/Penumbra.cs | 1 - .../AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs | 1 - Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs | 1 - Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs | 1 - Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 1 - Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 - Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 1 - Penumbra/Util/PenumbraSqPackStream.cs | 1 - 37 files changed, 7 insertions(+), 42 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7766b5af..148d4481 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -4,7 +4,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; -using System.Globalization; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -17,7 +16,6 @@ using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; using Penumbra.GameData.Enums; -using System.Diagnostics; using Penumbra.GameData.Structs; namespace Penumbra.Api; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index dabe4207..10b5b6bd 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -7,7 +7,6 @@ using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Compression; using Penumbra.Api.Enums; diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 05756e12..3b865d4b 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index d2dd48f8..8eb7a5a0 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Penumbra.GameData.Enums; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index eb230e9e..e50b9bdb 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 489e2f72..680f8b32 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index ed3c3d4b..4fe1e829 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 133a0990..d0edf19b 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 2d094970..36e0fd98 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,7 +1,6 @@ using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.Mods; -using System.Diagnostics.CodeAnalysis; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/GlobalUsings.cs b/Penumbra/GlobalUsings.cs index b1e6218c..51ba9ce5 100644 --- a/Penumbra/GlobalUsings.cs +++ b/Penumbra/GlobalUsings.cs @@ -5,12 +5,17 @@ global using System.Collections; global using System.Collections.Concurrent; global using System.Collections.Generic; global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; global using System.IO; +global using System.IO.Compression; global using System.Linq; global using System.Numerics; global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; global using System.Security.Cryptography; +global using System.Text; +global using System.Text.RegularExpressions; global using System.Threading; global using System.Threading.Tasks; diff --git a/Penumbra/Import/Structs/MetaFileInfo.cs b/Penumbra/Import/Structs/MetaFileInfo.cs index 81b869d9..f7c9b419 100644 --- a/Penumbra/Import/Structs/MetaFileInfo.cs +++ b/Penumbra/Import/Structs/MetaFileInfo.cs @@ -1,5 +1,4 @@ using Penumbra.GameData.Enums; -using System.Text.RegularExpressions; using Penumbra.GameData; namespace Penumbra.Import.Structs; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index ad61398f..3f3304b8 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,8 +1,6 @@ -using System.Text; using Newtonsoft.Json; using OtterGui.Compression; using Penumbra.Import.Structs; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using FileMode = System.IO.FileMode; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 3b67ac50..6ddafdd7 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -7,9 +7,9 @@ using Penumbra.Mods; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; -using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Readers; +using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 73b5d976..dbe76ae3 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,7 +4,7 @@ using Penumbra.Import.Structs; using Penumbra.Mods; using Penumbra.Mods.Subclasses; using Penumbra.Util; -using SharpCompress.Archives.Zip; +using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 46636362..90ffaf60 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -1,4 +1,3 @@ -using System.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index a188975c..1108c965 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,4 +1,3 @@ -using System.Text; using Penumbra.GameData; using Penumbra.Import.Structs; using Penumbra.Meta; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 20713fe7..12e5e280 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index 6b751db7..ca9c2577 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -1,4 +1,3 @@ -using System.Text; using Dalamud.Hooking; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 5f724b14..9614e9aa 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 0aa70e61..791778e3 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 1c1a10b4..8881ac4b 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,5 +1,3 @@ -using System.Text; -using System.Text.RegularExpressions; using OtterGui; using OtterGui.Compression; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 8de93bcc..994ca0b5 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -1,4 +1,3 @@ -using System.IO.Compression; using OtterGui.Tasks; using Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 7c2f50c4..90bee553 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 013d0e40..73efe5d9 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index cc91dfa6..96cf146b 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Dalamud.Interface.Internal.Notifications; using Penumbra.Import; using Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index ddd88a72..452da366 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 377bb4a7..83d20969 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using OtterGui.Classes; using OtterGui.Widgets; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 89d35cd2..236f1539 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -1,5 +1,3 @@ -using System.Text; -using System.Text.RegularExpressions; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 019355af..2cca1789 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,3 @@ -using System.Text; using Dalamud.Plugin; using ImGuiNET; using Lumina.Excel.GeneratedSheets; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs index 7b80920d..1f5db38e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -1,4 +1,3 @@ -using System.Globalization; using ImGuiNET; using OtterGui.Raii; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index a5746ec7..25869d9c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,4 +1,3 @@ -using System.Text; using Dalamud.Interface; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 5d74dc33..b95ba393 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -3,7 +3,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; -using System.Globalization; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index e475f47f..6b867b27 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,4 +1,3 @@ -using System.Text; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; using ImGuiNET; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 1ee0a128..a3b17848 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index febb01cb..c659ada0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,3 @@ -using System.Text; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.DragDrop; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index de5a179d..000e50db 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index d913a019..562eca91 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -1,4 +1,3 @@ -using System.IO.Compression; using Lumina.Data.Structs; using Lumina.Extensions; From 8aebd441a1867e824acb029f0d8b6d42b923e3a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 23 Sep 2023 14:25:32 +0200 Subject: [PATCH 1192/2451] Add option to disable conflicts from conflict panel. --- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 38737274..7fd0ae77 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -38,6 +38,8 @@ public class ModPanelConflictsTab : ITab { if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) _selector.SelectByValue(otherMod); + var hovered = ImGui.IsItemHovered(); + var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); ImGui.SameLine(); using (var color = ImRaii.PushColor(ImGuiCol.Text, @@ -47,6 +49,16 @@ public class ModPanelConflictsTab : ITab ? conflict.Mod2.Priority : _collectionManager.Active.Current[conflict.Mod2.Index].Settings!.Priority; ImGui.TextUnformatted($"(Priority {priority})"); + hovered |= ImGui.IsItemHovered(); + rightClicked |= ImGui.IsItemClicked(ImGuiMouseButton.Right); + } + + if (conflict.Mod2 is Mod otherMod2) + { + if (hovered) + ImGui.SetTooltip("Click to jump to mod, Control + Right-Click to disable mod."); + if (rightClicked && ImGui.GetIO().KeyCtrl) + _collectionManager.Editor.SetModState(_collectionManager.Active.Current, otherMod2, false); } using var indent = ImRaii.PushIndent(30f); From 4c73453b4cd4ef7364674a5e5b879953166bab14 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 23 Sep 2023 12:30:49 +0000 Subject: [PATCH 1193/2451] [CI] Updating repo.json for testing_0.7.3.13 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6eeaf412..60f0978a 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.12", + "TestingAssemblyVersion": "0.7.3.13", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.13/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 613092998532893cbe0291690d27ae7dfca2cd6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Sep 2023 14:16:09 +0200 Subject: [PATCH 1194/2451] Update API. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 6cc41dc1..9472b6e3 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 6cc41dc15241417fd72dc46ebca48cf7d2092f53 +Subproject commit 9472b6e327109216368c3dc1720159f5295bdb13 From 677d44442b8692d35160a5bbaa34a960d1cbeece Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Sep 2023 14:18:45 +0200 Subject: [PATCH 1195/2451] Enable reset of substitutions. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index fb966fe8..498c25e3 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -39,11 +39,10 @@ public class DalamudSubstitutionProvider : IDisposable public void ResetSubstitutions(IEnumerable paths) { - // TODO fix - //var transformed = paths - // .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) - // .Select(p => p.ToString()); - //_substitution.InvalidatePaths(transformed); + var transformed = paths + .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) + .Select(p => p.ToString()); + _substitution.InvalidatePaths(transformed); } public void Enable() From efdebeca54eb8f95b6020da0ceaa4f4a78382a69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Sep 2023 15:36:22 +0200 Subject: [PATCH 1196/2451] Add 0.8.0.0 Changelog --- OtterGui | 2 +- Penumbra/UI/Changelog.cs | 83 +++++++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/OtterGui b/OtterGui index 0f8a8664..05f7fba0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0f8a866491b246e819e0618a43078170686780ab +Subproject commit 05f7fba04156e81c099c87348632ecb03c877730 diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index ed62bf83..8a2b0fcb 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -41,10 +41,67 @@ public class PenumbraChangelog Add7_1_2(Changelog); Add7_2_0(Changelog); Add7_3_0(Changelog); + Add8_0_0(Changelog); } #region Changelogs + private static void Add8_0_0(Changelog log) + => log.NextVersion("Version 0.8.0.0") + .RegisterEntry( + "Penumbra now uses Windows' transparent file system compression by default (on Windows systems). You can disable this functionality in the settings.") + .RegisterImportant("You can retroactively compress your existing mods in the settings via the press of a button, too.", 1) + .RegisterEntry( + "In our tests, this not only was able to reduce storage space by 30-60%, it even decreased loading times since less I/O had to take place.", + 1) + .RegisterEntry("Added emotes to changed item identification.") + .RegisterEntry( + "Added quick select buttons to switch to the current interface collection or the collection applying to the current player character in the mods tab, reworked their text and tooltips slightly.") + .RegisterHighlight("Drag & Drop of multiple mods and folders at once is now supported by holding Control while clicking them.") + .RegisterEntry("You can now disable conflicting mods from the Conflicts panel via Control + Right-click.") + .RegisterEntry("Added checks for your deletion-modifiers for restoring mods from backups or deleting backups.") + .RegisterEntry( + "Penumbra now should automatically try to restore your custom sort order (mod folders) and your active collections from backups if they fail to load. No guarantees though.") + .RegisterEntry("The resource watcher now displays a column providing load state information of resources.") + .RegisterEntry( + "Custom RSP scaling outside of the collection assigned to Base should now be respected for emotes that adjust your stance on height differences.") + .RegisterEntry( + "Mods that replace the skin shaders will not cause visual glitches like loss of head shadows or Free Company crest tattoos anymore (by Ny).") + .RegisterEntry("The Material editor has been improved (by Ny):") + .RegisterHighlight( + "Live-Preview for materials yourself or entities owned by you are currently using, so you can see color set edits in real time.", + 1) + .RegisterEntry( + "Colors on the color table of a material can be highlighted on yourself or entities owned by you by hovering a button.", 1) + .RegisterEntry("The color table has improved color accuracy.", 1) + .RegisterEntry("Materials with non-dyable color tables can be made dyable, and vice-versa.", 1) + .RegisterEntry("The 'Advanced Shader Resources' section has been split apart into dedicated sections.", 1) + .RegisterEntry( + "Addition and removal of shader keys, textures, constants and a color table has been automated following shader requirements and can not be done manually anymore.", + 1) + .RegisterEntry("Plain English names and tooltips can now be displayed instead of hexadecimal identifiers or code names by providing dev-kit files installed via certain mods.", 1) + .RegisterEntry("The Texture editor has been improved (by Ny):") + .RegisterHighlight("The overlay texture can now be combined in several ways and automatically resized to match the input texture.", + 1) + .RegisterEntry("New color manipulation options have been added.", 1) + .RegisterEntry("Modifications to the selected texture can now be saved in-place.", 1) + .RegisterEntry("The On-Screen tab has been improved (by Ny):") + .RegisterEntry("The character list will load more quickly.", 1) + .RegisterEntry("It is now able to deal with characters under transformation effects.", 1) + .RegisterEntry( + "The headers are now color-coded to distinguish between you and other players, and between NPCs that are handled locally or on the server. Colors are customizable.", + 1) + .RegisterEntry("More file types will be recognized and shown.", 1) + .RegisterEntry("The actual paths for game files will be displayed and copied correctly.", 1) + .RegisterEntry("The Shader editor has been improved (by Ny):") + .RegisterEntry( + "New sections 'Shader Resources' and 'Shader Selection' have been added, expanding on some data that was in 'Further Content' before.", + 1) + .RegisterEntry("A fail-safe mode for shader decompilation on platforms that do not fully support it has been added.", 1) + .RegisterEntry("Fixed invalid game paths generated for variants of customizations.") + .RegisterEntry("Lots of minor improvements across the codebase.") + .RegisterEntry("Some unnamed mounts were made available for actor identification. (0.7.3.2)"); + private static void Add7_3_0(Changelog log) => log.NextVersion("Version 0.7.3.0") .RegisterEntry( @@ -124,7 +181,7 @@ public class PenumbraChangelog private static void Add7_1_0(Changelog log) => log.NextVersion("Version 0.7.1.0") .RegisterEntry("Updated for patch 6.4 - there may be some oversights on edge cases, but I could not find any issues myself.") - .RegisterHighlight( + .RegisterImportant( "This update changed some Dragoon skills that were moving the player character before to not do that anymore. If you have any mods that applied to those skills, please make sure that they do not contain any redirections for .tmb files. If skills that should no longer move your character still do that for some reason, this is detectable by the server.", 1) .RegisterEntry( @@ -171,7 +228,7 @@ public class PenumbraChangelog private static void Add7_0_0(Changelog log) => log.NextVersion("Version 0.7.0.0") - .RegisterHighlight( + .RegisterImportant( "The entire backend was reworked (this is still in progress). While this does not come with a lot of functionality changes, basically every file and functionality was touched.") .RegisterEntry( "This may have (re-)introduced some bugs that have not yet been noticed despite a long testing period - there are not many users of the testing branch.", @@ -375,10 +432,10 @@ public class PenumbraChangelog .RegisterEntry("You can now specify individual collections for players (by name) of specific worlds or any world.", 1) .RegisterEntry("You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", 1) - .RegisterHighlight( + .RegisterImportant( "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", 1) - .RegisterHighlight( + .RegisterImportant( "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", 1) .RegisterEntry("You can also manually sort your Individual Collections by drag and drop now.", 1) @@ -424,7 +481,7 @@ public class PenumbraChangelog .RegisterEntry( "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", 1) - .RegisterHighlight( + .RegisterImportant( "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", 1); @@ -472,7 +529,7 @@ public class PenumbraChangelog private static void Add5_8_7(Changelog log) => log.NextVersion("Version 0.5.8.7") .RegisterEntry("Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7).") - .RegisterHighlight( + .RegisterImportant( "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", 1); @@ -482,11 +539,11 @@ public class PenumbraChangelog .RegisterEntry("Added an Interface Collection assignment.") .RegisterEntry("All your UI mods will have to be in the interface collection.", 1) .RegisterEntry("Files that are categorized as UI files by the game will only check for redirections in this collection.", 1) - .RegisterHighlight( + .RegisterImportant( "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1) .RegisterEntry("New API / IPC for the Interface Collection added.", 1) - .RegisterHighlight("API / IPC consumers should verify whether they need to change resolving to the new collection.", 1) - .RegisterHighlight( + .RegisterImportant("API / IPC consumers should verify whether they need to change resolving to the new collection.", 1) + .RegisterImportant( "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being.") .RegisterEntry( "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured.") @@ -499,7 +556,7 @@ public class PenumbraChangelog .RegisterEntry("Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1) .RegisterEntry("Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1) .RegisterEntry("Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1) - .RegisterHighlight( + .RegisterImportant( "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", 1) .RegisterEntry( @@ -524,9 +581,9 @@ public class PenumbraChangelog => log.NextVersion("Version 0.5.7.0") .RegisterEntry("Added a Changelog!") .RegisterEntry("Files in the UI category will no longer be deduplicated for the moment.") - .RegisterHighlight("If you experience UI-related crashes, please re-import your UI mods.", 1) + .RegisterImportant("If you experience UI-related crashes, please re-import your UI mods.", 1) .RegisterEntry("This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1) - .RegisterHighlight( + .RegisterImportant( "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", 1) .RegisterEntry( @@ -534,7 +591,7 @@ public class PenumbraChangelog .RegisterEntry( "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", 1) - .RegisterHighlight( + .RegisterImportant( "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", 1) .RegisterEntry("Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1) From 6ca8ad2385d0b3eec2de35150a40657fe3343077 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 26 Sep 2023 13:46:33 +0000 Subject: [PATCH 1197/2451] [CI] Updating repo.json for 0.8.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 60f0978a..1fc24a13 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.7.3.2", - "TestingAssemblyVersion": "0.7.3.13", + "AssemblyVersion": "0.8.0.0", + "TestingAssemblyVersion": "0.8.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 8, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.7.3.13/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.7.3.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 6799bdbb0381dc4f473576ab9dec1a49f1c3cfe4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 27 Sep 2023 03:15:18 +0200 Subject: [PATCH 1198/2451] Material Editor: Allow intentional 0 gloss --- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index cccc43ee..f1e48200 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -347,10 +347,11 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f") + float glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") && FixFloat(ref tmpFloat, row.GlossStrength)) { - row.GlossStrength = Math.Max(tmpFloat, HalfEpsilon); + row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); ret = true; tab.UpdateColorTableRowPreview(rowIdx); } From 929db5c1a47d69c4a1966ddfe54bf7e107b97894 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Sep 2023 15:52:42 +0200 Subject: [PATCH 1199/2451] Make renaming search paths in context more clear. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 05f7fba0..45117034 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 05f7fba04156e81c099c87348632ecb03c877730 +Subproject commit 4511703462faa6d3126a52e29b1c509bd275599c diff --git a/Penumbra.GameData b/Penumbra.GameData index ef403be9..d400686c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ef403be979bfac5ef805030ce76066151d36f112 +Subproject commit d400686c8bfb0b9f72fa0c3301d1fe1df9be525b diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 3a7a7343..39fc7a0e 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -65,6 +65,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); SubscribeRightClickMain(() => ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); SubscribeRightClickMain(() => ClearQuickMove(2, _config.QuickMoveFolder3, () => {_config.QuickMoveFolder3 = string.Empty; _config.Save();}), 130); + UnsubscribeRightClickLeaf(RenameLeaf); + SubscribeRightClickLeaf(RenameLeafMod, 1000); AddButton(AddNewModButton, 0); AddButton(AddImportModButton, 1); AddButton(AddHelpButton, 2); @@ -268,6 +270,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Wed, 27 Sep 2023 15:59:42 +0200 Subject: [PATCH 1200/2451] Oops. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 45117034..9b431c1f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4511703462faa6d3126a52e29b1c509bd275599c +Subproject commit 9b431c1f491c0739132da65da08bc092d7ff79da From 50f6de78092e36f6ee6b7906cb06b362539a21d4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Sep 2023 17:26:39 +0200 Subject: [PATCH 1201/2451] Update API Level. --- Penumbra/Penumbra.json | 4 ++-- repo.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index cf7170d8..d1682985 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -7,9 +7,9 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 8, + "DalamudApiLevel": 9, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" -} \ No newline at end of file +} diff --git a/repo.json b/repo.json index 1fc24a13..639eb985 100644 --- a/repo.json +++ b/repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "0.8.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 8, + "DalamudApiLevel": 9, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 21d503a8cdab3b5b267654d185bd616287f9751a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Sep 2023 18:12:27 +0200 Subject: [PATCH 1202/2451] Update for API 9 --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester.cs | 2 +- .../Cache/CollectionCacheManager.cs | 4 +-- Penumbra/CommandHandler.cs | 6 ++-- Penumbra/Import/Textures/BaseImage.cs | 2 +- .../Textures/CombinedTexture.Manipulation.cs | 12 +++---- Penumbra/Import/Textures/TexFileParser.cs | 14 ++++---- Penumbra/Import/Textures/Texture.cs | 12 +++---- Penumbra/Import/Textures/TextureDrawer.cs | 4 +-- Penumbra/Import/Textures/TextureManager.cs | 10 +++--- .../LiveColorTablePreviewer.cs | 8 ++--- .../PathResolving/AnimationHookService.cs | 6 ++-- .../Interop/PathResolving/DrawObjectState.cs | 4 +-- .../IdentifiedCollectionCache.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 7 ++-- Penumbra/Interop/PathResolving/PathState.cs | 13 +++---- .../Interop/PathResolving/ResolvePathHooks.cs | 35 ++++++++++--------- .../Interop/PathResolving/SubfileHelper.cs | 5 +-- .../ResourceLoading/CreateFileWHook.cs | 5 +-- .../ResourceLoading/FileReadService.cs | 5 +-- .../ResourceLoading/ResourceManagerService.cs | 5 +-- .../ResourceLoading/ResourceService.cs | 10 +++--- .../Interop/ResourceLoading/TexMdlService.cs | 5 +-- .../Interop/SafeHandles/SafeTextureHandle.cs | 2 +- Penumbra/Interop/Services/CharacterUtility.cs | 10 +++--- Penumbra/Interop/Services/FontReloader.cs | 3 +- Penumbra/Interop/Services/GameEventManager.cs | 5 +-- Penumbra/Interop/Services/RedrawService.cs | 6 ++-- .../Services/ResidentResourceManager.cs | 7 ++-- Penumbra/Interop/Services/SkinFixer.cs | 7 ++-- Penumbra/Interop/Structs/CharacterBaseExt.cs | 2 +- Penumbra/Interop/Structs/Material.cs | 2 +- Penumbra/Interop/Structs/TextureUtility.cs | 32 +++++++---------- Penumbra/Meta/MetaFileManager.cs | 4 +-- Penumbra/Mods/Manager/ModStorage.cs | 2 +- Penumbra/Penumbra.cs | 8 +++-- Penumbra/Services/ChatService.cs | 8 ++--- Penumbra/Services/DalamudServices.cs | 17 ++++----- Penumbra/Services/ServiceManager.cs | 5 ++- Penumbra/Services/StainService.cs | 8 ++--- Penumbra/Services/Wrappers.cs | 12 +++---- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +-- .../ModEditWindow.Materials.ColorTable.cs | 1 + .../AdvancedWindow/ModEditWindow.Materials.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 1 + Penumbra/UI/ChangedItemDrawer.cs | 18 +++++----- Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 1 + .../CollectionTab/IndividualAssignmentUi.cs | 12 +++---- Penumbra/UI/LaunchButton.cs | 17 ++++----- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 7 ++-- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/UI/Tabs/CollectionsTab.cs | 1 + Penumbra/UI/Tabs/DebugTab.cs | 5 +-- Penumbra/UI/UiHelpers.cs | 2 +- 61 files changed, 210 insertions(+), 192 deletions(-) diff --git a/OtterGui b/OtterGui index 9b431c1f..c70fcc06 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9b431c1f491c0739132da65da08bc092d7ff79da +Subproject commit c70fcc069ea44e1ffb8b33fc409c4ccfdef5e298 diff --git a/Penumbra.GameData b/Penumbra.GameData index d400686c..3c9e0d03 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d400686c8bfb0b9f72fa0c3301d1fe1df9be525b +Subproject commit 3c9e0d03281c350f8260debb0eab4059408cd0cd diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 148d4481..dd800f0a 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; @@ -15,7 +16,6 @@ using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Penumbra.Api; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5daecaa9..a24eb2fa 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Plugin.Services; using OtterGui.Classes; using Penumbra.Api; using Penumbra.Api.Enums; @@ -382,7 +382,7 @@ public class CollectionCacheManager : IDisposable /// /// Update forced files only on framework. /// - private void OnFramework(Framework _) + private void OnFramework(IFramework _) { while (_changeQueue.TryDequeue(out var changeData)) changeData.Apply(); diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 920e9cef..3249cc43 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,6 +1,4 @@ -using Dalamud.Game; using Dalamud.Game.Command; -using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using ImGuiNET; @@ -23,7 +21,7 @@ public class CommandHandler : IDisposable private readonly ICommandManager _commandManager; private readonly RedrawService _redrawService; - private readonly ChatGui _chat; + private readonly IChatGui _chat; private readonly Configuration _config; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; @@ -32,7 +30,7 @@ public class CommandHandler : IDisposable private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; - public CommandHandler(Framework framework, ICommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, + public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra, CollectionEditor collectionEditor) { diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs index 4f8c5305..a4a0e203 100644 --- a/Penumbra/Import/Textures/BaseImage.cs +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -103,7 +103,7 @@ public readonly struct BaseImage : IDisposable { null => 0, ScratchImage s => s.Meta.MipLevels, - TexFile t => t.Header.MipLevels, + TexFile t => t.Header.MipLevelsCount, _ => 1, }; } diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 8bac0a3b..2d131d71 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -2,7 +2,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; -using Dalamud.Interface; +using Dalamud.Interface.Utility; using Penumbra.UI; namespace Penumbra.Import.Textures; @@ -13,11 +13,11 @@ public partial class CombinedTexture private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; private Vector4 _constantRight = Vector4.Zero; - private int _offsetX = 0; - private int _offsetY = 0; - private CombineOp _combineOp = CombineOp.Over; - private ResizeOp _resizeOp = ResizeOp.None; - private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; + private int _offsetX; + private int _offsetY; + private CombineOp _combineOp = CombineOp.Over; + private ResizeOp _resizeOp = ResizeOp.None; + private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; private RgbaPixelData _leftPixels = RgbaPixelData.Empty; private RgbaPixelData _rightPixels = RgbaPixelData.Empty; diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 15d45be6..629cdff5 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,7 +79,7 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write((byte)header.MipLevels); + w.Write(header.MipLevelsCount); w.Write((byte)0); // TODO Lumina Update unsafe { @@ -96,11 +96,11 @@ public static class TexFileParser var meta = scratch.Meta; var ret = new TexFile.TexHeader() { - Height = (ushort)meta.Height, - Width = (ushort)meta.Width, - Depth = (ushort)Math.Max(meta.Depth, 1), - MipLevels = (byte)Math.Min(meta.MipLevels, 13), - Format = meta.Format.ToTexFormat(), + Height = (ushort)meta.Height, + Width = (ushort)meta.Width, + Depth = (ushort)Math.Max(meta.Depth, 1), + MipLevelsCount = (byte)Math.Min(meta.MipLevels, 13), + Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, @@ -143,7 +143,7 @@ public static class TexFileParser Height = header.Height, Width = header.Width, Depth = Math.Max(header.Depth, (ushort)1), - MipLevels = header.MipLevels, + MipLevels = header.MipLevelsCount, ArraySize = 1, Format = header.Format.ToDXGI(), Dimension = header.Type.ToDimension(), diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index fe1d3371..c4d6dc56 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,4 +1,4 @@ -using ImGuiScene; +using Dalamud.Interface.Internal; using OtterTex; namespace Penumbra.Import.Textures; @@ -21,7 +21,7 @@ public sealed class Texture : IDisposable internal string? TmpPath; // If the load failed, an exception is stored. - public Exception? LoadError = null; + public Exception? LoadError; // The pixels of the main image in RGBA order. // Empty if LoadError != null or Path is empty. @@ -29,7 +29,7 @@ public sealed class Texture : IDisposable // The ImGui wrapper to load the image. // null if LoadError != null or Path is empty. - public TextureWrap? TextureWrap = null; + public IDalamudTextureWrap? TextureWrap; // The base image in whatever format it has. public BaseImage BaseImage; @@ -76,9 +76,9 @@ public sealed class Texture : IDisposable try { - (BaseImage, Type) = textures.Load(path); - (RgbaPixels, var width, var height) = BaseImage.GetPixelData(); - TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels); + (BaseImage, Type) = textures.Load(path); + (RgbaPixels, _, _) = BaseImage.GetPixelData(); + TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels); Loaded?.Invoke(true); } catch (Exception e) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index a5692c23..bea28749 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -94,7 +94,7 @@ public static class TextureDrawer ImGuiUtil.DrawTableColumn("Format"); ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn(t.Header.MipLevelsCount.ToString()); ImGuiUtil.DrawTableColumn("Data Size"); ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); break; @@ -106,7 +106,7 @@ public static class TextureDrawer private int _skipPrefix = 0; public PathSelectCombo(TextureManager textures, ModEditor editor) - : base(() => CreateFiles(textures, editor)) + : base(() => CreateFiles(textures, editor), Penumbra.Log) { } protected override string ToString((string, bool) obj) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 31c3275e..5653d760 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; +using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; -using ImGuiScene; using Lumina.Data.Files; using OtterGui.Log; using OtterGui.Tasks; @@ -19,7 +19,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable private readonly IDataManager _gameData; private readonly ConcurrentDictionary _tasks = new(); - private bool _disposed = false; + private bool _disposed; public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) { @@ -209,14 +209,14 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable } /// Load a texture wrap for a given image. - public TextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + public IDalamudTextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) { (rgba, width, height) = GetData(image, rgba, width, height); return LoadTextureWrap(rgba, width, height); } /// Load a texture wrap for a given image. - public TextureWrap LoadTextureWrap(byte[] rgba, int width, int height) + public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) => _uiBuilder.LoadImageRaw(rgba, width, height, 4); /// Load any supported file from game data or drive depending on extension and if the path is rooted. @@ -335,7 +335,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable if (numMips == input.Meta.MipLevels) return input; - var flags = (Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha; + var flags = (Dalamud.Utility.Util.IsWine() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha; var ec = input.GenerateMipMaps(out var ret, numMips, flags); if (ec != ErrorCode.Ok) throw new Exception( diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index e89f0d10..bacc72fa 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,7 +1,5 @@ -using Dalamud.Game; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using Penumbra.GameData.Files; using Penumbra.Interop.SafeHandles; @@ -13,7 +11,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase public const int TextureHeight = MtrlFile.ColorTable.NumRows; public const int TextureLength = TextureWidth * TextureHeight * 4; - private readonly Framework _framework; + private readonly IFramework _framework; private readonly Texture** _colorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture; @@ -24,7 +22,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase public Half[] ColorTable => _colorTable; - public LiveColorTablePreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo) + public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo) : base(objects, materialInfo) { _framework = framework; @@ -66,7 +64,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase _updatePending = true; } - private void OnFrameworkUpdate(Framework _) + private void OnFrameworkUpdate(IFramework _) { if (!_updatePending) return; diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 616cb2f6..7f69ceef 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -21,13 +21,13 @@ public unsafe class AnimationHookService : IDisposable private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly CollectionResolver _resolver; - private readonly Condition _conditions; + private readonly ICondition _conditions; private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); public AnimationHookService(PerformanceTracker performance, IObjectTable objects, CollectionResolver collectionResolver, - DrawObjectState drawObjectState, CollectionResolver resolver, Condition conditions) + DrawObjectState drawObjectState, CollectionResolver resolver, ICondition conditions, IGameInteropProvider interop) { _performance = performance; _objects = objects; @@ -36,7 +36,7 @@ public unsafe class AnimationHookService : IDisposable _resolver = resolver; _conditions = conditions; - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _loadCharacterSoundHook.Enable(); _loadTimelineResourcesHook.Enable(); diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index be1484db..9726d84c 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -20,9 +20,9 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; - public DrawObjectState(IObjectTable objects, GameEventManager gameEvents) + public DrawObjectState(IObjectTable objects, GameEventManager gameEvents, IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _enableDrawHook.Enable(); _objects = objects; _gameEvents = gameEvents; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 0b456b3c..73c20ab9 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -85,7 +85,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A _dirty = _cache.Count > 0; } - private void TerritoryClear(object? _1, ushort _2) + private void TerritoryClear(ushort _2) => _dirty = _cache.Count > 0; private void OnCharacterDestruct(Character* character) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a68e376b..6defe78c 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -59,7 +60,7 @@ public unsafe class MetaState : IDisposable private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config) + ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config, IGameInteropProvider interop) { _performance = performance; _communicator = communicator; @@ -68,8 +69,8 @@ public unsafe class MetaState : IDisposable _gameEventManager = gameEventManager; _characterUtility = characterUtility; _config = config; - SignatureHelper.Initialise(this); - _onModelLoadCompleteHook = Hook.FromAddress(_humanVTable[58], OnModelLoadCompleteDetour); + interop.InitializeFromAttributes(this); + _onModelLoadCompleteHook = interop.HookFromAddress(_humanVTable[58], OnModelLoadCompleteDetour); _getEqpIndirectHook.Enable(); _updateModelsHook.Enable(); _onModelLoadCompleteHook.Enable(); diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index 0c6566a3..4fb3d31d 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.GameData; @@ -34,16 +35,16 @@ public unsafe class PathState : IDisposable public IList CurrentData => _resolveData.Values; - public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility) + public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); CollectionResolver = collectionResolver; MetaState = metaState; CharacterUtility = characterUtility; - _human = new ResolvePathHooks(this, _humanVTable, ResolvePathHooks.Type.Human); - _weapon = new ResolvePathHooks(this, _weaponVTable, ResolvePathHooks.Type.Weapon); - _demiHuman = new ResolvePathHooks(this, _demiHumanVTable, ResolvePathHooks.Type.Other); - _monster = new ResolvePathHooks(this, _monsterVTable, ResolvePathHooks.Type.Other); + _human = new ResolvePathHooks(interop, this, _humanVTable, ResolvePathHooks.Type.Human); + _weapon = new ResolvePathHooks(interop, this, _weaponVTable, ResolvePathHooks.Type.Weapon); + _demiHuman = new ResolvePathHooks(interop, this, _demiHumanVTable, ResolvePathHooks.Type.Other); + _monster = new ResolvePathHooks(interop, this, _monsterVTable, ResolvePathHooks.Type.Other); _human.Enable(); _weapon.Enable(); _demiHuman.Enable(); diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index 6f8f93dd..f9a341b9 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; @@ -35,21 +36,21 @@ public unsafe class ResolvePathHooks : IDisposable private readonly PathState _parent; - public ResolvePathHooks(PathState parent, nint* vTable, Type type) + public ResolvePathHooks(IGameInteropProvider interop, PathState parent, nint* vTable, Type type) { _parent = parent; - _resolveDecalPathHook = Create(vTable[83], type, ResolveDecalWeapon, ResolveDecal); - _resolveEidPathHook = Create(vTable[85], type, ResolveEidWeapon, ResolveEid); - _resolveImcPathHook = Create(vTable[81], type, ResolveImcWeapon, ResolveImc); - _resolveMPapPathHook = Create(vTable[79], type, ResolveMPapWeapon, ResolveMPap); - _resolveMdlPathHook = Create(vTable[73], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman); - _resolveMtrlPathHook = Create(vTable[82], type, ResolveMtrlWeapon, ResolveMtrl); - _resolvePapPathHook = Create(vTable[76], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman); - _resolvePhybPathHook = Create(vTable[75], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman); - _resolveSklbPathHook = Create(vTable[72], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman); - _resolveSkpPathHook = Create(vTable[74], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman); - _resolveTmbPathHook = Create(vTable[77], type, ResolveTmbWeapon, ResolveTmb); - _resolveVfxPathHook = Create(vTable[84], type, ResolveVfxWeapon, ResolveVfx); + _resolveDecalPathHook = Create(interop, vTable[83], type, ResolveDecalWeapon, ResolveDecal); + _resolveEidPathHook = Create(interop, vTable[85], type, ResolveEidWeapon, ResolveEid); + _resolveImcPathHook = Create(interop, vTable[81], type, ResolveImcWeapon, ResolveImc); + _resolveMPapPathHook = Create(interop, vTable[79], type, ResolveMPapWeapon, ResolveMPap); + _resolveMdlPathHook = Create(interop, vTable[73], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman); + _resolveMtrlPathHook = Create(interop, vTable[82], type, ResolveMtrlWeapon, ResolveMtrl); + _resolvePapPathHook = Create(interop, vTable[76], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman); + _resolvePhybPathHook = Create(interop, vTable[75], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman); + _resolveSklbPathHook = Create(interop, vTable[72], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman); + _resolveSkpPathHook = Create(interop, vTable[74], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman); + _resolveTmbPathHook = Create(interop, vTable[77], type, ResolveTmbWeapon, ResolveTmb); + _resolveVfxPathHook = Create(interop, vTable[84], type, ResolveVfxWeapon, ResolveVfx); } public void Enable() @@ -217,7 +218,7 @@ public unsafe class ResolvePathHooks : IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(nint address, Type type, T weapon, T other, T human) where T : Delegate + private static Hook Create(IGameInteropProvider interop, nint address, Type type, T weapon, T other, T human) where T : Delegate { var del = type switch { @@ -225,12 +226,12 @@ public unsafe class ResolvePathHooks : IDisposable Type.Weapon => weapon, _ => other, }; - return Hook.FromAddress(address, del); + return interop.HookFromAddress(address, del); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(nint address, Type type, T weapon, T other) where T : Delegate - => Create(address, type, weapon, other, other); + private static Hook Create(IGameInteropProvider interop, nint address, Type type, T weapon, T other) where T : Delegate + => Create(interop, address, type, weapon, other, other); // Implementation diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index a8fd816a..3a60450d 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -30,9 +31,9 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection _subFileCollection = new(); - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator) + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator, IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _performance = performance; _loader = loader; diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index ca9c2577..b77ac1e0 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -14,9 +15,9 @@ public unsafe class CreateFileWHook : IDisposable { public const int RequiredSize = 28; - public CreateFileWHook() + public CreateFileWHook(IGameInteropProvider interop) { - _createFileWHook = Hook.FromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); + _createFileWHook = interop.HookFromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); _createFileWHook.Enable(); } diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 9dc89ab2..64442771 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -8,11 +9,11 @@ namespace Penumbra.Interop.ResourceLoading; public unsafe class FileReadService : IDisposable { - public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager) + public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _resourceManager = resourceManager; _performance = performance; - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _readSqPackHook.Enable(); } diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index ce7d3d4c..a087a659 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -10,8 +11,8 @@ namespace Penumbra.Interop.ResourceLoading; public unsafe class ResourceManagerService { - public ResourceManagerService() - => SignatureHelper.Initialise(this); + public ResourceManagerService(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); /// The SE Resource Manager as pointer. public ResourceManager* ResourceManager diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 5d060d85..47107f44 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -1,8 +1,8 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String; @@ -16,19 +16,19 @@ public unsafe class ResourceService : IDisposable private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; - public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager) + public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _performance = performance; _resourceManager = resourceManager; - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _getResourceSyncHook.Enable(); _getResourceAsyncHook.Enable(); _resourceHandleDestructorHook.Enable(); - _incRefHook = Hook.FromAddress( + _incRefHook = interop.HookFromAddress( (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); _incRefHook.Enable(); - _decRefHook = Hook.FromAddress( + _decRefHook = interop.HookFromAddress( (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); _decRefHook.Enable(); diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index 574da240..68ad518c 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.Api.Enums; @@ -19,9 +20,9 @@ public unsafe class TexMdlService public IReadOnlySet CustomFileCrc => _customFileCrc; - public TexMdlService() + public TexMdlService(IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _checkFileStateHook.Enable(); _loadTexFileExternHook.Enable(); _loadMdlFileExternHook.Enable(); diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs index dee28797..df97371b 100644 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using Penumbra.Interop.Structs; namespace Penumbra.Interop.SafeHandles; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 48824888..699b59e0 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.Collections.Manager; using Penumbra.GameData; @@ -6,7 +6,7 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; -public unsafe partial class CharacterUtility : IDisposable +public unsafe class CharacterUtility : IDisposable { public record struct InternalIndex(int Value); @@ -52,12 +52,12 @@ public unsafe partial class CharacterUtility : IDisposable public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; - private readonly Framework _framework; + private readonly IFramework _framework; public readonly ActiveCollectionData Active; - public CharacterUtility(Framework framework, ActiveCollectionData active) + public CharacterUtility(IFramework framework, IGameInteropProvider interop, ActiveCollectionData active) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _lists = Enumerable.Range(0, RelevantIndices.Length) .Select(idx => new MetaList(this, new InternalIndex(idx))) .ToArray(); diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 76a205dc..2f4a3cfd 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.GameData; @@ -24,7 +25,7 @@ public unsafe class FontReloader private AtkModule* _atkModule = null!; private delegate* unmanaged _reloadFontsFunc = null!; - public FontReloader(Dalamud.Game.Framework dFramework) + public FontReloader(IFramework dFramework) { dFramework.RunOnFrameworkThread(() => { diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index 2e8a23f0..59714ff0 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.GameData; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -20,9 +21,9 @@ public unsafe class GameEventManager : IDisposable public event WeaponReloadingEvent? WeaponReloading; public event WeaponReloadedEvent? WeaponReloaded; - public GameEventManager() + public GameEventManager(IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _characterDtorHook.Enable(); _copyCharacterHook.Enable(); _resourceHandleDestructorHook.Enable(); diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 65d5b0c0..0864bb71 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -100,10 +100,10 @@ public unsafe partial class RedrawService public sealed unsafe partial class RedrawService : IDisposable { - private readonly Framework _framework; + private readonly IFramework _framework; private readonly IObjectTable _objects; private readonly ITargetManager _targets; - private readonly Condition _conditions; + private readonly ICondition _conditions; private readonly List _queue = new(100); private readonly List _afterGPoseQueue = new(GPoseSlots); @@ -111,7 +111,7 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(Framework framework, IObjectTable objects, ITargetManager targets, Condition conditions) + public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions) { _framework = framework; _objects = objects; diff --git a/Penumbra/Interop/Services/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs index ff7f95a5..72697185 100644 --- a/Penumbra/Interop/Services/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Penumbra.GameData; @@ -21,10 +22,8 @@ public unsafe class ResidentResourceManager public Structs.ResidentResourceManager* Address => *_residentResourceManagerAddress; - public ResidentResourceManager() - { - SignatureHelper.Initialise(this); - } + public ResidentResourceManager(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); // Reload certain player resources by force. public void Reload() diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 211a062c..be5b778e 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui.Classes; @@ -48,13 +49,13 @@ public sealed unsafe class SkinFixer : IDisposable public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator) + public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator, IGameInteropProvider interop) { - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); _gameEvents = gameEvents; _utility = utility; _communicator = communicator; - _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; _onRenderMaterialHook.Enable(); diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs index 7cdcc6fe..53fda2cd 100644 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 0165a8ff..f7c8679e 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs index a81480fb..eeea4c33 100644 --- a/Penumbra/Interop/Structs/TextureUtility.cs +++ b/Penumbra/Interop/Structs/TextureUtility.cs @@ -1,37 +1,31 @@ +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -public static unsafe class TextureUtility +public unsafe class TextureUtility { - private static readonly Functions Funcs = new(); + public TextureUtility(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); + + + [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] + private static nint _textureCreate2D = nint.Zero; + + [Signature("E9 ?? ?? ?? ?? 8B 02 25")] + private static nint _textureInitializeContents = nint.Zero; public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) - => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, + => ((delegate* unmanaged)_textureCreate2D)(device, size, mipLevel, textureFormat, flags, unk); public static bool InitializeContents(Texture* texture, void* contents) - => ((delegate* unmanaged)Funcs.TextureInitializeContents)(texture, contents); + => ((delegate* unmanaged)_textureInitializeContents)(texture, contents); public static void IncRef(Texture* texture) => ((delegate* unmanaged)(*(void***)texture)[2])(texture); public static void DecRef(Texture* texture) => ((delegate* unmanaged)(*(void***)texture)[3])(texture); - - private sealed class Functions - { - [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] - public nint TextureCreate2D = nint.Zero; - - [Signature("E9 ?? ?? ?? ?? 8B 02 25")] - public nint TextureInitializeContents = nint.Zero; - - public Functions() - { - SignatureHelper.Initialise(this); - } - } } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 3155e188..d918bda2 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -29,7 +29,7 @@ public unsafe class MetaFileManager public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier, - FileCompactor compactor) + FileCompactor compactor, IGameInteropProvider interop) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -39,7 +39,7 @@ public unsafe class MetaFileManager ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; - SignatureHelper.Initialise(this); + interop.InitializeFromAttributes(this); } public void WriteAllTexToolsMeta(Mod mod) diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 83d20969..490381d6 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -12,7 +12,7 @@ public class ModCombo : FilterComboCache => obj.Name.Text; public ModCombo(Func> generator) - : base(generator) + : base(generator, Penumbra.Log) { } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2cca1789..0519baae 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -19,7 +19,9 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; +using Penumbra.Interop.Structs; using Penumbra.UI; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra; @@ -53,7 +55,7 @@ public class Penumbra : IDalamudPlugin var startTimer = new StartTracker(); using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); - Chat = _services.GetRequiredService(); + Chat = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() @@ -73,11 +75,13 @@ public class Penumbra : IDalamudPlugin _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { _services.GetRequiredService(); } + _services.GetRequiredService(); SetupInterface(); @@ -187,7 +191,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); sb.Append($"> **`Enable Mods: `** {_config.EnableMods}\n"); sb.Append($"> **`Enable HTTP API: `** {_config.EnableHttpApi}\n"); - sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); + sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n"); sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); diff --git a/Penumbra/Services/ChatService.cs b/Penumbra/Services/ChatService.cs index adb24618..3e715a4f 100644 --- a/Penumbra/Services/ChatService.cs +++ b/Penumbra/Services/ChatService.cs @@ -1,8 +1,8 @@ -using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; @@ -10,9 +10,9 @@ namespace Penumbra.Services; public class ChatService : OtterGui.Classes.ChatService { - private readonly ChatGui _chat; + private readonly IChatGui _chat; - public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat) + public ChatService(Logger log, DalamudPluginInterface pi, IChatGui chat) : base(log, pi) => _chat = chat; @@ -37,7 +37,7 @@ public class ChatService : OtterGui.Classes.ChatService var payload = new SeString(payloadList); - _chat.PrintChat(new XivChatEntry + _chat.Print(new XivChatEntry { Message = payload, }); diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 0cd4c97a..99539c39 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -1,8 +1,5 @@ using Dalamud.Game; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Gui; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.Plugin; @@ -75,6 +72,8 @@ public class DalamudServices services.AddSingleton(DragDropManager); services.AddSingleton(TextureProvider); services.AddSingleton(TextureSubstitutionProvider); + services.AddSingleton(Interop); + services.AddSingleton(Log); } // TODO remove static @@ -83,18 +82,20 @@ public class DalamudServices [PluginService][RequiredVersion("1.0")] public ICommandManager Commands { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IDataManager GameData { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IFramework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ICondition Conditions { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ITargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ITitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IGameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IKeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ISigScanner SigScanner { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ITextureProvider TextureProvider { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public ITextureSubstitutionProvider TextureSubstitutionProvider { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IGameInteropProvider Interop { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public IPluginLog Log { get; private set; } = null!; // @formatter:on public UiBuilder UiBuilder diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index b76c39ef..9e3b9b1a 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -13,6 +13,7 @@ using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -23,6 +24,7 @@ using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.Services; @@ -88,7 +90,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 30e22a7a..bbbfcc71 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -12,7 +12,7 @@ public class StainService : IDisposable public sealed class StainTemplateCombo : FilterComboCache { public StainTemplateCombo(IEnumerable items) - : base(items) + : base(items, Penumbra.Log) { } } @@ -21,12 +21,12 @@ public class StainService : IDisposable public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, IDataManager dataManager) + public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, IDataManager dataManager, IPluginLog dalamudLog) { using var t = timer.Measure(StartTimeType.Stains); - StainData = new StainData(pluginInterface, dataManager, dataManager.Language); + StainData = new StainData(pluginInterface, dataManager, dataManager.Language, dalamudLog); StainCombo = new FilterComboColors(140, - StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))), Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs index 69dbbeab..b1f17d4d 100644 --- a/Penumbra/Services/Wrappers.cs +++ b/Penumbra/Services/Wrappers.cs @@ -11,24 +11,24 @@ namespace Penumbra.Services; public sealed class IdentifierService : AsyncServiceWrapper { - public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, IDataManager data, ItemService items) + public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, IDataManager data, ItemService items, IPluginLog log) : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, - () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService)) + () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService, log)) { } } public sealed class ItemService : AsyncServiceWrapper { - public ItemService(StartTracker tracker, DalamudPluginInterface pi, IDataManager gameData) - : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language)) + public ItemService(StartTracker tracker, DalamudPluginInterface pi, IDataManager gameData, IPluginLog log) + : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language, log)) { } } public sealed class ActorService : AsyncServiceWrapper { public ActorService(StartTracker tracker, DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, - Framework framework, IDataManager gameData, IGameGui gui, CutsceneService cutscene) + IFramework framework, IDataManager gameData, IGameGui gui, CutsceneService cutscene, IPluginLog log, IGameInteropProvider interop) : base(nameof(ActorService), tracker, StartTimeType.Actors, - () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx))) + () => new ActorManager(pi, objects, clientState, framework, interop, gameData, gui, idx => (short)cutscene.GetParentIndex(idx), log)) { } } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 2591145f..9354afaf 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -279,7 +279,7 @@ public class FileEditor : IDisposable where T : class, IWritable private readonly Configuration _config; public Combo(Configuration config, Func> generator) - : base(generator) + : base(generator, Penumbra.Log) => _config = config; protected override bool DrawSelectable(int globalIdx, bool selected) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 87598f5a..967e8d01 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -129,7 +129,7 @@ public class ItemSwapTab : IDisposable, ITab private class ItemSelector : FilterComboCache { public ItemSelector(ItemService data, FullEquipType type) - : base(() => data.AwaitedService[type]) + : base(() => data.AwaitedService[type], Penumbra.Log) { } protected override string ToString(EquipItem obj) @@ -139,7 +139,7 @@ public class ItemSwapTab : IDisposable, ITab private class WeaponSelector : FilterComboCache { public WeaponSelector() - : base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes)) + : base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes), Penumbra.Log) { } protected override string ToString(FullEquipType type) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index cccc43ee..447382cf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 73aad323..df20d60f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index c659ada0..0f171e21 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.DragDrop; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 60247d81..7d4fa96f 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 39728cd4..ef847d8d 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui.Raii; using OtterGui; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 13a5787e..324c354a 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -1,9 +1,9 @@ using Dalamud.Interface; +using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; -using ImGuiScene; using Lumina.Data.Files; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; @@ -46,11 +46,11 @@ public class ChangedItemDrawer : IDisposable public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; - private readonly Configuration _config; - private readonly ExcelSheet _items; - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - private float _smallestIconWidth; + private readonly Configuration _config; + private readonly ExcelSheet _items; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth; public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) @@ -357,7 +357,7 @@ public class ChangedItemDrawer : IDisposable if (!equipTypeIcons.Valid) return false; - void Add(ChangedItemIcon icon, TextureWrap? tex) + void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex) { if (tex != null) _icons.Add(icon, tex); @@ -387,7 +387,7 @@ public class ChangedItemDrawer : IDisposable return true; } - private static unsafe TextureWrap? LoadUnknownTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, UiBuilder uiBuilder) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) @@ -402,7 +402,7 @@ public class ChangedItemDrawer : IDisposable return uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4); } - private static unsafe TextureWrap? LoadEmoteTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, UiBuilder uiBuilder) { var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); if (emote == null) diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index fc37eaf2..b2ee5c3b 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -13,7 +13,7 @@ public sealed class CollectionCombo : FilterComboCache private readonly ImRaii.Color _color = new(); public CollectionCombo(CollectionManager manager, Func> items) - : base(items) + : base(items, Penumbra.Log) => _collectionManager = manager; protected override void DrawFilter(int currentSelected, float width) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 3ebd3252..bd37a484 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index da011bde..5f463c43 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -126,12 +126,12 @@ public class IndividualAssignmentUi : IDisposable /// Create combos when ready. private void SetupCombos() { - _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds); - _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts); - _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions); - _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments); - _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs); - _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs); + _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds, Penumbra.Log); + _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts, Penumbra.Log); + _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions, Penumbra.Log); + _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments, Penumbra.Log); + _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs, Penumbra.Log); + _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs, Penumbra.Log); _ready = true; _actorService.FinishedCreation -= SetupCombos; } diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 5b9bf5a4..9650ccf8 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; +using Dalamud.Interface.Internal; using Dalamud.Plugin; -using ImGuiScene; +using Dalamud.Plugin.Services; namespace Penumbra.UI; @@ -10,18 +11,18 @@ namespace Penumbra.UI; /// public class LaunchButton : IDisposable { - private readonly ConfigWindow _configWindow; - private readonly UiBuilder _uiBuilder; - private readonly TitleScreenMenu _title; - private readonly string _fileName; + private readonly ConfigWindow _configWindow; + private readonly UiBuilder _uiBuilder; + private readonly ITitleScreenMenu _title; + private readonly string _fileName; - private TextureWrap? _icon; - private TitleScreenMenu.TitleScreenMenuEntry? _entry; + private IDalamudTextureWrap? _icon; + private TitleScreenMenuEntry? _entry; /// /// Register the launch button to be created on the next draw event. /// - public LaunchButton(DalamudPluginInterface pi, TitleScreenMenu title, ConfigWindow ui) + public LaunchButton(DalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui) { _uiBuilder = pi.UiBuilder; _configWindow = ui; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 39fc7a0e..0d9b0a70 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -35,10 +36,10 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 29 Sep 2023 02:18:44 +0200 Subject: [PATCH 1203/2451] Add multi deletion to mod selector. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/OtterGui b/OtterGui index c70fcc06..06a5ea00 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c70fcc069ea44e1ffb8b33fc409c4ccfdef5e298 +Subproject commit 06a5ea005d5a08febbbc2e45cffde7582ffa7607 diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0d9b0a70..215a0269 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -279,19 +279,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector DeleteSelectionButton(size, _config.DeleteModModifier, "mod", "mods", _modManager.DeleteMod); private void AddHelpButton(Vector2 size) { From 8f16aa7ee91fd0d350616542fa288785dae3b94b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Oct 2023 23:21:07 +0200 Subject: [PATCH 1204/2451] Fix collection button ids. --- Penumbra/UI/Classes/CollectionSelectHeader.cs | 11 ++++++----- Penumbra/UI/Tabs/ModsTab.cs | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index f4fa1b68..de2b6a34 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -36,10 +36,10 @@ public class CollectionSelectHeader var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) { - DrawCollectionButton(buttonSize, GetDefaultCollectionInfo()); - DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo()); - DrawCollectionButton(buttonSize, GetPlayerCollectionInfo()); - DrawCollectionButton(buttonSize, GetInheritedCollectionInfo()); + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); + DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); + DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); _collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value()); } @@ -126,9 +126,10 @@ public class CollectionSelectHeader }; } - private void DrawCollectionButton(Vector2 buttonWidth, (ModCollection?, string, string, bool) tuple) + private void DrawCollectionButton(Vector2 buttonWidth, (ModCollection?, string, string, bool) tuple, int id) { var (collection, name, tooltip, disabled) = tuple; + using var _ = ImRaii.PushId(id); if (ImGuiUtil.DrawDisabledButton(name, buttonWidth, tooltip, disabled)) _activeCollections.SetCollection(collection!, CollectionType.Current); ImGui.SameLine(); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 16f0180e..1e675036 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -146,6 +146,7 @@ public class ModsTab : ITab ImGuiUtil.HoverTooltip(lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'."); } + using var id = ImRaii.PushId("Redraw"); using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; From 5394bdc535b0e9adbb9679630aec40faa5077059 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Oct 2023 23:23:26 +0200 Subject: [PATCH 1205/2451] Update Submodules. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 06a5ea00..df07c4ed 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 06a5ea005d5a08febbbc2e45cffde7582ffa7607 +Subproject commit df07c4ed08e8e6c1188867c7863a19e02c8adb53 diff --git a/Penumbra.Api b/Penumbra.Api index 9472b6e3..bc9bd5f6 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 9472b6e327109216368c3dc1720159f5295bdb13 +Subproject commit bc9bd5f6bb06e61069704733841868307aed7a5c diff --git a/Penumbra.GameData b/Penumbra.GameData index 3c9e0d03..acf8cf68 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3c9e0d03281c350f8260debb0eab4059408cd0cd +Subproject commit acf8cf68f06e37bbe64f1100a68937da9efb762c From 3d2ce1f4bb0718660e3900efcd837cc7f030f957 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 2 Oct 2023 23:25:15 +0200 Subject: [PATCH 1206/2451] Use ClientStructs hook for CalculateHeight. --- Penumbra/Interop/PathResolving/MetaState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6defe78c..c41d651e 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -70,6 +70,8 @@ public unsafe class MetaState : IDisposable _characterUtility = characterUtility; _config = config; interop.InitializeFromAttributes(this); + _calculateHeightHook = + interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); _onModelLoadCompleteHook = interop.HookFromAddress(_humanVTable[58], OnModelLoadCompleteDetour); _getEqpIndirectHook.Enable(); _updateModelsHook.Enable(); @@ -249,8 +251,6 @@ public unsafe class MetaState : IDisposable private delegate ulong CalculateHeightDelegate(Character* character); - // TODO: use client structs - [Signature(Sigs.CalculateHeight, DetourName = nameof(CalculateHeightDetour))] private readonly Hook _calculateHeightHook = null!; private ulong CalculateHeightDetour(Character* character) From e5427858e0e91d7e590b74fa2843d474ab477df4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Oct 2023 01:42:28 +0200 Subject: [PATCH 1207/2451] Add support for ActorIdentifier.FromUserString returning multiple identifiers. --- Penumbra/CommandHandler.cs | 113 +++++++++++--------- Penumbra/Interop/PathResolving/MetaState.cs | 3 +- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 3249cc43..71bd19c2 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -21,7 +21,7 @@ public class CommandHandler : IDisposable private readonly ICommandManager _commandManager; private readonly RedrawService _redrawService; - private readonly IChatGui _chat; + private readonly IChatGui _chat; private readonly Configuration _config; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; @@ -30,7 +30,8 @@ public class CommandHandler : IDisposable private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; - public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, Configuration config, + public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, + Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra, CollectionEditor collectionEditor) { @@ -270,7 +271,7 @@ public class CommandHandler : IDisposable if (!GetModCollection(split[1], out var collection)) return false; - var identifier = ActorIdentifier.Invalid; + var identifiers = Array.Empty(); if (type is CollectionType.Individual) { if (split.Length == 2) @@ -284,17 +285,22 @@ public class CommandHandler : IDisposable { if (_redrawService.GetName(split[2].ToLowerInvariant(), out var obj)) { - identifier = _actors.FromObject(obj, false, true, true); + var identifier = _actors.FromObject(obj, false, true, true); if (!identifier.IsValid) { _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(split[2]) .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); return false; } + + identifiers = new[] + { + identifier, + }; } else { - identifier = _actors.FromUserString(split[2]); + identifiers = _actors.FromUserString(split[2], false); } } catch (ActorManager.IdentifierParseError e) @@ -306,55 +312,60 @@ public class CommandHandler : IDisposable } } - var oldCollection = _collectionManager.Active.ByType(type, identifier); - if (collection == oldCollection) + var anySuccess = false; + foreach (var identifier in identifiers.Distinct()) { - _chat.Print(collection == null - ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" - : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); - return false; + var oldCollection = _collectionManager.Active.ByType(type, identifier); + if (collection == oldCollection) + { + _chat.Print(collection == null + ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" + : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); + continue; + } + + var individualIndex = _collectionManager.Active.Individuals.Index(identifier); + + if (oldCollection == null) + { + if (type.IsSpecial()) + { + _collectionManager.Active.CreateSpecialCollection(type); + } + else if (identifier.IsValid) + { + var identifierGroup = _collectionManager.Active.Individuals.GetGroup(identifier); + individualIndex = _collectionManager.Active.Individuals.Count; + _collectionManager.Active.CreateIndividualCollection(identifierGroup); + } + } + else if (collection == null) + { + if (type.IsSpecial()) + { + _collectionManager.Active.RemoveSpecialCollection(type); + } + else if (individualIndex >= 0) + { + _collectionManager.Active.RemoveIndividualCollection(individualIndex); + } + else + { + _chat.Print( + $"Can not remove the {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); + continue; + } + + Print( + $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); + anySuccess = true; + } + + _collectionManager.Active.SetCollection(collection!, type, individualIndex); + Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); } - var individualIndex = _collectionManager.Active.Individuals.Index(identifier); - - if (oldCollection == null) - { - if (type.IsSpecial()) - { - _collectionManager.Active.CreateSpecialCollection(type); - } - else if (identifier.IsValid) - { - var identifiers = _collectionManager.Active.Individuals.GetGroup(identifier); - individualIndex = _collectionManager.Active.Individuals.Count; - _collectionManager.Active.CreateIndividualCollection(identifiers); - } - } - else if (collection == null) - { - if (type.IsSpecial()) - { - _collectionManager.Active.RemoveSpecialCollection(type); - } - else if (individualIndex >= 0) - { - _collectionManager.Active.RemoveIndividualCollection(individualIndex); - } - else - { - _chat.Print( - $"Can not remove the {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); - return false; - } - - Print( - $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); - return true; - } - - _collectionManager.Active.SetCollection(collection!, type, individualIndex); - Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); - return true; + return anySuccess; } private bool SetMod(string arguments) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index c41d651e..0048dc8c 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -60,7 +60,8 @@ public unsafe class MetaState : IDisposable private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config, IGameInteropProvider interop) + ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config, + IGameInteropProvider interop) { _performance = performance; _communicator = communicator; From fb591429d60e6f6efa054be20dd9b70ffc1d0d59 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 03:19:05 +0200 Subject: [PATCH 1208/2451] Update Sigs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index acf8cf68..5b23ba04 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit acf8cf68f06e37bbe64f1100a68937da9efb762c +Subproject commit 5b23ba04e5fd0e934c882c38024ab1b5661b44e1 From 5fefdfa33b7a2637e74d623ff437f4a5c23f1c49 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 14:03:51 +0200 Subject: [PATCH 1209/2451] Fix error in log about existing command. --- Penumbra/CommandHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 71bd19c2..11ccca71 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -47,7 +47,8 @@ public class CommandHandler : IDisposable _collectionEditor = collectionEditor; framework.RunOnFrameworkThread(() => { - _commandManager.RemoveHandler(CommandName); + if (_commandManager.Commands.ContainsKey(CommandName)) + _commandManager.RemoveHandler(CommandName); _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", From 58b5c4415771535b4739891ac7dced328ea81ae5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 14:35:03 +0200 Subject: [PATCH 1210/2451] Fix an issue with memory locations that suddenly caused issues? --- .../Interop/ResourceLoading/ResourceLoader.cs | 7 +++--- .../ResourceLoading/ResourceService.cs | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 94fdcce4..b8cc4742 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -76,8 +76,7 @@ public unsafe class ResourceLoader : IDisposable } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, - Utf8GamePath original, - GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (returnValue != null) return; @@ -93,7 +92,7 @@ public unsafe class ResourceLoader : IDisposable if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, ref category, ref type, ref hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; } @@ -103,7 +102,7 @@ public unsafe class ResourceLoader : IDisposable hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, ref category, ref type, ref hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 47107f44..792f8a8e 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -110,18 +110,26 @@ public unsafe class ResourceService : IDisposable if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk); + return GetOriginalResource(isSync, categoryId, resourceType, resourceHash, gamePath.Path.Path, pGetResParams, isUnk); } - /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path, + private ResourceHandle* GetOriginalResource(bool sync, ResourceCategory* categoryId, ResourceType* type, int* hash, byte* path, GetResourceParameters* resourceParameters = null, bool unk = false) => sync - ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, categoryId, type, hash, path, resourceParameters) - : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, - unk); + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, categoryId, type, hash, path, + resourceParameters, unk); + + /// Call the original GetResource function. + public ResourceHandle* GetOriginalResource(bool sync, ref ResourceCategory categoryId, ref ResourceType type, ref int hash, ByteString path, + GetResourceParameters* resourceParameters = null, bool unk = false) + { + var ptrCategory = (ResourceCategory*)Unsafe.AsPointer(ref categoryId); + var ptrType = (ResourceType*)Unsafe.AsPointer(ref type); + var ptrHash = (int*)Unsafe.AsPointer(ref hash); + return GetOriginalResource(sync, ptrCategory, ptrType, ptrHash, path.Path, resourceParameters, unk); + } #endregion From a18ace433abe08fa0d62b75700b42595942cae60 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 4 Oct 2023 12:37:47 +0000 Subject: [PATCH 1211/2451] [CI] Updating repo.json for testing_0.8.0.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 639eb985..65368656 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.0.0", - "TestingAssemblyVersion": "0.8.0.0", + "TestingAssemblyVersion": "0.8.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 83ab8e80033aff2fd02046f78f8365f505aeb32f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 16:16:08 +0200 Subject: [PATCH 1212/2451] Temporarily fix ShaderPackage.MaterialElement. --- Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 9d2fc9eb..3ef31382 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -80,7 +80,8 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) { - ref var parameter = ref _shaderPackage->MaterialElements[i]; + // TODO fix when CS updated + ref var parameter = ref ((ShaderPackage.MaterialElement*) ((byte*)_shaderPackage + 0xA0))[i]; if (parameter.CRC == parameterCrc) { if ((parameter.Offset & 0x3) != 0 From 8e0877659f009253c830e32391dc403dc0c4c664 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 16:17:03 +0200 Subject: [PATCH 1213/2451] Update LoadCharacterSound. --- .../PathResolving/AnimationHookService.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 7f69ceef..9b089658 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -21,7 +21,7 @@ public unsafe class AnimationHookService : IDisposable private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly CollectionResolver _resolver; - private readonly ICondition _conditions; + private readonly ICondition _conditions; private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); @@ -111,17 +111,19 @@ public unsafe class AnimationHookService : IDisposable } /// Characters load some of their voice lines or whatever with this function. - private delegate IntPtr LoadCharacterSound(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7); + private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); + // TODO: Use ClientStructs [Signature(Sigs.LoadCharacterSound, DetourName = nameof(LoadCharacterSoundDetour))] private readonly Hook _loadCharacterSoundHook = null!; - private IntPtr LoadCharacterSoundDetour(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7) + private nint LoadCharacterSoundDetour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) { using var performance = _performance.Measure(PerformanceType.LoadSound); var last = _characterSoundData.Value; - _characterSoundData.Value = _collectionResolver.IdentifyCollection((GameObject*)character, true); - var ret = _loadCharacterSoundHook.Original(character, unk1, unk2, unk3, unk4, unk5, unk6, unk7); + var character = *(GameObject**)(container + 8); + _characterSoundData.Value = _collectionResolver.IdentifyCollection(character, true); + var ret = _loadCharacterSoundHook.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); _characterSoundData.Value = last; return ret; } @@ -130,12 +132,12 @@ public unsafe class AnimationHookService : IDisposable /// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. /// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. /// - private delegate ulong LoadTimelineResourcesDelegate(IntPtr timeline); + private delegate ulong LoadTimelineResourcesDelegate(nint timeline); [Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))] private readonly Hook _loadTimelineResourcesHook = null!; - private ulong LoadTimelineResourcesDetour(IntPtr timeline) + private ulong LoadTimelineResourcesDetour(nint timeline) { using var performance = _performance.Measure(PerformanceType.TimelineResources); // Do not check timeline loading in cutscenes. @@ -153,12 +155,12 @@ public unsafe class AnimationHookService : IDisposable /// Probably used when the base idle animation gets loaded. /// Make it aware of the correct collection to load the correct pap files. /// - private delegate void CharacterBaseNoArgumentDelegate(IntPtr drawBase); + private delegate void CharacterBaseNoArgumentDelegate(nint drawBase); [Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))] private readonly Hook _characterBaseLoadAnimationHook = null!; - private void CharacterBaseLoadAnimationDetour(IntPtr drawObject) + private void CharacterBaseLoadAnimationDetour(nint drawObject) { using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation); var last = _animationLoadData.Value; @@ -171,17 +173,17 @@ public unsafe class AnimationHookService : IDisposable } /// Unknown what exactly this is but it seems to load a bunch of paps. - private delegate void LoadSomePap(IntPtr a1, int a2, IntPtr a3, int a4); + private delegate void LoadSomePap(nint a1, int a2, nint a3, int a4); [Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))] private readonly Hook _loadSomePapHook = null!; - private void LoadSomePapDetour(IntPtr a1, int a2, IntPtr a3, int a4) + private void LoadSomePapDetour(nint a1, int a2, nint a3, int a4) { using var performance = _performance.Measure(PerformanceType.LoadPap); var timelinePtr = a1 + Offsets.TimeLinePtr; var last = _animationLoadData.Value; - if (timelinePtr != IntPtr.Zero) + if (timelinePtr != nint.Zero) { var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); if (actorIdx >= 0 && actorIdx < _objects.Length) @@ -206,12 +208,12 @@ public unsafe class AnimationHookService : IDisposable } /// Load a VFX specifically for a character. - private delegate IntPtr LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); + private delegate nint LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); [Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))] private readonly Hook _loadCharacterVfxHook = null!; - private IntPtr LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) + private nint LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) { using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx); var last = _animationLoadData.Value; @@ -296,7 +298,7 @@ public unsafe class AnimationHookService : IDisposable { try { - if (timeline != IntPtr.Zero) + if (timeline != nint.Zero) { var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; var idx = getGameObjectIdx(timeline); From c21cbcdcd36f7a5d6f724d2c5d722f0460ed3735 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 4 Oct 2023 14:20:40 +0000 Subject: [PATCH 1214/2451] [CI] Updating repo.json for testing_0.8.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 65368656..77bbc8c3 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.0.0", - "TestingAssemblyVersion": "0.8.0.1", + "TestingAssemblyVersion": "0.8.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 069929ce24feee1edfcaaeef4c7bbfaf35086a2d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 20:30:04 +0200 Subject: [PATCH 1215/2451] Some updates. --- .../MaterialPreview/LiveMaterialPreviewer.cs | 2 +- .../Interop/MaterialPreview/MaterialInfo.cs | 10 +++++----- .../PathResolving/AnimationHookService.cs | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 3ef31382..15989638 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -81,7 +81,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) { // TODO fix when CS updated - ref var parameter = ref ((ShaderPackage.MaterialElement*) ((byte*)_shaderPackage + 0xA0))[i]; + ref var parameter = ref ((ShaderPackage.MaterialElement*) ((byte*)_shaderPackage + 0x98))[i]; if (parameter.CRC == parameterCrc) { if ((parameter.Offset & 0x3) != 0 diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 7dd6f983..c64e4d0b 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -33,9 +33,9 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy return type switch { DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(), - DrawObjectType.Mainhand => *((nint*)&gameObject->DrawData.MainHand + 1), - DrawObjectType.Offhand => *((nint*)&gameObject->DrawData.OffHand + 1), - DrawObjectType.Vfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), + DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject, + DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject, + DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject, _ => nint.Zero, }; } @@ -72,7 +72,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy if (gameObject == null) continue; - var index = (ObjectIndex) gameObject->GameObject.ObjectIndex; + var index = (ObjectIndex)gameObject->GameObject.ObjectIndex; foreach (var type in Enum.GetValues()) { @@ -93,7 +93,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); if (path == needle) result.Add(new MaterialInfo(index, type, i, j)); } diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 9b089658..7fa0ed35 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.Api.Enums; @@ -37,6 +38,10 @@ public unsafe class AnimationHookService : IDisposable _conditions = conditions; interop.InitializeFromAttributes(this); + _loadCharacterSoundHook = + interop.HookFromAddress( + (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, + LoadCharacterSoundDetour); _loadCharacterSoundHook.Enable(); _loadTimelineResourcesHook.Enable(); @@ -113,9 +118,7 @@ public unsafe class AnimationHookService : IDisposable /// Characters load some of their voice lines or whatever with this function. private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); - // TODO: Use ClientStructs - [Signature(Sigs.LoadCharacterSound, DetourName = nameof(LoadCharacterSoundDetour))] - private readonly Hook _loadCharacterSoundHook = null!; + private readonly Hook _loadCharacterSoundHook; private nint LoadCharacterSoundDetour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) { @@ -194,16 +197,18 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = last; } + private delegate void SomeActionLoadDelegate(ActionTimelineManager* timelineManager); + /// Seems to load character actions when zoning or changing class, maybe. [Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))] - private readonly Hook _someActionLoadHook = null!; + private readonly Hook _someActionLoadHook = null!; - private void SomeActionLoadDetour(nint gameObject) + private void SomeActionLoadDetour(ActionTimelineManager* timelineManager) { using var performance = _performance.Measure(PerformanceType.LoadAction); var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)gameObject, true); - _someActionLoadHook.Original(gameObject); + _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); + _someActionLoadHook.Original(timelineManager); _animationLoadData.Value = last; } From 53f1efa88b13d91203ec44c17b62a00a2a873eec Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 4 Oct 2023 18:33:22 +0000 Subject: [PATCH 1216/2451] [CI] Updating repo.json for testing_0.8.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 77bbc8c3..e6c3ad8e 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.0.0", - "TestingAssemblyVersion": "0.8.0.2", + "TestingAssemblyVersion": "0.8.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0aeb407a012dec22d75ef40d0dcf585f965c3e3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 4 Oct 2023 22:02:23 +0200 Subject: [PATCH 1217/2451] Update CopyCharacterEvent. --- Penumbra/Interop/Services/GameEventManager.cs | 15 ++++++++---- Penumbra/Interop/Services/RedrawService.cs | 24 +++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index 59714ff0..d11b7159 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -24,6 +24,10 @@ public unsafe class GameEventManager : IDisposable public GameEventManager(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); + + _copyCharacterHook = + interop.HookFromAddress((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour); + _characterDtorHook.Enable(); _copyCharacterHook.Enable(); _resourceHandleDestructorHook.Enable(); @@ -78,19 +82,20 @@ public unsafe class GameEventManager : IDisposable #region Copy Character - private delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk); + private delegate ulong CopyCharacterDelegate(CharacterSetup* target, GameObject* source, uint unk); - [Signature(Sigs.CopyCharacter, DetourName = nameof(CopyCharacterDetour))] - private readonly Hook _copyCharacterHook = null!; + private readonly Hook _copyCharacterHook; - private ulong CopyCharacterDetour(GameObject* target, GameObject* source, uint unk) + private ulong CopyCharacterDetour(CharacterSetup* target, GameObject* source, uint unk) { + // TODO: update when CS updated. + var character = ((Character**)target)[1]; if (CopyCharacter != null) foreach (var subscriber in CopyCharacter.GetInvocationList()) { try { - ((CopyCharacterEvent)subscriber).Invoke((Character*)target, (Character*)source); + ((CopyCharacterEvent)subscriber).Invoke(character, (Character*)source); } catch (Exception ex) { diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 0864bb71..5cc493ad 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -1,4 +1,3 @@ -using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Enums; @@ -20,8 +19,10 @@ public unsafe partial class RedrawService public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; private readonly string?[] _gPoseNames = new string?[GPoseSlots]; - private int _gPoseNameCounter = 0; - private bool _inGPose = false; + private int _gPoseNameCounter; + + private bool InGPose + => _clientState.IsGPosing; // VFuncs that disable and enable draw, used only for GPose actors. private static void DisableDraw(GameObject actor) @@ -33,10 +34,7 @@ public unsafe partial class RedrawService // Check whether we currently are in GPose. // Also clear the name list. private void SetGPose() - { - _inGPose = _objects[GPosePlayerIdx] != null; - _gPoseNameCounter = 0; - } + => _gPoseNameCounter = 0; private static bool IsGPoseActor(int idx) => idx is >= GPosePlayerIdx and < GPoseEndIdx; @@ -50,7 +48,7 @@ public unsafe partial class RedrawService private bool FindCorrectActor(int idx, out GameObject? obj) { obj = _objects[idx]; - if (!_inGPose || obj == null || IsGPoseActor(idx)) + if (!InGPose || obj == null || IsGPoseActor(idx)) return false; var name = obj.Name.ToString(); @@ -100,10 +98,11 @@ public unsafe partial class RedrawService public sealed unsafe partial class RedrawService : IDisposable { - private readonly IFramework _framework; + private readonly IFramework _framework; private readonly IObjectTable _objects; private readonly ITargetManager _targets; - private readonly ICondition _conditions; + private readonly ICondition _conditions; + private readonly IClientState _clientState; private readonly List _queue = new(100); private readonly List _afterGPoseQueue = new(GPoseSlots); @@ -111,12 +110,13 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions) + public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState) { _framework = framework; _objects = objects; _targets = targets; _conditions = conditions; + _clientState = clientState; _framework.Update += OnUpdateEvent; } @@ -241,7 +241,7 @@ public sealed unsafe partial class RedrawService : IDisposable private void HandleAfterGPose() { - if (_afterGPoseQueue.Count == 0 || _inGPose) + if (_afterGPoseQueue.Count == 0 || InGPose) return; var numKept = 0; From 30a55d401f111ecd0dbf973019cb4c850a2b0164 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 4 Oct 2023 20:06:28 +0000 Subject: [PATCH 1218/2451] [CI] Updating repo.json for testing_0.8.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e6c3ad8e..1c13cdab 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.0.0", - "TestingAssemblyVersion": "0.8.0.3", + "TestingAssemblyVersion": "0.8.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 73b4227310d59e81aa23081aae07c5cd69736bd8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 16:23:09 +0200 Subject: [PATCH 1219/2451] Fix slash commands. --- Penumbra/CommandHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 11ccca71..b6c675a2 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -314,7 +314,7 @@ public class CommandHandler : IDisposable } var anySuccess = false; - foreach (var identifier in identifiers.Distinct()) + foreach (var identifier in identifiers.Distinct().DefaultIfEmpty(ActorIdentifier.Invalid)) { var oldCollection = _collectionManager.Active.ByType(type, identifier); if (collection == oldCollection) @@ -360,6 +360,7 @@ public class CommandHandler : IDisposable Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); anySuccess = true; + continue; } _collectionManager.Active.SetCollection(collection!, type, individualIndex); From 8b5437c2c70237b786716c1fd82cf89361de4442 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 17:39:38 +0200 Subject: [PATCH 1220/2451] Remove CS temp fix. --- Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 15989638..420e929f 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -80,8 +80,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) { - // TODO fix when CS updated - ref var parameter = ref ((ShaderPackage.MaterialElement*) ((byte*)_shaderPackage + 0x98))[i]; + ref var parameter = ref _shaderPackage->MaterialElementsSpan[i]; if (parameter.CRC == parameterCrc) { if ((parameter.Offset & 0x3) != 0 From c487fb12ec0eeeb1c023c26d26390aafe75c87a6 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 5 Oct 2023 17:50:38 +0200 Subject: [PATCH 1221/2451] CS-ify LiveMaterialPreviewer and add new shpk name --- .../MaterialPreview/LiveMaterialPreviewer.cs | 29 +++++++++---------- .../ModEditWindow.Materials.Shpk.cs | 1 + 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 420e929f..fa03ac49 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -26,14 +26,11 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (_shaderPackage == null) throw new InvalidOperationException("Material doesn't have a shader package"); - var material = (Structs.Material*)Material; + var material = Material; - _originalShPkFlags = material->ShaderPackageFlags; + _originalShPkFlags = material->ShaderFlags; - if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) - _originalMaterialParameter = materialParameter.ToArray(); - else - _originalMaterialParameter = Array.Empty(); + _originalMaterialParameter = material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); _originalSamplerFlags = new uint[material->TextureCount]; for (var i = 0; i < _originalSamplerFlags.Length; ++i) @@ -46,11 +43,12 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (reset) { - var material = (Structs.Material*)Material; + var material = Material; - material->ShaderPackageFlags = _originalShPkFlags; + material->ShaderFlags = _originalShPkFlags; - if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + var materialParameter = material->MaterialParameterCBuffer->TryGetBuffer(); + if (!materialParameter.IsEmpty) _originalMaterialParameter.AsSpan().CopyTo(materialParameter); for (var i = 0; i < _originalSamplerFlags.Length; ++i) @@ -71,11 +69,12 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (!CheckValidity()) return; - var constantBuffer = ((Structs.Material*)Material)->MaterialParameter; + var constantBuffer = Material->MaterialParameterCBuffer; if (constantBuffer == null) return; - if (!constantBuffer->TryGetBuffer(out var buffer)) + var buffer = constantBuffer->TryGetBuffer(); + if (buffer.IsEmpty) return; for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) @@ -102,10 +101,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase var id = 0u; var found = false; - var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; + var samplers = _shaderPackage->Samplers; for (var i = 0; i < _shaderPackage->SamplerCount; ++i) { - if (samplers[i].Crc == samplerCrc) + if (samplers[i].CRC == samplerCrc) { id = samplers[i].Id; found = true; @@ -116,7 +115,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (!found) return; - var material = (Structs.Material*)Material; + var material = Material; for (var i = 0; i < material->TextureCount; ++i) { if (material->Textures[i].Id == id) @@ -136,7 +135,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (mtrlHandle == null) return false; - var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + var shpkHandle = mtrlHandle->ShaderPackageResourceHandle; if (shpkHandle == null) return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 25869d9c..9e9557d3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -36,6 +36,7 @@ public partial class ModEditWindow "bguvscroll.shpk", "channeling.shpk", "characterglass.shpk", + "charactershadowoffset.shpk", "character.shpk", "cloud.shpk", "createviewposition.shpk", From 779d6b37a55c753e5a8ea8ae52adf971874ee9af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 18:20:41 +0200 Subject: [PATCH 1222/2451] Improved messaging. --- OtterGui | 2 +- Penumbra.Api | 2 +- .../Manager/ActiveCollectionMigration.cs | 7 ++--- .../Collections/Manager/ActiveCollections.cs | 20 +++++-------- .../Collections/Manager/CollectionStorage.cs | 29 ++++++++----------- .../Manager/IndividualCollections.Files.cs | 24 +++++++-------- .../Collections/Manager/InheritanceManager.cs | 9 +++--- Penumbra/Configuration.cs | 4 +-- Penumbra/Mods/Editor/ModMerger.cs | 5 ++-- Penumbra/Mods/Editor/ModNormalizer.cs | 21 ++++++-------- Penumbra/Mods/Manager/ModImportManager.cs | 4 +-- Penumbra/Mods/Manager/ModOptionEditor.cs | 5 ++-- Penumbra/Mods/ModCreator.cs | 4 +-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 ++-- Penumbra/Penumbra.cs | 6 ++-- Penumbra/Services/BackupService.cs | 2 +- .../{ChatService.cs => MessageService.cs} | 14 ++++----- Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/Services/ValidityChecker.cs | 4 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 +- .../ModEditWindow.Materials.MtrlTab.cs | 3 +- .../ModEditWindow.ShaderPackages.cs | 20 ++++++------- Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 14 ++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 25 ++++++++-------- Penumbra/UI/Tabs/ConfigTabBar.cs | 17 +++++++---- Penumbra/UI/Tabs/MessagesTab.cs | 21 ++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 4 +-- Penumbra/UI/UiHelpers.cs | 3 +- 30 files changed, 146 insertions(+), 139 deletions(-) rename Penumbra/Services/{ChatService.cs => MessageService.cs} (80%) create mode 100644 Penumbra/UI/Tabs/MessagesTab.cs diff --git a/OtterGui b/OtterGui index df07c4ed..96c9055a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit df07c4ed08e8e6c1188867c7863a19e02c8adb53 +Subproject commit 96c9055a1d8a19d9cdb61f41ddfb372871e204ac diff --git a/Penumbra.Api b/Penumbra.Api index bc9bd5f6..839cc8f2 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit bc9bd5f6bb06e61069704733841868307aed7a5c +Subproject commit 839cc8f270abb6a938d71596bef05b4a9b3ab0ea diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 5872dea1..2f9e9b15 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Classes; using Penumbra.Services; namespace Penumbra.Collections.Manager; @@ -46,10 +47,8 @@ public static class ActiveCollectionMigration { if (!storage.ByName(collectionName, out var collection)) { - Penumbra.Chat.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", - "Load Failure", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); dict.Add(player, ModCollection.Empty); } else diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 7e6d691e..3da009a3 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; @@ -331,10 +332,8 @@ public class ActiveCollections : ISavable, IDisposable ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); if (!_storage.ByName(defaultName, out var defaultCollection)) { - Penumbra.Chat.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", - "Load Failure", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; } @@ -347,9 +346,8 @@ public class ActiveCollections : ISavable, IDisposable var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; if (!_storage.ByName(interfaceName, out var interfaceCollection)) { - Penumbra.Chat.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", - "Load Failure", NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; } @@ -362,9 +360,8 @@ public class ActiveCollections : ISavable, IDisposable var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; if (!_storage.ByName(currentName, out var currentCollection)) { - Penumbra.Chat.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", - "Load Failure", NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; } @@ -381,8 +378,7 @@ public class ActiveCollections : ISavable, IDisposable { if (!_storage.ByName(typeName, out var typeCollection)) { - Penumbra.Chat.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", - "Load Failure", + Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", NotificationType.Warning); configChanged = true; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index e50b9bdb..70b2cd13 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; @@ -102,9 +103,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (!CanAddCollection(name, out var fixedName)) { - Penumbra.Chat.NotificationMessage( - $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, false); return false; } @@ -113,8 +113,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); - Penumbra.Chat.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", - NotificationType.Success); + Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); return true; } @@ -126,13 +125,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) { - Penumbra.Chat.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error); + Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); return false; } if (collection.Index == DefaultNamed.Index) { - Penumbra.Chat.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error); + Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); return false; } @@ -142,7 +141,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable for (var i = collection.Index; i < Count; ++i) _collections[i].Index = i; - Penumbra.Chat.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success); + Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } @@ -185,23 +184,20 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!IsValidName(name)) { // TODO: handle better. - Penumbra.Chat.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", - "Warning", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", NotificationType.Warning); continue; } if (ByName(name, out _)) { - Penumbra.Chat.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", - "Warning", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", NotificationType.Warning); continue; } var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.Chat.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", NotificationType.Warning); _collections.Add(collection); } @@ -221,9 +217,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (AddCollection(ModCollection.DefaultCollectionName, null)) return _collections[^1]; - Penumbra.Chat.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", "Error", - NotificationType.Error); + Penumbra.Messager.NotificationMessage( + $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 21a0c730..fa6019c6 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; +using OtterGui.Classes; using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; @@ -56,7 +57,7 @@ public partial class IndividualCollections if (group.Length == 0 || group.Any(i => !i.IsValid)) { changes = true; - Penumbra.Chat.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure", + Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", NotificationType.Warning); continue; } @@ -65,9 +66,8 @@ public partial class IndividualCollections if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) { changes = true; - Penumbra.Chat.NotificationMessage( + Penumbra.Messager.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", - "Load Failure", NotificationType.Warning); continue; } @@ -75,16 +75,14 @@ public partial class IndividualCollections if (!Add(group, collection)) { changes = true; - Penumbra.Chat.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", - "Load Failure", + Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", NotificationType.Warning); } } catch (Exception e) { changes = true; - Penumbra.Chat.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error); } } @@ -124,9 +122,9 @@ public partial class IndividualCollections if (Add($"{_actorService.AwaitedService.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else - Penumbra.Chat.NotificationMessage( + Penumbra.Messager.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", - "Migration Failure", NotificationType.Error); + NotificationType.Error); } // If it is not a valid NPC name, check if it can be a player name. else if (ActorManager.VerifyPlayerName(name)) @@ -140,15 +138,15 @@ public partial class IndividualCollections }, collection)) Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); else - Penumbra.Chat.NotificationMessage( + Penumbra.Messager.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", - "Migration Failure", NotificationType.Error); + NotificationType.Error); } else { - Penumbra.Chat.NotificationMessage( + Penumbra.Messager.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", - "Migration Failure", NotificationType.Error); + NotificationType.Error); } } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 4c1bdc5a..771f9463 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -143,14 +144,12 @@ public class InheritanceManager : IDisposable continue; changes = true; - Penumbra.Chat.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); } else { - Penumbra.Chat.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning); changes = true; } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 7c9ae665..d221b4a2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -137,9 +137,9 @@ public class Configuration : IPluginConfiguration, ISavable } catch (Exception ex) { - Penumbra.Chat.NotificationMessage(ex, + Penumbra.Messager.NotificationMessage(ex, "Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Penumbra directory.", - "Error reading Configuration", "Error", NotificationType.Error); + "Error reading Configuration", NotificationType.Error); } migrator.Migrate(utility, this); diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f90f6c0a..37ffdcfe 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using OtterGui; +using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Meta.Manipulations; @@ -81,9 +82,7 @@ public class ModMerger : IDisposable catch (Exception ex) { Error = ex; - Penumbra.Chat.NotificationMessage( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.:\n{ex}", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(ex, $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.", NotificationType.Error, false); FailureCleanup(); DataCleanup(); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index eebc8ab4..3610c99a 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Classes; using OtterGui.Tasks; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -74,7 +75,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not normalize mod {Mod.Name}.", NotificationType.Error, false); } finally { @@ -87,17 +88,15 @@ public class ModNormalizer { if (Directory.Exists(_normalizationDirName)) { - Penumbra.Chat.NotificationMessage("Could not normalize mod:\n" - + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage($"Could not normalize mod {Mod.Name}:\n" + + "The directory TmpNormalization may not already exist when normalizing a mod.", NotificationType.Error, false); return false; } if (Directory.Exists(_oldDirName)) { - Penumbra.Chat.NotificationMessage("Could not normalize mod:\n" - + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage($"Could not normalize mod {Mod.Name}:\n" + + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", NotificationType.Error, false); return false; } @@ -201,7 +200,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not normalize mod {Mod.Name}.", NotificationType.Error, false); } return false; @@ -229,8 +228,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not move old files out of the way while normalizing mod {Mod.Name}.", NotificationType.Error, false); } return false; @@ -253,8 +251,7 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not move new files into the mod while normalizing mod {Mod.Name}.", NotificationType.Error, false); foreach (var dir in Mod.ModPath.EnumerateDirectories()) { if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase) diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 96cf146b..73571ea4 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; using Penumbra.Import; using Penumbra.Mods.Editor; @@ -42,8 +43,7 @@ public class ModImportManager : IDisposable if (File.Exists(s)) return true; - Penumbra.Chat.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, false); return false; }).Select(s => new FileInfo(s)).ToArray(); diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 3eeb13c6..0a3034fc 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; @@ -395,9 +396,9 @@ public class ModOptionEditor return true; if (message) - Penumbra.Chat.NotificationMessage( + Penumbra.Messager.NotificationMessage( $"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); + NotificationType.Warning, false); return false; } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 236f1539..98770edc 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.GameData; @@ -45,8 +46,7 @@ public partial class ModCreator } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not create directory for new Mod {newName}.", NotificationType.Error, false); return null; } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 4d29c58d..07f84722 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; @@ -54,9 +55,8 @@ public sealed class MultiModGroup : IModGroup { if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - Penumbra.Chat.NotificationMessage( - $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", - NotificationType.Warning); + Penumbra.Messager.NotificationMessage( + $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", NotificationType.Warning); break; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0519baae..73d1013e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -31,7 +31,7 @@ public class Penumbra : IDalamudPlugin => "Penumbra"; public static readonly Logger Log = new(); - public static ChatService Chat { get; private set; } = null!; + public static MessageService Messager { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -55,7 +55,7 @@ public class Penumbra : IDalamudPlugin var startTimer = new StartTracker(); using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); - Chat = _services.GetRequiredService(); + Messager = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() @@ -118,7 +118,7 @@ public class Penumbra : IDalamudPlugin _communicatorService.ChangedItemClick.Subscribe((button, it) => { if (button == MouseButton.Left && it is Item item) - Chat.LinkItem(item); + Messager.LinkItem(item); }, ChangedItemClick.Priority.Link); } diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index f6e2c3e4..e623be3e 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -41,7 +41,7 @@ public class BackupService { Penumbra.Log.Error($"Failed to load {fileName}, trying to restore from backup:\n{ex}"); Backup.TryGetFile(new DirectoryInfo(fileNames.ConfigDirectory), fileName, out ret, out var messages, JObject.Parse); - Penumbra.Chat.NotificationMessage(messages); + Penumbra.Messager.NotificationMessage(messages); } return ret; diff --git a/Penumbra/Services/ChatService.cs b/Penumbra/Services/MessageService.cs similarity index 80% rename from Penumbra/Services/ChatService.cs rename to Penumbra/Services/MessageService.cs index 3e715a4f..c893b00f 100644 --- a/Penumbra/Services/ChatService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,20 +1,18 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Plugin; +using Dalamud.Interface; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; namespace Penumbra.Services; -public class ChatService : OtterGui.Classes.ChatService +public class MessageService : OtterGui.Classes.MessageService { - private readonly IChatGui _chat; - - public ChatService(Logger log, DalamudPluginInterface pi, IChatGui chat) - : base(log, pi) - => _chat = chat; + public MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) + : base(log, uiBuilder, chat) + { } public void LinkItem(Item item) { @@ -37,7 +35,7 @@ public class ChatService : OtterGui.Classes.ChatService var payload = new SeString(payloadList); - _chat.Print(new XivChatEntry + Chat.Print(new XivChatEntry { Message = payload, }); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 9e3b9b1a..84f89f6d 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -65,7 +65,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -165,6 +165,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index b8d9b30a..749da5b9 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; +using OtterGui.Classes; namespace Penumbra.Services; @@ -33,8 +34,7 @@ public class ValidityChecker public void LogExceptions() { if (ImcExceptions.Count > 0) - Penumbra.Chat.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", - "Warning", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", NotificationType.Warning); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 9354afaf..b84fa84c 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -137,7 +137,7 @@ public class FileEditor : IDisposable where T : class, IWritable } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not export {_defaultPath}.", NotificationType.Error); } }, _getInitialPath(), false); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 967e8d01..8597bc0c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -4,6 +4,7 @@ using Dalamud.Utility; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; @@ -321,7 +322,7 @@ public class ItemSwapTab : IDisposable, ITab } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { if (optionCreated && _selectedGroup != null) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index ebe980d7..20efe757 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -113,8 +113,7 @@ public partial class ModEditWindow LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - Penumbra.Chat.NotificationMessage($"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); } if (LoadedShpkPath.InternalName.IsEmpty) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 6b867b27..804feae1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -4,6 +4,7 @@ using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; using OtterGui; +using OtterGui.Classes; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; @@ -87,15 +88,14 @@ public partial class ModEditWindow } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", - "Penumbra Advanced Editing", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not export {defaultName}{tab.Extension} to {name}.", + NotificationType.Error, false); return; } - Penumbra.Chat.NotificationMessage( - $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}", - "Penumbra Advanced Editing", NotificationType.Success); + Penumbra.Messager.NotificationMessage( + $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}.", + NotificationType.Success, false); }, null, false); } @@ -116,8 +116,7 @@ public partial class ModEditWindow } catch (Exception e) { - Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not import {name}.", NotificationType.Error, false); return; } @@ -129,9 +128,8 @@ public partial class ModEditWindow catch (Exception e) { tab.Shpk.SetInvalid(); - Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}", - "Penumbra Advanced Editing", - NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Failed to update resources after importing {name}.", NotificationType.Error, + false); return; } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 5cedd824..0f209686 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -142,7 +142,7 @@ public sealed class ConfigWindow : Window ImGui.NewLine(); ImGui.NewLine(); - CustomGui.DrawDiscordButton(Penumbra.Chat, 0); + CustomGui.DrawDiscordButton(Penumbra.Messager, 0); ImGui.SameLine(); UiHelpers.DrawSupportButton(_penumbra!); ImGui.NewLine(); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 215a0269..cc4ceb55 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -18,14 +18,14 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.Classes; -using ChatService = Penumbra.Services.ChatService; +using MessageService = Penumbra.Services.MessageService; namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector { private readonly CommunicatorService _communicator; - private readonly ChatService _chat; + private readonly MessageService _messager; private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; @@ -37,7 +37,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Penumbra.Chat.NotificationMessage(e.Message, "Failure", NotificationType.Warning); + => Penumbra.Messager.NotificationMessage(e, e.Message, NotificationType.Warning); #endregion diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 44c40247..18d0e613 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -18,15 +19,15 @@ namespace Penumbra.UI.ModsTab; public class ModPanelEditTab : ITab { - private readonly ChatService _chat; - private readonly FilenameService _filenames; - private readonly ModManager _modManager; - private readonly ModExportManager _modExportManager; - private readonly ModFileSystem _fileSystem; - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModEditor _editor; - private readonly Configuration _config; + private readonly Services.MessageService _messager; + private readonly FilenameService _filenames; + private readonly ModManager _modManager; + private readonly ModExportManager _modExportManager; + private readonly ModFileSystem _fileSystem; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModEditor _editor; + private readonly Configuration _config; private readonly TagButtons _modTags = new(); @@ -35,13 +36,13 @@ public class ModPanelEditTab : ITab private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; - public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, + public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config) { _modManager = modManager; _selector = selector; _fileSystem = fileSystem; - _chat = chat; + _messager = messager; _editWindow = editWindow; _editor = editor; _filenames = filenames; @@ -75,7 +76,7 @@ public class ModPanelEditTab : ITab } catch (Exception e) { - _chat.NotificationMessage(e.Message, "Warning", NotificationType.Warning); + _messager.NotificationMessage(e.Message, NotificationType.Warning, false); } UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index ee66ca86..1cc29d88 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -19,7 +19,8 @@ public class ConfigTabBar : IDisposable public readonly DebugTab Debug; public readonly ResourceTab Resource; public readonly Watcher Watcher; - public readonly OnScreenTab OnScreenTab; + public readonly OnScreenTab OnScreen; + public readonly MessagesTab Messages; public readonly ITab[] Tabs; @@ -28,7 +29,7 @@ public class ConfigTabBar : IDisposable public ConfigTabBar(CommunicatorService communicator, SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, DebugTab debug, ResourceTab resource, Watcher watcher, - OnScreenTab onScreenTab) + OnScreenTab onScreen, MessagesTab messages) { _communicator = communicator; @@ -40,7 +41,8 @@ public class ConfigTabBar : IDisposable Debug = debug; Resource = resource; Watcher = watcher; - OnScreenTab = onScreenTab; + OnScreen = onScreen; + Messages = messages; Tabs = new ITab[] { Settings, @@ -48,10 +50,11 @@ public class ConfigTabBar : IDisposable Mods, ChangedItems, Effective, - OnScreenTab, + OnScreen, Debug, Resource, Watcher, + Messages, }; _communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar); } @@ -75,10 +78,11 @@ public class ConfigTabBar : IDisposable TabType.Collections => Collections.Label, TabType.ChangedItems => ChangedItems.Label, TabType.EffectiveChanges => Effective.Label, - TabType.OnScreen => OnScreenTab.Label, + TabType.OnScreen => OnScreen.Label, TabType.ResourceWatcher => Watcher.Label, TabType.Debug => Debug.Label, TabType.ResourceManager => Resource.Label, + TabType.Messages => Messages.Label, _ => ReadOnlySpan.Empty, }; @@ -90,7 +94,8 @@ public class ConfigTabBar : IDisposable if (label == Settings.Label) return TabType.Settings; if (label == ChangedItems.Label) return TabType.ChangedItems; if (label == Effective.Label) return TabType.EffectiveChanges; - if (label == OnScreenTab.Label) return TabType.OnScreen; + if (label == OnScreen.Label) return TabType.OnScreen; + if (label == Messages.Label) return TabType.Messages; if (label == Watcher.Label) return TabType.ResourceWatcher; if (label == Debug.Label) return TabType.Debug; if (label == Resource.Label) return TabType.ResourceManager; diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/Tabs/MessagesTab.cs new file mode 100644 index 00000000..e834a4b4 --- /dev/null +++ b/Penumbra/UI/Tabs/MessagesTab.cs @@ -0,0 +1,21 @@ +using OtterGui.Widgets; +using Penumbra.Services; + +namespace Penumbra.UI.Tabs; + +public class MessagesTab : ITab +{ + public ReadOnlySpan Label + => "Messages"u8; + + private readonly MessageService _messages; + + public MessagesTab(MessageService messages) + => _messages = messages; + + public bool IsVisible + => _messages.Count > 0; + + public void DrawContent() + => _messages.Draw(); +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 7c350106..32bd6b8f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -851,10 +851,10 @@ public class SettingsTab : ITab UiHelpers.DrawSupportButton(_penumbra); ImGui.SetCursorPos(new Vector2(xPos, 0)); - CustomGui.DrawDiscordButton(Penumbra.Chat, width); + CustomGui.DrawDiscordButton(Penumbra.Messager, width); ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); - CustomGui.DrawGuideButton(Penumbra.Chat, width); + CustomGui.DrawGuideButton(Penumbra.Messager, width); ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index a37d8e77..6c64bd55 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Interop.Structs; using Penumbra.String; @@ -56,7 +57,7 @@ public static class UiHelpers var text = penumbra.GatherSupportInformation(); ImGui.SetClipboardText(text); - Penumbra.Chat.NotificationMessage($"Copied Support Info to Clipboard.", "Success", NotificationType.Success); + Penumbra.Messager.NotificationMessage($"Copied Support Info to Clipboard.", NotificationType.Success, false); } /// Draw a button to open a specific directory in a file explorer. From 52d38eda3a0617e6a2228561128e22238f22bb55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 18:21:47 +0200 Subject: [PATCH 1223/2451] 0.8.1.0 Changelog --- Penumbra/UI/Changelog.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 8a2b0fcb..c1a2671c 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -42,10 +42,21 @@ public class PenumbraChangelog Add7_2_0(Changelog); Add7_3_0(Changelog); Add8_0_0(Changelog); + Add8_1_0(Changelog); } #region Changelogs + private static void Add8_1_0(Changelog log) + => log.NextVersion("Version 0.8.1.0") + .RegisterImportant( + "Updated for 6.5 - Square Enix shuffled around a lot of things this update, so some things still might not work but have not been noticed yet. Please report any issues.") + .RegisterEntry("Added support for chat commands to affect multiple individuals matching the supplied string at once.") + .RegisterEntry( + "Improved messaging: many warnings or errors appearing will stay a little longer and can now be looked at in a Messages tab (visible only if there have been any).") + .RegisterEntry("Fixed an issue with leading or trailing spaces when renaming mods."); + + private static void Add8_0_0(Changelog log) => log.NextVersion("Version 0.8.0.0") .RegisterEntry( @@ -79,7 +90,9 @@ public class PenumbraChangelog .RegisterEntry( "Addition and removal of shader keys, textures, constants and a color table has been automated following shader requirements and can not be done manually anymore.", 1) - .RegisterEntry("Plain English names and tooltips can now be displayed instead of hexadecimal identifiers or code names by providing dev-kit files installed via certain mods.", 1) + .RegisterEntry( + "Plain English names and tooltips can now be displayed instead of hexadecimal identifiers or code names by providing dev-kit files installed via certain mods.", + 1) .RegisterEntry("The Texture editor has been improved (by Ny):") .RegisterHighlight("The overlay texture can now be combined in several ways and automatically resized to match the input texture.", 1) From dd587350ea432990eee0d75b892d1af7caa3742e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 18:30:57 +0200 Subject: [PATCH 1224/2451] Update changelog discord export. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 96c9055a..44ec7c38 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 96c9055a1d8a19d9cdb61f41ddfb372871e204ac +Subproject commit 44ec7c38442dad765746abcc0dc838ad3936a0b7 diff --git a/Penumbra.GameData b/Penumbra.GameData index 5b23ba04..f97df642 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 5b23ba04e5fd0e934c882c38024ab1b5661b44e1 +Subproject commit f97df642999d4edc6dce7fac64903485b5a2fa11 From 3b593103ba5962c7ad3f46429637ef7fa088336f Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 5 Oct 2023 16:33:35 +0000 Subject: [PATCH 1225/2451] [CI] Updating repo.json for 0.8.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1c13cdab..62c4be77 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.0.0", - "TestingAssemblyVersion": "0.8.0.4", + "AssemblyVersion": "0.8.1.0", + "TestingAssemblyVersion": "0.8.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From de3a74bbe88d17f3fadd6857ba0dbb36a1f750c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 20:29:34 +0200 Subject: [PATCH 1226/2451] Change SubModules to https --- .gitmodules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index c03525eb..ea1199ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,16 @@ [submodule "OtterGui"] path = OtterGui - url = git@github.com:Ottermandias/OtterGui.git + url = https://github.com/Ottermandias/OtterGui.git branch = main [submodule "Penumbra.Api"] path = Penumbra.Api - url = git@github.com:Ottermandias/Penumbra.Api.git + url = https://github.com/Ottermandias/Penumbra.Api.git branch = main [submodule "Penumbra.String"] path = Penumbra.String - url = git@github.com:Ottermandias/Penumbra.String.git + url = https://github.com/Ottermandias/Penumbra.String.git branch = main [submodule "Penumbra.GameData"] path = Penumbra.GameData - url = git@github.com:Ottermandias/Penumbra.GameData.git + url = https://github.com/Ottermandias/Penumbra.GameData.git branch = main From 422324b6d7fc77d5991b6b02e5a76e584db68ef0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 21:22:09 +0200 Subject: [PATCH 1227/2451] Revert stupid changes due to ResourceCategory change. --- Penumbra.GameData | 2 +- .../Interop/ResourceLoading/ResourceLoader.cs | 4 ++-- .../ResourceLoading/ResourceService.cs | 23 ++++++------------- Penumbra/UI/Tabs/DebugTab.cs | 2 +- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f97df642..34e3299f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f97df642999d4edc6dce7fac64903485b5a2fa11 +Subproject commit 34e3299f28c5e1d2b7d071ba8a3f851f5d1fa057 diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index b8cc4742..8ccdfa80 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -92,7 +92,7 @@ public unsafe class ResourceLoader : IDisposable if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, ref category, ref type, ref hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; } @@ -102,7 +102,7 @@ public unsafe class ResourceLoader : IDisposable hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, ref category, ref type, ref hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 792f8a8e..6fb2e560 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -110,26 +110,17 @@ public unsafe class ResourceService : IDisposable if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, categoryId, resourceType, resourceHash, gamePath.Path.Path, pGetResParams, isUnk); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk); } - private ResourceHandle* GetOriginalResource(bool sync, ResourceCategory* categoryId, ResourceType* type, int* hash, byte* path, - GetResourceParameters* resourceParameters = null, bool unk = false) - => sync - ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, categoryId, type, hash, path, - resourceParameters) - : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, categoryId, type, hash, path, - resourceParameters, unk); - /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ref ResourceCategory categoryId, ref ResourceType type, ref int hash, ByteString path, + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path, GetResourceParameters* resourceParameters = null, bool unk = false) - { - var ptrCategory = (ResourceCategory*)Unsafe.AsPointer(ref categoryId); - var ptrType = (ResourceType*)Unsafe.AsPointer(ref type); - var ptrHash = (int*)Unsafe.AsPointer(ref hash); - return GetOriginalResource(sync, ptrCategory, ptrType, ptrHash, path.Path, resourceParameters, unk); - } + => sync + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters) + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk); #endregion diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index b0715da1..5abb3c2f 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -858,7 +858,7 @@ public class DebugTab : Window, ITab return; ImGui.TableNextColumn(); - ImGui.TextUnformatted(r->Category.ToString()); + ImGui.TextUnformatted(((ResourceCategory)r->Type.Value).ToString()); ImGui.TableNextColumn(); ImGui.TextUnformatted(r->FileType.ToString("X")); ImGui.TableNextColumn(); From 110298f280afe74bc9e7e016cfa31a09e1f7b427 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Oct 2023 21:23:02 +0200 Subject: [PATCH 1228/2451] Increment Changelog Version. --- Penumbra/UI/Changelog.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c1a2671c..5540285b 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -42,13 +42,13 @@ public class PenumbraChangelog Add7_2_0(Changelog); Add7_3_0(Changelog); Add8_0_0(Changelog); - Add8_1_0(Changelog); + Add8_1_1(Changelog); } #region Changelogs - private static void Add8_1_0(Changelog log) - => log.NextVersion("Version 0.8.1.0") + private static void Add8_1_1(Changelog log) + => log.NextVersion("Version 0.8.1.1") .RegisterImportant( "Updated for 6.5 - Square Enix shuffled around a lot of things this update, so some things still might not work but have not been noticed yet. Please report any issues.") .RegisterEntry("Added support for chat commands to affect multiple individuals matching the supplied string at once.") From 9fda19d4c0621ca1676f03ea7d1533a7c8637b31 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 5 Oct 2023 19:25:42 +0000 Subject: [PATCH 1229/2451] [CI] Updating repo.json for 0.8.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 62c4be77..c8a04440 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.0", - "TestingAssemblyVersion": "0.8.1.0", + "AssemblyVersion": "0.8.1.1", + "TestingAssemblyVersion": "0.8.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 2d007c189fac933449b641d5d9869ffbd7749e37 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 6 Oct 2023 16:16:27 +0200 Subject: [PATCH 1230/2451] Fix deletion in selector. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 44ec7c38..c466bd33 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 44ec7c38442dad765746abcc0dc838ad3936a0b7 +Subproject commit c466bd33442dda3ade26f05c9e8d694443564118 From 7cdd8656eff237109c7ec4227ae98ddc4af8e5f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 6 Oct 2023 16:21:33 +0200 Subject: [PATCH 1231/2451] Maybe fix issue with individual assignments sometimes getting eaten. --- Penumbra.GameData | 2 +- Penumbra/Collections/Manager/IndividualCollections.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 34e3299f..ac0710e9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 34e3299f28c5e1d2b7d071ba8a3f851f5d1fa057 +Subproject commit ac0710e9a116bec8633f3dcde2c9b6e38dffaaa9 diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 4fe1e829..31695a94 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -132,8 +132,7 @@ public sealed partial class IndividualCollections }; return table.Where(kvp => kvp.Value == name) .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, - identifier.Kind, - kvp.Key)).ToArray(); + identifier.Kind, kvp.Key)).ToArray(); } return identifier.Type switch From 987142163242e3443bbdc404dc3909f453a20032 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 7 Oct 2023 15:31:18 +0200 Subject: [PATCH 1232/2451] 0.8.1.2 --- Penumbra/UI/Changelog.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 5540285b..7589e6ec 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -43,10 +43,16 @@ public class PenumbraChangelog Add7_3_0(Changelog); Add8_0_0(Changelog); Add8_1_1(Changelog); + Add8_1_2(Changelog); } #region Changelogs + private static void Add8_1_2(Changelog log) + => log.NextVersion("Version 0.8.1.2") + .RegisterEntry("Fixed an issue keeping mods selected after their deletion.") + .RegisterEntry("Maybe fixed an issue causing individual assignments to get lost on game start."); + private static void Add8_1_1(Changelog log) => log.NextVersion("Version 0.8.1.1") .RegisterImportant( From 717ddba8d2d2df76ca6f48a04201d471671a379b Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 7 Oct 2023 13:34:11 +0000 Subject: [PATCH 1233/2451] [CI] Updating repo.json for 0.8.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c8a04440..af898a90 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.1", - "TestingAssemblyVersion": "0.8.1.1", + "AssemblyVersion": "0.8.1.2", + "TestingAssemblyVersion": "0.8.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 4378c826f07bf6c1d9727c489ccfc133d448a874 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Oct 2023 13:19:01 +0200 Subject: [PATCH 1234/2451] Add some log statements. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Collections/Manager/ActiveCollections.cs | 3 +++ Penumbra/Collections/Manager/CollectionStorage.cs | 2 ++ .../Manager/IndividualCollections.Files.cs | 14 ++++++++++++-- Penumbra/Import/Structs/StreamDisposer.cs | 7 ++----- Penumbra/Util/PenumbraSqPackStream.cs | 4 ++++ 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index c466bd33..934e991f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c466bd33442dda3ade26f05c9e8d694443564118 +Subproject commit 934e991f9a39c7d864501532003b9548ef73f896 diff --git a/Penumbra.GameData b/Penumbra.GameData index ac0710e9..54b56ada 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ac0710e9a116bec8633f3dcde2c9b6e38dffaaa9 +Subproject commit 54b56ada57529221d3fc812193ffe65c424c1521 diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 3da009a3..0814da90 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -325,6 +325,7 @@ public class ActiveCollections : ISavable, IDisposable /// private void LoadCollections() { + Penumbra.Log.Debug("[Collections] Reading collection assignments..."); var configChanged = !Load(_saveService.FileNames, out var jObject); // Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed. @@ -389,6 +390,8 @@ public class ActiveCollections : ISavable, IDisposable } } + Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments."); + configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage); diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 70b2cd13..7c94d705 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -176,6 +176,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// private void ReadCollections(out ModCollection defaultNamedCollection) { + Penumbra.Log.Debug("[Collections] Reading saved collections..."); foreach (var file in _saveService.FileNames.CollectionFiles) { if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)) @@ -202,6 +203,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable } defaultNamedCollection = SetDefaultNamedCollection(); + Penumbra.Log.Debug($"[Collections] Found {Count} saved collections."); } /// diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index fa6019c6..45a1d98c 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -27,7 +27,10 @@ public partial class IndividualCollections public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage) { if (_actorService.Valid) - return ReadJObjectInternal(obj, storage); + { + var ret = ReadJObjectInternal(obj, storage); + return ret; + } void Func() { @@ -38,14 +41,19 @@ public partial class IndividualCollections _actorService.FinishedCreation -= Func; } + Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready..."); _actorService.FinishedCreation += Func; return false; } private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage) { + Penumbra.Log.Debug("[Collections] Reading individual assignments..."); if (obj == null) + { + Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments..."); return true; + } var changes = false; foreach (var data in obj) @@ -58,7 +66,7 @@ public partial class IndividualCollections { changes = true; Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", - NotificationType.Warning); + NotificationType.Error); continue; } @@ -86,6 +94,8 @@ public partial class IndividualCollections } } + Penumbra.Log.Debug($"Finished reading {Count} individual assignments..."); + return changes; } diff --git a/Penumbra/Import/Structs/StreamDisposer.cs b/Penumbra/Import/Structs/StreamDisposer.cs index 84719331..c38362ce 100644 --- a/Penumbra/Import/Structs/StreamDisposer.cs +++ b/Penumbra/Import/Structs/StreamDisposer.cs @@ -3,7 +3,7 @@ using Penumbra.Util; namespace Penumbra.Import.Structs; // Create an automatically disposing SqPack stream. -public class StreamDisposer : PenumbraSqPackStream, IDisposable +public class StreamDisposer : PenumbraSqPackStream { private readonly FileStream _fileStream; @@ -11,13 +11,10 @@ public class StreamDisposer : PenumbraSqPackStream, IDisposable : base(stream) => _fileStream = stream; - public new void Dispose() + protected override void Dispose(bool _) { var filePath = _fileStream.Name; - - base.Dispose(); _fileStream.Dispose(); - File.Delete(filePath); } } diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 562eca91..392730e2 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -327,8 +327,12 @@ public class PenumbraSqPackStream : IDisposable public void Dispose() { Reader.Dispose(); + Dispose(true); } + protected virtual void Dispose(bool _) + { } + public class PenumbraFileInfo { public uint HeaderSize; From 369992393802f8c42361d77e16f10d1e4e128ca4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Oct 2023 15:03:24 +0200 Subject: [PATCH 1235/2451] Improve save logging. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 934e991f..a9dac59d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 934e991f9a39c7d864501532003b9548ef73f896 +Subproject commit a9dac59d36e25064ebd9cd17d519bfac91acc17e From 764ef76e1a9b80d362f8f2237c81422e54cc7170 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 8 Oct 2023 13:08:02 +0000 Subject: [PATCH 1236/2451] [CI] Updating repo.json for 0.8.1.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index af898a90..763f39ae 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.2", - "TestingAssemblyVersion": "0.8.1.2", + "AssemblyVersion": "0.8.1.3", + "TestingAssemblyVersion": "0.8.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 4bdc8f126d107da097c2f88e2badd3e23148cb3b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Oct 2023 00:10:38 +0200 Subject: [PATCH 1237/2451] Make ActorData great again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 54b56ada..8a343f78 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 54b56ada57529221d3fc812193ffe65c424c1521 +Subproject commit 8a343f78abc4ecb7485489cd13a313980227b47e From f5822cf2c8e5ebdfa62f0b4a8785f9d52498f379 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 9 Oct 2023 22:18:33 +0000 Subject: [PATCH 1238/2451] [CI] Updating repo.json for 0.8.1.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 763f39ae..14ee0e6c 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.3", - "TestingAssemblyVersion": "0.8.1.3", + "AssemblyVersion": "0.8.1.4", + "TestingAssemblyVersion": "0.8.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c24a40fd9f67f0fceda9e64202ed0f0218c30166 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 10 Oct 2023 18:01:55 +0200 Subject: [PATCH 1239/2451] Add support for middle-clicking mods. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 35 ++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index a9dac59d..791a9c98 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a9dac59d36e25064ebd9cd17d519bfac91acc17e +Subproject commit 791a9c98aa5a533f754f4e1085d1ae6f890717ac diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index cc4ceb55..bff021bc 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal.Notifications; @@ -25,7 +24,7 @@ namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector { private readonly CommunicatorService _communicator; - private readonly MessageService _messager; + private readonly MessageService _messager; private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; @@ -37,7 +36,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 38.5f * ImGui.GetTextLineHeightWithSpacing()), () => { ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); ImGui.TextUnformatted("Mod Management"); @@ -363,6 +381,11 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Tue, 10 Oct 2023 21:48:01 +0200 Subject: [PATCH 1240/2451] Disable window sounds in ImportPopup --- Penumbra/UI/ImportPopup.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 228cf4e1..71164d1d 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -29,10 +29,11 @@ public sealed class ImportPopup : Window | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar, true) { - _modImportManager = modImportManager; - IsOpen = true; - RespectCloseHotkey = false; - Collapsed = false; + _modImportManager = modImportManager; + DisableWindowSounds = true; + IsOpen = true; + RespectCloseHotkey = false; + Collapsed = false; SizeConstraints = new WindowSizeConstraints { MinimumSize = Vector2.Zero, From 5d2fc72883f0c0c8b6db889106a01774ee895766 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Oct 2023 15:06:31 +0200 Subject: [PATCH 1241/2451] Update submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 791a9c98..a4f9b285 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 791a9c98aa5a533f754f4e1085d1ae6f890717ac +Subproject commit a4f9b285c82f84ff0841695c0787dbba93afc59b diff --git a/Penumbra.GameData b/Penumbra.GameData index 8a343f78..6a0daf2f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 8a343f78abc4ecb7485489cd13a313980227b47e +Subproject commit 6a0daf2f309c27c5a260825bce31094987fce3d1 From 5e79a137083253c1cf183f783a494de3a57653b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Oct 2023 15:10:41 +0200 Subject: [PATCH 1242/2451] Expand tooltip for Wait for Plugins on Startup. --- Penumbra/UI/Tabs/SettingsTab.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 32bd6b8f..ae3a939c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -830,7 +830,8 @@ public class SettingsTab : ITab } else { - Checkbox("Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", + Checkbox("Wait for Plugins on Startup", + "Some mods need to change files that are loaded once when the game starts and never afterwards.\nThis can cause issues with Penumbra loading after the files are already loaded.\nThis setting causes the game to wait until certain plugins have finished loading, making those mods work (in the base collection).\n\nThis changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, v => _dalamud.SetDalamudConfig(DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup")); } From 23f46438a202b0d16427cdcb34c89c4fd20a72ea Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 16 Oct 2023 13:13:05 +0000 Subject: [PATCH 1243/2451] [CI] Updating repo.json for 0.8.1.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 14ee0e6c..2a299ae3 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.4", - "TestingAssemblyVersion": "0.8.1.4", + "AssemblyVersion": "0.8.1.5", + "TestingAssemblyVersion": "0.8.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f2ef0e15d3fdce68c6f2dec0418fb45ad6ad06ed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Oct 2023 19:42:46 +0200 Subject: [PATCH 1244/2451] Draw associated BNPCs in debug tab. --- Penumbra.GameData | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 208 ++++++++++++++++---- 2 files changed, 168 insertions(+), 42 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 6a0daf2f..4a639dbe 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6a0daf2f309c27c5a260825bce31094987fce3d1 +Subproject commit 4a639dbeebc3cbbaf8518b7626892855689f7440 diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 7fd0ae77..926ba77b 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -1,6 +1,10 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; +using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -20,60 +24,182 @@ public class ModPanelConflictsTab : ITab _selector = selector; } + private int? _currentPriority = null; + public ReadOnlySpan Label => "Conflicts"u8; public bool IsVisible => _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0; + private readonly ConditionalWeakTable _expandedMods = new(); + + private int GetPriority(ModConflicts conflicts) + { + if (conflicts.Mod2.Index < 0) + return conflicts.Mod2.Priority; + + return _collectionManager.Active.Current[conflicts.Mod2.Index].Settings?.Priority ?? 0; + } + public void DrawContent() { - using var box = ImRaii.ListBox("##conflicts", -Vector2.One); - if (!box) + using var table = ImRaii.Table("conflicts", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table) return; + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; + var priorityRowWidth = ImGui.CalcTextSize("Priority").X + 20 * ImGuiHelpers.GlobalScale + 2 * buttonSize.X; + var priorityWidth = priorityRowWidth - 2 * (buttonSize.X + spacing.X); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + ImGui.TableSetupColumn("Conflicting Mod", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, priorityRowWidth); + ImGui.TableSetupColumn("Files", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Files").X + spacing.X); + + ImGui.TableSetupScrollFreeze(2, 2); + ImGui.TableHeadersRow(); + DrawCurrentRow(priorityWidth); + // Can not be null because otherwise the tab bar is never drawn. var mod = _selector.Selected!; - foreach (var conflict in _collectionManager.Active.Current.Conflicts(mod)) + foreach (var (conflict, index) in _collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { - if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) - _selector.SelectByValue(otherMod); - var hovered = ImGui.IsItemHovered(); - var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); - - ImGui.SameLine(); - using (var color = ImRaii.PushColor(ImGuiCol.Text, - conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value())) - { - var priority = conflict.Mod2.Index < 0 - ? conflict.Mod2.Priority - : _collectionManager.Active.Current[conflict.Mod2.Index].Settings!.Priority; - ImGui.TextUnformatted($"(Priority {priority})"); - hovered |= ImGui.IsItemHovered(); - rightClicked |= ImGui.IsItemClicked(ImGuiMouseButton.Right); - } - - if (conflict.Mod2 is Mod otherMod2) - { - if (hovered) - ImGui.SetTooltip("Click to jump to mod, Control + Right-Click to disable mod."); - if (rightClicked && ImGui.GetIO().KeyCtrl) - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, otherMod2, false); - } - - using var indent = ImRaii.PushIndent(30f); - foreach (var data in conflict.Conflicts) - { - unsafe - { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), - _ => false, - }; - } - } + using var id = ImRaii.PushId(index); + DrawConflictRow(conflict, priorityWidth, buttonSize); } } + + private void DrawCurrentRow(float priorityWidth) + { + ImGui.TableNextColumn(); + using var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderLine.Value()); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_selector.Selected!.Name); + ImGui.TableNextColumn(); + var priority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + ImGui.SetNextItemWidth(priorityWidth); + if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + _currentPriority = priority; + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) + { + if (_currentPriority != _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)_selector.Selected!, + _currentPriority.Value); + + _currentPriority = null; + } + else if (ImGui.IsItemDeactivated()) + { + _currentPriority = null; + } + + ImGui.TableNextColumn(); + } + + private void DrawConflictSelectable(ModConflicts conflict) + { + ImGui.AlignTextToFramePadding(); + if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) + _selector.SelectByValue(otherMod); + var hovered = ImGui.IsItemHovered(); + var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); + if (conflict.Mod2 is Mod otherMod2) + { + if (hovered) + ImGui.SetTooltip("Click to jump to mod, Control + Right-Click to disable mod."); + if (rightClicked && ImGui.GetIO().KeyCtrl) + _collectionManager.Editor.SetModState(_collectionManager.Active.Current, otherMod2, false); + } + } + + private bool DrawExpandedFiles(ModConflicts conflict) + { + if (!_expandedMods.TryGetValue(conflict.Mod2, out _)) + return false; + + using var indent = ImRaii.PushIndent(30f); + foreach (var data in conflict.Conflicts) + { + unsafe + { + var _ = data switch + { + Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, + MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), + _ => false, + }; + } + } + + return true; + } + + private void DrawConflictRow(ModConflicts conflict, float priorityWidth, Vector2 buttonSize) + { + ImGui.TableNextColumn(); + DrawConflictSelectable(conflict); + var expanded = DrawExpandedFiles(conflict); + ImGui.TableNextColumn(); + var conflictPriority = DrawPriorityInput(conflict, priorityWidth); + ImGui.SameLine(); + var selectedPriority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + DrawPriorityButtons(conflict.Mod2 as Mod, conflictPriority, selectedPriority, buttonSize); + ImGui.TableNextColumn(); + DrawExpandButton(conflict.Mod2, expanded, buttonSize); + } + + private void DrawExpandButton(IMod mod, bool expanded, Vector2 buttonSize) + { + var (icon, tt) = expanded + ? (FontAwesomeIcon.CaretUp.ToIconString(), "Hide the conflicting files for this mod.") + : (FontAwesomeIcon.CaretDown.ToIconString(), "Show the conflicting files for this mod."); + if (ImGuiUtil.DrawDisabledButton(icon, buttonSize, tt, false, true)) + { + if (expanded) + _expandedMods.Remove(mod); + else + _expandedMods.Add(mod, new object()); + } + } + + private int DrawPriorityInput(ModConflicts conflict, float priorityWidth) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, + conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value()); + using var disabled = ImRaii.Disabled(conflict.Mod2.Index < 0); + var priority = _currentPriority ?? GetPriority(conflict); + + ImGui.SetNextItemWidth(priorityWidth); + if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + _currentPriority = priority; + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) + { + if (_currentPriority != GetPriority(conflict)) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)conflict.Mod2, _currentPriority.Value); + + _currentPriority = null; + } + else if (ImGui.IsItemDeactivated()) + { + _currentPriority = null; + } + + return priority; + } + + private void DrawPriorityButtons(Mod? conflict, int conflictPriority, int selectedPriority, Vector2 buttonSize) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericUpAlt.ToIconString(), buttonSize, + $"Set the priority of the currently selected mod to this mods priority plus one. ({selectedPriority} -> {conflictPriority + 1})", selectedPriority > conflictPriority, true)) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, conflictPriority + 1); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericDownAlt.ToIconString(), buttonSize, + $"Set the priority of this mod to the currently selected mods priority minus one. ({conflictPriority} -> {selectedPriority - 1})", + selectedPriority > conflictPriority || conflict == null, true)) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, conflict!, selectedPriority - 1); + } } From f910dcf1e0c90b6eb330166416e1fcb3e353ea85 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Oct 2023 15:36:47 +0200 Subject: [PATCH 1245/2451] Add ReverseResolvePlayerPathsAsync. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 110 +++++++++++------- Penumbra/Api/PenumbraApi.cs | 24 ++++ Penumbra/Api/PenumbraIpcProviders.cs | 34 +++--- Penumbra/Collections/Cache/CollectionCache.cs | 6 +- .../Collections/ModCollection.Cache.Access.cs | 2 +- 6 files changed, 115 insertions(+), 63 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 839cc8f2..eb9e6d65 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 839cc8f270abb6a938d71596bef05b4a9b3ab0ea +Subproject commit eb9e6d65d51db9c8ed11d74332f8390d5d813727 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index dd800f0a..675a61a3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,6 +16,7 @@ using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; +using ImGuiScene; using Penumbra.GameData.Structs; namespace Penumbra.Api; @@ -566,10 +567,11 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private string _currentResolvePath = string.Empty; - private string _currentResolveCharacter = string.Empty; - private string _currentReversePath = string.Empty; - private int _currentReverseIdx = 0; + private string _currentResolvePath = string.Empty; + private string _currentResolveCharacter = string.Empty; + private string _currentReversePath = string.Empty; + private int _currentReverseIdx = 0; + private Task<(string[], string[][])> _task = Task.FromException<(string[], string[][])>(new Exception()); public Resolve(DalamudPluginInterface pi) => _pi = pi; @@ -645,37 +647,55 @@ public class IpcTester : IDisposable } } - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); - if (_currentResolvePath.Length > 0 || _currentReversePath.Length > 0) - { - var forwardArray = _currentResolvePath.Length > 0 - ? new[] - { - _currentResolvePath, - } - : Array.Empty(); - var reverseArray = _currentReversePath.Length > 0 - ? new[] - { - _currentReversePath, - } - : Array.Empty(); - var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray); - var text = string.Empty; - if (ret.Item1.Length > 0) + var forwardArray = _currentResolvePath.Length > 0 + ? new[] { - if (ret.Item2.Length > 0) - text = $"Forward: {ret.Item1[0]} | Reverse: {string.Join("; ", ret.Item2[0])}."; - else - text = $"Forward: {ret.Item1[0]}."; + _currentResolvePath, } - else if (ret.Item2.Length > 0) + : Array.Empty(); + var reverseArray = _currentReversePath.Length > 0 + ? new[] { - text = $"Reverse: {string.Join("; ", ret.Item2[0])}."; + _currentReversePath, + } + : Array.Empty(); + + string ConvertText((string[], string[][]) data) + { + var text = string.Empty; + if (data.Item1.Length > 0) + { + if (data.Item2.Length > 0) + text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}."; + else + text = $"Forward: {data.Item1[0]}."; + } + else if (data.Item2.Length > 0) + { + text = $"Reverse: {string.Join("; ", data.Item2[0])}."; } - ImGui.TextUnformatted(text); + return text; } + + ; + + DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (forwardArray.Length > 0 || reverseArray.Length > 0) + { + var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray); + ImGui.TextUnformatted(ConvertText(ret)); + } + + DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); + if (ImGui.Button("Start")) + _task = Ipc.ResolvePlayerPathsAsync.Subscriber(_pi).Invoke(forwardArray, reverseArray); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_task.Status.ToString()); + if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) + ImGui.SetTooltip(ConvertText(_task.Result)); } } @@ -1442,12 +1462,12 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); if (ImGui.Button("Get##GameObjectResourcePaths")) { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(_pi); + var gameObjects = GetSelectedGameObjects(); + var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(_pi); _stopwatch.Restart(); var resourcePaths = subscriber.Invoke(gameObjects); - _lastCallDuration = _stopwatch.Elapsed; + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcePaths = gameObjects .Select(i => GameObjectToString(i)) .Zip(resourcePaths) @@ -1459,11 +1479,11 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); if (ImGui.Button("Get##PlayerResourcePaths")) { - var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(_pi); + var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(_pi); _stopwatch.Restart(); var resourcePaths = subscriber.Invoke(); - _lastCallDuration = _stopwatch.Elapsed; + _lastCallDuration = _stopwatch.Elapsed; _lastPlayerResourcePaths = resourcePaths .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); @@ -1474,12 +1494,12 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); if (ImGui.Button("Get##GameObjectResourcesOfType")) { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi); + var gameObjects = GetSelectedGameObjects(); + var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi); _stopwatch.Restart(); var resourcesOfType = subscriber.Invoke(_type, _withUIData, gameObjects); - _lastCallDuration = _stopwatch.Elapsed; + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcesOfType = gameObjects .Select(i => GameObjectToString(i)) .Zip(resourcesOfType) @@ -1491,11 +1511,11 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); if (ImGui.Button("Get##PlayerResourcesOfType")) { - var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(_pi); + var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(_pi); _stopwatch.Restart(); var resourcesOfType = subscriber.Invoke(_type, _withUIData); - _lastCallDuration = _stopwatch.Elapsed; + _lastCallDuration = _stopwatch.Elapsed; _lastPlayerResourcesOfType = resourcesOfType .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); @@ -1504,10 +1524,10 @@ public class IpcTester : IDisposable } DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); } private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class @@ -1573,7 +1593,7 @@ public class IpcTester : IDisposable return; ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); - ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); ImGui.TableHeadersRow(); foreach (var (actualPath, gamePaths) in paths) @@ -1596,7 +1616,7 @@ public class IpcTester : IDisposable return; ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUIData ? 0.55f : 0.85f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUIData ? 0.55f : 0.85f); if (_withUIData) ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableHeadersRow(); @@ -1626,8 +1646,8 @@ public class IpcTester : IDisposable private ushort[] GetSelectedGameObjects() => _gameObjectIndices.Split(',') - .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) - .ToArray(); + .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) + .ToArray(); private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) { diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 10b5b6bd..0ae4fcca 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -388,6 +388,30 @@ public class PenumbraApi : IDisposable, IPenumbraApi return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } + public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) + { + CheckInitialized(); + if (!_config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); + + return await Task.Run(async () => + { + var playerCollection = await _dalamud.Framework.RunOnFrameworkThread(_collectionResolver.PlayerCollection).ConfigureAwait(false); + var forwardTask = Task.Run(() => + { + var forwardRet = new string[forward.Length]; + Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], _modManager, playerCollection)); + return forwardRet; + }).ConfigureAwait(false); + var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false); + var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); + return (await forwardTask, reverseResolved); + }); + } + public T? GetFile(string gamePath) where T : FileResource => GetFileIntern(ResolveDefaultPath(gamePath)); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 87de6a0c..b72073fb 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -53,15 +53,16 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider GameObjectResourcePathResolved; // Resolve - internal readonly FuncProvider ResolveDefaultPath; - internal readonly FuncProvider ResolveInterfacePath; - internal readonly FuncProvider ResolvePlayerPath; - internal readonly FuncProvider ResolveGameObjectPath; - internal readonly FuncProvider ResolveCharacterPath; - internal readonly FuncProvider ReverseResolvePath; - internal readonly FuncProvider ReverseResolveGameObjectPath; - internal readonly FuncProvider ReverseResolvePlayerPath; - internal readonly FuncProvider ResolvePlayerPaths; + internal readonly FuncProvider ResolveDefaultPath; + internal readonly FuncProvider ResolveInterfacePath; + internal readonly FuncProvider ResolvePlayerPath; + internal readonly FuncProvider ResolveGameObjectPath; + internal readonly FuncProvider ResolveCharacterPath; + internal readonly FuncProvider ReverseResolvePath; + internal readonly FuncProvider ReverseResolveGameObjectPath; + internal readonly FuncProvider ReverseResolvePlayerPath; + internal readonly FuncProvider ResolvePlayerPaths; + internal readonly FuncProvider> ResolvePlayerPathsAsync; // Collections internal readonly FuncProvider> GetCollections; @@ -119,10 +120,15 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider RemoveTemporaryMod; // Resource Tree - internal readonly FuncProvider?[]> GetGameObjectResourcePaths; - internal readonly FuncProvider>> GetPlayerResourcePaths; - internal readonly FuncProvider?[]> GetGameObjectResourcesOfType; - internal readonly FuncProvider>> GetPlayerResourcesOfType; + internal readonly FuncProvider?[]> GetGameObjectResourcePaths; + internal readonly FuncProvider>> GetPlayerResourcePaths; + + internal readonly FuncProvider?[]> + GetGameObjectResourcesOfType; + + internal readonly + FuncProvider>> + GetPlayerResourcesOfType; public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) @@ -184,6 +190,7 @@ public class PenumbraIpcProviders : IDisposable ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath); ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath); ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths); + ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync); // Collections GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections); @@ -301,6 +308,7 @@ public class PenumbraIpcProviders : IDisposable ReverseResolveGameObjectPath.Dispose(); ReverseResolvePlayerPath.Dispose(); ResolvePlayerPaths.Dispose(); + ResolvePlayerPathsAsync.Dispose(); // Collections GetCollections.Dispose(); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 80539d96..3761424a 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -23,7 +23,7 @@ public class CollectionCache : IDisposable private readonly ModCollection _collection; public readonly CollectionModData ModData = new(); public readonly SortedList, object?)> _changedItems = new(); - public readonly Dictionary ResolvedFiles = new(); + public readonly ConcurrentDictionary ResolvedFiles = new(); public readonly MetaCache Meta; public readonly Dictionary> _conflicts = new(); @@ -146,7 +146,7 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); if (fullPath.FullName.Length > 0) { - ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } @@ -157,7 +157,7 @@ public class CollectionCache : IDisposable } else if (fullPath.FullName.Length > 0) { - ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 36e0fd98..a695c463 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -56,7 +56,7 @@ public partial class ModCollection } internal IReadOnlyDictionary ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary(); + => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); internal IReadOnlyDictionary, object?)> ChangedItems => _cache?.ChangedItems ?? new Dictionary, object?)>(); From 25e9a99799b956ee2be6bd2c886c0167ab3262a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Oct 2023 15:37:34 +0200 Subject: [PATCH 1246/2451] Fix portraits not respecting card settings. --- .../Collections/Manager/IndividualCollections.Access.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 680f8b32..78eff98c 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -90,13 +90,13 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return (identifier, SpecialResult.Invalid); if (_actorService.AwaitedService.ResolvePartyBannerPlayer(identifier.Special, out var id)) - return (id, SpecialResult.PartyBanner); + return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.PartyBanner) : (identifier, SpecialResult.Invalid); if (_actorService.AwaitedService.ResolvePvPBannerPlayer(identifier.Special, out id)) - return (id, SpecialResult.PvPBanner); + return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.PvPBanner) : (identifier, SpecialResult.Invalid); if (_actorService.AwaitedService.ResolveMahjongPlayer(identifier.Special, out id)) - return (id, SpecialResult.Mahjong); + return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.Mahjong) : (identifier, SpecialResult.Invalid); switch (identifier.Special) { From 2cb92d817adc2e55143c401cecce1fc406bf2ce0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Oct 2023 15:37:52 +0200 Subject: [PATCH 1247/2451] Fix directory rename not updating paths in advanced window. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0f171e21..7171a0e2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -598,7 +598,7 @@ public partial class ModEditWindow : Window, IDisposable private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { - if (type is ModPathChangeType.Reloaded) + if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved) ChangeMod(mod); } } From 8fd755c5e6da50610581a7f25310929f4b2c3afe Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 22 Oct 2023 13:40:09 +0000 Subject: [PATCH 1248/2451] [CI] Updating repo.json for 0.8.1.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2a299ae3..d264be81 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.5", - "TestingAssemblyVersion": "0.8.1.5", + "AssemblyVersion": "0.8.1.6", + "TestingAssemblyVersion": "0.8.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c76a9ace24cb5b71f51ca3e7b6809e2c0e688af3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Oct 2023 15:40:30 +0200 Subject: [PATCH 1249/2451] Update API Nuget. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index eb9e6d65..f9069dfd 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit eb9e6d65d51db9c8ed11d74332f8390d5d813727 +Subproject commit f9069dfdf1f0a7011c3b0ea7c0be5330c42959dd From 06e06b81e9e095d91ba7f05a3909fa62bf7e7731 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 28 Oct 2023 01:13:18 +0200 Subject: [PATCH 1250/2451] Support correct handling of offhands in changed items. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Communication/ChangedItemClick.cs | 3 ++- Penumbra/UI/ChangedItemDrawer.cs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index f9069dfd..80f9793e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f9069dfdf1f0a7011c3b0ea7c0be5330c42959dd +Subproject commit 80f9793ef2ddaa50246b7112fde4d9b2098d8823 diff --git a/Penumbra.GameData b/Penumbra.GameData index 4a639dbe..e1a62d8e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4a639dbeebc3cbbaf8518b7626892855689f7440 +Subproject commit e1a62d8e6b4e1d8c482253ad14850fd3dc372d86 diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 1e5bc863..ea389bb6 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; namespace Penumbra.Communication; @@ -7,7 +8,7 @@ namespace Penumbra.Communication; /// Triggered when a Changed Item in Penumbra is clicked. /// /// Parameter is the clicked mouse button. -/// Parameter is the clicked object data if any.. +/// Parameter is the clicked object data if any. /// /// public sealed class ChangedItemClick : EventWrapper, ChangedItemClick.Priority> diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 324c354a..3c74aa20 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -285,7 +285,7 @@ public class ChangedItemDrawer : IDisposable private object? Convert(object? data) { if (data is EquipItem it) - return _items.GetRow(it.ItemId.Id); + return (_items.GetRow(it.ItemId.Id), it.Type); return data; } From 8e63452e84cd5b6decb57368fe49e434ca5fc1fa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Oct 2023 11:32:40 +0100 Subject: [PATCH 1251/2451] Use some CS sigs. --- Penumbra.GameData | 2 +- Penumbra/Interop/Services/GameEventManager.cs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e1a62d8e..04ddadb4 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e1a62d8e6b4e1d8c482253ad14850fd3dc372d86 +Subproject commit 04ddadb44600a382e26661e1db08fd16c3b671d8 diff --git a/Penumbra/Interop/Services/GameEventManager.cs b/Penumbra/Interop/Services/GameEventManager.cs index d11b7159..30319060 100644 --- a/Penumbra/Interop/Services/GameEventManager.cs +++ b/Penumbra/Interop/Services/GameEventManager.cs @@ -4,6 +4,7 @@ using Dalamud.Utility.Signatures; using Penumbra.GameData; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; @@ -27,7 +28,13 @@ public unsafe class GameEventManager : IDisposable _copyCharacterHook = interop.HookFromAddress((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour); - + _characterBaseCreateHook = + interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour); + _characterBaseDestructorHook = + interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Destroy, + CharacterBaseDestructorDetour); + _weaponReloadHook = + interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, WeaponReloadDetour); _characterDtorHook.Enable(); _copyCharacterHook.Enable(); _resourceHandleDestructorHook.Enable(); @@ -148,8 +155,7 @@ public unsafe class GameEventManager : IDisposable private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d); - [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))] - private readonly Hook _characterBaseCreateHook = null!; + private readonly Hook _characterBaseCreateHook; private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d) { @@ -194,8 +200,7 @@ public unsafe class GameEventManager : IDisposable public delegate void CharacterBaseDestructorEvent(nint drawBase); - [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))] - private readonly Hook _characterBaseDestructorHook = null!; + private readonly Hook _characterBaseDestructorHook; private void CharacterBaseDestructorDetour(IntPtr drawBase) { @@ -222,8 +227,7 @@ public unsafe class GameEventManager : IDisposable private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7); - [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))] - private readonly Hook _weaponReloadHook = null!; + private readonly Hook _weaponReloadHook; private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7) { From 6375faa7586e6adccbb289bfcc599501cd2b8be3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 31 Oct 2023 11:23:59 +0000 Subject: [PATCH 1252/2451] [CI] Updating repo.json for 0.8.1.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index d264be81..bc8d02d7 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.6", - "TestingAssemblyVersion": "0.8.1.6", + "AssemblyVersion": "0.8.1.7", + "TestingAssemblyVersion": "0.8.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 00dc5f48b1c32da73f4d8d10c60e9a9c698ff7d0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 01:52:16 +0200 Subject: [PATCH 1253/2451] ClientStructs-ify a few things --- OtterGui | 2 +- .../LiveColorTablePreviewer.cs | 8 +-- .../MaterialPreview/LiveMaterialPreviewer.cs | 4 +- .../Interop/MaterialPreview/MaterialInfo.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 47 +++++++------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 21 ++++--- .../Interop/SafeHandles/SafeTextureHandle.cs | 5 +- Penumbra/Interop/Services/SkinFixer.cs | 13 ++-- Penumbra/Interop/Structs/CharacterBaseExt.cs | 14 ----- Penumbra/Interop/Structs/ConstantBuffer.cs | 28 --------- Penumbra/Interop/Structs/HumanExt.cs | 19 ------ Penumbra/Interop/Structs/Material.cs | 47 -------------- Penumbra/Interop/Structs/MtrlResource.cs | 45 ------------- Penumbra/Interop/Structs/RenderModel.cs | 3 + Penumbra/Interop/Structs/ResourceHandle.cs | 63 +++---------------- .../Interop/Structs/ShaderPackageUtility.cs | 17 ----- Penumbra/Interop/Structs/StructExtensions.cs | 24 +++++++ Penumbra/Interop/Structs/TextureUtility.cs | 31 --------- Penumbra/Penumbra.cs | 1 - Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/DebugTab.cs | 4 +- Penumbra/UI/Tabs/ResourceTab.cs | 4 +- Penumbra/UI/UiHelpers.cs | 11 +++- 23 files changed, 102 insertions(+), 314 deletions(-) delete mode 100644 Penumbra/Interop/Structs/CharacterBaseExt.cs delete mode 100644 Penumbra/Interop/Structs/ConstantBuffer.cs delete mode 100644 Penumbra/Interop/Structs/HumanExt.cs delete mode 100644 Penumbra/Interop/Structs/Material.cs delete mode 100644 Penumbra/Interop/Structs/MtrlResource.cs delete mode 100644 Penumbra/Interop/Structs/ShaderPackageUtility.cs create mode 100644 Penumbra/Interop/Structs/StructExtensions.cs delete mode 100644 Penumbra/Interop/Structs/TextureUtility.cs diff --git a/OtterGui b/OtterGui index a4f9b285..6f17ef70 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a4f9b285c82f84ff0841695c0787dbba93afc59b +Subproject commit 6f17ef70c41f3b31a401fdc9d6e37087e64f2035 diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index bacc72fa..0b7bafe0 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -31,7 +31,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures; + var colorSetTextures = DrawObject->ColorTableTextures; if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color table textures"); @@ -79,7 +79,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase textureSize[1] = TextureHeight; using var texture = - new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false); + new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); if (texture.IsInvalid) return; @@ -88,7 +88,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { fixed (Half* colorTable = _colorTable) { - success = Structs.TextureUtility.InitializeContents(texture.Texture, colorTable); + success = texture.Texture->InitializeContents(colorTable); } } @@ -101,7 +101,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (!base.IsStillValid()) return false; - var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures; + var colorSetTextures = DrawObject->ColorTableTextures; if (colorSetTextures == null) return false; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index fa03ac49..972d81be 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -18,7 +18,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + var shpkHandle = mtrlHandle->ShaderPackageResourceHandle; if (shpkHandle == null) throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); @@ -61,7 +61,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (!CheckValidity()) return; - ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; + Material->ShaderFlags = shPkFlags; } public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index c64e4d0b..ec0ddd29 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -93,7 +93,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + var path = ResolveContext.GetResourceHandlePath(&mtrlHandle->ResourceHandle); if (path == needle) result.Add(new MaterialInfo(index, type, i, j)); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 55893cab..5e3970cc 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,14 +1,15 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; +using static Penumbra.Interop.Structs.StructExtensions; namespace Penumbra.Interop.ResourceTree; @@ -36,7 +37,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal); + return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path, @internal); } private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) @@ -68,7 +69,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); + return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path, @internal); } private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, @@ -118,22 +119,22 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, false); if (node != null) Nodes.Add((nint)tex, node); return node; } - public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) + public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) { - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + if (mdl == null || mdl->ModelResourceHandle == null || mdl->ModelResourceHandle->ResourceHandle.Type.Category != ResourceHandleType.HandleCategory.Chara) return null; - if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) + if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, false); if (node == null) return null; @@ -149,7 +150,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree } } - Nodes.Add((nint)mdl->ResourceHandle, node); + Nodes.Add((nint)mdl->ModelResourceHandle, node); return node; } @@ -169,27 +170,27 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree } static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) - => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) + => mtrl->TexturesSpan.FindFirst(p => p.Texture == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) ? p.Id : null; static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - => new ReadOnlySpan(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s) - ? s.Crc + => shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s) + ? s.CRC : null; if (mtrl == null) return null; - var resource = mtrl->ResourceHandle; + var resource = mtrl->MaterialResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, false); if (node == null) return null; - var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName), false); if (shpkNode != null) { if (WithUiData) @@ -200,10 +201,10 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); - for (var i = 0; i < resource->NumTex; i++) + for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, - resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), false, + resource->Textures[i].IsDX11); if (texNode == null) continue; @@ -212,12 +213,12 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree string? name = null; if (shpk != null) { - var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds); + var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); uint? samplerId; if (index != 0x001F) samplerId = mtrl->Textures[index].Id; else - samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds); + samplerId = GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds); if (samplerId.HasValue) { alreadyProcessedSamplerIds.Add(samplerId.Value); @@ -367,7 +368,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (handle == null) return ByteString.Empty; - var name = handle->FileName(); + var name = handle->FileName.AsByteString(); if (name.IsEmpty) return ByteString.Empty; @@ -388,6 +389,6 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (handle == null) return 0; - return ResourceHandle.GetLength(handle); + return handle->GetLength(); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index bc2cca26..53e7db35 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; @@ -50,11 +50,12 @@ public class ResourceTree { var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; + var human = model->GetModelType() == CharacterBase.ModelType.Human ? (Human*)model : null; var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; - RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace)((Human*)model)->RaceSexId : GenderRace.Unknown; + RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; for (var i = 0; i < model->SlotCount; ++i) { @@ -72,7 +73,7 @@ public class ResourceTree Nodes.Add(imcNode); } - var mdl = (RenderModel*)model->Models[i]; + var mdl = model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { @@ -84,13 +85,13 @@ public class ResourceTree AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); - if (model->GetModelType() == CharacterBase.ModelType.Human) - AddHumanResources(globalContext, (HumanExt*)model); + if (human != null) + AddHumanResources(globalContext, human); } - private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human) + private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject; + var firstSubObject = (CharacterBase*)human->CharacterBase.DrawObject.Object.ChildObject; if (firstSubObject != null) { var subObjectNodes = new List(); @@ -116,7 +117,7 @@ public class ResourceTree subObjectNodes.Add(imcNode); } - var mdl = (RenderModel*)subObject->Models[i]; + var mdl = subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { @@ -137,7 +138,7 @@ public class ResourceTree var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); + var decalNode = context.CreateNodeFromTex(human->Decal); if (decalNode != null) { if (globalContext.WithUiData) @@ -149,7 +150,7 @@ public class ResourceTree Nodes.Add(decalNode); } - var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); + var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); if (legacyDecalNode != null) { if (globalContext.WithUiData) diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs index df97371b..fd020804 100644 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -1,5 +1,4 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Penumbra.Interop.Structs; namespace Penumbra.Interop.SafeHandles; @@ -18,7 +17,7 @@ public unsafe class SafeTextureHandle : SafeHandle throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); if (incRef && handle != null) - TextureUtility.IncRef(handle); + handle->IncRef(); SetHandle((nint)handle); } @@ -43,7 +42,7 @@ public unsafe class SafeTextureHandle : SafeHandle } if (handle != 0) - TextureUtility.DecRef((Texture*)handle); + ((Texture*)handle)->DecRef(); return true; } diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index be5b778e..d25a5638 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData; @@ -73,19 +74,19 @@ public sealed unsafe class SkinFixer : IDisposable public ulong GetAndResetSlowPathCallDelta() => Interlocked.Exchange(ref _slowPathCallDelta, 0); - private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource) + private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource) { if (mtrlResource == null) return false; - var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString); + var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName); return SkinShpkName.SequenceEqual(shpkName); } private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) { - var mtrl = (Structs.MtrlResource*)mtrlResourceHandle; - var shpk = mtrl->ShpkResourceHandle; + var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; + var shpk = mtrl->ShaderPackageResourceHandle; if (shpk == null) return; @@ -109,7 +110,7 @@ public sealed unsafe class SkinFixer : IDisposable return _onRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; - var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle; + var mtrlResource = material->MaterialResourceHandle; if (!IsSkinMaterial(mtrlResource)) return _onRenderMaterialHook.Original(human, param); @@ -124,7 +125,7 @@ public sealed unsafe class SkinFixer : IDisposable { try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle; + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; return _onRenderMaterialHook.Original(human, param); } finally diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs deleted file mode 100644 index 53fda2cd..00000000 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.Interop.Structs; - -[StructLayout(LayoutKind.Explicit)] -public unsafe struct CharacterBaseExt -{ - [FieldOffset(0x0)] - public CharacterBase CharacterBase; - - [FieldOffset(0x258)] - public Texture** ColorTableTextures; -} diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs deleted file mode 100644 index d61aaeea..00000000 --- a/Penumbra/Interop/Structs/ConstantBuffer.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Penumbra.Interop.Structs; - -[StructLayout(LayoutKind.Explicit, Size = 0x70)] -public unsafe struct ConstantBuffer -{ - [FieldOffset(0x20)] - public int Size; - - [FieldOffset(0x24)] - public int Flags; - - [FieldOffset(0x28)] - private void* _maybeSourcePointer; - - public bool TryGetBuffer(out Span buffer) - { - if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null) - { - buffer = new Span(_maybeSourcePointer, Size >> 2); - return true; - } - else - { - buffer = null; - return false; - } - } -} diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs deleted file mode 100644 index 274b4fb2..00000000 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; - -namespace Penumbra.Interop.Structs; - -[StructLayout(LayoutKind.Explicit)] -public unsafe struct HumanExt -{ - [FieldOffset(0x0)] - public Human Human; - - [FieldOffset(0x0)] - public CharacterBaseExt CharacterBase; - - [FieldOffset(0x9E8)] - public ResourceHandle* Decal; - - [FieldOffset(0x9F0)] - public ResourceHandle* LegacyBodyDecal; -} diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs deleted file mode 100644 index f7c8679e..00000000 --- a/Penumbra/Interop/Structs/Material.cs +++ /dev/null @@ -1,47 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; - -namespace Penumbra.Interop.Structs; - -[StructLayout(LayoutKind.Explicit, Size = 0x40)] -public unsafe struct Material -{ - [FieldOffset(0x10)] - public MtrlResource* ResourceHandle; - - [FieldOffset(0x18)] - public uint ShaderPackageFlags; - - [FieldOffset(0x20)] - public uint* ShaderKeys; - - public int ShaderKeyCount - => (int)((uint*)Textures - ShaderKeys); - - [FieldOffset(0x28)] - public ConstantBuffer* MaterialParameter; - - [FieldOffset(0x30)] - public TextureEntry* Textures; - - [FieldOffset(0x38)] - public ushort TextureCount; - - public Texture* Texture(int index) - => Textures[index].ResourceHandle->KernelTexture; - - [StructLayout(LayoutKind.Explicit, Size = 0x18)] - public struct TextureEntry - { - [FieldOffset(0x00)] - public uint Id; - - [FieldOffset(0x08)] - public TextureResourceHandle* ResourceHandle; - - [FieldOffset(0x10)] - public uint SamplerFlags; - } - - public ReadOnlySpan TextureSpan - => new(Textures, TextureCount); -} diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs deleted file mode 100644 index c3b86e14..00000000 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Penumbra.Interop.Structs; - -[StructLayout(LayoutKind.Explicit)] -public unsafe struct MtrlResource -{ - [FieldOffset(0x00)] - public ResourceHandle Handle; - - [FieldOffset(0xC8)] - public ShaderPackageResourceHandle* ShpkResourceHandle; - - [FieldOffset(0xD0)] - public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. - - [FieldOffset(0xE0)] - public byte* StringList; - - [FieldOffset(0xF8)] - public ushort ShpkOffset; - - [FieldOffset(0xFA)] - public byte NumTex; - - public byte* ShpkString - => StringList + ShpkOffset; - - public byte* TexString(int idx) - => StringList + TexSpace[idx].PathOffset; - - public bool TexIsDX11(int idx) - => TexSpace[idx].Flags >= 0x8000; - - [StructLayout(LayoutKind.Explicit, Size = 0x10)] - public struct TextureEntry - { - [FieldOffset(0x00)] - public TextureResourceHandle* ResourceHandle; - - [FieldOffset(0x08)] - public ushort PathOffset; - - [FieldOffset(0x0A)] - public ushort Flags; - } -} diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs index f9cb2d56..86b09e8d 100644 --- a/Penumbra/Interop/Structs/RenderModel.cs +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -5,6 +5,9 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct RenderModel { + [FieldOffset(0)] + public Model Model; + [FieldOffset(0x18)] public RenderModel* PreviousModel; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 3cceb949..382368b4 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,10 +1,8 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.String; using Penumbra.String.Classes; +using CsHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; namespace Penumbra.Interop.Structs; @@ -14,24 +12,8 @@ public unsafe struct TextureResourceHandle [FieldOffset(0x0)] public ResourceHandle Handle; - [FieldOffset(0x38)] - public IntPtr Unk; - - [FieldOffset(0x118)] - public Texture* KernelTexture; - - [FieldOffset(0x20)] - public IntPtr NewKernelTexture; -} - -[StructLayout(LayoutKind.Explicit)] -public unsafe struct ShaderPackageResourceHandle -{ [FieldOffset(0x0)] - public ResourceHandle Handle; - - [FieldOffset(0xB0)] - public ShaderPackage* ShaderPackage; + public CsHandle.TextureResourceHandle CsHandle; } public enum LoadState : byte @@ -59,27 +41,14 @@ public unsafe struct ResourceHandle public ulong DataLength; } - public const int SsoSize = 15; + public readonly ByteString FileName() + => CsHandle.FileName.AsByteString(); - public byte* FileNamePtr() - { - if (FileNameLength > SsoSize) - return FileNameData; + public readonly bool GamePath(out Utf8GamePath path) + => Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), out path); - fixed (byte** name = &FileNameData) - { - return (byte*)name; - } - } - - public ByteString FileName() - => ByteString.FromByteStringUnsafe(FileNamePtr(), FileNameLength, true); - - public ReadOnlySpan FileNameAsSpan() - => new(FileNamePtr(), FileNameLength); - - public bool GamePath(out Utf8GamePath path) - => Utf8GamePath.FromSpan(FileNameAsSpan(), out path); + [FieldOffset(0x00)] + public CsHandle.ResourceHandle CsHandle; [FieldOffset(0x00)] public void** VTable; @@ -90,18 +59,9 @@ public unsafe struct ResourceHandle [FieldOffset(0x0C)] public ResourceType FileType; - [FieldOffset(0x10)] - public uint Id; - [FieldOffset(0x28)] public uint FileSize; - [FieldOffset(0x2C)] - public uint FileSize2; - - [FieldOffset(0x34)] - public uint FileSize3; - [FieldOffset(0x48)] public byte* FileNameData; @@ -114,13 +74,6 @@ public unsafe struct ResourceHandle [FieldOffset(0xAC)] public uint RefCount; - // May return null. - public static byte* GetData(ResourceHandle* handle) - => ((delegate* unmanaged< ResourceHandle*, byte* >)handle->VTable[Offsets.ResourceHandleGetDataVfunc])(handle); - - public static ulong GetLength(ResourceHandle* handle) - => ((delegate* unmanaged< ResourceHandle*, ulong >)handle->VTable[Offsets.ResourceHandleGetLengthVfunc])(handle); - // Only use these if you know what you are doing. // Those are actually only sure to be accessible for DefaultResourceHandles. diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs deleted file mode 100644 index 9f7ec1f5..00000000 --- a/Penumbra/Interop/Structs/ShaderPackageUtility.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Penumbra.Interop.Structs; - -public static class ShaderPackageUtility -{ - [StructLayout(LayoutKind.Explicit, Size = 0xC)] - public unsafe struct Sampler - { - [FieldOffset(0x0)] - public uint Crc; - - [FieldOffset(0x4)] - public uint Id; - - [FieldOffset(0xA)] - public ushort Slot; - } -} diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs new file mode 100644 index 00000000..d1a38ae4 --- /dev/null +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -0,0 +1,24 @@ +using FFXIVClientStructs.STD; +using Penumbra.String; + +namespace Penumbra.Interop.Structs; + +internal static class StructExtensions +{ + // TODO submit this to ClientStructs + public static unsafe ReadOnlySpan AsSpan(in this StdString str) + { + if (str.Length < 16) + { + fixed (StdString* pStr = &str) + { + return new(pStr->Buffer, (int)str.Length); + } + } + else + return new(str.BufferPtr, (int)str.Length); + } + + public static unsafe ByteString AsByteString(in this StdString str) + => ByteString.FromSpanUnsafe(str.AsSpan(), true); +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs deleted file mode 100644 index eeea4c33..00000000 --- a/Penumbra/Interop/Structs/TextureUtility.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; - -namespace Penumbra.Interop.Structs; - -public unsafe class TextureUtility -{ - public TextureUtility(IGameInteropProvider interop) - => interop.InitializeFromAttributes(this); - - - [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] - private static nint _textureCreate2D = nint.Zero; - - [Signature("E9 ?? ?? ?? ?? 8B 02 25")] - private static nint _textureInitializeContents = nint.Zero; - - public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) - => ((delegate* unmanaged)_textureCreate2D)(device, size, mipLevel, textureFormat, - flags, unk); - - public static bool InitializeContents(Texture* texture, void* contents) - => ((delegate* unmanaged)_textureInitializeContents)(texture, contents); - - public static void IncRef(Texture* texture) - => ((delegate* unmanaged)(*(void***)texture)[2])(texture); - - public static void DecRef(Texture* texture) - => ((delegate* unmanaged)(*(void***)texture)[3])(texture); -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 73d1013e..ce1bdae5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -75,7 +75,6 @@ public class Penumbra : IDalamudPlugin _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 84f89f6d..6a522ca2 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -90,8 +90,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 5abb3c2f..1c5f7946 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -669,8 +669,8 @@ public class DebugTab : Window, ITab UiHelpers.Text(resource); ImGui.TableNextColumn(); - var data = (nint)ResourceHandle.GetData(resource); - var length = ResourceHandle.GetLength(resource); + var data = (nint)resource->CsHandle.GetData(); + var length = resource->CsHandle.GetLength(); if (ImGui.Selectable($"0x{data:X}")) if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 020493d1..6f3dec30 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -99,10 +99,10 @@ public class ResourceTab : ITab UiHelpers.Text(resource); if (ImGui.IsItemClicked()) { - var data = Interop.Structs.ResourceHandle.GetData(resource); + var data = resource->CsHandle.GetData(); if (data != null) { - var length = (int)Interop.Structs.ResourceHandle.GetLength(resource); + var length = (int)resource->CsHandle.GetLength(); ImGui.SetClipboardText(string.Join(" ", new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); } diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 6c64bd55..8fbce6d0 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -19,9 +19,18 @@ public static class UiHelpers public static unsafe void Text(byte* s, int length) => ImGuiNative.igTextUnformatted(s, s + length); + /// Draw text given by a byte span. + public static unsafe void Text(ReadOnlySpan s) + { + fixed (byte* pS = s) + { + Text(pS, s.Length); + } + } + /// Draw the name of a resource file. public static unsafe void Text(ResourceHandle* resource) - => Text(resource->FileName().Path, resource->FileNameLength); + => Text(resource->CsHandle.FileName.AsSpan()); /// Draw a ByteString as a selectable. public static unsafe bool Selectable(ByteString s, bool selected) From 5085aa500c167bba4853346a7e4a651129a7d54d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 02:13:57 +0200 Subject: [PATCH 1254/2451] ResourceTree: Use DrawObject as equipment source + CS-ify a bit more --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 53e7db35..836e79e2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -50,9 +50,14 @@ public class ResourceTree { var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; - var human = model->GetModelType() == CharacterBase.ModelType.Human ? (Human*)model : null; - var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); - // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); + var modelType = model->GetModelType(); + var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; + var equipment = modelType switch + { + CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(&character->DrawData.Head, 10), + _ => ReadOnlySpan.Empty, + }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; @@ -91,50 +96,51 @@ public class ResourceTree private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var firstSubObject = (CharacterBase*)human->CharacterBase.DrawObject.Object.ChildObject; - if (firstSubObject != null) + var subObjectIndex = 0; + var weaponIndex = 0; + var subObjectNodes = new List(); + foreach (var baseSubObject in human->CharacterBase.DrawObject.Object.ChildObjects) { - var subObjectNodes = new List(); - var subObject = firstSubObject; - var subObjectIndex = 0; - do + if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) + continue; + var subObject = (CharacterBase*)baseSubObject; + + var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; + var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. + var subObjectContext = globalContext.CreateContext( + weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown, + weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default + ); + + for (var i = 0; i < subObject->SlotCount; ++i) { - var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; - var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; - var subObjectContext = globalContext.CreateContext( - weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown, - weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default - ); - - for (var i = 0; i < subObject->SlotCount; ++i) + var imc = (ResourceHandle*)subObject->IMCArray[i]; + var imcNode = subObjectContext.CreateNodeFromImc(imc); + if (imcNode != null) { - var imc = (ResourceHandle*)subObject->IMCArray[i]; - var imcNode = subObjectContext.CreateNodeFromImc(imc); - if (imcNode != null) - { - if (globalContext.WithUiData) - imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; - subObjectNodes.Add(imcNode); - } - - var mdl = subObject->Models[i]; - var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); - if (mdlNode != null) - { - if (globalContext.WithUiData) - mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; - subObjectNodes.Add(mdlNode); - } + if (globalContext.WithUiData) + imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; + subObjectNodes.Add(imcNode); } - AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + var mdl = subObject->Models[i]; + var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + if (mdlNode != null) + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; + subObjectNodes.Add(mdlNode); + } + } - subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; - ++subObjectIndex; - } while (subObject != null && subObject != firstSubObject); + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); - Nodes.InsertRange(0, subObjectNodes); + ++subObjectIndex; + if (weapon != null) + ++weaponIndex; } + Nodes.InsertRange(0, subObjectNodes); var context = globalContext.CreateContext(EquipSlot.Unknown, default); From db9bfb00a32cbac3fbae13460e7ba1f86559f39a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 02:17:41 +0200 Subject: [PATCH 1255/2451] ResourceTree: Show SKP files out of Debug --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 5e3970cc..ade3ccd7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -268,7 +268,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, false); if (node != null) { if (WithUiData) From 3da20f2d8971553050715bd94a046d0c1a839701 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:08:02 +0100 Subject: [PATCH 1256/2451] ResourceTree: Rework Internal flag, improve null checks, simplify --- .../Interop/ResourceTree/ResolveContext.cs | 91 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 8 +- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ade3ccd7..8e286ad0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -27,8 +27,11 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) { + if (resourceHandle == null) + return null; + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; @@ -37,11 +40,14 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path, @internal); + return CreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } - private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) + private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) { + if (resourceHandle == null) + return null; + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; @@ -65,82 +71,73 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree gamePath = tmp.Path.Clone(); } } + else + { + // Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues. + if (!gamePath.IsOwned) + gamePath = gamePath.Clone(); + } if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path, @internal); + return CreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); } - private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, - Utf8GamePath gamePath, bool @internal) + private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + Utf8GamePath gamePath, bool autoAdd = true) { + if (resourceHandle == null) + throw new ArgumentNullException(nameof(resourceHandle)); + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; - var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this) + var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this) { GamePath = gamePath, FullPath = fullPath, }; - if (resourceHandle != null) + if (autoAdd) Nodes.Add((nint)resourceHandle, node); return node; } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal) - { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; - if (fullPath.InternalName.IsEmpty) - return null; - - return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this) - { - FullPath = fullPath, - }; - } - public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { + if (imc == null) + return null; + if (Nodes.TryGetValue((nint)imc, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true); - if (node == null) - return null; - - Nodes.Add((nint)imc, node); - - return node; + return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) { + if (tex == null) + return null; + if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, false); - if (node != null) - Nodes.Add((nint)tex, node); - - return node; + return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); } public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) { - if (mdl == null || mdl->ModelResourceHandle == null || mdl->ModelResourceHandle->ResourceHandle.Type.Category != ResourceHandleType.HandleCategory.Chara) + if (mdl == null || mdl->ModelResourceHandle == null) return null; if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, false); - if (node == null) - return null; + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); for (var i = 0; i < mdl->MaterialCount; i++) { - var mtrl = (Material*)mdl->Materials[i]; + var mtrl = mdl->Materials[i]; var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) { @@ -179,18 +176,18 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree ? s.CRC : null; - if (mtrl == null) + if (mtrl == null || mtrl->MaterialResourceHandle == null) return null; var resource = mtrl->MaterialResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); if (node == null) return null; - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName), false); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); if (shpkNode != null) { if (WithUiData) @@ -203,7 +200,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), false, + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), resource->Textures[i].IsDX11); if (texNode == null) continue; @@ -240,15 +237,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return node; } - public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb) { - if (sklb->SkeletonResourceHandle == null) + if (sklb == null || sklb->SkeletonResourceHandle == null) return null; if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false); + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, Utf8GamePath.Empty, false); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -260,15 +257,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return node; } - private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb) { - if (sklb->SkeletonParameterResourceHandle == null) + if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, false); + var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); if (node != null) { if (WithUiData) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dfca5805..f520c83a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -15,7 +15,6 @@ public class ResourceNode : ICloneable public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; public readonly ulong Length; - public readonly bool Internal; public readonly List Children; internal ResolveContext? ResolveContext; @@ -31,14 +30,16 @@ public class ResourceNode : ICloneable } } - internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext) + public bool Internal + => Type is ResourceType.Imc; + + internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; PossibleGamePaths = Array.Empty(); Length = length; - Internal = @internal; Children = new List(); ResolveContext = resolveContext; } @@ -54,7 +55,6 @@ public class ResourceNode : ICloneable PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; Length = other.Length; - Internal = other.Internal; Children = other.Children; ResolveContext = other.ResolveContext; } From 28a396470baae8e2162b94d2e0126b1d4e6b914d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:11:48 +0100 Subject: [PATCH 1257/2451] ResourceTree: De-inline GlobalResolveContext in ResolveContext --- .../Interop/ResourceTree/ResolveContext.cs | 49 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8e286ad0..e5a122ac 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -19,11 +19,10 @@ internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCach public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment); + => new(this, slot, equipment); } -internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData, - Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -32,7 +31,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (resourceHandle == null) return null; - if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; if (gamePath.IsEmpty) @@ -48,7 +47,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (resourceHandle == null) return null; - if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; if (dx11) @@ -98,7 +97,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree FullPath = fullPath, }; if (autoAdd) - Nodes.Add((nint)resourceHandle, node); + Global.Nodes.Add((nint)resourceHandle, node); return node; } @@ -108,7 +107,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (imc == null) return null; - if (Nodes.TryGetValue((nint)imc, out var cached)) + if (Global.Nodes.TryGetValue((nint)imc, out var cached)) return cached; return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); @@ -119,7 +118,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (tex == null) return null; - if (Nodes.TryGetValue((nint)tex, out var cached)) + if (Global.Nodes.TryGetValue((nint)tex, out var cached)) return cached; return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); @@ -130,7 +129,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (mdl == null || mdl->ModelResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); @@ -141,13 +140,13 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) { - if (WithUiData) + if (Global.WithUiData) mtrlNode.FallbackName = $"Material #{i}"; node.Children.Add(mtrlNode); } } - Nodes.Add((nint)mdl->ModelResourceHandle, node); + Global.Nodes.Add((nint)mdl->ModelResourceHandle, node); return node; } @@ -180,7 +179,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return null; var resource = mtrl->MaterialResourceHandle; - if (Nodes.TryGetValue((nint)resource, out var cached)) + if (Global.Nodes.TryGetValue((nint)resource, out var cached)) return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); @@ -190,12 +189,12 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); if (shpkNode != null) { - if (WithUiData) + if (Global.WithUiData) shpkNode.Name = "Shader Package"; node.Children.Add(shpkNode); } - var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -205,7 +204,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (texNode == null) continue; - if (WithUiData) + if (Global.WithUiData) { string? name = null; if (shpk != null) @@ -232,7 +231,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree node.Children.Add(texNode); } - Nodes.Add((nint)resource, node); + Global.Nodes.Add((nint)resource, node); return node; } @@ -242,7 +241,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, Utf8GamePath.Empty, false); @@ -251,7 +250,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); if (skpNode != null) node.Children.Add(skpNode); - Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + Global.Nodes.Add((nint)sklb->SkeletonResourceHandle, node); } return node; @@ -262,15 +261,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); if (node != null) { - if (WithUiData) + if (Global.WithUiData) node.FallbackName = "Skeleton Parameters"; - Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + Global.Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } return node; @@ -297,7 +296,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree { "accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Id:D4}"), "equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Id:D4}"), - "monster" => SafeGet(path, 2) == $"m{Skeleton:D4}", + "monster" => SafeGet(path, 2) == $"m{Global.Skeleton:D4}", "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"), _ => null, }, @@ -319,7 +318,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree // Weapons intentionally left out. var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment"; if (isEquipment) - foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) + foreach (var item in Global.Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) { var name = Slot switch { @@ -342,7 +341,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath) { - foreach (var obj in Identifier.Identify(gamePath.ToString())) + foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; if (name.StartsWith("Customization:")) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 836e79e2..38dae6b8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -179,7 +179,7 @@ public class ResourceTree var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) { - if (context.WithUiData) + if (context.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; nodes.Add(sklbNode); } From 57f8587a4358853f85be4a3d75a59f5df24e69c0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:18:20 +0100 Subject: [PATCH 1258/2451] ResourceTree: Use both game path and resource handle as keys for dedup --- .../Interop/ResourceTree/ResolveContext.cs | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index e5a122ac..a359411b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -16,7 +16,7 @@ namespace Penumbra.Interop.ResourceTree; internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData) { - public readonly Dictionary Nodes = new(128); + public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) => new(this, slot, equipment); @@ -30,16 +30,12 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char { if (resourceHandle == null) return null; - - if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) - return cached; - if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) @@ -47,9 +43,6 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (resourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) - return cached; - if (dx11) { var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); @@ -80,7 +73,19 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); + } + + private unsafe ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + Utf8GamePath gamePath) + { + if (resourceHandle == null) + throw new ArgumentNullException(nameof(resourceHandle)); + + if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached)) + return cached; + + return CreateNode(type, objectAddress, resourceHandle, gamePath); } private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, @@ -97,7 +102,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char FullPath = fullPath, }; if (autoAdd) - Global.Nodes.Add((nint)resourceHandle, node); + Global.Nodes.Add((gamePath, (nint)resourceHandle), node); return node; } @@ -107,10 +112,9 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (imc == null) return null; - if (Global.Nodes.TryGetValue((nint)imc, out var cached)) - return cached; + var path = Utf8GamePath.Empty; // TODO - return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); + return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) @@ -118,10 +122,9 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (tex == null) return null; - if (Global.Nodes.TryGetValue((nint)tex, out var cached)) - return cached; + var path = Utf8GamePath.Empty; // TODO - return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); + return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) @@ -129,10 +132,12 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mdl == null || mdl->ModelResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, path, false); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -146,7 +151,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char } } - Global.Nodes.Add((nint)mdl->ModelResourceHandle, node); + Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); return node; } @@ -178,11 +183,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mtrl == null || mtrl->MaterialResourceHandle == null) return null; + var path = Utf8GamePath.Empty; // TODO + var resource = mtrl->MaterialResourceHandle; - if (Global.Nodes.TryGetValue((nint)resource, out var cached)) + if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); if (node == null) return null; @@ -231,7 +238,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char node.Children.Add(texNode); } - Global.Nodes.Add((nint)resource, node); + Global.Nodes.Add((path, (nint)resource), node); return node; } @@ -241,16 +248,18 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); if (skpNode != null) node.Children.Add(skpNode); - Global.Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); } return node; @@ -261,15 +270,17 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); if (node != null) { if (Global.WithUiData) node.FallbackName = "Skeleton Parameters"; - Global.Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node); } return node; From da54222bb1f66ce189b335a7cbf78521a5a1583d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 20:59:09 +0100 Subject: [PATCH 1259/2451] ResourceTree: Add EID files --- .../Interop/ResourceTree/ResolveContext.cs | 10 ++++++++++ Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a359411b..26d64afe 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -107,6 +107,16 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } + public unsafe ResourceNode? CreateNodeFromEid(ResourceHandle* eid) + { + if (eid == null) + return null; + + var path = Utf8GamePath.Empty; // TODO + + return GetOrCreateNode(ResourceType.Eid, 0, eid, path); + } + public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { if (imc == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index f520c83a..53dedfa0 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -31,7 +31,7 @@ public class ResourceNode : ICloneable } public bool Internal - => Type is ResourceType.Imc; + => Type is ResourceType.Eid or ResourceType.Imc; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 38dae6b8..687c14ec 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -62,6 +62,15 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + var eid = (ResourceHandle*)model->EID; + var eidNode = globalContext.CreateContext(EquipSlot.Unknown, default).CreateNodeFromEid(eid); + if (eidNode != null) + { + if (globalContext.WithUiData) + eidNode.FallbackName = "EID"; + Nodes.Add(eidNode); + } + for (var i = 0; i < model->SlotCount; ++i) { var context = globalContext.CreateContext( @@ -113,6 +122,15 @@ public class ResourceTree weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default ); + var eid = (ResourceHandle*)subObject->EID; + var eidNode = subObjectContext.CreateNodeFromEid(eid); + if (eidNode != null) + { + if (globalContext.WithUiData) + eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID"; + Nodes.Add(eidNode); + } + for (var i = 0; i < subObject->SlotCount; ++i) { var imc = (ResourceHandle*)subObject->IMCArray[i]; From 69a4e2b52ef7ce0485d9881ddc5007bd2edd8f29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Nov 2023 12:59:35 +0100 Subject: [PATCH 1260/2451] Fix Linking changed items not working. --- Penumbra/Penumbra.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 73d1013e..99a81fd1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -19,6 +19,7 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; @@ -111,13 +112,13 @@ public class Penumbra : IDalamudPlugin _services.GetRequiredService(); _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is Item) + if (it is (Item, FullEquipType)) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); _communicatorService.ChangedItemClick.Subscribe((button, it) => { - if (button == MouseButton.Left && it is Item item) + if (button == MouseButton.Left && it is (Item item, FullEquipType type)) Messager.LinkItem(item); }, ChangedItemClick.Priority.Link); } From 79c43fe7b1edc00d1afb357f37e447ddfde35773 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 21:01:40 +0100 Subject: [PATCH 1261/2451] PathResolving: Better function signatures? (names + types + TMB param count + dedupe) --- Penumbra/Interop/PathResolving/PathState.cs | 2 +- .../Interop/PathResolving/ResolvePathHooks.cs | 175 +++++++----------- 2 files changed, 71 insertions(+), 106 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index 4fb3d31d..f300a666 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -42,7 +42,7 @@ public unsafe class PathState : IDisposable MetaState = metaState; CharacterUtility = characterUtility; _human = new ResolvePathHooks(interop, this, _humanVTable, ResolvePathHooks.Type.Human); - _weapon = new ResolvePathHooks(interop, this, _weaponVTable, ResolvePathHooks.Type.Weapon); + _weapon = new ResolvePathHooks(interop, this, _weaponVTable, ResolvePathHooks.Type.Other); _demiHuman = new ResolvePathHooks(interop, this, _demiHumanVTable, ResolvePathHooks.Type.Other); _monster = new ResolvePathHooks(interop, this, _monsterVTable, ResolvePathHooks.Type.Other); _human.Enable(); diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index f9a341b9..9d010d64 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -12,45 +12,47 @@ public unsafe class ResolvePathHooks : IDisposable public enum Type { Human, - Weapon, Other, } - private delegate nint GeneralResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4); - private delegate nint MPapResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, uint unk5); - private delegate nint MaterialResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5); - private delegate nint EidResolveDelegate(nint drawObject, nint path, nint unk3); + private delegate nint MPapResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, uint sId); + private delegate nint NamedResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint name); + private delegate nint PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex); + private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize); + private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName); + // Kept separate from NamedResolveDelegate because the 5th parameter has out semantics here, instead of in. + private delegate nint VfxResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam); - private readonly Hook _resolveDecalPathHook; - private readonly Hook _resolveEidPathHook; - private readonly Hook _resolveImcPathHook; - private readonly Hook _resolveMPapPathHook; - private readonly Hook _resolveMdlPathHook; - private readonly Hook _resolveMtrlPathHook; - private readonly Hook _resolvePapPathHook; - private readonly Hook _resolvePhybPathHook; - private readonly Hook _resolveSklbPathHook; - private readonly Hook _resolveSkpPathHook; - private readonly Hook _resolveTmbPathHook; - private readonly Hook _resolveVfxPathHook; + private readonly Hook _resolveDecalPathHook; + private readonly Hook _resolveEidPathHook; + private readonly Hook _resolveImcPathHook; + private readonly Hook _resolveMPapPathHook; + private readonly Hook _resolveMdlPathHook; + private readonly Hook _resolveMtrlPathHook; + private readonly Hook _resolvePapPathHook; + private readonly Hook _resolvePhybPathHook; + private readonly Hook _resolveSklbPathHook; + private readonly Hook _resolveSkpPathHook; + private readonly Hook _resolveTmbPathHook; + private readonly Hook _resolveVfxPathHook; private readonly PathState _parent; public ResolvePathHooks(IGameInteropProvider interop, PathState parent, nint* vTable, Type type) { _parent = parent; - _resolveDecalPathHook = Create(interop, vTable[83], type, ResolveDecalWeapon, ResolveDecal); - _resolveEidPathHook = Create(interop, vTable[85], type, ResolveEidWeapon, ResolveEid); - _resolveImcPathHook = Create(interop, vTable[81], type, ResolveImcWeapon, ResolveImc); - _resolveMPapPathHook = Create(interop, vTable[79], type, ResolveMPapWeapon, ResolveMPap); - _resolveMdlPathHook = Create(interop, vTable[73], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman); - _resolveMtrlPathHook = Create(interop, vTable[82], type, ResolveMtrlWeapon, ResolveMtrl); - _resolvePapPathHook = Create(interop, vTable[76], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman); - _resolvePhybPathHook = Create(interop, vTable[75], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman); - _resolveSklbPathHook = Create(interop, vTable[72], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman); - _resolveSkpPathHook = Create(interop, vTable[74], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman); - _resolveTmbPathHook = Create(interop, vTable[77], type, ResolveTmbWeapon, ResolveTmb); - _resolveVfxPathHook = Create(interop, vTable[84], type, ResolveVfxWeapon, ResolveVfx); + _resolveDecalPathHook = Create(interop, vTable[83], ResolveDecal); + _resolveEidPathHook = Create(interop, vTable[85], ResolveEid); + _resolveImcPathHook = Create(interop, vTable[81], ResolveImc); + _resolveMPapPathHook = Create(interop, vTable[79], ResolveMPap); + _resolveMdlPathHook = Create(interop, vTable[73], type, ResolveMdl, ResolveMdlHuman); + _resolveMtrlPathHook = Create(interop, vTable[82], ResolveMtrl); + _resolvePapPathHook = Create(interop, vTable[76], type, ResolvePap, ResolvePapHuman); + _resolvePhybPathHook = Create(interop, vTable[75], type, ResolvePhyb, ResolvePhybHuman); + _resolveSklbPathHook = Create(interop, vTable[72], type, ResolveSklb, ResolveSklbHuman); + _resolveSkpPathHook = Create(interop, vTable[74], type, ResolveSkp, ResolveSkpHuman); + _resolveTmbPathHook = Create(interop, vTable[77], ResolveTmb); + _resolveVfxPathHook = Create(interop, vTable[84], ResolveVfx); } public void Enable() @@ -101,74 +103,74 @@ public unsafe class ResolvePathHooks : IDisposable _resolveVfxPathHook.Dispose(); } - private nint ResolveDecal(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4)); + private nint ResolveDecal(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) + => ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); - private nint ResolveEid(nint drawObject, nint path, nint unk3) - => ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3)); + private nint ResolveEid(nint drawObject, nint pathBuffer, nint pathBufferSize) + => ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, pathBuffer, pathBufferSize)); - private nint ResolveImc(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4)); + private nint ResolveImc(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) + => ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); - private nint ResolveMPap(nint drawObject, nint path, nint unk3, uint unk4, uint unk5) - => ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + private nint ResolveMPap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, uint unkSId) + => ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, unkSId)); - private nint ResolveMdl(nint drawObject, nint path, nint unk3, uint modelType) - => ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); + private nint ResolveMdl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) + => ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); - private nint ResolveMtrl(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5)); + private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName) + => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); - private nint ResolvePap(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) + => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); - private nint ResolvePhyb(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); + private nint ResolvePhyb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - private nint ResolveSklb(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); + private nint ResolveSklb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - private nint ResolveSkp(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); + private nint ResolveSkp(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - private nint ResolveTmb(nint drawObject, nint path, nint unk3) - => ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3)); + private nint ResolveTmb(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName) + => ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, timelineName)); - private nint ResolveVfx(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5)); + private nint ResolveVfx(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) + => ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam)); - private nint ResolveMdlHuman(nint drawObject, nint path, nint unk3, uint modelType) + private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqdp = modelType > 9 + using var eqdp = slotIndex > 9 ? DisposableContainer.Empty - : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), modelType < 5, modelType > 4); - return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); + : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4); + return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); } - private nint ResolvePapHuman(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) + private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); + return ResolvePath(data, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); } - private nint ResolvePhybHuman(nint drawObject, nint path, nint unk3, uint unk4) + private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); + return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } - private nint ResolveSklbHuman(nint drawObject, nint path, nint unk3, uint unk4) + private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); + return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } - private nint ResolveSkpHuman(nint drawObject, nint path, nint unk3, uint unk4) + private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); + return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) @@ -180,58 +182,21 @@ public unsafe class ResolvePathHooks : IDisposable data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Head)); } - private nint ResolveDecalWeapon(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4)); - - private nint ResolveEidWeapon(nint drawObject, nint path, nint unk3) - => ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3)); - - private nint ResolveImcWeapon(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4)); - - private nint ResolveMPapWeapon(nint drawObject, nint path, nint unk3, uint unk4, uint unk5) - => ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5)); - - private nint ResolveMdlWeapon(nint drawObject, nint path, nint unk3, uint modelType) - => ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType)); - - private nint ResolveMtrlWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5)); - - private nint ResolvePapWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5)); - - private nint ResolvePhybWeapon(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4)); - - private nint ResolveSklbWeapon(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4)); - - private nint ResolveSkpWeapon(nint drawObject, nint path, nint unk3, uint unk4) - => ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4)); - - private nint ResolveTmbWeapon(nint drawObject, nint path, nint unk3) - => ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3)); - - private nint ResolveVfxWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5) - => ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5)); - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(IGameInteropProvider interop, nint address, Type type, T weapon, T other, T human) where T : Delegate + private static Hook Create(IGameInteropProvider interop, nint address, Type type, T other, T human) where T : Delegate { var del = type switch { Type.Human => human, - Type.Weapon => weapon, _ => other, }; return interop.HookFromAddress(address, del); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(IGameInteropProvider interop, nint address, Type type, T weapon, T other) where T : Delegate - => Create(interop, address, type, weapon, other, other); + private static Hook Create(IGameInteropProvider interop, nint address, T del) where T : Delegate + => interop.HookFromAddress(address, del); // Implementation From 7dabb3c647b08848dfa99915f2e46e1e746a7b29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 Nov 2023 15:23:48 +0100 Subject: [PATCH 1262/2451] Add some Redrawing Debug UI. --- Penumbra/Interop/Services/RedrawService.cs | 16 +++++++- Penumbra/UI/Tabs/DebugTab.cs | 46 +++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 5cc493ad..ec858290 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -18,10 +18,13 @@ public unsafe partial class RedrawService public const int GPoseSlots = 42; public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; - private readonly string?[] _gPoseNames = new string?[GPoseSlots]; + private readonly string?[] _gPoseNames = new string?[GPoseSlots]; private int _gPoseNameCounter; - private bool InGPose + internal IReadOnlyList GPoseNames + => _gPoseNames; + + internal bool InGPose => _clientState.IsGPosing; // VFuncs that disable and enable draw, used only for GPose actors. @@ -108,6 +111,15 @@ public sealed unsafe partial class RedrawService : IDisposable private readonly List _afterGPoseQueue = new(GPoseSlots); private int _target = -1; + internal IReadOnlyList Queue + => _queue; + + internal IReadOnlyList AfterGPoseQueue + => _afterGPoseQueue; + + internal int Target + => _target; + public event GameObjectRedrawnDelegate? GameObjectRedrawn; public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState) diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 5abb3c2f..dc39707a 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -65,6 +65,7 @@ public class DebugTab : Window, ITab private readonly TextureManager _textureManager; private readonly SkinFixer _skinFixer; private readonly IdentifierService _identifier; + private readonly RedrawService _redraws; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -72,7 +73,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer, IdentifierService identifier) + TextureManager textureManager, SkinFixer skinFixer, IdentifierService identifier, RedrawService redraws) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -107,6 +108,7 @@ public class DebugTab : Window, ITab _textureManager = textureManager; _skinFixer = skinFixer; _identifier = identifier; + _redraws = redraws; } public ReadOnlySpan Label @@ -317,6 +319,48 @@ public class DebugTab : Window, ITab } } } + + using (var tree = TreeNode("Redraw Service")) + { + if (tree) + { + using var table = Table("##redraws", 3, ImGuiTableFlags.RowBg); + if (table) + { + ImGuiUtil.DrawTableColumn("In GPose"); + ImGuiUtil.DrawTableColumn(_redraws.InGPose.ToString()); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Target"); + ImGuiUtil.DrawTableColumn(_redraws.Target.ToString()); + ImGui.TableNextColumn(); + + foreach (var (objectIdx, idx) in _redraws.Queue.WithIndex()) + { + var (actualIdx, state) = objectIdx < 0 ? (~objectIdx, "Queued") : (objectIdx, "Invisible"); + ImGuiUtil.DrawTableColumn($"Redraw Queue #{idx}"); + ImGuiUtil.DrawTableColumn(actualIdx.ToString()); + ImGuiUtil.DrawTableColumn(state); + } + + foreach (var (objectIdx, idx) in _redraws.AfterGPoseQueue.WithIndex()) + { + var (actualIdx, state) = objectIdx < 0 ? (~objectIdx, "Queued") : (objectIdx, "Invisible"); + ImGuiUtil.DrawTableColumn($"GPose Queue #{idx}"); + ImGuiUtil.DrawTableColumn(actualIdx.ToString()); + ImGuiUtil.DrawTableColumn(state); + } + + foreach (var (name, idx) in _redraws.GPoseNames.OfType().WithIndex()) + { + ImGuiUtil.DrawTableColumn($"GPose Name #{idx}"); + ImGuiUtil.DrawTableColumn(name); + ImGui.TableNextColumn(); + } + + } + } + } } private void DrawPerformanceTab() From 2852562a03d4a09043691eeb2f5dad28ba956c30 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 22:41:20 +0100 Subject: [PATCH 1263/2451] ResourceTree: Use ResolveXXXPath where possible --- Penumbra.GameData | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 35 +++++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 59 ++++++++++++------ .../Interop/Structs/CharacterBaseUtility.cs | 62 +++++++++++++++++++ 4 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 Penumbra/Interop/Structs/CharacterBaseUtility.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 04ddadb4..b141301c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 04ddadb44600a382e26661e1db08fd16c3b671d8 +Subproject commit b141301c4ee65422d6802f3038c8f344911d4ae2 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 26d64afe..d700131d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,6 +1,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; using OtterGui; using Penumbra.Api.Enums; using Penumbra.GameData; @@ -9,6 +11,7 @@ using Penumbra.GameData.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; +using static Penumbra.Interop.Structs.CharacterBaseUtility; using static Penumbra.Interop.Structs.StructExtensions; namespace Penumbra.Interop.ResourceTree; @@ -18,11 +21,11 @@ internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCach { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(this, slot, equipment); + public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment) + => new(this, characterBase, slotIndex, slot, equipment); } -internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -112,7 +115,8 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (eid == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveEidPath(CharacterBase), out var path)) + return null; return GetOrCreateNode(ResourceType.Eid, 0, eid, path); } @@ -122,17 +126,19 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (imc == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveImcPath(CharacterBase, SlotIndex), out var path)) + return null; return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } - public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { if (tex == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromString(gamePath, out var path)) + return null; return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } @@ -142,7 +148,8 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mdl == null || mdl->ModelResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) return cached; @@ -253,12 +260,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } - public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb) + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; @@ -266,7 +274,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); if (node != null) { - var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); + var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); if (skpNode != null) node.Children.Add(skpNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); @@ -275,12 +283,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } - private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 687c14ec..7c58d6a8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,7 +1,9 @@ +using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.UI; @@ -62,8 +64,10 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default); + var eid = (ResourceHandle*)model->EID; - var eidNode = globalContext.CreateContext(EquipSlot.Unknown, default).CreateNodeFromEid(eid); + var eidNode = genericContext.CreateNodeFromEid(eid); if (eidNode != null) { if (globalContext.WithUiData) @@ -73,13 +77,15 @@ public class ResourceTree for (var i = 0; i < model->SlotCount; ++i) { - var context = globalContext.CreateContext( + var slotContext = globalContext.CreateContext( + model, + (uint)i, i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown, i < equipment.Length ? equipment[i] : default ); var imc = (ResourceHandle*)model->IMCArray[i]; - var imcNode = context.CreateNodeFromImc(imc); + var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) @@ -88,7 +94,7 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = context.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { if (globalContext.WithUiData) @@ -97,18 +103,20 @@ public class ResourceTree } } - AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); + AddSkeleton(Nodes, genericContext, model->Skeleton); + + AddSubObjects(globalContext, model); if (human != null) AddHumanResources(globalContext, human); } - private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) + private unsafe void AddSubObjects(GlobalResolveContext globalContext, CharacterBase* model) { var subObjectIndex = 0; var weaponIndex = 0; var subObjectNodes = new List(); - foreach (var baseSubObject in human->CharacterBase.DrawObject.Object.ChildObjects) + foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; @@ -117,13 +125,13 @@ public class ResourceTree var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var subObjectContext = globalContext.CreateContext( - weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown, - weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default - ); + var slot = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown; + var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default; + + var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment); var eid = (ResourceHandle*)subObject->EID; - var eidNode = subObjectContext.CreateNodeFromEid(eid); + var eidNode = genericContext.CreateNodeFromEid(eid); if (eidNode != null) { if (globalContext.WithUiData) @@ -133,8 +141,10 @@ public class ResourceTree for (var i = 0; i < subObject->SlotCount; ++i) { + var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment); + var imc = (ResourceHandle*)subObject->IMCArray[i]; - var imcNode = subObjectContext.CreateNodeFromImc(imc); + var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) @@ -143,7 +153,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { if (globalContext.WithUiData) @@ -152,17 +162,24 @@ public class ResourceTree } } - AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); ++subObjectIndex; if (weapon != null) ++weaponIndex; } Nodes.InsertRange(0, subObjectNodes); + } - var context = globalContext.CreateContext(EquipSlot.Unknown, default); + private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) + { + var genericContext = globalContext.CreateContext(&human->CharacterBase, 0xFFFFFFFFu, EquipSlot.Unknown, default); - var decalNode = context.CreateNodeFromTex(human->Decal); + var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); + var decalPath = decalId != 0 + ? GamePaths.Human.Decal.FaceDecalPath(decalId) + : GamePaths.Tex.TransparentPath; + var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath); if (decalNode != null) { if (globalContext.WithUiData) @@ -174,7 +191,11 @@ public class ResourceTree Nodes.Add(decalNode); } - var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); + var hasLegacyDecal = (human->Customize[(int)CustomizeIndex.FaceFeatures] & 0x80) != 0; + var legacyDecalPath = hasLegacyDecal + ? GamePaths.Human.Decal.LegacyDecalPath + : GamePaths.Tex.TransparentPath; + var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); if (legacyDecalNode != null) { if (globalContext.WithUiData) @@ -194,7 +215,7 @@ public class ResourceTree for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i); if (sklbNode != null) { if (context.Global.WithUiData) diff --git a/Penumbra/Interop/Structs/CharacterBaseUtility.cs b/Penumbra/Interop/Structs/CharacterBaseUtility.cs new file mode 100644 index 00000000..c29f44a3 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseUtility.cs @@ -0,0 +1,62 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.String; + +namespace Penumbra.Interop.Structs; + +// TODO submit these to ClientStructs +public static unsafe class CharacterBaseUtility +{ + private const int PathBufferSize = 260; + + private const uint ResolveSklbPathVf = 72; + private const uint ResolveMdlPathVf = 73; + private const uint ResolveSkpPathVf = 74; + private const uint ResolveImcPathVf = 81; + private const uint ResolveMtrlPathVf = 82; + private const uint ResolveEidPathVf = 85; + + private static void* GetVFunc(CharacterBase* characterBase, uint vfIndex) + => ((void**)characterBase->VTable)[vfIndex]; + + private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex) + { + var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); + var pathBuffer = stackalloc byte[PathBufferSize]; + var path = vFunc(characterBase, pathBuffer, PathBufferSize); + return path != null ? new ByteString(path).Clone() : null; + } + + private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex) + { + var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); + var pathBuffer = stackalloc byte[PathBufferSize]; + var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex); + return path != null ? new ByteString(path).Clone() : null; + } + + private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex, byte* name) + { + var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); + var pathBuffer = stackalloc byte[PathBufferSize]; + var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex, name); + return path != null ? new ByteString(path).Clone() : null; + } + + public static ByteString? ResolveEidPath(CharacterBase* characterBase) + => ResolvePath(characterBase, ResolveEidPathVf); + + public static ByteString? ResolveImcPath(CharacterBase* characterBase, uint slotIndex) + => ResolvePath(characterBase, ResolveImcPathVf, slotIndex); + + public static ByteString? ResolveMdlPath(CharacterBase* characterBase, uint slotIndex) + => ResolvePath(characterBase, ResolveMdlPathVf, slotIndex); + + public static ByteString? ResolveMtrlPath(CharacterBase* characterBase, uint slotIndex, byte* mtrlFileName) + => ResolvePath(characterBase, ResolveMtrlPathVf, slotIndex, mtrlFileName); + + public static ByteString? ResolveSklbPath(CharacterBase* characterBase, uint partialSkeletonIndex) + => ResolvePath(characterBase, ResolveSklbPathVf, partialSkeletonIndex); + + public static ByteString? ResolveSkpPath(CharacterBase* characterBase, uint partialSkeletonIndex) + => ResolvePath(characterBase, ResolveSkpPathVf, partialSkeletonIndex); +} From fd163f8f66b1bc6edaa9a0cb2a431dcb94eef29d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 4 Nov 2023 18:30:36 +0100 Subject: [PATCH 1264/2451] ResourceTree: WIP - Path resolution --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EstCache.cs | 21 ++ Penumbra/Collections/Cache/MetaCache.cs | 18 ++ Penumbra/Interop/PathResolving/PathState.cs | 44 +++- .../Interop/PathResolving/ResolvePathHooks.cs | 6 +- .../ResolveContext.PathResolution.cs | 248 ++++++++++++++++++ .../Interop/ResourceTree/ResolveContext.cs | 105 +++----- Penumbra/Interop/ResourceTree/ResourceTree.cs | 88 +++---- .../ResourceTree/ResourceTreeFactory.cs | 33 ++- .../Structs/ModelResourceHandleUtility.cs | 18 ++ Penumbra/Meta/Files/ImcFile.cs | 7 + Penumbra/Penumbra.cs | 1 + Penumbra/Services/ServiceManager.cs | 3 +- 13 files changed, 452 insertions(+), 142 deletions(-) create mode 100644 Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs create mode 100644 Penumbra/Interop/Structs/ModelResourceHandleUtility.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index b141301c..1f274b41 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b141301c4ee65422d6802f3038c8f344911d4ae2 +Subproject commit 1f274b41e3e703712deb83f3abd8727e10614ebe diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 43ebcf56..9e2cdef9 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,5 +1,6 @@ using OtterGui.Filesystem; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -61,6 +62,26 @@ public struct EstCache : IDisposable return manager.TemporarilySetFile(file, idx); } + private readonly EstFile? GetEstFile(EstManipulation.EstType type) + { + return type switch + { + EstManipulation.EstType.Face => _estFaceFile, + EstManipulation.EstType.Hair => _estHairFile, + EstManipulation.EstType.Body => _estBodyFile, + EstManipulation.EstType.Head => _estHeadFile, + _ => null, + }; + } + + internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, SetId setId) + { + var file = GetEstFile(type); + return file != null + ? file[genderRace, setId.Id] + : EstFile.GetDefault(manager, type, genderRace, setId); + } + public void Reset() { _estFaceFile?.Reset(); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 8eb7a5a0..0da11022 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -186,6 +187,23 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.GetImcFile(path, out file); + public ImcEntry GetImcEntry(Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists) + => GetImcFile(path, out var file) + ? file.GetEntry(Meta.Files.ImcFile.PartIndex(slot), variantIdx, out exists) + : Meta.Files.ImcFile.GetDefault(_manager, path, slot, variantIdx, out exists); + + internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId) + { + var eqdpFile = _eqdpCache.EqdpFile(race, accessory); + if (eqdpFile != null) + return setId.Id < eqdpFile.Count ? eqdpFile[setId] : default; + else + return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, setId); + } + + internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, SetId setId) + => _estCache.GetEstEntry(_manager, type, genderRace, setId); + /// Use this when CharacterUtility becomes ready. private void ApplyStoredManipulations() { diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index f300a666..6d7840d8 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -30,11 +30,15 @@ public unsafe class PathState : IDisposable private readonly ResolvePathHooks _demiHuman; private readonly ResolvePathHooks _monster; - private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); + private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); + private readonly ThreadLocal _internalResolve = new(() => 0, false); public IList CurrentData => _resolveData.Values; + public bool InInternalResolve + => _internalResolve.Value != 0u; + public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); @@ -55,6 +59,7 @@ public unsafe class PathState : IDisposable public void Dispose() { _resolveData.Dispose(); + _internalResolve.Dispose(); _human.Dispose(); _weapon.Dispose(); _demiHuman.Dispose(); @@ -80,7 +85,10 @@ public unsafe class PathState : IDisposable if (path == nint.Zero) return path; - _resolveData.Value = collection.ToResolveData(gameObject); + if (!InInternalResolve) + { + _resolveData.Value = collection.ToResolveData(gameObject); + } return path; } @@ -90,7 +98,37 @@ public unsafe class PathState : IDisposable if (path == nint.Zero) return path; - _resolveData.Value = data; + if (!InInternalResolve) + { + _resolveData.Value = data; + } return path; } + + /// + /// Temporarily disables metadata mod application and resolve data capture on the current thread. + /// Must be called to prevent race conditions between Penumbra's internal path resolution (for example for Resource Trees) and the game's path resolution. + /// Please note that this will make path resolution cases that depend on metadata incorrect. + /// + /// A struct that will undo this operation when disposed. Best used with: using (var _ = pathState.EnterInternalResolve()) { ... } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public InternalResolveRaii EnterInternalResolve() + => new(this); + + public readonly ref struct InternalResolveRaii + { + private readonly ThreadLocal _internalResolve; + + public InternalResolveRaii(PathState parent) + { + _internalResolve = parent._internalResolve; + ++_internalResolve.Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void Dispose() + { + --_internalResolve.Value; + } + } } diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index 9d010d64..3be7ffdd 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -143,7 +143,7 @@ public unsafe class ResolvePathHooks : IDisposable private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqdp = slotIndex > 9 + using var eqdp = slotIndex > 9 || _parent.InInternalResolve ? DisposableContainer.Empty : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4); return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); @@ -176,6 +176,10 @@ public unsafe class ResolvePathHooks : IDisposable private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) { data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + if (_parent.InInternalResolve) + { + return DisposableContainer.Empty; + } return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs new file mode 100644 index 00000000..bcc957df --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -0,0 +1,248 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.String; +using Penumbra.String.Classes; +using static Penumbra.Interop.Structs.CharacterBaseUtility; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; + +namespace Penumbra.Interop.ResourceTree; + +internal partial record ResolveContext +{ + + private Utf8GamePath ResolveModelPath() + { + // Correctness: + // Resolving a model path through the game's code can use EQDP metadata for human equipment models. + return ModelType switch + { + ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), + _ => ResolveModelPathNative(), + }; + } + + private Utf8GamePath ResolveEquipmentModelPath() + { + var path = SlotIndex < 5 + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe GenderRace ResolveModelRaceCode() + => ResolveEqdpRaceCode(Slot, Equipment.Set); + + private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, SetId setId) + { + var slotIndex = slot.ToIndex(); + if (slotIndex >= 10 || ModelType != ModelType.Human) + return GenderRace.MidlanderMale; + + var characterRaceCode = (GenderRace)((Human*)CharacterBase.Value)->RaceSexId; + if (characterRaceCode == GenderRace.MidlanderMale) + return GenderRace.MidlanderMale; + + var accessory = slotIndex >= 5; + if ((ushort)characterRaceCode % 10 != 1 && accessory) + return GenderRace.MidlanderMale; + + var metaCache = Global.Collection.MetaCache; + if (metaCache == null) + return GenderRace.MidlanderMale; + + var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, setId); + if (entry.ToBits(slot).Item2) + return characterRaceCode; + + var fallbackRaceCode = characterRaceCode.Fallback(); + if (fallbackRaceCode == GenderRace.MidlanderMale) + return GenderRace.MidlanderMale; + + entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, setId); + if (entry.ToBits(slot).Item2) + return fallbackRaceCode; + + return GenderRace.MidlanderMale; + } + + private unsafe Utf8GamePath ResolveModelPathNative() + { + var path = ResolveMdlPath(CharacterBase, SlotIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + { + // Safety: + // Resolving a material path through the game's code can dereference null pointers for equipment materials. + return ModelType switch + { + ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.Weapon => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + _ => ResolveMaterialPathNative(mtrlFileName), + }; + } + + private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + { + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var modelPathSpan = modelPath.Path.Span; + var baseDirectory = modelPathSpan[..modelPathSpan.IndexOf("/model/"u8)]; + + var variant = ResolveMaterialVariant(imcPath); + + Span pathBuffer = stackalloc byte[260]; + baseDirectory.CopyTo(pathBuffer); + "/material/v"u8.CopyTo(pathBuffer[baseDirectory.Length..]); + WriteZeroPaddedNumber(pathBuffer.Slice(baseDirectory.Length + 11, 4), variant); + pathBuffer[baseDirectory.Length + 15] = (byte)'/'; + fileName.CopyTo(pathBuffer[(baseDirectory.Length + 16)..]); + + return Utf8GamePath.FromSpan(pathBuffer[..(baseDirectory.Length + 16 + fileName.Length)], out var path) ? path.Clone() : Utf8GamePath.Empty; + } + + private byte ResolveMaterialVariant(Utf8GamePath imcPath) + { + var metaCache = Global.Collection.MetaCache; + if (metaCache == null) + return Equipment.Variant.Id; + + var entry = metaCache.GetImcEntry(imcPath, Slot, Equipment.Variant, out var exists); + if (!exists) + return Equipment.Variant.Id; + + return entry.MaterialId; + } + + private static void WriteZeroPaddedNumber(Span destination, ushort number) + { + for (var i = destination.Length; i-- > 0;) + { + destination[i] = (byte)('0' + number % 10); + number /= 10; + } + } + + private unsafe Utf8GamePath ResolveMaterialPathNative(byte* mtrlFileName) + { + ByteString? path; + try + { + path = ResolveMtrlPath(CharacterBase, SlotIndex, mtrlFileName); + } + catch (AccessViolationException) + { + Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase.Value:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); + return Utf8GamePath.Empty; + } + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private Utf8GamePath ResolveSkeletonPath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a skeleton path through the game's code can use EST metadata for human skeletons. + // Additionally, it can dereference null pointers for human equipment skeletons. + return ModelType switch + { + ModelType.Human => ResolveHumanSkeletonPath(partialSkeletonIndex), + _ => ResolveSkeletonPathNative(partialSkeletonIndex), + }; + } + + private unsafe Utf8GamePath ResolveHumanSkeletonPath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Skeleton.Sklb.Path(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex) + { + var human = (Human*)CharacterBase.Value; + var characterRaceCode = (GenderRace)human->RaceSexId; + switch (partialSkeletonIndex) + { + case 0: + return (characterRaceCode, "base", 1); + case 1: + var faceId = human->FaceId; + var tribe = human->Customize[(int)CustomizeIndex.Tribe]; + var modelType = human->Customize[(int)CustomizeIndex.ModelType]; + if (faceId < 201) + { + faceId -= tribe switch + { + 0xB when modelType == 4 => 100, + 0xE | 0xF => 100, + _ => 0, + }; + } + return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId); + case 2: + return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId); + case 3: + return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head); + case 4: + return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body); + default: + return (0, string.Empty, 0); + } + } + + private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) + { + var human = (Human*)CharacterBase.Value; + var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; + return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); + } + + private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, SetId set) + { + var metaCache = Global.Collection.MetaCache; + var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, set); + return (raceCode, EstManipulation.ToName(type), skeletonSet); + } + + private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) + { + var path = ResolveSklbPath(CharacterBase, partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private Utf8GamePath ResolveSkeletonParameterPath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a skeleton parameter path through the game's code can use EST metadata for human skeletons. + // Additionally, it can dereference null pointers for human equipment skeletons. + return ModelType switch + { + ModelType.Human => ResolveHumanSkeletonParameterPath(partialSkeletonIndex), + _ => ResolveSkeletonParameterPathNative(partialSkeletonIndex), + }; + } + + private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveSkeletonParameterPathNative(uint partialSkeletonIndex) + { + var path = ResolveSkpPath(CharacterBase, partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } +} diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d700131d..f34a6ae2 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -12,23 +13,29 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; using static Penumbra.Interop.Structs.CharacterBaseUtility; +using static Penumbra.Interop.Structs.ModelResourceHandleUtility; using static Penumbra.Interop.Structs.StructExtensions; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, - int Skeleton, bool WithUiData) +internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData) { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment) - => new(this, characterBase, slotIndex, slot, equipment); + public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, + EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, WeaponType weaponType = default) + => new(this, characterBase, slotIndex, slot, equipment, weaponType); } -internal record ResolveContext(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment) +internal partial record ResolveContext(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, + EquipSlot Slot, CharacterArmor Equipment, WeaponType WeaponType) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private unsafe ModelType ModelType + => CharacterBase.Value->GetModelType(); + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) { if (resourceHandle == null) @@ -46,35 +53,33 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer gamePath.Length - 3) return null; - if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-') - { - Span prefixed = stackalloc byte[gamePath.Length + 2]; - gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); - prefixed[lastDirectorySeparator + 1] = (byte)'-'; - prefixed[lastDirectorySeparator + 2] = (byte)'-'; - gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + Span prefixed = stackalloc byte[260]; + gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); + prefixed[lastDirectorySeparator + 1] = (byte)'-'; + prefixed[lastDirectorySeparator + 2] = (byte)'-'; + gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); - if (!Utf8GamePath.FromSpan(prefixed, out var tmp)) - return null; + if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp)) + return null; - gamePath = tmp.Path.Clone(); - } + path = tmp.Clone(); } else { // Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues. if (!gamePath.IsOwned) gamePath = gamePath.Clone(); - } - if (!Utf8GamePath.FromByteString(gamePath, out var path)) - return null; + if (!Utf8GamePath.FromByteString(gamePath, out path)) + return null; + } return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); } @@ -143,23 +148,28 @@ internal record ResolveContext(GlobalResolveContext Global, PointerTexture, &tex->ResourceHandle, path); } - public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) + public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath) { if (mdl == null || mdl->ModelResourceHandle == null) return null; + var mdlResource = mdl->ModelResourceHandle; if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path)) return null; - if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) + if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, path, false); + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdlResource->ResourceHandle, path, false); for (var i = 0; i < mdl->MaterialCount; i++) { - var mtrl = mdl->Materials[i]; - var mtrlNode = CreateNodeFromMaterial(mtrl); + var mtrl = mdl->Materials[i]; + if (mtrl == null) + continue; + + var mtrlFileName = GetMaterialFileNameBySlot(mdlResource, (uint)i); + var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imcPath, mtrlFileName)); if (mtrlNode != null) { if (Global.WithUiData) @@ -173,7 +183,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer alreadyVisitedSamplerIds) { @@ -200,8 +210,6 @@ internal record ResolveContext(GlobalResolveContext Global, PointerMaterialResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO - var resource = mtrl->MaterialResourceHandle; if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; @@ -265,8 +273,7 @@ internal record ResolveContext(GlobalResolveContext Global, PointerSkeletonResourceHandle == null) return null; - if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path)) - return null; + var path = ResolveSkeletonPath(partialSkeletonIndex); if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; @@ -288,8 +295,7 @@ internal record ResolveContext(GlobalResolveContext Global, PointerSkeletonParameterResourceHandle == null) return null; - if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path)) - return null; + var path = ResolveSkeletonParameterPath(partialSkeletonIndex); if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; @@ -305,43 +311,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer FilterGamePaths(IReadOnlyCollection gamePaths) - { - var filtered = new List(gamePaths.Count); - foreach (var path in gamePaths) - { - // In doubt, keep the paths. - if (IsMatch(path.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries)) - ?? true) - filtered.Add(path); - } - - return filtered; - } - - private bool? IsMatch(ReadOnlySpan path) - => SafeGet(path, 0) switch - { - "chara" => SafeGet(path, 1) switch - { - "accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Id:D4}"), - "equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Id:D4}"), - "monster" => SafeGet(path, 2) == $"m{Global.Skeleton:D4}", - "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"), - _ => null, - }, - _ => null, - }; - - private bool? IsMatchEquipment(ReadOnlySpan path, string equipmentDir) - => SafeGet(path, 0) == equipmentDir - ? SafeGet(path, 1) switch - { - "material" => SafeGet(path, 2) == $"v{Equipment.Variant.Id:D4}", - _ => null, - } - : false; - internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 7c58d6a8..5e96d8bf 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String.Classes; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; @@ -64,25 +65,13 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; - var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default); - - var eid = (ResourceHandle*)model->EID; - var eidNode = genericContext.CreateNodeFromEid(eid); - if (eidNode != null) - { - if (globalContext.WithUiData) - eidNode.FallbackName = "EID"; - Nodes.Add(eidNode); - } + var genericContext = globalContext.CreateContext(model); for (var i = 0; i < model->SlotCount; ++i) { - var slotContext = globalContext.CreateContext( - model, - (uint)i, - i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown, - i < equipment.Length ? equipment[i] : default - ); + var slotContext = i < equipment.Length + ? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i]) + : globalContext.CreateContext(model, (uint)i); var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); @@ -94,7 +83,7 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); if (mdlNode != null) { if (globalContext.WithUiData) @@ -103,77 +92,68 @@ public class ResourceTree } } - AddSkeleton(Nodes, genericContext, model->Skeleton); + AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton); - AddSubObjects(globalContext, model); + AddWeapons(globalContext, model); if (human != null) AddHumanResources(globalContext, human); } - private unsafe void AddSubObjects(GlobalResolveContext globalContext, CharacterBase* model) + private unsafe void AddWeapons(GlobalResolveContext globalContext, CharacterBase* model) { - var subObjectIndex = 0; - var weaponIndex = 0; - var subObjectNodes = new List(); + var weaponIndex = 0; + var weaponNodes = new List(); foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; var subObject = (CharacterBase*)baseSubObject; - var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; - var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) + continue; + var weapon = (Weapon*)subObject; + // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var slot = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown; - var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default; + var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; + var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown); + var weaponType = weapon->SecondaryId; - var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment); - - var eid = (ResourceHandle*)subObject->EID; - var eidNode = genericContext.CreateNodeFromEid(eid); - if (eidNode != null) - { - if (globalContext.WithUiData) - eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID"; - Nodes.Add(eidNode); - } + var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); for (var i = 0; i < subObject->SlotCount; ++i) { - var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment); + var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) - imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; - subObjectNodes.Add(imcNode); + imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}"; + weaponNodes.Add(imcNode); } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); if (mdlNode != null) { if (globalContext.WithUiData) - mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; - subObjectNodes.Add(mdlNode); + mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; + weaponNodes.Add(mdlNode); } } - AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, "); - ++subObjectIndex; - if (weapon != null) - ++weaponIndex; + ++weaponIndex; } - Nodes.InsertRange(0, subObjectNodes); + Nodes.InsertRange(0, weaponNodes); } private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var genericContext = globalContext.CreateContext(&human->CharacterBase, 0xFFFFFFFFu, EquipSlot.Unknown, default); + var genericContext = globalContext.CreateContext(&human->CharacterBase); var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId != 0 @@ -208,8 +188,16 @@ public class ResourceTree } } - private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "") { + var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); + if (eidNode != null) + { + if (context.Global.WithUiData) + eidNode.FallbackName = $"{prefix}EID"; + Nodes.Add(eidNode); + } + if (skeleton == null) return; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 6353d5b5..0e3a92e2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,5 +1,4 @@ using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -18,9 +17,10 @@ public class ResourceTreeFactory private readonly IdentifierService _identifier; private readonly Configuration _config; private readonly ActorService _actors; + private readonly PathState _pathState; public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier, - Configuration config, ActorService actors) + Configuration config, ActorService actors, PathState pathState) { _gameData = gameData; _objects = objects; @@ -28,6 +28,7 @@ public class ResourceTreeFactory _identifier = identifier; _config = config; _actors = actors; + _pathState = pathState; } private TreeBuildCache CreateTreeBuildCache() @@ -87,13 +88,17 @@ public class ResourceTreeFactory var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); - var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUiData) != 0); - tree.LoadResources(globalContext); + var globalContext = new GlobalResolveContext(_identifier.AwaitedService, collectionResolveData.ModCollection, + cache, (flags & Flags.WithUiData) != 0); + using (var _ = _pathState.EnterInternalResolve()) + { + tree.LoadResources(globalContext); + } tree.FlatNodes.UnionWith(globalContext.Nodes.Values); tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node)); - ResolveGamePaths(tree, collectionResolveData.ModCollection); + // This is currently unneeded as we can resolve all paths by querying the draw object: + // ResolveGamePaths(tree, collectionResolveData.ModCollection); if (globalContext.WithUiData) ResolveUiData(tree); FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null); @@ -128,23 +133,15 @@ public class ResourceTreeFactory if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) continue; - IReadOnlyCollection resolvedList = resolvedSet; - if (resolvedList.Count > 1) - { - var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList); - if (filteredList.Count > 0) - resolvedList = filteredList; - } - - if (resolvedList.Count != 1) + if (resolvedSet.Count != 1) { Penumbra.Log.Debug( - $"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); - foreach (var gamePath in resolvedList) + $"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); + foreach (var gamePath in resolvedSet) Penumbra.Log.Debug($"Game path: {gamePath}"); } - node.PossibleGamePaths = resolvedList.ToArray(); + node.PossibleGamePaths = resolvedSet.ToArray(); } else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) { diff --git a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs new file mode 100644 index 00000000..008cd59a --- /dev/null +++ b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Structs; + +// TODO submit this to ClientStructs +public class ModelResourceHandleUtility +{ + public ModelResourceHandleUtility(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); + + [Signature("E8 ?? ?? ?? ?? 44 8B CD 48 89 44 24")] + private static nint _getMaterialFileNameBySlot = nint.Zero; + + public static unsafe byte* GetMaterialFileNameBySlot(ModelResourceHandle* handle, uint slot) + => ((delegate* unmanaged)_getMaterialFileNameBySlot)(handle, slot); +} diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 94bc2428..e3c31a42 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -65,6 +65,13 @@ public unsafe class ImcFile : MetaBaseFile return ptr == null ? new ImcEntry() : *ptr; } + public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists) + { + var ptr = VariantPtr(Data, partIdx, variantIdx); + exists = ptr != null; + return exists ? *ptr : new ImcEntry(); + } + public static int PartIndex(EquipSlot slot) => slot switch { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index df470d63..d7daaf70 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -76,6 +76,7 @@ public class Penumbra : IDalamudPlugin _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 6a522ca2..2c4f385d 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -90,7 +90,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() From 50a7015bc5a89d280c86ea4f3a73c683ecbc3128 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Nov 2023 13:58:14 +0100 Subject: [PATCH 1265/2451] Update BNPC Data. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 04ddadb4..545aab1f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 04ddadb44600a382e26661e1db08fd16c3b671d8 +Subproject commit 545aab1f007158a5d53bc6a7d73b6b2992deb9b3 From 5a64eadb5c2c5418f715027804a6135222e628d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Nov 2023 13:59:59 +0100 Subject: [PATCH 1266/2451] Update Stain Data. --- OtterGui | 2 +- Penumbra/Services/StainService.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index a4f9b285..b09bbcc2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a4f9b285c82f84ff0841695c0787dbba93afc59b +Subproject commit b09bbcc276363bc994d90b641871e6280898b6e5 diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index bbbfcc71..4bec85db 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -26,7 +26,8 @@ public class StainService : IDisposable using var t = timer.Measure(StartTimeType.Stains); StainData = new StainData(pluginInterface, dataManager, dataManager.Language, dalamudLog); StainCombo = new FilterComboColors(140, - StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))), Penumbra.Log); + () => StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); From da880bd76cceb503cae0d997744a0ba5f57af915 Mon Sep 17 00:00:00 2001 From: HoloWise Date: Thu, 9 Nov 2023 21:10:22 +0100 Subject: [PATCH 1267/2451] Fix broken tooltips --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 821f4454..20550a15 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -27,6 +27,7 @@ public partial class ModEditWindow private const string GenderTooltip = "Gender"; private const string ObjectTypeTooltip = "Object Type"; private const string SecondaryIdTooltip = "Secondary ID"; + private const string PrimaryIDTooltip = "Primary ID"; private const string VariantIdTooltip = "Variant ID"; private const string EstTypeTooltip = "EST Type"; private const string RacialTribeTooltip = "Racial Tribe"; @@ -415,6 +416,8 @@ public partial class ModEditWindow _new.Entry).Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); + ImGuiUtil.HoverTooltip(VariantIdTooltip); + ImGui.TableNextColumn(); if (_new.ObjectType is ObjectType.DemiHuman) { @@ -431,7 +434,6 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); } - ImGuiUtil.HoverTooltip(VariantIdTooltip); // Values using var disabled = ImRaii.Disabled(); @@ -475,7 +477,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip("Primary ID"); + ImGuiUtil.HoverTooltip(PrimaryIDTooltip); ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); @@ -498,7 +500,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); if (meta.ObjectType is ObjectType.DemiHuman) + { ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } // Values using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, From b5377a961f9a4c61d794715cab0a05828cf8d51c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Nov 2023 13:18:07 +0000 Subject: [PATCH 1268/2451] [CI] Updating repo.json for 0.8.1.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index bc8d02d7..061eb283 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.7", - "TestingAssemblyVersion": "0.8.1.7", + "AssemblyVersion": "0.8.1.8", + "TestingAssemblyVersion": "0.8.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 51dba221c44a6ecc0717df4dd774dd72d301d9c2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Nov 2023 21:09:49 +0100 Subject: [PATCH 1269/2451] Add option to open window at game start instead of coupling it with debug mode --- Penumbra/Configuration.cs | 1 + Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index d221b4a2..a1cf6a72 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -35,6 +35,7 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; + public bool OpenWindowAtStart { get; set; } = false; public bool HideUiInGPose { get; set; } = false; public bool HideUiInCutscenes { get; set; } = true; public bool HideUiWhenUiHidden { get; set; } = false; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 0f209686..804a1d01 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -32,7 +32,7 @@ public sealed class ConfigWindow : Window RespectCloseHotkey = true; tutorial.UpdateTutorialStep(); - IsOpen = _config.DebugMode; + IsOpen = _config.OpenWindowAtStart; } public void Setup(Penumbra penumbra, ConfigTabBar configTabs) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ae3a939c..6274f209 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -345,22 +345,25 @@ public class SettingsTab : ITab /// Draw the window hiding state checkboxes. private void DrawHidingSettings() { + Checkbox("Open Config Window at Game Start", "Whether the Penumbra main window should be open or closed after launching the game.", + _config.OpenWindowAtStart, v => _config.OpenWindowAtStart = v); + Checkbox("Hide Config Window when UI is Hidden", - "Hide the penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, + "Hide the Penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, v => { _config.HideUiWhenUiHidden = v; _dalamud.UiBuilder.DisableUserUiHide = !v; }); Checkbox("Hide Config Window when in Cutscenes", - "Hide the penumbra main window when you are currently watching a cutscene.", _config.HideUiInCutscenes, + "Hide the Penumbra main window when you are currently watching a cutscene.", _config.HideUiInCutscenes, v => { _config.HideUiInCutscenes = v; _dalamud.UiBuilder.DisableCutsceneUiHide = !v; }); Checkbox("Hide Config Window when in GPose", - "Hide the penumbra main window when you are currently in GPose mode.", _config.HideUiInGPose, + "Hide the Penumbra main window when you are currently in GPose mode.", _config.HideUiInGPose, v => { _config.HideUiInGPose = v; From b2bf6eb0f7615f54ac5f19b76b35555918987c76 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 13 Nov 2023 07:44:48 +0100 Subject: [PATCH 1270/2451] ResourceTree: Handle weapon MTRL special cases --- .../ResolveContext.PathResolution.cs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index bcc957df..d7d80c21 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -13,7 +13,6 @@ namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { - private Utf8GamePath ResolveModelPath() { // Correctness: @@ -83,27 +82,57 @@ internal partial record ResolveContext { ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.Weapon => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imcPath, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) { - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var modelPathSpan = modelPath.Path.Span; - var baseDirectory = modelPathSpan[..modelPathSpan.IndexOf("/model/"u8)]; - - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imcPath); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - baseDirectory.CopyTo(pathBuffer); - "/material/v"u8.CopyTo(pathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(pathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - pathBuffer[baseDirectory.Length + 15] = (byte)'/'; - fileName.CopyTo(pathBuffer[(baseDirectory.Length + 16)..]); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer[..(baseDirectory.Length + 16 + fileName.Length)], out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + { + var setIdHigh = Equipment.Set.Id / 100; + // All MCH (20??) weapons' materials C are one and the same + if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') + return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; + + // MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand + if (setIdHigh is 3 or 16 or 18 or 26) + { + var setIdLow = Equipment.Set.Id % 100; + if (setIdLow > 50) + { + var variant = ResolveMaterialVariant(imcPath); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + + var mirroredSetId = (ushort)(Equipment.Set.Id - 50); + + Span mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + + Span pathBuffer = stackalloc byte[260]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + + var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); + if (weaponPosition >= 0) + WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); + + return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + } + } + + return ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName); } private byte ResolveMaterialVariant(Utf8GamePath imcPath) @@ -119,6 +148,23 @@ internal partial record ResolveContext return entry.MaterialId; } + private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, ReadOnlySpan mtrlFileName) + { + var modelPosition = modelPath.IndexOf("/model/"u8); + if (modelPosition < 0) + return Span.Empty; + + var baseDirectory = modelPath[..modelPosition]; + + baseDirectory.CopyTo(materialPathBuffer); + "/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); + WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); + materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; + mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); + + return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; + } + private static void WriteZeroPaddedNumber(Span destination, ushort number) { for (var i = destination.Length; i-- > 0;) From 60551c87393e4a2b988852688040b7a4ba36f441 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 14 Nov 2023 20:38:21 +0100 Subject: [PATCH 1271/2451] ResourceTree: Are we fast yet? --- Penumbra/Collections/Cache/MetaCache.cs | 5 --- .../ResolveContext.PathResolution.cs | 31 +++++++++++-------- .../Interop/ResourceTree/ResolveContext.cs | 8 ++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 +-- Penumbra/Meta/Files/ImcFile.cs | 10 +++++- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 0da11022..d5acf249 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -187,11 +187,6 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.GetImcFile(path, out file); - public ImcEntry GetImcEntry(Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists) - => GetImcFile(path, out var file) - ? file.GetEntry(Meta.Files.ImcFile.PartIndex(slot), variantIdx, out exists) - : Meta.Files.ImcFile.GetDefault(_manager, path, slot, variantIdx, out exists); - internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId) { var eqdpFile = _eqdpCache.EqdpFile(race, accessory); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index d7d80c21..f4081de1 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,8 +1,10 @@ using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; @@ -74,22 +76,22 @@ internal partial record ResolveContext return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } - private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { // Safety: // Resolving a material path through the game's code can dereference null pointers for equipment materials. return ModelType switch { - ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } - private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imc); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -98,7 +100,7 @@ internal partial record ResolveContext return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } - private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { var setIdHigh = Equipment.Set.Id / 100; // All MCH (20??) weapons' materials C are one and the same @@ -111,7 +113,7 @@ internal partial record ResolveContext var setIdLow = Equipment.Set.Id % 100; if (setIdLow > 50) { - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imc); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var mirroredSetId = (ushort)(Equipment.Set.Id - 50); @@ -132,16 +134,19 @@ internal partial record ResolveContext } } - return ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName); + return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private byte ResolveMaterialVariant(Utf8GamePath imcPath) + private unsafe byte ResolveMaterialVariant(ResourceHandle* imc) { - var metaCache = Global.Collection.MetaCache; - if (metaCache == null) + var imcFileData = imc->GetDataSpan(); + if (imcFileData.IsEmpty) + { + Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span"); return Equipment.Variant.Id; + } - var entry = metaCache.GetImcEntry(imcPath, Slot, Equipment.Variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot, Equipment.Variant, out var exists); if (!exists) return Equipment.Variant.Id; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index f34a6ae2..73abcb4d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -148,7 +148,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerTexture, &tex->ResourceHandle, path); } - public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath) + public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) { if (mdl == null || mdl->ModelResourceHandle == null) return null; @@ -169,7 +169,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, Pointer= 0 && i < array.Length ? array[i] : null; } - internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) + internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true) { if (handle == null) return ByteString.Empty; @@ -367,7 +367,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerModels[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); if (mdlNode != null) { if (globalContext.WithUiData) @@ -135,7 +135,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); if (mdlNode != null) { if (globalContext.WithUiData) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index e3c31a42..68d3f5b3 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -168,11 +168,19 @@ public unsafe class ImcFile : MetaBaseFile if (file == null) throw new Exception(); - fixed (byte* ptr = file.Data) + return GetEntry(file.Data, slot, variantIdx, out exists); + } + + public static ImcEntry GetEntry(ReadOnlySpan imcFileData, EquipSlot slot, Variant variantIdx, out bool exists) + { + fixed (byte* ptr = imcFileData) { var entry = VariantPtr(ptr, PartIndex(slot), variantIdx); if (entry == null) + { + exists = false; return new ImcEntry(); + } exists = true; return *entry; From cb43fed9d3785674ef016f4f7b3cbf968ef89dd0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 14 Nov 2023 21:13:54 +0100 Subject: [PATCH 1272/2451] ResourceTree: Handle monster MTRL --- .../ResolveContext.PathResolution.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index f4081de1..1c9dfaa1 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -78,20 +78,21 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - // Safety: - // Resolving a material path through the game's code can dereference null pointers for equipment materials. + // Safety and correctness: + // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imc); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -113,7 +114,7 @@ internal partial record ResolveContext var setIdLow = Equipment.Set.Id % 100; if (setIdLow > 50) { - var variant = ResolveMaterialVariant(imc); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var mirroredSetId = (ushort)(Equipment.Set.Id - 50); @@ -137,18 +138,30 @@ internal partial record ResolveContext return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private unsafe byte ResolveMaterialVariant(ResourceHandle* imc) + private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) + { + // TODO: Submit this (Monster->Variant) to ClientStructs + var variant = ResolveMaterialVariant(imc, ((byte*)CharacterBase.Value)[0x8F4]); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + + Span pathBuffer = stackalloc byte[260]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); + + return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + } + + private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant) { var imcFileData = imc->GetDataSpan(); if (imcFileData.IsEmpty) { Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span"); - return Equipment.Variant.Id; + return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot, Equipment.Variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists); if (!exists) - return Equipment.Variant.Id; + return variant.Id; return entry.MaterialId; } From 13044763cbfcd8d899a1f05a67c34c41959370cc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 15 Nov 2023 01:10:35 +0100 Subject: [PATCH 1273/2451] Redraw player ornament, allow redrawing by index --- Penumbra/Interop/Services/RedrawService.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index ec858290..ee09ea6e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -154,11 +154,11 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { - *ActorDrawState(mount) |= DrawState.Invisibility; + *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; if (gPose) - DisableDraw(mount); + DisableDraw(mountOrOrnament); } } @@ -173,11 +173,11 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { - *ActorDrawState(mount) &= ~DrawState.Invisibility; + *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; if (gPose) - EnableDraw(mount); + EnableDraw(mountOrOrnament); } GameObjectRedrawn?.Invoke(actor!.Address, tableIndex); @@ -323,6 +323,12 @@ public sealed unsafe partial class RedrawService : IDisposable "mouseover" => (_targets.MouseOverTarget, true), _ => (null, false), }; + if (!ret && lowerName.Length > 1 && lowerName[0] == '#' && ushort.TryParse(lowerName[1..], out var objectIndex)) + { + ret = true; + actor = _objects[objectIndex]; + } + return ret; } From ab902cbe9e784a80f7d0cf5cd77826333022c9fc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 15 Nov 2023 16:09:48 +0100 Subject: [PATCH 1274/2451] Fix issue with ring identification --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 545aab1f..f39a716a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 545aab1f007158a5d53bc6a7d73b6b2992deb9b3 +Subproject commit f39a716ad4f908c301d497728ede047ee6bd61c0 From d026ca888fc41511770ecaab255c3587c2247865 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 15 Nov 2023 16:10:15 +0100 Subject: [PATCH 1275/2451] Add Furniture Redrawing despite crash. --- Penumbra/Interop/Services/RedrawService.cs | 58 +++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index ee09ea6e..ddabacd0 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -4,6 +4,8 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using FFXIVClientStructs.Interop; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.GameData; @@ -101,6 +103,8 @@ public unsafe partial class RedrawService public sealed unsafe partial class RedrawService : IDisposable { + private const int FurnitureIdx = 1337; + private readonly IFramework _framework; private readonly IObjectTable _objects; private readonly ITargetManager _targets; @@ -231,6 +235,18 @@ public sealed unsafe partial class RedrawService : IDisposable for (var i = 0; i < _queue.Count; ++i) { var idx = _queue[i]; + if (idx == FurnitureIdx) + { + EnableFurniture(); + continue; + } + + if (idx == ~FurnitureIdx) + { + DisableFurniture(); + continue; + } + if (FindCorrectActor(idx < 0 ? ~idx : idx, out var obj)) _afterGPoseQueue.Add(idx < 0 ? idx : ~idx); @@ -340,8 +356,10 @@ public sealed unsafe partial class RedrawService : IDisposable public void RedrawObject(string name, RedrawType settings) { - var lowerName = name.ToLowerInvariant(); - if (GetName(lowerName, out var target)) + var lowerName = name.ToLowerInvariant().Trim(); + if (lowerName == "furniture") + _queue.Add(~FurnitureIdx); + else if (GetName(lowerName, out var target)) RedrawObject(target, settings); else foreach (var actor in _objects.Where(a => a.Name.ToString().ToLowerInvariant() == lowerName)) @@ -353,4 +371,40 @@ public sealed unsafe partial class RedrawService : IDisposable foreach (var actor in _objects) RedrawObject(actor, settings); } + + private void DisableFurniture() + { + var housingManager = HousingManager.Instance(); + if (housingManager == null) + return; + var currentTerritory = housingManager->CurrentTerritory; + if (currentTerritory == null) + return; + + foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) + { + var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; + if (gameObject == null) + continue; + gameObject->DisableDraw(); + } + } + + private void EnableFurniture() + { + var housingManager = HousingManager.Instance(); + if (housingManager == null) + return; + var currentTerritory = housingManager->CurrentTerritory; + if (currentTerritory == null) + return; + + foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) + { + var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; + if (gameObject == null) + continue; + gameObject->EnableDraw(); + } + } } From aee942468ebb681e2fd153ffe2333d3ece597cb0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 15 Nov 2023 18:34:24 +0100 Subject: [PATCH 1276/2451] Only allow redrawing furniture inside. --- Penumbra/Interop/Services/RedrawService.cs | 26 ++-------------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index ddabacd0..49d688af 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -235,12 +235,6 @@ public sealed unsafe partial class RedrawService : IDisposable for (var i = 0; i < _queue.Count; ++i) { var idx = _queue[i]; - if (idx == FurnitureIdx) - { - EnableFurniture(); - continue; - } - if (idx == ~FurnitureIdx) { DisableFurniture(); @@ -380,6 +374,8 @@ public sealed unsafe partial class RedrawService : IDisposable var currentTerritory = housingManager->CurrentTerritory; if (currentTerritory == null) return; + if (!housingManager->IsInside()) + return; foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) { @@ -389,22 +385,4 @@ public sealed unsafe partial class RedrawService : IDisposable gameObject->DisableDraw(); } } - - private void EnableFurniture() - { - var housingManager = HousingManager.Instance(); - if (housingManager == null) - return; - var currentTerritory = housingManager->CurrentTerritory; - if (currentTerritory == null) - return; - - foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) - { - var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; - if (gameObject == null) - continue; - gameObject->EnableDraw(); - } - } } From acfd5d24848988ea0232bee49c49b9bec7fb9cc0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 15 Nov 2023 20:04:30 +0100 Subject: [PATCH 1277/2451] Update Penumbra.GameData Also remove a check that, if it was still valid, would always be false with the new changes. --- Penumbra.GameData | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 20e8002b..a807e426 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 20e8002bfe701e54b05721c3b7b80c495a692adc +Subproject commit a807e426eed5b26a5d1043d5c47c98b28c93982e diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 0048dc8c..c1e0bb80 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -102,8 +102,6 @@ public unsafe class MetaState : IDisposable public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) { var races = race.Dependencies(); - if (races.Length == 0) - return DisposableContainer.Empty; var equipmentEnumerable = equipment ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false)) From 63ca0445867656980b8280282ac1cb0f14fa61fe Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 16 Nov 2023 05:48:30 +0100 Subject: [PATCH 1278/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2e3237c9..c5c3b027 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2e3237c9018f4531f8ecdc1783b4f00e5eba7f34 +Subproject commit c5c3b0272ee47462c20b692787f9748571eb01dc From 8caba8c339375849e78a6b406a97379826ab6d84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Nov 2023 21:16:38 +0100 Subject: [PATCH 1279/2451] Update GameData again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c5c3b027..ffdb966f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c5c3b0272ee47462c20b692787f9748571eb01dc +Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f From 7e8cd719fd73408501211e590ddf080cda94e6c7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 17 Nov 2023 12:11:06 +0000 Subject: [PATCH 1280/2451] [CI] Updating repo.json for testing_0.8.1.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 061eb283..e804fb1e 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.1.8", - "TestingAssemblyVersion": "0.8.1.8", + "TestingAssemblyVersion": "0.8.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.1.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c88f1a7b1cefd8985524f0a39210fcbabb0b7aed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 15:25:03 +0100 Subject: [PATCH 1281/2451] Add color preview for dye template selection. --- Penumbra/Services/StainService.cs | 56 +++++++++++++++++-- .../ModEditWindow.Materials.MtrlTab.cs | 1 - 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 4bec85db..58d9af21 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,5 +1,8 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using ImGuiNET; using OtterGui.Widgets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; @@ -11,9 +14,54 @@ public class StainService : IDisposable { public sealed class StainTemplateCombo : FilterComboCache { - public StainTemplateCombo(IEnumerable items) - : base(items, Penumbra.Log) - { } + private readonly StmFile _stmFile; + private readonly FilterComboColors _stainCombo; + + private float _rightOffset; + + public StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) + : base(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) + { + _stainCombo = stainCombo; + _stmFile = stmFile; + } + + protected override float GetFilterWidth() + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; + if (_stainCombo.CurrentSelection.Key == 0) + return baseSize; + return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; + } + + protected override string ToString(ushort obj) + => $"{obj,4}"; + + protected override void DrawList(float width, float itemHeight) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }); + base.DrawList(width, itemHeight); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var ret = base.DrawSelectable(globalIdx, selected); + var selection = _stainCombo.CurrentSelection.Key; + if (selection == 0 || !_stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) + return ret; + + ImGui.SameLine(); + var frame = new Vector2(ImGui.GetTextLineHeight()); + ImGui.ColorButton("D", new Vector4(colors.Diffuse, 1), 0, frame); + ImGui.SameLine(); + ImGui.ColorButton("E", new Vector4(colors.Emissive, 1), 0, frame); + ImGui.SameLine(); + ImGui.ColorButton("S", new Vector4(colors.Specular, 1), 0, frame); + return ret; + } } public readonly StainData StainData; @@ -29,7 +77,7 @@ public class StainService : IDisposable () => StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); StmFile = new StmFile(dataManager); - TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0)); + TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 20efe757..954e9b0f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,6 +1,5 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; From 2fd8c981472f1e584f1a4126d17dc04bfa163752 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 15:25:37 +0100 Subject: [PATCH 1282/2451] Add furniture to redraw bar and help, improve redraw bar slightly. --- Penumbra/UI/Tabs/ModsTab.cs | 57 ++++++++++++++++++++++++++-------- Penumbra/UI/TutorialService.cs | 1 + 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 1e675036..7b30f7fc 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -1,9 +1,11 @@ +using Dalamud.Game.ClientState.Objects; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; using Dalamud.Interface; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Housing; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop.Services; @@ -26,10 +28,12 @@ public class ModsTab : ITab private readonly Configuration _config; private readonly IClientState _clientState; private readonly CollectionSelectHeader _collectionHeader; + private readonly ITargetManager _targets; + private readonly IObjectTable _objectTable; public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, TutorialService tutorial, RedrawService redrawService, Configuration config, IClientState clientState, - CollectionSelectHeader collectionHeader) + CollectionSelectHeader collectionHeader, ITargetManager targets, IObjectTable objectTable) { _modManager = modManager; _activeCollections = collectionManager.Active; @@ -40,6 +44,8 @@ public class ModsTab : ITab _config = config; _clientState = clientState; _collectionHeader = collectionHeader; + _targets = targets; + _objectTable = objectTable; } public bool IsVisible @@ -133,29 +139,54 @@ public class ModsTab : ITab if (hovered) ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); - void DrawButton(Vector2 size, string label, string lower) + void DrawButton(Vector2 size, string label, string lower, string additionalTooltip) { - if (ImGui.Button(label, size)) + using (var disabled = ImRaii.Disabled(additionalTooltip.Length > 0)) { - if (lower.Length > 0) - _redrawService.RedrawObject(lower, RedrawType.Redraw); - else - _redrawService.RedrawAll(RedrawType.Redraw); + if (ImGui.Button(label, size)) + { + if (lower.Length > 0) + _redrawService.RedrawObject(lower, RedrawType.Redraw); + else + _redrawService.RedrawAll(RedrawType.Redraw); + } } - ImGuiUtil.HoverTooltip(lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'."); + ImGuiUtil.HoverTooltip(lower.Length > 0 + ? $"Execute '/penumbra redraw {lower}'.{additionalTooltip}" + : $"Execute '/penumbra redraw'.{additionalTooltip}", ImGuiHoveredFlags.AllowWhenDisabled); } using var id = ImRaii.PushId("Redraw"); using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); ImGui.SameLine(); - var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; - DrawButton(buttonWidth, "All", string.Empty); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; + var tt = _objectTable.GetObjectAddress(0) == nint.Zero + ? "\nCan only be used when you are logged in and your character is available." + : string.Empty; + DrawButton(buttonWidth, "All", string.Empty, tt); ImGui.SameLine(); - DrawButton(buttonWidth, "Self", "self"); + DrawButton(buttonWidth, "Self", "self", tt); ImGui.SameLine(); - DrawButton(buttonWidth, "Target", "target"); + + tt = _targets.Target == null && _targets.GPoseTarget == null + ? "\nCan only be used when you have a target." + : string.Empty; + DrawButton(buttonWidth, "Target", "target", tt); ImGui.SameLine(); - DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus"); + + tt = _targets.FocusTarget == null + ? "\nCan only be used when you have a focus target." + : string.Empty; + DrawButton(buttonWidth, "Focus", "focus", tt); + ImGui.SameLine(); + + tt = !IsIndoors() + ? "\nCan currently only be used for indoor furniture." + : string.Empty; + DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Furniture", "furniture", tt); } + + private static unsafe bool IsIndoors() + => HousingManager.Instance()->IsInside(); } diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 1a589c28..86c4dd46 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -52,6 +52,7 @@ public class TutorialService + " - 'target' or '': your target\n" + " - 'focus' or ': your focus target\n" + " - 'mouseover' or '': the actor you are currently hovering over\n" + + " - 'furniture': most indoor furniture, does not currently work outdoors\n" + " - any specific actor name to redraw all actors of that exactly matching name."; private readonly Configuration _config; From dc1c8f42c0d8a01aa9f6cc90ae63cde78369bbdd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 15:46:54 +0100 Subject: [PATCH 1283/2451] Improve Color Preview. --- Penumbra/Services/StainService.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 58d9af21..63f6d05e 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -17,8 +17,6 @@ public class StainService : IDisposable private readonly StmFile _stmFile; private readonly FilterComboColors _stainCombo; - private float _rightOffset; - public StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) : base(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) { @@ -28,7 +26,6 @@ public class StainService : IDisposable protected override float GetFilterWidth() { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; if (_stainCombo.CurrentSelection.Key == 0) return baseSize; @@ -38,12 +35,21 @@ public class StainService : IDisposable protected override string ToString(ushort obj) => $"{obj,4}"; - protected override void DrawList(float width, float itemHeight) + protected override void DrawFilter(int currentSelected, float width) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }); - base.DrawList(width, itemHeight); + using var font = ImRaii.PushFont(UiBuilder.DefaultFont); + base.DrawFilter(currentSelected, width); + } + + public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, + ImGuiComboFlags flags = ImGuiComboFlags.None) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f)) + .Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }); + var spaceSize = ImGui.CalcTextSize(" ").X; + var spaces = (int) (previewWidth / spaceSize) - 1; + return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags); } protected override bool DrawSelectable(int globalIdx, bool selected) From ea65296ab771462fb4c6acc796b3f18ac7b4aeea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 16:51:09 +0100 Subject: [PATCH 1284/2451] Add EphemeralConfig. --- Penumbra/CommandHandler.cs | 8 +- Penumbra/Configuration.cs | 66 +++++--------- Penumbra/EphemeralConfig.cs | 86 +++++++++++++++++++ Penumbra/Penumbra.cs | 2 +- Penumbra/Services/BackupService.cs | 4 + Penumbra/Services/ConfigMigrationService.cs | 44 +++++++++- Penumbra/Services/FilenameService.cs | 10 ++- Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/ChangedItemDrawer.cs | 20 ++--- Penumbra/UI/Changelog.cs | 16 +++- Penumbra/UI/ConfigWindow.cs | 10 +-- .../UI/ResourceWatcher/ResourceWatcher.cs | 52 +++++------ .../ResourceWatcher/ResourceWatcherTable.cs | 14 +-- Penumbra/UI/Tabs/CollectionsTab.cs | 4 +- Penumbra/UI/Tabs/DebugTab.cs | 16 ++-- Penumbra/UI/Tabs/SettingsTab.cs | 28 ++++-- Penumbra/UI/TutorialService.cs | 6 +- 17 files changed, 264 insertions(+), 125 deletions(-) create mode 100644 Penumbra/EphemeralConfig.cs diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index b6c675a2..c151e7e4 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -184,8 +184,8 @@ public class CommandHandler : IDisposable private bool SetUiLockState(string arguments) { - var value = ParseTrueFalseToggle(arguments) ?? !_config.FixMainWindow; - if (value == _config.FixMainWindow) + var value = ParseTrueFalseToggle(arguments) ?? !_config.Ephemeral.FixMainWindow; + if (value == _config.Ephemeral.FixMainWindow) return false; if (value) @@ -199,8 +199,8 @@ public class CommandHandler : IDisposable _configWindow.Flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); } - _config.FixMainWindow = value; - _config.Save(); + _config.Ephemeral.FixMainWindow = value; + _config.Ephemeral.Save(); return true; } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a1cf6a72..8f21ee52 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -5,17 +5,13 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Enums; using Penumbra.Import.Structs; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.UI.ResourceWatcher; -using Penumbra.UI.Tabs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; @@ -26,9 +22,11 @@ public class Configuration : IPluginConfiguration, ISavable [JsonIgnore] private readonly SaveService _saveService; + [JsonIgnore] + public readonly EphemeralConfig Ephemeral; + public int Version { get; set; } = Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; public bool EnableMods { get; set; } = true; @@ -53,52 +51,33 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; - public bool DebugSeparateWindow = false; - public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); + public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); #if DEBUG public bool DebugMode { get; set; } = true; #else public bool DebugMode { get; set; } = false; #endif - - public int TutorialStep { get; set; } = 0; - - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; - - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - + public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; [JsonConverter(typeof(SortModeConverter))] [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; - public bool OpenFoldersByDefault { get; set; } = false; - public int SingleGroupRadioMax { get; set; } = 2; - public string DefaultImportFolder { get; set; } = string.Empty; - public string QuickMoveFolder1 { get; set; } = string.Empty; - public string QuickMoveFolder2 { get; set; } = string.Empty; - public string QuickMoveFolder3 { get; set; } = string.Empty; - public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); - public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - - public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; - - public bool PrintSuccessfulCommandsToChat { get; set; } = true; - public bool FixMainWindow { get; set; } = false; - public bool AutoDeduplicateOnImport { get; set; } = true; - public bool UseFileSystemCompression { get; set; } = true; - public bool EnableHttpApi { get; set; } = true; + public bool ScaleModSelector { get; set; } = false; + public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; + public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool OpenFoldersByDefault { get; set; } = false; + public int SingleGroupRadioMax { get; set; } = 2; + public string DefaultImportFolder { get; set; } = string.Empty; + public string QuickMoveFolder1 { get; set; } = string.Empty; + public string QuickMoveFolder2 { get; set; } = string.Empty; + public string QuickMoveFolder3 { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public bool PrintSuccessfulCommandsToChat { get; set; } = true; + public bool AutoDeduplicateOnImport { get; set; } = true; + public bool UseFileSystemCompression { get; set; } = true; + public bool EnableHttpApi { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; @@ -112,9 +91,10 @@ public class Configuration : IPluginConfiguration, ISavable /// Load the current configuration. /// Includes adding new colors and migrating from old versions. /// - public Configuration(CharacterUtility utility, ConfigMigrationService migrator, SaveService saveService) + public Configuration(CharacterUtility utility, ConfigMigrationService migrator, SaveService saveService, EphemeralConfig ephemeral) { _saveService = saveService; + Ephemeral = ephemeral; Load(utility, migrator); } @@ -148,12 +128,12 @@ public class Configuration : IPluginConfiguration, ISavable /// Save the current configuration. public void Save() - => _saveService.DelaySave(this); + => _saveService.QueueSave(this); /// Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 7; + public const int CurrentVersion = 8; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs new file mode 100644 index 00000000..8003629d --- /dev/null +++ b/Penumbra/EphemeralConfig.cs @@ -0,0 +1,86 @@ +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using OtterGui.Classes; +using Penumbra.Api.Enums; +using Penumbra.Enums; +using Penumbra.Interop.Services; +using Penumbra.Services; +using Penumbra.UI; +using Penumbra.UI.ResourceWatcher; +using Penumbra.UI.Tabs; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; + +namespace Penumbra; + +public class EphemeralConfig : ISavable +{ + [JsonIgnore] + private readonly SaveService _saveService; + + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; + public bool FixMainWindow { get; set; } = false; + + /// + /// Load the current configuration. + /// Includes adding new colors and migrating from old versions. + /// + public EphemeralConfig(SaveService saveService) + { + _saveService = saveService; + Load(); + } + + private void Load() + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Penumbra.Log.Error( + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (File.Exists(_saveService.FileNames.EphemeralConfigFile)) + try + { + var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, + "Error reading ephemeral Configuration, reverting to default.", + "Error reading ephemeral Configuration", NotificationType.Error); + } + } + + /// Save the current configuration. + public void Save() + => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); + + + public string ToFilename(FilenameService fileNames) + => fileNames.EphemeralConfigFile; + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d7daaf70..ef8a1a05 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -201,7 +201,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); sb.Append( - $"> **`Logging: `** Log: {_config.EnableResourceLogging}, Watcher: {_config.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); + $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); sb.AppendLine("**Mods**"); sb.Append($"> **`Installed Mods: `** {_modManager.Count}\n"); diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index e623be3e..7b8ace29 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -14,6 +14,10 @@ public class BackupService Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); } + public static void CreatePermanentBackup(FilenameService fileNames) + => Backup.CreatePermanentBackup(Penumbra.Log, new DirectoryInfo(fileNames.ConfigDirectory), PenumbraFiles(fileNames), + "pre_ephemeral_config"); + // Collect all relevant files for penumbra configuration. private static IReadOnlyList PenumbraFiles(FilenameService fileNames) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index d896e526..03aedc57 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -1,14 +1,20 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui.Classes; using OtterGui.Filesystem; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Enums; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; +using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.ResourceWatcher; +using Penumbra.UI.Tabs; namespace Penumbra.Services; @@ -26,7 +32,7 @@ public class ConfigMigrationService public string CurrentCollection = ModCollection.DefaultCollectionName; public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; - public Dictionary CharacterCollections = new(); + public Dictionary CharacterCollections = []; public Dictionary ModSortOrder = new(); public bool InvertModListOrder; public bool SortFoldersFirst; @@ -71,9 +77,41 @@ public class ConfigMigrationService Version4To5(); Version5To6(); Version6To7(); + Version7To8(); AddColors(config, true); } + // Migrate to ephemeral config. + private void Version7To8() + { + if (_config.Version != 7) + return; + + _config.Version = 8; + _config.Ephemeral.Version = 8; + + _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; + _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; + _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; + _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging; + _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter; + _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher; + _config.Ephemeral.OnlyAddMatchingResources = + _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources; + _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject() + ?? _config.Ephemeral.ResourceWatcherResourceTypes; + _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject() + ?? _config.Ephemeral.ResourceWatcherResourceCategories; + _config.Ephemeral.ResourceWatcherRecordTypes = + _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; + _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; + _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; + _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() + ?? _config.Ephemeral.ChangedItemFilter; + _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; + _config.Ephemeral.Save(); + } + // Gendered special collections were added. private void Version6To7() { @@ -93,8 +131,8 @@ public class ConfigMigrationService if (_config.Version != 5) return; - if (_config.TutorialStep == 25) - _config.TutorialStep = 27; + if (_config.Ephemeral.TutorialStep == 25) + _config.Ephemeral.TutorialStep = 27; _config.Version = 6; } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index c7ed6061..e525c1f4 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -11,17 +11,19 @@ public class FilenameService public readonly string CollectionDirectory; public readonly string LocalDataDirectory; public readonly string ConfigFile; + public readonly string EphemeralConfigFile; public readonly string FilesystemFile; public readonly string ActiveCollectionsFile; public FilenameService(DalamudPluginInterface pi) { ConfigDirectory = pi.ConfigDirectory.FullName; - CollectionDirectory = Path.Combine(pi.GetPluginConfigDirectory(), "collections"); - LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); + CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); + LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); ConfigFile = pi.ConfigFile.FullName; - FilesystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json"); - ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); + ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json"); } /// Obtain the path of a collection file given its name. diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 2c4f385d..227f65d7 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -95,7 +95,8 @@ public static class ServiceManager private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddCollections(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 3c74aa20..8916d365 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -70,7 +70,7 @@ public class ChangedItemDrawer : IDisposable /// Check if a changed item should be drawn based on its category. public bool FilterChangedItem(string name, object? data, LowerString filter) - => (_config.ChangedItemFilter == AllFlags || _config.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) + => (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); /// Draw the icon corresponding to the category of a changed item. @@ -172,20 +172,20 @@ public class ChangedItemDrawer : IDisposable void DrawIcon(ChangedItemIcon type) { var icon = _icons[type]; - var flag = _config.ChangedItemFilter.HasFlag(type); + var flag = _config.Ephemeral.ChangedItemFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { - _config.ChangedItemFilter = flag ? _config.ChangedItemFilter & ~type : _config.ChangedItemFilter | type; - _config.Save(); + _config.Ephemeral.ChangedItemFilter = flag ? _config.Ephemeral.ChangedItemFilter & ~type : _config.Ephemeral.ChangedItemFilter | type; + _config.Ephemeral.Save(); } using var popup = ImRaii.ContextPopupItem(type.ToString()); if (popup) if (ImGui.MenuItem("Enable Only This")) { - _config.ChangedItemFilter = type; - _config.Save(); + _config.Ephemeral.ChangedItemFilter = type; + _config.Ephemeral.Save(); ImGui.CloseCurrentPopup(); } @@ -206,12 +206,12 @@ public class ChangedItemDrawer : IDisposable ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - _config.ChangedItemFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - _config.ChangedItemFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); + _config.Ephemeral.ChangedItemFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : + _config.Ephemeral.ChangedItemFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); if (ImGui.IsItemClicked()) { - _config.ChangedItemFilter = _config.ChangedItemFilter == AllFlags ? 0 : AllFlags; - _config.Save(); + _config.Ephemeral.ChangedItemFilter = _config.Ephemeral.ChangedItemFilter == AllFlags ? 0 : AllFlags; + _config.Ephemeral.Save(); } } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 7589e6ec..dac415d5 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -627,12 +627,20 @@ public class PenumbraChangelog #endregion private (int, ChangeLogDisplayType) ConfigData() - => (_config.LastSeenVersion, _config.ChangeLogDisplayType); + => (_config.Ephemeral.LastSeenVersion, _config.ChangeLogDisplayType); private void Save(int version, ChangeLogDisplayType type) { - _config.LastSeenVersion = version; - _config.ChangeLogDisplayType = type; - _config.Save(); + if (_config.Ephemeral.LastSeenVersion != version) + { + _config.Ephemeral.LastSeenVersion = version; + _config.Ephemeral.Save(); + } + + if (_config.ChangeLogDisplayType != type) + { + _config.ChangeLogDisplayType = type; + _config.Save(); + } } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 804a1d01..d52ebb99 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -39,7 +39,7 @@ public sealed class ConfigWindow : Window { _penumbra = penumbra; _configTabs = configTabs; - _configTabs.SelectTab = _config.SelectedTab; + _configTabs.SelectTab = _config.Ephemeral.SelectedTab; } public override bool DrawConditions() @@ -47,7 +47,7 @@ public sealed class ConfigWindow : Window public override void PreDraw() { - if (_config.FixMainWindow) + if (_config.Ephemeral.FixMainWindow) Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; else Flags &= ~(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); @@ -99,10 +99,10 @@ public sealed class ConfigWindow : Window else { var type = _configTabs.Draw(); - if (type != _config.SelectedTab) + if (type != _config.Ephemeral.SelectedTab) { - _config.SelectedTab = type; - _config.Save(); + _config.Ephemeral.SelectedTab = type; + _config.Ephemeral.Save(); } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 000e50db..0ac3b9a0 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -21,6 +21,7 @@ public class ResourceWatcher : IDisposable, ITab public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; private readonly Configuration _config; + private readonly EphemeralConfig _ephemeral; private readonly ResourceService _resources; private readonly ResourceLoader _loader; private readonly ActorService _actors; @@ -35,14 +36,15 @@ public class ResourceWatcher : IDisposable, ITab { _actors = actors; _config = config; + _ephemeral = config.Ephemeral; _resources = resources; _loader = loader; - _table = new ResourceWatcherTable(config, _records); + _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; _resources.ResourceHandleDestructor += OnResourceDestroyed; _loader.ResourceLoaded += OnResourceLoaded; _loader.FileLoaded += OnFileLoaded; - UpdateFilter(_config.ResourceLoggingFilter, false); + UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } @@ -71,11 +73,11 @@ public class ResourceWatcher : IDisposable, ITab UpdateRecords(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() / 2); - var isEnabled = _config.EnableResourceWatcher; + var isEnabled = _ephemeral.EnableResourceWatcher; if (ImGui.Checkbox("Enable", ref isEnabled)) { - _config.EnableResourceWatcher = isEnabled; - _config.Save(); + _ephemeral.EnableResourceWatcher = isEnabled; + _ephemeral.Save(); } ImGui.SameLine(); @@ -85,19 +87,19 @@ public class ResourceWatcher : IDisposable, ITab Clear(); ImGui.SameLine(); - var onlyMatching = _config.OnlyAddMatchingResources; + var onlyMatching = _ephemeral.OnlyAddMatchingResources; if (ImGui.Checkbox("Store Only Matching", ref onlyMatching)) { - _config.OnlyAddMatchingResources = onlyMatching; - _config.Save(); + _ephemeral.OnlyAddMatchingResources = onlyMatching; + _ephemeral.Save(); } ImGui.SameLine(); - var writeToLog = _config.EnableResourceLogging; + var writeToLog = _ephemeral.EnableResourceLogging; if (ImGui.Checkbox("Write to Log", ref writeToLog)) { - _config.EnableResourceLogging = writeToLog; - _config.Save(); + _ephemeral.EnableResourceLogging = writeToLog; + _ephemeral.Save(); } ImGui.SameLine(); @@ -136,8 +138,8 @@ public class ResourceWatcher : IDisposable, ITab if (config) { - _config.ResourceLoggingFilter = newString; - _config.Save(); + _ephemeral.ResourceLoggingFilter = newString; + _ephemeral.Save(); } } @@ -196,20 +198,20 @@ public class ResourceWatcher : IDisposable, ITab Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if (_config.EnableResourceLogging && FilterMatch(original.Path, out var match)) + if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); - if (!_config.EnableResourceWatcher) + if (!_ephemeral.EnableResourceWatcher) return; var record = Record.CreateRequest(original.Path, sync); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) { - if (_config.EnableResourceLogging) + if (_ephemeral.EnableResourceLogging) { var log = FilterMatch(path.Path, out var name); var name2 = string.Empty; @@ -224,41 +226,41 @@ public class ResourceWatcher : IDisposable, ITab } } - if (!_config.EnableResourceWatcher) + if (!_ephemeral.EnableResourceWatcher) return; var record = manipulatedPath == null ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data)); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) { - if (_config.EnableResourceLogging && FilterMatch(path, out var match)) + if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); - if (!_config.EnableResourceWatcher) + if (!_ephemeral.EnableResourceWatcher) return; var record = Record.CreateFileLoad(path, resource, success, custom); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } private unsafe void OnResourceDestroyed(ResourceHandle* resource) { - if (_config.EnableResourceLogging && FilterMatch(resource->FileName(), out var match)) + if (_ephemeral.EnableResourceLogging && FilterMatch(resource->FileName(), out var match)) Penumbra.Log.Information( $"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}."); - if (!_config.EnableResourceWatcher) + if (!_ephemeral.EnableResourceWatcher) return; var record = Record.CreateDestruction(resource); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 89dd42bb..b47574d0 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -13,7 +13,7 @@ namespace Penumbra.UI.ResourceWatcher; internal sealed class ResourceWatcherTable : Table { - public ResourceWatcherTable(Configuration config, IReadOnlyCollection records) + public ResourceWatcherTable(EphemeralConfig config, IReadOnlyCollection records) : base("##records", records, new PathColumn { Label = "Path" }, @@ -86,9 +86,9 @@ internal sealed class ResourceWatcherTable : Table private sealed class RecordTypeColumn : ColumnFlags { - private readonly Configuration _config; + private readonly EphemeralConfig _config; - public RecordTypeColumn(Configuration config) + public RecordTypeColumn(EphemeralConfig config) { AllFlags = ResourceWatcher.AllRecords; _config = config; @@ -174,9 +174,9 @@ internal sealed class ResourceWatcherTable : Table private sealed class ResourceCategoryColumn : ColumnFlags { - private readonly Configuration _config; + private readonly EphemeralConfig _config; - public ResourceCategoryColumn(Configuration config) + public ResourceCategoryColumn(EphemeralConfig config) { _config = config; AllFlags = ResourceExtensions.AllResourceCategories; @@ -209,9 +209,9 @@ internal sealed class ResourceWatcherTable : Table private sealed class ResourceTypeColumn : ColumnFlags { - private readonly Configuration _config; + private readonly EphemeralConfig _config; - public ResourceTypeColumn(Configuration config) + public ResourceTypeColumn(EphemeralConfig config) { _config = config; AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 24aa933c..2d30f9cf 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI.Tabs; public class CollectionsTab : IDisposable, ITab { - private readonly Configuration _config; + private readonly EphemeralConfig _config; private readonly CollectionSelector _selector; private readonly CollectionPanel _panel; private readonly TutorialService _tutorial; @@ -42,7 +42,7 @@ public class CollectionsTab : IDisposable, ITab public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, CollectionManager collectionManager, ModStorage modStorage, ActorService actors, ITargetManager targets, TutorialService tutorial) { - _config = configuration; + _config = configuration.Ephemeral; _tutorial = tutorial; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage); diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index c5811051..4f554ac5 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -115,7 +115,7 @@ public class DebugTab : Window, ITab => "Debug"u8; public bool IsVisible - => _config.DebugMode && !_config.DebugSeparateWindow; + => _config is { DebugMode: true, Ephemeral.DebugSeparateWindow: false }; #if DEBUG private const string DebugVersionString = "(Debug)"; @@ -201,12 +201,12 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("General")) return; - var separateWindow = _config.DebugSeparateWindow; + var separateWindow = _config.Ephemeral.DebugSeparateWindow; if (ImGui.Checkbox("Draw as Separate Window", ref separateWindow)) { - IsOpen = true; - _config.DebugSeparateWindow = separateWindow; - _config.Save(); + IsOpen = true; + _config.Ephemeral.DebugSeparateWindow = separateWindow; + _config.Ephemeral.Save(); } using (var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit)) @@ -949,11 +949,11 @@ public class DebugTab : Window, ITab => DrawContent(); public override bool DrawConditions() - => _config.DebugMode && _config.DebugSeparateWindow; + => _config.DebugMode && _config.Ephemeral.DebugSeparateWindow; public override void OnClose() { - _config.DebugSeparateWindow = false; - _config.Save(); + _config.Ephemeral.DebugSeparateWindow = false; + _config.Ephemeral.Save(); } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 6274f209..70a94ecd 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -78,8 +78,8 @@ public class SettingsTab : ITab return; DrawEnabledBox(); - Checkbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.FixMainWindow, - v => _config.FixMainWindow = v); + EphemeralCheckbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.Ephemeral.FixMainWindow, + v => _config.Ephemeral.FixMainWindow = v); ImGui.NewLine(); DrawRootFolder(); @@ -107,6 +107,21 @@ public class SettingsTab : ITab ImGuiUtil.LabeledHelpMarker(label, tooltip); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void EphemeralCheckbox(string label, string tooltip, bool current, Action setter) + { + using var id = ImRaii.PushId(label); + var tmp = current; + if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + { + setter(tmp); + _config.Ephemeral.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(label, tooltip); + } + #region Main Settings /// @@ -384,7 +399,10 @@ public class SettingsTab : ITab { _config.HideChangedItemFilters = v; if (v) - _config.ChangedItemFilter = ChangedItemDrawer.AllFlags; + { + _config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags; + _config.Ephemeral.Save(); + } }); Checkbox("Hide Priority Numbers in Mod Selector", "Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them.", @@ -863,8 +881,8 @@ public class SettingsTab : ITab ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) { - _config.TutorialStep = 0; - _config.Save(); + _config.Ephemeral.TutorialStep = 0; + _config.Ephemeral.Save(); } ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 86c4dd46..6c6b0612 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -55,10 +55,10 @@ public class TutorialService + " - 'furniture': most indoor furniture, does not currently work outdoors\n" + " - any specific actor name to redraw all actors of that exactly matching name."; - private readonly Configuration _config; - private readonly Tutorial _tutorial; + private readonly EphemeralConfig _config; + private readonly Tutorial _tutorial; - public TutorialService(Configuration config) + public TutorialService(EphemeralConfig config) { _config = config; _tutorial = new Tutorial() From 908239bf13bef8c95edab99285d7bc3a93b6d42f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 16:53:42 +0100 Subject: [PATCH 1285/2451] Meep. --- Penumbra/EphemeralConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 8003629d..b6a06100 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -48,7 +48,7 @@ public class EphemeralConfig : ISavable static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) { Penumbra.Log.Error( - $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + $"Error parsing ephemeral Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); errorArgs.ErrorContext.Handled = true; } From 69c493b9d6c734e867eca82cd00951002752fdce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Nov 2023 17:09:11 +0100 Subject: [PATCH 1286/2451] Morp. --- Penumbra/EphemeralConfig.cs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index b6a06100..bb4a5eb8 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -52,21 +52,23 @@ public class EphemeralConfig : ISavable errorArgs.ErrorContext.Handled = true; } - if (File.Exists(_saveService.FileNames.EphemeralConfigFile)) - try + if (!File.Exists(_saveService.FileNames.EphemeralConfigFile)) + return; + + try + { + var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { - var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile); - JsonConvert.PopulateObject(text, this, new JsonSerializerSettings - { - Error = HandleDeserializationError, - }); - } - catch (Exception ex) - { - Penumbra.Messager.NotificationMessage(ex, - "Error reading ephemeral Configuration, reverting to default.", - "Error reading ephemeral Configuration", NotificationType.Error); - } + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, + "Error reading ephemeral Configuration, reverting to default.", + "Error reading ephemeral Configuration", NotificationType.Error); + } } /// Save the current configuration. From d84715ad27372aae31b50958d655800066d79888 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Nov 2023 23:51:42 +0100 Subject: [PATCH 1287/2451] =?UTF-8?q?Fix=20stain=20preview=20(=E2=88=9A=20?= =?UTF-8?q?+=20order)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Penumbra/Services/StainService.cs | 7 ++++--- .../AdvancedWindow/ModEditWindow.Materials.ColorTable.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 63f6d05e..a0bca570 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui.Widgets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.UI.AdvancedWindow; using Penumbra.Util; namespace Penumbra.Services; @@ -61,11 +62,11 @@ public class StainService : IDisposable ImGui.SameLine(); var frame = new Vector2(ImGui.GetTextLineHeight()); - ImGui.ColorButton("D", new Vector4(colors.Diffuse, 1), 0, frame); + ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("E", new Vector4(colors.Emissive, 1), 0, frame); + ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("S", new Vector4(colors.Specular, 1), 0, frame); + ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame); return ret; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index d1cb4c94..a4e25f77 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -528,7 +528,7 @@ public partial class ModEditWindow private static float PseudoSqrtRgb(float x) => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - private static Vector3 PseudoSqrtRgb(Vector3 vec) + internal static Vector3 PseudoSqrtRgb(Vector3 vec) => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); private static Vector4 PseudoSqrtRgb(Vector4 vec) From 3e6967002b95b1ffee3b4d0898ae28b618424533 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Nov 2023 13:15:14 +0100 Subject: [PATCH 1288/2451] Allow filtering for none in certain cases. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 42 +++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index bff021bc..c307f6f4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -517,7 +517,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. @@ -531,12 +532,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), - 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), - 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), - 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), - 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), - 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), + 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), + 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), + 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), + 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), + 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), _ => (new LowerString(filterValue), 0), }, _ => (new LowerString(filterValue), 0), @@ -545,6 +546,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector /// Check the state filter for a specific pair of has/has-not flags. /// Uses count == 0 to check for has-not and count != 0 for has. @@ -584,13 +595,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector false, - 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), - 1 => !mod.Name.Contains(_modFilter), - 2 => !mod.Author.Contains(_modFilter), - 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), - 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), - _ => false, // Should never happen + -1 => false, + 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), + 1 => !mod.Name.Contains(_modFilter), + 2 => !mod.Author.Contains(_modFilter), + 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), + 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), + 2 + EmptyOffset => !mod.Author.IsEmpty, + 3 + EmptyOffset => mod.LowerChangedItemsString.Length > 0, + 4 + EmptyOffset => mod.AllTagsLower.Length > 0, + _ => false, // Should never happen }; } From 85500f0e9d89cda2e26e00730b28409a82d883db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Nov 2023 13:15:33 +0100 Subject: [PATCH 1289/2451] Improve Multi Mod selection. --- Penumbra/Services/FilenameService.cs | 28 ++---- Penumbra/Services/ServiceManager.cs | 1 + Penumbra/UI/ModsTab/ModPanel.cs | 51 ++--------- Penumbra/UI/ModsTab/MultiModPanel.cs | 125 +++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 66 deletions(-) create mode 100644 Penumbra/UI/ModsTab/MultiModPanel.cs diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index e525c1f4..cf99c6c8 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -5,26 +5,15 @@ using Penumbra.Mods; namespace Penumbra.Services; -public class FilenameService +public class FilenameService(DalamudPluginInterface pi) { - public readonly string ConfigDirectory; - public readonly string CollectionDirectory; - public readonly string LocalDataDirectory; - public readonly string ConfigFile; - public readonly string EphemeralConfigFile; - public readonly string FilesystemFile; - public readonly string ActiveCollectionsFile; - - public FilenameService(DalamudPluginInterface pi) - { - ConfigDirectory = pi.ConfigDirectory.FullName; - CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); - LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); - ConfigFile = pi.ConfigFile.FullName; - FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); - ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); - EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json"); - } + public readonly string ConfigDirectory = pi.ConfigDirectory.FullName; + public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); + public readonly string LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data"); + public readonly string ConfigFile = pi.ConfigFile.FullName; + public readonly string EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json"); + public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); + public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) @@ -34,7 +23,6 @@ public class FilenameService public string CollectionFile(string collectionName) => Path.Combine(CollectionDirectory, $"{collectionName}.json"); - /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. public string LocalDataFile(Mod mod) => LocalDataFile(mod.ModPath.FullName); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 227f65d7..73be8834 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -149,6 +149,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 59c9d279..15961ff3 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,26 +1,24 @@ -using Dalamud.Interface; using Dalamud.Plugin; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; using Penumbra.Mods; -using Penumbra.Mods.Manager; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; public class ModPanel : IDisposable { + private readonly MultiModPanel _multiModPanel; private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; private readonly ModPanelHeader _header; private readonly ModPanelTabBar _tabs; - public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) + public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, + MultiModPanel multiModPanel) { _selector = selector; _editWindow = editWindow; _tabs = tabs; + _multiModPanel = multiModPanel; _header = new ModPanelHeader(pi); _selector.SelectionChanged += OnSelectionChange; } @@ -29,7 +27,7 @@ public class ModPanel : IDisposable { if (!_valid) { - DrawMultiSelection(); + _multiModPanel.Draw(); return; } @@ -43,45 +41,6 @@ public class ModPanel : IDisposable _header.Dispose(); } - private void DrawMultiSelection() - { - if (_selector.SelectedPaths.Count == 0) - return; - - var sizeType = ImGui.GetFrameHeight(); - var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; - var sizeMods = availableSizePercent * 35; - var sizeFolders = availableSizePercent * 65; - - ImGui.NewLine(); - ImGui.TextUnformatted("Currently Selected Objects"); - ImGui.Separator(); - using var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg); - ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); - ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); - - var i = 0; - foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) - .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) - { - using var id = ImRaii.PushId(i++); - ImGui.TableNextColumn(); - var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); - if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiselection(path); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(fullName); - } - } - - private bool _valid; private Mod _mod = null!; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs new file mode 100644 index 00000000..1e4117ec --- /dev/null +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -0,0 +1,125 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ModsTab; + +public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) +{ + public void Draw() + { + if (_selector.SelectedPaths.Count == 0) + return; + + ImGui.NewLine(); + DrawModList(); + DrawMultiTagger(); + } + + private void DrawModList() + { + using var tree = ImRaii.TreeNode("Currently Selected Objects", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + ImGui.Separator(); + if (!tree) + return; + + var sizeType = ImGui.GetFrameHeight(); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; + var sizeMods = availableSizePercent * 35; + var sizeFolders = availableSizePercent * 65; + + using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg)) + { + if (!table) + return; + + ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); + ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); + + var i = 0; + foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) + .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) + { + using var id = ImRaii.PushId(i++); + ImGui.TableNextColumn(); + var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); + if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) + _selector.RemovePathFromMultiselection(path); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(fullName); + } + } + + ImGui.Separator(); + } + + private string _tag = string.Empty; + private readonly List _addMods = []; + private readonly List<(Mod, int)> _removeMods = []; + + private void DrawMultiTagger() + { + var width = ImGuiHelpers.ScaledVector2(150, 0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Multi Tagger:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); + ImGui.InputTextWithHint("##tag", "Local Tag Name...", ref _tag, 128); + + UpdateTagCache(); + var label = _addMods.Count > 0 + ? $"Add to {_addMods.Count} Mods" + : "Add"; + var tooltip = _addMods.Count == 0 + ? _tag.Length == 0 + ? "No tag specified." + : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." + : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name.Text))}"; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addMods.Count == 0)) + foreach (var mod in _addMods) + _editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); + + label = _removeMods.Count > 0 + ? $"Remove from {_removeMods.Count} Mods" + : "Remove"; + tooltip = _removeMods.Count == 0 + ? _tag.Length == 0 + ? "No tag specified." + : $"No selected mod contains the tag \"{_tag}\" locally." + : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name.Text))}"; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeMods.Count == 0)) + foreach (var (mod, index) in _removeMods) + _editor.ChangeLocalTag(mod, index, string.Empty); + ImGui.Separator(); + } + + private void UpdateTagCache() + { + _addMods.Clear(); + _removeMods.Clear(); + if (_tag.Length == 0) + return; + + foreach (var leaf in _selector.SelectedPaths.OfType()) + { + var index = leaf.Value.LocalTags.IndexOf(_tag); + if (index >= 0) + _removeMods.Add((leaf.Value, index)); + else if (!leaf.Value.ModTags.Contains(_tag)) + _addMods.Add(leaf.Value); + } + } +} From 357b11eb256d0ed8914ee313b5b7810756c613fb Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 18 Nov 2023 12:17:35 +0000 Subject: [PATCH 1290/2451] [CI] Updating repo.json for testing_0.8.1.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e804fb1e..ae281fee 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.1.8", - "TestingAssemblyVersion": "0.8.1.9", + "TestingAssemblyVersion": "0.8.1.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.1.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.1.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 55ddafea4bdb2257513ccd81f4602a5e7a5e356e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Nov 2023 15:16:05 +0100 Subject: [PATCH 1291/2451] 0.8.2.0 --- Penumbra/UI/Changelog.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index dac415d5..c9b3e96d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -44,10 +44,38 @@ public class PenumbraChangelog Add8_0_0(Changelog); Add8_1_1(Changelog); Add8_1_2(Changelog); + Add8_2_0(Changelog); } #region Changelogs + private static void Add8_2_0(Changelog log) + => log.NextVersion("Version 0.8.2.0") + .RegisterHighlight( + "You can now redraw indoor furniture. This may not be entirely stable and might break some customizable decoration like wallpapered walls.") + .RegisterEntry("The redraw bar has been slightly improved and disables currently unavailable redraw commands now.") + .RegisterEntry("Redrawing players now also actively redraws any accessories they are using.") + .RegisterEntry("Power-users can now redraw game objects by index via chat command.") + .RegisterHighlight( + "You can now filter for the special case 'None' for filters where that makes sense (like Tags or Changed Items).") + .RegisterHighlight("When selecting multiple mods, you can now add or remove tags from them at once.") + .RegisterEntry( + "The dye template combo in advanced material editing now displays the currently selected dye as it would appear with the respective template.") + .RegisterEntry("Fixed an issue with the changed item identification for left rings.") + .RegisterEntry("Updated BNPC data.") + .RegisterEntry( + "Some configuration like the currently selected tab states are now stored in a separate file that is not backed up and saved less often.") + .RegisterEntry("Added option to open the Penumbra main window at game start independently of Debug Mode.") + .RegisterEntry("Fixed some tooltips in the advanced editing window. (0.8.1.8)") + .RegisterEntry("Fixed clicking to linked changed items not working. (0.8.1.8)") + .RegisterEntry("Support correct handling of offhand-parts for two-handed weapons for changed items. (0.8.1.7)") + .RegisterEntry("Fixed renaming the mod directory not updating paths in the advanced window. (0.8.1.6)") + .RegisterEntry("Fixed portraits not respecting your card settings. (0.8.1.6)") + .RegisterEntry("Added ReverseResolvePlayerPathsAsync for IPC. (0.8.1.6)") + .RegisterEntry("Expanded the tooltip for Wait for Plugins on Startup. (0.8.1.5)") + .RegisterEntry("Disabled window sounds for some popup windows. (0.8.1.5)") + .RegisterEntry("Added support for middle-clicking mods to enable/disable them. (0.8.1.5)"); + private static void Add8_1_2(Changelog log) => log.NextVersion("Version 0.8.1.2") .RegisterEntry("Fixed an issue keeping mods selected after their deletion.") From 8a675215119d91b4559579daae8c0ae7f9fc3388 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Nov 2023 15:16:05 +0100 Subject: [PATCH 1292/2451] 0.8.2.0 --- Penumbra/UI/Changelog.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c9b3e96d..1cdb8ae4 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -61,6 +61,7 @@ public class PenumbraChangelog .RegisterHighlight("When selecting multiple mods, you can now add or remove tags from them at once.") .RegisterEntry( "The dye template combo in advanced material editing now displays the currently selected dye as it would appear with the respective template.") + .RegisterEntry("The On-Screen tab and associated functionality has been heavily improved by Ny.") .RegisterEntry("Fixed an issue with the changed item identification for left rings.") .RegisterEntry("Updated BNPC data.") .RegisterEntry( From 42042622368c9e0a1a8ac2cbd1a34efad73b35f6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 22 Nov 2023 14:20:25 +0000 Subject: [PATCH 1293/2451] [CI] Updating repo.json for 0.8.2.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index ae281fee..4f7947af 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.1.8", - "TestingAssemblyVersion": "0.8.1.10", + "AssemblyVersion": "0.8.2.0", + "TestingAssemblyVersion": "0.8.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.1.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.1.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a6f7fd623c7285dabf06dc2b3422a993dc7b3537 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 21 Nov 2023 19:14:01 +0100 Subject: [PATCH 1294/2451] ResourceTree: Fix model path resolving --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 73abcb4d..5a5ecdd9 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -154,8 +154,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerModelResourceHandle; - if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path)) - return null; + var path = ResolveModelPath(); if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached)) return cached; From 43c6b52d0bd779038d3c8371c1fabca98bf03360 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 22 Nov 2023 14:27:30 +0000 Subject: [PATCH 1295/2451] [CI] Updating repo.json for 0.8.2.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4f7947af..741a93c3 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.2.0", - "TestingAssemblyVersion": "0.8.2.0", + "AssemblyVersion": "0.8.2.1", + "TestingAssemblyVersion": "0.8.2.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a408b8918ce91acd15513557202ce213b672a550 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Nov 2023 18:30:43 +0100 Subject: [PATCH 1296/2451] Make hooks not leak. --- OtterGui | 2 +- Penumbra/Interop/ResourceLoading/TexMdlService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index b09bbcc2..3a1a3f1a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b09bbcc276363bc994d90b641871e6280898b6e5 +Subproject commit 3a1a3f1a1f2021b063617ac9b294b579a154706e diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index 68ad518c..b9279f54 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -8,7 +8,7 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceLoading; -public unsafe class TexMdlService +public unsafe class TexMdlService : IDisposable { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); From 5e76ab3b843a88f514f1a6f51b0d15464b496fff Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 26 Nov 2023 20:18:36 +0100 Subject: [PATCH 1297/2451] ResourceTree: Add filtering to the UI --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 + .../ResourceTree/ResourceTreeFactory.cs | 3 + .../UI/AdvancedWindow/ResourceTreeViewer.cs | 123 ++++++++++++++++-- Penumbra/UI/ChangedItemDrawer.cs | 39 ++++-- 4 files changed, 145 insertions(+), 22 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 53dedfa0..7ec75893 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -9,6 +9,7 @@ public class ResourceNode : ICloneable public string? Name; public string? FallbackName; public ChangedItemIcon Icon; + public ChangedItemIcon DescendentIcons; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -49,6 +50,7 @@ public class ResourceNode : ICloneable Name = other.Name; FallbackName = other.FallbackName; Icon = other.Icon; + DescendentIcons = other.DescendentIcons; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 0e3a92e2..b3b62359 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -171,6 +171,9 @@ public class ResourceTreeFactory { if (node.Name == parent?.Name) node.Name = null; + + if (parent != null) + parent.DescendentIcons |= node.Icon | node.DescendentIcons; }); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index ef847d8d..dc7fb55c 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -23,6 +23,10 @@ public class ResourceTreeViewer private readonly Action _drawActions; private readonly HashSet _unfolded; + private TreeCategory _categoryFilter; + private ChangedItemDrawer.ChangedItemIcon _typeFilter; + private string _nameFilter; + private Task? _task; public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, @@ -35,6 +39,10 @@ public class ResourceTreeViewer _onRefresh = onRefresh; _drawActions = drawActions; _unfolded = new HashSet(); + + _categoryFilter = AllCategories; + _typeFilter = ChangedItemDrawer.AllFlags; + _nameFilter = string.Empty; } public void Draw() @@ -42,6 +50,8 @@ public class ResourceTreeViewer if (ImGui.Button("Refresh Character List") || _task == null) _task = RefreshCharacterList(); + DrawFilters(); + using var child = ImRaii.Child("##Data"); if (!child) return; @@ -62,12 +72,11 @@ public class ResourceTreeViewer var debugMode = _config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { - var headerColorId = - tree.LocalPlayerRelated ? ColorId.ResTreeLocalPlayer : - tree.PlayerRelated ? ColorId.ResTreePlayer : - tree.Networked ? ColorId.ResTreeNetworked : - ColorId.ResTreeNonNetworked; - using (var c = ImRaii.PushColor(ImGuiCol.Text, headerColorId.Value())) + var category = Classify(tree); + if (!_categoryFilter.HasFlag(category) || !tree.Name.Contains(_nameFilter)) + continue; + + using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) @@ -102,6 +111,34 @@ public class ResourceTreeViewer } } + private void DrawFilters() + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + + using (var id = ImRaii.PushId("TreeCategoryFilter")) + { + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var categoryFilter = (uint)_categoryFilter; + foreach (var category in Enum.GetValues()) + { + ImGui.SameLine(0.0f, spacing); + using var c = ImRaii.PushColor(ImGuiCol.CheckMark, CategoryColor(category).Value()); + ImGui.CheckboxFlags($"##{category}", ref categoryFilter, (uint)category); + ImGuiUtil.HoverTooltip(CategoryFilterDescription(category)); + } + _categoryFilter = (TreeCategory)categoryFilter; + } + + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + + ImGui.SameLine(); + _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + + ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + } + private Task RefreshCharacterList() => Task.Run(() => { @@ -120,12 +157,27 @@ public class ResourceTreeViewer private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) { + var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + + NodeVisibility GetNodeVisibility(ResourceNode node) + { + if (node.Internal && !debugMode) + return NodeVisibility.Hidden; + + if (_typeFilter.HasFlag(node.Icon)) + return NodeVisibility.Visible; + if ((_typeFilter & node.DescendentIcons) != 0) + return NodeVisibility.DescendentsOnly; + return NodeVisibility.Hidden; + } + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { - if (resourceNode.Internal && !debugMode) + var visibility = GetNodeVisibility(resourceNode); + if (visibility == NodeVisibility.Hidden) continue; var textColor = ImGui.GetColorU32(ImGuiCol.Text); @@ -140,9 +192,8 @@ public class ResourceTreeViewer var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var unfoldable = debugMode - ? resourceNode.Children.Count > 0 - : resourceNode.Children.Any(child => !child.Internal); + var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(child) != NodeVisibility.Hidden); + var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); @@ -154,6 +205,11 @@ public class ResourceTreeViewer } else { + if (hasVisibleChildren && !unfolded) + { + _unfolded.Add(nodePathHash); + unfolded = true; + } ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } @@ -200,7 +256,7 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath}\n\nClick to copy to clipboard."); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard."); } else { @@ -223,4 +279,49 @@ public class ResourceTreeViewer DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } + + [Flags] + private enum TreeCategory : uint + { + LocalPlayer = 1, + Player = 2, + Networked = 4, + NonNetworked = 8, + } + + private const TreeCategory AllCategories = (TreeCategory)(((uint)TreeCategory.NonNetworked << 1) - 1); + + private static TreeCategory Classify(ResourceTree tree) + => tree.LocalPlayerRelated ? TreeCategory.LocalPlayer : + tree.PlayerRelated ? TreeCategory.Player : + tree.Networked ? TreeCategory.Networked : + TreeCategory.NonNetworked; + + private static ColorId CategoryColor(TreeCategory category) + => category switch + { + TreeCategory.LocalPlayer => ColorId.ResTreeLocalPlayer, + TreeCategory.Player => ColorId.ResTreePlayer, + TreeCategory.Networked => ColorId.ResTreeNetworked, + TreeCategory.NonNetworked => ColorId.ResTreeNonNetworked, + _ => throw new ArgumentException(), + }; + + private static string CategoryFilterDescription(TreeCategory category) + => category switch + { + TreeCategory.LocalPlayer => "Show you and what you own (mount, minion, accessory, pets and so on).", + TreeCategory.Player => "Show other players and what they own.", + TreeCategory.Networked => "Show non-player entities handled by the game server.", + TreeCategory.NonNetworked => "Show non-player entities handled locally.", + _ => throw new ArgumentException(), + }; + + [Flags] + private enum NodeVisibility : uint + { + Hidden = 0, + Visible = 1, + DescendentsOnly = 2, + } } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 8916d365..ddd58a8a 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -145,6 +145,18 @@ public class ChangedItemDrawer : IDisposable if (_config.HideChangedItemFilters) return; + var typeFilter = _config.Ephemeral.ChangedItemFilter; + if (DrawTypeFilter(ref typeFilter)) + { + _config.Ephemeral.ChangedItemFilter = typeFilter; + _config.Ephemeral.Save(); + } + } + + /// Draw a header line with the different icon types to filter them. + public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + { + var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); var size = new Vector2(2 * ImGui.GetTextLineHeight()); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -169,23 +181,24 @@ public class ChangedItemDrawer : IDisposable ChangedItemIcon.Unknown, }; - void DrawIcon(ChangedItemIcon type) + bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) { + var ret = false; var icon = _icons[type]; - var flag = _config.Ephemeral.ChangedItemFilter.HasFlag(type); + var flag = typeFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { - _config.Ephemeral.ChangedItemFilter = flag ? _config.Ephemeral.ChangedItemFilter & ~type : _config.Ephemeral.ChangedItemFilter | type; - _config.Ephemeral.Save(); + typeFilter = flag ? typeFilter & ~type : typeFilter | type; + ret = true; } using var popup = ImRaii.ContextPopupItem(type.ToString()); if (popup) if (ImGui.MenuItem("Enable Only This")) { - _config.Ephemeral.ChangedItemFilter = type; - _config.Ephemeral.Save(); + typeFilter = type; + ret = true; ImGui.CloseCurrentPopup(); } @@ -196,23 +209,27 @@ public class ChangedItemDrawer : IDisposable ImGui.SameLine(); ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); } + + return ret; } foreach (var iconType in order) { - DrawIcon(iconType); + ret |= DrawIcon(iconType, ref typeFilter); ImGui.SameLine(); } ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - _config.Ephemeral.ChangedItemFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - _config.Ephemeral.ChangedItemFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); + typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : + typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); if (ImGui.IsItemClicked()) { - _config.Ephemeral.ChangedItemFilter = _config.Ephemeral.ChangedItemFilter == AllFlags ? 0 : AllFlags; - _config.Ephemeral.Save(); + typeFilter = typeFilter == AllFlags ? 0 : AllFlags; + ret = true; } + + return ret; } /// Obtain the icon category corresponding to a changed item. From d05f369a94636f911293b57487163ec49f84a056 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 26 Nov 2023 20:56:35 +0100 Subject: [PATCH 1298/2451] ResourceTree: Adjustments on filtering --- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 18 +++++++++++------- Penumbra/UI/ChangedItemDrawer.cs | 12 ++++++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index dc7fb55c..75f5c5b5 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -47,10 +47,8 @@ public class ResourceTreeViewer public void Draw() { - if (ImGui.Button("Refresh Character List") || _task == null) - _task = RefreshCharacterList(); - - DrawFilters(); + DrawControls(); + _task ??= RefreshCharacterList(); using var child = ImRaii.Child("##Data"); if (!child) @@ -73,7 +71,7 @@ public class ResourceTreeViewer foreach (var (tree, index) in _task.Result.WithIndex()) { var category = Classify(tree); - if (!_categoryFilter.HasFlag(category) || !tree.Name.Contains(_nameFilter)) + if (!_categoryFilter.HasFlag(category) || !tree.Name.Contains(_nameFilter, StringComparison.OrdinalIgnoreCase)) continue; using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) @@ -111,8 +109,14 @@ public class ResourceTreeViewer } } - private void DrawFilters() + private void DrawControls() { + var yOffset = (ChangedItemDrawer.TypeFilterIconSize.Y - ImGui.GetFrameHeight()) / 2f; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); + + if (ImGui.Button("Refresh Character List")) + _task = RefreshCharacterList(); + ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); @@ -134,7 +138,7 @@ public class ResourceTreeViewer ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + _changedItemDrawer.DrawTypeFilter(ref _typeFilter, -yOffset); ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index ddd58a8a..0a1d58f9 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -52,6 +52,9 @@ public class ChangedItemDrawer : IDisposable private readonly Dictionary _icons = new(16); private float _smallestIconWidth; + public static Vector2 TypeFilterIconSize + => new(2 * ImGui.GetTextLineHeight()); + public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { @@ -146,7 +149,7 @@ public class ChangedItemDrawer : IDisposable return; var typeFilter = _config.Ephemeral.ChangedItemFilter; - if (DrawTypeFilter(ref typeFilter)) + if (DrawTypeFilter(ref typeFilter, 0.0f)) { _config.Ephemeral.ChangedItemFilter = typeFilter; _config.Ephemeral.Save(); @@ -154,11 +157,11 @@ public class ChangedItemDrawer : IDisposable } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + public bool DrawTypeFilter(ref ChangedItemIcon typeFilter, float yOffset) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); - var size = new Vector2(2 * ImGui.GetTextLineHeight()); + var size = TypeFilterIconSize; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); var order = new[] { @@ -186,6 +189,7 @@ public class ChangedItemDrawer : IDisposable var ret = false; var icon = _icons[type]; var flag = typeFilter.HasFlag(type); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { @@ -219,7 +223,7 @@ public class ChangedItemDrawer : IDisposable ImGui.SameLine(); } - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); + ImGui.SetCursorPos(new(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); From 5ebab472b822fd46fcae9e8684c99f429d1aea39 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 26 Nov 2023 21:26:35 +0100 Subject: [PATCH 1299/2451] Import from Screen: Add option selector --- .../AdvancedWindow/ModEditWindow.QuickImport.cs | 2 ++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 48d617db..626b1161 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -28,6 +28,8 @@ public partial class ModEditWindow return; } + if (DrawOptionSelectHeader()) + _quickImportActions.Clear(); _quickImportViewer.Draw(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7171a0e2..6ca22976 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -380,18 +380,25 @@ public partial class ModEditWindow : Window, IDisposable } } - private void DrawOptionSelectHeader() + private bool DrawOptionSelectHeader() { const string defaultOption = "Default Option"; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); + var ret = false; if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", _editor.Option!.IsDefault)) + { _editor.LoadOption(-1, 0); + ret = true; + } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + { _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + ret = true; + } ImGui.SameLine(); ImGui.SetNextItemWidth(width.X); @@ -399,14 +406,19 @@ public partial class ModEditWindow : Window, IDisposable using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); if (!combo) - return; + return ret; foreach (var (option, idx) in _mod!.AllSubMods.WithIndex()) { using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) + { _editor.LoadOption(option.GroupIdx, option.OptionIdx); + ret = true; + } } + + return ret; } private string _newSwapKey = string.Empty; From 73af509885d1aa69086798bd35daff19384dd524 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 28 Nov 2023 10:28:37 -0800 Subject: [PATCH 1300/2451] Add GetGameObjectResourceTrees ipc method --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 9 +++++ Penumbra/Api/PenumbraIpcProviders.cs | 3 ++ .../ResourceTree/ResourceTreeApiHelper.cs | 35 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 80f9793e..3f3af19d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 80f9793ef2ddaa50246b7112fde4d9b2098d8823 +Subproject commit 3f3af19d11ec4d7a83ee6c17810eb55ec4237675 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0ae4fcca..7b2e09d0 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1075,6 +1075,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi return resDictionaries.AsReadOnly(); } + public IEnumerable?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => resDictionary.TryGetValue(obj, out var nodes) ? nodes : null); + } + // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index b72073fb..289fc38b 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -130,6 +130,8 @@ public class PenumbraIpcProviders : IDisposable FuncProvider>> GetPlayerResourcesOfType; + internal readonly FuncProvider?[]> GetGameObjectResourceTrees; + public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { @@ -254,6 +256,7 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); + GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 6c1e4d1e..3df47086 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -1,5 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.String.Classes; using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; @@ -71,4 +73,37 @@ internal static class ResourceTreeApiHelper return resDictionaries.ToDictionary(pair => pair.Key, pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); } + + public static Dictionary> EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + { + static Ipc.ResourceNode GetIpcNode(ResourceNode[] tree, ResourceNode node) => + new() + { + ChildrenIndices = node.Children.Select(c => Array.IndexOf(tree, c)).ToArray(), + Type = node.Type, + Icon = ChangedItemDrawer.ToApiIcon(node.Icon), + Name = node.Name, + GamePath = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), + ActualPath = node.FullPath.ToString(), + ObjectAddress = node.ObjectAddress, + ResourceHandle = node.ResourceHandle, + }; + + static IEnumerable GetIpcNodes(ResourceTree tree) + { + var nodes = tree.FlatNodes.ToArray(); + return nodes.Select(n => GetIpcNode(nodes, n)).ToArray(); + } + + var resDictionary = new Dictionary>(4); + foreach (var (gameObject, resourceTree) in resourceTrees) + { + if (resDictionary.ContainsKey(gameObject.ObjectIndex)) + continue; + + resDictionary.Add(gameObject.ObjectIndex, GetIpcNodes(resourceTree)); + } + + return resDictionary; + } } From 0f03e0484c33ee9c87d239c8f1fa097b43e2bdda Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 28 Nov 2023 10:46:03 -0800 Subject: [PATCH 1301/2451] Add ipc GetPlayerResourceTrees, change ipc resource node to be nested --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 8 ++++++++ Penumbra/Api/PenumbraIpcProviders.cs | 4 +++- .../Interop/ResourceTree/ResourceTreeApiHelper.cs | 11 ++++------- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 3f3af19d..1fa1839a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 3f3af19d11ec4d7a83ee6c17810eb55ec4237675 +Subproject commit 1fa1839aef7ddd4a90f53e9642403f950579c2eb diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7b2e09d0..1e79099c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1084,6 +1084,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Array.ConvertAll(gameObjects, obj => resDictionary.TryGetValue(obj, out var nodes) ? nodes : null); } + public IReadOnlyDictionary> GetPlayerResourceTrees(bool withUIData) + { + var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return resDictionary.AsReadOnly(); + } // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 289fc38b..9f1e79b9 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -131,6 +131,7 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcesOfType; internal readonly FuncProvider?[]> GetGameObjectResourceTrees; + internal readonly FuncProvider>> GetPlayerResourceTrees; public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) @@ -256,7 +257,8 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); - GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); + GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); + GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees); Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 3df47086..02e0f380 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -76,10 +76,9 @@ internal static class ResourceTreeApiHelper public static Dictionary> EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) { - static Ipc.ResourceNode GetIpcNode(ResourceNode[] tree, ResourceNode node) => + static Ipc.ResourceNode GetIpcNode(ResourceNode node) => new() { - ChildrenIndices = node.Children.Select(c => Array.IndexOf(tree, c)).ToArray(), Type = node.Type, Icon = ChangedItemDrawer.ToApiIcon(node.Icon), Name = node.Name, @@ -87,13 +86,11 @@ internal static class ResourceTreeApiHelper ActualPath = node.FullPath.ToString(), ObjectAddress = node.ObjectAddress, ResourceHandle = node.ResourceHandle, + Children = node.Children.Select(GetIpcNode).ToArray(), }; - static IEnumerable GetIpcNodes(ResourceTree tree) - { - var nodes = tree.FlatNodes.ToArray(); - return nodes.Select(n => GetIpcNode(nodes, n)).ToArray(); - } + static IEnumerable GetIpcNodes(ResourceTree tree) => + tree.Nodes.Select(GetIpcNode).ToArray(); var resDictionary = new Dictionary>(4); foreach (var (gameObject, resourceTree) in resourceTrees) From d647a62e82fef780cfe36a63d457d260937475e8 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 28 Nov 2023 12:33:19 -0800 Subject: [PATCH 1302/2451] Add ResourceTree ipc structure --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 4 ++-- Penumbra/Api/PenumbraIpcProviders.cs | 4 ++-- .../Interop/ResourceTree/ResourceTreeApiHelper.cs | 15 ++++++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 1fa1839a..3567cf22 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1fa1839aef7ddd4a90f53e9642403f950579c2eb +Subproject commit 3567cf225b469dd5bb5f723e96e2abaaa4d16a1c diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1e79099c..8974e823 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1075,7 +1075,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return resDictionaries.AsReadOnly(); } - public IEnumerable?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) + public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) { var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); @@ -1084,7 +1084,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Array.ConvertAll(gameObjects, obj => resDictionary.TryGetValue(obj, out var nodes) ? nodes : null); } - public IReadOnlyDictionary> GetPlayerResourceTrees(bool withUIData) + public IReadOnlyDictionary GetPlayerResourceTrees(bool withUIData) { var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 9f1e79b9..10980f98 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -130,8 +130,8 @@ public class PenumbraIpcProviders : IDisposable FuncProvider>> GetPlayerResourcesOfType; - internal readonly FuncProvider?[]> GetGameObjectResourceTrees; - internal readonly FuncProvider>> GetPlayerResourceTrees; + internal readonly FuncProvider GetGameObjectResourceTrees; + internal readonly FuncProvider> GetPlayerResourceTrees; public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 02e0f380..df34c51a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -74,7 +74,7 @@ internal static class ResourceTreeApiHelper pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); } - public static Dictionary> EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) { static Ipc.ResourceNode GetIpcNode(ResourceNode node) => new() @@ -89,16 +89,21 @@ internal static class ResourceTreeApiHelper Children = node.Children.Select(GetIpcNode).ToArray(), }; - static IEnumerable GetIpcNodes(ResourceTree tree) => - tree.Nodes.Select(GetIpcNode).ToArray(); + static Ipc.ResourceTree GetIpcTree(ResourceTree tree) => + new() + { + Name = tree.Name, + RaceCode = (ushort)tree.RaceCode, + Nodes = tree.Nodes.Select(GetIpcNode).ToArray(), + }; - var resDictionary = new Dictionary>(4); + var resDictionary = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) { if (resDictionary.ContainsKey(gameObject.ObjectIndex)) continue; - resDictionary.Add(gameObject.ObjectIndex, GetIpcNodes(resourceTree)); + resDictionary.Add(gameObject.ObjectIndex, GetIpcTree(resourceTree)); } return resDictionary; From bb3d3657ed7d487acb4e575a6f91b76051bfef00 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 28 Nov 2023 12:33:37 -0800 Subject: [PATCH 1303/2451] Add ResourceTree ipc tests --- Penumbra/Api/IpcTester.cs | 103 +++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 675a61a3..f7b740b9 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,8 +16,8 @@ using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; -using ImGuiScene; using Penumbra.GameData.Structs; +using Penumbra.GameData.Enums; namespace Penumbra.Api; @@ -1437,6 +1437,8 @@ public class IpcTester : IDisposable private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private (string, Ipc.ResourceTree?)[]? _lastGameObjectResourceTrees; + private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees; private TimeSpan _lastCallDuration; public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) @@ -1523,11 +1525,46 @@ public class IpcTester : IDisposable ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); } + DrawIntro(Ipc.GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); + if (ImGui.Button("Get##GameObjectResourceTrees")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(_pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUIData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourceTrees = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(trees) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourceTrees)); + } + + DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees"); + if (ImGui.Button("Get##PlayerResourceTrees")) + { + var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(_pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUIData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourceTrees = trees + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(Ipc.GetPlayerResourceTrees)); + } + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); + + DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); } private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class @@ -1638,6 +1675,70 @@ public class IpcTester : IDisposable }); } + private void DrawResourceTrees((string, Ipc.ResourceTree?)[] result) + { + DrawWithHeaders(result, tree => + { + ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); + + using var table = ImRaii.Table(string.Empty, _withUIData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); + if (!table) + return; + + if (_withUIData) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); + ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); + } + else + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); + } + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + void DrawNode(Ipc.ResourceNode node) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + var hasChildren = node.Children.Any(); + using var treeNode = ImRaii.TreeNode( + $"{(_withUIData ? (node.Name ?? "Unknown") : node.Type)}##{node.ObjectAddress:X8}", + hasChildren ? + ImGuiTreeNodeFlags.SpanFullWidth : + (ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen)); + if (_withUIData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(node.Type.ToString()); + ImGui.TableNextColumn(); + TextUnformattedMono(node.Icon.ToString()); + } + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.GamePath ?? "Unknown"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.ActualPath); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ObjectAddress:X8}"); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ResourceHandle:X8}"); + + if (treeNode) + { + foreach (var child in node.Children) + DrawNode(child); + } + } + + foreach (var node in tree.Nodes) + DrawNode(node); + }); + } + private static void TextUnformattedMono(string text) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); From 18d38a997445bbbabceea91f9e0ec60437b974ef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 29 Nov 2023 14:31:47 +0100 Subject: [PATCH 1304/2451] Mod Edit Window: Highlight paths of files on player --- Penumbra/Import/Textures/TextureDrawer.cs | 24 ++++++++++++------- Penumbra/Mods/Editor/FileRegistry.cs | 2 ++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 4 +++- .../ModEditWindow.QuickImport.cs | 20 ++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 9 +++---- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index bea28749..6d68efbd 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -101,24 +101,25 @@ public static class TextureDrawer } } - public sealed class PathSelectCombo : FilterComboCache<(string, bool)> + public sealed class PathSelectCombo : FilterComboCache<(string Path, bool Game, bool IsOnPlayer)> { private int _skipPrefix = 0; - public PathSelectCombo(TextureManager textures, ModEditor editor) - : base(() => CreateFiles(textures, editor), Penumbra.Log) + public PathSelectCombo(TextureManager textures, ModEditor editor, Func> getPlayerResources) + : base(() => CreateFiles(textures, editor, getPlayerResources), Penumbra.Log) { } - protected override string ToString((string, bool) obj) - => obj.Item1; + protected override string ToString((string Path, bool Game, bool IsOnPlayer) obj) + => obj.Path; protected override bool DrawSelectable(int globalIdx, bool selected) { - var (path, game) = Items[globalIdx]; + var (path, game, isOnPlayer) = Items[globalIdx]; bool ret; using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) { - var equals = string.Equals(CurrentSelection.Item1, path, StringComparison.OrdinalIgnoreCase); + color.Push(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), isOnPlayer); + var equals = string.Equals(CurrentSelection.Path, path, StringComparison.OrdinalIgnoreCase); var p = game ? $"--> {path}" : path[_skipPrefix..]; ret = ImGui.Selectable(p, selected) && !equals; } @@ -129,11 +130,16 @@ public static class TextureDrawer return ret; } - private static IReadOnlyList<(string, bool)> CreateFiles(TextureManager textures, ModEditor editor) - => editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + private static IReadOnlyList<(string Path, bool Game, bool IsOnPlayer)> CreateFiles(TextureManager textures, ModEditor editor, Func> getPlayerResources) + { + var playerResources = getPlayerResources(); + + return editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))) .Where(p => p.Item2 ? textures.GameFileExists(p.Item1) : File.Exists(p.Item1)) + .Select(p => (p.Item1, p.Item2, playerResources.Contains(p.Item1))) .ToList(); + } public bool Draw(string label, string tooltip, string current, int skipPrefix, out string newPath) { diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 791778e3..a223b51e 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -10,6 +10,7 @@ public class FileRegistry : IEquatable public Utf8RelPath RelPath { get; private init; } public long FileSize { get; private init; } public int CurrentUsage; + public bool IsOnPlayer; public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) { @@ -26,6 +27,7 @@ public class FileRegistry : IEquatable RelPath = relPath, FileSize = file.Length, CurrentUsage = 0, + IsOnPlayer = false, }; return true; } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index b84fa84c..c3a1c0dc 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -285,7 +285,9 @@ public class FileEditor : IDisposable where T : class, IWritable protected override bool DrawSelectable(int globalIdx, bool selected) { var file = Items[globalIdx]; - var ret = ImGui.Selectable(file.RelPath.ToString(), selected); + bool ret; + using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) + ret = ImGui.Selectable(file.RelPath.ToString(), selected); if (ImGui.IsItemHovered()) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 48d617db..1ab1ed88 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -3,6 +3,7 @@ using ImGuiNET; using Lumina.Data; using OtterGui; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; @@ -19,6 +20,25 @@ public partial class ModEditWindow private readonly Dictionary _quickImportWritables = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); + private HashSet GetPlayerResourcesOfType(ResourceType type) + { + var resources = ResourceTreeApiHelper.GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) + .Values + .SelectMany(resources => resources.Values) + .Select(resource => resource.Item1); + + return new(resources, StringComparer.OrdinalIgnoreCase); + } + + private IReadOnlyList PopulateIsOnPlayer(IReadOnlyList files, ResourceType type) + { + var playerResources = GetPlayerResourcesOfType(type); + foreach (var file in files) + file.IsOnPlayer = playerResources.Contains(file.File.ToPath()); + + return files; + } + private void DrawQuickImportTab() { using var tab = ImRaii.TabItem("Import from Screen"); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7171a0e2..ce2bb2a4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; @@ -569,15 +570,15 @@ public partial class ModEditWindow : Window, IDisposable _fileDialog = fileDialog; _gameEvents = gameEvents; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", - () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, + () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", - () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, + () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); - _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); + _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); From 1101a7a98625e8d51e23473b64ca57e30246a8f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Nov 2023 17:23:37 +0100 Subject: [PATCH 1305/2451] Make sure the SubstitutionProvider is initialized before the interface. --- Penumbra/Penumbra.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ef8a1a05..bcf94ed1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -85,6 +85,7 @@ public class Penumbra : IDalamudPlugin _services.GetRequiredService(); + _services.GetRequiredService(); // Initialize before Interface. SetupInterface(); SetupApi(); From b7272207755d8f87d35bb1b2cba83eaba69255a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Nov 2023 17:27:09 +0100 Subject: [PATCH 1306/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3a1a3f1a..3e2d4ae9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3a1a3f1a1f2021b063617ac9b294b579a154706e +Subproject commit 3e2d4ae934694918d312280d62127cf1a55b03e4 From 4aa1388b344558df1dada39f8152004df79c5c6c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Nov 2023 17:30:57 +0100 Subject: [PATCH 1307/2451] Update actions for recursive submodules. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a317f236..3dd1d45b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Setup .NET uses: actions/setup-dotnet@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44f9fd2f..3141c6ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Setup .NET uses: actions/setup-dotnet@v1 with: diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 80c0ce8f..2644974b 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Setup .NET uses: actions/setup-dotnet@v1 with: From 3d9f8355d234272e1d75a071f231fc9002ef8edb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Nov 2023 17:32:59 +0100 Subject: [PATCH 1308/2451] Make braces clear for scoped using. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c3a1c0dc..336be1a4 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -287,7 +287,9 @@ public class FileEditor : IDisposable where T : class, IWritable var file = Items[globalIdx]; bool ret; using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) + { ret = ImGui.Selectable(file.RelPath.ToString(), selected); + } if (ImGui.IsItemHovered()) { From e497414cb7c30222a38d72695ed4976ba7f759d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 29 Nov 2023 18:00:30 +0100 Subject: [PATCH 1309/2451] Auto-Formatting and some imgui layouting. --- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 75f5c5b5..d31f3e52 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -11,9 +11,7 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = - ResourceTreeFactory.Flags.RedactExternalPaths | - ResourceTreeFactory.Flags.WithUiData | - ResourceTreeFactory.Flags.WithOwnership; + ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly Configuration _config; private readonly ResourceTreeFactory _treeFactory; @@ -83,6 +81,7 @@ public class ResourceTreeViewer ImGuiUtil.HoverTooltip( $"Object Index: {tree.GameObjectIndex}\nObject Address: 0x{tree.GameObjectAddress:X16}\nDraw Object Address: 0x{tree.DrawObjectAddress:X16}"); } + if (!isOpen) continue; } @@ -117,29 +116,29 @@ public class ResourceTreeViewer if (ImGui.Button("Refresh Character List")) _task = RefreshCharacterList(); - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + var checkSpacing = ImGui.GetStyle().ItemInnerSpacing.X; + var checkPadding = 10 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X; + ImGui.SameLine(0, checkPadding); using (var id = ImRaii.PushId("TreeCategoryFilter")) { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; var categoryFilter = (uint)_categoryFilter; foreach (var category in Enum.GetValues()) { - ImGui.SameLine(0.0f, spacing); using var c = ImRaii.PushColor(ImGuiCol.CheckMark, CategoryColor(category).Value()); ImGui.CheckboxFlags($"##{category}", ref categoryFilter, (uint)category); ImGuiUtil.HoverTooltip(CategoryFilterDescription(category)); + ImGui.SameLine(0.0f, checkSpacing); } + _categoryFilter = (TreeCategory)categoryFilter; } - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(0, checkPadding); - ImGui.SameLine(); _changedItemDrawer.DrawTypeFilter(ref _typeFilter, -yOffset); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); } @@ -161,7 +160,6 @@ public class ResourceTreeViewer private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) { - var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; @@ -175,6 +173,7 @@ public class ResourceTreeViewer return NodeVisibility.Visible; if ((_typeFilter & node.DescendentIcons) != 0) return NodeVisibility.DescendentsOnly; + return NodeVisibility.Hidden; } @@ -214,6 +213,7 @@ public class ResourceTreeViewer _unfolded.Add(nodePathHash); unfolded = true; } + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } @@ -297,9 +297,9 @@ public class ResourceTreeViewer private static TreeCategory Classify(ResourceTree tree) => tree.LocalPlayerRelated ? TreeCategory.LocalPlayer : - tree.PlayerRelated ? TreeCategory.Player : - tree.Networked ? TreeCategory.Networked : - TreeCategory.NonNetworked; + tree.PlayerRelated ? TreeCategory.Player : + tree.Networked ? TreeCategory.Networked : + TreeCategory.NonNetworked; private static ColorId CategoryColor(TreeCategory category) => category switch From eb0e334437e07c276c72bf0bab9aa7b758054b20 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 30 Nov 2023 09:53:16 -0800 Subject: [PATCH 1310/2451] Add ResourceTree ipc disposes --- Penumbra/Api/PenumbraIpcProviders.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 10980f98..a564588b 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -375,6 +375,8 @@ public class PenumbraIpcProviders : IDisposable GetPlayerResourcePaths.Dispose(); GetGameObjectResourcesOfType.Dispose(); GetPlayerResourcesOfType.Dispose(); + GetGameObjectResourceTrees.Dispose(); + GetPlayerResourceTrees.Dispose(); Disposed.Invoke(); Disposed.Dispose(); From b595a0da0ff0ceb81e428b87518b2ab487293bf7 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 30 Nov 2023 10:01:49 -0800 Subject: [PATCH 1311/2451] Replace ResourceTree IEnumerables with lists --- Penumbra.Api | 2 +- Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 3567cf22..e2f578a9 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 3567cf225b469dd5bb5f723e96e2abaaa4d16a1c +Subproject commit e2f578a903f4e2de6c5967eb92f1b5a0a413d287 diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index df34c51a..386caf9d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -86,7 +86,7 @@ internal static class ResourceTreeApiHelper ActualPath = node.FullPath.ToString(), ObjectAddress = node.ObjectAddress, ResourceHandle = node.ResourceHandle, - Children = node.Children.Select(GetIpcNode).ToArray(), + Children = node.Children.Select(GetIpcNode).ToList(), }; static Ipc.ResourceTree GetIpcTree(ResourceTree tree) => @@ -94,7 +94,7 @@ internal static class ResourceTreeApiHelper { Name = tree.Name, RaceCode = (ushort)tree.RaceCode, - Nodes = tree.Nodes.Select(GetIpcNode).ToArray(), + Nodes = tree.Nodes.Select(GetIpcNode).ToList(), }; var resDictionary = new Dictionary(4); From 07e20fb6701087932930b98e2ae2c00e9e746ac7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Dec 2023 13:49:29 +0100 Subject: [PATCH 1312/2451] Test not redrawing while the fishing rod is out. --- Penumbra/EphemeralConfig.cs | 1 - Penumbra/Interop/Services/RedrawService.cs | 44 +++++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index bb4a5eb8..6c87d331 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Enums; -using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.ResourceWatcher; diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 49d688af..8e47fa0b 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -11,6 +11,7 @@ using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.Interop.Structs; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Services; @@ -248,8 +249,15 @@ public sealed unsafe partial class RedrawService : IDisposable { if (idx < 0) { - WriteInvisible(obj); - _queue[numKept++] = ObjectTableIndex(obj); + if (DelayRedraw(obj)) + { + _queue[numKept++] = ~ObjectTableIndex(obj); + } + else + { + WriteInvisible(obj); + _queue[numKept++] = ObjectTableIndex(obj); + } } else { @@ -261,6 +269,30 @@ public sealed unsafe partial class RedrawService : IDisposable _queue.RemoveRange(numKept, _queue.Count - numKept); } + private static uint GetCurrentAnimationId(GameObject obj) + { + var gameObj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address; + if (gameObj == null || !gameObj->IsCharacter()) + return 0; + + var chara = (Character*)gameObj; + var ptr = (byte*)&chara->ActionTimelineManager + 0xF0; + return *(uint*)ptr; + } + + private static bool DelayRedraw(GameObject obj) + => ((Character*)obj.Address)->Mode switch + { + (Character.CharacterModes)6 => // fishing + GetCurrentAnimationId(obj) switch + { + 278 => true, // line out. + 283 => true, // reeling in + _ => false, + }, + _ => false, + }; + private void HandleAfterGPose() { if (_afterGPoseQueue.Count == 0 || InGPose) @@ -369,10 +401,11 @@ public sealed unsafe partial class RedrawService : IDisposable private void DisableFurniture() { var housingManager = HousingManager.Instance(); - if (housingManager == null) + if (housingManager == null) return; + var currentTerritory = housingManager->CurrentTerritory; - if (currentTerritory == null) + if (currentTerritory == null) return; if (!housingManager->IsInside()) return; @@ -380,8 +413,9 @@ public sealed unsafe partial class RedrawService : IDisposable foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) { var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; - if (gameObject == null) + if (gameObject == null) continue; + gameObject->DisableDraw(); } } From 2c1ce660115be08312f175efefa12b78e210a2ac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Dec 2023 14:32:13 +0100 Subject: [PATCH 1313/2451] Update Penumbra.Api. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index e2f578a9..cfc51714 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit e2f578a903f4e2de6c5967eb92f1b5a0a413d287 +Subproject commit cfc51714f74cae93608bc507775a9580cd1801de From e0fa8c9285100df589b3067814575cd87f93a3a4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 1 Dec 2023 13:35:51 +0000 Subject: [PATCH 1314/2451] [CI] Updating repo.json for testing_0.8.2.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 741a93c3..4f815483 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.2.1", - "TestingAssemblyVersion": "0.8.2.1", + "TestingAssemblyVersion": "0.8.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.2.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a0328aab35430d45f95b8f3fd2ca5284dc2bfc12 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Dec 2023 16:29:36 +0100 Subject: [PATCH 1315/2451] Compile releases from release dalamud, not staging. --- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- Penumbra/Interop/Services/RedrawService.cs | 24 +++++++++++----------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3141c6ed..f3afe9c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 2644974b..0968430d 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 8e47fa0b..7a73857a 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -245,25 +245,25 @@ public sealed unsafe partial class RedrawService : IDisposable if (FindCorrectActor(idx < 0 ? ~idx : idx, out var obj)) _afterGPoseQueue.Add(idx < 0 ? idx : ~idx); - if (obj != null) + if (obj == null) + continue; + + if (idx < 0) { - if (idx < 0) + if (DelayRedraw(obj)) { - if (DelayRedraw(obj)) - { - _queue[numKept++] = ~ObjectTableIndex(obj); - } - else - { - WriteInvisible(obj); - _queue[numKept++] = ObjectTableIndex(obj); - } + _queue[numKept++] = ~ObjectTableIndex(obj); } else { - WriteVisible(obj); + WriteInvisible(obj); + _queue[numKept++] = ObjectTableIndex(obj); } } + else + { + WriteVisible(obj); + } } _queue.RemoveRange(numKept, _queue.Count - numKept); From 7128f237da0b39490ecba7e67a9f9dc027983f7f Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 1 Dec 2023 15:31:41 +0000 Subject: [PATCH 1316/2451] [CI] Updating repo.json for testing_0.8.2.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4f815483..af1104be 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.2.1", - "TestingAssemblyVersion": "0.8.2.2", + "TestingAssemblyVersion": "0.8.2.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.2.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.2.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a9f36c6aef01df1370d45a1fa34a211cc34282a6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 10 Dec 2023 15:09:37 +0100 Subject: [PATCH 1317/2451] Fix inverted percentage, skill. --- Penumbra/Import/TexToolsImporter.Gui.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index e150d10d..78665f30 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -33,7 +33,7 @@ public partial class TexToolsImporter else { ImGui.NewLine(); - var percentage = _modPackCount / (float)_currentModPackIdx; + var percentage = (float)_currentModPackIdx / _modPackCount; ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); if (State == ImporterState.DeduplicatingFiles) From bb742463e99ebdc5e3645a39f585646087801611 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 10 Dec 2023 15:10:43 +0100 Subject: [PATCH 1318/2451] Wait for saves to finish when the file might be read immediately after saving. --- OtterGui | 2 +- Penumbra/Collections/Manager/ModCollectionMigration.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/ModCreator.cs | 8 ++++---- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/Services/SaveService.cs | 3 ++- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index 3e2d4ae9..7098e957 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3e2d4ae934694918d312280d62127cf1a55b03e4 +Subproject commit 7098e9577117a3555f5f6181edae6cd306a4b5d4 diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 025df9ef..b2b8df0d 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -14,7 +14,7 @@ internal static class ModCollectionMigration { var changes = MigrateV0ToV1(collection, ref version); if (changes) - saver.ImmediateSave(new ModCollectionSave(mods, collection)); + saver.ImmediateSaveSync(new ModCollectionSave(mods, collection)); } /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 488c1c91..7df0389e 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -82,7 +82,7 @@ public class DuplicateManager { var sub = (SubMod)subMod; sub.FileData = dict; - _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx)); } } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 98770edc..383b6d2d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -177,7 +177,7 @@ public partial class ModCreator return; _saveService.SaveAllOptionGroups(mod, false); - _saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default)); } @@ -261,7 +261,7 @@ public partial class ModCreator DefaultSettings = defaultSettings, }; group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, idx))); - _saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index)); break; } case GroupType.Single: @@ -274,7 +274,7 @@ public partial class ModCreator DefaultSettings = defaultSettings, }; group.OptionData.AddRange(subMods.OfType()); - _saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index)); break; } } @@ -321,7 +321,7 @@ public partial class ModCreator } IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSave(new ModSaveGroup(mod, -1)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1)); } /// Return the name of a new valid directory based on the base directory and the given name. diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 03aedc57..beb23fa2 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -372,7 +372,7 @@ public class ConfigMigrationService var emptyStorage = new ModStorage(); var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); - _saveService.ImmediateSave(new ModCollectionSave(emptyStorage, collection)); + _saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) { diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 0e61c565..3f54160d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -36,7 +36,8 @@ public sealed class SaveService : SaveServiceBase } } - for (var i = 0; i < mod.Groups.Count; ++i) + for (var i = 0; i < mod.Groups.Count - 1; ++i) ImmediateSave(new ModSaveGroup(mod, i)); + ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1)); } } From 59ea1f2dd637641a3aaf4164a1eb3e409bf03160 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 10 Dec 2023 15:41:26 +0100 Subject: [PATCH 1319/2451] Add option to clear non-ascii symbols from paths again. --- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImporter.Archives.cs | 6 +-- Penumbra/Import/TexToolsImporter.ModPack.cs | 10 ++--- Penumbra/Meta/MetaFileManager.cs | 4 +- Penumbra/Mods/Editor/ModMerger.cs | 12 +++--- Penumbra/Mods/Editor/ModNormalizer.cs | 18 ++++----- Penumbra/Mods/Manager/ModManager.cs | 2 +- Penumbra/Mods/ModCreator.cs | 37 ++++++------------- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 ++- .../ModEditWindow.QuickImport.cs | 15 ++++---- Penumbra/UI/Tabs/SettingsTab.cs | 3 ++ 13 files changed, 56 insertions(+), 62 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 8974e823..5ce28510 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -877,7 +877,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name || name.Contains('|')) + if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name, _config.ReplaceNonAsciiOnImport) != name || name.Contains('|')) return PenumbraApiEc.InvalidArgument; return _tempCollections.CreateTemporaryCollection(name).Length > 0 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8f21ee52..a5a615bd 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -46,6 +46,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseOwnerNameForCharacterCollection { get; set; } = true; public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 6ddafdd7..57313ab1 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -44,7 +44,7 @@ public partial class TexToolsImporter }; Penumbra.Log.Information($" -> Importing {archive.Type} Archive."); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName()); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -97,13 +97,13 @@ public partial class TexToolsImporter // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if (leadDir) { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, false); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); Directory.Delete(oldName); } else { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, false); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); Directory.Move(oldName, _currentModDirectory.FullName); } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index dbe76ae3..94a5e5ac 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -35,7 +35,7 @@ public partial class TexToolsImporter var modList = modListRaw.Select(m => JsonConvert.DeserializeObject(m, JsonSettings)!).ToList(); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name)); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name), _config.ReplaceNonAsciiOnImport, true); // Create a new ModMeta from the TTMP mod list info _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null); @@ -88,7 +88,7 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; Penumbra.Log.Information(" -> Importing Simple V2 ModPack"); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName, _config.ReplaceNonAsciiOnImport, true); _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty(modList.Description) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url); @@ -131,7 +131,7 @@ public partial class TexToolsImporter _currentNumOptions = GetOptionCount(modList); _currentModName = modList.Name; - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, _currentModName, _config.ReplaceNonAsciiOnImport, true); _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url); @@ -168,7 +168,7 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = ModCreator.NewSubFolderName(_currentModDirectory, name) + var groupFolder = ModCreator.NewSubFolderName(_currentModDirectory, name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(_currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}")); @@ -178,7 +178,7 @@ public partial class TexToolsImporter var option = allOptions[i + optionIdx]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name) + var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ExtractSimpleModList(optionFolder, option.ModsJsons); options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index d918bda2..9c42b9fc 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -49,13 +49,13 @@ public unsafe class MetaFileManager TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); foreach (var group in mod.Groups) { - var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name); + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); if (!dir.Exists) dir.Create(); foreach (var option in group.OfType()) { - var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) optionDir.Create(); diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 37ffdcfe..1dfe9e76 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -15,6 +14,7 @@ namespace Penumbra.Mods.Editor; public class ModMerger : IDisposable { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ModOptionEditor _editor; private readonly ModFileSystemSelector _selector; @@ -40,13 +40,14 @@ public class ModMerger : IDisposable public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, - CommunicatorService communicator, ModCreator creator) + CommunicatorService communicator, ModCreator creator, Configuration config) { _editor = editor; _selector = selector; _duplicates = duplicates; _communicator = communicator; _creator = creator; + _config = config; _mods = mods; _selector.SelectionChanged += OnSelectionChange; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); @@ -82,7 +83,8 @@ public class ModMerger : IDisposable catch (Exception ex) { Error = ex; - Penumbra.Messager.NotificationMessage(ex, $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(ex, $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.", + NotificationType.Error, false); FailureCleanup(); DataCleanup(); } @@ -138,10 +140,10 @@ public class ModMerger : IDisposable var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); if (optionCreated) _createdOptions.Add(option); - var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName); + var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); - dir = ModCreator.NewOptionDirectory(dir, optionName); + dir = ModCreator.NewOptionDirectory(dir, optionName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 3610c99a..c146b6f4 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -6,11 +6,10 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; -public class ModNormalizer +public class ModNormalizer(ModManager _modManager, Configuration _config) { - private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -25,9 +24,6 @@ public class ModNormalizer public bool Running => !Worker.IsCompleted; - public ModNormalizer(ModManager modManager) - => _modManager = modManager; - public void Normalize(Mod mod) { if (Step < TotalSteps) @@ -175,10 +171,10 @@ public class ModNormalizer for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) _redirections[groupIdx + 1].Add(new Dictionary()); - var groupDir = ModCreator.CreateModFolder(directory, group.Name); + var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); foreach (var option in group.OfType()) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name); + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); newDict = _redirections[groupIdx + 1][option.OptionIdx]; newDict.Clear(); @@ -228,7 +224,8 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Messager.NotificationMessage(e, $"Could not move old files out of the way while normalizing mod {Mod.Name}.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(e, $"Could not move old files out of the way while normalizing mod {Mod.Name}.", + NotificationType.Error, false); } return false; @@ -251,7 +248,8 @@ public class ModNormalizer } catch (Exception e) { - Penumbra.Messager.NotificationMessage(e, $"Could not move new files into the mod while normalizing mod {Mod.Name}.", NotificationType.Error, false); + Penumbra.Messager.NotificationMessage(e, $"Could not move new files into the mod while normalizing mod {Mod.Name}.", + NotificationType.Error, false); foreach (var dir in Mod.ModPath.EnumerateDirectories()) { if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index e258f996..40585520 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -217,7 +217,7 @@ public sealed class ModManager : ModStorage, IDisposable if (oldName == newName) return NewDirectoryState.Identical; - var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); + var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName, _config.ReplaceNonAsciiOnImport); if (fixedNewName != newName) return NewDirectoryState.ContainsInvalidSymbols; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 383b6d2d..31fa64ab 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,30 +16,15 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator +public partial class ModCreator(SaveService _saveService, Configuration _config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, + IGamePathParser _gamePathParser) { - private readonly Configuration _config; - private readonly SaveService _saveService; - private readonly ModDataEditor _dataEditor; - private readonly MetaFileManager _metaFileManager; - private readonly IGamePathParser _gamePathParser; - - public ModCreator(SaveService saveService, Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager, - IGamePathParser gamePathParser) - { - _saveService = saveService; - _config = config; - _dataEditor = dataEditor; - _metaFileManager = metaFileManager; - _gamePathParser = gamePathParser; - } - /// Creates directory and files necessary for a new mod without adding it to the manager. public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "") { try { - var newDir = CreateModFolder(basePath, newName); + var newDir = CreateModFolder(basePath, newName, _config.ReplaceNonAsciiOnImport, true); _dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; @@ -138,13 +123,13 @@ public partial class ModCreator /// - Unique, by appending (digit) for duplicates.
/// - Containing no symbols invalid for FFXIV or windows paths.
/// - public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool create = true) + public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool onlyAscii, bool create) { var name = modListName; if (name.Length == 0) name = "_"; - var newModFolderBase = NewOptionDirectory(outDirectory, name); + var newModFolderBase = NewOptionDirectory(outDirectory, name, onlyAscii); var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); if (newModFolder.Length == 0) throw new IOException("Could not create mod folder: too many folders of the same name exist."); @@ -238,9 +223,9 @@ public partial class ModCreator /// Create the name for a group or option subfolder based on its parent folder and given name. /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. /// - public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName) + public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName, bool onlyAscii) { - var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName); + var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName, onlyAscii); var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); return newModFolder.Length == 0 ? null : new DirectoryInfo(newModFolder); } @@ -325,14 +310,14 @@ public partial class ModCreator } /// Return the name of a new valid directory based on the base directory and the given name. - public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName) + public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName, bool onlyAscii) { - var option = ReplaceBadXivSymbols(optionName); + var option = ReplaceBadXivSymbols(optionName, onlyAscii); return new DirectoryInfo(Path.Combine(baseDir.FullName, option.Length > 0 ? option : "_")); } /// Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols(string s, string replacement = "_") + public static string ReplaceBadXivSymbols(string s, bool onlyAscii, string replacement = "_") { switch (s) { @@ -345,6 +330,8 @@ public partial class ModCreator { if (c.IsInvalidInPath()) sb.Append(replacement); + else if (onlyAscii && c.IsInvalidAscii()) + sb.Append(replacement); else sb.Append(c); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 73273707..dc73b451 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -52,7 +52,7 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name); + dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 8597bc0c..5347208e 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -25,6 +25,7 @@ namespace Penumbra.UI.AdvancedWindow; public class ItemSwapTab : IDisposable, ITab { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ItemService _itemService; private readonly CollectionManager _collectionManager; @@ -32,13 +33,14 @@ public class ItemSwapTab : IDisposable, ITab private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager) + ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager, Configuration config) { _communicator = communicator; _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; _metaFileManager = metaFileManager; + _config = config; _swapData = new ItemSwapContainer(metaFileManager, identifier); _selectors = new Dictionary @@ -296,7 +298,7 @@ public class ItemSwapTab : IDisposable, ITab { optionFolderName = ModCreator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), - _newOptionName); + _newOptionName, _config.ReplaceNonAsciiOnImport); if (optionFolderName?.Exists == true) throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 63ea8581..64457c25 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -22,12 +22,13 @@ public partial class ModEditWindow private HashSet GetPlayerResourcesOfType(ResourceType type) { - var resources = ResourceTreeApiHelper.GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) + var resources = ResourceTreeApiHelper + .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) .Values .SelectMany(resources => resources.Values) .Select(resource => resource.Item1); - return new(resources, StringComparer.OrdinalIgnoreCase); + return new HashSet(resources, StringComparer.OrdinalIgnoreCase); } private IReadOnlyList PopulateIsOnPlayer(IReadOnlyList files, ResourceType type) @@ -198,7 +199,7 @@ public partial class ModEditWindow if (mod == null) return new QuickImportAction(editor, optionName, gamePath); - var (preferredPath, subDirs) = GetPreferredPath(mod, subMod); + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) return new QuickImportAction(editor, optionName, gamePath); @@ -226,7 +227,7 @@ public partial class ModEditWindow return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; @@ -237,13 +238,13 @@ public partial class ModEditWindow var fullName = subMod.FullName; if (fullName.EndsWith(": " + name)) { - path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); - path = ModCreator.NewOptionDirectory(path, name); + path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)], replaceNonAscii); + path = ModCreator.NewOptionDirectory(path, name, replaceNonAscii); subDirs = 2; } else { - path = ModCreator.NewOptionDirectory(path, fullName); + path = ModCreator.NewOptionDirectory(path, fullName, replaceNonAscii); subDirs = 1; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 70a94ecd..104f8d91 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -535,6 +535,9 @@ public class SettingsTab : ITab /// Draw all settings pertaining to import and export of mods. private void DrawModHandlingSettings() { + Checkbox("Replace Non-Standard Symbols On Import", + "Replace all non-ASCII symbols in mod and option names with underscores when importing mods.", _config.ReplaceNonAsciiOnImport, + v => _config.ReplaceNonAsciiOnImport = v); Checkbox("Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); From 54776c45ea5a86c0133322752055e1d4db2a8f74 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 10 Dec 2023 15:41:38 +0100 Subject: [PATCH 1320/2451] 0.8.3.0 --- Penumbra/UI/Changelog.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 1cdb8ae4..d172cfb9 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -45,10 +45,24 @@ public class PenumbraChangelog Add8_1_1(Changelog); Add8_1_2(Changelog); Add8_2_0(Changelog); + Add8_3_0(Changelog); } #region Changelogs + private static void Add8_3_0(Changelog log) + => log.NextVersion("Version 0.8.3.0") + .RegisterHighlight("Improved the UI for the On-Screen tabs with highlighting of used paths, filtering and more selections. (by Ny)") + .RegisterEntry("Added an option to replace non-ASCII symbols with underscores for folder paths on mod import since this causes problems on some WINE systems. This option is off by default.") + .RegisterEntry( + "Added support for the Changed Item Icons to load modded icons, but this depends on a not-yet-released Dalamud update.") + .RegisterEntry( + "Penumbra should no longer redraw characters while they are fishing, but wait for them to reel in, because that could cause soft-locks. This may cause other issues, but I have not found any.") + .RegisterEntry( + "Hopefully fixed a bug on mod import where files were being read while they were still saving, causing Penumbra to create wrong options.") + .RegisterEntry("Fixed a few display issues.") + .RegisterEntry("Added some IPC functionality for Xande. (by Asriel)"); + private static void Add8_2_0(Changelog log) => log.NextVersion("Version 0.8.2.0") .RegisterHighlight( From 76cb09b3b557093932348662c303e3d1f20fe476 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 10 Dec 2023 14:43:34 +0000 Subject: [PATCH 1321/2451] [CI] Updating repo.json for 0.8.3.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index af1104be..a04b35b5 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.2.1", - "TestingAssemblyVersion": "0.8.2.3", + "AssemblyVersion": "0.8.3.0", + "TestingAssemblyVersion": "0.8.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.2.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.2.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 173b4d7306b662ccbfae466ac50d488ebf0c700e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Dec 2023 21:05:08 +0100 Subject: [PATCH 1322/2451] Respect ascii setting for group names. --- Penumbra/Mods/Editor/DuplicateManager.cs | 6 ++-- Penumbra/Mods/Manager/ModMigration.cs | 4 +-- Penumbra/Mods/Manager/ModOptionEditor.cs | 44 +++++++++++++----------- Penumbra/Mods/ModCreator.cs | 29 +++++++++------- Penumbra/Mods/Subclasses/IModGroup.cs | 18 ++++++---- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/Services/FilenameService.cs | 8 ++--- Penumbra/Services/SaveService.cs | 6 ++-- Penumbra/UI/Changelog.cs | 3 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 8 ++--- 10 files changed, 70 insertions(+), 58 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 7df0389e..47c34ce5 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -7,15 +7,17 @@ namespace Penumbra.Mods.Editor; public class DuplicateManager { + private readonly Configuration _config; private readonly SaveService _saveService; private readonly ModManager _modManager; private readonly SHA256 _hasher = SHA256.Create(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - public DuplicateManager(ModManager modManager, SaveService saveService) + public DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) { _modManager = modManager; _saveService = saveService; + _config = config; } public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates @@ -82,7 +84,7 @@ public class DuplicateManager { var sub = (SubMod)subMod; sub.FileData = dict; - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 452da366..8b73cae5 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -83,7 +83,7 @@ public static partial class ModMigration creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); foreach (var (_, index) in mod.Groups.WithIndex()) - saveService.ImmediateSave(new ModSaveGroup(mod, index)); + saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -111,7 +111,7 @@ public static partial class ModMigration } fileVersion = 1; - saveService.ImmediateSave(new ModSaveGroup(mod, -1)); + saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport)); return true; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 0a3034fc..73cb80cc 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -33,13 +33,15 @@ public enum ModOptionChangeType public class ModOptionEditor { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; - public ModOptionEditor(CommunicatorService communicator, SaveService saveService) + public ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { _communicator = communicator; _saveService = saveService; + _config = config; } /// Change the type of a group given by mod and index to type, if possible. @@ -50,7 +52,7 @@ public class ModOptionEditor return; mod.Groups[groupIdx] = group.Convert(type); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } @@ -62,7 +64,7 @@ public class ModOptionEditor return; group.DefaultSettings = defaultOption; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); } @@ -74,7 +76,7 @@ public class ModOptionEditor if (oldName == newName || !VerifyFileName(mod, group, newName, true)) return; - _saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx)); + _saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); var _ = group switch { SingleModGroup s => s.Name = newName, @@ -82,7 +84,7 @@ public class ModOptionEditor _ => newName, }; - _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); + _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } @@ -105,7 +107,7 @@ public class ModOptionEditor Name = newName, Priority = maxPriority, }); - _saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1)); + _saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } @@ -129,7 +131,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); UpdateSubModPositions(mod, groupIdx); - _saveService.SaveAllOptionGroups(mod, false); + _saveService.SaveAllOptionGroups(mod, false, _config.ReplaceNonAsciiOnImport); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } @@ -140,7 +142,7 @@ public class ModOptionEditor return; UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - _saveService.SaveAllOptionGroups(mod, false); + _saveService.SaveAllOptionGroups(mod, false, _config.ReplaceNonAsciiOnImport); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } @@ -157,7 +159,7 @@ public class ModOptionEditor MultiModGroup m => m.Description = newDescription, _ => newDescription, }; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } @@ -170,7 +172,7 @@ public class ModOptionEditor return; s.Description = newDescription; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -187,7 +189,7 @@ public class ModOptionEditor MultiModGroup m => m.Priority = newPriority, _ => newPriority, }; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } @@ -204,7 +206,7 @@ public class ModOptionEditor return; m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; } @@ -230,7 +232,7 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -250,7 +252,7 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } @@ -296,7 +298,7 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } @@ -317,7 +319,7 @@ public class ModOptionEditor } group.UpdatePositions(optionIdx); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } @@ -328,7 +330,7 @@ public class ModOptionEditor if (!group.MoveOption(optionIdxFrom, optionIdxTo)) return; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); } @@ -342,7 +344,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData.SetTo(manipulations); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } @@ -355,7 +357,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData.SetTo(replacements); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } @@ -367,7 +369,7 @@ public class ModOptionEditor subMod.FileData.AddFrom(additions); if (oldCount != subMod.FileData.Count) { - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); } } @@ -381,7 +383,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData.SetTo(swaps); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 31fa64ab..9a31cf46 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,16 +16,18 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator(SaveService _saveService, Configuration _config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, +public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, IGamePathParser _gamePathParser) { + public readonly Configuration Config = config; + /// Creates directory and files necessary for a new mod without adding it to the manager. public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "") { try { - var newDir = CreateModFolder(basePath, newName, _config.ReplaceNonAsciiOnImport, true); - _dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty); + var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); + _dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; } @@ -86,7 +88,8 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes - || _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name) != file.FullName; + || _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true) + != Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true)); mod.Groups.Add(group); } else @@ -96,13 +99,13 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, } if (changes) - _saveService.SaveAllOptionGroups(mod, true); + _saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); } /// Load the default option for a given mod. public void LoadDefaultOption(Mod mod) { - var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1); + var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); mod.Default.SetPosition(-1, 0); try { @@ -161,8 +164,8 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, if (!changes) return; - _saveService.SaveAllOptionGroups(mod, false); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default)); + _saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } @@ -188,7 +191,7 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, continue; var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName), - _config.KeepDefaultMetaChanges); + Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -201,7 +204,7 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, continue; var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), - _config.KeepDefaultMetaChanges); + Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -246,7 +249,7 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, DefaultSettings = defaultSettings, }; group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, idx))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index)); + _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -259,7 +262,7 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, DefaultSettings = defaultSettings, }; group.OptionData.AddRange(subMods.OfType()); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index)); + _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -306,7 +309,7 @@ public partial class ModCreator(SaveService _saveService, Configuration _config, } IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 957fe21d..ea5f176c 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -39,8 +39,9 @@ public readonly struct ModSaveGroup : ISavable private readonly IModGroup? _group; private readonly int _groupIdx; private readonly ISubMod? _defaultMod; + private readonly bool _onlyAscii; - public ModSaveGroup(Mod mod, int groupIdx) + public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) { _basePath = mod.ModPath; _groupIdx = groupIdx; @@ -48,24 +49,27 @@ public readonly struct ModSaveGroup : ISavable _defaultMod = mod.Default; else _group = mod.Groups[_groupIdx]; + _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx) + public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) { - _basePath = basePath; - _group = group; - _groupIdx = groupIdx; + _basePath = basePath; + _group = group; + _groupIdx = groupIdx; + _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, ISubMod @default) + public ModSaveGroup(DirectoryInfo basePath, ISubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; _defaultMod = @default; + _onlyAscii = onlyAscii; } public string ToFilename(FilenameService fileNames) - => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty); + => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); public void Save(StreamWriter writer) { diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index dc73b451..52159258 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -86,7 +86,7 @@ public class TemporaryMod : IMod foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) defaultMod.ManipulationData.Add(manip); - saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod)); + saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}."); } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index cf99c6c8..52881b9e 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -60,14 +60,14 @@ public class FilenameService(DalamudPluginInterface pi) => Path.Combine(modDirectory, "meta.json"); /// Obtain the path of the file describing a given option group by its index and the mod. If the index is < 0, return the path for the default mod file. - public string OptionGroupFile(Mod mod, int index) - => OptionGroupFile(mod.ModPath.FullName, index, index >= 0 ? mod.Groups[index].Name : string.Empty); + public string OptionGroupFile(Mod mod, int index, bool onlyAscii) + => OptionGroupFile(mod.ModPath.FullName, index, index >= 0 ? mod.Groups[index].Name : string.Empty, onlyAscii); /// Obtain the path of the file describing a given option group by its index, name and basepath. If the index is < 0, return the path for the default mod file. - public string OptionGroupFile(string basePath, int index, string name) + public string OptionGroupFile(string basePath, int index, string name, bool onlyAscii) { var fileName = index >= 0 - ? $"group_{index + 1:D3}_{name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" + ? $"group_{index + 1:D3}_{ModCreator.ReplaceBadXivSymbols(name.ToLowerInvariant(), onlyAscii)}.json" : "default_mod.json"; return Path.Combine(basePath, fileName); } diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 3f54160d..40dc4107 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -18,7 +18,7 @@ public sealed class SaveService : SaveServiceBase { } /// Immediately delete all existing option group files for a mod and save them anew. - public void SaveAllOptionGroups(Mod mod, bool backup) + public void SaveAllOptionGroups(Mod mod, bool backup, bool onlyAscii) { foreach (var file in FileNames.GetOptionGroupFiles(mod)) { @@ -37,7 +37,7 @@ public sealed class SaveService : SaveServiceBase } for (var i = 0; i < mod.Groups.Count - 1; ++i) - ImmediateSave(new ModSaveGroup(mod, i)); - ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1)); + ImmediateSave(new ModSaveGroup(mod, i, onlyAscii)); + ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii)); } } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index d172cfb9..d5b77bf4 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -53,7 +53,8 @@ public class PenumbraChangelog private static void Add8_3_0(Changelog log) => log.NextVersion("Version 0.8.3.0") .RegisterHighlight("Improved the UI for the On-Screen tabs with highlighting of used paths, filtering and more selections. (by Ny)") - .RegisterEntry("Added an option to replace non-ASCII symbols with underscores for folder paths on mod import since this causes problems on some WINE systems. This option is off by default.") + .RegisterEntry( + "Added an option to replace non-ASCII symbols with underscores for folder paths on mod import since this causes problems on some WINE systems. This option is off by default.") .RegisterEntry( "Added support for the Changed Item Icons to load modded icons, but this depends on a not-yet-released Dalamud update.") .RegisterEntry( diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 18d0e613..ed7a6a67 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -86,7 +86,7 @@ public class ModPanelEditTab : ITab _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(_filenames, _modManager, _mod); + AddOptionGroup.Draw(_filenames, _modManager, _mod, _config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) @@ -235,13 +235,13 @@ public class ModPanelEditTab : ITab public static void Reset() => _newGroupName = string.Empty; - public static void Draw(FilenameService filenames, ModManager modManager, Mod mod) + public static void Draw(FilenameService filenames, ModManager modManager, Mod mod, bool onlyAscii) { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); ImGui.SameLine(); - var defaultFile = filenames.OptionGroupFile(mod, -1); + var defaultFile = filenames.OptionGroupFile(mod, -1, onlyAscii); var fileExists = File.Exists(defaultFile); var tt = fileExists ? "Open the default option json file in the text editor of your choice." @@ -438,7 +438,7 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); ImGui.SameLine(); - var fileName = _filenames.OptionGroupFile(_mod, groupIdx); + var fileName = _filenames.OptionGroupFile(_mod, groupIdx, _config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." From 0514e72d47a271892d9533f7e02eba4c3a076475 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Dec 2023 20:47:18 +0100 Subject: [PATCH 1323/2451] Update sizing for option groups. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index ed7a6a67..20da8fde 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -475,10 +475,11 @@ public class ModPanelEditTab : ITab if (!table) return; - ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, 60 * UiHelpers.Scale); + var maxWidth = ImGui.CalcTextSize("Option #88.").X; + ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, maxWidth); ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, - UiHelpers.InputTextWidth.X - 72 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); + UiHelpers.InputTextWidth.X - maxWidth - 12 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); @@ -644,7 +645,7 @@ public class ModPanelEditTab : ITab _ => "Unknown", }; - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 3 * (UiHelpers.IconButtonSize.X - 4 * UiHelpers.Scale)); + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); if (!combo) return; From 330525048213693cf4d2ad578d2795619537b618 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Dec 2023 14:24:05 +0100 Subject: [PATCH 1324/2451] Misc. --- OtterGui | 2 +- Penumbra/Services/BackupService.cs | 6 +----- Penumbra/Services/ValidityChecker.cs | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 7098e957..5f0eec50 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 7098e9577117a3555f5f6181edae6cd306a4b5d4 +Subproject commit 5f0eec50ea7f7a4727ceab056bc3756f0ed58a30 diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index 7b8ace29..0059cf9f 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -14,11 +14,7 @@ public class BackupService Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); } - public static void CreatePermanentBackup(FilenameService fileNames) - => Backup.CreatePermanentBackup(Penumbra.Log, new DirectoryInfo(fileNames.ConfigDirectory), PenumbraFiles(fileNames), - "pre_ephemeral_config"); - - // Collect all relevant files for penumbra configuration. + /// Collect all relevant files for penumbra configuration. private static IReadOnlyList PenumbraFiles(FilenameService fileNames) { var list = fileNames.CollectionFiles.ToList(); diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 749da5b9..0688850b 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -26,7 +26,7 @@ public class ValidityChecker IsNotInstalledPenumbra = CheckIsNotInstalled(pi); IsValidSourceRepo = CheckSourceRepo(pi); - var assembly = Assembly.GetExecutingAssembly(); + var assembly = GetType().Assembly; Version = assembly.GetName().Version?.ToString() ?? string.Empty; CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; } From 7d612df95156841dcfe0c780be246a9227a98a22 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Dec 2023 11:51:24 +0100 Subject: [PATCH 1325/2451] Update for changed GameData. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/PenumbraApi.cs | 18 ++-- Penumbra/Collections/Cache/CollectionCache.cs | 24 ++--- .../Cache/CollectionCacheManager.cs | 4 +- .../Collections/Manager/ActiveCollections.cs | 10 +- .../Manager/IndividualCollections.Access.cs | 33 +++---- .../Manager/IndividualCollections.Files.cs | 40 ++++---- .../Manager/IndividualCollections.cs | 81 +++++++--------- .../Manager/TempCollectionManager.cs | 8 +- Penumbra/CommandHandler.cs | 4 +- Penumbra/Import/Structs/MetaFileInfo.cs | 3 +- Penumbra/Import/TexToolsMeta.cs | 3 +- .../PathResolving/CollectionResolver.cs | 16 ++-- .../Interop/PathResolving/CutsceneService.cs | 5 +- .../Interop/ResourceTree/ResolveContext.cs | 26 +++-- .../ResourceTree/ResourceTreeFactory.cs | 32 ++++--- .../Interop/ResourceTree/TreeBuildCache.cs | 40 +++----- Penumbra/Interop/Services/RedrawService.cs | 2 +- Penumbra/Meta/MetaFileManager.cs | 5 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 14 +-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 19 ++-- Penumbra/Mods/Manager/ModCacheManager.cs | 31 +++--- Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Penumbra.cs | 26 ++--- Penumbra/Services/BackupService.cs | 4 +- Penumbra/Services/ServiceManager.cs | 40 ++++++-- Penumbra/Services/ServiceWrapper.cs | 30 +----- Penumbra/Services/StainService.cs | 11 +-- Penumbra/Services/Wrappers.cs | 34 ------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 32 +++---- .../ModEditWindow.ShaderPackages.cs | 1 + .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 1 + Penumbra/UI/CollectionTab/CollectionPanel.cs | 8 +- .../CollectionTab/IndividualAssignmentUi.cs | 61 ++++++------ .../UI/ResourceWatcher/ResourceWatcher.cs | 16 ++-- Penumbra/UI/Tabs/CollectionsTab.cs | 5 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 1 + Penumbra/UI/Tabs/{ => Debug}/DebugTab.cs | 95 ++++++++++++------- Penumbra/UI/WindowSystem.cs | 2 +- Penumbra/Util/PerformanceType.cs | 33 ------- Penumbra/packages.lock.json | 3 +- 42 files changed, 374 insertions(+), 455 deletions(-) delete mode 100644 Penumbra/Services/Wrappers.cs rename Penumbra/UI/Tabs/{ => Debug}/DebugTab.cs (94%) diff --git a/OtterGui b/OtterGui index 5f0eec50..bde59c34 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5f0eec50ea7f7a4727ceab056bc3756f0ed58a30 +Subproject commit bde59c34f7108520002c21cdbf21e8ee5b586944 diff --git a/Penumbra.GameData b/Penumbra.GameData index ffdb966f..afc56d9f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f +Subproject commit afc56d9f07a2a54ab791a4c85cf627b6f884aec2 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5ce28510..97e69089 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -95,7 +95,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private DalamudServices _dalamud; private TempCollectionManager _tempCollections; private TempModManager _tempMods; - private ActorService _actors; + private ActorManager _actors; private CollectionResolver _collectionResolver; private CutsceneService _cutsceneService; private ModImportManager _modImportManager; @@ -108,7 +108,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, + TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) { @@ -889,13 +889,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (!_actors.Valid) - return PenumbraApiEc.SystemDisposed; - if (actorIndex < 0 || actorIndex >= _dalamud.Objects.Length) return PenumbraApiEc.InvalidArgument; - var identifier = _actors.AwaitedService.FromObject(_dalamud.Objects[actorIndex], false, false, true); + var identifier = _actors.FromObject(_dalamud.Objects[actorIndex], false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; @@ -1143,11 +1140,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid) + if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) return ActorIdentifier.Invalid; var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx); - return _actors.AwaitedService.FromObject(ptr, out _, false, true, true); + return _actors.FromObject(ptr, out _, false, true, true); } // Resolve a path given by string for a specific collection. @@ -1241,12 +1238,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi // TODO: replace all usages with ActorIdentifier stuff when incrementing API private ActorIdentifier NameToIdentifier(string name, ushort worldId) { - if (!_actors.Valid) - return ActorIdentifier.Invalid; - // Verified to be valid name beforehand. var b = ByteString.FromStringUnsafe(name, false); - return _actors.AwaitedService.CreatePlayer(b, worldId); + return _actors.CreatePlayer(b, worldId); } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 3761424a..a6e5fef5 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -22,10 +22,10 @@ public class CollectionCache : IDisposable private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; public readonly CollectionModData ModData = new(); - public readonly SortedList, object?)> _changedItems = new(); + private readonly SortedList, object?)> _changedItems = []; public readonly ConcurrentDictionary ResolvedFiles = new(); public readonly MetaCache Meta; - public readonly Dictionary> _conflicts = new(); + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -33,10 +33,10 @@ public class CollectionCache : IDisposable => _collection.AnonymizedName; public IEnumerable> AllConflicts - => _conflicts.Values; + => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => _conflicts.TryGetValue(mod, out var c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -195,7 +195,7 @@ public class CollectionCache : IDisposable $"Invalid mod state, removing {mod.Name} and associated manipulation {manipulation} returned current mod {mp.Name}."); } - _conflicts.Remove(mod); + ConflictDict.Remove(mod); foreach (var conflict in conflicts) { if (conflict.HasPriority) @@ -206,9 +206,9 @@ public class CollectionCache : IDisposable { var newConflicts = Conflicts(conflict.Mod2).Remove(c => c.Mod2 == mod); if (newConflicts.Count > 0) - _conflicts[conflict.Mod2] = newConflicts; + ConflictDict[conflict.Mod2] = newConflicts; else - _conflicts.Remove(conflict.Mod2); + ConflictDict.Remove(conflict.Mod2); } } @@ -336,9 +336,9 @@ public class CollectionCache : IDisposable return false; }); if (changedConflicts.Count == 0) - _conflicts.Remove(mod); + ConflictDict.Remove(mod); else - _conflicts[mod] = changedConflicts; + ConflictDict[mod] = changedConflicts; } // Add a new conflict between the added mod and the existing mod. @@ -373,9 +373,9 @@ public class CollectionCache : IDisposable { // Add the same conflict list to both conflict directions. var conflictList = new List { data }; - _conflicts[addedMod] = addedConflicts.Append(new ModConflicts(existingMod, conflictList, existingPriority < addedPriority, + ConflictDict[addedMod] = addedConflicts.Append(new ModConflicts(existingMod, conflictList, existingPriority < addedPriority, existingPriority != addedPriority)); - _conflicts[existingMod] = existingConflicts.Append(new ModConflicts(addedMod, conflictList, + ConflictDict[existingMod] = existingConflicts.Append(new ModConflicts(addedMod, conflictList, existingPriority >= addedPriority, existingPriority != addedPriority)); } @@ -426,7 +426,7 @@ public class CollectionCache : IDisposable _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. - var identifier = _manager.MetaFileManager.Identifier.AwaitedService; + var identifier = _manager.MetaFileManager.Identifier; var items = new SortedList(512); void AddItems(IMod mod) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index a24eb2fa..7d4a5722 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -159,7 +159,7 @@ public class CollectionCacheManager : IDisposable null); cache.ResolvedFiles.Clear(); cache.Meta.Reset(); - cache._conflicts.Clear(); + cache.ConflictDict.Clear(); // Add all forced redirects. foreach (var tempMod in _tempMods.ModsForAllCollections @@ -372,7 +372,7 @@ public class CollectionCacheManager : IDisposable { collection._cache!.ResolvedFiles.Clear(); collection._cache!.Meta.Reset(); - collection._cache!._conflicts.Clear(); + collection._cache!.ConflictDict.Clear(); } } diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 0814da90..38679612 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -28,9 +28,9 @@ public class ActiveCollections : ISavable, IDisposable private readonly CommunicatorService _communicator; private readonly SaveService _saveService; private readonly ActiveCollectionData _data; - private readonly ActorService _actors; + private readonly ActorManager _actors; - public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, + public ActiveCollections(Configuration config, CollectionStorage storage, ActorManager actors, CommunicatorService communicator, SaveService saveService, ActiveCollectionData data) { _storage = storage; @@ -475,7 +475,7 @@ public class ActiveCollections : ISavable, IDisposable { case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: { - var global = ByType(CollectionType.Individual, _actors.AwaitedService.CreatePlayer(id.PlayerName, ushort.MaxValue)); + var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); return global?.Index == checkAssignment.Index ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." : string.Empty; @@ -484,12 +484,12 @@ public class ActiveCollections : ISavable, IDisposable if (id.HomeWorld != ushort.MaxValue) { var global = ByType(CollectionType.Individual, - _actors.AwaitedService.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + _actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); if (global?.Index == checkAssignment.Index) return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; } - var unowned = ByType(CollectionType.Individual, _actors.AwaitedService.CreateNpc(id.Kind, id.DataId)); + var unowned = ByType(CollectionType.Individual, _actors.CreateNpc(id.Kind, id.DataId)); return unowned?.Index == checkAssignment.Index ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." : string.Empty; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 78eff98c..785f0013 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.String; namespace Penumbra.Collections.Manager; @@ -36,7 +37,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return true; if (identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && _config.UseOwnerNameForCharacterCollection) - return CheckWorlds(_actorService.AwaitedService.GetCurrentPlayer(), out collection); + return CheckWorlds(_actors.GetCurrentPlayer(), out collection); break; } @@ -46,7 +47,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return true; // Handle generic NPC - var npcIdentifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, + var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId); if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) @@ -56,7 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (!_config.UseOwnerNameForCharacterCollection) return false; - identifier = _actorService.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); @@ -89,37 +90,37 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (identifier.Type != IdentifierType.Special) return (identifier, SpecialResult.Invalid); - if (_actorService.AwaitedService.ResolvePartyBannerPlayer(identifier.Special, out var id)) + if (_actors.ResolvePartyBannerPlayer(identifier.Special, out var id)) return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.PartyBanner) : (identifier, SpecialResult.Invalid); - if (_actorService.AwaitedService.ResolvePvPBannerPlayer(identifier.Special, out id)) + if (_actors.ResolvePvPBannerPlayer(identifier.Special, out id)) return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.PvPBanner) : (identifier, SpecialResult.Invalid); - if (_actorService.AwaitedService.ResolveMahjongPlayer(identifier.Special, out id)) + if (_actors.ResolveMahjongPlayer(identifier.Special, out id)) return _config.UseCharacterCollectionsInCards ? (id, SpecialResult.Mahjong) : (identifier, SpecialResult.Invalid); switch (identifier.Special) { case ScreenActor.CharacterScreen when _config.UseCharacterCollectionInMainWindow: - return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.CharacterScreen); + return (_actors.GetCurrentPlayer(), SpecialResult.CharacterScreen); case ScreenActor.FittingRoom when _config.UseCharacterCollectionInTryOn: - return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.FittingRoom); + return (_actors.GetCurrentPlayer(), SpecialResult.FittingRoom); case ScreenActor.DyePreview when _config.UseCharacterCollectionInTryOn: - return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.DyePreview); + return (_actors.GetCurrentPlayer(), SpecialResult.DyePreview); case ScreenActor.Portrait when _config.UseCharacterCollectionsInCards: - return (_actorService.AwaitedService.GetCurrentPlayer(), SpecialResult.Portrait); + return (_actors.GetCurrentPlayer(), SpecialResult.Portrait); case ScreenActor.ExamineScreen: { - identifier = _actorService.AwaitedService.GetInspectPlayer(); + identifier = _actors.GetInspectPlayer(); if (identifier.IsValid) return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect); - identifier = _actorService.AwaitedService.GetCardPlayer(); + identifier = _actors.GetCardPlayer(); if (identifier.IsValid) return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card); return _config.UseCharacterCollectionInTryOn - ? (_actorService.AwaitedService.GetGlamourPlayer(), SpecialResult.Glamour) + ? (_actors.GetGlamourPlayer(), SpecialResult.Glamour) : (identifier, SpecialResult.Invalid); } default: return (identifier, SpecialResult.Invalid); @@ -127,10 +128,10 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa } public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection) - => TryGetCollection(_actorService.AwaitedService.FromObject(gameObject, true, false, false), out collection); + => TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection); public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection) - => TryGetCollection(_actorService.AwaitedService.FromObject(gameObject, out _, true, false, false), out collection); + => TryGetCollection(_actors.FromObject(gameObject, out _, true, false, false), out collection); private bool CheckWorlds(ActorIdentifier identifier, out ModCollection? collection) { @@ -143,7 +144,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa if (_individuals.TryGetValue(identifier, out collection)) return true; - identifier = _actorService.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, + identifier = _actors.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId); if (identifier.IsValid && _individuals.TryGetValue(identifier, out collection)) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 45a1d98c..dc20da1e 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -3,6 +3,8 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Actors; +using Penumbra.GameData.DataContainers.Bases; +using Penumbra.GameData.Structs; using Penumbra.Services; using Penumbra.String; @@ -26,23 +28,20 @@ public partial class IndividualCollections public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage) { - if (_actorService.Valid) + if (_actors.Awaiter.IsCompletedSuccessfully) { var ret = ReadJObjectInternal(obj, storage); return ret; } - void Func() + Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready..."); + _actors.Awaiter.ContinueWith(_ => { if (ReadJObjectInternal(obj, storage)) saver.ImmediateSave(parent); IsLoaded = true; Loaded.Invoke(); - _actorService.FinishedCreation -= Func; - } - - Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready..."); - _actorService.FinishedCreation += Func; + }); return false; } @@ -60,7 +59,7 @@ public partial class IndividualCollections { try { - var identifier = _actorService.AwaitedService.FromJson(data as JObject); + var identifier = _actors.FromJson(data as JObject); var group = GetGroup(identifier); if (group.Length == 0 || group.Any(i => !i.IsValid)) { @@ -101,10 +100,10 @@ public partial class IndividualCollections internal void Migrate0To1(Dictionary old) { - static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) + static bool FindDataId(string name, NameDictionary data, out NpcId dataId) { var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), - new KeyValuePair(uint.MaxValue, string.Empty)); + new KeyValuePair(uint.MaxValue, string.Empty)); dataId = kvp.Key; return kvp.Value.Length > 0; } @@ -114,22 +113,22 @@ public partial class IndividualCollections var kind = ObjectKind.None; var lowerName = name.ToLowerInvariant(); // Prefer matching NPC names, fewer false positives than preferring players. - if (FindDataId(lowerName, _actorService.AwaitedService.Data.Companions, out var dataId)) + if (FindDataId(lowerName, _actors.Data.Companions, out var dataId)) kind = ObjectKind.Companion; - else if (FindDataId(lowerName, _actorService.AwaitedService.Data.Mounts, out dataId)) + else if (FindDataId(lowerName, _actors.Data.Mounts, out dataId)) kind = ObjectKind.MountType; - else if (FindDataId(lowerName, _actorService.AwaitedService.Data.BNpcs, out dataId)) + else if (FindDataId(lowerName, _actors.Data.BNpcs, out dataId)) kind = ObjectKind.BattleNpc; - else if (FindDataId(lowerName, _actorService.AwaitedService.Data.ENpcs, out dataId)) + else if (FindDataId(lowerName, _actors.Data.ENpcs, out dataId)) kind = ObjectKind.EventNpc; - var identifier = _actorService.AwaitedService.CreateNpc(kind, dataId); + var identifier = _actors.CreateNpc(kind, dataId); if (identifier.IsValid) { // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. var group = GetGroup(identifier); var ids = string.Join(", ", group.Select(i => i.DataId.ToString())); - if (Add($"{_actorService.AwaitedService.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) + if (Add($"{_actors.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else Penumbra.Messager.NotificationMessage( @@ -137,15 +136,12 @@ public partial class IndividualCollections NotificationType.Error); } // If it is not a valid NPC name, check if it can be a player name. - else if (ActorManager.VerifyPlayerName(name)) + else if (ActorIdentifierFactory.VerifyPlayerName(name)) { - identifier = _actorService.AwaitedService.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); + identifier = _actors.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); // Try to migrate the player name without logging full names. - if (Add($"{name} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})", new[] - { - identifier, - }, collection)) + if (Add($"{name} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", [identifier], collection)) Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); else Penumbra.Messager.NotificationMessage( diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 31695a94..67ab0b21 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -1,7 +1,9 @@ using Dalamud.Game.ClientState.Objects.Enums; using OtterGui.Filesystem; using Penumbra.GameData.Actors; -using Penumbra.Services; +using Penumbra.GameData.DataContainers.Bases; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.String; namespace Penumbra.Collections.Manager; @@ -11,9 +13,9 @@ public sealed partial class IndividualCollections public record struct IndividualAssignment(string DisplayName, IReadOnlyList Identifiers, ModCollection Collection); private readonly Configuration _config; - private readonly ActorService _actorService; - private readonly Dictionary _individuals = new(); - private readonly List _assignments = new(); + private readonly ActorManager _actors; + private readonly Dictionary _individuals = []; + private readonly List _assignments = []; public event Action Loaded; public bool IsLoaded { get; private set; } @@ -21,12 +23,12 @@ public sealed partial class IndividualCollections public IReadOnlyList Assignments => _assignments; - public IndividualCollections(ActorService actorService, Configuration config, bool temporary) + public IndividualCollections(ActorManager actors, Configuration config, bool temporary) { - _config = config; - _actorService = actorService; - IsLoaded = temporary; - Loaded += () => Penumbra.Log.Information($"{_assignments.Count} Individual Assignments loaded after delay."); + _config = config; + _actors = actors; + IsLoaded = temporary; + Loaded += () => Penumbra.Log.Information($"{_assignments.Count} Individual Assignments loaded after delay."); } public enum AddResult @@ -69,44 +71,34 @@ public sealed partial class IndividualCollections return set ? AddResult.AlreadySet : AddResult.Valid; } - public AddResult CanAdd(IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable dataIds, + public AddResult CanAdd(IdentifierType type, string name, WorldId homeWorld, ObjectKind kind, IEnumerable dataIds, out ActorIdentifier[] identifiers) { - identifiers = Array.Empty(); + identifiers = []; - var manager = _actorService.AwaitedService; switch (type) { case IdentifierType.Player: if (!ByteString.FromString(name, out var playerName)) return AddResult.Invalid; - identifiers = new[] - { - manager.CreatePlayer(playerName, homeWorld), - }; + identifiers = [_actors.CreatePlayer(playerName, homeWorld)]; break; case IdentifierType.Retainer: if (!ByteString.FromString(name, out var retainerName)) return AddResult.Invalid; - identifiers = new[] - { - manager.CreateRetainer(retainerName, ActorIdentifier.RetainerType.Both), - }; + identifiers = [_actors.CreateRetainer(retainerName, ActorIdentifier.RetainerType.Both)]; break; case IdentifierType.Owned: if (!ByteString.FromString(name, out var ownerName)) return AddResult.Invalid; - identifiers = dataIds.Select(id => manager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); + identifiers = dataIds.Select(id => _actors.CreateOwned(ownerName, homeWorld, kind, id)).ToArray(); break; case IdentifierType.Npc: identifiers = dataIds - .Select(id => manager.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray(); - break; - default: - identifiers = Array.Empty(); + .Select(id => _actors.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray(); break; } @@ -116,12 +108,22 @@ public sealed partial class IndividualCollections public ActorIdentifier[] GetGroup(ActorIdentifier identifier) { if (!identifier.IsValid) - return Array.Empty(); + return []; + + return identifier.Type switch + { + IdentifierType.Player => [identifier.CreatePermanent()], + IdentifierType.Special => [identifier], + IdentifierType.Retainer => [identifier.CreatePermanent()], + IdentifierType.Owned => CreateNpcs(_actors, identifier.CreatePermanent()), + IdentifierType.Npc => CreateNpcs(_actors, identifier), + _ => [], + }; static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) { var name = manager.Data.ToName(identifier.Kind, identifier.DataId); - var table = identifier.Kind switch + NameDictionary table = identifier.Kind switch { ObjectKind.BattleNpc => manager.Data.BNpcs, ObjectKind.EventNpc => manager.Data.ENpcs, @@ -134,25 +136,6 @@ public sealed partial class IndividualCollections .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, identifier.Kind, kvp.Key)).ToArray(); } - - return identifier.Type switch - { - IdentifierType.Player => new[] - { - identifier.CreatePermanent(), - }, - IdentifierType.Special => new[] - { - identifier, - }, - IdentifierType.Retainer => new[] - { - identifier.CreatePermanent(), - }, - IdentifierType.Owned => CreateNpcs(_actorService.AwaitedService, identifier.CreatePermanent()), - IdentifierType.Npc => CreateNpcs(_actorService.AwaitedService, identifier), - _ => Array.Empty(), - }; } internal bool Add(ActorIdentifier[] identifiers, ModCollection collection) @@ -241,12 +224,12 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => - $"{identifier.PlayerName} ({_actorService.AwaitedService.Data.ToWorldName(identifier.HomeWorld)})'s {_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)}", + $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})'s {_actors.Data.ToName(identifier.Kind, identifier.DataId)}", IdentifierType.Npc => - $"{_actorService.AwaitedService.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", + $"{_actors.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})", _ => string.Empty, }; } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index d0edf19b..5d9de13d 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -14,10 +14,10 @@ public class TempCollectionManager : IDisposable private readonly CommunicatorService _communicator; private readonly CollectionStorage _storage; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly Dictionary _customCollections = new(); - public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorService actors, CollectionStorage storage) + public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage) { _communicator = communicator; _actors = actors; @@ -111,7 +111,7 @@ public class TempCollectionManager : IDisposable if (!ByteString.FromString(characterName, out var byteString, false)) return false; - var identifier = _actors.AwaitedService.CreatePlayer(byteString, worldId); + var identifier = _actors.CreatePlayer(byteString, worldId); if (!identifier.IsValid) return false; @@ -123,7 +123,7 @@ public class TempCollectionManager : IDisposable if (!ByteString.FromString(characterName, out var byteString, false)) return false; - var identifier = _actors.AwaitedService.CreatePlayer(byteString, worldId); + var identifier = _actors.CreatePlayer(byteString, worldId); return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); } } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index c151e7e4..537b08da 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -32,7 +32,7 @@ public class CommandHandler : IDisposable public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, Configuration config, - ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra, + ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra, CollectionEditor collectionEditor) { _commandManager = commandManager; @@ -41,7 +41,7 @@ public class CommandHandler : IDisposable _configWindow = configWindow; _modManager = modManager; _collectionManager = collectionManager; - _actors = actors.AwaitedService; + _actors = actors; _chat = chat; _penumbra = penumbra; _collectionEditor = collectionEditor; diff --git a/Penumbra/Import/Structs/MetaFileInfo.cs b/Penumbra/Import/Structs/MetaFileInfo.cs index f7c9b419..693c77b1 100644 --- a/Penumbra/Import/Structs/MetaFileInfo.cs +++ b/Penumbra/Import/Structs/MetaFileInfo.cs @@ -1,5 +1,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData; +using Penumbra.GameData.Data; namespace Penumbra.Import.Structs; @@ -47,7 +48,7 @@ public partial struct MetaFileInfo _ => false, }; - public MetaFileInfo(IGamePathParser parser, string fileName) + public MetaFileInfo(GamePathParser parser, string fileName) { // Set the primary type from the gamePath start. PrimaryType = parser.PathToObjectType(fileName); diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 1108c965..83b430fb 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,4 +1,5 @@ using Penumbra.GameData; +using Penumbra.GameData.Data; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; @@ -28,7 +29,7 @@ public partial class TexToolsMeta private readonly MetaFileManager _metaFileManager; - public TexToolsMeta(MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault) + public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault) { _metaFileManager = metaFileManager; _keepDefault = keepDefault; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index ecd4eb2e..fe51a5cd 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.Services; using Penumbra.Util; @@ -21,7 +21,7 @@ public unsafe class CollectionResolver private readonly IClientState _clientState; private readonly IGameGui _gameGui; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly CutsceneService _cutscenes; private readonly Configuration _config; @@ -30,7 +30,7 @@ public unsafe class CollectionResolver private readonly DrawObjectState _drawObjectState; public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, IGameGui gameGui, - ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, + ActorManager actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels) { _performance = performance; @@ -58,7 +58,7 @@ public unsafe class CollectionResolver return _collectionManager.Active.ByType(CollectionType.Yourself) ?? _collectionManager.Active.Default; - var player = _actors.AwaitedService.GetCurrentPlayer(); + var player = _actors.GetCurrentPlayer(); var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) @@ -147,7 +147,7 @@ public unsafe class CollectionResolver return false; } - var player = _actors.AwaitedService.GetCurrentPlayer(); + var player = _actors.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) ?? _collectionManager.Active.ByType(CollectionType.Yourself) @@ -163,7 +163,7 @@ public unsafe class CollectionResolver /// private ResolveData DefaultState(GameObject* gameObject) { - var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false); + var identifier = _actors.FromObject(gameObject, out var owner, true, false, false); if (identifier.Type is IdentifierType.Special) { (identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); @@ -193,7 +193,7 @@ public unsafe class CollectionResolver { if (actor->ObjectIndex == 0 || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 - || identifier.Equals(_actors.AwaitedService.GetCurrentPlayer())) + || identifier.Equals(_actors.GetCurrentPlayer())) return _collectionManager.Active.ByType(CollectionType.Yourself); return null; @@ -242,7 +242,7 @@ public unsafe class CollectionResolver if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) return null; - var id = _actors.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, + var id = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckYourself(id, owner) diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index add121b6..18c016b9 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.Interop.Services; namespace Penumbra.Interop.PathResolving; @@ -45,6 +45,9 @@ public class CutsceneService : IDisposable /// Return the currently set index of a parent or -1 if none is set or the index is invalid. public int GetParentIndex(int idx) + => GetParentIndex((ushort)idx); + + public short GetParentIndex(ushort idx) { if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) return _copiedCharacters[idx - CutsceneStartIdx]; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 5a5ecdd9..d03ee508 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -7,6 +7,7 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String; @@ -19,7 +20,7 @@ using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.M namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData) +internal record GlobalResolveContext(ObjectIdentification Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData) { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); @@ -28,8 +29,13 @@ internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection => new(this, characterBase, slotIndex, slot, equipment, weaponType); } -internal partial record ResolveContext(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, - EquipSlot Slot, CharacterArmor Equipment, WeaponType WeaponType) +internal partial record ResolveContext( + GlobalResolveContext Global, + Pointer CharacterBase, + uint SlotIndex, + EquipSlot Slot, + CharacterArmor Equipment, + WeaponType WeaponType) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -152,6 +158,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerModelResourceHandle == null) return null; + var mdlResource = mdl->ModelResourceHandle; var path = ResolveModelPath(); @@ -224,6 +231,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, Pointer "L: ", _ => string.Empty, } - + item.Name.ToString(); + + item.Name; return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } - var dataFromPath = GuessUIDataFromPath(gamePath); + var dataFromPath = GuessUiDataFromPath(gamePath); if (dataFromPath.Name != null) return dataFromPath; @@ -337,7 +345,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, Pointer tree.FlatNodes.Add(node)); @@ -161,9 +163,9 @@ public class ResourceTreeFactory var gamePath = node.PossibleGamePaths[0]; node.SetUiData(node.Type switch { - ResourceType.Imc => node.ResolveContext!.GuessModelUIData(gamePath).PrependName("IMC: "), - ResourceType.Mdl => node.ResolveContext!.GuessModelUIData(gamePath), - _ => node.ResolveContext!.GuessUIDataFromPath(gamePath), + ResourceType.Imc => node.ResolveContext!.GuessModelUiData(gamePath).PrependName("IMC: "), + ResourceType.Mdl => node.ResolveContext!.GuessModelUiData(gamePath), + _ => node.ResolveContext!.GuessUiDataFromPath(gamePath), }); } @@ -215,7 +217,7 @@ public class ResourceTreeFactory private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache) { - var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = _actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); switch (identifier.Type) { case IdentifierType.Player: return (identifier.PlayerName.ToString(), true); diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 9614e9aa..7582c753 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,36 +1,26 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal readonly struct TreeBuildCache +internal readonly struct TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorManager actors) { - private readonly IDataManager _dataManager; - private readonly ActorService _actors; - private readonly Dictionary _shaderPackages = new(); - private readonly IObjectTable _objects; - - public TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorService actors) - { - _dataManager = dataManager; - _objects = objects; - _actors = actors; - } + private readonly Dictionary _shaderPackages = []; public unsafe bool IsLocalPlayerRelated(Character character) { - var player = _objects[0]; + var player = objects[0]; if (player == null) return false; var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)character.Address; - var parent = _actors.AwaitedService.ToCutsceneParent(gameObject->ObjectIndex); + var parent = actors.ToCutsceneParent(gameObject->ObjectIndex); var actualIndex = parent >= 0 ? (ushort)parent : gameObject->ObjectIndex; return actualIndex switch { @@ -41,38 +31,38 @@ internal readonly struct TreeBuildCache } public IEnumerable GetCharacters() - => _objects.OfType(); + => objects.OfType(); public IEnumerable GetLocalPlayerRelatedCharacters() { - var player = _objects[0]; + var player = objects[0]; if (player == null) yield break; yield return (Character)player; - var minion = _objects[1]; + var minion = objects[1]; if (minion != null) yield return (Character)minion; var playerId = player.ObjectId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { - if (_objects[i] is Character owned && owned.OwnerId == playerId) + if (objects[i] is Character owned && owned.OwnerId == playerId) yield return owned; } for (var i = ObjectIndex.CutsceneStart.Index; i < ObjectIndex.CharacterScreen.Index; ++i) { - var character = _objects[i] as Character; + var character = objects[i] as Character; if (character == null) continue; - var parent = _actors.AwaitedService.ToCutsceneParent(i); + var parent = actors.ToCutsceneParent(i); if (parent < 0) continue; - if (parent is 0 or 1 || _objects[parent]?.OwnerId == playerId) + if (parent is 0 or 1 || objects[parent]?.OwnerId == playerId) yield return character; } } @@ -85,11 +75,11 @@ internal readonly struct TreeBuildCache private unsafe bool GetOwnedId(ByteString playerName, uint playerId, int idx, [NotNullWhen(true)] out Character? character) { - character = _objects[idx] as Character; + character = objects[idx] as Character; if (character == null) return false; - var actorId = _actors.AwaitedService.FromObject(character, out var owner, true, true, true); + var actorId = actors.FromObject(character, out var owner, true, true, true); if (!actorId.IsValid) return false; if (owner != null && owner->OwnerID != playerId) @@ -102,7 +92,7 @@ internal readonly struct TreeBuildCache /// Try to read a shpk file from the given path and cache it on success. public ShpkFile? ReadShaderPackage(FullPath path) - => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); + => ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func parseFile) where T : class diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 7a73857a..e2e57b1c 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -9,7 +9,7 @@ using FFXIVClientStructs.Interop; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 9c42b9fc..5283f77e 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -5,6 +5,7 @@ using OtterGui.Compression; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData; +using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; @@ -24,11 +25,11 @@ public unsafe class MetaFileManager internal readonly IDataManager GameData; internal readonly ActiveCollectionData ActiveCollections; internal readonly ValidityChecker ValidityChecker; - internal readonly IdentifierService Identifier; + internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, - ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier, + ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, FileCompactor compactor, IGameInteropProvider interop) { CharacterUtility = characterUtility; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3d8ab1b6..d634349e 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,5 +1,4 @@ using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -41,7 +40,7 @@ public static class EquipmentSwap : Array.Empty(); } - public static EquipItem[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, Func manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { @@ -99,7 +98,7 @@ public static class EquipmentSwap return affectedItems; } - public static EquipItem[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, Func manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { @@ -247,7 +246,7 @@ public static class EquipmentSwap variant = i.Variant; } - private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, + private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, Variant variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); @@ -256,11 +255,8 @@ public static class EquipmentSwap Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray(); - variants = new[] - { - variantFrom, - }; + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + variants = [variantFrom]; } else { diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 1db890ed..9ca02c4e 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,5 +1,5 @@ -using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -7,17 +7,16 @@ using Penumbra.String.Classes; using Penumbra.Meta; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; -using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; public class ItemSwapContainer { - private readonly MetaFileManager _manager; - private readonly IdentifierService _identifier; + private readonly MetaFileManager _manager; + private readonly ObjectIdentification _identifier; - private Dictionary _modRedirections = new(); - private HashSet _modManipulations = new(); + private Dictionary _modRedirections = []; + private HashSet _modManipulations = []; public IReadOnlyDictionary ModRedirections => _modRedirections; @@ -25,7 +24,7 @@ public class ItemSwapContainer public IReadOnlySet ModManipulations => _modManipulations; - public readonly List Swaps = new(); + public readonly List Swaps = []; public bool Loaded { get; private set; } @@ -107,7 +106,7 @@ public class ItemSwapContainer } } - public ItemSwapContainer(MetaFileManager manager, IdentifierService identifier) + public ItemSwapContainer(MetaFileManager manager, ObjectIdentification identifier) { _manager = manager; _identifier = identifier; @@ -130,7 +129,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateItemSwap(_manager, _identifier.AwaitedService, Swaps, PathResolver(collection), MetaResolver(collection), + var ret = EquipmentSwap.CreateItemSwap(_manager, _identifier, Swaps, PathResolver(collection), MetaResolver(collection), from, to, useRightRing, useLeftRing); Loaded = true; return ret; @@ -140,7 +139,7 @@ public class ItemSwapContainer { Swaps.Clear(); Loaded = false; - var ret = EquipmentSwap.CreateTypeSwap(_manager, _identifier.AwaitedService, Swaps, PathResolver(collection), MetaResolver(collection), + var ret = EquipmentSwap.CreateTypeSwap(_manager, _identifier, Swaps, PathResolver(collection), MetaResolver(collection), slotFrom, from, slotTo, to); Loaded = true; return ret; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index afd42f95..0af78431 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,5 +1,4 @@ using Penumbra.Communication; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; @@ -9,11 +8,12 @@ namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable { - private readonly CommunicatorService _communicator; - private readonly IdentifierService _identifier; - private readonly ModStorage _modManager; + private readonly CommunicatorService _communicator; + private readonly ObjectIdentification _identifier; + private readonly ModStorage _modManager; + private bool _updatingItems = false; - public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModStorage modStorage) + public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage) { _communicator = communicator; _identifier = identifier; @@ -23,8 +23,7 @@ public class ModCacheManager : IDisposable _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager); _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager); _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager); - if (!identifier.Valid) - identifier.FinishedCreation += OnIdentifierCreation; + identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation()); OnModDiscoveryFinished(); } @@ -37,7 +36,7 @@ public class ModCacheManager : IDisposable } /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void ComputeChangedItems(IObjectIdentifier identifier, IDictionary changedItems, MetaManipulation manip) + public static void ComputeChangedItems(ObjectIdentification identifier, IDictionary changedItems, MetaManipulation manip) { switch (manip.ManipulationType) { @@ -155,10 +154,7 @@ public class ModCacheManager : IDisposable => Parallel.ForEach(_modManager, Refresh); private void OnIdentifierCreation() - { - Parallel.ForEach(_modManager, UpdateChangedItems); - _identifier.FinishedCreation -= OnIdentifierCreation; - } + => Parallel.ForEach(_modManager, UpdateChangedItems); private static void UpdateFileCount(Mod mod) => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); @@ -177,18 +173,23 @@ public class ModCacheManager : IDisposable private void UpdateChangedItems(Mod mod) { + if (_updatingItems) + return; + + _updatingItems = true; var changedItems = (SortedList)mod.ChangedItems; changedItems.Clear(); - if (!_identifier.Valid) + if (!_identifier.Awaiter.IsCompletedSuccessfully) return; foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) - _identifier.AwaitedService.Identify(changedItems, gamePath.ToString()); + _identifier.Identify(changedItems, gamePath.ToString()); foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) - ComputeChangedItems(_identifier.AwaitedService, changedItems, manip); + ComputeChangedItems(_identifier, changedItems, manip); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + _updatingItems = false; } private static void UpdateCounts(Mod mod) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 9a31cf46..042c98b4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -5,7 +5,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.GameData; +using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; @@ -17,7 +17,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, - IGamePathParser _gamePathParser) + GamePathParser _gamePathParser) { public readonly Configuration Config = config; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index bcf94ed1..9be40e66 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin; +using Dalamud.Plugin.Services; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.DependencyInjection; @@ -6,7 +7,6 @@ using OtterGui; using OtterGui.Log; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.Util; using Penumbra.Collections; using Penumbra.Collections.Cache; using Penumbra.Interop.ResourceLoading; @@ -19,6 +19,7 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.UI; @@ -31,7 +32,7 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static readonly Logger Log = new(); + public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; private readonly ValidityChecker _validityChecker; @@ -53,10 +54,8 @@ public class Penumbra : IDalamudPlugin { try { - var startTimer = new StartTracker(); - using var timer = startTimer.Measure(StartTimeType.Total); - _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); - Messager = _services.GetRequiredService(); + _services = ServiceManager.CreateProvider(this, pluginInterface, Log); + Messager = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() @@ -74,14 +73,11 @@ public class Penumbra : IDalamudPlugin _tempCollections = _services.GetRequiredService(); _redrawService = _services.GetRequiredService(); _communicatorService = _services.GetRequiredService(); - _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); - using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) - { - _services.GetRequiredService(); - } + _services.GetRequiredService(); _services.GetRequiredService(); @@ -108,8 +104,7 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { - using var timer = _services.GetRequiredService().Measure(StartTimeType.Api); - var api = _services.GetRequiredService(); + var api = _services.GetRequiredService(); _services.GetRequiredService(); _communicatorService.ChangedItemHover.Subscribe(it => { @@ -128,8 +123,7 @@ public class Penumbra : IDalamudPlugin { AsyncTask.Run(() => { - using var tInterface = _services.GetRequiredService().Measure(StartTimeType.Interface); - var system = _services.GetRequiredService(); + var system = _services.GetRequiredService(); system.Window.Setup(this, _services.GetRequiredService()); _services.GetRequiredService(); if (!_disposed) diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index 0059cf9f..e8684f9d 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -1,15 +1,13 @@ using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Log; -using Penumbra.Util; namespace Penumbra.Services; public class BackupService { - public BackupService(Logger logger, StartTracker timer, FilenameService fileNames) + public BackupService(Logger logger, FilenameService fileNames) { - using var t = timer.Measure(StartTimeType.Backup); var files = PenumbraFiles(fileNames); Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); } diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 73be8834..049ab328 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -7,7 +7,10 @@ using Penumbra.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; +using Penumbra.GameData.Actors; using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.DataContainers.Bases; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; @@ -24,17 +27,17 @@ using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; +using Penumbra.UI.Tabs.Debug; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.Services; public static class ServiceManager { - public static ServiceProvider CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log, StartTracker startTimer) + public static ServiceProvider CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { var services = new ServiceCollection() .AddSingleton(log) - .AddSingleton(startTimer) .AddSingleton(penumbra) .AddDalamud(pi) .AddMeta() @@ -47,7 +50,9 @@ public static class ServiceManager .AddResolvers() .AddInterface() .AddModEditor() - .AddApi(); + .AddApi() + .AddDataContainers() + .AddAsyncServices(); return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } @@ -59,6 +64,22 @@ public static class ServiceManager return services; } + private static IServiceCollection AddDataContainers(this IServiceCollection services) + { + foreach (var type in typeof(IDataContainer).Assembly.GetExportedTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IDataContainer)))) + services.AddSingleton(type); + return services; + } + + private static IServiceCollection AddAsyncServices(this IServiceCollection services) + { + foreach (var type in typeof(IDataContainer).Assembly.GetExportedTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncService)))) + services.AddSingleton(type); + return services; + } + private static IServiceCollection AddMeta(this IServiceCollection services) => services.AddSingleton() .AddSingleton() @@ -71,17 +92,19 @@ public static class ServiceManager private static IServiceCollection AddGameData(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) => services.AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton(p => + { + var cutsceneService = p.GetRequiredService(); + return new CutsceneResolver(cutsceneService.GetParentIndex); + }) .AddSingleton() .AddSingleton() .AddSingleton() @@ -173,7 +196,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(p => new Diagnostics(p)); private static IServiceCollection AddModEditor(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 67ddb63d..37acdfd0 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,5 +1,4 @@ using OtterGui.Tasks; -using Penumbra.Util; namespace Penumbra.Services; @@ -12,10 +11,9 @@ public abstract class SyncServiceWrapper : IDisposable public bool Valid => !_isDisposed; - protected SyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) + protected SyncServiceWrapper(string name, Func factory) { Name = name; - using var timer = tracker.Measure(type); Service = factory(); Penumbra.Log.Verbose($"[{Name}] Created."); } @@ -54,32 +52,6 @@ public abstract class AsyncServiceWrapper : IDisposable private bool _isDisposed; - protected AsyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func factory) - { - Name = name; - _task = TrackedTask.Run(() => - { - using var timer = tracker.Measure(type); - var service = factory(); - if (_isDisposed) - { - if (service is IDisposable d) - d.Dispose(); - } - else - { - Service = service; - Penumbra.Log.Verbose($"[{Name}] Created."); - _task = null; - } - }); - _task.ContinueWith((t, x) => - { - if (!_isDisposed) - FinishedCreation?.Invoke(); - }, null); - } - protected AsyncServiceWrapper(string name, Func factory) { Name = name; diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index a0bca570..d11e4d64 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -4,7 +4,7 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Widgets; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; using Penumbra.UI.AdvancedWindow; using Penumbra.Util; @@ -71,17 +71,16 @@ public class StainService : IDisposable } } - public readonly StainData StainData; + public readonly DictStains StainData; public readonly FilterComboColors StainCombo; public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, IDataManager dataManager, IPluginLog dalamudLog) + public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, IPluginLog dalamudLog) { - using var t = timer.Measure(StartTimeType.Stains); - StainData = new StainData(pluginInterface, dataManager, dataManager.Language, dalamudLog); + StainData = new DictStains(pluginInterface, dalamudLog, dataManager); StainCombo = new FilterComboColors(140, - () => StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); diff --git a/Penumbra/Services/Wrappers.cs b/Penumbra/Services/Wrappers.cs deleted file mode 100644 index b1f17d4d..00000000 --- a/Penumbra/Services/Wrappers.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Dalamud.Game; -using Dalamud.Plugin; -using Dalamud.Plugin.Services; -using Penumbra.GameData; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; -using Penumbra.Interop.PathResolving; -using Penumbra.Util; - -namespace Penumbra.Services; - -public sealed class IdentifierService : AsyncServiceWrapper -{ - public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, IDataManager data, ItemService items, IPluginLog log) - : base(nameof(IdentifierService), tracker, StartTimeType.Identifier, - () => GameData.GameData.GetIdentifier(pi, data, items.AwaitedService, log)) - { } -} - -public sealed class ItemService : AsyncServiceWrapper -{ - public ItemService(StartTracker tracker, DalamudPluginInterface pi, IDataManager gameData, IPluginLog log) - : base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language, log)) - { } -} - -public sealed class ActorService : AsyncServiceWrapper -{ - public ActorService(StartTracker tracker, DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, - IFramework framework, IDataManager gameData, IGameGui gui, CutsceneService cutscene, IPluginLog log, IGameInteropProvider interop) - : base(nameof(ActorService), tracker, StartTimeType.Actors, - () => new ActorManager(pi, objects, clientState, framework, interop, gameData, gui, idx => (short)cutscene.GetParentIndex(idx), log)) - { } -} diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 5347208e..0ec11542 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,8 +1,5 @@ -using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Utility; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -11,6 +8,7 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -27,16 +25,14 @@ public class ItemSwapTab : IDisposable, ITab { private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly ItemService _itemService; private readonly CollectionManager _collectionManager; private readonly ModManager _modManager; private readonly MetaFileManager _metaFileManager; - public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, - ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager, Configuration config) + public ItemSwapTab(CommunicatorService communicator, ItemData itemService, CollectionManager collectionManager, + ModManager modManager, ObjectIdentification identifier, MetaFileManager metaFileManager, Configuration config) { _communicator = communicator; - _itemService = itemService; _collectionManager = collectionManager; _modManager = modManager; _metaFileManager = metaFileManager; @@ -46,15 +42,15 @@ public class ItemSwapTab : IDisposable, ITab _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(_itemService, FullEquipType.Head), new ItemSelector(_itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(_itemService, FullEquipType.Body), new ItemSelector(_itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(_itemService, FullEquipType.Hands), new ItemSelector(_itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(_itemService, FullEquipType.Legs), new ItemSelector(_itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(_itemService, FullEquipType.Feet), new ItemSelector(_itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(_itemService, FullEquipType.Ears), new ItemSelector(_itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(_itemService, FullEquipType.Neck), new ItemSelector(_itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(_itemService, FullEquipType.Wrists), new ItemSelector(_itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(_itemService, FullEquipType.Finger), new ItemSelector(_itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, FullEquipType.Head), new ItemSelector(itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, FullEquipType.Body), new ItemSelector(itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, FullEquipType.Hands), new ItemSelector(itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, FullEquipType.Legs), new ItemSelector(itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, FullEquipType.Feet), new ItemSelector(itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, FullEquipType.Ears), new ItemSelector(itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, FullEquipType.Neck), new ItemSelector(itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, FullEquipType.Wrists), new ItemSelector(itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, FullEquipType.Finger), new ItemSelector(itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), // @formatter:on }; @@ -131,8 +127,8 @@ public class ItemSwapTab : IDisposable, ITab private class ItemSelector : FilterComboCache { - public ItemSelector(ItemService data, FullEquipType type) - : base(() => data.AwaitedService[type], Penumbra.Log) + public ItemSelector(ItemData data, FullEquipType type) + : base(() => data.ByType[type], Penumbra.Log) { } protected override string ToString(EquipItem obj) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 804feae1..82fc78c0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 7f14165c..12b8d761 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -3,6 +3,7 @@ using Lumina.Misc; using OtterGui; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index bd37a484..8f90750f 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -22,7 +22,7 @@ public sealed class CollectionPanel : IDisposable private readonly CollectionStorage _collections; private readonly ActiveCollections _active; private readonly CollectionSelector _selector; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly ITargetManager _targets; private readonly IndividualAssignmentUi _individualAssignmentUi; private readonly InheritanceUi _inheritanceUi; @@ -37,7 +37,7 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorService actors, ITargetManager targets, ModStorage mods) + CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods) { _collections = manager.Storage; _active = manager.Active; @@ -382,11 +382,11 @@ public sealed class CollectionPanel : IDisposable } private void DrawCurrentCharacter(Vector2 width) - => DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.AwaitedService.GetCurrentPlayer()); + => DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.GetCurrentPlayer()); private void DrawCurrentTarget(Vector2 width) => DrawIndividualButton("Current Target", width, string.Empty, 't', - _actors.AwaitedService.FromObject(_targets.Target, false, true, true)); + _actors.FromObject(_targets.Target, false, true, true)); private void DrawNewPlayer(Vector2 width) => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index 5f463c43..d3e4ab5e 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -5,6 +5,9 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui; +using Penumbra.GameData.Structs; using Penumbra.Services; namespace Penumbra.UI.CollectionTab; @@ -12,7 +15,7 @@ namespace Penumbra.UI.CollectionTab; public class IndividualAssignmentUi : IDisposable { private readonly CommunicatorService _communicator; - private readonly ActorService _actorService; + private readonly ActorManager _actors; private readonly CollectionManager _collectionManager; private WorldCombo _worldCombo = null!; @@ -24,16 +27,13 @@ public class IndividualAssignmentUi : IDisposable private bool _ready; - public IndividualAssignmentUi(CommunicatorService communicator, ActorService actors, CollectionManager collectionManager) + public IndividualAssignmentUi(CommunicatorService communicator, ActorManager actors, CollectionManager collectionManager) { _communicator = communicator; - _actorService = actors; + _actors = actors; _collectionManager = collectionManager; _communicator.CollectionChange.Subscribe(UpdateIdentifiers, CollectionChange.Priority.IndividualAssignmentUi); - if (_actorService.Valid) - SetupCombos(); - else - _actorService.FinishedCreation += SetupCombos; + _actors.Awaiter.ContinueWith(_ => SetupCombos()); } public string PlayerTooltip { get; private set; } = NewPlayerTooltipEmpty; @@ -91,10 +91,10 @@ public class IndividualAssignmentUi : IDisposable // Input Selections. private string _newCharacterName = string.Empty; private ObjectKind _newKind = ObjectKind.BattleNpc; - private ActorIdentifier[] _playerIdentifiers = Array.Empty(); - private ActorIdentifier[] _retainerIdentifiers = Array.Empty(); - private ActorIdentifier[] _npcIdentifiers = Array.Empty(); - private ActorIdentifier[] _ownedIdentifiers = Array.Empty(); + private ActorIdentifier[] _playerIdentifiers = []; + private ActorIdentifier[] _retainerIdentifiers = []; + private ActorIdentifier[] _npcIdentifiers = []; + private ActorIdentifier[] _ownedIdentifiers = []; private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; @@ -126,14 +126,13 @@ public class IndividualAssignmentUi : IDisposable /// Create combos when ready. private void SetupCombos() { - _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds, Penumbra.Log); - _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts, Penumbra.Log); - _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions, Penumbra.Log); - _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments, Penumbra.Log); - _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs, Penumbra.Log); - _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs, Penumbra.Log); - _ready = true; - _actorService.FinishedCreation -= SetupCombos; + _worldCombo = new WorldCombo(_actors.Data.Worlds, Penumbra.Log, WorldId.AnyWorld); + _mountCombo = new NpcCombo("##mountCombo", _actors.Data.Mounts, Penumbra.Log); + _companionCombo = new NpcCombo("##companionCombo", _actors.Data.Companions, Penumbra.Log); + _ornamentCombo = new NpcCombo("##ornamentCombo", _actors.Data.Ornaments, Penumbra.Log); + _bnpcCombo = new NpcCombo("##bnpcCombo", _actors.Data.BNpcs, Penumbra.Log); + _enpcCombo = new NpcCombo("##enpcCombo", _actors.Data.ENpcs, Penumbra.Log); + _ready = true; } private void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) @@ -146,22 +145,22 @@ public class IndividualAssignmentUi : IDisposable { var combo = GetNpcCombo(_newKind); PlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, - _worldCombo.CurrentSelection.Key, ObjectKind.None, - Array.Empty(), out _playerIdentifiers) switch + _worldCombo.CurrentSelection.Key, ObjectKind.None, [], out _playerIdentifiers) switch { _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - RetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, - Array.Empty(), out _retainerIdentifiers) switch - { - _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; + RetainerTooltip = + _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, [], + out _retainerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; if (combo.CurrentSelection.Ids != null) { NpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, @@ -184,8 +183,8 @@ public class IndividualAssignmentUi : IDisposable { NpcTooltip = NewNpcTooltipEmpty; OwnedTooltip = NewNpcTooltipEmpty; - _npcIdentifiers = Array.Empty(); - _ownedIdentifiers = Array.Empty(); + _npcIdentifiers = []; + _ownedIdentifiers = []; } } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 0ac3b9a0..d5ff1abd 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -6,16 +6,16 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; -public class ResourceWatcher : IDisposable, ITab +public sealed class ResourceWatcher : IDisposable, ITab { public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; @@ -24,15 +24,15 @@ public class ResourceWatcher : IDisposable, ITab private readonly EphemeralConfig _ephemeral; private readonly ResourceService _resources; private readonly ResourceLoader _loader; - private readonly ActorService _actors; - private readonly List _records = new(); - private readonly ConcurrentQueue _newRecords = new(); + private readonly ActorManager _actors; + private readonly List _records = []; + private readonly ConcurrentQueue _newRecords = []; private readonly ResourceWatcherTable _table; private string _logFilter = string.Empty; private Regex? _logRegex; private int _newMaxEntries; - public unsafe ResourceWatcher(ActorService actors, Configuration config, ResourceService resources, ResourceLoader loader) + public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader) { _actors = actors; _config = config; @@ -266,12 +266,12 @@ public class ResourceWatcher : IDisposable, ITab public unsafe string Name(ResolveData resolve, string none = "") { - if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Valid) + if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Awaiter.IsCompletedSuccessfully) return none; try { - var id = _actors.AwaitedService.FromObject((GameObject*)resolve.AssociatedGameObject, out _, false, true, true); + var id = _actors.FromObject((GameObject*)resolve.AssociatedGameObject, out _, false, true, true); if (id.IsValid) { if (id.Type is not (IdentifierType.Player or IdentifierType.Owned)) diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 2d30f9cf..3c6a3ed9 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -7,6 +7,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; @@ -14,7 +15,7 @@ using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; -public class CollectionsTab : IDisposable, ITab +public sealed class CollectionsTab : IDisposable, ITab { private readonly EphemeralConfig _config; private readonly CollectionSelector _selector; @@ -40,7 +41,7 @@ public class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, - CollectionManager collectionManager, ModStorage modStorage, ActorService actors, ITargetManager targets, TutorialService tutorial) + CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial) { _config = configuration.Ephemeral; _tutorial = tutorial; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 1cc29d88..ad3fdb3d 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -3,6 +3,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Services; +using Penumbra.UI.Tabs.Debug; using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; namespace Penumbra.UI.Tabs; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs similarity index 94% rename from Penumbra/UI/Tabs/DebugTab.cs rename to Penumbra/UI/Tabs/Debug/DebugTab.cs index 4f554ac5..57ca68fc 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1,24 +1,30 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; +using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.DataContainers.Bases; using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Import.Textures; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -31,22 +37,39 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using Penumbra.Interop.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using ImGuiClip = OtterGui.ImGuiClip; -namespace Penumbra.UI.Tabs; +namespace Penumbra.UI.Tabs.Debug; + +public class Diagnostics(IServiceProvider provider) +{ + public void DrawDiagnostics() + { + if (!ImGui.CollapsingHeader("Diagnostics")) + return; + + using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg); + foreach (var type in typeof(IAsyncDataContainer).Assembly.GetTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) + { + var container = (IAsyncDataContainer) provider.GetRequiredService(type); + ImGuiUtil.DrawTableColumn(container.Name); + ImGuiUtil.DrawTableColumn(container.Time.ToString()); + ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); + ImGuiUtil.DrawTableColumn(container.TotalCount.ToString()); + } + } +} public class DebugTab : Window, ITab { - private readonly StartTracker _timer; private readonly PerformanceTracker _performance; private readonly Configuration _config; private readonly CollectionManager _collectionManager; private readonly ModManager _modManager; private readonly ValidityChecker _validityChecker; private readonly HttpApi _httpApi; - private readonly ActorService _actorService; + private readonly ActorManager _actors; private readonly DalamudServices _dalamud; private readonly StainService _stains; private readonly CharacterUtility _characterUtility; @@ -64,16 +87,17 @@ public class DebugTab : Window, ITab private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; private readonly SkinFixer _skinFixer; - private readonly IdentifierService _identifier; private readonly RedrawService _redraws; + private readonly DictEmotes _emotes; + private readonly Diagnostics _diagnostics; - public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, - ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, + public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, + ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer, IdentifierService identifier, RedrawService redraws) + TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmotes emotes, Diagnostics diagnostics) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -82,14 +106,13 @@ public class DebugTab : Window, ITab MinimumSize = new Vector2(200, 200), MaximumSize = new Vector2(2000, 2000), }; - _timer = timer; _performance = performance; _config = config; _collectionManager = collectionManager; _validityChecker = validityChecker; _modManager = modManager; _httpApi = httpApi; - _actorService = actorService; + _actors = actors; _dalamud = dalamud; _stains = stains; _characterUtility = characterUtility; @@ -107,8 +130,9 @@ public class DebugTab : Window, ITab _framework = framework; _textureManager = textureManager; _skinFixer = skinFixer; - _identifier = identifier; _redraws = redraws; + _emotes = emotes; + _diagnostics = diagnostics; } public ReadOnlySpan Label @@ -130,6 +154,7 @@ public class DebugTab : Window, ITab return; DrawDebugTabGeneral(); + _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); ImGui.NewLine(); DrawPathResolverDebug(); @@ -357,7 +382,6 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(name); ImGui.TableNextColumn(); } - } } } @@ -372,10 +396,7 @@ public class DebugTab : Window, ITab using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen)) { if (start) - { - _timer.Draw("##startTimer", TimingExtensions.ToName); ImGui.NewLine(); - } } _performance.Draw("##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName); @@ -391,22 +412,10 @@ public class DebugTab : Window, ITab if (!table) return; - void DrawSpecial(string name, ActorIdentifier id) - { - if (!id.IsValid) - return; - - ImGuiUtil.DrawTableColumn(name); - ImGuiUtil.DrawTableColumn(string.Empty); - ImGuiUtil.DrawTableColumn(string.Empty); - ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); - ImGuiUtil.DrawTableColumn(string.Empty); - } - - DrawSpecial("Current Player", _actorService.AwaitedService.GetCurrentPlayer()); - DrawSpecial("Current Inspect", _actorService.AwaitedService.GetInspectPlayer()); - DrawSpecial("Current Card", _actorService.AwaitedService.GetCardPlayer()); - DrawSpecial("Current Glamour", _actorService.AwaitedService.GetGlamourPlayer()); + DrawSpecial("Current Player", _actors.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); + DrawSpecial("Current Card", _actors.GetCardPlayer()); + DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); foreach (var obj in _dalamud.Objects) { @@ -415,11 +424,25 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); - var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); - ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); + var identifier = _actors.FromObject(obj, false, true, false); + ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } + + return; + + void DrawSpecial(string name, ActorIdentifier id) + { + if (!id.IsValid) + return; + + ImGuiUtil.DrawTableColumn(name); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(_actors.ToString(id)); + ImGuiUtil.DrawTableColumn(string.Empty); + } } /// @@ -616,7 +639,7 @@ public class DebugTab : Window, ITab return; var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - var dummy = ImGuiClip.FilteredClippedDraw(_identifier.AwaitedService.Emotes, skips, + var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips, p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase) && (_emoteSearchName.Length == 0 || p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))), diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 25d91644..62ad5a6e 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -2,7 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI.AdvancedWindow; -using Penumbra.UI.Tabs; +using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index 932072f0..b84813cc 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,23 +1,7 @@ -global using StartTracker = OtterGui.Classes.StartTimeTracker; global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; namespace Penumbra.Util; -public enum StartTimeType -{ - Total, - Identifier, - Stains, - Items, - Actors, - Backup, - Mods, - Collections, - PathResolver, - Interface, - Api, -} - public enum PerformanceType { UiMainWindow, @@ -48,23 +32,6 @@ public enum PerformanceType public static class TimingExtensions { - public static string ToName(this StartTimeType type) - => type switch - { - StartTimeType.Total => "Total Construction", - StartTimeType.Identifier => "Identification Data", - StartTimeType.Stains => "Stain Data", - StartTimeType.Items => "Item Data", - StartTimeType.Actors => "Actor Data", - StartTimeType.Backup => "Checking Backups", - StartTimeType.Mods => "Loading Mods", - StartTimeType.Collections => "Loading Collections", - StartTimeType.Api => "Setting Up API", - StartTimeType.Interface => "Setting Up Interface", - StartTimeType.PathResolver => "Setting Up Path Resolver", - _ => $"Unknown {(int)type}", - }; - public static string ToName(this PerformanceType type) => type switch { diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index eed5d7c8..9172bb60 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -81,7 +81,8 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.8, )", + "OtterGui": "[1.0.0, )", + "Penumbra.Api": "[1.0.13, )", "Penumbra.String": "[1.0.4, )" } }, From b494892d62630f1e446c255d5b19abbce0dd64fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Dec 2023 16:50:52 +0100 Subject: [PATCH 1326/2451] Update for changed GameData. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index afc56d9f..0cb10c3d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit afc56d9f07a2a54ab791a4c85cf627b6f884aec2 +Subproject commit 0cb10c3d915fd9c589195e24d44db3e2ca57841a diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index acd6eae9..36237e47 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -46,7 +46,7 @@ public static class CustomizationSwap SetId idFrom, SetId idTo, byte variant, ref string fileName, ref bool dataWasChanged) { - variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; + variant = slot is BodySlot.Face or BodySlot.Ear ? byte.MaxValue : variant; var mtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); var mtrlToPath = GamePaths.Character.Mtrl.Path(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 0ec11542..c2910b51 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -221,7 +221,7 @@ public class ItemSwapTab : IDisposable, ITab _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: - _swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), + _swapData.LoadCustomization(_metaFileManager, BodySlot.Ear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, (SetId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); From 6dc5916f2b711faa489ac00fe67c352c923c9d61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Dec 2023 17:02:08 +0100 Subject: [PATCH 1327/2451] Fix some issues. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/ServiceManager.cs | 5 +++-- Penumbra/Services/StainService.cs | 5 +++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/OtterGui b/OtterGui index bde59c34..ac88abfe 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bde59c34f7108520002c21cdbf21e8ee5b586944 +Subproject commit ac88abfe9eb0bb5d03aec092dc8f4db642ecbd6a diff --git a/Penumbra.GameData b/Penumbra.GameData index 0cb10c3d..c53e578e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0cb10c3d915fd9c589195e24d44db3e2ca57841a +Subproject commit c53e578e750d26f83a4e81aca1681c5b01d25a5a diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 049ab328..30f58701 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Compression; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; @@ -74,8 +75,8 @@ public static class ServiceManager private static IServiceCollection AddAsyncServices(this IServiceCollection services) { - foreach (var type in typeof(IDataContainer).Assembly.GetExportedTypes() - .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncService)))) + foreach (var type in typeof(ActorManager).Assembly.GetExportedTypes() + .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IService)))) services.AddSingleton(type); return services; } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index d11e4d64..714576b2 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ImGuiNET; +using OtterGui.Log; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; @@ -76,9 +77,9 @@ public class StainService : IDisposable public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, IPluginLog dalamudLog) + public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, Logger logger) { - StainData = new DictStains(pluginInterface, dalamudLog, dataManager); + StainData = new DictStains(pluginInterface, logger, dataManager); StainCombo = new FilterComboColors(140, () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 57ca68fc..34628c60 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -13,12 +13,12 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; -using Penumbra.GameData.DataContainers.Bases; using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Import.Textures; @@ -49,7 +49,7 @@ public class Diagnostics(IServiceProvider provider) return; using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg); - foreach (var type in typeof(IAsyncDataContainer).Assembly.GetTypes() + foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { var container = (IAsyncDataContainer) provider.GetRequiredService(type); From 5d28904bdf7e343f9df883c205cfcd2b10ec8ed3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Dec 2023 16:39:26 +0100 Subject: [PATCH 1328/2451] Update for GameData changes. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqdpCache.cs | 2 +- Penumbra/Collections/Cache/EstCache.cs | 6 +-- Penumbra/Collections/Cache/MetaCache.cs | 10 ++--- .../ResolveContext.PathResolution.cs | 14 +++---- .../Interop/ResourceTree/ResolveContext.cs | 6 +-- Penumbra/Meta/Files/EqdpFile.cs | 22 +++++----- Penumbra/Meta/Files/EqpGmpFile.cs | 30 ++++++------- Penumbra/Meta/Files/EstFile.cs | 12 +++--- .../Meta/Manipulations/EqdpManipulation.cs | 4 +- .../Meta/Manipulations/EqpManipulation.cs | 4 +- .../Meta/Manipulations/EstManipulation.cs | 5 ++- .../Meta/Manipulations/GmpManipulation.cs | 6 +-- .../Meta/Manipulations/ImcManipulation.cs | 12 +++--- .../Meta/Manipulations/MetaManipulation.cs | 20 ++++++++- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 6 +-- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 42 +++++++++---------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 10 ++--- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 40 ++++++++++++------ Penumbra/Mods/Subclasses/SubMod.cs | 6 +-- Penumbra/Penumbra.csproj | 5 ++- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 16 +++---- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 1 + 24 files changed, 160 insertions(+), 123 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c53e578e..bee73fbe 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c53e578e750d26f83a4e81aca1681c5b01d25a5a +Subproject commit bee73fbe1e263d81067029ad97c8e4a263c88147 diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 3937fa72..6b4982a7 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -45,7 +45,7 @@ public readonly struct EqdpCache : IDisposable foreach (var file in _eqdpFiles.OfType()) { var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (SetId)m.SetId)); + file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId)); } _eqdpManipulations.Clear(); diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 9e2cdef9..2552cd4a 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -74,12 +74,12 @@ public struct EstCache : IDisposable }; } - internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, SetId setId) + internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) { var file = GetEstFile(type); return file != null - ? file[genderRace, setId.Id] - : EstFile.GetDefault(manager, type, genderRace, setId); + ? file[genderRace, primaryId.Id] + : EstFile.GetDefault(manager, type, genderRace, primaryId); } public void Reset() diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index d5acf249..650bd536 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -187,17 +187,17 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.GetImcFile(path, out file); - internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId) + internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) { var eqdpFile = _eqdpCache.EqdpFile(race, accessory); if (eqdpFile != null) - return setId.Id < eqdpFile.Count ? eqdpFile[setId] : default; + return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; else - return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, setId); + return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId); } - internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, SetId setId) - => _estCache.GetEstEntry(_manager, type, genderRace, setId); + internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) + => _estCache.GetEstEntry(_manager, type, genderRace, primaryId); /// Use this when CharacterUtility becomes ready. private void ApplyStoredManipulations() diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 1c9dfaa1..f2059253 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -37,7 +37,7 @@ internal partial record ResolveContext private unsafe GenderRace ResolveModelRaceCode() => ResolveEqdpRaceCode(Slot, Equipment.Set); - private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, SetId setId) + private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { var slotIndex = slot.ToIndex(); if (slotIndex >= 10 || ModelType != ModelType.Human) @@ -55,7 +55,7 @@ internal partial record ResolveContext if (metaCache == null) return GenderRace.MidlanderMale; - var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, setId); + var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return characterRaceCode; @@ -63,7 +63,7 @@ internal partial record ResolveContext if (fallbackRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, setId); + entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return fallbackRaceCode; @@ -229,7 +229,7 @@ internal partial record ResolveContext return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } - private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex) + private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex) { var human = (Human*)CharacterBase.Value; var characterRaceCode = (GenderRace)human->RaceSexId; @@ -262,17 +262,17 @@ internal partial record ResolveContext } } - private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) + private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) { var human = (Human*)CharacterBase.Value; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, SetId set) + private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) { var metaCache = Global.Collection.MetaCache; - var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, set); + var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, primary); return (raceCode, EstManipulation.ToName(type), skeletonSet); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d03ee508..431f1ac0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -25,8 +25,8 @@ internal record GlobalResolveContext(ObjectIdentification Identifier, ModCollect public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, - EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, WeaponType weaponType = default) - => new(this, characterBase, slotIndex, slot, equipment, weaponType); + EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) + => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } internal partial record ResolveContext( @@ -35,7 +35,7 @@ internal partial record ResolveContext( uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, - WeaponType WeaponType) + SecondaryId SecondaryId) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index 8a99225f..c76c4efd 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -38,7 +38,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public int Count => (Length - DataOffset) / EqdpEntrySize; - public EqdpEntry this[SetId id] + public EqdpEntry this[PrimaryId id] { get { @@ -79,7 +79,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile MemoryUtility.MemSet(myDataPtr, 0, Length - (int)((byte*)myDataPtr - Data)); } - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(entry); @@ -101,18 +101,18 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile Reset(); } - public EqdpEntry GetDefault(SetId setId) - => GetDefault(Manager, Index, setId); + public EqdpEntry GetDefault(PrimaryId primaryId) + => GetDefault(Manager, Index, primaryId); - public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, SetId setId) - => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, setId); + public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, PrimaryId primaryId) + => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, primaryId); - public static EqdpEntry GetDefault(byte* data, SetId setId) + public static EqdpEntry GetDefault(byte* data, PrimaryId primaryId) { var blockSize = *(ushort*)(data + IdentifierSize); var totalBlockCount = *(ushort*)(data + IdentifierSize + 2); - var blockIdx = setId.Id / blockSize; + var blockIdx = primaryId.Id / blockSize; if (blockIdx >= totalBlockCount) return 0; @@ -121,9 +121,9 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile return 0; var blockData = (ushort*)(data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2); - return (EqdpEntry)(*(blockData + setId.Id % blockSize)); + return (EqdpEntry)(*(blockData + primaryId.Id % blockSize)); } - public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, SetId setId) - => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setId); + public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, PrimaryId primaryId) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], primaryId); } diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 6e9fd010..97f57703 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -24,7 +24,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile public ulong ControlBlock => *(ulong*)Data; - protected ulong GetInternal(SetId idx) + protected ulong GetInternal(PrimaryId idx) { return idx.Id switch { @@ -34,7 +34,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile }; } - protected void SetInternal(SetId idx, ulong value) + protected void SetInternal(PrimaryId idx, ulong value) { idx = idx.Id switch { @@ -81,13 +81,13 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile Reset(); } - protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, SetId setId, ulong def) + protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, PrimaryId primaryId, ulong def) { var data = (byte*)manager.CharacterUtility.DefaultResource(fileIndex).Address; - if (setId == 0) - setId = 1; + if (primaryId == 0) + primaryId = 1; - var blockIdx = setId.Id / BlockSize; + var blockIdx = primaryId.Id / BlockSize; if (blockIdx >= NumBlocks) return def; @@ -97,7 +97,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile return def; var count = BitOperations.PopCount(control & (blockBit - 1)); - var idx = setId.Id % BlockSize; + var idx = primaryId.Id % BlockSize; var ptr = (ulong*)data + BlockSize * count + idx; return *ptr; } @@ -112,15 +112,15 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable : base(manager, false) { } - public EqpEntry this[SetId idx] + public EqpEntry this[PrimaryId idx] { get => (EqpEntry)GetInternal(idx); set => SetInternal(idx, (ulong)value); } - public static EqpEntry GetDefault(MetaFileManager manager, SetId setIdx) - => (EqpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)Eqp.DefaultEntry); + public static EqpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) + => (EqpEntry)GetDefaultInternal(manager, InternalIndex, primaryIdx, (ulong)Eqp.DefaultEntry); protected override unsafe void SetEmptyBlock(int idx) { @@ -130,7 +130,7 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable *ptr = (ulong)Eqp.DefaultEntry; } - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(Manager, entry); @@ -155,16 +155,16 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable : base(manager, true) { } - public GmpEntry this[SetId idx] + public GmpEntry this[PrimaryId idx] { get => (GmpEntry)GetInternal(idx); set => SetInternal(idx, (ulong)value); } - public static GmpEntry GetDefault(MetaFileManager manager, SetId setIdx) - => (GmpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)GmpEntry.Default); + public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) + => (GmpEntry)GetDefaultInternal(manager, InternalIndex, primaryIdx, (ulong)GmpEntry.Default); - public void Reset(IEnumerable entries) + public void Reset(IEnumerable entries) { foreach (var entry in entries) this[entry] = GetDefault(Manager, entry); diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 2c7409b4..af441b22 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -167,21 +167,21 @@ public sealed unsafe class EstFile : MetaBaseFile public ushort GetDefault(GenderRace genderRace, ushort setId) => GetDefault(Manager, Index, genderRace, setId); - public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, SetId setId) + public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) { var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; var count = *(int*)data; var span = new ReadOnlySpan(data + 4, count); - var (idx, found) = FindEntry(span, genderRace, setId.Id); + var (idx, found) = FindEntry(span, genderRace, primaryId.Id); if (!found) return 0; return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); } - public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, SetId setId) - => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, setId); + public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, primaryId); - public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, SetId setId) - => GetDefault(manager, (MetaIndex)estType, genderRace, setId); + public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, PrimaryId primaryId) + => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index df7ed2e4..0426dfce 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -18,13 +18,13 @@ public readonly struct EqdpManipulation : IMetaManipulation [JsonConverter(typeof(StringEnumConverter))] public ModelRace Race { get; private init; } - public SetId SetId { get; private init; } + public PrimaryId SetId { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, SetId setId) + public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) { Gender = gender; Race = race; diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 4373e8e9..d59938b6 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -15,13 +15,13 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConverter(typeof(ForceNumericFlagEnumConverter))] public EqpEntry Entry { get; private init; } - public SetId SetId { get; private init; } + public PrimaryId SetId { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public EquipSlot Slot { get; private init; } [JsonConstructor] - public EqpManipulation(EqpEntry entry, EquipSlot slot, SetId setId) + public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) { Slot = slot; SetId = setId; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 455c39ff..d3c92ad3 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -36,13 +36,14 @@ public readonly struct EstManipulation : IMetaManipulation [JsonConverter(typeof(StringEnumConverter))] public ModelRace Race { get; private init; } - public SetId SetId { get; private init; } + public PrimaryId SetId { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public EstType Slot { get; private init; } + [JsonConstructor] - public EstManipulation(Gender gender, ModelRace race, EstType slot, SetId setId, ushort entry) + public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, ushort entry) { Entry = entry; Gender = gender; diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 928b6f55..ee58295d 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -8,11 +8,11 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct GmpManipulation : IMetaManipulation { - public GmpEntry Entry { get; private init; } - public SetId SetId { get; private init; } + public GmpEntry Entry { get; private init; } + public PrimaryId SetId { get; private init; } [JsonConstructor] - public GmpManipulation(GmpEntry entry, SetId setId) + public GmpManipulation(GmpEntry entry, PrimaryId setId) { Entry = entry; SetId = setId; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 391daacc..a1c4b5bf 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -12,10 +12,10 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct ImcManipulation : IMetaManipulation { - public ImcEntry Entry { get; private init; } - public SetId PrimaryId { get; private init; } - public SetId SecondaryId { get; private init; } - public Variant Variant { get; private init; } + public ImcEntry Entry { get; private init; } + public PrimaryId PrimaryId { get; private init; } + public PrimaryId SecondaryId { get; private init; } + public Variant Variant { get; private init; } [JsonConverter(typeof(StringEnumConverter))] public ObjectType ObjectType { get; private init; } @@ -26,7 +26,7 @@ public readonly struct ImcManipulation : IMetaManipulation [JsonConverter(typeof(StringEnumConverter))] public BodySlot BodySlot { get; private init; } - public ImcManipulation(EquipSlot equipSlot, ushort variant, SetId primaryId, ImcEntry entry) + public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) { Entry = entry; PrimaryId = primaryId; @@ -42,7 +42,7 @@ public readonly struct ImcManipulation : IMetaManipulation // so we change the unused value to something nonsensical in that case, just so they do not compare equal, // and clamp the variant to 255. [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, SetId primaryId, SetId secondaryId, ushort variant, + internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, PrimaryId secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry) { Entry = entry; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 94b45cdf..e057d1a4 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -269,5 +269,23 @@ public readonly struct MetaManipulation : IEquatable, ICompara Type.Gmp => $"{Gmp.Entry.Value}", Type.Rsp => $"{Rsp.Entry}", _ => string.Empty, - }; + }; + + public static bool operator ==(MetaManipulation left, MetaManipulation right) + => left.Equals(right); + + public static bool operator !=(MetaManipulation left, MetaManipulation right) + => !(left == right); + + public static bool operator <(MetaManipulation left, MetaManipulation right) + => left.CompareTo(right) < 0; + + public static bool operator <=(MetaManipulation left, MetaManipulation right) + => left.CompareTo(right) <= 0; + + public static bool operator >(MetaManipulation left, MetaManipulation right) + => left.CompareTo(right) > 0; + + public static bool operator >=(MetaManipulation left, MetaManipulation right) + => left.CompareTo(right) >= 0; } diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index 36237e47..fc32df0c 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -12,7 +12,7 @@ public static class CustomizationSwap { /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, - SetId idFrom, SetId idTo) + PrimaryId idFrom, PrimaryId idTo) { if (idFrom.Id > byte.MaxValue) throw new Exception($"The Customization ID {idFrom} is too large for {slot}."); @@ -43,7 +43,7 @@ public static class CustomizationSwap } public static FileSwap CreateMtrl(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, - SetId idFrom, SetId idTo, byte variant, + PrimaryId idFrom, PrimaryId idTo, byte variant, ref string fileName, ref bool dataWasChanged) { variant = slot is BodySlot.Face or BodySlot.Ear ? byte.MaxValue : variant; @@ -79,7 +79,7 @@ public static class CustomizationSwap } public static FileSwap CreateTex(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, - SetId idFrom, ref MtrlFile.Texture texture, + PrimaryId idFrom, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var path = texture.Path; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index d634349e..f7f82a59 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -185,13 +185,13 @@ public static class EquipmentSwap } public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, GenderRace gr, SetId idFrom, - SetId idTo, byte mtrlTo) + Func manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, + PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, - SetId idTo, byte mtrlTo) + Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, + PrimaryId idTo, byte mtrlTo) { var (gender, race) = gr.Split(); var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom), slotFrom, gender, @@ -214,11 +214,11 @@ public static class EquipmentSwap } public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slot, GenderRace gr, - SetId idFrom, SetId idTo, byte mtrlTo) + PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateMdl(manager, redirections, slot, slot, gr, idFrom, idTo, mtrlTo); public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, - GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo) + GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { var mdlPathFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) @@ -236,7 +236,7 @@ public static class EquipmentSwap return mdl; } - private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out Variant variant) + private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant) { slot = i.Type.ToSlot(); if (!slot.IsEquipmentPiece()) @@ -247,7 +247,7 @@ public static class EquipmentSwap } private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, - SetId idFrom, SetId idTo, Variant variantFrom) + PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); var imc = new ImcFile(manager, entry); @@ -270,8 +270,8 @@ public static class EquipmentSwap return (imc, variants, items); } - public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, SetId idFrom, - SetId idTo) + public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, + PrimaryId idTo) { if (slot is not EquipSlot.Head) return null; @@ -283,12 +283,12 @@ public static class EquipmentSwap public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, EquipSlot slot, - SetId idFrom, SetId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, Func manips, - EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, + EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); @@ -322,7 +322,7 @@ public static class EquipmentSwap // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, SetId idFrom, SetId idTo, byte vfxId) + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, byte vfxId) { if (vfxId == 0) return null; @@ -340,8 +340,8 @@ public static class EquipmentSwap return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, SetId idFrom, - SetId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, + PrimaryId idTo) { if (slot.IsAccessory()) return null; @@ -353,13 +353,13 @@ public static class EquipmentSwap return new MetaSwap(manips, eqpFrom, eqpTo); } - public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, SetId idFrom, - SetId idTo, byte variantTo, ref string fileName, + public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, + PrimaryId idTo, byte variantTo, ref string fileName, ref bool dataWasChanged) => CreateMtrl(manager, redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged); public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, - SetId idFrom, SetId idTo, byte variantTo, ref string fileName, + PrimaryId idFrom, PrimaryId idTo, byte variantTo, ref string fileName, ref bool dataWasChanged) { var prefix = slotTo.IsAccessory() ? 'a' : 'e'; @@ -397,13 +397,13 @@ public static class EquipmentSwap return mtrl; } - public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, SetId idFrom, SetId idTo, + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, - EquipSlot slotTo, SetId idFrom, - SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) + EquipSlot slotTo, PrimaryId idFrom, + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var path = texture.Path; var addedDashes = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 90bee553..b269d89c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -150,7 +150,7 @@ public static class ItemSwap /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, Func manips, EstManipulation.EstType type, - GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl) + GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) return null; @@ -195,7 +195,7 @@ public static class ItemSwap } } - public static string ReplaceAnyId(string path, char idType, SetId id, bool condition = true) + public static string ReplaceAnyId(string path, char idType, PrimaryId id, bool condition = true) => condition ? Regex.Replace(path, $"{idType}\\d{{4}}", $"{idType}{id.Id:D4}") : path; @@ -203,10 +203,10 @@ public static class ItemSwap public static string ReplaceAnyRace(string path, GenderRace to, bool condition = true) => ReplaceAnyId(path, 'c', (ushort)to, condition); - public static string ReplaceAnyBody(string path, BodySlot slot, SetId to, bool condition = true) + public static string ReplaceAnyBody(string path, BodySlot slot, PrimaryId to, bool condition = true) => ReplaceAnyId(path, slot.ToAbbreviation(), to, condition); - public static string ReplaceId(string path, char type, SetId idFrom, SetId idTo, bool condition = true) + public static string ReplaceId(string path, char type, PrimaryId idFrom, PrimaryId idTo, bool condition = true) => condition ? path.Replace($"{type}{idFrom.Id:D4}", $"{type}{idTo.Id:D4}") : path; @@ -219,7 +219,7 @@ public static class ItemSwap public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true) => ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); - public static string ReplaceBody(string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true) + public static string ReplaceBody(string path, BodySlot slot, PrimaryId idFrom, PrimaryId idTo, bool condition = true) => ReplaceId(path, slot.ToAbbreviation(), idFrom, idTo, condition); public static string AddSuffix(string path, string ext, string suffix, bool condition = true) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 9ca02c4e..ff722945 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -145,7 +145,7 @@ public class ItemSwapContainer return ret; } - public bool LoadCustomization(MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, + public bool LoadCustomization(MetaFileManager manager, BodySlot slot, GenderRace race, PrimaryId from, PrimaryId to, ModCollection? collection = null) { var pathResolver = PathResolver(collection); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 0af78431..a95567ce 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -139,7 +139,7 @@ public class ModCacheManager : IDisposable { case ModPathChangeType.Added: case ModPathChangeType.Reloaded: - Refresh(mod); + RefreshWithChangedItems(mod); break; } } @@ -151,10 +151,28 @@ public class ModCacheManager : IDisposable } private void OnModDiscoveryFinished() - => Parallel.ForEach(_modManager, Refresh); + { + if (!_identifier.Awaiter.IsCompletedSuccessfully || _updatingItems) + { + Parallel.ForEach(_modManager, RefreshWithoutChangedItems); + } + else + { + _updatingItems = true; + Parallel.ForEach(_modManager, RefreshWithChangedItems); + _updatingItems = false; + } + } private void OnIdentifierCreation() - => Parallel.ForEach(_modManager, UpdateChangedItems); + { + if (_updatingItems) + return; + + _updatingItems = true; + Parallel.ForEach(_modManager, UpdateChangedItems); + _updatingItems = false; + } private static void UpdateFileCount(Mod mod) => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); @@ -173,15 +191,8 @@ public class ModCacheManager : IDisposable private void UpdateChangedItems(Mod mod) { - if (_updatingItems) - return; - - _updatingItems = true; var changedItems = (SortedList)mod.ChangedItems; changedItems.Clear(); - if (!_identifier.Awaiter.IsCompletedSuccessfully) - return; - foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) _identifier.Identify(changedItems, gamePath.ToString()); @@ -189,7 +200,6 @@ public class ModCacheManager : IDisposable ComputeChangedItems(_identifier, changedItems, manip); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); - _updatingItems = false; } private static void UpdateCounts(Mod mod) @@ -210,10 +220,16 @@ public class ModCacheManager : IDisposable } } - private void Refresh(Mod mod) + private void RefreshWithChangedItems(Mod mod) { UpdateTags(mod); UpdateCounts(mod); UpdateChangedItems(mod); } + + private void RefreshWithoutChangedItems(Mod mod) + { + UpdateTags(mod); + UpdateCounts(mod); + } } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 542b14d2..a081f7bc 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -30,9 +30,9 @@ public sealed class SubMod : ISubMod public bool IsDefault => GroupIdx < 0; - public Dictionary FileData = new(); - public Dictionary FileSwapData = new(); - public HashSet ManipulationData = new(); + public Dictionary FileData = []; + public Dictionary FileSwapData = []; + public HashSet ManipulationData = []; public SubMod(IMod parentMod) => ParentMod = parentMod; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ec433113..54f7a16f 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -91,8 +91,9 @@ - - + + + diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index c2910b51..d31a08ae 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -210,26 +210,26 @@ public class ItemSwapTab : IDisposable, ITab break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), - (SetId)_sourceId, - (SetId)_targetId, + (PrimaryId)_sourceId, + (PrimaryId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), - (SetId)_sourceId, - (SetId)_targetId, + (PrimaryId)_sourceId, + (PrimaryId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(_metaFileManager, BodySlot.Ear, Names.CombinedRace(_currentGender, ModelRace.Viera), - (SetId)_sourceId, - (SetId)_targetId, + (PrimaryId)_sourceId, + (PrimaryId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), - (SetId)_sourceId, - (SetId)_targetId, + (PrimaryId)_sourceId, + (PrimaryId)_targetId, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Weapon: break; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c307f6f4..cf65901e 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -169,6 +169,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Wed, 20 Dec 2023 16:43:30 +0100 Subject: [PATCH 1329/2451] Move signatures and add Footsteps. --- Penumbra.GameData | 2 +- .../PathResolving/AnimationHookService.cs | 26 ++++++++++++++----- .../Structs/ModelResourceHandleUtility.cs | 3 ++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bee73fbe..4d3570fd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bee73fbe1e263d81067029ad97c8e4a263c88147 +Subproject commit 4d3570fd47d78dbc49cf5e41fd3545a533ef9e81 diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs index 7fa0ed35..d13ef7f2 100644 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ b/Penumbra/Interop/PathResolving/AnimationHookService.cs @@ -55,6 +55,7 @@ public unsafe class AnimationHookService : IDisposable _unkParasolAnimationHook.Enable(); _dismountHook.Enable(); _apricotListenerSoundPlayHook.Enable(); + _footStepHook.Enable(); } public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) @@ -113,6 +114,7 @@ public unsafe class AnimationHookService : IDisposable _unkParasolAnimationHook.Dispose(); _dismountHook.Dispose(); _apricotListenerSoundPlayHook.Dispose(); + _footStepHook.Dispose(); } /// Characters load some of their voice lines or whatever with this function. @@ -324,7 +326,7 @@ public unsafe class AnimationHookService : IDisposable private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); - [Signature("48 89 5C 24 ?? 48 89 6C 24 ?? 89 54 24", DetourName = nameof(UnkMountAnimationDetour))] + [Signature(Sigs.UnkMountAnimation, DetourName = nameof(UnkMountAnimationDetour))] private readonly Hook _unkMountAnimationHook = null!; private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) @@ -335,10 +337,9 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = last; } - private delegate void UnkParasolAnimationDelegate(DrawObject* drawObject, int unk1); - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(UnkParasolAnimationDetour))] + [Signature(Sigs.UnkParasolAnimation, DetourName = nameof(UnkParasolAnimationDetour))] private readonly Hook _unkParasolAnimationHook = null!; private void UnkParasolAnimationDetour(DrawObject* drawObject, int unk1) @@ -347,9 +348,9 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); _unkParasolAnimationHook.Original(drawObject, unk1); _animationLoadData.Value = last; - } + } - [Signature("E8 ?? ?? ?? ?? F6 43 ?? ?? 74 ?? 48 8B CB", DetourName = nameof(DismountDetour))] + [Signature(Sigs.Dismount, DetourName = nameof(DismountDetour))] private readonly Hook _dismountHook = null!; private delegate void DismountDelegate(nint a1, nint a2); @@ -375,7 +376,7 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = last; } - [Signature("48 89 6C 24 ?? 41 54 41 56 41 57 48 81 EC", DetourName = nameof(ApricotListenerSoundPlayDetour))] + [Signature(Sigs.ApricotListenerSoundPlay, DetourName = nameof(ApricotListenerSoundPlayDetour))] private readonly Hook _apricotListenerSoundPlayHook = null!; private delegate nint ApricotListenerSoundPlayDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); @@ -406,4 +407,17 @@ public unsafe class AnimationHookService : IDisposable _animationLoadData.Value = last; return ret; } + + private delegate void FootStepDelegate(GameObject* gameObject, int id, int unk); + + [Signature(Sigs.FootStepSound, DetourName = nameof(FootStepDetour))] + private readonly Hook _footStepHook = null!; + + private void FootStepDetour(GameObject* gameObject, int id, int unk) + { + var last = _animationLoadData.Value; + _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); + _footStepHook.Original(gameObject, id, unk); + _animationLoadData.Value = last; + } } diff --git a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs index 008cd59a..bcfa2fa2 100644 --- a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs +++ b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; namespace Penumbra.Interop.Structs; @@ -10,7 +11,7 @@ public class ModelResourceHandleUtility public ModelResourceHandleUtility(IGameInteropProvider interop) => interop.InitializeFromAttributes(this); - [Signature("E8 ?? ?? ?? ?? 44 8B CD 48 89 44 24")] + [Signature(Sigs.GetMaterialFileNameBySlot)] private static nint _getMaterialFileNameBySlot = nint.Zero; public static unsafe byte* GetMaterialFileNameBySlot(ModelResourceHandle* handle, uint slot) From f022d2be6480f459e2163f8dc8bfff50c288636f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Dec 2023 18:47:30 +0100 Subject: [PATCH 1330/2451] Rework DalamudServices, --- OtterGui | 2 +- Penumbra/Api/IpcTester.cs | 85 ++++++++++--------- Penumbra/Api/PenumbraApi.cs | 47 +++++----- Penumbra/Api/PenumbraIpcProviders.cs | 8 +- Penumbra/Import/Textures/TexFileParser.cs | 6 +- .../ResourceLoading/CreateFileWHook.cs | 6 +- Penumbra/Penumbra.cs | 60 +++++++------ Penumbra/Penumbra.json | 2 +- Penumbra/Services/DalamudServices.cs | 81 +++++++----------- .../{ServiceManager.cs => ServiceManagerA.cs} | 74 ++++++---------- Penumbra/Services/ValidityChecker.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 8 +- .../ModEditWindow.QuickImport.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 20 +++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 4 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 24 +++--- Penumbra/UI/Tabs/SettingsTab.cs | 42 +++++---- Penumbra/packages.lock.json | 5 +- 19 files changed, 230 insertions(+), 250 deletions(-) rename Penumbra/Services/{ServiceManager.cs => ServiceManagerA.cs} (72%) diff --git a/OtterGui b/OtterGui index ac88abfe..197d23ee 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ac88abfe9eb0bb5d03aec092dc8f4db642ecbd6a +Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index f7b740b9..aea95156 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -40,23 +40,24 @@ public class IpcTester : IDisposable private readonly Temporary _temporary; private readonly ResourceTree _resourceTree; - public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, - CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) + public IpcTester(Configuration config, DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, + PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, + TempCollectionManager tempCollections, SaveService saveService) { _ipcProviders = ipcProviders; - _pluginState = new PluginState(dalamud.PluginInterface); - _ipcConfiguration = new IpcConfiguration(dalamud.PluginInterface); - _ui = new Ui(dalamud.PluginInterface); - _redrawing = new Redrawing(dalamud); - _gameState = new GameState(dalamud.PluginInterface); - _resolve = new Resolve(dalamud.PluginInterface); - _collections = new Collections(dalamud.PluginInterface); - _meta = new Meta(dalamud.PluginInterface); - _mods = new Mods(dalamud.PluginInterface); - _modSettings = new ModSettings(dalamud.PluginInterface); - _editing = new Editing(dalamud.PluginInterface); - _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); - _resourceTree = new ResourceTree(dalamud.PluginInterface, dalamud.Objects); + _pluginState = new PluginState(pi); + _ipcConfiguration = new IpcConfiguration(pi); + _ui = new Ui(pi); + _redrawing = new Redrawing(pi, objects, clientState); + _gameState = new GameState(pi); + _resolve = new Resolve(pi); + _collections = new Collections(pi); + _meta = new Meta(pi); + _mods = new Mods(pi); + _modSettings = new ModSettings(pi); + _editing = new Editing(pi); + _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config); + _resourceTree = new ResourceTree(pi, objects); UnsubscribeEvents(); } @@ -398,17 +399,21 @@ public class IpcTester : IDisposable private class Redrawing { - private readonly DalamudServices _dalamud; + private readonly DalamudPluginInterface _pi; + private readonly IClientState _clientState; + private readonly IObjectTable _objects; public readonly EventSubscriber Redrawn; private string _redrawName = string.Empty; private int _redrawIndex = 0; private string _lastRedrawnString = "None"; - public Redrawing(DalamudServices dalamud) + public Redrawing(DalamudPluginInterface pi, IObjectTable objects, IClientState clientState) { - _dalamud = dalamud; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); + _pi = pi; + _objects = objects; + _clientState = clientState; + Redrawn = Ipc.GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); } public void Draw() @@ -426,25 +431,25 @@ public class IpcTester : IDisposable ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32); ImGui.SameLine(); if (ImGui.Button("Redraw##Name")) - Ipc.RedrawObjectByName.Subscriber(_dalamud.PluginInterface).Invoke(_redrawName, RedrawType.Redraw); + Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw); DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character"); - if (ImGui.Button("Redraw##pc") && _dalamud.ClientState.LocalPlayer != null) - Ipc.RedrawObject.Subscriber(_dalamud.PluginInterface).Invoke(_dalamud.ClientState.LocalPlayer, RedrawType.Redraw); + if (ImGui.Button("Redraw##pc") && _clientState.LocalPlayer != null) + Ipc.RedrawObject.Subscriber(_pi).Invoke(_clientState.LocalPlayer, RedrawType.Redraw); DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); var tmp = _redrawIndex; ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _dalamud.Objects.Length)) - _redrawIndex = Math.Clamp(tmp, 0, _dalamud.Objects.Length); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.Length)) + _redrawIndex = Math.Clamp(tmp, 0, _objects.Length); ImGui.SameLine(); if (ImGui.Button("Redraw##Index")) - Ipc.RedrawObjectByIndex.Subscriber(_dalamud.PluginInterface).Invoke(_redrawIndex, RedrawType.Redraw); + Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw); DrawIntro(Ipc.RedrawAll.Label, "Redraw All"); if (ImGui.Button("Redraw##All")) - Ipc.RedrawAll.Subscriber(_dalamud.PluginInterface).Invoke(RedrawType.Redraw); + Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw); DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:"); ImGui.TextUnformatted(_lastRedrawnString); @@ -453,12 +458,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn(IntPtr address, int index) { if (index < 0 - || index > _dalamud.Objects.Length + || index > _objects.Length || address == IntPtr.Zero - || _dalamud.Objects[index]?.Address != address) + || _objects[index]?.Address != address) _lastRedrawnString = "Invalid"; - _lastRedrawnString = $"{_dalamud.Objects[index]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{_objects[index]!.Name} (0x{address:X}, {index})"; } } @@ -1529,7 +1534,7 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourceTrees")) { var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(_pi); + var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(_pi); _stopwatch.Restart(); var trees = subscriber.Invoke(_withUIData, gameObjects); @@ -1563,7 +1568,7 @@ public class IpcTester : IDisposable DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); - DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); + DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); } @@ -1695,9 +1700,10 @@ public class IpcTester : IDisposable { ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); } - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); + + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableHeadersRow(); @@ -1707,10 +1713,10 @@ public class IpcTester : IDisposable ImGui.TableNextColumn(); var hasChildren = node.Children.Any(); using var treeNode = ImRaii.TreeNode( - $"{(_withUIData ? (node.Name ?? "Unknown") : node.Type)}##{node.ObjectAddress:X8}", - hasChildren ? - ImGuiTreeNodeFlags.SpanFullWidth : - (ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen)); + $"{(_withUIData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", + hasChildren + ? ImGuiTreeNodeFlags.SpanFullWidth + : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); if (_withUIData) { ImGui.TableNextColumn(); @@ -1718,6 +1724,7 @@ public class IpcTester : IDisposable ImGui.TableNextColumn(); TextUnformattedMono(node.Icon.ToString()); } + ImGui.TableNextColumn(); ImGui.TextUnformatted(node.GamePath ?? "Unknown"); ImGui.TableNextColumn(); @@ -1728,10 +1735,8 @@ public class IpcTester : IDisposable TextUnformattedMono($"0x{node.ResourceHandle:X8}"); if (treeNode) - { foreach (var child in node.Children) DrawNode(child); - } } foreach (var node in tree.Nodes) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 97e69089..b7a46ae2 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; using Lumina.Data; using Newtonsoft.Json; using OtterGui; @@ -88,11 +89,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi private CommunicatorService _communicator; private Lumina.GameData? _lumina; + private IDataManager _gameData; + private IFramework _framework; + private IObjectTable _objects; private ModManager _modManager; private ResourceLoader _resourceLoader; private Configuration _config; private CollectionManager _collectionManager; - private DalamudServices _dalamud; private TempCollectionManager _tempCollections; private TempModManager _tempMods; private ActorManager _actors; @@ -106,18 +109,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi private TextureManager _textureManager; private ResourceTreeFactory _resourceTreeFactory; - public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, - Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, - TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, - ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, - ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) + public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, IObjectTable objects, + ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, + TempCollectionManager tempCollections, TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, + CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, + ModFileSystem modFileSystem, ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) { _communicator = communicator; + _gameData = gameData; + _framework = framework; + _objects = objects; _modManager = modManager; _resourceLoader = resourceLoader; _config = config; _collectionManager = collectionManager; - _dalamud = dalamud; _tempCollections = tempCollections; _tempMods = tempMods; _actors = actors; @@ -130,7 +135,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _configWindow = configWindow; _textureManager = textureManager; _resourceTreeFactory = resourceTreeFactory; - _lumina = _dalamud.GameData.GameData; + _lumina = gameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); @@ -153,7 +158,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader = null!; _config = null!; _collectionManager = null!; - _dalamud = null!; _tempCollections = null!; _tempMods = null!; _actors = null!; @@ -166,6 +170,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _configWindow = null!; _textureManager = null!; _resourceTreeFactory = null!; + _framework = null!; } public event ChangedItemClick? ChangedItemClicked @@ -399,7 +404,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return await Task.Run(async () => { - var playerCollection = await _dalamud.Framework.RunOnFrameworkThread(_collectionResolver.PlayerCollection).ConfigureAwait(false); + var playerCollection = await _framework.RunOnFrameworkThread(_collectionResolver.PlayerCollection).ConfigureAwait(false); var forwardTask = Task.Run(() => { var forwardRet = new string[forward.Length]; @@ -851,7 +856,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) + if (!ActorIdentifierFactory.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) return (PenumbraApiEc.InvalidArgument, string.Empty); var identifier = NameToIdentifier(character, ushort.MaxValue); @@ -889,10 +894,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (actorIndex < 0 || actorIndex >= _dalamud.Objects.Length) + if (actorIndex < 0 || actorIndex >= _objects.Length) return PenumbraApiEc.InvalidArgument; - var identifier = _actors.FromObject(_dalamud.Objects[actorIndex], false, false, true); + var identifier = _actors.FromObject(_objects[actorIndex], false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; @@ -1037,7 +1042,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { - var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var characters = gameObjects.Select(index => _objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); @@ -1055,7 +1060,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUIData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var characters = gameObjects.Select(index => _objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); @@ -1074,7 +1079,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => _dalamud.Objects[index]).OfType(); + var characters = gameObjects.Select(index => _objects[index]).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); @@ -1126,10 +1131,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = _collectionManager.Active.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Length) return false; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(gameObjectIdx); var data = _collectionResolver.IdentifyCollection(ptr, false); if (data.Valid) collection = data.ModCollection; @@ -1140,10 +1145,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Length) return ActorIdentifier.Invalid; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(gameObjectIdx); return _actors.FromObject(ptr, out _, false, true, true); } @@ -1168,7 +1173,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (Path.IsPathRooted(resolvedPath)) return _lumina?.GetFileFromDisk(resolvedPath); - return _dalamud.GameData.GetFile(resolvedPath); + return _gameData.GetFile(resolvedPath); } catch (Exception e) { diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index a564588b..1df90dd9 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -15,7 +15,6 @@ using CurrentSettings = ValueTuple GetGameObjectResourceTrees; internal readonly FuncProvider> GetPlayerResourceTrees; - public PenumbraIpcProviders(DalamudServices dalamud, IPenumbraApi api, ModManager modManager, CollectionManager collections, + public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) { - var pi = dalamud.PluginInterface; Api = api; // Plugin State @@ -260,15 +258,11 @@ public class PenumbraIpcProviders : IDisposable GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees); - Tester = new IpcTester(config, dalamud, this, modManager, collections, tempMods, tempCollections, saveService); - Initialized.Invoke(); } public void Dispose() { - Tester.Dispose(); - // Plugin State Initialized.Dispose(); ApiVersion.Dispose(); diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 629cdff5..6f854022 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -44,8 +44,8 @@ public static class TexFileParser if (offset == 0) return i; - var requiredSize = width * height * bits / 8; - if (offset + requiredSize > data.Length) + var Size = width * height * bits / 8; + if (offset + Size > data.Length) return i; var diff = offset - lastOffset; @@ -55,7 +55,7 @@ public static class TexFileParser width = Math.Max(width / 2, minSize); height = Math.Max(height / 2, minSize); lastOffset = offset; - lastSize = requiredSize; + lastSize = Size; } return 13; diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index b77ac1e0..7d94b1d5 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -13,7 +13,7 @@ namespace Penumbra.Interop.ResourceLoading; /// public unsafe class CreateFileWHook : IDisposable { - public const int RequiredSize = 28; + public const int Size = 28; public CreateFileWHook(IGameInteropProvider interop) { @@ -57,8 +57,8 @@ public unsafe class CreateFileWHook : IDisposable ptr[22] = (byte)(l >> 16); ptr[24] = (byte)(l >> 24); - ptr[RequiredSize - 2] = 0; - ptr[RequiredSize - 1] = 0; + ptr[Size - 2] = 0; + ptr[Size - 1] = 0; } public void Dispose() diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9be40e66..900d2770 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,10 +1,9 @@ using Dalamud.Plugin; -using Dalamud.Plugin.Services; using ImGuiNET; using Lumina.Excel.GeneratedSheets; -using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -19,7 +18,6 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.UI; @@ -48,40 +46,40 @@ public class Penumbra : IDalamudPlugin private PenumbraWindowSystem? _windowSystem; private bool _disposed; - private readonly ServiceProvider _services; + private readonly ServiceManager _services; public Penumbra(DalamudPluginInterface pluginInterface) { try { - _services = ServiceManager.CreateProvider(this, pluginInterface, Log); - Messager = _services.GetRequiredService(); - _validityChecker = _services.GetRequiredService(); - var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) + _services = ServiceManagerA.CreateProvider(this, pluginInterface, Log); + Messager = _services.GetService(); + _validityChecker = _services.GetService(); + var startup = _services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s) ? s.ToString() : "Unknown"; Log.Information( $"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); - _services.GetRequiredService(); // Initialize because not required anywhere else. - _config = _services.GetRequiredService(); - _characterUtility = _services.GetRequiredService(); - _tempMods = _services.GetRequiredService(); - _residentResources = _services.GetRequiredService(); - _services.GetRequiredService(); // Initialize because not required anywhere else. - _modManager = _services.GetRequiredService(); - _collectionManager = _services.GetRequiredService(); - _tempCollections = _services.GetRequiredService(); - _redrawService = _services.GetRequiredService(); - _communicatorService = _services.GetRequiredService(); - _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetService(); // Initialize because not required anywhere else. + _config = _services.GetService(); + _characterUtility = _services.GetService(); + _tempMods = _services.GetService(); + _residentResources = _services.GetService(); + _services.GetService(); // Initialize because not required anywhere else. + _modManager = _services.GetService(); + _collectionManager = _services.GetService(); + _tempCollections = _services.GetService(); + _redrawService = _services.GetService(); + _communicatorService = _services.GetService(); + _services.GetService(); // Initialize because not required anywhere else. + _services.GetService(); // Initialize because not required anywhere else. + _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); - _services.GetRequiredService(); + _services.GetService(); - _services.GetRequiredService(); + _services.GetService(); - _services.GetRequiredService(); // Initialize before Interface. + _services.GetService(); // Initialize before Interface. SetupInterface(); SetupApi(); @@ -104,8 +102,8 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { - var api = _services.GetRequiredService(); - _services.GetRequiredService(); + var api = _services.GetService(); + _services.GetService(); _communicatorService.ChangedItemHover.Subscribe(it => { if (it is (Item, FullEquipType)) @@ -123,9 +121,9 @@ public class Penumbra : IDalamudPlugin { AsyncTask.Run(() => { - var system = _services.GetRequiredService(); - system.Window.Setup(this, _services.GetRequiredService()); - _services.GetRequiredService(); + var system = _services.GetService(); + system.Window.Setup(this, _services.GetService()); + _services.GetService(); if (!_disposed) _windowSystem = system; else @@ -194,7 +192,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append( - $"> **`Synchronous Load (Dalamud): `** {(_services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); + $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index d1682985..28dbc90d 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -9,7 +9,7 @@ "Tags": [ "modding" ], "DalamudApiLevel": 9, "LoadPriority": 69420, - "LoadRequiredState": 2, + "LoadState": 2, "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index 99539c39..51fb1192 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -5,15 +5,15 @@ using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Interface.DragDrop; using Dalamud.Plugin.Services; -using Microsoft.Extensions.DependencyInjection; +using OtterGui.Services; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local namespace Penumbra.Services; -public class DalamudServices +public class DalamudConfigService { - public DalamudServices(DalamudPluginInterface pluginInterface) + public DalamudConfigService(DalamudPluginInterface pluginInterface) { pluginInterface.Inject(this); try @@ -52,55 +52,6 @@ public class DalamudServices } } - public void AddServices(IServiceCollection services) - { - services.AddSingleton(PluginInterface); - services.AddSingleton(Commands); - services.AddSingleton(GameData); - services.AddSingleton(ClientState); - services.AddSingleton(Chat); - services.AddSingleton(Framework); - services.AddSingleton(Conditions); - services.AddSingleton(Targets); - services.AddSingleton(Objects); - services.AddSingleton(TitleScreenMenu); - services.AddSingleton(GameGui); - services.AddSingleton(KeyState); - services.AddSingleton(SigScanner); - services.AddSingleton(this); - services.AddSingleton(UiBuilder); - services.AddSingleton(DragDropManager); - services.AddSingleton(TextureProvider); - services.AddSingleton(TextureSubstitutionProvider); - services.AddSingleton(Interop); - services.AddSingleton(Log); - } - - // TODO remove static - // @formatter:off - [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ICommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IDataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IFramework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ICondition Conditions { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITitleScreenMenu TitleScreenMenu { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IGameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IKeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ISigScanner SigScanner { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITextureProvider TextureProvider { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITextureSubstitutionProvider TextureSubstitutionProvider { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IGameInteropProvider Interop { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IPluginLog Log { get; private set; } = null!; - // @formatter:on - - public UiBuilder UiBuilder - => PluginInterface.UiBuilder; - public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; private readonly object? _dalamudConfig; @@ -164,3 +115,29 @@ public class DalamudServices } } } + +public static class DalamudServices +{ + public static void AddServices(ServiceManager services, DalamudPluginInterface pi) + { + services.AddExistingService(pi); + services.AddExistingService(pi.UiBuilder); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + } +} diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManagerA.cs similarity index 72% rename from Penumbra/Services/ServiceManager.cs rename to Penumbra/Services/ServiceManagerA.cs index 30f58701..410acfb9 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -7,11 +7,10 @@ using OtterGui.Services; using Penumbra.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; -using Penumbra.GameData; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; -using Penumbra.GameData.DataContainers.Bases; +using Penumbra.GameData.Structs; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; @@ -33,14 +32,13 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage namespace Penumbra.Services; -public static class ServiceManager +public static class ServiceManagerA { - public static ServiceProvider CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { - var services = new ServiceCollection() - .AddSingleton(log) - .AddSingleton(penumbra) - .AddDalamud(pi) + var services = new ServiceManager(log) + .AddExistingService(log) + .AddExistingService(penumbra) .AddMeta() .AddGameData() .AddInterop() @@ -51,37 +49,15 @@ public static class ServiceManager .AddResolvers() .AddInterface() .AddModEditor() - .AddApi() - .AddDataContainers() - .AddAsyncServices(); - - return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); - } - - private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi) - { - var dalamud = new DalamudServices(pi); - dalamud.AddServices(services); + .AddApi(); + services.AddIServices(typeof(EquipItem).Assembly); + services.AddIServices(typeof(Penumbra).Assembly); + DalamudServices.AddServices(services, pi); + services.CreateProvider(); return services; } - private static IServiceCollection AddDataContainers(this IServiceCollection services) - { - foreach (var type in typeof(IDataContainer).Assembly.GetExportedTypes() - .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IDataContainer)))) - services.AddSingleton(type); - return services; - } - - private static IServiceCollection AddAsyncServices(this IServiceCollection services) - { - foreach (var type in typeof(ActorManager).Assembly.GetExportedTypes() - .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IService)))) - services.AddSingleton(type); - return services; - } - - private static IServiceCollection AddMeta(this IServiceCollection services) + private static ServiceManager AddMeta(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -89,15 +65,16 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); - private static IServiceCollection AddGameData(this IServiceCollection services) + private static ServiceManager AddGameData(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddInterop(this IServiceCollection services) + private static ServiceManager AddInterop(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -117,12 +94,12 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddConfiguration(this IServiceCollection services) - => services.AddTransient() + private static ServiceManager AddConfiguration(this ServiceManager services) + => services.AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddCollections(this IServiceCollection services) + private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -132,7 +109,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddMods(this IServiceCollection services) + private static ServiceManager AddMods(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -144,14 +121,14 @@ public static class ServiceManager .AddSingleton() .AddSingleton(s => (ModStorage)s.GetRequiredService()); - private static IServiceCollection AddResources(this IServiceCollection services) + private static ServiceManager AddResources(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddResolvers(this IServiceCollection services) + private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -162,7 +139,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddInterface(this IServiceCollection services) + private static ServiceManager AddInterface(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -200,7 +177,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(p => new Diagnostics(p)); - private static IServiceCollection AddModEditor(this IServiceCollection services) + private static ServiceManager AddModEditor(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -212,10 +189,11 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddApi(this IServiceCollection services) + private static ServiceManager AddApi(this ServiceManager services) => services.AddSingleton() .AddSingleton(x => x.GetRequiredService()) .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 0688850b..7287938c 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -15,7 +15,7 @@ public class ValidityChecker public readonly bool IsNotInstalledPenumbra; public readonly bool IsValidSourceRepo; - public readonly List ImcExceptions = new(); + public readonly List ImcExceptions = []; public readonly string Version; public readonly string CommitHash; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 954e9b0f..09e22d67 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -103,7 +103,7 @@ public partial class ModEditWindow LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._dalamud.GameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; + : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); LoadedShpkPathName = path.ToPath(); } @@ -457,13 +457,13 @@ public partial class ModEditWindow var foundMaterials = new HashSet(); foreach (var materialInfo in instances) { - var material = materialInfo.GetDrawObjectMaterial(_edit._dalamud.Objects); + var material = materialInfo.GetDrawObjectMaterial(_edit._objects); if (foundMaterials.Contains((nint)material)) continue; try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo)); + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -481,7 +481,7 @@ public partial class ModEditWindow { try { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); } catch (InvalidOperationException) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 64457c25..c9cd3d06 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -71,7 +71,7 @@ public partial class ModEditWindow } else { - var file = _dalamud.GameData.GetFile(path); + var file = _gameData.GetFile(path); writable = file == null ? null : new RawGameFileWritable(file); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index a3b17848..e9facdf4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -219,7 +219,7 @@ public partial class ModEditWindow if (tex.Path != path) return; - _dalamud.Framework.RunOnFrameworkThread(() => tex.Reload(_textures)); + _framework.RunOnFrameworkThread(() => tex.Reload(_textures)); }); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index db9201ca..9b127fe4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -36,7 +36,6 @@ public partial class ModEditWindow : Window, IDisposable private readonly ModEditor _editor; private readonly Configuration _config; private readonly ItemSwapTab _itemSwapTab; - private readonly DalamudServices _dalamud; private readonly MetaFileManager _metaFileManager; private readonly ActiveCollections _activeCollections; private readonly StainService _stainService; @@ -44,6 +43,9 @@ public partial class ModEditWindow : Window, IDisposable private readonly CommunicatorService _communicator; private readonly IDragDropManager _dragDropManager; private readonly GameEventManager _gameEvents; + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly IObjectTable _objects; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -562,37 +564,41 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, + StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, - ChangedItemDrawer changedItemDrawer) + ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework) : base(WindowBaseLabel) { _performance = performance; _itemSwapTab = itemSwapTab; + _gameData = gameData; _config = config; _editor = editor; _metaFileManager = metaFileManager; _stainService = stainService; _activeCollections = activeCollections; - _dalamud = dalamud; _modMergeTab = modMergeTab; _communicator = communicator; _dragDropManager = dragDropManager; _textures = textures; _fileDialog = fileDialog; _gameEvents = gameEvents; + _objects = objects; + _framework = framework; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, + (bytes, _, _) => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", - () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, + () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, + () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index cf65901e..8f12afbb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -190,9 +190,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector Label @@ -417,7 +421,7 @@ public class DebugTab : Window, ITab DrawSpecial("Current Card", _actors.GetCardPlayer()); DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - foreach (var obj in _dalamud.Objects) + foreach (var obj in _objects) { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); @@ -827,7 +831,7 @@ public class DebugTab : Window, ITab /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { - var player = _dalamud.ClientState.LocalPlayer; + var player = _clientState.LocalPlayer; var name = player?.Name.ToString() ?? "NULL"; if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; @@ -952,11 +956,11 @@ public class DebugTab : Window, ITab { if (!ImGui.CollapsingHeader("IPC")) { - _ipc.Tester.UnsubscribeEvents(); + _ipcTester.UnsubscribeEvents(); return; } - _ipc.Tester.Draw(); + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 104f8d91..a03e7b87 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,5 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using OtterGui; @@ -33,19 +35,23 @@ public class SettingsTab : ITab private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; - private readonly DalamudServices _dalamud; private readonly HttpApi _httpApi; private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private readonly FileCompactor _compactor; + private readonly DalamudConfigService _dalamudConfig; + private readonly DalamudPluginInterface _pluginInterface; + private readonly IDataManager _gameData; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; - public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, - FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi, - DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor) + public SettingsTab(DalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, + Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, + DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, + IDataManager gameData) { + _pluginInterface = pluginInterface; _config = config; _fontReloader = fontReloader; _tutorial = tutorial; @@ -55,11 +61,12 @@ public class SettingsTab : ITab _selector = selector; _characterUtility = characterUtility; _residentResources = residentResources; - _dalamud = dalamud; _modExportManager = modExportManager; _httpApi = httpApi; _dalamudSubstitutionProvider = dalamudSubstitutionProvider; _compactor = compactor; + _dalamudConfig = dalamudConfig; + _gameData = gameData; if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; } @@ -164,14 +171,14 @@ public class SettingsTab : ITab if (IsSubPathOf(programFiles, newName) || IsSubPathOf(programFilesX86, newName)) return ("Path is not allowed to be in ProgramFiles.", false); - var dalamud = _dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; + var dalamud = _pluginInterface.ConfigDirectory.Parent!.Parent!; if (IsSubPathOf(dalamud.FullName, newName)) return ("Path is not allowed to be inside your Dalamud directories.", false); if (Functions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName)) return ("Path is not allowed to be inside your Downloads folder.", false); - var gameDir = _dalamud.GameData.GameData.DataPath.Parent!.Parent!.FullName; + var gameDir = _gameData.GameData.DataPath.Parent!.Parent!.FullName; if (IsSubPathOf(gameDir, newName)) return ("Path is not allowed to be inside your game folder.", false); @@ -368,21 +375,21 @@ public class SettingsTab : ITab v => { _config.HideUiWhenUiHidden = v; - _dalamud.UiBuilder.DisableUserUiHide = !v; + _pluginInterface.UiBuilder.DisableUserUiHide = !v; }); Checkbox("Hide Config Window when in Cutscenes", "Hide the Penumbra main window when you are currently watching a cutscene.", _config.HideUiInCutscenes, v => { - _config.HideUiInCutscenes = v; - _dalamud.UiBuilder.DisableCutsceneUiHide = !v; + _config.HideUiInCutscenes = v; + _pluginInterface.UiBuilder.DisableCutsceneUiHide = !v; }); Checkbox("Hide Config Window when in GPose", "Hide the Penumbra main window when you are currently in GPose mode.", _config.HideUiInGPose, v => { - _config.HideUiInGPose = v; - _dalamud.UiBuilder.DisableGposeUiHide = !v; + _config.HideUiInGPose = v; + _pluginInterface.UiBuilder.DisableGposeUiHide = !v; }); } @@ -847,7 +854,7 @@ public class SettingsTab : ITab /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. private void DrawWaitForPluginsReflection() { - if (!_dalamud.GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool value)) + if (!_dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool value)) { using var disabled = ImRaii.Disabled(); Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { }); @@ -855,9 +862,12 @@ public class SettingsTab : ITab else { Checkbox("Wait for Plugins on Startup", - "Some mods need to change files that are loaded once when the game starts and never afterwards.\nThis can cause issues with Penumbra loading after the files are already loaded.\nThis setting causes the game to wait until certain plugins have finished loading, making those mods work (in the base collection).\n\nThis changes a setting in the Dalamud Configuration found at /xlsettings -> General.", + "Some mods need to change files that are loaded once when the game starts and never afterwards.\n" + + "This can cause issues with Penumbra loading after the files are already loaded.\n" + + "This setting causes the game to wait until certain plugins have finished loading, making those mods work (in the base collection).\n\n" + + "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => _dalamud.SetDalamudConfig(DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup")); + v => _dalamudConfig.SetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup")); } } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 9172bb60..16353828 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -73,7 +73,10 @@ } }, "ottergui": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[7.0.0, )" + } }, "penumbra.api": { "type": "Project" From 969ba38ffe01578472547430aeff6427b2291130 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Dec 2023 10:40:31 +0100 Subject: [PATCH 1331/2451] Prevent layer editing. --- Penumbra/Interop/PathResolving/PathResolver.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 12e5e280..6db97b63 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -51,6 +51,10 @@ public class PathResolver : IDisposable if (!_config.EnableMods) return (null, ResolveData.Invalid); + // Do not allow manipulating layers to prevent very obvious cheating and softlocks. + if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) + return (null, ResolveData.Invalid); + path = path.ToLower(); return category switch { From 6d89ea5a712c04b01de80ebdbec560eaeb599aa1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 21 Dec 2023 09:43:40 +0000 Subject: [PATCH 1332/2451] [CI] Updating repo.json for 0.8.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a04b35b5..173f6592 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.3.0", - "TestingAssemblyVersion": "0.8.3.0", + "AssemblyVersion": "0.8.3.1", + "TestingAssemblyVersion": "0.8.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 2a0e6ce1aa61b8d882364812b3a929cc50697fa3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 02:48:11 +1100 Subject: [PATCH 1333/2451] WIP .mdl updates --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 113 ++++++++++++++++-- 1 file changed, 101 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b95ba393..001e1c78 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,6 +1,8 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -10,28 +12,115 @@ public partial class ModEditWindow { private readonly FileEditor _modelTab; + private static List _submeshAttributeTagWidgets = new(); + private static bool DrawModelPanel(MdlFile file, bool disabled) { - var ret = false; - for (var i = 0; i < file.Materials.Length; ++i) + var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_submeshAttributeTagWidgets.Count != submeshTotal) { - using var id = ImRaii.PushId(i); - var tmp = file.Materials[i]; - if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && tmp.Length > 0 - && tmp != file.Materials[i]) - { - file.Materials[i] = tmp; - ret = true; - } + _submeshAttributeTagWidgets.Clear(); + _submeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, submeshTotal).Select(_ => new TagButtons()) + ); } + var ret = false; + + for (var i = 0; i < file.Meshes.Length; ++i) + ret |= DrawMeshDetails(file, i, disabled); + ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } + private static bool DrawMeshDetails(MdlFile file, int meshIndex, bool disabled) + { + if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) + return false; + + using var id = ImRaii.PushId(meshIndex); + + var mesh = file.Meshes[meshIndex]; + + var ret = false; + + // Mesh material. + var temp = file.Materials[mesh.MaterialIndex]; + if ( + ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && temp.Length > 0 + && temp != file.Materials[mesh.MaterialIndex] + ) { + file.Materials[mesh.MaterialIndex] = temp; + ret = true; + } + + // Submeshes. + for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) + ret |= DrawSubMeshDetails(file, mesh.SubMeshIndex + submeshOffset, disabled); + + return ret; + } + + private static bool DrawSubMeshDetails(MdlFile file, int submeshIndex, bool disabled) + { + using var id = ImRaii.PushId(submeshIndex); + + var submesh = file.SubMeshes[submeshIndex]; + var widget = _submeshAttributeTagWidgets[submeshIndex]; + + var attributes = Enumerable + .Range(0, 32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => file.Attributes[index]) + .ToArray(); + + UiHelpers.DefaultLineSpace(); + var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); + if (tagIndex >= 0) + { + // Eagerly remove the edited attribute from the attribute mask. + if (tagIndex < attributes.Length) + { + var previousAttributeIndex = file.Attributes.IndexOf(attributes[tagIndex]); + submesh.AttributeIndexMask &= ~(1u << previousAttributeIndex); + + // If no other submeshes use this attribute, remove it. + var usages = file.SubMeshes + .Where(submesh => ((submesh.AttributeIndexMask >> previousAttributeIndex) & 1) == 1) + .Count(); + if (usages <= 1) + { + // TODO THIS BLOWS UP ALL OTHER INDICES BEYOND WHAT WE JUST REMOVED - I NEED TO VIRTUALISE THIS SHIT + // file.Attributes = file.Attributes.RemoveItems(previousAttributeIndex); + } + } + + // If there's a new or edited name, add it to the mask, and the attribute list if it's not already known. + if (editedAttribute != "") + { + var attributeIndex = file.Attributes.IndexOf(editedAttribute); + if (attributeIndex == -1) + { + file.Attributes.AddItem(editedAttribute); + attributeIndex = file.Attributes.Length - 1; + } + submesh.AttributeIndexMask |= 1u << attributeIndex; + } + + file.SubMeshes[submeshIndex] = submesh; + + return true; + } + + ImGui.SameLine(); + ImGui.Text($"{Convert.ToString(submesh.AttributeIndexMask, 2)}"); + + return false; + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 49b63d2208bf8b4c62bc02f83a7b447c951d8410 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 03:32:44 +1100 Subject: [PATCH 1334/2451] Draw the rest of the owl --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 001e1c78..8edc4082 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -71,56 +72,73 @@ public partial class ModEditWindow var submesh = file.SubMeshes[submeshIndex]; var widget = _submeshAttributeTagWidgets[submeshIndex]; - var attributes = Enumerable - .Range(0, 32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => file.Attributes[index]) - .ToArray(); + var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); UiHelpers.DefaultLineSpace(); var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { - // Eagerly remove the edited attribute from the attribute mask. - if (tagIndex < attributes.Length) - { - var previousAttributeIndex = file.Attributes.IndexOf(attributes[tagIndex]); - submesh.AttributeIndexMask &= ~(1u << previousAttributeIndex); - - // If no other submeshes use this attribute, remove it. - var usages = file.SubMeshes - .Where(submesh => ((submesh.AttributeIndexMask >> previousAttributeIndex) & 1) == 1) - .Count(); - if (usages <= 1) - { - // TODO THIS BLOWS UP ALL OTHER INDICES BEYOND WHAT WE JUST REMOVED - I NEED TO VIRTUALISE THIS SHIT - // file.Attributes = file.Attributes.RemoveItems(previousAttributeIndex); - } - } - - // If there's a new or edited name, add it to the mask, and the attribute list if it's not already known. - if (editedAttribute != "") - { - var attributeIndex = file.Attributes.IndexOf(editedAttribute); - if (attributeIndex == -1) - { - file.Attributes.AddItem(editedAttribute); - attributeIndex = file.Attributes.Length - 1; - } - submesh.AttributeIndexMask |= 1u << attributeIndex; - } - - file.SubMeshes[submeshIndex] = submesh; + EditSubmeshAttribute( + file, + submeshIndex, + tagIndex < attributes.Length ? attributes[tagIndex] : null, + editedAttribute != "" ? editedAttribute : null + ); return true; } - ImGui.SameLine(); - ImGui.Text($"{Convert.ToString(submesh.AttributeIndexMask, 2)}"); - return false; } + private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) + { + // Build a hydrated view of all attributes in the model + var submeshAttributes = file.SubMeshes + .Select(submesh => HydrateAttributes(file, submesh.AttributeIndexMask).ToList()) + .ToArray(); + + // Make changes to the submesh we're actually editing here. + var changedSubmesh = submeshAttributes[changedSubmeshIndex]; + + if (old != null) + changedSubmesh.Remove(old); + + if (new_ != null) + changedSubmesh.Add(new_); + + // Re-serialize all the attributes. + var allAttributes = new List(); + foreach (var (attributes, submeshIndex) in submeshAttributes.WithIndex()) + { + var mask = 0u; + + foreach (var attribute in attributes) + { + var attributeIndex = allAttributes.IndexOf(attribute); + if (attributeIndex == -1) + { + allAttributes.Add(attribute); + attributeIndex = allAttributes.Count() - 1; + } + + mask |= 1u << attributeIndex; + } + + file.SubMeshes[submeshIndex].AttributeIndexMask = mask; + } + + file.Attributes = allAttributes.ToArray(); + } + + private static IEnumerable HydrateAttributes(MdlFile file, uint mask) + { + return Enumerable + .Range(0, 32) + .Where(index => ((mask >> index) & 1) == 1) + .Select(index => file.Attributes[index]); + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 8ba20218c620cef270d4cfcdf0ccb1591d5b5ef0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 03:35:17 +1100 Subject: [PATCH 1335/2451] whoops --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8edc4082..b41dbf0c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,9 +1,7 @@ using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; From 27123f2a640ee93908344e0351b3080a2808ca42 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 19:00:06 +1100 Subject: [PATCH 1336/2451] Inline submesh UI, fix visual offset --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b41dbf0c..d43ae55b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -58,35 +58,32 @@ public partial class ModEditWindow // Submeshes. for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) - ret |= DrawSubMeshDetails(file, mesh.SubMeshIndex + submeshOffset, disabled); - - return ret; - } - - private static bool DrawSubMeshDetails(MdlFile file, int submeshIndex, bool disabled) - { - using var id = ImRaii.PushId(submeshIndex); - - var submesh = file.SubMeshes[submeshIndex]; - var widget = _submeshAttributeTagWidgets[submeshIndex]; - - var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); - - UiHelpers.DefaultLineSpace(); - var tagIndex = widget.Draw($"Submesh {submeshIndex} Attributes", "", attributes, out var editedAttribute, !disabled); - if (tagIndex >= 0) { - EditSubmeshAttribute( - file, - submeshIndex, - tagIndex < attributes.Length ? attributes[tagIndex] : null, - editedAttribute != "" ? editedAttribute : null - ); + using var submeshId = ImRaii.PushId(submeshOffset); - return true; + var submeshIndex = mesh.SubMeshIndex + submeshOffset; + + var submesh = file.SubMeshes[submeshIndex]; + var widget = _submeshAttributeTagWidgets[submeshIndex]; + + var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); + + UiHelpers.DefaultLineSpace(); + var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); + if (tagIndex >= 0) + { + EditSubmeshAttribute( + file, + submeshIndex, + tagIndex < attributes.Length ? attributes[tagIndex] : null, + editedAttribute != "" ? editedAttribute : null + ); + + ret = true; + } } - return false; + return ret; } private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) From f04b2959891252453310a1eb9bec5c80ac7dba3a Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 19:06:55 +1100 Subject: [PATCH 1337/2451] Scaffold tab file --- .../ModEditWindow.Models.MdlTab.cs | 20 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs new file mode 100644 index 00000000..aeae20cc --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -0,0 +1,20 @@ +using Penumbra.GameData.Files; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private class MdlTab : IWritable + { + public readonly MdlFile Mdl; + + public MdlTab(byte[] bytes) + { + Mdl = new MdlFile(bytes); + } + + public bool Valid => Mdl.Valid; + + public byte[] Write() => Mdl.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index d43ae55b..bcfc77ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -9,12 +9,14 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor _modelTab; + private readonly FileEditor _modelTab; private static List _submeshAttributeTagWidgets = new(); - private static bool DrawModelPanel(MdlFile file, bool disabled) + private static bool DrawModelPanel(MdlTab tab, bool disabled) { + var file = tab.Mdl; + var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); if (_submeshAttributeTagWidgets.Count != submeshTotal) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index db9201ca..365c4a4a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -584,8 +584,8 @@ public partial class ModEditWindow : Window, IDisposable _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); - _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); + _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlTab(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); From 28246244cd04c37689278a2c13259fc17508bf68 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:25:01 +1100 Subject: [PATCH 1338/2451] Move persitence logic to tab file --- .../ModEditWindow.Models.MdlTab.cs | 87 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 69 ++------------- 2 files changed, 96 insertions(+), 60 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index aeae20cc..3ba39543 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; +using OtterGui; using Penumbra.GameData.Files; namespace Penumbra.UI.AdvancedWindow; @@ -8,9 +10,94 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; + private List _materials; + private List[] _attributes; + public MdlTab(byte[] bytes) { Mdl = new MdlFile(bytes); + + _materials = Mdl.Meshes.Select(mesh => Mdl.Materials[mesh.MaterialIndex]).ToList(); + _attributes = HydrateAttributes(Mdl); + } + + private List[] HydrateAttributes(MdlFile mdl) + { + return mdl.SubMeshes.Select(submesh => + Enumerable.Range(0,32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => mdl.Attributes[index]) + .ToList() + ).ToArray(); + } + + public string GetMeshMaterial(int meshIndex) => _materials[meshIndex]; + + public void SetMeshMaterial(int meshIndex, string materialPath) + { + _materials[meshIndex] = materialPath; + + PersistMaterials(); + } + + private void PersistMaterials() + { + var allMaterials = new List(); + + foreach (var (material, meshIndex) in _materials.WithIndex()) + { + var materialIndex = allMaterials.IndexOf(material); + if (materialIndex == -1) + { + allMaterials.Add(material); + materialIndex = allMaterials.Count() - 1; + } + + Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + } + + Mdl.Materials = allMaterials.ToArray(); + } + + public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; + + public void UpdateSubmeshAttribute(int submeshIndex, string? old, string? new_) + { + var attributes = _attributes[submeshIndex]; + + if (old != null) + attributes.Remove(old); + + if (new_ != null) + attributes.Add(new_); + + PersistAttributes(); + } + + private void PersistAttributes() + { + var allAttributes = new List(); + + foreach (var (attributes, submeshIndex) in _attributes.WithIndex()) + { + var mask = 0u; + + foreach (var attribute in attributes) + { + var attributeIndex = allAttributes.IndexOf(attribute); + if (attributeIndex == -1) + { + allAttributes.Add(attribute); + attributeIndex = allAttributes.Count() - 1; + } + + mask |= 1u << attributeIndex; + } + + Mdl.SubMeshes[submeshIndex].AttributeIndexMask = mask; + } + + Mdl.Attributes = allAttributes.ToArray(); } public bool Valid => Mdl.Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bcfc77ad..0a4c9c1b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -29,32 +29,33 @@ public partial class ModEditWindow var ret = false; for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(file, i, disabled); + ret |= DrawMeshDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } - private static bool DrawMeshDetails(MdlFile file, int meshIndex, bool disabled) + private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) return false; using var id = ImRaii.PushId(meshIndex); + var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; var ret = false; // Mesh material. - var temp = file.Materials[mesh.MaterialIndex]; + var temp = tab.GetMeshMaterial(meshIndex); if ( ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) && temp.Length > 0 - && temp != file.Materials[mesh.MaterialIndex] + && temp != tab.GetMeshMaterial(meshIndex) ) { - file.Materials[mesh.MaterialIndex] = temp; + tab.SetMeshMaterial(meshIndex, temp); ret = true; } @@ -64,20 +65,16 @@ public partial class ModEditWindow using var submeshId = ImRaii.PushId(submeshOffset); var submeshIndex = mesh.SubMeshIndex + submeshOffset; - - var submesh = file.SubMeshes[submeshIndex]; var widget = _submeshAttributeTagWidgets[submeshIndex]; - - var attributes = HydrateAttributes(file, submesh.AttributeIndexMask).ToArray(); + var attributes = tab.GetSubmeshAttributes(submeshIndex); UiHelpers.DefaultLineSpace(); var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { - EditSubmeshAttribute( - file, + tab.UpdateSubmeshAttribute( submeshIndex, - tagIndex < attributes.Length ? attributes[tagIndex] : null, + tagIndex < attributes.Count() ? attributes.ElementAt(tagIndex) : null, editedAttribute != "" ? editedAttribute : null ); @@ -88,54 +85,6 @@ public partial class ModEditWindow return ret; } - private static void EditSubmeshAttribute(MdlFile file, int changedSubmeshIndex, string? old, string? new_) - { - // Build a hydrated view of all attributes in the model - var submeshAttributes = file.SubMeshes - .Select(submesh => HydrateAttributes(file, submesh.AttributeIndexMask).ToList()) - .ToArray(); - - // Make changes to the submesh we're actually editing here. - var changedSubmesh = submeshAttributes[changedSubmeshIndex]; - - if (old != null) - changedSubmesh.Remove(old); - - if (new_ != null) - changedSubmesh.Add(new_); - - // Re-serialize all the attributes. - var allAttributes = new List(); - foreach (var (attributes, submeshIndex) in submeshAttributes.WithIndex()) - { - var mask = 0u; - - foreach (var attribute in attributes) - { - var attributeIndex = allAttributes.IndexOf(attribute); - if (attributeIndex == -1) - { - allAttributes.Add(attribute); - attributeIndex = allAttributes.Count() - 1; - } - - mask |= 1u << attributeIndex; - } - - file.SubMeshes[submeshIndex].AttributeIndexMask = mask; - } - - file.Attributes = allAttributes.ToArray(); - } - - private static IEnumerable HydrateAttributes(MdlFile file, uint mask) - { - return Enumerable - .Range(0, 32) - .Where(index => ((mask >> index) & 1) == 1) - .Select(index => file.Attributes[index]); - } - private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 7ef50f7bb4767441387db5c9af1c71ac30511c28 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:28:29 +1100 Subject: [PATCH 1339/2451] Add material list to further content --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0a4c9c1b..1960614a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -135,6 +135,13 @@ public partial class ModEditWindow } } + using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (materials) + foreach (var material in file.Materials) + ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose(); + } + using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) From 17e6838422965a83056726a1388cdaaacc1f12a8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 21 Dec 2023 20:52:14 +1100 Subject: [PATCH 1340/2451] Swap to tree nodes for more compact UX --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1960614a..7e2f8f5f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -28,8 +28,9 @@ public partial class ModEditWindow var ret = false; - for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(tab, i, disabled); + if (ImGui.CollapsingHeader($"{file.Meshes.Length} Meshes###meshes")) + for (var i = 0; i < file.Meshes.Length; ++i) + ret |= DrawMeshDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); @@ -38,7 +39,8 @@ public partial class ModEditWindow private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - if (!ImGui.CollapsingHeader($"Mesh {meshIndex}")) + using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!meshNode) return false; using var id = ImRaii.PushId(meshIndex); From c138c39c068ef8eb10da401d73583db3995bf223 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Dec 2023 15:12:41 +0100 Subject: [PATCH 1341/2451] Change CharacterWeapon names. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 4d3570fd..fe0bf13f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4d3570fd47d78dbc49cf5e41fd3545a533ef9e81 +Subproject commit fe0bf13f1ae47b77684ccb4bb9e9f44e430acfbf From 2051197c65e823142bfa31667120cbf0984f0241 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Dec 2023 15:20:39 +0100 Subject: [PATCH 1342/2451] Change again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fe0bf13f..3787e82d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fe0bf13f1ae47b77684ccb4bb9e9f44e430acfbf +Subproject commit 3787e82d1b84d2542b6e4238060d75383a4b12a1 From 72f57d292b074d710f7c3325773669a1e6b1f6c7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 03:34:20 +1100 Subject: [PATCH 1343/2451] group meshes by lod --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 7e2f8f5f..c4c86e24 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -28,15 +28,31 @@ public partial class ModEditWindow var ret = false; - if (ImGui.CollapsingHeader($"{file.Meshes.Length} Meshes###meshes")) - for (var i = 0; i < file.Meshes.Length; ++i) - ret |= DrawMeshDetails(tab, i, disabled); + if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes")) + for (var i = 0; i < file.LodCount; ++i) + ret |= DrawLodDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } + private static bool DrawLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"LOD {lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!lodNode) + return false; + + var lod = tab.Mdl.Lods[lodIndex]; + + var ret = false; + + for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) + ret |= DrawMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); + + return ret; + } + private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) { using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); From 829016a1c4b236b5516e92f84101a6ea3ade3a5a Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 23:16:23 +1100 Subject: [PATCH 1344/2451] Spike improved UI --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 124 +++++++++++++++--- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index c4c86e24..0095ece8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -9,6 +10,8 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private const int MdlMaterialMaximum = 4; + private readonly FileEditor _modelTab; private static List _submeshAttributeTagWidgets = new(); @@ -28,18 +31,88 @@ public partial class ModEditWindow var ret = false; + ret |= DrawModelMaterialDetails(tab, disabled); + if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes")) for (var i = 0; i < file.LodCount; ++i) - ret |= DrawLodDetails(tab, i, disabled); + ret |= DrawModelLodDetails(tab, i, disabled); ret |= DrawOtherModelDetails(file, disabled); return !disabled && ret; } - private static bool DrawLodDetails(MdlTab tab, int lodIndex, bool disabled) + private static bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { - using var lodNode = ImRaii.TreeNode($"LOD {lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + if (!ImGui.CollapsingHeader("Materials")) + return false; + + var materials = tab.Mdl.Materials; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return false; + + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + + var inputFlags = ImGuiInputTextFlags.None; + if (disabled) + inputFlags |= ImGuiInputTextFlags.ReadOnly; + + for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text($"Material #{materialIndex + 1}"); + + var temp = materials[materialIndex]; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + + ImGui.TableNextColumn(); + var todoDelete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !ImGui.GetIO().KeyCtrl, true); + } + + if (materials.Length < MdlMaterialMaximum) + { + ImGui.TableNextColumn(); + + // todo: persist + var temp = ""; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + + // todo: flesh out this validation + var validName = temp != ""; + ImGui.TableNextColumn(); + var todoAdd = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true); + } + + // for (var index = 0; index < MdlMaterialMaximum; index++) + // { + // var temp = ""; + // ImGui.InputText($"Material {index}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + // } + + // var temp = tab.GetMeshMaterial(meshIndex); + // if ( + // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + // && temp.Length > 0 + // && temp != tab.GetMeshMaterial(meshIndex) + // ) { + // tab.SetMeshMaterial(meshIndex, temp); + // ret = true; + // } + return false; + } + + private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); if (!lodNode) return false; @@ -48,18 +121,24 @@ public partial class ModEditWindow var ret = false; for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) - ret |= DrawMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); + ret |= DrawModelMeshDetails(tab, lod.MeshIndex + meshOffset, disabled); return ret; } - private static bool DrawMeshDetails(MdlTab tab, int meshIndex, bool disabled) + private static bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - using var meshNode = ImRaii.TreeNode($"Mesh {meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); if (!meshNode) return false; using var id = ImRaii.PushId(meshIndex); + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return false; + + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("field", ImGuiTableColumnFlags.WidthStretch, 1); var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; @@ -67,14 +146,23 @@ public partial class ModEditWindow var ret = false; // Mesh material. - var temp = tab.GetMeshMaterial(meshIndex); - if ( - ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && temp.Length > 0 - && temp != tab.GetMeshMaterial(meshIndex) - ) { - tab.SetMeshMaterial(meshIndex, temp); - ret = true; + // var temp = tab.GetMeshMaterial(meshIndex); + // if ( + // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + // && temp.Length > 0 + // && temp != tab.GetMeshMaterial(meshIndex) + // ) { + // tab.SetMeshMaterial(meshIndex, temp); + // ret = true; + // } + ImGui.TableNextColumn(); + ImGui.Text("Material"); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + using (var materialCombo = ImRaii.Combo("##material", tab.GetMeshMaterial(meshIndex))) + { + // todo } // Submeshes. @@ -83,11 +171,15 @@ public partial class ModEditWindow using var submeshId = ImRaii.PushId(submeshOffset); var submeshIndex = mesh.SubMeshIndex + submeshOffset; + + ImGui.TableNextColumn(); + ImGui.Text($"Attributes #{submeshOffset}"); + + ImGui.TableNextColumn(); var widget = _submeshAttributeTagWidgets[submeshIndex]; var attributes = tab.GetSubmeshAttributes(submeshIndex); - UiHelpers.DefaultLineSpace(); - var tagIndex = widget.Draw($"Submesh {submeshOffset} Attributes", "", attributes, out var editedAttribute, !disabled); + var tagIndex = widget.Draw("", "", attributes, out var editedAttribute, !disabled); if (tagIndex >= 0) { tab.UpdateSubmeshAttribute( From a581495c7ea15058779c578acc51e12f247d1515 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 22 Dec 2023 23:49:50 +1100 Subject: [PATCH 1345/2451] Flesh out material wiring --- .../ModEditWindow.Models.MdlTab.cs | 57 ++++++--------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 69 +++++++++++-------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 3ba39543..5fac4f5e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using OtterGui; +using Penumbra.GameData; using Penumbra.GameData.Files; namespace Penumbra.UI.AdvancedWindow; @@ -10,53 +11,37 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; - private List _materials; private List[] _attributes; public MdlTab(byte[] bytes) { Mdl = new MdlFile(bytes); - - _materials = Mdl.Meshes.Select(mesh => Mdl.Materials[mesh.MaterialIndex]).ToList(); - _attributes = HydrateAttributes(Mdl); + _attributes = PopulateAttributes(); } - private List[] HydrateAttributes(MdlFile mdl) + public void RemoveMaterial(int materialIndex) { - return mdl.SubMeshes.Select(submesh => - Enumerable.Range(0,32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => mdl.Attributes[index]) - .ToList() - ).ToArray(); - } - - public string GetMeshMaterial(int meshIndex) => _materials[meshIndex]; - - public void SetMeshMaterial(int meshIndex, string materialPath) - { - _materials[meshIndex] = materialPath; - - PersistMaterials(); - } - - private void PersistMaterials() - { - var allMaterials = new List(); - - foreach (var (material, meshIndex) in _materials.WithIndex()) + // Meshes using the removed material are redirected to material 0, and those after the index are corrected. + for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { - var materialIndex = allMaterials.IndexOf(material); - if (materialIndex == -1) - { - allMaterials.Add(material); - materialIndex = allMaterials.Count() - 1; - } - - Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + var mesh = Mdl.Meshes[meshIndex]; + if (mesh.MaterialIndex == materialIndex) + mesh.MaterialIndex = 0; + else if (mesh.MaterialIndex > materialIndex) + mesh.MaterialIndex -= 1; } - Mdl.Materials = allMaterials.ToArray(); + Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); + } + + private List[] PopulateAttributes() + { + return Mdl.SubMeshes.Select(submesh => + Enumerable.Range(0,32) + .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) + .Select(index => Mdl.Attributes[index]) + .ToList() + ).ToArray(); } public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0095ece8..fe3ca644 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -14,6 +15,7 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; + private static string _modelNewMaterial = string.Empty; private static List _submeshAttributeTagWidgets = new(); private static bool DrawModelPanel(MdlTab tab, bool disabled) @@ -47,12 +49,13 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Materials")) return false; - var materials = tab.Mdl.Materials; - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return false; + var ret = false; + var materials = tab.Mdl.Materials; + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); @@ -63,6 +66,8 @@ public partial class ModEditWindow for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) { + using var id = ImRaii.PushId(materialIndex); + ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text($"Material #{materialIndex + 1}"); @@ -70,44 +75,52 @@ public partial class ModEditWindow var temp = materials[materialIndex]; ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); - + if ( + ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) + && temp.Length > 0 + && temp != materials[materialIndex] + ) { + materials[materialIndex] = temp; + ret = true; + } + ImGui.TableNextColumn(); - var todoDelete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !ImGui.GetIO().KeyCtrl, true); + + // Need to have at least one material. + if (materials.Length <= 1) + continue; + + if (ImGuiUtil.DrawDisabledButton( + FontAwesomeIcon.Trash.ToIconString(), + UiHelpers.IconButtonSize, + "Delete this material.\nAny meshes targeting this material will be updated to use material #1.\nHold Control while clicking to delete.", + disabled || !ImGui.GetIO().KeyCtrl, + true + )) { + tab.RemoveMaterial(materialIndex); + ret = true; + } } if (materials.Length < MdlMaterialMaximum) { ImGui.TableNextColumn(); - // todo: persist - var temp = ""; ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); + ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - // todo: flesh out this validation - var validName = temp != ""; + var validName = _modelNewMaterial != ""; ImGui.TableNextColumn(); - var todoAdd = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true)) + { + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + ret = true; + } } - // for (var index = 0; index < MdlMaterialMaximum; index++) - // { - // var temp = ""; - // ImGui.InputText($"Material {index}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags); - // } - - // var temp = tab.GetMeshMaterial(meshIndex); - // if ( - // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - // && temp.Length > 0 - // && temp != tab.GetMeshMaterial(meshIndex) - // ) { - // tab.SetMeshMaterial(meshIndex, temp); - // ret = true; - // } - return false; + return ret; } private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) @@ -160,7 +173,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", tab.GetMeshMaterial(meshIndex))) + using (var materialCombo = ImRaii.Combo("##material", "TODO material")) { // todo } From b22470ac79bf3f89bf3a4d671427d3aada448588 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 23 Dec 2023 00:12:59 +1100 Subject: [PATCH 1346/2451] Finish up mesh material combos --- .../ModEditWindow.Models.MdlTab.cs | 13 ++++----- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 5fac4f5e..f488c987 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -24,11 +23,13 @@ public partial class ModEditWindow // Meshes using the removed material are redirected to material 0, and those after the index are corrected. for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { - var mesh = Mdl.Meshes[meshIndex]; - if (mesh.MaterialIndex == materialIndex) - mesh.MaterialIndex = 0; - else if (mesh.MaterialIndex > materialIndex) - mesh.MaterialIndex -= 1; + var newIndex = Mdl.Meshes[meshIndex].MaterialIndex; + if (newIndex == materialIndex) + newIndex = 0; + else if (newIndex > materialIndex) + newIndex -= 1; + + Mdl.Meshes[meshIndex].MaterialIndex = newIndex; } Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index fe3ca644..92953ae4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -158,27 +158,28 @@ public partial class ModEditWindow var ret = false; - // Mesh material. - // var temp = tab.GetMeshMaterial(meshIndex); - // if ( - // ImGui.InputText("Material", ref temp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - // && temp.Length > 0 - // && temp != tab.GetMeshMaterial(meshIndex) - // ) { - // tab.SetMeshMaterial(meshIndex, temp); - // ret = true; - // } + // Mesh material ImGui.TableNextColumn(); ImGui.Text("Material"); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", "TODO material")) + using (var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex])) { - // todo + if (materialCombo) + { + foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) + { + if (ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) + { + file.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + ret = true; + } + } + } } - // Submeshes. + // Submeshes for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) { using var submeshId = ImRaii.PushId(submeshOffset); From 4aa19e49d59ac3a31f9ba05d7ba37243f8fb2d0f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Dec 2023 14:22:03 +0100 Subject: [PATCH 1347/2451] Add filtering mods by changed item categories. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 92 ++++++++++++++----- .../CollectionTab/IndividualAssignmentUi.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 25 +++-- 5 files changed, 89 insertions(+), 34 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3787e82d..58a3e794 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3787e82d1b84d2542b6e4238060d75383a4b12a1 +Subproject commit 58a3e7947c207452f5fa0d328c47c5ed6bdd9a0f diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index f7f82a59..516df251 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -242,7 +242,7 @@ public static class EquipmentSwap if (!slot.IsEquipmentPiece()) throw new ItemSwap.InvalidItemTypeException(); - modelId = i.ModelId; + modelId = i.PrimaryId; variant = i.Variant; } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 0a1d58f9..638afef0 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -43,8 +43,71 @@ public class ChangedItemDrawer : IDisposable Emote = 0x01_00_00, } - public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; - public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; + private static readonly ChangedItemIcon[] Order = + [ + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Emote, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + ChangedItemIcon.Unknown, + ]; + + private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray(); + + public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIcon slot) + { + // Handle numeric cases before TryParse because numbers + // are not logical otherwise. + if (int.TryParse(input, out var idx)) + { + // We assume users will use 1-based index, but if they enter 0, just use the first. + if (idx == 0) + { + slot = Order[0]; + return true; + } + + // Use 1-based index. + --idx; + if (idx >= 0 && idx < Order.Length) + { + slot = Order[idx]; + return true; + } + } + + slot = 0; + return false; + } + + public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot) + { + if (TryParseIndex(lowerInput, out slot)) + return true; + + slot = 0; + foreach (var (item, flag) in LowerNames.Zip(Order)) + if (item.Contains(lowerInput, StringComparison.Ordinal)) + slot |= flag; + + return slot != 0; + } + + public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; + public static readonly int NumCategories = Order.Length; + public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; private readonly Configuration _config; private readonly ExcelSheet _items; @@ -163,26 +226,7 @@ public class ChangedItemDrawer : IDisposable using var _ = ImRaii.PushId("ChangedItemIconFilter"); var size = TypeFilterIconSize; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - var order = new[] - { - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Emote, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - ChangedItemIcon.Unknown, - }; + bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) { @@ -217,13 +261,13 @@ public class ChangedItemDrawer : IDisposable return ret; } - foreach (var iconType in order) + foreach (var iconType in Order) { ret |= DrawIcon(iconType, ref typeFilter); ImGui.SameLine(); } - ImGui.SetCursorPos(new(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index d3e4ab5e..a0e35cff 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -126,7 +126,7 @@ public class IndividualAssignmentUi : IDisposable /// Create combos when ready. private void SetupCombos() { - _worldCombo = new WorldCombo(_actors.Data.Worlds, Penumbra.Log, WorldId.AnyWorld); + _worldCombo = new WorldCombo(_actors.Data.Worlds, Penumbra.Log); _mountCombo = new NpcCombo("##mountCombo", _actors.Data.Mounts, Penumbra.Log); _companionCombo = new NpcCombo("##companionCombo", _actors.Data.Companions, Penumbra.Log); _ornamentCombo = new NpcCombo("##ornamentCombo", _actors.Data.Ornaments, Penumbra.Log); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8f12afbb..c42b1018 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -12,6 +12,8 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -190,7 +192,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), + 's' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), + 'S' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), _ => (new LowerString(filterValue), 0), }, _ => (new LowerString(filterValue), 0), @@ -549,10 +555,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector !mod.Author.Contains(_modFilter), 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), + 5 => mod.ChangedItems.All(p => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & _slotFilter) == 0), 2 + EmptyOffset => !mod.Author.IsEmpty, 3 + EmptyOffset => mod.LowerChangedItemsString.Length > 0, 4 + EmptyOffset => mod.AllTagsLower.Length > 0, + 5 + EmptyOffset => mod.ChangedItems.Count == 0, _ => false, // Should never happen }; } From dc583cb8e24338e9370ee1caab1875edffd65a17 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Dec 2023 14:24:41 +0100 Subject: [PATCH 1348/2451] Update gamedata. --- Penumbra.GameData | 2 +- .../Interop/ResourceTree/ResolveContext.PathResolution.cs | 5 ++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 58a3e794..ed37f834 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 58a3e7947c207452f5fa0d328c47c5ed6bdd9a0f +Subproject commit ed37f83424c11a5a601e74f4660cd52ebd68a7b3 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index f2059253..0ab1e0e3 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; @@ -239,8 +238,8 @@ internal partial record ResolveContext return (characterRaceCode, "base", 1); case 1: var faceId = human->FaceId; - var tribe = human->Customize[(int)CustomizeIndex.Tribe]; - var modelType = human->Customize[(int)CustomizeIndex.ModelType]; + var tribe = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.Tribe]; + var modelType = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.ModelType]; if (faceId < 201) { faceId -= tribe switch diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index bd994242..24112a9f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -6,9 +6,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.String.Classes; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; +using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; namespace Penumbra.Interop.ResourceTree; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 599832f4..66b93b04 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -88,7 +88,7 @@ public class DebugTab : Window, ITab private readonly TextureManager _textureManager; private readonly SkinFixer _skinFixer; private readonly RedrawService _redraws; - private readonly DictEmotes _emotes; + private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; private readonly IObjectTable _objects; private readonly IClientState _clientState; @@ -99,7 +99,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmotes emotes, Diagnostics diagnostics, IpcTester ipcTester) + TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; From a001fcf24ff4333dbdd1bb496caa8f4b4d28ec7a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Dec 2023 15:18:43 +0100 Subject: [PATCH 1349/2451] Some cleanup. --- .../ModEditWindow.Models.MdlTab.cs | 64 +++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 246 +++++++++--------- 2 files changed, 166 insertions(+), 144 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f488c987..4986963f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -10,24 +10,33 @@ public partial class ModEditWindow { public readonly MdlFile Mdl; - private List[] _attributes; + private readonly List[] _attributes; public MdlTab(byte[] bytes) { - Mdl = new MdlFile(bytes); - _attributes = PopulateAttributes(); + Mdl = new MdlFile(bytes); + _attributes = CreateAttributes(Mdl); } + /// + public bool Valid + => Mdl.Valid; + + /// + public byte[] Write() + => Mdl.Write(); + + /// Remove the material given by the index. + /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) { - // Meshes using the removed material are redirected to material 0, and those after the index are corrected. for (var meshIndex = 0; meshIndex < Mdl.Meshes.Length; meshIndex++) { var newIndex = Mdl.Meshes[meshIndex].MaterialIndex; if (newIndex == materialIndex) newIndex = 0; else if (newIndex > materialIndex) - newIndex -= 1; + --newIndex; Mdl.Meshes[meshIndex].MaterialIndex = newIndex; } @@ -35,36 +44,41 @@ public partial class ModEditWindow Mdl.Materials = Mdl.Materials.RemoveItems(materialIndex); } - private List[] PopulateAttributes() - { - return Mdl.SubMeshes.Select(submesh => - Enumerable.Range(0,32) - .Where(index => ((submesh.AttributeIndexMask >> index) & 1) == 1) - .Select(index => Mdl.Attributes[index]) - .ToList() + /// Create a list of attributes per sub mesh. + private static List[] CreateAttributes(MdlFile mdl) + => mdl.SubMeshes.Select(s => Enumerable.Range(0, 32) + .Where(idx => ((s.AttributeIndexMask >> idx) & 1) == 1) + .Select(idx => mdl.Attributes[idx]) + .ToList() ).ToArray(); - } - public IReadOnlyCollection GetSubmeshAttributes(int submeshIndex) => _attributes[submeshIndex]; + /// Obtain the attributes associated with a sub mesh by its index. + public IReadOnlyList GetSubMeshAttributes(int subMeshIndex) + => _attributes[subMeshIndex]; - public void UpdateSubmeshAttribute(int submeshIndex, string? old, string? new_) + /// Remove or add attributes from a sub mesh by its index. + /// The index of the sub mesh to update. + /// If non-null, remove this attribute. + /// If non-null, add this attribute. + public void UpdateSubMeshAttribute(int subMeshIndex, string? old, string? @new) { - var attributes = _attributes[submeshIndex]; + var attributes = _attributes[subMeshIndex]; if (old != null) attributes.Remove(old); - if (new_ != null) - attributes.Add(new_); + if (@new != null) + attributes.Add(@new); PersistAttributes(); } + /// Apply changes to attributes to the file in memory. private void PersistAttributes() { var allAttributes = new List(); - foreach (var (attributes, submeshIndex) in _attributes.WithIndex()) + foreach (var (attributes, subMeshIndex) in _attributes.WithIndex()) { var mask = 0u; @@ -74,20 +88,16 @@ public partial class ModEditWindow if (attributeIndex == -1) { allAttributes.Add(attribute); - attributeIndex = allAttributes.Count() - 1; + attributeIndex = allAttributes.Count - 1; } mask |= 1u << attributeIndex; } - Mdl.SubMeshes[submeshIndex].AttributeIndexMask = mask; + Mdl.SubMeshes[subMeshIndex].AttributeIndexMask = mask; } - Mdl.Attributes = allAttributes.ToArray(); + Mdl.Attributes = [.. allAttributes]; } - - public bool Valid => Mdl.Valid; - - public byte[] Write() => Mdl.Write(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 92953ae4..25bb012a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,19 +15,19 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; - private static string _modelNewMaterial = string.Empty; - private static List _submeshAttributeTagWidgets = new(); + private string _modelNewMaterial = string.Empty; + private readonly List _subMeshAttributeTagWidgets = []; - private static bool DrawModelPanel(MdlTab tab, bool disabled) + private bool DrawModelPanel(MdlTab tab, bool disabled) { var file = tab.Mdl; - var submeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_submeshAttributeTagWidgets.Count != submeshTotal) + var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_subMeshAttributeTagWidgets.Count != subMeshTotal) { - _submeshAttributeTagWidgets.Clear(); - _submeshAttributeTagWidgets.AddRange( - Enumerable.Range(0, submeshTotal).Select(_ => new TagButtons()) + _subMeshAttributeTagWidgets.Clear(); + _subMeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) ); } @@ -44,93 +44,92 @@ public partial class ModEditWindow return !disabled && ret; } - private static bool DrawModelMaterialDetails(MdlTab tab, bool disabled) + private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Materials")) return false; - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table(string.Empty, disabled ? 2 : 3, ImGuiTableFlags.SizingFixedFit); if (!table) return false; - var ret = false; + var ret = false; var materials = tab.Mdl.Materials; - ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); - ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - - var inputFlags = ImGuiInputTextFlags.None; - if (disabled) - inputFlags |= ImGuiInputTextFlags.ReadOnly; + ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); + ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); + if (!disabled) + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) - { - using var id = ImRaii.PushId(materialIndex); + ret |= DrawMaterialRow(tab, disabled, materials, materialIndex, inputFlags); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text($"Material #{materialIndex + 1}"); + if (materials.Length >= MdlMaterialMaximum || disabled) + return ret; - var temp = materials[materialIndex]; - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - if ( - ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) - && temp.Length > 0 - && temp != materials[materialIndex] - ) { - materials[materialIndex] = temp; - ret = true; - } + ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); + var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; + ImGui.TableNextColumn(); + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) + return ret; - // Need to have at least one material. - if (materials.Length <= 1) - continue; - - if (ImGuiUtil.DrawDisabledButton( - FontAwesomeIcon.Trash.ToIconString(), - UiHelpers.IconButtonSize, - "Delete this material.\nAny meshes targeting this material will be updated to use material #1.\nHold Control while clicking to delete.", - disabled || !ImGui.GetIO().KeyCtrl, - true - )) { - tab.RemoveMaterial(materialIndex); - ret = true; - } - } - - if (materials.Length < MdlMaterialMaximum) - { - ImGui.TableNextColumn(); - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint($"##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - - var validName = _modelNewMaterial != ""; - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, "description", disabled || !validName, true)) - { - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; - ret = true; - } - } - - return ret; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + return true; } - private static bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + private bool DrawMaterialRow(MdlTab tab, bool disabled, string[] materials, int materialIndex, ImGuiInputTextFlags inputFlags) { - using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var id = ImRaii.PushId(materialIndex); + var ret = false; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Material #{materialIndex + 1}"); + + var temp = materials[materialIndex]; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText($"##material{materialIndex}", ref temp, Utf8GamePath.MaxGamePathLength, inputFlags) + && temp.Length > 0 + && temp != materials[materialIndex] + ) + { + materials[materialIndex] = temp; + ret = true; + } + + if (disabled) + return ret; + + ImGui.TableNextColumn(); + + // Need to have at least one material. + if (materials.Length <= 1) + return ret; + + var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; + var modifierActive = _config.DeleteModModifier.IsActive(); + if (!modifierActive) + tt += $"\nHold {_config.DeleteModModifier} to delete."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) + return ret; + + tab.RemoveMaterial(materialIndex); + return true; + } + + private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) + { + using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); if (!lodNode) return false; var lod = tab.Mdl.Lods[lodIndex]; - var ret = false; for (var meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) @@ -139,76 +138,89 @@ public partial class ModEditWindow return ret; } - private static bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) + private bool DrawModelMeshDetails(MdlTab tab, int meshIndex, bool disabled) { - using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex}", ImGuiTreeNodeFlags.DefaultOpen); + using var meshNode = ImRaii.TreeNode($"Mesh #{meshIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); if (!meshNode) return false; - using var id = ImRaii.PushId(meshIndex); + using var id = ImRaii.PushId(meshIndex); using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); if (!table) return false; - ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); ImGui.TableSetupColumn("field", ImGuiTableColumnFlags.WidthStretch, 1); var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; - var ret = false; // Mesh material ImGui.TableNextColumn(); - ImGui.Text("Material"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Material"); ImGui.TableNextColumn(); + var ret = DrawMaterialCombo(tab, meshIndex, disabled); + + // Sub meshes + for (var subMeshOffset = 0; subMeshOffset < mesh.SubMeshCount; subMeshOffset++) + ret |= DrawSubMeshAttributes(tab, meshIndex, disabled, subMeshOffset); + + return ret; + } + + private bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) + { + var mesh = tab.Mdl.Meshes[meshIndex]; + using var _ = ImRaii.Disabled(disabled); ImGui.SetNextItemWidth(-1); - using (var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex])) + using var materialCombo = ImRaii.Combo("##material", tab.Mdl.Materials[mesh.MaterialIndex]); + + if (!materialCombo) + return false; + + var ret = false; + foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) { - if (materialCombo) - { - foreach (var (material, materialIndex) in tab.Mdl.Materials.WithIndex()) - { - if (ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) - { - file.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; - ret = true; - } - } - } - } + if (!ImGui.Selectable(material, mesh.MaterialIndex == materialIndex)) + continue; - // Submeshes - for (var submeshOffset = 0; submeshOffset < mesh.SubMeshCount; submeshOffset++) - { - using var submeshId = ImRaii.PushId(submeshOffset); - - var submeshIndex = mesh.SubMeshIndex + submeshOffset; - - ImGui.TableNextColumn(); - ImGui.Text($"Attributes #{submeshOffset}"); - - ImGui.TableNextColumn(); - var widget = _submeshAttributeTagWidgets[submeshIndex]; - var attributes = tab.GetSubmeshAttributes(submeshIndex); - - var tagIndex = widget.Draw("", "", attributes, out var editedAttribute, !disabled); - if (tagIndex >= 0) - { - tab.UpdateSubmeshAttribute( - submeshIndex, - tagIndex < attributes.Count() ? attributes.ElementAt(tagIndex) : null, - editedAttribute != "" ? editedAttribute : null - ); - - ret = true; - } + tab.Mdl.Meshes[meshIndex].MaterialIndex = (ushort)materialIndex; + ret = true; } return ret; } + private bool DrawSubMeshAttributes(MdlTab tab, int meshIndex, bool disabled, int subMeshOffset) + { + using var _ = ImRaii.PushId(subMeshOffset); + + var mesh = tab.Mdl.Meshes[meshIndex]; + var subMeshIndex = mesh.SubMeshIndex + subMeshOffset; + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Attributes #{subMeshOffset + 1}"); + + ImGui.TableNextColumn(); + var widget = _subMeshAttributeTagWidgets[subMeshIndex]; + var attributes = tab.GetSubMeshAttributes(subMeshIndex); + + var tagIndex = widget.Draw(string.Empty, string.Empty, attributes, + out var editedAttribute, !disabled); + if (tagIndex < 0) + return false; + + var oldName = tagIndex < attributes.Count ? attributes[tagIndex] : null; + var newName = editedAttribute.Length > 0 ? editedAttribute : null; + tab.UpdateSubMeshAttribute(subMeshIndex, oldName, newName); + + return true; + } + private static bool DrawOtherModelDetails(MdlFile file, bool _) { if (!ImGui.CollapsingHeader("Further Content")) From 28752e2630869929944657287b2ad5aa1a6bbbb3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 24 Dec 2023 14:35:59 +0100 Subject: [PATCH 1350/2451] Fix issues with EQDP files for invalid characters. --- Penumbra/Collections/Cache/EqdpCache.cs | 16 +++++++++++++--- Penumbra/Collections/Cache/MetaCache.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 11 ++++++++--- Penumbra/Collections/ModCollection.cs | 4 ++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 3937fa72..ddc161cd 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -31,12 +31,22 @@ public readonly struct EqdpCache : IDisposable manager.SetFile(_eqdpFiles[i], index); } - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) + public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) { var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - Debug.Assert(idx >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + if (idx < 0) + { + Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + return null; + } + var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); - Debug.Assert(i >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + if (i < 0) + { + Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); + return null; + } + return manager.TemporarilySetFile(_eqdpFiles[i], idx); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index d5acf249..0fc665ed 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -170,7 +170,7 @@ public class MetaCache : IDisposable, IEnumerable _eqpCache.TemporarilySetFiles(_manager); - public MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) + public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); public MetaList.MetaReverter TemporarilySetGmpFile() diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index a695c463..5d1d10e2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -89,9 +89,14 @@ public partial class ModCollection } // Used for short periods of changed files. - public MetaList.MetaReverter TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) - => _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory) - ?? utility.TemporarilyResetResource(CharacterUtilityData.EqdpIdx(genderRace, accessory)); + public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) + { + if (_cache != null) + return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory); + + var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); + return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; + } public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetEqpFile() diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index a9f565c6..b63be6cd 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -7,7 +7,7 @@ using Penumbra.Services; namespace Penumbra.Collections; /// -/// A ModCollection is a named set of ModSettings to all of the users' installed mods. +/// A ModCollection is a named set of ModSettings to all the users' installed mods. /// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. /// Invariants: /// - Index is the collections index in the ModCollection.Manager @@ -113,7 +113,7 @@ public partial class ModCollection { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), - DirectlyInheritsFrom.ToList(), UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. From f8331bc4d818454e48f225257668f20e1c411b75 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 24 Dec 2023 14:36:21 +0100 Subject: [PATCH 1351/2451] Fix the mod panels header not resetting data when a selected mod updates. --- Penumbra/Communication/ModDataChanged.cs | 5 +++- Penumbra/Mods/Manager/ModFileSystem.cs | 6 ++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 3 +-- Penumbra/UI/ModsTab/ModPanel.cs | 5 ++-- Penumbra/UI/ModsTab/ModPanelHeader.cs | 23 +++++++++++++++++-- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index 9ec60aa3..2f50f005 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -21,8 +21,11 @@ public sealed class ModDataChanged : EventWrapper ModCacheManager = 0, - /// + /// ModFileSystem = 0, + + /// + ModPanelHeader = 0, } public ModDataChanged() diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 1851399d..c8a0a5db 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -21,7 +21,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable Reload(); Changed += OnChange; _communicator.ModDiscoveryFinished.Subscribe(Reload, ModDiscoveryFinished.Priority.ModFileSystem); - _communicator.ModDataChanged.Subscribe(OnDataChange, ModDataChanged.Priority.ModFileSystem); + _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystem); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModFileSystem); } @@ -29,7 +29,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { _communicator.ModPathChanged.Unsubscribe(OnModPathChange); _communicator.ModDiscoveryFinished.Unsubscribe(Reload); - _communicator.ModDataChanged.Unsubscribe(OnDataChange); + _communicator.ModDataChanged.Unsubscribe(OnModDataChange); } public struct ImportDate : ISortMode @@ -75,7 +75,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable } // Update sort order when defaulted mod names change. - private void OnDataChange(ModDataChangeType type, Mod mod, string? oldName) + private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) { if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf)) return; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 25bb012a..e4646d07 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -155,7 +155,6 @@ public partial class ModEditWindow var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; - // Mesh material ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -171,7 +170,7 @@ public partial class ModEditWindow return ret; } - private bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) + private static bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) { var mesh = tab.Mdl.Meshes[meshIndex]; using var _ = ImRaii.Disabled(disabled); diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 15961ff3..f9a3262f 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; @@ -13,13 +14,13 @@ public class ModPanel : IDisposable private readonly ModPanelTabBar _tabs; public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, - MultiModPanel multiModPanel) + MultiModPanel multiModPanel, CommunicatorService communicator) { _selector = selector; _editWindow = editWindow; _tabs = tabs; _multiModPanel = multiModPanel; - _header = new ModPanelHeader(pi); + _header = new ModPanelHeader(pi, communicator); _selector.SelectionChanged += OnSelectionChange; } diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 2c71426f..4b127059 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -3,7 +3,10 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Communication; using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; @@ -13,8 +16,14 @@ public class ModPanelHeader : IDisposable /// We use a big, nice game font for the title. private readonly GameFontHandle _nameFont; - public ModPanelHeader(DalamudPluginInterface pi) - => _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + private readonly CommunicatorService _communicator; + + public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) + { + _communicator = communicator; + _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader); + } /// /// Draw the header for the current mod, @@ -76,6 +85,7 @@ public class ModPanelHeader : IDisposable public void Dispose() { _nameFont.Dispose(); + _communicator.ModDataChanged.Unsubscribe(OnModDataChange); } // Header data. @@ -218,4 +228,13 @@ public class ModPanelHeader : IDisposable ImGui.TextUnformatted(_modWebsite); } } + + /// Just update the data when any relevant field changes. + private void OnModDataChange(ModDataChangeType changeType, Mod mod, string? _2) + { + const ModDataChangeType relevantChanges = + ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version; + if ((changeType & relevantChanges) != 0) + UpdateModData(mod); + } } From df430831010f85b7970b5ec790107ef4ee726f81 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 27 Dec 2023 01:21:26 +1100 Subject: [PATCH 1352/2451] export per example --- Penumbra/Import/Models/ModelManager.cs | 45 +++++++++++++++++++ Penumbra/Penumbra.csproj | 2 + Penumbra/Services/ServiceManager.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- Penumbra/packages.lock.json | 23 ++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Import/Models/ModelManager.cs diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs new file mode 100644 index 00000000..33ad9249 --- /dev/null +++ b/Penumbra/Import/Models/ModelManager.cs @@ -0,0 +1,45 @@ +using Penumbra.GameData.Files; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; + +namespace Penumbra.Import.Models; + +public sealed class ModelManager +{ + public ModelManager() + { + // + } + + // TODO: Consider moving import/export onto an async queue, check ../textures/texturemanager + + public void ExportToGltf(/* MdlFile mdl, */string path) + { + var mesh = new MeshBuilder("mesh"); + + var material1 = new MaterialBuilder() + .WithDoubleSide(true) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 0, 1)); + var primitive1 = mesh.UsePrimitive(material1); + primitive1.AddTriangle(new VertexPosition(-10, 0, 0), new VertexPosition(10, 0, 0), new VertexPosition(0, 10, 0)); + primitive1.AddTriangle(new VertexPosition(10, 0, 0), new VertexPosition(-10, 0, 0), new VertexPosition(0, -10, 0)); + + var material2 = new MaterialBuilder() + .WithDoubleSide(true) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 1, 1)); + var primitive2 = mesh.UsePrimitive(material2); + primitive2.AddQuadrangle(new VertexPosition(-5, 0, 3), new VertexPosition(0, -5, 3), new VertexPosition(5, 0, 3), new VertexPosition(0, 5, 3)); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + + var model = scene.ToGltf2(); + model.SaveGLTF(path); + + // TODO: Draw the rest of the owl. + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ec433113..122e17b5 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -72,6 +72,8 @@ + + diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 73be8834..5a107060 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -8,6 +8,7 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Import.Models; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; @@ -185,7 +186,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index e4646d07..80831dab 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; +using Penumbra.Import.Models; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -13,6 +14,8 @@ public partial class ModEditWindow { private const int MdlMaterialMaximum = 4; + private readonly ModelManager _models; + private readonly FileEditor _modelTab; private string _modelNewMaterial = string.Empty; @@ -31,6 +34,11 @@ public partial class ModEditWindow ); } + if (ImGui.Button("bingo bango")) + { + _models.ExportToGltf("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); + } + var ret = false; ret |= DrawModelMaterialDetails(tab, disabled); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 365c4a4a..1a3d9182 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -12,6 +12,7 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.Import.Models; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; @@ -563,7 +564,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, + CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, GameEventManager gameEvents, ChangedItemDrawer changedItemDrawer) : base(WindowBaseLabel) { @@ -579,6 +580,7 @@ public partial class ModEditWindow : Window, IDisposable _communicator = communicator; _dragDropManager = dragDropManager; _textures = textures; + _models = models; _fileDialog = fileDialog; _gameEvents = gameEvents; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index eed5d7c8..cef49e9c 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -26,6 +26,21 @@ "resolved": "0.33.0", "contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw==" }, + "SharpGLTF.Core": { + "type": "Direct", + "requested": "[1.0.0-alpha0030, )", + "resolved": "1.0.0-alpha0030", + "contentHash": "HVL6PcrM0H/uEk96nRZfhtPeYvSFGHnni3g1aIckot2IWVp0jLMH5KWgaWfsatEz4Yds3XcdSLUWmJZivDBUPA==" + }, + "SharpGLTF.Toolkit": { + "type": "Direct", + "requested": "[1.0.0-alpha0030, )", + "resolved": "1.0.0-alpha0030", + "contentHash": "nsoJWAFhXgEky9bVCY0zLeZVDx+S88u7VjvuebvMb6dJiNyFOGF6FrrMHiJe+x5pcVBxxlc3VoXliBF7r/EqYA==", + "dependencies": { + "SharpGLTF.Runtime": "1.0.0-alpha0030" + } + }, "SixLabors.ImageSharp": { "type": "Direct", "requested": "[2.1.2, )", @@ -46,6 +61,14 @@ "resolved": "5.0.0", "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, + "SharpGLTF.Runtime": { + "type": "Transitive", + "resolved": "1.0.0-alpha0030", + "contentHash": "Ysn+fyj9EVXj6mfG0BmzSTBGNi/QvcnTrMd54dBMOlI/TsMRvnOY3JjTn0MpeH2CgHXX4qogzlDt4m+rb3n4Og==", + "dependencies": { + "SharpGLTF.Core": "1.0.0-alpha0030" + } + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "5.0.0", From ed283afe2caa835cdbe6994c31852ff25510481d Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 27 Dec 2023 01:44:24 +1100 Subject: [PATCH 1353/2451] async is a great idea lets do more of that --- Penumbra/Import/Models/ModelManager.cs | 98 ++++++++++++++----- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 11 ++- 2 files changed, 81 insertions(+), 28 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 33ad9249..fbccf4b7 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Tasks; using Penumbra.GameData.Files; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; @@ -6,40 +7,89 @@ using SharpGLTF.Scenes; namespace Penumbra.Import.Models; -public sealed class ModelManager +public sealed class ModelManager : SingleTaskQueue, IDisposable { + private readonly ConcurrentDictionary _tasks = new(); + private bool _disposed = false; + public ModelManager() { // } - // TODO: Consider moving import/export onto an async queue, check ../textures/texturemanager - - public void ExportToGltf(/* MdlFile mdl, */string path) + public void Dispose() { - var mesh = new MeshBuilder("mesh"); + _disposed = true; + foreach (var (_, cancel) in _tasks.Values.ToArray()) + cancel.Cancel(); + _tasks.Clear(); + } - var material1 = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 0, 1)); - var primitive1 = mesh.UsePrimitive(material1); - primitive1.AddTriangle(new VertexPosition(-10, 0, 0), new VertexPosition(10, 0, 0), new VertexPosition(0, 10, 0)); - primitive1.AddTriangle(new VertexPosition(10, 0, 0), new VertexPosition(-10, 0, 0), new VertexPosition(0, -10, 0)); - - var material2 = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 1, 1)); - var primitive2 = mesh.UsePrimitive(material2); - primitive2.AddQuadrangle(new VertexPosition(-5, 0, 3), new VertexPosition(0, -5, 3), new VertexPosition(5, 0, 3), new VertexPosition(0, 5, 3)); + private Task Enqueue(IAction action) + { + if (_disposed) + return Task.FromException(new ObjectDisposedException(nameof(ModelManager))); - var scene = new SceneBuilder(); - scene.AddRigidMesh(mesh, Matrix4x4.Identity); + Task task; + lock (_tasks) + { + task = _tasks.GetOrAdd(action, action => + { + var token = new CancellationTokenSource(); + var task = Enqueue(action, token.Token); + task.ContinueWith(_ => _tasks.TryRemove(action, out var unused), CancellationToken.None); + return (task, token); + }).Item1; + } - var model = scene.ToGltf2(); - model.SaveGLTF(path); + return task; + } - // TODO: Draw the rest of the owl. + public Task ExportToGltf(/* MdlFile mdl, */string path) + => Enqueue(new ExportToGltfAction(path)); + + private class ExportToGltfAction : IAction + { + private readonly string _path; + + public ExportToGltfAction(string path) + { + _path = path; + } + + public void Execute(CancellationToken token) + { + var mesh = new MeshBuilder("mesh"); + + var material1 = new MaterialBuilder() + .WithDoubleSide(true) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 0, 1)); + var primitive1 = mesh.UsePrimitive(material1); + primitive1.AddTriangle(new VertexPosition(-10, 0, 0), new VertexPosition(10, 0, 0), new VertexPosition(0, 10, 0)); + primitive1.AddTriangle(new VertexPosition(10, 0, 0), new VertexPosition(-10, 0, 0), new VertexPosition(0, -10, 0)); + + var material2 = new MaterialBuilder() + .WithDoubleSide(true) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 1, 1)); + var primitive2 = mesh.UsePrimitive(material2); + primitive2.AddQuadrangle(new VertexPosition(-5, 0, 3), new VertexPosition(0, -5, 3), new VertexPosition(5, 0, 3), new VertexPosition(0, 5, 3)); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + + var model = scene.ToGltf2(); + model.SaveGLTF(_path); + } + + public bool Equals(IAction? other) + { + if (other is not ExportToGltfAction rhs) + return false; + + // TODO: compare configuration + return true; + } } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 80831dab..89497cfd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -14,10 +14,11 @@ public partial class ModEditWindow { private const int MdlMaterialMaximum = 4; - private readonly ModelManager _models; - private readonly FileEditor _modelTab; + private readonly ModelManager _models; + private bool _pendingIo = false; + private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; @@ -34,9 +35,11 @@ public partial class ModEditWindow ); } - if (ImGui.Button("bingo bango")) + if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", _pendingIo)) { - _models.ExportToGltf("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); + _pendingIo = true; + var task = _models.ExportToGltf("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); + task.ContinueWith(_ => _pendingIo = false); } var ret = false; From b7472f722ee991166fb5d874419d3e25cbfb97fa Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 27 Dec 2023 16:17:39 +1100 Subject: [PATCH 1354/2451] poc submesh position export --- Penumbra/Import/Models/ModelManager.cs | 71 ++++++++++++++----- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index fbccf4b7..bded3b0c 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,3 +1,4 @@ +using Lumina.Extensions; using OtterGui.Tasks; using Penumbra.GameData.Files; using SharpGLTF.Geometry; @@ -45,39 +46,75 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return task; } - public Task ExportToGltf(/* MdlFile mdl, */string path) - => Enqueue(new ExportToGltfAction(path)); + public Task ExportToGltf(MdlFile mdl, string path) + => Enqueue(new ExportToGltfAction(mdl, path)); private class ExportToGltfAction : IAction { + private readonly MdlFile _mdl; private readonly string _path; - public ExportToGltfAction(string path) + public ExportToGltfAction(MdlFile mdl, string path) { + _mdl = mdl; _path = path; } public void Execute(CancellationToken token) { - var mesh = new MeshBuilder("mesh"); + var meshBuilder = new MeshBuilder("mesh"); - var material1 = new MaterialBuilder() + var material = new MaterialBuilder() .WithDoubleSide(true) .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 0, 1)); - var primitive1 = mesh.UsePrimitive(material1); - primitive1.AddTriangle(new VertexPosition(-10, 0, 0), new VertexPosition(10, 0, 0), new VertexPosition(0, 10, 0)); - primitive1.AddTriangle(new VertexPosition(10, 0, 0), new VertexPosition(-10, 0, 0), new VertexPosition(0, -10, 0)); - - var material2 = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 0, 1, 1)); - var primitive2 = mesh.UsePrimitive(material2); - primitive2.AddQuadrangle(new VertexPosition(-5, 0, 3), new VertexPosition(0, -5, 3), new VertexPosition(5, 0, 3), new VertexPosition(0, 5, 3)); + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); + + // lol, lmao even + var meshIndex = 2; + var lod = 0; + + var mesh = _mdl.Meshes[meshIndex]; + var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now + + var positionVertexElement = _mdl.VertexDeclarations[meshIndex].VertexElements + .Where(decl => decl.Usage == 0 /* POSITION */) + .First(); + + // reading in the entire indices list + var dataReader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); + dataReader.Seek(_mdl.IndexOffset[lod]); + var indices = dataReader.ReadStructuresAsArray((int)_mdl.IndexBufferSize[lod] / sizeof(ushort)); + + // read in verts for this mesh + var baseOffset = _mdl.VertexOffset[lod] + mesh.VertexBufferOffset[positionVertexElement.Stream] + positionVertexElement.Offset; + var vertices = new List(); + for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) + { + dataReader.Seek(baseOffset + vertexIndex * mesh.VertexBufferStride[positionVertexElement.Stream]); + // todo handle type + vertices.Add(new VertexPosition( + dataReader.ReadSingle(), + dataReader.ReadSingle(), + dataReader.ReadSingle() + )); + } + + // build a primitive for the submesh + var primitiveBuilder = meshBuilder.UsePrimitive(material); + // they're all tri list + for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) + { + var index = indexOffset + submesh.IndexOffset; + + primitiveBuilder.AddTriangle( + vertices[indices[index + 0]], + vertices[indices[index + 1]], + vertices[indices[index + 2]] + ); + } var scene = new SceneBuilder(); - scene.AddRigidMesh(mesh, Matrix4x4.Identity); + scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); var model = scene.ToGltf2(); model.SaveGLTF(_path); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 89497cfd..b64b4f40 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -38,7 +38,7 @@ public partial class ModEditWindow if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", _pendingIo)) { _pendingIo = true; - var task = _models.ExportToGltf("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); + var task = _models.ExportToGltf(file, "C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); task.ContinueWith(_ => _pendingIo = false); } From 81425b458e043ae98c25c0cddbf1e53de3a94c35 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 27 Dec 2023 17:25:14 +1100 Subject: [PATCH 1355/2451] Use vertex element enums --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ffdb966f..0dc4c892 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f +Subproject commit 0dc4c892308aea30314d118362b3ebab7706f4e5 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bded3b0c..5e931b36 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -77,7 +77,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now var positionVertexElement = _mdl.VertexDeclarations[meshIndex].VertexElements - .Where(decl => decl.Usage == 0 /* POSITION */) + .Where(decl => (MdlFile.VertexUsage)decl.Usage == MdlFile.VertexUsage.Position) .First(); // reading in the entire indices list From ca46e7482f5dd2eeba323bd082a8d5d67b05d826 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 28 Dec 2023 00:44:19 +1100 Subject: [PATCH 1356/2451] Flesh out geometry handling --- Penumbra/Import/Models/ModelManager.cs | 149 ++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 18 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 5e931b36..af285cbb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui.Tasks; using Penumbra.GameData.Files; @@ -62,17 +64,26 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken token) { - var meshBuilder = new MeshBuilder("mesh"); + // lol, lmao even + var meshIndex = 2; + var lod = 0; + + var elements = _mdl.VertexDeclarations[meshIndex].VertexElements; + + var usages = elements + .Select(element => (MdlFile.VertexUsage)element.Usage) + .ToImmutableHashSet(); + var geometryType = GetGeometryType(usages); + + // TODO: probablly can do this a bit later but w/e + var meshBuilderType = typeof(MeshBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); + var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, "mesh2")!; var material = new MaterialBuilder() .WithDoubleSide(true) .WithMetallicRoughnessShader() .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); - // lol, lmao even - var meshIndex = 2; - var lod = 0; - var mesh = _mdl.Meshes[meshIndex]; var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now @@ -86,18 +97,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var indices = dataReader.ReadStructuresAsArray((int)_mdl.IndexBufferSize[lod] / sizeof(ushort)); // read in verts for this mesh - var baseOffset = _mdl.VertexOffset[lod] + mesh.VertexBufferOffset[positionVertexElement.Stream] + positionVertexElement.Offset; - var vertices = new List(); - for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) - { - dataReader.Seek(baseOffset + vertexIndex * mesh.VertexBufferStride[positionVertexElement.Stream]); - // todo handle type - vertices.Add(new VertexPosition( - dataReader.ReadSingle(), - dataReader.ReadSingle(), - dataReader.ReadSingle() - )); - } + var vertices = BuildVertices(lod, mesh, _mdl.VertexDeclarations[meshIndex].VertexElements, geometryType); // build a primitive for the submesh var primitiveBuilder = meshBuilder.UsePrimitive(material); @@ -120,12 +120,125 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable model.SaveGLTF(_path); } + // todo all of this is mesh specific so probably should be a class per mesh? with the lod, too? + private IReadOnlyList BuildVertices(int lod, MdlStructs.MeshStruct mesh, IEnumerable elements, Type geometryType) + { + var vertexBuilderType = typeof(VertexBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); + + // todo: demagic the 3 + // todo note this assumes that the buffer streams are tightly packed. that's a safe assumption - right? lumina assumes as much + var streams = new BinaryReader[3]; + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + { + streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); + streams[streamIndex].Seek(_mdl.VertexOffset[lod] + mesh.VertexBufferOffset[streamIndex]); + } + + var sortedElements = elements + .OrderBy(element => element.Offset) + .ToList(); + + var vertices = new List(); + + // note this is being reused + var attributes = new Dictionary(); + for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) + { + attributes.Clear(); + + foreach (var element in sortedElements) + attributes[(MdlFile.VertexUsage)element.Usage] = ReadVertexAttribute(streams[element.Stream], element); + + var vertexGeometry = BuildVertexGeometry(geometryType, attributes); + + var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!; + vertices.Add(vertexBuilder); + } + + return vertices; + } + + // todo i fucking hate this `object` type god i hate c# gimme sum types pls + private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element) + { + return (MdlFile.VertexType)element.Type switch + { + MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.UInt => reader.ReadBytes(4), + MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), + MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + + _ => throw new ArgumentOutOfRangeException() + }; + } + + private Type GetGeometryType(IReadOnlySet usages) + { + if (!usages.Contains(MdlFile.VertexUsage.Position)) + throw new Exception("Mesh does not contain position vertex elements."); + + if (!usages.Contains(MdlFile.VertexUsage.Normal)) + return typeof(VertexPosition); + + if (!usages.Contains(MdlFile.VertexUsage.Tangent1)) + return typeof(VertexPositionNormal); + + return typeof(VertexPositionNormalTangent); + } + + private IVertexGeometry BuildVertexGeometry(Type geometryType, IReadOnlyDictionary attributes) + { + if (geometryType == typeof(VertexPosition)) + return new VertexPosition( + ToVector3(attributes[MdlFile.VertexUsage.Position]) + ); + + if (geometryType == typeof(VertexPositionNormal)) + return new VertexPositionNormal( + ToVector3(attributes[MdlFile.VertexUsage.Position]), + ToVector3(attributes[MdlFile.VertexUsage.Normal]) + ); + + if (geometryType == typeof(VertexPositionNormalTangent)) + return new VertexPositionNormalTangent( + ToVector3(attributes[MdlFile.VertexUsage.Position]), + ToVector3(attributes[MdlFile.VertexUsage.Normal]), + ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) + ); + + throw new Exception($"Unknown geometry type {geometryType}."); + } + + private Vector3 ToVector3(object data) + { + return data switch + { + Vector2 v2 => new Vector3(v2.X, v2.Y, 0), + Vector3 v3 => v3, + Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z), + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + }; + } + + private Vector4 ToVector4(object data) + { + return data switch + { + Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), + Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1), + Vector4 v4 => v4, + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + }; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) return false; - // TODO: compare configuration + // TODO: compare configuration and such return true; } } From bc24110c9f6860ab5da2082174911a9eb992c4ae Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 28 Dec 2023 02:15:14 +1100 Subject: [PATCH 1357/2451] Move mesh logic to new file, export all meshes --- Penumbra/Import/Models/MeshConverter.cs | 191 ++++++++++++++++++++++++ Penumbra/Import/Models/ModelManager.cs | 183 ++--------------------- 2 files changed, 205 insertions(+), 169 deletions(-) create mode 100644 Penumbra/Import/Models/MeshConverter.cs diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs new file mode 100644 index 00000000..2fcd2816 --- /dev/null +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -0,0 +1,191 @@ +using System.Collections.Immutable; +using Lumina.Data.Parsing; +using Lumina.Extensions; +using Penumbra.GameData.Files; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; + +namespace Penumbra.Import.Modules; + +public sealed class MeshConverter +{ + public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex) + { + var self = new MeshConverter(mdl, lod, meshIndex); + return self.BuildMesh(); + } + + private const byte MaximumMeshBufferStreams = 3; + + private readonly MdlFile _mdl; + private readonly byte _lod; + private readonly ushort _meshIndex; + private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex]; + + private readonly Type _geometryType; + + private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex) + { + _mdl = mdl; + _lod = lod; + _meshIndex = meshIndex; + + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements + .Select(element => (MdlFile.VertexUsage)element.Usage) + .ToImmutableHashSet(); + + _geometryType = GetGeometryType(usages); + } + + private IMeshBuilder BuildMesh() + { + var indices = BuildIndices(); + var vertices = BuildVertices(); + + var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( + typeof(MaterialBuilder), + _geometryType, + typeof(VertexEmpty), + typeof(VertexEmpty) + ); + var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; + + // TODO: share materials &c + var materialBuilder = new MaterialBuilder() + .WithDoubleSide(true) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); + + var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder); + + // All XIV meshes use triangle lists. + // TODO: split by submeshes + for (var indexOffset = 0; indexOffset < Mesh.IndexCount; indexOffset += 3) + primitiveBuilder.AddTriangle( + vertices[indices[indexOffset + 0]], + vertices[indices[indexOffset + 1]], + vertices[indices[indexOffset + 2]] + ); + + return meshBuilder; + } + + private IReadOnlyList BuildIndices() + { + var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); + reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort)); + return reader.ReadStructuresAsArray((int)Mesh.IndexCount); + } + + private IReadOnlyList BuildVertices() + { + var vertexBuilderType = typeof(VertexBuilder<,,>) + .MakeGenericType(_geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); + + // NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop. + var streams = new BinaryReader[MaximumMeshBufferStreams]; + for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) + { + streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + Mesh.VertexBufferOffset[streamIndex]); + } + + var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements + .OrderBy(element => element.Offset) + .Select(element => ((MdlFile.VertexUsage)element.Usage, element)) + .ToList(); + + var vertices = new List(); + + var attributes = new Dictionary(); + for (var vertexIndex = 0; vertexIndex < Mesh.VertexCount; vertexIndex++) + { + attributes.Clear(); + + foreach (var (usage, element) in sortedElements) + attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); + + var vertexGeometry = BuildVertexGeometry(attributes); + + var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!; + vertices.Add(vertexBuilder); + } + + return vertices; + } + + private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element) + { + return (MdlFile.VertexType)element.Type switch + { + MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + MdlFile.VertexType.UInt => reader.ReadBytes(4), + MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), + MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + + _ => throw new ArgumentOutOfRangeException() + }; + } + + private Type GetGeometryType(IReadOnlySet usages) + { + if (!usages.Contains(MdlFile.VertexUsage.Position)) + throw new Exception("Mesh does not contain position vertex elements."); + + if (!usages.Contains(MdlFile.VertexUsage.Normal)) + return typeof(VertexPosition); + + if (!usages.Contains(MdlFile.VertexUsage.Tangent1)) + return typeof(VertexPositionNormal); + + return typeof(VertexPositionNormalTangent); + } + + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) + { + if (_geometryType == typeof(VertexPosition)) + return new VertexPosition( + ToVector3(attributes[MdlFile.VertexUsage.Position]) + ); + + if (_geometryType == typeof(VertexPositionNormal)) + return new VertexPositionNormal( + ToVector3(attributes[MdlFile.VertexUsage.Position]), + ToVector3(attributes[MdlFile.VertexUsage.Normal]) + ); + + if (_geometryType == typeof(VertexPositionNormalTangent)) + return new VertexPositionNormalTangent( + ToVector3(attributes[MdlFile.VertexUsage.Position]), + ToVector3(attributes[MdlFile.VertexUsage.Normal]), + FixTangentVector(ToVector4(attributes[MdlFile.VertexUsage.Tangent1])) + ); + + throw new Exception($"Unknown geometry type {_geometryType}."); + } + + // Some tangent W values that should be -1 are stored as 0. + private Vector4 FixTangentVector(Vector4 tangent) + => tangent with { W = tangent.W == 1 ? 1 : -1 }; + + private Vector3 ToVector3(object data) + => data switch + { + Vector2 v2 => new Vector3(v2.X, v2.Y, 0), + Vector3 v3 => v3, + Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z), + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + }; + + private Vector4 ToVector4(object data) + => data switch + { + Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), + Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1), + Vector4 v4 => v4, + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + }; +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index af285cbb..429aad54 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,11 +1,6 @@ -using System.Collections.Immutable; -using Lumina.Data.Parsing; -using Lumina.Extensions; using OtterGui.Tasks; using Penumbra.GameData.Files; -using SharpGLTF.Geometry; -using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.Materials; +using Penumbra.Import.Modules; using SharpGLTF.Scenes; namespace Penumbra.Import.Models; @@ -64,175 +59,25 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken token) { - // lol, lmao even - var meshIndex = 2; - var lod = 0; - - var elements = _mdl.VertexDeclarations[meshIndex].VertexElements; - - var usages = elements - .Select(element => (MdlFile.VertexUsage)element.Usage) - .ToImmutableHashSet(); - var geometryType = GetGeometryType(usages); - - // TODO: probablly can do this a bit later but w/e - var meshBuilderType = typeof(MeshBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); - var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, "mesh2")!; - - var material = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); - - var mesh = _mdl.Meshes[meshIndex]; - var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now - - var positionVertexElement = _mdl.VertexDeclarations[meshIndex].VertexElements - .Where(decl => (MdlFile.VertexUsage)decl.Usage == MdlFile.VertexUsage.Position) - .First(); - - // reading in the entire indices list - var dataReader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - dataReader.Seek(_mdl.IndexOffset[lod]); - var indices = dataReader.ReadStructuresAsArray((int)_mdl.IndexBufferSize[lod] / sizeof(ushort)); - - // read in verts for this mesh - var vertices = BuildVertices(lod, mesh, _mdl.VertexDeclarations[meshIndex].VertexElements, geometryType); - - // build a primitive for the submesh - var primitiveBuilder = meshBuilder.UsePrimitive(material); - // they're all tri list - for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) - { - var index = indexOffset + submesh.IndexOffset; - - primitiveBuilder.AddTriangle( - vertices[indices[index + 0]], - vertices[indices[index + 1]], - vertices[indices[index + 2]] - ); - } - var scene = new SceneBuilder(); - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + + // TODO: group by LoD in output tree + for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) + { + var lod = _mdl.Lods[lodIndex]; + + // TODO: consider other types? + for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) + { + var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset)); + scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + } + } var model = scene.ToGltf2(); model.SaveGLTF(_path); } - // todo all of this is mesh specific so probably should be a class per mesh? with the lod, too? - private IReadOnlyList BuildVertices(int lod, MdlStructs.MeshStruct mesh, IEnumerable elements, Type geometryType) - { - var vertexBuilderType = typeof(VertexBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); - - // todo: demagic the 3 - // todo note this assumes that the buffer streams are tightly packed. that's a safe assumption - right? lumina assumes as much - var streams = new BinaryReader[3]; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - { - streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[lod] + mesh.VertexBufferOffset[streamIndex]); - } - - var sortedElements = elements - .OrderBy(element => element.Offset) - .ToList(); - - var vertices = new List(); - - // note this is being reused - var attributes = new Dictionary(); - for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) - { - attributes.Clear(); - - foreach (var element in sortedElements) - attributes[(MdlFile.VertexUsage)element.Usage] = ReadVertexAttribute(streams[element.Stream], element); - - var vertexGeometry = BuildVertexGeometry(geometryType, attributes); - - var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!; - vertices.Add(vertexBuilder); - } - - return vertices; - } - - // todo i fucking hate this `object` type god i hate c# gimme sum types pls - private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element) - { - return (MdlFile.VertexType)element.Type switch - { - MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - MdlFile.VertexType.UInt => reader.ReadBytes(4), - MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), - MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), - - _ => throw new ArgumentOutOfRangeException() - }; - } - - private Type GetGeometryType(IReadOnlySet usages) - { - if (!usages.Contains(MdlFile.VertexUsage.Position)) - throw new Exception("Mesh does not contain position vertex elements."); - - if (!usages.Contains(MdlFile.VertexUsage.Normal)) - return typeof(VertexPosition); - - if (!usages.Contains(MdlFile.VertexUsage.Tangent1)) - return typeof(VertexPositionNormal); - - return typeof(VertexPositionNormalTangent); - } - - private IVertexGeometry BuildVertexGeometry(Type geometryType, IReadOnlyDictionary attributes) - { - if (geometryType == typeof(VertexPosition)) - return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position]) - ); - - if (geometryType == typeof(VertexPositionNormal)) - return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]) - ); - - if (geometryType == typeof(VertexPositionNormalTangent)) - return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]), - ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) - ); - - throw new Exception($"Unknown geometry type {geometryType}."); - } - - private Vector3 ToVector3(object data) - { - return data switch - { - Vector2 v2 => new Vector3(v2.X, v2.Y, 0), - Vector3 v3 => v3, - Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z), - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") - }; - } - - private Vector4 ToVector4(object data) - { - return data switch - { - Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), - Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1), - Vector4 v4 => v4, - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") - }; - } - public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) From 635d606112979cf7661938a88afd711e92357cae Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 28 Dec 2023 15:51:20 +1100 Subject: [PATCH 1358/2451] Initial skeleton tests --- Penumbra/Import/Models/HavokConverter.cs | 141 +++++++++++++ Penumbra/Import/Models/ModelManager.cs | 187 +++++++++++++++++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 4 + 3 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Import/Models/HavokConverter.cs diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs new file mode 100644 index 00000000..515c6f97 --- /dev/null +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -0,0 +1,141 @@ +using FFXIVClientStructs.Havok; + +namespace Penumbra.Import.Models; + +// TODO: where should this live? interop i guess, in penum? or game data? +public unsafe class HavokConverter +{ + /// Creates a temporary file and returns its path. + /// Path to a temporary file. + private string CreateTempFile() + { + var s = File.Create(Path.GetTempFileName()); + s.Close(); + return s.Name; + } + + /// Converts a .hkx file to a .xml file. + /// A byte array representing the .hkx file. + /// A string representing the .xml file. + /// Thrown if parsing the .hkx file fails. + /// Thrown if writing the .xml file fails. + public string HkxToXml(byte[] hkx) + { + var tempHkx = CreateTempFile(); + File.WriteAllBytes(tempHkx, hkx); + + var resource = Read(tempHkx); + File.Delete(tempHkx); + + if (resource == null) throw new Exception("HavokReadException"); + + var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.TextFormat + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + + var file = Write(resource, options); + file.Close(); + + var bytes = File.ReadAllText(file.Name); + File.Delete(file.Name); + + return bytes; + } + + /// Converts a .xml file to a .hkx file. + /// A string representing the .xml file. + /// A byte array representing the .hkx file. + /// Thrown if parsing the .xml file fails. + /// Thrown if writing the .hkx file fails. + public byte[] XmlToHkx(string xml) + { + var tempXml = CreateTempFile(); + File.WriteAllText(tempXml, xml); + + var resource = Read(tempXml); + File.Delete(tempXml); + + if (resource == null) throw new Exception("HavokReadException"); + + var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + + var file = Write(resource, options); + file.Close(); + + var bytes = File.ReadAllBytes(file.Name); + File.Delete(file.Name); + + return bytes; + } + + /// + /// Parses a serialized file into an hkResource*. + /// The type is guessed automatically by Havok. + /// This pointer might be null - you should check for that. + /// + /// Path to a file on the filesystem. + /// A (potentially null) pointer to an hkResource. + private hkResource* Read(string filePath) + { + var path = Marshal.StringToHGlobalAnsi(filePath); + + var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); + + var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; + loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; + loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); + loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); + + // TODO: probably can loadfrombuffer this + var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); + return resource; + } + + /// Serializes an hkResource* to a temporary file. + /// A pointer to the hkResource, opened through Read(). + /// Flags representing how to serialize the file. + /// An opened FileStream of a temporary file. You are expected to read the file and delete it. + /// Thrown if accessing the root level container fails. + /// Thrown if an unknown failure in writing occurs. + private FileStream Write( + hkResource* resource, + hkSerializeUtil.SaveOptionBits optionBits + ) + { + var tempFile = CreateTempFile(); + var path = Marshal.StringToHGlobalAnsi(tempFile); + var oStream = new hkOstream(); + oStream.Ctor((byte*)path); + + var result = stackalloc hkResult[1]; + + var saveOptions = new hkSerializeUtil.SaveOptions() + { + Flags = new() { Storage = (int)optionBits } + }; + + + var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); + var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); + var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); + + try + { + var name = "hkRootLevelContainer"; + + var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); + if (resourcePtr == null) throw new Exception("HavokWriteException"); + + var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); + if (hkRootLevelContainerClass == null) throw new Exception("HavokWriteException"); + + hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); + } + finally { oStream.Dtor(); } + + if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("HavokFailureException"); + + return new FileStream(tempFile, FileMode.Open); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 429aad54..c4c46353 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,18 +1,26 @@ +using System.Xml; +using Dalamud.Plugin.Services; +using Lumina.Data; +using Lumina.Extensions; +using OtterGui; using OtterGui.Tasks; using Penumbra.GameData.Files; using Penumbra.Import.Modules; using SharpGLTF.Scenes; +using SharpGLTF.Transforms; namespace Penumbra.Import.Models; public sealed class ModelManager : SingleTaskQueue, IDisposable { + private readonly IDataManager _gameData; + private readonly ConcurrentDictionary _tasks = new(); private bool _disposed = false; - public ModelManager() + public ModelManager(IDataManager gameData) { - // + _gameData = gameData; } public void Dispose() @@ -46,6 +54,181 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public Task ExportToGltf(MdlFile mdl, string path) => Enqueue(new ExportToGltfAction(mdl, path)); + public void SkeletonTest() + { + var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb"; + + var something = _gameData.GetFile(sklbPath); + + var fuck = new HavokConverter(); + var killme = fuck.HkxToXml(something.Skeleton); + + var doc = new XmlDocument(); + doc.LoadXml(killme); + + var skels = doc.SelectNodes("/hktagfile/object[@type='hkaSkeleton']") + .Cast() + .Select(element => new Skel(element)) + .ToArray(); + + // todo: look into how this is selecting the skel - only first? + var animSkel = doc.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']") + .SelectNodes("array[@name='skeletons']") + .Cast() + .First(); + var mainSkelId = animSkel.ChildNodes[0].InnerText; + + var mainSkel = skels.First(skel => skel.Id == mainSkelId); + + // this is atrocious + NodeBuilder? root = null; + var boneMap = new Dictionary(); + for (var boneIndex = 0; boneIndex < mainSkel.BoneNames.Length; boneIndex++) + { + var name = mainSkel.BoneNames[boneIndex]; + if (boneMap.ContainsKey(name)) continue; + + var node = new NodeBuilder(name); + + var rp = mainSkel.ReferencePose[boneIndex]; + var transform = new AffineTransform( + new Vector3(rp[8], rp[9], rp[10]), + new Quaternion(rp[4], rp[5], rp[6], rp[7]), + new Vector3([rp[0], rp[1], rp[2]]) + ); + node.SetLocalTransform(transform, false); + + boneMap[name] = node; + + var parentId = mainSkel.ParentIndices[boneIndex]; + if (parentId == -1) + { + root = node; + continue; + } + + var parent = boneMap[mainSkel.BoneNames[parentId]]; + parent.AddNode(node); + } + + var scene = new SceneBuilder(); + scene.AddNode(root); + var model = scene.ToGltf2(); + model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf"); + + Penumbra.Log.Information($"zoingo {string.Join(',', mainSkel.ParentIndices)}"); + } + + // this is garbage that should be in gamedata + + private sealed class Garbage : FileResource + { + public byte[] Skeleton; + + public override void LoadFile() + { + var magic = Reader.ReadUInt32(); + if (magic != 0x736B6C62) + throw new InvalidDataException("Invalid sklb magic"); + + // todo do this all properly jfc + var version = Reader.ReadUInt32(); + + var oldHeader = version switch { + 0x31313030 or 0x31313130 or 0x31323030 => true, + 0x31333030 => false, + _ => throw new InvalidDataException($"Unknown version {version}") + }; + + // Skeleton offset directly follows the layer offset. + uint skeletonOffset; + if (oldHeader) + { + Reader.ReadInt16(); + skeletonOffset = Reader.ReadUInt16(); + } + else + { + Reader.ReadUInt32(); + skeletonOffset = Reader.ReadUInt32(); + } + + Reader.Seek(skeletonOffset); + Skeleton = Reader.ReadBytes((int)(Reader.BaseStream.Length - skeletonOffset)); + } + } + + private class Skel + { + public readonly string Id; + + public readonly float[][] ReferencePose; + public readonly int[] ParentIndices; + public readonly string[] BoneNames; + + // TODO: this shouldn't have any reference to the skel xml - i should just make it a bare class that can be repr'd in gamedata or whatever + public Skel(XmlElement el) + { + Id = el.GetAttribute("id"); + + ReferencePose = ReadReferencePose(el); + ParentIndices = ReadParentIndices(el); + BoneNames = ReadBoneNames(el); + } + + private float[][] ReadReferencePose(XmlElement el) + { + return ReadArray( + (XmlElement)el.SelectSingleNode("array[@name='referencePose']"), + ReadVec12 + ); + } + + private float[] ReadVec12(XmlElement el) + { + return el.ChildNodes + .Cast() + .Where(node => node.NodeType != XmlNodeType.Comment) + .Select(node => { + var t = node.InnerText.Trim()[1..]; + // todo: surely there's a less shit way to do this i mean seriously + return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(t, NumberStyles.HexNumber))); + }) + .ToArray(); + } + + private int[] ReadParentIndices(XmlElement el) + { + // todo: would be neat to genericise array between bare and children + return el.SelectSingleNode("array[@name='parentIndices']") + .InnerText + .Split(new char[] {' ', '\n'}, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + } + + private string[] ReadBoneNames(XmlElement el) + { + return ReadArray( + (XmlElement)el.SelectSingleNode("array[@name='bones']"), + el => el.SelectSingleNode("string[@name='name']").InnerText + ); + } + + private T[] ReadArray(XmlElement el, Func convert) + { + var size = int.Parse(el.GetAttribute("size")); + + var array = new T[size]; + foreach (var (node, index) in el.ChildNodes.Cast().WithIndex()) + { + array[index] = convert(node); + } + + return array; + } + } + private class ExportToGltfAction : IAction { private readonly MdlFile _mdl; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b64b4f40..f0cc34a6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -41,6 +41,10 @@ public partial class ModEditWindow var task = _models.ExportToGltf(file, "C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); task.ContinueWith(_ => _pendingIo = false); } + if (ImGui.Button("zoingo boingo")) + { + _models.SkeletonTest(); + } var ret = false; From d646c5e4b5ffc256c768258ff4c72765877311af Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 28 Dec 2023 16:49:44 +1100 Subject: [PATCH 1359/2451] Resolve skeleton path --- Penumbra/Import/Models/ModelManager.cs | 43 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index c4c46353..9074b67a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,11 +1,12 @@ using System.Xml; using Dalamud.Plugin.Services; -using Lumina.Data; using Lumina.Extensions; using OtterGui; using OtterGui.Tasks; +using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Modules; +using Penumbra.String.Classes; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -14,13 +15,15 @@ namespace Penumbra.Import.Models; public sealed class ModelManager : SingleTaskQueue, IDisposable { private readonly IDataManager _gameData; + private readonly ActiveCollectionData _activeCollectionData; private readonly ConcurrentDictionary _tasks = new(); private bool _disposed = false; - public ModelManager(IDataManager gameData) + public ModelManager(IDataManager gameData, ActiveCollectionData activeCollectionData) { _gameData = gameData; + _activeCollectionData = activeCollectionData; } public void Dispose() @@ -58,7 +61,18 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable { var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb"; - var something = _gameData.GetFile(sklbPath); + var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true); + var testResolve = _activeCollectionData.Current.ResolvePath(utf8Path); + Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}"); + + // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... + var bytes = testResolve switch + { + null => _gameData.GetFile(sklbPath).Data, + FullPath path => File.ReadAllBytes(path.ToPath()) + }; + + var something = new Garbage(bytes); var fuck = new HavokConverter(); var killme = fuck.HkxToXml(something.Skeleton); @@ -121,18 +135,21 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable // this is garbage that should be in gamedata - private sealed class Garbage : FileResource + private sealed class Garbage { public byte[] Skeleton; - public override void LoadFile() + public Garbage(byte[] data) { - var magic = Reader.ReadUInt32(); + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var magic = reader.ReadUInt32(); if (magic != 0x736B6C62) throw new InvalidDataException("Invalid sklb magic"); // todo do this all properly jfc - var version = Reader.ReadUInt32(); + var version = reader.ReadUInt32(); var oldHeader = version switch { 0x31313030 or 0x31313130 or 0x31323030 => true, @@ -144,17 +161,17 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable uint skeletonOffset; if (oldHeader) { - Reader.ReadInt16(); - skeletonOffset = Reader.ReadUInt16(); + reader.ReadInt16(); + skeletonOffset = reader.ReadUInt16(); } else { - Reader.ReadUInt32(); - skeletonOffset = Reader.ReadUInt32(); + reader.ReadUInt32(); + skeletonOffset = reader.ReadUInt32(); } - Reader.Seek(skeletonOffset); - Skeleton = Reader.ReadBytes((int)(Reader.BaseStream.Length - skeletonOffset)); + reader.Seek(skeletonOffset); + Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset)); } } From d7cac3e09a9f531261cf60e8bf5020149bc4073e Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 29 Dec 2023 02:31:02 +1100 Subject: [PATCH 1360/2451] Clean up and refactor skeleton logic --- Penumbra/Import/Models/ModelManager.cs | 171 +++----------------- Penumbra/Import/Models/Skeleton.cs | 25 +++ Penumbra/Import/Models/SkeletonConverter.cs | 132 +++++++++++++++ Penumbra/Import/Models/SklbFile.cs | 44 +++++ 4 files changed, 223 insertions(+), 149 deletions(-) create mode 100644 Penumbra/Import/Models/Skeleton.cs create mode 100644 Penumbra/Import/Models/SkeletonConverter.cs create mode 100644 Penumbra/Import/Models/SklbFile.cs diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9074b67a..6a9ef334 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -61,6 +61,8 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable { var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb"; + // NOTE: to resolve game path from _mod_, will need to wire the mod class via the modeditwindow to the model editor, through to here. + // NOTE: to get the game path for a model we'll probably need to use a reverse resolve - there's no guarantee for a modded model that they're named per game path, nor that there's only one name. var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true); var testResolve = _activeCollectionData.Current.ResolvePath(utf8Path); Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}"); @@ -72,56 +74,40 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable FullPath path => File.ReadAllBytes(path.ToPath()) }; - var something = new Garbage(bytes); + var sklb = new SklbFile(bytes); - var fuck = new HavokConverter(); - var killme = fuck.HkxToXml(something.Skeleton); + // TODO: Consider making these static methods. + var havokConverter = new HavokConverter(); + var xml = havokConverter.HkxToXml(sklb.Skeleton); - var doc = new XmlDocument(); - doc.LoadXml(killme); + var skeletonConverter = new SkeletonConverter(); + var skeleton = skeletonConverter.FromXml(xml); - var skels = doc.SelectNodes("/hktagfile/object[@type='hkaSkeleton']") - .Cast() - .Select(element => new Skel(element)) - .ToArray(); - - // todo: look into how this is selecting the skel - only first? - var animSkel = doc.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']") - .SelectNodes("array[@name='skeletons']") - .Cast() - .First(); - var mainSkelId = animSkel.ChildNodes[0].InnerText; - - var mainSkel = skels.First(skel => skel.Id == mainSkelId); - - // this is atrocious + // this is (less) atrocious NodeBuilder? root = null; var boneMap = new Dictionary(); - for (var boneIndex = 0; boneIndex < mainSkel.BoneNames.Length; boneIndex++) + for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) { - var name = mainSkel.BoneNames[boneIndex]; - if (boneMap.ContainsKey(name)) continue; + var bone = skeleton.Bones[boneIndex]; - var node = new NodeBuilder(name); + if (boneMap.ContainsKey(bone.Name)) continue; - var rp = mainSkel.ReferencePose[boneIndex]; - var transform = new AffineTransform( - new Vector3(rp[8], rp[9], rp[10]), - new Quaternion(rp[4], rp[5], rp[6], rp[7]), - new Vector3([rp[0], rp[1], rp[2]]) - ); - node.SetLocalTransform(transform, false); + var node = new NodeBuilder(bone.Name); + boneMap[bone.Name] = node; - boneMap[name] = node; + node.SetLocalTransform(new AffineTransform( + bone.Transform.Scale, + bone.Transform.Rotation, + bone.Transform.Translation + ), false); - var parentId = mainSkel.ParentIndices[boneIndex]; - if (parentId == -1) + if (bone.ParentIndex == -1) { root = node; continue; } - var parent = boneMap[mainSkel.BoneNames[parentId]]; + var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name]; parent.AddNode(node); } @@ -130,120 +116,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var model = scene.ToGltf2(); model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf"); - Penumbra.Log.Information($"zoingo {string.Join(',', mainSkel.ParentIndices)}"); - } - - // this is garbage that should be in gamedata - - private sealed class Garbage - { - public byte[] Skeleton; - - public Garbage(byte[] data) - { - using var stream = new MemoryStream(data); - using var reader = new BinaryReader(stream); - - var magic = reader.ReadUInt32(); - if (magic != 0x736B6C62) - throw new InvalidDataException("Invalid sklb magic"); - - // todo do this all properly jfc - var version = reader.ReadUInt32(); - - var oldHeader = version switch { - 0x31313030 or 0x31313130 or 0x31323030 => true, - 0x31333030 => false, - _ => throw new InvalidDataException($"Unknown version {version}") - }; - - // Skeleton offset directly follows the layer offset. - uint skeletonOffset; - if (oldHeader) - { - reader.ReadInt16(); - skeletonOffset = reader.ReadUInt16(); - } - else - { - reader.ReadUInt32(); - skeletonOffset = reader.ReadUInt32(); - } - - reader.Seek(skeletonOffset); - Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset)); - } - } - - private class Skel - { - public readonly string Id; - - public readonly float[][] ReferencePose; - public readonly int[] ParentIndices; - public readonly string[] BoneNames; - - // TODO: this shouldn't have any reference to the skel xml - i should just make it a bare class that can be repr'd in gamedata or whatever - public Skel(XmlElement el) - { - Id = el.GetAttribute("id"); - - ReferencePose = ReadReferencePose(el); - ParentIndices = ReadParentIndices(el); - BoneNames = ReadBoneNames(el); - } - - private float[][] ReadReferencePose(XmlElement el) - { - return ReadArray( - (XmlElement)el.SelectSingleNode("array[@name='referencePose']"), - ReadVec12 - ); - } - - private float[] ReadVec12(XmlElement el) - { - return el.ChildNodes - .Cast() - .Where(node => node.NodeType != XmlNodeType.Comment) - .Select(node => { - var t = node.InnerText.Trim()[1..]; - // todo: surely there's a less shit way to do this i mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(t, NumberStyles.HexNumber))); - }) - .ToArray(); - } - - private int[] ReadParentIndices(XmlElement el) - { - // todo: would be neat to genericise array between bare and children - return el.SelectSingleNode("array[@name='parentIndices']") - .InnerText - .Split(new char[] {' ', '\n'}, StringSplitOptions.RemoveEmptyEntries) - .Select(int.Parse) - .ToArray(); - } - - private string[] ReadBoneNames(XmlElement el) - { - return ReadArray( - (XmlElement)el.SelectSingleNode("array[@name='bones']"), - el => el.SelectSingleNode("string[@name='name']").InnerText - ); - } - - private T[] ReadArray(XmlElement el, Func convert) - { - var size = int.Parse(el.GetAttribute("size")); - - var array = new T[size]; - foreach (var (node, index) in el.ChildNodes.Cast().WithIndex()) - { - array[index] = convert(node); - } - - return array; - } + Penumbra.Log.Information($"zoingo!"); } private class ExportToGltfAction : IAction diff --git a/Penumbra/Import/Models/Skeleton.cs b/Penumbra/Import/Models/Skeleton.cs new file mode 100644 index 00000000..fb5c8284 --- /dev/null +++ b/Penumbra/Import/Models/Skeleton.cs @@ -0,0 +1,25 @@ +namespace Penumbra.Import.Models; + +// TODO: this should almost certainly live in gamedata. if not, it should at _least_ be adjacent to the model handling. +public class Skeleton +{ + public Bone[] Bones; + + public Skeleton(Bone[] bones) + { + Bones = bones; + } + + public struct Bone + { + public string Name; + public int ParentIndex; + public Transform Transform; + } + + public struct Transform { + public Vector3 Scale; + public Quaternion Rotation; + public Vector3 Translation; + } +} diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs new file mode 100644 index 00000000..d54b0294 --- /dev/null +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -0,0 +1,132 @@ +using System.Xml; +using OtterGui; + +namespace Penumbra.Import.Models; + +// TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up. +public class SkeletonConverter +{ + public Skeleton FromXml(string xml) + { + var document = new XmlDocument(); + document.LoadXml(xml); + + var mainSkeletonId = GetMainSkeletonId(document); + + var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']"); + if (skeletonNode == null) + throw new InvalidDataException(); + + var referencePose = ReadReferencePose(skeletonNode); + var parentIndices = ReadParentIndices(skeletonNode); + var boneNames = ReadBoneNames(skeletonNode); + + if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length) + throw new InvalidDataException(); + + var bones = referencePose + .Zip(parentIndices, boneNames) + .Select(values => + { + var (transform, parentIndex, name) = values; + return new Skeleton.Bone() + { + Transform = transform, + ParentIndex = parentIndex, + Name = name, + }; + }) + .ToArray(); + + return new Skeleton(bones); + } + + /// Get the main skeleton ID for a given skeleton document. + /// XML skeleton document. + private string GetMainSkeletonId(XmlNode node) + { + var animationSkeletons = node + .SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")? + .ChildNodes; + + if (animationSkeletons?.Count != 1) + throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}"); + + return animationSkeletons[0]!.InnerText; + } + + /// Read the reference pose transforms for a skeleton. + /// XML node for the skeleton. + private Skeleton.Transform[] ReadReferencePose(XmlNode node) + { + return ReadArray( + CheckExists(node.SelectSingleNode("array[@name='referencePose']")), + node => + { + var raw = ReadVec12(node); + return new Skeleton.Transform() + { + Translation = new(raw[0], raw[1], raw[2]), + Rotation = new(raw[4], raw[5], raw[6], raw[7]), + Scale = new(raw[8], raw[9], raw[10]), + }; + } + ); + } + + private float[] ReadVec12(XmlNode node) + { + var array = node.ChildNodes + .Cast() + .Where(node => node.NodeType != XmlNodeType.Comment) + .Select(node => + { + var text = node.InnerText.Trim()[1..]; + // TODO: surely there's a less shit way to do this i mean seriously + return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + }) + .ToArray(); + + if (array.Length != 12) + throw new InvalidDataException(); + + return array; + } + + private int[] ReadParentIndices(XmlNode node) + { + // todo: would be neat to genericise array between bare and children + return CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) + .InnerText + .Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + } + + private string[] ReadBoneNames(XmlNode node) + { + return ReadArray( + CheckExists(node.SelectSingleNode("array[@name='bones']")), + node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText + ); + } + + private T[] ReadArray(XmlNode node, Func convert) + { + var element = (XmlElement)node; + + var size = int.Parse(element.GetAttribute("size")); + + var array = new T[size]; + foreach (var (childNode, index) in element.ChildNodes.Cast().WithIndex()) + array[index] = convert(childNode); + + return array; + } + + private static T CheckExists(T? value) + { + ArgumentNullException.ThrowIfNull(value); + return value; + } +} diff --git a/Penumbra/Import/Models/SklbFile.cs b/Penumbra/Import/Models/SklbFile.cs new file mode 100644 index 00000000..9ae6f7db --- /dev/null +++ b/Penumbra/Import/Models/SklbFile.cs @@ -0,0 +1,44 @@ +using Lumina.Extensions; + +namespace Penumbra.Import.Models; + +// TODO: yeah this goes in gamedata. +public class SklbFile +{ + public byte[] Skeleton; + + public SklbFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var magic = reader.ReadUInt32(); + if (magic != 0x736B6C62) + throw new InvalidDataException("Invalid sklb magic"); + + // todo do this all properly jfc + var version = reader.ReadUInt32(); + + var oldHeader = version switch { + 0x31313030 or 0x31313130 or 0x31323030 => true, + 0x31333030 => false, + _ => throw new InvalidDataException($"Unknown version {version}") + }; + + // Skeleton offset directly follows the layer offset. + uint skeletonOffset; + if (oldHeader) + { + reader.ReadInt16(); + skeletonOffset = reader.ReadUInt16(); + } + else + { + reader.ReadUInt32(); + skeletonOffset = reader.ReadUInt32(); + } + + reader.Seek(skeletonOffset); + Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset)); + } +} From 71fc901798ec9f90feab880c891667efab3f8893 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 29 Dec 2023 19:16:42 +1100 Subject: [PATCH 1361/2451] Resolve mdl game paths --- .../ModEditWindow.Models.MdlTab.cs | 18 +++++++++++++++++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 3 +++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 4986963f..254db841 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,6 +1,8 @@ using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -9,12 +11,14 @@ public partial class ModEditWindow private class MdlTab : IWritable { public readonly MdlFile Mdl; + public readonly List GamePaths; private readonly List[] _attributes; - public MdlTab(byte[] bytes) + public MdlTab(byte[] bytes, string path, Mod? mod) { Mdl = new MdlFile(bytes); + GamePaths = mod == null ? new() : FindGamePaths(path, mod); _attributes = CreateAttributes(Mdl); } @@ -26,6 +30,18 @@ public partial class ModEditWindow public byte[] Write() => Mdl.Write(); + // TODO: this _needs_ to be done asynchronously, kart mods hang for a good second or so + private List FindGamePaths(string path, Mod mod) + { + // todo: might be worth ordering based on prio + selection for disambiguating between multiple matches? not sure. same for the multi group case + return mod.AllSubMods + .SelectMany(submod => submod.Files.Concat(submod.FileSwaps)) + // todo: using ordinal ignore case because the option group paths in mods being lowerecased somewhere, but the mod editor using fs paths, which may be uppercase. i'd say this will blow up on linux, but it's already the case so can't be too much worse than present right + .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) + .Select(kv => kv.Key) + .ToList(); + } + /// Remove the material given by the index. /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index f0cc34a6..d5ca6fa5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -45,6 +45,9 @@ public partial class ModEditWindow { _models.SkeletonTest(); } + ImGui.TextUnformatted("blippity blap"); + foreach (var gamePath in tab.GamePaths) + ImGui.TextUnformatted(gamePath.ToString()); var ret = false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 1a3d9182..181538ec 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -587,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlTab(bytes)); + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(bytes, path, _mod)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); From 18fd36d2d7123d3519b29c12bb580943b0f67e1c Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 29 Dec 2023 23:49:55 +1100 Subject: [PATCH 1362/2451] Bit of cleanup --- Penumbra/Import/Models/ModelManager.cs | 12 +++++------ .../ModEditWindow.Models.MdlTab.cs | 21 +++++++++++++++++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 7 ++----- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 6a9ef334..42134037 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -54,8 +54,8 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return task; } - public Task ExportToGltf(MdlFile mdl, string path) - => Enqueue(new ExportToGltfAction(mdl, path)); + public Task ExportToGltf(MdlFile mdl, string outputPath) + => Enqueue(new ExportToGltfAction(mdl, outputPath)); public void SkeletonTest() { @@ -122,12 +122,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable private class ExportToGltfAction : IAction { private readonly MdlFile _mdl; - private readonly string _path; + private readonly string _outputPath; - public ExportToGltfAction(MdlFile mdl, string path) + public ExportToGltfAction(MdlFile mdl, string outputPath) { _mdl = mdl; - _path = path; + _outputPath = outputPath; } public void Execute(CancellationToken token) @@ -148,7 +148,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } var model = scene.ToGltf2(); - model.SaveGLTF(_path); + model.SaveGLTF(_outputPath); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 254db841..4552078a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -10,13 +10,18 @@ public partial class ModEditWindow { private class MdlTab : IWritable { + private ModEditWindow _edit; + public readonly MdlFile Mdl; public readonly List GamePaths; - private readonly List[] _attributes; - public MdlTab(byte[] bytes, string path, Mod? mod) + public bool PendingIo { get; private set; } = false; + + public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) { + _edit = edit; + Mdl = new MdlFile(bytes); GamePaths = mod == null ? new() : FindGamePaths(path, mod); _attributes = CreateAttributes(Mdl); @@ -31,6 +36,9 @@ public partial class ModEditWindow => Mdl.Write(); // TODO: this _needs_ to be done asynchronously, kart mods hang for a good second or so + /// Find the list of game paths that may correspond to this model. + /// Resolved path to a .mdl. + /// Mod within which the .mdl is resolved. private List FindGamePaths(string path, Mod mod) { // todo: might be worth ordering based on prio + selection for disambiguating between multiple matches? not sure. same for the multi group case @@ -42,6 +50,15 @@ public partial class ModEditWindow .ToList(); } + /// Export model to an interchange format. + /// Disk path to save the resulting file to. + public void Export(string outputPath) + { + PendingIo = true; + _edit._models.ExportToGltf(Mdl, outputPath) + .ContinueWith(_ => PendingIo = false); + } + /// Remove the material given by the index. /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index d5ca6fa5..0b145b89 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -17,7 +17,6 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; private readonly ModelManager _models; - private bool _pendingIo = false; private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; @@ -35,11 +34,9 @@ public partial class ModEditWindow ); } - if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", _pendingIo)) + if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", tab.PendingIo)) { - _pendingIo = true; - var task = _models.ExportToGltf(file, "C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); - task.ContinueWith(_ => _pendingIo = false); + tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); } if (ImGui.Button("zoingo boingo")) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 181538ec..20ad92c3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -587,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(bytes, path, _mod)); + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path, _mod)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); From 695c18439db5dc353def4cd1edd72e761cb0a456 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 30 Dec 2023 02:41:19 +1100 Subject: [PATCH 1363/2451] Hook up rudimentary skeleton resolution for equipment models --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 134 ++++++++---------- Penumbra/Import/Models/SklbFile.cs | 44 ------ .../ModEditWindow.Models.MdlTab.cs | 60 +++++++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 4 - 5 files changed, 121 insertions(+), 123 deletions(-) delete mode 100644 Penumbra/Import/Models/SklbFile.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 0dc4c892..b6a68ab6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0dc4c892308aea30314d118362b3ebab7706f4e5 +Subproject commit b6a68ab60be6a46f8ede63425cd0716dedf693a3 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 42134037..9f56588a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,12 +1,8 @@ -using System.Xml; using Dalamud.Plugin.Services; -using Lumina.Extensions; -using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Modules; -using Penumbra.String.Classes; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -14,14 +10,16 @@ namespace Penumbra.Import.Models; public sealed class ModelManager : SingleTaskQueue, IDisposable { + private readonly IFramework _framework; private readonly IDataManager _gameData; private readonly ActiveCollectionData _activeCollectionData; private readonly ConcurrentDictionary _tasks = new(); private bool _disposed = false; - public ModelManager(IDataManager gameData, ActiveCollectionData activeCollectionData) + public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData) { + _framework = framework; _gameData = gameData; _activeCollectionData = activeCollectionData; } @@ -54,86 +52,33 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return task; } - public Task ExportToGltf(MdlFile mdl, string outputPath) - => Enqueue(new ExportToGltfAction(mdl, outputPath)); - - public void SkeletonTest() - { - var sklbPath = "chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb"; - - // NOTE: to resolve game path from _mod_, will need to wire the mod class via the modeditwindow to the model editor, through to here. - // NOTE: to get the game path for a model we'll probably need to use a reverse resolve - there's no guarantee for a modded model that they're named per game path, nor that there's only one name. - var succeeded = Utf8GamePath.FromString(sklbPath, out var utf8Path, true); - var testResolve = _activeCollectionData.Current.ResolvePath(utf8Path); - Penumbra.Log.Information($"resolved: {(testResolve == null ? "NULL" : testResolve.ToString())}"); - - // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = testResolve switch - { - null => _gameData.GetFile(sklbPath).Data, - FullPath path => File.ReadAllBytes(path.ToPath()) - }; - - var sklb = new SklbFile(bytes); - - // TODO: Consider making these static methods. - var havokConverter = new HavokConverter(); - var xml = havokConverter.HkxToXml(sklb.Skeleton); - - var skeletonConverter = new SkeletonConverter(); - var skeleton = skeletonConverter.FromXml(xml); - - // this is (less) atrocious - NodeBuilder? root = null; - var boneMap = new Dictionary(); - for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) - { - var bone = skeleton.Bones[boneIndex]; - - if (boneMap.ContainsKey(bone.Name)) continue; - - var node = new NodeBuilder(bone.Name); - boneMap[bone.Name] = node; - - node.SetLocalTransform(new AffineTransform( - bone.Transform.Scale, - bone.Transform.Rotation, - bone.Transform.Translation - ), false); - - if (bone.ParentIndex == -1) - { - root = node; - continue; - } - - var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name]; - parent.AddNode(node); - } - - var scene = new SceneBuilder(); - scene.AddNode(root); - var model = scene.ToGltf2(); - model.SaveGLTF(@"C:\Users\ackwell\blender\gltf-tests\zoingo.gltf"); - - Penumbra.Log.Information($"zoingo!"); - } + public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); private class ExportToGltfAction : IAction { + private readonly ModelManager _manager; + private readonly MdlFile _mdl; + private readonly SklbFile? _sklb; private readonly string _outputPath; - public ExportToGltfAction(MdlFile mdl, string outputPath) + public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath) { + _manager = manager; _mdl = mdl; + _sklb = sklb; _outputPath = outputPath; } - public void Execute(CancellationToken token) + public void Execute(CancellationToken cancel) { var scene = new SceneBuilder(); + var skeletonRoot = BuildSkeleton(cancel); + if (skeletonRoot != null) + scene.AddNode(skeletonRoot); + // TODO: group by LoD in output tree for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) { @@ -151,6 +96,53 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable model.SaveGLTF(_outputPath); } + // TODO: this should be moved to a seperate model converter or something + private NodeBuilder? BuildSkeleton(CancellationToken cancel) + { + if (_sklb == null) + return null; + + // TODO: Consider making these static methods. + // TODO: work out how i handle this havok deal. running it outside the framework causes an immediate ctd. + var havokConverter = new HavokConverter(); + var xmlTask = _manager._framework.RunOnFrameworkThread(() => havokConverter.HkxToXml(_sklb.Skeleton)); + xmlTask.Wait(cancel); + var xml = xmlTask.Result; + + var skeletonConverter = new SkeletonConverter(); + var skeleton = skeletonConverter.FromXml(xml); + + // this is (less) atrocious + NodeBuilder? root = null; + var boneMap = new Dictionary(); + for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) + { + var bone = skeleton.Bones[boneIndex]; + + if (boneMap.ContainsKey(bone.Name)) continue; + + var node = new NodeBuilder(bone.Name); + boneMap[bone.Name] = node; + + node.SetLocalTransform(new AffineTransform( + bone.Transform.Scale, + bone.Transform.Rotation, + bone.Transform.Translation + ), false); + + if (bone.ParentIndex == -1) + { + root = node; + continue; + } + + var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name]; + parent.AddNode(node); + } + + return root; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) diff --git a/Penumbra/Import/Models/SklbFile.cs b/Penumbra/Import/Models/SklbFile.cs deleted file mode 100644 index 9ae6f7db..00000000 --- a/Penumbra/Import/Models/SklbFile.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Lumina.Extensions; - -namespace Penumbra.Import.Models; - -// TODO: yeah this goes in gamedata. -public class SklbFile -{ - public byte[] Skeleton; - - public SklbFile(byte[] data) - { - using var stream = new MemoryStream(data); - using var reader = new BinaryReader(stream); - - var magic = reader.ReadUInt32(); - if (magic != 0x736B6C62) - throw new InvalidDataException("Invalid sklb magic"); - - // todo do this all properly jfc - var version = reader.ReadUInt32(); - - var oldHeader = version switch { - 0x31313030 or 0x31313130 or 0x31323030 => true, - 0x31333030 => false, - _ => throw new InvalidDataException($"Unknown version {version}") - }; - - // Skeleton offset directly follows the layer offset. - uint skeletonOffset; - if (oldHeader) - { - reader.ReadInt16(); - skeletonOffset = reader.ReadUInt16(); - } - else - { - reader.ReadUInt32(); - skeletonOffset = reader.ReadUInt32(); - } - - reader.Seek(skeletonOffset); - Skeleton = reader.ReadBytes((int)(reader.BaseStream.Length - skeletonOffset)); - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 4552078a..99c32761 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -8,16 +8,20 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private class MdlTab : IWritable + private partial class MdlTab : IWritable { private ModEditWindow _edit; public readonly MdlFile Mdl; public readonly List GamePaths; private readonly List[] _attributes; - + public bool PendingIo { get; private set; } = false; + // TODO: this can probably be genericised across all of chara + [GeneratedRegex(@"chara/equipment/e(?'Set'\d{4})/model/c(?'Race'\d{4})e\k'Set'_.+\.mdl", RegexOptions.Compiled)] + private static partial Regex CharaEquipmentRegex(); + public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) { _edit = edit; @@ -54,11 +58,61 @@ public partial class ModEditWindow /// Disk path to save the resulting file to. public void Export(string outputPath) { + // NOTES ON EST (i don't think it's worth supporting yet...) + // for collection wide lookup; + // Collections.Cache.EstCache::GetEstEntry + // Collections.Cache.MetaCache::GetEstEntry + // Collections.ModCollection.MetaCache? + // for default lookup, probably; + // EstFile.GetDefault(...) + + // TODO: allow user to pick the gamepath in the ui + // TODO: what if there's no gamepaths? + var mdlPath = GamePaths.First(); + var sklbPath = GetSklbPath(mdlPath.ToString()); + var sklb = sklbPath != null ? ReadSklb(sklbPath) : null; + PendingIo = true; - _edit._models.ExportToGltf(Mdl, outputPath) + _edit._models.ExportToGltf(Mdl, sklb, outputPath) .ContinueWith(_ => PendingIo = false); } + /// Try to find the .sklb path for a .mdl file. + /// .mdl file to look up the skeleton for. + private string? GetSklbPath(string mdlPath) + { + // TODO: This needs to be drastically expanded, it's dodgy af rn + + var match = CharaEquipmentRegex().Match(mdlPath); + if (!match.Success) + return null; + + var race = match.Groups["Race"].Value; + + return $"chara/human/c{race}/skeleton/base/b0001/skl_c{race}b0001.sklb"; + } + + /// Read a .sklb from the active collection or game. + /// Game path to the .sklb to load. + private SklbFile ReadSklb(string sklbPath) + { + // TODO: if cross-collection lookups are turned off, this conversion can be skipped + if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) + throw new Exception("TODO: handle - should it throw, or try to fail gracefully?"); + + var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); + // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... + var bytes = resolvedPath switch + { + null => _edit._dalamud.GameData.GetFile(sklbPath)?.Data, + FullPath path => File.ReadAllBytes(path.ToPath()), + }; + if (bytes == null) + throw new Exception("TODO: handle - this effectively means that the resolved path doesn't exist. graceful?"); + + return new SklbFile(bytes); + } + /// Remove the material given by the index. /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0b145b89..ff2c1ae5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -38,10 +38,6 @@ public partial class ModEditWindow { tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); } - if (ImGui.Button("zoingo boingo")) - { - _models.SkeletonTest(); - } ImGui.TextUnformatted("blippity blap"); foreach (var gamePath in tab.GamePaths) ImGui.TextUnformatted(gamePath.ToString()); From 697b5fac6531d5dfc549f18f5c22540a1bb54033 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 29 Dec 2023 17:20:11 +0000 Subject: [PATCH 1364/2451] [CI] Updating repo.json for testing_0.8.3.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 173f6592..dbe1fe87 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.1", + "TestingAssemblyVersion": "0.8.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 727fa3c18352472927899f2826e70fbb15048d1f Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 30 Dec 2023 17:07:34 +1100 Subject: [PATCH 1365/2451] Initial pass on skinned mesh output --- Penumbra/Import/Models/MeshConverter.cs | 84 ++++++++++++++++++++++--- Penumbra/Import/Models/ModelManager.cs | 33 ++++++---- 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index 2fcd2816..30d24c17 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -10,9 +10,9 @@ namespace Penumbra.Import.Modules; public sealed class MeshConverter { - public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex) + public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { - var self = new MeshConverter(mdl, lod, meshIndex); + var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap); return self.BuildMesh(); } @@ -23,21 +23,49 @@ public sealed class MeshConverter private readonly ushort _meshIndex; private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex]; - private readonly Type _geometryType; + private readonly Dictionary? _boneIndexMap; - private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex) + private readonly Type _geometryType; + private readonly Type _skinningType; + + private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { _mdl = mdl; _lod = lod; _meshIndex = meshIndex; + if (boneNameMap != null) + _boneIndexMap = BuildBoneIndexMap(boneNameMap); + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements .Select(element => (MdlFile.VertexUsage)element.Usage) .ToImmutableHashSet(); _geometryType = GetGeometryType(usages); + _skinningType = GetSkinningType(usages); } + private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) + { + // todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... + var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex]; + + var indexMap = new Dictionary(); + + foreach (var xivBoneIndex in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount)) + { + var boneName = _mdl.Bones[xivBoneIndex]; + if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) + // TODO: handle - i think this is a hard failure, it means that a bone name in the model doesn't exist in the armature. + throw new Exception($"looking for {boneName} in {string.Join(", ", boneNameMap.Keys)}"); + + indexMap.Add(xivBoneIndex, gltfBoneIndex); + } + + return indexMap; + } + + // TODO: consider a struct return type private IMeshBuilder BuildMesh() { var indices = BuildIndices(); @@ -47,7 +75,7 @@ public sealed class MeshConverter typeof(MaterialBuilder), _geometryType, typeof(VertexEmpty), - typeof(VertexEmpty) + _skinningType ); var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; @@ -81,7 +109,7 @@ public sealed class MeshConverter private IReadOnlyList BuildVertices() { var vertexBuilderType = typeof(VertexBuilder<,,>) - .MakeGenericType(_geometryType, typeof(VertexEmpty), typeof(VertexEmpty)); + .MakeGenericType(_geometryType, typeof(VertexEmpty), _skinningType); // NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop. var streams = new BinaryReader[MaximumMeshBufferStreams]; @@ -107,8 +135,9 @@ public sealed class MeshConverter attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); var vertexGeometry = BuildVertexGeometry(attributes); + var vertexSkinning = BuildVertexSkinning(attributes); - var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!; + var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), vertexSkinning)!; vertices.Add(vertexBuilder); } @@ -167,6 +196,40 @@ public sealed class MeshConverter throw new Exception($"Unknown geometry type {_geometryType}."); } + private Type GetSkinningType(IReadOnlySet usages) + { + // TODO: possibly need to check only index - weight might be missing? + if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices)) + return typeof(VertexJoints4); + + return typeof(VertexEmpty); + } + + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) + { + if (_skinningType == typeof(VertexEmpty)) + return new VertexEmpty(); + + if (_skinningType == typeof(VertexJoints4)) + { + // todo: this shouldn't happen... right? better approach? + if (_boneIndexMap == null) + throw new Exception("cannot build skinned vertex without index mapping"); + + var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); + var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); + + // todo: if this throws on the bone index map, the mod is broken, as it contains weights for bones that do not exist. + // i've not seen any of these that even tt can understand + var bindings = Enumerable.Range(0, 4) + .Select(index => (_boneIndexMap[indices[index]], weights[index])) + .ToArray(); + return new VertexJoints4(bindings); + } + + throw new Exception($"Unknown skinning type {_skinningType}"); + } + // Some tangent W values that should be -1 are stored as 0. private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; @@ -188,4 +251,11 @@ public sealed class MeshConverter Vector4 v4 => v4, _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + + private byte[] ToByteArray(object data) + => data switch + { + byte[] value => value, + _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}") + }; } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9f56588a..027ac841 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -75,20 +75,24 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable { var scene = new SceneBuilder(); - var skeletonRoot = BuildSkeleton(cancel); - if (skeletonRoot != null) - scene.AddNode(skeletonRoot); + var skeleton = BuildSkeleton(cancel); + if (skeleton != null) + scene.AddNode(skeleton.Value.Root); // TODO: group by LoD in output tree for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) { var lod = _mdl.Lods[lodIndex]; - // TODO: consider other types? + // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset)); - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); + // TODO: use a value from the mesh converter for this check, rather than assuming that it has joints + if (skeleton == null) + scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + else + scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); } } @@ -97,7 +101,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // TODO: this should be moved to a seperate model converter or something - private NodeBuilder? BuildSkeleton(CancellationToken cancel) + private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary Names)? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) return null; @@ -114,15 +118,17 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable // this is (less) atrocious NodeBuilder? root = null; - var boneMap = new Dictionary(); + var names = new Dictionary(); + var joints = new List(); for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) { var bone = skeleton.Bones[boneIndex]; - if (boneMap.ContainsKey(bone.Name)) continue; + if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); - boneMap[bone.Name] = node; + names[bone.Name] = joints.Count; + joints.Add(node); node.SetLocalTransform(new AffineTransform( bone.Transform.Scale, @@ -136,11 +142,14 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable continue; } - var parent = boneMap[skeleton.Bones[bone.ParentIndex].Name]; + var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; parent.AddNode(node); } - return root; + if (root == null) + return null; + + return (root, joints.ToArray(), names); } public bool Equals(IAction? other) From f7a2c174152c6899618e437adb0566f3924073a9 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 30 Dec 2023 18:31:15 +1100 Subject: [PATCH 1366/2451] Quick submesh implementation --- Penumbra/Import/Models/MeshConverter.cs | 27 +++++++++++++++++-------- Penumbra/Import/Models/ModelManager.cs | 11 +++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index 30d24c17..9f55383e 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -10,7 +10,7 @@ namespace Penumbra.Import.Modules; public sealed class MeshConverter { - public static IMeshBuilder ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + public static IMeshBuilder[] ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap); return self.BuildMesh(); @@ -65,12 +65,23 @@ public sealed class MeshConverter return indexMap; } - // TODO: consider a struct return type - private IMeshBuilder BuildMesh() + private IMeshBuilder[] BuildMesh() { - var indices = BuildIndices(); var vertices = BuildVertices(); + // TODO: handle submeshCount = 0 + + return _mdl.SubMeshes + .Skip(Mesh.SubMeshIndex) + .Take(Mesh.SubMeshCount) + .Select(submesh => BuildSubMesh(submesh, vertices)) + .ToArray(); + } + + private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList vertices) + { + var indices = BuildIndices(submesh); + var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), _geometryType, @@ -89,7 +100,7 @@ public sealed class MeshConverter // All XIV meshes use triangle lists. // TODO: split by submeshes - for (var indexOffset = 0; indexOffset < Mesh.IndexCount; indexOffset += 3) + for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) primitiveBuilder.AddTriangle( vertices[indices[indexOffset + 0]], vertices[indices[indexOffset + 1]], @@ -99,11 +110,11 @@ public sealed class MeshConverter return meshBuilder; } - private IReadOnlyList BuildIndices() + private IReadOnlyList BuildIndices(MdlStructs.SubmeshStruct submesh) { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort)); - return reader.ReadStructuresAsArray((int)Mesh.IndexCount); + reader.Seek(_mdl.IndexOffset[_lod] + submesh.IndexOffset * sizeof(ushort)); + return reader.ReadStructuresAsArray((int)submesh.IndexCount); } private IReadOnlyList BuildVertices() diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 027ac841..2dd64235 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -87,12 +87,13 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); + var meshBuilders = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); // TODO: use a value from the mesh converter for this check, rather than assuming that it has joints - if (skeleton == null) - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); - else - scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); + foreach (var meshBuilder in meshBuilders) + if (skeleton == null) + scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); + else + scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); } } From 81cdcad72e62d4089e5501da066bbf1bbf014702 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Dec 2023 14:37:31 +0100 Subject: [PATCH 1367/2451] Improve automatic service detection. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/BackupService.cs | 15 ++++-- Penumbra/Services/CommunicatorService.cs | 3 +- Penumbra/Services/ConfigMigrationService.cs | 31 +++++------- ...mudServices.cs => DalamudConfigService.cs} | 39 +-------------- Penumbra/Services/FilenameService.cs | 4 +- Penumbra/Services/MessageService.cs | 7 +-- Penumbra/Services/SaveService.cs | 8 ++- Penumbra/Services/ServiceManagerA.cs | 50 +++++++++++-------- Penumbra/Services/StainService.cs | 45 ++++++----------- Penumbra/Services/ValidityChecker.cs | 3 +- Penumbra/Util/PerformanceType.cs | 5 +- 13 files changed, 86 insertions(+), 128 deletions(-) rename Penumbra/Services/{DalamudServices.cs => DalamudConfigService.cs} (72%) diff --git a/OtterGui b/OtterGui index 197d23ee..f6a8ad0f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c +Subproject commit f6a8ad0f8e585408e0aa17c90209358403b52535 diff --git a/Penumbra.GameData b/Penumbra.GameData index ed37f834..192fd1e6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ed37f83424c11a5a601e74f4660cd52ebd68a7b3 +Subproject commit 192fd1e6ad269c3cbdb81aa8c43a8bc20c5ae7f0 diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index e8684f9d..a542dab5 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -1,15 +1,24 @@ using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; namespace Penumbra.Services; -public class BackupService +public class BackupService : IAsyncService { + /// + public Task Awaiter { get; } + + /// + public bool Finished + => Awaiter.IsCompletedSuccessfully; + + /// Start a backup process on the collected files. public BackupService(Logger logger, FilenameService fileNames) { - var files = PenumbraFiles(fileNames); - Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + var files = PenumbraFiles(fileNames); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files)); } /// Collect all relevant files for penumbra configuration. diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 3e61e3c1..c7efac04 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,10 +1,11 @@ using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Communication; namespace Penumbra.Services; -public class CommunicatorService : IDisposable +public class CommunicatorService : IDisposable, IService { public CommunicatorService(Logger logger) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index beb23fa2..b84c0996 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -22,10 +22,8 @@ namespace Penumbra.Services; /// Contains everything to migrate from older versions of the config to the current, /// including deprecated fields. /// -public class ConfigMigrationService +public class ConfigMigrationService(SaveService saveService) : IService { - private readonly SaveService _saveService; - private Configuration _config = null!; private JObject _data = null!; @@ -33,14 +31,11 @@ public class ConfigMigrationService public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = []; - public Dictionary ModSortOrder = new(); + public Dictionary ModSortOrder = []; public bool InvertModListOrder; public bool SortFoldersFirst; public SortModeV3 SortMode = SortModeV3.FoldersFirst; - public ConfigMigrationService(SaveService saveService) - => _saveService = saveService; - /// Add missing colors to the dictionary if necessary. private static void AddColors(Configuration config, bool forceSave) { @@ -61,13 +56,13 @@ public class ConfigMigrationService // because it stayed alive for a bunch of people for some reason. DeleteMetaTmp(); - if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile)) + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigFile)) { AddColors(config, false); return; } - _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); + _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile)); CreateBackup(); Version0To1(); @@ -118,7 +113,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - ActiveCollectionMigration.MigrateUngenderedCollections(_saveService.FileNames); + ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); _config.Version = 7; } @@ -223,7 +218,7 @@ public class ConfigMigrationService return; // Add the previous forced collection to all current collections except itself as an inheritance. - foreach (var collection in _saveService.FileNames.CollectionFiles) + foreach (var collection in saveService.FileNames.CollectionFiles) { try { @@ -246,7 +241,7 @@ public class ConfigMigrationService private void ResettleSortOrder() { ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; - var file = _saveService.FileNames.FilesystemFile; + var file = saveService.FileNames.FilesystemFile; using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); using var j = new JsonTextWriter(writer); @@ -281,7 +276,7 @@ public class ConfigMigrationService private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, IEnumerable<(CollectionType, string)> special) { - var file = _saveService.FileNames.ActiveCollectionsFile; + var file = saveService.FileNames.ActiveCollectionsFile; try { using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); @@ -337,7 +332,7 @@ public class ConfigMigrationService if (!collectionJson.Exists) return; - var defaultCollectionFile = new FileInfo(_saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); + var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; @@ -370,9 +365,9 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); - _saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); + saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) { @@ -384,7 +379,7 @@ public class ConfigMigrationService // Create a backup of the configuration file specifically. private void CreateBackup() { - var name = _saveService.FileNames.ConfigFile; + var name = saveService.FileNames.ConfigFile; var bakName = name + ".bak"; try { diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudConfigService.cs similarity index 72% rename from Penumbra/Services/DalamudServices.cs rename to Penumbra/Services/DalamudConfigService.cs index 51fb1192..8379a3e7 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudConfigService.cs @@ -1,21 +1,12 @@ -using Dalamud.Game; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Interface; -using Dalamud.IoC; using Dalamud.Plugin; -using Dalamud.Interface.DragDrop; -using Dalamud.Plugin.Services; using OtterGui.Services; -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - namespace Penumbra.Services; -public class DalamudConfigService +public class DalamudConfigService : IService { - public DalamudConfigService(DalamudPluginInterface pluginInterface) + public DalamudConfigService() { - pluginInterface.Inject(this); try { var serviceType = @@ -115,29 +106,3 @@ public class DalamudConfigService } } } - -public static class DalamudServices -{ - public static void AddServices(ServiceManager services, DalamudPluginInterface pi) - { - services.AddExistingService(pi); - services.AddExistingService(pi.UiBuilder); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - } -} diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 52881b9e..5f918a90 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -1,11 +1,11 @@ using Dalamud.Plugin; -using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Mods; namespace Penumbra.Services; -public class FilenameService(DalamudPluginInterface pi) +public class FilenameService(DalamudPluginInterface pi) : IService { public readonly string ConfigDirectory = pi.ConfigDirectory.FullName; public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index c893b00f..06c3c4d0 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -5,15 +5,12 @@ using Dalamud.Interface; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; +using OtterGui.Services; namespace Penumbra.Services; -public class MessageService : OtterGui.Classes.MessageService +public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) : OtterGui.Classes.MessageService(log, uiBuilder, chat), IService { - public MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) - : base(log, uiBuilder, chat) - { } - public void LinkItem(Item item) { // @formatter:off diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 40dc4107..801e0c1d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Subclasses; @@ -11,12 +12,9 @@ namespace Penumbra.Services; public interface ISavable : ISavable { } -public sealed class SaveService : SaveServiceBase +public sealed class SaveService(Logger log, FrameworkManager framework, FilenameService fileNames, BackupService backupService) + : SaveServiceBase(log, framework, fileNames, backupService.Awaiter), IService { - public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) - : base(log, framework, fileNames) - { } - /// Immediately delete all existing option group files for a mod and save them anew. public void SaveAllOptionGroups(Mod mod, bool backup, bool onlyAscii) { diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index 410acfb9..5a1c9f74 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -1,5 +1,10 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface.DragDrop; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; +using OtterGui; using OtterGui.Classes; using OtterGui.Compression; using OtterGui.Log; @@ -8,7 +13,6 @@ using Penumbra.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Structs; using Penumbra.Import.Textures; @@ -37,10 +41,9 @@ public static class ServiceManagerA public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { var services = new ServiceManager(log) + .AddDalamudServices(pi) .AddExistingService(log) .AddExistingService(penumbra) - .AddMeta() - .AddGameData() .AddInterop() .AddConfiguration() .AddCollections() @@ -52,27 +55,31 @@ public static class ServiceManagerA .AddApi(); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Penumbra).Assembly); - DalamudServices.AddServices(services, pi); + services.AddIServices(typeof(ImGuiUtil).Assembly); services.CreateProvider(); return services; } - private static ServiceManager AddMeta(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - - private static ServiceManager AddGameData(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(); + private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi) + => services.AddExistingService(pi) + .AddExistingService(pi.UiBuilder) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi); private static ServiceManager AddInterop(this ServiceManager services) => services.AddSingleton() @@ -95,8 +102,7 @@ public static class ServiceManagerA .AddSingleton(); private static ServiceManager AddConfiguration(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 714576b2..00fc0737 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,36 +1,26 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin; using Dalamud.Plugin.Services; using ImGuiNET; -using OtterGui.Log; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; using Penumbra.UI.AdvancedWindow; -using Penumbra.Util; namespace Penumbra.Services; -public class StainService : IDisposable +public class StainService : IService { - public sealed class StainTemplateCombo : FilterComboCache + public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) { - private readonly StmFile _stmFile; - private readonly FilterComboColors _stainCombo; - - public StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : base(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) - { - _stainCombo = stainCombo; - _stmFile = stmFile; - } - protected override float GetFilterWidth() { - var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (_stainCombo.CurrentSelection.Key == 0) + var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; + if (stainCombo.CurrentSelection.Key == 0) return baseSize; + return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; } @@ -46,19 +36,19 @@ public class StainService : IDisposable public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, ImGuiComboFlags flags = ImGuiComboFlags.None) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }); var spaceSize = ImGui.CalcTextSize(" ").X; - var spaces = (int) (previewWidth / spaceSize) - 1; + var spaces = (int)(previewWidth / spaceSize) - 1; return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags); } protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = _stainCombo.CurrentSelection.Key; - if (selection == 0 || !_stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) + var selection = stainCombo.CurrentSelection.Key; + if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); @@ -72,25 +62,18 @@ public class StainService : IDisposable } } - public readonly DictStains StainData; + public readonly DictStain StainData; public readonly FilterComboColors StainCombo; public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, Logger logger) + public StainService(IDataManager dataManager, DictStain stainData) { - StainData = new DictStains(pluginInterface, logger, dataManager); + StainData = stainData; StainCombo = new FilterComboColors(140, () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); - Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); - } - - public void Dispose() - { - StainData.Dispose(); - Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed."); } } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 7287938c..4d071f85 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,10 +1,11 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; using OtterGui.Classes; +using OtterGui.Services; namespace Penumbra.Services; -public class ValidityChecker +public class ValidityChecker : IService { public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; public const string SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json"; diff --git a/Penumbra/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index b84813cc..d5755dfd 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,7 +1,10 @@ -global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; +using Dalamud.Plugin.Services; +using OtterGui.Services; namespace Penumbra.Util; +public sealed class PerformanceTracker(IFramework framework) : OtterGui.Classes.PerformanceTracker(framework), IService; + public enum PerformanceType { UiMainWindow, From 309f0351fa2f5d95e831eb52ae78374a3d1ac772 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 31 Dec 2023 15:33:37 +1100 Subject: [PATCH 1368/2451] Build indices for entire mesh --- Penumbra/Import/Models/MeshConverter.cs | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index 9f55383e..4aa60331 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -40,7 +40,7 @@ public sealed class MeshConverter var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements .Select(element => (MdlFile.VertexUsage)element.Usage) .ToImmutableHashSet(); - + _geometryType = GetGeometryType(usages); _skinningType = GetSkinningType(usages); } @@ -58,7 +58,7 @@ public sealed class MeshConverter if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) // TODO: handle - i think this is a hard failure, it means that a bone name in the model doesn't exist in the armature. throw new Exception($"looking for {boneName} in {string.Join(", ", boneNameMap.Keys)}"); - + indexMap.Add(xivBoneIndex, gltfBoneIndex); } @@ -67,6 +67,7 @@ public sealed class MeshConverter private IMeshBuilder[] BuildMesh() { + var indices = BuildIndices(); var vertices = BuildVertices(); // TODO: handle submeshCount = 0 @@ -74,13 +75,14 @@ public sealed class MeshConverter return _mdl.SubMeshes .Skip(Mesh.SubMeshIndex) .Take(Mesh.SubMeshCount) - .Select(submesh => BuildSubMesh(submesh, vertices)) + .Select(submesh => BuildSubMesh(submesh, indices, vertices)) .ToArray(); } - private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList vertices) + private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList indices, IReadOnlyList vertices) { - var indices = BuildIndices(submesh); + // Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh. + var startIndex = (int)(submesh.IndexOffset - Mesh.StartIndex); var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), @@ -102,19 +104,19 @@ public sealed class MeshConverter // TODO: split by submeshes for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) primitiveBuilder.AddTriangle( - vertices[indices[indexOffset + 0]], - vertices[indices[indexOffset + 1]], - vertices[indices[indexOffset + 2]] + vertices[indices[indexOffset + startIndex + 0]], + vertices[indices[indexOffset + startIndex + 1]], + vertices[indices[indexOffset + startIndex + 2]] ); return meshBuilder; } - private IReadOnlyList BuildIndices(MdlStructs.SubmeshStruct submesh) + private IReadOnlyList BuildIndices() { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - reader.Seek(_mdl.IndexOffset[_lod] + submesh.IndexOffset * sizeof(ushort)); - return reader.ReadStructuresAsArray((int)submesh.IndexCount); + reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort)); + return reader.ReadStructuresAsArray((int)Mesh.IndexCount); } private IReadOnlyList BuildVertices() @@ -244,7 +246,7 @@ public sealed class MeshConverter // Some tangent W values that should be -1 are stored as 0. private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; - + private Vector3 ToVector3(object data) => data switch { From 989915ddbe36e6d5bedea429d3e132c97897a584 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 31 Dec 2023 16:10:20 +1100 Subject: [PATCH 1369/2451] Add initial shape key support --- Penumbra/Import/Models/MeshConverter.cs | 42 +++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index 4aa60331..ff52b967 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -100,14 +100,52 @@ public sealed class MeshConverter var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder); + // Store a list of the glTF indices. The list index will be equivalent to the xiv (submesh) index. + var gltfIndices = new List(); + // All XIV meshes use triangle lists. - // TODO: split by submeshes for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) - primitiveBuilder.AddTriangle( + { + var (a, b, c) = primitiveBuilder.AddTriangle( vertices[indices[indexOffset + startIndex + 0]], vertices[indices[indexOffset + startIndex + 1]], vertices[indices[indexOffset + startIndex + 2]] ); + gltfIndices.AddRange([a, b, c]); + } + + var primitiveVertices = meshBuilder.Primitives.First().Vertices; + var shapeNames = new List(); + + foreach (var shape in _mdl.Shapes) + { + // Filter down to shape values for the current mesh that sit within the bounds of the current submesh. + var shapeValues = _mdl.ShapeMeshes + .Skip(shape.ShapeMeshStartIndex[_lod]) + .Take(shape.ShapeMeshCount[_lod]) + .Where(shapeMesh => shapeMesh.MeshIndexOffset == Mesh.StartIndex) + .SelectMany(shapeMesh => + _mdl.ShapeValues + .Skip((int)shapeMesh.ShapeValueOffset) + .Take((int)shapeMesh.ShapeValueCount) + ) + .Where(shapeValue => + shapeValue.BaseIndicesIndex >= startIndex + && shapeValue.BaseIndicesIndex < startIndex + submesh.IndexCount + ) + .ToList(); + + if (shapeValues.Count == 0) continue; + + var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count); + shapeNames.Add(shape.ShapeName); + + foreach (var shapeValue in shapeValues) + morphBuilder.SetVertex( + primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - startIndex]].GetGeometry(), + vertices[shapeValue.ReplacingVertexIndex].GetGeometry() + ); + } return meshBuilder; } From 6a2b802196953d78033cdc6824c0c22bd87e6192 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 31 Dec 2023 17:11:08 +1100 Subject: [PATCH 1370/2451] Add shape key names --- Penumbra/Import/Models/MeshConverter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/MeshConverter.cs index ff52b967..97121798 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/MeshConverter.cs @@ -4,6 +4,7 @@ using Lumina.Extensions; using Penumbra.GameData.Files; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.IO; using SharpGLTF.Materials; namespace Penumbra.Import.Modules; @@ -124,7 +125,7 @@ public sealed class MeshConverter .Skip(shape.ShapeMeshStartIndex[_lod]) .Take(shape.ShapeMeshCount[_lod]) .Where(shapeMesh => shapeMesh.MeshIndexOffset == Mesh.StartIndex) - .SelectMany(shapeMesh => + .SelectMany(shapeMesh => _mdl.ShapeValues .Skip((int)shapeMesh.ShapeValueOffset) .Take((int)shapeMesh.ShapeValueCount) @@ -147,6 +148,10 @@ public sealed class MeshConverter ); } + meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() { + {"targetNames", shapeNames} + }); + return meshBuilder; } From 551c25a64cf93a58fe2e7ca253a79542da345474 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 00:18:03 +1100 Subject: [PATCH 1371/2451] Move a few things to export subdir --- .../MeshExporter.cs} | 60 +++++++---- .../Import/Models/Export/ModelExporter.cs | 100 ++++++++++++++++++ .../Import/Models/{ => Export}/Skeleton.cs | 16 ++- Penumbra/Import/Models/ModelManager.cs | 69 ++---------- Penumbra/Import/Models/SkeletonConverter.cs | 11 +- 5 files changed, 169 insertions(+), 87 deletions(-) rename Penumbra/Import/Models/{MeshConverter.cs => Export/MeshExporter.cs} (84%) create mode 100644 Penumbra/Import/Models/Export/ModelExporter.cs rename Penumbra/Import/Models/{ => Export}/Skeleton.cs (53%) diff --git a/Penumbra/Import/Models/MeshConverter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs similarity index 84% rename from Penumbra/Import/Models/MeshConverter.cs rename to Penumbra/Import/Models/Export/MeshExporter.cs index 97121798..fdfa59e4 100644 --- a/Penumbra/Import/Models/MeshConverter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -6,15 +6,39 @@ using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.IO; using SharpGLTF.Materials; +using SharpGLTF.Scenes; -namespace Penumbra.Import.Modules; +namespace Penumbra.Import.Models.Export; -public sealed class MeshConverter +public class MeshExporter { - public static IMeshBuilder[] ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + public class Mesh { - var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap); - return self.BuildMesh(); + private IMeshBuilder[] _meshes; + private NodeBuilder[]? _joints; + + public Mesh(IMeshBuilder[] meshes, NodeBuilder[]? joints) + { + _meshes = meshes; + _joints = joints; + } + + public void AddToScene(SceneBuilder scene) + { + // TODO: throw if mesh has skinned vertices but no joints are available? + foreach (var mesh in _meshes) + if (_joints == null) + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + else + scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints); + } + } + + // TODO: replace bonenamemap with a gltfskeleton + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton) + { + var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names); + return new Mesh(self.BuildMeshes(), skeleton?.Joints); } private const byte MaximumMeshBufferStreams = 3; @@ -22,14 +46,14 @@ public sealed class MeshConverter private readonly MdlFile _mdl; private readonly byte _lod; private readonly ushort _meshIndex; - private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex]; + private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; private readonly Dictionary? _boneIndexMap; private readonly Type _geometryType; private readonly Type _skinningType; - private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) { _mdl = mdl; _lod = lod; @@ -49,7 +73,7 @@ public sealed class MeshConverter private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) { // todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... - var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex]; + var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); @@ -66,16 +90,16 @@ public sealed class MeshConverter return indexMap; } - private IMeshBuilder[] BuildMesh() + private IMeshBuilder[] BuildMeshes() { var indices = BuildIndices(); var vertices = BuildVertices(); - // TODO: handle submeshCount = 0 + // TODO: handle SubMeshCount = 0 return _mdl.SubMeshes - .Skip(Mesh.SubMeshIndex) - .Take(Mesh.SubMeshCount) + .Skip(XivMesh.SubMeshIndex) + .Take(XivMesh.SubMeshCount) .Select(submesh => BuildSubMesh(submesh, indices, vertices)) .ToArray(); } @@ -83,7 +107,7 @@ public sealed class MeshConverter private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList indices, IReadOnlyList vertices) { // Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh. - var startIndex = (int)(submesh.IndexOffset - Mesh.StartIndex); + var startIndex = (int)(submesh.IndexOffset - XivMesh.StartIndex); var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), @@ -124,7 +148,7 @@ public sealed class MeshConverter var shapeValues = _mdl.ShapeMeshes .Skip(shape.ShapeMeshStartIndex[_lod]) .Take(shape.ShapeMeshCount[_lod]) - .Where(shapeMesh => shapeMesh.MeshIndexOffset == Mesh.StartIndex) + .Where(shapeMesh => shapeMesh.MeshIndexOffset == XivMesh.StartIndex) .SelectMany(shapeMesh => _mdl.ShapeValues .Skip((int)shapeMesh.ShapeValueOffset) @@ -158,8 +182,8 @@ public sealed class MeshConverter private IReadOnlyList BuildIndices() { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort)); - return reader.ReadStructuresAsArray((int)Mesh.IndexCount); + reader.Seek(_mdl.IndexOffset[_lod] + XivMesh.StartIndex * sizeof(ushort)); + return reader.ReadStructuresAsArray((int)XivMesh.IndexCount); } private IReadOnlyList BuildVertices() @@ -172,7 +196,7 @@ public sealed class MeshConverter for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) { streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + Mesh.VertexBufferOffset[streamIndex]); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]); } var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements @@ -183,7 +207,7 @@ public sealed class MeshConverter var vertices = new List(); var attributes = new Dictionary(); - for (var vertexIndex = 0; vertexIndex < Mesh.VertexCount; vertexIndex++) + for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs new file mode 100644 index 00000000..c8716cf3 --- /dev/null +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -0,0 +1,100 @@ +using Penumbra.GameData.Files; +using SharpGLTF.Scenes; +using SharpGLTF.Transforms; + +namespace Penumbra.Import.Models.Export; + +public class ModelExporter +{ + public class Model + { + private List _meshes; + private GltfSkeleton? _skeleton; + + public Model(List meshes, GltfSkeleton? skeleton) + { + _meshes = meshes; + _skeleton = skeleton; + } + + public void AddToScene(SceneBuilder scene) + { + // If there's a skeleton, the root node should be added before we add any potentially skinned meshes. + var skeletonRoot = _skeleton?.Root; + if (skeletonRoot != null) + scene.AddNode(skeletonRoot); + + // Add all the meshes to the scene. + foreach (var mesh in _meshes) + mesh.AddToScene(scene); + } + } + + public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) + { + var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; + var meshes = ConvertMeshes(mdl, gltfSkeleton); + return new Model(meshes, gltfSkeleton); + } + + private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) + { + var meshes = new List(); + + for (byte lodIndex = 0; lodIndex < mdl.LodCount; lodIndex++) + { + var lod = mdl.Lods[lodIndex]; + + // TODO: consider other types of mesh? + for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) + { + var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton); + meshes.Add(mesh); + } + } + + return meshes; + } + + private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) + { + NodeBuilder? root = null; + var names = new Dictionary(); + var joints = new List(); + for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) + { + var bone = skeleton.Bones[boneIndex]; + + if (names.ContainsKey(bone.Name)) continue; + + var node = new NodeBuilder(bone.Name); + names[bone.Name] = joints.Count; + joints.Add(node); + + node.SetLocalTransform(new AffineTransform( + bone.Transform.Scale, + bone.Transform.Rotation, + bone.Transform.Translation + ), false); + + if (bone.ParentIndex == -1) + { + root = node; + continue; + } + + var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; + parent.AddNode(node); + } + + if (root == null) + return null; + + return new() + { + Root = root, + Joints = joints.ToArray(), + Names = names, + }; + } +} diff --git a/Penumbra/Import/Models/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs similarity index 53% rename from Penumbra/Import/Models/Skeleton.cs rename to Penumbra/Import/Models/Export/Skeleton.cs index fb5c8284..13379dc4 100644 --- a/Penumbra/Import/Models/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -1,11 +1,12 @@ -namespace Penumbra.Import.Models; +using SharpGLTF.Scenes; -// TODO: this should almost certainly live in gamedata. if not, it should at _least_ be adjacent to the model handling. -public class Skeleton +namespace Penumbra.Import.Models.Export; + +public class XivSkeleton { public Bone[] Bones; - public Skeleton(Bone[] bones) + public XivSkeleton(Bone[] bones) { Bones = bones; } @@ -23,3 +24,10 @@ public class Skeleton public Vector3 Translation; } } + +public struct GltfSkeleton +{ + public NodeBuilder Root; + public NodeBuilder[] Joints; + public Dictionary Names; +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 2dd64235..9f72619f 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; -using Penumbra.Import.Modules; +using Penumbra.Import.Models.Export; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -73,36 +73,18 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken cancel) { + var xivSkeleton = BuildSkeleton(cancel); + var model = ModelExporter.Export(_mdl, xivSkeleton); + var scene = new SceneBuilder(); + model.AddToScene(scene); - var skeleton = BuildSkeleton(cancel); - if (skeleton != null) - scene.AddNode(skeleton.Value.Root); - - // TODO: group by LoD in output tree - for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++) - { - var lod = _mdl.Lods[lodIndex]; - - // TODO: consider other types of mesh? - for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) - { - var meshBuilders = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names); - // TODO: use a value from the mesh converter for this check, rather than assuming that it has joints - foreach (var meshBuilder in meshBuilders) - if (skeleton == null) - scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity); - else - scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints); - } - } - - var model = scene.ToGltf2(); - model.SaveGLTF(_outputPath); + var gltfModel = scene.ToGltf2(); + gltfModel.SaveGLTF(_outputPath); } // TODO: this should be moved to a seperate model converter or something - private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary Names)? BuildSkeleton(CancellationToken cancel) + private XivSkeleton? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) return null; @@ -117,40 +99,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var skeletonConverter = new SkeletonConverter(); var skeleton = skeletonConverter.FromXml(xml); - // this is (less) atrocious - NodeBuilder? root = null; - var names = new Dictionary(); - var joints = new List(); - for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) - { - var bone = skeleton.Bones[boneIndex]; - - if (names.ContainsKey(bone.Name)) continue; - - var node = new NodeBuilder(bone.Name); - names[bone.Name] = joints.Count; - joints.Add(node); - - node.SetLocalTransform(new AffineTransform( - bone.Transform.Scale, - bone.Transform.Rotation, - bone.Transform.Translation - ), false); - - if (bone.ParentIndex == -1) - { - root = node; - continue; - } - - var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]]; - parent.AddNode(node); - } - - if (root == null) - return null; - - return (root, joints.ToArray(), names); + return skeleton; } public bool Equals(IAction? other) diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index d54b0294..e265e5c3 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -1,12 +1,13 @@ using System.Xml; using OtterGui; +using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; // TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up. public class SkeletonConverter { - public Skeleton FromXml(string xml) + public XivSkeleton FromXml(string xml) { var document = new XmlDocument(); document.LoadXml(xml); @@ -29,7 +30,7 @@ public class SkeletonConverter .Select(values => { var (transform, parentIndex, name) = values; - return new Skeleton.Bone() + return new XivSkeleton.Bone() { Transform = transform, ParentIndex = parentIndex, @@ -38,7 +39,7 @@ public class SkeletonConverter }) .ToArray(); - return new Skeleton(bones); + return new XivSkeleton(bones); } /// Get the main skeleton ID for a given skeleton document. @@ -57,14 +58,14 @@ public class SkeletonConverter /// Read the reference pose transforms for a skeleton. /// XML node for the skeleton. - private Skeleton.Transform[] ReadReferencePose(XmlNode node) + private XivSkeleton.Transform[] ReadReferencePose(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), node => { var raw = ReadVec12(node); - return new Skeleton.Transform() + return new XivSkeleton.Transform() { Translation = new(raw[0], raw[1], raw[2]), Rotation = new(raw[4], raw[5], raw[6], raw[7]), From f1379af92cbf292622c79cc1b1ff4e82a441ec8b Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 00:38:24 +1100 Subject: [PATCH 1372/2451] Add UV export --- Penumbra/Import/Models/Export/MeshExporter.cs | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index fdfa59e4..06ca747b 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -51,6 +51,7 @@ public class MeshExporter private readonly Dictionary? _boneIndexMap; private readonly Type _geometryType; + private readonly Type _materialType; private readonly Type _skinningType; private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) @@ -67,6 +68,7 @@ public class MeshExporter .ToImmutableHashSet(); _geometryType = GetGeometryType(usages); + _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); } @@ -112,7 +114,7 @@ public class MeshExporter var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), _geometryType, - typeof(VertexEmpty), + _materialType, _skinningType ); var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; @@ -189,7 +191,7 @@ public class MeshExporter private IReadOnlyList BuildVertices() { var vertexBuilderType = typeof(VertexBuilder<,,>) - .MakeGenericType(_geometryType, typeof(VertexEmpty), _skinningType); + .MakeGenericType(_geometryType, _materialType, _skinningType); // NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop. var streams = new BinaryReader[MaximumMeshBufferStreams]; @@ -215,9 +217,10 @@ public class MeshExporter attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); var vertexGeometry = BuildVertexGeometry(attributes); + var vertexMaterial = BuildVertexMaterial(attributes); var vertexSkinning = BuildVertexSkinning(attributes); - var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), vertexSkinning)!; + var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, vertexMaterial, vertexSkinning)!; vertices.Add(vertexBuilder); } @@ -276,6 +279,43 @@ public class MeshExporter throw new Exception($"Unknown geometry type {_geometryType}."); } + private Type GetMaterialType(IReadOnlySet usages) + { + // TODO: IIUC, xiv's uv2 is usually represented as the second two components of a vec4 uv attribute - add support. + var materialUsages = ( + usages.Contains(MdlFile.VertexUsage.UV), + usages.Contains(MdlFile.VertexUsage.Color) + ); + + return materialUsages switch + { + (true, true) => typeof(VertexColor1Texture1), + (true, false) => typeof(VertexTexture1), + (false, true) => typeof(VertexColor1), + (false, false) => typeof(VertexEmpty), + }; + } + + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) + { + if (_materialType == typeof(VertexEmpty)) + return new VertexEmpty(); + + if (_materialType == typeof(VertexColor1)) + return new VertexColor1(ToVector4(attributes[MdlFile.VertexUsage.Color])); + + if (_materialType == typeof(VertexTexture1)) + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV])); + + if (_materialType == typeof(VertexColor1Texture1)) + return new VertexColor1Texture1( + ToVector4(attributes[MdlFile.VertexUsage.Color]), + ToVector2(attributes[MdlFile.VertexUsage.UV]) + ); + + throw new Exception($"Unknown material type {_skinningType}"); + } + private Type GetSkinningType(IReadOnlySet usages) { // TODO: possibly need to check only index - weight might be missing? @@ -314,6 +354,15 @@ public class MeshExporter private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; + private Vector2 ToVector2(object data) + => data switch + { + Vector2 v2 => v2, + Vector3 v3 => new Vector2(v3.X, v3.Y), + Vector4 v4 => new Vector2(v4.X, v4.Y), + _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}") + }; + private Vector3 ToVector3(object data) => data switch { From dc845b766e654a1cfef2ff1c792091c3999eabea Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 00:57:27 +1100 Subject: [PATCH 1373/2451] Clean up top-level conversion utilities. --- Penumbra/Import/Models/HavokConverter.cs | 60 +++++++++------------ Penumbra/Import/Models/ModelManager.cs | 9 +--- Penumbra/Import/Models/SkeletonConverter.cs | 47 +++++++++------- 3 files changed, 55 insertions(+), 61 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 515c6f97..7f87d50a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -2,24 +2,19 @@ using FFXIVClientStructs.Havok; namespace Penumbra.Import.Models; -// TODO: where should this live? interop i guess, in penum? or game data? -public unsafe class HavokConverter +public static unsafe class HavokConverter { - /// Creates a temporary file and returns its path. - /// Path to a temporary file. - private string CreateTempFile() + /// Creates a temporary file and returns its path. + private static string CreateTempFile() { - var s = File.Create(Path.GetTempFileName()); - s.Close(); - return s.Name; + var stream = File.Create(Path.GetTempFileName()); + stream.Close(); + return stream.Name; } - /// Converts a .hkx file to a .xml file. - /// A byte array representing the .hkx file. - /// A string representing the .xml file. - /// Thrown if parsing the .hkx file fails. - /// Thrown if writing the .xml file fails. - public string HkxToXml(byte[] hkx) + /// Converts a .hkx file to a .xml file. + /// A byte array representing the .hkx file. + public static string HkxToXml(byte[] hkx) { var tempHkx = CreateTempFile(); File.WriteAllBytes(tempHkx, hkx); @@ -27,7 +22,7 @@ public unsafe class HavokConverter var resource = Read(tempHkx); File.Delete(tempHkx); - if (resource == null) throw new Exception("HavokReadException"); + if (resource == null) throw new Exception("Failed to read havok file."); var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers | hkSerializeUtil.SaveOptionBits.TextFormat @@ -42,12 +37,9 @@ public unsafe class HavokConverter return bytes; } - /// Converts a .xml file to a .hkx file. - /// A string representing the .xml file. - /// A byte array representing the .hkx file. - /// Thrown if parsing the .xml file fails. - /// Thrown if writing the .hkx file fails. - public byte[] XmlToHkx(string xml) + /// Converts an .xml file to a .hkx file. + /// A string representing the .xml file. + public static byte[] XmlToHkx(string xml) { var tempXml = CreateTempFile(); File.WriteAllText(tempXml, xml); @@ -55,7 +47,7 @@ public unsafe class HavokConverter var resource = Read(tempXml); File.Delete(tempXml); - if (resource == null) throw new Exception("HavokReadException"); + if (resource == null) throw new Exception("Failed to read havok file."); var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers | hkSerializeUtil.SaveOptionBits.WriteAttributes; @@ -74,9 +66,8 @@ public unsafe class HavokConverter /// The type is guessed automatically by Havok. /// This pointer might be null - you should check for that. /// - /// Path to a file on the filesystem. - /// A (potentially null) pointer to an hkResource. - private hkResource* Read(string filePath) + /// Path to a file on the filesystem. + private static hkResource* Read(string filePath) { var path = Marshal.StringToHGlobalAnsi(filePath); @@ -87,18 +78,15 @@ public unsafe class HavokConverter loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); - // TODO: probably can loadfrombuffer this + // TODO: probably can use LoadFromBuffer for this. var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); return resource; } - /// Serializes an hkResource* to a temporary file. - /// A pointer to the hkResource, opened through Read(). - /// Flags representing how to serialize the file. - /// An opened FileStream of a temporary file. You are expected to read the file and delete it. - /// Thrown if accessing the root level container fails. - /// Thrown if an unknown failure in writing occurs. - private FileStream Write( + /// Serializes an hkResource* to a temporary file. + /// A pointer to the hkResource, opened through Read(). + /// Flags representing how to serialize the file. + private static FileStream Write( hkResource* resource, hkSerializeUtil.SaveOptionBits optionBits ) @@ -125,16 +113,16 @@ public unsafe class HavokConverter var name = "hkRootLevelContainer"; var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); - if (resourcePtr == null) throw new Exception("HavokWriteException"); + if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource."); var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); - if (hkRootLevelContainerClass == null) throw new Exception("HavokWriteException"); + if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type."); hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); } finally { oStream.Dtor(); } - if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("HavokFailureException"); + if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file."); return new FileStream(tempFile, FileMode.Open); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9f72619f..a56d7168 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -89,17 +89,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable if (_sklb == null) return null; - // TODO: Consider making these static methods. // TODO: work out how i handle this havok deal. running it outside the framework causes an immediate ctd. - var havokConverter = new HavokConverter(); - var xmlTask = _manager._framework.RunOnFrameworkThread(() => havokConverter.HkxToXml(_sklb.Skeleton)); + var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton)); xmlTask.Wait(cancel); var xml = xmlTask.Result; - var skeletonConverter = new SkeletonConverter(); - var skeleton = skeletonConverter.FromXml(xml); - - return skeleton; + return SkeletonConverter.FromXml(xml); } public bool Equals(IAction? other) diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index e265e5c3..24bcf3e0 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -4,10 +4,11 @@ using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; -// TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up. -public class SkeletonConverter +public static class SkeletonConverter { - public XivSkeleton FromXml(string xml) + /// Parse XIV skeleton data from a havok XML tagfile. + /// Havok XML tagfile containing skeleton data. + public static XivSkeleton FromXml(string xml) { var document = new XmlDocument(); document.LoadXml(xml); @@ -16,14 +17,14 @@ public class SkeletonConverter var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']"); if (skeletonNode == null) - throw new InvalidDataException(); + throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); var referencePose = ReadReferencePose(skeletonNode); var parentIndices = ReadParentIndices(skeletonNode); var boneNames = ReadBoneNames(skeletonNode); if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length) - throw new InvalidDataException(); + throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); var bones = referencePose .Zip(parentIndices, boneNames) @@ -38,27 +39,27 @@ public class SkeletonConverter }; }) .ToArray(); - + return new XivSkeleton(bones); } - /// Get the main skeleton ID for a given skeleton document. - /// XML skeleton document. - private string GetMainSkeletonId(XmlNode node) + /// Get the main skeleton ID for a given skeleton document. + /// XML skeleton document. + private static string GetMainSkeletonId(XmlNode node) { var animationSkeletons = node .SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")? .ChildNodes; if (animationSkeletons?.Count != 1) - throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}"); + throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}."); return animationSkeletons[0]!.InnerText; } - /// Read the reference pose transforms for a skeleton. - /// XML node for the skeleton. - private XivSkeleton.Transform[] ReadReferencePose(XmlNode node) + /// Read the reference pose transforms for a skeleton. + /// XML node for the skeleton. + private static XivSkeleton.Transform[] ReadReferencePose(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), @@ -75,7 +76,9 @@ public class SkeletonConverter ); } - private float[] ReadVec12(XmlNode node) + /// Read a 12-item vector from a tagfile. + /// Havok Vec12 XML node. + private static float[] ReadVec12(XmlNode node) { var array = node.ChildNodes .Cast() @@ -89,12 +92,14 @@ public class SkeletonConverter .ToArray(); if (array.Length != 12) - throw new InvalidDataException(); + throw new InvalidDataException($"Unexpected Vector12 length ({array.Length})."); return array; } - private int[] ReadParentIndices(XmlNode node) + /// Read the bone parent relations for a skeleton. + /// XML node for the skeleton. + private static int[] ReadParentIndices(XmlNode node) { // todo: would be neat to genericise array between bare and children return CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) @@ -104,7 +109,9 @@ public class SkeletonConverter .ToArray(); } - private string[] ReadBoneNames(XmlNode node) + /// Read the names of bones in a skeleton. + /// XML node for the skeleton. + private static string[] ReadBoneNames(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='bones']")), @@ -112,7 +119,10 @@ public class SkeletonConverter ); } - private T[] ReadArray(XmlNode node, Func convert) + /// Read an XML tagfile array, converting it via the provided conversion function. + /// Tagfile XML array node. + /// Function to convert array item nodes to required data types. + private static T[] ReadArray(XmlNode node, Func convert) { var element = (XmlElement)node; @@ -125,6 +135,7 @@ public class SkeletonConverter return array; } + /// Check if the argument is null, returning a non-nullable value if it exists, and throwing if not. private static T CheckExists(T? value) { ArgumentNullException.ThrowIfNull(value); From da019e729d80690288ba7ff585ba7d9cd53c8fe6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 31 Dec 2023 15:10:30 +0100 Subject: [PATCH 1374/2451] Move all animation and game event hooks to own classes. --- OtterGui | 2 +- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Communication/ChangedItemClick.cs | 9 +- Penumbra/Communication/ChangedItemHover.cs | 9 +- Penumbra/Communication/CollectionChange.cs | 10 +- .../CollectionInheritanceChanged.cs | 10 +- .../Communication/CreatedCharacterBase.cs | 10 +- .../Communication/CreatingCharacterBase.cs | 10 +- Penumbra/Communication/EnabledChanged.cs | 9 +- Penumbra/Communication/ModDataChanged.cs | 9 +- Penumbra/Communication/ModDirectoryChanged.cs | 9 +- .../Communication/ModDiscoveryFinished.cs | 10 +- Penumbra/Communication/ModDiscoveryStarted.cs | 9 +- Penumbra/Communication/ModOptionChanged.cs | 10 +- Penumbra/Communication/ModPathChanged.cs | 10 +- Penumbra/Communication/ModSettingChanged.cs | 10 +- Penumbra/Communication/MtrlShpkLoaded.cs | 9 +- .../Communication/PostSettingsPanelDraw.cs | 9 +- .../Communication/PreSettingsPanelDraw.cs | 9 +- Penumbra/Communication/ResolvedFileChanged.cs | 14 +- Penumbra/Communication/SelectTab.cs | 9 +- .../Communication/TemporaryGlobalModChange.cs | 10 +- Penumbra/Interop/GameState.cs | 117 +++++ .../Animation/ApricotListenerSoundPlay.cs | 54 +++ .../Animation/CharacterBaseLoadAnimation.cs | 41 ++ Penumbra/Interop/Hooks/Animation/Dismount.cs | 44 ++ .../Interop/Hooks/Animation/LoadAreaVfx.cs | 38 ++ .../Hooks/Animation/LoadCharacterSound.cs | 34 ++ .../Hooks/Animation/LoadCharacterVfx.cs | 65 +++ .../Hooks/Animation/LoadTimelineResources.cs | 71 +++ .../Interop/Hooks/Animation/PlayFootstep.cs | 30 ++ .../Hooks/Animation/ScheduleClipUpdate.cs | 35 ++ .../Interop/Hooks/Animation/SomeActionLoad.cs | 32 ++ .../Hooks/Animation/SomeMountAnimation.cs | 31 ++ .../Interop/Hooks/Animation/SomePapLoad.cs | 46 ++ .../Hooks/Animation/SomeParasolAnimation.cs | 31 ++ .../Interop/Hooks/CharacterBaseDestructor.cs | 49 ++ Penumbra/Interop/Hooks/CharacterDestructor.cs | 49 ++ Penumbra/Interop/Hooks/CopyCharacter.cs | 47 ++ Penumbra/Interop/Hooks/CreateCharacterBase.cs | 74 +++ Penumbra/Interop/Hooks/DebugHook.cs | 43 ++ Penumbra/Interop/Hooks/EnableDraw.cs | 48 ++ .../Interop/Hooks/ResourceHandleDestructor.cs | 50 +++ Penumbra/Interop/Hooks/WeaponReload.cs | 71 +++ .../PathResolving/AnimationHookService.cs | 423 ------------------ .../PathResolving/CollectionResolver.cs | 127 +++--- .../Interop/PathResolving/CutsceneService.cs | 27 +- .../Interop/PathResolving/DrawObjectState.cs | 90 ++-- .../IdentifiedCollectionCache.cs | 18 +- Penumbra/Interop/PathResolving/MetaState.cs | 40 +- .../Interop/PathResolving/PathResolver.cs | 38 +- .../Interop/PathResolving/SubfileHelper.cs | 23 +- Penumbra/Interop/Services/GameEventManager.cs | 299 ------------- Penumbra/Interop/Services/SkinFixer.cs | 22 +- Penumbra/Mods/Manager/ModDataEditor.cs | 1 - Penumbra/Penumbra.cs | 4 + Penumbra/Penumbra.csproj | 5 +- Penumbra/Services/CommunicatorService.cs | 2 +- Penumbra/Services/ServiceManagerA.cs | 6 +- .../ModEditWindow.Materials.MtrlTab.cs | 16 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 72 +-- Penumbra/packages.lock.json | 34 +- 62 files changed, 1402 insertions(+), 1143 deletions(-) create mode 100644 Penumbra/Interop/GameState.cs create mode 100644 Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs create mode 100644 Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs create mode 100644 Penumbra/Interop/Hooks/Animation/Dismount.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs create mode 100644 Penumbra/Interop/Hooks/Animation/PlayFootstep.cs create mode 100644 Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs create mode 100644 Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs create mode 100644 Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs create mode 100644 Penumbra/Interop/Hooks/Animation/SomePapLoad.cs create mode 100644 Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs create mode 100644 Penumbra/Interop/Hooks/CharacterBaseDestructor.cs create mode 100644 Penumbra/Interop/Hooks/CharacterDestructor.cs create mode 100644 Penumbra/Interop/Hooks/CopyCharacter.cs create mode 100644 Penumbra/Interop/Hooks/CreateCharacterBase.cs create mode 100644 Penumbra/Interop/Hooks/DebugHook.cs create mode 100644 Penumbra/Interop/Hooks/EnableDraw.cs create mode 100644 Penumbra/Interop/Hooks/ResourceHandleDestructor.cs create mode 100644 Penumbra/Interop/Hooks/WeaponReload.cs delete mode 100644 Penumbra/Interop/PathResolving/AnimationHookService.cs delete mode 100644 Penumbra/Interop/Services/GameEventManager.cs diff --git a/OtterGui b/OtterGui index f6a8ad0f..22ae2a89 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f6a8ad0f8e585408e0aa17c90209358403b52535 +Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index b7a46ae2..2a7a9bfb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -259,7 +259,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } else if (tab != TabType.None) { - _communicator.SelectTab.Invoke(tab); + _communicator.SelectTab.Invoke(tab, null); } return PenumbraApiEc.Success; diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index ea389bb6..b11f2306 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -11,7 +11,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick : EventWrapper, ChangedItemClick.Priority> +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { @@ -21,11 +21,4 @@ public sealed class ChangedItemClick : EventWrapper /// Link = 1, } - - public ChangedItemClick() - : base(nameof(ChangedItemClick)) - { } - - public void Invoke(MouseButton button, object? data) - => Invoke(this, button, data); } diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index cf270ba0..10607da4 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -8,7 +8,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover : EventWrapper, ChangedItemHover.Priority> +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { @@ -19,13 +19,6 @@ public sealed class ChangedItemHover : EventWrapper, ChangedItem Link = 1, } - public ChangedItemHover() - : base(nameof(ChangedItemHover)) - { } - - public void Invoke(object? data) - => Invoke(this, data); - public bool HasTooltip => HasSubscribers; } diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index b713cc72..95d4ac4d 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -12,7 +12,8 @@ namespace Penumbra.Communication; /// Parameter is the new collection, or null on deletions. /// Parameter is the display name for Individual collections or an empty string otherwise. /// -public sealed class CollectionChange : EventWrapper, CollectionChange.Priority> +public sealed class CollectionChange() + : EventWrapper(nameof(CollectionChange)) { public enum Priority { @@ -46,11 +47,4 @@ public sealed class CollectionChange : EventWrapper ModFileSystemSelector = 0, } - - public CollectionChange() - : base(nameof(CollectionChange)) - { } - - public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) - => Invoke(this, collectionType, oldCollection, newCollection, displayName); } diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index 8288341d..dbcf9e4a 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -10,7 +10,8 @@ namespace Penumbra.Communication; /// Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). /// /// -public sealed class CollectionInheritanceChanged : EventWrapper, CollectionInheritanceChanged.Priority> +public sealed class CollectionInheritanceChanged() + : EventWrapper(nameof(CollectionInheritanceChanged)) { public enum Priority { @@ -23,11 +24,4 @@ public sealed class CollectionInheritanceChanged : EventWrapper ModFileSystemSelector = 0, } - - public CollectionInheritanceChanged() - : base(nameof(CollectionInheritanceChanged)) - { } - - public void Invoke(ModCollection collection, bool inherited) - => Invoke(this, collection, inherited); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index b1903e5b..397f7bfd 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -9,18 +9,12 @@ namespace Penumbra.Communication; /// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase() + : EventWrapper(nameof(CreatedCharacterBase)) { public enum Priority { /// Api = int.MinValue, } - - public CreatedCharacterBase() - : base(nameof(CreatedCharacterBase)) - { } - - public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) - => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 090ca40b..1e232761 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -12,18 +12,12 @@ namespace Penumbra.Communication; /// Parameter is a pointer to the customize array. /// Parameter is a pointer to the equip data array. /// -public sealed class CreatingCharacterBase : EventWrapper, CreatingCharacterBase.Priority> +public sealed class CreatingCharacterBase() + : EventWrapper(nameof(CreatingCharacterBase)) { public enum Priority { /// Api = 0, } - - public CreatingCharacterBase() - : base(nameof(CreatingCharacterBase)) - { } - - public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) - => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); } diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index fa768235..be6343b7 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -9,7 +9,7 @@ namespace Penumbra.Communication; /// Parameter is whether Penumbra is now Enabled (true) or Disabled (false). /// /// -public sealed class EnabledChanged : EventWrapper, EnabledChanged.Priority> +public sealed class EnabledChanged() : EventWrapper(nameof(EnabledChanged)) { public enum Priority { @@ -19,11 +19,4 @@ public sealed class EnabledChanged : EventWrapper, EnabledChanged.P /// DalamudSubstitutionProvider = 0, } - - public EnabledChanged() - : base(nameof(EnabledChanged)) - { } - - public void Invoke(bool enabled) - => Invoke(this, enabled); } diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index 2f50f005..ffa43d43 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -11,7 +11,7 @@ namespace Penumbra.Communication; /// Parameter is the changed mod. /// Parameter is the old name of the mod in case of a name change, and null otherwise. /// -public sealed class ModDataChanged : EventWrapper, ModDataChanged.Priority> +public sealed class ModDataChanged() : EventWrapper(nameof(ModDataChanged)) { public enum Priority { @@ -27,11 +27,4 @@ public sealed class ModDataChanged : EventWrapper ModPanelHeader = 0, } - - public ModDataChanged() - : base(nameof(ModDataChanged)) - { } - - public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) - => Invoke(this, changeType, mod, oldName); } diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 9fdb261e..20d13b20 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -10,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is whether the new directory is valid. /// /// -public sealed class ModDirectoryChanged : EventWrapper, ModDirectoryChanged.Priority> +public sealed class ModDirectoryChanged() : EventWrapper(nameof(ModDirectoryChanged)) { public enum Priority { @@ -20,11 +20,4 @@ public sealed class ModDirectoryChanged : EventWrapper, Mod /// FileDialogService = 0, } - - public ModDirectoryChanged() - : base(nameof(ModDirectoryChanged)) - { } - - public void Invoke(string newModDirectory, bool newDirectoryValid) - => Invoke(this, newModDirectory, newDirectoryValid); } diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs index 04c13e95..759ea42e 100644 --- a/Penumbra/Communication/ModDiscoveryFinished.cs +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -1,10 +1,9 @@ using OtterGui.Classes; -using Penumbra.Mods.Manager; namespace Penumbra.Communication; /// Triggered whenever a new mod discovery has finished. -public sealed class ModDiscoveryFinished : EventWrapper +public sealed class ModDiscoveryFinished() : EventWrapper(nameof(ModDiscoveryFinished)) { public enum Priority { @@ -23,11 +22,4 @@ public sealed class ModDiscoveryFinished : EventWrapper ModFileSystem = 0, } - - public ModDiscoveryFinished() - : base(nameof(ModDiscoveryFinished)) - { } - - public void Invoke() - => Invoke(this); } diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs index cf45528d..5cafd1ea 100644 --- a/Penumbra/Communication/ModDiscoveryStarted.cs +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -3,7 +3,7 @@ using OtterGui.Classes; namespace Penumbra.Communication; /// Triggered whenever mods are prepared to be rediscovered. -public sealed class ModDiscoveryStarted : EventWrapper +public sealed class ModDiscoveryStarted() : EventWrapper(nameof(ModDiscoveryStarted)) { public enum Priority { @@ -16,11 +16,4 @@ public sealed class ModDiscoveryStarted : EventWrapper ModFileSystemSelector = 200, } - - public ModDiscoveryStarted() - : base(nameof(ModDiscoveryStarted)) - { } - - public void Invoke() - => Invoke(this); } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index 416cc8df..a0b4d26c 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -13,7 +13,8 @@ namespace Penumbra.Communication; /// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. /// Parameter is the index of the group an option was moved to. /// -public sealed class ModOptionChanged : EventWrapper, ModOptionChanged.Priority> +public sealed class ModOptionChanged() + : EventWrapper(nameof(ModOptionChanged)) { public enum Priority { @@ -29,11 +30,4 @@ public sealed class ModOptionChanged : EventWrapper CollectionStorage = 100, } - - public ModOptionChanged() - : base(nameof(ModOptionChanged)) - { } - - public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) - => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); } diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 83c3b5a5..3ec64f7e 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -14,7 +14,8 @@ namespace Penumbra.Communication; /// Parameter is the new directory on addition, move or reload and null on deletion. /// /// -public sealed class ModPathChanged : EventWrapper, ModPathChanged.Priority> +public sealed class ModPathChanged() + : EventWrapper(nameof(ModPathChanged)) { public enum Priority { @@ -48,11 +49,4 @@ public sealed class ModPathChanged : EventWrapper CollectionCacheManagerRemoval = 100, } - - public ModPathChanged() - : base(nameof(ModPathChanged)) - { } - - public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) - => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 65bcf3ed..5e0bc0c0 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -17,7 +17,8 @@ namespace Penumbra.Communication; /// Parameter is whether the change was inherited from another collection. /// /// -public sealed class ModSettingChanged : EventWrapper, ModSettingChanged.Priority> +public sealed class ModSettingChanged() + : EventWrapper(nameof(ModSettingChanged)) { public enum Priority { @@ -33,11 +34,4 @@ public sealed class ModSettingChanged : EventWrapper ModFileSystemSelector = 0, } - - public ModSettingChanged() - : base(nameof(ModSettingChanged)) - { } - - public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) - => Invoke(this, collection, type, mod, oldValue, groupIdx, inherited); } diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 868692cd..bd560fd8 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -6,18 +6,11 @@ namespace Penumbra.Communication; /// Parameter is the material resource handle for which the shader package has been loaded. /// Parameter is the associated game object. /// -public sealed class MtrlShpkLoaded : EventWrapper, MtrlShpkLoaded.Priority> +public sealed class MtrlShpkLoaded() : EventWrapper(nameof(MtrlShpkLoaded)) { public enum Priority { /// SkinFixer = 0, } - - public MtrlShpkLoaded() - : base(nameof(MtrlShpkLoaded)) - { } - - public void Invoke(nint mtrlResourceHandle, nint gameObject) - => Invoke(this, mtrlResourceHandle, gameObject); } diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index b32b0dfa..a918b610 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -8,18 +8,11 @@ namespace Penumbra.Communication; /// Parameter is the identifier (directory name) of the currently selected mod. /// /// -public sealed class PostSettingsPanelDraw : EventWrapper, PostSettingsPanelDraw.Priority> +public sealed class PostSettingsPanelDraw() : EventWrapper(nameof(PostSettingsPanelDraw)) { public enum Priority { /// Default = 0, } - - public PostSettingsPanelDraw() - : base(nameof(PostSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); } diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index e5857474..cda00d78 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -8,18 +8,11 @@ namespace Penumbra.Communication; /// Parameter is the identifier (directory name) of the currently selected mod. /// /// -public sealed class PreSettingsPanelDraw : EventWrapper, PreSettingsPanelDraw.Priority> +public sealed class PreSettingsPanelDraw() : EventWrapper(nameof(PreSettingsPanelDraw)) { public enum Priority { /// Default = 0, } - - public PreSettingsPanelDraw() - : base(nameof(PreSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); } diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 55e95320..3211a26a 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -15,8 +15,9 @@ namespace Penumbra.Communication; /// Parameter is the old redirection path for Replaced, or empty. /// Parameter is the mod responsible for the new redirection if any. /// -public sealed class ResolvedFileChanged : EventWrapper, - ResolvedFileChanged.Priority> +public sealed class ResolvedFileChanged() + : EventWrapper( + nameof(ResolvedFileChanged)) { public enum Type { @@ -29,14 +30,7 @@ public sealed class ResolvedFileChanged : EventWrapper + /// DalamudSubstitutionProvider = 0, } - - public ResolvedFileChanged() - : base(nameof(ResolvedFileChanged)) - { } - - public void Invoke(ModCollection collection, Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod) - => Invoke(this, collection, type, key, value, old, mod); } diff --git a/Penumbra/Communication/SelectTab.cs b/Penumbra/Communication/SelectTab.cs index aaa362f6..cb7e2e56 100644 --- a/Penumbra/Communication/SelectTab.cs +++ b/Penumbra/Communication/SelectTab.cs @@ -11,18 +11,11 @@ namespace Penumbra.Communication; /// Parameter is the selected mod, if any. /// /// -public sealed class SelectTab : EventWrapper, SelectTab.Priority> +public sealed class SelectTab() : EventWrapper(nameof(SelectTab)) { public enum Priority { /// ConfigTabBar = 0, } - - public SelectTab() - : base(nameof(SelectTab)) - { } - - public void Invoke(TabType tab = TabType.None, Mod? mod = null) - => Invoke(this, tab, mod); } diff --git a/Penumbra/Communication/TemporaryGlobalModChange.cs b/Penumbra/Communication/TemporaryGlobalModChange.cs index 12d42e48..6edf26d7 100644 --- a/Penumbra/Communication/TemporaryGlobalModChange.cs +++ b/Penumbra/Communication/TemporaryGlobalModChange.cs @@ -10,7 +10,8 @@ namespace Penumbra.Communication; /// Parameter is whether the mod was newly created. /// Parameter is whether the mod was deleted. /// -public sealed class TemporaryGlobalModChange : EventWrapper, TemporaryGlobalModChange.Priority> +public sealed class TemporaryGlobalModChange() + : EventWrapper(nameof(TemporaryGlobalModChange)) { public enum Priority { @@ -20,11 +21,4 @@ public sealed class TemporaryGlobalModChange : EventWrapper TempCollectionManager = 0, } - - public TemporaryGlobalModChange() - : base(nameof(TemporaryGlobalModChange)) - { } - - public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) - => Invoke(this, temporaryMod, newlyCreated, deleted); } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs new file mode 100644 index 00000000..2552f1a7 --- /dev/null +++ b/Penumbra/Interop/GameState.cs @@ -0,0 +1,117 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String.Classes; + +namespace Penumbra.Interop; + +public class GameState : IService +{ + #region Last Game Object + + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + + public nint LastGameObject + => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public unsafe void QueueGameObject(GameObject* gameObject) + => QueueGameObject((nint)gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void QueueGameObject(nint gameObject) + => _lastGameObject.Value!.Enqueue(gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void DequeueGameObject() + => _lastGameObject.Value!.TryDequeue(out _); + + #endregion + + #region Animation Data + + private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); + + public ResolveData AnimationData + => _animationLoadData.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ResolveData SetAnimationData(ResolveData data) + { + var old = _animationLoadData.Value; + _animationLoadData.Value = data; + return old; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void RestoreAnimationData(ResolveData old) + => _animationLoadData.Value = old; + + #endregion + + #region Sound Data + + private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); + + public ResolveData SoundData + => _animationLoadData.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ResolveData SetSoundData(ResolveData data) + { + var old = _characterSoundData.Value; + _characterSoundData.Value = data; + return old; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void RestoreSoundData(ResolveData old) + => _characterSoundData.Value = old; + + #endregion + + /// Return the correct resolve data from the stored data. + public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData) + { + switch (type) + { + case ResourceType.Scd: + if (_characterSoundData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _characterSoundData.Value; + return true; + } + + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Avfx: + case ResourceType.Atex: + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + } + + var lastObj = LastGameObject; + if (lastObj != nint.Zero) + { + resolveData = resolver.IdentifyCollection((GameObject*)lastObj, true); + return true; + } + + resolveData = ResolveData.Invalid; + return false; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs new file mode 100644 index 00000000..b91c5375 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -0,0 +1,54 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some sound effects caused by animations or VFX. +public sealed unsafe class ApricotListenerSoundPlay : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); + } + + public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + { + Penumbra.Log.Excessive($"[Apricot Listener Sound Play] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}, {a5}, {a6}."); + if (a6 == nint.Zero) + return Task.Result.Original(a1, a2, a3, a4, a5, a6); + + // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. + var gameObject = (*(delegate* unmanaged**)a6)[1](a6); + var newData = ResolveData.Invalid; + if (gameObject != null) + { + newData = _collectionResolver.IdentifyCollection(gameObject, true); + } + else + { + // for VfxListenner we can obtain the associated draw object as its first member, + // if the object has different type, drawObject will contain other values or garbage, + // but only be used in a dictionary pointer lookup, so this does not hurt. + var drawObject = ((DrawObject**)a6)[1]; + if (drawObject != null) + newData = _collectionResolver.IdentifyCollection(drawObject, true); + } + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6); + _state.RestoreAnimationData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs new file mode 100644 index 00000000..959165a6 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -0,0 +1,41 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// +/// Probably used when the base idle animation gets loaded. +/// Make it aware of the correct collection to load the correct pap files. +/// +public sealed unsafe class CharacterBaseLoadAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + + public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver, + DrawObjectState drawObjectState) + { + _state = state; + _collectionResolver = collectionResolver; + _drawObjectState = drawObjectState; + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawBase); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + var lastObj = _state.LastGameObject; + if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p)) + lastObj = p.Item1; + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)lastObj, true)); + Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}"); + Task.Result.Original(drawObject); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs new file mode 100644 index 00000000..8085bcdb --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -0,0 +1,44 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when dismounting. +public sealed unsafe class Dismount : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public Dismount(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, true); + } + + public delegate void Delegate(nint a1, nint a2); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(nint a1, nint a2) + { + Penumbra.Log.Excessive($"[Dismount] Invoked on {a1:X} with {a2:X}."); + if (a1 == nint.Zero) + { + Task.Result.Original(a1, a2); + return; + } + + var gameObject = *(GameObject**)(a1 + 8); + if (gameObject == null) + { + Task.Result.Original(a1, a2); + return; + } + + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + Task.Result.Original(a1, a2); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs new file mode 100644 index 00000000..7be420be --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -0,0 +1,38 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a ground-based area VFX. +public sealed unsafe class LoadAreaVfx : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); + } + + public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) + { + var newData = caster != null + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); + Penumbra.Log.Excessive( + $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); + _state.RestoreAnimationData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs new file mode 100644 index 00000000..af13805d --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Characters load some of their voice lines or whatever with this function. +public sealed unsafe class LoadCharacterSound : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Load Character Sound", + (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, + true); + } + + public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) + { + var character = *(GameObject**)(container + 8); + var last = _state.SetSoundData(_collectionResolver.IdentifyCollection(character, true)); + var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); + Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); + _state.RestoreSoundData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs new file mode 100644 index 00000000..240c062e --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -0,0 +1,65 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a VFX specifically for a character. +public sealed unsafe class LoadCharacterVfx : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); + } + + public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) + { + var newData = ResolveData.Invalid; + if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1)) + { + var obj = vfxParams->GameObjectType switch + { + 0 => _objects.SearchById(vfxParams->GameObjectId), + 2 => _objects[(int)vfxParams->GameObjectId], + 4 => GetOwnedObject(vfxParams->GameObjectId), + _ => null, + }; + newData = obj != null + ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) + : ResolveData.Invalid; + } + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); + Penumbra.Log.Excessive( + $"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); + _state.RestoreAnimationData(last); + return ret; + } + + /// Search an object by its id, then get its minion/mount/ornament. + private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) + { + var owner = _objects.SearchById(id); + if (owner == null) + return null; + + var idx = ((GameObject*)owner.Address)->ObjectIndex; + return _objects[idx + 1]; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs new file mode 100644 index 00000000..2ca8ffe7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -0,0 +1,71 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// +/// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. +/// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. +/// +public sealed unsafe class LoadTimelineResources : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly ICondition _conditions; + private readonly IObjectTable _objects; + + public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions, + IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _conditions = conditions; + _objects = objects; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + } + + public delegate ulong Delegate(nint timeline); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private ulong Detour(nint timeline) + { + Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {timeline:X}."); + // Do not check timeline loading in cutscenes. + if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) + return Task.Result.Original(timeline); + + var last = _state.SetAnimationData(GetDataFromTimeline(_objects, _collectionResolver, timeline)); + var ret = Task.Result.Original(timeline); + _state.RestoreAnimationData(last); + return ret; + } + + /// Use timelines vfuncs to obtain the associated game object. + public static ResolveData GetDataFromTimeline(IObjectTable objects, CollectionResolver resolver, nint timeline) + { + try + { + if (timeline != nint.Zero) + { + var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; + var idx = getGameObjectIdx(timeline); + if (idx >= 0 && idx < objects.Length) + { + var obj = (GameObject*)objects.GetObjectAddress(idx); + return obj != null ? resolver.IdentifyCollection(obj, true) : ResolveData.Invalid; + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}"); + } + + return ResolveData.Invalid; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs new file mode 100644 index 00000000..491d7662 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -0,0 +1,30 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +public sealed unsafe class PlayFootstep : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, true); + } + + public delegate void Delegate(GameObject* gameObject, int id, int unk); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(GameObject* gameObject, int id, int unk) + { + Penumbra.Log.Excessive($"[Play Footstep] Invoked on 0x{(nint)gameObject:X} with {id}, {unk}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + Task.Result.Original(gameObject, id, unk); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs new file mode 100644 index 00000000..8428f8ff --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -0,0 +1,35 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called when some action timelines update. +public sealed unsafe class ScheduleClipUpdate : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); + } + + public delegate void Delegate(ClipScheduler* x); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(ClipScheduler* clipScheduler) + { + Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}."); + var last = _state.SetAnimationData( + LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline)); + Task.Result.Original(clipScheduler); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs new file mode 100644 index 00000000..48931d73 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Seems to load character actions when zoning or changing class, maybe. +public sealed unsafe class SomeActionLoad : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, true); + } + + public delegate void Delegate(ActionTimelineManager* timelineManager); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(ActionTimelineManager* timelineManager) + { + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true)); + Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}."); + Task.Result.Original(timelineManager); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs new file mode 100644 index 00000000..5dd8227d --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when mounted or mounting. +public sealed unsafe class SomeMountAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeMountAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) + { + Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}, {unk2}, {unk3}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true)); + Task.Result.Original(drawObject, unk1, unk2, unk3); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs new file mode 100644 index 00000000..75caacee --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -0,0 +1,46 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Unknown what exactly this is, but it seems to load a bunch of paps. +public sealed unsafe class SomePapLoad : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, true); + } + + public delegate void Delegate(nint a1, int a2, nint a3, int a4); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(nint a1, int a2, nint a3, int a4) + { + Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}."); + var timelinePtr = a1 + Offsets.TimeLinePtr; + if (timelinePtr != nint.Zero) + { + var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); + if (actorIdx >= 0 && actorIdx < _objects.Length) + { + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), + true)); + Task.Result.Original(a1, a2, a3, a4); + _state.RestoreAnimationData(last); + return; + } + } + + Task.Result.Original(a1, a2, a3, a4); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs new file mode 100644 index 00000000..ab4a7201 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when using a Parasol. +public sealed unsafe class SomeParasolAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeParasolAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, int unk1); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, int unk1) + { + Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true)); + Task.Result.Original(drawObject, unk1); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs new file mode 100644 index 00000000..435ddea6 --- /dev/null +++ b/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs @@ -0,0 +1,49 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.UI.AdvancedWindow; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + DrawObjectState = 0, + + /// + MtrlTab = -1000, + } + + public CharacterBaseDestructor(HookManager hooks) + : base("Destroy CharacterBase") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Destroy; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate nint Delegate(CharacterBase* characterBase); + + private nint Detour(CharacterBase* characterBase) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}."); + Invoke(characterBase); + return _task.Result.Original(characterBase); + } +} diff --git a/Penumbra/Interop/Hooks/CharacterDestructor.cs b/Penumbra/Interop/Hooks/CharacterDestructor.cs new file mode 100644 index 00000000..4a0e9367 --- /dev/null +++ b/Penumbra/Interop/Hooks/CharacterDestructor.cs @@ -0,0 +1,49 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CharacterDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + CutsceneService = 0, + + /// + IdentifiedCollectionCache = 0, + } + + public CharacterDestructor(HookManager hooks) + : base("Character Destructor") + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, true); + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(Character* character); + + private void Detour(Character* character) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}."); + Invoke(character); + _task.Result.Original(character); + } +} diff --git a/Penumbra/Interop/Hooks/CopyCharacter.cs b/Penumbra/Interop/Hooks/CopyCharacter.cs new file mode 100644 index 00000000..d2e8d816 --- /dev/null +++ b/Penumbra/Interop/Hooks/CopyCharacter.cs @@ -0,0 +1,47 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CopyCharacter : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + CutsceneService = 0, + } + + public CopyCharacter(HookManager hooks) + : base("Copy Character") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk); + + private ulong Detour(CharacterSetup* target, Character* source, uint unk) + { + // TODO: update when CS updated. + var character = ((Character**)target)[1]; + Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); + Invoke(character, source); + return _task.Result.Original(target, source, unk); + } +} diff --git a/Penumbra/Interop/Hooks/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/CreateCharacterBase.cs new file mode 100644 index 00000000..7dbde666 --- /dev/null +++ b/Penumbra/Interop/Hooks/CreateCharacterBase.cs @@ -0,0 +1,74 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CreateCharacterBase : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + MetaState = 0, + } + + public CreateCharacterBase(HookManager hooks) + : base("Create CharacterBase") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Create; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate CharacterBase* Delegate(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk); + + private CharacterBase* Detour(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with model: {model.Id}, customize: 0x{(nint) customize:X}, equipment: 0x{(nint)equipment:X}, unk: {unk}."); + Invoke(&model, customize, equipment); + var ret = _task.Result.Original(model, customize, equipment, unk); + _postEvent.Invoke(model, customize, equipment, ret); + return ret; + } + + public void Subscribe(ActionPtr234 subscriber, PostEvent.Priority priority) + => _postEvent.Subscribe(subscriber, priority); + + public void Unsubscribe(ActionPtr234 subscriber) + => _postEvent.Unsubscribe(subscriber); + + + private readonly PostEvent _postEvent = new("Created CharacterBase"); + + protected override void Dispose(bool disposing) + { + _postEvent.Dispose(); + } + + public class PostEvent(string name) : EventWrapperPtr234(name) + { + public enum Priority + { + /// + DrawObjectState = 0, + + /// + MetaState = 0, + } + } +} diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs new file mode 100644 index 00000000..67823e94 --- /dev/null +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -0,0 +1,43 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks; + +#if DEBUG +public sealed unsafe class DebugHook : IHookService +{ + public const string Signature = ""; + + public DebugHook(HookManager hooks) + { + if (Signature.Length > 0) + _task = hooks.CreateHook("Debug Hook", Signature, Detour, true); + } + + private readonly Task>? _task; + + public nint Address + => _task?.Result.Address ?? nint.Zero; + + public void Enable() + => _task?.Result.Enable(); + + public void Disable() + => _task?.Result.Disable(); + + public Task Awaiter + => _task ?? Task.CompletedTask; + + public bool Finished + => _task?.IsCompletedSuccessfully ?? true; + + private delegate nint Delegate(ResourceHandle* resourceHandle); + + private nint Detour(ResourceHandle* resourceHandle) + { + Penumbra.Log.Information($"[Debug Hook] Triggered with 0x{(nint)resourceHandle:X}."); + return _task!.Result.Original(resourceHandle); + } +} +#endif diff --git a/Penumbra/Interop/Hooks/EnableDraw.cs b/Penumbra/Interop/Hooks/EnableDraw.cs new file mode 100644 index 00000000..884b643d --- /dev/null +++ b/Penumbra/Interop/Hooks/EnableDraw.cs @@ -0,0 +1,48 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks; + +/// +/// EnableDraw is what creates DrawObjects for gameObjects, +/// so we always keep track of the current GameObject to be able to link it to the DrawObject. +/// +public sealed unsafe class EnableDraw : IHookService +{ + private readonly Task> _task; + private readonly GameState _state; + + public EnableDraw(HookManager hooks, GameState state) + { + _state = state; + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); + } + + private delegate void Delegate(GameObject* gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(GameObject* gameObject) + { + _state.QueueGameObject(gameObject); + Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint) gameObject:X}."); + _task.Result.Original.Invoke(gameObject); + _state.DequeueGameObject(); + } + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); +} diff --git a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs new file mode 100644 index 00000000..99eb1c23 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs @@ -0,0 +1,50 @@ +using Dalamud.Hooking; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + SubfileHelper, + + /// + SkinFixer, + } + + public ResourceHandleDestructor(HookManager hooks) + : base("Destroy ResourceHandle") + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, true); + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate nint Delegate(ResourceHandle* resourceHandle); + + private nint Detour(ResourceHandle* resourceHandle) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)resourceHandle:X}."); + Invoke(resourceHandle); + return _task.Result.Original(resourceHandle); + } +} diff --git a/Penumbra/Interop/Hooks/WeaponReload.cs b/Penumbra/Interop/Hooks/WeaponReload.cs new file mode 100644 index 00000000..b931f8fb --- /dev/null +++ b/Penumbra/Interop/Hooks/WeaponReload.cs @@ -0,0 +1,71 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class WeaponReload : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + DrawObjectState = 0, + } + + public WeaponReload(HookManager hooks) + : base("Reload Weapon") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g); + + private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g) + { + var gameObject = drawData->Parent; + Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); + Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); + _task.Result.Original(drawData, slot, weapon, d, e, f, g); + _postEvent.Invoke(drawData, gameObject); + } + + public void Subscribe(ActionPtr subscriber, PostEvent.Priority priority) + => _postEvent.Subscribe(subscriber, priority); + + public void Unsubscribe(ActionPtr subscriber) + => _postEvent.Unsubscribe(subscriber); + + + private readonly PostEvent _postEvent = new("Created CharacterBase"); + + protected override void Dispose(bool disposing) + { + _postEvent.Dispose(); + } + + public class PostEvent(string name) : EventWrapperPtr(name) + { + public enum Priority + { + /// + DrawObjectState = 0, + } + } +} diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs deleted file mode 100644 index d13ef7f2..00000000 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ /dev/null @@ -1,423 +0,0 @@ -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Collections; -using Penumbra.Api.Enums; -using Penumbra.GameData; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; - -namespace Penumbra.Interop.PathResolving; - -public unsafe class AnimationHookService : IDisposable -{ - private readonly PerformanceTracker _performance; - private readonly IObjectTable _objects; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; - private readonly CollectionResolver _resolver; - private readonly ICondition _conditions; - - private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); - private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); - - public AnimationHookService(PerformanceTracker performance, IObjectTable objects, CollectionResolver collectionResolver, - DrawObjectState drawObjectState, CollectionResolver resolver, ICondition conditions, IGameInteropProvider interop) - { - _performance = performance; - _objects = objects; - _collectionResolver = collectionResolver; - _drawObjectState = drawObjectState; - _resolver = resolver; - _conditions = conditions; - - interop.InitializeFromAttributes(this); - _loadCharacterSoundHook = - interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, - LoadCharacterSoundDetour); - - _loadCharacterSoundHook.Enable(); - _loadTimelineResourcesHook.Enable(); - _characterBaseLoadAnimationHook.Enable(); - _loadSomePapHook.Enable(); - _someActionLoadHook.Enable(); - _loadCharacterVfxHook.Enable(); - _loadAreaVfxHook.Enable(); - _scheduleClipUpdateHook.Enable(); - _unkMountAnimationHook.Enable(); - _unkParasolAnimationHook.Enable(); - _dismountHook.Enable(); - _apricotListenerSoundPlayHook.Enable(); - _footStepHook.Enable(); - } - - public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) - { - switch (type) - { - case ResourceType.Scd: - if (_characterSoundData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _characterSoundData.Value; - return true; - } - - if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _animationLoadData.Value; - return true; - } - - break; - case ResourceType.Tmb: - case ResourceType.Pap: - case ResourceType.Avfx: - case ResourceType.Atex: - if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _animationLoadData.Value; - return true; - } - - break; - } - - var lastObj = _drawObjectState.LastGameObject; - if (lastObj != nint.Zero) - { - resolveData = _resolver.IdentifyCollection((GameObject*)lastObj, true); - return true; - } - - resolveData = ResolveData.Invalid; - return false; - } - - public void Dispose() - { - _loadCharacterSoundHook.Dispose(); - _loadTimelineResourcesHook.Dispose(); - _characterBaseLoadAnimationHook.Dispose(); - _loadSomePapHook.Dispose(); - _someActionLoadHook.Dispose(); - _loadCharacterVfxHook.Dispose(); - _loadAreaVfxHook.Dispose(); - _scheduleClipUpdateHook.Dispose(); - _unkMountAnimationHook.Dispose(); - _unkParasolAnimationHook.Dispose(); - _dismountHook.Dispose(); - _apricotListenerSoundPlayHook.Dispose(); - _footStepHook.Dispose(); - } - - /// Characters load some of their voice lines or whatever with this function. - private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); - - private readonly Hook _loadCharacterSoundHook; - - private nint LoadCharacterSoundDetour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) - { - using var performance = _performance.Measure(PerformanceType.LoadSound); - var last = _characterSoundData.Value; - var character = *(GameObject**)(container + 8); - _characterSoundData.Value = _collectionResolver.IdentifyCollection(character, true); - var ret = _loadCharacterSoundHook.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - _characterSoundData.Value = last; - return ret; - } - - /// - /// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files. - /// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection. - /// - private delegate ulong LoadTimelineResourcesDelegate(nint timeline); - - [Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))] - private readonly Hook _loadTimelineResourcesHook = null!; - - private ulong LoadTimelineResourcesDetour(nint timeline) - { - using var performance = _performance.Measure(PerformanceType.TimelineResources); - // Do not check timeline loading in cutscenes. - if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) - return _loadTimelineResourcesHook.Original(timeline); - - var last = _animationLoadData.Value; - _animationLoadData.Value = GetDataFromTimeline(timeline); - var ret = _loadTimelineResourcesHook.Original(timeline); - _animationLoadData.Value = last; - return ret; - } - - /// - /// Probably used when the base idle animation gets loaded. - /// Make it aware of the correct collection to load the correct pap files. - /// - private delegate void CharacterBaseNoArgumentDelegate(nint drawBase); - - [Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))] - private readonly Hook _characterBaseLoadAnimationHook = null!; - - private void CharacterBaseLoadAnimationDetour(nint drawObject) - { - using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation); - var last = _animationLoadData.Value; - var lastObj = _drawObjectState.LastGameObject; - if (lastObj == nint.Zero && _drawObjectState.TryGetValue(drawObject, out var p)) - lastObj = p.Item1; - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true); - _characterBaseLoadAnimationHook.Original(drawObject); - _animationLoadData.Value = last; - } - - /// Unknown what exactly this is but it seems to load a bunch of paps. - private delegate void LoadSomePap(nint a1, int a2, nint a3, int a4); - - [Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))] - private readonly Hook _loadSomePapHook = null!; - - private void LoadSomePapDetour(nint a1, int a2, nint a3, int a4) - { - using var performance = _performance.Measure(PerformanceType.LoadPap); - var timelinePtr = a1 + Offsets.TimeLinePtr; - var last = _animationLoadData.Value; - if (timelinePtr != nint.Zero) - { - var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); - if (actorIdx >= 0 && actorIdx < _objects.Length) - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true); - } - - _loadSomePapHook.Original(a1, a2, a3, a4); - _animationLoadData.Value = last; - } - - private delegate void SomeActionLoadDelegate(ActionTimelineManager* timelineManager); - - /// Seems to load character actions when zoning or changing class, maybe. - [Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))] - private readonly Hook _someActionLoadHook = null!; - - private void SomeActionLoadDetour(ActionTimelineManager* timelineManager) - { - using var performance = _performance.Measure(PerformanceType.LoadAction); - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); - _someActionLoadHook.Original(timelineManager); - _animationLoadData.Value = last; - } - - /// Load a VFX specifically for a character. - private delegate nint LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); - - [Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))] - private readonly Hook _loadCharacterVfxHook = null!; - - private nint LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) - { - using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx); - var last = _animationLoadData.Value; - if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1)) - { - var obj = vfxParams->GameObjectType switch - { - 0 => _objects.SearchById(vfxParams->GameObjectId), - 2 => _objects[(int)vfxParams->GameObjectId], - 4 => GetOwnedObject(vfxParams->GameObjectId), - _ => null, - }; - _animationLoadData.Value = obj != null - ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) - : ResolveData.Invalid; - } - else - { - _animationLoadData.Value = ResolveData.Invalid; - } - - var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); - Penumbra.Log.Excessive( - $"Load Character VFX: {new ByteString(vfxPath)} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> " - + $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); - _animationLoadData.Value = last; - return ret; - } - - /// Load a ground-based area VFX. - private delegate nint LoadAreaVfxDelegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); - - [Signature(Sigs.LoadAreaVfx, DetourName = nameof(LoadAreaVfxDetour))] - private readonly Hook _loadAreaVfxHook = null!; - - private nint LoadAreaVfxDetour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) - { - using var performance = _performance.Measure(PerformanceType.LoadAreaVfx); - var last = _animationLoadData.Value; - _animationLoadData.Value = caster != null - ? _collectionResolver.IdentifyCollection(caster, true) - : ResolveData.Invalid; - - var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Excessive( - $"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}" - + $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); - _animationLoadData.Value = last; - return ret; - } - - - /// Called when some action timelines update. - private delegate void ScheduleClipUpdate(ClipScheduler* x); - - [Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))] - private readonly Hook _scheduleClipUpdateHook = null!; - - private void ScheduleClipUpdateDetour(ClipScheduler* x) - { - using var performance = _performance.Measure(PerformanceType.ScheduleClipUpdate); - var last = _animationLoadData.Value; - var timeline = x->SchedulerTimeline; - _animationLoadData.Value = GetDataFromTimeline(timeline); - _scheduleClipUpdateHook.Original(x); - _animationLoadData.Value = last; - } - - /// Search an object by its id, then get its minion/mount/ornament. - private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) - { - var owner = _objects.SearchById(id); - if (owner == null) - return null; - - var idx = ((GameObject*)owner.Address)->ObjectIndex; - return _objects[idx + 1]; - } - - /// Use timelines vfuncs to obtain the associated game object. - private ResolveData GetDataFromTimeline(nint timeline) - { - try - { - if (timeline != nint.Zero) - { - var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; - var idx = getGameObjectIdx(timeline); - if (idx >= 0 && idx < _objects.Length) - { - var obj = (GameObject*)_objects.GetObjectAddress(idx); - return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid; - } - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}"); - } - - return ResolveData.Invalid; - } - - private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); - - [Signature(Sigs.UnkMountAnimation, DetourName = nameof(UnkMountAnimationDetour))] - private readonly Hook _unkMountAnimationHook = null!; - - private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - _unkMountAnimationHook.Original(drawObject, unk1, unk2, unk3); - _animationLoadData.Value = last; - } - - private delegate void UnkParasolAnimationDelegate(DrawObject* drawObject, int unk1); - - [Signature(Sigs.UnkParasolAnimation, DetourName = nameof(UnkParasolAnimationDetour))] - private readonly Hook _unkParasolAnimationHook = null!; - - private void UnkParasolAnimationDetour(DrawObject* drawObject, int unk1) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - _unkParasolAnimationHook.Original(drawObject, unk1); - _animationLoadData.Value = last; - } - - [Signature(Sigs.Dismount, DetourName = nameof(DismountDetour))] - private readonly Hook _dismountHook = null!; - - private delegate void DismountDelegate(nint a1, nint a2); - - private void DismountDetour(nint a1, nint a2) - { - if (a1 == nint.Zero) - { - _dismountHook.Original(a1, a2); - return; - } - - var gameObject = *(GameObject**)(a1 + 8); - if (gameObject == null) - { - _dismountHook.Original(a1, a2); - return; - } - - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - _dismountHook.Original(a1, a2); - _animationLoadData.Value = last; - } - - [Signature(Sigs.ApricotListenerSoundPlay, DetourName = nameof(ApricotListenerSoundPlayDetour))] - private readonly Hook _apricotListenerSoundPlayHook = null!; - - private delegate nint ApricotListenerSoundPlayDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); - - private nint ApricotListenerSoundPlayDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) - { - if (a6 == nint.Zero) - return _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - - var last = _animationLoadData.Value; - // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. - var gameObject = (*(delegate* unmanaged**)a6)[1](a6); - if (gameObject != null) - { - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - } - else - { - // for VfxListenner we can obtain the associated draw object as its first member, - // if the object has different type, drawObject will contain other values or garbage, - // but only be used in a dictionary pointer lookup, so this does not hurt. - var drawObject = ((DrawObject**)a6)[1]; - if (drawObject != null) - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - } - - var ret = _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - _animationLoadData.Value = last; - return ret; - } - - private delegate void FootStepDelegate(GameObject* gameObject, int id, int unk); - - [Signature(Sigs.FootStepSound, DetourName = nameof(FootStepDetour))] - private readonly Hook _footStepHook = null!; - - private void FootStepDetour(GameObject* gameObject, int id, int unk) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - _footStepHook.Original(gameObject, id, unk); - _animationLoadData.Value = last; - } -} diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index fe51a5cd..c649147a 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,11 +1,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; -using Penumbra.Services; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -13,70 +13,51 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.PathResolving; -public unsafe class CollectionResolver +public sealed unsafe class CollectionResolver( + PerformanceTracker performance, + IdentifiedCollectionCache cache, + IClientState clientState, + IGameGui gameGui, + ActorManager actors, + CutsceneService cutscenes, + Configuration config, + CollectionManager collectionManager, + TempCollectionManager tempCollections, + DrawObjectState drawObjectState, + HumanModelList humanModels) + : IService { - private readonly PerformanceTracker _performance; - private readonly IdentifiedCollectionCache _cache; - private readonly HumanModelList _humanModels; - - private readonly IClientState _clientState; - private readonly IGameGui _gameGui; - private readonly ActorManager _actors; - private readonly CutsceneService _cutscenes; - - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TempCollectionManager _tempCollections; - private readonly DrawObjectState _drawObjectState; - - public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, IGameGui gameGui, - ActorManager actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels) - { - _performance = performance; - _cache = cache; - _clientState = clientState; - _gameGui = gameGui; - _actors = actors; - _cutscenes = cutscenes; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _drawObjectState = drawObjectState; - _humanModels = humanModels; - } - /// /// Get the collection applying to the current player character /// or the Yourself or Default collection if no player exists. /// public ModCollection PlayerCollection() { - using var performance = _performance.Measure(PerformanceType.IdentifyCollection); - var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero); + using var performance1 = performance.Measure(PerformanceType.IdentifyCollection); + var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero); if (gameObject == null) - return _collectionManager.Active.ByType(CollectionType.Yourself) - ?? _collectionManager.Active.Default; + return collectionManager.Active.ByType(CollectionType.Yourself) + ?? collectionManager.Active.Default; - var player = _actors.GetCurrentPlayer(); + var player = actors.GetCurrentPlayer(); var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) ?? CollectionByAttributes(gameObject, ref _) - ?? _collectionManager.Active.Default; + ?? collectionManager.Active.Default; } /// Identify the correct collection for a game object. public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache) { - using var t = _performance.Measure(PerformanceType.IdentifyCollection); + using var t = performance.Measure(PerformanceType.IdentifyCollection); if (gameObject == null) - return _collectionManager.Active.Default.ToResolveData(); + return collectionManager.Active.Default.ToResolveData(); try { - if (useCache && _cache.TryGetValue(gameObject, out var data)) + if (useCache && cache.TryGetValue(gameObject, out var data)) return data; if (LoginScreen(gameObject, out data)) @@ -90,26 +71,26 @@ public unsafe class CollectionResolver catch (Exception ex) { Penumbra.Log.Error($"Error identifying collection:\n{ex}"); - return _collectionManager.Active.Default.ToResolveData(gameObject); + return collectionManager.Active.Default.ToResolveData(gameObject); } } /// Identify the correct collection for the last created game object. public ResolveData IdentifyLastGameObjectCollection(bool useCache) - => IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache); + => IdentifyCollection((GameObject*)drawObjectState.LastGameObject, useCache); /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) { - var obj = (GameObject*)(_drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject) ? gameObject.Item1 - : _drawObjectState.LastGameObject); + : drawObjectState.LastGameObject); return IdentifyCollection(obj, useCache); } /// Return whether the given ModelChara id refers to a human-type model. public bool IsModelHuman(uint modelCharaId) - => _humanModels.IsHuman(modelCharaId); + => humanModels.IsHuman(modelCharaId); /// Return whether the given character has a human model. public bool IsModelHuman(Character* character) @@ -124,36 +105,36 @@ public unsafe class CollectionResolver { // Also check for empty names because sometimes named other characters // might be loaded before being officially logged in. - if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0') + if (clientState.IsLoggedIn || gameObject->Name[0] != '\0') { ret = ResolveData.Invalid; return false; } var notYetReady = false; - var collection = _collectionManager.Active.ByType(CollectionType.Yourself) + var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ?? collectionManager.Active.Default; + ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } /// Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. private bool Aesthetician(GameObject* gameObject, out ResolveData ret) { - if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) + if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) { ret = ResolveData.Invalid; return false; } - var player = _actors.GetCurrentPlayer(); + var player = actors.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) - ?? _collectionManager.Active.ByType(CollectionType.Yourself) + ?? collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ?? collectionManager.Active.Default; + ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -163,12 +144,12 @@ public unsafe class CollectionResolver /// private ResolveData DefaultState(GameObject* gameObject) { - var identifier = _actors.FromObject(gameObject, out var owner, true, false, false); + var identifier = actors.FromObject(gameObject, out var owner, true, false, false); if (identifier.Type is IdentifierType.Special) { - (identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); - if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) - return _cache.Set(ModCollection.Empty, identifier, gameObject); + (identifier, var type) = collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); + if (config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) + return cache.Set(ModCollection.Empty, identifier, gameObject); } var notYetReady = false; @@ -176,15 +157,15 @@ public unsafe class CollectionResolver ?? CheckYourself(identifier, gameObject) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? CheckOwnedCollection(identifier, owner, ref notYetReady) - ?? _collectionManager.Active.Default; + ?? collectionManager.Active.Default; - return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject); + return notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, identifier, gameObject); } /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => _tempCollections.Collections.TryGetCollection(identifier, out var collection) - || _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) + => tempCollections.Collections.TryGetCollection(identifier, out var collection) + || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) ? collection : null; @@ -192,9 +173,9 @@ public unsafe class CollectionResolver private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor) { if (actor->ObjectIndex == 0 - || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 - || identifier.Equals(_actors.GetCurrentPlayer())) - return _collectionManager.Active.ByType(CollectionType.Yourself); + || cutscenes.GetParentIndex(actor->ObjectIndex) == 0 + || identifier.Equals(actors.GetCurrentPlayer())) + return collectionManager.Active.ByType(CollectionType.Yourself); return null; } @@ -219,8 +200,8 @@ public unsafe class CollectionResolver var bodyType = character->DrawData.CustomizeData[2]; var collection = bodyType switch { - 3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly), - 4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild), + 3 => collectionManager.Active.ByType(CollectionType.NonPlayerElderly), + 4 => collectionManager.Active.ByType(CollectionType.NonPlayerChild), _ => null, }; if (collection != null) @@ -231,18 +212,18 @@ public unsafe class CollectionResolver var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); - collection = _collectionManager.Active.ByType(type); - collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); + collection = collectionManager.Active.ByType(type); + collection ??= collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); return collection; } /// Get the collection applying to the owner if it is available. private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady) { - if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) + if (identifier.Type != IdentifierType.Owned || !config.UseOwnerNameForCharacterCollection || owner == null) return null; - var id = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, + var id = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckYourself(id, owner) diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 18c016b9..c7b24bd7 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,31 +1,34 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; using Penumbra.GameData.Enums; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks; namespace Penumbra.Interop.PathResolving; -public class CutsceneService : IDisposable +public sealed class CutsceneService : IService, IDisposable { public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; - private readonly GameEventManager _events; - private readonly IObjectTable _objects; - private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); + private readonly IObjectTable _objects; + private readonly CopyCharacter _copyCharacter; + private readonly CharacterDestructor _characterDestructor; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public unsafe CutsceneService(IObjectTable objects, GameEventManager events) + public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor) { - _objects = objects; - _events = events; - _events.CopyCharacter += OnCharacterCopy; - _events.CharacterDestructor += OnCharacterDestructor; + _objects = objects; + _copyCharacter = copyCharacter; + _characterDestructor = characterDestructor; + _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); } /// @@ -57,8 +60,8 @@ public class CutsceneService : IDisposable public unsafe void Dispose() { - _events.CopyCharacter -= OnCharacterCopy; - _events.CharacterDestructor -= OnCharacterDestructor; + _copyCharacter.Unsubscribe(OnCharacterCopy); + _characterDestructor.Unsubscribe(OnCharacterDestructor); } private unsafe void OnCharacterDestructor(Character* character) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 9726d84c..19c0fd10 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,35 +1,39 @@ -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; -using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Interop.Hooks; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; +using Penumbra.GameData.Structs; namespace Penumbra.Interop.PathResolving; -public class DrawObjectState : IDisposable, IReadOnlyDictionary +public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { - private readonly IObjectTable _objects; - private readonly GameEventManager _gameEvents; + private readonly IObjectTable _objects; + private readonly CreateCharacterBase _createCharacterBase; + private readonly WeaponReload _weaponReload; + private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly GameState _gameState; - private readonly Dictionary _drawObjectToGameObject = new(); - - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + private readonly Dictionary _drawObjectToGameObject = []; public nint LastGameObject - => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; + => _gameState.LastGameObject; - public DrawObjectState(IObjectTable objects, GameEventManager gameEvents, IGameInteropProvider interop) + public unsafe DrawObjectState(IObjectTable objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, + CharacterBaseDestructor characterBaseDestructor, GameState gameState) { - interop.InitializeFromAttributes(this); - _enableDrawHook.Enable(); - _objects = objects; - _gameEvents = gameEvents; - _gameEvents.WeaponReloading += OnWeaponReloading; - _gameEvents.WeaponReloaded += OnWeaponReloaded; - _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; - _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _objects = objects; + _createCharacterBase = createCharacterBase; + _weaponReload = weaponReload; + _characterBaseDestructor = characterBaseDestructor; + _gameState = gameState; + _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); + _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); + _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); InitializeDrawObjects(); } @@ -57,32 +61,32 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary Values => _drawObjectToGameObject.Values; - public void Dispose() + public unsafe void Dispose() { - _gameEvents.WeaponReloading -= OnWeaponReloading; - _gameEvents.WeaponReloaded -= OnWeaponReloaded; - _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; - _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; - _enableDrawHook.Dispose(); + _weaponReload.Unsubscribe(OnWeaponReloading); + _weaponReload.Unsubscribe(OnWeaponReloaded); + _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); + _characterBaseDestructor.Unsubscribe(OnCharacterBaseDestructor); } - private void OnWeaponReloading(nint _, nint gameObject) - => _lastGameObject.Value!.Enqueue(gameObject); + private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2) + => _gameState.QueueGameObject((nint)character); - private unsafe void OnWeaponReloaded(nint _, nint gameObject) + private unsafe void OnWeaponReloaded(DrawDataContainer* _, Character* character) { - _lastGameObject.Value!.Dequeue(); - IterateDrawObjectTree((Object*)((GameObject*)gameObject)->DrawObject, gameObject, false, false); + _gameState.DequeueGameObject(); + IterateDrawObjectTree((Object*)character->GameObject.DrawObject, (nint)character, false, false); } - private void OnCharacterBaseDestructor(nint characterBase) - => _drawObjectToGameObject.Remove(characterBase); + private unsafe void OnCharacterBaseDestructor(CharacterBase* characterBase) + => _drawObjectToGameObject.Remove((nint)characterBase); - private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + private unsafe void OnCharacterBaseCreated(ModelCharaId modelCharaId, CustomizeArray* customize, CharacterArmor* equipment, + CharacterBase* drawObject) { var gameObject = LastGameObject; if (gameObject != nint.Zero) - _drawObjectToGameObject[drawObject] = (gameObject, false); + _drawObjectToGameObject[(nint)drawObject] = (gameObject, false); } /// @@ -123,20 +127,4 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionaryPreviousSiblingObject; } } - - /// - /// EnableDraw is what creates DrawObjects for gameObjects, - /// so we always keep track of the current GameObject to be able to link it to the DrawObject. - /// - private delegate void EnableDrawDelegate(nint gameObject); - - [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] - private readonly Hook _enableDrawHook = null!; - - private void EnableDrawDetour(nint gameObject) - { - _lastGameObject.Value!.Enqueue(gameObject); - _enableDrawHook.Original.Invoke(gameObject); - _lastGameObject.Value!.TryDequeue(out _); - } } diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 73c20ab9..3e7171f8 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,7 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Actors; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks; using Penumbra.Services; namespace Penumbra.Interop.PathResolving; @@ -13,20 +13,20 @@ namespace Penumbra.Interop.PathResolving; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> { private readonly CommunicatorService _communicator; - private readonly GameEventManager _events; + private readonly CharacterDestructor _characterDestructor; private readonly IClientState _clientState; private readonly Dictionary _cache = new(317); private bool _dirty; - public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, GameEventManager events) + public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, CharacterDestructor characterDestructor) { - _clientState = clientState; - _communicator = communicator; - _events = events; + _clientState = clientState; + _communicator = communicator; + _characterDestructor = characterDestructor; _communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache); _clientState.TerritoryChanged += TerritoryClear; - _events.CharacterDestructor += OnCharacterDestruct; + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.IdentifiedCollectionCache); } public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) @@ -62,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A { _communicator.CollectionChange.Unsubscribe(CollectionChangeClear); _clientState.TerritoryChanged -= TerritoryClear; - _events.CharacterDestructor -= OnCharacterDestruct; + _characterDestructor.Unsubscribe(OnCharacterDestructor); } public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() @@ -88,6 +88,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A private void TerritoryClear(ushort _2) => _dirty = _cache.Count > 0; - private void OnCharacterDestruct(Character* character) + private void OnCharacterDestructor(Character* character) => _cache.Remove((nint)character); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index c1e0bb80..9ef291c7 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -9,6 +9,8 @@ using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; @@ -52,24 +54,24 @@ public unsafe class MetaState : IDisposable private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; - private readonly GameEventManager _gameEventManager; private readonly CharacterUtility _characterUtility; + private readonly CreateCharacterBase _createCharacterBase; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private ResolveData _customizeChangeCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config, + ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config, IGameInteropProvider interop) { - _performance = performance; - _communicator = communicator; - _collectionResolver = collectionResolver; - _resources = resources; - _gameEventManager = gameEventManager; - _characterUtility = characterUtility; - _config = config; + _performance = performance; + _communicator = communicator; + _collectionResolver = collectionResolver; + _resources = resources; + _createCharacterBase = createCharacterBase; + _characterUtility = characterUtility; + _config = config; interop.InitializeFromAttributes(this); _calculateHeightHook = interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); @@ -81,8 +83,8 @@ public unsafe class MetaState : IDisposable _rspSetupCharacterHook.Enable(); _changeCustomize.Enable(); _calculateHeightHook.Enable(); - _gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated; + _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) @@ -124,31 +126,31 @@ public unsafe class MetaState : IDisposable _rspSetupCharacterHook.Dispose(); _changeCustomize.Dispose(); _calculateHeightHook.Dispose(); - _gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; + _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); + _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); } - private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData) + private void OnCreatingCharacterBase(ModelCharaId* modelCharaId, CustomizeArray* customize, CharacterArmor* equipData) { _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData); + _lastCreatedCollection.ModCollection.Name, (nint) modelCharaId, (nint) customize, (nint) equipData); var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, - UsesDecal(*(uint*)modelCharaId, customize)); + UsesDecal(*(uint*)modelCharaId, (nint) customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); } - private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject) + private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject) { _characterBaseCreateMetaChanges.Dispose(); _characterBaseCreateMetaChanges = DisposableContainer.Empty; - if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != nint.Zero) + if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection, drawObject); + _lastCreatedCollection.ModCollection, (nint)drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 6db97b63..7c16b97b 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -18,26 +18,28 @@ public class PathResolver : IDisposable private readonly TempCollectionManager _tempCollections; private readonly ResourceLoader _loader; - private readonly AnimationHookService _animationHookService; - private readonly SubfileHelper _subfileHelper; - private readonly PathState _pathState; - private readonly MetaState _metaState; + private readonly SubfileHelper _subfileHelper; + private readonly PathState _pathState; + private readonly MetaState _metaState; + private readonly GameState _gameState; + private readonly CollectionResolver _collectionResolver; public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, - PathState pathState, MetaState metaState) + TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper, + PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { - _performance = performance; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _animationHookService = animationHookService; - _subfileHelper = subfileHelper; - _pathState = pathState; - _metaState = metaState; - _loader = loader; - _loader.ResolvePath = ResolvePath; - _loader.FileLoaded += ImcLoadResource; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _tempCollections = tempCollections; + _subfileHelper = subfileHelper; + _pathState = pathState; + _metaState = metaState; + _gameState = gameState; + _collectionResolver = collectionResolver; + _loader = loader; + _loader.ResolvePath = ResolvePath; + _loader.FileLoaded += ImcLoadResource; } /// Obtain a temporary or permanent collection by name. @@ -98,7 +100,7 @@ public class PathResolver : IDisposable // A potential next request will add the path anew. var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData) || _pathState.Consume(gamePath.Path, out resolveData) - || _animationHookService.HandleFiles(type, gamePath, out resolveData) + || _gameState.HandleFiles(_collectionResolver, type, gamePath, out resolveData) || _metaState.HandleDecalFile(type, gamePath, out resolveData); if (!nonDefault || !resolveData.Valid) resolveData = _collectionManager.Active.Default.ToResolveData(); diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 3a60450d..370118ea 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -4,6 +4,7 @@ using Dalamud.Utility.Signatures; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; @@ -21,30 +22,30 @@ namespace Penumbra.Interop.PathResolving; /// public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> { - private readonly PerformanceTracker _performance; - private readonly ResourceLoader _loader; - private readonly GameEventManager _events; - private readonly CommunicatorService _communicator; + private readonly PerformanceTracker _performance; + private readonly ResourceLoader _loader; + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary _subFileCollection = new(); - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator, IGameInteropProvider interop) + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor) { interop.InitializeFromAttributes(this); - _performance = performance; - _loader = loader; - _events = events; - _communicator = communicator; + _performance = performance; + _loader = loader; + _communicator = communicator; + _resourceHandleDestructor = resourceHandleDestructor; _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); _apricotResourceLoadHook.Enable(); _loader.ResourceLoaded += SubfileContainerRequested; - _events.ResourceHandleDestructor += ResourceDestroyed; + _resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper); } @@ -105,7 +106,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour); - _characterBaseCreateHook = - interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour); - _characterBaseDestructorHook = - interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Destroy, - CharacterBaseDestructorDetour); - _weaponReloadHook = - interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, WeaponReloadDetour); - _characterDtorHook.Enable(); - _copyCharacterHook.Enable(); - _resourceHandleDestructorHook.Enable(); - _characterBaseCreateHook.Enable(); - _characterBaseDestructorHook.Enable(); - _weaponReloadHook.Enable(); - EnableDebugHook(); - Penumbra.Log.Verbose($"{Prefix} Created."); - } - - public void Dispose() - { - _characterDtorHook.Dispose(); - _copyCharacterHook.Dispose(); - _resourceHandleDestructorHook.Dispose(); - _characterBaseCreateHook.Dispose(); - _characterBaseDestructorHook.Dispose(); - _weaponReloadHook.Dispose(); - DisposeDebugHook(); - Penumbra.Log.Verbose($"{Prefix} Disposed."); - } - - #region Character Destructor - - private delegate void CharacterDestructorDelegate(Character* character); - - [Signature(Sigs.CharacterDestructor, DetourName = nameof(CharacterDestructorDetour))] - private readonly Hook _characterDtorHook = null!; - - private void CharacterDestructorDetour(Character* character) - { - if (CharacterDestructor != null) - foreach (var subscriber in CharacterDestructor.GetInvocationList()) - { - try - { - ((CharacterDestructorEvent)subscriber).Invoke(character); - } - catch (Exception ex) - { - Penumbra.Log.Error($"{Prefix} Error in {nameof(CharacterDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Verbose($"{Prefix} {nameof(CharacterDestructor)} triggered with 0x{(nint)character:X}."); - _characterDtorHook.Original(character); - } - - public delegate void CharacterDestructorEvent(Character* character); - - #endregion - - #region Copy Character - - private delegate ulong CopyCharacterDelegate(CharacterSetup* target, GameObject* source, uint unk); - - private readonly Hook _copyCharacterHook; - - private ulong CopyCharacterDetour(CharacterSetup* target, GameObject* source, uint unk) - { - // TODO: update when CS updated. - var character = ((Character**)target)[1]; - if (CopyCharacter != null) - foreach (var subscriber in CopyCharacter.GetInvocationList()) - { - try - { - ((CopyCharacterEvent)subscriber).Invoke(character, (Character*)source); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CopyCharacter)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Verbose( - $"{Prefix} {nameof(CopyCharacter)} triggered with target 0x{(nint)target:X} and source 0x{(nint)source:X}."); - return _copyCharacterHook.Original(target, source, unk); - } - - public delegate void CopyCharacterEvent(Character* target, Character* source); - - #endregion - - #region ResourceHandle Destructor - - private delegate IntPtr ResourceHandleDestructorDelegate(ResourceHandle* handle); - - [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] - private readonly Hook _resourceHandleDestructorHook = null!; - - private IntPtr ResourceHandleDestructorDetour(ResourceHandle* handle) - { - if (ResourceHandleDestructor != null) - foreach (var subscriber in ResourceHandleDestructor.GetInvocationList()) - { - try - { - ((ResourceHandleDestructorEvent)subscriber).Invoke(handle); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(ResourceHandleDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Excessive($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}."); - return _resourceHandleDestructorHook!.Original(handle); - } - - public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); - - #endregion - - #region CharacterBaseCreate - - private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d); - - private readonly Hook _characterBaseCreateHook; - - private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d) - { - if (CreatingCharacterBase != null) - foreach (var subscriber in CreatingCharacterBase.GetInvocationList()) - { - try - { - ((CreatingCharacterBaseEvent)subscriber).Invoke((nint)(&a), b, c); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - var ret = _characterBaseCreateHook.Original(a, b, c, d); - if (CharacterBaseCreated != null) - foreach (var subscriber in CharacterBaseCreated.GetInvocationList()) - { - try - { - ((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - return ret; - } - - public delegate void CreatingCharacterBaseEvent(nint modelCharaId, nint customize, nint equipment); - public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject); - - #endregion - - #region CharacterBase Destructor - - public delegate void CharacterBaseDestructorEvent(nint drawBase); - - private readonly Hook _characterBaseDestructorHook; - - private void CharacterBaseDestructorDetour(IntPtr drawBase) - { - if (CharacterBaseDestructor != null) - foreach (var subscriber in CharacterBaseDestructor.GetInvocationList()) - { - try - { - ((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - _characterBaseDestructorHook.Original.Invoke(drawBase); - } - - #endregion - - #region Weapon Reload - - private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7); - - private readonly Hook _weaponReloadHook; - - private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7) - { - var gameObject = *(nint*)(a1 + 8); - if (WeaponReloading != null) - foreach (var subscriber in WeaponReloading.GetInvocationList()) - { - try - { - ((WeaponReloadingEvent)subscriber).Invoke(a1, gameObject); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - _weaponReloadHook.Original(a1, a2, a3, a4, a5, a6, a7); - - if (WeaponReloaded != null) - foreach (var subscriber in WeaponReloaded.GetInvocationList()) - { - try - { - ((WeaponReloadedEvent)subscriber).Invoke(a1, gameObject); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - } - - public delegate void WeaponReloadingEvent(nint drawDataContainer, nint gameObject); - public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject); - - #endregion - - #region Testing - -#if DEBUG - //[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(TestDetour))] - private readonly Hook? _testHook = null; - - private delegate void TestDelegate(nint a1, int a2); - - private void TestDetour(nint a1, int a2) - { - Penumbra.Log.Information($"Test: {a1:X} {a2}"); - _testHook!.Original(a1, a2); - } - - private void EnableDebugHook() - => _testHook?.Enable(); - - private void DisposeDebugHook() - => _testHook?.Dispose(); -#else - private void EnableDebugHook() - { } - - private void DisposeDebugHook() - { } -#endif - - #endregion -} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index d25a5638..444b9a48 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData; +using Penumbra.Interop.Hooks; using Penumbra.Services; namespace Penumbra.Interop.Services; @@ -32,9 +33,9 @@ public sealed unsafe class SkinFixer : IDisposable private readonly Hook _onRenderMaterialHook; - private readonly GameEventManager _gameEvents; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _utility; // MaterialResourceHandle set private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); @@ -50,15 +51,16 @@ public sealed unsafe class SkinFixer : IDisposable public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator, IGameInteropProvider interop) + public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator, + IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - _gameEvents = gameEvents; - _utility = utility; - _communicator = communicator; - _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _communicator = communicator; + _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); - _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; + _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer); _onRenderMaterialHook.Enable(); } @@ -66,7 +68,7 @@ public sealed unsafe class SkinFixer : IDisposable { _onRenderMaterialHook.Dispose(); _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); - _gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor; + _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); _moddedSkinShpkMaterials.Clear(); _moddedSkinShpkCount = 0; } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 66101dcd..6c5f9c25 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -2,7 +2,6 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 900d2770..350c20b2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -80,6 +80,10 @@ public class Penumbra : IDalamudPlugin _services.GetService(); _services.GetService(); // Initialize before Interface. + + foreach (var service in _services.GetServicesImplementing()) + service.Awaiter.Wait(); + SetupInterface(); SetupApi(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 54f7a16f..b7f3de9d 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -69,6 +69,7 @@ + @@ -92,8 +93,8 @@ - - + + diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index c7efac04..be94a31e 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -9,7 +9,7 @@ public class CommunicatorService : IDisposable, IService { public CommunicatorService(Logger logger) { - EventWrapper.ChangeLogger(logger); + EventWrapperBase.ChangeLogger(logger); } /// diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index 5a1c9f74..8038152e 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -82,8 +82,7 @@ public static class ServiceManagerA .AddDalamudService(pi); private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton(p => { @@ -135,8 +134,7 @@ public static class ServiceManagerA .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 09e22d67..376bbcf7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; @@ -8,6 +9,7 @@ using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks; using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; @@ -503,12 +505,12 @@ public partial class ModEditWindow ColorTablePreviewers.Clear(); } - private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) { for (var i = MaterialPreviewers.Count; i-- > 0;) { var previewer = MaterialPreviewers[i]; - if ((nint)previewer.DrawObject != characterBase) + if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); @@ -518,7 +520,7 @@ public partial class ModEditWindow for (var i = ColorTablePreviewers.Count; i-- > 0;) { var previewer = ColorTablePreviewers[i]; - if ((nint)previewer.DrawObject != characterBase) + if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); @@ -663,7 +665,7 @@ public partial class ModEditWindow UpdateConstants(); } - public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) { _edit = edit; Mtrl = file; @@ -673,16 +675,16 @@ public partial class ModEditWindow LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) { - _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; + _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); BindToMaterialInstances(); } } - public void Dispose() + public unsafe void Dispose() { UnbindFromMaterialInstances(); if (Writable) - _edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances; + _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index df379a1c..8a4bd52b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,8 +13,8 @@ using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceTree; -using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -32,20 +32,20 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; - private readonly CommunicatorService _communicator; - private readonly IDragDropManager _dragDropManager; - private readonly GameEventManager _gameEvents; - private readonly IDataManager _gameData; - private readonly IFramework _framework; - private readonly IObjectTable _objects; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly StainService _stainService; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly IObjectTable _objects; + private readonly CharacterBaseDestructor _characterBaseDestructor; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -565,26 +565,26 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, - ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, + ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _fileDialog = fileDialog; - _gameEvents = gameEvents; - _objects = objects; - _framework = framework; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _fileDialog = fileDialog; + _objects = objects; + _framework = framework; + _characterBaseDestructor = characterBaseDestructor; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); @@ -598,12 +598,12 @@ public partial class ModEditWindow : Window, IDisposable _resourceTreeFactory = resourceTreeFactory; _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); - _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); } public void Dispose() { - _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); _editor?.Dispose(); _materialTab.Dispose(); _modelTab.Dispose(); @@ -613,7 +613,7 @@ public partial class ModEditWindow : Window, IDisposable _center.Dispose(); } - private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved) ChangeMod(mod); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 16353828..a0073d05 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,6 +11,18 @@ "Unosquare.Swan.Lite": "3.0.0" } }, + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", "requested": "[7.0.0, )", @@ -36,6 +48,11 @@ "System.Text.Encoding.CodePages": "5.0.0" } }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "7.0.0", @@ -46,10 +63,23 @@ "resolved": "5.0.0", "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, "System.Text.Encoding.CodePages": { "type": "Transitive", From 68c782f0b9317a1230d1d1b529b378a1fb356117 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 Jan 2024 00:17:15 +0100 Subject: [PATCH 1375/2451] Move all meta hooks to own classes. --- Penumbra/Interop/CharacterBaseVTables.cs | 24 ++ .../Interop/Hooks/Meta/CalculateHeight.cs | 31 +++ .../Interop/Hooks/Meta/ChangeCustomize.cs | 34 +++ Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 36 +++ .../Interop/Hooks/Meta/ModelLoadComplete.cs | 30 +++ .../Interop/Hooks/Meta/RspSetupCharacter.cs | 37 +++ Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 35 +++ Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 36 +++ .../PathResolving/CollectionResolver.cs | 4 +- .../IdentifiedCollectionCache.cs | 4 +- Penumbra/Interop/PathResolving/MetaState.cs | 223 ++++-------------- 11 files changed, 314 insertions(+), 180 deletions(-) create mode 100644 Penumbra/Interop/CharacterBaseVTables.cs create mode 100644 Penumbra/Interop/Hooks/Meta/CalculateHeight.cs create mode 100644 Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs create mode 100644 Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs create mode 100644 Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs create mode 100644 Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs create mode 100644 Penumbra/Interop/Hooks/Meta/SetupVisor.cs create mode 100644 Penumbra/Interop/Hooks/Meta/UpdateModel.cs diff --git a/Penumbra/Interop/CharacterBaseVTables.cs b/Penumbra/Interop/CharacterBaseVTables.cs new file mode 100644 index 00000000..40b9a588 --- /dev/null +++ b/Penumbra/Interop/CharacterBaseVTables.cs @@ -0,0 +1,24 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop; + +public sealed unsafe class CharacterBaseVTables : IService +{ + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* HumanVTable = null!; + + [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* WeaponVTable = null!; + + [Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* DemiHumanVTable = null!; + + [Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* MonsterVTable = null!; + + public CharacterBaseVTables(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); +} diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs new file mode 100644 index 00000000..2fd87f6e --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class CalculateHeight : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, true); + } + + public delegate ulong Delegate(Character* character); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private ulong Detour(Character* character) + { + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + using var cmp = _metaState.ResolveRspData(collection.ModCollection); + var ret = Task.Result.Original.Invoke(character); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs new file mode 100644 index 00000000..81f6d552 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class ChangeCustomize : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public ChangeCustomize(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, true); + } + + public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) + { + _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection); + using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); + using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); + var ret = Task.Result.Original.Invoke(human, data, skipEquipment); + Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs new file mode 100644 index 00000000..c1b6eacc --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class GetEqpIndirect : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public GetEqpIndirect(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0) + return; + + Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + Task.Result.Original(drawObject); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs new file mode 100644 index 00000000..9f191fdd --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -0,0 +1,30 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class ModelLoadComplete : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public ModelLoadComplete(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vtables) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[58], Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + Penumbra.Log.Excessive($"[Model Load Complete] Invoked on {(nint)drawObject:X}."); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); + Task.Result.Original(drawObject); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs new file mode 100644 index 00000000..8f8f1d78 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -0,0 +1,37 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class RspSetupCharacter : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public RspSetupCharacter(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5) + { + Penumbra.Log.Excessive($"[RSP Setup Character] Invoked on {(nint)drawObject:X} with {unk2}, {unk3}, {unk4}, {unk5}."); + // Skip if we are coming from ChangeCustomize. + if (_metaState.CustomizeChangeCollection.Valid) + { + Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5); + return; + } + + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var cmp = _metaState.ResolveRspData(collection.ModCollection); + Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs new file mode 100644 index 00000000..e451f118 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +/// +/// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, +/// but it only applies a changed gmp file after a redraw for some reason. +/// +public sealed unsafe class SetupVisor : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public SetupVisor(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, true); + } + + public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) + { + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var gmp = _metaState.ResolveGmpData(collection.ModCollection); + var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); + Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs new file mode 100644 index 00000000..c169ead2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class UpdateModel : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public UpdateModel(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + // Shortcut because this is called all the time. + // Same thing is checked at the beginning of the original function. + if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0) + return; + + Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); + Task.Result.Original.Invoke(drawObject); + } +} diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index c649147a..1a715f13 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -24,12 +24,12 @@ public sealed unsafe class CollectionResolver( CollectionManager collectionManager, TempCollectionManager tempCollections, DrawObjectState drawObjectState, - HumanModelList humanModels) + HumanModelList humanModels) : IService { /// /// Get the collection applying to the current player character - /// or the Yourself or Default collection if no player exists. + /// or the 'Yourself' or 'Default' collection if no player exists. /// public ModCollection PlayerCollection() { diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 3e7171f8..b944011d 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -20,8 +20,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, CharacterDestructor characterDestructor) { - _clientState = clientState; - _communicator = communicator; + _clientState = clientState; + _communicator = communicator; _characterDestructor = characterDestructor; _communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache); diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 9ef291c7..9d899648 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,13 +1,7 @@ -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks; @@ -15,10 +9,8 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.Interop.PathResolving; @@ -44,56 +36,40 @@ namespace Penumbra.Interop.PathResolving; // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. -public unsafe class MetaState : IDisposable +public sealed unsafe class MetaState : IDisposable { - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; private readonly CharacterUtility _characterUtility; private readonly CreateCharacterBase _createCharacterBase; + public ResolveData CustomizeChangeCollection = ResolveData.Invalid; + private ResolveData _lastCreatedCollection = ResolveData.Invalid; - private ResolveData _customizeChangeCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; - public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config, - IGameInteropProvider interop) + public MetaState(CommunicatorService communicator, CollectionResolver collectionResolver, + ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config) { - _performance = performance; _communicator = communicator; _collectionResolver = collectionResolver; _resources = resources; _createCharacterBase = createCharacterBase; _characterUtility = characterUtility; _config = config; - interop.InitializeFromAttributes(this); - _calculateHeightHook = - interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); - _onModelLoadCompleteHook = interop.HookFromAddress(_humanVTable[58], OnModelLoadCompleteDetour); - _getEqpIndirectHook.Enable(); - _updateModelsHook.Enable(); - _onModelLoadCompleteHook.Enable(); - _setupVisorHook.Enable(); - _rspSetupCharacterHook.Enable(); - _changeCustomize.Enable(); - _calculateHeightHook.Enable(); _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); - _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) { if (type == ResourceType.Tex - && (_lastCreatedCollection.Valid || _customizeChangeCollection.Valid) + && (_lastCreatedCollection.Valid || CustomizeChangeCollection.Valid) && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) { - resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : _customizeChangeCollection; + resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : CustomizeChangeCollection; return true; } @@ -102,30 +78,49 @@ public unsafe class MetaState : IDisposable } public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) - { - var races = race.Dependencies(); + => (equipment, accessory) switch + { + (true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[] + { + collection.TemporarilySetEqdpFile(_characterUtility, r, false), + collection.TemporarilySetEqdpFile(_characterUtility, r, true), + })), + (true, false) => new DisposableContainer(race.Dependencies() + .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))), + (false, true) => new DisposableContainer(race.Dependencies() + .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))), + _ => DisposableContainer.Empty, + }; - var equipmentEnumerable = equipment - ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false)) - : Array.Empty().AsEnumerable(); - var accessoryEnumerable = accessory - ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true)) - : Array.Empty().AsEnumerable(); - return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable)); - } + public MetaList.MetaReverter ResolveEqpData(ModCollection collection) + => collection.TemporarilySetEqpFile(_characterUtility); + + public MetaList.MetaReverter ResolveGmpData(ModCollection collection) + => collection.TemporarilySetGmpFile(_characterUtility); + + public MetaList.MetaReverter ResolveRspData(ModCollection collection) + => collection.TemporarilySetCmpFile(_characterUtility); + + public DecalReverter ResolveDecal(ResolveData resolve, bool which) + => new(_config, _characterUtility, _resources, resolve, which); public static GenderRace GetHumanGenderRace(nint human) => (GenderRace)((Human*)human)->RaceSexId; + public static GenderRace GetDrawObjectGenderRace(nint drawObject) + { + var draw = (DrawObject*)drawObject; + if (draw->Object.GetObjectType() != ObjectType.CharacterBase) + return GenderRace.Unknown; + + var c = (CharacterBase*)drawObject; + return c->GetModelType() == CharacterBase.ModelType.Human + ? GetHumanGenderRace(drawObject) + : GenderRace.Unknown; + } + public void Dispose() { - _getEqpIndirectHook.Dispose(); - _updateModelsHook.Dispose(); - _onModelLoadCompleteHook.Dispose(); - _setupVisorHook.Dispose(); - _rspSetupCharacterHook.Dispose(); - _changeCustomize.Dispose(); - _calculateHeightHook.Dispose(); _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); } @@ -135,10 +130,10 @@ public unsafe class MetaState : IDisposable _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, (nint) modelCharaId, (nint) customize, (nint) equipData); + _lastCreatedCollection.ModCollection.Name, (nint)modelCharaId, (nint)customize, (nint)equipData); var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, - UsesDecal(*(uint*)modelCharaId, (nint) customize)); + UsesDecal(*(uint*)modelCharaId, (nint)customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); @@ -154,134 +149,10 @@ public unsafe class MetaState : IDisposable _lastCreatedCollection = ResolveData.Invalid; } - private delegate void OnModelLoadCompleteDelegate(nint drawObject); - private readonly Hook _onModelLoadCompleteHook; - - private void OnModelLoadCompleteDetour(nint drawObject) - { - var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); - using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); - _onModelLoadCompleteHook.Original.Invoke(drawObject); - } - - private delegate void UpdateModelDelegate(nint drawObject); - - [Signature(Sigs.UpdateModel, DetourName = nameof(UpdateModelsDetour))] - private readonly Hook _updateModelsHook = null!; - - private void UpdateModelsDetour(nint drawObject) - { - // Shortcut because this is called all the time. - // Same thing is checked at the beginning of the original function. - if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0) - return; - - using var performance = _performance.Measure(PerformanceType.UpdateModels); - - var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); - using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); - _updateModelsHook.Original.Invoke(drawObject); - } - - private static GenderRace GetDrawObjectGenderRace(nint drawObject) - { - var draw = (DrawObject*)drawObject; - if (draw->Object.GetObjectType() != ObjectType.CharacterBase) - return Unknown; - - var c = (CharacterBase*)drawObject; - return c->GetModelType() == CharacterBase.ModelType.Human - ? GetHumanGenderRace(drawObject) - : Unknown; - } - - [Signature(Sigs.GetEqpIndirect, DetourName = nameof(GetEqpIndirectDetour))] - private readonly Hook _getEqpIndirectHook = null!; - - private void GetEqpIndirectDetour(nint drawObject) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0) - return; - - using var performance = _performance.Measure(PerformanceType.GetEqp); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(_characterUtility); - _getEqpIndirectHook.Original(drawObject); - } - - - // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, - // but it only applies a changed gmp file after a redraw for some reason. - private delegate byte SetupVisorDelegate(nint drawObject, ushort modelId, byte visorState); - - [Signature(Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] - private readonly Hook _setupVisorHook = null!; - - private byte SetupVisorDetour(nint drawObject, ushort modelId, byte visorState) - { - using var performance = _performance.Measure(PerformanceType.SetupVisor); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(_characterUtility); - return _setupVisorHook.Original(drawObject, modelId, visorState); - } - - // RSP - private delegate void RspSetupCharacterDelegate(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5); - - [Signature(Sigs.RspSetupCharacter, DetourName = nameof(RspSetupCharacterDetour))] - private readonly Hook _rspSetupCharacterHook = null!; - - private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5) - { - if (_customizeChangeCollection.Valid) - { - _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); - } - else - { - using var performance = _performance.Measure(PerformanceType.SetupCharacter); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); - } - } - - private delegate ulong CalculateHeightDelegate(Character* character); - - private readonly Hook _calculateHeightHook = null!; - - private ulong CalculateHeightDetour(Character* character) - { - var resolveData = _collectionResolver.IdentifyCollection((GameObject*)character, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - return _calculateHeightHook.Original(character); - } - - private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment); - - [Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))] - private readonly Hook _changeCustomize = null!; - - private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment) - { - using var performance = _performance.Measure(PerformanceType.ChangeCustomize); - _customizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = _customizeChangeCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); - using var decals = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, true); - using var decal2 = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, false); - var ret = _changeCustomize.Original(human, data, skipEquipment); - _customizeChangeCollection = ResolveData.Invalid; - return ret; - } - /// /// Check the customize array for the FaceCustomization byte and the last bit of that. /// Also check for humans. /// - public static bool UsesDecal(uint modelId, nint customizeData) + private static bool UsesDecal(uint modelId, nint customizeData) => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; } From bc068f991389d2adcaa05b26d2877ddf6acd63bc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 Jan 2024 00:48:20 +0100 Subject: [PATCH 1376/2451] Fix offsets. --- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 7 +++---- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index c1b6eacc..8ffc050f 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -22,13 +22,12 @@ public sealed unsafe class GetEqpIndirect : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Detour(DrawObject* drawObject) { - if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0) + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if ((*(byte*)((nint)drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)((nint)drawObject + Offsets.GetEqpIndirectSkip2) == 0) return; Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - var collection = _collectionResolver.IdentifyCollection(drawObject, true); using var eqp = _metaState.ResolveEqpData(collection.ModCollection); Task.Result.Original(drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index c169ead2..786ad5f2 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -24,7 +24,7 @@ public sealed unsafe class UpdateModel : FastHook { // Shortcut because this is called all the time. // Same thing is checked at the beginning of the original function. - if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0) + if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0) return; Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); From 518117b25a5a31c5b1347541c78c5c0f57aeb98d Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 11:01:31 +1100 Subject: [PATCH 1377/2451] Add submeshless support --- Penumbra/Import/Models/Export/MeshExporter.cs | 34 +++++++++++-------- Penumbra/Import/Models/ModelManager.cs | 6 +++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 06ca747b..7b51ca31 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -34,7 +34,6 @@ public class MeshExporter } } - // TODO: replace bonenamemap with a gltfskeleton public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton) { var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names); @@ -74,7 +73,7 @@ public class MeshExporter private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) { - // todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... + // TODO: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); @@ -97,20 +96,25 @@ public class MeshExporter var indices = BuildIndices(); var vertices = BuildVertices(); - // TODO: handle SubMeshCount = 0 + // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. + + if (XivMesh.SubMeshCount == 0) + return [BuildMesh(indices, vertices, 0, (int)XivMesh.IndexCount)]; return _mdl.SubMeshes .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) - .Select(submesh => BuildSubMesh(submesh, indices, vertices)) + .Select(submesh => BuildMesh(indices, vertices, (int)(submesh.IndexOffset - XivMesh.StartIndex), (int)submesh.IndexCount)) .ToArray(); } - private IMeshBuilder BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList indices, IReadOnlyList vertices) + private IMeshBuilder BuildMesh( + IReadOnlyList indices, + IReadOnlyList vertices, + int indexBase, + int indexCount + ) { - // Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh. - var startIndex = (int)(submesh.IndexOffset - XivMesh.StartIndex); - var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( typeof(MaterialBuilder), _geometryType, @@ -131,12 +135,12 @@ public class MeshExporter var gltfIndices = new List(); // All XIV meshes use triangle lists. - for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3) + for (var indexOffset = 0; indexOffset < indexCount; indexOffset += 3) { var (a, b, c) = primitiveBuilder.AddTriangle( - vertices[indices[indexOffset + startIndex + 0]], - vertices[indices[indexOffset + startIndex + 1]], - vertices[indices[indexOffset + startIndex + 2]] + vertices[indices[indexBase + indexOffset + 0]], + vertices[indices[indexBase + indexOffset + 1]], + vertices[indices[indexBase + indexOffset + 2]] ); gltfIndices.AddRange([a, b, c]); } @@ -157,8 +161,8 @@ public class MeshExporter .Take((int)shapeMesh.ShapeValueCount) ) .Where(shapeValue => - shapeValue.BaseIndicesIndex >= startIndex - && shapeValue.BaseIndicesIndex < startIndex + submesh.IndexCount + shapeValue.BaseIndicesIndex >= indexBase + && shapeValue.BaseIndicesIndex < indexBase + indexCount ) .ToList(); @@ -169,7 +173,7 @@ public class MeshExporter foreach (var shapeValue in shapeValues) morphBuilder.SetVertex( - primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - startIndex]].GetGeometry(), + primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(), vertices[shapeValue.ReplacingVertexIndex].GetGeometry() ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index a56d7168..4f761549 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -73,17 +73,21 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken cancel) { + Penumbra.Log.Debug("Reading skeleton."); var xivSkeleton = BuildSkeleton(cancel); + + Penumbra.Log.Debug("Converting model."); var model = ModelExporter.Export(_mdl, xivSkeleton); + Penumbra.Log.Debug("Building scene."); var scene = new SceneBuilder(); model.AddToScene(scene); + Penumbra.Log.Debug("Saving."); var gltfModel = scene.ToGltf2(); gltfModel.SaveGLTF(_outputPath); } - // TODO: this should be moved to a seperate model converter or something private XivSkeleton? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) From 08ed3ca447149d7cc3c70ca99a74f6c6dfe1d108 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 11:31:38 +1100 Subject: [PATCH 1378/2451] Handle mesh skeleton edge cases --- Penumbra/Import/Models/Export/MeshExporter.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 7b51ca31..e835fe62 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -69,11 +69,18 @@ public class MeshExporter _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); + + // If there's skinning usages but no bone mapping, there's probably something wrong with the data. + if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) + Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); } - private Dictionary BuildBoneIndexMap(Dictionary boneNameMap) + private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap) { - // TODO: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign... + // A BoneTableIndex of 255 means that this mesh is not skinned. + if (XivMesh.BoneTableIndex == 255) + return null; + var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); @@ -82,8 +89,7 @@ public class MeshExporter { var boneName = _mdl.Bones[xivBoneIndex]; if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) - // TODO: handle - i think this is a hard failure, it means that a bone name in the model doesn't exist in the armature. - throw new Exception($"looking for {boneName} in {string.Join(", ", boneNameMap.Keys)}"); + throw new Exception($"Armature does not contain bone \"{boneName}\" requested by mesh {_meshIndex}."); indexMap.Add(xivBoneIndex, gltfBoneIndex); } From a059942bb2c4d4ae6efbbef19a92b36a512695a8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 12:57:56 +1100 Subject: [PATCH 1379/2451] Clean up + docs --- Penumbra/Import/Models/Export/MeshExporter.cs | 52 ++++++++++++++----- .../Import/Models/Export/ModelExporter.cs | 3 ++ Penumbra/Import/Models/Export/Skeleton.cs | 7 +++ Penumbra/Import/Models/ModelManager.cs | 1 + 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index e835fe62..cf7cc975 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Lumina.Data.Parsing; using Lumina.Extensions; +using OtterGui; using Penumbra.GameData.Files; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; @@ -25,7 +26,6 @@ public class MeshExporter public void AddToScene(SceneBuilder scene) { - // TODO: throw if mesh has skinned vertices but no joints are available? foreach (var mesh in _meshes) if (_joints == null) scene.AddRigidMesh(mesh, Matrix4x4.Identity); @@ -73,8 +73,11 @@ public class MeshExporter // If there's skinning usages but no bone mapping, there's probably something wrong with the data. if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); + + Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); } + /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap) { // A BoneTableIndex of 255 means that this mesh is not skinned. @@ -97,6 +100,7 @@ public class MeshExporter return indexMap; } + /// Build glTF meshes for this XIV mesh. private IMeshBuilder[] BuildMeshes() { var indices = BuildIndices(); @@ -105,16 +109,19 @@ public class MeshExporter // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. if (XivMesh.SubMeshCount == 0) - return [BuildMesh(indices, vertices, 0, (int)XivMesh.IndexCount)]; + return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; return _mdl.SubMeshes .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) - .Select(submesh => BuildMesh(indices, vertices, (int)(submesh.IndexOffset - XivMesh.StartIndex), (int)submesh.IndexCount)) + .WithIndex() + .Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount)) .ToArray(); } + /// Build a mesh from the provided indices and vertices. A subset of the full indices may be built by providing an index base and count. private IMeshBuilder BuildMesh( + string name, IReadOnlyList indices, IReadOnlyList vertices, int indexBase, @@ -127,7 +134,7 @@ public class MeshExporter _materialType, _skinningType ); - var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!; + var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, name)!; // TODO: share materials &c var materialBuilder = new MaterialBuilder() @@ -191,6 +198,7 @@ public class MeshExporter return meshBuilder; } + /// Read in the indices for this mesh. private IReadOnlyList BuildIndices() { var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData)); @@ -198,6 +206,7 @@ public class MeshExporter return reader.ReadStructuresAsArray((int)XivMesh.IndexCount); } + /// Build glTF-compatible vertex data for all vertices in this mesh. private IReadOnlyList BuildVertices() { var vertexBuilderType = typeof(VertexBuilder<,,>) @@ -224,7 +233,7 @@ public class MeshExporter attributes.Clear(); foreach (var (usage, element) in sortedElements) - attributes[usage] = ReadVertexAttribute(streams[element.Stream], element); + attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -237,9 +246,10 @@ public class MeshExporter return vertices; } - private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element) + /// Read a vertex attribute of the specified type from a vertex buffer stream. + private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { - return (MdlFile.VertexType)element.Type switch + return type switch { MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), @@ -252,6 +262,7 @@ public class MeshExporter }; } + /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlySet usages) { if (!usages.Contains(MdlFile.VertexUsage.Position)) @@ -266,6 +277,7 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } + /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) { if (_geometryType == typeof(VertexPosition)) @@ -289,6 +301,7 @@ public class MeshExporter throw new Exception($"Unknown geometry type {_geometryType}."); } + /// Get the vertex material type for this mesh's vertex usages. private Type GetMaterialType(IReadOnlySet usages) { // TODO: IIUC, xiv's uv2 is usually represented as the second two components of a vec4 uv attribute - add support. @@ -306,6 +319,7 @@ public class MeshExporter }; } + /// Build a material vertex from a vertex's attributes. private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) { if (_materialType == typeof(VertexEmpty)) @@ -326,15 +340,16 @@ public class MeshExporter throw new Exception($"Unknown material type {_skinningType}"); } + /// Get the vertex skinning type for this mesh's vertex usages. private Type GetSkinningType(IReadOnlySet usages) { - // TODO: possibly need to check only index - weight might be missing? if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices)) return typeof(VertexJoints4); return typeof(VertexEmpty); } + /// Build a skinning vertex from a vertex's attributes. private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) { if (_skinningType == typeof(VertexEmpty)) @@ -342,17 +357,21 @@ public class MeshExporter if (_skinningType == typeof(VertexJoints4)) { - // todo: this shouldn't happen... right? better approach? if (_boneIndexMap == null) - throw new Exception("cannot build skinned vertex without index mapping"); + throw new Exception("Tried to build skinned vertex but no bone mappings are available."); var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - // todo: if this throws on the bone index map, the mod is broken, as it contains weights for bones that do not exist. - // i've not seen any of these that even tt can understand var bindings = Enumerable.Range(0, 4) - .Select(index => (_boneIndexMap[indices[index]], weights[index])) + .Select(bindingIndex => { + // NOTE: I've not seen any files that throw this error that aren't completely broken. + var xivBoneIndex = indices[bindingIndex]; + if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) + throw new Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); + + return (jointIndex, weights[bindingIndex]); + }) .ToArray(); return new VertexJoints4(bindings); } @@ -360,10 +379,12 @@ public class MeshExporter throw new Exception($"Unknown skinning type {_skinningType}"); } - // Some tangent W values that should be -1 are stored as 0. + /// Clamps any tangent W value other than 1 to -1. + /// Some XIV models seemingly store -1 as 0, this patches over that. private Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; + /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private Vector2 ToVector2(object data) => data switch { @@ -373,6 +394,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}") }; + /// Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. private Vector3 ToVector3(object data) => data switch { @@ -382,6 +404,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + /// Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. private Vector4 ToVector4(object data) => data switch { @@ -391,6 +414,7 @@ public class MeshExporter _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") }; + /// Convert a vertex attribute value to a byte array. private byte[] ToByteArray(object data) => data switch { diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index c8716cf3..35819e7a 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -30,6 +30,7 @@ public class ModelExporter } } + /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; @@ -37,6 +38,7 @@ public class ModelExporter return new Model(meshes, gltfSkeleton); } + /// Convert a .mdl to a mesh (group) per LoD. private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) { var meshes = new List(); @@ -56,6 +58,7 @@ public class ModelExporter return meshes; } + /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) { NodeBuilder? root = null; diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index 13379dc4..09cdcc32 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -2,6 +2,7 @@ using SharpGLTF.Scenes; namespace Penumbra.Import.Models.Export; +/// Representation of a skeleton within XIV. public class XivSkeleton { public Bone[] Bones; @@ -25,9 +26,15 @@ public class XivSkeleton } } +/// Representation of a glTF-compatible skeleton. public struct GltfSkeleton { + /// Root node of the skeleton. public NodeBuilder Root; + + /// Flattened list of skeleton nodes. public NodeBuilder[] Joints; + + /// Mapping of bone names to their index within the joints array. public Dictionary Names; } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4f761549..e71b8baf 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -88,6 +88,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable gltfModel.SaveGLTF(_outputPath); } + /// Attempt to read out the pertinent information from a .sklb. private XivSkeleton? BuildSkeleton(CancellationToken cancel) { if (_sklb == null) From 9f981a3e52268c6b1c98959c9eb4c0d398ef8837 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 13:10:50 +1100 Subject: [PATCH 1380/2451] Render export errors --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 6 +++++- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 99c32761..31f2f7e0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -17,6 +17,7 @@ public partial class ModEditWindow private readonly List[] _attributes; public bool PendingIo { get; private set; } = false; + public string? IoException { get; private set; } = null; // TODO: this can probably be genericised across all of chara [GeneratedRegex(@"chara/equipment/e(?'Set'\d{4})/model/c(?'Race'\d{4})e\k'Set'_.+\.mdl", RegexOptions.Compiled)] @@ -74,7 +75,10 @@ public partial class ModEditWindow PendingIo = true; _edit._models.ExportToGltf(Mdl, sklb, outputPath) - .ContinueWith(_ => PendingIo = false); + .ContinueWith(task => { + IoException = task.Exception?.ToString(); + PendingIo = false; + }); } /// Try to find the .sklb path for a .mdl file. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index ff2c1ae5..57c47b8f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -42,6 +42,9 @@ public partial class ModEditWindow foreach (var gamePath in tab.GamePaths) ImGui.TextUnformatted(gamePath.ToString()); + if (tab.IoException != null) + ImGui.TextUnformatted(tab.IoException); + var ret = false; ret |= DrawModelMaterialDetails(tab, disabled); From 73ff3642fc7ae2e41234dc7eca6c33a3e6ae50a6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 13:30:04 +1100 Subject: [PATCH 1381/2451] Async game path resolution --- Penumbra/Import/Models/ModelManager.cs | 1 - .../ModEditWindow.Models.MdlTab.cs | 41 +++++++++++-------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 12 +++--- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index e71b8baf..217450dd 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -4,7 +4,6 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; using SharpGLTF.Scenes; -using SharpGLTF.Transforms; namespace Penumbra.Import.Models; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 31f2f7e0..ba6435c7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -13,7 +13,7 @@ public partial class ModEditWindow private ModEditWindow _edit; public readonly MdlFile Mdl; - public readonly List GamePaths; + public List? GamePaths { get; private set ;} private readonly List[] _attributes; public bool PendingIo { get; private set; } = false; @@ -28,8 +28,10 @@ public partial class ModEditWindow _edit = edit; Mdl = new MdlFile(bytes); - GamePaths = mod == null ? new() : FindGamePaths(path, mod); _attributes = CreateAttributes(Mdl); + + if (mod != null) + FindGamePaths(path, mod); } /// @@ -40,36 +42,41 @@ public partial class ModEditWindow public byte[] Write() => Mdl.Write(); - // TODO: this _needs_ to be done asynchronously, kart mods hang for a good second or so /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. /// Mod within which the .mdl is resolved. - private List FindGamePaths(string path, Mod mod) + private void FindGamePaths(string path, Mod mod) { - // todo: might be worth ordering based on prio + selection for disambiguating between multiple matches? not sure. same for the multi group case - return mod.AllSubMods - .SelectMany(submod => submod.Files.Concat(submod.FileSwaps)) - // todo: using ordinal ignore case because the option group paths in mods being lowerecased somewhere, but the mod editor using fs paths, which may be uppercase. i'd say this will blow up on linux, but it's already the case so can't be too much worse than present right - .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) - .Select(kv => kv.Key) - .ToList(); + PendingIo = true; + var task = Task.Run(() => { + // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? + // NOTE: We're using case insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. + return mod.AllSubMods + .SelectMany(submod => submod.Files.Concat(submod.FileSwaps)) + .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) + .Select(kv => kv.Key) + .ToList(); + }); + + task.ContinueWith(task => { + IoException = task.Exception?.ToString(); + PendingIo = false; + GamePaths = task.Result; + }); } /// Export model to an interchange format. /// Disk path to save the resulting file to. - public void Export(string outputPath) + public void Export(string outputPath, Utf8GamePath mdlPath) { - // NOTES ON EST (i don't think it's worth supporting yet...) + // NOTES ON EST // for collection wide lookup; // Collections.Cache.EstCache::GetEstEntry // Collections.Cache.MetaCache::GetEstEntry // Collections.ModCollection.MetaCache? // for default lookup, probably; // EstFile.GetDefault(...) - - // TODO: allow user to pick the gamepath in the ui - // TODO: what if there's no gamepaths? - var mdlPath = GamePaths.First(); + var sklbPath = GetSklbPath(mdlPath.ToString()); var sklb = sklbPath != null ? ReadSklb(sklbPath) : null; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 57c47b8f..9af2dd91 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -34,13 +34,13 @@ public partial class ModEditWindow ); } - if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", tab.PendingIo)) - { - tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf"); - } + if (tab.GamePaths != null) + if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", tab.PendingIo)) + tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf", tab.GamePaths.First()); ImGui.TextUnformatted("blippity blap"); - foreach (var gamePath in tab.GamePaths) - ImGui.TextUnformatted(gamePath.ToString()); + if (tab.GamePaths != null) + foreach (var gamePath in tab.GamePaths) + ImGui.TextUnformatted(gamePath.ToString()); if (tab.IoException != null) ImGui.TextUnformatted(tab.IoException); From bb9e7cac074e86987b939c115948cd85f2372e9a Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 14:21:38 +1100 Subject: [PATCH 1382/2451] Clean up UI --- Penumbra/Import/Models/ModelManager.cs | 1 - .../ModEditWindow.Models.MdlTab.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 73 ++++++++++++++++--- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 217450dd..35a5e53e 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -93,7 +93,6 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable if (_sklb == null) return null; - // TODO: work out how i handle this havok deal. running it outside the framework causes an immediate ctd. var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton)); xmlTask.Wait(cancel); var xml = xmlTask.Result; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index ba6435c7..38c1c6bd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -13,8 +13,10 @@ public partial class ModEditWindow private ModEditWindow _edit; public readonly MdlFile Mdl; - public List? GamePaths { get; private set ;} private readonly List[] _attributes; + + public List? GamePaths { get; private set ;} + public int GamePathIndex; public bool PendingIo { get; private set; } = false; public string? IoException { get; private set; } = null; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 9af2dd91..aa69953b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -34,16 +34,7 @@ public partial class ModEditWindow ); } - if (tab.GamePaths != null) - if (ImGuiUtil.DrawDisabledButton("bingo bango", Vector2.Zero, "description", tab.PendingIo)) - tab.Export("C:\\Users\\ackwell\\blender\\gltf-tests\\bingo.gltf", tab.GamePaths.First()); - ImGui.TextUnformatted("blippity blap"); - if (tab.GamePaths != null) - foreach (var gamePath in tab.GamePaths) - ImGui.TextUnformatted(gamePath.ToString()); - - if (tab.IoException != null) - ImGui.TextUnformatted(tab.IoException); + DrawExport(tab, disabled); var ret = false; @@ -58,6 +49,68 @@ public partial class ModEditWindow return !disabled && ret; } + private void DrawExport(MdlTab tab, bool disabled) + { + // IO on a disabled panel doesn't really make sense. + if (disabled) + return; + + if (!ImGui.CollapsingHeader("Export")) + return; + + if (tab.GamePaths == null) + { + if (tab.IoException == null) + ImGui.TextUnformatted("Resolving model game paths."); + else + ImGuiUtil.TextWrapped(tab.IoException); + + return; + } + + DrawGamePathCombo(tab); + + if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo)) + { + var gamePath = tab.GamePaths[tab.GamePathIndex]; + + _fileDialog.OpenSavePicker( + "Save model as glTF.", + ".gltf", + Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), + ".gltf", + (valid, path) => { + if (!valid) + return; + + tab.Export(path, gamePath); + }, + _mod!.ModPath.FullName, + false + ); + } + + if (tab.IoException != null) + ImGuiUtil.TextWrapped(tab.IoException); + + return; + } + + private void DrawGamePathCombo(MdlTab tab) + { + using var combo = ImRaii.Combo("Game Path", tab.GamePaths![tab.GamePathIndex].ToString()); + if (!combo) + return; + + foreach (var (path, index) in tab.GamePaths.WithIndex()) + { + if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex)) + continue; + + tab.GamePathIndex = index; + } + } + private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Materials")) From 215f8074833ed5c7e9550853fba58a08871b1659 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 23:52:37 +1100 Subject: [PATCH 1383/2451] Fix oversight in bone index mapping generation --- Penumbra/Import/Models/Export/MeshExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index cf7cc975..75283732 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -88,13 +88,13 @@ public class MeshExporter var indexMap = new Dictionary(); - foreach (var xivBoneIndex in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount)) + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) throw new Exception($"Armature does not contain bone \"{boneName}\" requested by mesh {_meshIndex}."); - indexMap.Add(xivBoneIndex, gltfBoneIndex); + indexMap.Add((ushort)tableIndex, gltfBoneIndex); } return indexMap; From d85cbd805124ad877a7cc45f418812c0fa43284c Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 2 Jan 2024 12:41:14 +1100 Subject: [PATCH 1384/2451] Add UV2 support --- Penumbra/Import/Models/Export/MeshExporter.cs | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 75283732..5f67114d 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -63,8 +63,10 @@ public class MeshExporter _boneIndexMap = BuildBoneIndexMap(boneNameMap); var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements - .Select(element => (MdlFile.VertexUsage)element.Usage) - .ToImmutableHashSet(); + .ToImmutableDictionary( + element => (MdlFile.VertexUsage)element.Usage, + element => (MdlFile.VertexType)element.Type + ); _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); @@ -263,15 +265,15 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlySet usages) + private Type GetGeometryType(IReadOnlyDictionary usages) { - if (!usages.Contains(MdlFile.VertexUsage.Position)) + if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw new Exception("Mesh does not contain position vertex elements."); - if (!usages.Contains(MdlFile.VertexUsage.Normal)) + if (!usages.ContainsKey(MdlFile.VertexUsage.Normal)) return typeof(VertexPosition); - if (!usages.Contains(MdlFile.VertexUsage.Tangent1)) + if (!usages.ContainsKey(MdlFile.VertexUsage.Tangent1)) return typeof(VertexPositionNormal); return typeof(VertexPositionNormalTangent); @@ -302,20 +304,32 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlySet usages) + private Type GetMaterialType(IReadOnlyDictionary usages) { - // TODO: IIUC, xiv's uv2 is usually represented as the second two components of a vec4 uv attribute - add support. + var uvCount = 0; + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) + uvCount = type switch + { + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, + _ => throw new Exception($"Unexpected UV vertex type {type}.") + }; + var materialUsages = ( - usages.Contains(MdlFile.VertexUsage.UV), - usages.Contains(MdlFile.VertexUsage.Color) + uvCount, + usages.ContainsKey(MdlFile.VertexUsage.Color) ); return materialUsages switch { - (true, true) => typeof(VertexColor1Texture1), - (true, false) => typeof(VertexTexture1), - (false, true) => typeof(VertexColor1), - (false, false) => typeof(VertexEmpty), + (2, true) => typeof(VertexColor1Texture2), + (2, false) => typeof(VertexTexture2), + (1, true) => typeof(VertexColor1Texture1), + (1, false) => typeof(VertexTexture1), + (0, true) => typeof(VertexColor1), + (0, false) => typeof(VertexEmpty), + + _ => throw new Exception("Unreachable."), }; } @@ -337,13 +351,34 @@ public class MeshExporter ToVector2(attributes[MdlFile.VertexUsage.UV]) ); + // XIV packs two UVs into a single vec4 attribute. + + if (_materialType == typeof(VertexTexture2)) + { + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + return new VertexTexture2( + new Vector2(uv.X, uv.Y), + new Vector2(uv.Z, uv.W) + ); + } + + if (_materialType == typeof(VertexColor1Texture2)) + { + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + return new VertexColor1Texture2( + ToVector4(attributes[MdlFile.VertexUsage.Color]), + new Vector2(uv.X, uv.Y), + new Vector2(uv.Z, uv.W) + ); + } + throw new Exception($"Unknown material type {_skinningType}"); } /// Get the vertex skinning type for this mesh's vertex usages. - private Type GetSkinningType(IReadOnlySet usages) + private Type GetSkinningType(IReadOnlyDictionary usages) { - if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices)) + if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) return typeof(VertexJoints4); return typeof(VertexEmpty); From 655e2fd2cae574b3536d86ea9b4649fe9561812b Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 2 Jan 2024 14:36:18 +1100 Subject: [PATCH 1385/2451] Flesh out skeleton path resolution a bit --- .../ModEditWindow.Models.MdlTab.cs | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index c4fb10f8..20a4129d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -21,10 +21,15 @@ public partial class ModEditWindow public bool PendingIo { get; private set; } = false; public string? IoException { get; private set; } = null; - // TODO: this can probably be genericised across all of chara - [GeneratedRegex(@"chara/equipment/e(?'Set'\d{4})/model/c(?'Race'\d{4})e\k'Set'_.+\.mdl", RegexOptions.Compiled)] + [GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] private static partial Regex CharaEquipmentRegex(); + [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] + private static partial Regex CharaHumanRegex(); + + [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", RegexOptions.Compiled)] + private static partial Regex CharaBodyRegex(); + public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) { _edit = edit; @@ -71,16 +76,14 @@ public partial class ModEditWindow /// Disk path to save the resulting file to. public void Export(string outputPath, Utf8GamePath mdlPath) { - // NOTES ON EST - // for collection wide lookup; - // Collections.Cache.EstCache::GetEstEntry - // Collections.Cache.MetaCache::GetEstEntry - // Collections.ModCollection.MetaCache? - // for default lookup, probably; - // EstFile.GetDefault(...) - - var sklbPath = GetSklbPath(mdlPath.ToString()); - var sklb = sklbPath != null ? ReadSklb(sklbPath) : null; + SklbFile? sklb = null; + try { + var sklbPath = GetSklbPath(mdlPath.ToString()); + sklb = sklbPath != null ? ReadSklb(sklbPath) : null; + } catch (Exception exception) { + IoException = exception?.ToString(); + return; + } PendingIo = true; _edit._models.ExportToGltf(Mdl, sklb, outputPath) @@ -94,15 +97,37 @@ public partial class ModEditWindow /// .mdl file to look up the skeleton for. private string? GetSklbPath(string mdlPath) { - // TODO: This needs to be drastically expanded, it's dodgy af rn - + // Equipment is skinned to the base body skeleton of the race they target. var match = CharaEquipmentRegex().Match(mdlPath); - if (!match.Success) - return null; + if (match.Success) + { + var race = match.Groups["Race"].Value; + return $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb"; + } - var race = match.Groups["Race"].Value; + // Some parts of human have their own skeletons. + match = CharaHumanRegex().Match(mdlPath); + if (match.Success) + { + var type = match.Groups["Type"].Value; + var race = match.Groups["Race"].Value; + return type switch + { + "body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb", + _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), + }; + } - return $"chara/human/c{race}/skeleton/base/b0001/skl_c{race}b0001.sklb"; + // A few subcategories - such as weapons, demihumans, and monsters - have dedicated per-"body" skeletons. + match = CharaBodyRegex().Match(mdlPath); + if (match.Success) + { + var subCategory = match.Groups["SubCategory"].Value; + var set = match.Groups["Set"].Value; + return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb"; + } + + return null; } /// Read a .sklb from the active collection or game. @@ -111,7 +136,7 @@ public partial class ModEditWindow { // TODO: if cross-collection lookups are turned off, this conversion can be skipped if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) - throw new Exception("TODO: handle - should it throw, or try to fail gracefully?"); + throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... @@ -121,7 +146,7 @@ public partial class ModEditWindow FullPath path => File.ReadAllBytes(path.ToPath()), }; if (bytes == null) - throw new Exception("TODO: handle - this effectively means that the resolved path doesn't exist. graceful?"); + throw new Exception($"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); return new SklbFile(bytes); } From e8e87cc6cbadec1616cac63dca7b7f2f2642d173 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 2 Jan 2024 14:36:24 +1100 Subject: [PATCH 1386/2451] Whoops --- Penumbra/Import/Models/Export/MeshExporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 5f67114d..1b53df8a 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -312,6 +312,7 @@ public class MeshExporter { MdlFile.VertexType.Half2 => 1, MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single4 => 2, _ => throw new Exception($"Unexpected UV vertex type {type}.") }; From b7edf521b62d205e43d82eef66c746a4b4e0f4f3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 19:27:04 +1100 Subject: [PATCH 1387/2451] SuzanneWalker --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 437 ++++++++++++++++++ .../ModEditWindow.Models.MdlTab.cs | 22 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 6 + 4 files changed, 461 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index db421413..821194d0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6 +Subproject commit 821194d0650a2dac98b7cbba9ff4a79e32b32d4d diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 35a5e53e..243390a7 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,9 +1,14 @@ using Dalamud.Plugin.Services; +using Lumina.Data.Parsing; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; using SharpGLTF.Scenes; +using SharpGLTF.Schema2; namespace Penumbra.Import.Models; @@ -54,6 +59,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); + public Task ImportGltf() + { + var action = new ImportGltfAction(); + return Enqueue(action).ContinueWith(_ => action.Out!); + } + private class ExportToGltfAction : IAction { private readonly ModelManager _manager; @@ -109,4 +120,430 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return true; } } + + private class ImportGltfAction : IAction + { + public MdlFile? Out; + + public ImportGltfAction() + { + // + } + + private ModelRoot Build() + { + // Build a super simple plane as a fake gltf input. + var material = new MaterialBuilder(); + var mesh = new MeshBuilder("mesh 0.0"); + var prim = mesh.UsePrimitive(material); + var tangent = new Vector4(.5f, .5f, 0, 1); + var vert1 = new VertexBuilder( + new VertexPositionNormalTangent(new Vector3(-1, 0, 1), Vector3.UnitY, tangent), + new VertexColor1Texture2(Vector4.One, Vector2.UnitY, Vector2.Zero), + new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) + ); + var vert2 = new VertexBuilder( + new VertexPositionNormalTangent(new Vector3(1, 0, 1), Vector3.UnitY, tangent), + new VertexColor1Texture2(Vector4.One, Vector2.One, Vector2.Zero), + new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) + ); + var vert3 = new VertexBuilder( + new VertexPositionNormalTangent(new Vector3(-1, 0, -1), Vector3.UnitY, tangent), + new VertexColor1Texture2(Vector4.One, Vector2.Zero, Vector2.Zero), + new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) + ); + var vert4 = new VertexBuilder( + new VertexPositionNormalTangent(new Vector3(1, 0, -1), Vector3.UnitY, tangent), + new VertexColor1Texture2(Vector4.One, Vector2.UnitX, Vector2.Zero), + new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) + ); + prim.AddTriangle(vert2, vert3, vert1); + prim.AddTriangle(vert2, vert4, vert3); + var jKosi = new NodeBuilder("j_kosi"); + var scene = new SceneBuilder(); + scene.AddNode(jKosi); + scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, [jKosi]); + var model = scene.ToGltf2(); + + return model; + } + + private (MdlStructs.VertexElement, Action>) GetPositionWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("POSITION", out var accessor)) + throw new Exception("todo: some error about position being hard required"); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, + }; + + IList values = accessor.AsVector3Array(); + + return ( + element, + (index, bytes) => WriteSingle3(values[index], bytes) + ); + } + + // TODO: probably should sanity check that if there's weights or indexes, both are available? game is always symmetric + private (MdlStructs.VertexElement, Action>)? GetBlendWeightWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. + private (MdlStructs.VertexElement, Action>)? GetBlendIndexWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteUInt(values[index], bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetNormalWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("NORMAL", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, + }; + + var values = accessor.AsVector3Array(); + + return ( + element, + (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetUvWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) + return null; + + // We're omitting type here, and filling it in on return, as there's two different types we might use. + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Usage = (byte)MdlFile.VertexUsage.UV, + }; + + var values1 = accessor1.AsVector2Array(); + + if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) + return ( + element with {Type = (byte)MdlFile.VertexType.Half2}, + (index, bytes) => WriteHalf2(values1[index], bytes) + ); + + var values2 = accessor2.AsVector2Array(); + + return ( + element with {Type = (byte)MdlFile.VertexType.Half4}, + (index, bytes) => { + var value1 = values1[index]; + var value2 = values2[index]; + WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + } + ); + } + + private (MdlStructs.VertexElement, Action>)? GetTangent1Writer(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("TANGENT", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private (MdlStructs.VertexElement, Action>)? GetColorWriter(IReadOnlyDictionary accessors) + { + if (!accessors.TryGetValue("COLOR_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, + }; + + var values = accessor.AsVector4Array(); + + return ( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private void WriteSingle3(Vector3 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes(input.X)); + bytes.AddRange(BitConverter.GetBytes(input.Y)); + bytes.AddRange(BitConverter.GetBytes(input.Z)); + } + + private void WriteUInt(Vector4 input, List bytes) + { + bytes.Add((byte)input.X); + bytes.Add((byte)input.Y); + bytes.Add((byte)input.Z); + bytes.Add((byte)input.W); + } + + private void WriteByteFloat4(Vector4 input, List bytes) + { + bytes.Add((byte)Math.Round(input.X * 255f)); + bytes.Add((byte)Math.Round(input.Y * 255f)); + bytes.Add((byte)Math.Round(input.Z * 255f)); + bytes.Add((byte)Math.Round(input.W * 255f)); + } + + private void WriteHalf2(Vector2 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + } + + private void WriteHalf4(Vector4 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); + bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + } + + private byte TypeSize(MdlFile.VertexType type) + { + return type switch + { + MdlFile.VertexType.Single3 => 12, + MdlFile.VertexType.Single4 => 16, + MdlFile.VertexType.UInt => 4, + MdlFile.VertexType.ByteFloat4 => 4, + MdlFile.VertexType.Half2 => 4, + MdlFile.VertexType.Half4 => 8, + + _ => throw new Exception($"Unhandled vertex type {type}"), + }; + } + + public void Execute(CancellationToken cancel) + { + var model = Build(); + + // --- + + // todo this'll need to check names and such. also loop. i'm relying on a single mesh here which is Wrong:tm: + var mesh = model.LogicalNodes + .Where(node => node.Mesh != null) + .Select(node => node.Mesh) + .First(); + + // todo check how many prims there are - maybe throw if more than one? not sure + var prim = mesh.Primitives[0]; + + var accessors = prim.VertexAccessors; + + var rawWriters = new[] { + GetPositionWriter(accessors), + GetBlendWeightWriter(accessors), + GetBlendIndexWriter(accessors), + GetNormalWriter(accessors), + GetTangent1Writer(accessors), + GetColorWriter(accessors), + GetUvWriter(accessors), + }; + + var writers = new List<(MdlStructs.VertexElement, Action>)>(); + var offsets = new byte[] {0, 0, 0}; + foreach (var writer in rawWriters) + { + if (writer == null) continue; + var element = writer.Value.Item1; + writers.Add(( + element with {Offset = offsets[element.Stream]}, + writer.Value.Item2 + )); + offsets[element.Stream] += TypeSize((MdlFile.VertexType)element.Type); + } + var strides = offsets; + + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + + // todo: this is a bit lmao but also... probably the most sane option? getting the count that is + var vertexCount = prim.VertexAccessors["POSITION"].Count; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + foreach (var (element, writer) in writers) + { + writer(vertexIndex, streams[element.Stream]); + } + } + + // indices + var indexCount = prim.GetIndexAccessor().Count; + var indices = prim.GetIndices() + .SelectMany(index => BitConverter.GetBytes((ushort)index)) + .ToArray(); + + var dataBuffer = streams[0].Concat(streams[1]).Concat(streams[2]).Concat(indices); + + var lod1VertLen = (uint)(streams[0].Count + streams[1].Count + streams[2].Count); + + var mdl = new MdlFile() + { + Radius = 1, + // todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow. + VertexOffset = [0, 0, 0], + IndexOffset = [lod1VertLen, 0, 0], + VertexBufferSize = [lod1VertLen, 0, 0], + IndexBufferSize = [(uint)indices.Length, 0, 0], + LodCount = 1, + BoundingBoxes = new MdlStructs.BoundingBoxStruct() + { + Min = [-1, 0, -1, 1], + Max = [1, 0, 1, 1], + }, + VertexDeclarations = [new MdlStructs.VertexDeclarationStruct() + { + VertexElements = writers.Select(x => x.Item1).ToArray(), + }], + Meshes = [new MdlStructs.MeshStruct() + { + VertexCount = (ushort)vertexCount, + IndexCount = (uint)indexCount, + MaterialIndex = 0, + SubMeshIndex = 0, + SubMeshCount = 1, + BoneTableIndex = 0, + StartIndex = 0, + // todo: this will need to be composed down across multiple submeshes. given submeshes store contiguous buffers + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + VertexStreamCount = 2, + }], + BoneTables = [new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }], + BoneBoundingBoxes = [ + // new MdlStructs.BoundingBoxStruct() + // { + // Min = [ + // -0.081672676f, + // -0.113717034f, + // -0.11905348f, + // 1.0f, + // ], + // Max = [ + // 0.03941727f, + // 0.09845419f, + // 0.107391916f, + // 1.0f, + // ], + // }, + + // _would_ be nice if i didn't need to fill out this + new MdlStructs.BoundingBoxStruct() + { + Min = [0, 0, 0, 0], + Max = [0, 0, 0, 0], + } + ], + SubMeshes = [new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)indexCount, + AttributeIndexMask = 0, + BoneStartIndex = 0, + BoneCount = 1, + }], + + // TODO pretty sure this is garbage data as far as textools functions + // game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh + SubMeshBoneMap = [0], + + Lods = [new MdlStructs.LodStruct() + { + MeshIndex = 0, + MeshCount = 1, + ModelLodRange = 0, + TextureLodRange = 0, + VertexBufferSize = lod1VertLen, + VertexDataOffset = 0, + IndexBufferSize = (uint)indexCount, + IndexDataOffset = lod1VertLen, + }, + ], + Bones = [ + "j_kosi", + ], + Materials = [ + "/mt_c0201e6180_top_b.mtrl", + ], + RemainingData = dataBuffer.ToArray(), + }; + + Out = mdl; + } + + public bool Equals(IAction? other) + { + if (other is not ImportGltfAction rhs) + return false; + + return true; + } + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 20a4129d..5d9abda6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -12,10 +12,10 @@ public partial class ModEditWindow { private ModEditWindow _edit; - public readonly MdlFile Mdl; - private readonly List[] _attributes; + public MdlFile Mdl { get; private set; } + private List[] _attributes; - public List? GamePaths { get; private set ;} + public List? GamePaths { get; private set; } public int GamePathIndex; public bool PendingIo { get; private set; } = false; @@ -34,13 +34,19 @@ public partial class ModEditWindow { _edit = edit; - Mdl = new MdlFile(bytes); - _attributes = CreateAttributes(Mdl); + Initialize(new MdlFile(bytes)); if (mod != null) FindGamePaths(path, mod); } + [MemberNotNull(nameof(Mdl), nameof(_attributes))] + private void Initialize(MdlFile mdl) + { + Mdl = mdl; + _attributes = CreateAttributes(Mdl); + } + /// public bool Valid => Mdl.Valid; @@ -72,6 +78,12 @@ public partial class ModEditWindow }); } + public void Import() + { + // TODO: this needs to be fleshed out a bunch. + _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result)); + } + /// Export model to an interchange format. /// Disk path to save the resulting file to. public void Export(string outputPath, Utf8GamePath mdlPath) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index aa69953b..d59cf1e5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -37,6 +37,12 @@ public partial class ModEditWindow DrawExport(tab, disabled); var ret = false; + + if (ImGui.Button("import test")) + { + tab.Import(); + ret |= true; + } ret |= DrawModelMaterialDetails(tab, disabled); From b3fe538219bf3e7169a020719c4c44046a075e2b Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 21:47:48 +1100 Subject: [PATCH 1388/2451] Split vertex attribute logic into seperate file --- .../Import/Models/Import/VertexAttribute.cs | 232 ++++++++++++++++ Penumbra/Import/Models/ModelManager.cs | 247 ++---------------- 2 files changed, 253 insertions(+), 226 deletions(-) create mode 100644 Penumbra/Import/Models/Import/VertexAttribute.cs diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs new file mode 100644 index 00000000..7c605ba8 --- /dev/null +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -0,0 +1,232 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +using Writer = Action>; +using Accessors = IReadOnlyDictionary; + +public class VertexAttribute +{ + /// XIV vertex element metadata structure. + public readonly MdlStructs.VertexElement Element; + /// Write this vertex attribute's value at the specified index to the provided byte array. + public readonly Writer Write; + + /// Size in bytes of a single vertex's attribute value. + public byte Size => (MdlFile.VertexType)Element.Type switch + { + MdlFile.VertexType.Single3 => 12, + MdlFile.VertexType.Single4 => 16, + MdlFile.VertexType.UInt => 4, + MdlFile.VertexType.ByteFloat4 => 4, + MdlFile.VertexType.Half2 => 4, + MdlFile.VertexType.Half4 => 8, + + _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), + }; + + public VertexAttribute(MdlStructs.VertexElement element, Writer write) + { + Element = element; + Write = write; + } + + public static VertexAttribute Position(Accessors accessors) + { + if (!accessors.TryGetValue("POSITION", out var accessor)) + throw new Exception("Meshes must contain a POSITION attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, + }; + + var values = accessor.AsVector3Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteSingle3(values[index], bytes) + ); + } + + public static VertexAttribute? BlendWeight(Accessors accessors) + { + if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + return null; + + if (!accessors.ContainsKey("JOINTS_0")) + throw new Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. + public static VertexAttribute? BlendIndex(Accessors accessors) + { + if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + return null; + + if (!accessors.ContainsKey("WEIGHTS_0")) + throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteUInt(values[index], bytes) + ); + } + + public static VertexAttribute? Normal(Accessors accessors) + { + if (!accessors.TryGetValue("NORMAL", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, + }; + + var values = accessor.AsVector3Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + ); + } + + public static VertexAttribute? Uv(Accessors accessors) + { + if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) + return null; + + // We're omitting type here, and filling it in on return, as there's two different types we might use. + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Usage = (byte)MdlFile.VertexUsage.UV, + }; + + var values1 = accessor1.AsVector2Array(); + + if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) + return new VertexAttribute( + element with { Type = (byte)MdlFile.VertexType.Half2 }, + (index, bytes) => WriteHalf2(values1[index], bytes) + ); + + var values2 = accessor2.AsVector2Array(); + + return new VertexAttribute( + element with { Type = (byte)MdlFile.VertexType.Half4 }, + (index, bytes) => + { + var value1 = values1[index]; + var value2 = values2[index]; + WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + } + ); + } + + public static VertexAttribute? Tangent1(Accessors accessors) + { + if (!accessors.TryGetValue("TANGENT", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + public static VertexAttribute? Color(Accessors accessors) + { + if (!accessors.TryGetValue("COLOR_0", out var accessor)) + return null; + + var element = new MdlStructs.VertexElement() + { + Stream = 1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, + }; + + var values = accessor.AsVector4Array(); + + return new VertexAttribute( + element, + (index, bytes) => WriteByteFloat4(values[index], bytes) + ); + } + + private static void WriteSingle3(Vector3 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes(input.X)); + bytes.AddRange(BitConverter.GetBytes(input.Y)); + bytes.AddRange(BitConverter.GetBytes(input.Z)); + } + + private static void WriteUInt(Vector4 input, List bytes) + { + bytes.Add((byte)input.X); + bytes.Add((byte)input.Y); + bytes.Add((byte)input.Z); + bytes.Add((byte)input.W); + } + + private static void WriteByteFloat4(Vector4 input, List bytes) + { + bytes.Add((byte)Math.Round(input.X * 255f)); + bytes.Add((byte)Math.Round(input.Y * 255f)); + bytes.Add((byte)Math.Round(input.Z * 255f)); + bytes.Add((byte)Math.Round(input.W * 255f)); + } + + private static void WriteHalf2(Vector2 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + } + + private static void WriteHalf4(Vector4 input, List bytes) + { + bytes.AddRange(BitConverter.GetBytes((Half)input.X)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); + bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 243390a7..e5349308 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -4,6 +4,7 @@ using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; +using Penumbra.Import.Models.Import; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; @@ -127,7 +128,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public ImportGltfAction() { - // + // } private ModelRoot Build() @@ -168,212 +169,6 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable return model; } - private (MdlStructs.VertexElement, Action>) GetPositionWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("POSITION", out var accessor)) - throw new Exception("todo: some error about position being hard required"); - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.Single3, - Usage = (byte)MdlFile.VertexUsage.Position, - }; - - IList values = accessor.AsVector3Array(); - - return ( - element, - (index, bytes) => WriteSingle3(values[index], bytes) - ); - } - - // TODO: probably should sanity check that if there's weights or indexes, both are available? game is always symmetric - private (MdlStructs.VertexElement, Action>)? GetBlendWeightWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. - private (MdlStructs.VertexElement, Action>)? GetBlendIndexWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("JOINTS_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UInt, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteUInt(values[index], bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetNormalWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("NORMAL", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.Half4, - Usage = (byte)MdlFile.VertexUsage.Normal, - }; - - var values = accessor.AsVector3Array(); - - return ( - element, - (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetUvWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1)) - return null; - - // We're omitting type here, and filling it in on return, as there's two different types we might use. - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Usage = (byte)MdlFile.VertexUsage.UV, - }; - - var values1 = accessor1.AsVector2Array(); - - if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) - return ( - element with {Type = (byte)MdlFile.VertexType.Half2}, - (index, bytes) => WriteHalf2(values1[index], bytes) - ); - - var values2 = accessor2.AsVector2Array(); - - return ( - element with {Type = (byte)MdlFile.VertexType.Half4}, - (index, bytes) => { - var value1 = values1[index]; - var value2 = values2[index]; - WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); - } - ); - } - - private (MdlStructs.VertexElement, Action>)? GetTangent1Writer(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("TANGENT", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Tangent1, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - private (MdlStructs.VertexElement, Action>)? GetColorWriter(IReadOnlyDictionary accessors) - { - if (!accessors.TryGetValue("COLOR_0", out var accessor)) - return null; - - var element = new MdlStructs.VertexElement() - { - Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Color, - }; - - var values = accessor.AsVector4Array(); - - return ( - element, - (index, bytes) => WriteByteFloat4(values[index], bytes) - ); - } - - private void WriteSingle3(Vector3 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes(input.X)); - bytes.AddRange(BitConverter.GetBytes(input.Y)); - bytes.AddRange(BitConverter.GetBytes(input.Z)); - } - - private void WriteUInt(Vector4 input, List bytes) - { - bytes.Add((byte)input.X); - bytes.Add((byte)input.Y); - bytes.Add((byte)input.Z); - bytes.Add((byte)input.W); - } - - private void WriteByteFloat4(Vector4 input, List bytes) - { - bytes.Add((byte)Math.Round(input.X * 255f)); - bytes.Add((byte)Math.Round(input.Y * 255f)); - bytes.Add((byte)Math.Round(input.Z * 255f)); - bytes.Add((byte)Math.Round(input.W * 255f)); - } - - private void WriteHalf2(Vector2 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - } - - private void WriteHalf4(Vector4 input, List bytes) - { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); - bytes.AddRange(BitConverter.GetBytes((Half)input.W)); - } - - private byte TypeSize(MdlFile.VertexType type) - { - return type switch - { - MdlFile.VertexType.Single3 => 12, - MdlFile.VertexType.Single4 => 16, - MdlFile.VertexType.UInt => 4, - MdlFile.VertexType.ByteFloat4 => 4, - MdlFile.VertexType.Half2 => 4, - MdlFile.VertexType.Half4 => 8, - - _ => throw new Exception($"Unhandled vertex type {type}"), - }; - } - public void Execute(CancellationToken cancel) { var model = Build(); @@ -391,27 +186,27 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var accessors = prim.VertexAccessors; - var rawWriters = new[] { - GetPositionWriter(accessors), - GetBlendWeightWriter(accessors), - GetBlendIndexWriter(accessors), - GetNormalWriter(accessors), - GetTangent1Writer(accessors), - GetColorWriter(accessors), - GetUvWriter(accessors), + var rawAttributes = new[] { + VertexAttribute.Position(accessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors), + VertexAttribute.Normal(accessors), + VertexAttribute.Tangent1(accessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), }; - var writers = new List<(MdlStructs.VertexElement, Action>)>(); + var attributes = new List(); var offsets = new byte[] {0, 0, 0}; - foreach (var writer in rawWriters) + foreach (var attribute in rawAttributes) { - if (writer == null) continue; - var element = writer.Value.Item1; - writers.Add(( + if (attribute == null) continue; + var element = attribute.Element; + attributes.Add(new VertexAttribute( element with {Offset = offsets[element.Stream]}, - writer.Value.Item2 + attribute.Write )); - offsets[element.Stream] += TypeSize((MdlFile.VertexType)element.Type); + offsets[element.Stream] += attribute.Size; } var strides = offsets; @@ -423,9 +218,9 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var vertexCount = prim.VertexAccessors["POSITION"].Count; for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { - foreach (var (element, writer) in writers) + foreach (var attribute in attributes) { - writer(vertexIndex, streams[element.Stream]); + attribute.Write(vertexIndex, streams[attribute.Element.Stream]); } } @@ -455,7 +250,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable }, VertexDeclarations = [new MdlStructs.VertexDeclarationStruct() { - VertexElements = writers.Select(x => x.Item1).ToArray(), + VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), }], Meshes = [new MdlStructs.MeshStruct() { @@ -530,7 +325,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable "j_kosi", ], Materials = [ - "/mt_c0201e6180_top_b.mtrl", + "/mt_c0201e6180_top_a.mtrl", ], RemainingData = dataBuffer.ToArray(), }; From 79de6f1714730efcde6cbf861d43d171d51d9f35 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 4 Jan 2024 23:33:54 +1100 Subject: [PATCH 1389/2451] Basic multi mesh handling --- .../Import/Models/Import/VertexAttribute.cs | 8 +- Penumbra/Import/Models/ModelManager.cs | 285 ++++++++++++------ 2 files changed, 194 insertions(+), 99 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 7c605ba8..430bc422 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -72,7 +72,9 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS + // (index, bytes) => WriteByteFloat4(values[index], bytes) + (index, bytes) => WriteByteFloat4(Vector4.UnitX, bytes) ); } @@ -96,7 +98,9 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteUInt(values[index], bytes) + // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS + // (index, bytes) => WriteUInt(values[index], bytes) + (index, bytes) => WriteUInt(Vector4.Zero, bytes) ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index e5349308..66f5202e 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -171,107 +171,75 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken cancel) { - var model = Build(); + var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); - // --- - - // todo this'll need to check names and such. also loop. i'm relying on a single mesh here which is Wrong:tm: - var mesh = model.LogicalNodes + // TODO: for grouping, should probably use `node.name ?? mesh.name`, as which are set seems to depend on the exporter. + var nodes = model.LogicalNodes .Where(node => node.Mesh != null) - .Select(node => node.Mesh) - .First(); + // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that. + .Take(3); - // todo check how many prims there are - maybe throw if more than one? not sure - var prim = mesh.Primitives[0]; - - var accessors = prim.VertexAccessors; - - var rawAttributes = new[] { - VertexAttribute.Position(accessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors), - VertexAttribute.Normal(accessors), - VertexAttribute.Tangent1(accessors), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; - - var attributes = new List(); - var offsets = new byte[] {0, 0, 0}; - foreach (var attribute in rawAttributes) + // this is a representation of a single LoD + var vertexDeclarations = new List(); + var boneTables = new List(); + var meshes = new List(); + var submeshes = new List(); + var vertexBuffer = new List(); + var indices = new List(); + + foreach (var node in nodes) { - if (attribute == null) continue; - var element = attribute.Element; - attributes.Add(new VertexAttribute( - element with {Offset = offsets[element.Stream]}, - attribute.Write - )); - offsets[element.Stream] += attribute.Size; + var boneTableOffset = boneTables.Count; + var meshOffset = meshes.Count; + var subOffset = submeshes.Count; + var vertOffset = vertexBuffer.Count; + var idxOffset = indices.Count; + + var ( + vertexDeclaration, + boneTable, + xivMesh, + xivSubmesh, + meshVertexBuffer, + meshIndices + ) = MeshThing(node); + + vertexDeclarations.Add(vertexDeclaration); + boneTables.Add(boneTable); + meshes.Add(xivMesh with { + SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), + // TODO: should probably define a type for index type hey. + BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), + StartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)), + VertexBufferOffset = xivMesh.VertexBufferOffset + .Select(offset => (uint)(offset + vertOffset)) + .ToArray(), + }); + submeshes.Add(xivSubmesh with { + // TODO: this will need to keep ticking up for each submesh in the same mesh + IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) + }); + vertexBuffer.AddRange(meshVertexBuffer); + indices.AddRange(meshIndices); } - var strides = offsets; - - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - - // todo: this is a bit lmao but also... probably the most sane option? getting the count that is - var vertexCount = prim.VertexAccessors["POSITION"].Count; - for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) - { - foreach (var attribute in attributes) - { - attribute.Write(vertexIndex, streams[attribute.Element.Stream]); - } - } - - // indices - var indexCount = prim.GetIndexAccessor().Count; - var indices = prim.GetIndices() - .SelectMany(index => BitConverter.GetBytes((ushort)index)) - .ToArray(); - - var dataBuffer = streams[0].Concat(streams[1]).Concat(streams[2]).Concat(indices); - - var lod1VertLen = (uint)(streams[0].Count + streams[1].Count + streams[2].Count); var mdl = new MdlFile() { Radius = 1, // todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow. VertexOffset = [0, 0, 0], - IndexOffset = [lod1VertLen, 0, 0], - VertexBufferSize = [lod1VertLen, 0, 0], - IndexBufferSize = [(uint)indices.Length, 0, 0], + IndexOffset = [(uint)vertexBuffer.Count, 0, 0], + VertexBufferSize = [(uint)vertexBuffer.Count, 0, 0], + IndexBufferSize = [(uint)indices.Count, 0, 0], LodCount = 1, BoundingBoxes = new MdlStructs.BoundingBoxStruct() { Min = [-1, 0, -1, 1], Max = [1, 0, 1, 1], }, - VertexDeclarations = [new MdlStructs.VertexDeclarationStruct() - { - VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), - }], - Meshes = [new MdlStructs.MeshStruct() - { - VertexCount = (ushort)vertexCount, - IndexCount = (uint)indexCount, - MaterialIndex = 0, - SubMeshIndex = 0, - SubMeshCount = 1, - BoneTableIndex = 0, - StartIndex = 0, - // todo: this will need to be composed down across multiple submeshes. given submeshes store contiguous buffers - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - VertexStreamCount = 2, - }], - BoneTables = [new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }], + VertexDeclarations = vertexDeclarations.ToArray(), + Meshes = meshes.ToArray(), + BoneTables = boneTables.ToArray(), BoneBoundingBoxes = [ // new MdlStructs.BoundingBoxStruct() // { @@ -296,14 +264,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Max = [0, 0, 0, 0], } ], - SubMeshes = [new MdlStructs.SubmeshStruct() - { - IndexOffset = 0, - IndexCount = (uint)indexCount, - AttributeIndexMask = 0, - BoneStartIndex = 0, - BoneCount = 1, - }], + SubMeshes = submeshes.ToArray(), // TODO pretty sure this is garbage data as far as textools functions // game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh @@ -312,13 +273,13 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Lods = [new MdlStructs.LodStruct() { MeshIndex = 0, - MeshCount = 1, + MeshCount = (ushort)meshes.Count, ModelLodRange = 0, TextureLodRange = 0, - VertexBufferSize = lod1VertLen, + VertexBufferSize = (uint)vertexBuffer.Count, VertexDataOffset = 0, - IndexBufferSize = (uint)indexCount, - IndexDataOffset = lod1VertLen, + IndexBufferSize = (uint)indices.Count, + IndexDataOffset = (uint)vertexBuffer.Count, }, ], Bones = [ @@ -327,12 +288,142 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Materials = [ "/mt_c0201e6180_top_a.mtrl", ], - RemainingData = dataBuffer.ToArray(), + RemainingData = vertexBuffer.Concat(indices).ToArray(), }; Out = mdl; } + // this return type is an absolute meme, class that shit up. + public ( + MdlStructs.VertexDeclarationStruct, + MdlStructs.BoneTableStruct, + MdlStructs.MeshStruct, + MdlStructs.SubmeshStruct, + IEnumerable, + IEnumerable + ) MeshThing(Node node) + { + // BoneTable (mesh.btidx = 255 means unskinned) + // vertexdecl + + var mesh = node.Mesh; + + // TODO: should probably say _what_ mesh + // TODO: would be cool to support >1 primitive (esp. given they're effectively what submeshes are modeled as), but blender doesn't really use them, so not going to prio that at all. + if (mesh.Primitives.Count != 1) + throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); + var primitive = mesh.Primitives[0]; + + var accessors = primitive.VertexAccessors; + + var rawAttributes = new[] { + VertexAttribute.Position(accessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors), + VertexAttribute.Normal(accessors), + VertexAttribute.Tangent1(accessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + var offsets = new byte[] {0, 0, 0}; + foreach (var attribute in rawAttributes) + { + if (attribute == null) continue; + var element = attribute.Element; + attributes.Add(new VertexAttribute( + element with {Offset = offsets[element.Stream]}, + attribute.Write + )); + offsets[element.Stream] += attribute.Size; + } + var strides = offsets; + + // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. + + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + + // todo: this is a bit lmao but also... probably the most sane option? getting the count that is + var vertexCount = primitive.VertexAccessors["POSITION"].Count; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + foreach (var attribute in attributes) + { + attribute.Write(vertexIndex, streams[attribute.Element.Stream]); + } + } + + // indices + var indexCount = primitive.GetIndexAccessor().Count; + var indices = primitive.GetIndices() + .SelectMany(index => BitConverter.GetBytes((ushort)index)) + .ToArray(); + + // one of these per mesh + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() + { + VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), + }; + + // one of these per skinned mesh. + // TODO: check if mesh has skinning at all. + var boneTable = new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }; + + // mesh + var xivMesh = new MdlStructs.MeshStruct() + { + // TODO: sum across submeshes. + // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + VertexCount = (ushort)vertexCount, + IndexCount = (uint)indexCount, + // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + MaterialIndex = 0, + // TODO: this will need adjusting by parent + SubMeshIndex = 0, + SubMeshCount = 1, + // TODO: update in parent + BoneTableIndex = 0, + // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + StartIndex = 0, + // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + }; + + // submesh + // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. + var xivSubmesh = new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)indexCount, + AttributeIndexMask = 0, + // TODO: not sure how i want to handle these ones + BoneStartIndex = 0, + BoneCount = 1, + }; + + var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); + + return ( + vertexDeclaration, + boneTable, + xivMesh, + xivSubmesh, + vertexBuffer, + indices + ); + } + public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 4e8695e7a4e1e6c18b13b93773eb391fa09d06ef Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 01:03:54 +1100 Subject: [PATCH 1390/2451] Spike submeshes --- Penumbra/Import/Models/ModelManager.cs | 209 ++++++++++++++++++------- 1 file changed, 155 insertions(+), 54 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 66f5202e..43630f6e 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; +using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; @@ -13,7 +14,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed class ModelManager : SingleTaskQueue, IDisposable +public sealed partial class ModelManager : SingleTaskQueue, IDisposable { private readonly IFramework _framework; private readonly IDataManager _gameData; @@ -122,8 +123,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } } - private class ImportGltfAction : IAction + private partial class ImportGltfAction : IAction { + // TODO: clean this up a bit, i don't actually need all of it. + [GeneratedRegex(@".*[_ ^](?'Mesh'[0-9]+)[\\.\\-]?([0-9]+)?$", RegexOptions.Compiled)] + private static partial Regex MeshNameGroupingRegex(); + public MdlFile? Out; public ImportGltfAction() @@ -174,10 +179,25 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); // TODO: for grouping, should probably use `node.name ?? mesh.name`, as which are set seems to depend on the exporter. + // var nodes = model.LogicalNodes + // .Where(node => node.Mesh != null) + // // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that. + // .Take(3); + + // tt uses this + // ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" var nodes = model.LogicalNodes .Where(node => node.Mesh != null) - // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that. - .Take(3); + .Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 + .SelectWhere(node => { + var name = node.Name ?? node.Mesh.Name; + var match = MeshNameGroupingRegex().Match(name); + return match.Success + ? (true, (node, int.Parse(match.Groups["Mesh"].Value))) + : (false, (node, -1)); + }) + .GroupBy(pair => pair.Item2, pair => pair.node) + .OrderBy(group => group.Key); // this is a representation of a single LoD var vertexDeclarations = new List(); @@ -187,7 +207,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var vertexBuffer = new List(); var indices = new List(); - foreach (var node in nodes) + foreach (var submeshnodes in nodes) { var boneTableOffset = boneTables.Count; var meshOffset = meshes.Count; @@ -199,10 +219,10 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable vertexDeclaration, boneTable, xivMesh, - xivSubmesh, + xivSubmeshes, meshVertexBuffer, meshIndices - ) = MeshThing(node); + ) = MeshThing(submeshnodes); vertexDeclarations.Add(vertexDeclaration); boneTables.Add(boneTable); @@ -215,12 +235,14 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable .Select(offset => (uint)(offset + vertOffset)) .ToArray(), }); - submeshes.Add(xivSubmesh with { - // TODO: this will need to keep ticking up for each submesh in the same mesh - IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) - }); + // TODO: could probably do this with linq cleaner + foreach (var xivSubmesh in xivSubmeshes) + submeshes.Add(xivSubmesh with { + // TODO: this will need to keep ticking up for each submesh in the same mesh + IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) + }); vertexBuffer.AddRange(meshVertexBuffer); - indices.AddRange(meshIndices); + indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); } var mdl = new MdlFile() @@ -295,14 +317,99 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // this return type is an absolute meme, class that shit up. - public ( + private ( MdlStructs.VertexDeclarationStruct, MdlStructs.BoneTableStruct, MdlStructs.MeshStruct, - MdlStructs.SubmeshStruct, + IEnumerable, IEnumerable, - IEnumerable - ) MeshThing(Node node) + IEnumerable + ) MeshThing(IEnumerable nodes) + { + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; + var vertexCount = (ushort)0; + // there's gotta be a better way to do this with streams or enumerables or something, surely + var streams = new List[3]; + for (var i = 0; i < 3; i++) + streams[i] = new List(); + var indexCount = (uint)0; + var indices = new List(); + var strides = new byte[] {0, 0, 0}; + var submeshes = new List(); + + // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. + foreach (var node in nodes) + { + var vertOff = vertexCount; + var idxOff = indexCount; + + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs) = NodeMeshThing(node); + vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST + strides = newStrides; // ALSO CHECK EQUAL + vertexCount += vertCount; + for (var i = 0; i < 3; i++) + streams[i].AddRange(vertStreams[i]); + indexCount += idxCount; + // we need to offset the indexes to point into the new stuff + indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); + submeshes.Add(submesh with { + IndexOffset = submesh.IndexOffset + idxOff + // TODO: bone stuff probably + }); + } + + // one of these per skinned mesh. + // TODO: check if mesh has skinning at all. (err if mixed?) + var boneTable = new MdlStructs.BoneTableStruct() + { + BoneCount = 1, + // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + BoneIndex = new ushort[64], + }; + + // mesh + var xivMesh = new MdlStructs.MeshStruct() + { + // TODO: sum across submeshes. + // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + VertexCount = vertexCount, + IndexCount = indexCount, + // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + MaterialIndex = 0, + // TODO: this will need adjusting by parent + SubMeshIndex = 0, + SubMeshCount = (ushort)submeshes.Count, + // TODO: update in parent + BoneTableIndex = 0, + // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + StartIndex = 0, + // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + VertexBufferStride = strides, + // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) + }; + + return ( + vertexDeclaration, + boneTable, + xivMesh, + submeshes, + streams[0].Concat(streams[1]).Concat(streams[2]), + indices + ); + } + + private ( + MdlStructs.VertexDeclarationStruct, + byte[], + // MdlStructs.MeshStruct, + MdlStructs.SubmeshStruct, + ushort, + IEnumerable[], + uint, + IEnumerable + ) NodeMeshThing(Node node) { // BoneTable (mesh.btidx = 255 means unskinned) // vertexdecl @@ -358,10 +465,11 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable } // indices - var indexCount = primitive.GetIndexAccessor().Count; - var indices = primitive.GetIndices() - .SelectMany(index => BitConverter.GetBytes((ushort)index)) - .ToArray(); + // var indexCount = primitive.GetIndexAccessor().Count; + // var indices = primitive.GetIndices() + // .SelectMany(index => BitConverter.GetBytes((ushort)index)) + // .ToArray(); + var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); // one of these per mesh var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() @@ -369,57 +477,50 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), }; - // one of these per skinned mesh. - // TODO: check if mesh has skinning at all. - var boneTable = new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }; - // mesh - var xivMesh = new MdlStructs.MeshStruct() - { - // TODO: sum across submeshes. - // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - VertexCount = (ushort)vertexCount, - IndexCount = (uint)indexCount, - // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - MaterialIndex = 0, - // TODO: this will need adjusting by parent - SubMeshIndex = 0, - SubMeshCount = 1, - // TODO: update in parent - BoneTableIndex = 0, - // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - StartIndex = 0, - // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - }; + // var xivMesh = new MdlStructs.MeshStruct() + // { + // // TODO: sum across submeshes. + // // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. + // VertexCount = (ushort)vertexCount, + // IndexCount = (uint)indexCount, + // // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? + // MaterialIndex = 0, + // // TODO: this will need adjusting by parent + // SubMeshIndex = 0, + // SubMeshCount = 1, + // // TODO: update in parent + // BoneTableIndex = 0, + // // TODO: this is relative to the lod's index buffer, and is an index, not byte offset + // StartIndex = 0, + // // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust + // VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], + // VertexBufferStride = strides, + // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), + // }; // submesh // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. var xivSubmesh = new MdlStructs.SubmeshStruct() { IndexOffset = 0, - IndexCount = (uint)indexCount, + IndexCount = (uint)indices.Length, AttributeIndexMask = 0, // TODO: not sure how i want to handle these ones BoneStartIndex = 0, BoneCount = 1, }; - var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); + // var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); return ( vertexDeclaration, - boneTable, - xivMesh, + strides, + // xivMesh, xivSubmesh, - vertexBuffer, + (ushort)vertexCount, + streams, + (uint)indices.Length, indices ); } From acaa49fec5520d5b0d8ce32b84b0cd9b14fb4eed Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 15:32:31 +1100 Subject: [PATCH 1391/2451] Add shape key support --- .../Import/Models/Import/VertexAttribute.cs | 132 +++++++++---- Penumbra/Import/Models/ModelManager.cs | 176 +++++++++++++++++- 2 files changed, 260 insertions(+), 48 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 430bc422..9fd50513 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -4,7 +4,9 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -using Writer = Action>; +using BuildFn = Func; +using HasMorphFn = Func; +using BuildMorphFn = Func; using Accessors = IReadOnlyDictionary; public class VertexAttribute @@ -12,7 +14,11 @@ public class VertexAttribute /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; /// Write this vertex attribute's value at the specified index to the provided byte array. - public readonly Writer Write; + public readonly BuildFn Build; + + public readonly HasMorphFn HasMorph; + + public readonly BuildMorphFn BuildMorph; /// Size in bytes of a single vertex's attribute value. public byte Size => (MdlFile.VertexType)Element.Type switch @@ -27,13 +33,32 @@ public class VertexAttribute _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; - public VertexAttribute(MdlStructs.VertexElement element, Writer write) + public VertexAttribute( + MdlStructs.VertexElement element, + BuildFn write, + HasMorphFn? hasMorph = null, + BuildMorphFn? buildMorph = null + ) { Element = element; - Write = write; + Build = write; + HasMorph = hasMorph ?? DefaultHasMorph; + BuildMorph = buildMorph ?? DefaultBuildMorph; } - public static VertexAttribute Position(Accessors accessors) + // todo: this is per-shape at the moment - consider if it should do them all at once (i mean we always want to check all of them, it's mostly a semantics question on who owns the loop) + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) + { + return false; + } + + // xiv stores shapes as full vertex replacements, so the default value for a morph attribute is simply it's built state (rather than a delta or w/e) + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) + { + return Build(vertexIndex); + } + + public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("POSITION", out var accessor)) throw new Exception("Meshes must contain a POSITION attribute."); @@ -47,9 +72,32 @@ public class VertexAttribute var values = accessor.AsVector3Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("POSITION")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - (index, bytes) => WriteSingle3(values[index], bytes) + index => BuildSingle3(values[index]), + // TODO: at the moment this is only defined for position - is it worth setting one up for normal, too? + (morphIndex, vertexIndex) => + { + var deltas = foo[morphIndex]; + if (deltas == null) return false; + var delta = deltas[vertexIndex]; + return delta != Vector3.Zero; + }, + // TODO: this will _need_ to be defined for any values that appear in morphs, i.e. geom and maybe mats + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += delta.Value; + + return BuildSingle3(value); + } ); } @@ -73,8 +121,7 @@ public class VertexAttribute return new VertexAttribute( element, // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - // (index, bytes) => WriteByteFloat4(values[index], bytes) - (index, bytes) => WriteByteFloat4(Vector4.UnitX, bytes) + index => BuildByteFloat4(Vector4.UnitX) ); } @@ -99,8 +146,7 @@ public class VertexAttribute return new VertexAttribute( element, // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - // (index, bytes) => WriteUInt(values[index], bytes) - (index, bytes) => WriteUInt(Vector4.Zero, bytes) + index => BuildUInt(Vector4.Zero) ); } @@ -120,7 +166,7 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes) + index => BuildHalf4(new Vector4(values[index], 0)) ); } @@ -141,18 +187,18 @@ public class VertexAttribute if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half2 }, - (index, bytes) => WriteHalf2(values1[index], bytes) + index => BuildHalf2(values1[index]) ); var values2 = accessor2.AsVector2Array(); return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half4 }, - (index, bytes) => + index => { var value1 = values1[index]; var value2 = values2[index]; - WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes); + return BuildHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y)); } ); } @@ -173,7 +219,7 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + index => BuildByteFloat4(values[index]) ); } @@ -193,44 +239,54 @@ public class VertexAttribute return new VertexAttribute( element, - (index, bytes) => WriteByteFloat4(values[index], bytes) + index => BuildByteFloat4(values[index]) ); } - private static void WriteSingle3(Vector3 input, List bytes) + private static byte[] BuildSingle3(Vector3 input) { - bytes.AddRange(BitConverter.GetBytes(input.X)); - bytes.AddRange(BitConverter.GetBytes(input.Y)); - bytes.AddRange(BitConverter.GetBytes(input.Z)); + return [ + ..BitConverter.GetBytes(input.X), + ..BitConverter.GetBytes(input.Y), + ..BitConverter.GetBytes(input.Z), + ]; } - private static void WriteUInt(Vector4 input, List bytes) + private static byte[] BuildUInt(Vector4 input) { - bytes.Add((byte)input.X); - bytes.Add((byte)input.Y); - bytes.Add((byte)input.Z); - bytes.Add((byte)input.W); + return [ + (byte)input.X, + (byte)input.Y, + (byte)input.Z, + (byte)input.W, + ]; } - private static void WriteByteFloat4(Vector4 input, List bytes) + private static byte[] BuildByteFloat4(Vector4 input) { - bytes.Add((byte)Math.Round(input.X * 255f)); - bytes.Add((byte)Math.Round(input.Y * 255f)); - bytes.Add((byte)Math.Round(input.Z * 255f)); - bytes.Add((byte)Math.Round(input.W * 255f)); + return [ + (byte)Math.Round(input.X * 255f), + (byte)Math.Round(input.Y * 255f), + (byte)Math.Round(input.Z * 255f), + (byte)Math.Round(input.W * 255f), + ]; } - private static void WriteHalf2(Vector2 input, List bytes) + private static byte[] BuildHalf2(Vector2 input) { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); + return [ + ..BitConverter.GetBytes((Half)input.X), + ..BitConverter.GetBytes((Half)input.Y), + ]; } - private static void WriteHalf4(Vector4 input, List bytes) + private static byte[] BuildHalf4(Vector4 input) { - bytes.AddRange(BitConverter.GetBytes((Half)input.X)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Y)); - bytes.AddRange(BitConverter.GetBytes((Half)input.Z)); - bytes.AddRange(BitConverter.GetBytes((Half)input.W)); + return [ + ..BitConverter.GetBytes((Half)input.X), + ..BitConverter.GetBytes((Half)input.Y), + ..BitConverter.GetBytes((Half)input.Z), + ..BitConverter.GetBytes((Half)input.W), + ]; } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 43630f6e..223f43ea 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -206,6 +206,9 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var submeshes = new List(); var vertexBuffer = new List(); var indices = new List(); + + var shapeData = new Dictionary>(); + var shapeValues = new List(); foreach (var submeshnodes in nodes) { @@ -214,6 +217,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var subOffset = submeshes.Count; var vertOffset = vertexBuffer.Count; var idxOffset = indices.Count; + var shapeValueOffset = shapeValues.Count; var ( vertexDeclaration, @@ -221,16 +225,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable xivMesh, xivSubmeshes, meshVertexBuffer, - meshIndices + meshIndices, + meshShapeData // fasdfasd ) = MeshThing(submeshnodes); vertexDeclarations.Add(vertexDeclaration); boneTables.Add(boneTable); + var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); meshes.Add(xivMesh with { SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), - StartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)), + StartIndex = meshStartIndex, VertexBufferOffset = xivMesh.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) .ToArray(), @@ -243,6 +249,39 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); vertexBuffer.AddRange(meshVertexBuffer); indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); + foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData) + { + List keyshapedata; + if (!shapeData.TryGetValue(key, out keyshapedata)) + { + keyshapedata = new(); + shapeData.Add(key, keyshapedata); + } + + keyshapedata.Add(shapeMesh with { + MeshIndexOffset = meshStartIndex, + ShapeValueOffset = (uint)shapeValueOffset, + }); + + shapeValues.AddRange(meshShapeValues); + } + } + + var shapes = new List(); + var shapeMeshes = new List(); + + foreach (var (name, sms) in shapeData) + { + var smOff = shapeMeshes.Count; + + shapeMeshes.AddRange(sms); + shapes.Add(new MdlFile.Shape() + { + ShapeName = name, + // TODO: THESE IS PER LOD + ShapeMeshStartIndex = [(ushort)smOff, 0, 0], + ShapeMeshCount = [(ushort)sms.Count, 0, 0], + }); } var mdl = new MdlFile() @@ -292,6 +331,10 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh SubMeshBoneMap = [0], + Shapes = shapes.ToArray(), + ShapeMeshes = shapeMeshes.ToArray(), + ShapeValues = shapeValues.ToArray(), + Lods = [new MdlStructs.LodStruct() { MeshIndex = 0, @@ -323,7 +366,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable MdlStructs.MeshStruct, IEnumerable, IEnumerable, - IEnumerable + IEnumerable, + IDictionary)> ) MeshThing(IEnumerable nodes) { var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; @@ -336,6 +380,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var indices = new List(); var strides = new byte[] {0, 0, 0}; var submeshes = new List(); + var morphData = new Dictionary>(); // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. foreach (var node in nodes) @@ -343,7 +388,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var vertOff = vertexCount; var idxOff = indexCount; - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs) = NodeMeshThing(node); + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST strides = newStrides; // ALSO CHECK EQUAL vertexCount += vertCount; @@ -356,6 +401,25 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable IndexOffset = submesh.IndexOffset + idxOff // TODO: bone stuff probably }); + // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS + foreach (var (key, shapeValues) in subMorphData) + { + List valueList; + if (!morphData.TryGetValue(key, out valueList)) + { + valueList = new(); + morphData.Add(key, valueList); + } + valueList.AddRange( + shapeValues + .Select(value => value with { + // but this is actually an index index + BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), + // this is a vert idx + ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff), + }) + ); + } } // one of these per skinned mesh. @@ -390,13 +454,30 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) }; + // TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point + var shapeData = morphData.ToDictionary( + (pair) => pair.Key, + pair => ( + new MdlStructs.ShapeMeshStruct() + { + // TODO: this needs to be adjusted by the parent + MeshIndexOffset = 0, + ShapeValueCount = (uint)pair.Value.Count, + // TODO: Also update by parent + ShapeValueOffset = 0, + }, + pair.Value + ) + ); + return ( vertexDeclaration, boneTable, xivMesh, submeshes, streams[0].Concat(streams[1]).Concat(streams[2]), - indices + indices, + shapeData ); } @@ -408,7 +489,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable ushort, IEnumerable[], uint, - IEnumerable + IEnumerable, + IDictionary> ) NodeMeshThing(Node node) { // BoneTable (mesh.btidx = 255 means unskinned) @@ -423,9 +505,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var primitive = mesh.Primitives[0]; var accessors = primitive.VertexAccessors; + + // var foo = primitive.GetMorphTargetAccessors(0); + // var bar = foo["POSITION"]; + // var baz = bar.AsVector3Array(); + + var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount) + // todo: map by name, probably? or do that later (probably later) + .Select(index => primitive.GetMorphTargetAccessors(index)); + + // TODO: name + var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount) + .Select(_ => new List()) + .ToArray(); var rawAttributes = new[] { - VertexAttribute.Position(accessors), + VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors), VertexAttribute.Normal(accessors), @@ -440,9 +535,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { if (attribute == null) continue; var element = attribute.Element; + // recreating this here really sucks - add a "withstream" or something. attributes.Add(new VertexAttribute( element with {Offset = offsets[element.Stream]}, - attribute.Write + attribute.Build, + attribute.HasMorph, + attribute.BuildMorph )); offsets[element.Stream] += attribute.Size; } @@ -460,7 +558,18 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { foreach (var attribute in attributes) { - attribute.Write(vertexIndex, streams[attribute.Element.Stream]); + streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex)); + } + + // this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long + foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + { + var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex)); + // Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}"); + if (hasMorph) + { + list.Add(vertexIndex); + } } } @@ -471,6 +580,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // .ToArray(); var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + // BLAH + // foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + // { + // Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}"); + // } + // TODO BUILD THE MORPH VERTS + // (source, target) + var morphmappingstuff = new List[morphChangedVerts.Length]; + foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) + { + var morphmaplist = morphmappingstuff[morphIndex] = new(); + foreach (var vertIdx in list) + { + foreach (var attribute in attributes) + { + streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx)); + } + + var fuck = indices.WithIndex() + .Where(pair => pair.Value == vertIdx) + .Select(pair => pair.Index); + + foreach (var something in fuck) + { + morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ + BaseIndicesIndex = (ushort)something, + ReplacingVertexIndex = (ushort)vertexCount, + }); + } + vertexCount++; + } + } + + // TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world + var morphData = new Dictionary>(); + if (morphmappingstuff.Length > 0) + { + var morphnames = mesh.Extras.GetNode("targetNames").Deserialize>(); + morphData = morphmappingstuff + .Zip(morphnames) + .ToDictionary( + (pair) => pair.Second, + (pair) => pair.First + ); + } + // one of these per mesh var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { @@ -521,7 +676,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable (ushort)vertexCount, streams, (uint)indices.Length, - indices + indices, + morphData ); } From 6641f5425bf8cfe6f12f7e433a7709c57f597031 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 20:13:39 +1100 Subject: [PATCH 1392/2451] Add morph handling for normal/tangent --- .../Import/Models/Import/VertexAttribute.cs | 38 +++++++++++++++++-- Penumbra/Import/Models/ModelManager.cs | 4 +- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 9fd50513..37ccb79d 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -150,7 +150,7 @@ public class VertexAttribute ); } - public static VertexAttribute? Normal(Accessors accessors) + public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("NORMAL", out var accessor)) return null; @@ -164,9 +164,24 @@ public class VertexAttribute var values = accessor.AsVector3Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("NORMAL")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - index => BuildHalf4(new Vector4(values[index], 0)) + index => BuildHalf4(new Vector4(values[index], 0)), + null, + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += delta.Value; + + return BuildHalf4(new Vector4(value, 0)); + } ); } @@ -203,7 +218,7 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors) { if (!accessors.TryGetValue("TANGENT", out var accessor)) return null; @@ -217,9 +232,24 @@ public class VertexAttribute var values = accessor.AsVector4Array(); + var foo = morphAccessors + .Select(ma => ma.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .ToArray(); + return new VertexAttribute( element, - index => BuildByteFloat4(values[index]) + index => BuildByteFloat4(values[index]), + null, + (morphIndex, vertexIndex) => + { + var value = values[vertexIndex]; + + var delta = foo[morphIndex]?[vertexIndex]; + if (delta != null) + value += new Vector4(delta.Value, 0); + + return BuildByteFloat4(value); + } ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 223f43ea..d849f3eb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -523,8 +523,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors), - VertexAttribute.Normal(accessors), - VertexAttribute.Tangent1(accessors), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; From 70a09264a8eba3275412f01535b55f2471865ef3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 5 Jan 2024 22:35:36 +1100 Subject: [PATCH 1393/2451] Bone table imports --- .../Import/Models/Import/VertexAttribute.cs | 19 +- Penumbra/Import/Models/ModelManager.cs | 189 ++++++++++++++---- .../ModEditWindow.Models.MdlTab.cs | 2 +- 3 files changed, 170 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 37ccb79d..f2ec6f1a 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -120,13 +120,12 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildByteFloat4(Vector4.UnitX) + index => BuildByteFloat4(values[index]) ); } // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. - public static VertexAttribute? BlendIndex(Accessors accessors) + public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) return null; @@ -134,6 +133,9 @@ public class VertexAttribute if (!accessors.ContainsKey("WEIGHTS_0")) throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + if (boneMap == null) + throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); + var element = new MdlStructs.VertexElement() { Stream = 0, @@ -145,8 +147,15 @@ public class VertexAttribute return new VertexAttribute( element, - // TODO: TEMP TESTING PINNED TO BONE 0 UNTIL I SET UP BONE MAPPINGS - index => BuildUInt(Vector4.Zero) + index => { + var foo = values[index]; + return BuildUInt(new Vector4( + boneMap[(ushort)foo.X], + boneMap[(ushort)foo.Y], + boneMap[(ushort)foo.Z], + boneMap[(ushort)foo.W] + )); + } ); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index d849f3eb..875c6071 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -189,7 +189,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var nodes = model.LogicalNodes .Where(node => node.Mesh != null) .Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 - .SelectWhere(node => { + .SelectWhere(node => + { var name = node.Name ?? node.Mesh.Name; var match = MeshNameGroupingRegex().Match(name); return match.Success @@ -201,6 +202,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // this is a representation of a single LoD var vertexDeclarations = new List(); + var bones = new List(); var boneTables = new List(); var meshes = new List(); var submeshes = new List(); @@ -209,7 +211,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var shapeData = new Dictionary>(); var shapeValues = new List(); - + foreach (var submeshnodes in nodes) { var boneTableOffset = boneTables.Count; @@ -221,21 +223,52 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, xivSubmeshes, meshVertexBuffer, meshIndices, - meshShapeData // fasdfasd + meshShapeData, + meshBoneList ) = MeshThing(submeshnodes); + var boneTableIndex = 255; + // TODO: a better check than this would be real good + if (meshBoneList.Count() > 0) + { + var boneIndices = new List(); + foreach (var mb in meshBoneList) + { + var boneIndex = bones.IndexOf(mb); + if (boneIndex == -1) + { + boneIndex = bones.Count; + bones.Add(mb); + } + boneIndices.Add((ushort)boneIndex); + } + + if (boneIndices.Count > 64) + throw new Exception("One mesh cannot be weighted to more than 64 bones."); + + var boneIndicesArray = new ushort[64]; + Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + + boneTableIndex = boneTableOffset; + boneTables.Add(new MdlStructs.BoneTableStruct() + { + BoneCount = (byte)boneIndices.Count, + BoneIndex = boneIndicesArray, + }); + } + vertexDeclarations.Add(vertexDeclaration); - boneTables.Add(boneTable); var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(xivMesh with { + meshes.Add(xivMesh with + { SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. - BoneTableIndex = (ushort)(xivMesh.BoneTableIndex + boneTableOffset), + BoneTableIndex = (ushort)boneTableIndex, StartIndex = meshStartIndex, VertexBufferOffset = xivMesh.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) @@ -243,7 +276,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); // TODO: could probably do this with linq cleaner foreach (var xivSubmesh in xivSubmeshes) - submeshes.Add(xivSubmesh with { + submeshes.Add(xivSubmesh with + { // TODO: this will need to keep ticking up for each submesh in the same mesh IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) }); @@ -258,7 +292,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable shapeData.Add(key, keyshapedata); } - keyshapedata.Add(shapeMesh with { + keyshapedata.Add(shapeMesh with + { MeshIndexOffset = meshStartIndex, ShapeValueOffset = (uint)shapeValueOffset, }); @@ -347,9 +382,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable IndexDataOffset = (uint)vertexBuffer.Count, }, ], - Bones = [ - "j_kosi", - ], + Bones = bones.ToArray(), Materials = [ "/mt_c0201e6180_top_a.mtrl", ], @@ -362,15 +395,16 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // this return type is an absolute meme, class that shit up. private ( MdlStructs.VertexDeclarationStruct, - MdlStructs.BoneTableStruct, + // MdlStructs.BoneTableStruct, MdlStructs.MeshStruct, IEnumerable, IEnumerable, IEnumerable, - IDictionary)> + IDictionary)>, + IEnumerable ) MeshThing(IEnumerable nodes) { - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty()}; + var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty() }; var vertexCount = (ushort)0; // there's gotta be a better way to do this with streams or enumerables or something, surely var streams = new List[3]; @@ -378,17 +412,57 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable streams[i] = new List(); var indexCount = (uint)0; var indices = new List(); - var strides = new byte[] {0, 0, 0}; + var strides = new byte[] { 0, 0, 0 }; var submeshes = new List(); var morphData = new Dictionary>(); + /* + THOUGHTS + per submesh node, before calling down to build the mesh, build a bone mapping of joint index -> bone name (not node index) - the joint indexes are what will be used in the vertices. + per submesh node, eagerly collect all blend indexes (joints) used before building anything - just as a set or something + the above means i can create a limited set and a mapping, i.e. if skeleton contains {0->a 1->b 2->c}, and mesh contains 0, 2, then i can output [a, c] + {0->0, 2->1} + (throw if >64 entries in that name array) + + then for the second prim, + again get the joint-name mapping, and again get the joint set + then can extend the values. using the samme example, if skeleton2 contains {0->c 1->d, 2->e} and mesh contains [0,2] again, then bone array can be extended to [a, c, e] and the mesh-specific mapping would be {0->1, 2->2} + + repeat, etc + */ + + var usedBones = new List(); + // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. foreach (var node in nodes) { var vertOff = vertexCount; var idxOff = indexCount; - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node); + Dictionary? nodeBoneMap = null; + var bonething = WalkBoneThing(node); + if (bonething.HasValue) + { + var (boneNames, usedJoints) = bonething.Value; + nodeBoneMap = new(); + + // todo: probably linq this shit + foreach (var usedJoint in usedJoints) + { + // this is the 0,2 + var boneName = boneNames[usedJoint]; + var boneIdx = usedBones.IndexOf(boneName); + if (boneIdx == -1) + { + boneIdx = usedBones.Count; + usedBones.Add(boneName); + } + nodeBoneMap.Add(usedJoint, (ushort)boneIdx); + } + + Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); + } + + var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node, nodeBoneMap); vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST strides = newStrides; // ALSO CHECK EQUAL vertexCount += vertCount; @@ -397,7 +471,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable indexCount += idxCount; // we need to offset the indexes to point into the new stuff indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with { + submeshes.Add(submesh with + { IndexOffset = submesh.IndexOffset + idxOff // TODO: bone stuff probably }); @@ -412,7 +487,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable } valueList.AddRange( shapeValues - .Select(value => value with { + .Select(value => value with + { // but this is actually an index index BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), // this is a vert idx @@ -424,12 +500,12 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // one of these per skinned mesh. // TODO: check if mesh has skinning at all. (err if mixed?) - var boneTable = new MdlStructs.BoneTableStruct() - { - BoneCount = 1, - // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - BoneIndex = new ushort[64], - }; + // var boneTable = new MdlStructs.BoneTableStruct() + // { + // BoneCount = 1, + // // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. + // BoneIndex = new ushort[64], + // }; // mesh var xivMesh = new MdlStructs.MeshStruct() @@ -472,15 +548,59 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return ( vertexDeclaration, - boneTable, + // boneTable, xivMesh, submeshes, streams[0].Concat(streams[1]).Concat(streams[2]), indices, - shapeData + shapeData, + usedBones ); } + private (string[], ISet)? WalkBoneThing(Node node) + { + // + if (node.Skin == null) + return null; + + var jointNames = Enumerable.Range(0, node.Skin.JointsCount) + .Select(index => node.Skin.GetJoint(index).Joint.Name ?? $"UNNAMED") + .ToArray(); + + // it might make sense to do this in the submesh handling - i do need to maintain the mesh-wide bone list, but that can be passed in/out, perhaps? + var mesh = node.Mesh; + if (mesh.Primitives.Count != 1) + throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); + var primitive = mesh.Primitives[0]; + + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); + if (jointsAccessor == null) + throw new Exception($"Skinned meshes must contain a JOINTS_0 attribute."); + + // var weightsAccssor = primitive.GetVertexAccessor("WEIGHTS_0"); + // if (weightsAccssor == null) + // throw new Exception($"Skinned meshes must contain a WEIGHTS_0 attribute."); + + var usedJoints = new HashSet(); + + // TODO: would be neat to omit any joints that are only used in 0-weight positions, but doing so would require being a _little_ smarter in vertex attrs on how to fall back when mappings aren't found - or otherwise try to ensure that mappings for unused stuff always exists + // foreach (var (joints, weights) in jointsAccessor.AsVector4Array().Zip(weightsAccssor.AsVector4Array())) + // { + // for (var index = 0; index < 4; index++) + // if (weights[index] > 0) + // usedJoints.Add((ushort)joints[index]); + // } + + foreach (var joints in jointsAccessor.AsVector4Array()) + { + for (var index = 0; index < 4; index++) + usedJoints.Add((ushort)joints[index]); + } + + return (jointNames, usedJoints); + } + private ( MdlStructs.VertexDeclarationStruct, byte[], @@ -491,7 +611,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable uint, IEnumerable, IDictionary> - ) NodeMeshThing(Node node) + ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) { // BoneTable (mesh.btidx = 255 means unskinned) // vertexdecl @@ -505,7 +625,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var primitive = mesh.Primitives[0]; var accessors = primitive.VertexAccessors; - + // var foo = primitive.GetMorphTargetAccessors(0); // var bar = foo["POSITION"]; // var baz = bar.AsVector3Array(); @@ -522,7 +642,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var rawAttributes = new[] { VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors), + VertexAttribute.BlendIndex(accessors, nodeBoneMap), VertexAttribute.Normal(accessors, morphAccessors), VertexAttribute.Tangent1(accessors, morphAccessors), VertexAttribute.Color(accessors), @@ -530,14 +650,14 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }; var attributes = new List(); - var offsets = new byte[] {0, 0, 0}; + var offsets = new byte[] { 0, 0, 0 }; foreach (var attribute in rawAttributes) { if (attribute == null) continue; var element = attribute.Element; // recreating this here really sucks - add a "withstream" or something. attributes.Add(new VertexAttribute( - element with {Offset = offsets[element.Stream]}, + element with { Offset = offsets[element.Stream] }, attribute.Build, attribute.HasMorph, attribute.BuildMorph @@ -547,7 +667,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var strides = offsets; // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. - + var streams = new List[3]; for (var i = 0; i < 3; i++) streams[i] = new List(); @@ -604,7 +724,8 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable foreach (var something in fuck) { - morphmaplist.Add(new MdlStructs.ShapeValueStruct(){ + morphmaplist.Add(new MdlStructs.ShapeValueStruct() + { BaseIndicesIndex = (ushort)something, ReplacingVertexIndex = (ushort)vertexCount, }); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 5d9abda6..e030c8c0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow public void Import() { // TODO: this needs to be fleshed out a bunch. - _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result)); + _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result ?? Mdl)); } /// Export model to an interchange format. From 41d900ff5148d42b8d1cd1e585bac557618e4743 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jan 2024 14:27:05 +0100 Subject: [PATCH 1394/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 192fd1e6..ac3fc098 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 192fd1e6ad269c3cbdb81aa8c43a8bc20c5ae7f0 +Subproject commit ac3fc0981ac8f503ac91d2419bd28c54f271763e From 306a9c217a2436ae58e2eb454eab0a9639fdd789 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jan 2024 14:51:41 +0100 Subject: [PATCH 1395/2451] Fix FileDialog being drawn multiple times. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 -- Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs | 3 --- 2 files changed, 5 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 336be1a4..4783e76b 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -158,8 +158,6 @@ public class FileEditor : IDisposable where T : class, IWritable _quickImport = null; } - - _fileDialog.Draw(); } public void Reset() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 82fc78c0..8d1c8cb7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -6,7 +6,6 @@ using OtterGui.Raii; using OtterGui; using OtterGui.Classes; using Penumbra.GameData; -using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.String; @@ -43,8 +42,6 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawOtherShaderPackageDetails(file); - file.FileDialog.Draw(); - ret |= file.Shpk.IsChanged(); return !disabled && ret; From 55f38865e3d9885b3643b7d7e588d5c069d966e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jan 2024 18:13:03 +0100 Subject: [PATCH 1396/2451] Memorize last selected mod and state of advanced editing window. --- Penumbra/Communication/ModPathChanged.cs | 3 ++ Penumbra/EphemeralConfig.cs | 32 +++++++++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 49 ++++++++++++-------- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 28 ++++++++--- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 3ec64f7e..e6291781 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -19,6 +19,9 @@ public sealed class ModPathChanged() { public enum Priority { + /// + EphemeralConfig = -500, + /// CollectionCacheManagerAddition = -100, diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 6c87d331..8cf23de6 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -2,7 +2,10 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.Communication; using Penumbra.Enums; +using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.ResourceWatcher; @@ -11,11 +14,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; -public class EphemeralConfig : ISavable +public class EphemeralConfig : ISavable, IDisposable { [JsonIgnore] private readonly SaveService _saveService; + [JsonIgnore] + private readonly ModPathChanged _modPathChanged; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public bool DebugSeparateWindow { get; set; } = false; @@ -31,17 +37,24 @@ public class EphemeralConfig : ISavable public TabType SelectedTab { get; set; } = TabType.Settings; public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; public bool FixMainWindow { get; set; } = false; + public string LastModPath { get; set; } = string.Empty; + public bool AdvancedEditingOpen { get; set; } = false; /// /// Load the current configuration. /// Includes adding new colors and migrating from old versions. /// - public EphemeralConfig(SaveService saveService) + public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged) { - _saveService = saveService; + _saveService = saveService; + _modPathChanged = modPathChanged; Load(); + _modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig); } + public void Dispose() + => _modPathChanged.Unsubscribe(OnModPathChanged); + private void Load() { static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) @@ -80,8 +93,19 @@ public class EphemeralConfig : ISavable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } + + /// Overwrite the last saved mod path if it changes. + private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _) + { + if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase)) + return; + + LastModPath = mod.Identifier; + Save(); + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 96957ba8..167adafe 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -145,12 +145,20 @@ public partial class ModEditWindow : Window, IDisposable _materialTab.Reset(); _modelTab.Reset(); _shaderPackageTab.Reset(); + _config.Ephemeral.AdvancedEditingOpen = false; + _config.Ephemeral.Save(); } public override void Draw() { using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow); + if (!_config.Ephemeral.AdvancedEditingOpen) + { + _config.Ephemeral.AdvancedEditingOpen = true; + _config.Ephemeral.Save(); + } + using var tabBar = ImRaii.TabBar("##tabs"); if (!tabBar) return; @@ -566,34 +574,36 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, + CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _models = models; - _fileDialog = fileDialog; - _objects = objects; - _framework = framework; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _models = models; + _fileDialog = fileDialog; + _objects = objects; + _framework = framework; _characterBaseDestructor = characterBaseDestructor; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path, _mod)); + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, + (bytes, path, _) => new MdlTab(this, bytes, path, _mod)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", - () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, + () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, + () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); @@ -601,6 +611,7 @@ public partial class ModEditWindow : Window, IDisposable _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); + IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; } public void Dispose() diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c42b1018..0990f27b 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -39,8 +39,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) + { + var mod = _modManager.FirstOrDefault(m + => string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase)); + if (mod != null) + SelectByValue(mod); + } + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector); _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector); _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector); @@ -87,15 +94,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 5 Jan 2024 19:02:50 +0100 Subject: [PATCH 1397/2451] Rework game path selection a bit. --- .../ModEditWindow.Models.MdlTab.cs | 63 +++++++++------ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 79 +++++++++++++------ 2 files changed, 93 insertions(+), 49 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 20a4129d..93e674ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -12,27 +12,29 @@ public partial class ModEditWindow { private ModEditWindow _edit; - public readonly MdlFile Mdl; + public readonly MdlFile Mdl; private readonly List[] _attributes; - public List? GamePaths { get; private set ;} - public int GamePathIndex; - - public bool PendingIo { get; private set; } = false; + public List? GamePaths { get; private set; } + public int GamePathIndex; + + public bool PendingIo { get; private set; } = false; public string? IoException { get; private set; } = null; [GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] private static partial Regex CharaEquipmentRegex(); - [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] + [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", + RegexOptions.Compiled)] private static partial Regex CharaHumanRegex(); - [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", RegexOptions.Compiled)] + [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", + RegexOptions.Compiled)] private static partial Regex CharaBodyRegex(); public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) { - _edit = edit; + _edit = edit; Mdl = new MdlFile(bytes); _attributes = CreateAttributes(Mdl); @@ -54,21 +56,29 @@ public partial class ModEditWindow /// Mod within which the .mdl is resolved. private void FindGamePaths(string path, Mod mod) { + if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p)) + { + GamePaths = [p]; + return; + } + PendingIo = true; - var task = Task.Run(() => { + var task = Task.Run(() => + { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? - // NOTE: We're using case insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. + // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. return mod.AllSubMods - .SelectMany(submod => submod.Files.Concat(submod.FileSwaps)) + .SelectMany(m => m.Files.Concat(m.FileSwaps)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Select(kv => kv.Key) .ToList(); }); - task.ContinueWith(task => { - IoException = task.Exception?.ToString(); - PendingIo = false; - GamePaths = task.Result; + task.ContinueWith(t => + { + IoException = t.Exception?.ToString(); + PendingIo = false; + GamePaths = t.Result; }); } @@ -77,19 +87,23 @@ public partial class ModEditWindow public void Export(string outputPath, Utf8GamePath mdlPath) { SklbFile? sklb = null; - try { + try + { var sklbPath = GetSklbPath(mdlPath.ToString()); sklb = sklbPath != null ? ReadSklb(sklbPath) : null; - } catch (Exception exception) { + } + catch (Exception exception) + { IoException = exception?.ToString(); return; } PendingIo = true; _edit._models.ExportToGltf(Mdl, sklb, outputPath) - .ContinueWith(task => { + .ContinueWith(task => + { IoException = task.Exception?.ToString(); - PendingIo = false; + PendingIo = false; }); } @@ -114,7 +128,7 @@ public partial class ModEditWindow return type switch { "body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb", - _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), + _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), }; } @@ -123,7 +137,7 @@ public partial class ModEditWindow if (match.Success) { var subCategory = match.Groups["SubCategory"].Value; - var set = match.Groups["Set"].Value; + var set = match.Groups["Set"].Value; return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb"; } @@ -137,16 +151,17 @@ public partial class ModEditWindow // TODO: if cross-collection lookups are turned off, this conversion can be skipped if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); - + var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... var bytes = resolvedPath switch { - null => _edit._gameData.GetFile(sklbPath)?.Data, + null => _edit._gameData.GetFile(sklbPath)?.Data, FullPath path => File.ReadAllBytes(path.ToPath()), }; if (bytes == null) - throw new Exception($"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); + throw new Exception( + $"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); return new SklbFile(bytes); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index aa69953b..3891eb95 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -20,6 +20,8 @@ public partial class ModEditWindow private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; + private string _customPath = string.Empty; + private Utf8GamePath _customGamePath = Utf8GamePath.Empty; private bool DrawModelPanel(MdlTab tab, bool disabled) { @@ -51,10 +53,6 @@ public partial class ModEditWindow private void DrawExport(MdlTab tab, bool disabled) { - // IO on a disabled panel doesn't really make sense. - if (disabled) - return; - if (!ImGui.CollapsingHeader("Export")) return; @@ -70,16 +68,14 @@ public partial class ModEditWindow DrawGamePathCombo(tab); - if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo)) - { - var gamePath = tab.GamePaths[tab.GamePathIndex]; - - _fileDialog.OpenSavePicker( - "Save model as glTF.", - ".gltf", - Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), - ".gltf", - (valid, path) => { + var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count + ? tab.GamePaths[tab.GamePathIndex] + : _customGamePath; + if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", + tab.PendingIo || gamePath.IsEmpty)) + _fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), + ".gltf", (valid, path) => + { if (!valid) return; @@ -88,27 +84,60 @@ public partial class ModEditWindow _mod!.ModPath.FullName, false ); - } if (tab.IoException != null) ImGuiUtil.TextWrapped(tab.IoException); - - return; } private void DrawGamePathCombo(MdlTab tab) { - using var combo = ImRaii.Combo("Game Path", tab.GamePaths![tab.GamePathIndex].ToString()); - if (!combo) - return; - - foreach (var (path, index) in tab.GamePaths.WithIndex()) + if (tab.GamePaths!.Count == 0) { - if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex)) - continue; + ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting."); + if (ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) + if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) + _customGamePath = Utf8GamePath.Empty; - tab.GamePathIndex = index; + return; } + + DrawComboButton(tab); + } + + private static void DrawComboButton(MdlTab tab) + { + const string label = "Game Path"; + var preview = tab.GamePaths![tab.GamePathIndex].ToString(); + var labelWidth = ImGui.CalcTextSize(label).X + ImGui.GetStyle().ItemInnerSpacing.X; + var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth; + if (tab.GamePaths!.Count == 1) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.FrameBg)) + .Push(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBgHovered)) + .Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBgActive)); + using var group = ImRaii.Group(); + ImGui.Button(preview, new Vector2(buttonWidth, 0)); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TextUnformatted("Game Path"); + } + else + { + ImGui.SetNextItemWidth(buttonWidth); + using var combo = ImRaii.Combo("Game Path", preview); + if (combo.Success) + foreach (var (path, index) in tab.GamePaths.WithIndex()) + { + if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex)) + continue; + + tab.GamePathIndex = index; + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(preview); + ImGuiUtil.HoverTooltip("Right-Click to copy to clipboard.", ImGuiHoveredFlags.AllowWhenDisabled); } private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) From b5b3e1b1f26184380dc49a425e7ccc74836262d0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 11:55:37 +1100 Subject: [PATCH 1398/2451] Tidy up vertex attributes --- .../Import/Models/Import/VertexAttribute.cs | 113 +++++++++--------- Penumbra/Import/Models/ModelManager.cs | 8 +- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index f2ec6f1a..cae068db 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -13,11 +13,11 @@ public class VertexAttribute { /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; - /// Write this vertex attribute's value at the specified index to the provided byte array. + /// Build a byte array containing this vertex attribute's data for the specified vertex index. public readonly BuildFn Build; - + /// Check if the specified morph target index contains a morph for the specified vertex index. public readonly HasMorphFn HasMorph; - + /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; /// Size in bytes of a single vertex's attribute value. @@ -33,7 +33,7 @@ public class VertexAttribute _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; - public VertexAttribute( + private VertexAttribute( MdlStructs.VertexElement element, BuildFn write, HasMorphFn? hasMorph = null, @@ -46,17 +46,19 @@ public class VertexAttribute BuildMorph = buildMorph ?? DefaultBuildMorph; } - // todo: this is per-shape at the moment - consider if it should do them all at once (i mean we always want to check all of them, it's mostly a semantics question on who owns the loop) - private static bool DefaultHasMorph(int morphIndex, int vertexIndex) - { - return false; - } + public VertexAttribute WithOffset(byte offset) => new VertexAttribute( + Element with { Offset = offset }, + Build, + HasMorph, + BuildMorph + ); - // xiv stores shapes as full vertex replacements, so the default value for a morph attribute is simply it's built state (rather than a delta or w/e) - private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) - { - return Build(vertexIndex); - } + // We assume that attributes don't have morph data unless explicitly configured. + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) => false; + + // XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. + // As a fallback, we're just building the normal vertex data for the index. + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { @@ -72,27 +74,27 @@ public class VertexAttribute var values = accessor.AsVector3Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("POSITION")?.AsVector3Array()) + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("POSITION")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildSingle3(values[index]), - // TODO: at the moment this is only defined for position - is it worth setting one up for normal, too? - (morphIndex, vertexIndex) => + + hasMorph: (morphIndex, vertexIndex) => { - var deltas = foo[morphIndex]; + var deltas = morphValues[morphIndex]; if (deltas == null) return false; var delta = deltas[vertexIndex]; return delta != Vector3.Zero; }, - // TODO: this will _need_ to be defined for any values that appear in morphs, i.e. geom and maybe mats - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += delta.Value; @@ -124,7 +126,6 @@ public class VertexAttribute ); } - // TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton. public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) @@ -147,13 +148,14 @@ public class VertexAttribute return new VertexAttribute( element, - index => { - var foo = values[index]; + index => + { + var gltfIndices = values[index]; return BuildUInt(new Vector4( - boneMap[(ushort)foo.X], - boneMap[(ushort)foo.Y], - boneMap[(ushort)foo.Z], - boneMap[(ushort)foo.W] + boneMap[(ushort)gltfIndices.X], + boneMap[(ushort)gltfIndices.Y], + boneMap[(ushort)gltfIndices.Z], + boneMap[(ushort)gltfIndices.W] )); } ); @@ -173,19 +175,19 @@ public class VertexAttribute var values = accessor.AsVector3Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("NORMAL")?.AsVector3Array()) + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("NORMAL")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildHalf4(new Vector4(values[index], 0)), - null, - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += delta.Value; @@ -208,6 +210,7 @@ public class VertexAttribute var values1 = accessor1.AsVector2Array(); + // There's only one TEXCOORD, output UV coordinates as vec2s. if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half2 }, @@ -216,6 +219,7 @@ public class VertexAttribute var values2 = accessor2.AsVector2Array(); + // Two TEXCOORDs are available, repack them into xiv's vec4 [0X, 0Y, 1X, 1Y] format. return new VertexAttribute( element with { Type = (byte)MdlFile.VertexType.Half4 }, index => @@ -241,19 +245,20 @@ public class VertexAttribute var values = accessor.AsVector4Array(); - var foo = morphAccessors - .Select(ma => ma.GetValueOrDefault("TANGENT")?.AsVector3Array()) + // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. + var morphValues = morphAccessors + .Select(accessors => accessors.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildByteFloat4(values[index]), - null, - (morphIndex, vertexIndex) => + + buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; - var delta = foo[morphIndex]?[vertexIndex]; + var delta = morphValues[morphIndex]?[vertexIndex]; if (delta != null) value += new Vector4(delta.Value, 0); @@ -282,50 +287,40 @@ public class VertexAttribute ); } - private static byte[] BuildSingle3(Vector3 input) - { - return [ + private static byte[] BuildSingle3(Vector3 input) => + [ ..BitConverter.GetBytes(input.X), ..BitConverter.GetBytes(input.Y), ..BitConverter.GetBytes(input.Z), ]; - } - private static byte[] BuildUInt(Vector4 input) - { - return [ + private static byte[] BuildUInt(Vector4 input) => + [ (byte)input.X, (byte)input.Y, (byte)input.Z, (byte)input.W, ]; - } - private static byte[] BuildByteFloat4(Vector4 input) - { - return [ + private static byte[] BuildByteFloat4(Vector4 input) => + [ (byte)Math.Round(input.X * 255f), (byte)Math.Round(input.Y * 255f), (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; - } - private static byte[] BuildHalf2(Vector2 input) - { - return [ + private static byte[] BuildHalf2(Vector2 input) => + [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), ]; - } - private static byte[] BuildHalf4(Vector4 input) - { - return [ + private static byte[] BuildHalf4(Vector4 input) => + [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), ..BitConverter.GetBytes((Half)input.Z), ..BitConverter.GetBytes((Half)input.W), ]; - } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 875c6071..b3a1d9a1 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -655,13 +655,7 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable { if (attribute == null) continue; var element = attribute.Element; - // recreating this here really sucks - add a "withstream" or something. - attributes.Add(new VertexAttribute( - element with { Offset = offsets[element.Stream] }, - attribute.Build, - attribute.HasMorph, - attribute.BuildMorph - )); + attributes.Add(attribute.WithOffset(offsets[element.Stream])); offsets[element.Stream] += attribute.Size; } var strides = offsets; From 6de3077afa93b1f5491ca261c4a7279de83ec5ed Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 16:37:41 +1100 Subject: [PATCH 1399/2451] Clean up submeshes --- .../Import/Models/Import/SubMeshImporter.cs | 217 ++++++++++++++++++ .../Import/Models/Import/VertexAttribute.cs | 2 + Penumbra/Import/Models/ModelManager.cs | 215 +---------------- 3 files changed, 229 insertions(+), 205 deletions(-) create mode 100644 Penumbra/Import/Models/Import/SubMeshImporter.cs diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs new file mode 100644 index 00000000..941fa1d5 --- /dev/null +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -0,0 +1,217 @@ +using Lumina.Data.Parsing; +using OtterGui; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class SubMeshImporter +{ + public struct SubMesh + { + public MdlStructs.SubmeshStruct Struct; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + + public ushort VertexCount; + public byte[] Strides; + public List[] Streams; + + public ushort[] Indices; + + public Dictionary> ShapeValues; + } + + public static SubMesh Import(Node node, IDictionary? nodeBoneMap) + { + var importer = new SubMeshImporter(node, nodeBoneMap); + return importer.Create(); + } + + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + + private List? _attributes; + + private ushort _vertexCount = 0; + private byte[] _strides = [0, 0, 0]; + private readonly List[] _streams; + + private ushort[]? _indices; + + private readonly List? _morphNames; + private Dictionary>? _shapeValues; + + private SubMeshImporter(Node node, IDictionary? nodeBoneMap) + { + var mesh = node.Mesh; + + var primitiveCount = mesh.Primitives.Count; + if (primitiveCount != 1) + { + var name = node.Name ?? mesh.Name ?? "(no name)"; + throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); + } + + _primitive = mesh.Primitives[0]; + _nodeBoneMap = nodeBoneMap; + + try + { + _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); + } + catch + { + _morphNames = null; + } + + // All meshes may use up to 3 byte streams. + _streams = new List[3]; + for (var i = 0; i < 3; i++) + _streams[i] = new List(); + } + + private SubMesh Create() + { + // Build all the data we'll need. + BuildIndices(); + BuildAttributes(); + BuildVertices(); + + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_shapeValues); + + return new SubMesh() + { + Struct = new MdlStructs.SubmeshStruct() + { + IndexOffset = 0, + IndexCount = (uint)_indices.Length, + AttributeIndexMask = 0, + + // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. + BoneStartIndex = 0, + BoneCount = 0, + }, + + VertexDeclaration = new MdlStructs.VertexDeclarationStruct() + { + VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), + }, + + VertexCount = _vertexCount, + Strides = _strides, + Streams = _streams, + + Indices = _indices, + + ShapeValues = _shapeValues, + }; + } + + private void BuildIndices() + { + _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + } + + private void BuildAttributes() + { + var accessors = _primitive.VertexAccessors; + + var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(index => _primitive.GetMorphTargetAccessors(index)); + + // Try to build all the attributes the mesh might use. + // The order here is chosen to match a typical model's element order. + var rawAttributes = new[] { + VertexAttribute.Position(accessors, morphAccessors), + VertexAttribute.BlendWeight(accessors), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + var offsets = new byte[] { 0, 0, 0 }; + foreach (var attribute in rawAttributes) + { + if (attribute == null) continue; + attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); + offsets[attribute.Stream] += attribute.Size; + } + + _attributes = attributes; + // After building the attributes, the resulting next offsets are our stream strides. + _strides = offsets; + } + + private void BuildVertices() + { + ArgumentNullException.ThrowIfNull(_attributes); + + // Lists of vertex indices that are effected by each morph target for this primitive. + var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(_ => new List()) + .ToArray(); + + // We can safely assume that POSITION exists by this point - and if, by some bizarre chance, it doesn't, failing out is sane. + _vertexCount = (ushort)_primitive.VertexAccessors["POSITION"].Count; + + for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) + { + // Write out vertex data to streams for each attribute. + foreach (var attribute in _attributes) + _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); + + // Record which morph targets have values for this vertex, if any. + var changedMorphs = morphModifiedVertices + .WithIndex() + .Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Select(pair => pair.Value); + foreach (var modifiedVertices in changedMorphs) + modifiedVertices.Add(vertexIndex); + } + + BuildShapeValues(morphModifiedVertices); + } + + private void BuildShapeValues(List[] morphModifiedVertices) + { + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_attributes); + + var morphShapeValues = new Dictionary>(); + + foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) + { + // Each for a given mesh, each shape key contains a list of shape value mappings. + var shapeValues = new List(); + + foreach (var vertexIndex in modifiedVertices) + { + // Write out the morphed vertex to the vertex streams. + foreach (var attribute in _attributes) + _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); + + // Find any indices that target this vertex index and create a mapping. + var targetingIndices = _indices.WithIndex() + .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); + foreach (var targetingIndex in targetingIndices) + shapeValues.Add(new MdlStructs.ShapeValueStruct() + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + }); + + _vertexCount++; + } + + var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; + morphShapeValues.Add(name, shapeValues); + } + + _shapeValues = morphShapeValues; + } +} diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index cae068db..6bb9971c 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -20,6 +20,8 @@ public class VertexAttribute /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; + public byte Stream => Element.Stream; + /// Size in bytes of a single vertex's attribute value. public byte Size => (MdlFile.VertexType)Element.Type switch { diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index b3a1d9a1..3153da78 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -462,22 +462,22 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); } - var (vertDecl, newStrides, submesh, vertCount, vertStreams, idxCount, idxs, subMorphData) = NodeMeshThing(node, nodeBoneMap); - vertexDeclaration = vertDecl; // TODO: CHECK EQUAL AFTER FIRST - strides = newStrides; // ALSO CHECK EQUAL - vertexCount += vertCount; + var subMeshThingy = SubMeshImporter.Import(node, nodeBoneMap); + vertexDeclaration = subMeshThingy.VertexDeclaration; // TODO: CHECK EQUAL AFTER FIRST + strides = subMeshThingy.Strides; // ALSO CHECK EQUAL + vertexCount += subMeshThingy.VertexCount; for (var i = 0; i < 3; i++) - streams[i].AddRange(vertStreams[i]); - indexCount += idxCount; + streams[i].AddRange(subMeshThingy.Streams[i]); + indexCount += (uint)subMeshThingy.Indices.Length; // we need to offset the indexes to point into the new stuff - indices.AddRange(idxs.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(submesh with + indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff))); + submeshes.Add(subMeshThingy.Struct with { - IndexOffset = submesh.IndexOffset + idxOff + IndexOffset = subMeshThingy.Struct.IndexOffset + idxOff // TODO: bone stuff probably }); // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS - foreach (var (key, shapeValues) in subMorphData) + foreach (var (key, shapeValues) in subMeshThingy.ShapeValues) { List valueList; if (!morphData.TryGetValue(key, out valueList)) @@ -601,201 +601,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable return (jointNames, usedJoints); } - private ( - MdlStructs.VertexDeclarationStruct, - byte[], - // MdlStructs.MeshStruct, - MdlStructs.SubmeshStruct, - ushort, - IEnumerable[], - uint, - IEnumerable, - IDictionary> - ) NodeMeshThing(Node node, IDictionary? nodeBoneMap) - { - // BoneTable (mesh.btidx = 255 means unskinned) - // vertexdecl - - var mesh = node.Mesh; - - // TODO: should probably say _what_ mesh - // TODO: would be cool to support >1 primitive (esp. given they're effectively what submeshes are modeled as), but blender doesn't really use them, so not going to prio that at all. - if (mesh.Primitives.Count != 1) - throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); - var primitive = mesh.Primitives[0]; - - var accessors = primitive.VertexAccessors; - - // var foo = primitive.GetMorphTargetAccessors(0); - // var bar = foo["POSITION"]; - // var baz = bar.AsVector3Array(); - - var morphAccessors = Enumerable.Range(0, primitive.MorphTargetsCount) - // todo: map by name, probably? or do that later (probably later) - .Select(index => primitive.GetMorphTargetAccessors(index)); - - // TODO: name - var morphChangedVerts = Enumerable.Range(0, primitive.MorphTargetsCount) - .Select(_ => new List()) - .ToArray(); - - var rawAttributes = new[] { - VertexAttribute.Position(accessors, morphAccessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors, nodeBoneMap), - VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; - - var attributes = new List(); - var offsets = new byte[] { 0, 0, 0 }; - foreach (var attribute in rawAttributes) - { - if (attribute == null) continue; - var element = attribute.Element; - attributes.Add(attribute.WithOffset(offsets[element.Stream])); - offsets[element.Stream] += attribute.Size; - } - var strides = offsets; - - // TODO: when merging submeshes, i'll need to check that vert els are the same for all of them, as xiv only stores verts at the mesh level and shares them. - - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - - // todo: this is a bit lmao but also... probably the most sane option? getting the count that is - var vertexCount = primitive.VertexAccessors["POSITION"].Count; - for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.Build(vertexIndex)); - } - - // this is a meme but idk maybe it's the best approach? it's not like the attr array is ever long - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var hasMorph = attributes.Aggregate(false, (cur, attr) => cur || attr.HasMorph(morphIndex, vertexIndex)); - // Penumbra.Log.Information($"eh? {vertexIndex} {morphIndex}: {hasMorph}"); - if (hasMorph) - { - list.Add(vertexIndex); - } - } - } - - // indices - // var indexCount = primitive.GetIndexAccessor().Count; - // var indices = primitive.GetIndices() - // .SelectMany(index => BitConverter.GetBytes((ushort)index)) - // .ToArray(); - var indices = primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); - - // BLAH - // foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - // { - // Penumbra.Log.Information($"morph {morphIndex}: {string.Join(",", list)}"); - // } - // TODO BUILD THE MORPH VERTS - // (source, target) - var morphmappingstuff = new List[morphChangedVerts.Length]; - foreach (var (list, morphIndex) in morphChangedVerts.WithIndex()) - { - var morphmaplist = morphmappingstuff[morphIndex] = new(); - foreach (var vertIdx in list) - { - foreach (var attribute in attributes) - { - streams[attribute.Element.Stream].AddRange(attribute.BuildMorph(morphIndex, vertIdx)); - } - - var fuck = indices.WithIndex() - .Where(pair => pair.Value == vertIdx) - .Select(pair => pair.Index); - - foreach (var something in fuck) - { - morphmaplist.Add(new MdlStructs.ShapeValueStruct() - { - BaseIndicesIndex = (ushort)something, - ReplacingVertexIndex = (ushort)vertexCount, - }); - } - vertexCount++; - } - } - - // TODO: HANDLE THIS BEING MISSING - probably warn or something, it's not the end of the world - var morphData = new Dictionary>(); - if (morphmappingstuff.Length > 0) - { - var morphnames = mesh.Extras.GetNode("targetNames").Deserialize>(); - morphData = morphmappingstuff - .Zip(morphnames) - .ToDictionary( - (pair) => pair.Second, - (pair) => pair.First - ); - } - - // one of these per mesh - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() - { - VertexElements = attributes.Select(attribute => attribute.Element).ToArray(), - }; - - // mesh - // var xivMesh = new MdlStructs.MeshStruct() - // { - // // TODO: sum across submeshes. - // // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - // VertexCount = (ushort)vertexCount, - // IndexCount = (uint)indexCount, - // // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - // MaterialIndex = 0, - // // TODO: this will need adjusting by parent - // SubMeshIndex = 0, - // SubMeshCount = 1, - // // TODO: update in parent - // BoneTableIndex = 0, - // // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - // StartIndex = 0, - // // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - // VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - // VertexBufferStride = strides, - // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - // }; - - // submesh - // TODO: once we have multiple submeshes, the _first_ should probably set an index offset of 0, and then further ones delta from there - and then they can be blindly adjusted by the parent that's laying out the meshes. - var xivSubmesh = new MdlStructs.SubmeshStruct() - { - IndexOffset = 0, - IndexCount = (uint)indices.Length, - AttributeIndexMask = 0, - // TODO: not sure how i want to handle these ones - BoneStartIndex = 0, - BoneCount = 1, - }; - - // var vertexBuffer = streams[0].Concat(streams[1]).Concat(streams[2]); - - return ( - vertexDeclaration, - strides, - // xivMesh, - xivSubmesh, - (ushort)vertexCount, - streams, - (uint)indices.Length, - indices, - morphData - ); - } - public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 1a1c662364fb47fbc07e330bd0ccb4b1c42fa3cd Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 20:40:30 +1100 Subject: [PATCH 1400/2451] Clean up meshes --- Penumbra/Import/Models/Import/MeshImporter.cs | 240 +++++++++++++++++ .../Import/Models/Import/SubMeshImporter.cs | 8 +- Penumbra/Import/Models/ModelManager.cs | 251 ++---------------- 3 files changed, 260 insertions(+), 239 deletions(-) create mode 100644 Penumbra/Import/Models/Import/MeshImporter.cs diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs new file mode 100644 index 00000000..e67b7c4e --- /dev/null +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -0,0 +1,240 @@ +using Lumina.Data.Parsing; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class MeshImporter +{ + public struct Mesh + { + public MdlStructs.MeshStruct MeshStruct; + public List SubMeshStructs; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + public IEnumerable VertexBuffer; + + public List Indicies; + + public List? Bones; + + public List ShapeKeys; + } + + public struct MeshShapeKey + { + public string Name; + public MdlStructs.ShapeMeshStruct ShapeMesh; + public List ShapeValues; + } + + public static Mesh Import(IEnumerable nodes) + { + var importer = new MeshImporter(nodes); + return importer.Create(); + } + + private IEnumerable _nodes; + + private List _subMeshes = new(); + + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; + private byte[]? _strides; + private ushort _vertexCount = 0; + private List[] _streams; + + private List _indices = new(); + + private List? _bones; + + private readonly Dictionary> _shapeValues = new(); + + private MeshImporter(IEnumerable nodes) + { + _nodes = nodes; + + // All meshes may use up to 3 byte streams. + _streams = new List[3]; + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex] = new List(); + } + + private Mesh Create() + { + foreach (var node in _nodes) + BuildSubMeshForNode(node); + + ArgumentNullException.ThrowIfNull(_strides); + ArgumentNullException.ThrowIfNull(_vertexDeclaration); + + return new Mesh() + { + MeshStruct = new MdlStructs.MeshStruct() + { + VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], + VertexBufferStride = _strides, + VertexCount = _vertexCount, + VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements + .Select(element => element.Stream + 1) + .Max(), + + StartIndex = 0, + IndexCount = (uint)_indices.Count, + + // TODO: import material names + MaterialIndex = 0, + + SubMeshIndex = 0, + SubMeshCount = (ushort)_subMeshes.Count, + + BoneTableIndex = 0, + }, + SubMeshStructs = _subMeshes, + + VertexDeclaration = _vertexDeclaration.Value, + VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), + + Indicies = _indices, + + Bones = _bones, + + ShapeKeys = _shapeValues + .Select(pair => new MeshShapeKey() + { + Name = pair.Key, + ShapeMesh = new MdlStructs.ShapeMeshStruct() + { + MeshIndexOffset = 0, + ShapeValueOffset = 0, + ShapeValueCount = (uint)pair.Value.Count, + }, + ShapeValues = pair.Value, + }) + .ToList(), + }; + } + + private void BuildSubMeshForNode(Node node) + { + // Record some offsets we'll be using later, before they get mutated with sub-mesh values. + var vertexOffset = _vertexCount; + var indexOffset = _indices.Count; + + var nodeBoneMap = CreateNodeBoneMap(node); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap); + + var subMeshName = node.Name ?? node.Mesh.Name; + + // Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution. + if (_vertexDeclaration == null) + _vertexDeclaration = subMesh.VertexDeclaration; + else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) + throw new Exception($"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + + // Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine. + // TODO: I mean, given that strides are derivable, might be worth dropping strides from the submesh return structure and computing when needed. + if (_strides == null) + _strides = subMesh.Strides; + + // Merge the sub-mesh streams into the main mesh stream bodies. + _vertexCount += subMesh.VertexCount; + + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex].AddRange(subMesh.Streams[streamIndex]); + + // As we're appending vertex data to the buffers, we need to update indices to point into that later block. + _indices.AddRange(subMesh.Indices.Select(index => (ushort)(index + vertexOffset))); + + // Merge the sub-mesh's shape values into the mesh's. + foreach (var (name, subMeshShapeValues) in subMesh.ShapeValues) + { + if (!_shapeValues.TryGetValue(name, out var meshShapeValues)) + { + meshShapeValues = new(); + _shapeValues.Add(name, meshShapeValues); + } + + meshShapeValues.AddRange(subMeshShapeValues.Select(value => value with + { + BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset), + ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset), + })); + } + + // And finally, merge in the sub-mesh struct itself. + _subMeshes.Add(subMesh.SubMeshStruct with + { + IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + }); + } + + private bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) + { + var elA = a.VertexElements; + var elB = b.VertexElements; + + if (elA.Length != elB.Length) return true; + + // NOTE: This assumes that elements will always be in the same order. Under the current implementation, that's guaranteed. + return elA.Zip(elB).Any(pair => + pair.First.Usage != pair.Second.Usage + || pair.First.Type != pair.Second.Type + || pair.First.Offset != pair.Second.Offset + || pair.First.Stream != pair.Second.Stream + ); + } + + private Dictionary? CreateNodeBoneMap(Node node) + { + // Unskinned assets can skip this all of this. + if (node.Skin == null) + return null; + + // Build an array of joint names, preserving the joint index from the skin. + // Any unnamed joints we'll be coalescing on a fallback bone name - though this is realistically unlikely to occur. + var jointNames = Enumerable.Range(0, node.Skin.JointsCount) + .Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint") + .ToArray(); + + // TODO: This is duplicated with the submesh importer - would be good to avoid (not that it's a huge issue). + var mesh = node.Mesh; + var meshName = node.Name ?? mesh.Name ?? "(no name)"; + var primitiveCount = mesh.Primitives.Count; + if (primitiveCount != 1) + { + throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); + } + var primitive = mesh.Primitives[0]; + + // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); + if (jointsAccessor == null) + throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + + // Build a set of joints that are referenced by this mesh. + // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. + var usedJoints = new HashSet(); + foreach (var joints in jointsAccessor.AsVector4Array()) + for (var index = 0; index < 4; index++) + usedJoints.Add((ushort)joints[index]); + + // Only initialise the bones list if we're actually going to put something in it. + if (_bones == null) + _bones = new(); + + // Build a dictionary of node-specific joint indices mesh-wide bone indices. + var nodeBoneMap = new Dictionary(); + foreach (var usedJoint in usedJoints) + { + var jointName = jointNames[usedJoint]; + var boneIndex = _bones.IndexOf(jointName); + if (boneIndex == -1) + { + boneIndex = _bones.Count; + _bones.Add(jointName); + } + nodeBoneMap.Add(usedJoint, (ushort)boneIndex); + } + + return nodeBoneMap; + } +} diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 941fa1d5..1d604105 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -8,7 +8,7 @@ public class SubMeshImporter { public struct SubMesh { - public MdlStructs.SubmeshStruct Struct; + public MdlStructs.SubmeshStruct SubMeshStruct; public MdlStructs.VertexDeclarationStruct VertexDeclaration; @@ -66,8 +66,8 @@ public class SubMeshImporter // All meshes may use up to 3 byte streams. _streams = new List[3]; - for (var i = 0; i < 3; i++) - _streams[i] = new List(); + for (var streamIndex = 0; streamIndex < 3; streamIndex++) + _streams[streamIndex] = new List(); } private SubMesh Create() @@ -83,7 +83,7 @@ public class SubMeshImporter return new SubMesh() { - Struct = new MdlStructs.SubmeshStruct() + SubMeshStruct = new MdlStructs.SubmeshStruct() { IndexOffset = 0, IndexCount = (uint)_indices.Length, diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 3153da78..65067242 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -221,23 +221,13 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable var idxOffset = indices.Count; var shapeValueOffset = shapeValues.Count; - var ( - vertexDeclaration, - // boneTable, - xivMesh, - xivSubmeshes, - meshVertexBuffer, - meshIndices, - meshShapeData, - meshBoneList - ) = MeshThing(submeshnodes); + var meshthing = MeshImporter.Import(submeshnodes); var boneTableIndex = 255; - // TODO: a better check than this would be real good - if (meshBoneList.Count() > 0) + if (meshthing.Bones != null) { var boneIndices = new List(); - foreach (var mb in meshBoneList) + foreach (var mb in meshthing.Bones) { var boneIndex = bones.IndexOf(mb); if (boneIndex == -1) @@ -262,43 +252,43 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable }); } - vertexDeclarations.Add(vertexDeclaration); - var meshStartIndex = (uint)(xivMesh.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(xivMesh with + vertexDeclarations.Add(meshthing.VertexDeclaration); + var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort)); + meshes.Add(meshthing.MeshStruct with { - SubMeshIndex = (ushort)(xivMesh.SubMeshIndex + subOffset), + SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset), // TODO: should probably define a type for index type hey. BoneTableIndex = (ushort)boneTableIndex, StartIndex = meshStartIndex, - VertexBufferOffset = xivMesh.VertexBufferOffset + VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset .Select(offset => (uint)(offset + vertOffset)) .ToArray(), }); // TODO: could probably do this with linq cleaner - foreach (var xivSubmesh in xivSubmeshes) + foreach (var xivSubmesh in meshthing.SubMeshStructs) submeshes.Add(xivSubmesh with { // TODO: this will need to keep ticking up for each submesh in the same mesh IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) }); - vertexBuffer.AddRange(meshVertexBuffer); - indices.AddRange(meshIndices.SelectMany(index => BitConverter.GetBytes((ushort)index))); - foreach (var (key, (shapeMesh, meshShapeValues)) in meshShapeData) + vertexBuffer.AddRange(meshthing.VertexBuffer); + indices.AddRange(meshthing.Indicies.SelectMany(index => BitConverter.GetBytes((ushort)index))); + foreach (var shapeKey in meshthing.ShapeKeys) { List keyshapedata; - if (!shapeData.TryGetValue(key, out keyshapedata)) + if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata)) { keyshapedata = new(); - shapeData.Add(key, keyshapedata); + shapeData.Add(shapeKey.Name, keyshapedata); } - keyshapedata.Add(shapeMesh with + keyshapedata.Add(shapeKey.ShapeMesh with { MeshIndexOffset = meshStartIndex, ShapeValueOffset = (uint)shapeValueOffset, }); - shapeValues.AddRange(meshShapeValues); + shapeValues.AddRange(shapeKey.ShapeValues); } } @@ -392,215 +382,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable Out = mdl; } - // this return type is an absolute meme, class that shit up. - private ( - MdlStructs.VertexDeclarationStruct, - // MdlStructs.BoneTableStruct, - MdlStructs.MeshStruct, - IEnumerable, - IEnumerable, - IEnumerable, - IDictionary)>, - IEnumerable - ) MeshThing(IEnumerable nodes) - { - var vertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = Array.Empty() }; - var vertexCount = (ushort)0; - // there's gotta be a better way to do this with streams or enumerables or something, surely - var streams = new List[3]; - for (var i = 0; i < 3; i++) - streams[i] = new List(); - var indexCount = (uint)0; - var indices = new List(); - var strides = new byte[] { 0, 0, 0 }; - var submeshes = new List(); - var morphData = new Dictionary>(); - - /* - THOUGHTS - per submesh node, before calling down to build the mesh, build a bone mapping of joint index -> bone name (not node index) - the joint indexes are what will be used in the vertices. - per submesh node, eagerly collect all blend indexes (joints) used before building anything - just as a set or something - the above means i can create a limited set and a mapping, i.e. if skeleton contains {0->a 1->b 2->c}, and mesh contains 0, 2, then i can output [a, c] + {0->0, 2->1} - (throw if >64 entries in that name array) - - then for the second prim, - again get the joint-name mapping, and again get the joint set - then can extend the values. using the samme example, if skeleton2 contains {0->c 1->d, 2->e} and mesh contains [0,2] again, then bone array can be extended to [a, c, e] and the mesh-specific mapping would be {0->1, 2->2} - - repeat, etc - */ - - var usedBones = new List(); - - // TODO: check that attrs/elems/strides match - we should be generating per-mesh stuff for sanity's sake, but we need to make sure they match if there's >1 node mesh in a mesh. - foreach (var node in nodes) - { - var vertOff = vertexCount; - var idxOff = indexCount; - - Dictionary? nodeBoneMap = null; - var bonething = WalkBoneThing(node); - if (bonething.HasValue) - { - var (boneNames, usedJoints) = bonething.Value; - nodeBoneMap = new(); - - // todo: probably linq this shit - foreach (var usedJoint in usedJoints) - { - // this is the 0,2 - var boneName = boneNames[usedJoint]; - var boneIdx = usedBones.IndexOf(boneName); - if (boneIdx == -1) - { - boneIdx = usedBones.Count; - usedBones.Add(boneName); - } - nodeBoneMap.Add(usedJoint, (ushort)boneIdx); - } - - Penumbra.Log.Information($"nbm {string.Join(",", nodeBoneMap.Select(kv => $"{kv.Key}:{kv.Value}"))}"); - } - - var subMeshThingy = SubMeshImporter.Import(node, nodeBoneMap); - vertexDeclaration = subMeshThingy.VertexDeclaration; // TODO: CHECK EQUAL AFTER FIRST - strides = subMeshThingy.Strides; // ALSO CHECK EQUAL - vertexCount += subMeshThingy.VertexCount; - for (var i = 0; i < 3; i++) - streams[i].AddRange(subMeshThingy.Streams[i]); - indexCount += (uint)subMeshThingy.Indices.Length; - // we need to offset the indexes to point into the new stuff - indices.AddRange(subMeshThingy.Indices.Select(idx => (ushort)(idx + vertOff))); - submeshes.Add(subMeshThingy.Struct with - { - IndexOffset = subMeshThingy.Struct.IndexOffset + idxOff - // TODO: bone stuff probably - }); - // TODO: HANDLE MORPHS, NEED TO ADJUST EVERY VALUE'S INDEX OFFSETS - foreach (var (key, shapeValues) in subMeshThingy.ShapeValues) - { - List valueList; - if (!morphData.TryGetValue(key, out valueList)) - { - valueList = new(); - morphData.Add(key, valueList); - } - valueList.AddRange( - shapeValues - .Select(value => value with - { - // but this is actually an index index - BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + idxOff), - // this is a vert idx - ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertOff), - }) - ); - } - } - - // one of these per skinned mesh. - // TODO: check if mesh has skinning at all. (err if mixed?) - // var boneTable = new MdlStructs.BoneTableStruct() - // { - // BoneCount = 1, - // // this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files. - // BoneIndex = new ushort[64], - // }; - - // mesh - var xivMesh = new MdlStructs.MeshStruct() - { - // TODO: sum across submeshes. - // TODO: would be cool to share verts on submesh boundaries but that's way out of scope for now. - VertexCount = vertexCount, - IndexCount = indexCount, - // TODO: will have to think about how to represent this - materials can be named, so maybe adjust in parent? - MaterialIndex = 0, - // TODO: this will need adjusting by parent - SubMeshIndex = 0, - SubMeshCount = (ushort)submeshes.Count, - // TODO: update in parent - BoneTableIndex = 0, - // TODO: this is relative to the lod's index buffer, and is an index, not byte offset - StartIndex = 0, - // TODO: these are relative to the lod vertex buffer. these values are accurate for a 0 offset, but lod will need to adjust - VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)], - VertexBufferStride = strides, - // VertexStreamCount = /* 2 */ (byte)(attributes.Select(attribute => attribute.Element.Stream).Max() + 1), - VertexStreamCount = (byte)(vertexDeclaration.VertexElements.Select(element => element.Stream).Max() + 1) - }; - - // TODO: can probably get away with flattening the values and blindly setting offsets in parent - mesh matters above, but the values are already Dealt With at this point - var shapeData = morphData.ToDictionary( - (pair) => pair.Key, - pair => ( - new MdlStructs.ShapeMeshStruct() - { - // TODO: this needs to be adjusted by the parent - MeshIndexOffset = 0, - ShapeValueCount = (uint)pair.Value.Count, - // TODO: Also update by parent - ShapeValueOffset = 0, - }, - pair.Value - ) - ); - - return ( - vertexDeclaration, - // boneTable, - xivMesh, - submeshes, - streams[0].Concat(streams[1]).Concat(streams[2]), - indices, - shapeData, - usedBones - ); - } - - private (string[], ISet)? WalkBoneThing(Node node) - { - // - if (node.Skin == null) - return null; - - var jointNames = Enumerable.Range(0, node.Skin.JointsCount) - .Select(index => node.Skin.GetJoint(index).Joint.Name ?? $"UNNAMED") - .ToArray(); - - // it might make sense to do this in the submesh handling - i do need to maintain the mesh-wide bone list, but that can be passed in/out, perhaps? - var mesh = node.Mesh; - if (mesh.Primitives.Count != 1) - throw new Exception($"Mesh has {mesh.Primitives.Count} primitives, expected 1."); - var primitive = mesh.Primitives[0]; - - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); - if (jointsAccessor == null) - throw new Exception($"Skinned meshes must contain a JOINTS_0 attribute."); - - // var weightsAccssor = primitive.GetVertexAccessor("WEIGHTS_0"); - // if (weightsAccssor == null) - // throw new Exception($"Skinned meshes must contain a WEIGHTS_0 attribute."); - - var usedJoints = new HashSet(); - - // TODO: would be neat to omit any joints that are only used in 0-weight positions, but doing so would require being a _little_ smarter in vertex attrs on how to fall back when mappings aren't found - or otherwise try to ensure that mappings for unused stuff always exists - // foreach (var (joints, weights) in jointsAccessor.AsVector4Array().Zip(weightsAccssor.AsVector4Array())) - // { - // for (var index = 0; index < 4; index++) - // if (weights[index] > 0) - // usedJoints.Add((ushort)joints[index]); - // } - - foreach (var joints in jointsAccessor.AsVector4Array()) - { - for (var index = 0; index < 4; index++) - usedJoints.Add((ushort)joints[index]); - } - - return (jointNames, usedJoints); - } - public bool Equals(IAction? other) { if (other is not ImportGltfAction rhs) From 13d594ca878c6a3cd7e12beb4ea7e8601aa5ad93 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 6 Jan 2024 23:13:34 +1100 Subject: [PATCH 1401/2451] Clean up models --- .../Import/Models/Import/ModelImporter.cs | 224 ++++++++++++++++ Penumbra/Import/Models/ModelManager.cs | 252 +----------------- 2 files changed, 226 insertions(+), 250 deletions(-) create mode 100644 Penumbra/Import/Models/Import/ModelImporter.cs diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs new file mode 100644 index 00000000..f53d2b64 --- /dev/null +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -0,0 +1,224 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public partial class ModelImporter +{ + public static MdlFile Import(ModelRoot model) + { + var importer = new ModelImporter(model); + return importer.Create(); + } + + // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled)] + private static partial Regex MeshNameGroupingRegex(); + + private readonly ModelRoot _model; + + private List _meshes = new(); + private List _subMeshes = new(); + + private List _vertexDeclarations = new(); + private List _vertexBuffer = new(); + + private List _indices = new(); + + private List _bones = new(); + private List _boneTables = new(); + + private Dictionary> _shapeMeshes = new(); + private List _shapeValues = new(); + + private ModelImporter(ModelRoot model) + { + _model = model; + } + + private MdlFile Create() + { + // Group and build out meshes in this model. + foreach (var subMeshNodes in GroupedMeshNodes()) + BuildMeshForGroup(subMeshNodes); + + // Now that all of the meshes have been built, we can build some of the model-wide metadata. + var shapes = new List(); + var shapeMeshes = new List(); + foreach (var (keyName, keyMeshes) in _shapeMeshes) + { + shapes.Add(new MdlFile.Shape() + { + ShapeName = keyName, + // NOTE: these values are per-LoD. + ShapeMeshStartIndex = [(ushort)shapeMeshes.Count, 0, 0], + ShapeMeshCount = [(ushort)keyMeshes.Count, 0, 0], + }); + shapeMeshes.AddRange(keyMeshes); + } + + var indexBuffer = _indices.SelectMany(BitConverter.GetBytes).ToArray(); + + var emptyBoundingBox = new MdlStructs.BoundingBoxStruct() + { + Min = [0, 0, 0, 0], + Max = [0, 0, 0, 0], + }; + + // And finally, the MdlFile itself. + return new MdlFile() + { + VertexOffset = [0, 0, 0], + VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0], + IndexOffset = [(uint)_vertexBuffer.Count, 0, 0], + IndexBufferSize = [(uint)indexBuffer.Length, 0, 0], + + VertexDeclarations = _vertexDeclarations.ToArray(), + Meshes = _meshes.ToArray(), + SubMeshes = _subMeshes.ToArray(), + + BoneTables = _boneTables.ToArray(), + Bones = _bones.ToArray(), + // TODO: Game doesn't seem to rely on this, but would be good to populate. + SubMeshBoneMap = [], + + Shapes = shapes.ToArray(), + ShapeMeshes = shapeMeshes.ToArray(), + ShapeValues = _shapeValues.ToArray(), + + LodCount = 1, + + Lods = [new MdlStructs.LodStruct() + { + MeshIndex = 0, + MeshCount = (ushort)_meshes.Count, + + ModelLodRange = 0, + TextureLodRange = 0, + + VertexDataOffset = 0, + VertexBufferSize = (uint)_vertexBuffer.Count, + IndexDataOffset = (uint)_vertexBuffer.Count, + IndexBufferSize = (uint)indexBuffer.Length, + }], + + // TODO: Would be good to populate from gltf material names. + Materials = ["/NO_MATERIAL"], + + // TODO: Would be good to calculate all of this up the tree. + Radius = 1, + BoundingBoxes = emptyBoundingBox, + BoneBoundingBoxes = Enumerable.Repeat(emptyBoundingBox, _bones.Count).ToArray(), + + RemainingData = [.._vertexBuffer, ..indexBuffer], + }; + } + + /// Returns an iterator over sorted, grouped mesh nodes. + private IEnumerable> GroupedMeshNodes() => + _model.LogicalNodes + .Where(node => node.Mesh != null) + .Select(node => + { + var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; + var match = MeshNameGroupingRegex().Match(name); + return (node, match); + }) + .Where(pair => pair.match.Success) + .OrderBy(pair => + { + var subMeshGroup = pair.match.Groups["SubMesh"]; + return subMeshGroup.Success ? int.Parse(subMeshGroup.Value) : 0; + }) + .GroupBy( + pair => int.Parse(pair.match.Groups["Mesh"].Value), + pair => pair.node + ) + .OrderBy(group => group.Key); + + private void BuildMeshForGroup(IEnumerable subMeshNodes) + { + // Record some offsets we'll be using later, before they get mutated with mesh values. + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; + var shapeValueOffset = _shapeValues.Count; + + var mesh = MeshImporter.Import(subMeshNodes); + var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); + + // If no bone table is used for a mesh, the index is set to 255. + var boneTableIndex = 255; + if (mesh.Bones != null) + boneTableIndex = BuildBoneTable(mesh.Bones); + + _meshes.Add(mesh.MeshStruct with + { + SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), + BoneTableIndex = (ushort)boneTableIndex, + StartIndex = meshStartIndex, + VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset + .Select(offset => (uint)(offset + vertexOffset)) + .ToArray(), + }); + + foreach (var subMesh in mesh.SubMeshStructs) + _subMeshes.Add(subMesh with + { + IndexOffset = (uint)(subMesh.IndexOffset + indexOffset), + }); + + _vertexDeclarations.Add(mesh.VertexDeclaration); + _vertexBuffer.AddRange(mesh.VertexBuffer); + + _indices.AddRange(mesh.Indicies); + + foreach (var meshShapeKey in mesh.ShapeKeys) + { + if (!_shapeMeshes.TryGetValue(meshShapeKey.Name, out var shapeMeshes)) + { + shapeMeshes = new(); + _shapeMeshes.Add(meshShapeKey.Name, shapeMeshes); + } + + shapeMeshes.Add(meshShapeKey.ShapeMesh with + { + MeshIndexOffset = meshStartIndex, + ShapeValueOffset = (uint)shapeValueOffset, + }); + + _shapeValues.AddRange(meshShapeKey.ShapeValues); + } + } + + private ushort BuildBoneTable(List boneNames) + { + var boneIndices = new List(); + foreach (var boneName in boneNames) + { + var boneIndex = _bones.IndexOf(boneName); + if (boneIndex == -1) + { + boneIndex = _bones.Count; + _bones.Add(boneName); + } + boneIndices.Add((ushort)boneIndex); + } + + if (boneIndices.Count > 64) + throw new Exception("XIV does not support meshes weighted to more than 64 bones."); + + var boneIndicesArray = new ushort[64]; + Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + + var boneTableIndex = _boneTables.Count; + _boneTables.Add(new MdlStructs.BoneTableStruct() + { + BoneIndex = boneIndicesArray, + BoneCount = (byte)boneIndices.Count, + }); + + return (ushort)boneTableIndex; + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 65067242..ebbe0411 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,20 +1,15 @@ using Dalamud.Plugin.Services; -using Lumina.Data.Parsing; -using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; -using SharpGLTF.Geometry; -using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.Materials; using SharpGLTF.Scenes; using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed partial class ModelManager : SingleTaskQueue, IDisposable +public sealed class ModelManager : SingleTaskQueue, IDisposable { private readonly IFramework _framework; private readonly IDataManager _gameData; @@ -125,10 +120,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable private partial class ImportGltfAction : IAction { - // TODO: clean this up a bit, i don't actually need all of it. - [GeneratedRegex(@".*[_ ^](?'Mesh'[0-9]+)[\\.\\-]?([0-9]+)?$", RegexOptions.Compiled)] - private static partial Regex MeshNameGroupingRegex(); - public MdlFile? Out; public ImportGltfAction() @@ -136,250 +127,11 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable // } - private ModelRoot Build() - { - // Build a super simple plane as a fake gltf input. - var material = new MaterialBuilder(); - var mesh = new MeshBuilder("mesh 0.0"); - var prim = mesh.UsePrimitive(material); - var tangent = new Vector4(.5f, .5f, 0, 1); - var vert1 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(-1, 0, 1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.UnitY, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert2 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(1, 0, 1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.One, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert3 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(-1, 0, -1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.Zero, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - var vert4 = new VertexBuilder( - new VertexPositionNormalTangent(new Vector3(1, 0, -1), Vector3.UnitY, tangent), - new VertexColor1Texture2(Vector4.One, Vector2.UnitX, Vector2.Zero), - new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)]) - ); - prim.AddTriangle(vert2, vert3, vert1); - prim.AddTriangle(vert2, vert4, vert3); - var jKosi = new NodeBuilder("j_kosi"); - var scene = new SceneBuilder(); - scene.AddNode(jKosi); - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, [jKosi]); - var model = scene.ToGltf2(); - - return model; - } - public void Execute(CancellationToken cancel) { var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); - // TODO: for grouping, should probably use `node.name ?? mesh.name`, as which are set seems to depend on the exporter. - // var nodes = model.LogicalNodes - // .Where(node => node.Mesh != null) - // // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that. - // .Take(3); - - // tt uses this - // ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - var nodes = model.LogicalNodes - .Where(node => node.Mesh != null) - .Take(6) // this model has all 3 lods in it - the first 6 are the real lod0 - .SelectWhere(node => - { - var name = node.Name ?? node.Mesh.Name; - var match = MeshNameGroupingRegex().Match(name); - return match.Success - ? (true, (node, int.Parse(match.Groups["Mesh"].Value))) - : (false, (node, -1)); - }) - .GroupBy(pair => pair.Item2, pair => pair.node) - .OrderBy(group => group.Key); - - // this is a representation of a single LoD - var vertexDeclarations = new List(); - var bones = new List(); - var boneTables = new List(); - var meshes = new List(); - var submeshes = new List(); - var vertexBuffer = new List(); - var indices = new List(); - - var shapeData = new Dictionary>(); - var shapeValues = new List(); - - foreach (var submeshnodes in nodes) - { - var boneTableOffset = boneTables.Count; - var meshOffset = meshes.Count; - var subOffset = submeshes.Count; - var vertOffset = vertexBuffer.Count; - var idxOffset = indices.Count; - var shapeValueOffset = shapeValues.Count; - - var meshthing = MeshImporter.Import(submeshnodes); - - var boneTableIndex = 255; - if (meshthing.Bones != null) - { - var boneIndices = new List(); - foreach (var mb in meshthing.Bones) - { - var boneIndex = bones.IndexOf(mb); - if (boneIndex == -1) - { - boneIndex = bones.Count; - bones.Add(mb); - } - boneIndices.Add((ushort)boneIndex); - } - - if (boneIndices.Count > 64) - throw new Exception("One mesh cannot be weighted to more than 64 bones."); - - var boneIndicesArray = new ushort[64]; - Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); - - boneTableIndex = boneTableOffset; - boneTables.Add(new MdlStructs.BoneTableStruct() - { - BoneCount = (byte)boneIndices.Count, - BoneIndex = boneIndicesArray, - }); - } - - vertexDeclarations.Add(meshthing.VertexDeclaration); - var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort)); - meshes.Add(meshthing.MeshStruct with - { - SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset), - // TODO: should probably define a type for index type hey. - BoneTableIndex = (ushort)boneTableIndex, - StartIndex = meshStartIndex, - VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset - .Select(offset => (uint)(offset + vertOffset)) - .ToArray(), - }); - // TODO: could probably do this with linq cleaner - foreach (var xivSubmesh in meshthing.SubMeshStructs) - submeshes.Add(xivSubmesh with - { - // TODO: this will need to keep ticking up for each submesh in the same mesh - IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort)) - }); - vertexBuffer.AddRange(meshthing.VertexBuffer); - indices.AddRange(meshthing.Indicies.SelectMany(index => BitConverter.GetBytes((ushort)index))); - foreach (var shapeKey in meshthing.ShapeKeys) - { - List keyshapedata; - if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata)) - { - keyshapedata = new(); - shapeData.Add(shapeKey.Name, keyshapedata); - } - - keyshapedata.Add(shapeKey.ShapeMesh with - { - MeshIndexOffset = meshStartIndex, - ShapeValueOffset = (uint)shapeValueOffset, - }); - - shapeValues.AddRange(shapeKey.ShapeValues); - } - } - - var shapes = new List(); - var shapeMeshes = new List(); - - foreach (var (name, sms) in shapeData) - { - var smOff = shapeMeshes.Count; - - shapeMeshes.AddRange(sms); - shapes.Add(new MdlFile.Shape() - { - ShapeName = name, - // TODO: THESE IS PER LOD - ShapeMeshStartIndex = [(ushort)smOff, 0, 0], - ShapeMeshCount = [(ushort)sms.Count, 0, 0], - }); - } - - var mdl = new MdlFile() - { - Radius = 1, - // todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow. - VertexOffset = [0, 0, 0], - IndexOffset = [(uint)vertexBuffer.Count, 0, 0], - VertexBufferSize = [(uint)vertexBuffer.Count, 0, 0], - IndexBufferSize = [(uint)indices.Count, 0, 0], - LodCount = 1, - BoundingBoxes = new MdlStructs.BoundingBoxStruct() - { - Min = [-1, 0, -1, 1], - Max = [1, 0, 1, 1], - }, - VertexDeclarations = vertexDeclarations.ToArray(), - Meshes = meshes.ToArray(), - BoneTables = boneTables.ToArray(), - BoneBoundingBoxes = [ - // new MdlStructs.BoundingBoxStruct() - // { - // Min = [ - // -0.081672676f, - // -0.113717034f, - // -0.11905348f, - // 1.0f, - // ], - // Max = [ - // 0.03941727f, - // 0.09845419f, - // 0.107391916f, - // 1.0f, - // ], - // }, - - // _would_ be nice if i didn't need to fill out this - new MdlStructs.BoundingBoxStruct() - { - Min = [0, 0, 0, 0], - Max = [0, 0, 0, 0], - } - ], - SubMeshes = submeshes.ToArray(), - - // TODO pretty sure this is garbage data as far as textools functions - // game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh - SubMeshBoneMap = [0], - - Shapes = shapes.ToArray(), - ShapeMeshes = shapeMeshes.ToArray(), - ShapeValues = shapeValues.ToArray(), - - Lods = [new MdlStructs.LodStruct() - { - MeshIndex = 0, - MeshCount = (ushort)meshes.Count, - ModelLodRange = 0, - TextureLodRange = 0, - VertexBufferSize = (uint)vertexBuffer.Count, - VertexDataOffset = 0, - IndexBufferSize = (uint)indices.Count, - IndexDataOffset = (uint)vertexBuffer.Count, - }, - ], - Bones = bones.ToArray(), - Materials = [ - "/mt_c0201e6180_top_a.mtrl", - ], - RemainingData = vertexBuffer.Concat(indices).ToArray(), - }; - - Out = mdl; + Out = ModelImporter.Import(model); } public bool Equals(IAction? other) From 51bb9cf7cdabb7f0c47eb91405aa80e4945389ed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 6 Jan 2024 18:26:30 +0100 Subject: [PATCH 1402/2451] Use existing game path functionality for sklb resolving, some cleanup. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 84 ++++++++++--------- .../ModEditWindow.Models.MdlTab.cs | 79 +++-------------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 17 ++-- 4 files changed, 69 insertions(+), 113 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ac3fc098..83c01275 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ac3fc0981ac8f503ac91d2419bd28c54f271763e +Subproject commit 83c012752cd9d13d39248eda85ab18cc59070a76 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 35a5e53e..dd796a42 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,27 +1,20 @@ using Dalamud.Plugin.Services; using OtterGui.Tasks; -using Penumbra.Collections.Manager; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Models.Export; using SharpGLTF.Scenes; namespace Penumbra.Import.Models; -public sealed class ModelManager : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable { - private readonly IFramework _framework; - private readonly IDataManager _gameData; - private readonly ActiveCollectionData _activeCollectionData; + private readonly IFramework _framework = framework; private readonly ConcurrentDictionary _tasks = new(); - private bool _disposed = false; - public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData) - { - _framework = framework; - _gameData = gameData; - _activeCollectionData = activeCollectionData; - } + private bool _disposed; public void Dispose() { @@ -31,6 +24,31 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable _tasks.Clear(); } + public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); + + /// Try to find the .sklb path for a .mdl file. + /// .mdl file to look up the skeleton for. + public string? ResolveSklbForMdl(string mdlPath) + { + var info = _parser.GetFileInfo(mdlPath); + if (info.FileType is not FileType.Model) + return null; + + return info.ObjectType switch + { + ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), + ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), + ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", + 1), + ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), + ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId), + ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId), + ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId), + _ => null, + }; + } + private Task Enqueue(IAction action) { if (_disposed) @@ -39,44 +57,34 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Task task; lock (_tasks) { - task = _tasks.GetOrAdd(action, action => + task = _tasks.GetOrAdd(action, a => { var token = new CancellationTokenSource(); - var task = Enqueue(action, token.Token); - task.ContinueWith(_ => _tasks.TryRemove(action, out var unused), CancellationToken.None); - return (task, token); + var t = Enqueue(a, token.Token); + t.ContinueWith(_ => + { + lock (_tasks) + { + return _tasks.TryRemove(a, out var unused); + } + }, CancellationToken.None); + return (t, token); }).Item1; } return task; } - public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); - - private class ExportToGltfAction : IAction + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath) + : IAction { - private readonly ModelManager _manager; - - private readonly MdlFile _mdl; - private readonly SklbFile? _sklb; - private readonly string _outputPath; - - public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath) - { - _manager = manager; - _mdl = mdl; - _sklb = sklb; - _outputPath = outputPath; - } - public void Execute(CancellationToken cancel) { Penumbra.Log.Debug("Reading skeleton."); var xivSkeleton = BuildSkeleton(cancel); Penumbra.Log.Debug("Converting model."); - var model = ModelExporter.Export(_mdl, xivSkeleton); + var model = ModelExporter.Export(mdl, xivSkeleton); Penumbra.Log.Debug("Building scene."); var scene = new SceneBuilder(); @@ -84,16 +92,16 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable Penumbra.Log.Debug("Saving."); var gltfModel = scene.ToGltf2(); - gltfModel.SaveGLTF(_outputPath); + gltfModel.SaveGLTF(outputPath); } /// Attempt to read out the pertinent information from a .sklb. private XivSkeleton? BuildSkeleton(CancellationToken cancel) { - if (_sklb == null) + if (sklb == null) return null; - var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton)); + var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton)); xmlTask.Wait(cancel); var xml = xmlTask.Result; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 93e674ea..b8573780 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -8,9 +8,9 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private partial class MdlTab : IWritable + private class MdlTab : IWritable { - private ModEditWindow _edit; + private readonly ModEditWindow _edit; public readonly MdlFile Mdl; private readonly List[] _attributes; @@ -18,21 +18,10 @@ public partial class ModEditWindow public List? GamePaths { get; private set; } public int GamePathIndex; - public bool PendingIo { get; private set; } = false; - public string? IoException { get; private set; } = null; + public bool PendingIo { get; private set; } + public string? IoException { get; private set; } - [GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] - private static partial Regex CharaEquipmentRegex(); - - [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", - RegexOptions.Compiled)] - private static partial Regex CharaHumanRegex(); - - [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", - RegexOptions.Compiled)] - private static partial Regex CharaBodyRegex(); - - public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) + public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod) { _edit = edit; @@ -54,7 +43,7 @@ public partial class ModEditWindow /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. /// Mod within which the .mdl is resolved. - private void FindGamePaths(string path, Mod mod) + private void FindGamePaths(string path, IMod mod) { if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p)) { @@ -77,8 +66,8 @@ public partial class ModEditWindow task.ContinueWith(t => { IoException = t.Exception?.ToString(); - PendingIo = false; GamePaths = t.Result; + PendingIo = false; }); } @@ -89,7 +78,7 @@ public partial class ModEditWindow SklbFile? sklb = null; try { - var sklbPath = GetSklbPath(mdlPath.ToString()); + var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString()); sklb = sklbPath != null ? ReadSklb(sklbPath) : null; } catch (Exception exception) @@ -107,43 +96,6 @@ public partial class ModEditWindow }); } - /// Try to find the .sklb path for a .mdl file. - /// .mdl file to look up the skeleton for. - private string? GetSklbPath(string mdlPath) - { - // Equipment is skinned to the base body skeleton of the race they target. - var match = CharaEquipmentRegex().Match(mdlPath); - if (match.Success) - { - var race = match.Groups["Race"].Value; - return $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb"; - } - - // Some parts of human have their own skeletons. - match = CharaHumanRegex().Match(mdlPath); - if (match.Success) - { - var type = match.Groups["Type"].Value; - var race = match.Groups["Race"].Value; - return type switch - { - "body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb", - _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), - }; - } - - // A few subcategories - such as weapons, demihumans, and monsters - have dedicated per-"body" skeletons. - match = CharaBodyRegex().Match(mdlPath); - if (match.Success) - { - var subCategory = match.Groups["SubCategory"].Value; - var set = match.Groups["Set"].Value; - return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb"; - } - - return null; - } - /// Read a .sklb from the active collection or game. /// Game path to the .sklb to load. private SklbFile ReadSklb(string sklbPath) @@ -153,17 +105,12 @@ public partial class ModEditWindow throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); - // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath switch - { - null => _edit._gameData.GetFile(sklbPath)?.Data, - FullPath path => File.ReadAllBytes(path.ToPath()), - }; - if (bytes == null) - throw new Exception( + // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... + var bytes = resolvedPath == null ? _edit._gameData.GetFile(sklbPath)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); + return bytes != null + ? new SklbFile(bytes) + : throw new Exception( $"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); - - return new SklbFile(bytes); } /// Remove the material given by the index. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 3891eb95..24b45b88 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,7 +15,6 @@ public partial class ModEditWindow private const int MdlMaterialMaximum = 4; private readonly FileEditor _modelTab; - private readonly ModelManager _models; private string _modelNewMaterial = string.Empty; @@ -91,19 +90,21 @@ public partial class ModEditWindow private void DrawGamePathCombo(MdlTab tab) { - if (tab.GamePaths!.Count == 0) + if (tab.GamePaths!.Count != 0) { - ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting."); - if (ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) - if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) - _customGamePath = Utf8GamePath.Empty; - + DrawComboButton(tab); return; } - DrawComboButton(tab); + ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting."); + if (!ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) + return; + + if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) + _customGamePath = Utf8GamePath.Empty; } + /// I disliked the combo with only one selection so turn it into a button in that case. private static void DrawComboButton(MdlTab tab) { const string label = "Game Path"; From 677c9bd801490ddb961f5c923ab11a5c879c946f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 6 Jan 2024 18:37:52 +0100 Subject: [PATCH 1403/2451] Some cleanup and using new features / intellisense recommendations. --- Penumbra/Import/Models/Export/MeshExporter.cs | 113 +++++++++--------- .../Import/Models/Export/ModelExporter.cs | 23 +--- Penumbra/Import/Models/Export/Skeleton.cs | 9 +- Penumbra/Import/Models/HavokConverter.cs | 50 ++++---- Penumbra/Import/Models/SkeletonConverter.cs | 50 ++++---- 5 files changed, 117 insertions(+), 128 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 1b53df8a..84628c2c 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,24 +13,17 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh + public class Mesh(IEnumerable> meshes, NodeBuilder[]? joints) { - private IMeshBuilder[] _meshes; - private NodeBuilder[]? _joints; - - public Mesh(IMeshBuilder[] meshes, NodeBuilder[]? joints) - { - _meshes = meshes; - _joints = joints; - } - public void AddToScene(SceneBuilder scene) { - foreach (var mesh in _meshes) - if (_joints == null) + foreach (var mesh in meshes) + { + if (joints == null) scene.AddRigidMesh(mesh, Matrix4x4.Identity); else - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints); + scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints); + } } } @@ -43,9 +36,11 @@ public class MeshExporter private const byte MaximumMeshBufferStreams = 3; private readonly MdlFile _mdl; - private readonly byte _lod; - private readonly ushort _meshIndex; - private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; + private readonly byte _lod; + private readonly ushort _meshIndex; + + private MdlStructs.MeshStruct XivMesh + => _mdl.Meshes[_meshIndex]; private readonly Dictionary? _boneIndexMap; @@ -53,10 +48,10 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary? boneNameMap) { - _mdl = mdl; - _lod = lod; + _mdl = mdl; + _lod = lod; _meshIndex = meshIndex; if (boneNameMap != null) @@ -76,11 +71,12 @@ public class MeshExporter if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); - Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); + Penumbra.Log.Debug( + $"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); } - /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. - private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap) + /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. + private Dictionary? BuildBoneIndexMap(IReadOnlyDictionary boneNameMap) { // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) @@ -105,11 +101,10 @@ public class MeshExporter /// Build glTF meshes for this XIV mesh. private IMeshBuilder[] BuildMeshes() { - var indices = BuildIndices(); + var indices = BuildIndices(); var vertices = BuildVertices(); // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. - if (XivMesh.SubMeshCount == 0) return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; @@ -117,7 +112,8 @@ public class MeshExporter .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) .WithIndex() - .Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount)) + .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount)) .ToArray(); } @@ -161,7 +157,7 @@ public class MeshExporter } var primitiveVertices = meshBuilder.Primitives.First().Vertices; - var shapeNames = new List(); + var shapeNames = new List(); foreach (var shape in _mdl.Shapes) { @@ -177,24 +173,28 @@ public class MeshExporter ) .Where(shapeValue => shapeValue.BaseIndicesIndex >= indexBase - && shapeValue.BaseIndicesIndex < indexBase + indexCount + && shapeValue.BaseIndicesIndex < indexBase + indexCount ) .ToList(); - if (shapeValues.Count == 0) continue; + if (shapeValues.Count == 0) + continue; var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count); shapeNames.Add(shape.ShapeName); foreach (var shapeValue in shapeValues) + { morphBuilder.SetVertex( primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(), vertices[shapeValue.ReplacingVertexIndex].GetGeometry() ); + } } - meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() { - {"targetNames", shapeNames} + meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() + { + { "targetNames", shapeNames }, }); return meshBuilder; @@ -249,23 +249,25 @@ public class MeshExporter } /// Read a vertex attribute of the specified type from a vertex buffer stream. - private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) + private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { return type switch { MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - MdlFile.VertexType.UInt => reader.ReadBytes(4), - MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), + MdlFile.VertexType.UInt => reader.ReadBytes(4), + MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), + (float)reader.ReadHalf()), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(), }; } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary usages) + private static Type GetGeometryType(IReadOnlyDictionary usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw new Exception("Mesh does not contain position vertex elements."); @@ -304,16 +306,16 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary usages) + private static Type GetMaterialType(IReadOnlyDictionary usages) { var uvCount = 0; if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) uvCount = type switch { - MdlFile.VertexType.Half2 => 1, - MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, MdlFile.VertexType.Single4 => 2, - _ => throw new Exception($"Unexpected UV vertex type {type}.") + _ => throw new Exception($"Unexpected UV vertex type {type}."), }; var materialUsages = ( @@ -323,11 +325,11 @@ public class MeshExporter return materialUsages switch { - (2, true) => typeof(VertexColor1Texture2), + (2, true) => typeof(VertexColor1Texture2), (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexColor1Texture1), + (1, true) => typeof(VertexColor1Texture1), (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColor1), + (0, true) => typeof(VertexColor1), (0, false) => typeof(VertexEmpty), _ => throw new Exception("Unreachable."), @@ -377,7 +379,7 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private Type GetSkinningType(IReadOnlyDictionary usages) + private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) return typeof(VertexJoints4); @@ -400,7 +402,8 @@ public class MeshExporter var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); var bindings = Enumerable.Range(0, 4) - .Select(bindingIndex => { + .Select(bindingIndex => + { // NOTE: I've not seen any files that throw this error that aren't completely broken. var xivBoneIndex = indices[bindingIndex]; if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) @@ -417,44 +420,44 @@ public class MeshExporter /// Clamps any tangent W value other than 1 to -1. /// Some XIV models seemingly store -1 as 0, this patches over that. - private Vector4 FixTangentVector(Vector4 tangent) + private static Vector4 FixTangentVector(Vector4 tangent) => tangent with { W = tangent.W == 1 ? 1 : -1 }; /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. - private Vector2 ToVector2(object data) + private static Vector2 ToVector2(object data) => data switch { Vector2 v2 => v2, Vector3 v3 => new Vector2(v3.X, v3.Y), Vector4 v4 => new Vector2(v4.X, v4.Y), - _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}"), }; /// Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. - private Vector3 ToVector3(object data) + private static Vector3 ToVector3(object data) => data switch { Vector2 v2 => new Vector3(v2.X, v2.Y, 0), Vector3 v3 => v3, Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z), - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"), }; /// Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. - private Vector4 ToVector4(object data) + private static Vector4 ToVector4(object data) => data switch { - Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), + Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0), Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1), Vector4 v4 => v4, - _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"), }; /// Convert a vertex attribute value to a byte array. - private byte[] ToByteArray(object data) + private static byte[] ToByteArray(object data) => data switch { byte[] value => value, - _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}") + _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 35819e7a..2060c323 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -6,26 +6,17 @@ namespace Penumbra.Import.Models.Export; public class ModelExporter { - public class Model + public class Model(List meshes, GltfSkeleton? skeleton) { - private List _meshes; - private GltfSkeleton? _skeleton; - - public Model(List meshes, GltfSkeleton? skeleton) - { - _meshes = meshes; - _skeleton = skeleton; - } - public void AddToScene(SceneBuilder scene) { // If there's a skeleton, the root node should be added before we add any potentially skinned meshes. - var skeletonRoot = _skeleton?.Root; + var skeletonRoot = skeleton?.Root; if (skeletonRoot != null) scene.AddNode(skeletonRoot); // Add all the meshes to the scene. - foreach (var mesh in _meshes) + foreach (var mesh in meshes) mesh.AddToScene(scene); } } @@ -64,10 +55,8 @@ public class ModelExporter NodeBuilder? root = null; var names = new Dictionary(); var joints = new List(); - for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++) + foreach (var bone in skeleton.Bones) { - var bone = skeleton.Bones[boneIndex]; - if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); @@ -93,10 +82,10 @@ public class ModelExporter if (root == null) return null; - return new() + return new GltfSkeleton { Root = root, - Joints = joints.ToArray(), + Joints = [.. joints], Names = names, }; } diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index 09cdcc32..fee107a0 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -3,14 +3,9 @@ using SharpGLTF.Scenes; namespace Penumbra.Import.Models.Export; /// Representation of a skeleton within XIV. -public class XivSkeleton +public class XivSkeleton(XivSkeleton.Bone[] bones) { - public Bone[] Bones; - - public XivSkeleton(Bone[] bones) - { - Bones = bones; - } + public Bone[] Bones = bones; public struct Bone { diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 7f87d50a..01c27b61 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -16,17 +16,18 @@ public static unsafe class HavokConverter /// A byte array representing the .hkx file. public static string HkxToXml(byte[] hkx) { + const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.TextFormat + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + var tempHkx = CreateTempFile(); File.WriteAllBytes(tempHkx, hkx); var resource = Read(tempHkx); File.Delete(tempHkx); - if (resource == null) throw new Exception("Failed to read havok file."); - - var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers - | hkSerializeUtil.SaveOptionBits.TextFormat - | hkSerializeUtil.SaveOptionBits.WriteAttributes; + if (resource == null) + throw new Exception("Failed to read havok file."); var file = Write(resource, options); file.Close(); @@ -41,17 +42,19 @@ public static unsafe class HavokConverter /// A string representing the .xml file. public static byte[] XmlToHkx(string xml) { + const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers + | hkSerializeUtil.SaveOptionBits.WriteAttributes; + var tempXml = CreateTempFile(); File.WriteAllText(tempXml, xml); var resource = Read(tempXml); File.Delete(tempXml); - if (resource == null) throw new Exception("Failed to read havok file."); - - var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers - | hkSerializeUtil.SaveOptionBits.WriteAttributes; + if (resource == null) + throw new Exception("Failed to read havok file."); + g var file = Write(resource, options); file.Close(); @@ -74,7 +77,7 @@ public static unsafe class HavokConverter var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; - loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; + loadOptions->Flags = new hkFlags { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); @@ -92,37 +95,42 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); - var oStream = new hkOstream(); + var path = Marshal.StringToHGlobalAnsi(tempFile); + var oStream = new hkOstream(); oStream.Ctor((byte*)path); var result = stackalloc hkResult[1]; var saveOptions = new hkSerializeUtil.SaveOptions() { - Flags = new() { Storage = (int)optionBits } + Flags = new hkFlags { Storage = (int)optionBits }, }; - var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); - var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); - var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); + var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); + var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); try { - var name = "hkRootLevelContainer"; + const string name = "hkRootLevelContainer"; var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); - if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource."); + if (resourcePtr == null) + throw new Exception("Failed to retrieve havok root level container resource."); var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); - if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type."); + if (hkRootLevelContainerClass == null) + throw new Exception("Failed to retrieve havok root level container type."); hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); } - finally { oStream.Dtor(); } + finally + { + oStream.Dtor(); + } - if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file."); + if (result->Result == hkResult.hkResultEnum.Failure) + throw new Exception("Failed to serialize havok file."); return new FileStream(tempFile, FileMode.Open); } diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 24bcf3e0..7058a159 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -15,16 +15,15 @@ public static class SkeletonConverter var mainSkeletonId = GetMainSkeletonId(document); - var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']"); - if (skeletonNode == null) - throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); - + var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']") + ?? throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); var referencePose = ReadReferencePose(skeletonNode); var parentIndices = ReadParentIndices(skeletonNode); - var boneNames = ReadBoneNames(skeletonNode); + var boneNames = ReadBoneNames(skeletonNode); if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length) - throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); + throw new InvalidDataException( + $"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); var bones = referencePose .Zip(parentIndices, boneNames) @@ -33,9 +32,9 @@ public static class SkeletonConverter var (transform, parentIndex, name) = values; return new XivSkeleton.Bone() { - Transform = transform, + Transform = transform, ParentIndex = parentIndex, - Name = name, + Name = name, }; }) .ToArray(); @@ -63,14 +62,14 @@ public static class SkeletonConverter { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), - node => + n => { - var raw = ReadVec12(node); + var raw = ReadVec12(n); return new XivSkeleton.Transform() { - Translation = new(raw[0], raw[1], raw[2]), - Rotation = new(raw[4], raw[5], raw[6], raw[7]), - Scale = new(raw[8], raw[9], raw[10]), + Translation = new Vector3(raw[0], raw[1], raw[2]), + Rotation = new Quaternion(raw[4], raw[5], raw[6], raw[7]), + Scale = new Vector3(raw[8], raw[9], raw[10]), }; } ); @@ -82,11 +81,11 @@ public static class SkeletonConverter { var array = node.ChildNodes .Cast() - .Where(node => node.NodeType != XmlNodeType.Comment) - .Select(node => + .Where(n => n.NodeType != XmlNodeType.Comment) + .Select(n => { - var text = node.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this i mean seriously + var text = n.InnerText.Trim()[1..]; + // TODO: surely there's a less shit way to do this I mean seriously return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); }) .ToArray(); @@ -100,24 +99,20 @@ public static class SkeletonConverter /// Read the bone parent relations for a skeleton. /// XML node for the skeleton. private static int[] ReadParentIndices(XmlNode node) - { // todo: would be neat to genericise array between bare and children - return CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) + => CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) .InnerText - .Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Split((char[]) [' ', '\n'], StringSplitOptions.RemoveEmptyEntries) .Select(int.Parse) .ToArray(); - } /// Read the names of bones in a skeleton. /// XML node for the skeleton. private static string[] ReadBoneNames(XmlNode node) - { - return ReadArray( + => ReadArray( CheckExists(node.SelectSingleNode("array[@name='bones']")), - node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText + n => CheckExists(n.SelectSingleNode("string[@name='name']")).InnerText ); - } /// Read an XML tagfile array, converting it via the provided conversion function. /// Tagfile XML array node. @@ -125,10 +120,9 @@ public static class SkeletonConverter private static T[] ReadArray(XmlNode node, Func convert) { var element = (XmlElement)node; + var size = int.Parse(element.GetAttribute("size")); + var array = new T[size]; - var size = int.Parse(element.GetAttribute("size")); - - var array = new T[size]; foreach (var (childNode, index) in element.ChildNodes.Cast().WithIndex()) array[index] = convert(childNode); From 9311f80455e169d44bfdc370d52b69291a0c2bf7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 6 Jan 2024 17:49:43 +0000 Subject: [PATCH 1404/2451] [CI] Updating repo.json for testing_0.8.3.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index dbe1fe87..e27d716a 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.2", + "TestingAssemblyVersion": "0.8.3.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 981721ae858ccee8ef47828884b15b12a5442320 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 6 Jan 2024 23:26:35 +0100 Subject: [PATCH 1405/2451] Fix issue with unloaded Message Service. --- OtterGui | 2 +- Penumbra/Penumbra.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 22ae2a89..2c603cea 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a +Subproject commit 2c603cea9b1d4dd500e30972b64bd2f25012dc4c diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 350c20b2..5a03dc04 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -55,7 +55,10 @@ public class Penumbra : IDalamudPlugin _services = ServiceManagerA.CreateProvider(this, pluginInterface, Log); Messager = _services.GetService(); _validityChecker = _services.GetService(); - var startup = _services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s) + _services.EnsureRequiredServices(); + + var startup = _services.GetService() + .GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s) ? s.ToString() : "Unknown"; Log.Information( From b62bc44564c143f4aec0b14111c46dd18ddfa2df Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 11:29:31 +1100 Subject: [PATCH 1406/2451] Clean up model import UI/wiring --- Penumbra/Import/Models/ModelManager.cs | 19 +++---- .../ModEditWindow.Models.MdlTab.cs | 35 +++++++++++-- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 49 +++++++++++++------ 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index e77a94e3..afb92fc0 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -10,7 +10,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, GamePathParser parser) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; @@ -29,17 +29,17 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); - public Task ImportGltf() + public Task ImportGltf(string inputPath) { - var action = new ImportGltfAction(); - return Enqueue(action).ContinueWith(_ => action.Out!); + var action = new ImportGltfAction(inputPath); + return Enqueue(action).ContinueWith(_ => action.Out); } /// Try to find the .sklb path for a .mdl file. /// .mdl file to look up the skeleton for. public string? ResolveSklbForMdl(string mdlPath) { - var info = _parser.GetFileInfo(mdlPath); + var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) return null; @@ -126,18 +126,13 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : } } - private partial class ImportGltfAction : IAction + private partial class ImportGltfAction(string inputPath) : IAction { public MdlFile? Out; - public ImportGltfAction() - { - // - } - public void Execute(CancellationToken cancel) { - var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf"); + var model = ModelRoot.Load(inputPath); Out = ModelImporter.Import(model); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 90a6645a..06196610 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -18,8 +18,9 @@ public partial class ModEditWindow public List? GamePaths { get; private set; } public int GamePathIndex; - public bool PendingIo { get; private set; } - public string? IoException { get; private set; } + private bool _dirty; + public bool PendingIo { get; private set; } + public string? IoException { get; private set; } public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod) { @@ -46,6 +47,16 @@ public partial class ModEditWindow public byte[] Write() => Mdl.Write(); + public bool Dirty + { + get + { + var dirty = _dirty; + _dirty = false; + return dirty; + } + } + /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. /// Mod within which the .mdl is resolved. @@ -77,14 +88,28 @@ public partial class ModEditWindow }); } - public void Import() + /// Import a model from an interchange format. + /// Disk path to load model data from. + public void Import(string inputPath) { - // TODO: this needs to be fleshed out a bunch. - _edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result ?? Mdl)); + PendingIo = true; + _edit._models.ImportGltf(inputPath) + .ContinueWith(task => + { + IoException = task.Exception?.ToString(); + PendingIo = false; + + if (task.IsCompletedSuccessfully && task.Result != null) + { + Initialize(task.Result); + _dirty = true; + } + }); } /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// Game path to consider as the canonical .mdl path during export, used for resolution of other files. public void Export(string outputPath, Utf8GamePath mdlPath) { SklbFile? sklb = null; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 5703c882..b3598b9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -35,15 +35,9 @@ public partial class ModEditWindow ); } - DrawExport(tab, disabled); + DrawImportExport(tab, disabled); - var ret = false; - - if (ImGui.Button("import test")) - { - tab.Import(); - ret |= true; - } + var ret = tab.Dirty; ret |= DrawModelMaterialDetails(tab, disabled); @@ -56,11 +50,41 @@ public partial class ModEditWindow return !disabled && ret; } - private void DrawExport(MdlTab tab, bool disabled) + private void DrawImportExport(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Export")) + if (!ImGui.CollapsingHeader("Import / Export")) return; + var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + var childWidth = (windowWidth - ImGui.GetStyle().ItemSpacing.X * 3) / 2; + var childSize = new Vector2(childWidth, 0); + + DrawImport(tab, childSize, disabled); + ImGui.SameLine(); + DrawExport(tab, childSize, disabled); + + if (tab.IoException != null) + ImGuiUtil.TextWrapped(tab.IoException); + } + + private void DrawImport(MdlTab tab, Vector2 size, bool disabled) + { + using var frame = ImRaii.FramedGroup("Import", size); + + if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) + { + _fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) => + { + if (success && paths.Count > 0) + tab.Import(paths[0]); + }, 1, _mod!.ModPath.FullName, false); + } + } + + private void DrawExport(MdlTab tab, Vector2 size, bool disabled) + { + using var frame = ImRaii.FramedGroup("Export", size); + if (tab.GamePaths == null) { if (tab.IoException == null) @@ -89,9 +113,6 @@ public partial class ModEditWindow _mod!.ModPath.FullName, false ); - - if (tab.IoException != null) - ImGuiUtil.TextWrapped(tab.IoException); } private void DrawGamePathCombo(MdlTab tab) @@ -116,7 +137,7 @@ public partial class ModEditWindow const string label = "Game Path"; var preview = tab.GamePaths![tab.GamePathIndex].ToString(); var labelWidth = ImGui.CalcTextSize(label).X + ImGui.GetStyle().ItemInnerSpacing.X; - var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth; + var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth - ImGui.GetStyle().ItemSpacing.X; if (tab.GamePaths!.Count == 1) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); From aa7f0bace9a55da8fdcd08d7acb427081735f153 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 19:49:13 +1100 Subject: [PATCH 1407/2451] Wire up hair EST resolution --- .../Import/Models/Export/ModelExporter.cs | 9 +- Penumbra/Import/Models/ModelManager.cs | 89 ++++++++++++++----- .../ModEditWindow.Models.MdlTab.cs | 42 ++++++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 4 files changed, 104 insertions(+), 38 deletions(-) diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 2060c323..07b37eeb 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -22,7 +22,7 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; var meshes = ConvertMeshes(mdl, gltfSkeleton); @@ -50,12 +50,15 @@ public class ModelExporter } /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. - private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) + private static GltfSkeleton? ConvertSkeleton(IEnumerable skeletons) { NodeBuilder? root = null; var names = new Dictionary(); var joints = new List(); - foreach (var bone in skeleton.Bones) + + // Flatten out the bones across all the recieved skeletons, but retain a reference to the parent skeleton for lookups. + var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); + foreach (var (skeleton, bone) in iterator) { if (names.ContainsKey(bone.Name)) continue; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index dd796a42..a9e1b32d 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,14 +1,19 @@ using Dalamud.Plugin.Services; +using OtterGui; using OtterGui.Tasks; +using Penumbra.Collections.Manager; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; using Penumbra.Import.Models.Export; +using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; @@ -24,31 +29,55 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable? sklbs, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); - /// Try to find the .sklb path for a .mdl file. - /// .mdl file to look up the skeleton for. - public string? ResolveSklbForMdl(string mdlPath) + /// Try to find the .sklb paths for a .mdl file. + /// .mdl file to look up the skeletons for. + public string[]? ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) { - var info = _parser.GetFileInfo(mdlPath); + var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) return null; + var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); + return info.ObjectType switch { - ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), - ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), - ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", - 1), + ObjectType.Equipment => [baseSkeleton], + ObjectType.Accessory => [baseSkeleton], + ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], + ObjectType.Character when info.BodySlot is BodySlot.Hair + => [baseSkeleton, ResolveHairSkeleton(info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), - ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId), - ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId), - ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId), + ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], + ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], + ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], _ => null, }; } + private string ResolveHairSkeleton(GameObjectInfo info, EstManipulation[] estManipulations) + { + // TODO: might be able to genericse this over esttype based on incoming info + var (gender, race) = info.GenderRace.Split(); + var modEst = estManipulations + .FirstOrNull(est => + est.Gender == gender + && est.Race == race + && est.Slot == EstManipulation.EstType.Hair + && est.SetId == info.PrimaryId + ); + + // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. + var targetId = modEst?.Entry + ?? collections.Current.MetaCache?.GetEstEntry(EstManipulation.EstType.Hair, info.GenderRace, info.PrimaryId) + ?? info.PrimaryId; + + // TODO: i'm not conviced ToSuffix is correct - check! + return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); + } + private Task Enqueue(IAction action) { if (_disposed) @@ -75,16 +104,16 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable? sklbs, string outputPath) : IAction { public void Execute(CancellationToken cancel) { - Penumbra.Log.Debug("Reading skeleton."); - var xivSkeleton = BuildSkeleton(cancel); + Penumbra.Log.Debug("Reading skeletons."); + var xivSkeletons = BuildSkeletons(cancel); Penumbra.Log.Debug("Converting model."); - var model = ModelExporter.Export(mdl, xivSkeleton); + var model = ModelExporter.Export(mdl, xivSkeletons); Penumbra.Log.Debug("Building scene."); var scene = new SceneBuilder(); @@ -96,16 +125,28 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : } /// Attempt to read out the pertinent information from a .sklb. - private XivSkeleton? BuildSkeleton(CancellationToken cancel) + private IEnumerable? BuildSkeletons(CancellationToken cancel) { - if (sklb == null) + if (sklbs == null) return null; - var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton)); - xmlTask.Wait(cancel); - var xml = xmlTask.Result; + // The havok methods we're relying on for this conversion are a bit + // finicky at the best of times, and can outright cause a CTD if they + // get upset. Running each conversion on its own tick seems to make + // this consistently non-crashy across my testing. + Task CreateHavokTask((SklbFile Sklb, int Index) pair) => + manager._framework.RunOnTick( + () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), + delayTicks: pair.Index + ); - return SkeletonConverter.FromXml(xml); + var havokTasks = sklbs + .WithIndex() + .Select(CreateHavokTask) + .ToArray(); + Task.WaitAll(havokTasks, cancel); + + return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b8573780..d4e75487 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,7 +1,7 @@ using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; -using Penumbra.Mods; +using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -21,15 +21,14 @@ public partial class ModEditWindow public bool PendingIo { get; private set; } public string? IoException { get; private set; } - public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod) + public MdlTab(ModEditWindow edit, byte[] bytes, string path) { _edit = edit; Mdl = new MdlFile(bytes); _attributes = CreateAttributes(Mdl); - if (mod != null) - FindGamePaths(path, mod); + FindGamePaths(path); } /// @@ -42,9 +41,13 @@ public partial class ModEditWindow /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. - /// Mod within which the .mdl is resolved. - private void FindGamePaths(string path, IMod mod) + private void FindGamePaths(string path) { + // If there's no current mod (somehow), there's nothing to resolve the model within. + var mod = _edit._editor.Mod; + if (mod == null) + return; + if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p)) { GamePaths = [p]; @@ -71,15 +74,34 @@ public partial class ModEditWindow }); } + private EstManipulation[] GetCurrentEstManipulations() + { + var mod = _edit._editor.Mod; + var option = _edit._editor.Option; + if (mod == null || option == null) + return []; + + // Filter then prepend the current option to ensure it's chosen first. + return mod.AllSubMods + .Where(subMod => subMod != option) + .Prepend(option) + .SelectMany(subMod => subMod.Manipulations) + .Where(manipulation => manipulation.ManipulationType == MetaManipulation.Type.Est) + .Select(manipulation => manipulation.Est) + .ToArray(); + } + /// Export model to an interchange format. /// Disk path to save the resulting file to. public void Export(string outputPath, Utf8GamePath mdlPath) { - SklbFile? sklb = null; + IEnumerable? sklbs = null; try { - var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString()); - sklb = sklbPath != null ? ReadSklb(sklbPath) : null; + var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); + sklbs = sklbPaths != null + ? sklbPaths.Select(ReadSklb).ToArray() + : null; } catch (Exception exception) { @@ -88,7 +110,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklb, outputPath) + _edit._models.ExportToGltf(Mdl, sklbs, outputPath) .ContinueWith(task => { IoException = task.Exception?.ToString(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 167adafe..8d3e32f9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -600,7 +600,7 @@ public partial class ModEditWindow : Window, IDisposable (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, - (bytes, path, _) => new MdlTab(this, bytes, path, _mod)); + (bytes, path, _) => new MdlTab(this, bytes, path)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, From 0440324432dd00e0c14d55f16aa65336736e95d6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 20:16:15 +1100 Subject: [PATCH 1408/2451] Genericise est logic to handle face --- Penumbra/Import/Models/ModelManager.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index a9e1b32d..4564968d 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -48,7 +48,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ResolveHairSkeleton(info, estManipulations)], + => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], + ObjectType.Character when info.BodySlot is BodySlot.Face + => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -57,24 +59,22 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string ResolveHairSkeleton(GameObjectInfo info, EstManipulation[] estManipulations) + private string ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { - // TODO: might be able to genericse this over esttype based on incoming info var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations .FirstOrNull(est => est.Gender == gender && est.Race == race - && est.Slot == EstManipulation.EstType.Hair + && est.Slot == type && est.SetId == info.PrimaryId ); // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. var targetId = modEst?.Entry - ?? collections.Current.MetaCache?.GetEstEntry(EstManipulation.EstType.Hair, info.GenderRace, info.PrimaryId) + ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? info.PrimaryId; - // TODO: i'm not conviced ToSuffix is correct - check! return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); } From 8bc71fb1b318b6589dc3a564f5698ccd80ccdf2e Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 20:47:44 +1100 Subject: [PATCH 1409/2451] Fix viera ears --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 83c01275..e4ab3e91 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 83c012752cd9d13d39248eda85ab18cc59070a76 +Subproject commit e4ab3e914ab8b5651cea313af367e811a253d174 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4564968d..7ea19aab 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -49,7 +49,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], - ObjectType.Character when info.BodySlot is BodySlot.Face + ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], @@ -75,7 +75,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? info.PrimaryId; - return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); + return GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId); } private Task Enqueue(IAction action) From 3f8ac1e8d04f95ad85576212d25c1cc3f493b4f0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 21:56:21 +1100 Subject: [PATCH 1410/2451] Add support for body and head slot EST --- Penumbra/Import/Models/ModelManager.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 7ea19aab..692a214f 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -44,13 +44,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return info.ObjectType switch { + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], ObjectType.Equipment => [baseSkeleton], ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear - => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -59,8 +63,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { + // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations .FirstOrNull(est => @@ -70,12 +75,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect && est.SetId == info.PrimaryId ); - // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. + // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? info.PrimaryId; + ?? 0; - return GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId); + // If there's no entries, we can assume that there's no additional skeleton. + if (targetId == 0) + return []; + + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; } private Task Enqueue(IAction action) From 2f6905cf357b81aed33e67476575a39809538db5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 14:42:16 +0100 Subject: [PATCH 1411/2451] Minimal cleanup. --- Penumbra/Communication/ChangedItemClick.cs | 1 - .../Import/Models/Export/ModelExporter.cs | 5 ++- Penumbra/Import/Models/ModelManager.cs | 43 +++++++++---------- .../ModEditWindow.Models.MdlTab.cs | 13 +++--- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index b11f2306..754570e2 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,5 @@ using OtterGui.Classes; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; namespace Penumbra.Communication; diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 07b37eeb..8271f266 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -56,11 +56,12 @@ public class ModelExporter var names = new Dictionary(); var joints = new List(); - // Flatten out the bones across all the recieved skeletons, but retain a reference to the parent skeleton for lookups. + // Flatten out the bones across all the received skeletons, but retain a reference to the parent skeleton for lookups. var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); foreach (var (skeleton, bone) in iterator) { - if (names.ContainsKey(bone.Name)) continue; + if (names.ContainsKey(bone.Name)) + continue; var node = new NodeBuilder(bone.Name); names[bone.Name] = joints.Count; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 692a214f..f4c17080 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -29,16 +29,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable? sklbs, string outputPath) + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbs, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. - public string[]? ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) + /// Modified extra skeleton template parameters. + public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) { var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) - return null; + return []; var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); @@ -59,7 +60,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], - _ => null, + _ => [], }; } @@ -113,31 +114,38 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable? sklbs, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbs, string outputPath) : IAction { public void Execute(CancellationToken cancel) { - Penumbra.Log.Debug("Reading skeletons."); + Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); + Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); var xivSkeletons = BuildSkeletons(cancel); - Penumbra.Log.Debug("Converting model."); + Penumbra.Log.Debug("[GLTF Export] Converting model..."); var model = ModelExporter.Export(mdl, xivSkeletons); - Penumbra.Log.Debug("Building scene."); + Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); model.AddToScene(scene); - Penumbra.Log.Debug("Saving."); + Penumbra.Log.Debug("[GLTF Export] Saving..."); var gltfModel = scene.ToGltf2(); gltfModel.SaveGLTF(outputPath); + Penumbra.Log.Debug("[GLTF Export] Done."); } /// Attempt to read out the pertinent information from a .sklb. - private IEnumerable? BuildSkeletons(CancellationToken cancel) + private IEnumerable BuildSkeletons(CancellationToken cancel) { - if (sklbs == null) - return null; + var havokTasks = sklbs + .WithIndex() + .Select(CreateHavokTask) + .ToArray(); + + // Result waits automatically. + return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); // The havok methods we're relying on for this conversion are a bit // finicky at the best of times, and can outright cause a CTD if they @@ -146,16 +154,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Task CreateHavokTask((SklbFile Sklb, int Index) pair) => manager._framework.RunOnTick( () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), - delayTicks: pair.Index - ); - - var havokTasks = sklbs - .WithIndex() - .Select(CreateHavokTask) - .ToArray(); - Task.WaitAll(havokTasks, cancel); - - return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); + delayTicks: pair.Index, cancellationToken: cancel); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index d4e75487..dbedc164 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -86,31 +86,30 @@ public partial class ModEditWindow .Where(subMod => subMod != option) .Prepend(option) .SelectMany(subMod => subMod.Manipulations) - .Where(manipulation => manipulation.ManipulationType == MetaManipulation.Type.Est) + .Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est) .Select(manipulation => manipulation.Est) .ToArray(); } /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// The game path of the model. public void Export(string outputPath, Utf8GamePath mdlPath) { - IEnumerable? sklbs = null; + IEnumerable skeletons; try { var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); - sklbs = sklbPaths != null - ? sklbPaths.Select(ReadSklb).ToArray() - : null; + skeletons = sklbPaths.Select(ReadSklb).ToArray(); } catch (Exception exception) { - IoException = exception?.ToString(); + IoException = exception.ToString(); return; } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklbs, outputPath) + _edit._models.ExportToGltf(Mdl, skeletons, outputPath) .ContinueWith(task => { IoException = task.Exception?.ToString(); From 2e935a637815371cf2e4e65ffa9d200447024567 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 15:14:58 +0100 Subject: [PATCH 1412/2451] Update GameData. --- Penumbra.GameData | 2 +- Penumbra/Collections/Manager/CollectionStorage.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e4ab3e91..1dad8d07 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e4ab3e914ab8b5651cea313af367e811a253d174 +Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98 diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 7c94d705..c43c3817 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -17,10 +17,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable private readonly ModStorage _modStorage; /// The empty collection is always available at Index 0. - private readonly List _collections = new() - { + private readonly List _collections = + [ ModCollection.Empty, - }; + ]; public readonly ModCollection DefaultNamed; From a2b92f129656a8212312e694cdbbc5e819bc3eb5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:03:52 +0100 Subject: [PATCH 1413/2451] Some rework, add drag & drop. --- .../ModEditWindow.Models.MdlTab.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 51 ++++++++++++++----- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index c3fc4963..f9e19599 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -142,13 +142,12 @@ public partial class ModEditWindow .ContinueWith(task => { IoException = task.Exception?.ToString(); - PendingIo = false; - - if (task.IsCompletedSuccessfully && task.Result != null) + if (task is { IsCompletedSuccessfully: true, Result: not null }) { Initialize(task.Result); _dirty = true; } + PendingIo = false; }); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b3598b9d..41c5591a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,7 +15,7 @@ public partial class ModEditWindow private const int MdlMaterialMaximum = 4; private readonly FileEditor _modelTab; - private readonly ModelManager _models; + private readonly ModelManager _models; private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; @@ -55,9 +55,7 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Import / Export")) return; - var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - var childWidth = (windowWidth - ImGui.GetStyle().ItemSpacing.X * 3) / 2; - var childSize = new Vector2(childWidth, 0); + var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); DrawImport(tab, childSize, disabled); ImGui.SameLine(); @@ -67,21 +65,35 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped(tab.IoException); } - private void DrawImport(MdlTab tab, Vector2 size, bool disabled) + private void DrawImport(MdlTab tab, Vector2 size, bool _1) { - using var frame = ImRaii.FramedGroup("Import", size); - - if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) - { - _fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) => + _dragDropManager.CreateImGuiSource("ModelDragDrop", + m => m.Extensions.Any(e => ValidModelExtensions.Contains(e.ToLowerInvariant())), m => { - if (success && paths.Count > 0) - tab.Import(paths[0]); - }, 1, _mod!.ModPath.FullName, false); + if (!GetFirstModel(m.Files, out var file)) + return false; + + ImGui.TextUnformatted($"Dragging model for editing: {Path.GetFileName(file)}"); + return true; + }); + + using (var frame = ImRaii.FramedGroup("Import", size)) + { + if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", + tab.PendingIo)) + _fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) => + { + if (success && paths.Count > 0) + tab.Import(paths[0]); + }, 1, _mod!.ModPath.FullName, false); + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); } + + if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var file)) + tab.Import(file); } - private void DrawExport(MdlTab tab, Vector2 size, bool disabled) + private void DrawExport(MdlTab tab, Vector2 size, bool _) { using var frame = ImRaii.FramedGroup("Export", size); @@ -431,4 +443,15 @@ public partial class ModEditWindow return false; } + + private static bool GetFirstModel(IEnumerable files, [NotNullWhen(true)] out string? file) + { + file = files.FirstOrDefault(f => ValidModelExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())); + return file != null; + } + + private static readonly string[] ValidModelExtensions = + [ + ".gltf", + ]; } From b0f61e6929dddd0d184da55d10d363fd23af46e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:22:48 +0100 Subject: [PATCH 1414/2451] Auto formatting, some cleanup, some initialization changes. --- Penumbra/Import/Models/Import/MeshImporter.cs | 122 +++++++-------- .../Import/Models/Import/ModelImporter.cs | 140 ++++++++---------- .../Import/Models/Import/SubMeshImporter.cs | 63 ++++---- .../Import/Models/Import/VertexAttribute.cs | 119 ++++++++------- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- 5 files changed, 213 insertions(+), 233 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index e67b7c4e..95eede2b 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -3,17 +3,17 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public class MeshImporter +public class MeshImporter(IEnumerable nodes) { public struct Mesh { - public MdlStructs.MeshStruct MeshStruct; + public MdlStructs.MeshStruct MeshStruct; public List SubMeshStructs; public MdlStructs.VertexDeclarationStruct VertexDeclaration; - public IEnumerable VertexBuffer; + public IEnumerable VertexBuffer; - public List Indicies; + public List Indices; public List? Bones; @@ -22,8 +22,8 @@ public class MeshImporter public struct MeshShapeKey { - public string Name; - public MdlStructs.ShapeMeshStruct ShapeMesh; + public string Name; + public MdlStructs.ShapeMeshStruct ShapeMesh; public List ShapeValues; } @@ -33,79 +33,60 @@ public class MeshImporter return importer.Create(); } - private IEnumerable _nodes; + private readonly List _subMeshes = []; - private List _subMeshes = new(); + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; + private byte[]? _strides; + private ushort _vertexCount; + private readonly List[] _streams = [[], [], []]; - private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; - private byte[]? _strides; - private ushort _vertexCount = 0; - private List[] _streams; - - private List _indices = new(); + private readonly List _indices = []; private List? _bones; - private readonly Dictionary> _shapeValues = new(); - - private MeshImporter(IEnumerable nodes) - { - _nodes = nodes; - - // All meshes may use up to 3 byte streams. - _streams = new List[3]; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex] = new List(); - } + private readonly Dictionary> _shapeValues = []; private Mesh Create() { - foreach (var node in _nodes) + foreach (var node in nodes) BuildSubMeshForNode(node); ArgumentNullException.ThrowIfNull(_strides); ArgumentNullException.ThrowIfNull(_vertexDeclaration); - return new Mesh() + return new Mesh { - MeshStruct = new MdlStructs.MeshStruct() + MeshStruct = new MdlStructs.MeshStruct { VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], VertexBufferStride = _strides, - VertexCount = _vertexCount, + VertexCount = _vertexCount, VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements .Select(element => element.Stream + 1) .Max(), - StartIndex = 0, IndexCount = (uint)_indices.Count, // TODO: import material names - MaterialIndex = 0, - - SubMeshIndex = 0, - SubMeshCount = (ushort)_subMeshes.Count, - + MaterialIndex = 0, + SubMeshIndex = 0, + SubMeshCount = (ushort)_subMeshes.Count, BoneTableIndex = 0, }, - SubMeshStructs = _subMeshes, - + SubMeshStructs = _subMeshes, VertexDeclaration = _vertexDeclaration.Value, - VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), - - Indicies = _indices, - - Bones = _bones, - + VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), + Indices = _indices, + Bones = _bones, ShapeKeys = _shapeValues .Select(pair => new MeshShapeKey() { Name = pair.Key, ShapeMesh = new MdlStructs.ShapeMeshStruct() { - MeshIndexOffset = 0, + MeshIndexOffset = 0, ShapeValueOffset = 0, - ShapeValueCount = (uint)pair.Value.Count, + ShapeValueCount = (uint)pair.Value.Count, }, ShapeValues = pair.Value, }) @@ -117,10 +98,10 @@ public class MeshImporter { // Record some offsets we'll be using later, before they get mutated with sub-mesh values. var vertexOffset = _vertexCount; - var indexOffset = _indices.Count; + var indexOffset = _indices.Count; var nodeBoneMap = CreateNodeBoneMap(node); - var subMesh = SubMeshImporter.Import(node, nodeBoneMap); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap); var subMeshName = node.Name ?? node.Mesh.Name; @@ -128,18 +109,18 @@ public class MeshImporter if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) - throw new Exception($"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + throw new Exception( + $"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); // Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine. - // TODO: I mean, given that strides are derivable, might be worth dropping strides from the submesh return structure and computing when needed. - if (_strides == null) - _strides = subMesh.Strides; + // TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed. + _strides ??= subMesh.Strides; // Merge the sub-mesh streams into the main mesh stream bodies. _vertexCount += subMesh.VertexCount; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex].AddRange(subMesh.Streams[streamIndex]); + foreach (var (stream, subStream) in _streams.Zip(subMesh.Streams)) + stream.AddRange(subStream); // As we're appending vertex data to the buffers, we need to update indices to point into that later block. _indices.AddRange(subMesh.Indices.Select(index => (ushort)(index + vertexOffset))); @@ -149,7 +130,7 @@ public class MeshImporter { if (!_shapeValues.TryGetValue(name, out var meshShapeValues)) { - meshShapeValues = new(); + meshShapeValues = []; _shapeValues.Add(name, meshShapeValues); } @@ -167,19 +148,20 @@ public class MeshImporter }); } - private bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) + private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { var elA = a.VertexElements; var elB = b.VertexElements; - if (elA.Length != elB.Length) return true; + if (elA.Length != elB.Length) + return true; // NOTE: This assumes that elements will always be in the same order. Under the current implementation, that's guaranteed. return elA.Zip(elB).Any(pair => pair.First.Usage != pair.Second.Usage - || pair.First.Type != pair.Second.Type - || pair.First.Offset != pair.Second.Offset - || pair.First.Stream != pair.Second.Stream + || pair.First.Type != pair.Second.Type + || pair.First.Offset != pair.Second.Offset + || pair.First.Stream != pair.Second.Stream ); } @@ -195,31 +177,30 @@ public class MeshImporter .Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint") .ToArray(); - // TODO: This is duplicated with the submesh importer - would be good to avoid (not that it's a huge issue). - var mesh = node.Mesh; - var meshName = node.Name ?? mesh.Name ?? "(no name)"; + // TODO: This is duplicated with the sub mesh importer - would be good to avoid (not that it's a huge issue). + var mesh = node.Mesh; + var meshName = node.Name ?? mesh.Name ?? "(no name)"; var primitiveCount = mesh.Primitives.Count; if (primitiveCount != 1) - { throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); - } + var primitive = mesh.Primitives[0]; // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0"); - if (jointsAccessor == null) - throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") + ?? throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. var usedJoints = new HashSet(); foreach (var joints in jointsAccessor.AsVector4Array()) + { for (var index = 0; index < 4; index++) usedJoints.Add((ushort)joints[index]); - + } + // Only initialise the bones list if we're actually going to put something in it. - if (_bones == null) - _bones = new(); + _bones ??= []; // Build a dictionary of node-specific joint indices mesh-wide bone indices. var nodeBoneMap = new Dictionary(); @@ -232,6 +213,7 @@ public class MeshImporter boneIndex = _bones.Count; _bones.Add(jointName); } + nodeBoneMap.Add(usedJoint, (ushort)boneIndex); } diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index f53d2b64..abe87934 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -4,7 +4,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public partial class ModelImporter +public partial class ModelImporter(ModelRoot _model) { public static MdlFile Import(ModelRoot model) { @@ -13,29 +13,22 @@ public partial class ModelImporter } // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled)] + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex MeshNameGroupingRegex(); - private readonly ModelRoot _model; + private readonly List _meshes = []; + private readonly List _subMeshes = []; - private List _meshes = new(); - private List _subMeshes = new(); + private readonly List _vertexDeclarations = []; + private readonly List _vertexBuffer = []; - private List _vertexDeclarations = new(); - private List _vertexBuffer = new(); + private readonly List _indices = []; - private List _indices = new(); + private readonly List _bones = []; + private readonly List _boneTables = []; - private List _bones = new(); - private List _boneTables = new(); - - private Dictionary> _shapeMeshes = new(); - private List _shapeValues = new(); - - private ModelImporter(ModelRoot model) - { - _model = model; - } + private readonly Dictionary> _shapeMeshes = []; + private readonly List _shapeValues = []; private MdlFile Create() { @@ -43,8 +36,8 @@ public partial class ModelImporter foreach (var subMeshNodes in GroupedMeshNodes()) BuildMeshForGroup(subMeshNodes); - // Now that all of the meshes have been built, we can build some of the model-wide metadata. - var shapes = new List(); + // Now that all the meshes have been built, we can build some of the model-wide metadata. + var shapes = new List(); var shapeMeshes = new List(); foreach (var (keyName, keyMeshes) in _shapeMeshes) { @@ -53,75 +46,64 @@ public partial class ModelImporter ShapeName = keyName, // NOTE: these values are per-LoD. ShapeMeshStartIndex = [(ushort)shapeMeshes.Count, 0, 0], - ShapeMeshCount = [(ushort)keyMeshes.Count, 0, 0], + ShapeMeshCount = [(ushort)keyMeshes.Count, 0, 0], }); shapeMeshes.AddRange(keyMeshes); } var indexBuffer = _indices.SelectMany(BitConverter.GetBytes).ToArray(); - var emptyBoundingBox = new MdlStructs.BoundingBoxStruct() - { - Min = [0, 0, 0, 0], - Max = [0, 0, 0, 0], - }; - // And finally, the MdlFile itself. - return new MdlFile() + return new MdlFile { - VertexOffset = [0, 0, 0], - VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0], - IndexOffset = [(uint)_vertexBuffer.Count, 0, 0], - IndexBufferSize = [(uint)indexBuffer.Length, 0, 0], - - VertexDeclarations = _vertexDeclarations.ToArray(), - Meshes = _meshes.ToArray(), - SubMeshes = _subMeshes.ToArray(), - - BoneTables = _boneTables.ToArray(), - Bones = _bones.ToArray(), + VertexOffset = [0, 0, 0], + VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0], + IndexOffset = [(uint)_vertexBuffer.Count, 0, 0], + IndexBufferSize = [(uint)indexBuffer.Length, 0, 0], + VertexDeclarations = [.. _vertexDeclarations], + Meshes = [.. _meshes], + SubMeshes = [.. _subMeshes], + BoneTables = [.. _boneTables], + Bones = [.. _bones], // TODO: Game doesn't seem to rely on this, but would be good to populate. SubMeshBoneMap = [], - - Shapes = shapes.ToArray(), - ShapeMeshes = shapeMeshes.ToArray(), - ShapeValues = _shapeValues.ToArray(), - - LodCount = 1, - - Lods = [new MdlStructs.LodStruct() - { - MeshIndex = 0, - MeshCount = (ushort)_meshes.Count, - - ModelLodRange = 0, - TextureLodRange = 0, - - VertexDataOffset = 0, - VertexBufferSize = (uint)_vertexBuffer.Count, - IndexDataOffset = (uint)_vertexBuffer.Count, - IndexBufferSize = (uint)indexBuffer.Length, - }], + Shapes = [.. shapes], + ShapeMeshes = [.. shapeMeshes], + ShapeValues = [.. _shapeValues], + LodCount = 1, + Lods = + [ + new MdlStructs.LodStruct + { + MeshIndex = 0, + MeshCount = (ushort)_meshes.Count, + ModelLodRange = 0, + TextureLodRange = 0, + VertexDataOffset = 0, + VertexBufferSize = (uint)_vertexBuffer.Count, + IndexDataOffset = (uint)_vertexBuffer.Count, + IndexBufferSize = (uint)indexBuffer.Length, + }, + ], // TODO: Would be good to populate from gltf material names. Materials = ["/NO_MATERIAL"], // TODO: Would be good to calculate all of this up the tree. - Radius = 1, - BoundingBoxes = emptyBoundingBox, - BoneBoundingBoxes = Enumerable.Repeat(emptyBoundingBox, _bones.Count).ToArray(), - - RemainingData = [.._vertexBuffer, ..indexBuffer], + Radius = 1, + BoundingBoxes = MdlFile.EmptyBoundingBox, + BoneBoundingBoxes = Enumerable.Repeat(MdlFile.EmptyBoundingBox, _bones.Count).ToArray(), + RemainingData = [.._vertexBuffer, ..indexBuffer], }; } /// Returns an iterator over sorted, grouped mesh nodes. - private IEnumerable> GroupedMeshNodes() => - _model.LogicalNodes + private IEnumerable> GroupedMeshNodes() + => _model.LogicalNodes .Where(node => node.Mesh != null) - .Select(node => + .Select(node => { - var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; + var name = node.Name ?? node.Mesh.Name ?? "NOMATCH"; var match = MeshNameGroupingRegex().Match(name); return (node, match); }) @@ -140,12 +122,12 @@ public partial class ModelImporter private void BuildMeshForGroup(IEnumerable subMeshNodes) { // Record some offsets we'll be using later, before they get mutated with mesh values. - var subMeshOffset = _subMeshes.Count; - var vertexOffset = _vertexBuffer.Count; - var indexOffset = _indices.Count; + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; var shapeValueOffset = _shapeValues.Count; - var mesh = MeshImporter.Import(subMeshNodes); + var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); // If no bone table is used for a mesh, the index is set to 255. @@ -163,22 +145,21 @@ public partial class ModelImporter .ToArray(), }); - foreach (var subMesh in mesh.SubMeshStructs) - _subMeshes.Add(subMesh with - { - IndexOffset = (uint)(subMesh.IndexOffset + indexOffset), - }); + _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with + { + IndexOffset = (uint)(m.IndexOffset + indexOffset), + })); _vertexDeclarations.Add(mesh.VertexDeclaration); _vertexBuffer.AddRange(mesh.VertexBuffer); - _indices.AddRange(mesh.Indicies); + _indices.AddRange(mesh.Indices); foreach (var meshShapeKey in mesh.ShapeKeys) { if (!_shapeMeshes.TryGetValue(meshShapeKey.Name, out var shapeMeshes)) { - shapeMeshes = new(); + shapeMeshes = []; _shapeMeshes.Add(meshShapeKey.Name, shapeMeshes); } @@ -203,6 +184,7 @@ public partial class ModelImporter boneIndex = _bones.Count; _bones.Add(boneName); } + boneIndices.Add((ushort)boneIndex); } diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 1d604105..5dec4384 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -12,8 +12,8 @@ public class SubMeshImporter public MdlStructs.VertexDeclarationStruct VertexDeclaration; - public ushort VertexCount; - public byte[] Strides; + public ushort VertexCount; + public byte[] Strides; public List[] Streams; public ushort[] Indices; @@ -27,19 +27,19 @@ public class SubMeshImporter return importer.Create(); } - private readonly MeshPrimitive _primitive; + private readonly MeshPrimitive _primitive; private readonly IDictionary? _nodeBoneMap; private List? _attributes; - private ushort _vertexCount = 0; - private byte[] _strides = [0, 0, 0]; + private ushort _vertexCount; + private byte[] _strides = [0, 0, 0]; private readonly List[] _streams; private ushort[]? _indices; - private readonly List? _morphNames; - private Dictionary>? _shapeValues; + private readonly List? _morphNames; + private Dictionary>? _shapeValues; private SubMeshImporter(Node node, IDictionary? nodeBoneMap) { @@ -52,7 +52,7 @@ public class SubMeshImporter throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); } - _primitive = mesh.Primitives[0]; + _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; try @@ -67,7 +67,7 @@ public class SubMeshImporter // All meshes may use up to 3 byte streams. _streams = new List[3]; for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex] = new List(); + _streams[streamIndex] = []; } private SubMesh Create() @@ -85,26 +85,22 @@ public class SubMeshImporter { SubMeshStruct = new MdlStructs.SubmeshStruct() { - IndexOffset = 0, - IndexCount = (uint)_indices.Length, + IndexOffset = 0, + IndexCount = (uint)_indices.Length, AttributeIndexMask = 0, // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. BoneStartIndex = 0, - BoneCount = 0, + BoneCount = 0, }, - VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), }, - VertexCount = _vertexCount, - Strides = _strides, - Streams = _streams, - - Indices = _indices, - + Strides = _strides, + Streams = _streams, + Indices = _indices, ShapeValues = _shapeValues, }; } @@ -119,11 +115,12 @@ public class SubMeshImporter var accessors = _primitive.VertexAccessors; var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) - .Select(index => _primitive.GetMorphTargetAccessors(index)); + .Select(index => _primitive.GetMorphTargetAccessors(index)).ToList(); // Try to build all the attributes the mesh might use. // The order here is chosen to match a typical model's element order. - var rawAttributes = new[] { + var rawAttributes = new[] + { VertexAttribute.Position(accessors, morphAccessors), VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors, _nodeBoneMap), @@ -134,10 +131,17 @@ public class SubMeshImporter }; var attributes = new List(); - var offsets = new byte[] { 0, 0, 0 }; + var offsets = new byte[] + { + 0, + 0, + 0, + }; foreach (var attribute in rawAttributes) { - if (attribute == null) continue; + if (attribute == null) + continue; + attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); offsets[attribute.Stream] += attribute.Size; } @@ -177,7 +181,7 @@ public class SubMeshImporter BuildShapeValues(morphModifiedVertices); } - private void BuildShapeValues(List[] morphModifiedVertices) + private void BuildShapeValues(IEnumerable> morphModifiedVertices) { ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_attributes); @@ -198,12 +202,11 @@ public class SubMeshImporter // Find any indices that target this vertex index and create a mapping. var targetingIndices = _indices.WithIndex() .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); - foreach (var targetingIndex in targetingIndices) - shapeValues.Add(new MdlStructs.ShapeValueStruct() - { - BaseIndicesIndex = (ushort)targetingIndex, - ReplacingVertexIndex = _vertexCount, - }); + shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + })); _vertexCount++; } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 6bb9971c..0b0e90ba 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -13,27 +13,32 @@ public class VertexAttribute { /// XIV vertex element metadata structure. public readonly MdlStructs.VertexElement Element; + /// Build a byte array containing this vertex attribute's data for the specified vertex index. public readonly BuildFn Build; + /// Check if the specified morph target index contains a morph for the specified vertex index. public readonly HasMorphFn HasMorph; + /// Build a byte array containing this vertex attribute's data, as modified by the specified morph target, for the specified vertex index. public readonly BuildMorphFn BuildMorph; - public byte Stream => Element.Stream; + public byte Stream + => Element.Stream; /// Size in bytes of a single vertex's attribute value. - public byte Size => (MdlFile.VertexType)Element.Type switch - { - MdlFile.VertexType.Single3 => 12, - MdlFile.VertexType.Single4 => 16, - MdlFile.VertexType.UInt => 4, - MdlFile.VertexType.ByteFloat4 => 4, - MdlFile.VertexType.Half2 => 4, - MdlFile.VertexType.Half4 => 8, + public byte Size + => (MdlFile.VertexType)Element.Type switch + { + MdlFile.VertexType.Single3 => 12, + MdlFile.VertexType.Single4 => 16, + MdlFile.VertexType.UInt => 4, + MdlFile.VertexType.ByteFloat4 => 4, + MdlFile.VertexType.Half2 => 4, + MdlFile.VertexType.Half4 => 8, - _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), - }; + _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), + }; private VertexAttribute( MdlStructs.VertexElement element, @@ -42,25 +47,30 @@ public class VertexAttribute BuildMorphFn? buildMorph = null ) { - Element = element; - Build = write; - HasMorph = hasMorph ?? DefaultHasMorph; + Element = element; + Build = write; + HasMorph = hasMorph ?? DefaultHasMorph; BuildMorph = buildMorph ?? DefaultBuildMorph; } - public VertexAttribute WithOffset(byte offset) => new VertexAttribute( - Element with { Offset = offset }, - Build, - HasMorph, - BuildMorph - ); + public VertexAttribute WithOffset(byte offset) + => new( + Element with { Offset = offset }, + Build, + HasMorph, + BuildMorph + ); - // We assume that attributes don't have morph data unless explicitly configured. - private static bool DefaultHasMorph(int morphIndex, int vertexIndex) => false; + /// We assume that attributes don't have morph data unless explicitly configured. + private static bool DefaultHasMorph(int morphIndex, int vertexIndex) + => false; - // XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. - // As a fallback, we're just building the normal vertex data for the index. - private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); + /// + /// XIV stores shapes as full vertex replacements, so all attributes need to output something for a morph. + /// As a fallback, we're just building the normal vertex data for the index. + /// > + private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) + => Build(vertexIndex); public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) { @@ -70,29 +80,29 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.Single3, - Usage = (byte)MdlFile.VertexUsage.Position, + Type = (byte)MdlFile.VertexType.Single3, + Usage = (byte)MdlFile.VertexUsage.Position, }; var values = accessor.AsVector3Array(); var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("POSITION")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("POSITION")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildSingle3(values[index]), - - hasMorph: (morphIndex, vertexIndex) => + (morphIndex, vertexIndex) => { var deltas = morphValues[morphIndex]; - if (deltas == null) return false; + if (deltas == null) + return false; + var delta = deltas[vertexIndex]; return delta != Vector3.Zero; }, - - buildMorph: (morphIndex, vertexIndex) => + (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -116,8 +126,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, }; var values = accessor.AsVector4Array(); @@ -142,8 +152,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.UInt, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, + Type = (byte)MdlFile.VertexType.UInt, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; var values = accessor.AsVector4Array(); @@ -171,20 +181,19 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.Half4, - Usage = (byte)MdlFile.VertexUsage.Normal, + Type = (byte)MdlFile.VertexType.Half4, + Usage = (byte)MdlFile.VertexUsage.Normal, }; var values = accessor.AsVector3Array(); var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("NORMAL")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("NORMAL")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildHalf4(new Vector4(values[index], 0)), - buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -207,7 +216,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Usage = (byte)MdlFile.VertexUsage.UV, + Usage = (byte)MdlFile.VertexUsage.UV, }; var values1 = accessor1.AsVector2Array(); @@ -241,21 +250,20 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Tangent1, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Tangent1, }; var values = accessor.AsVector4Array(); // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. var morphValues = morphAccessors - .Select(accessors => accessors.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, index => BuildByteFloat4(values[index]), - buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -277,8 +285,8 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, - Usage = (byte)MdlFile.VertexUsage.Color, + Type = (byte)MdlFile.VertexType.ByteFloat4, + Usage = (byte)MdlFile.VertexUsage.Color, }; var values = accessor.AsVector4Array(); @@ -289,14 +297,16 @@ public class VertexAttribute ); } - private static byte[] BuildSingle3(Vector3 input) => + private static byte[] BuildSingle3(Vector3 input) + => [ ..BitConverter.GetBytes(input.X), ..BitConverter.GetBytes(input.Y), ..BitConverter.GetBytes(input.Z), ]; - private static byte[] BuildUInt(Vector4 input) => + private static byte[] BuildUInt(Vector4 input) + => [ (byte)input.X, (byte)input.Y, @@ -304,7 +314,8 @@ public class VertexAttribute (byte)input.W, ]; - private static byte[] BuildByteFloat4(Vector4 input) => + private static byte[] BuildByteFloat4(Vector4 input) + => [ (byte)Math.Round(input.X * 255f), (byte)Math.Round(input.Y * 255f), @@ -312,13 +323,15 @@ public class VertexAttribute (byte)Math.Round(input.W * 255f), ]; - private static byte[] BuildHalf2(Vector2 input) => + private static byte[] BuildHalf2(Vector2 input) + => [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), ]; - private static byte[] BuildHalf4(Vector4 input) => + private static byte[] BuildHalf4(Vector4 input) + => [ ..BitConverter.GetBytes((Half)input.X), ..BitConverter.GetBytes((Half)input.Y), diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 1e4117ec..595240f4 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -49,7 +49,7 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito ImGui.TableNextColumn(); var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiselection(path); + _selector.RemovePathFromMultiSelection(path); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); From 8c7c7e20a0626ebda47889d661f12019a57acee1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 23:23:25 +0100 Subject: [PATCH 1415/2451] Update OtterGui --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 2c603cea..df754445 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2c603cea9b1d4dd500e30972b64bd2f25012dc4c +Subproject commit df754445aa6f67fbeb84a292fe808ee560bc3cf7 From 025e3798a74290e88452200f6b18538d8330b5c8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 7 Jan 2024 22:25:53 +0000 Subject: [PATCH 1416/2451] [CI] Updating repo.json for testing_0.8.3.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e27d716a..47e102c9 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.3", + "TestingAssemblyVersion": "0.8.3.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 36cbca4684d38ccdd2be7f743599a809c0b271f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jan 2024 13:52:45 +0100 Subject: [PATCH 1417/2451] Add icons to import/export headers. --- OtterGui | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index df754445..4c32d2d4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit df754445aa6f67fbeb84a292fe808ee560bc3cf7 +Subproject commit 4c32d2d448c4e36ea665276ed755a96fa4907c33 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 41c5591a..4f9303f8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -77,7 +77,7 @@ public partial class ModEditWindow return true; }); - using (var frame = ImRaii.FramedGroup("Import", size)) + using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) @@ -89,13 +89,13 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); } - if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var file)) - tab.Import(file); + if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile)) + tab.Import(importFile); } private void DrawExport(MdlTab tab, Vector2 size, bool _) { - using var frame = ImRaii.FramedGroup("Export", size); + using var frame = ImRaii.FramedGroup("Export", size, headerPreIcon: FontAwesomeIcon.FileExport); if (tab.GamePaths == null) { From c3ba8a22313fe3df96d75436c2d1cd86feb1c645 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 9 Jan 2024 01:15:08 +1100 Subject: [PATCH 1418/2451] Improve error messaging --- Penumbra/Import/Models/ModelManager.cs | 7 +++- .../ModEditWindow.Models.MdlTab.cs | 23 +++++++++---- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 32 ++++++++++++++++--- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bae9569f..f099a0e0 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,12 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public Task ImportGltf(string inputPath) { var action = new ImportGltfAction(inputPath); - return Enqueue(action).ContinueWith(_ => action.Out); + return Enqueue(action).ContinueWith(task => + { + if (task.IsFaulted && task.Exception != null) + throw task.Exception; + return action.Out; + }); } /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f9e19599..d52bf3f1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -18,9 +18,9 @@ public partial class ModEditWindow public List? GamePaths { get; private set; } public int GamePathIndex; - private bool _dirty; - public bool PendingIo { get; private set; } - public string? IoException { get; private set; } + private bool _dirty; + public bool PendingIo { get; private set; } + public List IoExceptions { get; private set; } = []; public MdlTab(ModEditWindow edit, byte[] bytes, string path) { @@ -85,7 +85,7 @@ public partial class ModEditWindow task.ContinueWith(t => { - IoException = t.Exception?.ToString(); + RecordIoExceptions(t.Exception); GamePaths = t.Result; PendingIo = false; }); @@ -120,7 +120,7 @@ public partial class ModEditWindow } catch (Exception exception) { - IoException = exception.ToString(); + RecordIoExceptions(exception); return; } @@ -128,7 +128,7 @@ public partial class ModEditWindow _edit._models.ExportToGltf(Mdl, skeletons, outputPath) .ContinueWith(task => { - IoException = task.Exception?.ToString(); + RecordIoExceptions(task.Exception); PendingIo = false; }); } @@ -141,7 +141,7 @@ public partial class ModEditWindow _edit._models.ImportGltf(inputPath) .ContinueWith(task => { - IoException = task.Exception?.ToString(); + RecordIoExceptions(task.Exception); if (task is { IsCompletedSuccessfully: true, Result: not null }) { Initialize(task.Result); @@ -151,6 +151,15 @@ public partial class ModEditWindow }); } + private void RecordIoExceptions(Exception? exception) + { + IoExceptions = exception switch { + null => [], + AggregateException ae => ae.Flatten().InnerExceptions.ToList(), + Exception other => [other], + }; + } + /// Read a .sklb from the active collection or game. /// Game path to the .sklb to load. private SklbFile ReadSklb(string sklbPath) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 4f9303f8..28f41936 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -7,6 +7,7 @@ using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; using Penumbra.String.Classes; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -61,8 +62,7 @@ public partial class ModEditWindow ImGui.SameLine(); DrawExport(tab, childSize, disabled); - if (tab.IoException != null) - ImGuiUtil.TextWrapped(tab.IoException); + DrawIoExceptions(tab); } private void DrawImport(MdlTab tab, Vector2 size, bool _1) @@ -99,10 +99,10 @@ public partial class ModEditWindow if (tab.GamePaths == null) { - if (tab.IoException == null) + if (tab.IoExceptions.Count == 0) ImGui.TextUnformatted("Resolving model game paths."); else - ImGuiUtil.TextWrapped(tab.IoException); + ImGui.TextUnformatted("Failed to resolve model game paths."); return; } @@ -126,6 +126,30 @@ public partial class ModEditWindow false ); } + + private void DrawIoExceptions(MdlTab tab) + { + if (tab.IoExceptions.Count == 0) + return; + + var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); + using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, borderColor: Colors.RegexWarningBorder); + + var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; + foreach (var exception in tab.IoExceptions) + { + var message = $"{exception.GetType().Name}: {exception.Message} {exception.Message}"; + var textSize = ImGui.CalcTextSize(message).X; + if (textSize > spaceAvail) + message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; + + using (var exceptionNode = ImRaii.TreeNode(message)) + { + if (exceptionNode) + ImGuiUtil.TextWrapped(exception.ToString()); + } + } + } private void DrawGamePathCombo(MdlTab tab) { From ec114b3f6a21321e6570ca1091b72011d711e1d0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 00:03:32 +1100 Subject: [PATCH 1419/2451] Prevent import of models with too many shape values --- Penumbra/Import/Models/Import/ModelImporter.cs | 12 ++++++++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index abe87934..1c49d4bd 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -4,7 +4,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public partial class ModelImporter(ModelRoot _model) +public partial class ModelImporter(ModelRoot model) { public static MdlFile Import(ModelRoot model) { @@ -99,7 +99,7 @@ public partial class ModelImporter(ModelRoot _model) /// Returns an iterator over sorted, grouped mesh nodes. private IEnumerable> GroupedMeshNodes() - => _model.LogicalNodes + => model.LogicalNodes .Where(node => node.Mesh != null) .Select(node => { @@ -171,6 +171,14 @@ public partial class ModelImporter(ModelRoot _model) _shapeValues.AddRange(meshShapeKey.ShapeValues); } + + // The number of shape values in a model is bounded by the count + // value, which is stored as a u16. + // While technically there are similar bounds on other shape struct + // arrays, values is practically guaranteed to be the highest of the + // group, so a failure on any of them will be a failure on it. + if (_shapeValues.Count > ushort.MaxValue) + throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort BuildBoneTable(List boneNames) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 28f41936..d8818b21 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -138,7 +138,7 @@ public partial class ModEditWindow var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; foreach (var exception in tab.IoExceptions) { - var message = $"{exception.GetType().Name}: {exception.Message} {exception.Message}"; + var message = $"{exception.GetType().Name}: {exception.Message}"; var textSize = ImGui.CalcTextSize(message).X; if (textSize > spaceAvail) message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; From 3cd438bb5dc495e257a9f8731293010b8ea58ad6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 01:17:47 +1100 Subject: [PATCH 1420/2451] Export material names --- Penumbra/Import/Models/Export/MeshExporter.cs | 18 +++++++------- .../Import/Models/Export/ModelExporter.cs | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 84628c2c..6e6169ee 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -27,9 +27,9 @@ public class MeshExporter } } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton) + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) { - var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names); + var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); return new Mesh(self.BuildMeshes(), skeleton?.Joints); } @@ -42,18 +42,22 @@ public class MeshExporter private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; + private readonly MaterialBuilder _material; + private readonly Dictionary? _boneIndexMap; private readonly Type _geometryType; private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap) { _mdl = mdl; _lod = lod; _meshIndex = meshIndex; + _material = materials[XivMesh.MaterialIndex]; + if (boneNameMap != null) _boneIndexMap = BuildBoneIndexMap(boneNameMap); @@ -134,13 +138,7 @@ public class MeshExporter ); var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, name)!; - // TODO: share materials &c - var materialBuilder = new MaterialBuilder() - .WithDoubleSide(true) - .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); - - var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder); + var primitiveBuilder = meshBuilder.UsePrimitive(_material); // Store a list of the glTF indices. The list index will be equivalent to the xiv (submesh) index. var gltfIndices = new List(); diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 8271f266..6a25af61 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Files; +using SharpGLTF.Materials; using SharpGLTF.Scenes; using SharpGLTF.Transforms; @@ -14,7 +15,7 @@ public class ModelExporter var skeletonRoot = skeleton?.Root; if (skeletonRoot != null) scene.AddNode(skeletonRoot); - + // Add all the meshes to the scene. foreach (var mesh in meshes) mesh.AddToScene(scene); @@ -25,12 +26,13 @@ public class ModelExporter public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var meshes = ConvertMeshes(mdl, gltfSkeleton); + var materials = ConvertMaterials(mdl); + var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton) + private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton) { var meshes = new List(); @@ -41,7 +43,7 @@ public class ModelExporter // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton); + var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), materials, skeleton); meshes.Add(mesh); } } @@ -49,6 +51,18 @@ public class ModelExporter return meshes; } + // TODO: Compose textures for use with these materials + /// Build placeholder materials for each of the material slots in the .mdl. + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl) + => mdl.Materials + .Select(name => + new MaterialBuilder(name) + .WithMetallicRoughnessShader() + .WithDoubleSide(true) + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One) + ) + .ToArray(); + /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. private static GltfSkeleton? ConvertSkeleton(IEnumerable skeletons) { @@ -60,7 +74,7 @@ public class ModelExporter var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); foreach (var (skeleton, bone) in iterator) { - if (names.ContainsKey(bone.Name)) + if (names.ContainsKey(bone.Name)) continue; var node = new NodeBuilder(bone.Name); From d2f93f85625d789189938ad4b0200b759779e722 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 20:33:24 +1100 Subject: [PATCH 1421/2451] Import material names --- Penumbra/Import/Models/Import/MeshImporter.cs | 8 +++++ .../Import/Models/Import/ModelImporter.cs | 33 ++++++++++++++++--- .../Import/Models/Import/SubMeshImporter.cs | 7 ++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 95eede2b..00663e43 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -10,6 +10,8 @@ public class MeshImporter(IEnumerable nodes) public MdlStructs.MeshStruct MeshStruct; public List SubMeshStructs; + public string? Material; + public MdlStructs.VertexDeclarationStruct VertexDeclaration; public IEnumerable VertexBuffer; @@ -35,6 +37,8 @@ public class MeshImporter(IEnumerable nodes) private readonly List _subMeshes = []; + private string? _material; + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; private byte[]? _strides; private ushort _vertexCount; @@ -74,6 +78,7 @@ public class MeshImporter(IEnumerable nodes) BoneTableIndex = 0, }, SubMeshStructs = _subMeshes, + Material = _material, VertexDeclaration = _vertexDeclaration.Value, VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), Indices = _indices, @@ -105,6 +110,9 @@ public class MeshImporter(IEnumerable nodes) var subMeshName = node.Name ?? node.Mesh.Name; + // TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh. + _material ??= subMesh.Material; + // Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution. if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index abe87934..d5d4bb53 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -19,6 +19,8 @@ public partial class ModelImporter(ModelRoot _model) private readonly List _meshes = []; private readonly List _subMeshes = []; + private readonly List _materials = []; + private readonly List _vertexDeclarations = []; private readonly List _vertexBuffer = []; @@ -37,6 +39,8 @@ public partial class ModelImporter(ModelRoot _model) BuildMeshForGroup(subMeshNodes); // Now that all the meshes have been built, we can build some of the model-wide metadata. + var materials = _materials.Count > 0 ? _materials : ["/NO_MATERIAL"]; + var shapes = new List(); var shapeMeshes = new List(); foreach (var (keyName, keyMeshes) in _shapeMeshes) @@ -86,8 +90,7 @@ public partial class ModelImporter(ModelRoot _model) }, ], - // TODO: Would be good to populate from gltf material names. - Materials = ["/NO_MATERIAL"], + Materials = [.. materials], // TODO: Would be good to calculate all of this up the tree. Radius = 1, @@ -130,15 +133,20 @@ public partial class ModelImporter(ModelRoot _model) var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); + ushort materialIndex = 0; + if (mesh.Material != null) + materialIndex = GetMaterialIndex(mesh.Material); + // If no bone table is used for a mesh, the index is set to 255. - var boneTableIndex = 255; + ushort boneTableIndex = 255; if (mesh.Bones != null) boneTableIndex = BuildBoneTable(mesh.Bones); _meshes.Add(mesh.MeshStruct with { + MaterialIndex = materialIndex, SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), - BoneTableIndex = (ushort)boneTableIndex, + BoneTableIndex = boneTableIndex, StartIndex = meshStartIndex, VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset .Select(offset => (uint)(offset + vertexOffset)) @@ -173,6 +181,23 @@ public partial class ModelImporter(ModelRoot _model) } } + private ushort GetMaterialIndex(string materialName) + { + // If we already have this material, grab the current one + var index = _materials.IndexOf(materialName); + if (index >= 0) + return (ushort)index; + + // If there's already 4 materials, we can't add any more. + // TODO: permit, with a warning to reduce, and validation in MdlTab. + var count = _materials.Count; + if (count >= 4) + return 0; + + _materials.Add(materialName); + return (ushort)count; + } + private ushort BuildBoneTable(List boneNames) { var boneIndices = new List(); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 5dec4384..0d1dafb3 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -10,6 +10,8 @@ public class SubMeshImporter { public MdlStructs.SubmeshStruct SubMeshStruct; + public string? Material; + public MdlStructs.VertexDeclarationStruct VertexDeclaration; public ushort VertexCount; @@ -81,6 +83,10 @@ public class SubMeshImporter ArgumentNullException.ThrowIfNull(_attributes); ArgumentNullException.ThrowIfNull(_shapeValues); + var material = _primitive.Material.Name; + if (material == "") + material = null; + return new SubMesh() { SubMeshStruct = new MdlStructs.SubmeshStruct() @@ -93,6 +99,7 @@ public class SubMeshImporter BoneStartIndex = 0, BoneCount = 0, }, + Material = material, VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), From 64aed56f7c2949d9791354299deebd45b8d1cf27 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 10 Jan 2024 22:39:48 +1100 Subject: [PATCH 1422/2451] Allow keeping existing mdl materials --- .../ModEditWindow.Models.MdlTab.cs | 34 ++++++++++++++++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 3 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f9e19599..bd599133 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -15,6 +15,8 @@ public partial class ModEditWindow public MdlFile Mdl { get; private set; } private List[] _attributes; + public bool ImportKeepMaterials; + public List? GamePaths { get; private set; } public int GamePathIndex; @@ -110,6 +112,7 @@ public partial class ModEditWindow /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// .mdl game path to resolve satellite files such as skeletons relative to. public void Export(string outputPath, Utf8GamePath mdlPath) { IEnumerable skeletons; @@ -143,14 +146,37 @@ public partial class ModEditWindow { IoException = task.Exception?.ToString(); if (task is { IsCompletedSuccessfully: true, Result: not null }) - { - Initialize(task.Result); - _dirty = true; - } + FinalizeImport(task.Result); PendingIo = false; }); } + /// Finalise the import of a .mdl, applying any post-import transformations and state updates. + /// Model data to finalize. + private void FinalizeImport(MdlFile newMdl) + { + if (ImportKeepMaterials) + MergeMaterials(newMdl, Mdl); + + Initialize(newMdl); + _dirty = true; + } + + /// Merge material configuration from the source onto the target. + /// Model that will be updated. + /// Model to copy material configuration from. + public void MergeMaterials(MdlFile target, MdlFile source) + { + target.Materials = source.Materials; + + for (var meshIndex = 0; meshIndex < target.Meshes.Length; meshIndex++) + { + target.Meshes[meshIndex].MaterialIndex = meshIndex < source.Meshes.Length + ? source.Meshes[meshIndex].MaterialIndex + : (ushort)0; + } + } + /// Read a .sklb from the active collection or game. /// Game path to the .sklb to load. private SklbFile ReadSklb(string sklbPath) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 4f9303f8..9a69a4e8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -79,6 +79,8 @@ public partial class ModEditWindow using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { + ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); + if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) _fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) => @@ -86,7 +88,6 @@ public partial class ModEditWindow if (success && paths.Count > 0) tab.Import(paths[0]); }, 1, _mod!.ModPath.FullName, false); - ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); } if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile)) From 182550ce1538bead0f263f6b2a0c858629626e06 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 00:34:18 +1100 Subject: [PATCH 1423/2451] Export attributes --- Penumbra/Import/Models/Export/MeshExporter.cs | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 6e6169ee..8127f348 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,20 +13,31 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh(IEnumerable> meshes, NodeBuilder[]? joints) + public class Mesh(IEnumerable meshes, NodeBuilder[]? joints) { public void AddToScene(SceneBuilder scene) { - foreach (var mesh in meshes) + foreach (var data in meshes) { - if (joints == null) - scene.AddRigidMesh(mesh, Matrix4x4.Identity); - else - scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints); + var instance = joints != null + ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints) + : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); + + var extras = new Dictionary(); + foreach (var attribute in data.Attributes) + extras.Add(attribute, true); + + instance.WithExtras(JsonContent.CreateFrom(extras)); } } } + public struct MeshData + { + public IMeshBuilder Mesh; + public string[] Attributes; + } + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) { var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); @@ -103,31 +114,33 @@ public class MeshExporter } /// Build glTF meshes for this XIV mesh. - private IMeshBuilder[] BuildMeshes() + private MeshData[] BuildMeshes() { var indices = BuildIndices(); var vertices = BuildVertices(); // NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base. if (XivMesh.SubMeshCount == 0) - return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)]; + return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount, 0)]; return _mdl.SubMeshes .Skip(XivMesh.SubMeshIndex) .Take(XivMesh.SubMeshCount) .WithIndex() .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, - (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount)) + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, + subMesh.Value.AttributeIndexMask)) .ToArray(); } /// Build a mesh from the provided indices and vertices. A subset of the full indices may be built by providing an index base and count. - private IMeshBuilder BuildMesh( + private MeshData BuildMesh( string name, IReadOnlyList indices, IReadOnlyList vertices, int indexBase, - int indexCount + int indexCount, + uint attributeMask ) { var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType( @@ -190,12 +203,23 @@ public class MeshExporter } } + // Named morph targets aren't part of the specification, however `MESH.extras.targetNames` + // is a commonly-accepted means of providing the data. meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() { { "targetNames", shapeNames }, }); - return meshBuilder; + var attributes = Enumerable.Range(0, 32) + .Where(index => ((attributeMask >> index) & 1) == 1) + .Select(index => _mdl.Attributes[index]) + .ToArray(); + + return new MeshData + { + Mesh = meshBuilder, + Attributes = attributes, + }; } /// Read in the indices for this mesh. From 4b198ef1e474c072013ce6d0202be1235183b2ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Jan 2024 16:27:11 +0100 Subject: [PATCH 1424/2451] Misc --- Penumbra/UI/Classes/Colors.cs | 1 - Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 0e3b9377..93d7e091 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -33,7 +33,6 @@ public enum ColorId public static class Colors { // These are written as 0xAABBGGRR. - public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index d63e42ef..195c07d6 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -209,7 +209,7 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, description:group.Description); void DrawOptions() { @@ -288,7 +288,7 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, description: group.Description); void DrawOptions() { From 7f7b35f3709ee85526379188cae49b779472b69b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Jan 2024 21:20:19 +0100 Subject: [PATCH 1425/2451] Fix issue with RSP values. --- Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 81f6d552..2f717491 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; +using OtterGui.Services; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -29,6 +30,7 @@ public sealed unsafe class ChangeCustomize : FastHook using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); var ret = Task.Result.Original.Invoke(human, data, skipEquipment); Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); + _metaState.CustomizeChangeCollection = ResolveData.Invalid; return ret; } } From 0c5d47e3d1889f09fd9de16b5e0de3272f7d4cf8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Jan 2024 22:40:10 +0100 Subject: [PATCH 1426/2451] Make Save-in-Place require modifier. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index e9facdf4..34d0800c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -108,11 +108,14 @@ public partial class ModEditWindow MipMapInput(); var canSaveInPlace = Path.IsPathRooted(_left.Path) && _left.Type is TextureType.Tex or TextureType.Dds or TextureType.Png; + var isActive = _config.DeleteModModifier.IsActive(); + var tt = isActive + ? "This saves the texture in place. This is not revertible." + : $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save."; var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, - "This saves the texture in place. This is not revertible.", - !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) + tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); AddReloadTask(_left.Path, false); From dada03905f3191004a4769eb2349bfff4524b497 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 18:54:53 +1100 Subject: [PATCH 1427/2451] Import attributes --- Penumbra/Import/Models/Import/MeshImporter.cs | 7 +++ .../Import/Models/Import/ModelImporter.cs | 5 ++ .../Import/Models/Import/SubMeshImporter.cs | 63 ++++++++++++++----- Penumbra/Import/Models/Import/Utility.cs | 34 ++++++++++ 4 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 Penumbra/Import/Models/Import/Utility.cs diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 00663e43..7da4d1d7 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -19,6 +19,8 @@ public class MeshImporter(IEnumerable nodes) public List? Bones; + public List MetaAttributes; + public List ShapeKeys; } @@ -48,6 +50,8 @@ public class MeshImporter(IEnumerable nodes) private List? _bones; + private readonly List _metaAttributes = []; + private readonly Dictionary> _shapeValues = []; private Mesh Create() @@ -83,6 +87,7 @@ public class MeshImporter(IEnumerable nodes) VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), Indices = _indices, Bones = _bones, + MetaAttributes = _metaAttributes, ShapeKeys = _shapeValues .Select(pair => new MeshShapeKey() { @@ -153,6 +158,8 @@ public class MeshImporter(IEnumerable nodes) _subMeshes.Add(subMesh.SubMeshStruct with { IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + AttributeIndexMask = Utility.GetMergedAttributeMask( + subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes), }); } diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index d5d4bb53..d02d143c 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -29,6 +29,8 @@ public partial class ModelImporter(ModelRoot _model) private readonly List _bones = []; private readonly List _boneTables = []; + private readonly List _metaAttributes = []; + private readonly Dictionary> _shapeMeshes = []; private readonly List _shapeValues = []; @@ -71,6 +73,7 @@ public partial class ModelImporter(ModelRoot _model) Bones = [.. _bones], // TODO: Game doesn't seem to rely on this, but would be good to populate. SubMeshBoneMap = [], + Attributes = [.. _metaAttributes], Shapes = [.. shapes], ShapeMeshes = [.. shapeMeshes], ShapeValues = [.. _shapeValues], @@ -155,6 +158,8 @@ public partial class ModelImporter(ModelRoot _model) _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with { + AttributeIndexMask = Utility.GetMergedAttributeMask( + m.AttributeIndexMask, mesh.MetaAttributes, _metaAttributes), IndexOffset = (uint)(m.IndexOffset + indexOffset), })); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 0d1dafb3..6b12ee09 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Lumina.Data.Parsing; using OtterGui; using SharpGLTF.Schema2; @@ -20,6 +21,8 @@ public class SubMeshImporter public ushort[] Indices; + public string[] MetaAttributes; + public Dictionary> ShapeValues; } @@ -29,10 +32,11 @@ public class SubMeshImporter return importer.Create(); } - private readonly MeshPrimitive _primitive; - private readonly IDictionary? _nodeBoneMap; + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + private readonly IDictionary? _nodeExtras; - private List? _attributes; + private List? _vertexAttributes; private ushort _vertexCount; private byte[] _strides = [0, 0, 0]; @@ -40,6 +44,8 @@ public class SubMeshImporter private ushort[]? _indices; + private string[]? _metaAttributes; + private readonly List? _morphNames; private Dictionary>? _shapeValues; @@ -57,6 +63,15 @@ public class SubMeshImporter _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; + try + { + _nodeExtras = node.Extras.Deserialize>(); + } + catch + { + _nodeExtras = null; + } + try { _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); @@ -76,24 +91,34 @@ public class SubMeshImporter { // Build all the data we'll need. BuildIndices(); - BuildAttributes(); + BuildVertexAttributes(); BuildVertices(); + BuildMetaAttributes(); ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); ArgumentNullException.ThrowIfNull(_shapeValues); + ArgumentNullException.ThrowIfNull(_metaAttributes); var material = _primitive.Material.Name; if (material == "") material = null; + // At this level, we assume that attributes are wholly controlled by this sub-mesh. + var attributeMask = _metaAttributes.Length switch + { + < 32 => (1u << _metaAttributes.Length) - 1, + 32 => uint.MaxValue, + > 32 => throw new Exception("Models may utilise a maximum of 32 attributes."), + }; + return new SubMesh() { SubMeshStruct = new MdlStructs.SubmeshStruct() { IndexOffset = 0, IndexCount = (uint)_indices.Length, - AttributeIndexMask = 0, + AttributeIndexMask = attributeMask, // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. BoneStartIndex = 0, @@ -102,12 +127,13 @@ public class SubMeshImporter Material = material, VertexDeclaration = new MdlStructs.VertexDeclarationStruct() { - VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(), + VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(), }, VertexCount = _vertexCount, Strides = _strides, Streams = _streams, Indices = _indices, + MetaAttributes = _metaAttributes, ShapeValues = _shapeValues, }; } @@ -117,7 +143,7 @@ public class SubMeshImporter _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); } - private void BuildAttributes() + private void BuildVertexAttributes() { var accessors = _primitive.VertexAccessors; @@ -153,14 +179,14 @@ public class SubMeshImporter offsets[attribute.Stream] += attribute.Size; } - _attributes = attributes; + _vertexAttributes = attributes; // After building the attributes, the resulting next offsets are our stream strides. _strides = offsets; } private void BuildVertices() { - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); // Lists of vertex indices that are effected by each morph target for this primitive. var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) @@ -173,13 +199,13 @@ public class SubMeshImporter for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) { // Write out vertex data to streams for each attribute. - foreach (var attribute in _attributes) + foreach (var attribute in _vertexAttributes) _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); // Record which morph targets have values for this vertex, if any. var changedMorphs = morphModifiedVertices .WithIndex() - .Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) .Select(pair => pair.Value); foreach (var modifiedVertices in changedMorphs) modifiedVertices.Add(vertexIndex); @@ -191,7 +217,7 @@ public class SubMeshImporter private void BuildShapeValues(IEnumerable> morphModifiedVertices) { ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_attributes); + ArgumentNullException.ThrowIfNull(_vertexAttributes); var morphShapeValues = new Dictionary>(); @@ -203,7 +229,7 @@ public class SubMeshImporter foreach (var vertexIndex in modifiedVertices) { // Write out the morphed vertex to the vertex streams. - foreach (var attribute in _attributes) + foreach (var attribute in _vertexAttributes) _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); // Find any indices that target this vertex index and create a mapping. @@ -224,4 +250,13 @@ public class SubMeshImporter _shapeValues = morphShapeValues; } + + private void BuildMetaAttributes() + { + // We consider any "extras" key with a boolean value set to `true` to be an attribute. + _metaAttributes = _nodeExtras? + .Where(pair => pair.Value.ValueKind == JsonValueKind.True) + .Select(pair => pair.Key) + .ToArray() ?? []; + } } diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs new file mode 100644 index 00000000..449d19e4 --- /dev/null +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -0,0 +1,34 @@ +namespace Penumbra.Import.Models.Import; + +public static class Utility +{ + /// Merge attributes into an existing attribute array, providing an updated submesh mask. + /// Old submesh attribute mask. + /// Old attribute array that should be merged. + /// New attribute array. Will be mutated. + /// New submesh attribute mask, updated to match the merged attribute array. + public static uint GetMergedAttributeMask(uint oldMask, IList oldAttributes, List newAttributes) + { + var metaAttributes = Enumerable.Range(0, 32) + .Where(index => ((oldMask >> index) & 1) == 1) + .Select(index => oldAttributes[index]); + + var newMask = 0u; + + foreach (var metaAttribute in metaAttributes) + { + var attributeIndex = newAttributes.IndexOf(metaAttribute); + if (attributeIndex == -1) + { + if (newAttributes.Count >= 32) + throw new Exception("Models may utilise a maximum of 32 attributes."); + + newAttributes.Add(metaAttribute); + attributeIndex = newAttributes.Count - 1; + } + newMask |= 1u << attributeIndex; + } + + return newMask; + } +} From edcffb9d9f7f6ee7ac5e454a15842b3c4d3ed01d Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:26:34 +1100 Subject: [PATCH 1428/2451] Allow keeping existing mdl attributes --- .../ModEditWindow.Models.MdlTab.cs | 34 +++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + 2 files changed, 35 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index d38d8d92..cdaf399f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -16,6 +16,7 @@ public partial class ModEditWindow private List[] _attributes; public bool ImportKeepMaterials; + public bool ImportKeepAttributes; public List? GamePaths { get; private set; } public int GamePathIndex; @@ -158,6 +159,9 @@ public partial class ModEditWindow if (ImportKeepMaterials) MergeMaterials(newMdl, Mdl); + if (ImportKeepAttributes) + MergeAttributes(newMdl, Mdl); + Initialize(newMdl); _dirty = true; } @@ -177,6 +181,36 @@ public partial class ModEditWindow } } + /// Merge attribute configuration from the source onto the target. + /// + /// Model to copy attribute configuration from. + public void MergeAttributes(MdlFile target, MdlFile source) + { + target.Attributes = source.Attributes; + + var indexEnumerator = Enumerable.Range(0, target.Meshes.Length) + .SelectMany(mi => Enumerable.Range(0, target.Meshes[mi].SubMeshCount).Select(so => (mi, so))); + foreach (var (meshIndex, subMeshOffset) in indexEnumerator) + { + var subMeshIndex = target.Meshes[meshIndex].SubMeshIndex + subMeshOffset; + + // Preemptively reset the mask in case we need to shortcut out. + target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u; + + // Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt + // to maintain semantic connection betwen mesh index and submesh attributes. + if (meshIndex >= source.Meshes.Length) + continue; + var sourceMesh = source.Meshes[meshIndex]; + + if (subMeshOffset >= sourceMesh.SubMeshCount) + continue; + var sourceSubMesh = source.SubMeshes[sourceMesh.SubMeshIndex + subMeshOffset]; + + target.SubMeshes[subMeshIndex].AttributeIndexMask = sourceSubMesh.AttributeIndexMask; + } + } + private void RecordIoExceptions(Exception? exception) { IoExceptions = exception switch { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index fbdfcc74..8c298d4f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -80,6 +80,7 @@ public partial class ModEditWindow using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); + ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes); if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", tab.PendingIo)) From 2e473a62f4587b7c67724b4d6d219847e7f846c2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:28:52 +1100 Subject: [PATCH 1429/2451] Sneak a small one in --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8c298d4f..c92e2926 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -479,5 +479,6 @@ public partial class ModEditWindow private static readonly string[] ValidModelExtensions = [ ".gltf", + ".glb", ]; } From b81f3f423c10a6bfba2c7bc00e7bcf1d221e576e Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 11 Jan 2024 21:54:52 +1100 Subject: [PATCH 1430/2451] Cleanup pass --- Penumbra/Import/Models/Import/ModelImporter.cs | 14 +++++++------- Penumbra/Import/Models/Import/SubMeshImporter.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 0d8c029d..3b3d2cd0 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -136,14 +136,14 @@ public partial class ModelImporter(ModelRoot model) var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); - ushort materialIndex = 0; - if (mesh.Material != null) - materialIndex = GetMaterialIndex(mesh.Material); + var materialIndex = mesh.Material != null + ? GetMaterialIndex(mesh.Material) + : (ushort)0; // If no bone table is used for a mesh, the index is set to 255. - ushort boneTableIndex = 255; - if (mesh.Bones != null) - boneTableIndex = BuildBoneTable(mesh.Bones); + var boneTableIndex = mesh.Bones != null + ? BuildBoneTable(mesh.Bones) + : (ushort)255; _meshes.Add(mesh.MeshStruct with { @@ -196,7 +196,7 @@ public partial class ModelImporter(ModelRoot model) private ushort GetMaterialIndex(string materialName) { - // If we already have this material, grab the current one + // If we already have this material, grab the current index. var index = _materials.IndexOf(materialName); if (index >= 0) return (ushort)index; diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 6b12ee09..51443f64 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -69,7 +69,7 @@ public class SubMeshImporter } catch { - _nodeExtras = null; + _nodeExtras = null; } try From 4a6e7fccecd3a7796ebae4c8d25b96f7de286b60 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Jan 2024 13:31:33 +0100 Subject: [PATCH 1431/2451] Fix scrolling weirdness. --- Penumbra/UI/ModsTab/ModPanel.cs | 16 +++++++++++++- Penumbra/UI/Tabs/ModsTab.cs | 37 +++++++++++++++++---------------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index f9a3262f..b5542e43 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,4 +1,6 @@ +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; +using ImGuiNET; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; @@ -12,6 +14,7 @@ public class ModPanel : IDisposable private readonly ModEditWindow _editWindow; private readonly ModPanelHeader _header; private readonly ModPanelTabBar _tabs; + private bool _resetCursor; public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, MultiModPanel multiModPanel, CommunicatorService communicator) @@ -32,8 +35,18 @@ public class ModPanel : IDisposable return; } + if (_resetCursor) + { + _resetCursor = false; + ImGui.SetScrollX(0); + } + _header.Draw(); - _tabs.Draw(_mod); + ImGui.SetCursorPosX(ImGui.GetScrollX() + ImGui.GetCursorPosX()); + using var child = ImRaii.Child("Tabs", + new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X, ImGui.GetContentRegionAvail().Y)); + if (child) + _tabs.Draw(_mod); } public void Dispose() @@ -47,6 +60,7 @@ public class ModPanel : IDisposable private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _) { + _resetCursor = true; if (mod == null || _selector.Selected == null) { _editWindow.IsOpen = false; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 7b30f7fc..9f070d35 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -139,24 +139,6 @@ public class ModsTab : ITab if (hovered) ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); - void DrawButton(Vector2 size, string label, string lower, string additionalTooltip) - { - using (var disabled = ImRaii.Disabled(additionalTooltip.Length > 0)) - { - if (ImGui.Button(label, size)) - { - if (lower.Length > 0) - _redrawService.RedrawObject(lower, RedrawType.Redraw); - else - _redrawService.RedrawAll(RedrawType.Redraw); - } - } - - ImGuiUtil.HoverTooltip(lower.Length > 0 - ? $"Execute '/penumbra redraw {lower}'.{additionalTooltip}" - : $"Execute '/penumbra redraw'.{additionalTooltip}", ImGuiHoveredFlags.AllowWhenDisabled); - } - using var id = ImRaii.PushId("Redraw"); using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); ImGui.SameLine(); @@ -185,6 +167,25 @@ public class ModsTab : ITab ? "\nCan currently only be used for indoor furniture." : string.Empty; DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Furniture", "furniture", tt); + return; + + void DrawButton(Vector2 size, string label, string lower, string additionalTooltip) + { + using (_ = ImRaii.Disabled(additionalTooltip.Length > 0)) + { + if (ImGui.Button(label, size)) + { + if (lower.Length > 0) + _redrawService.RedrawObject(lower, RedrawType.Redraw); + else + _redrawService.RedrawAll(RedrawType.Redraw); + } + } + + ImGuiUtil.HoverTooltip(lower.Length > 0 + ? $"Execute '/penumbra redraw {lower}'.{additionalTooltip}" + : $"Execute '/penumbra redraw'.{additionalTooltip}", ImGuiHoveredFlags.AllowWhenDisabled); + } } private static unsafe bool IsIndoors() From be588e2fa31e027ed804a6acd38c872bf506f191 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 11 Jan 2024 12:33:42 +0000 Subject: [PATCH 1432/2451] [CI] Updating repo.json for testing_0.8.3.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 47e102c9..a9c1829a 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.4", + "TestingAssemblyVersion": "0.8.3.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 91cea50f028bee2902cf21431c29d037c419ead5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Jan 2024 15:31:25 +0100 Subject: [PATCH 1433/2451] Move more hooks in own classes. --- Penumbra/Interop/GameState.cs | 19 ++- .../{ => Objects}/CharacterBaseDestructor.cs | 2 +- .../{ => Objects}/CharacterDestructor.cs | 2 +- .../Hooks/{ => Objects}/CopyCharacter.cs | 2 +- .../{ => Objects}/CreateCharacterBase.cs | 4 +- .../Interop/Hooks/{ => Objects}/EnableDraw.cs | 8 +- .../Hooks/{ => Objects}/WeaponReload.cs | 2 +- .../Hooks/Resources/ApricotResourceLoad.cs | 28 +++++ .../Interop/Hooks/Resources/LoadMtrlShpk.cs | 32 +++++ .../Interop/Hooks/Resources/LoadMtrlTex.cs | 28 +++++ .../Hooks/Resources/ResolvePathHooks.cs | 38 ++++++ .../Resources/ResolvePathHooksBase.cs} | 55 +++++---- .../ResourceHandleDestructor.cs | 3 +- .../LiveColorTablePreviewer.cs | 17 +-- .../MaterialPreview/LiveMaterialPreviewer.cs | 75 +++++------ .../LiveMaterialPreviewerBase.cs | 5 +- .../Interop/MaterialPreview/MaterialInfo.cs | 32 ++--- .../Interop/PathResolving/CutsceneService.cs | 2 +- .../Interop/PathResolving/DrawObjectState.cs | 1 + .../IdentifiedCollectionCache.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- Penumbra/Interop/PathResolving/PathState.cs | 55 +-------- .../Interop/PathResolving/SubfileHelper.cs | 116 +++--------------- Penumbra/Interop/Services/SkinFixer.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 26 files changed, 274 insertions(+), 262 deletions(-) rename Penumbra/Interop/Hooks/{ => Objects}/CharacterBaseDestructor.cs (96%) rename Penumbra/Interop/Hooks/{ => Objects}/CharacterDestructor.cs (96%) rename Penumbra/Interop/Hooks/{ => Objects}/CopyCharacter.cs (96%) rename Penumbra/Interop/Hooks/{ => Objects}/CreateCharacterBase.cs (94%) rename Penumbra/Interop/Hooks/{ => Objects}/EnableDraw.cs (86%) rename Penumbra/Interop/Hooks/{ => Objects}/WeaponReload.cs (98%) create mode 100644 Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs create mode 100644 Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs create mode 100644 Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs create mode 100644 Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs rename Penumbra/Interop/{PathResolving/ResolvePathHooks.cs => Hooks/Resources/ResolvePathHooksBase.cs} (78%) rename Penumbra/Interop/Hooks/{ => Resources}/ResourceHandleDestructor.cs (95%) diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 2552f1a7..7e7abcd8 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -72,7 +72,24 @@ public class GameState : IService #endregion - /// Return the correct resolve data from the stored data. + #region Subfiles + + public readonly ThreadLocal MtrlData = new(() => ResolveData.Invalid); + public readonly ThreadLocal AvfxData = new(() => ResolveData.Invalid); + + public readonly ConcurrentDictionary SubFileCollection = new(); + + public ResolveData LoadSubFileHelper(nint resourceHandle) + { + if (resourceHandle == nint.Zero) + return ResolveData.Invalid; + + return SubFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid; + } + + #endregion + + /// Return the correct resolve data from the stored data. public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData) { switch (type) diff --git a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs similarity index 96% rename from Penumbra/Interop/Hooks/CharacterBaseDestructor.cs rename to Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index 435ddea6..fc6dbfe6 100644 --- a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -4,7 +4,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.UI.AdvancedWindow; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Objects; public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr, IHookService { diff --git a/Penumbra/Interop/Hooks/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs similarity index 96% rename from Penumbra/Interop/Hooks/CharacterDestructor.cs rename to Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index 4a0e9367..6e10c5e3 100644 --- a/Penumbra/Interop/Hooks/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -4,7 +4,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Objects; public sealed unsafe class CharacterDestructor : EventWrapperPtr, IHookService { diff --git a/Penumbra/Interop/Hooks/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs similarity index 96% rename from Penumbra/Interop/Hooks/CopyCharacter.cs rename to Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index d2e8d816..7b730f84 100644 --- a/Penumbra/Interop/Hooks/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Classes; using OtterGui.Services; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Objects; public sealed unsafe class CopyCharacter : EventWrapperPtr, IHookService { diff --git a/Penumbra/Interop/Hooks/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs similarity index 94% rename from Penumbra/Interop/Hooks/CreateCharacterBase.cs rename to Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 7dbde666..299f312a 100644 --- a/Penumbra/Interop/Hooks/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -4,7 +4,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Structs; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Objects; public sealed unsafe class CreateCharacterBase : EventWrapperPtr, IHookService { @@ -39,7 +39,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr /// EnableDraw is what creates DrawObjects for gameObjects, @@ -12,12 +12,12 @@ namespace Penumbra.Interop.Hooks; public sealed unsafe class EnableDraw : IHookService { private readonly Task> _task; - private readonly GameState _state; + private readonly GameState _state; public EnableDraw(HookManager hooks, GameState state) { _state = state; - _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); } private delegate void Delegate(GameObject* gameObject); @@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService private void Detour(GameObject* gameObject) { _state.QueueGameObject(gameObject); - Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint) gameObject:X}."); + Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}."); _task.Result.Original.Invoke(gameObject); _state.DequeueGameObject(); } diff --git a/Penumbra/Interop/Hooks/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs similarity index 98% rename from Penumbra/Interop/Hooks/WeaponReload.cs rename to Penumbra/Interop/Hooks/Objects/WeaponReload.cs index b931f8fb..31c6b883 100644 --- a/Penumbra/Interop/Hooks/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -4,7 +4,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Structs; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Objects; public sealed unsafe class WeaponReload : EventWrapperPtr, IHookService { diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs new file mode 100644 index 00000000..2e5698a3 --- /dev/null +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -0,0 +1,28 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.Resources; + +public sealed unsafe class ApricotResourceLoad : FastHook +{ + private readonly GameState _gameState; + + public ApricotResourceLoad(HookManager hooks, GameState gameState) + { + _gameState = gameState; + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, true); + } + + public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private byte Detour(ResourceHandle* handle, nint unk1, byte unk2) + { + var last = _gameState.AvfxData.Value; + _gameState.AvfxData.Value = _gameState.LoadSubFileHelper((nint)handle); + var ret = Task.Result.Original(handle, unk1, unk2); + _gameState.AvfxData.Value = last; + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs new file mode 100644 index 00000000..5ef3bf37 --- /dev/null +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.Resources; + +public sealed unsafe class LoadMtrlShpk : FastHook +{ + private readonly GameState _gameState; + private readonly CommunicatorService _communicator; + + public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator) + { + _gameState = gameState; + _communicator = communicator; + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, true); + } + + public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); + + private byte Detour(MaterialResourceHandle* handle) + { + var last = _gameState.MtrlData.Value; + var mtrlData = _gameState.LoadSubFileHelper((nint)handle); + _gameState.MtrlData.Value = mtrlData; + var ret = Task.Result.Original(handle); + _gameState.MtrlData.Value = last; + _communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs new file mode 100644 index 00000000..14a011ea --- /dev/null +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -0,0 +1,28 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.Resources; + +public sealed unsafe class LoadMtrlTex : FastHook +{ + private readonly GameState _gameState; + + public LoadMtrlTex(HookManager hooks, GameState gameState) + { + _gameState = gameState; + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, true); + } + + public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private byte Detour(MaterialResourceHandle* handle) + { + var last = _gameState.MtrlData.Value; + _gameState.MtrlData.Value = _gameState.LoadSubFileHelper((nint)handle); + var ret = Task.Result.Original(handle); + _gameState.MtrlData.Value = last; + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs new file mode 100644 index 00000000..8a52acd2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooks.cs @@ -0,0 +1,38 @@ +using OtterGui.Services; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Resources; + +public sealed unsafe class ResolvePathHooks(HookManager hooks, CharacterBaseVTables vTables, PathState pathState) : IDisposable, IRequiredService +{ + // @formatter:off + private readonly ResolvePathHooksBase _human = new("Human", hooks, pathState, vTables.HumanVTable, ResolvePathHooksBase.Type.Human); + private readonly ResolvePathHooksBase _weapon = new("Weapon", hooks, pathState, vTables.WeaponVTable, ResolvePathHooksBase.Type.Other); + private readonly ResolvePathHooksBase _demiHuman = new("DemiHuman", hooks, pathState, vTables.DemiHumanVTable, ResolvePathHooksBase.Type.Other); + private readonly ResolvePathHooksBase _monster = new("Monster", hooks, pathState, vTables.MonsterVTable, ResolvePathHooksBase.Type.Other); + // @formatter:on + + public void Enable() + { + _human.Enable(); + _weapon.Enable(); + _demiHuman.Enable(); + _monster.Enable(); + } + + public void Disable() + { + _human.Disable(); + _weapon.Disable(); + _demiHuman.Disable(); + _monster.Disable(); + } + + public void Dispose() + { + _human.Dispose(); + _weapon.Dispose(); + _demiHuman.Dispose(); + _monster.Dispose(); + } +} diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs similarity index 78% rename from Penumbra/Interop/PathResolving/ResolvePathHooks.cs rename to Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 3be7ffdd..6b4abf90 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,13 +1,14 @@ using Dalamud.Hooking; -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Collections; +using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; -namespace Penumbra.Interop.PathResolving; +namespace Penumbra.Interop.Hooks.Resources; -public unsafe class ResolvePathHooks : IDisposable +public sealed unsafe class ResolvePathHooksBase : IDisposable { public enum Type { @@ -19,7 +20,9 @@ public unsafe class ResolvePathHooks : IDisposable private delegate nint NamedResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint name); private delegate nint PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex); private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize); + private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName); + // Kept separate from NamedResolveDelegate because the 5th parameter has out semantics here, instead of in. private delegate nint VfxResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam); @@ -38,21 +41,24 @@ public unsafe class ResolvePathHooks : IDisposable private readonly PathState _parent; - public ResolvePathHooks(IGameInteropProvider interop, PathState parent, nint* vTable, Type type) + public ResolvePathHooksBase(string name, HookManager hooks, PathState parent, nint* vTable, Type type) { - _parent = parent; - _resolveDecalPathHook = Create(interop, vTable[83], ResolveDecal); - _resolveEidPathHook = Create(interop, vTable[85], ResolveEid); - _resolveImcPathHook = Create(interop, vTable[81], ResolveImc); - _resolveMPapPathHook = Create(interop, vTable[79], ResolveMPap); - _resolveMdlPathHook = Create(interop, vTable[73], type, ResolveMdl, ResolveMdlHuman); - _resolveMtrlPathHook = Create(interop, vTable[82], ResolveMtrl); - _resolvePapPathHook = Create(interop, vTable[76], type, ResolvePap, ResolvePapHuman); - _resolvePhybPathHook = Create(interop, vTable[75], type, ResolvePhyb, ResolvePhybHuman); - _resolveSklbPathHook = Create(interop, vTable[72], type, ResolveSklb, ResolveSklbHuman); - _resolveSkpPathHook = Create(interop, vTable[74], type, ResolveSkp, ResolveSkpHuman); - _resolveTmbPathHook = Create(interop, vTable[77], ResolveTmb); - _resolveVfxPathHook = Create(interop, vTable[84], ResolveVfx); + _parent = parent; + // @formatter:off + _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal); + _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid); + _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc); + _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap); + _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman); + _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl); + _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman); + _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman); + _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman); + _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); + _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx); + // @formatter:on + Enable(); } public void Enable() @@ -177,9 +183,8 @@ public unsafe class ResolvePathHooks : IDisposable { data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); if (_parent.InInternalResolve) - { return DisposableContainer.Empty; - } + return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), @@ -188,19 +193,19 @@ public unsafe class ResolvePathHooks : IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(IGameInteropProvider interop, nint address, Type type, T other, T human) where T : Delegate + private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate { var del = type switch { - Type.Human => human, - _ => other, + Type.Human => human, + _ => other, }; - return interop.HookFromAddress(address, del); + return hooks.CreateHook(name, address, del).Result; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(IGameInteropProvider interop, nint address, T del) where T : Delegate - => interop.HookFromAddress(address, del); + private static Hook Create(string name, HookManager hooks, nint address, T del) where T : Delegate + => hooks.CreateHook(name, address, del).Result; // Implementation diff --git a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs similarity index 95% rename from Penumbra/Interop/Hooks/ResourceHandleDestructor.cs rename to Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 99eb1c23..776f2f92 100644 --- a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -2,10 +2,9 @@ using Dalamud.Hooking; using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData; -using Penumbra.Interop.Services; using Penumbra.Interop.Structs; -namespace Penumbra.Interop.Hooks; +namespace Penumbra.Interop.Hooks.Resources; public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr, IHookService { diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 0b7bafe0..801c3bf0 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -16,11 +16,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private readonly Texture** _colorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture; - private Half[] _colorTable; - private bool _updatePending; + private bool _updatePending; - public Half[] ColorTable - => _colorTable; + public Half[] ColorTable { get; } public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo) : base(objects, materialInfo) @@ -41,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - _colorTable = new Half[TextureLength]; + ColorTable = new Half[TextureLength]; _updatePending = true; framework.Update += OnFrameworkUpdate; @@ -84,9 +82,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase return; bool success; - lock (_colorTable) + lock (ColorTable) { - fixed (Half* colorTable = _colorTable) + fixed (Half* colorTable = ColorTable) { success = texture.Texture->InitializeContents(colorTable); } @@ -105,9 +103,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) return false; - if (_colorTableTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) - return false; - - return true; + return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); } } diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 972d81be..9ed7ca3d 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -26,34 +26,29 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (_shaderPackage == null) throw new InvalidOperationException("Material doesn't have a shader package"); - var material = Material; + _originalShPkFlags = Material->ShaderFlags; - _originalShPkFlags = material->ShaderFlags; + _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); - _originalMaterialParameter = material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); - - _originalSamplerFlags = new uint[material->TextureCount]; + _originalSamplerFlags = new uint[Material->TextureCount]; for (var i = 0; i < _originalSamplerFlags.Length; ++i) - _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; + _originalSamplerFlags[i] = Material->Textures[i].SamplerFlags; } protected override void Clear(bool disposing, bool reset) { base.Clear(disposing, reset); - if (reset) - { - var material = Material; + if (!reset) + return; - material->ShaderFlags = _originalShPkFlags; + Material->ShaderFlags = _originalShPkFlags; + var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); + if (!materialParameter.IsEmpty) + _originalMaterialParameter.AsSpan().CopyTo(materialParameter); - var materialParameter = material->MaterialParameterCBuffer->TryGetBuffer(); - if (!materialParameter.IsEmpty) - _originalMaterialParameter.AsSpan().CopyTo(materialParameter); - - for (var i = 0; i < _originalSamplerFlags.Length; ++i) - material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; - } + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + Material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; } public void SetShaderPackageFlags(uint shPkFlags) @@ -80,16 +75,16 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) { ref var parameter = ref _shaderPackage->MaterialElementsSpan[i]; - if (parameter.CRC == parameterCrc) - { - if ((parameter.Offset & 0x3) != 0 - || (parameter.Size & 0x3) != 0 - || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) - return; + if (parameter.CRC != parameterCrc) + continue; - value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + if ((parameter.Offset & 0x3) != 0 + || (parameter.Size & 0x3) != 0 + || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) return; - } + + value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + return; } } @@ -104,25 +99,24 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase var samplers = _shaderPackage->Samplers; for (var i = 0; i < _shaderPackage->SamplerCount; ++i) { - if (samplers[i].CRC == samplerCrc) - { - id = samplers[i].Id; - found = true; - break; - } + if (samplers[i].CRC != samplerCrc) + continue; + + id = samplers[i].Id; + found = true; + break; } if (!found) return; - var material = Material; - for (var i = 0; i < material->TextureCount; ++i) + for (var i = 0; i < Material->TextureCount; ++i) { - if (material->Textures[i].Id == id) - { - material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; - break; - } + if (Material->Textures[i].Id != id) + continue; + + Material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; + break; } } @@ -139,9 +133,6 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (shpkHandle == null) return false; - if (_shaderPackage != shpkHandle->ShaderPackage) - return false; - - return true; + return _shaderPackage == shpkHandle->ShaderPackage; } } diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs index 86fee976..07986f52 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -61,9 +61,6 @@ public abstract unsafe class LiveMaterialPreviewerBase : IDisposable if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject)) return false; - if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject)) - return false; - - return true; + return Material == MaterialInfo.GetDrawObjectMaterial(DrawObject); } } diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index ec0ddd29..686b5a86 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -24,22 +24,6 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy public nint GetDrawObject(nint address) => GetDrawObject(Type, address); - public static unsafe nint GetDrawObject(DrawObjectType type, nint address) - { - var gameObject = (Character*)address; - if (gameObject == null) - return nint.Zero; - - return type switch - { - DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(), - DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject, - DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject, - DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject, - _ => nint.Zero, - }; - } - public unsafe Material* GetDrawObjectMaterial(IObjectTable objects) => GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects))); @@ -103,4 +87,20 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy return result; } + + private static unsafe nint GetDrawObject(DrawObjectType type, nint address) + { + var gameObject = (Character*)address; + if (gameObject == null) + return nint.Zero; + + return type switch + { + DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject, + DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject, + DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject, + _ => nint.Zero, + }; + } } diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index c7b24bd7..2eeefbd8 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; using Penumbra.GameData.Enums; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 19c0fd10..dd4b03f2 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -6,6 +6,7 @@ using OtterGui.Services; using Penumbra.Interop.Hooks; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index b944011d..32090f7c 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,7 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Actors; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Objects; using Penumbra.Services; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 9d899648..a3400540 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -4,13 +4,13 @@ using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; +using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index 6d7840d8..f4218e9c 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,34 +1,15 @@ -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using Penumbra.Collections; -using Penumbra.GameData; using Penumbra.Interop.Services; using Penumbra.String; namespace Penumbra.Interop.PathResolving; -public unsafe class PathState : IDisposable +public sealed class PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility) + : IDisposable { - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - - [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _weaponVTable = null!; - - [Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _demiHumanVTable = null!; - - [Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _monsterVTable = null!; - - public readonly CollectionResolver CollectionResolver; - public readonly MetaState MetaState; - public readonly CharacterUtility CharacterUtility; - - private readonly ResolvePathHooks _human; - private readonly ResolvePathHooks _weapon; - private readonly ResolvePathHooks _demiHuman; - private readonly ResolvePathHooks _monster; + public readonly CollectionResolver CollectionResolver = collectionResolver; + public readonly MetaState MetaState = metaState; + public readonly CharacterUtility CharacterUtility = characterUtility; private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); private readonly ThreadLocal _internalResolve = new(() => 0, false); @@ -39,31 +20,11 @@ public unsafe class PathState : IDisposable public bool InInternalResolve => _internalResolve.Value != 0u; - public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop) - { - interop.InitializeFromAttributes(this); - CollectionResolver = collectionResolver; - MetaState = metaState; - CharacterUtility = characterUtility; - _human = new ResolvePathHooks(interop, this, _humanVTable, ResolvePathHooks.Type.Human); - _weapon = new ResolvePathHooks(interop, this, _weaponVTable, ResolvePathHooks.Type.Other); - _demiHuman = new ResolvePathHooks(interop, this, _demiHumanVTable, ResolvePathHooks.Type.Other); - _monster = new ResolvePathHooks(interop, this, _monsterVTable, ResolvePathHooks.Type.Other); - _human.Enable(); - _weapon.Enable(); - _demiHuman.Enable(); - _monster.Enable(); - } - public void Dispose() { _resolveData.Dispose(); _internalResolve.Dispose(); - _human.Dispose(); - _weapon.Dispose(); - _demiHuman.Dispose(); - _monster.Dispose(); } public bool Consume(ByteString _, out ResolveData collection) @@ -86,9 +47,7 @@ public unsafe class PathState : IDisposable return path; if (!InInternalResolve) - { _resolveData.Value = collection.ToResolveData(gameObject); - } return path; } @@ -99,9 +58,7 @@ public unsafe class PathState : IDisposable return path; if (!InInternalResolve) - { _resolveData.Value = data; - } return path; } @@ -126,7 +83,7 @@ public unsafe class PathState : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public readonly void Dispose() + public void Dispose() { --_internalResolve.Value; } diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 370118ea..2359c36e 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,17 +1,10 @@ -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.Services; using Penumbra.Interop.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Interop.PathResolving; @@ -20,49 +13,37 @@ namespace Penumbra.Interop.PathResolving; /// Those are loaded synchronously. /// Thus, we need to ensure the correct files are loaded when a material is loaded. /// -public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> +public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> { - private readonly PerformanceTracker _performance; + private readonly GameState _gameState; private readonly ResourceLoader _loader; private readonly ResourceHandleDestructor _resourceHandleDestructor; - private readonly CommunicatorService _communicator; - private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); - private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); - - private readonly ConcurrentDictionary _subFileCollection = new(); - - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor) + public SubfileHelper(GameState gameState, ResourceLoader loader, ResourceHandleDestructor resourceHandleDestructor) { - interop.InitializeFromAttributes(this); - - _performance = performance; - _loader = loader; - _communicator = communicator; + _gameState = gameState; + _loader = loader; _resourceHandleDestructor = resourceHandleDestructor; - _loadMtrlShpkHook.Enable(); - _loadMtrlTexHook.Enable(); - _apricotResourceLoadHook.Enable(); - _loader.ResourceLoaded += SubfileContainerRequested; + _loader.ResourceLoaded += SubfileContainerRequested; _resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper); } public IEnumerator> GetEnumerator() - => _subFileCollection.GetEnumerator(); + => _gameState.SubFileCollection.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count - => _subFileCollection.Count; + => _gameState.SubFileCollection.Count; public ResolveData MtrlData - => _mtrlData.IsValueCreated ? _mtrlData.Value : ResolveData.Invalid; + => _gameState.MtrlData.IsValueCreated ? _gameState.MtrlData.Value : ResolveData.Invalid; public ResolveData AvfxData - => _avfxData.IsValueCreated ? _avfxData.Value : ResolveData.Invalid; + => _gameState.AvfxData.IsValueCreated ? _gameState.AvfxData.Value : ResolveData.Invalid; /// /// Check specifically for shpk and tex files whether we are currently in a material load, @@ -71,13 +52,13 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollectionFileSize == 0) - _subFileCollection[(nint)handle] = resolveData; + _gameState.SubFileCollection[(nint)handle] = resolveData; break; } } private void ResourceDestroyed(ResourceHandle* handle) - => _subFileCollection.TryRemove((nint)handle, out _); - - private delegate byte LoadMtrlFilesDelegate(nint mtrlResourceHandle); - - [Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))] - private readonly Hook _loadMtrlTexHook = null!; - - private byte LoadMtrlTexDetour(nint mtrlResourceHandle) - { - using var performance = _performance.Measure(PerformanceType.LoadTextures); - var last = _mtrlData.Value; - _mtrlData.Value = LoadFileHelper(mtrlResourceHandle); - var ret = _loadMtrlTexHook.Original(mtrlResourceHandle); - _mtrlData.Value = last; - return ret; - } - - [Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))] - private readonly Hook _loadMtrlShpkHook = null!; - - private byte LoadMtrlShpkDetour(nint mtrlResourceHandle) - { - using var performance = _performance.Measure(PerformanceType.LoadShaders); - var last = _mtrlData.Value; - var mtrlData = LoadFileHelper(mtrlResourceHandle); - _mtrlData.Value = mtrlData; - var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle); - _mtrlData.Value = last; - _communicator.MtrlShpkLoaded.Invoke(mtrlResourceHandle, mtrlData.AssociatedGameObject); - return ret; - } - - private ResolveData LoadFileHelper(nint resourceHandle) - { - if (resourceHandle == nint.Zero) - return ResolveData.Invalid; - - return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid; - } - - - private delegate byte ApricotResourceLoadDelegate(nint handle, nint unk1, byte unk2); - - [Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))] - private readonly Hook _apricotResourceLoadHook = null!; - - private byte ApricotResourceLoadDetour(nint handle, nint unk1, byte unk2) - { - using var performance = _performance.Measure(PerformanceType.LoadApricotResources); - var last = _avfxData.Value; - _avfxData.Value = LoadFileHelper(handle); - var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2); - _avfxData.Value = last; - return ret; - } + => _gameState.SubFileCollection.TryRemove((nint)handle, out _); } diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 444b9a48..21331916 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Services; namespace Penumbra.Interop.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 376bbcf7..b4801f5f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -9,7 +9,7 @@ using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 8d3e32f9..8b6ef331 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -14,7 +14,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Models; using Penumbra.Import.Textures; -using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; From c6642c4fa30eaee0f10dc1cbd208101d76b33630 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 11:32:26 +1100 Subject: [PATCH 1434/2451] Spike material export workflow --- .../Import/Models/Export/MaterialExporter.cs | 81 +++++++++++++++++++ .../Import/Models/Export/ModelExporter.cs | 18 ++--- Penumbra/Import/Models/ModelManager.cs | 65 ++++++++++++++- 3 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 Penumbra/Import/Models/Export/MaterialExporter.cs diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs new file mode 100644 index 00000000..ef417e35 --- /dev/null +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -0,0 +1,81 @@ +using Lumina.Data.Parsing; +using Penumbra.GameData.Files; +using SharpGLTF.Materials; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Models.Export; + +public class MaterialExporter +{ + // input stuff + public struct Material + { + public MtrlFile Mtrl; + public Sampler[] Samplers; + // variant? + } + + public struct Sampler + { + public TextureUsage Usage; + public Image Texture; + } + + public static MaterialBuilder Export(Material material, string name) + { + return material.Mtrl.ShaderPackage.Name switch + { + "character.shpk" => BuildCharacter(material, name), + _ => BuildFallback(material, name), + }; + } + + private static MaterialBuilder BuildCharacter(Material material, string name) + { + // TODO: pixelbashing time + var sampler = material.Samplers + .Where(s => s.Usage == TextureUsage.SamplerNormal) + .First(); + + // TODO: clean up this name generation a bunch. probably a method. + var imageName = name.Replace("/", ""); + var baseColor = BuildImage(sampler.Texture, $"{imageName}_basecolor"); + + return BuildSharedBase(material, name) + .WithBaseColor(baseColor); + } + + private static MaterialBuilder BuildFallback(Material material, string name) + { + Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); + return BuildSharedBase(material, name) + .WithMetallicRoughnessShader() + .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One); + } + + private static MaterialBuilder BuildSharedBase(Material material, string name) + { + // TODO: Move this and potentially the other known stuff into MtrlFile? + const uint backfaceMask = 0x1; + var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; + + return new MaterialBuilder(name) + .WithDoubleSide(showBackfaces); + } + + private static ImageBuilder BuildImage(Image image, string name) + { + byte[] textureBytes; + using (var memoryStream = new MemoryStream()) + { + image.Save(memoryStream, PngFormat.Instance); + textureBytes = memoryStream.ToArray(); + } + + var imageBuilder = ImageBuilder.From(textureBytes, name); + imageBuilder.AlternateWriteFileName = $"{name}.*"; + return imageBuilder; + } +} diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 6a25af61..da24fbb0 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,10 +23,10 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var materials = ConvertMaterials(mdl); + var materials = ConvertMaterials(mdl, rawMaterials); var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); return new Model(meshes, gltfSkeleton); } @@ -51,16 +51,12 @@ public class ModelExporter return meshes; } - // TODO: Compose textures for use with these materials - /// Build placeholder materials for each of the material slots in the .mdl. - private static MaterialBuilder[] ConvertMaterials(MdlFile mdl) + /// Build materials for each of the material slots in the .mdl. + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials) => mdl.Materials - .Select(name => - new MaterialBuilder(name) - .WithMetallicRoughnessShader() - .WithDoubleSide(true) - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One) - ) + // TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback. + // fallback can likely be a static on the material exporter. + .Select(name => MaterialExporter.Export(rawMaterials[name], name)) .ToArray(); /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index f099a0e0..ccf56fe8 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using Lumina.Data.Parsing; using OtterGui; using OtterGui.Tasks; using Penumbra.Collections.Manager; @@ -9,15 +10,22 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; +using Penumbra.Import.Textures; using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes; -using SharpGLTF.Schema2; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable +using Schema2 = SharpGLTF.Schema2; +using LuminaMaterial = Lumina.Models.Materials.Material; + +public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; + private readonly IDataManager _gameData = gameData; + private readonly TextureManager _textureManager = textureManager; private readonly ConcurrentDictionary _tasks = new(); @@ -132,11 +140,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public void Execute(CancellationToken cancel) { Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); + Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); var xivSkeletons = BuildSkeletons(cancel); + Penumbra.Log.Debug("[GLTF Export] Reading materials..."); + var materials = mdl.Materials.ToDictionary( + path => path, + path => BuildMaterial(path, cancel) + ); + Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons); + var model = ModelExporter.Export(mdl, xivSkeletons, materials); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); @@ -169,6 +184,48 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect delayTicks: pair.Index, cancellationToken: cancel); } + private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) + { + // TODO: this should probably be chosen in the export settings + var variantId = 1; + + var absolutePath = relativePath.StartsWith("/") + ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) + : relativePath; + + // TODO: this should be a recoverable warning - as should the one below it i think + if (absolutePath == null) + throw new Exception("Failed to resolve material path."); + + // TODO: collection lookup and such. this is currently in mdltab (readsklb), and should be wholesale moved in here. + var data = manager._gameData.GetFile(absolutePath); + if (data == null) + throw new Exception("Failed to fetch material game data."); + + var mtrl = new MtrlFile(data.Data); + + return new MaterialExporter.Material + { + Mtrl = mtrl, + Samplers = mtrl.ShaderPackage.Samplers + .Select(sampler => new MaterialExporter.Sampler + { + Usage = (TextureUsage)sampler.SamplerId, + Texture = ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel), + }) + .ToArray(), + }; + } + + private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) + { + var (image, _) = manager._textureManager.Load(texture.Path); + var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; + if (pngImage == null) + throw new Exception("Failed to convert texture to png."); + return pngImage; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) @@ -185,7 +242,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public void Execute(CancellationToken cancel) { - var model = ModelRoot.Load(inputPath); + var model = Schema2.ModelRoot.Load(inputPath); Out = ModelImporter.Import(model); } From c8e58c08a064a948b3b3bc333caed3df20b55c4a Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 16:11:51 +1100 Subject: [PATCH 1435/2451] Compose character diffuse --- .../Import/Models/Export/MaterialExporter.cs | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index ef417e35..4d085f18 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -34,19 +34,51 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { - // TODO: pixelbashing time - var sampler = material.Samplers + var table = material.Mtrl.Table; + var normal = material.Samplers .Where(s => s.Usage == TextureUsage.SamplerNormal) - .First(); + .First() + .Texture; + + var baseColorTarget = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(baseColorTarget, (sourceAccessor, targetAccessor) => + { + for (int y = 0; y < sourceAccessor.Height; y++) + { + var sourceRow = sourceAccessor.GetRowSpan(y); + var targetRow = targetAccessor.GetRowSpan(y); + + for (int x = 0; x < sourceRow.Length; x++) + { + var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); + var prevRow = table[(int)MathF.Floor(smoothed)]; + var nextRow = table[(int)MathF.Ceiling(smoothed)]; + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); + targetRow[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + } + } + }); // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); - var baseColor = BuildImage(sampler.Texture, $"{imageName}_basecolor"); + var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); return BuildSharedBase(material, name) .WithBaseColor(baseColor); } + private static (float Smooth, float Stepped) GetTableRowIndices(float input) + { + // These calculations are ported from character.shpk. + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; + + var stepped = MathF.Floor(smoothed + 0.5f); + + return (smoothed, stepped); + } + private static MaterialBuilder BuildFallback(Material material, string name) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); From 509b4c8866bd0ed37a994d9237059b6be0bb3ec2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 17:05:57 +1100 Subject: [PATCH 1436/2451] Wire up normals and opacity --- .../Import/Models/Export/MaterialExporter.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 4d085f18..d16e9367 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -50,11 +50,22 @@ public class MaterialExporter for (int x = 0; x < sourceRow.Length; x++) { + ref var sourcePixel = ref sourceRow[x]; + ref var targetPixel = ref targetRow[x]; + var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); var prevRow = table[(int)MathF.Floor(smoothed)]; var nextRow = table[(int)MathF.Ceiling(smoothed)]; + + // Base colour (table[.a], .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); - targetRow[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + targetPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); + targetPixel.A = sourcePixel.B; + + // Normal (.rg) + // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. + sourcePixel.B = byte.MaxValue; + sourcePixel.A = byte.MaxValue; } } }); @@ -62,9 +73,13 @@ public class MaterialExporter // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); + var normalThing = BuildImage(normal, $"{imageName}_normal"); return BuildSharedBase(material, name) - .WithBaseColor(baseColor); + // NOTE: this isn't particularly precise to game behavior, but good enough for now. + .WithAlpha(AlphaMode.MASK, 0.5f) + .WithBaseColor(baseColor) + .WithNormal(normalThing); } private static (float Smooth, float Stepped) GetTableRowIndices(float input) From 96f40b7ddcb25f19bcd9fe306b473f7c5a9d5e3f Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:33:45 +1100 Subject: [PATCH 1437/2451] Expand to more general-purpose transform codepath --- .../Import/Models/Export/MaterialExporter.cs | 99 ++++++++++++------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index d16e9367..c08d954c 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -2,11 +2,15 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; using SharpGLTF.Materials; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.Import.Models.Export; +using ImageSharpConfiguration = SixLabors.ImageSharp.Configuration; + public class MaterialExporter { // input stuff @@ -34,52 +38,81 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { + // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; + // TODO: this should probably be a dict var normal = material.Samplers .Where(s => s.Usage == TextureUsage.SamplerNormal) .First() .Texture; - var baseColorTarget = new Image(normal.Width, normal.Height); - normal.ProcessPixelRows(baseColorTarget, (sourceAccessor, targetAccessor) => + var operation = new CharacterOperation() { - for (int y = 0; y < sourceAccessor.Height; y++) - { - var sourceRow = sourceAccessor.GetRowSpan(y); - var targetRow = targetAccessor.GetRowSpan(y); - - for (int x = 0; x < sourceRow.Length; x++) - { - ref var sourcePixel = ref sourceRow[x]; - ref var targetPixel = ref targetRow[x]; - - var (smoothed, stepped) = GetTableRowIndices(sourceRow[x].A / 255f); - var prevRow = table[(int)MathF.Floor(smoothed)]; - var nextRow = table[(int)MathF.Ceiling(smoothed)]; - - // Base colour (table[.a], .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, smoothed % 1); - targetPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); - targetPixel.A = sourcePixel.B; - - // Normal (.rg) - // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. - sourcePixel.B = byte.MaxValue; - sourcePixel.A = byte.MaxValue; - } - } - }); + Table = table, + Normal = normal, + BaseColor = new Image(normal.Width, normal.Height), + Emissive = new Image(normal.Width, normal.Height), + }; + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", ""); - var baseColor = BuildImage(baseColorTarget, $"{imageName}_basecolor"); - var normalThing = BuildImage(normal, $"{imageName}_normal"); return BuildSharedBase(material, name) + // .WithSpecularGlossinessShader() + // .WithDiffuse() // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) - .WithBaseColor(baseColor) - .WithNormal(normalThing); + .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) + .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) + .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + } + + private readonly struct CharacterOperation : IRowOperation + { + public required MtrlFile.ColorTable Table { get; init; } + + public required Image Normal { get; init; } + public required Image BaseColor { get; init; } + public required Image Emissive { get; init; } + + private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; + private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; + private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) + { + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); + var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + ref var normalPixel = ref normalSpan[x]; + ref var baseColorPixel = ref baseColorSpan[x]; + ref var emissivePixel = ref emissiveSpan[x]; + + // Table row data (.a) + var (smoothed, stepped) = GetTableRowIndices(normalPixel.A / 255f); + var weight = smoothed % 1; + var prevRow = Table[(int)MathF.Floor(smoothed)]; + var nextRow = Table[(int)MathF.Ceiling(smoothed)]; + + // Base colour (table, .b) + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, weight); + baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorPixel.A = normalPixel.B; + + // Emissive (table) + var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, weight); + emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); + + // Normal (.rg) + // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. + normalPixel.B = byte.MaxValue; + normalPixel.A = byte.MaxValue; + } + } } private static (float Smooth, float Stepped) GetTableRowIndices(float input) @@ -112,7 +145,7 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } - private static ImageBuilder BuildImage(Image image, string name) + private static ImageBuilder BuildImage(Image image, string name) { byte[] textureBytes; using (var memoryStream = new MemoryStream()) From 74ffc56d6c48d10bd91fee053ded67baa8647418 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:48:15 +1100 Subject: [PATCH 1438/2451] Fix errors with same name expanding together --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index c92e2926..4ac789ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -138,14 +138,14 @@ public partial class ModEditWindow using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, borderColor: Colors.RegexWarningBorder); var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; - foreach (var exception in tab.IoExceptions) + foreach (var (exception, index) in tab.IoExceptions.WithIndex()) { var message = $"{exception.GetType().Name}: {exception.Message}"; var textSize = ImGui.CalcTextSize(message).X; if (textSize > spaceAvail) message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; - using (var exceptionNode = ImRaii.TreeNode(message)) + using (var exceptionNode = ImRaii.TreeNode($"{message}###exception{index}")) { if (exceptionNode) ImGuiUtil.TextWrapped(exception.ToString()); From 4572cb83f0f1b311510536d5dd1622e98d0d0517 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 20:48:46 +1100 Subject: [PATCH 1439/2451] Move table calcs into struct --- .../Import/Models/Export/MaterialExporter.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index c08d954c..437b57f7 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -93,18 +93,17 @@ public class MaterialExporter ref var emissivePixel = ref emissiveSpan[x]; // Table row data (.a) - var (smoothed, stepped) = GetTableRowIndices(normalPixel.A / 255f); - var weight = smoothed % 1; - var prevRow = Table[(int)MathF.Floor(smoothed)]; - var nextRow = Table[(int)MathF.Ceiling(smoothed)]; + var tableRow = GetTableRowIndices(normalPixel.A / 255f); + var prevRow = Table[tableRow.Previous]; + var nextRow = Table[tableRow.Next]; // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, weight); + var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorPixel.A = normalPixel.B; // Emissive (table) - var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, weight); + var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) @@ -115,7 +114,7 @@ public class MaterialExporter } } - private static (float Smooth, float Stepped) GetTableRowIndices(float input) + private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) @@ -124,7 +123,21 @@ public class MaterialExporter var stepped = MathF.Floor(smoothed + 0.5f); - return (smoothed, stepped); + return new TableRow + { + Stepped = (int)stepped, + Previous = (int)MathF.Floor(smoothed), + Next = (int)MathF.Ceiling(smoothed), + Weight = smoothed % 1, + }; + } + + private ref struct TableRow + { + public int Stepped; + public int Previous; + public int Next; + public float Weight; } private static MaterialBuilder BuildFallback(Material material, string name) From db2081f14d944283f890244f50f1acb4b84ff3da Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 21:36:42 +1100 Subject: [PATCH 1440/2451] More refactors of operation, extract specular color --- .../Import/Models/Export/MaterialExporter.cs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 437b57f7..de2f1425 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -46,65 +46,63 @@ public class MaterialExporter .First() .Texture; - var operation = new CharacterOperation() - { - Table = table, - Normal = normal, - BaseColor = new Image(normal.Width, normal.Height), - Emissive = new Image(normal.Width, normal.Height), - }; + var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // TODO: clean up this name generation a bunch. probably a method. - var imageName = name.Replace("/", ""); + var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) - // .WithSpecularGlossinessShader() - // .WithDiffuse() // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) + .WithSpecularColor(BuildImage(operation.Specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); } - private readonly struct CharacterOperation : IRowOperation + // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. + // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. + private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { - public required MtrlFile.ColorTable Table { get; init; } - - public required Image Normal { get; init; } - public required Image BaseColor { get; init; } - public required Image Emissive { get; init; } + public Image Normal { get; private init; } = normal.Clone(); + public Image BaseColor { get; private init; } = new Image(normal.Width, normal.Height); + public Image Specular { get; private init; } = new Image(normal.Width, normal.Height); + public Image Emissive { get; private init; } = new Image(normal.Width, normal.Height); private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; + private Buffer2D SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer; private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; public void Invoke(int y) { var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); + var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); for (int x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; - ref var baseColorPixel = ref baseColorSpan[x]; - ref var emissivePixel = ref emissiveSpan[x]; // Table row data (.a) var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = Table[tableRow.Previous]; - var nextRow = Table[tableRow.Next]; + var prevRow = table[tableRow.Previous]; + var nextRow = table[tableRow.Next]; // Base colour (table, .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); - baseColorPixel.FromVector4(new Vector4(lerpedDiffuse, 1)); - baseColorPixel.A = normalPixel.B; + baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorSpan[x].A = normalPixel.B; + + // Specular (table) + var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); // Emissive (table) var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); - emissivePixel.FromVector4(new Vector4(lerpedEmissive, 1)); + emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. From e8fd452b8f12d23a0e30a6c684919edaac1f32fe Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 22:16:47 +1100 Subject: [PATCH 1441/2451] Improve file reading --- .../Import/Models/Export/MaterialExporter.cs | 19 ++++-------- Penumbra/Import/Models/ModelManager.cs | 30 +++++++----------- .../ModEditWindow.Models.MdlTab.cs | 31 ++++++++++--------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index de2f1425..dee386df 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -13,22 +13,16 @@ using ImageSharpConfiguration = SixLabors.ImageSharp.Configuration; public class MaterialExporter { - // input stuff public struct Material { public MtrlFile Mtrl; - public Sampler[] Samplers; + public Dictionary> Textures; // variant? } - public struct Sampler - { - public TextureUsage Usage; - public Image Texture; - } - public static MaterialBuilder Export(Material material, string name) { + Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch { "character.shpk" => BuildCharacter(material, name), @@ -40,11 +34,10 @@ public class MaterialExporter { // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; - // TODO: this should probably be a dict - var normal = material.Samplers - .Where(s => s.Usage == TextureUsage.SamplerNormal) - .First() - .Texture; + + // TODO: there's a few normal usages i should check, i think. + // TODO: tryget + var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index ccf56fe8..4f652436 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -39,8 +39,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbs, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath)); public Task ImportGltf(string inputPath) { @@ -134,7 +134,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbs, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) : IAction { public void Execute(CancellationToken cancel) @@ -166,7 +166,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Attempt to read out the pertinent information from a .sklb. private IEnumerable BuildSkeletons(CancellationToken cancel) { - var havokTasks = sklbs + var havokTasks = sklbPaths + .Select(path => new SklbFile(read(path))) .WithIndex() .Select(CreateHavokTask) .ToArray(); @@ -197,29 +198,22 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect if (absolutePath == null) throw new Exception("Failed to resolve material path."); - // TODO: collection lookup and such. this is currently in mdltab (readsklb), and should be wholesale moved in here. - var data = manager._gameData.GetFile(absolutePath); - if (data == null) - throw new Exception("Failed to fetch material game data."); - - var mtrl = new MtrlFile(data.Data); + var mtrl = new MtrlFile(read(absolutePath)); return new MaterialExporter.Material { Mtrl = mtrl, - Samplers = mtrl.ShaderPackage.Samplers - .Select(sampler => new MaterialExporter.Sampler - { - Usage = (TextureUsage)sampler.SamplerId, - Texture = ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel), - }) - .ToArray(), + Textures = mtrl.ShaderPackage.Samplers.ToDictionary( + sampler => (TextureUsage)sampler.SamplerId, + sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) + ), }; } private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { - var (image, _) = manager._textureManager.Load(texture.Path); + using var textureData = new MemoryStream(read(texture.Path)); + var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; if (pngImage == null) throw new Exception("Failed to convert texture to png."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cdaf399f..f79d161e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -116,11 +116,10 @@ public partial class ModEditWindow /// .mdl game path to resolve satellite files such as skeletons relative to. public void Export(string outputPath, Utf8GamePath mdlPath) { - IEnumerable skeletons; + IEnumerable sklbPaths; try { - var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); - skeletons = sklbPaths.Select(ReadSklb).ToArray(); + sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); } catch (Exception exception) { @@ -129,7 +128,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, skeletons, outputPath) + _edit._models.ExportToGltf(Mdl, sklbPaths, ReadFile, outputPath) .ContinueWith(task => { RecordIoExceptions(task.Exception); @@ -219,22 +218,24 @@ public partial class ModEditWindow Exception other => [other], }; } - - /// Read a .sklb from the active collection or game. - /// Game path to the .sklb to load. - private SklbFile ReadSklb(string sklbPath) + + /// Read a file from the active collection or game. + /// Game path to the file to load. + // TODO: Also look up files within the current mod regardless of mod state? + private byte[] ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) - throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); + if (!Utf8GamePath.FromString(path, out var utf8SklbPath, true)) + throw new Exception($"Resolved path {path} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath == null ? _edit._gameData.GetFile(sklbPath)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); - return bytes != null - ? new SklbFile(bytes) - : throw new Exception( - $"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); + var bytes = resolvedPath == null + ? _edit._gameData.GetFile(path)?.Data + : File.ReadAllBytes(resolvedPath.Value.ToPath()); + + return bytes ?? throw new Exception( + $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } /// Remove the material given by the index. From 2fa72727622fb872d2c65f761892540d060a389b Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 13 Jan 2024 23:52:26 +1100 Subject: [PATCH 1442/2451] Add support for explicit diffuse + specular textures --- .../Import/Models/Export/MaterialExporter.cs | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index dee386df..3d274ff9 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace Penumbra.Import.Models.Export; @@ -32,25 +33,37 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { - // TODO: handle models with an underlying diffuse var table = material.Mtrl.Table; // TODO: there's a few normal usages i should check, i think. - // TODO: tryget var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); + var baseColor = operation.BaseColor; + if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) + { + MultiplyOperation.Execute(diffuse, baseColor); + baseColor = diffuse; + } + + // TODO: what about the two specularmaps? + var specular = operation.Specular; + if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var newSpecular)) + { + MultiplyOperation.Execute(newSpecular, specular); + } + // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) // NOTE: this isn't particularly precise to game behavior, but good enough for now. .WithAlpha(AlphaMode.MASK, 0.5f) - .WithBaseColor(BuildImage(operation.BaseColor, $"{imageName}_basecolor")) + .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) - .WithSpecularColor(BuildImage(operation.Specular, $"{imageName}_specular")) + .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); } @@ -105,6 +118,40 @@ public class MaterialExporter } } + private readonly struct MultiplyOperation + { + public static void Execute(Image target, Image multiplier) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + { + // Ensure the images are the same size + var (small, large) = target.Width < multiplier.Width && target.Height < multiplier.Height + ? ((Image)target, (Image)multiplier) + : (multiplier, target); + small.Mutate(context => context.Resize(large.Width, large.Height)); + + var operation = new MultiplyOperation(target, multiplier); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds(), in operation); + } + } + + private readonly struct MultiplyOperation(Image target, Image multiplier) : IRowOperation + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + { + + public void Invoke(int y) + { + var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + + for (int x = 0; x < targetSpan.Length; x++) + { + targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4()); + } + } + } + private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. From ca58c81bcebc051b0ac8e25255e5bc28095adee6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 00:06:16 +1100 Subject: [PATCH 1443/2451] Add characterglass support --- Penumbra/Import/Models/Export/MaterialExporter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 3d274ff9..eff5f835 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -26,8 +26,10 @@ public class MaterialExporter Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch { - "character.shpk" => BuildCharacter(material, name), - _ => BuildFallback(material, name), + // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. + "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + _ => BuildFallback(material, name), }; } @@ -59,8 +61,6 @@ public class MaterialExporter var imageName = name.Replace("/", "").Replace(".mtrl", ""); return BuildSharedBase(material, name) - // NOTE: this isn't particularly precise to game behavior, but good enough for now. - .WithAlpha(AlphaMode.MASK, 0.5f) .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) From 2d8b7efc006ad6303754900b941c0b1b21899054 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Jan 2024 14:06:45 +0100 Subject: [PATCH 1444/2451] Some cleanup. --- Penumbra/Mods/Editor/DuplicateManager.cs | 68 +++++++-------- Penumbra/Mods/Editor/FileRegistry.cs | 4 +- Penumbra/Mods/Editor/ModMerger.cs | 10 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 1 + .../UI/AdvancedWindow/ModEditWindow.Files.cs | 1 + Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 85 +++++++++---------- 6 files changed, 78 insertions(+), 91 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 47c34ce5..4773d840 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -5,25 +6,15 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class DuplicateManager +public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) { - private readonly Configuration _config; - private readonly SaveService _saveService; - private readonly ModManager _modManager; - private readonly SHA256 _hasher = SHA256.Create(); - private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - - public DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) - { - _modManager = modManager; - _saveService = saveService; - _config = config; - } + private readonly SHA256 _hasher = SHA256.Create(); + private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates => _duplicates; - public long SavedSpace { get; private set; } = 0; + public long SavedSpace { get; private set; } public Task Worker { get; private set; } = Task.CompletedTask; private CancellationTokenSource _cancellationTokenSource = new(); @@ -68,6 +59,19 @@ public class DuplicateManager private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) { + ModEditor.ApplyToAllOptions(mod, HandleSubMod); + + try + { + File.Delete(duplicate.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}"); + } + + return; + void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) { var changes = false; @@ -78,26 +82,15 @@ public class DuplicateManager if (useModManager) { - _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); } else { var sub = (SubMod)subMod; sub.FileData = dict; - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } - - ModEditor.ApplyToAllOptions(mod, HandleSubMod); - - try - { - File.Delete(duplicate.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}"); - } } private static FullPath ChangeDuplicatePath(Mod mod, FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes) @@ -199,15 +192,6 @@ public class DuplicateManager } } - public static bool CompareHashes(byte[] f1, byte[] f2) - => StructuralComparisons.StructuralEqualityComparer.Equals(f1, f2); - - public byte[] ComputeHash(FullPath f) - { - 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. @@ -232,14 +216,13 @@ public class DuplicateManager } } - /// Deduplicate a mod simply by its directory without any confirmation or waiting time. internal void DeduplicateMod(DirectoryInfo modDirectory) { try { var mod = new Mod(modDirectory); - _modManager.Creator.ReloadMod(mod, true, out _); + modManager.Creator.ReloadMod(mod, true, out _); Clear(); var files = new ModFileCollection(); @@ -252,4 +235,13 @@ public class DuplicateManager Penumbra.Log.Warning($"Could not deduplicate mod {modDirectory.Name}:\n{e}"); } } + + private static bool CompareHashes(byte[] f1, byte[] f2) + => StructuralComparisons.StructuralEqualityComparer.Equals(f1, f2); + + private byte[] ComputeHash(FullPath f) + { + using var stream = File.OpenRead(f.FullName); + return _hasher.ComputeHash(stream); + } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index a223b51e..96d027b3 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,11 +1,11 @@ using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = new(); + public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = []; public FullPath File { get; private init; } public Utf8RelPath RelPath { get; private init; } public long FileSize { get; private init; } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 1dfe9e76..f5d0e4a4 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -29,12 +29,12 @@ public class ModMerger : IDisposable public string OptionGroupName = "Merges"; public string OptionName = string.Empty; - private readonly Dictionary _fileToFile = new(); - private readonly HashSet _createdDirectories = new(); - private readonly HashSet _createdGroups = new(); - private readonly HashSet _createdOptions = new(); + private readonly Dictionary _fileToFile = []; + private readonly HashSet _createdDirectories = []; + private readonly HashSet _createdGroups = []; + private readonly HashSet _createdOptions = []; - public readonly HashSet SelectedOptions = new(); + public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = new List(); public Exception? Error { get; private set; } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 4783e76b..16cacaa4 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -9,6 +9,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 4a193591..bae23729 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 7d4fa96f..1df814da 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -9,22 +9,14 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ModMergeTab +public class ModMergeTab(ModMerger modMerger) { - private readonly ModMerger _modMerger; - private readonly ModCombo _modCombo; - - private string _newModName = string.Empty; - - public ModMergeTab(ModMerger modMerger) - { - _modMerger = modMerger; - _modCombo = new ModCombo(() => _modMerger.ModsWithoutCurrent.ToList()); - } + private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList()); + private string _newModName = string.Empty; public void Draw() { - if (_modMerger.MergeFromMod == null) + if (modMerger.MergeFromMod == null) return; using var tab = ImRaii.TabItem("Merge Mods"); @@ -54,23 +46,23 @@ public class ModMergeTab { using var bigGroup = ImRaii.Group(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod!.Name} into "); + ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into "); ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); var width = ImGui.GetItemRectSize(); using (var g = ImRaii.Group()) { - using var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions); + using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1); - var group = _modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == _modMerger.OptionGroupName); - var color = group != null || _modMerger.OptionGroupName.Length == 0 && _modMerger.OptionName.Length == 0 + var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName); + var color = group != null || modMerger.OptionGroupName.Length == 0 && modMerger.OptionName.Length == 0 ? Colors.PressEnterWarningBg : Colors.DiscordColor; using var c = ImRaii.PushColor(ImGuiCol.Border, color); ImGui.SetNextItemWidth(buttonWidth); - ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref modMerger.OptionGroupName, 64); ImGuiUtil.HoverTooltip( "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n" + "A red border indicates an existing option group, a blue border indicates a new one."); @@ -79,29 +71,29 @@ public class ModMergeTab color = color == Colors.DiscordColor ? Colors.DiscordColor - : group == null || group.Any(o => o.Name == _modMerger.OptionName) + : group == null || group.Any(o => o.Name == modMerger.OptionName) ? Colors.PressEnterWarningBg : Colors.DiscordColor; c.Push(ImGuiCol.Border, color); ImGui.SetNextItemWidth(buttonWidth); - ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref modMerger.OptionName, 64); ImGuiUtil.HoverTooltip( "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n" + "A red border indicates an existing option, a blue border indicates a new one."); } - if (_modMerger.MergeFromMod.HasOptions) + if (modMerger.MergeFromMod.HasOptions) ImGuiUtil.HoverTooltip("You can only specify a target option if the source mod has no true options itself.", ImGuiHoveredFlags.AllowWhenDisabled); if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0), - _modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !_modMerger.CanMerge)) - _modMerger.Merge(); + modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge)) + modMerger.Merge(); } private void DrawMergeIntoDesc() { - ImGuiUtil.TextWrapped(_modMerger.MergeFromMod!.HasOptions + ImGuiUtil.TextWrapped(modMerger.MergeFromMod!.HasOptions ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break." : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."); } @@ -110,7 +102,7 @@ public class ModMergeTab { _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? "Select the target Mod...", string.Empty, width, ImGui.GetTextLineHeight()); - _modMerger.MergeToMod = _modCombo.CurrentSelection; + modMerger.MergeToMod = _modCombo.CurrentSelection; } private void DrawSplitOff(float size) @@ -121,24 +113,24 @@ public class ModMergeTab ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique."); var tt = _newModName.Length == 0 ? "Please enter a name for the newly created mod first." - : _modMerger.SelectedOptions.Count == 0 + : modMerger.SelectedOptions.Count == 0 ? "Please select at least one option to split off." : string.Empty; var buttonText = - $"Split Off {_modMerger.SelectedOptions.Count} Option{(_modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; + $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0)) - _modMerger.SplitIntoMod(_newModName); + modMerger.SplitIntoMod(_newModName); ImGui.Dummy(Vector2.One); var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); if (ImGui.Button("Select All", buttonSize)) - _modMerger.SelectedOptions.UnionWith(_modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods); ImGui.SameLine(); if (ImGui.Button("Unselect All", buttonSize)) - _modMerger.SelectedOptions.Clear(); + modMerger.SelectedOptions.Clear(); ImGui.SameLine(); if (ImGui.Button("Invert Selection", buttonSize)) - _modMerger.SelectedOptions.SymmetricExceptWith(_modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods); DrawOptionTable(size); } @@ -152,8 +144,8 @@ public class ModMergeTab private void DrawOptionTable(float size) { - var options = _modMerger.MergeFromMod!.AllSubMods.ToList(); - var height = _modMerger.Warnings.Count == 0 && _modMerger.Error == null + var options = modMerger.MergeFromMod!.AllSubMods.ToList(); + var height = modMerger.Warnings.Count == 0 && modMerger.Error == null ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() : 8 * ImGui.GetFrameHeightWithSpacing(); height = Math.Min(height, (options.Count + 1) * ImGui.GetFrameHeightWithSpacing()); @@ -178,15 +170,7 @@ public class ModMergeTab foreach (var (option, idx) in options.WithIndex()) { using var id = ImRaii.PushId(idx); - var selected = _modMerger.SelectedOptions.Contains(option); - - void Handle(SubMod option2, bool selected2) - { - if (selected2) - _modMerger.SelectedOptions.Add(option2); - else - _modMerger.SelectedOptions.Remove(option2); - } + var selected = modMerger.SelectedOptions.Contains(option); ImGui.TableNextColumn(); if (ImGui.Checkbox("##check", ref selected)) @@ -222,34 +206,43 @@ public class ModMergeTab ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + continue; + + void Handle(SubMod option2, bool selected2) + { + if (selected2) + modMerger.SelectedOptions.Add(option2); + else + modMerger.SelectedOptions.Remove(option2); + } } } private void DrawWarnings() { - if (_modMerger.Warnings.Count == 0) + if (modMerger.Warnings.Count == 0) return; ImGui.Separator(); ImGui.Dummy(Vector2.One); using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); - foreach (var warning in _modMerger.Warnings.SkipLast(1)) + foreach (var warning in modMerger.Warnings.SkipLast(1)) { ImGuiUtil.TextWrapped(warning); ImGui.Separator(); } - ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); + ImGuiUtil.TextWrapped(modMerger.Warnings[^1]); } private void DrawError() { - if (_modMerger.Error == null) + if (modMerger.Error == null) return; ImGui.Separator(); ImGui.Dummy(Vector2.One); using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); + ImGuiUtil.TextWrapped(modMerger.Error.ToString()); } } From f71096f8b07fd69f8a505a7a68628784da45a125 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 00:38:28 +1100 Subject: [PATCH 1445/2451] Handle mask ambient occlusion --- .../Import/Models/Export/MaterialExporter.cs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index eff5f835..97bfb6bc 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -43,28 +43,50 @@ public class MaterialExporter var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); - var baseColor = operation.BaseColor; + Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { - MultiplyOperation.Execute(diffuse, baseColor); + MultiplyOperation.Execute(diffuse, operation.BaseColor); baseColor = diffuse; } // TODO: what about the two specularmaps? - var specular = operation.Specular; - if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var newSpecular)) + Image specular = operation.Specular; + if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { - MultiplyOperation.Execute(newSpecular, specular); + MultiplyOperation.Execute(specularTexture, operation.Specular); + specular = specularTexture; + } + + Image? occlusion = null; + if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) + { + // Extract the red channel for ambient occlusion. + maskTexture.Mutate(context => context.Filter(new ColorMatrix( + 1f, 1f, 1f, 0f, + 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 1f, + 0f, 0f, 0f, 0f + ))); + occlusion = maskTexture; + + // TODO: handle other textures stored in the mask? } // TODO: clean up this name generation a bunch. probably a method. var imageName = name.Replace("/", "").Replace(".mtrl", ""); - return BuildSharedBase(material, name) + var materialBuilder = BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + + if (occlusion != null) + materialBuilder.WithOcclusion(BuildImage(occlusion, $"{imageName}_occlusion")); + + return materialBuilder; } // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. From b5d4b31301927aff8e4f81dc877d87cb2ffdf693 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 12:35:26 +1100 Subject: [PATCH 1446/2451] Add skin.shpk support --- .../Import/Models/Export/MaterialExporter.cs | 92 ++++++++++++++----- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 97bfb6bc..923b9c95 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -29,6 +29,7 @@ public class MaterialExporter // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "skin.shpk" => BuildSkin(material, name), _ => BuildFallback(material, name), }; } @@ -74,17 +75,14 @@ public class MaterialExporter // TODO: handle other textures stored in the mask? } - // TODO: clean up this name generation a bunch. probably a method. - var imageName = name.Replace("/", "").Replace(".mtrl", ""); - var materialBuilder = BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, $"{imageName}_basecolor")) - .WithNormal(BuildImage(operation.Normal, $"{imageName}_normal")) - .WithSpecularColor(BuildImage(specular, $"{imageName}_specular")) - .WithEmissive(BuildImage(operation.Emissive, $"{imageName}_emissive"), Vector3.One, 1); + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(operation.Normal, name, "normal")) + .WithSpecularColor(BuildImage(specular, name, "specular")) + .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); if (occlusion != null) - materialBuilder.WithOcclusion(BuildImage(occlusion, $"{imageName}_occlusion")); + materialBuilder.WithOcclusion(BuildImage(occlusion, name, "occlusion")); return materialBuilder; } @@ -140,6 +138,24 @@ public class MaterialExporter } } + private static TableRow GetTableRowIndices(float input) + { + // These calculations are ported from character.shpk. + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; + + var stepped = MathF.Floor(smoothed + 0.5f); + + return new TableRow + { + Stepped = (int)stepped, + Previous = (int)MathF.Floor(smoothed), + Next = (int)MathF.Ceiling(smoothed), + Weight = smoothed % 1, + }; + } + private readonly struct MultiplyOperation { public static void Execute(Image target, Image multiplier) @@ -174,22 +190,54 @@ public class MaterialExporter } } - private static TableRow GetTableRowIndices(float input) + private static MaterialBuilder BuildSkin(Material material, string name) { - // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + // Trust me bro. + const uint categorySkinType = 0x380CAED0; + const uint valueFace = 0xF5673524; - var stepped = MathF.Floor(smoothed + 0.5f); + // Face is the default for the skin shader, so a lack of skin type category is also correct. + var isFace = !material.Mtrl.ShaderPackage.ShaderKeys + .Any(key => key.Category == categorySkinType && key.Value != valueFace); - return new TableRow + // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. + // TODO: Specular? + var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; + var normal = material.Textures[TextureUsage.SamplerNormal]; + + // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. + var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); + diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1, - }; + for (int y = 0; y < diffuseAccessor.Height; y++) + { + var diffuseSpan = diffuseAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + + for (int x = 0; x < diffuseSpan.Length; x++) + { + diffuseSpan[x].A = normalSpan[x].B; + } + } + }); + + // Clear the blue channel out of the normal now that we're done with it. + normal.ProcessPixelRows(normalAccessor => { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + normalSpan[x].B = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(diffuse, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } private ref struct TableRow @@ -218,8 +266,10 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } - private static ImageBuilder BuildImage(Image image, string name) + private static ImageBuilder BuildImage(Image image, string materialName, string suffix) { + var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}"; + byte[] textureBytes; using (var memoryStream = new MemoryStream()) { From a6788c6dd3c69a2f183df1f7af06d1188c9c5ff7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 17:30:13 +1100 Subject: [PATCH 1447/2451] Add hair and iris support --- .../Import/Models/Export/MaterialExporter.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 923b9c95..98e4b3b9 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -29,6 +29,8 @@ public class MaterialExporter // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "hair.shpk" => BuildHair(material, name), + "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), _ => BuildFallback(material, name), }; @@ -190,6 +192,84 @@ public class MaterialExporter } } + // TODO: These are hardcoded colours - I'm not keen on supporting highly customiseable exports, but there's possibly some more sensible values to use here. + private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); + private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + + private static MaterialBuilder BuildHair(Material material, string name) + { + // Trust me bro. + const uint categoryHairType = 0x24826489; + const uint valueFace = 0x6E5B8F10; + + var isFace = material.Mtrl.ShaderPackage.ShaderKeys + .Any(key => key.Category == categoryHairType && key.Value == valueFace); + + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + + mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + + var baseColor = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + var color = Vector4.Lerp(_defaultHairColor, _defaultHighlightColor, maskSpan[x].A / 255f); + baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(isFace? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); + } + + private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + + // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. + private static MaterialBuilder BuildIris(Material material, string name) + { + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + + mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + + var baseColor = new Image(normal.Width, normal.Height); + normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < normalSpan.Length; x++) + { + baseColorSpan[x].FromVector4(_defaultEyeColor * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")); + } + private static MaterialBuilder BuildSkin(Material material, string name) { // Trust me bro. From 5e6ca8b22c384fa5b4246d09ab3371d671454df3 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 17:37:34 +1100 Subject: [PATCH 1448/2451] Improve fallback handling --- .../Import/Models/Export/MaterialExporter.cs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 98e4b3b9..0b109ddf 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -158,6 +158,14 @@ public class MaterialExporter }; } + private ref struct TableRow + { + public int Stepped; + public int Previous; + public int Next; + public float Weight; + } + private readonly struct MultiplyOperation { public static void Execute(Image target, Image multiplier) @@ -320,20 +328,21 @@ public class MaterialExporter .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } - private ref struct TableRow - { - public int Stepped; - public int Previous; - public int Next; - public float Weight; - } - private static MaterialBuilder BuildFallback(Material material, string name) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); - return BuildSharedBase(material, name) + + var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() - .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One); + .WithBaseColor(Vector4.One); + + if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) + materialBuilder.WithBaseColor(BuildImage(diffuse, name, "basecolor")); + + if (material.Textures.TryGetValue(TextureUsage.SamplerNormal, out var normal)) + materialBuilder.WithNormal(BuildImage(normal, name, "normal")); + + return materialBuilder; } private static MaterialBuilder BuildSharedBase(Material material, string name) From 9ff3227cf4105b602c40f3eec2adddf5759cb507 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 20:12:17 +1100 Subject: [PATCH 1449/2451] Cleanup pass --- .../Import/Models/Export/MaterialExporter.cs | 29 +++++++++++++------ Penumbra/Import/Models/ModelManager.cs | 12 ++++---- .../ModEditWindow.Models.MdlTab.cs | 5 ++-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0b109ddf..a189e7bc 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -21,6 +21,7 @@ public class MaterialExporter // variant? } + /// Build a glTF material from a hydrated XIV model, with the provided name. public static MaterialBuilder Export(Material material, string name) { Penumbra.Log.Debug($"Exporting material \"{name}\"."); @@ -36,16 +37,18 @@ public class MaterialExporter }; } + /// Build a material following the semantics of character.shpk. private static MaterialBuilder BuildCharacter(Material material, string name) { + // Build the textures from the color table. var table = material.Mtrl.Table; - // TODO: there's a few normal usages i should check, i think. var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); + // Check if full textures are provided, and merge in if available. Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { @@ -53,7 +56,6 @@ public class MaterialExporter baseColor = diffuse; } - // TODO: what about the two specularmaps? Image specular = operation.Specular; if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { @@ -61,6 +63,7 @@ public class MaterialExporter specular = specularTexture; } + // Pull further information from the mask. Image? occlusion = null; if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { @@ -73,7 +76,7 @@ public class MaterialExporter 0f, 0f, 0f, 0f ))); occlusion = maskTexture; - + // TODO: handle other textures stored in the mask? } @@ -89,7 +92,7 @@ public class MaterialExporter return materialBuilder; } - // TODO: It feels a little silly to request the entire normal here when extrating the normal only needs some of the components. + // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { @@ -143,7 +146,7 @@ public class MaterialExporter private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) + var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + input * 15; @@ -204,6 +207,7 @@ public class MaterialExporter private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + /// Build a material following the semantics of hair.shpk. private static MaterialBuilder BuildHair(Material material, string name) { // Trust me bro. @@ -241,11 +245,12 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithNormal(BuildImage(normal, name, "normal")) - .WithAlpha(isFace? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); + .WithAlpha(isFace ? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); } private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + /// Build a material following the semantics of iris.shpk. // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. private static MaterialBuilder BuildIris(Material material, string name) { @@ -278,6 +283,7 @@ public class MaterialExporter .WithNormal(BuildImage(normal, name, "normal")); } + /// Build a material following the semantics of skin.shpk. private static MaterialBuilder BuildSkin(Material material, string name) { // Trust me bro. @@ -310,7 +316,8 @@ public class MaterialExporter }); // Clear the blue channel out of the normal now that we're done with it. - normal.ProcessPixelRows(normalAccessor => { + normal.ProcessPixelRows(normalAccessor => + { for (int y = 0; y < normalAccessor.Height; y++) { var normalSpan = normalAccessor.GetRowSpan(y); @@ -325,14 +332,16 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(diffuse, name, "basecolor")) .WithNormal(BuildImage(normal, name, "normal")) - .WithAlpha(isFace? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); + .WithAlpha(isFace ? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } + /// Build a material from a source with unknown semantics. + /// Will make a loose effort to fetch common / simple textures. private static MaterialBuilder BuildFallback(Material material, string name) { Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); - var materialBuilder = BuildSharedBase(material, name) + var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() .WithBaseColor(Vector4.One); @@ -345,6 +354,7 @@ public class MaterialExporter return materialBuilder; } + /// Build a material pre-configured with settings common to all XIV materials/shaders. private static MaterialBuilder BuildSharedBase(Material material, string name) { // TODO: Move this and potentially the other known stuff into MtrlFile? @@ -355,6 +365,7 @@ public class MaterialExporter .WithDoubleSide(showBackfaces); } + /// Convert an ImageSharp Image into an ImageBuilder for use with SharpGLTF. private static ImageBuilder BuildImage(Image image, string materialName, string suffix) { var name = materialName.Replace("/", "").Replace(".mtrl", "") + $"_{suffix}"; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4f652436..bbc274a5 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -23,8 +23,8 @@ using LuminaMaterial = Lumina.Models.Materials.Material; public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable { - private readonly IFramework _framework = framework; - private readonly IDataManager _gameData = gameData; + private readonly IFramework _framework = framework; + private readonly IDataManager _gameData = gameData; private readonly TextureManager _textureManager = textureManager; private readonly ConcurrentDictionary _tasks = new(); @@ -163,7 +163,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Penumbra.Log.Debug("[GLTF Export] Done."); } - /// Attempt to read out the pertinent information from a .sklb. + /// Attempt to read out the pertinent information from the sklb file paths provided. private IEnumerable BuildSkeletons(CancellationToken cancel) { var havokTasks = sklbPaths @@ -185,6 +185,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect delayTicks: pair.Index, cancellationToken: cancel); } + /// Read a .mtrl and hydrate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { // TODO: this should probably be chosen in the export settings @@ -194,7 +195,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) : relativePath; - // TODO: this should be a recoverable warning - as should the one below it i think + // TODO: this should be a recoverable warning if (absolutePath == null) throw new Exception("Failed to resolve material path."); @@ -202,7 +203,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return new MaterialExporter.Material { - Mtrl = mtrl, + Mtrl = mtrl, Textures = mtrl.ShaderPackage.Samplers.ToDictionary( sampler => (TextureUsage)sampler.SamplerId, sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) @@ -210,6 +211,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } + /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { using var textureData = new MemoryStream(read(texture.Path)); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f79d161e..43a06012 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -225,15 +225,16 @@ public partial class ModEditWindow private byte[] ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(path, out var utf8SklbPath, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path, true)) throw new Exception($"Resolved path {path} could not be converted to a game path."); - var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); + var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path); // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... var bytes = resolvedPath == null ? _edit._gameData.GetFile(path)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); + // TODO: some callers may not care about failures - handle exceptions seperately? return bytes ?? throw new Exception( $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } From 0e50cc9c47ddffe5e9e6d6f9019d59402ae74209 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 22:27:23 +1100 Subject: [PATCH 1450/2451] Handle character mask R same as other shaders --- .../Import/Models/Export/MaterialExporter.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index a189e7bc..0bacb98a 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // Check if full textures are provided, and merge in if available. - Image baseColor = operation.BaseColor; + Image baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { MultiplyOperation.Execute(diffuse, operation.BaseColor); @@ -64,32 +64,31 @@ public class MaterialExporter } // Pull further information from the mask. - Image? occlusion = null; if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { - // Extract the red channel for ambient occlusion. - maskTexture.Mutate(context => context.Filter(new ColorMatrix( - 1f, 1f, 1f, 0f, - 0f, 0f, 0f, 0f, - 0f, 0f, 0f, 0f, - 0f, 0f, 0f, 1f, - 0f, 0f, 0f, 0f - ))); - occlusion = maskTexture; + // Extract the red channel for "ambient occlusion". + maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); + maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => + { + for (int y = 0; y < maskAccessor.Height; y++) + { + var maskSpan = maskAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (int x = 0; x < maskSpan.Length; x++) + baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); + } + }); + // TODO: handle other textures stored in the mask? } - var materialBuilder = BuildSharedBase(material, name) + return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithNormal(BuildImage(operation.Normal, name, "normal")) .WithSpecularColor(BuildImage(specular, name, "specular")) .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); - - if (occlusion != null) - materialBuilder.WithOcclusion(BuildImage(occlusion, name, "occlusion")); - - return materialBuilder; } // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. From ec92f93d229389d901cd57107a8fbd967007e24c Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 14 Jan 2024 22:36:33 +1100 Subject: [PATCH 1451/2451] Improve material and texture path resolution --- Penumbra/Import/Models/ModelManager.cs | 63 ++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bbc274a5..c6e2d836 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -108,6 +108,40 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; } + /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. + private string ResolveMtrlPath(string rawPath) + { + // TODO: this should probably be chosen in the export settings + var variantId = 1; + + // Get standardised paths + var absolutePath = rawPath.StartsWith('/') + ? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId) + : rawPath; + var relativePath = rawPath.StartsWith('/') + ? rawPath + : '/' + Path.GetFileName(rawPath); + + // TODO: this should be a recoverable warning + if (absolutePath == null) + throw new Exception("Failed to resolve material path."); + + var info = parser.GetFileInfo(absolutePath); + if (info.FileType is not FileType.Material) + throw new Exception($"Material path {rawPath} does not conform to material conventions."); + + var resolvedPath = info.ObjectType switch + { + ObjectType.Character => GamePaths.Character.Mtrl.Path( + info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), + _ => absolutePath, + }; + + Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}"); + + return resolvedPath; + } + private Task Enqueue(IAction action) { if (_disposed) @@ -188,18 +222,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Read a .mtrl and hydrate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { - // TODO: this should probably be chosen in the export settings - var variantId = 1; - - var absolutePath = relativePath.StartsWith("/") - ? LuminaMaterial.ResolveRelativeMaterialPath(relativePath, variantId) - : relativePath; - - // TODO: this should be a recoverable warning - if (absolutePath == null) - throw new Exception("Failed to resolve material path."); - - var mtrl = new MtrlFile(read(absolutePath)); + var path = manager.ResolveMtrlPath(relativePath); + var mtrl = new MtrlFile(read(path)); return new MaterialExporter.Material { @@ -214,7 +238,20 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { - using var textureData = new MemoryStream(read(texture.Path)); + // Work out the texture's path - the DX11 material flag controls a file name prefix. + var texturePath = texture.Path; + if (texture.DX11) + { + var lastSlashIndex = texturePath.LastIndexOf('/'); + var directory = lastSlashIndex == -1 ? texturePath : texturePath.Substring(0, lastSlashIndex); + var fileName = Path.GetFileName(texturePath); + if (!fileName.StartsWith("--")) + { + texturePath = $"{directory}/--{fileName}"; + } + } + + using var textureData = new MemoryStream(read(texturePath)); var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; if (pngImage == null) From 65fbf13afe7b957ec12b9dc9f3cbc2fd6eb4d388 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jan 2024 13:16:30 +0100 Subject: [PATCH 1452/2451] Parsing... --- Penumbra.GameData | 2 +- Penumbra/Penumbra.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1dad8d07..96e95378 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98 +Subproject commit 96e95378325ff1533ca41b934fcb712f24d5260b diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5a03dc04..706e4a01 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -79,7 +79,6 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); _services.GetService(); // Initialize before Interface. From 3b9d841014029a38a67c83caff89ddc89cad8be7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jan 2024 13:35:46 +0100 Subject: [PATCH 1453/2451] Minor cleanup. --- .../Import/Models/Export/MaterialExporter.cs | 137 +++++++++--------- Penumbra/Import/Models/ModelManager.cs | 67 +++++---- .../ModEditWindow.Models.MdlTab.cs | 12 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 17 +-- 4 files changed, 115 insertions(+), 118 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0bacb98a..2a49e77f 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -17,6 +17,7 @@ public class MaterialExporter public struct Material { public MtrlFile Mtrl; + public Dictionary> Textures; // variant? } @@ -49,7 +50,7 @@ public class MaterialExporter ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); // Check if full textures are provided, and merge in if available. - Image baseColor = operation.BaseColor; + var baseColor = operation.BaseColor; if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { MultiplyOperation.Execute(diffuse, operation.BaseColor); @@ -70,24 +71,22 @@ public class MaterialExporter maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => { - for (int y = 0; y < maskAccessor.Height; y++) + for (var y = 0; y < maskAccessor.Height; y++) { - var maskSpan = maskAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < maskSpan.Length; x++) + for (var x = 0; x < maskSpan.Length; x++) baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); } }); - - // TODO: handle other textures stored in the mask? } return BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithSpecularColor(BuildImage(specular, name, "specular")) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(operation.Normal, name, "normal")) + .WithSpecularColor(BuildImage(specular, name, "specular")) .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); } @@ -95,31 +94,38 @@ public class MaterialExporter // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation { - public Image Normal { get; private init; } = normal.Clone(); - public Image BaseColor { get; private init; } = new Image(normal.Width, normal.Height); - public Image Specular { get; private init; } = new Image(normal.Width, normal.Height); - public Image Emissive { get; private init; } = new Image(normal.Width, normal.Height); + public Image Normal { get; } = normal.Clone(); + public Image BaseColor { get; } = new(normal.Width, normal.Height); + public Image Specular { get; } = new(normal.Width, normal.Height); + public Image Emissive { get; } = new(normal.Width, normal.Height); - private Buffer2D NormalBuffer => Normal.Frames.RootFrame.PixelBuffer; - private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; - private Buffer2D SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer; - private Buffer2D EmissiveBuffer => Emissive.Frames.RootFrame.PixelBuffer; + private Buffer2D NormalBuffer + => Normal.Frames.RootFrame.PixelBuffer; + + private Buffer2D BaseColorBuffer + => BaseColor.Frames.RootFrame.PixelBuffer; + + private Buffer2D SpecularBuffer + => Specular.Frames.RootFrame.PixelBuffer; + + private Buffer2D EmissiveBuffer + => Emissive.Frames.RootFrame.PixelBuffer; public void Invoke(int y) { - var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); - var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); - var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); + var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; // Table row data (.a) var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = table[tableRow.Previous]; - var nextRow = table[tableRow.Next]; + var prevRow = table[tableRow.Previous]; + var nextRow = table[tableRow.Next]; // Base colour (table, .b) var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); @@ -145,9 +151,9 @@ public class MaterialExporter private static TableRow GetTableRowIndices(float input) { // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(((input * 7.5f) % 1.0f) * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) + * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) + + input * 15; var stepped = MathF.Floor(smoothed + 0.5f); @@ -162,9 +168,9 @@ public class MaterialExporter private ref struct TableRow { - public int Stepped; - public int Previous; - public int Next; + public int Stepped; + public int Previous; + public int Next; public float Weight; } @@ -189,50 +195,47 @@ public class MaterialExporter where TPixel1 : unmanaged, IPixel where TPixel2 : unmanaged, IPixel { - public void Invoke(int y) { - var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var targetSpan = target.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); var multiplierSpan = multiplier.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); - for (int x = 0; x < targetSpan.Length; x++) - { + for (var x = 0; x < targetSpan.Length; x++) targetSpan[x].FromVector4(targetSpan[x].ToVector4() * multiplierSpan[x].ToVector4()); - } } } - // TODO: These are hardcoded colours - I'm not keen on supporting highly customiseable exports, but there's possibly some more sensible values to use here. - private static Vector4 _defaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); - private static Vector4 _defaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); + // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here. + private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); + private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); /// Build a material following the semantics of hair.shpk. private static MaterialBuilder BuildHair(Material material, string name) { // Trust me bro. const uint categoryHairType = 0x24826489; - const uint valueFace = 0x6E5B8F10; + const uint valueFace = 0x6E5B8F10; var isFace = material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key.Category == categoryHairType && key.Value == valueFace); + .Any(key => key is { Category: categoryHairType, Value: valueFace }); var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var mask = material.Textures[TextureUsage.SamplerMask]; mask.Mutate(context => context.Resize(normal.Width, normal.Height)); var baseColor = new Image(normal.Width, normal.Height); normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { - var color = Vector4.Lerp(_defaultHairColor, _defaultHighlightColor, maskSpan[x].A / 255f); + var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f); baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].A = normalSpan[x].A; @@ -243,33 +246,33 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")) + .WithNormal(BuildImage(normal, name, "normal")) .WithAlpha(isFace ? AlphaMode.BLEND : AlphaMode.MASK, 0.5f); } - private static Vector4 _defaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); + private static readonly Vector4 DefaultEyeColor = new Vector4(21, 176, 172, 255) / new Vector4(255); /// Build a material following the semantics of iris.shpk. - // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping seperate for now. + // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now. private static MaterialBuilder BuildIris(Material material, string name) { var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var mask = material.Textures[TextureUsage.SamplerMask]; mask.Mutate(context => context.Resize(normal.Width, normal.Height)); var baseColor = new Image(normal.Width, normal.Height); normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < normalSpan.Length; x++) { - baseColorSpan[x].FromVector4(_defaultEyeColor * new Vector4(maskSpan[x].R / 255f)); + baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f)); baseColorSpan[x].A = normalSpan[x].A; normalSpan[x].A = byte.MaxValue; @@ -279,7 +282,7 @@ public class MaterialExporter return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")); + .WithNormal(BuildImage(normal, name, "normal")); } /// Build a material following the semantics of skin.shpk. @@ -287,7 +290,7 @@ public class MaterialExporter { // Trust me bro. const uint categorySkinType = 0x380CAED0; - const uint valueFace = 0xF5673524; + const uint valueFace = 0xF5673524; // Face is the default for the skin shader, so a lack of skin type category is also correct. var isFace = !material.Mtrl.ShaderPackage.ShaderKeys @@ -296,41 +299,37 @@ public class MaterialExporter // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. // TODO: Specular? var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; - var normal = material.Textures[TextureUsage.SamplerNormal]; + var normal = material.Textures[TextureUsage.SamplerNormal]; // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => { - for (int y = 0; y < diffuseAccessor.Height; y++) + for (var y = 0; y < diffuseAccessor.Height; y++) { var diffuseSpan = diffuseAccessor.GetRowSpan(y); - var normalSpan = normalAccessor.GetRowSpan(y); + var normalSpan = normalAccessor.GetRowSpan(y); - for (int x = 0; x < diffuseSpan.Length; x++) - { + for (var x = 0; x < diffuseSpan.Length; x++) diffuseSpan[x].A = normalSpan[x].B; - } } }); // Clear the blue channel out of the normal now that we're done with it. normal.ProcessPixelRows(normalAccessor => { - for (int y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < normalAccessor.Height; y++) { var normalSpan = normalAccessor.GetRowSpan(y); - for (int x = 0; x < normalSpan.Length; x++) - { + for (var x = 0; x < normalSpan.Length; x++) normalSpan[x].B = byte.MaxValue; - } } }); return BuildSharedBase(material, name) .WithBaseColor(BuildImage(diffuse, name, "basecolor")) - .WithNormal(BuildImage(normal, name, "normal")) + .WithNormal(BuildImage(normal, name, "normal")) .WithAlpha(isFace ? AlphaMode.MASK : AlphaMode.OPAQUE, 0.5f); } @@ -357,8 +356,8 @@ public class MaterialExporter private static MaterialBuilder BuildSharedBase(Material material, string name) { // TODO: Move this and potentially the other known stuff into MtrlFile? - const uint backfaceMask = 0x1; - var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; + const uint backfaceMask = 0x1; + var showBackfaces = (material.Mtrl.ShaderPackage.Flags & backfaceMask) == 0; return new MaterialBuilder(name) .WithDoubleSide(showBackfaces); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index c6e2d836..8c6dc31a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -21,11 +21,9 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, IDataManager gameData, GamePathParser parser, TextureManager textureManager) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable { - private readonly IFramework _framework = framework; - private readonly IDataManager _gameData = gameData; - private readonly TextureManager _textureManager = textureManager; + private readonly IFramework _framework = framework; private readonly ConcurrentDictionary _tasks = new(); @@ -45,13 +43,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect public Task ImportGltf(string inputPath) { var action = new ImportGltfAction(inputPath); - return Enqueue(action).ContinueWith(task => + return Enqueue(action).ContinueWith(task => { - if (task.IsFaulted && task.Exception != null) + if (task is { IsFaulted: true, Exception: not null }) throw task.Exception; + return action.Out; }); } + /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. /// Modified extra skeleton template parameters. @@ -69,8 +69,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], - ObjectType.Equipment => [baseSkeleton], - ObjectType.Accessory => [baseSkeleton], + ObjectType.Equipment => [baseSkeleton], + ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], @@ -89,17 +89,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations - .FirstOrNull(est => + .FirstOrNull(est => est.Gender == gender - && est.Race == race - && est.Slot == type - && est.SetId == info.PrimaryId + && est.Race == race + && est.Slot == type + && est.SetId == info.PrimaryId ); - + // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry - ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? 0; + ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) + ?? 0; // If there's no entries, we can assume that there's no additional skeleton. if (targetId == 0) @@ -121,11 +121,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var relativePath = rawPath.StartsWith('/') ? rawPath : '/' + Path.GetFileName(rawPath); - + // TODO: this should be a recoverable warning if (absolutePath == null) throw new Exception("Failed to resolve material path."); - + var info = parser.GetFileInfo(absolutePath); if (info.FileType is not FileType.Material) throw new Exception($"Material path {rawPath} does not conform to material conventions."); @@ -168,7 +168,12 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + private class ExportToGltfAction( + ModelManager manager, + MdlFile mdl, + IEnumerable sklbPaths, + Func read, + string outputPath) : IAction { public void Execute(CancellationToken cancel) @@ -213,21 +218,21 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // finicky at the best of times, and can outright cause a CTD if they // get upset. Running each conversion on its own tick seems to make // this consistently non-crashy across my testing. - Task CreateHavokTask((SklbFile Sklb, int Index) pair) => - manager._framework.RunOnTick( + Task CreateHavokTask((SklbFile Sklb, int Index) pair) + => manager._framework.RunOnTick( () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), delayTicks: pair.Index, cancellationToken: cancel); } - /// Read a .mtrl and hydrate its textures. + /// Read a .mtrl and populate its textures. private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) { var path = manager.ResolveMtrlPath(relativePath); var mtrl = new MtrlFile(read(path)); - + return new MaterialExporter.Material { - Mtrl = mtrl, + Mtrl = mtrl, Textures = mtrl.ShaderPackage.Samplers.ToDictionary( sampler => (TextureUsage)sampler.SamplerId, sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) @@ -242,21 +247,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var texturePath = texture.Path; if (texture.DX11) { - var lastSlashIndex = texturePath.LastIndexOf('/'); - var directory = lastSlashIndex == -1 ? texturePath : texturePath.Substring(0, lastSlashIndex); - var fileName = Path.GetFileName(texturePath); + var fileName = Path.GetFileName(texturePath); if (!fileName.StartsWith("--")) - { - texturePath = $"{directory}/--{fileName}"; - } + texturePath = $"{Path.GetDirectoryName(texturePath)}/--{fileName}"; } using var textureData = new MemoryStream(read(texturePath)); - var image = TexFileParser.Parse(textureData); - var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; - if (pngImage == null) - throw new Exception("Failed to convert texture to png."); - return pngImage; + var image = TexFileParser.Parse(textureData); + var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; + return pngImage ?? throw new Exception("Failed to convert texture to png."); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 43a06012..9cfe0739 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -181,9 +181,9 @@ public partial class ModEditWindow } /// Merge attribute configuration from the source onto the target. - /// + /// Model that will be updated. > /// Model to copy attribute configuration from. - public void MergeAttributes(MdlFile target, MdlFile source) + public static void MergeAttributes(MdlFile target, MdlFile source) { target.Attributes = source.Attributes; @@ -197,7 +197,7 @@ public partial class ModEditWindow target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u; // Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt - // to maintain semantic connection betwen mesh index and submesh attributes. + // to maintain semantic connection between mesh index and sub mesh attributes. if (meshIndex >= source.Meshes.Length) continue; var sourceMesh = source.Meshes[meshIndex]; @@ -214,8 +214,8 @@ public partial class ModEditWindow { IoExceptions = exception switch { null => [], - AggregateException ae => ae.Flatten().InnerExceptions.ToList(), - Exception other => [other], + AggregateException ae => [.. ae.Flatten().InnerExceptions], + _ => [exception], }; } @@ -234,7 +234,7 @@ public partial class ModEditWindow ? _edit._gameData.GetFile(path)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath()); - // TODO: some callers may not care about failures - handle exceptions seperately? + // TODO: some callers may not care about failures - handle exceptions separately? return bytes ?? throw new Exception( $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 4ac789ad..ad609285 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -129,7 +129,7 @@ public partial class ModEditWindow ); } - private void DrawIoExceptions(MdlTab tab) + private static void DrawIoExceptions(MdlTab tab) { if (tab.IoExceptions.Count == 0) return; @@ -140,16 +140,15 @@ public partial class ModEditWindow var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; foreach (var (exception, index) in tab.IoExceptions.WithIndex()) { - var message = $"{exception.GetType().Name}: {exception.Message}"; - var textSize = ImGui.CalcTextSize(message).X; + using var id = ImRaii.PushId(index); + var message = $"{exception.GetType().Name}: {exception.Message}"; + var textSize = ImGui.CalcTextSize(message).X; if (textSize > spaceAvail) - message = message.Substring(0, (int)Math.Floor(message.Length * (spaceAvail / textSize))) + "..."; + message = message[..(int)Math.Floor(message.Length * (spaceAvail / textSize))] + "..."; - using (var exceptionNode = ImRaii.TreeNode($"{message}###exception{index}")) - { - if (exceptionNode) - ImGuiUtil.TextWrapped(exception.ToString()); - } + using var exceptionNode = ImRaii.TreeNode(message); + if (exceptionNode) + ImGuiUtil.TextWrapped(exception.ToString()); } } From 574a129772a31b02594e4a21c830ddcd92ec27f6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 14 Jan 2024 12:37:57 +0000 Subject: [PATCH 1454/2451] [CI] Updating repo.json for testing_0.8.3.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a9c1829a..bcf1f82b 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.5", + "TestingAssemblyVersion": "0.8.3.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 965f8efd80cb6d9e66f567935f1edfbf657c1711 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jan 2024 14:15:07 +0100 Subject: [PATCH 1455/2451] Add function that handles prepending DX11 dashes. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 9 +-------- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 16 ++-------------- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 18 +++--------------- 4 files changed, 7 insertions(+), 38 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 96e95378..13545143 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 96e95378325ff1533ca41b934fcb712f24d5260b +Subproject commit 135451430344f2f12e8c02fd4c4c6f0875d74e60 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 8c6dc31a..7f1171f3 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -244,14 +244,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) { // Work out the texture's path - the DX11 material flag controls a file name prefix. - var texturePath = texture.Path; - if (texture.DX11) - { - var fileName = Path.GetFileName(texturePath); - if (!fileName.StartsWith("--")) - texturePath = $"{Path.GetDirectoryName(texturePath)}/--{fileName}"; - } - + GamePaths.Tex.HandleDx11Path(texture, out var texturePath); using var textureData = new MemoryStream(read(texturePath)); var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index fc32df0c..78c49b59 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -79,21 +79,9 @@ public static class CustomizationSwap } public static FileSwap CreateTex(MetaFileManager manager, Func redirections, BodySlot slot, GenderRace race, - PrimaryId idFrom, ref MtrlFile.Texture texture, - ref bool dataWasChanged) + PrimaryId idFrom, ref MtrlFile.Texture texture, ref bool dataWasChanged) { - var path = texture.Path; - var addedDashes = false; - if (texture.DX11) - { - var fileName = Path.GetFileName(path); - if (!fileName.StartsWith("--")) - { - path = path.Replace(fileName, $"--{fileName}"); - addedDashes = true; - } - } - + var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); var newPath = ItemSwap.ReplaceAnyRace(path, race); newPath = ItemSwap.ReplaceAnyBody(newPath, slot, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}", true); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 516df251..5a5181a5 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -402,22 +402,10 @@ public static class EquipmentSwap => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, - EquipSlot slotTo, PrimaryId idFrom, - PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) + EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { - var path = texture.Path; - var addedDashes = false; - if (texture.DX11) - { - var fileName = Path.GetFileName(path); - if (!fileName.StartsWith("--")) - { - path = path.Replace(fileName, $"--{fileName}"); - addedDashes = true; - } - } - - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) From f147e66953f63c1afd32922bf2dfad1aee5bea7c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 14 Jan 2024 13:17:42 +0000 Subject: [PATCH 1456/2451] [CI] Updating repo.json for testing_0.8.3.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index bcf1f82b..4dfac5a0 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.6", + "TestingAssemblyVersion": "0.8.3.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 70e72f57901fc47e11f916d7c40d799f869da018 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jan 2024 19:28:31 +0100 Subject: [PATCH 1457/2451] Update OtterGui and maybe handle dependency issues. --- Penumbra/Import/Textures/TextureDrawer.cs | 10 ++++++++++ Penumbra/lib/DirectXTexC.dll | Bin 806400 -> 983552 bytes Penumbra/lib/OtterTex.dll | Bin 32256 -> 41984 bytes 3 files changed, 10 insertions(+) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 6d68efbd..04422116 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -27,7 +27,17 @@ public static class TextureDrawer } else if (texture.LoadError != null) { + const string link = "https://aka.ms/vcredist"; ImGui.TextUnformatted("Could not load file:"); + + if (texture.LoadError is DllNotFoundException) + { + ImGuiUtil.TextColored(Colors.RegexWarningBorder, "A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."); + if (ImGui.Button("Microsoft VC Redistributables")) + Dalamud.Utility.Util.OpenLink(link); + ImGuiUtil.HoverTooltip($"Open {link} in your browser."); + } + ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString()); } } diff --git a/Penumbra/lib/DirectXTexC.dll b/Penumbra/lib/DirectXTexC.dll index 5e0aa0c43150ac1a01cfb8e0e016517559f6328a..2cab1dcea5a148712aa14312788227fd61b8800d 100644 GIT binary patch delta 364593 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%0D0D-3zbM(Yjab2m}SIf&(9)q=9r5Fk4N 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℞?}PSs#N*{X9O;~fS)mo58~ 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 BY|a1x delta 189779 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 diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index d4819ff2a44a62390ff6e861cf98b902676fd91e..29912e6215ff28a7959b73aa1e00cc9ebb919d76 100644 GIT binary patch literal 41984 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

6!_KBfJth4basr$B2eOH&>0GlJ>So@a7 zaQ_y#(<1wrXX~d&J0!%Ua0`P49JC_Rvl(QUeql*H-xQHr(JB2d3H5H;N{txUD$k+S z=U{`nV9h|Z{T%Fames8c$Il^a2_sy6w0}z^CM1u{a8G{}ornwXkhk_lV(=MHI*pJnyqBWoawbatV=vO;Za^8nA6s|Wg5cQ)~o+d3EEW#959tisYHNoH-& zIX%7Gdw3(qXSb=IL06IUT=aEp=vVgk^b&9B#Ut2UVrz!O5QAIS+l3r3Zxn?adAN%O zX>*1-w6UA@J6m!LLEX_l9@J&uv$zX07d(k3iUx~jTU)*~x?`YEKP*~-MP@jLnl%W0 z<8Cl1BZAZ`MsrrimRb&HGfLN|3-z5d)#{bPRclbaTuG+8dG%M{+g-xt<259$?e9d7 zO`9oLTR+FyAr*OUWZ zakD#Mqus-8G@e>5W@o_Ox`$gT$HPH{EOQA*2xf<{&XTQzYF!0Ky_gu|5uL+{X$pr^ zzZ5RHUD9=>g*3_uyb&8cRS(kplc}aZq@Z)1e4I`7vGDeWo(`ESG490CfZAid@!rmU zbPa5sutM7zkH(45(Yy32!}@UrHgMp0@t5TIE`t%!rH4yD;Tb$m`b-|DI-F>(Z|P|X zMk-`i~Dx0LunYK1*0Mo1$j+xe?IkoEb<&~)dy{n~3F$-H7SV&lO>inA_Ol2(^ z%Q}lTr(jE9&cc?Usl0Mob&|tGo5EP69&LR}Sh>DrND=Y<4l6=(j!x|xY16Xwq8L_> z>9xrHN2K-ElOyR0$tOz4+HL8ZWv=7}*a=lM4{SoCOc`2aX}P+yQ>6OTAoE{Sc>B6& zWV_x5^-~R6jj@>@%`w=tCdQ{|DUa=7!Bw`btPB&H)Mr||%8=-6eMNby+@cL4W!0D} zv1px0`c~&tri?TPw|4S)POCR<#fpOZ+ZY4YGBMF2a@%yAo`C zqW#3%XWqwQ;Fg;{IQWj^-Vb$l_otP(852QLkiwayeoZN4iohiGtE>lWt1ZSL^h-+T zU7MC<3TKk~HQ8B(U!Uyt(lJG7R!uW^N+Viw5uYM3N&RZ%)MA84>Fik4N*?t~z0qx& zbrwymJtE=803Buz8cT^tha5maq6f-SgZJVqdN=bvY*lYRx{$0=(>qAJA>_3#7LV4J z$DFn>&_{NpojRIxmORX3taeU|?2L4*#0GZ<)%EU3Ij^tfeLl70m_^^`n>R@p>WgG) zSmvn+g^gP##*WN5aY@f_`eCmA)LL5hcmk1Q@8rGm(JUk%U^=>ml<>5UoyL5!nY zNC_XsLh`|$qgzM`=aO0HxB7nFypO^)Ez>Q|jhJreiSXphoUt}DXGolh_CZQxR^jR9 zAT6O7{`KNpLNu+OqM#!96=FZ0m~H{J1HV)(!P^MB_%GYdYru2S=i+&5Y-}M50=vO& zhF%cYcKpMM4F16uKF?l#Z*^^jd-0x3BDg7^(>P${X8exU#NxyP(17I_dA)+E${~IQp7RZ7@N(7Hvpg@D_l~7zzT;;o8opm zUC4W&%jMDn9ZPZ@?jUg?@ce#|5T@m|1(w@ADzA;BcI3iCtQP3V33PF^u`Z{SJzeTg zsmJN;AUQbL4G(9F8@miEh;C=&MkHiSSU1dYUOQ0NT3*<} z8FwIyfey)HphGW8ha@MNbmiiKc=(m`!x=H`yAPex`X7PkJtw%OtpVKTv(avTW|=>3 zyM86NspQ5(_wsSeUq@se^O_U>EWyS)8)hEj$y27a%*yw2TT z$y5~szd_p0FKf0}Gc07__ZQnwWZ*yVV&}i=Vn3OI|AvdbmZ6SeF+)AW5{9J=%NUk3 zG%%dXa2mr3hLsGf7*;bhGOS@}VrXV)VOYzsj^T8M^$Z&r&R{r`VI#v?3}-X6GPE&- z88$JrGjuRS7&;j?Gi+gqGHhiyhXLa$*}EBf7?{5i2Nz>;i^mjB>WK_svD{T0 zY#i8xl`s}WE%;qS&q|z>7j%}DbySsxE5c({io^lHfNv!-^rRY3zs0iMfisBEylMJ-)ejp;ah`mJ-*9r&KyM6e9n}K2l5tt*Wr$>?vV6n4$xi?~vYdAf^;osZu<9I(VAA zj3)@p3BbwtF&9Xgy$jH+^{EFle_**k(BdImU^%9xK#SkYBuW-&$-$olBR_^?OtpX% zpX89OfEN=m@+B2W+#<;oX9ew8Ot@hN?+gg60GVZ^6Ifvn5~f&vN)2?_f~3JQ3R4>< z6AhO@FFh?G z^B22odmN~{Y-08doAgUACDK0_EN_64f5SSpk&hz^FxXv=wR9yD=gppH%?S7_=Y zpHn10E44*>YKxMF12qU0!$KLkD%SOiQ}pUBy?UR~l-MnmC}b|zoU|P{rkZ_X2{7bT z`yj#JE;dvG)o5gBj3)2p;1q+4GUL}|C={nF)cXP~;a-dtFuDNN!$vO#q4#1Ina#fi zOU@K_eLA~QXN}%qhM{V5TZ}!$M=h7GnyvRhFg%D!YeLe@>Ky?LhEl6F znXK8GM0bVVXieY>ykMj?3By*T%dy2uh>VH=%M}5EQ4t)@6#(jtS&V1XYy0|&&2`neSXjY;Ki_%+%6LGK9|Ur9j36}d zW817|+;5Qk^Muz1k_*rO@J~STTXDPw(|W8Y_`^Gb<)!7=jj&FczGx%-HE=2sX5NC1 zaA|2}MR|LB+5Gm%f-rP9b#!)=Rh2JTP`$8vQ&n~6{PInm*t%q+i01PEMEDuC=X9Ka z^=^+NKZLgxQcmX619%xBg_8G1j5}W?X%7+^_b7T}9$rx{EGb)vpVbnUW^fY_OSHrf zMw*6))+Vk^Uz?72oF)CnB<`SPu4Xf1{4cC-10xdU>W1%mEv{iBPc%!>9&CU#w32N7 zwzB@S;0Od+4L!Kuiu@+7sVswxcA0hix|Qm|wZkXvS7RNngjNjHLd5c2o~2*;wMtBayBm9f^06 zBjl{N9N+argm)e?r++GKd}Of>?`CTSF2Q>xn(@o{)wBwH18^y5{ybv;^T+xLlI*4+ z(XV`R!5?Y&I%#1&C68QT*^M$DLLJY_v!XSXQV7hcTkYZk&ANT zlPjV+$-`ZyLtjLYd>EN=%%kCW-Kq`&6T z0$2_s-=%ola0PPDXYge-Uryo|NC=k*zjr~6c7Upa&I0_a#yg>_A+G{K&FB9CxT`hm literal 32256 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 Date: Sun, 14 Jan 2024 19:28:42 +0100 Subject: [PATCH 1458/2451] Some further cleanup. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 1 + Penumbra/Collections/Cache/CollectionCache.cs | 1 + .../Collections/Cache/CollectionModData.cs | 1 + Penumbra/Collections/Cache/MetaCache.cs | 1 + .../Collections/ModCollection.Cache.Access.cs | 1 + Penumbra/Communication/ResolvedFileChanged.cs | 1 + Penumbra/Interop/Hooks/DebugHook.cs | 9 ++- Penumbra/Mods/Editor/DuplicateManager.cs | 1 - Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 14 ++--- Penumbra/Mods/Editor/ModEditor.cs | 41 ++++++------- Penumbra/Mods/Editor/ModFileCollection.cs | 21 +++---- Penumbra/Mods/Editor/ModFileEditor.cs | 60 ++++++++----------- Penumbra/Mods/Editor/ModMetaEditor.cs | 24 +++----- Penumbra/Mods/Editor/ModNormalizer.cs | 6 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 10 +--- Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/Subclasses/SubMod.cs | 1 + Penumbra/Mods/TemporaryMod.cs | 1 + Penumbra/Services/ServiceManagerA.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 1 + Penumbra/UI/Tabs/ChangedItemsTab.cs | 1 + Penumbra/UI/Tabs/EffectiveTab.cs | 1 + 24 files changed, 90 insertions(+), 112 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 498c25e3..0374e31a 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -3,6 +3,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index a6e5fef5..9a0e525b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -4,6 +4,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Api.Enums; using Penumbra.Communication; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index fcbccc96..3a3afad2 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -1,5 +1,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index b22aaa6e..4c147c3c 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -5,6 +5,7 @@ using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 5d1d10e2..7c29676d 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -7,6 +7,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Collections.Cache; using Penumbra.Interop.Services; +using Penumbra.Mods.Editor; namespace Penumbra.Collections; diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 3211a26a..75444340 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Communication; diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs index 67823e94..db14805c 100644 --- a/Penumbra/Interop/Hooks/DebugHook.cs +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -1,5 +1,4 @@ using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; namespace Penumbra.Interop.Hooks; @@ -32,12 +31,12 @@ public sealed unsafe class DebugHook : IHookService public bool Finished => _task?.IsCompletedSuccessfully ?? true; - private delegate nint Delegate(ResourceHandle* resourceHandle); + private delegate void Delegate(nint a, int b, nint c, float* d); - private nint Detour(ResourceHandle* resourceHandle) + private void Detour(nint a, int b, nint c, float* d) { - Penumbra.Log.Information($"[Debug Hook] Triggered with 0x{(nint)resourceHandle:X}."); - return _task!.Result.Original(resourceHandle); + _task!.Result.Original(a, b, c, d); + Penumbra.Log.Information($"[Debug Hook] Results with 0x{a:X} {b} {c:X} {d[0]} {d[1]} {d[2]} {d[3]}."); } } #endif diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 4773d840..dad05102 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,4 +1,3 @@ -using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index cd8a9594..78250341 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,7 +1,7 @@ using OtterGui.Classes; using Penumbra.Mods.Subclasses; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public interface IMod { diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 8881ac4b..738e606e 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -2,25 +2,19 @@ using OtterGui; using OtterGui.Compression; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; -using Penumbra.Mods.Editor; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; -public partial class MdlMaterialEditor +public partial class MdlMaterialEditor(ModFileCollection files) { [GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] private static partial Regex MaterialRegex(); - private readonly ModFileCollection _files; - - private readonly List _modelFiles = new(); + private readonly List _modelFiles = []; public IReadOnlyList ModelFiles => _modelFiles; - public MdlMaterialEditor(ModFileCollection files) - => _files = files; - public void SaveAllModels(FileCompactor compactor) { foreach (var info in _modelFiles) @@ -73,7 +67,7 @@ public partial class MdlMaterialEditor public void ScanModels(Mod mod) { _modelFiles.Clear(); - foreach (var file in _files.Mdl) + foreach (var file in files.Mdl) { try { diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 2f39970d..b22aea17 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -4,16 +4,25 @@ using Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Editor; -public class ModEditor : IDisposable +public class ModEditor( + ModNormalizer modNormalizer, + ModMetaEditor metaEditor, + ModFileCollection files, + ModFileEditor fileEditor, + DuplicateManager duplicates, + ModSwapEditor swapEditor, + MdlMaterialEditor mdlMaterialEditor, + FileCompactor compactor) + : IDisposable { - public readonly ModNormalizer ModNormalizer; - public readonly ModMetaEditor MetaEditor; - public readonly ModFileEditor FileEditor; - public readonly DuplicateManager Duplicates; - public readonly ModFileCollection Files; - public readonly ModSwapEditor SwapEditor; - public readonly MdlMaterialEditor MdlMaterialEditor; - public readonly FileCompactor Compactor; + public readonly ModNormalizer ModNormalizer = modNormalizer; + public readonly ModMetaEditor MetaEditor = metaEditor; + public readonly ModFileEditor FileEditor = fileEditor; + public readonly DuplicateManager Duplicates = duplicates; + public readonly ModFileCollection Files = files; + public readonly ModSwapEditor SwapEditor = swapEditor; + public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; + public readonly FileCompactor Compactor = compactor; public Mod? Mod { get; private set; } public int GroupIdx { get; private set; } @@ -22,20 +31,6 @@ public class ModEditor : IDisposable public IModGroup? Group { get; private set; } public ISubMod? Option { get; private set; } - public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files, - ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor, - FileCompactor compactor) - { - ModNormalizer = modNormalizer; - MetaEditor = metaEditor; - Files = files; - FileEditor = fileEditor; - Duplicates = duplicates; - SwapEditor = swapEditor; - MdlMaterialEditor = mdlMaterialEditor; - Compactor = compactor; - } - public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index e3862c90..2f8bdfb1 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -6,20 +6,20 @@ namespace Penumbra.Mods.Editor; public class ModFileCollection : IDisposable { - private readonly List _available = new(); - private readonly List _mtrl = new(); - private readonly List _mdl = new(); - private readonly List _tex = new(); - private readonly List _shpk = new(); + private readonly List _available = []; + private readonly List _mtrl = []; + private readonly List _mdl = []; + private readonly List _tex = []; + private readonly List _shpk = []; - private readonly SortedSet _missing = new(); - private readonly HashSet _usedPaths = new(); + private readonly SortedSet _missing = []; + private readonly HashSet _usedPaths = []; public IReadOnlySet Missing - => Ready ? _missing : new HashSet(); + => Ready ? _missing : []; public IReadOnlySet UsedPaths - => Ready ? _usedPaths : new HashSet(); + => Ready ? _usedPaths : []; public IReadOnlyList Available => Ready ? _available : Array.Empty(); @@ -38,9 +38,6 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public ModFileCollection() - { } - public void UpdateAll(Mod mod, ISubMod option) { UpdateFiles(mod, new CancellationToken()); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 82629971..5328b8fe 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,23 +1,13 @@ -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; -public class ModFileEditor +public class ModFileEditor(ModFileCollection files, ModManager modManager) { - private readonly ModFileCollection _files; - private readonly ModManager _modManager; - public bool Changes { get; private set; } - public ModFileEditor(ModFileCollection files, ModManager modManager) - { - _files = files; - _modManager = modManager; - } - public void Clear() { Changes = false; @@ -27,21 +17,21 @@ public class ModFileEditor { var dict = new Dictionary(); var num = 0; - foreach (var file in _files.Available) + foreach (var file in files.Available) { foreach (var path in file.SubModUsage.Where(p => p.Item1 == option)) num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - _modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); - _files.UpdatePaths(mod, option); + modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + files.UpdatePaths(mod, option); Changes = false; return num; } public void Revert(Mod mod, ISubMod option) { - _files.UpdateAll(mod, option); + files.UpdateAll(mod, option); Changes = false; } @@ -53,16 +43,16 @@ public class ModFileEditor var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (newDict.Count != subMod.Files.Count) - _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); } ModEditor.ApplyToAllOptions(mod, HandleSubMod); - _files.ClearMissingFiles(); + files.ClearMissingFiles(); } ///

Return whether the given path is already used in the current option. public bool CanAddGamePath(Utf8GamePath path) - => !_files.UsedPaths.Contains(path); + => !files.UsedPaths.Contains(path); /// /// Try to set a given path for a given file. @@ -72,17 +62,17 @@ public class ModFileEditor /// public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path) { - if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > _files.Available.Count) + if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; - var registry = _files.Available[fileIdx]; + var registry = files.Available[fileIdx]; if (pathIdx > registry.SubModUsage.Count) return false; if ((pathIdx == -1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty) - _files.AddUsedPath(option, registry, path); + files.AddUsedPath(option, registry, path); else - _files.ChangeUsedPath(registry, pathIdx, path); + files.ChangeUsedPath(registry, pathIdx, path); Changes = true; @@ -93,10 +83,10 @@ public class ModFileEditor /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. ///
- public int AddPathsToSelected(ISubMod option, IEnumerable files, int skipFolders = 0) + public int AddPathsToSelected(ISubMod option, IEnumerable files1, int skipFolders = 0) { var failed = 0; - foreach (var file in files) + foreach (var file in files1) { var gamePath = file.RelPath.ToGamePath(skipFolders); if (gamePath.IsEmpty) @@ -107,7 +97,7 @@ public class ModFileEditor if (CanAddGamePath(gamePath)) { - _files.AddUsedPath(option, file, gamePath); + files.AddUsedPath(option, file, gamePath); Changes = true; } else @@ -120,9 +110,9 @@ public class ModFileEditor } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(ISubMod option, IEnumerable files) + public void RemovePathsFromSelected(ISubMod option, IEnumerable files1) { - foreach (var file in files) + foreach (var file in files1) { for (var i = 0; i < file.SubModUsage.Count; ++i) { @@ -130,7 +120,7 @@ public class ModFileEditor if (option != opt) continue; - _files.RemoveUsedPath(option, file, path); + files.RemoveUsedPath(option, file, path); Changes = true; --i; } @@ -138,10 +128,10 @@ public class ModFileEditor } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files) + public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files1) { var deletions = 0; - foreach (var file in files) + foreach (var file in files1) { try { @@ -158,18 +148,18 @@ public class ModFileEditor if (deletions <= 0) return; - _modManager.Creator.ReloadMod(mod, false, out _); - _files.UpdateAll(mod, option); + modManager.Creator.ReloadMod(mod, false, out _); + files.UpdateAll(mod, option); } private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed) { - if (!_files.Missing.Contains(file)) + if (!files.Missing.Contains(file)) return true; if (removeUsed) - _files.RemoveUsedPath(option, file, key); + files.RemoveUsedPath(option, file, key); Penumbra.Log.Debug($"[RemoveMissingPaths] Removing {key} -> {file} from {mod.Name}."); return false; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 09c5c77a..bbf0d4b5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -4,16 +4,14 @@ using Penumbra.Mods.Subclasses; namespace Penumbra.Mods; -public class ModMetaEditor +public class ModMetaEditor(ModManager modManager) { - private readonly ModManager _modManager; - - private readonly HashSet _imc = new(); - private readonly HashSet _eqp = new(); - private readonly HashSet _eqdp = new(); - private readonly HashSet _gmp = new(); - private readonly HashSet _est = new(); - private readonly HashSet _rsp = new(); + private readonly HashSet _imc = []; + private readonly HashSet _eqp = []; + private readonly HashSet _eqdp = []; + private readonly HashSet _gmp = []; + private readonly HashSet _est = []; + private readonly HashSet _rsp = []; public int OtherImcCount { get; private set; } public int OtherEqpCount { get; private set; } @@ -22,11 +20,7 @@ public class ModMetaEditor public int OtherEstCount { get; private set; } public int OtherRspCount { get; private set; } - - public ModMetaEditor(ModManager modManager) - => _modManager = modManager; - - public bool Changes { get; private set; } = false; + public bool Changes { get; private set; } public IReadOnlySet Imc => _imc; @@ -156,7 +150,7 @@ public class ModMetaEditor if (!Changes) return; - _modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index c146b6f4..a9a31212 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -10,7 +10,7 @@ namespace Penumbra.Mods.Editor; public class ModNormalizer(ModManager _modManager, Configuration _config) { - private readonly List>> _redirections = new(); + private readonly List>> _redirections = []; public Mod Mod { get; private set; } = null!; private string _normalizationDirName = null!; @@ -141,7 +141,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) { var directory = Directory.CreateDirectory(_normalizationDirName); for (var i = _redirections.Count; i < Mod.Groups.Count + 1; ++i) - _redirections.Add(new List>()); + _redirections.Add([]); if (_redirections[0].Count == 0) _redirections[0].Add(new Dictionary(Mod.Default.Files.Count)); @@ -169,7 +169,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) { _redirections[groupIdx + 1].EnsureCapacity(group.Count); for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) - _redirections[groupIdx + 1].Add(new Dictionary()); + _redirections[groupIdx + 1].Add([]); var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); foreach (var option in group.OfType()) diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index b9834da8..ada06264 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -4,17 +4,13 @@ using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; using Penumbra.Util; -public class ModSwapEditor +public class ModSwapEditor(ModManager modManager) { - private readonly ModManager _modManager; - private readonly Dictionary _swaps = new(); + private readonly Dictionary _swaps = []; public IReadOnlyDictionary Swaps => _swaps; - public ModSwapEditor(ModManager modManager) - => _modManager = modManager; - public void Revert(ISubMod option) { _swaps.SetTo(option.FileSwaps); @@ -26,7 +22,7 @@ public class ModSwapEditor if (!Changes) return; - _modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); Changes = false; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 41cc023e..a9ef22cb 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index a081f7bc..88c4e4ce 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 52159258..c80334aa 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index a81ae55b..2b2bbf95 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -33,6 +33,7 @@ using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; using Penumbra.UI.Tabs.Debug; +using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 8b6ef331..6d406461 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -26,6 +26,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; +using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 926ba77b..9d57d3a8 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -8,6 +8,7 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index f7e64125..76fb8c96 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -6,6 +6,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 0cc2e5c1..8ef6c30e 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,6 +9,7 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.UI.Classes; From 9fae88934d2ee827021d01f6bbaf98c7ed3a9bc6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 00:00:58 +1100 Subject: [PATCH 1459/2451] Calculate missing tangents on import, convert all to bitangents for use --- .../Import/Models/Import/SubMeshImporter.cs | 6 +- .../Import/Models/Import/VertexAttribute.cs | 123 +++++++++++++++++- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 51443f64..e5b5bc8e 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -140,11 +140,15 @@ public class SubMeshImporter private void BuildIndices() { + // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaulating the indices to triangles with GetTriangleIndices, but will need investigation. _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); } private void BuildVertexAttributes() { + // Tangent calculation requires indices if missing. + ArgumentNullException.ThrowIfNull(_indices); + var accessors = _primitive.VertexAccessors; var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) @@ -158,7 +162,7 @@ public class SubMeshImporter VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors, _nodeBoneMap), VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors, _indices), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 0b0e90ba..5008b58e 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -242,10 +242,24 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices) { - if (!accessors.TryGetValue("TANGENT", out var accessor)) + if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) + { + Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents."); return null; + } + + var normals = normalAccessor.AsVector3Array(); + var values = accessors.TryGetValue("TANGENT", out var accessor) + ? accessor.AsVector4Array() + : CalculateTangents(accessors, indices, normals); + + if (values == null) + { + Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); + return null; + } var element = new MdlStructs.VertexElement() { @@ -254,8 +268,6 @@ public class VertexAttribute Usage = (byte)MdlFile.VertexUsage.Tangent1, }; - var values = accessor.AsVector4Array(); - // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. var morphValues = morphAccessors .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) @@ -263,7 +275,7 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildByteFloat4(values[index]), + index => BuildBitangent(values[index], normals[index]), buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -272,11 +284,110 @@ public class VertexAttribute if (delta != null) value += new Vector4(delta.Value, 0); - return BuildByteFloat4(value); + return BuildBitangent(value, normals[vertexIndex]); } ); } + /// Build a byte array representing bitagent data computed from the provided tangent and normal. + /// XIV primarily stores bitangents, rather than tangents as with most other software, so we calculate on import. + private static byte[] BuildBitangent(Vector4 tangent, Vector3 normal) + { + var handedness = tangent.W; + var tangent3 = new Vector3(tangent.X, tangent.Y, tangent.Z); + var bitangent = Vector3.Normalize(Vector3.Cross(normal, tangent3)); + bitangent *= handedness; + + // Byte floats encode 0..1, and bitangents are stored as -1..1. Convert. + bitangent = (bitangent + Vector3.One) / 2; + return BuildByteFloat4(new Vector4(bitangent, handedness)); + } + + /// Attempt to calculate tangent values based on other pre-existing data. + private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals) + { + // To calculate tangents, we will also need access to uv data. + if (!accessors.TryGetValue("TEXCOORD_0", out var uvAccessor)) + return null; + + var positions = accessors["POSITION"].AsVector3Array(); + var uvs = uvAccessor.AsVector2Array(); + + // TODO: Surface this in the UI. + Penumbra.Log.Warning("Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools."); + + var vertexCount = positions.Count; + + // https://github.com/TexTools/xivModdingFramework/blob/master/xivModdingFramework/Models/Helpers/ModelModifiers.cs#L1569 + // https://gamedev.stackexchange.com/a/68617 + // https://marti.works/posts/post-calculating-tangents-for-your-mesh/post/ + var tangents = new Vector3[vertexCount]; + var bitangents = new Vector3[vertexCount]; + + // Iterate over triangles, calculating tangents relative to the UVs. + for (var index = 0; index < indices.Length; index += 3) + { + // Collect information for this triangle. + var vertexIndex1 = indices[index]; + var vertexIndex2 = indices[index + 1]; + var vertexIndex3 = indices[index + 2]; + + var position1 = positions[vertexIndex1]; + var position2 = positions[vertexIndex2]; + var position3 = positions[vertexIndex3]; + + var texcoord1 = uvs[vertexIndex1]; + var texcoord2 = uvs[vertexIndex2]; + var texcoord3 = uvs[vertexIndex3]; + + // Calculate deltas for the position XYZ, and texcoord UV. + var edge1 = position2 - position1; + var edge2 = position3 - position1; + + var uv1 = texcoord2 - texcoord1; + var uv2 = texcoord3 - texcoord1; + + // Solve. + var r = 1.0f / (uv1.X * uv2.Y - uv1.Y * uv2.X); + var tangent = new Vector3( + (edge1.X * uv2.Y - edge2.X * uv1.Y) * r, + (edge1.Y * uv2.Y - edge2.Y * uv1.Y) * r, + (edge1.Z * uv2.Y - edge2.Z * uv1.Y) * r + ); + var bitangent = new Vector3( + (edge1.X * uv2.X - edge2.X * uv1.X) * r, + (edge1.Y * uv2.X - edge2.Y * uv1.X) * r, + (edge1.Z * uv2.X - edge2.Z * uv1.X) * r + ); + + // Update vertex values. + tangents[vertexIndex1] += tangent; + tangents[vertexIndex2] += tangent; + tangents[vertexIndex3] += tangent; + + bitangents[vertexIndex1] += bitangent; + bitangents[vertexIndex2] += bitangent; + bitangents[vertexIndex3] += bitangent; + } + + // All the triangles have been calcualted, normalise the results for each vertex. + var result = new Vector4[vertexCount]; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + var n = normals[vertexIndex]; + var t = tangents[vertexIndex]; + var b = bitangents[vertexIndex]; + + // Gram-Schmidt orthogonalize and calculate handedness. + var tangent = Vector3.Normalize(t - n * Vector3.Dot(n, t)); + var handedness = Vector3.Dot(Vector3.Cross(t, b), n) > 0 ? 1 : -1; + + result[vertexIndex] = new Vector4(tangent, handedness); + } + + return result; + } + public static VertexAttribute? Color(Accessors accessors) { if (!accessors.TryGetValue("COLOR_0", out var accessor)) From ea04cc554f9e1b6be72ba538b9ea737e6dbfec45 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 00:22:13 +1100 Subject: [PATCH 1460/2451] Fix up export a little --- Penumbra/Import/Models/Export/MeshExporter.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 8127f348..b00ca49e 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -318,11 +318,17 @@ public class MeshExporter ); if (_geometryType == typeof(VertexPositionNormalTangent)) + { + // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. + // TODO: While this assumption is safe, it would be sensible to actually check. + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One; + return new VertexPositionNormalTangent( ToVector3(attributes[MdlFile.VertexUsage.Position]), ToVector3(attributes[MdlFile.VertexUsage.Normal]), - FixTangentVector(ToVector4(attributes[MdlFile.VertexUsage.Tangent1])) + bitangent ); + } throw new Exception($"Unknown geometry type {_geometryType}."); } @@ -440,11 +446,6 @@ public class MeshExporter throw new Exception($"Unknown skinning type {_skinningType}"); } - /// Clamps any tangent W value other than 1 to -1. - /// Some XIV models seemingly store -1 as 0, this patches over that. - private static Vector4 FixTangentVector(Vector4 tangent) - => tangent with { W = tangent.W == 1 ? 1 : -1 }; - /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) => data switch From 5e794b73baca68c557093ebcd0b4a1cecdd41659 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 01:40:19 +1100 Subject: [PATCH 1461/2451] Consider normal morphs for morphed bitangent calculations --- .../Import/Models/Import/VertexAttribute.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 5008b58e..bbf49bcf 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -251,11 +251,11 @@ public class VertexAttribute } var normals = normalAccessor.AsVector3Array(); - var values = accessors.TryGetValue("TANGENT", out var accessor) + var tangents = accessors.TryGetValue("TANGENT", out var accessor) ? accessor.AsVector4Array() : CalculateTangents(accessors, indices, normals); - if (values == null) + if (tangents == null) { Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); return null; @@ -269,22 +269,30 @@ public class VertexAttribute }; // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. - var morphValues = morphAccessors + var tangentMorphValues = morphAccessors + .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .ToArray(); + + var normalMorphValues = morphAccessors .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, - index => BuildBitangent(values[index], normals[index]), + index => BuildBitangent(tangents[index], normals[index]), buildMorph: (morphIndex, vertexIndex) => { - var value = values[vertexIndex]; + var tangent = tangents[vertexIndex]; + var tangentDelta = tangentMorphValues[morphIndex]?[vertexIndex]; + if (tangentDelta != null) + tangent += new Vector4(tangentDelta.Value, 0); - var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) - value += new Vector4(delta.Value, 0); + var normal = normals[vertexIndex]; + var normalDelta = normalMorphValues[morphIndex]?[vertexIndex]; + if (normalDelta != null) + normal += normalDelta.Value; - return BuildBitangent(value, normals[vertexIndex]); + return BuildBitangent(tangent, normal); } ); } From 0ff7e49e4d2e0572bd1b3873af4f77a39a050cb4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 15 Jan 2024 21:38:25 +0100 Subject: [PATCH 1462/2451] Prepare changelog. --- Penumbra/Services/MessageService.cs | 4 ++-- Penumbra/UI/Changelog.cs | 19 +++++++++++++++++++ Penumbra/UI/Tabs/MessagesTab.cs | 11 +++-------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 06c3c4d0..daad29ef 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -25,8 +25,8 @@ public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) : Ot new UIForegroundPayload(0), new UIGlowPayload(0), new TextPayload(item.Name), - new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }), - new RawPayload(new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 }), + new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]), + new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]), }; // @formatter:on diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index d5b77bf4..94084fea 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -46,10 +46,29 @@ public class PenumbraChangelog Add8_1_2(Changelog); Add8_2_0(Changelog); Add8_3_0(Changelog); + Add1_0_0_0(Changelog); } #region Changelogs + private static void Add1_0_0_0(Changelog log) + => log.NextVersion("Version 1.0.0.0") + .RegisterHighlight("Mods in the mod selector can now be filtered by changed item categories.") + .RegisterHighlight("Model Editing options in the Advanced Editing Window have been greatly extended (by Ackwell):") + .RegisterEntry("Attributes and referenced materials can now be set per mesh.", 1) + .RegisterEntry("Model files (.mdl) can now be exported to the well-established glTF format, which can be imported e.g. by Blender.", 1) + .RegisterEntry("glTF files can also be imported back to a .mdl file.", 1) + .RegisterHighlight("Model Export and Import are a work in progress and may encounter issues, not support all cases or produce wrong results, please let us know!", 1) + .RegisterEntry("The last selected mod and the open/close state of the Advanced Editing Window are now stored across launches.") + .RegisterEntry("Footsteps of certain mounts will now be associated to collections correctly.") + .RegisterEntry("Save-in-Place in the texture tab now requires the configurable modifier.") + .RegisterEntry("Updated OtterTex to a newer version of DirectXTex.") + .RegisterEntry("Fixed an issue with horizontal scrolling if a mod title was very long.") + .RegisterEntry("Fixed an issue with the mod panels header not updating its data when the selected mod updates.") + .RegisterEntry("Fixed some issues with EQDP files for invalid characters.") + .RegisterEntry("Fixed an issue with the FileDialog being drawn twice in certain situations.") + .RegisterEntry("A lot of backend changes that should not have an effect on users, but may cause issues if something got messed up."); + private static void Add8_3_0(Changelog log) => log.NextVersion("Version 0.8.3.0") .RegisterHighlight("Improved the UI for the On-Screen tabs with highlighting of used paths, filtering and more selections. (by Ny)") diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/Tabs/MessagesTab.cs index e834a4b4..abaf2ba6 100644 --- a/Penumbra/UI/Tabs/MessagesTab.cs +++ b/Penumbra/UI/Tabs/MessagesTab.cs @@ -3,19 +3,14 @@ using Penumbra.Services; namespace Penumbra.UI.Tabs; -public class MessagesTab : ITab +public class MessagesTab(MessageService messages) : ITab { public ReadOnlySpan Label => "Messages"u8; - private readonly MessageService _messages; - - public MessagesTab(MessageService messages) - => _messages = messages; - public bool IsVisible - => _messages.Count > 0; + => messages.Count > 0; public void DrawContent() - => _messages.Draw(); + => messages.Draw(); } From da1b9e9e90e3ab97545bc013e152e6dec5d9c990 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jan 2024 16:34:57 +0100 Subject: [PATCH 1463/2451] Cleanup, fix tangent/normal mixup. --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- .../Import/Models/Import/SubMeshImporter.cs | 2 +- .../Import/Models/Import/VertexAttribute.cs | 56 +++++++++---------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index b00ca49e..da6b4df4 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -23,7 +23,7 @@ public class MeshExporter ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints) : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); - var extras = new Dictionary(); + var extras = new Dictionary(data.Attributes.Length); foreach (var attribute in data.Attributes) extras.Add(attribute, true); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index e5b5bc8e..bca75090 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -140,7 +140,7 @@ public class SubMeshImporter private void BuildIndices() { - // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaulating the indices to triangles with GetTriangleIndices, but will need investigation. + // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaluating the indices to triangles with GetTriangleIndices, but will need investigation. _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index bbf49bcf..b5b20a3a 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -244,7 +244,7 @@ public class VertexAttribute public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices) { - if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) + if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) { Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents."); return null; @@ -261,7 +261,7 @@ public class VertexAttribute return null; } - var element = new MdlStructs.VertexElement() + var element = new MdlStructs.VertexElement { Stream = 1, Type = (byte)MdlFile.VertexType.ByteFloat4, @@ -269,26 +269,23 @@ public class VertexAttribute }; // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. - var tangentMorphValues = morphAccessors - .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) - .ToArray(); - - var normalMorphValues = morphAccessors - .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) - .ToArray(); + var morphValues = morphAccessors + .Select(a => (Tangent: a.GetValueOrDefault("TANGENT")?.AsVector3Array(), + Normal: a.GetValueOrDefault("NORMAL")?.AsVector3Array())) + .ToList(); return new VertexAttribute( element, index => BuildBitangent(tangents[index], normals[index]), buildMorph: (morphIndex, vertexIndex) => { - var tangent = tangents[vertexIndex]; - var tangentDelta = tangentMorphValues[morphIndex]?[vertexIndex]; + var tangent = tangents[vertexIndex]; + var tangentDelta = morphValues[morphIndex].Tangent?[vertexIndex]; if (tangentDelta != null) tangent += new Vector4(tangentDelta.Value, 0); - var normal = normals[vertexIndex]; - var normalDelta = normalMorphValues[morphIndex]?[vertexIndex]; + var normal = normals[vertexIndex]; + var normalDelta = morphValues[morphIndex].Normal?[vertexIndex]; if (normalDelta != null) normal += normalDelta.Value; @@ -297,13 +294,13 @@ public class VertexAttribute ); } - /// Build a byte array representing bitagent data computed from the provided tangent and normal. + /// Build a byte array representing bitangent data computed from the provided tangent and normal. /// XIV primarily stores bitangents, rather than tangents as with most other software, so we calculate on import. private static byte[] BuildBitangent(Vector4 tangent, Vector3 normal) { var handedness = tangent.W; - var tangent3 = new Vector3(tangent.X, tangent.Y, tangent.Z); - var bitangent = Vector3.Normalize(Vector3.Cross(normal, tangent3)); + var tangent3 = new Vector3(tangent.X, tangent.Y, tangent.Z); + var bitangent = Vector3.Normalize(Vector3.Cross(normal, tangent3)); bitangent *= handedness; // Byte floats encode 0..1, and bitangents are stored as -1..1. Convert. @@ -319,19 +316,20 @@ public class VertexAttribute return null; var positions = accessors["POSITION"].AsVector3Array(); - var uvs = uvAccessor.AsVector2Array(); + var uvs = uvAccessor.AsVector2Array(); // TODO: Surface this in the UI. - Penumbra.Log.Warning("Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools."); + Penumbra.Log.Warning( + "Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools."); var vertexCount = positions.Count; // https://github.com/TexTools/xivModdingFramework/blob/master/xivModdingFramework/Models/Helpers/ModelModifiers.cs#L1569 // https://gamedev.stackexchange.com/a/68617 // https://marti.works/posts/post-calculating-tangents-for-your-mesh/post/ - var tangents = new Vector3[vertexCount]; + var tangents = new Vector3[vertexCount]; var bitangents = new Vector3[vertexCount]; - + // Iterate over triangles, calculating tangents relative to the UVs. for (var index = 0; index < indices.Length; index += 3) { @@ -344,16 +342,16 @@ public class VertexAttribute var position2 = positions[vertexIndex2]; var position3 = positions[vertexIndex3]; - var texcoord1 = uvs[vertexIndex1]; - var texcoord2 = uvs[vertexIndex2]; - var texcoord3 = uvs[vertexIndex3]; - + var texCoord1 = uvs[vertexIndex1]; + var texCoord2 = uvs[vertexIndex2]; + var texCoord3 = uvs[vertexIndex3]; + // Calculate deltas for the position XYZ, and texcoord UV. var edge1 = position2 - position1; var edge2 = position3 - position1; - - var uv1 = texcoord2 - texcoord1; - var uv2 = texcoord3 - texcoord1; + + var uv1 = texCoord2 - texCoord1; + var uv2 = texCoord3 - texCoord1; // Solve. var r = 1.0f / (uv1.X * uv2.Y - uv1.Y * uv2.X); @@ -378,7 +376,7 @@ public class VertexAttribute bitangents[vertexIndex3] += bitangent; } - // All the triangles have been calcualted, normalise the results for each vertex. + // All the triangles have been calculated, normalise the results for each vertex. var result = new Vector4[vertexCount]; for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { @@ -387,7 +385,7 @@ public class VertexAttribute var b = bitangents[vertexIndex]; // Gram-Schmidt orthogonalize and calculate handedness. - var tangent = Vector3.Normalize(t - n * Vector3.Dot(n, t)); + var tangent = Vector3.Normalize(t - n * Vector3.Dot(n, t)); var handedness = Vector3.Dot(Vector3.Cross(t, b), n) > 0 ? 1 : -1; result[vertexIndex] = new Vector4(tangent, handedness); From 8509ccba30ab52c938470d059e64dc00782b0358 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jan 2024 15:39:56 +0000 Subject: [PATCH 1464/2451] [CI] Updating repo.json for 1.0.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4dfac5a0..01d169b5 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.8.3.1", - "TestingAssemblyVersion": "0.8.3.7", + "AssemblyVersion": "1.0.0.0", + "TestingAssemblyVersion": "1.0.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e5ddae585c536b94987699dfe2afe8fad72a7a45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jan 2024 18:21:46 +0100 Subject: [PATCH 1465/2451] Update Submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 4c32d2d4..6d6e7b37 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4c32d2d448c4e36ea665276ed755a96fa4907c33 +Subproject commit 6d6e7b37c31bc82b8b2811c85a09f67fb0434f8a diff --git a/Penumbra.GameData b/Penumbra.GameData index 13545143..119ffaa2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 135451430344f2f12e8c02fd4c4c6f0875d74e60 +Subproject commit 119ffaa240f0446bdd05c5254c4e0dd8539a2d7d From 0b50593acdc6c6fb7a3a556a83ca802a3158d79f Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jan 2024 17:24:05 +0000 Subject: [PATCH 1466/2451] [CI] Updating repo.json for 1.0.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 01d169b5..c328dd6f 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.0", - "TestingAssemblyVersion": "1.0.0.0", + "AssemblyVersion": "1.0.0.1", + "TestingAssemblyVersion": "1.0.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From bbac3daf0130ea55d2ab8275ae0eae7384aa3e69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jan 2024 12:14:22 +0100 Subject: [PATCH 1467/2451] Update BNPCs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 119ffaa2..afc03458 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 119ffaa240f0446bdd05c5254c4e0dd8539a2d7d +Subproject commit afc0345819ca64ec08432e0011b65ff9880c2967 From fdd01459759b3b31e149f6f862a8565da393684e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jan 2024 12:14:58 +0100 Subject: [PATCH 1468/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 6d6e7b37..92590901 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6d6e7b37c31bc82b8b2811c85a09f67fb0434f8a +Subproject commit 9259090121b26f097948e7bbd83b32708ea0410d From eb02d2a7a74d7f5e93dd76fe7a32ee3810e44fd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jan 2024 14:37:49 +0100 Subject: [PATCH 1469/2451] Fix variant path generation. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index afc03458..ab09e21f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit afc0345819ca64ec08432e0011b65ff9880c2967 +Subproject commit ab09e21fa46be83f82c400dfd2fe05a281b6f28d From 4b81b065aa406903398de4bee7292aca4094bf9f Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 23:06:02 +1100 Subject: [PATCH 1470/2451] Calculate primary BoundingBox --- Penumbra/Import/Models/Import/BoundingBox.cs | 33 +++++++++++++++++++ Penumbra/Import/Models/Import/MeshImporter.cs | 7 ++++ .../Import/Models/Import/ModelImporter.cs | 7 +++- .../Import/Models/Import/SubMeshImporter.cs | 14 ++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Import/Models/Import/BoundingBox.cs diff --git a/Penumbra/Import/Models/Import/BoundingBox.cs b/Penumbra/Import/Models/Import/BoundingBox.cs new file mode 100644 index 00000000..34aac827 --- /dev/null +++ b/Penumbra/Import/Models/Import/BoundingBox.cs @@ -0,0 +1,33 @@ +using Lumina.Data.Parsing; + +namespace Penumbra.Import.Models.Import; + +/// Mutable representation of the bounding box surrouding a collection of vertices. +public class BoundingBox +{ + private Vector3 _minimum = Vector3.Zero; + private Vector3 _maximum = Vector3.Zero; + + /// Use the specified position to update this bounding box, expanding it if necessary. + public void Merge(Vector3 position) + { + _minimum = Vector3.Min(_minimum, position); + _maximum = Vector3.Max(_maximum, position); + } + + /// Merge the provided bounding box into this one, expanding it if necessary. + /// + public void Merge(BoundingBox other) + { + _minimum = Vector3.Min(_minimum, other._minimum); + _maximum = Vector3.Max(_maximum, other._maximum); + } + + /// Convert this bounding box to the struct format used in .mdl data structures. + public MdlStructs.BoundingBoxStruct ToStruct() + => new MdlStructs.BoundingBoxStruct + { + Min = [_minimum.X, _minimum.Y, _minimum.Z, 1], + Max = [_maximum.X, _maximum.Y, _maximum.Z, 1], + }; +} diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 7da4d1d7..2a461304 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -19,6 +19,8 @@ public class MeshImporter(IEnumerable nodes) public List? Bones; + public BoundingBox BoundingBox; + public List MetaAttributes; public List ShapeKeys; @@ -50,6 +52,8 @@ public class MeshImporter(IEnumerable nodes) private List? _bones; + private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly List _metaAttributes = []; private readonly Dictionary> _shapeValues = []; @@ -87,6 +91,7 @@ public class MeshImporter(IEnumerable nodes) VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]), Indices = _indices, Bones = _bones, + BoundingBox = _boundingBox, MetaAttributes = _metaAttributes, ShapeKeys = _shapeValues .Select(pair => new MeshShapeKey() @@ -154,6 +159,8 @@ public class MeshImporter(IEnumerable nodes) })); } + _boundingBox.Merge(subMesh.BoundingBox); + // And finally, merge in the sub-mesh struct itself. _subMeshes.Add(subMesh.SubMeshStruct with { diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 3b3d2cd0..1b7fdfa5 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -29,6 +29,8 @@ public partial class ModelImporter(ModelRoot model) private readonly List _bones = []; private readonly List _boneTables = []; + private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly List _metaAttributes = []; private readonly Dictionary> _shapeMeshes = []; @@ -95,9 +97,10 @@ public partial class ModelImporter(ModelRoot model) Materials = [.. materials], + BoundingBoxes = _boundingBox.ToStruct(), + // TODO: Would be good to calculate all of this up the tree. Radius = 1, - BoundingBoxes = MdlFile.EmptyBoundingBox, BoneBoundingBoxes = Enumerable.Repeat(MdlFile.EmptyBoundingBox, _bones.Count).ToArray(), RemainingData = [.._vertexBuffer, ..indexBuffer], }; @@ -156,6 +159,8 @@ public partial class ModelImporter(ModelRoot model) .ToArray(), }); + _boundingBox.Merge(mesh.BoundingBox); + _subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with { AttributeIndexMask = Utility.GetMergedAttributeMask( diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index bca75090..6a5d0d52 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -21,6 +21,8 @@ public class SubMeshImporter public ushort[] Indices; + public BoundingBox BoundingBox; + public string[] MetaAttributes; public Dictionary> ShapeValues; @@ -44,6 +46,8 @@ public class SubMeshImporter private ushort[]? _indices; + private BoundingBox _boundingBox = new BoundingBox(); + private string[]? _metaAttributes; private readonly List? _morphNames; @@ -90,9 +94,11 @@ public class SubMeshImporter private SubMesh Create() { // Build all the data we'll need. + // TODO: This structure is verging on a little silly. Reconsider. BuildIndices(); BuildVertexAttributes(); BuildVertices(); + BuildBoundingBox(); BuildMetaAttributes(); ArgumentNullException.ThrowIfNull(_indices); @@ -133,6 +139,7 @@ public class SubMeshImporter Strides = _strides, Streams = _streams, Indices = _indices, + BoundingBox = _boundingBox, MetaAttributes = _metaAttributes, ShapeValues = _shapeValues, }; @@ -255,6 +262,13 @@ public class SubMeshImporter _shapeValues = morphShapeValues; } + private void BuildBoundingBox() + { + var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array(); + foreach (var position in positions) + _boundingBox.Merge(position); + } + private void BuildMetaAttributes() { // We consider any "extras" key with a boolean value set to `true` to be an attribute. From b089bbca37b3006401b4950f6847d9c38c9c7391 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 23:13:53 +1100 Subject: [PATCH 1471/2451] Merge element ids and flags --- .../ModEditWindow.Models.MdlTab.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 9cfe0739..841eff4c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,3 +1,4 @@ +using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -161,6 +162,13 @@ public partial class ModEditWindow if (ImportKeepAttributes) MergeAttributes(newMdl, Mdl); + // Until someone works out how to actually author these, unconditionally merge element ids. + MergeElementIds(newMdl, Mdl); + + // TODO: Add flag editing. + newMdl.Flags1 = Mdl.Flags1; + newMdl.Flags2 = Mdl.Flags2; + Initialize(newMdl); _dirty = true; } @@ -210,6 +218,29 @@ public partial class ModEditWindow } } + /// Merge element ids from the source onto the target. + /// Model that will be updated. > + /// Model to copy element ids from. + private static void MergeElementIds(MdlFile target, MdlFile source) + { + var elementIds = new List(); + + foreach (var sourceElement in source.ElementIds) + { + var sourceBone = source.Bones[sourceElement.ParentBoneName]; + var targetIndex = target.Bones.IndexOf(sourceBone); + // Given that there's no means of authoring these at the moment, this should probably remain a hard error. + if (targetIndex == -1) + throw new Exception($"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); + elementIds.Add(sourceElement with + { + ParentBoneName = (uint)targetIndex, + }); + } + + target.ElementIds = [.. elementIds]; + } + private void RecordIoExceptions(Exception? exception) { IoExceptions = exception switch { From 107f6706d315eb0dd5917f44e8fb87fdd45d70e6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 23:14:12 +1100 Subject: [PATCH 1472/2451] i am immensely vain --- Penumbra/UI/Changelog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 94084fea..67ab1a87 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -54,7 +54,7 @@ public class PenumbraChangelog private static void Add1_0_0_0(Changelog log) => log.NextVersion("Version 1.0.0.0") .RegisterHighlight("Mods in the mod selector can now be filtered by changed item categories.") - .RegisterHighlight("Model Editing options in the Advanced Editing Window have been greatly extended (by Ackwell):") + .RegisterHighlight("Model Editing options in the Advanced Editing Window have been greatly extended (by ackwell):") .RegisterEntry("Attributes and referenced materials can now be set per mesh.", 1) .RegisterEntry("Model files (.mdl) can now be exported to the well-established glTF format, which can be imported e.g. by Blender.", 1) .RegisterEntry("glTF files can also be imported back to a .mdl file.", 1) From 2c5f22047a1d84eff94cbce4f14f9b9ee1f89bb2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 23:25:20 +1100 Subject: [PATCH 1473/2451] Generate white vertex colours if missing --- Penumbra/Import/Models/Import/VertexAttribute.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b5b20a3a..7c875162 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -394,10 +394,9 @@ public class VertexAttribute return result; } - public static VertexAttribute? Color(Accessors accessors) + public static VertexAttribute Color(Accessors accessors) { - if (!accessors.TryGetValue("COLOR_0", out var accessor)) - return null; + accessors.TryGetValue("COLOR_0", out var accessor); var element = new MdlStructs.VertexElement() { @@ -406,11 +405,12 @@ public class VertexAttribute Usage = (byte)MdlFile.VertexUsage.Color, }; - var values = accessor.AsVector4Array(); + // Some shaders rely on the presence of vertex colors to render - fall back to a pure white value if it's missing. + var values = accessor?.AsVector4Array(); return new VertexAttribute( element, - index => BuildByteFloat4(values[index]) + index => BuildByteFloat4(values?[index] ?? Vector4.One) ); } From 202f6e472869547de56910f732d3769260cf4aca Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 18 Jan 2024 00:09:19 +1100 Subject: [PATCH 1474/2451] Fix file path resolution for file swaps --- .../UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 841eff4c..26fcd1ee 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -259,11 +259,12 @@ public partial class ModEditWindow if (!Utf8GamePath.FromString(path, out var utf8Path, true)) throw new Exception($"Resolved path {path} could not be converted to a game path."); - var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path); + var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(utf8Path); + // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath == null - ? _edit._gameData.GetFile(path)?.Data - : File.ReadAllBytes(resolvedPath.Value.ToPath()); + var bytes = resolvedPath.IsRooted + ? File.ReadAllBytes(resolvedPath.FullName) + : _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data; // TODO: some callers may not care about failures - handle exceptions separately? return bytes ?? throw new Exception( From 65af4267f073533b4795953808621bc8120d4004 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jan 2024 14:46:44 +0100 Subject: [PATCH 1475/2451] Fix variant path generation (2), file was unsaved :/ --- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index 78c49b59..cd36de93 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -46,7 +46,7 @@ public static class CustomizationSwap PrimaryId idFrom, PrimaryId idTo, byte variant, ref string fileName, ref bool dataWasChanged) { - variant = slot is BodySlot.Face or BodySlot.Ear ? byte.MaxValue : variant; + variant = slot is BodySlot.Face or BodySlot.Ear ? Variant.None.Id : variant; var mtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); var mtrlToPath = GamePaths.Character.Mtrl.Path(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); From 8c763d5379778bfc0bbf90cba265f0be8e80305d Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 17 Jan 2024 16:24:57 +0000 Subject: [PATCH 1476/2451] [CI] Updating repo.json for 1.0.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c328dd6f..aa03fe77 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.1", - "TestingAssemblyVersion": "1.0.0.1", + "AssemblyVersion": "1.0.0.2", + "TestingAssemblyVersion": "1.0.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5c15a3a4ffdac635b82f11aeb7bba4ad8b715899 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:09:43 +1100 Subject: [PATCH 1477/2451] Set up notifier infrastructure --- Penumbra/Import/Models/IoNotifier.cs | 52 +++++++++++++++++++ .../ModEditWindow.Models.MdlTab.cs | 1 + .../UI/AdvancedWindow/ModEditWindow.Models.cs | 37 +++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 Penumbra/Import/Models/IoNotifier.cs diff --git a/Penumbra/Import/Models/IoNotifier.cs b/Penumbra/Import/Models/IoNotifier.cs new file mode 100644 index 00000000..e1d649f6 --- /dev/null +++ b/Penumbra/Import/Models/IoNotifier.cs @@ -0,0 +1,52 @@ +using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; + +namespace Penumbra.Import.Models; + +public record class IoNotifier +{ + /// Notification subclass so that we have a distinct type to filter by. + private class LegallyDistinctNotification : Notification + { + public LegallyDistinctNotification(string content, NotificationType type): base(content, type) + {} + } + + private readonly DateTime _startTime = DateTime.UtcNow; + private string _context = ""; + + /// Create a new notifier with the specified context appended to any other context already present. + public IoNotifier WithContext(string context) + => this with { _context = $"{_context}{context}: "}; + + /// Send a warning with any current context to notification channels. + public void Warning(string content) + => SendNotification(content, NotificationType.Warning); + + /// Get the current warnings for this notifier. + /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. + public IEnumerable GetWarnings() + => GetFilteredNotifications(NotificationType.Warning); + + /// Create an exception with any current context. + [StackTraceHidden] + public Exception Exception(string message) + => Exception(message); + + /// Create an exception of the provided type with any current context. + [StackTraceHidden] + public TException Exception(string message) + where TException : Exception, new() + => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; + + private void SendNotification(string message, NotificationType type) + => Penumbra.Messager.AddMessage( + new LegallyDistinctNotification($"{_context}{message}", type), + true, false, true, false + ); + + private IEnumerable GetFilteredNotifications(NotificationType type) + => Penumbra.Messager + .Where(p => p.Key >= _startTime && p.Value is LegallyDistinctNotification && p.Value.NotificationType == type) + .Select(p => p.Value.PrintMessage); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 26fcd1ee..15c6cb21 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -25,6 +25,7 @@ public partial class ModEditWindow private bool _dirty; public bool PendingIo { get; private set; } public List IoExceptions { get; private set; } = []; + public List IoWarnings { get; private set; } = []; public MdlTab(ModEditWindow edit, byte[] bytes, string path) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index ad609285..1a200fdf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -63,6 +63,7 @@ public partial class ModEditWindow DrawExport(tab, childSize, disabled); DrawIoExceptions(tab); + DrawIoWarnings(tab); } private void DrawImport(MdlTab tab, Vector2 size, bool _1) @@ -148,7 +149,43 @@ public partial class ModEditWindow using var exceptionNode = ImRaii.TreeNode(message); if (exceptionNode) + { + ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); + ImGui.SameLine(); ImGuiUtil.TextWrapped(exception.ToString()); + } + } + } + + private static void DrawIoWarnings(MdlTab tab) + { + if (tab.IoWarnings.Count == 0) + return; + + var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); + using var frame = ImRaii.FramedGroup("Warnings", size, headerPreIcon: FontAwesomeIcon.ExclamationCircle, borderColor: 0xFF40FFFF); + + var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; + foreach (var (warning, index) in tab.IoWarnings.WithIndex()) + { + using var id = ImRaii.PushId(index); + var textSize = ImGui.CalcTextSize(warning).X; + + if (textSize <= spaceAvail) + { + ImRaii.TreeNode(warning, ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + var firstLine = warning[..(int)Math.Floor(warning.Length * (spaceAvail / textSize))] + "..."; + + using var warningNode = ImRaii.TreeNode(firstLine); + if (warningNode) + { + ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); + ImGui.SameLine(); + ImGuiUtil.TextWrapped(warning.ToString()); + } } } From 6f3be39cb9288f2c7e46559b77b5264ec36b4ca4 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:11:50 +1100 Subject: [PATCH 1478/2451] Wire up notifications through export --- .../Import/Models/Export/MaterialExporter.cs | 8 ++--- Penumbra/Import/Models/Export/MeshExporter.cs | 35 ++++++++++--------- .../Import/Models/Export/ModelExporter.cs | 15 ++++---- Penumbra/Import/Models/ModelManager.cs | 35 ++++++++++++------- .../ModEditWindow.Models.MdlTab.cs | 2 ++ 5 files changed, 55 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 2a49e77f..61609bb5 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -23,7 +23,7 @@ public class MaterialExporter } /// Build a glTF material from a hydrated XIV model, with the provided name. - public static MaterialBuilder Export(Material material, string name) + public static MaterialBuilder Export(Material material, string name, IoNotifier notifier) { Penumbra.Log.Debug($"Exporting material \"{name}\"."); return material.Mtrl.ShaderPackage.Name switch @@ -34,7 +34,7 @@ public class MaterialExporter "hair.shpk" => BuildHair(material, name), "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), - _ => BuildFallback(material, name), + _ => BuildFallback(material, name, notifier), }; } @@ -335,9 +335,9 @@ public class MaterialExporter /// Build a material from a source with unknown semantics. /// Will make a loose effort to fetch common / simple textures. - private static MaterialBuilder BuildFallback(Material material, string name) + private static MaterialBuilder BuildFallback(Material material, string name, IoNotifier notifier) { - Penumbra.Log.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); + notifier.Warning($"Unhandled shader package: {material.Mtrl.ShaderPackage.Name}"); var materialBuilder = BuildSharedBase(material, name) .WithMetallicRoughnessShader() diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index da6b4df4..71e8f082 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -38,14 +38,16 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton) + public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { - var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names); + var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names, notifier); return new Mesh(self.BuildMeshes(), skeleton?.Joints); } private const byte MaximumMeshBufferStreams = 3; + private readonly IoNotifier _notifier; + private readonly MdlFile _mdl; private readonly byte _lod; private readonly ushort _meshIndex; @@ -61,8 +63,9 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap) + private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap, IoNotifier notifier) { + _notifier = notifier; _mdl = mdl; _lod = lod; _meshIndex = meshIndex; @@ -84,7 +87,7 @@ public class MeshExporter // If there's skinning usages but no bone mapping, there's probably something wrong with the data. if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null) - Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided."); + _notifier.Warning($"Skinned vertex usages but no bone information was provided."); Penumbra.Log.Debug( $"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}"); @@ -105,7 +108,7 @@ public class MeshExporter { var boneName = _mdl.Bones[xivBoneIndex]; if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) - throw new Exception($"Armature does not contain bone \"{boneName}\" requested by mesh {_meshIndex}."); + throw _notifier.Exception($"Armature does not contain bone \"{boneName}\". Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured."); indexMap.Add((ushort)tableIndex, gltfBoneIndex); } @@ -271,7 +274,7 @@ public class MeshExporter } /// Read a vertex attribute of the specified type from a vertex buffer stream. - private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) + private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader) { return type switch { @@ -284,15 +287,15 @@ public class MeshExporter MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), - _ => throw new ArgumentOutOfRangeException(), + var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } /// Get the vertex geometry type for this mesh's vertex usages. - private static Type GetGeometryType(IReadOnlyDictionary usages) + private Type GetGeometryType(IReadOnlyDictionary usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) - throw new Exception("Mesh does not contain position vertex elements."); + throw _notifier.Exception("Mesh does not contain position vertex elements."); if (!usages.ContainsKey(MdlFile.VertexUsage.Normal)) return typeof(VertexPosition); @@ -330,11 +333,11 @@ public class MeshExporter ); } - throw new Exception($"Unknown geometry type {_geometryType}."); + throw _notifier.Exception($"Unknown geometry type {_geometryType}."); } /// Get the vertex material type for this mesh's vertex usages. - private static Type GetMaterialType(IReadOnlyDictionary usages) + private Type GetMaterialType(IReadOnlyDictionary usages) { var uvCount = 0; if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) @@ -343,7 +346,7 @@ public class MeshExporter MdlFile.VertexType.Half2 => 1, MdlFile.VertexType.Half4 => 2, MdlFile.VertexType.Single4 => 2, - _ => throw new Exception($"Unexpected UV vertex type {type}."), + _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), }; var materialUsages = ( @@ -403,7 +406,7 @@ public class MeshExporter ); } - throw new Exception($"Unknown material type {_skinningType}"); + throw _notifier.Exception($"Unknown material type {_skinningType}"); } /// Get the vertex skinning type for this mesh's vertex usages. @@ -424,7 +427,7 @@ public class MeshExporter if (_skinningType == typeof(VertexJoints4)) { if (_boneIndexMap == null) - throw new Exception("Tried to build skinned vertex but no bone mappings are available."); + throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); @@ -435,7 +438,7 @@ public class MeshExporter // NOTE: I've not seen any files that throw this error that aren't completely broken. var xivBoneIndex = indices[bindingIndex]; if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex)) - throw new Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); + throw _notifier.Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}."); return (jointIndex, weights[bindingIndex]); }) @@ -443,7 +446,7 @@ public class MeshExporter return new VertexJoints4(bindings); } - throw new Exception($"Unknown skinning type {_skinningType}"); + throw _notifier.Exception($"Unknown skinning type {_skinningType}"); } /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index da24fbb0..550aaf11 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,16 +23,16 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials, IoNotifier notifier) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; - var materials = ConvertMaterials(mdl, rawMaterials); - var meshes = ConvertMeshes(mdl, materials, gltfSkeleton); + var materials = ConvertMaterials(mdl, rawMaterials, notifier); + var meshes = ConvertMeshes(mdl, materials, gltfSkeleton, notifier); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton) + private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); @@ -43,7 +43,8 @@ public class ModelExporter // TODO: consider other types of mesh? for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { - var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), materials, skeleton); + var meshIndex = (ushort)(lod.MeshIndex + meshOffset); + var mesh = MeshExporter.Export(mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); meshes.Add(mesh); } } @@ -52,11 +53,11 @@ public class ModelExporter } /// Build materials for each of the material slots in the .mdl. - private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials) + private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials, IoNotifier notifier) => mdl.Materials // TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback. // fallback can likely be a static on the material exporter. - .Select(name => MaterialExporter.Export(rawMaterials[name], name)) + .Select(name => MaterialExporter.Export(rawMaterials[name], name, notifier.WithContext($"Material {name}"))) .ToArray(); /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 7f1171f3..ffcb5bbe 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,20 +37,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + => EnqueueWithResult( + new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), + action => action.Notifier + ); public Task ImportGltf(string inputPath) - { - var action = new ImportGltfAction(inputPath); - return Enqueue(action).ContinueWith(task => - { - if (task is { IsFaulted: true, Exception: not null }) - throw task.Exception; - - return action.Out; - }); - } + => EnqueueWithResult( + new ImportGltfAction(inputPath), + action => action.Out + ); /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. @@ -168,6 +165,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } + private Task EnqueueWithResult(TAction action, Func process) + where TAction : IAction + => Enqueue(action).ContinueWith(task => + { + if (task is { IsFaulted: true, Exception: not null }) + throw task.Exception; + + return process(action); + }); + private class ExportToGltfAction( ModelManager manager, MdlFile mdl, @@ -176,6 +183,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect string outputPath) : IAction { + public IoNotifier Notifier = new IoNotifier(); + public void Execute(CancellationToken cancel) { Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); @@ -190,7 +199,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ); Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons, materials); + var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 15c6cb21..17b46626 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -134,6 +134,8 @@ public partial class ModEditWindow .ContinueWith(task => { RecordIoExceptions(task.Exception); + if (task is { IsCompletedSuccessfully: true, Result: not null }) + IoWarnings = task.Result.GetWarnings().ToList(); PendingIo = false; }); } From 6da725350affe1bf78194acdac6663d510c29709 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 02:56:01 +1100 Subject: [PATCH 1479/2451] Wire up notifier through import --- Penumbra/Import/Models/Import/MeshImporter.cs | 26 +++++++++++------- .../Import/Models/Import/ModelImporter.cs | 19 ++++++------- .../Import/Models/Import/SubMeshImporter.cs | 25 ++++++++--------- .../Import/Models/Import/VertexAttribute.cs | 27 +++++++++---------- Penumbra/Import/Models/ModelManager.cs | 7 ++--- .../ModEditWindow.Models.MdlTab.cs | 10 ++++--- 6 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 2a461304..28a7a9c1 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -3,7 +3,7 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public class MeshImporter(IEnumerable nodes) +public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { public struct Mesh { @@ -33,9 +33,9 @@ public class MeshImporter(IEnumerable nodes) public List ShapeValues; } - public static Mesh Import(IEnumerable nodes) + public static Mesh Import(IEnumerable nodes, IoNotifier notifier) { - var importer = new MeshImporter(nodes); + var importer = new MeshImporter(nodes, notifier); return importer.Create(); } @@ -115,11 +115,11 @@ public class MeshImporter(IEnumerable nodes) var vertexOffset = _vertexCount; var indexOffset = _indices.Count; - var nodeBoneMap = CreateNodeBoneMap(node); - var subMesh = SubMeshImporter.Import(node, nodeBoneMap); - var subMeshName = node.Name ?? node.Mesh.Name; + var nodeBoneMap = CreateNodeBoneMap(node); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}")); + // TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh. _material ??= subMesh.Material; @@ -127,8 +127,11 @@ public class MeshImporter(IEnumerable nodes) if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) - throw new Exception( - $"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations."); + throw notifier.Exception( + $@"All sub-meshes of a mesh must have equivalent vertex declarations. + Current: {FormatVertexDeclaration(_vertexDeclaration.Value)} + Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}" + ); // Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine. // TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed. @@ -170,6 +173,9 @@ public class MeshImporter(IEnumerable nodes) }); } + private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) + => string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { var elA = a.VertexElements; @@ -204,13 +210,13 @@ public class MeshImporter(IEnumerable nodes) var meshName = node.Name ?? mesh.Name ?? "(no name)"; var primitiveCount = mesh.Primitives.Count; if (primitiveCount != 1) - throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); + throw notifier.Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); var primitive = mesh.Primitives[0]; // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + ?? throw notifier.Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 1b7fdfa5..bf59f278 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,14 +1,15 @@ using Lumina.Data.Parsing; +using OtterGui; using Penumbra.GameData.Files; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; -public partial class ModelImporter(ModelRoot model) +public partial class ModelImporter(ModelRoot model, IoNotifier notifier) { - public static MdlFile Import(ModelRoot model) + public static MdlFile Import(ModelRoot model, IoNotifier notifier) { - var importer = new ModelImporter(model); + var importer = new ModelImporter(model, notifier); return importer.Create(); } @@ -39,8 +40,8 @@ public partial class ModelImporter(ModelRoot model) private MdlFile Create() { // Group and build out meshes in this model. - foreach (var subMeshNodes in GroupedMeshNodes()) - BuildMeshForGroup(subMeshNodes); + foreach (var (subMeshNodes, index) in GroupedMeshNodes().WithIndex()) + BuildMeshForGroup(subMeshNodes, index); // Now that all the meshes have been built, we can build some of the model-wide metadata. var materials = _materials.Count > 0 ? _materials : ["/NO_MATERIAL"]; @@ -128,7 +129,7 @@ public partial class ModelImporter(ModelRoot model) ) .OrderBy(group => group.Key); - private void BuildMeshForGroup(IEnumerable subMeshNodes) + private void BuildMeshForGroup(IEnumerable subMeshNodes, int index) { // Record some offsets we'll be using later, before they get mutated with mesh values. var subMeshOffset = _subMeshes.Count; @@ -136,7 +137,7 @@ public partial class ModelImporter(ModelRoot model) var indexOffset = _indices.Count; var shapeValueOffset = _shapeValues.Count; - var mesh = MeshImporter.Import(subMeshNodes); + var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); var materialIndex = mesh.Material != null @@ -196,7 +197,7 @@ public partial class ModelImporter(ModelRoot model) // arrays, values is practically guaranteed to be the highest of the // group, so a failure on any of them will be a failure on it. if (_shapeValues.Count > ushort.MaxValue) - throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); + throw notifier.Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort GetMaterialIndex(string materialName) @@ -232,7 +233,7 @@ public partial class ModelImporter(ModelRoot model) } if (boneIndices.Count > 64) - throw new Exception("XIV does not support meshes weighted to more than 64 bones."); + throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones."); var boneIndicesArray = new ushort[64]; Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 6a5d0d52..a7e0d583 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -28,12 +28,14 @@ public class SubMeshImporter public Dictionary> ShapeValues; } - public static SubMesh Import(Node node, IDictionary? nodeBoneMap) + public static SubMesh Import(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { - var importer = new SubMeshImporter(node, nodeBoneMap); + var importer = new SubMeshImporter(node, nodeBoneMap, notifier); return importer.Create(); } + private readonly IoNotifier _notifier; + private readonly MeshPrimitive _primitive; private readonly IDictionary? _nodeBoneMap; private readonly IDictionary? _nodeExtras; @@ -53,16 +55,15 @@ public class SubMeshImporter private readonly List? _morphNames; private Dictionary>? _shapeValues; - private SubMeshImporter(Node node, IDictionary? nodeBoneMap) + private SubMeshImporter(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { + _notifier = notifier; + var mesh = node.Mesh; var primitiveCount = mesh.Primitives.Count; if (primitiveCount != 1) - { - var name = node.Name ?? mesh.Name ?? "(no name)"; - throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1."); - } + throw _notifier.Exception($"Mesh has {primitiveCount} primitives, expected 1."); _primitive = mesh.Primitives[0]; _nodeBoneMap = nodeBoneMap; @@ -115,7 +116,7 @@ public class SubMeshImporter { < 32 => (1u << _metaAttributes.Length) - 1, 32 => uint.MaxValue, - > 32 => throw new Exception("Models may utilise a maximum of 32 attributes."), + > 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."), }; return new SubMesh() @@ -165,11 +166,11 @@ public class SubMeshImporter // The order here is chosen to match a typical model's element order. var rawAttributes = new[] { - VertexAttribute.Position(accessors, morphAccessors), - VertexAttribute.BlendWeight(accessors), - VertexAttribute.BlendIndex(accessors, _nodeBoneMap), + VertexAttribute.Position(accessors, morphAccessors, _notifier), + VertexAttribute.BlendWeight(accessors, _notifier), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier), VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors, _indices), + VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 7c875162..b73f6a89 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -72,10 +72,10 @@ public class VertexAttribute private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex) => Build(vertexIndex); - public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors) + public static VertexAttribute Position(Accessors accessors, IEnumerable morphAccessors, IoNotifier notifier) { if (!accessors.TryGetValue("POSITION", out var accessor)) - throw new Exception("Meshes must contain a POSITION attribute."); + throw notifier.Exception("Meshes must contain a POSITION attribute."); var element = new MdlStructs.VertexElement() { @@ -115,13 +115,13 @@ public class VertexAttribute ); } - public static VertexAttribute? BlendWeight(Accessors accessors) + public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) - throw new Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); + throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); var element = new MdlStructs.VertexElement() { @@ -138,16 +138,16 @@ public class VertexAttribute ); } - public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap) + public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { if (!accessors.TryGetValue("JOINTS_0", out var accessor)) return null; if (!accessors.ContainsKey("WEIGHTS_0")) - throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); + throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) - throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); + throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); var element = new MdlStructs.VertexElement() { @@ -242,22 +242,22 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices, IoNotifier notifier) { if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) { - Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents."); + notifier.Warning("Normals are required to facilitate import or calculation of tangents."); return null; } var normals = normalAccessor.AsVector3Array(); var tangents = accessors.TryGetValue("TANGENT", out var accessor) ? accessor.AsVector4Array() - : CalculateTangents(accessors, indices, normals); + : CalculateTangents(accessors, indices, normals, notifier); if (tangents == null) { - Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); + notifier.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); return null; } @@ -309,7 +309,7 @@ public class VertexAttribute } /// Attempt to calculate tangent values based on other pre-existing data. - private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals) + private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals, IoNotifier notifier) { // To calculate tangents, we will also need access to uv data. if (!accessors.TryGetValue("TEXCOORD_0", out var uvAccessor)) @@ -318,8 +318,7 @@ public class VertexAttribute var positions = accessors["POSITION"].AsVector3Array(); var uvs = uvAccessor.AsVector2Array(); - // TODO: Surface this in the UI. - Penumbra.Log.Warning( + notifier.Warning( "Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools."); var vertexCount = positions.Count; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index ffcb5bbe..c41f28e5 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -43,10 +43,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect action => action.Notifier ); - public Task ImportGltf(string inputPath) + public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath) => EnqueueWithResult( new ImportGltfAction(inputPath), - action => action.Out + action => (action.Out, action.Notifier) ); /// Try to find the .sklb paths for a .mdl file. @@ -273,12 +273,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private partial class ImportGltfAction(string inputPath) : IAction { public MdlFile? Out; + public IoNotifier Notifier = new IoNotifier(); public void Execute(CancellationToken cancel) { var model = Schema2.ModelRoot.Load(inputPath); - Out = ModelImporter.Import(model); + Out = ModelImporter.Import(model, Notifier); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 17b46626..6decd344 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -2,6 +2,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; +using Penumbra.Import.Models; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -146,11 +147,14 @@ public partial class ModEditWindow { PendingIo = true; _edit._models.ImportGltf(inputPath) - .ContinueWith(task => + .ContinueWith((Task<(MdlFile?, IoNotifier)> task) => { RecordIoExceptions(task.Exception); - if (task is { IsCompletedSuccessfully: true, Result: not null }) - FinalizeImport(task.Result); + if (task is { IsCompletedSuccessfully: true, Result: (not null, _) }) + { + IoWarnings = task.Result.Item2.GetWarnings().ToList(); + FinalizeImport(task.Result.Item1); + } PendingIo = false; }); } From aa01acd76a8fd9b37cfbadcb45e791a3c4fa43bf Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 19:41:00 +1100 Subject: [PATCH 1480/2451] Move off messager --- Penumbra/Import/Models/IoNotifier.cs | 34 +++++++++------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/Penumbra/Import/Models/IoNotifier.cs b/Penumbra/Import/Models/IoNotifier.cs index e1d649f6..56ef7103 100644 --- a/Penumbra/Import/Models/IoNotifier.cs +++ b/Penumbra/Import/Models/IoNotifier.cs @@ -1,19 +1,11 @@ -using Dalamud.Interface.Internal.Notifications; -using OtterGui.Classes; +using OtterGui.Log; namespace Penumbra.Import.Models; public record class IoNotifier { - /// Notification subclass so that we have a distinct type to filter by. - private class LegallyDistinctNotification : Notification - { - public LegallyDistinctNotification(string content, NotificationType type): base(content, type) - {} - } - - private readonly DateTime _startTime = DateTime.UtcNow; - private string _context = ""; + private readonly List _messages = []; + private string _context = ""; /// Create a new notifier with the specified context appended to any other context already present. public IoNotifier WithContext(string context) @@ -21,12 +13,12 @@ public record class IoNotifier /// Send a warning with any current context to notification channels. public void Warning(string content) - => SendNotification(content, NotificationType.Warning); + => SendMessage(content, Logger.LogLevel.Warning); /// Get the current warnings for this notifier. /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. public IEnumerable GetWarnings() - => GetFilteredNotifications(NotificationType.Warning); + => _messages; /// Create an exception with any current context. [StackTraceHidden] @@ -39,14 +31,10 @@ public record class IoNotifier where TException : Exception, new() => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; - private void SendNotification(string message, NotificationType type) - => Penumbra.Messager.AddMessage( - new LegallyDistinctNotification($"{_context}{message}", type), - true, false, true, false - ); - - private IEnumerable GetFilteredNotifications(NotificationType type) - => Penumbra.Messager - .Where(p => p.Key >= _startTime && p.Value is LegallyDistinctNotification && p.Value.NotificationType == type) - .Select(p => p.Value.PrintMessage); + private void SendMessage(string message, Logger.LogLevel type) + { + var fullText = $"{_context}{message}"; + Penumbra.Log.Message(type, fullText); + _messages.Add(fullText); + } } From 0486d049b0dbec021d9f9a3497eecf8980e33473 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Jan 2024 22:20:54 +1100 Subject: [PATCH 1481/2451] Make material export fallible --- .../Import/Models/Export/MaterialExporter.cs | 6 +++ .../Import/Models/Export/ModelExporter.cs | 11 ++-- Penumbra/Import/Models/ModelManager.cs | 53 +++++++++++++------ .../ModEditWindow.Models.MdlTab.cs | 8 +-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 61609bb5..307e9d2b 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -22,6 +22,12 @@ public class MaterialExporter // variant? } + /// Dependency-less material configuration, for use when no material data can be resolved. + public static readonly MaterialBuilder Unknown = new MaterialBuilder("UNKNOWN") + .WithMetallicRoughnessShader() + .WithDoubleSide(true) + .WithBaseColor(Vector4.One); + /// Build a glTF material from a hydrated XIV model, with the provided name. public static MaterialBuilder Export(Material material, string name, IoNotifier notifier) { diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 550aaf11..9bc33697 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -55,9 +55,14 @@ public class ModelExporter /// Build materials for each of the material slots in the .mdl. private static MaterialBuilder[] ConvertMaterials(MdlFile mdl, Dictionary rawMaterials, IoNotifier notifier) => mdl.Materials - // TODO: material generation should be fallible, which means this lookup should be a tryget, with a fallback. - // fallback can likely be a static on the material exporter. - .Select(name => MaterialExporter.Export(rawMaterials[name], name, notifier.WithContext($"Material {name}"))) + .Select(name => + { + if (rawMaterials.TryGetValue(name, out var rawMaterial)) + return MaterialExporter.Export(rawMaterial, name, notifier.WithContext($"Material {name}")); + + notifier.Warning($"Material \"{name}\" missing, using blank fallback."); + return MaterialExporter.Unknown; + }) .ToArray(); /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index c41f28e5..bfd55281 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -106,7 +106,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. - private string ResolveMtrlPath(string rawPath) + private string? ResolveMtrlPath(string rawPath, IoNotifier notifier) { // TODO: this should probably be chosen in the export settings var variantId = 1; @@ -119,13 +119,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ? rawPath : '/' + Path.GetFileName(rawPath); - // TODO: this should be a recoverable warning if (absolutePath == null) - throw new Exception("Failed to resolve material path."); + { + notifier.Warning($"Material path \"{rawPath}\" could not be resolved."); + return null; + } var info = parser.GetFileInfo(absolutePath); if (info.FileType is not FileType.Material) - throw new Exception($"Material path {rawPath} does not conform to material conventions."); + { + notifier.Warning($"Material path {rawPath} does not conform to material conventions."); + return null; + } var resolvedPath = info.ObjectType switch { @@ -179,7 +184,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ModelManager manager, MdlFile mdl, IEnumerable sklbPaths, - Func read, + Func read, string outputPath) : IAction { @@ -193,10 +198,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var xivSkeletons = BuildSkeletons(cancel); Penumbra.Log.Debug("[GLTF Export] Reading materials..."); - var materials = mdl.Materials.ToDictionary( - path => path, - path => BuildMaterial(path, cancel) - ); + var materials = mdl.Materials + .Select(path => (path, material: BuildMaterial(path, Notifier, cancel))) + .Where(pair => pair.material != null) + .ToDictionary(pair => pair.path, pair => pair.material!.Value); Penumbra.Log.Debug("[GLTF Export] Converting model..."); var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); @@ -215,7 +220,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private IEnumerable BuildSkeletons(CancellationToken cancel) { var havokTasks = sklbPaths - .Select(path => new SklbFile(read(path))) + .Select(path => read(path) ?? throw new Exception( + $"Resolved skeleton \"{path}\" could not be read. Ensure EST metadata is configured, and/or relevant mods are enabled in the current collection.")) + .Select(bytes => new SklbFile(bytes)) .WithIndex() .Select(CreateHavokTask) .ToArray(); @@ -234,10 +241,15 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect } /// Read a .mtrl and populate its textures. - private MaterialExporter.Material BuildMaterial(string relativePath, CancellationToken cancel) + private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel) { - var path = manager.ResolveMtrlPath(relativePath); - var mtrl = new MtrlFile(read(path)); + var path = manager.ResolveMtrlPath(relativePath, notifier); + if (path == null) + return null; + var bytes = read(path); + if (bytes == null) + return null; + var mtrl = new MtrlFile(bytes); return new MaterialExporter.Material { @@ -254,12 +266,23 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect { // Work out the texture's path - the DX11 material flag controls a file name prefix. GamePaths.Tex.HandleDx11Path(texture, out var texturePath); - using var textureData = new MemoryStream(read(texturePath)); + var bytes = read(texturePath); + if (bytes == null) + return CreateDummyImage(); + + using var textureData = new MemoryStream(bytes); var image = TexFileParser.Parse(textureData); var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; return pngImage ?? throw new Exception("Failed to convert texture to png."); } + private Image CreateDummyImage() + { + var image = new Image(1, 1); + image[0, 0] = Color.White; + return image; + } + public bool Equals(IAction? other) { if (other is not ExportToGltfAction rhs) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 6decd344..cb8e662f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -260,7 +260,7 @@ public partial class ModEditWindow /// Read a file from the active collection or game. /// Game path to the file to load. // TODO: Also look up files within the current mod regardless of mod state? - private byte[] ReadFile(string path) + private byte[]? ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped if (!Utf8GamePath.FromString(path, out var utf8Path, true)) @@ -269,13 +269,9 @@ public partial class ModEditWindow var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(utf8Path); // TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... - var bytes = resolvedPath.IsRooted + return resolvedPath.IsRooted ? File.ReadAllBytes(resolvedPath.FullName) : _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data; - - // TODO: some callers may not care about failures - handle exceptions separately? - return bytes ?? throw new Exception( - $"Resolved path {path} could not be found. If modded, is it enabled in the current collection?"); } /// Remove the material given by the index. From cbd99f833a1fd3f13ed149dea8bd1344049c56be Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 00:03:58 +1100 Subject: [PATCH 1482/2451] Allow export of missing bones with warnings --- Penumbra/Import/Models/Export/Config.cs | 6 +++ Penumbra/Import/Models/Export/MeshExporter.cs | 39 ++++++++++++------- .../Import/Models/Export/ModelExporter.cs | 12 +++--- Penumbra/Import/Models/Export/Skeleton.cs | 12 +++++- Penumbra/Import/Models/ModelManager.cs | 16 +++++--- .../ModEditWindow.Models.MdlTab.cs | 5 ++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++ 7 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 Penumbra/Import/Models/Export/Config.cs diff --git a/Penumbra/Import/Models/Export/Config.cs b/Penumbra/Import/Models/Export/Config.cs new file mode 100644 index 00000000..58329a1d --- /dev/null +++ b/Penumbra/Import/Models/Export/Config.cs @@ -0,0 +1,6 @@ +namespace Penumbra.Import.Models.Export; + +public struct ExportConfig +{ + public bool GenerateMissingBones; +} diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 71e8f082..83a0c3cf 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -13,14 +13,14 @@ namespace Penumbra.Import.Models.Export; public class MeshExporter { - public class Mesh(IEnumerable meshes, NodeBuilder[]? joints) + public class Mesh(IEnumerable meshes, GltfSkeleton? skeleton) { public void AddToScene(SceneBuilder scene) { foreach (var data in meshes) { - var instance = joints != null - ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints) + var instance = skeleton != null + ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints]) : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); var extras = new Dictionary(data.Attributes.Length); @@ -38,15 +38,16 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + public static Mesh Export(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { - var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names, notifier); - return new Mesh(self.BuildMeshes(), skeleton?.Joints); + var self = new MeshExporter(config, mdl, lod, meshIndex, materials, skeleton, notifier); + return new Mesh(self.BuildMeshes(), skeleton); } private const byte MaximumMeshBufferStreams = 3; - private readonly IoNotifier _notifier; + private readonly ExportConfig _config; + private readonly IoNotifier _notifier; private readonly MdlFile _mdl; private readonly byte _lod; @@ -63,8 +64,10 @@ public class MeshExporter private readonly Type _materialType; private readonly Type _skinningType; - private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary? boneNameMap, IoNotifier notifier) + // TODO: This signature is getting out of control. + private MeshExporter(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { + _config = config; _notifier = notifier; _mdl = mdl; _lod = lod; @@ -72,8 +75,8 @@ public class MeshExporter _material = materials[XivMesh.MaterialIndex]; - if (boneNameMap != null) - _boneIndexMap = BuildBoneIndexMap(boneNameMap); + if (skeleton != null) + _boneIndexMap = BuildBoneIndexMap(skeleton.Value); var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements .ToImmutableDictionary( @@ -94,7 +97,7 @@ public class MeshExporter } /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. - private Dictionary? BuildBoneIndexMap(IReadOnlyDictionary boneNameMap) + private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) @@ -107,8 +110,18 @@ public class MeshExporter foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; - if (!boneNameMap.TryGetValue(boneName, out var gltfBoneIndex)) - throw _notifier.Exception($"Armature does not contain bone \"{boneName}\". Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured."); + if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) + { + if (!_config.GenerateMissingBones) + throw _notifier.Exception( + $@"Armature does not contain bone ""{boneName}"". + Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured. + If this is a known issue with this model and you would like to export anyway, enable the ""Generate missing bones"" option." + ); + + (_, gltfBoneIndex) = skeleton.GenerateBone(boneName); + _notifier.Warning($"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); + } indexMap.Add((ushort)tableIndex, gltfBoneIndex); } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 9bc33697..b3e9c68d 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,16 +23,16 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton, Dictionary rawMaterials, IoNotifier notifier) + public static Model Export(ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) { - var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; + var gltfSkeleton = ConvertSkeleton(xivSkeletons); var materials = ConvertMaterials(mdl, rawMaterials, notifier); - var meshes = ConvertMeshes(mdl, materials, gltfSkeleton, notifier); + var meshes = ConvertMeshes(config, mdl, materials, gltfSkeleton, notifier); return new Model(meshes, gltfSkeleton); } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private static List ConvertMeshes(ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); @@ -44,7 +44,7 @@ public class ModelExporter for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++) { var meshIndex = (ushort)(lod.MeshIndex + meshOffset); - var mesh = MeshExporter.Export(mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); + var mesh = MeshExporter.Export(config, mdl, lodIndex, meshIndex, materials, skeleton, notifier.WithContext($"Mesh {meshIndex}")); meshes.Add(mesh); } } @@ -105,7 +105,7 @@ public class ModelExporter return new GltfSkeleton { Root = root, - Joints = [.. joints], + Joints = joints, Names = names, }; } diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs index fee107a0..ca72a1f8 100644 --- a/Penumbra/Import/Models/Export/Skeleton.cs +++ b/Penumbra/Import/Models/Export/Skeleton.cs @@ -28,8 +28,18 @@ public struct GltfSkeleton public NodeBuilder Root; /// Flattened list of skeleton nodes. - public NodeBuilder[] Joints; + public List Joints; /// Mapping of bone names to their index within the joints array. public Dictionary Names; + + public (NodeBuilder, int) GenerateBone(string name) + { + var node = new NodeBuilder(name); + var index = Joints.Count; + Names[name] = index; + Joints.Add(node); + Root.AddNode(node); + return (node, index); + } } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index bfd55281..5340d556 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,9 +37,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( - new ExportToGltfAction(this, mdl, sklbPaths, read, outputPath), + new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier ); @@ -182,6 +182,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private class ExportToGltfAction( ModelManager manager, + ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, @@ -204,7 +205,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect .ToDictionary(pair => pair.path, pair => pair.material!.Value); Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(mdl, xivSkeletons, materials, Notifier); + var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier); Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); @@ -219,10 +220,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Attempt to read out the pertinent information from the sklb file paths provided. private IEnumerable BuildSkeletons(CancellationToken cancel) { + // We're intentionally filtering failed reads here - the failure will + // be picked up, if relevant, when the model tries to create mappings + // for a bone in the failed sklb. var havokTasks = sklbPaths - .Select(path => read(path) ?? throw new Exception( - $"Resolved skeleton \"{path}\" could not be read. Ensure EST metadata is configured, and/or relevant mods are enabled in the current collection.")) - .Select(bytes => new SklbFile(bytes)) + .Select(path => read(path)) + .Where(bytes => bytes != null) + .Select(bytes => new SklbFile(bytes!)) .WithIndex() .Select(CreateHavokTask) .ToArray(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cb8e662f..5b2f024c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -3,6 +3,7 @@ using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; +using Penumbra.Import.Models.Export; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -20,6 +21,8 @@ public partial class ModEditWindow public bool ImportKeepMaterials; public bool ImportKeepAttributes; + public ExportConfig ExportConfig; + public List? GamePaths { get; private set; } public int GamePathIndex; @@ -131,7 +134,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklbPaths, ReadFile, outputPath) + _edit._models.ExportToGltf(ExportConfig, Mdl, sklbPaths, ReadFile, outputPath) .ContinueWith(task => { RecordIoExceptions(task.Exception); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1a200fdf..7304d3dd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -112,6 +112,14 @@ public partial class ModEditWindow DrawGamePathCombo(tab); + // ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportGenerateMissingBones); + ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportConfig.GenerateMissingBones); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Generate missing bones", + "WARNING: Enabling this option can result in unusable exported meshes.\n" + + "It is primarily intended to allow exporting models weighted to bones that do not exist.\n" + + "Before enabling, ensure dependencies are enabled in the current collection, and EST metadata is correctly configured."); + var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count ? tab.GamePaths[tab.GamePathIndex] : _customGamePath; From 0d3dde7df39306320a32a9ba31473ca92c01fa09 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 01:09:26 +1100 Subject: [PATCH 1483/2451] Tweaks --- Penumbra/Import/Models/Export/ModelExporter.cs | 2 +- Penumbra/Import/Models/Import/MeshImporter.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index b3e9c68d..e0c42d40 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -59,7 +59,7 @@ public class ModelExporter { if (rawMaterials.TryGetValue(name, out var rawMaterial)) return MaterialExporter.Export(rawMaterial, name, notifier.WithContext($"Material {name}")); - + notifier.Warning($"Material \"{name}\" missing, using blank fallback."); return MaterialExporter.Unknown; }) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 28a7a9c1..b6b146b5 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -216,7 +216,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw notifier.Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); + ?? throw notifier.Exception($"Mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 7304d3dd..43b26f10 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -112,7 +112,6 @@ public partial class ModEditWindow DrawGamePathCombo(tab); - // ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportGenerateMissingBones); ImGui.Checkbox("##exportGeneratedMissingBones", ref tab.ExportConfig.GenerateMissingBones); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker("Generate missing bones", From ae409c2cd1732f8889949d5f10865703285441b9 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 15:31:27 +1100 Subject: [PATCH 1484/2451] Fix shape value offset handling --- Penumbra/Import/Models/Import/ModelImporter.cs | 3 +-- Penumbra/Import/Models/Import/SubMeshImporter.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 1b7fdfa5..3c7e97c7 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -134,7 +134,6 @@ public partial class ModelImporter(ModelRoot model) var subMeshOffset = _subMeshes.Count; var vertexOffset = _vertexBuffer.Count; var indexOffset = _indices.Count; - var shapeValueOffset = _shapeValues.Count; var mesh = MeshImporter.Import(subMeshNodes); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); @@ -184,7 +183,7 @@ public partial class ModelImporter(ModelRoot model) shapeMeshes.Add(meshShapeKey.ShapeMesh with { MeshIndexOffset = meshStartIndex, - ShapeValueOffset = (uint)shapeValueOffset, + ShapeValueOffset = (uint)_shapeValues.Count, }); _shapeValues.AddRange(meshShapeKey.ShapeValues); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 6a5d0d52..023e5c2f 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -234,7 +234,7 @@ public class SubMeshImporter foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) { - // Each for a given mesh, each shape key contains a list of shape value mappings. + // For a given mesh, each shape key contains a list of shape value mappings. var shapeValues = new List(); foreach (var vertexIndex in modifiedVertices) From c11519c95ef223bd4964bfd94739dfecc470012a Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 15:32:44 +1100 Subject: [PATCH 1485/2451] Fix further content expanding other sections --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index ad609285..cc6493b6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -386,6 +386,8 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Further Content")) return false; + using var furtherContentId = ImRaii.PushId("furtherContent"); + using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) { if (table) From 655d1722c18fbd24b5a569129c6005c439d7102d Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 17:49:39 +1100 Subject: [PATCH 1486/2451] Fail soft on invalid attribute masks --- .../ModEditWindow.Models.MdlTab.cs | 27 +++++++++++++------ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 10 +++++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 26fcd1ee..98bd66d6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -14,7 +14,7 @@ public partial class ModEditWindow private readonly ModEditWindow _edit; public MdlFile Mdl { get; private set; } - private List[] _attributes; + private List?[] _attributes; public bool ImportKeepMaterials; public bool ImportKeepAttributes; @@ -290,15 +290,21 @@ public partial class ModEditWindow } /// Create a list of attributes per sub mesh. - private static List[] CreateAttributes(MdlFile mdl) - => mdl.SubMeshes.Select(s => Enumerable.Range(0, 32) - .Where(idx => ((s.AttributeIndexMask >> idx) & 1) == 1) - .Select(idx => mdl.Attributes[idx]) - .ToList() - ).ToArray(); + private static List?[] CreateAttributes(MdlFile mdl) + => mdl.SubMeshes.Select(s => + { + var maxAttribute = 31 - BitOperations.LeadingZeroCount(s.AttributeIndexMask); + // TODO: Research what results in this - it seems to primarily be reproducible on bgparts, is it garbage data, or an alternative usage of the value? + return maxAttribute < mdl.Attributes.Length + ? Enumerable.Range(0, 32) + .Where(idx => ((s.AttributeIndexMask >> idx) & 1) == 1) + .Select(idx => mdl.Attributes[idx]) + .ToList() + : null; + }).ToArray(); /// Obtain the attributes associated with a sub mesh by its index. - public IReadOnlyList GetSubMeshAttributes(int subMeshIndex) + public IReadOnlyList? GetSubMeshAttributes(int subMeshIndex) => _attributes[subMeshIndex]; /// Remove or add attributes from a sub mesh by its index. @@ -308,6 +314,8 @@ public partial class ModEditWindow public void UpdateSubMeshAttribute(int subMeshIndex, string? old, string? @new) { var attributes = _attributes[subMeshIndex]; + if (attributes == null) + return; if (old != null) attributes.Remove(old); @@ -325,6 +333,9 @@ public partial class ModEditWindow foreach (var (attributes, subMeshIndex) in _attributes.WithIndex()) { + if (attributes == null) + continue; + var mask = 0u; foreach (var attribute in attributes) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index cc6493b6..bc763d7d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -326,7 +326,7 @@ public partial class ModEditWindow // Sub meshes for (var subMeshOffset = 0; subMeshOffset < mesh.SubMeshCount; subMeshOffset++) - ret |= DrawSubMeshAttributes(tab, meshIndex, disabled, subMeshOffset); + ret |= DrawSubMeshAttributes(tab, meshIndex, subMeshOffset, disabled); return ret; } @@ -354,7 +354,7 @@ public partial class ModEditWindow return ret; } - private bool DrawSubMeshAttributes(MdlTab tab, int meshIndex, bool disabled, int subMeshOffset) + private bool DrawSubMeshAttributes(MdlTab tab, int meshIndex, int subMeshOffset, bool disabled) { using var _ = ImRaii.PushId(subMeshOffset); @@ -369,6 +369,12 @@ public partial class ModEditWindow var widget = _subMeshAttributeTagWidgets[subMeshIndex]; var attributes = tab.GetSubMeshAttributes(subMeshIndex); + if (attributes == null) + { + attributes = ["invalid attribute data"]; + disabled = true; + } + var tagIndex = widget.Draw(string.Empty, string.Empty, attributes, out var editedAttribute, !disabled); if (tagIndex < 0) From de08862a88c19dba406db90b7e3770386be68225 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Jan 2024 21:13:53 +1100 Subject: [PATCH 1487/2451] Add documentation links --- OtterGui | 2 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 92590901..5d0aed2b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9259090121b26f097948e7bbd83b32708ea0410d +Subproject commit 5d0aed2b32a61654321a6616689932635cb35dde diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bc763d7d..1fb744ae 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData; @@ -13,7 +14,9 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 4; + private const int MdlMaterialMaximum = 4; + private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#import"; + private const string MdlExportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#export"; private readonly FileEditor _modelTab; private readonly ModelManager _models; @@ -67,6 +70,8 @@ public partial class ModEditWindow private void DrawImport(MdlTab tab, Vector2 size, bool _1) { + using var id = ImRaii.PushId("import"); + _dragDropManager.CreateImGuiSource("ModelDragDrop", m => m.Extensions.Any(e => ValidModelExtensions.Contains(e.ToLowerInvariant())), m => { @@ -89,6 +94,9 @@ public partial class ModEditWindow if (success && paths.Count > 0) tab.Import(paths[0]); }, 1, _mod!.ModPath.FullName, false); + + ImGui.SameLine(); + DrawDocumentationLink(MdlImportDocumentation); } if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile)) @@ -97,6 +105,7 @@ public partial class ModEditWindow private void DrawExport(MdlTab tab, Vector2 size, bool _) { + using var id = ImRaii.PushId("export"); using var frame = ImRaii.FramedGroup("Export", size, headerPreIcon: FontAwesomeIcon.FileExport); if (tab.GamePaths == null) @@ -110,10 +119,10 @@ public partial class ModEditWindow } DrawGamePathCombo(tab); - var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count ? tab.GamePaths[tab.GamePathIndex] : _customGamePath; + if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo || gamePath.IsEmpty)) _fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), @@ -127,6 +136,9 @@ public partial class ModEditWindow _mod!.ModPath.FullName, false ); + + ImGui.SameLine(); + DrawDocumentationLink(MdlExportDocumentation); } private static void DrawIoExceptions(MdlTab tab) @@ -205,6 +217,24 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip("Right-Click to copy to clipboard.", ImGuiHoveredFlags.AllowWhenDisabled); } + private void DrawDocumentationLink(string address) + { + const string text = "Documentation →"; + + var framePadding = ImGui.GetStyle().FramePadding; + var width = ImGui.CalcTextSize(text).X + framePadding.X * 2; + + // Draw the link button. We set the background colour to transparent to mimic the look of a link. + using var color = ImRaii.PushColor(ImGuiCol.Button, 0x00000000); + CustomGui.DrawLinkButton(Penumbra.Messager, text, address, width); + + // Draw an underline for the text. + var lineStart = ImGui.GetItemRectMax(); + lineStart -= framePadding; + var lineEnd = lineStart with { X = ImGui.GetItemRectMin().X + framePadding.X }; + ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, 0xFFFFFFFF); + } + private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Materials")) From c752835d2c3d59692815b3f565c61b1a17bd3d71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 13:12:08 +0100 Subject: [PATCH 1488/2451] Make CopySettings save even for unused settings. --- Penumbra/Collections/Manager/CollectionEditor.cs | 9 +++++++-- .../Interop/Hooks/Objects/CharacterBaseDestructor.cs | 4 ++-- Penumbra/Interop/PathResolving/DrawObjectState.cs | 1 - 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index f0b4d509..73950942 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -156,9 +156,14 @@ public class CollectionEditor // Either copy the unused source settings directly if they are not inheriting, // or remove any unused settings for the target if they are inheriting. if (savedSettings != null) + { ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; - else - ((Dictionary)collection.UnusedSettings).Remove(targetName); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) + { + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } } return true; diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index fc6dbfe6..e01a6550 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -10,10 +10,10 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MtrlTab = -1000, } diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index dd4b03f2..b3ae108b 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Interop.Hooks; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.Objects; From 153b1e0d83170547ce77aa8e0ff2611a1b7d8f64 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 13:17:12 +0100 Subject: [PATCH 1489/2451] Meep --- OtterGui | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index 5d0aed2b..c6f101bb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5d0aed2b32a61654321a6616689932635cb35dde +Subproject commit c6f101bbef976b74eb651523445563dd81fafbaf diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1fb744ae..022f48f1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -419,11 +419,9 @@ public partial class ModEditWindow private static bool DrawOtherModelDetails(MdlFile file, bool _) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImRaii.CollapsingHeader("Further Content")) return false; - using var furtherContentId = ImRaii.PushId("furtherContent"); - using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) { if (table) From 3debd470643ccee23cdfc109ebb39c7012d7a020 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 13:33:51 +0100 Subject: [PATCH 1490/2451] stupid. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 022f48f1..d799834c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -419,7 +419,8 @@ public partial class ModEditWindow private static bool DrawOtherModelDetails(MdlFile file, bool _) { - if (!ImRaii.CollapsingHeader("Further Content")) + using var header = ImRaii.CollapsingHeader("Further Content"); + if (!header) return false; using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) From edad7d9ec97863ff066da3588698d7342397074e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 13:47:03 +0100 Subject: [PATCH 1491/2451] Update persistent links. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index d799834c..a12be0f6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -15,8 +15,8 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { private const int MdlMaterialMaximum = 4; - private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#import"; - private const string MdlExportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#export"; + private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; + private const string MdlExportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-25968400-ebe5-4861-b610-cb1556db7ec4"; private readonly FileEditor _modelTab; private readonly ModelManager _models; From 7db95995113a4545f8c0feb3a7d6a1666f2d9e62 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Jan 2024 16:06:33 +0100 Subject: [PATCH 1492/2451] Auto-format and stuff. --- Penumbra/Import/Models/Export/MeshExporter.cs | 16 +++++--- .../Import/Models/Export/ModelExporter.cs | 4 +- Penumbra/Import/Models/ModelManager.cs | 12 +++--- .../ModEditWindow.Models.MdlTab.cs | 38 +++++++++++-------- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++-- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 83a0c3cf..928c8670 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -38,7 +38,9 @@ public class MeshExporter public string[] Attributes; } - public static Mesh Export(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + public static Mesh Export(in ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, + GltfSkeleton? skeleton, + IoNotifier notifier) { var self = new MeshExporter(config, mdl, lod, meshIndex, materials, skeleton, notifier); return new Mesh(self.BuildMeshes(), skeleton); @@ -65,7 +67,8 @@ public class MeshExporter private readonly Type _skinningType; // TODO: This signature is getting out of control. - private MeshExporter(ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private MeshExporter(in ExportConfig config, MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, + GltfSkeleton? skeleton, IoNotifier notifier) { _config = config; _notifier = notifier; @@ -118,9 +121,10 @@ public class MeshExporter Ensure all dependencies are enabled in the current collection, and EST entries (if required) are configured. If this is a known issue with this model and you would like to export anyway, enable the ""Generate missing bones"" option." ); - + (_, gltfBoneIndex) = skeleton.GenerateBone(boneName); - _notifier.Warning($"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); + _notifier.Warning( + $"Generated missing bone \"{boneName}\". Vertices weighted to this bone will not move with the rest of the armature."); } indexMap.Add((ushort)tableIndex, gltfBoneIndex); @@ -144,7 +148,7 @@ public class MeshExporter .Take(XivMesh.SubMeshCount) .WithIndex() .Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices, - (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, + (int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount, subMesh.Value.AttributeIndexMask)) .ToArray(); } @@ -233,7 +237,7 @@ public class MeshExporter return new MeshData { - Mesh = meshBuilder, + Mesh = meshBuilder, Attributes = attributes, }; } diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index e0c42d40..55997ef8 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -23,7 +23,7 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) + public static Model Export(in ExportConfig config, MdlFile mdl, IEnumerable xivSkeletons, Dictionary rawMaterials, IoNotifier notifier) { var gltfSkeleton = ConvertSkeleton(xivSkeletons); var materials = ConvertMaterials(mdl, rawMaterials, notifier); @@ -32,7 +32,7 @@ public class ModelExporter } /// Convert a .mdl to a mesh (group) per LoD. - private static List ConvertMeshes(ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) + private static List ConvertMeshes(in ExportConfig config, MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton, IoNotifier notifier) { var meshes = new List(); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 5340d556..2c341c8b 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -189,7 +189,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect string outputPath) : IAction { - public IoNotifier Notifier = new IoNotifier(); + public readonly IoNotifier Notifier = new(); public void Execute(CancellationToken cancel) { @@ -224,7 +224,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // be picked up, if relevant, when the model tries to create mappings // for a bone in the failed sklb. var havokTasks = sklbPaths - .Select(path => read(path)) + .Select(read) .Where(bytes => bytes != null) .Select(bytes => new SklbFile(bytes!)) .WithIndex() @@ -280,7 +280,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return pngImage ?? throw new Exception("Failed to convert texture to png."); } - private Image CreateDummyImage() + private static Image CreateDummyImage() { var image = new Image(1, 1); image[0, 0] = Color.White; @@ -299,8 +299,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect private partial class ImportGltfAction(string inputPath) : IAction { - public MdlFile? Out; - public IoNotifier Notifier = new IoNotifier(); + public MdlFile? Out; + public readonly IoNotifier Notifier = new(); public void Execute(CancellationToken cancel) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 8b3a7040..f24464d1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -15,7 +15,7 @@ public partial class ModEditWindow { private readonly ModEditWindow _edit; - public MdlFile Mdl { get; private set; } + public MdlFile Mdl { get; private set; } private List?[] _attributes; public bool ImportKeepMaterials; @@ -43,7 +43,7 @@ public partial class ModEditWindow [MemberNotNull(nameof(Mdl), nameof(_attributes))] private void Initialize(MdlFile mdl) { - Mdl = mdl; + Mdl = mdl; _attributes = CreateAttributes(Mdl); } @@ -95,14 +95,14 @@ public partial class ModEditWindow task.ContinueWith(t => { RecordIoExceptions(t.Exception); - GamePaths = t.Result; - PendingIo = false; + GamePaths = t.Result; + PendingIo = false; }); } private EstManipulation[] GetCurrentEstManipulations() { - var mod = _edit._editor.Mod; + var mod = _edit._editor.Mod; var option = _edit._editor.Option; if (mod == null || option == null) return []; @@ -140,17 +140,17 @@ public partial class ModEditWindow RecordIoExceptions(task.Exception); if (task is { IsCompletedSuccessfully: true, Result: not null }) IoWarnings = task.Result.GetWarnings().ToList(); - PendingIo = false; + PendingIo = false; }); } - - /// Import a model from an interchange format. + + /// Import a model from an interchange format. /// Disk path to load model data from. public void Import(string inputPath) { PendingIo = true; _edit._models.ImportGltf(inputPath) - .ContinueWith((Task<(MdlFile?, IoNotifier)> task) => + .ContinueWith(task => { RecordIoExceptions(task.Exception); if (task is { IsCompletedSuccessfully: true, Result: (not null, _) }) @@ -158,6 +158,7 @@ public partial class ModEditWindow IoWarnings = task.Result.Item2.GetWarnings().ToList(); FinalizeImport(task.Result.Item1); } + PendingIo = false; }); } @@ -178,11 +179,11 @@ public partial class ModEditWindow // TODO: Add flag editing. newMdl.Flags1 = Mdl.Flags1; newMdl.Flags2 = Mdl.Flags2; - + Initialize(newMdl); _dirty = true; } - + /// Merge material configuration from the source onto the target. /// Model that will be updated. /// Model to copy material configuration from. @@ -218,10 +219,12 @@ public partial class ModEditWindow // to maintain semantic connection between mesh index and sub mesh attributes. if (meshIndex >= source.Meshes.Length) continue; + var sourceMesh = source.Meshes[meshIndex]; if (subMeshOffset >= sourceMesh.SubMeshCount) continue; + var sourceSubMesh = source.SubMeshes[sourceMesh.SubMeshIndex + subMeshOffset]; target.SubMeshes[subMeshIndex].AttributeIndexMask = sourceSubMesh.AttributeIndexMask; @@ -237,11 +240,13 @@ public partial class ModEditWindow foreach (var sourceElement in source.ElementIds) { - var sourceBone = source.Bones[sourceElement.ParentBoneName]; + var sourceBone = source.Bones[sourceElement.ParentBoneName]; var targetIndex = target.Bones.IndexOf(sourceBone); // Given that there's no means of authoring these at the moment, this should probably remain a hard error. if (targetIndex == -1) - throw new Exception($"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); + throw new Exception( + $"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); + elementIds.Add(sourceElement with { ParentBoneName = (uint)targetIndex, @@ -253,13 +258,14 @@ public partial class ModEditWindow private void RecordIoExceptions(Exception? exception) { - IoExceptions = exception switch { + IoExceptions = exception switch + { null => [], AggregateException ae => [.. ae.Flatten().InnerExceptions], _ => [exception], }; } - + /// Read a file from the active collection or game. /// Game path to the file to load. // TODO: Also look up files within the current mod regardless of mod state? @@ -297,7 +303,7 @@ public partial class ModEditWindow /// Create a list of attributes per sub mesh. private static List?[] CreateAttributes(MdlFile mdl) - => mdl.SubMeshes.Select(s => + => mdl.SubMeshes.Select(s => { var maxAttribute = 31 - BitOperations.LeadingZeroCount(s.AttributeIndexMask); // TODO: Research what results in this - it seems to primarily be reproducible on bgparts, is it garbage data, or an alternative usage of the value? diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 744e9ea2..561cbed7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -170,8 +170,7 @@ public partial class ModEditWindow using var exceptionNode = ImRaii.TreeNode(message); if (exceptionNode) { - ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); - ImGui.SameLine(); + using var indent = ImRaii.PushIndent(); ImGuiUtil.TextWrapped(exception.ToString()); } } @@ -202,9 +201,8 @@ public partial class ModEditWindow using var warningNode = ImRaii.TreeNode(firstLine); if (warningNode) { - ImGui.Dummy(new Vector2(ImGui.GetStyle().IndentSpacing, 0)); - ImGui.SameLine(); - ImGuiUtil.TextWrapped(warning.ToString()); + using var indent = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped(warning); } } } From 38d855684bb66c0ad5b42a5472bff7653ad98232 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Jan 2024 15:09:07 +0000 Subject: [PATCH 1493/2451] [CI] Updating repo.json for 1.0.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index aa03fe77..eef374b5 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.2", - "TestingAssemblyVersion": "1.0.0.2", + "AssemblyVersion": "1.0.0.3", + "TestingAssemblyVersion": "1.0.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ca393267f6ab467f5f05a24c06658e45afd10c3f Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 21 Jan 2024 19:33:20 +1100 Subject: [PATCH 1494/2451] Spike custom vertex attribute handling --- Penumbra/Import/Models/Export/MeshExporter.cs | 10 +- .../Import/Models/Export/VertexFragment.cs | 92 +++++++++++++++++++ .../Import/Models/Import/VertexAttribute.cs | 4 +- 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Import/Models/Export/VertexFragment.cs diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 928c8670..03734db8 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -373,7 +373,7 @@ public class MeshExporter return materialUsages switch { - (2, true) => typeof(VertexColor1Texture2), + (2, true) => typeof(VertexTexture2ColorFfxiv), (2, false) => typeof(VertexTexture2), (1, true) => typeof(VertexColor1Texture1), (1, false) => typeof(VertexTexture1), @@ -413,13 +413,13 @@ public class MeshExporter ); } - if (_materialType == typeof(VertexColor1Texture2)) + if (_materialType == typeof(VertexTexture2ColorFfxiv)) { var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); - return new VertexColor1Texture2( - ToVector4(attributes[MdlFile.VertexUsage.Color]), + return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), - new Vector2(uv.Z, uv.W) + new Vector2(uv.Z, uv.W), + ToVector4(attributes[MdlFile.VertexUsage.Color]) ); } diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs new file mode 100644 index 00000000..234844d8 --- /dev/null +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -0,0 +1,92 @@ +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Export; + +public struct VertexTexture2ColorFfxiv : IVertexCustom +{ + public const string FFXIV_COLOR = "_FFXIV_COLOR"; + + [VertexAttribute("TEXCOORD_0")] + public Vector2 TexCoord0; + + [VertexAttribute("TEXCOORD_1")] + public Vector2 TexCoord1; + + [VertexAttribute(FFXIV_COLOR, EncodingType.UNSIGNED_BYTE, false)] + public Vector4 FfxivColor; + + public int MaxColors => 0; + + public int MaxTextCoords => 2; + + private static readonly string[] CustomNames = [FFXIV_COLOR]; + public IEnumerable CustomAttributes => CustomNames; + + public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) + { + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; + FfxivColor = ffxivColor; + } + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + { + return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + } + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) TexCoord0 = coord; + if (setIndex == 1) TexCoord1 = coord; + if (setIndex >= 2) throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case FFXIV_COLOR: + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == FFXIV_COLOR && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { + } + + public void Validate() + { + var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b73f6a89..b8576108 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -395,7 +395,9 @@ public class VertexAttribute public static VertexAttribute Color(Accessors accessors) { - accessors.TryGetValue("COLOR_0", out var accessor); + // Try to retrieve the custom color attribute we use for export, falling back to the glTF standard name. + if (!accessors.TryGetValue("_FFXIV_COLOR", out var accessor)) + accessors.TryGetValue("COLOR_0", out accessor); var element = new MdlStructs.VertexElement() { From 8167907d91fe785f0f3c32a756fbe07d3e1a156a Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 22 Jan 2024 00:34:16 +1100 Subject: [PATCH 1495/2451] The other two --- Penumbra/Import/Models/Export/MeshExporter.cs | 16 +- .../Import/Models/Export/VertexFragment.cs | 161 +++++++++++++++++- 2 files changed, 163 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 03734db8..1c266e52 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -375,9 +375,9 @@ public class MeshExporter { (2, true) => typeof(VertexTexture2ColorFfxiv), (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexColor1Texture1), + (1, true) => typeof(VertexTexture1ColorFfxiv), (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColor1), + (0, true) => typeof(VertexColorFfxiv), (0, false) => typeof(VertexEmpty), _ => throw new Exception("Unreachable."), @@ -390,16 +390,16 @@ public class MeshExporter if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); - if (_materialType == typeof(VertexColor1)) - return new VertexColor1(ToVector4(attributes[MdlFile.VertexUsage.Color])); + if (_materialType == typeof(VertexColorFfxiv)) + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color])); if (_materialType == typeof(VertexTexture1)) return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV])); - if (_materialType == typeof(VertexColor1Texture1)) - return new VertexColor1Texture1( - ToVector4(attributes[MdlFile.VertexUsage.Color]), - ToVector2(attributes[MdlFile.VertexUsage.UV]) + if (_materialType == typeof(VertexTexture1ColorFfxiv)) + return new VertexTexture1ColorFfxiv( + ToVector2(attributes[MdlFile.VertexUsage.UV]), + ToVector4(attributes[MdlFile.VertexUsage.Color]) ); // XIV packs two UVs into a single vec4 attribute. diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 234844d8..27d2ab10 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -3,24 +3,173 @@ using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Export; +/* +Yeah, look, I tried to make this file less garbage. It's a little difficult. +Realistically, it will need to stick around until transforms/mutations are built +and there's reason to overhaul the export pipeline. +*/ + +public struct VertexColorFfxiv : IVertexCustom +{ + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] + public Vector4 FfxivColor; + + public int MaxColors => 0; + + public int MaxTextCoords => 0; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; + public IEnumerable CustomAttributes => CustomNames; + + public VertexColorFfxiv(Vector4 ffxivColor) + { + FfxivColor = ffxivColor; + } + + public void Add(in VertexMaterialDelta delta) + { + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetTexCoord(int setIndex, Vector2 coord) + { + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR": + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { + } + + public void Validate() + { + var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} + +public struct VertexTexture1ColorFfxiv : IVertexCustom +{ + [VertexAttribute("TEXCOORD_0")] + public Vector2 TexCoord0; + + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] + public Vector4 FfxivColor; + + public int MaxColors => 0; + + public int MaxTextCoords => 1; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; + public IEnumerable CustomAttributes => CustomNames; + + public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) + { + TexCoord0 = texCoord0; + FfxivColor = ffxivColor; + } + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + { + return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); + } + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) TexCoord0 = coord; + if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR": + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { + } + + public void Validate() + { + var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} + public struct VertexTexture2ColorFfxiv : IVertexCustom { - public const string FFXIV_COLOR = "_FFXIV_COLOR"; - [VertexAttribute("TEXCOORD_0")] public Vector2 TexCoord0; [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - [VertexAttribute(FFXIV_COLOR, EncodingType.UNSIGNED_BYTE, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] public Vector4 FfxivColor; public int MaxColors => 0; public int MaxTextCoords => 2; - private static readonly string[] CustomNames = [FFXIV_COLOR]; + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; public IEnumerable CustomAttributes => CustomNames; public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) @@ -60,7 +209,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom { switch (attributeName) { - case FFXIV_COLOR: + case "_FFXIV_COLOR": value = FfxivColor; return true; @@ -72,7 +221,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom public void SetCustomAttribute(string attributeName, object value) { - if (attributeName == FFXIV_COLOR && value is Vector4 valueVector4) + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) FfxivColor = valueVector4; } From 4183e29249ddce748902c6b50dea43e11b3f2d6d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 21 Jan 2024 18:33:57 +0100 Subject: [PATCH 1496/2451] Increase version. (Cargo Cult'd from Glamourer) --- Penumbra/Penumbra.csproj | 4 ++-- Penumbra/Penumbra.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f1956742..01b3d680 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -7,8 +7,8 @@ absolute gangstas Penumbra Copyright © 2022 - 1.0.0.0 - 1.0.0.0 + 9.0.0.1 + 9.0.0.1 bin\$(Configuration)\ true enable diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 28dbc90d..8173e001 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -3,7 +3,7 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.0", + "AssemblyVersion": "9.0.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], From b5a71ed7b3d37d630d4e6d95c8a2de63475cb95b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 21 Jan 2024 17:17:26 +0100 Subject: [PATCH 1497/2451] Add locale environment variables to support info --- Penumbra/Penumbra.cs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 706e4a01..9ecd2232 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -192,6 +192,8 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Enable Mods: `** {_config.EnableMods}\n"); sb.Append($"> **`Enable HTTP API: `** {_config.EnableHttpApi}\n"); sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n"); + if (Dalamud.Utility.Util.IsWine()) + sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n"); sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); @@ -243,4 +245,39 @@ public class Penumbra : IDalamudPlugin return sb.ToString(); } + + private static string CollectLocaleEnvironmentVariables() + { + var variableNames = new List(); + var variables = new Dictionary(StringComparer.Ordinal); + foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + var key = (string)variable.Key; + if (key.Equals("LANG", StringComparison.Ordinal) || key.StartsWith("LC_", StringComparison.Ordinal)) + { + variableNames.Add(key); + variables.Add(key, ((string?)variable.Value) ?? string.Empty); + } + } + + variableNames.Sort(); + + var pos = variableNames.IndexOf("LC_ALL"); + if (pos > 0) // If it's == 0, we're going to do a no-op. + { + variableNames.RemoveAt(pos); + variableNames.Insert(0, "LC_ALL"); + } + + pos = variableNames.IndexOf("LANG"); + if (pos >= 0 && pos < variableNames.Count - 1) + { + variableNames.RemoveAt(pos); + variableNames.Add("LANG"); + } + + return variableNames.Count == 0 + ? "None" + : string.Join(", ", variableNames.Select(name => $"`{name}={variables[name]}`")); + } } From c2e5499aef456fab9a2ba9ae13ab3a9d9a09eb03 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 22 Jan 2024 01:51:42 +0100 Subject: [PATCH 1498/2451] ClientStructs-ify some stuff --- .../ResolveContext.PathResolution.cs | 13 ++-- .../Interop/ResourceTree/ResolveContext.cs | 8 +-- .../Interop/Structs/CharacterBaseUtility.cs | 62 ------------------- .../Structs/ModelResourceHandleUtility.cs | 19 ------ Penumbra/Interop/Structs/StructExtensions.cs | 57 ++++++++++++----- Penumbra/Penumbra.cs | 1 - Penumbra/Services/ServiceManagerA.cs | 3 +- 7 files changed, 53 insertions(+), 110 deletions(-) delete mode 100644 Penumbra/Interop/Structs/CharacterBaseUtility.cs delete mode 100644 Penumbra/Interop/Structs/ModelResourceHandleUtility.cs diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 0ab1e0e3..66af5521 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -7,7 +7,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; -using static Penumbra.Interop.Structs.CharacterBaseUtility; +using static Penumbra.Interop.Structs.StructExtensions; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -71,7 +71,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveModelPathNative() { - var path = ResolveMdlPath(CharacterBase, SlotIndex); + var path = CharacterBase.Value->ResolveMdlPathAsByteString(SlotIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -139,8 +139,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - // TODO: Submit this (Monster->Variant) to ClientStructs - var variant = ResolveMaterialVariant(imc, ((byte*)CharacterBase.Value)[0x8F4]); + var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase.Value)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -196,7 +195,7 @@ internal partial record ResolveContext ByteString? path; try { - path = ResolveMtrlPath(CharacterBase, SlotIndex, mtrlFileName); + path = CharacterBase.Value->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) { @@ -277,7 +276,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) { - var path = ResolveSklbPath(CharacterBase, partialSkeletonIndex); + var path = CharacterBase.Value->ResolveSklbPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -305,7 +304,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveSkeletonParameterPathNative(uint partialSkeletonIndex) { - var path = ResolveSkpPath(CharacterBase, partialSkeletonIndex); + var path = CharacterBase.Value->ResolveSkpPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 431f1ac0..7a31c9dd 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -13,8 +13,6 @@ using Penumbra.GameData.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; -using static Penumbra.Interop.Structs.CharacterBaseUtility; -using static Penumbra.Interop.Structs.ModelResourceHandleUtility; using static Penumbra.Interop.Structs.StructExtensions; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; @@ -126,7 +124,7 @@ internal partial record ResolveContext( if (eid == null) return null; - if (!Utf8GamePath.FromByteString(ResolveEidPath(CharacterBase), out var path)) + if (!Utf8GamePath.FromByteString(CharacterBase.Value->ResolveEidPathAsByteString(), out var path)) return null; return GetOrCreateNode(ResourceType.Eid, 0, eid, path); @@ -137,7 +135,7 @@ internal partial record ResolveContext( if (imc == null) return null; - if (!Utf8GamePath.FromByteString(ResolveImcPath(CharacterBase, SlotIndex), out var path)) + if (!Utf8GamePath.FromByteString(CharacterBase.Value->ResolveImcPathAsByteString(SlotIndex), out var path)) return null; return GetOrCreateNode(ResourceType.Imc, 0, imc, path); @@ -174,7 +172,7 @@ internal partial record ResolveContext( if (mtrl == null) continue; - var mtrlFileName = GetMaterialFileNameBySlot(mdlResource, (uint)i); + var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i); var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName)); if (mtrlNode != null) { diff --git a/Penumbra/Interop/Structs/CharacterBaseUtility.cs b/Penumbra/Interop/Structs/CharacterBaseUtility.cs deleted file mode 100644 index c29f44a3..00000000 --- a/Penumbra/Interop/Structs/CharacterBaseUtility.cs +++ /dev/null @@ -1,62 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.String; - -namespace Penumbra.Interop.Structs; - -// TODO submit these to ClientStructs -public static unsafe class CharacterBaseUtility -{ - private const int PathBufferSize = 260; - - private const uint ResolveSklbPathVf = 72; - private const uint ResolveMdlPathVf = 73; - private const uint ResolveSkpPathVf = 74; - private const uint ResolveImcPathVf = 81; - private const uint ResolveMtrlPathVf = 82; - private const uint ResolveEidPathVf = 85; - - private static void* GetVFunc(CharacterBase* characterBase, uint vfIndex) - => ((void**)characterBase->VTable)[vfIndex]; - - private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex) - { - var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); - var pathBuffer = stackalloc byte[PathBufferSize]; - var path = vFunc(characterBase, pathBuffer, PathBufferSize); - return path != null ? new ByteString(path).Clone() : null; - } - - private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex) - { - var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); - var pathBuffer = stackalloc byte[PathBufferSize]; - var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex); - return path != null ? new ByteString(path).Clone() : null; - } - - private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex, byte* name) - { - var vFunc = (delegate* unmanaged)GetVFunc(characterBase, vfIndex); - var pathBuffer = stackalloc byte[PathBufferSize]; - var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex, name); - return path != null ? new ByteString(path).Clone() : null; - } - - public static ByteString? ResolveEidPath(CharacterBase* characterBase) - => ResolvePath(characterBase, ResolveEidPathVf); - - public static ByteString? ResolveImcPath(CharacterBase* characterBase, uint slotIndex) - => ResolvePath(characterBase, ResolveImcPathVf, slotIndex); - - public static ByteString? ResolveMdlPath(CharacterBase* characterBase, uint slotIndex) - => ResolvePath(characterBase, ResolveMdlPathVf, slotIndex); - - public static ByteString? ResolveMtrlPath(CharacterBase* characterBase, uint slotIndex, byte* mtrlFileName) - => ResolvePath(characterBase, ResolveMtrlPathVf, slotIndex, mtrlFileName); - - public static ByteString? ResolveSklbPath(CharacterBase* characterBase, uint partialSkeletonIndex) - => ResolvePath(characterBase, ResolveSklbPathVf, partialSkeletonIndex); - - public static ByteString? ResolveSkpPath(CharacterBase* characterBase, uint partialSkeletonIndex) - => ResolvePath(characterBase, ResolveSkpPathVf, partialSkeletonIndex); -} diff --git a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs deleted file mode 100644 index bcfa2fa2..00000000 --- a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Penumbra.GameData; - -namespace Penumbra.Interop.Structs; - -// TODO submit this to ClientStructs -public class ModelResourceHandleUtility -{ - public ModelResourceHandleUtility(IGameInteropProvider interop) - => interop.InitializeFromAttributes(this); - - [Signature(Sigs.GetMaterialFileNameBySlot)] - private static nint _getMaterialFileNameBySlot = nint.Zero; - - public static unsafe byte* GetMaterialFileNameBySlot(ModelResourceHandle* handle, uint slot) - => ((delegate* unmanaged)_getMaterialFileNameBySlot)(handle, slot); -} diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index d1a38ae4..3cd87424 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.STD; using Penumbra.String; @@ -5,20 +6,48 @@ namespace Penumbra.Interop.Structs; internal static class StructExtensions { - // TODO submit this to ClientStructs - public static unsafe ReadOnlySpan AsSpan(in this StdString str) - { - if (str.Length < 16) - { - fixed (StdString* pStr = &str) - { - return new(pStr->Buffer, (int)str.Length); - } - } - else - return new(str.BufferPtr, (int)str.Length); - } - public static unsafe ByteString AsByteString(in this StdString str) => ByteString.FromSpanUnsafe(str.AsSpan(), true); + + public static ByteString ResolveEidPathAsByteString(in this CharacterBase character) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); + } + + public static ByteString ResolveImcPathAsByteString(in this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); + } + + public static ByteString ResolveMdlPathAsByteString(in this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); + } + + public static unsafe ByteString ResolveMtrlPathAsByteString(in this CharacterBase character, uint slotIndex, byte* mtrlFileName) + { + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); + } + + public static ByteString ResolveSklbPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); + } + + public static ByteString ResolveSkpPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); + } + + private static unsafe ByteString ToOwnedByteString(byte* str) + => str == null ? ByteString.Empty : new ByteString(str).Clone(); + + private static ByteString ToOwnedByteString(ReadOnlySpan str) + => str.Length == 0 ? ByteString.Empty : ByteString.FromSpanUnsafe(str, true).Clone(); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9ecd2232..15b7ce56 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -76,7 +76,6 @@ public class Penumbra : IDalamudPlugin _communicatorService = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _services.GetService(); // Initialize because not required anywhere else. - _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); _services.GetService(); diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index 2b2bbf95..f25aac7c 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -99,8 +99,7 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() From d76411e66c6a4ea52c2fc3d3ad61630eb57317e5 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 23 Jan 2024 00:40:29 +1100 Subject: [PATCH 1499/2451] Fix bounding box calculations for models offset from the origin --- Penumbra/Import/Models/Import/BoundingBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Import/BoundingBox.cs b/Penumbra/Import/Models/Import/BoundingBox.cs index 34aac827..be47ebb8 100644 --- a/Penumbra/Import/Models/Import/BoundingBox.cs +++ b/Penumbra/Import/Models/Import/BoundingBox.cs @@ -5,8 +5,8 @@ namespace Penumbra.Import.Models.Import; /// Mutable representation of the bounding box surrouding a collection of vertices. public class BoundingBox { - private Vector3 _minimum = Vector3.Zero; - private Vector3 _maximum = Vector3.Zero; + private Vector3 _minimum = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + private Vector3 _maximum = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); /// Use the specified position to update this bounding box, expanding it if necessary. public void Merge(Vector3 position) From 363b22061318f69ff20463cd29e351ef6e927094 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jan 2024 16:53:13 +0100 Subject: [PATCH 1500/2451] Some improvements of the cutscene service. --- .../Interop/PathResolving/CutsceneService.cs | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 2eeefbd8..85944d74 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,8 +1,10 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.Interop.Hooks.Objects; +using Penumbra.String; namespace Penumbra.Interop.PathResolving; @@ -22,15 +24,19 @@ public sealed class CutsceneService : IService, IDisposable .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor) + public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, + IClientState clientState) { _objects = objects; _copyCharacter = copyCharacter; _characterDestructor = characterDestructor; _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); + if (clientState.IsGPosing) + RecoverGPoseActors(); } + /// /// Get the related actor to a cutscene actor. /// Does not check for valid input index. @@ -66,11 +72,28 @@ public sealed class CutsceneService : IService, IDisposable private unsafe void OnCharacterDestructor(Character* character) { - if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) - return; + if (character->GameObject.ObjectIndex < CutsceneStartIdx) + { + // Remove all associations for now non-existing actor. + for (var i = 0; i < _copiedCharacters.Length; ++i) + { + if (_copiedCharacters[i] == character->GameObject.ObjectIndex) + { + // A hack to deal with GPose actors leaving and thus losing the link, we just set the home world instead. + // I do not think this breaks anything? + var address = (GameObject*)_objects.GetObjectAddress(i + CutsceneStartIdx); + if (address != null && address->GetObjectKind() is (byte)ObjectKind.Pc) + ((Character*)address)->HomeWorld = character->HomeWorld; - var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; - _copiedCharacters[idx] = -1; + _copiedCharacters[i] = -1; + } + } + } + else if (character->GameObject.ObjectIndex < CutsceneEndIdx) + { + var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = -1; + } } private unsafe void OnCharacterCopy(Character* target, Character* source) @@ -81,4 +104,45 @@ public sealed class CutsceneService : IService, IDisposable var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } + + /// Try to recover GPose actors on reloads into a running game. + /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. + private unsafe void RecoverGPoseActors() + { + Dictionary? actors = null; + + for (var i = CutsceneStartIdx; i < CutsceneEndIdx; ++i) + { + if (!TryGetName(i, out var name)) + continue; + + if ((actors ??= CreateActors()).TryGetValue(name, out var idx)) + _copiedCharacters[i - CutsceneStartIdx] = idx; + } + + return; + + bool TryGetName(int idx, out ByteString name) + { + name = ByteString.Empty; + var address = (GameObject*)_objects.GetObjectAddress(idx); + if (address == null) + return false; + + name = new ByteString(address->Name); + return !name.IsEmpty; + } + + Dictionary CreateActors() + { + var ret = new Dictionary(); + for (short i = 0; i < CutsceneStartIdx; ++i) + { + if (TryGetName(i, out var name)) + ret.TryAdd(name, i); + } + + return ret; + } + } } From b79fbc7653b5d91f17f4e56f7cf33db70fc3c03f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jan 2024 16:53:35 +0100 Subject: [PATCH 1501/2451] Some cleanup. --- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 6 ++-- Penumbra/Mods/Subclasses/ModSettings.cs | 38 +++++++++------------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index ff722945..e229738d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -95,10 +95,10 @@ public class ItemSwapContainer public void LoadMod(Mod? mod, ModSettings? settings) { Clear(); - if (mod == null) + if (mod == null || mod.Index < 0) { - _modRedirections = new Dictionary(); - _modManipulations = new HashSet(); + _modRedirections = []; + _modManipulations = []; } else { diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 122c6d29..71a56c80 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -11,7 +11,7 @@ namespace Penumbra.Mods.Subclasses; public class ModSettings { public static readonly ModSettings Empty = new(); - public List< uint > Settings { get; private init; } = new(); + public List< uint > Settings { get; private init; } = []; public int Priority { get; set; } public bool Enabled { get; set; } @@ -21,7 +21,7 @@ public class ModSettings { Enabled = Enabled, Priority = Priority, - Settings = Settings.ToList(), + Settings = [.. Settings], }; // Create default settings for a given mod. @@ -36,31 +36,14 @@ public class ModSettings // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. public static (Dictionary< Utf8GamePath, FullPath >, HashSet< MetaManipulation >) GetResolveData( Mod mod, ModSettings? settings ) { - if( settings == null ) - { - settings = DefaultSettings( mod ); - } + if (settings == null) + settings = DefaultSettings(mod); else - { settings.AddMissingSettings( mod ); - } var dict = new Dictionary< Utf8GamePath, FullPath >(); var set = new HashSet< MetaManipulation >(); - void AddOption( ISubMod option ) - { - foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) ) - { - dict.TryAdd( path, file ); - } - - foreach( var manip in option.Manipulations ) - { - set.Add( manip ); - } - } - foreach( var (group, index) in mod.Groups.WithIndex().OrderByDescending( g => g.Value.Priority ) ) { if( group.Type is GroupType.Single ) @@ -81,6 +64,19 @@ public class ModSettings AddOption( mod.Default ); return ( dict, set ); + + void AddOption( ISubMod option ) + { + foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) ) + { + dict.TryAdd( path, file ); + } + + foreach( var manip in option.Manipulations ) + { + set.Add( manip ); + } + } } // Automatically react to changes in a mods available options. From 77762734d7c759a4dd0bd5bf3cdfc65542f8d5c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jan 2024 16:59:51 +0100 Subject: [PATCH 1502/2451] Some Cleanup. --- .../Interop/ResourceTree/ResolveContext.cs | 118 ++++++++---------- 1 file changed, 49 insertions(+), 69 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 7a31c9dd..acac9a6f 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -6,7 +6,6 @@ using FFXIVClientStructs.Interop; using OtterGui; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -27,20 +26,23 @@ internal record GlobalResolveContext(ObjectIdentification Identifier, ModCollect => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } -internal partial record ResolveContext( +internal unsafe partial record ResolveContext( GlobalResolveContext Global, - Pointer CharacterBase, + Pointer CharacterBasePointer, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { + public CharacterBase* CharacterBase + => CharacterBasePointer.Value; + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private unsafe ModelType ModelType - => CharacterBase.Value->GetModelType(); + private ModelType ModelType + => CharacterBase->GetModelType(); - private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) + private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) { if (resourceHandle == null) return null; @@ -52,7 +54,7 @@ internal partial record ResolveContext( return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } - private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) + private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) { if (resourceHandle == null) return null; @@ -88,7 +90,7 @@ internal partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); } - private unsafe ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath) { if (resourceHandle == null) @@ -100,7 +102,7 @@ internal partial record ResolveContext( return CreateNode(type, objectAddress, resourceHandle, gamePath); } - private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool autoAdd = true) { if (resourceHandle == null) @@ -119,29 +121,29 @@ internal partial record ResolveContext( return node; } - public unsafe ResourceNode? CreateNodeFromEid(ResourceHandle* eid) + public ResourceNode? CreateNodeFromEid(ResourceHandle* eid) { if (eid == null) return null; - if (!Utf8GamePath.FromByteString(CharacterBase.Value->ResolveEidPathAsByteString(), out var path)) + if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path)) return null; return GetOrCreateNode(ResourceType.Eid, 0, eid, path); } - public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) + public ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { if (imc == null) return null; - if (!Utf8GamePath.FromByteString(CharacterBase.Value->ResolveImcPathAsByteString(SlotIndex), out var path)) + if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path)) return null; return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } - public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) + public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { if (tex == null) return null; @@ -152,7 +154,7 @@ internal partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } - public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) + public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) { if (mdl == null || mdl->ModelResourceHandle == null) return null; @@ -187,30 +189,8 @@ internal partial record ResolveContext( return node; } - private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path) + private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path) { - static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet alreadyVisitedSamplerIds) - { - if ((texFlags & 0x001F) != 0x001F && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[texFlags & 0x001F].Id)) - return (ushort)(texFlags & 0x001F); - if ((texFlags & 0x03E0) != 0x03E0 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 5) & 0x001F].Id)) - return (ushort)((texFlags >> 5) & 0x001F); - if ((texFlags & 0x7C00) != 0x7C00 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 10) & 0x001F].Id)) - return (ushort)((texFlags >> 10) & 0x001F); - - return 0x001F; - } - - static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) - => mtrl->TexturesSpan.FindFirst(p => p.Texture == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) - ? p.Id - : null; - - static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - => shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s) - ? s.CRC - : null; - if (mtrl == null || mtrl->MaterialResourceHandle == null) return null; @@ -219,9 +199,6 @@ internal partial record ResolveContext( return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); - if (node == null) - return null; - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); if (shpkNode != null) { @@ -247,11 +224,9 @@ internal partial record ResolveContext( if (shpk != null) { var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); - uint? samplerId; - if (index != 0x001F) - samplerId = mtrl->Textures[index].Id; - else - samplerId = GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds); + var samplerId = index != 0x001F + ? mtrl->Textures[index].Id + : GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds); if (samplerId.HasValue) { alreadyProcessedSamplerIds.Add(samplerId.Value); @@ -271,9 +246,31 @@ internal partial record ResolveContext( Global.Nodes.Add((path, (nint)resource), node); return node; + + static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) + => shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s) + ? s.CRC + : null; + + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) + => mtrl->TexturesSpan.FindFirst(p => p.Texture == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) + ? p.Id + : null; + + static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet alreadyVisitedSamplerIds) + { + if ((texFlags & 0x001F) != 0x001F && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[texFlags & 0x001F].Id)) + return (ushort)(texFlags & 0x001F); + if ((texFlags & 0x03E0) != 0x03E0 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 5) & 0x001F].Id)) + return (ushort)((texFlags >> 5) & 0x001F); + if ((texFlags & 0x7C00) != 0x7C00 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 10) & 0x001F].Id)) + return (ushort)((texFlags >> 10) & 0x001F); + + return 0x001F; + } } - public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) + public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonResourceHandle == null) return null; @@ -283,19 +280,10 @@ internal partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); - if (node != null) - { - var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); - if (skpNode != null) - node.Children.Add(skpNode); - Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); - } - - return node; + return CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); } - private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) + private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; @@ -305,15 +293,7 @@ internal partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); - if (node != null) - { - if (Global.WithUiData) - node.FallbackName = "Skeleton Parameters"; - Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node); - } - - return node; + return CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); } internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) @@ -363,7 +343,7 @@ internal partial record ResolveContext( return i >= 0 && i < array.Length ? array[i] : null; } - internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true) + internal static ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true) { if (handle == null) return ByteString.Empty; @@ -384,7 +364,7 @@ internal partial record ResolveContext( return name; } - private static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + private static ulong GetResourceHandleLength(ResourceHandle* handle) { if (handle == null) return 0; From 7b1e28c2cfc8658f912cc02c09bccdbcbe99ebbd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jan 2024 17:08:38 +0100 Subject: [PATCH 1503/2451] Woops. --- .../Interop/ResourceTree/ResolveContext.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index acac9a6f..0637cba6 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -198,7 +198,7 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); if (shpkNode != null) { @@ -223,9 +223,9 @@ internal unsafe partial record ResolveContext( string? name = null; if (shpk != null) { - var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); - var samplerId = index != 0x001F - ? mtrl->Textures[index].Id + var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); + var samplerId = index != 0x001F + ? mtrl->Textures[index].Id : GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds); if (samplerId.HasValue) { @@ -280,7 +280,13 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - return CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); + var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); + if (skpNode != null) + node.Children.Add(skpNode); + Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); + + return node; } private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) @@ -293,7 +299,12 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; - return CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); + var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); + if (Global.WithUiData) + node.FallbackName = "Skeleton Parameters"; + Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node); + + return node; } internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) From 6c93cc20df70987e0be7bf03bf5930412983b307 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jan 2024 17:24:08 +0100 Subject: [PATCH 1504/2451] Woops2 --- .../ResolveContext.PathResolution.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 66af5521..cf939292 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -33,7 +33,7 @@ internal partial record ResolveContext return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } - private unsafe GenderRace ResolveModelRaceCode() + private GenderRace ResolveModelRaceCode() => ResolveEqdpRaceCode(Slot, Equipment.Set); private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) @@ -42,7 +42,7 @@ internal partial record ResolveContext if (slotIndex >= 10 || ModelType != ModelType.Human) return GenderRace.MidlanderMale; - var characterRaceCode = (GenderRace)((Human*)CharacterBase.Value)->RaceSexId; + var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; if (characterRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; @@ -71,7 +71,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveModelPathNative() { - var path = CharacterBase.Value->ResolveMdlPathAsByteString(SlotIndex); + var path = CharacterBase->ResolveMdlPathAsByteString(SlotIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -139,7 +139,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase.Value)->Variant); + var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -195,11 +195,11 @@ internal partial record ResolveContext ByteString? path; try { - path = CharacterBase.Value->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); + path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) { - Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase.Value:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); + Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); return Utf8GamePath.Empty; } return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; @@ -217,7 +217,7 @@ internal partial record ResolveContext }; } - private unsafe Utf8GamePath ResolveHumanSkeletonPath(uint partialSkeletonIndex) + private Utf8GamePath ResolveHumanSkeletonPath(uint partialSkeletonIndex) { var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); if (set == 0) @@ -229,7 +229,7 @@ internal partial record ResolveContext private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex) { - var human = (Human*)CharacterBase.Value; + var human = (Human*)CharacterBase; var characterRaceCode = (GenderRace)human->RaceSexId; switch (partialSkeletonIndex) { @@ -262,21 +262,21 @@ internal partial record ResolveContext private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) { - var human = (Human*)CharacterBase.Value; + var human = (Human*)CharacterBase; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) + private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) { var metaCache = Global.Collection.MetaCache; - var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, primary); + var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; return (raceCode, EstManipulation.ToName(type), skeletonSet); } private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) { - var path = CharacterBase.Value->ResolveSklbPathAsByteString(partialSkeletonIndex); + var path = CharacterBase->ResolveSklbPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -304,7 +304,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveSkeletonParameterPathNative(uint partialSkeletonIndex) { - var path = CharacterBase.Value->ResolveSkpPathAsByteString(partialSkeletonIndex); + var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } } From 8487661bc80f4b19ffe5f695f6e38496a3be794e Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 23 Jan 2024 21:25:18 +1100 Subject: [PATCH 1505/2451] Increase imported vertex precision --- Penumbra.GameData | 2 +- Penumbra/Import/Models/Export/MeshExporter.cs | 4 +- .../Import/Models/Import/VertexAttribute.cs | 82 ++++++++++--------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ab09e21f..63f4de73 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ab09e21fa46be83f82c400dfd2fe05a281b6f28d +Subproject commit 63f4de7305616b6cb8921513e5d83baa8913353f diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 928c8670..d5e168a3 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -297,8 +297,8 @@ public class MeshExporter { MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - MdlFile.VertexType.UInt => reader.ReadBytes(4), - MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + MdlFile.VertexType.UByte4 => reader.ReadBytes(4), + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b73f6a89..74a29f72 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -30,12 +30,16 @@ public class VertexAttribute public byte Size => (MdlFile.VertexType)Element.Type switch { - MdlFile.VertexType.Single3 => 12, - MdlFile.VertexType.Single4 => 16, - MdlFile.VertexType.UInt => 4, - MdlFile.VertexType.ByteFloat4 => 4, - MdlFile.VertexType.Half2 => 4, - MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.Single1 => 4, + MdlFile.VertexType.Single2 => 8, + MdlFile.VertexType.Single3 => 12, + MdlFile.VertexType.Single4 => 16, + MdlFile.VertexType.UByte4 => 4, + MdlFile.VertexType.Short2 => 4, + MdlFile.VertexType.Short4 => 8, + MdlFile.VertexType.NByte4 => 4, + MdlFile.VertexType.Half2 => 4, + MdlFile.VertexType.Half4 => 8, _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; @@ -126,7 +130,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.ByteFloat4, + Type = (byte)MdlFile.VertexType.NByte4, Usage = (byte)MdlFile.VertexUsage.BlendWeights, }; @@ -134,7 +138,7 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildByteFloat4(values[index]) + index => BuildNByte4(values[index]) ); } @@ -152,7 +156,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 0, - Type = (byte)MdlFile.VertexType.UInt, + Type = (byte)MdlFile.VertexType.UByte4, Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; @@ -163,7 +167,7 @@ public class VertexAttribute index => { var gltfIndices = values[index]; - return BuildUInt(new Vector4( + return BuildUByte4(new Vector4( boneMap[(ushort)gltfIndices.X], boneMap[(ushort)gltfIndices.Y], boneMap[(ushort)gltfIndices.Z], @@ -181,7 +185,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.Half4, + Type = (byte)MdlFile.VertexType.Single3, Usage = (byte)MdlFile.VertexUsage.Normal, }; @@ -193,7 +197,7 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildHalf4(new Vector4(values[index], 0)), + index => BuildSingle3(values[index]), buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -202,7 +206,7 @@ public class VertexAttribute if (delta != null) value += delta.Value; - return BuildHalf4(new Vector4(value, 0)); + return BuildSingle3(value); } ); } @@ -224,20 +228,20 @@ public class VertexAttribute // There's only one TEXCOORD, output UV coordinates as vec2s. if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2)) return new VertexAttribute( - element with { Type = (byte)MdlFile.VertexType.Half2 }, - index => BuildHalf2(values1[index]) + element with { Type = (byte)MdlFile.VertexType.Single2 }, + index => BuildSingle2(values1[index]) ); var values2 = accessor2.AsVector2Array(); // Two TEXCOORDs are available, repack them into xiv's vec4 [0X, 0Y, 1X, 1Y] format. return new VertexAttribute( - element with { Type = (byte)MdlFile.VertexType.Half4 }, + element with { Type = (byte)MdlFile.VertexType.Single4 }, index => { var value1 = values1[index]; var value2 = values2[index]; - return BuildHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y)); + return BuildSingle4(new Vector4(value1.X, value1.Y, value2.X, value2.Y)); } ); } @@ -264,7 +268,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, + Type = (byte)MdlFile.VertexType.NByte4, Usage = (byte)MdlFile.VertexUsage.Tangent1, }; @@ -305,7 +309,7 @@ public class VertexAttribute // Byte floats encode 0..1, and bitangents are stored as -1..1. Convert. bitangent = (bitangent + Vector3.One) / 2; - return BuildByteFloat4(new Vector4(bitangent, handedness)); + return BuildNByte4(new Vector4(bitangent, handedness)); } /// Attempt to calculate tangent values based on other pre-existing data. @@ -400,7 +404,7 @@ public class VertexAttribute var element = new MdlStructs.VertexElement() { Stream = 1, - Type = (byte)MdlFile.VertexType.ByteFloat4, + Type = (byte)MdlFile.VertexType.NByte4, Usage = (byte)MdlFile.VertexUsage.Color, }; @@ -409,10 +413,17 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildByteFloat4(values?[index] ?? Vector4.One) + index => BuildNByte4(values?[index] ?? Vector4.One) ); } + private static byte[] BuildSingle2(Vector2 input) + => + [ + ..BitConverter.GetBytes(input.X), + ..BitConverter.GetBytes(input.Y), + ]; + private static byte[] BuildSingle3(Vector3 input) => [ @@ -421,7 +432,16 @@ public class VertexAttribute ..BitConverter.GetBytes(input.Z), ]; - private static byte[] BuildUInt(Vector4 input) + private static byte[] BuildSingle4(Vector4 input) + => + [ + ..BitConverter.GetBytes(input.X), + ..BitConverter.GetBytes(input.Y), + ..BitConverter.GetBytes(input.Z), + ..BitConverter.GetBytes(input.W), + ]; + + private static byte[] BuildUByte4(Vector4 input) => [ (byte)input.X, @@ -430,7 +450,7 @@ public class VertexAttribute (byte)input.W, ]; - private static byte[] BuildByteFloat4(Vector4 input) + private static byte[] BuildNByte4(Vector4 input) => [ (byte)Math.Round(input.X * 255f), @@ -438,20 +458,4 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; - - private static byte[] BuildHalf2(Vector2 input) - => - [ - ..BitConverter.GetBytes((Half)input.X), - ..BitConverter.GetBytes((Half)input.Y), - ]; - - private static byte[] BuildHalf4(Vector4 input) - => - [ - ..BitConverter.GetBytes((Half)input.X), - ..BitConverter.GetBytes((Half)input.Y), - ..BitConverter.GetBytes((Half)input.Z), - ..BitConverter.GetBytes((Half)input.W), - ]; } From b2bd31a1663b67d2bce4546c4f4aa7518c4bd04e Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 24 Jan 2024 01:54:56 +1100 Subject: [PATCH 1506/2451] Import all primitives of a mesh --- Penumbra/Import/Models/Import/MeshImporter.cs | 72 ++---- .../Import/Models/Import/PrimitiveImporter.cs | 210 +++++++++++++++ .../Import/Models/Import/SubMeshImporter.cs | 243 ++++++------------ Penumbra/Import/Models/Import/Utility.cs | 33 +++ .../ModEditWindow.Models.MdlTab.cs | 1 - 5 files changed, 341 insertions(+), 218 deletions(-) create mode 100644 Penumbra/Import/Models/Import/PrimitiveImporter.cs diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index b6b146b5..f76fa1ea 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,4 +1,5 @@ using Lumina.Data.Parsing; +using OtterGui; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -117,21 +118,19 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) var subMeshName = node.Name ?? node.Mesh.Name; - var nodeBoneMap = CreateNodeBoneMap(node); - var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}")); + var subNotifier = notifier.WithContext($"Sub-mesh {subMeshName}"); + var nodeBoneMap = CreateNodeBoneMap(node, subNotifier); + var subMesh = SubMeshImporter.Import(node, nodeBoneMap, subNotifier); - // TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh. _material ??= subMesh.Material; + if (subMesh.Material != null && _material != subMesh.Material) + notifier.Warning($"Meshes may only reference one material. Sub-mesh {subMeshName} material \"{subMesh.Material}\" has been ignored."); // Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution. if (_vertexDeclaration == null) _vertexDeclaration = subMesh.VertexDeclaration; - else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value)) - throw notifier.Exception( - $@"All sub-meshes of a mesh must have equivalent vertex declarations. - Current: {FormatVertexDeclaration(_vertexDeclaration.Value)} - Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}" - ); + else + Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, subMesh.VertexDeclaration, notifier); // Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine. // TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed. @@ -173,27 +172,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) }); } - private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) - => string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); - - private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) - { - var elA = a.VertexElements; - var elB = b.VertexElements; - - if (elA.Length != elB.Length) - return true; - - // NOTE: This assumes that elements will always be in the same order. Under the current implementation, that's guaranteed. - return elA.Zip(elB).Any(pair => - pair.First.Usage != pair.Second.Usage - || pair.First.Type != pair.Second.Type - || pair.First.Offset != pair.Second.Offset - || pair.First.Stream != pair.Second.Stream - ); - } - - private Dictionary? CreateNodeBoneMap(Node node) + private Dictionary? CreateNodeBoneMap(Node node, IoNotifier notifier) { // Unskinned assets can skip this all of this. if (node.Skin == null) @@ -205,32 +184,27 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) .Select(index => node.Skin.GetJoint(index).Joint.Name ?? "unnamed_joint") .ToArray(); - // TODO: This is duplicated with the sub mesh importer - would be good to avoid (not that it's a huge issue). - var mesh = node.Mesh; - var meshName = node.Name ?? mesh.Name ?? "(no name)"; - var primitiveCount = mesh.Primitives.Count; - if (primitiveCount != 1) - throw notifier.Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1."); - - var primitive = mesh.Primitives[0]; - - // Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw notifier.Exception($"Mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes."); - - // Build a set of joints that are referenced by this mesh. - // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. var usedJoints = new HashSet(); - foreach (var joints in jointsAccessor.AsVector4Array()) + + foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { - for (var index = 0; index < 4; index++) - usedJoints.Add((ushort)joints[index]); + // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") + ?? throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); + + // Build a set of joints that are referenced by this mesh. + // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. + foreach (var joints in jointsAccessor.AsVector4Array()) + { + for (var index = 0; index < 4; index++) + usedJoints.Add((ushort)joints[index]); + } } // Only initialise the bones list if we're actually going to put something in it. _bones ??= []; - // Build a dictionary of node-specific joint indices mesh-wide bone indices. + // Build a dictionary of node-specific joint indices mapped to mesh-wide bone indices. var nodeBoneMap = new Dictionary(); foreach (var usedJoint in usedJoints) { diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs new file mode 100644 index 00000000..9ac3d73c --- /dev/null +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -0,0 +1,210 @@ +using Lumina.Data.Parsing; +using OtterGui; +using SharpGLTF.Schema2; + +namespace Penumbra.Import.Models.Import; + +public class PrimitiveImporter +{ + public struct Primitive + { + public string? Material; + + public MdlStructs.VertexDeclarationStruct VertexDeclaration; + + public ushort VertexCount; + public byte[] Strides; + public List[] Streams; + + public ushort[] Indices; + + public BoundingBox BoundingBox; + + public List> ShapeValues; + } + + public static Primitive Import(MeshPrimitive primitive, IDictionary? nodeBoneMap, IoNotifier notifier) + { + var importer = new PrimitiveImporter(primitive, nodeBoneMap, notifier); + return importer.Create(); + } + + private readonly IoNotifier _notifier; + + private readonly MeshPrimitive _primitive; + private readonly IDictionary? _nodeBoneMap; + + private ushort[]? _indices; + + private List? _vertexAttributes; + + private ushort _vertexCount; + private byte[] _strides = [0, 0, 0]; + private readonly List[] _streams = [[], [], []]; + + private BoundingBox _boundingBox = new BoundingBox(); + + private List>? _shapeValues; + + private PrimitiveImporter(MeshPrimitive primitive, IDictionary? nodeBoneMap, IoNotifier notifier) + { + _notifier = notifier; + _primitive = primitive; + _nodeBoneMap = nodeBoneMap; + } + + private Primitive Create() + { + // TODO: This structure is verging on a little silly. Reconsider. + BuildIndices(); + BuildVertexAttributes(); + BuildVertices(); + BuildBoundingBox(); + + ArgumentNullException.ThrowIfNull(_vertexAttributes); + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_shapeValues); + + var material = _primitive.Material.Name; + if (material == "") + material = null; + + return new Primitive + { + Material = material, + VertexDeclaration = new MdlStructs.VertexDeclarationStruct + { + VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(), + }, + VertexCount = _vertexCount, + Strides = _strides, + Streams = _streams, + Indices = _indices, + BoundingBox = _boundingBox, + ShapeValues = _shapeValues, + }; + } + + private void BuildIndices() + { + // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaluating the indices to triangles with GetTriangleIndices, but will need investigation. + _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); + } + + private void BuildVertexAttributes() + { + // Tangent calculation requires indices if missing. + ArgumentNullException.ThrowIfNull(_indices); + + var accessors = _primitive.VertexAccessors; + + var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(index => _primitive.GetMorphTargetAccessors(index)).ToList(); + + // Try to build all the attributes the mesh might use. + // The order here is chosen to match a typical model's element order. + var rawAttributes = new[] + { + VertexAttribute.Position(accessors, morphAccessors, _notifier), + VertexAttribute.BlendWeight(accessors, _notifier), + VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier), + VertexAttribute.Normal(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier), + VertexAttribute.Color(accessors), + VertexAttribute.Uv(accessors), + }; + + var attributes = new List(); + var offsets = new byte[] + { + 0, + 0, + 0, + }; + foreach (var attribute in rawAttributes) + { + if (attribute == null) + continue; + + attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); + offsets[attribute.Stream] += attribute.Size; + } + + _vertexAttributes = attributes; + // After building the attributes, the resulting next offsets are our stream strides. + _strides = offsets; + } + + private void BuildVertices() + { + ArgumentNullException.ThrowIfNull(_vertexAttributes); + + // Lists of vertex indices that are effected by each morph target for this primitive. + var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) + .Select(_ => new List()) + .ToArray(); + + // We can safely assume that POSITION exists by this point - and if, by some bizarre chance, it doesn't, failing out is sane. + _vertexCount = (ushort)_primitive.VertexAccessors["POSITION"].Count; + + for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) + { + // Write out vertex data to streams for each attribute. + foreach (var attribute in _vertexAttributes) + _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); + + // Record which morph targets have values for this vertex, if any. + var changedMorphs = morphModifiedVertices + .WithIndex() + .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Select(pair => pair.Value); + foreach (var modifiedVertices in changedMorphs) + modifiedVertices.Add(vertexIndex); + } + + BuildShapeValues(morphModifiedVertices); + } + + private void BuildShapeValues(IEnumerable> morphModifiedVertices) + { + ArgumentNullException.ThrowIfNull(_indices); + ArgumentNullException.ThrowIfNull(_vertexAttributes); + + var morphShapeValues = new List>(); + + foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) + { + // For a given mesh, each shape key contains a list of shape value mappings. + var shapeValues = new List(); + + foreach (var vertexIndex in modifiedVertices) + { + // Write out the morphed vertex to the vertex streams. + foreach (var attribute in _vertexAttributes) + _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); + + // Find any indices that target this vertex index and create a mapping. + var targetingIndices = _indices.WithIndex() + .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); + shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct + { + BaseIndicesIndex = (ushort)targetingIndex, + ReplacingVertexIndex = _vertexCount, + })); + + _vertexCount++; + } + + morphShapeValues.Add(shapeValues); + } + + _shapeValues = morphShapeValues; + } + + private void BuildBoundingBox() + { + var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array(); + foreach (var position in positions) + _boundingBox.Merge(position); + } +} diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index caa48757..26f98b04 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -19,7 +19,7 @@ public class SubMeshImporter public byte[] Strides; public List[] Streams; - public ushort[] Indices; + public List Indices; public BoundingBox BoundingBox; @@ -36,85 +36,56 @@ public class SubMeshImporter private readonly IoNotifier _notifier; - private readonly MeshPrimitive _primitive; - private readonly IDictionary? _nodeBoneMap; - private readonly IDictionary? _nodeExtras; + private readonly Node _node; + private readonly IDictionary? _nodeBoneMap; - private List? _vertexAttributes; + private string? _material; - private ushort _vertexCount; - private byte[] _strides = [0, 0, 0]; - private readonly List[] _streams; + private MdlStructs.VertexDeclarationStruct? _vertexDeclaration; + private ushort _vertexCount; + private byte[]? _strides; + private readonly List[] _streams = [[], [], []]; - private ushort[]? _indices; + private List _indices = []; private BoundingBox _boundingBox = new BoundingBox(); - private string[]? _metaAttributes; - - private readonly List? _morphNames; - private Dictionary>? _shapeValues; + private readonly List? _morphNames; + private Dictionary> _shapeValues = []; private SubMeshImporter(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { - _notifier = notifier; - - var mesh = node.Mesh; - - var primitiveCount = mesh.Primitives.Count; - if (primitiveCount != 1) - throw _notifier.Exception($"Mesh has {primitiveCount} primitives, expected 1."); - - _primitive = mesh.Primitives[0]; + _notifier = notifier; + _node = node; _nodeBoneMap = nodeBoneMap; try { - _nodeExtras = node.Extras.Deserialize>(); - } - catch - { - _nodeExtras = null; - } - - try - { - _morphNames = mesh.Extras.GetNode("targetNames").Deserialize>(); + _morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize>(); } catch { _morphNames = null; } - - // All meshes may use up to 3 byte streams. - _streams = new List[3]; - for (var streamIndex = 0; streamIndex < 3; streamIndex++) - _streams[streamIndex] = []; } private SubMesh Create() { // Build all the data we'll need. - // TODO: This structure is verging on a little silly. Reconsider. - BuildIndices(); - BuildVertexAttributes(); - BuildVertices(); - BuildBoundingBox(); - BuildMetaAttributes(); + foreach (var (primitive, index) in _node.Mesh.Primitives.WithIndex()) + BuildPrimitive(primitive, index); ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_vertexAttributes); + ArgumentNullException.ThrowIfNull(_vertexDeclaration); + ArgumentNullException.ThrowIfNull(_strides); ArgumentNullException.ThrowIfNull(_shapeValues); - ArgumentNullException.ThrowIfNull(_metaAttributes); - var material = _primitive.Material.Name; - if (material == "") - material = null; + var metaAttributes = BuildMetaAttributes(); // At this level, we assume that attributes are wholly controlled by this sub-mesh. - var attributeMask = _metaAttributes.Length switch + var attributeMask = metaAttributes.Length switch { - < 32 => (1u << _metaAttributes.Length) - 1, + < 32 => (1u << metaAttributes.Length) - 1, 32 => uint.MaxValue, > 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."), }; @@ -124,156 +95,92 @@ public class SubMeshImporter SubMeshStruct = new MdlStructs.SubmeshStruct() { IndexOffset = 0, - IndexCount = (uint)_indices.Length, + IndexCount = (uint)_indices.Count, AttributeIndexMask = attributeMask, // TODO: Flesh these out. Game doesn't seem to rely on them existing, though. BoneStartIndex = 0, BoneCount = 0, }, - Material = material, - VertexDeclaration = new MdlStructs.VertexDeclarationStruct() - { - VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(), - }, + Material = _material, + VertexDeclaration = _vertexDeclaration.Value, VertexCount = _vertexCount, Strides = _strides, Streams = _streams, Indices = _indices, BoundingBox = _boundingBox, - MetaAttributes = _metaAttributes, + MetaAttributes = metaAttributes, ShapeValues = _shapeValues, }; } - private void BuildIndices() + private void BuildPrimitive(MeshPrimitive meshPrimitive, int index) { - // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaluating the indices to triangles with GetTriangleIndices, but will need investigation. - _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); - } + var vertexOffset = _vertexCount; + var indexOffset = _indices.Count; - private void BuildVertexAttributes() - { - // Tangent calculation requires indices if missing. - ArgumentNullException.ThrowIfNull(_indices); + var primitive = PrimitiveImporter.Import(meshPrimitive, _nodeBoneMap, _notifier.WithContext($"Primitive {index}")); - var accessors = _primitive.VertexAccessors; + // Material + _material ??= primitive.Material; + if (primitive.Material != null && _material != primitive.Material) + _notifier.Warning($"Meshes may only reference one material. Primitive {index} material \"{primitive.Material}\" has been ignored."); - var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) - .Select(index => _primitive.GetMorphTargetAccessors(index)).ToList(); + // Vertex metadata + if (_vertexDeclaration == null) + _vertexDeclaration = primitive.VertexDeclaration; + else + Utility.EnsureVertexDeclarationMatch(_vertexDeclaration.Value, primitive.VertexDeclaration, _notifier); - // Try to build all the attributes the mesh might use. - // The order here is chosen to match a typical model's element order. - var rawAttributes = new[] + _strides ??= primitive.Strides; + + // Vertices + _vertexCount += primitive.VertexCount; + + foreach (var (stream, primitiveStream) in _streams.Zip(primitive.Streams)) + stream.AddRange(primitiveStream); + + // Indices + _indices.AddRange(primitive.Indices.Select(index => (ushort)(index + vertexOffset))); + + // Shape values + foreach (var (primitiveShapeValues, morphIndex) in primitive.ShapeValues.WithIndex()) { - VertexAttribute.Position(accessors, morphAccessors, _notifier), - VertexAttribute.BlendWeight(accessors, _notifier), - VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier), - VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier), - VertexAttribute.Color(accessors), - VertexAttribute.Uv(accessors), - }; + // Per glTF spec, all primitives MUST have the same number of morph targets in the same order. + // As such, this lookup should be safe - a failure here is a broken glTF file. + var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; - var attributes = new List(); - var offsets = new byte[] - { - 0, - 0, - 0, - }; - foreach (var attribute in rawAttributes) - { - if (attribute == null) - continue; - - attributes.Add(attribute.WithOffset(offsets[attribute.Stream])); - offsets[attribute.Stream] += attribute.Size; - } - - _vertexAttributes = attributes; - // After building the attributes, the resulting next offsets are our stream strides. - _strides = offsets; - } - - private void BuildVertices() - { - ArgumentNullException.ThrowIfNull(_vertexAttributes); - - // Lists of vertex indices that are effected by each morph target for this primitive. - var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount) - .Select(_ => new List()) - .ToArray(); - - // We can safely assume that POSITION exists by this point - and if, by some bizarre chance, it doesn't, failing out is sane. - _vertexCount = (ushort)_primitive.VertexAccessors["POSITION"].Count; - - for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++) - { - // Write out vertex data to streams for each attribute. - foreach (var attribute in _vertexAttributes) - _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); - - // Record which morph targets have values for this vertex, if any. - var changedMorphs = morphModifiedVertices - .WithIndex() - .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) - .Select(pair => pair.Value); - foreach (var modifiedVertices in changedMorphs) - modifiedVertices.Add(vertexIndex); - } - - BuildShapeValues(morphModifiedVertices); - } - - private void BuildShapeValues(IEnumerable> morphModifiedVertices) - { - ArgumentNullException.ThrowIfNull(_indices); - ArgumentNullException.ThrowIfNull(_vertexAttributes); - - var morphShapeValues = new Dictionary>(); - - foreach (var (modifiedVertices, morphIndex) in morphModifiedVertices.WithIndex()) - { - // For a given mesh, each shape key contains a list of shape value mappings. - var shapeValues = new List(); - - foreach (var vertexIndex in modifiedVertices) + if (!_shapeValues.TryGetValue(name, out var subMeshShapeValues)) { - // Write out the morphed vertex to the vertex streams. - foreach (var attribute in _vertexAttributes) - _streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex)); - - // Find any indices that target this vertex index and create a mapping. - var targetingIndices = _indices.WithIndex() - .SelectWhere(pair => (pair.Value == vertexIndex, pair.Index)); - shapeValues.AddRange(targetingIndices.Select(targetingIndex => new MdlStructs.ShapeValueStruct - { - BaseIndicesIndex = (ushort)targetingIndex, - ReplacingVertexIndex = _vertexCount, - })); - - _vertexCount++; + subMeshShapeValues = []; + _shapeValues.Add(name, subMeshShapeValues); } - var name = _morphNames != null ? _morphNames[morphIndex] : $"unnamed_shape_{morphIndex}"; - morphShapeValues.Add(name, shapeValues); + subMeshShapeValues.AddRange(primitiveShapeValues.Select(value => value with + { + BaseIndicesIndex = (ushort)(value.BaseIndicesIndex + indexOffset), + ReplacingVertexIndex = (ushort)(value.ReplacingVertexIndex + vertexOffset), + })); } - _shapeValues = morphShapeValues; + // Bounds + _boundingBox.Merge(primitive.BoundingBox); } - private void BuildBoundingBox() + private string[] BuildMetaAttributes() { - var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array(); - foreach (var position in positions) - _boundingBox.Merge(position); - } + Dictionary? nodeExtras; + try + { + nodeExtras = _node.Extras.Deserialize>(); + } + catch + { + nodeExtras = null; + } - private void BuildMetaAttributes() - { // We consider any "extras" key with a boolean value set to `true` to be an attribute. - _metaAttributes = _nodeExtras? + return nodeExtras? .Where(pair => pair.Value.ValueKind == JsonValueKind.True) .Select(pair => pair.Key) .ToArray() ?? []; diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs index 449d19e4..28f47024 100644 --- a/Penumbra/Import/Models/Import/Utility.cs +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -1,3 +1,5 @@ +using Lumina.Data.Parsing; + namespace Penumbra.Import.Models.Import; public static class Utility @@ -31,4 +33,35 @@ public static class Utility return newMask; } + + /// Ensures that the two vertex declarations provided are equal, throwing if not. + public static void EnsureVertexDeclarationMatch(MdlStructs.VertexDeclarationStruct current, MdlStructs.VertexDeclarationStruct @new, IoNotifier notifier) + { + if (VertexDeclarationMismatch(current, @new)) + throw notifier.Exception( + $@"All sub-meshes of a mesh must have equivalent vertex declarations. + Current: {FormatVertexDeclaration(current)} + New: {FormatVertexDeclaration(@new)}" + ); + } + + private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) + => string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + + private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) + { + var elA = a.VertexElements; + var elB = b.VertexElements; + + if (elA.Length != elB.Length) + return true; + + // NOTE: This assumes that elements will always be in the same order. Under the current implementation, that's guaranteed. + return elA.Zip(elB).Any(pair => + pair.First.Usage != pair.Second.Usage + || pair.First.Type != pair.Second.Type + || pair.First.Offset != pair.Second.Offset + || pair.First.Stream != pair.Second.Stream + ); + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index f24464d1..ace2bcfc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -2,7 +2,6 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; -using Penumbra.Import.Models; using Penumbra.Import.Models.Export; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; From a159ef28c37b3fa5fabc621610d9ebce3e0e8c0b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Jan 2024 23:23:03 +0100 Subject: [PATCH 1507/2451] Cleanup --- Penumbra/Import/Models/Import/BoundingBox.cs | 8 ++--- Penumbra/Import/Models/Import/MeshImporter.cs | 5 +-- .../Import/Models/Import/PrimitiveImporter.cs | 17 ++++----- .../Import/Models/Import/SubMeshImporter.cs | 35 ++++++++++--------- Penumbra/Import/Models/Import/Utility.cs | 21 ++++++----- 5 files changed, 47 insertions(+), 39 deletions(-) diff --git a/Penumbra/Import/Models/Import/BoundingBox.cs b/Penumbra/Import/Models/Import/BoundingBox.cs index be47ebb8..b6d670ae 100644 --- a/Penumbra/Import/Models/Import/BoundingBox.cs +++ b/Penumbra/Import/Models/Import/BoundingBox.cs @@ -2,11 +2,11 @@ using Lumina.Data.Parsing; namespace Penumbra.Import.Models.Import; -/// Mutable representation of the bounding box surrouding a collection of vertices. +/// Mutable representation of the bounding box surrounding a collection of vertices. public class BoundingBox { - private Vector3 _minimum = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); - private Vector3 _maximum = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + private Vector3 _minimum = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + private Vector3 _maximum = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); /// Use the specified position to update this bounding box, expanding it if necessary. public void Merge(Vector3 position) @@ -25,7 +25,7 @@ public class BoundingBox /// Convert this bounding box to the struct format used in .mdl data structures. public MdlStructs.BoundingBoxStruct ToStruct() - => new MdlStructs.BoundingBoxStruct + => new() { Min = [_minimum.X, _minimum.Y, _minimum.Z, 1], Max = [_maximum.X, _maximum.Y, _maximum.Z, 1], diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index f76fa1ea..8ab55734 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -53,7 +53,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) private List? _bones; - private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly BoundingBox _boundingBox = new(); private readonly List _metaAttributes = []; @@ -124,7 +124,8 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) _material ??= subMesh.Material; if (subMesh.Material != null && _material != subMesh.Material) - notifier.Warning($"Meshes may only reference one material. Sub-mesh {subMeshName} material \"{subMesh.Material}\" has been ignored."); + notifier.Warning( + $"Meshes may only reference one material. Sub-mesh {subMeshName} material \"{subMesh.Material}\" has been ignored."); // Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution. if (_vertexDeclaration == null) diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs index 9ac3d73c..0c2968df 100644 --- a/Penumbra/Import/Models/Import/PrimitiveImporter.cs +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -35,21 +35,21 @@ public class PrimitiveImporter private readonly IDictionary? _nodeBoneMap; private ushort[]? _indices; - + private List? _vertexAttributes; - + private ushort _vertexCount; private byte[] _strides = [0, 0, 0]; private readonly List[] _streams = [[], [], []]; - - private BoundingBox _boundingBox = new BoundingBox(); + + private readonly BoundingBox _boundingBox = new(); private List>? _shapeValues; private PrimitiveImporter(MeshPrimitive primitive, IDictionary? nodeBoneMap, IoNotifier notifier) { - _notifier = notifier; - _primitive = primitive; + _notifier = notifier; + _primitive = primitive; _nodeBoneMap = nodeBoneMap; } @@ -154,9 +154,10 @@ public class PrimitiveImporter _streams[attribute.Stream].AddRange(attribute.Build(vertexIndex)); // Record which morph targets have values for this vertex, if any. + var index = vertexIndex; var changedMorphs = morphModifiedVertices .WithIndex() - .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex))) + .Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, index))) .Select(pair => pair.Value); foreach (var modifiedVertices in changedMorphs) modifiedVertices.Add(vertexIndex); @@ -200,7 +201,7 @@ public class PrimitiveImporter _shapeValues = morphShapeValues; } - + private void BuildBoundingBox() { var positions = _primitive.VertexAccessors["POSITION"].AsVector3Array(); diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 26f98b04..e81bb622 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -46,12 +46,12 @@ public class SubMeshImporter private byte[]? _strides; private readonly List[] _streams = [[], [], []]; - private List _indices = []; + private readonly List _indices = []; - private BoundingBox _boundingBox = new BoundingBox(); + private readonly BoundingBox _boundingBox = new(); private readonly List? _morphNames; - private Dictionary> _shapeValues = []; + private readonly Dictionary> _shapeValues = []; private SubMeshImporter(Node node, IDictionary? nodeBoneMap, IoNotifier notifier) { @@ -86,7 +86,7 @@ public class SubMeshImporter var attributeMask = metaAttributes.Length switch { < 32 => (1u << metaAttributes.Length) - 1, - 32 => uint.MaxValue, + 32 => uint.MaxValue, > 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."), }; @@ -102,22 +102,22 @@ public class SubMeshImporter BoneStartIndex = 0, BoneCount = 0, }, - Material = _material, + Material = _material, VertexDeclaration = _vertexDeclaration.Value, - VertexCount = _vertexCount, - Strides = _strides, - Streams = _streams, - Indices = _indices, - BoundingBox = _boundingBox, - MetaAttributes = metaAttributes, - ShapeValues = _shapeValues, + VertexCount = _vertexCount, + Strides = _strides, + Streams = _streams, + Indices = _indices, + BoundingBox = _boundingBox, + MetaAttributes = metaAttributes, + ShapeValues = _shapeValues, }; } private void BuildPrimitive(MeshPrimitive meshPrimitive, int index) { var vertexOffset = _vertexCount; - var indexOffset = _indices.Count; + var indexOffset = _indices.Count; var primitive = PrimitiveImporter.Import(meshPrimitive, _nodeBoneMap, _notifier.WithContext($"Primitive {index}")); @@ -141,7 +141,7 @@ public class SubMeshImporter stream.AddRange(primitiveStream); // Indices - _indices.AddRange(primitive.Indices.Select(index => (ushort)(index + vertexOffset))); + _indices.AddRange(primitive.Indices.Select(i => (ushort)(i + vertexOffset))); // Shape values foreach (var (primitiveShapeValues, morphIndex) in primitive.ShapeValues.WithIndex()) @@ -181,8 +181,9 @@ public class SubMeshImporter // We consider any "extras" key with a boolean value set to `true` to be an attribute. return nodeExtras? - .Where(pair => pair.Value.ValueKind == JsonValueKind.True) - .Select(pair => pair.Key) - .ToArray() ?? []; + .Where(pair => pair.Value.ValueKind == JsonValueKind.True) + .Select(pair => pair.Key) + .ToArray() + ?? []; } } diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs index 28f47024..a1e44136 100644 --- a/Penumbra/Import/Models/Import/Utility.cs +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -4,11 +4,11 @@ namespace Penumbra.Import.Models.Import; public static class Utility { - /// Merge attributes into an existing attribute array, providing an updated submesh mask. - /// Old submesh attribute mask. + /// Merge attributes into an existing attribute array, providing an updated sub mesh mask. + /// Old sub mesh attribute mask. /// Old attribute array that should be merged. /// New attribute array. Will be mutated. - /// New submesh attribute mask, updated to match the merged attribute array. + /// New sub mesh attribute mask, updated to match the merged attribute array. public static uint GetMergedAttributeMask(uint oldMask, IList oldAttributes, List newAttributes) { var metaAttributes = Enumerable.Range(0, 32) @@ -28,6 +28,7 @@ public static class Utility newAttributes.Add(metaAttribute); attributeIndex = newAttributes.Count - 1; } + newMask |= 1u << attributeIndex; } @@ -35,18 +36,22 @@ public static class Utility } /// Ensures that the two vertex declarations provided are equal, throwing if not. - public static void EnsureVertexDeclarationMatch(MdlStructs.VertexDeclarationStruct current, MdlStructs.VertexDeclarationStruct @new, IoNotifier notifier) + public static void EnsureVertexDeclarationMatch(MdlStructs.VertexDeclarationStruct current, MdlStructs.VertexDeclarationStruct @new, + IoNotifier notifier) { if (VertexDeclarationMismatch(current, @new)) throw notifier.Exception( - $@"All sub-meshes of a mesh must have equivalent vertex declarations. - Current: {FormatVertexDeclaration(current)} - New: {FormatVertexDeclaration(@new)}" + $""" + All sub-meshes of a mesh must have equivalent vertex declarations. + Current: {FormatVertexDeclaration(current)} + New: {FormatVertexDeclaration(@new)} + """ ); } private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) - => string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + => string.Join(", ", + vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { From 6693a1e0bad504916906aa161cec69365fcbb8a6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 27 Jan 2024 13:53:56 +1100 Subject: [PATCH 1508/2451] Clear errors/warnings before starting IO --- .../ModEditWindow.Models.MdlTab.cs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index ace2bcfc..b3c0cacd 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -2,6 +2,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; +using Penumbra.Import.Models; using Penumbra.Import.Models.Export; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; @@ -79,7 +80,7 @@ public partial class ModEditWindow return; } - PendingIo = true; + BeginIo(); var task = Task.Run(() => { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? @@ -93,9 +94,7 @@ public partial class ModEditWindow task.ContinueWith(t => { - RecordIoExceptions(t.Exception); - GamePaths = t.Result; - PendingIo = false; + GamePaths = FinalizeIo(task); }); } @@ -132,33 +131,22 @@ public partial class ModEditWindow return; } - PendingIo = true; + BeginIo(); _edit._models.ExportToGltf(ExportConfig, Mdl, sklbPaths, ReadFile, outputPath) - .ContinueWith(task => - { - RecordIoExceptions(task.Exception); - if (task is { IsCompletedSuccessfully: true, Result: not null }) - IoWarnings = task.Result.GetWarnings().ToList(); - PendingIo = false; - }); + .ContinueWith(FinalizeIo); } /// Import a model from an interchange format. /// Disk path to load model data from. public void Import(string inputPath) { - PendingIo = true; + BeginIo(); _edit._models.ImportGltf(inputPath) .ContinueWith(task => { - RecordIoExceptions(task.Exception); - if (task is { IsCompletedSuccessfully: true, Result: (not null, _) }) - { - IoWarnings = task.Result.Item2.GetWarnings().ToList(); - FinalizeImport(task.Result.Item1); - } - - PendingIo = false; + var mdlFile = FinalizeIo(task, result => result.Item1, result => result.Item2); + if (mdlFile != null) + FinalizeImport(mdlFile); }); } @@ -255,6 +243,34 @@ public partial class ModEditWindow target.ElementIds = [.. elementIds]; } + private void BeginIo() + { + PendingIo = true; + IoWarnings = []; + IoExceptions = []; + } + + private void FinalizeIo(Task task) + => FinalizeIo(task, notifier => null, notifier => notifier); + + private TResult? FinalizeIo(Task task) + => FinalizeIo(task, result => result, null); + + private TResult? FinalizeIo(Task task, Func getResult, Func? getNotifier) + { + TResult? result = default; + RecordIoExceptions(task.Exception); + if (task is { IsCompletedSuccessfully: true, Result: not null }) + { + result = getResult(task.Result); + if (getNotifier != null) + IoWarnings = getNotifier(task.Result).GetWarnings().ToList(); + } + PendingIo = false; + + return result; + } + private void RecordIoExceptions(Exception? exception) { IoExceptions = exception switch From 72775a80bfbcc1b7c7ab06827f1caf6722195a9d Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 27 Jan 2024 14:08:56 +1100 Subject: [PATCH 1509/2451] Ignore invalid attributes on export --- Penumbra/Import/Models/Export/MeshExporter.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 5347b87d..1a06acd1 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -230,10 +230,19 @@ public class MeshExporter { "targetNames", shapeNames }, }); - var attributes = Enumerable.Range(0, 32) - .Where(index => ((attributeMask >> index) & 1) == 1) - .Select(index => _mdl.Attributes[index]) - .ToArray(); + string[] attributes = []; + var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); + if (maxAttribute < _mdl.Attributes.Length) + { + attributes = Enumerable.Range(0, 32) + .Where(index => ((attributeMask >> index) & 1) == 1) + .Select(index => _mdl.Attributes[index]) + .ToArray(); + } + else + { + _notifier.Warning($"Invalid attribute data, ignoring."); + } return new MeshData { From 1649da70a899fa90ad3ced2a37ff1d8e726b0a16 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 27 Jan 2024 17:56:32 +1100 Subject: [PATCH 1510/2451] Fix Blender 3.6 support for custom colour attribute --- Penumbra/Import/Models/Export/VertexFragment.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 27d2ab10..08b2a214 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -11,7 +11,8 @@ and there's reason to overhaul the export pipeline. public struct VertexColorFfxiv : IVertexCustom { - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] + // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -80,7 +81,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_0")] public Vector2 TexCoord0; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -162,7 +163,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_BYTE, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] public Vector4 FfxivColor; public int MaxColors => 0; From e9628afaf84ac8afc24eaeddc5a92f12c00e3f2f Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 27 Jan 2024 18:35:26 +1100 Subject: [PATCH 1511/2451] Include specular factor in character material export --- .../Import/Models/Export/MaterialExporter.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 307e9d2b..cb2cf6f5 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -89,11 +89,15 @@ public class MaterialExporter // TODO: handle other textures stored in the mask? } + // Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture. + var specularImage = BuildImage(specular, name, "specular"); + return BuildSharedBase(material, name) .WithBaseColor(BuildImage(baseColor, name, "basecolor")) .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithSpecularColor(BuildImage(specular, name, "specular")) - .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1); + .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1) + .WithSpecularFactor(specularImage, 1) + .WithSpecularColor(specularImage); } // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. @@ -102,7 +106,7 @@ public class MaterialExporter { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); - public Image Specular { get; } = new(normal.Width, normal.Height); + public Image Specular { get; } = new(normal.Width, normal.Height); public Image Emissive { get; } = new(normal.Width, normal.Height); private Buffer2D NormalBuffer @@ -111,7 +115,7 @@ public class MaterialExporter private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; - private Buffer2D SpecularBuffer + private Buffer2D SpecularBuffer => Specular.Frames.RootFrame.PixelBuffer; private Buffer2D EmissiveBuffer @@ -140,7 +144,9 @@ public class MaterialExporter // Specular (table) var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + // float.Lerp is .NET8 ;-; + var lerpedSpecularFactor = prevRow.SpecularStrength * (1.0f - tableRow.Weight) + nextRow.SpecularStrength * tableRow.Weight; + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); // Emissive (table) var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); From 5a80e65d3b6dced06f7bcd1445c9660c507e72b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Jan 2024 18:51:16 +0100 Subject: [PATCH 1512/2451] Add SetCutsceneParentIndex. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 17 +++++++++++++++-- Penumbra/Api/PenumbraApi.cs | 9 +++++++++ Penumbra/Api/PenumbraIpcProviders.cs | 3 +++ .../Interop/PathResolving/CutsceneService.cs | 18 ++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index cfc51714..b28288ee 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit cfc51714f74cae93608bc507775a9580cd1801de +Subproject commit b28288ee9668425f49f41cba88e8dc417ad62aff diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index aea95156..380b741c 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -484,7 +484,9 @@ public class IpcTester : IDisposable private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; private string _currentDrawObjectString = string.Empty; private IntPtr _currentDrawObject = IntPtr.Zero; - private int _currentCutsceneActor = 0; + private int _currentCutsceneActor; + private int _currentCutsceneParent; + private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; public GameState(DalamudPluginInterface pi) { @@ -507,7 +509,14 @@ public class IpcTester : IDisposable ? tmp : IntPtr.Zero; - ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); + ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); + ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0); + if (_cutsceneError is not PenumbraApiEc.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted("Invalid Argument on last Call"); + } + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -526,6 +535,10 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent"); ImGui.TextUnformatted(Ipc.GetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor).ToString()); + DrawIntro(Ipc.SetCutsceneParentIndex.Label, "Cutscene Parent"); + if (ImGui.Button("Set Parent")) + _cutsceneError = Ipc.SetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor, _currentCutsceneParent); + DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created"); if (_lastCreatedGameObjectTime < DateTimeOffset.Now) ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2a7a9bfb..04c0499b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -600,6 +600,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi return _cutsceneService.GetParentIndex(actorIdx); } + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) + { + CheckInitialized(); + if (_cutsceneService.SetParentIndex(copyIdx, newParentIdx)) + return PenumbraApiEc.Success; + + return PenumbraApiEc.InvalidArgument; + } + public IList<(string, string)> GetModList() { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 1df90dd9..d478b675 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -47,6 +47,7 @@ public class PenumbraIpcProviders : IDisposable // Game State internal readonly FuncProvider GetDrawObjectInfo; internal readonly FuncProvider GetCutsceneParentIndex; + internal readonly FuncProvider SetCutsceneParentIndex; internal readonly EventProvider CreatingCharacterBase; internal readonly EventProvider CreatedCharacterBase; internal readonly EventProvider GameObjectResourcePathResolved; @@ -171,6 +172,7 @@ public class PenumbraIpcProviders : IDisposable // Game State GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo); GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex); + SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex); CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi, () => Api.CreatingCharacterBase += CreatingCharacterBaseEvent, () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent); @@ -293,6 +295,7 @@ public class PenumbraIpcProviders : IDisposable // Game State GetDrawObjectInfo.Dispose(); GetCutsceneParentIndex.Dispose(); + SetCutsceneParentIndex.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); GameObjectResourcePathResolved.Dispose(); diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 85944d74..89b9f917 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -56,6 +56,24 @@ public sealed class CutsceneService : IService, IDisposable public int GetParentIndex(int idx) => GetParentIndex((ushort)idx); + public bool SetParentIndex(int copyIdx, int parentIdx) + { + if (copyIdx is < CutsceneStartIdx or >= CutsceneEndIdx) + return false; + + if (parentIdx is < -1 or >= CutsceneEndIdx) + return false; + + if (_objects.GetObjectAddress(copyIdx) == nint.Zero) + return false; + + if (parentIdx != -1 && _objects.GetObjectAddress(parentIdx) == nint.Zero) + return false; + + _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; + return true; + } + public short GetParentIndex(ushort idx) { if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) From 076dab924fa21e67d28e88ee5a01601c532160a5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Jan 2024 18:58:26 +0100 Subject: [PATCH 1513/2451] Reuse same list for warnings and exceptions. --- .../ModEditWindow.Models.MdlTab.cs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b3c0cacd..37b9dfb5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -28,8 +28,8 @@ public partial class ModEditWindow private bool _dirty; public bool PendingIo { get; private set; } - public List IoExceptions { get; private set; } = []; - public List IoWarnings { get; private set; } = []; + public List IoExceptions { get; } = []; + public List IoWarnings { get; } = []; public MdlTab(ModEditWindow edit, byte[] bytes, string path) { @@ -92,10 +92,7 @@ public partial class ModEditWindow .ToList(); }); - task.ContinueWith(t => - { - GamePaths = FinalizeIo(task); - }); + task.ContinueWith(t => { GamePaths = FinalizeIo(task); }); } private EstManipulation[] GetCurrentEstManipulations() @@ -246,8 +243,8 @@ public partial class ModEditWindow private void BeginIo() { PendingIo = true; - IoWarnings = []; - IoExceptions = []; + IoWarnings.Clear(); + IoExceptions.Clear(); } private void FinalizeIo(Task task) @@ -264,8 +261,9 @@ public partial class ModEditWindow { result = getResult(task.Result); if (getNotifier != null) - IoWarnings = getNotifier(task.Result).GetWarnings().ToList(); + IoWarnings.AddRange(getNotifier(task.Result).GetWarnings()); } + PendingIo = false; return result; @@ -273,12 +271,16 @@ public partial class ModEditWindow private void RecordIoExceptions(Exception? exception) { - IoExceptions = exception switch + switch (exception) { - null => [], - AggregateException ae => [.. ae.Flatten().InnerExceptions], - _ => [exception], - }; + case null: break; + case AggregateException ae: + IoExceptions.AddRange(ae.Flatten().InnerExceptions); + break; + default: + IoExceptions.Add(exception); + break; + } } /// Read a file from the active collection or game. From 1e4570bd79193e22d6870e1d057888d7f9b1653a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Jan 2024 19:05:05 +0100 Subject: [PATCH 1514/2451] Slight cleanup. --- Penumbra/Import/Models/Export/MaterialExporter.cs | 2 +- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index cb2cf6f5..f17fdaa2 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -144,7 +144,7 @@ public class MaterialExporter // Specular (table) var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - // float.Lerp is .NET8 ;-; + // float.Lerp is .NET8 ;-; #TODO var lerpedSpecularFactor = prevRow.SpecularStrength * (1.0f - tableRow.Weight) + nextRow.SpecularStrength * tableRow.Weight; specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 1a06acd1..df315094 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -241,7 +241,7 @@ public class MeshExporter } else { - _notifier.Warning($"Invalid attribute data, ignoring."); + _notifier.Warning("Invalid attribute data, ignoring."); } return new MeshData diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 37b9dfb5..7adc4379 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -92,7 +92,7 @@ public partial class ModEditWindow .ToList(); }); - task.ContinueWith(t => { GamePaths = FinalizeIo(task); }); + task.ContinueWith(t => { GamePaths = FinalizeIo(t); }); } private EstManipulation[] GetCurrentEstManipulations() @@ -171,7 +171,7 @@ public partial class ModEditWindow /// Merge material configuration from the source onto the target. /// Model that will be updated. /// Model to copy material configuration from. - public void MergeMaterials(MdlFile target, MdlFile source) + private static void MergeMaterials(MdlFile target, MdlFile source) { target.Materials = source.Materials; @@ -186,7 +186,7 @@ public partial class ModEditWindow /// Merge attribute configuration from the source onto the target. /// Model that will be updated. > /// Model to copy attribute configuration from. - public static void MergeAttributes(MdlFile target, MdlFile source) + private static void MergeAttributes(MdlFile target, MdlFile source) { target.Attributes = source.Attributes; @@ -248,7 +248,7 @@ public partial class ModEditWindow } private void FinalizeIo(Task task) - => FinalizeIo(task, notifier => null, notifier => notifier); + => FinalizeIo(task, _ => null, notifier => notifier); private TResult? FinalizeIo(Task task) => FinalizeIo(task, result => result, null); From e321cbdf9691b3e97d73761b4852a08866425bb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Jan 2024 19:07:51 +0100 Subject: [PATCH 1515/2451] Update API minor Version. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index b28288ee..31bf4ad9 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b28288ee9668425f49f41cba88e8dc417ad62aff +Subproject commit 31bf4ad9b82fc980d6bda049da595368ad754931 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 04c0499b..1a0764c7 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -30,7 +30,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 22); + => (4, 23); public event Action? PreSettingsPanelDraw { From 4610686a70636863d0671bde99298e6e198c7e53 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 Jan 2024 18:10:34 +0000 Subject: [PATCH 1516/2451] [CI] Updating repo.json for 1.0.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index eef374b5..fc95fef3 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.3", - "TestingAssemblyVersion": "1.0.0.3", + "AssemblyVersion": "1.0.0.4", + "TestingAssemblyVersion": "1.0.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a5f0c2f94398c85884f9646b4a6046d98be5680d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jan 2024 13:27:05 +0100 Subject: [PATCH 1517/2451] Fix exception with empty option groups. --- Penumbra/Mods/Subclasses/ModSettings.cs | 180 ++++++++++----------- Penumbra/Mods/Subclasses/SingleModGroup.cs | 2 +- 2 files changed, 87 insertions(+), 95 deletions(-) diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 71a56c80..a20cb9cb 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -11,9 +11,9 @@ namespace Penumbra.Mods.Subclasses; public class ModSettings { public static readonly ModSettings Empty = new(); - public List< uint > Settings { get; private init; } = []; - public int Priority { get; set; } - public bool Enabled { get; set; } + public List Settings { get; private init; } = []; + public int Priority { get; set; } + public bool Enabled { get; set; } // Create an independent copy of the current settings. public ModSettings DeepCopy() @@ -25,148 +25,143 @@ public class ModSettings }; // Create default settings for a given mod. - public static ModSettings DefaultSettings( Mod mod ) + public static ModSettings DefaultSettings(Mod mod) => new() { Enabled = false, Priority = 0, - Settings = mod.Groups.Select( g => g.DefaultSettings ).ToList(), + Settings = mod.Groups.Select(g => g.DefaultSettings).ToList(), }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. - public static (Dictionary< Utf8GamePath, FullPath >, HashSet< MetaManipulation >) GetResolveData( Mod mod, ModSettings? settings ) + public static (Dictionary, HashSet) GetResolveData(Mod mod, ModSettings? settings) { if (settings == null) settings = DefaultSettings(mod); else - settings.AddMissingSettings( mod ); + settings.AddMissingSettings(mod); - var dict = new Dictionary< Utf8GamePath, FullPath >(); - var set = new HashSet< MetaManipulation >(); + var dict = new Dictionary(); + var set = new HashSet(); - foreach( var (group, index) in mod.Groups.WithIndex().OrderByDescending( g => g.Value.Priority ) ) + foreach (var (group, index) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { - if( group.Type is GroupType.Single ) + if (group.Type is GroupType.Single) { - AddOption( group[ ( int )settings.Settings[ index ] ] ); + if (group.Count > 0) + AddOption(group[(int)settings.Settings[index]]); } else { - foreach( var (option, optionIdx) in group.WithIndex().OrderByDescending( o => group.OptionPriority( o.Index ) ) ) + foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) { - if( ( ( settings.Settings[ index ] >> optionIdx ) & 1 ) == 1 ) - { - AddOption( option ); - } + if (((settings.Settings[index] >> optionIdx) & 1) == 1) + AddOption(option); } } } - AddOption( mod.Default ); - return ( dict, set ); + AddOption(mod.Default); + return (dict, set); - void AddOption( ISubMod option ) + void AddOption(ISubMod option) { - foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) ) - { - dict.TryAdd( path, file ); - } + foreach (var (path, file) in option.Files.Concat(option.FileSwaps)) + dict.TryAdd(path, file); - foreach( var manip in option.Manipulations ) - { - set.Add( manip ); - } + foreach (var manip in option.Manipulations) + set.Add(manip); } } // Automatically react to changes in a mods available options. - public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) + public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) { - switch( type ) + switch (type) { case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert( groupIdx, mod.Groups[ groupIdx ].DefaultSettings ); + Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. - Settings.RemoveAt( groupIdx ); + Settings.RemoveAt(groupIdx); return true; case ModOptionChangeType.GroupTypeChanged: { // Fix settings for a changed group type. // Single -> Multi: set single as enabled, rest as disabled // Multi -> Single: set the first enabled option or 0. - var group = mod.Groups[ groupIdx ]; - var config = Settings[ groupIdx ]; - Settings[ groupIdx ] = group.Type switch + var group = mod.Groups[groupIdx]; + var config = Settings[groupIdx]; + Settings[groupIdx] = group.Type switch { - GroupType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), - GroupType.Multi => 1u << ( int )config, + GroupType.Single => (uint)Math.Max(Math.Min(group.Count - 1, BitOperations.TrailingZeroCount(config)), 0), + GroupType.Multi => 1u << (int)config, _ => config, }; - return config != Settings[ groupIdx ]; + return config != Settings[groupIdx]; } case ModOptionChangeType.OptionDeleted: { // Single -> select the previous option if any. // Multi -> excise the corresponding bit. - var group = mod.Groups[ groupIdx ]; - var config = Settings[ groupIdx ]; - Settings[ groupIdx ] = group.Type switch + var group = mod.Groups[groupIdx]; + var config = Settings[groupIdx]; + Settings[groupIdx] = group.Type switch { GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, - GroupType.Multi => Functions.RemoveBit( config, optionIdx ), + GroupType.Multi => Functions.RemoveBit(config, optionIdx), _ => config, }; - return config != Settings[ groupIdx ]; + return config != Settings[groupIdx]; } case ModOptionChangeType.GroupMoved: // Move the group the same way. - return Settings.Move( groupIdx, movedToIdx ); + return Settings.Move(groupIdx, movedToIdx); case ModOptionChangeType.OptionMoved: { // Single -> select the moved option if it was currently selected // Multi -> move the corresponding bit - var group = mod.Groups[ groupIdx ]; - var config = Settings[ groupIdx ]; - Settings[ groupIdx ] = group.Type switch + var group = mod.Groups[groupIdx]; + var config = Settings[groupIdx]; + Settings[groupIdx] = group.Type switch { - GroupType.Single => config == optionIdx ? ( uint )movedToIdx : config, - GroupType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), + GroupType.Single => config == optionIdx ? (uint)movedToIdx : config, + GroupType.Multi => Functions.MoveBit(config, optionIdx, movedToIdx), _ => config, }; - return config != Settings[ groupIdx ]; + return config != Settings[groupIdx]; } default: return false; } } // Ensure that a value is valid for a group. - private static uint FixSetting( IModGroup group, uint value ) + private static uint FixSetting(IModGroup group, uint value) => group.Type switch { - GroupType.Single => ( uint )Math.Min( value, group.Count - 1 ), - GroupType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), + GroupType.Single => (uint)Math.Min(value, group.Count - 1), + GroupType.Multi => (uint)(value & ((1ul << group.Count) - 1)), _ => value, }; // Set a setting. Ensures that there are enough settings and fixes the setting beforehand. - public void SetValue( Mod mod, int groupIdx, uint newValue ) + public void SetValue(Mod mod, int groupIdx, uint newValue) { - AddMissingSettings( mod ); - var group = mod.Groups[ groupIdx ]; - Settings[ groupIdx ] = FixSetting( group, newValue ); + AddMissingSettings(mod); + var group = mod.Groups[groupIdx]; + Settings[groupIdx] = FixSetting(group, newValue); } // Add defaulted settings up to the required count. - private bool AddMissingSettings( Mod mod ) + private bool AddMissingSettings(Mod mod) { var changes = false; - for( var i = Settings.Count; i < mod.Groups.Count; ++i ) + for (var i = Settings.Count; i < mod.Groups.Count; ++i) { - Settings.Add( mod.Groups[ i ].DefaultSettings ); + Settings.Add(mod.Groups[i].DefaultSettings); changes = true; } @@ -176,51 +171,47 @@ public class ModSettings // A simple struct conversion to easily save settings by name instead of value. public struct SavedSettings { - public Dictionary< string, long > Settings; - public int Priority; - public bool Enabled; + public Dictionary Settings; + public int Priority; + public bool Enabled; public SavedSettings DeepCopy() => new() { Enabled = Enabled, Priority = Priority, - Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; - public SavedSettings( ModSettings settings, Mod mod ) + public SavedSettings(ModSettings settings, Mod mod) { Priority = settings.Priority; Enabled = settings.Enabled; - Settings = new Dictionary< string, long >( mod.Groups.Count ); - settings.AddMissingSettings( mod ); + Settings = new Dictionary(mod.Groups.Count); + settings.AddMissingSettings(mod); - foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) - { - Settings.Add( group.Name, setting ); - } + foreach (var (group, setting) in mod.Groups.Zip(settings.Settings)) + Settings.Add(group.Name, setting); } // Convert and fix. - public bool ToSettings( Mod mod, out ModSettings settings ) + public bool ToSettings(Mod mod, out ModSettings settings) { - var list = new List< uint >( mod.Groups.Count ); + var list = new List(mod.Groups.Count); var changes = Settings.Count != mod.Groups.Count; - foreach( var group in mod.Groups ) + foreach (var group in mod.Groups) { - if( Settings.TryGetValue( group.Name, out var config ) ) + if (Settings.TryGetValue(group.Name, out var config)) { - var castConfig = ( uint )Math.Clamp( config, 0, uint.MaxValue ); - var actualConfig = FixSetting( group, castConfig ); - list.Add( actualConfig ); - if( actualConfig != config ) - { + var castConfig = (uint)Math.Clamp(config, 0, uint.MaxValue); + var actualConfig = FixSetting(group, castConfig); + list.Add(actualConfig); + if (actualConfig != config) changes = true; - } } else { - list.Add( 0 ); + list.Add(0); changes = true; } } @@ -238,28 +229,29 @@ public class ModSettings // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Does not repair settings but ignores settings not fitting to the given mod. - public (bool Enabled, int Priority, Dictionary< string, IList< string > > Settings) ConvertToShareable( Mod mod ) + public (bool Enabled, int Priority, Dictionary> Settings) ConvertToShareable(Mod mod) { - var dict = new Dictionary< string, IList< string > >( Settings.Count ); - foreach( var (setting, idx) in Settings.WithIndex() ) + var dict = new Dictionary>(Settings.Count); + foreach (var (setting, idx) in Settings.WithIndex()) { - if( idx >= mod.Groups.Count ) - { + if (idx >= mod.Groups.Count) break; - } - var group = mod.Groups[ idx ]; - if( group.Type == GroupType.Single && setting < group.Count ) + var group = mod.Groups[idx]; + if (group.Type == GroupType.Single && setting < group.Count) { - dict.Add( group.Name, new[] { group[ ( int )setting ].Name } ); + dict.Add(group.Name, new[] + { + group[(int)setting].Name, + }); } else { - var list = group.Where( ( _, optionIdx ) => ( setting & ( 1 << optionIdx ) ) != 0 ).Select( o => o.Name ).ToList(); - dict.Add( group.Name, list ); + var list = group.Where((_, optionIdx) => (setting & (1 << optionIdx)) != 0).Select(o => o.Name).ToList(); + dict.Add(group.Name, list); } } - return ( Enabled, Priority, dict ); + return (Enabled, Priority, dict); } } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 1184b6ed..2b7ebd09 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -17,7 +17,7 @@ public sealed class SingleModGroup : IModGroup public int Priority { get; set; } public uint DefaultSettings { get; set; } - public readonly List OptionData = new(); + public readonly List OptionData = []; public int OptionPriority(Index _) => Priority; From 95d5d6c4b01c9269950bf42851d20f303317541f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Feb 2024 23:30:41 +0100 Subject: [PATCH 1518/2451] Update submodules. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index c6f101bb..1a187f75 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c6f101bbef976b74eb651523445563dd81fafbaf +Subproject commit 1a187f756f2e8823197bd43db1c3383231f5eaff diff --git a/Penumbra.Api b/Penumbra.Api index 31bf4ad9..a28219ac 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 31bf4ad9b82fc980d6bda049da595368ad754931 +Subproject commit a28219ac57b53c3be6ca8c252ceb9f76ae0b6c21 diff --git a/Penumbra.GameData b/Penumbra.GameData index 63f4de73..0d267233 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 63f4de7305616b6cb8921513e5d83baa8913353f +Subproject commit 0d267233724d493d9ae2bf8d1e67bfbb8b337916 From d2bfcefb899d7e8ede7f4a424ea5de9da66c681e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Feb 2024 23:32:45 +0100 Subject: [PATCH 1519/2451] Update Combos. --- Penumbra/Import/Textures/TextureDrawer.cs | 14 +++++++------- Penumbra/Mods/Manager/ModStorage.cs | 2 +- Penumbra/Services/StainService.cs | 6 +++--- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 17 ++--------------- Penumbra/UI/CollectionTab/CollectionCombo.cs | 14 +++++--------- 6 files changed, 19 insertions(+), 36 deletions(-) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 04422116..427db92d 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -32,7 +32,8 @@ public static class TextureDrawer if (texture.LoadError is DllNotFoundException) { - ImGuiUtil.TextColored(Colors.RegexWarningBorder, "A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."); + ImGuiUtil.TextColored(Colors.RegexWarningBorder, + "A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."); if (ImGui.Button("Microsoft VC Redistributables")) Dalamud.Utility.Util.OpenLink(link); ImGuiUtil.HoverTooltip($"Open {link} in your browser."); @@ -111,14 +112,12 @@ public static class TextureDrawer } } - public sealed class PathSelectCombo : FilterComboCache<(string Path, bool Game, bool IsOnPlayer)> + public sealed class PathSelectCombo(TextureManager textures, ModEditor editor, Func> getPlayerResources) + : FilterComboCache<(string Path, bool Game, bool IsOnPlayer)>(() => CreateFiles(textures, editor, getPlayerResources), + MouseWheelType.None, Penumbra.Log) { private int _skipPrefix = 0; - public PathSelectCombo(TextureManager textures, ModEditor editor, Func> getPlayerResources) - : base(() => CreateFiles(textures, editor, getPlayerResources), Penumbra.Log) - { } - protected override string ToString((string Path, bool Game, bool IsOnPlayer) obj) => obj.Path; @@ -140,7 +139,8 @@ public static class TextureDrawer return ret; } - private static IReadOnlyList<(string Path, bool Game, bool IsOnPlayer)> CreateFiles(TextureManager textures, ModEditor editor, Func> getPlayerResources) + private static IReadOnlyList<(string Path, bool Game, bool IsOnPlayer)> CreateFiles(TextureManager textures, ModEditor editor, + Func> getPlayerResources) { var playerResources = getPlayerResources(); diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 490381d6..1e5df6b9 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -12,7 +12,7 @@ public class ModCombo : FilterComboCache => obj.Name.Text; public ModCombo(Func> generator) - : base(generator, Penumbra.Log) + : base(generator, MouseWheelType.None, Penumbra.Log) { } } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 00fc0737..26b39229 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -13,7 +13,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) { protected override float GetFilterWidth() { @@ -70,8 +70,8 @@ public class StainService : IService public StainService(IDataManager dataManager, DictStain stainData) { StainData = stainData; - StainCombo = new FilterComboColors(140, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + StainCombo = new FilterComboColors(140, MouseWheelType.None, + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 16cacaa4..89d47eb2 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -278,7 +278,7 @@ public class FileEditor : IDisposable where T : class, IWritable private readonly Configuration _config; public Combo(Configuration config, Func> generator) - : base(generator, Penumbra.Log) + : base(generator, MouseWheelType.None, Penumbra.Log) => _config = config; protected override bool DrawSelectable(int globalIdx, bool selected) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index d31a08ae..0205f3c6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -125,26 +125,13 @@ public class ItemSwapTab : IDisposable, ITab Weapon, } - private class ItemSelector : FilterComboCache + private class ItemSelector(ItemData data, FullEquipType type) + : FilterComboCache(() => data.ByType[type], MouseWheelType.None, Penumbra.Log) { - public ItemSelector(ItemData data, FullEquipType type) - : base(() => data.ByType[type], Penumbra.Log) - { } - protected override string ToString(EquipItem obj) => obj.Name; } - private class WeaponSelector : FilterComboCache - { - public WeaponSelector() - : base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes), Penumbra.Log) - { } - - protected override string ToString(FullEquipType type) - => type.ToName(); - } - private readonly Dictionary _selectors; private readonly ItemSwapContainer _swapData; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index b2ee5c3b..9d195eed 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -7,14 +7,10 @@ using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; -public sealed class CollectionCombo : FilterComboCache +public sealed class CollectionCombo(CollectionManager manager, Func> items) + : FilterComboCache(items, MouseWheelType.None, Penumbra.Log) { - private readonly CollectionManager _collectionManager; - private readonly ImRaii.Color _color = new(); - - public CollectionCombo(CollectionManager manager, Func> items) - : base(items, Penumbra.Log) - => _collectionManager = manager; + private readonly ImRaii.Color _color = new(); protected override void DrawFilter(int currentSelected, float width) { @@ -24,11 +20,11 @@ public sealed class CollectionCombo : FilterComboCache public void Draw(string label, float width, uint color) { - var current = _collectionManager.Active.ByType(CollectionType.Current, ActorIdentifier.Invalid); + var current = manager.Active.ByType(CollectionType.Current, ActorIdentifier.Invalid); _color.Push(ImGuiCol.FrameBg, color).Push(ImGuiCol.FrameBgHovered, color); if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Current); + manager.Active.SetCollection(CurrentSelection, CollectionType.Current); _color.Dispose(); } From a0ac0dfcfa16befa83d5c9f94e9191e0c0bf7e40 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Feb 2024 00:22:44 +0100 Subject: [PATCH 1520/2451] Bit more update. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 0d267233..3a7f6d86 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0d267233724d493d9ae2bf8d1e67bfbb8b337916 +Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 From 06cf6ce752b1c864cad72b5e509984cc1065d44f Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 13 Feb 2024 12:42:23 +0000 Subject: [PATCH 1521/2451] [CI] Updating repo.json for 1.0.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index fc95fef3..547ccca1 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.4", - "TestingAssemblyVersion": "1.0.0.4", + "AssemblyVersion": "1.0.0.5", + "TestingAssemblyVersion": "1.0.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 2e0e125913d2b9e92587cdefc55e0650fcbd8469 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Feb 2024 13:35:42 +0100 Subject: [PATCH 1522/2451] Fix issue with renaming mods with open advanced window. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6d406461..38fdf482 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -629,7 +629,10 @@ public partial class ModEditWindow : Window, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { - if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved) - ChangeMod(mod); + if (type is not (ModPathChangeType.Reloaded or ModPathChangeType.Moved) || mod != _mod) + return; + + _mod = null; + ChangeMod(mod); } } From 80ce6fe21f92793340267b01d8c4d9f3d2a3cafd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Feb 2024 12:59:06 +0100 Subject: [PATCH 1523/2451] Use new font functionality. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 11 ++++++----- Penumbra/UI/ModsTab/ModPanelHeader.cs | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8f90750f..6f6e4dbd 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; @@ -28,7 +29,7 @@ public sealed class CollectionPanel : IDisposable private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; - private readonly GameFontHandle _nameFont; + private readonly IFontHandle _nameFont; private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); @@ -47,7 +48,7 @@ public sealed class CollectionPanel : IDisposable _mods = mods; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); _inheritanceUi = new InheritanceUi(manager, _selector); - _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); } public void Dispose() @@ -426,7 +427,7 @@ public sealed class CollectionPanel : IDisposable ImGui.Dummy(Vector2.One); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); - using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var f = _nameFont.Available ? _nameFont.Push() : null; var name = Name(collection); var size = ImGui.CalcTextSize(name).X; var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2; @@ -445,7 +446,7 @@ public sealed class CollectionPanel : IDisposable if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) { ImGui.Dummy(Vector2.One); - using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var f = _nameFont.Available ? _nameFont.Push() : null; ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight), Colors.PressEnterWarningBg); ImGui.Dummy(Vector2.One); @@ -512,7 +513,7 @@ public sealed class CollectionPanel : IDisposable ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); } - using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var f = _nameFont.Available ? _nameFont.Push() : null; using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 4b127059..a28c3668 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Plugin; using ImGuiNET; using OtterGui; @@ -14,14 +15,14 @@ namespace Penumbra.UI.ModsTab; public class ModPanelHeader : IDisposable { /// We use a big, nice game font for the title. - private readonly GameFontHandle _nameFont; + private readonly IFontHandle _nameFont; private readonly CommunicatorService _communicator; public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) { _communicator = communicator; - _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader); } @@ -46,7 +47,7 @@ public class ModPanelHeader : IDisposable var name = $" {mod.Name} "; if (name != _modName) { - using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var f = _nameFont.Available ? _nameFont.Push() : null; _modName = name; _modNameWidth = ImGui.CalcTextSize(name).X + 2 * (ImGui.GetStyle().FramePadding.X + 2 * UiHelpers.Scale); } @@ -121,7 +122,7 @@ public class ModPanelHeader : IDisposable using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); - using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + using var f = _nameFont.Available ? _nameFont.Push() : null; ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); return offset; } From 31bc5ec6f9d4b21dfa1b09f8764f87860cffc14b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Feb 2024 13:03:56 +0100 Subject: [PATCH 1524/2451] Make settings change invoke on Temporary Mods. --- Penumbra.Api | 2 +- Penumbra/Api/TempModManager.cs | 3 +++ Penumbra/Collections/Cache/CollectionCacheManager.cs | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index a28219ac..2b6bcf33 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a28219ac57b53c3be6ca8c252ceb9f76ae0b6c21 +Subproject commit 2b6bcf338794b34bcba2730c70dcbb73ce97311b diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index efbfd7f9..c7840b75 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,3 +1,4 @@ +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -84,11 +85,13 @@ public class TempModManager : IDisposable { Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); collection.Remove(mod); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 0, 0, false); } else { Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); collection.Apply(mod, created); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 1, 0, false); } } else diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 7d4a5722..4e524ddf 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -322,6 +322,9 @@ public class CollectionCacheManager : IDisposable case ModSettingChange.MultiEnableState: FullRecalculation(collection); break; + case ModSettingChange.TemporaryMod: + // handled otherwise + break; } } From a2bf477481f78c62333947498fc21c092fc505f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Feb 2024 14:46:22 +0100 Subject: [PATCH 1525/2451] Cleanup. --- Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 776f2f92..31387101 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -42,7 +42,7 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr Date: Sun, 18 Feb 2024 13:48:18 +0000 Subject: [PATCH 1526/2451] [CI] Updating repo.json for 1.0.0.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 547ccca1..29b1647d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.5", - "TestingAssemblyVersion": "1.0.0.5", + "AssemblyVersion": "1.0.0.6", + "TestingAssemblyVersion": "1.0.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 529788d2e57b95adf434dac84b119b17c02921d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Feb 2024 16:26:33 +0100 Subject: [PATCH 1527/2451] Change font pushes. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 6 +++--- Penumbra/UI/ModsTab/ModPanelHeader.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 6f6e4dbd..4d922af5 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -427,7 +427,7 @@ public sealed class CollectionPanel : IDisposable ImGui.Dummy(Vector2.One); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); - using var f = _nameFont.Available ? _nameFont.Push() : null; + using var f = _nameFont.Push(); var name = Name(collection); var size = ImGui.CalcTextSize(name).X; var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2; @@ -446,7 +446,7 @@ public sealed class CollectionPanel : IDisposable if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) { ImGui.Dummy(Vector2.One); - using var f = _nameFont.Available ? _nameFont.Push() : null; + using var f = _nameFont.Push(); ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight), Colors.PressEnterWarningBg); ImGui.Dummy(Vector2.One); @@ -513,7 +513,7 @@ public sealed class CollectionPanel : IDisposable ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); } - using var f = _nameFont.Available ? _nameFont.Push() : null; + using var f = _nameFont.Push(); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index a28c3668..ed499b4f 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -47,7 +47,7 @@ public class ModPanelHeader : IDisposable var name = $" {mod.Name} "; if (name != _modName) { - using var f = _nameFont.Available ? _nameFont.Push() : null; + using var f = _nameFont.Push(); _modName = name; _modNameWidth = ImGui.CalcTextSize(name).X + 2 * (ImGui.GetStyle().FramePadding.X + 2 * UiHelpers.Scale); } @@ -122,7 +122,7 @@ public class ModPanelHeader : IDisposable using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); - using var f = _nameFont.Available ? _nameFont.Push() : null; + using var f = _nameFont.Push(); ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); return offset; } From add4b8aa83b55f1e274bd994396faaeda3809226 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Feb 2024 21:44:49 +0100 Subject: [PATCH 1528/2451] Misc. --- Penumbra/Collections/ModCollectionSave.cs | 31 ++++++------------- .../ResourceLoading/CreateFileWHook.cs | 2 +- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 4cc7706e..f2cb4ada 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -1,32 +1,21 @@ using Newtonsoft.Json.Linq; -using Penumbra.Mods; using Penumbra.Services; using Newtonsoft.Json; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; -using Penumbra.Util; namespace Penumbra.Collections; /// /// Handle saving and loading a collection. /// -internal readonly struct ModCollectionSave : ISavable +internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection modCollection) : ISavable { - private readonly ModStorage _modStorage; - private readonly ModCollection _modCollection; - - public ModCollectionSave(ModStorage modStorage, ModCollection modCollection) - { - _modStorage = modStorage; - _modCollection = modCollection; - } - public string ToFilename(FilenameService fileNames) - => fileNames.CollectionFile(_modCollection); + => fileNames.CollectionFile(modCollection); public string LogName(string _) - => _modCollection.AnonymizedName; + => modCollection.AnonymizedName; public string TypeName => "Collection"; @@ -40,20 +29,20 @@ internal readonly struct ModCollectionSave : ISavable j.WritePropertyName("Version"); j.WriteValue(ModCollection.CurrentVersion); j.WritePropertyName(nameof(ModCollection.Name)); - j.WriteValue(_modCollection.Name); + j.WriteValue(modCollection.Name); j.WritePropertyName(nameof(ModCollection.Settings)); // Write all used and unused settings by mod directory name. j.WriteStartObject(); - var list = new List<(string, ModSettings.SavedSettings)>(_modCollection.Settings.Count + _modCollection.UnusedSettings.Count); - for (var i = 0; i < _modCollection.Settings.Count; ++i) + var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count); + for (var i = 0; i < modCollection.Settings.Count; ++i) { - var settings = _modCollection.Settings[i]; + var settings = modCollection.Settings[i]; if (settings != null) - list.Add((_modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, _modStorage[i]))); + list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i]))); } - list.AddRange(_modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); + list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); foreach (var (modDir, settings) in list) @@ -66,7 +55,7 @@ internal readonly struct ModCollectionSave : ISavable // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, _modCollection.InheritanceByName ?? _modCollection.DirectlyInheritsFrom.Select(c => c.Name)); + x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Name)); j.WriteEndObject(); } diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index 7d94b1d5..8a5e779b 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -100,7 +100,7 @@ public unsafe class CreateFileWHook : IDisposable { // Use static storage. var ptr = WriteFileName(name); - Penumbra.Log.Verbose($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}."); + Penumbra.Log.Excessive($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}."); return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template); } From 1000841f69b8c439e498d2b36986514aadc9d3a7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Feb 2024 12:06:57 +0100 Subject: [PATCH 1529/2451] Add some settingchanged events. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 46 +++++--- .../Cache/CollectionCacheManager.cs | 1 + .../Collections/Manager/CollectionManager.cs | 31 +++--- Penumbra/Communication/ModOptionChanged.cs | 4 + Penumbra/Communication/ModPathChanged.cs | 6 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 61 +++++------ Penumbra/Mods/Manager/ModOptionEditor.cs | 101 ++++++++---------- 9 files changed, 128 insertions(+), 126 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 2b6bcf33..79ffdd69 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2b6bcf338794b34bcba2730c70dcbb73ce97311b +Subproject commit 79ffdd69a28141a1ac93daa24d76573b2fa0d71e diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1a0764c7..59ef6677 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -141,6 +141,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); + _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); } public unsafe void Dispose() @@ -342,10 +343,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_config.EnableMods) - return new[] - { - path, - }; + return [path]; var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); @@ -355,10 +353,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_config.EnableMods) - return new[] - { - path, - }; + return [path]; AssociatedCollection(gameObjectIdx, out var collection); var ret = collection.ReverseResolvePath(new FullPath(path)); @@ -369,10 +364,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_config.EnableMods) - return new[] - { - path, - }; + return [path]; var ret = _collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); @@ -698,6 +690,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi { switch (type) { + case ModPathChangeType.Reloaded: + TriggerSettingEdited(mod); + break; case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break; @@ -1262,4 +1257,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); + + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + { + switch (type) + { + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.GroupMoved: + case ModOptionChangeType.GroupTypeChanged: + case ModOptionChangeType.PriorityChanged: + case ModOptionChangeType.OptionDeleted: + case ModOptionChangeType.OptionMoved: + case ModOptionChangeType.OptionFilesChanged: + case ModOptionChangeType.OptionFilesAdded: + case ModOptionChangeType.OptionSwapsChanged: + case ModOptionChangeType.OptionMetaChanged: + TriggerSettingEdited(mod); + break; + } + } + + private void TriggerSettingEdited(Mod mod) + { + var collection = _collectionResolver.PlayerCollection(); + var (settings, parent) = collection[mod.Index]; + if (settings != null) + ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); + } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 4e524ddf..94b3ef5a 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -323,6 +323,7 @@ public class CollectionCacheManager : IDisposable FullRecalculation(collection); break; case ModSettingChange.TemporaryMod: + case ModSettingChange.Edited: // handled otherwise break; } diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index 16bf754c..e95617b1 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -2,23 +2,18 @@ using Penumbra.Collections.Cache; namespace Penumbra.Collections.Manager; -public class CollectionManager +public class CollectionManager( + CollectionStorage storage, + ActiveCollections active, + InheritanceManager inheritances, + CollectionCacheManager caches, + TempCollectionManager temp, + CollectionEditor editor) { - public readonly CollectionStorage Storage; - public readonly ActiveCollections Active; - public readonly InheritanceManager Inheritances; - public readonly CollectionCacheManager Caches; - public readonly TempCollectionManager Temp; - public readonly CollectionEditor Editor; - - public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances, - CollectionCacheManager caches, TempCollectionManager temp, CollectionEditor editor) - { - Storage = storage; - Active = active; - Inheritances = inheritances; - Caches = caches; - Temp = temp; - Editor = editor; - } + public readonly CollectionStorage Storage = storage; + public readonly ActiveCollections Active = active; + public readonly InheritanceManager Inheritances = inheritances; + public readonly CollectionCacheManager Caches = caches; + public readonly TempCollectionManager Temp = temp; + public readonly CollectionEditor Editor = editor; } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index a0b4d26c..f02b17dc 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -18,6 +19,9 @@ public sealed class ModOptionChanged() { public enum Priority { + /// + Api = int.MinValue, + /// CollectionCacheManager = -100, diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index e6291781..01c8fa64 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -19,15 +19,15 @@ public sealed class ModPathChanged() { public enum Priority { + /// + Api = int.MinValue, + /// EphemeralConfig = -500, /// CollectionCacheManagerAddition = -100, - /// - Api = 0, - /// ModCacheManager = 0, diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index bbf0d4b5..31aefdf5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -2,7 +2,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class ModMetaEditor(ModManager modManager) { diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 6c5f9c25..e0af6f36 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -23,17 +23,8 @@ public enum ModDataChangeType : ushort Note = 0x0800, } -public class ModDataEditor +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) { - private readonly SaveService _saveService; - private readonly CommunicatorService _communicatorService; - - public ModDataEditor(SaveService saveService, CommunicatorService communicatorService) - { - _saveService = saveService; - _communicatorService = communicatorService; - } - /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, string? website) @@ -44,12 +35,12 @@ public class ModDataEditor mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - _saveService.ImmediateSave(new ModMeta(mod)); + saveService.ImmediateSave(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) { - var dataFile = _saveService.FileNames.LocalDataFile(mod); + var dataFile = saveService.FileNames.LocalDataFile(mod); var importDate = 0L; var localTags = Enumerable.Empty(); @@ -101,14 +92,14 @@ public class ModDataEditor } if (save) - _saveService.QueueSave(new ModLocalData(mod)); + saveService.QueueSave(new ModLocalData(mod)); return changes; } public ModDataChangeType LoadMeta(ModCreator creator, Mod mod) { - var metaFile = _saveService.FileNames.ModMetaPath(mod); + var metaFile = saveService.FileNames.ModMetaPath(mod); if (!File.Exists(metaFile)) { Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); @@ -161,10 +152,10 @@ public class ModDataEditor } if (newFileVersion != ModMeta.FileVersion) - if (ModMigration.Migrate(creator, _saveService, mod, json, ref newFileVersion)) + if (ModMigration.Migrate(creator, saveService, mod, json, ref newFileVersion)) { changes |= ModDataChangeType.Migration; - _saveService.ImmediateSave(new ModMeta(mod)); + saveService.ImmediateSave(new ModMeta(mod)); } if (importDate != null && mod.ImportDate != importDate.Value) @@ -191,8 +182,8 @@ public class ModDataEditor var oldName = mod.Name; mod.Name = newName; - _saveService.QueueSave(new ModMeta(mod)); - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); } public void ChangeModAuthor(Mod mod, string newAuthor) @@ -201,8 +192,8 @@ public class ModDataEditor return; mod.Author = newAuthor; - _saveService.QueueSave(new ModMeta(mod)); - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); } public void ChangeModDescription(Mod mod, string newDescription) @@ -211,8 +202,8 @@ public class ModDataEditor return; mod.Description = newDescription; - _saveService.QueueSave(new ModMeta(mod)); - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); } public void ChangeModVersion(Mod mod, string newVersion) @@ -221,8 +212,8 @@ public class ModDataEditor return; mod.Version = newVersion; - _saveService.QueueSave(new ModMeta(mod)); - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); } public void ChangeModWebsite(Mod mod, string newWebsite) @@ -231,8 +222,8 @@ public class ModDataEditor return; mod.Website = newWebsite; - _saveService.QueueSave(new ModMeta(mod)); - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); } public void ChangeModTag(Mod mod, int tagIdx, string newTag) @@ -247,9 +238,9 @@ public class ModDataEditor return; mod.Favorite = state; - _saveService.QueueSave(new ModLocalData(mod)); + saveService.QueueSave(new ModLocalData(mod)); ; - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } public void ChangeModNote(Mod mod, string newNote) @@ -258,9 +249,9 @@ public class ModDataEditor return; mod.Note = newNote; - _saveService.QueueSave(new ModLocalData(mod)); + saveService.QueueSave(new ModLocalData(mod)); ; - _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } private void ChangeTag(Mod mod, int tagIdx, string newTag, bool local) @@ -282,19 +273,19 @@ public class ModDataEditor } if (flags.HasFlag(ModDataChangeType.ModTags)) - _saveService.QueueSave(new ModMeta(mod)); + saveService.QueueSave(new ModMeta(mod)); if (flags.HasFlag(ModDataChangeType.LocalTags)) - _saveService.QueueSave(new ModLocalData(mod)); + saveService.QueueSave(new ModLocalData(mod)); if (flags != 0) - _communicatorService.ModDataChanged.Invoke(flags, mod, null); + communicatorService.ModDataChanged.Invoke(flags, mod, null); } public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod) { - var oldFile = _saveService.FileNames.LocalDataFile(oldMod.Name); - var newFile = _saveService.FileNames.LocalDataFile(newMod.Name); + var oldFile = saveService.FileNames.LocalDataFile(oldMod.Name); + var newFile = saveService.FileNames.LocalDataFile(newMod.Name); if (!File.Exists(oldFile)) return; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 73cb80cc..3459ce1a 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -31,19 +31,8 @@ public enum ModOptionChangeType DefaultOptionChanged, } -public class ModOptionEditor +public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - - public ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) - { - _communicator = communicator; - _saveService = saveService; - _config = config; - } - /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { @@ -52,8 +41,8 @@ public class ModOptionEditor return; mod.Groups[groupIdx] = group.Convert(type); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } /// Change the settings stored as default options in a mod. @@ -64,8 +53,8 @@ public class ModOptionEditor return; group.DefaultSettings = defaultOption; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); } /// Rename an option group if possible. @@ -76,7 +65,7 @@ public class ModOptionEditor if (oldName == newName || !VerifyFileName(mod, group, newName, true)) return; - _saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); + saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); var _ = group switch { SingleModGroup s => s.Name = newName, @@ -84,8 +73,8 @@ public class ModOptionEditor _ => newName, }; - _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); + saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } /// Add a new, empty option group of the given type and name. @@ -107,8 +96,8 @@ public class ModOptionEditor Name = newName, Priority = maxPriority, }); - _saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); + saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } /// Add a new mod, empty option group of the given type and name if it does not exist already. @@ -128,11 +117,11 @@ public class ModOptionEditor /// Delete a given option group. Fires an event to prepare before actually deleting. public void DeleteModGroup(Mod mod, int groupIdx) { - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); UpdateSubModPositions(mod, groupIdx); - _saveService.SaveAllOptionGroups(mod, false, _config.ReplaceNonAsciiOnImport); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } /// Move the index of a given option group. @@ -142,8 +131,8 @@ public class ModOptionEditor return; UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - _saveService.SaveAllOptionGroups(mod, false, _config.ReplaceNonAsciiOnImport); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } /// Change the description of the given option group. @@ -159,8 +148,8 @@ public class ModOptionEditor MultiModGroup m => m.Description = newDescription, _ => newDescription, }; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } /// Change the description of the given option. @@ -172,8 +161,8 @@ public class ModOptionEditor return; s.Description = newDescription; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } /// Change the internal priority of the given option group. @@ -189,8 +178,8 @@ public class ModOptionEditor MultiModGroup m => m.Priority = newPriority, _ => newPriority, }; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } /// Change the internal priority of the given option. @@ -206,8 +195,8 @@ public class ModOptionEditor return; m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; } } @@ -232,8 +221,8 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } /// Add a new empty option of the given name for the given group. @@ -252,8 +241,8 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } /// Add a new empty option of the given name for the given group if it does not exist already. @@ -298,15 +287,15 @@ public class ModOptionEditor break; } - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } /// Delete the given option from the given group. public void DeleteOption(Mod mod, int groupIdx, int optionIdx) { var group = mod.Groups[groupIdx]; - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); switch (group) { case SingleModGroup s: @@ -319,8 +308,8 @@ public class ModOptionEditor } group.UpdatePositions(optionIdx); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } /// Move an option inside the given option group. @@ -330,8 +319,8 @@ public class ModOptionEditor if (!group.MoveOption(optionIdxFrom, optionIdxTo)) return; - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); } /// Set the meta manipulations for a given option. Replaces existing manipulations. @@ -342,10 +331,10 @@ public class ModOptionEditor && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData.SetTo(manipulations); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. @@ -355,10 +344,10 @@ public class ModOptionEditor if (subMod.FileData.SetEquals(replacements)) return; - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData.SetTo(replacements); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. @@ -369,8 +358,8 @@ public class ModOptionEditor subMod.FileData.AddFrom(additions); if (oldCount != subMod.FileData.Count) { - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); } } @@ -381,10 +370,10 @@ public class ModOptionEditor if (subMod.FileSwapData.SetEquals(swaps)) return; - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData.SetTo(swaps); - _saveService.QueueSave(new ModSaveGroup(mod, groupIdx, _config.ReplaceNonAsciiOnImport)); - _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } From 883580d4654c66a92e0435a628878d3b4fe38a17 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 24 Feb 2024 11:09:19 +0000 Subject: [PATCH 1530/2451] [CI] Updating repo.json for 1.0.0.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 29b1647d..1a599362 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.6", - "TestingAssemblyVersion": "1.0.0.6", + "AssemblyVersion": "1.0.0.7", + "TestingAssemblyVersion": "1.0.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 7b0be25f6ee4d07eae56248887abbb16847b1160 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Feb 2024 14:04:39 +0100 Subject: [PATCH 1531/2451] Add even more setting changed events and add auto-player-redraw on saving files. --- Penumbra/Api/PenumbraApi.cs | 14 ++- .../Collections/Manager/CollectionStorage.cs | 32 +++++- Penumbra/Communication/ModFileChanged.cs | 28 ++++++ Penumbra/Configuration.cs | 1 - Penumbra/EphemeralConfig.cs | 1 + Penumbra/Interop/Services/RedrawService.cs | 31 ++++-- Penumbra/Mods/Editor/ModFileEditor.cs | 4 +- Penumbra/Services/CommunicatorService.cs | 3 + Penumbra/UI/AdvancedWindow/FileEditor.cs | 99 ++++++++++--------- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../ModEditWindow.Materials.Shpk.cs | 2 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +- .../ModEditWindow.QuickImport.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 38 ++++++- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 55 ++++++----- 17 files changed, 221 insertions(+), 99 deletions(-) create mode 100644 Penumbra/Communication/ModFileChanged.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 59ef6677..aed1a963 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -24,6 +24,7 @@ using Penumbra.Interop.Services; using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; using Penumbra.Interop.ResourceTree; +using Penumbra.Mods.Editor; namespace Penumbra.Api; @@ -142,6 +143,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); } public unsafe void Dispose() @@ -153,6 +155,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); _lumina = null; _communicator = null!; _modManager = null!; @@ -1277,11 +1281,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + TriggerSettingEdited(mod); + } + private void TriggerSettingEdited(Mod mod) { var collection = _collectionResolver.PlayerCollection(); var (settings, parent) = collection[mod.Index]; - if (settings != null) + if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); } } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index c43c3817..a84c79e6 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -4,6 +4,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -56,6 +57,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage); ReadCollections(out DefaultNamed); } @@ -65,6 +67,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); _communicator.ModPathChanged.Unsubscribe(OnModPathChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } /// @@ -104,7 +107,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!CanAddCollection(name, out var fixedName)) { Penumbra.Messager.NotificationMessage( - $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, false); + $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, + false); return false; } @@ -185,20 +189,23 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!IsValidName(name)) { // TODO: handle better. - Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + NotificationType.Warning); continue; } if (ByName(name, out _)) { - Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", + NotificationType.Warning); continue; } var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", + NotificationType.Warning); _collections.Add(collection); } @@ -220,7 +227,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable return _collections[^1]; Penumbra.Messager.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", NotificationType.Error); + $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", + NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; } @@ -273,4 +281,18 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } + + /// Update change counters when changing files. + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + foreach (var collection in this) + { + var (settings, _) = collection[mod.Index]; + if (settings is { Enabled: true }) + collection.IncrementCounter(); + } + } } diff --git a/Penumbra/Communication/ModFileChanged.cs b/Penumbra/Communication/ModFileChanged.cs new file mode 100644 index 00000000..8b4b6f5d --- /dev/null +++ b/Penumbra/Communication/ModFileChanged.cs @@ -0,0 +1,28 @@ +using OtterGui.Classes; +using Penumbra.Api; +using Penumbra.Mods; +using Penumbra.Mods.Editor; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever an existing file in a mod is overwritten by Penumbra. +/// +/// Parameter is the changed mod. +/// Parameter file registry of the changed file. +/// +public sealed class ModFileChanged() + : EventWrapper(nameof(ModFileChanged)) +{ + public enum Priority + { + /// + Api = int.MinValue, + + /// + RedrawService = -50, + + /// + CollectionStorage = 0, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a5a615bd..188be65d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -47,7 +47,6 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 8cf23de6..98b1a5d6 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -39,6 +39,7 @@ public class EphemeralConfig : ISavable, IDisposable public bool FixMainWindow { get; set; } = false; public string LastModPath { get; set; } = string.Empty; public bool AdvancedEditingOpen { get; set; } = false; + public bool ForceRedrawOnFileChange { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index e2e57b1c..c1bd8573 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -8,9 +8,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Housing; using FFXIVClientStructs.Interop; using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Mods.Editor; +using Penumbra.Services; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Services; @@ -106,11 +110,13 @@ public sealed unsafe partial class RedrawService : IDisposable { private const int FurnitureIdx = 1337; - private readonly IFramework _framework; - private readonly IObjectTable _objects; - private readonly ITargetManager _targets; - private readonly ICondition _conditions; - private readonly IClientState _clientState; + private readonly IFramework _framework; + private readonly IObjectTable _objects; + private readonly ITargetManager _targets; + private readonly ICondition _conditions; + private readonly IClientState _clientState; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; private readonly List _queue = new(100); private readonly List _afterGPoseQueue = new(GPoseSlots); @@ -127,19 +133,24 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState) + public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState, + Configuration config, CommunicatorService communicator) { _framework = framework; _objects = objects; _targets = targets; _conditions = conditions; _clientState = clientState; + _config = config; + _communicator = communicator; _framework.Update += OnUpdateEvent; + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.RedrawService); } public void Dispose() { _framework.Update -= OnUpdateEvent; + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } public static DrawState* ActorDrawState(GameObject actor) @@ -419,4 +430,12 @@ public sealed unsafe partial class RedrawService : IDisposable gameObject->DisableDraw(); } } + + private void OnModFileChanged(Mod _1, FileRegistry _2) + { + if (!_config.ForceRedrawOnFileChange) + return; + + RedrawObject(0, RedrawType.Redraw); + } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 5328b8fe..30e97093 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,10 +1,11 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileEditor(ModFileCollection files, ModManager modManager) +public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) { public bool Changes { get; private set; } @@ -136,6 +137,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager) try { File.Delete(file.File.FullName); + communicator.ModFileChanged.Invoke(mod, file); Penumbra.Log.Debug($"[DeleteFiles] Deleted {file.File.FullName} from {mod.Name}."); ++deletions; } diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index be94a31e..da852855 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -42,6 +42,9 @@ public class CommunicatorService : IDisposable, IService /// public readonly ModDirectoryChanged ModDirectoryChanged = new(); + /// + public readonly ModFileChanged ModFileChanged = new(); + /// public readonly ModPathChanged ModPathChanged = new(); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 89d47eb2..c891d33a 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -8,39 +8,32 @@ using OtterGui.Compression; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData.Files; -using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor : IDisposable where T : class, IWritable +public class FileEditor( + ModEditWindow owner, + CommunicatorService communicator, + IDataManager gameData, + Configuration config, + FileCompactor compactor, + FileDialogService fileDialog, + string tabName, + string fileType, + Func> getFiles, + Func drawEdit, + Func getInitialPath, + Func parseFile) + : IDisposable + where T : class, IWritable { - private readonly FileDialogService _fileDialog; - private readonly IDataManager _gameData; - private readonly ModEditWindow _owner; - private readonly FileCompactor _compactor; - - public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileCompactor compactor, FileDialogService fileDialog, - string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) - { - _owner = owner; - _gameData = gameData; - _fileDialog = fileDialog; - _tabName = tabName; - _fileType = fileType; - _drawEdit = drawEdit; - _getInitialPath = getInitialPath; - _parseFile = parseFile; - _compactor = compactor; - _combo = new Combo(config, getFiles); - } - public void Draw() { - using var tab = ImRaii.TabItem(_tabName); + using var tab = ImRaii.TabItem(tabName); if (!tab) { _quickImport = null; @@ -53,12 +46,26 @@ public class FileEditor : IDisposable where T : class, IWritable ImGui.SameLine(); ResetButton(); ImGui.SameLine(); + RedrawOnSaveBox(); + ImGui.SameLine(); DefaultInput(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawFilePanel(); } + private void RedrawOnSaveBox() + { + var redraw = config.Ephemeral.ForceRedrawOnFileChange; + if (ImGui.Checkbox("Redraw on Save", ref redraw)) + { + config.Ephemeral.ForceRedrawOnFileChange = redraw; + config.Ephemeral.Save(); + } + + ImGuiUtil.HoverTooltip("Force a redraw of your player character whenever you save a file here."); + } + public void Dispose() { (_currentFile as IDisposable)?.Dispose(); @@ -67,12 +74,6 @@ public class FileEditor : IDisposable where T : class, IWritable _defaultFile = null; } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; - private FileRegistry? _currentPath; private T? _currentFile; private Exception? _currentException; @@ -85,7 +86,7 @@ public class FileEditor : IDisposable where T : class, IWritable private T? _defaultFile; private Exception? _defaultException; - private readonly Combo _combo; + private readonly Combo _combo = new(config, getFiles); private ModEditWindow.QuickImportAction? _quickImport; @@ -99,16 +100,16 @@ public class FileEditor : IDisposable where T : class, IWritable { _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); _quickImport = null; - _fileDialog.Reset(); + fileDialog.Reset(); try { - var file = _gameData.GetFile(_defaultPath); + var file = gameData.GetFile(_defaultPath); if (file != null) { _defaultException = null; (_defaultFile as IDisposable)?.Dispose(); _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _defaultFile = _parseFile(file.Data, _defaultPath, false); + _defaultFile = parseFile(file.Data, _defaultPath, false); } else { @@ -126,7 +127,7 @@ public class FileEditor : IDisposable where T : class, IWritable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", _defaultFile == null, true)) - _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, + fileDialog.OpenSavePicker($"Export {_defaultPath} to...", fileType, Path.GetFileNameWithoutExtension(_defaultPath), fileType, (success, name) => { if (!success) @@ -134,16 +135,16 @@ public class FileEditor : IDisposable where T : class, IWritable try { - _compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); } catch (Exception e) { Penumbra.Messager.NotificationMessage(e, $"Could not export {_defaultPath}.", NotificationType.Error); } - }, _getInitialPath(), false); + }, getInitialPath(), false); _quickImport ??= - ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); + ModEditWindow.QuickImportAction.Prepare(owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) @@ -172,7 +173,7 @@ public class FileEditor : IDisposable where T : class, IWritable private void DrawFileSelectCombo() { - if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...", string.Empty, + if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty, ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight()) && _combo.CurrentSelection != null) UpdateCurrentFile(_combo.CurrentSelection); @@ -191,7 +192,7 @@ public class FileEditor : IDisposable where T : class, IWritable var bytes = File.ReadAllBytes(_currentPath.File.FullName); (_currentFile as IDisposable)?.Dispose(); _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); + _currentFile = parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { @@ -204,9 +205,11 @@ public class FileEditor : IDisposable where T : class, IWritable private void SaveButton() { if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) + $"Save the selected {fileType} file with all changes applied. This is not revertible.", !_changed)) { - _compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + if (owner.Mod != null) + communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); _changed = false; } } @@ -214,7 +217,7 @@ public class FileEditor : IDisposable where T : class, IWritable private void ResetButton() { if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed)) + $"Reset all changes made to the {fileType} file.", !_changed)) { var tmp = _currentPath; _currentPath = null; @@ -232,7 +235,7 @@ public class FileEditor : IDisposable where T : class, IWritable { if (_currentFile == null) { - ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); + ImGui.TextUnformatted($"Could not parse selected {fileType} file."); if (_currentException != null) { using var tab = ImRaii.PushIndent(); @@ -242,7 +245,7 @@ public class FileEditor : IDisposable where T : class, IWritable else { using var id = ImRaii.PushId(0); - _changed |= _drawEdit(_currentFile, false); + _changed |= drawEdit(_currentFile, false); } } @@ -258,7 +261,7 @@ public class FileEditor : IDisposable where T : class, IWritable if (_defaultFile == null) { - ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); + ImGui.TextUnformatted($"Could not parse provided {fileType} game file:\n"); if (_defaultException != null) { using var tab = ImRaii.PushIndent(); @@ -268,7 +271,7 @@ public class FileEditor : IDisposable where T : class, IWritable else { using var id = ImRaii.PushId(1); - _drawEdit(_defaultFile, true); + drawEdit(_defaultFile, true); } } } @@ -283,7 +286,7 @@ public class FileEditor : IDisposable where T : class, IWritable protected override bool DrawSelectable(int globalIdx, bool selected) { - var file = Items[globalIdx]; + var file = Items[globalIdx]; bool ret; using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index bae23729..c8db7770 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -80,7 +80,7 @@ public partial class ModEditWindow return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, - _editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); }); void DrawLine((string, string, string, uint) data) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 9e9557d3..3ce10224 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -143,7 +143,7 @@ public partial class ModEditWindow { if (success) tab.LoadShpk(new FullPath(name[0])); - }, 1, _mod!.ModPath.FullName, false); + }, 1, Mod!.ModPath.FullName, false); var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index df20d60f..fab41c7d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -209,7 +209,7 @@ public partial class ModEditWindow info.Restore(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]); + ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); var tmp = info.CurrentMaterials[0]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 20550a15..aad70cb3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -60,7 +60,7 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) - _metaFileManager.WriteAllTexToolsMeta(_mod!); + _metaFileManager.WriteAllTexToolsMeta(Mod!); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 561cbed7..67ec97f2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -94,7 +94,7 @@ public partial class ModEditWindow { if (success && paths.Count > 0) tab.Import(paths[0]); - }, 1, _mod!.ModPath.FullName, false); + }, 1, Mod!.ModPath.FullName, false); ImGui.SameLine(); DrawDocumentationLink(MdlImportDocumentation); @@ -142,7 +142,7 @@ public partial class ModEditWindow tab.Export(path, gamePath); }, - _mod!.ModPath.FullName, + Mod!.ModPath.FullName, false ); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index c9cd3d06..9a38a5d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -195,7 +195,7 @@ public partial class ModEditWindow if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) return new QuickImportAction(editor, optionName, gamePath); - var mod = owner._mod; + var mod = owner.Mod; if (mod == null) return new QuickImportAction(editor, optionName, gamePath); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 34d0800c..71c64059 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Raii; using OtterTex; using Penumbra.Import.Textures; +using Penumbra.Mods; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -45,10 +46,10 @@ public partial class ModEditWindow using (var disabled = ImRaii.Disabled(!_center.SaveTask.IsCompleted)) { TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", - "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); + "Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); if (_textureSelectCombo.Draw("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, - _mod.ModPath.FullName.Length + 1, out var newPath) + Mod.ModPath.FullName.Length + 1, out var newPath) && newPath != tex.Path) tex.Load(_textures, newPath); @@ -84,6 +85,18 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(newDesc); } + } + + private void RedrawOnSaveBox() + { + var redraw = _config.Ephemeral.ForceRedrawOnFileChange; + if (ImGui.Checkbox("Redraw on Save", ref redraw)) + { + _config.Ephemeral.ForceRedrawOnFileChange = redraw; + _config.Ephemeral.Save(); + } + + ImGuiUtil.HoverTooltip("Force a redraw of your player character whenever you save a file here."); } private void MipMapInput() @@ -103,6 +116,8 @@ public partial class ModEditWindow if (_center.IsLoaded) { + RedrawOnSaveBox(); + ImGui.SameLine(); SaveAsCombo(); ImGui.SameLine(); MipMapInput(); @@ -118,6 +133,7 @@ public partial class ModEditWindow tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -141,6 +157,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -150,6 +167,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -160,6 +178,7 @@ public partial class ModEditWindow || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } } @@ -192,6 +211,18 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } + private void InvokeChange(Mod? mod, string path) + { + if (mod == null) + return; + + if (!_editor.Files.Tex.FindFirst(r => string.Equals(r.File.FullName, path, StringComparison.OrdinalIgnoreCase), + out var registry)) + return; + + _communicator.ModFileChanged.Invoke(mod, registry); + } + private void OpenSaveAsDialog(string defaultExtension) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); @@ -201,12 +232,13 @@ public partial class ModEditWindow if (a) { _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + InvokeChange(Mod, b); if (b == _left.Path) AddReloadTask(_left.Path, false); else if (b == _right.Path) AddReloadTask(_right.Path, true); } - }, _mod!.ModPath.FullName, _forceTextureStartPath); + }, Mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 38fdf482..afa846b5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -49,17 +49,18 @@ public partial class ModEditWindow : Window, IDisposable private readonly IObjectTable _objects; private readonly CharacterBaseDestructor _characterBaseDestructor; - private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; + public Mod? Mod { get; private set; } + public void ChangeMod(Mod mod) { - if (mod == _mod) + if (mod == Mod) return; _editor.LoadMod(mod, -1, 0); - _mod = mod; + Mod = mod; SizeConstraints = new WindowSizeConstraints { @@ -80,12 +81,12 @@ public partial class ModEditWindow : Window, IDisposable public void UpdateModels() { - if (_mod != null) - _editor.MdlMaterialEditor.ScanModels(_mod); + if (Mod != null) + _editor.MdlMaterialEditor.ScanModels(Mod); } public override bool DrawConditions() - => _mod != null; + => Mod != null; public override void PreDraw() { @@ -106,13 +107,13 @@ public partial class ModEditWindow : Window, IDisposable }); var manipulations = 0; var subMods = 0; - var swaps = _mod!.AllSubMods.Sum(m => + var swaps = Mod!.AllSubMods.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; return m.FileSwaps.Count; }); - sb.Append(_mod!.Name); + sb.Append(Mod!.Name); if (subMods > 1) sb.Append($" | {subMods} Options"); @@ -271,7 +272,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.NewLine(); if (ImGui.Button("Remove Missing Files from Mod")) - _editor.FileEditor.RemoveMissingPaths(_mod!, _editor.Option!); + _editor.FileEditor.RemoveMissingPaths(Mod!, _editor.Option!); using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true); if (!child) @@ -324,8 +325,8 @@ public partial class ModEditWindow : Window, IDisposable } else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - _editor.ModNormalizer.Normalize(_mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx)); + _editor.ModNormalizer.Normalize(Mod!); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx)); } if (!_editor.Duplicates.Worker.IsCompleted) @@ -363,7 +364,7 @@ public partial class ModEditWindow : Window, IDisposable foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); - using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], + using var tree = ImRaii.TreeNode(set[0].FullName[(Mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.NoTreePushOnOpen); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(Functions.HumanReadableSize(size)); @@ -384,7 +385,7 @@ public partial class ModEditWindow : Window, IDisposable { ImGui.TableNextColumn(); ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); - using var node = ImRaii.TreeNode(duplicate.FullName[(_mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); + using var node = ImRaii.TreeNode(duplicate.FullName[(Mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); ImGui.TableNextColumn(); ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); ImGui.TableNextColumn(); @@ -421,7 +422,7 @@ public partial class ModEditWindow : Window, IDisposable if (!combo) return ret; - foreach (var (option, idx) in _mod!.AllSubMods.WithIndex()) + foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) { using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) @@ -537,10 +538,10 @@ public partial class ModEditWindow : Window, IDisposable if (currentFile != null) return currentFile.Value; - if (_mod != null) - foreach (var option in _mod.Groups.OrderByDescending(g => g.Priority) + if (Mod != null) + foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority) .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) - .Append(_mod.Default)) + .Append(Mod.Default)) { if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) return value; @@ -559,8 +560,8 @@ public partial class ModEditWindow : Window, IDisposable ret.Add(path); } - if (_mod != null) - foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + if (Mod != null) + foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) { foreach (var path in option.Files.Keys) { @@ -596,15 +597,15 @@ public partial class ModEditWindow : Window, IDisposable _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; - _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", - () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, + _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", + () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); - _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, + _modelTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path)); - _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", + _shaderPackageTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, - () => _mod?.ModPath.FullName ?? string.Empty, + () => Mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); @@ -629,10 +630,10 @@ public partial class ModEditWindow : Window, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { - if (type is not (ModPathChangeType.Reloaded or ModPathChangeType.Moved) || mod != _mod) + if (type is not (ModPathChangeType.Reloaded or ModPathChangeType.Moved) || mod != Mod) return; - _mod = null; + Mod = null; ChangeMod(mod); } } From a89aafb31008961eea0993e155150b77673c1b71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Feb 2024 14:24:50 +0100 Subject: [PATCH 1532/2451] Let's do this one more time. --- Penumbra/Interop/Services/RedrawService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index c1bd8573..e0a94d30 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -433,7 +433,7 @@ public sealed unsafe partial class RedrawService : IDisposable private void OnModFileChanged(Mod _1, FileRegistry _2) { - if (!_config.ForceRedrawOnFileChange) + if (!_config.Ephemeral.ForceRedrawOnFileChange) return; RedrawObject(0, RedrawType.Redraw); From 1d74001281083620af6ec6f1423fa74fb9217a88 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 24 Feb 2024 13:27:11 +0000 Subject: [PATCH 1533/2451] [CI] Updating repo.json for testing_1.0.0.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 1a599362..cbc5677f 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.0.7", - "TestingAssemblyVersion": "1.0.0.7", + "TestingAssemblyVersion": "1.0.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.0.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From af6100dfe4d8af2f475e09df8411b2f5cbb07bf4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 1 Mar 2024 14:35:41 +0100 Subject: [PATCH 1534/2451] Mawp. --- Penumbra/Collections/ResolveData.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 0f3a1155..8fe160b3 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,15 +1,15 @@ namespace Penumbra.Collections; -public readonly struct ResolveData +public readonly struct ResolveData(ModCollection collection, nint gameObject) { public static readonly ResolveData Invalid = new(); - private readonly ModCollection? _modCollection; + private readonly ModCollection? _modCollection = collection; public ModCollection ModCollection => _modCollection ?? ModCollection.Empty; - public readonly nint AssociatedGameObject; + public readonly nint AssociatedGameObject = gameObject; public bool Valid => _modCollection != null; @@ -18,12 +18,6 @@ public readonly struct ResolveData : this(null!, nint.Zero) { } - public ResolveData(ModCollection collection, nint gameObject) - { - _modCollection = collection; - AssociatedGameObject = gameObject; - } - public ResolveData(ModCollection collection) : this(collection, nint.Zero) { } From 0220257efa49d57acea181eddb94561de2fbbe01 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 1 Mar 2024 13:37:51 +0000 Subject: [PATCH 1535/2451] [CI] Updating repo.json for 1.0.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cbc5677f..1780c6c0 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.0.7", - "TestingAssemblyVersion": "1.0.0.8", + "AssemblyVersion": "1.0.1.0", + "TestingAssemblyVersion": "1.0.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.0.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.0.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 7128326ab9f30e2fb2a3449953ed9d6a4c0e0ba1 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 17:10:33 -0500 Subject: [PATCH 1536/2451] Add shared tag system for tagging individual mods Adds a new system of shared tags that are saved in the Penumbra config, and can then be 1-click added or removed to/from mods via a popup menu. The use case for this new system is to allow users to more easily re-use tags and to allow them to quickly tag individual mods. Shared tags can be added/removed/modified via a new Tags section of the main Penumbra Settings tab. Once any shared tags have been saved, they can be added via a new tags button that shows up in the Description and Edit Mod tabs, to the right of the existing + button that already existed for typing in new tags. Shared tags have the same restrictions as regular mod tags, and the application of shared tags should respect the same limits as application of normal tags. Signed-off-by: AeAstralis --- Penumbra/Configuration.cs | 2 + Penumbra/Services/ServiceManagerA.cs | 3 +- Penumbra/UI/Classes/Colors.cs | 4 + Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 29 +++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 30 +++- Penumbra/UI/SharedTagManager.cs | 160 ++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 26 ++- 7 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 Penumbra/UI/SharedTagManager.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 188be65d..43253223 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -87,6 +87,8 @@ public class Configuration : IPluginConfiguration, ISavable public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); + public IReadOnlyList SharedTags { get; set; } + /// /// Load the current configuration. /// Includes adding new colors and migrating from old versions. diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index f25aac7c..b0ecdcf0 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -103,7 +103,8 @@ public static class ServiceManagerA private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 93d7e091..50096696 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -28,6 +28,8 @@ public enum ColorId ResTreePlayer, ResTreeNetworked, ResTreeNonNetworked, + SharedTagAdd, + SharedTagRemove } public static class Colors @@ -73,6 +75,8 @@ public static class Colors ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), + ColorId.SharedTagAdd => ( 0xFF44AA44, "Shared Tags: Add Tag", "A shared tag that is not present on the current mod and can be added." ), + ColorId.SharedTagRemove => ( 0xFF2222AA, "Shared Tags: Remove Tag", "A shared tag that is already present on the current mod and can be removed." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 3cc59661..7da13966 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -12,14 +12,16 @@ public class ModPanelDescriptionTab : ITab private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; private readonly ModManager _modManager; + private readonly SharedTagManager _sharedTagManager; private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); - public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager) + public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager, SharedTagManager sharedTagsConfig) { _selector = selector; _tutorial = tutorial; _modManager = modManager; + _sharedTagManager = sharedTagsConfig; } public ReadOnlySpan Label @@ -34,14 +36,37 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _selector.Selected!.LocalTags, - out var editedTag); + out var editedTag, rightEndOffset: sharedTagButtonOffset); _tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag); + if (sharedTagsEnabled) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true); + if (sharedTag.Length > 0) + { + var index = _selector.Selected!.LocalTags.IndexOf(sharedTag); + if (index < 0) + { + index = _selector.Selected!.LocalTags.Count; + _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, string.Empty); + } + + } + } + if (_selector.Selected!.ModTags.Count > 0) _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _selector.Selected!.ModTags, out var _, false, diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 20da8fde..3620c7ac 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -28,6 +28,7 @@ public class ModPanelEditTab : ITab private readonly ModEditWindow _editWindow; private readonly ModEditor _editor; private readonly Configuration _config; + private readonly SharedTagManager _sharedTagManager; private readonly TagButtons _modTags = new(); @@ -37,7 +38,8 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, + SharedTagManager sharedTagManager) { _modManager = modManager; _selector = selector; @@ -48,6 +50,7 @@ public class ModPanelEditTab : ITab _filenames = filenames; _modExportManager = modExportManager; _config = config; + _sharedTagManager = sharedTagManager; } public ReadOnlySpan Label @@ -80,11 +83,34 @@ public class ModPanelEditTab : ITab } UiHelpers.DefaultLineSpace(); + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, - out var editedTag); + out var editedTag, rightEndOffset: sharedTagButtonOffset); if (tagIdx >= 0) _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); + if (sharedTagsEnabled) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false); + if (sharedTag.Length > 0) + { + var index = _selector.Selected!.ModTags.IndexOf(sharedTag); + if (index < 0) + { + index = _selector.Selected!.ModTags.Count; + _modManager.DataEditor.ChangeModTag(_selector.Selected, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeModTag(_selector.Selected, index, string.Empty); + } + + } + } + UiHelpers.DefaultLineSpace(); AddOptionGroup.Draw(_filenames, _modManager, _mod, _config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/SharedTagManager.cs new file mode 100644 index 00000000..9562b24c --- /dev/null +++ b/Penumbra/UI/SharedTagManager.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; +public sealed class SharedTagManager +{ + private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); + private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); + + private static float _minTagButtonWidth = 15; + + private const string PopupContext = "SharedTagsPopup"; + private bool _isPopupOpen = false; + + + public IReadOnlyList SharedTags { get; internal set; } = Array.Empty(); + + public SharedTagManager() + { + } + + public void ChangeSharedTag(int tagIdx, string tag) + { + if (tagIdx < 0 || tagIdx > SharedTags.Count) + return; + + if (tagIdx == SharedTags.Count) // Adding a new tag + { + SharedTags = SharedTags.Append(tag).Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + } + else // Editing an existing tag + { + var tmpTags = SharedTags.ToArray(); + tmpTags[tagIdx] = tag; + SharedTags = tmpTags.Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + } + } + + public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + { + var tagToAdd = ""; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", + false, true) || _isPopupOpen) + return DrawSharedTagsPopup(localTags, modTags, editLocal); + + + return tagToAdd; + } + + private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + { + var selected = ""; + if (!ImGui.IsPopupOpen(PopupContext)) + { + ImGui.OpenPopup(PopupContext); + _isPopupOpen = true; + } + + var display = ImGui.GetIO().DisplaySize; + var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 6; + var size = new Vector2(width, height); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup(PopupContext); + if (!popup) + return selected; + + ImGui.Text("Shared Tags"); + ImGuiUtil.HoverTooltip("Right-click to close popup"); + ImGui.Separator(); + + foreach (var tag in SharedTags) + { + if (DrawColoredButton(localTags, modTags, tag, editLocal)) + { + selected = tag; + return selected; + } + ImGui.SameLine(); + } + + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + _isPopupOpen = false; + } + + return selected; + } + + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal) + { + var isLocalTagPresent = localTags.Contains(buttonLabel); + var isModTagPresent = modTags.Contains(buttonLabel); + + var buttonWidth = CalcTextButtonWidth(buttonLabel); + // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. + var maxButtonWidth = (ImGui.GetContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) * 0.5f - ImGui.GetStyle().ItemSpacing.X; + var displayedLabel = buttonLabel; + if (buttonWidth >= maxButtonWidth) + { + displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); + buttonWidth = CalcTextButtonWidth(displayedLabel); + } + + // Prevent adding a new tag past the right edge of the popup + if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + + // Trimmed tag names can collide, but the full tags are guaranteed distinct so use the full tag as the ID to avoid an ImGui moment. + ImRaii.PushId(buttonLabel); + + if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) + { + using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); + ImGui.Button(displayedLabel); + alpha.Pop(); + return false; + } + + using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) + { + return ImGui.Button(displayedLabel); + } + } + + private static string TrimButtonTextToWidth(string fullText, float maxWidth) + { + var trimmedText = fullText; + + while (trimmedText.Length > _minTagButtonWidth) + { + var nextTrim = trimmedText.Substring(0, Math.Max(trimmedText.Length - 1, 0)); + + // An ellipsis will be used to indicate trimmed tags + if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) + { + return nextTrim + "..."; + } + trimmedText = nextTrim; + } + + return trimmedText; + } + + private static float CalcTextButtonWidth(string text) + { + return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; + } + +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a03e7b87..f37c2c81 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,15 +41,18 @@ public class SettingsTab : ITab private readonly DalamudConfigService _dalamudConfig; private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; + private readonly SharedTagManager _sharedTagManager; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; + private readonly TagButtons _sharedTags = new(); + public SettingsTab(DalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData) + IDataManager gameData, SharedTagManager sharedTagConfig) { _pluginInterface = pluginInterface; _config = config; @@ -69,6 +72,9 @@ public class SettingsTab : ITab _gameData = gameData; if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; + _sharedTagManager = sharedTagConfig; + if (sharedTagConfig.SharedTags.Count == 0 && _config.SharedTags != null) + sharedTagConfig.SharedTags = _config.SharedTags; } public void DrawHeader() @@ -96,6 +102,7 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); DrawAdvancedSettings(); + DrawSharedTagsSection(); DrawSupportButtons(); } @@ -902,4 +909,21 @@ public class SettingsTab : ITab if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); } + + private void DrawSharedTagsSection() + { + if (!ImGui.CollapsingHeader("Tags")) + return; + + var tagIdx = _sharedTags.Draw("Shared Tags: ", + "Tags that can be added/removed from mods with 1 click.", _sharedTagManager.SharedTags, + out var editedTag); + + if (tagIdx >= 0) + { + _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); + _config.SharedTags = _sharedTagManager.SharedTags; + _config.Save(); + } + } } From 334be441f88dc554461cdddbb81ba658abe5b958 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 18:04:03 -0500 Subject: [PATCH 1537/2451] Update OtterGui to 97ac353 Updates OtterGui to 97ac3538536a17e980027f783ec5e5167b371f71 to include change that xivdev/Penumbra#399 is dependent on. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1a187f75..97ac3538 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1a187f756f2e8823197bd43db1c3383231f5eaff +Subproject commit 97ac3538536a17e980027f783ec5e5167b371f71 From 282f6d48551e0208d4ad06da18167ad74c48d9e2 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 21:03:34 -0500 Subject: [PATCH 1538/2451] Migrate shared tag to own config, address comments Migrates the configuration for shared tags to a separate config file, and addresses CR feedback. --- Penumbra/Configuration.cs | 2 - Penumbra/Services/FilenameService.cs | 1 + Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 20 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 20 +- Penumbra/UI/SharedTagManager.cs | 171 ++++++++++++++---- Penumbra/UI/Tabs/SettingsTab.cs | 6 +- 6 files changed, 143 insertions(+), 77 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 43253223..188be65d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -87,8 +87,6 @@ public class Configuration : IPluginConfiguration, ISavable public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); - public IReadOnlyList SharedTags { get; set; } - /// /// Load the current configuration. /// Includes adding new colors and migrating from old versions. diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 5f918a90..23694ebc 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -14,6 +14,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService public readonly string EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json"); public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + public readonly string SharedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "shared_tags.json"); /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 7da13966..5f2687c3 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -36,7 +36,7 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" @@ -48,23 +48,7 @@ public class ModPanelDescriptionTab : ITab if (sharedTagsEnabled) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true); - if (sharedTag.Length > 0) - { - var index = _selector.Selected!.LocalTags.IndexOf(sharedTag); - if (index < 0) - { - index = _selector.Selected!.LocalTags.Count; - _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, string.Empty); - } - - } + _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true, _selector.Selected!); } if (_selector.Selected!.ModTags.Count > 0) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 3620c7ac..9b4a582f 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -83,7 +83,7 @@ public class ModPanelEditTab : ITab } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); @@ -92,23 +92,7 @@ public class ModPanelEditTab : ITab if (sharedTagsEnabled) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false); - if (sharedTag.Length > 0) - { - var index = _selector.Selected!.ModTags.IndexOf(sharedTag); - if (index < 0) - { - index = _selector.Selected!.ModTags.Count; - _modManager.DataEditor.ChangeModTag(_selector.Selected, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeModTag(_selector.Selected, index, string.Empty); - } - - } + _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false, _selector.Selected!); } UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/SharedTagManager.cs index 9562b24c..23196319 100644 --- a/Penumbra/UI/SharedTagManager.cs +++ b/Penumbra/UI/SharedTagManager.cs @@ -1,19 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dalamud.Interface; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using ImGuiNET; +using Newtonsoft.Json; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.UI.Classes; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class SharedTagManager +public sealed class SharedTagManager : ISavable { + private readonly ModManager _modManager; + private readonly SaveService _saveService; + private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); @@ -22,11 +25,66 @@ public sealed class SharedTagManager private const string PopupContext = "SharedTagsPopup"; private bool _isPopupOpen = false; + // Operations on this list assume that it is sorted and will keep it sorted if that is the case. + // The list also gets re-sorted when first loaded from config in case the config was modified. + [JsonRequired] + private readonly List _sharedTags = []; + [JsonIgnore] + public IReadOnlyList SharedTags => _sharedTags; - public IReadOnlyList SharedTags { get; internal set; } = Array.Empty(); + public int ConfigVersion = 1; - public SharedTagManager() + public SharedTagManager(ModManager modManager, SaveService saveService) { + _modManager = modManager; + _saveService = saveService; + Load(); + } + + public string ToFilename(FilenameService fileNames) + { + return fileNames.SharedTagFile; + } + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } + + public void Save() + => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); + + private void Load() + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Penumbra.Log.Error( + $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (!File.Exists(_saveService.FileNames.SharedTagFile)) + return; + + try + { + var text = File.ReadAllText(_saveService.FileNames.SharedTagFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + + // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. + _sharedTags.Sort(); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, + "Error reading shared tags Configuration, reverting to default.", + "Error reading shared tags Configuration", NotificationType.Error); + } } public void ChangeSharedTag(int tagIdx, string tag) @@ -34,32 +92,74 @@ public sealed class SharedTagManager if (tagIdx < 0 || tagIdx > SharedTags.Count) return; - if (tagIdx == SharedTags.Count) // Adding a new tag + // In the case of editing a tag, remove what's there prior to doing an insert. + if (tagIdx != SharedTags.Count) { - SharedTags = SharedTags.Append(tag).Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + _sharedTags.RemoveAt(tagIdx); } - else // Editing an existing tag + + if (!string.IsNullOrEmpty(tag)) { - var tmpTags = SharedTags.ToArray(); - tmpTags[tagIdx] = tag; - SharedTags = tmpTags.Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. + var existingIdx = _sharedTags.BinarySearch(tag); + if (existingIdx < 0) + _sharedTags.Insert(~existingIdx, tag); + } + + Save(); + } + + public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + + var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); + + if (sharedTag.Length > 0) + { + var index = editLocal ? mod.LocalTags.IndexOf(sharedTag) : mod.ModTags.IndexOf(sharedTag); + + if (editLocal) + { + if (index < 0) + { + index = mod.LocalTags.Count; + _modManager.DataEditor.ChangeLocalTag(mod, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); + } + } else + { + if (index < 0) + { + index = mod.ModTags.Count; + _modManager.DataEditor.ChangeModTag(mod, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); + } + } + } } public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { - var tagToAdd = ""; + var tagToAdd = string.Empty; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", false, true) || _isPopupOpen) return DrawSharedTagsPopup(localTags, modTags, editLocal); - return tagToAdd; } private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { - var selected = ""; + var selected = string.Empty; if (!ImGui.IsPopupOpen(PopupContext)) { ImGui.OpenPopup(PopupContext); @@ -75,16 +175,15 @@ public sealed class SharedTagManager if (!popup) return selected; - ImGui.Text("Shared Tags"); + ImGui.TextUnformatted("Shared Tags"); ImGuiUtil.HoverTooltip("Right-click to close popup"); ImGui.Separator(); - foreach (var tag in SharedTags) + foreach (var (tag, idx) in SharedTags.WithIndex()) { - if (DrawColoredButton(localTags, modTags, tag, editLocal)) + if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) { selected = tag; - return selected; } ImGui.SameLine(); } @@ -97,8 +196,10 @@ public sealed class SharedTagManager return selected; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal) + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal, int index) { + var ret = false; + var isLocalTagPresent = localTags.Contains(buttonLabel); var isModTagPresent = modTags.Contains(buttonLabel); @@ -116,21 +217,24 @@ public sealed class SharedTagManager if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) ImGui.NewLine(); - // Trimmed tag names can collide, but the full tags are guaranteed distinct so use the full tag as the ID to avoid an ImGui moment. - ImRaii.PushId(buttonLabel); + // Trimmed tag names can collide, and while tag names are currently distinct this may not always be the case. As such use the index to avoid an ImGui moment. + using var id = ImRaii.PushId(index); if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) { using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); ImGui.Button(displayedLabel); - alpha.Pop(); - return false; + } + else + { + using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) + { + if (ImGui.Button(displayedLabel)) + ret = true; + } } - using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) - { - return ImGui.Button(displayedLabel); - } + return ret; } private static string TrimButtonTextToWidth(string fullText, float maxWidth) @@ -156,5 +260,4 @@ public sealed class SharedTagManager { return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } - } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index f37c2c81..71f108c2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -73,8 +73,6 @@ public class SettingsTab : ITab if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; _sharedTagManager = sharedTagConfig; - if (sharedTagConfig.SharedTags.Count == 0 && _config.SharedTags != null) - sharedTagConfig.SharedTags = _config.SharedTags; } public void DrawHeader() @@ -916,14 +914,12 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Shared Tags: ", - "Tags that can be added/removed from mods with 1 click.", _sharedTagManager.SharedTags, + "Predefined tags that can be added or removed from mods with a single click.", _sharedTagManager.SharedTags, out var editedTag); if (tagIdx >= 0) { _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); - _config.SharedTags = _sharedTagManager.SharedTags; - _config.Save(); } } } From c1cdb28bb5f35a9816b3042d786bcde40e4bed18 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 29 Jan 2024 16:58:47 +1100 Subject: [PATCH 1539/2451] Use named enum values for vertex decl mismatch error --- Penumbra/Import/Models/Import/Utility.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Import/Utility.cs b/Penumbra/Import/Models/Import/Utility.cs index a1e44136..21655563 100644 --- a/Penumbra/Import/Models/Import/Utility.cs +++ b/Penumbra/Import/Models/Import/Utility.cs @@ -1,4 +1,5 @@ using Lumina.Data.Parsing; +using Penumbra.GameData.Files; namespace Penumbra.Import.Models.Import; @@ -43,15 +44,15 @@ public static class Utility throw notifier.Exception( $""" All sub-meshes of a mesh must have equivalent vertex declarations. - Current: {FormatVertexDeclaration(current)} - New: {FormatVertexDeclaration(@new)} + Current: {FormatVertexDeclaration(current)} + New: {FormatVertexDeclaration(@new)} """ ); } private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration) => string.Join(", ", - vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})")); + vertexDeclaration.VertexElements.Select(element => $"{(MdlFile.VertexUsage)element.Usage} ({(MdlFile.VertexType)element.Type}@{element.Stream}:{element.Offset})")); private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b) { From 1cee1c24ec219a00f20bb5112771b77591f32844 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 30 Jan 2024 21:39:12 +1100 Subject: [PATCH 1540/2451] Skip degenerate triangles targeted by shape keys --- Penumbra/Import/Models/Export/MeshExporter.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index df315094..d3ca87dc 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -214,10 +214,18 @@ public class MeshExporter var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count); shapeNames.Add(shape.ShapeName); - foreach (var shapeValue in shapeValues) + foreach (var (shapeValue, shapeValueIndex) in shapeValues.WithIndex()) { + var gltfIndex = gltfIndices[shapeValue.BaseIndicesIndex - indexBase]; + + if (gltfIndex == -1) + { + _notifier.Warning($"{name}: Shape {shape.ShapeName} mapping {shapeValueIndex} targets a degenerate triangle, ignoring."); + continue; + } + morphBuilder.SetVertex( - primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(), + primitiveVertices[gltfIndex].GetGeometry(), vertices[shapeValue.ReplacingVertexIndex].GetGeometry() ); } From a4bd015836a6a7e8dec83a3a3553e946832c40b8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 20 Feb 2024 21:35:14 +1100 Subject: [PATCH 1541/2451] Fix index offset mis-cast causing overflow --- Penumbra/Import/Models/Import/MeshImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 8ab55734..efebdba4 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -167,7 +167,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // And finally, merge in the sub-mesh struct itself. _subMeshes.Add(subMesh.SubMeshStruct with { - IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset), + IndexOffset = (uint)(subMesh.SubMeshStruct.IndexOffset + indexOffset), AttributeIndexMask = Utility.GetMergedAttributeMask( subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes), }); From 5c6e0701d96f626ba7f58fcf7541ddc1b62be930 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 25 Feb 2024 22:09:55 +1100 Subject: [PATCH 1542/2451] Simplify EID handling because IDFK at this point --- Penumbra/Import/Models/Import/MeshImporter.cs | 1 - .../ModEditWindow.Models.MdlTab.cs | 21 +++---------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index efebdba4..1d4b223d 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -80,7 +80,6 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) StartIndex = 0, IndexCount = (uint)_indices.Count, - // TODO: import material names MaterialIndex = 0, SubMeshIndex = 0, SubMeshCount = (ushort)_subMeshes.Count, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 7adc4379..637c8401 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -220,24 +220,9 @@ public partial class ModEditWindow /// Model to copy element ids from. private static void MergeElementIds(MdlFile target, MdlFile source) { - var elementIds = new List(); - - foreach (var sourceElement in source.ElementIds) - { - var sourceBone = source.Bones[sourceElement.ParentBoneName]; - var targetIndex = target.Bones.IndexOf(sourceBone); - // Given that there's no means of authoring these at the moment, this should probably remain a hard error. - if (targetIndex == -1) - throw new Exception( - $"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model."); - - elementIds.Add(sourceElement with - { - ParentBoneName = (uint)targetIndex, - }); - } - - target.ElementIds = [.. elementIds]; + // This is overly simplistic, but effectively reproduces what TT did, sort of. + // TODO: Get a better idea of what these values represent. `ParentBoneName`, if it is a pointer into the bone array, does not seem to be _bounded_ by the bone array length, at least in the model. I'm guessing it _may_ be pointing into a .sklb instead? (i.e. the weapon's skeleton). EID stuff in general needs more work. + target.ElementIds = [.. source.ElementIds]; } private void BeginIo() From da423b746400d72f3fd7e79ab9d191dc7593bcf6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Mar 2024 18:13:44 +0100 Subject: [PATCH 1543/2451] Make lack of Root Directory louder. --- Penumbra/UI/Tabs/SettingsTab.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a03e7b87..7ea37a75 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -223,7 +224,15 @@ public class SettingsTab : ITab using var group = ImRaii.Group(); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - var save = ImGui.InputText("##rootDirectory", ref _newModDirectory, RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + bool save; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + var selected = ImGui.IsItemActive(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); ImGui.SameLine(); From 29c93f46a0131ba28eca812697b3010b86e520db Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Mar 2024 22:28:46 +0100 Subject: [PATCH 1544/2451] Add characterglass.shpk to the skin.shpk fixer --- Penumbra.GameData | 2 +- Penumbra/Communication/MtrlShpkLoaded.cs | 4 +- .../Resources/ResourceHandleDestructor.cs | 4 +- Penumbra/Interop/Services/ModelRenderer.cs | 71 +++++++ .../Services/ShaderReplacementFixer.cs | 197 ++++++++++++++++++ Penumbra/Interop/Services/SkinFixer.cs | 139 ------------ Penumbra/Penumbra.cs | 2 +- Penumbra/Services/ServiceManagerA.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 23 +- 9 files changed, 290 insertions(+), 155 deletions(-) create mode 100644 Penumbra/Interop/Services/ModelRenderer.cs create mode 100644 Penumbra/Interop/Services/ShaderReplacementFixer.cs delete mode 100644 Penumbra/Interop/Services/SkinFixer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a7f6d86..c0c7eb0d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 +Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319 diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index bd560fd8..8aab0e0e 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper - SkinFixer = 0, + /// + ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 31387101..5ddb7eaa 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -13,8 +13,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr SubfileHelper, - /// - SkinFixer, + /// + ShaderReplacementFixer, } public ResourceHandleDestructor(HookManager hooks) diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs new file mode 100644 index 00000000..6a3bf776 --- /dev/null +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -0,0 +1,71 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; + +namespace Penumbra.Interop.Services; + +// TODO ClientStructs-ify (https://github.com/aers/FFXIVClientStructs/pull/817) +public unsafe class ModelRenderer : IDisposable +{ + // Will be Manager.Instance()->ModelRenderer.CharacterGlassShaderPackage in CS + private const nint ModelRendererOffset = 0x13660; + private const nint CharacterGlassShaderPackageOffset = 0xD0; + + /// A static pointer to the Render::Manager address. + [Signature(Sigs.RenderManager, ScanType = ScanType.StaticAddress)] + private readonly nint* _renderManagerAddress = null; + + public bool Ready { get; private set; } + + public ShaderPackageResourceHandle** CharacterGlassShaderPackage + => *_renderManagerAddress == 0 + ? null + : (ShaderPackageResourceHandle**)(*_renderManagerAddress + ModelRendererOffset + CharacterGlassShaderPackageOffset).ToPointer(); + + public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } + + private readonly IFramework _framework; + + public ModelRenderer(IFramework framework, IGameInteropProvider interop) + { + interop.InitializeFromAttributes(this); + _framework = framework; + LoadDefaultResources(null!); + if (!Ready) + _framework.Update += LoadDefaultResources; + } + + /// We store the default data of the resources so we can always restore them. + private void LoadDefaultResources(object _) + { + if (*_renderManagerAddress == 0) + return; + + var anyMissing = false; + + if (DefaultCharacterGlassShaderPackage == null) + { + DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; + anyMissing |= DefaultCharacterGlassShaderPackage == null; + } + + if (anyMissing) + return; + + Ready = true; + _framework.Update -= LoadDefaultResources; + } + + /// Return all relevant resources to the default resource. + public void ResetAll() + { + if (!Ready) + return; + + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + } + + public void Dispose() + => ResetAll(); +} diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs new file mode 100644 index 00000000..e57fe313 --- /dev/null +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -0,0 +1,197 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Classes; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.Interop.Hooks.Resources; +using Penumbra.Services; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class ShaderReplacementFixer : IDisposable +{ + public static ReadOnlySpan SkinShpkName + => "skin.shpk"u8; + + public static ReadOnlySpan CharacterGlassShpkName + => "characterglass.shpk"u8; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint CharacterBaseOnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + private delegate nint ModelRendererOnRenderMaterialDelegate(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _humanOnRenderMaterialHook; + + [Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))] + private readonly Hook _modelRendererOnRenderMaterialHook = null!; + + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _utility; + private readonly ModelRenderer _modelRenderer; + + // MaterialResourceHandle set + private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); + private readonly ConcurrentSet _moddedCharacterGlassShpkMaterials = new(); + + private readonly object _skinLock = new(); + private readonly object _characterGlassLock = new(); + + // ConcurrentDictionary.Count uses a lock in its current implementation. + private int _moddedSkinShpkCount; + private int _moddedCharacterGlassShpkCount; + private ulong _skinSlowPathCallDelta; + private ulong _characterGlassSlowPathCallDelta; + + public bool Enabled { get; internal set; } = true; + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public int ModdedCharacterGlassShpkCount + => _moddedCharacterGlassShpkCount; + + public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, + CommunicatorService communicator, IGameInteropProvider interop) + { + interop.InitializeFromAttributes(this); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _modelRenderer = modelRenderer; + _communicator = communicator; + _humanOnRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); + _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); + _humanOnRenderMaterialHook.Enable(); + _modelRendererOnRenderMaterialHook.Enable(); + } + + public void Dispose() + { + _modelRendererOnRenderMaterialHook.Dispose(); + _humanOnRenderMaterialHook.Dispose(); + _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); + _moddedCharacterGlassShpkMaterials.Clear(); + _moddedSkinShpkMaterials.Clear(); + _moddedCharacterGlassShpkCount = 0; + _moddedSkinShpkCount = 0; + } + + public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() + => (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0)); + + private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) + { + if (mtrlResource == null) + return false; + + return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); + } + + private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + { + var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; + var shpk = mtrl->ShaderPackageResourceHandle; + if (shpk == null) + return; + + var shpkName = mtrl->ShpkNameSpan; + + if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) + { + if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + + if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) + { + if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _moddedCharacterGlassShpkCount); + } + } + + private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) + { + if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _moddedSkinShpkCount); + + if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0) + return _humanOnRenderMaterialHook.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var mtrlResource = material->MaterialResourceHandle; + if (!IsMaterialWithShpk(mtrlResource, SkinShpkName)) + return _humanOnRenderMaterialHook.Original(human, param); + + Interlocked.Increment(ref _skinSlowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_skinLock) + { + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; + return _humanOnRenderMaterialHook.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } + } + + private nint ModelRendererOnRenderMaterialDetour(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex) + { + + // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. + if (!Enabled || _moddedCharacterGlassShpkCount == 0) + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + + var mtrlResource = material->MaterialResourceHandle; + if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName)) + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + + Interlocked.Increment(ref _characterGlassSlowPathCallDelta); + + // Same performance considerations as above. + lock (_characterGlassLock) + { + try + { + *_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle; + return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); + } + finally + { + *_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage; + } + } + } +} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs deleted file mode 100644 index 21331916..00000000 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using OtterGui.Classes; -using Penumbra.Communication; -using Penumbra.GameData; -using Penumbra.Interop.Hooks.Resources; -using Penumbra.Services; - -namespace Penumbra.Interop.Services; - -public sealed unsafe class SkinFixer : IDisposable -{ - public static ReadOnlySpan SkinShpkName - => "skin.shpk"u8; - - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - - private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); - - [StructLayout(LayoutKind.Explicit)] - private struct OnRenderMaterialParams - { - [FieldOffset(0x0)] - public Model* Model; - - [FieldOffset(0x8)] - public uint MaterialIndex; - } - - private readonly Hook _onRenderMaterialHook; - - private readonly ResourceHandleDestructor _resourceHandleDestructor; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - - // MaterialResourceHandle set - private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); - - private readonly object _lock = new(); - - // ConcurrentDictionary.Count uses a lock in its current implementation. - private int _moddedSkinShpkCount; - private ulong _slowPathCallDelta; - - public bool Enabled { get; internal set; } = true; - - public int ModdedSkinShpkCount - => _moddedSkinShpkCount; - - public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator, - IGameInteropProvider interop) - { - interop.InitializeFromAttributes(this); - _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _communicator = communicator; - _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); - _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); - _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer); - _onRenderMaterialHook.Enable(); - } - - public void Dispose() - { - _onRenderMaterialHook.Dispose(); - _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); - _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); - _moddedSkinShpkMaterials.Clear(); - _moddedSkinShpkCount = 0; - } - - public ulong GetAndResetSlowPathCallDelta() - => Interlocked.Exchange(ref _slowPathCallDelta, 0); - - private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource) - { - if (mtrlResource == null) - return false; - - var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName); - return SkinShpkName.SequenceEqual(shpkName); - } - - private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) - { - var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; - var shpk = mtrl->ShaderPackageResourceHandle; - if (shpk == null) - return; - - if (!IsSkinMaterial(mtrl) || (nint)shpk == _utility.DefaultSkinShpkResource) - return; - - if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedSkinShpkCount); - } - - private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) - { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedSkinShpkCount); - } - - private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) - { - // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || _moddedSkinShpkCount == 0) - return _onRenderMaterialHook.Original(human, param); - - var material = param->Model->Materials[param->MaterialIndex]; - var mtrlResource = material->MaterialResourceHandle; - if (!IsSkinMaterial(mtrlResource)) - return _onRenderMaterialHook.Original(human, param); - - Interlocked.Increment(ref _slowPathCallDelta); - - // Performance considerations: - // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; - // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; - // - Swapping path is taken up to hundreds of times a frame. - // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. - lock (_lock) - { - try - { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; - return _onRenderMaterialHook.Original(human, param); - } - finally - { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; - } - } - } -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 15b7ce56..67f523ba 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); + _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index f25aac7c..191d8d11 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -92,6 +92,7 @@ public static class ServiceManagerA return new CutsceneResolver(cutsceneService.GetParentIndex); }) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -132,7 +133,7 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 66b93b04..f4ddbe31 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -86,7 +86,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; - private readonly SkinFixer _skinFixer; + private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly RedrawService _redraws; private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; @@ -99,7 +99,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) + TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -130,7 +130,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; - _skinFixer = skinFixer; + _shaderReplacementFixer = shaderReplacementFixer; _redraws = redraws; _emotes = emotes; _diagnostics = diagnostics; @@ -702,20 +702,25 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableSkinFixer = _skinFixer.Enabled; - if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) - _skinFixer.Enabled = enableSkinFixer; + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - if (enableSkinFixer) + if (enableShaderReplacementFixer) { ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); ImGui.SameLine(); - ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}"); + ImGui.SameLine(); + ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}"); ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.SameLine(); + ImGui.TextUnformatted($"characterglass.shpk: {_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); } using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, From 9ba6e4d0afb74c9603dc9353ea27244223dcbd0b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Mar 2024 22:49:19 +0100 Subject: [PATCH 1545/2451] Update API. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 79ffdd69..34921fd2 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 79ffdd69a28141a1ac93daa24d76573b2fa0d71e +Subproject commit 34921fd2c5a9aff5d34aef664bdb78331e8b9436 From e08e9c4d1368765af5fa36a5537673cda645f3a5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:20:34 +0100 Subject: [PATCH 1546/2451] Add crash handler stuff. --- .../Buffers/AnimationInvocationBuffer.cs | 119 +++++++ .../Buffers/CharacterBaseBuffer.cs | 85 +++++ .../Buffers/MemoryMappedBuffer.cs | 215 +++++++++++++ .../Buffers/ModdedFileBuffer.cs | 99 ++++++ Penumbra.CrashHandler/CrashData.cs | 62 ++++ Penumbra.CrashHandler/GameEventLogReader.cs | 53 ++++ Penumbra.CrashHandler/GameEventLogWriter.cs | 17 + .../Penumbra.CrashHandler.csproj | 28 ++ Penumbra.CrashHandler/Program.cs | 30 ++ Penumbra.sln | 6 + .../Communication/CreatingCharacterBase.cs | 4 + Penumbra/Configuration.cs | 1 + Penumbra/EphemeralConfig.cs | 8 +- .../Animation/ApricotListenerSoundPlay.cs | 11 +- .../Animation/CharacterBaseLoadAnimation.cs | 16 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 15 +- .../Hooks/Animation/LoadCharacterSound.cs | 16 +- .../Hooks/Animation/LoadCharacterVfx.cs | 14 +- .../Hooks/Animation/LoadTimelineResources.cs | 24 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 32 +- .../Interop/Hooks/Animation/SomeActionLoad.cs | 26 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 18 +- Penumbra/Penumbra.csproj | 9 +- Penumbra/Services/CrashHandlerService.cs | 296 ++++++++++++++++++ Penumbra/Services/FilenameService.cs | 6 + Penumbra/UI/Tabs/ChangedItemsTab.cs | 37 +-- Penumbra/UI/Tabs/ConfigTabBar.cs | 6 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 104 ++++++ Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs | 136 ++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 23 +- Penumbra/UI/Tabs/EffectiveTab.cs | 17 +- Penumbra/UI/Tabs/ModsTab.cs | 85 +++-- Penumbra/UI/Tabs/OnScreenTab.cs | 2 +- Penumbra/UI/Tabs/ResourceTab.cs | 26 +- Penumbra/packages.lock.json | 63 +--- 35 files changed, 1472 insertions(+), 237 deletions(-) create mode 100644 Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs create mode 100644 Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs create mode 100644 Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs create mode 100644 Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs create mode 100644 Penumbra.CrashHandler/CrashData.cs create mode 100644 Penumbra.CrashHandler/GameEventLogReader.cs create mode 100644 Penumbra.CrashHandler/GameEventLogWriter.cs create mode 100644 Penumbra.CrashHandler/Penumbra.CrashHandler.csproj create mode 100644 Penumbra.CrashHandler/Program.cs create mode 100644 Penumbra/Services/CrashHandlerService.cs create mode 100644 Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs create mode 100644 Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs new file mode 100644 index 00000000..dd966542 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -0,0 +1,119 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// The types of currently hooked and relevant animation loading functions. +public enum AnimationInvocationType : int +{ + PapLoad, + ActionLoad, + ScheduleClipUpdate, + LoadTimelineResources, + LoadCharacterVfx, + LoadCharacterSound, + ApricotSoundPlay, + LoadAreaVfx, + CharacterBaseLoadAnimation, +} + +/// The full crash entry for an invoked vfx function. +public record struct VfxFuncInvokedEntry( + double Age, + DateTimeOffset Timestamp, + int ThreadId, + string InvocationType, + string CharacterName, + string CharacterAddress, + string CollectionName) : ICrashDataEntry; + +/// Only expose the write interface for the buffer. +public interface IAnimationInvocationBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + /// The type of VFX func called. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type); +} + +internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader +{ + private const int _version = 1; + private const int _lineCount = 64; + private const int _lineCapacity = 256; + private const string _name = "Penumbra.AnimationInvocation"; + + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type) + { + var accessor = GetCurrentLineLocking(); + lock (accessor) + { + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(12, (int)type); + accessor.Write(16, characterAddress); + var span = GetSpan(accessor, 24, 104); + WriteSpan(characterName, span); + span = GetSpan(accessor, 128); + WriteString(collectionName, span); + } + } + + public uint TotalCount + => TotalWrittenLines; + + public IEnumerable GetLines(DateTimeOffset crashTime) + { + var lineCount = (int)CurrentLineCount; + for (var i = lineCount - 1; i >= 0; --i) + { + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]); + var address = BitConverter.ToUInt64(line[16..]); + var characterName = ReadString(line[24..]); + var collectionName = ReadString(line[128..]); + yield return new JsonObject() + { + [nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(VfxFuncInvokedEntry.Timestamp)] = timestamp, + [nameof(VfxFuncInvokedEntry.ThreadId)] = thread, + [nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type), + [nameof(VfxFuncInvokedEntry.CharacterName)] = characterName, + [nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"), + [nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName, + }; + } + } + + public static IBufferReader CreateReader() + => new AnimationInvocationBuffer(false); + + public static IAnimationInvocationBufferWriter CreateWriter() + => new AnimationInvocationBuffer(); + + private AnimationInvocationBuffer(bool writer) + : base(_name, _version) + { } + + private AnimationInvocationBuffer() + : base(_name, _version, _lineCount, _lineCapacity) + { } + + private static string ToName(AnimationInvocationType type) + => type switch + { + AnimationInvocationType.PapLoad => "PAP Load", + AnimationInvocationType.ActionLoad => "Action Load", + AnimationInvocationType.ScheduleClipUpdate => "Schedule Clip Update", + AnimationInvocationType.LoadTimelineResources => "Load Timeline Resources", + AnimationInvocationType.LoadCharacterVfx => "Load Character VFX", + AnimationInvocationType.LoadCharacterSound => "Load Character Sound", + AnimationInvocationType.ApricotSoundPlay => "Apricot Sound Play", + AnimationInvocationType.LoadAreaVfx => "Load Area VFX", + AnimationInvocationType.CharacterBaseLoadAnimation => "Load Animation (CharacterBase)", + _ => $"Unknown ({(int)type})", + }; +} diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs new file mode 100644 index 00000000..1fe5d7ba --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -0,0 +1,85 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// Only expose the write interface for the buffer. +public interface ICharacterBaseBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName); +} + +/// The full crash entry for a loaded character base. +public record struct CharacterLoadedEntry( + double Age, + DateTimeOffset Timestamp, + int ThreadId, + string CharacterName, + string CharacterAddress, + string CollectionName) : ICrashDataEntry; + +internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader +{ + private const int _version = 1; + private const int _lineCount = 10; + private const int _lineCapacity = 256; + private const string _name = "Penumbra.CharacterBase"; + + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName) + { + var accessor = GetCurrentLineLocking(); + lock (accessor) + { + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(12, characterAddress); + var span = GetSpan(accessor, 20, 108); + WriteSpan(characterName, span); + span = GetSpan(accessor, 128); + WriteString(collectionName, span); + } + } + + public IEnumerable GetLines(DateTimeOffset crashTime) + { + var lineCount = (int)CurrentLineCount; + for (var i = lineCount - 1; i >= 0; --i) + { + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var characterName = ReadString(line[20..]); + var collectionName = ReadString(line[128..]); + yield return new JsonObject + { + [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(CharacterLoadedEntry.Timestamp)] = timestamp, + [nameof(CharacterLoadedEntry.ThreadId)] = thread, + [nameof(CharacterLoadedEntry.CharacterName)] = characterName, + [nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"), + [nameof(CharacterLoadedEntry.CollectionName)] = collectionName, + }; + } + } + + public uint TotalCount + => TotalWrittenLines; + + public static IBufferReader CreateReader() + => new CharacterBaseBuffer(false); + + public static ICharacterBaseBufferWriter CreateWriter() + => new CharacterBaseBuffer(); + + private CharacterBaseBuffer(bool writer) + : base(_name, _version) + { } + + private CharacterBaseBuffer() + : base(_name, _version, _lineCount, _lineCapacity) + { } +} diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs new file mode 100644 index 00000000..35055864 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -0,0 +1,215 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO.MemoryMappedFiles; +using System.Numerics; +using System.Text; + +namespace Penumbra.CrashHandler.Buffers; + +public class MemoryMappedBuffer : IDisposable +{ + private const int MinHeaderLength = 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4; + + private readonly MemoryMappedFile _file; + private readonly MemoryMappedViewAccessor _header; + private readonly MemoryMappedViewAccessor[] _lines; + + public readonly int Version; + public readonly uint LineCount; + public readonly uint LineCapacity; + private readonly uint _lineMask; + private bool _disposed; + + protected uint CurrentLineCount + { + get => _header.ReadUInt32(16); + set => _header.Write(16, value); + } + + protected uint CurrentLinePosition + { + get => _header.ReadUInt32(20); + set => _header.Write(20, value); + } + + public uint TotalWrittenLines + { + get => _header.ReadUInt32(24); + protected set => _header.Write(24, value); + } + + public MemoryMappedBuffer(string mapName, int version, uint lineCount, uint lineCapacity) + { + Version = version; + LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3)); + LineCapacity = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCapacity, 2, int.MaxValue >> 3)); + _lineMask = LineCount - 1; + var fileName = Encoding.UTF8.GetBytes(mapName); + var headerLength = (uint)(4 + 4 + 4 + 4 + 4 + 4 + 4 + fileName.Length + 1); + headerLength = (headerLength & 0b111) > 0 ? (headerLength & ~0b111u) + 0b1000 : headerLength; + var capacity = LineCount * LineCapacity + headerLength; + _file = MemoryMappedFile.CreateNew(mapName, capacity, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, + HandleInheritability.Inheritable); + _header = _file.CreateViewAccessor(0, headerLength); + _header.Write(0, headerLength); + _header.Write(4, Version); + _header.Write(8, LineCount); + _header.Write(12, LineCapacity); + _header.WriteArray(28, fileName, 0, fileName.Length); + _header.Write(fileName.Length + 28, (byte)0); + _lines = Enumerable.Range(0, (int)LineCount).Select(i + => _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite)) + .ToArray(); + } + + public MemoryMappedBuffer(string mapName, int? expectedVersion = null, uint? expectedMinLineCount = null, + uint? expectedMinLineCapacity = null) + { + _file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable); + using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read); + var headerLength = headerLine.ReadUInt32(0); + if (headerLength < MinHeaderLength) + Throw($"Map {mapName} did not contain a valid header."); + + _header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite); + Version = _header.ReadInt32(4); + LineCount = _header.ReadUInt32(8); + LineCapacity = _header.ReadUInt32(12); + _lineMask = LineCount - 1; + if (expectedVersion.HasValue && expectedVersion.Value != Version) + Throw($"Map {mapName} has version {Version} instead of {expectedVersion.Value}."); + + if (LineCount < expectedMinLineCount) + Throw($"Map {mapName} has line count {LineCount} but line count >= {expectedMinLineCount.Value} is required."); + + if (LineCapacity < expectedMinLineCapacity) + Throw($"Map {mapName} has line capacity {LineCapacity} but line capacity >= {expectedMinLineCapacity.Value} is required."); + + var name = ReadString(GetSpan(_header, 28)); + if (name != mapName) + Throw($"Map {mapName} does not contain its map name at the expected location."); + + _lines = Enumerable.Range(0, (int)LineCount).Select(i + => _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite)) + .ToArray(); + + [DoesNotReturn] + void Throw(string text) + { + _file.Dispose(); + _disposed = true; + throw new Exception(text); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + _disposed = true; + } + + protected static string ReadString(Span span) + { + if (span.IsEmpty) + throw new Exception("String from empty span requested."); + + var termination = span.IndexOf((byte)0); + if (termination < 0) + throw new Exception("String in span is not terminated."); + + return Encoding.UTF8.GetString(span[..termination]); + } + + protected static int WriteString(string text, Span span) + { + var bytes = Encoding.UTF8.GetBytes(text); + var length = bytes.Length + 1; + if (length > span.Length) + throw new Exception($"String {text} is too long to write into span."); + + bytes.CopyTo(span); + span[bytes.Length] = 0; + return length; + } + + protected static int WriteSpan(ReadOnlySpan input, Span span) + { + var length = input.Length + 1; + if (length > span.Length) + throw new Exception("Byte array is too long to write into span."); + + input.CopyTo(span); + span[input.Length] = 0; + return length; + } + + protected Span GetLine(int i) + { + if (i < 0 || i > LineCount) + return null; + + lock (_header) + { + var lineIdx = CurrentLinePosition + i & _lineMask; + if (lineIdx > CurrentLineCount) + return null; + + return GetSpan(_lines[lineIdx]); + } + } + + + protected MemoryMappedViewAccessor GetCurrentLineLocking() + { + MemoryMappedViewAccessor view; + lock (_header) + { + var currentLineCount = CurrentLineCount; + if (currentLineCount == LineCount) + { + var currentLinePos = CurrentLinePosition; + view = _lines[currentLinePos]!; + CurrentLinePosition = currentLinePos + 1 & _lineMask; + } + else + { + view = _lines[currentLineCount]; + ++CurrentLineCount; + } + + ++TotalWrittenLines; + _header.Flush(); + } + + return view; + } + + protected static Span GetSpan(MemoryMappedViewAccessor accessor, int offset = 0) + => GetSpan(accessor, offset, (int)accessor.Capacity - offset); + + protected static unsafe Span GetSpan(MemoryMappedViewAccessor accessor, int offset, int size) + { + byte* ptr = null; + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + size = Math.Min(size, (int)accessor.Capacity - offset); + if (size < 0) + return []; + + var span = new Span(ptr + offset + accessor.PointerOffset, size); + return span; + } + + protected void Dispose(bool disposing) + { + if (_disposed) + return; + + _header.Dispose(); + foreach (var line in _lines) + line.Dispose(); + _file.Dispose(); + } + + ~MemoryMappedBuffer() + => Dispose(false); +} diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs new file mode 100644 index 00000000..b472d413 --- /dev/null +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -0,0 +1,99 @@ +using System.Text.Json.Nodes; + +namespace Penumbra.CrashHandler.Buffers; + +/// Only expose the write interface for the buffer. +public interface IModdedFileBufferWriter +{ + /// Write a line into the buffer with the given data. + /// The address of the related character, if known. + /// The name of the related character, anonymized or relying on index if unavailable, if known. + /// The name of the associated collection. Not anonymized. + /// The file name as requested by the game. + /// The actual modded file name loaded. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + ReadOnlySpan actualFileName); +} + +/// The full crash entry for a loaded modded file. +public record struct ModdedFileLoadedEntry( + double Age, + DateTimeOffset Timestamp, + int ThreadId, + string CharacterName, + string CharacterAddress, + string CollectionName, + string RequestedFileName, + string ActualFileName) : ICrashDataEntry; + +internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader +{ + private const int _version = 1; + private const int _lineCount = 128; + private const int _lineCapacity = 1024; + private const string _name = "Penumbra.ModdedFile"; + + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + ReadOnlySpan actualFileName) + { + var accessor = GetCurrentLineLocking(); + lock (accessor) + { + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(12, characterAddress); + var span = GetSpan(accessor, 20, 80); + WriteSpan(characterName, span); + span = GetSpan(accessor, 92, 80); + WriteString(collectionName, span); + span = GetSpan(accessor, 172, 260); + WriteSpan(requestedFileName, span); + span = GetSpan(accessor, 432); + WriteSpan(actualFileName, span); + } + } + + public uint TotalCount + => TotalWrittenLines; + + public IEnumerable GetLines(DateTimeOffset crashTime) + { + var lineCount = (int)CurrentLineCount; + for (var i = lineCount - 1; i >= 0; --i) + { + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var characterName = ReadString(line[20..]); + var collectionName = ReadString(line[92..]); + var requestedFileName = ReadString(line[172..]); + var actualFileName = ReadString(line[432..]); + yield return new JsonObject() + { + [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp, + [nameof(ModdedFileLoadedEntry.ThreadId)] = thread, + [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName, + [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"), + [nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName, + [nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName, + [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName, + }; + } + } + + public static IBufferReader CreateReader() + => new ModdedFileBuffer(false); + + public static IModdedFileBufferWriter CreateWriter() + => new ModdedFileBuffer(); + + private ModdedFileBuffer(bool writer) + : base(_name, _version) + { } + + private ModdedFileBuffer() + : base(_name, _version, _lineCount, _lineCapacity) + { } +} diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs new file mode 100644 index 00000000..956a3db7 --- /dev/null +++ b/Penumbra.CrashHandler/CrashData.cs @@ -0,0 +1,62 @@ +using Penumbra.CrashHandler.Buffers; + +namespace Penumbra.CrashHandler; + +/// A base entry for crash data. +public interface ICrashDataEntry +{ + /// The timestamp of the event. + DateTimeOffset Timestamp { get; } + + /// The thread invoking the event. + int ThreadId { get; } + + /// The age of the event compared to the crash. (Redundantly with the timestamp) + double Age { get; } +} + +/// A full set of crash data. +public class CrashData +{ + /// The mode this data was obtained - manually or from a crash. + public string Mode { get; set; } = "Unknown"; + + /// The time this crash data was generated. + public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch; + + /// The FFXIV process ID when this data was generated. + public int ProcessId { get; set; } = 0; + + /// The FFXIV Exit Code (if any) when this data was generated. + public int ExitCode { get; set; } = 0; + + /// The total amount of characters loaded during this session. + public int TotalCharactersLoaded { get; set; } = 0; + + /// The total amount of modded files loaded during this session. + public int TotalModdedFilesLoaded { get; set; } = 0; + + /// The total amount of vfx functions invoked during this session. + public int TotalVFXFuncsInvoked { get; set; } = 0; + + /// The last character loaded before this crash data was generated. + public CharacterLoadedEntry? LastCharacterLoaded + => LastCharactersLoaded.Count == 0 ? default : LastCharactersLoaded[0]; + + /// The last modded file loaded before this crash data was generated. + public ModdedFileLoadedEntry? LastModdedFileLoaded + => LastModdedFilesLoaded.Count == 0 ? default : LastModdedFilesLoaded[0]; + + /// The last vfx function invoked before this crash data was generated. + public VfxFuncInvokedEntry? LastVfxFuncInvoked + => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0]; + + /// A collection of the last few characters loaded before this crash data was generated. + public List LastCharactersLoaded { get; } = []; + + /// A collection of the last few modded files loaded before this crash data was generated. + public List LastModdedFilesLoaded { get; } = []; + + /// A collection of the last few vfx functions invoked before this crash data was generated. + public List LastVfxFuncsInvoked { get; } = []; +} diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs new file mode 100644 index 00000000..283be526 --- /dev/null +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Nodes; +using Penumbra.CrashHandler.Buffers; + +namespace Penumbra.CrashHandler; + +public interface IBufferReader +{ + public uint TotalCount { get; } + public IEnumerable GetLines(DateTimeOffset crashTime); +} + +public sealed class GameEventLogReader : IDisposable +{ + public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers = + [ + (CharacterBaseBuffer.CreateReader(), "CharacterLoaded", "CharactersLoaded"), + (ModdedFileBuffer.CreateReader(), "ModdedFileLoaded", "ModdedFilesLoaded"), + (AnimationInvocationBuffer.CreateReader(), "VFXFuncInvoked", "VFXFuncsInvoked"), + ]; + + public void Dispose() + { + foreach (var (reader, _, _) in Readers) + (reader as IDisposable)?.Dispose(); + } + + + public JsonObject Dump(string mode, int processId, int exitCode) + { + var crashTime = DateTimeOffset.UtcNow; + var obj = new JsonObject + { + [nameof(CrashData.Mode)] = mode, + [nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow, + [nameof(CrashData.ProcessId)] = processId, + [nameof(CrashData.ExitCode)] = exitCode, + }; + + foreach (var (reader, singular, _) in Readers) + obj["Last" + singular] = reader.GetLines(crashTime).FirstOrDefault(); + + foreach (var (reader, _, plural) in Readers) + { + obj["Total" + plural] = reader.TotalCount; + var array = new JsonArray(); + foreach (var file in reader.GetLines(crashTime)) + array.Add(file); + obj["Last" + plural] = array; + } + + return obj; + } +} diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs new file mode 100644 index 00000000..8e809cec --- /dev/null +++ b/Penumbra.CrashHandler/GameEventLogWriter.cs @@ -0,0 +1,17 @@ +using Penumbra.CrashHandler.Buffers; + +namespace Penumbra.CrashHandler; + +public sealed class GameEventLogWriter : IDisposable +{ + public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter(); + public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter(); + public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter(); + + public void Dispose() + { + (CharacterBase as IDisposable)?.Dispose(); + (FileLoaded as IDisposable)?.Dispose(); + (AnimationFuncInvoked as IDisposable)?.Dispose(); + } +} diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj new file mode 100644 index 00000000..ea61b968 --- /dev/null +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -0,0 +1,28 @@ + + + + Exe + net7.0-windows + preview + enable + x64 + enable + true + false + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(HOME)/.xlcore/dalamud/Hooks/dev/ + $(DALAMUD_HOME)/ + + + + embedded + + + + embedded + + + diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs new file mode 100644 index 00000000..e4a46348 --- /dev/null +++ b/Penumbra.CrashHandler/Program.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using System.Text.Json; + +namespace Penumbra.CrashHandler; + +public class CrashHandler +{ + public static void Main(string[] args) + { + if (args.Length < 2 || !int.TryParse(args[1], out var pid)) + return; + + try + { + using var reader = new GameEventLogReader(); + var parent = Process.GetProcessById(pid); + + parent.WaitForExit(); + var exitCode = parent.ExitCode; + var obj = reader.Dump("Crash", pid, exitCode); + using var fs = File.Open(args[0], FileMode.Create); + using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true }); + obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true }); + } + catch (Exception ex) + { + File.WriteAllText(args[0], $"{DateTime.UtcNow} {pid} {ex}"); + } + } +} diff --git a/Penumbra.sln b/Penumbra.sln index 5c11aaea..78fa1543 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -18,6 +18,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,6 +46,10 @@ Global {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 1e232761..2f249c14 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Services; namespace Penumbra.Communication; @@ -19,5 +20,8 @@ public sealed class CreatingCharacterBase() { /// Api = 0, + + /// + CrashHandler = 0, } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 188be65d..9c0b4f2d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -33,6 +33,7 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; + public bool UseCrashHandler { get; set; } = true; public bool OpenWindowAtStart { get; set; } = false; public bool HideUiInGPose { get; set; } = false; public bool HideUiInCutscenes { get; set; } = true; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 98b1a5d6..0a542d04 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -47,7 +47,7 @@ public class EphemeralConfig : ISavable, IDisposable /// public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged) { - _saveService = saveService; + _saveService = saveService; _modPathChanged = modPathChanged; Load(); _modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig); @@ -94,13 +94,13 @@ public class EphemeralConfig : ISavable, IDisposable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer); + using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } - /// Overwrite the last saved mod path if it changes. + /// Overwrite the last saved mod path if it changes. private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _) { if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase)) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index b91c5375..77927593 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -2,21 +2,25 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; using Penumbra.Interop.PathResolving; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; /// Called for some sound effects caused by animations or VFX. public sealed unsafe class ApricotListenerSoundPlay : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly CrashHandlerService _crashHandler; - public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver) + public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; + _crashHandler = crashHandler; Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); } @@ -46,6 +50,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook public sealed unsafe class CharacterBaseLoadAnimation : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly CrashHandlerService _crashHandler; public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver, - DrawObjectState drawObjectState) + DrawObjectState drawObjectState, CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; + _crashHandler = crashHandler; Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); } @@ -33,7 +37,9 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook Load a ground-based area VFX. public sealed unsafe class LoadAreaVfx : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly CrashHandlerService _crashHandler; - public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver) + public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; + _crashHandler = crashHandler; Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); } @@ -25,10 +29,11 @@ public sealed unsafe class LoadAreaVfx : FastHook private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) { var newData = caster != null - ? _collectionResolver.IdentifyCollection(caster, true) - : ResolveData.Invalid; + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; var last = _state.SetAnimationData(newData); + _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx); var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); Penumbra.Log.Excessive( $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index af13805d..98454a77 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -1,19 +1,23 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; +using Penumbra.CrashHandler.Buffers; using Penumbra.Interop.PathResolving; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; /// Characters load some of their voice lines or whatever with this function. public sealed unsafe class LoadCharacterSound : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly CrashHandlerService _crashHandler; - public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver) + public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { - _state = state; + _state = state; _collectionResolver = collectionResolver; + _crashHandler = crashHandler; Task = hooks.CreateHook("Load Character Sound", (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, true); @@ -25,7 +29,9 @@ public sealed unsafe class LoadCharacterSound : FastHook {ret}."); _state.RestoreSoundData(last); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 240c062e..69c22773 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -2,9 +2,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; namespace Penumbra.Interop.Hooks.Animation; @@ -12,15 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation; /// Load a VFX specifically for a character. public sealed unsafe class LoadCharacterVfx : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; - private readonly IObjectTable _objects; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + private readonly CrashHandlerService _crashHandler; - public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects, + CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _objects = objects; + _crashHandler = crashHandler; Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); } @@ -45,6 +50,7 @@ public sealed unsafe class LoadCharacterVfx : FastHookGameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 2ca8ffe7..ade957b9 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -3,8 +3,10 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.CrashHandler; using Penumbra.GameData; using Penumbra.Interop.PathResolving; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; @@ -14,19 +16,21 @@ namespace Penumbra.Interop.Hooks.Animation; /// public sealed unsafe class LoadTimelineResources : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; - private readonly ICondition _conditions; - private readonly IObjectTable _objects; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly ICondition _conditions; + private readonly IObjectTable _objects; + private readonly CrashHandlerService _crashHandler; public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions, - IObjectTable objects) + IObjectTable objects, CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _conditions = conditions; _objects = objects; - Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); } public delegate ulong Delegate(nint timeline); @@ -39,7 +43,13 @@ public sealed unsafe class LoadTimelineResources : FastHook Called when some action timelines update. public sealed unsafe class ScheduleClipUpdate : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; - private readonly IObjectTable _objects; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + private readonly CrashHandlerService _crashHandler; - public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects, + CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _objects = objects; + _crashHandler = crashHandler; Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); } @@ -27,8 +32,9 @@ public sealed unsafe class ScheduleClipUpdate : FastHookSchedulerTimeline)); + var newData = LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline); + var last = _state.SetAnimationData(newData); + _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ScheduleClipUpdate); Task.Result.Original(clipScheduler); _state.RestoreAnimationData(last); } diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 48931d73..6de3aeb0 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -1,21 +1,25 @@ -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using OtterGui.Services; -using Penumbra.GameData; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.CrashHandler.Buffers; +using Penumbra.GameData; using Penumbra.Interop.PathResolving; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.Animation; -namespace Penumbra.Interop.Hooks.Animation; - /// Seems to load character actions when zoning or changing class, maybe. public sealed unsafe class SomeActionLoad : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly CrashHandlerService _crashHandler; - public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver) + public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; + _crashHandler = crashHandler; Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, true); } @@ -24,8 +28,10 @@ public sealed unsafe class SomeActionLoad : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Detour(ActionTimelineManager* timelineManager) { - var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true)); + var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); + var last = _state.SetAnimationData(newData); Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}."); + _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad); Task.Result.Original(timelineManager); _state.RestoreAnimationData(last); } diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 75caacee..3e60e62f 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -1,23 +1,28 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; +using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; using Penumbra.Interop.PathResolving; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; /// Unknown what exactly this is, but it seems to load a bunch of paps. public sealed unsafe class SomePapLoad : FastHook { - private readonly GameState _state; - private readonly CollectionResolver _collectionResolver; - private readonly IObjectTable _objects; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + private readonly CrashHandlerService _crashHandler; - public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects, + CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _objects = objects; + _crashHandler = crashHandler; Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, true); } @@ -33,8 +38,9 @@ public sealed unsafe class SomePapLoad : FastHook var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); if (actorIdx >= 0 && actorIdx < _objects.Length) { - var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), - true)); + var newData = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true); + var last = _state.SetAnimationData(newData); + _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.PapLoad); Task.Result.Original(a1, a2, a3, a4); _state.RestoreAnimationData(last); return; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 01b3d680..796eae01 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + net7.0-windows preview @@ -69,8 +69,7 @@ - - + @@ -79,8 +78,10 @@ + + @@ -103,4 +104,4 @@ $(GitCommitHash) - \ No newline at end of file + diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs new file mode 100644 index 00000000..4e2bce0f --- /dev/null +++ b/Penumbra/Services/CrashHandlerService.cs @@ -0,0 +1,296 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.CrashHandler; +using Penumbra.CrashHandler.Buffers; +using Penumbra.GameData.Actors; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.String.Classes; +using FileMode = System.IO.FileMode; + +namespace Penumbra.Services; + +public sealed class CrashHandlerService : IDisposable, IService +{ + private readonly FilenameService _files; + private readonly CommunicatorService _communicator; + private readonly ActorManager _actors; + private readonly ResourceLoader _resourceLoader; + private readonly Configuration _config; + + public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader, + Configuration config) + { + _files = files; + _communicator = communicator; + _actors = actors; + _resourceLoader = resourceLoader; + _config = config; + + if (!_config.UseCrashHandler) + return; + + OpenEventWriter(); + LaunchCrashHandler(); + if (_eventWriter != null) + Subscribe(); + } + + public void Dispose() + { + CloseEventWriter(); + _eventWriter?.Dispose(); + if (_child != null) + { + _child.Kill(); + Penumbra.Log.Debug($"Killed crash handler child process {_child.Id}."); + } + + Unsubscribe(); + } + + private Process? _child; + private GameEventLogWriter? _eventWriter; + + public string CopiedExe = string.Empty; + + public string OriginalExe + => _files.CrashHandlerExe; + + public string LogPath + => _files.LogFileName; + + public int ChildProcessId + => _child?.Id ?? -1; + + public int ProcessId + => Environment.ProcessId; + + public bool IsRunning + => _eventWriter != null && _child is { HasExited: false }; + + public int ChildExitCode + => IsRunning ? 0 : _child?.ExitCode ?? 0; + + public void Enable() + { + if (_config.UseCrashHandler) + return; + + _config.UseCrashHandler = true; + _config.Save(); + OpenEventWriter(); + LaunchCrashHandler(); + if (_eventWriter != null) + Subscribe(); + } + + public void Disable() + { + if (!_config.UseCrashHandler) + return; + + _config.UseCrashHandler = false; + _config.Save(); + CloseEventWriter(); + CloseCrashHandler(); + Unsubscribe(); + } + + public JsonObject? Load(string fileName) + { + if (!File.Exists(fileName)) + return null; + + try + { + var data = File.ReadAllText(fileName); + return JsonNode.Parse(data) as JsonObject; + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not parse crash dump at {fileName}:\n{ex}"); + return null; + } + } + + public void CloseCrashHandler() + { + if (_child == null) + return; + + try + { + if (_child.HasExited) + return; + + _child.Kill(); + Penumbra.Log.Debug($"Closed Crash Handler at {CopiedExe}."); + } + catch (Exception ex) + { + _child = null; + Penumbra.Log.Debug($"Closed not close Crash Handler at {CopiedExe}:\n{ex}."); + } + } + + public void LaunchCrashHandler() + { + try + { + CloseCrashHandler(); + CopiedExe = CopyExecutables(); + var info = new ProcessStartInfo() + { + CreateNoWindow = true, + FileName = CopiedExe, + }; + info.ArgumentList.Add(_files.LogFileName); + info.ArgumentList.Add(Environment.ProcessId.ToString()); + _child = Process.Start(info); + if (_child == null) + throw new Exception("Child Process could not be created."); + + Penumbra.Log.Information($"Opened Crash Handler at {CopiedExe}, PID {_child.Id}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not launch crash handler process:\n{ex}"); + CloseCrashHandler(); + _child = null; + } + } + + public JsonObject? Dump() + { + if (_eventWriter == null) + return null; + + try + { + using var reader = new GameEventLogReader(); + JsonObject jObj; + lock (_eventWriter) + { + jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0); + } + + var logFile = _files.LogFileName; + using var s = File.Open(logFile, FileMode.Create); + using var jw = new Utf8JsonWriter(s, new JsonWriterOptions() { Indented = true }); + jObj.WriteTo(jw); + Penumbra.Log.Information($"Dumped crash handler memory to {logFile}."); + return jObj; + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error dumping crash handler memory to file:\n{ex}"); + return null; + } + } + + private string CopyExecutables() + { + var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!; + var folder = Path.Combine(parent, "temp"); + Directory.CreateDirectory(folder); + foreach (var file in Directory.EnumerateFiles(parent, "Penumbra.CrashHandler.*")) + File.Copy(file, Path.Combine(folder, Path.GetFileName(file)), true); + return Path.Combine(folder, Path.GetFileName(_files.CrashHandlerExe)); + } + + public void LogAnimation(nint character, ModCollection collection, AnimationInvocationType type) + { + if (_eventWriter == null) + return; + + var name = GetActorName(character); + lock (_eventWriter) + { + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type); + } + } + + private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3) + { + if (_eventWriter == null) + return; + + var name = GetActorName(address); + + lock (_eventWriter) + { + _eventWriter?.CharacterBase.WriteLine(address, name.Span, collection); + } + } + + private unsafe ByteString GetActorName(nint address) + { + var obj = (GameObject*)address; + if (obj == null) + return ByteString.FromSpanUnsafe("Unknown"u8, true, false, true); + + var id = _actors.FromObject(obj, out _, false, true, false); + return id.IsValid ? ByteString.FromStringUnsafe(id.Incognito(null), false) : + obj->Name[0] != 0 ? new ByteString(obj->Name) : ByteString.FromStringUnsafe($"Actor #{obj->ObjectIndex}", false); + } + + private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) + { + if (manipulatedPath == null || _eventWriter == null) + return; + + var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1; + if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1))) + return; + + var name = GetActorName(resolveData.AssociatedGameObject); + lock (_eventWriter) + { + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name, + manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); + } + } + + private void CloseEventWriter() + { + if (_eventWriter == null) + return; + + _eventWriter.Dispose(); + _eventWriter = null; + Penumbra.Log.Debug("Closed Event Writer for crash handler."); + } + + private void OpenEventWriter() + { + try + { + CloseEventWriter(); + _eventWriter = new GameEventLogWriter(); + Penumbra.Log.Debug("Opened new Event Writer for crash handler."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not open Event Writer:\n{ex}"); + CloseEventWriter(); + } + } + + private unsafe void Subscribe() + { + _communicator.CreatingCharacterBase.Subscribe(OnCreatingCharacterBase, CreatingCharacterBase.Priority.CrashHandler); + _resourceLoader.ResourceLoaded += OnResourceLoaded; + } + + private unsafe void Unsubscribe() + { + _communicator.CreatingCharacterBase.Unsubscribe(OnCreatingCharacterBase); + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + } +} diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 5f918a90..49b4cb42 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -15,6 +15,12 @@ public class FilenameService(DalamudPluginInterface pi) : IService public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); + public readonly string CrashHandlerExe = + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe"); + + public readonly string LogFileName = + Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(pi.ConfigDirectory.FullName)!)!, "Penumbra.log"); + /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) => CollectionFile(collection.Name); diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 76fb8c96..ab0badf4 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -12,22 +12,13 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; -public class ChangedItemsTab : ITab +public class ChangedItemsTab( + CollectionManager collectionManager, + CollectionSelectHeader collectionHeader, + ChangedItemDrawer drawer, + CommunicatorService communicator) + : ITab { - private readonly CollectionManager _collectionManager; - private readonly ChangedItemDrawer _drawer; - private readonly CollectionSelectHeader _collectionHeader; - private readonly CommunicatorService _communicator; - - public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, - CommunicatorService communicator) - { - _collectionManager = collectionManager; - _collectionHeader = collectionHeader; - _drawer = drawer; - _communicator = communicator; - } - public ReadOnlySpan Label => "Changed Items"u8; @@ -36,8 +27,8 @@ public class ChangedItemsTab : ITab public void DrawContent() { - _collectionHeader.Draw(true); - _drawer.DrawTypeFilter(); + collectionHeader.Draw(true); + drawer.DrawTypeFilter(); var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); if (!child) @@ -54,7 +45,7 @@ public class ChangedItemsTab : ITab ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); - var items = _collectionManager.Active.Current.ChangedItems; + var items = collectionManager.Active.Current.ChangedItems; var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); ImGuiClip.DrawEndDummy(rest, height); } @@ -75,21 +66,21 @@ public class ChangedItemsTab : ITab /// Apply the current filters. private bool FilterChangedItem(KeyValuePair, object?)> item) - => _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) + => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. private void DrawChangedItemColumn(KeyValuePair, object?)> item) { ImGui.TableNextColumn(); - _drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Key, item.Value.Item2); ImGui.SameLine(); - _drawer.DrawChangedItem(item.Key, item.Value.Item2); + drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - _drawer.DrawModelData(item.Value.Item2); + drawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) @@ -102,7 +93,7 @@ public class ChangedItemsTab : ITab if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) && ImGui.GetIO().KeyCtrl && first is Mod mod) - _communicator.SelectTab.Invoke(TabType.Mods, mod); + communicator.SelectTab.Invoke(TabType.Mods, mod); if (ImGui.IsItemHovered()) { diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index ad3fdb3d..9fd07f27 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -44,8 +44,8 @@ public class ConfigTabBar : IDisposable Watcher = watcher; OnScreen = onScreen; Messages = messages; - Tabs = new ITab[] - { + Tabs = + [ Settings, Collections, Mods, @@ -56,7 +56,7 @@ public class ConfigTabBar : IDisposable Resource, Watcher, Messages, - }; + ]; _communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar); } diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs new file mode 100644 index 00000000..3d32d267 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -0,0 +1,104 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.CrashHandler; + +namespace Penumbra.UI.Tabs.Debug; + +public static class CrashDataExtensions +{ + public static void DrawMeta(this CrashData data) + { + using (ImRaii.Group()) + { + ImGui.TextUnformatted(nameof(data.Mode)); + ImGui.TextUnformatted(nameof(data.CrashTime)); + ImGui.TextUnformatted(nameof(data.ExitCode)); + ImGui.TextUnformatted(nameof(data.ProcessId)); + ImGui.TextUnformatted(nameof(data.TotalModdedFilesLoaded)); + ImGui.TextUnformatted(nameof(data.TotalCharactersLoaded)); + ImGui.TextUnformatted(nameof(data.TotalVFXFuncsInvoked)); + } + + ImGui.SameLine(); + using (ImRaii.Group()) + { + ImGui.TextUnformatted(data.Mode); + ImGui.TextUnformatted(data.CrashTime.ToString()); + ImGui.TextUnformatted(data.ExitCode.ToString()); + ImGui.TextUnformatted(data.ProcessId.ToString()); + ImGui.TextUnformatted(data.TotalModdedFilesLoaded.ToString()); + ImGui.TextUnformatted(data.TotalCharactersLoaded.ToString()); + ImGui.TextUnformatted(data.TotalVFXFuncsInvoked.ToString()); + } + } + + public static void DrawCharacters(this CrashData data) + { + using var tree = ImRaii.TreeNode("Last Characters"); + if (!tree) + return; + + using var table = ImRaii.Table("##characterTable", 6, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner); + if (!table) + return; + + ImGuiClip.ClippedDraw(data.LastCharactersLoaded, character => + { + ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(character.ThreadId.ToString()); + ImGuiUtil.DrawTableColumn(character.CharacterName); + ImGuiUtil.DrawTableColumn(character.CollectionName); + ImGuiUtil.DrawTableColumn(character.CharacterAddress); + ImGuiUtil.DrawTableColumn(character.Timestamp.ToString()); + }, ImGui.GetTextLineHeightWithSpacing()); + } + + public static void DrawFiles(this CrashData data) + { + using var tree = ImRaii.TreeNode("Last Files"); + if (!tree) + return; + + using var table = ImRaii.Table("##filesTable", 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner); + if (!table) + return; + + ImGuiClip.ClippedDraw(data.LastModdedFilesLoaded, file => + { + ImGuiUtil.DrawTableColumn(file.Age.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(file.ThreadId.ToString()); + ImGuiUtil.DrawTableColumn(file.ActualFileName); + ImGuiUtil.DrawTableColumn(file.RequestedFileName); + ImGuiUtil.DrawTableColumn(file.CharacterName); + ImGuiUtil.DrawTableColumn(file.CollectionName); + ImGuiUtil.DrawTableColumn(file.CharacterAddress); + ImGuiUtil.DrawTableColumn(file.Timestamp.ToString()); + }, ImGui.GetTextLineHeightWithSpacing()); + } + + public static void DrawVfxInvocations(this CrashData data) + { + using var tree = ImRaii.TreeNode("Last VFX Invocations"); + if (!tree) + return; + + using var table = ImRaii.Table("##vfxTable", 7, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner); + if (!table) + return; + + ImGuiClip.ClippedDraw(data.LastVfxFuncsInvoked, vfx => + { + ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); + ImGuiUtil.DrawTableColumn(vfx.InvocationType); + ImGuiUtil.DrawTableColumn(vfx.CharacterName); + ImGuiUtil.DrawTableColumn(vfx.CollectionName); + ImGuiUtil.DrawTableColumn(vfx.CharacterAddress); + ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString()); + }, ImGui.GetTextLineHeightWithSpacing()); + } +} diff --git a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs new file mode 100644 index 00000000..c8e7f001 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using Dalamud.Interface.DragDrop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.CrashHandler; +using Penumbra.Services; + +namespace Penumbra.UI.Tabs.Debug; + +public class CrashHandlerPanel(CrashHandlerService _service, Configuration _config, IDragDropManager _dragDrop) : IService +{ + private CrashData? _lastDump; + private string _lastLoadedFile = string.Empty; + private CrashData? _lastLoad; + private Exception? _lastLoadException; + + public void Draw() + { + DrawDropSource(); + DrawData(); + DrawDropTarget(); + } + + private void DrawData() + { + using var _ = ImRaii.Group(); + using var header = ImRaii.CollapsingHeader("Crash Handler"); + if (!header) + return; + + DrawButtons(); + DrawMainData(); + DrawObject("Last Manual Dump", _lastDump, null); + DrawObject(_lastLoadedFile.Length > 0 ? $"Loaded File ({_lastLoadedFile})###Loaded File" : "Loaded File", _lastLoad, + _lastLoadException); + } + + private void DrawMainData() + { + using var table = ImRaii.Table("##CrashHandlerTable", 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + PrintValue("Enabled", _config.UseCrashHandler); + PrintValue("Copied Executable Path", _service.CopiedExe); + PrintValue("Original Executable Path", _service.OriginalExe); + PrintValue("Log File Path", _service.LogPath); + PrintValue("XIV Process ID", _service.ProcessId.ToString()); + PrintValue("Crash Handler Running", _service.IsRunning.ToString()); + PrintValue("Crash Handler Process ID", _service.ChildProcessId.ToString()); + PrintValue("Crash Handler Exit Code", _service.ChildExitCode.ToString()); + } + + private void DrawButtons() + { + if (ImGui.Button("Dump Crash Handler Memory")) + _lastDump = _service.Dump()?.Deserialize(); + + if (ImGui.Button("Enable")) + _service.Enable(); + + ImGui.SameLine(); + if (ImGui.Button("Disable")) + _service.Disable(); + + if (ImGui.Button("Shutdown Crash Handler")) + _service.CloseCrashHandler(); + ImGui.SameLine(); + if (ImGui.Button("Relaunch Crash Handler")) + _service.LaunchCrashHandler(); + } + + private void DrawDropSource() + { + _dragDrop.CreateImGuiSource("LogDragDrop", m => m.Files.Any(f => f.EndsWith("Penumbra.log")), m => + { + ImGui.TextUnformatted("Dragging Penumbra.log for import."); + return true; + }); + } + + private void DrawDropTarget() + { + if (!_dragDrop.CreateImGuiTarget("LogDragDrop", out var files, out _)) + return; + + var file = files.FirstOrDefault(f => f.EndsWith("Penumbra.log")); + if (file == null) + return; + + _lastLoadedFile = file; + try + { + var jObj = _service.Load(file); + _lastLoad = jObj?.Deserialize(); + _lastLoadException = null; + } + catch (Exception ex) + { + _lastLoad = null; + _lastLoadException = ex; + } + } + + private static void DrawObject(string name, CrashData? data, Exception? ex) + { + using var tree = ImRaii.TreeNode(name); + if (!tree) + return; + + if (ex != null) + { + ImGuiUtil.TextWrapped(ex.ToString()); + return; + } + + if (data == null) + { + ImGui.TextUnformatted("Nothing loaded."); + return; + } + + data.DrawMeta(); + data.DrawFiles(); + data.DrawCharacters(); + data.DrawVfxInvocations(); + } + + private static void PrintValue(string label, in T data) + { + ImGuiUtil.DrawTableColumn(label); + ImGuiUtil.DrawTableColumn(data?.ToString() ?? "NULL"); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index f4ddbe31..ab7ccf0c 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -53,7 +53,7 @@ public class Diagnostics(IServiceProvider provider) foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { - var container = (IAsyncDataContainer) provider.GetRequiredService(type); + var container = (IAsyncDataContainer)provider.GetRequiredService(type); ImGuiUtil.DrawTableColumn(container.Name); ImGuiUtil.DrawTableColumn(container.Time.ToString()); ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); @@ -88,18 +88,22 @@ public class DebugTab : Window, ITab private readonly TextureManager _textureManager; private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly RedrawService _redraws; - private readonly DictEmote _emotes; + private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; private readonly IObjectTable _objects; private readonly IClientState _clientState; private readonly IpcTester _ipcTester; + private readonly CrashHandlerPanel _crashHandlerPanel; - public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, IClientState clientState, - ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, + public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, + IClientState clientState, + ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester) + TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, + Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -134,7 +138,8 @@ public class DebugTab : Window, ITab _redraws = redraws; _emotes = emotes; _diagnostics = diagnostics; - _ipcTester = ipcTester; + _ipcTester = ipcTester; + _crashHandlerPanel = crashHandlerPanel; _objects = objects; _clientState = clientState; } @@ -158,6 +163,9 @@ public class DebugTab : Window, ITab return; DrawDebugTabGeneral(); + ImGui.NewLine(); + _crashHandlerPanel.Draw(); + ImGui.NewLine(); _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); ImGui.NewLine(); @@ -257,6 +265,7 @@ public class DebugTab : Window, ITab } } + var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index); using (var tree = TreeNode($"Mods ({issues} Issues)###Mods")) { @@ -394,7 +403,7 @@ public class DebugTab : Window, ITab private void DrawPerformanceTab() { ImGui.NewLine(); - if (ImGui.CollapsingHeader("Performance")) + if (!ImGui.CollapsingHeader("Performance")) return; using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen)) diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 8ef6c30e..37561000 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -8,31 +8,22 @@ using Penumbra.Collections; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; -public class EffectiveTab : ITab +public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) + : ITab { - private readonly CollectionManager _collectionManager; - private readonly CollectionSelectHeader _collectionHeader; - - public EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) - { - _collectionManager = collectionManager; - _collectionHeader = collectionHeader; - } - public ReadOnlySpan Label => "Effective Changes"u8; public void DrawContent() { SetupEffectiveSizes(); - _collectionHeader.Draw(true); + collectionHeader.Draw(true); DrawFilters(); using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); if (!child) @@ -48,7 +39,7 @@ public class EffectiveTab : ITab ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); - DrawEffectiveRows(_collectionManager.Active.Current, skips, height, + DrawEffectiveRows(collectionManager.Active.Current, skips, height, _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 9f070d35..d111c465 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -17,68 +17,53 @@ using Penumbra.Collections.Manager; namespace Penumbra.UI.Tabs; -public class ModsTab : ITab +public class ModsTab( + ModManager modManager, + CollectionManager collectionManager, + ModFileSystemSelector selector, + ModPanel panel, + TutorialService tutorial, + RedrawService redrawService, + Configuration config, + IClientState clientState, + CollectionSelectHeader collectionHeader, + ITargetManager targets, + IObjectTable objectTable) + : ITab { - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _panel; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private readonly ActiveCollections _activeCollections; - private readonly RedrawService _redrawService; - private readonly Configuration _config; - private readonly IClientState _clientState; - private readonly CollectionSelectHeader _collectionHeader; - private readonly ITargetManager _targets; - private readonly IObjectTable _objectTable; - - public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, - TutorialService tutorial, RedrawService redrawService, Configuration config, IClientState clientState, - CollectionSelectHeader collectionHeader, ITargetManager targets, IObjectTable objectTable) - { - _modManager = modManager; - _activeCollections = collectionManager.Active; - _selector = selector; - _panel = panel; - _tutorial = tutorial; - _redrawService = redrawService; - _config = config; - _clientState = clientState; - _collectionHeader = collectionHeader; - _targets = targets; - _objectTable = objectTable; - } + private readonly ActiveCollections _activeCollections = collectionManager.Active; public bool IsVisible - => _modManager.Valid; + => modManager.Valid; public ReadOnlySpan Label => "Mods"u8; public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.Mods); + => tutorial.OpenTutorial(BasicTutorialSteps.Mods); public Mod SelectMod { - set => _selector.SelectByValue(value); + set => selector.SelectByValue(value); } public void DrawContent() { try { - _selector.Draw(GetModSelectorSize(_config)); + selector.Draw(GetModSelectorSize(config)); ImGui.SameLine(); using var group = ImRaii.Group(); - _collectionHeader.Draw(false); + collectionHeader.Draw(false); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, _config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), true, ImGuiWindowFlags.HorizontalScrollbar)) { style.Pop(); if (child) - _panel.Draw(); + panel.Draw(); style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); } @@ -89,14 +74,14 @@ public class ModsTab : ITab catch (Exception e) { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); - Penumbra.Log.Error($"{_modManager.Count} Mods\n" + Penumbra.Log.Error($"{modManager.Count} Mods\n" + $"{_activeCollections.Current.AnonymizedName} Current Collection\n" + $"{_activeCollections.Current.Settings.Count} Settings\n" - + $"{_selector.SortMode.Name} Sort Mode\n" - + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{selector.SortMode.Name} Sort Mode\n" + + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" - + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + + $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n"); } } @@ -115,9 +100,9 @@ public class ModsTab : ITab private void DrawRedrawLine() { - if (_config.HideRedrawBar) + if (config.HideRedrawBar) { - _tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); + tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); return; } @@ -135,15 +120,15 @@ public class ModsTab : ITab } var hovered = ImGui.IsItemHovered(); - _tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); + tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); if (hovered) ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); using var id = ImRaii.PushId("Redraw"); - using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null); + using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; - var tt = _objectTable.GetObjectAddress(0) == nint.Zero + var tt = objectTable.GetObjectAddress(0) == nint.Zero ? "\nCan only be used when you are logged in and your character is available." : string.Empty; DrawButton(buttonWidth, "All", string.Empty, tt); @@ -151,13 +136,13 @@ public class ModsTab : ITab DrawButton(buttonWidth, "Self", "self", tt); ImGui.SameLine(); - tt = _targets.Target == null && _targets.GPoseTarget == null + tt = targets.Target == null && targets.GPoseTarget == null ? "\nCan only be used when you have a target." : string.Empty; DrawButton(buttonWidth, "Target", "target", tt); ImGui.SameLine(); - tt = _targets.FocusTarget == null + tt = targets.FocusTarget == null ? "\nCan only be used when you have a focus target." : string.Empty; DrawButton(buttonWidth, "Focus", "focus", tt); @@ -176,9 +161,9 @@ public class ModsTab : ITab if (ImGui.Button(label, size)) { if (lower.Length > 0) - _redrawService.RedrawObject(lower, RedrawType.Redraw); + redrawService.RedrawObject(lower, RedrawType.Redraw); else - _redrawService.RedrawAll(RedrawType.Redraw); + redrawService.RedrawAll(RedrawType.Redraw); } } diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 8d323baf..09772d8e 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -7,7 +7,7 @@ namespace Penumbra.UI.Tabs; public class OnScreenTab : ITab { private readonly Configuration _config; - private ResourceTreeViewer _viewer; + private readonly ResourceTreeViewer _viewer; public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) { diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 6f3dec30..bbb0561b 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -12,24 +12,14 @@ using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; -public class ResourceTab : ITab +public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) + : ITab { - private readonly Configuration _config; - private readonly ResourceManagerService _resourceManager; - private readonly ISigScanner _sigScanner; - - public ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) - { - _config = config; - _resourceManager = resourceManager; - _sigScanner = sigScanner; - } - public ReadOnlySpan Label => "Resource Manager"u8; public bool IsVisible - => _config.DebugMode; + => config.DebugMode; /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. public void DrawContent() @@ -44,15 +34,15 @@ public class ResourceTab : ITab unsafe { - _resourceManager.IterateGraphs(DrawCategoryContainer); + resourceManager.IterateGraphs(DrawCategoryContainer); } ImGui.NewLine(); unsafe { ImGui.TextUnformatted( - $"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress:X})"); - ImGui.TextUnformatted($"Actual Address: 0x{(ulong)_resourceManager.ResourceManager:X}"); + $"Static Address: 0x{(ulong)resourceManager.ResourceManagerAddress:X} (+0x{(ulong)resourceManager.ResourceManagerAddress - (ulong)sigScanner.Module.BaseAddress:X})"); + ImGui.TextUnformatted($"Actual Address: 0x{(ulong)resourceManager.ResourceManager:X}"); } } @@ -82,7 +72,7 @@ public class ResourceTab : ITab ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth); ImGui.TableHeadersRow(); - _resourceManager.IterateResourceMap(map, (hash, r) => + resourceManager.IterateResourceMap(map, (hash, r) => { // Filter unwanted names. if (_resourceManagerFilter.Length != 0 @@ -125,7 +115,7 @@ public class ResourceTab : ITab if (tree) { SetTableWidths(); - _resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); + resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); } } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index cb873592..d0e8fa1a 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,18 +11,6 @@ "Unosquare.Swan.Lite": "3.0.0" } }, - "Microsoft.CodeAnalysis.Common": { - "type": "Direct", - "requested": "[4.8.0, )", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", "requested": "[7.0.0, )", @@ -55,29 +43,15 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[2.1.2, )", - "resolved": "2.1.2", - "contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "5.0.0", - "System.Text.Encoding.CodePages": "5.0.0" - } - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + "requested": "[3.1.3, )", + "resolved": "3.1.3", + "contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg==" }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" - }, "SharpGLTF.Runtime": { "type": "Transitive", "resolved": "1.0.0-alpha0030", @@ -86,32 +60,6 @@ "SharpGLTF.Core": "1.0.0-alpha0030" } }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", - "dependencies": { - "System.Collections.Immutable": "7.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, "System.ValueTuple": { "type": "Transitive", "resolved": "4.5.0", @@ -134,11 +82,14 @@ "penumbra.api": { "type": "Project" }, + "penumbra.crashhandler": { + "type": "Project" + }, "penumbra.gamedata": { "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[1.0.13, )", + "Penumbra.Api": "[1.0.14, )", "Penumbra.String": "[1.0.4, )" } }, From 8318a4bd84786858c67171261f18f55a6a8a6cc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:20:46 +0100 Subject: [PATCH 1547/2451] Compatibility fix. --- Penumbra/Import/Models/Export/MaterialExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index f17fdaa2..82e13eb9 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -53,7 +53,7 @@ public class MaterialExporter var normal = material.Textures[TextureUsage.SamplerNormal]; var operation = new ProcessCharacterNormalOperation(normal, table); - ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation); // Check if full textures are provided, and merge in if available. var baseColor = operation.BaseColor; @@ -199,7 +199,7 @@ public class MaterialExporter small.Mutate(context => context.Resize(large.Width, large.Height)); var operation = new MultiplyOperation(target, multiplier); - ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds(), in operation); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds, in operation); } } From 4fc763c9aa719948d41b4075e684e0647c5e96de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:21:06 +0100 Subject: [PATCH 1548/2451] Keep first empty option in its desired location. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 94a5e5ac..7c4b94d8 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OtterGui; using Penumbra.Api.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; @@ -35,7 +36,8 @@ public partial class TexToolsImporter var modList = modListRaw.Select(m => JsonConvert.DeserializeObject(m, JsonSettings)!).ToList(); - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name), _config.ReplaceNonAsciiOnImport, true); + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name), + _config.ReplaceNonAsciiOnImport, true); // Create a new ModMeta from the TTMP mod list info _modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null); @@ -193,15 +195,17 @@ public partial class TexToolsImporter optionIdx += maxOptions; // Handle empty options for single select groups without creating a folder for them. - // We only want one of those at most, and it should usually be the first option. + // We only want one of those at most. if (group.SelectionType == GroupType.Single) { - var empty = group.OptionList.FirstOrDefault(o => o.Name.Length > 0 && o.ModsJsons.Length == 0); - if (empty != null) + var idx = group.OptionList.IndexOf(o => o.Name.Length > 0 && o.ModsJsons.Length == 0); + if (idx >= 0) { - _currentOptionName = empty.Name; - options.Insert(0, ModCreator.CreateEmptySubMod(empty.Name)); - defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; + var option = group.OptionList[idx]; + _currentOptionName = option.Name; + options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); + if (option.IsChecked) + defaultSettings = (uint) idx; } } From 50f81cc889999c2cee7634a72d547eeecd3516b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:21:23 +0100 Subject: [PATCH 1549/2451] Skip locals init. --- .../MaterialPreview/LiveColorTablePreviewer.cs | 1 + .../ResolveContext.PathResolution.cs | 4 +++- .../Interop/ResourceTree/ResolveContext.cs | 1 + Penumbra/Mods/Editor/DuplicateManager.cs | 18 +++++++++--------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 801c3bf0..a8e4ea4d 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -62,6 +62,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase _updatePending = true; } + [SkipLocalsInit] private void OnFrameworkUpdate(IFramework _) { if (!_updatePending) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index cf939292..d5b4fa39 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -89,6 +89,7 @@ internal partial record ResolveContext }; } + [SkipLocalsInit] private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { var variant = ResolveMaterialVariant(imc, Equipment.Variant); @@ -100,6 +101,7 @@ internal partial record ResolveContext return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } + [SkipLocalsInit] private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { var setIdHigh = Equipment.Set.Id / 100; @@ -168,7 +170,7 @@ internal partial record ResolveContext { var modelPosition = modelPath.IndexOf("/model/"u8); if (modelPosition < 0) - return Span.Empty; + return []; var baseDirectory = modelPath[..modelPosition]; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0637cba6..d1701f47 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -54,6 +54,7 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } + [SkipLocalsInit] private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) { if (resourceHandle == null) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index dad05102..77d10cc4 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -7,8 +7,8 @@ namespace Penumbra.Mods.Editor; public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) { - private readonly SHA256 _hasher = SHA256.Create(); - private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; + private readonly SHA256 _hasher = SHA256.Create(); + private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates => _duplicates; @@ -164,17 +164,17 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } /// Check if two files are identical on a binary level. Returns true if they are identical. + [SkipLocalsInit] public static unsafe bool CompareFilesDirectly(FullPath f1, FullPath f2) { + const int size = 256; if (!f1.Exists || !f2.Exists) return false; - using var s1 = File.OpenRead(f1.FullName); - using var s2 = File.OpenRead(f2.FullName); - var buffer1 = stackalloc byte[256]; - var buffer2 = stackalloc byte[256]; - var span1 = new Span(buffer1, 256); - var span2 = new Span(buffer2, 256); + using var s1 = File.OpenRead(f1.FullName); + using var s2 = File.OpenRead(f2.FullName); + Span span1 = stackalloc byte[size]; + Span span2 = stackalloc byte[size]; while (true) { @@ -186,7 +186,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (!span1[..bytes1].SequenceEqual(span2[..bytes2])) return false; - if (bytes1 < 256) + if (bytes1 < size) return true; } } From c1a9c798ae844c79023a1248fa4ce80731dcf50b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:22:07 +0100 Subject: [PATCH 1550/2451] auto format. --- Penumbra/Penumbra.cs | 71 ++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 67f523ba..b532339f 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -19,7 +19,6 @@ using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; @@ -74,8 +73,8 @@ public class Penumbra : IDalamudPlugin _tempCollections = _services.GetService(); _redrawService = _services.GetService(); _communicatorService = _services.GetService(); - _services.GetService(); // Initialize because not required anywhere else. - _services.GetService(); // Initialize because not required anywhere else. + _services.GetService(); // Initialize because not required anywhere else. + _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); _services.GetService(); @@ -245,38 +244,38 @@ public class Penumbra : IDalamudPlugin return sb.ToString(); } - private static string CollectLocaleEnvironmentVariables() - { - var variableNames = new List(); - var variables = new Dictionary(StringComparer.Ordinal); - foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) - { - var key = (string)variable.Key; - if (key.Equals("LANG", StringComparison.Ordinal) || key.StartsWith("LC_", StringComparison.Ordinal)) - { - variableNames.Add(key); - variables.Add(key, ((string?)variable.Value) ?? string.Empty); - } - } - - variableNames.Sort(); - - var pos = variableNames.IndexOf("LC_ALL"); - if (pos > 0) // If it's == 0, we're going to do a no-op. - { - variableNames.RemoveAt(pos); - variableNames.Insert(0, "LC_ALL"); - } - - pos = variableNames.IndexOf("LANG"); - if (pos >= 0 && pos < variableNames.Count - 1) - { - variableNames.RemoveAt(pos); - variableNames.Add("LANG"); - } - - return variableNames.Count == 0 - ? "None" - : string.Join(", ", variableNames.Select(name => $"`{name}={variables[name]}`")); + private static string CollectLocaleEnvironmentVariables() + { + var variableNames = new List(); + var variables = new Dictionary(StringComparer.Ordinal); + foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + var key = (string)variable.Key; + if (key.Equals("LANG", StringComparison.Ordinal) || key.StartsWith("LC_", StringComparison.Ordinal)) + { + variableNames.Add(key); + variables.Add(key, (string?)variable.Value ?? string.Empty); + } + } + + variableNames.Sort(); + + var pos = variableNames.IndexOf("LC_ALL"); + if (pos > 0) // If it's == 0, we're going to do a no-op. + { + variableNames.RemoveAt(pos); + variableNames.Insert(0, "LC_ALL"); + } + + pos = variableNames.IndexOf("LANG"); + if (pos >= 0 && pos < variableNames.Count - 1) + { + variableNames.RemoveAt(pos); + variableNames.Add("LANG"); + } + + return variableNames.Count == 0 + ? "None" + : string.Join(", ", variableNames.Select(name => $"`{name}={variables[name]}`")); } } From 7cb50030f97d353b3870d41c4edb00e082b2c177 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Mar 2024 16:58:02 +0100 Subject: [PATCH 1551/2451] Fix some stuff, add versions. --- Penumbra.CrashHandler/CrashData.cs | 12 +++++++++--- Penumbra.CrashHandler/GameEventLogReader.cs | 12 +++++++----- Penumbra.CrashHandler/Program.cs | 4 ++-- Penumbra/Services/CrashHandlerService.cs | 18 +++++++++++------- Penumbra/Services/ValidityChecker.cs | 13 ++++++++++--- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 6 ++++++ 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs index 956a3db7..cdac103f 100644 --- a/Penumbra.CrashHandler/CrashData.cs +++ b/Penumbra.CrashHandler/CrashData.cs @@ -24,6 +24,12 @@ public class CrashData /// The time this crash data was generated. public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch; + /// Penumbra's Version when this crash data was created. + public string Version { get; set; } = string.Empty; + + /// The Game's Version when this crash data was created. + public string GameVersion { get; set; } = string.Empty; + /// The FFXIV process ID when this data was generated. public int ProcessId { get; set; } = 0; @@ -52,11 +58,11 @@ public class CrashData => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0]; /// A collection of the last few characters loaded before this crash data was generated. - public List LastCharactersLoaded { get; } = []; + public List LastCharactersLoaded { get; set; } = []; /// A collection of the last few modded files loaded before this crash data was generated. - public List LastModdedFilesLoaded { get; } = []; + public List LastModdedFilesLoaded { get; set; } = []; /// A collection of the last few vfx functions invoked before this crash data was generated. - public List LastVfxFuncsInvoked { get; } = []; + public List LastVfxFuncsInvoked { get; set; } = []; } diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs index 283be526..1ae49fa5 100644 --- a/Penumbra.CrashHandler/GameEventLogReader.cs +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -25,15 +25,17 @@ public sealed class GameEventLogReader : IDisposable } - public JsonObject Dump(string mode, int processId, int exitCode) + public JsonObject Dump(string mode, int processId, int exitCode, string version, string gameVersion) { var crashTime = DateTimeOffset.UtcNow; var obj = new JsonObject { - [nameof(CrashData.Mode)] = mode, - [nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow, - [nameof(CrashData.ProcessId)] = processId, - [nameof(CrashData.ExitCode)] = exitCode, + [nameof(CrashData.Mode)] = mode, + [nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow, + [nameof(CrashData.ProcessId)] = processId, + [nameof(CrashData.ExitCode)] = exitCode, + [nameof(CrashData.Version)] = version, + [nameof(CrashData.GameVersion)] = gameVersion, }; foreach (var (reader, singular, _) in Readers) diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index e4a46348..518e2d04 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -7,7 +7,7 @@ public class CrashHandler { public static void Main(string[] args) { - if (args.Length < 2 || !int.TryParse(args[1], out var pid)) + if (args.Length < 4 || !int.TryParse(args[1], out var pid)) return; try @@ -17,7 +17,7 @@ public class CrashHandler parent.WaitForExit(); var exitCode = parent.ExitCode; - var obj = reader.Dump("Crash", pid, exitCode); + var obj = reader.Dump("Crash", pid, exitCode, args[2], args[3]); using var fs = File.Open(args[0], FileMode.Create); using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true }); obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true }); diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 4e2bce0f..a3a35b78 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -22,15 +22,17 @@ public sealed class CrashHandlerService : IDisposable, IService private readonly ActorManager _actors; private readonly ResourceLoader _resourceLoader; private readonly Configuration _config; + private readonly ValidityChecker _validityChecker; public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader, - Configuration config) + Configuration config, ValidityChecker validityChecker) { - _files = files; - _communicator = communicator; - _actors = actors; - _resourceLoader = resourceLoader; - _config = config; + _files = files; + _communicator = communicator; + _actors = actors; + _resourceLoader = resourceLoader; + _config = config; + _validityChecker = validityChecker; if (!_config.UseCrashHandler) return; @@ -152,6 +154,8 @@ public sealed class CrashHandlerService : IDisposable, IService }; info.ArgumentList.Add(_files.LogFileName); info.ArgumentList.Add(Environment.ProcessId.ToString()); + info.ArgumentList.Add($"{_validityChecker.Version} ({_validityChecker.CommitHash})"); + info.ArgumentList.Add(_validityChecker.GameVersion); _child = Process.Start(info); if (_child == null) throw new Exception("Child Process could not be created."); @@ -177,7 +181,7 @@ public sealed class CrashHandlerService : IDisposable, IService JsonObject jObj; lock (_eventWriter) { - jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0); + jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0, $"{_validityChecker.Version} ({_validityChecker.CommitHash})", _validityChecker.GameVersion); } var logFile = _files.LogFileName; diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 4d071f85..d4b5005f 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.System.Framework; using OtterGui.Classes; using OtterGui.Services; @@ -20,6 +21,7 @@ public class ValidityChecker : IService public readonly string Version; public readonly string CommitHash; + public readonly string GameVersion; public ValidityChecker(DalamudPluginInterface pi) { @@ -28,14 +30,19 @@ public class ValidityChecker : IService IsValidSourceRepo = CheckSourceRepo(pi); var assembly = GetType().Assembly; - Version = assembly.GetName().Version?.ToString() ?? string.Empty; - CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + Version = assembly.GetName().Version?.ToString() ?? string.Empty; + CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + GameVersion = GetGameVersion(); } + private static unsafe string GetGameVersion() + => Framework.Instance()->GameVersion[0]; + public void LogExceptions() { if (ImcExceptions.Count > 0) - Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", + NotificationType.Warning); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 3d32d267..78014054 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -13,6 +13,9 @@ public static class CrashDataExtensions { ImGui.TextUnformatted(nameof(data.Mode)); ImGui.TextUnformatted(nameof(data.CrashTime)); + ImGui.TextUnformatted("Current Age"); + ImGui.TextUnformatted(nameof(data.Version)); + ImGui.TextUnformatted(nameof(data.GameVersion)); ImGui.TextUnformatted(nameof(data.ExitCode)); ImGui.TextUnformatted(nameof(data.ProcessId)); ImGui.TextUnformatted(nameof(data.TotalModdedFilesLoaded)); @@ -25,6 +28,9 @@ public static class CrashDataExtensions { ImGui.TextUnformatted(data.Mode); ImGui.TextUnformatted(data.CrashTime.ToString()); + ImGui.TextUnformatted((DateTimeOffset.UtcNow - data.CrashTime).ToString(@"dd\.hh\:mm\:ss")); + ImGui.TextUnformatted(data.Version); + ImGui.TextUnformatted(data.GameVersion); ImGui.TextUnformatted(data.ExitCode.ToString()); ImGui.TextUnformatted(data.ProcessId.ToString()); ImGui.TextUnformatted(data.TotalModdedFilesLoaded.ToString()); From ceb3bbc5c37f1f4d929d36cd6e11e4eac8bd0d1d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 13:34:49 +0100 Subject: [PATCH 1552/2451] Fix issue with compressed file sizes. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1a187f75..1be9365d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1a187f756f2e8823197bd43db1c3383231f5eaff +Subproject commit 1be9365d048bf1da3700e8cf1df9acbe42523f5c From 038c230427f6bf8c7bd2283939b343fcc60c6d4e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 13:59:40 +0100 Subject: [PATCH 1553/2451] Rename to Predefined. --- Penumbra/Services/FilenameService.cs | 6 +- Penumbra/Services/ServiceManagerA.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 8 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 42 ++++------ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 83 ++++++++----------- ...dTagManager.cs => PredefinedTagManager.cs} | 71 ++++++++-------- Penumbra/UI/Tabs/SettingsTab.cs | 22 +++-- 7 files changed, 102 insertions(+), 132 deletions(-) rename Penumbra/UI/{SharedTagManager.cs => PredefinedTagManager.cs} (83%) diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 20794f12..2de4bff0 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -14,7 +14,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService public readonly string EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json"); public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json"); public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); - public readonly string SharedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "shared_tags.json"); + public readonly string PredefinedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "predefined_tags.json"); public readonly string CrashHandlerExe = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe"); @@ -44,7 +44,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService get { var directory = new DirectoryInfo(CollectionDirectory); - return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + return directory.Exists ? directory.EnumerateFiles("*.json") : []; } } @@ -54,7 +54,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService get { var directory = new DirectoryInfo(LocalDataDirectory); - return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + return directory.Exists ? directory.EnumerateFiles("*.json") : []; } } diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index cb2032a2..39ef0560 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -105,7 +105,7 @@ public static class ServiceManagerA private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 50096696..4d0c62af 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -28,8 +28,8 @@ public enum ColorId ResTreePlayer, ResTreeNetworked, ResTreeNonNetworked, - SharedTagAdd, - SharedTagRemove + PredefinedTagAdd, + PredefinedTagRemove, } public static class Colors @@ -75,8 +75,8 @@ public static class Colors ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), - ColorId.SharedTagAdd => ( 0xFF44AA44, "Shared Tags: Add Tag", "A shared tag that is not present on the current mod and can be added." ), - ColorId.SharedTagRemove => ( 0xFF2222AA, "Shared Tags: Remove Tag", "A shared tag that is already present on the current mod and can be removed." ), + ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), + ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 5f2687c3..4c5e68ff 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -7,22 +7,15 @@ using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class ModPanelDescriptionTab : ITab +public class ModPanelDescriptionTab( + ModFileSystemSelector selector, + TutorialService tutorial, + ModManager modManager, + PredefinedTagManager predefinedTagsConfig) + : ITab { - private readonly ModFileSystemSelector _selector; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private readonly SharedTagManager _sharedTagManager; - private readonly TagButtons _localTags = new(); - private readonly TagButtons _modTags = new(); - - public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager, SharedTagManager sharedTagsConfig) - { - _selector = selector; - _tutorial = tutorial; - _modManager = modManager; - _sharedTagManager = sharedTagsConfig; - } + private readonly TagButtons _localTags = new(); + private readonly TagButtons _modTags = new(); public ReadOnlySpan Label => "Description"u8; @@ -36,29 +29,28 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagsConfig.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" - + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _selector.Selected!.LocalTags, + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); - _tutorial.OpenTutorial(BasicTutorialSteps.Tags); + tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) - _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag); + modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); if (sharedTagsEnabled) - { - _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true, _selector.Selected!); - } + predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, + selector.Selected!); - if (_selector.Selected!.ModTags.Count > 0) + if (selector.Selected!.ModTags.Count > 0) _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", - _selector.Selected!.ModTags, out var _, false, + selector.Selected!.ModTags, out _, false, ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Separator(); - ImGuiUtil.TextWrapped(_selector.Selected!.Description); + ImGuiUtil.TextWrapped(selector.Selected!.Description); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 9b4a582f..275c89ef 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -17,18 +17,20 @@ using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; -public class ModPanelEditTab : ITab +public class ModPanelEditTab( + ModManager modManager, + ModFileSystemSelector selector, + ModFileSystem fileSystem, + Services.MessageService messager, + ModEditWindow editWindow, + ModEditor editor, + FilenameService filenames, + ModExportManager modExportManager, + Configuration config, + PredefinedTagManager predefinedTagManager) + : ITab { - private readonly Services.MessageService _messager; - private readonly FilenameService _filenames; - private readonly ModManager _modManager; - private readonly ModExportManager _modExportManager; - private readonly ModFileSystem _fileSystem; - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly SharedTagManager _sharedTagManager; + private readonly ModManager _modManager = modManager; private readonly TagButtons _modTags = new(); @@ -37,22 +39,6 @@ public class ModPanelEditTab : ITab private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; - public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, - SharedTagManager sharedTagManager) - { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _messager = messager; - _editWindow = editWindow; - _editor = editor; - _filenames = filenames; - _modExportManager = modExportManager; - _config = config; - _sharedTagManager = sharedTagManager; - } - public ReadOnlySpan Label => "Edit Mod"u8; @@ -62,8 +48,8 @@ public class ModPanelEditTab : ITab if (!child) return; - _leaf = _selector.SelectedLeaf!; - _mod = _selector.Selected!; + _leaf = selector.SelectedLeaf!; + _mod = selector.Selected!; _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; @@ -75,15 +61,15 @@ public class ModPanelEditTab : ITab if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X)) try { - _fileSystem.RenameAndMove(_leaf, newPath); + fileSystem.RenameAndMove(_leaf, newPath); } catch (Exception e) { - _messager.NotificationMessage(e.Message, NotificationType.Warning, false); + messager.NotificationMessage(e.Message, NotificationType.Warning, false); } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); @@ -91,12 +77,11 @@ public class ModPanelEditTab : ITab _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); if (sharedTagsEnabled) - { - _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false, _selector.Selected!); - } + predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, + selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(_filenames, _modManager, _mod, _config.ReplaceNonAsciiOnImport); + AddOptionGroup.Draw(filenames, _modManager, _mod, config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) @@ -144,11 +129,11 @@ public class ModPanelEditTab : ITab { if (ImGui.Button("Update Bibo Material", buttonSize)) { - _editor.LoadMod(_mod); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); - _editor.MdlMaterialEditor.SaveAllModels(_editor.Compactor); - _editWindow.UpdateModels(); + editor.LoadMod(_mod); + editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); + editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); + editor.MdlMaterialEditor.SaveAllModels(editor.Compactor); + editWindow.UpdateModels(); } ImGuiUtil.HoverTooltip( @@ -160,7 +145,7 @@ public class ModPanelEditTab : ITab private void BackupButtons(Vector2 buttonSize) { - var backup = new ModBackup(_modExportManager, _mod); + var backup = new ModBackup(modExportManager, _mod); var tt = ModBackup.CreatingBackup ? "Already exporting a mod." : backup.Exists @@ -171,16 +156,16 @@ public class ModPanelEditTab : ITab ImGui.SameLine(); tt = backup.Exists - ? $"Delete existing mod export \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." + ? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; - if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) + if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Delete(); tt = backup.Exists - ? $"Restore mod from exported file \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." + ? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) + if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Restore(_modManager); if (backup.Exists) { @@ -218,13 +203,13 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); ImGui.SameLine(); - var fileExists = File.Exists(_filenames.ModMetaPath(_mod)); + var fileExists = File.Exists(filenames.ModMetaPath(_mod)); var tt = fileExists ? "Open the metadata json file in the text editor of your choice." : "The metadata json file does not exist."; if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(_filenames.ModMetaPath(_mod)) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } /// Do some edits outside of iterations. @@ -448,7 +433,7 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); ImGui.SameLine(); - var fileName = _filenames.OptionGroupFile(_mod, groupIdx, _config.ReplaceNonAsciiOnImport); + var fileName = filenames.OptionGroupFile(_mod, groupIdx, config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs similarity index 83% rename from Penumbra/UI/SharedTagManager.cs rename to Penumbra/UI/PredefinedTagManager.cs index 23196319..fafca101 100644 --- a/Penumbra/UI/SharedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,6 +1,5 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Utility; using ImGuiNET; using Newtonsoft.Json; using OtterGui; @@ -12,44 +11,45 @@ using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class SharedTagManager : ISavable + +public sealed class PredefinedTagManager : ISavable { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly SaveService _saveService; - private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); - private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); + private static uint _tagButtonAddColor = ColorId.PredefinedTagAdd.Value(); + private static uint _tagButtonRemoveColor = ColorId.PredefinedTagRemove.Value(); private static float _minTagButtonWidth = 15; private const string PopupContext = "SharedTagsPopup"; - private bool _isPopupOpen = false; + private bool _isPopupOpen = false; // Operations on this list assume that it is sorted and will keep it sorted if that is the case. // The list also gets re-sorted when first loaded from config in case the config was modified. [JsonRequired] private readonly List _sharedTags = []; + [JsonIgnore] - public IReadOnlyList SharedTags => _sharedTags; + public IReadOnlyList SharedTags + => _sharedTags; public int ConfigVersion = 1; - public SharedTagManager(ModManager modManager, SaveService saveService) + public PredefinedTagManager(ModManager modManager, SaveService saveService) { - _modManager = modManager; + _modManager = modManager; _saveService = saveService; Load(); } public string ToFilename(FilenameService fileNames) - { - return fileNames.SharedTagFile; - } + => fileNames.PredefinedTagFile; public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } @@ -65,12 +65,12 @@ public sealed class SharedTagManager : ISavable errorArgs.ErrorContext.Handled = true; } - if (!File.Exists(_saveService.FileNames.SharedTagFile)) + if (!File.Exists(_saveService.FileNames.PredefinedTagFile)) return; try { - var text = File.ReadAllText(_saveService.FileNames.SharedTagFile); + var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, @@ -94,9 +94,7 @@ public sealed class SharedTagManager : ISavable // In the case of editing a tag, remove what's there prior to doing an insert. if (tagIdx != SharedTags.Count) - { _sharedTags.RemoveAt(tagIdx); - } if (!string.IsNullOrEmpty(tag)) { @@ -109,7 +107,8 @@ public sealed class SharedTagManager : ISavable Save(); } - public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) + public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, + Mods.Mod mod) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); @@ -131,7 +130,8 @@ public sealed class SharedTagManager : ISavable { _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); } - } else + } + else { if (index < 0) { @@ -143,15 +143,16 @@ public sealed class SharedTagManager : ISavable _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); } } - } } public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { var tagToAdd = string.Empty; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", - false, true) || _isPopupOpen) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Add Shared Tag... (Right-click to close popup)", + false, true) + || _isPopupOpen) return DrawSharedTagsPopup(localTags, modTags, editLocal); return tagToAdd; @@ -167,9 +168,9 @@ public sealed class SharedTagManager : ISavable } var display = ImGui.GetIO().DisplaySize; - var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 6; - var size = new Vector2(width, height); + var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 6; + var size = new Vector2(width, height); ImGui.SetNextWindowSize(size); using var popup = ImRaii.Popup(PopupContext); if (!popup) @@ -182,26 +183,23 @@ public sealed class SharedTagManager : ISavable foreach (var (tag, idx) in SharedTags.WithIndex()) { if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) - { selected = tag; - } ImGui.SameLine(); } if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) - { _isPopupOpen = false; - } return selected; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal, int index) + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, + bool editLocal, int index) { var ret = false; var isLocalTagPresent = localTags.Contains(buttonLabel); - var isModTagPresent = modTags.Contains(buttonLabel); + var isModTagPresent = modTags.Contains(buttonLabel); var buttonWidth = CalcTextButtonWidth(buttonLabel); // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. @@ -210,7 +208,7 @@ public sealed class SharedTagManager : ISavable if (buttonWidth >= maxButtonWidth) { displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); - buttonWidth = CalcTextButtonWidth(displayedLabel); + buttonWidth = CalcTextButtonWidth(displayedLabel); } // Prevent adding a new tag past the right edge of the popup @@ -247,9 +245,8 @@ public sealed class SharedTagManager : ISavable // An ellipsis will be used to indicate trimmed tags if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) - { return nextTrim + "..."; - } + trimmedText = nextTrim; } @@ -257,7 +254,5 @@ public sealed class SharedTagManager : ISavable } private static float CalcTextButtonWidth(string text) - { - return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; - } + => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index b311bb93..c36c63b2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -42,7 +42,7 @@ public class SettingsTab : ITab private readonly DalamudConfigService _dalamudConfig; private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; - private readonly SharedTagManager _sharedTagManager; + private readonly PredefinedTagManager _predefinedTagManager; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -53,7 +53,7 @@ public class SettingsTab : ITab Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, SharedTagManager sharedTagConfig) + IDataManager gameData, PredefinedTagManager predefinedTagConfig) { _pluginInterface = pluginInterface; _config = config; @@ -73,7 +73,7 @@ public class SettingsTab : ITab _gameData = gameData; if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; - _sharedTagManager = sharedTagConfig; + _predefinedTagManager = predefinedTagConfig; } public void DrawHeader() @@ -101,7 +101,7 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); DrawAdvancedSettings(); - DrawSharedTagsSection(); + DrawPredefinedTagsSection(); DrawSupportButtons(); } @@ -239,7 +239,7 @@ public class SettingsTab : ITab } var selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); ImGui.SameLine(); DrawDirectoryPickerButton(); style.Pop(); @@ -388,7 +388,7 @@ public class SettingsTab : ITab "Hide the Penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, v => { - _config.HideUiWhenUiHidden = v; + _config.HideUiWhenUiHidden = v; _pluginInterface.UiBuilder.DisableUserUiHide = !v; }); Checkbox("Hide Config Window when in Cutscenes", @@ -917,18 +917,16 @@ public class SettingsTab : ITab _penumbra.ForceChangelogOpen(); } - private void DrawSharedTagsSection() + private void DrawPredefinedTagsSection() { if (!ImGui.CollapsingHeader("Tags")) return; - var tagIdx = _sharedTags.Draw("Shared Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _sharedTagManager.SharedTags, + var tagIdx = _sharedTags.Draw("Predefined Tags: ", + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.SharedTags, out var editedTag); if (tagIdx >= 0) - { - _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); - } + _predefinedTagManager.ChangeSharedTag(tagIdx, editedTag); } } From 814aa92e19e7a971ef12c2950c0cff023d3e0b8d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 14:08:14 +0100 Subject: [PATCH 1554/2451] Move tags before advanced. --- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c36c63b2..60c18d5f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -100,8 +100,8 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); - DrawAdvancedSettings(); DrawPredefinedTagsSection(); + DrawAdvancedSettings(); DrawSupportButtons(); } From 19526dd92d4361fe7eebbeb1bc6fe0844be7a8c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 14:09:01 +0100 Subject: [PATCH 1555/2451] Help marker position. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1be9365d..cf42043c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1be9365d048bf1da3700e8cf1df9acbe42523f5c +Subproject commit cf42043c2b0e76b59919688dc250a762fe52d4b1 From 0ec440c388e0667e73069fa0cc8ae2216e7972a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Mar 2024 16:41:40 +0100 Subject: [PATCH 1556/2451] Fix reloading mods not fixing settings, add some messages on IPC. --- Penumbra/Api/PenumbraApi.cs | 93 +++++++++++++++---- .../Collections/Manager/CollectionStorage.cs | 8 ++ Penumbra/Mods/Subclasses/ModSettings.cs | 16 ++++ Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 9 +- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index aed1a963..f5bb67bd 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -10,6 +10,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Compression; +using OtterGui.Log; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.Interop.ResourceLoading; @@ -642,10 +643,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; + return Return(PenumbraApiEc.ModMissing, Args("ModDirectory", modDirectory, "ModName", modName)); _modManager.ReloadMod(mod); - return PenumbraApiEc.Success; + return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); } public PenumbraApiEc InstallMod(string modFilePackagePath) @@ -653,11 +654,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (File.Exists(modFilePackagePath)) { _modImportManager.AddUnpack(modFilePackagePath); - return PenumbraApiEc.Success; + return Return(PenumbraApiEc.Success, Args("ModFilePackagePath", modFilePackagePath)); } else { - return PenumbraApiEc.FileMissing; + return Return(PenumbraApiEc.FileMissing, Args("ModFilePackagePath", modFilePackagePath)); } } @@ -666,23 +667,24 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); if (!dir.Exists) - return PenumbraApiEc.FileMissing; + return Return(PenumbraApiEc.FileMissing, Args("ModDirectory", modDirectory)); + _modManager.AddMod(dir); if (_config.UseFileSystemCompression) new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); - return PenumbraApiEc.Success; + return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory)); } public PenumbraApiEc DeleteMod(string modDirectory, string modName) { CheckInitialized(); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.NothingChanged; + return Return(PenumbraApiEc.NothingChanged, Args("ModDirectory", modDirectory, "ModName", modName)); _modManager.DeleteMod(mod); - return PenumbraApiEc.Success; + return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); } public event Action? ModDeleted; @@ -784,22 +786,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; + return Return(PenumbraApiEc.CollectionMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "OptionName", optionName)); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; + return Return(PenumbraApiEc.ModMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "OptionName", optionName)); var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); if (groupIdx < 0) - return PenumbraApiEc.OptionGroupMissing; + return Return(PenumbraApiEc.OptionGroupMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "OptionName", optionName)); var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); if (optionIdx < 0) - return PenumbraApiEc.OptionMissing; + return Return(PenumbraApiEc.OptionMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "OptionName", optionName)); var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; - return _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return Return( + _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "OptionName", optionName)); } public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, @@ -807,14 +820,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; + return Return(PenumbraApiEc.CollectionMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "#optionNames", optionNames.Count.ToString())); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; + return Return(PenumbraApiEc.ModMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "#optionNames", optionNames.Count.ToString())); var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); if (groupIdx < 0) - return PenumbraApiEc.OptionGroupMissing; + return Return(PenumbraApiEc.OptionGroupMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "#optionNames", optionNames.Count.ToString())); var group = mod.Groups[groupIdx]; @@ -823,7 +842,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi { var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) - return PenumbraApiEc.OptionMissing; + return Return(PenumbraApiEc.OptionMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "#optionNames", optionNames.Count.ToString())); setting = (uint)optionIdx; } @@ -833,13 +854,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi { var optionIdx = group.IndexOf(o => o.Name == name); if (optionIdx < 0) - return PenumbraApiEc.OptionMissing; + return Return(PenumbraApiEc.OptionMissing, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", + optionGroupName, "#optionNames", optionNames.Count.ToString())); setting |= 1u << optionIdx; } } - return _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return Return( + _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, + Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, + "#optionNames", optionNames.Count.ToString())); } @@ -1296,4 +1322,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static LazyString Args(params string[] arguments) + { + if (arguments.Length == 0) + return new LazyString(() => "no arguments"); + + return new LazyString(() => + { + var sb = new StringBuilder(); + for (var i = 0; i < arguments.Length / 2; ++i) + { + sb.Append(arguments[2 * i]); + sb.Append(" = "); + sb.Append(arguments[2 * i + 1]); + sb.Append(", "); + } + + return sb.ToString(0, sb.Length - 2); + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") + { + Penumbra.Log.Debug( + $"[{name}] Called with {args}, returned {ec}."); + return ec; + } } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index a84c79e6..0ee55376 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -265,6 +265,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; + case ModPathChangeType.Reloaded: + foreach (var collection in this) + { + if (collection.Settings[mod.Index]?.FixAllSettings(mod) ?? false) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + + break; } } diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index a20cb9cb..ed8ad84e 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -138,6 +138,22 @@ public class ModSettings } } + public bool FixAllSettings(Mod mod) + { + var ret = false; + for (var i = 0; i < Settings.Count; ++i) + { + var newValue = FixSetting(mod.Groups[i], Settings[i]); + if (newValue != Settings[i]) + { + ret = true; + Settings[i] = newValue; + } + } + + return AddMissingSettings(mod) || ret; + } + // Ensure that a value is valid for a group. private static uint FixSetting(IModGroup group, uint value) => group.Type switch diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 195c07d6..5c7ddbf3 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -211,6 +211,11 @@ public class ModPanelSettingsTab : ITab var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; var minWidth = Widget.BeginFramedGroup(group.Name, description:group.Description); + DrawCollapseHandling(group, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + return; + void DrawOptions() { for (var idx = 0; idx < group.Count; ++idx) @@ -227,10 +232,6 @@ public class ModPanelSettingsTab : ITab ImGuiComponents.HelpMarker(option.Description); } } - - DrawCollapseHandling(group, minWidth, DrawOptions); - - Widget.EndFramedGroup(); } From 9f7b95746dff2f83a82e810276bf48f9f3f93a2f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Mar 2024 16:50:26 +0100 Subject: [PATCH 1557/2451] Use default task scheduler. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Collections/Manager/IndividualCollections.Files.cs | 2 +- Penumbra/Import/Models/ModelManager.cs | 4 ++-- Penumbra/Import/TexToolsImport.cs | 4 ++-- Penumbra/Import/Textures/TextureManager.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 2 +- Penumbra/Services/ServiceWrapper.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 6 +++--- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/OtterGui b/OtterGui index 1be9365d..5a2e12a1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1be9365d048bf1da3700e8cf1df9acbe42523f5c +Subproject commit 5a2e12a1acd6760a3a592447a92215135e79197c diff --git a/Penumbra.GameData b/Penumbra.GameData index c0c7eb0d..c39f683d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319 +Subproject commit c39f683d65d4541e9f97ed4ea1abcb10e8ca5690 diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index dc20da1e..21a8cf8a 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -41,7 +41,7 @@ public partial class IndividualCollections saver.ImmediateSave(parent); IsLoaded = true; Loaded.Invoke(); - }); + }, TaskScheduler.Default); return false; } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 2c341c8b..1a52c4dd 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -162,7 +162,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect { return _tasks.TryRemove(a, out var unused); } - }, CancellationToken.None); + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); return (t, token); }).Item1; } @@ -178,7 +178,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect throw task.Exception; return process(action); - }); + }, TaskScheduler.Default); private class ExportToGltfAction( ModelManager manager, diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 3f3304b8..bb006d8d 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -46,12 +46,12 @@ public partial class TexToolsImporter : IDisposable ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); _token = _cancellation.Token; Task.Run(ImportFiles, _token) - .ContinueWith(_ => CloseStreams()) + .ContinueWith(_ => CloseStreams(), TaskScheduler.Default) .ContinueWith(_ => { foreach (var (file, dir, error) in ExtractedMods) handler(file, dir, error); - }); + }, TaskScheduler.Default); } private void CloseStreams() diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 5653d760..976bc179 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -64,7 +64,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable { var token = new CancellationTokenSource(); var task = Enqueue(a, token.Token); - task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None); + task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); return (task, token); }).Item1; } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index a95567ce..99ad1a4f 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -23,7 +23,7 @@ public class ModCacheManager : IDisposable _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager); _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager); _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager); - identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation()); + identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation(), TaskScheduler.Default); OnModDiscoveryFinished(); } diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 37acdfd0..e321b35c 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -74,7 +74,7 @@ public abstract class AsyncServiceWrapper : IDisposable { if (!_isDisposed) FinishedCreation?.Invoke(); - }, null); + }, TaskScheduler.Default); } public void Dispose() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 637c8401..cca8fe10 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -92,7 +92,7 @@ public partial class ModEditWindow .ToList(); }); - task.ContinueWith(t => { GamePaths = FinalizeIo(t); }); + task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default); } private EstManipulation[] GetCurrentEstManipulations() @@ -130,7 +130,7 @@ public partial class ModEditWindow BeginIo(); _edit._models.ExportToGltf(ExportConfig, Mdl, sklbPaths, ReadFile, outputPath) - .ContinueWith(FinalizeIo); + .ContinueWith(FinalizeIo, TaskScheduler.Default); } /// Import a model from an interchange format. @@ -144,7 +144,7 @@ public partial class ModEditWindow var mdlFile = FinalizeIo(task, result => result.Item1, result => result.Item2); if (mdlFile != null) FinalizeImport(mdlFile); - }); + }, TaskScheduler.Default); } /// Finalise the import of a .mdl, applying any post-import transformations and state updates. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 71c64059..652ecb49 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -255,7 +255,7 @@ public partial class ModEditWindow return; _framework.RunOnFrameworkThread(() => tex.Reload(_textures)); - }); + }, TaskScheduler.Default); } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index afa846b5..72dd91d3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -326,7 +326,7 @@ public partial class ModEditWindow : Window, IDisposable else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { _editor.ModNormalizer.Normalize(Mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx)); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx), TaskScheduler.Default); } if (!_editor.Duplicates.Worker.IsCompleted) diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index a0e35cff..fd8f9b25 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -33,7 +33,7 @@ public class IndividualAssignmentUi : IDisposable _actors = actors; _collectionManager = collectionManager; _communicator.CollectionChange.Subscribe(UpdateIdentifiers, CollectionChange.Priority.IndividualAssignmentUi); - _actors.Awaiter.ContinueWith(_ => SetupCombos()); + _actors.Awaiter.ContinueWith(_ => SetupCombos(), TaskScheduler.Default); } public string PlayerTooltip { get; private set; } = NewPlayerTooltipEmpty; From 0f89e243779334d3c916698290e9157fd787775c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Mar 2024 17:12:30 +0100 Subject: [PATCH 1558/2451] Improve tag button positioning. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 9 +++++---- Penumbra/UI/PredefinedTagManager.cs | 4 +--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/OtterGui b/OtterGui index 5a2e12a1..c59b1c09 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5a2e12a1acd6760a3a592447a92215135e79197c +Subproject commit c59b1c09ff7b8093d3a70c45957f9c41341dd3a4 diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 4c5e68ff..e1b80b23 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,17 +29,18 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = predefinedTagsConfig.SharedTags.Count > 0; - var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.SharedTags.Count > 0 + ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) + : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, - out var editedTag, rightEndOffset: sharedTagButtonOffset); + out var editedTag, rightEndOffset: predefinedTagButtonOffset); tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); - if (sharedTagsEnabled) + if (predefinedTagsEnabled) predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, selector.Selected!); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index fafca101..b85b5dea 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -110,9 +110,7 @@ public sealed class PredefinedTagManager : ISavable public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - + ImGui.SameLine(ImGui.GetContentRegionMax().X - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.X); var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); if (sharedTag.Length > 0) From 05b7234748ddc2cbcb7d5cfe329cd7a39e50276b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 17:35:05 +0100 Subject: [PATCH 1559/2451] Update to .net8. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Import/Models/Export/MaterialExporter.cs | 3 +-- Penumbra/Penumbra.csproj | 2 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/Services/MessageService.cs | 3 ++- Penumbra/Services/ServiceManagerA.cs | 3 ++- Penumbra/UI/TutorialService.cs | 2 +- Penumbra/packages.lock.json | 2 +- 12 files changed, 14 insertions(+), 13 deletions(-) diff --git a/OtterGui b/OtterGui index 5a2e12a1..b4b14367 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5a2e12a1acd6760a3a592447a92215135e79197c +Subproject commit b4b14367d8235eabedd561ad3626beb1d2a83889 diff --git a/Penumbra.Api b/Penumbra.Api index 34921fd2..1df06807 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 34921fd2c5a9aff5d34aef664bdb78331e8b9436 +Subproject commit 1df06807650a79813791effaa01fb7c4710b3dab diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index ea61b968..c9f97fde 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -2,7 +2,7 @@ Exe - net7.0-windows + net8.0-windows preview enable x64 diff --git a/Penumbra.GameData b/Penumbra.GameData index c39f683d..33b51274 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c39f683d65d4541e9f97ed4ea1abcb10e8ca5690 +Subproject commit 33b512746e80b7b1276b644430923eee9bec9fba diff --git a/Penumbra.String b/Penumbra.String index 620a7edf..14e00f77 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 620a7edf009b92288257ce7d64fffb8fba44d8b5 +Subproject commit 14e00f77d42bc677e02325660db765ef11932560 diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 82e13eb9..2fa4e1b2 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -144,8 +144,7 @@ public class MaterialExporter // Specular (table) var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - // float.Lerp is .NET8 ;-; #TODO - var lerpedSpecularFactor = prevRow.SpecularStrength * (1.0f - tableRow.Weight) + nextRow.SpecularStrength * tableRow.Weight; + var lerpedSpecularFactor = float.Lerp(prevRow.SpecularStrength, nextRow.SpecularStrength, tableRow.Weight); specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); // Emissive (table) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 796eae01..df18ff13 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,6 +1,6 @@ - net7.0-windows + net8.0-windows preview x64 Penumbra diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 49b4cb42..c1b067d0 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -16,7 +16,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); public readonly string CrashHandlerExe = - Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe"); + Path.Combine(pi.AssemblyLocation.DirectoryName!, "Penumbra.CrashHandler.exe"); public readonly string LogFileName = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(pi.ConfigDirectory.FullName)!)!, "Penumbra.log"); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index daad29ef..0a85a569 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -9,7 +9,8 @@ using OtterGui.Services; namespace Penumbra.Services; -public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) : OtterGui.Classes.MessageService(log, uiBuilder, chat), IService +public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat, INotificationManager notificationManager) + : OtterGui.Classes.MessageService(log, uiBuilder, chat, notificationManager), IService { public void LinkItem(Item item) { diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index 191d8d11..a5de33bb 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -81,7 +81,8 @@ public static class ServiceManagerA .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) - .AddDalamudService(pi); + .AddDalamudService(pi) + .AddDalamudService(pi); private static ServiceManager AddInterop(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 6c6b0612..d87df19e 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -108,7 +108,7 @@ public class TutorialService .Register("Initial Setup, Step 8: Mod Import", "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" - + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") // TODO + + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") .Register("Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" + "Import and select a mod now to continue.") .Register("Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here.") diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index d0e8fa1a..3f795631 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net7.0-windows7.0": { + "net8.0-windows7.0": { "EmbedIO": { "type": "Direct", "requested": "[3.4.3, )", From 02f2bf1bc18fd9362f6e765a424a23797f2442f3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 17:38:02 +0100 Subject: [PATCH 1560/2451] Update actions. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd1d45b..b40b2538 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.x.x' + dotnet-version: '8.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3afe9c1..7c9e2909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.x.x' + dotnet-version: '8.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 0968430d..91361646 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.x.x' + dotnet-version: '8.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From a6e08a18652499caf5ede1a010b34e2ac7da4073 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 18:30:00 +0100 Subject: [PATCH 1561/2451] Rename ServiceManagerA. --- Penumbra/Penumbra.cs | 2 +- .../{ServiceManagerA.cs => StaticServiceManager.cs} | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) rename Penumbra/Services/{ServiceManagerA.cs => StaticServiceManager.cs} (97%) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b532339f..ff068928 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -51,7 +51,7 @@ public class Penumbra : IDalamudPlugin { try { - _services = ServiceManagerA.CreateProvider(this, pluginInterface, Log); + _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); Messager = _services.GetService(); _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/StaticServiceManager.cs similarity index 97% rename from Penumbra/Services/ServiceManagerA.cs rename to Penumbra/Services/StaticServiceManager.cs index a5de33bb..66c90e84 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -6,7 +6,6 @@ using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; -using OtterGui.Compression; using OtterGui.Log; using OtterGui.Services; using Penumbra.Api; @@ -14,14 +13,12 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Import.Models; -using Penumbra.GameData.DataContainers; using Penumbra.GameData.Structs; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -38,7 +35,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage namespace Penumbra.Services; -public static class ServiceManagerA +public static class StaticServiceManager { public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { @@ -105,7 +102,8 @@ public static class ServiceManagerA private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() From fe6e1edecc7877d99ed2a467601310a420d1575f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 18:31:02 +0100 Subject: [PATCH 1562/2451] Fix a game object table warning. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 33b51274..d53db6a3 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 33b512746e80b7b1276b644430923eee9bec9fba +Subproject commit d53db6a358cedecd3ef18f62f12a07deff4b61ee From 52c1708dd270ec46a3de242d57a42116a9de094c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 20:45:16 +0100 Subject: [PATCH 1563/2451] Change predefined tag handling. --- Penumbra/Mods/Mod.cs | 6 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 210 ++++++------------ Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 65 +++--- 6 files changed, 112 insertions(+), 175 deletions(-) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index a9ef22cb..c5e671af 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -45,19 +45,19 @@ public sealed class Mod : IMod public string Description { get; internal set; } = string.Empty; public string Version { get; internal set; } = string.Empty; public string Website { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); + public IReadOnlyList ModTags { get; internal set; } = []; // Local Data public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public IReadOnlyList LocalTags { get; internal set; } = Array.Empty(); + public IReadOnlyList LocalTags { get; internal set; } = []; public string Note { get; internal set; } = string.Empty; public bool Favorite { get; internal set; } = false; // Options public readonly SubMod Default; - public readonly List Groups = new(); + public readonly List Groups = []; ISubMod IMod.Default => Default; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index e1b80b23..4ad30a6f 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,7 +29,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.SharedTags.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.PredefinedTags.Count > 0 ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 275c89ef..a0e32c22 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.PredefinedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index b85b5dea..63be42de 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -17,22 +17,19 @@ public sealed class PredefinedTagManager : ISavable private readonly ModManager _modManager; private readonly SaveService _saveService; - private static uint _tagButtonAddColor = ColorId.PredefinedTagAdd.Value(); - private static uint _tagButtonRemoveColor = ColorId.PredefinedTagRemove.Value(); + private bool _isListOpen = false; + private uint _enabledColor; + private uint _disabledColor; - private static float _minTagButtonWidth = 15; - - private const string PopupContext = "SharedTagsPopup"; - private bool _isPopupOpen = false; // Operations on this list assume that it is sorted and will keep it sorted if that is the case. // The list also gets re-sorted when first loaded from config in case the config was modified. [JsonRequired] - private readonly List _sharedTags = []; + private readonly List _predefinedTags = []; [JsonIgnore] - public IReadOnlyList SharedTags - => _sharedTags; + public IReadOnlyList PredefinedTags + => _predefinedTags; public int ConfigVersion = 1; @@ -48,8 +45,9 @@ public sealed class PredefinedTagManager : ISavable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } @@ -58,13 +56,6 @@ public sealed class PredefinedTagManager : ISavable private void Load() { - static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) - { - Penumbra.Log.Error( - $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); - errorArgs.ErrorContext.Handled = true; - } - if (!File.Exists(_saveService.FileNames.PredefinedTagFile)) return; @@ -77,7 +68,7 @@ public sealed class PredefinedTagManager : ISavable }); // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. - _sharedTags.Sort(); + _predefinedTags.Sort(); } catch (Exception ex) { @@ -85,23 +76,32 @@ public sealed class PredefinedTagManager : ISavable "Error reading shared tags Configuration, reverting to default.", "Error reading shared tags Configuration", NotificationType.Error); } + + return; + + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Penumbra.Log.Error( + $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } } public void ChangeSharedTag(int tagIdx, string tag) { - if (tagIdx < 0 || tagIdx > SharedTags.Count) + if (tagIdx < 0 || tagIdx > PredefinedTags.Count) return; // In the case of editing a tag, remove what's there prior to doing an insert. - if (tagIdx != SharedTags.Count) - _sharedTags.RemoveAt(tagIdx); + if (tagIdx != PredefinedTags.Count) + _predefinedTags.RemoveAt(tagIdx); if (!string.IsNullOrEmpty(tag)) { // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. - var existingIdx = _sharedTags.BinarySearch(tag); + var existingIdx = _predefinedTags.BinarySearch(tag); if (existingIdx < 0) - _sharedTags.Insert(~existingIdx, tag); + _predefinedTags.Insert(~existingIdx, tag); } Save(); @@ -110,147 +110,83 @@ public sealed class PredefinedTagManager : ISavable public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) { - ImGui.SameLine(ImGui.GetContentRegionMax().X - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.X); - var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); + DrawToggleButton(); + if (!DrawList(localTags, modTags, editLocal, out var changedTag, out var index)) + return; - if (sharedTag.Length > 0) - { - var index = editLocal ? mod.LocalTags.IndexOf(sharedTag) : mod.ModTags.IndexOf(sharedTag); - - if (editLocal) - { - if (index < 0) - { - index = mod.LocalTags.Count; - _modManager.DataEditor.ChangeLocalTag(mod, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); - } - } - else - { - if (index < 0) - { - index = mod.ModTags.Count; - _modManager.DataEditor.ChangeModTag(mod, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); - } - } - } + if (editLocal) + _modManager.DataEditor.ChangeLocalTag(mod, index, changedTag); + else + _modManager.DataEditor.ChangeModTag(mod, index, changedTag); } - public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + private void DrawToggleButton() { - var tagToAdd = string.Empty; + ImGui.SameLine(ImGui.GetContentRegionMax().X + - ImGui.GetFrameHeight() + - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), _isListOpen); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Add Shared Tag... (Right-click to close popup)", - false, true) - || _isPopupOpen) - return DrawSharedTagsPopup(localTags, modTags, editLocal); - - return tagToAdd; + "Add Predefined Tags...", false, true)) + _isListOpen = !_isListOpen; } - private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + private bool DrawList(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, out string changedTag, + out int changedIndex) { - var selected = string.Empty; - if (!ImGui.IsPopupOpen(PopupContext)) - { - ImGui.OpenPopup(PopupContext); - _isPopupOpen = true; - } + changedTag = string.Empty; + changedIndex = -1; - var display = ImGui.GetIO().DisplaySize; - var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 6; - var size = new Vector2(width, height); - ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup(PopupContext); - if (!popup) - return selected; + if (!_isListOpen) + return false; - ImGui.TextUnformatted("Shared Tags"); - ImGuiUtil.HoverTooltip("Right-click to close popup"); + ImGui.TextUnformatted("Predefined Tags"); ImGui.Separator(); - foreach (var (tag, idx) in SharedTags.WithIndex()) + var ret = false; + _enabledColor = ColorId.PredefinedTagAdd.Value(); + _disabledColor = ColorId.PredefinedTagRemove.Value(); + var (edited, others) = editLocal ? (localTags, modTags) : (modTags, localTags); + foreach (var (tag, idx) in PredefinedTags.WithIndex()) { - if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) - selected = tag; + var tagIdx = edited.IndexOf(tag); + var inOther = tagIdx < 0 && others.IndexOf(tag) >= 0; + if (DrawColoredButton(tag, idx, tagIdx, inOther)) + { + (changedTag, changedIndex) = tagIdx >= 0 ? (string.Empty, tagIdx) : (tag, edited.Count); + ret = true; + } + ImGui.SameLine(); } - if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) - _isPopupOpen = false; - - return selected; + ImGui.NewLine(); + ImGui.Separator(); + return ret; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, - bool editLocal, int index) + private bool DrawColoredButton(string buttonLabel, int index, int tagIdx, bool inOther) { - var ret = false; - - var isLocalTagPresent = localTags.Contains(buttonLabel); - var isModTagPresent = modTags.Contains(buttonLabel); - - var buttonWidth = CalcTextButtonWidth(buttonLabel); - // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. - var maxButtonWidth = (ImGui.GetContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) * 0.5f - ImGui.GetStyle().ItemSpacing.X; - var displayedLabel = buttonLabel; - if (buttonWidth >= maxButtonWidth) - { - displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); - buttonWidth = CalcTextButtonWidth(displayedLabel); - } - + using var id = ImRaii.PushId(index); + var buttonWidth = CalcTextButtonWidth(buttonLabel); // Prevent adding a new tag past the right edge of the popup if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) ImGui.NewLine(); - // Trimmed tag names can collide, and while tag names are currently distinct this may not always be the case. As such use the index to avoid an ImGui moment. - using var id = ImRaii.PushId(index); + bool ret; + using (ImRaii.Disabled(inOther)) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, tagIdx >= 0 || inOther ? _disabledColor : _enabledColor); + ret = ImGui.Button(buttonLabel); + } + + if (inOther && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("This tag is already present in the other set of tags."); - if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) - { - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); - ImGui.Button(displayedLabel); - } - else - { - using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) - { - if (ImGui.Button(displayedLabel)) - ret = true; - } - } return ret; } - private static string TrimButtonTextToWidth(string fullText, float maxWidth) - { - var trimmedText = fullText; - - while (trimmedText.Length > _minTagButtonWidth) - { - var nextTrim = trimmedText.Substring(0, Math.Max(trimmedText.Length - 1, 0)); - - // An ellipsis will be used to indicate trimmed tags - if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) - return nextTrim + "..."; - - trimmedText = nextTrim; - } - - return trimmedText; - } - private static float CalcTextButtonWidth(string text) => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index d111c465..bb8856b3 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -110,7 +110,7 @@ public class ModsTab( var frameColor = ImGui.GetColorU32(ImGuiCol.FrameBg); using (var _ = ImRaii.Group()) { - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) { ImGuiUtil.DrawTextButton(FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor); ImGui.SameLine(); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 60c18d5f..9f8ffb38 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -227,37 +227,38 @@ public class SettingsTab : ITab if (_newModDirectory.IsNullOrEmpty()) _newModDirectory = _config.ModDirectory; - using var group = ImRaii.Group(); - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - bool save; - using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) - { - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) - .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); - save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, - RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + bool save, selected; + using (ImRaii.Group()) + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + + selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); } - var selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker(tt); - _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); - ImGui.SameLine(); - ImGui.TextUnformatted("Root Directory"); - ImGuiUtil.HoverTooltip(tt); - - group.Dispose(); _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); ImGui.SameLine(); var pos = ImGui.GetCursorPosX(); @@ -685,7 +686,7 @@ public class SettingsTab : ITab foreach (var color in Enum.GetValues()) { var (defaultColor, name, description) = color.Data(); - var currentColor = _config.Colors.TryGetValue(color, out var current) ? current : defaultColor; + var currentColor = _config.Colors.GetValueOrDefault(color, defaultColor); if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor)) _config.Save(); } @@ -871,7 +872,7 @@ public class SettingsTab : ITab if (!_dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool value)) { using var disabled = ImRaii.Disabled(); - Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { }); + Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, _ => { }); } else { @@ -923,7 +924,7 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Predefined Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.SharedTags, + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.PredefinedTags, out var editedTag); if (tagIdx >= 0) From 0e50a8a9e5155423ab8db2334a07861d83cf44db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 21:01:48 +0100 Subject: [PATCH 1564/2451] More future proof structure for tags. --- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 86 ++++++++++--------- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 4ad30a6f..ed6340ab 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,7 +29,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.PredefinedTags.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Count > 0 ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index a0e32c22..eb79869e 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.PredefinedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 63be42de..17e8432b 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -12,8 +13,13 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class PredefinedTagManager : ISavable +public sealed class PredefinedTagManager : ISavable, IReadOnlyList { + public const int Version = 1; + + public record struct TagData + { } + private readonly ModManager _modManager; private readonly SaveService _saveService; @@ -21,17 +27,7 @@ public sealed class PredefinedTagManager : ISavable private uint _enabledColor; private uint _disabledColor; - - // Operations on this list assume that it is sorted and will keep it sorted if that is the case. - // The list also gets re-sorted when first loaded from config in case the config was modified. - [JsonRequired] - private readonly List _predefinedTags = []; - - [JsonIgnore] - public IReadOnlyList PredefinedTags - => _predefinedTags; - - public int ConfigVersion = 1; + private readonly SortedList _predefinedTags = []; public PredefinedTagManager(ModManager modManager, SaveService saveService) { @@ -47,8 +43,12 @@ public sealed class PredefinedTagManager : ISavable { using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - serializer.Serialize(jWriter, this); + var jObj = new JObject() + { + ["Version"] = Version, + ["Tags"] = JObject.FromObject(_predefinedTags), + }; + jObj.WriteTo(jWriter); } public void Save() @@ -61,48 +61,38 @@ public sealed class PredefinedTagManager : ISavable try { - var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); - JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) { - Error = HandleDeserializationError, - }); - - // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. - _predefinedTags.Sort(); + case 1: + var tags = jObj["Tags"]?.ToObject>() ?? []; + foreach (var (tag, data) in tags) + _predefinedTags.TryAdd(tag, data); + break; + default: + throw new Exception($"Invalid version {version}."); + } } catch (Exception ex) { Penumbra.Messager.NotificationMessage(ex, - "Error reading shared tags Configuration, reverting to default.", - "Error reading shared tags Configuration", NotificationType.Error); - } - - return; - - static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) - { - Penumbra.Log.Error( - $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); - errorArgs.ErrorContext.Handled = true; + "Error reading predefined tags Configuration, reverting to default.", + "Error reading predefined tags Configuration", NotificationType.Error); } } public void ChangeSharedTag(int tagIdx, string tag) { - if (tagIdx < 0 || tagIdx > PredefinedTags.Count) + if (tagIdx < 0 || tagIdx > _predefinedTags.Count) return; - // In the case of editing a tag, remove what's there prior to doing an insert. - if (tagIdx != PredefinedTags.Count) + if (tagIdx != _predefinedTags.Count) _predefinedTags.RemoveAt(tagIdx); if (!string.IsNullOrEmpty(tag)) - { - // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. - var existingIdx = _predefinedTags.BinarySearch(tag); - if (existingIdx < 0) - _predefinedTags.Insert(~existingIdx, tag); - } + _predefinedTags.TryAdd(tag, default); Save(); } @@ -147,7 +137,7 @@ public sealed class PredefinedTagManager : ISavable _enabledColor = ColorId.PredefinedTagAdd.Value(); _disabledColor = ColorId.PredefinedTagRemove.Value(); var (edited, others) = editLocal ? (localTags, modTags) : (modTags, localTags); - foreach (var (tag, idx) in PredefinedTags.WithIndex()) + foreach (var (tag, idx) in _predefinedTags.Keys.WithIndex()) { var tagIdx = edited.IndexOf(tag); var inOther = tagIdx < 0 && others.IndexOf(tag) >= 0; @@ -189,4 +179,16 @@ public sealed class PredefinedTagManager : ISavable private static float CalcTextButtonWidth(string text) => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; + + public IEnumerator GetEnumerator() + => _predefinedTags.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _predefinedTags.Count; + + public string this[int index] + => _predefinedTags.Keys[index]; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9f8ffb38..80fe6fb6 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -924,7 +924,7 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Predefined Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.PredefinedTags, + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager, out var editedTag); if (tagIdx >= 0) From c8216b0accd2d8ce99dc2ae44d50ac037e2daf45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 22:52:20 +0100 Subject: [PATCH 1565/2451] Use ObjectManager, Actor and Model. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester.cs | 140 +++++++++--------- Penumbra/Api/PenumbraApi.cs | 47 +++--- .../Hooks/Animation/LoadCharacterVfx.cs | 26 ++-- .../Hooks/Animation/LoadTimelineResources.cs | 14 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 6 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 11 +- .../LiveColorTablePreviewer.cs | 3 +- .../MaterialPreview/LiveMaterialPreviewer.cs | 4 +- .../LiveMaterialPreviewerBase.cs | 5 +- .../Interop/MaterialPreview/MaterialInfo.cs | 36 ++--- .../PathResolving/CollectionResolver.cs | 32 ++-- .../Interop/PathResolving/CutsceneService.cs | 31 ++-- .../Interop/PathResolving/DrawObjectState.cs | 15 +- .../ResourceTree/ResourceTreeFactory.cs | 89 ++--------- .../Interop/ResourceTree/TreeBuildCache.cs | 41 ++--- Penumbra/Interop/Services/RedrawService.cs | 39 ++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 7 +- Penumbra/UI/PredefinedTagManager.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 7 +- Penumbra/UI/Tabs/ModsTab.cs | 5 +- 22 files changed, 240 insertions(+), 325 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 1df06807..d2a1406b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1df06807650a79813791effaa01fb7c4710b3dab +Subproject commit d2a1406bc32f715c0687613f02e3f74caf7ceea9 diff --git a/Penumbra.GameData b/Penumbra.GameData index d53db6a3..a1262e24 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d53db6a358cedecd3ef18f62f12a07deff4b61ee +Subproject commit a1262e242ca33bb0e9e4f080d294d9160b5e54eb diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 380b741c..e1dd81cb 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; @@ -18,6 +19,7 @@ using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; using Penumbra.GameData.Structs; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; namespace Penumbra.Api; @@ -40,7 +42,7 @@ public class IpcTester : IDisposable private readonly Temporary _temporary; private readonly ResourceTree _resourceTree; - public IpcTester(Configuration config, DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, + public IpcTester(Configuration config, DalamudPluginInterface pi, ObjectManager objects, IClientState clientState, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService) { @@ -280,16 +282,16 @@ public class IpcTester : IDisposable { ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); using var popup = ImRaii.Popup("Config Popup"); - if (popup) - { - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImGuiUtil.TextWrapped(_currentConfiguration); - } + if (!popup) + return; - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.TextWrapped(_currentConfiguration); } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); } private void UpdateModDirectoryChanged(string path, bool valid) @@ -304,15 +306,15 @@ public class IpcTester : IDisposable public readonly EventSubscriber Tooltip; public readonly EventSubscriber Click; - private string _lastDrawnMod = string.Empty; - private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; - private bool _subscribedToTooltip = false; - private bool _subscribedToClick = false; - private string _lastClicked = string.Empty; - private string _lastHovered = string.Empty; - private TabType _selectTab = TabType.None; - private string _modName = string.Empty; - private PenumbraApiEc _ec = PenumbraApiEc.Success; + private string _lastDrawnMod = string.Empty; + private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; + private bool _subscribedToTooltip; + private bool _subscribedToClick; + private string _lastClicked = string.Empty; + private string _lastHovered = string.Empty; + private TabType _selectTab = TabType.None; + private string _modName = string.Empty; + private PenumbraApiEc _ec = PenumbraApiEc.Success; public Ui(DalamudPluginInterface pi) { @@ -401,14 +403,14 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; private readonly IClientState _clientState; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; public readonly EventSubscriber Redrawn; - private string _redrawName = string.Empty; - private int _redrawIndex = 0; + private string _redrawName = string.Empty; + private int _redrawIndex; private string _lastRedrawnString = "None"; - public Redrawing(DalamudPluginInterface pi, IObjectTable objects, IClientState clientState) + public Redrawing(DalamudPluginInterface pi, ObjectManager objects, IClientState clientState) { _pi = pi; _objects = objects; @@ -440,8 +442,8 @@ public class IpcTester : IDisposable DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); var tmp = _redrawIndex; ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.Length)) - _redrawIndex = Math.Clamp(tmp, 0, _objects.Length); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.Count)) + _redrawIndex = Math.Clamp(tmp, 0, _objects.Count); ImGui.SameLine(); if (ImGui.Button("Redraw##Index")) @@ -458,12 +460,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn(IntPtr address, int index) { if (index < 0 - || index > _objects.Length + || index > _objects.Count || address == IntPtr.Zero - || _objects[index]?.Address != address) + || _objects[index].Address != address) _lastRedrawnString = "Invalid"; - _lastRedrawnString = $"{_objects[index]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})"; } } @@ -588,8 +590,8 @@ public class IpcTester : IDisposable private string _currentResolvePath = string.Empty; private string _currentResolveCharacter = string.Empty; private string _currentReversePath = string.Empty; - private int _currentReverseIdx = 0; - private Task<(string[], string[][])> _task = Task.FromException<(string[], string[][])>(new Exception()); + private int _currentReverseIdx; + private Task<(string[], string[][])> _task = Task.FromException<(string[], string[][])>(new Exception()); public Resolve(DalamudPluginInterface pi) => _pi = pi; @@ -696,8 +698,6 @@ public class IpcTester : IDisposable return text; } - ; - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); if (forwardArray.Length > 0 || reverseArray.Length > 0) { @@ -721,18 +721,18 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private int _objectIdx = 0; + private int _objectIdx; private string _collectionName = string.Empty; private bool _allowCreation = true; private bool _allowDeletion = true; private ApiCollectionType _type = ApiCollectionType.Current; private string _characterCollectionName = string.Empty; - private IList _collections = new List(); + private IList _collections = []; private string _changedItemCollection = string.Empty; private IReadOnlyDictionary _changedItems = new Dictionary(); private PenumbraApiEc _returnCode = PenumbraApiEc.Success; - private string? _oldCollection = null; + private string? _oldCollection; public Collections(DalamudPluginInterface pi) => _pi = pi; @@ -845,8 +845,8 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; - private string _characterName = string.Empty; - private int _gameObjectIndex = 0; + private string _characterName = string.Empty; + private int _gameObjectIndex; public Meta(DalamudPluginInterface pi) => _pi = pi; @@ -1040,11 +1040,11 @@ public class IpcTester : IDisposable private string _settingsModName = string.Empty; private string _settingsCollection = string.Empty; private bool _settingsAllowInheritance = true; - private bool _settingsInherit = false; - private bool _settingsEnabled = false; - private int _settingsPriority = 0; + private bool _settingsInherit; + private bool _settingsEnabled; + private int _settingsPriority; private IDictionary, GroupType)>? _availableSettings; - private IDictionary>? _currentSettings = null; + private IDictionary>? _currentSettings; public ModSettings(DalamudPluginInterface pi) { @@ -1287,7 +1287,7 @@ public class IpcTester : IDisposable private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; private PenumbraApiEc _lastTempError; - private int _tempActorIndex = 0; + private int _tempActorIndex; private bool _forceOverwrite; public void Draw() @@ -1441,15 +1441,13 @@ public class IpcTester : IDisposable } } - private class ResourceTree + private class ResourceTree(DalamudPluginInterface pi, ObjectManager objects) { - private readonly DalamudPluginInterface _pi; - private readonly IObjectTable _objects; - private readonly Stopwatch _stopwatch = new(); + private readonly Stopwatch _stopwatch = new(); private string _gameObjectIndices = "0"; private ResourceType _type = ResourceType.Mtrl; - private bool _withUIData = false; + private bool _withUiData; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; @@ -1459,12 +1457,6 @@ public class IpcTester : IDisposable private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees; private TimeSpan _lastCallDuration; - public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) - { - _pi = pi; - _objects = objects; - } - public void Draw() { using var _ = ImRaii.TreeNode("Resource Tree"); @@ -1473,7 +1465,7 @@ public class IpcTester : IDisposable ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); - ImGui.Checkbox("Also get names and icons", ref _withUIData); + ImGui.Checkbox("Also get names and icons", ref _withUiData); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) @@ -1483,7 +1475,7 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcePaths")) { var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(_pi); + var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(pi); _stopwatch.Restart(); var resourcePaths = subscriber.Invoke(gameObjects); @@ -1499,7 +1491,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); if (ImGui.Button("Get##PlayerResourcePaths")) { - var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(_pi); + var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(pi); _stopwatch.Restart(); var resourcePaths = subscriber.Invoke(); @@ -1515,9 +1507,9 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcesOfType")) { var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi); + var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(pi); _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUIData, gameObjects); + var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects); _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcesOfType = gameObjects @@ -1531,9 +1523,9 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); if (ImGui.Button("Get##PlayerResourcesOfType")) { - var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(_pi); + var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(pi); _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUIData); + var resourcesOfType = subscriber.Invoke(_type, _withUiData); _lastCallDuration = _stopwatch.Elapsed; _lastPlayerResourcesOfType = resourcesOfType @@ -1547,9 +1539,9 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourceTrees")) { var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(_pi); + var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(pi); _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUIData, gameObjects); + var trees = subscriber.Invoke(_withUiData, gameObjects); _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourceTrees = gameObjects @@ -1563,9 +1555,9 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees"); if (ImGui.Button("Get##PlayerResourceTrees")) { - var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(_pi); + var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(pi); _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUIData); + var trees = subscriber.Invoke(_withUiData); _lastCallDuration = _stopwatch.Elapsed; _lastPlayerResourceTrees = trees @@ -1666,13 +1658,13 @@ public class IpcTester : IDisposable { DrawWithHeaders(result, resources => { - using var table = ImRaii.Table(string.Empty, _withUIData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); if (!table) return; ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUIData ? 0.55f : 0.85f); - if (_withUIData) + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f); + if (_withUiData) ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableHeadersRow(); @@ -1682,7 +1674,7 @@ public class IpcTester : IDisposable TextUnformattedMono($"0x{resourceHandle:X}"); ImGui.TableNextColumn(); ImGui.TextUnformatted(actualPath); - if (_withUIData) + if (_withUiData) { ImGui.TableNextColumn(); TextUnformattedMono(icon.ToString()); @@ -1699,11 +1691,11 @@ public class IpcTester : IDisposable { ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); - using var table = ImRaii.Table(string.Empty, _withUIData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); + using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); if (!table) return; - if (_withUIData) + if (_withUiData) { ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); @@ -1726,11 +1718,11 @@ public class IpcTester : IDisposable ImGui.TableNextColumn(); var hasChildren = node.Children.Any(); using var treeNode = ImRaii.TreeNode( - $"{(_withUIData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", + $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", hasChildren ? ImGuiTreeNodeFlags.SpanFullWidth : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); - if (_withUIData) + if (_withUiData) { ImGui.TableNextColumn(); TextUnformattedMono(node.Type.ToString()); @@ -1770,10 +1762,10 @@ public class IpcTester : IDisposable private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) { - var gameObject = _objects[gameObjectIndex.Index]; + var gameObject = objects[gameObjectIndex]; - return gameObject != null - ? $"[{gameObjectIndex}] {gameObject.Name} ({gameObject.ObjectKind})" + return gameObject.Valid + ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})" : $"[{gameObjectIndex}] null"; } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index f5bb67bd..5146383d 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -20,6 +20,7 @@ using Penumbra.String.Classes; using Penumbra.Services; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.GameData.Interop; using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.UI; @@ -93,7 +94,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private IDataManager _gameData; private IFramework _framework; - private IObjectTable _objects; + private ObjectManager _objects; private ModManager _modManager; private ResourceLoader _resourceLoader; private Configuration _config; @@ -111,7 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private TextureManager _textureManager; private ResourceTreeFactory _resourceTreeFactory; - public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, IObjectTable objects, + public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, ObjectManager objects, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, @@ -928,10 +929,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (actorIndex < 0 || actorIndex >= _objects.Length) + if (actorIndex < 0 || actorIndex >= _objects.Count) return PenumbraApiEc.InvalidArgument; - var identifier = _actors.FromObject(_objects[actorIndex], false, false, true); + var identifier = _actors.FromObject(_objects[actorIndex], out _, false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; @@ -1076,11 +1077,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { - var characters = gameObjects.Select(index => _objects[index]).OfType(); + var characters = gameObjects.Select(index => _objects.GetDalamudObject((int) index)).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); - return Array.ConvertAll(gameObjects, obj => pathDictionaries.TryGetValue(obj, out var pathDict) ? pathDict : null); + return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj)); } public IReadOnlyDictionary> GetPlayerResourcePaths() @@ -1091,39 +1092,39 @@ public class PenumbraApi : IDisposable, IPenumbraApi return pathDictionaries.AsReadOnly(); } - public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUIData, + public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => _objects[index]).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); + var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); - return Array.ConvertAll(gameObjects, obj => resDictionaries.TryGetValue(obj, out var resDict) ? resDict : null); + return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj)); } public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, - bool withUIData) + bool withUiData) { var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); return resDictionaries.AsReadOnly(); } - public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUIData, params ushort[] gameObjects) + public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => _objects[index]).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUIData ? ResourceTreeFactory.Flags.WithUiData : 0); + var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); - return Array.ConvertAll(gameObjects, obj => resDictionary.TryGetValue(obj, out var nodes) ? nodes : null); + return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj)); } - public IReadOnlyDictionary GetPlayerResourceTrees(bool withUIData) + public IReadOnlyDictionary GetPlayerResourceTrees(bool withUiData) { var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUIData ? ResourceTreeFactory.Flags.WithUiData : 0)); + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); return resDictionary.AsReadOnly(); @@ -1165,11 +1166,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = _collectionManager.Active.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Count) return false; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(gameObjectIdx); - var data = _collectionResolver.IdentifyCollection(ptr, false); + var ptr = _objects[gameObjectIdx]; + var data = _collectionResolver.IdentifyCollection(ptr.AsObject, false); if (data.Valid) collection = data.ModCollection; @@ -1179,10 +1180,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Count) return ActorIdentifier.Invalid; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(gameObjectIdx); + var ptr = _objects[gameObjectIdx]; return _actors.FromObject(ptr, out _, false, true, true); } diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 69c22773..77aaa742 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -1,9 +1,9 @@ -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Collections; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; +using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Services; @@ -16,10 +16,10 @@ public sealed unsafe class LoadCharacterVfx : FastHookGameObjectType switch { - 0 => _objects.SearchById(vfxParams->GameObjectId), + 0 => _objects.ById(vfxParams->GameObjectId), 2 => _objects[(int)vfxParams->GameObjectId], 4 => GetOwnedObject(vfxParams->GameObjectId), - _ => null, + _ => Actor.Null, }; - newData = obj != null + newData = obj.Valid ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) : ResolveData.Invalid; } var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterVfx); - var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); + var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); Penumbra.Log.Excessive( $"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); _state.RestoreAnimationData(last); @@ -59,13 +59,11 @@ public sealed unsafe class LoadCharacterVfx : FastHook Search an object by its id, then get its minion/mount/ornament. - private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) + private Actor GetOwnedObject(uint id) { - var owner = _objects.SearchById(id); - if (owner == null) - return null; - - var idx = ((GameObject*)owner.Address)->ObjectIndex; - return _objects[idx + 1]; + var owner = _objects.ById(id); + return !owner.Valid + ? Actor.Null + : _objects[owner.Index.Index + 1]; } } diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index ade957b9..4bae084e 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -3,8 +3,8 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Collections; -using Penumbra.CrashHandler; using Penumbra.GameData; +using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Services; @@ -19,11 +19,11 @@ public sealed unsafe class LoadTimelineResources : FastHook Use timelines vfuncs to obtain the associated game object.
- public static ResolveData GetDataFromTimeline(IObjectTable objects, CollectionResolver resolver, nint timeline) + public static ResolveData GetDataFromTimeline(ObjectManager objects, CollectionResolver resolver, nint timeline) { try { @@ -64,10 +64,10 @@ public sealed unsafe class LoadTimelineResources : FastHook**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; var idx = getGameObjectIdx(timeline); - if (idx >= 0 && idx < objects.Length) + if (idx >= 0 && idx < objects.Count) { - var obj = (GameObject*)objects.GetObjectAddress(idx); - return obj != null ? resolver.IdentifyCollection(obj, true) : ResolveData.Invalid; + var obj = objects[idx]; + return obj.Valid ? resolver.IdentifyCollection(obj.AsObject, true) : ResolveData.Invalid; } } } diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index 91b63838..342ffc25 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -1,7 +1,7 @@ -using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; +using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; using Penumbra.Services; @@ -13,10 +13,10 @@ public sealed unsafe class ScheduleClipUpdate : FastHook { private readonly GameState _state; private readonly CollectionResolver _collectionResolver; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly CrashHandlerService _crashHandler; - public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects, + public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, ObjectManager objects, CrashHandlerService crashHandler) { _state = state; @@ -36,9 +35,9 @@ public sealed unsafe class SomePapLoad : FastHook if (timelinePtr != nint.Zero) { var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); - if (actorIdx >= 0 && actorIdx < _objects.Length) + if (actorIdx >= 0 && actorIdx < _objects.Count) { - var newData = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true); + var newData = _collectionResolver.IdentifyCollection(_objects[actorIdx].AsObject, true); var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.PapLoad); Task.Result.Original(a1, a2, a3, a4); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index a8e4ea4d..4d35e68a 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; namespace Penumbra.Interop.MaterialPreview; @@ -20,7 +21,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase public Half[] ColorTable { get; } - public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, MaterialInfo materialInfo) + public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo) : base(objects, materialInfo) { _framework = framework; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 9ed7ca3d..0556fdc4 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -1,5 +1,5 @@ -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using Penumbra.GameData.Interop; namespace Penumbra.Interop.MaterialPreview; @@ -11,7 +11,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase private readonly float[] _originalMaterialParameter; private readonly uint[] _originalSamplerFlags; - public LiveMaterialPreviewer(IObjectTable objects, MaterialInfo materialInfo) + public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo) : base(objects, materialInfo) { var mtrlHandle = Material->MaterialResourceHandle; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs index 07986f52..f176990e 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -1,12 +1,13 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Interop; namespace Penumbra.Interop.MaterialPreview; public abstract unsafe class LiveMaterialPreviewerBase : IDisposable { - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; public readonly MaterialInfo MaterialInfo; public readonly CharacterBase* DrawObject; @@ -14,7 +15,7 @@ public abstract unsafe class LiveMaterialPreviewerBase : IDisposable protected bool Valid; - public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo) + public LiveMaterialPreviewerBase(ObjectManager objects, MaterialInfo materialInfo) { _objects = objects; diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 686b5a86..61e7c764 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -1,10 +1,11 @@ -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.String; +using Model = Penumbra.GameData.Interop.Model; namespace Penumbra.Interop.MaterialPreview; @@ -18,13 +19,13 @@ public enum DrawObjectType public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectType Type, int ModelSlot, int MaterialSlot) { - public nint GetCharacter(IObjectTable objects) - => objects.GetObjectAddress(ObjectIndex.Index); + public Actor GetCharacter(ObjectManager objects) + => objects[ObjectIndex]; - public nint GetDrawObject(nint address) + public nint GetDrawObject(Actor address) => GetDrawObject(Type, address); - public unsafe Material* GetDrawObjectMaterial(IObjectTable objects) + public unsafe Material* GetDrawObjectMaterial(ObjectManager objects) => GetDrawObjectMaterial((CharacterBase*)GetDrawObject(GetCharacter(objects))); public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject) @@ -60,13 +61,13 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy foreach (var type in Enum.GetValues()) { - var drawObject = (CharacterBase*)GetDrawObject(type, objectPtr); - if (drawObject == null) + var drawObject = GetDrawObject(type, objectPtr); + if (!drawObject.Valid) continue; - for (var i = 0; i < drawObject->SlotCount; ++i) + for (var i = 0; i < drawObject.AsCharacterBase->SlotCount; ++i) { - var model = drawObject->Models[i]; + var model = drawObject.AsCharacterBase->Models[i]; if (model == null) continue; @@ -88,19 +89,18 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy return result; } - private static unsafe nint GetDrawObject(DrawObjectType type, nint address) + private static unsafe Model GetDrawObject(DrawObjectType type, Actor address) { - var gameObject = (Character*)address; - if (gameObject == null) - return nint.Zero; + if (!address.Valid) + return Model.Null; return type switch { - DrawObjectType.Character => (nint)gameObject->GameObject.GetDrawObject(), - DrawObjectType.Mainhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject, - DrawObjectType.Offhand => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject, - DrawObjectType.Vfx => (nint)gameObject->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject, - _ => nint.Zero, + DrawObjectType.Character => address.Model, + DrawObjectType.Mainhand => address.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject, + DrawObjectType.Offhand => address.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject, + DrawObjectType.Vfx => address.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.Unk).DrawObject, + _ => Model.Null, }; } } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1a715f13..fa122e39 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -6,6 +6,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -170,10 +171,10 @@ public sealed unsafe class CollectionResolver( : null; /// Check for the Yourself collection. - private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor) + private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor) { - if (actor->ObjectIndex == 0 - || cutscenes.GetParentIndex(actor->ObjectIndex) == 0 + if (actor.Index == 0 + || cutscenes.GetParentIndex(actor.Index.Index) == 0 || identifier.Equals(actors.GetCurrentPlayer())) return collectionManager.Active.ByType(CollectionType.Yourself); @@ -181,23 +182,23 @@ public sealed unsafe class CollectionResolver( } /// Check special collections given the actor. Returns notYetReady if the customize array is not filled. - private ModCollection? CollectionByAttributes(GameObject* actor, ref bool notYetReady) + private ModCollection? CollectionByAttributes(Actor actor, ref bool notYetReady) { - if (!actor->IsCharacter()) + if (!actor.IsCharacter) return null; // Only handle human models. - var character = (Character*)actor; - if (!IsModelHuman((uint)character->CharacterData.ModelCharaId)) + + if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return null; - if (character->DrawData.CustomizeData[0] == 0) + if (actor.Customize->Data[0] == 0) { notYetReady = true; return null; } - var bodyType = character->DrawData.CustomizeData[2]; + var bodyType = actor.Customize->Data[2]; var collection = bodyType switch { 3 => collectionManager.Active.ByType(CollectionType.NonPlayerElderly), @@ -207,9 +208,9 @@ public sealed unsafe class CollectionResolver( if (collection != null) return collection; - var race = (SubRace)character->DrawData.CustomizeData[4]; - var gender = (Gender)(character->DrawData.CustomizeData[1] + 1); - var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; + var race = (SubRace)actor.Customize->Data[4]; + var gender = (Gender)(actor.Customize->Data[1] + 1); + var isNpc = !actor.IsPlayer; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); collection = collectionManager.Active.ByType(type); @@ -218,15 +219,14 @@ public sealed unsafe class CollectionResolver( } /// Get the collection applying to the owner if it is available. - private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady) + private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, Actor owner, ref bool notYetReady) { - if (identifier.Type != IdentifierType.Owned || !config.UseOwnerNameForCharacterCollection || owner == null) + if (identifier.Type != IdentifierType.Owned || !config.UseOwnerNameForCharacterCollection || !owner.Valid) return null; var id = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); - return CheckYourself(id, owner) - ?? CollectionByAttributes(owner, ref notYetReady); + return CheckYourself(id, owner) ?? CollectionByAttributes(owner, ref notYetReady); } } diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 89b9f917..93fee11e 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -3,6 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.Interop.Hooks.Objects; using Penumbra.String; @@ -14,17 +15,17 @@ public sealed class CutsceneService : IService, IDisposable public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly CopyCharacter _copyCharacter; private readonly CharacterDestructor _characterDestructor; private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) - .Where(i => _objects[i] != null) - .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); + .Where(i => _objects[i].Valid) + .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); - public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, + public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, IClientState clientState) { _objects = objects; @@ -42,13 +43,13 @@ public sealed class CutsceneService : IService, IDisposable /// Does not check for valid input index. /// Returns null if no connected actor is set or the actor does not exist anymore. /// - public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] + private Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] { get { Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx); idx = _copiedCharacters[idx - CutsceneStartIdx]; - return idx < 0 ? null : _objects[idx]; + return idx < 0 ? null : _objects.GetDalamudObject(idx); } } @@ -64,10 +65,10 @@ public sealed class CutsceneService : IService, IDisposable if (parentIdx is < -1 or >= CutsceneEndIdx) return false; - if (_objects.GetObjectAddress(copyIdx) == nint.Zero) + if (!_objects[copyIdx].Valid) return false; - if (parentIdx != -1 && _objects.GetObjectAddress(parentIdx) == nint.Zero) + if (parentIdx != -1 && !_objects[parentIdx].Valid) return false; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; @@ -99,9 +100,9 @@ public sealed class CutsceneService : IService, IDisposable { // A hack to deal with GPose actors leaving and thus losing the link, we just set the home world instead. // I do not think this breaks anything? - var address = (GameObject*)_objects.GetObjectAddress(i + CutsceneStartIdx); - if (address != null && address->GetObjectKind() is (byte)ObjectKind.Pc) - ((Character*)address)->HomeWorld = character->HomeWorld; + var address = _objects[i + CutsceneStartIdx]; + if (address.IsPlayer) + address.AsCharacter->HomeWorld = character->HomeWorld; _copiedCharacters[i] = -1; } @@ -125,7 +126,7 @@ public sealed class CutsceneService : IService, IDisposable /// Try to recover GPose actors on reloads into a running game. /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. - private unsafe void RecoverGPoseActors() + private void RecoverGPoseActors() { Dictionary? actors = null; @@ -143,11 +144,11 @@ public sealed class CutsceneService : IService, IDisposable bool TryGetName(int idx, out ByteString name) { name = ByteString.Empty; - var address = (GameObject*)_objects.GetObjectAddress(idx); - if (address == null) + var address = _objects[idx]; + if (!address.Valid) return false; - name = new ByteString(address->Name); + name = address.Utf8Name; return !name.IsEmpty; } diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index b3ae108b..784ac3fb 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,8 +1,7 @@ -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData.Interop; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.Objects; @@ -11,7 +10,7 @@ namespace Penumbra.Interop.PathResolving; public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly CreateCharacterBase _createCharacterBase; private readonly WeaponReload _weaponReload; private readonly CharacterBaseDestructor _characterBaseDestructor; @@ -22,7 +21,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; - public unsafe DrawObjectState(IObjectTable objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, + public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, CharacterBaseDestructor characterBaseDestructor, GameState gameState) { _objects = objects; @@ -95,11 +94,11 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary private unsafe void InitializeDrawObjects() { - for (var i = 0; i < _objects.Length; ++i) + for (var i = 0; i < _objects.Count; ++i) { - var ptr = (GameObject*)_objects.GetObjectAddress(i); - if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) - IterateDrawObjectTree(&ptr->DrawObject->Object, (nint)ptr, false, false); + var ptr = _objects[i]; + if (ptr is { IsCharacter: true, Model.Valid: true }) + IterateDrawObjectTree((Object*)ptr.Model.Address, ptr, false, false); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index df5e1964..ae7187f0 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,39 +1,26 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api.Enums; -using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -public class ResourceTreeFactory +public class ResourceTreeFactory( + IDataManager gameData, + ObjectManager objects, + CollectionResolver resolver, + ObjectIdentification identifier, + Configuration config, + ActorManager actors, + PathState pathState) { - private readonly IDataManager _gameData; - private readonly IObjectTable _objects; - private readonly CollectionResolver _collectionResolver; - private readonly ObjectIdentification _identifier; - private readonly Configuration _config; - private readonly ActorManager _actors; - private readonly PathState _pathState; - - public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, ObjectIdentification identifier, - Configuration config, ActorManager actors, PathState pathState) - { - _gameData = gameData; - _objects = objects; - _collectionResolver = resolver; - _identifier = identifier; - _config = config; - _actors = actors; - _pathState = pathState; - } - private TreeBuildCache CreateTreeBuildCache() - => new(_objects, _gameData, _actors); + => new(objects, gameData, actors); public IEnumerable GetLocalPlayerRelatedCharacters() { @@ -80,7 +67,7 @@ public class ResourceTreeFactory if (drawObjStruct == null) return null; - var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true); + var collectionResolveData = resolver.IdentifyCollection(gameObjStruct, true); if (!collectionResolveData.Valid) return null; @@ -89,9 +76,9 @@ public class ResourceTreeFactory var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); - var globalContext = new GlobalResolveContext(_identifier, collectionResolveData.ModCollection, + var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); - using (var _ = _pathState.EnterInternalResolve()) + using (var _ = pathState.EnterInternalResolve()) { tree.LoadResources(globalContext); } @@ -103,56 +90,12 @@ public class ResourceTreeFactory // ResolveGamePaths(tree, collectionResolveData.ModCollection); if (globalContext.WithUiData) ResolveUiData(tree); - FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null); + FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null); Cleanup(tree); return tree; } - private static void ResolveGamePaths(ResourceTree tree, ModCollection collection) - { - var forwardDictionary = new Dictionary(); - var reverseDictionary = new Dictionary>(); - foreach (var node in tree.FlatNodes) - { - if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) - reverseDictionary.TryAdd(node.FullPath.ToPath(), null!); - else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) - forwardDictionary.TryAdd(node.GamePath, null); - } - - foreach (var key in forwardDictionary.Keys) - forwardDictionary[key] = collection.ResolvePath(key); - - var reverseResolvedArray = collection.ReverseResolvePaths(reverseDictionary.Keys); - foreach (var (key, set) in reverseDictionary.Keys.Zip(reverseResolvedArray)) - reverseDictionary[key] = set; - - foreach (var node in tree.FlatNodes) - { - if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) - { - if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) - continue; - - if (resolvedSet.Count != 1) - { - Penumbra.Log.Debug( - $"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); - foreach (var gamePath in resolvedSet) - Penumbra.Log.Debug($"Game path: {gamePath}"); - } - - node.PossibleGamePaths = resolvedSet.ToArray(); - } - else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) - { - if (forwardDictionary.TryGetValue(node.GamePath, out var resolved)) - node.FullPath = resolved ?? new FullPath(node.GamePath); - } - } - } - private static void ResolveUiData(ResourceTree tree) { foreach (var node in tree.FlatNodes) @@ -217,12 +160,12 @@ public class ResourceTreeFactory private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache) { - var identifier = _actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); switch (identifier.Type) { case IdentifierType.Player: return (identifier.PlayerName.ToString(), true); case IdentifierType.Owned: - var ownerChara = _objects.CreateObjectReference((nint)owner) as Dalamud.Game.ClientState.Objects.Types.Character; + var ownerChara = objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character; if (ownerChara != null) { var ownerName = GetCharacterName(ownerChara, cache); diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 7582c753..2798002a 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -3,19 +3,19 @@ using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal readonly struct TreeBuildCache(IObjectTable objects, IDataManager dataManager, ActorManager actors) +internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) { private readonly Dictionary _shaderPackages = []; public unsafe bool IsLocalPlayerRelated(Character character) { - var player = objects[0]; + var player = objects.GetDalamudObject(0); if (player == null) return false; @@ -31,30 +31,30 @@ internal readonly struct TreeBuildCache(IObjectTable objects, IDataManager dataM } public IEnumerable GetCharacters() - => objects.OfType(); + => objects.Objects.OfType(); public IEnumerable GetLocalPlayerRelatedCharacters() { - var player = objects[0]; + var player = objects.GetDalamudObject(0); if (player == null) yield break; yield return (Character)player; - var minion = objects[1]; + var minion = objects.GetDalamudObject(1); if (minion != null) yield return (Character)minion; var playerId = player.ObjectId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { - if (objects[i] is Character owned && owned.OwnerId == playerId) + if (objects.GetDalamudObject(i) is Character owned && owned.OwnerId == playerId) yield return owned; } for (var i = ObjectIndex.CutsceneStart.Index; i < ObjectIndex.CharacterScreen.Index; ++i) { - var character = objects[i] as Character; + var character = objects.GetDalamudObject((int) i) as Character; if (character == null) continue; @@ -62,34 +62,11 @@ internal readonly struct TreeBuildCache(IObjectTable objects, IDataManager dataM if (parent < 0) continue; - if (parent is 0 or 1 || objects[parent]?.OwnerId == playerId) + if (parent is 0 or 1 || objects.GetDalamudObject(parent)?.OwnerId == playerId) yield return character; } } - private unsafe ByteString GetPlayerName(GameObject player) - { - var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)player.Address; - return new ByteString(gameObject->Name); - } - - private unsafe bool GetOwnedId(ByteString playerName, uint playerId, int idx, [NotNullWhen(true)] out Character? character) - { - character = objects[idx] as Character; - if (character == null) - return false; - - var actorId = actors.FromObject(character, out var owner, true, true, true); - if (!actorId.IsValid) - return false; - if (owner != null && owner->OwnerID != playerId) - return false; - if (actorId.Type is not IdentifierType.Player || !actorId.PlayerName.Equals(playerName)) - return false; - - return true; - } - /// Try to read a shpk file from the given path and cache it on success. public ShpkFile? ReadShaderPackage(FullPath path) => ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index e0a94d30..a6fec0b5 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -11,6 +11,7 @@ using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.Interop.Structs; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -57,7 +58,7 @@ public unsafe partial class RedrawService // this will be in obj and true will be returned. private bool FindCorrectActor(int idx, out GameObject? obj) { - obj = _objects[idx]; + obj = _objects.GetDalamudObject(idx); if (!InGPose || obj == null || IsGPoseActor(idx)) return false; @@ -70,21 +71,21 @@ public unsafe partial class RedrawService if (name == gPoseName) { - obj = _objects[GPosePlayerIdx + i]; + obj = _objects.GetDalamudObject(GPosePlayerIdx + i); return true; } } for (; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter) { - var gPoseName = _objects[GPosePlayerIdx + _gPoseNameCounter]?.Name.ToString(); + var gPoseName = _objects.GetDalamudObject(GPosePlayerIdx + _gPoseNameCounter)?.Name.ToString(); _gPoseNames[_gPoseNameCounter] = gPoseName; if (gPoseName == null) break; if (name == gPoseName) { - obj = _objects[GPosePlayerIdx + _gPoseNameCounter]; + obj = _objects.GetDalamudObject(GPosePlayerIdx + _gPoseNameCounter); return true; } } @@ -111,7 +112,7 @@ public sealed unsafe partial class RedrawService : IDisposable private const int FurnitureIdx = 1337; private readonly IFramework _framework; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly ITargetManager _targets; private readonly ICondition _conditions; private readonly IClientState _clientState; @@ -133,7 +134,7 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState, + public RedrawService(IFramework framework, ObjectManager objects, ITargetManager targets, ICondition conditions, IClientState clientState, Configuration config, CommunicatorService communicator) { _framework = framework; @@ -170,7 +171,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; if (gPose) @@ -189,7 +190,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; if (gPose) @@ -212,7 +213,7 @@ public sealed unsafe partial class RedrawService : IDisposable private void ReloadActorAfterGPose(GameObject? actor) { - if (_objects[GPosePlayerIdx] != null) + if (_objects[GPosePlayerIdx].Valid) { ReloadActor(actor); return; @@ -230,7 +231,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (_target < 0) return; - var actor = _objects[_target]; + var actor = _objects.GetDalamudObject(_target); if (actor == null || _targets.Target != null) return; @@ -316,12 +317,12 @@ public sealed unsafe partial class RedrawService : IDisposable if (idx < 0) { var newIdx = ~idx; - WriteInvisible(_objects[newIdx]); + WriteInvisible(_objects.GetDalamudObject(newIdx)); _afterGPoseQueue[numKept++] = newIdx; } else { - WriteVisible(_objects[idx]); + WriteVisible(_objects.GetDalamudObject(idx)); } } @@ -357,8 +358,8 @@ public sealed unsafe partial class RedrawService : IDisposable private GameObject? GetLocalPlayer() { - var gPosePlayer = _objects[GPosePlayerIdx]; - return gPosePlayer ?? _objects[0]; + var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx); + return gPosePlayer ?? _objects.GetDalamudObject(0); } public bool GetName(string lowerName, out GameObject? actor) @@ -379,7 +380,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (!ret && lowerName.Length > 1 && lowerName[0] == '#' && ushort.TryParse(lowerName[1..], out var objectIndex)) { ret = true; - actor = _objects[objectIndex]; + actor = _objects.GetDalamudObject((int) objectIndex); } return ret; @@ -387,8 +388,8 @@ public sealed unsafe partial class RedrawService : IDisposable public void RedrawObject(int tableIndex, RedrawType settings) { - if (tableIndex >= 0 && tableIndex < _objects.Length) - RedrawObject(_objects[tableIndex], settings); + if (tableIndex >= 0 && tableIndex < _objects.Count) + RedrawObject(_objects.GetDalamudObject(tableIndex), settings); } public void RedrawObject(string name, RedrawType settings) @@ -399,13 +400,13 @@ public sealed unsafe partial class RedrawService : IDisposable else if (GetName(lowerName, out var target)) RedrawObject(target, settings); else - foreach (var actor in _objects.Where(a => a.Name.ToString().ToLowerInvariant() == lowerName)) + foreach (var actor in _objects.Objects.Where(a => a.Name.ToString().ToLowerInvariant() == lowerName)) RedrawObject(actor, settings); } public void RedrawAll(RedrawType settings) { - foreach (var actor in _objects) + foreach (var actor in _objects.Objects) RedrawObject(actor, settings); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 72dd91d3..6cf24f62 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -12,6 +12,7 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; using Penumbra.Import.Models; using Penumbra.Import.Textures; using Penumbra.Interop.Hooks.Objects; @@ -46,7 +47,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly IDragDropManager _dragDropManager; private readonly IDataManager _gameData; private readonly IFramework _framework; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly CharacterBaseDestructor _characterBaseDestructor; private Vector2 _iconSize = Vector2.Zero; @@ -446,7 +447,7 @@ public partial class ModEditWindow : Window, IDisposable DrawOptionSelectHeader(); - var setsEqual = !_editor!.SwapEditor.Changes; + var setsEqual = !_editor.SwapEditor.Changes; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) @@ -577,7 +578,7 @@ public partial class ModEditWindow : Window, IDisposable Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) + ChangedItemDrawer changedItemDrawer, ObjectManager objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { _performance = performance; diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 17e8432b..0e5377d6 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -71,8 +71,7 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList foreach (var (tag, data) in tags) _predefinedTags.TryAdd(tag, data); break; - default: - throw new Exception($"Invalid version {version}."); + default: throw new Exception($"Invalid version {version}."); } } catch (Exception ex) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ab7ccf0c..2003f0ef 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -21,6 +21,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; using Penumbra.Import.Structs; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; @@ -90,12 +91,12 @@ public class DebugTab : Window, ITab private readonly RedrawService _redraws; private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; - private readonly IObjectTable _objects; + private readonly ObjectManager _objects; private readonly IClientState _clientState; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; - public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, + public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, @@ -430,7 +431,7 @@ public class DebugTab : Window, ITab DrawSpecial("Current Card", _actors.GetCardPlayer()); DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - foreach (var obj in _objects) + foreach (var obj in _objects.Objects) { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index bb8856b3..e4d94bb5 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -14,6 +14,7 @@ using Penumbra.Mods.Manager; using Penumbra.UI.ModsTab; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using Penumbra.Collections.Manager; +using Penumbra.GameData.Interop; namespace Penumbra.UI.Tabs; @@ -28,7 +29,7 @@ public class ModsTab( IClientState clientState, CollectionSelectHeader collectionHeader, ITargetManager targets, - IObjectTable objectTable) + ObjectManager objects) : ITab { private readonly ActiveCollections _activeCollections = collectionManager.Active; @@ -128,7 +129,7 @@ public class ModsTab( using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; - var tt = objectTable.GetObjectAddress(0) == nint.Zero + var tt = !objects[0].Valid ? "\nCan only be used when you are logged in and your character is available." : string.Empty; DrawButton(buttonWidth, "All", string.Empty, tt); From 26f3742b31b93787ed608b23c7cfbacf7b107876 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Mar 2024 15:27:51 +0100 Subject: [PATCH 1566/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a1262e24..74a30576 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a1262e242ca33bb0e9e4f080d294d9160b5e54eb +Subproject commit 74a305768880cd783b21e85ef97e9be77b119885 From 6cb4f7ac8a141faa908b0641ff7c6991ac582122 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Mar 2024 17:35:45 +0100 Subject: [PATCH 1567/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 74a30576..529e1811 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 74a305768880cd783b21e85ef97e9be77b119885 +Subproject commit 529e18115023732794994bfb8df4818b68951ea4 From e8fffa47a05d33e2c4f2e9364e72c2b61e0e71d2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Mar 2024 18:20:08 +0100 Subject: [PATCH 1568/2451] 1.0.2.0 --- Penumbra/UI/Changelog.cs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 67ab1a87..8b00ab8d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -47,18 +47,46 @@ public class PenumbraChangelog Add8_2_0(Changelog); Add8_3_0(Changelog); Add1_0_0_0(Changelog); + Add1_0_2_0(Changelog); } #region Changelogs + private static void Add1_0_2_0(Changelog log) + => log.NextVersion("Version 1.0.2.0") + .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") + .RegisterHighlight( + "Added an experimental crash handler that is supposed to write a Penumbra.log file when the game crashes, containing Penumbra-specific information.") + .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") + .RegisterHighlight( + "Added predefined tags that can be setup in the Settings tab and can be more easily applied or removed from mods. (by DZD)") + .RegisterEntry( + "The first empty option in a single-select option group imported from a TTMP will now keep its location instead of being moved to the first option.") + .RegisterEntry("Further empty options are still removed.", 1) + .RegisterEntry("Made it more obvious if a user has not set their root directory yet.") + .RegisterEntry("Added the characterglass.shpk shader file to special shader treatment to fix issues when replacing it. (By Ny)") + .RegisterEntry("Fixed some issues with the file sizes of compressed files.") + .RegisterEntry("Fixed an issue where reloading a mod did not ensure settings for that mod being correct afterwards.") + .RegisterEntry("Added an option to automatically redraw the player character when saving files. (1.0.0.8)") + .RegisterEntry("Fixed issue with manipulating mods not triggering some events. (1.0.0.7)") + .RegisterEntry("Fixed issue with temporary mods not triggering some events. (1.0.0.6)") + .RegisterEntry("Fixed issue when renaming mods while the advanced edit window is open. (1.0.0.6)") + .RegisterEntry("Fixed issue with empty option groups. (1.0.0.5)") + .RegisterEntry("Fixed issues with cutscene character identification. (1.0.0.4)") + .RegisterEntry("Added locale environment information to support info. (1.0.0.4)") + .RegisterEntry("Fixed an issue with copied mod settings in IPC missing unused settings. (1.0.0.3)"); + private static void Add1_0_0_0(Changelog log) => log.NextVersion("Version 1.0.0.0") .RegisterHighlight("Mods in the mod selector can now be filtered by changed item categories.") .RegisterHighlight("Model Editing options in the Advanced Editing Window have been greatly extended (by ackwell):") .RegisterEntry("Attributes and referenced materials can now be set per mesh.", 1) - .RegisterEntry("Model files (.mdl) can now be exported to the well-established glTF format, which can be imported e.g. by Blender.", 1) + .RegisterEntry("Model files (.mdl) can now be exported to the well-established glTF format, which can be imported e.g. by Blender.", + 1) .RegisterEntry("glTF files can also be imported back to a .mdl file.", 1) - .RegisterHighlight("Model Export and Import are a work in progress and may encounter issues, not support all cases or produce wrong results, please let us know!", 1) + .RegisterHighlight( + "Model Export and Import are a work in progress and may encounter issues, not support all cases or produce wrong results, please let us know!", + 1) .RegisterEntry("The last selected mod and the open/close state of the Advanced Editing Window are now stored across launches.") .RegisterEntry("Footsteps of certain mounts will now be associated to collections correctly.") .RegisterEntry("Save-in-Place in the texture tab now requires the configurable modifier.") @@ -67,7 +95,8 @@ public class PenumbraChangelog .RegisterEntry("Fixed an issue with the mod panels header not updating its data when the selected mod updates.") .RegisterEntry("Fixed some issues with EQDP files for invalid characters.") .RegisterEntry("Fixed an issue with the FileDialog being drawn twice in certain situations.") - .RegisterEntry("A lot of backend changes that should not have an effect on users, but may cause issues if something got messed up."); + .RegisterEntry( + "A lot of backend changes that should not have an effect on users, but may cause issues if something got messed up."); private static void Add8_3_0(Changelog log) => log.NextVersion("Version 0.8.3.0") From fda77b49cdd79a60ede094262473088ca6f2ebf6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 20 Mar 2024 17:22:25 +0000 Subject: [PATCH 1569/2451] [CI] Updating repo.json for testing_1.0.2.0 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 1780c6c0..a4dc2f49 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.1.0", + "TestingAssemblyVersion": "1.0.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 978f41a4d9a282c53d5cbc176fecfe462acc75e3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Mar 2024 22:38:39 +0100 Subject: [PATCH 1570/2451] Make some stuff safer maybe. --- .../Buffers/MemoryMappedBuffer.cs | 60 +++++++++---------- Penumbra.CrashHandler/Program.cs | 19 ++++-- Penumbra/Services/ValidityChecker.cs | 44 +++++++------- 3 files changed, 67 insertions(+), 56 deletions(-) diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs index 35055864..c4e2627e 100644 --- a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -9,15 +9,15 @@ public class MemoryMappedBuffer : IDisposable { private const int MinHeaderLength = 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4; - private readonly MemoryMappedFile _file; - private readonly MemoryMappedViewAccessor _header; + private readonly MemoryMappedFile _file; + private readonly MemoryMappedViewAccessor _header; private readonly MemoryMappedViewAccessor[] _lines; - public readonly int Version; - public readonly uint LineCount; - public readonly uint LineCapacity; + public readonly int Version; + public readonly uint LineCount; + public readonly uint LineCapacity; private readonly uint _lineMask; - private bool _disposed; + private bool _disposed; protected uint CurrentLineCount { @@ -39,20 +39,20 @@ public class MemoryMappedBuffer : IDisposable public MemoryMappedBuffer(string mapName, int version, uint lineCount, uint lineCapacity) { - Version = version; - LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3)); + Version = version; + LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3)); LineCapacity = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCapacity, 2, int.MaxValue >> 3)); - _lineMask = LineCount - 1; - var fileName = Encoding.UTF8.GetBytes(mapName); + _lineMask = LineCount - 1; + var fileName = Encoding.UTF8.GetBytes(mapName); var headerLength = (uint)(4 + 4 + 4 + 4 + 4 + 4 + 4 + fileName.Length + 1); headerLength = (headerLength & 0b111) > 0 ? (headerLength & ~0b111u) + 0b1000 : headerLength; var capacity = LineCount * LineCapacity + headerLength; _file = MemoryMappedFile.CreateNew(mapName, capacity, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, HandleInheritability.Inheritable); _header = _file.CreateViewAccessor(0, headerLength); - _header.Write(0, headerLength); - _header.Write(4, Version); - _header.Write(8, LineCount); + _header.Write(0, headerLength); + _header.Write(4, Version); + _header.Write(8, LineCount); _header.Write(12, LineCapacity); _header.WriteArray(28, fileName, 0, fileName.Length); _header.Write(fileName.Length + 28, (byte)0); @@ -65,16 +65,16 @@ public class MemoryMappedBuffer : IDisposable uint? expectedMinLineCapacity = null) { _file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable); - using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read); - var headerLength = headerLine.ReadUInt32(0); + using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read); + var headerLength = headerLine.ReadUInt32(0); if (headerLength < MinHeaderLength) Throw($"Map {mapName} did not contain a valid header."); - _header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite); - Version = _header.ReadInt32(4); - LineCount = _header.ReadUInt32(8); + _header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite); + Version = _header.ReadInt32(4); + LineCount = _header.ReadUInt32(8); LineCapacity = _header.ReadUInt32(12); - _lineMask = LineCount - 1; + _lineMask = LineCount - 1; if (expectedVersion.HasValue && expectedVersion.Value != Version) Throw($"Map {mapName} has version {Version} instead of {expectedVersion.Value}."); @@ -122,25 +122,25 @@ public class MemoryMappedBuffer : IDisposable protected static int WriteString(string text, Span span) { - var bytes = Encoding.UTF8.GetBytes(text); - var length = bytes.Length + 1; + var bytes = Encoding.UTF8.GetBytes(text); + var source = (Span)bytes; + var length = source.Length + 1; if (length > span.Length) - throw new Exception($"String {text} is too long to write into span."); - - bytes.CopyTo(span); + source = source[..(span.Length - 1)]; + source.CopyTo(span); span[bytes.Length] = 0; - return length; + return source.Length + 1; } protected static int WriteSpan(ReadOnlySpan input, Span span) { var length = input.Length + 1; if (length > span.Length) - throw new Exception("Byte array is too long to write into span."); + input = input[..(span.Length - 1)]; input.CopyTo(span); span[input.Length] = 0; - return length; + return input.Length + 1; } protected Span GetLine(int i) @@ -150,7 +150,7 @@ public class MemoryMappedBuffer : IDisposable lock (_header) { - var lineIdx = CurrentLinePosition + i & _lineMask; + var lineIdx = (CurrentLinePosition + i) & _lineMask; if (lineIdx > CurrentLineCount) return null; @@ -168,8 +168,8 @@ public class MemoryMappedBuffer : IDisposable if (currentLineCount == LineCount) { var currentLinePos = CurrentLinePosition; - view = _lines[currentLinePos]!; - CurrentLinePosition = currentLinePos + 1 & _lineMask; + view = _lines[currentLinePos]!; + CurrentLinePosition = (currentLinePos + 1) & _lineMask; } else { diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index 518e2d04..0ea76ac6 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -14,12 +14,21 @@ public class CrashHandler { using var reader = new GameEventLogReader(); var parent = Process.GetProcessById(pid); - + using var handle = parent.SafeHandle; parent.WaitForExit(); - var exitCode = parent.ExitCode; - var obj = reader.Dump("Crash", pid, exitCode, args[2], args[3]); - using var fs = File.Open(args[0], FileMode.Create); - using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true }); + int exitCode; + try + { + exitCode = parent.ExitCode; + } + catch (Exception ex) + { + exitCode = -1; + } + + var obj = reader.Dump("Crash", pid, exitCode, args[2], args[3]); + using var fs = File.Open(args[0], FileMode.Create); + using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true }); obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true }); } catch (Exception ex) diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index d4b5005f..cc70306b 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -21,7 +21,15 @@ public class ValidityChecker : IService public readonly string Version; public readonly string CommitHash; - public readonly string GameVersion; + + public unsafe string GameVersion + { + get + { + var framework = Framework.Instance(); + return framework == null ? string.Empty : framework->GameVersion[0]; + } + } public ValidityChecker(DalamudPluginInterface pi) { @@ -30,14 +38,10 @@ public class ValidityChecker : IService IsValidSourceRepo = CheckSourceRepo(pi); var assembly = GetType().Assembly; - Version = assembly.GetName().Version?.ToString() ?? string.Empty; - CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - GameVersion = GetGameVersion(); + Version = assembly.GetName().Version?.ToString() ?? string.Empty; + CommitHash = assembly.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; } - private static unsafe string GetGameVersion() - => Framework.Instance()->GameVersion[0]; - public void LogExceptions() { if (ImcExceptions.Count > 0) @@ -49,16 +53,16 @@ public class ValidityChecker : IService private static bool CheckDevPluginPenumbra(DalamudPluginInterface pi) { #if !DEBUG - var path = Path.Combine( pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var path = Path.Combine(pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra"); + var dir = new DirectoryInfo(path); try { - return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); + return dir.Exists && dir.EnumerateFiles("*.dll", SearchOption.AllDirectories).Any(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + Penumbra.Log.Error($"Could not check for dev plugin Penumbra:\n{e}"); return true; } #else @@ -71,11 +75,9 @@ public class ValidityChecker : IService { #if !DEBUG var checkedDirectory = pi.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; - if( !ret ) - { - Penumbra.Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{pi.AssemblyLocation.Directory!.FullName}\"." ); - } + var ret = checkedDirectory?.Equals("installedPlugins", StringComparison.OrdinalIgnoreCase) ?? false; + if (!ret) + Penumbra.Log.Error($"Penumbra is not correctly installed. Application loaded from \"{pi.AssemblyLocation.Directory!.FullName}\"."); return !ret; #else @@ -89,10 +91,10 @@ public class ValidityChecker : IService #if !DEBUG return pi.SourceRepository?.Trim().ToLowerInvariant() switch { - null => false, - RepositoryLower => true, - SeaOfStarsLower => true, - _ => false, + null => false, + RepositoryLower => true, + SeaOfStarsLower => true, + _ => false, }; #else return true; From 384b8fd489c1e4b952703ed1d75544875bf790c8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 20 Mar 2024 21:40:54 +0000 Subject: [PATCH 1571/2451] [CI] Updating repo.json for testing_1.0.2.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a4dc2f49..9487b2b3 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.0", + "TestingAssemblyVersion": "1.0.2.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0ad769e08e5c3b92249ff7bc1f3ac65878e73449 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Wed, 20 Mar 2024 22:33:35 -0400 Subject: [PATCH 1572/2451] Add tri count per LoD to models tab Adds the tri count of each LoD on the selected model to the Models tab under Advanced Editing. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 67ec97f2..494ad3f6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -508,6 +508,11 @@ public partial class ModEditWindow ImGuiUtil.DrawTableColumn(file.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Stack Size"); ImGuiUtil.DrawTableColumn(file.StackSize.ToString()); + for (var lod = 0; lod < file.Lods.Length; lod++) + { + ImGuiUtil.DrawTableColumn("LoD " + lod + " Triangle Count"); + ImGuiUtil.DrawTableColumn(GetTriangleCountForLod(file, lod).ToString()); + } } } @@ -555,6 +560,18 @@ public partial class ModEditWindow return file != null; } + private static long GetTriangleCountForLod(MdlFile model, int lod) + { + var vertSum = 0u; + var meshIndex = model.Lods[lod].MeshIndex; + var meshCount = model.Lods[lod].MeshCount; + + for (var i = meshIndex; i < meshIndex + meshCount; i++) + vertSum += model.Meshes[i].IndexCount; + + return vertSum / 3; + } + private static readonly string[] ValidModelExtensions = [ ".gltf", From 739b5b5ad6a7c58c0d95c90557bf44c4a342e895 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 20 Mar 2024 20:02:09 +0100 Subject: [PATCH 1573/2451] CS-ify ShaderReplacementFixer and ModelRenderer --- Penumbra/Interop/Services/ModelRenderer.cs | 25 ++++++------------- .../Services/ShaderReplacementFixer.cs | 20 +++++---------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 6a3bf776..7df83cf7 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -1,35 +1,26 @@ using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Penumbra.GameData; namespace Penumbra.Interop.Services; -// TODO ClientStructs-ify (https://github.com/aers/FFXIVClientStructs/pull/817) public unsafe class ModelRenderer : IDisposable { - // Will be Manager.Instance()->ModelRenderer.CharacterGlassShaderPackage in CS - private const nint ModelRendererOffset = 0x13660; - private const nint CharacterGlassShaderPackageOffset = 0xD0; - - /// A static pointer to the Render::Manager address. - [Signature(Sigs.RenderManager, ScanType = ScanType.StaticAddress)] - private readonly nint* _renderManagerAddress = null; - public bool Ready { get; private set; } public ShaderPackageResourceHandle** CharacterGlassShaderPackage - => *_renderManagerAddress == 0 - ? null - : (ShaderPackageResourceHandle**)(*_renderManagerAddress + ModelRendererOffset + CharacterGlassShaderPackageOffset).ToPointer(); + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, + }; public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } private readonly IFramework _framework; - public ModelRenderer(IFramework framework, IGameInteropProvider interop) + public ModelRenderer(IFramework framework) { - interop.InitializeFromAttributes(this); _framework = framework; LoadDefaultResources(null!); if (!Ready) @@ -39,7 +30,7 @@ public unsafe class ModelRenderer : IDisposable /// We store the default data of the resources so we can always restore them. private void LoadDefaultResources(object _) { - if (*_renderManagerAddress == 0) + if (Manager.Instance() == null) return; var anyMissing = false; diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index e57fe313..26906ace 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -2,12 +2,14 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; using Penumbra.Services; +using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; namespace Penumbra.Interop.Services; @@ -22,18 +24,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; - private delegate nint CharacterBaseOnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); - private delegate nint ModelRendererOnRenderMaterialDelegate(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex); - - [StructLayout(LayoutKind.Explicit)] - private struct OnRenderMaterialParams - { - [FieldOffset(0x0)] - public Model* Model; - - [FieldOffset(0x8)] - public uint MaterialIndex; - } + private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); + private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); private readonly Hook _humanOnRenderMaterialHook; @@ -135,7 +127,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); } - private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. if (!Enabled || _moddedSkinShpkCount == 0) @@ -167,7 +159,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable } } - private nint ModelRendererOnRenderMaterialDetour(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex) + private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. From c55d6966cd02e0d765ddfccfd928649680592206 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Mar 2024 14:28:57 +0100 Subject: [PATCH 1574/2451] Update Internal csproj. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b4b14367..2bbb9b2a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b4b14367d8235eabedd561ad3626beb1d2a83889 +Subproject commit 2bbb9b2a8a4479461b252594b9d1b788b551c13c From f3ceb9034e8ad4456e6541fbe3d61024e7a55a55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Mar 2024 20:27:55 +0100 Subject: [PATCH 1575/2451] Make crashhandler somewhat more stable, support multi boxing maybe? --- .../Buffers/AnimationInvocationBuffer.cs | 16 ++-- .../Buffers/CharacterBaseBuffer.cs | 16 ++-- .../Buffers/MemoryMappedBuffer.cs | 12 +-- .../Buffers/ModdedFileBuffer.cs | 16 ++-- Penumbra.CrashHandler/GameEventLogReader.cs | 8 +- Penumbra.CrashHandler/GameEventLogWriter.cs | 8 +- Penumbra.CrashHandler/Program.cs | 2 +- Penumbra/Services/CrashHandlerService.cs | 88 ++++++++++++++----- 8 files changed, 105 insertions(+), 61 deletions(-) diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index dd966542..c92a14fd 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -88,18 +88,18 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation } } - public static IBufferReader CreateReader() - => new AnimationInvocationBuffer(false); + public static IBufferReader CreateReader(int pid) + => new AnimationInvocationBuffer(false, pid); - public static IAnimationInvocationBufferWriter CreateWriter() - => new AnimationInvocationBuffer(); + public static IAnimationInvocationBufferWriter CreateWriter(int pid) + => new AnimationInvocationBuffer(pid); - private AnimationInvocationBuffer(bool writer) - : base(_name, _version) + private AnimationInvocationBuffer(bool writer, int pid) + : base($"{_name}_{pid}_{_version}", _version) { } - private AnimationInvocationBuffer() - : base(_name, _version, _lineCount, _lineCapacity) + private AnimationInvocationBuffer(int pid) + : base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity) { } private static string ToName(AnimationInvocationType type) diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index 1fe5d7ba..d83c6e6c 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -69,17 +69,17 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu public uint TotalCount => TotalWrittenLines; - public static IBufferReader CreateReader() - => new CharacterBaseBuffer(false); + public static IBufferReader CreateReader(int pid) + => new CharacterBaseBuffer(false, pid); - public static ICharacterBaseBufferWriter CreateWriter() - => new CharacterBaseBuffer(); + public static ICharacterBaseBufferWriter CreateWriter(int pid) + => new CharacterBaseBuffer(pid); - private CharacterBaseBuffer(bool writer) - : base(_name, _version) + private CharacterBaseBuffer(bool writer, int pid) + : base($"{_name}_{pid}_{_version}", _version) { } - private CharacterBaseBuffer() - : base(_name, _version, _lineCount, _lineCapacity) + private CharacterBaseBuffer(int pid) + : base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity) { } } diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs index c4e2627e..a1b3de52 100644 --- a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -11,7 +11,7 @@ public class MemoryMappedBuffer : IDisposable private readonly MemoryMappedFile _file; private readonly MemoryMappedViewAccessor _header; - private readonly MemoryMappedViewAccessor[] _lines; + private readonly MemoryMappedViewAccessor[] _lines = []; public readonly int Version; public readonly uint LineCount; @@ -64,7 +64,8 @@ public class MemoryMappedBuffer : IDisposable public MemoryMappedBuffer(string mapName, int? expectedVersion = null, uint? expectedMinLineCount = null, uint? expectedMinLineCapacity = null) { - _file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable); + _lines = []; + _file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable); using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read); var headerLength = headerLine.ReadUInt32(0); if (headerLength < MinHeaderLength) @@ -96,6 +97,7 @@ public class MemoryMappedBuffer : IDisposable void Throw(string text) { _file.Dispose(); + _header?.Dispose(); _disposed = true; throw new Exception(text); } @@ -204,10 +206,10 @@ public class MemoryMappedBuffer : IDisposable if (_disposed) return; - _header.Dispose(); + _header?.Dispose(); foreach (var line in _lines) - line.Dispose(); - _file.Dispose(); + line?.Dispose(); + _file?.Dispose(); } ~MemoryMappedBuffer() diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index b472d413..6c774e4b 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -83,17 +83,17 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr } } - public static IBufferReader CreateReader() - => new ModdedFileBuffer(false); + public static IBufferReader CreateReader(int pid) + => new ModdedFileBuffer(false, pid); - public static IModdedFileBufferWriter CreateWriter() - => new ModdedFileBuffer(); + public static IModdedFileBufferWriter CreateWriter(int pid) + => new ModdedFileBuffer(pid); - private ModdedFileBuffer(bool writer) - : base(_name, _version) + private ModdedFileBuffer(bool writer, int pid) + : base($"{_name}_{pid}_{_version}", _version) { } - private ModdedFileBuffer() - : base(_name, _version, _lineCount, _lineCapacity) + private ModdedFileBuffer(int pid) + : base($"{_name}_{pid}_{_version}", _version, _lineCount, _lineCapacity) { } } diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs index 1ae49fa5..1813a671 100644 --- a/Penumbra.CrashHandler/GameEventLogReader.cs +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -9,13 +9,13 @@ public interface IBufferReader public IEnumerable GetLines(DateTimeOffset crashTime); } -public sealed class GameEventLogReader : IDisposable +public sealed class GameEventLogReader(int pid) : IDisposable { public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers = [ - (CharacterBaseBuffer.CreateReader(), "CharacterLoaded", "CharactersLoaded"), - (ModdedFileBuffer.CreateReader(), "ModdedFileLoaded", "ModdedFilesLoaded"), - (AnimationInvocationBuffer.CreateReader(), "VFXFuncInvoked", "VFXFuncsInvoked"), + (CharacterBaseBuffer.CreateReader(pid), "CharacterLoaded", "CharactersLoaded"), + (ModdedFileBuffer.CreateReader(pid), "ModdedFileLoaded", "ModdedFilesLoaded"), + (AnimationInvocationBuffer.CreateReader(pid), "VFXFuncInvoked", "VFXFuncsInvoked"), ]; public void Dispose() diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs index 8e809cec..e2c461f4 100644 --- a/Penumbra.CrashHandler/GameEventLogWriter.cs +++ b/Penumbra.CrashHandler/GameEventLogWriter.cs @@ -2,11 +2,11 @@ namespace Penumbra.CrashHandler; -public sealed class GameEventLogWriter : IDisposable +public sealed class GameEventLogWriter(int pid) : IDisposable { - public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter(); - public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter(); - public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter(); + public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter(pid); + public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter(pid); + public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter(pid); public void Dispose() { diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index 0ea76ac6..94e90bfc 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -12,7 +12,7 @@ public class CrashHandler try { - using var reader = new GameEventLogReader(); + using var reader = new GameEventLogReader(pid); var parent = Process.GetProcessById(pid); using var handle = parent.SafeHandle; parent.WaitForExit(); diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index a3a35b78..c713d623 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -24,6 +24,8 @@ public sealed class CrashHandlerService : IDisposable, IService private readonly Configuration _config; private readonly ValidityChecker _validityChecker; + private string _tempExecutableDirectory = string.Empty; + public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader, Configuration config, ValidityChecker validityChecker) { @@ -54,6 +56,7 @@ public sealed class CrashHandlerService : IDisposable, IService } Unsubscribe(); + CleanExecutables(); } private Process? _child; @@ -177,11 +180,12 @@ public sealed class CrashHandlerService : IDisposable, IService try { - using var reader = new GameEventLogReader(); + using var reader = new GameEventLogReader(Environment.ProcessId); JsonObject jObj; lock (_eventWriter) { - jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0, $"{_validityChecker.Version} ({_validityChecker.CommitHash})", _validityChecker.GameVersion); + jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0, $"{_validityChecker.Version} ({_validityChecker.CommitHash})", + _validityChecker.GameVersion); } var logFile = _files.LogFileName; @@ -198,14 +202,31 @@ public sealed class CrashHandlerService : IDisposable, IService } } - private string CopyExecutables() + private void CleanExecutables() { var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!; - var folder = Path.Combine(parent, "temp"); - Directory.CreateDirectory(folder); + foreach (var dir in Directory.EnumerateDirectories(parent, "temp_*")) + { + try + { + Directory.Delete(dir, true); + } + catch (Exception ex) + { + Penumbra.Log.Debug($"Could not delete {dir}:\n{ex}"); + } + } + } + + private string CopyExecutables() + { + CleanExecutables(); + var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!; + _tempExecutableDirectory = Path.Combine(parent, $"temp_{Environment.ProcessId}"); + Directory.CreateDirectory(_tempExecutableDirectory); foreach (var file in Directory.EnumerateFiles(parent, "Penumbra.CrashHandler.*")) - File.Copy(file, Path.Combine(folder, Path.GetFileName(file)), true); - return Path.Combine(folder, Path.GetFileName(_files.CrashHandlerExe)); + File.Copy(file, Path.Combine(_tempExecutableDirectory, Path.GetFileName(file)), true); + return Path.Combine(_tempExecutableDirectory, Path.GetFileName(_files.CrashHandlerExe)); } public void LogAnimation(nint character, ModCollection collection, AnimationInvocationType type) @@ -213,10 +234,17 @@ public sealed class CrashHandlerService : IDisposable, IService if (_eventWriter == null) return; - var name = GetActorName(character); - lock (_eventWriter) + try { - _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type); + var name = GetActorName(character); + lock (_eventWriter) + { + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type); + } + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Error logging animation function {type} to crash handler:\n{ex}"); } } @@ -225,11 +253,18 @@ public sealed class CrashHandlerService : IDisposable, IService if (_eventWriter == null) return; - var name = GetActorName(address); - - lock (_eventWriter) + try { - _eventWriter?.CharacterBase.WriteLine(address, name.Span, collection); + var name = GetActorName(address); + + lock (_eventWriter) + { + _eventWriter?.CharacterBase.WriteLine(address, name.Span, collection); + } + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Error logging character creation to crash handler:\n{ex}"); } } @@ -249,15 +284,22 @@ public sealed class CrashHandlerService : IDisposable, IService if (manipulatedPath == null || _eventWriter == null) return; - var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1; - if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1))) - return; - - var name = GetActorName(resolveData.AssociatedGameObject); - lock (_eventWriter) + try { - _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name, - manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); + var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1; + if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1))) + return; + + var name = GetActorName(resolveData.AssociatedGameObject); + lock (_eventWriter) + { + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name, + manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); + } + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Error logging resource to crash handler:\n{ex}"); } } @@ -276,7 +318,7 @@ public sealed class CrashHandlerService : IDisposable, IService try { CloseEventWriter(); - _eventWriter = new GameEventLogWriter(); + _eventWriter = new GameEventLogWriter(Environment.ProcessId); Penumbra.Log.Debug("Opened new Event Writer for crash handler."); } catch (Exception ex) From 72979a9743158749d01d74f400c878c3e3f9c41e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Mar 2024 21:31:30 +0100 Subject: [PATCH 1576/2451] Remarked changes. --- OtterGui | 2 +- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 139 ++++++++++-------- 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/OtterGui b/OtterGui index b4b14367..2bbb9b2a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b4b14367d8235eabedd561ad3626beb1d2a83889 +Subproject commit 2bbb9b2a8a4479461b252594b9d1b788b551c13c diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 494ad3f6..3672dce7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -7,6 +7,7 @@ using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; +using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -14,9 +15,13 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 4; - private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; - private const string MdlExportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-25968400-ebe5-4861-b610-cb1556db7ec4"; + private const int MdlMaterialMaximum = 4; + + private const string MdlImportDocumentation = + @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; + + private const string MdlExportDocumentation = + @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-25968400-ebe5-4861-b610-cb1556db7ec4"; private readonly FileEditor _modelTab; private readonly ModelManager _models; @@ -25,31 +30,51 @@ public partial class ModEditWindow private readonly List _subMeshAttributeTagWidgets = []; private string _customPath = string.Empty; private Utf8GamePath _customGamePath = Utf8GamePath.Empty; + private MdlFile _lastFile = null!; + private long[] _lodTriCount = []; + + private void UpdateFile(MdlFile file, bool force) + { + if (file == _lastFile) + { + if (force) + UpdateMeshes(); + } + else + { + UpdateMeshes(); + _lastFile = file; + _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + } + + return; + + void UpdateMeshes() + { + var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_subMeshAttributeTagWidgets.Count != subMeshTotal) + { + _subMeshAttributeTagWidgets.Clear(); + _subMeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) + ); + } + } + } private bool DrawModelPanel(MdlTab tab, bool disabled) { - var file = tab.Mdl; - - var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_subMeshAttributeTagWidgets.Count != subMeshTotal) - { - _subMeshAttributeTagWidgets.Clear(); - _subMeshAttributeTagWidgets.AddRange( - Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) - ); - } - + UpdateFile(tab.Mdl, tab.Dirty); DrawImportExport(tab, disabled); var ret = tab.Dirty; - ret |= DrawModelMaterialDetails(tab, disabled); - if (ImGui.CollapsingHeader($"Meshes ({file.Meshes.Length})###meshes")) - for (var i = 0; i < file.LodCount; ++i) + if (ImGui.CollapsingHeader($"Meshes ({_lastFile.Meshes.Length})###meshes")) + for (var i = 0; i < _lastFile.LodCount; ++i) ret |= DrawModelLodDetails(tab, i, disabled); - ret |= DrawOtherModelDetails(file, disabled); + ret |= DrawOtherModelDetails(disabled); return !disabled && ret; } @@ -85,7 +110,7 @@ public partial class ModEditWindow using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { - ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); + ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes); if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.", @@ -111,10 +136,7 @@ public partial class ModEditWindow if (tab.GamePaths == null) { - if (tab.IoExceptions.Count == 0) - ImGui.TextUnformatted("Resolving model game paths."); - else - ImGui.TextUnformatted("Failed to resolve model game paths."); + ImGui.TextUnformatted(tab.IoExceptions.Count == 0 ? "Resolving model game paths." : "Failed to resolve model game paths."); return; } @@ -149,14 +171,15 @@ public partial class ModEditWindow ImGui.SameLine(); DrawDocumentationLink(MdlExportDocumentation); } - + private static void DrawIoExceptions(MdlTab tab) { if (tab.IoExceptions.Count == 0) return; var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); - using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, borderColor: Colors.RegexWarningBorder); + using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, + borderColor: Colors.RegexWarningBorder); var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; foreach (var (exception, index) in tab.IoExceptions.WithIndex()) @@ -181,7 +204,7 @@ public partial class ModEditWindow if (tab.IoWarnings.Count == 0) return; - var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); + var size = new Vector2(ImGui.GetContentRegionAvail().X, 0); using var frame = ImRaii.FramedGroup("Warnings", size, headerPreIcon: FontAwesomeIcon.ExclamationCircle, borderColor: 0xFF40FFFF); var spaceAvail = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X - 100; @@ -445,7 +468,7 @@ public partial class ModEditWindow if (attributes == null) { attributes = ["invalid attribute data"]; - disabled = true; + disabled = true; } var tagIndex = widget.Draw(string.Empty, string.Empty, attributes, @@ -460,7 +483,7 @@ public partial class ModEditWindow return true; } - private static bool DrawOtherModelDetails(MdlFile file, bool _) + private bool DrawOtherModelDetails(bool _) { using var header = ImRaii.CollapsingHeader("Further Content"); if (!header) @@ -471,47 +494,47 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn(file.Version.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.Version.ToString()); ImGuiUtil.DrawTableColumn("Radius"); - ImGuiUtil.DrawTableColumn(file.Radius.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); - ImGuiUtil.DrawTableColumn(file.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(_lastFile.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Shadow Clip Out Distance"); - ImGuiUtil.DrawTableColumn(file.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(_lastFile.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("LOD Count"); - ImGuiUtil.DrawTableColumn(file.LodCount.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.LodCount.ToString()); ImGuiUtil.DrawTableColumn("Enable Index Buffer Streaming"); - ImGuiUtil.DrawTableColumn(file.EnableIndexBufferStreaming.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.EnableIndexBufferStreaming.ToString()); ImGuiUtil.DrawTableColumn("Enable Edge Geometry"); - ImGuiUtil.DrawTableColumn(file.EnableEdgeGeometry.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.EnableEdgeGeometry.ToString()); ImGuiUtil.DrawTableColumn("Flags 1"); - ImGuiUtil.DrawTableColumn(file.Flags1.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.Flags1.ToString()); ImGuiUtil.DrawTableColumn("Flags 2"); - ImGuiUtil.DrawTableColumn(file.Flags2.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.Flags2.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(file.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Bounding Boxes"); - ImGuiUtil.DrawTableColumn(file.BoneBoundingBoxes.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.BoneBoundingBoxes.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Tables"); - ImGuiUtil.DrawTableColumn(file.BoneTables.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.BoneTables.Length.ToString()); ImGuiUtil.DrawTableColumn("Element IDs"); - ImGuiUtil.DrawTableColumn(file.ElementIds.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.ElementIds.Length.ToString()); ImGuiUtil.DrawTableColumn("Extra LoDs"); - ImGuiUtil.DrawTableColumn(file.ExtraLods.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.ExtraLods.Length.ToString()); ImGuiUtil.DrawTableColumn("Meshes"); - ImGuiUtil.DrawTableColumn(file.Meshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.Meshes.Length.ToString()); ImGuiUtil.DrawTableColumn("Shape Meshes"); - ImGuiUtil.DrawTableColumn(file.ShapeMeshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.ShapeMeshes.Length.ToString()); ImGuiUtil.DrawTableColumn("LoDs"); - ImGuiUtil.DrawTableColumn(file.Lods.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.Lods.Length.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(file.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Stack Size"); - ImGuiUtil.DrawTableColumn(file.StackSize.ToString()); - for (var lod = 0; lod < file.Lods.Length; lod++) + ImGuiUtil.DrawTableColumn(_lastFile.StackSize.ToString()); + foreach (var (triCount, lod) in _lodTriCount.WithIndex()) { - ImGuiUtil.DrawTableColumn("LoD " + lod + " Triangle Count"); - ImGuiUtil.DrawTableColumn(GetTriangleCountForLod(file, lod).ToString()); + ImGuiUtil.DrawTableColumn($"LOD #{lod + 1} Triangle Count"); + ImGuiUtil.DrawTableColumn(triCount.ToString()); } } } @@ -519,36 +542,36 @@ public partial class ModEditWindow using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen)) { if (materials) - foreach (var material in file.Materials) + foreach (var material in _lastFile.Materials) ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) - foreach (var attribute in file.Attributes) + foreach (var attribute in _lastFile.Attributes) ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { if (bones) - foreach (var bone in file.Bones) + foreach (var bone in _lastFile.Bones) ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { if (shapes) - foreach (var shape in file.Shapes) + foreach (var shape in _lastFile.Shapes) ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); } - if (file.RemainingData.Length > 0) + if (_lastFile.RemainingData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {file.RemainingData.Length})###AdditionalData"); + using var t = ImRaii.TreeNode($"Additional Data (Size: {_lastFile.RemainingData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', file.RemainingData.Select(c => $"{c:X2}"))); + ImGuiUtil.TextWrapped(string.Join(' ', _lastFile.RemainingData.Select(c => $"{c:X2}"))); } return false; @@ -562,7 +585,7 @@ public partial class ModEditWindow private static long GetTriangleCountForLod(MdlFile model, int lod) { - var vertSum = 0u; + var vertSum = 0u; var meshIndex = model.Lods[lod].MeshIndex; var meshCount = model.Lods[lod].MeshCount; From cc88738e3e28232b6735e3279240b833bccb5b1c Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 21 Mar 2024 20:33:37 +0000 Subject: [PATCH 1577/2451] [CI] Updating repo.json for testing_1.0.2.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9487b2b3..3aac5de6 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.1", + "TestingAssemblyVersion": "1.0.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1a9239a358fbbd0e423f606995ab8cdbfe484105 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Mar 2024 23:02:07 +0100 Subject: [PATCH 1578/2451] Test improving object manager. --- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester.cs | 6 +++--- Penumbra/Api/PenumbraApi.cs | 6 +++--- .../Interop/Hooks/Animation/LoadTimelineResources.cs | 2 +- Penumbra/Interop/Hooks/Animation/SomePapLoad.cs | 2 +- Penumbra/Interop/PathResolving/DrawObjectState.cs | 9 ++++----- Penumbra/Interop/Services/RedrawService.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 6 +++--- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 529e1811..9a1e5e72 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 529e18115023732794994bfb8df4818b68951ea4 +Subproject commit 9a1e5e7294a6929ee1cc72b1b5d76e12c000c327 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index e1dd81cb..30f3c80f 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -442,8 +442,8 @@ public class IpcTester : IDisposable DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); var tmp = _redrawIndex; ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.Count)) - _redrawIndex = Math.Clamp(tmp, 0, _objects.Count); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount)) + _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount); ImGui.SameLine(); if (ImGui.Button("Redraw##Index")) @@ -460,7 +460,7 @@ public class IpcTester : IDisposable private void SetLastRedrawn(IntPtr address, int index) { if (index < 0 - || index > _objects.Count + || index > _objects.TotalCount || address == IntPtr.Zero || _objects[index].Address != address) _lastRedrawnString = "Invalid"; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5146383d..c8be90aa 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -929,7 +929,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (actorIndex < 0 || actorIndex >= _objects.Count) + if (actorIndex < 0 || actorIndex >= _objects.TotalCount) return PenumbraApiEc.InvalidArgument; var identifier = _actors.FromObject(_objects[actorIndex], out _, false, false, true); @@ -1166,7 +1166,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { collection = _collectionManager.Active.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Count) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) return false; var ptr = _objects[gameObjectIdx]; @@ -1180,7 +1180,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.Count) + if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) return ActorIdentifier.Invalid; var ptr = _objects[gameObjectIdx]; diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 4bae084e..018892a0 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -64,7 +64,7 @@ public sealed unsafe class LoadTimelineResources : FastHook**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; var idx = getGameObjectIdx(timeline); - if (idx >= 0 && idx < objects.Count) + if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; return obj.Valid ? resolver.IdentifyCollection(obj.AsObject, true) : ResolveData.Invalid; diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index a4563bf3..fad1f819 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -35,7 +35,7 @@ public sealed unsafe class SomePapLoad : FastHook if (timelinePtr != nint.Zero) { var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); - if (actorIdx >= 0 && actorIdx < _objects.Count) + if (actorIdx >= 0 && actorIdx < _objects.TotalCount) { var newData = _collectionResolver.IdentifyCollection(_objects[actorIdx].AsObject, true); var last = _state.SetAnimationData(newData); diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 784ac3fb..2a9ec7a9 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -94,11 +94,10 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary private unsafe void InitializeDrawObjects() { - for (var i = 0; i < _objects.Count; ++i) - { - var ptr = _objects[i]; - if (ptr is { IsCharacter: true, Model.Valid: true }) - IterateDrawObjectTree((Object*)ptr.Model.Address, ptr, false, false); + foreach(var actor in _objects) + { + if (actor is { IsCharacter: true, Model.Valid: true }) + IterateDrawObjectTree((Object*)actor.Model.Address, actor, false, false); } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index a6fec0b5..21ecfd4f 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -388,7 +388,7 @@ public sealed unsafe partial class RedrawService : IDisposable public void RedrawObject(int tableIndex, RedrawType settings) { - if (tableIndex >= 0 && tableIndex < _objects.Count) + if (tableIndex >= 0 && tableIndex < _objects.TotalCount) RedrawObject(_objects.GetDalamudObject(tableIndex), settings); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 2003f0ef..06f1d126 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -431,16 +431,16 @@ public class DebugTab : Window, ITab DrawSpecial("Current Card", _actors.GetCardPlayer()); DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - foreach (var obj in _objects.Objects) + foreach (var obj in _objects) { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); - var identifier = _actors.FromObject(obj, false, true, false); + var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); + var id = obj.AsObject->ObjectKind ==(byte) ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.AsObject->DataID}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } From 8fded888137117823f521d6d4a8e8d4d826db87d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 21 Mar 2024 23:50:55 +0100 Subject: [PATCH 1579/2451] Update. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9a1e5e72..66687643 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9a1e5e7294a6929ee1cc72b1b5d76e12c000c327 +Subproject commit 66687643da2163c938575ad6949c8d0fbd03afe7 From 5ea140db980ff112149fdb0dbc7cf069a68f1ec3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 00:45:52 +0100 Subject: [PATCH 1580/2451] Add new draw events. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 14 ++++++++++++- Penumbra/Communication/PostEnabledDraw.cs | 18 +++++++++++++++++ .../Communication/PreSettingsTabBarDraw.cs | 20 +++++++++++++++++++ Penumbra/Penumbra.cs | 11 ++++++++++ Penumbra/Services/CommunicatorService.cs | 8 ++++++++ Penumbra/UI/ModsTab/ModPanelHeader.cs | 16 ++++++++++++--- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 6 ++++-- 8 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 Penumbra/Communication/PostEnabledDraw.cs create mode 100644 Penumbra/Communication/PreSettingsTabBarDraw.cs diff --git a/Penumbra.Api b/Penumbra.Api index d2a1406b..a28a2537 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit d2a1406bc32f715c0687613f02e3f74caf7ceea9 +Subproject commit a28a2537c0ff030cf09f740bff3b1a51ac0b41f6 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index c8be90aa..da1eafd0 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -33,12 +33,24 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 23); + => (4, 24); + + public event Action? PreSettingsTabBarDraw + { + add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); + remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); + } public event Action? PreSettingsPanelDraw { add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); + } + + public event Action? PostEnabledDraw + { + add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default); + remove => _communicator.PostEnabledDraw.Unsubscribe(value!); } public event Action? PostSettingsPanelDraw diff --git a/Penumbra/Communication/PostEnabledDraw.cs b/Penumbra/Communication/PostEnabledDraw.cs new file mode 100644 index 00000000..68637442 --- /dev/null +++ b/Penumbra/Communication/PostEnabledDraw.cs @@ -0,0 +1,18 @@ +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered after the Enabled Checkbox line in settings is drawn, but before options are drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// +/// +public sealed class PostEnabledDraw() : EventWrapper(nameof(PostEnabledDraw)) +{ + public enum Priority + { + /// + Default = 0, + } +} diff --git a/Penumbra/Communication/PreSettingsTabBarDraw.cs b/Penumbra/Communication/PreSettingsTabBarDraw.cs new file mode 100644 index 00000000..2c14cdf1 --- /dev/null +++ b/Penumbra/Communication/PreSettingsTabBarDraw.cs @@ -0,0 +1,20 @@ +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered before the settings tab bar for a mod is drawn, after the title group is drawn. +/// +/// Parameter is the identifier (directory name) of the currently selected mod. +/// is the total width of the header group. +/// is the width of the title box. +/// +/// +public sealed class PreSettingsTabBarDraw() : EventWrapper(nameof(PreSettingsTabBarDraw)) +{ + public enum Priority + { + /// + Default = 0, + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ff068928..34451dfa 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -95,6 +95,17 @@ public class Penumbra : IDalamudPlugin if (_characterUtility.Ready) _residentResources.Reload(); + + var api = _services.GetService(); + api.PostEnabledDraw += ImGui.TextUnformatted; + api.PreSettingsTabBarDraw += (name, width, titleWidth) => + { + ImGui.TextUnformatted(width.ToString()); + ImGui.TextUnformatted(titleWidth.ToString()); + ImGui.TextUnformatted(ImGui.GetContentRegionMax().X.ToString()); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (width - titleWidth) / 2); + ImGui.Button("Test", new Vector2(titleWidth, 0)); + }; } catch (Exception ex) { diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index da852855..cacbe689 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -57,9 +57,15 @@ public class CommunicatorService : IDisposable, IService /// public readonly EnabledChanged EnabledChanged = new(); + /// + public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = new(); + /// public readonly PreSettingsPanelDraw PreSettingsPanelDraw = new(); + /// + public readonly PostEnabledDraw PostEnabledDraw = new(); + /// public readonly PostSettingsPanelDraw PostSettingsPanelDraw = new(); @@ -91,7 +97,9 @@ public class CommunicatorService : IDisposable, IService ModSettingChanged.Dispose(); CollectionInheritanceChanged.Dispose(); EnabledChanged.Dispose(); + PreSettingsTabBarDraw.Dispose(); PreSettingsPanelDraw.Dispose(); + PostEnabledDraw.Dispose(); PostSettingsPanelDraw.Dispose(); ChangedItemHover.Dispose(); ChangedItemClick.Dispose(); diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index ed499b4f..05f47809 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -32,9 +32,14 @@ public class ModPanelHeader : IDisposable /// public void Draw() { - var offset = DrawModName(); - DrawVersion(offset); - DrawSecondRow(offset); + using (ImRaii.Group()) + { + var offset = DrawModName(); + DrawVersion(offset); + DrawSecondRow(offset); + } + + _communicator.PreSettingsTabBarDraw.Invoke(_mod.Identifier, ImGui.GetItemRectSize().X, _nameWidth); } /// @@ -43,6 +48,7 @@ public class ModPanelHeader : IDisposable /// public void UpdateModData(Mod mod) { + _mod = mod; // Name var name = $" {mod.Name} "; if (name != _modName) @@ -90,6 +96,7 @@ public class ModPanelHeader : IDisposable } // Header data. + private Mod _mod = null!; private string _modName = string.Empty; private string _modAuthor = string.Empty; private string _modVersion = string.Empty; @@ -103,6 +110,8 @@ public class ModPanelHeader : IDisposable private float _modWebsiteButtonWidth; private float _secondRowWidth; + private float _nameWidth; + /// /// Draw the mod name in the game font with a 2px border, centered, /// with at least the width of the version space to each side. @@ -124,6 +133,7 @@ public class ModPanelHeader : IDisposable using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); using var f = _nameFont.Push(); ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); + _nameWidth = ImGui.GetItemRectSize().X; return offset; } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 5c7ddbf3..b14cad01 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -61,7 +61,7 @@ public class ModPanelSettingsTab : ITab DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.ModPath.Name); + _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); DrawEnabledInput(); _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); @@ -69,6 +69,8 @@ public class ModPanelSettingsTab : ITab _tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); + _communicator.PostEnabledDraw.Invoke(_selector.Selected!.Identifier); + if (_selector.Selected!.Groups.Count > 0) { var useDummy = true; @@ -98,7 +100,7 @@ public class ModPanelSettingsTab : ITab } UiHelpers.DefaultLineSpace(); - _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.ModPath.Name); + _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); } /// Draw a big red bar if the current setting is inherited. From ca95b9c14c1eb487a1e38a6aa1d7272e7549ecba Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 00:55:53 +0100 Subject: [PATCH 1581/2451] PackageVersion. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index a28a2537..8787efc8 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a28a2537c0ff030cf09f740bff3b1a51ac0b41f6 +Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42 From 26730efc1bee0a415c433fd06d8cbf53ac93d3b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 13:31:38 +0100 Subject: [PATCH 1582/2451] Update OtterGui --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 2bbb9b2a..4e06921d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2bbb9b2a8a4479461b252594b9d1b788b551c13c +Subproject commit 4e06921da239788331a4527aa6a2943cf0e809fe From d171cea627c65a22b9fed2671b55b140085e4ca4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 13:36:55 +0100 Subject: [PATCH 1583/2451] Remove DI Reference. --- Penumbra.CrashHandler/Program.cs | 2 +- Penumbra/Penumbra.csproj | 1 - Penumbra/packages.lock.json | 25 ++++++++++++------------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index 94e90bfc..3bc461f7 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -21,7 +21,7 @@ public class CrashHandler { exitCode = parent.ExitCode; } - catch (Exception ex) + catch { exitCode = -1; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index df18ff13..b07917e8 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -71,7 +71,6 @@ - diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 3f795631..68e1b880 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,15 +11,6 @@ "Unosquare.Swan.Lite": "3.0.0" } }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Direct", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0" - } - }, "SharpCompress": { "type": "Direct", "requested": "[0.33.0, )", @@ -47,10 +38,18 @@ "resolved": "3.1.3", "contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg==" }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, "SharpGLTF.Runtime": { "type": "Transitive", @@ -76,7 +75,7 @@ "ottergui": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "[7.0.0, )" + "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" } }, "penumbra.api": { @@ -89,7 +88,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[1.0.14, )", + "Penumbra.Api": "[1.0.15, )", "Penumbra.String": "[1.0.4, )" } }, From aabd988a7747fc974171298b28bb6e9458555969 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Mar 2024 12:38:55 +0000 Subject: [PATCH 1584/2451] [CI] Updating repo.json for testing_1.2.1.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3aac5de6..a359a4ce 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.2", + "TestingAssemblyVersion": "1.2.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d0d35a79387c19e40baefc20eb339505d0533767 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:39:46 +0100 Subject: [PATCH 1585/2451] Update repo.json --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a359a4ce..3aac5de6 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.2.1.2", + "TestingAssemblyVersion": "1.0.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8dcd0451babfb5354808279a0cca8b62c18a5490 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:43:38 +0100 Subject: [PATCH 1586/2451] Update repo.json --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3aac5de6..44ada6e8 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.2", + "TestingAssemblyVersion": "1.0.2.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6792ed4f9489f061f657a536c323d588e6d53d56 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 14:44:41 +0100 Subject: [PATCH 1587/2451] Remove the fucking test I'm a fucking moron. --- Penumbra/Penumbra.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 34451dfa..ff068928 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -95,17 +95,6 @@ public class Penumbra : IDalamudPlugin if (_characterUtility.Ready) _residentResources.Reload(); - - var api = _services.GetService(); - api.PostEnabledDraw += ImGui.TextUnformatted; - api.PreSettingsTabBarDraw += (name, width, titleWidth) => - { - ImGui.TextUnformatted(width.ToString()); - ImGui.TextUnformatted(titleWidth.ToString()); - ImGui.TextUnformatted(ImGui.GetContentRegionMax().X.ToString()); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (width - titleWidth) / 2); - ImGui.Button("Test", new Vector2(titleWidth, 0)); - }; } catch (Exception ex) { From 78a3ff177f8304c7ed9f0bc59b6f15985c6929c1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Mar 2024 13:46:45 +0000 Subject: [PATCH 1588/2451] [CI] Updating repo.json for testing_1.0.2.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 44ada6e8..2af4a645 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.3", + "TestingAssemblyVersion": "1.0.2.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4175a582b8ece7d088a27c6fd95cd2be5c2e54ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 15:15:41 +0100 Subject: [PATCH 1589/2451] Add IPC Providers because I'm still a fucking moron. --- Penumbra/Api/PenumbraIpcProviders.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index d478b675..78887156 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -30,7 +30,9 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider ModDirectoryChanged; // UI + internal readonly EventProvider PreSettingsTabBarDraw; internal readonly EventProvider PreSettingsDraw; + internal readonly EventProvider PostEnabledDraw; internal readonly EventProvider PostSettingsDraw; internal readonly EventProvider ChangedItemTooltip; internal readonly EventProvider ChangedItemClick; @@ -130,8 +132,8 @@ public class PenumbraIpcProviders : IDisposable FuncProvider>> GetPlayerResourcesOfType; - internal readonly FuncProvider GetGameObjectResourceTrees; - internal readonly FuncProvider> GetPlayerResourceTrees; + internal readonly FuncProvider GetGameObjectResourceTrees; + internal readonly FuncProvider> GetPlayerResourceTrees; public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) @@ -153,7 +155,11 @@ public class PenumbraIpcProviders : IDisposable ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a); // UI - PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); + PreSettingsTabBarDraw = + Ipc.PreSettingsTabBarDraw.Provider(pi, a => Api.PreSettingsTabBarDraw += a, a => Api.PreSettingsTabBarDraw -= a); + PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); + PostEnabledDraw = + Ipc.PostEnabledDraw.Provider(pi, a => Api.PostEnabledDraw += a, a => Api.PostEnabledDraw -= a); PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a); ChangedItemTooltip = Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip); @@ -278,7 +284,9 @@ public class PenumbraIpcProviders : IDisposable ModDirectoryChanged.Dispose(); // UI + PreSettingsTabBarDraw.Dispose(); PreSettingsDraw.Dispose(); + PostEnabledDraw.Dispose(); PostSettingsDraw.Dispose(); ChangedItemTooltip.Dispose(); ChangedItemClick.Dispose(); From 12532dee2810c9f770a467a2f61c0c007226e373 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Mar 2024 14:17:47 +0000 Subject: [PATCH 1590/2451] [CI] Updating repo.json for testing_1.0.2.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2af4a645..5a51afa2 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.4", + "TestingAssemblyVersion": "1.0.2.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From efdd5a824b16fb9e5a423062b644cea038a9275f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 26 Mar 2024 02:29:07 +0100 Subject: [PATCH 1591/2451] Make human.pbd moddable --- Penumbra/Collections/Cache/CollectionCache.cs | 31 ++++-- .../Interop/ResourceLoading/ResourceLoader.cs | 28 ++++++ .../ResourceLoading/ResourceService.cs | 9 +- .../Interop/ResourceTree/ResolveContext.cs | 9 ++ Penumbra/Interop/ResourceTree/ResourceTree.cs | 18 +++- .../Interop/SafeHandles/SafeResourceHandle.cs | 10 +- Penumbra/Interop/Services/CharacterUtility.cs | 8 ++ .../Services/PreBoneDeformerReplacer.cs | 95 +++++++++++++++++++ .../Interop/Structs/CharacterUtilityData.cs | 4 + Penumbra/Penumbra.cs | 1 + Penumbra/Services/StaticServiceManager.cs | 1 + 11 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 Penumbra/Interop/Services/PreBoneDeformerReplacer.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 9a0e525b..c2c215aa 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -8,6 +8,9 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; +using Penumbra.Interop.SafeHandles; +using System.IO; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Collections.Cache; @@ -20,13 +23,14 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// public class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, object?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly ConcurrentDictionary LoadedResources = new(); + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -136,6 +140,13 @@ public class CollectionCache : IDisposable public void RemoveMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); + /// Invalidates caches subsequently to a resolved file being modified. + private void InvalidateResolvedFile(Utf8GamePath path) + { + if (LoadedResources.Remove(path, out var handle)) + handle.Dispose(); + } + /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -148,17 +159,20 @@ public class CollectionCache : IDisposable if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } else { + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); } } else if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } @@ -181,6 +195,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.Remove(path, out var mp)) { + InvalidateResolvedFile(path); if (mp.Mod != mod) Penumbra.Log.Warning( $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); @@ -295,6 +310,7 @@ public class CollectionCache : IDisposable if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { ModData.AddPath(mod, path); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -309,6 +325,7 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 8ccdfa80..1b467a8c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; @@ -39,6 +40,33 @@ public unsafe class ResourceLoader : IDisposable return ret; } + /// Load a resource for a given path and a specific collection. + public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + { + _resolvedData = resolveData; + var ret = _resources.GetSafeResource(category, type, path); + _resolvedData = ResolveData.Invalid; + return ret; + } + + public SafeResourceHandle LoadCacheableSafeResource(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData resolveData) + { + var cache = resolveData.ModCollection._cache; + if (cache == null) + return LoadResolvedSafeResource(category, type, path.Path, resolveData); + + if (cache.LoadedResources.TryGetValue(path, out var cached)) + return cached.Clone(); + + var ret = LoadResolvedSafeResource(category, type, path.Path, resolveData); + + cached = ret.Clone(); + if (!cache.LoadedResources.TryAdd(path, cached)) + cached.Dispose(); + + return ret; + } + /// The function to use to resolve a given path. public Func ResolvePath = null!; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 6fb2e560..e3338e6c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -4,10 +4,12 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.GameData; +using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; +using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.ResourceLoading; @@ -25,11 +27,11 @@ public unsafe class ResourceService : IDisposable _getResourceAsyncHook.Enable(); _resourceHandleDestructorHook.Enable(); _incRefHook = interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, + (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); _incRefHook.Enable(); _decRefHook = interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, + (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); _decRefHook.Enable(); } @@ -41,6 +43,9 @@ public unsafe class ResourceService : IDisposable &category, &type, &hash, path.Path, null, false); } + public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, ByteString path) + => new((CSResourceHandle*)GetResource(category, type, path), false); + public void Dispose() { _getResourceSyncHook.Dispose(); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d1701f47..615ef2b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -144,6 +145,14 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } + public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd) + { + if (pbd == null) + return null; + + return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath); + } + public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { if (tex == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 24112a9f..15546ed3 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -6,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; @@ -155,6 +155,22 @@ public class ResourceTree { var genericContext = globalContext.CreateContext(&human->CharacterBase); + var cache = globalContext.Collection._cache; + if (cache != null && cache.LoadedResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + { + var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); + if (pbdNode != null) + { + if (globalContext.WithUiData) + { + pbdNode = pbdNode.Clone(); + pbdNode.FallbackName = "Racial Deformer"; + pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(pbdNode); + } + } + var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId != 0 ? GamePaths.Human.Decal.FaceDecalPath(decalId) diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs index 1f788a39..a5e73867 100644 --- a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -1,8 +1,8 @@ -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; namespace Penumbra.Interop.SafeHandles; -public unsafe class SafeResourceHandle : SafeHandle +public unsafe class SafeResourceHandle : SafeHandle, ICloneable { public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; @@ -21,6 +21,12 @@ public unsafe class SafeResourceHandle : SafeHandle SetHandle((nint)handle); } + public SafeResourceHandle Clone() + => new(ResourceHandle, true); + + object ICloneable.Clone() + => Clone(); + public static SafeResourceHandle CreateInvalid() => new(null, false); diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 699b59e0..da04bf90 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -28,6 +28,7 @@ public unsafe class CharacterUtility : IDisposable public bool Ready { get; private set; } public event Action LoadingFinished; + public nint DefaultHumanPbdResource { get; private set; } public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } public nint DefaultSkinShpkResource { get; private set; } @@ -88,6 +89,12 @@ public unsafe class CharacterUtility : IDisposable anyMissing |= !_lists[i].Ready; } + if (DefaultHumanPbdResource == nint.Zero) + { + DefaultHumanPbdResource = (nint)Address->HumanPbdResource; + anyMissing |= DefaultHumanPbdResource == nint.Zero; + } + if (DefaultTransparentResource == nint.Zero) { DefaultTransparentResource = (nint)Address->TransparentTexResource; @@ -151,6 +158,7 @@ public unsafe class CharacterUtility : IDisposable foreach (var list in _lists) list.Dispose(); + Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs new file mode 100644 index 00000000..44c6541e --- /dev/null +++ b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs @@ -0,0 +1,95 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class PreBoneDeformerReplacer : IDisposable +{ + public static readonly Utf8GamePath PreBoneDeformerPath = + Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + // Approximate name guesses. + private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + + private readonly Hook _humanSetupScalingHook; + private readonly Hook _humanCreateDeformerHook; + + private readonly CharacterUtility _utility; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceLoader _resourceLoader; + private readonly IFramework _framework; + + public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, IGameInteropProvider interop, IFramework framework) + { + interop.InitializeFromAttributes(this); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = interop.HookFromAddress(_humanVTable[57], SetupScaling); + _humanCreateDeformerHook = interop.HookFromAddress(_humanVTable[91], CreateDeformer); + _humanSetupScalingHook.Enable(); + _humanCreateDeformerHook.Enable(); + } + + public void Dispose() + { + _humanCreateDeformerHook.Dispose(); + _humanSetupScalingHook.Dispose(); + } + + private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) + { + var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); + return _resourceLoader.LoadCacheableSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); + } + + private void SetupScaling(CharacterBase* drawObject, uint slotIndex) + { + if (!_framework.IsInFrameworkUpdateThread) + Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + + using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + try + { + if (!preBoneDeformer.IsInvalid) + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; + _humanSetupScalingHook.Original(drawObject, slotIndex); + } + finally + { + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + } + } + + private void* CreateDeformer(CharacterBase* drawObject, uint slotIndex) + { + if (!_framework.IsInFrameworkUpdateThread) + Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + + using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + try + { + if (!preBoneDeformer.IsInvalid) + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; + return _humanCreateDeformerHook.Original(drawObject, slotIndex); + } + finally + { + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 08857292..22150cc1 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,6 +5,7 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { + public const int IndexHumanPbd = 63; public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; public const int IndexSkinShpk = 76; @@ -72,6 +73,9 @@ public unsafe struct CharacterUtilityData public ResourceHandle* EqdpResource(GenderRace raceCode, bool accessory) => Resource((int)EqdpIdx(raceCode, accessory)); + [FieldOffset(8 + IndexHumanPbd * 8)] + public ResourceHandle* HumanPbdResource; + [FieldOffset(8 + (int)MetaIndex.HumanCmp * 8)] public ResourceHandle* HumanCmpResource; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ff068928..c34a7771 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,6 +77,7 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); + _services.GetService(); _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 66c90e84..6f85ddd2 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -132,6 +132,7 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) From 3066bf84d5fc4398e642d21c0449ba7a2300e6c2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 26 Mar 2024 02:32:33 +0100 Subject: [PATCH 1592/2451] Where did these `using`s even come from? --- Penumbra/Collections/Cache/CollectionCache.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index c2c215aa..00968175 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -9,8 +9,6 @@ using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Interop.SafeHandles; -using System.IO; -using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Collections.Cache; From e793e7793b1e4eeae991ba0beb4f7f49745b2bd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 15:19:19 +0100 Subject: [PATCH 1593/2451] Fix model import resetting dirty flag. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 3672dce7..737c41d9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -35,39 +35,28 @@ public partial class ModEditWindow private void UpdateFile(MdlFile file, bool force) { - if (file == _lastFile) + if (file == _lastFile && !force) + return; + + _lastFile = file; + var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_subMeshAttributeTagWidgets.Count != subMeshTotal) { - if (force) - UpdateMeshes(); - } - else - { - UpdateMeshes(); - _lastFile = file; - _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + _subMeshAttributeTagWidgets.Clear(); + _subMeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) + ); } - return; - - void UpdateMeshes() - { - var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_subMeshAttributeTagWidgets.Count != subMeshTotal) - { - _subMeshAttributeTagWidgets.Clear(); - _subMeshAttributeTagWidgets.AddRange( - Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) - ); - } - } + _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); } private bool DrawModelPanel(MdlTab tab, bool disabled) { - UpdateFile(tab.Mdl, tab.Dirty); + var ret = tab.Dirty; + UpdateFile(tab.Mdl, ret); DrawImportExport(tab, disabled); - var ret = tab.Dirty; ret |= DrawModelMaterialDetails(tab, disabled); if (ImGui.CollapsingHeader($"Meshes ({_lastFile.Meshes.Length})###meshes")) From 1ba5011bfa5120821a30a39a8953f33329a59bc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 17:37:17 +0100 Subject: [PATCH 1594/2451] Small changes. --- Penumbra/Collections/Cache/CollectionCache.cs | 65 +++++++++---------- .../Cache/CollectionCacheManager.cs | 8 ++- .../Collections/Cache/CustomResourceCache.cs | 49 ++++++++++++++ .../Interop/ResourceLoading/ResourceLoader.cs | 18 ----- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- .../Services/PreBoneDeformerReplacer.cs | 34 +++++----- .../Services/ShaderReplacementFixer.cs | 33 +++++----- Penumbra/Penumbra.cs | 2 - Penumbra/Services/StaticServiceManager.cs | 4 +- 9 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 Penumbra/Collections/Cache/CustomResourceCache.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 00968175..6b2b688b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -8,7 +8,6 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; -using Penumbra.Interop.SafeHandles; namespace Penumbra.Collections.Cache; @@ -19,16 +18,16 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// The Cache contains all required temporary data to use a collection. /// It will only be setup if a collection gets activated in any way. /// -public class CollectionCache : IDisposable +public sealed class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly ConcurrentDictionary LoadedResources = new(); - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, object?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly CustomResourceCache CustomResources; + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -39,7 +38,7 @@ public class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -56,16 +55,21 @@ public class CollectionCache : IDisposable // The cache reacts through events on its collection changing. public CollectionCache(CollectionCacheManager manager, ModCollection collection) { - _manager = manager; - _collection = collection; - Meta = new MetaCache(manager.MetaFileManager, _collection); + _manager = manager; + _collection = collection; + Meta = new MetaCache(manager.MetaFileManager, _collection); + CustomResources = new CustomResourceCache(manager.ResourceLoader); } public void Dispose() - => Meta.Dispose(); + { + Meta.Dispose(); + CustomResources.Dispose(); + GC.SuppressFinalize(this); + } ~CollectionCache() - => Meta.Dispose(); + => Dispose(); // Resolve a given game path according to this collection. public FullPath? ResolvePath(Utf8GamePath gameResourcePath) @@ -74,7 +78,7 @@ public class CollectionCache : IDisposable return null; if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists) + || candidate.Path is { IsRooted: true, Exists: false }) return null; return candidate.Path; @@ -102,7 +106,7 @@ public class CollectionCache : IDisposable public HashSet[] ReverseResolvePaths(IReadOnlyCollection fullPaths) { if (fullPaths.Count == 0) - return Array.Empty>(); + return []; var ret = new HashSet[fullPaths.Count]; var dict = new Dictionary(fullPaths.Count); @@ -110,8 +114,8 @@ public class CollectionCache : IDisposable { dict[new FullPath(path)] = idx; ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8) - ? new HashSet { utf8 } - : new HashSet(); + ? [utf8] + : []; } foreach (var (game, full) in ResolvedFiles) @@ -138,13 +142,6 @@ public class CollectionCache : IDisposable public void RemoveMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); - /// Invalidates caches subsequently to a resolved file being modified. - private void InvalidateResolvedFile(Utf8GamePath path) - { - if (LoadedResources.Remove(path, out var handle)) - handle.Dispose(); - } - /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -157,20 +154,20 @@ public class CollectionCache : IDisposable if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } else { - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); } } else if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } @@ -193,7 +190,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.Remove(path, out var mp)) { - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); if (mp.Mod != mod) Penumbra.Log.Warning( $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); @@ -308,7 +305,7 @@ public class CollectionCache : IDisposable if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { ModData.AddPath(mod, path); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -323,14 +320,14 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } catch (Exception ex) { Penumbra.Log.Error( - $"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); + $"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 94b3ef5a..5a6b5593 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -4,6 +4,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -21,8 +22,8 @@ public class CollectionCacheManager : IDisposable private readonly CollectionStorage _storage; private readonly ActiveCollections _active; internal readonly ResolvedFileChanged ResolvedFileChanged; - - internal readonly MetaFileManager MetaFileManager; + internal readonly MetaFileManager MetaFileManager; + internal readonly ResourceLoader ResourceLoader; private readonly ConcurrentQueue _changeQueue = new(); @@ -35,7 +36,7 @@ public class CollectionCacheManager : IDisposable => _storage.Where(c => c.HasCache); public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, - MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader) { _framework = framework; _communicator = communicator; @@ -44,6 +45,7 @@ public class CollectionCacheManager : IDisposable MetaFileManager = metaFileManager; _active = active; _storage = storage; + ResourceLoader = resourceLoader; ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs new file mode 100644 index 00000000..46c28393 --- /dev/null +++ b/Penumbra/Collections/Cache/CustomResourceCache.cs @@ -0,0 +1,49 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.String.Classes; + +namespace Penumbra.Collections.Cache; + +/// A cache for resources owned by a collection. +public sealed class CustomResourceCache(ResourceLoader loader) + : ConcurrentDictionary, IDisposable +{ + /// Invalidate an existing resource by clearing it from the cache and disposing it. + public void Invalidate(Utf8GamePath path) + { + if (TryRemove(path, out var handle)) + handle.Dispose(); + } + + public void Dispose() + { + foreach (var handle in Values) + handle.Dispose(); + Clear(); + } + + /// Get the requested resource either from the cached resource, or load a new one if it does not exist. + public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data) + { + if (TryGetClonedValue(path, out var handle)) + return handle; + + handle = loader.LoadResolvedSafeResource(category, type, path.Path, data); + var clone = handle.Clone(); + if (!TryAdd(path, clone)) + clone.Dispose(); + return handle; + } + + /// Get a cloned cached resource if it exists. + private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle) + { + if (!TryGetValue(path, out handle)) + return false; + + handle = handle.Clone(); + return true; + } +} diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 1b467a8c..6c2c83b3 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -49,24 +49,6 @@ public unsafe class ResourceLoader : IDisposable return ret; } - public SafeResourceHandle LoadCacheableSafeResource(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData resolveData) - { - var cache = resolveData.ModCollection._cache; - if (cache == null) - return LoadResolvedSafeResource(category, type, path.Path, resolveData); - - if (cache.LoadedResources.TryGetValue(path, out var cached)) - return cached.Clone(); - - var ret = LoadResolvedSafeResource(category, type, path.Path, resolveData); - - cached = ret.Clone(); - if (!cache.LoadedResources.TryAdd(path, cached)) - cached.Dispose(); - - return ret; - } - /// The function to use to resolve a given path. public Func ResolvePath = null!; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 15546ed3..dac86e44 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -156,7 +156,7 @@ public class ResourceTree var genericContext = globalContext.CreateContext(&human->CharacterBase); var cache = globalContext.Collection._cache; - if (cache != null && cache.LoadedResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) { var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); if (pbdNode != null) diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs index 44c6541e..9f553257 100644 --- a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs @@ -1,10 +1,9 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; @@ -12,14 +11,11 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.Services; -public sealed unsafe class PreBoneDeformerReplacer : IDisposable +public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { public static readonly Utf8GamePath PreBoneDeformerPath = Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - // Approximate name guesses. private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); @@ -32,15 +28,16 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private readonly ResourceLoader _resourceLoader; private readonly IFramework _framework; - public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, IGameInteropProvider interop, IFramework framework) + public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, + IGameInteropProvider interop, IFramework framework, CharacterBaseVTables vTables) { interop.InitializeFromAttributes(this); - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = interop.HookFromAddress(_humanVTable[57], SetupScaling); - _humanCreateDeformerHook = interop.HookFromAddress(_humanVTable[91], CreateDeformer); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = interop.HookFromAddress(vTables.HumanVTable[57], SetupScaling); + _humanCreateDeformerHook = interop.HookFromAddress(vTables.HumanVTable[91], CreateDeformer); _humanSetupScalingHook.Enable(); _humanCreateDeformerHook.Enable(); } @@ -54,13 +51,17 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) { var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); - return _resourceLoader.LoadCacheableSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); + if (resolveData.ModCollection._cache is not { } cache) + return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData); + + return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } private void SetupScaling(CharacterBase* drawObject, uint slotIndex) { if (!_framework.IsInFrameworkUpdateThread) - Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + Penumbra.Log.Warning( + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try @@ -78,7 +79,8 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private void* CreateDeformer(CharacterBase* drawObject, uint slotIndex) { if (!_framework.IsInFrameworkUpdateThread) - Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + Penumbra.Log.Warning( + $"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index 26906ace..3809ecbd 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; @@ -13,7 +14,7 @@ using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRen namespace Penumbra.Interop.Services; -public sealed unsafe class ShaderReplacementFixer : IDisposable +public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService { public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; @@ -21,11 +22,10 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable public static ReadOnlySpan CharacterGlassShpkName => "characterglass.shpk"u8; - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); - private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); + + private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, + CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); private readonly Hook _humanOnRenderMaterialHook; @@ -59,14 +59,15 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable => _moddedCharacterGlassShpkCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, IGameInteropProvider interop) + CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables) { interop.InitializeFromAttributes(this); - _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _modelRenderer = modelRenderer; - _communicator = communicator; - _humanOnRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _modelRenderer = modelRenderer; + _communicator = communicator; + _humanOnRenderMaterialHook = + interop.HookFromAddress(vTables.HumanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); _humanOnRenderMaterialHook.Enable(); @@ -82,7 +83,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable _moddedCharacterGlassShpkMaterials.Clear(); _moddedSkinShpkMaterials.Clear(); _moddedCharacterGlassShpkCount = 0; - _moddedSkinShpkCount = 0; + _moddedSkinShpkCount = 0; } public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() @@ -106,16 +107,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable var shpkName = mtrl->ShpkNameSpan; if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) - { if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) Interlocked.Increment(ref _moddedSkinShpkCount); - } if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) - { if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) Interlocked.Increment(ref _moddedCharacterGlassShpkCount); - } } private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) @@ -159,9 +156,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable } } - private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) + private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, + CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { - // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. if (!Enabled || _moddedCharacterGlassShpkCount == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c34a7771..b76780c0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,8 +77,6 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); - _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 6f85ddd2..9e6071b4 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -131,9 +131,7 @@ public static class StaticServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() From b04cb343dd5aacd4a6134ad28e7dca1154cce979 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 19:32:10 +0100 Subject: [PATCH 1595/2451] Make setting for crash handler. --- Penumbra/Configuration.cs | 18 +++--- Penumbra/Services/CrashHandlerService.cs | 6 +- Penumbra/UI/Tabs/SettingsTab.cs | 77 +++++++++++++++--------- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9c0b4f2d..f91e0534 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -33,12 +33,12 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool UseCrashHandler { get; set; } = true; - public bool OpenWindowAtStart { get; set; } = false; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; - public bool HideUiWhenUiHidden { get; set; } = false; - public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool? UseCrashHandler { get; set; } = null; + public bool OpenWindowAtStart { get; set; } = false; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiWhenUiHidden { get; set; } = false; + public bool UseDalamudUiTextureRedirection { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; @@ -48,9 +48,9 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index c713d623..6025c2c0 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -36,7 +36,7 @@ public sealed class CrashHandlerService : IDisposable, IService _config = config; _validityChecker = validityChecker; - if (!_config.UseCrashHandler) + if (_config.UseCrashHandler ?? false) return; OpenEventWriter(); @@ -84,7 +84,7 @@ public sealed class CrashHandlerService : IDisposable, IService public void Enable() { - if (_config.UseCrashHandler) + if (_config.UseCrashHandler ?? false) return; _config.UseCrashHandler = true; @@ -97,7 +97,7 @@ public sealed class CrashHandlerService : IDisposable, IService public void Disable() { - if (!_config.UseCrashHandler) + if (!(_config.UseCrashHandler ?? false)) return; _config.UseCrashHandler = false; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 80fe6fb6..c524a840 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -43,6 +43,7 @@ public class SettingsTab : ITab private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; + private readonly CrashHandlerService _crashService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -53,7 +54,7 @@ public class SettingsTab : ITab Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, PredefinedTagManager predefinedTagConfig) + IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService) { _pluginInterface = pluginInterface; _config = config; @@ -74,6 +75,7 @@ public class SettingsTab : ITab if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; _predefinedTagManager = predefinedTagConfig; + _crashService = crashService; } public void DrawHeader() @@ -228,35 +230,35 @@ public class SettingsTab : ITab _newModDirectory = _config.ModDirectory; bool save, selected; - using (ImRaii.Group()) - { - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) - { - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) - .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); - save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, - RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); - } - - selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker(tt); - _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); - ImGui.SameLine(); - ImGui.TextUnformatted("Root Directory"); - ImGuiUtil.HoverTooltip(tt); + using (ImRaii.Group()) + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + + selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); } _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); @@ -704,6 +706,7 @@ public class SettingsTab : ITab if (!header) return; + DrawCrashHandler(); DrawMinimumDimensionConfig(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", @@ -721,6 +724,20 @@ public class SettingsTab : ITab ImGui.NewLine(); } + private void DrawCrashHandler() + { + Checkbox("Enable Penumbra Crash Logging (Experimental)", + "Enables Penumbra to launch a secondary process that records some game activity which may or may not help diagnosing Penumbra-related game crashes.", + _config.UseCrashHandler ?? false, + v => + { + if (v) + _crashService.Enable(); + else + _crashService.Disable(); + }); + } + private void DrawCompressionBox() { if (!_compactor.CanCompact) From 47c5187ad929468610f49ae04a4f534e21dc4a21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 19:34:30 +0100 Subject: [PATCH 1596/2451] Derp. --- Penumbra/Services/CrashHandlerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 6025c2c0..5423ec15 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -36,7 +36,7 @@ public sealed class CrashHandlerService : IDisposable, IService _config = config; _validityChecker = validityChecker; - if (_config.UseCrashHandler ?? false) + if (!(_config.UseCrashHandler ?? false)) return; OpenEventWriter(); From a39419288c1432f5fa9a9a7e1b7b97a8e48ea574 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 28 Mar 2024 18:36:31 +0000 Subject: [PATCH 1597/2451] [CI] Updating repo.json for testing_1.0.2.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5a51afa2..936adfc0 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.5", + "TestingAssemblyVersion": "1.0.2.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From de239578ccb83137d2c4f69ad2b7bd8781d686d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Mar 2024 14:44:08 +0100 Subject: [PATCH 1598/2451] Fix weird exception. --- Penumbra/Api/IpcTester.cs | 76 +++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 30f3c80f..898c5de3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -583,23 +583,18 @@ public class IpcTester : IDisposable } } - private class Resolve + private class Resolve(DalamudPluginInterface pi) { - private readonly DalamudPluginInterface _pi; - private string _currentResolvePath = string.Empty; private string _currentResolveCharacter = string.Empty; private string _currentReversePath = string.Empty; private int _currentReverseIdx; - private Task<(string[], string[][])> _task = Task.FromException<(string[], string[][])>(new Exception()); - - public Resolve(DalamudPluginInterface pi) - => _pi = pi; + private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); public void Draw() { - using var _ = ImRaii.TreeNode("Resolving"); - if (!_) + using var tree = ImRaii.TreeNode("Resolving"); + if (!tree) return; ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); @@ -613,28 +608,28 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve"); if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentResolveCharacter)); + ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(pi).Invoke(_currentResolvePath, _currentResolveCharacter)); DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentReverseIdx)); + ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(pi).Invoke(_currentResolvePath, _currentReverseIdx)); DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePath.Subscriber(_pi).Invoke(_currentReversePath, _currentResolveCharacter); + var list = Ipc.ReverseResolvePath.Subscriber(pi).Invoke(_currentReversePath, _currentResolveCharacter); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -646,7 +641,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePlayerPath.Subscriber(_pi).Invoke(_currentReversePath); + var list = Ipc.ReverseResolvePlayerPath.Subscriber(pi).Invoke(_currentReversePath); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -658,7 +653,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolveGameObjectPath.Subscriber(_pi).Invoke(_currentReversePath, _currentReverseIdx); + var list = Ipc.ReverseResolveGameObjectPath.Subscriber(pi).Invoke(_currentReversePath, _currentReverseIdx); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -668,19 +663,31 @@ public class IpcTester : IDisposable } var forwardArray = _currentResolvePath.Length > 0 - ? new[] - { - _currentResolvePath, - } + ? [_currentResolvePath] : Array.Empty(); var reverseArray = _currentReversePath.Length > 0 - ? new[] - { - _currentReversePath, - } + ? [_currentReversePath] : Array.Empty(); - string ConvertText((string[], string[][]) data) + DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (forwardArray.Length > 0 || reverseArray.Length > 0) + { + var ret = Ipc.ResolvePlayerPaths.Subscriber(pi).Invoke(forwardArray, reverseArray); + ImGui.TextUnformatted(ConvertText(ret)); + } + + DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); + if (ImGui.Button("Start")) + _task = Ipc.ResolvePlayerPathsAsync.Subscriber(pi).Invoke(forwardArray, reverseArray); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_task.Status.ToString()); + if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) + ImGui.SetTooltip(ConvertText(_task.Result)); + return; + + static string ConvertText((string[], string[][]) data) { var text = string.Empty; if (data.Item1.Length > 0) @@ -697,23 +704,6 @@ public class IpcTester : IDisposable return text; } - - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); - if (forwardArray.Length > 0 || reverseArray.Length > 0) - { - var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray); - ImGui.TextUnformatted(ConvertText(ret)); - } - - DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); - if (ImGui.Button("Start")) - _task = Ipc.ResolvePlayerPathsAsync.Subscriber(_pi).Invoke(forwardArray, reverseArray); - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_task.Status.ToString()); - if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) - ImGui.SetTooltip(ConvertText(_task.Result)); } } From b4b813fe5e05ef321cda034de6691d58cc8d5b9d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 29 Mar 2024 20:05:25 +0100 Subject: [PATCH 1599/2451] Advanced Editing minor improvements --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../UI/AdvancedWindow/ModEditWindow.Materials.cs | 3 ++- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- .../AdvancedWindow/ModEditWindow.ShaderPackages.cs | 12 +++++++++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 4e06921d..b70c7cc2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4e06921da239788331a4527aa6a2943cf0e809fe +Subproject commit b70c7cc2f6c71f80884de30a237cab201d7fe150 diff --git a/Penumbra.GameData b/Penumbra.GameData index 66687643..45679aa3 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 66687643da2163c938575ad6949c8d0fbd03afe7 +Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index fab41c7d..68b3717f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -170,7 +171,7 @@ public partial class ModEditWindow using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(file.AdditionalData); } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 737c41d9..03f276ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -560,7 +560,7 @@ public partial class ModEditWindow { using var t = ImRaii.TreeNode($"Additional Data (Size: {_lastFile.RemainingData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', _lastFile.RemainingData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(_lastFile.RemainingData); } return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 8d1c8cb7..070895b5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; +using OtterGui.Widgets; namespace Penumbra.UI.AdvancedWindow; @@ -172,11 +173,16 @@ public partial class ModEditWindow ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); - if (shader.AdditionalHeader.Length > 0) + if (shader.DeclaredInputs != 0) + ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + if (shader.UsedInputs != 0) + ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + + if (shader.AdditionalHeader.Length > 8) { using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); if (t2) - ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(shader.AdditionalHeader); } if (tab.Shpk.Disassembled) @@ -549,7 +555,7 @@ public partial class ModEditWindow { using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(tab.Shpk.AdditionalData); } } From 5cebddb0ab680fa38edb62e3b5cc6cfe23203388 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Mar 2024 14:59:31 +0100 Subject: [PATCH 1600/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b70c7cc2..f641a34f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b70c7cc2f6c71f80884de30a237cab201d7fe150 +Subproject commit f641a34ffa80e89bd61701f60f15d15c4c5b361e From a65009dfb0f7714a4343c44d9f0dd0dc61ee0760 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 Apr 2024 13:59:09 +0200 Subject: [PATCH 1601/2451] Fix issue with merging and deduplicating. --- OtterGui | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 16 ++++--- Penumbra/Mods/Editor/ModMerger.cs | 58 ++++++++++++------------ Penumbra/Mods/Manager/ModOptionEditor.cs | 31 +++++++------ Penumbra/Services/CrashHandlerService.cs | 2 +- 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/OtterGui b/OtterGui index f641a34f..f48c6886 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f641a34ffa80e89bd61701f60f15d15c4c5b361e +Subproject commit f48c6886cbc163c5a292fa8b9fd919cb01c11d7b diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 77d10cc4..c8530936 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -81,7 +82,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (useModManager) { - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync); } else { @@ -216,18 +217,21 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } /// Deduplicate a mod simply by its directory without any confirmation or waiting time. - internal void DeduplicateMod(DirectoryInfo modDirectory) + internal void DeduplicateMod(DirectoryInfo modDirectory, bool useModManager = false) { try { - var mod = new Mod(modDirectory); - modManager.Creator.ReloadMod(mod, true, out _); + if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod)) + { + mod = new Mod(modDirectory); + modManager.Creator.ReloadMod(mod, true, out _); + } Clear(); var files = new ModFileCollection(); files.UpdateAll(mod, mod.Default); - CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray(), CancellationToken.None); - DeleteDuplicates(files, mod, mod.Default, false); + CheckDuplicates([.. files.Available.OrderByDescending(f => f.FileSize)], CancellationToken.None); + DeleteDuplicates(files, mod, mod.Default, useModManager); } catch (Exception e) { diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f5d0e4a4..842b1bb3 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -36,7 +36,7 @@ public class ModMerger : IDisposable public readonly HashSet SelectedOptions = []; - public readonly IReadOnlyList Warnings = new List(); + public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, @@ -78,7 +78,8 @@ public class ModMerger : IDisposable MergeWithOptions(); else MergeIntoOption(OptionGroupName, OptionName); - _duplicates.DeduplicateMod(MergeToMod.ModPath); + + _duplicates.DeduplicateMod(MergeToMod.ModPath, true); } catch (Exception ex) { @@ -134,10 +135,10 @@ public class ModMerger : IDisposable return; } - var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName); + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); + var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); @@ -156,27 +157,6 @@ public class ModMerger : IDisposable var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var manips = option.ManipulationData.ToHashSet(); - bool GetFullPath(FullPath input, out FullPath ret) - { - if (fromFileToFile) - { - if (!_fileToFile.TryGetValue(input.FullName, out var s)) - { - ret = input; - return false; - } - - ret = new FullPath(s); - return true; - } - - if (!Utf8RelPath.FromFile(input, MergeFromMod!.ModPath, out var relPath)) - throw new Exception($"Could not create relative path from {input} and {MergeFromMod!.ModPath}."); - - ret = new FullPath(MergeToMod!.ModPath, relPath); - return true; - } - foreach (var originalOption in mergeOptions) { foreach (var manip in originalOption.Manipulations) @@ -204,9 +184,31 @@ public class ModMerger : IDisposable } } - _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections); - _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps); - _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips); + _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); + return; + + bool GetFullPath(FullPath input, out FullPath ret) + { + if (fromFileToFile) + { + if (!_fileToFile.TryGetValue(input.FullName, out var s)) + { + ret = input; + return false; + } + + ret = new FullPath(s); + return true; + } + + if (!Utf8RelPath.FromFile(input, MergeFromMod!.ModPath, out var relPath)) + throw new Exception($"Could not create relative path from {input} and {MergeFromMod!.ModPath}."); + + ret = new FullPath(MergeToMod!.ModPath, relPath); + return true; + } } private void CopyFiles(DirectoryInfo directory) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 3459ce1a..60508d33 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -78,7 +78,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new, empty option group of the given type and name. - public void AddModGroup(Mod mod, GroupType type, string newName) + public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) { if (!VerifyFileName(mod, null, newName, true)) return; @@ -96,18 +96,18 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Name = newName, Priority = maxPriority, }); - saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } /// Add a new mod, empty option group of the given type and name if it does not exist already. - public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName) + public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) return (mod.Groups[idx], idx, false); - AddModGroup(mod, type, newName); + AddModGroup(mod, type, newName, saveType); if (mod.Groups[^1].Name != newName) throw new Exception($"Could not create new mod group with name {newName}."); @@ -226,7 +226,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new empty option of the given name for the given group. - public void AddOption(Mod mod, int groupIdx, string newName) + public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; var subMod = new SubMod(mod) { Name = newName }; @@ -241,19 +241,19 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; } - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName) + public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; var idx = group.IndexOf(o => o.Name == newName); if (idx >= 0) return ((SubMod)group[idx], false); - AddOption(mod, groupIdx, newName); + AddOption(mod, groupIdx, newName, saveType); if (group[^1].Name != newName) throw new Exception($"Could not create new option with name {newName} in {group.Name}."); @@ -324,7 +324,8 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.Manipulations.Count == manipulations.Count @@ -333,12 +334,13 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData.SetTo(manipulations); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements) + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileData.SetEquals(replacements)) @@ -346,7 +348,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData.SetTo(replacements); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } @@ -364,7 +366,8 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps) + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileSwapData.SetEquals(swaps)) @@ -372,7 +375,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData.SetTo(swaps); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 5423ec15..078b812b 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -213,7 +213,7 @@ public sealed class CrashHandlerService : IDisposable, IService } catch (Exception ex) { - Penumbra.Log.Debug($"Could not delete {dir}:\n{ex}"); + Penumbra.Log.Verbose($"Could not delete {dir}. This is generally not an error:\n{ex}"); } } } From 6e7512c13e20d0585f4d9c36aeb0c1563b62c568 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 14:53:30 +0200 Subject: [PATCH 1602/2451] Add Punchline. --- Penumbra/Penumbra.json | 1 + repo.json | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 8173e001..85e01c84 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -1,6 +1,7 @@ { "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "9.0.0.1", diff --git a/repo.json b/repo.json index 936adfc0..232afaa0 100644 --- a/repo.json +++ b/repo.json @@ -2,6 +2,7 @@ { "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", From 77bf441e626a00a1d7e4b4de2c791472c2de66ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 15:29:19 +0200 Subject: [PATCH 1603/2451] Update Open Settings and Main UI. --- Penumbra/UI/ConfigWindow.cs | 7 +++++++ Penumbra/UI/WindowSystem.cs | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index d52ebb99..9ae11fc3 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; @@ -35,6 +36,12 @@ public sealed class ConfigWindow : Window IsOpen = _config.OpenWindowAtStart; } + public void OpenSettings() + { + _configTabs.SelectTab = TabType.Settings; + IsOpen = true; + } + public void Setup(Penumbra penumbra, ConfigTabBar configTabs) { _penumbra = penumbra; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 62ad5a6e..c5418eb3 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -27,7 +27,8 @@ public class PenumbraWindowSystem : IDisposable _windowSystem.AddWindow(editWindow); _windowSystem.AddWindow(importPopup); _windowSystem.AddWindow(debugTab); - _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.OpenMainUi += Window.Toggle; + _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; @@ -40,7 +41,8 @@ public class PenumbraWindowSystem : IDisposable public void Dispose() { - _uiBuilder.OpenConfigUi -= Window.Toggle; + _uiBuilder.OpenMainUi -= Window.Toggle; + _uiBuilder.OpenConfigUi -= Window.OpenSettings; _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _fileDialog.Draw; } From b1ca073276e140ab88cc2121ee52b0a166af02dc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 16:35:55 +0200 Subject: [PATCH 1604/2451] Turn Settings and Priority into their own types. --- Penumbra/Api/PenumbraApi.cs | 49 ++++---- Penumbra/Api/TempModManager.cs | 21 ++-- Penumbra/Collections/Cache/CollectionCache.cs | 8 +- .../Cache/CollectionCacheManager.cs | 7 +- .../Collections/Manager/CollectionEditor.cs | 66 +++------- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/ModCollectionMigration.cs | 4 +- Penumbra/Communication/ModSettingChanged.cs | 5 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 10 +- .../PathResolving/CollectionResolver.cs | 5 +- Penumbra/Mods/Editor/IMod.cs | 4 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 5 +- Penumbra/Mods/Mod.cs | 8 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Mods/Subclasses/IModGroup.cs | 7 +- Penumbra/Mods/Subclasses/ModPriority.cs | 61 ++++++++++ Penumbra/Mods/Subclasses/ModSettings.cs | 115 ++++++------------ Penumbra/Mods/Subclasses/MultiModGroup.cs | 22 ++-- Penumbra/Mods/Subclasses/Setting.cs | 62 ++++++++++ Penumbra/Mods/Subclasses/SettingList.cs | 57 +++++++++ Penumbra/Mods/Subclasses/SingleModGroup.cs | 32 ++--- Penumbra/Mods/TemporaryMod.cs | 7 +- Penumbra/Services/ConfigMigrationService.cs | 17 ++- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 18 +-- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 57 ++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 10 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 55 +++++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 29 files changed, 422 insertions(+), 298 deletions(-) create mode 100644 Penumbra/Mods/Subclasses/ModPriority.cs create mode 100644 Penumbra/Mods/Subclasses/Setting.cs create mode 100644 Penumbra/Mods/Subclasses/SettingList.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index da1eafd0..dc1e8472 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -27,6 +27,7 @@ using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; using Penumbra.Interop.ResourceTree; using Penumbra.Mods.Editor; +using Penumbra.Mods.Subclasses; namespace Penumbra.Api; @@ -39,13 +40,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi { add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); - } + } public event Action? PreSettingsPanelDraw { add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); - } + } public event Action? PostEnabledDraw { @@ -649,7 +650,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var shareSettings = settings.ConvertToShareable(mod); return (PenumbraApiEc.Success, - (shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[mod.Index] != null)); + (shareSettings.Enabled, shareSettings.Priority.Value, shareSettings.Settings, collection.Settings[mod.Index] != null)); } public PenumbraApiEc ReloadMod(string modDirectory, string modName) @@ -791,7 +792,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - return _collectionEditor.SetModPriority(collection, mod, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, @@ -820,7 +823,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "OptionName", optionName)); - var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; + var setting = mod.Groups[groupIdx].Type switch + { + GroupType.Multi => Setting.Multi(optionIdx), + GroupType.Single => Setting.Single(optionIdx), + _ => Setting.Zero, + }; return Return( _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, @@ -850,7 +858,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var group = mod.Groups[groupIdx]; - uint setting = 0; + var setting = Setting.Zero; if (group.Type == GroupType.Single) { var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); @@ -859,7 +867,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "#optionNames", optionNames.Count.ToString())); - setting = (uint)optionIdx; + setting = Setting.Single(optionIdx); } else { @@ -871,7 +879,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "#optionNames", optionNames.Count.ToString())); - setting |= 1u << optionIdx; + setting |= Setting.Multi(optionIdx); } } @@ -993,7 +1001,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return _tempMods.Register(tag, null, p, m, priority) switch + return _tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -1014,7 +1022,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return _tempMods.Register(tag, collection, p, m, priority) switch + return _tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -1024,7 +1032,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return _tempMods.Unregister(tag, null, priority) switch + return _tempMods.Unregister(tag, null, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1039,7 +1047,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - return _tempMods.Unregister(tag, collection, priority) switch + return _tempMods.Unregister(tag, collection, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1089,7 +1097,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int) index)).OfType(); + var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); @@ -1153,7 +1161,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c : _collectionManager.Active.Individual(identifier); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1161,7 +1169,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1190,7 +1198,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + private ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) return ActorIdentifier.Invalid; @@ -1217,10 +1225,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (Path.IsPathRooted(resolvedPath)) - return _lumina?.GetFileFromDisk(resolvedPath); - - return _gameData.GetFile(resolvedPath); + return Path.IsPathRooted(resolvedPath) + ? _lumina?.GetFileFromDisk(resolvedPath) + : _gameData.GetFile(resolvedPath); } catch (Exception e) { @@ -1295,7 +1302,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return _actors.CreatePlayer(b, worldId); } - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index c7840b75..7d682338 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -6,6 +6,7 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Mods.Subclasses; namespace Penumbra.Api; @@ -21,8 +22,8 @@ public class TempModManager : IDisposable { private readonly CommunicatorService _communicator; - private readonly Dictionary> _mods = new(); - private readonly List _modsForAllCollections = new(); + private readonly Dictionary> _mods = []; + private readonly List _modsForAllCollections = []; public TempModManager(CommunicatorService communicator) { @@ -42,7 +43,7 @@ public class TempModManager : IDisposable => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet manips, int priority) + HashSet manips, ModPriority priority) { var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); @@ -51,10 +52,10 @@ public class TempModManager : IDisposable return RedirectResult.Success; } - public RedirectResult Unregister(string tag, ModCollection? collection, int? priority) + public RedirectResult Unregister(string tag, ModCollection? collection, ModPriority? priority) { Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}..."); - var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; + var list = collection == null ? _modsForAllCollections : _mods.GetValueOrDefault(collection); if (list == null) return RedirectResult.NotRegistered; @@ -85,13 +86,13 @@ public class TempModManager : IDisposable { Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); collection.Remove(mod); - _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 0, 0, false); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false); } else { Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); collection.Apply(mod, created); - _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 1, 0, false); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false); } } else @@ -116,7 +117,7 @@ public class TempModManager : IDisposable // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Returns the found or created mod and whether it was newly created. - private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) + private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, ModPriority priority, out bool created) { List list; if (collection == null) @@ -129,14 +130,14 @@ public class TempModManager : IDisposable } else { - list = new List(); + list = []; _mods.Add(collection, list); } var mod = list.Find(m => m.Priority == priority && m.Name == tag); if (mod == null) { - mod = new TemporaryMod() + mod = new TemporaryMod { Name = tag, Priority = priority, diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 6b2b688b..72f0fb59 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -237,7 +237,7 @@ public sealed class CollectionCache : IDisposable if (settings is not { Enabled: true }) return; - foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority)) + foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { if (group.Count == 0) continue; @@ -246,13 +246,13 @@ public sealed class CollectionCache : IDisposable switch (group.Type) { case GroupType.Single: - AddSubMod(group[(int)config], mod); + AddSubMod(group[config.AsIndex], mod); break; case GroupType.Multi: { foreach (var (option, _) in group.WithIndex() - .Where(p => ((1 << p.Item2) & config) != 0) - .OrderByDescending(p => group.OptionPriority(p.Item2))) + .Where(p => config.HasFlag(p.Index)) + .OrderByDescending(p => group.OptionPriority(p.Index))) AddSubMod(option, mod); break; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5a6b5593..f6c6e14a 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,6 +8,7 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; @@ -288,7 +289,7 @@ public class CollectionCacheManager : IDisposable MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _) { if (!collection.HasCache) return; @@ -300,9 +301,9 @@ public class CollectionCacheManager : IDisposable cache.ReloadMod(mod!, true); break; case ModSettingChange.EnableState: - if (oldValue == 0) + if (oldValue == Setting.False) cache.AddMod(mod!, true); - else if (oldValue == 1) + else if (oldValue == Setting.True) cache.RemoveMod(mod!, true); else if (collection[mod!.Index].Settings?.Enabled == true) cache.ReloadMod(mod!, true); diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 73950942..4af19e6b 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -7,26 +7,15 @@ using Penumbra.Services; namespace Penumbra.Collections.Manager; -public class CollectionEditor +public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) { - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - private readonly ModStorage _modStorage; - - public CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) - { - _saveService = saveService; - _communicator = communicator; - _modStorage = modStorage; - } - /// Enable or disable the mod inheritance of mod idx. public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) { if (!FixInheritance(collection, mod, inherit)) return false; - InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? 0 : 1, 0); + InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? Setting.False : Setting.True, 0); return true; } @@ -42,7 +31,8 @@ public class CollectionEditor var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.Enabled = newValue; - InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0); + InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True, + 0); return true; } @@ -52,7 +42,7 @@ public class CollectionEditor if (!mods.Aggregate(false, (current, mod) => current | FixInheritance(collection, mod, inherit))) return; - InvokeChange(collection, ModSettingChange.MultiInheritance, null, -1, 0); + InvokeChange(collection, ModSettingChange.MultiInheritance, null, Setting.Indefinite, 0); } /// @@ -76,22 +66,22 @@ public class CollectionEditor if (!changes) return; - InvokeChange(collection, ModSettingChange.MultiEnableState, null, -1, 0); + InvokeChange(collection, ModSettingChange.MultiEnableState, null, Setting.Indefinite, 0); } /// /// Set the priority of mod idx to newValue if it differs from the current priority. /// If the mod is currently inherited, stop the inheritance. /// - public bool SetModPriority(ModCollection collection, Mod mod, int newValue) + public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue) { - var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0; + var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.Priority = newValue; - InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0); + InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0); return true; } @@ -99,7 +89,7 @@ public class CollectionEditor /// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. /// /// If the mod is currently inherited, stop the inheritance. /// - public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, uint newValue) + public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { var settings = collection.Settings[mod.Index] != null ? collection.Settings[mod.Index]!.Settings @@ -110,7 +100,7 @@ public class CollectionEditor var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); - InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx); + InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx); return true; } @@ -158,35 +148,17 @@ public class CollectionEditor if (savedSettings != null) { ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } } return true; } - /// - /// Change one of the available mod settings for mod idx discerned by type. - /// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. - /// The setting will also be automatically fixed if it is invalid for that setting group. - /// For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - /// - public bool ChangeModSetting(ModCollection collection, ModSettingChange type, Mod mod, int newValue, int groupIdx) - { - return type switch - { - ModSettingChange.Inheritance => SetModInheritance(collection, mod, newValue != 0), - ModSettingChange.EnableState => SetModState(collection, mod, newValue != 0), - ModSettingChange.Priority => SetModPriority(collection, mod, newValue), - ModSettingChange.Setting => SetModSetting(collection, mod, groupIdx, (uint)newValue), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - } - /// /// Set inheritance of a mod without saving, /// to be used as an intermediary. @@ -204,16 +176,16 @@ public class CollectionEditor /// Queue saves and trigger changes for any non-inherited change in a collection, then trigger changes for all inheritors. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection)); - _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); + saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); + communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } /// Trigger changes in all inherited collections. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { foreach (var directInheritor in directParent.DirectParentOf) { @@ -221,11 +193,11 @@ public class CollectionEditor { case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - _communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); + communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: if (directInheritor.Settings[mod!.Index] == null) - _communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); + communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 0ee55376..d0b61e57 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -268,7 +268,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable case ModPathChangeType.Reloaded: foreach (var collection in this) { - if (collection.Settings[mod.Index]?.FixAllSettings(mod) ?? false) + if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index b2b8df0d..053f0a2b 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -40,9 +40,9 @@ internal static class ModCollectionMigration /// We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); + => setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.Values.All(s => s == Setting.Zero); /// private static bool SettingIsDefaultV0(ModSettings? setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0); + => setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.All(s => s == Setting.Zero); } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 5e0bc0c0..412b3003 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; namespace Penumbra.Communication; @@ -12,13 +13,13 @@ namespace Penumbra.Communication; /// Parameter is the collection in which the setting was changed. /// Parameter is the type of change. /// Parameter is the mod the setting was changed for, unless it was a multi-change. -/// Parameter is the old value of the setting before the change as int. +/// Parameter is the old value of the setting before the change as Setting. /// Parameter is the index of the changed group if the change type is Setting. /// Parameter is whether the change was inherited from another collection. /// /// public sealed class ModSettingChanged() - : EventWrapper(nameof(ModSettingChanged)) + : EventWrapper(nameof(ModSettingChanged)) { public enum Priority { diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7c4b94d8..7a247a53 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -174,7 +174,7 @@ public partial class TexToolsImporter ?? new DirectoryInfo(Path.Combine(_currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}")); - uint? defaultSettings = group.SelectionType == GroupType.Multi ? 0u : null; + Setting? defaultSettings = group.SelectionType == GroupType.Multi ? Setting.Zero : null; for (var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i) { var option = allOptions[i + optionIdx]; @@ -186,8 +186,8 @@ public partial class TexToolsImporter options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi - ? defaultSettings!.Value | (1u << i) - : (uint)i; + ? defaultSettings!.Value | Setting.Multi(i) + : Setting.Single(i); ++_currentOptionIdx; } @@ -205,12 +205,12 @@ public partial class TexToolsImporter _currentOptionName = option.Name; options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); if (option.IsChecked) - defaultSettings = (uint) idx; + defaultSettings = Setting.Single(idx); } } _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - defaultSettings ?? 0, group.Description, options); + defaultSettings ?? Setting.Zero, group.Description, options); ++groupPriority; } } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index fa122e39..aea58304 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -18,6 +18,7 @@ public sealed unsafe class CollectionResolver( PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, + ObjectManager objects, IGameGui gameGui, ActorManager actors, CutsceneService cutscenes, @@ -35,8 +36,8 @@ public sealed unsafe class CollectionResolver( public ModCollection PlayerCollection() { using var performance1 = performance.Measure(PerformanceType.IdentifyCollection); - var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero); - if (gameObject == null) + var gameObject = objects[0]; + if (!gameObject.Valid) return collectionManager.Active.ByType(CollectionType.Yourself) ?? collectionManager.Active.Default; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 78250341..d3bc19b0 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -7,8 +7,8 @@ public interface IMod { LowerString Name { get; } - public int Index { get; } - public int Priority { get; } + public int Index { get; } + public ModPriority Priority { get; } public ISubMod Default { get; } public IReadOnlyList Groups { get; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 60508d33..ea6a62df 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,3 +1,4 @@ +using System.Security.AccessControl; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -33,6 +34,8 @@ public enum ModOptionChangeType public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { + + /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { @@ -46,7 +49,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the settings stored as default options in a mod. - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption) { var group = mod.Groups[groupIdx]; if (group.DefaultSettings == defaultOption) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index c5e671af..b7d1186d 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -12,7 +12,7 @@ public sealed class Mod : IMod { Name = "Forced Files", Index = -1, - Priority = int.MaxValue, + Priority = ModPriority.MaxValue, }; // Main Data @@ -26,9 +26,9 @@ public sealed class Mod : IMod public bool IsTemporary => Index < 0; - /// Unused if Index < 0 but used for special temporary mods. - public int Priority - => 0; + /// Unused if Index is less than 0 but used for special temporary mods. + public ModPriority Priority + => ModPriority.Default; internal Mod(DirectoryInfo modPath) { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 042c98b4..c324af48 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable subMods) + int priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index ea5f176c..2f6b2403 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -12,7 +12,7 @@ public interface IModGroup : IEnumerable public string Description { get; } public GroupType Type { get; } public int Priority { get; } - public uint DefaultSettings { get; set; } + public Setting DefaultSettings { get; set; } public int OptionPriority(Index optionIdx); @@ -31,6 +31,9 @@ public interface IModGroup : IEnumerable public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); public void UpdatePositions(int from = 0); + + /// Ensure that a value is valid for a group. + public Setting FixSetting(Setting setting); } public readonly struct ModSaveGroup : ISavable @@ -87,7 +90,7 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName(nameof(Type)); j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); - j.WriteValue(_group.DefaultSettings); + j.WriteValue(_group.DefaultSettings.Value); j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Subclasses/ModPriority.cs new file mode 100644 index 00000000..3302c627 --- /dev/null +++ b/Penumbra/Mods/Subclasses/ModPriority.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; + +namespace Penumbra.Mods.Subclasses; + +[JsonConverter(typeof(Converter))] +public readonly record struct ModPriority(int Value) : + IComparisonOperators, + IAdditionOperators, + IAdditionOperators, + ISubtractionOperators, + ISubtractionOperators +{ + public static readonly ModPriority Default = new(0); + public static readonly ModPriority MaxValue = new(int.MaxValue); + + public bool IsDefault + => Value == Default.Value; + + public Setting AsSetting + => new((uint)Value); + + public ModPriority Max(ModPriority other) + => this < other ? other : this; + + public override string ToString() + => Value.ToString(); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ModPriority value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override ModPriority ReadJson(JsonReader reader, Type objectType, ModPriority existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } + + public static bool operator >(ModPriority left, ModPriority right) + => left.Value > right.Value; + + public static bool operator >=(ModPriority left, ModPriority right) + => left.Value >= right.Value; + + public static bool operator <(ModPriority left, ModPriority right) + => left.Value < right.Value; + + public static bool operator <=(ModPriority left, ModPriority right) + => left.Value <= right.Value; + + public static ModPriority operator +(ModPriority left, ModPriority right) + => new(left.Value + right.Value); + + public static ModPriority operator +(ModPriority left, int right) + => new(left.Value + right); + + public static ModPriority operator -(ModPriority left, ModPriority right) + => new(left.Value - right.Value); + + public static ModPriority operator -(ModPriority left, int right) + => new(left.Value - right); +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index ed8ad84e..b79b3242 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -11,8 +11,8 @@ namespace Penumbra.Mods.Subclasses; public class ModSettings { public static readonly ModSettings Empty = new(); - public List Settings { get; private init; } = []; - public int Priority { get; set; } + public SettingList Settings { get; private init; } = []; + public ModPriority Priority { get; set; } public bool Enabled { get; set; } // Create an independent copy of the current settings. @@ -21,7 +21,7 @@ public class ModSettings { Enabled = Enabled, Priority = Priority, - Settings = [.. Settings], + Settings = Settings.Clone(), }; // Create default settings for a given mod. @@ -29,8 +29,8 @@ public class ModSettings => new() { Enabled = false, - Priority = 0, - Settings = mod.Groups.Select(g => g.DefaultSettings).ToList(), + Priority = ModPriority.Default, + Settings = SettingList.Default(mod), }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. @@ -39,7 +39,7 @@ public class ModSettings if (settings == null) settings = DefaultSettings(mod); else - settings.AddMissingSettings(mod); + settings.Settings.FixSize(mod); var dict = new Dictionary(); var set = new HashSet(); @@ -49,13 +49,13 @@ public class ModSettings if (group.Type is GroupType.Single) { if (group.Count > 0) - AddOption(group[(int)settings.Settings[index]]); + AddOption(group[settings.Settings[index].AsIndex]); } else { foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) { - if (((settings.Settings[index] >> optionIdx) & 1) == 1) + if (settings.Settings[index].HasFlag(optionIdx)) AddOption(option); } } @@ -97,8 +97,8 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => (uint)Math.Max(Math.Min(group.Count - 1, BitOperations.TrailingZeroCount(config)), 0), - GroupType.Multi => 1u << (int)config, + GroupType.Single => config.TurnMulti(group.Count), + GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; return config != Settings[groupIdx]; @@ -111,9 +111,11 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, - GroupType.Multi => Functions.RemoveBit(config, optionIdx), - _ => config, + GroupType.Single => config.AsIndex >= optionIdx + ? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero + : config, + GroupType.Multi => config.RemoveBit(optionIdx), + _ => config, }; return config != Settings[groupIdx]; } @@ -128,8 +130,8 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config == optionIdx ? (uint)movedToIdx : config, - GroupType.Multi => Functions.MoveBit(config, optionIdx, movedToIdx), + GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, + GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), _ => config, }; return config != Settings[groupIdx]; @@ -138,96 +140,52 @@ public class ModSettings } } - public bool FixAllSettings(Mod mod) + /// Set a setting. Ensures that there are enough settings and fixes the setting beforehand. + public void SetValue(Mod mod, int groupIdx, Setting newValue) { - var ret = false; - for (var i = 0; i < Settings.Count; ++i) - { - var newValue = FixSetting(mod.Groups[i], Settings[i]); - if (newValue != Settings[i]) - { - ret = true; - Settings[i] = newValue; - } - } - - return AddMissingSettings(mod) || ret; - } - - // Ensure that a value is valid for a group. - private static uint FixSetting(IModGroup group, uint value) - => group.Type switch - { - GroupType.Single => (uint)Math.Min(value, group.Count - 1), - GroupType.Multi => (uint)(value & ((1ul << group.Count) - 1)), - _ => value, - }; - - // Set a setting. Ensures that there are enough settings and fixes the setting beforehand. - public void SetValue(Mod mod, int groupIdx, uint newValue) - { - AddMissingSettings(mod); + Settings.FixSize(mod); var group = mod.Groups[groupIdx]; - Settings[groupIdx] = FixSetting(group, newValue); - } - - // Add defaulted settings up to the required count. - private bool AddMissingSettings(Mod mod) - { - var changes = false; - for (var i = Settings.Count; i < mod.Groups.Count; ++i) - { - Settings.Add(mod.Groups[i].DefaultSettings); - changes = true; - } - - return changes; + Settings[groupIdx] = group.FixSetting(newValue); } // A simple struct conversion to easily save settings by name instead of value. public struct SavedSettings { - public Dictionary Settings; - public int Priority; - public bool Enabled; + public Dictionary Settings; + public ModPriority Priority; + public bool Enabled; public SavedSettings DeepCopy() - => new() - { - Enabled = Enabled, - Priority = Priority, - Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), - }; + => this with { Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; public SavedSettings(ModSettings settings, Mod mod) { Priority = settings.Priority; Enabled = settings.Enabled; - Settings = new Dictionary(mod.Groups.Count); - settings.AddMissingSettings(mod); + Settings = new Dictionary(mod.Groups.Count); + settings.Settings.FixSize(mod); foreach (var (group, setting) in mod.Groups.Zip(settings.Settings)) Settings.Add(group.Name, setting); } // Convert and fix. - public bool ToSettings(Mod mod, out ModSettings settings) + public readonly bool ToSettings(Mod mod, out ModSettings settings) { - var list = new List(mod.Groups.Count); + var list = new SettingList(mod.Groups.Count); var changes = Settings.Count != mod.Groups.Count; foreach (var group in mod.Groups) { if (Settings.TryGetValue(group.Name, out var config)) { - var castConfig = (uint)Math.Clamp(config, 0, uint.MaxValue); - var actualConfig = FixSetting(group, castConfig); + var actualConfig = group.FixSetting(config); list.Add(actualConfig); if (actualConfig != config) changes = true; } else { - list.Add(0); + list.Add(group.DefaultSettings); changes = true; } } @@ -245,7 +203,7 @@ public class ModSettings // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Does not repair settings but ignores settings not fitting to the given mod. - public (bool Enabled, int Priority, Dictionary> Settings) ConvertToShareable(Mod mod) + public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) { var dict = new Dictionary>(Settings.Count); foreach (var (setting, idx) in Settings.WithIndex()) @@ -254,16 +212,13 @@ public class ModSettings break; var group = mod.Groups[idx]; - if (group.Type == GroupType.Single && setting < group.Count) + if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count) { - dict.Add(group.Name, new[] - { - group[(int)setting].Name, - }); + dict.Add(group.Name, [group[(int)setting.Value].Name]); } else { - var list = group.Where((_, optionIdx) => (setting & (1 << optionIdx)) != 0).Select(o => o.Name).ToList(); + var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); dict.Add(group.Name, list); } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 07f84722..8a8e10bd 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -14,10 +14,10 @@ public sealed class MultiModGroup : IModGroup public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public Setting DefaultSettings { get; set; } public int OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; @@ -44,7 +44,7 @@ public sealed class MultiModGroup : IModGroup Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) return null; @@ -56,7 +56,8 @@ public sealed class MultiModGroup : IModGroup if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { Penumbra.Messager.NotificationMessage( - $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", NotificationType.Warning); + $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", + NotificationType.Warning); break; } @@ -66,7 +67,7 @@ public sealed class MultiModGroup : IModGroup ret.PrioritizedOptions.Add((subMod, priority)); } - ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } @@ -82,7 +83,7 @@ public sealed class MultiModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), + DefaultSettings = DefaultSettings.TurnMulti(Count), }; multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; @@ -95,7 +96,7 @@ public sealed class MultiModGroup : IModGroup if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) return false; - DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } @@ -105,4 +106,7 @@ public sealed class MultiModGroup : IModGroup foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) o.SetPosition(o.GroupIdx, i); } + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << Count) - 1)); } diff --git a/Penumbra/Mods/Subclasses/Setting.cs b/Penumbra/Mods/Subclasses/Setting.cs new file mode 100644 index 00000000..18b1e4ca --- /dev/null +++ b/Penumbra/Mods/Subclasses/Setting.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using OtterGui; + +namespace Penumbra.Mods.Subclasses; + +[JsonConverter(typeof(Converter))] +public readonly record struct Setting(ulong Value) +{ + public static readonly Setting Zero = new(0); + public static readonly Setting True = new(1); + public static readonly Setting False = new(0); + public static readonly Setting Indefinite = new(ulong.MaxValue); + + public static Setting Multi(int idx) + => new(1ul << idx); + + public static Setting Single(int idx) + => new(Math.Max(0ul, (ulong)idx)); + + public static Setting operator |(Setting lhs, Setting rhs) + => new(lhs.Value | rhs.Value); + + public int AsIndex + => (int)Math.Clamp(Value, 0ul, int.MaxValue); + + public bool HasFlag(int idx) + => idx >= 0 && (Value & (1ul << idx)) != 0; + + public Setting MoveBit(int idx1, int idx2) + => new(Functions.MoveBit(Value, idx1, idx2)); + + public Setting RemoveBit(int idx) + => new(Functions.RemoveBit(Value, idx)); + + public Setting SetBit(int idx, bool value) + => new(value ? Value | (1ul << idx) : Value & ~(1ul << idx)); + + public static Setting AllBits(int count) + => new((1ul << Math.Clamp(count, 0, 63)) - 1); + + public Setting TurnMulti(int count) + => new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); + + public ModPriority AsPriority + => new((int)(Value & 0xFFFFFFFF)); + + public static Setting FromBool(bool value) + => value ? True : False; + + public bool AsBool + => Value != 0; + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Setting value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override Setting ReadJson(JsonReader reader, Type objectType, Setting existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Mods/Subclasses/SettingList.cs b/Penumbra/Mods/Subclasses/SettingList.cs new file mode 100644 index 00000000..ea1e447f --- /dev/null +++ b/Penumbra/Mods/Subclasses/SettingList.cs @@ -0,0 +1,57 @@ +namespace Penumbra.Mods.Subclasses; + +public class SettingList : List +{ + public SettingList() + { } + + public SettingList(int capacity) + : base(capacity) + { } + + public SettingList(IEnumerable settings) + => AddRange(settings); + + public SettingList Clone() + => new(this); + + public static SettingList Default(Mod mod) + => new(mod.Groups.Select(g => g.DefaultSettings)); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool FixSize(Mod mod) + { + var diff = Count - mod.Groups.Count; + + switch (diff) + { + case 0: return false; + case > 0: + RemoveRange(mod.Groups.Count, diff); + return true; + default: + EnsureCapacity(mod.Groups.Count); + for (var i = Count; i < mod.Groups.Count; ++i) + Add(mod.Groups[i].DefaultSettings); + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool FixAll(Mod mod) + { + var ret = false; + for (var i = 0; i < Count; ++i) + { + var oldValue = this[i]; + var newValue = mod.Groups[i].FixSetting(oldValue); + if (newValue == oldValue) + continue; + + ret = true; + this[i] = newValue; + } + + return FixSize(mod) | ret; + } +} diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 2b7ebd09..be1dbde5 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -12,10 +12,10 @@ public sealed class SingleModGroup : IModGroup public GroupType Type => GroupType.Single; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -43,7 +43,7 @@ public sealed class SingleModGroup : IModGroup Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0u, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) return null; @@ -57,9 +57,7 @@ public sealed class SingleModGroup : IModGroup ret.OptionData.Add(subMod); } - if ((int)ret.DefaultSettings >= ret.Count) - ret.DefaultSettings = 0; - + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } @@ -74,7 +72,7 @@ public sealed class SingleModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = 1u << (int)DefaultSettings, + DefaultSettings = Setting.Multi((int) DefaultSettings.Value), }; multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, i))); return multi; @@ -87,19 +85,20 @@ public sealed class SingleModGroup : IModGroup if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; + var currentIndex = DefaultSettings.AsIndex; // Update default settings with the move. - if (DefaultSettings == optionIdxFrom) + if (currentIndex == optionIdxFrom) { - DefaultSettings = (uint)optionIdxTo; + DefaultSettings = Setting.Single(optionIdxTo); } else if (optionIdxFrom < optionIdxTo) { - if (DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo) - --DefaultSettings; + if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo) + DefaultSettings = Setting.Single(currentIndex - 1); } - else if (DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo) + else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo) { - ++DefaultSettings; + DefaultSettings = Setting.Single(currentIndex + 1); } UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); @@ -111,4 +110,7 @@ public sealed class SingleModGroup : IModGroup foreach (var (o, i) in OptionData.WithIndex().Skip(from)) o.SetPosition(o.GroupIdx, i); } + + public Setting FixSetting(Setting setting) + => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index c80334aa..4de2ac13 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -13,7 +13,7 @@ public class TemporaryMod : IMod { public LowerString Name { get; init; } = LowerString.Empty; public int Index { get; init; } = -2; - public int Priority { get; init; } = int.MaxValue; + public ModPriority Priority { get; init; } = ModPriority.MaxValue; public int TotalManipulations => Default.Manipulations.Count; @@ -27,10 +27,7 @@ public class TemporaryMod : IMod => Array.Empty(); public IEnumerable AllSubMods - => new[] - { - Default, - }; + => [Default]; public TemporaryMod() => Default = new SubMod(this); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index b84c0996..d1e952f1 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -341,15 +341,15 @@ public class ConfigMigrationService(SaveService saveService) : IService var text = File.ReadAllText(collectionJson.FullName); var data = JArray.Parse(text); - var maxPriority = 0; + var maxPriority = ModPriority.Default; var dict = new Dictionary(); foreach (var setting in data.Cast()) { - var modName = (string)setting["FolderName"]!; - var enabled = (bool)setting["Enabled"]!; - var priority = (int)setting["Priority"]!; - var settings = setting["Settings"]!.ToObject>() - ?? setting["Conf"]!.ToObject>(); + var modName = setting["FolderName"]?.ToObject()!; + var enabled = setting["Enabled"]?.ToObject() ?? false; + var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default; + var settings = setting["Settings"]!.ToObject>() + ?? setting["Conf"]!.ToObject>(); dict[modName] = new ModSettings.SavedSettings() { @@ -357,7 +357,7 @@ public class ConfigMigrationService(SaveService saveService) : IService Priority = priority, Settings = settings!, }; - maxPriority = Math.Max(maxPriority, priority); + maxPriority = maxPriority.Max(priority); } InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; @@ -365,8 +365,7 @@ public class ConfigMigrationService(SaveService saveService) : IService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, - Array.Empty()); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 0205f3c6..7b5ce2dc 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -694,7 +694,7 @@ public class ItemSwapTab : IDisposable, ITab UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); } - private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) + private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) { if (collection != _collectionManager.Active.Current || mod != _mod) return; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 15b18692..cd0eb982 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -74,7 +74,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { var mod = _modManager.FirstOrDefault(m @@ -92,7 +92,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Label => "Conflicts"u8; public bool IsVisible - => _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0; + => collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0; - private readonly ConditionalWeakTable _expandedMods = new(); + private readonly ConditionalWeakTable _expandedMods = []; - private int GetPriority(ModConflicts conflicts) + private ModPriority GetPriority(ModConflicts conflicts) { if (conflicts.Mod2.Index < 0) return conflicts.Mod2.Priority; - return _collectionManager.Active.Current[conflicts.Mod2.Index].Settings?.Priority ?? 0; + return collectionManager.Active.Current[conflicts.Mod2.Index].Settings?.Priority ?? ModPriority.Default; } public void DrawContent() @@ -63,8 +55,8 @@ public class ModPanelConflictsTab : ITab DrawCurrentRow(priorityWidth); // Can not be null because otherwise the tab bar is never drawn. - var mod = _selector.Selected!; - foreach (var (conflict, index) in _collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + var mod = selector.Selected!; + foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { using var id = ImRaii.PushId(index); @@ -77,18 +69,18 @@ public class ModPanelConflictsTab : ITab ImGui.TableNextColumn(); using var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderLine.Value()); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_selector.Selected!.Name); + ImGui.TextUnformatted(selector.Selected!.Name); ImGui.TableNextColumn(); - var priority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + var priority = collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value; ImGui.SetNextItemWidth(priorityWidth); if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)_selector.Selected!, - _currentPriority.Value); + if (_currentPriority != collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -104,7 +96,7 @@ public class ModPanelConflictsTab : ITab { ImGui.AlignTextToFramePadding(); if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) - _selector.SelectByValue(otherMod); + selector.SelectByValue(otherMod); var hovered = ImGui.IsItemHovered(); var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); if (conflict.Mod2 is Mod otherMod2) @@ -112,7 +104,7 @@ public class ModPanelConflictsTab : ITab if (hovered) ImGui.SetTooltip("Click to jump to mod, Control + Right-Click to disable mod."); if (rightClicked && ImGui.GetIO().KeyCtrl) - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, otherMod2, false); + collectionManager.Editor.SetModState(collectionManager.Active.Current, otherMod2, false); } } @@ -146,7 +138,7 @@ public class ModPanelConflictsTab : ITab ImGui.TableNextColumn(); var conflictPriority = DrawPriorityInput(conflict, priorityWidth); ImGui.SameLine(); - var selectedPriority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + var selectedPriority = collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value; DrawPriorityButtons(conflict.Mod2 as Mod, conflictPriority, selectedPriority, buttonSize); ImGui.TableNextColumn(); DrawExpandButton(conflict.Mod2, expanded, buttonSize); @@ -171,7 +163,7 @@ public class ModPanelConflictsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Text, conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value()); using var disabled = ImRaii.Disabled(conflict.Mod2.Index < 0); - var priority = _currentPriority ?? GetPriority(conflict); + var priority = _currentPriority ?? GetPriority(conflict).Value; ImGui.SetNextItemWidth(priorityWidth); if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) @@ -179,8 +171,9 @@ public class ModPanelConflictsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != GetPriority(conflict)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)conflict.Mod2, _currentPriority.Value); + if (_currentPriority != GetPriority(conflict).Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, (Mod)conflict.Mod2, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -195,12 +188,14 @@ public class ModPanelConflictsTab : ITab private void DrawPriorityButtons(Mod? conflict, int conflictPriority, int selectedPriority, Vector2 buttonSize) { if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericUpAlt.ToIconString(), buttonSize, - $"Set the priority of the currently selected mod to this mods priority plus one. ({selectedPriority} -> {conflictPriority + 1})", selectedPriority > conflictPriority, true)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, conflictPriority + 1); + $"Set the priority of the currently selected mod to this mods priority plus one. ({selectedPriority} -> {conflictPriority + 1})", + selectedPriority > conflictPriority, true)) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(conflictPriority + 1)); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericDownAlt.ToIconString(), buttonSize, $"Set the priority of this mod to the currently selected mods priority minus one. ({conflictPriority} -> {selectedPriority - 1})", selectedPriority > conflictPriority || conflict == null, true)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, conflict!, selectedPriority - 1); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, conflict!, new ModPriority(selectedPriority - 1)); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index eb79869e..1292367a 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -502,18 +502,16 @@ public class ModPanelEditTab( if (group.Type == GroupType.Single) { - if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx)); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } else { - var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption - ? group.DefaultSettings | (1u << optionIdx) - : group.DefaultSettings & ~(1u << optionIdx)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index b14cad01..1107aa20 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -26,7 +26,7 @@ public class ModPanelSettingsTab : ITab private ModSettings _settings = null!; private ModCollection _collection = null!; private bool _empty; - private int? _currentPriority = null; + private int? _currentPriority; public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, TutorialService tutorial, CommunicatorService communicator, Configuration config) @@ -136,15 +136,15 @@ public class ModPanelSettingsTab : ITab private void DrawPriorityInput() { using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority; + var priority = _currentPriority ?? _settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _settings.Priority) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, _currentPriority.Value); + if (_currentPriority != _settings.Priority.Value) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -179,7 +179,7 @@ public class ModPanelSettingsTab : ITab private void DrawSingleGroupCombo(IModGroup group, int groupIdx) { using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) { @@ -189,7 +189,8 @@ public class ModPanelSettingsTab : ITab id.Push(idx2); var option = group[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.Single(idx2)); if (option.Description.Length > 0) ImGuiUtil.SelectableHelpMarker(option.Description); @@ -210,8 +211,8 @@ public class ModPanelSettingsTab : ITab private void DrawSingleGroupRadio(IModGroup group, int groupIdx) { using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, description:group.Description); + var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); DrawCollapseHandling(group, minWidth, DrawOptions); @@ -225,7 +226,8 @@ public class ModPanelSettingsTab : ITab using var i = ImRaii.PushId(idx); var option = group[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.Single(idx)); if (option.Description.Length <= 0) continue; @@ -291,7 +293,17 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, description: group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + + DrawCollapseHandling(group, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + DrawMultiPopup(group, groupIdx, label); + return; void DrawOptions() { @@ -299,12 +311,11 @@ public class ModPanelSettingsTab : ITab { using var i = ImRaii.PushId(idx); var option = group[idx]; - var flag = 1u << idx; - var setting = (flags & flag) != 0; + var setting = flags.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref setting)) { - flags = setting ? flags | flag : flags & ~flag; + flags = flags.SetBit(idx, setting); _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); } @@ -315,14 +326,10 @@ public class ModPanelSettingsTab : ITab } } } + } - DrawCollapseHandling(group, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); - + private void DrawMultiPopup(IModGroup group, int groupIdx, string label) + { using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); using var popup = ImRaii.Popup(label); if (!popup) @@ -331,12 +338,10 @@ public class ModPanelSettingsTab : ITab ImGui.TextUnformatted(group.Name); ImGui.Separator(); if (ImGui.Selectable("Enable All")) - { - flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); - } + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.AllBits(group.Count)); if (ImGui.Selectable("Disable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, 0); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 06f1d126..9a956d2d 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -211,7 +211,7 @@ public class DebugTab : Window, ITab color.Pop(); foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) { - using var id = mod is TemporaryMod t ? PushId(t.Priority) : PushId(((Mod)mod).ModPath.Name); + using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); using var node2 = TreeNode(mod.Name.Text); if (!node2) continue; From e94cdaec4627cf06e4dfcb5cb55e746779a04f94 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 23:19:41 +0200 Subject: [PATCH 1605/2451] Some more. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +++--- Penumbra/Mods/Manager/ModMigration.cs | 12 ++++++------ Penumbra/Mods/Manager/ModOptionEditor.cs | 18 ++++++++++-------- Penumbra/Mods/ModCreator.cs | 4 ++-- Penumbra/Mods/Subclasses/IModGroup.cs | 18 +++++++++++------- Penumbra/Mods/Subclasses/ISubMod.cs | 4 ++-- Penumbra/Mods/Subclasses/ModPriority.cs | 10 +++++++++- Penumbra/Mods/Subclasses/MultiModGroup.cs | 14 +++++++------- Penumbra/Mods/Subclasses/SingleModGroup.cs | 16 ++++++++-------- Penumbra/Mods/Subclasses/SubMod.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 19 ++++++++++--------- 11 files changed, 70 insertions(+), 55 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7a247a53..099b133c 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -153,7 +153,7 @@ public partial class TexToolsImporter // Iterate through all pages var options = new List(); - var groupPriority = 0; + var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) { @@ -209,9 +209,9 @@ public partial class TexToolsImporter } } - _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority.Value, defaultSettings ?? Setting.Zero, group.Description, options); - ++groupPriority; + groupPriority += 1; } } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8b73cae5..295afd7b 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -61,10 +61,9 @@ public static partial class ModMigration if (fileVersion > 0) return false; - var swaps = json["FileSwaps"]?.ToObject>() - ?? new Dictionary(); - var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); - var priority = 1; + var swaps = json["FileSwaps"]?.ToObject>() ?? []; + var groups = json["Groups"]?.ToObject>() ?? []; + var priority = new ModPriority(1); var seenMetaFiles = new HashSet(); foreach (var group in groups.Values) ConvertGroup(creator, mod, group, ref priority, seenMetaFiles); @@ -116,7 +115,8 @@ public static partial class ModMigration return true; } - private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) + private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref ModPriority priority, + HashSet seenMetaFiles) { if (group.Options.Count == 0) return; @@ -125,7 +125,7 @@ public static partial class ModMigration { case GroupType.Multi: - var optionPriority = 0; + var optionPriority = ModPriority.Default; var newMultiGroup = new MultiModGroup() { Name = group.GroupName, diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index ea6a62df..0ffdc4af 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -34,8 +34,6 @@ public enum ModOptionChangeType public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { - - /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { @@ -86,7 +84,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!VerifyFileName(mod, null, newName, true)) return; - var maxPriority = mod.Groups.Count == 0 ? 0 : mod.Groups.Max(o => o.Priority) + 1; + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; mod.Groups.Add(type == GroupType.Multi ? new MultiModGroup @@ -169,7 +167,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the internal priority of the given option group. - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) + public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority) { var group = mod.Groups[groupIdx]; if (group.Priority == newPriority) @@ -186,7 +184,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the internal priority of the given option. - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority) { switch (mod.Groups[groupIdx]) { @@ -240,7 +238,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS s.OptionData.Add(subMod); break; case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); + m.PrioritizedOptions.Add((subMod, ModPriority.Default)); break; } @@ -263,8 +261,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return ((SubMod)group[^1], true); } - /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + /// Add an existing option to a given group with default priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option) + => AddOption(mod, groupIdx, option, ModPriority.Default); + + /// Add an existing option to a given group with a given priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option, ModPriority priority) { if (option is not SubMod o) return; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index c324af48..2bcdd3b1 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, idx))); + group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 2f6b2403..e9e2a93b 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -8,13 +8,13 @@ public interface IModGroup : IEnumerable { public const int MaxMultiOptions = 32; - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public int Priority { get; } - public Setting DefaultSettings { get; set; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public ModPriority Priority { get; } + public Setting DefaultSettings { get; set; } - public int OptionPriority(Index optionIdx); + public ModPriority OptionPriority(Index optionIdx); public ISubMod this[Index idx] { get; } @@ -94,7 +94,11 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) - ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type == GroupType.Multi ? _group.OptionPriority(idx) : null); + ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch + { + GroupType.Multi => _group.OptionPriority(idx), + _ => null, + }); j.WriteEndArray(); j.WriteEndObject(); diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 8c296f20..29323c1d 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -16,7 +16,7 @@ public interface ISubMod public bool IsDefault { get; } - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority) + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) { j.WriteStartObject(); j.WritePropertyName(nameof(Name)); @@ -26,7 +26,7 @@ public interface ISubMod if (priority != null) { j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value); + j.WriteValue(priority.Value.Value); } j.WritePropertyName(nameof(mod.Files)); diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Subclasses/ModPriority.cs index 3302c627..a99c12ed 100644 --- a/Penumbra/Mods/Subclasses/ModPriority.cs +++ b/Penumbra/Mods/Subclasses/ModPriority.cs @@ -8,7 +8,9 @@ public readonly record struct ModPriority(int Value) : IAdditionOperators, IAdditionOperators, ISubtractionOperators, - ISubtractionOperators + ISubtractionOperators, + IIncrementOperators, + IComparable { public static readonly ModPriority Default = new(0); public static readonly ModPriority MaxValue = new(int.MaxValue); @@ -58,4 +60,10 @@ public readonly record struct ModPriority(int Value) : public static ModPriority operator -(ModPriority left, int right) => new(left.Value - right); + + public static ModPriority operator ++(ModPriority value) + => new(value.Value + 1); + + public int CompareTo(ModPriority other) + => Value.CompareTo(other.Value); } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 8a8e10bd..444e8e2c 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -14,12 +14,12 @@ public sealed class MultiModGroup : IModGroup public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public Setting DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } - public int OptionPriority(Index idx) + public ModPriority OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; public ISubMod this[Index idx] @@ -29,7 +29,7 @@ public sealed class MultiModGroup : IModGroup public int Count => PrioritizedOptions.Count; - public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); + public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; public IEnumerator GetEnumerator() => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); @@ -43,7 +43,7 @@ public sealed class MultiModGroup : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index be1dbde5..0bfa04f4 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -12,14 +12,14 @@ public sealed class SingleModGroup : IModGroup public GroupType Type => GroupType.Single; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public Setting DefaultSettings { get; set; } + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; - public int OptionPriority(Index _) + public ModPriority OptionPriority(Index _) => Priority; public ISubMod this[Index idx] @@ -42,7 +42,7 @@ public sealed class SingleModGroup : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -72,9 +72,9 @@ public sealed class SingleModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = Setting.Multi((int) DefaultSettings.Value), + DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; - multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, i))); + multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); return multi; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 88c4e4ce..4f35cd33 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -53,7 +53,7 @@ public sealed class SubMod : ISubMod OptionIdx = optionIdx; } - public void Load(DirectoryInfo basePath, JToken json, out int priority) + public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) { FileData.Clear(); FileSwapData.Clear(); @@ -62,7 +62,7 @@ public sealed class SubMod : ISubMod // Every option has a name, but priorities are only relevant for multi group options. Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; - priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? 0; + priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; var files = (JObject?)json[nameof(Files)]; if (files != null) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1292367a..80af7b15 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -511,7 +511,8 @@ public class ModPanelEditTab( { var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, + group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } @@ -669,10 +670,10 @@ public class ModPanelEditTab( public const int Description = -7; // Temporary strings - private static string? _currentEdit; - private static int? _currentGroupPriority; - private static int _currentField = None; - private static int _optionIndex = None; + private static string? _currentEdit; + private static ModPriority? _currentGroupPriority; + private static int _currentField = None; + private static int _optionIndex = None; public static void Reset() { @@ -705,13 +706,13 @@ public class ModPanelEditTab( return false; } - public static bool Priority(string label, int field, int option, int oldValue, out int value, float width) + public static bool Priority(string label, int field, int option, ModPriority oldValue, out ModPriority value, float width) { - var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + var tmp = (field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue).Value; ImGui.SetNextItemWidth(width); if (ImGui.InputInt(label, ref tmp, 0, 0)) { - _currentGroupPriority = tmp; + _currentGroupPriority = new ModPriority(tmp); _optionIndex = option; _currentField = field; } @@ -724,7 +725,7 @@ public class ModPanelEditTab( return ret; } - value = 0; + value = ModPriority.Default; return false; } } From 21a55b95d9a3379e3df1c8cd885780412476fc1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 15:19:44 +0200 Subject: [PATCH 1606/2451] Add rename mod field. --- Penumbra/Configuration.cs | 24 +++++----- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 49 +++++++++++++++++++- Penumbra/UI/ModsTab/RenameField.cs | 26 +++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 30 ++++++++++++ 4 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 Penumbra/UI/ModsTab/RenameField.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f91e0534..98668e8a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,6 +11,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -40,17 +41,18 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index cd0eb982..11a2d96f 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -66,7 +66,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); SubscribeRightClickMain(() => ClearQuickMove(2, _config.QuickMoveFolder3, () => {_config.QuickMoveFolder3 = string.Empty; _config.Save();}), 130); UnsubscribeRightClickLeaf(RenameLeaf); - SubscribeRightClickLeaf(RenameLeafMod, 1000); + SetRenameSearchPath(_config.ShowRename); AddButton(AddNewModButton, 0); AddButton(AddImportModButton, 1); AddButton(AddHelpButton, 2); @@ -92,6 +92,37 @@ public sealed class ModFileSystemSelector : FileSystemSelector DeleteSelectionButton(size, _config.DeleteModModifier, "mod", "mods", _modManager.DeleteMod); diff --git a/Penumbra/UI/ModsTab/RenameField.cs b/Penumbra/UI/ModsTab/RenameField.cs new file mode 100644 index 00000000..00232750 --- /dev/null +++ b/Penumbra/UI/ModsTab/RenameField.cs @@ -0,0 +1,26 @@ +namespace Penumbra.UI.ModsTab; + +public enum RenameField +{ + None, + RenameSearchPath, + RenameData, + BothSearchPathPrio, + BothDataPrio, +} + +public static class RenameFieldExtensions +{ + public static (string Name, string Desc) GetData(this RenameField value) + => value switch + { + RenameField.None => ("None", "Show no rename fields in the context menu for mods."), + RenameField.RenameSearchPath => ("Search Path", "Show only the search path / move field in the context menu for mods."), + RenameField.RenameData => ("Mod Name", "Show only the mod name field in the context menu for mods."), + RenameField.BothSearchPathPrio => ("Both (Focus Search Path)", + "Show both rename fields in the context menu for mods, but put the keyboard cursor on the search path field."), + RenameField.BothDataPrio => ("Both (Focus Mod Name)", + "Show both rename fields in the context menu for mods, but put the keyboard cursor on the mod name field"), + _ => (string.Empty, string.Empty), + }; +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c524a840..439f7be4 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -533,12 +533,42 @@ public class SettingsTab : ITab "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); } + private void DrawRenameSettings() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + using (var combo = ImRaii.Combo("##renameSettings", _config.ShowRename.GetData().Name)) + { + if (combo) + foreach (var value in Enum.GetValues()) + { + var (name, desc) = value.GetData(); + if (ImGui.Selectable(name, _config.ShowRename == value)) + { + _config.ShowRename = value; + _selector.SetRenameSearchPath(value); + _config.Save(); + } + + ImGuiUtil.HoverTooltip(desc); + } + } + + ImGui.SameLine(); + const string tt = + "Select which of the two renaming input fields are visible when opening the right-click context menu of a mod in the mod selector."; + ImGuiComponents.HelpMarker(tt); + ImGui.SameLine(); + ImGui.TextUnformatted("Rename Fields in Mod Context Menu"); + ImGuiUtil.HoverTooltip(tt); + } + /// Draw all settings pertaining to the mod selector. private void DrawModSelectorSettings() { DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); + DrawRenameSettings(); Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", _config.OpenFoldersByDefault, v => { From 7280c4b2f72c6b14b2a04b0387a1e6536429f7b9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 15:20:38 +0200 Subject: [PATCH 1607/2451] Selector improvements. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f48c6886..5de71c22 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f48c6886cbc163c5a292fa8b9fd919cb01c11d7b +Subproject commit 5de71c22c03581738c25aa43d7dff10365ec7db3 From c0ee80629df66b987b391afb4625b742dc7a298c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 16:05:55 +0200 Subject: [PATCH 1608/2451] Allow right click to paste into filter as long as it is unfocused. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 5de71c22..4673e93f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5de71c22c03581738c25aa43d7dff10365ec7db3 +Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5 From 45b1c55b67bc41fbf16aa36692c5d2657c380134 Mon Sep 17 00:00:00 2001 From: ocealot Date: Thu, 11 Apr 2024 15:00:54 -0400 Subject: [PATCH 1609/2451] Add accessory vfxs --- .../Hooks/Resources/ResolvePathHooksBase.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 6b4abf90..537992e2 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; @@ -57,6 +58,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); // @formatter:on Enable(); } @@ -179,6 +181,27 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } + private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) + { + if (slotIndex <= 4) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + // Enable vfxs for accessories + var data = Marshal.ReadIntPtr(drawObject + 0xA38); + if (data == IntPtr.Zero) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + var slot = data + 12 * (nint)slotIndex; + var model = Marshal.ReadInt16(slot); + var variant = Marshal.ReadInt16(slot + 2); + var vfxId = Marshal.ReadInt16(slot + 8); + + if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + var path = "chara/accessory/a" + model.ToString().PadLeft(4, '0') + "/vfx/eff/va" + vfxId.ToString().PadLeft(4, '0') + ".avfx"; + + MemoryHelper.WriteString(pathBuffer, path); + Marshal.WriteIntPtr(unkOutParam, 0x00000004); + return ResolvePath(drawObject, pathBuffer); + } + private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) { data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); From eb0e7e2f5fd8ec434ff6caf0b047da8c8b5ee0eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Apr 2024 21:25:00 +0200 Subject: [PATCH 1610/2451] Update ocealots code #1. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 537992e2..f322267b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,5 +1,5 @@ +using System.Text.Unicode; using Dalamud.Hooking; -using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; @@ -57,7 +57,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman); _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx); _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); // @formatter:on Enable(); @@ -183,22 +182,27 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex <= 4) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (slotIndex <= 4) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + var changedEquipData = ((Human*)drawObject)->ChangedEquipData; // Enable vfxs for accessories - var data = Marshal.ReadIntPtr(drawObject + 0xA38); - if (data == IntPtr.Zero) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var slot = data + 12 * (nint)slotIndex; - var model = Marshal.ReadInt16(slot); - var variant = Marshal.ReadInt16(slot + 2); - var vfxId = Marshal.ReadInt16(slot + 8); + var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex); + var model = slot[0]; + var variant = slot[1]; + var vfxId = slot[4]; - if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var path = "chara/accessory/a" + model.ToString().PadLeft(4, '0') + "/vfx/eff/va" + vfxId.ToString().PadLeft(4, '0') + ".avfx"; + if (model == 0 || variant == 0 || vfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - MemoryHelper.WriteString(pathBuffer, path); - Marshal.WriteIntPtr(unkOutParam, 0x00000004); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + *(ulong*)unkOutParam = 4; return ResolvePath(drawObject, pathBuffer); } From 793ed4f0a722c78b11332b391f8beeed938c896b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 00:02:09 +0200 Subject: [PATCH 1611/2451] With explicit null-termination, maybe? --- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index f322267b..4e24ba39 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -198,7 +198,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx", + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0", out _)) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); From ba8999914fd24408fb0635cffbb4da48b0dafc44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:33:57 +0200 Subject: [PATCH 1612/2451] Rework API, use Collection ID in crash handler, use collection GUIDs in more places. --- OtterGui | 2 +- Penumbra.Api | 2 +- .../Buffers/AnimationInvocationBuffer.cs | 32 +- .../Buffers/CharacterBaseBuffer.cs | 48 +- .../Buffers/ModdedFileBuffer.cs | 60 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/ApiHelpers.cs | 76 + Penumbra/Api/Api/CollectionApi.cs | 141 ++ Penumbra/Api/Api/EditingApi.cs | 40 + Penumbra/Api/Api/GameStateApi.cs | 82 + Penumbra/Api/Api/MetaApi.cs | 23 + Penumbra/Api/Api/ModSettingsApi.cs | 282 +++ Penumbra/Api/Api/ModsApi.cs | 132 ++ Penumbra/Api/Api/PenumbraApi.cs | 40 + Penumbra/Api/Api/PluginStateApi.cs | 30 + Penumbra/Api/Api/RedrawApi.cs | 27 + Penumbra/Api/Api/ResolveApi.cs | 101 + Penumbra/Api/Api/ResourceTreeApi.cs | 63 + Penumbra/Api/Api/TemporaryApi.cs | 190 ++ Penumbra/Api/Api/UiApi.cs | 101 + Penumbra/Api/DalamudSubstitutionProvider.cs | 3 +- Penumbra/Api/HttpApi.cs | 22 +- Penumbra/Api/IpcProviders.cs | 118 ++ Penumbra/Api/IpcTester.cs | 1762 ----------------- .../Api/IpcTester/CollectionsIpcTester.cs | 166 ++ Penumbra/Api/IpcTester/EditingIpcTester.cs | 70 + Penumbra/Api/IpcTester/GameStateIpcTester.cs | 137 ++ Penumbra/Api/IpcTester/IpcTester.cs | 133 ++ Penumbra/Api/IpcTester/MetaIpcTester.cs | 38 + .../Api/IpcTester/ModSettingsIpcTester.cs | 181 ++ Penumbra/Api/IpcTester/ModsIpcTester.cs | 154 ++ .../Api/IpcTester/PluginStateIpcTester.cs | 132 ++ Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 72 + Penumbra/Api/IpcTester/ResolveIpcTester.cs | 114 ++ .../Api/IpcTester/ResourceTreeIpcTester.cs | 349 ++++ Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 203 ++ Penumbra/Api/IpcTester/UiIpcTester.cs | 128 ++ Penumbra/Api/PenumbraApi.cs | 1374 ------------- Penumbra/Api/PenumbraIpcProviders.cs | 435 ---- Penumbra/Collections/Cache/CollectionCache.cs | 10 +- .../Cache/CollectionCacheManager.cs | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 160 +- .../Collections/Manager/CollectionStorage.cs | 107 +- .../Manager/IndividualCollections.Files.cs | 98 +- .../Collections/Manager/InheritanceManager.cs | 11 +- .../Manager/TempCollectionManager.cs | 53 +- Penumbra/Collections/ModCollection.cs | 35 +- Penumbra/Collections/ModCollectionSave.cs | 18 +- Penumbra/CommandHandler.cs | 2 +- Penumbra/Communication/ChangedItemClick.cs | 3 +- Penumbra/Communication/ChangedItemHover.cs | 3 +- .../Communication/CreatedCharacterBase.cs | 1 + .../Communication/CreatingCharacterBase.cs | 3 +- Penumbra/Communication/EnabledChanged.cs | 3 +- Penumbra/Communication/ModDirectoryChanged.cs | 1 + Penumbra/Communication/ModFileChanged.cs | 1 + Penumbra/Communication/ModOptionChanged.cs | 1 + Penumbra/Communication/ModPathChanged.cs | 8 +- Penumbra/Communication/ModSettingChanged.cs | 1 + Penumbra/Communication/PostEnabledDraw.cs | 3 +- .../Communication/PostSettingsPanelDraw.cs | 3 +- .../Communication/PreSettingsPanelDraw.cs | 3 +- .../Communication/PreSettingsTabBarDraw.cs | 3 +- Penumbra/GuidExtensions.cs | 254 +++ Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathResolver.cs | 10 +- .../Interop/PathResolving/SubfileHelper.cs | 2 +- .../ResourceTree/ResourceTreeApiHelper.cs | 82 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 17 +- Penumbra/Mods/Manager/ModStorage.cs | 2 +- Penumbra/Mods/Subclasses/IModGroup.cs | 18 +- Penumbra/Mods/Subclasses/ModSettings.cs | 8 +- Penumbra/Mods/Subclasses/MultiModGroup.cs | 3 + Penumbra/Mods/Subclasses/SingleModGroup.cs | 3 + Penumbra/Mods/TemporaryMod.cs | 6 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/Services/ConfigMigrationService.cs | 4 +- Penumbra/Services/CrashHandlerService.cs | 6 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/Services/StaticServiceManager.cs | 10 +- .../ModEditWindow.QuickImport.cs | 4 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 54 +- .../UI/CollectionTab/CollectionSelector.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 4 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 6 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 18 +- 88 files changed, 4193 insertions(+), 3930 deletions(-) create mode 100644 Penumbra/Api/Api/ApiHelpers.cs create mode 100644 Penumbra/Api/Api/CollectionApi.cs create mode 100644 Penumbra/Api/Api/EditingApi.cs create mode 100644 Penumbra/Api/Api/GameStateApi.cs create mode 100644 Penumbra/Api/Api/MetaApi.cs create mode 100644 Penumbra/Api/Api/ModSettingsApi.cs create mode 100644 Penumbra/Api/Api/ModsApi.cs create mode 100644 Penumbra/Api/Api/PenumbraApi.cs create mode 100644 Penumbra/Api/Api/PluginStateApi.cs create mode 100644 Penumbra/Api/Api/RedrawApi.cs create mode 100644 Penumbra/Api/Api/ResolveApi.cs create mode 100644 Penumbra/Api/Api/ResourceTreeApi.cs create mode 100644 Penumbra/Api/Api/TemporaryApi.cs create mode 100644 Penumbra/Api/Api/UiApi.cs create mode 100644 Penumbra/Api/IpcProviders.cs delete mode 100644 Penumbra/Api/IpcTester.cs create mode 100644 Penumbra/Api/IpcTester/CollectionsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/EditingIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/GameStateIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/IpcTester.cs create mode 100644 Penumbra/Api/IpcTester/MetaIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ModSettingsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ModsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/PluginStateIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/RedrawingIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ResolveIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/TemporaryIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/UiIpcTester.cs delete mode 100644 Penumbra/Api/PenumbraApi.cs delete mode 100644 Penumbra/Api/PenumbraIpcProviders.cs create mode 100644 Penumbra/GuidExtensions.cs diff --git a/OtterGui b/OtterGui index 4673e93f..9599c806 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5 +Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84 diff --git a/Penumbra.Api b/Penumbra.Api index 8787efc8..e5c8f544 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42 +Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index c92a14fd..3446530a 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -24,7 +24,7 @@ public record struct VfxFuncInvokedEntry( string InvocationType, string CharacterName, string CharacterAddress, - string CollectionName) : ICrashDataEntry; + Guid CollectionId) : ICrashDataEntry; /// Only expose the write interface for the buffer. public interface IAnimationInvocationBufferWriter @@ -32,19 +32,19 @@ public interface IAnimationInvocationBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. + /// The GUID of the associated collection. /// The type of VFX func called. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type); + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type); } internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader { private const int _version = 1; private const int _lineCount = 64; - private const int _lineCapacity = 256; + private const int _lineCapacity = 128; private const string _name = "Penumbra.AnimationInvocation"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type) + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type) { var accessor = GetCurrentLineLocking(); lock (accessor) @@ -53,10 +53,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, (int)type); accessor.Write(16, characterAddress); - var span = GetSpan(accessor, 24, 104); + var span = GetSpan(accessor, 24, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 40); WriteSpan(characterName, span); - span = GetSpan(accessor, 128); - WriteString(collectionName, span); } } @@ -68,13 +68,13 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]); - var address = BitConverter.ToUInt64(line[16..]); - var characterName = ReadString(line[24..]); - var collectionName = ReadString(line[128..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]); + var address = BitConverter.ToUInt64(line[16..]); + var collectionId = new Guid(line[24..40]); + var characterName = ReadString(line[40..]); yield return new JsonObject() { [nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds, @@ -83,7 +83,7 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation [nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type), [nameof(VfxFuncInvokedEntry.CharacterName)] = characterName, [nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName, + [nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId, }; } } diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index d83c6e6c..4036455d 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -8,8 +8,8 @@ public interface ICharacterBaseBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName); + /// The GUID of the associated collection. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId); } /// The full crash entry for a loaded character base. @@ -19,27 +19,27 @@ public record struct CharacterLoadedEntry( int ThreadId, string CharacterName, string CharacterAddress, - string CollectionName) : ICrashDataEntry; + Guid CollectionId) : ICrashDataEntry; internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader { - private const int _version = 1; - private const int _lineCount = 10; - private const int _lineCapacity = 256; - private const string _name = "Penumbra.CharacterBase"; + private const int _version = 1; + private const int _lineCount = 10; + private const int _lineCapacity = 128; + private const string _name = "Penumbra.CharacterBase"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName) + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId) { var accessor = GetCurrentLineLocking(); lock (accessor) { - accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, characterAddress); - var span = GetSpan(accessor, 20, 108); + var span = GetSpan(accessor, 20, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 36); WriteSpan(characterName, span); - span = GetSpan(accessor, 128); - WriteString(collectionName, span); } } @@ -48,20 +48,20 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var address = BitConverter.ToUInt64(line[12..]); - var characterName = ReadString(line[20..]); - var collectionName = ReadString(line[128..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var collectionId = new Guid(line[20..36]); + var characterName = ReadString(line[36..]); yield return new JsonObject { - [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, - [nameof(CharacterLoadedEntry.Timestamp)] = timestamp, - [nameof(CharacterLoadedEntry.ThreadId)] = thread, - [nameof(CharacterLoadedEntry.CharacterName)] = characterName, + [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(CharacterLoadedEntry.Timestamp)] = timestamp, + [nameof(CharacterLoadedEntry.ThreadId)] = thread, + [nameof(CharacterLoadedEntry.CharacterName)] = characterName, [nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(CharacterLoadedEntry.CollectionName)] = collectionName, + [nameof(CharacterLoadedEntry.CollectionId)] = collectionId, }; } } diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index 6c774e4b..03f63ba4 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -8,10 +8,10 @@ public interface IModdedFileBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. + /// The GUID of the associated collection. /// The file name as requested by the game. /// The actual modded file name loaded. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName, ReadOnlySpan actualFileName); } @@ -22,33 +22,33 @@ public record struct ModdedFileLoadedEntry( int ThreadId, string CharacterName, string CharacterAddress, - string CollectionName, + Guid CollectionId, string RequestedFileName, string ActualFileName) : ICrashDataEntry; internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader { - private const int _version = 1; - private const int _lineCount = 128; - private const int _lineCapacity = 1024; - private const string _name = "Penumbra.ModdedFile"; + private const int _version = 1; + private const int _lineCount = 128; + private const int _lineCapacity = 1024; + private const string _name = "Penumbra.ModdedFile"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName, ReadOnlySpan actualFileName) { var accessor = GetCurrentLineLocking(); lock (accessor) { - accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, characterAddress); - var span = GetSpan(accessor, 20, 80); + var span = GetSpan(accessor, 20, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 36, 80); WriteSpan(characterName, span); - span = GetSpan(accessor, 92, 80); - WriteString(collectionName, span); - span = GetSpan(accessor, 172, 260); + span = GetSpan(accessor, 116, 260); WriteSpan(requestedFileName, span); - span = GetSpan(accessor, 432); + span = GetSpan(accessor, 376); WriteSpan(actualFileName, span); } } @@ -61,24 +61,24 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var address = BitConverter.ToUInt64(line[12..]); - var characterName = ReadString(line[20..]); - var collectionName = ReadString(line[92..]); - var requestedFileName = ReadString(line[172..]); - var actualFileName = ReadString(line[432..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var collectionId = new Guid(line[20..36]); + var characterName = ReadString(line[36..]); + var requestedFileName = ReadString(line[116..]); + var actualFileName = ReadString(line[376..]); yield return new JsonObject() { - [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, - [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp, - [nameof(ModdedFileLoadedEntry.ThreadId)] = thread, - [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName, - [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName, + [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp, + [nameof(ModdedFileLoadedEntry.ThreadId)] = thread, + [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName, + [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"), + [nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId, [nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName, - [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName, + [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName, }; } } diff --git a/Penumbra.GameData b/Penumbra.GameData index 45679aa3..60222d79 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0 +Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186 diff --git a/Penumbra/Api/Api/ApiHelpers.cs b/Penumbra/Api/Api/ApiHelpers.cs new file mode 100644 index 00000000..32a3956f --- /dev/null +++ b/Penumbra/Api/Api/ApiHelpers.cs @@ -0,0 +1,76 @@ +using OtterGui.Log; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Api.Api; + +public class ApiHelpers( + CollectionManager collectionManager, + ObjectManager objects, + CollectionResolver collectionResolver, + ActorManager actors) : IApiService +{ + /// Return the associated identifier for an object given by its index. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + { + if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount) + return ActorIdentifier.Invalid; + + var ptr = objects[gameObjectIdx]; + return actors.FromObject(ptr, out _, false, true, true); + } + + /// + /// Return the collection associated to a current game object. If it does not exist, return the default collection. + /// If the index is invalid, returns false and the default collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) + { + collection = collectionManager.Active.Default; + if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount) + return false; + + var ptr = objects[gameObjectIdx]; + var data = collectionResolver.IdentifyCollection(ptr.AsObject, false); + if (data.Valid) + collection = data.ModCollection; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") + { + Penumbra.Log.Debug( + $"[{name}] Called with {args}, returned {ec}."); + return ec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static LazyString Args(params object[] arguments) + { + if (arguments.Length == 0) + return new LazyString(() => "no arguments"); + + return new LazyString(() => + { + var sb = new StringBuilder(); + for (var i = 0; i < arguments.Length / 2; ++i) + { + sb.Append(arguments[2 * i]); + sb.Append(" = "); + sb.Append(arguments[2 * i + 1]); + sb.Append(", "); + } + + return sb.ToString(0, sb.Length - 2); + }); + } +} diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs new file mode 100644 index 00000000..de704460 --- /dev/null +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -0,0 +1,141 @@ +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; + +namespace Penumbra.Api.Api; + +public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService +{ + public Dictionary GetCollections() + => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + + public Dictionary GetChangedItemsForCollection(Guid collectionId) + { + try + { + if (!collections.Storage.ById(collectionId, out var collection)) + collection = ModCollection.Empty; + + if (collection.HasCache) + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); + + Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded."); + return []; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}"); + throw; + } + } + + public (Guid Id, string Name)? GetCollection(ApiCollectionType type) + { + if (!Enum.IsDefined(type)) + return null; + + var collection = collections.Active.ByType((CollectionType)type); + return collection == null ? null : (collection.Id, collection.Name); + } + + internal (Guid Id, string Name)? GetCollection(byte type) + => GetCollection((ApiCollectionType)type); + + public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx) + { + var id = helpers.AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name)); + + if (collections.Active.Individuals.TryGetValue(id, out var collection)) + return (true, true, (collection.Id, collection.Name)); + + helpers.AssociatedCollection(gameObjectIdx, out collection); + return (true, false, (collection.Id, collection.Name)); + } + + public Guid[] GetCollectionByName(string name) + => collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray(); + + public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, + bool allowCreateNew, bool allowDelete) + { + if (!Enum.IsDefined(type)) + return (PenumbraApiEc.InvalidArgument, null); + + var oldCollection = collections.Active.ByType((CollectionType)type); + var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + if (collectionId == null) + { + if (old == null) + return (PenumbraApiEc.NothingChanged, old); + + if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) + return (PenumbraApiEc.AssignmentDeletionDisallowed, old); + + collections.Active.RemoveSpecialCollection((CollectionType)type); + return (PenumbraApiEc.Success, old); + } + + if (!collections.Storage.ById(collectionId.Value, out var collection)) + return (PenumbraApiEc.CollectionMissing, old); + + if (old == null) + { + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, old); + + collections.Active.CreateSpecialCollection((CollectionType)type); + } + else if (old.Value.Item1 == collection.Id) + { + return (PenumbraApiEc.NothingChanged, old); + } + + collections.Active.SetCollection(collection, (CollectionType)type); + return (PenumbraApiEc.Success, old); + } + + public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId, + bool allowCreateNew, bool allowDelete) + { + var id = helpers.AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name)); + + var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null; + var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + if (collectionId == null) + { + if (old == null) + return (PenumbraApiEc.NothingChanged, old); + + if (!allowDelete) + return (PenumbraApiEc.AssignmentDeletionDisallowed, old); + + var idx = collections.Active.Individuals.Index(id); + collections.Active.RemoveIndividualCollection(idx); + return (PenumbraApiEc.Success, old); + } + + if (!collections.Storage.ById(collectionId.Value, out var collection)) + return (PenumbraApiEc.CollectionMissing, old); + + if (old == null) + { + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, old); + + var ids = collections.Active.Individuals.GetGroup(id); + collections.Active.CreateIndividualCollection(ids); + } + else if (old.Value.Item1 == collection.Id) + { + return (PenumbraApiEc.NothingChanged, old); + } + + collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id)); + return (PenumbraApiEc.Success, old); + } +} diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs new file mode 100644 index 00000000..93345053 --- /dev/null +++ b/Penumbra/Api/Api/EditingApi.cs @@ -0,0 +1,40 @@ +using OtterGui.Services; +using Penumbra.Import.Textures; +using TextureType = Penumbra.Api.Enums.TextureType; + +namespace Penumbra.Api.Api; + +public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService +{ + public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => textureManager.SavePng(inputFile, outputFile), + TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), + TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), + TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), + TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), + TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), + TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), + TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), + TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + + // @formatter:off + public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + // @formatter:on +} diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs new file mode 100644 index 00000000..becb55ee --- /dev/null +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -0,0 +1,82 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Structs; +using Penumbra.Services; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly CollectionResolver _collectionResolver; + private readonly CutsceneService _cutsceneService; + private readonly ResourceLoader _resourceLoader; + + public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, + ResourceLoader resourceLoader) + { + _communicator = communicator; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _resourceLoader = resourceLoader; + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); + } + + public unsafe void Dispose() + { + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); + } + + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; + + public event CreatingCharacterBaseDelegate? CreatingCharacterBase + { + add + { + if (value == null) + return; + + _communicator.CreatingCharacterBase.Subscribe(new Action(value), + Communication.CreatingCharacterBase.Priority.Api); + } + remove + { + if (value == null) + return; + + _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); + } + } + + public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject) + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name)); + } + + public int GetCutsceneParentIndex(int actorIdx) + => _cutsceneService.GetParentIndex(actorIdx); + + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) + => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) + ? PenumbraApiEc.Success + : PenumbraApiEc.InvalidArgument; + + private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) + { + if (resolveData.AssociatedGameObject != nint.Zero) + GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString()); + } + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject); +} diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs new file mode 100644 index 00000000..c467df58 --- /dev/null +++ b/Penumbra/Api/Api/MetaApi.cs @@ -0,0 +1,23 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Api.Api; + +public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService +{ + public string GetPlayerMetaManipulations() + { + var collection = collectionResolver.PlayerCollection(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + } + + public string GetMetaManipulations(int gameObjectIdx) + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + } +} diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs new file mode 100644 index 00000000..2604a49d --- /dev/null +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -0,0 +1,282 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Interop.PathResolving; +using Penumbra.Mods; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable +{ + private readonly CollectionResolver _collectionResolver; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly CollectionEditor _collectionEditor; + private readonly CommunicatorService _communicator; + + public ModSettingsApi(CollectionResolver collectionResolver, + ModManager modManager, + CollectionManager collectionManager, + CollectionEditor collectionEditor, + CommunicatorService communicator) + { + _collectionResolver = collectionResolver; + _modManager = modManager; + _collectionManager = collectionManager; + _collectionEditor = collectionEditor; + _communicator = communicator; + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); + } + + public void Dispose() + { + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); + } + + public event ModSettingChangedDelegate? ModSettingChanged; + + public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return null; + + var dict = new Dictionary(mod.Groups.Count); + foreach (var g in mod.Groups) + dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); + return new AvailableModSettings(dict); + } + + public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) + => _modManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type)) + : null; + + public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, + string modName, bool ignoreInheritance) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return (PenumbraApiEc.ModMissing, null); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); + + var settings = collection.Id == Guid.Empty + ? null + : ignoreInheritance + ? collection.Settings[mod.Index] + : collection[mod.Index].Settings; + if (settings == null) + return (PenumbraApiEc.Success, null); + + var (enabled, priority, dict) = settings.ConvertToShareable(mod); + return (PenumbraApiEc.Success, + (enabled, priority.Value, dict, collection.Settings[mod.Index] == null)); + } + + public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", + inherit.ToString()); + + if (collectionId == Guid.Empty) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModInheritance(collection, mod, inherit) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModState(collection, mod, enabled) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", + optionGroupName, "OptionName", optionName); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + var setting = mod.Groups[groupIdx] switch + { + MultiModGroup => Setting.Multi(optionIdx), + SingleModGroup => Setting.Single(optionIdx), + _ => Setting.Zero, + }; + var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName, + IReadOnlyList optionNames) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", + optionGroupName, "#optionNames", optionNames.Count); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var setting = Setting.Zero; + switch (mod.Groups[groupIdx]) + { + case SingleModGroup single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting = Setting.Single(optionIdx); + break; + } + case MultiModGroup multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo) + { + var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL", + "From", modDirectoryFrom, "To", modDirectoryTo); + var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase)); + var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase)); + if (collectionId == null) + foreach (var collection in _collectionManager.Storage) + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); + else if (_collectionManager.Storage.ById(collectionId.Value, out var collection)) + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); + else + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void TriggerSettingEdited(Mod mod) + { + var collection = _collectionResolver.PlayerCollection(); + var (settings, parent) = collection[mod.Index]; + if (settings is { Enabled: true }) + ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection); + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + { + if (type == ModPathChangeType.Reloaded) + TriggerSettingEdited(mod); + } + + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) + => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + { + switch (type) + { + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.GroupMoved: + case ModOptionChangeType.GroupTypeChanged: + case ModOptionChangeType.PriorityChanged: + case ModOptionChangeType.OptionDeleted: + case ModOptionChangeType.OptionMoved: + case ModOptionChangeType.OptionFilesChanged: + case ModOptionChangeType.OptionFilesAdded: + case ModOptionChangeType.OptionSwapsChanged: + case ModOptionChangeType.OptionMetaChanged: + TriggerSettingEdited(mod); + break; + } + } + + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + TriggerSettingEdited(mod); + } +} diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs new file mode 100644 index 00000000..c1e0c684 --- /dev/null +++ b/Penumbra/Api/Api/ModsApi.cs @@ -0,0 +1,132 @@ +using OtterGui.Compression; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class ModsApi : IPenumbraApiMods, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ModManager _modManager; + private readonly ModImportManager _modImportManager; + private readonly Configuration _config; + private readonly ModFileSystem _modFileSystem; + + public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, + CommunicatorService communicator) + { + _modManager = modManager; + _modImportManager = modImportManager; + _config = config; + _modFileSystem = modFileSystem; + _communicator = communicator; + _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); + } + + private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Deleted when oldDirectory != null: + ModDeleted?.Invoke(oldDirectory.Name); + break; + case ModPathChangeType.Added when newDirectory != null: + ModAdded?.Invoke(newDirectory.Name); + break; + case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: + ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); + break; + } + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + + public Dictionary GetModList() + => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text); + + public PenumbraApiEc InstallMod(string modFilePackagePath) + { + if (!File.Exists(modFilePackagePath)) + return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath)); + + _modImportManager.AddUnpack(modFilePackagePath); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath)); + } + + public PenumbraApiEc ReloadMod(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + + _modManager.ReloadMod(mod); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + } + + public PenumbraApiEc AddMod(string modDirectory) + { + var args = ApiHelpers.Args("ModDirectory", modDirectory); + + var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); + if (!dir.Exists) + return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); + + if (_modManager.BasePath.FullName != dir.Parent?.FullName) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + _modManager.AddMod(dir); + if (_config.UseFileSystemCompression) + new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), + CompressionAlgorithm.Xpress8K); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc DeleteMod(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + + _modManager.DeleteMod(mod); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + } + + public event Action? ModDeleted; + public event Action? ModAdded; + public event Action? ModMoved; + + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) + || !_modFileSystem.FindLeaf(mod, out var leaf)) + return (PenumbraApiEc.ModMissing, string.Empty, false, false); + + var fullPath = leaf.FullName(); + var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath); + var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); + return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault ); + } + + public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) + { + if (newPath.Length == 0) + return PenumbraApiEc.InvalidArgument; + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) + || !_modFileSystem.FindLeaf(mod, out var leaf)) + return PenumbraApiEc.ModMissing; + + try + { + _modFileSystem.RenameAndMove(leaf, newPath); + return PenumbraApiEc.Success; + } + catch + { + return PenumbraApiEc.PathRenameFailed; + } + } +} diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs new file mode 100644 index 00000000..1d5b1537 --- /dev/null +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -0,0 +1,40 @@ +using OtterGui.Services; + +namespace Penumbra.Api.Api; + +public class PenumbraApi( + CollectionApi collection, + EditingApi editing, + GameStateApi gameState, + MetaApi meta, + ModsApi mods, + ModSettingsApi modSettings, + PluginStateApi pluginState, + RedrawApi redraw, + ResolveApi resolve, + ResourceTreeApi resourceTree, + TemporaryApi temporary, + UiApi ui) : IDisposable, IApiService, IPenumbraApi +{ + public void Dispose() + { + Valid = false; + } + + public (int Breaking, int Feature) ApiVersion + => (5, 0); + + public bool Valid { get; private set; } = true; + public IPenumbraApiCollection Collection { get; } = collection; + public IPenumbraApiEditing Editing { get; } = editing; + public IPenumbraApiGameState GameState { get; } = gameState; + public IPenumbraApiMeta Meta { get; } = meta; + public IPenumbraApiMods Mods { get; } = mods; + public IPenumbraApiModSettings ModSettings { get; } = modSettings; + public IPenumbraApiPluginState PluginState { get; } = pluginState; + public IPenumbraApiRedraw Redraw { get; } = redraw; + public IPenumbraApiResolve Resolve { get; } = resolve; + public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree; + public IPenumbraApiTemporary Temporary { get; } = temporary; + public IPenumbraApiUi Ui { get; } = ui; +} diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs new file mode 100644 index 00000000..2e87486f --- /dev/null +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using OtterGui.Services; +using Penumbra.Communication; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService +{ + public string GetModDirectory() + => config.ModDirectory; + + public string GetConfiguration() + => JsonConvert.SerializeObject(config, Formatting.Indented); + + public event Action? ModDirectoryChanged + { + add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value!); + } + + public bool GetEnabledState() + => config.EnableMods; + + public event Action? EnabledChange + { + add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => communicator.EnabledChanged.Unsubscribe(value!); + } +} diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs new file mode 100644 index 00000000..03b42493 --- /dev/null +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -0,0 +1,27 @@ +using Dalamud.Game.ClientState.Objects.Types; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Interop.Services; + +namespace Penumbra.Api.Api; + +public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService +{ + public void RedrawObject(int gameObjectIndex, RedrawType setting) + => redrawService.RedrawObject(gameObjectIndex, setting); + + public void RedrawObject(string name, RedrawType setting) + => redrawService.RedrawObject(name, setting); + + public void RedrawObject(GameObject? gameObject, RedrawType setting) + => redrawService.RedrawObject(gameObject, setting); + + public void RedrawAll(RedrawType setting) + => redrawService.RedrawAll(setting); + + public event GameObjectRedrawnDelegate? GameObjectRedrawn + { + add => redrawService.GameObjectRedrawn += value; + remove => redrawService.GameObjectRedrawn -= value; + } +} diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs new file mode 100644 index 00000000..ec57eba7 --- /dev/null +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -0,0 +1,101 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class ResolveApi( + ModManager modManager, + CollectionManager collectionManager, + Configuration config, + CollectionResolver collectionResolver, + ApiHelpers helpers, + IFramework framework) : IPenumbraApiResolve, IApiService +{ + public string ResolveDefaultPath(string gamePath) + => ResolvePath(gamePath, modManager, collectionManager.Active.Default); + + public string ResolveInterfacePath(string gamePath) + => ResolvePath(gamePath, modManager, collectionManager.Active.Interface); + + public string ResolveGameObjectPath(string gamePath, int gameObjectIdx) + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + return ResolvePath(gamePath, modManager, collection); + } + + public string ResolvePlayerPath(string gamePath) + => ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection()); + + public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx) + { + if (!config.EnableMods) + return [moddedPath]; + + helpers.AssociatedCollection(gameObjectIdx, out var collection); + var ret = collection.ReverseResolvePath(new FullPath(moddedPath)); + return ret.Select(r => r.ToString()).ToArray(); + } + + public string[] ReverseResolvePlayerPath(string moddedPath) + { + if (!config.EnableMods) + return [moddedPath]; + + var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath)); + return ret.Select(r => r.ToString()).ToArray(); + } + + public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) + { + if (!config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); + + var playerCollection = collectionResolver.PlayerCollection(); + var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray(); + var reverseResolved = playerCollection.ReverseResolvePaths(reverse); + return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); + } + + public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) + { + if (!config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); + + return await Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false); + var forwardTask = Task.Run(() => + { + var forwardRet = new string[forward.Length]; + Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection)); + return forwardRet; + }).ConfigureAwait(false); + var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false); + var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); + return (await forwardTask, reverseResolved); + }).ConfigureAwait(false); + } + + /// Resolve a path given by string for a specific collection. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private string ResolvePath(string path, ModManager _, ModCollection collection) + { + if (!config.EnableMods) + return path; + + var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath(gamePath); + return ret?.ToString() ?? path; + } +} diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs new file mode 100644 index 00000000..6e9aaa48 --- /dev/null +++ b/Penumbra/Api/Api/ResourceTreeApi.cs @@ -0,0 +1,63 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.GameData.Interop; +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.Api.Api; + +public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService +{ + public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj)); + } + + public Dictionary>> GetPlayerResourcePaths() + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly); + return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); + } + + public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, + params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj)); + } + + public Dictionary GetPlayerResourcesOfType(ResourceType type, + bool withUiData) + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); + return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + } + + public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj)); + } + + public Dictionary GetPlayerResourceTrees(bool withUiData) + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return resDictionary; + } +} diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs new file mode 100644 index 00000000..b4ffa8f4 --- /dev/null +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -0,0 +1,190 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class TemporaryApi( + TempCollectionManager tempCollections, + ObjectManager objects, + ActorManager actors, + CollectionManager collectionManager, + TempModManager tempMods) : IPenumbraApiTemporary, IApiService +{ + public Guid CreateTemporaryCollection(string name) + => tempCollections.CreateTemporaryCollection(name); + + public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId) + => tempCollections.RemoveTemporaryCollection(collectionId) + ? PenumbraApiEc.Success + : PenumbraApiEc.CollectionMissing; + + public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment); + if (actorIndex < 0 || actorIndex >= objects.TotalCount) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true); + if (!identifier.IsValid) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!tempCollections.CollectionById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (forceAssignment) + { + if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier)) + return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args); + } + else if (tempCollections.Collections.ContainsKey(identifier) + || collectionManager.Active.Individuals.ContainsKey(identifier)) + { + return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args); + } + + var group = tempCollections.Collections.GetGroup(identifier); + var ret = tempCollections.AddIdentifier(collection, group) + ? PenumbraApiEc.Success + : PenumbraApiEc.UnknownError; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority); + if (!ConvertPaths(paths, out var p)) + return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); + + if (!ConvertManips(manipString, out var m)) + return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); + + var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary paths, string manipString, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString", + manipString, "Priority", priority); + + if (collectionId == Guid.Empty) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!tempCollections.CollectionById(collectionId, out var collection) + && !collectionManager.Storage.ById(collectionId, out collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!ConvertPaths(paths, out var p)) + return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); + + if (!ConvertManips(manipString, out var m)) + return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); + + var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) + { + var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority)); + } + + public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority); + + if (!tempCollections.CollectionById(collectionId, out var collection) + && !collectionManager.Storage.ById(collectionId, out collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + /// + /// Convert a dictionary of strings to a dictionary of game paths to full paths. + /// Only returns true if all paths can successfully be converted and added. + /// + private static bool ConvertPaths(IReadOnlyDictionary redirections, + [NotNullWhen(true)] out Dictionary? paths) + { + paths = new Dictionary(redirections.Count); + foreach (var (gString, fString) in redirections) + { + if (!Utf8GamePath.FromString(gString, out var path, false)) + { + paths = null; + return false; + } + + var fullPath = new FullPath(fString); + if (!paths.TryAdd(path, fullPath)) + { + paths = null; + return false; + } + } + + return true; + } + + /// + /// Convert manipulations from a transmitted base64 string to actual manipulations. + /// The empty string is treated as an empty set. + /// Only returns true if all conversions are successful and distinct. + /// + private static bool ConvertManips(string manipString, + [NotNullWhen(true)] out HashSet? manips) + { + if (manipString.Length == 0) + { + manips = []; + return true; + } + + if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) + { + manips = null; + return false; + } + + manips = new HashSet(manipArray!.Length); + foreach (var manip in manipArray.Where(m => m.Validate())) + { + if (manips.Add(manip)) + continue; + + Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); + manips = null; + return false; + } + + return true; + } +} diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs new file mode 100644 index 00000000..cf3cd8f2 --- /dev/null +++ b/Penumbra/Api/Api/UiApi.cs @@ -0,0 +1,101 @@ +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI; + +namespace Penumbra.Api.Api; + +public class UiApi : IPenumbraApiUi, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _configWindow; + private readonly ModManager _modManager; + + public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) + { + _communicator = communicator; + _configWindow = configWindow; + _modManager = modManager; + _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); + _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); + } + + public void Dispose() + { + _communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover); + _communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick); + } + + public event Action? ChangedItemTooltip; + + public event Action? ChangedItemClicked; + + public event Action? PreSettingsTabBarDraw + { + add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); + remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); + } + + public event Action? PreSettingsPanelDraw + { + add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); + remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); + } + + public event Action? PostEnabledDraw + { + add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default); + remove => _communicator.PostEnabledDraw.Unsubscribe(value!); + } + + public event Action? PostSettingsPanelDraw + { + add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default); + remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); + } + + public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) + { + _configWindow.IsOpen = true; + if (!Enum.IsDefined(tab)) + return PenumbraApiEc.InvalidArgument; + + if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) + { + if (_modManager.TryGetMod(modDirectory, modName, out var mod)) + _communicator.SelectTab.Invoke(tab, mod); + else + return PenumbraApiEc.ModMissing; + } + else if (tab != TabType.None) + { + _communicator.SelectTab.Invoke(tab, null); + } + + return PenumbraApiEc.Success; + } + + public void CloseMainWindow() + => _configWindow.IsOpen = false; + + private void OnChangedItemClick(MouseButton button, object? data) + { + if (ChangedItemClicked == null) + return; + + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + ChangedItemClicked.Invoke(button, type, id); + } + + private void OnChangedItemHover(object? data) + { + if (ChangedItemTooltip == null) + return; + + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + ChangedItemTooltip.Invoke(type, id); + } +} diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 0374e31a..1c2cebcc 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -9,7 +10,7 @@ using Penumbra.String.Classes; namespace Penumbra.Api; -public class DalamudSubstitutionProvider : IDisposable +public class DalamudSubstitutionProvider : IDisposable, IApiService { private readonly ITextureSubstitutionProvider _substitution; private readonly ActiveCollectionData _activeCollectionData; diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index e23f8b4f..859c46b4 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -1,11 +1,13 @@ using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; +using OtterGui.Services; +using Penumbra.Api.Api; using Penumbra.Api.Enums; namespace Penumbra.Api; -public class HttpApi : IDisposable +public class HttpApi : IDisposable, IApiService { private partial class Controller : WebApiController { @@ -67,7 +69,7 @@ public class HttpApi : IDisposable public partial object? GetMods() { Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); - return _api.GetModList(); + return _api.Mods.GetModList(); } public async partial Task Redraw() @@ -75,17 +77,15 @@ public class HttpApi : IDisposable var data = await HttpContext.GetRequestDataAsync(); Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); if (data.ObjectTableIndex >= 0) - _api.RedrawObject(data.ObjectTableIndex, data.Type); - else if (data.Name.Length > 0) - _api.RedrawObject(data.Name, data.Type); + _api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); else - _api.RedrawAll(data.Type); + _api.Redraw.RedrawAll(data.Type); } public partial void RedrawAll() { Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered."); - _api.RedrawAll(RedrawType.Redraw); + _api.Redraw.RedrawAll(RedrawType.Redraw); } public async partial Task ReloadMod() @@ -95,10 +95,10 @@ public class HttpApi : IDisposable // Add the mod if it is not already loaded and if the directory name is given. // AddMod returns Success if the mod is already loaded. if (data.Path.Length != 0) - _api.AddMod(data.Path); + _api.Mods.AddMod(data.Path); // Reload the mod by path or name, which will also remove no-longer existing mods. - _api.ReloadMod(data.Path, data.Name); + _api.Mods.ReloadMod(data.Path, data.Name); } public async partial Task InstallMod() @@ -106,13 +106,13 @@ public class HttpApi : IDisposable var data = await HttpContext.GetRequestDataAsync(); Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}."); if (data.Path.Length != 0) - _api.InstallMod(data.Path); + _api.Mods.InstallMod(data.Path); } public partial void OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); - _api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); + _api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } private record ModReloadData(string Path, string Name) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs new file mode 100644 index 00000000..293af588 --- /dev/null +++ b/Penumbra/Api/IpcProviders.cs @@ -0,0 +1,118 @@ +using Dalamud.Plugin; +using OtterGui.Services; +using Penumbra.Api.Api; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public sealed class IpcProviders : IDisposable, IApiService +{ + private readonly List _providers; + + private readonly EventProvider _disposedProvider; + private readonly EventProvider _initializedProvider; + + public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api) + { + _disposedProvider = IpcSubscribers.Disposed.Provider(pi); + _initializedProvider = IpcSubscribers.Initialized.Provider(pi); + _providers = + [ + IpcSubscribers.GetCollections.Provider(pi, api.Collection), + IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection), + IpcSubscribers.GetCollection.Provider(pi, api.Collection), + IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), + IpcSubscribers.SetCollection.Provider(pi, api.Collection), + IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection), + + IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing), + IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing), + + IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState), + IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState), + IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState), + IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState), + IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), + IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState), + + IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta), + IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta), + + IpcSubscribers.GetModList.Provider(pi, api.Mods), + IpcSubscribers.InstallMod.Provider(pi, api.Mods), + IpcSubscribers.ReloadMod.Provider(pi, api.Mods), + IpcSubscribers.AddMod.Provider(pi, api.Mods), + IpcSubscribers.DeleteMod.Provider(pi, api.Mods), + IpcSubscribers.ModDeleted.Provider(pi, api.Mods), + IpcSubscribers.ModAdded.Provider(pi, api.Mods), + IpcSubscribers.ModMoved.Provider(pi, api.Mods), + IpcSubscribers.GetModPath.Provider(pi, api.Mods), + IpcSubscribers.SetModPath.Provider(pi, api.Mods), + + IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings), + IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings), + + IpcSubscribers.ApiVersion.Provider(pi, api), + IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), + IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), + IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), + IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), + IpcSubscribers.EnabledChange.Provider(pi, api.PluginState), + + IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), + IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), + IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), + + IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), + IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve), + IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve), + IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve), + + IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree), + IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree), + IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree), + + IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary), + IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary), + + IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), + IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui), + IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), + IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), + ]; + _initializedProvider.Invoke(); + } + + public void Dispose() + { + foreach (var provider in _providers) + provider.Dispose(); + _providers.Clear(); + _initializedProvider.Dispose(); + _disposedProvider.Invoke(); + _disposedProvider.Dispose(); + } +} diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs deleted file mode 100644 index 898c5de3..00000000 --- a/Penumbra/Api/IpcTester.cs +++ /dev/null @@ -1,1762 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Mods; -using Dalamud.Utility; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.UI; -using Penumbra.Collections.Manager; -using Dalamud.Plugin.Services; -using Penumbra.GameData.Structs; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; - -namespace Penumbra.Api; - -public class IpcTester : IDisposable -{ - private readonly PenumbraIpcProviders _ipcProviders; - private bool _subscribed = true; - - private readonly PluginState _pluginState; - private readonly IpcConfiguration _ipcConfiguration; - private readonly Ui _ui; - private readonly Redrawing _redrawing; - private readonly GameState _gameState; - private readonly Resolve _resolve; - private readonly Collections _collections; - private readonly Meta _meta; - private readonly Mods _mods; - private readonly ModSettings _modSettings; - private readonly Editing _editing; - private readonly Temporary _temporary; - private readonly ResourceTree _resourceTree; - - public IpcTester(Configuration config, DalamudPluginInterface pi, ObjectManager objects, IClientState clientState, - PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, - TempCollectionManager tempCollections, SaveService saveService) - { - _ipcProviders = ipcProviders; - _pluginState = new PluginState(pi); - _ipcConfiguration = new IpcConfiguration(pi); - _ui = new Ui(pi); - _redrawing = new Redrawing(pi, objects, clientState); - _gameState = new GameState(pi); - _resolve = new Resolve(pi); - _collections = new Collections(pi); - _meta = new Meta(pi); - _mods = new Mods(pi); - _modSettings = new ModSettings(pi); - _editing = new Editing(pi); - _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config); - _resourceTree = new ResourceTree(pi, objects); - UnsubscribeEvents(); - } - - public void Draw() - { - try - { - SubscribeEvents(); - ImGui.TextUnformatted($"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}"); - _pluginState.Draw(); - _ipcConfiguration.Draw(); - _ui.Draw(); - _redrawing.Draw(); - _gameState.Draw(); - _resolve.Draw(); - _collections.Draw(); - _meta.Draw(); - _mods.Draw(); - _modSettings.Draw(); - _editing.Draw(); - _temporary.Draw(); - _temporary.DrawCollections(); - _temporary.DrawMods(); - _resourceTree.Draw(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Error during IPC Tests:\n{e}"); - } - } - - private void SubscribeEvents() - { - if (!_subscribed) - { - _pluginState.Initialized.Enable(); - _pluginState.Disposed.Enable(); - _pluginState.EnabledChange.Enable(); - _redrawing.Redrawn.Enable(); - _ui.PreSettingsDraw.Enable(); - _ui.PostSettingsDraw.Enable(); - _modSettings.SettingChanged.Enable(); - _gameState.CharacterBaseCreating.Enable(); - _gameState.CharacterBaseCreated.Enable(); - _ipcConfiguration.ModDirectoryChanged.Enable(); - _gameState.GameObjectResourcePathResolved.Enable(); - _mods.DeleteSubscriber.Enable(); - _mods.AddSubscriber.Enable(); - _mods.MoveSubscriber.Enable(); - _subscribed = true; - } - } - - public void UnsubscribeEvents() - { - if (_subscribed) - { - _pluginState.Initialized.Disable(); - _pluginState.Disposed.Disable(); - _pluginState.EnabledChange.Disable(); - _redrawing.Redrawn.Disable(); - _ui.PreSettingsDraw.Disable(); - _ui.PostSettingsDraw.Disable(); - _ui.Tooltip.Disable(); - _ui.Click.Disable(); - _modSettings.SettingChanged.Disable(); - _gameState.CharacterBaseCreating.Disable(); - _gameState.CharacterBaseCreated.Disable(); - _ipcConfiguration.ModDirectoryChanged.Disable(); - _gameState.GameObjectResourcePathResolved.Disable(); - _mods.DeleteSubscriber.Disable(); - _mods.AddSubscriber.Disable(); - _mods.MoveSubscriber.Disable(); - _subscribed = false; - } - } - - public void Dispose() - { - _pluginState.Initialized.Dispose(); - _pluginState.Disposed.Dispose(); - _pluginState.EnabledChange.Dispose(); - _redrawing.Redrawn.Dispose(); - _ui.PreSettingsDraw.Dispose(); - _ui.PostSettingsDraw.Dispose(); - _ui.Tooltip.Dispose(); - _ui.Click.Dispose(); - _modSettings.SettingChanged.Dispose(); - _gameState.CharacterBaseCreating.Dispose(); - _gameState.CharacterBaseCreated.Dispose(); - _ipcConfiguration.ModDirectoryChanged.Dispose(); - _gameState.GameObjectResourcePathResolved.Dispose(); - _mods.DeleteSubscriber.Dispose(); - _mods.AddSubscriber.Dispose(); - _mods.MoveSubscriber.Dispose(); - _subscribed = false; - } - - private static void DrawIntro(string label, string info) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(label); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(info); - ImGui.TableNextColumn(); - } - - - private class PluginState - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber Initialized; - public readonly EventSubscriber Disposed; - public readonly EventSubscriber EnabledChange; - - private readonly List _initializedList = new(); - private readonly List _disposedList = new(); - - private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; - private bool? _lastEnabledValue; - - public PluginState(DalamudPluginInterface pi) - { - _pi = pi; - Initialized = Ipc.Initialized.Subscriber(pi, AddInitialized); - Disposed = Ipc.Disposed.Subscriber(pi, AddDisposed); - EnabledChange = Ipc.EnabledChange.Subscriber(pi, SetLastEnabled); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Plugin State"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - void DrawList(string label, string text, List list) - { - DrawIntro(label, text); - if (list.Count == 0) - { - ImGui.TextUnformatted("Never"); - } - else - { - ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture)); - if (list.Count > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", - list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture)))); - } - } - - DrawList(Ipc.Initialized.Label, "Last Initialized", _initializedList); - DrawList(Ipc.Disposed.Label, "Last Disposed", _disposedList); - DrawIntro(Ipc.ApiVersions.Label, "Current Version"); - var (breaking, features) = Ipc.ApiVersions.Subscriber(_pi).Invoke(); - ImGui.TextUnformatted($"{breaking}.{features:D4}"); - DrawIntro(Ipc.GetEnabledState.Label, "Current State"); - ImGui.TextUnformatted($"{Ipc.GetEnabledState.Subscriber(_pi).Invoke()}"); - DrawIntro(Ipc.EnabledChange.Label, "Last Change"); - ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); - } - - private void AddInitialized() - => _initializedList.Add(DateTimeOffset.UtcNow); - - private void AddDisposed() - => _disposedList.Add(DateTimeOffset.UtcNow); - - private void SetLastEnabled(bool val) - => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); - } - - private class IpcConfiguration - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber ModDirectoryChanged; - - private string _currentConfiguration = string.Empty; - private string _lastModDirectory = string.Empty; - private bool _lastModDirectoryValid; - private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; - - public IpcConfiguration(DalamudPluginInterface pi) - { - _pi = pi; - ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Configuration"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetModDirectory.Label, "Current Mod Directory"); - ImGui.TextUnformatted(Ipc.GetModDirectory.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change"); - ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue - ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}" - : "None"); - DrawIntro(Ipc.GetConfiguration.Label, "Configuration"); - if (ImGui.Button("Get")) - { - _currentConfiguration = Ipc.GetConfiguration.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Config Popup"); - } - - DrawConfigPopup(); - } - - private void DrawConfigPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var popup = ImRaii.Popup("Config Popup"); - if (!popup) - return; - - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImGuiUtil.TextWrapped(_currentConfiguration); - } - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - - private void UpdateModDirectoryChanged(string path, bool valid) - => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now); - } - - private class Ui - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber PreSettingsDraw; - public readonly EventSubscriber PostSettingsDraw; - public readonly EventSubscriber Tooltip; - public readonly EventSubscriber Click; - - private string _lastDrawnMod = string.Empty; - private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; - private bool _subscribedToTooltip; - private bool _subscribedToClick; - private string _lastClicked = string.Empty; - private string _lastHovered = string.Empty; - private TabType _selectTab = TabType.None; - private string _modName = string.Empty; - private PenumbraApiEc _ec = PenumbraApiEc.Success; - - public Ui(DalamudPluginInterface pi) - { - _pi = pi; - PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); - PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); - Tooltip = Ipc.ChangedItemTooltip.Subscriber(pi, AddedTooltip); - Click = Ipc.ChangedItemClick.Subscriber(pi, AddedClick); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("UI"); - if (!_) - return; - - using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString())) - { - if (combo) - foreach (var val in Enum.GetValues()) - { - if (ImGui.Selectable(val.ToString(), _selectTab == val)) - _selectTab = val; - } - } - - ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.PostSettingsDraw.Label, "Last Drawn Mod"); - ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); - - DrawIntro(Ipc.ChangedItemTooltip.Label, "Add Tooltip"); - if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip)) - { - if (_subscribedToTooltip) - Tooltip.Enable(); - else - Tooltip.Disable(); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastHovered); - - DrawIntro(Ipc.ChangedItemClick.Label, "Subscribe Click"); - if (ImGui.Checkbox("##click", ref _subscribedToClick)) - { - if (_subscribedToClick) - Click.Enable(); - else - Click.Disable(); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastClicked); - DrawIntro(Ipc.OpenMainWindow.Label, "Open Mod Window"); - if (ImGui.Button("Open##window")) - _ec = Ipc.OpenMainWindow.Subscriber(_pi).Invoke(_selectTab, _modName, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_ec.ToString()); - - DrawIntro(Ipc.CloseMainWindow.Label, "Close Mod Window"); - if (ImGui.Button("Close##window")) - Ipc.CloseMainWindow.Subscriber(_pi).Invoke(); - } - - private void UpdateLastDrawnMod(string name) - => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); - - private void AddedTooltip(ChangedItemType type, uint id) - { - _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; - ImGui.TextUnformatted("IPC Test Successful"); - } - - private void AddedClick(MouseButton button, ChangedItemType type, uint id) - { - _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; - } - } - - private class Redrawing - { - private readonly DalamudPluginInterface _pi; - private readonly IClientState _clientState; - private readonly ObjectManager _objects; - public readonly EventSubscriber Redrawn; - - private string _redrawName = string.Empty; - private int _redrawIndex; - private string _lastRedrawnString = "None"; - - public Redrawing(DalamudPluginInterface pi, ObjectManager objects, IClientState clientState) - { - _pi = pi; - _objects = objects; - _clientState = clientState; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Redrawing"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.RedrawObjectByName.Label, "Redraw by Name"); - ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32); - ImGui.SameLine(); - if (ImGui.Button("Redraw##Name")) - Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character"); - if (ImGui.Button("Redraw##pc") && _clientState.LocalPlayer != null) - Ipc.RedrawObject.Subscriber(_pi).Invoke(_clientState.LocalPlayer, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); - var tmp = _redrawIndex; - ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount)) - _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount); - - ImGui.SameLine(); - if (ImGui.Button("Redraw##Index")) - Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawAll.Label, "Redraw All"); - if (ImGui.Button("Redraw##All")) - Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw); - - DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:"); - ImGui.TextUnformatted(_lastRedrawnString); - } - - private void SetLastRedrawn(IntPtr address, int index) - { - if (index < 0 - || index > _objects.TotalCount - || address == IntPtr.Zero - || _objects[index].Address != address) - _lastRedrawnString = "Invalid"; - - _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})"; - } - } - - private class GameState - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber CharacterBaseCreating; - public readonly EventSubscriber CharacterBaseCreated; - public readonly EventSubscriber GameObjectResourcePathResolved; - - - private string _lastCreatedGameObjectName = string.Empty; - private IntPtr _lastCreatedDrawObject = IntPtr.Zero; - private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; - private string _lastResolvedGamePath = string.Empty; - private string _lastResolvedFullPath = string.Empty; - private string _lastResolvedObject = string.Empty; - private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; - private string _currentDrawObjectString = string.Empty; - private IntPtr _currentDrawObject = IntPtr.Zero; - private int _currentCutsceneActor; - private int _currentCutsceneParent; - private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; - - public GameState(DalamudPluginInterface pi) - { - _pi = pi; - CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); - CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Game State"); - if (!_) - return; - - if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, - ImGuiInputTextFlags.CharsHexadecimal)) - _currentDrawObject = IntPtr.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, - out var tmp) - ? tmp - : IntPtr.Zero; - - ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); - ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0); - if (_cutsceneError is not PenumbraApiEc.Success) - { - ImGui.SameLine(); - ImGui.TextUnformatted("Invalid Argument on last Call"); - } - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetDrawObjectInfo.Label, "Draw Object Info"); - if (_currentDrawObject == IntPtr.Zero) - { - ImGui.TextUnformatted("Invalid"); - } - else - { - var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber(_pi).Invoke(_currentDrawObject); - ImGui.TextUnformatted(ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}"); - } - - DrawIntro(Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent"); - ImGui.TextUnformatted(Ipc.GetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor).ToString()); - - DrawIntro(Ipc.SetCutsceneParentIndex.Label, "Cutscene Parent"); - if (ImGui.Button("Set Parent")) - _cutsceneError = Ipc.SetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor, _currentCutsceneParent); - - DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created"); - if (_lastCreatedGameObjectTime < DateTimeOffset.Now) - ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero - ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" - : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"); - - DrawIntro(Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved"); - if (_lastResolvedGamePathTime < DateTimeOffset.Now) - ImGui.TextUnformatted( - $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}"); - } - - private void UpdateLastCreated(IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4) - { - _lastCreatedGameObjectName = GetObjectName(gameObject); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = IntPtr.Zero; - } - - private void UpdateLastCreated2(IntPtr gameObject, string _, IntPtr drawObject) - { - _lastCreatedGameObjectName = GetObjectName(gameObject); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = drawObject; - } - - private void UpdateGameObjectResourcePath(IntPtr gameObject, string gamePath, string fullPath) - { - _lastResolvedObject = GetObjectName(gameObject); - _lastResolvedGamePath = gamePath; - _lastResolvedFullPath = fullPath; - _lastResolvedGamePathTime = DateTimeOffset.Now; - } - - private static unsafe string GetObjectName(IntPtr gameObject) - { - var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; - var name = obj != null ? obj->Name : null; - return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; - } - } - - private class Resolve(DalamudPluginInterface pi) - { - private string _currentResolvePath = string.Empty; - private string _currentResolveCharacter = string.Empty; - private string _currentReversePath = string.Empty; - private int _currentReverseIdx; - private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); - - public void Draw() - { - using var tree = ImRaii.TreeNode("Resolving"); - if (!tree) - return; - - ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); - ImGui.InputTextWithHint("##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32); - ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, - Utf8GamePath.MaxGamePathLength); - ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve"); - if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(pi).Invoke(_currentResolvePath, _currentResolveCharacter)); - - DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(pi).Invoke(_currentResolvePath, _currentReverseIdx)); - - DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolvePath.Subscriber(pi).Invoke(_currentReversePath, _currentResolveCharacter); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolvePlayerPath.Subscriber(pi).Invoke(_currentReversePath); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolveGameObjectPath.Subscriber(pi).Invoke(_currentReversePath, _currentReverseIdx); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - var forwardArray = _currentResolvePath.Length > 0 - ? [_currentResolvePath] - : Array.Empty(); - var reverseArray = _currentReversePath.Length > 0 - ? [_currentReversePath] - : Array.Empty(); - - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); - if (forwardArray.Length > 0 || reverseArray.Length > 0) - { - var ret = Ipc.ResolvePlayerPaths.Subscriber(pi).Invoke(forwardArray, reverseArray); - ImGui.TextUnformatted(ConvertText(ret)); - } - - DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); - if (ImGui.Button("Start")) - _task = Ipc.ResolvePlayerPathsAsync.Subscriber(pi).Invoke(forwardArray, reverseArray); - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_task.Status.ToString()); - if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) - ImGui.SetTooltip(ConvertText(_task.Result)); - return; - - static string ConvertText((string[], string[][]) data) - { - var text = string.Empty; - if (data.Item1.Length > 0) - { - if (data.Item2.Length > 0) - text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}."; - else - text = $"Forward: {data.Item1[0]}."; - } - else if (data.Item2.Length > 0) - { - text = $"Reverse: {string.Join("; ", data.Item2[0])}."; - } - - return text; - } - } - } - - private class Collections - { - private readonly DalamudPluginInterface _pi; - - private int _objectIdx; - private string _collectionName = string.Empty; - private bool _allowCreation = true; - private bool _allowDeletion = true; - private ApiCollectionType _type = ApiCollectionType.Current; - - private string _characterCollectionName = string.Empty; - private IList _collections = []; - private string _changedItemCollection = string.Empty; - private IReadOnlyDictionary _changedItems = new Dictionary(); - private PenumbraApiEc _returnCode = PenumbraApiEc.Success; - private string? _oldCollection; - - public Collections(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Collections"); - if (!_) - return; - - ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); - ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); - ImGui.InputText("Collection Name##Collections", ref _collectionName, 64); - ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); - ImGui.SameLine(); - ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Return Code", _returnCode.ToString()); - if (_oldCollection != null) - ImGui.TextUnformatted(_oldCollection.Length == 0 ? "Created" : _oldCollection); - - DrawIntro(Ipc.GetCurrentCollectionName.Label, "Current Collection"); - ImGui.TextUnformatted(Ipc.GetCurrentCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetDefaultCollectionName.Label, "Default Collection"); - ImGui.TextUnformatted(Ipc.GetDefaultCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetInterfaceCollectionName.Label, "Interface Collection"); - ImGui.TextUnformatted(Ipc.GetInterfaceCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetCharacterCollectionName.Label, "Character"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.InputTextWithHint("##characterCollectionName", "Character Name...", ref _characterCollectionName, 64); - var (c, s) = Ipc.GetCharacterCollectionName.Subscriber(_pi).Invoke(_characterCollectionName); - ImGui.SameLine(); - ImGui.TextUnformatted($"{c}, {(s ? "Custom" : "Default")}"); - - DrawIntro(Ipc.GetCollections.Label, "Collections"); - if (ImGui.Button("Get##Collections")) - { - _collections = Ipc.GetCollections.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Collections"); - } - - DrawIntro(Ipc.GetCollectionForType.Label, "Get Special Collection"); - var name = Ipc.GetCollectionForType.Subscriber(_pi).Invoke(_type); - ImGui.TextUnformatted(name.Length == 0 ? "Unassigned" : name); - DrawIntro(Ipc.SetCollectionForType.Label, "Set Special Collection"); - if (ImGui.Button("Set##TypeCollection")) - (_returnCode, _oldCollection) = - Ipc.SetCollectionForType.Subscriber(_pi).Invoke(_type, _collectionName, _allowCreation, _allowDeletion); - - DrawIntro(Ipc.GetCollectionForObject.Label, "Get Object Collection"); - (var valid, var individual, name) = Ipc.GetCollectionForObject.Subscriber(_pi).Invoke(_objectIdx); - ImGui.TextUnformatted( - $"{(valid ? "Valid" : "Invalid")} Object, {(name.Length == 0 ? "Unassigned" : name)}{(individual ? " (Individual Assignment)" : string.Empty)}"); - DrawIntro(Ipc.SetCollectionForObject.Label, "Set Object Collection"); - if (ImGui.Button("Set##ObjectCollection")) - (_returnCode, _oldCollection) = Ipc.SetCollectionForObject.Subscriber(_pi) - .Invoke(_objectIdx, _collectionName, _allowCreation, _allowDeletion); - - if (_returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty()) - _oldCollection = null; - - DrawIntro(Ipc.GetChangedItems.Label, "Changed Item List"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.InputTextWithHint("##changedCollection", "Collection Name...", ref _changedItemCollection, 64); - ImGui.SameLine(); - if (ImGui.Button("Get")) - { - _changedItems = Ipc.GetChangedItems.Subscriber(_pi).Invoke(_changedItemCollection); - ImGui.OpenPopup("Changed Item List"); - } - - DrawChangedItemPopup(); - DrawCollectionPopup(); - } - - private void DrawChangedItemPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Changed Item List"); - if (!p) - return; - - foreach (var item in _changedItems) - ImGui.TextUnformatted(item.Key); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - - private void DrawCollectionPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Collections"); - if (!p) - return; - - foreach (var collection in _collections) - ImGui.TextUnformatted(collection); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - } - - private class Meta - { - private readonly DalamudPluginInterface _pi; - - private string _characterName = string.Empty; - private int _gameObjectIndex; - - public Meta(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Meta"); - if (!_) - return; - - ImGui.InputTextWithHint("##characterName", "Character Name...", ref _characterName, 64); - ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetMetaManipulations.Label, "Meta Manipulations"); - if (ImGui.Button("Copy to Clipboard")) - { - var base64 = Ipc.GetMetaManipulations.Subscriber(_pi).Invoke(_characterName); - ImGui.SetClipboardText(base64); - } - - DrawIntro(Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations"); - if (ImGui.Button("Copy to Clipboard##Player")) - { - var base64 = Ipc.GetPlayerMetaManipulations.Subscriber(_pi).Invoke(); - ImGui.SetClipboardText(base64); - } - - DrawIntro(Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations"); - if (ImGui.Button("Copy to Clipboard##GameObject")) - { - var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber(_pi).Invoke(_gameObjectIndex); - ImGui.SetClipboardText(base64); - } - } - } - - private class Mods - { - private readonly DalamudPluginInterface _pi; - - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; - private string _newInstallPath = string.Empty; - private PenumbraApiEc _lastReloadEc; - private PenumbraApiEc _lastAddEc; - private PenumbraApiEc _lastDeleteEc; - private PenumbraApiEc _lastSetPathEc; - private PenumbraApiEc _lastInstallEc; - private IList<(string, string)> _mods = new List<(string, string)>(); - - public readonly EventSubscriber DeleteSubscriber; - public readonly EventSubscriber AddSubscriber; - public readonly EventSubscriber MoveSubscriber; - - private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; - private string _lastDeletedMod = string.Empty; - private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch; - private string _lastAddedMod = string.Empty; - private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch; - private string _lastMovedModFrom = string.Empty; - private string _lastMovedModTo = string.Empty; - - public Mods(DalamudPluginInterface pi) - { - _pi = pi; - DeleteSubscriber = Ipc.ModDeleted.Subscriber(pi, s => - { - _lastDeletedModTime = DateTimeOffset.UtcNow; - _lastDeletedMod = s; - }); - AddSubscriber = Ipc.ModAdded.Subscriber(pi, s => - { - _lastAddedModTime = DateTimeOffset.UtcNow; - _lastAddedMod = s; - }); - MoveSubscriber = Ipc.ModMoved.Subscriber(pi, (s1, s2) => - { - _lastMovedModTime = DateTimeOffset.UtcNow; - _lastMovedModFrom = s1; - _lastMovedModTo = s2; - }); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Mods"); - if (!_) - return; - - ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100); - ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); - ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); - ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetMods.Label, "Mods"); - if (ImGui.Button("Get##Mods")) - { - _mods = Ipc.GetMods.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Mods"); - } - - DrawIntro(Ipc.ReloadMod.Label, "Reload Mod"); - if (ImGui.Button("Reload")) - _lastReloadEc = Ipc.ReloadMod.Subscriber(_pi).Invoke(_modDirectory, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastReloadEc.ToString()); - - DrawIntro(Ipc.InstallMod.Label, "Install Mod"); - if (ImGui.Button("Install")) - _lastInstallEc = Ipc.InstallMod.Subscriber(_pi).Invoke(_newInstallPath); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastInstallEc.ToString()); - - DrawIntro(Ipc.AddMod.Label, "Add Mod"); - if (ImGui.Button("Add")) - _lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastAddEc.ToString()); - - DrawIntro(Ipc.DeleteMod.Label, "Delete Mod"); - if (ImGui.Button("Delete")) - _lastDeleteEc = Ipc.DeleteMod.Subscriber(_pi).Invoke(_modDirectory, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastDeleteEc.ToString()); - - DrawIntro(Ipc.GetModPath.Label, "Current Path"); - var (ec, path, def) = Ipc.GetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName); - ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")}) [{ec}]"); - - DrawIntro(Ipc.SetModPath.Label, "Set Path"); - if (ImGui.Button("Set")) - _lastSetPathEc = Ipc.SetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName, _pathInput); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastSetPathEc.ToString()); - - DrawIntro(Ipc.ModDeleted.Label, "Last Mod Deleted"); - if (_lastDeletedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}"); - - DrawIntro(Ipc.ModAdded.Label, "Last Mod Added"); - if (_lastAddedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}"); - - DrawIntro(Ipc.ModMoved.Label, "Last Mod Moved"); - if (_lastMovedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}"); - - DrawModsPopup(); - } - - private void DrawModsPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Mods"); - if (!p) - return; - - foreach (var (modDir, modName) in _mods) - ImGui.TextUnformatted($"{modDir}: {modName}"); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - } - - private class ModSettings - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber SettingChanged; - - private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; - private ModSettingChange _lastSettingChangeType; - private string _lastSettingChangeCollection = string.Empty; - private string _lastSettingChangeMod = string.Empty; - private bool _lastSettingChangeInherited; - private DateTimeOffset _lastSettingChange; - - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private string _settingsCollection = string.Empty; - private bool _settingsAllowInheritance = true; - private bool _settingsInherit; - private bool _settingsEnabled; - private int _settingsPriority; - private IDictionary, GroupType)>? _availableSettings; - private IDictionary>? _currentSettings; - - public ModSettings(DalamudPluginInterface pi) - { - _pi = pi; - SettingChanged = Ipc.ModSettingChanged.Subscriber(pi, UpdateLastModSetting); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Mod Settings"); - if (!_) - return; - - ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); - ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); - ImGui.InputTextWithHint("##settingsCollection", "Collection...", ref _settingsCollection, 100); - ImGui.Checkbox("Allow Inheritance", ref _settingsAllowInheritance); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Error", _lastSettingsError.ToString()); - DrawIntro(Ipc.ModSettingChanged.Label, "Last Mod Setting Changed"); - ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0 - ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}" - : "None"); - DrawIntro(Ipc.GetAvailableModSettings.Label, "Get Available Settings"); - if (ImGui.Button("Get##Available")) - { - _availableSettings = Ipc.GetAvailableModSettings.Subscriber(_pi).Invoke(_settingsModDirectory, _settingsModName); - _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; - } - - - DrawIntro(Ipc.GetCurrentModSettings.Label, "Get Current Settings"); - if (ImGui.Button("Get##Current")) - { - var ret = Ipc.GetCurrentModSettings.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance); - _lastSettingsError = ret.Item1; - if (ret.Item1 == PenumbraApiEc.Success) - { - _settingsEnabled = ret.Item2?.Item1 ?? false; - _settingsInherit = ret.Item2?.Item4 ?? false; - _settingsPriority = ret.Item2?.Item2 ?? 0; - _currentSettings = ret.Item2?.Item3; - } - else - { - _currentSettings = null; - } - } - - DrawIntro(Ipc.TryInheritMod.Label, "Inherit Mod"); - ImGui.Checkbox("##inherit", ref _settingsInherit); - ImGui.SameLine(); - if (ImGui.Button("Set##Inherit")) - _lastSettingsError = Ipc.TryInheritMod.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit); - - DrawIntro(Ipc.TrySetMod.Label, "Set Enabled"); - ImGui.Checkbox("##enabled", ref _settingsEnabled); - ImGui.SameLine(); - if (ImGui.Button("Set##Enabled")) - _lastSettingsError = Ipc.TrySetMod.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled); - - DrawIntro(Ipc.TrySetModPriority.Label, "Set Priority"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.DragInt("##Priority", ref _settingsPriority); - ImGui.SameLine(); - if (ImGui.Button("Set##Priority")) - _lastSettingsError = Ipc.TrySetModPriority.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority); - - DrawIntro(Ipc.CopyModSettings.Label, "Copy Mod Settings"); - if (ImGui.Button("Copy Settings")) - _lastSettingsError = Ipc.CopyModSettings.Subscriber(_pi).Invoke(_settingsCollection, _settingsModDirectory, _settingsModName); - - ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection."); - - DrawIntro(Ipc.TrySetModSetting.Label, "Set Setting(s)"); - if (_availableSettings == null) - return; - - foreach (var (group, (list, type)) in _availableSettings) - { - using var id = ImRaii.PushId(group); - var preview = list.Count > 0 ? list[0] : string.Empty; - IList current; - if (_currentSettings != null && _currentSettings.TryGetValue(group, out current!) && current.Count > 0) - { - preview = current[0]; - } - else - { - current = new List(); - if (_currentSettings != null) - _currentSettings[group] = current; - } - - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - using (var c = ImRaii.Combo("##group", preview)) - { - if (c) - foreach (var s in list) - { - var contained = current.Contains(s); - if (ImGui.Checkbox(s, ref contained)) - { - if (contained) - current.Add(s); - else - current.Remove(s); - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Set##setting")) - { - if (type == GroupType.Single) - _lastSettingsError = Ipc.TrySetModSetting.Subscriber(_pi).Invoke(_settingsCollection, - _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[0] : string.Empty); - else - _lastSettingsError = Ipc.TrySetModSettings.Subscriber(_pi).Invoke(_settingsCollection, - _settingsModDirectory, _settingsModName, group, current.ToArray()); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(group); - } - } - - private void UpdateLastModSetting(ModSettingChange type, string collection, string mod, bool inherited) - { - _lastSettingChangeType = type; - _lastSettingChangeCollection = collection; - _lastSettingChangeMod = mod; - _lastSettingChangeInherited = inherited; - _lastSettingChange = DateTimeOffset.Now; - } - } - - private class Editing - { - private readonly DalamudPluginInterface _pi; - - private string _inputPath = string.Empty; - private string _inputPath2 = string.Empty; - private string _outputPath = string.Empty; - private string _outputPath2 = string.Empty; - - private TextureType _typeSelector; - private bool _mipMaps = true; - - private Task? _task1; - private Task? _task2; - - public Editing(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Editing"); - if (!_) - return; - - ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); - ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); - ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); - ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); - TypeCombo(); - ImGui.Checkbox("Add MipMaps", ref _mipMaps); - - using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 1"); - if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) - _task1 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); - ImGui.SameLine(); - ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); - if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) - ImGui.SetTooltip(_task1.Exception?.ToString()); - - DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 2"); - if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) - _task2 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); - ImGui.SameLine(); - ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); - if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) - ImGui.SetTooltip(_task2.Exception?.ToString()); - } - - private void TypeCombo() - { - using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); - if (!combo) - return; - - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), _typeSelector == value)) - _typeSelector = value; - } - } - } - - private class Temporary - { - private readonly DalamudPluginInterface _pi; - private readonly ModManager _modManager; - private readonly CollectionManager _collections; - private readonly TempModManager _tempMods; - private readonly TempCollectionManager _tempCollections; - private readonly SaveService _saveService; - private readonly Configuration _config; - - public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, - TempCollectionManager tempCollections, SaveService saveService, Configuration config) - { - _pi = pi; - _modManager = modManager; - _collections = collections; - _tempMods = tempMods; - _tempCollections = tempCollections; - _saveService = saveService; - _config = config; - } - - public string LastCreatedCollectionName = string.Empty; - - private string _tempCollectionName = string.Empty; - private string _tempCharacterName = string.Empty; - private string _tempModName = string.Empty; - private string _tempGamePath = "test/game/path.mtrl"; - private string _tempFilePath = "test/success.mtrl"; - private string _tempManipulation = string.Empty; - private PenumbraApiEc _lastTempError; - private int _tempActorIndex; - private bool _forceOverwrite; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Temporary"); - if (!_) - return; - - ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); - ImGui.InputTextWithHint("##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32); - ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); - ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); - ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); - ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); - ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); - - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Error", _lastTempError.ToString()); - DrawIntro("Last Created Collection", LastCreatedCollectionName); - DrawIntro(Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection"); -#pragma warning disable 0612 - if (ImGui.Button("Create##Collection")) - (_lastTempError, LastCreatedCollectionName) = Ipc.CreateTemporaryCollection.Subscriber(_pi) - .Invoke(_tempCollectionName, _tempCharacterName, _forceOverwrite); - - DrawIntro(Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection"); - if (ImGui.Button("Create##NamedCollection")) - _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName); - - DrawIntro(Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character"); - if (ImGui.Button("Delete##Collection")) - _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber(_pi).Invoke(_tempCharacterName); -#pragma warning restore 0612 - DrawIntro(Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection"); - if (ImGui.Button("Delete##NamedCollection")) - _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber(_pi).Invoke(_tempCollectionName); - - DrawIntro(Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection"); - if (ImGui.Button("Assign##NamedCollection")) - _lastTempError = Ipc.AssignTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName, _tempActorIndex, _forceOverwrite); - - DrawIntro(Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection"); - if (ImGui.Button("Add##Mod")) - _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, - new Dictionary { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); - - DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); - if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, - "Copies the effective list from the collection named in Temporary Mod Name...", - !_collections.Storage.ByName(_tempModName, out var copyCollection)) - && copyCollection is { HasCache: true }) - { - var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); - var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), - MetaManipulation.CurrentVersion); - _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, files, manips, 999); - } - - DrawIntro(Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections"); - if (ImGui.Button("Add##All")) - _lastTempError = Ipc.AddTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, - new Dictionary { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); - - DrawIntro(Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection"); - if (ImGui.Button("Remove##Mod")) - _lastTempError = Ipc.RemoveTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, int.MaxValue); - - DrawIntro(Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); - if (ImGui.Button("Remove##ModAll")) - _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, int.MaxValue); - } - - public void DrawCollections() - { - using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); - if (!collTree) - return; - - using var table = ImRaii.Table("##collTree", 5); - if (!table) - return; - - foreach (var collection in _tempCollections.Values) - { - ImGui.TableNextColumn(); - var character = _tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) - .FirstOrDefault() - ?? "Unknown"; - if (ImGui.Button($"Save##{collection.Name}")) - TemporaryMod.SaveTempCollection(_config, _saveService, _modManager, collection, character); - - ImGuiUtil.DrawTableColumn(collection.Name); - ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); - ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); - ImGuiUtil.DrawTableColumn(string.Join(", ", - _tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); - } - } - - public void DrawMods() - { - using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods"); - if (!modTree) - return; - - using var table = ImRaii.Table("##modTree", 5); - - void PrintList(string collectionName, IReadOnlyList list) - { - foreach (var mod in list) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Name); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Priority.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(collectionName); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Default.Files.Count.ToString()); - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - foreach (var (path, file) in mod.Default.Files) - ImGui.TextUnformatted($"{path} -> {file}"); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.TotalManipulations.ToString()); - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - foreach (var manip in mod.Default.Manipulations) - ImGui.TextUnformatted(manip.ToString()); - } - } - } - - if (table) - { - PrintList("All", _tempMods.ModsForAllCollections); - foreach (var (collection, list) in _tempMods.Mods) - PrintList(collection.Name, list); - } - } - } - - private class ResourceTree(DalamudPluginInterface pi, ObjectManager objects) - { - private readonly Stopwatch _stopwatch = new(); - - private string _gameObjectIndices = "0"; - private ResourceType _type = ResourceType.Mtrl; - private bool _withUiData; - - private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; - private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; - private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; - private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; - private (string, Ipc.ResourceTree?)[]? _lastGameObjectResourceTrees; - private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees; - private TimeSpan _lastCallDuration; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Resource Tree"); - if (!_) - return; - - ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); - ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); - ImGui.Checkbox("Also get names and icons", ref _withUiData); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); - if (ImGui.Button("Get##GameObjectResourcePaths")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(pi); - _stopwatch.Restart(); - var resourcePaths = subscriber.Invoke(gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourcePaths = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(resourcePaths) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcePaths)); - } - - DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); - if (ImGui.Button("Get##PlayerResourcePaths")) - { - var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(pi); - _stopwatch.Restart(); - var resourcePaths = subscriber.Invoke(); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourcePaths = resourcePaths - .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcePaths)); - } - - DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); - if (ImGui.Button("Get##GameObjectResourcesOfType")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(pi); - _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourcesOfType = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(resourcesOfType) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcesOfType)); - } - - DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); - if (ImGui.Button("Get##PlayerResourcesOfType")) - { - var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(pi); - _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUiData); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourcesOfType = resourcesOfType - .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); - } - - DrawIntro(Ipc.GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); - if (ImGui.Button("Get##GameObjectResourceTrees")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(pi); - _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUiData, gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourceTrees = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(trees) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourceTrees)); - } - - DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees"); - if (ImGui.Button("Get##PlayerResourceTrees")) - { - var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(pi); - _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUiData); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourceTrees = trees - .Select(pair => (GameObjectToString(pair.Key), pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourceTrees)); - } - - DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); - - DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); - - DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); - } - - private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); - using var popup = ImRaii.Popup(popupId); - if (!popup) - { - result = null; - return; - } - - if (result == null) - { - ImGui.CloseCurrentPopup(); - return; - } - - drawResult(result); - - ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - { - result = null; - ImGui.CloseCurrentPopup(); - } - } - - private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class - { - var firstSeen = new Dictionary(); - foreach (var (label, item) in result) - { - if (item == null) - { - ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); - continue; - } - - if (firstSeen.TryGetValue(item, out var firstLabel)) - { - ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); - continue; - } - - firstSeen.Add(item, label); - - using var header = ImRaii.TreeNode(label); - if (!header) - continue; - - drawItem(item); - } - } - - private static void DrawResourcePaths((string, IReadOnlyDictionary?)[] result) - { - DrawWithHeaders(result, paths => - { - using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); - ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); - ImGui.TableHeadersRow(); - - foreach (var (actualPath, gamePaths) in paths) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(actualPath); - ImGui.TableNextColumn(); - foreach (var gamePath in gamePaths) - ImGui.TextUnformatted(gamePath); - } - }); - } - - private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) - { - DrawWithHeaders(result, resources => - { - using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f); - if (_withUiData) - ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); - ImGui.TableHeadersRow(); - - foreach (var (resourceHandle, (actualPath, name, icon)) in resources) - { - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{resourceHandle:X}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(actualPath); - if (_withUiData) - { - ImGui.TableNextColumn(); - TextUnformattedMono(icon.ToString()); - ImGui.SameLine(); - ImGui.TextUnformatted(name); - } - } - }); - } - - private void DrawResourceTrees((string, Ipc.ResourceTree?)[] result) - { - DrawWithHeaders(result, tree => - { - ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); - - using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); - if (!table) - return; - - if (_withUiData) - { - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); - ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); - } - else - { - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); - } - - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImGui.TableHeadersRow(); - - void DrawNode(Ipc.ResourceNode node) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - var hasChildren = node.Children.Any(); - using var treeNode = ImRaii.TreeNode( - $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", - hasChildren - ? ImGuiTreeNodeFlags.SpanFullWidth - : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); - if (_withUiData) - { - ImGui.TableNextColumn(); - TextUnformattedMono(node.Type.ToString()); - ImGui.TableNextColumn(); - TextUnformattedMono(node.Icon.ToString()); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(node.GamePath ?? "Unknown"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(node.ActualPath); - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{node.ObjectAddress:X8}"); - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{node.ResourceHandle:X8}"); - - if (treeNode) - foreach (var child in node.Children) - DrawNode(child); - } - - foreach (var node in tree.Nodes) - DrawNode(node); - }); - } - - private static void TextUnformattedMono(string text) - { - using var _ = ImRaii.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(text); - } - - private ushort[] GetSelectedGameObjects() - => _gameObjectIndices.Split(',') - .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) - .ToArray(); - - private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) - { - var gameObject = objects[gameObjectIndex]; - - return gameObject.Valid - ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})" - : $"[{gameObjectIndex}] null"; - } - } -} diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs new file mode 100644 index 00000000..12314f0c --- /dev/null +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -0,0 +1,166 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Enums; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Penumbra.Api.IpcTester; + +public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService +{ + private int _objectIdx; + private string _collectionIdString = string.Empty; + private Guid? _collectionId = null; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Yourself; + + private Dictionary _collections = []; + private (string, ChangedItemType, uint)[] _changedItems = []; + private PenumbraApiEc _returnCode = PenumbraApiEc.Success; + private (Guid Id, string Name)? _oldCollection; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Collections"); + if (!_) + return; + + ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); + ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); + ImGuiUtil.GuidInput("Collection Id##Collections", "Collection GUID...", string.Empty, ref _collectionId, ref _collectionIdString); + ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); + ImGui.SameLine(); + ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); + + using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Return Code", _returnCode.ToString()); + if (_oldCollection != null) + ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString()); + + IpcTester.DrawIntro(GetCollection.Label, "Current Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current)); + + IpcTester.DrawIntro(GetCollection.Label, "Default Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default)); + + IpcTester.DrawIntro(GetCollection.Label, "Interface Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface)); + + IpcTester.DrawIntro(GetCollection.Label, "Special Collection"); + DrawCollection(new GetCollection(pi).Invoke(_type)); + + IpcTester.DrawIntro(GetCollections.Label, "Collections"); + DrawCollectionPopup(); + if (ImGui.Button("Get##Collections")) + { + _collections = new GetCollections(pi).Invoke(); + ImGui.OpenPopup("Collections"); + } + + IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection"); + var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx); + DrawCollection(effectiveCollection); + ImGui.SameLine(); + ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}"); + + IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection"); + if (ImGui.Button("Set##SpecialCollection")) + (_returnCode, _oldCollection) = + new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion); + ImGui.TableNextColumn(); + if (ImGui.Button("Remove##SpecialCollection")) + (_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion); + + IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection"); + if (ImGui.Button("Set##ObjectCollection")) + (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty), + _allowCreation, _allowDeletion); + ImGui.TableNextColumn(); + if (ImGui.Button("Remove##ObjectCollection")) + (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion); + + IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List"); + DrawChangedItemPopup(); + if (ImGui.Button("Get##ChangedItems")) + { + var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty)); + _changedItems = items.Select(kvp => + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value); + return (kvp.Key, type, id); + }).ToArray(); + ImGui.OpenPopup("Changed Item List"); + } + } + + private void DrawChangedItemPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Changed Item List"); + if (!p) + return; + + using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) + { + if (t) + ImGuiClip.ClippedDraw(_changedItems, t => + { + ImGuiUtil.DrawTableColumn(t.Item1); + ImGuiUtil.DrawTableColumn(t.Item2.ToString()); + ImGuiUtil.DrawTableColumn(t.Item3.ToString()); + }, ImGui.GetTextLineHeightWithSpacing()); + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private void DrawCollectionPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Collections"); + if (!p) + return; + + using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit)) + { + if (t) + foreach (var collection in _collections) + { + ImGui.TableNextColumn(); + DrawCollection((collection.Key, collection.Value)); + } + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private static void DrawCollection((Guid Id, string Name)? collection) + { + if (collection == null) + { + ImGui.TextUnformatted(""); + ImGui.TableNextColumn(); + return; + } + + ImGui.TextUnformatted(collection.Value.Name); + ImGui.TableNextColumn(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString()); + } + } +} diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs new file mode 100644 index 00000000..94b1e4e8 --- /dev/null +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -0,0 +1,70 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class EditingIpcTester(DalamudPluginInterface pi) : IUiService +{ + private string _inputPath = string.Empty; + private string _inputPath2 = string.Empty; + private string _outputPath = string.Empty; + private string _outputPath2 = string.Empty; + + private TextureType _typeSelector; + private bool _mipMaps = true; + + private Task? _task1; + private Task? _task2; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Editing"); + if (!_) + return; + + ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); + ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); + ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); + ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); + TypeCombo(); + ImGui.Checkbox("Add MipMaps", ref _mipMaps); + + using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1"); + if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) + _task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); + if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task1.Exception?.ToString()); + + IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2"); + if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) + _task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); + if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task2.Exception?.ToString()); + } + + private void TypeCombo() + { + using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); + if (!combo) + return; + + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), _typeSelector == value)) + _typeSelector = value; + } + } +} diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs new file mode 100644 index 00000000..2c41b882 --- /dev/null +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -0,0 +1,137 @@ +using Dalamud.Interface; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.String; + +namespace Penumbra.Api.IpcTester; + +public class GameStateIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber CharacterBaseCreating; + public readonly EventSubscriber CharacterBaseCreated; + public readonly EventSubscriber GameObjectResourcePathResolved; + + private string _lastCreatedGameObjectName = string.Empty; + private nint _lastCreatedDrawObject = nint.Zero; + private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; + private string _lastResolvedGamePath = string.Empty; + private string _lastResolvedFullPath = string.Empty; + private string _lastResolvedObject = string.Empty; + private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; + private string _currentDrawObjectString = string.Empty; + private nint _currentDrawObject = nint.Zero; + private int _currentCutsceneActor; + private int _currentCutsceneParent; + private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; + + public GameStateIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + CharacterBaseCreating = CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); + CharacterBaseCreated = CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); + GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); + } + + public void Dispose() + { + CharacterBaseCreating.Dispose(); + CharacterBaseCreated.Dispose(); + GameObjectResourcePathResolved.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Game State"); + if (!_) + return; + + if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, + ImGuiInputTextFlags.CharsHexadecimal)) + _currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var tmp) + ? tmp + : nint.Zero; + + ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); + ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0); + if (_cutsceneError is not PenumbraApiEc.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted("Invalid Argument on last Call"); + } + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info"); + if (_currentDrawObject == nint.Zero) + { + ImGui.TextUnformatted("Invalid"); + } + else + { + var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject); + ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TextUnformatted(collectionId.ToString()); + } + } + + IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent"); + ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString()); + + IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent"); + if (ImGui.Button("Set Parent")) + _cutsceneError = new SetCutsceneParentIndex(_pi) + .Invoke(_currentCutsceneActor, _currentCutsceneParent); + + IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created"); + if (_lastCreatedGameObjectTime < DateTimeOffset.Now) + ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero + ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" + : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"); + + IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved"); + if (_lastResolvedGamePathTime < DateTimeOffset.Now) + ImGui.TextUnformatted( + $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}"); + } + + private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4) + { + _lastCreatedGameObjectName = GetObjectName(gameObject); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = nint.Zero; + } + + private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject) + { + _lastCreatedGameObjectName = GetObjectName(gameObject); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = drawObject; + } + + private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath) + { + _lastResolvedObject = GetObjectName(gameObject); + _lastResolvedGamePath = gamePath; + _lastResolvedFullPath = fullPath; + _lastResolvedGamePathTime = DateTimeOffset.Now; + } + + private static unsafe string GetObjectName(nint gameObject) + { + var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; + var name = obj != null ? obj->Name : null; + return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; + } +} diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs new file mode 100644 index 00000000..201e7068 --- /dev/null +++ b/Penumbra/Api/IpcTester/IpcTester.cs @@ -0,0 +1,133 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using ImGuiNET; +using OtterGui.Services; +using Penumbra.Api.Api; + +namespace Penumbra.Api.IpcTester; + +public class IpcTester( + IpcProviders ipcProviders, + IPenumbraApi api, + PluginStateIpcTester pluginStateIpcTester, + UiIpcTester uiIpcTester, + RedrawingIpcTester redrawingIpcTester, + GameStateIpcTester gameStateIpcTester, + ResolveIpcTester resolveIpcTester, + CollectionsIpcTester collectionsIpcTester, + MetaIpcTester metaIpcTester, + ModsIpcTester modsIpcTester, + ModSettingsIpcTester modSettingsIpcTester, + EditingIpcTester editingIpcTester, + TemporaryIpcTester temporaryIpcTester, + ResourceTreeIpcTester resourceTreeIpcTester, + IFramework framework) : IUiService +{ + private readonly IpcProviders _ipcProviders = ipcProviders; + private DateTime _lastUpdate; + private bool _subscribed = false; + + public void Draw() + { + try + { + _lastUpdate = framework.LastUpdateUTC.AddSeconds(1); + Subscribe(); + + ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}"); + collectionsIpcTester.Draw(); + editingIpcTester.Draw(); + gameStateIpcTester.Draw(); + metaIpcTester.Draw(); + modSettingsIpcTester.Draw(); + modsIpcTester.Draw(); + pluginStateIpcTester.Draw(); + redrawingIpcTester.Draw(); + resolveIpcTester.Draw(); + resourceTreeIpcTester.Draw(); + uiIpcTester.Draw(); + temporaryIpcTester.Draw(); + temporaryIpcTester.DrawCollections(); + temporaryIpcTester.DrawMods(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error during IPC Tests:\n{e}"); + } + } + + internal static void DrawIntro(string label, string info) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(label); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(info); + ImGui.TableNextColumn(); + } + + private void Subscribe() + { + if (_subscribed) + return; + + Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); + gameStateIpcTester.GameObjectResourcePathResolved.Enable(); + gameStateIpcTester.CharacterBaseCreated.Enable(); + gameStateIpcTester.CharacterBaseCreating.Enable(); + modSettingsIpcTester.SettingChanged.Enable(); + modsIpcTester.DeleteSubscriber.Enable(); + modsIpcTester.AddSubscriber.Enable(); + modsIpcTester.MoveSubscriber.Enable(); + pluginStateIpcTester.ModDirectoryChanged.Enable(); + pluginStateIpcTester.Initialized.Enable(); + pluginStateIpcTester.Disposed.Enable(); + pluginStateIpcTester.EnabledChange.Enable(); + redrawingIpcTester.Redrawn.Enable(); + uiIpcTester.PreSettingsTabBar.Enable(); + uiIpcTester.PreSettingsPanel.Enable(); + uiIpcTester.PostEnabled.Enable(); + uiIpcTester.PostSettingsPanelDraw.Enable(); + uiIpcTester.ChangedItemTooltip.Enable(); + uiIpcTester.ChangedItemClicked.Enable(); + + framework.Update += CheckUnsubscribe; + _subscribed = true; + } + + private void CheckUnsubscribe(IFramework framework1) + { + if (_lastUpdate > framework.LastUpdateUTC) + return; + + Unsubscribe(); + framework.Update -= CheckUnsubscribe; + } + + private void Unsubscribe() + { + if (!_subscribed) + return; + + Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester."); + _subscribed = false; + gameStateIpcTester.GameObjectResourcePathResolved.Disable(); + gameStateIpcTester.CharacterBaseCreated.Disable(); + gameStateIpcTester.CharacterBaseCreating.Disable(); + modSettingsIpcTester.SettingChanged.Disable(); + modsIpcTester.DeleteSubscriber.Disable(); + modsIpcTester.AddSubscriber.Disable(); + modsIpcTester.MoveSubscriber.Disable(); + pluginStateIpcTester.ModDirectoryChanged.Disable(); + pluginStateIpcTester.Initialized.Disable(); + pluginStateIpcTester.Disposed.Disable(); + pluginStateIpcTester.EnabledChange.Disable(); + redrawingIpcTester.Redrawn.Disable(); + uiIpcTester.PreSettingsTabBar.Disable(); + uiIpcTester.PreSettingsPanel.Disable(); + uiIpcTester.PostEnabled.Disable(); + uiIpcTester.PostSettingsPanelDraw.Disable(); + uiIpcTester.ChangedItemTooltip.Disable(); + uiIpcTester.ChangedItemClicked.Disable(); + } +} diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs new file mode 100644 index 00000000..3fa7de7f --- /dev/null +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class MetaIpcTester(DalamudPluginInterface pi) : IUiService +{ + private int _gameObjectIndex; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Meta"); + if (!_) + return; + + ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations"); + if (ImGui.Button("Copy to Clipboard##Player")) + { + var base64 = new GetPlayerMetaManipulations(pi).Invoke(); + ImGui.SetClipboardText(base64); + } + + IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations"); + if (ImGui.Button("Copy to Clipboard##GameObject")) + { + var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex); + ImGui.SetClipboardText(base64); + } + } +} diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs new file mode 100644 index 00000000..c33fcdee --- /dev/null +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -0,0 +1,181 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.UI; + +namespace Penumbra.Api.IpcTester; + +public class ModSettingsIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber SettingChanged; + + private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; + private ModSettingChange _lastSettingChangeType; + private Guid _lastSettingChangeCollection = Guid.Empty; + private string _lastSettingChangeMod = string.Empty; + private bool _lastSettingChangeInherited; + private DateTimeOffset _lastSettingChange; + + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private Guid? _settingsCollection; + private string _settingsCollectionName = string.Empty; + private bool _settingsIgnoreInheritance; + private bool _settingsInherit; + private bool _settingsEnabled; + private int _settingsPriority; + private IReadOnlyDictionary? _availableSettings; + private Dictionary>? _currentSettings; + + public ModSettingsIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); + } + + public void Dispose() + { + SettingChanged.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Mod Settings"); + if (!_) + return; + + ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); + ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); + ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName); + ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance); + var collection = _settingsCollection.GetValueOrDefault(Guid.Empty); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString()); + + IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed"); + ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0 + ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}" + : "None"); + + IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings"); + if (ImGui.Button("Get##Available")) + { + _availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName); + _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; + } + + IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings"); + if (ImGui.Button("Get##Current")) + { + var ret = new GetCurrentModSettings(_pi) + .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance); + _lastSettingsError = ret.Item1; + if (ret.Item1 == PenumbraApiEc.Success) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod"); + ImGui.Checkbox("##inherit", ref _settingsInherit); + ImGui.SameLine(); + if (ImGui.Button("Set##Inherit")) + _lastSettingsError = new TryInheritMod(_pi) + .Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName); + + IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled"); + ImGui.Checkbox("##enabled", ref _settingsEnabled); + ImGui.SameLine(); + if (ImGui.Button("Set##Enabled")) + _lastSettingsError = new TrySetMod(_pi) + .Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName); + + IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority"); + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + ImGui.DragInt("##Priority", ref _settingsPriority); + ImGui.SameLine(); + if (ImGui.Button("Set##Priority")) + _lastSettingsError = new TrySetModPriority(_pi) + .Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName); + + IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings"); + if (ImGui.Button("Copy Settings")) + _lastSettingsError = new CopyModSettings(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName); + + ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection."); + + IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)"); + if (_availableSettings == null) + return; + + foreach (var (group, (list, type)) in _availableSettings) + { + using var id = ImRaii.PushId(group); + var preview = list.Length > 0 ? list[0] : string.Empty; + if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0) + { + preview = current[0]; + } + else + { + current = []; + if (_currentSettings != null) + _currentSettings[group] = current; + } + + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + using (var c = ImRaii.Combo("##group", preview)) + { + if (c) + foreach (var s in list) + { + var contained = current.Contains(s); + if (ImGui.Checkbox(s, ref contained)) + { + if (contained) + current.Add(s); + else + current.Remove(s); + } + } + } + + ImGui.SameLine(); + if (ImGui.Button("Set##setting")) + _lastSettingsError = type == GroupType.Single + ? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty, + _settingsModName) + : new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName); + + ImGui.SameLine(); + ImGui.TextUnformatted(group); + } + } + + private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited) + { + _lastSettingChangeType = type; + _lastSettingChangeCollection = collection; + _lastSettingChangeMod = mod; + _lastSettingChangeInherited = inherited; + _lastSettingChange = DateTimeOffset.Now; + } +} diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs new file mode 100644 index 00000000..878a8214 --- /dev/null +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -0,0 +1,154 @@ +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class ModsIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private string _newInstallPath = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private PenumbraApiEc _lastInstallEc; + private Dictionary _mods = []; + + public readonly EventSubscriber DeleteSubscriber; + public readonly EventSubscriber AddSubscriber; + public readonly EventSubscriber MoveSubscriber; + + private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; + private string _lastDeletedMod = string.Empty; + private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch; + private string _lastAddedMod = string.Empty; + private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch; + private string _lastMovedModFrom = string.Empty; + private string _lastMovedModTo = string.Empty; + + public ModsIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + DeleteSubscriber = ModDeleted.Subscriber(pi, s => + { + _lastDeletedModTime = DateTimeOffset.UtcNow; + _lastDeletedMod = s; + }); + AddSubscriber = ModAdded.Subscriber(pi, s => + { + _lastAddedModTime = DateTimeOffset.UtcNow; + _lastAddedMod = s; + }); + MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) => + { + _lastMovedModTime = DateTimeOffset.UtcNow; + _lastMovedModFrom = s1; + _lastMovedModTo = s2; + }); + } + + public void Dispose() + { + DeleteSubscriber.Dispose(); + AddSubscriber.Dispose(); + MoveSubscriber.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Mods"); + if (!_) + return; + + ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100); + ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); + ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); + ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetModList.Label, "Mods"); + DrawModsPopup(); + if (ImGui.Button("Get##Mods")) + { + _mods = new GetModList(_pi).Invoke(); + ImGui.OpenPopup("Mods"); + } + + IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod"); + if (ImGui.Button("Reload")) + _lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastReloadEc.ToString()); + + IpcTester.DrawIntro(InstallMod.Label, "Install Mod"); + if (ImGui.Button("Install")) + _lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastInstallEc.ToString()); + + IpcTester.DrawIntro(AddMod.Label, "Add Mod"); + if (ImGui.Button("Add")) + _lastAddEc = new AddMod(_pi).Invoke(_modDirectory); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastAddEc.ToString()); + + IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod"); + if (ImGui.Button("Delete")) + _lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastDeleteEc.ToString()); + + IpcTester.DrawIntro(GetModPath.Label, "Current Path"); + var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName); + ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]"); + + IpcTester.DrawIntro(SetModPath.Label, "Set Path"); + if (ImGui.Button("Set")) + _lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastSetPathEc.ToString()); + + IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted"); + if (_lastDeletedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}"); + + IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added"); + if (_lastAddedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}"); + + IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved"); + if (_lastMovedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}"); + } + + private void DrawModsPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Mods"); + if (!p) + return; + + foreach (var (modDir, modName) in _mods) + ImGui.TextUnformatted($"{modDir}: {modName}"); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } +} diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs new file mode 100644 index 00000000..0588e5bd --- /dev/null +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -0,0 +1,132 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class PluginStateIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber ModDirectoryChanged; + public readonly EventSubscriber Initialized; + public readonly EventSubscriber Disposed; + public readonly EventSubscriber EnabledChange; + + private string _currentConfiguration = string.Empty; + private string _lastModDirectory = string.Empty; + private bool _lastModDirectoryValid; + private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; + + private readonly List _initializedList = []; + private readonly List _disposedList = []; + + private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; + private bool? _lastEnabledValue; + + public PluginStateIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); + Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized); + Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed); + EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled); + } + + public void Dispose() + { + ModDirectoryChanged.Dispose(); + Initialized.Dispose(); + Disposed.Dispose(); + EnabledChange.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Plugin State"); + if (!_) + return; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList); + DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList); + + IpcTester.DrawIntro(ApiVersion.Label, "Current Version"); + var (breaking, features) = new ApiVersion(_pi).Invoke(); + ImGui.TextUnformatted($"{breaking}.{features:D4}"); + + IpcTester.DrawIntro(GetEnabledState.Label, "Current State"); + ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}"); + + IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change"); + ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); + + DrawConfigPopup(); + IpcTester.DrawIntro(GetConfiguration.Label, "Configuration"); + if (ImGui.Button("Get")) + { + _currentConfiguration = new GetConfiguration(_pi).Invoke(); + ImGui.OpenPopup("Config Popup"); + } + + IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory"); + ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke()); + + IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change"); + ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue + ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}" + : "None"); + + void DrawList(string label, string text, List list) + { + IpcTester.DrawIntro(label, text); + if (list.Count == 0) + { + ImGui.TextUnformatted("Never"); + } + else + { + ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture)); + if (list.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", + list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture)))); + } + } + } + + private void DrawConfigPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var popup = ImRaii.Popup("Config Popup"); + if (!popup) + return; + + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.TextWrapped(_currentConfiguration); + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private void UpdateModDirectoryChanged(string path, bool valid) + => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now); + + private void AddInitialized() + => _initializedList.Add(DateTimeOffset.UtcNow); + + private void AddDisposed() + => _disposedList.Add(DateTimeOffset.UtcNow); + + private void SetLastEnabled(bool val) + => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); +} diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs new file mode 100644 index 00000000..281c7ad4 --- /dev/null +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -0,0 +1,72 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.GameData.Interop; +using Penumbra.UI; + +namespace Penumbra.Api.IpcTester; + +public class RedrawingIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + private readonly ObjectManager _objects; + public readonly EventSubscriber Redrawn; + + private int _redrawIndex; + private string _lastRedrawnString = "None"; + + public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) + { + _pi = pi; + _objects = objects; + Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + } + + public void Dispose() + { + Redrawn.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Redrawing"); + if (!_) + return; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index"); + var tmp = _redrawIndex; + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount)) + _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount); + ImGui.SameLine(); + if (ImGui.Button("Redraw##Index")) + new RedrawObject(_pi).Invoke(_redrawIndex); + + IpcTester.DrawIntro(RedrawAll.Label, "Redraw All"); + if (ImGui.Button("Redraw##All")) + new RedrawAll(_pi).Invoke(); + + IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:"); + ImGui.TextUnformatted(_lastRedrawnString); + } + + private void SetLastRedrawn(nint address, int index) + { + if (index < 0 + || index > _objects.TotalCount + || address == nint.Zero + || _objects[index].Address != address) + _lastRedrawnString = "Invalid"; + + _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})"; + } +} diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs new file mode 100644 index 00000000..978ed8d6 --- /dev/null +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -0,0 +1,114 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.IpcSubscribers; +using Penumbra.String.Classes; + +namespace Penumbra.Api.IpcTester; + +public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService +{ + private string _currentResolvePath = string.Empty; + private string _currentReversePath = string.Empty; + private int _currentReverseIdx; + private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); + + public void Draw() + { + using var tree = ImRaii.TreeNode("Resolving"); + if (!tree) + return; + + ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); + ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, + Utf8GamePath.MaxGamePathLength); + ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx)); + + IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); + if (_currentReversePath.Length > 0) + { + var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath); + if (list.Length > 0) + { + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); + } + } + + IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); + if (_currentReversePath.Length > 0) + { + var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx); + if (list.Length > 0) + { + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); + } + } + + var forwardArray = _currentResolvePath.Length > 0 + ? [_currentResolvePath] + : Array.Empty(); + var reverseArray = _currentReversePath.Length > 0 + ? [_currentReversePath] + : Array.Empty(); + + IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (forwardArray.Length > 0 || reverseArray.Length > 0) + { + var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray); + ImGui.TextUnformatted(ConvertText(ret)); + } + + IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); + if (ImGui.Button("Start")) + _task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_task.Status.ToString()); + if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) + ImGui.SetTooltip(ConvertText(_task.Result)); + return; + + static string ConvertText((string[], string[][]) data) + { + var text = string.Empty; + if (data.Item1.Length > 0) + { + if (data.Item2.Length > 0) + text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}."; + else + text = $"Forward: {data.Item1[0]}."; + } + else if (data.Item2.Length > 0) + { + text = $"Reverse: {string.Join("; ", data.Item2[0])}."; + } + + return text; + } + } +} diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs new file mode 100644 index 00000000..1f57fc9d --- /dev/null +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -0,0 +1,349 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Penumbra.Api.IpcTester; + +public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService +{ + private readonly Stopwatch _stopwatch = new(); + + private string _gameObjectIndices = "0"; + private ResourceType _type = ResourceType.Mtrl; + private bool _withUiData; + + private (string, Dictionary>?)[]? _lastGameObjectResourcePaths; + private (string, Dictionary>?)[]? _lastPlayerResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees; + private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees; + private TimeSpan _lastCallDuration; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Resource Tree"); + if (!_) + return; + + ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); + ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); + ImGui.Checkbox("Also get names and icons", ref _withUiData); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); + if (ImGui.Button("Get##GameObjectResourcePaths")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourcePaths(pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourcePaths = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(resourcePaths) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourcePaths)); + } + + IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths"); + if (ImGui.Button("Get##PlayerResourcePaths")) + { + var subscriber = new GetPlayerResourcePaths(pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcePaths = resourcePaths + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray()!; + + ImGui.OpenPopup(nameof(GetPlayerResourcePaths)); + } + + IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); + if (ImGui.Button("Get##GameObjectResourcesOfType")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourcesOfType(pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourcesOfType = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(resourcesOfType) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType)); + } + + IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type"); + if (ImGui.Button("Get##PlayerResourcesOfType")) + { + var subscriber = new GetPlayerResourcesOfType(pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUiData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcesOfType = resourcesOfType + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(GetPlayerResourcesOfType)); + } + + IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); + if (ImGui.Button("Get##GameObjectResourceTrees")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourceTrees(pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUiData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourceTrees = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(trees) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourceTrees)); + } + + IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees"); + if (ImGui.Button("Get##PlayerResourceTrees")) + { + var subscriber = new GetPlayerResourceTrees(pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUiData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourceTrees = trees + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(GetPlayerResourceTrees)); + } + + DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration); + + DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, + _lastCallDuration); + + DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); + } + + private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); + using var popup = ImRaii.Popup(popupId); + if (!popup) + { + result = null; + return; + } + + if (result == null) + { + ImGui.CloseCurrentPopup(); + return; + } + + drawResult(result); + + ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + { + result = null; + ImGui.CloseCurrentPopup(); + } + } + + private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class + { + var firstSeen = new Dictionary(); + foreach (var (label, item) in result) + { + if (item == null) + { + ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + if (firstSeen.TryGetValue(item, out var firstLabel)) + { + ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + firstSeen.Add(item, label); + + using var header = ImRaii.TreeNode(label); + if (!header) + continue; + + drawItem(item); + } + } + + private static void DrawResourcePaths((string, Dictionary>?)[] result) + { + DrawWithHeaders(result, paths => + { + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (actualPath, gamePaths) in paths) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + ImGui.TableNextColumn(); + foreach (var gamePath in gamePaths) + ImGui.TextUnformatted(gamePath); + } + }); + } + + private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, resources => + { + using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f); + if (_withUiData) + ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var (resourceHandle, (actualPath, name, icon)) in resources) + { + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{resourceHandle:X}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + if (_withUiData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(icon.ToString()); + ImGui.SameLine(); + ImGui.TextUnformatted(name); + } + } + }); + } + + private void DrawResourceTrees((string, ResourceTreeDto?)[] result) + { + DrawWithHeaders(result, tree => + { + ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); + + using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); + if (!table) + return; + + if (_withUiData) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); + ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); + } + else + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); + } + + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + void DrawNode(ResourceNodeDto node) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + var hasChildren = node.Children.Any(); + using var treeNode = ImRaii.TreeNode( + $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", + hasChildren + ? ImGuiTreeNodeFlags.SpanFullWidth + : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); + if (_withUiData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(node.Type.ToString()); + ImGui.TableNextColumn(); + TextUnformattedMono(node.Icon.ToString()); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.GamePath ?? "Unknown"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.ActualPath); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ObjectAddress:X8}"); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ResourceHandle:X8}"); + + if (treeNode) + foreach (var child in node.Children) + DrawNode(child); + } + + foreach (var node in tree.Nodes) + DrawNode(node); + }); + } + + private static void TextUnformattedMono(string text) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(text); + } + + private ushort[] GetSelectedGameObjects() + => _gameObjectIndices.Split(',') + .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) + .ToArray(); + + private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) + { + var gameObject = objects[gameObjectIndex]; + + return gameObject.Valid + ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})" + : $"[{gameObjectIndex}] null"; + } +} diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs new file mode 100644 index 00000000..a8405eb2 --- /dev/null +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -0,0 +1,203 @@ +using Dalamud.Interface; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Collections.Manager; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Api.IpcTester; + +public class TemporaryIpcTester( + DalamudPluginInterface pi, + ModManager modManager, + CollectionManager collections, + TempModManager tempMods, + TempCollectionManager tempCollections, + SaveService saveService, + Configuration config) + : IUiService +{ + public Guid LastCreatedCollectionId = Guid.Empty; + + private Guid? _tempGuid; + private string _tempCollectionName = string.Empty; + private string _tempCollectionGuidName = string.Empty; + private string _tempModName = string.Empty; + private string _tempGamePath = "test/game/path.mtrl"; + private string _tempFilePath = "test/success.mtrl"; + private string _tempManipulation = string.Empty; + private PenumbraApiEc _lastTempError; + private int _tempActorIndex; + private bool _forceOverwrite; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Temporary"); + if (!_) + return; + + ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); + ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); + ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); + ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Error", _lastTempError.ToString()); + ImGuiUtil.DrawTableColumn("Last Created Collection"); + ImGui.TableNextColumn(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString()); + } + + IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection"); + if (ImGui.Button("Create##Collection")) + { + LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName); + if (_tempGuid == null) + { + _tempGuid = LastCreatedCollectionId; + _tempCollectionGuidName = LastCreatedCollectionId.ToString(); + } + } + + var guid = _tempGuid.GetValueOrDefault(Guid.Empty); + + IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection"); + if (ImGui.Button("Delete##Collection")) + _lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid); + ImGui.SameLine(); + if (ImGui.Button("Delete Last##Collection")) + _lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId); + + IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection"); + if (ImGui.Button("Assign##NamedCollection")) + _lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite); + + IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection"); + if (ImGui.Button("Add##Mod")) + _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); + + IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection"); + if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, + "Copies the effective list from the collection named in Temporary Mod Name...", + !collections.Storage.ByName(_tempModName, out var copyCollection)) + && copyCollection is { HasCache: true }) + { + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), + MetaManipulation.CurrentVersion); + _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); + } + + IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections"); + if (ImGui.Button("Add##All")) + _lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); + + IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection"); + if (ImGui.Button("Remove##Mod")) + _lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue); + + IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); + if (ImGui.Button("Remove##ModAll")) + _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue); + } + + public void DrawCollections() + { + using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); + if (!collTree) + return; + + using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var (collection, idx) in tempCollections.Values.WithIndex()) + { + using var id = ImRaii.PushId(idx); + ImGui.TableNextColumn(); + var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) + .FirstOrDefault() + ?? "Unknown"; + if (ImGui.Button("Save##Collection")) + TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character); + + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(collection.Identifier); + } + + ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); + ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); + ImGuiUtil.DrawTableColumn(string.Join(", ", + tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); + } + } + + public void DrawMods() + { + using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods"); + if (!modTree) + return; + + using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit); + + void PrintList(string collectionName, IReadOnlyList list) + { + foreach (var mod in list) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Priority.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collectionName); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Default.Files.Count.ToString()); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + foreach (var (path, file) in mod.Default.Files) + ImGui.TextUnformatted($"{path} -> {file}"); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.TotalManipulations.ToString()); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + foreach (var manip in mod.Default.Manipulations) + ImGui.TextUnformatted(manip.ToString()); + } + } + } + + if (table) + { + PrintList("All", tempMods.ModsForAllCollections); + foreach (var (collection, list) in tempMods.Mods) + PrintList(collection.Name, list); + } + } +} diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs new file mode 100644 index 00000000..29ddc22e --- /dev/null +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -0,0 +1,128 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Communication; + +namespace Penumbra.Api.IpcTester; + +public class UiIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber PreSettingsTabBar; + public readonly EventSubscriber PreSettingsPanel; + public readonly EventSubscriber PostEnabled; + public readonly EventSubscriber PostSettingsPanelDraw; + public readonly EventSubscriber ChangedItemTooltip; + public readonly EventSubscriber ChangedItemClicked; + + private string _lastDrawnMod = string.Empty; + private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; + private bool _subscribedToTooltip; + private bool _subscribedToClick; + private string _lastClicked = string.Empty; + private string _lastHovered = string.Empty; + private TabType _selectTab = TabType.None; + private string _modName = string.Empty; + private PenumbraApiEc _ec = PenumbraApiEc.Success; + + public UiIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); + PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); + ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); + } + + public void Dispose() + { + PreSettingsTabBar.Dispose(); + PreSettingsPanel.Dispose(); + PostEnabled.Dispose(); + PostSettingsPanelDraw.Dispose(); + ChangedItemTooltip.Dispose(); + ChangedItemClicked.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("UI"); + if (!_) + return; + + using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString())) + { + if (combo) + foreach (var val in Enum.GetValues()) + { + if (ImGui.Selectable(val.ToString(), _selectTab == val)) + _selectTab = val; + } + } + + ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod"); + ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); + + IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); + if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip)) + { + if (_subscribedToTooltip) + ChangedItemTooltip.Enable(); + else + ChangedItemTooltip.Disable(); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastHovered); + + IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click"); + if (ImGui.Checkbox("##click", ref _subscribedToClick)) + { + if (_subscribedToClick) + ChangedItemClicked.Enable(); + else + ChangedItemClicked.Disable(); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastClicked); + IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window"); + if (ImGui.Button("Open##window")) + _ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_ec.ToString()); + + IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window"); + if (ImGui.Button("Close##window")) + new CloseMainWindow(_pi).Invoke(); + } + + private void UpdateLastDrawnMod(string name) + => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); + + private void UpdateLastDrawnMod(string name, float _1, float _2) + => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); + + private void AddedTooltip(ChangedItemType type, uint id) + { + _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; + ImGui.TextUnformatted("IPC Test Successful"); + } + + private void AddedClick(MouseButton button, ChangedItemType type, uint id) + { + _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; + } +} diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs deleted file mode 100644 index dc1e8472..00000000 --- a/Penumbra/Api/PenumbraApi.cs +++ /dev/null @@ -1,1374 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin.Services; -using Lumina.Data; -using Newtonsoft.Json; -using OtterGui; -using Penumbra.Collections; -using Penumbra.Interop.PathResolving; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Compression; -using OtterGui.Log; -using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; -using Penumbra.Interop.ResourceLoading; -using Penumbra.Mods.Manager; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Services; -using Penumbra.Collections.Manager; -using Penumbra.Communication; -using Penumbra.GameData.Interop; -using Penumbra.Import.Textures; -using Penumbra.Interop.Services; -using Penumbra.UI; -using TextureType = Penumbra.Api.Enums.TextureType; -using Penumbra.Interop.ResourceTree; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; - -namespace Penumbra.Api; - -public class PenumbraApi : IDisposable, IPenumbraApi -{ - public (int, int) ApiVersion - => (4, 24); - - public event Action? PreSettingsTabBarDraw - { - add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); - remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); - } - - public event Action? PreSettingsPanelDraw - { - add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); - remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); - } - - public event Action? PostEnabledDraw - { - add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default); - remove => _communicator.PostEnabledDraw.Unsubscribe(value!); - } - - public event Action? PostSettingsPanelDraw - { - add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default); - remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); - } - - public event GameObjectRedrawnDelegate? GameObjectRedrawn - { - add - { - CheckInitialized(); - _redrawService.GameObjectRedrawn += value; - } - remove - { - CheckInitialized(); - _redrawService.GameObjectRedrawn -= value; - } - } - - public event ModSettingChangedDelegate? ModSettingChanged; - - public event CreatingCharacterBaseDelegate? CreatingCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatingCharacterBase.Subscribe(new Action(value), - Communication.CreatingCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); - } - } - - public event CreatedCharacterBaseDelegate? CreatedCharacterBase; - - public bool Valid - => _lumina != null; - - private CommunicatorService _communicator; - private Lumina.GameData? _lumina; - - private IDataManager _gameData; - private IFramework _framework; - private ObjectManager _objects; - private ModManager _modManager; - private ResourceLoader _resourceLoader; - private Configuration _config; - private CollectionManager _collectionManager; - private TempCollectionManager _tempCollections; - private TempModManager _tempMods; - private ActorManager _actors; - private CollectionResolver _collectionResolver; - private CutsceneService _cutsceneService; - private ModImportManager _modImportManager; - private CollectionEditor _collectionEditor; - private RedrawService _redrawService; - private ModFileSystem _modFileSystem; - private ConfigWindow _configWindow; - private TextureManager _textureManager; - private ResourceTreeFactory _resourceTreeFactory; - - public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, ObjectManager objects, - ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, - CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, - ModFileSystem modFileSystem, ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) - { - _communicator = communicator; - _gameData = gameData; - _framework = framework; - _objects = objects; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; - _modImportManager = modImportManager; - _collectionEditor = collectionEditor; - _redrawService = redrawService; - _modFileSystem = modFileSystem; - _configWindow = configWindow; - _textureManager = textureManager; - _resourceTreeFactory = resourceTreeFactory; - _lumina = gameData.GameData; - - _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); - _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); - _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); - _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); - _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); - } - - public unsafe void Dispose() - { - if (!Valid) - return; - - _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); - _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); - _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); - _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); - _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); - _lumina = null; - _communicator = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; - _modImportManager = null!; - _collectionEditor = null!; - _redrawService = null!; - _modFileSystem = null!; - _configWindow = null!; - _textureManager = null!; - _resourceTreeFactory = null!; - _framework = null!; - } - - public event ChangedItemClick? ChangedItemClicked - { - add => _communicator.ChangedItemClick.Subscribe(new Action(value!), - Communication.ChangedItemClick.Priority.Default); - remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!)); - } - - public string GetModDirectory() - { - CheckInitialized(); - return _config.ModDirectory; - } - - private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData) - { - if (resolveData.AssociatedGameObject != nint.Zero) - GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString()); - } - - public event Action? ModDirectoryChanged - { - add - { - CheckInitialized(); - _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - } - remove - { - CheckInitialized(); - _communicator.ModDirectoryChanged.Unsubscribe(value!); - } - } - - public bool GetEnabledState() - => _config.EnableMods; - - public event Action? EnabledChange - { - add - { - CheckInitialized(); - _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - } - remove - { - CheckInitialized(); - _communicator.EnabledChanged.Unsubscribe(value!); - } - } - - public string GetConfiguration() - { - CheckInitialized(); - return JsonConvert.SerializeObject(_config, Formatting.Indented); - } - - public event ChangedItemHover? ChangedItemTooltip - { - add => _communicator.ChangedItemHover.Subscribe(new Action(value!), Communication.ChangedItemHover.Priority.Default); - remove => _communicator.ChangedItemHover.Unsubscribe(new Action(value!)); - } - - public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - - public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) - { - CheckInitialized(); - if (_configWindow == null) - return PenumbraApiEc.SystemDisposed; - - _configWindow.IsOpen = true; - - if (!Enum.IsDefined(tab)) - return PenumbraApiEc.InvalidArgument; - - if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) - { - if (_modManager.TryGetMod(modDirectory, modName, out var mod)) - _communicator.SelectTab.Invoke(tab, mod); - else - return PenumbraApiEc.ModMissing; - } - else if (tab != TabType.None) - { - _communicator.SelectTab.Invoke(tab, null); - } - - return PenumbraApiEc.Success; - } - - public void CloseMainWindow() - { - CheckInitialized(); - if (_configWindow == null) - return; - - _configWindow.IsOpen = false; - } - - public void RedrawObject(int tableIndex, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(tableIndex, setting); - } - - public void RedrawObject(string name, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(name, setting); - } - - public void RedrawObject(GameObject? gameObject, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(gameObject, setting); - } - - public void RedrawAll(RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawAll(setting); - } - - public string ResolveDefaultPath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Active.Default); - } - - public string ResolveInterfacePath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Active.Interface); - } - - public string ResolvePlayerPath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionResolver.PlayerCollection()); - } - - // TODO: cleanup when incrementing API level - public string ResolvePath(string path, string characterName) - => ResolvePath(path, characterName, ushort.MaxValue); - - public string ResolveGameObjectPath(string path, int gameObjectIdx) - { - CheckInitialized(); - AssociatedCollection(gameObjectIdx, out var collection); - return ResolvePath(path, _modManager, collection); - } - - public string ResolvePath(string path, string characterName, ushort worldId) - { - CheckInitialized(); - return ResolvePath(path, _modManager, - _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId))); - } - - // TODO: cleanup when incrementing API level - public string[] ReverseResolvePath(string path, string characterName) - => ReverseResolvePath(path, characterName, ushort.MaxValue); - - public string[] ReverseResolvePath(string path, string characterName, ushort worldId) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - AssociatedCollection(gameObjectIdx, out var collection); - var ret = collection.ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public string[] ReverseResolvePlayerPath(string path) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - var ret = _collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) - { - CheckInitialized(); - if (!_config.EnableMods) - return (forward, reverse.Select(p => new[] - { - p, - }).ToArray()); - - var playerCollection = _collectionResolver.PlayerCollection(); - var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); - var reverseResolved = playerCollection.ReverseResolvePaths(reverse); - return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); - } - - public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) - { - CheckInitialized(); - if (!_config.EnableMods) - return (forward, reverse.Select(p => new[] - { - p, - }).ToArray()); - - return await Task.Run(async () => - { - var playerCollection = await _framework.RunOnFrameworkThread(_collectionResolver.PlayerCollection).ConfigureAwait(false); - var forwardTask = Task.Run(() => - { - var forwardRet = new string[forward.Length]; - Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], _modManager, playerCollection)); - return forwardRet; - }).ConfigureAwait(false); - var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false); - var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); - return (await forwardTask, reverseResolved); - }); - } - - public T? GetFile(string gamePath) where T : FileResource - => GetFileIntern(ResolveDefaultPath(gamePath)); - - public T? GetFile(string gamePath, string characterName) where T : FileResource - => GetFileIntern(ResolvePath(gamePath, characterName)); - - public IReadOnlyDictionary GetChangedItemsForCollection(string collectionName) - { - CheckInitialized(); - try - { - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - collection = ModCollection.Empty; - - if (collection.HasCache) - return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); - - Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded."); - return new Dictionary(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not obtain Changed Items for {collectionName}:\n{e}"); - throw; - } - } - - public string GetCollectionForType(ApiCollectionType type) - { - CheckInitialized(); - if (!Enum.IsDefined(type)) - return string.Empty; - - var collection = _collectionManager.Active.ByType((CollectionType)type); - return collection?.Name ?? string.Empty; - } - - public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew, - bool allowDelete) - { - CheckInitialized(); - if (!Enum.IsDefined(type)) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - var oldCollection = _collectionManager.Active.ByType((CollectionType)type)?.Name ?? string.Empty; - - if (collectionName.Length == 0) - { - if (oldCollection.Length == 0) - return (PenumbraApiEc.NothingChanged, oldCollection); - - if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) - return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - - _collectionManager.Active.RemoveSpecialCollection((CollectionType)type); - return (PenumbraApiEc.Success, oldCollection); - } - - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, oldCollection); - - if (oldCollection.Length == 0) - { - if (!allowCreateNew) - return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - - _collectionManager.Active.CreateSpecialCollection((CollectionType)type); - } - else if (oldCollection == collection.Name) - { - return (PenumbraApiEc.NothingChanged, oldCollection); - } - - _collectionManager.Active.SetCollection(collection, (CollectionType)type); - return (PenumbraApiEc.Success, oldCollection); - } - - public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject(int gameObjectIdx) - { - CheckInitialized(); - var id = AssociatedIdentifier(gameObjectIdx); - if (!id.IsValid) - return (false, false, _collectionManager.Active.Default.Name); - - if (_collectionManager.Active.Individuals.TryGetValue(id, out var collection)) - return (true, true, collection.Name); - - AssociatedCollection(gameObjectIdx, out collection); - return (true, false, collection.Name); - } - - public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew, - bool allowDelete) - { - CheckInitialized(); - var id = AssociatedIdentifier(gameObjectIdx); - if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Active.Default.Name); - - var oldCollection = _collectionManager.Active.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; - - if (collectionName.Length == 0) - { - if (oldCollection.Length == 0) - return (PenumbraApiEc.NothingChanged, oldCollection); - - if (!allowDelete) - return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - - var idx = _collectionManager.Active.Individuals.Index(id); - _collectionManager.Active.RemoveIndividualCollection(idx); - return (PenumbraApiEc.Success, oldCollection); - } - - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, oldCollection); - - if (oldCollection.Length == 0) - { - if (!allowCreateNew) - return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - - var ids = _collectionManager.Active.Individuals.GetGroup(id); - _collectionManager.Active.CreateIndividualCollection(ids); - } - else if (oldCollection == collection.Name) - { - return (PenumbraApiEc.NothingChanged, oldCollection); - } - - _collectionManager.Active.SetCollection(collection, CollectionType.Individual, _collectionManager.Active.Individuals.Index(id)); - return (PenumbraApiEc.Success, oldCollection); - } - - public IList GetCollections() - { - CheckInitialized(); - return _collectionManager.Storage.Select(c => c.Name).ToArray(); - } - - public string GetCurrentCollection() - { - CheckInitialized(); - return _collectionManager.Active.Current.Name; - } - - public string GetDefaultCollection() - { - CheckInitialized(); - return _collectionManager.Active.Default.Name; - } - - public string GetInterfaceCollection() - { - CheckInitialized(); - return _collectionManager.Active.Interface.Name; - } - - // TODO: cleanup when incrementing API level - public (string, bool) GetCharacterCollection(string characterName) - => GetCharacterCollection(characterName, ushort.MaxValue); - - public (string, bool) GetCharacterCollection(string characterName, ushort worldId) - { - CheckInitialized(); - return _collectionManager.Active.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) - ? (collection.Name, true) - : (_collectionManager.Active.Default.Name, false); - } - - public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) - { - CheckInitialized(); - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return (data.AssociatedGameObject, data.ModCollection.Name); - } - - public int GetCutsceneParentIndex(int actorIdx) - { - CheckInitialized(); - return _cutsceneService.GetParentIndex(actorIdx); - } - - public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) - { - CheckInitialized(); - if (_cutsceneService.SetParentIndex(copyIdx, newParentIdx)) - return PenumbraApiEc.Success; - - return PenumbraApiEc.InvalidArgument; - } - - public IList<(string, string)> GetModList() - { - CheckInitialized(); - return _modManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); - } - - public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) - { - CheckInitialized(); - return _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) - : null; - } - - public (PenumbraApiEc, (bool, int, IDictionary>, bool)?) GetCurrentModSettings(string collectionName, - string modDirectory, string modName, bool allowInheritance) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, null); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return (PenumbraApiEc.ModMissing, null); - - var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; - if (settings == null) - return (PenumbraApiEc.Success, null); - - var shareSettings = settings.ConvertToShareable(mod); - return (PenumbraApiEc.Success, - (shareSettings.Enabled, shareSettings.Priority.Value, shareSettings.Settings, collection.Settings[mod.Index] != null)); - } - - public PenumbraApiEc ReloadMod(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, Args("ModDirectory", modDirectory, "ModName", modName)); - - _modManager.ReloadMod(mod); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); - } - - public PenumbraApiEc InstallMod(string modFilePackagePath) - { - if (File.Exists(modFilePackagePath)) - { - _modImportManager.AddUnpack(modFilePackagePath); - return Return(PenumbraApiEc.Success, Args("ModFilePackagePath", modFilePackagePath)); - } - else - { - return Return(PenumbraApiEc.FileMissing, Args("ModFilePackagePath", modFilePackagePath)); - } - } - - public PenumbraApiEc AddMod(string modDirectory) - { - CheckInitialized(); - var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); - if (!dir.Exists) - return Return(PenumbraApiEc.FileMissing, Args("ModDirectory", modDirectory)); - - - _modManager.AddMod(dir); - if (_config.UseFileSystemCompression) - new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), - CompressionAlgorithm.Xpress8K); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory)); - } - - public PenumbraApiEc DeleteMod(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.NothingChanged, Args("ModDirectory", modDirectory, "ModName", modName)); - - _modManager.DeleteMod(mod); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); - } - - public event Action? ModDeleted; - public event Action? ModAdded; - public event Action? ModMoved; - - private void ModPathChangeSubscriber(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { - case ModPathChangeType.Reloaded: - TriggerSettingEdited(mod); - break; - case ModPathChangeType.Deleted when oldDirectory != null: - ModDeleted?.Invoke(oldDirectory.Name); - break; - case ModPathChangeType.Added when newDirectory != null: - ModAdded?.Invoke(newDirectory.Name); - break; - case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: - ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); - break; - } - } - - public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) - return (PenumbraApiEc.ModMissing, string.Empty, false); - - var fullPath = leaf.FullName(); - - return (PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath(mod, fullPath)); - } - - public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) - { - CheckInitialized(); - if (newPath.Length == 0) - return PenumbraApiEc.InvalidArgument; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) - return PenumbraApiEc.ModMissing; - - try - { - _modFileSystem.RenameAndMove(leaf, newPath); - return PenumbraApiEc.Success; - } - catch - { - return PenumbraApiEc.PathRenameFailed; - } - } - - public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - - return _collectionEditor.SetModInheritance(collection, mod, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - return _collectionEditor.SetModState(collection, mod, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - return _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, - string optionName) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return Return(PenumbraApiEc.CollectionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return Return(PenumbraApiEc.OptionGroupMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var setting = mod.Groups[groupIdx].Type switch - { - GroupType.Multi => Setting.Multi(optionIdx), - GroupType.Single => Setting.Single(optionIdx), - _ => Setting.Zero, - }; - - return Return( - _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - } - - public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList optionNames) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return Return(PenumbraApiEc.CollectionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return Return(PenumbraApiEc.OptionGroupMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - var group = mod.Groups[groupIdx]; - - var setting = Setting.Zero; - if (group.Type == GroupType.Single) - { - var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - setting = Setting.Single(optionIdx); - } - else - { - foreach (var name in optionNames) - { - var optionIdx = group.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", - optionGroupName, "#optionNames", optionNames.Count.ToString())); - - setting |= Setting.Multi(optionIdx); - } - } - - return Return( - _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - } - - - public PenumbraApiEc CopyModSettings(string? collectionName, string modDirectoryFrom, string modDirectoryTo) - { - CheckInitialized(); - - var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase)); - var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase)); - if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in _collectionManager.Storage) - _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); - else if (_collectionManager.Storage.ByName(collectionName, out var collection)) - _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); - else - return PenumbraApiEc.CollectionMissing; - - return PenumbraApiEc.Success; - } - - public (PenumbraApiEc, string) CreateTemporaryCollection(string tag, string character, bool forceOverwriteCharacter) - { - CheckInitialized(); - - if (!ActorIdentifierFactory.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - var identifier = NameToIdentifier(character, ushort.MaxValue); - if (!identifier.IsValid) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - if (!forceOverwriteCharacter && _collectionManager.Active.Individuals.ContainsKey(identifier) - || _tempCollections.Collections.ContainsKey(identifier)) - return (PenumbraApiEc.CharacterCollectionExists, string.Empty); - - var name = $"{tag}_{character}"; - var ret = CreateNamedTemporaryCollection(name); - if (ret != PenumbraApiEc.Success) - return (ret, name); - - if (_tempCollections.AddIdentifier(name, identifier)) - return (PenumbraApiEc.Success, name); - - _tempCollections.RemoveTemporaryCollection(name); - return (PenumbraApiEc.UnknownError, string.Empty); - } - - public PenumbraApiEc CreateNamedTemporaryCollection(string name) - { - CheckInitialized(); - if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name, _config.ReplaceNonAsciiOnImport) != name || name.Contains('|')) - return PenumbraApiEc.InvalidArgument; - - return _tempCollections.CreateTemporaryCollection(name).Length > 0 - ? PenumbraApiEc.Success - : PenumbraApiEc.CollectionExists; - } - - public PenumbraApiEc AssignTemporaryCollection(string collectionName, int actorIndex, bool forceAssignment) - { - CheckInitialized(); - - if (actorIndex < 0 || actorIndex >= _objects.TotalCount) - return PenumbraApiEc.InvalidArgument; - - var identifier = _actors.FromObject(_objects[actorIndex], out _, false, false, true); - if (!identifier.IsValid) - return PenumbraApiEc.InvalidArgument; - - if (!_tempCollections.CollectionByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (forceAssignment) - { - if (_tempCollections.Collections.ContainsKey(identifier) && !_tempCollections.Collections.Delete(identifier)) - return PenumbraApiEc.AssignmentDeletionFailed; - } - else if (_tempCollections.Collections.ContainsKey(identifier) - || _collectionManager.Active.Individuals.ContainsKey(identifier)) - { - return PenumbraApiEc.CharacterCollectionExists; - } - - var group = _tempCollections.Collections.GetGroup(identifier); - return _tempCollections.AddIdentifier(collection, group) - ? PenumbraApiEc.Success - : PenumbraApiEc.UnknownError; - } - - public PenumbraApiEc RemoveTemporaryCollection(string character) - { - CheckInitialized(); - return _tempCollections.RemoveByCharacterName(character) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc RemoveTemporaryCollectionByName(string name) - { - CheckInitialized(); - return _tempCollections.RemoveTemporaryCollection(name) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) - { - CheckInitialized(); - if (!ConvertPaths(paths, out var p)) - return PenumbraApiEc.InvalidGamePath; - - if (!ConvertManips(manipString, out var m)) - return PenumbraApiEc.InvalidManipulation; - - return _tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc AddTemporaryMod(string tag, string collectionName, Dictionary paths, string manipString, - int priority) - { - CheckInitialized(); - if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.Storage.ByName(collectionName, out collection)) - return PenumbraApiEc.CollectionMissing; - - if (!ConvertPaths(paths, out var p)) - return PenumbraApiEc.InvalidGamePath; - - if (!ConvertManips(manipString, out var m)) - return PenumbraApiEc.InvalidManipulation; - - return _tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) - { - CheckInitialized(); - return _tempMods.Unregister(tag, null, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) - { - CheckInitialized(); - if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.Storage.ByName(collectionName, out collection)) - return PenumbraApiEc.CollectionMissing; - - return _tempMods.Unregister(tag, collection, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, - _ => PenumbraApiEc.UnknownError, - }; - } - - public string GetPlayerMetaManipulations() - { - CheckInitialized(); - var collection = _collectionResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) - => textureType switch - { - TextureType.Png => _textureManager.SavePng(inputFile, outputFile), - TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), - TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), - TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), - TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), - TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), - TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), - TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), - TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), - _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), - }; - - // @formatter:off - public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) - => textureType switch - { - TextureType.Png => _textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), - }; - // @formatter:on - - public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); - - return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary> GetPlayerResourcePaths() - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); - - return pathDictionaries.AsReadOnly(); - } - - public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, - params ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); - var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); - - return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, - bool withUiData) - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); - var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); - - return resDictionaries.AsReadOnly(); - } - - public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); - var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); - - return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary GetPlayerResourceTrees(bool withUiData) - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); - var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); - - return resDictionary.AsReadOnly(); - } - - // TODO: cleanup when incrementing API - public string GetMetaManipulations(string characterName) - => GetMetaManipulations(characterName, ushort.MaxValue); - - public string GetMetaManipulations(string characterName, ushort worldId) - { - CheckInitialized(); - var identifier = NameToIdentifier(characterName, worldId); - var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) - ? c - : _collectionManager.Active.Individual(identifier); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - public string GetGameObjectMetaManipulations(int gameObjectIdx) - { - CheckInitialized(); - AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void CheckInitialized() - { - if (!Valid) - throw new Exception("PluginShare is not initialized."); - } - - // Return the collection associated to a current game object. If it does not exist, return the default collection. - // If the index is invalid, returns false and the default collection. - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) - { - collection = _collectionManager.Active.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) - return false; - - var ptr = _objects[gameObjectIdx]; - var data = _collectionResolver.IdentifyCollection(ptr.AsObject, false); - if (data.Valid) - collection = data.ModCollection; - - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ActorIdentifier AssociatedIdentifier(int gameObjectIdx) - { - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) - return ActorIdentifier.Invalid; - - var ptr = _objects[gameObjectIdx]; - return _actors.FromObject(ptr, out _, false, true, true); - } - - // Resolve a path given by string for a specific collection. - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private string ResolvePath(string path, ModManager _, ModCollection collection) - { - if (!_config.EnableMods) - return path; - - var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath(gamePath); - return ret?.ToString() ?? path; - } - - // Get a file for a resolved path. - private T? GetFileIntern(string resolvedPath) where T : FileResource - { - CheckInitialized(); - try - { - return Path.IsPathRooted(resolvedPath) - ? _lumina?.GetFileFromDisk(resolvedPath) - : _gameData.GetFile(resolvedPath); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not load file {resolvedPath}:\n{e}"); - return null; - } - } - - - // Convert a dictionary of strings to a dictionary of gamepaths to full paths. - // Only returns true if all paths can successfully be converted and added. - private static bool ConvertPaths(IReadOnlyDictionary redirections, - [NotNullWhen(true)] out Dictionary? paths) - { - paths = new Dictionary(redirections.Count); - foreach (var (gString, fString) in redirections) - { - if (!Utf8GamePath.FromString(gString, out var path, false)) - { - paths = null; - return false; - } - - var fullPath = new FullPath(fString); - if (!paths.TryAdd(path, fullPath)) - { - paths = null; - return false; - } - } - - return true; - } - - // Convert manipulations from a transmitted base64 string to actual manipulations. - // The empty string is treated as an empty set. - // Only returns true if all conversions are successful and distinct. - private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? manips) - { - if (manipString.Length == 0) - { - manips = new HashSet(); - return true; - } - - if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) - { - manips = null; - return false; - } - - manips = new HashSet(manipArray!.Length); - foreach (var manip in manipArray.Where(m => m.Validate())) - { - if (manips.Add(manip)) - continue; - - Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); - manips = null; - return false; - } - - return true; - } - - // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private ActorIdentifier NameToIdentifier(string name, ushort worldId) - { - // Verified to be valid name beforehand. - var b = ByteString.FromStringUnsafe(name, false); - return _actors.CreatePlayer(b, worldId); - } - - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); - - private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) - => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); - - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) - { - switch (type) - { - case ModOptionChangeType.GroupDeleted: - case ModOptionChangeType.GroupMoved: - case ModOptionChangeType.GroupTypeChanged: - case ModOptionChangeType.PriorityChanged: - case ModOptionChangeType.OptionDeleted: - case ModOptionChangeType.OptionMoved: - case ModOptionChangeType.OptionFilesChanged: - case ModOptionChangeType.OptionFilesAdded: - case ModOptionChangeType.OptionSwapsChanged: - case ModOptionChangeType.OptionMetaChanged: - TriggerSettingEdited(mod); - break; - } - } - - private void OnModFileChanged(Mod mod, FileRegistry file) - { - if (file.CurrentUsage == 0) - return; - - TriggerSettingEdited(mod); - } - - private void TriggerSettingEdited(Mod mod) - { - var collection = _collectionResolver.PlayerCollection(); - var (settings, parent) = collection[mod.Index]; - if (settings is { Enabled: true }) - ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static LazyString Args(params string[] arguments) - { - if (arguments.Length == 0) - return new LazyString(() => "no arguments"); - - return new LazyString(() => - { - var sb = new StringBuilder(); - for (var i = 0; i < arguments.Length / 2; ++i) - { - sb.Append(arguments[2 * i]); - sb.Append(" = "); - sb.Append(arguments[2 * i + 1]); - sb.Append(", "); - } - - return sb.ToString(0, sb.Length - 2); - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") - { - Penumbra.Log.Debug( - $"[{name}] Called with {args}, returned {ec}."); - return ec; - } -} diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs deleted file mode 100644 index 78887156..00000000 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ /dev/null @@ -1,435 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Penumbra.GameData.Enums; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; -using Penumbra.Collections.Manager; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Api; - -using CurrentSettings = ValueTuple>, bool)?>; - -public class PenumbraIpcProviders : IDisposable -{ - internal readonly IPenumbraApi Api; - - // Plugin State - internal readonly EventProvider Initialized; - internal readonly EventProvider Disposed; - internal readonly FuncProvider ApiVersion; - internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions; - internal readonly FuncProvider GetEnabledState; - internal readonly EventProvider EnabledChange; - - // Configuration - internal readonly FuncProvider GetModDirectory; - internal readonly FuncProvider GetConfiguration; - internal readonly EventProvider ModDirectoryChanged; - - // UI - internal readonly EventProvider PreSettingsTabBarDraw; - internal readonly EventProvider PreSettingsDraw; - internal readonly EventProvider PostEnabledDraw; - internal readonly EventProvider PostSettingsDraw; - internal readonly EventProvider ChangedItemTooltip; - internal readonly EventProvider ChangedItemClick; - internal readonly FuncProvider OpenMainWindow; - internal readonly ActionProvider CloseMainWindow; - - // Redrawing - internal readonly ActionProvider RedrawAll; - internal readonly ActionProvider RedrawObject; - internal readonly ActionProvider RedrawObjectByIndex; - internal readonly ActionProvider RedrawObjectByName; - internal readonly EventProvider GameObjectRedrawn; - - // Game State - internal readonly FuncProvider GetDrawObjectInfo; - internal readonly FuncProvider GetCutsceneParentIndex; - internal readonly FuncProvider SetCutsceneParentIndex; - internal readonly EventProvider CreatingCharacterBase; - internal readonly EventProvider CreatedCharacterBase; - internal readonly EventProvider GameObjectResourcePathResolved; - - // Resolve - internal readonly FuncProvider ResolveDefaultPath; - internal readonly FuncProvider ResolveInterfacePath; - internal readonly FuncProvider ResolvePlayerPath; - internal readonly FuncProvider ResolveGameObjectPath; - internal readonly FuncProvider ResolveCharacterPath; - internal readonly FuncProvider ReverseResolvePath; - internal readonly FuncProvider ReverseResolveGameObjectPath; - internal readonly FuncProvider ReverseResolvePlayerPath; - internal readonly FuncProvider ResolvePlayerPaths; - internal readonly FuncProvider> ResolvePlayerPathsAsync; - - // Collections - internal readonly FuncProvider> GetCollections; - internal readonly FuncProvider GetCurrentCollectionName; - internal readonly FuncProvider GetDefaultCollectionName; - internal readonly FuncProvider GetInterfaceCollectionName; - internal readonly FuncProvider GetCharacterCollectionName; - internal readonly FuncProvider GetCollectionForType; - internal readonly FuncProvider SetCollectionForType; - internal readonly FuncProvider GetCollectionForObject; - internal readonly FuncProvider SetCollectionForObject; - internal readonly FuncProvider> GetChangedItems; - - // Meta - internal readonly FuncProvider GetPlayerMetaManipulations; - internal readonly FuncProvider GetMetaManipulations; - internal readonly FuncProvider GetGameObjectMetaManipulations; - - // Mods - internal readonly FuncProvider> GetMods; - internal readonly FuncProvider ReloadMod; - internal readonly FuncProvider InstallMod; - internal readonly FuncProvider AddMod; - internal readonly FuncProvider DeleteMod; - internal readonly FuncProvider GetModPath; - internal readonly FuncProvider SetModPath; - internal readonly EventProvider ModDeleted; - internal readonly EventProvider ModAdded; - internal readonly EventProvider ModMoved; - - // ModSettings - internal readonly FuncProvider, GroupType)>?> GetAvailableModSettings; - internal readonly FuncProvider GetCurrentModSettings; - internal readonly FuncProvider TryInheritMod; - internal readonly FuncProvider TrySetMod; - internal readonly FuncProvider TrySetModPriority; - internal readonly FuncProvider TrySetModSetting; - internal readonly FuncProvider, PenumbraApiEc> TrySetModSettings; - internal readonly EventProvider ModSettingChanged; - internal readonly FuncProvider CopyModSettings; - - // Editing - internal readonly FuncProvider ConvertTextureFile; - internal readonly FuncProvider ConvertTextureData; - - // Temporary - internal readonly FuncProvider CreateTemporaryCollection; - internal readonly FuncProvider RemoveTemporaryCollection; - internal readonly FuncProvider CreateNamedTemporaryCollection; - internal readonly FuncProvider RemoveTemporaryCollectionByName; - internal readonly FuncProvider AssignTemporaryCollection; - internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryModAll; - internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryMod; - internal readonly FuncProvider RemoveTemporaryModAll; - internal readonly FuncProvider RemoveTemporaryMod; - - // Resource Tree - internal readonly FuncProvider?[]> GetGameObjectResourcePaths; - internal readonly FuncProvider>> GetPlayerResourcePaths; - - internal readonly FuncProvider?[]> - GetGameObjectResourcesOfType; - - internal readonly - FuncProvider>> - GetPlayerResourcesOfType; - - internal readonly FuncProvider GetGameObjectResourceTrees; - internal readonly FuncProvider> GetPlayerResourceTrees; - - public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, - TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) - { - Api = api; - - // Plugin State - Initialized = Ipc.Initialized.Provider(pi); - Disposed = Ipc.Disposed.Provider(pi); - ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion); - ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion); - GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState); - EnabledChange = - Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent); - - // Configuration - GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory); - GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration); - ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a); - - // UI - PreSettingsTabBarDraw = - Ipc.PreSettingsTabBarDraw.Provider(pi, a => Api.PreSettingsTabBarDraw += a, a => Api.PreSettingsTabBarDraw -= a); - PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); - PostEnabledDraw = - Ipc.PostEnabledDraw.Provider(pi, a => Api.PostEnabledDraw += a, a => Api.PostEnabledDraw -= a); - PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a); - ChangedItemTooltip = - Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip); - ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick); - OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow); - CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow); - - // Redrawing - RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll); - RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject); - RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject); - RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject); - GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn, - () => Api.GameObjectRedrawn -= OnGameObjectRedrawn); - - // Game State - GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo); - GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex); - SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex); - CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi, - () => Api.CreatingCharacterBase += CreatingCharacterBaseEvent, - () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent); - CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi, - () => Api.CreatedCharacterBase += CreatedCharacterBaseEvent, - () => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi, - () => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent, - () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent); - - // Resolve - ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath); - ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath); - ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath); - ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath); - ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath); - ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath); - ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath); - ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath); - ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths); - ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync); - - // Collections - GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections); - GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection); - GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection); - GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection); - GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection); - GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType); - SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType); - GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject); - SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject); - GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection); - - // Meta - GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations); - GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations); - GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations); - - // Mods - GetMods = Ipc.GetMods.Provider(pi, Api.GetModList); - ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod); - InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod); - AddMod = Ipc.AddMod.Provider(pi, Api.AddMod); - DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod); - GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath); - SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath); - ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent); - ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent); - ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent); - - // ModSettings - GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings); - GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings); - TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod); - TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod); - TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority); - TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting); - TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings); - ModSettingChanged = Ipc.ModSettingChanged.Provider(pi, - () => Api.ModSettingChanged += ModSettingChangedEvent, - () => Api.ModSettingChanged -= ModSettingChangedEvent); - CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings); - - // Editing - ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile); - ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData); - - // Temporary - CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection); - RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection); - CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection); - RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName); - AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection); - AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll); - AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod); - RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); - RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - - // ResourceTree - GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths); - GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); - GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); - GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); - GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); - GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees); - - Initialized.Invoke(); - } - - public void Dispose() - { - // Plugin State - Initialized.Dispose(); - ApiVersion.Dispose(); - ApiVersions.Dispose(); - GetEnabledState.Dispose(); - EnabledChange.Dispose(); - - // Configuration - GetModDirectory.Dispose(); - GetConfiguration.Dispose(); - ModDirectoryChanged.Dispose(); - - // UI - PreSettingsTabBarDraw.Dispose(); - PreSettingsDraw.Dispose(); - PostEnabledDraw.Dispose(); - PostSettingsDraw.Dispose(); - ChangedItemTooltip.Dispose(); - ChangedItemClick.Dispose(); - OpenMainWindow.Dispose(); - CloseMainWindow.Dispose(); - - // Redrawing - RedrawAll.Dispose(); - RedrawObject.Dispose(); - RedrawObjectByIndex.Dispose(); - RedrawObjectByName.Dispose(); - GameObjectRedrawn.Dispose(); - - // Game State - GetDrawObjectInfo.Dispose(); - GetCutsceneParentIndex.Dispose(); - SetCutsceneParentIndex.Dispose(); - CreatingCharacterBase.Dispose(); - CreatedCharacterBase.Dispose(); - GameObjectResourcePathResolved.Dispose(); - - // Resolve - ResolveDefaultPath.Dispose(); - ResolveInterfacePath.Dispose(); - ResolvePlayerPath.Dispose(); - ResolveGameObjectPath.Dispose(); - ResolveCharacterPath.Dispose(); - ReverseResolvePath.Dispose(); - ReverseResolveGameObjectPath.Dispose(); - ReverseResolvePlayerPath.Dispose(); - ResolvePlayerPaths.Dispose(); - ResolvePlayerPathsAsync.Dispose(); - - // Collections - GetCollections.Dispose(); - GetCurrentCollectionName.Dispose(); - GetDefaultCollectionName.Dispose(); - GetInterfaceCollectionName.Dispose(); - GetCharacterCollectionName.Dispose(); - GetCollectionForType.Dispose(); - SetCollectionForType.Dispose(); - GetCollectionForObject.Dispose(); - SetCollectionForObject.Dispose(); - GetChangedItems.Dispose(); - - // Meta - GetPlayerMetaManipulations.Dispose(); - GetMetaManipulations.Dispose(); - GetGameObjectMetaManipulations.Dispose(); - - // Mods - GetMods.Dispose(); - ReloadMod.Dispose(); - InstallMod.Dispose(); - AddMod.Dispose(); - DeleteMod.Dispose(); - GetModPath.Dispose(); - SetModPath.Dispose(); - ModDeleted.Dispose(); - ModAdded.Dispose(); - ModMoved.Dispose(); - - // ModSettings - GetAvailableModSettings.Dispose(); - GetCurrentModSettings.Dispose(); - TryInheritMod.Dispose(); - TrySetMod.Dispose(); - TrySetModPriority.Dispose(); - TrySetModSetting.Dispose(); - TrySetModSettings.Dispose(); - ModSettingChanged.Dispose(); - CopyModSettings.Dispose(); - - // Temporary - CreateTemporaryCollection.Dispose(); - RemoveTemporaryCollection.Dispose(); - CreateNamedTemporaryCollection.Dispose(); - RemoveTemporaryCollectionByName.Dispose(); - AssignTemporaryCollection.Dispose(); - AddTemporaryModAll.Dispose(); - AddTemporaryMod.Dispose(); - RemoveTemporaryModAll.Dispose(); - RemoveTemporaryMod.Dispose(); - - // Editing - ConvertTextureFile.Dispose(); - ConvertTextureData.Dispose(); - - // Resource Tree - GetGameObjectResourcePaths.Dispose(); - GetPlayerResourcePaths.Dispose(); - GetGameObjectResourcesOfType.Dispose(); - GetPlayerResourcesOfType.Dispose(); - GetGameObjectResourceTrees.Dispose(); - GetPlayerResourceTrees.Dispose(); - - Disposed.Invoke(); - Disposed.Dispose(); - } - - // Wrappers - private int DeprecatedVersion() - { - Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead."); - return Api.ApiVersion.Breaking; - } - - private void OnClick(MouseButton click, object? item) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); - ChangedItemClick.Invoke(click, type, id); - } - - private void OnTooltip(object? item) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); - ChangedItemTooltip.Invoke(type, id); - } - - private void EnabledChangeEvent(bool value) - => EnabledChange.Invoke(value); - - private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex) - => GameObjectRedrawn.Invoke(objectAddress, objectTableIndex); - - private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData) - => CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData); - - private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject) - => CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject); - - private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath) - => GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath); - - private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited) - => ModSettingChanged.Invoke(type, collection, mod, inherited); - - private void ModDeletedEvent(string name) - => ModDeleted.Invoke(name); - - private void ModAddedEvent(string name) - => ModAdded.Invoke(name); - - private void ModMovedEvent(string from, string to) - => ModMoved.Invoke(from, to); -} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 72f0fb59..e1b32204 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -243,14 +243,14 @@ public sealed class CollectionCache : IDisposable continue; var config = settings.Settings[groupIndex]; - switch (group.Type) + switch (group) { - case GroupType.Single: - AddSubMod(group[config.AsIndex], mod); + case SingleModGroup single: + AddSubMod(single[config.AsIndex], mod); break; - case GroupType.Multi: + case MultiModGroup multi: { - foreach (var (option, _) in group.WithIndex() + foreach (var (option, _) in multi.WithIndex() .Where(p => config.HasFlag(p.Index)) .OrderByDescending(p => group.OptionPriority(p.Index))) AddSubMod(option, mod); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index f6c6e14a..4b5c4337 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -119,7 +119,7 @@ public class CollectionCacheManager : IDisposable /// Does not create caches. /// public void CalculateEffectiveFileList(ModCollection collection) - => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, + => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier, () => CalculateEffectiveFileListInternal(collection)); private void CalculateEffectiveFileListInternal(ModCollection collection) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 3b865d4b..bc928360 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -116,7 +116,7 @@ public readonly struct ImcCache : IDisposable } private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path) - => new($"|{collection.Name}_{collection.ChangeCounter}|{path}"); + => new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}"); public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 38679612..4e8ebe36 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -22,7 +22,7 @@ public class ActiveCollectionData public class ActiveCollections : ISavable, IDisposable { - public const int Version = 1; + public const int Version = 2; private readonly CollectionStorage _storage; private readonly CommunicatorService _communicator; @@ -261,16 +261,17 @@ public class ActiveCollections : ISavable, IDisposable var jObj = new JObject { { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, + { nameof(Default), Default.Id }, + { nameof(Interface), Interface.Id }, + { nameof(Current), Current.Id }, }; foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null) .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); + jObj.Add(type.ToString(), collection.Id); jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; jObj.WriteTo(j); } @@ -319,22 +320,16 @@ public class ActiveCollections : ISavable, IDisposable } } - /// - /// Load default, current, special, and character collections from config. - /// If a collection does not exist anymore, reset it to an appropriate default. - /// - private void LoadCollections() + private bool LoadCollectionsV1(JObject jObject) { - Penumbra.Log.Debug("[Collections] Reading collection assignments..."); - var configChanged = !Load(_saveService.FileNames, out var jObject); - - // Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed. - var defaultName = jObject[nameof(Default)]?.ToObject() - ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); + var configChanged = false; + // Load the default collection. If the name does not exist take the empty collection. + var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Name; if (!_storage.ByName(defaultName, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; } @@ -348,7 +343,8 @@ public class ActiveCollections : ISavable, IDisposable if (!_storage.ByName(interfaceName, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; } @@ -362,7 +358,8 @@ public class ActiveCollections : ISavable, IDisposable if (!_storage.ByName(currentName, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", NotificationType.Warning); + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; } @@ -393,11 +390,124 @@ public class ActiveCollections : ISavable, IDisposable Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments."); configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); - configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage); + configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 1); - // Save any changes. - if (configChanged) - _saveService.ImmediateSave(this); + return configChanged; + } + + private bool LoadCollectionsV2(JObject jObject) + { + var configChanged = false; + // Load the default collection. If the guid does not exist take the empty collection. + var defaultId = jObject[nameof(Default)]?.ToObject() ?? Guid.Empty; + if (!_storage.ById(defaultId, out var defaultCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = defaultCollection; + } + + // Load the interface collection. If no string is set, use the name of whatever was set as Default. + var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Id; + if (!_storage.ById(interfaceId, out var interfaceCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = interfaceCollection; + } + + // Load the current collection. + var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Id; + if (!_storage.ById(currentId, out var currentCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.", + NotificationType.Warning); + Current = _storage.DefaultNamed; + configChanged = true; + } + else + { + Current = currentCollection; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeId = jObject[type.ToString()]?.ToObject(); + if (typeId == null) + continue; + + if (!_storage.ById(typeId.Value, out var typeCollection)) + { + Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeId.Value} is not available, removed.", + NotificationType.Warning); + configChanged = true; + } + else + { + SpecialCollections[(int)type] = typeCollection; + } + } + + Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments."); + + configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); + configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 2); + + return configChanged; + } + + private bool LoadCollectionsNew() + { + Current = _storage.DefaultNamed; + Default = _storage.DefaultNamed; + Interface = _storage.DefaultNamed; + return true; + } + + /// + /// Load default, current, special, and character collections from config. + /// If a collection does not exist anymore, reset it to an appropriate default. + /// + private void LoadCollections() + { + Penumbra.Log.Debug("[Collections] Reading collection assignments..."); + var configChanged = !Load(_saveService.FileNames, out var jObject); + var version = jObject["Version"]?.ToObject() ?? 0; + var changed = false; + switch (version) + { + case 1: + changed = LoadCollectionsV1(jObject); + break; + case 2: + changed = LoadCollectionsV2(jObject); + break; + case 0 when configChanged: + changed = LoadCollectionsNew(); + break; + case 0: + Penumbra.Messager.NotificationMessage("Active Collections File has unknown version and will be reset.", + NotificationType.Warning); + changed = LoadCollectionsNew(); + break; + } + + if (changed) + _saveService.ImmediateSaveSync(this); } /// @@ -410,7 +520,7 @@ public class ActiveCollections : ISavable, IDisposable var jObj = BackupService.GetJObjectForFile(fileNames, file); if (jObj == null) { - ret = new JObject(); + ret = []; return false; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index d0b61e57..2da2a569 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -48,6 +47,25 @@ public class CollectionStorage : IReadOnlyList, IDisposable return true; } + /// Find a collection by its id. If the GUID is empty, the empty collection is returned. + public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + { + if (id != Guid.Empty) + return _collections.FindFirst(c => c.Id == id, out collection); + + collection = ModCollection.Empty; + return true; + } + + /// Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. + public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection) + { + if (Guid.TryParse(identifier, out var guid)) + return ById(guid, out collection); + + return ByName(identifier, out collection); + } + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) { _communicator = communicator; @@ -70,31 +88,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } - /// - /// Returns true if the name is not empty, it is not the name of the empty collection - /// and no existing collection results in the same filename as name. Also returns the fixed name. - /// - public bool CanAddCollection(string name, out string fixedName) - { - if (!IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.ToLowerInvariant(); - if (name.Length == 0 - || name == ModCollection.Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; - } - /// /// Add a new collection of the given name. /// If duplicate is not-null, the new collection will be a duplicate of it. @@ -104,14 +97,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Messager.NotificationMessage( - $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, - false); - return false; - } - var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); _collections.Add(newCollection); @@ -166,16 +151,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } - /// - /// Check if a name is valid to use for a collection. - /// Does not check for uniqueness. - /// - private static bool IsValidName(string name) - => name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); - /// /// Read all collection files in the Collection Directory. - /// Ensure that the default named collection exists, and apply inheritances afterwards. + /// Ensure that the default named collection exists, and apply inheritances afterward. /// Duplicate collection files are not deleted, just not added here. /// private void ReadCollections(out ModCollection defaultNamedCollection) @@ -183,29 +161,46 @@ public class CollectionStorage : IReadOnlyList, IDisposable Penumbra.Log.Debug("[Collections] Reading saved collections..."); foreach (var file in _saveService.FileNames.CollectionFiles) { - if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)) + if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance)) continue; - if (!IsValidName(name)) + if (id == Guid.Empty) { - // TODO: handle better. - Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning); + continue; + } + + if (ById(id, out _)) + { + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.", NotificationType.Warning); continue; } - if (ByName(name, out _)) - { - Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", - NotificationType.Warning); - continue; - } - - var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); + var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", - NotificationType.Warning); + try + { + if (version >= 2) + { + File.Move(file.FullName, correctName, false); + Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + NotificationType.Warning); + } + else + { + _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); + File.Delete(file.FullName); + Penumbra.Log.Information($"Migrated collection {name} to Guid {id}."); + } + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, + $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", NotificationType.Error); + } + _collections.Add(collection); } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 21a8cf8a..8a717b35 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -18,7 +18,7 @@ public partial class IndividualCollections foreach (var (name, identifiers, collection) in Assignments) { var tmp = identifiers[0].ToJson(); - tmp.Add("Collection", collection.Name); + tmp.Add("Collection", collection.Id); tmp.Add("Display", name); ret.Add(tmp); } @@ -26,18 +26,28 @@ public partial class IndividualCollections return ret; } - public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage) + public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage, int version) { if (_actors.Awaiter.IsCompletedSuccessfully) { - var ret = ReadJObjectInternal(obj, storage); + var ret = version switch + { + 1 => ReadJObjectInternalV1(obj, storage), + 2 => ReadJObjectInternalV2(obj, storage), + _ => true, + }; return ret; } Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready..."); _actors.Awaiter.ContinueWith(_ => { - if (ReadJObjectInternal(obj, storage)) + if (version switch + { + 1 => ReadJObjectInternalV1(obj, storage), + 2 => ReadJObjectInternalV2(obj, storage), + _ => true, + }) saver.ImmediateSave(parent); IsLoaded = true; Loaded.Invoke(); @@ -45,7 +55,55 @@ public partial class IndividualCollections return false; } - private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage) + private bool ReadJObjectInternalV1(JArray? obj, CollectionStorage storage) + { + Penumbra.Log.Debug("[Collections] Reading individual assignments..."); + if (obj == null) + { + Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments..."); + return true; + } + + foreach (var data in obj) + { + try + { + var identifier = _actors.FromJson(data as JObject); + var group = GetGroup(identifier); + if (group.Length == 0 || group.Any(i => !i.IsValid)) + { + Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", + NotificationType.Error); + continue; + } + + var collectionName = data["Collection"]?.ToObject() ?? string.Empty; + if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + { + Penumbra.Messager.NotificationMessage( + $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + NotificationType.Warning); + continue; + } + + if (!Add(group, collection)) + { + Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + NotificationType.Warning); + } + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error); + } + } + + Penumbra.Log.Debug($"Finished reading {Count} individual assignments..."); + + return true; + } + + private bool ReadJObjectInternalV2(JArray? obj, CollectionStorage storage) { Penumbra.Log.Debug("[Collections] Reading individual assignments..."); if (obj == null) @@ -64,17 +122,17 @@ public partial class IndividualCollections if (group.Length == 0 || group.Any(i => !i.IsValid)) { changes = true; - Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", + Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed assignment.", NotificationType.Error); continue; } - var collectionName = data["Collection"]?.ToObject() ?? string.Empty; - if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + var collectionId = data["Collection"]?.ToObject(); + if (!collectionId.HasValue || !storage.ById(collectionId.Value, out var collection)) { changes = true; Penumbra.Messager.NotificationMessage( - $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + $"Could not load the collection {collectionId} as individual collection for {identifier}, removed assignment.", NotificationType.Warning); continue; } @@ -82,14 +140,14 @@ public partial class IndividualCollections if (!Add(group, collection)) { changes = true; - Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed assignment.", NotificationType.Warning); } } catch (Exception e) { changes = true; - Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed assignment.", NotificationType.Error); } } @@ -100,14 +158,6 @@ public partial class IndividualCollections internal void Migrate0To1(Dictionary old) { - static bool FindDataId(string name, NameDictionary data, out NpcId dataId) - { - var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), - new KeyValuePair(uint.MaxValue, string.Empty)); - dataId = kvp.Key; - return kvp.Value.Length > 0; - } - foreach (var (name, collection) in old) { var kind = ObjectKind.None; @@ -155,5 +205,15 @@ public partial class IndividualCollections NotificationType.Error); } } + + return; + + static bool FindDataId(string name, NameDictionary data, out NpcId dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 771f9463..6003b5f9 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -138,7 +138,7 @@ public class InheritanceManager : IDisposable var changes = false; foreach (var subCollectionName in collection.InheritanceByName) { - if (_storage.ByName(subCollectionName, out var subCollection)) + if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection)) { if (AddInheritance(collection, subCollection, false)) continue; @@ -146,6 +146,15 @@ public class InheritanceManager : IDisposable changes = true; Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); } + else if (_storage.ByName(subCollectionName, out subCollection)) + { + changes = true; + Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID."); + if (AddInheritance(collection, subCollection, false)) + continue; + + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + } else { Penumbra.Messager.NotificationMessage( diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 5d9de13d..de08c6a2 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,3 +1,4 @@ +using OtterGui; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; @@ -9,13 +10,13 @@ namespace Penumbra.Collections.Manager; public class TempCollectionManager : IDisposable { - public int GlobalChangeCounter { get; private set; } = 0; + public int GlobalChangeCounter { get; private set; } public readonly IndividualCollections Collections; - private readonly CommunicatorService _communicator; - private readonly CollectionStorage _storage; - private readonly ActorManager _actors; - private readonly Dictionary _customCollections = new(); + private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; + private readonly ActorManager _actors; + private readonly Dictionary _customCollections = []; public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage) { @@ -42,36 +43,36 @@ public class TempCollectionManager : IDisposable => _customCollections.Values; public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _customCollections.TryGetValue(name.ToLowerInvariant(), out collection); + => _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection); - public string CreateTemporaryCollection(string name) + public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + => _customCollections.TryGetValue(id, out collection); + + public Guid CreateTemporaryCollection(string name) { - if (_storage.ByName(name, out _)) - return string.Empty; - if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); - Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}."); - if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) + Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); + if (_customCollections.TryAdd(collection.Id, collection)) { // Temporary collection created. _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty); - return collection.Name; + return collection.Id; } - return string.Empty; + return Guid.Empty; } - public bool RemoveTemporaryCollection(string collectionName) + public bool RemoveTemporaryCollection(Guid collectionId) { - if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection)) + if (!_customCollections.Remove(collectionId, out var collection)) { - Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist."); + Penumbra.Log.Debug($"Tried to delete temporary collection {collectionId}, but did not exist."); return false; } - Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}."); + Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { @@ -80,7 +81,7 @@ public class TempCollectionManager : IDisposable // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); - Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}."); + Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}."); Collections.Delete(i--); } @@ -98,32 +99,32 @@ public class TempCollectionManager : IDisposable return true; } - public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) + public bool AddIdentifier(Guid collectionId, params ActorIdentifier[] identifiers) { - if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection)) + if (!_customCollections.TryGetValue(collectionId, out var collection)) return false; return AddIdentifier(collection, identifiers); } - public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue) + public bool AddIdentifier(Guid collectionId, string characterName, ushort worldId = ushort.MaxValue) { - if (!ByteString.FromString(characterName, out var byteString, false)) + if (!ByteString.FromString(characterName, out var byteString)) return false; var identifier = _actors.CreatePlayer(byteString, worldId); if (!identifier.IsValid) return false; - return AddIdentifier(collectionName, identifier); + return AddIdentifier(collectionId, identifier); } internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue) { - if (!ByteString.FromString(characterName, out var byteString, false)) + if (!ByteString.FromString(characterName, out var byteString)) return false; var identifier = _actors.CreatePlayer(byteString, worldId); - return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); + return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index b63be6cd..c1143c71 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -17,7 +17,7 @@ namespace Penumbra.Collections; /// public partial class ModCollection { - public const int CurrentVersion = 1; + public const int CurrentVersion = 2; public const string DefaultCollectionName = "Default"; public const string EmptyCollectionName = "None"; @@ -27,15 +27,23 @@ public partial class ModCollection /// public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); - /// The name of a collection can not contain characters invalid in a path. - public string Name { get; internal init; } + /// The name of a collection. + public string Name { get; set; } = string.Empty; + + public Guid Id { get; } + + public string Identifier + => Id.ToString(); + + public string ShortIdentifier + => Identifier[..8]; public override string ToString() - => Name; + => Name.Length > 0 ? Name : ShortIdentifier; /// Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; + => this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier; /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } @@ -112,16 +120,16 @@ public partial class ModCollection public ModCollection Duplicate(string name, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), + return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index, Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings) + var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -134,8 +142,7 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List(), new List(), - new Dictionary()); + var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []); return ret; } @@ -143,9 +150,8 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), - new List(), - new Dictionary()); + return new ModCollection(Guid.Empty, name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + []); } /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. @@ -193,10 +199,11 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(string name, int index, int changeCounter, int version, List appliedSettings, + private ModCollection(Guid id, string name, int index, int changeCounter, int version, List appliedSettings, List inheritsFrom, Dictionary settings) { Name = name; + Id = id; Index = index; ChangeCounter = changeCounter; Settings = appliedSettings; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index f2cb4ada..acc38d83 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -28,6 +28,8 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteStartObject(); j.WritePropertyName("Version"); j.WriteValue(ModCollection.CurrentVersion); + j.WritePropertyName(nameof(ModCollection.Id)); + j.WriteValue(modCollection.Identifier); j.WritePropertyName(nameof(ModCollection.Name)); j.WriteValue(modCollection.Name); j.WritePropertyName(nameof(ModCollection.Settings)); @@ -55,20 +57,20 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Name)); + x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier)); j.WriteEndObject(); } - public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary settings, + public static bool LoadFromFile(FileInfo file, out Guid id, out string name, out int version, out Dictionary settings, out IReadOnlyList inheritance) { - settings = new Dictionary(); - inheritance = Array.Empty(); + settings = []; + inheritance = []; if (!file.Exists) { Penumbra.Log.Error("Could not read collection because file does not exist."); - name = string.Empty; - + name = string.Empty; + id = Guid.Empty; version = 0; return false; } @@ -76,8 +78,9 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection try { var obj = JObject.Parse(File.ReadAllText(file.FullName)); - name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; version = obj["Version"]?.ToObject() ?? 0; + name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; + id = obj[nameof(ModCollection.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; @@ -87,6 +90,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection { name = string.Empty; version = 0; + id = Guid.Empty; Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); return false; } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 537b08da..4e1d6453 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -513,7 +513,7 @@ public class CommandHandler : IDisposable collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager.Storage.ByName(lowerName, out var c) + : _collectionManager.Storage.ByIdentifier(lowerName, out var c) ? c : null; if (collection != null) diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 754570e2..554e2221 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; using Penumbra.Api.Enums; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ public sealed class ChangedItemClick() : EventWrapper + /// Default = 0, /// diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 10607da4..2dcced35 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class ChangedItemHover() : EventWrapper + /// Default = 0, /// diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index 397f7bfd..8992f9fc 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Collections; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 2f249c14..8a906ca0 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Services; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ namespace Penumbra.Communication; /// Parameter is a pointer to the equip data array. /// public sealed class CreatingCharacterBase() - : EventWrapper(nameof(CreatingCharacterBase)) + : EventWrapper(nameof(CreatingCharacterBase)) { public enum Priority { diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index be6343b7..846b1a58 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.IpcSubscribers; namespace Penumbra.Communication; @@ -13,7 +14,7 @@ public sealed class EnabledChanged() : EventWrapper + /// Api = int.MinValue, /// diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 20d13b20..02293873 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModFileChanged.cs b/Penumbra/Communication/ModFileChanged.cs index 8b4b6f5d..8cda48e9 100644 --- a/Penumbra/Communication/ModFileChanged.cs +++ b/Penumbra/Communication/ModFileChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Editor; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index f02b17dc..0df58b5f 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 01c8fa64..1e4f8d36 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -19,8 +20,11 @@ public sealed class ModPathChanged() { public enum Priority { - /// - Api = int.MinValue, + /// + ApiMods = int.MinValue, + + /// + ApiModSettings = int.MinValue, /// EphemeralConfig = -500, diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 412b3003..968f78a7 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; diff --git a/Penumbra/Communication/PostEnabledDraw.cs b/Penumbra/Communication/PostEnabledDraw.cs index 68637442..e21f0183 100644 --- a/Penumbra/Communication/PostEnabledDraw.cs +++ b/Penumbra/Communication/PostEnabledDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PostEnabledDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index a918b610..525ac73e 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PostSettingsPanelDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index cda00d78..33f6b4e1 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PreSettingsPanelDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PreSettingsTabBarDraw.cs b/Penumbra/Communication/PreSettingsTabBarDraw.cs index 2c14cdf1..8614bbbe 100644 --- a/Penumbra/Communication/PreSettingsTabBarDraw.cs +++ b/Penumbra/Communication/PreSettingsTabBarDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ public sealed class PreSettingsTabBarDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/GuidExtensions.cs b/Penumbra/GuidExtensions.cs new file mode 100644 index 00000000..fcbc8a3b --- /dev/null +++ b/Penumbra/GuidExtensions.cs @@ -0,0 +1,254 @@ +using System.Collections.Frozen; +using OtterGui; + +namespace Penumbra; + +public static class GuidExtensions +{ + private const string Chars = + "0123456789" + + "abcdefghij" + + "klmnopqrst" + + "uv"; + + private static ReadOnlySpan Bytes + => "0123456789abcdefghijklmnopqrstuv"u8; + + private static readonly FrozenDictionary + ReverseChars = Chars.WithIndex().ToFrozenDictionary(t => t.Value, t => (byte)t.Index); + + private static readonly FrozenDictionary ReverseBytes = + ReverseChars.ToFrozenDictionary(kvp => (byte)kvp.Key, kvp => kvp.Value); + + public static unsafe string OptimizedString(this Guid guid) + { + var bytes = stackalloc ulong[2]; + if (!guid.TryWriteBytes(new Span(bytes, 16))) + return guid.ToString("N"); + + var u1 = bytes[0]; + var u2 = bytes[1]; + Span text = + [ + Chars[(int)(u1 & 0x1F)], + Chars[(int)((u1 >> 5) & 0x1F)], + Chars[(int)((u1 >> 10) & 0x1F)], + Chars[(int)((u1 >> 15) & 0x1F)], + Chars[(int)((u1 >> 20) & 0x1F)], + Chars[(int)((u1 >> 25) & 0x1F)], + Chars[(int)((u1 >> 30) & 0x1F)], + Chars[(int)((u1 >> 35) & 0x1F)], + Chars[(int)((u1 >> 40) & 0x1F)], + Chars[(int)((u1 >> 45) & 0x1F)], + Chars[(int)((u1 >> 50) & 0x1F)], + Chars[(int)((u1 >> 55) & 0x1F)], + Chars[(int)((u1 >> 60) | ((u2 & 0x01) << 4))], + Chars[(int)((u2 >> 1) & 0x1F)], + Chars[(int)((u2 >> 6) & 0x1F)], + Chars[(int)((u2 >> 11) & 0x1F)], + Chars[(int)((u2 >> 16) & 0x1F)], + Chars[(int)((u2 >> 21) & 0x1F)], + Chars[(int)((u2 >> 26) & 0x1F)], + Chars[(int)((u2 >> 31) & 0x1F)], + Chars[(int)((u2 >> 36) & 0x1F)], + Chars[(int)((u2 >> 41) & 0x1F)], + Chars[(int)((u2 >> 46) & 0x1F)], + Chars[(int)((u2 >> 51) & 0x1F)], + Chars[(int)((u2 >> 56) & 0x1F)], + Chars[(int)((u2 >> 61) & 0x1F)], + ]; + return new string(text); + } + + public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) + { + if (text.Length != 26) + return Return(out guid); + + var bytes = stackalloc ulong[2]; + if (!ReverseChars.TryGetValue(text[0], out var b0)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[1], out var b1)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[2], out var b2)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[3], out var b3)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[4], out var b4)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[5], out var b5)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[6], out var b6)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[7], out var b7)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[8], out var b8)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[9], out var b9)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[10], out var b10)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[11], out var b11)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[12], out var b12)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[13], out var b13)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[14], out var b14)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[15], out var b15)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[16], out var b16)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[17], out var b17)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[18], out var b18)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[19], out var b19)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[20], out var b20)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[21], out var b21)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[22], out var b22)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[23], out var b23)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[24], out var b24)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[25], out var b25)) + return Return(out guid); + + bytes[0] = b0 + | ((ulong)b1 << 5) + | ((ulong)b2 << 10) + | ((ulong)b3 << 15) + | ((ulong)b4 << 20) + | ((ulong)b5 << 25) + | ((ulong)b6 << 30) + | ((ulong)b7 << 35) + | ((ulong)b8 << 40) + | ((ulong)b9 << 45) + | ((ulong)b10 << 50) + | ((ulong)b11 << 55) + | ((ulong)b12 << 60); + bytes[1] = ((ulong)b12 >> 4) + | ((ulong)b13 << 1) + | ((ulong)b14 << 6) + | ((ulong)b15 << 11) + | ((ulong)b16 << 16) + | ((ulong)b17 << 21) + | ((ulong)b18 << 26) + | ((ulong)b19 << 31) + | ((ulong)b20 << 36) + | ((ulong)b21 << 41) + | ((ulong)b22 << 46) + | ((ulong)b23 << 51) + | ((ulong)b24 << 56) + | ((ulong)b25 << 61); + guid = new Guid(new Span(bytes, 16)); + return true; + + static bool Return(out Guid guid) + { + guid = Guid.Empty; + return false; + } + } + + public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) + { + if (text.Length != 26) + return Return(out guid); + + var bytes = stackalloc ulong[2]; + if (!ReverseBytes.TryGetValue(text[0], out var b0)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[1], out var b1)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[2], out var b2)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[3], out var b3)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[4], out var b4)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[5], out var b5)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[6], out var b6)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[7], out var b7)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[8], out var b8)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[9], out var b9)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[10], out var b10)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[11], out var b11)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[12], out var b12)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[13], out var b13)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[14], out var b14)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[15], out var b15)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[16], out var b16)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[17], out var b17)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[18], out var b18)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[19], out var b19)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[20], out var b20)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[21], out var b21)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[22], out var b22)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[23], out var b23)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[24], out var b24)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[25], out var b25)) + return Return(out guid); + + bytes[0] = b0 + | ((ulong)b1 << 5) + | ((ulong)b2 << 10) + | ((ulong)b3 << 15) + | ((ulong)b4 << 20) + | ((ulong)b5 << 25) + | ((ulong)b6 << 30) + | ((ulong)b7 << 35) + | ((ulong)b8 << 40) + | ((ulong)b9 << 45) + | ((ulong)b10 << 50) + | ((ulong)b11 << 55) + | ((ulong)b12 << 60); + bytes[1] = ((ulong)b12 >> 4) + | ((ulong)b13 << 1) + | ((ulong)b14 << 6) + | ((ulong)b15 << 11) + | ((ulong)b16 << 16) + | ((ulong)b17 << 21) + | ((ulong)b18 << 26) + | ((ulong)b19 << 31) + | ((ulong)b20 << 36) + | ((ulong)b21 << 41) + | ((ulong)b22 << 46) + | ((ulong)b23 << 51) + | ((ulong)b24 << 56) + | ((ulong)b25 << 61); + guid = new Guid(new Span(bytes, 16)); + return true; + + static bool Return(out Guid guid) + { + guid = Guid.Empty; + return false; + } + } +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a3400540..5f07ffc5 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -130,7 +130,7 @@ public sealed unsafe class MetaState : IDisposable _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, (nint)modelCharaId, (nint)customize, (nint)equipData); + _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 7c16b97b..5c3d8d19 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Runtime; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -43,8 +44,8 @@ public class PathResolver : IDisposable } /// Obtain a temporary or permanent collection by name. - public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection); + public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + => _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection); /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -136,9 +137,10 @@ public class PathResolver : IDisposable return; var lastUnderscore = additionalData.LastIndexOf((byte)'_'); - var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString(); + var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore); if (Utf8GamePath.FromByteString(path, out var gamePath) - && CollectionByName(name, out var collection) + && GuidExtensions.FromOptimizedString(idString.Span, out var id) + && CollectionById(id, out var collection) && collection.HasCache && collection.GetImcFile(gamePath, out var file)) { diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 2359c36e..844baaa9 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -76,7 +76,7 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary>> GetResourcePathDictionaries( + IEnumerable<(Character, ResourceTree)> resourceTrees) { var pathDictionaries = new Dictionary>>(4); @@ -23,8 +25,7 @@ internal static class ResourceTreeApiHelper CollectResourcePaths(pathDictionary, resourceTree); } - return pathDictionaries.ToDictionary(pair => pair.Key, - pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + return pathDictionaries; } private static void CollectResourcePaths(Dictionary> pathDictionary, ResourceTree resourceTree) @@ -37,7 +38,7 @@ internal static class ResourceTreeApiHelper var fullPath = node.FullPath.ToPath(); if (!pathDictionary.TryGetValue(fullPath, out var gamePaths)) { - gamePaths = new(); + gamePaths = []; pathDictionary.Add(fullPath, gamePaths); } @@ -46,17 +47,17 @@ internal static class ResourceTreeApiHelper } } - public static Dictionary> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + public static Dictionary GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, ResourceType type) { - var resDictionaries = new Dictionary>(4); + var resDictionaries = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) { if (resDictionaries.ContainsKey(gameObject.ObjectIndex)) continue; - var resDictionary = new Dictionary(); - resDictionaries.Add(gameObject.ObjectIndex, resDictionary); + var resDictionary = new Dictionary(); + resDictionaries.Add(gameObject.ObjectIndex, new GameResourceDict(resDictionary)); foreach (var node in resourceTree.FlatNodes) { @@ -66,38 +67,16 @@ internal static class ResourceTreeApiHelper continue; var fullPath = node.FullPath.ToPath(); - resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, ChangedItemDrawer.ToApiIcon(node.Icon))); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon))); } } - return resDictionaries.ToDictionary(pair => pair.Key, - pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); + return resDictionaries; } - public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) { - static Ipc.ResourceNode GetIpcNode(ResourceNode node) => - new() - { - Type = node.Type, - Icon = ChangedItemDrawer.ToApiIcon(node.Icon), - Name = node.Name, - GamePath = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), - ActualPath = node.FullPath.ToString(), - ObjectAddress = node.ObjectAddress, - ResourceHandle = node.ResourceHandle, - Children = node.Children.Select(GetIpcNode).ToList(), - }; - - static Ipc.ResourceTree GetIpcTree(ResourceTree tree) => - new() - { - Name = tree.Name, - RaceCode = (ushort)tree.RaceCode, - Nodes = tree.Nodes.Select(GetIpcNode).ToList(), - }; - - var resDictionary = new Dictionary(4); + var resDictionary = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) { if (resDictionary.ContainsKey(gameObject.ObjectIndex)) @@ -107,5 +86,38 @@ internal static class ResourceTreeApiHelper } return resDictionary; + + static JObject GetIpcTree(ResourceTree tree) + { + var ret = new JObject + { + [nameof(ResourceTreeDto.Name)] = tree.Name, + [nameof(ResourceTreeDto.RaceCode)] = (ushort)tree.RaceCode, + }; + var children = new JArray(); + foreach (var child in tree.Nodes) + children.Add(GetIpcNode(child)); + ret[nameof(ResourceTreeDto.Nodes)] = children; + return ret; + } + + static JObject GetIpcNode(ResourceNode node) + { + var ret = new JObject + { + [nameof(ResourceNodeDto.Type)] = new JValue(node.Type), + [nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)), + [nameof(ResourceNodeDto.Name)] = node.Name, + [nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), + [nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(), + [nameof(ResourceNodeDto.ObjectAddress)] = node.ObjectAddress, + [nameof(ResourceNodeDto.ResourceHandle)] = node.ResourceHandle, + }; + var children = new JArray(); + foreach (var child in node.Children) + children.Add(GetIpcNode(child)); + ret[nameof(ResourceNodeDto.Children)] = children; + return ret; + } } } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 0ffdc4af..9efb8a3f 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -272,22 +272,19 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return; var group = mod.Groups[groupIdx]; - if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - switch (group) { + case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return; case SingleModGroup s: + o.SetPosition(groupIdx, s.Count); s.OptionData.Add(o); break; case MultiModGroup m: + o.SetPosition(groupIdx, m.Count); m.PrioritizedOptions.Add((o, priority)); break; } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 1e5df6b9..65b8ddd9 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -19,7 +19,7 @@ public class ModCombo : FilterComboCache public class ModStorage : IReadOnlyList { /// The actual list of mods. - protected readonly List Mods = new(); + protected readonly List Mods = []; public int Count => Mods.Count; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index e9e2a93b..2daf31e6 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -4,9 +4,9 @@ using Penumbra.Services; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IEnumerable +public interface IModGroup : IReadOnlyCollection { - public const int MaxMultiOptions = 32; + public const int MaxMultiOptions = 63; public string Name { get; } public string Description { get; } @@ -18,15 +18,7 @@ public interface IModGroup : IEnumerable public ISubMod this[Index idx] { get; } - public int Count { get; } - - public bool IsOption - => Type switch - { - GroupType.Single => Count > 1, - GroupType.Multi => Count > 0, - _ => false, - }; + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); @@ -94,11 +86,13 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) + { ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch { GroupType.Multi => _group.OptionPriority(idx), - _ => null, + _ => null, }); + } j.WriteEndArray(); j.WriteEndObject(); diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index b79b3242..380b242c 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -12,7 +12,7 @@ public class ModSettings { public static readonly ModSettings Empty = new(); public SettingList Settings { get; private init; } = []; - public ModPriority Priority { get; set; } + public ModPriority Priority { get; set; } public bool Enabled { get; set; } // Create an independent copy of the current settings. @@ -152,7 +152,7 @@ public class ModSettings public struct SavedSettings { public Dictionary Settings; - public ModPriority Priority; + public ModPriority Priority; public bool Enabled; public SavedSettings DeepCopy() @@ -203,9 +203,9 @@ public class ModSettings // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Does not repair settings but ignores settings not fitting to the given mod. - public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) + public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) { - var dict = new Dictionary>(Settings.Count); + var dict = new Dictionary>(Settings.Count); foreach (var (setting, idx) in Settings.WithIndex()) { if (idx >= mod.Groups.Count) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 444e8e2c..7479cd54 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -25,6 +25,9 @@ public sealed class MultiModGroup : IModGroup public ISubMod this[Index idx] => PrioritizedOptions[idx].Mod; + public bool IsOption + => Count > 0; + [JsonIgnore] public int Count => PrioritizedOptions.Count; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 0bfa04f4..74769c7e 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -25,6 +25,9 @@ public sealed class SingleModGroup : IModGroup public ISubMod this[Index idx] => OptionData[idx]; + public bool IsOption + => Count > 1; + [JsonIgnore] public int Count => OptionData.Count; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 4de2ac13..6be07881 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -53,7 +53,7 @@ public class TemporaryMod : IMod dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null); + $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null); var mod = new Mod(dir); var defaultMod = mod.Default; foreach (var (gamePath, fullPath) in collection.ResolvedFiles) @@ -86,11 +86,11 @@ public class TemporaryMod : IMod saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); - Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}."); + Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } catch (Exception e) { - Penumbra.Log.Error($"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}"); + Penumbra.Log.Error($"Could not save temporary collection {collection.Identifier} to permanent Mod:\n{e}"); if (dir != null && Directory.Exists(dir.FullName)) { try diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b76780c0..42be0aa3 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -20,6 +20,7 @@ using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; +using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra; @@ -105,8 +106,7 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { - var api = _services.GetService(); - _services.GetService(); + _services.GetService(); _communicatorService.ChangedItemHover.Subscribe(it => { if (it is (Item, FullEquipType)) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b07917e8..c8961579 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + net8.0-windows preview diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index d1e952f1..e775d81a 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -223,7 +223,7 @@ public class ConfigMigrationService(SaveService saveService) : IService try { var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); - if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) + if (jObject["Name"]?.ToObject() == ForcedCollection) continue; jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); @@ -365,7 +365,7 @@ public class ConfigMigrationService(SaveService saveService) : IService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, []); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 078b812b..6805e7db 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -239,7 +239,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(character); lock (_eventWriter) { - _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type); + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type); } } catch (Exception ex) @@ -248,7 +248,7 @@ public sealed class CrashHandlerService : IDisposable, IService } } - private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3) + private void OnCreatingCharacterBase(nint address, Guid collection, nint _1, nint _2, nint _3) { if (_eventWriter == null) return; @@ -293,7 +293,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(resolveData.AssociatedGameObject); lock (_eventWriter) { - _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name, + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id, manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 40c63f15..e1c482f7 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -24,7 +24,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => CollectionFile(collection.Name); + => CollectionFile(collection.Identifier); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 9e6071b4..e758aa35 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -30,8 +31,10 @@ using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; using Penumbra.UI.Tabs.Debug; +using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Api.IpcTester; namespace Penumbra.Services; @@ -195,10 +198,5 @@ public static class StaticServiceManager .AddSingleton(); private static ServiceManager AddApi(this ServiceManager services) - => services.AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + => services.AddSingleton(x => x.GetRequiredService()); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 9a38a5d5..10956deb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -25,8 +25,8 @@ public partial class ModEditWindow var resources = ResourceTreeApiHelper .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) .Values - .SelectMany(resources => resources.Values) - .Select(resource => resource.Item1); + .SelectMany(r => r.Values) + .Select(r => r.Item1); return new HashSet(resources, StringComparer.OrdinalIgnoreCase); } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 4d922af5..cbeabbd6 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,11 +2,13 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -28,8 +30,8 @@ public sealed class CollectionPanel : IDisposable private readonly IndividualAssignmentUi _individualAssignmentUi; private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; - - private readonly IFontHandle _nameFont; + private readonly FilenameService _fileNames; + private readonly IFontHandle _nameFont; private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); @@ -38,7 +40,7 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods) + CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames) { _collections = manager.Storage; _active = manager.Active; @@ -46,6 +48,7 @@ public sealed class CollectionPanel : IDisposable _actors = actors; _targets = targets; _mods = mods; + _fileNames = fileNames; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); _inheritanceUi = new InheritanceUi(manager, _selector); _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); @@ -206,12 +209,57 @@ public sealed class CollectionPanel : IDisposable var collection = _active.Current; DrawCollectionName(collection); DrawStatistics(collection); + DrawCollectionData(collection); _inheritanceUi.Draw(); ImGui.Separator(); DrawInactiveSettingsList(collection); DrawSettingsList(collection); } + private void DrawCollectionData(ModCollection collection) + { + ImGui.Dummy(Vector2.Zero); + ImGui.BeginGroup(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Name"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Identifier"); + ImGui.EndGroup(); + ImGui.SameLine(); + ImGui.BeginGroup(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var name = collection.Name; + var identifier = collection.Identifier; + var width = ImGui.GetContentRegionAvail().X; + var fileName = _fileNames.CollectionFile(collection); + ImGui.SetNextItemWidth(width); + ImGui.InputText("##name", ref name, 128); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", + NotificationType.Warning); + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(identifier); + + ImGuiUtil.HoverTooltip( + $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); + + ImGui.EndGroup(); + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + } + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix) { var label = $"{type}{text}{suffix}"; diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index e568ecaf..fac85d4d 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -24,7 +24,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, TutorialService tutorial) - : base(new List(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) + : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) { _config = config; _communicator = communicator; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 3c6a3ed9..fe1471b3 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -41,12 +41,12 @@ public sealed class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, - CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial) + CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) { _config = configuration.Ephemeral; _tutorial = tutorial; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames); } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 78014054..4649e548 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -55,7 +55,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn(character.ThreadId.ToString()); ImGuiUtil.DrawTableColumn(character.CharacterName); - ImGuiUtil.DrawTableColumn(character.CollectionName); + ImGuiUtil.DrawTableColumn(character.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(character.CharacterAddress); ImGuiUtil.DrawTableColumn(character.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); @@ -79,7 +79,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(file.ActualFileName); ImGuiUtil.DrawTableColumn(file.RequestedFileName); ImGuiUtil.DrawTableColumn(file.CharacterName); - ImGuiUtil.DrawTableColumn(file.CollectionName); + ImGuiUtil.DrawTableColumn(file.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(file.CharacterAddress); ImGuiUtil.DrawTableColumn(file.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); @@ -102,7 +102,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); ImGuiUtil.DrawTableColumn(vfx.InvocationType); ImGuiUtil.DrawTableColumn(vfx.CharacterName); - ImGuiUtil.DrawTableColumn(vfx.CollectionName); + ImGuiUtil.DrawTableColumn(vfx.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(vfx.CharacterAddress); ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a956d2d..1813a7e3 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -40,6 +40,7 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; +using Penumbra.Api.IpcTester; namespace Penumbra.UI.Tabs.Debug; @@ -76,7 +77,6 @@ public class DebugTab : Window, ITab private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly ResourceManagerService _resourceManager; - private readonly PenumbraIpcProviders _ipc; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly PathState _pathState; @@ -100,7 +100,7 @@ public class DebugTab : Window, ITab IClientState clientState, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, - ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, + ResourceManagerService resourceManager, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, @@ -124,7 +124,6 @@ public class DebugTab : Window, ITab _characterUtility = characterUtility; _residentResources = residentResources; _resourceManager = resourceManager; - _ipc = ipc; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _pathState = pathState; @@ -440,7 +439,9 @@ public class DebugTab : Window, ITab : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind ==(byte) ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.AsObject->DataID}" : identifier.DataId.ToString(); + var id = obj.AsObject->ObjectKind == (byte)ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->DataID}" + : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } @@ -969,13 +970,8 @@ public class DebugTab : Window, ITab /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { - if (!ImGui.CollapsingHeader("IPC")) - { - _ipcTester.UnsubscribeEvents(); - return; - } - - _ipcTester.Draw(); + if (ImGui.CollapsingHeader("IPC")) + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. From 1ef9346eab9e827bf31664dae0171656d2cae1af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:42:16 +0200 Subject: [PATCH 1613/2451] Allow renaming of collection. --- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 37 ++++++++++++------- Penumbra/UI/CollectionTab/InheritanceUi.cs | 29 +++++---------- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 7c29676d..b073e731 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -77,7 +77,7 @@ public partial class ModCollection else { _cache.Meta.SetFiles(); - Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}."); + Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}."); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index c1143c71..327d6544 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -28,7 +28,7 @@ public partial class ModCollection public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); /// The name of a collection. - public string Name { get; set; } = string.Empty; + public string Name { get; set; } public Guid Id { get; } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cbeabbd6..8625335e 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -35,7 +35,8 @@ public sealed class CollectionPanel : IDisposable private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); - private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new(); + private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = []; + private string? _newName; private int _draggedIndividualAssignment = -1; @@ -93,6 +94,18 @@ public sealed class CollectionPanel : IDisposable var first = true; + Button(CollectionType.NonPlayerChild); + Button(CollectionType.NonPlayerElderly); + foreach (var race in Enum.GetValues().Skip(1)) + { + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); + } + + return; + void Button(CollectionType type) { var (name, border) = Buttons[type]; @@ -112,16 +125,6 @@ public sealed class CollectionPanel : IDisposable if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) ImGui.NewLine(); } - - Button(CollectionType.NonPlayerChild); - Button(CollectionType.NonPlayerElderly); - foreach (var race in Enum.GetValues().Skip(1)) - { - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); - } } /// Draw the panel containing new and existing individual assignments. @@ -228,12 +231,20 @@ public sealed class CollectionPanel : IDisposable ImGui.SameLine(); ImGui.BeginGroup(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = collection.Name; + var name = _newName ?? collection.Name; var identifier = collection.Identifier; var width = ImGui.GetContentRegionAvail().X; var fileName = _fileNames.CollectionFile(collection); ImGui.SetNextItemWidth(width); - ImGui.InputText("##name", ref name, 128); + if (ImGui.InputText("##name", ref name, 128)) + _newName = name; + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null) + { + collection.Name = _newName; + _newName = null; + } + else if (ImGui.IsItemDeactivated()) + _newName = null; using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 88344e6a..2290592d 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -2,30 +2,21 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public class InheritanceUi +public class InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) : IUiService { private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly CollectionStorage _collections; - private readonly ActiveCollections _active; - private readonly InheritanceManager _inheritance; - private readonly CollectionSelector _selector; - - public InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) - { - _selector = selector; - _collections = collectionManager.Storage; - _active = collectionManager.Active; - _inheritance = collectionManager.Inheritances; - } - + private readonly CollectionStorage _collections = collectionManager.Storage; + private readonly ActiveCollections _active = collectionManager.Active; + private readonly InheritanceManager _inheritance = collectionManager.Inheritances; /// Draw the whole inheritance block. public void Draw() @@ -59,7 +50,7 @@ public class InheritanceUi private (int, int)? _inheritanceAction; private ModCollection? _newCurrentCollection; - private void DrawRightText() + private static void DrawRightText() { using var group = ImRaii.Group(); ImGuiUtil.TextWrapped( @@ -68,7 +59,7 @@ public class InheritanceUi "You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can."); } - private void DrawHelpPopup() + private static void DrawHelpPopup() => ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () => { ImGui.NewLine(); @@ -123,7 +114,7 @@ public class InheritanceUi _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Id}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); @@ -134,7 +125,7 @@ public class InheritanceUi // Draw the notch and increase the line length. var midPoint = (minRect.Y + maxRect.Y) / 2f - 1f; - drawList.AddLine(new Vector2(lineStart.X, midPoint), new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, + drawList.AddLine(lineStart with { Y = midPoint }, new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, UiHelpers.Scale); lineEnd.Y = midPoint; } @@ -321,5 +312,5 @@ public class InheritanceUi } private string Name(ModCollection collection) - => _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + => selector.IncognitoMode ? collection.AnonymizedName : collection.Name; } From 791583e183d4afd0a72e047d988f8ad5cb62a728 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:51:33 +0200 Subject: [PATCH 1614/2451] Silence readme warnings. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index e5c8f544..9bbc3b98 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc +Subproject commit 9bbc3b98efc2af3707adc75b716d4f3072908e31 diff --git a/Penumbra.String b/Penumbra.String index 14e00f77..caa58c5c 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 14e00f77d42bc677e02325660db765ef11932560 +Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488 From e4f9150c9fc22ab85582f13a5dc866b115e61626 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:30:47 +0200 Subject: [PATCH 1615/2451] Fix? --- Penumbra.Api | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 8 ++++---- Penumbra/Communication/ModDirectoryChanged.cs | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 9bbc3b98..cd56068a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 9bbc3b98efc2af3707adc75b716d4f3072908e31 +Subproject commit cd56068aac3762c7b011d13a04637a3c3f09775f diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index 2e87486f..e1eec1b2 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -13,16 +13,16 @@ public class PluginStateApi(Configuration config, CommunicatorService communicat public string GetConfiguration() => JsonConvert.SerializeObject(config, Formatting.Indented); - public event Action? ModDirectoryChanged + public event Action ModDirectoryChanged { - add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - remove => communicator.ModDirectoryChanged.Unsubscribe(value!); + add => communicator.ModDirectoryChanged.Subscribe(value, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value); } public bool GetEnabledState() => config.EnableMods; - public event Action? EnabledChange + public event Action EnabledChange { add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); remove => communicator.EnabledChanged.Unsubscribe(value!); diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 02293873..9c64573f 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -15,7 +14,7 @@ public sealed class ModDirectoryChanged() : EventWrapper + /// Api = 0, /// From d5ed4a38e4b760b369cc2cbe773a9d017ff464d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:39:12 +0200 Subject: [PATCH 1616/2451] Fix2? --- Penumbra.Api | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 43 ++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index cd56068a..a8e2fe02 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit cd56068aac3762c7b011d13a04637a3c3f09775f +Subproject commit a8e2fe0219b8fd1f787171f11e33571317e531c1 diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index e1eec1b2..e053e56f 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -5,26 +5,41 @@ using Penumbra.Services; namespace Penumbra.Api.Api; -public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService +public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisposable { + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + + public PluginStateApi(Configuration config, CommunicatorService communicator) + { + _config = config; + _communicator = communicator; + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChanged, Communication.ModDirectoryChanged.Priority.Api); + _communicator.EnabledChanged.Subscribe(OnEnabledChanged, EnabledChanged.Priority.Api); + } + + public void Dispose() + { + _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChanged); + _communicator.EnabledChanged.Unsubscribe(OnEnabledChanged); + } + public string GetModDirectory() - => config.ModDirectory; + => _config.ModDirectory; public string GetConfiguration() - => JsonConvert.SerializeObject(config, Formatting.Indented); + => JsonConvert.SerializeObject(_config, Formatting.Indented); - public event Action ModDirectoryChanged - { - add => communicator.ModDirectoryChanged.Subscribe(value, Communication.ModDirectoryChanged.Priority.Api); - remove => communicator.ModDirectoryChanged.Unsubscribe(value); - } + public event Action? ModDirectoryChanged; public bool GetEnabledState() - => config.EnableMods; + => _config.EnableMods; - public event Action EnabledChange - { - add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - remove => communicator.EnabledChanged.Unsubscribe(value!); - } + public event Action? EnabledChange; + + private void OnModDirectoryChanged(string modDirectory, bool valid) + => ModDirectoryChanged?.Invoke(modDirectory, valid); + + private void OnEnabledChanged(bool value) + => EnabledChange?.Invoke(value); } From d9bd05c9ecf675da9d2d793b1c0654c8a6c5a613 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:45:25 +0200 Subject: [PATCH 1617/2451] Fix issue with Hex viewer. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9599c806..a50d2aed 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84 +Subproject commit a50d2aedbb7b2e37d30987bd0b3b96a832fdc0af From 6b5321dad8103ca3a9077f511f47047e90ccdddf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:46:26 +0200 Subject: [PATCH 1618/2451] Test subscription like before but without primary constructor. --- Penumbra/Api/Api/PluginStateApi.cs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index e053e56f..d69df448 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -5,7 +5,7 @@ using Penumbra.Services; namespace Penumbra.Api.Api; -public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisposable +public class PluginStateApi : IPenumbraApiPluginState, IApiService { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -14,14 +14,6 @@ public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisp { _config = config; _communicator = communicator; - _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChanged, Communication.ModDirectoryChanged.Priority.Api); - _communicator.EnabledChanged.Subscribe(OnEnabledChanged, EnabledChanged.Priority.Api); - } - - public void Dispose() - { - _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChanged); - _communicator.EnabledChanged.Unsubscribe(OnEnabledChanged); } public string GetModDirectory() @@ -30,16 +22,18 @@ public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisp public string GetConfiguration() => JsonConvert.SerializeObject(_config, Formatting.Indented); - public event Action? ModDirectoryChanged; + public event Action? ModDirectoryChanged + { + add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => _communicator.ModDirectoryChanged.Unsubscribe(value!); + } public bool GetEnabledState() => _config.EnableMods; - public event Action? EnabledChange; - - private void OnModDirectoryChanged(string modDirectory, bool valid) - => ModDirectoryChanged?.Invoke(modDirectory, valid); - - private void OnEnabledChanged(bool value) - => EnabledChange?.Invoke(value); + public event Action? EnabledChange + { + add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => _communicator.EnabledChanged.Unsubscribe(value!); + } } From 42ad941ec22906f2d631ca22dd7dba16b16bca18 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Apr 2024 16:05:44 +0200 Subject: [PATCH 1619/2451] Add GetCollectionsByIdentifier. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ApiHelpers.cs | 6 ++++-- Penumbra/Api/Api/CollectionApi.cs | 19 +++++++++++++++++ Penumbra/Api/IpcProviders.cs | 1 + .../Api/IpcTester/CollectionsIpcTester.cs | 21 ++++++++++++++++++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index a8e2fe02..2f76f42e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a8e2fe0219b8fd1f787171f11e33571317e531c1 +Subproject commit 2f76f42e54141258d89300aa78d42fb3f1878092 diff --git a/Penumbra/Api/Api/ApiHelpers.cs b/Penumbra/Api/Api/ApiHelpers.cs index 32a3956f..92a30bce 100644 --- a/Penumbra/Api/Api/ApiHelpers.cs +++ b/Penumbra/Api/Api/ApiHelpers.cs @@ -48,8 +48,10 @@ public class ApiHelpers( [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") { - Penumbra.Log.Debug( - $"[{name}] Called with {args}, returned {ec}."); + if (ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged) + Penumbra.Log.Verbose($"[{name}] Called with {args}, returned {ec}."); + else + Penumbra.Log.Debug($"[{name}] Called with {args}, returned {ec}."); return ec; } diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index de704460..e99850a6 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -10,6 +11,24 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : public Dictionary GetCollections() => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier) + { + if (identifier.Length == 0) + return []; + + var list = new List<(Guid Id, string Name)>(4); + if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty) + list.Add((collection.Id, collection.Name)); + else if (identifier.Length >= 8) + list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) + .Select(c => (c.Id, c.Name))); + + list.AddRange(collections.Storage + .Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name))) + .Select(c => (c.Id, c.Name))); + return list; + } + public Dictionary GetChangedItemsForCollection(Guid collectionId) { try diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 293af588..cc98ef0d 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -19,6 +19,7 @@ public sealed class IpcProviders : IDisposable, IApiService _providers = [ IpcSubscribers.GetCollections.Provider(pi, api.Collection), + IpcSubscribers.GetCollectionsByIdentifier.Provider(pi, api.Collection), IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection), IpcSubscribers.GetCollection.Provider(pi, api.Collection), IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 12314f0c..2679bc69 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -35,7 +35,7 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); - ImGuiUtil.GuidInput("Collection Id##Collections", "Collection GUID...", string.Empty, ref _collectionId, ref _collectionIdString); + ImGuiUtil.GuidInput("Collection Id##Collections", "Collection Identifier...", string.Empty, ref _collectionId, ref _collectionIdString); ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); ImGui.SameLine(); ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); @@ -48,6 +48,25 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService if (_oldCollection != null) ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString()); + IpcTester.DrawIntro(GetCollectionsByIdentifier.Label, "Collection Identifier"); + var collectionList = new GetCollectionsByIdentifier(pi).Invoke(_collectionIdString); + if (collectionList.Count == 0) + { + DrawCollection(null); + } + else + { + DrawCollection(collectionList[0]); + foreach (var pair in collectionList.Skip(1)) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + DrawCollection(pair); + } + } + IpcTester.DrawIntro(GetCollection.Label, "Current Collection"); DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current)); From 94b53ce7fab320286a08c2533fc400e0055eccd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Apr 2024 16:06:04 +0200 Subject: [PATCH 1620/2451] Meh. --- Penumbra/Api/Api/CollectionApi.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index e99850a6..ff393aaf 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; From d4183a03c0734fb72e810fdbcfc2f3b33495ed1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Apr 2024 15:00:48 +0200 Subject: [PATCH 1621/2451] Fix bug with new empty collections. --- Penumbra/Collections/ModCollection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 327d6544..e666b151 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -25,7 +25,7 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); + public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []); /// The name of a collection. public string Name { get; set; } @@ -150,7 +150,7 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.Empty, name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } From aeccf2b1c60074a49ec01b6430d9806323c89079 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Apr 2024 15:38:14 +0200 Subject: [PATCH 1622/2451] Update Submodules. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index a50d2aed..3460a817 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a50d2aedbb7b2e37d30987bd0b3b96a832fdc0af +Subproject commit 3460a817fc5e01a6b60eb834c3c59031938388fc diff --git a/Penumbra.Api b/Penumbra.Api index 2f76f42e..0c8578cf 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2f76f42e54141258d89300aa78d42fb3f1878092 +Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f diff --git a/Penumbra.GameData b/Penumbra.GameData index 60222d79..fe9d563d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186 +Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 From 0fa62f40d76c1fbbddf9f4c569a7195db811ee82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Apr 2024 16:57:23 +0200 Subject: [PATCH 1623/2451] Add Versions provider. --- Penumbra/Api/IpcProviders.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index cc98ef0d..21fe0a7c 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -61,6 +61,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings), IpcSubscribers.ApiVersion.Provider(pi, api), + new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), From 1641166d6e168d8b7e185ae0762e574cddf63201 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Apr 2024 18:15:26 +0200 Subject: [PATCH 1624/2451] Disable IPC listeners by default. --- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 7 +++++-- Penumbra/Api/IpcTester/ModSettingsIpcTester.cs | 1 + Penumbra/Api/IpcTester/ModsIpcTester.cs | 6 ++++++ Penumbra/Api/IpcTester/PluginStateIpcTester.cs | 2 ++ Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 7 ++++--- Penumbra/Api/IpcTester/UiIpcTester.cs | 7 ++++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 2c41b882..93806162 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -33,9 +33,12 @@ public class GameStateIpcTester : IUiService, IDisposable public GameStateIpcTester(DalamudPluginInterface pi) { _pi = pi; - CharacterBaseCreating = CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); - CharacterBaseCreated = CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); + CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); + CharacterBaseCreated = IpcSubscribers.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); + CharacterBaseCreating.Disable(); + CharacterBaseCreated.Disable(); + GameObjectResourcePathResolved.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index c33fcdee..b117d603 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -37,6 +37,7 @@ public class ModSettingsIpcTester : IUiService, IDisposable { _pi = pi; SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); + SettingChanged.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 878a8214..43f397e5 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -55,13 +55,19 @@ public class ModsIpcTester : IUiService, IDisposable _lastMovedModFrom = s1; _lastMovedModTo = s2; }); + DeleteSubscriber.Disable(); + AddSubscriber.Disable(); + MoveSubscriber.Disable(); } public void Dispose() { DeleteSubscriber.Dispose(); + DeleteSubscriber.Disable(); AddSubscriber.Dispose(); + AddSubscriber.Disable(); MoveSubscriber.Dispose(); + MoveSubscriber.Disable(); } public void Draw() diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index 0588e5bd..984f17b1 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -36,6 +36,8 @@ public class PluginStateIpcTester : IUiService, IDisposable Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized); Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed); EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled); + ModDirectoryChanged.Disable(); + EnabledChange.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index 281c7ad4..801f0b97 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -22,9 +22,10 @@ public class RedrawingIpcTester : IUiService, IDisposable public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) { - _pi = pi; - _objects = objects; - Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + _pi = pi; + _objects = objects; + Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + Redrawn.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index 29ddc22e..d95b79b8 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -5,7 +5,6 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; -using Penumbra.Communication; namespace Penumbra.Api.IpcTester; @@ -38,6 +37,12 @@ public class UiIpcTester : IUiService, IDisposable PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); + PreSettingsTabBar.Disable(); + PreSettingsPanel.Disable(); + PostEnabled.Disable(); + PostSettingsPanelDraw.Disable(); + ChangedItemTooltip.Disable(); + ChangedItemClicked.Disable(); } public void Dispose() From fd1f9b95d60b85d036f27addd3f7a965e815be75 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 18 Apr 2024 21:23:18 +1000 Subject: [PATCH 1625/2451] Add Single2 support for UVs --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index d3ca87dc..c2562293 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -312,6 +312,7 @@ public class MeshExporter { return type switch { + MdlFile.VertexType.Single2 => new Vector2(reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), @@ -379,6 +380,7 @@ public class MeshExporter { MdlFile.VertexType.Half2 => 1, MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single2 => 1, MdlFile.VertexType.Single4 => 2, _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), }; From dbfaf37800f20c202f8d01a6dacf6348ecdb7a9f Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 18 Apr 2024 21:47:07 +1000 Subject: [PATCH 1626/2451] Export to .glb --- Penumbra/Import/Models/ModelManager.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 1a52c4dd..485a76a7 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -213,7 +213,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Penumbra.Log.Debug("[GLTF Export] Saving..."); var gltfModel = scene.ToGltf2(); - gltfModel.SaveGLTF(outputPath); + gltfModel.Save(outputPath); Penumbra.Log.Debug("[GLTF Export] Done."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03f276ea..6cd9b912 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -145,8 +145,8 @@ public partial class ModEditWindow if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo || gamePath.IsEmpty)) - _fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), - ".gltf", (valid, path) => + _fileDialog.OpenSavePicker("Save model as glTF.", ".glb", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), + ".glb", (valid, path) => { if (!valid) return; From aeb7bd5431d0e3822010305ebeeb87de5b52604b Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Apr 2024 00:34:08 +1000 Subject: [PATCH 1627/2451] Ensure materials contain at least one / --- .../ModEditWindow.Models.MdlTab.cs | 13 ++++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 54 +++++++++++++------ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cca8fe10..b8c0176a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -49,7 +49,7 @@ public partial class ModEditWindow /// public bool Valid - => Mdl.Valid; + => Mdl.Valid && Mdl.Materials.All(ValidateMaterial); /// public byte[] Write() @@ -285,6 +285,17 @@ public partial class ModEditWindow : _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data; } + /// Validate the specified material. + /// + /// While materials can be relative (`/mt_...`) or absolute (`bg/...`), + /// they invariably must contain at least one directory seperator. + /// Missing this can lead to a crash. + /// + public bool ValidateMaterial(string material) + { + return material.Contains('/'); + } + /// Remove the material given by the index. /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 6cd9b912..1cfa7585 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Custom; @@ -295,7 +296,7 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Materials")) return false; - using var table = ImRaii.Table(string.Empty, disabled ? 2 : 3, ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table(string.Empty, disabled ? 2 : 4, ImGuiTableFlags.SizingFixedFit); if (!table) return false; @@ -305,7 +306,10 @@ public partial class ModEditWindow ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); if (!disabled) + { ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + } var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) @@ -321,12 +325,15 @@ public partial class ModEditWindow ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; ImGui.TableNextColumn(); - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) - return ret; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) + { + ret |= true; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + } + ImGui.TableNextColumn(); - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; - return true; + return ret; } private bool DrawMaterialRow(MdlTab tab, bool disabled, string[] materials, int materialIndex, ImGuiInputTextFlags inputFlags) @@ -353,20 +360,33 @@ public partial class ModEditWindow return ret; ImGui.TableNextColumn(); - // Need to have at least one material. - if (materials.Length <= 1) - return ret; + if (materials.Length > 1) + { + var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; + var modifierActive = _config.DeleteModModifier.IsActive(); + if (!modifierActive) + tt += $"\nHold {_config.DeleteModModifier} to delete."; - var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; - var modifierActive = _config.DeleteModModifier.IsActive(); - if (!modifierActive) - tt += $"\nHold {_config.DeleteModModifier} to delete."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) - return ret; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) + { + tab.RemoveMaterial(materialIndex); + ret |= true; + } + } - tab.RemoveMaterial(materialIndex); - return true; + ImGui.TableNextColumn(); + // Add markers to invalid materials. + if (!tab.ValidateMaterial(temp)) + using (var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true)) + { + ImGuiComponents.HelpMarker( + "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + + "or absolute (e.g. \"chara/full/path/to/filename.mtrl\").", + FontAwesomeIcon.TimesCircle); + } + + return ret; } private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) From ef1bbb6d9d8443791a0c4d534bf373f0e490e966 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:40:12 +0200 Subject: [PATCH 1628/2451] I don't know what I'm doing --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 3 +- Penumbra/Import/Models/Export/MeshExporter.cs | 19 +++++----- Penumbra/Import/Models/Import/MeshImporter.cs | 13 ++++--- .../Import/Models/Import/ModelImporter.cs | 34 +++++++++--------- .../LiveColorTablePreviewer.cs | 7 ++-- .../ModEditWindow.Materials.ColorTable.cs | 35 ++++++++++--------- .../ModEditWindow.Materials.MtrlTab.cs | 9 ++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 9 files changed, 65 insertions(+), 59 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fe9d563d..845d1f99 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 +Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 2fa4e1b2..73a5e725 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using SharpGLTF.Materials; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; @@ -102,7 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation + private readonly struct ProcessCharacterNormalOperation(Image normal, ColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index d3ca87dc..f372f665 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -3,6 +3,7 @@ using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.IO; @@ -55,7 +56,7 @@ public class MeshExporter private readonly byte _lod; private readonly ushort _meshIndex; - private MdlStructs.MeshStruct XivMesh + private MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; private readonly MaterialBuilder _material; @@ -109,8 +110,8 @@ public class MeshExporter var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); - - foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) + // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) @@ -238,19 +239,15 @@ public class MeshExporter { "targetNames", shapeNames }, }); - string[] attributes = []; - var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); + string[] attributes = []; + var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); if (maxAttribute < _mdl.Attributes.Length) - { attributes = Enumerable.Range(0, 32) .Where(index => ((attributeMask >> index) & 1) == 1) .Select(index => _mdl.Attributes[index]) .ToArray(); - } else - { _notifier.Warning("Invalid attribute data, ignoring."); - } return new MeshData { @@ -278,7 +275,7 @@ public class MeshExporter for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) { streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset(streamIndex)); } var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements @@ -315,7 +312,7 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1d4b223d..3a11cb04 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using OtterGui; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -8,7 +9,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { public struct Mesh { - public MdlStructs.MeshStruct MeshStruct; + public MeshStruct MeshStruct; public List SubMeshStructs; public string? Material; @@ -69,10 +70,14 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) return new Mesh { - MeshStruct = new MdlStructs.MeshStruct + MeshStruct = new MeshStruct { - VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], - VertexBufferStride = _strides, + VertexBufferOffset1 = 0, + VertexBufferOffset2 = (uint)_streams[0].Count, + VertexBufferOffset3 = (uint)(_streams[0].Count + _streams[1].Count), + VertexBufferStride1 = _strides[0], + VertexBufferStride2 = _strides[1], + VertexBufferStride3 = _strides[2], VertexCount = _vertexCount, VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements .Select(element => element.Stream + 1) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 8f917b0e..eedd12ab 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -14,10 +15,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) } // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", + RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex MeshNameGroupingRegex(); - private readonly List _meshes = []; + private readonly List _meshes = []; private readonly List _subMeshes = []; private readonly List _materials = []; @@ -27,10 +29,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private readonly List _indices = []; - private readonly List _bones = []; - private readonly List _boneTables = []; + private readonly List _bones = []; + private readonly List _boneTables = []; - private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly BoundingBox _boundingBox = new(); private readonly List _metaAttributes = []; @@ -95,9 +97,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) IndexBufferSize = (uint)indexBuffer.Length, }, ], - - Materials = [.. materials], - + Materials = [.. materials], BoundingBoxes = _boundingBox.ToStruct(), // TODO: Would be good to calculate all of this up the tree. @@ -132,9 +132,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private void BuildMeshForGroup(IEnumerable subMeshNodes, int index) { // Record some offsets we'll be using later, before they get mutated with mesh values. - var subMeshOffset = _subMeshes.Count; - var vertexOffset = _vertexBuffer.Count; - var indexOffset = _indices.Count; + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); @@ -154,9 +154,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), BoneTableIndex = boneTableIndex, StartIndex = meshStartIndex, - VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset - .Select(offset => (uint)(offset + vertexOffset)) - .ToArray(), + VertexBufferOffset1 = (uint)(mesh.MeshStruct.VertexBufferOffset1 + vertexOffset), + VertexBufferOffset2 = (uint)(mesh.MeshStruct.VertexBufferOffset2 + vertexOffset), + VertexBufferOffset3 = (uint)(mesh.MeshStruct.VertexBufferOffset3 + vertexOffset), }); _boundingBox.Merge(mesh.BoundingBox); @@ -196,7 +196,8 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) // arrays, values is practically guaranteed to be the highest of the // group, so a failure on any of them will be a failure on it. if (_shapeValues.Count > ushort.MaxValue) - throw notifier.Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); + throw notifier.Exception( + $"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort GetMaterialIndex(string materialName) @@ -216,6 +217,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) return (ushort)count; } + // #TODO @ackwell fix for V6 Models private ushort BuildBoneTable(List boneNames) { var boneIndices = new List(); @@ -238,7 +240,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); var boneTableIndex = _boneTables.Count; - _boneTables.Add(new MdlStructs.BoneTableStruct() + _boneTables.Add(new BoneTableStruct() { BoneIndex = boneIndicesArray, BoneCount = (byte)boneIndices.Count, diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 4d35e68a..f211e0bc 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -9,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = MtrlFile.ColorTable.NumRows; + public const int TextureHeight = GameData.Files.MaterialStructs.ColorTable.NumUsedRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly IFramework _framework; @@ -17,7 +16,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private readonly Texture** _colorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture; - private bool _updatePending; + private bool _updatePending; public Half[] ColorTable { get; } @@ -40,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + ColorTable = new Half[TextureLength]; _updatePending = true; framework.Update += OnFrameworkUpdate; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index a4e25f77..54c0eff6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Functions; namespace Penumbra.UI.AdvancedWindow; @@ -74,7 +75,7 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ret |= DrawColorTableRow(tab, i, disabled); ImGui.TableNextRow(); @@ -115,8 +116,8 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); + for (var i = 0; i < ColorTable.NumUsedRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); tab.UpdateColorTablePreview(); @@ -140,21 +141,21 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) + if (data.Length < Marshal.SizeOf()) return false; ref var rows = ref tab.Mtrl.Table; fixed (void* ptr = data, output = &rows) { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() && tab.Mtrl.HasDyeTable) { ref var dyeRows = ref tab.Mtrl.DyeTable; fixed (void* output2 = &dyeRows) { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } @@ -169,7 +170,7 @@ public partial class ModEditWindow } } - private static unsafe void ColorTableCopyClipboardButton(MtrlFile.ColorTable.Row row, MtrlFile.ColorDyeTable.Row dye) + private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -177,11 +178,11 @@ public partial class ModEditWindow try { - var data = new byte[MtrlFile.ColorTable.Row.Size + 2]; + var data = new byte[ColorTable.Row.Size + 2]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorTable.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2); } var text = Convert.ToBase64String(data); @@ -217,15 +218,15 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != MtrlFile.ColorTable.Row.Size + 2 + if (data.Length != ColorTable.Row.Size + 2 || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.Table[rowIdx] = *(MtrlFile.ColorTable.Row*)ptr; + tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(MtrlFile.ColorDyeTable.Row*)(ptr + MtrlFile.ColorTable.Row.Size); + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); } tab.UpdateColorTableRowPreview(rowIdx); @@ -451,7 +452,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, MtrlFile.ColorDyeTable.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) @@ -463,7 +464,7 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); if (ret) tab.UpdateColorTableRowPreview(rowIdx); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index b4801f5f..9421493e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.MaterialPreview; @@ -601,7 +602,7 @@ public partial class ModEditWindow var stm = _edit._stainService.StmFile; var dye = Mtrl.DyeTable[rowIdx]; if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } if (HighlightedColorTableRow == rowIdx) @@ -628,12 +629,12 @@ public partial class ModEditWindow { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ref var row = ref rows[i]; var dye = Mtrl.DyeTable[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } } @@ -647,7 +648,7 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref MtrlFile.ColorTable.Row row, float time) + private static void ApplyHighlight(ref ColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03f276ea..80b1a5d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -483,7 +483,7 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn(_lastFile.Version.ToString()); + ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}"); ImGuiUtil.DrawTableColumn("Radius"); ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); From 624dd40d58bc3817c3fbd271564042c58ad72439 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:46:52 +0200 Subject: [PATCH 1629/2451] Handle writing. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 845d1f99..aff136e2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f +Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 From 75cfffeba73e80f970d617e7d36622e709e444c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:51:23 +0200 Subject: [PATCH 1630/2451] Oops. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index aff136e2..9208c9c2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 +Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af From ceb3d39a9ac71373acc3e2d33623876488c872e8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 01:22:03 +1000 Subject: [PATCH 1631/2451] Normalise _FFXIV_COLOR values Fixes xivdev/Penumbra#411 --- Penumbra/Import/Models/Export/VertexFragment.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 08b2a214..7a82e994 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -12,7 +12,7 @@ and there's reason to overhaul the export pipeline. public struct VertexColorFfxiv : IVertexCustom { // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -81,7 +81,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_0")] public Vector2 TexCoord0; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -163,7 +163,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; From 8fc7de64d9d23c9874861567dd6ada6a0f246d57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 17:55:28 +0200 Subject: [PATCH 1632/2451] Start group rework. --- Penumbra/Collections/Cache/CollectionCache.cs | 50 +++++-------------- .../Collections/Cache/CollectionModData.cs | 10 ++-- Penumbra/Mods/Editor/IMod.cs | 14 +++++- Penumbra/Mods/Mod.cs | 19 +++++++ Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++ Penumbra/Mods/Subclasses/ISubMod.cs | 10 ++++ Penumbra/Mods/Subclasses/MultiModGroup.cs | 11 ++++ Penumbra/Mods/Subclasses/SingleModGroup.cs | 5 ++ Penumbra/Mods/TemporaryMod.cs | 28 ++++++++++- 9 files changed, 106 insertions(+), 45 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e1b32204..ded1dc73 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -2,12 +2,10 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; namespace Penumbra.Collections.Cache; @@ -231,37 +229,12 @@ public sealed class CollectionCache : IDisposable /// Add all files and possibly manipulations of a given mod according to its settings in this collection. internal void AddModSync(IMod mod, bool addMetaChanges) { - if (mod.Index >= 0) - { - var settings = _collection[mod.Index].Settings; - if (settings is not { Enabled: true }) - return; + var files = GetFiles(mod); + foreach (var (path, file) in files.FileRedirections) + AddFile(path, file, mod); - foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Count == 0) - continue; - - var config = settings.Settings[groupIndex]; - switch (group) - { - case SingleModGroup single: - AddSubMod(single[config.AsIndex], mod); - break; - case MultiModGroup multi: - { - foreach (var (option, _) in multi.WithIndex() - .Where(p => config.HasFlag(p.Index)) - .OrderByDescending(p => group.OptionPriority(p.Index))) - AddSubMod(option, mod); - - break; - } - } - } - } - - AddSubMod(mod.Default, mod); + foreach (var manip in files.Manipulations) + AddManipulation(manip, mod); if (addMetaChanges) { @@ -273,14 +246,15 @@ public sealed class CollectionCache : IDisposable } } - // Add all files and possibly manipulations of a specific submod - private void AddSubMod(ISubMod subMod, IMod parentMod) + private AppliedModData GetFiles(IMod mod) { - foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) - AddFile(path, file, parentMod); + if (mod.Index < 0) + return mod.GetData(); - foreach (var manip in subMod.Manipulations) - AddManipulation(manip, parentMod); + var settings = _collection[mod.Index].Settings; + return settings is not { Enabled: true } + ? AppliedModData.Empty + : mod.GetData(settings); } /// Invoke only if not in a full recalculation. diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index 3a3afad2..d0a3bc76 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -1,10 +1,12 @@ using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; +/// +/// Contains associations between a mod and the paths and meta manipulations affected by that mod. +/// public class CollectionModData { private readonly Dictionary, HashSet)> _data = new(); @@ -17,7 +19,7 @@ public class CollectionModData if (_data.Remove(mod, out var data)) return data; - return (Array.Empty(), Array.Empty()); + return ([], []); } public void AddPath(IMod mod, Utf8GamePath path) @@ -28,7 +30,7 @@ public class CollectionModData } else { - data = (new HashSet { path }, new HashSet()); + data = ([path], []); _data.Add(mod, data); } } @@ -41,7 +43,7 @@ public class CollectionModData } else { - data = (new HashSet(), new HashSet { manipulation }); + data = ([], [manipulation]); _data.Add(mod, data); } } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d3bc19b0..8b5b65e1 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,15 +1,27 @@ using OtterGui.Classes; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Subclasses; +using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; +public record struct AppliedModData( + IReadOnlyCollection> FileRedirections, + IReadOnlyCollection Manipulations) +{ + public static readonly AppliedModData Empty = new([], []); +} + public interface IMod { LowerString Name { get; } - public int Index { get; } + public int Index { get; } public ModPriority Priority { get; } + public AppliedModData GetData(ModSettings? settings = null); + + public ISubMod Default { get; } public IReadOnlyList Groups { get; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index b7d1186d..3c996c8f 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,7 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.Collections.Cache; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -59,6 +61,23 @@ public sealed class Mod : IMod public readonly SubMod Default; public readonly List Groups = []; + public AppliedModData GetData(ModSettings? settings = null) + { + if (settings is not { Enabled: true }) + return AppliedModData.Empty; + + var dictRedirections = new Dictionary(TotalFileCount); + var setManips = new HashSet(TotalManipulations); + foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) + { + var config = settings.Settings[groupIndex]; + group.AddData(config, dictRedirections, setManips); + } + + ((ISubMod)Default).AddData(dictRedirections, setManips); + return new AppliedModData(dictRedirections, setManips); + } + ISubMod IMod.Default => Default; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 2daf31e6..57ef4e98 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -24,6 +26,8 @@ public interface IModGroup : IReadOnlyCollection public bool MoveOption(int optionIdxFrom, int optionIdxTo); public void UpdatePositions(int from = 0); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 29323c1d..e997e07d 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -14,6 +14,16 @@ public interface ISubMod public IReadOnlyDictionary FileSwaps { get; } public IReadOnlySet Manipulations { get; } + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + public bool IsDefault { get; } public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 7479cd54..266d3037 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -5,6 +5,8 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -110,6 +112,15 @@ public sealed class MultiModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + { + if (setting.HasFlag(index)) + ((ISubMod)option.Mod).AddData(redirections, manipulations); + } + } + public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << Count) - 1)); } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 74769c7e..f797a709 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -3,6 +3,8 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -114,6 +116,9 @@ public sealed class SingleModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + => this[setting.AsIndex].AddData(redirections, manipulations); + public Setting FixSetting(Setting setting) => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6be07881..8f27e201 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -20,6 +20,28 @@ public class TemporaryMod : IMod public readonly SubMod Default; + public AppliedModData GetData(ModSettings? settings = null) + { + Dictionary dict; + if (Default.FileSwapData.Count == 0) + { + dict = Default.FileData; + } + else if (Default.FileData.Count == 0) + { + dict = Default.FileSwapData; + } + else + { + // Need to ensure uniqueness. + dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict.TryAdd(gamePath, file); + } + + return new AppliedModData(dict, Default.Manipulations); + } + ISubMod IMod.Default => Default; @@ -53,7 +75,8 @@ public class TemporaryMod : IMod dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null); + $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", + null, null); var mod = new Mod(dir); var defaultMod = mod.Default; foreach (var (gamePath, fullPath) in collection.ResolvedFiles) @@ -86,7 +109,8 @@ public class TemporaryMod : IMod saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); - Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); + Penumbra.Log.Information( + $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } catch (Exception e) { From 9f4c6767f822be23632b39e3ab73792d19290ec3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 18:28:25 +0200 Subject: [PATCH 1633/2451] Remove ISubMod. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 7 +- Penumbra/Mods/Editor/FileRegistry.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 12 ++-- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModFileCollection.cs | 14 ++-- Penumbra/Mods/Editor/ModFileEditor.cs | 16 ++--- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 15 ++--- Penumbra/Mods/Manager/ModOptionEditor.cs | 15 ++--- Penumbra/Mods/Mod.cs | 13 ++-- Penumbra/Mods/ModCreator.cs | 8 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 12 ++-- Penumbra/Mods/Subclasses/ISubMod.cs | 67 ------------------- Penumbra/Mods/Subclasses/ModSettings.cs | 35 +--------- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 +- Penumbra/Mods/Subclasses/SingleModGroup.cs | 4 +- Penumbra/Mods/Subclasses/SubMod.cs | 58 ++++++++++++++-- Penumbra/Mods/TemporaryMod.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 4 +- .../ModEditWindow.QuickImport.cs | 2 +- 23 files changed, 123 insertions(+), 184 deletions(-) delete mode 100644 Penumbra/Mods/Subclasses/ISubMod.cs diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 099b133c..f4b7d47e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index c8530936..938199aa 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,8 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - var sub = (SubMod)subMod; - sub.FileData = dict; + subMod.FileData = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 96d027b3..427c58ca 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,7 +5,7 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = []; + public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; public FullPath File { get; private init; } public Utf8RelPath RelPath { get; private init; } public long FileSize { get; private init; } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 8b5b65e1..c4c4be2f 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -6,8 +6,8 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; public record struct AppliedModData( - IReadOnlyCollection> FileRedirections, - IReadOnlyCollection Manipulations) + Dictionary FileRedirections, + HashSet Manipulations) { public static readonly AppliedModData Empty = new([], []); } @@ -19,14 +19,10 @@ public interface IMod public int Index { get; } public ModPriority Priority { get; } + public IReadOnlyList Groups { get; } + public AppliedModData GetData(ModSettings? settings = null); - - public ISubMod Default { get; } - public IReadOnlyList Groups { get; } - - public IEnumerable AllSubMods { get; } - // Cache public int TotalManipulations { get; } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index b22aea17..d9781c06 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,7 +29,7 @@ public class ModEditor( public int OptionIdx { get; private set; } public IModGroup? Group { get; private set; } - public ISubMod? Option { get; private set; } + public SubMod? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); @@ -104,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 2f8bdfb1..9dd78217 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, ISubMod option) + public void UpdateAll(Mod mod, SubMod option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, ISubMod option) + public void UpdatePaths(Mod mod, SubMod option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,7 +154,7 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, ISubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 30e97093..4bdf4b1b 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -30,16 +30,16 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu return num; } - public void Revert(Mod mod, ISubMod option) + public void Revert(Mod mod, SubMod option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, ISubMod option) + public void RemoveMissingPaths(Mod mod, SubMod option) { - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -61,7 +61,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -84,7 +84,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(ISubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -111,7 +111,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(ISubMod option, IEnumerable files1) + public void RemovePathsFromSelected(SubMod option, IEnumerable files1) { foreach (var file in files1) { @@ -129,7 +129,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -155,7 +155,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 842b1bb3..25590c49 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -151,7 +151,7 @@ public class ModMerger : IDisposable MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) { var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 31aefdf5..a6218c6f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, ISubMod currentOption) + public void Load(Mod mod, SubMod currentOption) { OtherImcCount = 0; OtherEqpCount = 0; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index ada06264..0d5f05a9 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(ISubMod option) + public void Revert(SubMod option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index e229738d..21b9ef2c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -15,14 +16,13 @@ public class ItemSwapContainer private readonly MetaFileManager _manager; private readonly ObjectIdentification _identifier; - private Dictionary _modRedirections = []; - private HashSet _modManipulations = []; + private AppliedModData _appliedModData = AppliedModData.Empty; public IReadOnlyDictionary ModRedirections - => _modRedirections; + => _appliedModData.FileRedirections; public IReadOnlySet ModManipulations - => _modManipulations; + => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -97,12 +97,11 @@ public class ItemSwapContainer Clear(); if (mod == null || mod.Index < 0) { - _modRedirections = []; - _modManipulations = []; + _appliedModData = AppliedModData.Empty; } else { - (_modRedirections, _modManipulations) = ModSettings.GetResolveData(mod, settings); + _appliedModData = ModSettings.GetResolveData(mod, settings); } } @@ -120,7 +119,7 @@ public class ItemSwapContainer private Func MetaResolver(ModCollection? collection) { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _modManipulations; + var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; return m => set.TryGetValue(m, out var a) ? a : m; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9efb8a3f..07c6f38e 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -262,15 +262,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option) + public void AddOption(Mod mod, int groupIdx, SubMod option) => AddOption(mod, groupIdx, option, ModPriority.Default); /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option, ModPriority priority) + public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { - if (option is not SubMod o) - return; - var group = mod.Groups[groupIdx]; switch (group) { @@ -280,12 +277,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - o.SetPosition(groupIdx, s.Count); - s.OptionData.Add(o); + option.SetPosition(groupIdx, s.Count); + s.OptionData.Add(option); break; case MultiModGroup m: - o.SetPosition(groupIdx, m.Count); - m.PrioritizedOptions.Add((o, priority)); + option.SetPosition(groupIdx, m.Count); + m.PrioritizedOptions.Add((option, priority)); break; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 3c996c8f..25f3c510 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -32,6 +32,9 @@ public sealed class Mod : IMod public ModPriority Priority => ModPriority.Default; + IReadOnlyList IMod.Groups + => Groups; + internal Mod(DirectoryInfo modPath) { ModPath = modPath; @@ -74,18 +77,12 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - ((ISubMod)Default).AddData(dictRedirections, setManips); + Default.AddData(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - ISubMod IMod.Default - => Default; - - IReadOnlyList IMod.Groups - => Groups; - public IEnumerable AllSubMods - => Groups.SelectMany(o => o).OfType().Prepend(Default); + => Groups.SelectMany(o => o).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 2bcdd3b1..661dd6fb 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, new ModPriority(idx)))); + group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -269,7 +269,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create the data for a given sub mod from its data and the folder it is based on. - public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) @@ -288,7 +288,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod(string name) + internal static SubMod CreateEmptySubMod(string name) => new SubMod(null!) // Mod is irrelevant here, only used for saving. { Name = name, diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 57ef4e98..3f363542 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,7 +6,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup : IReadOnlyCollection { public const int MaxMultiOptions = 63; @@ -18,7 +18,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority OptionPriority(Index optionIdx); - public ISubMod this[Index idx] { get; } + public SubMod this[Index idx] { get; } public bool IsOption { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly ISubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) @@ -59,7 +59,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, ISubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -91,7 +91,7 @@ public readonly struct ModSaveGroup : ISavable j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) { - ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch + SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch { GroupType.Multi => _group.OptionPriority(idx), _ => null, @@ -103,7 +103,7 @@ public readonly struct ModSaveGroup : ISavable } else { - ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); + SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); } } } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs deleted file mode 100644 index e997e07d..00000000 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Subclasses; - -public interface ISubMod -{ - public string Name { get; } - public string FullName { get; } - public string Description { get; } - - public IReadOnlyDictionary Files { get; } - public IReadOnlyDictionary FileSwaps { get; } - public IReadOnlySet Manipulations { get; } - - public void AddData(Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); - } - - public bool IsDefault { get; } - - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 380b242c..81a3bb41 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -2,6 +2,7 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; @@ -34,44 +35,14 @@ public class ModSettings }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. - public static (Dictionary, HashSet) GetResolveData(Mod mod, ModSettings? settings) + public static AppliedModData GetResolveData(Mod mod, ModSettings? settings) { if (settings == null) settings = DefaultSettings(mod); else settings.Settings.FixSize(mod); - var dict = new Dictionary(); - var set = new HashSet(); - - foreach (var (group, index) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Type is GroupType.Single) - { - if (group.Count > 0) - AddOption(group[settings.Settings[index].AsIndex]); - } - else - { - foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) - { - if (settings.Settings[index].HasFlag(optionIdx)) - AddOption(option); - } - } - } - - AddOption(mod.Default); - return (dict, set); - - void AddOption(ISubMod option) - { - foreach (var (path, file) in option.Files.Concat(option.FileSwaps)) - dict.TryAdd(path, file); - - foreach (var manip in option.Manipulations) - set.Add(manip); - } + return mod.GetData(settings); } // Automatically react to changes in a mods available options. diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 266d3037..1600072e 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -24,7 +24,7 @@ public sealed class MultiModGroup : IModGroup public ModPriority OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; public bool IsOption @@ -36,7 +36,7 @@ public sealed class MultiModGroup : IModGroup public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -117,7 +117,7 @@ public sealed class MultiModGroup : IModGroup foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - ((ISubMod)option.Mod).AddData(redirections, manipulations); + option.Mod.AddData(redirections, manipulations); } } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index f797a709..2d49fd1f 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -24,7 +24,7 @@ public sealed class SingleModGroup : IModGroup public ModPriority OptionPriority(Index _) => Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => OptionData[idx]; public bool IsOption @@ -34,7 +34,7 @@ public sealed class SingleModGroup : IModGroup public int Count => OptionData.Count; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => OptionData.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 4f35cd33..386910e5 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -15,7 +16,7 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod : ISubMod +public sealed class SubMod { public string Name { get; set; } = "Default"; @@ -29,7 +30,17 @@ public sealed class SubMod : ISubMod internal int OptionIdx { get; private set; } public bool IsDefault - => GroupIdx < 0; + => GroupIdx < 0; + + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } public Dictionary FileData = []; public Dictionary FileSwapData = []; @@ -60,8 +71,8 @@ public sealed class SubMod : ISubMod ManipulationData.Clear(); // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; + Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + Description = json[nameof(Description)]?.ToObject() ?? string.Empty; priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; var files = (JObject?)json[nameof(Files)]; @@ -104,4 +115,43 @@ public sealed class SubMod : ISubMod } } } + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) + { + j.WriteStartObject(); + j.WritePropertyName(nameof(Name)); + j.WriteValue(mod.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(mod.Description); + if (priority != null) + { + j.WritePropertyName(nameof(IModGroup.Priority)); + j.WriteValue(priority.Value.Value); + } + + j.WritePropertyName(nameof(mod.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.Manipulations)); + serializer.Serialize(j, mod.Manipulations); + j.WriteEndObject(); + } } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 8f27e201..41c1211f 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -39,12 +39,9 @@ public class TemporaryMod : IMod dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.Manipulations); + return new AppliedModData(dict, Default.ManipulationData); } - ISubMod IMod.Default - => Default; - public IReadOnlyList Groups => Array.Empty(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index c8db7770..f765b47e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -192,7 +192,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +228,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, ISubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 10956deb..4ecacece 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -227,7 +227,7 @@ public partial class ModEditWindow return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; From 2d5afde61274f3602f15252eb64efbcb899c5cae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:02:30 +0200 Subject: [PATCH 1634/2451] Fix group priority writing. --- Penumbra/Mods/Subclasses/IModGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 3f363542..7554f6dc 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -82,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName(nameof(_group.Description)); j.WriteValue(_group.Description); j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority); + j.WriteValue(_group.Priority.Value); j.WritePropertyName(nameof(Type)); j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); From f86f29b44a4d3dc78929107a9c99538b0d314d6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:03:50 +0200 Subject: [PATCH 1635/2451] Some fixes. --- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 33 ++++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 07c6f38e..9d942574 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -158,10 +158,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { var group = mod.Groups[groupIdx]; var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) + if (option.Description == newDescription) return; - s.Description = newDescription; + option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 7554f6dc..38f070b3 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -72,8 +72,9 @@ public readonly struct ModSaveGroup : ISavable public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; if (_groupIdx >= 0) { j.WriteStartObject(); @@ -87,19 +88,25 @@ public readonly struct ModSaveGroup : ISavable j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); j.WriteValue(_group.DefaultSettings.Value); - j.WritePropertyName("Options"); - j.WriteStartArray(); - for (var idx = 0; idx < _group.Count; ++idx) + switch (_group) { - SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch - { - GroupType.Multi => _group.OptionPriority(idx), - _ => null, - }); + case SingleModGroup single: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var option in single.OptionData) + SubMod.WriteSubMod(j, serializer, option, _basePath, null); + j.WriteEndArray(); + j.WriteEndObject(); + break; + case MultiModGroup multi: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var (option, priority) in multi.PrioritizedOptions) + SubMod.WriteSubMod(j, serializer, option, _basePath, priority); + j.WriteEndArray(); + j.WriteEndObject(); + break; } - - j.WriteEndArray(); - j.WriteEndObject(); } else { From b99a809eba50019fad9aa7e1b25ce73c5ebca6fa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:26:12 +0200 Subject: [PATCH 1636/2451] Remove OptionPriority from general option groups. --- Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 ++++-- Penumbra/Mods/Subclasses/SingleModGroup.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 13 ++++++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 10 +++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 38f070b3..96d7c6b7 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -16,7 +16,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority Priority { get; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index optionIdx); + public FullPath? FindBestMatch(Utf8GamePath gamePath); public SubMod this[Index idx] { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly SubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 1600072e..02ae07f4 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -21,8 +21,10 @@ public sealed class MultiModGroup : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index idx) - => PrioritizedOptions[idx].Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => PrioritizedOptions.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 2d49fd1f..b854d2b1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -21,8 +21,10 @@ public sealed class SingleModGroup : IModGroup public readonly List OptionData = []; - public ModPriority OptionPriority(Index _) - => Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => OptionData + .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => OptionData[idx]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6cf24f62..a70da628 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -540,14 +540,17 @@ public partial class ModEditWindow : Window, IDisposable return currentFile.Value; if (Mod != null) - foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority) - .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) - .Append(Mod.Default)) + { + foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority)) { - if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) - return value; + if (option.FindBestMatch(path) is { } fullPath) + return fullPath; } + if (Mod.Default.Files.TryGetValue(path, out var value) || Mod.Default.FileSwaps.TryGetValue(path, out value)) + return value; + } + return new FullPath(path); } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 80af7b15..b002dedd 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -532,10 +532,10 @@ public class ModPanelEditTab( panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); - if (group.Type != GroupType.Multi) + if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -613,7 +613,11 @@ public class ModPanelEditTab( var sourceGroup = panel._mod.Groups[sourceGroupIdx]; var currentCount = group.Count; var option = sourceGroup[sourceOption]; - var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); + var priority = sourceGroup switch + { + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, + _ => ModPriority.Default, + }; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); From 4a6d94f0fb817810d14a9920130db7bada5ff47c Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:02:43 +1000 Subject: [PATCH 1637/2451] Avoid inclusion of zero-weighted bones in name mapping --- Penumbra/Import/Models/Import/MeshImporter.cs | 18 ++++++++++++++---- .../Import/Models/Import/VertexAttribute.cs | 19 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1d4b223d..5d5df948 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -189,15 +189,25 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + + if (jointsAccessor == null || weightsAccessor == null) + throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. - foreach (var joints in jointsAccessor.AsVector4Array()) + for (var i = 0; i < jointsAccessor.Count; i++) { + var joints = jointsAccessor[i]; + var weights = weightsAccessor[i]; for (var index = 0; index < 4; index++) + { + // If a joint has absolutely no weight, we omit the bone entirely. + if (weights[index] == 0) + continue; + usedJoints.Add((ushort)joints[index]); + } } } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 3cfedd6f..b7f5dcf1 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -144,10 +144,10 @@ public class VertexAttribute public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) return null; - if (!accessors.ContainsKey("WEIGHTS_0")) + if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) @@ -160,18 +160,21 @@ public class VertexAttribute Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; - var values = accessor.AsVector4Array(); + var joints = jointsAccessor.AsVector4Array(); + var weights = weightsAccessor.AsVector4Array(); return new VertexAttribute( element, index => { - var gltfIndices = values[index]; + var gltfIndices = joints[index]; + var gltfWeights = weights[index]; + return BuildUByte4(new Vector4( - boneMap[(ushort)gltfIndices.X], - boneMap[(ushort)gltfIndices.Y], - boneMap[(ushort)gltfIndices.Z], - boneMap[(ushort)gltfIndices.W] + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] )); } ); From 11acd7d3f46534ce445ef3700849f1f2c706491e Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:53:55 +1000 Subject: [PATCH 1638/2451] Prevent import failure when no materials are present --- Penumbra/Import/Models/Import/PrimitiveImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs index 0c2968df..5df7597e 100644 --- a/Penumbra/Import/Models/Import/PrimitiveImporter.cs +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -65,7 +65,7 @@ public class PrimitiveImporter ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_shapeValues); - var material = _primitive.Material.Name; + var material = _primitive.Material?.Name; if (material == "") material = null; From cc2f72b73df1218c094c45f9cfea3e5fa17ce534 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:55:04 +1000 Subject: [PATCH 1639/2451] Use bg/ for absolute path example --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1cfa7585..1f4607cf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -382,7 +382,7 @@ public partial class ModEditWindow { ImGuiComponents.HelpMarker( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"chara/full/path/to/filename.mtrl\").", + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\").", FontAwesomeIcon.TimesCircle); } From 1bc3bb17c9b8a95cbe6c3edd8cf7912f45b174f2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 22 Apr 2024 23:45:30 +1000 Subject: [PATCH 1640/2451] Fix havok parsing for non-ANSI user paths Also improve parsing because otter is better at c# than me --- Penumbra/Import/Models/HavokConverter.cs | 10 ++++------ Penumbra/Import/Models/SkeletonConverter.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 89f9ac4f..38c8749a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -71,8 +71,7 @@ public static unsafe class HavokConverter /// Path to a file on the filesystem. private static hkResource* Read(string filePath) { - var path = Marshal.StringToHGlobalAnsi(filePath); - + var path = Encoding.UTF8.GetBytes(filePath); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; @@ -81,8 +80,7 @@ public static unsafe class HavokConverter loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); // TODO: probably can use LoadFromBuffer for this. - var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); - return resource; + return hkSerializeUtil.LoadFromFile(path, null, loadOptions); } /// Serializes an hkResource* to a temporary file. @@ -94,9 +92,9 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); + var path = Encoding.UTF8.GetBytes(tempFile); var oStream = new hkOstream(); - oStream.Ctor((byte*)path); + oStream.Ctor(path); var result = stackalloc hkResult[1]; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 7058a159..25e74332 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -84,9 +84,8 @@ public static class SkeletonConverter .Where(n => n.NodeType != XmlNodeType.Comment) .Select(n => { - var text = n.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this I mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + var text = n.InnerText.AsSpan().Trim()[1..]; + return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber)); }) .ToArray(); From c276f922a53fc5798a9ae71673614a54bc88d9df Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Apr 2024 18:22:03 +0200 Subject: [PATCH 1641/2451] Update API. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0c8578cf..590629df 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f +Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a From b34114400fab2f83b119ddb1ac2d8c2ff7e4a708 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:09:53 +0200 Subject: [PATCH 1642/2451] Fix Havok ANSI / UTF8 Issue. --- Penumbra/Import/Models/HavokConverter.cs | 10 ++++------ Penumbra/Import/Models/SkeletonConverter.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 89f9ac4f..dc9d3e6a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -71,8 +71,7 @@ public static unsafe class HavokConverter /// Path to a file on the filesystem. private static hkResource* Read(string filePath) { - var path = Marshal.StringToHGlobalAnsi(filePath); - + var path = Encoding.UTF8.GetBytes(filePath); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; @@ -81,8 +80,7 @@ public static unsafe class HavokConverter loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); // TODO: probably can use LoadFromBuffer for this. - var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); - return resource; + return hkSerializeUtil.LoadFromFile(path, null, loadOptions); } /// Serializes an hkResource* to a temporary file. @@ -94,9 +92,9 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); + var path = Encoding.UTF8.GetBytes(tempFile); var oStream = new hkOstream(); - oStream.Ctor((byte*)path); + oStream.Ctor(path); var result = stackalloc hkResult[1]; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 7058a159..25e74332 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -84,9 +84,8 @@ public static class SkeletonConverter .Where(n => n.NodeType != XmlNodeType.Comment) .Select(n => { - var text = n.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this I mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + var text = n.InnerText.AsSpan().Trim()[1..]; + return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber)); }) .ToArray(); From e21c9fb6d1d71e0f952d416a8d1b0e817e450826 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:11:09 +0200 Subject: [PATCH 1643/2451] Fix some IPC stuff. --- Penumbra/Api/IpcProviders.cs | 5 +++-- Penumbra/Api/IpcTester/UiIpcTester.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 21fe0a7c..ebf71176 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -62,6 +62,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -99,9 +100,9 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui), - IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui), - IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), ]; diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index d95b79b8..a2c36938 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -32,9 +32,9 @@ public class UiIpcTester : IUiService, IDisposable { _pi = pi; PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); - PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod); - PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); PreSettingsTabBar.Disable(); @@ -76,7 +76,7 @@ public class UiIpcTester : IUiService, IDisposable if (!table) return; - IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod"); + IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod"); ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); From 792a04337f31f2c53ff562c3d8116821192fe58e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:50:09 +0200 Subject: [PATCH 1644/2451] Add a try-catch when scanning for mods. --- Penumbra/Mods/Manager/ModManager.cs | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 40585520..d912e292 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -311,22 +312,31 @@ public sealed class ModManager : ModStorage, IDisposable /// private void ScanMods() { - var options = new ParallelOptions() + try { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), - }; - var queue = new ConcurrentQueue(); - Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => - { - var mod = Creator.LoadMod(dir, false); - if (mod != null) - queue.Enqueue(mod); - }); + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Creator.LoadMod(dir, false); + if (mod != null) + queue.Enqueue(mod); + }); - foreach (var mod in queue) + foreach (var mod in queue) + { + mod.Index = Count; + Mods.Add(mod); + } + } + catch (Exception ex) { - mod.Index = Count; - Mods.Add(mod); + Valid = false; + _communicator.ModDirectoryChanged.Invoke(BasePath.FullName, false); + Penumbra.Log.Error($"Could not scan for mods:\n{ex}"); } } } From 07afbfb22974c4ca49d76bce109ff0a301d17b60 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 17:41:55 +0200 Subject: [PATCH 1645/2451] Rework options, pre-submod types. --- Penumbra/Api/Api/ModSettingsApi.cs | 10 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Meta/MetaFileManager.cs | 8 +- Penumbra/Mods/Editor/ModEditor.cs | 30 +++- Penumbra/Mods/Editor/ModFileEditor.cs | 3 +- Penumbra/Mods/Editor/ModMerger.cs | 38 ++++-- Penumbra/Mods/Editor/ModNormalizer.cs | 72 +++++++--- Penumbra/Mods/Manager/ModCacheManager.cs | 10 +- Penumbra/Mods/Manager/ModMigration.cs | 12 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 128 ++++++++---------- Penumbra/Mods/Mod.cs | 9 +- Penumbra/Mods/ModCreator.cs | 45 +++--- Penumbra/Mods/Subclasses/IModDataContainer.cs | 80 +++++++++++ Penumbra/Mods/Subclasses/IModGroup.cs | 14 +- Penumbra/Mods/Subclasses/IModOption.cs | 25 ++++ Penumbra/Mods/Subclasses/ModSettings.cs | 18 +-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 86 ++++++++---- Penumbra/Mods/Subclasses/SingleModGroup.cs | 82 +++++++---- Penumbra/Mods/Subclasses/SubMod.cs | 110 +++++++++++++-- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 17 ++- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 10 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 20 ++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 50 ++++--- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 39 +++--- 25 files changed, 620 insertions(+), 300 deletions(-) create mode 100644 Penumbra/Mods/Subclasses/IModDataContainer.cs create mode 100644 Penumbra/Mods/Subclasses/IModOption.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 2604a49d..5ed26ce5 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -56,13 +56,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var dict = new Dictionary(mod.Groups.Count); foreach (var g in mod.Groups) - dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); + dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)); return new AvailableModSettings(dict); } public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type)) + ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)) : null; public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, @@ -153,7 +153,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (groupIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -190,7 +190,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { case SingleModGroup single: { - var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); + var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.IndexOf(o => o.Name == name); + var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index f4b7d47e..7d9388a9 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); + options.Insert(idx, SubMod.CreateForSaving(option.Name)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 5283f77e..b1823bd7 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -54,7 +54,13 @@ public unsafe class MetaFileManager if (!dir.Exists) dir.Create(); - foreach (var option in group.OfType()) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + foreach (var option in optionEnumerator) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index d9781c06..0a96e0fd 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,3 +1,4 @@ +using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -72,12 +73,18 @@ public class ModEditor( if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - if (optionIdx >= 0 && optionIdx < Group.Count) + switch(Group) { - Option = Group[optionIdx]; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; + case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: + Option = single.OptionData[optionIdx]; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; + case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count: + Option = multi.PrioritizedOptions[optionIdx].Mod; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; } } } @@ -109,8 +116,17 @@ public class ModEditor( action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) { - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - action(group[optionIdx], groupIdx, optionIdx); + switch (group) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + action(single.OptionData[optionIdx], groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); + break; + } } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 4bdf4b1b..51615b05 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,7 +24,8 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + var (groupIdx, optionIdx) = option.GetIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 25590c49..74e9007c 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; +using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -104,9 +105,16 @@ public class ModMerger : IDisposable ((List)Warnings).Add( $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); - foreach (var originalOption in originalGroup) + var optionEnumerator = group switch { - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var originalOption in optionEnumerator) + { + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); if (optionCreated) { _createdOptions.Add(option); @@ -138,7 +146,7 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); @@ -184,9 +192,10 @@ public class ModMerger : IDisposable } } - _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -251,7 +260,7 @@ public class ModMerger : IDisposable Mod? result = null; try { - dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); + dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].Mod.Name}."); if (dir == null) throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); @@ -268,7 +277,6 @@ public class ModMerger : IDisposable { foreach (var originalOption in mods) { - var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx]; if (originalOption.IsDefault) { var files = CopySubModFiles(mods[0], dir); @@ -278,13 +286,14 @@ public class ModMerger : IDisposable } else { + var originalGroup = originalOption.Group; var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); + _editor.OptionSetFiles(result, groupIdx, optionIdx, files); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); } } } @@ -309,7 +318,7 @@ public class ModMerger : IDisposable private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) { var ret = new Dictionary(option.FileData.Count); - var parentPath = ((Mod)option.ParentMod).ModPath.FullName; + var parentPath = ((Mod)option.Mod).ModPath.FullName; foreach (var (path, file) in option.FileData) { var target = Path.GetRelativePath(parentPath, file.FullName); @@ -339,7 +348,8 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - _editor.DeleteOption(MergeToMod!, option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index a9a31212..9698fdcb 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -167,28 +167,27 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) // Normalize all other options. foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { - _redirections[groupIdx + 1].EnsureCapacity(group.Count); - for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) - _redirections[groupIdx + 1].Add([]); - var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - foreach (var option in group.OfType()) + switch (group) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + case SingleModGroup single: + _redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); + for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) + _redirections[groupIdx + 1].Add([]); - newDict = _redirections[groupIdx + 1][option.OptionIdx]; - newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) - { - var relPath = new Utf8RelPath(gamePath).ToString(); - var newFullPath = Path.Combine(optionDir.FullName, relPath); - var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); - Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); - File.Copy(fullPath.FullName, newFullPath, true); - newDict.Add(gamePath, redirectPath); - ++Step; - } + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; + case MultiModGroup multi: + _redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count); + for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i) + _redirections[groupIdx + 1].Add([]); + + foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; } } @@ -200,6 +199,24 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) } return false; + + void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + { + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + + newDict.Clear(); + newDict.EnsureCapacity(option.FileData.Count); + foreach (var (gamePath, fullPath) in option.FileData) + { + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFullPath = Path.Combine(optionDir.FullName, relPath); + var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); + Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); + File.Copy(fullPath.FullName, newFullPath, true); + newDict.Add(gamePath, redirectPath); + ++Step; + } + } } private bool MoveOldFiles() @@ -274,9 +291,20 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods) - _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, - _redirections[option.GroupIdx + 1][option.OptionIdx]); + foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) + { + switch (group) + { + case SingleModGroup single: + foreach (var (_, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + case MultiModGroup multi: + foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + } + } ++Step; } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 99ad1a4f..df243781 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,7 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -211,7 +212,14 @@ public class ModCacheManager : IDisposable foreach (var group in mod.Groups) { mod.HasOptions |= group.IsOption; - foreach (var s in group) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var s in optionEnumerator) { mod.TotalFileCount += s.Files.Count; mod.TotalSwapCount += s.FileSwaps.Count; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 295afd7b..9c8ced89 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -126,7 +126,7 @@ public static partial class ModMigration case GroupType.Multi: var optionPriority = ModPriority.Default; - var newMultiGroup = new MultiModGroup() + var newMultiGroup = new MultiModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -134,7 +134,7 @@ public static partial class ModMigration }; mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++)); + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++)); break; case GroupType.Single: @@ -144,7 +144,7 @@ public static partial class ModMigration return; } - var newSingleGroup = new SingleModGroup() + var newSingleGroup = new SingleModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -152,7 +152,7 @@ public static partial class ModMigration }; mod.Groups.Add(newSingleGroup); foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles)); + newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, newSingleGroup, option, seenMetaFiles)); break; } @@ -171,9 +171,9 @@ public static partial class ModMigration } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet seenMetaFiles) + private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SubMod(mod) { Name = option.OptionName }; + var subMod = new SubMod(mod, group) { Name = option.OptionName }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9d942574..e78b6209 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -87,12 +87,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; mod.Groups.Add(type == GroupType.Multi - ? new MultiModGroup + ? new MultiModGroup(mod) { Name = newName, Priority = maxPriority, } - : new SingleModGroup + : new SingleModGroup(mod) { Name = newName, Priority = maxPriority, @@ -120,7 +120,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } @@ -131,7 +130,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) return; - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } @@ -156,12 +154,9 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod.Groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription) + if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription)) return; - option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -173,12 +168,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (group.Priority == newPriority) return; - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; + group.Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } @@ -188,14 +178,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { switch (mod.Groups[groupIdx]) { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + case MultiModGroup multi: + if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) return; - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -205,60 +192,63 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch (mod.Groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } + if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName)) + return; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } /// Add a new empty option of the given name for the given group. - public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, ModPriority.Default)); - break; - } + var group = mod.Groups[groupIdx]; + var idx = group.AddOption(mod, newName); + if (idx < 0) + return -1; saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return idx; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - var idx = group.IndexOf(o => o.Name == newName); - if (idx >= 0) - return ((SubMod)group[idx], false); + switch (group) + { + case SingleModGroup single: + { + var idx = single.OptionData.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (single.OptionData[idx], idx, false); - AddOption(mod, groupIdx, newName, saveType); - if (group[^1].Name != newName) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = single.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - return ((SubMod)group[^1], true); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (single.OptionData[^1], single.OptionData.Count - 1, true); + } + case MultiModGroup multi: + { + var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName); + if (idx >= 0) + return (multi.PrioritizedOptions[idx].Mod, idx, false); + + idx = multi.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true); + } + } + + throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}."); } /// Add an existing option to a given group with default priority. @@ -269,25 +259,28 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { var group = mod.Groups[groupIdx]; + int idx; switch (group) { - case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - option.SetPosition(groupIdx, s.Count); + idx = s.OptionData.Count; s.OptionData.Add(option); break; case MultiModGroup m: - option.SetPosition(groupIdx, m.Count); + idx = m.PrioritizedOptions.Count; m.PrioritizedOptions.Add((option, priority)); break; + default: + return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); } /// Delete the given option from the given group. @@ -306,7 +299,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; } - group.UpdatePositions(optionIdx); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } @@ -396,16 +388,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return false; } - /// Update the indices stored in options from a given group on. - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup)) - { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } - /// Get the correct option for the given group and option index. private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 25f3c510..71f64205 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -38,7 +38,7 @@ public sealed class Mod : IMod internal Mod(DirectoryInfo modPath) { ModPath = modPath; - Default = new SubMod(this); + Default = SubMod.CreateDefault(this); } public override string ToString() @@ -82,7 +82,12 @@ public sealed class Mod : IMod } public IEnumerable AllSubMods - => Groups.SelectMany(o => o).Prepend(Default); + => Groups.SelectMany(o => o switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), + _ => [], + }).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 661dd6fb..4d32f395 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,7 +16,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, +public partial class ModCreator( + SaveService _saveService, + Configuration config, + ModDataEditor _dataEditor, + MetaFileManager _metaFileManager, GamePathParser _gamePathParser) { public readonly Configuration Config = config; @@ -106,7 +110,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, public void LoadDefaultOption(Mod mod) { var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); - mod.Default.SetPosition(-1, 0); try { if (!File.Exists(defaultFile)) @@ -241,27 +244,21 @@ public partial class ModCreator(SaveService _saveService, Configuration config, { case GroupType.Multi: { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; + var group = MultiModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange(subMods.OfType()); + var group = SingleModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; + group.OptionData.AddRange(subMods); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -275,11 +272,8 @@ public partial class ModCreator(SaveService _saveService, Configuration config, .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; + var mod = SubMod.CreateForSaving(option.Name); + mod.Description = option.Description; foreach (var (_, gamePath, file) in list) mod.FileData.TryAdd(gamePath, file); @@ -287,13 +281,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, return mod; } - /// Create an empty sub mod for single groups with None options. - internal static SubMod CreateEmptySubMod(string name) - => new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - /// /// Create the default data file from all unused files that were not handled before /// and are used in sub mods. diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs new file mode 100644 index 00000000..d0b444b8 --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Subclasses; + +public interface IModDataContainer +{ + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) + { + data.Files.Clear(); + data.FileSwaps.Clear(); + data.Manipulations.Clear(); + + var files = (JObject?)json[nameof(Files)]; + if (files != null) + foreach (var property in files.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); + } + + var swaps = (JObject?)json[nameof(FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); + } + + var manips = json[nameof(Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.Validate())) + data.Manipulations.Add(s); + } + + public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) + { + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 96d7c6b7..a046ade0 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,25 +6,27 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup { public const int MaxMultiOptions = 63; + public Mod Mod { get; } public string Name { get; } public string Description { get; } public GroupType Type { get; } - public ModPriority Priority { get; } + public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); + public int AddOption(Mod mod, string name, string description = ""); + public bool ChangeOptionDescription(int optionIndex, string newDescription); + public bool ChangeOptionName(int optionIndex, string newName); - public SubMod this[Index idx] { get; } - - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); - public void UpdatePositions(int from = 0); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs new file mode 100644 index 00000000..bb52a2cd --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.Mods.Subclasses; + +public interface IModOption +{ + public string Name { get; set; } + public string FullName { get; } + public string Description { get; set; } + + public static void Load(JToken json, IModOption option) + { + option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; + } + + public static void WriteModOption(JsonWriter j, IModOption option) + { + j.WritePropertyName(nameof(Name)); + j.WriteValue(option.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(option.Description); + } +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 81a3bb41..2ddabdb8 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -68,7 +68,7 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config.TurnMulti(group.Count), + GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; @@ -182,15 +182,15 @@ public class ModSettings if (idx >= mod.Groups.Count) break; - var group = mod.Groups[idx]; - if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count) + switch (mod.Groups[idx]) { - dict.Add(group.Name, [group[(int)setting.Value].Name]); - } - else - { - var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); - dict.Add(group.Name, list); + case SingleModGroup single when setting.Value < (ulong)single.Options.Count: + dict.Add(single.Name, [single.Options[setting.AsIndex].Name]); + break; + case MultiModGroup multi: + var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList(); + dict.Add(multi.Name, list); + break; } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 02ae07f4..4ec2c72a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,5 +1,4 @@ using Dalamud.Interface.Internal.Notifications; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -11,11 +10,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Multi; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,27 +26,58 @@ public sealed class MultiModGroup : IModGroup .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => PrioritizedOptions[idx].Mod; + public int AddOption(Mod mod, string name, string description = "") + { + var groupIdx = mod.Groups.IndexOf(this); + if (groupIdx < 0) + return -1; + + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + PrioritizedOptions.Add((subMod, ModPriority.Default)); + return PrioritizedOptions.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => PrioritizedOptions.Select(p => p.Mod).ToArray(); public bool IsOption - => Count > 0; - - [JsonIgnore] - public int Count - => PrioritizedOptions.Count; + => PrioritizedOptions.Count > 0; public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() - => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { - var ret = new MultiModGroup() + var ret = new MultiModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -68,8 +99,7 @@ public sealed class MultiModGroup : IModGroup break; } - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out var priority); ret.PrioritizedOptions.Add((subMod, priority)); } @@ -85,12 +115,12 @@ public sealed class MultiModGroup : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup() + var multi = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(Count), + DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), }; multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; @@ -104,16 +134,9 @@ public sealed class MultiModGroup : IModGroup return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) @@ -124,5 +147,12 @@ public sealed class MultiModGroup : IModGroup } public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << Count) - 1)); + => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + + /// Create a group without a mod only for saving it in the creator. + internal static MultiModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index b854d2b1..994a1f96 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -9,11 +8,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Single; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,26 +26,53 @@ public sealed class SingleModGroup : IModGroup .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => OptionData[idx]; + public int AddOption(Mod mod, string name, string description = "") + { + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + OptionData.Add(subMod); + return OptionData.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => OptionData; public bool IsOption - => Count > 1; - - [JsonIgnore] - public int Count - => OptionData.Count; - - public IEnumerator GetEnumerator() - => OptionData.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + => OptionData.Count > 1; public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) { var options = json["Options"]; - var ret = new SingleModGroup + var ret = new SingleModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -58,8 +85,7 @@ public sealed class SingleModGroup : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.OptionData.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out _); ret.OptionData.Add(subMod); } @@ -74,7 +100,7 @@ public sealed class SingleModGroup : IModGroup { case GroupType.Single: return this; case GroupType.Multi: - var multi = new MultiModGroup() + var multi = new MultiModGroup(Mod) { Name = Name, Description = Description, @@ -108,19 +134,19 @@ public sealed class SingleModGroup : IModGroup DefaultSettings = Setting.Single(currentIndex + 1); } - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var (o, i) in OptionData.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => this[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddData(redirections, manipulations); public Setting FixSetting(Setting setting) - => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); + => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + + /// Create a group without a mod only for saving it in the creator. + internal static SingleModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 386910e5..bc93fcc4 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,11 +1,62 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly SingleModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly MultiModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + public ModPriority Priority { get; set; } = ModPriority.Default; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class DefaultSubMod(IMod mod) : IModDataContainer +{ + public string FullName + => "Default Option"; + + public string Description + => string.Empty; + + internal readonly IMod Mod = mod; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + /// /// A sub mod is a collection of /// - file replacements @@ -16,21 +67,51 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod +public sealed class SubMod(IMod mod, IModGroup group) : IModOption { public string Name { get; set; } = "Default"; public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}"; + => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; public string Description { get; set; } = string.Empty; - internal IMod ParentMod { get; private init; } - internal int GroupIdx { get; private set; } - internal int OptionIdx { get; private set; } + internal readonly IMod Mod = mod; + internal readonly IModGroup? Group = group; + internal (int GroupIdx, int OptionIdx) GetIndices() + { + if (IsDefault) + return (-1, 0); + + var groupIdx = Mod.Groups.IndexOf(Group); + if (groupIdx < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); + + return (groupIdx, GetOptionIndex()); + } + + private int GetOptionIndex() + { + var optionIndex = Group switch + { + null => 0, + SingleModGroup single => single.OptionData.IndexOf(this), + MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), + _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), + }; + if (optionIndex < 0) + throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); + + return optionIndex; + } + + public static SubMod CreateDefault(IMod mod) + => new(mod, null!); + + [MemberNotNullWhen(false, nameof(Group))] public bool IsDefault - => GroupIdx < 0; + => Group == null; public void AddData(Dictionary redirections, HashSet manipulations) { @@ -46,9 +127,6 @@ public sealed class SubMod public Dictionary FileSwapData = []; public HashSet ManipulationData = []; - public SubMod(IMod parentMod) - => ParentMod = parentMod; - public IReadOnlyDictionary Files => FileData; @@ -58,12 +136,6 @@ public sealed class SubMod public IReadOnlySet Manipulations => ManipulationData; - public void SetPosition(int groupIdx, int optionIdx) - { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - } - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) { FileData.Clear(); @@ -116,6 +188,14 @@ public sealed class SubMod } } + /// Create a sub mod without a mod or group only for saving it in the creator. + internal static SubMod CreateForSaving(string name) + => new(null!, null!) + { + Name = name, + }; + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) { j.WriteStartObject(); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 41c1211f..a599b3bb 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -49,7 +49,7 @@ public class TemporaryMod : IMod => [Default]; public TemporaryMod() - => Default = new SubMod(this); + => Default = SubMod.CreateDefault(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) => Default.FileData[gamePath] = fullPath; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 7b5ce2dc..5125a5b2 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -253,7 +253,7 @@ public class ItemSwapTab : IDisposable, ITab _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 - && (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); + && (_selectedGroup?.Options.All(o => o.Name != _newOptionName) ?? true); } private void CreateMod() @@ -275,7 +275,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = false; + var optionCreated = -1; DirectoryInfo? optionFolderName = null; try { @@ -294,14 +294,17 @@ public class ItemSwapTab : IDisposable, ITab groupCreated = true; } - _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - optionCreated = true; + var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + if (optionIdx < 0) + throw new Exception($"Failure creating mod option."); + + optionCreated = optionIdx; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) + _mod.Groups.IndexOf(_selectedGroup), optionIdx)) throw new Exception("Failure writing files for mod swap."); } } @@ -310,8 +313,8 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + if (optionCreated >= 0 && _selectedGroup != null) + _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); if (groupCreated) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a70da628..3f5f6c37 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -78,7 +78,10 @@ public partial class ModEditWindow : Window, IDisposable } public void ChangeOption(SubMod? subMod) - => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); + { + var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, optionIdx); + } public void UpdateModels() { @@ -428,7 +431,8 @@ public partial class ModEditWindow : Window, IDisposable using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) { - _editor.LoadOption(option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.LoadOption(groupIdx, optionIdx); ret = true; } } @@ -565,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) + foreach (var option in Mod.AllSubMods) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 1df814da..c34c7ef0 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -71,7 +71,7 @@ public class ModMergeTab(ModMerger modMerger) color = color == Colors.DiscordColor ? Colors.DiscordColor - : group == null || group.Any(o => o.Name == modMerger.OptionName) + : group == null || group.Options.Any(o => o.Name == modMerger.OptionName) ? Colors.PressEnterWarningBg : Colors.DiscordColor; c.Push(ImGuiCol.Border, color); @@ -184,18 +184,26 @@ public class ModMergeTab(ModMerger modMerger) else { ImGuiUtil.DrawTableColumn(option.Name); - var group = option.ParentMod.Groups[option.GroupIdx]; + var group = option.Group; + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) - foreach (var opt in group) - Handle((SubMod)opt, true); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, true); if (ImGui.MenuItem("Unselect All")) - foreach (var opt in group) - Handle((SubMod)opt, false); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, false); ImGui.EndPopup(); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index b002dedd..0dc694d8 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -324,7 +324,7 @@ public class ModPanelEditTab( ? mod.Description : optionIdx < 0 ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx][optionIdx].Description; + : mod.Groups[groupIdx].Options[optionIdx].Description; _oldDescription = _newDescription; _mod = mod; @@ -479,17 +479,24 @@ public class ModPanelEditTab( ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - var group = panel._mod.Groups[groupIdx]; - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - EditOption(panel, group, groupIdx, optionIdx); - + switch (panel._mod.Groups[groupIdx]) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + EditOption(panel, single, groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + EditOption(panel, multi, groupIdx, optionIdx); + break; + } DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } /// Draw a line for a single option. private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { - var option = group[optionIdx]; + var option = group.Options[optionIdx]; using var id = ImRaii.PushId(optionIdx); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -547,10 +554,16 @@ public class ModPanelEditTab( { var mod = panel._mod; var group = mod.Groups[groupIdx]; + var count = group switch + { + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{group.Count + 1}"); - Target(panel, group, groupIdx, group.Count); + ImGui.Selectable($"Option #{count + 1}"); + Target(panel, group, groupIdx, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -562,7 +575,7 @@ public class ModPanelEditTab( } ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; + var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." @@ -588,7 +601,7 @@ public class ModPanelEditTab( _dragDropOptionIdx = optionIdx; } - ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); } private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) @@ -611,12 +624,17 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.Count; - var option = sourceGroup[sourceOption]; - var priority = sourceGroup switch + var currentCount = group switch { - MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, - _ => ModPriority.Default, + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; + var (option, priority) = sourceGroup switch + { + SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default), + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx], + _ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."), }; panel._delayedActions.Enqueue(() => { @@ -651,7 +669,7 @@ public class ModPanelEditTab( if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1107aa20..cb76088c 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -75,7 +75,7 @@ public class ModPanelSettingsTab : ITab { var useDummy = true; foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) + .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) { ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; @@ -92,7 +92,7 @@ public class ModPanelSettingsTab : ITab case GroupType.Multi: DrawMultiGroup(group, idx); break; - case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: + case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: DrawSingleGroupRadio(group, idx); break; } @@ -181,13 +181,14 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) { if (combo) - for (var idx2 = 0; idx2 < group.Count; ++idx2) + for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); - var option = group[idx2]; + var option = options[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx2)); @@ -213,18 +214,18 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); return; void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < group.Options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx)); @@ -239,9 +240,9 @@ public class ModPanelSettingsTab : ITab } - private void DrawCollapseHandling(IModGroup group, float minWidth, Action draw) + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (group.Count <= _config.OptionGroupCollapsibleMin) + if (options.Count <= _config.OptionGroupCollapsibleMin) { draw(); } @@ -249,8 +250,8 @@ public class ModPanelSettingsTab : ITab { var collapseId = ImGui.GetID("Collapse"); var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {group.Count} Options"; - var buttonTextHide = $"Hide {group.Count} Options"; + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); @@ -274,7 +275,7 @@ public class ModPanelSettingsTab : ITab } else { - var optionWidth = group.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; @@ -294,8 +295,8 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; @@ -307,10 +308,10 @@ public class ModPanelSettingsTab : ITab void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; var setting = flags.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref setting)) @@ -339,7 +340,7 @@ public class ModPanelSettingsTab : ITab ImGui.Separator(); if (ImGui.Selectable("Enable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Count)); + Setting.AllBits(group.Options.Count)); if (ImGui.Selectable("Disable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); From 6b1743b776da300fbe1b80a16e662b66ea519a42 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:04:04 +0200 Subject: [PATCH 1646/2451] This sucks so hard... --- Penumbra/Api/Api/ModSettingsApi.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +- Penumbra/Meta/MetaFileManager.cs | 14 +- Penumbra/Mods/Editor/DuplicateManager.cs | 6 +- Penumbra/Mods/Editor/FileRegistry.cs | 12 +- Penumbra/Mods/Editor/ModEditor.cs | 63 +-- Penumbra/Mods/Editor/ModFileCollection.cs | 16 +- Penumbra/Mods/Editor/ModFileEditor.cs | 22 +- Penumbra/Mods/Editor/ModMerger.cs | 79 ++- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 38 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 29 +- Penumbra/Mods/Manager/ModMigration.cs | 39 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 144 +++--- Penumbra/Mods/Mod.cs | 19 +- Penumbra/Mods/ModCreator.cs | 41 +- Penumbra/Mods/Subclasses/IModDataContainer.cs | 48 ++ Penumbra/Mods/Subclasses/IModGroup.cs | 126 +++-- Penumbra/Mods/Subclasses/IModOption.cs | 2 + Penumbra/Mods/Subclasses/MultiModGroup.cs | 122 ++--- Penumbra/Mods/Subclasses/SingleModGroup.cs | 75 +-- Penumbra/Mods/Subclasses/SubMod.cs | 477 +++++++++++------- Penumbra/Mods/TemporaryMod.cs | 35 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 15 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- .../ModEditWindow.Models.MdlTab.cs | 4 +- .../ModEditWindow.QuickImport.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 28 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 33 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- 33 files changed, 852 insertions(+), 695 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 5ed26ce5..e145e027 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); + var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7d9388a9..b9cdda71 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) @@ -183,7 +183,7 @@ public partial class TexToolsImporter var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ExtractSimpleModList(optionFolder, option.ModsJsons); - options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); + options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option, new ModPriority(i))); if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi ? defaultSettings!.Value | Setting.Multi(i) @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, SubMod.CreateForSaving(option.Name)); + options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index b1823bd7..0e2e638b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -50,17 +50,15 @@ public unsafe class MetaFileManager TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); foreach (var group in mod.Groups) { + if (group is not ITexToolsGroup texToolsGroup) + continue; + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); if (!dir.Exists) dir.Create(); - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - foreach (var option in optionEnumerator) + + foreach (var option in texToolsGroup.OptionData) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) @@ -99,7 +97,7 @@ public unsafe class MetaFileManager return; ResidentResources.Reload(); - if (collection?._cache == null) + if (collection._cache == null) CharacterUtility.ResetAll(); else collection._cache.Meta.SetFiles(); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 938199aa..92ec58b9 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, IModDataContainer option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,7 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - subMod.FileData = dict; + subMod.Files = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 427c58ca..44d349ce 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,12 +5,12 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; - public FullPath File { get; private init; } - public Utf8RelPath RelPath { get; private init; } - public long FileSize { get; private init; } - public int CurrentUsage; - public bool IsOnPlayer; + public readonly List<(IModDataContainer, Utf8GamePath)> SubModUsage = []; + public FullPath File { get; private init; } + public Utf8RelPath RelPath { get; private init; } + public long FileSize { get; private init; } + public int CurrentUsage; + public bool IsOnPlayer; public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) { diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 0a96e0fd..1118f890 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,4 +1,3 @@ -using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -25,20 +24,20 @@ public class ModEditor( public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly FileCompactor Compactor = compactor; - public Mod? Mod { get; private set; } - public int GroupIdx { get; private set; } - public int OptionIdx { get; private set; } + public Mod? Mod { get; private set; } + public int GroupIdx { get; private set; } + public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public SubMod? Option { get; private set; } + public IModGroup? Group { get; private set; } + public IModDataContainer? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); - public void LoadMod(Mod mod, int groupIdx, int optionIdx) + public void LoadMod(Mod mod, int groupIdx, int dataIdx) { Mod = mod; - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); Files.UpdateAll(mod, Option!); SwapEditor.Revert(Option!); MetaEditor.Load(Mod!, Option!); @@ -46,9 +45,9 @@ public class ModEditor( MdlMaterialEditor.ScanModels(Mod!); } - public void LoadOption(int groupIdx, int optionIdx) + public void LoadOption(int groupIdx, int dataIdx) { - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); SwapEditor.Revert(Option!); Files.UpdatePaths(Mod!, Option!); MetaEditor.Load(Mod!, Option!); @@ -57,44 +56,38 @@ public class ModEditor( } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. - private void LoadOption(int groupIdx, int optionIdx, bool message) + private void LoadOption(int groupIdx, int dataIdx, bool message) { if (Mod != null && Mod.Groups.Count > groupIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - OptionIdx = optionIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - switch(Group) + if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: - Option = single.OptionData[optionIdx]; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; - case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count: - Option = multi.PrioritizedOptions[optionIdx].Mod; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; + return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - OptionIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) - Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); + Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } public void Clear() @@ -111,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) @@ -123,8 +116,8 @@ public class ModEditor( action(single.OptionData[optionIdx], groupIdx, optionIdx); break; case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) - action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) + action(multi.OptionData[optionIdx], groupIdx, optionIdx); break; } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 9dd78217..ede35914 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, SubMod option) + public void UpdateAll(Mod mod, IModDataContainer option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, SubMod option) + public void UpdatePaths(Mod mod, IModDataContainer option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,14 +154,14 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); tok.ThrowIfCancellationRequested(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { foreach (var (gamePath, file) in subMod.Files) { diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 51615b05..11e35334 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -14,7 +14,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu Changes = false; } - public int Apply(Mod mod, SubMod option) + public int Apply(Mod mod, IModDataContainer option) { var dict = new Dictionary(); var num = 0; @@ -24,23 +24,23 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, optionIdx) = option.GetIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + var (groupIdx, dataIdx) = option.GetDataIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; } - public void Revert(Mod mod, SubMod option) + public void Revert(Mod mod, IModDataContainer option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, SubMod option) + public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -62,7 +62,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(IModDataContainer option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -85,7 +85,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(IModDataContainer option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -112,7 +112,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(SubMod option, IEnumerable files1) + public void RemovePathsFromSelected(IModDataContainer option, IEnumerable files1) { foreach (var file in files1) { @@ -130,7 +130,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, IModDataContainer option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -156,7 +156,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, IModDataContainer option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 74e9007c..541c84ae 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,6 +1,5 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; -using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -33,9 +32,9 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = []; private readonly HashSet _createdDirectories = []; private readonly HashSet _createdGroups = []; - private readonly HashSet _createdOptions = []; + private readonly HashSet _createdOptions = []; - public readonly HashSet SelectedOptions = []; + public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } @@ -94,7 +93,7 @@ public class ModMerger : IDisposable private void MergeWithOptions() { - MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false); + MergeIntoOption([MergeFromMod!.Default], MergeToMod!.Default, false); foreach (var originalGroup in MergeFromMod!.Groups) { @@ -105,20 +104,13 @@ public class ModMerger : IDisposable ((List)Warnings).Add( $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); - var optionEnumerator = group switch + foreach (var originalOption in group.DataContainers) { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - - foreach (var originalOption in optionEnumerator) - { - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add(option); - MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false); + _createdOptions.Add((IModDataOption)option); + MergeIntoOption([originalOption], (IModDataOption)option, false); } else { @@ -136,7 +128,7 @@ public class ModMerger : IDisposable if (groupName.Length == 0 && optionName.Length == 0) { CopyFiles(MergeToMod!.ModPath); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), MergeToMod!.Default, true); } else if (groupName.Length * optionName.Length == 0) { @@ -148,7 +140,7 @@ public class ModMerger : IDisposable _createdGroups.Add(groupIdx); var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add(option); + _createdOptions.Add((IModDataOption)option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -156,14 +148,14 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) { - var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.ManipulationData.ToHashSet(); + var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var manips = option.Manipulations.ToHashSet(); foreach (var originalOption in mergeOptions) { @@ -171,31 +163,31 @@ public class ModMerger : IDisposable { if (!manips.Add(manip)) throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.FullName} to {option.FullName} because another manipulation of the same data already exists in this option."); + $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); } foreach (var (swapA, swapB) in originalOption.FileSwaps) { if (!swaps.TryAdd(swapA, swapB)) throw new Exception( - $"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); + $"Could not add file swap {swapB} -> {swapA} from {originalOption.GetFullName()} to {option.GetFullName()} because another swap of the key already exists."); } foreach (var (gamePath, path) in originalOption.Files) { if (!GetFullPath(path, out var newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because the file does not exist in the new mod."); if (!redirections.TryAdd(gamePath, newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because a redirection for the game path already exists."); } } - var (groupIdx, optionIdx) = option.GetIndices(); - _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -270,30 +262,29 @@ public class ModMerger : IDisposable { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.IsDefault) + if (originalOption.Group is not {} originalGroup) { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { - var originalGroup = originalOption.Group; - var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); _editor.OptionSetFiles(result, groupIdx, optionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); } } } @@ -315,11 +306,11 @@ public class ModMerger : IDisposable } } - private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) + private static Dictionary CopySubModFiles(IModDataContainer option, DirectoryInfo newMod) { - var ret = new Dictionary(option.FileData.Count); + var ret = new Dictionary(option.Files.Count); var parentPath = ((Mod)option.Mod).ModPath.FullName; - foreach (var (path, file) in option.FileData) + foreach (var (path, file) in option.Files) { var target = Path.GetRelativePath(parentPath, file.FullName); target = Path.Combine(newMod.FullName, target); @@ -348,7 +339,7 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetIndices(); + var (groupIdx, optionIdx) = option.GetOptionIndices(); _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index a6218c6f..88e48f0f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, SubMod currentOption) + public void Load(Mod mod, IModDataContainer currentOption) { OtherImcCount = 0; OtherEqpCount = 0; @@ -111,7 +111,7 @@ public class ModMetaEditor(ModManager modManager) OtherGmpCount = 0; OtherEstCount = 0; OtherRspCount = 0; - foreach (var option in mod.AllSubMods) + foreach (var option in mod.AllDataContainers) { if (option == currentOption) continue; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9698fdcb..db00a1c7 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -168,27 +169,11 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - switch (group) - { - case SingleModGroup single: - _redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); - for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) - _redirections[groupIdx + 1].Add([]); - - foreach (var (option, optionIdx) in single.OptionData.WithIndex()) - HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); - - break; - case MultiModGroup multi: - _redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count); - for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i) - _redirections[groupIdx + 1].Add([]); - - foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex()) - HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); - - break; - } + _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); + for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) + _redirections[groupIdx + 1].Add([]); + foreach (var (data, dataIdx) in group.DataContainers.WithIndex()) + HandleSubMod(groupDir, data, _redirections[groupIdx + 1][dataIdx]); } return true; @@ -200,13 +185,14 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) return false; - void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + var name = option.GetName(); + var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) + newDict.EnsureCapacity(option.Files.Count); + foreach (var (gamePath, fullPath) in option.Files) { var relPath = new Utf8RelPath(gamePath).ToString(); var newFullPath = Path.Combine(optionDir.FullName, relPath); @@ -300,7 +286,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0d5f05a9..64788cf3 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(SubMod option) + public void Revert(IModDataContainer option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index df243781..4f9e8648 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -176,13 +176,13 @@ public class ModCacheManager : IDisposable } private static void UpdateFileCount(Mod mod) - => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); + => mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count); private static void UpdateSwapCount(Mod mod) - => mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + => mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count); private static void UpdateMetaCount(Mod mod) - => mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count); + => mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count); private static void UpdateHasOptions(Mod mod) => mod.HasOptions = mod.Groups.Any(o => o.IsOption); @@ -194,10 +194,10 @@ public class ModCacheManager : IDisposable { var changedItems = (SortedList)mod.ChangedItems; changedItems.Clear(); - foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) + foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) _identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) + foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); @@ -211,20 +211,11 @@ public class ModCacheManager : IDisposable mod.HasOptions = false; foreach (var group in mod.Groups) { - mod.HasOptions |= group.IsOption; - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - - foreach (var s in optionEnumerator) - { - mod.TotalFileCount += s.Files.Count; - mod.TotalSwapCount += s.FileSwaps.Count; - mod.TotalManipulations += s.Manipulations.Count; - } + mod.HasOptions |= group.IsOption; + var (files, swaps, manips) = group.GetCounts(); + mod.TotalFileCount += files; + mod.TotalSwapCount += swaps; + mod.TotalManipulations += manips; } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 9c8ced89..8c4a5674 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -71,14 +71,14 @@ public static partial class ModMigration foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) { if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod.Default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}."); + && !mod.Default.Files.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.Files[gamePath]}."); } - mod.Default.FileSwapData.Clear(); - mod.Default.FileSwapData.EnsureCapacity(swaps.Count); + mod.Default.FileSwaps.Clear(); + mod.Default.FileSwaps.EnsureCapacity(swaps.Count); foreach (var (gamePath, swapPath) in swaps) - mod.Default.FileSwapData.Add(gamePath, swapPath); + mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); foreach (var (_, index) in mod.Groups.WithIndex()) @@ -134,7 +134,7 @@ public static partial class ModMigration }; mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++)); + newMultiGroup.OptionData.Add(SubModFromOption(creator, mod, newMultiGroup, option, optionPriority++, seenMetaFiles)); break; case GroupType.Single: @@ -158,22 +158,41 @@ public static partial class ModMigration } } - private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) + private static void AddFilesToSubMod(IModDataContainer mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) { foreach (var (relPath, gamePaths) in option.OptionFiles) { var fullPath = new FullPath(basePath, relPath); foreach (var gamePath in gamePaths) - mod.FileData.TryAdd(gamePath, fullPath); + mod.Files.TryAdd(gamePath, fullPath); if (fullPath.Extension is ".meta" or ".rgsp") seenMetaFiles.Add(fullPath); } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) + private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, + HashSet seenMetaFiles) { - var subMod = new SubMod(mod, group) { Name = option.OptionName }; + var subMod = new SingleSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + return subMod; + } + + private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, + ModPriority priority, HashSet seenMetaFiles) + { + var subMod = new MultiSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + Priority = priority, + }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index e78b6209..4d3a5717 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -179,10 +178,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS switch (mod.Groups[groupIdx]) { case MultiModGroup multi: - if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) + if (multi.OptionData[optionIdx].Priority == newPriority) return; - multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.OptionData[optionIdx].Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -213,70 +212,62 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - switch (group) - { - case SingleModGroup single: - { - var idx = single.OptionData.IndexOf(o => o.Name == newName); - if (idx >= 0) - return (single.OptionData[idx], idx, false); + var idx = group.Options.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (group.Options[idx], idx, false); - idx = single.AddOption(mod, newName); - if (idx < 0) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = group.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (single.OptionData[^1], single.OptionData.Count - 1, true); - } - case MultiModGroup multi: - { - var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName); - if (idx >= 0) - return (multi.PrioritizedOptions[idx].Mod, idx, false); - - idx = multi.AddOption(mod, newName); - if (idx < 0) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true); - } - } - - throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}."); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (group.Options[idx], idx, true); } - /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, SubMod option) - => AddOption(mod, groupIdx, option, ModPriority.Default); - - /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) + /// Add an existing option to a given group. + public void AddOption(Mod mod, int groupIdx, IModOption option) { var group = mod.Groups[groupIdx]; int idx; switch (group) { - case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }: Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: + { idx = s.OptionData.Count; - s.OptionData.Add(option); + var newOption = new SingleSubMod(s.Mod, s) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + s.OptionData.Add(newOption); break; + } case MultiModGroup m: - idx = m.PrioritizedOptions.Count; - m.PrioritizedOptions.Add((option, priority)); + { + idx = m.OptionData.Count; + var newOption = new MultiSubMod(m.Mod, m) + { + Name = option.Name, + Description = option.Description, + Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + m.OptionData.Add(newOption); break; - default: - return; + } + default: return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); @@ -295,7 +286,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); + m.OptionData.RemoveAt(optionIdx); break; } @@ -315,59 +306,59 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations, + public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); if (subMod.Manipulations.Count == manipulations.Count && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData.SetTo(manipulations); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Manipulations.SetTo(manipulations); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements, + public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.Files.SetEquals(replacements)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData.SetTo(replacements); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Files.SetTo(replacements); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary additions) + public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary additions) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) { saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1); } } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps, + public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.FileSwaps.SetEquals(swaps)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData.SetTo(swaps); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.FileSwaps.SetTo(swaps); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1); } @@ -389,17 +380,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Get the correct option for the given group and option index. - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataContainerIdx == 0) return mod.Default; - return mod.Groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; + return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 71f64205..5c02213e 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -38,7 +38,7 @@ public sealed class Mod : IMod internal Mod(DirectoryInfo modPath) { ModPath = modPath; - Default = SubMod.CreateDefault(this); + Default = new DefaultSubMod(this); } public override string ToString() @@ -61,8 +61,8 @@ public sealed class Mod : IMod // Options - public readonly SubMod Default; - public readonly List Groups = []; + public readonly DefaultSubMod Default; + public readonly List Groups = []; public AppliedModData GetData(ModSettings? settings = null) { @@ -77,21 +77,16 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - Default.AddData(dictRedirections, setManips); + Default.AddDataTo(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - public IEnumerable AllSubMods - => Groups.SelectMany(o => o switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), - _ => [], - }).Prepend(Default); + public IEnumerable AllDataContainers + => Groups.SelectMany(o => o.DataContainers).Prepend(Default); public List FindUnusedFiles() { - var modFiles = AllSubMods.SelectMany(o => o.Files) + var modFiles = AllDataContainers.SelectMany(o => o.Files) .Select(p => p.Value) .ToHashSet(); return ModPath.EnumerateDirectories() diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 4d32f395..c1236037 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -112,10 +112,8 @@ public partial class ModCreator( var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); try { - if (!File.Exists(defaultFile)) - mod.Default.Load(mod.ModPath, new JObject(), out _); - else - mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); + var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); + IModDataContainer.Load(jObject, mod.Default, mod.ModPath); } catch (Exception e) { @@ -154,7 +152,7 @@ public partial class ModCreator( { var changes = false; List deleteList = new(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); changes |= localChanges; @@ -162,7 +160,7 @@ public partial class ModCreator( deleteList.AddRange(localDeleteList); } - SubMod.DeleteDeleteList(deleteList, delete); + IModDataContainer.DeleteDeleteList(deleteList, delete); if (!changes) return; @@ -176,10 +174,10 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) { var deleteList = new List(); - var oldSize = option.ManipulationData.Count; + var oldSize = option.Manipulations.Count; var deleteString = delete ? "with deletion." : "without deletion."; foreach (var (key, file) in option.Files.ToList()) { @@ -189,7 +187,7 @@ public partial class ModCreator( { if (ext1 == ".meta" || ext2 == ".meta") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -198,11 +196,11 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(meta.MetaManipulations); + option.Manipulations.UnionWith(meta.MetaManipulations); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -212,7 +210,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(rgsp.MetaManipulations); + option.Manipulations.UnionWith(rgsp.MetaManipulations); } } catch (Exception e) @@ -221,8 +219,8 @@ public partial class ModCreator( } } - SubMod.DeleteDeleteList(deleteList, delete); - return (oldSize < option.ManipulationData.Count, deleteList); + IModDataContainer.DeleteDeleteList(deleteList, delete); + return (oldSize < option.Manipulations.Count, deleteList); } /// @@ -238,7 +236,7 @@ public partial class ModCreator( /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +246,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); + group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -258,7 +256,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -266,16 +264,15 @@ public partial class ModCreator( } /// Create the data for a given sub mod from its data and the folder it is based on. - public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = SubMod.CreateForSaving(option.Name); - mod.Description = option.Description; + var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) - mod.FileData.TryAdd(gamePath, file); + mod.Files.TryAdd(gamePath, file); IncorporateMetaChanges(mod, baseFolder, true); return mod; @@ -292,7 +289,7 @@ public partial class ModCreator( foreach (var file in mod.FindUnusedFiles()) { if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) - mod.Default.FileData.TryAdd(gamePath, file); + mod.Default.Files.TryAdd(gamePath, file); } IncorporateMetaChanges(mod.Default, directory, true); diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs index d0b444b8..a26beb2a 100644 --- a/Penumbra/Mods/Subclasses/IModDataContainer.cs +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -1,12 +1,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; public interface IModDataContainer { + public IMod Mod { get; } + public IModGroup? Group { get; } + public Dictionary Files { get; set; } public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } @@ -21,6 +25,32 @@ public interface IModDataContainer manipulations.UnionWith(Manipulations); } + public string GetName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public string GetFullName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public static void Clone(IModDataContainer from, IModDataContainer to) + { + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); + to.Manipulations = [.. from.Manipulations]; + } + + public (int GroupIndex, int DataIndex) GetDataIndices(); + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -77,4 +107,22 @@ public interface IModDataContainer serializer.Serialize(j, data.Manipulations); j.WriteEndObject(); } + + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) + { + if (!delete) + return; + + foreach (var file in deleteList) + { + try + { + File.Delete(file); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); + } + } + } } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index a046ade0..5c500793 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; @@ -6,6 +7,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public interface ITexToolsGroup +{ + public IReadOnlyList OptionData { get; } +} + public interface IModGroup { public const int MaxMultiOptions = 63; @@ -19,28 +25,89 @@ public interface IModGroup public FullPath? FindBestMatch(Utf8GamePath gamePath); public int AddOption(Mod mod, string name, string description = ""); - public bool ChangeOptionDescription(int optionIndex, string newDescription); - public bool ChangeOptionName(int optionIndex, string newName); - public IReadOnlyList Options { get; } - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public IReadOnlyList DataContainers { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public int GetIndex(); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null); + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) + { + jWriter.WriteStartObject(); + jWriter.WritePropertyName(nameof(group.Name)); + jWriter.WriteValue(group!.Name); + jWriter.WritePropertyName(nameof(group.Description)); + jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Priority)); + jWriter.WriteValue(group.Priority.Value); + jWriter.WritePropertyName(nameof(group.Type)); + jWriter.WriteValue(group.Type.ToString()); + jWriter.WritePropertyName(nameof(group.DefaultSettings)); + jWriter.WriteValue(group.DefaultSettings.Value); + } + + public (int Redirections, int Swaps, int Manips) GetCounts(); + + public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) + { + var redirectionCount = 0; + var swapCount = 0; + var manipCount = 0; + foreach (var option in group.DataContainers) + { + redirectionCount += option.Files.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; + } + + return (redirectionCount, swapCount, manipCount); + } } public readonly struct ModSaveGroup : ISavable { - private readonly DirectoryInfo _basePath; - private readonly IModGroup? _group; - private readonly int _groupIdx; - private readonly SubMod? _defaultMod; - private readonly bool _onlyAscii; + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly DefaultSubMod? _defaultMod; + private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) { @@ -61,7 +128,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -77,42 +144,11 @@ public readonly struct ModSaveGroup : ISavable using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); if (_groupIdx >= 0) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(_group.Name)); - j.WriteValue(_group!.Name); - j.WritePropertyName(nameof(_group.Description)); - j.WriteValue(_group.Description); - j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority.Value); - j.WritePropertyName(nameof(Type)); - j.WriteValue(_group.Type.ToString()); - j.WritePropertyName(nameof(_group.DefaultSettings)); - j.WriteValue(_group.DefaultSettings.Value); - switch (_group) - { - case SingleModGroup single: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var option in single.OptionData) - SubMod.WriteSubMod(j, serializer, option, _basePath, null); - j.WriteEndArray(); - j.WriteEndObject(); - break; - case MultiModGroup multi: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var (option, priority) in multi.PrioritizedOptions) - SubMod.WriteSubMod(j, serializer, option, _basePath, priority); - j.WriteEndArray(); - j.WriteEndObject(); - break; - } - } + _group!.WriteJson(j, serializer); else - { - SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); - } + IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + j.WriteEndObject(); } } diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs index bb52a2cd..f66ce44e 100644 --- a/Penumbra/Mods/Subclasses/IModOption.cs +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -15,6 +15,8 @@ public interface IModOption option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; } + public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(Name)); diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 4ec2c72a..f194350a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -10,20 +11,30 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup(Mod mod) : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => OptionData; + + public bool IsOption + => OptionData.Count > 0; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => PrioritizedOptions.OrderByDescending(o => o.Priority) - .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + => OptionData.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") @@ -32,49 +43,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (groupIdx < 0) return -1; - var subMod = new SubMod(mod, this) + var subMod = new MultiSubMod(mod, this) { Name = name, Description = description, }; - PrioritizedOptions.Add((subMod, ModPriority.Default)); - return PrioritizedOptions.Count - 1; + OptionData.Add(subMod); + return OptionData.Count - 1; } - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) - return false; - - var option = PrioritizedOptions[optionIndex].Mod; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) - return false; - - var option = PrioritizedOptions[optionIndex].Mod; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - - public IReadOnlyList Options - => PrioritizedOptions.Select(p => p.Mod).ToArray(); - - public bool IsOption - => PrioritizedOptions.Count > 0; - - public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { var ret = new MultiModGroup(mod) @@ -91,7 +68,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) + if (ret.OptionData.Count == IModGroup.MaxMultiOptions) { Penumbra.Messager.NotificationMessage( $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", @@ -99,9 +76,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup break; } - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out var priority); - ret.PrioritizedOptions.Add((subMod, priority)); + var subMod = new MultiSubMod(mod, ret, child); + ret.OptionData.Add(subMod); } ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); @@ -115,39 +91,68 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup(Mod) + var single = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; - multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); - return multi; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); + return single; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) + if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { - foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - option.Mod.AddData(redirections, manipulations); + option.AddDataTo(redirections, manipulations); } } + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.Priority)); + jWriter.WriteValue(option.Priority.Value); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + => new(setting.Value & ((1ul << OptionData.Count) - 1)); /// Create a group without a mod only for saving it in the creator. internal static MultiModGroup CreateForSaving(string name) @@ -155,4 +160,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 994a1f96..d1a3b6d1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -8,7 +9,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup(Mod mod) : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { public GroupType Type => GroupType.Single; @@ -19,16 +20,19 @@ public sealed class SingleModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public readonly List OptionData = []; + public readonly List OptionData = []; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData - .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") { - var subMod = new SubMod(mod, this) + var subMod = new SingleSubMod(mod, this) { Name = name, Description = description, @@ -37,35 +41,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup return OptionData.Count - 1; } - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= OptionData.Count) - return false; - - var option = OptionData[optionIndex]; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= OptionData.Count) - return false; - - var option = OptionData[optionIndex]; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - public IReadOnlyList Options => OptionData; + public IReadOnlyList DataContainers + => OptionData; + public bool IsOption => OptionData.Count > 1; @@ -85,8 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out _); + var subMod = new SingleSubMod(mod, ret, child); ret.OptionData.Add(subMod); } @@ -107,7 +87,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup Priority = Priority, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; - multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); return multi; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } @@ -137,12 +117,39 @@ public sealed class SingleModGroup(Mod mod) : IModGroup return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + /// Create a group without a mod only for saving it in the creator. internal static SingleModGroup CreateForSaving(string name) => new(null!) diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index bc93fcc4..a2425eb7 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -7,7 +7,9 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +public interface IModDataOption : IModOption, IModDataContainer; + +public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly SingleModGroup Group = group; @@ -19,12 +21,68 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataC public string Description { get; set; } = string.Empty; + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + public SingleSubMod(Mod mod, SingleModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + } + + public SingleSubMod Clone(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } -public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly MultiModGroup Group = group; @@ -40,12 +98,76 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataCon public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + + public MultiSubMod(Mod mod, MultiModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + } + + public MultiSubMod Clone(Mod mod, MultiModGroup group) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = Priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) + => new(null!, null!) + { + Name = name, + Description = description, + Priority = priority, + }; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } public class DefaultSubMod(IMod mod) : IModDataContainer { - public string FullName - => "Default Option"; + public const string FullName = "Default Option"; public string Description => string.Empty; @@ -55,183 +177,176 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup? IModDataContainer.Group + => null; + + + public DefaultSubMod(Mod mod, JToken json) + : this(mod) + { + IModDataContainer.Load(json, this, mod.ModPath); + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (-1, 0); } -/// -/// A sub mod is a collection of -/// - file replacements -/// - file swaps -/// - meta manipulations -/// that can be used either as an option or as the default data for a mod. -/// It can be loaded and reloaded from Json. -/// Nothing is checked for existence or validity when loading. -/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. -/// -public sealed class SubMod(IMod mod, IModGroup group) : IModOption -{ - public string Name { get; set; } = "Default"; - public string FullName - => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - internal readonly IMod Mod = mod; - internal readonly IModGroup? Group = group; - - internal (int GroupIdx, int OptionIdx) GetIndices() - { - if (IsDefault) - return (-1, 0); - - var groupIdx = Mod.Groups.IndexOf(Group); - if (groupIdx < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); - - return (groupIdx, GetOptionIndex()); - } - - private int GetOptionIndex() - { - var optionIndex = Group switch - { - null => 0, - SingleModGroup single => single.OptionData.IndexOf(this), - MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), - _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), - }; - if (optionIndex < 0) - throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); - - return optionIndex; - } - - public static SubMod CreateDefault(IMod mod) - => new(mod, null!); - - [MemberNotNullWhen(false, nameof(Group))] - public bool IsDefault - => Group == null; - - public void AddData(Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); - } - - public Dictionary FileData = []; - public Dictionary FileSwapData = []; - public HashSet ManipulationData = []; - - public IReadOnlyDictionary Files - => FileData; - - public IReadOnlyDictionary FileSwaps - => FileSwapData; - - public IReadOnlySet Manipulations - => ManipulationData; - - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) - { - FileData.Clear(); - FileSwapData.Clear(); - ManipulationData.Clear(); - - // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(Description)]?.ToObject() ?? string.Empty; - priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; - - var files = (JObject?)json[nameof(Files)]; - if (files != null) - foreach (var property in files.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); - } - - var swaps = (JObject?)json[nameof(FileSwaps)]; - if (swaps != null) - foreach (var property in swaps.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); - } - - var manips = json[nameof(Manipulations)]; - if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - ManipulationData.Add(s); - } - - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) - { - if (!delete) - return; - - foreach (var file in deleteList) - { - try - { - File.Delete(file); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); - } - } - } - - /// Create a sub mod without a mod or group only for saving it in the creator. - internal static SubMod CreateForSaving(string name) - => new(null!, null!) - { - Name = name, - }; - - - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} +//public sealed class SubMod(IMod mod, IModGroup group) : IModOption +//{ +// public string Name { get; set; } = "Default"; +// +// public string FullName +// => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; +// +// public string Description { get; set; } = string.Empty; +// +// internal readonly IMod Mod = mod; +// internal readonly IModGroup? Group = group; +// +// internal (int GroupIdx, int OptionIdx) GetIndices() +// { +// if (IsDefault) +// return (-1, 0); +// +// var groupIdx = Mod.Groups.IndexOf(Group); +// if (groupIdx < 0) +// throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); +// +// return (groupIdx, GetOptionIndex()); +// } +// +// private int GetOptionIndex() +// { +// var optionIndex = Group switch +// { +// null => 0, +// SingleModGroup single => single.OptionData.IndexOf(this), +// MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this), +// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), +// }; +// if (optionIndex < 0) +// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); +// +// return optionIndex; +// } +// +// public static SubMod CreateDefault(IMod mod) +// => new(mod, null!); +// +// [MemberNotNullWhen(false, nameof(Group))] +// public bool IsDefault +// => Group == null; +// +// public void AddData(Dictionary redirections, HashSet manipulations) +// { +// foreach (var (path, file) in Files) +// redirections.TryAdd(path, file); +// +// foreach (var (path, file) in FileSwaps) +// redirections.TryAdd(path, file); +// manipulations.UnionWith(Manipulations); +// } +// +// public Dictionary FileData = []; +// public Dictionary FileSwapData = []; +// public HashSet ManipulationData = []; +// +// public IReadOnlyDictionary Files +// => FileData; +// +// public IReadOnlyDictionary FileSwaps +// => FileSwapData; +// +// public IReadOnlySet Manipulations +// => ManipulationData; +// +// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) +// { +// FileData.Clear(); +// FileSwapData.Clear(); +// ManipulationData.Clear(); +// +// // Every option has a name, but priorities are only relevant for multi group options. +// Name = json[nameof(Name)]?.ToObject() ?? string.Empty; +// Description = json[nameof(Description)]?.ToObject() ?? string.Empty; +// priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; +// +// var files = (JObject?)json[nameof(Files)]; +// if (files != null) +// foreach (var property in files.Properties()) +// { +// if (Utf8GamePath.FromString(property.Name, out var p, true)) +// FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); +// } +// +// var swaps = (JObject?)json[nameof(FileSwaps)]; +// if (swaps != null) +// foreach (var property in swaps.Properties()) +// { +// if (Utf8GamePath.FromString(property.Name, out var p, true)) +// FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); +// } +// +// var manips = json[nameof(Manipulations)]; +// if (manips != null) +// foreach (var s in manips.Children().Select(c => c.ToObject()) +// .Where(m => m.Validate())) +// ManipulationData.Add(s); +// } +// +// +// /// Create a sub mod without a mod or group only for saving it in the creator. +// internal static SubMod CreateForSaving(string name) +// => new(null!, null!) +// { +// Name = name, +// }; +// +// +// public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) +// { +// j.WriteStartObject(); +// j.WritePropertyName(nameof(Name)); +// j.WriteValue(mod.Name); +// j.WritePropertyName(nameof(Description)); +// j.WriteValue(mod.Description); +// if (priority != null) +// { +// j.WritePropertyName(nameof(IModGroup.Priority)); +// j.WriteValue(priority.Value.Value); +// } +// +// j.WritePropertyName(nameof(mod.Files)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.Files) +// { +// if (file.ToRelPath(basePath, out var relPath)) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(relPath.ToString()); +// } +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.FileSwaps)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.FileSwaps) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(file.ToString()); +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.Manipulations)); +// serializer.Serialize(j, mod.Manipulations); +// j.WriteEndObject(); +// } +//} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index a599b3bb..393369d4 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -18,49 +18,46 @@ public class TemporaryMod : IMod public int TotalManipulations => Default.Manipulations.Count; - public readonly SubMod Default; + public readonly DefaultSubMod Default; public AppliedModData GetData(ModSettings? settings = null) { Dictionary dict; - if (Default.FileSwapData.Count == 0) + if (Default.FileSwaps.Count == 0) { - dict = Default.FileData; + dict = Default.Files; } - else if (Default.FileData.Count == 0) + else if (Default.Files.Count == 0) { - dict = Default.FileSwapData; + dict = Default.FileSwaps; } else { // Need to ensure uniqueness. - dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); - foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict = new Dictionary(Default.Files.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.Files.Concat(Default.FileSwaps)) dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.ManipulationData); + return new AppliedModData(dict, Default.Manipulations); } public IReadOnlyList Groups => Array.Empty(); - public IEnumerable AllSubMods - => [Default]; - public TemporaryMod() - => Default = SubMod.CreateDefault(this); + => Default = new(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.FileData[gamePath] = fullPath; + => Default.Files[gamePath] = fullPath; public bool SetManipulation(MetaManipulation manip) - => Default.ManipulationData.Remove(manip) | Default.ManipulationData.Add(manip); + => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); public void SetAll(Dictionary dict, HashSet manips) { - Default.FileData = dict; - Default.ManipulationData = manips; + Default.Files = dict; + Default.Manipulations = manips; } public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, @@ -93,16 +90,16 @@ public class TemporaryMod : IMod { var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath)); File.Copy(targetPath, target, true); - defaultMod.FileData[gamePath] = new FullPath(target); + defaultMod.Files[gamePath] = new FullPath(target); } else { - defaultMod.FileSwapData[gamePath] = new FullPath(targetPath); + defaultMod.FileSwaps[gamePath] = new FullPath(targetPath); } } foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.ManipulationData.Add(manip); + defaultMod.Manipulations.Add(manip); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c891d33a..503d64b7 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -305,7 +305,7 @@ public class FileEditor( UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.TextUnformatted(option.FullName); + ImGui.TextUnformatted(option.GetFullName()); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index f765b47e..94f1d577 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -3,7 +3,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -79,7 +78,7 @@ public partial class ModEditWindow var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) - : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, + : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(), _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); }); @@ -148,13 +147,13 @@ public partial class ModEditWindow (string, int) GetMulti() { var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); - return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); + return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length); } var (text, groupCount) = color switch { ColorId.ConflictingMod => (string.Empty, 0), - ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), + ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1), ColorId.InheritedMod => GetMulti(), _ => (string.Empty, 0), }; @@ -192,7 +191,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +227,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); @@ -301,9 +300,9 @@ public partial class ModEditWindow tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); if (failedFiles > 0) - Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.GetFullName()}."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index aad70cb3..92a9dd66 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -45,7 +45,7 @@ public partial class ModEditWindow var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cca8fe10..6b9965b8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -85,7 +85,7 @@ public partial class ModEditWindow { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. - return mod.AllSubMods + return mod.AllDataContainers .SelectMany(m => m.Files.Concat(m.FileSwaps)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Select(kv => kv.Key) @@ -103,7 +103,7 @@ public partial class ModEditWindow return []; // Filter then prepend the current option to ensure it's chosen first. - return mod.AllSubMods + return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) .SelectMany(subMod => subMod.Manipulations) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 4ecacece..70854fe7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -187,8 +187,8 @@ public partial class ModEditWindow if (editor == null) return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); - var subMod = editor.Option; - var optionName = subMod!.FullName; + var subMod = editor.Option!; + var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName; if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) return new QuickImportAction(editor, optionName, gamePath); @@ -199,7 +199,7 @@ public partial class ModEditWindow if (mod == null) return new QuickImportAction(editor, optionName, gamePath); - var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) return new QuickImportAction(editor, optionName, gamePath); @@ -222,16 +222,16 @@ public partial class ModEditWindow { fileRegistry, }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, IModOption? subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; - if (subMod == mod.Default) + if (subMod == null) return (path, subDirs); var name = subMod.Name; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 3f5f6c37..21d14f78 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -77,10 +77,10 @@ public partial class ModEditWindow : Window, IDisposable _forceTextureStartPath = true; } - public void ChangeOption(SubMod? subMod) + public void ChangeOption(IModDataContainer? subMod) { - var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, dataIdx); } public void UpdateModels() @@ -111,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable }); var manipulations = 0; var subMods = 0; - var swaps = Mod!.AllSubMods.Sum(m => + var swaps = Mod!.AllDataContainers.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; @@ -330,7 +330,7 @@ public partial class ModEditWindow : Window, IDisposable else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { _editor.ModNormalizer.Normalize(Mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx), TaskScheduler.Default); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.DataIdx), TaskScheduler.Default); } if (!_editor.Duplicates.Worker.IsCompleted) @@ -405,7 +405,7 @@ public partial class ModEditWindow : Window, IDisposable var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var ret = false; if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor.Option!.IsDefault)) + _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0); ret = true; @@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) { - _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ret = true; } @@ -422,17 +422,17 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SetNextItemWidth(width.X); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); + using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName()); if (!combo) return ret; - foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) + foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex()) { using var id = ImRaii.PushId(idx); - if (ImGui.Selectable(option.FullName, option == _editor.Option)) + if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) { - var (groupIdx, optionIdx) = option.GetIndices(); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.LoadOption(groupIdx, dataIdx); ret = true; } } @@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -569,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.AllSubMods) + foreach (var option in Mod.AllDataContainers) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index c34c7ef0..bed31ab8 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -50,8 +50,7 @@ public class ModMergeTab(ModMerger modMerger) ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); - var width = ImGui.GetItemRectSize(); - using (var g = ImRaii.Group()) + using (ImRaii.Group()) { using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; @@ -124,13 +123,13 @@ public class ModMergeTab(ModMerger modMerger) ImGui.Dummy(Vector2.One); var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); if (ImGui.Button("Select All", buttonSize)) - modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers); ImGui.SameLine(); if (ImGui.Button("Unselect All", buttonSize)) modMerger.SelectedOptions.Clear(); ImGui.SameLine(); if (ImGui.Button("Invert Selection", buttonSize)) - modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers); DrawOptionTable(size); } @@ -144,7 +143,7 @@ public class ModMergeTab(ModMerger modMerger) private void DrawOptionTable(float size) { - var options = modMerger.MergeFromMod!.AllSubMods.ToList(); + var options = modMerger.MergeFromMod!.AllDataContainers.ToList(); var height = modMerger.Warnings.Count == 0 && modMerger.Error == null ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() : 8 * ImGui.GetFrameHeightWithSpacing(); @@ -176,47 +175,41 @@ public class ModMergeTab(ModMerger modMerger) if (ImGui.Checkbox("##check", ref selected)) Handle(option, selected); - if (option.IsDefault) + if (option.Group is not { } group) { - ImGuiUtil.DrawTableColumn(option.FullName); + ImGuiUtil.DrawTableColumn(option.GetFullName()); ImGui.TableNextColumn(); } else { - ImGuiUtil.DrawTableColumn(option.Name); - var group = option.Group; - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; + ImGuiUtil.DrawTableColumn(option.GetName()); + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, true); if (ImGui.MenuItem("Unselect All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, false); ImGui.EndPopup(); } } ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); continue; - void Handle(SubMod option2, bool selected2) + void Handle(IModDataContainer option2, bool selected2) { if (selected2) modMerger.SelectedOptions.Add(option2); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 0dc694d8..c05f1ac1 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -486,7 +486,7 @@ public class ModPanelEditTab( EditOption(panel, single, groupIdx, optionIdx); break; case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) EditOption(panel, multi, groupIdx, optionIdx); break; } @@ -542,7 +542,7 @@ public class ModPanelEditTab( if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -557,7 +557,7 @@ public class ModPanelEditTab( var count = group switch { SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.PrioritizedOptions.Count, + MultiModGroup multi => multi.OptionData.Count, _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), }; ImGui.TableNextColumn(); @@ -591,6 +591,9 @@ public class ModPanelEditTab( // Handle drag and drop to move options inside a group or into another group. private static void Source(IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var source = ImRaii.DragDropSource(); if (!source) return; @@ -606,6 +609,9 @@ public class ModPanelEditTab( private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var target = ImRaii.DragDropTarget(); if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; @@ -624,22 +630,12 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group switch - { - SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.PrioritizedOptions.Count, - _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), - }; - var (option, priority) = sourceGroup switch - { - SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default), - MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx], - _ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."), - }; + var currentCount = group.DataContainers.Count; + var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); }); } diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 02ec9a32..1aaa7741 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -114,7 +114,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((SubMod)mod.Default); + _modEditWindow.ChangeOption(mod.Default); _modEditWindow.IsOpen = true; } From 514121d8c133baf711c19a9ca1dfa585f6043f6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:28:12 +0200 Subject: [PATCH 1647/2451] Reorder stuff. --- Penumbra/Api/Api/ModSettingsApi.cs | 3 +- Penumbra/Api/Api/TemporaryApi.cs | 2 +- Penumbra/Api/TempModManager.cs | 2 +- .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Manager/CollectionEditor.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/ModCollectionMigration.cs | 2 +- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/Collections/ModCollectionSave.cs | 2 +- Penumbra/Communication/ModSettingChanged.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 4 +- Penumbra/Meta/MetaFileManager.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +- Penumbra/Mods/Editor/FileRegistry.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 3 +- Penumbra/Mods/Editor/ModEditor.cs | 3 +- Penumbra/Mods/Editor/ModFileCollection.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 3 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- .../Mods/{Subclasses => Groups}/IModGroup.cs | 86 +---- Penumbra/Mods/Groups/ModSaveGroup.cs | 57 +++ .../{Subclasses => Groups}/MultiModGroup.cs | 30 +- .../{Subclasses => Groups}/SingleModGroup.cs | 28 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 1 - Penumbra/Mods/Manager/ModMigration.cs | 4 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +- Penumbra/Mods/Mod.cs | 5 +- Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Mods/ModLocalData.cs | 20 +- Penumbra/Mods/ModMeta.cs | 24 +- .../{Subclasses => Settings}/ModPriority.cs | 2 +- .../{Subclasses => Settings}/ModSettings.cs | 5 +- .../Mods/{Subclasses => Settings}/Setting.cs | 2 +- .../{Subclasses => Settings}/SettingList.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 30 ++ .../IModDataContainer.cs | 27 +- .../{Subclasses => SubMods}/IModOption.cs | 8 +- Penumbra/Mods/SubMods/MultiSubMod.cs | 92 +++++ Penumbra/Mods/SubMods/SingleSubMod.cs | 82 ++++ Penumbra/Mods/Subclasses/SubMod.cs | 352 ------------------ Penumbra/Mods/TemporaryMod.cs | 6 +- Penumbra/Penumbra.csproj | 4 + Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/Services/SaveService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../ModEditWindow.QuickImport.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 1 - 58 files changed, 416 insertions(+), 539 deletions(-) rename Penumbra/Mods/{Subclasses => Groups}/IModGroup.cs (50%) create mode 100644 Penumbra/Mods/Groups/ModSaveGroup.cs rename Penumbra/Mods/{Subclasses => Groups}/MultiModGroup.cs (83%) rename Penumbra/Mods/{Subclasses => Groups}/SingleModGroup.cs (85%) rename Penumbra/Mods/{Subclasses => Settings}/ModPriority.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/ModSettings.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/Setting.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/SettingList.cs (97%) create mode 100644 Penumbra/Mods/SubMods/DefaultSubMod.cs rename Penumbra/Mods/{Subclasses => SubMods}/IModDataContainer.cs (80%) rename Penumbra/Mods/{Subclasses => SubMods}/IModOption.cs (72%) create mode 100644 Penumbra/Mods/SubMods/MultiSubMod.cs create mode 100644 Penumbra/Mods/SubMods/SingleSubMod.cs delete mode 100644 Penumbra/Mods/Subclasses/SubMod.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index e145e027..039fbfa9 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -8,8 +8,9 @@ using Penumbra.Communication; using Penumbra.Interop.PathResolving; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Api.Api; diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index b4ffa8f4..38d080cc 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -5,7 +5,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; namespace Penumbra.Api.Api; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 7d682338..aee2b447 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -6,7 +6,7 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Api; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 4b5c4337..c1296414 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,7 +8,7 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 4af19e6b..0243de1e 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -2,7 +2,7 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2da2a569..4e2fb7b7 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -5,7 +5,7 @@ using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 053f0a2b..89743aa2 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,6 +1,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.Util; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index e666b151..4580e37a 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,7 +1,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index acc38d83..e6bb069b 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Linq; using Penumbra.Services; using Newtonsoft.Json; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Collections; diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 968f78a7..a7da345b 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -4,7 +4,7 @@ using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Communication; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index b9cdda71..eb6d0b0c 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -3,7 +3,9 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Util; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 0e2e638b..cd99396b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -11,7 +11,7 @@ using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; using Penumbra.Services; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 92ec58b9..31aacbe1 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 44d349ce..a484c8c2 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,4 +1,4 @@ -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index c4c4be2f..d4c881e9 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 1118f890..e1c5962f 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,6 +1,7 @@ using OtterGui; using OtterGui.Compression; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index ede35914..551d04cf 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,5 +1,5 @@ using OtterGui; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 11e35334..00685c94 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,5 +1,5 @@ using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 541c84ae..0f629bc7 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -5,7 +5,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.ModsTab; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 88e48f0f..dee700d5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index db00a1c7..e2088b32 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -3,8 +3,9 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; using OtterGui.Tasks; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 64788cf3..3247cfdf 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,6 +1,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs similarity index 50% rename from Penumbra/Mods/Subclasses/IModGroup.cs rename to Penumbra/Mods/Groups/IModGroup.cs index 5c500793..dc5150cf 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -1,11 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Services; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; public interface ITexToolsGroup { @@ -16,22 +16,22 @@ public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public int AddOption(Mod mod, string name, string description = ""); - public IReadOnlyList Options { get; } + public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } - public bool IsOption { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); public int GetIndex(); @@ -88,67 +88,15 @@ public interface IModGroup public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) { var redirectionCount = 0; - var swapCount = 0; - var manipCount = 0; + var swapCount = 0; + var manipCount = 0; foreach (var option in group.DataContainers) { redirectionCount += option.Files.Count; - swapCount += option.FileSwaps.Count; - manipCount += option.Manipulations.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; } return (redirectionCount, swapCount, manipCount); } } - -public readonly struct ModSaveGroup : ISavable -{ - private readonly DirectoryInfo _basePath; - private readonly IModGroup? _group; - private readonly int _groupIdx; - private readonly DefaultSubMod? _defaultMod; - private readonly bool _onlyAscii; - - public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) - { - _basePath = mod.ModPath; - _groupIdx = groupIdx; - if (_groupIdx < 0) - _defaultMod = mod.Default; - else - _group = mod.Groups[_groupIdx]; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) - { - _basePath = basePath; - _group = group; - _groupIdx = groupIdx; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) - { - _basePath = basePath; - _groupIdx = -1; - _defaultMod = @default; - _onlyAscii = onlyAscii; - } - - public string ToFilename(FilenameService fileNames) - => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); - - public void Save(StreamWriter writer) - { - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - j.WriteStartObject(); - if (_groupIdx >= 0) - _group!.WriteJson(j, serializer); - else - IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); - j.WriteEndObject(); - } -} diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs new file mode 100644 index 00000000..ed81f42f --- /dev/null +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Groups; + +public readonly struct ModSaveGroup : ISavable +{ + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly DefaultSubMod? _defaultMod; + private readonly bool _onlyAscii; + + public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) + { + _basePath = mod.ModPath; + _groupIdx = groupIdx; + if (_groupIdx < 0) + _defaultMod = mod.Default; + else + _group = mod.Groups[_groupIdx]; + _onlyAscii = onlyAscii; + } + + public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) + { + _basePath = basePath; + _group = group; + _groupIdx = groupIdx; + _onlyAscii = onlyAscii; + } + + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) + { + _basePath = basePath; + _groupIdx = -1; + _defaultMod = @default; + _onlyAscii = onlyAscii; + } + + public string ToFilename(FilenameService fileNames) + => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); + if (_groupIdx >= 0) + _group!.WriteJson(j, serializer); + else + IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs similarity index 83% rename from Penumbra/Mods/Subclasses/MultiModGroup.cs rename to Penumbra/Mods/Groups/MultiModGroup.cs index f194350a..f39f2e70 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -6,9 +6,11 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; /// Groups that allow all available options to be selected at once. public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup @@ -16,11 +18,11 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; public IReadOnlyList Options @@ -45,7 +47,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup var subMod = new MultiSubMod(mod, this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); @@ -56,9 +58,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { var ret = new MultiModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -93,9 +95,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup case GroupType.Single: var single = new SingleModGroup(Mod) { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); @@ -152,7 +154,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => IModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << OptionData.Count) - 1)); + => new(setting.Value & (1ul << OptionData.Count) - 1); /// Create a group without a mod only for saving it in the creator. internal static MultiModGroup CreateForSaving(string name) diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs similarity index 85% rename from Penumbra/Mods/Subclasses/SingleModGroup.cs rename to Penumbra/Mods/Groups/SingleModGroup.cs index d1a3b6d1..6011185a 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -4,9 +4,11 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; /// Groups that allow only one of their available options to be selected. public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup @@ -14,11 +16,11 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -34,7 +36,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { var subMod = new SingleSubMod(mod, this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); @@ -55,9 +57,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup var options = json["Options"]; var ret = new SingleModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -82,9 +84,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup case GroupType.Multi: var multi = new MultiModGroup(Mod) { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 21b9ef2c..1545811e 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -7,7 +7,7 @@ using Penumbra.String.Classes; using Penumbra.Meta; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Mods.ItemSwap; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 4f9e8648..3ff1a333 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,7 +2,6 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8c4a5674..f160d5bd 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -2,7 +2,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 4d3a5717..b66fec17 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -4,7 +4,9 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 5c02213e..fc84afcc 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,9 +1,10 @@ using OtterGui; using OtterGui.Classes; -using Penumbra.Collections.Cache; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index c1236037..e8113ee1 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -9,8 +9,10 @@ using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index 51fe8d58..beda0dc7 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -5,29 +5,25 @@ using Penumbra.Services; namespace Penumbra.Mods; -public readonly struct ModLocalData : ISavable +public readonly struct ModLocalData(Mod mod) : ISavable { public const int FileVersion = 3; - private readonly Mod _mod; - - public ModLocalData(Mod mod) - => _mod = mod; - public string ToFilename(FilenameService fileNames) - => fileNames.LocalDataFile(_mod); + => fileNames.LocalDataFile(mod); public void Save(StreamWriter writer) { var jObject = new JObject { { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, - { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, - { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, - { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, + { nameof(Mod.ImportDate), JToken.FromObject(mod.ImportDate) }, + { nameof(Mod.LocalTags), JToken.FromObject(mod.LocalTags) }, + { nameof(Mod.Note), JToken.FromObject(mod.Note) }, + { nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) }, }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index d29cdb9c..870d6d4f 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -4,31 +4,27 @@ using Penumbra.Services; namespace Penumbra.Mods; -public readonly struct ModMeta : ISavable +public readonly struct ModMeta(Mod mod) : ISavable { public const uint FileVersion = 3; - private readonly Mod _mod; - - public ModMeta(Mod mod) - => _mod = mod; - public string ToFilename(FilenameService fileNames) - => fileNames.ModMetaPath(_mod); + => fileNames.ModMetaPath(mod); public void Save(StreamWriter writer) { var jObject = new JObject { { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, - { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, - { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, - { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, - { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, - { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, + { nameof(Mod.Name), JToken.FromObject(mod.Name) }, + { nameof(Mod.Author), JToken.FromObject(mod.Author) }, + { nameof(Mod.Description), JToken.FromObject(mod.Description) }, + { nameof(Mod.Version), JToken.FromObject(mod.Version) }, + { nameof(Mod.Website), JToken.FromObject(mod.Website) }, + { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } } diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Settings/ModPriority.cs similarity index 98% rename from Penumbra/Mods/Subclasses/ModPriority.cs rename to Penumbra/Mods/Settings/ModPriority.cs index a99c12ed..993bd577 100644 --- a/Penumbra/Mods/Subclasses/ModPriority.cs +++ b/Penumbra/Mods/Settings/ModPriority.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; [JsonConverter(typeof(Converter))] public readonly record struct ModPriority(int Value) : diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs similarity index 98% rename from Penumbra/Mods/Subclasses/ModSettings.cs rename to Penumbra/Mods/Settings/ModSettings.cs index 2ddabdb8..db9e0521 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -1,12 +1,11 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; /// Contains the settings for a given mod. public class ModSettings diff --git a/Penumbra/Mods/Subclasses/Setting.cs b/Penumbra/Mods/Settings/Setting.cs similarity index 98% rename from Penumbra/Mods/Subclasses/Setting.cs rename to Penumbra/Mods/Settings/Setting.cs index 18b1e4ca..231529b8 100644 --- a/Penumbra/Mods/Subclasses/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OtterGui; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; [JsonConverter(typeof(Converter))] public readonly record struct Setting(ulong Value) diff --git a/Penumbra/Mods/Subclasses/SettingList.cs b/Penumbra/Mods/Settings/SettingList.cs similarity index 97% rename from Penumbra/Mods/Subclasses/SettingList.cs rename to Penumbra/Mods/Settings/SettingList.cs index ea1e447f..67b1b947 100644 --- a/Penumbra/Mods/Subclasses/SettingList.cs +++ b/Penumbra/Mods/Settings/SettingList.cs @@ -1,4 +1,4 @@ -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; public class SettingList : List { diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs new file mode 100644 index 00000000..ced0cd0d --- /dev/null +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class DefaultSubMod(IMod mod) : IModDataContainer +{ + public const string FullName = "Default Option"; + + internal readonly IMod Mod = mod; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup? IModDataContainer.Group + => null; + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (-1, 0); +} diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs similarity index 80% rename from Penumbra/Mods/Subclasses/IModDataContainer.cs rename to Penumbra/Mods/SubMods/IModDataContainer.cs index a26beb2a..18b3b23f 100644 --- a/Penumbra/Mods/Subclasses/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -2,18 +2,19 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.SubMods; public interface IModDataContainer { - public IMod Mod { get; } + public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } public void AddDataTo(Dictionary redirections, HashSet manipulations) { @@ -28,24 +29,24 @@ public interface IModDataContainer public string GetName() => this switch { - IModOption o => o.FullName, + IModOption o => o.FullName, DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public string GetFullName() => this switch { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public static void Clone(IModDataContainer from, IModDataContainer to) { - to.Files = new Dictionary(from.Files); - to.FileSwaps = new Dictionary(from.FileSwaps); + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); to.Manipulations = [.. from.Manipulations]; } @@ -126,3 +127,5 @@ public interface IModDataContainer } } } + +public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs similarity index 72% rename from Penumbra/Mods/Subclasses/IModOption.cs rename to Penumbra/Mods/SubMods/IModOption.cs index f66ce44e..f1ce1d4c 100644 --- a/Penumbra/Mods/Subclasses/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,17 +1,17 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.SubMods; public interface IModOption { - public string Name { get; set; } - public string FullName { get; } + public string Name { get; set; } + public string FullName { get; } public string Description { get; set; } public static void Load(JToken json, IModOption option) { - option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; } diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs new file mode 100644 index 00000000..00216b77 --- /dev/null +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption +{ + internal readonly Mod Mod = mod; + internal readonly MultiModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + public ModPriority Priority { get; set; } = ModPriority.Default; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + + public MultiSubMod(Mod mod, MultiModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + } + + public MultiSubMod Clone(Mod mod, MultiModGroup group) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = Priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) + => new(null!, null!) + { + Name = name, + Description = description, + Priority = priority, + }; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs new file mode 100644 index 00000000..499ab192 --- /dev/null +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption +{ + internal readonly Mod Mod = mod; + internal readonly SingleModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + public SingleSubMod(Mod mod, SingleModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + } + + public SingleSubMod Clone(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs deleted file mode 100644 index a2425eb7..00000000 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ /dev/null @@ -1,352 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Subclasses; - -public interface IModDataOption : IModOption, IModDataContainer; - -public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption -{ - internal readonly Mod Mod = mod; - internal readonly SingleModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - public SingleSubMod(Mod mod, SingleModGroup group, JToken json) - : this(mod, group) - { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); - } - - public SingleSubMod Clone(Mod mod, SingleModGroup group) - { - var ret = new SingleSubMod(mod, group) - { - Name = Name, - Description = Description, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) - { - var ret = new MultiSubMod(mod, group) - { - Name = Name, - Description = Description, - Priority = priority, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} - -public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption -{ - internal readonly Mod Mod = mod; - internal readonly MultiModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - public ModPriority Priority { get; set; } = ModPriority.Default; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) - : this(mod, group) - { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); - Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; - } - - public MultiSubMod Clone(Mod mod, MultiModGroup group) - { - var ret = new MultiSubMod(mod, group) - { - Name = Name, - Description = Description, - Priority = Priority, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) - { - var ret = new SingleSubMod(mod, group) - { - Name = Name, - Description = Description, - }; - IModDataContainer.Clone(this, ret); - return ret; - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) - => new(null!, null!) - { - Name = name, - Description = description, - Priority = priority, - }; - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} - -public class DefaultSubMod(IMod mod) : IModDataContainer -{ - public const string FullName = "Default Option"; - - public string Description - => string.Empty; - - internal readonly IMod Mod = mod; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup? IModDataContainer.Group - => null; - - - public DefaultSubMod(Mod mod, JToken json) - : this(mod) - { - IModDataContainer.Load(json, this, mod.ModPath); - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (-1, 0); -} - - -//public sealed class SubMod(IMod mod, IModGroup group) : IModOption -//{ -// public string Name { get; set; } = "Default"; -// -// public string FullName -// => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; -// -// public string Description { get; set; } = string.Empty; -// -// internal readonly IMod Mod = mod; -// internal readonly IModGroup? Group = group; -// -// internal (int GroupIdx, int OptionIdx) GetIndices() -// { -// if (IsDefault) -// return (-1, 0); -// -// var groupIdx = Mod.Groups.IndexOf(Group); -// if (groupIdx < 0) -// throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); -// -// return (groupIdx, GetOptionIndex()); -// } -// -// private int GetOptionIndex() -// { -// var optionIndex = Group switch -// { -// null => 0, -// SingleModGroup single => single.OptionData.IndexOf(this), -// MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this), -// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), -// }; -// if (optionIndex < 0) -// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); -// -// return optionIndex; -// } -// -// public static SubMod CreateDefault(IMod mod) -// => new(mod, null!); -// -// [MemberNotNullWhen(false, nameof(Group))] -// public bool IsDefault -// => Group == null; -// -// public void AddData(Dictionary redirections, HashSet manipulations) -// { -// foreach (var (path, file) in Files) -// redirections.TryAdd(path, file); -// -// foreach (var (path, file) in FileSwaps) -// redirections.TryAdd(path, file); -// manipulations.UnionWith(Manipulations); -// } -// -// public Dictionary FileData = []; -// public Dictionary FileSwapData = []; -// public HashSet ManipulationData = []; -// -// public IReadOnlyDictionary Files -// => FileData; -// -// public IReadOnlyDictionary FileSwaps -// => FileSwapData; -// -// public IReadOnlySet Manipulations -// => ManipulationData; -// -// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) -// { -// FileData.Clear(); -// FileSwapData.Clear(); -// ManipulationData.Clear(); -// -// // Every option has a name, but priorities are only relevant for multi group options. -// Name = json[nameof(Name)]?.ToObject() ?? string.Empty; -// Description = json[nameof(Description)]?.ToObject() ?? string.Empty; -// priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; -// -// var files = (JObject?)json[nameof(Files)]; -// if (files != null) -// foreach (var property in files.Properties()) -// { -// if (Utf8GamePath.FromString(property.Name, out var p, true)) -// FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); -// } -// -// var swaps = (JObject?)json[nameof(FileSwaps)]; -// if (swaps != null) -// foreach (var property in swaps.Properties()) -// { -// if (Utf8GamePath.FromString(property.Name, out var p, true)) -// FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); -// } -// -// var manips = json[nameof(Manipulations)]; -// if (manips != null) -// foreach (var s in manips.Children().Select(c => c.ToObject()) -// .Where(m => m.Validate())) -// ManipulationData.Add(s); -// } -// -// -// /// Create a sub mod without a mod or group only for saving it in the creator. -// internal static SubMod CreateForSaving(string name) -// => new(null!, null!) -// { -// Name = name, -// }; -// -// -// public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) -// { -// j.WriteStartObject(); -// j.WritePropertyName(nameof(Name)); -// j.WriteValue(mod.Name); -// j.WritePropertyName(nameof(Description)); -// j.WriteValue(mod.Description); -// if (priority != null) -// { -// j.WritePropertyName(nameof(IModGroup.Priority)); -// j.WriteValue(priority.Value.Value); -// } -// -// j.WritePropertyName(nameof(mod.Files)); -// j.WriteStartObject(); -// foreach (var (gamePath, file) in mod.Files) -// { -// if (file.ToRelPath(basePath, out var relPath)) -// { -// j.WritePropertyName(gamePath.ToString()); -// j.WriteValue(relPath.ToString()); -// } -// } -// -// j.WriteEndObject(); -// j.WritePropertyName(nameof(mod.FileSwaps)); -// j.WriteStartObject(); -// foreach (var (gamePath, file) in mod.FileSwaps) -// { -// j.WritePropertyName(gamePath.ToString()); -// j.WriteValue(file.ToString()); -// } -// -// j.WriteEndObject(); -// j.WritePropertyName(nameof(mod.Manipulations)); -// serializer.Serialize(j, mod.Manipulations); -// j.WriteEndObject(); -// } -//} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 393369d4..d08c8b06 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -2,8 +2,10 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; @@ -46,7 +48,7 @@ public class TemporaryMod : IMod => Array.Empty(); public TemporaryMod() - => Default = new(this); + => Default = new DefaultSubMod(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) => Default.Files[gamePath] = fullPath; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index c8961579..0ec1fd44 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -93,6 +93,10 @@ + + + + diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index e775d81a..fafaa0e5 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -10,7 +10,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.UI.ResourceWatcher; diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 801e0c1d..8d3cb641 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -2,7 +2,7 @@ using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 5125a5b2..77bdb161 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -13,9 +13,10 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 94f1d577..107c56e6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -4,7 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 70854fe7..55b7e748 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -8,7 +8,7 @@ using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 21d14f78..dbb88fb7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -21,7 +21,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index bed31ab8..5dad66b4 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -4,7 +4,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 11a2d96f..5b6cfa99 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,7 +14,7 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; using MessageService = Penumbra.Services.MessageService; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index b2cfaf25..5e3aac48 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -9,7 +9,7 @@ using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index c05f1ac1..afbef45d 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -11,9 +11,10 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index cb76088c..1326a763 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -8,8 +8,10 @@ using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.Services; +using Penumbra.Mods.SubMods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 1aaa7741..8b09d8b9 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -5,7 +5,6 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; From cd76c31d8ce73402b40e45d73dace053cc8a28b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:41:55 +0200 Subject: [PATCH 1648/2451] Fix stack overflow. --- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 8 ++++---- Penumbra/Mods/SubMods/MultiSubMod.cs | 2 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index ced0cd0d..8b166505 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -23,7 +23,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer => null; public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 18b3b23f..c9420821 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -16,14 +16,14 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public void AddDataTo(Dictionary redirections, HashSet manipulations) + public static void AddDataTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) { - foreach (var (path, file) in Files) + foreach (var (path, file) in container.Files) redirections.TryAdd(path, file); - foreach (var (path, file) in FileSwaps) + foreach (var (path, file) in container.FileSwaps) redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); + manipulations.UnionWith(container.Manipulations); } public string GetName() diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 00216b77..12dfcada 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -65,7 +65,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 499ab192..ba91e271 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -63,7 +63,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); From 72db023804f1d7e529b2bca49e5293b884469432 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 17:58:32 +0200 Subject: [PATCH 1649/2451] Some cleanup. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 6 +- Penumbra/Mods/Groups/SingleModGroup.cs | 6 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +- Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/ModCreator.cs | 40 +++++--- Penumbra/Mods/SubMods/DefaultSubMod.cs | 6 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 105 ++------------------- Penumbra/Mods/SubMods/IModOption.cs | 21 +---- Penumbra/Mods/SubMods/MultiSubMod.cs | 10 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 10 +- Penumbra/Mods/SubMods/SubModHelpers.cs | 105 +++++++++++++++++++++ 12 files changed, 163 insertions(+), 154 deletions(-) create mode 100644 Penumbra/Mods/SubMods/SubModHelpers.cs diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index ed81f42f..e87fc012 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -51,7 +51,7 @@ public readonly struct ModSaveGroup : ISavable if (_groupIdx >= 0) _group!.WriteJson(j, serializer); else - IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index f39f2e70..1c8c769c 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -54,7 +54,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup return OptionData.Count - 1; } - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) + public static MultiModGroup? Load(Mod mod, JObject json) { var ret = new MultiModGroup(mod) { @@ -140,10 +140,10 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup jWriter.WriteStartArray(); foreach (var option in OptionData) { - IModOption.WriteModOption(jWriter, option); + SubModHelpers.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); - IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 6011185a..11542968 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -52,7 +52,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public bool IsOption => OptionData.Count > 1; - public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) + public static SingleModGroup? Load(Mod mod, JObject json) { var options = json["Options"]; var ret = new SingleModGroup(mod) @@ -144,8 +144,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup jWriter.WriteStartArray(); foreach (var option in OptionData) { - IModOption.WriteModOption(jWriter, option); - IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubModHelpers.WriteModOption(jWriter, option); + SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index b66fec17..a02c8d68 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -251,7 +251,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Description = option.Description, }; if (option is IModDataContainer data) - IModDataContainer.Clone(data, newOption); + SubModHelpers.Clone(data, newOption); s.OptionData.Add(newOption); break; } @@ -265,7 +265,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, }; if (option is IModDataContainer data) - IModDataContainer.Clone(data, newOption); + SubModHelpers.Clone(data, newOption); m.OptionData.Add(newOption); break; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index fc84afcc..6f6eb8ce 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -78,7 +78,7 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - Default.AddDataTo(dictRedirections, setManips); + Default.AddTo(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index e8113ee1..0626bc9d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -115,7 +115,7 @@ public partial class ModCreator( try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); - IModDataContainer.Load(jObject, mod.Default, mod.ModPath); + SubModHelpers.LoadDataContainer(jObject, mod.Default, mod.ModPath); } catch (Exception e) { @@ -162,7 +162,7 @@ public partial class ModCreator( deleteList.AddRange(localDeleteList); } - IModDataContainer.DeleteDeleteList(deleteList, delete); + DeleteDeleteList(deleteList, delete); if (!changes) return; @@ -221,7 +221,7 @@ public partial class ModCreator( } } - IModDataContainer.DeleteDeleteList(deleteList, delete); + DeleteDeleteList(deleteList, delete); return (oldSize < option.Manipulations.Count, deleteList); } @@ -392,10 +392,8 @@ public partial class ModCreator( Penumbra.Log.Debug($"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName(oldPath)} after split."); using (var oldFile = File.CreateText(oldPath)) { - using var j = new JsonTextWriter(oldFile) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(oldFile); + j.Formatting = Formatting.Indented; json.WriteTo(j); } @@ -403,10 +401,8 @@ public partial class ModCreator( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName(newPath)} after split."); using (var newFile = File.CreateText(newPath)) { - using var j = new JsonTextWriter(newFile) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(newFile); + j.Formatting = Formatting.Indented; clone.WriteTo(j); } @@ -436,8 +432,8 @@ public partial class ModCreator( var json = JObject.Parse(File.ReadAllText(file.FullName)); switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) { - case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx); - case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx); + case GroupType.Multi: return MultiModGroup.Load(mod, json); + case GroupType.Single: return SingleModGroup.Load(mod, json); } } catch (Exception e) @@ -446,5 +442,23 @@ public partial class ModCreator( } return null; + } + + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) + { + if (!delete) + return; + + foreach (var file in deleteList) + { + try + { + File.Delete(file); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); + } + } } } diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 8b166505..8b00e2ae 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -22,9 +22,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer IModGroup? IModDataContainer.Group => null; - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + public void AddTo(Dictionary redirections, HashSet manipulations) + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); -} +} diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index c9420821..1e676816 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -1,5 +1,3 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -7,6 +5,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; + public interface IModDataContainer { public IMod Mod { get; } @@ -16,116 +15,24 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public static void AddDataTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in container.Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in container.FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(container.Manipulations); - } - public string GetName() => this switch { - IModOption o => o.FullName, + IModOption o => o.FullName, DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public string GetFullName() => this switch { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; - public static void Clone(IModDataContainer from, IModDataContainer to) - { - to.Files = new Dictionary(from.Files); - to.FileSwaps = new Dictionary(from.FileSwaps); - to.Manipulations = [.. from.Manipulations]; - } - public (int GroupIndex, int DataIndex) GetDataIndices(); - - public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) - { - data.Files.Clear(); - data.FileSwaps.Clear(); - data.Manipulations.Clear(); - - var files = (JObject?)json[nameof(Files)]; - if (files != null) - foreach (var property in files.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); - } - - var swaps = (JObject?)json[nameof(FileSwaps)]; - if (swaps != null) - foreach (var property in swaps.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); - } - - var manips = json[nameof(Manipulations)]; - if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - data.Manipulations.Add(s); - } - - public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) - { - j.WritePropertyName(nameof(data.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.Manipulations)); - serializer.Serialize(j, data.Manipulations); - j.WriteEndObject(); - } - - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) - { - if (!delete) - return; - - foreach (var file in deleteList) - { - try - { - File.Delete(file); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); - } - } - } } public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/SubMods/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs index f1ce1d4c..83d632a0 100644 --- a/Penumbra/Mods/SubMods/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,27 +1,10 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - namespace Penumbra.Mods.SubMods; public interface IModOption { - public string Name { get; set; } - public string FullName { get; } + public string Name { get; set; } + public string FullName { get; } public string Description { get; set; } - public static void Load(JToken json, IModOption option) - { - option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; - option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; - } - public (int GroupIndex, int OptionIndex) GetOptionIndices(); - - public static void WriteModOption(JsonWriter j, IModOption option) - { - j.WritePropertyName(nameof(Name)); - j.WriteValue(option.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(option.Description); - } } diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 12dfcada..ccb787a5 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -35,8 +35,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption public MultiSubMod(Mod mod, MultiModGroup group, JToken json) : this(mod, group) { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); + SubModHelpers.LoadOptionData(json, this); + SubModHelpers.LoadDataContainer(json, this, mod.ModPath); Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; } @@ -48,7 +48,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption Description = Description, Priority = Priority, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } @@ -60,12 +60,12 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption Name = Name, Description = Description, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index ba91e271..e6740a47 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -33,8 +33,8 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption public SingleSubMod(Mod mod, SingleModGroup group, JToken json) : this(mod, group) { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); + SubModHelpers.LoadOptionData(json, this); + SubModHelpers.LoadDataContainer(json, this, mod.ModPath); } public SingleSubMod Clone(Mod mod, SingleModGroup group) @@ -44,7 +44,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption Name = Name, Description = Description, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } @@ -57,13 +57,13 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption Description = Description, Priority = priority, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); diff --git a/Penumbra/Mods/SubMods/SubModHelpers.cs b/Penumbra/Mods/SubMods/SubModHelpers.cs new file mode 100644 index 00000000..9992b6e8 --- /dev/null +++ b/Penumbra/Mods/SubMods/SubModHelpers.cs @@ -0,0 +1,105 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.ItemSwap; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public static class SubModHelpers +{ + /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. + public static void AddContainerTo(IModDataContainer container, Dictionary redirections, + HashSet manipulations) + { + foreach (var (path, file) in container.Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in container.FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(container.Manipulations); + } + + /// Replace all data of with the data of . + public static void Clone(IModDataContainer from, IModDataContainer to) + { + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); + to.Manipulations = [.. from.Manipulations]; + } + + /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. + public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) + { + data.Files.Clear(); + data.FileSwaps.Clear(); + data.Manipulations.Clear(); + + var files = (JObject?)json[nameof(data.Files)]; + if (files != null) + foreach (var property in files.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); + } + + var swaps = (JObject?)json[nameof(data.FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); + } + + var manips = json[nameof(data.Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.Validate())) + data.Manipulations.Add(s); + } + + /// Load the relevant data for a selectable option from a JToken of that option. + public static void LoadOptionData(JToken json, IModOption option) + { + option.Name = json[nameof(option.Name)]?.ToObject() ?? string.Empty; + option.Description = json[nameof(option.Description)]?.ToObject() ?? string.Empty; + } + + /// Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. + public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) + { + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); + j.WriteEndObject(); + } + + /// Write the data for a selectable mod option on a JsonWriter. + public static void WriteModOption(JsonWriter j, IModOption option) + { + j.WritePropertyName(nameof(option.Name)); + j.WriteValue(option.Name); + j.WritePropertyName(nameof(option.Description)); + j.WriteValue(option.Description); + } +} From 0fd14ffefc11475920e7a7f1cf00c70e8ba46e22 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 18:14:21 +0200 Subject: [PATCH 1650/2451] More cleanup. --- Penumbra/Mods/SubMods/DefaultSubMod.cs | 7 ++- Penumbra/Mods/SubMods/IModDataContainer.cs | 21 +------ Penumbra/Mods/SubMods/MultiSubMod.cs | 64 ++++------------------ Penumbra/Mods/SubMods/OptionSubMod.cs | 57 +++++++++++++++++++ Penumbra/Mods/SubMods/SingleSubMod.cs | 64 ++++------------------ Penumbra/Mods/SubMods/SubModHelpers.cs | 5 +- 6 files changed, 89 insertions(+), 129 deletions(-) create mode 100644 Penumbra/Mods/SubMods/OptionSubMod.cs diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 8b00e2ae..1eddcef6 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -25,6 +24,12 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public void AddTo(Dictionary redirections, HashSet manipulations) => SubModHelpers.AddContainerTo(this, redirections, manipulations); + public string GetName() + => FullName; + + public string GetFullName() + => FullName; + public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); } diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 1e676816..a6ab491f 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -15,24 +15,7 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public string GetName() - => this switch - { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", - }; - - public string GetFullName() - => this switch - { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, - _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", - }; - + public string GetName(); + public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } - -public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index ccb787a5..c43d4b9e 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -1,37 +1,13 @@ -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - -public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption + +namespace Penumbra.Mods.SubMods; + +public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(mod, group) { - internal readonly Mod Mod = mod; - internal readonly MultiModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } = ModPriority.Default; - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) : this(mod, group) { @@ -44,9 +20,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { var ret = new MultiSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, - Priority = Priority, + Priority = Priority, }; SubModHelpers.Clone(this, ret); @@ -57,36 +33,18 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { var ret = new SingleSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, }; SubModHelpers.Clone(this, ret); return ret; } - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); - public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) { - Name = name, + Name = name, Description = description, - Priority = priority, + Priority = priority, }; - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} +} diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs new file mode 100644 index 00000000..79c50e51 --- /dev/null +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -0,0 +1,57 @@ +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public interface IModDataOption : IModDataContainer, IModOption; + +public abstract class OptionSubMod(Mod mod, T group) : IModDataOption + where T : IModGroup +{ + internal readonly Mod Mod = mod; + internal readonly IModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group!.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => SubModHelpers.AddContainerTo(this, redirections, manipulations); + + public string GetName() + => Name; + + public string GetFullName() + => FullName; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index e6740a47..5d68e401 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -1,37 +1,13 @@ -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - -public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption + +namespace Penumbra.Mods.SubMods; + +public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod(mod, singleGroup) { - internal readonly Mod Mod = mod; - internal readonly SingleModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - public SingleSubMod(Mod mod, SingleModGroup group, JToken json) - : this(mod, group) + public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) + : this(mod, singleGroup) { SubModHelpers.LoadOptionData(json, this); SubModHelpers.LoadDataContainer(json, this, mod.ModPath); @@ -41,7 +17,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { var ret = new SingleSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, }; SubModHelpers.Clone(this, ret); @@ -53,30 +29,12 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { var ret = new MultiSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, - Priority = priority, + Priority = priority, }; SubModHelpers.Clone(this, ret); return ret; } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} +} diff --git a/Penumbra/Mods/SubMods/SubModHelpers.cs b/Penumbra/Mods/SubMods/SubModHelpers.cs index 9992b6e8..2a09fbc3 100644 --- a/Penumbra/Mods/SubMods/SubModHelpers.cs +++ b/Penumbra/Mods/SubMods/SubModHelpers.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.ItemSwap; using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; @@ -92,9 +91,9 @@ public static class SubModHelpers j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); j.WriteEndObject(); - } + } - /// Write the data for a selectable mod option on a JsonWriter. + /// Write the data for a selectable mod option on a JsonWriter. public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(option.Name)); From 06953c175dbb1e733612c02e8bf815ed82247d29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 18:18:57 +0200 Subject: [PATCH 1651/2451] mooooore --- Penumbra/Mods/Groups/IModGroup.cs | 15 --------------- Penumbra/Mods/Groups/ModSaveGroup.cs | 15 +++++++++++++++ Penumbra/Mods/Groups/MultiModGroup.cs | 4 +++- Penumbra/Mods/Groups/SingleModGroup.cs | 4 +++- Penumbra/Mods/SubMods/DefaultSubMod.cs | 20 ++++++++++---------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index dc5150cf..0cabc9f3 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -68,21 +68,6 @@ public interface IModGroup return true; } - public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) - { - jWriter.WriteStartObject(); - jWriter.WritePropertyName(nameof(group.Name)); - jWriter.WriteValue(group!.Name); - jWriter.WritePropertyName(nameof(group.Description)); - jWriter.WriteValue(group.Description); - jWriter.WritePropertyName(nameof(group.Priority)); - jWriter.WriteValue(group.Priority.Value); - jWriter.WritePropertyName(nameof(group.Type)); - jWriter.WriteValue(group.Type.ToString()); - jWriter.WritePropertyName(nameof(group.DefaultSettings)); - jWriter.WriteValue(group.DefaultSettings.Value); - } - public (int Redirections, int Swaps, int Manips) GetCounts(); public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index e87fc012..92ccb36e 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -54,4 +54,19 @@ public readonly struct ModSaveGroup : ISavable SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } + + public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) + { + jWriter.WriteStartObject(); + jWriter.WritePropertyName(nameof(group.Name)); + jWriter.WriteValue(group!.Name); + jWriter.WritePropertyName(nameof(group.Description)); + jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Priority)); + jWriter.WriteValue(group.Priority.Value); + jWriter.WritePropertyName(nameof(group.Type)); + jWriter.WriteValue(group.Type.ToString()); + jWriter.WritePropertyName(nameof(group.DefaultSettings)); + jWriter.WriteValue(group.DefaultSettings.Value); + } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 1c8c769c..7e900ef5 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -135,15 +135,17 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { - IModGroup.WriteJsonBase(jWriter, this); + ModSaveGroup.WriteJsonBase(jWriter, this); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) { + jWriter.WriteStartObject(); SubModHelpers.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 11542968..6aa9160e 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -139,13 +139,15 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { - IModGroup.WriteJsonBase(jWriter, this); + ModSaveGroup.WriteJsonBase(jWriter, this); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) { + jWriter.WriteStartObject(); SubModHelpers.WriteModOption(jWriter, option); SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1eddcef6..980b805d 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -1,19 +1,19 @@ -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Groups; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + public class DefaultSubMod(IMod mod) : IModDataContainer { public const string FullName = "Default Option"; internal readonly IMod Mod = mod; - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; IMod IModDataContainer.Mod => Mod; From a72be22d3b295250716408fa0d01976f44a6e7b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:56:06 +0200 Subject: [PATCH 1652/2451] Make sure HS image does not displace the settings entirely. --- Penumbra/UI/ModsTab/ModPanelHeader.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 05f47809..a8b393b1 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -18,6 +18,7 @@ public class ModPanelHeader : IDisposable private readonly IFontHandle _nameFont; private readonly CommunicatorService _communicator; + private float _lastPreSettingsHeight = 0; public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) { @@ -32,6 +33,11 @@ public class ModPanelHeader : IDisposable /// public void Draw() { + var height = ImGui.GetContentRegionAvail().Y; + var maxHeight = 3 * height / 4; + using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers + ? ImRaii.Child("HeaderChild", new Vector2(ImGui.GetContentRegionAvail().X, maxHeight), false) + : null; using (ImRaii.Group()) { var offset = DrawModName(); @@ -40,6 +46,7 @@ public class ModPanelHeader : IDisposable } _communicator.PreSettingsTabBarDraw.Invoke(_mod.Identifier, ImGui.GetItemRectSize().X, _nameWidth); + _lastPreSettingsHeight = ImGui.GetCursorPosY(); } /// @@ -48,6 +55,7 @@ public class ModPanelHeader : IDisposable /// public void UpdateModData(Mod mod) { + _lastPreSettingsHeight = 0; _mod = mod; // Name var name = $" {mod.Name} "; From e40c4999b6d18deab2bf98a60cd9cf5d1ac2b198 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:56:36 +0200 Subject: [PATCH 1653/2451] Improve collection migration maybe. --- .../Collections/Manager/CollectionStorage.cs | 30 +++++++++++++++---- .../Manager/ModCollectionMigration.cs | 2 -- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 4e2fb7b7..1fe5b227 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -184,21 +184,39 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (version >= 2) { - File.Move(file.FullName, correctName, false); - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", - NotificationType.Warning); + try + { + File.Move(file.FullName, correctName, false); + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + NotificationType.Warning); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}", + NotificationType.Warning); + } } else { _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); - File.Delete(file.FullName); - Penumbra.Log.Information($"Migrated collection {name} to Guid {id}."); + try + { + File.Move(file.FullName, file.FullName + ".bak", true); + Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file."); + } + catch (Exception ex) + { + Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}"); + } } } catch (Exception e) { Penumbra.Messager.NotificationMessage(e, - $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", NotificationType.Error); + $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", + NotificationType.Error); } _collections.Add(collection); diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 89743aa2..fe61285d 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,8 +1,6 @@ -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Collections.Manager; From 297be487b50066c4a79c4cff03af18fa452b0672 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:57:09 +0200 Subject: [PATCH 1654/2451] More cleanup on groups. --- .../Communication/PreSettingsTabBarDraw.cs | 3 +- Penumbra/Mods/Groups/IModGroup.cs | 61 +++---------------- Penumbra/Mods/Groups/ModGroup.cs | 42 +++++++++++++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 6 +- Penumbra/Mods/Groups/SingleModGroup.cs | 6 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 42 +++++-------- Penumbra/Mods/Manager/ModStorage.cs | 6 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/MultiSubMod.cs | 8 +-- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 8 +-- .../SubMods/{SubModHelpers.cs => SubMod.cs} | 19 +++++- 14 files changed, 107 insertions(+), 102 deletions(-) create mode 100644 Penumbra/Mods/Groups/ModGroup.cs rename Penumbra/Mods/SubMods/{SubModHelpers.cs => SubMod.cs} (87%) diff --git a/Penumbra/Communication/PreSettingsTabBarDraw.cs b/Penumbra/Communication/PreSettingsTabBarDraw.cs index 8614bbbe..e1d67297 100644 --- a/Penumbra/Communication/PreSettingsTabBarDraw.cs +++ b/Penumbra/Communication/PreSettingsTabBarDraw.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Api; +using Penumbra.Api.IpcSubscribers; namespace Penumbra.Communication; @@ -15,7 +16,7 @@ public sealed class PreSettingsTabBarDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 0cabc9f3..b13799cd 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -16,22 +16,22 @@ public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; } + public string Description { get; set; } + public GroupType Type { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public int AddOption(Mod mod, string name, string description = ""); - public IReadOnlyList Options { get; } + public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } - public bool IsOption { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); public int GetIndex(); @@ -42,46 +42,5 @@ public interface IModGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null); - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= Options.Count) - return false; - - var option = Options[optionIndex]; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= Options.Count) - return false; - - var option = Options[optionIndex]; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - public (int Redirections, int Swaps, int Manips) GetCounts(); - - public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) - { - var redirectionCount = 0; - var swapCount = 0; - var manipCount = 0; - foreach (var option in group.DataContainers) - { - redirectionCount += option.Files.Count; - swapCount += option.FileSwaps.Count; - manipCount += option.Manipulations.Count; - } - - return (redirectionCount, swapCount, manipCount); - } } diff --git a/Penumbra/Mods/Groups/ModGroup.cs b/Penumbra/Mods/Groups/ModGroup.cs new file mode 100644 index 00000000..da302714 --- /dev/null +++ b/Penumbra/Mods/Groups/ModGroup.cs @@ -0,0 +1,42 @@ +using Penumbra.Api.Enums; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.Groups; + +public static class ModGroup +{ + public static IModGroup Create(Mod mod, GroupType type, string name) + { + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + return type switch + { + GroupType.Single => new SingleModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, + GroupType.Multi => new MultiModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + } + + + public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) + { + var redirectionCount = 0; + var swapCount = 0; + var manipCount = 0; + foreach (var option in group.DataContainers) + { + redirectionCount += option.Files.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; + } + + return (redirectionCount, swapCount, manipCount); + } +} diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 92ccb36e..332879cb 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -51,7 +51,7 @@ public readonly struct ModSaveGroup : ISavable if (_groupIdx >= 0) _group!.WriteJson(j, serializer); else - SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); + SubMod.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7e900ef5..6b352f66 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -141,10 +141,10 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup foreach (var option in OptionData) { jWriter.WriteStartObject(); - SubModHelpers.WriteModOption(jWriter, option); + SubMod.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); - SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubMod.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); jWriter.WriteEndObject(); } @@ -153,7 +153,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } public (int Redirections, int Swaps, int Manips) GetCounts() - => IModGroup.GetCountsBase(this); + => ModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) => new(setting.Value & (1ul << OptionData.Count) - 1); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 6aa9160e..ac85e2bc 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -135,7 +135,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); public (int Redirections, int Swaps, int Manips) GetCounts() - => IModGroup.GetCountsBase(this); + => ModGroup.GetCountsBase(this); public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { @@ -145,8 +145,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup foreach (var option in OptionData) { jWriter.WriteStartObject(); - SubModHelpers.WriteModOption(jWriter, option); - SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubMod.WriteModOption(jWriter, option); + SubMod.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); jWriter.WriteEndObject(); } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index a02c8d68..c6122ea8 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -68,7 +68,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return; saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - var _ = group switch + _ = group switch { SingleModGroup s => s.Name = newName, MultiModGroup m => m.Name = newName, @@ -85,21 +85,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!VerifyFileName(mod, null, newName, true)) return; - var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - - mod.Groups.Add(type == GroupType.Multi - ? new MultiModGroup(mod) - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup(mod) - { - Name = newName, - Priority = maxPriority, - }); - saveService.Save(saveType, new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); + var idx = mod.Groups.Count; + var group = ModGroup.Create(mod, type, newName); + mod.Groups.Add(group); + saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); } /// Add a new mod, empty option group of the given type and name if it does not exist already. @@ -142,12 +132,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (group.Description == newDescription) return; - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; + group.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } @@ -155,9 +140,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription)) + var option = mod.Groups[groupIdx].Options[optionIdx]; + if (option.Description == newDescription) return; + option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -193,9 +180,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName)) + var option = mod.Groups[groupIdx].Options[optionIdx]; + if (option.Name == newName) return; + option.Name = newName; + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -251,7 +241,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Description = option.Description, }; if (option is IModDataContainer data) - SubModHelpers.Clone(data, newOption); + SubMod.Clone(data, newOption); s.OptionData.Add(newOption); break; } @@ -265,7 +255,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, }; if (option is IModDataContainer data) - SubModHelpers.Clone(data, newOption); + SubMod.Clone(data, newOption); m.OptionData.Add(newOption); break; } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 65b8ddd9..acb2c1ab 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -3,17 +3,13 @@ using OtterGui.Widgets; namespace Penumbra.Mods.Manager; -public class ModCombo : FilterComboCache +public class ModCombo(Func> generator) : FilterComboCache(generator, MouseWheelType.None, Penumbra.Log) { protected override bool IsVisible(int globalIndex, LowerString filter) => Items[globalIndex].Name.Contains(filter); protected override string ToString(Mod obj) => obj.Name.Text; - - public ModCombo(Func> generator) - : base(generator, MouseWheelType.None, Penumbra.Log) - { } } public class ModStorage : IReadOnlyList diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0626bc9d..40f943c8 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -115,7 +115,7 @@ public partial class ModCreator( try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); - SubModHelpers.LoadDataContainer(jObject, mod.Default, mod.ModPath); + SubMod.LoadDataContainer(jObject, mod.Default, mod.ModPath); } catch (Exception e) { diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 980b805d..1a234879 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -22,7 +22,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer => null; public void AddTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); + => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() => FullName; diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index c43d4b9e..3bcaffab 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -11,8 +11,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod() ?? ModPriority.Default; } @@ -24,7 +24,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(Mod mod, T group) : IModDataOption public HashSet Manipulations { get; set; } = []; public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); + => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() => Name; diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 5d68e401..98c56151 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -9,8 +9,8 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod group switch + { + SingleModGroup single => new SingleSubMod(group.Mod, single) + { + Name = name, + Description = description, + }, + MultiModGroup multi => new MultiSubMod(group.Mod, multi) + { + Name = name, + Description = description, + }, + _ => throw new ArgumentOutOfRangeException(nameof(group)), + }; + /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. public static void AddContainerTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) From 616db0dcc3a43fae6faeadde80260fa625d71cc9 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 26 Apr 2024 21:23:31 +1000 Subject: [PATCH 1655/2451] Add mesh vertex element readout --- Penumbra/Import/Models/Export/MeshExporter.cs | 1 - .../UI/AdvancedWindow/ModEditWindow.Models.cs | 44 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 19b06d55..219a046e 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 39924021..03b5169a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using ImGuiNET; +using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; @@ -8,7 +9,6 @@ using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; -using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -421,6 +421,14 @@ public partial class ModEditWindow var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; + // Vertex elements + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Vertex Elements"); + + ImGui.TableNextColumn(); + DrawVertexElementDetails(file.VertexDeclarations[meshIndex].VertexElements); + // Mesh material ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -436,6 +444,40 @@ public partial class ModEditWindow return ret; } + private static void DrawVertexElementDetails(MdlStructs.VertexElement[] vertexElements) + { + using var node = ImRaii.TreeNode($"Click to expand"); + if (!node) + return; + + var flags = ImGuiTableFlags.SizingFixedFit + | ImGuiTableFlags.RowBg + | ImGuiTableFlags.Borders + | ImGuiTableFlags.NoHostExtendX; + using var table = ImRaii.Table(string.Empty, 4, flags); + if (!table) + return; + + ImGui.TableSetupColumn("Usage"); + ImGui.TableSetupColumn("Type"); + ImGui.TableSetupColumn("Stream"); + ImGui.TableSetupColumn("Offset"); + + ImGui.TableHeadersRow(); + + foreach (var element in vertexElements) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{(MdlFile.VertexUsage)element.Usage}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{(MdlFile.VertexType)element.Type}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{element.Stream}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{element.Offset}"); + } + } + private static bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) { var mesh = tab.Mdl.Meshes[meshIndex]; From 1e5ed1c41450ba0d8b3b5dd85a8571ee49b6dd71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 18:43:45 +0200 Subject: [PATCH 1656/2451] Now that was a lot of work. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 3 +- .../Cache/CollectionCacheManager.cs | 4 +- .../Collections/Manager/CollectionStorage.cs | 6 +- Penumbra/Communication/ModOptionChanged.cs | 17 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 13 +- Penumbra/Mods/Editor/ModEditor.cs | 46 +- Penumbra/Mods/Editor/ModFileEditor.cs | 9 +- Penumbra/Mods/Editor/ModMerger.cs | 69 +-- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 8 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 4 +- Penumbra/Mods/Groups/IModGroup.cs | 11 +- Penumbra/Mods/Groups/ImcModGroup.cs | 158 +++++++ Penumbra/Mods/Groups/ModGroup.cs | 15 + Penumbra/Mods/Groups/ModSaveGroup.cs | 65 ++- Penumbra/Mods/Groups/MultiModGroup.cs | 78 ++-- Penumbra/Mods/Groups/SingleModGroup.cs | 91 ++-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 11 +- Penumbra/Mods/Manager/ImcModGroupEditor.cs | 38 ++ Penumbra/Mods/Manager/ModCacheManager.cs | 4 +- Penumbra/Mods/Manager/ModGroupEditor.cs | 289 +++++++++++++ Penumbra/Mods/Manager/ModManager.cs | 9 +- Penumbra/Mods/Manager/ModMigration.cs | 16 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 392 +++--------------- Penumbra/Mods/Manager/MultiModGroupEditor.cs | 84 ++++ Penumbra/Mods/Manager/SingleModGroupEditor.cs | 57 +++ Penumbra/Mods/ModCreator.cs | 20 +- Penumbra/Mods/Settings/ModSettings.cs | 44 +- Penumbra/Mods/Settings/Setting.cs | 28 ++ Penumbra/Mods/SubMods/IModOption.cs | 7 +- Penumbra/Mods/SubMods/ImcSubMod.cs | 32 ++ Penumbra/Mods/SubMods/MultiSubMod.cs | 20 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 47 ++- Penumbra/Mods/SubMods/SingleSubMod.cs | 16 +- Penumbra/Mods/SubMods/SubMod.cs | 32 +- Penumbra/Penumbra.csproj | 10 +- Penumbra/Services/SaveService.cs | 9 +- Penumbra/Services/StaticServiceManager.cs | 1 - Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 35 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 132 +++--- 44 files changed, 1182 insertions(+), 766 deletions(-) create mode 100644 Penumbra/Mods/Groups/ImcModGroup.cs create mode 100644 Penumbra/Mods/Manager/ImcModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/ModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/MultiModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/SingleModGroupEditor.cs create mode 100644 Penumbra/Mods/SubMods/ImcSubMod.cs diff --git a/Penumbra.Api b/Penumbra.Api index 590629df..69d106b4 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a +Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 039fbfa9..bfd134bb 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -11,6 +11,7 @@ using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Api.Api; @@ -254,7 +255,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) { switch (type) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c1296414..9c104cef 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -7,8 +7,10 @@ using Penumbra.Communication; using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; @@ -257,7 +259,7 @@ public class CollectionCacheManager : IDisposable } /// Prepare Changes by removing mods from caches with collections or add or reload mods. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { if (type is ModOptionChangeType.PrepareChange) { diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 1fe5b227..bfae2dc0 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -4,8 +4,10 @@ using OtterGui.Classes; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Collections.Manager; @@ -290,7 +292,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable } /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) @@ -298,7 +300,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index 0df58b5f..a20592ec 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,8 +1,10 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; +using static Penumbra.Communication.ModOptionChanged; namespace Penumbra.Communication; @@ -11,22 +13,23 @@ namespace Penumbra.Communication; /// /// Parameter is the type option change. /// Parameter is the changed mod. -/// Parameter is the index of the changed group inside the mod. -/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. -/// Parameter is the index of the group an option was moved to. +/// Parameter is the changed group inside the mod. +/// Parameter is the changed option inside the group or null if it does not concern a specific option. +/// Parameter is the changed data container inside the group or null if it does not concern a specific data container. +/// Parameter is the index of the group or option moved or deleted from. /// public sealed class ModOptionChanged() - : EventWrapper(nameof(ModOptionChanged)) + : EventWrapper(nameof(ModOptionChanged)) { public enum Priority { - /// + /// Api = int.MinValue, /// CollectionCacheManager = -100, - /// + /// ModCacheManager = 0, /// diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index eb6d0b0c..2b45ecbe 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -205,7 +205,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); + options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 31aacbe1..84a832a2 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -60,7 +60,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) { - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); try { @@ -73,7 +73,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -82,14 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; if (useModManager) - { - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync); - } + modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); else - { - subMod.Files = dict; - saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - } + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index e1c5962f..37524da1 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,8 +29,8 @@ public class ModEditor( public int GroupIdx { get; private set; } public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public IModDataContainer? Option { get; private set; } + public IModGroup? Group { get; private set; } + public IModDataContainer? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); @@ -63,10 +63,10 @@ public class ModEditor( { if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } @@ -75,18 +75,18 @@ public class ModEditor( Group = Mod.Groups[groupIdx]; if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - Option = Group.DataContainers[dataIdx]; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - DataIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } @@ -105,23 +105,11 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllContainers(Mod mod, Action action) { - action(mod.Default, -1, 0); - foreach (var (group, groupIdx) in mod.Groups.WithIndex()) - { - switch (group) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - action(single.OptionData[optionIdx], groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - action(multi.OptionData[optionIdx], groupIdx, optionIdx); - break; - } - } + action(mod.Default); + foreach (var container in mod.Groups.SelectMany(g => g.DataContainers)) + action(container); } // Does not delete the base directory itself even if it is completely empty at the end. diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 00685c94..e2c0b726 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,8 +24,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, dataIdx) = option.GetDataIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); + modManager.OptionEditor.SetFiles(option, dict); files.UpdatePaths(mod, option); Changes = false; return num; @@ -40,15 +39,15 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (newDict.Count != subMod.Files.Count) - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + modManager.OptionEditor.SetFiles(subMod, newDict); } - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); files.ClearMissingFiles(); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 0f629bc7..3a6f4a81 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -16,7 +16,7 @@ public class ModMerger : IDisposable { private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly ModOptionEditor _editor; + private readonly ModGroupEditor _editor; private readonly ModFileSystemSelector _selector; private readonly DuplicateManager _duplicates; private readonly ModManager _mods; @@ -32,14 +32,14 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = []; private readonly HashSet _createdDirectories = []; private readonly HashSet _createdGroups = []; - private readonly HashSet _createdOptions = []; + private readonly HashSet _createdOptions = []; public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } - public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { _editor = editor; @@ -100,22 +100,23 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); if (groupCreated) _createdGroups.Add(groupIdx); - if (group.Type != originalGroup.Type) - ((List)Warnings).Add( - $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); + if (group == null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); foreach (var originalOption in group.DataContainers) { - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); + var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add((IModDataOption)option); - MergeIntoOption([originalOption], (IModDataOption)option, false); + _createdOptions.Add(option!); + // #TODO DataContainer <> Option. + MergeIntoOption([originalOption], (IModDataContainer)option!, false); } else { throw new Exception( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option.FullName} already existed."); + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); } } } @@ -138,9 +139,9 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(group!, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add((IModDataOption)option); + _createdOptions.Add(option!); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -148,7 +149,8 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); + // #TODO DataContainer <> Option. + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); } private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) @@ -184,10 +186,9 @@ public class ModMerger : IDisposable } } - var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync); + _editor.SetFiles(option, redirections, SaveType.None); + _editor.SetFileSwaps(option, swaps, SaveType.None); + _editor.SetManipulations(option, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -261,30 +262,31 @@ public class ModMerger : IDisposable if (mods.Count == 1) { var files = CopySubModFiles(mods[0], dir); - _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.Group is not {} originalGroup) + if (originalOption.Group is not { } originalGroup) { var files = CopySubModFiles(mods[0], dir); - _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { - var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); - var folder = Path.Combine(dir.FullName, group.Name, option.Name); + // TODO DataContainer <> Option. + var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); + var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.OptionSetFiles(result, groupIdx, optionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); + _editor.SetFiles((IModDataContainer)option, files); + _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps); + _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations); } } } @@ -339,16 +341,15 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetOptionIndices(); - _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); + _editor.DeleteOption(option); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } foreach (var group in _createdGroups) { var groupName = MergeToMod!.Groups[group]; - _editor.DeleteModGroup(MergeToMod!, group); - Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}."); + _editor.DeleteModGroup(groupName); + Penumbra.Log.Verbose($"[Merger] Removed option group {groupName.Name}."); } foreach (var dir in _createdDirectories) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index dee700d5..2f7fd04c 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -145,12 +145,12 @@ public class ModMetaEditor(ModManager modManager) Split(currentOption.Manipulations); } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index e2088b32..437600c9 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -283,12 +283,12 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) switch (group) { case SingleModGroup single: - foreach (var (_, optionIdx) in single.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in multi.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; } } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 3247cfdf..0250efae 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -17,12 +17,12 @@ public class ModSwapEditor(ModManager modManager) Changes = false; } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + modManager.OptionEditor.SetFileSwaps(container, _swaps); Changes = false; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index b13799cd..a268ba0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -9,7 +9,7 @@ namespace Penumbra.Mods.Groups; public interface ITexToolsGroup { - public IReadOnlyList OptionData { get; } + public IReadOnlyList OptionData { get; } } public interface IModGroup @@ -17,22 +17,19 @@ public interface IModGroup public const int MaxMultiOptions = 63; public Mod Mod { get; } - public string Name { get; } + public string Name { get; set; } public string Description { get; set; } public GroupType Type { get; } public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public FullPath? FindBestMatch(Utf8GamePath gamePath); + public IModOption? AddOption(string name, string description = ""); public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } public bool IsOption { get; } - public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); - public int GetIndex(); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs new file mode 100644 index 00000000..e233f82e --- /dev/null +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Groups; + +public class ImcModGroup(Mod mod) : IModGroup +{ + public const int DisabledIndex = 30; + public const int NumAttributes = 10; + + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A single IMC manipulation."; + + public GroupType Type + => GroupType.Imc; + + public ModPriority Priority { get; set; } = ModPriority.Default; + public Setting DefaultSettings { get; set; } = Setting.Zero; + + public PrimaryId PrimaryId; + public SecondaryId SecondaryId; + public ObjectType ObjectType; + public BodySlot BodySlot; + public EquipSlot EquipSlot; + public Variant Variant; + + public ImcEntry DefaultEntry; + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => null; + + private bool _canBeDisabled = false; + + public bool CanBeDisabled + { + get => _canBeDisabled; + set + { + _canBeDisabled = value; + if (!value) + DefaultSettings = FixSetting(DefaultSettings); + } + } + + public IModOption? AddOption(string name, string description = "") + { + uint fullMask = GetFullMask(); + var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); + // All attributes handled. + if (firstUnset >= NumAttributes) + return null; + + var groupIdx = Mod.Groups.IndexOf(this); + if (groupIdx < 0) + return null; + + var subMod = new ImcSubMod(this) + { + Name = name, + Description = description, + AttributeIndex = firstUnset, + }; + OptionData.Add(subMod); + return subMod; + } + + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => []; + + public bool IsOption + => CanBeDisabled || OptionData.Count > 0; + + public int GetIndex() + => ModGroup.GetIndex(this); + + private ushort GetCurrentMask(Setting setting) + { + var mask = DefaultEntry.AttributeMask; + for (var i = 0; i < OptionData.Count; ++i) + { + if (!setting.HasFlag(i)) + continue; + + var option = OptionData[i]; + mask |= option.Attribute; + } + + return mask; + } + + private ushort GetFullMask() + => GetCurrentMask(Setting.AllBits(63)); + + private ImcManipulation GetManip(ushort mask) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, + DefaultEntry with { AttributeMask = mask }); + + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (CanBeDisabled && setting.HasFlag(DisabledIndex)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public Setting FixSetting(Setting setting) + => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName(nameof(ObjectType)); + jWriter.WriteValue(ObjectType.ToString()); + jWriter.WritePropertyName(nameof(BodySlot)); + jWriter.WriteValue(BodySlot.ToString()); + jWriter.WritePropertyName(nameof(EquipSlot)); + jWriter.WriteValue(EquipSlot.ToString()); + jWriter.WritePropertyName(nameof(PrimaryId)); + jWriter.WriteValue(PrimaryId.Id); + jWriter.WritePropertyName(nameof(SecondaryId)); + jWriter.WriteValue(SecondaryId.Id); + jWriter.WritePropertyName(nameof(Variant)); + jWriter.WriteValue(Variant.Id); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.AttributeIndex)); + jWriter.WriteValue(option.AttributeIndex); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); +} diff --git a/Penumbra/Mods/Groups/ModGroup.cs b/Penumbra/Mods/Groups/ModGroup.cs index da302714..8b55a035 100644 --- a/Penumbra/Mods/Groups/ModGroup.cs +++ b/Penumbra/Mods/Groups/ModGroup.cs @@ -5,6 +5,7 @@ namespace Penumbra.Mods.Groups; public static class ModGroup { + /// Create a new mod group based on the given type. public static IModGroup Create(Mod mod, GroupType type, string name) { var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; @@ -20,6 +21,11 @@ public static class ModGroup Name = name, Priority = maxPriority, }, + GroupType.Imc => new ImcModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } @@ -38,5 +44,14 @@ public static class ModGroup } return (redirectionCount, swapCount, manipCount); + } + + public static int GetIndex(IModGroup group) + { + var groupIndex = group.Mod.Groups.IndexOf(group); + if (groupIndex < 0) + throw new Exception($"Mod {group.Mod.Name} from Group {group.Name} does not contain this group."); + + return groupIndex; } } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 332879cb..efdcde09 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using Penumbra.Mods.SubMods; -using Penumbra.Services; - -namespace Penumbra.Mods.Groups; - +using Newtonsoft.Json; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Groups; + public readonly struct ModSaveGroup : ISavable { private readonly DirectoryInfo _basePath; @@ -12,25 +12,21 @@ public readonly struct ModSaveGroup : ISavable private readonly DefaultSubMod? _defaultMod; private readonly bool _onlyAscii; - public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) - { - _basePath = mod.ModPath; - _groupIdx = groupIdx; - if (_groupIdx < 0) - _defaultMod = mod.Default; - else - _group = mod.Groups[_groupIdx]; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) + private ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) { _basePath = basePath; _group = group; - _groupIdx = groupIdx; + _groupIdx = groupIndex; _onlyAscii = onlyAscii; } + public static ModSaveGroup WithoutMod(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) + => new(basePath, group, groupIndex, onlyAscii); + + public ModSaveGroup(IModGroup group, bool onlyAscii) + : this(group.Mod.ModPath, group, group.GetIndex(), onlyAscii) + { } + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; @@ -39,6 +35,33 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } + public ModSaveGroup(DirectoryInfo basePath, IModDataContainer container, bool onlyAscii) + { + _basePath = basePath; + _defaultMod = container as DefaultSubMod; + _onlyAscii = onlyAscii; + if (_defaultMod == null) + { + _groupIdx = -1; + _group = null; + } + else + { + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + } + + public ModSaveGroup(IModDataContainer container, bool onlyAscii) + { + _basePath = (container.Mod as Mod)?.ModPath + ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. + _defaultMod = null; + _onlyAscii = onlyAscii; + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + public string ToFilename(FilenameService fileNames) => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); @@ -59,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable { jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); - jWriter.WriteValue(group!.Name); + jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Priority)); @@ -69,4 +92,4 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } -} +} diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 6b352f66..a0034be0 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -18,11 +17,11 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; public IReadOnlyList Options @@ -39,28 +38,28 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public int AddOption(Mod mod, string name, string description = "") + public IModOption? AddOption(string name, string description = "") { - var groupIdx = mod.Groups.IndexOf(this); + var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) - return -1; + return null; - var subMod = new MultiSubMod(mod, this) + var subMod = new MultiSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public static MultiModGroup? Load(Mod mod, JObject json) { var ret = new MultiModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -78,7 +77,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup break; } - var subMod = new MultiSubMod(mod, ret, child); + var subMod = new MultiSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -87,42 +86,21 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public SingleModGroup ConvertToSingle() { - switch (type) + var single = new SingleModGroup(Mod) { - case GroupType.Multi: return this; - case GroupType.Single: - var single = new SingleModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), - }; - single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); - return single; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } - - public bool MoveOption(int optionIdxFrom, int optionIdxTo) - { - if (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - return true; + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), + }; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); + return single; } public int GetIndex() - { - var groupIndex = Mod.Groups.IndexOf(this); - if (groupIndex < 0) - throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); - - return groupIndex; - } + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { @@ -156,15 +134,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) - => new(setting.Value & (1ul << OptionData.Count) - 1); + => new(setting.Value & ((1ul << OptionData.Count) - 1)); /// Create a group without a mod only for saving it in the creator. - internal static MultiModGroup CreateForSaving(string name) + internal static MultiModGroup WithoutMod(string name) => new(null!) { Name = name, }; - IReadOnlyList ITexToolsGroup.OptionData + IReadOnlyList ITexToolsGroup.OptionData => OptionData; } diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ac85e2bc..0776c2af 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -16,31 +15,28 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; - IReadOnlyList ITexToolsGroup.OptionData - => OptionData; - public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public int AddOption(Mod mod, string name, string description = "") + public IModOption AddOption(string name, string description = "") { - var subMod = new SingleSubMod(mod, this) + var subMod = new SingleSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public IReadOnlyList Options @@ -57,9 +53,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup var options = json["Options"]; var ret = new SingleModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -68,7 +64,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SingleSubMod(mod, ret, child); + var subMod = new SingleSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -76,57 +72,21 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public MultiModGroup ConvertToMulti() { - switch (type) + var multi = new MultiModGroup(Mod) { - case GroupType.Single: return this; - case GroupType.Multi: - var multi = new MultiModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = Setting.Multi((int)DefaultSettings.Value), - }; - multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); - return multi; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = Setting.Multi((int)DefaultSettings.Value), + }; + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); + return multi; } - public bool MoveOption(int optionIdxFrom, int optionIdxTo) - { - if (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - var currentIndex = DefaultSettings.AsIndex; - // Update default settings with the move. - if (currentIndex == optionIdxFrom) - { - DefaultSettings = Setting.Single(optionIdxTo); - } - else if (optionIdxFrom < optionIdxTo) - { - if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo) - DefaultSettings = Setting.Single(currentIndex - 1); - } - else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo) - { - DefaultSettings = Setting.Single(currentIndex + 1); - } - - return true; - } - - public int GetIndex() - { - var groupIndex = Mod.Groups.IndexOf(this); - if (groupIndex < 0) - throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); - - return groupIndex; - } + public int GetIndex() + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); @@ -160,4 +120,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 1545811e..449405a0 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -8,6 +9,7 @@ using Penumbra.Meta; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.ItemSwap; @@ -40,8 +42,7 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, - int groupIndex = -1, int optionIndex = 0) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) { var convertedManips = new HashSet(Swaps.Count); var convertedFiles = new Dictionary(Swaps.Count); @@ -80,9 +81,9 @@ public class ItemSwapContainer } } - manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles); - manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps); - manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips); + manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None); + manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None); + manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync); return true; } catch (Exception e) diff --git a/Penumbra/Mods/Manager/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/ImcModGroupEditor.cs new file mode 100644 index 00000000..4e2b2194 --- /dev/null +++ b/Penumbra/Mods/Manager/ImcModGroupEditor.cs @@ -0,0 +1,38 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) + => null; + + protected override void RemoveOption(ImcModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.FixSetting(group.DefaultSettings); + } + + protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 3ff1a333..0669696f 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,8 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -103,7 +105,7 @@ public class ModCacheManager : IDisposable } } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx) { switch (type) { diff --git a/Penumbra/Mods/Manager/ModGroupEditor.cs b/Penumbra/Mods/Manager/ModGroupEditor.cs new file mode 100644 index 00000000..9f41fa6f --- /dev/null +++ b/Penumbra/Mods/Manager/ModGroupEditor.cs @@ -0,0 +1,289 @@ +using System.Text.RegularExpressions; +using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; + +namespace Penumbra.Mods.Manager; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, + DefaultOptionChanged, +} + +public class ModGroupEditor( + SingleModGroupEditor singleEditor, + MultiModGroupEditor multiEditor, + ImcModGroupEditor imcEditor, + CommunicatorService Communicator, + SaveService SaveService, + Configuration Config) : IService +{ + public SingleModGroupEditor SingleEditor + => singleEditor; + + public MultiModGroupEditor MultiEditor + => multiEditor; + + public ImcModGroupEditor ImcEditor + => imcEditor; + + /// Change the settings stored as default options in a mod. + public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption) + { + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); + } + + /// Rename an option group if possible. + public void RenameModGroup(IModGroup group, string newName) + { + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) + return; + + SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + group.Name = newName; + SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); + } + + /// Delete a given option group. Fires an event to prepare before actually deleting. + public void DeleteModGroup(IModGroup group) + { + var mod = group.Mod; + var idx = group.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); + mod.Groups.RemoveAt(idx); + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); + } + + /// Move the index of a given option group. + public void MoveModGroup(IModGroup group, int groupIdxTo) + { + var mod = group.Mod; + var idxFrom = group.GetIndex(); + if (!mod.Groups.Move(idxFrom, groupIdxTo)) + return; + + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); + } + + /// Change the internal priority of the given option group. + public void ChangeGroupPriority(IModGroup group, ModPriority newPriority) + { + if (group.Priority == newPriority) + return; + + group.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); + } + + /// Change the description of the given option group. + public void ChangeGroupDescription(IModGroup group, string newDescription) + { + if (group.Description == newDescription) + return; + + group.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); + } + + /// Rename the given option. + public void RenameOption(IModOption option, string newName) + { + if (option.Name == newName) + return; + + option.Name = newName; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Change the description of the given option. + public void ChangeOptionDescription(IModOption option, string newDescription) + { + if (option.Description == newDescription) + return; + + option.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Set the meta manipulations for a given option. Replaces existing manipulations. + public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + { + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Manipulations.SetTo(manipulations); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Set the file redirections for a given option. Replaces existing redirections. + public void SetFiles(IModDataContainer subMod, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) + { + if (subMod.Files.SetEquals(replacements)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Files.SetTo(replacements); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. + public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) + { + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) + { + SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + } + + /// Set the file swaps for a given option. Replaces existing swaps. + public void SetFileSwaps(IModDataContainer subMod, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) + { + if (subMod.FileSwaps.SetEquals(swaps)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.FileSwaps.SetTo(swaps); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Verify that a new option group name is unique in this mod. + public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.Messager.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + NotificationType.Warning, false); + + return false; + } + + public void DeleteOption(IModOption option) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.DeleteOption(s); + return; + case MultiSubMod m: + MultiEditor.DeleteOption(m); + return; + case ImcSubMod i: + ImcEditor.DeleteOption(i); + return; + } + } + + public IModOption? AddOption(IModGroup group, IModOption option) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, option), + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, + }; + + public IModOption? AddOption(IModGroup group, string newName) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, newName), + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, + }; + + public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + _ => null, + }; + + public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), + }; + + public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) + => group switch + { + SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), + }; + + public void MoveOption(IModOption option, int toIdx) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.MoveOption(s, toIdx); + return; + case MultiSubMod m: + MultiEditor.MoveOption(m, toIdx); + return; + case ImcSubMod i: + ImcEditor.MoveOption(i, toIdx); + return; + } + } +} diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index d912e292..7a266a31 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -32,14 +31,14 @@ public sealed class ModManager : ModStorage, IDisposable private readonly Configuration _config; private readonly CommunicatorService _communicator; - public readonly ModCreator Creator; - public readonly ModDataEditor DataEditor; - public readonly ModOptionEditor OptionEditor; + public readonly ModCreator Creator; + public readonly ModDataEditor DataEditor; + public readonly ModGroupEditor OptionEditor; public DirectoryInfo BasePath { get; private set; } = null!; public bool Valid { get; private set; } - public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModGroupEditor optionEditor, ModCreator creator) { _config = config; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index f160d5bd..c7eb7cc5 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -83,8 +83,8 @@ public static partial class ModMigration mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); - foreach (var (_, index) in mod.Groups.WithIndex()) - saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport)); + foreach (var group in mod.Groups) + saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -112,7 +112,7 @@ public static partial class ModMigration } fileVersion = 1; - saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default, creator.Config.ReplaceNonAsciiOnImport)); return true; } @@ -176,7 +176,7 @@ public static partial class ModMigration private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SingleSubMod(mod, group) + var subMod = new SingleSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -189,7 +189,7 @@ public static partial class ModMigration private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, ModPriority priority, HashSet seenMetaFiles) { - var subMod = new MultiSubMod(mod, group) + var subMod = new MultiSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -219,7 +219,7 @@ public static partial class ModMigration [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public GroupType SelectionType = GroupType.Single; - public List Options = new(); + public List Options = []; public OptionGroupV0() { } @@ -236,12 +236,12 @@ public static partial class ModMigration var token = JToken.Load(reader); if (token.Type == JTokenType.Array) - return token.ToObject>() ?? new HashSet(); + return token.ToObject>() ?? []; var tmp = token.ToObject(); return tmp != null ? new HashSet { tmp } - : new HashSet(); + : []; } public override bool CanWrite diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index c6122ea8..7370a933 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,384 +1,122 @@ -using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods.Manager; -public enum ModOptionChangeType +public abstract class ModOptionEditor( + CommunicatorService communicator, + SaveService saveService, + Configuration config) + where TGroup : class, IModGroup + where TOption : class, IModOption { - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionFilesAdded, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, - PrepareChange, - DefaultOptionChanged, -} - -public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) -{ - /// Change the type of a group given by mod and index to type, if possible. - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) - { - var group = mod.Groups[groupIdx]; - if (group.Type == type) - return; - - mod.Groups[groupIdx] = group.Convert(type); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } - - /// Change the settings stored as default options in a mod. - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption) - { - var group = mod.Groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; - - group.DefaultSettings = defaultOption; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } - - /// Rename an option group if possible. - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod.Groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - _ = group switch - { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } + protected readonly CommunicatorService Communicator = communicator; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. - public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { - if (!VerifyFileName(mod, null, newName, true)) - return; + if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) + return null; - var idx = mod.Groups.Count; - var group = ModGroup.Create(mod, type, newName); + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); - saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); + return group; } /// Add a new mod, empty option group of the given type and name if it does not exist already. - public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public (TGroup, int, bool) FindOrAddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) - return (mod.Groups[idx], idx, false); + { + var existingGroup = mod.Groups[idx] as TGroup + ?? throw new Exception($"Mod group with name {newName} exists, but is of the wrong type."); + return (existingGroup, idx, false); + } - AddModGroup(mod, type, newName, saveType); - if (mod.Groups[^1].Name != newName) + idx = mod.Groups.Count; + if (AddModGroup(mod, newName, saveType) is not { } group) throw new Exception($"Could not create new mod group with name {newName}."); - return (mod.Groups[^1], mod.Groups.Count - 1, true); - } - - /// Delete a given option group. Fires an event to prepare before actually deleting. - public void DeleteModGroup(Mod mod, int groupIdx) - { - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod.Groups.RemoveAt(groupIdx); - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - /// Move the index of a given option group. - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) - return; - - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); - } - - /// Change the description of the given option group. - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) - { - var group = mod.Groups[groupIdx]; - if (group.Description == newDescription) - return; - - group.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - /// Change the description of the given option. - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Description == newDescription) - return; - - option.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - /// Change the internal priority of the given option group. - public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority) - { - var group = mod.Groups[groupIdx]; - if (group.Priority == newPriority) - return; - - group.Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - /// Change the internal priority of the given option. - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority) - { - switch (mod.Groups[groupIdx]) - { - case MultiModGroup multi: - if (multi.OptionData[optionIdx].Priority == newPriority) - return; - - multi.OptionData[optionIdx].Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - /// Rename the given option. - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Name == newName) - return; - - option.Name = newName; - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + return (group, idx, true); } /// Add a new empty option of the given name for the given group. - public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public TOption? AddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.AddOption(mod, newName); - if (idx < 0) - return -1; + if (group.AddOption(newName) is not TOption option) + return null; - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return idx; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, option, null, -1); + return option; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (TOption, int, bool) FindOrAddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.Options.IndexOf(o => o.Name == newName); + var idx = group.Options.IndexOf(o => o.Name == newName); if (idx >= 0) - return (group.Options[idx], idx, false); + { + var existingOption = group.Options[idx] as TOption + ?? throw new Exception($"Mod option with name {newName} exists, but is of the wrong type."); // Should never happen. + return (existingOption, idx, false); + } - idx = group.AddOption(mod, newName); - if (idx < 0) + if (AddOption(group, newName, saveType) is not { } option) throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (group.Options[idx], idx, true); + return (option, idx, true); } /// Add an existing option to a given group. - public void AddOption(Mod mod, int groupIdx, IModOption option) + public TOption? AddOption(TGroup group, IModOption option) { - var group = mod.Groups[groupIdx]; - int idx; - switch (group) - { - case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }: - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); - return; - case SingleModGroup s: - { - idx = s.OptionData.Count; - var newOption = new SingleSubMod(s.Mod, s) - { - Name = option.Name, - Description = option.Description, - }; - if (option is IModDataContainer data) - SubMod.Clone(data, newOption); - s.OptionData.Add(newOption); - break; - } - case MultiModGroup m: - { - idx = m.OptionData.Count; - var newOption = new MultiSubMod(m.Mod, m) - { - Name = option.Name, - Description = option.Description, - Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, - }; - if (option is IModDataContainer data) - SubMod.Clone(data, newOption); - m.OptionData.Add(newOption); - break; - } - default: return; - } + if (CloneOption(group, option) is not { } clonedOption) + return null; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, clonedOption, null, -1); + return clonedOption; } /// Delete the given option from the given group. - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + public void DeleteOption(TOption option) { - var group = mod.Groups[groupIdx]; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.OptionData.RemoveAt(optionIdx); - break; - } - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + var mod = option.Mod; + var group = option.Group; + var optionIdx = option.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); + RemoveOption((TGroup)group, optionIdx); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, group, null, null, optionIdx); } /// Move an option inside the given option group. - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + public void MoveOption(TOption option, int optionIdxTo) { - var group = mod.Groups[groupIdx]; - if (!group.MoveOption(optionIdxFrom, optionIdxTo)) + var idx = option.GetIndex(); + var group = (TGroup)option.Group; + if (!MoveOption(group, idx, optionIdxTo)) return; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.Manipulations.SetTo(manipulations); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1); - } - - /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary replacements, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.Files.SetEquals(replacements)) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.Files.SetTo(replacements); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1); - } - - /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - var oldCount = subMod.Files.Count; - subMod.Files.AddFrom(additions); - if (oldCount != subMod.Files.Count) - { - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1); - } - } - - /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.FileSwaps.SetEquals(swaps)) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.FileSwaps.SetTo(swaps); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1); - } - - - /// Verify that a new option group name is unique in this mod. - public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.Messager.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - NotificationType.Warning, false); - - return false; - } - - /// Get the correct option for the given group and option index. - private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) - { - if (groupIdx == -1 && dataContainerIdx == 0) - return mod.Default; - - return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; - } + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TOption? CloneOption(TGroup group, IModOption option); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension diff --git a/Penumbra/Mods/Manager/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/MultiModGroupEditor.cs new file mode 100644 index 00000000..e6b2bac1 --- /dev/null +++ b/Penumbra/Mods/Manager/MultiModGroupEditor.cs @@ -0,0 +1,84 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToSingle(MultiModGroup group) + { + var idx = group.GetIndex(); + var singleGroup = group.ConvertToSingle(); + group.Mod.Groups[idx] = singleGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + } + + /// Change the internal priority of the given option. + public void ChangeOptionPriority(MultiSubMod option, ModPriority newPriority) + { + if (option.Priority == newPriority) + return; + + option.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, option.Mod, option.Group, option, null, -1); + } + + protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override MultiSubMod? CloneOption(MultiModGroup group, IModOption option) + { + if (group.OptionData.Count >= IModGroup.MaxMultiOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return null; + } + + var newOption = new MultiSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + + if (option is IModDataContainer data) + { + SubMod.Clone(data, newOption); + if (option is MultiSubMod m) + newOption.Priority = m.Priority; + else + newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); + } + + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(MultiModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + } + + protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/SingleModGroupEditor.cs new file mode 100644 index 00000000..4999ff60 --- /dev/null +++ b/Penumbra/Mods/Manager/SingleModGroupEditor.cs @@ -0,0 +1,57 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToMulti(SingleModGroup group) + { + var idx = group.GetIndex(); + var multiGroup = group.ConvertToMulti(); + group.Mod.Groups[idx] = multiGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + } + + protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override SingleSubMod CloneOption(SingleModGroup group, IModOption option) + { + var newOption = new SingleSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + SubMod.Clone(data, newOption); + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(SingleModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveSingle(optionIndex); + } + + protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 40f943c8..47261c6d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -90,7 +90,7 @@ public partial class ModCreator( var changes = false; foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) { - var group = LoadModGroup(mod, file, mod.Groups.Count); + var group = LoadModGroup(mod, file); if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes @@ -244,12 +244,12 @@ public partial class ModCreator( { case GroupType.Multi: { - var group = MultiModGroup.CreateForSaving(name); + var group = MultiModGroup.WithoutMod(name); group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.Clone(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -258,8 +258,8 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -272,7 +272,7 @@ public partial class ModCreator( .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); + var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); @@ -295,7 +295,7 @@ public partial class ModCreator( } IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -422,7 +422,7 @@ public partial class ModCreator( /// Load an option group for a specific mod by its file and index. - private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) + private static IModGroup? LoadModGroup(Mod mod, FileInfo file) { if (!File.Exists(file.FullName)) return null; @@ -442,7 +442,7 @@ public partial class ModCreator( } return null; - } + } internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) { diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index db9e0521..39ee1860 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Settings; @@ -45,63 +46,64 @@ public class ModSettings } // Automatically react to changes in a mods available options. - public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + public bool HandleChanges(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, int fromIdx) { switch (type) { case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings); + Settings.Insert(group!.GetIndex(), group.DefaultSettings); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. - Settings.RemoveAt(groupIdx); + Settings.RemoveAt(fromIdx); return true; case ModOptionChangeType.GroupTypeChanged: { // Fix settings for a changed group type. // Single -> Multi: set single as enabled, rest as disabled // Multi -> Single: set the first enabled option or 0. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var idx = group!.GetIndex(); + var config = Settings[idx]; + Settings[idx] = group.Type switch { GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; - return config != Settings[groupIdx]; + return config != Settings[idx]; } case ModOptionChangeType.OptionDeleted: { // Single -> select the previous option if any. // Multi -> excise the corresponding bit. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex >= optionIdx - ? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero - : config, - GroupType.Multi => config.RemoveBit(optionIdx), - _ => config, + GroupType.Single => config.RemoveSingle(fromIdx), + GroupType.Multi => config.RemoveBit(fromIdx), + GroupType.Imc => config.RemoveBit(fromIdx), + _ => config, }; return config != Settings[groupIdx]; } case ModOptionChangeType.GroupMoved: // Move the group the same way. - return Settings.Move(groupIdx, movedToIdx); + return Settings.Move(fromIdx, group!.GetIndex()); case ModOptionChangeType.OptionMoved: { // Single -> select the moved option if it was currently selected // Multi -> move the corresponding bit - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var toIdx = option!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, - GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), + GroupType.Single => config.MoveSingle(fromIdx, toIdx), + GroupType.Multi => config.MoveBit(fromIdx, toIdx), + GroupType.Imc => config.MoveBit(fromIdx, toIdx), _ => config, }; return config != Settings[groupIdx]; diff --git a/Penumbra/Mods/Settings/Setting.cs b/Penumbra/Mods/Settings/Setting.cs index 231529b8..059cbf51 100644 --- a/Penumbra/Mods/Settings/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -41,6 +41,34 @@ public readonly record struct Setting(ulong Value) public Setting TurnMulti(int count) => new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); + public Setting RemoveSingle(int singleIdx) + { + var settingIndex = AsIndex; + if (settingIndex >= singleIdx) + return settingIndex > 1 ? Single(settingIndex - 1) : Zero; + + return this; + } + + public Setting MoveSingle(int singleIdxFrom, int singleIdxTo) + { + var currentIndex = AsIndex; + if (currentIndex == singleIdxFrom) + return Single(singleIdxTo); + + if (singleIdxFrom < singleIdxTo) + { + if (currentIndex > singleIdxFrom && currentIndex <= singleIdxTo) + return Single(currentIndex - 1); + } + else if (currentIndex < singleIdxFrom && currentIndex >= singleIdxTo) + { + return Single(currentIndex + 1); + } + + return this; + } + public ModPriority AsPriority => new((int)(Value & 0xFFFFFFFF)); diff --git a/Penumbra/Mods/SubMods/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs index 83d632a0..ecfcf91a 100644 --- a/Penumbra/Mods/SubMods/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,10 +1,15 @@ +using Penumbra.Mods.Groups; + namespace Penumbra.Mods.SubMods; public interface IModOption { + public Mod Mod { get; } + public IModGroup Group { get; } + public string Name { get; set; } public string FullName { get; } public string Description { get; set; } - public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public int GetIndex(); } diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs new file mode 100644 index 00000000..167c8a6c --- /dev/null +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -0,0 +1,32 @@ +using Penumbra.Mods.Groups; + +namespace Penumbra.Mods.SubMods; + +public class ImcSubMod(ImcModGroup group) : IModOption +{ + public readonly ImcModGroup Group = group; + + public Mod Mod + => Group.Mod; + + public byte AttributeIndex; + + public ushort Attribute + => (ushort)(1 << AttributeIndex); + + Mod IModOption.Mod + => Mod; + + IModGroup IModOption.Group + => Group; + + public string Name { get; set; } = "Part"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public int GetIndex() + => SubMod.GetIndex(this); +} diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 3bcaffab..c01dcce9 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -4,21 +4,21 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(mod, group) +public class MultiSubMod(MultiModGroup group) : OptionSubMod(group) { public ModPriority Priority { get; set; } = ModPriority.Default; - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) - : this(mod, group) + public MultiSubMod(MultiModGroup group, JToken json) + : this(group) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, group.Mod.ModPath); Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; } - public MultiSubMod Clone(Mod mod, MultiModGroup group) + public MultiSubMod Clone(MultiModGroup group) { - var ret = new MultiSubMod(mod, group) + var ret = new MultiSubMod(group) { Name = Name, Description = Description, @@ -29,9 +29,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod new(null!, null!) + public static MultiSubMod WithoutGroup(string name, string description, ModPriority priority) + => new(null!) { Name = name, Description = description, diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index fbf03243..02d86af2 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -1,25 +1,26 @@ -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Groups; -using Penumbra.String.Classes; - +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + namespace Penumbra.Mods.SubMods; -public interface IModDataOption : IModDataContainer, IModOption; - -public abstract class OptionSubMod(Mod mod, T group) : IModDataOption - where T : IModGroup +public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContainer { - internal readonly Mod Mod = mod; - internal readonly IModGroup Group = group; + protected readonly IModGroup Group = group; - public string Name { get; set; } = "Option"; + public Mod Mod + => Group.Mod; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; public string FullName - => $"{Group!.Name}: {Name}"; + => $"{Group.Name}: {Name}"; - public string Description { get; set; } = string.Empty; + Mod IModOption.Mod + => Mod; IMod IModDataContainer.Mod => Mod; @@ -27,6 +28,9 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption IModGroup IModDataContainer.Group => Group; + IModGroup IModOption.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; @@ -43,8 +47,8 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); + public int GetIndex() + => SubMod.GetIndex(this); private int GetDataIndex() { @@ -54,4 +58,11 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption return dataIndex; } -} +} + +public abstract class OptionSubMod(T group) : OptionSubMod(group) + where T : IModGroup +{ + public new T Group + => (T)base.Group; +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 98c56151..675f37bc 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -4,18 +4,18 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod(mod, singleGroup) +public class SingleSubMod(SingleModGroup singleGroup) : OptionSubMod(singleGroup) { - public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) - : this(mod, singleGroup) + public SingleSubMod(SingleModGroup singleGroup, JToken json) + : this(singleGroup) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, singleGroup.Mod.ModPath); } - public SingleSubMod Clone(Mod mod, SingleModGroup group) + public SingleSubMod Clone(SingleModGroup group) { - var ret = new SingleSubMod(mod, group) + var ret = new SingleSubMod(group) { Name = Name, Description = Description, @@ -25,9 +25,9 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod group switch - { - SingleModGroup single => new SingleSubMod(group.Mod, single) - { - Name = name, - Description = description, - }, - MultiModGroup multi => new MultiSubMod(group.Mod, multi) - { - Name = name, - Description = description, - }, - _ => throw new ArgumentOutOfRangeException(nameof(group)), - }; + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static int GetIndex(IModOption option) + { + var dataIndex = option.Group.Options.IndexOf(option); + if (dataIndex < 0) + throw new Exception($"Group {option.Group.Name} from option {option.Name} does not contain this option."); + + return dataIndex; + } /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) { @@ -37,6 +32,7 @@ public static class SubMod } /// Replace all data of with the data of . + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void Clone(IModDataContainer from, IModDataContainer to) { to.Files = new Dictionary(from.Files); @@ -45,6 +41,7 @@ public static class SubMod } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -75,6 +72,7 @@ public static class SubMod } /// Load the relevant data for a selectable option from a JToken of that option. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadOptionData(JToken json, IModOption option) { option.Name = json[nameof(option.Name)]?.ToObject() ?? string.Empty; @@ -82,6 +80,7 @@ public static class SubMod } /// Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { j.WritePropertyName(nameof(data.Files)); @@ -111,6 +110,7 @@ public static class SubMod } /// Write the data for a selectable mod option on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(option.Name)); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0ec1fd44..2d595ec1 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,6 +23,12 @@ PROFILING; + + + + + + PreserveNewest @@ -93,10 +99,6 @@ - - - - diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 8d3cb641..eff3295d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -34,8 +34,11 @@ public sealed class SaveService(Logger log, FrameworkManager framework, Filename } } - for (var i = 0; i < mod.Groups.Count - 1; ++i) - ImmediateSave(new ModSaveGroup(mod, i, onlyAscii)); - ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii)); + if (mod.Groups.Count > 0) + { + foreach (var group in mod.Groups.SkipLast(1)) + ImmediateSave(new ModSaveGroup(group, onlyAscii)); + ImmediateSaveSync(new ModSaveGroup(mod.Groups[^1], onlyAscii)); + } } } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index e758aa35..5fa1a848 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -121,7 +121,6 @@ public static class StaticServiceManager private static ServiceManager AddMods(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 77bdb161..cd55beb0 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -17,6 +17,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -264,9 +265,10 @@ public class ItemSwapTab : IDisposable, ITab return; _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager, _modManager[^1], + var mod = _modManager[^1]; + if (!_swapData.WriteMod(_modManager, mod, mod.Default, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) - _modManager.DeleteMod(_modManager[^1]); + _modManager.DeleteMod(mod); } private void CreateOption() @@ -276,7 +278,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = -1; + IModOption? createdOption = null; DirectoryInfo? optionFolderName = null; try { @@ -290,22 +292,22 @@ public class ItemSwapTab : IDisposable, ITab { if (_selectedGroup == null) { - _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); - _selectedGroup = _mod.Groups.Last(); + if (_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName) is not { } group) + throw new Exception($"Failure creating option group."); + + _selectedGroup = group; groupCreated = true; } - var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - if (optionIdx < 0) + if (_modManager.OptionEditor.AddOption(_selectedGroup, _newOptionName) is not { } option) throw new Exception($"Failure creating mod option."); - optionCreated = optionIdx; + createdOption = option; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_modManager, _mod, - _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, - optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), optionIdx)) + // #TODO ModOption <> DataContainer + if (!_swapData.WriteMod(_modManager, _mod, (IModDataContainer)option, + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName)) throw new Exception("Failure writing files for mod swap."); } } @@ -314,12 +316,12 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated >= 0 && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); + if (createdOption != null) + _modManager.OptionEditor.DeleteOption(createdOption); if (groupCreated) { - _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.OptionEditor.DeleteModGroup(_selectedGroup!); _selectedGroup = null; } @@ -717,7 +719,8 @@ public class ItemSwapTab : IDisposable, ITab _dirty = true; } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int fromIdx) { if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod) return; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 92a9dd66..743310ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -27,7 +27,7 @@ public partial class ModEditWindow private const string GenderTooltip = "Gender"; private const string ObjectTypeTooltip = "Object Type"; private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIDTooltip = "Primary ID"; + private const string PrimaryIdTooltipShort = "Primary ID"; private const string VariantIdTooltip = "Variant ID"; private const string EstTypeTooltip = "EST Type"; private const string RacialTribeTooltip = "Racial Tribe"; @@ -45,7 +45,7 @@ public partial class ModEditWindow var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.MetaEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -477,7 +477,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIDTooltip); + ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index dbb88fb7..6b48a048 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.SwapEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -627,7 +627,7 @@ public partial class ModEditWindow : Window, IDisposable public void Dispose() { _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - _editor?.Dispose(); + _editor.Dispose(); _materialTab.Dispose(); _modelTab.Dispose(); _shaderPackageTab.Dispose(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index afbef45d..fcd76a51 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -15,6 +15,7 @@ using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab; @@ -248,13 +249,13 @@ public class ModPanelEditTab( ImGui.SameLine(); - var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false); + var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, !nameValid, true)) return; - modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); + modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName); Reset(); } } @@ -364,9 +365,9 @@ public class ModPanelEditTab( break; case >= 0: if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); else - modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, + modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], _newDescription); break; @@ -396,18 +397,18 @@ public class ModPanelEditTab( .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName); + _modManager.OptionEditor.RenameModGroup(group, newGroupName); ImGuiUtil.HoverTooltip("Group Name"); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); ImGui.SameLine(); if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority); + _modManager.OptionEditor.ChangeGroupPriority(group, priority); ImGuiUtil.HoverTooltip("Group Priority"); @@ -417,7 +418,7 @@ public class ModPanelEditTab( var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); ImGui.SameLine(); tt = groupIdx == _mod.Groups.Count - 1 @@ -425,7 +426,7 @@ public class ModPanelEditTab( : $"Move this group down to group {groupIdx + 2}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); ImGui.SameLine(); @@ -452,17 +453,17 @@ public class ModPanelEditTab( { private const string DragDropLabel = "##DragOption"; - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static int _dragDropGroupIdx = -1; - private static int _dragDropOptionIdx = -1; + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static IModGroup? _dragDropGroup; + private static IModOption? _dragDropOption; public static void Reset() { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroup = null; + _dragDropOption = null; } public static void Draw(ModPanelEditTab panel, int groupIdx) @@ -482,7 +483,7 @@ public class ModPanelEditTab( switch (panel._mod.Groups[groupIdx]) { - case SingleModGroup single: + case SingleModGroup single: for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) EditOption(panel, single, groupIdx, optionIdx); break; @@ -491,6 +492,7 @@ public class ModPanelEditTab( EditOption(panel, multi, groupIdx, optionIdx); break; } + DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } @@ -502,8 +504,8 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(group, groupIdx, optionIdx); - Target(panel, group, groupIdx, optionIdx); + Source(option); + Target(panel, group, optionIdx); ImGui.TableNextColumn(); @@ -511,7 +513,7 @@ public class ModPanelEditTab( if (group.Type == GroupType.Single) { if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } @@ -519,15 +521,14 @@ public class ModPanelEditTab( { var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, - group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } ImGui.TableNextColumn(); if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + panel._modManager.OptionEditor.RenameOption(option, newOptionName); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", @@ -537,15 +538,15 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); ImGui.TableNextColumn(); - if (group is not MultiModGroup multi) + if (option is not MultiSubMod multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); ImGuiUtil.HoverTooltip("Option priority."); } @@ -564,7 +565,7 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, groupIdx, count); + Target(panel, group, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -585,14 +586,14 @@ public class ModPanelEditTab( tt, !(canAddGroup && validName), true)) return; - panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); + panel._modManager.OptionEditor.AddOption(group, _newOptionName); _newOptionName = string.Empty; } // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModGroup group, int groupIdx, int optionIdx) + private static void Source(IModOption option) { - if (group is not ITexToolsGroup) + if (option.Group is not ITexToolsGroup) return; using var source = ImRaii.DragDropSource(); @@ -601,14 +602,14 @@ public class ModPanelEditTab( if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; + _dragDropGroup = option.Group; + _dragDropOption = option; } - ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); } - private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) { if (group is not ITexToolsGroup) return; @@ -617,39 +618,53 @@ public class ModPanelEditTab( if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; - if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0) + if (_dragDropGroup != null && _dragDropOption != null) { - if (_dragDropGroupIdx == groupIdx) + if (_dragDropGroup == group) { - var sourceOption = _dragDropOptionIdx; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); } else { // Move from one group to another by deleting, then adding, then moving the option. - var sourceGroupIdx = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.DataContainers.Count; - var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue(() => { - panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); - panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + panel._modManager.OptionEditor.DeleteOption(sourceOption); + if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); }); } } - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _dragDropGroup = null; + _dragDropOption = null; } } /// Draw a combo to select single or multi group and switch between them. private void DrawGroupCombo(IModGroup group, int groupIdx) { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); + using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); + if (!combo) + return; + + if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) + _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); + + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); + if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) + _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); + + style.Pop(); + if (!canSwitchToMulti) + ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); + return; + static string GroupTypeName(GroupType type) => type switch { @@ -657,23 +672,6 @@ public class ModPanelEditTab( GroupType.Multi => "Multi Group", _ => "Unknown", }; - - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); } /// Handles input text and integers in separate fields without buffers for every single one. From cff617245356a3721d3e5849585e89856cbebcc5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Apr 2024 00:07:10 +0200 Subject: [PATCH 1657/2451] Move editors into folder. --- OtterGui | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 1 + .../Cache/CollectionCacheManager.cs | 1 + .../Collections/Manager/CollectionStorage.cs | 1 + Penumbra/Communication/ModOptionChanged.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 1 + Penumbra/Mods/Manager/ModCacheManager.cs | 1 + Penumbra/Mods/Manager/ModManager.cs | 1 + .../{ => OptionEditor}/ImcModGroupEditor.cs | 4 +- .../{ => OptionEditor}/ModGroupEditor.cs | 37 +++++++------- .../{ => OptionEditor}/ModOptionEditor.cs | 50 +++++++++---------- .../{ => OptionEditor}/MultiModGroupEditor.cs | 8 +-- .../SingleModGroupEditor.cs | 8 +-- Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/packages.lock.json | 14 ++++-- 18 files changed, 74 insertions(+), 62 deletions(-) rename Penumbra/Mods/Manager/{ => OptionEditor}/ImcModGroupEditor.cs (91%) rename Penumbra/Mods/Manager/{ => OptionEditor}/ModGroupEditor.cs (88%) rename Penumbra/Mods/Manager/{ => OptionEditor}/ModOptionEditor.cs (73%) rename Penumbra/Mods/Manager/{ => OptionEditor}/MultiModGroupEditor.cs (92%) rename Penumbra/Mods/Manager/{ => OptionEditor}/SingleModGroupEditor.cs (90%) diff --git a/OtterGui b/OtterGui index 3460a817..20c4a6c5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3460a817fc5e01a6b60eb834c3c59031938388fc +Subproject commit 20c4a6c53103d9fa8dec63babc628c9d01f094c0 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index bfd134bb..56b80e63 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -10,6 +10,7 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 9c104cef..fb9ee9a3 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -9,6 +9,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index bfae2dc0..de5d0a14 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -6,6 +6,7 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index a20592ec..67f2c0c3 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using static Penumbra.Communication.ModOptionChanged; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 3a6f4a81..d6e21076 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -5,6 +5,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 0669696f..59c88cf0 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -3,6 +3,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 7a266a31..adaca85e 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,5 +1,6 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Manager/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs similarity index 91% rename from Penumbra/Mods/Manager/ImcModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 4e2b2194..1194f961 100644 --- a/Penumbra/Mods/Manager/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -6,7 +6,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService @@ -14,7 +14,7 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; diff --git a/Penumbra/Mods/Manager/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs similarity index 88% rename from Penumbra/Mods/Manager/ModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 9f41fa6f..b2b48ac0 100644 --- a/Penumbra/Mods/Manager/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -1,10 +1,8 @@ -using System.Text.RegularExpressions; using Dalamud.Interface.Internal.Notifications; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -12,9 +10,8 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public enum ModOptionChangeType { @@ -91,7 +88,7 @@ public class ModGroupEditor( /// Move the index of a given option group. public void MoveModGroup(IModGroup group, int groupIdxTo) { - var mod = group.Mod; + var mod = group.Mod; var idxFrom = group.GetIndex(); if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; @@ -230,45 +227,45 @@ public class ModGroupEditor( => group switch { SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), - _ => null, + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs similarity index 73% rename from Penumbra/Mods/Manager/ModOptionEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index 7370a933..c067102e 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -5,7 +5,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public abstract class ModOptionEditor( CommunicatorService communicator, @@ -15,8 +15,8 @@ public abstract class ModOptionEditor( where TOption : class, IModOption { protected readonly CommunicatorService Communicator = communicator; - protected readonly SaveService SaveService = saveService; - protected readonly Configuration Config = config; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) @@ -25,7 +25,7 @@ public abstract class ModOptionEditor( return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, maxPriority); + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -92,8 +92,8 @@ public abstract class ModOptionEditor( /// Delete the given option from the given group. public void DeleteOption(TOption option) { - var mod = option.Mod; - var group = option.Group; + var mod = option.Mod; + var group = option.Group; var optionIdx = option.GetIndex(); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); RemoveOption((TGroup)group, optionIdx); @@ -104,7 +104,7 @@ public abstract class ModOptionEditor( /// Move an option inside the given option group. public void MoveOption(TOption option, int optionIdxTo) { - var idx = option.GetIndex(); + var idx = option.GetIndex(); var group = (TGroup)option.Group; if (!MoveOption(group, idx, optionIdxTo)) return; @@ -113,10 +113,10 @@ public abstract class ModOptionEditor( Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); protected abstract TOption? CloneOption(TGroup group, IModOption option); - protected abstract void RemoveOption(TGroup group, int optionIndex); - protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension @@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension { (requiresSaving, requiresReloading, wasPrepared) = type switch { - ModOptionChangeType.GroupRenamed => (true, false, false), - ModOptionChangeType.GroupAdded => (true, false, false), - ModOptionChangeType.GroupDeleted => (true, true, false), - ModOptionChangeType.GroupMoved => (true, false, false), - ModOptionChangeType.GroupTypeChanged => (true, true, true), - ModOptionChangeType.PriorityChanged => (true, true, true), - ModOptionChangeType.OptionAdded => (true, true, true), - ModOptionChangeType.OptionDeleted => (true, true, false), - ModOptionChangeType.OptionMoved => (true, false, false), - ModOptionChangeType.OptionFilesChanged => (false, true, false), - ModOptionChangeType.OptionFilesAdded => (false, true, true), - ModOptionChangeType.OptionSwapsChanged => (false, true, false), - ModOptionChangeType.OptionMetaChanged => (false, true, false), - ModOptionChangeType.DisplayChange => (false, false, false), + ModOptionChangeType.GroupRenamed => (true, false, false), + ModOptionChangeType.GroupAdded => (true, false, false), + ModOptionChangeType.GroupDeleted => (true, true, false), + ModOptionChangeType.GroupMoved => (true, false, false), + ModOptionChangeType.GroupTypeChanged => (true, true, true), + ModOptionChangeType.PriorityChanged => (true, true, true), + ModOptionChangeType.OptionAdded => (true, true, true), + ModOptionChangeType.OptionDeleted => (true, true, false), + ModOptionChangeType.OptionMoved => (true, false, false), + ModOptionChangeType.OptionFilesChanged => (false, true, false), + ModOptionChangeType.OptionFilesAdded => (false, true, true), + ModOptionChangeType.OptionSwapsChanged => (false, true, false), + ModOptionChangeType.OptionMetaChanged => (false, true, false), + ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false), - _ => (false, false, false), + _ => (false, false, false), }; } } diff --git a/Penumbra/Mods/Manager/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs similarity index 92% rename from Penumbra/Mods/Manager/MultiModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index e6b2bac1..c48d2d40 100644 --- a/Penumbra/Mods/Manager/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -6,14 +6,14 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { public void ChangeToSingle(MultiModGroup group) { - var idx = group.GetIndex(); + var idx = group.GetIndex(); var singleGroup = group.ConvertToSingle(); group.Mod.Groups[idx] = singleGroup; SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); @@ -34,7 +34,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; @@ -50,7 +50,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe var newOption = new MultiSubMod(group) { - Name = option.Name, + Name = option.Name, Description = option.Description, }; diff --git a/Penumbra/Mods/Manager/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs similarity index 90% rename from Penumbra/Mods/Manager/SingleModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 4999ff60..556416c6 100644 --- a/Penumbra/Mods/Manager/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -6,14 +6,14 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { public void ChangeToMulti(SingleModGroup group) { - var idx = group.GetIndex(); + var idx = group.GetIndex(); var multiGroup = group.ConvertToMulti(); group.Mod.Groups[idx] = multiGroup; SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); @@ -23,7 +23,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; @@ -31,7 +31,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS { var newOption = new SingleSubMod(group) { - Name = option.Name, + Name = option.Name, Description = option.Description, }; if (option is IModDataContainer data) diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 39ee1860..7fe48365 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -3,7 +3,7 @@ using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Settings; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2d595ec1..2e53bd22 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -75,7 +75,7 @@ - + diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index cd55beb0..4db0df47 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -16,6 +16,7 @@ using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index fcd76a51..862852fa 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -16,6 +16,7 @@ using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; +using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 68e1b880..b431e595 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -34,9 +34,14 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.3, )", - "resolved": "3.1.3", - "contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg==" + "requested": "[3.1.4, )", + "resolved": "3.1.4", + "contentHash": "lFIdxgGDA5iYkUMRFOze7BGLcdpoLFbR+a20kc1W7NepvzU7ejtxtWOg9RvgG7kb9tBoJ3ONYOK6kLil/dgF1w==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", @@ -75,6 +80,7 @@ "ottergui": { "type": "Project", "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" } }, @@ -88,7 +94,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[1.0.15, )", + "Penumbra.Api": "[5.0.0, )", "Penumbra.String": "[1.0.4, )" } }, From 9084b43e3ebef4e00471aa8839ed29aecd0fc25a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Apr 2024 12:01:54 +0200 Subject: [PATCH 1658/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9208c9c2..1b0b5469 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af +Subproject commit 1b0b5469f792999e5b412d4f0c3ed77d9994d7b7 From 2e76148fba396e8c0e3fc0dc8ad632e0726e059d Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:14:40 +1000 Subject: [PATCH 1659/2451] Ensure materials end in .mtrl --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b8c0176a..b3460667 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -290,10 +290,13 @@ public partial class ModEditWindow /// While materials can be relative (`/mt_...`) or absolute (`bg/...`), /// they invariably must contain at least one directory seperator. /// Missing this can lead to a crash. + /// + /// They must also be at least one character (though this is enforced + /// by containing a `/`), and end with `.mtrl`. /// public bool ValidateMaterial(string material) { - return material.Contains('/'); + return material.Contains('/') && material.EndsWith(".mtrl"); } /// Remove the material given by the index. From c96adcf557597a712582b09eee62978ad479f2b7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:15:00 +1000 Subject: [PATCH 1660/2451] Prevent saving invalid files --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c891d33a..66f38bab 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -204,8 +204,9 @@ public class FileEditor( private void SaveButton() { + var canSave = _changed && _currentFile != null && _currentFile.Valid; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, - $"Save the selected {fileType} file with all changes applied. This is not revertible.", !_changed)) + $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) { compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); if (owner.Mod != null) From 078688454a5e67a05497f92b0a834e9c5daae594 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:17:54 +1000 Subject: [PATCH 1661/2451] Show an invalid material count --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03b5169a..e075b592 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -293,7 +293,21 @@ public partial class ModEditWindow private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Materials")) + var invalidMaterialCount = tab.Mdl.Materials.Count(material => !tab.ValidateMaterial(material)); + + var oldPos = ImGui.GetCursorPosY(); + var header = ImGui.CollapsingHeader("Materials"); + var newPos = ImGui.GetCursorPos(); + if (invalidMaterialCount > 0) + { + var text = $"{invalidMaterialCount} invalid material{(invalidMaterialCount > 1 ? "s" : "")}"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(0xFF0000FF, text); + ImGui.SetCursorPos(newPos); + } + + if (!header) return false; using var table = ImRaii.Table(string.Empty, disabled ? 2 : 4, ImGuiTableFlags.SizingFixedFit); @@ -382,7 +396,8 @@ public partial class ModEditWindow { ImGuiComponents.HelpMarker( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\").", + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" + + "and must end in \".mtrl\".", FontAwesomeIcon.TimesCircle); } From 9063d131bae492f5186b2a96d01e8135f7e2364f Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:41:35 +1000 Subject: [PATCH 1662/2451] Use validation logic for new material field --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index e075b592..0be95c99 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -337,7 +337,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; + var validName = tab.ValidateMaterial(_modelNewMaterial); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) { @@ -346,6 +346,8 @@ public partial class ModEditWindow _modelNewMaterial = string.Empty; } ImGui.TableNextColumn(); + if (!validName && _modelNewMaterial.Length > 0) + DrawInvalidMaterialMarker(); return ret; } @@ -392,18 +394,22 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Add markers to invalid materials. if (!tab.ValidateMaterial(temp)) - using (var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true)) - { - ImGuiComponents.HelpMarker( - "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" - + "and must end in \".mtrl\".", - FontAwesomeIcon.TimesCircle); - } + DrawInvalidMaterialMarker(); return ret; } + private void DrawInvalidMaterialMarker() + { + using var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true); + + ImGuiComponents.HelpMarker( + "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" + + "and must end in \".mtrl\".", + FontAwesomeIcon.TimesCircle); + } + private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) { using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); From 46a111d15287112a09ee3acf0b7cba0a30454ef2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 22:42:28 +1000 Subject: [PATCH 1663/2451] Fix marker --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0be95c99..a7d39c6e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -401,13 +401,13 @@ public partial class ModEditWindow private void DrawInvalidMaterialMarker() { - using var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); - ImGuiComponents.HelpMarker( + ImGuiUtil.HoverTooltip( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" - + "and must end in \".mtrl\".", - FontAwesomeIcon.TimesCircle); + + "and must end in \".mtrl\"."); } private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) From 36fc251d5b4265c9fb6d0af8c9cfc88233c71697 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 May 2024 18:52:52 +0200 Subject: [PATCH 1664/2451] Fix a bunch of issues. --- Penumbra.GameData | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 19 +- Penumbra/Mods/Groups/ImcModGroup.cs | 4 +- Penumbra/Mods/Groups/ModSaveGroup.cs | 7 +- Penumbra/Mods/Groups/MultiModGroup.cs | 4 +- Penumbra/Mods/Groups/SingleModGroup.cs | 13 +- .../OptionEditor/MultiModGroupEditor.cs | 4 +- .../OptionEditor/SingleModGroupEditor.cs | 4 +- Penumbra/Mods/SubMods/SubMod.cs | 1 - Penumbra/Services/BackupService.cs | 1 + Penumbra/Services/StaticServiceManager.cs | 1 - Penumbra/UI/ModsTab/ModGroupDrawer.cs | 233 +++++++++++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 214 ++++++++++++++ Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 272 ++---------------- 14 files changed, 513 insertions(+), 266 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ModGroupDrawer.cs create mode 100644 Penumbra/UI/ModsTab/ModGroupEditDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 1b0b5469..595ac572 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1b0b5469f792999e5b412d4f0c3ed77d9994d7b7 +Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index a268ba0f..d7138434 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -12,16 +12,23 @@ public interface ITexToolsGroup public IReadOnlyList OptionData { get; } } +public enum GroupDrawBehaviour +{ + SingleSelection, + MultiSelection, +} + public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e233f82e..e58d855a 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -21,6 +21,9 @@ public class ImcModGroup(Mod mod) : IModGroup public GroupType Type => GroupType.Imc; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; @@ -150,7 +153,6 @@ public class ImcModGroup(Mod mod) : IModGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index efdcde09..ed3c8857 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -56,10 +56,10 @@ public readonly struct ModSaveGroup : ISavable { _basePath = (container.Mod as Mod)?.ModPath ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. - _defaultMod = null; + _defaultMod = container as DefaultSubMod; _onlyAscii = onlyAscii; - _group = container.Group!; - _groupIdx = _group.GetIndex(); + _group = container.Group; + _groupIdx = _group?.GetIndex() ?? -1; } public string ToFilename(FilenameService fileNames) @@ -80,7 +80,6 @@ public readonly struct ModSaveGroup : ISavable public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) { - jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index a0034be0..7495c4b4 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -17,6 +17,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; @@ -127,7 +130,6 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 0776c2af..459cec4a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -15,7 +15,10 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; } = mod; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.SingleSelection; + + public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -89,7 +92,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + { + if (!IsOption) + return; + + OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + } public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); @@ -111,7 +119,6 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } /// Create a group without a mod only for saving it in the creator. diff --git a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index c48d2d40..74362325 100644 --- a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe var idx = group.GetIndex(); var singleGroup = group.ConvertToSingle(); group.Mod.Groups[idx] = singleGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(singleGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, singleGroup.Mod, singleGroup, null, null, -1); } /// Change the internal priority of the given option. diff --git a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 556416c6..15a899a0 100644 --- a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS var idx = group.GetIndex(); var multiGroup = group.ConvertToMulti(); group.Mod.Groups[idx] = multiGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(multiGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, multiGroup.Mod, multiGroup, null, null, -1); } protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a50e397f..b984b570 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -106,7 +106,6 @@ public static class SubMod j.WriteEndObject(); j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); - j.WriteEndObject(); } /// Write the data for a selectable mod option on a JsonWriter. diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index a542dab5..bd5b3bcc 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -29,6 +29,7 @@ public class BackupService : IAsyncService list.Add(new FileInfo(fileNames.ConfigFile)); list.Add(new FileInfo(fileNames.FilesystemFile)); list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); + list.Add(new FileInfo(fileNames.PredefinedTagFile)); return list; } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 5fa1a848..19ae31a2 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -162,7 +162,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/ModsTab/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/ModGroupDrawer.cs new file mode 100644 index 00000000..e9b0b396 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupDrawer.cs @@ -0,0 +1,233 @@ +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService +{ + private readonly List<(IModGroup, int)> _blockGroupCache = []; + + public void Draw(Mod mod, ModSettings settings) + { + if (mod.Groups.Count <= 0) + return; + + _blockGroupCache.Clear(); + var useDummy = true; + foreach (var (group, idx) in mod.Groups.WithIndex()) + { + if (!group.IsOption) + continue; + + switch (group.Behaviour) + { + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: + case GroupDrawBehaviour.MultiSelection: + _blockGroupCache.Add((group, idx)); + break; + + case GroupDrawBehaviour.SingleSelection: + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + DrawSingleGroupCombo(group, idx, settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]); + break; + } + } + + useDummy = true; + foreach (var (group, idx) in _blockGroupCache) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + var option = settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]; + if (group.Behaviour is GroupDrawBehaviour.MultiSelection) + DrawMultiGroup(group, idx, option); + else + DrawSingleGroupRadio(group, idx, option); + } + } + + /// + /// Draw a single group selector as a combo box. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + { + if (combo) + for (var idx2 = 0; idx2 < options.Count; ++idx2) + { + id.Push(idx2); + var option = options[idx2]; + if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + SetModSetting(group, groupIdx, Setting.Single(idx2)); + + if (option.Description.Length > 0) + ImGuiUtil.SelectableHelpMarker(option.Description); + + id.Pop(); + } + } + + ImGui.SameLine(); + if (group.Description.Length > 0) + ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + else + ImGui.TextUnformatted(group.Name); + } + + /// + /// Draw a single group selector as a set of radio buttons. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + return; + + void DrawOptions() + { + for (var idx = 0; idx < group.Options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + if (ImGui.RadioButton(option.Name, selectedOption == idx)) + SetModSetting(group, groupIdx, Setting.Single(idx)); + + if (option.Description.Length <= 0) + continue; + + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + /// + /// Draw a multi group selector as a bordered set of checkboxes. + /// If a description is provided, add a help marker in the title. + /// + private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + DrawMultiPopup(group, groupIdx, label); + return; + + void DrawOptions() + { + for (var idx = 0; idx < options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); + + if (ImGui.Checkbox(option.Name, ref enabled)) + SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + } + + private void DrawMultiPopup(IModGroup group, int groupIdx, string label) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); + using var popup = ImRaii.Popup(label); + if (!popup) + return; + + ImGui.TextUnformatted(group.Name); + ImGui.Separator(); + if (ImGui.Selectable("Enable All")) + SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); + + if (ImGui.Selectable("Disable All")) + SetModSetting(group, groupIdx, Setting.Zero); + } + + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) + { + if (options.Count <= config.OptionGroupCollapsibleMin) + { + draw(); + } + else + { + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; + var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + + 2 * ImGui.GetStyle().FramePadding.X; + minWidth = Math.Max(buttonWidth, minWidth); + if (shown) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy(UiHelpers.IconButtonSize); + using (var _ = ImRaii.Group()) + { + draw(); + } + + + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(pos); + if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + + ImGui.SetCursorPos(endPos); + } + else + { + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetFrameHeight() + + ImGui.GetStyle().FramePadding.X; + var width = Math.Max(optionWidth, minWidth); + if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + } + } + } + + private ModCollection Current + => collectionManager.Active.Current; + + private void SetModSetting(IModGroup group, int groupIdx, Setting setting) + => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs new file mode 100644 index 00000000..6b62d5b8 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -0,0 +1,214 @@ +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +{ + private Vector2 _buttonSize; + private float _priorityWidth; + private float _groupNameWidth; + private float _spacing; + + private string? _currentGroupName; + private ModPriority? _currentGroupPriority; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid; + private IModGroup? _deleteGroup; + private IModGroup? _moveGroup; + private int _moveTo; + + private string? _currentOptionName; + private ModPriority? _currentOptionPriority; + private IModOption? _currentOptionEdited; + private IModOption? _deleteOption; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SameLine() + => ImGui.SameLine(0, _spacing); + + public void Draw(Mod mod) + { + _buttonSize = new Vector2(ImGui.GetFrameHeight()); + _priorityWidth = 50 * ImGuiHelpers.GlobalScale; + _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + + + FinishGroupCleanup(); + } + + private void FinishGroupCleanup() + { + if (_deleteGroup != null) + { + modManager.OptionEditor.DeleteModGroup(_deleteGroup); + _deleteGroup = null; + } + + if (_deleteOption != null) + { + modManager.OptionEditor.DeleteOption(_deleteOption); + _deleteOption = null; + } + + if (_moveGroup != null) + { + modManager.OptionEditor.MoveModGroup(_moveGroup, _moveTo); + _moveGroup = null; + } + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = ImRaii.PushId(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group); + switch (group) + { + case SingleModGroup s: + DrawSingleGroup(s, idx); + break; + case MultiModGroup m: + DrawMultiGroup(m, idx); + break; + case ImcModGroup i: + DrawImcGroup(i, idx); + break; + } + } + + private void DrawGroupNameRow(IModGroup group) + { + DrawGroupName(group); + SameLine(); + DrawGroupDelete(group); + SameLine(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; + ImGui.SetNextItemWidth(_groupNameWidth); + using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); + if (ImGui.InputText("##GroupName", ref text, 256)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupName != null && _isGroupNameValid) + modManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Group Name" + : "Current name can not be used for this group."; + ImGuiUtil.HoverTooltip(tt); + } + + private void DrawGroupDelete(IModGroup group) + { + var enabled = config.DeleteModModifier.IsActive(); + var tt = enabled + ? "Delete this option group." + : $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) + _deleteGroup = group; + } + + private void DrawGroupPriority(IModGroup group) + { + var priority = _currentGroupEdited == group + ? (_currentGroupPriority ?? group.Priority).Value + : group.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) + { + _currentGroupEdited = group; + _currentGroupPriority = new ModPriority(priority); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupPriority.HasValue) + modManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); + _currentGroupEdited = null; + _currentGroupPriority = null; + } + + ImGuiUtil.HoverTooltip("Group Priority"); + } + + private void DrawGroupMoveButtons(IModGroup group, int idx) + { + var isFirst = idx == 0; + var tt = isFirst ? "Can not move this group further upwards." : $"Move this group up to group {idx}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, isFirst, true)) + { + _moveGroup = group; + _moveTo = idx - 1; + } + + SameLine(); + var isLast = idx == group.Mod.Groups.Count - 1; + tt = isLast + ? "Can not move this group further downwards." + : $"Move this group down to group {idx + 2}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, isLast, true)) + { + _moveGroup = group; + _moveTo = idx + 1; + } + } + + private void DrawGroupOpenFile(IModGroup group, int idx) + { + var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); + var fileExists = File.Exists(fileName); + var tt = fileExists + ? $"Open the {group.Name} json file in the text editor of your choice." + : $"The {group.Name} json file does not exist."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + } + + + private void DrawSingleGroup(SingleModGroup group, int idx) + { } + + private void DrawMultiGroup(MultiModGroup group, int idx) + { } + + private void DrawImcGroup(ImcModGroup group, int idx) + { } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1326a763..fc5311d9 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,51 +1,37 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.UI.Classes; -using Dalamud.Interface.Components; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Mods.SubMods; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; -public class ModPanelSettingsTab : ITab +public class ModPanelSettingsTab( + CollectionManager collectionManager, + ModManager modManager, + ModFileSystemSelector selector, + TutorialService tutorial, + CommunicatorService communicator, + ModGroupDrawer modGroupDrawer) + : ITab, IUiService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionManager _collectionManager; - private readonly ModFileSystemSelector _selector; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private bool _inherited; private ModSettings _settings = null!; private ModCollection _collection = null!; - private bool _empty; private int? _currentPriority; - public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, - TutorialService tutorial, CommunicatorService communicator, Configuration config) - { - _collectionManager = collectionManager; - _communicator = communicator; - _modManager = modManager; - _selector = selector; - _tutorial = tutorial; - _config = config; - } - public ReadOnlySpan Label => "Settings"u8; public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); public void Reset() => _currentPriority = null; @@ -56,53 +42,24 @@ public class ModPanelSettingsTab : ITab if (!child) return; - _settings = _selector.SelectedSettings; - _collection = _selector.SelectedSettingCollection; - _inherited = _collection != _collectionManager.Active.Current; - _empty = _settings == ModSettings.Empty; - + _settings = selector.SelectedSettings; + _collection = selector.SelectedSettingCollection; + _inherited = _collection != collectionManager.Active.Current; DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier); DrawEnabledInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); DrawPriorityInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.Priority); + tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); - _communicator.PostEnabledDraw.Invoke(_selector.Selected!.Identifier); - - if (_selector.Selected!.Groups.Count > 0) - { - var useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - DrawSingleGroupCombo(group, idx); - } - - useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex().Where(g => g.Value.IsOption)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - switch (group.Type) - { - case GroupType.Multi: - DrawMultiGroup(group, idx); - break; - case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: - DrawSingleGroupRadio(group, idx); - break; - } - } - } + communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier); + modGroupDrawer.Draw(selector.Selected!, _settings); UiHelpers.DefaultLineSpace(); - _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier); } /// Draw a big red bar if the current setting is inherited. @@ -113,8 +70,8 @@ public class ModPanelSettingsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, false); + if (ImUtf8.Button($"These settings are inherited from {_collection.Name}.", width)) + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -127,8 +84,8 @@ public class ModPanelSettingsTab : ITab if (!ImGui.Checkbox("Enabled", ref enabled)) return; - _modManager.SetKnown(_selector.Selected!); - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, _selector.Selected!, enabled); + modManager.SetKnown(selector.Selected!); + collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled); } /// @@ -146,7 +103,8 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority.Value) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, new ModPriority(_currentPriority.Value)); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -162,189 +120,15 @@ public class ModPanelSettingsTab : ITab private void DrawRemoveSettings() { const string text = "Inherit Settings"; - if (_inherited || _empty) + if (_inherited || _settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, true); + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); } - - /// - /// Draw a single group selector as a combo box. - /// If a description is provided, add a help marker besides it. - /// - private void DrawSingleGroupCombo(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) - { - if (combo) - for (var idx2 = 0; idx2 < options.Count; ++idx2) - { - id.Push(idx2); - var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx2)); - - if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); - - id.Pop(); - } - } - - ImGui.SameLine(); - if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); - else - ImGui.TextUnformatted(group.Name); - } - - // Draw a single group selector as a set of radio buttons. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupRadio(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - return; - - void DrawOptions() - { - for (var idx = 0; idx < group.Options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx)); - - if (option.Description.Length <= 0) - continue; - - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - - - private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) - { - if (options.Count <= _config.OptionGroupCollapsibleMin) - { - draw(); - } - else - { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {options.Count} Options"; - var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) - + 2 * ImGui.GetStyle().FramePadding.X; - minWidth = Math.Max(buttonWidth, minWidth); - if (shown) - { - var pos = ImGui.GetCursorPos(); - ImGui.Dummy(UiHelpers.IconButtonSize); - using (var _ = ImRaii.Group()) - { - draw(); - } - - - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); - var endPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - - ImGui.SetCursorPos(endPos); - } - else - { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) - + ImGui.GetStyle().ItemInnerSpacing.X - + ImGui.GetFrameHeight() - + ImGui.GetStyle().FramePadding.X; - var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - } - } - } - - /// - /// Draw a multi group selector as a bordered set of checkboxes. - /// If a description is provided, add a help marker in the title. - /// - private void DrawMultiGroup(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); - - DrawMultiPopup(group, groupIdx, label); - return; - - void DrawOptions() - { - for (var idx = 0; idx < options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var setting = flags.HasFlag(idx); - - if (ImGui.Checkbox(option.Name, ref setting)) - { - flags = flags.SetBit(idx, setting); - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); - } - - if (option.Description.Length > 0) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - } - - private void DrawMultiPopup(IModGroup group, int groupIdx, string label) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); - using var popup = ImRaii.Popup(label); - if (!popup) - return; - - ImGui.TextUnformatted(group.Name); - ImGui.Separator(); - if (ImGui.Selectable("Enable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Options.Count)); - - if (ImGui.Selectable("Disable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); - } } From 7553b5da8ac44fe4d058934033a5aa18ff32ffa1 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 4 May 2024 04:01:28 +1000 Subject: [PATCH 1665/2451] Fix float imprecision on blend weights --- .../Import/Models/Import/VertexAttribute.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b7f5dcf1..a4651776 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -138,7 +138,27 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildNByte4(values[index]) + index => { + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var originalValues = values[index]; + var byteValues = BuildNByte4(originalValues); + + var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + while (adjustment != 0) + { + var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); + var closestIndex = Enumerable.Range(0, 4) + .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); + adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + } + + return byteValues; + } ); } From 2a5df2dfb05469a15cf1386e70139a93eaad1d7d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:19:07 +0200 Subject: [PATCH 1666/2451] Create permanent backup before migrating collections. --- Penumbra/Configuration.cs | 2 +- Penumbra/Services/BackupService.cs | 14 ++++++++++++-- Penumbra/Services/ConfigMigrationService.cs | 16 +++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 98668e8a..a065bc26 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -136,7 +136,7 @@ public class Configuration : IPluginConfiguration, ISavable /// Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 8; + public const int CurrentVersion = 9; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index bd5b3bcc..88b99de1 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -7,6 +7,10 @@ namespace Penumbra.Services; public class BackupService : IAsyncService { + private readonly Logger _logger; + private readonly DirectoryInfo _configDirectory; + private readonly IReadOnlyList _fileNames; + /// public Task Awaiter { get; } @@ -17,10 +21,16 @@ public class BackupService : IAsyncService /// Start a backup process on the collected files. public BackupService(Logger logger, FilenameService fileNames) { - var files = PenumbraFiles(fileNames); - Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files)); + _logger = logger; + _fileNames = PenumbraFiles(fileNames); + _configDirectory = new DirectoryInfo(fileNames.ConfigDirectory); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), _fileNames)); } + /// Create a permanent backup with a given name for migrations. + public void CreateMigrationBackup(string name) + => Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name); + /// Collect all relevant files for penumbra configuration. private static IReadOnlyList PenumbraFiles(FilenameService fileNames) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index fafaa0e5..1f6ac170 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -22,7 +22,7 @@ namespace Penumbra.Services; /// Contains everything to migrate from older versions of the config to the current, /// including deprecated fields. /// -public class ConfigMigrationService(SaveService saveService) : IService +public class ConfigMigrationService(SaveService saveService, BackupService backupService) : IService { private Configuration _config = null!; private JObject _data = null!; @@ -73,9 +73,23 @@ public class ConfigMigrationService(SaveService saveService) : IService Version5To6(); Version6To7(); Version7To8(); + Version8To9(); AddColors(config, true); } + // Migrate to ephemeral config. + private void Version8To9() + { + if (_config.Version != 8) + return; + + backupService.CreateMigrationBackup("pre_collection_identifiers"); + _config.Version = 9; + _config.Ephemeral.Version = 9; + _config.Save(); + _config.Ephemeral.Save(); + } + // Migrate to ephemeral config. private void Version7To8() { From d8dad91e899a4055e5fe19dbc1255333a07f9033 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:24:01 +0200 Subject: [PATCH 1667/2451] Oop, not yet. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index fc5311d9..107a2d04 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -2,7 +2,6 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; -using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.UI.Classes; @@ -70,7 +69,7 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImUtf8.Button($"These settings are inherited from {_collection.Name}.", width)) + if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" From 1f2f66b1141f83613c3799faf80439b1142bd571 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:34:02 +0200 Subject: [PATCH 1668/2451] Meh. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 20c4a6c5..bc2afed8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 20c4a6c53103d9fa8dec63babc628c9d01f094c0 +Subproject commit bc2afed8a873d1f9517eefe7a7296bc5b83e693b From bbbf65eb4c0c9d8dd77ac8f8101ad2ac86433c1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 May 2024 15:49:49 +0200 Subject: [PATCH 1669/2451] Fix bug preventing deduplication. --- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 84a832a2..2e6dc1d6 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -84,7 +84,10 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (useModManager) modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); else + { + subMod.Files = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport)); + } } } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index ed3c8857..7efc76a6 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -40,7 +40,7 @@ public readonly struct ModSaveGroup : ISavable _basePath = basePath; _defaultMod = container as DefaultSubMod; _onlyAscii = onlyAscii; - if (_defaultMod == null) + if (_defaultMod != null) { _groupIdx = -1; _group = null; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index de2b6a34..bff0092a 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -89,7 +89,7 @@ public class CollectionSelectHeader var collection = _resolver.PlayerCollection(); return CheckCollection(collection) switch { - CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), + CollectionState.Empty => (collection, "None", "The loaded player character is configured to use no mods.", true), CollectionState.Selected => (collection, collection.Name, "The collection configured to apply to the loaded player character is already selected as the current collection.", true), CollectionState.Available => (collection, collection.Name, From 32dbf419e254ca432055f6e4b08b684b5a03447b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 May 2024 19:58:35 +0200 Subject: [PATCH 1670/2451] Fix single group default options not applying. --- Penumbra/Mods/Editor/DuplicateManager.cs | 2 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 2e6dc1d6..47aa18dc 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -82,7 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; if (useModManager) + { modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); + } else { subMod.Files = dict; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 459cec4a..bc463c1e 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -93,7 +93,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { - if (!IsOption) + if (OptionData.Count == 0) return; OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); From d47d31b665ffa4cd14774f04888a6c50c7a37a72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 May 2024 18:10:59 +0200 Subject: [PATCH 1671/2451] achieve feature parity... I think. --- OtterGui | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/UI/ModsTab/DescriptionEditPopup.cs | 114 ++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 424 ++++++++++++++++---- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 420 +------------------ 5 files changed, 473 insertions(+), 489 deletions(-) create mode 100644 Penumbra/UI/ModsTab/DescriptionEditPopup.cs diff --git a/OtterGui b/OtterGui index bc2afed8..866389b3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bc2afed8a873d1f9517eefe7a7296bc5b83e693b +Subproject commit 866389b3988d9c4926a786f6c78ac9d5265591ac diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index d7138434..2ec60f7e 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -20,7 +20,7 @@ public enum GroupDrawBehaviour public interface IModGroup { - public const int MaxMultiOptions = 63; + public const int MaxMultiOptions = 32; public Mod Mod { get; } public string Name { get; set; } diff --git a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs new file mode 100644 index 00000000..c284afc3 --- /dev/null +++ b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs @@ -0,0 +1,114 @@ +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public class DescriptionEditPopup(ModManager modManager) : IUiService +{ + private static ReadOnlySpan PopupId + => "PenumbraEditDescription"u8; + + private bool _hasBeenEdited; + private string _description = string.Empty; + + private object? _current; + private bool _opened; + + public void Open(Mod mod) + { + _current = mod; + _opened = true; + _hasBeenEdited = false; + _description = mod.Description; + } + + public void Open(IModGroup group) + { + _current = group; + _opened = true; + _hasBeenEdited = false; + _description = group.Description; + } + + public void Open(IModOption option) + { + _current = option; + _opened = true; + _hasBeenEdited = false; + _description = option.Description; + } + + public void Draw() + { + if (_current == null) + return; + + if (_opened) + { + _opened = false; + ImUtf8.OpenPopup(PopupId); + } + + var inputSize = ImGuiHelpers.ScaledVector2(800); + using var popup = ImUtf8.Popup(PopupId); + if (!popup) + return; + + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + + ImUtf8.InputMultiLineOnDeactivated("##editDescription"u8, ref _description, inputSize); + _hasBeenEdited |= ImGui.IsItemEdited(); + UiHelpers.DefaultLineSpace(); + + var buttonSize = new Vector2(ImUtf8.GlobalScale * 100, 0); + + var width = 2 * buttonSize.X + + 4 * ImUtf8.FramePadding.X + + ImUtf8.ItemSpacing.X; + + ImGui.SetCursorPosX((inputSize.X - width) / 2); + DrawSaveButton(buttonSize); + ImGui.SameLine(); + DrawCancelButton(buttonSize); + } + + private void DrawSaveButton(Vector2 buttonSize) + { + if (!ImUtf8.ButtonEx("Save"u8, _hasBeenEdited ? [] : "No changes made yet."u8, buttonSize, !_hasBeenEdited)) + return; + + switch (_current) + { + case Mod mod: + modManager.DataEditor.ChangeModDescription(mod, _description); + break; + case IModGroup group: + modManager.OptionEditor.ChangeGroupDescription(group, _description); + break; + case IModOption option: + modManager.OptionEditor.ChangeOptionDescription(option, _description); + break; + } + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } + + private void DrawCancelButton(Vector2 buttonSize) + { + if (!ImUtf8.Button("Cancel"u8, buttonSize) && !ImGui.IsKeyPressed(ImGuiKey.Escape)) + return; + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index 6b62d5b8..5652fa98 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,11 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -17,87 +18,79 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup) : IUiService { + private static ReadOnlySpan DragDropLabel + => "##DragOption"u8; + private Vector2 _buttonSize; + private Vector2 _availableWidth; private float _priorityWidth; private float _groupNameWidth; + private float _optionNameWidth; private float _spacing; + private Vector2 _optionIdxSelectable; + private bool _deleteEnabled; private string? _currentGroupName; private ModPriority? _currentGroupPriority; private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid; - private IModGroup? _deleteGroup; - private IModGroup? _moveGroup; - private int _moveTo; + private bool _isGroupNameValid = true; - private string? _currentOptionName; - private ModPriority? _currentOptionPriority; - private IModOption? _currentOptionEdited; - private IModOption? _deleteOption; + private string? _newOptionName; + private IModGroup? _newOptionGroup; + private readonly Queue _actionQueue = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SameLine() - => ImGui.SameLine(0, _spacing); + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; public void Draw(Mod mod) { - _buttonSize = new Vector2(ImGui.GetFrameHeight()); - _priorityWidth = 50 * ImGuiHelpers.GlobalScale; - _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; - _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + PrepareStyle(); + using var id = ImUtf8.PushId("##GroupEdit"u8); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + DrawGroup(group, groupIdx); - FinishGroupCleanup(); - } - - private void FinishGroupCleanup() - { - if (_deleteGroup != null) - { - modManager.OptionEditor.DeleteModGroup(_deleteGroup); - _deleteGroup = null; - } - - if (_deleteOption != null) - { - modManager.OptionEditor.DeleteOption(_deleteOption); - _deleteOption = null; - } - - if (_moveGroup != null) - { - modManager.OptionEditor.MoveModGroup(_moveGroup, _moveTo); - _moveGroup = null; - } + while (_actionQueue.TryDequeue(out var action)) + action.Invoke(); } private void DrawGroup(IModGroup group, int idx) { - using var id = ImRaii.PushId(idx); + using var id = ImUtf8.PushId(idx); using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group); + DrawGroupNameRow(group, idx); switch (group) { case SingleModGroup s: - DrawSingleGroup(s, idx); + DrawSingleGroup(s); break; case MultiModGroup m: - DrawMultiGroup(m, idx); + DrawMultiGroup(m); break; case ImcModGroup i: - DrawImcGroup(i, idx); + DrawImcGroup(i); break; } } - private void DrawGroupNameRow(IModGroup group) + private void DrawGroupNameRow(IModGroup group, int idx) { DrawGroupName(group); - SameLine(); + ImUtf8.SameLineInner(); + DrawGroupMoveButtons(group, idx); + ImUtf8.SameLineInner(); + DrawGroupOpenFile(group, idx); + ImUtf8.SameLineInner(); + DrawGroupDescription(group); + ImUtf8.SameLineInner(); DrawGroupDelete(group); - SameLine(); + ImUtf8.SameLineInner(); DrawGroupPriority(group); } @@ -106,11 +99,11 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; ImGui.SetNextItemWidth(_groupNameWidth); using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImGui.InputText("##GroupName", ref text, 256)) + if (ImUtf8.InputText("##GroupName"u8, ref text)) { _currentGroupEdited = group; _currentGroupName = text; - _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); } if (ImGui.IsItemDeactivated()) @@ -123,20 +116,21 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf } var tt = _isGroupNameValid - ? "Group Name" - : "Current name can not be used for this group."; - ImGuiUtil.HoverTooltip(tt); + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); } private void DrawGroupDelete(IModGroup group) { - var enabled = config.DeleteModModifier.IsActive(); - var tt = enabled - ? "Delete this option group." - : $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."; + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteModGroup(group)); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) - _deleteGroup = group; + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option group."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); } private void DrawGroupPriority(IModGroup group) @@ -162,36 +156,41 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf ImGuiUtil.HoverTooltip("Group Priority"); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) + descriptionPopup.Open(group); + } + private void DrawGroupMoveButtons(IModGroup group, int idx) { var isFirst = idx == 0; - var tt = isFirst ? "Can not move this group further upwards." : $"Move this group up to group {idx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, isFirst, true)) - { - _moveGroup = group; - _moveTo = idx - 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx - 1)); - SameLine(); + if (isFirst) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + ImUtf8.HoverTooltip($"Move this group up to group {idx}."); + + + ImUtf8.SameLineInner(); var isLast = idx == group.Mod.Groups.Count - 1; - tt = isLast - ? "Can not move this group further downwards." - : $"Move this group down to group {idx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, isLast, true)) - { - _moveGroup = group; - _moveTo = idx + 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); } private void DrawGroupOpenFile(IModGroup group, int idx) { var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); - var tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); @@ -200,15 +199,274 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf { Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); } + + if (fileExists) + ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); } + private void DrawSingleGroup(SingleModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); - private void DrawSingleGroup(SingleModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - private void DrawMultiGroup(MultiModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionName(option); - private void DrawImcGroup(ImcModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + } + + DrawNewOption(group); + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + if (ImUtf8.ButtonEx("Convert to Multi Group", _availableWidth, !convertible)) + _actionQueue.Enqueue(() => modManager.OptionEditor.SingleEditor.ChangeToMulti(group)); + if (!convertible) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawMultiGroup(MultiModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + DrawOptionPriority(option); + } + + DrawNewOption(group); + if (ImUtf8.Button("Convert to Single Group"u8, _availableWidth)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MultiEditor.ChangeToSingle(group)); + } + + private void DrawImcGroup(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + ImGui.AlignTextToFramePadding(); + ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: _optionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDescription(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPriority(MultiSubMod option) + { + var priority = option.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + modManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); + ImUtf8.HoverTooltip("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionName(IModOption option) + { + var name = option.Name; + ImGui.SetNextItemWidth(_optionNameWidth); + if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) + modManager.OptionEditor.RenameOption(option, name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDelete(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawNewOption(SingleModGroup group) + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(MultiModGroup group) + { + var count = group.Options.Count; + if (count >= IModGroup.MaxMultiOptions) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawNewOptionBase(IModGroup group, int count) + { + ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); + Target(group, count); + + ImUtf8.SameLineInner(); + ImUtf8.IconDummy(); + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? _newOptionName ?? string.Empty + : string.Empty; + if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) + { + _newOptionName = newName; + _newOptionGroup = group; + } + + ImUtf8.SameLineInner(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + if (option.Group is not ITexToolsGroup) + return; + + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + } + + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (group is not ITexToolsGroup) + return; + + if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + return; + + if (_dragDropGroup != null && _dragDropOption != null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => + { + modManager.OptionEditor.DeleteOption(sourceOption); + if (modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + modManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * ImUtf8.GlobalScale; + _buttonSize = new Vector2(ImUtf8.FrameHeight); + _priorityWidth = 50 * ImUtf8.GlobalScale; + _availableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + _priorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + _optionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); + _optionNameWidth = totalWidth - _optionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 862852fa..a5db15b6 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,21 +1,17 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; -using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab; @@ -30,15 +26,13 @@ public class ModPanelEditTab( FilenameService filenames, ModExportManager modExportManager, Configuration config, - PredefinedTagManager predefinedTagManager) + PredefinedTagManager predefinedTagManager, + ModGroupEditDrawer groupEditDrawer, + DescriptionEditPopup descriptionPopup) : ITab { - private readonly ModManager _modManager = modManager; - private readonly TagButtons _modTags = new(); - private Vector2 _cellPadding = Vector2.Zero; - private Vector2 _itemSpacing = Vector2.Zero; private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; @@ -54,9 +48,6 @@ public class ModPanelEditTab( _leaf = selector.SelectedLeaf!; _mod = selector.Selected!; - _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; - _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; - EditButtons(); EditRegularMeta(); UiHelpers.DefaultLineSpace(); @@ -77,21 +68,18 @@ public class ModPanelEditTab( var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); if (tagIdx >= 0) - _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); + modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); if (sharedTagsEnabled) predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(filenames, _modManager, _mod, config.ReplaceNonAsciiOnImport); + AddOptionGroup.Draw(filenames, modManager, _mod, config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); - for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) - EditGroup(groupIdx); - - EndActions(); - DescriptionEdit.DrawPopup(_modManager); + groupEditDrawer.Draw(_mod); + descriptionPopup.Draw(); } public void Reset() @@ -99,7 +87,6 @@ public class ModPanelEditTab( AddOptionGroup.Reset(); MoveDirectory.Reset(); Input.Reset(); - OptionTable.Reset(); } /// The general edit row for non-detailed mod edits. @@ -117,10 +104,10 @@ public class ModPanelEditTab( if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n" + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", false)) - _modManager.ReloadMod(_mod); + modManager.ReloadMod(_mod); BackupButtons(buttonSize); - MoveDirectory.Draw(_modManager, _mod, buttonSize); + MoveDirectory.Draw(modManager, _mod, buttonSize); UiHelpers.DefaultLineSpace(); DrawUpdateBibo(buttonSize); @@ -169,7 +156,7 @@ public class ModPanelEditTab( : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) - backup.Restore(_modManager); + backup.Restore(modManager); if (backup.Exists) { ImGui.SameLine(); @@ -186,24 +173,24 @@ public class ModPanelEditTab( private void EditRegularMeta() { if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModName(_mod, newName); + modManager.DataEditor.ChangeModName(_mod, newName); if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); + modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModVersion(_mod, newVersion); + modManager.DataEditor.ChangeModVersion(_mod, newVersion); if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); + modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + descriptionPopup.Open(_mod); ImGui.SameLine(); var fileExists = File.Exists(filenames.ModMetaPath(_mod)); @@ -215,16 +202,6 @@ public class ModPanelEditTab( Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } - /// Do some edits outside of iterations. - private readonly Queue _delayedActions = new(); - - /// Delete a marked group or option outside of iteration. - private void EndActions() - { - while (_delayedActions.TryDequeue(out var action)) - action.Invoke(); - } - /// Text input to add a new option group at the end of the current groups. private static class AddOptionGroup { @@ -309,372 +286,6 @@ public class ModPanelEditTab( } } - /// Open a popup to edit a multi-line mod or option description. - private static class DescriptionEdit - { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static string _oldDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; - - public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) - { - _newDescriptionIdx = groupIdx; - _newDescriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 - ? mod.Description - : optionIdx < 0 - ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx].Options[optionIdx].Description; - _oldDescription = _newDescription; - - _mod = mod; - ImGui.OpenPopup(PopupName); - } - - public static void DrawPopup(ModManager modManager) - { - if (_mod == null) - return; - - using var popup = ImRaii.Popup(PopupName); - if (!popup) - return; - - if (ImGui.IsWindowAppearing()) - ImGui.SetKeyboardFocusHere(); - - ImGui.InputTextMultiline("##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2(800, 800)); - UiHelpers.DefaultLineSpace(); - - var buttonSize = ImGuiHelpers.ScaledVector2(100, 0); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); - - var tooltip = _newDescription != _oldDescription ? string.Empty : "No changes made yet."; - - if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) - { - switch (_newDescriptionIdx) - { - case Input.Description: - modManager.DataEditor.ChangeModDescription(_mod, _newDescription); - break; - case >= 0: - if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); - else - modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], - _newDescription); - - break; - } - - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (!ImGui.Button("Cancel", buttonSize) - && !ImGui.IsKeyPressed(ImGuiKey.Escape)) - return; - - _newDescriptionIdx = Input.None; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); - } - } - - private void EditGroup(int groupIdx) - { - var group = _mod.Groups[groupIdx]; - using var id = ImRaii.PushId(groupIdx); - using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}"); - - using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding) - .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); - - if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(group, newGroupName); - - ImGuiUtil.HoverTooltip("Group Name"); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); - - ImGui.SameLine(); - - if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(group, priority); - - ImGuiUtil.HoverTooltip("Group Priority"); - - DrawGroupCombo(group, groupIdx); - ImGui.SameLine(); - - var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); - - ImGui.SameLine(); - tt = groupIdx == _mod.Groups.Count - 1 - ? "Can not move this group further downwards." - : $"Move this group down to group {groupIdx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); - - ImGui.SameLine(); - - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, - "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); - - ImGui.SameLine(); - var fileName = filenames.OptionGroupFile(_mod, groupIdx, config.ReplaceNonAsciiOnImport); - var fileExists = File.Exists(fileName); - tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - - UiHelpers.DefaultLineSpace(); - - OptionTable.Draw(this, groupIdx); - } - - /// Draw the table displaying all options and the add new option line. - private static class OptionTable - { - private const string DragDropLabel = "##DragOption"; - - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static IModGroup? _dragDropGroup; - private static IModOption? _dragDropOption; - - public static void Reset() - { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroup = null; - _dragDropOption = null; - } - - public static void Draw(ModPanelEditTab panel, int groupIdx) - { - using var table = ImRaii.Table(string.Empty, 6, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - var maxWidth = ImGui.CalcTextSize("Option #88.").X; - ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, maxWidth); - ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, - UiHelpers.InputTextWidth.X - maxWidth - 12 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - - switch (panel._mod.Groups[groupIdx]) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - EditOption(panel, single, groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - EditOption(panel, multi, groupIdx, optionIdx); - break; - } - - DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); - } - - /// Draw a line for a single option. - private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) - { - var option = group.Options[optionIdx]; - using var id = ImRaii.PushId(optionIdx); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(option); - Target(panel, group, optionIdx); - - ImGui.TableNextColumn(); - - - if (group.Type == GroupType.Single) - { - if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - - ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); - } - else - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - - ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); - } - - ImGui.TableNextColumn(); - if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(option, newOptionName); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", - false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); - - ImGui.TableNextColumn(); - if (option is not MultiSubMod multi) - return; - - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, - 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); - - ImGuiUtil.HoverTooltip("Option priority."); - } - - /// Draw the line to add a new option. - private static void DrawNewOption(ModPanelEditTab panel, int groupIdx, Vector2 iconButtonSize) - { - var mod = panel._mod; - var group = mod.Groups[groupIdx]; - var count = group switch - { - SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.OptionData.Count, - _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), - }; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, count); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; - if (ImGui.InputTextWithHint("##newOption", "Add new option...", ref tmp, 256)) - { - _newOptionName = tmp; - _newOptionNameIdx = groupIdx; - } - - ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; - var tt = canAddGroup - ? validName ? "Add a new option to this group." : "Please enter a name for the new option." - : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - tt, !(canAddGroup && validName), true)) - return; - - panel._modManager.OptionEditor.AddOption(group, _newOptionName); - _newOptionName = string.Empty; - } - - // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModOption option) - { - if (option.Group is not ITexToolsGroup) - return; - - using var source = ImRaii.DragDropSource(); - if (!source) - return; - - if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - } - - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) - { - if (group is not ITexToolsGroup) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue(() => - { - panel._modManager.OptionEditor.DeleteOption(sourceOption); - if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - } - } - - /// Draw a combo to select single or multi group and switch between them. - private void DrawGroupCombo(IModGroup group, int groupIdx) - { - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) - _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) - _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); - return; - - static string GroupTypeName(GroupType type) - => type switch - { - GroupType.Single => "Single Group", - GroupType.Multi => "Multi Group", - _ => "Unknown", - }; - } - /// Handles input text and integers in separate fields without buffers for every single one. private static class Input { @@ -705,6 +316,7 @@ public class ModPanelEditTab( { var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; ImGui.SetNextItemWidth(width); + if (ImGui.InputText(label, ref tmp, maxLength)) { _currentEdit = tmp; From df6eb3fdd2f4afac8b8b5e911b137ce0b9d54280 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 May 2024 18:30:40 +0200 Subject: [PATCH 1672/2451] Add some early support for IMC groups. --- OtterGui | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 4 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- .../Meta/Manipulations/ImcManipulation.cs | 4 +- .../Meta/Manipulations/MetaManipulation.cs | 4 +- Penumbra/Mods/Groups/ImcModGroup.cs | 43 ++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 33 ++- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/ModCreator.cs | 1 + Penumbra/Mods/SubMods/ImcSubMod.cs | 7 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 80 +----- Penumbra/UI/Classes/Combos.cs | 40 ++- Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 238 +++++++++++++++++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 42 +--- 15 files changed, 360 insertions(+), 146 deletions(-) diff --git a/OtterGui b/OtterGui index 866389b3..5028fba7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 866389b3988d9c4926a786f6c78ac9d5265591ac +Subproject commit 5028fba767ca8febd75a1a5ebc312bd354efc81b diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index bc928360..843fe195 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -36,7 +36,7 @@ public readonly struct ImcCache : IDisposable public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) { - if (!manip.Validate()) + if (!manip.Validate(true)) return false; var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); @@ -77,7 +77,7 @@ public readonly struct ImcCache : IDisposable public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) { - if (!m.Validate()) + if (!m.Validate(false)) return false; var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 64eff8ba..325c9143 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -120,7 +120,7 @@ public partial class TexToolsMeta { var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value); - if (imc.Validate()) + if (imc.Validate(true)) MetaManipulations.Add(imc); } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index a1c4b5bf..45295990 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -146,7 +146,7 @@ public readonly struct ImcManipulation : IMetaManipulation public bool Apply(ImcFile file) => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); - public bool Validate() + public bool Validate(bool withMaterial) { switch (ObjectType) { @@ -178,7 +178,7 @@ public readonly struct ImcManipulation : IMetaManipulation break; } - if (Entry.MaterialId == 0) + if (withMaterial && Entry.MaterialId == 0) return false; return true; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index e057d1a4..ed184d52 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -98,7 +98,7 @@ public readonly struct MetaManipulation : IEquatable, ICompara return; case ImcManipulation m: Imc = m; - ManipulationType = m.Validate() ? Type.Imc : Type.Unknown; + ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; return; } } @@ -108,7 +108,7 @@ public readonly struct MetaManipulation : IEquatable, ICompara { return ManipulationType switch { - Type.Imc => Imc.Validate(), + Type.Imc => Imc.Validate(true), Type.Eqdp => Eqdp.Validate(), Type.Eqp => Eqp.Validate(), Type.Est => Est.Validate(), diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e58d855a..21d8abe0 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,4 +1,7 @@ +using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -157,4 +160,44 @@ public class ImcModGroup(Mod mod) : IModGroup public (int Redirections, int Swaps, int Manips) GetCounts() => (0, 0, 1); + + public static ImcModGroup? Load(Mod mod, JObject json) + { + var options = json["Options"]; + var ret = new ImcModGroup(mod) + { + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, + ObjectType = json[nameof(ObjectType)]?.ToObject() ?? ObjectType.Unknown, + BodySlot = json[nameof(BodySlot)]?.ToObject() ?? BodySlot.Unknown, + EquipSlot = json[nameof(EquipSlot)]?.ToObject() ?? EquipSlot.Unknown, + PrimaryId = new PrimaryId(json[nameof(PrimaryId)]?.ToObject() ?? 0), + SecondaryId = new SecondaryId(json[nameof(SecondaryId)]?.ToObject() ?? 0), + Variant = new Variant(json[nameof(Variant)]?.ToObject() ?? 0), + CanBeDisabled = json[nameof(CanBeDisabled)]?.ToObject() ?? false, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + }; + if (ret.Name.Length == 0) + return null; + + if (options != null) + foreach (var child in options.Children()) + { + var subMod = new ImcSubMod(ret, child); + ret.OptionData.Add(subMod); + } + + if (!new ImcManipulation(ret.ObjectType, ret.BodySlot, ret.PrimaryId, ret.SecondaryId.Id, ret.Variant.Id, ret.EquipSlot, + ret.DefaultEntry).Validate(true)) + { + Penumbra.Messager.NotificationMessage($"Could not add IMC group because the associated IMC Entry is invalid.", + NotificationType.Warning); + return null; + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + return ret; + } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 1194f961..f71547ba 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -11,13 +12,43 @@ namespace Penumbra.Mods.Manager.OptionEditor; public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { + /// Add a new, empty imc group with the given manipulation data. + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcManipulation manip, SaveType saveType = SaveType.ImmediateSync) + { + if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) + return null; + + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + var group = CreateGroup(mod, newName, manip, maxPriority); + mod.Groups.Add(group); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); + return group; + } + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; + + private static ImcModGroup CreateGroup(Mod mod, string newName, ImcManipulation manip, ModPriority priority, + SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + ObjectType = manip.ObjectType, + EquipSlot = manip.EquipSlot, + BodySlot = manip.BodySlot, + PrimaryId = manip.PrimaryId, + SecondaryId = manip.SecondaryId.Id, + Variant = manip.Variant, + DefaultEntry = manip.Entry, + }; + protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) => null; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index b2b48ac0..3c00dcc1 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -246,7 +246,7 @@ public class ModGroupEditor( { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), _ => null, }; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 47261c6d..ed4245c4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -434,6 +434,7 @@ public partial class ModCreator( { case GroupType.Multi: return MultiModGroup.Load(mod, json); case GroupType.Single: return SingleModGroup.Load(mod, json); + case GroupType.Imc: return ImcModGroup.Load(mod, json); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index 167c8a6c..fca817aa 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; namespace Penumbra.Mods.SubMods; @@ -6,6 +7,12 @@ public class ImcSubMod(ImcModGroup group) : IModOption { public readonly ImcModGroup Group = group; + public ImcSubMod(ImcModGroup group, JToken json) + : this(group) + { + SubMod.LoadOptionData(json, this); + } + public Mod Mod => Group.Mod; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 4db0df47..6010cdaf 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -639,11 +639,11 @@ public class ItemSwapTab : IDisposable, ITab ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - _dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender); + _dirty |= Combos.Gender("##Gender", _currentGender, out _currentGender, InputWidth); if (drawRace == 1) { ImGui.SameLine(); - _dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace); + _dirty |= Combos.Race("##Race", _currentRace, out _currentRace, InputWidth); } else if (drawRace == 2) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 743310ea..55125375 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -10,6 +10,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; @@ -145,7 +146,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(ModelSetIdTooltip); ImGui.TableNextColumn(); - if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot)) + if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot)) _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -351,90 +352,31 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (Combos.ImcType("##imcType", _new.ObjectType, out var type)) - { - var equipSlot = type switch - { - ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - _new = new ImcManipulation(type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? (ushort)1 : _new.SecondaryId, - _new.Variant.Id, equipSlot, _new.Entry); - } - - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + var change = MetaManipulationDrawer.DrawObjectType(ref _new); ImGui.TableNextColumn(); - if (IdInput("##imcId", IdWidth, _new.PrimaryId.Id, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant.Id, _new.EquipSlot, _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(PrimaryIdTooltip); - + change |= MetaManipulationDrawer.DrawPrimaryId(ref _new); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if (_new.ObjectType is ObjectType.Equipment) - { - if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else if (_new.ObjectType is ObjectType.Accessory) - { - if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } + if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= MetaManipulationDrawer.DrawSlot(ref _new); else - { - if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId.Id, out var setId2, 0, ushort.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant.Id, _new.EquipSlot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } + change |= MetaManipulationDrawer.DrawSecondaryId(ref _new); ImGui.TableNextColumn(); - if (IdInput("##imcVariant", SmallIdWidth, _new.Variant.Id, out var variant, 0, byte.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, - _new.Entry).Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(VariantIdTooltip); + change |= MetaManipulationDrawer.DrawVariant(ref _new); ImGui.TableNextColumn(); if (_new.ObjectType is ObjectType.DemiHuman) - { - if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } + change |= MetaManipulationDrawer.DrawSlot(ref _new, 70); else - { ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); - } - + if (change) + _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); // Values using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 2cba7cf5..253bf0e0 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -8,41 +8,35 @@ namespace Penumbra.UI.Classes; public static class Combos { // Different combos to use with enums. - public static bool Race(string label, ModelRace current, out ModelRace race) - => Race(label, 100, current, out race); - - public static bool Race(string label, float unscaledWidth, ModelRace current, out ModelRace race) + public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1); - public static bool Gender(string label, Gender current, out Gender gender) - => Gender(label, 120, current, out gender); + public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, RaceEnumExtensions.ToName, 1); - public static bool Gender(string label, float unscaledWidth, Gender current, out Gender gender) - => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1); - - public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, + public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName); - public static bool EqpEquipSlot(string label, float width, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, + public static bool EqpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName); - public static bool AccessorySlot(string label, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, + public static bool AccessorySlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName); - public static bool SubRace(string label, SubRace current, out SubRace subRace) - => ImGuiUtil.GenericEnumCombo(label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1); + public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1); - public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute) - => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute, + public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1); - public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute) - => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute); + public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute, float unscaledWidth = 200) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute); - public static bool ImcType(string label, ObjectType current, out ObjectType type) - => ImGuiUtil.GenericEnumCombo(label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, + public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, ObjectTypeExtensions.ToName); } diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index 5652fa98..b7262f95 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,12 +1,19 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; using ImGuiNET; +using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -15,9 +22,236 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; +using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.UI.ModsTab; +public static class MetaManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + // A number input for ids with a optional max id of given width. + // Returns true if newId changed against currentId. + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } +} + +public class AddGroupDrawer : IUiService +{ + private string _groupName = string.Empty; + private bool _groupNameValid = false; + + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly MetaFileManager _metaManager; + private readonly ModManager _modManager; + + public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + { + _metaManager = metaManager; + _modManager = modManager; + UpdateEntry(); + } + + public void Draw(Mod mod, float width) + { + DrawBasicGroups(mod, width); + DrawImcData(mod, width); + } + + private void UpdateEntry() + { + try + { + _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, + out _entryExists); + _imcFileExists = true; + } + catch (Exception) + { + _defaultEntry = new ImcEntry(); + _imcFileExists = false; + _entryExists = false; + } + + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _entryInvalid = !_imcManip.Validate(true); + } + + + private void DrawBasicGroups(Mod mod, float width) + { + ImGui.SetNextItemWidth(width); + if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); + + var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + if (ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + buttonWidth, !_groupNameValid)) + { + _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + ImUtf8.SameLineInner(); + if (ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + buttonWidth, !_groupNameValid)) + { + _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + } + + private void DrawImcData(Mod mod, float width) + { + var halfWidth = (width - ImUtf8.ItemInnerSpacing.X) / 2 / ImUtf8.GlobalScale; + var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, halfWidth); + if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + { + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); + } + else if (_imcManip.ObjectType is ObjectType.DemiHuman) + { + var quarterWidth = (halfWidth - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + } + else + { + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); + } + + if (change) + UpdateEntry(); + + var buttonWidth = new Vector2(halfWidth * ImUtf8.GlobalScale, 0); + + if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid ? + "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, + buttonWidth, !_groupNameValid || _entryInvalid)) + { + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); + _groupName = string.Empty; + _groupNameValid = false; + } + + if (_entryInvalid) + { + ImUtf8.SameLineInner(); + var text = _imcFileExists + ? "IMC Entry Does Not Exist" + : "IMC File Does Not Exist"; + ImGuiUtil.DrawTextButton(text, buttonWidth, Colors.PressEnterWarningBg); + } + } +} + public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, @@ -267,9 +501,7 @@ public sealed class ModGroupEditDrawer( } private void DrawImcGroup(ImcModGroup group) - { - // TODO - } + { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index a5db15b6..b7951c49 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -28,7 +28,8 @@ public class ModPanelEditTab( Configuration config, PredefinedTagManager predefinedTagManager, ModGroupEditDrawer groupEditDrawer, - DescriptionEditPopup descriptionPopup) + DescriptionEditPopup descriptionPopup, + AddGroupDrawer addGroupDrawer) : ITab { private readonly TagButtons _modTags = new(); @@ -75,7 +76,7 @@ public class ModPanelEditTab( selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(filenames, modManager, _mod, config.ReplaceNonAsciiOnImport); + addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); groupEditDrawer.Draw(_mod); @@ -84,7 +85,6 @@ public class ModPanelEditTab( public void Reset() { - AddOptionGroup.Reset(); MoveDirectory.Reset(); Input.Reset(); } @@ -202,42 +202,6 @@ public class ModPanelEditTab( Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } - /// Text input to add a new option group at the end of the current groups. - private static class AddOptionGroup - { - private static string _newGroupName = string.Empty; - - public static void Reset() - => _newGroupName = string.Empty; - - public static void Draw(FilenameService filenames, ModManager modManager, Mod mod, bool onlyAscii) - { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); - ImGui.SameLine(); - var defaultFile = filenames.OptionGroupFile(mod, -1, onlyAscii); - var fileExists = File.Exists(defaultFile); - var tt = fileExists - ? "Open the default option json file in the text editor of your choice." - : "The default option json file does not exist."; - if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", UiHelpers.IconButtonSize, tt, - !fileExists, true)) - Process.Start(new ProcessStartInfo(defaultFile) { UseShellExecute = true }); - - ImGui.SameLine(); - - var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false); - tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, - tt, !nameValid, true)) - return; - - modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName); - Reset(); - } - } - /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { From bb56faa288be5bf200e0e44af93a649b382ef632 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 May 2024 18:28:40 +0200 Subject: [PATCH 1673/2451] Improvements. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 27 +- Penumbra/Mods/Manager/ModCacheManager.cs | 3 + .../Manager/OptionEditor/ImcAttributeCache.cs | 123 ++++++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 63 ++++ Penumbra/Mods/SubMods/ImcSubMod.cs | 7 +- Penumbra/UI/ModsTab/AddGroupDrawer.cs | 161 ++++++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 280 +++++++++--------- 9 files changed, 498 insertions(+), 170 deletions(-) create mode 100644 Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs create mode 100644 Penumbra/UI/ModsTab/AddGroupDrawer.cs diff --git a/OtterGui b/OtterGui index 5028fba7..462acb87 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5028fba767ca8febd75a1a5ebc312bd354efc81b +Subproject commit 462acb87099650019996e4306d18cc70f76ca576 diff --git a/Penumbra.GameData b/Penumbra.GameData index 595ac572..5fa4d0e7 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 +Subproject commit 5fa4d0e7972423b73f8cf569bb2bfbeddd825c8a diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 21d8abe0..671d684f 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -14,8 +14,7 @@ namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 30; - public const int NumAttributes = 10; + public const int DisabledIndex = 60; public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; @@ -55,23 +54,20 @@ public class ImcModGroup(Mod mod) : IModGroup } } + public bool DefaultDisabled + => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + public IModOption? AddOption(string name, string description = "") { - uint fullMask = GetFullMask(); - var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); - // All attributes handled. - if (firstUnset >= NumAttributes) - return null; - var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) return null; var subMod = new ImcSubMod(this) { - Name = name, - Description = description, - AttributeIndex = firstUnset, + Name = name, + Description = description, + AttributeMask = 0, }; OptionData.Add(subMod); return subMod; @@ -100,7 +96,7 @@ public class ImcModGroup(Mod mod) : IModGroup continue; var option = OptionData[i]; - mask |= option.Attribute; + mask |= option.AttributeMask; } return mask; @@ -109,11 +105,10 @@ public class ImcModGroup(Mod mod) : IModGroup private ushort GetFullMask() => GetCurrentMask(Setting.AllBits(63)); - private ImcManipulation GetManip(ushort mask) + public ImcManipulation GetManip(ushort mask) => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, DefaultEntry with { AttributeMask = mask }); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { if (CanBeDisabled && setting.HasFlag(DisabledIndex)) @@ -150,8 +145,8 @@ public class ImcModGroup(Mod mod) : IModGroup { jWriter.WriteStartObject(); SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeIndex)); - jWriter.WriteValue(option.AttributeIndex); + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); jWriter.WriteEndObject(); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 59c88cf0..c6a723a0 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -202,6 +202,9 @@ public class ModCacheManager : IDisposable foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); + foreach(var imcGroup in mod.Groups.OfType()) + ComputeChangedItems(_identifier, changedItems, imcGroup.GetManip(0)); + mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs new file mode 100644 index 00000000..e1235c5b --- /dev/null +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -0,0 +1,123 @@ +using OtterGui; +using Penumbra.GameData.Structs; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Mods.Manager.OptionEditor; + +public unsafe ref struct ImcAttributeCache +{ + private fixed bool _canChange[ImcEntry.NumAttributes]; + private fixed byte _option[ImcEntry.NumAttributes]; + + /// Obtain the earliest unset flag, or 0 if none are unset. + public readonly ushort LowestUnsetMask; + + public ImcAttributeCache(ImcModGroup group) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + _canChange[i] = true; + _option[i] = byte.MaxValue; + + var flag = (ushort)(1 << i); + var set = (group.DefaultEntry.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = true; + _option[i] = byte.MaxValue - 1; + continue; + } + + foreach (var (option, idx) in group.OptionData.WithIndex()) + { + set = (option.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = option.AttributeMask != flag; + _option[i] = (byte)idx; + break; + } + } + + if (_option[i] == byte.MaxValue && LowestUnsetMask is 0) + LowestUnsetMask = flag; + } + } + + + /// Checks whether an attribute flag can be set by anything, i.e. if it might be the only flag for an option and thus could not be removed from that option. + public readonly bool CanChange(int idx) + => _canChange[idx]; + + /// Set a default attribute flag to a value if possible, remove it from its prior option if necessary, and return if anything changed. + public readonly bool Set(ImcModGroup group, int idx, bool value) + { + var flag = 1 << idx; + var oldMask = group.DefaultEntry.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = newMask }; + return true; + } + + if (!_canChange[idx]) + return false; + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = mask }; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var option = group.OptionData[_option[idx]]; + option.AttributeMask = (ushort)(option.AttributeMask & ~flag); + } + + return true; + } + + /// Set an attribute flag to a value if possible, remove it from its prior option or the default entry if necessary, and return if anything changed. + public readonly bool Set(ImcSubMod option, int idx, bool value) + { + if (!_canChange[idx]) + return false; + + var flag = 1 << idx; + var oldMask = option.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + option.AttributeMask = newMask; + return true; + } + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + option.AttributeMask = mask; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var oldOption = option.Group.OptionData[_option[idx]]; + oldOption.AttributeMask = (ushort)(oldOption.AttributeMask & ~flag); + } + else if (_option[idx] is byte.MaxValue - 1) + { + option.Group.DefaultEntry = option.Group.DefaultEntry with + { + AttributeMask = (ushort)(option.Group.DefaultEntry.AttributeMask & ~flag), + }; + } + + return true; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f71547ba..20021d29 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -1,11 +1,13 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -26,6 +28,67 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ return group; } + public ImcSubMod? AddOption(ImcModGroup group, in ImcAttributeCache cache, string name, string description = "", + SaveType saveType = SaveType.Queue) + { + if (cache.LowestUnsetMask == 0) + return null; + + var subMod = new ImcSubMod(group) + { + Name = name, + Description = description, + AttributeMask = cache.LowestUnsetMask, + }; + group.OptionData.Add(subMod); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, subMod, null, -1); + return subMod; + } + + // Hide this method. + private new ImcSubMod? AddOption(ImcModGroup group, string name, SaveType saveType) + => null; + + public void ChangeDefaultAttribute(ImcModGroup group, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(group, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeDefaultEntry(ImcModGroup group, in ImcEntry newEntry, SaveType saveType = SaveType.Queue) + { + var entry = newEntry with { AttributeMask = group.DefaultEntry.AttributeMask }; + if (entry.MaterialId == 0 || group.DefaultEntry.Equals(entry)) + return; + + group.DefaultEntry = entry; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeOptionAttribute(ImcSubMod option, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(option, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); + } + + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) + { + if (group.CanBeDisabled == canBeDisabled) + return; + + group.CanBeDisabled = canBeDisabled; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index fca817aa..7f46bc95 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; namespace Penumbra.Mods.SubMods; @@ -11,15 +12,13 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); } public Mod Mod => Group.Mod; - public byte AttributeIndex; - - public ushort Attribute - => (ushort)(1 << AttributeIndex); + public ushort AttributeMask; Mod IModOption.Mod => Mod; diff --git a/Penumbra/UI/ModsTab/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/AddGroupDrawer.cs new file mode 100644 index 00000000..79c1bf9d --- /dev/null +++ b/Penumbra/UI/ModsTab/AddGroupDrawer.cs @@ -0,0 +1,161 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public class AddGroupDrawer : IUiService +{ + private string _groupName = string.Empty; + private bool _groupNameValid = false; + + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly MetaFileManager _metaManager; + private readonly ModManager _modManager; + + public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + { + _metaManager = metaManager; + _modManager = modManager; + UpdateEntry(); + } + + public void Draw(Mod mod, float width) + { + var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + DrawBasicGroups(mod, width, buttonWidth); + DrawImcData(mod, buttonWidth); + } + + private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth) + { + ImGui.SetNextItemWidth(width); + if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); + + DrawSingleGroupButton(mod, buttonWidth); + ImUtf8.SameLineInner(); + DrawMultiGroupButton(mod, buttonWidth); + } + + private void DrawSingleGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawMultiGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawImcInput(float width) + { + var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, width); + if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + { + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + else if (_imcManip.ObjectType is ObjectType.DemiHuman) + { + var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + } + else + { + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + + if (change) + UpdateEntry(); + } + + private void DrawImcData(Mod mod, Vector2 width) + { + var halfWidth = width.X / ImUtf8.GlobalScale; + DrawImcInput(halfWidth); + DrawImcButton(mod, width); + } + + private void DrawImcButton(Mod mod, Vector2 width) + { + if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid + ? "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, + width, !_groupNameValid || _entryInvalid)) + { + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); + _groupName = string.Empty; + _groupNameValid = false; + } + + if (_entryInvalid) + { + ImUtf8.SameLineInner(); + var text = _imcFileExists + ? "IMC Entry Does Not Exist"u8 + : "IMC File Does Not Exist"u8; + ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width); + } + } + + private void UpdateEntry() + { + try + { + _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, + out _entryExists); + _imcFileExists = true; + } + catch (Exception) + { + _defaultEntry = new ImcEntry(); + _imcFileExists = false; + _entryExists = false; + } + + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _entryInvalid = !_imcManip.Validate(true); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index b7262f95..a94c25ea 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,15 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; -using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; -using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -22,7 +19,6 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; -using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.UI.ModsTab; @@ -104,8 +100,10 @@ public static class MetaManipulationDrawer return ret; } - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, bool border) { @@ -121,142 +119,12 @@ public static class MetaManipulationDrawer } } -public class AddGroupDrawer : IUiService -{ - private string _groupName = string.Empty; - private bool _groupNameValid = false; - - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; - - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) - { - _metaManager = metaManager; - _modManager = modManager; - UpdateEntry(); - } - - public void Draw(Mod mod, float width) - { - DrawBasicGroups(mod, width); - DrawImcData(mod, width); - } - - private void UpdateEntry() - { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); - } - - - private void DrawBasicGroups(Mod mod, float width) - { - ImGui.SetNextItemWidth(width); - if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) - _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); - - var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); - if (ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid - ? "Add a new single selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - - ImUtf8.SameLineInner(); - if (ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid - ? "Add a new multi selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - } - - private void DrawImcData(Mod mod, float width) - { - var halfWidth = (width - ImUtf8.ItemInnerSpacing.X) / 2 / ImUtf8.GlobalScale; - var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, halfWidth); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) - { - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) - { - var quarterWidth = (halfWidth - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); - } - else - { - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - - if (change) - UpdateEntry(); - - var buttonWidth = new Vector2(halfWidth * ImUtf8.GlobalScale, 0); - - if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid - ? "Can not add a new group of this name."u8 - : _entryInvalid ? - "The associated IMC entry is invalid."u8 - : "Add a new multi selection option group to this mod."u8, - buttonWidth, !_groupNameValid || _entryInvalid)) - { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; - _groupNameValid = false; - } - - if (_entryInvalid) - { - ImUtf8.SameLineInner(); - var text = _imcFileExists - ? "IMC Entry Does Not Exist" - : "IMC File Does Not Exist"; - ImGuiUtil.DrawTextButton(text, buttonWidth, Colors.PressEnterWarningBg); - } - } -} - public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + MetaFileManager metaManager) : IUiService { private static ReadOnlySpan DragDropLabel => "##DragOption"u8; @@ -501,7 +369,111 @@ public sealed class ModGroupEditDrawer( } private void DrawImcGroup(ImcModGroup group) - { } + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Object Type"u8); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text("Slot"u8); + ImUtf8.Text("Primary ID"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text("Secondary ID"); + ImUtf8.Text("Variant"u8); + + ImUtf8.TextFrameAligned("Material ID"u8); + ImUtf8.TextFrameAligned("Material Animation ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Sound ID"u8); + ImUtf8.TextFrameAligned("Can Be Disabled"u8); + ImUtf8.TextFrameAligned("Default Attributes"u8); + } + + ImGui.SameLine(); + + var attributeCache = new ImcAttributeCache(group); + + using (ImUtf8.Group()) + { + ImUtf8.Text(group.ObjectType.ToName()); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text(group.EquipSlot.ToName()); + ImUtf8.Text($"{group.PrimaryId.Id}"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text($"{group.SecondaryId.Id}"); + ImUtf8.Text($"{group.Variant.Id}"); + + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); + + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + modManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); + + var defaultDisabled = group.DefaultDisabled; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, + group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); + + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + } + + + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + _optionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + + DrawNewOption(group, attributeCache); + return; + + static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip($"{(char)('A' + i)}"); + if (i != 9) + ImUtf8.SameLineInner(); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) @@ -575,14 +547,14 @@ public sealed class ModGroupEditDrawer( if (count >= int.MaxValue) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.SingleEditor.AddOption(group, name); _newOptionName = null; } } @@ -593,25 +565,36 @@ public sealed class ModGroupEditDrawer( if (count >= IModGroup.MaxMultiOptions) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.MultiEditor.AddOption(group, name); _newOptionName = null; } } - private void DrawNewOption(ImcModGroup group) + private void DrawNewOption(ImcModGroup group, in ImcAttributeCache cache) { - // TODO + if (cache.LowestUnsetMask == 0) + return; + + var name = DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + _newOptionName = null; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawNewOptionBase(IModGroup group, int count) + private string DrawNewOptionBase(IModGroup group, int count) { ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); Target(group, count); @@ -631,6 +614,7 @@ public sealed class ModGroupEditDrawer( } ImUtf8.SameLineInner(); + return newName; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From e85b84dafe543b3a2e8bcfe13ed665fbe92b4c93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 May 2024 18:24:21 +0200 Subject: [PATCH 1674/2451] Add the option to omit mch offhands from changed items. --- Penumbra/Collections/Cache/CollectionCache.cs | 10 +- .../Cache/CollectionCacheManager.cs | 8 +- Penumbra/Configuration.cs | 25 ++-- Penumbra/Mods/Groups/IModGroup.cs | 2 + Penumbra/Mods/Groups/ImcModGroup.cs | 5 + Penumbra/Mods/Groups/MultiModGroup.cs | 8 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 8 ++ Penumbra/Mods/Manager/ModCacheManager.cs | 92 ++------------ Penumbra/Mods/Manager/ModImportManager.cs | 19 +-- Penumbra/Mods/Mod.cs | 6 +- Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 8 ++ Penumbra/Util/IdentifierExtensions.cs | 115 ++++++++++++++++++ 13 files changed, 192 insertions(+), 117 deletions(-) create mode 100644 Penumbra/Util/IdentifierExtensions.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index ded1dc73..e2f20b46 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; +using Penumbra.Util; namespace Penumbra.Collections.Cache; @@ -252,8 +253,8 @@ public sealed class CollectionCache : IDisposable return mod.GetData(); var settings = _collection[mod.Index].Settings; - return settings is not { Enabled: true } - ? AppliedModData.Empty + return settings is not { Enabled: true } + ? AppliedModData.Empty : mod.GetData(settings); } @@ -439,9 +440,12 @@ public sealed class CollectionCache : IDisposable foreach (var (manip, mod) in Meta) { - ModCacheManager.ComputeChangedItems(identifier, items, manip); + identifier.MetaChangedItems(items, manip); AddItems(mod); } + + if (_manager.Config.HideMachinistOffhandFromChangedItems) + _changedItems.RemoveMachinistOffhands(); } catch (Exception e) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index fb9ee9a3..ca57c8b9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -25,6 +25,7 @@ public class CollectionCacheManager : IDisposable private readonly ModStorage _modStorage; private readonly CollectionStorage _storage; private readonly ActiveCollections _active; + internal readonly Configuration Config; internal readonly ResolvedFileChanged ResolvedFileChanged; internal readonly MetaFileManager MetaFileManager; internal readonly ResourceLoader ResourceLoader; @@ -40,7 +41,8 @@ public class CollectionCacheManager : IDisposable => _storage.Where(c => c.HasCache); public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, - MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader) + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader, + Configuration config) { _framework = framework; _communicator = communicator; @@ -50,6 +52,7 @@ public class CollectionCacheManager : IDisposable _active = active; _storage = storage; ResourceLoader = resourceLoader; + Config = config; ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) @@ -260,7 +263,8 @@ public class CollectionCacheManager : IDisposable } /// Prepare Changes by removing mods from caches with collections or add or reload mods. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int movedToIdx) { if (type is ModOptionChangeType.PrepareChange) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a065bc26..b81e84d8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -41,18 +41,19 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 2ec60f7e..ab367532 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -40,6 +41,7 @@ public interface IModGroup public int GetIndex(); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 671d684f..173bf57e 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -3,12 +3,14 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -119,6 +121,9 @@ public class ImcModGroup(Mod mod) : IModGroup manipulations.Add(imc); } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.MetaChangedItems(changedItems, GetManip(0)); + public Setting FixSetting(Setting setting) => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7495c4b4..f587fc8f 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -4,10 +4,12 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -114,6 +116,12 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { ModSaveGroup.WriteJsonBase(jWriter, this); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index bc463c1e..7a551322 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -2,10 +2,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -99,6 +101,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index c6a723a0..8ab8cf33 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,26 +1,27 @@ using Penumbra.Communication; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ObjectIdentification _identifier; private readonly ModStorage _modManager; private bool _updatingItems = false; - public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage) + public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config) { _communicator = communicator; _identifier = identifier; _modManager = modStorage; + _config = config; _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager); @@ -38,75 +39,8 @@ public class ModCacheManager : IDisposable _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); } - /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void ComputeChangedItems(ObjectIdentification identifier, IDictionary changedItems, MetaManipulation manip) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - switch (manip.Imc.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - identifier.Identify(changedItems, - GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Weapon: - identifier.Identify(changedItems, - GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - case ObjectType.DemiHuman: - identifier.Identify(changedItems, - GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Monster: - identifier.Identify(changedItems, - GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); - break; - case MetaManipulation.Type.Eqp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); - break; - case MetaManipulation.Type.Est: - switch (manip.Est.Slot) - { - case EstManipulation.EstType.Hair: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); - break; - case EstManipulation.EstType.Face: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); - break; - case EstManipulation.EstType.Body: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Body)); - break; - case EstManipulation.EstType.Head: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Head)); - break; - } - - break; - case MetaManipulation.Type.Gmp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); - break; - } - } - - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int fromIdx) { switch (type) { @@ -194,16 +128,14 @@ public class ModCacheManager : IDisposable private void UpdateChangedItems(Mod mod) { - var changedItems = (SortedList)mod.ChangedItems; - changedItems.Clear(); - foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) - _identifier.Identify(changedItems, gamePath.ToString()); + mod.ChangedItems.Clear(); - foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) - ComputeChangedItems(_identifier, changedItems, manip); + _identifier.AddChangedItems(mod.Default, mod.ChangedItems); + foreach (var group in mod.Groups) + group.AddChangedItems(_identifier, mod.ChangedItems); - foreach(var imcGroup in mod.Groups.OfType()) - ComputeChangedItems(_identifier, changedItems, imcGroup.GetManip(0)); + if (_config.HideMachinistOffhandFromChangedItems) + mod.ChangedItems.RemoveMachinistOffhands(); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 73571ea4..c99b7d0e 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -5,12 +5,8 @@ using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; -public class ModImportManager : IDisposable +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable { - private readonly ModManager _modManager; - private readonly Configuration _config; - private readonly ModEditor _modEditor; - private readonly ConcurrentQueue _modsToUnpack = new(); /// Mods need to be added thread-safely outside of iteration. @@ -26,13 +22,6 @@ public class ModImportManager : IDisposable => _modsToAdd; - public ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) - { - _modManager = modManager; - _config = config; - _modEditor = modEditor; - } - public void TryUnpacking() { if (Importing || !_modsToUnpack.TryDequeue(out var newMods)) @@ -51,7 +40,7 @@ public class ModImportManager : IDisposable if (files.Length == 0) return; - _import = new TexToolsImporter(files.Length, files, AddNewMod, _config, _modEditor, _modManager, _modEditor.Compactor); + _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor); } public bool Importing @@ -87,8 +76,8 @@ public class ModImportManager : IDisposable return false; } - _modManager.AddMod(directory); - mod = _modManager.LastOrDefault(); + modManager.AddMod(directory); + mod = modManager.LastOrDefault(); return mod != null && mod.ModPath == directory; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 6f6eb8ce..783ef3e6 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -62,8 +62,8 @@ public sealed class Mod : IMod // Options - public readonly DefaultSubMod Default; - public readonly List Groups = []; + public readonly DefaultSubMod Default; + public readonly List Groups = []; public AppliedModData GetData(ModSettings? settings = null) { @@ -99,7 +99,7 @@ public sealed class Mod : IMod } // Cache - public readonly IReadOnlyDictionary ChangedItems = new SortedList(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index a94c25ea..4ef1577f 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -123,8 +123,7 @@ public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup, - MetaFileManager metaManager) : IUiService + DescriptionEditPopup descriptionPopup) : IUiService { private static ReadOnlySpan DragDropLabel => "##DragOption"u8; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 439f7be4..30384538 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -428,6 +428,14 @@ public class SettingsTab : ITab _config.Ephemeral.Save(); } }); + Checkbox("Omit Machinist Offhands in Changed Items", + "Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n" + + "Changing this triggers a rediscovery of your mods so all changed items can be updated.", + _config.HideMachinistOffhandFromChangedItems, v => + { + _config.HideMachinistOffhandFromChangedItems = v; + _modManager.DiscoverMods(); + }); Checkbox("Hide Priority Numbers in Mod Selector", "Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them.", _config.HidePrioritiesInSelector, v => _config.HidePrioritiesInSelector = v); diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs new file mode 100644 index 00000000..392a5aba --- /dev/null +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -0,0 +1,115 @@ +using OtterGui.Classes; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Util; + +public static class IdentifierExtensions +{ + /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. + public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary changedItems, + MetaManipulation manip) + { + switch (manip.ManipulationType) + { + case MetaManipulation.Type.Imc: + switch (manip.Imc.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + identifier.Identify(changedItems, + GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Weapon: + identifier.Identify(changedItems, + GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + case ObjectType.DemiHuman: + identifier.Identify(changedItems, + GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Monster: + identifier.Identify(changedItems, + GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + } + + break; + case MetaManipulation.Type.Eqdp: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); + break; + case MetaManipulation.Type.Eqp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); + break; + case MetaManipulation.Type.Est: + switch (manip.Est.Slot) + { + case EstManipulation.EstType.Hair: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Face: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Body: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Body)); + break; + case EstManipulation.EstType.Head: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Head)); + break; + } + + break; + case MetaManipulation.Type.Gmp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + break; + case MetaManipulation.Type.Rsp: + changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); + break; + } + } + + public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, + IDictionary changedItems) + { + foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) + identifier.Identify(changedItems, gamePath.ToString()); + + foreach (var manip in container.Manipulations) + MetaChangedItems(identifier, changedItems, manip); + } + + public static void RemoveMachinistOffhands(this SortedList changedItems) + { + for (var i = 0; i < changedItems.Count; i++) + { + { + var value = changedItems.Values[i]; + if (value is EquipItem { Type: FullEquipType.GunOff }) + changedItems.RemoveAt(i--); + } + } + } + + public static void RemoveMachinistOffhands(this SortedList, object?)> changedItems) + { + for (var i = 0; i < changedItems.Count; i++) + { + { + var value = changedItems.Values[i].Item2; + if (value is EquipItem { Type: FullEquipType.GunOff }) + changedItems.RemoveAt(i--); + } + } + } +} From 2585de8b21e25fd62cf2295d58ac2520e526752b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 May 2024 22:01:20 +0200 Subject: [PATCH 1675/2451] Cleanup group drawing somewhat. --- Penumbra/Mods/Editor/ModNormalizer.cs | 16 +- Penumbra/Mods/Groups/IModGroup.cs | 3 + Penumbra/Mods/Groups/ImcModGroup.cs | 5 + Penumbra/Mods/Groups/MultiModGroup.cs | 4 + Penumbra/Mods/Groups/SingleModGroup.cs | 4 + .../Manager/OptionEditor/ModGroupEditor.cs | 94 +-- .../UI/ModsTab/{ => Groups}/AddGroupDrawer.cs | 32 +- .../UI/ModsTab/Groups/IModGroupEditDrawer.cs | 6 + .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 138 ++++ .../UI/ModsTab/{ => Groups}/ModGroupDrawer.cs | 36 +- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 360 +++++++++ .../ModsTab/Groups/MultiModGroupEditDrawer.cs | 63 ++ .../Groups/SingleModGroupEditDrawer.cs | 68 ++ Penumbra/UI/ModsTab/MetaManipulationDrawer.cs | 105 +++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 687 ------------------ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 1 + 17 files changed, 841 insertions(+), 782 deletions(-) rename Penumbra/UI/ModsTab/{ => Groups}/AddGroupDrawer.cs (83%) create mode 100644 Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs rename Penumbra/UI/ModsTab/{ => Groups}/ModGroupDrawer.cs (85%) create mode 100644 Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/MetaManipulationDrawer.cs delete mode 100644 Penumbra/UI/ModsTab/ModGroupEditDrawer.cs diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 437600c9..58e4fc08 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -279,19 +278,8 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) private void ApplyRedirections() { foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) - { - switch (group) - { - case SingleModGroup single: - foreach (var (option, optionIdx) in single.OptionData.WithIndex()) - _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); - break; - case MultiModGroup multi: - foreach (var (option, optionIdx) in multi.OptionData.WithIndex()) - _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); - break; - } - } + foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) + _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); ++Step; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index ab367532..fcc8c093 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -5,6 +5,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.Mods.Groups; @@ -40,6 +41,8 @@ public interface IModGroup public int GetIndex(); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 173bf57e..d2c41f34 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -10,6 +10,8 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab; +using Penumbra.UI.ModsTab.Groups; using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -89,6 +91,9 @@ public class ImcModGroup(Mod mod) : IModGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new ImcModGroupEditDrawer(editDrawer, this); + private ushort GetCurrentMask(Setting setting) { var mask = DefaultEntry.AttributeMask; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index f587fc8f..7fc9acb3 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -9,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -107,6 +108,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new MultiModGroupEditDrawer(editDrawer, this); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 7a551322..4eec0746 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -7,6 +7,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -93,6 +94,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new SingleModGroupEditDrawer(editDrawer, this); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { if (OptionData.Count == 0) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 3c00dcc1..969ad3fa 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -37,8 +37,8 @@ public class ModGroupEditor( SingleModGroupEditor singleEditor, MultiModGroupEditor multiEditor, ImcModGroupEditor imcEditor, - CommunicatorService Communicator, - SaveService SaveService, + CommunicatorService communicator, + SaveService saveService, Configuration Config) : IService { public SingleModGroupEditor SingleEditor @@ -57,8 +57,8 @@ public class ModGroupEditor( return; group.DefaultSettings = defaultOption; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); } /// Rename an option group if possible. @@ -68,10 +68,10 @@ public class ModGroupEditor( if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) return; - SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); group.Name = newName; - SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); + saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); } /// Delete a given option group. Fires an event to prepare before actually deleting. @@ -79,22 +79,22 @@ public class ModGroupEditor( { var mod = group.Mod; var idx = group.GetIndex(); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); mod.Groups.RemoveAt(idx); - SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); } /// Move the index of a given option group. public void MoveModGroup(IModGroup group, int groupIdxTo) { - var mod = group.Mod; + var mod = group.Mod; var idxFrom = group.GetIndex(); if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; - SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); } /// Change the internal priority of the given option group. @@ -104,8 +104,8 @@ public class ModGroupEditor( return; group.Priority = newPriority; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); } /// Change the description of the given option group. @@ -115,8 +115,8 @@ public class ModGroupEditor( return; group.Description = newDescription; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); } /// Rename the given option. @@ -126,8 +126,8 @@ public class ModGroupEditor( return; option.Name = newName; - SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } /// Change the description of the given option. @@ -137,8 +137,8 @@ public class ModGroupEditor( return; option.Description = newDescription; - SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } /// Set the meta manipulations for a given option. Replaces existing manipulations. @@ -148,10 +148,10 @@ public class ModGroupEditor( && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Manipulations.SetTo(manipulations); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Set the file redirections for a given option. Replaces existing redirections. @@ -160,10 +160,10 @@ public class ModGroupEditor( if (subMod.Files.SetEquals(replacements)) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Files.SetTo(replacements); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. @@ -173,8 +173,8 @@ public class ModGroupEditor( subMod.Files.AddFrom(additions); if (oldCount != subMod.Files.Count) { - SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } } @@ -184,10 +184,10 @@ public class ModGroupEditor( if (subMod.FileSwaps.SetEquals(swaps)) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.FileSwaps.SetTo(swaps); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Verify that a new option group name is unique in this mod. @@ -227,45 +227,45 @@ public class ModGroupEditor( => group switch { SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), - _ => null, + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) diff --git a/Penumbra/UI/ModsTab/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs similarity index 83% rename from Penumbra/UI/ModsTab/AddGroupDrawer.cs rename to Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 79c1bf9d..06cb4154 100644 --- a/Penumbra/UI/ModsTab/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -12,25 +12,25 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModsTab; +namespace Penumbra.UI.ModsTab.Groups; public class AddGroupDrawer : IUiService { - private string _groupName = string.Empty; - private bool _groupNameValid = false; + private string _groupName = string.Empty; + private bool _groupNameValid = false; - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; + private readonly ModManager _modManager; public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) { _metaManager = metaManager; - _modManager = modManager; + _modManager = modManager; UpdateEntry(); } @@ -61,7 +61,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -74,7 +74,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -126,7 +126,7 @@ public class AddGroupDrawer : IUiService width, !_groupNameValid || _entryInvalid)) { _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -150,12 +150,12 @@ public class AddGroupDrawer : IUiService } catch (Exception) { - _defaultEntry = new ImcEntry(); + _defaultEntry = new ImcEntry(); _imcFileExists = false; - _entryExists = false; + _entryExists = false; } - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); _entryInvalid = !_imcManip.Validate(true); } } diff --git a/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs new file mode 100644 index 00000000..d7114147 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs @@ -0,0 +1,6 @@ +namespace Penumbra.UI.ModsTab.Groups; + +public interface IModGroupEditDrawer +{ + public void Draw(); +} diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs new file mode 100644 index 00000000..2418c5cb --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -0,0 +1,138 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Object Type"u8); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text("Slot"u8); + ImUtf8.Text("Primary ID"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text("Secondary ID"); + ImUtf8.Text("Variant"u8); + + ImUtf8.TextFrameAligned("Material ID"u8); + ImUtf8.TextFrameAligned("Material Animation ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Sound ID"u8); + ImUtf8.TextFrameAligned("Can Be Disabled"u8); + ImUtf8.TextFrameAligned("Default Attributes"u8); + } + + ImGui.SameLine(); + + var attributeCache = new ImcAttributeCache(group); + + using (ImUtf8.Group()) + { + ImUtf8.Text(group.ObjectType.ToName()); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text(group.EquipSlot.ToName()); + ImUtf8.Text($"{group.PrimaryId.Id}"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text($"{group.SecondaryId.Id}"); + ImUtf8.Text($"{group.Variant.Id}"); + + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); + + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); + + var defaultDisabled = group.DefaultDisabled; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) + editor.ModManager.OptionEditor.ChangeModGroupDefaultOption(group, + group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); + + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + } + + + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + editor.OptionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + + DrawNewOption(attributeCache); + return; + + static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & 1 << i) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip($"{(char)('A' + i)}"); + if (i != 9) + ImUtf8.SameLineInner(); + } + } + } + + private void DrawNewOption(in ImcAttributeCache cache) + { + if (cache.LowestUnsetMask == 0) + return; + + var name = editor.DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs similarity index 85% rename from Penumbra/UI/ModsTab/ModGroupDrawer.cs rename to Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index e9b0b396..dec77430 100644 --- a/Penumbra/UI/ModsTab/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -11,7 +11,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; -namespace Penumbra.UI.ModsTab; +namespace Penumbra.UI.ModsTab.Groups; public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService { @@ -63,8 +63,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); var options = group.Options; using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) @@ -97,10 +97,10 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -110,8 +110,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { for (var idx = 0; idx < group.Options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; + using var i = ImRaii.PushId(idx); + var option = options[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) SetModSetting(group, groupIdx, Setting.Single(idx)); @@ -130,9 +130,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImRaii.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -147,9 +147,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { for (var idx = 0; idx < options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var enabled = setting.HasFlag(idx); + using var i = ImRaii.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref enabled)) SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); @@ -187,8 +187,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {options.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options"; var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) @@ -204,7 +204,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs new file mode 100644 index 00000000..e7d70922 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -0,0 +1,360 @@ +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab.Groups; + +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup) : IUiService +{ + private static ReadOnlySpan DragDropLabel + => "##DragOption"u8; + + internal readonly ModManager ModManager = modManager; + internal readonly Queue ActionQueue = new(); + + internal Vector2 OptionIdxSelectable; + internal Vector2 AvailableWidth; + internal float PriorityWidth; + + internal string? NewOptionName; + private IModGroup? _newOptionGroup; + + private Vector2 _buttonSize; + private float _groupNameWidth; + private float _optionNameWidth; + private float _spacing; + private bool _deleteEnabled; + + private string? _currentGroupName; + private ModPriority? _currentGroupPriority; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid = true; + + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; + + public void Draw(Mod mod) + { + PrepareStyle(); + + using var id = ImUtf8.PushId("##GroupEdit"u8); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + DrawGroup(group, groupIdx); + + while (ActionQueue.TryDequeue(out var action)) + action.Invoke(); + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = ImUtf8.PushId(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group, idx); + group.EditDrawer(this).Draw(); + } + + private void DrawGroupNameRow(IModGroup group, int idx) + { + DrawGroupName(group); + ImUtf8.SameLineInner(); + DrawGroupMoveButtons(group, idx); + ImUtf8.SameLineInner(); + DrawGroupOpenFile(group, idx); + ImUtf8.SameLineInner(); + DrawGroupDescription(group); + ImUtf8.SameLineInner(); + DrawGroupDelete(group); + ImUtf8.SameLineInner(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; + ImGui.SetNextItemWidth(_groupNameWidth); + using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); + if (ImUtf8.InputText("##GroupName"u8, ref text)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupName != null && _isGroupNameValid) + ModManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); + } + + private void DrawGroupDelete(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option group."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawGroupPriority(IModGroup group) + { + var priority = _currentGroupEdited == group + ? (_currentGroupPriority ?? group.Priority).Value + : group.Priority.Value; + ImGui.SetNextItemWidth(PriorityWidth); + if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) + { + _currentGroupEdited = group; + _currentGroupPriority = new ModPriority(priority); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupPriority.HasValue) + ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); + _currentGroupEdited = null; + _currentGroupPriority = null; + } + + ImGuiUtil.HoverTooltip("Group Priority"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) + descriptionPopup.Open(group); + } + + private void DrawGroupMoveButtons(IModGroup group, int idx) + { + var isFirst = idx == 0; + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1)); + + if (isFirst) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + ImUtf8.HoverTooltip($"Move this group up to group {idx}."); + + + ImUtf8.SameLineInner(); + var isLast = idx == group.Mod.Groups.Count - 1; + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); + } + + private void DrawGroupOpenFile(IModGroup group, int idx) + { + var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); + var fileExists = File.Exists(fileName); + if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + + if (fileExists) + ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + ImGui.AlignTextToFramePadding(); + ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDescription(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPriority(MultiSubMod option) + { + var priority = option.Priority.Value; + ImGui.SetNextItemWidth(PriorityWidth); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); + ImUtf8.HoverTooltip("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionName(IModOption option) + { + var name = option.Name; + ImGui.SetNextItemWidth(_optionNameWidth); + if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) + ModManager.OptionEditor.RenameOption(option, name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDelete(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal string DrawNewOptionBase(IModGroup group, int count) + { + ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable); + Target(group, count); + + ImUtf8.SameLineInner(); + ImUtf8.IconDummy(); + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? NewOptionName ?? string.Empty + : string.Empty; + if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) + { + NewOptionName = newName; + _newOptionGroup = group; + } + + ImUtf8.SameLineInner(); + return newName; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + if (option.Group is not ITexToolsGroup) + return; + + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + } + + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (group is not ITexToolsGroup) + return; + + if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + return; + + if (_dragDropGroup != null && _dragDropOption != null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => + { + ModManager.OptionEditor.DeleteOption(sourceOption); + if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + ModManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * ImUtf8.GlobalScale; + _buttonSize = new Vector2(ImUtf8.FrameHeight); + PriorityWidth = 50 * ImUtf8.GlobalScale; + AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); + _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } +} diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs new file mode 100644 index 00000000..e6701a03 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -0,0 +1,63 @@ +using Dalamud.Interface; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionPriority(option); + } + + DrawNewOption(); + DrawConvertButton(); + } + + private void DrawConvertButton() + { + var g = group; + var e = editor.ModManager.OptionEditor.MultiEditor; + if (ImUtf8.Button("Convert to Single Group"u8, editor.AvailableWidth)) + editor.ActionQueue.Enqueue(() => e.ChangeToSingle(g)); + } + + private void DrawNewOption() + { + var count = group.Options.Count; + if (count >= IModGroup.MaxMultiOptions) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.MultiEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs new file mode 100644 index 00000000..75fbc63a --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -0,0 +1,68 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); + } + + DrawNewOption(); + DrawConvertButton(); + } + + private void DrawConvertButton() + { + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + var g = group; + var e = editor.ModManager.OptionEditor.SingleEditor; + if (ImUtf8.ButtonEx("Convert to Multi Group", editor.AvailableWidth, !convertible)) + editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g)); + if (!convertible) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawNewOption() + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs new file mode 100644 index 00000000..1f2273b5 --- /dev/null +++ b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs @@ -0,0 +1,105 @@ +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public static class MetaManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs deleted file mode 100644 index 4ef1577f..00000000 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ /dev/null @@ -1,687 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Services; -using OtterGui.Text; -using OtterGui.Text.EndObjects; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager; -using Penumbra.Mods.Manager.OptionEditor; -using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class MetaManipulationDrawer -{ - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) - { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (manip.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); - return ret; - } - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } -} - -public sealed class ModGroupEditDrawer( - ModManager modManager, - Configuration config, - FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService -{ - private static ReadOnlySpan DragDropLabel - => "##DragOption"u8; - - private Vector2 _buttonSize; - private Vector2 _availableWidth; - private float _priorityWidth; - private float _groupNameWidth; - private float _optionNameWidth; - private float _spacing; - private Vector2 _optionIdxSelectable; - private bool _deleteEnabled; - - private string? _currentGroupName; - private ModPriority? _currentGroupPriority; - private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid = true; - - private string? _newOptionName; - private IModGroup? _newOptionGroup; - private readonly Queue _actionQueue = new(); - - private IModGroup? _dragDropGroup; - private IModOption? _dragDropOption; - - public void Draw(Mod mod) - { - PrepareStyle(); - - using var id = ImUtf8.PushId("##GroupEdit"u8); - foreach (var (group, groupIdx) in mod.Groups.WithIndex()) - DrawGroup(group, groupIdx); - - while (_actionQueue.TryDequeue(out var action)) - action.Invoke(); - } - - private void DrawGroup(IModGroup group, int idx) - { - using var id = ImUtf8.PushId(idx); - using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group, idx); - switch (group) - { - case SingleModGroup s: - DrawSingleGroup(s); - break; - case MultiModGroup m: - DrawMultiGroup(m); - break; - case ImcModGroup i: - DrawImcGroup(i); - break; - } - } - - private void DrawGroupNameRow(IModGroup group, int idx) - { - DrawGroupName(group); - ImUtf8.SameLineInner(); - DrawGroupMoveButtons(group, idx); - ImUtf8.SameLineInner(); - DrawGroupOpenFile(group, idx); - ImUtf8.SameLineInner(); - DrawGroupDescription(group); - ImUtf8.SameLineInner(); - DrawGroupDelete(group); - ImUtf8.SameLineInner(); - DrawGroupPriority(group); - } - - private void DrawGroupName(IModGroup group) - { - var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; - ImGui.SetNextItemWidth(_groupNameWidth); - using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImUtf8.InputText("##GroupName"u8, ref text)) - { - _currentGroupEdited = group; - _currentGroupName = text; - _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupName != null && _isGroupNameValid) - modManager.OptionEditor.RenameModGroup(group, _currentGroupName); - _currentGroupName = null; - _currentGroupEdited = null; - _isGroupNameValid = true; - } - - var tt = _isGroupNameValid - ? "Change the Group name."u8 - : "Current name can not be used for this group."u8; - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); - } - - private void DrawGroupDelete(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteModGroup(group)); - - if (_deleteEnabled) - ImUtf8.HoverTooltip("Delete this option group."u8); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - private void DrawGroupPriority(IModGroup group) - { - var priority = _currentGroupEdited == group - ? (_currentGroupPriority ?? group.Priority).Value - : group.Priority.Value; - ImGui.SetNextItemWidth(_priorityWidth); - if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) - { - _currentGroupEdited = group; - _currentGroupPriority = new ModPriority(priority); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupPriority.HasValue) - modManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); - _currentGroupEdited = null; - _currentGroupPriority = null; - } - - ImGuiUtil.HoverTooltip("Group Priority"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawGroupDescription(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) - descriptionPopup.Open(group); - } - - private void DrawGroupMoveButtons(IModGroup group, int idx) - { - var isFirst = idx == 0; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx - 1)); - - if (isFirst) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); - else - ImUtf8.HoverTooltip($"Move this group up to group {idx}."); - - - ImUtf8.SameLineInner(); - var isLast = idx == group.Mod.Groups.Count - 1; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx + 1)); - - if (isLast) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); - else - ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); - } - - private void DrawGroupOpenFile(IModGroup group, int idx) - { - var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); - var fileExists = File.Exists(fileName); - if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) - try - { - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - } - catch (Exception e) - { - Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); - } - - if (fileExists) - ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); - } - - private void DrawSingleGroup(SingleModGroup group) - { - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(_priorityWidth, 0)); - } - - DrawNewOption(group); - var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; - if (ImUtf8.ButtonEx("Convert to Multi Group", _availableWidth, !convertible)) - _actionQueue.Enqueue(() => modManager.OptionEditor.SingleEditor.ChangeToMulti(group)); - if (!convertible) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - "Can not convert to multi group since maximum number of options is exceeded."u8); - } - - private void DrawMultiGroup(MultiModGroup group) - { - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - DrawOptionPriority(option); - } - - DrawNewOption(group); - if (ImUtf8.Button("Convert to Single Group"u8, _availableWidth)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MultiEditor.ChangeToSingle(group)); - } - - private void DrawImcGroup(ImcModGroup group) - { - using (ImUtf8.Group()) - { - ImUtf8.Text("Object Type"u8); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text("Slot"u8); - ImUtf8.Text("Primary ID"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text("Secondary ID"); - ImUtf8.Text("Variant"u8); - - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); - ImUtf8.TextFrameAligned("VFX ID"u8); - ImUtf8.TextFrameAligned("Sound ID"u8); - ImUtf8.TextFrameAligned("Can Be Disabled"u8); - ImUtf8.TextFrameAligned("Default Attributes"u8); - } - - ImGui.SameLine(); - - var attributeCache = new ImcAttributeCache(group); - - using (ImUtf8.Group()) - { - ImUtf8.Text(group.ObjectType.ToName()); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text(group.EquipSlot.ToName()); - ImUtf8.Text($"{group.PrimaryId.Id}"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text($"{group.SecondaryId.Id}"); - ImUtf8.Text($"{group.Variant.Id}"); - - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); - - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - modManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); - - var defaultDisabled = group.DefaultDisabled; - ImUtf8.SameLineInner(); - if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, - group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); - - DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); - } - - - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(_priorityWidth, 0)); - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + _optionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); - DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); - } - - DrawNewOption(group, attributeCache); - return; - - static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var value = (mask & (1 << i)) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) - { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - ImUtf8.HoverTooltip($"{(char)('A' + i)}"); - if (i != 9) - ImUtf8.SameLineInner(); - } - } - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) - { - ImGui.AlignTextToFramePadding(); - ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: _optionIdxSelectable); - Target(group, optionIdx); - Source(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; - if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDescription(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) - descriptionPopup.Open(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionPriority(MultiSubMod option) - { - var priority = option.Priority.Value; - ImGui.SetNextItemWidth(_priorityWidth); - if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) - modManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); - ImUtf8.HoverTooltip("Option priority inside the mod."u8); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionName(IModOption option) - { - var name = option.Name; - ImGui.SetNextItemWidth(_optionNameWidth); - if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) - modManager.OptionEditor.RenameOption(option, name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDelete(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteOption(option)); - - if (_deleteEnabled) - ImUtf8.HoverTooltip("Delete this option."u8); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - private void DrawNewOption(SingleModGroup group) - { - var count = group.Options.Count; - if (count >= int.MaxValue) - return; - - var name = DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.SingleEditor.AddOption(group, name); - _newOptionName = null; - } - } - - private void DrawNewOption(MultiModGroup group) - { - var count = group.Options.Count; - if (count >= IModGroup.MaxMultiOptions) - return; - - var name = DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.MultiEditor.AddOption(group, name); - _newOptionName = null; - } - } - - private void DrawNewOption(ImcModGroup group, in ImcAttributeCache cache) - { - if (cache.LowestUnsetMask == 0) - return; - - var name = DrawNewOptionBase(group, group.Options.Count); - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.ImcEditor.AddOption(group, cache, name); - _newOptionName = null; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string DrawNewOptionBase(IModGroup group, int count) - { - ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); - Target(group, count); - - ImUtf8.SameLineInner(); - ImUtf8.IconDummy(); - - ImUtf8.SameLineInner(); - ImGui.SetNextItemWidth(_optionNameWidth); - var newName = _newOptionGroup == group - ? _newOptionName ?? string.Empty - : string.Empty; - if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) - { - _newOptionName = newName; - _newOptionGroup = group; - } - - ImUtf8.SameLineInner(); - return newName; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Source(IModOption option) - { - if (option.Group is not ITexToolsGroup) - return; - - using var source = ImUtf8.DragDropSource(); - if (!source) - return; - - if (!DragDropSource.SetPayload(DragDropLabel)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - } - - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private void Target(IModGroup group, int optionIdx) - { - if (group is not ITexToolsGroup) - return; - - if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - _actionQueue.Enqueue(() => - { - modManager.OptionEditor.DeleteOption(sourceOption); - if (modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - modManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrepareStyle() - { - var totalWidth = 400f * ImUtf8.GlobalScale; - _buttonSize = new Vector2(ImUtf8.FrameHeight); - _priorityWidth = 50 * ImUtf8.GlobalScale; - _availableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + _priorityWidth, 0); - _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); - _spacing = ImGui.GetStyle().ItemInnerSpacing.X; - _optionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); - _optionNameWidth = totalWidth - _optionIdxSelectable.X - _buttonSize.X - 2 * _spacing; - _deleteEnabled = config.DeleteModModifier.IsActive(); - } -} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index b7951c49..125f539e 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -13,6 +13,7 @@ using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 107a2d04..7e3b8a95 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -9,6 +9,7 @@ using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; From c06d5b08715924aba67910f7e0fe718b4f83b274 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 16:57:16 +0200 Subject: [PATCH 1676/2451] Update for new gamedata. --- Penumbra.GameData | 2 +- Penumbra/Import/TexToolsMeta.Deserialization.cs | 4 +--- Penumbra/Meta/Files/CmpFile.cs | 6 +++--- Penumbra/Meta/Files/EqpGmpFile.cs | 6 +++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 5fa4d0e7..e8220a0a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 5fa4d0e7972423b73f8cf569bb2bfbeddd825c8a +Subproject commit e8220a0a74e9480330e98ed7ca462353434b9649 diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 325c9143..f062ae25 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -57,9 +57,7 @@ public partial class TexToolsMeta if (data == null) return; - using var reader = new BinaryReader(new MemoryStream(data)); - var value = (GmpEntry)reader.ReadUInt32(); - value.UnknownTotal = reader.ReadByte(); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); if (_keepDefault || value != def) MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId)); diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 8a6040ec..b265a5e8 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -19,8 +19,8 @@ public sealed unsafe class CmpFile : MetaBaseFile public float this[SubRace subRace, RspAttribute attribute] { - get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); - set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4) = value; + get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; } public override void Reset() @@ -42,7 +42,7 @@ public sealed unsafe class CmpFile : MetaBaseFile public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); + return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } private static int ToRspIndex(SubRace subRace) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 97f57703..70067c2b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -157,12 +157,12 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable public GmpEntry this[PrimaryId idx] { - get => (GmpEntry)GetInternal(idx); - set => SetInternal(idx, (ulong)value); + get => new() { Value = GetInternal(idx) }; + set => SetInternal(idx, value.Value); } public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) - => (GmpEntry)GetDefaultInternal(manager, InternalIndex, primaryIdx, (ulong)GmpEntry.Default); + => new() { Value = GetDefaultInternal(manager, InternalIndex, primaryIdx, GmpEntry.Default.Value) }; public void Reset(IEnumerable entries) { From dfdd5167a84c5a9c1aeb5280eb3fb91d895a613b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 17:24:42 +0200 Subject: [PATCH 1677/2451] Remove auto descriptions from newly generated option groups. --- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index d2c41f34..cf228889 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -22,7 +22,7 @@ public class ImcModGroup(Mod mod) : IModGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A single IMC manipulation."; + public string Description { get; set; } = string.Empty; public GroupType Type => GroupType.Imc; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7fc9acb3..38c0ef15 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -25,7 +25,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; + public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 4eec0746..49190e34 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -23,7 +23,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; + public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } From 7df9ddcb995b1f4b86abcfb827a63fb5488e6c1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 17:25:27 +0200 Subject: [PATCH 1678/2451] Re-Add button to open default mod json. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 125f539e..468e97b9 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -35,8 +35,8 @@ public class ModPanelEditTab( { private readonly TagButtons _modTags = new(); - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; public ReadOnlySpan Label => "Edit Mod"u8; @@ -193,6 +193,7 @@ public class ModPanelEditTab( if (ImGui.Button("Edit Description", reducedSize)) descriptionPopup.Open(_mod); + ImGui.SameLine(); var fileExists = File.Exists(filenames.ModMetaPath(_mod)); var tt = fileExists @@ -201,8 +202,22 @@ public class ModPanelEditTab( if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); + + DrawOpenDefaultMod(); } + private void DrawOpenDefaultMod() + { + var file = filenames.OptionGroupFile(_mod, -1, false); + var fileExists = File.Exists(file); + var tt = fileExists + ? "Open the default mod data file in the text editor of your choice." + : "The default mod data file does not exist."; + if (ImGuiUtil.DrawDisabledButton("Open Default Data", UiHelpers.InputTextWidth, tt, !fileExists)) + Process.Start(new ProcessStartInfo(file) { UseShellExecute = true }); + } + + /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { From fca1bf9d946dc505c1834ccef6feb206ee3039f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 22:30:42 +0200 Subject: [PATCH 1679/2451] Add ImcIdentifier. --- .../Meta/Manipulations/IMetaIdentifier.cs | 16 ++ Penumbra/Meta/Manipulations/Imc.cs | 187 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 Penumbra/Meta/Manipulations/IMetaIdentifier.cs create mode 100644 Penumbra/Meta/Manipulations/Imc.cs diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs new file mode 100644 index 00000000..4ad6bd3d --- /dev/null +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public interface IMetaIdentifier +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + + public MetaIndex FileIndex(); + + public bool Validate(); + + public JObject AddToJson(JObject jObj); +} diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs new file mode 100644 index 00000000..f0101be2 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -0,0 +1,187 @@ +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.String.Classes; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct ImcIdentifier( + PrimaryId PrimaryId, + Variant Variant, + ObjectType ObjectType, + SecondaryId SecondaryId, + EquipSlot EquipSlot, + BodySlot BodySlot) : IMetaIdentifier, IComparable +{ + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, ushort variant) + : this(primaryId, (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue), + slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, + variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown) + { } + + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, Variant variant) + : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) + { } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + var path = ObjectType switch + { + ObjectType.Equipment or ObjectType.Accessory => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, + Variant, + "a"), + ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, + "a"), + ObjectType.Monster => GamePaths.Monster.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + _ => string.Empty, + }; + if (path.Length == 0) + return; + + identifier.Identify(changedItems, path); + } + + public Utf8GamePath GamePath() + { + return ObjectType switch + { + ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + _ => throw new NotImplementedException(), + }; + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public override string ToString() + => ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}" + : $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}"; + + public bool Validate() + { + switch (ObjectType) + { + case ObjectType.Accessory: + case ObjectType.Equipment: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) + return false; + if (SecondaryId != 0) + return false; + + break; + case ObjectType.DemiHuman: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) + return false; + + break; + default: + if (!Enum.IsDefined(BodySlot)) + return false; + if (EquipSlot is not EquipSlot.Unknown) + return false; + if (!Enum.IsDefined(ObjectType)) + return false; + + break; + } + + return true; + } + + public int CompareTo(ImcIdentifier other) + { + var o = ObjectType.CompareTo(other.ObjectType); + if (o != 0) + return o; + + var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); + if (i != 0) + return i; + + if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + var e = EquipSlot.CompareTo(other.EquipSlot); + return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); + } + + if (ObjectType is ObjectType.DemiHuman) + { + var e = EquipSlot.CompareTo(other.EquipSlot); + if (e != 0) + return e; + } + + var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); + if (s != 0) + return s; + + var b = BodySlot.CompareTo(other.BodySlot); + return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); + } + + public static ImcIdentifier? FromJson(JObject jObj) + { + var objectType = jObj["PrimaryId"]?.ToObject() ?? ObjectType.Unknown; + var primaryId = new PrimaryId(jObj["PrimaryId"]?.ToObject() ?? 0); + var variant = jObj["Variant"]?.ToObject() ?? 0; + if (variant > byte.MaxValue) + return null; + + ImcIdentifier ret; + switch (objectType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + { + var slot = jObj["EquipSlot"]?.ToObject() ?? EquipSlot.Unknown; + ret = new ImcIdentifier(slot, primaryId, variant); + break; + } + case ObjectType.DemiHuman: + { + var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown); + break; + } + + case ObjectType.Monster: + case ObjectType.Weapon: + { + var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); + ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, EquipSlot.Unknown, BodySlot.Body); + break; + } + default: return null; + } + + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} From 992cdff58d3a82c64dc166147de7de4a10be87f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 22:31:39 +0200 Subject: [PATCH 1680/2451] Improve some IMC things. --- OtterGui | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 14 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 92 +++----- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 18 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 221 ++++++++++++++++++ Penumbra/UI/ModsTab/MetaManipulationDrawer.cs | 105 --------- 6 files changed, 267 insertions(+), 185 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ImcManipulationDrawer.cs delete mode 100644 Penumbra/UI/ModsTab/MetaManipulationDrawer.cs diff --git a/OtterGui b/OtterGui index 462acb87..1d936516 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 462acb87099650019996e4306d18cc70f76ca576 +Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index f0101be2..9b123df1 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -134,7 +135,7 @@ public readonly record struct ImcIdentifier( var b = BodySlot.CompareTo(other.BodySlot); return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); - } + } public static ImcIdentifier? FromJson(JObject jObj) { @@ -177,11 +178,12 @@ public readonly record struct ImcIdentifier( public JObject AddToJson(JObject jObj) { - var (gender, race) = GenderRace.Split(); - jObj["Gender"] = gender.ToString(); - jObj["Race"] = race.ToString(); - jObj["SetId"] = SetId.Id.ToString(); - jObj["Slot"] = Slot.ToString(); + jObj["ObjectType"] = ObjectType.ToString(); + jObj["PrimaryId"] = PrimaryId.Id; + jObj["PrimaryId"] = SecondaryId.Id; + jObj["Variant"] = Variant.Id; + jObj["EquipSlot"] = EquipSlot.ToString(); + jObj["BodySlot"] = BodySlot.ToString(); return jObj; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 55125375..99889360 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -352,26 +352,26 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - var change = MetaManipulationDrawer.DrawObjectType(ref _new); + var change = ImcManipulationDrawer.DrawObjectType(ref _new); ImGui.TableNextColumn(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _new); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _new); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= MetaManipulationDrawer.DrawSlot(ref _new); + change |= ImcManipulationDrawer.DrawSlot(ref _new); else - change |= MetaManipulationDrawer.DrawSecondaryId(ref _new); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _new); ImGui.TableNextColumn(); - change |= MetaManipulationDrawer.DrawVariant(ref _new); + change |= ImcManipulationDrawer.DrawVariant(ref _new); ImGui.TableNextColumn(); if (_new.ObjectType is ObjectType.DemiHuman) - change |= MetaManipulationDrawer.DrawSlot(ref _new, 70); + change |= ImcManipulationDrawer.DrawSlot(ref _new, 70); else ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); @@ -379,32 +379,20 @@ public partial class ModEditWindow _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); // Values using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput("##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, - 1, byte.MaxValue, 0f); - ImGui.SameLine(); - IntDragInput("##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, - defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f); - ImGui.TableNextColumn(); - IntDragInput("##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, - byte.MaxValue, 0f); - ImGui.SameLine(); - IntDragInput("##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, - 0f); - ImGui.SameLine(); - IntDragInput("##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, - 0f); - ImGui.TableNextColumn(); - for (var i = 0; i < 10; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - Checkmark("##attribute", $"{(char)('A' + i)}", (defaultEntry.Value.AttributeMask & flag) != 0, - (defaultEntry.Value.AttributeMask & flag) != 0, out _); - ImGui.SameLine(); - } - ImGui.NewLine(); + var entry = defaultEntry.Value; + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawMaterialId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawMaterialAnimationId(entry, ref entry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawDecalId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawVfxId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawSoundId(entry, ref entry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawAttributes(entry, ref entry); } public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) @@ -452,46 +440,22 @@ public partial class ModEditWindow new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); - if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, - defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId })); + var newEntry = meta.Entry; + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, - meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialAnimationId = (byte)materialAnimId })); - + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); - if (IntDragInput("##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, - defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { DecalId = (byte)decalId })); - + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, - out var vfxId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { VfxId = (byte)vfxId })); - + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, - defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { SoundId = (byte)soundId })); - + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); - for (var i = 0; i < 10; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - if (Checkmark("##attribute", $"{(char)('A' + i)}", (meta.Entry.AttributeMask & flag) != 0, - (defaultEntry.AttributeMask & flag) != 0, out var val)) - { - var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; - editor.MetaEditor.Change(meta.Copy(meta.Entry with { AttributeMask = (ushort)attributes })); - } + changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - ImGui.SameLine(); - } - - ImGui.NewLine(); + if (changes) + editor.MetaEditor.Change(meta.Copy(newEntry)); } } diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 06cb4154..2d80d3df 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -80,29 +80,29 @@ public class AddGroupDrawer : IUiService private void DrawImcInput(float width) { - var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, width); + var change = ImcManipulationDrawer.DrawObjectType(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcManip, width); if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); } else if (_imcManip.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); } else { - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); } if (change) diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs new file mode 100644 index 00000000..5873119e --- /dev/null +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -0,0 +1,221 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.HelperObjects; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public static class ImcManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, + out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialId = newValue }; + return true; + } + + public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, + defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialAnimationId = newValue }; + return true; + } + + public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { DecalId = newValue }; + return true; + } + + public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, + byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { VfxId = newValue }; + return true; + } + + public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { SoundId = newValue }; + return true; + } + + public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + { + var changes = false; + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (entry.AttributeMask & flag) != 0; + var def = (defaultEntry.AttributeMask & flag) != 0; + if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) + { + var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); + entry = entry with { AttributeMask = newMask }; + changes = true; + } + + if (i < ImcEntry.NumAttributes - 1) + ImGui.SameLine(); + } + + return changes; + } + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } +} diff --git a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs deleted file mode 100644 index 1f2273b5..00000000 --- a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs +++ /dev/null @@ -1,105 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class MetaManipulationDrawer -{ - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) - { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (manip.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); - return ret; - } - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } -} From 125e5628ecbf2e42d75e8c499db39bdeef3ba668 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 23:24:37 +0200 Subject: [PATCH 1681/2451] Fix fuckup. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 7efc76a6..05437e3d 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -72,7 +72,7 @@ public readonly struct ModSaveGroup : ISavable var serializer = new JsonSerializer { Formatting = Formatting.Indented }; j.WriteStartObject(); if (_groupIdx >= 0) - _group!.WriteJson(j, serializer); + _group!.WriteJson(j, serializer, _basePath); else SubMod.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); From 65627b5002c2b1ff9ed6b73cd5d1e7efb7ef2963 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 16:14:48 +0200 Subject: [PATCH 1682/2451] Fix a weird age-old bug apparently? --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index ca57c8b9..ae424b94 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -274,17 +274,17 @@ public class CollectionCacheManager : IDisposable return; } - type.HandlingInfo(out _, out var recomputeList, out var reload); + type.HandlingInfo(out _, out var recomputeList, out var justAdd); if (!recomputeList) return; foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) { - if (reload) - collection._cache!.ReloadMod(mod, true); - else + if (justAdd) collection._cache!.AddMod(mod, true); + else + collection._cache!.ReloadMod(mod, true); } } From 4743acf76786518307782677d6d38c48faeb0b95 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 16:15:04 +0200 Subject: [PATCH 1683/2451] Make IMC handling even better. --- Penumbra.GameData | 2 +- Penumbra/Meta/ImcChecker.cs | 36 +++ Penumbra/Meta/Manipulations/Imc.cs | 24 +- .../Meta/Manipulations/ImcManipulation.cs | 169 ++++-------- Penumbra/Meta/MetaFileManager.cs | 2 + Penumbra/Mods/Groups/ImcModGroup.cs | 251 ++++++++++-------- .../Manager/OptionEditor/ImcModGroupEditor.cs | 16 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/SubMods/ImcSubMod.cs | 12 +- Penumbra/Services/StaticServiceManager.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 65 ++--- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 68 ++--- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 161 +++++------ .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 32 ++- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 53 ++-- 15 files changed, 437 insertions(+), 459 deletions(-) create mode 100644 Penumbra/Meta/ImcChecker.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index e8220a0a..ec35e664 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e8220a0a74e9480330e98ed7ca462353434b9649 +Subproject commit ec35e66499eb388b4e7917e4fae4615218d33335 diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs new file mode 100644 index 00000000..14486e21 --- /dev/null +++ b/Penumbra/Meta/ImcChecker.cs @@ -0,0 +1,36 @@ +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public class ImcChecker(MetaFileManager metaFileManager) +{ + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); + + private readonly Dictionary _cachedDefaultEntries = new(); + + public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + { + if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + return entry; + + try + { + var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + entry = new CachedEntry(e, true, entryExists); + } + catch (Exception) + { + entry = new CachedEntry(default, false, false); + } + + if (storeCache) + _cachedDefaultEntries.Add(identifier, entry); + return entry; + } + + public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) + => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, + imcManip.EquipSlot, imcManip.BodySlot), storeCache); +} diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 9b123df1..fef86520 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -15,6 +15,8 @@ public readonly record struct ImcIdentifier( EquipSlot EquipSlot, BodySlot BodySlot) : IMetaIdentifier, IComparable { + public static readonly ImcIdentifier Default = new(EquipSlot.Body, 1, (Variant)1); + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, ushort variant) : this(primaryId, (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue), slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, @@ -25,6 +27,9 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } + public ImcManipulation ToManipulation(ImcEntry entry) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = ObjectType switch @@ -137,9 +142,12 @@ public readonly record struct ImcIdentifier( return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); } - public static ImcIdentifier? FromJson(JObject jObj) + public static ImcIdentifier? FromJson(JObject? jObj) { - var objectType = jObj["PrimaryId"]?.ToObject() ?? ObjectType.Unknown; + if (jObj == null) + return null; + + var objectType = jObj["ObjectType"]?.ToObject() ?? ObjectType.Unknown; var primaryId = new PrimaryId(jObj["PrimaryId"]?.ToObject() ?? 0); var variant = jObj["Variant"]?.ToObject() ?? 0; if (variant > byte.MaxValue) @@ -178,12 +186,12 @@ public readonly record struct ImcIdentifier( public JObject AddToJson(JObject jObj) { - jObj["ObjectType"] = ObjectType.ToString(); - jObj["PrimaryId"] = PrimaryId.Id; - jObj["PrimaryId"] = SecondaryId.Id; - jObj["Variant"] = Variant.Id; - jObj["EquipSlot"] = EquipSlot.ToString(); - jObj["BodySlot"] = BodySlot.ToString(); + jObj["ObjectType"] = ObjectType.ToString(); + jObj["PrimaryId"] = PrimaryId.Id; + jObj["SecondaryId"] = SecondaryId.Id; + jObj["Variant"] = Variant.Id; + jObj["EquipSlot"] = EquipSlot.ToString(); + jObj["BodySlot"] = BodySlot.ToString(); return jObj; } } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 45295990..945aab04 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -12,171 +12,96 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct ImcManipulation : IMetaManipulation { - public ImcEntry Entry { get; private init; } - public PrimaryId PrimaryId { get; private init; } - public PrimaryId SecondaryId { get; private init; } - public Variant Variant { get; private init; } + [JsonIgnore] + public ImcIdentifier Identifier { get; private init; } + + public ImcEntry Entry { get; private init; } + + + public PrimaryId PrimaryId + => Identifier.PrimaryId; + + public SecondaryId SecondaryId + => Identifier.SecondaryId; + + public Variant Variant + => Identifier.Variant; [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType { get; private init; } + public ObjectType ObjectType + => Identifier.ObjectType; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot { get; private init; } + public EquipSlot EquipSlot + => Identifier.EquipSlot; [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot { get; private init; } + public BodySlot BodySlot + => Identifier.BodySlot; public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) + : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) + { } + + public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) { - Entry = entry; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - SecondaryId = 0; - ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; - EquipSlot = equipSlot; - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; + Identifier = identifier; + Entry = entry; } + // Variants were initially ushorts but got shortened to bytes. // There are still some manipulations around that have values > 255 for variant, // so we change the unused value to something nonsensical in that case, just so they do not compare equal, // and clamp the variant to 255. [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, PrimaryId secondaryId, ushort variant, + internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry) { - Entry = entry; - ObjectType = objectType; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - - if (objectType is ObjectType.Accessory or ObjectType.Equipment) + Entry = entry; + var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); + Identifier = objectType switch { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = 0; - EquipSlot = equipSlot; - } - else if (objectType is ObjectType.DemiHuman) - { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = secondaryId; - EquipSlot = equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot; - } - else - { - BodySlot = bodySlot; - SecondaryId = secondaryId; - EquipSlot = variant > byte.MaxValue ? EquipSlot.All : EquipSlot.Unknown; - } + ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, + variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, + equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, + bodySlot), + }; } public ImcManipulation Copy(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant.Id, EquipSlot, entry); + => new(Identifier, entry); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}"; + => Identifier.ToString(); public bool Equals(ImcManipulation other) - => PrimaryId == other.PrimaryId - && Variant == other.Variant - && SecondaryId == other.SecondaryId - && ObjectType == other.ObjectType - && EquipSlot == other.EquipSlot - && BodySlot == other.BodySlot; + => Identifier == other.Identifier; public override bool Equals(object? obj) => obj is ImcManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine(PrimaryId, Variant, SecondaryId, (int)ObjectType, (int)EquipSlot, (int)BodySlot); + => Identifier.GetHashCode(); public int CompareTo(ImcManipulation other) - { - var o = ObjectType.CompareTo(other.ObjectType); - if (o != 0) - return o; - - var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); - if (i != 0) - return i; - - if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); - } - - if (ObjectType is ObjectType.DemiHuman) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - if (e != 0) - return e; - } - - var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); - if (s != 0) - return s; - - var b = BodySlot.CompareTo(other.BodySlot); - return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); - } + => Identifier.CompareTo(other.Identifier); public MetaIndex FileIndex() - => (MetaIndex)(-1); + => Identifier.FileIndex(); public Utf8GamePath GamePath() - { - return ObjectType switch - { - ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId), out var p) ? p : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), - }; - } + => Identifier.GamePath(); public bool Apply(ImcFile file) => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); public bool Validate(bool withMaterial) { - switch (ObjectType) - { - case ObjectType.Accessory: - case ObjectType.Equipment: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - if (SecondaryId != 0) - return false; - - break; - case ObjectType.DemiHuman: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - - break; - default: - if (!Enum.IsDefined(BodySlot)) - return false; - if (EquipSlot is not EquipSlot.Unknown) - return false; - if (!Enum.IsDefined(ObjectType)) - return false; - - break; - } + if (!Identifier.Validate()) + return false; if (withMaterial && Entry.MaterialId == 0) return false; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index cd99396b..40fceb07 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -27,6 +27,7 @@ public unsafe class MetaFileManager internal readonly ValidityChecker ValidityChecker; internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; + internal readonly ImcChecker ImcChecker; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -40,6 +41,7 @@ public unsafe class MetaFileManager ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + ImcChecker = new ImcChecker(this); interop.InitializeFromAttributes(this); } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index cf228889..e0d70aa6 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,25 +1,21 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab.Groups; -using Penumbra.Util; namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 60; - public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = string.Empty; @@ -33,33 +29,35 @@ public class ImcModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; - public PrimaryId PrimaryId; - public SecondaryId SecondaryId; - public ObjectType ObjectType; - public BodySlot BodySlot; - public EquipSlot EquipSlot; - public Variant Variant; - - public ImcEntry DefaultEntry; + public ImcIdentifier Identifier; + public ImcEntry DefaultEntry; public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; - private bool _canBeDisabled = false; + private bool _canBeDisabled; public bool CanBeDisabled { - get => _canBeDisabled; + get => OptionData.Any(m => m.IsDisableSubMod); set { _canBeDisabled = value; if (!value) + { + OptionData.RemoveAll(m => m.IsDisableSubMod); DefaultSettings = FixSetting(DefaultSettings); + } + else + { + if (!OptionData.Any(m => m.IsDisableSubMod)) + OptionData.Add(ImcSubMod.DisableSubMod(this)); + } } } public bool DefaultDisabled - => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + => IsDisabled(DefaultSettings); public IModOption? AddOption(string name, string description = "") { @@ -86,7 +84,7 @@ public class ImcModGroup(Mod mod) : IModGroup => []; public bool IsOption - => CanBeDisabled || OptionData.Count > 0; + => OptionData.Count > 0; public int GetIndex() => ModGroup.GetIndex(this); @@ -94,6 +92,128 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); + public ImcManipulation GetManip(ushort mask) + => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, Identifier.Variant.Id, + Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (IsDisabled(setting)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => Identifier.AddChangedItems(identifier, changedItems); + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << OptionData.Count) - 1)); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + var jObj = Identifier.AddToJson(new JObject()); + jWriter.WritePropertyName(nameof(Identifier)); + jObj.WriteTo(jWriter); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + if (option.IsDisableSubMod) + { + jWriter.WritePropertyName(nameof(option.IsDisableSubMod)); + jWriter.WriteValue(true); + } + else + { + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); + } + + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); + + public static ImcModGroup? Load(Mod mod, JObject json) + { + var options = json["Options"]; + var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); + var ret = new ImcModGroup(mod) + { + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + }; + if (ret.Name.Length == 0) + return null; + + if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) + { + Penumbra.Messager.NotificationMessage($"Could not add IMC group {ret.Name} because the associated IMC Entry is invalid.", + NotificationType.Warning); + return null; + } + + var rollingMask = ret.DefaultEntry.AttributeMask; + if (options != null) + foreach (var child in options.Children()) + { + var subMod = new ImcSubMod(ret, child); + + if (subMod.IsDisableSubMod) + ret._canBeDisabled = true; + + if (subMod.IsDisableSubMod && ret.OptionData.FirstOrDefault(m => m.IsDisableSubMod) is { } disable) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it already contains {disable.Name} as disable option.", + NotificationType.Warning); + } + else if ((subMod.AttributeMask & rollingMask) != 0) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it contains attributes already in use.", + NotificationType.Warning); + } + else + { + rollingMask |= subMod.AttributeMask; + ret.OptionData.Add(subMod); + } + } + + ret.Identifier = identifier.Value; + ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + return ret; + } + + private bool IsDisabled(Setting setting) + { + if (!CanBeDisabled) + return false; + + var idx = OptionData.IndexOf(m => m.IsDisableSubMod); + if (idx >= 0) + return setting.HasFlag(idx); + + Penumbra.Log.Warning($"A IMC Group should be able to be disabled, but does not contain a disable option."); + return false; + } + private ushort GetCurrentMask(Setting setting) { var mask = DefaultEntry.AttributeMask; @@ -108,101 +228,4 @@ public class ImcModGroup(Mod mod) : IModGroup return mask; } - - private ushort GetFullMask() - => GetCurrentMask(Setting.AllBits(63)); - - public ImcManipulation GetManip(ushort mask) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, - DefaultEntry with { AttributeMask = mask }); - - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - { - if (CanBeDisabled && setting.HasFlag(DisabledIndex)) - return; - - var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); - } - - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.MetaChangedItems(changedItems, GetManip(0)); - - public Setting FixSetting(Setting setting) - => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); - - public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) - { - ModSaveGroup.WriteJsonBase(jWriter, this); - jWriter.WritePropertyName(nameof(ObjectType)); - jWriter.WriteValue(ObjectType.ToString()); - jWriter.WritePropertyName(nameof(BodySlot)); - jWriter.WriteValue(BodySlot.ToString()); - jWriter.WritePropertyName(nameof(EquipSlot)); - jWriter.WriteValue(EquipSlot.ToString()); - jWriter.WritePropertyName(nameof(PrimaryId)); - jWriter.WriteValue(PrimaryId.Id); - jWriter.WritePropertyName(nameof(SecondaryId)); - jWriter.WriteValue(SecondaryId.Id); - jWriter.WritePropertyName(nameof(Variant)); - jWriter.WriteValue(Variant.Id); - jWriter.WritePropertyName(nameof(DefaultEntry)); - serializer.Serialize(jWriter, DefaultEntry); - jWriter.WritePropertyName("Options"); - jWriter.WriteStartArray(); - foreach (var option in OptionData) - { - jWriter.WriteStartObject(); - SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeMask)); - jWriter.WriteValue(option.AttributeMask); - jWriter.WriteEndObject(); - } - - jWriter.WriteEndArray(); - } - - public (int Redirections, int Swaps, int Manips) GetCounts() - => (0, 0, 1); - - public static ImcModGroup? Load(Mod mod, JObject json) - { - var options = json["Options"]; - var ret = new ImcModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - ObjectType = json[nameof(ObjectType)]?.ToObject() ?? ObjectType.Unknown, - BodySlot = json[nameof(BodySlot)]?.ToObject() ?? BodySlot.Unknown, - EquipSlot = json[nameof(EquipSlot)]?.ToObject() ?? EquipSlot.Unknown, - PrimaryId = new PrimaryId(json[nameof(PrimaryId)]?.ToObject() ?? 0), - SecondaryId = new SecondaryId(json[nameof(SecondaryId)]?.ToObject() ?? 0), - Variant = new Variant(json[nameof(Variant)]?.ToObject() ?? 0), - CanBeDisabled = json[nameof(CanBeDisabled)]?.ToObject() ?? false, - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - }; - if (ret.Name.Length == 0) - return null; - - if (options != null) - foreach (var child in options.Children()) - { - var subMod = new ImcSubMod(ret, child); - ret.OptionData.Add(subMod); - } - - if (!new ImcManipulation(ret.ObjectType, ret.BodySlot, ret.PrimaryId, ret.SecondaryId.Id, ret.Variant.Id, ret.EquipSlot, - ret.DefaultEntry).Validate(true)) - { - Penumbra.Messager.NotificationMessage($"Could not add IMC group because the associated IMC Entry is invalid.", - NotificationType.Warning); - return null; - } - - ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); - return ret; - } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 20021d29..f9fd532f 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -7,7 +7,6 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -15,13 +14,13 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ : ModOptionEditor(communicator, saveService, config), IService { /// Add a new, empty imc group with the given manipulation data. - public ImcModGroup? AddModGroup(Mod mod, string newName, ImcManipulation manip, SaveType saveType = SaveType.ImmediateSync) + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, manip, maxPriority); + var group = CreateGroup(mod, newName, identifier, defaultEntry, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -97,19 +96,14 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ }; - private static ImcModGroup CreateGroup(Mod mod, string newName, ImcManipulation manip, ModPriority priority, + private static ImcModGroup CreateGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { Name = newName, Priority = priority, - ObjectType = manip.ObjectType, - EquipSlot = manip.EquipSlot, - BodySlot = manip.BodySlot, - PrimaryId = manip.PrimaryId, - SecondaryId = manip.SecondaryId.Id, - Variant = manip.Variant, - DefaultEntry = manip.Entry, + Identifier = identifier, + DefaultEntry = defaultEntry, }; protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 969ad3fa..e1db0ccf 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -246,7 +246,7 @@ public class ModGroupEditor( { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), _ => null, }; diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index 7f46bc95..c5c8f002 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -12,13 +12,23 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); - AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + IsDisableSubMod = json[nameof(IsDisableSubMod)]?.ToObject() ?? false; } + public static ImcSubMod DisableSubMod(ImcModGroup group) + => new(group) + { + Name = "Disable", + AttributeMask = 0, + IsDisableSubMod = true, + }; + public Mod Mod => Group.Mod; public ushort AttributeMask; + public bool IsDisableSubMod { get; private init; } Mod IModOption.Mod => Mod; diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 19ae31a2..0c6648ba 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -101,7 +101,8 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(p => p.GetRequiredService().ImcChecker); private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 99889360..68933c9e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -19,9 +19,6 @@ public partial class ModEditWindow private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string PrimaryIdTooltip = - "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; private const string EquipSlotTooltip = "Equip Slot"; private const string ModelRaceTooltip = "Model Race"; @@ -316,7 +313,7 @@ public partial class ModEditWindow private static class ImcRow { - private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; private static float IdWidth => 80 * UiHelpers.Scale; @@ -324,75 +321,60 @@ public partial class ModEditWindow private static float SmallIdWidth => 45 * UiHelpers.Scale; - /// Convert throwing to null-return if the file does not exist. - private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc) - { - try - { - return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _); - } - catch - { - return null; - } - } - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, _new); - var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; - defaultEntry ??= new ImcEntry(); + var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); + var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); + var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); + var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry.Value)); + editor.MetaEditor.Add(manip); // Identifier ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _new); + var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _new); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _new); + if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _new); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _new); + change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); ImGui.TableNextColumn(); - if (_new.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _new, 70); + if (_newIdentifier.ObjectType is ObjectType.DemiHuman) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); else ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); if (change) - _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); + defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; // Values using var disabled = ImRaii.Disabled(); - - var entry = defaultEntry.Value; ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(entry, ref entry, false); + ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(entry, ref entry, false); + ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(entry, ref entry, false); + ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(entry, ref entry); + ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); } public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) @@ -439,10 +421,9 @@ public partial class ModEditWindow using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); + var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; var newEntry = meta.Entry; - - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); ImGui.SameLine(); changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 2d80d3df..3ac10cd0 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -5,7 +5,6 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -17,20 +16,20 @@ namespace Penumbra.UI.ModsTab.Groups; public class AddGroupDrawer : IUiService { private string _groupName = string.Empty; - private bool _groupNameValid = false; + private bool _groupNameValid; - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; + private ImcIdentifier _imcIdentifier = ImcIdentifier.Default; + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly ImcChecker _imcChecker; + private readonly ModManager _modManager; - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) { - _metaManager = metaManager; _modManager = modManager; + _imcChecker = imcChecker; UpdateEntry(); } @@ -61,7 +60,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -74,35 +73,35 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcManip, width); + var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcManip, width); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) + else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) @@ -125,8 +124,8 @@ public class AddGroupDrawer : IUiService : "Add a new multi selection option group to this mod."u8, width, !_groupNameValid || _entryInvalid)) { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry); + _groupName = string.Empty; _groupNameValid = false; } @@ -142,20 +141,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); + (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 2418c5cb..045149c9 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,10 +1,8 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; @@ -16,59 +14,75 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr { public void Draw() { + var identifier = group.Identifier; + var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var entry = group.DefaultEntry; + var changes = false; + + ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + using (ImUtf8.Group()) { - ImUtf8.Text("Object Type"u8); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text("Slot"u8); - ImUtf8.Text("Primary ID"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text("Secondary ID"); - ImUtf8.Text("Variant"u8); - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + } + + ImGui.SameLine(0, editor.PriorityWidth); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Material Animation ID"u8); ImUtf8.TextFrameAligned("Sound ID"u8); ImUtf8.TextFrameAligned("Can Be Disabled"u8); - ImUtf8.TextFrameAligned("Default Attributes"u8); } ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); + } + + if (changes) + editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry); + + ImGui.Dummy(Vector2.Zero); + DrawOptions(); var attributeCache = new ImcAttributeCache(group); + DrawNewOption(attributeCache); + ImGui.Dummy(Vector2.Zero); + using (ImUtf8.Group()) { - ImUtf8.Text(group.ObjectType.ToName()); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text(group.EquipSlot.ToName()); - ImUtf8.Text($"{group.PrimaryId.Id}"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text($"{group.SecondaryId.Id}"); - ImUtf8.Text($"{group.Variant.Id}"); - - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); - - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); - - var defaultDisabled = group.DefaultDisabled; - ImUtf8.SameLineInner(); - if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) - editor.ModManager.OptionEditor.ChangeModGroupDefaultOption(group, - group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); - - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + ImUtf8.TextFrameAligned("Default Attributes"u8); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + ImUtf8.TextFrameAligned(option.Name); } + ImUtf8.SameLineInner(); + using (ImUtf8.Group()) + { + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + } + private void DrawOptions() + { foreach (var (option, optionIdx) in group.OptionData.WithIndex()) { using var id = ImRaii.PushId(optionIdx); @@ -83,56 +97,51 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImUtf8.SameLineInner(); editor.DrawOptionDescription(option); - ImUtf8.SameLineInner(); - editor.DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + editor.OptionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); - } - - DrawNewOption(attributeCache); - return; - - static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) + if (!option.IsDisableSubMod) { - using var id = ImRaii.PushId(i); - var value = (mask & 1 << i) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) - { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - ImUtf8.HoverTooltip($"{(char)('A' + i)}"); - if (i != 9) - ImUtf8.SameLineInner(); + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); } } } private void DrawNewOption(in ImcAttributeCache cache) { - if (cache.LowestUnsetMask == 0) - return; - - var name = editor.DrawNewOptionBase(group, group.Options.Count); + var dis = cache.LowestUnsetMask == 0; + var name = editor.DrawNewOptionBase(group, group.Options.Count); var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + var tt = dis + ? "No Free Attribute Slots for New Options..."u8 + : validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, !validName || dis)) { editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); editor.NewOptionName = null; } } + + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip("ABCDEFGHIJ"u8.Slice(i, 1)); + if (i != 9) + ImUtf8.SameLineInner(); + } + } } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e7d70922..e8a27a74 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -22,11 +23,16 @@ public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + ImcChecker imcChecker) : IUiService { - private static ReadOnlySpan DragDropLabel - => "##DragOption"u8; + private static ReadOnlySpan AcrossGroupsLabel + => "##DragOptionAcross"u8; + private static ReadOnlySpan InsideGroupLabel + => "##DragOptionInside"u8; + + internal readonly ImcChecker ImcChecker = imcChecker; internal readonly ModManager ModManager = modManager; internal readonly Queue ActionQueue = new(); @@ -50,6 +56,7 @@ public sealed class ModGroupEditDrawer( private IModGroup? _dragDropGroup; private IModOption? _dragDropOption; + private bool _draggingAcross; public void Draw(Mod mod) { @@ -292,32 +299,30 @@ public sealed class ModGroupEditDrawer( [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Source(IModOption option) { - if (option.Group is not ITexToolsGroup) - return; - using var source = ImUtf8.DragDropSource(); if (!source) return; - if (!DragDropSource.SetPayload(DragDropLabel)) + var across = option.Group is ITexToolsGroup; + + if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) { _dragDropGroup = option.Group; _dragDropOption = option; + _draggingAcross = across; } - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); } private void Target(IModGroup group, int optionIdx) { - if (group is not ITexToolsGroup) - return; - - if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + if (_dragDropGroup != group + && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) return; using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + if (!target.Success || !DragDropTarget.CheckPayload(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) return; if (_dragDropGroup != null && _dragDropOption != null) @@ -342,6 +347,7 @@ public sealed class ModGroupEditDrawer( _dragDropGroup = null; _dragDropOption = null; + _draggingAcross = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 5873119e..c14652ac 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -1,8 +1,6 @@ using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using OtterGui.Text.HelperObjects; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -12,79 +10,78 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); ImUtf8.HoverTooltip("Object Type"u8); if (ret) { var equipSlot = type switch { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, _ => EquipSlot.Unknown, }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); + identifier = identifier with + { + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; } return ret; } - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + "This should generally not be left <= 1 unless you explicitly want that."u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { PrimaryId = newId }; return ret; } - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); ImUtf8.HoverTooltip("Secondary ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { SecondaryId = newId }; return ret; } - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); ImUtf8.HoverTooltip("Variant ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); + identifier = identifier with { Variant = (byte)newId }; return ret; } - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) { bool ret; EquipSlot slot; - switch (manip.ObjectType) + switch (identifier.ObjectType) { case ObjectType.Equipment: case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; default: return false; } ImUtf8.HoverTooltip("Equip Slot"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); + identifier = identifier with { EquipSlot = slot }; return ret; } From bad1f45ab91f54576da1041577eaeca83cb653ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 17:34:56 +0200 Subject: [PATCH 1684/2451] Use different hooking method for EQP entries. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 34 ++++++++++++++++++ Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 5 +-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 36 +++++++++++++++++++ .../Interop/Hooks/Meta/ModelLoadComplete.cs | 4 ++- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 6 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 1 + 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EqpHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ec35e664..539d1387 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ec35e66499eb388b4e7917e4fae4615218d33335 +Subproject commit 539d138700543e7c2c6c918f9f68e33228111e4d diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs new file mode 100644 index 00000000..457b9428 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqpHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor); + + private readonly MetaState _metaState; + + public EqpHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) + { + if (_metaState.EqpCollection.Valid) + { + using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); + Task.Result.Original(utility, flags, armor); + } + else + { + Task.Result.Original(utility, flags, armor); + } + + Penumbra.Log.Excessive($"[GetEqpFlags] Invoked on 0x{(nint)utility:X} with 0x{(ulong)armor:X}, returned 0x{(ulong)*flags:X16}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index 8ffc050f..beae6acc 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -28,8 +29,8 @@ public sealed unsafe class GetEqpIndirect : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; } } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs new file mode 100644 index 00000000..89aaa9b0 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class GetEqpIndirect2 : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public GetEqpIndirect2(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if (((*(uint*)((nint)drawObject + Offsets.GetEqpIndirect2Skip) >> 0x12) & 1) == 0) + return; + + Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); + _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 9f191fdd..10c12594 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; @@ -23,8 +24,9 @@ public sealed unsafe class ModelLoadComplete : FastHook Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqp = _metaState.ResolveEqpData(collection.ModCollection); using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); - Task.Result.Original.Invoke(drawObject); + _metaState.EqpCollection = collection; + Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; } } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 5f07ffc5..6fa5c263 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -46,6 +46,7 @@ public sealed unsafe class MetaState : IDisposable private readonly CreateCharacterBase _createCharacterBase; public ResolveData CustomizeChangeCollection = ResolveData.Invalid; + public ResolveData EqpCollection = ResolveData.Invalid; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; From e32e314863e46fb4b806bc9b2c3adf86a5ec2d0e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 17:51:47 +0200 Subject: [PATCH 1685/2451] Add initial changelog for next release. --- Penumbra/UI/Changelog.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 8b00ab8d..add16f94 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -48,10 +48,34 @@ public class PenumbraChangelog Add8_3_0(Changelog); Add1_0_0_0(Changelog); Add1_0_2_0(Changelog); + Add1_0_3_0(Changelog); } #region Changelogs + private static void Add1_0_3_0(Changelog log) + => log.NextVersion("Version 1.0.3.0") + .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") + .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) + .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight("A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") + .RegisterEntry("Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", 1) + .RegisterEntry("This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", 1) + .RegisterHighlight("Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") + .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) + .RegisterEntry("You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") + .RegisterHighlight("Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") + .RegisterEntry("Added support for reading and writing the new material and model file formats from the benchmark.") + .RegisterEntry("Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") + .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") + .RegisterEntry("Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") + .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") + .RegisterEntry("Reworked the API and IPC structure heavily.") + .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") + .RegisterEntry("Fixed an issue with merging and deduplicating mods.") + .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") + .RegisterEntry("Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); + private static void Add1_0_2_0(Changelog log) => log.NextVersion("Version 1.0.2.0") .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") From f9527970cb610f8d8529478e9674d1bd32548bf9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 May 2024 00:43:02 +0200 Subject: [PATCH 1686/2451] Fix missing ID push for imc attributes. --- Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 045149c9..b10f123c 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -76,8 +76,11 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr using (ImUtf8.Group()) { DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); - foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + foreach (var (option, idx) in group.OptionData.WithIndex().Where(o => !o.Value.IsDisableSubMod)) + { + using var id = ImUtf8.PushId(idx); DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } } } From ed083f2a4ca375bf50eede01d72796408c320d7c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 13:30:35 +0200 Subject: [PATCH 1687/2451] Add support for Global EQP Changes. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 1 - Penumbra/Collections/Cache/MetaCache.cs | 48 ++++-- .../Collections/ModCollection.Cache.Access.cs | 4 + Penumbra/Import/TexToolsMeta.Export.cs | 3 + Penumbra/Import/TexToolsMeta.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 1 + Penumbra/Meta/Manipulations/GlobalEqpCache.cs | 80 +++++++++ .../Manipulations/GlobalEqpManipulation.cs | 50 ++++++ Penumbra/Meta/Manipulations/GlobalEqpType.cs | 61 +++++++ .../Meta/Manipulations/MetaManipulation.cs | 156 ++++++++++-------- Penumbra/Mods/Editor/ModMetaEditor.cs | 97 ++++++----- Penumbra/Mods/SubMods/IModDataContainer.cs | 13 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 66 ++++++++ Penumbra/Util/IdentifierExtensions.cs | 20 +++ 15 files changed, 471 insertions(+), 133 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpCache.cs create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpType.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 539d1387..07cc26f1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 539d138700543e7c2c6c918f9f68e33228111e4d +Subproject commit 07cc26f196984a44711b3bc4c412947d863288bd diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e2f20b46..4d8d0b4a 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -5,7 +5,6 @@ using Penumbra.Mods; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; -using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 4c147c3c..f42b72fc 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -4,7 +4,6 @@ using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; @@ -14,13 +13,14 @@ public class MetaCache : IDisposable, IEnumerable _manipulations = new(); - private EqpCache _eqpCache = new(); - private readonly EqdpCache _eqdpCache = new(); - private EstCache _estCache = new(); - private GmpCache _gmpCache = new(); - private CmpCache _cmpCache = new(); - private readonly ImcCache _imcCache = new(); + private readonly Dictionary _manipulations = new(); + private EqpCache _eqpCache = new(); + private readonly EqdpCache _eqdpCache = new(); + private EstCache _estCache = new(); + private GmpCache _gmpCache = new(); + private CmpCache _cmpCache = new(); + private readonly ImcCache _imcCache = new(); + private GlobalEqpCache _globalEqpCache = new(); public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) { @@ -69,6 +69,7 @@ public class MetaCache : IDisposable, IEnumerable _estCache.TemporarilySetFiles(_manager, type); + public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) + => _globalEqpCache.Apply(baseEntry, armor); + /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) @@ -193,8 +204,8 @@ public class MetaCache : IDisposable, IEnumerable _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, + MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), + MetaManipulation.Type.GlobalEqp => false, + MetaManipulation.Type.Unknown => false, + _ => false, } ? 1 : 0; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index b073e731..3f3733e0 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -8,6 +8,7 @@ using Penumbra.String.Classes; using Penumbra.Collections.Cache; using Penumbra.Interop.Services; using Penumbra.Mods.Editor; +using Penumbra.GameData.Structs; namespace Penumbra.Collections; @@ -114,4 +115,7 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); + + public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) + => _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 90ffaf60..03bdbd90 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -187,6 +187,9 @@ public partial class TexToolsMeta b.Write(manip.Gmp.Entry.UnknownTotal); } + break; + case MetaManipulation.Type.GlobalEqp: + // Not Supported break; } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 83b430fb..25e00bd7 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -44,7 +44,7 @@ public partial class TexToolsMeta var headerStart = reader.ReadUInt32(); reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); - List<(MetaManipulation.Type type, uint offset, int size)> entries = new(); + List<(MetaManipulation.Type type, uint offset, int size)> entries = []; for (var i = 0; i < numHeaders; ++i) { var currentOffset = reader.BaseStream.Position; diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 457b9428..6663c211 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -23,6 +23,7 @@ public unsafe class EqpHook : FastHook { using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); Task.Result.Original(utility, flags, armor); + *flags = _metaState.EqpCollection.ModCollection.ApplyGlobalEqp(*flags, armor); } else { diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs new file mode 100644 index 00000000..48ffb308 --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs @@ -0,0 +1,80 @@ +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Meta.Manipulations; + +public struct GlobalEqpCache : IService +{ + private readonly HashSet _doNotHideEarrings = []; + private readonly HashSet _doNotHideNecklace = []; + private readonly HashSet _doNotHideBracelets = []; + private readonly HashSet _doNotHideRingL = []; + private readonly HashSet _doNotHideRingR = []; + private bool _doNotHideVieraHats; + private bool _doNotHideHrothgarHats; + + public GlobalEqpCache() + { } + + public void Clear() + { + _doNotHideEarrings.Clear(); + _doNotHideNecklace.Clear(); + _doNotHideBracelets.Clear(); + _doNotHideRingL.Clear(); + _doNotHideRingR.Clear(); + _doNotHideHrothgarHats = false; + _doNotHideVieraHats = false; + } + + public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) + { + if (_doNotHideVieraHats) + original |= EqpEntry.HeadShowVieraHat; + + if (_doNotHideHrothgarHats) + original |= EqpEntry.HeadShowHrothgarHat; + + if (_doNotHideEarrings.Contains(armor[5].Set)) + original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman; + + if (_doNotHideNecklace.Contains(armor[6].Set)) + original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; + + if (_doNotHideBracelets.Contains(armor[7].Set)) + original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet; + + if (_doNotHideBracelets.Contains(armor[8].Set)) + original |= EqpEntry.HandShowRingR; + + if (_doNotHideBracelets.Contains(armor[9].Set)) + original |= EqpEntry.HandShowRingL; + return original; + } + + public bool Add(GlobalEqpManipulation manipulation) + => manipulation.Type switch + { + GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition), + GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition), + GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Add(manipulation.Condition), + GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition), + GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), + GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), + GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + _ => false, + }; + + public bool Remove(GlobalEqpManipulation manipulation) + => manipulation.Type switch + { + GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), + GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + _ => false, + }; +} diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs new file mode 100644 index 00000000..ada543dc --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -0,0 +1,50 @@ +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct GlobalEqpManipulation : IMetaManipulation +{ + public GlobalEqpType Type { get; init; } + public PrimaryId Condition { get; init; } + + public bool Validate() + { + if (!Enum.IsDefined(Type)) + return false; + + if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats) + return Condition == 0; + + return Condition != 0; + } + + + public bool Equals(GlobalEqpManipulation other) + => Type == other.Type + && Condition.Equals(other.Condition); + + public int CompareTo(GlobalEqpManipulation other) + { + var typeComp = Type.CompareTo(other); + return typeComp != 0 ? typeComp : Condition.Id.CompareTo(other.Condition.Id); + } + + public override bool Equals(object? obj) + => obj is GlobalEqpManipulation other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine((int)Type, Condition); + + public static bool operator ==(GlobalEqpManipulation left, GlobalEqpManipulation right) + => left.Equals(right); + + public static bool operator !=(GlobalEqpManipulation left, GlobalEqpManipulation right) + => !left.Equals(right); + + public override string ToString() + => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + + public MetaIndex FileIndex() + => (MetaIndex)(-1); +} diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs new file mode 100644 index 00000000..57d99d56 --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -0,0 +1,61 @@ +namespace Penumbra.Meta.Manipulations; + +public enum GlobalEqpType +{ + DoNotHideEarrings, + DoNotHideNecklace, + DoNotHideBracelets, + DoNotHideRingR, + DoNotHideRingL, + DoNotHideHrothgarHats, + DoNotHideVieraHats, +} + +public static class GlobalEqpExtensions +{ + public static bool HasCondition(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => true, + GlobalEqpType.DoNotHideNecklace => true, + GlobalEqpType.DoNotHideBracelets => true, + GlobalEqpType.DoNotHideRingR => true, + GlobalEqpType.DoNotHideRingL => true, + GlobalEqpType.DoNotHideHrothgarHats => false, + GlobalEqpType.DoNotHideVieraHats => false, + _ => false, + }; + + + public static ReadOnlySpan ToName(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => "Do Not Hide Earrings"u8, + GlobalEqpType.DoNotHideNecklace => "Do Not Hide Necklaces"u8, + GlobalEqpType.DoNotHideBracelets => "Do Not Hide Bracelets"u8, + GlobalEqpType.DoNotHideRingR => "Do Not Hide Rings (Right Finger)"u8, + GlobalEqpType.DoNotHideRingL => "Do Not Hide Rings (Left Finger)"u8, + GlobalEqpType.DoNotHideHrothgarHats => "Do Not Hide Hats for Hrothgar"u8, + GlobalEqpType.DoNotHideVieraHats => "Do Not Hide Hats for Viera"u8, + _ => "\0"u8, + }; + + public static ReadOnlySpan ToDescription(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => "Prevents the game from hiding earrings through other models when a specific earring is worn."u8, + GlobalEqpType.DoNotHideNecklace => + "Prevents the game from hiding necklaces through other models when a specific necklace is worn."u8, + GlobalEqpType.DoNotHideBracelets => + "Prevents the game from hiding bracelets through other models when a specific bracelet is worn."u8, + GlobalEqpType.DoNotHideRingR => + "Prevents the game from hiding rings worn on the right finger through other models when a specific ring is worn on the right finger."u8, + GlobalEqpType.DoNotHideRingL => + "Prevents the game from hiding rings worn on the left finger through other models when a specific ring is worn on the left finger."u8, + GlobalEqpType.DoNotHideHrothgarHats => + "Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8, + GlobalEqpType.DoNotHideVieraHats => + "Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8, + _ => "\0"u8, + }; +} diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index ed184d52..f22de809 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -21,13 +21,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public enum Type : byte { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, + GlobalEqp = 7, } [FieldOffset(0)] @@ -54,6 +55,10 @@ public readonly struct MetaManipulation : IEquatable, ICompara [JsonIgnore] public readonly ImcManipulation Imc = default; + [FieldOffset(0)] + [JsonIgnore] + public readonly GlobalEqpManipulation GlobalEqp = default; + [FieldOffset(15)] [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("Type")] @@ -63,14 +68,15 @@ public readonly struct MetaManipulation : IEquatable, ICompara { get => ManipulationType switch { - Type.Unknown => null, - Type.Imc => Imc, - Type.Eqdp => Eqdp, - Type.Eqp => Eqp, - Type.Est => Est, - Type.Gmp => Gmp, - Type.Rsp => Rsp, - _ => null, + Type.Unknown => null, + Type.Imc => Imc, + Type.Eqdp => Eqdp, + Type.Eqp => Eqp, + Type.Est => Est, + Type.Gmp => Gmp, + Type.Rsp => Rsp, + Type.GlobalEqp => GlobalEqp, + _ => null, }; init { @@ -100,6 +106,10 @@ public readonly struct MetaManipulation : IEquatable, ICompara Imc = m; ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; return; + case GlobalEqpManipulation m: + GlobalEqp = m; + ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown; + return; } } } @@ -108,13 +118,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara { return ManipulationType switch { - Type.Imc => Imc.Validate(true), - Type.Eqdp => Eqdp.Validate(), - Type.Eqp => Eqp.Validate(), - Type.Est => Est.Validate(), - Type.Gmp => Gmp.Validate(), - Type.Rsp => Rsp.Validate(), - _ => false, + Type.Imc => Imc.Validate(true), + Type.Eqdp => Eqdp.Validate(), + Type.Eqp => Eqp.Validate(), + Type.Est => Est.Validate(), + Type.Gmp => Gmp.Validate(), + Type.Rsp => Rsp.Validate(), + Type.GlobalEqp => GlobalEqp.Validate(), + _ => false, }; } @@ -154,6 +165,12 @@ public readonly struct MetaManipulation : IEquatable, ICompara ManipulationType = Type.Imc; } + public MetaManipulation(GlobalEqpManipulation eqp) + { + GlobalEqp = eqp; + ManipulationType = Type.GlobalEqp; + } + public static implicit operator MetaManipulation(EqpManipulation eqp) => new(eqp); @@ -172,6 +189,9 @@ public readonly struct MetaManipulation : IEquatable, ICompara public static implicit operator MetaManipulation(ImcManipulation imc) => new(imc); + public static implicit operator MetaManipulation(GlobalEqpManipulation eqp) + => new(eqp); + public bool EntryEquals(MetaManipulation other) { if (ManipulationType != other.ManipulationType) @@ -179,13 +199,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), - Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), - Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), - Type.Est => Est.Entry.Equals(other.Est.Entry), - Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), - Type.Imc => Imc.Entry.Equals(other.Imc.Entry), - _ => throw new ArgumentOutOfRangeException(), + Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), + Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), + Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), + Type.Est => Est.Entry.Equals(other.Est.Entry), + Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), + Type.Imc => Imc.Entry.Equals(other.Imc.Entry), + Type.GlobalEqp => true, + _ => throw new ArgumentOutOfRangeException(), }; } @@ -196,13 +217,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Equals(other.Eqp), - Type.Gmp => Gmp.Equals(other.Gmp), - Type.Eqdp => Eqdp.Equals(other.Eqdp), - Type.Est => Est.Equals(other.Est), - Type.Rsp => Rsp.Equals(other.Rsp), - Type.Imc => Imc.Equals(other.Imc), - _ => false, + Type.Eqp => Eqp.Equals(other.Eqp), + Type.Gmp => Gmp.Equals(other.Gmp), + Type.Eqdp => Eqdp.Equals(other.Eqdp), + Type.Est => Est.Equals(other.Est), + Type.Rsp => Rsp.Equals(other.Rsp), + Type.Imc => Imc.Equals(other.Imc), + Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp), + _ => false, }; } @@ -213,13 +235,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Copy(other.Eqp.Entry), - Type.Gmp => Gmp.Copy(other.Gmp.Entry), - Type.Eqdp => Eqdp.Copy(other.Eqdp), - Type.Est => Est.Copy(other.Est.Entry), - Type.Rsp => Rsp.Copy(other.Rsp.Entry), - Type.Imc => Imc.Copy(other.Imc.Entry), - _ => throw new ArgumentOutOfRangeException(), + Type.Eqp => Eqp.Copy(other.Eqp.Entry), + Type.Gmp => Gmp.Copy(other.Gmp.Entry), + Type.Eqdp => Eqdp.Copy(other.Eqdp), + Type.Est => Est.Copy(other.Est.Entry), + Type.Rsp => Rsp.Copy(other.Rsp.Entry), + Type.Imc => Imc.Copy(other.Imc.Entry), + Type.GlobalEqp => GlobalEqp, + _ => throw new ArgumentOutOfRangeException(), }; } @@ -229,13 +252,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public override int GetHashCode() => ManipulationType switch { - Type.Eqp => Eqp.GetHashCode(), - Type.Gmp => Gmp.GetHashCode(), - Type.Eqdp => Eqdp.GetHashCode(), - Type.Est => Est.GetHashCode(), - Type.Rsp => Rsp.GetHashCode(), - Type.Imc => Imc.GetHashCode(), - _ => 0, + Type.Eqp => Eqp.GetHashCode(), + Type.Gmp => Gmp.GetHashCode(), + Type.Eqdp => Eqdp.GetHashCode(), + Type.Est => Est.GetHashCode(), + Type.Rsp => Rsp.GetHashCode(), + Type.Imc => Imc.GetHashCode(), + Type.GlobalEqp => GlobalEqp.GetHashCode(), + _ => 0, }; public unsafe int CompareTo(MetaManipulation other) @@ -249,13 +273,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public override string ToString() => ManipulationType switch { - Type.Eqp => Eqp.ToString(), - Type.Gmp => Gmp.ToString(), - Type.Eqdp => Eqdp.ToString(), - Type.Est => Est.ToString(), - Type.Rsp => Rsp.ToString(), - Type.Imc => Imc.ToString(), - _ => "Invalid", + Type.Eqp => Eqp.ToString(), + Type.Gmp => Gmp.ToString(), + Type.Eqdp => Eqdp.ToString(), + Type.Est => Est.ToString(), + Type.Rsp => Rsp.ToString(), + Type.Imc => Imc.ToString(), + Type.GlobalEqp => GlobalEqp.ToString(), + _ => "Invalid", }; public string EntryToString() @@ -263,14 +288,15 @@ public readonly struct MetaManipulation : IEquatable, ICompara { Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", - Type.Est => $"{Est.Entry}", - Type.Gmp => $"{Gmp.Entry.Value}", - Type.Rsp => $"{Rsp.Entry}", - _ => string.Empty, - }; - + Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", + Type.Eqp => $"{(ulong)Eqp.Entry:X}", + Type.Est => $"{Est.Entry}", + Type.Gmp => $"{Gmp.Entry.Value}", + Type.Rsp => $"{Rsp.Entry}", + Type.GlobalEqp => string.Empty, + _ => string.Empty, + }; + public static bool operator ==(MetaManipulation left, MetaManipulation right) => left.Equals(right); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 2f7fd04c..829161f5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -6,19 +6,21 @@ namespace Penumbra.Mods.Editor; public class ModMetaEditor(ModManager modManager) { - private readonly HashSet _imc = []; - private readonly HashSet _eqp = []; - private readonly HashSet _eqdp = []; - private readonly HashSet _gmp = []; - private readonly HashSet _est = []; - private readonly HashSet _rsp = []; + private readonly HashSet _imc = []; + private readonly HashSet _eqp = []; + private readonly HashSet _eqdp = []; + private readonly HashSet _gmp = []; + private readonly HashSet _est = []; + private readonly HashSet _rsp = []; + private readonly HashSet _globalEqp = []; - public int OtherImcCount { get; private set; } - public int OtherEqpCount { get; private set; } - public int OtherEqdpCount { get; private set; } - public int OtherGmpCount { get; private set; } - public int OtherEstCount { get; private set; } - public int OtherRspCount { get; private set; } + public int OtherImcCount { get; private set; } + public int OtherEqpCount { get; private set; } + public int OtherEqdpCount { get; private set; } + public int OtherGmpCount { get; private set; } + public int OtherEstCount { get; private set; } + public int OtherRspCount { get; private set; } + public int OtherGlobalEqpCount { get; private set; } public bool Changes { get; private set; } @@ -40,17 +42,21 @@ public class ModMetaEditor(ModManager modManager) public IReadOnlySet Rsp => _rsp; + public IReadOnlySet GlobalEqp + => _globalEqp; + public bool CanAdd(MetaManipulation m) { return m.ManipulationType switch { - MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), - MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), - MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), - MetaManipulation.Type.Est => !_est.Contains(m.Est), - MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), - MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), + MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), + MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), + MetaManipulation.Type.Est => !_est.Contains(m.Est), + MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), + MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), + MetaManipulation.Type.GlobalEqp => !_globalEqp.Contains(m.GlobalEqp), + _ => false, }; } @@ -58,13 +64,14 @@ public class ModMetaEditor(ModManager modManager) { var added = m.ManipulationType switch { - MetaManipulation.Type.Imc => _imc.Add(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), - MetaManipulation.Type.Est => _est.Add(m.Est), - MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => _imc.Add(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), + MetaManipulation.Type.Est => _est.Add(m.Est), + MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), + MetaManipulation.Type.GlobalEqp => _globalEqp.Add(m.GlobalEqp), + _ => false, }; Changes |= added; return added; @@ -74,13 +81,14 @@ public class ModMetaEditor(ModManager modManager) { var deleted = m.ManipulationType switch { - MetaManipulation.Type.Imc => _imc.Remove(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), - MetaManipulation.Type.Est => _est.Remove(m.Est), - MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => _imc.Remove(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), + MetaManipulation.Type.Est => _est.Remove(m.Est), + MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), + MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(m.GlobalEqp), + _ => false, }; Changes |= deleted; return deleted; @@ -100,17 +108,19 @@ public class ModMetaEditor(ModManager modManager) _gmp.Clear(); _est.Clear(); _rsp.Clear(); + _globalEqp.Clear(); Changes = true; } public void Load(Mod mod, IModDataContainer currentOption) { - OtherImcCount = 0; - OtherEqpCount = 0; - OtherEqdpCount = 0; - OtherGmpCount = 0; - OtherEstCount = 0; - OtherRspCount = 0; + OtherImcCount = 0; + OtherEqpCount = 0; + OtherEqdpCount = 0; + OtherGmpCount = 0; + OtherEstCount = 0; + OtherRspCount = 0; + OtherGlobalEqpCount = 0; foreach (var option in mod.AllDataContainers) { if (option == currentOption) @@ -138,6 +148,9 @@ public class ModMetaEditor(ModManager modManager) case MetaManipulation.Type.Rsp: ++OtherRspCount; break; + case MetaManipulation.Type.GlobalEqp: + ++OtherGlobalEqpCount; + break; } } } @@ -179,6 +192,9 @@ public class ModMetaEditor(ModManager modManager) case MetaManipulation.Type.Rsp: _rsp.Add(manip.Rsp); break; + case MetaManipulation.Type.GlobalEqp: + _globalEqp.Add(manip.GlobalEqp); + break; } } @@ -191,5 +207,6 @@ public class ModMetaEditor(ModManager modManager) .Concat(_eqp.Select(m => (MetaManipulation)m)) .Concat(_est.Select(m => (MetaManipulation)m)) .Concat(_gmp.Select(m => (MetaManipulation)m)) - .Concat(_rsp.Select(m => (MetaManipulation)m)); + .Concat(_rsp.Select(m => (MetaManipulation)m)) + .Concat(_globalEqp.Select(m => (MetaManipulation)m)); } diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index a6ab491f..7f7ef4a6 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -5,17 +5,16 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; - public interface IModDataContainer { - public IMod Mod { get; } + public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } - public string GetName(); - public string GetFullName(); + public string GetName(); + public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 68933c9e..7cf75c03 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -75,6 +76,8 @@ public partial class ModEditWindow _editor.MetaEditor.OtherGmpCount); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, _editor.MetaEditor.OtherRspCount); + DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, + GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); } @@ -702,6 +705,69 @@ public partial class ModEditWindow } } + private static class GlobalEqpRow + { + private static GlobalEqpManipulation _new = new() + { + Type = GlobalEqpType.DoNotHideEarrings, + Condition = 1, + }; + + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize, + editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName())) + { + if (combo) + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) + _new = new GlobalEqpManipulation + { + Type = type, + Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, + }; + ImUtf8.HoverTooltip(type.ToDescription()); + } + } + + ImUtf8.HoverTooltip(_new.Type.ToDescription()); + + ImGui.TableNextColumn(); + if (!_new.Type.HasCondition()) + return; + + if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) + _new = _new with { Condition = newId }; + } + + public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImUtf8.Text(meta.Type.ToName()); + ImUtf8.HoverTooltip(meta.Type.ToDescription()); + ImGui.TableNextColumn(); + if (meta.Type.HasCondition()) + { + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImUtf8.Text($"{meta.Condition.Id}"); + } + } + } + // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 392a5aba..7368c7c8 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -76,6 +76,26 @@ public static class IdentifierExtensions case MetaManipulation.Type.Rsp: changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); break; + case MetaManipulation.Type.GlobalEqp: + var path = manip.GlobalEqp.Type switch + { + GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.LFinger), + GlobalEqpType.DoNotHideHrothgarHats => string.Empty, + GlobalEqpType.DoNotHideVieraHats => string.Empty, + _ => string.Empty, + }; + if (path.Length > 0) + identifier.Identify(changedItems, path); + break; } } From cd133bddbba7241138829ce7f7f7a45a8e50b98f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 14:50:22 +0200 Subject: [PATCH 1688/2451] Update Changelog. --- Penumbra/UI/Changelog.cs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index add16f94..2b2cfa99 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -55,26 +55,46 @@ public class PenumbraChangelog private static void Add1_0_3_0(Changelog log) => log.NextVersion("Version 1.0.3.0") + .RegisterImportant( + "This update comes, again, with a lot of very heavy backend changes (collections and groups) and thus may introduce new issues.") .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) - .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) - .RegisterHighlight("A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") - .RegisterEntry("Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", 1) - .RegisterEntry("This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", 1) - .RegisterHighlight("Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") + .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight( + "A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") + .RegisterEntry( + "Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", + 1) + .RegisterEntry( + "This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", + 1) + .RegisterHighlight("A new type of Meta Manipulation was added, 'Global EQP Manipulation'.") + .RegisterEntry( + "Global EQP Manipulations allow accessories to make other equipment pieces not hide them, e.g. whenever a character is wearing a specific Bracelet, neither body nor hand items will ever hide bracelets.", + 1) + .RegisterEntry( + "This can be used if something like a jacket or a stole is put onto an accessory to prevent it from being hidden in general.", + 1) + .RegisterHighlight( + "Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) - .RegisterEntry("You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") - .RegisterHighlight("Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") + .RegisterEntry( + "You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") + .RegisterHighlight( + "Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") .RegisterEntry("Added support for reading and writing the new material and model file formats from the benchmark.") - .RegisterEntry("Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") + .RegisterEntry( + "Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") - .RegisterEntry("Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") + .RegisterEntry( + "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") .RegisterEntry("Reworked the API and IPC structure heavily.") .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") .RegisterEntry("Fixed an issue with merging and deduplicating mods.") .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") - .RegisterEntry("Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); + .RegisterEntry( + "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); private static void Add1_0_2_0(Changelog log) => log.NextVersion("Version 1.0.2.0") From 1d230050c214df15aeb83e7f14b110a3e836a4eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 15:41:27 +0200 Subject: [PATCH 1689/2451] Fix typo and rename geqp options. --- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 14 +++++++------- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index 57d99d56..d57af1d9 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -30,13 +30,13 @@ public static class GlobalEqpExtensions public static ReadOnlySpan ToName(this GlobalEqpType type) => type switch { - GlobalEqpType.DoNotHideEarrings => "Do Not Hide Earrings"u8, - GlobalEqpType.DoNotHideNecklace => "Do Not Hide Necklaces"u8, - GlobalEqpType.DoNotHideBracelets => "Do Not Hide Bracelets"u8, - GlobalEqpType.DoNotHideRingR => "Do Not Hide Rings (Right Finger)"u8, - GlobalEqpType.DoNotHideRingL => "Do Not Hide Rings (Left Finger)"u8, - GlobalEqpType.DoNotHideHrothgarHats => "Do Not Hide Hats for Hrothgar"u8, - GlobalEqpType.DoNotHideVieraHats => "Do Not Hide Hats for Viera"u8, + GlobalEqpType.DoNotHideEarrings => "Always Show Earrings"u8, + GlobalEqpType.DoNotHideNecklace => "Always Show Necklaces"u8, + GlobalEqpType.DoNotHideBracelets => "Always Show Bracelets"u8, + GlobalEqpType.DoNotHideRingR => "Always Show Rings (Right Finger)"u8, + GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8, + GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8, + GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8, _ => "\0"u8, }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 7cf75c03..6f542377 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -76,7 +76,7 @@ public partial class ModEditWindow _editor.MetaEditor.OtherGmpCount); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, _editor.MetaEditor.OtherRspCount); - DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, + DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); } From ca777ba1bf6369f7fd6202f40620d882d56b4e86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 May 2024 17:43:13 +0200 Subject: [PATCH 1690/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07cc26f1..b8282970 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07cc26f196984a44711b3bc4c412947d863288bd +Subproject commit b8282970ee78a2c085e740f60450fecf7ea58b9c From fe266dca314f873422ec3f87a4769cbdc7dd520a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 27 May 2024 15:45:29 +0000 Subject: [PATCH 1691/2451] [CI] Updating repo.json for testing_1.0.3.0 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 232afaa0..2327420d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.6", + "TestingAssemblyVersion": "1.0.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From f11cefcec1e21d416d25f7cbb1a6f31d8a19705f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 00:11:31 +0200 Subject: [PATCH 1692/2451] Fix best match fullpath returning broken FullPath instead of nullopt. --- Penumbra/Mods/Groups/MultiModGroup.cs | 11 ++++++++--- Penumbra/Mods/Groups/SingleModGroup.cs | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 38c0ef15..7816d628 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -40,9 +41,13 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => OptionData.Count > 0; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => OptionData.OrderByDescending(o => o.Priority) - .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) - .FirstOrDefault(); + { + foreach (var path in OptionData.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } public IModOption? AddOption(string name, string description = "") { diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 49190e34..a6ebd846 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -30,9 +30,13 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public readonly List OptionData = []; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => OptionData - .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) - .FirstOrDefault(); + { + foreach (var path in OptionData + .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } public IModOption AddOption(string name, string description = "") { From b30de460e729c4d0eb6ccdfcab298738f10ef54a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 00:11:54 +0200 Subject: [PATCH 1693/2451] Fix ColorTable preview with legacy color tables. --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 4 ++-- .../LiveColorTablePreviewer.cs | 2 +- .../ModEditWindow.Materials.ColorTable.cs | 13 ++++++------ .../ModEditWindow.Materials.MtrlTab.cs | 21 +++++++++++-------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b8282970..b83ce830 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b8282970ee78a2c085e740f60450fecf7ea58b9c +Subproject commit b83ce830919ca56e8b066d48d588c889df3af39b diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 73a5e725..5df9e1c1 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = material.Mtrl.Table; + var table = new LegacyColorTable(material.Mtrl.Table); var normal = material.Textures[TextureUsage.SamplerNormal]; @@ -103,7 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - private readonly struct ProcessCharacterNormalOperation(Image normal, ColorTable table) : IRowOperation + private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index f211e0bc..8e75a895 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -8,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = GameData.Files.MaterialStructs.ColorTable.NumUsedRows; + public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly IFramework _framework; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 54c0eff6..15bd7cc9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -116,7 +116,7 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); tab.UpdateColorTablePreview(); @@ -170,6 +170,7 @@ public partial class ModEditWindow } } + [SkipLocalsInit] private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -178,11 +179,11 @@ public partial class ModEditWindow try { - var data = new byte[ColorTable.Row.Size + 2]; + Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); } var text = Convert.ToBase64String(data); @@ -218,7 +219,7 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + 2 + if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size || !tab.Mtrl.HasTable) return false; @@ -349,7 +350,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth(floatSize); - float glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") && FixFloat(ref tmpFloat, row.GlossStrength)) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9421493e..56e9482b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -455,7 +455,8 @@ public partial class ModEditWindow { UnbindFromMaterialInstances(); - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), FilePath); + var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); var foundMaterials = new HashSet(); foreach (var materialInfo in instances) @@ -596,13 +597,13 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var row = Mtrl.Table[rowIdx]; + var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; - var dye = Mtrl.DyeTable[rowIdx]; + var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes, default); + row.ApplyDyeTemplate(dye, dyes); } if (HighlightedColorTableRow == rowIdx) @@ -624,17 +625,18 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var rows = Mtrl.Table; + var rows = new LegacyColorTable(Mtrl.Table); + var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) { ref var row = ref rows[i]; - var dye = Mtrl.DyeTable[i]; + var dye = dyeRows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes, default); + row.ApplyDyeTemplate(dye, dyes); } } @@ -643,12 +645,13 @@ public partial class ModEditWindow foreach (var previewer in ColorTablePreviewers) { + // TODO: Dawntrail rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); } } - private static void ApplyHighlight(ref ColorTable.Row row, float time) + private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); From eb2a9b810922f409f8630e73a75c63353f1e709f Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 27 May 2024 22:13:55 +0000 Subject: [PATCH 1694/2451] [CI] Updating repo.json for testing_1.0.3.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2327420d..1cf9c104 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.3.0", + "TestingAssemblyVersion": "1.0.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 255d11974fcad8893e009766b9c8254be12ec9eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 11:20:17 +0200 Subject: [PATCH 1695/2451] Fix IMC Stupid. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 01bf112b..c95884c6 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -204,7 +204,7 @@ public class FileEditor( private void SaveButton() { - var canSave = _changed && _currentFile != null && _currentFile.Valid; + var canSave = _changed && _currentFile is { Valid: true }; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) { diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index c14652ac..694ae11c 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -26,6 +26,7 @@ public static class ImcManipulationDrawer }; identifier = identifier with { + ObjectType = type, EquipSlot = equipSlot, SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, }; From 5d1b17f96d159276e3470e462b56fbd2b101dd6e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:51:12 +0200 Subject: [PATCH 1696/2451] Fix mdl imports not being savable. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/Import/ModelImporter.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b83ce830..f2cea65b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b83ce830919ca56e8b066d48d588c889df3af39b +Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5 diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index eedd12ab..a141d754 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -104,6 +104,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) Radius = 1, BoneBoundingBoxes = Enumerable.Repeat(MdlFile.EmptyBoundingBox, _bones.Count).ToArray(), RemainingData = [.._vertexBuffer, ..indexBuffer], + Valid = true, }; } From f5d6ac8bdbd43edf11f5d7ed41d7f587c49590d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:51:28 +0200 Subject: [PATCH 1697/2451] Fix Remove Assignment being visible for base and interface. --- Penumbra/Collections/Manager/CollectionType.cs | 3 +++ Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index 8c51fd90..c25413b8 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -107,6 +107,9 @@ public static class CollectionTypeExtensions public static bool IsSpecial(this CollectionType collectionType) => collectionType < CollectionType.Default; + public static bool CanBeRemoved(this CollectionType collectionType) + => collectionType.IsSpecial() || collectionType is CollectionType.Individual; + public static readonly (CollectionType, string, string)[] Special = Enum.GetValues() .Where(IsSpecial) .Select(s => (s, s.ToName(), s.ToDescription())) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8625335e..bb22e6a7 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -287,7 +287,7 @@ public sealed class CollectionPanel : IDisposable _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); } - if (collection != null) + if (collection != null && type.CanBeRemoved()) { using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); if (ImGui.MenuItem("Remove this assignment.")) From 8891ea057086094e56220b60989741d7fc36dfc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:53:02 +0200 Subject: [PATCH 1698/2451] Fix imc identifiers setting equip slot to something where they should not. --- Penumbra/Meta/Manipulations/ImcManipulation.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 945aab04..eb3720c9 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -64,10 +64,8 @@ public readonly struct ImcManipulation : IMetaManipulation { ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, - equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, - bodySlot), + ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown), }; } From 09742e2e50d24acfe62e4876451f9be556b457fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 28 May 2024 10:56:23 +0000 Subject: [PATCH 1699/2451] [CI] Updating repo.json for testing_1.0.3.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 1cf9c104..a996e2d0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.3.1", + "TestingAssemblyVersion": "1.0.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b2e1bff782f5aff12766ecbe82bfa743242e0705 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 May 2024 17:18:39 +0200 Subject: [PATCH 1700/2451] Consolidate path-data encoding into a single file and make it neater. --- Penumbra/Collections/Cache/ImcCache.cs | 16 +- .../Collections/Manager/CollectionStorage.cs | 64 ++++++- .../Manager/TempCollectionManager.cs | 3 +- Penumbra/Collections/ModCollection.cs | 27 +-- .../Interop/PathResolving/PathDataHandler.cs | 162 ++++++++++++++++++ .../Interop/PathResolving/PathResolver.cs | 52 +++--- .../Interop/PathResolving/SubfileHelper.cs | 19 +- .../ResourceLoading/CreateFileWHook.cs | 3 +- .../Interop/ResourceLoading/ResourceLoader.cs | 24 ++- .../Interop/ResourceTree/ResolveContext.cs | 11 +- Penumbra/Mods/TemporaryMod.cs | 7 +- Penumbra/Services/ConfigMigrationService.cs | 3 +- Penumbra/Services/CrashHandlerService.cs | 4 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- 14 files changed, 302 insertions(+), 95 deletions(-) create mode 100644 Penumbra/Interop/PathResolving/PathDataHandler.cs diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 843fe195..33b366d3 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,3 +1,4 @@ +using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -7,8 +8,8 @@ namespace Penumbra.Collections.Cache; public readonly struct ImcCache : IDisposable { - private readonly Dictionary _imcFiles = new(); - private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new(); + private readonly Dictionary _imcFiles = []; + private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = []; public ImcCache() { } @@ -17,10 +18,10 @@ public readonly struct ImcCache : IDisposable { if (fromFullCompute) foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFileSync(path, CreateImcPath(collection, path)); + collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection)); else foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFile(path, CreateImcPath(collection, path)); + collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection)); } public void Reset(ModCollection collection) @@ -57,7 +58,7 @@ public readonly struct ImcCache : IDisposable return false; _imcFiles[path] = file; - var fullPath = CreateImcPath(collection, path); + var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); collection._cache!.ForceFile(path, fullPath); return true; @@ -100,7 +101,7 @@ public readonly struct ImcCache : IDisposable if (!manip.Apply(file)) return false; - var fullPath = CreateImcPath(collection, file.Path); + var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); collection._cache!.ForceFile(file.Path, fullPath); return true; @@ -115,9 +116,6 @@ public readonly struct ImcCache : IDisposable _imcManipulations.Clear(); } - private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path) - => new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}"); - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index de5d0a14..39068e87 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -10,23 +11,71 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; +using Penumbra.UI.CollectionTab; namespace Penumbra.Collections.Manager; +/// A contiguously incrementing ID managed by the CollectionCreator. +public readonly record struct LocalCollectionId(int Id) : IAdditionOperators +{ + public static readonly LocalCollectionId Zero = new(0); + + public static LocalCollectionId operator +(LocalCollectionId left, int right) + => new(left.Id + right); +} + public class CollectionStorage : IReadOnlyList, IDisposable { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; private readonly ModStorage _modStorage; + public ModCollection Create(string name, int index, ModCollection? duplicate) + { + var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) + ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, + IReadOnlyList inheritances) + { + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings, + inheritances); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) + { + var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public void Delete(ModCollection collection) + => _collectionsByLocal.Remove(collection.LocalId); + /// The empty collection is always available at Index 0. private readonly List _collections = [ ModCollection.Empty, ]; + /// A list of all collections ever created still existing by their local id. + private readonly Dictionary + _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; + + public readonly ModCollection DefaultNamed; + /// Incremented by 1 because the empty collection gets Zero. + public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; + /// Default enumeration skips the empty collection. public IEnumerator GetEnumerator() => _collections.Skip(1).GetEnumerator(); @@ -69,6 +118,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable return ByName(identifier, out collection); } + /// Find a collection by its local ID if it still exists, otherwise returns the empty collection. + public ModCollection ByLocalId(LocalCollectionId localId) + => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty; + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) { _communicator = communicator; @@ -100,10 +153,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { - var newCollection = duplicate?.Duplicate(name, _collections.Count) - ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); - _collections.Add(newCollection); - + var newCollection = Create(name, _collections.Count, duplicate); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); @@ -132,6 +182,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable // Update indices. for (var i = collection.Index; i < Count; ++i) _collections[i].Index = i; + _collectionsByLocal.Remove(collection.LocalId); Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); @@ -180,7 +231,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable continue; } - var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance); + var collection = CreateFromData(id, name, version, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) try @@ -293,7 +344,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable } /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int movedToIdx) { type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index de08c6a2..ce438a6b 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -52,7 +52,7 @@ public class TempCollectionManager : IDisposable { if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; - var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); + var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++); Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); if (_customCollections.TryAdd(collection.Id, collection)) { @@ -72,6 +72,7 @@ public class TempCollectionManager : IDisposable return false; } + _storage.Delete(collection); Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 4580e37a..9286d459 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -25,13 +25,15 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []); /// The name of a collection. public string Name { get; set; } public Guid Id { get; } + public LocalCollectionId LocalId { get; } + public string Identifier => Id.ToString(); @@ -117,19 +119,20 @@ public partial class ModCollection /// /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. /// - public ModCollection Duplicate(string name, int index) + public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), + return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version, + int index, Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings) + var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -139,18 +142,19 @@ public partial class ModCollection } /// Constructor for temporary collections. - public static ModCollection CreateTemporary(string name, int index, int changeCounter) + public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []); return ret; } /// Constructor for empty collections. - public static ModCollection CreateEmpty(string name, int index, int modCount) + public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, + Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } @@ -199,11 +203,12 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(Guid id, string name, int index, int changeCounter, int version, List appliedSettings, - List inheritsFrom, Dictionary settings) + private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version, + List appliedSettings, List inheritsFrom, Dictionary settings) { Name = name; Id = id; + LocalId = localId; Index = index; ChangeCounter = changeCounter; Settings = appliedSettings; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs new file mode 100644 index 00000000..5627e015 --- /dev/null +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -0,0 +1,162 @@ +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.PathResolving; + +public static class PathDataHandler +{ + public static readonly ushort Discriminator = (ushort)(Environment.TickCount >> 12); + private static readonly string DiscriminatorString = $"{Discriminator:X4}"; + private const int MinimumLength = 8; + + /// Additional Data encoded in a path. + /// The local ID of the collection. + /// The change counter of that collection when this file was loaded. + /// The CRC32 of the originally requested path, only used for materials. + /// A discriminator to differ between multiple loads of Penumbra. + public readonly record struct AdditionalPathData( + LocalCollectionId Collection, + int ChangeCounter, + int OriginalPathCrc32, + ushort Discriminator) + { + public static readonly AdditionalPathData Invalid = new(LocalCollectionId.Zero, 0, 0, PathDataHandler.Discriminator); + + /// Any collection but the empty collection can appear. In particular, they can be negative for temporary collections. + public bool Valid + => Collection.Id != 0; + } + + /// Create the encoding path for an IMC file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateImc(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for a TMB file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateTmb(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for an AVFX file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateAvfx(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for a MTRL file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateMtrl(ByteString path, ModCollection collection, Utf8GamePath originalPath) + => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + + /// The base function shared by most file types. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static FullPath CreateBase(ByteString path, ModCollection collection) + => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); + + /// Read an additional data blurb and parse it into usable data for all file types but Materials. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Read(ReadOnlySpan additionalData, out AdditionalPathData data) + => ReadBase(additionalData, out data, out _); + + /// Read an additional data blurb and parse it into usable data for Materials. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadMtrl(ReadOnlySpan additionalData, out AdditionalPathData data) + { + if (!ReadBase(additionalData, out data, out var remaining)) + return false; + + if (!int.TryParse(remaining, out var crc32)) + return false; + + data = data with { OriginalPathCrc32 = crc32 }; + return true; + } + + /// Parse the common attributes of an additional data blurb and return remaining data if there is any. + private static bool ReadBase(ReadOnlySpan additionalData, out AdditionalPathData data, out ReadOnlySpan remainingData) + { + data = AdditionalPathData.Invalid; + remainingData = []; + + // At least (\d_\d_\x\x\x\x) + if (additionalData.Length < MinimumLength) + return false; + + // Fetch discriminator, constant length. + var discriminatorSpan = additionalData[^4..]; + if (!ushort.TryParse(discriminatorSpan, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var discriminator)) + return false; + + additionalData = additionalData[..^5]; + var collectionSplit = additionalData.IndexOf((byte)'_'); + if (collectionSplit == -1) + return false; + + var collectionSpan = additionalData[..collectionSplit]; + additionalData = additionalData[(collectionSplit + 1)..]; + + if (!int.TryParse(collectionSpan, out var id)) + return false; + + var changeCounterSpan = additionalData; + var changeCounterSplit = additionalData.IndexOf((byte)'_'); + if (changeCounterSplit != -1) + { + changeCounterSpan = additionalData[..changeCounterSplit]; + remainingData = additionalData[(changeCounterSplit + 1)..]; + } + + if (!int.TryParse(changeCounterSpan, out var changeCounter)) + return false; + + data = new AdditionalPathData(new LocalCollectionId(id), changeCounter, 0, discriminator); + return true; + } + + /// Split a given span into the actual path and the additional data blurb. Returns true if a blurb exists. + public static bool Split(ReadOnlySpan text, out ReadOnlySpan path, out ReadOnlySpan data) + { + if (text.IsEmpty || text[0] is not (byte)'|') + { + path = text; + data = []; + return false; + } + + var endIdx = text[1..].IndexOf((byte)'|'); + if (endIdx++ < 0) + { + path = text; + data = []; + return false; + } + + data = text.Slice(1, endIdx - 1); + path = ++endIdx == text.Length ? [] : text[endIdx..]; + return true; + } + + /// + public static bool Split(ReadOnlySpan text, out ReadOnlySpan path, out ReadOnlySpan data) + { + if (text.Length == 0 || text[0] is not '|') + { + path = text; + data = []; + return false; + } + + var endIdx = text[1..].IndexOf('|'); + if (endIdx++ < 0) + { + path = text; + data = []; + return false; + } + + data = text.Slice(1, endIdx - 1); + path = ++endIdx >= text.Length ? [] : text[endIdx..]; + return true; + } +} diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 5c3d8d19..e5c75327 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -13,11 +13,10 @@ namespace Penumbra.Interop.PathResolving; public class PathResolver : IDisposable { - private readonly PerformanceTracker _performance; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TempCollectionManager _tempCollections; - private readonly ResourceLoader _loader; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly ResourceLoader _loader; private readonly SubfileHelper _subfileHelper; private readonly PathState _pathState; @@ -25,14 +24,12 @@ public class PathResolver : IDisposable private readonly GameState _gameState; private readonly CollectionResolver _collectionResolver; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper, - PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) + public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, + SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { _performance = performance; _config = config; _collectionManager = collectionManager; - _tempCollections = tempCollections; _subfileHelper = subfileHelper; _pathState = pathState; _metaState = metaState; @@ -43,9 +40,12 @@ public class PathResolver : IDisposable _loader.FileLoaded += ImcLoadResource; } - /// Obtain a temporary or permanent collection by name. - public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection); + /// Obtain a temporary or permanent collection by local ID. + public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collection) + { + collection = _collectionManager.Storage.ByLocalId(id); + return collection != ModCollection.Empty; + } /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -113,7 +113,7 @@ public class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, gamePath, out var pair); return pair; } @@ -131,23 +131,21 @@ public class PathResolver : IDisposable } /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData) + private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) { - if (resource->FileType != ResourceType.Imc) + if (resource->FileType != ResourceType.Imc + || !PathDataHandler.Read(additionalData, out var data) + || data.Discriminator != PathDataHandler.Discriminator + || !Utf8GamePath.FromByteString(path, out var gamePath) + || !CollectionByLocalId(data.Collection, out var collection) + || !collection.HasCache + || !collection.GetImcFile(gamePath, out var file)) return; - var lastUnderscore = additionalData.LastIndexOf((byte)'_'); - var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore); - if (Utf8GamePath.FromByteString(path, out var gamePath) - && GuidExtensions.FromOptimizedString(idString.Span, out var id) - && CollectionById(id, out var collection) - && collection.HasCache - && collection.GetImcFile(gamePath, out var file)) - { - file.Replace(resource); - Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } + file.Replace(resource); + Penumbra.Log.Verbose( + $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); } /// Resolve a path from the interface collection. diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 844baaa9..793ea20b 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -66,21 +66,18 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection Materials, TMB, and AVFX need to be set per collection so they can load their sub files independently from each other. + /// Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, ResolveData) data) + Utf8GamePath originalPath, out (FullPath?, ResolveData) data) { if (nonDefault) - switch (type) + resolved = type switch { - case ResourceType.Mtrl: - case ResourceType.Avfx: - case ResourceType.Tmb: - var fullPath = new FullPath($"|{resolveData.ModCollection.Id.OptimizedString()}_{resolveData.ModCollection.ChangeCounter}|{path}"); - data = (fullPath, resolveData); - return; - } - + ResourceType.Mtrl => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), + ResourceType.Avfx => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), + ResourceType.Tmb => PathDataHandler.CreateTmb(path, resolveData.ModCollection), + _ => resolved, + }; data = (resolved, resolveData); } diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index 8a5e779b..bde640d2 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; +using OtterGui.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -11,7 +12,7 @@ namespace Penumbra.Interop.ResourceLoading; /// we use the fixed size buffers of their formats to only store pointers to the actual path instead. /// Then we translate the stored pointer to the path in CreateFileW, if the prefix matches. /// -public unsafe class CreateFileWHook : IDisposable +public unsafe class CreateFileWHook : IDisposable, IRequiredService { public const int Size = 28; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 6c2c83b3..7b49beab 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; @@ -17,8 +18,7 @@ public unsafe class ResourceLoader : IDisposable private ResolveData _resolvedData = ResolveData.Invalid; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, - CreateFileWHook _) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { _resources = resources; _fileReadService = fileReadService; @@ -54,7 +54,7 @@ public unsafe class ResourceLoader : IDisposable /// Reset the ResolvePath function to always return null. public void ResetResolvePath() - => ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid); + => ResolvePath = (_, _, _) => (null, ResolveData.Invalid); public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData); @@ -67,7 +67,7 @@ public unsafe class ResourceLoader : IDisposable public event ResourceLoadedDelegate? ResourceLoaded; public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ByteString additionalData); + ReadOnlySpan additionalData); /// /// Event fired whenever a resource is newly loaded. @@ -132,19 +132,17 @@ public unsafe class ResourceLoader : IDisposable // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - if (gamePath.Path[0] != (byte)'|') + if (!PathDataHandler.Split(gamePath.Path.Span, out var actualPath, out var data)) { - returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty); + returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, []); return; } - // Split the path into the special-treatment part (between the first and second '|') - // and the actual path. - var split = gamePath.Path.Split((byte)'|', 3, false); - fileDescriptor->ResourceHandle->FileNameData = split[2].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; + var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); + fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); - returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]); + returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; @@ -153,7 +151,7 @@ public unsafe class ResourceLoader : IDisposable /// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, - bool isSync, ByteString additionalData) + bool isSync, ReadOnlySpan additionalData) { if (Utf8GamePath.IsRooted(gamePath)) { diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 615ef2b0..7c8da41f 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; @@ -373,14 +374,8 @@ internal unsafe partial record ResolveContext( if (name.IsEmpty) return ByteString.Empty; - if (stripPrefix && name[0] == (byte)'|') - { - var pos = name.IndexOf((byte)'|', 1); - if (pos < 0) - return ByteString.Empty; - - name = name.Substring(pos + 1); - } + if (stripPrefix && PathDataHandler.Split(name.Span, out var path, out _)) + name = ByteString.FromSpanUnsafe(path, name.IsNullTerminated, name.IsAsciiLowerCase, name.IsAscii); return name; } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index d08c8b06..6e6e72ab 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -83,10 +84,8 @@ public class TemporaryMod : IMod } var targetPath = fullPath.Path.FullName; - if (fullPath.Path.Name.StartsWith('|')) - { - targetPath = targetPath.Split('|', 3, StringSplitOptions.RemoveEmptyEntries).Last(); - } + if (PathDataHandler.Split(fullPath.Path.FullName, out var actualPath, out _)) + targetPath = actualPath.ToString(); if (Path.IsPathRooted(targetPath)) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 1f6ac170..70b05a73 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -379,7 +379,8 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []); + // Only used for saving and immediately discarded, so the local collection id here is irrelevant. + var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, LocalCollectionId.Zero, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 6805e7db..1239578b 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -7,6 +7,7 @@ using Penumbra.Communication; using Penumbra.CrashHandler; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData.Actors; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; @@ -286,8 +287,7 @@ public sealed class CrashHandlerService : IDisposable, IService try { - var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1; - if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1))) + if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && Path.IsPathRooted(actualPath)) return; var name = GetActorName(resolveData.AssociatedGameObject); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d5ff1abd..65a8fe76 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -236,7 +236,7 @@ public sealed class ResourceWatcher : IDisposable, ITab _newRecords.Enqueue(record); } - private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) + private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( From a6661f15e87ad8b8b15d386943d08e20001c9848 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 30 May 2024 20:46:04 +0200 Subject: [PATCH 1701/2451] Display the additional path data in ResourceTree --- .../Interop/MaterialPreview/MaterialInfo.cs | 11 +++++--- .../ResolveContext.PathResolution.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 27 +++++++------------ Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 +++ .../UI/AdvancedWindow/ResourceTreeViewer.cs | 8 ++++-- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 61e7c764..f7e6caf0 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -3,8 +3,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using Penumbra.Interop.ResourceTree; +using Penumbra.Interop.PathResolving; using Penumbra.String; +using static Penumbra.Interop.Structs.StructExtensions; using Model = Penumbra.GameData.Interop.Model; namespace Penumbra.Interop.MaterialPreview; @@ -78,8 +79,12 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath(&mtrlHandle->ResourceHandle); - if (path == needle) + if (mtrlHandle == null) + continue; + + PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); + var fileName = ByteString.FromSpanUnsafe(path, true); + if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index d5b4fa39..236c7051 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -155,7 +155,7 @@ internal partial record ResolveContext var imcFileData = imc->GetDataSpan(); if (imcFileData.IsEmpty) { - Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span"); + Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span"); return variant.Id; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 7c8da41f..e38bf4f6 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -111,12 +111,18 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + var fileName = resourceHandle->FileName.AsSpan(); + var additionalData = ByteString.Empty; + if (PathDataHandler.Split(fileName, out fileName, out var data)) + additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); + + var fullPath = Utf8GamePath.FromSpan(fileName, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this) { - GamePath = gamePath, - FullPath = fullPath, + GamePath = gamePath, + FullPath = fullPath, + AdditionalData = additionalData, }; if (autoAdd) Global.Nodes.Add((gamePath, (nint)resourceHandle), node); @@ -365,21 +371,6 @@ internal unsafe partial record ResolveContext( return i >= 0 && i < array.Length ? array[i] : null; } - internal static ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true) - { - if (handle == null) - return ByteString.Empty; - - var name = handle->FileName.AsByteString(); - if (name.IsEmpty) - return ByteString.Empty; - - if (stripPrefix && PathDataHandler.Split(name.Span, out var path, out _)) - name = ByteString.FromSpanUnsafe(path, name.IsNullTerminated, name.IsAsciiLowerCase, name.IsAscii); - - return name; - } - private static ulong GetResourceHandleLength(ResourceHandle* handle) { if (handle == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 7ec75893..e74edb91 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,5 @@ using Penumbra.Api.Enums; +using Penumbra.String; using Penumbra.String.Classes; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; @@ -15,6 +16,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public ByteString AdditionalData; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -40,6 +42,7 @@ public class ResourceNode : ICloneable ObjectAddress = objectAddress; ResourceHandle = resourceHandle; PossibleGamePaths = Array.Empty(); + AdditionalData = ByteString.Empty; Length = length; Children = new List(); ResolveContext = resolveContext; @@ -56,6 +59,7 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + AdditionalData = other.AdditionalData; Length = other.Length; Children = other.Children; ResolveContext = other.ResolveContext; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index d31f3e52..3ada77df 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; +using Penumbra.String; namespace Penumbra.UI.AdvancedWindow; @@ -177,6 +178,9 @@ public class ResourceTreeViewer return NodeVisibility.Hidden; } + string GetAdditionalDataSuffix(ByteString data) + => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { var visibility = GetNodeVisibility(resourceNode); @@ -260,13 +264,13 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard."); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); - ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); + ImGuiUtil.HoverTooltip($"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); From f4bdbcac5338f6bb7deb5e021ca2e4d6236de739 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 30 May 2024 23:09:26 +0200 Subject: [PATCH 1702/2451] Make Resource Trees honor Incognito Mode --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 26 ++++++----- .../ResourceTree/ResourceTreeFactory.cs | 43 ++++++++++--------- Penumbra/Services/StaticServiceManager.cs | 2 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 +-- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 17 +++++--- .../ResourceTreeViewerFactory.cs | 13 ++++++ Penumbra/UI/ChangedItemDrawer.cs | 7 ++- Penumbra/UI/CollectionTab/CollectionPanel.cs | 11 +++-- .../UI/CollectionTab/CollectionSelector.cs | 8 ++-- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 +- Penumbra/UI/IncognitoService.cs | 29 +++++++++++++ Penumbra/UI/Tabs/CollectionsTab.cs | 25 ++++------- Penumbra/UI/Tabs/OnScreenTab.cs | 7 +-- 13 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs create mode 100644 Penumbra/UI/IncognitoService.cs diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index dac86e44..fc8c805a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -15,6 +15,7 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { public readonly string Name; + public readonly string AnonymizedName; public readonly int GameObjectIndex; public readonly nint GameObjectAddress; public readonly nint DrawObjectAddress; @@ -22,6 +23,7 @@ public class ResourceTree public readonly bool PlayerRelated; public readonly bool Networked; public readonly string CollectionName; + public readonly string AnonymizedCollectionName; public readonly List Nodes; public readonly HashSet FlatNodes; @@ -29,18 +31,20 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName) + public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) { - Name = name; - GameObjectIndex = gameObjectIndex; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - LocalPlayerRelated = localPlayerRelated; - Networked = networked; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Name = name; + AnonymizedName = anonymizedName; + GameObjectIndex = gameObjectIndex; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + LocalPlayerRelated = localPlayerRelated; + Networked = networked; + PlayerRelated = playerRelated; + CollectionName = collectionName; + AnonymizedCollectionName = anonymizedCollectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } public void ProcessPostfix(Action action) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index ae7187f0..a722e344 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -72,10 +72,10 @@ public class ResourceTreeFactory( return null; var localPlayerRelated = cache.IsLocalPlayerRelated(character); - var (name, related) = GetCharacterName(character, cache); + var (name, anonymizedName, related) = GetCharacterName(character); var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; - var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, - networked, collectionResolveData.ModCollection.Name); + var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, + networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) @@ -157,27 +157,30 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, - TreeBuildCache cache) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character) { var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); - switch (identifier.Type) - { - case IdentifierType.Player: return (identifier.PlayerName.ToString(), true); - case IdentifierType.Owned: - var ownerChara = objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character; - if (ownerChara != null) - { - var ownerName = GetCharacterName(ownerChara, cache); - return ($"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})", ownerName.PlayerRelated); - } - - break; - } - - return ($"{character.Name} ({identifier.Kind.ToName()})", false); + var identifierStr = identifier.ToString(); + return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } + private unsafe bool IsPlayerRelated(Dalamud.Game.ClientState.Objects.Types.Character? character) + { + if (character == null) + return false; + + var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + return IsPlayerRelated(identifier, owner); + } + + private bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) + => identifier.Type switch + { + IdentifierType.Player => true, + IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character), + _ => false, + }; + [Flags] public enum Flags { diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 0c6648ba..146d7ee0 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -149,6 +149,7 @@ public static class StaticServiceManager private static ServiceManager AddInterface(this ServiceManager services) => services.AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -181,6 +182,7 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(p => new Diagnostics(p)); private static ServiceManager AddModEditor(this ServiceManager services) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6b48a048..af01047b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -585,7 +585,8 @@ public partial class ModEditWindow : Window, IDisposable Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ChangedItemDrawer changedItemDrawer, ObjectManager objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) + ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, + CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { _performance = performance; @@ -618,8 +619,7 @@ public partial class ModEditWindow : Window, IDisposable _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = - new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3ada77df..c0c49e47 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -6,6 +6,7 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; using Penumbra.String; +using Penumbra.UI.Tabs; namespace Penumbra.UI.AdvancedWindow; @@ -17,6 +18,7 @@ public class ResourceTreeViewer private readonly Configuration _config; private readonly ResourceTreeFactory _treeFactory; private readonly ChangedItemDrawer _changedItemDrawer; + private readonly IncognitoService _incognito; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; @@ -29,11 +31,12 @@ public class ResourceTreeViewer private Task? _task; public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - int actionCapacity, Action onRefresh, Action drawActions) + IncognitoService incognito, int actionCapacity, Action onRefresh, Action drawActions) { _config = config; _treeFactory = treeFactory; _changedItemDrawer = changedItemDrawer; + _incognito = incognito; _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; @@ -75,7 +78,7 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); @@ -89,7 +92,7 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {tree.CollectionName}"); + ImGui.TextUnformatted($"Collection: {(_incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -137,10 +140,14 @@ public class ResourceTreeViewer ImGui.SameLine(0, checkPadding); - _changedItemDrawer.DrawTypeFilter(ref _typeFilter, -yOffset); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); + using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) + _changedItemDrawer.DrawTypeFilter(ref _typeFilter); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - checkSpacing - ImGui.GetFrameHeightWithSpacing()); ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + ImGui.SameLine(0, checkSpacing); + _incognito.DrawToggle(); } private Task RefreshCharacterList() diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs new file mode 100644 index 00000000..91dab6cb --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -0,0 +1,13 @@ +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.UI.AdvancedWindow; + +public class ResourceTreeViewerFactory( + Configuration config, + ResourceTreeFactory treeFactory, + ChangedItemDrawer changedItemDrawer, + IncognitoService incognito) +{ + public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); +} diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 638afef0..29a1f291 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -212,7 +212,7 @@ public class ChangedItemDrawer : IDisposable return; var typeFilter = _config.Ephemeral.ChangedItemFilter; - if (DrawTypeFilter(ref typeFilter, 0.0f)) + if (DrawTypeFilter(ref typeFilter)) { _config.Ephemeral.ChangedItemFilter = typeFilter; _config.Ephemeral.Save(); @@ -220,7 +220,7 @@ public class ChangedItemDrawer : IDisposable } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter, float yOffset) + public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); @@ -233,7 +233,6 @@ public class ChangedItemDrawer : IDisposable var ret = false; var icon = _icons[type]; var flag = typeFilter.HasFlag(type); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { @@ -267,7 +266,7 @@ public class ChangedItemDrawer : IDisposable ImGui.SameLine(); } - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index bb22e6a7..cb4dbe20 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -31,6 +31,7 @@ public sealed class CollectionPanel : IDisposable private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; private readonly FilenameService _fileNames; + private readonly IncognitoService _incognito; private readonly IFontHandle _nameFont; private static readonly IReadOnlyDictionary Buttons = CreateButtons(); @@ -41,7 +42,8 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames) + CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames, + IncognitoService incognito) { _collections = manager.Storage; _active = manager.Active; @@ -50,8 +52,9 @@ public sealed class CollectionPanel : IDisposable _targets = targets; _mods = mods; _fileNames = fileNames; + _incognito = incognito; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); - _inheritanceUi = new InheritanceUi(manager, _selector); + _inheritanceUi = new InheritanceUi(manager, incognito); _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); } @@ -415,7 +418,7 @@ public sealed class CollectionPanel : IDisposable /// Respect incognito mode for names of identifiers. private string Name(ActorIdentifier id, string? name) - => _selector.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + => _incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned ? id.Incognito(name) : name ?? id.ToString(); @@ -423,7 +426,7 @@ public sealed class CollectionPanel : IDisposable private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index fac85d4d..24d3f591 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -17,13 +17,12 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl private readonly CollectionStorage _storage; private readonly ActiveCollections _active; private readonly TutorialService _tutorial; + private readonly IncognitoService _incognito; private ModCollection? _dragging; - public bool IncognitoMode; - public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, - TutorialService tutorial) + TutorialService tutorial, IncognitoService incognito) : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) { _config = config; @@ -31,6 +30,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl _storage = storage; _active = active; _tutorial = tutorial; + _incognito = incognito; _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); // Set items. @@ -109,7 +109,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => IncognitoMode ? collection.AnonymizedName : collection.Name; + => _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 2290592d..418fe52c 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -9,7 +9,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public class InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) : IUiService +public class InheritanceUi(CollectionManager collectionManager, IncognitoService incognito) : IUiService { private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; @@ -312,5 +312,5 @@ public class InheritanceUi(CollectionManager collectionManager, CollectionSelect } private string Name(ModCollection collection) - => selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + => incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; } diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs new file mode 100644 index 00000000..d4b1828f --- /dev/null +++ b/Penumbra/UI/IncognitoService.cs @@ -0,0 +1,29 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using Penumbra.UI.Classes; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public class IncognitoService(TutorialService tutorial) +{ + public bool IncognitoMode; + + public void DrawToggle(float? buttonWidth = null) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) + .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); + if (ImGuiUtil.DrawDisabledButton( + $"{(IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", + new Vector2(buttonWidth ?? ImGui.GetFrameHeightWithSpacing(), ImGui.GetFrameHeight()), string.Empty, false, true)) + IncognitoMode = !IncognitoMode; + var hovered = ImGui.IsItemHovered(); + tutorial.OpenTutorial(BasicTutorialSteps.Incognito); + color.Pop(2); + if (hovered) + ImGui.SetTooltip(IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index fe1471b3..1eaece50 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -21,6 +21,7 @@ public sealed class CollectionsTab : IDisposable, ITab private readonly CollectionSelector _selector; private readonly CollectionPanel _panel; private readonly TutorialService _tutorial; + private readonly IncognitoService _incognito; public enum PanelMode { @@ -40,13 +41,14 @@ public sealed class CollectionsTab : IDisposable, ITab } } - public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, + public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) { - _config = configuration.Ephemeral; - _tutorial = tutorial; - _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames); + _config = configuration.Ephemeral; + _tutorial = tutorial; + _incognito = incognito; + _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames, incognito); } public void Dispose() @@ -116,18 +118,7 @@ public sealed class CollectionsTab : IDisposable, ITab _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); ImGui.SameLine(); - style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); - if (ImGuiUtil.DrawDisabledButton( - $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", - buttonSize with { X = withSpacing }, string.Empty, false, true)) - _selector.IncognitoMode = !_selector.IncognitoMode; - var hovered = ImGui.IsItemHovered(); - _tutorial.OpenTutorial(BasicTutorialSteps.Incognito); - color.Pop(2); - if (hovered) - ImGui.SetTooltip(_selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); + _incognito.DrawToggle(withSpacing); } private void DrawPanel() diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 09772d8e..787e07a1 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,18 +1,15 @@ using OtterGui.Widgets; -using Penumbra.Interop.ResourceTree; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.Tabs; public class OnScreenTab : ITab { - private readonly Configuration _config; private readonly ResourceTreeViewer _viewer; - public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) + public OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) { - _config = config; - _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { }); + _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); } public ReadOnlySpan Label From c7046ec0069bef1ee9d98602589740ed0d3a74bb Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 31 May 2024 01:05:05 +0200 Subject: [PATCH 1703/2451] ResourceTree: Add name/path filter --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 - .../ResourceTree/ResourceTreeFactory.cs | 3 - .../UI/AdvancedWindow/ResourceTreeViewer.cs | 74 +++++++++++++++---- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index e74edb91..9c911791 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -10,7 +10,6 @@ public class ResourceNode : ICloneable public string? Name; public string? FallbackName; public ChangedItemIcon Icon; - public ChangedItemIcon DescendentIcons; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -53,7 +52,6 @@ public class ResourceNode : ICloneable Name = other.Name; FallbackName = other.FallbackName; Icon = other.Icon; - DescendentIcons = other.DescendentIcons; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a722e344..5a190e52 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -116,9 +116,6 @@ public class ResourceTreeFactory( { if (node.Name == parent?.Name) node.Name = null; - - if (parent != null) - parent.DescendentIcons |= node.Icon | node.DescendentIcons; }); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c0c49e47..5f376b26 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -24,9 +24,12 @@ public class ResourceTreeViewer private readonly Action _drawActions; private readonly HashSet _unfolded; + private readonly Dictionary _filterCache; + private TreeCategory _categoryFilter; private ChangedItemDrawer.ChangedItemIcon _typeFilter; private string _nameFilter; + private string _nodeFilter; private Task? _task; @@ -40,11 +43,14 @@ public class ResourceTreeViewer _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; - _unfolded = new HashSet(); + _unfolded = []; + + _filterCache = []; _categoryFilter = AllCategories; _typeFilter = ChangedItemDrawer.AllFlags; _nameFilter = string.Empty; + _nodeFilter = string.Empty; } public void Draw() @@ -107,7 +113,7 @@ public class ResourceTreeViewer (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31)); + DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); } } } @@ -140,14 +146,22 @@ public class ResourceTreeViewer ImGui.SameLine(0, checkPadding); + var filterChanged = false; ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) - _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - checkSpacing - ImGui.GetFrameHeightWithSpacing()); - ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; + ImGui.SetNextItemWidth(fieldWidth); + filterChanged |= ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + ImGui.SameLine(0, checkSpacing); + ImGui.SetNextItemWidth(fieldWidth); + filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); _incognito.DrawToggle(); + + if (filterChanged) + _filterCache.Clear(); } private Task RefreshCharacterList() @@ -161,36 +175,68 @@ public class ResourceTreeViewer } finally { + _filterCache.Clear(); _unfolded.Clear(); _onRefresh(); } }); - private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - NodeVisibility GetNodeVisibility(ResourceNode node) + bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon) + { + if (!_typeFilter.HasFlag(filterIcon)) + return false; + + if (_nodeFilter.Length == 0) + return true; + + return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + } + + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { if (node.Internal && !debugMode) return NodeVisibility.Hidden; - if (_typeFilter.HasFlag(node.Icon)) + var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon; + if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; - if ((_typeFilter & node.DescendentIcons) != 0) - return NodeVisibility.DescendentsOnly; + + foreach (var child in node.Children) + { + if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) + return NodeVisibility.DescendentsOnly; + } return NodeVisibility.Hidden; } + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + { + if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) + { + visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); + _filterCache.Add(nodePathHash, visibility); + } + return visibility; + } + string GetAdditionalDataSuffix(ByteString data) => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { - var visibility = GetNodeVisibility(resourceNode); + var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); + + var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon); if (visibility == NodeVisibility.Hidden) continue; @@ -199,14 +245,14 @@ public class ResourceTreeViewer using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); - var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); + var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(child) != NodeVisibility.Hidden); + var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { @@ -291,7 +337,7 @@ public class ResourceTreeViewer } if (unfolded) - DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); } } From 81fdbf6ccff729daedc003eba80b3969a4f799f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 16:59:51 +0200 Subject: [PATCH 1704/2451] Small cleanup. --- Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e8a27a74..b8faadf7 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -321,8 +321,8 @@ public sealed class ModGroupEditDrawer( && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) return; - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) + using var target = ImUtf8.DragDropTarget(); + if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) return; if (_dragDropGroup != null && _dragDropOption != null) From 67bb95f6e64232480ced89d33318edc976206e63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 17:00:40 +0200 Subject: [PATCH 1705/2451] Update submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 1d936516..0b5afffd 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db +Subproject commit 0b5afffda19d3e16aec9e8682d18c8f11f67f1c6 diff --git a/Penumbra.GameData b/Penumbra.GameData index f2cea65b..29b71cf7 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5 +Subproject commit 29b71cf7b3cc68995d38f0954fa38c4b9500a81d From f61bd8bb8a5cd2bb45156803d1eddd8b8d74b8f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 19:37:24 +0200 Subject: [PATCH 1706/2451] Update Changelog and improve metamanipulation display in advanced editing. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 58 +++++++------------ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 30 ++++++---- Penumbra/UI/Changelog.cs | 47 ++++++++------- 3 files changed, 64 insertions(+), 71 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 829161f5..86d5e02e 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -14,13 +15,19 @@ public class ModMetaEditor(ModManager modManager) private readonly HashSet _rsp = []; private readonly HashSet _globalEqp = []; - public int OtherImcCount { get; private set; } - public int OtherEqpCount { get; private set; } - public int OtherEqdpCount { get; private set; } - public int OtherGmpCount { get; private set; } - public int OtherEstCount { get; private set; } - public int OtherRspCount { get; private set; } - public int OtherGlobalEqpCount { get; private set; } + public sealed class OtherOptionData : List + { + public int TotalCount; + + public new void Clear() + { + TotalCount = 0; + base.Clear(); + } + } + + public readonly FrozenDictionary OtherData = + Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); public bool Changes { get; private set; } @@ -114,13 +121,9 @@ public class ModMetaEditor(ModManager modManager) public void Load(Mod mod, IModDataContainer currentOption) { - OtherImcCount = 0; - OtherEqpCount = 0; - OtherEqdpCount = 0; - OtherGmpCount = 0; - OtherEstCount = 0; - OtherRspCount = 0; - OtherGlobalEqpCount = 0; + foreach (var type in Enum.GetValues()) + OtherData[type].Clear(); + foreach (var option in mod.AllDataContainers) { if (option == currentOption) @@ -128,30 +131,9 @@ public class ModMetaEditor(ModManager modManager) foreach (var manip in option.Manipulations) { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - ++OtherImcCount; - break; - case MetaManipulation.Type.Eqdp: - ++OtherEqdpCount; - break; - case MetaManipulation.Type.Eqp: - ++OtherEqpCount; - break; - case MetaManipulation.Type.Est: - ++OtherEstCount; - break; - case MetaManipulation.Type.Gmp: - ++OtherGmpCount; - break; - case MetaManipulation.Type.Rsp: - ++OtherRspCount; - break; - case MetaManipulation.Type.GlobalEqp: - ++OtherGlobalEqpCount; - break; - } + var data = OtherData[manip.ManipulationType]; + ++data.TotalCount; + data.Add(option.GetFullName()); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 6f542377..a2a6925a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -66,37 +66,45 @@ public partial class ModEditWindow return; DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, - _editor.MetaEditor.OtherEqpCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqp]); DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, - _editor.MetaEditor.OtherEqdpCount); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, _editor.MetaEditor.OtherImcCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqdp]); + DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, + _editor.MetaEditor.OtherData[MetaManipulation.Type.Imc]); DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, - _editor.MetaEditor.OtherEstCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Est]); DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, - _editor.MetaEditor.OtherGmpCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Gmp]); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, - _editor.MetaEditor.OtherRspCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Rsp]); DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, - GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); + GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherData[MetaManipulation.Type.GlobalEqp]); } /// The headers for the different meta changes all have basically the same structure for different types. private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, - Action draw, - Action drawNew, int otherCount) + Action draw, Action drawNew, + ModMetaEditor.OtherOptionData otherOptionData) { const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; var oldPos = ImGui.GetCursorPosY(); var header = ImGui.CollapsingHeader($"{items.Count} {label}"); var newPos = ImGui.GetCursorPos(); - if (otherCount > 0) + if (otherOptionData.TotalCount > 0) { - var text = $"{otherCount} Edits in other Options"; + var text = $"{otherOptionData.TotalCount} Edits in other Options"; var size = ImGui.CalcTextSize(text).X; ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var name in otherOptionData) + ImUtf8.Text(name); + } + ImGui.SetCursorPos(newPos); } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 2b2cfa99..3f5a446a 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -47,19 +47,26 @@ public class PenumbraChangelog Add8_2_0(Changelog); Add8_3_0(Changelog); Add1_0_0_0(Changelog); - Add1_0_2_0(Changelog); - Add1_0_3_0(Changelog); + AddDummy(Changelog); + AddDummy(Changelog); + Add1_1_0_0(Changelog); } #region Changelogs - private static void Add1_0_3_0(Changelog log) - => log.NextVersion("Version 1.0.3.0") + private static void Add1_1_0_0(Changelog log) + => log.NextVersion("Version 1.1.0.0") .RegisterImportant( "This update comes, again, with a lot of very heavy backend changes (collections and groups) and thus may introduce new issues.") + .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") + .RegisterHighlight( + "Added an experimental crash handler that is supposed to write a Penumbra.log file when the game crashes, containing Penumbra-specific information.") + .RegisterEntry("This is disabled by default. It can be enabled in Advanced Settings.", 1) .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight( + "Added predefined tags that can be setup in the Settings tab and can be more easily applied or removed from mods. (by DZD)") .RegisterHighlight( "A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") .RegisterEntry( @@ -75,9 +82,14 @@ public class PenumbraChangelog .RegisterEntry( "This can be used if something like a jacket or a stole is put onto an accessory to prevent it from being hidden in general.", 1) + .RegisterEntry( + "The first empty option in a single-select option group imported from a TTMP will now keep its location instead of being moved to the first option.") + .RegisterEntry("Further empty options are still removed.", 1) .RegisterHighlight( "Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) + .RegisterEntry("Added the characterglass.shpk shader file to special shader treatment to fix issues when replacing it. (By Ny)") + .RegisterEntry("Made it more obvious if a user has not set their root directory yet.") .RegisterEntry( "You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") .RegisterHighlight( @@ -88,29 +100,17 @@ public class PenumbraChangelog .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") .RegisterEntry( "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") - .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") + .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") + .RegisterEntry("Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") .RegisterEntry("Reworked the API and IPC structure heavily.") + .RegisterImportant("This means some plugins interacting with Penumbra may not work correctly until they update.", 1) .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") + .RegisterEntry("Fixed an issue where reloading a mod did not ensure settings for that mod being correct afterwards.") + .RegisterEntry("Fixed some issues with the file sizes of compressed files.") .RegisterEntry("Fixed an issue with merging and deduplicating mods.") .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") .RegisterEntry( - "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); - - private static void Add1_0_2_0(Changelog log) - => log.NextVersion("Version 1.0.2.0") - .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") - .RegisterHighlight( - "Added an experimental crash handler that is supposed to write a Penumbra.log file when the game crashes, containing Penumbra-specific information.") - .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") - .RegisterHighlight( - "Added predefined tags that can be setup in the Settings tab and can be more easily applied or removed from mods. (by DZD)") - .RegisterEntry( - "The first empty option in a single-select option group imported from a TTMP will now keep its location instead of being moved to the first option.") - .RegisterEntry("Further empty options are still removed.", 1) - .RegisterEntry("Made it more obvious if a user has not set their root directory yet.") - .RegisterEntry("Added the characterglass.shpk shader file to special shader treatment to fix issues when replacing it. (By Ny)") - .RegisterEntry("Fixed some issues with the file sizes of compressed files.") - .RegisterEntry("Fixed an issue where reloading a mod did not ensure settings for that mod being correct afterwards.") + "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer.") .RegisterEntry("Added an option to automatically redraw the player character when saving files. (1.0.0.8)") .RegisterEntry("Fixed issue with manipulating mods not triggering some events. (1.0.0.7)") .RegisterEntry("Fixed issue with temporary mods not triggering some events. (1.0.0.6)") @@ -762,6 +762,9 @@ public class PenumbraChangelog #endregion + private static void AddDummy(Changelog log) + => log.NextVersion(string.Empty); + private (int, ChangeLogDisplayType) ConfigData() => (_config.Ephemeral.LastSeenVersion, _config.ChangeLogDisplayType); From ce11bec985770af2f9bfdaf68f8e265823dfa38a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 22:55:22 +0200 Subject: [PATCH 1707/2451] Use strings for global eqp. --- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index d57af1d9..1a7396f9 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -1,5 +1,9 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + namespace Penumbra.Meta.Manipulations; +[JsonConverter(typeof(StringEnumConverter))] public enum GlobalEqpType { DoNotHideEarrings, From b79600ea1489dab7cb3dc7dc5e2c28708de6ea3f Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 09:54:26 +0000 Subject: [PATCH 1708/2451] [CI] Updating repo.json for 1.1.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a996e2d0..54e6b8b4 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.3.2", + "AssemblyVersion": "1.1.0.0", + "TestingAssemblyVersion": "1.1.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 24d4e9fac6b15dd700a01cc6ec05b224d508b7f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 18:13:46 +0200 Subject: [PATCH 1709/2451] Fix collections not being added on creation. --- Penumbra/Collections/Manager/CollectionStorage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 39068e87..68bd08cb 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -154,6 +154,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable public bool AddCollection(string name, ModCollection? duplicate) { var newCollection = Create(name, _collections.Count, duplicate); + _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); From 331b7fbc1de13181aba85099518a14665ec0130e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 18:14:08 +0200 Subject: [PATCH 1710/2451] Fix other options displaying the same option multiple times. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 86d5e02e..86853755 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -15,7 +15,7 @@ public class ModMetaEditor(ModManager modManager) private readonly HashSet _rsp = []; private readonly HashSet _globalEqp = []; - public sealed class OtherOptionData : List + public sealed class OtherOptionData : HashSet { public int TotalCount; From aba68cfb925a0dc97dd9fa6cc5be3f0fe8310f7a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 16:18:13 +0000 Subject: [PATCH 1711/2451] [CI] Updating repo.json for 1.1.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 54e6b8b4..3f5d7262 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.0.0", - "TestingAssemblyVersion": "1.1.0.0", + "AssemblyVersion": "1.1.0.1", + "TestingAssemblyVersion": "1.1.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5101b73fdcb9db2e4cd992d70b0d74bf66316616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 20:28:43 +0200 Subject: [PATCH 1712/2451] Fix issue with creating unnamed collections. --- Penumbra/Collections/Manager/CollectionStorage.cs | 3 +++ Penumbra/UI/CollectionTab/CollectionSelector.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 68bd08cb..f6287320 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -153,6 +153,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { + if (name.Length == 0) + return false; + var newCollection = Create(name, _collections.Count, duplicate); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index fac85d4d..cecb41d7 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -109,7 +109,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => IncognitoMode ? collection.AnonymizedName : collection.Name; + => IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { From e7cf9d35c9ca7208423c95bc59fbfcddaa415df9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:33:21 +0200 Subject: [PATCH 1713/2451] Add GetChangedItems for Mods. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 9 +++-- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 1 + Penumbra/Api/IpcTester/ModsIpcTester.cs | 44 +++++++++++++++++++------ 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 69d106b4..f1e4e520 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0 +Subproject commit f1e4e520daaa8f23e5c8b71d55e5992b8f6768e2 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index c1e0c684..16dd8be9 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -106,8 +106,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable var fullPath = leaf.FullName(); var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath); - var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); - return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault ); + var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); + return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault); } public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) @@ -129,4 +129,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return PenumbraApiEc.PathRenameFailed; } } + + public Dictionary GetChangedItems(string modDirectory, string modName) + => _modManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + : []; } diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 1d5b1537..0400c694 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 0); + => (5, 1); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index ebf71176..6b146c39 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -49,6 +49,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModMoved.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 43f397e5..2be51a80 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -13,16 +14,17 @@ public class ModsIpcTester : IUiService, IDisposable { private readonly DalamudPluginInterface _pi; - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; - private string _newInstallPath = string.Empty; - private PenumbraApiEc _lastReloadEc; - private PenumbraApiEc _lastAddEc; - private PenumbraApiEc _lastDeleteEc; - private PenumbraApiEc _lastSetPathEc; - private PenumbraApiEc _lastInstallEc; - private Dictionary _mods = []; + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private string _newInstallPath = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private PenumbraApiEc _lastInstallEc; + private Dictionary _mods = []; + private Dictionary _changedItems = []; public readonly EventSubscriber DeleteSubscriber; public readonly EventSubscriber AddSubscriber; @@ -120,6 +122,14 @@ public class ModsIpcTester : IUiService, IDisposable ImGui.SameLine(); ImGui.TextUnformatted(_lastDeleteEc.ToString()); + IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items"); + DrawChangedItemsPopup(); + if (ImUtf8.Button("Get##ChangedItems"u8)) + { + _changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName); + ImUtf8.OpenPopup("ChangedItems"u8); + } + IpcTester.DrawIntro(GetModPath.Label, "Current Path"); var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName); ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]"); @@ -157,4 +167,18 @@ public class ModsIpcTester : IUiService, IDisposable if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); } + + private void DrawChangedItemsPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImUtf8.Popup("ChangedItems"u8); + if (!p) + return; + + foreach (var (name, data) in _changedItems) + ImUtf8.Text($"{name}: {data}"); + + if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } } From cfa58ee19672a335592e5d06bad26a71aa58aee6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:42:04 +0200 Subject: [PATCH 1714/2451] Fix global EQP rings checking bracelets instead. --- Penumbra/Meta/Manipulations/GlobalEqpCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs index 48ffb308..26eb1d05 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs @@ -44,10 +44,10 @@ public struct GlobalEqpCache : IService if (_doNotHideBracelets.Contains(armor[7].Set)) original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet; - if (_doNotHideBracelets.Contains(armor[8].Set)) + if (_doNotHideRingR.Contains(armor[8].Set)) original |= EqpEntry.HandShowRingR; - if (_doNotHideBracelets.Contains(armor[9].Set)) + if (_doNotHideRingL.Contains(armor[9].Set)) original |= EqpEntry.HandShowRingL; return original; } From ef9d81c061c659c0b43643991b15e8be44b068af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:42:17 +0200 Subject: [PATCH 1715/2451] Fix mod merger. --- Penumbra/Mods/Editor/ModMerger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index d6e21076..32a207ff 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -37,7 +37,7 @@ public class ModMerger : IDisposable public readonly HashSet SelectedOptions = []; - public readonly IReadOnlyList Warnings = []; + public readonly IReadOnlyList Warnings = new List(); public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, From 2e6473dc096363901c5665f79eb8afb750c2eb7c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 21:44:53 +0000 Subject: [PATCH 1716/2451] [CI] Updating repo.json for 1.1.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 3f5d7262..5e9e5b37 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.0.1", - "TestingAssemblyVersion": "1.1.0.1", + "AssemblyVersion": "1.1.0.2", + "TestingAssemblyVersion": "1.1.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 3deda68eeca875ebd6403359f52877ec7d53b55a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 01:03:51 +0200 Subject: [PATCH 1717/2451] Small updates. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/StaticServiceManager.cs | 2 -- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- .../ResourceTreeViewerFactory.cs | 3 +- Penumbra/UI/IncognitoService.cs | 29 +++++++++---------- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- .../ModsTab/Groups/MultiModGroupEditDrawer.cs | 2 +- .../Groups/SingleModGroupEditDrawer.cs | 2 +- 9 files changed, 21 insertions(+), 25 deletions(-) diff --git a/OtterGui b/OtterGui index 0b5afffd..becacbca 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0b5afffda19d3e16aec9e8682d18c8f11f67f1c6 +Subproject commit becacbca4f35595d16ff40dc9639cfa24be3461f diff --git a/Penumbra.GameData b/Penumbra.GameData index 29b71cf7..fed687b5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 29b71cf7b3cc68995d38f0954fa38c4b9500a81d +Subproject commit fed687b536b7c709484db251b690b8821c5ef403 diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 146d7ee0..0c6648ba 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -149,7 +149,6 @@ public static class StaticServiceManager private static ServiceManager AddInterface(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -182,7 +181,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton(p => new Diagnostics(p)); private static ServiceManager AddModEditor(this ServiceManager services) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 5f376b26..7315f136 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -158,7 +158,7 @@ public class ResourceTreeViewer ImGui.SetNextItemWidth(fieldWidth); filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); - _incognito.DrawToggle(); + _incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); if (filterChanged) _filterCache.Clear(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 91dab6cb..ea64c0bf 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Interop.ResourceTree; namespace Penumbra.UI.AdvancedWindow; @@ -6,7 +7,7 @@ public class ResourceTreeViewerFactory( Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito) + IncognitoService incognito) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index d4b1828f..d58ea1ec 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,29 +1,26 @@ -using Dalamud.Interface.Utility; using Dalamud.Interface; -using ImGuiNET; -using OtterGui; using Penumbra.UI.Classes; using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; namespace Penumbra.UI; -public class IncognitoService(TutorialService tutorial) +public class IncognitoService(TutorialService tutorial) : IService { public bool IncognitoMode; - public void DrawToggle(float? buttonWidth = null) + public void DrawToggle(float width) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); - if (ImGuiUtil.DrawDisabledButton( - $"{(IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", - new Vector2(buttonWidth ?? ImGui.GetFrameHeightWithSpacing(), ImGui.GetFrameHeight()), string.Empty, false, true)) - IncognitoMode = !IncognitoMode; - var hovered = ImGui.IsItemHovered(); + var color = ColorId.FolderExpanded.Value(); + using (ImRaii.PushFrameBorder(ImUtf8.GlobalScale, color)) + { + var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8; + var icon = IncognitoMode ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; + if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color)) + IncognitoMode = !IncognitoMode; + } + tutorial.OpenTutorial(BasicTutorialSteps.Incognito); - color.Pop(2); - if (hovered) - ImGui.SetTooltip(IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index b10f123c..b129d275 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -118,7 +118,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr : validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, !validName || dis)) + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, default, !validName || dis)) { editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); editor.NewOptionName = null; diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs index e6701a03..f0275853 100644 --- a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -54,7 +54,7 @@ public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiM var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8, default, !validName)) { editor.ModManager.OptionEditor.MultiEditor.AddOption(group, name); editor.NewOptionName = null; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index 75fbc63a..be2dbd73 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -59,7 +59,7 @@ public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, Singl var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8, default, !validName)) { editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); editor.NewOptionName = null; From 137b752196154960e2687be65728ae9d60bd913b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 01:53:29 +0200 Subject: [PATCH 1718/2451] Fix Dye Preview not applying. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fed687b5..ad12ddce 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fed687b536b7c709484db251b690b8821c5ef403 +Subproject commit ad12ddcef38a9ed4e4dd7424d748f41c4b97db10 From 05d010a281de9e494c389bcb34fdad35ba9fe13d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 12:08:49 +0200 Subject: [PATCH 1719/2451] Add some functionality to allow an IMC group to add apply to all variants. --- Penumbra/Collections/Cache/ImcCache.cs | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- Penumbra/Import/TexToolsMeta.Export.cs | 2 +- Penumbra/Meta/Files/ImcFile.cs | 21 +++++----- Penumbra/Meta/ImcChecker.cs | 40 ++++++++++++++++++- Penumbra/Meta/Manipulations/Imc.cs | 37 +++++++++-------- Penumbra/Mods/Groups/ImcModGroup.cs | 31 ++++++++++---- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 6 +-- .../Manager/OptionEditor/ImcModGroupEditor.cs | 14 ++++++- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 8 +++- 10 files changed, 117 insertions(+), 46 deletions(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 33b366d3..7990122a 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -51,7 +51,7 @@ public readonly struct ImcCache : IDisposable try { if (!_imcFiles.TryGetValue(path, out var file)) - file = new ImcFile(manager, manip); + file = new ImcFile(manager, manip.Identifier); _imcManipulations[idx] = (manip, file); if (!manip.Apply(file)) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f062ae25..554cf848 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -110,7 +110,7 @@ public partial class TexToolsMeta var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, new ImcEntry()); - var def = new ImcFile(_metaFileManager, manip); + var def = new ImcFile(_metaFileManager, manip.Identifier); var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 03bdbd90..09bd2c12 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -133,7 +133,7 @@ public partial class TexToolsMeta { case MetaManipulation.Type.Imc: var allManips = manips.ToList(); - var baseFile = new ImcFile(manager, allManips[0].Imc); + var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); foreach (var manip in allManips) manip.Imc.Apply(baseFile); diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 68d3f5b3..5d704cf8 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -9,20 +9,20 @@ namespace Penumbra.Meta.Files; public class ImcException : Exception { - public readonly ImcManipulation Manipulation; - public readonly string GamePath; + public readonly ImcIdentifier Identifier; + public readonly string GamePath; - public ImcException(ImcManipulation manip, Utf8GamePath path) + public ImcException(ImcIdentifier identifier, Utf8GamePath path) { - Manipulation = manip; - GamePath = path.ToString(); + Identifier = identifier; + GamePath = path.ToString(); } public override string Message => "Could not obtain default Imc File.\n" + " Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted.\n" + $" Game Path: {GamePath}\n" - + $" Manipulation: {Manipulation}"; + + $" Manipulation: {Identifier}"; } public unsafe class ImcFile : MetaBaseFile @@ -142,13 +142,14 @@ public unsafe class ImcFile : MetaBaseFile } } - public ImcFile(MetaFileManager manager, ImcManipulation manip) + public ImcFile(MetaFileManager manager, ImcIdentifier identifier) : base(manager, 0) { - Path = manip.GamePath(); - var file = manager.GameData.GetFile(Path.ToString()); + var path = identifier.GamePathString(); + Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; + var file = manager.GameData.GetFile(path); if (file == null) - throw new ImcException(manip, Path); + throw new ImcException(identifier, Path); fixed (byte* ptr = file.Data) { diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 14486e21..650919a3 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -4,11 +4,32 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; -public class ImcChecker(MetaFileManager metaFileManager) +public class ImcChecker { + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + + + public static int GetVariantCount(ImcIdentifier identifier) + { + if (VariantCounts.TryGetValue(identifier, out var count)) + return count; + + count = GetFile(identifier)?.Count ?? 0; + VariantCounts[identifier] = count; + return count; + } + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); private readonly Dictionary _cachedDefaultEntries = new(); + private readonly MetaFileManager _metaFileManager; + + public ImcChecker(MetaFileManager metaFileManager) + { + _metaFileManager = metaFileManager; + _dataManager = metaFileManager; + } public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { @@ -17,7 +38,7 @@ public class ImcChecker(MetaFileManager metaFileManager) try { - var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -33,4 +54,19 @@ public class ImcChecker(MetaFileManager metaFileManager) public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, imcManip.EquipSlot, imcManip.BodySlot), storeCache); + + private static ImcFile? GetFile(ImcIdentifier identifier) + { + if (_dataManager == null) + return null; + + try + { + return new ImcFile(_dataManager, identifier); + } + catch + { + return null; + } + } } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index fef86520..2a2f4c03 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -31,12 +31,16 @@ public readonly record struct ImcIdentifier( => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => AddChangedItems(identifier, changedItems, false); + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { - ObjectType.Equipment or ObjectType.Accessory => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, - Variant, - "a"), + ObjectType.Equipment when allVariants => GamePaths.Equipment.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Equipment => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Accessory when allVariants => GamePaths.Accessory.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Accessory => GamePaths.Accessory.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, "a"), @@ -49,24 +53,19 @@ public readonly record struct ImcIdentifier( identifier.Identify(changedItems, path); } - public Utf8GamePath GamePath() - { - return ObjectType switch + public string GamePathString() + => ObjectType switch { - ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), + ObjectType.Accessory => GamePaths.Accessory.Imc.Path(PrimaryId), + ObjectType.Equipment => GamePaths.Equipment.Imc.Path(PrimaryId), + ObjectType.DemiHuman => GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Monster => GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Weapon => GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), + _ => string.Empty, }; - } + + public Utf8GamePath GamePath() + => Utf8GamePath.FromString(GamePathString(), out var p) ? p : Utf8GamePath.Empty; public MetaIndex FileIndex() => (MetaIndex)(-1); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e0d70aa6..b336203d 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -31,6 +32,8 @@ public class ImcModGroup(Mod mod) : IModGroup public ImcIdentifier Identifier; public ImcEntry DefaultEntry; + public bool AllVariants; + public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; @@ -39,7 +42,7 @@ public class ImcModGroup(Mod mod) : IModGroup public bool CanBeDisabled { - get => OptionData.Any(m => m.IsDisableSubMod); + get => _canBeDisabled; set { _canBeDisabled = value; @@ -92,8 +95,8 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, Identifier.Variant.Id, + public ImcManipulation GetManip(ushort mask, Variant variant) + => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) @@ -102,12 +105,23 @@ public class ImcModGroup(Mod mod) : IModGroup return; var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); + if (AllVariants) + { + var count = ImcChecker.GetVariantCount(Identifier); + if (count == 0) + manipulations.Add(GetManip(mask, Identifier.Variant)); + else + for (var i = 0; i <= count; ++i) + manipulations.Add(GetManip(mask, (Variant)i)); + } + else + { + manipulations.Add(GetManip(mask, Identifier.Variant)); + } } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => Identifier.AddChangedItems(identifier, changedItems); + => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << OptionData.Count) - 1)); @@ -120,6 +134,8 @@ public class ImcModGroup(Mod mod) : IModGroup jObj.WriteTo(jWriter); jWriter.WritePropertyName(nameof(DefaultEntry)); serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName(nameof(AllVariants)); + jWriter.WriteValue(AllVariants); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -156,6 +172,7 @@ public class ImcModGroup(Mod mod) : IModGroup Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, }; if (ret.Name.Length == 0) return null; @@ -210,7 +227,7 @@ public class ImcModGroup(Mod mod) : IModGroup if (idx >= 0) return setting.HasFlag(idx); - Penumbra.Log.Warning($"A IMC Group should be able to be disabled, but does not contain a disable option."); + Penumbra.Log.Warning("A IMC Group should be able to be disabled, but does not contain a disable option."); return false; } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5a5181a5..ea4ef7b1 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -51,7 +51,7 @@ public static class EquipmentSwap var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var skipFemale = false; var skipMale = false; var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; @@ -121,7 +121,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -250,7 +250,7 @@ public static class EquipmentSwap PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry); + var imc = new ImcFile(manager, entry.Identifier); EquipItem[] items; Variant[] variants; if (idFrom == idTo) diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f9fd532f..4aae45a2 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -2,6 +2,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -14,7 +15,8 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ : ModOptionEditor(communicator, saveService, config), IService { /// Add a new, empty imc group with the given manipulation data. - public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, SaveType saveType = SaveType.ImmediateSync) + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, + SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; @@ -78,6 +80,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); } + public void ChangeAllVariants(ImcModGroup group, bool allVariants, SaveType saveType = SaveType.Queue) + { + if (group.AllVariants == allVariants) + return; + + group.AllVariants = allVariants; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index b129d275..d346e05c 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -19,7 +19,13 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr var entry = group.DefaultEntry; var changes = false; - ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); + var allVariants = group.AllVariants; + if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) + editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); + ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); using (ImUtf8.Group()) { From b63935e81ed45d562a1a898ba5361f6233516798 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 12:09:05 +0200 Subject: [PATCH 1720/2451] Fix issue with accessory vfx hook. --- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 4e24ba39..8118343d 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -182,7 +182,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex <= 4) + if (slotIndex is <= 4 or >= 10) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); var changedEquipData = ((Human*)drawObject)->ChangedEquipData; From 63b3a02e95b00dcbed78f55da4db6ba2ce874d23 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:45:22 +0200 Subject: [PATCH 1721/2451] Fix issue with crash handler and collections not saving on rename. --- OtterGui | 2 +- Penumbra.CrashHandler/CrashData.cs | 4 +- .../Collections/Manager/CollectionStorage.cs | 1 + Penumbra/Communication/ModSettingChanged.cs | 2 +- Penumbra/Services/CrashHandlerService.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 68 ++++++++----------- .../UI/CollectionTab/CollectionSelector.cs | 22 +++--- Penumbra/UI/Tabs/CollectionsTab.cs | 8 +-- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 2 +- 9 files changed, 52 insertions(+), 59 deletions(-) diff --git a/OtterGui b/OtterGui index becacbca..5de708b2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit becacbca4f35595d16ff40dc9639cfa24be3461f +Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323 diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs index cdac103f..dd75f46e 100644 --- a/Penumbra.CrashHandler/CrashData.cs +++ b/Penumbra.CrashHandler/CrashData.cs @@ -55,7 +55,7 @@ public class CrashData /// The last vfx function invoked before this crash data was generated. public VfxFuncInvokedEntry? LastVfxFuncInvoked - => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0]; + => LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0]; /// A collection of the last few characters loaded before this crash data was generated. public List LastCharactersLoaded { get; set; } = []; @@ -64,5 +64,5 @@ public class CrashData public List LastModdedFilesLoaded { get; set; } = []; /// A collection of the last few vfx functions invoked before this crash data was generated. - public List LastVfxFuncsInvoked { get; set; } = []; + public List LastVFXFuncsInvoked { get; set; } = []; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index f6287320..67de3a03 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -181,6 +181,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } + Delete(collection); _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); _collections.RemoveAt(collection.Index); // Update indices. diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index a7da345b..7fda2f35 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -24,7 +24,7 @@ public sealed class ModSettingChanged() { public enum Priority { - /// + /// Api = int.MinValue, /// diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 1239578b..25c6cf57 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -287,7 +287,7 @@ public sealed class CrashHandlerService : IDisposable, IService try { - if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && Path.IsPathRooted(actualPath)) + if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && !Path.IsPathRooted(actualPath)) return; var name = GetActorName(resolveData.AssociatedGameObject); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cb4dbe20..082b78b8 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -20,19 +20,23 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public sealed class CollectionPanel : IDisposable +public sealed class CollectionPanel( + DalamudPluginInterface pi, + CommunicatorService communicator, + CollectionManager manager, + CollectionSelector selector, + ActorManager actors, + ITargetManager targets, + ModStorage mods, + SaveService saveService, + IncognitoService incognito) + : IDisposable { - private readonly CollectionStorage _collections; - private readonly ActiveCollections _active; - private readonly CollectionSelector _selector; - private readonly ActorManager _actors; - private readonly ITargetManager _targets; - private readonly IndividualAssignmentUi _individualAssignmentUi; - private readonly InheritanceUi _inheritanceUi; - private readonly ModStorage _mods; - private readonly FilenameService _fileNames; - private readonly IncognitoService _incognito; - private readonly IFontHandle _nameFont; + private readonly CollectionStorage _collections = manager.Storage; + private readonly ActiveCollections _active = manager.Active; + private readonly IndividualAssignmentUi _individualAssignmentUi = new(communicator, actors, manager); + private readonly InheritanceUi _inheritanceUi = new(manager, incognito); + private readonly IFontHandle _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); @@ -41,23 +45,6 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; - public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames, - IncognitoService incognito) - { - _collections = manager.Storage; - _active = manager.Active; - _selector = selector; - _actors = actors; - _targets = targets; - _mods = mods; - _fileNames = fileNames; - _incognito = incognito; - _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); - _inheritanceUi = new InheritanceUi(manager, incognito); - _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); - } - public void Dispose() { _individualAssignmentUi.Dispose(); @@ -237,17 +224,22 @@ public sealed class CollectionPanel : IDisposable var name = _newName ?? collection.Name; var identifier = collection.Identifier; var width = ImGui.GetContentRegionAvail().X; - var fileName = _fileNames.CollectionFile(collection); + var fileName = saveService.FileNames.CollectionFile(collection); ImGui.SetNextItemWidth(width); if (ImGui.InputText("##name", ref name, 128)) _newName = name; - if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null) + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Name) { collection.Name = _newName; - _newName = null; + saveService.QueueSave(new ModCollectionSave(mods, collection)); + selector.RestoreCollections(); + _newName = null; } else if (ImGui.IsItemDeactivated()) + { _newName = null; + } + using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) @@ -329,7 +321,7 @@ public sealed class CollectionPanel : IDisposable DrawIndividualDragTarget(text, id); if (!invalid) { - _selector.DragTargetAssignment(type, id); + selector.DragTargetAssignment(type, id); var name = Name(collection); var size = ImGui.CalcTextSize(name); var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; @@ -418,7 +410,7 @@ public sealed class CollectionPanel : IDisposable /// Respect incognito mode for names of identifiers. private string Name(ActorIdentifier id, string? name) - => _incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + => incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned ? id.Incognito(name) : name ?? id.ToString(); @@ -426,7 +418,7 @@ public sealed class CollectionPanel : IDisposable private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { @@ -445,11 +437,11 @@ public sealed class CollectionPanel : IDisposable } private void DrawCurrentCharacter(Vector2 width) - => DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.GetCurrentPlayer()); + => DrawIndividualButton("Current Character", width, string.Empty, 'c', actors.GetCurrentPlayer()); private void DrawCurrentTarget(Vector2 width) => DrawIndividualButton("Current Target", width, string.Empty, 't', - _actors.FromObject(_targets.Target, false, true, true)); + actors.FromObject(targets.Target, false, true, true)); private void DrawNewPlayer(Vector2 width) => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', @@ -610,7 +602,7 @@ public sealed class CollectionPanel : IDisposable ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in _mods.Select(m => (m, collection[m.Index])) + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection[m.Index])) .Where(t => t.Item2.Settings != null) .OrderBy(t => t.m.Name)) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index c14baf5b..024873bf 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -44,7 +44,9 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl if (idx < 0 || idx >= Items.Count) return false; - return _storage.RemoveCollection(Items[idx]); + // Always return false since we handle the selection update ourselves. + _storage.RemoveCollection(Items[idx]); + return false; } protected override bool DeleteButtonEnabled() @@ -111,6 +113,15 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl private string Name(ModCollection collection) => _incognito.IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; + public void RestoreCollections() + { + Items.Clear(); + foreach (var c in _storage.OrderBy(c => c.Name)) + Items.Add(c); + SetFilterDirty(); + SetCurrent(_active.Current); + } + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { switch (type) @@ -122,14 +133,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl SetFilterDirty(); return; case CollectionType.Inactive: - Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Name)) - Items.Add(c); - - if (old == Current) - ClearCurrentSelection(); - else - TryRestoreCurrent(); + RestoreCollections(); SetFilterDirty(); return; default: diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 1eaece50..fabf7561 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,16 +1,12 @@ using Dalamud.Game.ClientState.Objects; -using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.Classes; using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; @@ -42,13 +38,13 @@ public sealed class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, - CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) + CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) { _config = configuration.Ephemeral; _tutorial = tutorial; _incognito = incognito; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames, incognito); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito); } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 4649e548..94c6cbd6 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -96,7 +96,7 @@ public static class CrashDataExtensions if (!table) return; - ImGuiClip.ClippedDraw(data.LastVfxFuncsInvoked, vfx => + ImGuiClip.ClippedDraw(data.LastVFXFuncsInvoked, vfx => { ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); From aeb2db9f5d7f390407e4ab760a55b5eb6a4a53ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:46:45 +0200 Subject: [PATCH 1722/2451] Add tooltip to global eqp condition. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index a2a6925a..d4049bd9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -758,6 +758,7 @@ public partial class ModEditWindow if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) _new = _new with { Condition = newId }; + ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); } public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) From 699ae8e1fb5939c8b7c8afe1614d73ba70d6317d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:47:11 +0200 Subject: [PATCH 1723/2451] Fix issue with collection settings being set to negative value for some reason. --- Penumbra/Mods/Settings/Setting.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Settings/Setting.cs b/Penumbra/Mods/Settings/Setting.cs index 059cbf51..e8ad103c 100644 --- a/Penumbra/Mods/Settings/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -85,6 +85,24 @@ public readonly record struct Setting(ulong Value) public override Setting ReadJson(JsonReader reader, Type objectType, Setting existingValue, bool hasExistingValue, JsonSerializer serializer) - => new(serializer.Deserialize(reader)); + { + try + { + return new Setting(serializer.Deserialize(reader)); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not deserialize setting {reader.Value} to unsigned long:\n{e}"); + try + { + return new Setting((ulong)serializer.Deserialize(reader)); + } + catch + { + Penumbra.Log.Warning($"Could not deserialize setting {reader.Value} to long:\n{e}"); + return Zero; + } + } + } } } From 87fec7783eb3d416f2ffb5aa3cdc1b2c78acfc75 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 4 Jun 2024 12:10:45 +1000 Subject: [PATCH 1724/2451] Fix blend weight adjustment getting stuck on near-bounds values --- Penumbra/Import/Models/Import/VertexAttribute.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index a4651776..af401ec1 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -150,6 +150,12 @@ public class VertexAttribute { var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); var closestIndex = Enumerable.Range(0, 4) + .Where(index => { + var byteValue = byteValues[index]; + if (adjustment < 0) return byteValue > 0; + if (adjustment > 0) return byteValue < 255; + return true; + }) .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) .MinBy(x => x.delta) .index; From 48dd4bcadb2a43e0cef94e89022da1fd802cec3a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 4 Jun 2024 15:53:35 +0200 Subject: [PATCH 1725/2451] Bleh. --- Penumbra/UI/FileDialogService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index e5b0fa19..88c0b00f 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -101,7 +101,7 @@ public class FileDialogService : IDisposable private static string HandleRoot(string path) { - if (path.Length == 2 && path[1] == ':') + if (path is [_, ':']) return path + '\\'; return path; From 03bfbcc3095042bdf7eccadc2494074bfe4e5189 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jun 2024 10:36:38 +0200 Subject: [PATCH 1726/2451] Fidx wrong group --- Penumbra/Mods/Editor/ModMerger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 32a207ff..8d47051c 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -105,7 +105,7 @@ public class ModMerger : IDisposable throw new Exception( $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); - foreach (var originalOption in group.DataContainers) + foreach (var originalOption in originalGroup.DataContainers) { var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); if (optionCreated) From ceed8531af0da494a9bd3909835e2f8a245491f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jun 2024 16:49:39 +0200 Subject: [PATCH 1727/2451] Fix GMP Entry edit. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index d4049bd9..0783dd98 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -640,7 +640,7 @@ public partial class ModEditWindow ImGui.SameLine(); if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkB })); + editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); } } From 2e9f1844546476713634c2343e4db2020544faad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jun 2024 17:26:25 +0200 Subject: [PATCH 1728/2451] Introduce Identifiers and strong entry types for each meta manipulation and use them in the manipulations. --- Penumbra/Collections/Cache/EstCache.cs | 38 +++--- Penumbra/Collections/Cache/MetaCache.cs | 4 +- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Import/Models/ModelManager.cs | 16 +-- .../Import/TexToolsMeta.Deserialization.cs | 12 +- Penumbra/Import/TexToolsMeta.Export.cs | 12 +- Penumbra/Import/TexToolsMeta.Rgsp.cs | 4 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 8 +- .../ResolveContext.PathResolution.cs | 14 +-- Penumbra/Meta/Files/CmpFile.cs | 11 +- Penumbra/Meta/Files/EstFile.cs | 44 +++---- Penumbra/Meta/Manipulations/Eqdp.cs | 87 ++++++++++++++ .../Meta/Manipulations/EqdpManipulation.cs | 23 ++-- Penumbra/Meta/Manipulations/Eqp.cs | 72 +++++++++++ .../Meta/Manipulations/EqpManipulation.cs | 14 ++- Penumbra/Meta/Manipulations/Est.cs | 113 ++++++++++++++++++ .../Meta/Manipulations/EstManipulation.cs | 36 +++--- Penumbra/Meta/Manipulations/Gmp.cs | 39 ++++++ .../Meta/Manipulations/GmpManipulation.cs | 12 +- Penumbra/Meta/Manipulations/Rsp.cs | 52 ++++++++ .../Meta/Manipulations/RspManipulation.cs | 22 ++-- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 6 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 16 +-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 6 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 23 ++-- Penumbra/UI/Classes/Combos.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 8 +- 27 files changed, 533 insertions(+), 163 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/Eqdp.cs create mode 100644 Penumbra/Meta/Manipulations/Eqp.cs create mode 100644 Penumbra/Meta/Manipulations/Est.cs create mode 100644 Penumbra/Meta/Manipulations/Gmp.cs create mode 100644 Penumbra/Meta/Manipulations/Rsp.cs diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 2552cd4a..3a0b4695 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -48,33 +48,33 @@ public struct EstCache : IDisposable } } - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type) { var (file, idx) = type switch { - EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst), - EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst), - EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst), - EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst), + EstType.Face => (_estFaceFile, MetaIndex.FaceEst), + EstType.Hair => (_estHairFile, MetaIndex.HairEst), + EstType.Body => (_estBodyFile, MetaIndex.BodyEst), + EstType.Head => (_estHeadFile, MetaIndex.HeadEst), _ => (null, 0), }; return manager.TemporarilySetFile(file, idx); } - private readonly EstFile? GetEstFile(EstManipulation.EstType type) + private readonly EstFile? GetEstFile(EstType type) { return type switch { - EstManipulation.EstType.Face => _estFaceFile, - EstManipulation.EstType.Hair => _estHairFile, - EstManipulation.EstType.Body => _estBodyFile, - EstManipulation.EstType.Head => _estHeadFile, + EstType.Face => _estFaceFile, + EstType.Hair => _estHairFile, + EstType.Body => _estBodyFile, + EstType.Head => _estHeadFile, _ => null, }; } - internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) + internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId) { var file = GetEstFile(type); return file != null @@ -96,10 +96,10 @@ public struct EstCache : IDisposable _estManipulations.AddOrReplace(m); var file = m.Slot switch { - EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair), - EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face), - EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body), - EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head), + EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair), + EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face), + EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body), + EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head), _ => throw new ArgumentOutOfRangeException(), }; return m.Apply(file); @@ -114,10 +114,10 @@ public struct EstCache : IDisposable var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); var file = m.Slot switch { - EstManipulation.EstType.Hair => _estHairFile!, - EstManipulation.EstType.Face => _estFaceFile!, - EstManipulation.EstType.Body => _estBodyFile!, - EstManipulation.EstType.Head => _estHeadFile!, + EstType.Hair => _estHairFile!, + EstType.Face => _estFaceFile!, + EstType.Body => _estBodyFile!, + EstType.Head => _estHeadFile!, _ => throw new ArgumentOutOfRangeException(), }; return manip.Apply(file); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index f42b72fc..fbca9c0e 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -188,7 +188,7 @@ public class MetaCache : IDisposable, IEnumerable _cmpCache.TemporarilySetFiles(_manager); - public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(EstType type) => _estCache.TemporarilySetFiles(_manager, type); public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) @@ -208,7 +208,7 @@ public class MetaCache : IDisposable, IEnumerable _estCache.GetEstEntry(_manager, type, genderRace, primaryId); /// Use this when CharacterUtility becomes ready. diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3f3733e0..484d4dd2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -112,7 +112,7 @@ public partial class ModCollection => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); - public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 485a76a7..fdd28ef1 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -63,16 +63,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return info.ObjectType switch { ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)], ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)], ObjectType.Equipment => [baseSkeleton], ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)], ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -81,7 +81,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); @@ -96,13 +96,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? 0; + ?? EstEntry.Zero; // If there's no entries, we can assume that there's no additional skeleton. - if (targetId == 0) + if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 554cf848..f6157747 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -75,14 +75,14 @@ public partial class TexToolsMeta { var gr = (GenderRace)reader.ReadUInt16(); var id = reader.ReadUInt16(); - var value = reader.ReadUInt16(); + var value = new EstEntry(reader.ReadUInt16()); var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch { - (BodySlot.Face, _) => EstManipulation.EstType.Face, - (BodySlot.Hair, _) => EstManipulation.EstType.Hair, - (_, EquipSlot.Head) => EstManipulation.EstType.Head, - (_, EquipSlot.Body) => EstManipulation.EstType.Body, - _ => (EstManipulation.EstType)0, + (BodySlot.Face, _) => EstType.Face, + (BodySlot.Hair, _) => EstType.Hair, + (_, EquipSlot.Head) => EstType.Head, + (_, EquipSlot.Body) => EstType.Body, + _ => (EstType)0, }; if (!gr.IsValid() || type == 0) continue; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 09bd2c12..4fb56df6 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -66,7 +66,7 @@ public partial class TexToolsMeta foreach (var attribute in attributes) { var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); - b.Write(value); + b.Write(value.Value); } } @@ -176,7 +176,7 @@ public partial class TexToolsMeta { b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); b.Write(manip.Est.SetId.Id); - b.Write(manip.Est.Entry); + b.Write(manip.Est.Entry.Value); } break; @@ -239,10 +239,10 @@ public partial class TexToolsMeta var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); return manip.Slot switch { - EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", - EstManipulation.EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", - EstManipulation.EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", - EstManipulation.EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", + EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", + EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", + EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", _ => throw new ArgumentOutOfRangeException(), }; } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 51faa175..71b9165f 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -46,8 +46,8 @@ public partial class TexToolsMeta void Add(RspAttribute attribute, float value) { var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def) - ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, value)); + if (keepDefault || value != def.Value) + ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value))); } if (gender == 1) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8118343d..9a68160b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -212,10 +212,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable if (_parent.InInternalResolve) return DisposableContainer.Empty; - return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Head)); + return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head)); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 236c7051..2b87e688 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -250,30 +250,30 @@ internal partial record ResolveContext _ => 0, }; } - return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId); + return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId); case 2: - return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId); + return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); case 3: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head); + return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); case 4: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body); + return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); default: return (0, string.Empty, 0); } } - private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) + private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstType type) { var human = (Human*)CharacterBase; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) + private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; - return (raceCode, EstManipulation.ToName(type), skeletonSet); + return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId); } private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index b265a5e8..96cda496 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -2,6 +2,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Interop.Services; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -17,10 +18,10 @@ public sealed unsafe class CmpFile : MetaBaseFile private const int RacialScalingStart = 0x2A800; - public float this[SubRace subRace, RspAttribute attribute] + public RspEntry this[SubRace subRace, RspAttribute attribute] { - get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); - set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; + get => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + set => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; } public override void Reset() @@ -39,10 +40,10 @@ public sealed unsafe class CmpFile : MetaBaseFile Reset(); } - public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) + public static RspEntry GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } private static int ToRspIndex(SubRace subRace) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index af441b22..ee38ea1e 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -34,26 +34,26 @@ public sealed unsafe class EstFile : MetaBaseFile Removed, } - public ushort this[GenderRace genderRace, ushort setId] + public EstEntry this[GenderRace genderRace, PrimaryId setId] { get { var (idx, exists) = FindEntry(genderRace, setId); if (!exists) - return 0; + return EstEntry.Zero; - return *(ushort*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); + return *(EstEntry*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); } set => SetEntry(genderRace, setId, value); } - private void InsertEntry(int idx, GenderRace genderRace, ushort setId, ushort skeletonId) + private void InsertEntry(int idx, GenderRace genderRace, PrimaryId setId, EstEntry skeletonId) { if (Length < Size + EntryDescSize + EntrySize) ResizeResources(Length + IncreaseSize); var control = (Info*)(Data + 4); - var entries = (ushort*)(control + Count); + var entries = (EstEntry*)(control + Count); for (var i = Count - 1; i >= idx; --i) entries[i + 3] = entries[i]; @@ -94,10 +94,10 @@ public sealed unsafe class EstFile : MetaBaseFile [StructLayout(LayoutKind.Sequential, Size = 4)] private struct Info : IComparable { - public readonly ushort SetId; + public readonly PrimaryId SetId; public readonly GenderRace GenderRace; - public Info(GenderRace gr, ushort setId) + public Info(GenderRace gr, PrimaryId setId) { GenderRace = gr; SetId = setId; @@ -106,42 +106,42 @@ public sealed unsafe class EstFile : MetaBaseFile public int CompareTo(Info other) { var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); - return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo(other.SetId); + return genderRaceComparison != 0 ? genderRaceComparison : SetId.Id.CompareTo(other.SetId.Id); } } - private static (int, bool) FindEntry(ReadOnlySpan data, GenderRace genderRace, ushort setId) + private static (int, bool) FindEntry(ReadOnlySpan data, GenderRace genderRace, PrimaryId setId) { var idx = data.BinarySearch(new Info(genderRace, setId)); return idx < 0 ? (~idx, false) : (idx, true); } - private (int, bool) FindEntry(GenderRace genderRace, ushort setId) + private (int, bool) FindEntry(GenderRace genderRace, PrimaryId setId) { var span = new ReadOnlySpan(Data + 4, Count); return FindEntry(span, genderRace, setId); } - public EstEntryChange SetEntry(GenderRace genderRace, ushort setId, ushort skeletonId) + public EstEntryChange SetEntry(GenderRace genderRace, PrimaryId setId, EstEntry skeletonId) { var (idx, exists) = FindEntry(genderRace, setId); if (exists) { - var value = *(ushort*)(Data + 4 * (Count + 1) + 2 * idx); + var value = *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx); if (value == skeletonId) return EstEntryChange.Unchanged; - if (skeletonId == 0) + if (skeletonId == EstEntry.Zero) { RemoveEntry(idx); return EstEntryChange.Removed; } - *(ushort*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; + *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; return EstEntryChange.Changed; } - if (skeletonId == 0) + if (skeletonId == EstEntry.Zero) return EstEntryChange.Unchanged; InsertEntry(idx, genderRace, setId, skeletonId); @@ -156,7 +156,7 @@ public sealed unsafe class EstFile : MetaBaseFile MemoryUtility.MemSet(Data + length, 0, Length - length); } - public EstFile(MetaFileManager manager, EstManipulation.EstType estType) + public EstFile(MetaFileManager manager, EstType estType) : base(manager, (MetaIndex)estType) { var length = DefaultData.Length; @@ -164,24 +164,24 @@ public sealed unsafe class EstFile : MetaBaseFile Reset(); } - public ushort GetDefault(GenderRace genderRace, ushort setId) + public EstEntry GetDefault(GenderRace genderRace, PrimaryId setId) => GetDefault(Manager, Index, genderRace, setId); - public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) { var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; var count = *(int*)data; var span = new ReadOnlySpan(data + 4, count); var (idx, found) = FindEntry(span, genderRace, primaryId.Id); if (!found) - return 0; + return EstEntry.Zero; - return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); + return *(EstEntry*)(data + 4 + count * EntryDescSize + idx * EntrySize); } - public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, primaryId); - public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); } diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs new file mode 100644 index 00000000..6d6942e6 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -0,0 +1,87 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, GenderRace GenderRace) + : IMetaIdentifier, IComparable +{ + public ModelRace Race + => GenderRace.Split().Item2; + + public Gender Gender + => GenderRace.Split().Item1; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); + + public MetaIndex FileIndex() + => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); + + public override string ToString() + => $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}"; + + public bool Validate() + { + var mask = Eqdp.Mask(Slot); + if (mask == 0) + return false; + + if (FileIndex() == (MetaIndex)(-1)) + return false; + + // No check for set id. + return true; + } + + public int CompareTo(EqdpIdentifier other) + { + var gr = GenderRace.CompareTo(other.GenderRace); + if (gr != 0) + return gr; + + var set = SetId.Id.CompareTo(other.SetId.Id); + if (set != 0) + return set; + + return Slot.CompareTo(other.Slot); + } + + public static EqdpIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var ret = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +public readonly record struct InternalEqdpEntry(bool Model, bool Material) +{ + private InternalEqdpEntry((bool, bool) val) + : this(val.Item1, val.Item2) + { } + + public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot) + : this(entry.ToBits(slot)) + { } + + + public EqdpEntry ToEntry(EquipSlot slot) + => Eqdp.FromSlotAndBits(slot, Model, Material); +} diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 0426dfce..2c01ce3f 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -10,27 +10,30 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct EqdpManipulation : IMetaManipulation { - public EqdpEntry Entry { get; private init; } + [JsonIgnore] + public EqdpIdentifier Identifier { get; private init; } + public EqdpEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender { get; private init; } + public Gender Gender + => Identifier.Gender; [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race { get; private init; } + public ModelRace Race + => Identifier.Race; - public PrimaryId SetId { get; private init; } + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot { get; private init; } + public EquipSlot Slot + => Identifier.Slot; [JsonConstructor] public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) { - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; - Entry = Eqdp.Mask(Slot) & entry; + Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); + Entry = Eqdp.Mask(Slot) & entry; } public EqdpManipulation Copy(EqdpManipulation entry) diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs new file mode 100644 index 00000000..572dc203 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); + + public MetaIndex FileIndex() + => MetaIndex.Eqp; + + public override string ToString() + => $"Eqp - {SetId} - {Slot}"; + + public bool Validate() + { + var mask = Eqp.Mask(Slot); + if (mask == 0) + return false; + + // No check for set id. + return true; + } + + public int CompareTo(EqpIdentifier other) + { + var set = SetId.Id.CompareTo(other.SetId.Id); + if (set != 0) + return set; + + return Slot.CompareTo(other.Slot); + } + + public static EqpIdentifier? FromJson(JObject jObj) + { + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var ret = new EqpIdentifier(setId, slot); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +public readonly record struct EqpEntryInternal(uint Value) +{ + public EqpEntryInternal(EqpEntry entry, EquipSlot slot) + : this(GetValue(entry, slot)) + { } + + public EqpEntry ToEntry(EquipSlot slot) + { + var (offset, mask) = Eqp.OffsetAndMask(slot); + return (EqpEntry)((ulong)Value << offset) & mask; + } + + private static uint GetValue(EqpEntry entry, EquipSlot slot) + { + var (offset, mask) = Eqp.OffsetAndMask(slot); + return (uint)((ulong)(entry & mask) >> offset); + } +} diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index d59938b6..3bced096 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -5,7 +5,6 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Util; -using SharpCompress.Common; namespace Penumbra.Meta.Manipulations; @@ -15,17 +14,20 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConverter(typeof(ForceNumericFlagEnumConverter))] public EqpEntry Entry { get; private init; } - public PrimaryId SetId { get; private init; } + public EqpIdentifier Identifier { get; private init; } + + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot { get; private init; } + public EquipSlot Slot + => Identifier.Slot; [JsonConstructor] public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) { - Slot = slot; - SetId = setId; - Entry = Eqp.Mask(slot) & entry; + Identifier = new EqpIdentifier(setId, slot); + Entry = Eqp.Mask(slot) & entry; } public EqpManipulation Copy(EqpEntry entry) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs new file mode 100644 index 00000000..9f878f97 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -0,0 +1,113 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public enum EstType : byte +{ + Hair = MetaIndex.HairEst, + Face = MetaIndex.FaceEst, + Body = MetaIndex.BodyEst, + Head = MetaIndex.HeadEst, +} + +public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, GenderRace GenderRace) + : IMetaIdentifier, IComparable +{ + public ModelRace Race + => GenderRace.Split().Item2; + + public Gender Gender + => GenderRace.Split().Item1; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + switch (Slot) + { + case EstType.Hair: + changedItems.TryAdd( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair (Hair) {SetId}", null); + break; + case EstType.Face: + changedItems.TryAdd( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face (Face) {SetId}", null); + break; + case EstType.Body: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Body)); + break; + case EstType.Head: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Head)); + break; + } + } + + public MetaIndex FileIndex() + => (MetaIndex)Slot; + + public override string ToString() + => $"Est - {SetId} - {Slot} - {GenderRace.ToName()}"; + + public bool Validate() + { + if (!Enum.IsDefined(Slot)) + return false; + if (GenderRace is GenderRace.Unknown || !Enum.IsDefined(GenderRace)) + return false; + + // No known check for set id. + return true; + } + + public int CompareTo(EstIdentifier other) + { + var gr = GenderRace.CompareTo(other.GenderRace); + if (gr != 0) + return gr; + + var id = SetId.Id.CompareTo(other.SetId.Id); + return id != 0 ? id : Slot.CompareTo(other.Slot); + } + + public static EstIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? 0; + var ret = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +[JsonConverter(typeof(Converter))] +public readonly record struct EstEntry(ushort Value) +{ + public static readonly EstEntry Zero = new(0); + + public PrimaryId AsId + => new(Value); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, EstEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override EstEntry ReadJson(JsonReader reader, Type objectType, EstEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index d3c92ad3..c3f9792f 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -10,14 +10,6 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct EstManipulation : IMetaManipulation { - public enum EstType : byte - { - Hair = MetaIndex.HairEst, - Face = MetaIndex.FaceEst, - Body = MetaIndex.BodyEst, - Head = MetaIndex.HeadEst, - } - public static string ToName(EstType type) => type switch { @@ -28,31 +20,33 @@ public readonly struct EstManipulation : IMetaManipulation _ => "unk", }; - public ushort Entry { get; private init; } // SkeletonIdx. + public EstIdentifier Identifier { get; private init; } + public EstEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender { get; private init; } + public Gender Gender + => Identifier.Gender; [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race { get; private init; } + public ModelRace Race + => Identifier.Race; - public PrimaryId SetId { get; private init; } + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EstType Slot { get; private init; } + public EstType Slot + => Identifier.Slot; [JsonConstructor] - public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, ushort entry) + public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) { - Entry = entry; - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; + Entry = entry; + Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); } - public EstManipulation Copy(ushort entry) + public EstManipulation Copy(EstEntry entry) => new(Gender, Race, Slot, SetId, entry); @@ -111,3 +105,5 @@ public readonly struct EstManipulation : IMetaManipulation return true; } } + + diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs new file mode 100644 index 00000000..1b7c70ba --- /dev/null +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + + public MetaIndex FileIndex() + => MetaIndex.Gmp; + + public override string ToString() + => $"Gmp - {SetId}"; + + public bool Validate() + // No known conditions. + => true; + + public int CompareTo(GmpIdentifier other) + => SetId.Id.CompareTo(other.SetId.Id); + + public static GmpIdentifier? FromJson(JObject jObj) + { + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var ret = new GmpIdentifier(setId); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + jObj["SetId"] = SetId.Id.ToString(); + return jObj; + } +} diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index ee58295d..0b2a9f4b 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -8,14 +8,18 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct GmpManipulation : IMetaManipulation { - public GmpEntry Entry { get; private init; } - public PrimaryId SetId { get; private init; } + public GmpIdentifier Identifier { get; private init; } + + public GmpEntry Entry { get; private init; } + + public PrimaryId SetId + => Identifier.SetId; [JsonConstructor] public GmpManipulation(GmpEntry entry, PrimaryId setId) { - Entry = entry; - SetId = setId; + Entry = entry; + Identifier = new GmpIdentifier(setId); } public GmpManipulation Copy(GmpEntry entry) diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs new file mode 100644 index 00000000..29cdfd71 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); + + public MetaIndex FileIndex() + => throw new NotImplementedException(); + + public bool Validate() + => throw new NotImplementedException(); + + public JObject AddToJson(JObject jObj) + => throw new NotImplementedException(); +} + +[JsonConverter(typeof(Converter))] +public readonly record struct RspEntry(float Value) : IComparisonOperators +{ + public const float MinValue = 0.01f; + public const float MaxValue = 512f; + public static readonly RspEntry One = new(1f); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override RspEntry ReadJson(JsonReader reader, Type objectType, RspEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } + + public static bool operator >(RspEntry left, RspEntry right) + => left.Value > right.Value; + + public static bool operator >=(RspEntry left, RspEntry right) + => left.Value >= right.Value; + + public static bool operator <(RspEntry left, RspEntry right) + => left.Value < right.Value; + + public static bool operator <=(RspEntry left, RspEntry right) + => left.Value <= right.Value; +} diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 7e5e3fcb..04691c9f 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -9,25 +9,25 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct RspManipulation : IMetaManipulation { - public const float MinValue = 0.01f; - public const float MaxValue = 512f; - public float Entry { get; private init; } + public RspIdentifier Identifier { get; private init; } + public RspEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public SubRace SubRace { get; private init; } + public SubRace SubRace + => Identifier.SubRace; [JsonConverter(typeof(StringEnumConverter))] - public RspAttribute Attribute { get; private init; } + public RspAttribute Attribute + => Identifier.Attribute; [JsonConstructor] - public RspManipulation(SubRace subRace, RspAttribute attribute, float entry) + public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) { - Entry = entry; - SubRace = subRace; - Attribute = attribute; + Entry = entry; + Identifier = new RspIdentifier(subRace, attribute); } - public RspManipulation Copy(float entry) + public RspManipulation Copy(RspEntry entry) => new(SubRace, Attribute, entry); public override string ToString() @@ -68,7 +68,7 @@ public readonly struct RspManipulation : IMetaManipulation return false; if (!Enum.IsDefined(Attribute)) return false; - if (Entry is < MinValue or > MaxValue) + if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) return false; return true; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index ea4ef7b1..3efee857 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -126,9 +126,9 @@ public static class EquipmentSwap var isAccessory = slot.IsAccessory(); var estType = slot switch { - EquipSlot.Head => EstManipulation.EstType.Head, - EquipSlot.Body => EstManipulation.EstType.Body, - _ => (EstManipulation.EstType)0, + EquipSlot.Head => EstType.Head, + EquipSlot.Body => EstType.Body, + _ => (EstType)0, }; var skipFemale = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index b269d89c..7fac52c1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -133,23 +133,23 @@ public static class ItemSwap } - public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, - GenderRace race, ushort estEntry) + public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, + GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } - public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, - GenderRace race, ushort estEntry) + public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, + GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, - Func manips, EstManipulation.EstType type, + Func manips, EstType type, GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) @@ -160,7 +160,7 @@ public static class ItemSwap var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); var est = new MetaSwap(manips, fromDefault, toDefault); - if (ownMdl && est.SwapApplied.Est.Entry >= 2) + if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2) { var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); est.ChildSwaps.Add(phyb); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 449405a0..48d687d0 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -152,9 +152,9 @@ public class ItemSwapContainer var mdl = CustomizationSwap.CreateMdl(manager, pathResolver, slot, race, from, to); var type = slot switch { - BodySlot.Hair => EstManipulation.EstType.Hair, - BodySlot.Face => EstManipulation.EstType.Face, - _ => (EstManipulation.EstType)0, + BodySlot.Hair => EstType.Hair, + BodySlot.Face => EstType.Face, + _ => (EstType)0, }; var metaResolver = MetaResolver(collection); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 0783dd98..b0a74637 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -453,7 +453,7 @@ public partial class ModEditWindow private static class EstRow { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); + private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); private static float IdWidth => 100 * UiHelpers.Scale; @@ -510,7 +510,7 @@ public partial class ModEditWindow // Values using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); - IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); + IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f); } public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) @@ -538,9 +538,9 @@ public partial class ModEditWindow // Values var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, + if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, out var entry, 0, ushort.MaxValue, 0.05f)) - editor.MetaEditor.Change(meta.Copy((ushort)entry)); + editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); } } @@ -646,7 +646,7 @@ public partial class ModEditWindow private static class RspRow { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); + private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); private static float FloatWidth => 150 * UiHelpers.Scale; @@ -680,7 +680,8 @@ public partial class ModEditWindow using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(FloatWidth); - ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); + var value = defaultEntry.Value; + ImGui.DragFloat("##rspValue", ref value, 0f); } public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) @@ -699,15 +700,15 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute); - var value = meta.Entry; + var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; + var value = meta.Entry.Value; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) - && value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) - editor.MetaEditor.Change(meta.Copy(value)); + if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue) + && value is >= RspEntry.MinValue and <= RspEntry.MaxValue) + editor.MetaEditor.Change(meta.Copy(new RspEntry(value))); ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); } diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 253bf0e0..234f7a3e 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -33,7 +33,7 @@ public static class Combos => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1); - public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute, float unscaledWidth = 200) + public static bool EstSlot(string label, EstType current, out EstType attribute, float unscaledWidth = 200) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute); public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110) diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 7368c7c8..0647ea8e 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -51,18 +51,18 @@ public static class IdentifierExtensions case MetaManipulation.Type.Est: switch (manip.Est.Slot) { - case EstManipulation.EstType.Hair: + case EstType.Hair: changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); break; - case EstManipulation.EstType.Face: + case EstType.Face: changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); break; - case EstManipulation.EstType.Body: + case EstType.Body: identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), EquipSlot.Body)); break; - case EstManipulation.EstType.Head: + case EstType.Head: identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), EquipSlot.Head)); From 50a7e7efb7f0ea41583ab04b1dd6dd16ca2f992e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jun 2024 16:34:09 +0200 Subject: [PATCH 1729/2451] Add more filter options. --- OtterGui | 2 +- .../Meta/Manipulations/ImcManipulation.cs | 1 - Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 106 ++++---------- .../UI/ModsTab/ModSearchStringSplitter.cs | 138 ++++++++++++++++++ 4 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ModSearchStringSplitter.cs diff --git a/OtterGui b/OtterGui index 5de708b2..ac176daf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323 +Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460 diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index eb3720c9..5065a06e 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 5b6cfa99..58f0b615 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -23,17 +23,20 @@ namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector { - private readonly CommunicatorService _communicator; - private readonly MessageService _messager; - private readonly Configuration _config; - private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; - private readonly CollectionManager _collectionManager; - private readonly TutorialService _tutorial; - private readonly ModImportManager _modImportManager; - private readonly IDragDropManager _dragDrop; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + private readonly CommunicatorService _communicator; + private readonly MessageService _messager; + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly TutorialService _tutorial; + private readonly ModImportManager _modImportManager; + private readonly IDragDropManager _dragDrop; + private readonly ModSearchStringSplitter Filter = new(); + + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, @@ -568,78 +571,49 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. protected override bool ChangeFilter(string filterValue) { - (_modFilter, _filterType) = filterValue.Length switch - { - 0 => (LowerString.Empty, -1), - > 1 when filterValue[1] == ':' => - filterValue[0] switch - { - 'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), - 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), - 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), - 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), - 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), - 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), - 's' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), - 'S' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), - _ => (new LowerString(filterValue), 0), - }, - _ => (new LowerString(filterValue), 0), - }; - + Filter.Parse(filterValue); return true; } - private const int EmptyOffset = 128; - - private (LowerString, int) ParseFilter(string value, int id) - { - value = value[2..]; - var lower = new LowerString(value); - if (id == 5 && !ChangedItemDrawer.TryParsePartial(lower.Lower, out _slotFilter)) - _slotFilter = 0; - - return (lower, lower.Lower is "none" ? id + EmptyOffset : id); - } - - /// /// Check the state filter for a specific pair of has/has-not flags. /// Uses count == 0 to check for has-not and count != 0 for has. /// Returns true if it should be filtered and false if not. /// private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag) - { - return count switch + => count switch { 0 when _stateFilter.HasFlag(hasNoFlag) => false, 0 => true, _ when _stateFilter.HasFlag(hasFlag) => false, _ => true, }; - } /// /// The overwritten filter method also computes the state. @@ -653,7 +627,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0 && !f.FullName().Contains(FilterValue, IgnoreCase); + || !Filter.IsVisible(f); } return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state); @@ -661,23 +635,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Apply the string filters. private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) - { - return _filterType switch - { - -1 => false, - 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), - 1 => !mod.Name.Contains(_modFilter), - 2 => !mod.Author.Contains(_modFilter), - 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), - 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), - 5 => mod.ChangedItems.All(p => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & _slotFilter) == 0), - 2 + EmptyOffset => !mod.Author.IsEmpty, - 3 + EmptyOffset => mod.LowerChangedItemsString.Length > 0, - 4 + EmptyOffset => mod.AllTagsLower.Length > 0, - 5 + EmptyOffset => mod.ChangedItems.Count == 0, - _ => false, // Should never happen - }; - } + => !Filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs new file mode 100644 index 00000000..1ea70731 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -0,0 +1,138 @@ +using OtterGui.Filesystem; +using OtterGui.Filesystem.Selector; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ModsTab; + +public enum ModSearchType : byte +{ + Default = 0, + ChangedItem, + Tag, + Name, + Author, + Category, +} + +public sealed class ModSearchStringSplitter : SearchStringSplitter.Leaf, ModSearchStringSplitter.Entry> +{ + public readonly struct Entry : ISplitterEntry + { + public string Needle { get; init; } + public ModSearchType Type { get; init; } + public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; } + + public bool Contains(Entry other) + { + if (Type != other.Type) + return false; + if (Type is ModSearchType.Category) + return IconFilter == other.IconFilter; + + return Needle.Contains(other.Needle); + } + } + + protected override bool ConvertToken(char token, out ModSearchType val) + { + val = token switch + { + 'c' or 'C' => ModSearchType.ChangedItem, + 't' or 'T' => ModSearchType.Tag, + 'n' or 'N' => ModSearchType.Name, + 'a' or 'A' => ModSearchType.Author, + 's' or 'S' => ModSearchType.Category, + _ => ModSearchType.Default, + }; + return val is not ModSearchType.Default; + } + + protected override bool AllowsNone(ModSearchType val) + => val switch + { + ModSearchType.Author => true, + ModSearchType.ChangedItem => true, + ModSearchType.Tag => true, + ModSearchType.Category => true, + _ => false, + }; + + protected override void PostProcessing() + { + base.PostProcessing(); + HandleList(General); + HandleList(Forced); + HandleList(Negated); + return; + + static void HandleList(List list) + { + for (var i = 0; i < list.Count; ++i) + { + var entry = list[i]; + if (entry.Type is not ModSearchType.Category) + continue; + + if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon)) + list[i] = entry with + { + IconFilter = icon, + Needle = string.Empty, + }; + else + list.RemoveAt(i--); + } + } + } + + public bool IsVisible(ModFileSystem.Folder folder) + { + switch (State) + { + case FilterState.NoFilters: return true; + case FilterState.NoMatches: return false; + } + + var fullName = folder.FullName(); + return Forced.All(i => MatchesName(i, folder.Name, fullName)) + && !Negated.Any(i => MatchesName(i, folder.Name, fullName)) + && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName))); + } + + protected override bool Matches(Entry entry, ModFileSystem.Leaf leaf) + => entry.Type switch + { + ModSearchType.Default => leaf.FullName().AsSpan().Contains(entry.Needle, StringComparison.OrdinalIgnoreCase) + || leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.ChangedItem => leaf.Value.LowerChangedItemsString.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Tag => leaf.Value.AllTagsLower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Name => leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Category => leaf.Value.ChangedItems.Any(p + => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0), + _ => true, + }; + + protected override bool MatchesNone(ModSearchType type, bool negated, ModFileSystem.Leaf haystack) + => type switch + { + ModSearchType.Author when negated => !haystack.Value.Author.IsEmpty, + ModSearchType.Author => haystack.Value.Author.IsEmpty, + ModSearchType.ChangedItem when negated => haystack.Value.LowerChangedItemsString.Length > 0, + ModSearchType.ChangedItem => haystack.Value.LowerChangedItemsString.Length == 0, + ModSearchType.Tag when negated => haystack.Value.AllTagsLower.Length > 0, + ModSearchType.Tag => haystack.Value.AllTagsLower.Length == 0, + ModSearchType.Category when negated => haystack.Value.ChangedItems.Count > 0, + ModSearchType.Category => haystack.Value.ChangedItems.Count == 0, + _ => true, + }; + + private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName) + => entry.Type switch + { + ModSearchType.Default => fullName.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), + ModSearchType.Name => name.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), + _ => false, + }; +} From 102e7335a7303526a6ae71c5f89194538c8c9f56 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 7 Jun 2024 14:40:25 +0000 Subject: [PATCH 1730/2451] [CI] Updating repo.json for testing_1.1.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5e9e5b37..5d6a9ed8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.0.2", - "TestingAssemblyVersion": "1.1.0.2", + "TestingAssemblyVersion": "1.1.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From de0309bfa7abcc925209086cbf88360bfaee1ee0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 12:45:12 +0200 Subject: [PATCH 1731/2451] Fix issue with ImcGroup settings and IPC. --- Penumbra/Api/Api/ModSettingsApi.cs | 27 +++++++++++++------------- Penumbra/Mods/Manager/ModDataEditor.cs | 6 ++---- Penumbra/Mods/Settings/ModSettings.cs | 4 ++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 56b80e63..e046ce30 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -77,10 +77,10 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Id == Guid.Empty - ? null - : ignoreInheritance - ? collection.Settings[mod.Index] + var settings = collection.Id == Guid.Empty + ? null + : ignoreInheritance + ? collection.Settings[mod.Index] : collection[mod.Index].Settings; if (settings == null) return (PenumbraApiEc.Success, null); @@ -160,11 +160,11 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - var setting = mod.Groups[groupIdx] switch + var setting = mod.Groups[groupIdx].Behaviour switch { - MultiModGroup => Setting.Multi(optionIdx), - SingleModGroup => Setting.Single(optionIdx), - _ => Setting.Zero, + GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx), + GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx), + _ => Setting.Zero, }; var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success @@ -191,20 +191,20 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var setting = Setting.Zero; switch (mod.Groups[groupIdx]) { - case SingleModGroup single: + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: { - var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]); + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); setting = Setting.Single(optionIdx); break; } - case MultiModGroup multi: + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: { foreach (var name in optionNames) { - var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name); + var optionIdx = multi.Options.IndexOf(o => o.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -256,7 +256,8 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int moveIndex) { switch (type) { diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index e0af6f36..c7c7c2cc 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -50,7 +50,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var save = true; if (File.Exists(dataFile)) { - save = false; try { var text = File.ReadAllText(dataFile); @@ -60,6 +59,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; note = json[nameof(Mod.Note)]?.Value() ?? note; localTags = json[nameof(Mod.LocalTags)]?.Values().OfType() ?? localTags; + save = false; } catch (Exception e) { @@ -239,7 +239,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Favorite = state; saveService.QueueSave(new ModLocalData(mod)); - ; communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -250,7 +249,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Note = newNote; saveService.QueueSave(new ModLocalData(mod)); - ; communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -260,7 +258,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic if (tagIdx < 0 || tagIdx > which.Count) return; - ModDataChangeType flags = 0; + ModDataChangeType flags; if (tagIdx == which.Count) { flags = ModLocalData.UpdateTags(mod, local ? null : which.Append(newTag), local ? which.Append(newTag) : null); diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 7fe48365..25e4805d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -185,10 +185,10 @@ public class ModSettings switch (mod.Groups[idx]) { - case SingleModGroup single when setting.Value < (ulong)single.Options.Count: + case { Behaviour: GroupDrawBehaviour.SingleSelection } single when setting.Value < (ulong)single.Options.Count: dict.Add(single.Name, [single.Options[setting.AsIndex].Name]); break; - case MultiModGroup multi: + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList(); dict.Add(multi.Name, list); break; From e884b269a9ca3f2ea4c6ef63cf7da9e53d1353cb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 14:55:42 +0200 Subject: [PATCH 1732/2451] Add a version field to mod group files. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 05437e3d..c82c67c7 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -6,6 +6,8 @@ namespace Penumbra.Mods.Groups; public readonly struct ModSaveGroup : ISavable { + public const int CurrentVersion = 0; + private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; @@ -71,6 +73,8 @@ public readonly struct ModSaveGroup : ISavable j.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; j.WriteStartObject(); + j.WritePropertyName("Version"); + j.WriteValue(CurrentVersion); if (_groupIdx >= 0) _group!.WriteJson(j, serializer, _basePath); else From 159942f29c229f3dafef7275c553cc9f7e2183b6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 20:33:07 +0200 Subject: [PATCH 1733/2451] Add by-name identification in the lobby. --- .../PathResolving/CollectionResolver.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index aea58304..ed111794 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -7,6 +8,8 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.String; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -94,14 +97,8 @@ public sealed unsafe class CollectionResolver( public bool IsModelHuman(uint modelCharaId) => humanModels.IsHuman(modelCharaId); - /// Return whether the given character has a human model. - public bool IsModelHuman(Character* character) - => character != null && IsModelHuman((uint)character->CharacterData.ModelCharaId); - /// - /// Used if on the Login screen. Names are populated after actors are drawn, - /// so it is not possible to fetch names from the ui list. - /// Actors are also not named. So use Yourself > Players > Racial > Default. + /// Used if on the Login screen. /// private bool LoginScreen(GameObject* gameObject, out ResolveData ret) { @@ -114,6 +111,27 @@ public sealed unsafe class CollectionResolver( } var notYetReady = false; + var lobby = AgentLobby.Instance(); + if (lobby != null) + { + var span = lobby->LobbyData.CharaSelectEntries.Span; + // The lobby uses the first 8 cutscene actors. + var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; + if (idx >= 0 && idx < span.Length && span[idx].Value != null) + { + var item = span[idx].Value; + var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); + Penumbra.Log.Verbose( + $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); + if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll) + { + // Do not add this to caches because game objects are reused for different draw objects. + ret = coll.ToResolveData(gameObject); + return true; + } + } + } + var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? collectionManager.Active.Default; @@ -189,7 +207,7 @@ public sealed unsafe class CollectionResolver( return null; // Only handle human models. - + if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return null; From f51fc2cafd985cf3b3cca605f74b85639cf8036f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:05:19 +0200 Subject: [PATCH 1734/2451] Allow root directory overwriting with case sensitivity. --- Penumbra/Mods/Manager/ModManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index adaca85e..010cad19 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -266,7 +266,7 @@ public sealed class ModManager : ModStorage, IDisposable /// private void SetBaseDirectory(string newPath, bool firstTime) { - if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.Ordinal)) return; if (newPath.Length == 0) From f6b35497c5610acc00663ea31e9c69190ff3e357 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:05:37 +0200 Subject: [PATCH 1735/2451] Change path comparison for AddMod. --- Penumbra/Api/Api/ModsApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 16dd8be9..548831d5 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -75,7 +75,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (!dir.Exists) return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); - if (_modManager.BasePath.FullName != dir.Parent?.FullName) + if (dir.Parent == null || Path.GetFullPath(_modManager.BasePath.FullName) != Path.GetFullPath(dir.Parent.FullName)) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); From f7adc83d631936bf76446af1205a333baa260c44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:06:18 +0200 Subject: [PATCH 1736/2451] Fix issue with preview of file in advanced model editing. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index a7d39c6e..bbed64b7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Components; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; @@ -27,44 +26,56 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; private readonly ModelManager _models; + private class LoadedData + { + public MdlFile LastFile = null!; + public readonly List SubMeshAttributeTags = []; + public long[] LodTriCount = []; + } + private string _modelNewMaterial = string.Empty; - private readonly List _subMeshAttributeTagWidgets = []; + + private readonly LoadedData _main = new(); + private readonly LoadedData _preview = new(); + private string _customPath = string.Empty; private Utf8GamePath _customGamePath = Utf8GamePath.Empty; - private MdlFile _lastFile = null!; - private long[] _lodTriCount = []; - private void UpdateFile(MdlFile file, bool force) + + + private LoadedData UpdateFile(MdlFile file, bool force, bool disabled) { - if (file == _lastFile && !force) - return; + var data = disabled ? _preview : _main; + if (file == data.LastFile && !force) + return data; - _lastFile = file; + data.LastFile = file; var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_subMeshAttributeTagWidgets.Count != subMeshTotal) + if (data.SubMeshAttributeTags.Count != subMeshTotal) { - _subMeshAttributeTagWidgets.Clear(); - _subMeshAttributeTagWidgets.AddRange( + data.SubMeshAttributeTags.Clear(); + data.SubMeshAttributeTags.AddRange( Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) ); } - _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + data.LodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + return data; } private bool DrawModelPanel(MdlTab tab, bool disabled) { var ret = tab.Dirty; - UpdateFile(tab.Mdl, ret); + var data = UpdateFile(tab.Mdl, ret, disabled); DrawImportExport(tab, disabled); ret |= DrawModelMaterialDetails(tab, disabled); - if (ImGui.CollapsingHeader($"Meshes ({_lastFile.Meshes.Length})###meshes")) - for (var i = 0; i < _lastFile.LodCount; ++i) + if (ImGui.CollapsingHeader($"Meshes ({data.LastFile.Meshes.Length})###meshes")) + for (var i = 0; i < data.LastFile.LodCount; ++i) ret |= DrawModelLodDetails(tab, i, disabled); - ret |= DrawOtherModelDetails(disabled); + ret |= DrawOtherModelDetails(data); return !disabled && ret; } @@ -98,7 +109,7 @@ public partial class ModEditWindow return true; }); - using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) + using (ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes); @@ -232,7 +243,7 @@ public partial class ModEditWindow if (!ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) return; - if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) + if (!Utf8GamePath.FromString(_customPath, out _customGamePath)) _customGamePath = Utf8GamePath.Empty; } @@ -399,9 +410,9 @@ public partial class ModEditWindow return ret; } - private void DrawInvalidMaterialMarker() + private static void DrawInvalidMaterialMarker() { - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); ImGuiUtil.HoverTooltip( @@ -534,7 +545,8 @@ public partial class ModEditWindow ImGui.TextUnformatted($"Attributes #{subMeshOffset + 1}"); ImGui.TableNextColumn(); - var widget = _subMeshAttributeTagWidgets[subMeshIndex]; + var data = disabled ? _preview : _main; + var widget = data.SubMeshAttributeTags[subMeshIndex]; var attributes = tab.GetSubMeshAttributes(subMeshIndex); if (attributes == null) @@ -555,7 +567,7 @@ public partial class ModEditWindow return true; } - private bool DrawOtherModelDetails(bool _) + private bool DrawOtherModelDetails(LoadedData data) { using var header = ImRaii.CollapsingHeader("Further Content"); if (!header) @@ -566,44 +578,44 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}"); + ImGuiUtil.DrawTableColumn($"0x{data.LastFile.Version:X}"); ImGuiUtil.DrawTableColumn("Radius"); - ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); - ImGuiUtil.DrawTableColumn(_lastFile.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Shadow Clip Out Distance"); - ImGuiUtil.DrawTableColumn(_lastFile.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("LOD Count"); - ImGuiUtil.DrawTableColumn(_lastFile.LodCount.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.LodCount.ToString()); ImGuiUtil.DrawTableColumn("Enable Index Buffer Streaming"); - ImGuiUtil.DrawTableColumn(_lastFile.EnableIndexBufferStreaming.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.EnableIndexBufferStreaming.ToString()); ImGuiUtil.DrawTableColumn("Enable Edge Geometry"); - ImGuiUtil.DrawTableColumn(_lastFile.EnableEdgeGeometry.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.EnableEdgeGeometry.ToString()); ImGuiUtil.DrawTableColumn("Flags 1"); - ImGuiUtil.DrawTableColumn(_lastFile.Flags1.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Flags1.ToString()); ImGuiUtil.DrawTableColumn("Flags 2"); - ImGuiUtil.DrawTableColumn(_lastFile.Flags2.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Flags2.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Bounding Boxes"); - ImGuiUtil.DrawTableColumn(_lastFile.BoneBoundingBoxes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.BoneBoundingBoxes.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Tables"); - ImGuiUtil.DrawTableColumn(_lastFile.BoneTables.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.BoneTables.Length.ToString()); ImGuiUtil.DrawTableColumn("Element IDs"); - ImGuiUtil.DrawTableColumn(_lastFile.ElementIds.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ElementIds.Length.ToString()); ImGuiUtil.DrawTableColumn("Extra LoDs"); - ImGuiUtil.DrawTableColumn(_lastFile.ExtraLods.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ExtraLods.Length.ToString()); ImGuiUtil.DrawTableColumn("Meshes"); - ImGuiUtil.DrawTableColumn(_lastFile.Meshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Meshes.Length.ToString()); ImGuiUtil.DrawTableColumn("Shape Meshes"); - ImGuiUtil.DrawTableColumn(_lastFile.ShapeMeshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ShapeMeshes.Length.ToString()); ImGuiUtil.DrawTableColumn("LoDs"); - ImGuiUtil.DrawTableColumn(_lastFile.Lods.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Lods.Length.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Stack Size"); - ImGuiUtil.DrawTableColumn(_lastFile.StackSize.ToString()); - foreach (var (triCount, lod) in _lodTriCount.WithIndex()) + ImGuiUtil.DrawTableColumn(data.LastFile.StackSize.ToString()); + foreach (var (triCount, lod) in data.LodTriCount.WithIndex()) { ImGuiUtil.DrawTableColumn($"LOD #{lod + 1} Triangle Count"); ImGuiUtil.DrawTableColumn(triCount.ToString()); @@ -614,36 +626,36 @@ public partial class ModEditWindow using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen)) { if (materials) - foreach (var material in _lastFile.Materials) + foreach (var material in data.LastFile.Materials) ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) - foreach (var attribute in _lastFile.Attributes) + foreach (var attribute in data.LastFile.Attributes) ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { if (bones) - foreach (var bone in _lastFile.Bones) + foreach (var bone in data.LastFile.Bones) ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { if (shapes) - foreach (var shape in _lastFile.Shapes) + foreach (var shape in data.LastFile.Shapes) ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); } - if (_lastFile.RemainingData.Length > 0) + if (data.LastFile.RemainingData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {_lastFile.RemainingData.Length})###AdditionalData"); + using var t = ImRaii.TreeNode($"Additional Data (Size: {data.LastFile.RemainingData.Length})###AdditionalData"); if (t) - Widget.DrawHexViewer(_lastFile.RemainingData); + Widget.DrawHexViewer(data.LastFile.RemainingData); } return false; From ecd5752d16d14f64ed15775a43459b2006c78593 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:12:55 +0200 Subject: [PATCH 1737/2451] Make imc attribute letter tooltip appear on disabled. --- Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index d346e05c..5c8edce6 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -148,7 +148,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr } } - ImUtf8.HoverTooltip("ABCDEFGHIJ"u8.Slice(i, 1)); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1)); if (i != 9) ImUtf8.SameLineInner(); } From 30a87e3f402495517d067235f8235fbf46c3ba81 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:28:33 +0200 Subject: [PATCH 1738/2451] Update valid world check. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index ac176daf..e95c0f04 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460 +Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8 diff --git a/Penumbra.GameData b/Penumbra.GameData index ad12ddce..6aeae346 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ad12ddcef38a9ed4e4dd7424d748f41c4b97db10 +Subproject commit 6aeae346332a255b7575ccfca554afb0f3cf1494 From 863a7edf0ee57c2e5abadb59cad8cd4d85e5272d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:50:13 +0200 Subject: [PATCH 1739/2451] Add changelog. --- Penumbra/Penumbra.cs | 1 - Penumbra/UI/Changelog.cs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 42be0aa3..3bbfdf65 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -20,7 +20,6 @@ using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; -using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra; diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 3f5a446a..184633f2 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -50,10 +50,39 @@ public class PenumbraChangelog AddDummy(Changelog); AddDummy(Changelog); Add1_1_0_0(Changelog); - } - + Add1_1_1_0(Changelog); + } + #region Changelogs + private static void Add1_1_1_0(Changelog log) + => log.NextVersion("Version 1.1.1.0") + .RegisterHighlight("Filtering for mods is now tokenized and can filter for multiple things at once, or exclude specific things.") + .RegisterEntry("Hover over the filter to see the new available options in the tooltip.", 1) + .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) + .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) + .RegisterHighlight("Added initial identification of characters in the login-screen by name.") + .RegisterEntry("Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", 1) + .RegisterEntry("Added functionality for IMC groups to apply to all variants for a model instead of a specific one.") + .RegisterEntry("Improved the resource tree view with filters and incognito mode. (by Ny)") + .RegisterEntry("Added a tooltip to the global EQP condition.") + .RegisterEntry("Fixed the new worlds not being identified correctly because Square Enix could not be bothered to turn them public.") + .RegisterEntry("Fixed model import getting stuck when doing weight adjustments. (by ackwell)") + .RegisterEntry("Fixed an issue with dye previews in the material editor not applying.") + .RegisterEntry("Fixed an issue with collections not saving on renames.") + .RegisterEntry("Fixed an issue parsing collections with settings set to negative values, which should now be set to 0.") + .RegisterEntry("Fixed an issue with the accessory VFX addition.") + .RegisterEntry("Fixed an issue with GMP animation type entries.") + .RegisterEntry("Fixed another issue with the mod merger.") + .RegisterEntry("Fixed an issue with IMC groups and IPC.") + .RegisterEntry("Fixed some issues with the capitalization of the root directory.") + .RegisterEntry("Fixed IMC attribute tooltips not appearing for disabled checkboxes.") + .RegisterEntry("Added GetChangedItems IPC for single mods. (1.1.0.2)") + .RegisterEntry("Fixed an issue with creating unnamed collections. (1.1.0.2)") + .RegisterEntry("Fixed an issue with the mod merger. (1.1.0.2)") + .RegisterEntry("Fixed the global EQP entry for rings checking for bracelets instead of rings. (1.1.0.2)") + .RegisterEntry("Fixed an issue with newly created collections not being added to the collection list. (1.1.0.1)"); + private static void Add1_1_0_0(Changelog log) => log.NextVersion("Version 1.1.0.0") .RegisterImportant( From c8ea33f8dddff7a8ee7bc92791a54c3d20c45f12 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 11 Jun 2024 10:52:49 +0000 Subject: [PATCH 1740/2451] [CI] Updating repo.json for 1.1.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5d6a9ed8..05de7ec7 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.0.2", - "TestingAssemblyVersion": "1.1.0.3", + "AssemblyVersion": "1.1.1.0", + "TestingAssemblyVersion": "1.1.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 447735f6091db767d5d06a39cb8959b3aaebc279 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 16:32:29 +0200 Subject: [PATCH 1741/2451] Add a configuration to disable showing mods in the lobby and at the aesthetician. --- Penumbra/Configuration.cs | 1 + .../Interop/PathResolving/CollectionResolver.cs | 13 +++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 3 +++ 3 files changed, 17 insertions(+) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b81e84d8..02286cc7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -41,6 +41,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool ShowModsInLobby { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; public bool UseCharacterCollectionInInspect { get; set; } = true; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index ed111794..b42571ac 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using Microsoft.VisualBasic; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -110,6 +111,12 @@ public sealed unsafe class CollectionResolver( return false; } + if (!config.ShowModsInLobby) + { + ret = ModCollection.Empty.ToResolveData(gameObject); + return true; + } + var notYetReady = false; var lobby = AgentLobby.Instance(); if (lobby != null) @@ -148,6 +155,12 @@ public sealed unsafe class CollectionResolver( return false; } + if (!config.ShowModsInLobby) + { + ret = ModCollection.Empty.ToResolveData(gameObject); + return true; + } + var player = actors.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 30384538..9989f90a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -449,6 +449,9 @@ public class SettingsTab : ITab Checkbox("Use Interface Collection for other Plugin UIs", "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); + Checkbox($"Use {TutorialService.AssignedCollections} in Lobby", + "If this is disabled, no mods are applied to characters in the lobby or at the aesthetician.", + _config.ShowModsInLobby, v => _config.ShowModsInLobby = v); Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); From 2346b7588a49049012f240eec2c1cad912c4b86c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 13:39:25 +0200 Subject: [PATCH 1742/2451] Fix GMP bug. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 6aeae346..0a2e2650 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6aeae346332a255b7575ccfca554afb0f3cf1494 +Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab From 532e8a0936acd38cdfe1701bc62c3f57d06c58ff Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 14 Jun 2024 12:11:16 +0000 Subject: [PATCH 1743/2451] [CI] Updating repo.json for 1.1.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 05de7ec7..318aafc2 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.1.0", - "TestingAssemblyVersion": "1.1.1.0", + "AssemblyVersion": "1.1.1.1", + "TestingAssemblyVersion": "1.1.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b1a059038221960133882e7b45dc697efd694981 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 23:08:04 +0200 Subject: [PATCH 1744/2451] Make modmerger file lookup case insensitive. --- Penumbra/Mods/Editor/ModMerger.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 8d47051c..74f182d3 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -196,7 +196,7 @@ public class ModMerger : IDisposable { if (fromFileToFile) { - if (!_fileToFile.TryGetValue(input.FullName, out var s)) + if (!_fileToFile.TryGetValue(input.FullName.ToLowerInvariant(), out var s)) { ret = input; return false; @@ -238,7 +238,7 @@ public class ModMerger : IDisposable Directory.CreateDirectory(finalDir); file.CopyTo(path); Penumbra.Log.Verbose($"[Merger] Copied file {file.FullName} to {path}."); - _fileToFile.Add(file.FullName, path); + _fileToFile.Add(file.FullName.ToLowerInvariant(), path); } } From ec207bdba2c01448250f6cbc3dc8e980a720fac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 20:48:37 +0200 Subject: [PATCH 1745/2451] Force saves independent of manipulations for swaps and merges. --- Penumbra/Mods/Editor/ModMerger.cs | 18 +++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 3 ++- .../Manager/OptionEditor/ModGroupEditor.cs | 4 ++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 74f182d3..f5fc9cd7 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -189,7 +189,8 @@ public class ModMerger : IDisposable _editor.SetFiles(option, redirections, SaveType.None); _editor.SetFileSwaps(option, swaps, SaveType.None); - _editor.SetManipulations(option, manips, SaveType.ImmediateSync); + _editor.SetManipulations(option, manips, SaveType.None); + _editor.ForceSave(option, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -263,9 +264,10 @@ public class ModMerger : IDisposable if (mods.Count == 1) { var files = CopySubModFiles(mods[0], dir); - _editor.SetFiles(result.Default, files); - _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); - _editor.SetManipulations(result.Default, mods[0].Manipulations); + _editor.SetFiles(result.Default, files, SaveType.None); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps, SaveType.None); + _editor.SetManipulations(result.Default, mods[0].Manipulations, SaveType.None); + _editor.ForceSave(result.Default); } else { @@ -277,6 +279,7 @@ public class ModMerger : IDisposable _editor.SetFiles(result.Default, files); _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); _editor.SetManipulations(result.Default, mods[0].Manipulations); + _editor.ForceSave(result.Default); } else { @@ -285,9 +288,10 @@ public class ModMerger : IDisposable var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.SetFiles((IModDataContainer)option, files); - _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps); - _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations); + _editor.SetFiles((IModDataContainer)option, files, SaveType.None); + _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps, SaveType.None); + _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations, SaveType.None); + _editor.ForceSave((IModDataContainer)option); } } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 48d687d0..af5b2d3a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -83,7 +83,8 @@ public class ItemSwapContainer manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None); manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None); - manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync); + manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.None); + manager.OptionEditor.ForceSave(container, SaveType.ImmediateSync); return true; } catch (Exception e) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index e1db0ccf..55e01015 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -166,6 +166,10 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } + /// Forces a file save of the given container's group. + public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue) + => saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) { From b3f87624949c9c48de7bc847782601577bd86336 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 14:50:07 +0200 Subject: [PATCH 1746/2451] Fix some crash handler issues --- Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs | 2 ++ Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs | 2 ++ Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index 3446530a..11dc52db 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -55,8 +55,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation accessor.Write(16, characterAddress); var span = GetSpan(accessor, 24, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 40); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index 4036455d..a48fe846 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -38,8 +38,10 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu accessor.Write(12, characterAddress); var span = GetSpan(accessor, 20, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 36); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index 03f63ba4..ac507e7f 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -44,12 +44,16 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr accessor.Write(12, characterAddress); var span = GetSpan(accessor, 20, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 36, 80); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 116, 260); WriteSpan(requestedFileName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 376); WriteSpan(actualFileName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } From 250c4034e04323b025cd39cd4641be8e04d431ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 14:50:33 +0200 Subject: [PATCH 1747/2451] Improve root directory behavior and AddMods. --- Penumbra/Api/Api/ModsApi.cs | 4 +++- Penumbra/Mods/Manager/ModManager.cs | 16 +++++++++------- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 548831d5..60b00d37 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -75,7 +75,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (!dir.Exists) return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); - if (dir.Parent == null || Path.GetFullPath(_modManager.BasePath.FullName) != Path.GetFullPath(dir.Parent.FullName)) + if (dir.Parent == null + || Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName)) + != Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName))) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 010cad19..62b54865 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -47,15 +47,15 @@ public sealed class ModManager : ModStorage, IDisposable DataEditor = dataEditor; OptionEditor = optionEditor; Creator = creator; - SetBaseDirectory(config.ModDirectory, true); + SetBaseDirectory(config.ModDirectory, true, out _); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); DiscoverMods(); } /// Change the mod base directory and discover available mods. - public void DiscoverMods(string newDir) + public void DiscoverMods(string newDir, out string resultNewDir) { - SetBaseDirectory(newDir, false); + SetBaseDirectory(newDir, false, out resultNewDir); DiscoverMods(); } @@ -264,8 +264,9 @@ public sealed class ModManager : ModStorage, IDisposable /// If its not the first time, check if it is the same directory as before. /// Also checks if the directory is available and tries to create it if it is not. /// - private void SetBaseDirectory(string newPath, bool firstTime) + private void SetBaseDirectory(string newPath, bool firstTime, out string resultNewDir) { + resultNewDir = newPath; if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.Ordinal)) return; @@ -278,7 +279,7 @@ public sealed class ModManager : ModStorage, IDisposable } else { - var newDir = new DirectoryInfo(newPath); + var newDir = new DirectoryInfo(Path.TrimEndingDirectorySeparator(newPath)); if (!newDir.Exists) try { @@ -290,8 +291,9 @@ public sealed class ModManager : ModStorage, IDisposable Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); } - BasePath = newDir; - Valid = Directory.Exists(newDir.FullName); + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + resultNewDir = BasePath.FullName; if (!firstTime && _config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(BasePath.FullName, Valid); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9989f90a..0de4f790 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -269,7 +269,7 @@ public class SettingsTab : ITab if (_config.ModDirectory != _newModDirectory && _newModDirectory.Length != 0 && DrawPressEnterWarning(_newModDirectory, _config.ModDirectory, pos, save, selected)) - _modManager.DiscoverMods(_newModDirectory); + _modManager.DiscoverMods(_newModDirectory, out _newModDirectory); } /// Draw the Open Directory and Rediscovery buttons. From d7b60206d77610d2d0ba62ca280e93470698c94e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 14:55:10 +0200 Subject: [PATCH 1748/2451] Improve meta manipulation handling a bit. --- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Api/TempModManager.cs | 2 +- .../Meta/Manipulations/EqdpManipulation.cs | 17 +-- .../Meta/Manipulations/EqpManipulation.cs | 17 +-- .../Meta/Manipulations/EstManipulation.cs | 15 +- .../Manipulations/GlobalEqpManipulation.cs | 31 +++- .../Meta/Manipulations/GmpManipulation.cs | 15 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 140 ++++++++++++++++++ Penumbra/Meta/Manipulations/Rsp.cs | 28 +++- .../Meta/Manipulations/RspManipulation.cs | 27 ++-- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 6 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 2 +- 14 files changed, 242 insertions(+), 66 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/MetaDictionary.cs diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 38d080cc..09a9b7c4 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -160,7 +160,7 @@ public class TemporaryApi( /// Only returns true if all conversions are successful and distinct. /// private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? manips) + [NotNullWhen(true)] out MetaDictionary? manips) { if (manipString.Length == 0) { @@ -174,7 +174,7 @@ public class TemporaryApi( return false; } - manips = new HashSet(manipArray!.Length); + manips = new MetaDictionary(manipArray!.Length); foreach (var manip in manipArray.Where(m => m.Validate())) { if (manips.Add(manip)) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index aee2b447..cbb07436 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -43,7 +43,7 @@ public class TempModManager : IDisposable => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet manips, ModPriority priority) + MetaDictionary manips, ModPriority priority) { var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 2c01ce3f..8c5f27e5 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -8,11 +8,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqdpManipulation : IMetaManipulation +public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation { [JsonIgnore] - public EqdpIdentifier Identifier { get; private init; } - public EqdpEntry Entry { get; private init; } + public EqdpIdentifier Identifier { get; } = identifier; + + public EqdpEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -31,20 +32,18 @@ public readonly struct EqdpManipulation : IMetaManipulation [JsonConstructor] public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) - { - Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); - Entry = Eqdp.Mask(Slot) & entry; - } + : this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry) + { } public EqdpManipulation Copy(EqdpManipulation entry) { if (entry.Slot != Slot) { var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); - return new EqdpManipulation(Eqdp.FromSlotAndBits(Slot, bit1, bit2), Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2)); } - return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, entry.Entry); } public EqdpManipulation Copy(EqdpEntry entry) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 3bced096..eef21d12 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -9,12 +9,13 @@ using Penumbra.Util; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqpManipulation : IMetaManipulation +public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation { - [JsonConverter(typeof(ForceNumericFlagEnumConverter))] - public EqpEntry Entry { get; private init; } + [JsonIgnore] + public EqpIdentifier Identifier { get; } = identifier; - public EqpIdentifier Identifier { get; private init; } + [JsonConverter(typeof(ForceNumericFlagEnumConverter))] + public EqpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; @@ -25,13 +26,11 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConstructor] public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) - { - Identifier = new EqpIdentifier(setId, slot); - Entry = Eqp.Mask(slot) & entry; - } + : this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry) + { } public EqpManipulation Copy(EqpEntry entry) - => new(entry, Slot, SetId); + => new(Identifier, entry); public override string ToString() => $"Eqp - {SetId} - {Slot}"; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index c3f9792f..09abbaa5 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -8,7 +8,7 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EstManipulation : IMetaManipulation +public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation { public static string ToName(EstType type) => type switch @@ -20,8 +20,9 @@ public readonly struct EstManipulation : IMetaManipulation _ => "unk", }; - public EstIdentifier Identifier { get; private init; } - public EstEntry Entry { get; private init; } + [JsonIgnore] + public EstIdentifier Identifier { get; } = identifier; + public EstEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -41,13 +42,11 @@ public readonly struct EstManipulation : IMetaManipulation [JsonConstructor] public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) - { - Entry = entry; - Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); - } + : this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry) + { } public EstManipulation Copy(EstEntry entry) - => new(Gender, Race, Slot, SetId, entry); + => new(Identifier, entry); public override string ToString() diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ada543dc..94c892e2 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -1,9 +1,11 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly struct GlobalEqpManipulation : IMetaManipulation +public readonly struct GlobalEqpManipulation : IMetaManipulation, IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -19,6 +21,28 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation() ?? (GlobalEqpType)100; + var condition = jObj[nameof(Condition)]?.ToObject() ?? 0; + var ret = new GlobalEqpManipulation + { + Type = type, + Condition = condition, + }; + return ret.Validate() ? ret : null; + } + public bool Equals(GlobalEqpManipulation other) => Type == other.Type @@ -45,6 +69,9 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { } + public MetaIndex FileIndex() - => (MetaIndex)(-1); + => MetaIndex.Eqp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 0b2a9f4b..431f6325 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -6,24 +6,23 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation : IMetaManipulation +public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation { - public GmpIdentifier Identifier { get; private init; } + [JsonIgnore] + public GmpIdentifier Identifier { get; } = identifier; - public GmpEntry Entry { get; private init; } + public GmpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; [JsonConstructor] public GmpManipulation(GmpEntry entry, PrimaryId setId) - { - Entry = entry; - Identifier = new GmpIdentifier(setId); - } + : this(new GmpIdentifier(setId), entry) + { } public GmpManipulation Copy(GmpEntry entry) - => new(entry, SetId); + => new(Identifier, entry); public override string ToString() => $"Gmp - {SetId}"; diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs new file mode 100644 index 00000000..65252c5d --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -0,0 +1,140 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; +using ImcEntry = Penumbra.GameData.Structs.ImcEntry; + +namespace Penumbra.Meta.Manipulations; + +[JsonConverter(typeof(Converter))] +public sealed class MetaDictionary : HashSet +{ + public MetaDictionary() + { } + + public MetaDictionary(int capacity) + : base(capacity) + { } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartArray(); + foreach (var item in value) + { + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(item.ManipulationType.ToString()); + writer.WritePropertyName("Manipulation"); + serializer.Serialize(writer, item.Manipulation); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var dict = existingValue ?? []; + dict.Clear(); + var jObj = JArray.Load(reader); + foreach (var item in jObj) + { + var type = item["Type"]?.ToObject() ?? MetaManipulation.Type.Unknown; + if (type is MetaManipulation.Type.Unknown) + { + Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); + continue; + } + + if (item["Manipulation"] is not JObject manip) + { + Penumbra.Log.Warning($"Manipulation of type {type} does not contain manipulation data."); + continue; + } + + switch (type) + { + case MetaManipulation.Type.Imc: + { + var identifier = ImcIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new ImcManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqdp: + { + var identifier = EqdpIdentifier.FromJson(manip); + var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqdpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqp: + { + var identifier = EqpIdentifier.FromJson(manip); + var entry = (EqpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Est: + { + var identifier = EstIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EstManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EST Manipulation encountered."); + break; + } + case MetaManipulation.Type.Gmp: + { + var identifier = GmpIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new GmpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Rsp: + { + var identifier = RspIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new RspManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); + break; + } + case MetaManipulation.Type.GlobalEqp: + { + var identifier = GlobalEqpManipulation.FromJson(manip); + if (identifier.HasValue) + dict.Add(new MetaManipulation(identifier.Value)); + else + Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); + break; + } + } + } + + return dict; + } + } +} diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 29cdfd71..ca7cb1c5 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,3 +1,4 @@ +using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -12,13 +13,31 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() - => throw new NotImplementedException(); + => MetaIndex.HumanCmp; public bool Validate() - => throw new NotImplementedException(); + => SubRace is not SubRace.Unknown + && Enum.IsDefined(SubRace) + && Attribute is not RspAttribute.NumAttributes + && Enum.IsDefined(Attribute); public JObject AddToJson(JObject jObj) - => throw new NotImplementedException(); + { + jObj["SubRace"] = SubRace.ToString(); + jObj["Attribute"] = Attribute.ToString(); + return jObj; + } + + public static RspIdentifier? FromJson(JObject? jObj) + { + if (jObj == null) + return null; + + var subRace = jObj["SubRace"]?.ToObject() ?? SubRace.Unknown; + var attribute = jObj["Attribute"]?.ToObject() ?? RspAttribute.NumAttributes; + var ret = new RspIdentifier(subRace, attribute); + return ret.Validate() ? ret : null; + } } [JsonConverter(typeof(Converter))] @@ -28,6 +47,9 @@ public readonly record struct RspEntry(float Value) : IComparisonOperators Value is >= MinValue and <= MaxValue; + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 04691c9f..e2282c41 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -7,10 +7,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation : IMetaManipulation +public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation { - public RspIdentifier Identifier { get; private init; } - public RspEntry Entry { get; private init; } + [JsonIgnore] + public RspIdentifier Identifier { get; } = identifier; + + public RspEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public SubRace SubRace @@ -22,13 +24,11 @@ public readonly struct RspManipulation : IMetaManipulation [JsonConstructor] public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - { - Entry = entry; - Identifier = new RspIdentifier(subRace, attribute); - } + : this(new RspIdentifier(subRace, attribute), entry) + { } public RspManipulation Copy(RspEntry entry) - => new(SubRace, Attribute, entry); + => new(Identifier, entry); public override string ToString() => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; @@ -63,14 +63,5 @@ public readonly struct RspManipulation : IMetaManipulation } public bool Validate() - { - if (SubRace is SubRace.Unknown || !Enum.IsDefined(SubRace)) - return false; - if (!Enum.IsDefined(Attribute)) - return false; - if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) - return false; - - return true; - } + => Identifier.Validate() && Entry.Validate(); } diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1a234879..5a300a48 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; IMod IModDataContainer.Mod => Mod; diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 7f7ef4a6..1a89ec17 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -10,9 +10,9 @@ public interface IModDataContainer public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public MetaDictionary Manipulations { get; set; } public string GetName(); public string GetFullName(); diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 02d86af2..378f6dc8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; public void AddDataTo(Dictionary redirections, HashSet manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6e6e72ab..e0a03c92 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -57,7 +57,7 @@ public class TemporaryMod : IMod public bool SetManipulation(MetaManipulation manip) => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - public void SetAll(Dictionary dict, HashSet manips) + public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; Default.Manipulations = manips; From 94fdd848b718ef2c7e932faff6b108f7ed7287d8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:06:37 +0200 Subject: [PATCH 1749/2451] Expand on MetaDictionary to use separate dictionaries. --- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 244 +++++++++++++++++- Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 21 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- 15 files changed, 257 insertions(+), 36 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 09a9b7c4..995ec388 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -174,8 +174,8 @@ public class TemporaryApi( return false; } - manips = new MetaDictionary(manipArray!.Length); - foreach (var manip in manipArray.Where(m => m.Validate())) + manips = []; + foreach (var manip in manipArray!.Where(m => m.Validate())) { if (manips.Add(manip)) continue; diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 65252c5d..b0b7f011 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,19 +1,237 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Structs; +using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public sealed class MetaDictionary : HashSet +public sealed class MetaDictionary : IEnumerable { - public MetaDictionary() - { } + private readonly Dictionary _imc = []; + private readonly Dictionary _eqp = []; + private readonly Dictionary _eqdp = []; + private readonly Dictionary _est = []; + private readonly Dictionary _rsp = []; + private readonly Dictionary _gmp = []; + private readonly HashSet _globalEqp = []; - public MetaDictionary(int capacity) - : base(capacity) - { } + public int Count { get; private set; } + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _est.Clear(); + _rsp.Clear(); + _gmp.Clear(); + _globalEqp.Clear(); + } + + public IEnumerator GetEnumerator() + => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) + .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value)))) + .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value)))) + .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) + .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) + .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) + .Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public bool Add(MetaManipulation manip) + { + var ret = manip.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), + MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, manip.Eqdp.Entry), + MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, manip.Eqp.Entry), + MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), + MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), + MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), + MetaManipulation.Type.GlobalEqp => _globalEqp.Add(manip.GlobalEqp), + _ => false, + }; + + if (ret) + ++Count; + return ret; + } + + public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EstIdentifier identifier, EstEntry entry) + { + if (!_est.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GlobalEqpManipulation identifier) + { + if (!_globalEqp.Add(identifier)) + return false; + + ++Count; + return true; + } + + public bool Remove(MetaManipulation manip) + { + var ret = manip.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Remove(manip.Imc.Identifier), + MetaManipulation.Type.Eqdp => _eqdp.Remove(manip.Eqdp.Identifier), + MetaManipulation.Type.Eqp => _eqp.Remove(manip.Eqp.Identifier), + MetaManipulation.Type.Est => _est.Remove(manip.Est.Identifier), + MetaManipulation.Type.Gmp => _gmp.Remove(manip.Gmp.Identifier), + MetaManipulation.Type.Rsp => _rsp.Remove(manip.Rsp.Identifier), + MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(manip.GlobalEqp), + _ => false, + }; + if (ret) + --Count; + return ret; + } + + public void UnionWith(IEnumerable manips) + { + foreach (var manip in manips) + Add(manip); + } + + public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) + { + switch (identifier.ManipulationType) + { + case MetaManipulation.Type.Imc: + if (_imc.TryGetValue(identifier.Imc.Identifier, out var oldImc)) + { + oldValue = new MetaManipulation(new ImcManipulation(identifier.Imc.Identifier, oldImc)); + return true; + } + + break; + case MetaManipulation.Type.Eqdp: + if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) + { + oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp)); + return true; + } + + break; + case MetaManipulation.Type.Eqp: + if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) + { + oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp)); + return true; + } + + break; + case MetaManipulation.Type.Est: + if (_est.TryGetValue(identifier.Est.Identifier, out var oldEst)) + { + oldValue = new MetaManipulation(new EstManipulation(identifier.Est.Identifier, oldEst)); + return true; + } + + break; + case MetaManipulation.Type.Gmp: + if (_gmp.TryGetValue(identifier.Gmp.Identifier, out var oldGmp)) + { + oldValue = new MetaManipulation(new GmpManipulation(identifier.Gmp.Identifier, oldGmp)); + return true; + } + + break; + case MetaManipulation.Type.Rsp: + if (_rsp.TryGetValue(identifier.Rsp.Identifier, out var oldRsp)) + { + oldValue = new MetaManipulation(new RspManipulation(identifier.Rsp.Identifier, oldRsp)); + return true; + } + + break; + case MetaManipulation.Type.GlobalEqp: + if (_globalEqp.TryGetValue(identifier.GlobalEqp, out var oldGlobalEqp)) + { + oldValue = new MetaManipulation(oldGlobalEqp); + return true; + } + + break; + } + + oldValue = default; + return false; + } + + public void SetTo(MetaDictionary other) + { + _imc.SetTo(other._imc); + _eqp.SetTo(other._eqp); + _eqdp.SetTo(other._eqdp); + _est.SetTo(other._est); + _rsp.SetTo(other._rsp); + _gmp.SetTo(other._gmp); + _globalEqp.SetTo(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + + public MetaDictionary Clone() + { + var ret = new MetaDictionary(); + ret.SetTo(this); + return ret; + } private class Converter : JsonConverter { @@ -67,7 +285,7 @@ public sealed class MetaDictionary : HashSet var identifier = ImcIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new ImcManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); break; @@ -77,7 +295,7 @@ public sealed class MetaDictionary : HashSet var identifier = EqdpIdentifier.FromJson(manip); var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EqdpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); break; @@ -87,7 +305,7 @@ public sealed class MetaDictionary : HashSet var identifier = EqpIdentifier.FromJson(manip); var entry = (EqpEntry?)manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EqpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); break; @@ -97,7 +315,7 @@ public sealed class MetaDictionary : HashSet var identifier = EstIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EstManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EST Manipulation encountered."); break; @@ -107,7 +325,7 @@ public sealed class MetaDictionary : HashSet var identifier = GmpIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new GmpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); break; @@ -117,7 +335,7 @@ public sealed class MetaDictionary : HashSet var identifier = RspIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new RspManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; @@ -126,7 +344,7 @@ public sealed class MetaDictionary : HashSet { var identifier = GlobalEqpManipulation.FromJson(manip); if (identifier.HasValue) - dict.Add(new MetaManipulation(identifier.Value)); + dict.TryAdd(identifier.Value); else Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); break; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d4c881e9..06c31846 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -8,7 +8,7 @@ namespace Penumbra.Mods.Editor; public record struct AppliedModData( Dictionary FileRedirections, - HashSet Manipulations) + MetaDictionary Manipulations) { public static readonly AppliedModData Empty = new([], []); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f5fc9cd7..4faced80 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -158,7 +158,7 @@ public class ModMerger : IDisposable { var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.Manipulations.ToHashSet(); + var manips = option.Manipulations.Clone(); foreach (var originalOption in mergeOptions) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 86853755..45d9f8a1 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -145,7 +145,7 @@ public class ModMetaEditor(ModManager modManager) if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, [..Recombine()]); Changes = false; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index fcc8c093..00f47e25 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -43,7 +43,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index b336203d..383bc9fd 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -99,7 +99,7 @@ public class ImcModGroup(Mod mod) : IModGroup => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7816d628..220d0a7c 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -116,7 +116,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new MultiModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index a6ebd846..a559d609 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -101,7 +101,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new SingleModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (OptionData.Count == 0) return; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index af5b2d3a..67a5d007 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -23,7 +23,7 @@ public class ItemSwapContainer public IReadOnlyDictionary ModRedirections => _appliedModData.FileRedirections; - public IReadOnlySet ModManipulations + public MetaDictionary ModManipulations => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -42,9 +42,10 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, + DirectoryInfo? directory = null) { - var convertedManips = new HashSet(Swaps.Count); + var convertedManips = new MetaDictionary(); var convertedFiles = new Dictionary(Swaps.Count); var convertedSwaps = new Dictionary(Swaps.Count); directory ??= mod.ModPath; @@ -98,13 +99,9 @@ public class ItemSwapContainer { Clear(); if (mod == null || mod.Index < 0) - { - _appliedModData = AppliedModData.Empty; - } + _appliedModData = AppliedModData.Empty; else - { _appliedModData = ModSettings.GetResolveData(mod, settings); - } } public ItemSwapContainer(MetaFileManager manager, ObjectIdentification identifier) @@ -121,7 +118,13 @@ public class ItemSwapContainer private Func MetaResolver(ModCollection? collection) { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; + if (collection?.MetaCache?.Manipulations is { } cache) + { + MetaDictionary dict = [.. cache]; + return m => dict.TryGetValue(m, out var a) ? a : m; + } + + var set = _appliedModData.Manipulations; return m => set.TryGetValue(m, out var a) ? a : m; } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 55e01015..01092862 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -142,7 +142,7 @@ public class ModGroupEditor( } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + public void SetManipulations(IModDataContainer subMod, MetaDictionary manipulations, SaveType saveType = SaveType.Queue) { if (subMod.Manipulations.Count == manipulations.Count && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 783ef3e6..a7f87dcd 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -71,7 +71,7 @@ public sealed class Mod : IMod return AppliedModData.Empty; var dictRedirections = new Dictionary(TotalFileCount); - var setManips = new HashSet(TotalManipulations); + var setManips = new MetaDictionary(); foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { var config = settings.Settings[groupIndex]; diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 5a300a48..dcd33610 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -21,7 +21,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer IModGroup? IModDataContainer.Group => null; - public void AddTo(Dictionary redirections, HashSet manipulations) + public void AddTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 378f6dc8..ed7b6ff8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -35,7 +35,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary FileSwaps { get; set; } = []; public MetaDictionary Manipulations { get; set; } = []; - public void AddDataTo(Dictionary redirections, HashSet manipulations) + public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index b984b570..06a924c8 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -21,7 +21,7 @@ public static class SubMod /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, - HashSet manipulations) + MetaDictionary manipulations) { foreach (var (path, file) in container.Files) redirections.TryAdd(path, file); From 13156a58e92ab64965879f994eb9f1aec77d8f12 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:09:34 +0200 Subject: [PATCH 1750/2451] Remove unused functions. --- Penumbra/Mods/TemporaryMod.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e0a03c92..a715f786 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -51,12 +51,6 @@ public class TemporaryMod : IMod public TemporaryMod() => Default = new DefaultSubMod(this); - public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.Files[gamePath] = fullPath; - - public bool SetManipulation(MetaManipulation manip) - => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; From e0339160e908276a97963fd498f9e785a88ee690 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:25:08 +0200 Subject: [PATCH 1751/2451] Start removing MetaManipulation functions. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 40 +++++++++---------- .../Manager/OptionEditor/ModGroupEditor.cs | 3 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index b0b7f011..b9d7990d 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -124,28 +124,28 @@ public sealed class MetaDictionary : IEnumerable return true; } - public bool Remove(MetaManipulation manip) + public void UnionWith(MetaDictionary manips) { - var ret = manip.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove(manip.Imc.Identifier), - MetaManipulation.Type.Eqdp => _eqdp.Remove(manip.Eqdp.Identifier), - MetaManipulation.Type.Eqp => _eqp.Remove(manip.Eqp.Identifier), - MetaManipulation.Type.Est => _est.Remove(manip.Est.Identifier), - MetaManipulation.Type.Gmp => _gmp.Remove(manip.Gmp.Identifier), - MetaManipulation.Type.Rsp => _rsp.Remove(manip.Rsp.Identifier), - MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(manip.GlobalEqp), - _ => false, - }; - if (ret) - --Count; - return ret; - } + foreach (var (identifier, entry) in manips._imc) + TryAdd(identifier, entry); - public void UnionWith(IEnumerable manips) - { - foreach (var manip in manips) - Add(manip); + foreach (var (identifier, entry) in manips._eqp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._eqdp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._gmp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._rsp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._est) + TryAdd(identifier, entry); + + foreach (var identifier in manips._globalEqp) + TryAdd(identifier); } public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 01092862..594ec9d2 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -144,8 +144,7 @@ public class ModGroupEditor( /// Set the meta manipulations for a given option. Replaces existing manipulations. public void SetManipulations(IModDataContainer subMod, MetaDictionary manipulations, SaveType saveType = SaveType.Queue) { - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + if (subMod.Manipulations.Equals(manipulations)) return; communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); From 5ca9e63a2a5f1f44dd9c5859578c3684bc6df4e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:34:51 +0200 Subject: [PATCH 1752/2451] Use internal entries. --- Penumbra/Meta/Manipulations/Eqdp.cs | 6 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 70 ++++++++++++------- Penumbra/Mods/ModCreator.cs | 4 +- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 6d6942e6..a986d475 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -71,13 +71,13 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge } } -public readonly record struct InternalEqdpEntry(bool Model, bool Material) +public readonly record struct EqdpEntryInternal(bool Model, bool Material) { - private InternalEqdpEntry((bool, bool) val) + private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) { } - public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot) + public EqdpEntryInternal(EqdpEntry entry, EquipSlot slot) : this(entry.ToBits(slot)) { } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index b9d7990d..941cdf34 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -9,13 +9,13 @@ namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] public sealed class MetaDictionary : IEnumerable { - private readonly Dictionary _imc = []; - private readonly Dictionary _eqp = []; - private readonly Dictionary _eqdp = []; - private readonly Dictionary _est = []; - private readonly Dictionary _rsp = []; - private readonly Dictionary _gmp = []; - private readonly HashSet _globalEqp = []; + private readonly Dictionary _imc = []; + private readonly Dictionary _eqp = []; + private readonly Dictionary _eqdp = []; + private readonly Dictionary _est = []; + private readonly Dictionary _rsp = []; + private readonly Dictionary _gmp = []; + private readonly HashSet _globalEqp = []; public int Count { get; private set; } @@ -30,10 +30,20 @@ public sealed class MetaDictionary : IEnumerable _globalEqp.Clear(); } + public bool Equals(MetaDictionary other) + => Count == other.Count + && _imc.SetEquals(other._imc) + && _eqp.SetEquals(other._eqp) + && _eqdp.SetEquals(other._eqdp) + && _est.SetEquals(other._est) + && _rsp.SetEquals(other._rsp) + && _gmp.SetEquals(other._gmp) + && _globalEqp.SetEquals(other._globalEqp); + public IEnumerator GetEnumerator() => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) - .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value)))) - .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value)))) + .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) + .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) @@ -47,8 +57,8 @@ public sealed class MetaDictionary : IEnumerable var ret = manip.ManipulationType switch { MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), - MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, manip.Eqdp.Entry), - MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, manip.Eqp.Entry), + MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, new EqdpEntryInternal(manip.Eqdp.Entry, manip.Eqdp.Slot)), + MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, new EqpEntryInternal(manip.Eqp.Entry, manip.Eqp.Slot)), MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), @@ -71,22 +81,10 @@ public sealed class MetaDictionary : IEnumerable } public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) - { - if (!_eqp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } + => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) - { - if (!_eqdp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } + => TryAdd(identifier, new EqdpEntryInternal(entry, identifier.Slot)); public bool TryAdd(EstIdentifier identifier, EstEntry entry) { @@ -163,7 +161,7 @@ public sealed class MetaDictionary : IEnumerable case MetaManipulation.Type.Eqdp: if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) { - oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp)); + oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp.ToEntry(identifier.Eqp.Slot))); return true; } @@ -171,7 +169,7 @@ public sealed class MetaDictionary : IEnumerable case MetaManipulation.Type.Eqp: if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) { - oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp)); + oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp.ToEntry(identifier.Eqdp.Slot))); return true; } @@ -355,4 +353,22 @@ public sealed class MetaDictionary : IEnumerable return dict; } } + + private bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + private bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ed4245c4..0035fd41 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,7 +198,7 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith(meta.MetaManipulations); + option.Manipulations.UnionWith([.. meta.MetaManipulations]); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -212,7 +212,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith(rgsp.MetaManipulations); + option.Manipulations.UnionWith([.. rgsp.MetaManipulations]); } } catch (Exception e) From 0445ed0ef9ed456a45ac100007f1e847ecd2e68e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 19:09:46 +0200 Subject: [PATCH 1753/2451] Remove TryGetValue(MetaManipulation) from MetaDictionary. --- Penumbra/Meta/Files/EqdpFile.cs | 4 + Penumbra/Meta/Files/EqpGmpFile.cs | 4 + Penumbra/Meta/Files/EstFile.cs | 3 + Penumbra/Meta/Files/ImcFile.cs | 3 + Penumbra/Meta/Manipulations/Eqdp.cs | 5 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 85 ++++-------- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 131 +++++++++--------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 21 ++- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 23 ++- Penumbra/Mods/ItemSwap/Swaps.cs | 82 +++++++---- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- 11 files changed, 182 insertions(+), 183 deletions(-) diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index c76c4efd..e46e82e9 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -2,6 +2,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -126,4 +127,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, PrimaryId primaryId) => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], primaryId); + + public static EqdpEntry GetDefault(MetaFileManager manager, EqdpIdentifier identifier) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)identifier.FileIndex()], identifier.SetId); } diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 70067c2b..17541c4f 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -1,6 +1,7 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -164,6 +165,9 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) => new() { Value = GetDefaultInternal(manager, InternalIndex, primaryIdx, GmpEntry.Default.Value) }; + public static GmpEntry GetDefault(MetaFileManager manager, GmpIdentifier identifier) + => new() { Value = GetDefaultInternal(manager, InternalIndex, identifier.SetId, GmpEntry.Default.Value) }; + public void Reset(IEnumerable entries) { foreach (var entry in entries) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index ee38ea1e..f3860416 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -184,4 +184,7 @@ public sealed unsafe class EstFile : MetaBaseFile public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); + + public static EstEntry GetDefault(MetaFileManager manager, EstIdentifier identifier) + => GetDefault(manager, identifier.FileIndex(), identifier.GenderRace, identifier.SetId); } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 5d704cf8..892f5b44 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -65,6 +65,9 @@ public unsafe class ImcFile : MetaBaseFile return ptr == null ? new ImcEntry() : *ptr; } + public ImcEntry GetEntry(EquipSlot slot, Variant variantIdx) + => GetEntry(PartIndex(slot), variantIdx); + public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists) { var ptr = VariantPtr(Data, partIdx, variantIdx); diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index a986d475..6306f419 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -71,7 +71,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge } } -public readonly record struct EqdpEntryInternal(bool Model, bool Material) +public readonly record struct EqdpEntryInternal(bool Material, bool Model) { private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) @@ -81,7 +81,6 @@ public readonly record struct EqdpEntryInternal(bool Model, bool Material) : this(entry.ToBits(slot)) { } - public EqdpEntry ToEntry(EquipSlot slot) - => Eqdp.FromSlotAndBits(slot, Model, Material); + => Eqdp.FromSlotAndBits(slot, Material, Model); } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 941cdf34..51149e3b 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,6 +52,19 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public bool Add(IMetaIdentifier identifier, object entry) + => identifier switch + { + EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && Add(eqdpIdentifier, e), + EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && Add(eqpIdentifier, e), + EstIdentifier estIdentifier => entry is EstEntry e && Add(estIdentifier, e), + GlobalEqpManipulation globalEqpManipulation => Add(globalEqpManipulation), + GmpIdentifier gmpIdentifier => entry is GmpEntry e && Add(gmpIdentifier, e), + ImcIdentifier imcIdentifier => entry is ImcEntry e && Add(imcIdentifier, e), + RspIdentifier rspIdentifier => entry is RspEntry e && Add(rspIdentifier, e), + _ => false, + }; + public bool Add(MetaManipulation manip) { var ret = manip.ManipulationType switch @@ -146,71 +159,23 @@ public sealed class MetaDictionary : IEnumerable TryAdd(identifier); } - public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) - { - switch (identifier.ManipulationType) - { - case MetaManipulation.Type.Imc: - if (_imc.TryGetValue(identifier.Imc.Identifier, out var oldImc)) - { - oldValue = new MetaManipulation(new ImcManipulation(identifier.Imc.Identifier, oldImc)); - return true; - } + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) + => _est.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Eqdp: - if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) - { - oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp.ToEntry(identifier.Eqp.Slot))); - return true; - } + public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) + => _eqp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Eqp: - if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) - { - oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp.ToEntry(identifier.Eqdp.Slot))); - return true; - } + public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) + => _eqdp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Est: - if (_est.TryGetValue(identifier.Est.Identifier, out var oldEst)) - { - oldValue = new MetaManipulation(new EstManipulation(identifier.Est.Identifier, oldEst)); - return true; - } + public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) + => _gmp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Gmp: - if (_gmp.TryGetValue(identifier.Gmp.Identifier, out var oldGmp)) - { - oldValue = new MetaManipulation(new GmpManipulation(identifier.Gmp.Identifier, oldGmp)); - return true; - } + public bool TryGetValue(RspIdentifier identifier, out RspEntry value) + => _rsp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Rsp: - if (_rsp.TryGetValue(identifier.Rsp.Identifier, out var oldRsp)) - { - oldValue = new MetaManipulation(new RspManipulation(identifier.Rsp.Identifier, oldRsp)); - return true; - } - - break; - case MetaManipulation.Type.GlobalEqp: - if (_globalEqp.TryGetValue(identifier.GlobalEqp, out var oldGlobalEqp)) - { - oldValue = new MetaManipulation(oldGlobalEqp); - return true; - } - - break; - } - - oldValue = default; - return false; - } + public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) + => _imc.TryGetValue(identifier, out value); public void SetTo(MetaDictionary other) { diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3efee857..e42a1d31 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,4 +1,4 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -16,32 +16,19 @@ public static class EquipmentSwap private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger) { if (slot != EquipSlot.RFinger) - return new[] - { - slot, - }; + return [slot]; return rFinger ? lFinger - ? new[] - { - EquipSlot.RFinger, - EquipSlot.LFinger, - } - : new[] - { - EquipSlot.RFinger, - } + ? [EquipSlot.RFinger, EquipSlot.LFinger] + : [EquipSlot.RFinger] : lFinger - ? new[] - { - EquipSlot.LFinger, - } - : Array.Empty(); + ? [EquipSlot.LFinger] + : []; } public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, + Func redirections, MetaDictionary manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); @@ -50,11 +37,14 @@ public static class EquipmentSwap throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -99,7 +89,7 @@ public static class EquipmentSwap } public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, EquipItem itemFrom, + Func redirections, MetaDictionary manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. @@ -120,8 +110,12 @@ public static class EquipmentSwap foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -133,7 +127,6 @@ public static class EquipmentSwap var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -154,7 +147,7 @@ public static class EquipmentSwap if (eqdp != null) swaps.Add(eqdp); - var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false; + var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); @@ -184,22 +177,22 @@ public static class EquipmentSwap return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, - PrimaryId idTo, byte mtrlTo) + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { - var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom), slotFrom, gender, - race, idFrom); - var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo), slotTo, gender, race, - idTo); - var meta = new MetaSwap(manips, eqdpFrom, eqdpTo); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom); + var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpToIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, + eqdpFromDefault, eqdpToIdentifier, + eqdpToDefault); + var (ownMtrl, ownMdl) = meta.SwapToModdedEntry; if (ownMdl) { var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo); @@ -270,38 +263,39 @@ public static class EquipmentSwap return (imc, variants, items); } - public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateGmp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot is not EquipSlot.Head) return null; - var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom), idFrom); - var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo), idTo); - return new MetaSwap(manips, manipFrom, manipTo); + var manipFromIdentifier = new GmpIdentifier(idFrom); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, - PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, + ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, - EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { - var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var manipulationFrom = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, entryFrom); - var manipulationTo = new ImcManipulation(slotTo, variantTo.Id, idTo, entryTo); - var imc = new MetaSwap(manips, manipulationFrom, manipulationTo); + var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); - var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId); + var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId); if (decal != null) imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId); + var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModdedEntry.VfxId); if (avfx != null) imc.ChildSwaps.Add(avfx); @@ -322,7 +316,8 @@ public static class EquipmentSwap // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, byte vfxId) + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, + byte vfxId) { if (vfxId == 0) return null; @@ -340,17 +335,18 @@ public static class EquipmentSwap return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot.IsAccessory()) return null; - var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom); - var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo); - var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom); - var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom); - return new MetaSwap(manips, eqpFrom, eqpTo); + var manipFromIdentifier = new EqpIdentifier(idFrom, slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, + manipFromDefault, manipToIdentifier, manipToDefault); } public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, @@ -397,7 +393,8 @@ public static class EquipmentSwap return mtrl; } - public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, PrimaryId idTo, + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 7fac52c1..efd8080c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -147,24 +147,23 @@ public static class ItemSwap return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } - /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, - Func manips, EstType type, - GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) + public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, + MetaDictionary manips, EstType type, GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) return null; - var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation(gender, race, type, idFrom, EstFile.GetDefault(manager, type, genderRace, idFrom)); - var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); - var est = new MetaSwap(manips, fromDefault, toDefault); + var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); + var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); + var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); + var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); - if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2) + if (ownMdl && est.SwapToModdedEntry.Value >= 2) { - var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(phyb); - var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(sklb); } else if (est.SwapAppliedIsDefault) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 67a5d007..021ee665 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -74,9 +74,9 @@ public class ItemSwapContainer } break; - case MetaSwap meta: + case IMetaSwap meta: if (!meta.SwapAppliedIsDefault) - convertedManips.Add(meta.SwapApplied); + convertedManips.Add(meta.SwapFromIdentifier, meta.SwapToModdedEntry); break; } @@ -116,17 +116,10 @@ public class ItemSwapContainer ? p => collection.ResolvePath(p) ?? new FullPath(p) : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); - private Func MetaResolver(ModCollection? collection) - { - if (collection?.MetaCache?.Manipulations is { } cache) - { - MetaDictionary dict = [.. cache]; - return m => dict.TryGetValue(m, out var a) ? a : m; - } - - var set = _appliedModData.Manipulations; - return m => set.TryGetValue(m, out var a) ? a : m; - } + private MetaDictionary MetaResolver(ModCollection? collection) + => collection?.MetaCache?.Manipulations is { } cache + ? [.. cache] + : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true) @@ -161,8 +154,8 @@ public class ItemSwapContainer _ => (EstType)0, }; - var metaResolver = MetaResolver(collection); - var est = ItemSwap.CreateEst(manager, pathResolver, metaResolver, type, race, from, to, true); + var estResolver = MetaResolver(collection); + var est = ItemSwap.CreateEst(manager, pathResolver, estResolver, type, race, from, to, true); Swaps.Add(mdl); if (est != null) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 27935ffb..36c54203 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,58 +1,91 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; -using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; public class Swap { /// Any further swaps belonging specifically to this tree of changes. - public readonly List ChildSwaps = new(); + public readonly List ChildSwaps = []; public IEnumerable WithChildren() => ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this); } -public sealed class MetaSwap : Swap +public interface IMetaSwap { + public IMetaIdentifier SwapFromIdentifier { get; } + public IMetaIdentifier SwapToIdentifier { get; } + + public object SwapFromDefaultEntry { get; } + public object SwapToDefaultEntry { get; } + public object SwapToModdedEntry { get; } + + public bool SwapToIsDefault { get; } + public bool SwapAppliedIsDefault { get; } +} + +public sealed class MetaSwap : Swap, IMetaSwap + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged, IEquatable +{ + public TIdentifier SwapFromIdentifier; + public TIdentifier SwapToIdentifier; + /// The default value of a specific meta manipulation that needs to be redirected. - public MetaManipulation SwapFrom; + public TEntry SwapFromDefaultEntry; /// The default value of the same Meta entry of the redirected item. - public MetaManipulation SwapToDefault; + public TEntry SwapToDefaultEntry; /// The modded value of the same Meta entry of the redirected item, or the same as SwapToDefault if unmodded. - public MetaManipulation SwapToModded; + public TEntry SwapToModdedEntry; - /// The modded value applied to the specific meta manipulation target before redirection. - public MetaManipulation SwapApplied; - - /// Whether SwapToModded equals SwapToDefault. - public bool SwapToIsDefault; + /// Whether SwapToModdedEntry equals SwapToDefaultEntry. + public bool SwapToIsDefault { get; } /// Whether the applied meta manipulation does not change anything against the default. - public bool SwapAppliedIsDefault; + public bool SwapAppliedIsDefault { get; } /// /// Create a new MetaSwap from the original meta identifier and the target meta identifier. /// - /// A function that converts the given manipulation to the modded one. - /// The original meta identifier with its default value. - /// The target meta identifier with its default value. - public MetaSwap(Func manipulations, MetaManipulation manipFrom, MetaManipulation manipTo) + /// A function that obtains a modded meta entry if it exists. + /// The original meta identifier. + /// The default value for the original meta identifier. + /// The target meta identifier. + /// The default value for the target meta identifier. + public MetaSwap(Func manipulations, TIdentifier manipFromIdentifier, TEntry manipFromEntry, + TIdentifier manipToIdentifier, TEntry manipToEntry) { - SwapFrom = manipFrom; - SwapToDefault = manipTo; + SwapFromIdentifier = manipFromIdentifier; + SwapToIdentifier = manipToIdentifier; + SwapFromDefaultEntry = manipFromEntry; + SwapToDefaultEntry = manipToEntry; - SwapToModded = manipulations(manipTo); - SwapToIsDefault = manipTo.EntryEquals(SwapToModded); - SwapApplied = SwapFrom.WithEntryOf(SwapToModded); - SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom); + SwapToModdedEntry = manipulations(SwapToIdentifier) ?? SwapToDefaultEntry; + SwapToIsDefault = SwapToModdedEntry.Equals(SwapToDefaultEntry); + SwapAppliedIsDefault = SwapToModdedEntry.Equals(SwapFromDefaultEntry); } + + IMetaIdentifier IMetaSwap.SwapFromIdentifier + => SwapFromIdentifier; + + IMetaIdentifier IMetaSwap.SwapToIdentifier + => SwapToIdentifier; + + object IMetaSwap.SwapFromDefaultEntry + => SwapFromDefaultEntry; + + object IMetaSwap.SwapToDefaultEntry + => SwapToDefaultEntry; + + object IMetaSwap.SwapToModdedEntry + => SwapToModdedEntry; } public sealed class FileSwap : Swap @@ -113,8 +146,7 @@ public sealed class FileSwap : Swap /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func redirections, - string swapFromRequest, string swapToRequest, - string? swapFromPreChange = null) + string swapFromRequest, string swapToRequest, string? swapFromPreChange = null) { var swap = new FileSwap { diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6010cdaf..62115dd6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -240,7 +240,7 @@ public class ItemSwapTab : IDisposable, ITab { return swap switch { - MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", + IMetaSwap meta => $"{meta.SwapFromIdentifier}: {meta.SwapFromDefaultEntry} -> {meta.SwapToModdedEntry}", FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}", _ => string.Empty, @@ -410,7 +410,7 @@ public class ItemSwapTab : IDisposable, ITab private ImRaii.IEndObject DrawTab(SwapType newTab) { - using var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); + var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); if (tab) { _dirty |= _lastTab != newTab; From d9b63320f07b5600bdd71b3420fd740e0f44e6d4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 20:46:29 +0200 Subject: [PATCH 1754/2451] Some small fixes, parse directly into MetaDictionary. --- Penumbra/Api/Api/TemporaryApi.cs | 24 ++++--------------- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 3 ++- Penumbra/Meta/Manipulations/MetaDictionary.cs | 16 ++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 995ec388..49958a0d 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -159,8 +159,7 @@ public class TemporaryApi( /// The empty string is treated as an empty set. /// Only returns true if all conversions are successful and distinct. /// - private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out MetaDictionary? manips) + private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) { if (manipString.Length == 0) { @@ -168,23 +167,10 @@ public class TemporaryApi( return true; } - if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) - { - manips = null; - return false; - } + if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion) + return true; - manips = []; - foreach (var manip in manipArray!.Where(m => m.Validate())) - { - if (manips.Add(manip)) - continue; - - Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); - manips = null; - return false; - } - - return true; + manips = null; + return false; } } diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index a8405eb2..15601867 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; @@ -49,7 +50,7 @@ public class TemporaryIpcTester( ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); - ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); + ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 51149e3b..3ce54afb 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,16 +52,16 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool Add(IMetaIdentifier identifier, object entry) + public bool TryAdd(IMetaIdentifier identifier, object entry) => identifier switch { - EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && Add(eqdpIdentifier, e), - EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && Add(eqpIdentifier, e), - EstIdentifier estIdentifier => entry is EstEntry e && Add(estIdentifier, e), - GlobalEqpManipulation globalEqpManipulation => Add(globalEqpManipulation), - GmpIdentifier gmpIdentifier => entry is GmpEntry e && Add(gmpIdentifier, e), - ImcIdentifier imcIdentifier => entry is ImcEntry e && Add(imcIdentifier, e), - RspIdentifier rspIdentifier => entry is RspEntry e && Add(rspIdentifier, e), + EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && TryAdd(eqdpIdentifier, e), + EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && TryAdd(eqpIdentifier, e), + EstIdentifier estIdentifier => entry is EstEntry e && TryAdd(estIdentifier, e), + GlobalEqpManipulation globalEqpManipulation => TryAdd(globalEqpManipulation), + GmpIdentifier gmpIdentifier => entry is GmpEntry e && TryAdd(gmpIdentifier, e), + ImcIdentifier imcIdentifier => entry is ImcEntry e && TryAdd(imcIdentifier, e), + RspIdentifier rspIdentifier => entry is RspEntry e && TryAdd(rspIdentifier, e), _ => false, }; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 021ee665..b0b588b4 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -76,7 +76,7 @@ public class ItemSwapContainer break; case IMetaSwap meta: if (!meta.SwapAppliedIsDefault) - convertedManips.Add(meta.SwapFromIdentifier, meta.SwapToModdedEntry); + convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry); break; } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 1813a7e3..396029d5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -432,7 +432,7 @@ public class DebugTab : Window, ITab foreach (var obj in _objects) { - ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); + ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty From 196ca2ce393ba160c1bc7c29f9375feeb64f1b1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 21:15:40 +0200 Subject: [PATCH 1755/2451] Remove all usages of Add(MetaManipulation) --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 137 +++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 54 ++++--- Penumbra/Mods/SubMods/SubMod.cs | 6 +- Penumbra/Mods/TemporaryMod.cs | 4 +- 4 files changed, 121 insertions(+), 80 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 3ce54afb..1dc6496e 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,38 +52,6 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool TryAdd(IMetaIdentifier identifier, object entry) - => identifier switch - { - EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && TryAdd(eqdpIdentifier, e), - EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && TryAdd(eqpIdentifier, e), - EstIdentifier estIdentifier => entry is EstEntry e && TryAdd(estIdentifier, e), - GlobalEqpManipulation globalEqpManipulation => TryAdd(globalEqpManipulation), - GmpIdentifier gmpIdentifier => entry is GmpEntry e && TryAdd(gmpIdentifier, e), - ImcIdentifier imcIdentifier => entry is ImcEntry e && TryAdd(imcIdentifier, e), - RspIdentifier rspIdentifier => entry is RspEntry e && TryAdd(rspIdentifier, e), - _ => false, - }; - - public bool Add(MetaManipulation manip) - { - var ret = manip.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), - MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, new EqdpEntryInternal(manip.Eqdp.Entry, manip.Eqdp.Slot)), - MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, new EqpEntryInternal(manip.Eqp.Entry, manip.Eqp.Slot)), - MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), - MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), - MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), - MetaManipulation.Type.GlobalEqp => _globalEqp.Add(manip.GlobalEqp), - _ => false, - }; - - if (ret) - ++Count; - return ret; - } - public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { if (!_imc.TryAdd(identifier, entry)) @@ -93,9 +61,29 @@ public sealed class MetaDictionary : IEnumerable return true; } + + public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) => TryAdd(identifier, new EqdpEntryInternal(entry, identifier.Slot)); @@ -159,6 +147,73 @@ public sealed class MetaDictionary : IEnumerable TryAdd(identifier); } + /// Try to merge all manipulations from manips into this, and return the first failure, if any. + public bool MergeForced(MetaDictionary manips, out IMetaIdentifier failedIdentifier) + { + foreach (var (identifier, entry) in manips._imc) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._eqp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._eqdp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._gmp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._rsp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._est) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var identifier in manips._globalEqp) + { + if (!TryAdd(identifier)) + { + failedIdentifier = identifier; + return false; + } + } + } + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) => _est.TryGetValue(identifier, out value); @@ -318,22 +373,4 @@ public sealed class MetaDictionary : IEnumerable return dict; } } - - private bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) - { - if (!_eqp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } - - private bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) - { - if (!_eqdp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index b0b588b4..72a6005d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -53,32 +53,38 @@ public class ItemSwapContainer { foreach (var swap in Swaps.SelectMany(s => s.WithChildren())) { - switch (swap) + if (swap is FileSwap file) { - case FileSwap file: - // Skip, nothing to do - if (file.SwapToModdedEqualsOriginal) - continue; + // Skip, nothing to do + if (file.SwapToModdedEqualsOriginal) + continue; - if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) - { - convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); - } - else - { - var path = file.GetNewPath(directory.FullName); - var bytes = file.FileData.Write(); - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - _manager.Compactor.WriteAllBytes(path, bytes); - convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); - } - - break; - case IMetaSwap meta: - if (!meta.SwapAppliedIsDefault) - convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry); - - break; + if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) + { + convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); + } + else + { + var path = file.GetNewPath(directory.FullName); + var bytes = file.FileData.Write(); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _manager.Compactor.WriteAllBytes(path, bytes); + convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); + } + } + else if (swap is IMetaSwap { SwapAppliedIsDefault: false }) + { + // @formatter:off + _ = swap switch + { + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + _ => false, + }; + // @formatter:on } } diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 06a924c8..40fd2e75 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -64,11 +64,9 @@ public static class SubMod data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } - var manips = json[nameof(data.Manipulations)]; + var manips = json[nameof(data.Manipulations)]?.ToObject(); if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - data.Manipulations.Add(s); + data.Manipulations.UnionWith(manips); } /// Load the relevant data for a selectable option from a JToken of that option. diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index a715f786..61ed4528 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,8 +93,8 @@ public class TemporaryMod : IMod } } - foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.Manipulations.Add(manip); + MetaDictionary manips = [.. collection.MetaCache?.Manipulations ?? []]; + defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); From 361082813b59c125949c3a3c092945a2844626e3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:23:08 +0200 Subject: [PATCH 1756/2451] tmp --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 198 +++++-- .../Meta/Manipulations/MetaManipulation.cs | 142 ++++- Penumbra/Mods/Editor/ModMerger.cs | 9 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 167 +----- Penumbra/Mods/Groups/ImcModGroup.cs | 14 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/Mods/ModCreator.cs | 6 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 3 +- Penumbra/Services/StaticServiceManager.cs | 1 - .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 353 ++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 522 ++++++++++-------- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 19 +- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 11 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 207 +------ Penumbra/Util/DictionaryExtensions.cs | 12 + 16 files changed, 1008 insertions(+), 660 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 1dc6496e..236157ae 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -7,7 +7,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public sealed class MetaDictionary : IEnumerable +public class MetaDictionary : IEnumerable { private readonly Dictionary _imc = []; private readonly Dictionary _eqp = []; @@ -17,8 +17,37 @@ public sealed class MetaDictionary : IEnumerable private readonly Dictionary _gmp = []; private readonly HashSet _globalEqp = []; + public IReadOnlyDictionary Imc + => _imc; + public int Count { get; private set; } + public int GetCount(MetaManipulation.Type type) + => type switch + { + MetaManipulation.Type.Imc => _imc.Count, + MetaManipulation.Type.Eqdp => _eqdp.Count, + MetaManipulation.Type.Eqp => _eqp.Count, + MetaManipulation.Type.Est => _est.Count, + MetaManipulation.Type.Gmp => _gmp.Count, + MetaManipulation.Type.Rsp => _rsp.Count, + MetaManipulation.Type.GlobalEqp => _globalEqp.Count, + _ => 0, + }; + + public bool CanAdd(IMetaIdentifier identifier) + => identifier switch + { + EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier), + EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier), + EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier), + GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation), + GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier), + ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier), + RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier), + _ => false, + }; + public void Clear() { _imc.Clear(); @@ -123,6 +152,68 @@ public sealed class MetaDictionary : IEnumerable return true; } + public bool Update(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.ContainsKey(identifier)) + return false; + + _imc[identifier] = entry; + return true; + } + + + public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.ContainsKey(identifier)) + return false; + + _eqp[identifier] = entry; + return true; + } + + public bool Update(EqpIdentifier identifier, EqpEntry entry) + => Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + + public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.ContainsKey(identifier)) + return false; + + _eqdp[identifier] = entry; + return true; + } + + public bool Update(EqdpIdentifier identifier, EqdpEntry entry) + => Update(identifier, new EqdpEntryInternal(entry, identifier.Slot)); + + public bool Update(EstIdentifier identifier, EstEntry entry) + { + if (!_est.ContainsKey(identifier)) + return false; + + _est[identifier] = entry; + return true; + } + + public bool Update(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.ContainsKey(identifier)) + return false; + + _gmp[identifier] = entry; + return true; + } + + public bool Update(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.ContainsKey(identifier)) + return false; + + _rsp[identifier] = entry; + return true; + } + public void UnionWith(MetaDictionary manips) { foreach (var (identifier, entry) in manips._imc) @@ -148,70 +239,52 @@ public sealed class MetaDictionary : IEnumerable } /// Try to merge all manipulations from manips into this, and return the first failure, if any. - public bool MergeForced(MetaDictionary manips, out IMetaIdentifier failedIdentifier) + public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) { - foreach (var (identifier, entry) in manips._imc) + foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._eqp) + foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._eqdp) + foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._gmp) + foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._rsp) + foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._est) + foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var identifier in manips._globalEqp) + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { - if (!TryAdd(identifier)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } + + failedIdentifier = default; + return false; } public bool TryGetValue(EstIdentifier identifier, out EstEntry value) @@ -244,6 +317,18 @@ public sealed class MetaDictionary : IEnumerable Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; } + public void UpdateTo(MetaDictionary other) + { + _imc.UpdateTo(other._imc); + _eqp.UpdateTo(other._eqp); + _eqdp.UpdateTo(other._eqdp); + _est.UpdateTo(other._est); + _rsp.UpdateTo(other._rsp); + _gmp.UpdateTo(other._gmp); + _globalEqp.UnionWith(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + public MetaDictionary Clone() { var ret = new MetaDictionary(); @@ -251,6 +336,31 @@ public sealed class MetaDictionary : IEnumerable return ret; } + private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry) + { + var type = identifier switch + { + ImcIdentifier => "Imc", + EqdpIdentifier => "Eqdp", + EqpIdentifier => "Eqp", + EstIdentifier => "Est", + GmpIdentifier => "Gmp", + RspIdentifier => "Rsp", + GlobalEqpManipulation => "GlobalEqp", + _ => string.Empty, + }; + + if (type.Length == 0) + return; + + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(type); + writer.WritePropertyName("Manipulation"); + + writer.WriteEndObject(); + } + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f22de809..b80681d2 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,9 +1,148 @@ +using Dalamud.Interface; +using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using OtterGui; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods.Editor; using Penumbra.String.Functions; +using Penumbra.UI; +using Penumbra.UI.ModsTab; -namespace Penumbra.Meta.Manipulations; +namespace Penumbra.Meta.Manipulations; + +#if false +private static class ImcRow +{ + private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; + + private static float IdWidth + => 80 * UiHelpers.Scale; + + private static float SmallIdWidth + => 45 * UiHelpers.Scale; + + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, + editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); + var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); + var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); + var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(manip); + + // Identifier + ImGui.TableNextColumn(); + var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); + + ImGui.TableNextColumn(); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + + ImGui.TableNextColumn(); + // Equipment and accessories are slightly different imcs than other types. + if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); + else + change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); + + ImGui.TableNextColumn(); + change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); + + ImGui.TableNextColumn(); + if (_newIdentifier.ObjectType is ObjectType.DemiHuman) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); + else + ImUtf8.ScaledDummy(new Vector2(70 * UiHelpers.Scale, 0)); + + if (change) + defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); + } + + public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.ObjectType.ToName()); + ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.PrimaryId.ToString()); + ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else + { + ImGui.TextUnformatted(meta.SecondaryId.ToString()); + ImGuiUtil.HoverTooltip(SecondaryIdTooltip); + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Variant.ToString()); + ImGuiUtil.HoverTooltip(VariantIdTooltip); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.DemiHuman) + { + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + + // Values + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.TableNextColumn(); + var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; + var newEntry = meta.Entry; + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); + ImGui.TableNextColumn(); + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); + ImGui.TableNextColumn(); + changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); + + if (changes) + editor.MetaEditor.Change(meta.Copy(newEntry)); + } +} + +#endif public interface IMetaManipulation { @@ -315,3 +454,4 @@ public readonly struct MetaManipulation : IEquatable, ICompara public static bool operator >=(MetaManipulation left, MetaManipulation right) => left.CompareTo(right) >= 0; } + diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 4faced80..2df76838 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -162,12 +162,9 @@ public class ModMerger : IDisposable foreach (var originalOption in mergeOptions) { - foreach (var manip in originalOption.Manipulations) - { - if (!manips.Add(manip)) - throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); - } + if (!manips.MergeForced(originalOption.Manipulations, out var failed)) + throw new Exception( + $"Could not add meta manipulation {failed} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); foreach (var (swapA, swapB) in originalOption.FileSwaps) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 45d9f8a1..42171378 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,24 +1,24 @@ using System.Collections.Frozen; +using OtterGui.Services; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager) +public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService { - private readonly HashSet _imc = []; - private readonly HashSet _eqp = []; - private readonly HashSet _eqdp = []; - private readonly HashSet _gmp = []; - private readonly HashSet _est = []; - private readonly HashSet _rsp = []; - private readonly HashSet _globalEqp = []; - public sealed class OtherOptionData : HashSet { public int TotalCount; + public void Add(string name, int count) + { + if (count > 0) + Add(name); + TotalCount += count; + } + public new void Clear() { TotalCount = 0; @@ -31,91 +31,9 @@ public class ModMetaEditor(ModManager modManager) public bool Changes { get; private set; } - public IReadOnlySet Imc - => _imc; - - public IReadOnlySet Eqp - => _eqp; - - public IReadOnlySet Eqdp - => _eqdp; - - public IReadOnlySet Gmp - => _gmp; - - public IReadOnlySet Est - => _est; - - public IReadOnlySet Rsp - => _rsp; - - public IReadOnlySet GlobalEqp - => _globalEqp; - - public bool CanAdd(MetaManipulation m) + public new void Clear() { - return m.ManipulationType switch - { - MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), - MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), - MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), - MetaManipulation.Type.Est => !_est.Contains(m.Est), - MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), - MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), - MetaManipulation.Type.GlobalEqp => !_globalEqp.Contains(m.GlobalEqp), - _ => false, - }; - } - - public bool Add(MetaManipulation m) - { - var added = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Add(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), - MetaManipulation.Type.Est => _est.Add(m.Est), - MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Add(m.GlobalEqp), - _ => false, - }; - Changes |= added; - return added; - } - - public bool Delete(MetaManipulation m) - { - var deleted = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), - MetaManipulation.Type.Est => _est.Remove(m.Est), - MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(m.GlobalEqp), - _ => false, - }; - Changes |= deleted; - return deleted; - } - - public bool Change(MetaManipulation m) - => Delete(m) && Add(m); - - public bool Set(MetaManipulation m) - => Delete(m) | Add(m); - - public void Clear() - { - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _gmp.Clear(); - _est.Clear(); - _rsp.Clear(); - _globalEqp.Clear(); + base.Clear(); Changes = true; } @@ -129,15 +47,19 @@ public class ModMetaEditor(ModManager modManager) if (option == currentOption) continue; - foreach (var manip in option.Manipulations) - { - var data = OtherData[manip.ManipulationType]; - ++data.TotalCount; - data.Add(option.GetFullName()); - } + var name = option.GetFullName(); + OtherData[MetaManipulation.Type.Imc].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Imc)); + OtherData[MetaManipulation.Type.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqp)); + OtherData[MetaManipulation.Type.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqdp)); + OtherData[MetaManipulation.Type.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Gmp)); + OtherData[MetaManipulation.Type.Est].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Est)); + OtherData[MetaManipulation.Type.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Rsp)); + OtherData[MetaManipulation.Type.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.GlobalEqp)); } - Split(currentOption.Manipulations); + Clear(); + UnionWith(currentOption.Manipulations); + Changes = false; } public void Apply(IModDataContainer container) @@ -145,50 +67,7 @@ public class ModMetaEditor(ModManager modManager) if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, [..Recombine()]); + modManager.OptionEditor.SetManipulations(container, this); Changes = false; } - - private void Split(IEnumerable manips) - { - Clear(); - foreach (var manip in manips) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - _imc.Add(manip.Imc); - break; - case MetaManipulation.Type.Eqdp: - _eqdp.Add(manip.Eqdp); - break; - case MetaManipulation.Type.Eqp: - _eqp.Add(manip.Eqp); - break; - case MetaManipulation.Type.Est: - _est.Add(manip.Est); - break; - case MetaManipulation.Type.Gmp: - _gmp.Add(manip.Gmp); - break; - case MetaManipulation.Type.Rsp: - _rsp.Add(manip.Rsp); - break; - case MetaManipulation.Type.GlobalEqp: - _globalEqp.Add(manip.GlobalEqp); - break; - } - } - - Changes = false; - } - - public IEnumerable Recombine() - => _imc.Select(m => (MetaManipulation)m) - .Concat(_eqdp.Select(m => (MetaManipulation)m)) - .Concat(_eqp.Select(m => (MetaManipulation)m)) - .Concat(_est.Select(m => (MetaManipulation)m)) - .Concat(_gmp.Select(m => (MetaManipulation)m)) - .Concat(_rsp.Select(m => (MetaManipulation)m)) - .Concat(_globalEqp.Select(m => (MetaManipulation)m)); } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 383bc9fd..c52828c0 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -95,28 +95,28 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask, Variant variant) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, - Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + public ImcEntry GetEntry(ushort mask) + => DefaultEntry with { AttributeMask = mask }; public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); + var mask = GetCurrentMask(setting); + var entry = GetEntry(mask); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); else for (var i = 0; i <= count; ++i) - manipulations.Add(GetManip(mask, (Variant)i)); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); } else { - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 72a6005d..8328edea 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -124,7 +124,7 @@ public class ItemSwapContainer private MetaDictionary MetaResolver(ModCollection? collection) => collection?.MetaCache?.Manipulations is { } cache - ? [.. cache] + ? [] // [.. cache] TODO : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0035fd41..e8ca3199 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,7 +198,8 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith([.. meta.MetaManipulations]); + // TODO + option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -212,7 +213,8 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith([.. rgsp.MetaManipulations]); + // TODO + option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 40fd2e75..a8c37369 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -37,7 +37,7 @@ public static class SubMod { to.Files = new Dictionary(from.Files); to.FileSwaps = new Dictionary(from.FileSwaps); - to.Manipulations = [.. from.Manipulations]; + to.Manipulations = from.Manipulations.Clone(); } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 61ed4528..91c4c5df 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,7 +93,8 @@ public class TemporaryMod : IMod } } - MetaDictionary manips = [.. collection.MetaCache?.Manipulations ?? []]; + // TODO + MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []]; defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 0c6648ba..35e36349 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -188,7 +188,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs new file mode 100644 index 00000000..d9a8c27c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -0,0 +1,353 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + private bool _fileExists; + + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string PrimaryIdTooltipShort = "Primary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; + + protected override void Initialize() + { + Identifier = ImcIdentifier.Default; + UpdateEntry(); + } + + private void UpdateEntry() + => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + // Copy To Clipboard + ImGui.TableNextColumn(); + var canAdd = _fileExists && Editor.MetaEditor.CanAdd(Identifier); + var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.MetaEditor.TryAdd(Identifier, Entry); + + if (DrawIdentifier(ref Identifier)) + UpdateEntry(); + + using var disabled = ImRaii.Disabled(); + DrawEntry(Entry, ref Entry, false); + } + + protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry) + { + const uint frameColor = 0; + // Meta Buttons + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.ObjectType.ToName(), frameColor); + ImUtf8.HoverTooltip("Object Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", frameColor); + ImUtf8.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + else + { + ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", frameColor); + ImUtf8.HoverTooltip("Secondary ID"u8); + } + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.Variant.Id}", frameColor); + ImUtf8.HoverTooltip("Variant"u8); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + if (DrawEntry(defaultEntry, ref entry, true)) + Editor.MetaEditor.Update(identifier, entry); + } + + private static bool DrawIdentifier(ref ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + var change = DrawObjectType(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= DrawSlot(ref identifier); + else + change |= DrawSecondaryId(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawVariant(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + change |= DrawSlot(ref identifier, 70f); + else + ImUtf8.ScaledDummy(70f); + return change; + } + + private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) + { + ImGui.TableNextColumn(); + var change = DrawMaterialId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawMaterialAnimationId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawDecalId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawVfxId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawSoundId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawAttributes(defaultEntry, ref entry); + return change; + } + + + protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() + => Editor.MetaEditor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) + { + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + identifier = identifier with + { + ObjectType = type, + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { PrimaryId = newId }; + return ret; + } + + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + identifier = identifier with { SecondaryId = newId }; + return ret; + } + + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + identifier = identifier with { Variant = (byte)newId }; + return ret; + } + + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (identifier.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { EquipSlot = slot }; + return ret; + } + + public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, + out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialId = newValue }; + return true; + } + + public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, + defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialAnimationId = newValue }; + return true; + } + + public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { DecalId = newValue }; + return true; + } + + public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, + byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { VfxId = newValue }; + return true; + } + + public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { SoundId = newValue }; + return true; + } + + public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + { + var changes = false; + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (entry.AttributeMask & flag) != 0; + var def = (defaultEntry.AttributeMask & flag) != 0; + if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) + { + var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); + entry = entry with { AttributeMask = newMask }; + changes = true; + } + + if (i < ImcEntry.NumAttributes - 1) + ImUtf8.SameLineInner(); + } + + return changes; + } + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index b0a74637..50862eec 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,10 +1,11 @@ +using System.Reflection.Emit; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; @@ -20,17 +21,7 @@ public partial class ModEditWindow private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIdTooltipShort = "Primary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + private void DrawMetaTab() { @@ -56,7 +47,7 @@ public partial class ModEditWindow ImGui.SameLine(); SetFromClipboardButton(); ImGui.SameLine(); - CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); + CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) _metaFileManager.WriteAllTexToolsMeta(Mod!); @@ -65,71 +56,103 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqp]); - DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqdp]); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Imc]); - DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Est]); - DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Gmp]); - DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Rsp]); - DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, - GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherData[MetaManipulation.Type.GlobalEqp]); + DrawEditHeader(MetaManipulation.Type.Eqp); + DrawEditHeader(MetaManipulation.Type.Eqdp); + DrawEditHeader(MetaManipulation.Type.Imc); + DrawEditHeader(MetaManipulation.Type.Est); + DrawEditHeader(MetaManipulation.Type.Gmp); + DrawEditHeader(MetaManipulation.Type.Rsp); + DrawEditHeader(MetaManipulation.Type.GlobalEqp); } - - /// The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, - Action draw, Action drawNew, - ModMetaEditor.OtherOptionData otherOptionData) - { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - - var oldPos = ImGui.GetCursorPosY(); - var header = ImGui.CollapsingHeader($"{items.Count} {label}"); - var newPos = ImGui.GetCursorPos(); - if (otherOptionData.TotalCount > 0) + private static ReadOnlySpan Label(MetaManipulation.Type type) + => type switch { - var text = $"{otherOptionData.TotalCount} Edits in other Options"; - var size = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); - ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); - if (ImGui.IsItemHovered()) - { - using var tt = ImUtf8.Tooltip(); - foreach (var name in otherOptionData) - ImUtf8.Text(name); - } + MetaManipulation.Type.Imc => "Variant Edits (IMC)###IMC"u8, + MetaManipulation.Type.Eqdp => "Racial Model Edits (EQDP)###EQDP"u8, + MetaManipulation.Type.Eqp => "Equipment Parameter Edits (EQP)###EQP"u8, + MetaManipulation.Type.Est => "Extra Skeleton Parameters (EST)###EST"u8, + MetaManipulation.Type.Gmp => "Visor/Gimmick Edits (GMP)###GMP"u8, + MetaManipulation.Type.Rsp => "Racial Scaling Edits (RSP)###RSP"u8, + MetaManipulation.Type.GlobalEqp => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8, + _ => "\0"u8, + }; - ImGui.SetCursorPos(newPos); - } + private static int ColumnCount(MetaManipulation.Type type) + => type switch + { + MetaManipulation.Type.Imc => 10, + MetaManipulation.Type.Eqdp => 7, + MetaManipulation.Type.Eqp => 5, + MetaManipulation.Type.Est => 7, + MetaManipulation.Type.Gmp => 7, + MetaManipulation.Type.Rsp => 5, + MetaManipulation.Type.GlobalEqp => 4, + _ => 0, + }; + private void DrawEditHeader(MetaManipulation.Type type) + { + var oldPos = ImGui.GetCursorPosY(); + var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {Label(type)}"); + DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos()); if (!header) return; - using (var table = ImRaii.Table(label, numColumns, flags)) - { - if (table) - { - drawNew(_metaFileManager, _editor, _iconSize); - foreach (var (item, index) in items.ToArray().WithIndex()) - { - using var id = ImRaii.PushId(index); - draw(_metaFileManager, item, _editor, _iconSize); - } - } - } + DrawTable(type); + } + private IMetaDrawer? Drawer(MetaManipulation.Type type) + => type switch + { + //MetaManipulation.Type.Imc => expr, + //MetaManipulation.Type.Eqdp => expr, + //MetaManipulation.Type.Eqp => expr, + //MetaManipulation.Type.Est => expr, + //MetaManipulation.Type.Gmp => expr, + //MetaManipulation.Type.Rsp => expr, + //MetaManipulation.Type.GlobalEqp => expr, + _ => null, + }; + + private void DrawTable(MetaManipulation.Type type) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + using var table = ImUtf8.Table(Label(type), ColumnCount(type), flags); + if (!table) + return; + + if (Drawer(type) is not { } drawer) + return; + + drawer.Draw(); ImGui.NewLine(); } + private void DrawOtherOptionData(MetaManipulation.Type type, float oldPos, Vector2 newPos) + { + var otherOptionData = _editor.MetaEditor.OtherData[type]; + if (otherOptionData.TotalCount <= 0) + return; + + var text = $"{otherOptionData.TotalCount} Edits in other Options"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var name in otherOptionData) + ImUtf8.Text(name); + } + + ImGui.SetCursorPos(newPos); + } + +#if false private static class EqpRow { - private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); + private static EqpIdentifier _newIdentifier = new(1, EquipSlot.Body); private static float IdWidth => 100 * UiHelpers.Scale; @@ -140,8 +163,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -197,7 +220,7 @@ public partial class ModEditWindow var idx = 0; foreach (var flag in Eqp.EqpAttributes[meta.Slot]) { - using var id = ImRaii.PushId(idx++); + using var id = ImRaii.PushId(idx++); var defaultValue = defaultEntry.HasFlag(flag); var currentValue = meta.Entry.HasFlag(flag); if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) @@ -209,8 +232,6 @@ public partial class ModEditWindow ImGui.NewLine(); } } - - private static class EqdpRow { private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); @@ -224,9 +245,9 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace(_new.Gender, _new.Race); + var raceCode = Names.CombinedRace(_new.Gender, _new.Race); var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; - var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); + var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; var defaultEntry = validRaceCode @@ -311,7 +332,7 @@ public partial class ModEditWindow var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); - var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); + var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); ImGui.TableNextColumn(); if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); @@ -321,136 +342,7 @@ public partial class ModEditWindow editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); } } - - private static class ImcRow - { - private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, - editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); - var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); - var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); - var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(manip); - - // Identifier - ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); - else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); - - ImGui.TableNextColumn(); - if (_newIdentifier.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); - else - ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); - - if (change) - defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); - } - - public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.ObjectType.ToName()); - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else - { - ImGui.TextUnformatted(meta.SecondaryId.ToString()); - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Variant.ToString()); - ImGuiUtil.HoverTooltip(VariantIdTooltip); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.DemiHuman) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - - // Values - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.TableNextColumn(); - var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; - var newEntry = meta.Entry; - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - - if (changes) - editor.MetaEditor.Change(meta.Copy(newEntry)); - } - } - + private static class EstRow { private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); @@ -464,8 +356,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -538,12 +430,11 @@ public partial class ModEditWindow // Values var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, + if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, out var entry, 0, ushort.MaxValue, 0.05f)) editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); } } - private static class GmpRow { private static GmpManipulation _new = new(GmpEntry.Default, 1); @@ -563,8 +454,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -643,7 +534,6 @@ public partial class ModEditWindow editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); } } - private static class RspRow { private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); @@ -657,8 +547,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -700,7 +590,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; + var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; var value = meta.Entry.Value; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, @@ -713,12 +603,11 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); } } - private static class GlobalEqpRow { private static GlobalEqpManipulation _new = new() { - Type = GlobalEqpType.DoNotHideEarrings, + Type = GlobalEqpType.DoNotHideEarrings, Condition = 1, }; @@ -729,7 +618,7 @@ public partial class ModEditWindow editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; + var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new); @@ -744,7 +633,7 @@ public partial class ModEditWindow if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) _new = new GlobalEqpManipulation { - Type = type, + Type = type, Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, }; ImUtf8.HoverTooltip(type.ToDescription()); @@ -777,6 +666,7 @@ public partial class ModEditWindow } } } +#endif // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. @@ -824,7 +714,7 @@ public partial class ModEditWindow return newValue != currentValue; } - private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, IEnumerable manipulations) + private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; @@ -840,10 +730,9 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); + var version = Functions.FromCompressedBase64(clipboard, out var manips); if (version == MetaManipulation.CurrentVersion && manips != null) - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); + _editor.MetaEditor.UpdateTo(manips); } ImGuiUtil.HoverTooltip( @@ -855,13 +744,9 @@ public partial class ModEditWindow if (ImGui.Button("Set from Clipboard")) { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); + var version = Functions.FromCompressedBase64(clipboard, out var manips); if (version == MetaManipulation.CurrentVersion && manips != null) - { - _editor.MetaEditor.Clear(); - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); - } + _editor.MetaEditor.SetTo(manips); } ImGuiUtil.HoverTooltip( @@ -870,11 +755,184 @@ public partial class ModEditWindow private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) - editor.MetaEditor.Delete(meta); + //ImGui.TableNextColumn(); + //CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); + // + //ImGui.TableNextColumn(); + //if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) + // editor.MetaEditor.Delete(meta); } +} + + +public interface IMetaDrawer +{ + public void Draw(); } + + + + +public abstract class MetaDrawer(ModEditor editor, MetaFileManager metaFiles) : IMetaDrawer + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + protected readonly ModEditor Editor = editor; + protected readonly MetaFileManager MetaFiles = metaFiles; + protected TIdentifier Identifier; + protected TEntry Entry; + private bool _initialized; + + public void Draw() + { + if (!_initialized) + { + Initialize(); + _initialized = true; + } + + DrawNew(); + foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) + { + using var id = ImUtf8.PushId(idx); + DrawEntry(identifier, entry); + } + } + + protected abstract void DrawNew(); + protected abstract void Initialize(); + protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); + + protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); +} + + +#if false +public sealed class GmpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new GmpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) + { } + + protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EstMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) + { } + + protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EqdpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqdpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntry entry) + { } + + protected override IEnumerable<(EqdpIdentifier, EqdpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EqpMetaDrawer(ModEditor editor, MetaFileManager metaManager) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EqpIdentifier identifier, EqpEntry entry) + { } + + protected override IEnumerable<(EqpIdentifier, EqpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class RspMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new RspIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) + { } + + protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + + + +public sealed class GlobalEqpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) + { } + + protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} +#endif diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 3ac10cd0..689571f3 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -9,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab.Groups; @@ -79,29 +80,29 @@ public class AddGroupDrawer : IUiService private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); + var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawPrimaryId(ref _imcIdentifier, width); if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 5c8edce6..5d10febd 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; +using Penumbra.UI.AdvancedWindow.Meta; namespace Penumbra.UI.ModsTab.Groups; @@ -37,9 +38,9 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImGui.SameLine(); using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); } ImGui.SameLine(0, editor.PriorityWidth); @@ -54,8 +55,8 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); var canBeDisabled = group.CanBeDisabled; if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 694ae11c..1291f568 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -10,210 +10,5 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) - { - var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - identifier = identifier with - { - ObjectType = type, - EquipSlot = equipSlot, - SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, - }; - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - identifier.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - identifier = identifier with { PrimaryId = newId }; - return ret; - } - - public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - identifier = identifier with { SecondaryId = newId }; - return ret; - } - - public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - identifier = identifier with { Variant = (byte)newId }; - return ret; - } - - public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (identifier.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - identifier = identifier with { EquipSlot = slot }; - return ret; - } - - public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, - out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialId = newValue }; - return true; - } - - public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, - defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialAnimationId = newValue }; - return true; - } - - public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { DecalId = newValue }; - return true; - } - - public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, - byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { VfxId = newValue }; - return true; - } - - public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { SoundId = newValue }; - return true; - } - - public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) - { - var changes = false; - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - var value = (entry.AttributeMask & flag) != 0; - var def = (defaultEntry.AttributeMask & flag) != 0; - if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) - { - var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); - entry = entry with { AttributeMask = newMask }; - changes = true; - } - - if (i < ImcEntry.NumAttributes - 1) - ImGui.SameLine(); - } - - return changes; - } - - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - /// - /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - /// Returns true if newValue changed against currentValue. - /// - private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, - out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) - newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; - - if (addDefault) - ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - - return newValue != currentValue; - } - - /// - /// A checkmark that compares against a default value and shows a tooltip. - /// Returns true if newValue is changed against currentValue. - /// - private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, - out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImUtf8.Checkbox(label, ref newValue); - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - return newValue != currentValue; - } + } diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index abf715e6..f7aa5598 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -45,6 +45,18 @@ public static class DictionaryExtensions lhs.Add(key, value); } + /// Set all entries in the right-hand dictionary to the same values in the left-hand dictionary, ensuring capacity beforehand. + public static void UpdateTo(this Dictionary lhs, IReadOnlyDictionary rhs) + where TKey : notnull + { + if (ReferenceEquals(lhs, rhs)) + return; + + lhs.EnsureCapacity(rhs.Count); + foreach (var (key, value) in rhs) + lhs[key] = value; + } + /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. public static void SetTo(this HashSet lhs, IReadOnlySet rhs) { From 3170edfeb659f0362d46f56fde9c000ee54ba0ee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 13:38:36 +0200 Subject: [PATCH 1757/2451] Get rid off all MetaManipulation things. --- Penumbra/Api/Api/MetaApi.cs | 28 +- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 8 +- Penumbra/Collections/Cache/CmpCache.cs | 56 -- Penumbra/Collections/Cache/CollectionCache.cs | 40 +- .../Collections/Cache/CollectionModData.cs | 12 +- Penumbra/Collections/Cache/EqdpCache.cs | 85 +- Penumbra/Collections/Cache/EqpCache.cs | 77 +- Penumbra/Collections/Cache/EstCache.cs | 136 ++- .../Cache}/GlobalEqpCache.cs | 44 +- Penumbra/Collections/Cache/GmpCache.cs | 74 +- Penumbra/Collections/Cache/IMetaCache.cs | 89 ++ Penumbra/Collections/Cache/ImcCache.cs | 153 ++-- Penumbra/Collections/Cache/MetaCache.cs | 269 +++--- Penumbra/Collections/Cache/RspCache.cs | 81 ++ Penumbra/Import/Models/ModelManager.cs | 23 +- .../Import/TexToolsMeta.Deserialization.cs | 61 +- Penumbra/Import/TexToolsMeta.Export.cs | 328 ++++--- Penumbra/Import/TexToolsMeta.Rgsp.cs | 17 +- Penumbra/Import/TexToolsMeta.cs | 25 +- .../PathResolving/CollectionResolver.cs | 1 - .../ResolveContext.PathResolution.cs | 2 +- Penumbra/Meta/ImcChecker.cs | 4 - Penumbra/Meta/Manipulations/Eqdp.cs | 9 + .../Meta/Manipulations/EqdpManipulation.cs | 110 --- Penumbra/Meta/Manipulations/Eqp.cs | 3 + .../Meta/Manipulations/EqpManipulation.cs | 80 -- Penumbra/Meta/Manipulations/Est.cs | 16 + .../Meta/Manipulations/EstManipulation.cs | 108 --- .../Manipulations/GlobalEqpManipulation.cs | 26 +- Penumbra/Meta/Manipulations/Gmp.cs | 3 + .../Meta/Manipulations/GmpManipulation.cs | 58 -- .../Meta/Manipulations/IMetaIdentifier.cs | 16 + Penumbra/Meta/Manipulations/Imc.cs | 6 +- .../Meta/Manipulations/ImcManipulation.cs | 108 --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 338 +++++-- .../Meta/Manipulations/MetaManipulation.cs | 457 ---------- Penumbra/Meta/Manipulations/Rsp.cs | 3 + .../Meta/Manipulations/RspManipulation.cs | 67 -- Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 24 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 41 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 29 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/Mods/ModCreator.cs | 6 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 3 +- .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 159 ++++ .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 134 +++ .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 147 +++ .../Meta/GlobalEqpMetaDrawer.cs | 111 +++ .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 148 +++ .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 164 ++-- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 154 ++++ .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 35 + .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 112 +++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 847 +----------------- .../ModEditWindow.Models.MdlTab.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 6 +- Penumbra/UI/Tabs/EffectiveTab.cs | 10 +- Penumbra/Util/IdentifierExtensions.cs | 94 +- 63 files changed, 2422 insertions(+), 2847 deletions(-) delete mode 100644 Penumbra/Collections/Cache/CmpCache.cs rename Penumbra/{Meta/Manipulations => Collections/Cache}/GlobalEqpCache.cs (75%) create mode 100644 Penumbra/Collections/Cache/IMetaCache.cs create mode 100644 Penumbra/Collections/Cache/RspCache.cs delete mode 100644 Penumbra/Meta/Manipulations/EqdpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/EqpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/EstManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/GmpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/ImcManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/MetaManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/RspManipulation.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index c467df58..ce1a9def 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -1,5 +1,8 @@ +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; @@ -7,17 +10,34 @@ namespace Penumbra.Api.Api; public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService { + public const int CurrentVersion = 0; + public string GetPlayerMetaManipulations() { var collection = collectionResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + return CompressMetaManipulations(collection); } public string GetMetaManipulations(int gameObjectIdx) { helpers.AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + return CompressMetaManipulations(collection); + } + + internal static string CompressMetaManipulations(ModCollection collection) + { + var array = new JArray(); + if (collection.MetaCache is { } cache) + { + MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key)); + MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + } + + return Functions.ToCompressedBase64(array, CurrentVersion); } } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 49958a0d..0894a8e5 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -163,11 +163,11 @@ public class TemporaryApi( { if (manipString.Length == 0) { - manips = []; + manips = new MetaDictionary(); return true; } - if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion) + if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion) return true; manips = null; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 15601867..0aa6821c 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; @@ -102,8 +103,7 @@ public class TemporaryIpcTester( && copyCollection is { HasCache: true }) { var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); - var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), - MetaManipulation.CurrentVersion); + var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -188,8 +188,8 @@ public class TemporaryIpcTester( if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - foreach (var manip in mod.Default.Manipulations) - ImGui.TextUnformatted(manip.ToString()); + foreach (var identifier in mod.Default.Manipulations.Identifiers) + ImGui.TextUnformatted(identifier.ToString()); } } } diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs deleted file mode 100644 index 470cadd4..00000000 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ /dev/null @@ -1,56 +0,0 @@ -using OtterGui.Filesystem; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections.Cache; - -public struct CmpCache : IDisposable -{ - private CmpFile? _cmpFile = null; - private readonly List _cmpManipulations = new(); - - public CmpCache() - { } - - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); - - public void Reset() - { - if (_cmpFile == null) - return; - - _cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute))); - _cmpManipulations.Clear(); - } - - public bool ApplyMod(MetaFileManager manager, RspManipulation manip) - { - _cmpManipulations.AddOrReplace(manip); - _cmpFile ??= new CmpFile(manager); - return manip.Apply(_cmpFile); - } - - public bool RevertMod(MetaFileManager manager, RspManipulation manip) - { - if (!_cmpManipulations.Remove(manip)) - return false; - - var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute); - manip = new RspManipulation(manip.SubRace, manip.Attribute, def); - return manip.Apply(_cmpFile!); - } - - public void Dispose() - { - _cmpFile?.Dispose(); - _cmpFile = null; - _cmpManipulations.Clear(); - } -} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4d8d0b4a..fd801d3b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -36,7 +36,7 @@ public sealed class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -233,8 +233,20 @@ public sealed class CollectionCache : IDisposable foreach (var (path, file) in files.FileRedirections) AddFile(path, file, mod); - foreach (var manip in files.Manipulations) - AddManipulation(manip, mod); + foreach (var (identifier, entry) in files.Manipulations.Eqp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Eqdp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Est) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Gmp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Rsp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Imc) + AddManipulation(mod, identifier, entry); + foreach (var identifier in files.Manipulations.GlobalEqp) + AddManipulation(mod, identifier, null!); if (addMetaChanges) { @@ -342,7 +354,7 @@ public sealed class CollectionCache : IDisposable foreach (var conflict in tmpConflicts) { if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0) + || data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0) AddConflict(data, addedMod, conflict.Mod2); } @@ -374,12 +386,12 @@ public sealed class CollectionCache : IDisposable // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddManipulation(MetaManipulation manip, IMod mod) + private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry) { - if (!Meta.TryGetValue(manip, out var existingMod)) + if (!Meta.TryGetMod(identifier, out var existingMod)) { - Meta.ApplyMod(manip, mod); - ModData.AddManip(mod, manip); + Meta.ApplyMod(mod, identifier, entry); + ModData.AddManip(mod, identifier); return; } @@ -387,11 +399,11 @@ public sealed class CollectionCache : IDisposable if (mod == existingMod) return; - if (AddConflict(manip, mod, existingMod)) + if (AddConflict(identifier, mod, existingMod)) { - ModData.RemoveManip(existingMod, manip); - Meta.ApplyMod(manip, mod); - ModData.AddManip(mod, manip); + ModData.RemoveManip(existingMod, identifier); + Meta.ApplyMod(mod, identifier, entry); + ModData.AddManip(mod, identifier); } } @@ -437,9 +449,9 @@ public sealed class CollectionCache : IDisposable AddItems(modPath.Mod); } - foreach (var (manip, mod) in Meta) + foreach (var (manip, mod) in Meta.IdentifierSources) { - identifier.MetaChangedItems(items, manip); + manip.AddChangedItems(identifier, items); AddItems(mod); } diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index d0a3bc76..295191d2 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache; /// public class CollectionModData { - private readonly Dictionary, HashSet)> _data = new(); + private readonly Dictionary, HashSet)> _data = new(); - public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data - => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); + public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data + => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); - public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) + public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) { if (_data.Remove(mod, out var data)) return data; @@ -35,7 +35,7 @@ public class CollectionModData } } - public void AddManip(IMod mod, MetaManipulation manipulation) + public void AddManip(IMod mod, IMetaIdentifier manipulation) { if (_data.TryGetValue(mod, out var data)) { @@ -54,7 +54,7 @@ public class CollectionModData _data.Remove(mod); } - public void RemoveManip(IMod mod, MetaManipulation manip) + public void RemoveManip(IMod mod, IMetaIdentifier manip) { if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0) _data.Remove(mod); diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index a0f27c23..f3475c7e 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,5 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui; -using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -10,28 +10,38 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public readonly struct EqdpCache : IDisposable +public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - private readonly List _eqdpManipulations = new(); + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - public EqdpCache() - { } - - public void SetFiles(MetaFileManager manager) + public override void SetFiles() { for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) - manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); + Manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); } - public void SetFile(MetaFileManager manager, MetaIndex index) + public void SetFile(MetaIndex index) { var i = CharacterUtilityData.EqdpIndices.IndexOf(index); if (i != -1) - manager.SetFile(_eqdpFiles[i], index); + Manager.SetFile(_eqdpFiles[i], index); } - public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); + + protected override void IncorporateChangesInternal() + { + foreach (var (identifier, (_, entry)) in this) + Apply(GetFile(identifier)!, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQDP manipulations."); + } + + public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) + => _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar + + public MetaList.MetaReverter? TemporarilySetFile(GenderRace genderRace, bool accessory) { var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); if (idx < 0) @@ -47,44 +57,44 @@ public readonly struct EqdpCache : IDisposable return null; } - return manager.TemporarilySetFile(_eqdpFiles[i], idx); + return Manager.TemporarilySetFile(_eqdpFiles[i], idx); } - public void Reset() + public override void Reset() { foreach (var file in _eqdpFiles.OfType()) { var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId)); + file.Reset(Keys.Where(m => m.FileIndex() == relevant).Select(m => m.SetId)); } - _eqdpManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip) + protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - _eqdpManipulations.AddOrReplace(manip); - var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= - new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar - return manip.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EqdpManipulation manip) + protected override void RevertModInternal(EqdpIdentifier identifier) { - if (!_eqdpManipulations.Remove(manip)) + if (GetFile(identifier) is { } file) + Apply(file, identifier, ExpandedEqdpFile.GetDefault(Manager, identifier)); + } + + public static bool Apply(ExpandedEqdpFile file, EqdpIdentifier identifier, EqdpEntry entry) + { + var origEntry = file[identifier.SetId]; + var mask = Eqdp.Mask(identifier.Slot); + if ((origEntry & mask) == entry) return false; - var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); - var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; - manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId); - return manip.Apply(file); + file[identifier.SetId] = (entry & ~mask) | origEntry; + return true; } - public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) - => _eqdpFiles - [Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar - - public void Dispose() + protected override void Dispose(bool _) { for (var i = 0; i < _eqdpFiles.Length; ++i) { @@ -92,6 +102,15 @@ public readonly struct EqdpCache : IDisposable _eqdpFiles[i] = null; } - _eqdpManipulations.Clear(); + Clear(); + } + + private ExpandedEqdpFile? GetFile(EqdpIdentifier identifier) + { + if (!Manager.CharacterUtility.Ready) + return null; + + var index = Array.IndexOf(CharacterUtilityData.EqdpIndices, identifier.FileIndex()); + return _eqdpFiles[index] ??= new ExpandedEqdpFile(Manager, identifier.GenderRace, identifier.Slot.IsAccessory()); } } diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 972ee5a5..599ae588 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,4 +1,4 @@ -using OtterGui.Filesystem; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -7,54 +7,77 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct EqpCache : IDisposable +public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedEqpFile? _eqpFile = null; - private readonly List _eqpManipulations = new(); + private ExpandedEqpFile? _eqpFile; - public EqpCache() - { } + public override void SetFiles() + => Manager.SetFile(_eqpFile, MetaIndex.Eqp); - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_eqpFile, MetaIndex.Eqp); + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); - public static void ResetFiles(MetaFileManager manager) - => manager.SetFile(null, MetaIndex.Eqp); + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); - public void Reset() + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); + + public override void Reset() { if (_eqpFile == null) return; - _eqpFile.Reset(_eqpManipulations.Select(m => m.SetId)); - _eqpManipulations.Clear(); + _eqpFile.Reset(Keys.Select(identifier => identifier.SetId)); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EqpManipulation manip) + protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) { - _eqpManipulations.AddOrReplace(manip); - _eqpFile ??= new ExpandedEqpFile(manager); - return manip.Apply(_eqpFile); + if (GetFile() is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EqpManipulation manip) + protected override void RevertModInternal(EqpIdentifier identifier) { - var idx = _eqpManipulations.FindIndex(manip.Equals); - if (idx < 0) + if (GetFile() is { } file) + Apply(file, identifier, ExpandedEqpFile.GetDefault(Manager, identifier.SetId)); + } + + public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) + { + var origEntry = file[identifier.SetId]; + var mask = Eqp.Mask(identifier.Slot); + if ((origEntry & mask) == entry) return false; - var def = ExpandedEqpFile.GetDefault(manager, manip.SetId); - manip = new EqpManipulation(def, manip.Slot, manip.SetId); - return manip.Apply(_eqpFile!); + file[identifier.SetId] = (origEntry & ~mask) | entry; + return true; } - public void Dispose() + protected override void Dispose(bool _) { _eqpFile?.Dispose(); _eqpFile = null; - _eqpManipulations.Clear(); + Clear(); + } + + private ExpandedEqpFile? GetFile() + { + if (_eqpFile != null) + return _eqpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _eqpFile = new ExpandedEqpFile(Manager); } } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 3a0b4695..412dd322 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,6 +1,3 @@ -using OtterGui.Filesystem; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -9,46 +6,41 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct EstCache : IDisposable +public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private EstFile? _estFaceFile = null; - private EstFile? _estHairFile = null; - private EstFile? _estBodyFile = null; - private EstFile? _estHeadFile = null; + private EstFile? _estFaceFile; + private EstFile? _estHairFile; + private EstFile? _estBodyFile; + private EstFile? _estHeadFile; - private readonly List _estManipulations = new(); - - public EstCache() - { } - - public void SetFiles(MetaFileManager manager) + public override void SetFiles() { - manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - manager.SetFile(_estHairFile, MetaIndex.HairEst); - manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - manager.SetFile(_estHeadFile, MetaIndex.HeadEst); + Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + Manager.SetFile(_estHairFile, MetaIndex.HairEst); + Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); } - public void SetFile(MetaFileManager manager, MetaIndex index) + public void SetFile(MetaIndex index) { switch (index) { case MetaIndex.FaceEst: - manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); break; case MetaIndex.HairEst: - manager.SetFile(_estHairFile, MetaIndex.HairEst); + Manager.SetFile(_estHairFile, MetaIndex.HairEst); break; case MetaIndex.BodyEst: - manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); break; case MetaIndex.HeadEst: - manager.SetFile(_estHeadFile, MetaIndex.HeadEst); + Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); break; } } - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type) + public MetaList.MetaReverter TemporarilySetFiles(EstType type) { var (file, idx) = type switch { @@ -56,74 +48,65 @@ public struct EstCache : IDisposable EstType.Hair => (_estHairFile, MetaIndex.HairEst), EstType.Body => (_estBodyFile, MetaIndex.BodyEst), EstType.Head => (_estHeadFile, MetaIndex.HeadEst), - _ => (null, 0), + _ => (null, 0), }; - return manager.TemporarilySetFile(file, idx); + return Manager.TemporarilySetFile(file, idx); } - private readonly EstFile? GetEstFile(EstType type) + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); + + protected override void IncorporateChangesInternal() { - return type switch - { - EstType.Face => _estFaceFile, - EstType.Hair => _estHairFile, - EstType.Body => _estBodyFile, - EstType.Head => _estHeadFile, - _ => null, - }; + if (!Manager.CharacterUtility.Ready) + return; + + foreach (var (identifier, (_, entry)) in this) + Apply(GetFile(identifier)!, identifier, entry); + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations."); } - internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId) + public EstEntry GetEstEntry(EstIdentifier identifier) { - var file = GetEstFile(type); + var file = GetFile(identifier); return file != null - ? file[genderRace, primaryId.Id] - : EstFile.GetDefault(manager, type, genderRace, primaryId); + ? file[identifier.GenderRace, identifier.SetId] + : EstFile.GetDefault(Manager, identifier); } - public void Reset() + public override void Reset() { _estFaceFile?.Reset(); _estHairFile?.Reset(); _estBodyFile?.Reset(); _estHeadFile?.Reset(); - _estManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EstManipulation m) + protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) { - _estManipulations.AddOrReplace(m); - var file = m.Slot switch - { - EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair), - EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face), - EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body), - EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head), - _ => throw new ArgumentOutOfRangeException(), - }; - return m.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EstManipulation m) + protected override void RevertModInternal(EstIdentifier identifier) { - if (!_estManipulations.Remove(m)) - return false; - - var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId); - var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); - var file = m.Slot switch - { - EstType.Hair => _estHairFile!, - EstType.Face => _estFaceFile!, - EstType.Body => _estBodyFile!, - EstType.Head => _estHeadFile!, - _ => throw new ArgumentOutOfRangeException(), - }; - return manip.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId)); } - public void Dispose() + public static bool Apply(EstFile file, EstIdentifier identifier, EstEntry entry) + => file.SetEntry(identifier.GenderRace, identifier.SetId, entry) switch + { + EstFile.EstEntryChange.Unchanged => false, + EstFile.EstEntryChange.Changed => true, + EstFile.EstEntryChange.Added => true, + EstFile.EstEntryChange.Removed => true, + _ => false, + }; + + protected override void Dispose(bool _) { _estFaceFile?.Dispose(); _estHairFile?.Dispose(); @@ -133,6 +116,21 @@ public struct EstCache : IDisposable _estHairFile = null; _estBodyFile = null; _estHeadFile = null; - _estManipulations.Clear(); + Clear(); + } + + private EstFile? GetFile(EstIdentifier identifier) + { + if (Manager.CharacterUtility.Ready) + return null; + + return identifier.Slot switch + { + EstType.Hair => _estHairFile ??= new EstFile(Manager, EstType.Hair), + EstType.Face => _estFaceFile ??= new EstFile(Manager, EstType.Face), + EstType.Body => _estBodyFile ??= new EstFile(Manager, EstType.Body), + EstType.Head => _estHeadFile ??= new EstFile(Manager, EstType.Head), + _ => null, + }; } } diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs similarity index 75% rename from Penumbra/Meta/Manipulations/GlobalEqpCache.cs rename to Penumbra/Collections/Cache/GlobalEqpCache.cs index 26eb1d05..1c80b47d 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -1,9 +1,11 @@ using OtterGui.Services; using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; -namespace Penumbra.Meta.Manipulations; +namespace Penumbra.Collections.Cache; -public struct GlobalEqpCache : IService +public class GlobalEqpCache : Dictionary, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _doNotHideNecklace = []; @@ -13,11 +15,9 @@ public struct GlobalEqpCache : IService private bool _doNotHideVieraHats; private bool _doNotHideHrothgarHats; - public GlobalEqpCache() - { } - - public void Clear() + public new void Clear() { + base.Clear(); _doNotHideEarrings.Clear(); _doNotHideNecklace.Clear(); _doNotHideBracelets.Clear(); @@ -29,6 +29,9 @@ public struct GlobalEqpCache : IService public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) { + if (Count == 0) + return original; + if (_doNotHideVieraHats) original |= EqpEntry.HeadShowVieraHat; @@ -52,8 +55,13 @@ public struct GlobalEqpCache : IService return original; } - public bool Add(GlobalEqpManipulation manipulation) - => manipulation.Type switch + public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation) + { + if (Remove(manipulation, out var oldMod) && oldMod == mod) + return false; + + this[manipulation] = mod; + _ = manipulation.Type switch { GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition), @@ -61,12 +69,18 @@ public struct GlobalEqpCache : IService GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), - GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), - _ => false, + GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + _ => false, }; + return true; + } - public bool Remove(GlobalEqpManipulation manipulation) - => manipulation.Type switch + public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod) + { + if (!Remove(manipulation, out mod)) + return false; + + _ = manipulation.Type switch { GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), @@ -74,7 +88,9 @@ public struct GlobalEqpCache : IService GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), - GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), - _ => false, + GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + _ => false, }; + return true; + } } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 0a713867..1475ffd5 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,4 +1,4 @@ -using OtterGui.Filesystem; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -7,50 +7,76 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct GmpCache : IDisposable +public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedGmpFile? _gmpFile = null; - private readonly List _gmpManipulations = new(); + private ExpandedGmpFile? _gmpFile; - public GmpCache() - { } + public override void SetFiles() + => Manager.SetFile(_gmpFile, MetaIndex.Gmp); - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_gmpFile, MetaIndex.Gmp); + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Gmp); - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; - public void Reset() + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed GMP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); + + public override void Reset() { if (_gmpFile == null) return; - _gmpFile.Reset(_gmpManipulations.Select(m => m.SetId)); - _gmpManipulations.Clear(); + _gmpFile.Reset(Keys.Select(identifier => identifier.SetId)); + Clear(); } - public bool ApplyMod(MetaFileManager manager, GmpManipulation manip) + protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) { - _gmpManipulations.AddOrReplace(manip); - _gmpFile ??= new ExpandedGmpFile(manager); - return manip.Apply(_gmpFile); + if (GetFile() is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, GmpManipulation manip) + protected override void RevertModInternal(GmpIdentifier identifier) { - if (!_gmpManipulations.Remove(manip)) + if (GetFile() is { } file) + Apply(file, identifier, ExpandedGmpFile.GetDefault(Manager, identifier.SetId)); + } + + public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) + { + var origEntry = file[identifier.SetId]; + if (entry == origEntry) return false; - var def = ExpandedGmpFile.GetDefault(manager, manip.SetId); - manip = new GmpManipulation(def, manip.SetId); - return manip.Apply(_gmpFile!); + file[identifier.SetId] = entry; + return true; } - public void Dispose() + protected override void Dispose(bool _) { _gmpFile?.Dispose(); _gmpFile = null; - _gmpManipulations.Clear(); + Clear(); + } + + private ExpandedGmpFile? GetFile() + { + if (_gmpFile != null) + return _gmpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _gmpFile = new ExpandedGmpFile(Manager); } } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs new file mode 100644 index 00000000..218c1840 --- /dev/null +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -0,0 +1,89 @@ +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.Collections.Cache; + +public interface IMetaCache : IDisposable +{ + public void SetFiles(); + public void Reset(); + public void ResetFiles(); + + public int Count { get; } +} + +public abstract class MetaCacheBase + : Dictionary, IMetaCache + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + public MetaCacheBase(MetaFileManager manager, ModCollection collection) + { + Manager = manager; + Collection = collection; + if (!Manager.CharacterUtility.Ready) + Manager.CharacterUtility.LoadingFinished += IncorporateChanges; + } + + protected readonly MetaFileManager Manager; + protected readonly ModCollection Collection; + + public void Dispose() + { + Manager.CharacterUtility.LoadingFinished -= IncorporateChanges; + Dispose(true); + } + + public abstract void SetFiles(); + public abstract void Reset(); + public abstract void ResetFiles(); + + public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) + { + lock (this) + { + if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) + return false; + + this[identifier] = (source, entry); + } + + ApplyModInternal(identifier, entry); + return true; + } + + public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + { + lock (this) + { + if (!Remove(identifier, out var pair)) + { + mod = null; + return false; + } + + mod = pair.Source; + } + + RevertModInternal(identifier); + return true; + } + + private void IncorporateChanges() + { + lock (this) + { + IncorporateChangesInternal(); + } + if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) + SetFiles(); + } + + protected abstract void ApplyModInternal(TIdentifier identifier, TEntry entry); + protected abstract void RevertModInternal(TIdentifier identifier); + protected abstract void IncorporateChangesInternal(); + + protected virtual void Dispose(bool _) + { } +} diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 7990122a..3d05e793 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,3 +1,6 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; @@ -6,116 +9,132 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public readonly struct ImcCache : IDisposable +public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary _imcFiles = []; - private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = []; + private readonly Dictionary)> _imcFiles = []; - public ImcCache() - { } + public override void SetFiles() + => SetFiles(false); - public void SetFiles(ModCollection collection, bool fromFullCompute) + public bool GetFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + { + if (!_imcFiles.TryGetValue(path, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; + } + + public void SetFiles(bool fromFullCompute) { if (fromFullCompute) - foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection)); + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection)); else - foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection)); + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); } - public void Reset(ModCollection collection) + public override void ResetFiles() { - foreach (var (path, file) in _imcFiles) + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFile(path, FullPath.Empty); + } + + protected override void IncorporateChangesInternal() + { + if (!Manager.CharacterUtility.Ready) + return; + + foreach (var (identifier, (_, entry)) in this) + ApplyFile(identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed IMC manipulations."); + } + + + public override void Reset() + { + foreach (var (path, (file, set)) in _imcFiles) { - collection._cache!.RemovePath(path); + Collection._cache!.RemovePath(path); file.Reset(); + set.Clear(); } - _imcManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) + protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { - if (!manip.Validate(true)) - return false; + if (Manager.CharacterUtility.Ready) + ApplyFile(identifier, entry); + } - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); - if (idx < 0) - { - idx = _imcManipulations.Count; - _imcManipulations.Add((manip, null!)); - } - - var path = manip.GamePath(); + private void ApplyFile(ImcIdentifier identifier, ImcEntry entry) + { + var path = identifier.GamePath(); try { - if (!_imcFiles.TryGetValue(path, out var file)) - file = new ImcFile(manager, manip.Identifier); + if (!_imcFiles.TryGetValue(path, out var pair)) + pair = (new ImcFile(Manager, identifier), []); - _imcManipulations[idx] = (manip, file); - if (!manip.Apply(file)) - return false; - _imcFiles[path] = file; - var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); - collection._cache!.ForceFile(path, fullPath); + if (!Apply(pair.Item1, identifier, entry)) + return; - return true; + pair.Item2.Add(identifier); + _imcFiles[path] = pair; + var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); + Collection._cache!.ForceFile(path, fullPath); } catch (ImcException e) { - manager.ValidityChecker.ImcExceptions.Add(e); + Manager.ValidityChecker.ImcExceptions.Add(e); Penumbra.Log.Error(e.ToString()); } catch (Exception e) { - Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}"); + Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}"); } - - return false; } - public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) + protected override void RevertModInternal(ImcIdentifier identifier) { - if (!m.Validate(false)) - return false; + var path = identifier.GamePath(); + if (!_imcFiles.TryGetValue(path, out var pair)) + return; - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); - if (idx < 0) - return false; + if (!pair.Item2.Remove(identifier)) + return; - var (_, file) = _imcManipulations[idx]; - _imcManipulations.RemoveAt(idx); - - if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file))) + if (pair.Item2.Count == 0) { - _imcFiles.Remove(file.Path); - collection._cache!.ForceFile(file.Path, FullPath.Empty); - file.Dispose(); - return true; + _imcFiles.Remove(path); + Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty); + pair.Item1.Dispose(); + return; } - var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _); - var manip = m.Copy(def); - if (!manip.Apply(file)) - return false; + var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _); + if (!Apply(pair.Item1, identifier, def)) + return; - var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); - collection._cache!.ForceFile(file.Path, fullPath); - - return true; + var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); + Collection._cache!.ForceFile(pair.Item1.Path, fullPath); } - public void Dispose() + public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry) + => file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry); + + protected override void Dispose(bool _) { - foreach (var file in _imcFiles.Values) + foreach (var (_, (file, _)) in _imcFiles) file.Dispose(); - + Clear(); _imcFiles.Clear(); - _imcManipulations.Clear(); } - - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) - => _imcFiles.TryGetValue(path, out file); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index fbca9c0e..bc6ef34d 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,3 +1,4 @@ +using System.IO.Pipes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -9,238 +10,174 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public class MetaCache : IDisposable, IEnumerable> +public class MetaCache(MetaFileManager manager, ModCollection collection) { - private readonly MetaFileManager _manager; - private readonly ModCollection _collection; - private readonly Dictionary _manipulations = new(); - private EqpCache _eqpCache = new(); - private readonly EqdpCache _eqdpCache = new(); - private EstCache _estCache = new(); - private GmpCache _gmpCache = new(); - private CmpCache _cmpCache = new(); - private readonly ImcCache _imcCache = new(); - private GlobalEqpCache _globalEqpCache = new(); - - public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) - { - lock (_manipulations) - { - return _manipulations.TryGetValue(manip, out mod); - } - } + public readonly EqpCache Eqp = new(manager, collection); + public readonly EqdpCache Eqdp = new(manager, collection); + public readonly EstCache Est = new(manager, collection); + public readonly GmpCache Gmp = new(manager, collection); + public readonly RspCache Rsp = new(manager, collection); + public readonly ImcCache Imc = new(manager, collection); + public readonly GlobalEqpCache GlobalEqp = new(); public int Count - => _manipulations.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; - public IReadOnlyCollection Manipulations - => _manipulations.Keys; - - public IEnumerator> GetEnumerator() - => _manipulations.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public MetaCache(MetaFileManager manager, ModCollection collection) - { - _manager = manager; - _collection = collection; - if (!_manager.CharacterUtility.Ready) - _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations; - } + public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources + => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) + .Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void SetFiles() { - _eqpCache.SetFiles(_manager); - _eqdpCache.SetFiles(_manager); - _estCache.SetFiles(_manager); - _gmpCache.SetFiles(_manager); - _cmpCache.SetFiles(_manager); - _imcCache.SetFiles(_collection, false); + Eqp.SetFiles(); + Eqdp.SetFiles(); + Est.SetFiles(); + Gmp.SetFiles(); + Rsp.SetFiles(); + Imc.SetFiles(false); } public void Reset() { - _eqpCache.Reset(); - _eqdpCache.Reset(); - _estCache.Reset(); - _gmpCache.Reset(); - _cmpCache.Reset(); - _imcCache.Reset(_collection); - _manipulations.Clear(); - _globalEqpCache.Clear(); + Eqp.Reset(); + Eqdp.Reset(); + Est.Reset(); + Gmp.Reset(); + Rsp.Reset(); + Imc.Reset(); + GlobalEqp.Clear(); } public void Dispose() { - _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - _eqpCache.Dispose(); - _eqdpCache.Dispose(); - _estCache.Dispose(); - _gmpCache.Dispose(); - _cmpCache.Dispose(); - _imcCache.Dispose(); - _manipulations.Clear(); + Eqp.Dispose(); + Eqdp.Dispose(); + Est.Dispose(); + Gmp.Dispose(); + Rsp.Dispose(); + Imc.Dispose(); } + public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + { + mod = null; + return identifier switch + { + EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod), + EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod), + EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod), + GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod), + ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), + RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), + GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), + _ => false, + }; + + static bool Convert((IMod, T) pair, out IMod mod) + { + mod = pair.Item1; + return true; + } + } + + public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + => identifier switch + { + EqdpIdentifier i => Eqdp.RevertMod(i, out mod), + EqpIdentifier i => Eqp.RevertMod(i, out mod), + EstIdentifier i => Est.RevertMod(i, out mod), + GmpIdentifier i => Gmp.RevertMod(i, out mod), + ImcIdentifier i => Imc.RevertMod(i, out mod), + RspIdentifier i => Rsp.RevertMod(i, out mod), + GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), + _ => (mod = null) != null, + }; + + public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry) + => identifier switch + { + EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e), + EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)), + EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e), + EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)), + EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e), + GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e), + ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), + RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), + GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), + _ => false, + }; + ~MetaCache() => Dispose(); - public bool ApplyMod(MetaManipulation manip, IMod mod) - { - lock (_manipulations) - { - if (_manipulations.ContainsKey(manip)) - _manipulations.Remove(manip); - - _manipulations[manip] = mod; - } - - if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp) - return _globalEqpCache.Add(manip.GlobalEqp); - - if (!_manager.CharacterUtility.Ready) - return true; - - // Imc manipulations do not require character utility, - // but they do require the file space to be ready. - return manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } - - public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) - { - lock (_manipulations) - { - var ret = _manipulations.Remove(manip, out mod); - - if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp) - return _globalEqpCache.Remove(manip.GlobalEqp); - - if (!_manager.CharacterUtility.Ready) - return ret; - } - - // Imc manipulations do not require character utility, - // but they do require the file space to be ready. - return manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } - /// Set a single file. public void SetFile(MetaIndex metaIndex) { switch (metaIndex) { case MetaIndex.Eqp: - _eqpCache.SetFiles(_manager); + Eqp.SetFiles(); break; case MetaIndex.Gmp: - _gmpCache.SetFiles(_manager); + Gmp.SetFiles(); break; case MetaIndex.HumanCmp: - _cmpCache.SetFiles(_manager); + Rsp.SetFiles(); break; case MetaIndex.FaceEst: case MetaIndex.HairEst: case MetaIndex.HeadEst: case MetaIndex.BodyEst: - _estCache.SetFile(_manager, metaIndex); + Est.SetFile(metaIndex); break; default: - _eqdpCache.SetFile(_manager, metaIndex); + Eqdp.SetFile(metaIndex); break; } } /// Set the currently relevant IMC files for the collection cache. public void SetImcFiles(bool fromFullCompute) - => _imcCache.SetFiles(_collection, fromFullCompute); + => Imc.SetFiles(fromFullCompute); public MetaList.MetaReverter TemporarilySetEqpFile() - => _eqpCache.TemporarilySetFiles(_manager); + => Eqp.TemporarilySetFile(); public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); + => Eqdp.TemporarilySetFile(genderRace, accessory); public MetaList.MetaReverter TemporarilySetGmpFile() - => _gmpCache.TemporarilySetFiles(_manager); + => Gmp.TemporarilySetFile(); public MetaList.MetaReverter TemporarilySetCmpFile() - => _cmpCache.TemporarilySetFiles(_manager); + => Rsp.TemporarilySetFile(); public MetaList.MetaReverter TemporarilySetEstFile(EstType type) - => _estCache.TemporarilySetFiles(_manager, type); + => Est.TemporarilySetFiles(type); public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => _globalEqpCache.Apply(baseEntry, armor); + => GlobalEqp.Apply(baseEntry, armor); /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => _imcCache.GetImcFile(path, out file); + => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) { - var eqdpFile = _eqdpCache.EqdpFile(race, accessory); + var eqdpFile = Eqdp.EqdpFile(race, accessory); if (eqdpFile != null) return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; - return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId); + return Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); } internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) - => _estCache.GetEstEntry(_manager, type, genderRace, primaryId); - - /// Use this when CharacterUtility becomes ready. - private void ApplyStoredManipulations() - { - if (!_manager.CharacterUtility.Ready) - return; - - var loaded = 0; - lock (_manipulations) - { - foreach (var manip in Manipulations) - { - loaded += manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.GlobalEqp => false, - MetaManipulation.Type.Unknown => false, - _ => false, - } - ? 1 - : 0; - } - } - - _manager.ApplyDefaultFiles(_collection); - _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations."); - } + => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); } diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs new file mode 100644 index 00000000..3889d6f1 --- /dev/null +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -0,0 +1,81 @@ +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + private CmpFile? _cmpFile; + + public override void SetFiles() + => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); + + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.HumanCmp); + + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; + + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed RSP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); + + public override void Reset() + { + if (_cmpFile == null) + return; + + _cmpFile.Reset(Keys.Select(identifier => (identifier.SubRace, identifier.Attribute))); + Clear(); + } + + protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) + { + if (GetFile() is { } file) + Apply(file, identifier, entry); + } + + protected override void RevertModInternal(RspIdentifier identifier) + { + if (GetFile() is { } file) + Apply(file, identifier, CmpFile.GetDefault(Manager, identifier.SubRace, identifier.Attribute)); + } + + public static bool Apply(CmpFile file, RspIdentifier identifier, RspEntry entry) + { + var value = file[identifier.SubRace, identifier.Attribute]; + if (value == entry) + return false; + + file[identifier.SubRace, identifier.Attribute] = entry; + return true; + } + + protected override void Dispose(bool _) + { + _cmpFile?.Dispose(); + _cmpFile = null; + Clear(); + } + + private CmpFile? GetFile() + { + if (_cmpFile != null) + return _cmpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _cmpFile = new CmpFile(Manager); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index fdd28ef1..9fa77784 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, + string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -52,7 +53,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. /// Modified extra skeleton template parameters. - public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) + public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair[] estManipulations) { var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) @@ -81,20 +82,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair[] estManipulations) { // Try to find an EST entry from the manipulations provided. - var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations - .FirstOrNull(est => - est.Gender == gender - && est.Race == race - && est.Slot == type - && est.SetId == info.PrimaryId + .FirstOrNull( + est => est.Key.GenderRace == info.GenderRace + && est.Key.Slot == type + && est.Key.SetId == info.PrimaryId ); // Try to use an entry from provided manipulations, falling back to the current collection. - var targetId = modEst?.Entry + var targetId = modEst?.Value ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? EstEntry.Zero; @@ -102,7 +101,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)]; + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. @@ -250,9 +249,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var path = manager.ResolveMtrlPath(relativePath, notifier); if (path == null) return null; + var bytes = read(path); if (bytes == null) return null; + var mtrl = new MtrlFile(bytes); return new MaterialExporter.Material diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f6157747..1f970dfe 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -13,15 +13,15 @@ public partial class TexToolsMeta private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data) { // Eqp can only be valid for equipment. - if (data == null || !metaFileInfo.EquipSlot.IsEquipment()) + var mask = Eqp.Mask(metaFileInfo.EquipSlot); + if (data == null || mask == 0) return; - var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data); - var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot, - metaFileInfo.PrimaryId); - var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId); - if (_keepDefault || def.Entry != manip.Entry) - MetaManipulations.Add(manip); + var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot); + var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask; + var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask; + if (_keepDefault || def != value) + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Eqdp Entries and add them to the list if they are non-default. @@ -40,14 +40,12 @@ public partial class TexToolsMeta if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory()) continue; - var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2); - var def = new EqdpManipulation( - ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId), - metaFileInfo.EquipSlot, - gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); - var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); - if (_keepDefault || def.Entry != manip.Entry) - MetaManipulations.Add(manip); + var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr); + var mask = Eqdp.Mask(metaFileInfo.EquipSlot); + var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; + var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask; + if (_keepDefault || def != value) + MetaManipulations.TryAdd(identifier, value); } } @@ -57,10 +55,10 @@ public partial class TexToolsMeta if (data == null) return; - var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); - var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); + var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); if (_keepDefault || value != def) - MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId)); + MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value); } // Deserialize and check Est Entries and add them to the list if they are non-default. @@ -74,7 +72,7 @@ public partial class TexToolsMeta for (var i = 0; i < num; ++i) { var gr = (GenderRace)reader.ReadUInt16(); - var id = reader.ReadUInt16(); + var id = (PrimaryId)reader.ReadUInt16(); var value = new EstEntry(reader.ReadUInt16()); var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch { @@ -87,9 +85,10 @@ public partial class TexToolsMeta if (!gr.IsValid() || type == 0) continue; - var def = EstFile.GetDefault(_metaFileManager, type, gr, id); + var identifier = new EstIdentifier(id, type, gr); + var def = EstFile.GetDefault(_metaFileManager, type, gr, id); if (_keepDefault || def != value) - MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value)); + MetaManipulations.TryAdd(identifier, value); } } @@ -107,20 +106,16 @@ public partial class TexToolsMeta ushort i = 0; try { - var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, - metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, - new ImcEntry()); - var def = new ImcFile(_metaFileManager, manip.Identifier); - var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. + var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId, + metaFileInfo.EquipSlot, metaFileInfo.SecondaryType); + var file = new ImcFile(_metaFileManager, identifier); + var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { - if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant)i))) - { - var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, - value); - if (imc.Validate(true)) - MetaManipulations.Add(imc); - } + identifier = identifier with { Variant = (Variant)i }; + var def = file.GetEntry(partIdx, (Variant)i); + if (_keepDefault || def != value && identifier.Validate()) + MetaManipulations.TryAdd(identifier, value); ++i; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 4fb56df6..9cce60e3 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -1,3 +1,4 @@ +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -8,7 +9,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { - public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable manipulations, DirectoryInfo basePath) + public static void WriteTexToolsMeta(MetaFileManager manager, MetaDictionary manipulations, DirectoryInfo basePath) { var files = ConvertToTexTools(manager, manipulations); @@ -27,49 +28,81 @@ public partial class TexToolsMeta } } - public static Dictionary ConvertToTexTools(MetaFileManager manager, IEnumerable manips) + public static Dictionary ConvertToTexTools(MetaFileManager manager, MetaDictionary manips) { var ret = new Dictionary(); - foreach (var group in manips.GroupBy(ManipToPath)) + foreach (var group in manips.Rsp.GroupBy(ManipToPath)) { if (group.Key.Length == 0) continue; - var bytes = group.Key.EndsWith(".rgsp") - ? WriteRgspFile(manager, group.Key, group) - : WriteMetaFile(manager, group.Key, group); + var bytes = WriteRgspFile(manager, group); if (bytes.Length == 0) continue; ret.Add(group.Key, bytes); } + foreach (var (file, dict) in SplitByFile(manips)) + { + var bytes = WriteMetaFile(manager, file, dict); + if (bytes.Length == 0) + continue; + + ret.Add(file, bytes); + } + return ret; } - private static byte[] WriteRgspFile(MetaFileManager manager, string path, IEnumerable manips) + private static Dictionary SplitByFile(MetaDictionary manips) { - var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp); + var ret = new Dictionary(); + foreach (var (identifier, key) in manips.Imc) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Eqp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Eqdp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Est) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Gmp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + ret.Remove(string.Empty); + + return ret; + + MetaDictionary GetDict(string path) + { + if (!ret.TryGetValue(path, out var dict)) + { + dict = new MetaDictionary(); + ret.Add(path, dict); + } + + return dict; + } + } + + private static byte[] WriteRgspFile(MetaFileManager manager, IEnumerable> manips) + { + var list = manips.GroupBy(m => m.Key.Attribute).ToDictionary(g => g.Key, g => g.Last()); using var m = new MemoryStream(45); using var b = new BinaryWriter(m); // Version b.Write(byte.MaxValue); b.Write((ushort)2); - var race = list.First().Value.SubRace; - var gender = list.First().Value.Attribute.ToGender(); + var race = list.First().Value.Key.SubRace; + var gender = list.First().Value.Key.Attribute.ToGender(); b.Write((byte)(race - 1)); // offset by one due to Unknown b.Write((byte)(gender - 1)); // offset by one due to Unknown - void Add(params RspAttribute[] attributes) - { - foreach (var attribute in attributes) - { - var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); - b.Write(value.Value); - } - } - if (gender == Gender.Male) { Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail); @@ -82,12 +115,24 @@ public partial class TexToolsMeta } return m.GetBuffer(); + + void Add(params RspAttribute[] attributes) + { + foreach (var attribute in attributes) + { + var value = list.TryGetValue(attribute, out var tmp) ? tmp.Value : CmpFile.GetDefault(manager, race, attribute); + b.Write(value.Value); + } + } } - private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnumerable manips) + private static byte[] WriteMetaFile(MetaFileManager manager, string path, MetaDictionary manips) { - var filteredManips = manips.GroupBy(m => m.ManipulationType).ToDictionary(p => p.Key, p => p.Select(x => x)); - + var headerCount = (manips.Imc.Count > 0 ? 1 : 0) + + (manips.Eqp.Count > 0 ? 1 : 0) + + (manips.Eqdp.Count > 0 ? 1 : 0) + + (manips.Est.Count > 0 ? 1 : 0) + + (manips.Gmp.Count > 0 ? 1 : 0); using var m = new MemoryStream(); using var b = new BinaryWriter(m); @@ -101,7 +146,7 @@ public partial class TexToolsMeta b.Write((byte)0); // Number of Headers - b.Write((uint)filteredManips.Count); + b.Write((uint)headerCount); // Current TT Size of Headers b.Write((uint)12); @@ -109,88 +154,44 @@ public partial class TexToolsMeta var headerStart = b.BaseStream.Position + 4; b.Write((uint)headerStart); - var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count); - foreach (var (header, data) in filteredManips) - { - b.Write((uint)header); - b.Write(offset); - - var size = WriteData(manager, b, offset, header, data); - b.Write(size); - offset += size; - } + var offset = (uint)(b.BaseStream.Position + 12 * manips.Count); + offset += WriteData(manager, b, offset, manips.Imc); + offset += WriteData(b, offset, manips.Eqdp); + offset += WriteData(b, offset, manips.Eqp); + offset += WriteData(b, offset, manips.Est); + offset += WriteData(b, offset, manips.Gmp); return m.ToArray(); } - private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, - IEnumerable manips) + private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, IReadOnlyDictionary manips) { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + var oldPos = b.BaseStream.Position; b.Seek((int)offset, SeekOrigin.Begin); - switch (type) + var refIdentifier = manips.First().Key; + var baseFile = new ImcFile(manager, refIdentifier); + foreach (var (identifier, entry) in manips) + ImcCache.Apply(baseFile, identifier, entry); + + var partIdx = refIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? ImcFile.PartIndex(refIdentifier.EquipSlot) + : 0; + + for (var i = 0; i <= baseFile.Count; ++i) { - case MetaManipulation.Type.Imc: - var allManips = manips.ToList(); - var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); - foreach (var manip in allManips) - manip.Imc.Apply(baseFile); - - var partIdx = allManips[0].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? ImcFile.PartIndex(allManips[0].Imc.EquipSlot) - : 0; - - for (var i = 0; i <= baseFile.Count; ++i) - { - var entry = baseFile.GetEntry(partIdx, (Variant)i); - b.Write(entry.MaterialId); - b.Write(entry.DecalId); - b.Write(entry.AttributeAndSound); - b.Write(entry.VfxId); - b.Write(entry.MaterialAnimationId); - } - - break; - case MetaManipulation.Type.Eqdp: - foreach (var manip in manips) - { - b.Write((uint)Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race)); - var entry = (byte)(((uint)manip.Eqdp.Entry >> Eqdp.Offset(manip.Eqdp.Slot)) & 0x03); - b.Write(entry); - } - - break; - case MetaManipulation.Type.Eqp: - foreach (var manip in manips) - { - var bytes = BitConverter.GetBytes((ulong)manip.Eqp.Entry); - var (numBytes, byteOffset) = Eqp.BytesAndOffset(manip.Eqp.Slot); - for (var i = byteOffset; i < numBytes + byteOffset; ++i) - b.Write(bytes[i]); - } - - break; - case MetaManipulation.Type.Est: - foreach (var manip in manips) - { - b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); - b.Write(manip.Est.SetId.Id); - b.Write(manip.Est.Entry.Value); - } - - break; - case MetaManipulation.Type.Gmp: - foreach (var manip in manips) - { - b.Write((uint)manip.Gmp.Entry.Value); - b.Write(manip.Gmp.Entry.UnknownTotal); - } - - break; - case MetaManipulation.Type.GlobalEqp: - // Not Supported - break; + var entry = baseFile.GetEntry(partIdx, (Variant)i); + b.Write(entry.MaterialId); + b.Write(entry.DecalId); + b.Write(entry.AttributeAndSound); + b.Write(entry.VfxId); + b.Write(entry.MaterialAnimationId); } var size = b.BaseStream.Position - offset; @@ -198,19 +199,98 @@ public partial class TexToolsMeta return (uint)size; } - private static string ManipToPath(MetaManipulation manip) - => manip.ManipulationType switch - { - MetaManipulation.Type.Imc => ManipToPath(manip.Imc), - MetaManipulation.Type.Eqdp => ManipToPath(manip.Eqdp), - MetaManipulation.Type.Eqp => ManipToPath(manip.Eqp), - MetaManipulation.Type.Est => ManipToPath(manip.Est), - MetaManipulation.Type.Gmp => ManipToPath(manip.Gmp), - MetaManipulation.Type.Rsp => ManipToPath(manip.Rsp), - _ => string.Empty, - }; + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; - private static string ManipToPath(ImcManipulation manip) + b.Write((uint)MetaManipulationType.Eqdp); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + b.Write((uint)identifier.GenderRace); + b.Write(entry.AsByte); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, + IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + var numBytes = Eqp.BytesAndOffset(identifier.Slot).Item1; + for (var i = 0; i < numBytes; ++i) + b.Write((byte)(entry.Value >> (8 * i))); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + b.Write((ushort)identifier.GenderRace); + b.Write(identifier.SetId.Id); + b.Write(entry.Value); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var entry in manips.Values) + { + b.Write((uint)entry.Value); + b.Write(entry.UnknownTotal); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static string ManipToPath(ImcIdentifier manip) { var path = manip.GamePath().ToString(); var replacement = manip.ObjectType switch @@ -224,33 +304,33 @@ public partial class TexToolsMeta return path.Replace(".imc", replacement); } - private static string ManipToPath(EqdpManipulation manip) + private static string ManipToPath(EqdpIdentifier manip) => manip.Slot.IsAccessory() - ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" - : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + ? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath(EqpManipulation manip) + private static string ManipToPath(EqpIdentifier manip) => manip.Slot.IsAccessory() - ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" - : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + ? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath(EstManipulation manip) + private static string ManipToPath(EstIdentifier manip) { var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); return manip.Slot switch { - EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", - EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", - EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", - EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", - _ => throw new ArgumentOutOfRangeException(), + EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId.Id:D4}/c{raceCode}h{manip.SetId.Id:D4}_hir.meta", + EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId.Id:D4}/c{raceCode}f{manip.SetId.Id:D4}_fac.meta", + EstType.Body => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstType.Head => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta", + _ => throw new ArgumentOutOfRangeException(), }; } - private static string ManipToPath(GmpManipulation manip) - => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; + private static string ManipToPath(GmpIdentifier manip) + => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta"; - private static string ManipToPath(RspManipulation manip) - => $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp"; + private static string ManipToPath(KeyValuePair manip) + => $"chara/xls/charamake/rgsp/{(int)manip.Key.SubRace - 1}-{(int)manip.Key.Attribute.ToGender() - 1}.rgsp"; } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 71b9165f..7b0bb5a8 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -42,14 +42,6 @@ public partial class TexToolsMeta return Invalid; } - // Add the given values to the manipulations if they are not default. - void Add(RspAttribute attribute, float value) - { - var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def.Value) - ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value))); - } - if (gender == 1) { Add(RspAttribute.FemaleMinSize, br.ReadSingle()); @@ -73,5 +65,14 @@ public partial class TexToolsMeta } return ret; + + // Add the given values to the manipulations if they are not default. + void Add(RspAttribute attribute, float value) + { + var identifier = new RspIdentifier(subRace, attribute); + var def = CmpFile.GetDefault(manager, subRace, attribute); + if (keepDefault || value != def.Value) + ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); + } } } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 25e00bd7..c4a8e81f 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,4 +1,3 @@ -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Import.Structs; using Penumbra.Meta; @@ -22,10 +21,10 @@ public partial class TexToolsMeta public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. - public readonly uint Version; - public readonly string FilePath; - public readonly List MetaManipulations = new(); - private readonly bool _keepDefault = false; + public readonly uint Version; + public readonly string FilePath; + public readonly MetaDictionary MetaManipulations = new(); + private readonly bool _keepDefault; private readonly MetaFileManager _metaFileManager; @@ -44,18 +43,18 @@ public partial class TexToolsMeta var headerStart = reader.ReadUInt32(); reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); - List<(MetaManipulation.Type type, uint offset, int size)> entries = []; + List<(MetaManipulationType type, uint offset, int size)> entries = []; for (var i = 0; i < numHeaders; ++i) { var currentOffset = reader.BaseStream.Position; - var type = (MetaManipulation.Type)reader.ReadUInt32(); + var type = (MetaManipulationType)reader.ReadUInt32(); var offset = reader.ReadUInt32(); var size = reader.ReadInt32(); entries.Add((type, offset, size)); reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin); } - byte[]? ReadEntry(MetaManipulation.Type type) + byte[]? ReadEntry(MetaManipulationType type) { var idx = entries.FindIndex(t => t.type == type); if (idx < 0) @@ -65,11 +64,11 @@ public partial class TexToolsMeta return reader.ReadBytes(entries[idx].size); } - DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp)); - DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp)); - DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp)); - DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est)); - DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc)); + DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulationType.Eqp)); + DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulationType.Gmp)); + DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulationType.Eqdp)); + DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulationType.Est)); + DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulationType.Imc)); } catch (Exception e) { diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index b42571ac..bc474952 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,7 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using Microsoft.VisualBasic; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 2b87e688..4dfefd96 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -273,7 +273,7 @@ internal partial record ResolveContext { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; - return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId); + return (raceCode, type.ToName(), skeletonSet.AsId); } private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 650919a3..751113a0 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -51,10 +51,6 @@ public class ImcChecker return entry; } - public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) - => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, - imcManip.EquipSlot, imcManip.BodySlot), storeCache); - private static ImcFile? GetFile(ImcIdentifier identifier) { if (_dataManager == null) diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 6306f419..3f856bd2 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -69,10 +69,16 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Eqdp; } public readonly record struct EqdpEntryInternal(bool Material, bool Model) { + public byte AsByte + => (byte)(Material ? Model ? 3 : 1 : Model ? 2 : 0); + private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) { } @@ -83,4 +89,7 @@ public readonly record struct EqdpEntryInternal(bool Material, bool Model) public EqdpEntry ToEntry(EquipSlot slot) => Eqdp.FromSlotAndBits(slot, Material, Model); + + public override string ToString() + => $"Material: {Material}, Model: {Model}"; } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs deleted file mode 100644 index 8c5f27e5..00000000 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public EqdpIdentifier Identifier { get; } = identifier; - - public EqdpEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender - => Identifier.Gender; - - [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race - => Identifier.Race; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot - => Identifier.Slot; - - [JsonConstructor] - public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) - : this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry) - { } - - public EqdpManipulation Copy(EqdpManipulation entry) - { - if (entry.Slot != Slot) - { - var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); - return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2)); - } - - return new EqdpManipulation(Identifier, entry.Entry); - } - - public EqdpManipulation Copy(EqdpEntry entry) - => new(entry, Slot, Gender, Race, SetId); - - public override string ToString() - => $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}"; - - public bool Equals(EqdpManipulation other) - => Gender == other.Gender - && Race == other.Race - && SetId == other.SetId - && Slot == other.Slot; - - public override bool Equals(object? obj) - => obj is EqdpManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - - public int CompareTo(EqdpManipulation other) - { - var r = Race.CompareTo(other.Race); - if (r != 0) - return r; - - var g = Gender.CompareTo(other.Gender); - if (g != 0) - return g; - - var set = SetId.Id.CompareTo(other.SetId.Id); - return set != 0 ? set : Slot.CompareTo(other.Slot); - } - - public MetaIndex FileIndex() - => CharacterUtilityData.EqdpIdx(Names.CombinedRace(Gender, Race), Slot.IsAccessory()); - - public bool Apply(ExpandedEqdpFile file) - { - var entry = file[SetId]; - var mask = Eqdp.Mask(Slot); - if ((entry & mask) == Entry) - return false; - - file[SetId] = (entry & ~mask) | Entry; - return true; - } - - public bool Validate() - { - var mask = Eqdp.Mask(Slot); - if (mask == 0) - return false; - - if ((mask & Entry) != Entry) - return false; - - if (FileIndex() == (MetaIndex)(-1)) - return false; - - // No check for set id. - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 572dc203..ec4dd6e7 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -50,6 +50,9 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Eqp; } public readonly record struct EqpEntryInternal(uint Value) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs deleted file mode 100644 index eef21d12..00000000 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Util; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public EqpIdentifier Identifier { get; } = identifier; - - [JsonConverter(typeof(ForceNumericFlagEnumConverter))] - public EqpEntry Entry { get; } = entry; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot - => Identifier.Slot; - - [JsonConstructor] - public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) - : this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry) - { } - - public EqpManipulation Copy(EqpEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Eqp - {SetId} - {Slot}"; - - public bool Equals(EqpManipulation other) - => Slot == other.Slot - && SetId == other.SetId; - - public override bool Equals(object? obj) - => obj is EqpManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Slot, SetId); - - public int CompareTo(EqpManipulation other) - { - var set = SetId.Id.CompareTo(other.SetId.Id); - return set != 0 ? set : Slot.CompareTo(other.Slot); - } - - public MetaIndex FileIndex() - => MetaIndex.Eqp; - - public bool Apply(ExpandedEqpFile file) - { - var entry = file[SetId]; - var mask = Eqp.Mask(Slot); - if ((entry & mask) == Entry) - return false; - - file[SetId] = (entry & ~mask) | Entry; - return true; - } - - public bool Validate() - { - var mask = Eqp.Mask(Slot); - if (mask == 0) - return false; - if ((Entry & mask) != Entry) - return false; - - // No check for set id. - - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 9f878f97..2955dba4 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -91,6 +91,9 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Est; } [JsonConverter(typeof(Converter))] @@ -111,3 +114,16 @@ public readonly record struct EstEntry(ushort Value) => new(serializer.Deserialize(reader)); } } + +public static class EstTypeExtension +{ + public static string ToName(this EstType type) + => type switch + { + EstType.Hair => "hair", + EstType.Face => "face", + EstType.Body => "top", + EstType.Head => "met", + _ => "unk", + }; +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs deleted file mode 100644 index 09abbaa5..00000000 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation -{ - public static string ToName(EstType type) - => type switch - { - EstType.Hair => "hair", - EstType.Face => "face", - EstType.Body => "top", - EstType.Head => "met", - _ => "unk", - }; - - [JsonIgnore] - public EstIdentifier Identifier { get; } = identifier; - public EstEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender - => Identifier.Gender; - - [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race - => Identifier.Race; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EstType Slot - => Identifier.Slot; - - - [JsonConstructor] - public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) - : this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry) - { } - - public EstManipulation Copy(EstEntry entry) - => new(Identifier, entry); - - - public override string ToString() - => $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}"; - - public bool Equals(EstManipulation other) - => Gender == other.Gender - && Race == other.Race - && SetId == other.SetId - && Slot == other.Slot; - - public override bool Equals(object? obj) - => obj is EstManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - - public int CompareTo(EstManipulation other) - { - var r = Race.CompareTo(other.Race); - if (r != 0) - return r; - - var g = Gender.CompareTo(other.Gender); - if (g != 0) - return g; - - var s = Slot.CompareTo(other.Slot); - return s != 0 ? s : SetId.Id.CompareTo(other.SetId.Id); - } - - public MetaIndex FileIndex() - => (MetaIndex)Slot; - - public bool Apply(EstFile file) - { - return file.SetEntry(Names.CombinedRace(Gender, Race), SetId.Id, Entry) switch - { - EstFile.EstEntryChange.Unchanged => false, - EstFile.EstEntryChange.Changed => true, - EstFile.EstEntryChange.Added => true, - EstFile.EstEntryChange.Removed => true, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public bool Validate() - { - if (!Enum.IsDefined(Slot)) - return false; - if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown) - return false; - - // No known check for set id or entry. - return true; - } -} - - diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 94c892e2..2b88d962 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -1,11 +1,12 @@ using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly struct GlobalEqpManipulation : IMetaManipulation, IMetaIdentifier +public readonly struct GlobalEqpManipulation : IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -70,8 +71,29 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - { } + { + var path = Type switch + { + GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), + GlobalEqpType.DoNotHideHrothgarHats => string.Empty, + GlobalEqpType.DoNotHideVieraHats => string.Empty, + _ => string.Empty, + }; + if (path.Length > 0) + identifier.Identify(changedItems, path); + else if (Type is GlobalEqpType.DoNotHideVieraHats) + changedItems["All Hats for Viera"] = null; + else if (Type is GlobalEqpType.DoNotHideHrothgarHats) + changedItems["All Hats for Hrothgar"] = null; + } public MetaIndex FileIndex() => MetaIndex.Eqp; + + MetaManipulationType IMetaIdentifier.Type + => MetaManipulationType.GlobalEqp; } diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 1b7c70ba..a6fcf58b 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -36,4 +36,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, jObj["SetId"] = SetId.Id.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Gmp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs deleted file mode 100644 index 431f6325..00000000 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public GmpIdentifier Identifier { get; } = identifier; - - public GmpEntry Entry { get; } = entry; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConstructor] - public GmpManipulation(GmpEntry entry, PrimaryId setId) - : this(new GmpIdentifier(setId), entry) - { } - - public GmpManipulation Copy(GmpEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Gmp - {SetId}"; - - public bool Equals(GmpManipulation other) - => SetId == other.SetId; - - public override bool Equals(object? obj) - => obj is GmpManipulation other && Equals(other); - - public override int GetHashCode() - => SetId.GetHashCode(); - - public int CompareTo(GmpManipulation other) - => SetId.Id.CompareTo(other.SetId.Id); - - public MetaIndex FileIndex() - => MetaIndex.Gmp; - - public bool Apply(ExpandedGmpFile file) - { - var entry = file[SetId]; - if (entry == Entry) - return false; - - file[SetId] = Entry; - return true; - } - - public bool Validate() - // No known conditions. - => true; -} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 4ad6bd3d..5707ffca 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -4,6 +4,18 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; +public enum MetaManipulationType : byte +{ + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, + GlobalEqp = 7, +} + public interface IMetaIdentifier { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); @@ -13,4 +25,8 @@ public interface IMetaIdentifier public bool Validate(); public JObject AddToJson(JObject jObj); + + public MetaManipulationType Type { get; } + + public string ToString(); } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 2a2f4c03..44c60942 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,9 +27,6 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public ImcManipulation ToManipulation(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); @@ -193,4 +190,7 @@ public readonly record struct ImcIdentifier( jObj["BodySlot"] = BodySlot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Imc; } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs deleted file mode 100644 index 5065a06e..00000000 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.String.Classes; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct ImcManipulation : IMetaManipulation -{ - [JsonIgnore] - public ImcIdentifier Identifier { get; private init; } - - public ImcEntry Entry { get; private init; } - - - public PrimaryId PrimaryId - => Identifier.PrimaryId; - - public SecondaryId SecondaryId - => Identifier.SecondaryId; - - public Variant Variant - => Identifier.Variant; - - [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType - => Identifier.ObjectType; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot - => Identifier.EquipSlot; - - [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot - => Identifier.BodySlot; - - public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) - : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) - { } - - public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) - { - Identifier = identifier; - Entry = entry; - } - - - // Variants were initially ushorts but got shortened to bytes. - // There are still some manipulations around that have values > 255 for variant, - // so we change the unused value to something nonsensical in that case, just so they do not compare equal, - // and clamp the variant to 255. - [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, - EquipSlot equipSlot, ImcEntry entry) - { - Entry = entry; - var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - Identifier = objectType switch - { - ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, - variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown), - }; - } - - public ImcManipulation Copy(ImcEntry entry) - => new(Identifier, entry); - - public override string ToString() - => Identifier.ToString(); - - public bool Equals(ImcManipulation other) - => Identifier == other.Identifier; - - public override bool Equals(object? obj) - => obj is ImcManipulation other && Equals(other); - - public override int GetHashCode() - => Identifier.GetHashCode(); - - public int CompareTo(ImcManipulation other) - => Identifier.CompareTo(other.Identifier); - - public MetaIndex FileIndex() - => Identifier.FileIndex(); - - public Utf8GamePath GamePath() - => Identifier.GamePath(); - - public bool Apply(ImcFile file) - => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); - - public bool Validate(bool withMaterial) - { - if (!Identifier.Validate()) - return false; - - if (withMaterial && Entry.MaterialId == 0) - return false; - - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 236157ae..5a51df83 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -7,7 +8,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public class MetaDictionary : IEnumerable +public class MetaDictionary { private readonly Dictionary _imc = []; private readonly Dictionary _eqp = []; @@ -20,32 +21,50 @@ public class MetaDictionary : IEnumerable public IReadOnlyDictionary Imc => _imc; + public IReadOnlyDictionary Eqp + => _eqp; + + public IReadOnlyDictionary Eqdp + => _eqdp; + + public IReadOnlyDictionary Est + => _est; + + public IReadOnlyDictionary Gmp + => _gmp; + + public IReadOnlyDictionary Rsp + => _rsp; + + public IReadOnlySet GlobalEqp + => _globalEqp; + public int Count { get; private set; } - public int GetCount(MetaManipulation.Type type) + public int GetCount(MetaManipulationType type) => type switch { - MetaManipulation.Type.Imc => _imc.Count, - MetaManipulation.Type.Eqdp => _eqdp.Count, - MetaManipulation.Type.Eqp => _eqp.Count, - MetaManipulation.Type.Est => _est.Count, - MetaManipulation.Type.Gmp => _gmp.Count, - MetaManipulation.Type.Rsp => _rsp.Count, - MetaManipulation.Type.GlobalEqp => _globalEqp.Count, - _ => 0, + MetaManipulationType.Imc => _imc.Count, + MetaManipulationType.Eqdp => _eqdp.Count, + MetaManipulationType.Eqp => _eqp.Count, + MetaManipulationType.Est => _est.Count, + MetaManipulationType.Gmp => _gmp.Count, + MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.GlobalEqp => _globalEqp.Count, + _ => 0, }; - public bool CanAdd(IMetaIdentifier identifier) + public bool Contains(IMetaIdentifier identifier) => identifier switch { - EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier), - EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier), - EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier), - GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation), - GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier), - ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier), - RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier), - _ => false, + EqdpIdentifier i => _eqdp.ContainsKey(i), + EqpIdentifier i => _eqp.ContainsKey(i), + EstIdentifier i => _est.ContainsKey(i), + GlobalEqpManipulation i => _globalEqp.Contains(i), + GmpIdentifier i => _gmp.ContainsKey(i), + ImcIdentifier i => _imc.ContainsKey(i), + RspIdentifier i => _rsp.ContainsKey(i), + _ => false, }; public void Clear() @@ -69,17 +88,16 @@ public class MetaDictionary : IEnumerable && _gmp.SetEquals(other._gmp) && _globalEqp.SetEquals(other._globalEqp); - public IEnumerator GetEnumerator() - => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) - .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) - .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) - .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) - .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) - .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) - .Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator(); + public IEnumerable Identifiers + => _imc.Keys.Cast() + .Concat(_eqdp.Keys.Cast()) + .Concat(_eqp.Keys.Cast()) + .Concat(_est.Keys.Cast()) + .Concat(_gmp.Keys.Cast()) + .Concat(_rsp.Keys.Cast()) + .Concat(_globalEqp.Cast()); - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + #region TryAdd public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { @@ -90,7 +108,6 @@ public class MetaDictionary : IEnumerable return true; } - public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) { if (!_eqp.TryAdd(identifier, entry)) @@ -103,7 +120,6 @@ public class MetaDictionary : IEnumerable public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); - public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) { if (!_eqdp.TryAdd(identifier, entry)) @@ -152,6 +168,10 @@ public class MetaDictionary : IEnumerable return true; } + #endregion + + #region Update + public bool Update(ImcIdentifier identifier, ImcEntry entry) { if (!_imc.ContainsKey(identifier)) @@ -161,7 +181,6 @@ public class MetaDictionary : IEnumerable return true; } - public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) { if (!_eqp.ContainsKey(identifier)) @@ -174,7 +193,6 @@ public class MetaDictionary : IEnumerable public bool Update(EqpIdentifier identifier, EqpEntry entry) => Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); - public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) { if (!_eqdp.ContainsKey(identifier)) @@ -214,6 +232,50 @@ public class MetaDictionary : IEnumerable return true; } + #endregion + + #region TryGetValue + + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) + => _est.TryGetValue(identifier, out value); + + public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) + => _eqp.TryGetValue(identifier, out value); + + public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) + => _eqdp.TryGetValue(identifier, out value); + + public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) + => _gmp.TryGetValue(identifier, out value); + + public bool TryGetValue(RspIdentifier identifier, out RspEntry value) + => _rsp.TryGetValue(identifier, out value); + + public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) + => _imc.TryGetValue(identifier, out value); + + #endregion + + public bool Remove(IMetaIdentifier identifier) + { + var ret = identifier switch + { + EqdpIdentifier i => _eqdp.Remove(i), + EqpIdentifier i => _eqp.Remove(i), + EstIdentifier i => _est.Remove(i), + GlobalEqpManipulation i => _globalEqp.Remove(i), + GmpIdentifier i => _gmp.Remove(i), + ImcIdentifier i => _imc.Remove(i), + RspIdentifier i => _rsp.Remove(i), + _ => false, + }; + if (ret) + --Count; + return ret; + } + + #region Merging + public void UnionWith(MetaDictionary manips) { foreach (var (identifier, entry) in manips._imc) @@ -287,24 +349,6 @@ public class MetaDictionary : IEnumerable return false; } - public bool TryGetValue(EstIdentifier identifier, out EstEntry value) - => _est.TryGetValue(identifier, out value); - - public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) - => _eqp.TryGetValue(identifier, out value); - - public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) - => _eqdp.TryGetValue(identifier, out value); - - public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) - => _gmp.TryGetValue(identifier, out value); - - public bool TryGetValue(RspIdentifier identifier, out RspEntry value) - => _rsp.TryGetValue(identifier, out value); - - public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) - => _imc.TryGetValue(identifier, out value); - public void SetTo(MetaDictionary other) { _imc.SetTo(other._imc); @@ -329,6 +373,8 @@ public class MetaDictionary : IEnumerable Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; } + #endregion + public MetaDictionary Clone() { var ret = new MetaDictionary(); @@ -336,29 +382,124 @@ public class MetaDictionary : IEnumerable return ret; } - private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry) - { - var type = identifier switch + public static JObject Serialize(EqpIdentifier identifier, EqpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); + + public static JObject Serialize(EqpIdentifier identifier, EqpEntry entry) + => new() { - ImcIdentifier => "Imc", - EqdpIdentifier => "Eqdp", - EqpIdentifier => "Eqp", - EstIdentifier => "Est", - GmpIdentifier => "Gmp", - RspIdentifier => "Rsp", - GlobalEqpManipulation => "GlobalEqp", - _ => string.Empty, + ["Type"] = MetaManipulationType.Eqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ulong)entry, + }), }; - if (type.Length == 0) - return; + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); - writer.WriteStartObject(); - writer.WritePropertyName("Type"); - writer.WriteValue(type); - writer.WritePropertyName("Manipulation"); + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Eqdp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ushort)entry, + }), + }; - writer.WriteEndObject(); + public static JObject Serialize(EstIdentifier identifier, EstEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Est.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GmpIdentifier identifier, GmpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Gmp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(ImcIdentifier identifier, ImcEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Imc.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(RspIdentifier identifier, RspEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Rsp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GlobalEqpManipulation identifier) + => new() + { + ["Type"] = MetaManipulationType.GlobalEqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject()), + }; + + public static JObject? Serialize(TIdentifier identifier, TEntry entry) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EstIdentifier) && typeof(TEntry) == typeof(EstEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GmpIdentifier) && typeof(TEntry) == typeof(GmpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(RspIdentifier) && typeof(TEntry) == typeof(RspEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) + return Serialize(Unsafe.As(ref identifier)); + + return null; + } + + public static JArray SerializeTo(JArray array, IEnumerable> manipulations) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + foreach (var (identifier, entry) in manipulations) + { + if (Serialize(identifier, entry) is { } jObj) + array.Add(jObj); + } + + return array; + } + + public static JArray SerializeTo(JArray array, IEnumerable manipulations) + { + foreach (var manip in manipulations) + array.Add(Serialize(manip)); + + return array; } private class Converter : JsonConverter @@ -371,30 +512,27 @@ public class MetaDictionary : IEnumerable return; } - writer.WriteStartArray(); - foreach (var item in value) - { - writer.WriteStartObject(); - writer.WritePropertyName("Type"); - writer.WriteValue(item.ManipulationType.ToString()); - writer.WritePropertyName("Manipulation"); - serializer.Serialize(writer, item.Manipulation); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); + var array = new JArray(); + SerializeTo(array, value._imc); + SerializeTo(array, value._eqp); + SerializeTo(array, value._eqdp); + SerializeTo(array, value._est); + SerializeTo(array, value._rsp); + SerializeTo(array, value._gmp); + SerializeTo(array, value._globalEqp); + array.WriteTo(writer); } public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, JsonSerializer serializer) { - var dict = existingValue ?? []; + var dict = existingValue ?? new MetaDictionary(); dict.Clear(); var jObj = JArray.Load(reader); foreach (var item in jObj) { - var type = item["Type"]?.ToObject() ?? MetaManipulation.Type.Unknown; - if (type is MetaManipulation.Type.Unknown) + var type = item["Type"]?.ToObject() ?? MetaManipulationType.Unknown; + if (type is MetaManipulationType.Unknown) { Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); continue; @@ -408,7 +546,7 @@ public class MetaDictionary : IEnumerable switch (type) { - case MetaManipulation.Type.Imc: + case MetaManipulationType.Imc: { var identifier = ImcIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -418,7 +556,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); break; } - case MetaManipulation.Type.Eqdp: + case MetaManipulationType.Eqdp: { var identifier = EqdpIdentifier.FromJson(manip); var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); @@ -428,7 +566,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); break; } - case MetaManipulation.Type.Eqp: + case MetaManipulationType.Eqp: { var identifier = EqpIdentifier.FromJson(manip); var entry = (EqpEntry?)manip["Entry"]?.ToObject(); @@ -438,7 +576,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); break; } - case MetaManipulation.Type.Est: + case MetaManipulationType.Est: { var identifier = EstIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -448,7 +586,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EST Manipulation encountered."); break; } - case MetaManipulation.Type.Gmp: + case MetaManipulationType.Gmp: { var identifier = GmpIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -458,7 +596,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); break; } - case MetaManipulation.Type.Rsp: + case MetaManipulationType.Rsp: { var identifier = RspIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -468,7 +606,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; } - case MetaManipulation.Type.GlobalEqp: + case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); if (identifier.HasValue) @@ -483,4 +621,22 @@ public class MetaDictionary : IEnumerable return dict; } } + + public MetaDictionary() + { } + + public MetaDictionary(MetaCache? cache) + { + if (cache == null) + return; + + _imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); + Count = cache.Count; + } } diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs deleted file mode 100644 index b80681d2..00000000 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ /dev/null @@ -1,457 +0,0 @@ -using Dalamud.Interface; -using ImGuiNET; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Mods.Editor; -using Penumbra.String.Functions; -using Penumbra.UI; -using Penumbra.UI.ModsTab; - -namespace Penumbra.Meta.Manipulations; - -#if false -private static class ImcRow -{ - private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, - editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); - var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); - var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); - var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(manip); - - // Identifier - ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); - else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); - - ImGui.TableNextColumn(); - if (_newIdentifier.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); - else - ImUtf8.ScaledDummy(new Vector2(70 * UiHelpers.Scale, 0)); - - if (change) - defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); - } - - public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.ObjectType.ToName()); - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else - { - ImGui.TextUnformatted(meta.SecondaryId.ToString()); - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Variant.ToString()); - ImGuiUtil.HoverTooltip(VariantIdTooltip); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.DemiHuman) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - - // Values - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.TableNextColumn(); - var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; - var newEntry = meta.Entry; - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - - if (changes) - editor.MetaEditor.Change(meta.Copy(newEntry)); - } -} - -#endif - -public interface IMetaManipulation -{ - public MetaIndex FileIndex(); -} - -public interface IMetaManipulation - : IMetaManipulation, IComparable, IEquatable where T : struct -{ } - -[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 16)] -public readonly struct MetaManipulation : IEquatable, IComparable -{ - public const int CurrentVersion = 0; - - public enum Type : byte - { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, - GlobalEqp = 7, - } - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqpManipulation Eqp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GmpManipulation Gmp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqdpManipulation Eqdp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EstManipulation Est = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly RspManipulation Rsp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly ImcManipulation Imc = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GlobalEqpManipulation GlobalEqp = default; - - [FieldOffset(15)] - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("Type")] - public readonly Type ManipulationType; - - public object? Manipulation - { - get => ManipulationType switch - { - Type.Unknown => null, - Type.Imc => Imc, - Type.Eqdp => Eqdp, - Type.Eqp => Eqp, - Type.Est => Est, - Type.Gmp => Gmp, - Type.Rsp => Rsp, - Type.GlobalEqp => GlobalEqp, - _ => null, - }; - init - { - switch (value) - { - case EqpManipulation m: - Eqp = m; - ManipulationType = m.Validate() ? Type.Eqp : Type.Unknown; - return; - case EqdpManipulation m: - Eqdp = m; - ManipulationType = m.Validate() ? Type.Eqdp : Type.Unknown; - return; - case GmpManipulation m: - Gmp = m; - ManipulationType = m.Validate() ? Type.Gmp : Type.Unknown; - return; - case EstManipulation m: - Est = m; - ManipulationType = m.Validate() ? Type.Est : Type.Unknown; - return; - case RspManipulation m: - Rsp = m; - ManipulationType = m.Validate() ? Type.Rsp : Type.Unknown; - return; - case ImcManipulation m: - Imc = m; - ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; - return; - case GlobalEqpManipulation m: - GlobalEqp = m; - ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown; - return; - } - } - } - - public bool Validate() - { - return ManipulationType switch - { - Type.Imc => Imc.Validate(true), - Type.Eqdp => Eqdp.Validate(), - Type.Eqp => Eqp.Validate(), - Type.Est => Est.Validate(), - Type.Gmp => Gmp.Validate(), - Type.Rsp => Rsp.Validate(), - Type.GlobalEqp => GlobalEqp.Validate(), - _ => false, - }; - } - - public MetaManipulation(EqpManipulation eqp) - { - Eqp = eqp; - ManipulationType = Type.Eqp; - } - - public MetaManipulation(GmpManipulation gmp) - { - Gmp = gmp; - ManipulationType = Type.Gmp; - } - - public MetaManipulation(EqdpManipulation eqdp) - { - Eqdp = eqdp; - ManipulationType = Type.Eqdp; - } - - public MetaManipulation(EstManipulation est) - { - Est = est; - ManipulationType = Type.Est; - } - - public MetaManipulation(RspManipulation rsp) - { - Rsp = rsp; - ManipulationType = Type.Rsp; - } - - public MetaManipulation(ImcManipulation imc) - { - Imc = imc; - ManipulationType = Type.Imc; - } - - public MetaManipulation(GlobalEqpManipulation eqp) - { - GlobalEqp = eqp; - ManipulationType = Type.GlobalEqp; - } - - public static implicit operator MetaManipulation(EqpManipulation eqp) - => new(eqp); - - public static implicit operator MetaManipulation(GmpManipulation gmp) - => new(gmp); - - public static implicit operator MetaManipulation(EqdpManipulation eqdp) - => new(eqdp); - - public static implicit operator MetaManipulation(EstManipulation est) - => new(est); - - public static implicit operator MetaManipulation(RspManipulation rsp) - => new(rsp); - - public static implicit operator MetaManipulation(ImcManipulation imc) - => new(imc); - - public static implicit operator MetaManipulation(GlobalEqpManipulation eqp) - => new(eqp); - - public bool EntryEquals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), - Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), - Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), - Type.Est => Est.Entry.Equals(other.Est.Entry), - Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), - Type.Imc => Imc.Entry.Equals(other.Imc.Entry), - Type.GlobalEqp => true, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public bool Equals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Equals(other.Eqp), - Type.Gmp => Gmp.Equals(other.Gmp), - Type.Eqdp => Eqdp.Equals(other.Eqdp), - Type.Est => Est.Equals(other.Est), - Type.Rsp => Rsp.Equals(other.Rsp), - Type.Imc => Imc.Equals(other.Imc), - Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp), - _ => false, - }; - } - - public MetaManipulation WithEntryOf(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return this; - - return ManipulationType switch - { - Type.Eqp => Eqp.Copy(other.Eqp.Entry), - Type.Gmp => Gmp.Copy(other.Gmp.Entry), - Type.Eqdp => Eqdp.Copy(other.Eqdp), - Type.Est => Est.Copy(other.Est.Entry), - Type.Rsp => Rsp.Copy(other.Rsp.Entry), - Type.Imc => Imc.Copy(other.Imc.Entry), - Type.GlobalEqp => GlobalEqp, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public override bool Equals(object? obj) - => obj is MetaManipulation other && Equals(other); - - public override int GetHashCode() - => ManipulationType switch - { - Type.Eqp => Eqp.GetHashCode(), - Type.Gmp => Gmp.GetHashCode(), - Type.Eqdp => Eqdp.GetHashCode(), - Type.Est => Est.GetHashCode(), - Type.Rsp => Rsp.GetHashCode(), - Type.Imc => Imc.GetHashCode(), - Type.GlobalEqp => GlobalEqp.GetHashCode(), - _ => 0, - }; - - public unsafe int CompareTo(MetaManipulation other) - { - fixed (MetaManipulation* lhs = &this) - { - return MemoryUtility.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); - } - } - - public override string ToString() - => ManipulationType switch - { - Type.Eqp => Eqp.ToString(), - Type.Gmp => Gmp.ToString(), - Type.Eqdp => Eqdp.ToString(), - Type.Est => Est.ToString(), - Type.Rsp => Rsp.ToString(), - Type.Imc => Imc.ToString(), - Type.GlobalEqp => GlobalEqp.ToString(), - _ => "Invalid", - }; - - public string EntryToString() - => ManipulationType switch - { - Type.Imc => - $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", - Type.Est => $"{Est.Entry}", - Type.Gmp => $"{Gmp.Entry.Value}", - Type.Rsp => $"{Rsp.Entry}", - Type.GlobalEqp => string.Empty, - _ => string.Empty, - }; - - public static bool operator ==(MetaManipulation left, MetaManipulation right) - => left.Equals(right); - - public static bool operator !=(MetaManipulation left, MetaManipulation right) - => !(left == right); - - public static bool operator <(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) < 0; - - public static bool operator <=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) <= 0; - - public static bool operator >(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) > 0; - - public static bool operator >=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) >= 0; -} - diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index ca7cb1c5..73d1d7e5 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -38,6 +38,9 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib var ret = new RspIdentifier(subRace, attribute); return ret.Validate() ? ret : null; } + + public MetaManipulationType Type + => MetaManipulationType.Rsp; } [JsonConverter(typeof(Converter))] diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs deleted file mode 100644 index e2282c41..00000000 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public RspIdentifier Identifier { get; } = identifier; - - public RspEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public SubRace SubRace - => Identifier.SubRace; - - [JsonConverter(typeof(StringEnumConverter))] - public RspAttribute Attribute - => Identifier.Attribute; - - [JsonConstructor] - public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - : this(new RspIdentifier(subRace, attribute), entry) - { } - - public RspManipulation Copy(RspEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; - - public bool Equals(RspManipulation other) - => SubRace == other.SubRace - && Attribute == other.Attribute; - - public override bool Equals(object? obj) - => obj is RspManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)SubRace, (int)Attribute); - - public int CompareTo(RspManipulation other) - { - var s = SubRace.CompareTo(other.SubRace); - return s != 0 ? s : Attribute.CompareTo(other.Attribute); - } - - public MetaIndex FileIndex() - => MetaIndex.HumanCmp; - - public bool Apply(CmpFile file) - { - var value = file[SubRace, Attribute]; - if (value == Entry) - return false; - - file[SubRace, Attribute] = Entry; - return true; - } - - public bool Validate() - => Identifier.Validate() && Entry.Validate(); -} diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 06c31846..3da38829 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -10,7 +10,7 @@ public record struct AppliedModData( Dictionary FileRedirections, MetaDictionary Manipulations) { - public static readonly AppliedModData Empty = new([], []); + public static readonly AppliedModData Empty = new([], new MetaDictionary()); } public interface IMod diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 42171378..bacf4122 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -26,20 +26,20 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService } } - public readonly FrozenDictionary OtherData = - Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); + public readonly FrozenDictionary OtherData = + Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); - public bool Changes { get; private set; } + public bool Changes { get; set; } public new void Clear() { + Changes = Count > 0; base.Clear(); - Changes = true; } public void Load(Mod mod, IModDataContainer currentOption) { - foreach (var type in Enum.GetValues()) + foreach (var type in Enum.GetValues()) OtherData[type].Clear(); foreach (var option in mod.AllDataContainers) @@ -48,13 +48,13 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService continue; var name = option.GetFullName(); - OtherData[MetaManipulation.Type.Imc].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Imc)); - OtherData[MetaManipulation.Type.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqp)); - OtherData[MetaManipulation.Type.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqdp)); - OtherData[MetaManipulation.Type.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Gmp)); - OtherData[MetaManipulation.Type.Est].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Est)); - OtherData[MetaManipulation.Type.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Rsp)); - OtherData[MetaManipulation.Type.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.GlobalEqp)); + OtherData[MetaManipulationType.Imc].Add(name, option.Manipulations.GetCount(MetaManipulationType.Imc)); + OtherData[MetaManipulationType.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqp)); + OtherData[MetaManipulationType.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqdp)); + OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); + OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); + OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); + OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } Clear(); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index e42a1d31..b7827c47 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -125,8 +125,8 @@ public static class EquipmentSwap _ => (EstType)0, }; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -242,8 +242,8 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry.Identifier); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; Variant[] variants; if (idFrom == idTo) @@ -273,7 +273,8 @@ public static class EquipmentSwap var manipToIdentifier = new GmpIdentifier(idTo); var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); - return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); } public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, @@ -286,16 +287,17 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId); if (decal != null) imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModdedEntry.VfxId); + var avfx = CreateAvfx(manager, redirections, slotFrom, slotTo, idFrom, idTo, imc.SwapToModdedEntry.VfxId); if (avfx != null) imc.ChildSwaps.Add(avfx); @@ -316,19 +318,21 @@ public static class EquipmentSwap // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, PrimaryId idTo, byte vfxId) { if (vfxId == 0) return null; var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); + var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { - var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged); + var atex = CreateAtex(manager, redirections, slotFrom, slotTo, idFrom, ref filePath, ref avfx.DataWasChanged); avfx.ChildSwaps.Add(atex); } @@ -394,8 +398,7 @@ public static class EquipmentSwap } public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, - PrimaryId idTo, - ref MtrlFile.Texture texture, ref bool dataWasChanged) + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, @@ -404,6 +407,7 @@ public static class EquipmentSwap var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); + newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { @@ -421,11 +425,12 @@ public static class EquipmentSwap return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } - public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, ref string filePath, - ref bool dataWasChanged) + public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index efd8080c..1f4c5e7a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -136,14 +136,14 @@ public static class ItemSwap public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } @@ -154,10 +154,11 @@ public static class ItemSwap return null; var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); - var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); - var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); - var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); + var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); + var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); if (ownMdl && est.SwapToModdedEntry.Value >= 2) { @@ -215,6 +216,22 @@ public static class ItemSwap ? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_") : path; + public static string ReplaceType(string path, EquipSlot from, EquipSlot to, PrimaryId idFrom) + { + var isAccessoryFrom = from.IsAccessory(); + if (isAccessoryFrom == to.IsAccessory()) + return path; + + if (isAccessoryFrom) + { + path = path.Replace("accessory/a", "equipment/e"); + return path.Replace($"a{idFrom.Id:D4}", $"e{idFrom.Id:D4}"); + } + + path = path.Replace("equipment/e", "accessory/a"); + return path.Replace($"e{idFrom.Id:D4}", $"a{idFrom.Id:D4}"); + } + public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true) => ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 8328edea..d2deb9ef 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -123,8 +123,8 @@ public class ItemSwapContainer : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); private MetaDictionary MetaResolver(ModCollection? collection) - => collection?.MetaCache?.Manipulations is { } cache - ? [] // [.. cache] TODO + => collection?.MetaCache is { } cache + ? new MetaDictionary(cache) : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index e8ca3199..ed4245c4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,8 +198,7 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - // TODO - option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]); + option.Manipulations.UnionWith(meta.MetaManipulations); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -213,8 +212,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - // TODO - option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]); + option.Manipulations.UnionWith(rgsp.MetaManipulations); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index dcd33610..3840468f 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public MetaDictionary Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); IMod IModDataContainer.Mod => Mod; diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index ed7b6ff8..8fac52d8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public MetaDictionary Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 91c4c5df..e1cf9f2b 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,8 +93,7 @@ public class TemporaryMod : IMod } } - // TODO - MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []]; + var manips = new MetaDictionary(collection.MetaCache); defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs new file mode 100644 index 00000000..b1ac93f1 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -0,0 +1,159 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Model Edits (EQDP)###EQDP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EqdpIdentifier(1, EquipSlot.Head, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, Identifier), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + + ImGui.TableNextColumn(); + var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; + var canAdd = validRaceCode && !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : + validRaceCode ? "This entry is already edited."u8 : "This combination of race and gender can not be used."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, identifier), identifier.Slot); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() + => Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EqdpEntryInternal defaultEntry, ref EqdpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + if (Checkmark("Material##eqdp"u8, "\0"u8, entry.Material, defaultEntry.Material, out var newMaterial)) + { + entry = entry with { Material = newMaterial }; + changes = true; + } + + ImGui.SameLine(); + if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel)) + { + entry = entry with { Material = newModel }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqdpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##eqdpRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EqdpIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##eqdpGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawEquipSlot(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqdpEquipSlot("##eqdpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs new file mode 100644 index 00000000..56c06bc9 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -0,0 +1,134 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Equipment Parameter Edits (EQP)###EQP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, Identifier.SetId), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Identifier.Slot, Entry, ref Entry, true); + } + + protected override void DrawEntry(EqpIdentifier identifier, EqpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, identifier.SetId), identifier.Slot); + if (DrawEntry(identifier.Slot, defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() + => Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EquipSlot slot, EqpEntryInternal defaultEntry, ref EqpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var offset = Eqp.OffsetAndMask(slot).Item1; + DrawBox(ref entry, 0); + for (var i = 1; i < Eqp.EqpAttributes[slot].Count; ++i) + { + ImUtf8.SameLineInner(); + DrawBox(ref entry, i); + } + + return changes; + + void DrawBox(ref EqpEntryInternal entry, int i) + { + using var id = ImUtf8.PushId(i); + var flag = 1u << i; + var eqpFlag = (EqpEntry)((ulong)flag << offset); + var defaultValue = (flag & defaultEntry.Value) != 0; + var value = (flag & entry.Value) != 0; + if (Checkmark("##eqp"u8, eqpFlag.ToLocalName(), value, defaultValue, out var newValue)) + { + entry = new EqpEntryInternal(newValue ? entry.Value | flag : entry.Value & ~flag); + changes = true; + } + } + } + + public static bool DrawPrimaryId(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawEquipSlot(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqpEquipSlot("##eqpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs new file mode 100644 index 00000000..5c3c5df5 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -0,0 +1,147 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Extra Skeleton Parameters (EST)###EST"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EstIdentifier(1, EstType.Hair, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = EstFile.GetDefault(MetaFiles, Identifier.Slot, Identifier.GenderRace, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = EstFile.GetDefault(MetaFiles, identifier.Slot, identifier.GenderRace, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() + => Editor.Est.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EstIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawSlot(ref identifier); + + return changes; + } + + private static void DrawIdentifier(EstIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToString(), FrameColor); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + } + + private static bool DrawEntry(EstEntry defaultEntry, ref EstEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##estValue"u8, [], 100f * ImUtf8.GlobalScale, entry.Value, defaultEntry.Value, out var newValue, (ushort)0, + ushort.MaxValue, 0.05f, !disabled); + if (ret) + entry = new EstEntry(newValue); + return ret; + } + + public static bool DrawPrimaryId(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##estPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##estRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EstIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##estGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawSlot(ref EstIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.EstSlot("##estSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs new file mode 100644 index 00000000..130831a0 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -0,0 +1,111 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8; + + public override int NumColumns + => 4; + + protected override void Initialize() + { + Identifier = new GlobalEqpManipulation() + { + Condition = 1, + Type = GlobalEqpType.DoNotHideEarrings, + }; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier); + + DrawIdentifierInput(ref Identifier); + } + + protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) + { + DrawMetaButtons(identifier, 0); + DrawIdentifier(identifier); + } + + protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() + => Editor.GlobalEqp.Select(identifier => (identifier, (byte)0)); + + private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + DrawType(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + DrawCondition(ref identifier); + else + ImUtf8.ScaledDummy(100); + } + + private static void DrawIdentifier(GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Global EQP Type"u8); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + { + ImUtf8.TextFramed($"{identifier.Condition.Id}", FrameColor); + ImUtf8.HoverTooltip("Conditional Model ID"u8); + } + } + + public static bool DrawType(ref GlobalEqpManipulation identifier, float unscaledWidth = 250) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var combo = ImUtf8.Combo("##geqpType"u8, identifier.Type.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(type.ToName(), type == identifier.Type)) + { + identifier = new GlobalEqpManipulation + { + Type = type, + Condition = type.HasCondition() ? identifier.Type.HasCondition() ? identifier.Condition : 1 : 0, + }; + ret = true; + } + + ImUtf8.HoverTooltip(type.ToDescription()); + } + + return ret; + } + + public static void DrawCondition(ref GlobalEqpManipulation identifier, float unscaledWidth = 100) + { + if (IdInput("##geqpCond"u8, unscaledWidth, identifier.Condition.Id, out var newId, 1, ushort.MaxValue, + identifier.Condition.Id <= 1)) + identifier = identifier with { Condition = newId }; + ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs new file mode 100644 index 00000000..87ed21dc --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -0,0 +1,148 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Visor/Gimmick Edits (GMP)###GMP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new GmpIdentifier(1); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedGmpFile.GetDefault(MetaFiles, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = ExpandedGmpFile.GetDefault(MetaFiles, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() + => Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + return DrawPrimaryId(ref identifier); + } + + private static void DrawIdentifier(GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + } + + private static bool DrawEntry(GmpEntry defaultEntry, ref GmpEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var changes = false; + if (Checkmark("##gmpEnabled"u8, "Gimmick Enabled", entry.Enabled, defaultEntry.Enabled, out var enabled)) + { + entry = entry with { Enabled = enabled }; + changes = true; + } + + ImGui.TableNextColumn(); + if (Checkmark("##gmpAnimated"u8, "Gimmick Animated", entry.Animated, defaultEntry.Animated, out var animated)) + { + entry = entry with { Animated = animated }; + changes = true; + } + + var rotationWidth = 75 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpRotationA"u8, "Rotation A in Degrees"u8, rotationWidth, entry.RotationA, defaultEntry.RotationA, out var rotationA, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationA = rotationA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationB"u8, "Rotation B in Degrees"u8, rotationWidth, entry.RotationB, defaultEntry.RotationB, out var rotationB, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationB = rotationB }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationC"u8, "Rotation C in Degrees"u8, rotationWidth, entry.RotationC, defaultEntry.RotationC, out var rotationC, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationC = rotationC }; + changes = true; + } + + var unkWidth = 50 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpUnkA"u8, "Animation Type A?"u8, unkWidth, entry.UnknownA, defaultEntry.UnknownA, out var unknownA, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownA = unknownA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpUnkB"u8, "Animation Type B?"u8, unkWidth, entry.UnknownB, defaultEntry.UnknownB, out var unknownB, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownB = unknownB }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref GmpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##gmpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = new GmpIdentifier(setId); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index d9a8c27c..58f626fc 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -12,22 +12,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow.Meta; -public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) +public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : MetaDrawer(editor, metaFiles), IService { - private bool _fileExists; + public override ReadOnlySpan Label + => "Variant Edits (IMC)###IMC"u8; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIdTooltipShort = "Primary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + public override int NumColumns + => 10; + + private bool _fileExists; protected override void Initialize() { @@ -41,14 +35,14 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override void DrawNew() { ImGui.TableNextColumn(); - // Copy To Clipboard + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); ImGui.TableNextColumn(); - var canAdd = _fileExists && Editor.MetaEditor.CanAdd(Identifier); + var canAdd = _fileExists && !Editor.Contains(Identifier); var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) - Editor.MetaEditor.TryAdd(Identifier, Entry); + Editor.Changes |= Editor.TryAdd(Identifier, Entry); - if (DrawIdentifier(ref Identifier)) + if (DrawIdentifierInput(ref Identifier)) UpdateEntry(); using var disabled = ImRaii.Disabled(); @@ -57,46 +51,15 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry) { - const uint frameColor = 0; - // Meta Buttons - - ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.ObjectType.ToName(), frameColor); - ImUtf8.HoverTooltip("Object Type"u8); - - ImGui.TableNextColumn(); - ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", frameColor); - ImUtf8.HoverTooltip("Primary ID"); - - ImGui.TableNextColumn(); - if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); - ImUtf8.HoverTooltip("Equip Slot"u8); - } - else - { - ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", frameColor); - ImUtf8.HoverTooltip("Secondary ID"u8); - } - - ImGui.TableNextColumn(); - ImUtf8.TextFramed($"{identifier.Variant.Id}", frameColor); - ImUtf8.HoverTooltip("Variant"u8); - - ImGui.TableNextColumn(); - if (identifier.ObjectType is ObjectType.DemiHuman) - { - ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); - ImUtf8.HoverTooltip("Equip Slot"u8); - } + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; if (DrawEntry(defaultEntry, ref entry, true)) - Editor.MetaEditor.Update(identifier, entry); + Editor.Changes |= Editor.Update(identifier, entry); } - private static bool DrawIdentifier(ref ImcIdentifier identifier) + private static bool DrawIdentifierInput(ref ImcIdentifier identifier) { ImGui.TableNextColumn(); var change = DrawObjectType(ref identifier); @@ -121,6 +84,41 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return change; } + private static void DrawIdentifier(ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.ObjectType.ToName(), FrameColor); + ImUtf8.HoverTooltip("Object Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + else + { + ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Secondary ID"u8); + } + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.Variant.Id}", FrameColor); + ImUtf8.HoverTooltip("Variant"u8); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + } + private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) { ImGui.TableNextColumn(); @@ -142,7 +140,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() - => Editor.MetaEditor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Imc.Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { @@ -270,7 +268,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return true; } - public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + private static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) { var changes = false; for (var i = 0; i < ImcEntry.NumAttributes; ++i) @@ -292,62 +290,4 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return changes; } - - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - /// - /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - /// Returns true if newValue changed against currentValue. - /// - private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, - out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) - newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; - - if (addDefault) - ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - - return newValue != currentValue; - } - - /// - /// A checkmark that compares against a default value and shows a tooltip. - /// Returns true if newValue is changed against currentValue. - /// - private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, - out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImUtf8.Checkbox(label, ref newValue); - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - return newValue != currentValue; - } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs new file mode 100644 index 00000000..229526c4 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -0,0 +1,154 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Api.Api; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public interface IMetaDrawer +{ + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public void Draw(); +} + +public abstract class MetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : IMetaDrawer + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + protected const uint FrameColor = 0; + + protected readonly ModMetaEditor Editor = editor; + protected readonly MetaFileManager MetaFiles = metaFiles; + protected TIdentifier Identifier; + protected TEntry Entry; + private bool _initialized; + + public void Draw() + { + if (!_initialized) + { + Initialize(); + _initialized = true; + } + + using var id = ImUtf8.PushId((int)Identifier.Type); + DrawNew(); + foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) + { + id.Push(idx); + DrawEntry(identifier, entry); + id.Pop(); + } + } + + public abstract ReadOnlySpan Label { get; } + public abstract int NumColumns { get; } + + protected abstract void DrawNew(); + protected abstract void Initialize(); + protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); + + protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + protected static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + protected static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + + ImGui.TableNextColumn(); + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) + Editor.Changes |= Editor.Remove(identifier); + } + + protected void CopyToClipboardButton(ReadOnlySpan tooltip, JToken? manipulations) + { + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) + return; + + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + if (text.Length > 0) + ImGui.SetClipboardText(text); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs new file mode 100644 index 00000000..b3dd9299 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -0,0 +1,35 @@ +using OtterGui.Services; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public class MetaDrawers( + EqdpMetaDrawer eqdp, + EqpMetaDrawer eqp, + EstMetaDrawer est, + GlobalEqpMetaDrawer globalEqp, + GmpMetaDrawer gmp, + ImcMetaDrawer imc, + RspMetaDrawer rsp) : IService +{ + public readonly EqdpMetaDrawer Eqdp = eqdp; + public readonly EqpMetaDrawer Eqp = eqp; + public readonly EstMetaDrawer Est = est; + public readonly GmpMetaDrawer Gmp = gmp; + public readonly RspMetaDrawer Rsp = rsp; + public readonly ImcMetaDrawer Imc = imc; + public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + + public IMetaDrawer? Get(MetaManipulationType type) + => type switch + { + MetaManipulationType.Imc => Imc, + MetaManipulationType.Eqdp => Eqdp, + MetaManipulationType.Eqp => Eqp, + MetaManipulationType.Est => Est, + MetaManipulationType.Gmp => Gmp, + MetaManipulationType.Rsp => Rsp, + MetaManipulationType.GlobalEqp => GlobalEqp, + _ => null, + }; +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs new file mode 100644 index 00000000..2b7904ce --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -0,0 +1,112 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Scaling Edits (RSP)###RSP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new RspIdentifier(SubRace.Midlander, RspAttribute.MaleMinSize); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = CmpFile.GetDefault(MetaFiles, Identifier.SubRace, Identifier.Attribute); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = CmpFile.GetDefault(MetaFiles, identifier.SubRace, identifier.Attribute); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() + => Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref RspIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawSubRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttribute(ref identifier); + return changes; + } + + private static void DrawIdentifier(RspIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.SubRace.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.ToFullString(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(RspEntry defaultEntry, ref RspEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, defaultEntry.Value, entry.Value, out var newValue, + RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled); + if (ret) + entry = new RspEntry(newValue); + return ret; + } + + public static bool DrawSubRace(ref RspIdentifier identifier, float unscaledWidth = 150) + { + var ret = Combos.SubRace("##rspSubRace", identifier.SubRace, out var subRace, unscaledWidth); + ImUtf8.HoverTooltip("Racial Clan"u8); + if (ret) + identifier = identifier with { SubRace = subRace }; + return ret; + } + + public static bool DrawAttribute(ref RspIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.RspAttribute("##rspAttribute", identifier.Attribute, out var attribute, unscaledWidth); + ImUtf8.HoverTooltip("Scaling Attribute"u8); + if (ret) + identifier = identifier with { Attribute = attribute }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 50862eec..3ec6a4d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,27 +1,18 @@ -using System.Reflection.Emit; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using OtterGui.Text.EndObjects; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; +using Penumbra.Api.Api; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const string ModelSetIdTooltip = - "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - - + private readonly MetaDrawers _metaDrawers; private void DrawMetaTab() { @@ -56,80 +47,42 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(MetaManipulation.Type.Eqp); - DrawEditHeader(MetaManipulation.Type.Eqdp); - DrawEditHeader(MetaManipulation.Type.Imc); - DrawEditHeader(MetaManipulation.Type.Est); - DrawEditHeader(MetaManipulation.Type.Gmp); - DrawEditHeader(MetaManipulation.Type.Rsp); - DrawEditHeader(MetaManipulation.Type.GlobalEqp); + DrawEditHeader(MetaManipulationType.Eqp); + DrawEditHeader(MetaManipulationType.Eqdp); + DrawEditHeader(MetaManipulationType.Imc); + DrawEditHeader(MetaManipulationType.Est); + DrawEditHeader(MetaManipulationType.Gmp); + DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.GlobalEqp); } - private static ReadOnlySpan Label(MetaManipulation.Type type) - => type switch - { - MetaManipulation.Type.Imc => "Variant Edits (IMC)###IMC"u8, - MetaManipulation.Type.Eqdp => "Racial Model Edits (EQDP)###EQDP"u8, - MetaManipulation.Type.Eqp => "Equipment Parameter Edits (EQP)###EQP"u8, - MetaManipulation.Type.Est => "Extra Skeleton Parameters (EST)###EST"u8, - MetaManipulation.Type.Gmp => "Visor/Gimmick Edits (GMP)###GMP"u8, - MetaManipulation.Type.Rsp => "Racial Scaling Edits (RSP)###RSP"u8, - MetaManipulation.Type.GlobalEqp => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8, - _ => "\0"u8, - }; - - private static int ColumnCount(MetaManipulation.Type type) - => type switch - { - MetaManipulation.Type.Imc => 10, - MetaManipulation.Type.Eqdp => 7, - MetaManipulation.Type.Eqp => 5, - MetaManipulation.Type.Est => 7, - MetaManipulation.Type.Gmp => 7, - MetaManipulation.Type.Rsp => 5, - MetaManipulation.Type.GlobalEqp => 4, - _ => 0, - }; - - private void DrawEditHeader(MetaManipulation.Type type) + private void DrawEditHeader(MetaManipulationType type) { + var drawer = _metaDrawers.Get(type); + if (drawer == null) + return; + var oldPos = ImGui.GetCursorPosY(); - var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {Label(type)}"); + var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {drawer.Label}"); DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos()); if (!header) return; - DrawTable(type); + DrawTable(drawer); } - private IMetaDrawer? Drawer(MetaManipulation.Type type) - => type switch - { - //MetaManipulation.Type.Imc => expr, - //MetaManipulation.Type.Eqdp => expr, - //MetaManipulation.Type.Eqp => expr, - //MetaManipulation.Type.Est => expr, - //MetaManipulation.Type.Gmp => expr, - //MetaManipulation.Type.Rsp => expr, - //MetaManipulation.Type.GlobalEqp => expr, - _ => null, - }; - - private void DrawTable(MetaManipulation.Type type) + private static void DrawTable(IMetaDrawer drawer) { const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - using var table = ImUtf8.Table(Label(type), ColumnCount(type), flags); + using var table = ImUtf8.Table(drawer.Label, drawer.NumColumns, flags); if (!table) return; - if (Drawer(type) is not { } drawer) - return; - drawer.Draw(); ImGui.NewLine(); } - private void DrawOtherOptionData(MetaManipulation.Type type, float oldPos, Vector2 newPos) + private void DrawOtherOptionData(MetaManipulationType type, float oldPos, Vector2 newPos) { var otherOptionData = _editor.MetaEditor.OtherData[type]; if (otherOptionData.TotalCount <= 0) @@ -149,577 +102,12 @@ public partial class ModEditWindow ImGui.SetCursorPos(newPos); } -#if false - private static class EqpRow - { - private static EqpIdentifier _newIdentifier = new(1, EquipSlot.Body); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - foreach (var flag in Eqp.EqpAttributes[_new.Slot]) - { - var value = defaultEntry.HasFlag(flag); - Checkmark("##eqp", flag.ToLocalName(), value, value, out _); - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - var idx = 0; - foreach (var flag in Eqp.EqpAttributes[meta.Slot]) - { - using var id = ImRaii.PushId(idx++); - var defaultValue = defaultEntry.HasFlag(flag); - var currentValue = meta.Entry.HasFlag(flag); - if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) - editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag)); - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - private static class EqdpRow - { - private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace(_new.Gender, _new.Race); - var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; - var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : - validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; - var defaultEntry = validRaceCode - ? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) - : 0; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - _new.Slot.IsAccessory(), setId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##eqdpRace", _new.Race, out var race)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - var (bit1, bit2) = defaultEntry.ToBits(_new.Slot); - Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _); - ImGui.SameLine(); - Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); - } - - public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), - meta.SetId); - var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); - var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); - ImGui.TableNextColumn(); - if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); - - ImGui.SameLine(); - if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); - } - } - - private static class EstRow - { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, - editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##estId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); - _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##estRace", _new.Race, out var race)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); - _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##estGender", _new.Gender, out var gender)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); - _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f); - } - - public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToString()); - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); - ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, - out var entry, 0, ushort.MaxValue, 0.05f)) - editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); - } - } - private static class GmpRow - { - private static GmpManipulation _new = new(GmpEntry.Default, 1); - - private static float RotationWidth - => 75 * UiHelpers.Scale; - - private static float UnkWidth - => 50 * UiHelpers.Scale; - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, - editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##gmpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _); - ImGui.TableNextColumn(); - Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _); - ImGui.TableNextColumn(); - IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, - 360, 0f); - ImGui.TableNextColumn(); - IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f); - ImGui.SameLine(); - IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - - // Values - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId); - ImGui.TableNextColumn(); - if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); - - ImGui.TableNextColumn(); - if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, - meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, - meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, - meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, - defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, - defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); - } - } - private static class RspRow - { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); - - private static float FloatWidth - => 150 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, - editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) - _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute)); - - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - - ImGui.TableNextColumn(); - if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) - _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute)); - - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(FloatWidth); - var value = defaultEntry.Value; - ImGui.DragFloat("##rspValue", ref value, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SubRace.ToName()); - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Attribute.ToFullString()); - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - ImGui.TableNextColumn(); - - // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; - var value = meta.Entry.Value; - ImGui.SetNextItemWidth(FloatWidth); - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), - def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue) - && value is >= RspEntry.MinValue and <= RspEntry.MaxValue) - editor.MetaEditor.Change(meta.Copy(new RspEntry(value))); - - ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); - } - } - private static class GlobalEqpRow - { - private static GlobalEqpManipulation _new = new() - { - Type = GlobalEqpType.DoNotHideEarrings, - Condition = 1, - }; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale); - using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName())) - { - if (combo) - foreach (var type in Enum.GetValues()) - { - if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) - _new = new GlobalEqpManipulation - { - Type = type, - Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, - }; - ImUtf8.HoverTooltip(type.ToDescription()); - } - } - - ImUtf8.HoverTooltip(_new.Type.ToDescription()); - - ImGui.TableNextColumn(); - if (!_new.Type.HasCondition()) - return; - - if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) - _new = _new with { Condition = newId }; - ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); - } - - public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text(meta.Type.ToName()); - ImUtf8.HoverTooltip(meta.Type.ToDescription()); - ImGui.TableNextColumn(); - if (meta.Type.HasCondition()) - { - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text($"{meta.Condition.Id}"); - } - } - } -#endif - - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. - private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(width); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImGui.InputInt(label, ref tmp, 0)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - // A checkmark that compares against a default value and shows a tooltip. - // Returns true if newValue is changed against currentValue. - private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImGui.Checkbox(label, ref newValue); - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - return newValue != currentValue; - } - - // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - // Returns true if newValue changed against currentValue. - private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, - int minValue, int maxValue, float speed) - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue)) - newValue = Math.Clamp(newValue, minValue, maxValue); - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return newValue != currentValue; - } - private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); if (text.Length > 0) ImGui.SetClipboardText(text); } @@ -731,8 +119,11 @@ public partial class ModEditWindow var clipboard = ImGuiUtil.GetClipboardText(); var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) + if (version == MetaApi.CurrentVersion && manips != null) + { _editor.MetaEditor.UpdateTo(manips); + _editor.MetaEditor.Changes = true; + } } ImGuiUtil.HoverTooltip( @@ -745,194 +136,14 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) + if (version == MetaApi.CurrentVersion && manips != null) + { _editor.MetaEditor.SetTo(manips); + _editor.MetaEditor.Changes = true; + } } ImGuiUtil.HoverTooltip( "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); } - - private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) - { - //ImGui.TableNextColumn(); - //CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); - // - //ImGui.TableNextColumn(); - //if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) - // editor.MetaEditor.Delete(meta); - } -} - - -public interface IMetaDrawer -{ - public void Draw(); } - - - - -public abstract class MetaDrawer(ModEditor editor, MetaFileManager metaFiles) : IMetaDrawer - where TIdentifier : unmanaged, IMetaIdentifier - where TEntry : unmanaged -{ - protected readonly ModEditor Editor = editor; - protected readonly MetaFileManager MetaFiles = metaFiles; - protected TIdentifier Identifier; - protected TEntry Entry; - private bool _initialized; - - public void Draw() - { - if (!_initialized) - { - Initialize(); - _initialized = true; - } - - DrawNew(); - foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) - { - using var id = ImUtf8.PushId(idx); - DrawEntry(identifier, entry); - } - } - - protected abstract void DrawNew(); - protected abstract void Initialize(); - protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); - - protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); -} - - -#if false -public sealed class GmpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new GmpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) - { } - - protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EstMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) - { } - - protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EqdpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqdpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntry entry) - { } - - protected override IEnumerable<(EqdpIdentifier, EqdpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EqpMetaDrawer(ModEditor editor, MetaFileManager metaManager) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EqpIdentifier identifier, EqpEntry entry) - { } - - protected override IEnumerable<(EqpIdentifier, EqpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class RspMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new RspIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) - { } - - protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - - - -public sealed class GlobalEqpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) - { } - - protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} -#endif diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 1f935eb6..72ab37b2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -95,7 +95,7 @@ public partial class ModEditWindow task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default); } - private EstManipulation[] GetCurrentEstManipulations() + private KeyValuePair[] GetCurrentEstManipulations() { var mod = _edit._editor.Mod; var option = _edit._editor.Option; @@ -106,9 +106,7 @@ public partial class ModEditWindow return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) - .SelectMany(subMod => subMod.Manipulations) - .Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est) - .Select(manipulation => manipulation.Est) + .SelectMany(subMod => subMod.Manipulations.Est) .ToArray(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index af01047b..9410b793 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -25,6 +25,7 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; @@ -586,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor) + CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers) : base(WindowBaseLabel) { _performance = performance; @@ -606,6 +607,7 @@ public partial class ModEditWindow : Window, IDisposable _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 5e3aac48..962d156d 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -120,9 +120,9 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy { var _ = data switch { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), - _ => false, + Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, + IMetaIdentifier m => ImGui.Selectable(m.ToString()), + _ => false, }; } } diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 37561000..7076b80f 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -98,7 +98,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH // Filters mean we can not use the known counts. if (hasFilters) { - var it2 = m.Select(p => (p.Key.ToString(), p.Value.Name)); + var it2 = m.IdentifierSources.Select(p => (p.Item1.ToString(), p.Item2.Name)); if (stop >= 0) { ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height); @@ -117,7 +117,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH } else { - stop = ImGuiClip.ClippedDraw(m, skips, DrawLine, m.Count, ~stop); + stop = ImGuiClip.ClippedDraw(m.IdentifierSources, skips, DrawLine, m.Count, ~stop); ImGuiClip.DrawEndDummy(stop, height); } } @@ -152,11 +152,11 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(name); + ImGuiUtil.CopyOnClickSelectable(name.Text); } /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine(KeyValuePair pair) + private static void DrawLine((IMetaIdentifier, IMod) pair) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); @@ -165,7 +165,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mod.Name); + ImGuiUtil.CopyOnClickSelectable(mod.Name.Text); } /// Check filters for file replacements. diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 0647ea8e..cb43ac06 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -2,7 +2,6 @@ using OtterGui.Classes; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; @@ -10,103 +9,14 @@ namespace Penumbra.Util; public static class IdentifierExtensions { - /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary changedItems, - MetaManipulation manip) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - switch (manip.Imc.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - identifier.Identify(changedItems, - GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Weapon: - identifier.Identify(changedItems, - GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - case ObjectType.DemiHuman: - identifier.Identify(changedItems, - GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Monster: - identifier.Identify(changedItems, - GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); - break; - case MetaManipulation.Type.Eqp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); - break; - case MetaManipulation.Type.Est: - switch (manip.Est.Slot) - { - case EstType.Hair: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); - break; - case EstType.Face: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); - break; - case EstType.Body: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Body)); - break; - case EstType.Head: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Head)); - break; - } - - break; - case MetaManipulation.Type.Gmp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); - break; - case MetaManipulation.Type.GlobalEqp: - var path = manip.GlobalEqp.Type switch - { - GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Ears), - GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Neck), - GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Wrists), - GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.RFinger), - GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.LFinger), - GlobalEqpType.DoNotHideHrothgarHats => string.Empty, - GlobalEqpType.DoNotHideVieraHats => string.Empty, - _ => string.Empty, - }; - if (path.Length > 0) - identifier.Identify(changedItems, path); - break; - } - } - public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in container.Manipulations) - MetaChangedItems(identifier, changedItems, manip); + foreach (var manip in container.Manipulations.Identifiers) + manip.AddChangedItems(identifier, changedItems); } public static void RemoveMachinistOffhands(this SortedList changedItems) From 4ca49598f8ffff55728e98e108183c88d679c2e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 17:40:47 +0200 Subject: [PATCH 1758/2451] Small improvement. --- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 962d156d..c1a3c1eb 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; @@ -116,15 +117,12 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy using var indent = ImRaii.PushIndent(30f); foreach (var data in conflict.Conflicts) { - unsafe + _ = data switch { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - IMetaIdentifier m => ImGui.Selectable(m.ToString()), - _ => false, - }; - } + Utf8GamePath p => ImUtf8.Selectable(p.Path.Span, false), + IMetaIdentifier m => ImUtf8.Selectable(m.ToString(), false), + _ => false, + }; } return true; From e33512cf7ff3dcc887b0516188e79d6d9fe09aa1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 23:09:38 +0200 Subject: [PATCH 1759/2451] Fix issue, remove IMetaCache. --- Penumbra/Collections/Cache/IMetaCache.cs | 14 +++----------- Penumbra/Meta/Manipulations/MetaDictionary.cs | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index 218c1840..eacbdcc2 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -4,21 +4,12 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public interface IMetaCache : IDisposable -{ - public void SetFiles(); - public void Reset(); - public void ResetFiles(); - - public int Count { get; } -} - public abstract class MetaCacheBase - : Dictionary, IMetaCache + : Dictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - public MetaCacheBase(MetaFileManager manager, ModCollection collection) + protected MetaCacheBase(MetaFileManager manager, ModCollection collection) { Manager = manager; Collection = collection; @@ -76,6 +67,7 @@ public abstract class MetaCacheBase { IncorporateChangesInternal(); } + if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) SetFiles(); } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 5a51df83..1093c6c5 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -346,7 +346,7 @@ public class MetaDictionary } failedIdentifier = default; - return false; + return true; } public void SetTo(MetaDictionary other) From ad0c64d4ac9e3b2d8ba89b7920b2f96a72480709 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 11:37:39 +0200 Subject: [PATCH 1760/2451] Change Eqp hook to not need eqp files anymore. --- Penumbra/Collections/Cache/EqpCache.cs | 12 ++++++++++++ Penumbra/Collections/Cache/MetaCache.cs | 8 -------- Penumbra/Collections/ModCollection.Cache.Access.cs | 7 ------- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 7 +++---- Penumbra/Interop/PathResolving/MetaState.cs | 5 ++--- Penumbra/UI/ConfigWindow.cs | 3 ++- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 599ae588..8dde9aba 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,3 +1,4 @@ +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; @@ -28,6 +29,17 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); } + public unsafe EqpEntry GetValues(CharacterArmor* armor) + => GetSingleValue(armor[0].Set, EquipSlot.Head) + | GetSingleValue(armor[1].Set, EquipSlot.Body) + | GetSingleValue(armor[2].Set, EquipSlot.Hands) + | GetSingleValue(armor[3].Set, EquipSlot.Legs) + | GetSingleValue(armor[4].Set, EquipSlot.Feet); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) + => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(manager, id) & Eqp.Mask(slot); + public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index bc6ef34d..45a85d0f 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,3 @@ -using System.IO.Pipes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -146,9 +145,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public void SetImcFiles(bool fromFullCompute) => Imc.SetFiles(fromFullCompute); - public MetaList.MetaReverter TemporarilySetEqpFile() - => Eqp.TemporarilySetFile(); - public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); @@ -161,10 +157,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetEstFile(EstType type) => Est.TemporarilySetFiles(type); - public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => GlobalEqp.Apply(baseEntry, armor); - - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 484d4dd2..3e3386ea 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -100,10 +100,6 @@ public partial class ModCollection return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetEqpFile() - ?? utility.TemporarilyResetResource(MetaIndex.Eqp); - public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetGmpFile() ?? utility.TemporarilyResetResource(MetaIndex.Gmp); @@ -115,7 +111,4 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); - - public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 6663c211..448605c1 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -19,11 +19,10 @@ public unsafe class EqpHook : FastHook private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) { - if (_metaState.EqpCollection.Valid) + if (_metaState.EqpCollection is { Valid: true, ModCollection.MetaCache: { } cache }) { - using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); - Task.Result.Original(utility, flags, armor); - *flags = _metaState.EqpCollection.ModCollection.ApplyGlobalEqp(*flags, armor); + *flags = cache.Eqp.GetValues(armor); + *flags = cache.GlobalEqp.Apply(*flags, armor); } else { diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6fa5c263..de7912e0 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -47,6 +47,8 @@ public sealed unsafe class MetaState : IDisposable public ResolveData CustomizeChangeCollection = ResolveData.Invalid; public ResolveData EqpCollection = ResolveData.Invalid; + public ResolveData GmpCollection = ResolveData.Invalid; + public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; @@ -93,9 +95,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveEqpData(ModCollection collection) - => collection.TemporarilySetEqpFile(_characterUtility); - public MetaList.MetaReverter ResolveGmpData(ModCollection collection) => collection.TemporarilySetGmpFile(_characterUtility); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 9ae11fc3..0ae16f6d 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Services; using Penumbra.UI.Classes; @@ -144,7 +145,7 @@ public sealed class ConfigWindow : Window using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); ImGui.NewLine(); - ImGuiUtil.TextWrapped(text); + ImUtf8.TextWrapped(text); color.Pop(); ImGui.NewLine(); From 30b32fdcd21e82d9f9e1176793f6b01082a79528 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 12:09:41 +0200 Subject: [PATCH 1761/2451] Fix EQDP bug. --- Penumbra/Collections/Cache/EqpCache.cs | 2 +- Penumbra/Meta/Files/EqpGmpFile.cs | 12 ++---------- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 8dde9aba..32c4c0ae 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -38,7 +38,7 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) - => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(manager, id) & Eqp.Mask(slot); + => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot); public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 17541c4f..a7470b75 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -104,15 +104,11 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } } -public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedEqpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, false), IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = CharacterUtility.ReverseIndices[(int)MetaIndex.Eqp]; - public ExpandedEqpFile(MetaFileManager manager) - : base(manager, false) - { } - public EqpEntry this[PrimaryId idx] { get => (EqpEntry)GetInternal(idx); @@ -147,15 +143,11 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable => GetEnumerator(); } -public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedGmpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, true), IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = CharacterUtility.ReverseIndices[(int)MetaIndex.Gmp]; - public ExpandedGmpFile(MetaFileManager manager) - : base(manager, true) - { } - public GmpEntry this[PrimaryId idx] { get => new() { Value = GetInternal(idx) }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index b1ac93f1..970b70cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -112,7 +112,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil ImGui.SameLine(); if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel)) { - entry = entry with { Material = newModel }; + entry = entry with { Model = newModel }; changes = true; } From a61a96f1ef5bcc9e0c96210595c07a52c0ac8115 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 14:04:16 +0200 Subject: [PATCH 1762/2451] Make GmpEntry readonly. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 0a2e2650..cf1ff07e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab +Subproject commit cf1ff07e900e2f93ab628a1fa535fc2b103794a5 From a7b90639c6deb2859adeac608521bd664ac2cbca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 14:46:42 +0200 Subject: [PATCH 1763/2451] Some fixes. --- Penumbra/Collections/Cache/EqdpCache.cs | 10 ++++++---- Penumbra/Collections/Cache/EstCache.cs | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index f3475c7e..7139bb72 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,4 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -28,7 +27,10 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) } public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { + foreach (var t in CharacterUtilityData.EqdpIndices) + Manager.SetFile(null, t); + } protected override void IncorporateChangesInternal() { @@ -89,8 +91,8 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) var mask = Eqdp.Mask(identifier.Slot); if ((origEntry & mask) == entry) return false; - - file[identifier.SetId] = (entry & ~mask) | origEntry; + + file[identifier.SetId] = (origEntry & ~mask) | entry; return true; } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 412dd322..8ee530cc 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -55,7 +55,12 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) } public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { + Manager.SetFile(null, MetaIndex.FaceEst); + Manager.SetFile(null, MetaIndex.HairEst); + Manager.SetFile(null, MetaIndex.BodyEst); + Manager.SetFile(null, MetaIndex.HeadEst); + } protected override void IncorporateChangesInternal() { From c53f29c257003bb8131441cc5bcf7086d808ad73 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 15:19:48 +0200 Subject: [PATCH 1764/2451] Fix unnecessary EST file creations. --- Penumbra/Collections/Cache/EstCache.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 8ee530cc..845ff128 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -74,7 +74,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) public EstEntry GetEstEntry(EstIdentifier identifier) { - var file = GetFile(identifier); + var file = GetCurrentFile(identifier); return file != null ? file[identifier.GenderRace, identifier.SetId] : EstFile.GetDefault(Manager, identifier); @@ -124,9 +124,19 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) Clear(); } + private EstFile? GetCurrentFile(EstIdentifier identifier) + => identifier.Slot switch + { + EstType.Hair => _estHairFile, + EstType.Face => _estFaceFile, + EstType.Body => _estBodyFile, + EstType.Head => _estHeadFile, + _ => null, + }; + private EstFile? GetFile(EstIdentifier identifier) { - if (Manager.CharacterUtility.Ready) + if (!Manager.CharacterUtility.Ready) return null; return identifier.Slot switch From 943207cae8923720fc8dbe56d763ddc3d1034bd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 15:55:51 +0200 Subject: [PATCH 1765/2451] Make GMP independent of file, cleanup unused functions. --- Penumbra/Collections/Cache/EqdpCache.cs | 4 +- Penumbra/Collections/Cache/EqpCache.cs | 59 ++--------------- Penumbra/Collections/Cache/EstCache.cs | 4 +- Penumbra/Collections/Cache/GmpCache.cs | 59 ++--------------- Penumbra/Collections/Cache/IMetaCache.cs | 2 - Penumbra/Collections/Cache/ImcCache.cs | 4 +- Penumbra/Collections/Cache/MetaCache.cs | 3 - Penumbra/Collections/Cache/RspCache.cs | 5 +- .../Collections/ModCollection.Cache.Access.cs | 4 -- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 64 +++++++++++++++++++ Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 24 +++---- Penumbra/Interop/PathResolving/MetaState.cs | 3 - Penumbra/Interop/Services/MetaList.cs | 23 ++----- Penumbra/Meta/Files/EqpGmpFile.cs | 8 +-- 14 files changed, 108 insertions(+), 158 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/GmpHook.cs diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 7139bb72..c63403ae 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -26,7 +26,7 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) Manager.SetFile(_eqdpFiles[i], index); } - public override void ResetFiles() + public void ResetFiles() { foreach (var t in CharacterUtilityData.EqdpIndices) Manager.SetFile(null, t); @@ -62,7 +62,7 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) return Manager.TemporarilySetFile(_eqdpFiles[i], idx); } - public override void Reset() + public void Reset() { foreach (var file in _eqdpFiles.OfType()) { diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 32c4c0ae..b1e03943 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,7 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -10,24 +8,11 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedEqpFile? _eqpFile; - public override void SetFiles() - => Manager.SetFile(_eqpFile, MetaIndex.Eqp); - - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); - } + { } public unsafe EqpEntry GetValues(CharacterArmor* armor) => GetSingleValue(armor[0].Set, EquipSlot.Head) @@ -40,29 +25,14 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot); - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); - - public override void Reset() - { - if (_eqpFile == null) - return; - - _eqpFile.Reset(Keys.Select(identifier => identifier.SetId)); - Clear(); - } + public void Reset() + => Clear(); protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(EqpIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, ExpandedEqpFile.GetDefault(Manager, identifier.SetId)); - } + { } public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) { @@ -76,20 +46,5 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) } protected override void Dispose(bool _) - { - _eqpFile?.Dispose(); - _eqpFile = null; - Clear(); - } - - private ExpandedEqpFile? GetFile() - { - if (_eqpFile != null) - return _eqpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _eqpFile = new ExpandedEqpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 845ff128..6a9fa909 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -54,7 +54,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) return Manager.TemporarilySetFile(file, idx); } - public override void ResetFiles() + public void ResetFiles() { Manager.SetFile(null, MetaIndex.FaceEst); Manager.SetFile(null, MetaIndex.HairEst); @@ -80,7 +80,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) : EstFile.GetDefault(Manager, identifier); } - public override void Reset() + public void Reset() { _estFaceFile?.Reset(); _estHairFile?.Reset(); diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 1475ffd5..0beb51d8 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,6 +1,4 @@ using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -9,48 +7,20 @@ namespace Penumbra.Collections.Cache; public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedGmpFile? _gmpFile; - public override void SetFiles() - => Manager.SetFile(_gmpFile, MetaIndex.Gmp); - - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Gmp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; + { } - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed GMP manipulations."); - } - - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); - - public override void Reset() - { - if (_gmpFile == null) - return; - - _gmpFile.Reset(Keys.Select(identifier => identifier.SetId)); - Clear(); - } + public void Reset() + => Clear(); protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(GmpIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, ExpandedGmpFile.GetDefault(Manager, identifier.SetId)); - } + { } public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) { @@ -63,20 +33,5 @@ public sealed class GmpCache(MetaFileManager manager, ModCollection collection) } protected override void Dispose(bool _) - { - _gmpFile?.Dispose(); - _gmpFile = null; - Clear(); - } - - private ExpandedGmpFile? GetFile() - { - if (_gmpFile != null) - return _gmpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _gmpFile = new ExpandedGmpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index eacbdcc2..dd218b48 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -27,8 +27,6 @@ public abstract class MetaCacheBase } public abstract void SetFiles(); - public abstract void Reset(); - public abstract void ResetFiles(); public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 3d05e793..a9daf795 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -38,7 +38,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); } - public override void ResetFiles() + public void ResetFiles() { foreach (var (path, _) in _imcFiles) Collection._cache!.ForceFile(path, FullPath.Empty); @@ -56,7 +56,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) } - public override void Reset() + public void Reset() { foreach (var (path, (file, set)) in _imcFiles) { diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 45a85d0f..c8a116eb 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -148,9 +148,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); - public MetaList.MetaReverter TemporarilySetGmpFile() - => Gmp.TemporarilySetFile(); - public MetaList.MetaReverter TemporarilySetCmpFile() => Rsp.TemporarilySetFile(); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 3889d6f1..8a5fe97d 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -13,9 +13,6 @@ public sealed class RspCache(MetaFileManager manager, ModCollection collection) public override void SetFiles() => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.HumanCmp); - protected override void IncorporateChangesInternal() { if (GetFile() is not { } file) @@ -30,7 +27,7 @@ public sealed class RspCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); - public override void Reset() + public void Reset() { if (_cmpFile == null) return; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3e3386ea..8701e3bb 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -100,10 +100,6 @@ public partial class ModCollection return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetGmpFile() - ?? utility.TemporarilyResetResource(MetaIndex.Gmp); - public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs new file mode 100644 index 00000000..60966fb7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -0,0 +1,64 @@ +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class GmpHook : FastHook +{ + public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + + private readonly MetaState _metaState; + + private static readonly Finalizer StablePointer = new(); + + public GmpHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true); + } + + /// + /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . + /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, + /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. + /// + private nint Detour(nint gmpResource, uint dividedHeadId) + { + nint ret; + if (_metaState.GmpCollection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Gmp.TryGetValue(new GmpIdentifier(_metaState.UndividedGmpId), out var entry)) + { + if (entry.Entry.Enabled) + { + *StablePointer.Pointer = entry.Entry.Value; + // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. + // We then go backwards from our pointer because this gets added by the calling functions. + ret = (nint)(StablePointer.Pointer - (_metaState.UndividedGmpId.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); + } + else + { + ret = nint.Zero; + } + } + else + { + ret = Task.Result.Original(gmpResource, dividedHeadId); + } + + Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + return ret; + } + + /// Allocate and clean up our single stable ulong pointer. + private class Finalizer + { + public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); + + ~Finalizer() + { + Marshal.FreeHGlobal((nint)Pointer); + } + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index e451f118..a3e56d7f 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -1,10 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + /// /// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, /// but it only applies a changed gmp file after a redraw for some reason. @@ -26,10 +27,11 @@ public sealed unsafe class SetupVisor : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) { - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var gmp = _metaState.ResolveGmpData(collection.ModCollection); - var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); - Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.UndividedGmpId = modelId; + var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); + Penumbra.Log.Information($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + _metaState.GmpCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index de7912e0..c8ebe18f 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -95,9 +95,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveGmpData(ModCollection collection) - => collection.TemporarilySetGmpFile(_characterUtility); - public MetaList.MetaReverter ResolveRspData(ModCollection collection) => collection.TemporarilySetCmpFile(_characterUtility); diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index e956040b..dc472b8e 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -102,30 +102,19 @@ public unsafe class MetaList : IDisposable ResetResourceInternal(); } - public sealed class MetaReverter : IDisposable + public sealed class MetaReverter(MetaList metaList, nint data, int length) : IDisposable { public static readonly MetaReverter Disabled = new(null!) { Disposed = true }; - public readonly MetaList MetaList; - public readonly nint Data; - public readonly int Length; + public readonly MetaList MetaList = metaList; + public readonly nint Data = data; + public readonly int Length = length; public readonly bool Resetter; public bool Disposed; - public MetaReverter(MetaList metaList, nint data, int length) - { - MetaList = metaList; - Data = data; - Length = length; - } - public MetaReverter(MetaList metaList) - { - MetaList = metaList; - Data = nint.Zero; - Length = 0; - Resetter = true; - } + : this(metaList, nint.Zero, 0) + => Resetter = true; public void Dispose() { diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index a7470b75..c47c84ef 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -15,10 +15,10 @@ namespace Penumbra.Meta.Files; /// public unsafe class ExpandedEqpGmpBase : MetaBaseFile { - protected const int BlockSize = 160; - protected const int NumBlocks = 64; - protected const int EntrySize = 8; - protected const int MaxSize = BlockSize * NumBlocks * EntrySize; + public const int BlockSize = 160; + public const int NumBlocks = 64; + public const int EntrySize = 8; + public const int MaxSize = BlockSize * NumBlocks * EntrySize; public const int Count = BlockSize * NumBlocks; From ebef4ff650c19aee26306135ab125a451210bab9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 16:17:44 +0200 Subject: [PATCH 1766/2451] No EST files anymore. --- Penumbra/Collections/Cache/EqpCache.cs | 11 -- Penumbra/Collections/Cache/EstCache.cs | 136 ++---------------- Penumbra/Collections/Cache/GmpCache.cs | 11 -- Penumbra/Collections/Cache/MetaCache.cs | 6 - .../Collections/ModCollection.Cache.Access.cs | 4 - Penumbra/Interop/Hooks/Meta/EstHook.cs | 49 +++++++ .../Hooks/Resources/ResolvePathHooksBase.cs | 29 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 5 +- 8 files changed, 68 insertions(+), 183 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EstHook.cs diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index b1e03943..7ba0c489 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -34,17 +34,6 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(EqpIdentifier identifier) { } - public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) - { - var origEntry = file[identifier.SetId]; - var mask = Eqp.Mask(identifier.Slot); - if ((origEntry & mask) == entry) - return false; - - file[identifier.SetId] = (origEntry & ~mask) | entry; - return true; - } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 6a9fa909..ff94324e 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,5 +1,3 @@ -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -8,144 +6,26 @@ namespace Penumbra.Collections.Cache; public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private EstFile? _estFaceFile; - private EstFile? _estHairFile; - private EstFile? _estBodyFile; - private EstFile? _estHeadFile; - public override void SetFiles() - { - Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - Manager.SetFile(_estHairFile, MetaIndex.HairEst); - Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); - } - - public void SetFile(MetaIndex index) - { - switch (index) - { - case MetaIndex.FaceEst: - Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - break; - case MetaIndex.HairEst: - Manager.SetFile(_estHairFile, MetaIndex.HairEst); - break; - case MetaIndex.BodyEst: - Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - break; - case MetaIndex.HeadEst: - Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); - break; - } - } - - public MetaList.MetaReverter TemporarilySetFiles(EstType type) - { - var (file, idx) = type switch - { - EstType.Face => (_estFaceFile, MetaIndex.FaceEst), - EstType.Hair => (_estHairFile, MetaIndex.HairEst), - EstType.Body => (_estBodyFile, MetaIndex.BodyEst), - EstType.Head => (_estHeadFile, MetaIndex.HeadEst), - _ => (null, 0), - }; - - return Manager.TemporarilySetFile(file, idx); - } - - public void ResetFiles() - { - Manager.SetFile(null, MetaIndex.FaceEst); - Manager.SetFile(null, MetaIndex.HairEst); - Manager.SetFile(null, MetaIndex.BodyEst); - Manager.SetFile(null, MetaIndex.HeadEst); - } + { } protected override void IncorporateChangesInternal() - { - if (!Manager.CharacterUtility.Ready) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(GetFile(identifier)!, identifier, entry); - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations."); - } + { } public EstEntry GetEstEntry(EstIdentifier identifier) - { - var file = GetCurrentFile(identifier); - return file != null - ? file[identifier.GenderRace, identifier.SetId] + => TryGetValue(identifier, out var entry) + ? entry.Entry : EstFile.GetDefault(Manager, identifier); - } public void Reset() - { - _estFaceFile?.Reset(); - _estHairFile?.Reset(); - _estBodyFile?.Reset(); - _estHeadFile?.Reset(); - Clear(); - } + => Clear(); protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) - { - if (GetFile(identifier) is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(EstIdentifier identifier) - { - if (GetFile(identifier) is { } file) - Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId)); - } - - public static bool Apply(EstFile file, EstIdentifier identifier, EstEntry entry) - => file.SetEntry(identifier.GenderRace, identifier.SetId, entry) switch - { - EstFile.EstEntryChange.Unchanged => false, - EstFile.EstEntryChange.Changed => true, - EstFile.EstEntryChange.Added => true, - EstFile.EstEntryChange.Removed => true, - _ => false, - }; + { } protected override void Dispose(bool _) - { - _estFaceFile?.Dispose(); - _estHairFile?.Dispose(); - _estBodyFile?.Dispose(); - _estHeadFile?.Dispose(); - _estFaceFile = null; - _estHairFile = null; - _estBodyFile = null; - _estHeadFile = null; - Clear(); - } - - private EstFile? GetCurrentFile(EstIdentifier identifier) - => identifier.Slot switch - { - EstType.Hair => _estHairFile, - EstType.Face => _estFaceFile, - EstType.Body => _estBodyFile, - EstType.Head => _estHeadFile, - _ => null, - }; - - private EstFile? GetFile(EstIdentifier identifier) - { - if (!Manager.CharacterUtility.Ready) - return null; - - return identifier.Slot switch - { - EstType.Hair => _estHairFile ??= new EstFile(Manager, EstType.Hair), - EstType.Face => _estFaceFile ??= new EstFile(Manager, EstType.Face), - EstType.Body => _estBodyFile ??= new EstFile(Manager, EstType.Body), - EstType.Head => _estHeadFile ??= new EstFile(Manager, EstType.Head), - _ => null, - }; - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 0beb51d8..541b424d 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,6 +1,5 @@ using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; @@ -22,16 +21,6 @@ public sealed class GmpCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(GmpIdentifier identifier) { } - public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) - { - var origEntry = file[identifier.SetId]; - if (entry == origEntry) - return false; - - file[identifier.SetId] = entry; - return true; - } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index c8a116eb..014c7552 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -121,10 +121,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) switch (metaIndex) { case MetaIndex.Eqp: - Eqp.SetFiles(); break; case MetaIndex.Gmp: - Gmp.SetFiles(); break; case MetaIndex.HumanCmp: Rsp.SetFiles(); @@ -133,7 +131,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) case MetaIndex.HairEst: case MetaIndex.HeadEst: case MetaIndex.BodyEst: - Est.SetFile(metaIndex); break; default: Eqdp.SetFile(metaIndex); @@ -151,9 +148,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetCmpFile() => Rsp.TemporarilySetFile(); - public MetaList.MetaReverter TemporarilySetEstFile(EstType type) - => Est.TemporarilySetFiles(type); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8701e3bb..dba971c6 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -103,8 +103,4 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); - - public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) - => _cache?.Meta.TemporarilySetEstFile(type) - ?? utility.TemporarilyResetResource((MetaIndex)type); } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs new file mode 100644 index 00000000..3fab1434 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -0,0 +1,49 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public class EstHook : FastHook +{ + public delegate EstEntry Delegate(uint id, int estType, uint genderRace); + + private readonly MetaState _metaState; + + public EstHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, true); + } + + private EstEntry Detour(uint genderRace, int estType, uint id) + { + EstEntry ret; + if (_metaState.EstCollection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) + ret = entry.Entry; + else + ret = Task.Result.Original(genderRace, estType, id); + + Penumbra.Log.Information($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + return ret; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static EstIdentifier Convert(uint genderRace, int estType, uint id) + { + var i = new PrimaryId((ushort)id); + var gr = (GenderRace)genderRace; + var type = estType switch + { + 1 => EstType.Face, + 2 => EstType.Hair, + 3 => EstType.Head, + 4 => EstType.Body, + _ => (EstType)0, + }; + return new EstIdentifier(i, type, gr); + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 9a68160b..6c9c1b7d 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -158,26 +158,26 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) @@ -206,19 +206,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(drawObject, pathBuffer); } - private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) - { - data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - if (_parent.InInternalResolve) - return DisposableContainer.Empty; - - return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate { diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index c8ebe18f..8fa09232 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -18,7 +18,7 @@ namespace Penumbra.Interop.PathResolving; // GetSlotEqpData seems to be the only function using the EQP table. // It is only called by CheckSlotsForUnload (called by UpdateModels), // SetupModelAttributes (called by UpdateModels and OnModelLoadComplete) -// and a unnamed function called by UpdateRender. +// and an unnamed function called by UpdateRender. // It seems to be enough to change the EQP entries for UpdateModels. // GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables. @@ -35,7 +35,7 @@ namespace Penumbra.Interop.PathResolving; // they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. -// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. +// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. public sealed unsafe class MetaState : IDisposable { private readonly Configuration _config; @@ -48,6 +48,7 @@ public sealed unsafe class MetaState : IDisposable public ResolveData CustomizeChangeCollection = ResolveData.Invalid; public ResolveData EqpCollection = ResolveData.Invalid; public ResolveData GmpCollection = ResolveData.Invalid; + public ResolveData EstCollection = ResolveData.Invalid; public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; From 9ecc4ab46d1b04e1bea0a6816d3e8ab06b862812 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 20:47:08 +0200 Subject: [PATCH 1767/2451] Remove CMP file. --- Penumbra/Collections/Cache/MetaCache.cs | 3 - Penumbra/Collections/Cache/RspCache.cs | 64 ++--------------- .../Collections/ModCollection.Cache.Access.cs | 4 -- .../Interop/Hooks/Meta/CalculateHeight.cs | 7 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 3 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 66 ++++++++++++++++++ Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 68 +++++++++++++++++++ .../Interop/Hooks/Meta/RspSetupCharacter.cs | 5 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 68 +++++++++++++++++++ Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 16 +++-- Penumbra/Interop/PathResolving/MetaState.cs | 9 ++- Penumbra/Meta/Files/CmpFile.cs | 8 +++ .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 2 +- 15 files changed, 244 insertions(+), 83 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/RspBustHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/RspHeightHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/RspTailHook.cs diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 014c7552..e6083351 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -145,9 +145,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); - public MetaList.MetaReverter TemporarilySetCmpFile() - => Rsp.TemporarilySetFile(); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 8a5fe97d..8a983c6c 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -1,78 +1,26 @@ -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private CmpFile? _cmpFile; - public override void SetFiles() - => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed RSP manipulations."); - } - - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); + { } public void Reset() - { - if (_cmpFile == null) - return; - - _cmpFile.Reset(Keys.Select(identifier => (identifier.SubRace, identifier.Attribute))); - Clear(); - } + => Clear(); protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(RspIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, CmpFile.GetDefault(Manager, identifier.SubRace, identifier.Attribute)); - } + { } - public static bool Apply(CmpFile file, RspIdentifier identifier, RspEntry entry) - { - var value = file[identifier.SubRace, identifier.Attribute]; - if (value == entry) - return false; - - file[identifier.SubRace, identifier.Attribute] = entry; - return true; - } protected override void Dispose(bool _) - { - _cmpFile?.Dispose(); - _cmpFile = null; - Clear(); - } - - private CmpFile? GetFile() - { - if (_cmpFile != null) - return _cmpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _cmpFile = new CmpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index dba971c6..d93a0f53 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -99,8 +99,4 @@ public partial class ModCollection var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - - public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetCmpFile() - ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 2fd87f6e..5a207491 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.Interop.PathResolving; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -22,10 +23,10 @@ public sealed unsafe class CalculateHeight : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private ulong Detour(Character* character) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); - using var cmp = _metaState.ResolveRspData(collection.ModCollection); - var ret = Task.Result.Original.Invoke(character); + _metaState.RspCollection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var ret = Task.Result.Original.Invoke(character); Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + _metaState.RspCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 2f717491..4e0a5744 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -25,12 +25,13 @@ public sealed unsafe class ChangeCustomize : FastHook private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) { _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection); + _metaState.RspCollection = _metaState.CustomizeChangeCollection; using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); var ret = Task.Result.Original.Invoke(human, data, skipEquipment); Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); _metaState.CustomizeChangeCollection = ResolveData.Invalid; + _metaState.RspCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 3fab1434..34935edb 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -27,7 +27,7 @@ public class EstHook : FastHook else ret = Task.Result.Original(genderRace, estType, id); - Penumbra.Log.Information($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + Penumbra.Log.Excessive($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs new file mode 100644 index 00000000..fc1d743a --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -0,0 +1,66 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class RspBustHook : FastHook +{ + public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, + byte bustSize); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspBustHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, true); + } + + private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + { + if (gender == 0) + { + storage[0] = 1f; + storage[1] = 1f; + storage[2] = 1f; + return storage; + } + + var ret = storage; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var bustScale = bustSize / 100f; + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); + storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); + storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); + storage[2] = GetValue(2, RspAttribute.BustMinZ, RspAttribute.BustMaxZ); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + float GetValue(int dimension, RspAttribute min, RspAttribute max) + { + var minValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, min), out var minEntry) + ? minEntry.Entry.Value + : (ptr + dimension)->Value; + var maxValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, max), out var maxEntry) + ? maxEntry.Entry.Value + : (ptr + 3 + dimension)->Value; + return (maxValue - minValue) * bustScale + minValue; + } + } + else + { + ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + } + + Penumbra.Log.Information( + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs new file mode 100644 index 00000000..883f5fc6 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -0,0 +1,68 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public class RspHeightHook : FastHook +{ + public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, true); + } + + private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + { + float scale; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var (minIdent, maxIdent) = gender == 0 + ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) + : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); + + float minEntry, maxEntry; + if (cache.Rsp.TryGetValue(minIdent, out var min)) + { + minEntry = min.Entry.Value; + maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max) + ? max.Entry.Value + : CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value; + } + else + { + var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute); + if (cache.Rsp.TryGetValue(maxIdent, out var max)) + { + minEntry = ptr->Value; + maxEntry = max.Entry.Value; + } + else + { + minEntry = ptr[0].Value; + maxEntry = ptr[1].Value; + } + } + + scale = (maxEntry - minEntry) * height / 100f + minEntry; + } + else + { + scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + } + + Penumbra.Log.Excessive( + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + return scale; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 8f8f1d78..831c99bb 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -30,8 +31,8 @@ public sealed unsafe class RspSetupCharacter : FastHook +{ + public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspTailHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, true); + } + + private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) + { + float scale; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var (minIdent, maxIdent) = gender == 0 + ? (new RspIdentifier(clan, RspAttribute.MaleMinTail), new RspIdentifier(clan, RspAttribute.MaleMaxTail)) + : (new RspIdentifier(clan, RspAttribute.FemaleMinTail), new RspIdentifier(clan, RspAttribute.FemaleMaxTail)); + + float minEntry, maxEntry; + if (cache.Rsp.TryGetValue(minIdent, out var min)) + { + minEntry = min.Entry.Value; + maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max) + ? max.Entry.Value + : CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value; + } + else + { + var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute); + if (cache.Rsp.TryGetValue(maxIdent, out var max)) + { + minEntry = ptr->Value; + maxEntry = max.Entry.Value; + } + else + { + minEntry = ptr[0].Value; + maxEntry = ptr[1].Value; + } + } + + scale = (maxEntry - minEntry) * tailLength / 100f + minEntry; + } + else + { + scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, tailLength); + } + + Penumbra.Log.Excessive( + $"[GetRspTail] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {tailLength}, returned {scale}."); + return scale; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index a3e56d7f..8479968f 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -30,7 +30,7 @@ public sealed unsafe class SetupVisor : FastHook _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); _metaState.UndividedGmpId = modelId; var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); - Penumbra.Log.Information($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); _metaState.GmpCollection = ResolveData.Invalid; return ret; } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 6c9c1b7d..17cfa3f6 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -159,25 +159,33 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 8fa09232..3da94ce3 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable public ResolveData EqpCollection = ResolveData.Invalid; public ResolveData GmpCollection = ResolveData.Invalid; public ResolveData EstCollection = ResolveData.Invalid; + public ResolveData RspCollection = ResolveData.Invalid; public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; @@ -96,9 +97,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveRspData(ModCollection collection) - => collection.TemporarilySetCmpFile(_characterUtility); - public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); @@ -132,9 +130,9 @@ public sealed unsafe class MetaState : IDisposable var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); - var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); + RspCollection = _lastCreatedCollection; _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. - _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); + _characterBaseCreateMetaChanges = new DisposableContainer(decal); } private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject) @@ -144,6 +142,7 @@ public sealed unsafe class MetaState : IDisposable if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, (nint)drawObject); + RspCollection = ResolveData.Invalid; _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 96cda496..8ca7cb80 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -46,6 +46,14 @@ public sealed unsafe class CmpFile : MetaBaseFile return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } + public static RspEntry* GetDefaults(MetaFileManager manager, SubRace subRace, RspAttribute attribute) + { + { + var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; + return (RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + } + } + private static int ToRspIndex(SubRace subRace) => subRace switch { diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 2b7904ce..be02e321 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -85,7 +85,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { using var dis = ImRaii.Disabled(disabled); ImGui.TableNextColumn(); - var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, defaultEntry.Value, entry.Value, out var newValue, + var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, entry.Value, defaultEntry.Value, out var newValue, RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled); if (ret) entry = new RspEntry(newValue); From 600fd2ecd36faa72292a7a8e2e8ce8982f6c708e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Jun 2024 01:02:42 +0200 Subject: [PATCH 1768/2451] Get rid off EQDP files --- Penumbra/Collections/Cache/EqdpCache.cs | 115 +++++------------- Penumbra/Collections/Cache/MetaCache.cs | 38 +----- .../Collections/ModCollection.Cache.Access.cs | 36 ------ .../Interop/Hooks/Meta/CalculateHeight.cs | 5 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 23 ++-- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 33 +++++ Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 32 +++++ Penumbra/Interop/Hooks/Meta/EqpHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 5 +- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 23 ++-- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 6 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 9 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 3 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 5 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 6 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 9 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 45 ++++--- Penumbra/Interop/PathResolving/MetaState.cs | 33 ++--- Penumbra/Interop/Services/MetaList.cs | 2 +- Penumbra/Penumbra.cs | 2 - 23 files changed, 192 insertions(+), 246 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index c63403ae..5bfe2dbf 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,8 +1,5 @@ -using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -11,108 +8,60 @@ namespace Penumbra.Collections.Cache; public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar + private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), EqdpEntry> _fullEntries = []; public override void SetFiles() - { - for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) - Manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); - } + { } - public void SetFile(MetaIndex index) - { - var i = CharacterUtilityData.EqdpIndices.IndexOf(index); - if (i != -1) - Manager.SetFile(_eqdpFiles[i], index); - } - - public void ResetFiles() - { - foreach (var t in CharacterUtilityData.EqdpIndices) - Manager.SetFile(null, t); - } + public bool TryGetFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, out EqdpEntry entry) + => _fullEntries.TryGetValue((id, genderRace, accessory), out entry); protected override void IncorporateChangesInternal() - { - foreach (var (identifier, (_, entry)) in this) - Apply(GetFile(identifier)!, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQDP manipulations."); - } - - public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) - => _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar - - public MetaList.MetaReverter? TemporarilySetFile(GenderRace genderRace, bool accessory) - { - var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - if (idx < 0) - { - Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); - return null; - } - - var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); - if (i < 0) - { - Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); - return null; - } - - return Manager.TemporarilySetFile(_eqdpFiles[i], idx); - } + { } public void Reset() { - foreach (var file in _eqdpFiles.OfType()) - { - var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(Keys.Where(m => m.FileIndex() == relevant).Select(m => m.SetId)); - } - Clear(); + _fullEntries.Clear(); } protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - if (GetFile(identifier) is { } file) - Apply(file, identifier, entry); + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); + if (!_fullEntries.TryGetValue(tuple, out var currentEntry)) + currentEntry = ExpandedEqdpFile.GetDefault(Manager, identifier); + + _fullEntries[tuple] = (currentEntry & ~mask) | (entry & mask); } protected override void RevertModInternal(EqdpIdentifier identifier) { - if (GetFile(identifier) is { } file) - Apply(file, identifier, ExpandedEqdpFile.GetDefault(Manager, identifier)); - } + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); - public static bool Apply(ExpandedEqdpFile file, EqdpIdentifier identifier, EqdpEntry entry) - { - var origEntry = file[identifier.SetId]; - var mask = Eqdp.Mask(identifier.Slot); - if ((origEntry & mask) == entry) - return false; - - file[identifier.SetId] = (origEntry & ~mask) | entry; - return true; + if (_fullEntries.TryGetValue(tuple, out var currentEntry)) + { + var def = ExpandedEqdpFile.GetDefault(Manager, identifier); + var newEntry = (currentEntry & ~mask) | (def & mask); + if (currentEntry != newEntry) + { + _fullEntries[tuple] = newEntry; + } + else + { + var slots = tuple.Item3 ? EquipSlotExtensions.AccessorySlots : EquipSlotExtensions.EquipmentSlots; + if (slots.All(s => !ContainsKey(identifier with { Slot = s }))) + _fullEntries.Remove(tuple); + else + _fullEntries[tuple] = newEntry; + } + } } protected override void Dispose(bool _) { - for (var i = 0; i < _eqdpFiles.Length; ++i) - { - _eqdpFiles[i]?.Dispose(); - _eqdpFiles[i] = null; - } - Clear(); - } - - private ExpandedEqdpFile? GetFile(EqdpIdentifier identifier) - { - if (!Manager.CharacterUtility.Ready) - return null; - - var index = Array.IndexOf(CharacterUtilityData.EqdpIndices, identifier.FileIndex()); - return _eqdpFiles[index] ??= new ExpandedEqdpFile(Manager, identifier.GenderRace, identifier.Slot.IsAccessory()); + _fullEntries.Clear(); } } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index e6083351..614a5a2c 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,7 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -115,48 +113,18 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Set a single file. - public void SetFile(MetaIndex metaIndex) - { - switch (metaIndex) - { - case MetaIndex.Eqp: - break; - case MetaIndex.Gmp: - break; - case MetaIndex.HumanCmp: - Rsp.SetFiles(); - break; - case MetaIndex.FaceEst: - case MetaIndex.HairEst: - case MetaIndex.HeadEst: - case MetaIndex.BodyEst: - break; - default: - Eqdp.SetFile(metaIndex); - break; - } - } - /// Set the currently relevant IMC files for the collection cache. public void SetImcFiles(bool fromFullCompute) => Imc.SetFiles(fromFullCompute); - public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => Eqdp.TemporarilySetFile(genderRace, accessory); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) - { - var eqdpFile = Eqdp.EqdpFile(race, accessory); - if (eqdpFile != null) - return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; - - return Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); - } + => Eqdp.TryGetFullEntry(primaryId, race, accessory, out var entry) + ? entry + : Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index d93a0f53..81751128 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,14 +1,9 @@ using OtterGui.Classes; -using Penumbra.GameData.Enums; using Penumbra.Mods; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Collections.Cache; -using Penumbra.Interop.Services; using Penumbra.Mods.Editor; -using Penumbra.GameData.Structs; namespace Penumbra.Collections; @@ -68,35 +63,4 @@ public partial class ModCollection internal SingleArray Conflicts(Mod mod) => _cache?.Conflicts(mod) ?? new SingleArray(); - - public void SetFiles(CharacterUtility utility) - { - if (_cache == null) - { - utility.ResetAll(); - } - else - { - _cache.Meta.SetFiles(); - Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}."); - } - } - - public void SetMetaFile(CharacterUtility utility, MetaIndex idx) - { - if (_cache == null) - utility.ResetResource(idx); - else - _cache.Meta.SetFile(idx); - } - - // Used for short periods of changed files. - public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) - { - if (_cache != null) - return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory); - - var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; - } } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 5a207491..7936b831 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -23,10 +23,11 @@ public sealed unsafe class CalculateHeight : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private ulong Detour(Character* character) { - _metaState.RspCollection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + _metaState.RspCollection.Push(collection); var ret = Task.Result.Original.Invoke(character); Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); - _metaState.RspCollection = ResolveData.Invalid; + _metaState.RspCollection.Pop(); return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 4e0a5744..f589cf4e 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -1,12 +1,12 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Structs; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using Penumbra.GameData; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + public sealed unsafe class ChangeCustomize : FastHook { private readonly CollectionResolver _collectionResolver; @@ -24,14 +24,15 @@ public sealed unsafe class ChangeCustomize : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) { - _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - _metaState.RspCollection = _metaState.CustomizeChangeCollection; + var collection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + _metaState.CustomizeChangeCollection = collection; + _metaState.RspCollection.Push(collection); using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); var ret = Task.Result.Original.Invoke(human, data, skipEquipment); Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); _metaState.CustomizeChangeCollection = ResolveData.Invalid; - _metaState.RspCollection = ResolveData.Invalid; + _metaState.RspCollection.Pop(); return ret; } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs new file mode 100644 index 00000000..475e1eb7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -0,0 +1,33 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqdpAccessoryHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); + + private readonly MetaState _metaState; + + public EqdpAccessoryHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) + { + if (_metaState.EqdpCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, out var newEntry)) + *entry = newEntry; + else + Task.Result.Original(utility, entry, setId, raceCode); + Penumbra.Log.Information( + $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs new file mode 100644 index 00000000..9b911710 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqdpEquipHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); + + private readonly MetaState _metaState; + + public EqdpEquipHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) + { + if (_metaState.EqdpCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, out var newEntry)) + *entry = newEntry; + else + Task.Result.Original(utility, entry, setId, raceCode); + Penumbra.Log.Information( + $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 448605c1..7107e26b 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -19,7 +19,7 @@ public unsafe class EqpHook : FastHook private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) { - if (_metaState.EqpCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (_metaState.EqpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { *flags = cache.Eqp.GetValues(armor); *flags = cache.GlobalEqp.Apply(*flags, armor); diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 34935edb..23931182 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -21,7 +21,7 @@ public class EstHook : FastHook private EstEntry Detour(uint genderRace, int estType, uint id) { EstEntry ret; - if (_metaState.EstCollection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) ret = entry.Entry; else diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index beae6acc..a10b511a 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); } } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 89aaa9b0..30ec2597 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,11 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + public sealed unsafe class GetEqpIndirect2 : FastHook { private readonly CollectionResolver _collectionResolver; @@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect2 : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); - _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 60966fb7..256d8702 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -27,15 +27,15 @@ public unsafe class GmpHook : FastHook private nint Detour(nint gmpResource, uint dividedHeadId) { nint ret; - if (_metaState.GmpCollection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Gmp.TryGetValue(new GmpIdentifier(_metaState.UndividedGmpId), out var entry)) + if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) { if (entry.Entry.Enabled) { *StablePointer.Pointer = entry.Entry.Value; // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (_metaState.UndividedGmpId.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); + ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); } else { diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 10c12594..2c17362d 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -23,10 +23,11 @@ public sealed unsafe class ModelLoadComplete : FastHook } var ret = storage; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 883f5fc6..cf88c34a 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; @@ -24,7 +25,7 @@ public class RspHeightHook : FastHook private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) { float scale; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 831c99bb..58856f52 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -31,8 +31,9 @@ public sealed unsafe class RspSetupCharacter : FastHook private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) { float scale; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 8479968f..82b24dc4 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -27,11 +27,11 @@ public sealed unsafe class SetupVisor : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) { - _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); - _metaState.UndividedGmpId = modelId; + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.GmpCollection.Push((collection, modelId)); var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); - _metaState.GmpCollection = ResolveData.Invalid; + _metaState.GmpCollection.Pop(); return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index b0298ac7..76854bca 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -29,10 +29,11 @@ public sealed unsafe class UpdateModel : FastHook return; Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); - _metaState.EqpCollection = collection; + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); + _metaState.EqdpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); + _metaState.EqdpCollection.Pop(); } } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 17cfa3f6..5941773f 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,11 +1,9 @@ using System.Text.Unicode; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Classes; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Interop.PathResolving; -using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Resources; @@ -149,42 +147,51 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { - var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqdp = slotIndex > 9 || _parent.InInternalResolve - ? DisposableContainer.Empty - : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4); - return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + if (slotIndex < 10) + _parent.MetaState.EqdpCollection.Push(collection); + + var ret = ResolvePath(collection, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + if (slotIndex < 10) + _parent.MetaState.EqdpCollection.Pop(); + + return ret; } private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, + _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 3da94ce3..4bd23cf8 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -45,12 +45,14 @@ public sealed unsafe class MetaState : IDisposable private readonly CharacterUtility _characterUtility; private readonly CreateCharacterBase _createCharacterBase; - public ResolveData CustomizeChangeCollection = ResolveData.Invalid; - public ResolveData EqpCollection = ResolveData.Invalid; - public ResolveData GmpCollection = ResolveData.Invalid; - public ResolveData EstCollection = ResolveData.Invalid; - public ResolveData RspCollection = ResolveData.Invalid; - public PrimaryId UndividedGmpId = 0; + public ResolveData CustomizeChangeCollection = ResolveData.Invalid; + public readonly Stack EqpCollection = []; + public readonly Stack EqdpCollection = []; + public readonly Stack EstCollection = []; + public readonly Stack RspCollection = []; + + public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; + private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; @@ -82,21 +84,6 @@ public sealed unsafe class MetaState : IDisposable return false; } - public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) - => (equipment, accessory) switch - { - (true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[] - { - collection.TemporarilySetEqdpFile(_characterUtility, r, false), - collection.TemporarilySetEqdpFile(_characterUtility, r, true), - })), - (true, false) => new DisposableContainer(race.Dependencies() - .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))), - (false, true) => new DisposableContainer(race.Dependencies() - .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))), - _ => DisposableContainer.Empty, - }; - public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); @@ -130,7 +117,7 @@ public sealed unsafe class MetaState : IDisposable var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); - RspCollection = _lastCreatedCollection; + RspCollection.Push(_lastCreatedCollection); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal); } @@ -142,7 +129,7 @@ public sealed unsafe class MetaState : IDisposable if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, (nint)drawObject); - RspCollection = ResolveData.Invalid; + RspCollection.Pop(); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index dc472b8e..24d3f088 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -87,7 +87,7 @@ public unsafe class MetaList : IDisposable => SetResourceInternal(_defaultResourceData, _defaultResourceSize); private void SetResourceToDefaultCollection() - => _utility.Active.Default.SetMetaFile(_utility, GlobalMetaIndex); + {} public void Dispose() { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3bbfdf65..905b998d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -144,7 +144,6 @@ public class Penumbra : IDalamudPlugin { if (_characterUtility.Ready) { - _collectionManager.Active.Default.SetFiles(_characterUtility); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } @@ -153,7 +152,6 @@ public class Penumbra : IDalamudPlugin { if (_characterUtility.Ready) { - _characterUtility.ResetAll(); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } From 91d9e465ede9850fd7d5d0a457870e068cfd0cc8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Jun 2024 12:30:47 +0200 Subject: [PATCH 1769/2451] Improve eqdp. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqdpCache.cs | 49 ++++++++----------- Penumbra/Collections/Cache/MetaCache.cs | 4 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 9 ++-- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 8 ++- Penumbra/Interop/PathResolving/MetaState.cs | 17 ------- .../Services/ShaderReplacementFixer.cs | 4 +- 7 files changed, 31 insertions(+), 62 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index cf1ff07e..3fbc7045 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit cf1ff07e900e2f93ab628a1fa535fc2b103794a5 +Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1 diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 5bfe2dbf..6047736b 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,20 +1,22 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), EqdpEntry> _fullEntries = []; + private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries = + []; public override void SetFiles() { } - public bool TryGetFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, out EqdpEntry entry) - => _fullEntries.TryGetValue((id, genderRace, accessory), out entry); + public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry) + => _fullEntries.TryGetValue((id, genderRace, accessory), out var pair) + ? (originalEntry & pair.InverseMask) | pair.Entry + : originalEntry; protected override void IncorporateChangesInternal() { } @@ -27,36 +29,27 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); - var mask = Eqdp.Mask(identifier.Slot); - if (!_fullEntries.TryGetValue(tuple, out var currentEntry)) - currentEntry = ExpandedEqdpFile.GetDefault(Manager, identifier); - - _fullEntries[tuple] = (currentEntry & ~mask) | (entry & mask); + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); + var inverseMask = ~mask; + if (_fullEntries.TryGetValue(tuple, out var pair)) + pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask); + else + pair = (entry & mask, inverseMask); + _fullEntries[tuple] = pair; } protected override void RevertModInternal(EqdpIdentifier identifier) { var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); - var mask = Eqdp.Mask(identifier.Slot); - if (_fullEntries.TryGetValue(tuple, out var currentEntry)) - { - var def = ExpandedEqdpFile.GetDefault(Manager, identifier); - var newEntry = (currentEntry & ~mask) | (def & mask); - if (currentEntry != newEntry) - { - _fullEntries[tuple] = newEntry; - } - else - { - var slots = tuple.Item3 ? EquipSlotExtensions.AccessorySlots : EquipSlotExtensions.EquipmentSlots; - if (slots.All(s => !ContainsKey(identifier with { Slot = s }))) - _fullEntries.Remove(tuple); - else - _fullEntries[tuple] = newEntry; - } - } + if (!_fullEntries.Remove(tuple, out var pair)) + return; + + var mask = Eqdp.Mask(identifier.Slot); + var newMask = pair.InverseMask | mask; + if (newMask is not EqdpEntry.FullMask) + _fullEntries[tuple] = (pair.Entry & ~mask, newMask); } protected override void Dispose(bool _) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 614a5a2c..92a445dd 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -122,9 +122,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) - => Eqdp.TryGetFullEntry(primaryId, race, accessory, out var entry) - ? entry - : Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); + => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 475e1eb7..f7390ea3 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -3,7 +3,6 @@ using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; -using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; @@ -21,12 +20,10 @@ public unsafe class EqdpAccessoryHook : FastHook private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) { + Task.Result.Original(utility, entry, setId, raceCode); if (_metaState.EqdpCollection.TryPeek(out var collection) - && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, out var newEntry)) - *entry = newEntry; - else - Task.Result.Original(utility, entry, setId, raceCode); + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, *entry); Penumbra.Log.Information( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 9b911710..9b635b1f 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -20,12 +20,10 @@ public unsafe class EqdpEquipHook : FastHook private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) { + Task.Result.Original(utility, entry, setId, raceCode); if (_metaState.EqdpCollection.TryPeek(out var collection) - && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, out var newEntry)) - *entry = newEntry; - else - Task.Result.Original(utility, entry, setId, raceCode); + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, *entry); Penumbra.Log.Information( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 4bd23cf8..7f820b4e 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -2,13 +2,11 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; -using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using Penumbra.Interop.Hooks.Objects; @@ -87,21 +85,6 @@ public sealed unsafe class MetaState : IDisposable public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); - public static GenderRace GetHumanGenderRace(nint human) - => (GenderRace)((Human*)human)->RaceSexId; - - public static GenderRace GetDrawObjectGenderRace(nint drawObject) - { - var draw = (DrawObject*)drawObject; - if (draw->Object.GetObjectType() != ObjectType.CharacterBase) - return GenderRace.Unknown; - - var c = (CharacterBase*)drawObject; - return c->GetModelType() == CharacterBase.ModelType.Human - ? GetHumanGenderRace(drawObject) - : GenderRace.Unknown; - } - public void Dispose() { _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index 3809ecbd..95e70b45 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -139,9 +139,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // Performance considerations: // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; - // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Function is called each frame for each material on screen, after culling, i.e. up to thousands of times a frame in crowded areas ; // - Swapping path is taken up to hundreds of times a frame. - // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either frame rate or CPU usage, but the swapping path shall still be avoided as much as possible. lock (_skinLock) { try From be729afd4b382e618c91558373e013f34959ba02 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 16:39:10 +0200 Subject: [PATCH 1770/2451] Some cleanup --- Penumbra/Collections/Cache/ImcCache.cs | 2 -- Penumbra/Communication/CreatingCharacterBase.cs | 3 +-- Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Mods/Manager/ModManager.cs | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index a9daf795..c6bb0330 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,5 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using Penumbra.Collections.Manager; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 8a906ca0..51d55868 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Services; @@ -19,7 +18,7 @@ public sealed class CreatingCharacterBase() { public enum Priority { - /// + /// Api = 0, /// diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index f7390ea3..aaaaccd4 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -24,7 +24,7 @@ public unsafe class EqdpAccessoryHook : FastHook if (_metaState.EqdpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, *entry); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 9b635b1f..2711f195 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -24,7 +24,7 @@ public unsafe class EqdpEquipHook : FastHook if (_metaState.EqdpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, *entry); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index 13a5410d..86759460 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -59,7 +59,7 @@ public unsafe class RspBustHook : FastHook ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); } - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 62b54865..42082383 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -261,7 +261,7 @@ public sealed class ModManager : ModStorage, IDisposable /// /// Set the mod base directory. - /// If its not the first time, check if it is the same directory as before. + /// If it's not the first time, check if it is the same directory as before. /// Also checks if the directory is available and tries to create it if it is not. /// private void SetBaseDirectory(string newPath, bool firstTime, out string resultNewDir) From d7a8c9415bb57f8fe13e3cfeed067e6d4a579470 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 23:11:42 +0200 Subject: [PATCH 1771/2451] Use specific counter for Imc. --- Penumbra/Collections/Cache/ImcCache.cs | 2 ++ Penumbra/Collections/ModCollection.cs | 2 ++ Penumbra/Interop/PathResolving/PathDataHandler.cs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index c6bb0330..786463bc 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -68,6 +68,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { + ++Collection.ImcChangeCounter; if (Manager.CharacterUtility.Ready) ApplyFile(identifier, entry); } @@ -102,6 +103,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { + ++Collection.ImcChangeCounter; var path = identifier.GamePath(); if (!_imcFiles.TryGetValue(path, out var pair)) return; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9286d459..eb5ab46a 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -56,6 +56,8 @@ public partial class ModCollection /// public int ChangeCounter { get; private set; } + public uint ImcChangeCounter { get; set; } + /// Increment the number of changes in the effective file list. public int IncrementCounter() => ++ChangeCounter; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 5627e015..a8be97c8 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(ByteString path, ModCollection collection) - => CreateBase(path, collection); + => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] From 03d3c38ad577756d3a4fb20c1bd0417344538727 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 17:52:34 +0200 Subject: [PATCH 1772/2451] Improve Imc Handling. --- Penumbra/Collections/Cache/CollectionCache.cs | 16 +--- .../Cache/CollectionCacheManager.cs | 2 - Penumbra/Collections/Cache/ImcCache.cs | 58 ++++---------- Penumbra/Collections/Cache/MetaCache.cs | 8 +- .../Interop/PathResolving/PathResolver.cs | 50 +++--------- .../Interop/PathResolving/SubfileHelper.cs | 17 ++-- .../Interop/ResourceLoading/ResourceLoader.cs | 69 +++++++++++++++++ Penumbra/Interop/Services/CharacterUtility.cs | 35 +-------- Penumbra/Meta/Files/CmpFile.cs | 2 +- Penumbra/Meta/Files/EqdpFile.cs | 2 +- Penumbra/Meta/Files/EqpGmpFile.cs | 2 +- Penumbra/Meta/Files/EstFile.cs | 2 +- Penumbra/Meta/Files/EvpFile.cs | 6 +- Penumbra/Meta/Files/ImcFile.cs | 30 ++++---- Penumbra/Meta/Files/MetaBaseFile.cs | 77 +++++++++++++++---- Penumbra/Meta/MetaFileManager.cs | 54 +------------ 16 files changed, 197 insertions(+), 233 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index fd801d3b..4755840e 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -36,7 +36,7 @@ public sealed class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -125,12 +125,6 @@ public sealed class CollectionCache : IDisposable return ret; } - public void ForceFile(Utf8GamePath path, FullPath fullPath) - => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath)); - - public void RemovePath(Utf8GamePath path) - => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); - public void ReloadMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges)); @@ -251,9 +245,6 @@ public sealed class CollectionCache : IDisposable if (addMetaChanges) { _collection.IncrementCounter(); - if (mod.TotalManipulations > 0) - AddMetaFiles(false); - _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -408,11 +399,6 @@ public sealed class CollectionCache : IDisposable } - // Add all necessary meta file redirects. - public void AddMetaFiles(bool fromFullCompute) - => Meta.SetImcFiles(fromFullCompute); - - // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index ae424b94..02c9c8a9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -180,8 +180,6 @@ public class CollectionCacheManager : IDisposable foreach (var mod in _modStorage) cache.AddModSync(mod, false); - cache.AddMetaFiles(true); - collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 786463bc..e7eedc04 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,20 +1,22 @@ using Penumbra.GameData.Structs; -using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; +using Penumbra.String; namespace Penumbra.Collections.Cache; public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary)> _imcFiles = []; + private readonly Dictionary)> _imcFiles = []; public override void SetFiles() - => SetFiles(false); + { } - public bool GetFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + public bool HasFile(ByteString path) + => _imcFiles.ContainsKey(path); + + public bool GetFile(ByteString path, [NotNullWhen(true)] out ImcFile? file) { if (!_imcFiles.TryGetValue(path, out var p)) { @@ -26,56 +28,31 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) return true; } - public void SetFiles(bool fromFullCompute) - { - if (fromFullCompute) - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection)); - else - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); - } - - public void ResetFiles() - { - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFile(path, FullPath.Empty); - } - protected override void IncorporateChangesInternal() - { - if (!Manager.CharacterUtility.Ready) - return; - - foreach (var (identifier, (_, entry)) in this) - ApplyFile(identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed IMC manipulations."); - } + { } public void Reset() { - foreach (var (path, (file, set)) in _imcFiles) + foreach (var (_, (file, set)) in _imcFiles) { - Collection._cache!.RemovePath(path); file.Reset(); set.Clear(); } + _imcFiles.Clear(); Clear(); } protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { ++Collection.ImcChangeCounter; - if (Manager.CharacterUtility.Ready) - ApplyFile(identifier, entry); + ApplyFile(identifier, entry); } private void ApplyFile(ImcIdentifier identifier, ImcEntry entry) { - var path = identifier.GamePath(); + var path = identifier.GamePath().Path; try { if (!_imcFiles.TryGetValue(path, out var pair)) @@ -87,8 +64,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) pair.Item2.Add(identifier); _imcFiles[path] = pair; - var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); - Collection._cache!.ForceFile(path, fullPath); } catch (ImcException e) { @@ -104,7 +79,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { ++Collection.ImcChangeCounter; - var path = identifier.GamePath(); + var path = identifier.GamePath().Path; if (!_imcFiles.TryGetValue(path, out var pair)) return; @@ -114,17 +89,12 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) if (pair.Item2.Count == 0) { _imcFiles.Remove(path); - Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty); pair.Item1.Dispose(); return; } var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _); - if (!Apply(pair.Item1, identifier, def)) - return; - - var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); - Collection._cache!.ForceFile(pair.Item1.Path, fullPath); + Apply(pair.Item1, identifier, def); } public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 92a445dd..253f3c7f 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -36,7 +36,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Est.SetFiles(); Gmp.SetFiles(); Rsp.SetFiles(); - Imc.SetFiles(false); + Imc.SetFiles(); } public void Reset() @@ -113,13 +113,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Set the currently relevant IMC files for the collection cache. - public void SetImcFiles(bool fromFullCompute) - => Imc.SetFiles(fromFullCompute); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => Imc.GetFile(path, out file); + => Imc.GetFile(path.Path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index e5c75327..e069e3ea 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,11 +1,8 @@ -using System.Runtime; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.Structs; -using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -27,24 +24,16 @@ public class PathResolver : IDisposable public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { - _performance = performance; - _config = config; - _collectionManager = collectionManager; - _subfileHelper = subfileHelper; - _pathState = pathState; - _metaState = metaState; - _gameState = gameState; - _collectionResolver = collectionResolver; - _loader = loader; - _loader.ResolvePath = ResolvePath; - _loader.FileLoaded += ImcLoadResource; - } - - /// Obtain a temporary or permanent collection by local ID. - public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collection) - { - collection = _collectionManager.Storage.ByLocalId(id); - return collection != ModCollection.Empty; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _subfileHelper = subfileHelper; + _pathState = pathState; + _metaState = metaState; + _gameState = gameState; + _collectionResolver = collectionResolver; + _loader = loader; + _loader.ResolvePath = ResolvePath; } /// Try to resolve the given game path to the replaced path. @@ -120,7 +109,6 @@ public class PathResolver : IDisposable public unsafe void Dispose() { _loader.ResetResolvePath(); - _loader.FileLoaded -= ImcLoadResource; } /// Use the default method of path replacement. @@ -130,24 +118,6 @@ public class PathResolver : IDisposable return (resolved, _collectionManager.Active.Default.ToResolveData()); } - /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) - { - if (resource->FileType != ResourceType.Imc - || !PathDataHandler.Read(additionalData, out var data) - || data.Discriminator != PathDataHandler.Discriminator - || !Utf8GamePath.FromByteString(path, out var gamePath) - || !CollectionByLocalId(data.Collection, out var collection) - || !collection.HasCache - || !collection.GetImcFile(gamePath, out var file)) - return; - - file.Replace(resource); - Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } - /// Resolve a path from the interface collection. private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 793ea20b..b9631cf2 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -70,14 +70,15 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), - ResourceType.Avfx => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), - ResourceType.Tmb => PathDataHandler.CreateTmb(path, resolveData.ModCollection), - _ => resolved, - }; + resolved = type switch + { + ResourceType.Mtrl when nonDefault => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), + ResourceType.Avfx when nonDefault => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), + ResourceType.Tmb when nonDefault => PathDataHandler.CreateTmb(path, resolveData.ModCollection), + ResourceType.Imc when resolveData.ModCollection.MetaCache?.Imc.HasFile(path) ?? false => PathDataHandler.CreateImc(path, + resolveData.ModCollection), + _ => resolved, + }; data = (resolved, resolveData); } diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 7b49beab..fae38907 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,9 @@ +using System.Collections.Frozen; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -10,6 +13,72 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; +public interface IFilePostProcessor : IService +{ + public ResourceType Type { get; } + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); +} + +public sealed class MaterialFilePostProcessor : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.ReadMtrl(additionalData, out var data)) + return; + } +} + +public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!cache.Imc.GetFile(originalGamePath, out var file)) + return; + + file.Replace(resource); + Penumbra.Log.Information( + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } +} + +public unsafe class FilePostProcessService : IRequiredService, IDisposable +{ + private readonly ResourceLoader _resourceLoader; + private readonly FrozenDictionary _processors; + + public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) + { + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.FileLoaded += OnFileLoaded; + } + + public void Dispose() + { + _resourceLoader.FileLoaded -= OnFileLoaded; + } + + private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) + { + if (_processors.TryGetValue(resource->FileType, out var processor)) + processor.PostProcess(resource, path, additionalData); + } +} + public unsafe class ResourceLoader : IDisposable { private readonly ResourceService _resources; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index da04bf90..9459df06 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -53,17 +52,15 @@ public unsafe class CharacterUtility : IDisposable public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; - private readonly IFramework _framework; - public readonly ActiveCollectionData Active; + private readonly IFramework _framework; - public CharacterUtility(IFramework framework, IGameInteropProvider interop, ActiveCollectionData active) + public CharacterUtility(IFramework framework, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lists = Enumerable.Range(0, RelevantIndices.Length) .Select(idx => new MetaList(this, new InternalIndex(idx))) .ToArray(); _framework = framework; - Active = active; LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); LoadDefaultResources(null!); if (!Ready) @@ -121,34 +118,6 @@ public unsafe class CharacterUtility : IDisposable LoadingFinished.Invoke(); } - public void SetResource(MetaIndex resourceIdx, nint data, int length) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - list.SetResource(data, length); - } - - public void ResetResource(MetaIndex resourceIdx) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - list.ResetResource(); - } - - public MetaList.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, nint data, int length) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - return list.TemporarilySetResource(data, length); - } - - public MetaList.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - return list.TemporarilyResetResource(); - } - /// Return all relevant resources to the default resource. public void ResetAll() { diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 8ca7cb80..5028a3de 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -34,7 +34,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public CmpFile(MetaFileManager manager) - : base(manager, MetaIndex.HumanCmp) + : base(manager, manager.MarshalAllocator, MetaIndex.HumanCmp) { AllocateData(DefaultData.Length); Reset(); diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index e46e82e9..34b4f25b 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -87,7 +87,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory) - : base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory)) + : base(manager, manager.MarshalAllocator, CharacterUtilityData.EqdpIdx(raceCode, accessory)) { var def = (byte*)DefaultData.Data; var blockSize = *(ushort*)(def + IdentifierSize); diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index c47c84ef..a7540f4b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -76,7 +76,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } public ExpandedEqpGmpBase(MetaFileManager manager, bool gmp) - : base(manager, gmp ? MetaIndex.Gmp : MetaIndex.Eqp) + : base(manager, manager.MarshalAllocator, gmp ? MetaIndex.Gmp : MetaIndex.Eqp) { AllocateData(MaxSize); Reset(); diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index f3860416..ba38d6d9 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -157,7 +157,7 @@ public sealed unsafe class EstFile : MetaBaseFile } public EstFile(MetaFileManager manager, EstType estType) - : base(manager, (MetaIndex)estType) + : base(manager, manager.MarshalAllocator, (MetaIndex)estType) { var length = DefaultData.Length; AllocateData(length + IncreaseSize); diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index 3d0b4dbe..6ab1591c 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -12,7 +12,7 @@ namespace Penumbra.Meta.Files; /// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. /// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. /// -public unsafe class EvpFile : MetaBaseFile +public unsafe class EvpFile(MetaFileManager manager) : MetaBaseFile(manager, manager.MarshalAllocator, (MetaIndex)1) { public const int FlagArraySize = 512; @@ -57,8 +57,4 @@ public unsafe class EvpFile : MetaBaseFile return EvpFlag.None; } - - public EvpFile(MetaFileManager manager) - : base(manager, (MetaIndex)1) // TODO: Name - { } } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 892f5b44..01ef3f16 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -7,16 +7,10 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -public class ImcException : Exception +public class ImcException(ImcIdentifier identifier, Utf8GamePath path) : Exception { - public readonly ImcIdentifier Identifier; - public readonly string GamePath; - - public ImcException(ImcIdentifier identifier, Utf8GamePath path) - { - Identifier = identifier; - GamePath = path.ToString(); - } + public readonly ImcIdentifier Identifier = identifier; + public readonly string GamePath = path.ToString(); public override string Message => "Could not obtain default Imc File.\n" @@ -146,7 +140,11 @@ public unsafe class ImcFile : MetaBaseFile } public ImcFile(MetaFileManager manager, ImcIdentifier identifier) - : base(manager, 0) + : this(manager, manager.MarshalAllocator, identifier) + { } + + public ImcFile(MetaFileManager manager, IFileAllocator alloc, ImcIdentifier identifier) + : base(manager, alloc, 0) { var path = identifier.GamePathString(); Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; @@ -194,7 +192,13 @@ public unsafe class ImcFile : MetaBaseFile public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - var newData = Manager.AllocateDefaultMemory(ActualLength, 8); + if (length == ActualLength) + { + MemoryUtility.MemCpyUnchecked((byte*)data, Data, ActualLength); + return; + } + + var newData = Manager.XivAllocator.Allocate(ActualLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); @@ -203,7 +207,7 @@ public unsafe class ImcFile : MetaBaseFile MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); - Manager.Free(data, length); - resource->SetData((IntPtr)newData, ActualLength); + Manager.XivAllocator.Release((void*)data, length); + resource->SetData((nint)newData, ActualLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index ab08efc2..86a55101 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,23 +1,75 @@ using Dalamud.Memory; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using OtterGui.Services; +using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Functions; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Meta.Files; -public unsafe class MetaBaseFile : IDisposable +public unsafe interface IFileAllocator { - protected readonly MetaFileManager Manager; + public T* Allocate(int length, int alignment = 1) where T : unmanaged; + public void Release(ref T* pointer, int length) where T : unmanaged; + + public void Release(void* pointer, int length) + { + var tmp = (byte*)pointer; + Release(ref tmp, length); + } + + public byte* Allocate(int length, int alignment = 1) + => Allocate(length, alignment); +} + +public sealed class MarshalAllocator : IFileAllocator +{ + public unsafe T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)Marshal.AllocHGlobal(length * sizeof(T)); + + public unsafe void Release(ref T* pointer, int length) where T : unmanaged + { + Marshal.FreeHGlobal((nint)pointer); + pointer = null; + } +} + +public sealed unsafe class XivFileAllocator : IFileAllocator, IService +{ + /// + /// Allocate in the games space for file storage. + /// We only need this if using any meta file. + /// + [Signature(Sigs.GetFileSpace)] + private readonly nint _getFileSpaceAddress = nint.Zero; + + public XivFileAllocator(IGameInteropProvider provider) + => provider.InitializeFromAttributes(this); + + public IMemorySpace* GetFileSpace() + => ((delegate* unmanaged)_getFileSpaceAddress)(); + + public T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + + public void Release(ref T* pointer, int length) where T : unmanaged + { + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + pointer = null; + } +} + +public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, MetaIndex idx) : IDisposable +{ + protected readonly MetaFileManager Manager = manager; + protected readonly IFileAllocator Allocator = alloc; public byte* Data { get; private set; } public int Length { get; private set; } - public CharacterUtility.InternalIndex Index { get; } - - public MetaBaseFile(MetaFileManager manager, MetaIndex idx) - { - Manager = manager; - Index = CharacterUtility.ReverseIndices[(int)idx]; - } + public CharacterUtility.InternalIndex Index { get; } = CharacterUtility.ReverseIndices[(int)idx]; protected (IntPtr Data, int Length) DefaultData => Manager.CharacterUtility.DefaultResource(Index); @@ -30,7 +82,7 @@ public unsafe class MetaBaseFile : IDisposable protected void AllocateData(int length) { Length = length; - Data = (byte*)Manager.AllocateFileMemory(length); + Data = Allocator.Allocate(length); if (length > 0) GC.AddMemoryPressure(length); } @@ -38,8 +90,7 @@ public unsafe class MetaBaseFile : IDisposable /// Free memory. protected void ReleaseUnmanagedResources() { - var ptr = (IntPtr)Data; - MemoryHelper.GameFree(ref ptr, (ulong)Length); + Allocator.Release(Data, Length); if (Length > 0) GC.RemoveMemoryPressure(Length); @@ -53,7 +104,7 @@ public unsafe class MetaBaseFile : IDisposable if (newLength == Length) return; - var data = (byte*)Manager.AllocateFileMemory((ulong)newLength); + var data = Allocator.Allocate(newLength); if (newLength > Length) { MemoryUtility.MemCpyUnchecked(data, Data, Length); diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 40fceb07..81c0fa3e 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,14 +1,10 @@ using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Memory; using OtterGui.Compression; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.Mods.Groups; @@ -28,6 +24,9 @@ public unsafe class MetaFileManager internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); + internal readonly IFileAllocator XivAllocator; + public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -42,6 +41,7 @@ public unsafe class MetaFileManager Identifier = identifier; Compactor = compactor; ImcChecker = new ImcChecker(this); + XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); } @@ -76,57 +76,11 @@ public unsafe class MetaFileManager } } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) - { - if (file == null || !Config.EnableMods) - CharacterUtility.ResetResource(metaIndex); - else - CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) - => Config.EnableMods - ? file == null - ? CharacterUtility.TemporarilyResetResource(metaIndex) - : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length) - : MetaList.MetaReverter.Disabled; - public void ApplyDefaultFiles(ModCollection? collection) { if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods) return; ResidentResources.Reload(); - if (collection._cache == null) - CharacterUtility.ResetAll(); - else - collection._cache.Meta.SetFiles(); } - - /// - /// Allocate in the games space for file storage. - /// We only need this if using any meta file. - /// - [Signature(Sigs.GetFileSpace)] - private readonly nint _getFileSpaceAddress = nint.Zero; - - public IMemorySpace* GetFileSpace() - => ((delegate* unmanaged)_getFileSpaceAddress)(); - - public void* AllocateFileMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateFileMemory(int length, int alignment = 0) - => AllocateFileMemory((ulong)length, (ulong)alignment); - - public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateDefaultMemory(int length, int alignment = 0) - => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); - - public void Free(nint ptr, int length) - => IMemorySpace.Free((void*)ptr, (ulong)length); } From f9c45a2f3f9bae991a67076622c6ef7d701c94ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 17:57:12 +0200 Subject: [PATCH 1773/2451] Clean unused functions. --- Penumbra/Collections/Cache/EqdpCache.cs | 10 ++----- Penumbra/Collections/Cache/EqpCache.cs | 12 --------- Penumbra/Collections/Cache/EstCache.cs | 12 --------- Penumbra/Collections/Cache/GmpCache.cs | 12 --------- Penumbra/Collections/Cache/IMetaCache.cs | 33 +++++------------------- Penumbra/Collections/Cache/ImcCache.cs | 7 ----- Penumbra/Collections/Cache/MetaCache.cs | 10 ------- Penumbra/Collections/Cache/RspCache.cs | 13 ---------- 8 files changed, 9 insertions(+), 100 deletions(-) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 6047736b..5e0626cf 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -10,17 +10,11 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries = []; - public override void SetFiles() - { } - public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry) => _fullEntries.TryGetValue((id, genderRace, accessory), out var pair) ? (originalEntry & pair.InverseMask) | pair.Entry : originalEntry; - protected override void IncorporateChangesInternal() - { } - public void Reset() { Clear(); @@ -46,8 +40,8 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) if (!_fullEntries.Remove(tuple, out var pair)) return; - var mask = Eqdp.Mask(identifier.Slot); - var newMask = pair.InverseMask | mask; + var mask = Eqdp.Mask(identifier.Slot); + var newMask = pair.InverseMask | mask; if (newMask is not EqdpEntry.FullMask) _fullEntries[tuple] = (pair.Entry & ~mask, newMask); } diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 7ba0c489..60e38aef 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -8,12 +8,6 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public unsafe EqpEntry GetValues(CharacterArmor* armor) => GetSingleValue(armor[0].Set, EquipSlot.Head) | GetSingleValue(armor[1].Set, EquipSlot.Body) @@ -28,12 +22,6 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) public void Reset() => Clear(); - protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) - { } - - protected override void RevertModInternal(EqpIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index ff94324e..aff8beef 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -6,12 +6,6 @@ namespace Penumbra.Collections.Cache; public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public EstEntry GetEstEntry(EstIdentifier identifier) => TryGetValue(identifier, out var entry) ? entry.Entry @@ -20,12 +14,6 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) public void Reset() => Clear(); - protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) - { } - - protected override void RevertModInternal(EstIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 541b424d..9170b871 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -6,21 +6,9 @@ namespace Penumbra.Collections.Cache; public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public void Reset() => Clear(); - protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) - { } - - protected override void RevertModInternal(GmpIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index dd218b48..fecc6f50 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -4,30 +4,19 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public abstract class MetaCacheBase +public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection) : Dictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - protected MetaCacheBase(MetaFileManager manager, ModCollection collection) - { - Manager = manager; - Collection = collection; - if (!Manager.CharacterUtility.Ready) - Manager.CharacterUtility.LoadingFinished += IncorporateChanges; - } - - protected readonly MetaFileManager Manager; - protected readonly ModCollection Collection; + protected readonly MetaFileManager Manager = manager; + protected readonly ModCollection Collection = collection; public void Dispose() { - Manager.CharacterUtility.LoadingFinished -= IncorporateChanges; Dispose(true); } - public abstract void SetFiles(); - public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { lock (this) @@ -59,20 +48,12 @@ public abstract class MetaCacheBase return true; } - private void IncorporateChanges() - { - lock (this) - { - IncorporateChangesInternal(); - } - if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) - SetFiles(); - } + protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry) + { } - protected abstract void ApplyModInternal(TIdentifier identifier, TEntry entry); - protected abstract void RevertModInternal(TIdentifier identifier); - protected abstract void IncorporateChangesInternal(); + protected virtual void RevertModInternal(TIdentifier identifier) + { } protected virtual void Dispose(bool _) { } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index e7eedc04..40c3d2c7 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -10,9 +10,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) { private readonly Dictionary)> _imcFiles = []; - public override void SetFiles() - { } - public bool HasFile(ByteString path) => _imcFiles.ContainsKey(path); @@ -28,10 +25,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) return true; } - protected override void IncorporateChangesInternal() - { } - - public void Reset() { foreach (var (_, (file, set)) in _imcFiles) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 253f3c7f..02056fad 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -29,16 +29,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); - public void SetFiles() - { - Eqp.SetFiles(); - Eqdp.SetFiles(); - Est.SetFiles(); - Gmp.SetFiles(); - Rsp.SetFiles(); - Imc.SetFiles(); - } - public void Reset() { Eqp.Reset(); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 8a983c6c..064b1f44 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -5,22 +5,9 @@ namespace Penumbra.Collections.Cache; public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public void Reset() => Clear(); - protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) - { } - - protected override void RevertModInternal(RspIdentifier identifier) - { } - - protected override void Dispose(bool _) => Clear(); } From cf1dcfcb7cb27e6928810b75c63f1f352da4ecd2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 18:33:37 +0200 Subject: [PATCH 1774/2451] Improve Path preprocessing. --- .../Interop/PathResolving/PathResolver.cs | 23 ++-- .../Interop/PathResolving/SubfileHelper.cs | 16 --- .../Processing/AvfxPathPreProcessor.cs | 16 +++ .../Processing/FilePostProcessService.cs | 39 ++++++ .../Processing/GamePathPreProcessService.cs | 37 +++++ .../Processing/ImcFilePostProcessor.cs | 30 ++++ .../Interop/Processing/ImcPathPreProcessor.cs | 18 +++ .../Processing/MaterialFilePostProcessor.cs | 18 +++ .../Processing/MtrlPathPreProcessor.cs | 16 +++ .../Interop/Processing/TmbPathPreProcessor.cs | 16 +++ .../Interop/ResourceLoading/ResourceLoader.cs | 69 ---------- Penumbra/Interop/Services/CharacterUtility.cs | 8 +- Penumbra/Interop/Services/MetaList.cs | 130 +----------------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 19 --- 14 files changed, 209 insertions(+), 246 deletions(-) create mode 100644 Penumbra/Interop/Processing/AvfxPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/FilePostProcessService.cs create mode 100644 Penumbra/Interop/Processing/GamePathPreProcessService.cs create mode 100644 Penumbra/Interop/Processing/ImcFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/ImcPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/MaterialFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/MtrlPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/TmbPathPreProcessor.cs diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index e069e3ea..f31b3323 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.Processing; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; using Penumbra.Util; @@ -15,14 +16,16 @@ public class PathResolver : IDisposable private readonly CollectionManager _collectionManager; private readonly ResourceLoader _loader; - private readonly SubfileHelper _subfileHelper; - private readonly PathState _pathState; - private readonly MetaState _metaState; - private readonly GameState _gameState; - private readonly CollectionResolver _collectionResolver; + private readonly SubfileHelper _subfileHelper; + private readonly PathState _pathState; + private readonly MetaState _metaState; + private readonly GameState _gameState; + private readonly CollectionResolver _collectionResolver; + private readonly GamePathPreProcessService _preprocessor; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, - SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) + public PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, + SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState, + GamePathPreProcessService preprocessor) { _performance = performance; _config = config; @@ -31,6 +34,7 @@ public class PathResolver : IDisposable _pathState = pathState; _metaState = metaState; _gameState = gameState; + _preprocessor = preprocessor; _collectionResolver = collectionResolver; _loader = loader; _loader.ResolvePath = ResolvePath; @@ -102,11 +106,10 @@ public class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, gamePath, out var pair); - return pair; + return _preprocessor.PreProcess(resolveData, path, nonDefault, type, resolved, gamePath); } - public unsafe void Dispose() + public void Dispose() { _loader.ResetResolvePath(); } diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index b9631cf2..3cefd98d 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -66,22 +66,6 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. - public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - Utf8GamePath originalPath, out (FullPath?, ResolveData) data) - { - resolved = type switch - { - ResourceType.Mtrl when nonDefault => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), - ResourceType.Avfx when nonDefault => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), - ResourceType.Tmb when nonDefault => PathDataHandler.CreateTmb(path, resolveData.ModCollection), - ResourceType.Imc when resolveData.ModCollection.MetaCache?.Imc.HasFile(path) ?? false => PathDataHandler.CreateImc(path, - resolveData.ModCollection), - _ => resolved, - }; - data = (resolved, resolveData); - } - public void Dispose() { _loader.ResourceLoaded -= SubfileContainerRequested; diff --git a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs new file mode 100644 index 00000000..56f693e6 --- /dev/null +++ b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class AvfxPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Avfx; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateAvfx(path, resolveData.ModCollection) : resolved; +} diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs new file mode 100644 index 00000000..0dc62b3d --- /dev/null +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -0,0 +1,39 @@ +using System.Collections.Frozen; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public interface IFilePostProcessor : IService +{ + public ResourceType Type { get; } + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); +} + +public unsafe class FilePostProcessService : IRequiredService, IDisposable +{ + private readonly ResourceLoader _resourceLoader; + private readonly FrozenDictionary _processors; + + public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) + { + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.FileLoaded += OnFileLoaded; + } + + public void Dispose() + { + _resourceLoader.FileLoaded -= OnFileLoaded; + } + + private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) + { + if (_processors.TryGetValue(resource->FileType, out var processor)) + processor.PostProcess(resource, path, additionalData); + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs new file mode 100644 index 00000000..004b7168 --- /dev/null +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -0,0 +1,37 @@ +using System.Collections.Frozen; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public interface IPathPreProcessor : IService +{ + public ResourceType Type { get; } + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); +} + +public class GamePathPreProcessService : IService +{ + private readonly FrozenDictionary _processors; + + public GamePathPreProcessService(ServiceManager services) + { + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + } + + + public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, + FullPath? resolved, + Utf8GamePath originalPath) + { + if (!_processors.TryGetValue(type, out var processor)) + return (resolved, resolveData); + + resolved = processor.PreProcess(resolveData, path, originalPath, nonDefault, resolved); + return (resolved, resolveData); + } +} diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs new file mode 100644 index 00000000..4a0ebe22 --- /dev/null +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -0,0 +1,30 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!cache.Imc.GetFile(originalGamePath, out var file)) + return; + + file.Replace(resource); + Penumbra.Log.Information( + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } +} diff --git a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs new file mode 100644 index 00000000..907d7587 --- /dev/null +++ b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs @@ -0,0 +1,18 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class ImcPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) + => resolveData.ModCollection.MetaCache?.Imc.HasFile(originalGamePath.Path) ?? false + ? PathDataHandler.CreateImc(path, resolveData.ModCollection) + : resolved; +} diff --git a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs new file mode 100644 index 00000000..02b5d46c --- /dev/null +++ b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs @@ -0,0 +1,18 @@ +using Penumbra.Api.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class MaterialFilePostProcessor //: IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.ReadMtrl(additionalData, out var data)) + return; + } +} diff --git a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs new file mode 100644 index 00000000..8fb2400b --- /dev/null +++ b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class MtrlPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalGamePath) : resolved; +} diff --git a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs new file mode 100644 index 00000000..dd887819 --- /dev/null +++ b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class TmbPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Tmb; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateTmb(path, resolveData.ModCollection) : resolved; +} diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index fae38907..7b49beab 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,9 +1,6 @@ -using System.Collections.Frozen; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -13,72 +10,6 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; -public interface IFilePostProcessor : IService -{ - public ResourceType Type { get; } - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); -} - -public sealed class MaterialFilePostProcessor : IFilePostProcessor -{ - public ResourceType Type - => ResourceType.Mtrl; - - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) - { - if (!PathDataHandler.ReadMtrl(additionalData, out var data)) - return; - } -} - -public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor -{ - public ResourceType Type - => ResourceType.Imc; - - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) - { - if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) - return; - - var collection = collections.ByLocalId(data.Collection); - if (collection.MetaCache is not { } cache) - return; - - if (!cache.Imc.GetFile(originalGamePath, out var file)) - return; - - file.Replace(resource); - Penumbra.Log.Information( - $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } -} - -public unsafe class FilePostProcessService : IRequiredService, IDisposable -{ - private readonly ResourceLoader _resourceLoader; - private readonly FrozenDictionary _processors; - - public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) - { - _resourceLoader = resourceLoader; - _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.FileLoaded += OnFileLoaded; - } - - public void Dispose() - { - _resourceLoader.FileLoaded -= OnFileLoaded; - } - - private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) - { - if (_processors.TryGetValue(resource->FileType, out var processor)) - processor.PostProcess(resource, path, additionalData); - } -} - public unsafe class ResourceLoader : IDisposable { private readonly ResourceService _resources; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 9459df06..0877d221 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -46,9 +46,6 @@ public unsafe class CharacterUtility : IDisposable private readonly MetaList[] _lists; - public IReadOnlyList Lists - => _lists; - public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; @@ -58,7 +55,7 @@ public unsafe class CharacterUtility : IDisposable { interop.InitializeFromAttributes(this); _lists = Enumerable.Range(0, RelevantIndices.Length) - .Select(idx => new MetaList(this, new InternalIndex(idx))) + .Select(idx => new MetaList(new InternalIndex(idx))) .ToArray(); _framework = framework; LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); @@ -124,9 +121,6 @@ public unsafe class CharacterUtility : IDisposable if (!Ready) return; - foreach (var list in _lists) - list.Dispose(); - Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index 24d3f088..839c289e 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -2,26 +2,14 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; -public unsafe class MetaList : IDisposable +public class MetaList(CharacterUtility.InternalIndex index) { - private readonly CharacterUtility _utility; - private readonly LinkedList _entries = new(); - public readonly CharacterUtility.InternalIndex Index; - public readonly MetaIndex GlobalMetaIndex; - - public IReadOnlyCollection Entries - => _entries; + public readonly CharacterUtility.InternalIndex Index = index; + public readonly MetaIndex GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value]; private nint _defaultResourceData = nint.Zero; - private int _defaultResourceSize = 0; - public bool Ready { get; private set; } = false; - - public MetaList(CharacterUtility utility, CharacterUtility.InternalIndex index) - { - _utility = utility; - Index = index; - GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value]; - } + private int _defaultResourceSize; + public bool Ready { get; private set; } public void SetDefaultResource(nint data, int size) { @@ -31,116 +19,8 @@ public unsafe class MetaList : IDisposable _defaultResourceData = data; _defaultResourceSize = size; Ready = _defaultResourceData != nint.Zero && size != 0; - if (_entries.Count <= 0) - return; - - var first = _entries.First!.Value; - SetResource(first.Data, first.Length); } public (nint Address, int Size) DefaultResource => (_defaultResourceData, _defaultResourceSize); - - public MetaReverter TemporarilySetResource(nint data, int length) - { - Penumbra.Log.Excessive($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); - var reverter = new MetaReverter(this, data, length); - _entries.AddFirst(reverter); - SetResourceInternal(data, length); - return reverter; - } - - public MetaReverter TemporarilyResetResource() - { - Penumbra.Log.Excessive( - $"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); - var reverter = new MetaReverter(this); - _entries.AddFirst(reverter); - ResetResourceInternal(); - return reverter; - } - - public void SetResource(nint data, int length) - { - Penumbra.Log.Excessive($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); - SetResourceInternal(data, length); - } - - public void ResetResource() - { - Penumbra.Log.Excessive($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); - ResetResourceInternal(); - } - - /// Set the currently stored data of this resource to new values. - private void SetResourceInternal(nint data, int length) - { - if (!Ready) - return; - - var resource = _utility.Address->Resource(GlobalMetaIndex); - resource->SetData(data, length); - } - - /// Reset the currently stored data of this resource to its default values. - private void ResetResourceInternal() - => SetResourceInternal(_defaultResourceData, _defaultResourceSize); - - private void SetResourceToDefaultCollection() - {} - - public void Dispose() - { - if (_entries.Count > 0) - { - foreach (var entry in _entries) - entry.Disposed = true; - - _entries.Clear(); - } - - ResetResourceInternal(); - } - - public sealed class MetaReverter(MetaList metaList, nint data, int length) : IDisposable - { - public static readonly MetaReverter Disabled = new(null!) { Disposed = true }; - - public readonly MetaList MetaList = metaList; - public readonly nint Data = data; - public readonly int Length = length; - public readonly bool Resetter; - public bool Disposed; - - public MetaReverter(MetaList metaList) - : this(metaList, nint.Zero, 0) - => Resetter = true; - - public void Dispose() - { - if (Disposed) - return; - - var list = MetaList._entries; - var wasCurrent = ReferenceEquals(this, list.First?.Value); - list.Remove(this); - if (!wasCurrent) - return; - - if (list.Count == 0) - { - MetaList.SetResourceToDefaultCollection(); - } - else - { - var next = list.First!.Value; - if (next.Resetter) - MetaList.ResetResourceInternal(); - else - MetaList.SetResourceInternal(next.Data, next.Length); - } - - Disposed = true; - } - } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 396029d5..1519ebf0 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -179,8 +179,6 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawData(); ImGui.NewLine(); - DrawDebugTabMetaLists(); - ImGui.NewLine(); DrawResourceProblems(); ImGui.NewLine(); DrawPlayerModelInfo(); @@ -788,23 +786,6 @@ public class DebugTab : Window, ITab } } - private void DrawDebugTabMetaLists() - { - if (!ImGui.CollapsingHeader("Metadata Changes")) - return; - - using var table = Table("##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - foreach (var list in _characterUtility.Lists) - { - ImGuiUtil.DrawTableColumn(list.GlobalMetaIndex.ToString()); - ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); - ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); - } - } - /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { From e05dbe988570d949f6af81f65b3073c875fb9fb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 21:59:04 +0200 Subject: [PATCH 1775/2451] Make everything services. --- OtterGui | 2 +- Penumbra/Api/TempModManager.cs | 3 +- .../Cache/CollectionCacheManager.cs | 3 +- .../Collections/Manager/ActiveCollections.cs | 5 +- .../Collections/Manager/CollectionEditor.cs | 3 +- .../Collections/Manager/CollectionManager.cs | 3 +- .../Collections/Manager/CollectionStorage.cs | 5 +- .../Collections/Manager/InheritanceManager.cs | 14 +- .../Manager/TempCollectionManager.cs | 3 +- Penumbra/CommandHandler.cs | 4 +- Penumbra/Configuration.cs | 3 +- Penumbra/EphemeralConfig.cs | 3 +- Penumbra/Import/Models/ModelManager.cs | 4 +- Penumbra/Import/Textures/TextureManager.cs | 26 ++- .../Interop/PathResolving/CutsceneService.cs | 3 +- .../IdentifiedCollectionCache.cs | 4 +- Penumbra/Interop/PathResolving/MetaState.cs | 3 +- .../Interop/PathResolving/PathResolver.cs | 3 +- Penumbra/Interop/PathResolving/PathState.cs | 3 +- .../Interop/PathResolving/SubfileHelper.cs | 4 +- .../ResourceLoading/FileReadService.cs | 3 +- .../Interop/ResourceLoading/ResourceLoader.cs | 5 +- .../ResourceLoading/ResourceManagerService.cs | 3 +- .../ResourceLoading/ResourceService.cs | 3 +- .../Interop/ResourceLoading/TexMdlService.cs | 3 +- .../ResourceTree/ResourceTreeFactory.cs | 3 +- Penumbra/Interop/Services/CharacterUtility.cs | 3 +- Penumbra/Interop/Services/FontReloader.cs | 3 +- Penumbra/Interop/Services/ModelRenderer.cs | 9 +- Penumbra/Interop/Services/RedrawService.cs | 11 +- .../Services/ResidentResourceManager.cs | 5 +- Penumbra/Meta/MetaFileManager.cs | 3 +- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 3 +- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModFileCollection.cs | 3 +- Penumbra/Mods/Editor/ModFileEditor.cs | 3 +- Penumbra/Mods/Editor/ModMerger.cs | 3 +- Penumbra/Mods/Editor/ModNormalizer.cs | 4 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 4 +- Penumbra/Mods/Manager/ModCacheManager.cs | 3 +- Penumbra/Mods/Manager/ModDataEditor.cs | 5 +- Penumbra/Mods/Manager/ModExportManager.cs | 3 +- Penumbra/Mods/Manager/ModFileSystem.cs | 5 +- Penumbra/Mods/Manager/ModImportManager.cs | 6 +- Penumbra/Mods/Manager/ModManager.cs | 3 +- Penumbra/Mods/ModCreator.cs | 3 +- Penumbra/Services/StaticServiceManager.cs | 154 +----------------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 3 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 5 +- Penumbra/UI/ChangedItemDrawer.cs | 5 +- Penumbra/UI/Changelog.cs | 3 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 3 +- Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/UI/FileDialogService.cs | 3 +- Penumbra/UI/ImportPopup.cs | 3 +- Penumbra/UI/LaunchButton.cs | 3 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 9 +- Penumbra/UI/ModsTab/ModPanel.cs | 3 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 28 +--- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 18 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 11 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 7 +- Penumbra/UI/PredefinedTagManager.cs | 4 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 3 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 3 +- Penumbra/UI/Tabs/CollectionsTab.cs | 3 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 6 +- Penumbra/UI/Tabs/EffectiveTab.cs | 3 +- Penumbra/UI/Tabs/MessagesTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 3 +- Penumbra/UI/Tabs/OnScreenTab.cs | 10 +- Penumbra/UI/Tabs/ResourceTab.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 3 +- Penumbra/UI/TutorialService.cs | 3 +- Penumbra/UI/WindowSystem.cs | 3 +- 81 files changed, 220 insertions(+), 317 deletions(-) diff --git a/OtterGui b/OtterGui index e95c0f04..caa9e9b9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8 +Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65 diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index cbb07436..0b52e64a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Meta.Manipulations; @@ -18,7 +19,7 @@ public enum RedirectResult FilteredGamePath = 3, } -public class TempModManager : IDisposable +public class TempModManager : IDisposable, IService { private readonly CommunicatorService _communicator; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 02c9c8a9..44c12856 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -17,7 +18,7 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public class CollectionCacheManager : IDisposable +public class CollectionCacheManager : IDisposable, IService { private readonly FrameworkManager _framework; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 4e8ebe36..6d48f74b 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; @@ -11,7 +12,7 @@ using Penumbra.UI; namespace Penumbra.Collections.Manager; -public class ActiveCollectionData +public class ActiveCollectionData : IService { public ModCollection Current { get; internal set; } = ModCollection.Empty; public ModCollection Default { get; internal set; } = ModCollection.Empty; @@ -20,7 +21,7 @@ public class ActiveCollectionData public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues().Length - 3]; } -public class ActiveCollections : ISavable, IDisposable +public class ActiveCollections : ISavable, IDisposable, IService { public const int Version = 2; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 0243de1e..caff2c86 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -7,7 +8,7 @@ using Penumbra.Services; namespace Penumbra.Collections.Manager; -public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) +public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService { /// Enable or disable the mod inheritance of mod idx. public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index e95617b1..85f5b957 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Collections.Cache; namespace Penumbra.Collections.Manager; @@ -8,7 +9,7 @@ public class CollectionManager( InheritanceManager inheritances, CollectionCacheManager caches, TempCollectionManager temp, - CollectionEditor editor) + CollectionEditor editor) : IService { public readonly CollectionStorage Storage = storage; public readonly ActiveCollections Active = active; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 67de3a03..cd680d36 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,7 +1,7 @@ -using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -11,7 +11,6 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using Penumbra.UI.CollectionTab; namespace Penumbra.Collections.Manager; @@ -24,7 +23,7 @@ public readonly record struct LocalCollectionId(int Id) : IAdditionOperators new(left.Id + right); } -public class CollectionStorage : IReadOnlyList, IDisposable +public class CollectionStorage : IReadOnlyList, IDisposable, IService { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 6003b5f9..f3482cdf 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -2,11 +2,10 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.CollectionTab; -using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -15,7 +14,7 @@ namespace Penumbra.Collections.Manager; /// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. /// Circular dependencies are resolved by distinctness. /// -public class InheritanceManager : IDisposable +public class InheritanceManager : IDisposable, IService { public enum ValidInheritance { @@ -144,7 +143,8 @@ public class InheritanceManager : IDisposable continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) { @@ -153,12 +153,14 @@ public class InheritanceManager : IDisposable if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + NotificationType.Warning); } else { Penumbra.Messager.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning); + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", + NotificationType.Warning); changes = true; } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index ce438a6b..5c893232 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; @@ -8,7 +9,7 @@ using Penumbra.String; namespace Penumbra.Collections.Manager; -public class TempCollectionManager : IDisposable +public class TempCollectionManager : IDisposable, IService { public int GlobalChangeCounter { get; private set; } public readonly IndividualCollections Collections; diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4e1d6453..484dd954 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -10,12 +11,11 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Services; using Penumbra.UI; namespace Penumbra; -public class CommandHandler : IDisposable +public class CommandHandler : IDisposable, IApiService { private const string CommandName = "/penumbra"; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 02286cc7..f6100b62 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Import.Structs; using Penumbra.Interop.Services; @@ -18,7 +19,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public class Configuration : IPluginConfiguration, ISavable +public class Configuration : IPluginConfiguration, ISavable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 0a542d04..52e276c7 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Enums; @@ -14,7 +15,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; -public class EphemeralConfig : ISavable, IDisposable +public class EphemeralConfig : ISavable, IDisposable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9fa77784..01396cfb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; using OtterGui; +using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData; @@ -21,7 +22,8 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) + : SingleTaskQueue, IDisposable, IService { private readonly IFramework _framework = framework; diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 976bc179..4aa64209 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; using Lumina.Data.Files; using OtterGui.Log; +using OtterGui.Services; using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; @@ -12,22 +13,14 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager : SingleTaskQueue, IDisposable +public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) + : SingleTaskQueue, IDisposable, IService { - private readonly Logger _logger; - private readonly UiBuilder _uiBuilder; - private readonly IDataManager _gameData; + private readonly Logger _logger = logger; - private readonly ConcurrentDictionary _tasks = new(); + private readonly ConcurrentDictionary _tasks = new(); private bool _disposed; - public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) - { - _uiBuilder = uiBuilder; - _gameData = gameData; - _logger = logger; - } - public IReadOnlyDictionary Tasks => _tasks; @@ -64,7 +57,8 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable { var token = new CancellationTokenSource(); var task = Enqueue(a, token.Token); - task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, + TaskScheduler.Default); return (task, token); }).Item1; } @@ -217,7 +211,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) - => _uiBuilder.LoadImageRaw(rgba, width, height, 4); + => uiBuilder.LoadImageRaw(rgba, width, height, 4); /// Load any supported file from game data or drive depending on extension and if the path is rooted. public (BaseImage, TextureType) Load(string path) @@ -326,7 +320,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable } public bool GameFileExists(string path) - => _gameData.FileExists(path); + => gameData.FileExists(path); /// Add up to 13 mip maps to the input if mip maps is true, otherwise return input. public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) @@ -382,7 +376,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable if (Path.IsPathRooted(path)) return File.OpenRead(path); - var file = _gameData.GetFile(path); + var file = gameData.GetFile(path); return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files."); } diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 93fee11e..feb27341 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; @@ -9,7 +8,7 @@ using Penumbra.String; namespace Penumbra.Interop.PathResolving; -public sealed class CutsceneService : IService, IDisposable +public sealed class CutsceneService : IRequiredService, IDisposable { public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 32090f7c..eeff7eee 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -10,7 +11,8 @@ using Penumbra.Services; namespace Penumbra.Interop.PathResolving; -public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>, + IService { private readonly CommunicatorService _communicator; private readonly CharacterDestructor _characterDestructor; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 7f820b4e..f7dcfc07 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData.Structs; @@ -34,7 +35,7 @@ namespace Penumbra.Interop.PathResolving; // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. -public sealed unsafe class MetaState : IDisposable +public sealed unsafe class MetaState : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index f31b3323..cc3e0e9b 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -9,7 +10,7 @@ using Penumbra.Util; namespace Penumbra.Interop.PathResolving; -public class PathResolver : IDisposable +public class PathResolver : IDisposable, IService { private readonly PerformanceTracker _performance; private readonly Configuration _config; diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index f4218e9c..bf9d1e25 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.String; @@ -5,7 +6,7 @@ using Penumbra.String; namespace Penumbra.Interop.PathResolving; public sealed class PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility) - : IDisposable + : IDisposable, IService { public readonly CollectionResolver CollectionResolver = collectionResolver; public readonly MetaState MetaState = metaState; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 3cefd98d..44a152f0 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,9 +1,9 @@ +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; -using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.PathResolving; @@ -13,7 +13,7 @@ namespace Penumbra.Interop.PathResolving; /// Those are loaded synchronously. /// Thus, we need to ensure the correct files are loaded when a material is loaded. /// -public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> +public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection>, IService { private readonly GameState _gameState; private readonly ResourceLoader _loader; diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 64442771..f1d7fe24 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -1,13 +1,14 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.Util; namespace Penumbra.Interop.ResourceLoading; -public unsafe class FileReadService : IDisposable +public unsafe class FileReadService : IDisposable, IRequiredService { public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 7b49beab..4a423993 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,4 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.PathResolving; @@ -10,7 +11,7 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceLoader : IDisposable +public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; @@ -212,7 +213,7 @@ public unsafe class ResourceLoader : IDisposable /// /// Catch weird errors with invalid decrements of the reference count. /// - private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) + private static void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) { if (handle->RefCount != 0) return; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index a087a659..c885c317 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceManagerService +public unsafe class ResourceManagerService : IRequiredService { public ResourceManagerService(IGameInteropProvider interop) => interop.InitializeFromAttributes(this); diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index e3338e6c..54c86777 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.Interop.SafeHandles; @@ -13,7 +14,7 @@ using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle. namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceService : IDisposable +public unsafe class ResourceService : IDisposable, IRequiredService { private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index b9279f54..e617673e 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -2,13 +2,14 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceLoading; -public unsafe class TexMdlService : IDisposable +public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 5a190e52..e26c1436 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; @@ -17,7 +18,7 @@ public class ResourceTreeFactory( ObjectIdentification identifier, Configuration config, ActorManager actors, - PathState pathState) + PathState pathState) : IService { private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 0877d221..532dc823 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,11 +1,12 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; -public unsafe class CharacterUtility : IDisposable +public unsafe class CharacterUtility : IDisposable, IRequiredService { public record struct InternalIndex(int Value); diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 2f4a3cfd..259fdd10 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; +using OtterGui.Services; using Penumbra.GameData; namespace Penumbra.Interop.Services; @@ -9,7 +10,7 @@ namespace Penumbra.Interop.Services; /// Handle font reloading via game functions. /// May cause a interface flicker while reloading. /// -public unsafe class FontReloader +public unsafe class FontReloader : IService { public bool Valid => _reloadFontsFunc != null; diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 7df83cf7..b268b395 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -1,10 +1,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; namespace Penumbra.Interop.Services; -public unsafe class ModelRenderer : IDisposable +public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } @@ -37,14 +38,14 @@ public unsafe class ModelRenderer : IDisposable if (DefaultCharacterGlassShaderPackage == null) { - DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; - anyMissing |= DefaultCharacterGlassShaderPackage == null; + DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; + anyMissing |= DefaultCharacterGlassShaderPackage == null; } if (anyMissing) return; - Ready = true; + Ready = true; _framework.Update -= LoadDefaultResources; } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 21ecfd4f..61d7b90c 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Housing; using FFXIVClientStructs.Interop; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Communication; @@ -20,7 +21,7 @@ using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Services; -public unsafe partial class RedrawService +public unsafe partial class RedrawService : IService { public const int GPosePlayerIdx = 201; public const int GPoseSlots = 42; @@ -171,7 +172,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter + && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; if (gPose) @@ -190,7 +192,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter + && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; if (gPose) @@ -380,7 +383,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (!ret && lowerName.Length > 1 && lowerName[0] == '#' && ushort.TryParse(lowerName[1..], out var objectIndex)) { ret = true; - actor = _objects.GetDalamudObject((int) objectIndex); + actor = _objects.GetDalamudObject((int)objectIndex); } return ret; diff --git a/Penumbra/Interop/Services/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs index 72697185..4f430aa1 100644 --- a/Penumbra/Interop/Services/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -1,10 +1,11 @@ -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; namespace Penumbra.Interop.Services; -public unsafe class ResidentResourceManager +public unsafe class ResidentResourceManager : IService { // A static pointer to the resident resource manager address. [Signature(Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress)] diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 81c0fa3e..3755afa2 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Data; @@ -13,7 +14,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage namespace Penumbra.Meta; -public unsafe class MetaFileManager +public class MetaFileManager : IService { internal readonly Configuration Config; internal readonly CharacterUtility CharacterUtility; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 47aa18dc..bcecf264 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -7,7 +8,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) +public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) : IService { private readonly SHA256 _hasher = SHA256.Create(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 738e606e..2a23ffad 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,11 +1,12 @@ using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; namespace Penumbra.Mods.Editor; -public partial class MdlMaterialEditor(ModFileCollection files) +public partial class MdlMaterialEditor(ModFileCollection files) : IService { [GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] private static partial Regex MaterialRegex(); diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 37524da1..cacb7f88 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods; @@ -14,7 +14,7 @@ public class ModEditor( ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor, FileCompactor compactor) - : IDisposable + : IDisposable, IService { public readonly ModNormalizer ModNormalizer = modNormalizer; public readonly ModMetaEditor MetaEditor = metaEditor; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 551d04cf..241f5b3b 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,10 +1,11 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileCollection : IDisposable +public class ModFileCollection : IDisposable, IService { private readonly List _available = []; private readonly List _mtrl = []; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index e2c0b726..55e0e94e 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -5,7 +6,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) +public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) : IService { public bool Changes { get; private set; } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 2df76838..9d31664b 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -13,7 +14,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.Mods.Editor; -public class ModMerger : IDisposable +public class ModMerger : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 58e4fc08..c6bc4939 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,15 +1,15 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using OtterGui.Tasks; -using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModNormalizer(ModManager _modManager, Configuration _config) +public class ModNormalizer(ModManager _modManager, Configuration _config) : IService { private readonly List>> _redirections = []; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0250efae..1a8ff2eb 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,10 +1,10 @@ -using Penumbra.Mods; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.Util; -public class ModSwapEditor(ModManager modManager) +public class ModSwapEditor(ModManager modManager) : IService { private readonly Dictionary _swaps = []; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 8ab8cf33..38d98d7c 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.Mods.Groups; @@ -8,7 +9,7 @@ using Penumbra.Util; namespace Penumbra.Mods.Manager; -public class ModCacheManager : IDisposable +public class ModCacheManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index c7c7c2cc..4ab9deb1 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,6 +1,7 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -23,7 +24,7 @@ public enum ModDataChangeType : ushort Note = 0x0800, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, @@ -49,7 +50,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var save = true; if (File.Exists(dataFile)) - { try { var text = File.ReadAllText(dataFile); @@ -65,7 +65,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } - } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 676018be..38b9c0fd 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,10 +1,11 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModExportManager : IDisposable +public class ModExportManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index c8a0a5db..e32fec0c 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public sealed class ModFileSystem : FileSystem, IDisposable, ISavable +public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, IService { private readonly ModManager _modManager; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index c99b7d0e..ff39b021 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,11 +1,12 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Import; using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; -public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService { private readonly ConcurrentQueue _modsToUnpack = new(); @@ -32,7 +33,8 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd if (File.Exists(s)) return true; - Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, false); + Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, + false); return false; }).Select(s => new FileInfo(s)).ToArray(); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 42082383..4b19ea4c 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; @@ -27,7 +28,7 @@ public enum ModPathChangeType StartingReload, } -public sealed class ModManager : ModStorage, IDisposable +public sealed class ModManager : ModStorage, IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ed4245c4..0e66367a 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Import; @@ -23,7 +24,7 @@ public partial class ModCreator( Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, - GamePathParser _gamePathParser) + GamePathParser _gamePathParser) : IService { public readonly Configuration Config = config; diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 35e36349..3279da96 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -5,36 +5,15 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using OtterGui; -using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; -using Penumbra.Api; using Penumbra.Api.Api; -using Penumbra.Collections.Cache; -using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.Import.Models; using Penumbra.GameData.Structs; -using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.ResourceTree; -using Penumbra.Interop.Services; using Penumbra.Meta; -using Penumbra.Mods; -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.UI; -using Penumbra.UI.AdvancedWindow; -using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; -using Penumbra.UI.ResourceWatcher; -using Penumbra.UI.Tabs; -using Penumbra.UI.Tabs.Debug; using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; -using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using Penumbra.Api.IpcTester; namespace Penumbra.Services; @@ -45,19 +24,18 @@ public static class StaticServiceManager var services = new ServiceManager(log) .AddDalamudServices(pi) .AddExistingService(log) - .AddExistingService(penumbra) - .AddInterop() - .AddConfiguration() - .AddCollections() - .AddMods() - .AddResources() - .AddResolvers() - .AddInterface() - .AddModEditor() - .AddApi(); + .AddExistingService(penumbra); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Penumbra).Assembly); services.AddIServices(typeof(ImGuiUtil).Assembly); + services.AddSingleton(p => + { + var cutsceneService = p.GetRequiredService(); + return new CutsceneResolver(cutsceneService.GetParentIndex); + }) + .AddSingleton(p => p.GetRequiredService().ImcChecker) + .AddSingleton(s => (ModStorage)s.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()); services.CreateProvider(); return services; } @@ -83,118 +61,4 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi); - - private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(p => - { - var cutsceneService = p.GetRequiredService(); - return new CutsceneResolver(cutsceneService.GetParentIndex); - }) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => p.GetRequiredService().ImcChecker); - - private static ServiceManager AddConfiguration(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddCollections(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddMods(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(s => (ModStorage)s.GetRequiredService()); - - private static ServiceManager AddResources(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddResolvers(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddInterface(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => new Diagnostics(p)); - - private static ServiceManager AddModEditor(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddApi(this ServiceManager services) - => services.AddSingleton(x => x.GetRequiredService()); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 62115dd6..652f928d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -24,7 +25,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ItemSwapTab : IDisposable, ITab +public class ItemSwapTab : IDisposable, ITab, IUiService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 9410b793..90fdc48e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -32,7 +33,7 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; -public partial class ModEditWindow : Window, IDisposable +public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 5dad66b4..b5f0255c 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -9,7 +10,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ModMergeTab(ModMerger modMerger) +public class ModMergeTab(ModMerger modMerger) : IUiService { private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList()); private string _newModName = string.Empty; @@ -183,7 +184,7 @@ public class ModMergeTab(ModMerger modMerger) else { ImGuiUtil.DrawTableColumn(option.GetName()); - + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 29a1f291..0afeeeeb 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -10,6 +10,7 @@ using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -19,7 +20,7 @@ using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; -public class ChangedItemDrawer : IDisposable +public class ChangedItemDrawer : IDisposable, IUiService { [Flags] public enum ChangedItemIcon : uint @@ -99,8 +100,10 @@ public class ChangedItemDrawer : IDisposable slot = 0; foreach (var (item, flag) in LowerNames.Zip(Order)) + { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; + } return slot != 0; } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 184633f2..f4cedf7d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -1,8 +1,9 @@ +using OtterGui.Services; using OtterGui.Widgets; namespace Penumbra.UI; -public class PenumbraChangelog +public class PenumbraChangelog : IUiService { public const int LastChangelogVersion = 0; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index bff0092a..6c8bbf64 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,6 +1,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -9,7 +10,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.UI.Classes; -public class CollectionSelectHeader +public class CollectionSelectHeader : IUiService { private readonly CollectionCombo _collectionCombo; private readonly ActiveCollections _activeCollections; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 0ae16f6d..67b0a50c 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Services; @@ -13,7 +14,7 @@ using Penumbra.Util; namespace Penumbra.UI; -public sealed class ConfigWindow : Window +public sealed class ConfigWindow : Window, IUiService { private readonly DalamudPluginInterface _pluginInterface; private readonly Configuration _config; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 88c0b00f..cc2a7f6a 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -3,12 +3,13 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.UI; -public class FileDialogService : IDisposable +public class FileDialogService : IDisposable, IUiService { private readonly CommunicatorService _communicator; private readonly FileDialogManager _manager; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 71164d1d..fb2028b5 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,13 +1,14 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; namespace Penumbra.UI; /// Draw the progress information for import. -public sealed class ImportPopup : Window +public sealed class ImportPopup : Window, IUiService { public const string WindowLabel = "Penumbra Import Status"; diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 9650ccf8..14e16432 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using OtterGui.Services; namespace Penumbra.UI; @@ -9,7 +10,7 @@ namespace Penumbra.UI; /// A Launch Button used in the title screen of the game, /// using the Dalamud-provided collapsible submenu. /// -public class LaunchButton : IDisposable +public class LaunchButton : IDisposable, IUiService { private readonly ConfigWindow _configWindow; private readonly UiBuilder _uiBuilder; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 58f0b615..0ca4d40c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -21,7 +22,7 @@ using MessageService = Penumbra.Services.MessageService; namespace Penumbra.UI.ModsTab; -public sealed class ModFileSystemSelector : FileSystemSelector +public sealed class ModFileSystemSelector : FileSystemSelector, IUiService { private readonly CommunicatorService _communicator; private readonly MessageService _messager; @@ -33,9 +34,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector().Aggregate((a, b) => a | b); - public ReadOnlySpan Label => "Changed Items"u8; - public ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) - { - _selector = selector; - _drawer = drawer; - } - public bool IsVisible - => _selector.Selected!.ChangedItems.Count > 0; + => selector.Selected!.ChangedItems.Count > 0; public void DrawContent() { - _drawer.DrawTypeFilter(); + drawer.DrawTypeFilter(); ImGui.Separator(); using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList((SortedList)selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -43,14 +33,14 @@ public class ModPanelChangedItemsTab : ITab } private bool CheckFilter((string Name, object? Data) kvp) - => _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); private void DrawChangedItem((string Name, object? Data) kvp) { ImGui.TableNextColumn(); - _drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Name, kvp.Data); ImGui.SameLine(); - _drawer.DrawChangedItem(kvp.Name, kvp.Data); - _drawer.DrawModelData(kvp.Data); + drawer.DrawChangedItem(kvp.Name, kvp.Data); + drawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index aa598557..9f37f847 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -10,25 +11,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelCollectionsTab : ITab +public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) : ITab, IUiService { - private readonly ModFileSystemSelector _selector; - private readonly CollectionStorage _collections; - - private readonly List<(ModCollection, ModCollection, uint, string)> _cache = new(); - - public ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) - { - _collections = storage; - _selector = selector; - } + private readonly List<(ModCollection, ModCollection, uint, string)> _cache = []; public ReadOnlySpan Label => "Collections"u8; public void DrawContent() { - var (direct, inherited) = CountUsage(_selector.Selected!); + var (direct, inherited) = CountUsage(selector.Selected!); ImGui.NewLine(); if (direct == 1) ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); @@ -80,7 +72,7 @@ public class ModPanelCollectionsTab : ITab var disInherited = ColorId.InheritedDisabledMod.Value(); var directCount = 0; var inheritedCount = 0; - foreach (var collection in _collections) + foreach (var collection in storage) { var (settings, parent) = collection[mod.Index]; var (color, text) = settings == null diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index c1a3c1eb..bee48068 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; @@ -16,7 +17,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab +public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab, IUiService { private int? _currentPriority; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index ed6340ab..6fe3e4c6 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods.Manager; @@ -12,7 +13,7 @@ public class ModPanelDescriptionTab( TutorialService tutorial, ModManager modManager, PredefinedTagManager predefinedTagsConfig) - : ITab + : ITab, IUiService { private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 468e97b9..1e371065 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -6,13 +6,13 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; -using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; @@ -31,7 +31,7 @@ public class ModPanelEditTab( ModGroupEditDrawer groupEditDrawer, DescriptionEditPopup descriptionPopup, AddGroupDrawer addGroupDrawer) - : ITab + : ITab, IUiService { private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 8b09d8b9..639118f5 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -9,7 +10,7 @@ using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; -public class ModPanelTabBar +public class ModPanelTabBar : IUiService { private enum ModPanelTabType { @@ -33,7 +34,7 @@ public class ModPanelTabBar public readonly ITab[] Tabs; private ModPanelTabType _preferredTab = ModPanelTabType.Settings; - private Mod? _lastMod = null; + private Mod? _lastMod; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, @@ -49,15 +50,15 @@ public class ModPanelTabBar _tutorial = tutorial; Collections = collections; - Tabs = new ITab[] - { + Tabs = + [ Settings, Description, Conflicts, ChangedItems, Collections, Edit, - }; + ]; } public void Draw(Mod mod) diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 595240f4..4079748e 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -3,12 +3,13 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) +public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) : IUiService { public void Draw() { @@ -65,8 +66,8 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito } private string _tag = string.Empty; - private readonly List _addMods = []; - private readonly List<(Mod, int)> _removeMods = []; + private readonly List _addMods = []; + private readonly List<(Mod, int)> _removeMods = []; private void DrawMultiTagger() { diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 0e5377d6..d531b1a2 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -6,14 +6,14 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; -using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class PredefinedTagManager : ISavable, IReadOnlyList +public sealed class PredefinedTagManager : ISavable, IReadOnlyList, IService { public const int Version = 1; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 65a8fe76..a7d1a8c6 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -15,7 +16,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; -public sealed class ResourceWatcher : IDisposable, ITab +public sealed class ResourceWatcher : IDisposable, ITab, IUiService { public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index ab0badf4..2aeaaea0 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -17,7 +18,7 @@ public class ChangedItemsTab( CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, CommunicatorService communicator) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Changed Items"u8; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index fabf7561..34e2cbcf 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -11,7 +12,7 @@ using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; -public sealed class CollectionsTab : IDisposable, ITab +public sealed class CollectionsTab : IDisposable, ITab, IUiService { private readonly EphemeralConfig _config; private readonly CollectionSelector _selector; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 9fd07f27..28827ad9 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; @@ -8,7 +9,7 @@ using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; namespace Penumbra.UI.Tabs; -public class ConfigTabBar : IDisposable +public class ConfigTabBar : IDisposable, IUiService { private readonly CommunicatorService _communicator; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 1519ebf0..0122a6f5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -44,7 +44,7 @@ using Penumbra.Api.IpcTester; namespace Penumbra.UI.Tabs.Debug; -public class Diagnostics(IServiceProvider provider) +public class Diagnostics(ServiceManager provider) : IUiService { public void DrawDiagnostics() { @@ -55,7 +55,7 @@ public class Diagnostics(IServiceProvider provider) foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { - var container = (IAsyncDataContainer)provider.GetRequiredService(type); + var container = (IAsyncDataContainer)provider.Provider!.GetRequiredService(type); ImGuiUtil.DrawTableColumn(container.Name); ImGuiUtil.DrawTableColumn(container.Time.ToString()); ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); @@ -64,7 +64,7 @@ public class Diagnostics(IServiceProvider provider) } } -public class DebugTab : Window, ITab +public class DebugTab : Window, ITab, IUiService { private readonly PerformanceTracker _performance; private readonly Configuration _config; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 7076b80f..1b9af75c 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -15,7 +16,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Effective Changes"u8; diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/Tabs/MessagesTab.cs index abaf2ba6..190f9407 100644 --- a/Penumbra/UI/Tabs/MessagesTab.cs +++ b/Penumbra/UI/Tabs/MessagesTab.cs @@ -1,9 +1,10 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Services; namespace Penumbra.UI.Tabs; -public class MessagesTab(MessageService messages) : ITab +public class MessagesTab(MessageService messages) : ITab, IUiService { public ReadOnlySpan Label => "Messages"u8; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index e4d94bb5..7faa3da8 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -6,6 +6,7 @@ using Penumbra.UI.Classes; using Dalamud.Interface; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop.Services; @@ -30,7 +31,7 @@ public class ModsTab( CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) - : ITab + : ITab, IUiService { private readonly ActiveCollections _activeCollections = collectionManager.Active; diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 787e07a1..fa33f702 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,16 +1,12 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.Tabs; -public class OnScreenTab : ITab +public class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab, IUiService { - private readonly ResourceTreeViewer _viewer; - - public OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) - { - _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); - } + private readonly ResourceTreeViewer _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); public ReadOnlySpan Label => "On-Screen"u8; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index bbb0561b..0b54c5e2 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; @@ -13,7 +14,7 @@ using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Resource Manager"u8; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 0de4f790..17db21c9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Interop.Services; @@ -19,7 +20,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.UI.Tabs; -public class SettingsTab : ITab +public class SettingsTab : ITab, IUiService { public const int RootDirectoryMaxLength = 64; diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index d87df19e..7d2a0d2a 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -40,7 +41,7 @@ public enum BasicTutorialSteps } /// Service for the in-game tutorial. -public class TutorialService +public class TutorialService : IUiService { public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index c5418eb3..99819fce 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -1,12 +1,13 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using OtterGui.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; -public class PenumbraWindowSystem : IDisposable +public class PenumbraWindowSystem : IDisposable, IUiService { private readonly UiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; From 90124e83df4dfc6c21c5a8107e0d749229df2309 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 19 Jun 2024 11:51:55 +0000 Subject: [PATCH 1776/2451] [CI] Updating repo.json for testing_1.1.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 318aafc2..fc7234bd 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.1", - "TestingAssemblyVersion": "1.1.1.1", + "TestingAssemblyVersion": "1.1.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a90e253c73b6bd6e68f37f911e7be3f2c5e0a69b Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:54:17 +0200 Subject: [PATCH 1777/2451] Update repo.json --- repo.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/repo.json b/repo.json index fc7234bd..c00e3c8f 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.1.1", + "AssemblyVersion": "1.1.1.2", "TestingAssemblyVersion": "1.1.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 29f8c91306daafc4bc5eab156015c895c8aa9a21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 19 Jun 2024 22:34:59 +0200 Subject: [PATCH 1778/2451] Make meta hooks respect Enable Mod setting and fix EQP composition. --- OtterGui | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 49 +++++++++++++++++-- Penumbra/Configuration.cs | 16 +++++- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EstHook.cs | 13 +++-- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 11 +++-- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 15 ++++-- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 8 ++- Penumbra/Interop/PathResolving/MetaState.cs | 8 +-- 12 files changed, 121 insertions(+), 33 deletions(-) diff --git a/OtterGui b/OtterGui index caa9e9b9..6fafc03b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65 +Subproject commit 6fafc03b34971be7c0e74fd9a638d1ed642ea19a diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 60e38aef..c681b230 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -9,11 +9,24 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public unsafe EqpEntry GetValues(CharacterArmor* armor) - => GetSingleValue(armor[0].Set, EquipSlot.Head) - | GetSingleValue(armor[1].Set, EquipSlot.Body) - | GetSingleValue(armor[2].Set, EquipSlot.Hands) - | GetSingleValue(armor[3].Set, EquipSlot.Legs) - | GetSingleValue(armor[4].Set, EquipSlot.Feet); + { + var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body); + var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead) + ? GetSingleValue(armor[0].Set, EquipSlot.Head) + : GetSingleValue(armor[1].Set, EquipSlot.Head); + var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand) + ? GetSingleValue(armor[2].Set, EquipSlot.Hands) + : GetSingleValue(armor[1].Set, EquipSlot.Hands); + var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg) + ? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3) + : (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1); + var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot) + ? GetSingleValue(armor[4].Set, EquipSlot.Feet) + : GetSingleValue(armor[legsId].Set, EquipSlot.Feet); + + var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry; + return PostProcessFeet(PostProcessHands(combined)); + } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) @@ -24,4 +37,30 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) protected override void Dispose(bool _) => Clear(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static EqpEntry PostProcessHands(EqpEntry entry) + { + if (!entry.HasFlag(EqpEntry.HandsHideForearm)) + return entry; + + var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow) + ? entry.HasFlag(EqpEntry.BodyHideGlovesL) + : entry.HasFlag(EqpEntry.BodyHideGlovesM); + return testFlag + ? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS + : entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static EqpEntry PostProcessFeet(EqpEntry entry) + { + if (!entry.HasFlag(EqpEntry.FeetHideCalf)) + return entry; + + if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20)) + return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM); + + return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS; + } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f6100b62..7faed6a2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -31,7 +31,21 @@ public class Configuration : IPluginConfiguration, ISavable, IService public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; - public bool EnableMods { get; set; } = true; + public event Action? ModsEnabled; + + [JsonIgnore] + private bool _enableMods = true; + + public bool EnableMods + { + get => _enableMods; + set + { + _enableMods = value; + ModsEnabled?.Invoke(value); + } + } + public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index aaaaccd4..583a2ac5 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -6,7 +6,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqdpAccessoryHook : FastHook +public unsafe class EqdpAccessoryHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); @@ -15,7 +15,8 @@ public unsafe class EqdpAccessoryHook : FastHook public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, true); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) @@ -27,4 +28,7 @@ public unsafe class EqdpAccessoryHook : FastHook Penumbra.Log.Excessive( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 2711f195..f5488f80 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -6,7 +6,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqdpEquipHook : FastHook +public unsafe class EqdpEquipHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); @@ -15,7 +15,8 @@ public unsafe class EqdpEquipHook : FastHook public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, true); + Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) @@ -27,4 +28,7 @@ public unsafe class EqdpEquipHook : FastHook Penumbra.Log.Excessive( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 7107e26b..e96d8115 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -5,7 +5,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqpHook : FastHook +public unsafe class EqpHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor); @@ -14,7 +14,8 @@ public unsafe class EqpHook : FastHook public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, true); + Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) @@ -31,4 +32,7 @@ public unsafe class EqpHook : FastHook Penumbra.Log.Excessive($"[GetEqpFlags] Invoked on 0x{(nint)utility:X} with 0x{(ulong)armor:X}, returned 0x{(ulong)*flags:X16}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 23931182..fa0bb3c5 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class EstHook : FastHook +public class EstHook : FastHook, IDisposable { public delegate EstEntry Delegate(uint id, int estType, uint genderRace); @@ -14,14 +14,16 @@ public class EstHook : FastHook public EstHook(HookManager hooks, MetaState metaState) { - _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, true); + _metaState = metaState; + Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private EstEntry Detour(uint genderRace, int estType, uint id) { EstEntry ret; - if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.EstCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) ret = entry.Entry; else @@ -46,4 +48,7 @@ public class EstHook : FastHook }; return new EstIdentifier(i, type, gr); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 256d8702..44d35f12 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -5,7 +5,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class GmpHook : FastHook +public unsafe class GmpHook : FastHook, IDisposable { public delegate nint Delegate(nint gmpResource, uint dividedHeadId); @@ -16,7 +16,8 @@ public unsafe class GmpHook : FastHook public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true); + Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } /// @@ -27,7 +28,8 @@ public unsafe class GmpHook : FastHook private nint Detour(nint gmpResource, uint dividedHeadId) { nint ret; - if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.GmpCollection.TryPeek(out var collection) + && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) { if (entry.Entry.Enabled) @@ -61,4 +63,7 @@ public unsafe class GmpHook : FastHook Marshal.FreeHGlobal((nint)Pointer); } } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index 86759460..db22d90c 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -7,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class RspBustHook : FastHook +public unsafe class RspBustHook : FastHook, IDisposable { public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize); @@ -19,7 +19,8 @@ public unsafe class RspBustHook : FastHook { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, true); + Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) @@ -63,4 +64,7 @@ public unsafe class RspBustHook : FastHook $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index cf88c34a..dcb3f19c 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,4 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; @@ -8,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class RspHeightHook : FastHook +public class RspHeightHook : FastHook, IDisposable { public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); @@ -17,15 +16,18 @@ public class RspHeightHook : FastHook public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) { - _metaState = metaState; + _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, true); + Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) { float scale; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 @@ -66,4 +68,7 @@ public class RspHeightHook : FastHook $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); return scale; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index e40f0161..8d333575 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -7,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class RspTailHook : FastHook +public class RspTailHook : FastHook, IDisposable { public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); @@ -18,7 +18,8 @@ public class RspTailHook : FastHook { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, true); + Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) @@ -65,4 +66,7 @@ public class RspTailHook : FastHook $"[GetRspTail] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {tailLength}, returned {scale}."); return scale; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index f7dcfc07..5eacbfb0 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -37,7 +37,7 @@ namespace Penumbra.Interop.PathResolving; // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. public sealed unsafe class MetaState : IDisposable, IService { - private readonly Configuration _config; + public readonly Configuration Config; private readonly CommunicatorService _communicator; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; @@ -64,7 +64,7 @@ public sealed unsafe class MetaState : IDisposable, IService _resources = resources; _createCharacterBase = createCharacterBase; _characterUtility = characterUtility; - _config = config; + Config = config; _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } @@ -84,7 +84,7 @@ public sealed unsafe class MetaState : IDisposable, IService } public DecalReverter ResolveDecal(ResolveData resolve, bool which) - => new(_config, _characterUtility, _resources, resolve, which); + => new(Config, _characterUtility, _resources, resolve, which); public void Dispose() { @@ -99,7 +99,7 @@ public sealed unsafe class MetaState : IDisposable, IService _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, + var decal = new DecalReverter(Config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); RspCollection.Push(_lastCreatedCollection); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. From f686a0ff09873a08eba20a332b27c0090834dbae Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 19 Jun 2024 20:37:08 +0000 Subject: [PATCH 1779/2451] [CI] Updating repo.json for testing_1.1.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c00e3c8f..03f13499 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.3", + "TestingAssemblyVersion": "1.1.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8cd8efa72347bda899a32baab1f878e55c3c7c9f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Jun 2024 14:24:35 +0200 Subject: [PATCH 1780/2451] Fix RSP scaling for NPC values. --- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index dcb3f19c..98e39061 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -29,6 +29,12 @@ public class RspHeightHook : FastHook, IDisposable && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { + // Special cases. + if (height == 0xFF) + return 1.0f; + if (height > 100) + height = 0; + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) From ab1e11aba1b42a67552d5557cc16402b41f0e4ad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Jun 2024 14:51:17 +0200 Subject: [PATCH 1781/2451] Improve support info a bit. --- Penumbra/Penumbra.cs | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 905b998d..38d9c7b2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using System.Xml.Linq; namespace Penumbra; @@ -175,6 +176,26 @@ public class Penumbra : IDalamudPlugin _disposed = true; } + private void GatherRelevantPlugins(StringBuilder sb) + { + ReadOnlySpan relevantPlugins = + [ + "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + ]; + var plugins = _services.GetService().InstalledPlugins + .GroupBy(p => p.InternalName) + .ToDictionary(g => g.Key, g => + { + var item = g.OrderByDescending(p => p.IsLoaded).ThenByDescending(p => p.Version).First(); + return (item.IsLoaded, item.Version, item.Name); + }); + foreach (var plugin in relevantPlugins) + { + if (plugins.TryGetValue(plugin, out var data)) + sb.Append($"> **`{data.Name + ':',-29}`** {data.Version}{(data.IsLoaded ? string.Empty : " (Disabled)")}\n"); + } + } + public string GatherSupportInformation() { var sb = new StringBuilder(10240); @@ -198,6 +219,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); + GatherRelevantPlugins(sb); sb.AppendLine("**Mods**"); sb.Append($"> **`Installed Mods: `** {_modManager.Count}\n"); sb.Append($"> **`Mods with Config: `** {_modManager.Count(m => m.HasOptions)}\n"); @@ -212,27 +234,25 @@ public class Penumbra : IDalamudPlugin $"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n"); void PrintCollection(ModCollection c, CollectionCache _) - => sb.Append($"**Collection {c.AnonymizedName}**\n" - + $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n" - + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" - + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); + => sb.Append( + $"> **`Collection {c.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); - sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); - sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); - sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); + sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); + sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); + sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { var collection = _collectionManager.Active.ByType(type); if (collection != null) - sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{name,-29}`** {collection.AnonymizedName}\n"); } foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) - sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.AnonymizedName}\n"); foreach (var collection in _collectionManager.Caches.Active) PrintCollection(collection, collection._cache!); From 045abc787d46bb472366a08bf22c020906ec5690 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 20 Jun 2024 12:53:23 +0000 Subject: [PATCH 1782/2451] [CI] Updating repo.json for testing_1.1.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 03f13499..3142f8d4 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.4", + "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b07af32deead6ee2c601ff26d1d8a5194ea3ae30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Jun 2024 23:04:09 +0200 Subject: [PATCH 1783/2451] Fix doubled hook. --- .../Resources/ResourceHandleDestructor.cs | 4 +++ .../ResourceLoading/ResourceService.cs | 28 +--------------- .../UI/ResourceWatcher/ResourceWatcher.cs | 32 +++++++++++-------- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 5ddb7eaa..ac3f504a 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -3,6 +3,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; +using Penumbra.UI.ResourceWatcher; namespace Penumbra.Interop.Hooks.Resources; @@ -15,6 +16,9 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr ShaderReplacementFixer, + + /// + ResourceWatcher, } public ResourceHandleDestructor(HookManager hooks) diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 54c86777..0947d2ec 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -26,7 +26,6 @@ public unsafe class ResourceService : IDisposable, IRequiredService interop.InitializeFromAttributes(this); _getResourceSyncHook.Enable(); _getResourceAsyncHook.Enable(); - _resourceHandleDestructorHook.Enable(); _incRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); @@ -51,7 +50,6 @@ public unsafe class ResourceService : IDisposable, IRequiredService { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); - _resourceHandleDestructorHook.Dispose(); _incRefHook.Dispose(); _decRefHook.Dispose(); } @@ -67,8 +65,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService /// Whether to request the resource synchronously or asynchronously. /// The returned resource handle. If this is not null, calling original will be skipped. public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, - Utf8GamePath original, - GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// /// Subscribers should be exception-safe. @@ -192,27 +189,4 @@ public unsafe class ResourceService : IDisposable, IRequiredService } #endregion - - #region Destructor - - /// Invoked before a resource handle is destructed. - /// The resource handle. - public delegate void ResourceHandleDtorDelegate(ResourceHandle* handle); - - /// - /// - /// Subscribers should be exception-safe. - /// - public event ResourceHandleDtorDelegate? ResourceHandleDestructor; - - [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] - private readonly Hook _resourceHandleDestructorHook = null!; - - private nint ResourceHandleDestructorDetour(ResourceHandle* handle) - { - ResourceHandleDestructor?.Invoke(handle); - return _resourceHandleDestructorHook.OriginalDisposeSafe(handle); - } - - #endregion } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index a7d1a8c6..3bf4cd88 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -8,8 +8,10 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -21,28 +23,30 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; - private readonly Configuration _config; - private readonly EphemeralConfig _ephemeral; - private readonly ResourceService _resources; - private readonly ResourceLoader _loader; - private readonly ActorManager _actors; - private readonly List _records = []; - private readonly ConcurrentQueue _newRecords = []; - private readonly ResourceWatcherTable _table; - private string _logFilter = string.Empty; - private Regex? _logRegex; - private int _newMaxEntries; + private readonly Configuration _config; + private readonly EphemeralConfig _ephemeral; + private readonly ResourceService _resources; + private readonly ResourceLoader _loader; + private readonly ResourceHandleDestructor _destructor; + private readonly ActorManager _actors; + private readonly List _records = []; + private readonly ConcurrentQueue _newRecords = []; + private readonly ResourceWatcherTable _table; + private string _logFilter = string.Empty; + private Regex? _logRegex; + private int _newMaxEntries; - public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader) + public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, ResourceHandleDestructor destructor) { _actors = actors; _config = config; _ephemeral = config.Ephemeral; _resources = resources; + _destructor = destructor; _loader = loader; _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; - _resources.ResourceHandleDestructor += OnResourceDestroyed; + _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _loader.ResourceLoaded += OnResourceLoaded; _loader.FileLoaded += OnFileLoaded; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); @@ -54,7 +58,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService Clear(); _records.TrimExcess(); _resources.ResourceRequested -= OnResourceRequested; - _resources.ResourceHandleDestructor -= OnResourceDestroyed; + _destructor.Unsubscribe(OnResourceDestroyed); _loader.ResourceLoaded -= OnResourceLoaded; _loader.FileLoaded -= OnFileLoaded; } From c2e74ed382c494272fd7ee1a7474ba33fb504c9b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jun 2024 22:50:52 +0200 Subject: [PATCH 1784/2451] Improve signatures. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EstHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 3 ++- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3fbc7045..4b55c05c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1 +Subproject commit 4b55c05c72eb194bec0d28c52cf076962010424b diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 583a2ac5..bfbe6866 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index f5488f80..6ea38ee2 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index e96d8115..19b870b0 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -14,7 +15,7 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index fa0bb3c5..3fc7080f 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public class EstHook : FastHook, IDisposable public EstHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 44d35f12..72c8d075 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -16,7 +17,7 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index db22d90c..d1019d3e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -19,7 +20,7 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 98e39061..d54fe31e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -18,7 +19,7 @@ public class RspHeightHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 8d333575..8aa7ea9f 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -18,7 +19,7 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } From 221b18751d092a8138d5e8087cf6d9143e47f25f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Jul 2024 17:08:27 +0200 Subject: [PATCH 1785/2451] Some updates. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/Api/RedrawApi.cs | 2 +- Penumbra/Api/Api/ResourceTreeApi.cs | 6 +- Penumbra/Api/IpcProviders.cs | 2 +- .../Api/IpcTester/CollectionsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/EditingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 7 +- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- .../Api/IpcTester/ModSettingsIpcTester.cs | 4 +- Penumbra/Api/IpcTester/ModsIpcTester.cs | 4 +- .../Api/IpcTester/PluginStateIpcTester.cs | 4 +- Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 4 +- Penumbra/Api/IpcTester/ResolveIpcTester.cs | 2 +- .../Api/IpcTester/ResourceTreeIpcTester.cs | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 2 +- Penumbra/Api/IpcTester/UiIpcTester.cs | 4 +- .../Manager/ActiveCollectionMigration.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/IndividualCollections.Access.cs | 2 +- .../Manager/IndividualCollections.Files.cs | 2 +- .../Collections/Manager/InheritanceManager.cs | 2 +- Penumbra/Configuration.cs | 2 +- Penumbra/EphemeralConfig.cs | 2 +- Penumbra/Import/Models/HavokConverter.cs | 5 +- Penumbra/Import/Textures/BaseImage.cs | 2 +- Penumbra/Import/Textures/TexFileParser.cs | 16 ++-- Penumbra/Import/Textures/Texture.cs | 2 +- Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Import/Textures/TextureManager.cs | 8 +- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Animation/CharacterBaseLoadAnimation.cs | 2 +- Penumbra/Interop/Hooks/Animation/Dismount.cs | 2 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 2 +- .../Hooks/Animation/LoadCharacterSound.cs | 9 +- .../Hooks/Animation/LoadCharacterVfx.cs | 2 +- .../Hooks/Animation/LoadTimelineResources.cs | 2 +- .../Interop/Hooks/Animation/PlayFootstep.cs | 2 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 2 +- .../Interop/Hooks/Animation/SomeActionLoad.cs | 10 +- .../Hooks/Animation/SomeMountAnimation.cs | 2 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Hooks/Animation/SomeParasolAnimation.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 8 ++ .../Interop/Hooks/Meta/CalculateHeight.cs | 3 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 2 +- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 2 +- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 2 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 3 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 2 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 6 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 26 ++--- .../PathResolving/CollectionResolver.cs | 4 +- .../Interop/PathResolving/CutsceneService.cs | 5 +- .../ResourceLoading/FileReadService.cs | 4 +- .../ResourceLoading/ResourceManagerService.cs | 33 ++----- .../ResourceLoading/ResourceService.cs | 2 +- .../Interop/ResourceLoading/TexMdlService.cs | 96 +++++++++++-------- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- .../ResourceTree/ResourceTreeApiHelper.cs | 6 +- .../ResourceTree/ResourceTreeFactory.cs | 21 ++-- .../Interop/ResourceTree/TreeBuildCache.cs | 20 ++-- Penumbra/Interop/Services/FontReloader.cs | 4 +- Penumbra/Interop/Services/RedrawService.cs | 59 ++++++------ Penumbra/Interop/Structs/ClipScheduler.cs | 4 +- Penumbra/Interop/Structs/ResourceHandle.cs | 8 +- Penumbra/Meta/Files/MetaBaseFile.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 3 +- Penumbra/Mods/Manager/ModImportManager.cs | 2 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Penumbra.csproj | 1 + Penumbra/Penumbra.json | 2 +- Penumbra/Services/DalamudConfigService.cs | 6 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/Services/MessageService.cs | 4 +- Penumbra/Services/StaticServiceManager.cs | 4 +- Penumbra/Services/ValidityChecker.cs | 12 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../ModEditWindow.Materials.Shpk.cs | 2 +- .../ModEditWindow.ShaderPackages.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 29 +++--- Penumbra/UI/CollectionTab/CollectionPanel.cs | 8 +- Penumbra/UI/ConfigWindow.cs | 4 +- Penumbra/UI/LaunchButton.cs | 25 ++--- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanel.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelHeader.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 11 +-- Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 4 +- Penumbra/UI/UiHelpers.cs | 2 +- Penumbra/UI/WindowSystem.cs | 4 +- repo.json | 2 +- 121 files changed, 338 insertions(+), 328 deletions(-) create mode 100644 Penumbra/Interop/Hooks/HookSettings.cs diff --git a/OtterGui b/OtterGui index 6fafc03b..437ef65c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6fafc03b34971be7c0e74fd9a638d1ed642ea19a +Subproject commit 437ef65c6464c54c8f40196dd2428da901d73aab diff --git a/Penumbra.Api b/Penumbra.Api index f1e4e520..43b0b47f 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f1e4e520daaa8f23e5c8b71d55e5992b8f6768e2 +Subproject commit 43b0b47f2d019af0fe4681dfc578f9232e3ba90c diff --git a/Penumbra.GameData b/Penumbra.GameData index 4b55c05c..3a97e5ae 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4b55c05c72eb194bec0d28c52cf076962010424b +Subproject commit 3a97e5aeee3b7375b333c1add5305d0ce80cbf83 diff --git a/Penumbra.String b/Penumbra.String index caa58c5c..f04abbab 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488 +Subproject commit f04abbabedf5e757c5cbb970f3e513fef23e53cf diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 03b42493..82d14f7b 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -13,7 +13,7 @@ public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiSe public void RedrawObject(string name, RedrawType setting) => redrawService.RedrawObject(name, setting); - public void RedrawObject(GameObject? gameObject, RedrawType setting) + public void RedrawObject(IGameObject? gameObject, RedrawType setting) => redrawService.RedrawObject(gameObject, setting); public void RedrawAll(RedrawType setting) diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs index 6e9aaa48..dcec99bf 100644 --- a/Penumbra/Api/Api/ResourceTreeApi.cs +++ b/Penumbra/Api/Api/ResourceTreeApi.cs @@ -12,7 +12,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana { public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); @@ -28,7 +28,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); @@ -45,7 +45,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 6b146c39..861225fa 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -12,7 +12,7 @@ public sealed class IpcProviders : IDisposable, IApiService private readonly EventProvider _disposedProvider; private readonly EventProvider _initializedProvider; - public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api) + public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api) { _disposedProvider = IpcSubscribers.Disposed.Provider(pi); _initializedProvider = IpcSubscribers.Initialized.Provider(pi); diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 2679bc69..026fabbc 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -13,7 +13,7 @@ using ImGuiClip = OtterGui.ImGuiClip; namespace Penumbra.Api.IpcTester; -public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService +public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService { private int _objectIdx; private string _collectionIdString = string.Empty; diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs index 94b1e4e8..a1001630 100644 --- a/Penumbra/Api/IpcTester/EditingIpcTester.cs +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -8,7 +8,7 @@ using Penumbra.Api.IpcSubscribers; namespace Penumbra.Api.IpcTester; -public class EditingIpcTester(DalamudPluginInterface pi) : IUiService +public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService { private string _inputPath = string.Empty; private string _inputPath2 = string.Empty; diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 93806162..04541a57 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class GameStateIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber CharacterBaseCreating; public readonly EventSubscriber CharacterBaseCreated; public readonly EventSubscriber GameObjectResourcePathResolved; @@ -30,7 +30,7 @@ public class GameStateIpcTester : IUiService, IDisposable private int _currentCutsceneParent; private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; - public GameStateIpcTester(DalamudPluginInterface pi) + public GameStateIpcTester(IDalamudPluginInterface pi) { _pi = pi; CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); @@ -134,7 +134,6 @@ public class GameStateIpcTester : IUiService, IDisposable private static unsafe string GetObjectName(nint gameObject) { var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; - var name = obj != null ? obj->Name : null; - return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; + return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown"; } } diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 3fa7de7f..8b393ade 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -6,7 +6,7 @@ using Penumbra.Api.IpcSubscribers; namespace Penumbra.Api.IpcTester; -public class MetaIpcTester(DalamudPluginInterface pi) : IUiService +public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService { private int _gameObjectIndex; diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index b117d603..23078576 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class ModSettingsIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber SettingChanged; private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; @@ -33,7 +33,7 @@ public class ModSettingsIpcTester : IUiService, IDisposable private IReadOnlyDictionary? _availableSettings; private Dictionary>? _currentSettings; - public ModSettingsIpcTester(DalamudPluginInterface pi) + public ModSettingsIpcTester(IDalamudPluginInterface pi) { _pi = pi; SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 2be51a80..a24861a3 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class ModsIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; private string _modDirectory = string.Empty; private string _modName = string.Empty; @@ -38,7 +38,7 @@ public class ModsIpcTester : IUiService, IDisposable private string _lastMovedModFrom = string.Empty; private string _lastMovedModTo = string.Empty; - public ModsIpcTester(DalamudPluginInterface pi) + public ModsIpcTester(IDalamudPluginInterface pi) { _pi = pi; DeleteSubscriber = ModDeleted.Subscriber(pi, s => diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index 984f17b1..df82033d 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class PluginStateIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber ModDirectoryChanged; public readonly EventSubscriber Initialized; public readonly EventSubscriber Disposed; @@ -29,7 +29,7 @@ public class PluginStateIpcTester : IUiService, IDisposable private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; private bool? _lastEnabledValue; - public PluginStateIpcTester(DalamudPluginInterface pi) + public PluginStateIpcTester(IDalamudPluginInterface pi) { _pi = pi; ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index 801f0b97..b862dde5 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -13,14 +13,14 @@ namespace Penumbra.Api.IpcTester; public class RedrawingIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; private readonly ObjectManager _objects; public readonly EventSubscriber Redrawn; private int _redrawIndex; private string _lastRedrawnString = "None"; - public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) + public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects) { _pi = pi; _objects = objects; diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs index 978ed8d6..a79b099d 100644 --- a/Penumbra/Api/IpcTester/ResolveIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -7,7 +7,7 @@ using Penumbra.String.Classes; namespace Penumbra.Api.IpcTester; -public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService +public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService { private string _currentResolvePath = string.Empty; private string _currentReversePath = string.Empty; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 1f57fc9d..088a77bd 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -15,7 +15,7 @@ using Penumbra.GameData.Structs; namespace Penumbra.Api.IpcTester; -public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService +public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService { private readonly Stopwatch _stopwatch = new(); diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 0aa6821c..6d4f17b2 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -17,7 +17,7 @@ using Penumbra.Services; namespace Penumbra.Api.IpcTester; public class TemporaryIpcTester( - DalamudPluginInterface pi, + IDalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index a2c36938..647a4dda 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -10,7 +10,7 @@ namespace Penumbra.Api.IpcTester; public class UiIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber PreSettingsTabBar; public readonly EventSubscriber PreSettingsPanel; public readonly EventSubscriber PostEnabled; @@ -28,7 +28,7 @@ public class UiIpcTester : IUiService, IDisposable private string _modName = string.Empty; private PenumbraApiEc _ec = PenumbraApiEc.Success; - public UiIpcTester(DalamudPluginInterface pi) + public UiIpcTester(IDalamudPluginInterface pi) { _pi = pi; PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 2f9e9b15..19f781fc 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 6d48f74b..60f9a427 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index cd680d36..a326fb92 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 785f0013..6b90a333 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -127,7 +127,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa } } - public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection) + public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection) => TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection); public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 8a717b35..f7a26384 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index f3482cdf..bc1a362c 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 7faed6a2..49aecfdc 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,5 +1,5 @@ using Dalamud.Configuration; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 52e276c7..7457c910 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index dc9d3e6a..e3797083 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -1,4 +1,7 @@ -using FFXIVClientStructs.Havok; +using FFXIVClientStructs.Havok.Common.Base.System.IO.OStream; +using FFXIVClientStructs.Havok.Common.Base.Types; +using FFXIVClientStructs.Havok.Common.Serialize.Resource; +using FFXIVClientStructs.Havok.Common.Serialize.Util; namespace Penumbra.Import.Models; diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs index a4a0e203..eba2d8ba 100644 --- a/Penumbra/Import/Textures/BaseImage.cs +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -103,7 +103,7 @@ public readonly struct BaseImage : IDisposable { null => 0, ScratchImage s => s.Meta.MipLevels, - TexFile t => t.Header.MipLevelsCount, + TexFile t => t.Header.MipCount, _ => 1, }; } diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 6f854022..09025b61 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,8 +79,8 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write(header.MipLevelsCount); - w.Write((byte)0); // TODO Lumina Update + w.Write(header.MipCount); + w.Write(header.MipUnknownFlag); // TODO Lumina Update unsafe { w.Write(header.LodOffset[0]); @@ -96,11 +96,11 @@ public static class TexFileParser var meta = scratch.Meta; var ret = new TexFile.TexHeader() { - Height = (ushort)meta.Height, - Width = (ushort)meta.Width, - Depth = (ushort)Math.Max(meta.Depth, 1), - MipLevelsCount = (byte)Math.Min(meta.MipLevels, 13), - Format = meta.Format.ToTexFormat(), + Height = (ushort)meta.Height, + Width = (ushort)meta.Width, + Depth = (ushort)Math.Max(meta.Depth, 1), + MipCount = (byte)Math.Min(meta.MipLevels, 13), + Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, @@ -143,7 +143,7 @@ public static class TexFileParser Height = header.Height, Width = header.Width, Depth = Math.Max(header.Depth, (ushort)1), - MipLevels = header.MipLevelsCount, + MipLevels = header.MipCount, ArraySize = 1, Format = header.Format.ToDXGI(), Dimension = header.Type.ToDimension(), diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c4d6dc56..c5207e94 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using OtterTex; namespace Penumbra.Import.Textures; diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 427db92d..bd95d1ab 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -105,7 +105,7 @@ public static class TextureDrawer ImGuiUtil.DrawTableColumn("Format"); ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipLevelsCount.ToString()); + ImGuiUtil.DrawTableColumn(t.Header.MipCount.ToString()); ImGuiUtil.DrawTableColumn("Data Size"); ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); break; diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 4aa64209..cc785d02 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,5 +1,5 @@ -using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Lumina.Data.Files; using OtterGui.Log; @@ -13,7 +13,7 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) +public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider) : SingleTaskQueue, IDisposable, IService { private readonly Logger _logger = logger; @@ -211,7 +211,7 @@ public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, L /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) - => uiBuilder.LoadImageRaw(rgba, width, height, 4); + => textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), rgba, "Penumbra.Texture"); /// Load any supported file from game data or drive depending on extension and if the path is rooted. public (BaseImage, TextureType) Load(string path) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 77927593..e58c7268 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -21,7 +21,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); + Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs index df25358e..f99d8ca4 100644 --- a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -26,7 +26,7 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawBase); diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 8085bcdb..523e750c 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -15,7 +15,7 @@ public sealed unsafe class Dismount : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, true); + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(nint a1, nint a2); diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 1a78d3b4..0f51157c 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -20,7 +20,7 @@ public sealed unsafe class LoadAreaVfx : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index 98454a77..ed04880e 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.CrashHandler.Buffers; @@ -15,12 +16,10 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", - (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, - true); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 77aaa742..af801345 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -26,7 +26,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 018892a0..4e9037bd 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -30,7 +30,7 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); } public delegate ulong Delegate(nint timeline); diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs index 491d7662..e4a8c83c 100644 --- a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -14,7 +14,7 @@ public sealed unsafe class PlayFootstep : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, true); + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(GameObject* gameObject, int id, int unk); diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index 342ffc25..645b3565 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -23,7 +23,7 @@ public sealed unsafe class ScheduleClipUpdate : FastHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(ClipScheduler* x); diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 6de3aeb0..1f3c0e3b 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.CrashHandler.Buffers; @@ -20,15 +20,15 @@ public sealed unsafe class SomeActionLoad : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, true); + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, HookSettings.VfxIdentificationHooks); } - public delegate void Delegate(ActionTimelineManager* timelineManager); + public delegate void Delegate(TimelineContainer* timelineManager); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(ActionTimelineManager* timelineManager) + private void Detour(TimelineContainer* timelineManager) { - var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); + var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->OwnerObject, true); var last = _state.SetAnimationData(newData); Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}."); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad); diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs index 5dd8227d..f2b48afe 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeMountAnimation : FastHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, true); + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index fad1f819..8f952df5 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -22,7 +22,7 @@ public sealed unsafe class SomePapLoad : FastHook _collectionResolver = collectionResolver; _objects = objects; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, true); + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(nint a1, int a2, nint a3, int a4); diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs index ab4a7201..165bd5eb 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeParasolAnimation : FastHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, true); + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawObject, int unk1); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs new file mode 100644 index 00000000..6d511a28 --- /dev/null +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Interop.Hooks; + +public static class HookSettings +{ + public const bool MetaEntryHooks = false; + public const bool MetaParentHooks = false; + public const bool VfxIdentificationHooks = false; +} diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 7936b831..aab64871 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.Interop.PathResolving; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -15,7 +14,7 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, true); + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, HookSettings.MetaParentHooks); } public delegate ulong Delegate(Character* character); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index f589cf4e..f69e98e7 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, true); + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, HookSettings.MetaParentHooks); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index bfbe6866..63cca53f 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -16,7 +16,7 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 6ea38ee2..5d5d2f84 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -16,7 +16,7 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 19b870b0..f47db795 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -15,7 +15,7 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 3fc7080f..5b272019 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -16,7 +16,7 @@ public class EstHook : FastHook, IDisposable public EstHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index a10b511a..8bd49500 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -15,7 +15,7 @@ public sealed unsafe class GetEqpIndirect : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, true); + Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 30ec2597..3767c4a2 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -15,7 +15,7 @@ public sealed unsafe class GetEqpIndirect2 : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, true); + Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 72c8d075..329a8beb 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -17,7 +17,7 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 2c17362d..79e7f6a6 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; @@ -14,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, true); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index d1019d3e..eb8a8a37 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -20,7 +20,7 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index d54fe31e..f8f9e51e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -19,7 +19,7 @@ public class RspHeightHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 58856f52..8bcc7593 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class RspSetupCharacter : FastHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, true); + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 8aa7ea9f..86d21c6f 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -19,7 +19,7 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 82b24dc4..83c0e0c4 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -19,7 +19,7 @@ public sealed unsafe class SetupVisor : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, true); + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, HookSettings.MetaParentHooks); } public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index 76854bca..a088a0f2 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -15,7 +15,7 @@ public sealed unsafe class UpdateModel : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, true); + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 7b730f84..20c96f56 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -20,7 +20,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr> _task; public nint Address - => (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter; + => (nint)CharacterSetupContainer.MemberFunctionPointers.CopyFromCharacter; public void Enable() => _task.Result.Enable(); @@ -34,9 +34,9 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task.IsCompletedSuccessfully; - private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk); + private delegate ulong Delegate(CharacterSetupContainer* target, Character* source, uint unk); - private ulong Detour(CharacterSetup* target, Character* source, uint unk) + private ulong Detour(CharacterSetupContainer* target, Character* source, uint unk) { // TODO: update when CS updated. var character = ((Character**)target)[1]; diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index 31c6b883..fec0a13f 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -39,7 +39,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtrParent; + var gameObject = drawData->OwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); _task.Result.Original(drawData, slot, weapon, d, e, f, g); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 5941773f..a7e82b72 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -44,18 +44,20 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable { _parent = parent; // @formatter:off - _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal); - _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid); - _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc); - _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap); - _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman); - _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl); - _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman); - _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman); - _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman); - _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); - _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); + _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); + _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); + _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); + _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); + + + _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); + _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); + _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); + _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); + _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); + _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); + _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); // @formatter:on Enable(); } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index bc474952..136da0f5 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -120,7 +120,7 @@ public sealed unsafe class CollectionResolver( var lobby = AgentLobby.Instance(); if (lobby != null) { - var span = lobby->LobbyData.CharaSelectEntries.Span; + var span = lobby->LobbyData.CharaSelectEntries.AsSpan(); // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (idx >= 0 && idx < span.Length && span[idx].Value != null) @@ -148,7 +148,7 @@ public sealed unsafe class CollectionResolver( /// Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. private bool Aesthetician(GameObject* gameObject, out ResolveData ret) { - if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) + if (gameGui.GetAddonByName("ScreenLog") != nint.Zero) { ret = ResolveData.Invalid; return false; diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index feb27341..8e32dd76 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; @@ -19,7 +20,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable private readonly CharacterDestructor _characterDestructor; private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); - public IEnumerable> Actors + public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) .Where(i => _objects[i].Valid) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); @@ -42,7 +43,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable /// Does not check for valid input index. /// Returns null if no connected actor is set or the actor does not exist anymore. /// - private Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] + private IGameObject? this[int idx] { get { diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index f1d7fe24..5edba790 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -63,7 +63,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService byte? ret = null; _lastFileThreadResourceManager.Value = resourceManager; ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret); - _lastFileThreadResourceManager.Value = IntPtr.Zero; + _lastFileThreadResourceManager.Value = nint.Zero; return ret ?? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); } @@ -82,7 +82,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService /// So we keep track of them per thread and use them. /// private nint GetResourceManager() - => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero + => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == nint.Zero ? (nint)_resourceManager.ResourceManager : _lastFileThreadResourceManager.Value; } diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index c885c317..0479d2a6 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -25,8 +25,8 @@ public unsafe class ResourceManagerService : IRequiredService ref var manager = ref *ResourceManager; var catIdx = (uint)cat >> 0x18; cat = (ResourceCategory)(ushort)cat; - ref var category = ref manager.ResourceGraph->ContainerArraySpan[(int)cat]; - var extMap = FindInMap(category.CategoryMapsSpan[(int)catIdx].Value, (uint)ext); + ref var category = ref manager.ResourceGraph->Containers[(int)cat]; + var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); if (extMap == null) return null; @@ -44,10 +44,10 @@ public unsafe class ResourceManagerService : IRequiredService ref var manager = ref *ResourceManager; foreach (var resourceType in Enum.GetValues().SkipLast(1)) { - ref var graph = ref manager.ResourceGraph->ContainerArraySpan[(int)resourceType]; + ref var graph = ref manager.ResourceGraph->Containers[(int)resourceType]; for (var i = 0; i < 20; ++i) { - var map = graph.CategoryMapsSpan[i]; + var map = graph.CategoryMaps[i]; if (map.Value != null) action(resourceType, map, i); } @@ -79,25 +79,10 @@ public unsafe class ResourceManagerService : IRequiredService where TKey : unmanaged, IComparable where TValue : unmanaged { - if (map == null || map->Count == 0) + if (map == null) return null; - var node = map->Head->Parent; - while (!node->IsNil) - { - switch (key.CompareTo(node->KeyValuePair.Item1)) - { - case 0: return &node->KeyValuePair.Item2; - case < 0: - node = node->Left; - break; - default: - node = node->Right; - break; - } - } - - return null; + return map->TryGetValuePointer(key, out var val) ? val : null; } // Iterate in tree-order through a map, applying action to each KeyValuePair. @@ -105,10 +90,10 @@ public unsafe class ResourceManagerService : IRequiredService where TKey : unmanaged where TValue : unmanaged { - if (map == null || map->Count == 0) + if (map == null) return; - for (var node = map->SmallestValue; !node->IsNil; node = node->Next()) - action(node->KeyValuePair.Item1, node->KeyValuePair.Item2); + foreach (var (key, value) in *map) + action(key, value); } } diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 0947d2ec..d623d72d 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -127,7 +127,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService #endregion - private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); + private delegate nint ResourceHandlePrototype(ResourceHandle* handle); #region IncRef diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index e617673e..a2b43c64 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,6 +1,7 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; using Penumbra.Api.Enums; @@ -12,88 +13,99 @@ namespace Penumbra.Interop.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. - public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); - + public static readonly nint CustomFileFlag = new(0xDEADBEEF); + /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// public IReadOnlySet CustomFileCrc => _customFileCrc; - + public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - _checkFileStateHook.Enable(); - _loadTexFileExternHook.Enable(); - _loadMdlFileExternHook.Enable(); + //_checkFileStateHook.Enable(); + //_loadTexFileExternHook.Enable(); + //_loadMdlFileExternHook.Enable(); } - + /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex) _customFileCrc.Add(path.Value.Crc64); } - + /// Add a fixed CRC64 value. public void AddCrc(ulong crc64) => _customFileCrc.Add(crc64); - + public void Dispose() { - _checkFileStateHook.Dispose(); - _loadTexFileExternHook.Dispose(); - _loadMdlFileExternHook.Dispose(); + //_checkFileStateHook.Dispose(); + //_loadTexFileExternHook.Dispose(); + //_loadMdlFileExternHook.Dispose(); } - + private readonly HashSet _customFileCrc = new(); - - private delegate IntPtr CheckFileStatePrototype(IntPtr unk1, ulong crc64); - + + private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); + [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. /// - private IntPtr CheckFileStateDetour(IntPtr ptr, ulong crc64) + private nint CheckFileStateDetour(nint ptr, ulong crc64) => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); - - - private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3); - + + + private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!; - - private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2); - + + private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, nint unk1, bool unk2); + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; - - - private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4); - - [Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] - private readonly Hook _loadTexFileExternHook = null!; - + + + private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + + private delegate byte TexResourceHandleVf32Prototype(ResourceHandle* handle, nint unk1, byte unk2); + + //[Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] + //private readonly Hook _vf32Hook = null!; + // + //private byte Vf32Detour(ResourceHandle* handle, nint unk1, byte unk2) + //{ + // var ret = _vf32Hook.Original(handle, unk1, unk2); + // return _loadTexFileLocal() + //} + + //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] + //private readonly Hook _loadTexFileExternHook = null!; + /// We hook the extern functions to just return the local one if given the custom flag as last argument. - private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr) - => ptr.Equals(CustomFileFlag) - ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) - : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3); - - + //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) + // => ptr.Equals(CustomFileFlag) + // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) + // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); + + public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); + + [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. - private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr) + private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, nint unk1, bool unk2, nint ptr) => ptr.Equals(CustomFileFlag) ? _loadMdlFileLocal.Invoke(resourceHandle, unk1, unk2) : _loadMdlFileExternHook.Original(resourceHandle, unk1, unk2, ptr); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index e38bf4f6..ca8836b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -111,7 +111,7 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fileName = resourceHandle->FileName.AsSpan(); + var fileName = (ReadOnlySpan) resourceHandle->FileName.AsSpan(); var additionalData = ByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index fc8c805a..b8bad84a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -62,7 +62,7 @@ public class ResourceTree var equipment = modelType switch { CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(&character->DrawData.Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => ReadOnlySpan.Empty, }; ModelId = character->CharacterData.ModelCharaId; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 0d1e3abc..22025dd6 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.ResourceTree; internal static class ResourceTreeApiHelper { public static Dictionary>> GetResourcePathDictionaries( - IEnumerable<(Character, ResourceTree)> resourceTrees) + IEnumerable<(ICharacter, ResourceTree)> resourceTrees) { var pathDictionaries = new Dictionary>>(4); @@ -47,7 +47,7 @@ internal static class ResourceTreeApiHelper } } - public static Dictionary GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + public static Dictionary GetResourcesOfType(IEnumerable<(ICharacter, ResourceTree)> resourceTrees, ResourceType type) { var resDictionaries = new Dictionary(4); @@ -74,7 +74,7 @@ internal static class ResourceTreeApiHelper return resDictionaries; } - public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary EncapsulateResourceTrees(IEnumerable<(ICharacter, ResourceTree)> resourceTrees) { var resDictionary = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index e26c1436..1f6d1f6f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; @@ -23,13 +24,13 @@ public class ResourceTreeFactory( private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); - public IEnumerable GetLocalPlayerRelatedCharacters() + public IEnumerable GetLocalPlayerRelatedCharacters() { var cache = CreateTreeBuildCache(); return cache.GetLocalPlayerRelatedCharacters(); } - public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( + public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromObjectTable( Flags flags) { var cache = CreateTreeBuildCache(); @@ -43,8 +44,8 @@ public class ResourceTreeFactory( } } - public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( - IEnumerable characters, Flags flags) + public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromCharacters( + IEnumerable characters, Flags flags) { var cache = CreateTreeBuildCache(); foreach (var character in characters) @@ -55,10 +56,10 @@ public class ResourceTreeFactory( } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, Flags flags) + public ResourceTree? FromCharacter(ICharacter character, Flags flags) => FromCharacter(character, CreateTreeBuildCache(), flags); - private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, Flags flags) + private unsafe ResourceTree? FromCharacter(ICharacter character, TreeBuildCache cache, Flags flags) { if (!character.IsValid()) return null; @@ -74,7 +75,7 @@ public class ResourceTreeFactory( var localPlayerRelated = cache.IsLocalPlayerRelated(character); var (name, anonymizedName, related) = GetCharacterName(character); - var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; + var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, @@ -155,14 +156,14 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(ICharacter character) { var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); var identifierStr = identifier.ToString(); return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } - private unsafe bool IsPlayerRelated(Dalamud.Game.ClientState.Objects.Types.Character? character) + private unsafe bool IsPlayerRelated(ICharacter? character) { if (character == null) return false; @@ -175,7 +176,7 @@ public class ResourceTreeFactory( => identifier.Type switch { IdentifierType.Player => true, - IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character), + IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as ICharacter), _ => false, }; diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 2798002a..ca5ff736 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -13,7 +13,7 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data { private readonly Dictionary _shaderPackages = []; - public unsafe bool IsLocalPlayerRelated(Character character) + public unsafe bool IsLocalPlayerRelated(ICharacter character) { var player = objects.GetDalamudObject(0); if (player == null) @@ -25,36 +25,36 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data return actualIndex switch { < 2 => true, - < (int)ScreenActor.CutsceneStart => gameObject->OwnerID == player.ObjectId, + < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == player.EntityId, _ => false, }; } - public IEnumerable GetCharacters() - => objects.Objects.OfType(); + public IEnumerable GetCharacters() + => objects.Objects.OfType(); - public IEnumerable GetLocalPlayerRelatedCharacters() + public IEnumerable GetLocalPlayerRelatedCharacters() { var player = objects.GetDalamudObject(0); if (player == null) yield break; - yield return (Character)player; + yield return (ICharacter)player; var minion = objects.GetDalamudObject(1); if (minion != null) - yield return (Character)minion; + yield return (ICharacter)minion; - var playerId = player.ObjectId; + var playerId = player.EntityId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { - if (objects.GetDalamudObject(i) is Character owned && owned.OwnerId == playerId) + if (objects.GetDalamudObject(i) is ICharacter owned && owned.OwnerId == playerId) yield return owned; } for (var i = ObjectIndex.CutsceneStart.Index; i < ObjectIndex.CharacterScreen.Index; ++i) { - var character = objects.GetDalamudObject((int) i) as Character; + var character = objects.GetDalamudObject((int) i) as ICharacter; if (character == null) continue; diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 259fdd10..4f48f08f 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -34,7 +34,7 @@ public unsafe class FontReloader : IService if (framework == null) return; - var uiModule = framework->GetUiModule(); + var uiModule = framework->GetUIModule(); if (uiModule == null) return; @@ -43,7 +43,7 @@ public unsafe class FontReloader : IService return; _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[Offsets.ReloadFontsVfunc]; }); } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 61d7b90c..163b2c0e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -4,8 +4,8 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Housing; -using FFXIVClientStructs.Interop; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; @@ -37,11 +37,11 @@ public unsafe partial class RedrawService : IService => _clientState.IsGPosing; // VFuncs that disable and enable draw, used only for GPose actors. - private static void DisableDraw(GameObject actor) - => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); + private static void DisableDraw(IGameObject actor) + => ((delegate* unmanaged**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); - private static void EnableDraw(GameObject actor) - => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + private static void EnableDraw(IGameObject actor) + => ((delegate* unmanaged**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); // Check whether we currently are in GPose. // Also clear the name list. @@ -57,7 +57,7 @@ public unsafe partial class RedrawService : IService // 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) + private bool FindCorrectActor(int idx, out IGameObject? obj) { obj = _objects.GetDalamudObject(idx); if (!InGPose || obj == null || IsGPoseActor(idx)) @@ -91,11 +91,11 @@ public unsafe partial class RedrawService : IService } } - return obj; + return false; } // Do not ever redraw any of the five UI Window actors. - private static bool BadRedrawIndices(GameObject? actor, out int tableIndex) + private static bool BadRedrawIndices(IGameObject? actor, out int tableIndex) { if (actor == null) { @@ -155,13 +155,13 @@ public sealed unsafe partial class RedrawService : IDisposable _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } - public static DrawState* ActorDrawState(GameObject actor) + public static DrawState* ActorDrawState(IGameObject actor) => (DrawState*)(&((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->RenderFlags); - private static int ObjectTableIndex(GameObject actor) + private static int ObjectTableIndex(IGameObject actor) => ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->ObjectIndex; - private void WriteInvisible(GameObject? actor) + private void WriteInvisible(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -172,7 +172,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter + if (actor is IPlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; @@ -181,7 +181,7 @@ public sealed unsafe partial class RedrawService : IDisposable } } - private void WriteVisible(GameObject? actor) + private void WriteVisible(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -192,7 +192,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter + if (actor is IPlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; @@ -203,7 +203,7 @@ public sealed unsafe partial class RedrawService : IDisposable GameObjectRedrawn?.Invoke(actor!.Address, tableIndex); } - private void ReloadActor(GameObject? actor) + private void ReloadActor(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -214,7 +214,7 @@ public sealed unsafe partial class RedrawService : IDisposable _queue.Add(~tableIndex); } - private void ReloadActorAfterGPose(GameObject? actor) + private void ReloadActorAfterGPose(IGameObject? actor) { if (_objects[GPosePlayerIdx].Valid) { @@ -284,21 +284,21 @@ public sealed unsafe partial class RedrawService : IDisposable _queue.RemoveRange(numKept, _queue.Count - numKept); } - private static uint GetCurrentAnimationId(GameObject obj) + private static uint GetCurrentAnimationId(IGameObject obj) { var gameObj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address; if (gameObj == null || !gameObj->IsCharacter()) return 0; var chara = (Character*)gameObj; - var ptr = (byte*)&chara->ActionTimelineManager + 0xF0; + var ptr = (byte*)&chara->Timeline + 0xF0; return *(uint*)ptr; } - private static bool DelayRedraw(GameObject obj) + private static bool DelayRedraw(IGameObject obj) => ((Character*)obj.Address)->Mode switch { - (Character.CharacterModes)6 => // fishing + (CharacterModes)6 => // fishing GetCurrentAnimationId(obj) switch { 278 => true, // line out. @@ -345,7 +345,7 @@ public sealed unsafe partial class RedrawService : IDisposable HandleTarget(); } - public void RedrawObject(GameObject? actor, RedrawType settings) + public void RedrawObject(IGameObject? actor, RedrawType settings) { switch (settings) { @@ -359,13 +359,13 @@ public sealed unsafe partial class RedrawService : IDisposable } } - private GameObject? GetLocalPlayer() + private IGameObject? GetLocalPlayer() { var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx); return gPosePlayer ?? _objects.GetDalamudObject(0); } - public bool GetName(string lowerName, out GameObject? actor) + public bool GetName(string lowerName, out IGameObject? actor) { (actor, var ret) = lowerName switch { @@ -419,15 +419,14 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = housingManager->CurrentTerritory; - if (currentTerritory == null) - return; - if (!housingManager->IsInside()) + var currentTerritory = (OutdoorTerritory*) housingManager->CurrentTerritory; + if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) return; - foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) + + foreach (ref var f in currentTerritory->Furniture) { - var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; + var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null; if (gameObject == null) continue; diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index 3211c4f9..44a905b8 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -4,8 +4,8 @@ namespace Penumbra.Interop.Structs; public unsafe struct ClipScheduler { [FieldOffset(0)] - public IntPtr* VTable; + public nint* VTable; [FieldOffset(0x38)] - public IntPtr SchedulerTimeline; + public nint SchedulerTimeline; } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 382368b4..058b9004 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -83,12 +83,12 @@ public unsafe struct ResourceHandle [FieldOffset(0xB8)] public uint DataLength; - public (IntPtr Data, int Length) GetData() + public (nint Data, int Length) GetData() => Data != null - ? ((IntPtr)Data->DataPtr, (int)Data->DataLength) - : (IntPtr.Zero, 0); + ? ((nint)Data->DataPtr, (int)Data->DataLength) + : (nint.Zero, 0); - public bool SetData(IntPtr data, int length) + public bool SetData(nint data, int length) { if (Data == null) return false; diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 86a55101..5bc36068 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -71,7 +71,7 @@ public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, public int Length { get; private set; } public CharacterUtility.InternalIndex Index { get; } = CharacterUtility.ReverseIndices[(int)idx]; - protected (IntPtr Data, int Length) DefaultData + protected (nint Data, int Length) DefaultData => Manager.CharacterUtility.DefaultResource(Index); /// Reset to default values. diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 9d31664b..b059813b 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index c6bc4939..c0876f5d 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index c52828c0..46204d6c 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 220d0a7c..95f49230 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -1,10 +1,9 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index ff39b021..39a53bb9 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Import; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 594ec9d2..712630c6 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0e66367a..546f5f5c 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 38d9c7b2..b1ad0b78 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -48,7 +48,7 @@ public class Penumbra : IDalamudPlugin private readonly ServiceManager _services; - public Penumbra(DalamudPluginInterface pluginInterface) + public Penumbra(IDalamudPluginInterface pluginInterface) { try { @@ -182,7 +182,7 @@ public class Penumbra : IDalamudPlugin [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", ]; - var plugins = _services.GetService().InstalledPlugins + var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) .ToDictionary(g => g.Key, g => { diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2e53bd22..ed5c5e30 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -37,6 +37,7 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\ + H:\Projects\FFPlugins\Dalamud\bin\Release\ diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 85e01c84..805f4d85 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "LoadPriority": 69420, "LoadState": 2, "LoadSync": true, diff --git a/Penumbra/Services/DalamudConfigService.cs b/Penumbra/Services/DalamudConfigService.cs index 8379a3e7..012a45f5 100644 --- a/Penumbra/Services/DalamudConfigService.cs +++ b/Penumbra/Services/DalamudConfigService.cs @@ -10,9 +10,9 @@ public class DalamudConfigService : IService try { var serviceType = - typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); - var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); - var interfaceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); + typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); + var configType = typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); + var interfaceType = typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); if (serviceType == null || configType == null || interfaceType == null) return; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index e1c482f7..817af0d2 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -5,7 +5,7 @@ using Penumbra.Mods; namespace Penumbra.Services; -public class FilenameService(DalamudPluginInterface pi) : IService +public class FilenameService(IDalamudPluginInterface pi) : IService { public readonly string ConfigDirectory = pi.ConfigDirectory.FullName; public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 0a85a569..08118483 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -9,8 +9,8 @@ using OtterGui.Services; namespace Penumbra.Services; -public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat, INotificationManager notificationManager) - : OtterGui.Classes.MessageService(log, uiBuilder, chat, notificationManager), IService +public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) + : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { public void LinkItem(Item item) { diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 3279da96..c0dc9314 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -19,7 +19,7 @@ namespace Penumbra.Services; public static class StaticServiceManager { - public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(Penumbra penumbra, IDalamudPluginInterface pi, Logger log) { var services = new ServiceManager(log) .AddDalamudServices(pi) @@ -40,7 +40,7 @@ public static class StaticServiceManager return services; } - private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi) + private static ServiceManager AddDalamudServices(this ServiceManager services, IDalamudPluginInterface pi) => services.AddExistingService(pi) .AddExistingService(pi.UiBuilder) .AddDalamudService(pi) diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index cc70306b..cefee139 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Client.System.Framework; using OtterGui.Classes; @@ -27,11 +27,11 @@ public class ValidityChecker : IService get { var framework = Framework.Instance(); - return framework == null ? string.Empty : framework->GameVersion[0]; + return framework == null ? string.Empty : framework->GameVersionString; } } - public ValidityChecker(DalamudPluginInterface pi) + public ValidityChecker(IDalamudPluginInterface pi) { DevPenumbraExists = CheckDevPluginPenumbra(pi); IsNotInstalledPenumbra = CheckIsNotInstalled(pi); @@ -50,7 +50,7 @@ public class ValidityChecker : IService } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. - private static bool CheckDevPluginPenumbra(DalamudPluginInterface pi) + private static bool CheckDevPluginPenumbra(IDalamudPluginInterface pi) { #if !DEBUG var path = Path.Combine(pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra"); @@ -71,7 +71,7 @@ public class ValidityChecker : IService } // Check if the loaded version of Penumbra itself is in devPlugins. - private static bool CheckIsNotInstalled(DalamudPluginInterface pi) + private static bool CheckIsNotInstalled(IDalamudPluginInterface pi) { #if !DEBUG var checkedDirectory = pi.AssemblyLocation.Directory?.Parent?.Parent?.Name; @@ -86,7 +86,7 @@ public class ValidityChecker : IService } // Check if the loaded version of Penumbra is installed from a valid source repo. - private static bool CheckSourceRepo(DalamudPluginInterface pi) + private static bool CheckSourceRepo(IDalamudPluginInterface pi) { #if !DEBUG return pi.SourceRepository?.Trim().ToLowerInvariant() switch diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c95884c6..eeb94c71 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 652f928d..6db4db5c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 56e9482b..91129129 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 3ce10224..b9525b29 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -287,7 +287,7 @@ public partial class ModEditWindow { fixed (ushort* v2 = &v) { - return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); + return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", flags); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 070895b5..a22c10ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,5 +1,5 @@ -using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 0afeeeeb..72bfa266 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -121,7 +122,7 @@ public class ChangedItemDrawer : IDisposable, IUiService public static Vector2 TypeFilterIconSize => new(2 * ImGui.GetTextLineHeight()); - public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, + public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { _items = gameData.GetExcelSheet()!; @@ -417,7 +418,7 @@ public class ChangedItemDrawer : IDisposable, IUiService }; /// Initialize the icons. - private bool CreateEquipSlotIcons(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) + private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); @@ -441,20 +442,20 @@ public class ChangedItemDrawer : IDisposable, IUiService Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, textureProvider.GetTextureFromGame("ui/icon/062000/062042_hr1.tex", true)); - Add(ChangedItemIcon.Demihuman, textureProvider.GetTextureFromGame("ui/icon/062000/062041_hr1.tex", true)); - Add(ChangedItemIcon.Customization, textureProvider.GetTextureFromGame("ui/icon/062000/062043_hr1.tex", true)); - Add(ChangedItemIcon.Action, textureProvider.GetTextureFromGame("ui/icon/062000/062001_hr1.tex", true)); - Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, uiBuilder)); - Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, uiBuilder)); - Add(AllFlags, textureProvider.GetTextureFromGame("ui/icon/114000/114052_hr1.tex", true)); + Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062042_hr1.tex")!)); + Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062041_hr1.tex")!)); + Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); _smallestIconWidth = _icons.Values.Min(i => i.Width); return true; } - private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) @@ -466,10 +467,10 @@ public class ChangedItemDrawer : IDisposable, IUiService for (var y = 0; y < unk.Header.Height; ++y) image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); - return uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon"); } - private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider) { var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); if (emote == null) @@ -486,6 +487,6 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - return uiBuilder.LoadImageRaw(image2, emote.Header.Width, emote.Header.Height, 4); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon"); } } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 082b78b8..914f10d9 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,7 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; @@ -21,7 +21,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; public sealed class CollectionPanel( - DalamudPluginInterface pi, + IDalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, CollectionSelector selector, @@ -318,7 +318,7 @@ public sealed class CollectionPanel( var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); DrawIndividualDragSource(text, id); - DrawIndividualDragTarget(text, id); + DrawIndividualDragTarget(id); if (!invalid) { selector.DragTargetAssignment(type, id); @@ -349,7 +349,7 @@ public sealed class CollectionPanel( _draggedIndividualAssignment = _active.Individuals.Index(id); } - private void DrawIndividualDragTarget(string text, ActorIdentifier id) + private void DrawIndividualDragTarget(ActorIdentifier id) { if (!id.IsValid) return; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 67b0a50c..53fa0b33 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI; public sealed class ConfigWindow : Window, IUiService { - private readonly DalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly Configuration _config; private readonly PerformanceTracker _tracker; private readonly ValidityChecker _validityChecker; @@ -24,7 +24,7 @@ public sealed class ConfigWindow : Window, IUiService private ConfigTabBar _configTabs = null!; private string? _lastException; - public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, + public ConfigWindow(PerformanceTracker tracker, IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) : base(GetLabel(checker)) { diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 14e16432..cb533a00 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin; using Dalamud.Plugin.Services; using OtterGui.Services; @@ -13,23 +13,25 @@ namespace Penumbra.UI; public class LaunchButton : IDisposable, IUiService { private readonly ConfigWindow _configWindow; - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly ITitleScreenMenu _title; private readonly string _fileName; + private readonly ITextureProvider _textureProvider; - private IDalamudTextureWrap? _icon; - private TitleScreenMenuEntry? _entry; + private IDalamudTextureWrap? _icon; + private IReadOnlyTitleScreenMenuEntry? _entry; /// /// Register the launch button to be created on the next draw event. /// - public LaunchButton(DalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui) + public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui, ITextureProvider textureProvider) { - _uiBuilder = pi.UiBuilder; - _configWindow = ui; - _title = title; - _icon = null; - _entry = null; + _uiBuilder = pi.UiBuilder; + _configWindow = ui; + _textureProvider = textureProvider; + _title = title; + _icon = null; + _entry = null; _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); _uiBuilder.Draw += CreateEntry; @@ -49,7 +51,8 @@ public class LaunchButton : IDisposable, IUiService { try { - _icon = _uiBuilder.LoadImage(_fileName); + // TODO: update when API updated. + _icon = _textureProvider.GetFromFile(_fileName).RentAsync().Result; if (_icon != null) _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index b8faadf7..ec5bb920 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0ca4d40c..88d6afa2 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.DragDrop; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 28f00a97..ee6fab1f 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -17,7 +17,7 @@ public class ModPanel : IDisposable, IUiService private readonly ModPanelTabBar _tabs; private bool _resetCursor; - public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, + public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, MultiModPanel multiModPanel, CommunicatorService communicator) { _selector = selector; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1e371065..f81b2831 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index a8b393b1..6c974f9c 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -20,7 +20,7 @@ public class ModPanelHeader : IDisposable private readonly CommunicatorService _communicator; private float _lastPreSettingsHeight = 0; - public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) + public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) { _communicator = communicator; _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index d531b1a2..8de613d4 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 3bf4cd88..c53f1b8e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -271,7 +271,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService public unsafe string Name(ResolveData resolve, string none = "") { - if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Awaiter.IsCompletedSuccessfully) + if (resolve.AssociatedGameObject == nint.Zero || !_actors.Awaiter.IsCompletedSuccessfully) return none; try diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 34e2cbcf..05a1f33b 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -38,7 +38,7 @@ public sealed class CollectionsTab : IDisposable, ITab, IUiService } } - public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, + public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) { _config = configuration.Ephemeral; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 0122a6f5..41f28ab9 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -37,7 +37,6 @@ using Penumbra.Util; using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; @@ -437,8 +436,8 @@ public class DebugTab : Window, ITab, IUiService : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind == (byte)ObjectKind.BattleNpc - ? $"{identifier.DataId} | {obj.AsObject->DataID}" + var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } @@ -587,11 +586,11 @@ public class DebugTab : Window, ITab, IUiService if (table) { ImGuiUtil.DrawTableColumn("Group Members"); - ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MemberCount.ToString()); + ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MainGroup.MemberCount.ToString()); for (var i = 0; i < 8; ++i) { ImGuiUtil.DrawTableColumn($"Member #{i}"); - var member = GroupManager.Instance()->GetPartyMemberByIndex(i); + var member = GroupManager.Instance()->MainGroup.GetPartyMemberByIndex(i); ImGuiUtil.DrawTableColumn(member == null ? "NULL" : new ByteString(member->Name).ToString()); } } @@ -612,7 +611,7 @@ public class DebugTab : Window, ITab, IUiService if (table) for (var i = 0; i < 8; ++i) { - ref var c = ref agent->Data->CharacterArraySpan[i]; + ref var c = ref agent->Data->Characters[i]; ImGuiUtil.DrawTableColumn($"Character {i}"); var name = c.Name1.ToString(); ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})"); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 7faa3da8..50fdc1d3 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -5,7 +5,7 @@ using OtterGui.Raii; using Penumbra.UI.Classes; using Dalamud.Interface; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using FFXIVClientStructs.FFXIV.Client.Game; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 0b54c5e2..14d4ed41 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -121,7 +121,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa } /// Obtain a label for an extension node. - private static string GetNodeLabel(uint label, uint type, ulong count) + private static string GetNodeLabel(uint label, uint type, int count) { var (lowest, mid1, mid2, highest) = Functions.SplitBytes(type); return highest == 0 diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 17db21c9..49e77a4d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,7 +41,7 @@ public class SettingsTab : ITab, IUiService private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private readonly FileCompactor _compactor; private readonly DalamudConfigService _dalamudConfig; - private readonly DalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; @@ -51,7 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly TagButtons _sharedTags = new(); - public SettingsTab(DalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, + public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 8fbce6d0..deba7023 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 99819fce..72ac0d01 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -9,13 +9,13 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; - public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, + public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab) { _uiBuilder = pi.UiBuilder; diff --git a/repo.json b/repo.json index 3142f8d4..6379595e 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 431933e9c16ac5e4e0e25504cda14eb1a670ea3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Jul 2024 18:27:53 +0200 Subject: [PATCH 1786/2451] Revert repo API version. --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index 6379595e..3142f8d4 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 9, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 9fb80907811f1fac339410ba60ce6384c40195a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 3 Jul 2024 17:29:49 +0200 Subject: [PATCH 1787/2451] Current state. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/GameStateApi.cs | 2 +- .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Cache/CustomResourceCache.cs | 2 +- Penumbra/Communication/MtrlShpkLoaded.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 18 ++- .../Hooks/Objects/CharacterBaseDestructor.cs | 2 +- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 2 +- .../Hooks/Objects/CreateCharacterBase.cs | 2 +- Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../PreBoneDeformerReplacer.cs | 24 ++-- .../PostProcessing}/ShaderReplacementFixer.cs | 20 ++-- .../ResourceLoading/CreateFileWHook.cs | 5 +- .../ResourceLoading/FileReadService.cs | 11 +- .../ResourceLoading/ResourceLoader.cs | 26 ++-- .../ResourceLoading/ResourceManagerService.cs | 6 +- .../ResourceLoading/ResourceService.cs | 13 +- .../ResourceLoading/TexMdlService.cs | 113 +++++++++++------- .../Hooks/Resources/ApricotResourceLoad.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlShpk.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 3 +- .../Resources/ResourceHandleDestructor.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathResolver.cs | 2 +- .../Interop/PathResolving/SubfileHelper.cs | 2 +- .../Processing/FilePostProcessService.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 +++-- Penumbra/Interop/Services/DecalReverter.cs | 2 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Penumbra.csproj | 1 - Penumbra/Services/CrashHandlerService.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- 39 files changed, 186 insertions(+), 139 deletions(-) rename Penumbra/Interop/{Services => Hooks/PostProcessing}/PreBoneDeformerReplacer.cs (81%) rename Penumbra/Interop/{Services => Hooks/PostProcessing}/ShaderReplacementFixer.cs (91%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/CreateFileWHook.cs (97%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/FileReadService.cs (92%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceLoader.cs (93%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceManagerService.cs (93%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceService.cs (95%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/TexMdlService.cs (60%) diff --git a/OtterGui b/OtterGui index 437ef65c..c2738e1d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 437ef65c6464c54c8f40196dd2428da901d73aab +Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a97e5ae..066637ab 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a97e5aeee3b7375b333c1add5305d0ce80cbf83 +Subproject commit 066637abe05c659b79d84f52e6db33487498f433 diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index becb55ee..b035c886 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -2,8 +2,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 44c12856..80d4cf1d 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -5,7 +5,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs index 46c28393..e63f8637 100644 --- a/Penumbra/Collections/Cache/CustomResourceCache.cs +++ b/Penumbra/Collections/Cache/CustomResourceCache.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 8aab0e0e..9d3597a8 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper + /// ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 6d511a28..ed4eb669 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -1,8 +1,14 @@ -namespace Penumbra.Interop.Hooks; - +namespace Penumbra.Interop.Hooks; + public static class HookSettings { - public const bool MetaEntryHooks = false; - public const bool MetaParentHooks = false; - public const bool VfxIdentificationHooks = false; -} + public const bool AllHooks = true; + + public const bool ObjectHooks = false && AllHooks; + public const bool ReplacementHooks = true && AllHooks; + public const bool ResourceHooks = false && AllHooks; + public const bool MetaEntryHooks = false && AllHooks; + public const bool MetaParentHooks = false && AllHooks; + public const bool VfxIdentificationHooks = false && AllHooks; + public const bool PostProcessingHooks = false && AllHooks; +} diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index e01a6550..c67bb9f3 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index 6e10c5e3..618d0bd7 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, true); + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 20c96f56..663209ae 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 299f312a..56b3d853 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -16,7 +16,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 267b4711..8b701fe5 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -17,7 +17,7 @@ public sealed unsafe class EnableDraw : IHookService public EnableDraw(HookManager hooks, GameState state) { _state = state; - _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, HookSettings.ObjectHooks); } private delegate void Delegate(GameObject* gameObject); diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index fec0a13f..da31840f 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -16,7 +16,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs similarity index 81% rename from Penumbra/Interop/Services/PreBoneDeformerReplacer.cs rename to Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 9f553257..903484ea 100644 --- a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -namespace Penumbra.Interop.Services; +namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { @@ -29,17 +30,16 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi private readonly IFramework _framework; public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, - IGameInteropProvider interop, IFramework framework, CharacterBaseVTables vTables) + HookManager hooks, IFramework framework, CharacterBaseVTables vTables) { - interop.InitializeFromAttributes(this); - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = interop.HookFromAddress(vTables.HumanVTable[57], SetupScaling); - _humanCreateDeformerHook = interop.HookFromAddress(vTables.HumanVTable[91], CreateDeformer); - _humanSetupScalingHook.Enable(); - _humanCreateDeformerHook.Enable(); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, + HookSettings.PostProcessingHooks).Result; + _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], + CreateDeformer, HookSettings.PostProcessingHooks).Result; } public void Dispose() diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs similarity index 91% rename from Penumbra/Interop/Services/ShaderReplacementFixer.cs rename to Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 95e70b45..b27ca4c5 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -1,6 +1,4 @@ using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -10,9 +8,11 @@ using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; using Penumbra.Services; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; +using ModelRenderer = Penumbra.Interop.Services.ModelRenderer; -namespace Penumbra.Interop.Services; +namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService { @@ -29,8 +29,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly Hook _humanOnRenderMaterialHook; - [Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))] - private readonly Hook _modelRendererOnRenderMaterialHook = null!; + private readonly Hook _modelRendererOnRenderMaterialHook; private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; @@ -59,19 +58,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _moddedCharacterGlassShpkCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables) + CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) { - interop.InitializeFromAttributes(this); _resourceHandleDestructor = resourceHandleDestructor; _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; - _humanOnRenderMaterialHook = - interop.HookFromAddress(vTables.HumanVTable[62], OnRenderHumanMaterial); + _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[62], + OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; + _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", + Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); - _humanOnRenderMaterialHook.Enable(); - _modelRendererOnRenderMaterialHook.Enable(); } public void Dispose() diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs similarity index 97% rename from Penumbra/Interop/ResourceLoading/CreateFileWHook.cs rename to Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index bde640d2..a8ac0608 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -5,7 +5,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; /// /// To allow XIV to load files of arbitrary path length, @@ -19,7 +19,8 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService public CreateFileWHook(IGameInteropProvider interop) { _createFileWHook = interop.HookFromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); - _createFileWHook.Enable(); + if (HookSettings.ReplacementHooks) + _createFileWHook.Enable(); } /// diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs similarity index 92% rename from Penumbra/Interop/ResourceLoading/FileReadService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs index 5edba790..199525fb 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs @@ -6,16 +6,17 @@ using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.Util; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class FileReadService : IDisposable, IRequiredService { public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _resourceManager = resourceManager; - _performance = performance; + _performance = performance; interop.InitializeFromAttributes(this); - _readSqPackHook.Enable(); + if (HookSettings.ReplacementHooks) + _readSqPackHook.Enable(); } /// Invoked when a file is supposed to be read from SqPack. @@ -49,7 +50,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService _readSqPackHook.Dispose(); } - private readonly PerformanceTracker _performance; + private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; private delegate byte ReadSqPackPrototype(nint resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); @@ -60,7 +61,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService private byte ReadSqPackDetour(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) { using var performance = _performance.Measure(PerformanceType.ReadSqPack); - byte? ret = null; + byte? ret = null; _lastFileThreadResourceManager.Value = resourceManager; ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret); _lastFileThreadResourceManager.Value = nint.Zero; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs similarity index 93% rename from Penumbra/Interop/ResourceLoading/ResourceLoader.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 4a423993..5cac2f32 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -9,27 +9,27 @@ using Penumbra.String; using Penumbra.String.Classes; using FileMode = Penumbra.Interop.Structs.FileMode; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlService _texMdlService; + private readonly TexMdlService _texMdlService; private ResolveData _resolvedData = ResolveData.Invalid; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { - _resources = resources; + _resources = resources; _fileReadService = fileReadService; - _texMdlService = texMdlService; + _texMdlService = texMdlService; ResetResolvePath(); - _resources.ResourceRequested += ResourceHandler; + _resources.ResourceRequested += ResourceHandler; _resources.ResourceHandleIncRef += IncRefProtection; _resources.ResourceHandleDecRef += DecRefProtection; - _fileReadService.ReadSqPack += ReadSqPackDetour; + _fileReadService.ReadSqPack += ReadSqPackDetour; } /// Load a resource for a given path and a specific collection. @@ -80,10 +80,10 @@ public unsafe class ResourceLoader : IDisposable, IService public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceRequested -= ResourceHandler; _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _fileReadService.ReadSqPack -= ReadSqPackDetour; } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, @@ -112,7 +112,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; - path = p; + path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -140,12 +140,12 @@ public unsafe class ResourceLoader : IDisposable, IService } var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); - fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; } @@ -165,7 +165,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; fileDescriptor->FileDescriptor = (byte*)fd + 1; - CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. @@ -206,7 +206,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; _incMode.Value = true; - returnValue = _resources.IncRef(handle); + returnValue = _resources.IncRef(handle); _incMode.Value = false; } diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs similarity index 93% rename from Penumbra/Interop/ResourceLoading/ResourceManagerService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs index 0479d2a6..1bff80ba 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs @@ -8,7 +8,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceManagerService : IRequiredService { @@ -23,10 +23,10 @@ public unsafe class ResourceManagerService : IRequiredService public ResourceHandle* FindResource(ResourceCategory cat, ResourceType ext, uint crc32) { ref var manager = ref *ResourceManager; - var catIdx = (uint)cat >> 0x18; + var catIdx = (uint)cat >> 0x18; cat = (ResourceCategory)(ushort)cat; ref var category = ref manager.ResourceGraph->Containers[(int)cat]; - var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); + var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); if (extMap == null) return null; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs similarity index 95% rename from Penumbra/Interop/ResourceLoading/ResourceService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index d623d72d..0b00452b 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -12,7 +12,7 @@ using Penumbra.String.Classes; using Penumbra.Util; using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceService : IDisposable, IRequiredService { @@ -24,16 +24,19 @@ public unsafe class ResourceService : IDisposable, IRequiredService _performance = performance; _resourceManager = resourceManager; interop.InitializeFromAttributes(this); - _getResourceSyncHook.Enable(); - _getResourceAsyncHook.Enable(); _incRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); - _incRefHook.Enable(); _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - _decRefHook.Enable(); + if (HookSettings.ReplacementHooks) + { + _getResourceSyncHook.Enable(); + _getResourceAsyncHook.Enable(); + _incRefHook.Enable(); + _decRefHook.Enable(); + } } public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path) diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs similarity index 60% rename from Penumbra/Interop/ResourceLoading/TexMdlService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index a2b43c64..28ad7aa4 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -1,109 +1,138 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.LayoutEngine; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Lumina.Excel.GeneratedSheets2; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; +using Penumbra.Interop.Structs; using Penumbra.String.Classes; +using FileMode = Penumbra.Interop.Structs.FileMode; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly nint CustomFileFlag = new(0xDEADBEEF); - + /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// public IReadOnlySet CustomFileCrc => _customFileCrc; - + public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - //_checkFileStateHook.Enable(); - //_loadTexFileExternHook.Enable(); - //_loadMdlFileExternHook.Enable(); + if (HookSettings.ReplacementHooks) + { + _checkFileStateHook.Enable(); + _loadMdlFileExternHook.Enable(); + _textureSomethingHook.Enable(); + _vf32Hook.Enable(); + //_loadTexFileExternHook.Enable(); + } } - + /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { - if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex) + if (path.HasValue && type is ResourceType.Mdl) _customFileCrc.Add(path.Value.Crc64); } - + /// Add a fixed CRC64 value. public void AddCrc(ulong crc64) => _customFileCrc.Add(crc64); - + public void Dispose() { - //_checkFileStateHook.Dispose(); + _checkFileStateHook.Dispose(); //_loadTexFileExternHook.Dispose(); - //_loadMdlFileExternHook.Dispose(); + _textureSomethingHook.Dispose(); + _loadMdlFileExternHook.Dispose(); + _vf32Hook.Dispose(); } - - private readonly HashSet _customFileCrc = new(); - + + private readonly HashSet _customFileCrc = []; + private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); - + + private delegate nint TextureSomethingDelegate(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor); + [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - + + [Signature("E8 ?? ?? ?? ?? 0F B6 C8 EB ?? 4C 8B 83", DetourName = nameof(TextureSomethingDetour))] + private readonly Hook _textureSomethingHook = null!; + + private nint TextureSomethingDetour(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor) + { + //Penumbra.Log.Information($"SomethingDetour {handle->Handle.FileName()}"); + //if (!handle->Handle.GamePath(out var path) || !path.IsRooted()) + return _textureSomethingHook.Original(handle, lod, descriptor); + + descriptor->FileMode = FileMode.LoadUnpackedResource; + return _loadTexFileLocal.Invoke((ResourceHandle*)handle, lod, (nint)descriptor, true); + } + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. /// private nint CheckFileStateDetour(nint ptr, ulong crc64) => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); - - + + private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); - + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!; - + private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, nint unk1, bool unk2); - + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; - - + + private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + + private delegate byte TexResourceHandleVf32Prototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); + + [Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] + private readonly Hook _vf32Hook = null!; - private delegate byte TexResourceHandleVf32Prototype(ResourceHandle* handle, nint unk1, byte unk2); - - //[Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] - //private readonly Hook _vf32Hook = null!; - // - //private byte Vf32Detour(ResourceHandle* handle, nint unk1, byte unk2) - //{ - // var ret = _vf32Hook.Original(handle, unk1, unk2); - // return _loadTexFileLocal() - //} - + private byte Vf32Detour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + { + //if (handle->Handle.GamePath(out var path) && path.IsRooted()) + //{ + // Penumbra.Log.Information($"Replacing {descriptor->FileMode} with {FileMode.LoadSqPackResource} in VF32 for {path}."); + // descriptor->FileMode = FileMode.LoadSqPackResource; + //} + + var ret = _vf32Hook.Original(handle, descriptor, unk2); + return ret; + } + //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] //private readonly Hook _loadTexFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) // => ptr.Equals(CustomFileFlag) // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); - - + + [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, nint unk1, bool unk2, nint ptr) => ptr.Equals(CustomFileFlag) diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index 2e5698a3..511e842f 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,7 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, true); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 5ef3bf37..8447762b 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -14,7 +14,7 @@ public sealed unsafe class LoadMtrlShpk : FastHook { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, true); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 14a011ea..7bc3c7b0 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, true); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index a7e82b72..8fa6d861 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -59,7 +59,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); // @formatter:on - Enable(); + if (HookSettings.ResourceHooks) + Enable(); } public void Enable() diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index ac3f504a..cd4a53c4 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -23,7 +23,7 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, true); + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, HookSettings.ResourceHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 5eacbfb0..e709c210 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -4,12 +4,12 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index cc3e0e9b..49035dc8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -3,8 +3,8 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Processing; -using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 44a152f0..836cf731 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,8 +1,8 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index 0dc62b3d..bba53c94 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -1,7 +1,7 @@ using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ca8836b0..a852a4cc 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,8 +9,8 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b8bad84a..810c946d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -5,7 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; @@ -31,7 +31,8 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) + public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, + bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) { Name = name; AnonymizedName = anonymizedName; @@ -61,9 +62,10 @@ public class ResourceTree var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), + _ => ReadOnlySpan.Empty, }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -112,15 +114,17 @@ public class ResourceTree { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; + var subObject = (CharacterBase*)baseSubObject; if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) continue; - var weapon = (Weapon*)subObject; + + var weapon = (Weapon*)subObject; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; - var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown); + var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain1, weapon->Stain2)); var weaponType = weapon->SecondaryId; var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); @@ -152,6 +156,7 @@ public class ResourceTree ++weaponIndex; } + Nodes.InsertRange(0, weaponNodes); } @@ -167,10 +172,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - pbdNode = pbdNode.Clone(); + pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(pbdNode); } } @@ -184,10 +190,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - decalNode = decalNode.Clone(); + decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(decalNode); } @@ -200,10 +207,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - legacyDecalNode = legacyDecalNode.Clone(); + legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(legacyDecalNode); } } diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 17d8d2e0..21b51fd2 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -1,7 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b1ad0b78..9f2db2e6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -8,7 +8,6 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Cache; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Services; using Penumbra.Interop.Services; @@ -22,6 +21,7 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ed5c5e30..2e53bd22 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -37,7 +37,6 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\ - H:\Projects\FFPlugins\Dalamud\bin\Release\ diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 25c6cf57..9103b29c 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -7,8 +7,8 @@ using Penumbra.Communication; using Penumbra.CrashHandler; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData.Actors; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index c53f1b8e..935f11e3 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -8,8 +8,8 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 41f28ab9..9a03f384 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -25,7 +25,6 @@ using Penumbra.GameData.Interop; using Penumbra.Import.Structs; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Mods; @@ -40,6 +39,8 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.UI.Tabs.Debug; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 14d4ed41..a4dbba2f 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -8,7 +8,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; From 4026dd58672fb38a6c83dfe1309ded1b10a9aa67 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 12:14:31 +0200 Subject: [PATCH 1788/2451] Change texture handling. --- Penumbra.GameData | 2 +- .../Hooks/ResourceLoading/TexMdlService.cs | 128 +++++++++--------- Penumbra/Interop/Structs/ResourceHandle.cs | 6 + 3 files changed, 73 insertions(+), 63 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 066637ab..19923f8d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 066637abe05c659b79d84f52e6db33487498f433 +Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 28ad7aa4..793d15af 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -1,93 +1,110 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using Lumina.Excel.GeneratedSheets2; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Classes; -using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { + /// + /// We need to be able to obtain the requested LoD level. + /// This replicates the LoD behavior of a textures OnLoad function. + /// + private readonly struct LodService + { + public LodService(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); + + [Signature(Sigs.LodConfig)] + private readonly nint _lodConfig = nint.Zero; + + public byte GetLod(TextureResourceHandle* handle) + { + if (handle->ChangeLod) + { + var config = *(byte*)_lodConfig + 0xE; + if (config == byte.MaxValue) + return 2; + } + + return 0; + } + } + /// Custom ulong flag to signal our files as opposed to SE files. public static readonly nint CustomFileFlag = new(0xDEADBEEF); - /// - /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, - /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. - /// - public IReadOnlySet CustomFileCrc - => _customFileCrc; + private readonly LodService _lodService; public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); + _lodService = new LodService(interop); if (HookSettings.ReplacementHooks) { _checkFileStateHook.Enable(); _loadMdlFileExternHook.Enable(); - _textureSomethingHook.Enable(); - _vf32Hook.Enable(); - //_loadTexFileExternHook.Enable(); + _textureOnLoadHook.Enable(); } } /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { - if (path.HasValue && type is ResourceType.Mdl) - _customFileCrc.Add(path.Value.Crc64); + _ = type switch + { + ResourceType.Mdl when path.HasValue => _customMdlCrc.Add(path.Value.Crc64), + ResourceType.Tex when path.HasValue => _customTexCrc.Add(path.Value.Crc64), + _ => false, + }; } - /// Add a fixed CRC64 value. - public void AddCrc(ulong crc64) - => _customFileCrc.Add(crc64); - public void Dispose() { _checkFileStateHook.Dispose(); - //_loadTexFileExternHook.Dispose(); - _textureSomethingHook.Dispose(); _loadMdlFileExternHook.Dispose(); - _vf32Hook.Dispose(); + _textureOnLoadHook.Dispose(); } - private readonly HashSet _customFileCrc = []; + /// + /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, + /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. + /// + private readonly HashSet _customMdlCrc = []; + + private readonly HashSet _customTexCrc = []; private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); - private delegate nint TextureSomethingDelegate(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor); - [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - [Signature("E8 ?? ?? ?? ?? 0F B6 C8 EB ?? 4C 8B 83", DetourName = nameof(TextureSomethingDetour))] - private readonly Hook _textureSomethingHook = null!; - - private nint TextureSomethingDetour(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor) - { - //Penumbra.Log.Information($"SomethingDetour {handle->Handle.FileName()}"); - //if (!handle->Handle.GamePath(out var path) || !path.IsRooted()) - return _textureSomethingHook.Original(handle, lod, descriptor); - - descriptor->FileMode = FileMode.LoadUnpackedResource; - return _loadTexFileLocal.Invoke((ResourceHandle*)handle, lod, (nint)descriptor, true); - } + private readonly ThreadLocal _texReturnData = new(() => default); /// /// The function that checks a files CRC64 to determine whether it is 'protected'. - /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. + /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. + /// Since Dawntrail inlined the RSF function for textures, we can not use the flag method here. + /// Instead, we signal the caller that this will fail and let it call the local function after intentionally failing. /// private nint CheckFileStateDetour(nint ptr, ulong crc64) - => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); + { + if (_customMdlCrc.Contains(crc64)) + return CustomFileFlag; + if (_customTexCrc.Contains(crc64)) + _texReturnData.Value = true; - private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); + return _checkFileStateHook.Original(ptr, crc64); + } + + private delegate byte LoadTexFileLocalDelegate(TextureResourceHandle* handle, int unk1, SeFileDescriptor* unk2, bool unk3); /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] @@ -99,36 +116,23 @@ public unsafe class TexMdlService : IDisposable, IRequiredService [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; + private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + [Signature(Sigs.TexResourceHandleOnLoad, DetourName = nameof(OnLoadDetour))] + private readonly Hook _textureOnLoadHook = null!; - private delegate byte TexResourceHandleVf32Prototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - - [Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] - private readonly Hook _vf32Hook = null!; - - private byte Vf32Detour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) { - //if (handle->Handle.GamePath(out var path) && path.IsRooted()) - //{ - // Penumbra.Log.Information($"Replacing {descriptor->FileMode} with {FileMode.LoadSqPackResource} in VF32 for {path}."); - // descriptor->FileMode = FileMode.LoadSqPackResource; - //} + var ret = _textureOnLoadHook.Original(handle, descriptor, unk2); + if (!_texReturnData.Value) + return ret; - var ret = _vf32Hook.Original(handle, descriptor, unk2); - return ret; + // Function failed on a replaced texture, call local. + _texReturnData.Value = false; + return _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); } - //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] - //private readonly Hook _loadTexFileExternHook = null!; - - /// We hook the extern functions to just return the local one if given the custom flag as last argument. - //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) - // => ptr.Equals(CustomFileFlag) - // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) - // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); - + private delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 058b9004..6e428f25 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -14,6 +14,12 @@ public unsafe struct TextureResourceHandle [FieldOffset(0x0)] public CsHandle.TextureResourceHandle CsHandle; + + [FieldOffset(0x104)] + public byte SomeLodFlag; + + public bool ChangeLod + => (SomeLodFlag & 1) != 0; } public enum LoadState : byte From 1284037554fdeebd4e21d7a3ed846629b1c43e6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 14:39:03 +0200 Subject: [PATCH 1789/2451] Fix some hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 10 ++-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 1 - Penumbra/Interop/Hooks/Meta/GmpHook.cs | 47 ++++--------------- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 ++-- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 ++-- .../Interop/Hooks/Objects/CopyCharacter.cs | 5 +- .../Hooks/Objects/CreateCharacterBase.cs | 6 +-- .../Interop/Structs/CharacterUtilityData.cs | 10 ++-- Penumbra/Interop/Structs/MetaIndex.cs | 13 +++-- 11 files changed, 40 insertions(+), 75 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19923f8d..27d15145 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 +Subproject commit 27d15145567c11e2bb1902857f8db25f02189390 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index ed4eb669..9dd8d74f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -4,11 +4,11 @@ public static class HookSettings { public const bool AllHooks = true; - public const bool ObjectHooks = false && AllHooks; + public const bool ObjectHooks = true && AllHooks; public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = false && AllHooks; - public const bool MetaEntryHooks = false && AllHooks; - public const bool MetaParentHooks = false && AllHooks; + public const bool ResourceHooks = true && AllHooks; + public const bool MetaEntryHooks = true && AllHooks; + public const bool MetaParentHooks = true && AllHooks; public const bool VfxIdentificationHooks = false && AllHooks; - public const bool PostProcessingHooks = false && AllHooks; + public const bool PostProcessingHooks = true && AllHooks; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 3767c4a2..e90674a8 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 329a8beb..12b221d9 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Data.Parsing.Uld; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -8,12 +10,10 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class GmpHook : FastHook, IDisposable { - public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + public delegate ulong Delegate(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId); private readonly MetaState _metaState; - private static readonly Finalizer StablePointer = new(); - public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; @@ -21,50 +21,21 @@ public unsafe class GmpHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - /// - /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . - /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, - /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. - /// - private nint Detour(nint gmpResource, uint dividedHeadId) + private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) { - nint ret; + ulong ret; if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) - { - if (entry.Entry.Enabled) - { - *StablePointer.Pointer = entry.Entry.Value; - // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. - // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); - } - else - { - ret = nint.Zero; - } - } + ret = (*outputEntry) = entry.Entry.Enabled ? entry.Entry.Value : 0ul; else - { - ret = Task.Result.Original(gmpResource, dividedHeadId); - } + ret = Task.Result.Original(characterUtility, outputEntry, setId); - Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + Penumbra.Log.Excessive( + $"[GetGmpFlags] Invoked on 0x{(ulong)characterUtility:X} for {setId} with 0x{(ulong)outputEntry:X} (={*outputEntry:X}), returned {ret:X10}."); return ret; } - /// Allocate and clean up our single stable ulong pointer. - private class Finalizer - { - public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); - - ~Finalizer() - { - Marshal.FreeHGlobal((nint)Pointer); - } - } - public void Dispose() => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 79e7f6a6..c1803745 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index eb8a8a37..e08dc393 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -10,8 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class RspBustHook : FastHook, IDisposable { - public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, - byte bustSize); + public delegate float* Delegate(nint cmpResource, float* storage, SubRace race, byte gender, byte bodyType, byte bustSize); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -24,7 +23,7 @@ public unsafe class RspBustHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) { if (gender == 0) { @@ -38,7 +37,6 @@ public unsafe class RspBustHook : FastHook, IDisposable if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); @@ -58,11 +56,11 @@ public unsafe class RspBustHook : FastHook, IDisposable } else { - ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + ret = Task.Result.Original(cmpResource, storage, clan, gender, bodyType, bustSize); } Penumbra.Log.Excessive( - $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index f8f9e51e..20e3c939 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public class RspHeightHook : FastHook, IDisposable { - public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + public delegate float Delegate(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -23,7 +23,7 @@ public class RspHeightHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) { float scale; if (bodyType < 2 @@ -36,7 +36,6 @@ public class RspHeightHook : FastHook, IDisposable if (height > 100) height = 0; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); @@ -68,11 +67,11 @@ public class RspHeightHook : FastHook, IDisposable } else { - scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + scale = Task.Result.Original(cmpResource, clan, gender, bodyType, height); } Penumbra.Log.Excessive( - $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {height}, returned {scale}."); return scale; } diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 663209ae..d81043c8 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -9,7 +9,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr + /// CutsceneService = 0, } @@ -38,8 +38,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtrOwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); Invoke(character, source); return _task.Result.Original(target, source, unk); diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 56b3d853..f00a9984 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -10,7 +10,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// MetaState = 0, } @@ -64,10 +64,10 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MetaState = 0, } } diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 22150cc1..d33da477 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -6,16 +6,16 @@ namespace Penumbra.Interop.Structs; public unsafe struct CharacterUtilityData { public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - public const int IndexSkinShpk = 76; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexSkinShpk = 83; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 87; + public const int TotalNumResources = 89; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) @@ -36,7 +36,7 @@ public unsafe struct CharacterUtilityData 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, - //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs index 65302264..2ec5fce4 100644 --- a/Penumbra/Interop/Structs/MetaIndex.cs +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -4,6 +4,7 @@ namespace Penumbra.Interop.Structs; public enum MetaIndex : int { Eqp = 0, + Evp = 1, Gmp = 2, Eqdp0101 = 3, @@ -21,9 +22,8 @@ public enum MetaIndex : int Eqdp1301, Eqdp1401, Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, + Eqdp1601, + Eqdp1701, Eqdp1801, Eqdp0104, Eqdp0204, @@ -51,9 +51,8 @@ public enum MetaIndex : int Eqdp1301Acc, Eqdp1401Acc, Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1601Acc, + Eqdp1701Acc, Eqdp1801Acc, Eqdp0104Acc, Eqdp0204Acc, @@ -66,7 +65,7 @@ public enum MetaIndex : int Eqdp9104Acc, Eqdp9204Acc, - HumanCmp = 64, + HumanCmp = 71, FaceEst, HairEst, HeadEst, From 41d271213efb97c34d173f3759d110780bd81d5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 5 Jul 2024 23:59:22 +0200 Subject: [PATCH 1790/2451] Update ShaderReplacementFixer for 7.0 --- .../PostProcessing/ShaderReplacementFixer.cs | 250 ++++++++++++++---- Penumbra/Interop/Services/ModelRenderer.cs | 82 +++++- Penumbra/UI/Tabs/Debug/DebugTab.cs | 97 +++++-- 3 files changed, 359 insertions(+), 70 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b27ca4c5..b87d33ef 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -19,9 +19,24 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan IrisShpkName + => "iris.shpk"u8; + public static ReadOnlySpan CharacterGlassShpkName => "characterglass.shpk"u8; + public static ReadOnlySpan CharacterTransparencyShpkName + => "charactertransparency.shpk"u8; + + public static ReadOnlySpan CharacterTattooShpkName + => "charactertattoo.shpk"u8; + + public static ReadOnlySpan CharacterOcclusionShpkName + => "characterocclusion.shpk"u8; + + public static ReadOnlySpan HairMaskShpkName + => "hairmask.shpk"u8; + private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, @@ -36,26 +51,36 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; - // MaterialResourceHandle set - private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); - private readonly ConcurrentSet _moddedCharacterGlassShpkMaterials = new(); - - private readonly object _skinLock = new(); - private readonly object _characterGlassLock = new(); - - // ConcurrentDictionary.Count uses a lock in its current implementation. - private int _moddedSkinShpkCount; - private int _moddedCharacterGlassShpkCount; - private ulong _skinSlowPathCallDelta; - private ulong _characterGlassSlowPathCallDelta; + private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _irisState; + private readonly ModdedShaderPackageState _characterGlassState; + private readonly ModdedShaderPackageState _characterTransparencyState; + private readonly ModdedShaderPackageState _characterTattooState; + private readonly ModdedShaderPackageState _characterOcclusionState; + private readonly ModdedShaderPackageState _hairMaskState; public bool Enabled { get; internal set; } = true; - public int ModdedSkinShpkCount - => _moddedSkinShpkCount; + public uint ModdedSkinShpkCount + => _skinState.MaterialCount; - public int ModdedCharacterGlassShpkCount - => _moddedCharacterGlassShpkCount; + public uint ModdedIrisShpkCount + => _irisState.MaterialCount; + + public uint ModdedCharacterGlassShpkCount + => _characterGlassState.MaterialCount; + + public uint ModdedCharacterTransparencyShpkCount + => _characterTransparencyState.MaterialCount; + + public uint ModdedCharacterTattooShpkCount + => _characterTattooState.MaterialCount; + + public uint ModdedCharacterOcclusionShpkCount + => _characterOcclusionState.MaterialCount; + + public uint ModdedHairMaskShpkCount + => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) @@ -64,7 +89,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; - _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[62], + + _skinState = new( + () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _irisState = new(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new(() => _modelRenderer.CharacterTransparencyShaderPackage, () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new(() => _modelRenderer.CharacterTattooShaderPackage, () => _modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new(() => _modelRenderer.CharacterOcclusionShaderPackage, () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + _hairMaskState = new(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + + _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; @@ -78,14 +114,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _humanOnRenderMaterialHook.Dispose(); _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); - _moddedCharacterGlassShpkMaterials.Clear(); - _moddedSkinShpkMaterials.Clear(); - _moddedCharacterGlassShpkCount = 0; - _moddedSkinShpkCount = 0; + _hairMaskState.ClearMaterials(); + _characterOcclusionState.ClearMaterials(); + _characterTattooState.ClearMaterials(); + _characterTransparencyState.ClearMaterials(); + _characterGlassState.ClearMaterials(); + _irisState.ClearMaterials(); + _skinState.ClearMaterials(); } - public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() - => (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0)); + public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() + => (_skinState.GetAndResetSlowPathCallDelta(), + _irisState.GetAndResetSlowPathCallDelta(), + _characterGlassState.GetAndResetSlowPathCallDelta(), + _characterTransparencyState.GetAndResetSlowPathCallDelta(), + _characterTattooState.GetAndResetSlowPathCallDelta(), + _characterOcclusionState.GetAndResetSlowPathCallDelta(), + _hairMaskState.GetAndResetSlowPathCallDelta()); private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) { @@ -102,54 +147,99 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; + var shpkName = mtrl->ShpkNameSpan; + var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); - if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) - if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedSkinShpkCount); - - if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) - if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedCharacterGlassShpkCount); + if (shpkState != null && shpk != shpkState.DefaultShaderPackage) + shpkState.TryAddMaterial(mtrlResourceHandle); } private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedSkinShpkCount); - - if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); + _skinState.TryRemoveMaterial(handle); + _irisState.TryRemoveMaterial(handle); + _characterGlassState.TryRemoveMaterial(handle); + _characterTransparencyState.TryRemoveMaterial(handle); + _characterTattooState.TryRemoveMaterial(handle); + _characterOcclusionState.TryRemoveMaterial(handle); + _hairMaskState.TryRemoveMaterial(handle); } + private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + { + if (SkinShpkName.SequenceEqual(shpkName)) + return _skinState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHuman() + => _skinState.MaterialCount; + + private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterGlassShpkName.SequenceEqual(shpkName)) + return _characterGlassState; + + if (CharacterTransparencyShpkName.SequenceEqual(shpkName)) + return _characterTransparencyState; + + if (CharacterTattooShpkName.SequenceEqual(shpkName)) + return _characterTattooState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (HairMaskShpkName.SequenceEqual(shpkName)) + return _hairMaskState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRenderer() + => _irisState.MaterialCount + _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || _moddedSkinShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForHuman() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, SkinShpkName)) + var shpkState = GetStateForHuman(mtrlResource); + if (shpkState == null) return _humanOnRenderMaterialHook.Original(human, param); - Interlocked.Increment(ref _skinSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // Performance considerations: // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; // - Function is called each frame for each material on screen, after culling, i.e. up to thousands of times a frame in crowded areas ; // - Swapping path is taken up to hundreds of times a frame. // At the time of writing, the lock doesn't seem to have a noticeable impact in either frame rate or CPU usage, but the swapping path shall still be avoided as much as possible. - lock (_skinLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _humanOnRenderMaterialHook.Original(human, param); } finally { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + *shpkReference = shpkState.DefaultShaderPackage; } } } @@ -158,27 +248,91 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. - if (!Enabled || _moddedCharacterGlassShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForModelRenderer() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName)) + var shpkState = GetStateForModelRenderer(mtrlResource); + if (shpkState == null) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); - Interlocked.Increment(ref _characterGlassSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // Same performance considerations as above. - lock (_characterGlassLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - *_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); } finally { - *_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage; + *shpkReference = shpkState.DefaultShaderPackage; } } } + + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) + { + // MaterialResourceHandle set + private readonly ConcurrentSet _materials = new(); + + // ConcurrentDictionary.Count uses a lock in its current implementation. + private uint _materialCount = 0; + + private ulong _slowPathCallDelta = 0; + + public uint MaterialCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => _materialCount; + } + + public ShaderPackageResourceHandle** ShaderPackageReference + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => referenceGetter(); + } + + public ShaderPackageResourceHandle* DefaultShaderPackage + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => defaultGetter(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryAddMaterial(nint mtrlResourceHandle) + { + if (_materials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryRemoveMaterial(Structs.ResourceHandle* handle) + { + if (_materials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void ClearMaterials() + { + _materials.Clear(); + _materialCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void IncrementSlowPathCallDelta() + => Interlocked.Increment(ref _slowPathCallDelta); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + } + + private delegate ShaderPackageResourceHandle* DefaultShaderPackageGetter(); + + private delegate ShaderPackageResourceHandle** ShaderPackageReferenceGetter(); } diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index b268b395..10f3977f 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -9,6 +9,13 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } + public ShaderPackageResourceHandle** IrisShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + }; + public ShaderPackageResourceHandle** CharacterGlassShaderPackage => Manager.Instance() switch { @@ -16,8 +23,46 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, }; + public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterTattooShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + }; + + public ShaderPackageResourceHandle** HairMaskShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + }; + + public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterTransparencyShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterTattooShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterOcclusionShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultHairMaskShaderPackage { get; private set; } + private readonly IFramework _framework; public ModelRenderer(IFramework framework) @@ -36,12 +81,42 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var anyMissing = false; + if (DefaultIrisShaderPackage == null) + { + DefaultIrisShaderPackage = *IrisShaderPackage; + anyMissing |= DefaultIrisShaderPackage == null; + } + if (DefaultCharacterGlassShaderPackage == null) { DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; anyMissing |= DefaultCharacterGlassShaderPackage == null; } + if (DefaultCharacterTransparencyShaderPackage == null) + { + DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + } + + if (DefaultCharacterTattooShaderPackage == null) + { + DefaultCharacterTattooShaderPackage = *CharacterTattooShaderPackage; + anyMissing |= DefaultCharacterTattooShaderPackage == null; + } + + if (DefaultCharacterOcclusionShaderPackage == null) + { + DefaultCharacterOcclusionShaderPackage = *CharacterOcclusionShaderPackage; + anyMissing |= DefaultCharacterOcclusionShaderPackage == null; + } + + if (DefaultHairMaskShaderPackage == null) + { + DefaultHairMaskShaderPackage = *HairMaskShaderPackage; + anyMissing |= DefaultHairMaskShaderPackage == null; + } + if (anyMissing) return; @@ -55,7 +130,12 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (!Ready) return; - *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *HairMaskShaderPackage = DefaultHairMaskShaderPackage; + *CharacterOcclusionShaderPackage = DefaultCharacterOcclusionShaderPackage; + *CharacterTattooShaderPackage = DefaultCharacterTattooShaderPackage; + *CharacterTransparencyShaderPackage = DefaultCharacterTransparencyShaderPackage; + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *IrisShaderPackage = DefaultIrisShaderPackage; } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a03f384..0c2581bf 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -177,6 +177,8 @@ public class DebugTab : Window, ITab, IUiService ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawShaderReplacementFixer(); + ImGui.NewLine(); DrawData(); ImGui.NewLine(); DrawResourceProblems(); @@ -711,27 +713,6 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; - if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) - _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - - if (enableShaderReplacementFixer) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); - ImGui.SameLine(); - ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}"); - ImGui.SameLine(); - ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}"); - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_shaderReplacementFixer.ModdedSkinShpkCount}"); - ImGui.SameLine(); - ImGui.TextUnformatted($"characterglass.shpk: {_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); - } - using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -786,6 +767,80 @@ public class DebugTab : Window, ITab, IUiService } } + private void DrawShaderReplacementFixer() + { + if (!ImGui.CollapsingHeader("Shader Replacement Fixer")) + return; + + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; + + if (!enableShaderReplacementFixer) + return; + + using var table = Table("##ShaderReplacementFixer", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); + + ImGui.TableSetupColumn("Shader Package Name", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Materials with Modded ShPk", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("skin.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("iris.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterglass.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertattoo.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterocclusion.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterOcclusionShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("hairmask.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + } + /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { From 68135f3757eb4de0cd7ce83f37c7d7a6544656fd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 14:39:03 +0200 Subject: [PATCH 1791/2451] Update Gamedata --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 10 ++-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 1 - Penumbra/Interop/Hooks/Meta/GmpHook.cs | 47 ++++--------------- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 ++-- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 ++-- .../Interop/Hooks/Objects/CopyCharacter.cs | 5 +- .../Hooks/Objects/CreateCharacterBase.cs | 6 +-- .../Interop/Structs/CharacterUtilityData.cs | 10 ++-- Penumbra/Interop/Structs/MetaIndex.cs | 13 +++-- 11 files changed, 40 insertions(+), 75 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19923f8d..62f6acfb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 +Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index ed4eb669..9dd8d74f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -4,11 +4,11 @@ public static class HookSettings { public const bool AllHooks = true; - public const bool ObjectHooks = false && AllHooks; + public const bool ObjectHooks = true && AllHooks; public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = false && AllHooks; - public const bool MetaEntryHooks = false && AllHooks; - public const bool MetaParentHooks = false && AllHooks; + public const bool ResourceHooks = true && AllHooks; + public const bool MetaEntryHooks = true && AllHooks; + public const bool MetaParentHooks = true && AllHooks; public const bool VfxIdentificationHooks = false && AllHooks; - public const bool PostProcessingHooks = false && AllHooks; + public const bool PostProcessingHooks = true && AllHooks; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 3767c4a2..e90674a8 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 329a8beb..12b221d9 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Data.Parsing.Uld; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -8,12 +10,10 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class GmpHook : FastHook, IDisposable { - public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + public delegate ulong Delegate(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId); private readonly MetaState _metaState; - private static readonly Finalizer StablePointer = new(); - public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; @@ -21,50 +21,21 @@ public unsafe class GmpHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - /// - /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . - /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, - /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. - /// - private nint Detour(nint gmpResource, uint dividedHeadId) + private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) { - nint ret; + ulong ret; if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) - { - if (entry.Entry.Enabled) - { - *StablePointer.Pointer = entry.Entry.Value; - // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. - // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); - } - else - { - ret = nint.Zero; - } - } + ret = (*outputEntry) = entry.Entry.Enabled ? entry.Entry.Value : 0ul; else - { - ret = Task.Result.Original(gmpResource, dividedHeadId); - } + ret = Task.Result.Original(characterUtility, outputEntry, setId); - Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + Penumbra.Log.Excessive( + $"[GetGmpFlags] Invoked on 0x{(ulong)characterUtility:X} for {setId} with 0x{(ulong)outputEntry:X} (={*outputEntry:X}), returned {ret:X10}."); return ret; } - /// Allocate and clean up our single stable ulong pointer. - private class Finalizer - { - public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); - - ~Finalizer() - { - Marshal.FreeHGlobal((nint)Pointer); - } - } - public void Dispose() => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 79e7f6a6..c1803745 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index eb8a8a37..e08dc393 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -10,8 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class RspBustHook : FastHook, IDisposable { - public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, - byte bustSize); + public delegate float* Delegate(nint cmpResource, float* storage, SubRace race, byte gender, byte bodyType, byte bustSize); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -24,7 +23,7 @@ public unsafe class RspBustHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) { if (gender == 0) { @@ -38,7 +37,6 @@ public unsafe class RspBustHook : FastHook, IDisposable if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); @@ -58,11 +56,11 @@ public unsafe class RspBustHook : FastHook, IDisposable } else { - ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + ret = Task.Result.Original(cmpResource, storage, clan, gender, bodyType, bustSize); } Penumbra.Log.Excessive( - $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index f8f9e51e..20e3c939 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public class RspHeightHook : FastHook, IDisposable { - public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + public delegate float Delegate(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -23,7 +23,7 @@ public class RspHeightHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) { float scale; if (bodyType < 2 @@ -36,7 +36,6 @@ public class RspHeightHook : FastHook, IDisposable if (height > 100) height = 0; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); @@ -68,11 +67,11 @@ public class RspHeightHook : FastHook, IDisposable } else { - scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + scale = Task.Result.Original(cmpResource, clan, gender, bodyType, height); } Penumbra.Log.Excessive( - $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {height}, returned {scale}."); return scale; } diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 663209ae..d81043c8 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -9,7 +9,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr + /// CutsceneService = 0, } @@ -38,8 +38,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtrOwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); Invoke(character, source); return _task.Result.Original(target, source, unk); diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 56b3d853..f00a9984 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -10,7 +10,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// MetaState = 0, } @@ -64,10 +64,10 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MetaState = 0, } } diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 22150cc1..d33da477 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -6,16 +6,16 @@ namespace Penumbra.Interop.Structs; public unsafe struct CharacterUtilityData { public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - public const int IndexSkinShpk = 76; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexSkinShpk = 83; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 87; + public const int TotalNumResources = 89; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) @@ -36,7 +36,7 @@ public unsafe struct CharacterUtilityData 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, - //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs index 65302264..2ec5fce4 100644 --- a/Penumbra/Interop/Structs/MetaIndex.cs +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -4,6 +4,7 @@ namespace Penumbra.Interop.Structs; public enum MetaIndex : int { Eqp = 0, + Evp = 1, Gmp = 2, Eqdp0101 = 3, @@ -21,9 +22,8 @@ public enum MetaIndex : int Eqdp1301, Eqdp1401, Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, + Eqdp1601, + Eqdp1701, Eqdp1801, Eqdp0104, Eqdp0204, @@ -51,9 +51,8 @@ public enum MetaIndex : int Eqdp1301Acc, Eqdp1401Acc, Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1601Acc, + Eqdp1701Acc, Eqdp1801Acc, Eqdp0104Acc, Eqdp0204Acc, @@ -66,7 +65,7 @@ public enum MetaIndex : int Eqdp9104Acc, Eqdp9204Acc, - HumanCmp = 64, + HumanCmp = 71, FaceEst, HairEst, HeadEst, From 4f0f3721a6296e0fad1c816c06e42d0ac3cf0de0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jul 2024 16:16:58 +0200 Subject: [PATCH 1792/2451] Update animation hooks. --- Penumbra/Interop/Hooks/Animation/Dismount.cs | 13 ++++---- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 8 ++--- .../Hooks/Animation/LoadCharacterSound.cs | 16 ++++++---- .../Hooks/Animation/LoadTimelineResources.cs | 32 +++++++++---------- Penumbra/Interop/Hooks/HookSettings.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 30 +++++++++-------- .../Hooks/ResourceLoading/TexMdlService.cs | 18 +++++++++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Interop/Structs/ClipScheduler.cs | 4 ++- Penumbra/Interop/Structs/StructExtensions.cs | 12 +++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 7 ++-- 11 files changed, 83 insertions(+), 61 deletions(-) diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 523e750c..034011e7 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData; @@ -18,26 +19,26 @@ public sealed unsafe class Dismount : FastHook Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); } - public delegate void Delegate(nint a1, nint a2); + public delegate void Delegate(MountContainer* a1, nint a2); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(nint a1, nint a2) + private void Detour(MountContainer* a1, nint a2) { - Penumbra.Log.Excessive($"[Dismount] Invoked on {a1:X} with {a2:X}."); - if (a1 == nint.Zero) + Penumbra.Log.Excessive($"[Dismount] Invoked on 0x{(nint)a1:X} with {a2:X}."); + if (a1 == null) { Task.Result.Original(a1, a2); return; } - var gameObject = *(GameObject**)(a1 + 8); + var gameObject = a1->OwnerObject; if (gameObject == null) { Task.Result.Original(a1, a2); return; } - var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*) gameObject, true)); Task.Result.Original(a1, a2); _state.RestoreAnimationData(last); } diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 0f51157c..cfab29d3 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -29,13 +29,13 @@ public sealed unsafe class LoadAreaVfx : FastHook private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) { var newData = caster != null - ? _collectionResolver.IdentifyCollection(caster, true) - : ResolveData.Invalid; + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx); - var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Excessive( + var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); + Penumbra.Log.Information( $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); _state.RestoreAnimationData(last); return ret; diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index ed04880e..8d1096d2 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -16,23 +16,25 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, HookSettings.VfxIdentificationHooks); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, + HookSettings.VfxIdentificationHooks); } - public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); + public delegate nint Delegate(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) + private nint Detour(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) { - var character = *(GameObject**)(container + 8); + var character = (GameObject*)container->OwnerObject; var newData = _collectionResolver.IdentifyCollection(character, true); var last = _state.SetSoundData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterSound); var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); + Penumbra.Log.Excessive( + $"[Load Character Sound] Invoked with {(nint)container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); _state.RestoreSoundData(last); return ret; } diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 4e9037bd..8bb14db6 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Base; using OtterGui.Services; using Penumbra.Collections; using Penumbra.GameData; @@ -25,45 +26,44 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); + _conditions = conditions; + _objects = objects; + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); } - public delegate ulong Delegate(nint timeline); + public delegate ulong Delegate(SchedulerTimeline* timeline); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(nint timeline) + private ulong Detour(SchedulerTimeline* timeline) { - Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {timeline:X}."); + Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {(nint)timeline:X}."); // Do not check timeline loading in cutscenes. if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) return Task.Result.Original(timeline); var newData = GetDataFromTimeline(_objects, _collectionResolver, timeline); - var last = _state.SetAnimationData(newData); - + var last = _state.SetAnimationData(newData); + #if false // This is called far too often and spams the log too much. _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadTimelineResources); #endif - var ret = Task.Result.Original(timeline); + var ret = Task.Result.Original(timeline); _state.RestoreAnimationData(last); return ret; } /// Use timelines vfuncs to obtain the associated game object. - public static ResolveData GetDataFromTimeline(ObjectManager objects, CollectionResolver resolver, nint timeline) + public static ResolveData GetDataFromTimeline(ObjectManager objects, CollectionResolver resolver, SchedulerTimeline* timeline) { try { - if (timeline != nint.Zero) + if (timeline != null) { - var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; - var idx = getGameObjectIdx(timeline); + var idx = timeline->GetOwningGameObjectIndex(); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; @@ -73,7 +73,7 @@ public sealed unsafe class LoadTimelineResources : FastHook Load a resource for a given path and a specific collection. @@ -80,10 +80,10 @@ public unsafe class ResourceLoader : IDisposable, IService public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceRequested -= ResourceHandler; _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _fileReadService.ReadSqPack -= ReadSqPackDetour; } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, @@ -94,6 +94,9 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); + if (path.ToString() == "vfx/common/eff/abi_cnj022g.avfx") + ; + // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) @@ -112,7 +115,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; - path = p; + path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -121,7 +124,8 @@ public unsafe class ResourceLoader : IDisposable, IService { if (fileDescriptor->ResourceHandle == null) { - Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor."); + Penumbra.Log.Verbose( + $"[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor: {Marshal.PtrToStringUni((nint)(&fileDescriptor->Utf16FileName))}"); return; } @@ -140,12 +144,12 @@ public unsafe class ResourceLoader : IDisposable, IService } var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); - fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; } @@ -165,7 +169,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; fileDescriptor->FileDescriptor = (byte*)fd + 1; - CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. @@ -206,7 +210,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; _incMode.Value = true; - returnValue = _resources.IncRef(handle); + returnValue = _resources.IncRef(handle); _incMode.Value = false; } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 793d15af..80ba5cb9 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -87,6 +87,11 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly ThreadLocal _texReturnData = new(() => default); + private delegate void UpdateCategoryDelegate(TextureResourceHandle* resourceHandle); + + [Signature(Sigs.TexHandleUpdateCategory)] + private readonly UpdateCategoryDelegate _updateCategory = null!; + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. @@ -99,9 +104,14 @@ public unsafe class TexMdlService : IDisposable, IRequiredService return CustomFileFlag; if (_customTexCrc.Contains(crc64)) + { _texReturnData.Value = true; + return nint.Zero; + } - return _checkFileStateHook.Original(ptr, crc64); + var ret = _checkFileStateHook.Original(ptr, crc64); + Penumbra.Log.Excessive($"[CheckFileState] Called on 0x{ptr:X} with CRC {crc64:X16}, returned 0x{ret:X}."); + return ret; } private delegate byte LoadTexFileLocalDelegate(TextureResourceHandle* handle, int unk1, SeFileDescriptor* unk2, bool unk3); @@ -118,7 +128,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - [Signature(Sigs.TexResourceHandleOnLoad, DetourName = nameof(OnLoadDetour))] + [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnLoadDetour))] private readonly Hook _textureOnLoadHook = null!; private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) @@ -129,7 +139,9 @@ public unsafe class TexMdlService : IDisposable, IRequiredService // Function failed on a replaced texture, call local. _texReturnData.Value = false; - return _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); + ret = _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); + _updateCategory(handle); + return ret; } private delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 810c946d..96125df2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -124,7 +124,7 @@ public class ResourceTree // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; - var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain1, weapon->Stain2)); + var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1)); var weaponType = weapon->SecondaryId; var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index 44a905b8..8270e0f2 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Base; + namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] @@ -7,5 +9,5 @@ public unsafe struct ClipScheduler public nint* VTable; [FieldOffset(0x38)] - public nint SchedulerTimeline; + public SchedulerTimeline* SchedulerTimeline; } diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 3cd87424..fc8b1c3d 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -9,37 +9,37 @@ internal static class StructExtensions public static unsafe ByteString AsByteString(in this StdString str) => ByteString.FromSpanUnsafe(str.AsSpan(), true); - public static ByteString ResolveEidPathAsByteString(in this CharacterBase character) + public static ByteString ResolveEidPathAsByteString(ref this CharacterBase character) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static ByteString ResolveImcPathAsByteString(in this CharacterBase character, uint slotIndex) + public static ByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); } - public static ByteString ResolveMdlPathAsByteString(in this CharacterBase character, uint slotIndex) + public static ByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); } - public static unsafe ByteString ResolveMtrlPathAsByteString(in this CharacterBase character, uint slotIndex, byte* mtrlFileName) + public static unsafe ByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) { var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); } - public static ByteString ResolveSklbPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + public static ByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); } - public static ByteString ResolveSkpPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + public static ByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a03f384..180049bc 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -430,7 +430,7 @@ public class DebugTab : Window, ITab, IUiService foreach (var obj in _objects) { - ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty @@ -482,14 +482,15 @@ public class DebugTab : Window, ITab, IUiService { var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{drawObject:X}"); + + ImGuiUtil.CopyOnClickSelectable($"0x{drawObject:X}"); ImGui.TableNextColumn(); ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); ImGui.TableNextColumn(); ImGui.TextUnformatted(child ? "Child" : "Main"); ImGui.TableNextColumn(); var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); - ImGui.TextUnformatted(address); + ImGuiUtil.CopyOnClickSelectable(address); ImGui.TableNextColumn(); ImGui.TextUnformatted(name); ImGui.TableNextColumn(); From 0d939b12f4381a8dc4e6c96d977fdbd03348424f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jul 2024 16:17:12 +0200 Subject: [PATCH 1793/2451] Add model update button. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 15 +++++++++------ .../AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 1 - .../UI/AdvancedWindow/ModEditWindow.Models.cs | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index eeb94c71..2c6ac170 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -207,12 +207,15 @@ public class FileEditor( var canSave = _changed && _currentFile is { Valid: true }; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) - { - compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); - if (owner.Mod != null) - communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); - _changed = false; - } + SaveFile(); + } + + public void SaveFile() + { + compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + if (owner.Mod != null) + communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); + _changed = false; } private void ResetButton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 72ab37b2..b05bcac2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,3 @@ -using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bbed64b7..de088736 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -4,6 +4,7 @@ using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -67,6 +68,7 @@ public partial class ModEditWindow { var ret = tab.Dirty; var data = UpdateFile(tab.Mdl, ret, disabled); + DrawVersionUpdate(tab, disabled); DrawImportExport(tab, disabled); ret |= DrawModelMaterialDetails(tab, disabled); @@ -80,6 +82,19 @@ public partial class ModEditWindow return !disabled && ret; } + private void DrawVersionUpdate(MdlTab tab, bool disabled) + { + if (disabled || tab.Mdl.Version is not MdlFile.V5) + return; + + if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return; + + tab.Mdl.ConvertV5ToV6(); + _modelTab.SaveFile(); + } + private void DrawImportExport(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Import / Export")) From 56e284a99eedb73f2053d4e961d714edcbfc1937 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 14:55:49 +0200 Subject: [PATCH 1794/2451] Add some migration things. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 2 + Penumbra/Import/TexToolsImport.cs | 33 +- Penumbra/Import/TexToolsImporter.Archives.cs | 26 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 23 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 3 - .../ResolveContext.PathResolution.cs | 50 +-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 6 +- Penumbra/Mods/Manager/ModImportManager.cs | 5 +- Penumbra/Services/MigrationManager.cs | 287 ++++++++++++++++++ .../AdvancedWindow/ModEditWindow.Materials.cs | 16 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 4 +- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 121 ++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- 17 files changed, 515 insertions(+), 80 deletions(-) create mode 100644 Penumbra/Services/MigrationManager.cs create mode 100644 Penumbra/UI/Classes/MigrationSectionDrawer.cs diff --git a/OtterGui b/OtterGui index c2738e1d..89b3b951 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d +Subproject commit 89b3b9513f9b4989045517a452ef971e24377203 diff --git a/Penumbra.GameData b/Penumbra.GameData index 62f6acfb..b5eb074d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390 +Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 49aecfdc..8d0f7fd8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -99,6 +99,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; + public bool MigrateImportedModelsToV6 { get; set; } = false; + public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; public bool KeepDefaultMetaChanges { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index bb006d8d..ba089662 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -3,6 +3,7 @@ using OtterGui.Compression; using Penumbra.Import.Structs; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Services; using FileMode = System.IO.FileMode; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry; @@ -27,24 +28,26 @@ public partial class TexToolsImporter : IDisposable public ImporterState State { get; private set; } public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods; - private readonly Configuration _config; - private readonly ModEditor _editor; - private readonly ModManager _modManager; - private readonly FileCompactor _compactor; + private readonly Configuration _config; + private readonly ModEditor _editor; + private readonly ModManager _modManager; + private readonly FileCompactor _compactor; + private readonly MigrationManager _migrationManager; public TexToolsImporter(int count, IEnumerable modPackFiles, Action handler, - Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor) + Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor, MigrationManager migrationManager) { - _baseDirectory = modManager.BasePath; - _tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName); - _modPackFiles = modPackFiles; - _config = config; - _editor = editor; - _modManager = modManager; - _compactor = compactor; - _modPackCount = count; - ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); - _token = _cancellation.Token; + _baseDirectory = modManager.BasePath; + _tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName); + _modPackFiles = modPackFiles; + _config = config; + _editor = editor; + _modManager = modManager; + _compactor = compactor; + _migrationManager = migrationManager; + _modPackCount = count; + ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); + _token = _cancellation.Token; Task.Run(ImportFiles, _token) .ContinueWith(_ => CloseStreams(), TaskScheduler.Default) .ContinueWith(_ => diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 57313ab1..a51dbc61 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -2,6 +2,7 @@ using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; +using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; @@ -9,12 +10,19 @@ using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; using SharpCompress.Readers; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; public partial class TexToolsImporter { + private static readonly ExtractionOptions _extractionOptions = new() + { + ExtractFullPath = true, + Overwrite = true, + }; + /// /// Extract regular compressed archives that are folders containing penumbra-formatted mods. /// The mod has to either contain a meta.json at top level, or one folder deep. @@ -45,11 +53,7 @@ public partial class TexToolsImporter Penumbra.Log.Information($" -> Importing {archive.Type} Archive."); _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true); - var options = new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true, - }; + State = ImporterState.ExtractingModFiles; _currentFileIdx = 0; @@ -86,7 +90,7 @@ public partial class TexToolsImporter } else { - reader.WriteEntryToDirectory(_currentModDirectory.FullName, options); + HandleFileMigrations(reader); } ++_currentFileIdx; @@ -114,6 +118,16 @@ public partial class TexToolsImporter } + private void HandleFileMigrations(IReader reader) + { + switch (Path.GetExtension(reader.Entry.Key)) + { + case ".mdl": + _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + break; + } + } + // Search the archive for the meta.json file which needs to exist. private static string FindArchiveModMeta(IArchive archive, out bool leadDir) { diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 2b45ecbe..ba294353 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -253,25 +253,12 @@ public partial class TexToolsImporter extractedFile.Directory?.Create(); - if (extractedFile.FullName.EndsWith(".mdl")) - ProcessMdl(data.Data); + data.Data = Path.GetExtension(extractedFile.FullName) switch + { + ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), + _ => data.Data, + }; _compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token); } - - private static void ProcessMdl(byte[] mdl) - { - const int modelHeaderLodOffset = 22; - - // Model file header LOD num - mdl[64] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32(mdl, 4); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32(mdl, (int)stringsLengthOffset); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - mdl[modelHeaderStart + modelHeaderLodOffset] = 1; - } } diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index cfab29d3..48dc0078 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -35,7 +35,7 @@ public sealed unsafe class LoadAreaVfx : FastHook var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx); var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); _state.RestoreAnimationData(last); return ret; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index af637768..195a8b9e 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -94,9 +94,6 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); - if (path.ToString() == "vfx/common/eff/abi_cnj022g.avfx") - ; - // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 4dfefd96..72cb1681 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -81,11 +81,12 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), - ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), - _ => ResolveMaterialPathNative(mtrlFileName), + ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), + _ => ResolveMaterialPathNative(mtrlFileName), }; } @@ -96,7 +97,7 @@ internal partial record ResolveContext var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } @@ -126,7 +127,7 @@ internal partial record ResolveContext WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); if (weaponPosition >= 0) @@ -145,7 +146,7 @@ internal partial record ResolveContext var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } @@ -166,7 +167,8 @@ internal partial record ResolveContext return entry.MaterialId; } - private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, ReadOnlySpan mtrlFileName) + private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, + ReadOnlySpan mtrlFileName) { var modelPosition = modelPath.IndexOf("/model/"u8); if (modelPosition < 0) @@ -187,8 +189,8 @@ internal partial record ResolveContext { for (var i = destination.Length; i-- > 0;) { - destination[i] = (byte)('0' + number % 10); - number /= 10; + destination[i] = (byte)('0' + number % 10); + number /= 10; } } @@ -197,13 +199,17 @@ internal partial record ResolveContext ByteString? path; try { + Penumbra.Log.Information($"{(nint)CharacterBase:X} {ModelType} {SlotIndex} 0x{(ulong)mtrlFileName:X}"); + Penumbra.Log.Information($"{new ByteString(mtrlFileName)}"); path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) { - Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); + Penumbra.Log.Error( + $"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); return Utf8GamePath.Empty; } + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -235,30 +241,23 @@ internal partial record ResolveContext var characterRaceCode = (GenderRace)human->RaceSexId; switch (partialSkeletonIndex) { - case 0: - return (characterRaceCode, "base", 1); + case 0: return (characterRaceCode, "base", 1); case 1: var faceId = human->FaceId; var tribe = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.Tribe]; var modelType = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.ModelType]; if (faceId < 201) - { faceId -= tribe switch { 0xB when modelType == 4 => 100, 0xE | 0xF => 100, _ => 0, }; - } return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId); - case 2: - return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); - case 3: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); - case 4: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); - default: - return (0, string.Empty, 0); + case 2: return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); + case 3: return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); + case 4: return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); + default: return (0, string.Empty, 0); } } @@ -269,7 +268,8 @@ internal partial record ResolveContext return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) + private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, + PrimaryId primary) { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 96125df2..6663fb40 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -73,11 +73,11 @@ public class ResourceTree var genericContext = globalContext.CreateContext(model); - for (var i = 0; i < model->SlotCount; ++i) + for (var i = 0u; i < model->SlotCount; ++i) { var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i]) - : globalContext.CreateContext(model, (uint)i); + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i); var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 39a53bb9..d984d374 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -3,10 +3,11 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.Import; using Penumbra.Mods.Editor; +using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor, MigrationManager migrationManager) : IDisposable, IService { private readonly ConcurrentQueue _modsToUnpack = new(); @@ -42,7 +43,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd if (files.Length == 0) return; - _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor); + _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor, migrationManager); } public bool Importing diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs new file mode 100644 index 00000000..a4b80656 --- /dev/null +++ b/Penumbra/Services/MigrationManager.cs @@ -0,0 +1,287 @@ +using Dalamud.Interface.ImGuiNotification; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Files; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace Penumbra.Services; + +public class MigrationManager(Configuration config) : IService +{ + private Task? _currentTask; + private CancellationTokenSource? _source; + + public bool HasCleanUpTask { get; private set; } + public bool HasMigrationTask { get; private set; } + public bool HasRestoreTask { get; private set; } + + public bool IsMigrationTask { get; private set; } + public bool IsRestorationTask { get; private set; } + public bool IsCleanupTask { get; private set; } + + + public int Restored { get; private set; } + public int RestoreFails { get; private set; } + + public int CleanedUp { get; private set; } + + public int CleanupFails { get; private set; } + + public int Migrated { get; private set; } + + public int Unchanged { get; private set; } + + public int Failed { get; private set; } + + public bool IsRunning + => _currentTask is { IsCompleted: false }; + + /// Writes or migrates a .mdl file during extraction from a regular archive. + public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedModelsToV6) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + using var b = new BinaryReader(s); + var version = b.ReadUInt32(); + if (version == MdlFile.V5) + { + var data = s.ToArray(); + var mdl = new MdlFile(data); + MigrateModel(path, mdl, false); + Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); + } + else + { + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + } + + public void CleanBackups(string path) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasCleanUpTask = true; + IsCleanupTask = true; + IsMigrationTask = false; + IsRestorationTask = false; + CleanedUp = 0; + CleanupFails = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + try + { + File.Delete(file); + ++CleanedUp; + Penumbra.Log.Debug($"Deleted model backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to delete model backup file {file}", NotificationType.Warning); + ++CleanupFails; + } + } + }, token); + } + + public void RestoreBackups(string path) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasRestoreTask = true; + IsCleanupTask = false; + IsMigrationTask = false; + IsRestorationTask = true; + CleanedUp = 0; + CleanupFails = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + var target = file[..^4]; + try + { + File.Copy(file, target, true); + ++Restored; + Penumbra.Log.Debug($"Restored model backup file {file} to {target}."); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to restore model backup file {file} to {target}", + NotificationType.Warning); + ++RestoreFails; + } + } + }, token); + } + + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpModel(string path, byte[] data) + { + FixLodNum(data); + if (!config.MigrateImportedModelsToV6) + return data; + + var version = BitConverter.ToUInt32(data); + if (version != 5) + return data; + + var mdl = new MdlFile(data); + if (!mdl.ConvertV5ToV6()) + return data; + + data = mdl.Write(); + Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); + return data; + } + + public void MigrateDirectory(string path, bool createBackups) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasMigrationTask = true; + IsCleanupTask = false; + IsMigrationTask = true; + IsRestorationTask = false; + Unchanged = 0; + Migrated = 0; + Failed = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + var timer = Stopwatch.StartNew(); + try + { + var data = File.ReadAllBytes(file); + var mdl = new MdlFile(data); + if (MigrateModel(file, mdl, createBackups)) + { + ++Migrated; + Penumbra.Log.Debug($"Migrated model file {file} from V5 to V6 in {timer.ElapsedMilliseconds} ms."); + } + else + { + ++Unchanged; + Penumbra.Log.Verbose($"Verified that model file {file} is already V6 in {timer.ElapsedMilliseconds} ms."); + } + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate model file {file} to V6 in {timer.ElapsedMilliseconds} ms", + NotificationType.Warning); + ++Failed; + } + } + }, token); + } + + public void Cancel() + { + _source?.Cancel(); + _source = null; + _currentTask = null; + } + + private static void FixLodNum(byte[] data) + { + const int modelHeaderLodOffset = 22; + + // Model file header LOD num + data[64] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32(data, 4); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + data[modelHeaderStart + modelHeaderLodOffset] = 1; + } + + public static bool TryMigrateSingleModel(string path, bool createBackup) + { + try + { + var data = File.ReadAllBytes(path); + var mdl = new MdlFile(data); + return MigrateModel(path, mdl, createBackup); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the model {path} to V6", NotificationType.Warning); + return false; + } + } + + public static bool TryMigrateSingleMaterial(string path, bool createBackup) + { + try + { + var data = File.ReadAllBytes(path); + var mtrl = new MtrlFile(data); + return MigrateMaterial(path, mtrl, createBackup); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the material {path} to Dawntrail", NotificationType.Warning); + return false; + } + } + + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) + { + if (!mdl.ConvertV5ToV6()) + return false; + + var data = mdl.Write(); + if (createBackup) + File.Copy(path, Path.ChangeExtension(path, ".mdl.bak")); + File.WriteAllBytes(path, data); + return true; + } + + private static bool MigrateMaterial(string path, MtrlFile mtrl, bool createBackup) + { + if (!mtrl.MigrateToDawntrail()) + return false; + + var data = mtrl.Write(); + + mtrl.Write(); + if (createBackup) + File.Copy(path, Path.ChangeExtension(path, ".mtrl.bak")); + File.WriteAllBytes(path, data); + return true; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 68b3717f..0223ca6b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -16,6 +17,7 @@ public partial class ModEditWindow private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { + DrawVersionUpdate(tab, disabled); DrawMaterialLivePreviewRebind(tab, disabled); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -34,6 +36,20 @@ public partial class ModEditWindow return !disabled && ret; } + private void DrawVersionUpdate(MtrlTab tab, bool disabled) + { + if (disabled || tab.Mtrl.IsDawnTrail) + return; + + if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, + "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return; + + tab.Mtrl.MigrateToDawntrail(); + _materialTab.SaveFile(); + } + private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) { if (disabled) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 90fdc48e..83a8958b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -37,6 +37,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; + public readonly MigrationManager MigrationManager; + private readonly PerformanceTracker _performance; private readonly ModEditor _editor; private readonly Configuration _config; @@ -588,7 +590,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers) + CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager) : base(WindowBaseLabel) { _performance = performance; @@ -608,6 +610,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; + MigrationManager = migrationManager; _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 6c8bbf64..0f9b2518 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -37,9 +37,9 @@ public class CollectionSelectHeader : IUiService var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) { - DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); - DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); _collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value()); diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs new file mode 100644 index 00000000..75d37368 --- /dev/null +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -0,0 +1,121 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Services; + +namespace Penumbra.UI.Classes; + +public class MigrationSectionDrawer(MigrationManager migrationManager, Configuration config) : IUiService +{ + private bool _createBackups = true; + private Vector2 _buttonSize; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Migration"u8); + if (!header) + return; + + _buttonSize = UiHelpers.InputTextWidth; + DrawSettings(); + ImGui.Separator(); + DrawMigration(); + ImGui.Separator(); + DrawCleanup(); + ImGui.Separator(); + DrawRestore(); + } + + private void DrawSettings() + { + var value = config.MigrateImportedModelsToV6; + if (ImUtf8.Checkbox("Automatically Migrate V5 Models to V6 on Import"u8, ref value)) + { + config.MigrateImportedModelsToV6 = value; + config.Save(); + } + } + + private void DrawMigration() + { + ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); + if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(0, "Cancel the migration. This does not revert already finished migrations."u8); + DrawSpinner(migrationManager is { IsMigrationTask: true, IsRunning: true }); + + if (!migrationManager.HasMigrationTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.Failed + migrationManager.Migrated + migrationManager.Unchanged; + if (total == 0) + ImUtf8.TextFrameAligned("No model files found."u8); + else + ImUtf8.TextFrameAligned($"{migrationManager.Migrated} files migrated, {migrationManager.Failed} files failed, {total} total files."); + } + + private void DrawCleanup() + { + if (ImUtf8.ButtonEx("Delete Existing Model Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.CleanBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(1, "Cancel the cleanup. This is not revertible."u8); + DrawSpinner(migrationManager is { IsCleanupTask: true, IsRunning: true }); + if (!migrationManager.HasCleanUpTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.CleanedUp + migrationManager.CleanupFails; + if (total == 0) + ImUtf8.TextFrameAligned("No model backup files found."u8); + else + ImUtf8.TextFrameAligned( + $"{migrationManager.CleanedUp} backups deleted, {migrationManager.CleanupFails} deletions failed, {total} total backups."); + } + + private void DrawSpinner(bool enabled) + { + if (!enabled) + return; + ImGui.SameLine(); + ImUtf8.Spinner("Spinner"u8, ImGui.GetTextLineHeight() / 2, 2, ImGui.GetColorU32(ImGuiCol.Text)); + } + + private void DrawRestore() + { + if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(2, "Cancel the restoration. This does not revert already finished restoration."u8); + DrawSpinner(migrationManager is { IsRestorationTask: true, IsRunning: true }); + + if (!migrationManager.HasRestoreTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.Restored + migrationManager.RestoreFails; + if (total == 0) + ImUtf8.TextFrameAligned("No model backup files found."u8); + else + ImUtf8.TextFrameAligned( + $"{migrationManager.Restored} backups restored, {migrationManager.RestoreFails} restorations failed, {total} total backups."); + } + + private void DrawCancelButton(int id, ReadOnlySpan tooltip) + { + using var _ = ImUtf8.PushId(id); + if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning)) + migrationManager.Cancel(); + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 49e77a4d..8a4d6874 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,10 +41,11 @@ public class SettingsTab : ITab, IUiService private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private readonly FileCompactor _compactor; private readonly DalamudConfigService _dalamudConfig; - private readonly IDalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; + private readonly MigrationSectionDrawer _migrationDrawer; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -55,7 +56,8 @@ public class SettingsTab : ITab, IUiService Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService) + IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, + MigrationSectionDrawer migrationDrawer) { _pluginInterface = pluginInterface; _config = config; @@ -77,6 +79,7 @@ public class SettingsTab : ITab, IUiService _compactor.Enabled = _config.UseFileSystemCompression; _predefinedTagManager = predefinedTagConfig; _crashService = crashService; + _migrationDrawer = migrationDrawer; } public void DrawHeader() @@ -102,6 +105,7 @@ public class SettingsTab : ITab, IUiService ImGui.NewLine(); DrawGeneralSettings(); + _migrationDrawer.Draw(); DrawColorSettings(); DrawPredefinedTagsSection(); DrawAdvancedSettings(); From f89eea721f524a4acc9b3e8041871bbbd1d8db66 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 14:59:35 +0200 Subject: [PATCH 1795/2451] Update game data. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b5eb074d..d7a56b70 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad +Subproject commit d7a56b708c73bc9917baeaa66842c1594ca3067b From 710f39768bec527db1de4172499dee2b80c7ac24 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 15:00:37 +0200 Subject: [PATCH 1796/2451] Disable the required ShadersKnown for the time being. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 91129129..cd7aca9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -691,8 +691,9 @@ public partial class ModEditWindow _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); } + // TODO Readd ShadersKnown public bool Valid - => ShadersKnown && Mtrl.Valid; + => (true || ShadersKnown) && Mtrl.Valid; public byte[] Write() { From b677a14ceffe6d8a1c0e94d47e1af3bfd19e1616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 16:34:31 +0200 Subject: [PATCH 1797/2451] Update. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 3 +- Penumbra/Import/TexToolsImporter.Archives.cs | 11 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 5 +- .../Animation/ApricotListenerSoundPlay.cs | 2 + Penumbra/Services/MigrationManager.cs | 340 +++++++++++------- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 155 +++++--- Penumbra/UI/Tabs/Debug/DebugTab.cs | 10 +- 9 files changed, 338 insertions(+), 192 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 43b0b47f..f4c6144c 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 43b0b47f2d019af0fe4681dfc578f9232e3ba90c +Subproject commit f4c6144ca2012b279e6d8aa52b2bef6cc2ba32d9 diff --git a/Penumbra.GameData b/Penumbra.GameData index d7a56b70..cf5be8af 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d7a56b708c73bc9917baeaa66842c1594ca3067b +Subproject commit cf5be8af4c9ecbd9190bd3db746743fa5cd1560f diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8d0f7fd8..f16569b5 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -99,7 +99,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; - public bool MigrateImportedModelsToV6 { get; set; } = false; + public bool MigrateImportedModelsToV6 { get; set; } = true; + public bool MigrateImportedMaterialsToLegacy { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index a51dbc61..63c170cb 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,3 +1,4 @@ +using System.IO; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -90,7 +91,7 @@ public partial class TexToolsImporter } else { - HandleFileMigrations(reader); + HandleFileMigrationsAndWrite(reader); } ++_currentFileIdx; @@ -118,13 +119,19 @@ public partial class TexToolsImporter } - private void HandleFileMigrations(IReader reader) + private void HandleFileMigrationsAndWrite(IReader reader) { switch (Path.GetExtension(reader.Entry.Key)) { case ".mdl": _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); break; + case ".mtrl": + _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + break; + default: + reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); + break; } } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index ba294353..3ae1eda9 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -255,8 +255,9 @@ public partial class TexToolsImporter data.Data = Path.GetExtension(extractedFile.FullName) switch { - ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), - _ => data.Data, + ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), + ".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data), + _ => data.Data, }; _compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token); diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index e58c7268..2e05c1b6 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -1,3 +1,4 @@ +using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -16,6 +17,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook Changed + Unchanged + Failed; + + public void Init() + { + Changed = 0; + Unchanged = 0; + Failed = 0; + HasData = true; + } + } + private Task? _currentTask; private CancellationTokenSource? _source; - public bool HasCleanUpTask { get; private set; } - public bool HasMigrationTask { get; private set; } - public bool HasRestoreTask { get; private set; } + public TaskType CurrentTask { get; private set; } - public bool IsMigrationTask { get; private set; } - public bool IsRestorationTask { get; private set; } - public bool IsCleanupTask { get; private set; } + public readonly MigrationData MdlMigration = new(true); + public readonly MigrationData MtrlMigration = new(true); + public readonly MigrationData MdlCleanup = new(false); + public readonly MigrationData MtrlCleanup = new(false); + public readonly MigrationData MdlRestoration = new(false); + public readonly MigrationData MtrlRestoration = new(false); - public int Restored { get; private set; } - public int RestoreFails { get; private set; } - - public int CleanedUp { get; private set; } - - public int CleanupFails { get; private set; } - - public int Migrated { get; private set; } - - public int Unchanged { get; private set; } - - public int Failed { get; private set; } - public bool IsRunning => _currentTask is { IsCompleted: false }; - /// Writes or migrates a .mdl file during extraction from a regular archive. - public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) - { - if (!config.MigrateImportedModelsToV6) - { - reader.WriteEntryToDirectory(directory, options); - return; - } + public void CleanMdlBackups(string path) + => CleanBackups(path, "*.mdl.bak", "model", MdlCleanup, TaskType.MdlCleanup); - var path = Path.Combine(directory, reader.Entry.Key); - using var s = new MemoryStream(); - using var e = reader.OpenEntryStream(); - e.CopyTo(s); - using var b = new BinaryReader(s); - var version = b.ReadUInt32(); - if (version == MdlFile.V5) - { - var data = s.ToArray(); - var mdl = new MdlFile(data); - MigrateModel(path, mdl, false); - Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); - } - else - { - using var f = File.Open(path, FileMode.Create, FileAccess.Write); - s.Seek(0, SeekOrigin.Begin); - s.WriteTo(f); - } - } + public void CleanMtrlBackups(string path) + => CleanBackups(path, "*.mtrl.bak", "material", MtrlCleanup, TaskType.MtrlCleanup); - public void CleanBackups(string path) + private void CleanBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) return; @@ -76,13 +72,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasCleanUpTask = true; - IsCleanupTask = true; - IsMigrationTask = false; - IsRestorationTask = false; - CleanedUp = 0; - CleanupFails = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -90,19 +82,25 @@ public class MigrationManager(Configuration config) : IService try { File.Delete(file); - ++CleanedUp; - Penumbra.Log.Debug($"Deleted model backup file {file}."); + ++data.Changed; + Penumbra.Log.Debug($"Deleted {fileType} backup file {file}."); } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to delete model backup file {file}", NotificationType.Warning); - ++CleanupFails; + Penumbra.Messager.NotificationMessage(ex, $"Failed to delete {fileType} backup file {file}", NotificationType.Warning); + ++data.Failed; } } }, token); } - public void RestoreBackups(string path) + public void RestoreMdlBackups(string path) + => RestoreBackups(path, "*.mdl.bak", "model", MdlRestoration, TaskType.MdlRestoration); + + public void RestoreMtrlBackups(string path) + => RestoreBackups(path, "*.mtrl.bak", "material", MtrlRestoration, TaskType.MtrlRestoration); + + private void RestoreBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) return; @@ -111,13 +109,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasRestoreTask = true; - IsCleanupTask = false; - IsMigrationTask = false; - IsRestorationTask = true; - CleanedUp = 0; - CleanupFails = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -126,40 +120,38 @@ public class MigrationManager(Configuration config) : IService try { File.Copy(file, target, true); - ++Restored; - Penumbra.Log.Debug($"Restored model backup file {file} to {target}."); + ++data.Changed; + Penumbra.Log.Debug($"Restored {fileType} backup file {file} to {target}."); } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to restore model backup file {file} to {target}", + Penumbra.Messager.NotificationMessage(ex, $"Failed to restore {fileType} backup file {file} to {target}", NotificationType.Warning); - ++RestoreFails; + ++data.Failed; } } }, token); } - /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. - public byte[] MigrateTtmpModel(string path, byte[] data) - { - FixLodNum(data); - if (!config.MigrateImportedModelsToV6) - return data; + public void MigrateMdlDirectory(string path, bool createBackups) + => MigrateDirectory(path, createBackups, "*.mdl", "model", MdlMigration, TaskType.MdlMigration, "from V5 to V6", "V6", + (file, fileData, backups) => + { + var mdl = new MdlFile(fileData); + return MigrateModel(file, mdl, backups); + }); - var version = BitConverter.ToUInt32(data); - if (version != 5) - return data; + public void MigrateMtrlDirectory(string path, bool createBackups) + => MigrateDirectory(path, createBackups, "*.mtrl", "material", MtrlMigration, TaskType.MtrlMigration, "to Dawntrail", "Dawntrail", + (file, fileData, backups) => + { + var mtrl = new MtrlFile(fileData); + return MigrateMaterial(file, mtrl, backups); + } + ); - var mdl = new MdlFile(data); - if (!mdl.ConvertV5ToV6()) - return data; - - data = mdl.Write(); - Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); - return data; - } - - public void MigrateDirectory(string path, bool createBackups) + private void MigrateDirectory(string path, bool createBackups, string extension, string fileType, MigrationData data, TaskType type, + string action, string state, Func func) { if (IsRunning) return; @@ -168,14 +160,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasMigrationTask = true; - IsCleanupTask = false; - IsMigrationTask = true; - IsRestorationTask = false; - Unchanged = 0; - Migrated = 0; - Failed = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -183,24 +170,24 @@ public class MigrationManager(Configuration config) : IService var timer = Stopwatch.StartNew(); try { - var data = File.ReadAllBytes(file); - var mdl = new MdlFile(data); - if (MigrateModel(file, mdl, createBackups)) + var fileData = File.ReadAllBytes(file); + if (func(file, fileData, createBackups)) { - ++Migrated; - Penumbra.Log.Debug($"Migrated model file {file} from V5 to V6 in {timer.ElapsedMilliseconds} ms."); + ++data.Changed; + Penumbra.Log.Debug($"Migrated {fileType} file {file} {action} in {timer.ElapsedMilliseconds} ms."); } else { - ++Unchanged; - Penumbra.Log.Verbose($"Verified that model file {file} is already V6 in {timer.ElapsedMilliseconds} ms."); + ++data.Unchanged; + Penumbra.Log.Verbose($"Verified that {fileType} file {file} is already {state} in {timer.ElapsedMilliseconds} ms."); } } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate model file {file} to V6 in {timer.ElapsedMilliseconds} ms", + ++data.Failed; + Penumbra.Messager.NotificationMessage(ex, + $"Failed to migrate {fileType} file {file} to {state} in {timer.ElapsedMilliseconds} ms", NotificationType.Warning); - ++Failed; } } }, token); @@ -213,22 +200,6 @@ public class MigrationManager(Configuration config) : IService _currentTask = null; } - private static void FixLodNum(byte[] data) - { - const int modelHeaderLodOffset = 22; - - // Model file header LOD num - data[64] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32(data, 4); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - data[modelHeaderStart + modelHeaderLodOffset] = 1; - } - public static bool TryMigrateSingleModel(string path, bool createBackup) { try @@ -259,6 +230,113 @@ public class MigrationManager(Configuration config) : IService } } + /// Writes or migrates a .mdl file during extraction from a regular archive. + public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedModelsToV6) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + using var b = new BinaryReader(s); + var version = b.ReadUInt32(); + if (version == MdlFile.V5) + { + var data = s.ToArray(); + var mdl = new MdlFile(data); + MigrateModel(path, mdl, false); + Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); + } + else + { + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + } + + public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedMaterialsToLegacy) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + var file = new MtrlFile(s.GetBuffer()); + if (!file.IsDawnTrail) + { + file.MigrateToDawntrail(); + Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); + } + + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpModel(string path, byte[] data) + { + FixLodNum(data); + if (!config.MigrateImportedModelsToV6) + return data; + + var version = BitConverter.ToUInt32(data); + if (version != 5) + return data; + + try + { + var mdl = new MdlFile(data); + if (!mdl.ConvertV5ToV6()) + return data; + + data = mdl.Write(); + Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); + return data; + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Failed to migrate model {path} from V5 to V6 during import:\n{ex}"); + return data; + } + } + + /// Update the data of a .mtrl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpMaterial(string path, byte[] data) + { + if (!config.MigrateImportedMaterialsToLegacy) + return data; + + try + { + var mtrl = new MtrlFile(data); + if (mtrl.IsDawnTrail) + return data; + + mtrl.MigrateToDawntrail(); + data = mtrl.Write(); + Penumbra.Log.Debug($"Migrated material {path} to Dawntrail during import."); + return data; + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Failed to migrate material {path} to Dawntrail during import:\n{ex}"); + return data; + } + } + + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) { if (!mdl.ConvertV5ToV6()) @@ -284,4 +362,20 @@ public class MigrationManager(Configuration config) : IService File.WriteAllBytes(path, data); return true; } + + private static void FixLodNum(byte[] data) + { + const int modelHeaderLodOffset = 22; + + // Model file header LOD num + data[64] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32(data, 4); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + data[modelHeaderStart + modelHeaderLodOffset] = 1; + } } diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index 75d37368..ec76ddae 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -19,11 +19,13 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura _buttonSize = UiHelpers.InputTextWidth; DrawSettings(); ImGui.Separator(); - DrawMigration(); + DrawMdlMigration(); + DrawMdlRestore(); + DrawMdlCleanup(); ImGui.Separator(); - DrawCleanup(); - ImGui.Separator(); - DrawRestore(); + DrawMtrlMigration(); + DrawMtrlRestore(); + DrawMtrlCleanup(); } private void DrawSettings() @@ -34,88 +36,125 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura config.MigrateImportedModelsToV6 = value; config.Save(); } - } - private void DrawMigration() - { - ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); - if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.MigrateDirectory(config.ModDirectory, _createBackups); + ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); - ImUtf8.SameLineInner(); - DrawCancelButton(0, "Cancel the migration. This does not revert already finished migrations."u8); - DrawSpinner(migrationManager is { IsMigrationTask: true, IsRunning: true }); - - if (!migrationManager.HasMigrationTask) + if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) { - ImUtf8.IconDummy(); - return; + config.MigrateImportedMaterialsToLegacy = value; + config.Save(); } - var total = migrationManager.Failed + migrationManager.Migrated + migrationManager.Unchanged; - if (total == 0) - ImUtf8.TextFrameAligned("No model files found."u8); - else - ImUtf8.TextFrameAligned($"{migrationManager.Migrated} files migrated, {migrationManager.Failed} files failed, {total} total files."); + ImUtf8.HoverTooltip( + "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); + + ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); } - private void DrawCleanup() + private static ReadOnlySpan MigrationTooltip + => "Cancel the migration. This does not revert already finished migrations."u8; + + private void DrawMdlMigration() + { + if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateMdlDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MdlMigration, "Cancel the migration. This does not revert already finished migrations."u8); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlMigration, IsRunning: true }); + DrawData(migrationManager.MdlMigration, "No model files found."u8, "migrated"u8); + } + + private void DrawMtrlMigration() + { + if (ImUtf8.ButtonEx("Migrate Material Files to Dawntrail"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateMtrlDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlMigration, MigrationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlMigration, IsRunning: true }); + DrawData(migrationManager.MtrlMigration, "No material files found."u8, "migrated"u8); + } + + + private static ReadOnlySpan CleanupTooltip + => "Cancel the cleanup. This is not revertible."u8; + + private void DrawMdlCleanup() { if (ImUtf8.ButtonEx("Delete Existing Model Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.CleanBackups(config.ModDirectory); + migrationManager.CleanMdlBackups(config.ModDirectory); ImUtf8.SameLineInner(); - DrawCancelButton(1, "Cancel the cleanup. This is not revertible."u8); - DrawSpinner(migrationManager is { IsCleanupTask: true, IsRunning: true }); - if (!migrationManager.HasCleanUpTask) - { - ImUtf8.IconDummy(); - return; - } - - var total = migrationManager.CleanedUp + migrationManager.CleanupFails; - if (total == 0) - ImUtf8.TextFrameAligned("No model backup files found."u8); - else - ImUtf8.TextFrameAligned( - $"{migrationManager.CleanedUp} backups deleted, {migrationManager.CleanupFails} deletions failed, {total} total backups."); + DrawCancelButton(MigrationManager.TaskType.MdlCleanup, CleanupTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlCleanup, IsRunning: true }); + DrawData(migrationManager.MdlCleanup, "No model backup files found."u8, "deleted"u8); } - private void DrawSpinner(bool enabled) + private void DrawMtrlCleanup() + { + if (ImUtf8.ButtonEx("Delete Existing Material Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.CleanMtrlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlCleanup, CleanupTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlCleanup, IsRunning: true }); + DrawData(migrationManager.MtrlCleanup, "No material backup files found."u8, "deleted"u8); + } + + private static ReadOnlySpan RestorationTooltip + => "Cancel the restoration. This does not revert already finished restoration."u8; + + private void DrawMdlRestore() + { + if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreMdlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MdlRestoration, RestorationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlRestoration, IsRunning: true }); + DrawData(migrationManager.MdlRestoration, "No model backup files found."u8, "restored"u8); + } + + private void DrawMtrlRestore() + { + if (ImUtf8.ButtonEx("Restore Material Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreMtrlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlRestoration, RestorationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlRestoration, IsRunning: true }); + DrawData(migrationManager.MtrlRestoration, "No material backup files found."u8, "restored"u8); + } + + private static void DrawSpinner(bool enabled) { if (!enabled) return; + ImGui.SameLine(); ImUtf8.Spinner("Spinner"u8, ImGui.GetTextLineHeight() / 2, 2, ImGui.GetColorU32(ImGuiCol.Text)); } - private void DrawRestore() + private void DrawCancelButton(MigrationManager.TaskType task, ReadOnlySpan tooltip) { - if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.RestoreBackups(config.ModDirectory); + using var _ = ImUtf8.PushId((int)task); + if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning || task != migrationManager.CurrentTask)) + migrationManager.Cancel(); + } - ImUtf8.SameLineInner(); - DrawCancelButton(2, "Cancel the restoration. This does not revert already finished restoration."u8); - DrawSpinner(migrationManager is { IsRestorationTask: true, IsRunning: true }); - - if (!migrationManager.HasRestoreTask) + private static void DrawData(MigrationManager.MigrationData data, ReadOnlySpan empty, ReadOnlySpan action) + { + if (!data.HasData) { ImUtf8.IconDummy(); return; } - var total = migrationManager.Restored + migrationManager.RestoreFails; + var total = data.Total; if (total == 0) - ImUtf8.TextFrameAligned("No model backup files found."u8); + ImUtf8.TextFrameAligned(empty); else - ImUtf8.TextFrameAligned( - $"{migrationManager.Restored} backups restored, {migrationManager.RestoreFails} restorations failed, {total} total backups."); - } - - private void DrawCancelButton(int id, ReadOnlySpan tooltip) - { - using var _ = ImUtf8.PushId(id); - if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning)) - migrationManager.Cancel(); + ImUtf8.TextFrameAligned($"{data.Changed} files {action}, {data.Failed} files failed, {total} files found."); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ace3d6a3..be92b94e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -433,10 +434,11 @@ public class DebugTab : Window, ITab, IUiService foreach (var obj in _objects) { ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); - ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); - ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero - ? string.Empty - : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}"); + ImGui.TableNextColumn(); + if (obj.Address != nint.Zero) + ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc From a0a3435918c8f644aa4cedefcd3d9efe8643ccf4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 16:50:48 +0200 Subject: [PATCH 1798/2451] Remove not-yet-existing CS requirement. --- Penumbra.GameData | 2 +- repo.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index cf5be8af..d8c784e4 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit cf5be8af4c9ecbd9190bd3db746743fa5cd1560f +Subproject commit d8c784e443112d17d93ba7e6ab5d54f00f7f1477 diff --git a/repo.json b/repo.json index 3142f8d4..6379595e 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 56502f19f9aa41003d532d68f225f7c956fa0f22 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 14:55:15 +0000 Subject: [PATCH 1799/2451] [CI] Updating repo.json for testing_1.2.0.0 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6379595e..c604a0dd 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.5", + "TestingAssemblyVersion": "1.2.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c2517499f27f1a7c83ad9510d3cbf9ad6ad255e1 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:04:27 +0200 Subject: [PATCH 1800/2451] Update repo.json --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index c604a0dd..4f567253 100644 --- a/repo.json +++ b/repo.json @@ -11,7 +11,7 @@ "ApplicableVersion": "any", "DalamudApiLevel": 10, "IsHide": "False", - "IsTestingExclusive": "False", + "IsTestingExclusive": "True", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, From 3c417d7aeca0fd5668475e7644185c73f31e50b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 17:48:34 +0200 Subject: [PATCH 1801/2451] Fix extraction of pmp failing --- Penumbra/Services/MigrationManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 1e6cb6b6..e377b65e 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -249,11 +249,13 @@ public class MigrationManager(Configuration config) : IService { var data = s.ToArray(); var mdl = new MdlFile(data); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); MigrateModel(path, mdl, false); Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); } else { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); @@ -279,6 +281,7 @@ public class MigrationManager(Configuration config) : IService Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); From 1efd4938343bb0106d4a2e7c7166c7eebf8c8f12 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 15:52:40 +0000 Subject: [PATCH 1802/2451] [CI] Updating repo.json for testing_1.2.0.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4f567253..4231cf65 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.0", + "TestingAssemblyVersion": "1.2.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 3b980c1a49e8292d5a602f8852c373345fe767e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:09:37 +0200 Subject: [PATCH 1803/2451] Fix two import bugs. --- Penumbra/Services/MigrationManager.cs | 1 + Penumbra/UI/Classes/MigrationSectionDrawer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index e377b65e..5b353912 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -243,6 +243,7 @@ public class MigrationManager(Configuration config) : IService using var s = new MemoryStream(); using var e = reader.OpenEntryStream(); e.CopyTo(s); + s.Position = 0; using var b = new BinaryReader(s); var version = b.ReadUInt32(); if (version == MdlFile.V5) diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index ec76ddae..d588eaa0 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -39,6 +39,7 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); + value = config.MigrateImportedMaterialsToLegacy; if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) { config.MigrateImportedMaterialsToLegacy = value; From 806e001bad44e7bb6748c6ff20856ff9dbb41633 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 16:11:41 +0000 Subject: [PATCH 1804/2451] [CI] Updating repo.json for testing_1.2.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4231cf65..a5ec39b5 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.1", + "TestingAssemblyVersion": "1.2.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From baa439d2463d3de0276005d70d9bc7ca954b9a26 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:31:24 +0200 Subject: [PATCH 1805/2451] Fix enable/disable draw offsets. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d8c784e4..49c5ba01 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d8c784e443112d17d93ba7e6ab5d54f00f7f1477 +Subproject commit 49c5ba0115814f809aaf4f31e6dcf321efb52237 From 37ffe528699191c4a06d7fa2a37412b927d10f04 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:36:45 +0200 Subject: [PATCH 1806/2451] Fix issue with file substitutions. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 1c2cebcc..6347447a 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Collections; @@ -13,6 +14,7 @@ namespace Penumbra.Api; public class DalamudSubstitutionProvider : IDisposable, IApiService { private readonly ITextureSubstitutionProvider _substitution; + private readonly IUiBuilder _uiBuilder; private readonly ActiveCollectionData _activeCollectionData; private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -21,9 +23,10 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService => _config.UseDalamudUiTextureRedirection; public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData, - Configuration config, CommunicatorService communicator) + Configuration config, CommunicatorService communicator, IUiBuilder ui) { _substitution = substitution; + _uiBuilder = ui; _activeCollectionData = activeCollectionData; _config = config; _communicator = communicator; @@ -41,6 +44,9 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService public void ResetSubstitutions(IEnumerable paths) { + if (!_uiBuilder.UiPrepared) + return; + var transformed = paths .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) .Select(p => p.ToString()); @@ -91,10 +97,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService case ResolvedFileChanged.Type.Added: case ResolvedFileChanged.Type.Removed: case ResolvedFileChanged.Type.Replaced: - ResetSubstitutions(new[] - { - key, - }); + ResetSubstitutions([key]); break; case ResolvedFileChanged.Type.FullRecomputeStart: case ResolvedFileChanged.Type.FullRecomputeFinished: From e2112202a05e50fb5ec1ea7c5f21b0b6f43a08d2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 16:38:39 +0000 Subject: [PATCH 1807/2451] [CI] Updating repo.json for testing_1.2.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a5ec39b5..758a10d0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.2", + "TestingAssemblyVersion": "1.2.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1be75444cd8344b8a5774193bdf097c9b9f47d17 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Jul 2024 12:51:47 +0200 Subject: [PATCH 1808/2451] Update BNPC Names --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 49c5ba01..a64a30bf 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 49c5ba0115814f809aaf4f31e6dcf321efb52237 +Subproject commit a64a30bf29cf297285ecde0579830b4d7fbae2d9 From 380dd0cffb8bc675c04c282d8bf92f72c5a1c3bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Jul 2024 01:40:45 +0200 Subject: [PATCH 1809/2451] Fix texture writing. --- Penumbra/Import/Textures/TexFileParser.cs | 4 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 7 +- Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs | 117 ++++++++++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 2 +- 4 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 09025b61..ae4a39c0 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,8 +79,8 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write(header.MipCount); - w.Write(header.MipUnknownFlag); // TODO Lumina Update + w.Write((byte)(header.MipCount | (header.MipUnknownFlag ? 0x80 : 0))); + w.Write(header.ArraySize); unsafe { w.Write(header.LodOffset[0]); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index be92b94e..4966dd64 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; -using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -96,6 +95,7 @@ public class DebugTab : Window, ITab, IUiService private readonly IClientState _clientState; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; + private readonly TexHeaderDrawer _texHeaderDrawer; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -105,7 +105,7 @@ public class DebugTab : Window, ITab, IUiService DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, - Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel) + Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -141,6 +141,7 @@ public class DebugTab : Window, ITab, IUiService _diagnostics = diagnostics; _ipcTester = ipcTester; _crashHandlerPanel = crashHandlerPanel; + _texHeaderDrawer = texHeaderDrawer; _objects = objects; _clientState = clientState; } @@ -176,6 +177,8 @@ public class DebugTab : Window, ITab, IUiService ImGui.NewLine(); DrawCollectionCaches(); ImGui.NewLine(); + _texHeaderDrawer.Draw(); + ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); DrawShaderReplacementFixer(); diff --git a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs new file mode 100644 index 00000000..08d51184 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs @@ -0,0 +1,117 @@ +using Dalamud.Interface.DragDrop; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using Lumina.Data.Files; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs.Debug; + +public class TexHeaderDrawer(IDragDropManager dragDrop) : IUiService +{ + private string? _path; + private TexFile.TexHeader _header; + private byte[]? _tex; + private Exception? _exception; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Tex Header"u8); + if (!header) + return; + + DrawDragDrop(); + DrawData(); + } + + private void DrawDragDrop() + { + dragDrop.CreateImGuiSource("TexFileDragDrop", m => m.Files.Count == 1 && m.Extensions.Contains(".tex"), m => + { + ImUtf8.Text($"Dragging {m.Files[0]}..."); + return true; + }); + + ImUtf8.Button("Drag .tex here..."); + if (dragDrop.CreateImGuiTarget("TexFileDragDrop", out var files, out _)) + ReadTex(files[0]); + } + + private void DrawData() + { + if (_path == null) + return; + + ImUtf8.TextFramed(_path, 0, borderColor: 0xFFFFFFFF); + + + if (_exception != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImUtf8.TextWrapped($"Failure to load file:\n{_exception}"); + } + else if (_tex != null) + { + using var table = ImRaii.Table("table", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + TableLine("Format"u8, _header.Format); + TableLine("Width"u8, _header.Width); + TableLine("Height"u8, _header.Height); + TableLine("Depth"u8, _header.Depth); + TableLine("Mip Levels"u8, _header.MipCount); + TableLine("Array Size"u8, _header.ArraySize); + TableLine("Type"u8, _header.Type); + TableLine("Mip Flag"u8, _header.MipUnknownFlag); + TableLine("Byte Size"u8, _tex.Length); + unsafe + { + TableLine("LoD Offset 0"u8, _header.LodOffset[0]); + TableLine("LoD Offset 1"u8, _header.LodOffset[1]); + TableLine("LoD Offset 2"u8, _header.LodOffset[2]); + TableLine("LoD Offset 0"u8, _header.OffsetToSurface[0]); + TableLine("LoD Offset 1"u8, _header.OffsetToSurface[1]); + TableLine("LoD Offset 2"u8, _header.OffsetToSurface[2]); + TableLine("LoD Offset 3"u8, _header.OffsetToSurface[3]); + TableLine("LoD Offset 4"u8, _header.OffsetToSurface[4]); + TableLine("LoD Offset 5"u8, _header.OffsetToSurface[5]); + TableLine("LoD Offset 6"u8, _header.OffsetToSurface[6]); + TableLine("LoD Offset 7"u8, _header.OffsetToSurface[7]); + TableLine("LoD Offset 8"u8, _header.OffsetToSurface[8]); + TableLine("LoD Offset 9"u8, _header.OffsetToSurface[9]); + TableLine("LoD Offset 10"u8, _header.OffsetToSurface[10]); + TableLine("LoD Offset 11"u8, _header.OffsetToSurface[11]); + TableLine("LoD Offset 12"u8, _header.OffsetToSurface[12]); + } + } + } + + private static void TableLine(ReadOnlySpan text, T value) + { + ImGui.TableNextColumn(); + ImUtf8.Text(text); + ImGui.TableNextColumn(); + ImUtf8.Text($"{value}"); + } + + private unsafe void ReadTex(string path) + { + try + { + _path = path; + _tex = File.ReadAllBytes(_path); + if (_tex.Length < sizeof(TexFile.TexHeader)) + throw new Exception($"Size {_tex.Length} does not include a header."); + + _header = MemoryMarshal.Read(_tex); + _exception = null; + } + catch (Exception ex) + { + _tex = null; + _exception = ex; + } + } +} diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 1b9af75c..e0cab43f 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -26,7 +26,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH SetupEffectiveSizes(); collectionHeader.Draw(true); DrawFilters(); - using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); + using var child = ImRaii.Child("##EffectiveChangesTab", ImGui.GetContentRegionAvail(), false); if (!child) return; From 34cbf37c32a24c9af7f1807653220d9ed0c47974 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 10 Jul 2024 23:43:04 +0000 Subject: [PATCH 1810/2451] [CI] Updating repo.json for testing_1.2.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 758a10d0..7e90f7a9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.3", + "TestingAssemblyVersion": "1.2.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 24597d7dc0cfa14bf25928f47b8e1f50665cbeee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 16:20:17 +0200 Subject: [PATCH 1811/2451] Fix mod normalization skipping the default submod. --- Penumbra/Mods/Editor/ModNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index c0876f5d..30bf3d3f 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -277,6 +277,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer private void ApplyRedirections() { + _modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); From 40be298d67d7a823ea67f62848028843d1b72244 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:37:19 +0200 Subject: [PATCH 1812/2451] Add automatic reduplication for ui files in pmps, test. --- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImporter.Archives.cs | 4 +- Penumbra/Import/TexToolsImporter.Gui.cs | 66 +++++------ Penumbra/Mods/Editor/ModNormalizer.cs | 109 ++++++++++++++++++- Penumbra/Penumbra.cs | 1 + Penumbra/UI/Tabs/SettingsTab.cs | 3 + 6 files changed, 145 insertions(+), 39 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f16569b5..63325433 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -96,6 +96,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true; + public bool AutoReduplicateUiOnImport { get; set; } = true; public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 63c170cb..dea343c6 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,9 +1,7 @@ -using System.IO; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; -using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; @@ -11,7 +9,6 @@ using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; using SharpCompress.Readers; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; @@ -114,6 +111,7 @@ public partial class TexToolsImporter _currentModDirectory.Refresh(); _modManager.Creator.SplitMultiGroups(_currentModDirectory); + _editor.ModNormalizer.NormalizeUi(_currentModDirectory); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 78665f30..309f107a 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -20,59 +20,61 @@ public partial class TexToolsImporter private string _currentOptionName = string.Empty; private string _currentFileName = string.Empty; - public void DrawProgressInfo(Vector2 size) + public bool DrawProgressInfo(Vector2 size) { if (_modPackCount == 0) { ImGuiUtil.Center("Nothing to extract."); + return false; } - else if (_modPackCount == _currentModPackIdx) - { - DrawEndState(); - } + + if (_modPackCount == _currentModPackIdx) + return DrawEndState(); + + ImGui.NewLine(); + var percentage = (float)_currentModPackIdx / _modPackCount; + ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); + ImGui.NewLine(); + if (State == ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); else + ImGui.TextUnformatted($"Extracting {_currentModName}..."); + + if (_currentNumOptions > 1) { ImGui.NewLine(); - var percentage = (float)_currentModPackIdx / _modPackCount; - ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); - if (State == ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); - else - ImGui.TextUnformatted($"Extracting {_currentModName}..."); - - if (_currentNumOptions > 1) - { - ImGui.NewLine(); - ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; - ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); - ImGui.NewLine(); - if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted( - $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); - } - - ImGui.NewLine(); - ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; - ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; + ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + ImGui.TextUnformatted( + $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } + + ImGui.NewLine(); + ImGui.NewLine(); + percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; + ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + ImGui.NewLine(); + if (State != ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + return false; } - private void DrawEndState() + private bool DrawEndState() { var success = ExtractedMods.Count(t => t.Error == null); + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + return true; + ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.NewLine(); using var table = ImRaii.Table("##files", 2); if (!table) - return; + return false; foreach (var (file, dir, ex) in ExtractedMods) { @@ -91,6 +93,8 @@ public partial class TexToolsImporter ImGuiUtil.HoverTooltip(ex.ToString()); } } + + return false; } public bool DrawCancelButton(Vector2 size) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 30bf3d3f..43cfc1ee 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -3,13 +3,15 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Services; using OtterGui.Tasks; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModNormalizer(ModManager _modManager, Configuration _config) : IService +public class ModNormalizer(ModManager modManager, Configuration config, SaveService saveService) : IService { private readonly List>> _redirections = []; @@ -39,6 +41,103 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer Worker = TrackedTask.Run(NormalizeSync); } + public void NormalizeUi(DirectoryInfo modDirectory) + { + if (!config.AutoReduplicateUiOnImport) + return; + + if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod) + return; + + Dictionary> paths = []; + Dictionary containers = []; + foreach (var container in mod.AllDataContainers) + { + foreach (var (gamePath, path) in container.Files) + { + if (!gamePath.Path.StartsWith("ui/"u8)) + continue; + + if (!paths.TryGetValue(path, out var list)) + { + list = []; + paths.Add(path, list); + } + + list.Add((container, gamePath)); + containers.TryAdd(container, string.Empty); + } + } + + foreach (var container in containers.Keys.ToList()) + { + if (container.Group == null) + containers[container] = mod.ModPath.FullName; + else + { + var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport); + var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport); + containers[container] = optionDir.FullName; + } + } + + var anyChanges = 0; + var modRootLength = mod.ModPath.FullName.Length + 1; + foreach (var (file, gamePaths) in paths) + { + if (gamePaths.Count < 2) + continue; + + var keptPath = false; + foreach (var (container, gamePath) in gamePaths) + { + var directory = containers[container]; + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFilePath = Path.Combine(directory, relPath); + if (newFilePath == file.FullName) + { + Penumbra.Log.Verbose($"[UIReduplication] Kept {file.FullName[modRootLength..]} because new path was identical."); + keptPath = true; + continue; + } + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)!); + File.Copy(file.FullName, newFilePath, false); + Penumbra.Log.Verbose($"[UIReduplication] Copied {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}."); + container.Files[gamePath] = new FullPath(newFilePath); + ++anyChanges; + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"[UIReduplication] Failed to copy {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}:\n{ex}"); + } + } + + if (keptPath) + continue; + + try + { + File.Delete(file.FullName); + Penumbra.Log.Verbose($"[UIReduplication] Deleted {file.FullName[modRootLength..]} because no new path matched."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[UIReduplication] Failed to delete {file.FullName[modRootLength..]}:\n{ex}"); + } + } + + if (anyChanges == 0) + return; + + saveService.Save(SaveType.ImmediateSync, new ModSaveGroup(mod.Default, config.ReplaceNonAsciiOnImport)); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); + Penumbra.Log.Information($"[UIReduplication] Saved groups after {anyChanges} changes."); + } + private void NormalizeSync() { try @@ -168,7 +267,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer // Normalize all other options. foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { - var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); + var groupDir = ModCreator.CreateModFolder(directory, group.Name, config.ReplaceNonAsciiOnImport, true); _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) _redirections[groupIdx + 1].Add([]); @@ -188,7 +287,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { var name = option.GetName(); - var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); + var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true); newDict.Clear(); newDict.EnsureCapacity(option.Files.Count); @@ -277,10 +376,10 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer private void ApplyRedirections() { - _modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); + modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) - _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); + modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); ++Step; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9f2db2e6..5f8d6805 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); + sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 8a4d6874..ab47ce7c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -757,6 +757,9 @@ public class SettingsTab : ITab, IUiService Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); + Checkbox("Auto Reduplicate UI Files on PMP Import", + "Automatically reduplicate and normalize UI-specific files on import from PMP files. This is STRONGLY recommended because deduplicated UI files crash the game.", + _config.AutoReduplicateUiOnImport, v => _config.AutoReduplicateUiOnImport = v); DrawCompressionBox(); Checkbox("Keep Default Metadata Changes on Import", "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " From 22af545e8db41e6ec712eb3294dfd54dec6b42e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:37:55 +0200 Subject: [PATCH 1813/2451] Add image strings to groups and mods to keep them in the json on saves. --- Penumbra/Mods/Groups/IModGroup.cs | 1 + Penumbra/Mods/Groups/ImcModGroup.cs | 2 ++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 ++ Penumbra/Mods/Groups/MultiModGroup.cs | 2 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 2 ++ Penumbra/Mods/Manager/ModDataEditor.cs | 8 ++++++++ Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/ModMeta.cs | 1 + 8 files changed, 19 insertions(+) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 00f47e25..9327ced9 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -27,6 +27,7 @@ public interface IModGroup public Mod Mod { get; } public string Name { get; set; } public string Description { get; set; } + public string Image { get; set; } public GroupType Type { get; } public GroupDrawBehaviour Behaviour { get; } public ModPriority Priority { get; set; } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 46204d6c..03896134 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -20,6 +20,7 @@ public class ImcModGroup(Mod mod) : IModGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; public GroupType Type => GroupType.Imc; @@ -170,6 +171,7 @@ public class ImcModGroup(Mod mod) : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index c82c67c7..c465822b 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -88,6 +88,8 @@ public readonly struct ModSaveGroup : ISavable jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Image)); + jWriter.WriteValue(group.Image); jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WriteValue(group.Priority.Value); jWriter.WritePropertyName(nameof(group.Type)); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 95f49230..ee27d534 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -26,6 +26,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -69,6 +70,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index a559d609..cc606f42 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -24,6 +24,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } @@ -65,6 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 4ab9deb1..91ae4a4c 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -22,6 +22,7 @@ public enum ModDataChangeType : ushort Favorite = 0x0200, LocalTags = 0x0400, Note = 0x0800, + Image = 0x1000, } public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService @@ -113,6 +114,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; @@ -138,6 +140,12 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = newDescription; } + if (mod.Image != newImage) + { + changes |= ModDataChangeType.Image; + mod.Image = newImage; + } + if (mod.Version != newVersion) { changes |= ModDataChangeType.Version; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index a7f87dcd..fcea7133 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -51,6 +51,7 @@ public sealed class Mod : IMod public string Description { get; internal set; } = string.Empty; public string Version { get; internal set; } = string.Empty; public string Website { get; internal set; } = string.Empty; + public string Image { get; internal set; } = string.Empty; public IReadOnlyList ModTags { get; internal set; } = []; diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 870d6d4f..39dd20e4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -19,6 +19,7 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.Name), JToken.FromObject(mod.Name) }, { nameof(Mod.Author), JToken.FromObject(mod.Author) }, { nameof(Mod.Description), JToken.FromObject(mod.Description) }, + { nameof(Mod.Image), JToken.FromObject(mod.Image) }, { nameof(Mod.Version), JToken.FromObject(mod.Version) }, { nameof(Mod.Website), JToken.FromObject(mod.Website) }, { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, From 94a05afbe0ce00f2c1195ca8ac206bc6f29c4584 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:54:47 +0200 Subject: [PATCH 1814/2451] Make the import popup closeable by clicking outside if it is finished. --- Penumbra/Import/TexToolsImporter.Gui.cs | 23 ++++++++++------------- Penumbra/UI/ImportPopup.cs | 11 ++++++++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 309f107a..a069204c 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -25,20 +25,22 @@ public partial class TexToolsImporter if (_modPackCount == 0) { ImGuiUtil.Center("Nothing to extract."); - return false; + return true; } if (_modPackCount == _currentModPackIdx) - return DrawEndState(); + { + DrawEndState(); + return true; + } ImGui.NewLine(); var percentage = (float)_currentModPackIdx / _modPackCount; ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); - if (State == ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); - else - ImGui.TextUnformatted($"Extracting {_currentModName}..."); + ImGui.TextUnformatted(State == ImporterState.DeduplicatingFiles + ? $"Deduplicating {_currentModName}..." + : $"Extracting {_currentModName}..."); if (_currentNumOptions > 1) { @@ -63,18 +65,15 @@ public partial class TexToolsImporter } - private bool DrawEndState() + private void DrawEndState() { var success = ExtractedMods.Count(t => t.Error == null); - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - return true; - ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.NewLine(); using var table = ImRaii.Table("##files", 2); if (!table) - return false; + return; foreach (var (file, dir, ex) in ExtractedMods) { @@ -93,8 +92,6 @@ public partial class TexToolsImporter ImGuiUtil.HoverTooltip(ex.ToString()); } } - - return false; } public bool DrawCancelButton(Vector2 size) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index fb2028b5..28767edc 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,4 +1,6 @@ +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; @@ -68,13 +70,16 @@ public sealed class ImportPopup : Window, IUiService ImGui.SetNextWindowSize(size); using var popup = ImRaii.Popup(importPopup, ImGuiWindowFlags.Modal); PopupWasDrawn = true; + var terminate = false; using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { - if (child) - import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); + if (child.Success && import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()))) + if (!ImGui.IsMouseHoveringRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize()) + && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + terminate = true; } - var terminate = import.State == ImporterState.Done + terminate |= import.State == ImporterState.Done ? ImGui.Button("Close", -Vector2.UnitX) : import.DrawCancelButton(-Vector2.UnitX); if (terminate) From d815266ed7b92ce2c474cd8e4da9c36a2d7fdcf4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 12 Jul 2024 15:56:58 +0000 Subject: [PATCH 1815/2451] [CI] Updating repo.json for testing_1.2.0.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7e90f7a9..51e458a8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.4", + "TestingAssemblyVersion": "1.2.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d6f61f06cbf16e23900b4f317e5ecc8f63c7fc1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 22:15:00 +0200 Subject: [PATCH 1816/2451] Add TestingDalamudApiLevel --- repo.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 51e458a8..59daa590 100644 --- a/repo.json +++ b/repo.json @@ -9,9 +9,10 @@ "TestingAssemblyVersion": "1.2.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 9, + "TestingDalamudApiLevel": 10, "IsHide": "False", - "IsTestingExclusive": "True", + "IsTestingExclusive": "False", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, From e46fcc4af1bbf083ab5ce07edea67ad5a554589d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jul 2024 20:38:54 +0200 Subject: [PATCH 1817/2451] Gracefully deal with invalid offhand IMCs. --- Penumbra.GameData | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 3 ++- Penumbra/Services/ValidityChecker.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a64a30bf..2c067b4f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a64a30bf29cf297285ecde0579830b4d7fbae2d9 +Subproject commit 2c067b4f3c1d84888c2b961a93fe2de01fffe5f1 diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 44c60942..d4887fe2 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -100,7 +100,8 @@ public readonly record struct ImcIdentifier( return false; if (!Enum.IsDefined(ObjectType)) return false; - + if (ItemData.AdaptOffhandImc(PrimaryId, out _)) + return false; break; } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index cefee139..5feeab02 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -45,7 +45,7 @@ public class ValidityChecker : IService public void LogExceptions() { if (ImcExceptions.Count > 0) - Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", + Penumbra.Messager.NotificationMessage($"{ImcExceptions.Count} IMC Exceptions thrown during Penumbra load. Please repair your game files.", NotificationType.Warning); } From 07c3be641ddc7950a3a818a44b5d155e828a3b7d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 14 Jul 2024 18:41:39 +0000 Subject: [PATCH 1818/2451] [CI] Updating repo.json for testing_1.2.0.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 59daa590..4e1b17ba 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.5", + "TestingAssemblyVersion": "1.2.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 9c781f8563fb0bc85247fb83aab79980943cb2d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:04:36 +0200 Subject: [PATCH 1819/2451] Disable material migration for now --- Penumbra.GameData | 2 +- Penumbra/Services/MigrationManager.cs | 22 ++++++++++----- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 28 ++++++++++--------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2c067b4f..c2a4c4ee 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2c067b4f3c1d84888c2b961a93fe2de01fffe5f1 +Subproject commit c2a4c4ee7470c5afbd3dd7731697ab49c055d1e3 diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 5b353912..7115fe4d 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -233,6 +233,8 @@ public class MigrationManager(Configuration config) : IService /// Writes or migrates a .mdl file during extraction from a regular archive. public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { + // TODO reactivate when this works. + return; if (!config.MigrateImportedModelsToV6) { reader.WriteEntryToDirectory(directory, options); @@ -265,6 +267,8 @@ public class MigrationManager(Configuration config) : IService public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { + // TODO reactivate when this works. + return; if (!config.MigrateImportedMaterialsToLegacy) { reader.WriteEntryToDirectory(directory, options); @@ -276,16 +280,20 @@ public class MigrationManager(Configuration config) : IService using var e = reader.OpenEntryStream(); e.CopyTo(s); var file = new MtrlFile(s.GetBuffer()); - if (!file.IsDawnTrail) - { - file.MigrateToDawntrail(); - Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); - } Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); - s.Seek(0, SeekOrigin.Begin); - s.WriteTo(f); + if (file.IsDawnTrail) + { + file.MigrateToDawntrail(); + Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); + f.Write(file.Write()); + } + else + { + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } } /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index d588eaa0..a4a2010f 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -22,10 +22,11 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura DrawMdlMigration(); DrawMdlRestore(); DrawMdlCleanup(); - ImGui.Separator(); - DrawMtrlMigration(); - DrawMtrlRestore(); - DrawMtrlCleanup(); + // TODO enable when this works + //ImGui.Separator(); + //DrawMtrlMigration(); + //DrawMtrlRestore(); + //DrawMtrlCleanup(); } private void DrawSettings() @@ -39,15 +40,16 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); - value = config.MigrateImportedMaterialsToLegacy; - if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) - { - config.MigrateImportedMaterialsToLegacy = value; - config.Save(); - } - - ImUtf8.HoverTooltip( - "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); + // TODO enable when this works + //value = config.MigrateImportedMaterialsToLegacy; + //if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) + //{ + // config.MigrateImportedMaterialsToLegacy = value; + // config.Save(); + //} + // + //ImUtf8.HoverTooltip( + // "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); } From 12dfaaef99192edfd00302d2c249f31f0c7a5509 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:05:10 +0200 Subject: [PATCH 1820/2451] Fix broken mods being deleted instead of removed. Fix tags crashing when null instead of empty. --- Penumbra/Mods/Manager/ModDataEditor.cs | 4 ++-- Penumbra/Mods/Manager/ModManager.cs | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 91ae4a4c..7a0467d0 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -59,7 +59,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = json[nameof(Mod.LocalTags)]?.Values().OfType() ?? localTags; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; save = false; } catch (Exception e) @@ -119,7 +119,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; var importDate = json[nameof(Mod.ImportDate)]?.Value(); - var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); + var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); ModDataChangeType changes = 0; if (mod.Name != newName) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 4b19ea4c..f170a31b 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -115,12 +115,21 @@ public sealed class ModManager : ModStorage, IDisposable, IService Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); } + RemoveMod(mod); + } + + /// + /// Remove a loaded mod. The event is invoked before the mod is removed from the list. + /// Does not delete the mod from the filesystem. + /// Updates indices of later mods. + /// + public void RemoveMod(Mod mod) + { _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); foreach (var remainingMod in Mods.Skip(mod.Index + 1)) --remainingMod.Index; Mods.RemoveAt(mod.Index); - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + Penumbra.Log.Debug($"Removed loaded mod {mod.Name} from list."); } /// @@ -135,10 +144,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService if (!Creator.ReloadMod(mod, true, out var metaChange)) { Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(mod); + ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); + RemoveMod(mod); return; } From d952d83adf1b61ccb664420b29e7eb81593d3f7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:06:02 +0200 Subject: [PATCH 1821/2451] Fix redrawing while fishing while sitting. --- Penumbra/Interop/Services/RedrawService.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 163b2c0e..f288a35e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -301,9 +301,14 @@ public sealed unsafe partial class RedrawService : IDisposable (CharacterModes)6 => // fishing GetCurrentAnimationId(obj) switch { - 278 => true, // line out. - 283 => true, // reeling in - _ => false, + 278 => true, // line out. + 283 => true, // reeling in + 284 => true, // reeling in + 287 => true, // reeling in 2 + 3149 => true, // line out sitting, + 3155 => true, // reeling in sitting, + 3159 => true, // reeling in sitting 2, + _ => false, }, _ => false, }; @@ -419,7 +424,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = (OutdoorTerritory*) housingManager->CurrentTerritory; + var currentTerritory = (OutdoorTerritory*)housingManager->CurrentTerritory; if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) return; From c98bee67a5bb28adf2f59efbb23968f862d7653a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:25:28 +0200 Subject: [PATCH 1822/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c2a4c4ee..67109fa9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c2a4c4ee7470c5afbd3dd7731697ab49c055d1e3 +Subproject commit 67109fa9e89d5ff5c9f93705208db92e836e9ef4 From 78af40d5078dc322174935c76606256fcc08fdc1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 20:27:30 +0000 Subject: [PATCH 1823/2451] [CI] Updating repo.json for testing_1.2.0.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4e1b17ba..9f11c118 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.6", + "TestingAssemblyVersion": "1.2.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 519b3d4891c67650d3a333fe9d52f77240f989f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:36:55 +0200 Subject: [PATCH 1824/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 67109fa9..c9e0d890 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 67109fa9e89d5ff5c9f93705208db92e836e9ef4 +Subproject commit c9e0d8905137fef6ed7c247ee1d2824d3f89f3b2 From 89cbb3f60dde8e72f3b4b8e2887eae5701268d39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 23:02:17 +0200 Subject: [PATCH 1825/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c9e0d890..45f2c901 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c9e0d8905137fef6ed7c247ee1d2824d3f89f3b2 +Subproject commit 45f2c901b3a0131eaee18b3520184baeb0d1049d From eb784dddf04e4380db02df6f0a58b95bd2635db8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 00:40:49 +0200 Subject: [PATCH 1826/2451] Fix missing file display. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 83a8958b..e915a879 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -132,7 +132,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService sb.Append($" | {unused} Unused Files"); if (_editor.Files.Missing.Count > 0) - sb.Append($" | {_editor.Files.Available.Count} Missing Files"); + sb.Append($" | {_editor.Files.Missing.Count} Missing Files"); if (redirections > 0) sb.Append($" | {redirections} Redirections"); From 67a35b9abbb22576dc8d5c5d16e7201d3fbb4fa9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 00:49:40 +0200 Subject: [PATCH 1827/2451] stupid --- Penumbra/Services/MigrationManager.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 7115fe4d..84318da6 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -233,8 +233,6 @@ public class MigrationManager(Configuration config) : IService /// Writes or migrates a .mdl file during extraction from a regular archive. public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { - // TODO reactivate when this works. - return; if (!config.MigrateImportedModelsToV6) { reader.WriteEntryToDirectory(directory, options); @@ -267,9 +265,7 @@ public class MigrationManager(Configuration config) : IService public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { - // TODO reactivate when this works. - return; - if (!config.MigrateImportedMaterialsToLegacy) + if (!config.MigrateImportedMaterialsToLegacy || true) // TODO change when this is working { reader.WriteEntryToDirectory(directory, options); return; @@ -327,7 +323,7 @@ public class MigrationManager(Configuration config) : IService /// Update the data of a .mtrl file during TTMP extraction. Returns either the existing array or a new one. public byte[] MigrateTtmpMaterial(string path, byte[] data) { - if (!config.MigrateImportedMaterialsToLegacy) + if (!config.MigrateImportedMaterialsToLegacy || true) // TODO fix when this is working return data; try From ad877e68e610fcdec3f7fee993154a74b8929d3b Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 22:51:33 +0000 Subject: [PATCH 1828/2451] [CI] Updating repo.json for testing_1.2.0.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9f11c118..850acf1b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.7", + "TestingAssemblyVersion": "1.2.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4824a96ab0f9854ab75248c38a3f7549b1aad97a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 01:35:56 +0200 Subject: [PATCH 1829/2451] Enable Mtrl Restore and Cleanup again. --- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index a4a2010f..a3dcd23a 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -23,10 +23,10 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura DrawMdlRestore(); DrawMdlCleanup(); // TODO enable when this works - //ImGui.Separator(); + ImGui.Separator(); //DrawMtrlMigration(); - //DrawMtrlRestore(); - //DrawMtrlCleanup(); + DrawMtrlRestore(); + DrawMtrlCleanup(); } private void DrawSettings() From c9379b6d60d2a1f7f133c9fa7da3d7a6952a3651 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 23:38:11 +0000 Subject: [PATCH 1830/2451] [CI] Updating repo.json for testing_1.2.0.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 850acf1b..b4becf0f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.8", + "TestingAssemblyVersion": "1.2.0.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1922353ba30e05a2b862f88b74a5e311fe5eb6d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 02:01:52 +0200 Subject: [PATCH 1831/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 45f2c901..c25ea7b1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 45f2c901b3a0131eaee18b3520184baeb0d1049d +Subproject commit c25ea7b19a6db37dd36e12b9a7a71f72a192ab57 From e7c786b239a53b3e145cc1e1101324170eb883b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:02:48 +0200 Subject: [PATCH 1832/2451] Add and rework hooks around EST entries. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 47 +++++++++++-------- .../Hooks/Resources/ResolvePathHooksBase.cs | 38 ++++++++++++++- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c25ea7b1..94df458d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c25ea7b19a6db37dd36e12b9a7a71f72a192ab57 +Subproject commit 94df458dfb2a704a611fa77d955808284aeb23ac diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 5b272019..ce002664 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -3,51 +3,58 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Interop.Hooks.Meta; -public class EstHook : FastHook, IDisposable +public unsafe class EstHook : FastHook, IDisposable { - public delegate EstEntry Delegate(uint id, int estType, uint genderRace); + public delegate EstEntry Delegate(ResourceHandle* estResource, uint id, uint genderRace); - private readonly MetaState _metaState; + private readonly CharacterUtility _characterUtility; + private readonly MetaState _metaState; - public EstHook(HookManager hooks, MetaState metaState) + public EstHook(HookManager hooks, MetaState metaState, CharacterUtility characterUtility) { - _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); + _metaState = metaState; + _characterUtility = characterUtility; + Task = hooks.CreateHook("FindEstEntry", Sigs.FindEstEntry, Detour, + metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } - private EstEntry Detour(uint genderRace, int estType, uint id) + private EstEntry Detour(ResourceHandle* estResource, uint genderRace, uint id) { EstEntry ret; if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) + && cache.Est.TryGetValue(Convert(estResource, genderRace, id), out var entry)) ret = entry.Entry; else - ret = Task.Result.Original(genderRace, estType, id); + ret = Task.Result.Original(estResource, genderRace, id); - Penumbra.Log.Excessive($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + Penumbra.Log.Information($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); return ret; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static EstIdentifier Convert(uint genderRace, int estType, uint id) + private EstIdentifier Convert(ResourceHandle* estResource, uint genderRace, uint id) { var i = new PrimaryId((ushort)id); var gr = (GenderRace)genderRace; - var type = estType switch - { - 1 => EstType.Face, - 2 => EstType.Hair, - 3 => EstType.Head, - 4 => EstType.Body, - _ => (EstType)0, - }; - return new EstIdentifier(i, type, gr); + + if (estResource == _characterUtility.Address->BodyEstResource) + return new EstIdentifier(i, EstType.Body, gr); + if (estResource == _characterUtility.Address->HairEstResource) + return new EstIdentifier(i, EstType.Hair, gr); + if (estResource == _characterUtility.Address->FaceEstResource) + return new EstIdentifier(i, EstType.Face, gr); + if (estResource == _characterUtility.Address->HeadEstResource) + return new EstIdentifier(i, EstType.Head, gr); + + return new EstIdentifier(i, 0, gr); } public void Dispose() diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8fa6d861..e1b6e46e 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -19,6 +19,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private delegate nint NamedResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint name); private delegate nint PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex); private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize); + private delegate nint SkeletonVFuncDelegate(nint drawObject, int estType, nint unk); private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName); @@ -37,6 +38,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveSkpPathHook; private readonly Hook _resolveTmbPathHook; private readonly Hook _resolveVfxPathHook; + private readonly Hook? _vFunc81Hook; + private readonly Hook? _vFunc83Hook; private readonly PathState _parent; @@ -49,6 +52,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); + _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); + + _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); @@ -58,6 +64,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); + + // @formatter:on if (HookSettings.ResourceHooks) Enable(); @@ -77,6 +85,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Enable(); _resolveTmbPathHook.Enable(); _resolveVfxPathHook.Enable(); + _vFunc81Hook?.Enable(); + _vFunc83Hook?.Enable(); } public void Disable() @@ -93,6 +103,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Disable(); _resolveTmbPathHook.Disable(); _resolveVfxPathHook.Disable(); + _vFunc81Hook?.Disable(); + _vFunc83Hook?.Disable(); } public void Dispose() @@ -109,6 +121,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Dispose(); _resolveTmbPathHook.Dispose(); _resolveVfxPathHook.Dispose(); + _vFunc81Hook?.Dispose(); + _vFunc83Hook?.Dispose(); } private nint ResolveDecal(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) @@ -224,14 +238,36 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(drawObject, pathBuffer); } + private nint VFunc81(nint drawObject, int estType, nint unk) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = _vFunc81Hook!.Original(drawObject, estType, unk); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + + private nint VFunc83(nint drawObject, int estType, nint unk) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = _vFunc83Hook!.Original(drawObject, estType, unk); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate + [return: NotNullIfNotNull(nameof(other))] + private static Hook? Create(string name, HookManager hooks, nint address, Type type, T? other, T human) where T : Delegate { var del = type switch { Type.Human => human, _ => other, }; + if (del == null) + return null; + return hooks.CreateHook(name, address, del).Result; } From 6d0562180acbae396cbcbb8c9ed21f0d1b1eec2c Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 17 Jul 2024 16:05:28 +0000 Subject: [PATCH 1833/2451] [CI] Updating repo.json for testing_1.2.0.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index b4becf0f..986c1707 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.9", + "TestingAssemblyVersion": "1.2.0.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 9bba1e2b31a26e890e34d72693e7d2521e43fea3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:05:57 +0200 Subject: [PATCH 1834/2451] Remove log spamming. --- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index ce002664..825b1244 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -35,7 +35,7 @@ public unsafe class EstHook : FastHook, IDisposable else ret = Task.Result.Original(estResource, genderRace, id); - Penumbra.Log.Information($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); + Penumbra.Log.Excessive($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); return ret; } From 5abbd8b1101090afba2729cfbbf4adeeb673e615 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:34:03 +0200 Subject: [PATCH 1835/2451] Hook UpdateRender despite per-frame calls. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 37 ------------------- .../{GetEqpIndirect2.cs => UpdateRender.cs} | 15 +++----- 3 files changed, 6 insertions(+), 48 deletions(-) delete mode 100644 Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs rename Penumbra/Interop/Hooks/Meta/{GetEqpIndirect2.cs => UpdateRender.cs} (55%) diff --git a/Penumbra.GameData b/Penumbra.GameData index 94df458d..c5ad1f3a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 94df458dfb2a704a611fa77d955808284aeb23ac +Subproject commit c5ad1f3ae9818baa446327bdcf49fac65088c703 diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs deleted file mode 100644 index 8bd49500..00000000 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - -public sealed unsafe class GetEqpIndirect : FastHook -{ - private readonly CollectionResolver _collectionResolver; - private readonly MetaState _metaState; - - public GetEqpIndirect(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) - { - _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, HookSettings.MetaParentHooks); - } - - public delegate void Delegate(DrawObject* drawObject); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(DrawObject* drawObject) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if ((*(byte*)((nint)drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)((nint)drawObject + Offsets.GetEqpIndirectSkip2) == 0) - return; - - Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - _metaState.EqpCollection.Push(collection); - Task.Result.Original(drawObject); - _metaState.EqpCollection.Pop(); - } -} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs similarity index 55% rename from Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs rename to Penumbra/Interop/Hooks/Meta/UpdateRender.cs index e90674a8..95cc0e15 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs @@ -1,20 +1,20 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.GameData; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public sealed unsafe class GetEqpIndirect2 : FastHook +/// The actual function is inlined, so we need to hook its only callsite: Human.UpdateRender instead. +public sealed unsafe class UpdateRender : FastHook { private readonly CollectionResolver _collectionResolver; private readonly MetaState _metaState; - public GetEqpIndirect2(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + public UpdateRender(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vTables) { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); @@ -22,12 +22,7 @@ public sealed unsafe class GetEqpIndirect2 : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Detour(DrawObject* drawObject) { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if (((*(uint*)((nint)drawObject + Offsets.GetEqpIndirect2Skip) >> 0x12) & 1) == 0) - return; - - Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); + Penumbra.Log.Excessive($"[Human.UpdateRender] Invoked on {(nint)drawObject:X}."); var collection = _collectionResolver.IdentifyCollection(drawObject, true); _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); From defba19b2d7714162d0eca036d212aef48aad667 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 17 Jul 2024 16:36:04 +0000 Subject: [PATCH 1836/2451] [CI] Updating repo.json for testing_1.2.0.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 986c1707..d715bca2 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.10", + "TestingAssemblyVersion": "1.2.0.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From f978b35b764fac7037f72f9ffa6f60dc2cd7bf13 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jul 2024 14:24:29 +0200 Subject: [PATCH 1837/2451] Make ResourceTrees work with UseNoMods. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/MetaCache.cs | 4 ---- .../Collections/ModCollection.Cache.Access.cs | 9 --------- Penumbra/Import/Models/ModelManager.cs | 6 ++++-- .../ResourceTree/ResolveContext.PathResolution.cs | 15 ++++++++------- Penumbra/Interop/ResourceTree/ResolveContext.cs | 10 ++++++++-- .../Interop/ResourceTree/ResourceTreeFactory.cs | 4 +++- 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c5ad1f3a..a1e637f8 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c5ad1f3ae9818baa446327bdcf49fac65088c703 +Subproject commit a1e637f835c1a42732825e8e0690aeef0024b101 diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 02056fad..1a6924a9 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -103,10 +103,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Try to obtain a manipulated IMC file. - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => Imc.GetFile(path.Path, out file); - internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 81751128..983509a4 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -43,15 +43,6 @@ public partial class ModCollection internal MetaCache? MetaCache => _cache?.Meta; - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) - { - if (_cache != null) - return _cache.Meta.GetImcFile(path, out file); - - file = null; - return false; - } - internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 01396cfb..0c19bc0a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -12,6 +12,8 @@ using Penumbra.GameData.Structs; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; using Penumbra.Import.Textures; +using Penumbra.Meta; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes; using SixLabors.ImageSharp; @@ -22,7 +24,7 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) +public sealed class ModelManager(IFramework framework, MetaFileManager metaFileManager, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable, IService { private readonly IFramework _framework = framework; @@ -97,7 +99,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Value ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? EstEntry.Zero; + ?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId); // If there's no entries, we can assume that there's no additional skeleton. if (targetId == EstEntry.Zero) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 72cb1681..07f305ac 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -3,6 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; @@ -51,10 +52,8 @@ internal partial record ResolveContext return GenderRace.MidlanderMale; var metaCache = Global.Collection.MetaCache; - if (metaCache == null) - return GenderRace.MidlanderMale; - - var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, primaryId); + var entry = metaCache?.GetEqdpEntry(characterRaceCode, accessory, primaryId) + ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, characterRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return characterRaceCode; @@ -62,7 +61,8 @@ internal partial record ResolveContext if (fallbackRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, primaryId); + entry = metaCache?.GetEqdpEntry(fallbackRaceCode, accessory, primaryId) + ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, fallbackRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return fallbackRaceCode; @@ -271,8 +271,9 @@ internal partial record ResolveContext private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) { - var metaCache = Global.Collection.MetaCache; - var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; + var metaCache = Global.Collection.MetaCache; + var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) + ?? EstFile.GetDefault(Global.MetaFileManager, type, raceCode, primary); return (raceCode, type.ToName(), skeletonSet.AsId); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a852a4cc..acb320d4 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.PathResolving; +using Penumbra.Meta; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -19,7 +20,12 @@ using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.M namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(ObjectIdentification Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData) +internal record GlobalResolveContext( + MetaFileManager MetaFileManager, + ObjectIdentification Identifier, + ModCollection Collection, + TreeBuildCache TreeBuildCache, + bool WithUiData) { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); @@ -111,7 +117,7 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fileName = (ReadOnlySpan) resourceHandle->FileName.AsSpan(); + var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); var additionalData = ByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 1f6d1f6f..46c7ce35 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -8,6 +8,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; +using Penumbra.Meta; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -15,6 +16,7 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTreeFactory( IDataManager gameData, ObjectManager objects, + MetaFileManager metaFileManager, CollectionResolver resolver, ObjectIdentification identifier, Configuration config, @@ -78,7 +80,7 @@ public class ResourceTreeFactory( var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); - var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, + var globalContext = new GlobalResolveContext(metaFileManager, identifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) { From f533ae66671c49682451982656f268d13192d7a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jul 2024 17:33:54 +0200 Subject: [PATCH 1838/2451] Some cleanup. --- .../ResolveContext.PathResolution.cs | 2 -- .../ResourceTree/ResourceTreeFactory.cs | 18 ++++++++++-------- .../Interop/Structs/CharacterUtilityData.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 07f305ac..678dd8a9 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -199,8 +199,6 @@ internal partial record ResolveContext ByteString? path; try { - Penumbra.Log.Information($"{(nint)CharacterBase:X} {ModelType} {SlotIndex} 0x{(ulong)mtrlFileName:X}"); - Penumbra.Log.Information($"{new ByteString(mtrlFileName)}"); path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 46c7ce35..65fac68f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -18,7 +18,7 @@ public class ResourceTreeFactory( ObjectManager objects, MetaFileManager metaFileManager, CollectionResolver resolver, - ObjectIdentification identifier, + ObjectIdentification objectIdentifier, Configuration config, ActorManager actors, PathState pathState) : IService @@ -80,7 +80,7 @@ public class ResourceTreeFactory( var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); - var globalContext = new GlobalResolveContext(metaFileManager, identifier, collectionResolveData.ModCollection, + var globalContext = new GlobalResolveContext(metaFileManager, objectIdentifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) { @@ -125,6 +125,14 @@ public class ResourceTreeFactory( private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) { + foreach (var node in tree.FlatNodes) + { + if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPath = FullPath.Empty; + } + + return; + static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) { if (!fullPath.IsRooted) @@ -139,12 +147,6 @@ public class ResourceTreeFactory( return fullPath.Exists; } - - foreach (var node in tree.FlatNodes) - { - if (!ShallKeepPath(node.FullPath, onlyWithinPath)) - node.FullPath = FullPath.Empty; - } } private static void Cleanup(ResourceTree tree) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index d33da477..197de0bb 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -15,7 +15,7 @@ public unsafe struct CharacterUtilityData .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 89; + public const int TotalNumResources = 114; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) From a4548bbf0426fb181e49396147677ebecfe87147 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:11:13 +0200 Subject: [PATCH 1839/2451] Apply unprioritized mod groups in reverse order. --- Penumbra/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index fcea7133..16f06de2 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -73,7 +73,7 @@ public sealed class Mod : IMod var dictRedirections = new Dictionary(TotalFileCount); var setManips = new MetaDictionary(); - foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) + foreach (var (group, groupIndex) in Groups.WithIndex().Reverse().OrderByDescending(g => g.Value.Priority)) { var config = settings.Settings[groupIndex]; group.AddData(config, dictRedirections, setManips); From 258f7e9732229f754f086fa79126e4246a7eb0b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:13:43 +0200 Subject: [PATCH 1840/2451] Reinstate the inlined ApricotSoundPlay hook one layer hup. --- Penumbra.GameData | 2 +- .../Animation/ApricotListenerSoundPlay.cs | 42 ++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a1e637f8..9f1816f1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a1e637f835c1a42732825e8e0690aeef0024b101 +Subproject commit 9f1816f1b75003d01c5576769831c10f3d8948a7 diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 2e05c1b6..361fcd4e 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -1,4 +1,3 @@ -using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -11,33 +10,48 @@ using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; /// Called for some sound effects caused by animations or VFX. -public sealed unsafe class ApricotListenerSoundPlay : FastHook +/// Actual function got inlined. +public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook { private readonly GameState _state; private readonly CollectionResolver _collectionResolver; private readonly CrashHandlerService _crashHandler; - // TODO because of inlining. - public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) + public ApricotListenerSoundPlayCaller(HookManager hooks, GameState state, CollectionResolver collectionResolver, + CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Apricot Listener Sound Play Caller", Sigs.ApricotListenerSoundPlayCaller, Detour, + true); //HookSettings.VfxIdentificationHooks); } - public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + public delegate nint Delegate(nint a1, nint a2, float a3); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private nint Detour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + private nint Detour(nint a1, nint unused, float timeOffset) { - Penumbra.Log.Excessive($"[Apricot Listener Sound Play] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}, {a5}, {a6}."); - if (a6 == nint.Zero) - return Task.Result.Original(a1, a2, a3, a4, a5, a6); + // Short-circuiting and sanity checks done by game. + var playTime = a1 == nint.Zero ? -1 : *(float*)(a1 + 0x250); + if (playTime < 0) + return Task.Result.Original(a1, unused, timeOffset); - // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. - var gameObject = (*(delegate* unmanaged**)a6)[1](a6); + var someIntermediate = *(nint*)(a1 + 0x1F8); + var flags = someIntermediate == nint.Zero ? (ushort)0 : *(ushort*)(someIntermediate + 0x49C); + if (((flags >> 13) & 1) == 0) + return Task.Result.Original(a1, unused, timeOffset); + + Penumbra.Log.Information( + $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); + // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) + var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); + if (apricotIInstanceListenner == nint.Zero) + return Task.Result.Original(a1, unused, timeOffset); + + // In some cases we can obtain the associated caster via vfunc 1. var newData = ResolveData.Invalid; + var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[1](apricotIInstanceListenner); if (gameObject != null) { newData = _collectionResolver.IdentifyCollection(gameObject, true); @@ -47,14 +61,14 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook Date: Sun, 21 Jul 2024 00:21:27 +0200 Subject: [PATCH 1841/2451] Fix field. --- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 15bd7cc9..25c0e448 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -75,7 +75,7 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < ColorTable.NumRows; ++i) { ret |= DrawColorTableRow(tab, i, disabled); ImGui.TableNextRow(); From 5b1c0cf0e3206fff550060f97375aeb8efae7aee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:21:41 +0200 Subject: [PATCH 1842/2451] Fix direction of furniture redrawing. --- Penumbra/Interop/Services/RedrawService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index f288a35e..2cdc1137 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -424,8 +424,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = (OutdoorTerritory*)housingManager->CurrentTerritory; - if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) + var currentTerritory = (IndoorTerritory*)housingManager->CurrentTerritory; + if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Indoor) return; From c3b7ddad2810e4687aec33b4da7f721de3b4625f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:48:48 +0200 Subject: [PATCH 1843/2451] Create newly added mods in import folder instead of moving them. --- OtterGui | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 21 +++++++++++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 31 -------------------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/OtterGui b/OtterGui index 89b3b951..dc17161b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 89b3b9513f9b4989045517a452ef971e24377203 +Subproject commit dc17161b1d9c47ffd6bcc17e91f4832cf7762993 diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index e32fec0c..693db944 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,3 +1,5 @@ +using Dalamud.Interface.ImGuiNotification; +using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Communication; @@ -10,13 +12,15 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer private readonly ModManager _modManager; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; + private readonly Configuration _config; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService) + public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService, Configuration config) { _modManager = modManager; _communicator = communicator; _saveService = saveService; + _config = config; Reload(); Changed += OnChange; _communicator.ModDiscoveryFinished.Subscribe(Reload, ModDiscoveryFinished.Priority.ModFileSystem); @@ -91,7 +95,20 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer switch (type) { case ModPathChangeType.Added: - CreateDuplicateLeaf(Root, mod.Name.Text, mod); + var parent = Root; + if (_config.DefaultImportFolder.Length != 0) + try + { + parent = FindOrCreateAllFolders(_config.DefaultImportFolder); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, + $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}.", + NotificationType.Warning); + } + + CreateDuplicateLeaf(parent, mod.Name.Text, mod); break; case ModPathChangeType.Deleted: if (FindLeaf(mod, out var leaf)) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 88d6afa2..55405313 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -196,10 +196,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) @@ -379,34 +376,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector - /// If a default import folder is setup, try to move the given mod in there. - /// If the folder does not exist, create it if possible. - /// - /// - private void MoveModToDefaultDirectory(Mod mod) - { - if (_config.DefaultImportFolder.Length == 0) - return; - - try - { - var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) - .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); - if (leaf == null) - throw new Exception("Mod was not found at root."); - - var folder = FileSystem.FindOrCreateAllFolders(_config.DefaultImportFolder); - FileSystem.Move(leaf, folder); - } - catch (Exception e) - { - _messager.NotificationMessage(e, - $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}.", - NotificationType.Warning); - } - } - private void DrawHelpPopup() { ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 38.5f * ImGui.GetTextLineHeightWithSpacing()), () => From 48ab98bee69117f5c74c425ab19d9c65fd545041 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Jul 2024 22:53:37 +0000 Subject: [PATCH 1844/2451] [CI] Updating repo.json for testing_1.2.0.12 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index d715bca2..ca13bb5b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.11", + "TestingAssemblyVersion": "1.2.0.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.12/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8c34c18643b34586346a902f6b0ffdcca6cc2907 Mon Sep 17 00:00:00 2001 From: pmgr <26606291+pmgr@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:34:27 +0100 Subject: [PATCH 1845/2451] Add scuffed pap handling --- .../Hooks/ResourceLoading/MappedCodeReader.cs | 13 ++ .../Hooks/ResourceLoading/PapHandler.cs | 23 +++ .../Hooks/ResourceLoading/PapRewriter.cs | 181 ++++++++++++++++ .../Hooks/ResourceLoading/PeSigScanner.cs | 194 ++++++++++++++++++ .../Hooks/ResourceLoading/ResourceLoader.cs | 26 +++ Penumbra/Penumbra.csproj | 13 +- 6 files changed, 446 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs diff --git a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs new file mode 100644 index 00000000..81712cca --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs @@ -0,0 +1,13 @@ +using Iced.Intel; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public class MappedCodeReader(UnmanagedMemoryAccessor data, long offset) : CodeReader +{ + public override int ReadByte() { + if (offset >= data.Capacity) + return -1; + + return data.ReadByte(offset++); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs new file mode 100644 index 00000000..29d77d83 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -0,0 +1,23 @@ +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +{ + private readonly PapRewriter _papRewriter = new(papResourceHandler); + + public void Enable() + { + _papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadMotionPacks); + _papRewriter.Rewrite(Sigs.LoadMotionPacks2); + _papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack); + } + + public void Dispose() + { + _papRewriter.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs new file mode 100644 index 00000000..cb437d9e --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -0,0 +1,181 @@ +using Dalamud.Hooking; +using Iced.Intel; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +{ + public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); + + private PeSigScanner Scanner { get; } = new(); + private Dictionary Hooks { get; }= []; + private List NativeAllocList { get; } = []; + private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler; + + public void Rewrite(string sig) + { + if (!Scanner.TryScanText(sig, out var addr)) + { + throw new Exception($"Sig is fucked: {sig}"); + } + + var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList(); + + var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); + + foreach (var hookPoint in hookPoints) + { + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + + var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength); + + { + // We'll need to grab our true hook point; the location where we can change the path at our leisure. + // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. + // Pretty scuffed, this might need a refactoring at some point. + // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL + var detourPoint = funcInstructions.Skip( + funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1 + ).First(instr => instr.Mnemonic == Mnemonic.Call); + + // We'll also remove all the 'hookPoints' from 'stackAccesses'. + // We're handling the char *path redirection here, so we don't want this to hit the later code + foreach (var hp in hookPoints) + { + stackAccesses.RemoveAll(instr => instr.IP == hp.IP); + } + + var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler); + var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var hookAddr = new IntPtr((long)detourPoint.IP); + + var caveLoc = NativeAlloc(16); + var hook = new AsmHook( + hookAddr, + [ + "use64", + $"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx) + + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + $"mov r9, 0x{caveLoc:x8}", + "mov [r9], rcx", + "mov [r9+0x8], rdx", + + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + $"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place + "call rax", // Call detour + + // Do the reverse process and retrieve the stored stuff + $"mov r9, 0x{caveLoc:x8}", + "mov rcx, [r9]", + "mov rdx, [r9+0x8]", + + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + "mov r8, rax", + ], "Pap Redirection" + ); + + Hooks.Add(hookAddr, hook); + hook.Enable(); + } + + // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' + foreach (var stackAccess in stackAccesses) + { + var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length); + + if (Hooks.ContainsKey(hookAddr)) + { + // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip + continue; + } + + var targetRegister = stackAccess.Op0Register.ToString().ToLower(); + var hook = new AsmHook( + hookAddr, + [ + "use64", + $"mov {targetRegister}, 0x{stringLoc:x8}", + ], "Pap Stack Accesses" + ); + + Hooks.Add(hookAddr, hook); + hook.Enable(); + } + } + + + + } + + private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) + { + return instructions.Where(instr => + instr.Code == hookPoint.Code + && instr.Op0Kind == hookPoint.Op0Kind + && instr.Op1Kind == hookPoint.Op1Kind + && instr.MemoryBase == hookPoint.MemoryBase + && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) + .GroupBy(instr => instr.IP) + .Select(grp => grp.First()); + } + + // This is utterly fucked and hardcoded, but, again, it works + // Might be a neat idea for a more versatile kind of signature though + private static IEnumerable ScanPapHookPoints(List funcInstructions) + { + for (var i = 0; i < funcInstructions.Count - 8; i++) + { + if (funcInstructions[i .. (i + 8)] is + [ + {Code : Code.Lea_r64_m}, + {Code : Code.Lea_r64_m}, + {Mnemonic: Mnemonic.Call}, + {Code : Code.Lea_r64_m}, + {Mnemonic: Mnemonic.Call}, + {Code : Code.Lea_r64_m}, + .., + ] + ) + { + yield return funcInstructions[i]; + } + } + } + + private unsafe IntPtr NativeAlloc(nuint size) + { + var caveLoc = new IntPtr(NativeMemory.Alloc(size)); + NativeAllocList.Add(caveLoc); + + return caveLoc; + } + + private static unsafe void NativeFree(IntPtr mem) + { + NativeMemory.Free(mem.ToPointer()); + } + + public void Dispose() + { + Scanner.Dispose(); + + foreach (var hook in Hooks.Values) + { + hook.Disable(); + hook.Dispose(); + } + + Hooks.Clear(); + + foreach (var mem in NativeAllocList) + { + NativeFree(mem); + } + + NativeAllocList.Clear(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs new file mode 100644 index 00000000..231e04f3 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -0,0 +1,194 @@ +using System.IO.MemoryMappedFiles; +using Iced.Intel; +using PeNet; +using Decoder = Iced.Intel.Decoder; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause I could not be faffed, maybe I'll rewrite it later +public class PeSigScanner : IDisposable +{ + private MemoryMappedFile File { get; } + + private uint TextSectionStart { get; } + private uint TextSectionSize { get; } + + private IntPtr ModuleBaseAddress { get; } + private uint TextSectionVirtualAddress { get; } + + private MemoryMappedViewAccessor TextSection { get; } + + + public PeSigScanner() + { + var mainModule = Process.GetCurrentProcess().MainModule!; + var fileName = mainModule.FileName; + ModuleBaseAddress = mainModule.BaseAddress; + + if (fileName == null) + { + throw new Exception("Can't get main module path, the fuck is going on?"); + } + + File = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + + using var fileStream = File.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); + var pe = new PeFile(fileStream); + + var textSection = pe.ImageSectionHeaders!.First(header => header.Name == ".text"); + + TextSectionStart = textSection.PointerToRawData; + TextSectionSize = textSection.SizeOfRawData; + TextSectionVirtualAddress = textSection.VirtualAddress; + + TextSection = File.CreateViewAccessor(TextSectionStart, TextSectionSize, MemoryMappedFileAccess.Read); + } + + + private IntPtr ScanText(string signature) + { + var scanRet = Scan(TextSection, signature); + + var instrByte = Marshal.ReadByte(scanRet); + + if (instrByte is 0xE8 or 0xE9) + scanRet = ReadJmpCallSig(scanRet); + + return scanRet; + } + + private static IntPtr ReadJmpCallSig(IntPtr sigLocation) + { + var jumpOffset = Marshal.ReadInt32(sigLocation, 1); + return IntPtr.Add(sigLocation, 5 + jumpOffset); + } + + public bool TryScanText(string signature, out IntPtr result) + { + try + { + result = ScanText(signature); + return true; + } + catch (KeyNotFoundException) + { + result = IntPtr.Zero; + return false; + } + } + + private IntPtr Scan(MemoryMappedViewAccessor section, string signature) + { + var (needle, mask) = ParseSignature(signature); + + var index = IndexOf(section, needle, mask); + if (index < 0) + throw new KeyNotFoundException($"Can't find a signature of {signature}"); + return new IntPtr(ModuleBaseAddress + index - section.PointerOffset + TextSectionVirtualAddress); + } + + private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) + { + signature = signature.Replace(" ", string.Empty); + if (signature.Length % 2 != 0) + throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature)); + + var needleLength = signature.Length / 2; + var needle = new byte[needleLength]; + var mask = new bool[needleLength]; + for (var i = 0; i < needleLength; i++) + { + var hexString = signature.Substring(i * 2, 2); + if (hexString == "??" || hexString == "**") + { + needle[i] = 0; + mask[i] = true; + continue; + } + + needle[i] = byte.Parse(hexString, NumberStyles.AllowHexSpecifier); + mask[i] = false; + } + + return (needle, mask); + } + + private static unsafe int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) + { + if (needle.Length > section.Capacity) return -1; + var badShift = BuildBadCharTable(needle, mask); + var last = needle.Length - 1; + var offset = 0; + var maxOffset = section.Capacity - needle.Length; + + byte* buffer = null; + section.SafeMemoryMappedViewHandle.AcquirePointer(ref buffer); + try + { + while (offset <= maxOffset) + { + int position; + for (position = last; needle[position] == *(buffer + position + offset) || mask[position]; position--) + { + if (position == 0) + return offset; + } + + offset += badShift[*(buffer + offset + last)]; + } + } + finally + { + section.SafeMemoryMappedViewHandle.ReleasePointer(); + } + + return -1; + } + + + private static int[] BuildBadCharTable(byte[] needle, bool[] mask) + { + int idx; + var last = needle.Length - 1; + var badShift = new int[256]; + for (idx = last; idx > 0 && !mask[idx]; --idx) + { + } + + var diff = last - idx; + if (diff == 0) diff = 1; + + for (idx = 0; idx <= 255; ++idx) + badShift[idx] = diff; + for (idx = last - diff; idx < last; ++idx) + badShift[needle[idx]] = last - idx; + return badShift; + } + + // Detects function termination; this is done in a really stupid way that will possibly break if looked at wrong, but it'll work for now + // If this shits itself, go bother Winter to implement proper CFG + basic block detection + public IEnumerable GetFunctionInstructions(IntPtr addr) + { + var fileOffset = addr - TextSectionVirtualAddress - ModuleBaseAddress; + + var codeReader = new MappedCodeReader(TextSection, fileOffset); + var decoder = Decoder.Create(64, codeReader, (ulong)addr.ToInt64()); + + do + { + decoder.Decode(out var instr); + + // Yes, this is catastrophically bad, but it works for some cases okay + if (instr.Mnemonic == Mnemonic.Int3) + break; + + yield return instr; + } while (true); + } + + public void Dispose() + { + TextSection.Dispose(); + File.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 195a8b9e..bc28c200 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -16,6 +16,8 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ResourceService _resources; private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; + + private readonly PapHandler _papHandler; private ResolveData _resolvedData = ResolveData.Invalid; @@ -30,6 +32,29 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleIncRef += IncRefProtection; _resources.ResourceHandleDecRef += DecRefProtection; _fileReadService.ReadSqPack += ReadSqPackDetour; + + _papHandler = new PapHandler(PapResourceHandler); + _papHandler.Enable(); + } + + private int PapResourceHandler(void* self, byte* path, int length) + { + Utf8GamePath.FromPointer(path, out var gamePath); + + var (resolvedPath, _) = _incMode.Value + ? (null, ResolveData.Invalid) + : _resolvedData.Valid + ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) + : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); + + if (!resolvedPath.HasValue || !Utf8GamePath.FromString(resolvedPath.Value.FullName, out var utf8ResolvedPath)) + { + return length; + } + + NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); + path[utf8ResolvedPath.Length] = 0; + return utf8ResolvedPath.Length; } /// Load a resource for a given path and a specific collection. @@ -84,6 +109,7 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; _fileReadService.ReadSqPack -= ReadSqPackDetour; + _papHandler.Dispose(); } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2e53bd22..70208737 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,10 +23,10 @@ PROFILING; - - - - + + + + @@ -68,6 +68,10 @@ $(DalamudLibPath)Newtonsoft.Json.dll False + + $(DalamudLibPath)Iced.dll + False + lib\OtterTex.dll @@ -79,6 +83,7 @@ + From 8351b74b21c5dfaca5691bc7e2956715573e7fa5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:23:31 +0200 Subject: [PATCH 1846/2451] Update GameData and packages. --- Penumbra.GameData | 2 +- Penumbra/packages.lock.json | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9f1816f1..f13818fd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9f1816f1b75003d01c5576769831c10f3d8948a7 +Subproject commit f13818fd85b436d0a0f66293fe7c6b60d4bffe3c diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index b431e595..42539e78 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,6 +11,16 @@ "Unosquare.Swan.Lite": "3.0.0" } }, + "PeNet": { + "type": "Direct", + "requested": "[4.0.5, )", + "resolved": "4.0.5", + "contentHash": "/OUfRnMG8STVuK8kTpdfm+WQGTDoUiJnI845kFw4QrDmv2gYourmSnH84pqVjHT1YHBSuRfCzfioIpHGjFJrGA==", + "dependencies": { + "PeNet.Asn1": "2.0.1", + "System.Security.Cryptography.Pkcs": "8.0.0" + } + }, "SharpCompress": { "type": "Direct", "requested": "[0.33.0, )", @@ -56,6 +66,11 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "PeNet.Asn1": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "YR2O2YokSAYB+7CXkCDN3bd6/p0K3/AicCPkOJHKUz500v1D/hulCuVlggguqNc3M0LgSfOZKGvVYg2ud1GA9A==" + }, "SharpGLTF.Runtime": { "type": "Transitive", "resolved": "1.0.0-alpha0030", @@ -64,6 +79,19 @@ "SharpGLTF.Core": "1.0.0-alpha0030" } }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==" + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", + "dependencies": { + "System.Formats.Asn1": "8.0.0" + } + }, "System.ValueTuple": { "type": "Transitive", "resolved": "4.5.0", @@ -94,7 +122,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.0.0, )", + "Penumbra.Api": "[5.2.0, )", "Penumbra.String": "[1.0.4, )" } }, From 0db70c89b105ec8b76a0c3a6764ab4c566e016f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:58:03 +0200 Subject: [PATCH 1847/2451] Some cleanup of PeSigScanner. --- .../Hooks/ResourceLoading/PapHandler.cs | 4 +- .../Hooks/ResourceLoading/PeSigScanner.cs | 118 +++++++++--------- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 29d77d83..f0fd8b0e 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -17,7 +17,5 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour } public void Dispose() - { - _papRewriter.Dispose(); - } + => _papRewriter.Dispose(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index 231e04f3..f5dd2d45 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -5,65 +5,56 @@ using Decoder = Iced.Intel.Decoder; namespace Penumbra.Interop.Hooks.ResourceLoading; -// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause I could not be faffed, maybe I'll rewrite it later -public class PeSigScanner : IDisposable +// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause Winter could not be faffed, Winter will definitely not rewrite it later +public unsafe class PeSigScanner : IDisposable { - private MemoryMappedFile File { get; } + private readonly MemoryMappedFile _file; + private readonly MemoryMappedViewAccessor _textSection; + + private readonly nint _moduleBaseAddress; + private readonly uint _textSectionVirtualAddress; + - private uint TextSectionStart { get; } - private uint TextSectionSize { get; } - - private IntPtr ModuleBaseAddress { get; } - private uint TextSectionVirtualAddress { get; } - - private MemoryMappedViewAccessor TextSection { get; } - - public PeSigScanner() { var mainModule = Process.GetCurrentProcess().MainModule!; - var fileName = mainModule.FileName; - ModuleBaseAddress = mainModule.BaseAddress; + var fileName = mainModule.FileName; + _moduleBaseAddress = mainModule.BaseAddress; if (fileName == null) - { - throw new Exception("Can't get main module path, the fuck is going on?"); - } - - File = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + throw new Exception("Unable to obtain main module path. This should not happen."); - using var fileStream = File.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); + _file = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + + using var fileStream = _file.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); var pe = new PeFile(fileStream); var textSection = pe.ImageSectionHeaders!.First(header => header.Name == ".text"); - TextSectionStart = textSection.PointerToRawData; - TextSectionSize = textSection.SizeOfRawData; - TextSectionVirtualAddress = textSection.VirtualAddress; + var textSectionStart = textSection.PointerToRawData; + var textSectionSize = textSection.SizeOfRawData; + _textSectionVirtualAddress = textSection.VirtualAddress; - TextSection = File.CreateViewAccessor(TextSectionStart, TextSectionSize, MemoryMappedFileAccess.Read); + _textSection = _file.CreateViewAccessor(textSectionStart, textSectionSize, MemoryMappedFileAccess.Read); } - private IntPtr ScanText(string signature) + private nint ScanText(string signature) { - var scanRet = Scan(TextSection, signature); - - var instrByte = Marshal.ReadByte(scanRet); - - if (instrByte is 0xE8 or 0xE9) + var scanRet = Scan(_textSection, signature); + if (*(byte*)scanRet is 0xE8 or 0xE9) scanRet = ReadJmpCallSig(scanRet); return scanRet; } - - private static IntPtr ReadJmpCallSig(IntPtr sigLocation) + + private static nint ReadJmpCallSig(nint sigLocation) { - var jumpOffset = Marshal.ReadInt32(sigLocation, 1); - return IntPtr.Add(sigLocation, 5 + jumpOffset); + var jumpOffset = *(int*)(sigLocation + 1); + return sigLocation + 5 + jumpOffset; } - - public bool TryScanText(string signature, out IntPtr result) + + public bool TryScanText(string signature, out nint result) { try { @@ -72,21 +63,22 @@ public class PeSigScanner : IDisposable } catch (KeyNotFoundException) { - result = IntPtr.Zero; + result = nint.Zero; return false; } } - - private IntPtr Scan(MemoryMappedViewAccessor section, string signature) + + private nint Scan(MemoryMappedViewAccessor section, string signature) { var (needle, mask) = ParseSignature(signature); - - var index = IndexOf(section, needle, mask); + + var index = IndexOf(section, needle, mask); if (index < 0) throw new KeyNotFoundException($"Can't find a signature of {signature}"); - return new IntPtr(ModuleBaseAddress + index - section.PointerOffset + TextSectionVirtualAddress); + + return (nint)(_moduleBaseAddress + index - section.PointerOffset + _textSectionVirtualAddress); } - + private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) { signature = signature.Replace(" ", string.Empty); @@ -99,7 +91,7 @@ public class PeSigScanner : IDisposable for (var i = 0; i < needleLength; i++) { var hexString = signature.Substring(i * 2, 2); - if (hexString == "??" || hexString == "**") + if (hexString is "??" or "**") { needle[i] = 0; mask[i] = true; @@ -112,10 +104,12 @@ public class PeSigScanner : IDisposable return (needle, mask); } - - private static unsafe int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) + + private static int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) { - if (needle.Length > section.Capacity) return -1; + if (needle.Length > section.Capacity) + return -1; + var badShift = BuildBadCharTable(needle, mask); var last = needle.Length - 1; var offset = 0; @@ -144,19 +138,19 @@ public class PeSigScanner : IDisposable return -1; } - - + + private static int[] BuildBadCharTable(byte[] needle, bool[] mask) { int idx; var last = needle.Length - 1; var badShift = new int[256]; for (idx = last; idx > 0 && !mask[idx]; --idx) - { - } + { } - var diff = last - idx; - if (diff == 0) diff = 1; + var diff = last - idx; + if (diff == 0) + diff = 1; for (idx = 0; idx <= 255; ++idx) badShift[idx] = diff; @@ -164,16 +158,16 @@ public class PeSigScanner : IDisposable badShift[needle[idx]] = last - idx; return badShift; } - + // Detects function termination; this is done in a really stupid way that will possibly break if looked at wrong, but it'll work for now // If this shits itself, go bother Winter to implement proper CFG + basic block detection - public IEnumerable GetFunctionInstructions(IntPtr addr) + public IEnumerable GetFunctionInstructions(nint address) { - var fileOffset = addr - TextSectionVirtualAddress - ModuleBaseAddress; - - var codeReader = new MappedCodeReader(TextSection, fileOffset); - var decoder = Decoder.Create(64, codeReader, (ulong)addr.ToInt64()); - + var fileOffset = address - _textSectionVirtualAddress - _moduleBaseAddress; + + var codeReader = new MappedCodeReader(_textSection, fileOffset); + var decoder = Decoder.Create(64, codeReader, (ulong)address.ToInt64()); + do { decoder.Decode(out var instr); @@ -188,7 +182,7 @@ public class PeSigScanner : IDisposable public void Dispose() { - TextSection.Dispose(); - File.Dispose(); + _textSection.Dispose(); + _file.Dispose(); } } From ee5a21f7a20dc459b1ec0d903c008f616fbcde3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:58:24 +0200 Subject: [PATCH 1848/2451] Add pap requested event, some cleanup. --- .../Hooks/ResourceLoading/ResourceLoader.cs | 22 +++++---- .../UI/ResourceWatcher/ResourceWatcher.cs | 47 ++++++++++++------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index bc28c200..cf87aa2b 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -16,10 +16,10 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ResourceService _resources; private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; - - private readonly PapHandler _papHandler; + private readonly PapHandler _papHandler; - private ResolveData _resolvedData = ResolveData.Invalid; + private ResolveData _resolvedData = ResolveData.Invalid; + public event Action? PapRequested; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { @@ -36,24 +36,28 @@ public unsafe class ResourceLoader : IDisposable, IService _papHandler = new PapHandler(PapResourceHandler); _papHandler.Enable(); } - + private int PapResourceHandler(void* self, byte* path, int length) { - Utf8GamePath.FromPointer(path, out var gamePath); - + if (!Utf8GamePath.FromPointer(path, out var gamePath)) + return length; + var (resolvedPath, _) = _incMode.Value ? (null, ResolveData.Invalid) : _resolvedData.Valid ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); - - if (!resolvedPath.HasValue || !Utf8GamePath.FromString(resolvedPath.Value.FullName, out var utf8ResolvedPath)) + + + if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) { + PapRequested?.Invoke(gamePath, gamePath, _resolvedData); return length; } - + NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); path[utf8ResolvedPath.Length] = 0; + PapRequested?.Invoke(gamePath, utf8ResolvedPath, _resolvedData); return utf8ResolvedPath.Length; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 935f11e3..a00b33c7 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -36,31 +36,47 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private Regex? _logRegex; private int _newMaxEntries; - public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, ResourceHandleDestructor destructor) + public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, + ResourceHandleDestructor destructor) { - _actors = actors; - _config = config; - _ephemeral = config.Ephemeral; - _resources = resources; - _destructor = destructor; - _loader = loader; - _table = new ResourceWatcherTable(config.Ephemeral, _records); - _resources.ResourceRequested += OnResourceRequested; + _actors = actors; + _config = config; + _ephemeral = config.Ephemeral; + _resources = resources; + _destructor = destructor; + _loader = loader; + _table = new ResourceWatcherTable(config.Ephemeral, _records); + _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.FileLoaded += OnFileLoaded; + _loader.PapRequested += OnPapRequested; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } + private void OnPapRequested(Utf8GamePath original) + { + if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); + + if (!_ephemeral.EnableResourceWatcher) + return; + + var record = Record.CreateRequest(original.Path, false); + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + public unsafe void Dispose() { Clear(); _records.TrimExcess(); - _resources.ResourceRequested -= OnResourceRequested; + _resources.ResourceRequested -= OnResourceRequested; _destructor.Unsubscribe(OnResourceDestroyed); - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.FileLoaded -= OnFileLoaded; + _loader.PapRequested -= OnPapRequested; } private void Clear() @@ -200,8 +216,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, - Utf8GamePath original, - GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); From ceaa9ca29a05125fd08cd76e664145cac37c7343 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 23:26:09 +0200 Subject: [PATCH 1849/2451] Some further cleanup. --- .../Hooks/ResourceLoading/PapHandler.cs | 25 +- .../Hooks/ResourceLoading/PapRewriter.cs | 230 ++++++++---------- .../Hooks/ResourceLoading/ResourceLoader.cs | 4 +- 3 files changed, 127 insertions(+), 132 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index f0fd8b0e..65add13c 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -5,17 +5,26 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { private readonly PapRewriter _papRewriter = new(papResourceHandler); - + public void Enable() { - _papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadMotionPacks); - _papRewriter.Rewrite(Sigs.LoadMotionPacks2); - _papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack); + ReadOnlySpan signatures = + [ + Sigs.LoadAlwaysResidentMotionPacks, + Sigs.LoadWeaponDependentResidentMotionPacks, + Sigs.LoadInitialResidentMotionPacks, + Sigs.LoadMotionPacks, + Sigs.LoadMotionPacks2, + Sigs.LoadMigratoryMotionPack, + ]; + + var stopwatch = Stopwatch.StartNew(); + foreach (var sig in signatures) + _papRewriter.Rewrite(sig); + Penumbra.Log.Debug( + $"[PapHandler] Rewrote {signatures.Length} .pap functions for inlined GetResourceAsync in {stopwatch.ElapsedMilliseconds} ms."); } - + public void Dispose() => _papRewriter.Dispose(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index cb437d9e..af16d706 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using Iced.Intel; +using OtterGui; using Penumbra.String.Classes; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -7,175 +8,160 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - - private PeSigScanner Scanner { get; } = new(); - private Dictionary Hooks { get; }= []; - private List NativeAllocList { get; } = []; - private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler; + + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly List _nativeAllocList = []; public void Rewrite(string sig) { - if (!Scanner.TryScanText(sig, out var addr)) - { - throw new Exception($"Sig is fucked: {sig}"); - } + if (!_scanner.TryScanText(sig, out var address)) + throw new Exception($"Signature [{sig}] could not be found."); - var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList(); - - var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); + var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); + var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); foreach (var hookPoint in hookPoints) { - var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); - var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength); + // We'll need to grab our true hook point; the location where we can change the path at our leisure. + // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. + // Pretty scuffed, this might need a refactoring at some point. + // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL + var skipIndex = funcInstructions.IndexOf(instr => instr.IP == hookPoint.IP) + 1; + var detourPoint = funcInstructions.Skip(skipIndex) + .First(instr => instr.Mnemonic == Mnemonic.Call); - { - // We'll need to grab our true hook point; the location where we can change the path at our leisure. - // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. - // Pretty scuffed, this might need a refactoring at some point. - // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL - var detourPoint = funcInstructions.Skip( - funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1 - ).First(instr => instr.Mnemonic == Mnemonic.Call); + // We'll also remove all the 'hookPoints' from 'stackAccesses'. + // We're handling the char *path redirection here, so we don't want this to hit the later code + foreach (var hp in hookPoints) + stackAccesses.RemoveAll(instr => instr.IP == hp.IP); - // We'll also remove all the 'hookPoints' from 'stackAccesses'. - // We're handling the char *path redirection here, so we don't want this to hit the later code - foreach (var hp in hookPoints) - { - stackAccesses.RemoveAll(instr => instr.IP == hp.IP); - } + var detourPointer = Marshal.GetFunctionPointerForDelegate(papResourceHandler); + var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var hookAddress = new IntPtr((long)detourPoint.IP); - var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler); - var targetRegister = hookPoint.Op0Register.ToString().ToLower(); - var hookAddr = new IntPtr((long)detourPoint.IP); + var caveAllocation = NativeAlloc(16); + var hook = new AsmHook( + hookAddress, + [ + "use64", + $"mov {targetRegister}, 0x{stringAllocation:x8}", // Move our char *path into the relevant register (rdx) - var caveLoc = NativeAlloc(16); - var hook = new AsmHook( - hookAddr, - [ - "use64", - $"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx) - - // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves - // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call - // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh - $"mov r9, 0x{caveLoc:x8}", - "mov [r9], rcx", - "mov [r9+0x8], rdx", - - // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway - $"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place - "call rax", // Call detour - - // Do the reverse process and retrieve the stored stuff - $"mov r9, 0x{caveLoc:x8}", - "mov rcx, [r9]", - "mov rdx, [r9+0x8]", - - // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call - "mov r8, rax", - ], "Pap Redirection" - ); + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + $"mov r9, 0x{caveAllocation:x8}", + "mov [r9], rcx", + "mov [r9+0x8], rdx", - Hooks.Add(hookAddr, hook); - hook.Enable(); - } + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + $"mov rax, 0x{detourPointer:x8}", // Get a pointer to our detour in place + "call rax", // Call detour + + // Do the reverse process and retrieve the stored stuff + $"mov r9, 0x{caveAllocation:x8}", + "mov rcx, [r9]", + "mov rdx, [r9+0x8]", + + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + "mov r8, rax", + ], "Pap Redirection" + ); + + _hooks.Add(hookAddress, hook); + hook.Enable(); // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' - foreach (var stackAccess in stackAccesses) - { - var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length); - - if (Hooks.ContainsKey(hookAddr)) - { - // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip - continue; - } - - var targetRegister = stackAccess.Op0Register.ToString().ToLower(); - var hook = new AsmHook( - hookAddr, - [ - "use64", - $"mov {targetRegister}, 0x{stringLoc:x8}", - ], "Pap Stack Accesses" - ); - - Hooks.Add(hookAddr, hook); - hook.Enable(); - } + UpdatePathAddresses(stackAccesses, stringAllocation); + } + } + + private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation) + { + foreach (var stackAccess in stackAccesses) + { + var hookAddress = new IntPtr((long)stackAccess.IP + stackAccess.Length); + + // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip + if (_hooks.ContainsKey(hookAddress)) + continue; + + var targetRegister = stackAccess.Op0Register.ToString().ToLower(); + var hook = new AsmHook( + hookAddress, + [ + "use64", + $"mov {targetRegister}, 0x{stringAllocation:x8}", + ], "Pap Stack Accesses" + ); + + _hooks.Add(hookAddress, hook); + hook.Enable(); } - - - } private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) { return instructions.Where(instr => - instr.Code == hookPoint.Code - && instr.Op0Kind == hookPoint.Op0Kind - && instr.Op1Kind == hookPoint.Op1Kind - && instr.MemoryBase == hookPoint.MemoryBase - && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) - .GroupBy(instr => instr.IP) - .Select(grp => grp.First()); + instr.Code == hookPoint.Code + && instr.Op0Kind == hookPoint.Op0Kind + && instr.Op1Kind == hookPoint.Op1Kind + && instr.MemoryBase == hookPoint.MemoryBase + && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) + .GroupBy(instr => instr.IP) + .Select(grp => grp.First()); } // This is utterly fucked and hardcoded, but, again, it works // Might be a neat idea for a more versatile kind of signature though - private static IEnumerable ScanPapHookPoints(List funcInstructions) + private static IEnumerable ScanPapHookPoints(Instruction[] funcInstructions) { - for (var i = 0; i < funcInstructions.Count - 8; i++) + for (var i = 0; i < funcInstructions.Length - 8; i++) { - if (funcInstructions[i .. (i + 8)] is + if (funcInstructions.AsSpan(i, 8) is [ - {Code : Code.Lea_r64_m}, - {Code : Code.Lea_r64_m}, - {Mnemonic: Mnemonic.Call}, - {Code : Code.Lea_r64_m}, - {Mnemonic: Mnemonic.Call}, - {Code : Code.Lea_r64_m}, + { Code : Code.Lea_r64_m }, + { Code : Code.Lea_r64_m }, + { Mnemonic: Mnemonic.Call }, + { Code : Code.Lea_r64_m }, + { Mnemonic: Mnemonic.Call }, + { Code : Code.Lea_r64_m }, .., ] ) - { yield return funcInstructions[i]; - } } } - private unsafe IntPtr NativeAlloc(nuint size) + private unsafe nint NativeAlloc(nuint size) { - var caveLoc = new IntPtr(NativeMemory.Alloc(size)); - NativeAllocList.Add(caveLoc); + var caveLoc = (nint)NativeMemory.Alloc(size); + _nativeAllocList.Add(caveLoc); return caveLoc; } - private static unsafe void NativeFree(IntPtr mem) - { - NativeMemory.Free(mem.ToPointer()); - } + private static unsafe void NativeFree(nint mem) + => NativeMemory.Free((void*)mem); public void Dispose() { - Scanner.Dispose(); - - foreach (var hook in Hooks.Values) + _scanner.Dispose(); + + foreach (var hook in _hooks.Values) { hook.Disable(); hook.Dispose(); } - - Hooks.Clear(); - - foreach (var mem in NativeAllocList) - { - NativeFree(mem); - } - NativeAllocList.Clear(); + _hooks.Clear(); + + foreach (var mem in _nativeAllocList) + NativeFree(mem); + + _nativeAllocList.Clear(); } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index cf87aa2b..002846fa 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -51,13 +51,13 @@ public unsafe class ResourceLoader : IDisposable, IService if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) { - PapRequested?.Invoke(gamePath, gamePath, _resolvedData); + PapRequested?.Invoke(gamePath); return length; } NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); path[utf8ResolvedPath.Length] = 0; - PapRequested?.Invoke(gamePath, utf8ResolvedPath, _resolvedData); + PapRequested?.Invoke(gamePath); return utf8ResolvedPath.Length; } From cec28a1823fcbb228136705ff98733d70ca71c9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 23:49:27 +0200 Subject: [PATCH 1850/2451] Provide actual hook names. --- .../Hooks/ResourceLoading/PapHandler.cs | 18 +++++++++--------- .../Hooks/ResourceLoading/PapRewriter.cs | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 65add13c..ea12a480 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -8,19 +8,19 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour public void Enable() { - ReadOnlySpan signatures = + ReadOnlySpan<(string Sig, string Name)> signatures = [ - Sigs.LoadAlwaysResidentMotionPacks, - Sigs.LoadWeaponDependentResidentMotionPacks, - Sigs.LoadInitialResidentMotionPacks, - Sigs.LoadMotionPacks, - Sigs.LoadMotionPacks2, - Sigs.LoadMigratoryMotionPack, + (Sigs.LoadAlwaysResidentMotionPacks, nameof(Sigs.LoadAlwaysResidentMotionPacks)), + (Sigs.LoadWeaponDependentResidentMotionPacks, nameof(Sigs.LoadWeaponDependentResidentMotionPacks)), + (Sigs.LoadInitialResidentMotionPacks, nameof(Sigs.LoadInitialResidentMotionPacks)), + (Sigs.LoadMotionPacks, nameof(Sigs.LoadMotionPacks)), + (Sigs.LoadMotionPacks2, nameof(Sigs.LoadMotionPacks2)), + (Sigs.LoadMigratoryMotionPack, nameof(Sigs.LoadMigratoryMotionPack)), ]; var stopwatch = Stopwatch.StartNew(); - foreach (var sig in signatures) - _papRewriter.Rewrite(sig); + foreach (var (sig, name) in signatures) + _papRewriter.Rewrite(sig, name); Penumbra.Log.Debug( $"[PapHandler] Rewrote {signatures.Length} .pap functions for inlined GetResourceAsync in {stopwatch.ElapsedMilliseconds} ms."); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index af16d706..5a2b09bf 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -13,10 +13,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou private readonly Dictionary _hooks = []; private readonly List _nativeAllocList = []; - public void Rewrite(string sig) + public void Rewrite(string sig, string name) { if (!_scanner.TryScanText(sig, out var address)) - throw new Exception($"Signature [{sig}] could not be found."); + throw new Exception($"Signature for {name} [{sig}] could not be found."); var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); @@ -68,20 +68,20 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call "mov r8, rax", - ], "Pap Redirection" + ], $"{name}.PapRedirection" ); _hooks.Add(hookAddress, hook); hook.Enable(); // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' - UpdatePathAddresses(stackAccesses, stringAllocation); + UpdatePathAddresses(stackAccesses, stringAllocation, name); } } - private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation) + private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation, string name) { - foreach (var stackAccess in stackAccesses) + foreach (var (stackAccess, index) in stackAccesses.WithIndex()) { var hookAddress = new IntPtr((long)stackAccess.IP + stackAccess.Length); @@ -95,7 +95,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou [ "use64", $"mov {targetRegister}, 0x{stringAllocation:x8}", - ], "Pap Stack Accesses" + ], $"{name}.PapStackAccess[{index}]" ); _hooks.Add(hookAddress, hook); From 1501bd4fbf5b05be2b770b0e1b8b856d71f1dce3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 12:41:20 +0200 Subject: [PATCH 1851/2451] Fix negative matching on folders with no matches. --- Penumbra/UI/ModsTab/ModSearchStringSplitter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index 1ea70731..e7550eea 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -95,9 +95,9 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter MatchesName(i, folder.Name, fullName)) - && !Negated.Any(i => MatchesName(i, folder.Name, fullName)) - && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName))); + return Forced.All(i => MatchesName(i, folder.Name, fullName, false)) + && !Negated.Any(i => MatchesName(i, folder.Name, fullName, true)) + && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName, false))); } protected override bool Matches(Entry entry, ModFileSystem.Leaf leaf) @@ -128,11 +128,11 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter true, }; - private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName) + private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName, bool defaultValue) => entry.Type switch { ModSearchType.Default => fullName.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), ModSearchType.Name => name.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), - _ => false, + _ => defaultValue, }; } From 29dce8f3ab3656253a6c843edde527d2442f9199 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 22 Jul 2024 13:16:22 +0000 Subject: [PATCH 1852/2451] [CI] Updating repo.json for testing_1.2.0.13 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ca13bb5b..b6d37a92 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.12", + "TestingAssemblyVersion": "1.2.0.13", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.13/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ca648f98a173fa75fc538e0a1c1bc0630b64ac55 Mon Sep 17 00:00:00 2001 From: pmgr <26606291+pmgr@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:37:57 +0100 Subject: [PATCH 1853/2451] Fix for pap weirdness, hopefully --- .../Hooks/ResourceLoading/PapRewriter.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5a2b09bf..84cd0c11 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Iced.Intel; using OtterGui; using Penumbra.String.Classes; +using Swan; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -9,9 +10,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); - private readonly Dictionary _hooks = []; - private readonly List _nativeAllocList = []; + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; + private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { @@ -24,7 +26,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou foreach (var hookPoint in hookPoints) { var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); - var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); + var stringAllocation = NativeAllocPath( + address, hookPoint.MemoryBase, hookPoint.MemoryDisplacement64, + Utf8GamePath.MaxGamePathLength + ); // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. @@ -43,7 +48,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou var targetRegister = hookPoint.Op0Register.ToString().ToLower(); var hookAddress = new IntPtr((long)detourPoint.IP); - var caveAllocation = NativeAlloc(16); + var caveAllocation = NativeAllocCave(16); var hook = new AsmHook( hookAddress, [ @@ -136,13 +141,24 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou } } - private unsafe nint NativeAlloc(nuint size) + private unsafe nint NativeAllocCave(nuint size) { var caveLoc = (nint)NativeMemory.Alloc(size); - _nativeAllocList.Add(caveLoc); + _nativeAllocCaves.Add(caveLoc); return caveLoc; } + + // This is a bit conked but, if we identify a path by: + // 1) The function it belongs to (starting address, 'funcAddress') + // 2) The stack register (not strictly necessary - should always be rbp - but abundance of caution, so I don't hit myself in the future) + // 3) The displacement on the stack + // Then we ensure we have a unique identifier for the specific variable location of that specific function + // This is useful because sometimes the stack address is reused within the same function for different GetResourceAsync calls + private unsafe nint NativeAllocPath(nint funcAddress, Register stackRegister, ulong stackDisplacement, nuint size) + { + return _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); + } private static unsafe void NativeFree(nint mem) => NativeMemory.Free((void*)mem); @@ -159,9 +175,14 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou _hooks.Clear(); - foreach (var mem in _nativeAllocList) + foreach (var mem in _nativeAllocCaves) NativeFree(mem); - _nativeAllocList.Clear(); + _nativeAllocCaves.Clear(); + + foreach (var mem in _nativeAllocPaths.Values) + NativeFree(mem); + + _nativeAllocPaths.Clear(); } } From a4cd5695fb3e4908e7e51c19e2d42551acc27d19 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 20:41:57 +0200 Subject: [PATCH 1854/2451] Fix some stuff. --- Penumbra/Api/Api/GameStateApi.cs | 23 +++++++++++++++---- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Hooks/ResourceLoading/PapRewriter.cs | 10 +++++++- .../Hooks/ResourceLoading/ResourceLoader.cs | 18 +++++++-------- .../UI/ResourceWatcher/ResourceWatcher.cs | 3 +-- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index b035c886..c2cae32b 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -25,12 +25,14 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable _cutsceneService = cutsceneService; _resourceLoader = resourceLoader; _resourceLoader.ResourceLoaded += OnResourceLoaded; + _resourceLoader.PapRequested += OnPapRequested; _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() { _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _resourceLoader.PapRequested -= OnPapRequested; _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); } @@ -67,14 +69,27 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) - ? PenumbraApiEc.Success + ? PenumbraApiEc.Success : PenumbraApiEc.InvalidArgument; private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) { - if (resolveData.AssociatedGameObject != nint.Zero) - GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString()); + if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null) + { + var original = originalPath.ToString(); + GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original, + manipulatedPath?.ToString() ?? original); + } + } + + private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) + { + if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null) + { + var original = originalPath.ToString(); + GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original, + manipulatedPath?.ToString() ?? original); + } } private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 361fcd4e..96a51027 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -42,7 +42,7 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook> 13) & 1) == 0) return Task.Result.Original(a1, unused, timeOffset); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5a2b09bf..33b124c8 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,3 +1,4 @@ +using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; using OtterGui; @@ -25,7 +26,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); - + WriteToAlloc(stringAllocation, Utf8GamePath.MaxGamePathLength, name); // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. // Pretty scuffed, this might need a refactoring at some point. @@ -164,4 +165,11 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou _nativeAllocList.Clear(); } + + [Conditional("DEBUG")] + private static unsafe void WriteToAlloc(nint alloc, int size, string name) + { + var span = new Span((void*)alloc, size); + Utf8.TryWrite(span, $"Penumbra.{name}\0", out _); + } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 002846fa..10821287 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -18,8 +18,8 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly TexMdlService _texMdlService; private readonly PapHandler _papHandler; - private ResolveData _resolvedData = ResolveData.Invalid; - public event Action? PapRequested; + private ResolveData _resolvedData = ResolveData.Invalid; + public event Action? PapRequested; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { @@ -42,23 +42,23 @@ public unsafe class ResourceLoader : IDisposable, IService if (!Utf8GamePath.FromPointer(path, out var gamePath)) return length; - var (resolvedPath, _) = _incMode.Value + var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : _resolvedData.Valid ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); - if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) + if (!resolvedPath.HasValue) { - PapRequested?.Invoke(gamePath); + PapRequested?.Invoke(gamePath, null, data); return length; } - NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); - path[utf8ResolvedPath.Length] = 0; - PapRequested?.Invoke(gamePath); - return utf8ResolvedPath.Length; + PapRequested?.Invoke(gamePath, resolvedPath.Value, data); + NativeMemory.Copy(resolvedPath.Value.InternalName.Path, path, (nuint)resolvedPath.Value.InternalName.Length); + path[resolvedPath.Value.InternalName.Length] = 0; + return resolvedPath.Value.InternalName.Length; } /// Load a resource for a given path and a specific collection. diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index a00b33c7..14d69489 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -11,7 +11,6 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -55,7 +54,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newMaxEntries = _config.MaxResourceWatcherRecords; } - private void OnPapRequested(Utf8GamePath original) + private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); From df335574778cfee6fa44a5d63a2ef869c5706c11 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 20:54:24 +0200 Subject: [PATCH 1855/2451] Cleanup. --- Penumbra.Api | 2 +- .../Hooks/ResourceLoading/PapRewriter.cs | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index f4c6144c..86249598 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f4c6144ca2012b279e6d8aa52b2bef6cc2ba32d9 +Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1 diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index afe00b8d..2fb1623d 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -11,10 +11,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); - private readonly Dictionary _hooks = []; - private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; - private readonly List _nativeAllocCaves = []; + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; + private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { @@ -26,13 +26,13 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou foreach (var hookPoint in hookPoints) { - var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); var stringAllocation = NativeAllocPath( address, hookPoint.MemoryBase, hookPoint.MemoryDisplacement64, Utf8GamePath.MaxGamePathLength - ); + ); WriteToAlloc(stringAllocation, Utf8GamePath.MaxGamePathLength, name); - + // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. // Pretty scuffed, this might need a refactoring at some point. @@ -150,7 +150,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou return caveLoc; } - + // This is a bit conked but, if we identify a path by: // 1) The function it belongs to (starting address, 'funcAddress') // 2) The stack register (not strictly necessary - should always be rbp - but abundance of caution, so I don't hit myself in the future) @@ -158,9 +158,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou // Then we ensure we have a unique identifier for the specific variable location of that specific function // This is useful because sometimes the stack address is reused within the same function for different GetResourceAsync calls private unsafe nint NativeAllocPath(nint funcAddress, Register stackRegister, ulong stackDisplacement, nuint size) - { - return _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); - } + => _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); private static unsafe void NativeFree(nint mem) => NativeMemory.Free((void*)mem); @@ -181,7 +179,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou NativeFree(mem); _nativeAllocCaves.Clear(); - + foreach (var mem in _nativeAllocPaths.Values) NativeFree(mem); From 30f92338627b2d29558a96aa66eb6e1184443138 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 22 Jul 2024 18:56:35 +0000 Subject: [PATCH 1856/2451] [CI] Updating repo.json for testing_1.2.0.14 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index b6d37a92..f1108cfe 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.13", + "TestingAssemblyVersion": "1.2.0.14", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.13/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.14/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c501d0b36524d163a45758901390cb840f7f1107 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Jul 2024 15:11:35 +0200 Subject: [PATCH 1857/2451] Fix card actor identification. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f13818fd..f5a74c70 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f13818fd85b436d0a0f66293fe7c6b60d4bffe3c +Subproject commit f5a74c70ad3861c5c66e1df6ae9a29fc7a0d736a From 72f2834dfd13352f70f10e77b1d40a2910198f13 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Jul 2024 15:11:52 +0200 Subject: [PATCH 1858/2451] Add some resource flags. --- Penumbra/Enums/ResourceTypeFlag.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Penumbra/Enums/ResourceTypeFlag.cs b/Penumbra/Enums/ResourceTypeFlag.cs index 0cfc5469..461e7ac1 100644 --- a/Penumbra/Enums/ResourceTypeFlag.cs +++ b/Penumbra/Enums/ResourceTypeFlag.cs @@ -60,6 +60,13 @@ public enum ResourceTypeFlag : ulong Uld = 0x0002_0000_0000_0000, Waoe = 0x0004_0000_0000_0000, Wtd = 0x0008_0000_0000_0000, + Bklb = 0x0010_0000_0000_0000, + Cutb = 0x0020_0000_0000_0000, + Eanb = 0x0040_0000_0000_0000, + Eslb = 0x0080_0000_0000_0000, + Fpeb = 0x0100_0000_0000_0000, + Kdb = 0x0200_0000_0000_0000, + Kdlb = 0x0400_0000_0000_0000, } [Flags] @@ -141,6 +148,13 @@ public static class ResourceExtensions ResourceType.Uld => ResourceTypeFlag.Uld, ResourceType.Waoe => ResourceTypeFlag.Waoe, ResourceType.Wtd => ResourceTypeFlag.Wtd, + ResourceType.Bklb => ResourceTypeFlag.Bklb, + ResourceType.Cutb => ResourceTypeFlag.Cutb, + ResourceType.Eanb => ResourceTypeFlag.Eanb, + ResourceType.Eslb => ResourceTypeFlag.Eslb, + ResourceType.Fpeb => ResourceTypeFlag.Fpeb, + ResourceType.Kdb => ResourceTypeFlag.Kdb , + ResourceType.Kdlb => ResourceTypeFlag.Kdlb, _ => 0, }; @@ -148,7 +162,7 @@ public static class ResourceExtensions => (type.ToFlag() & flags) != 0; public static ResourceCategoryFlag ToFlag(this ResourceCategory type) - => type switch + => (ResourceCategory)((uint) type & 0x00FFFFFF) switch { ResourceCategory.Common => ResourceCategoryFlag.Common, ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, From 246f4f65f5bf60a9c1ee8ecc7bc65075346c6229 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Jul 2024 18:42:48 +0200 Subject: [PATCH 1859/2451] Make items changed in a mod sort before other items for item swap, also color them. --- OtterGui | 2 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 10 ++-- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 65 ++++++++++++++--------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/OtterGui b/OtterGui index dc17161b..87a53262 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit dc17161b1d9c47ffd6bcc17e91f4832cf7762993 +Subproject commit 87a532620d622a00e60059b5dd42a04f0319b5b5 diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 1f4c5e7a..03abfc45 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -15,13 +15,9 @@ public static class ItemSwap public class InvalidItemTypeException : Exception { } - public class MissingFileException : Exception + public class MissingFileException(ResourceType type, object path) : Exception($"Could not load {type} File Data for \"{path}\".") { - public readonly ResourceType Type; - - public MissingFileException(ResourceType type, object path) - : base($"Could not load {type} File Data for \"{path}\".") - => Type = type; + public readonly ResourceType Type = type; } private static bool LoadFile(MetaFileManager manager, FullPath path, out byte[] data) @@ -47,7 +43,7 @@ public static class ItemSwap Penumbra.Log.Debug($"Could not load file {path}:\n{e}"); } - data = Array.Empty(); + data = []; return false; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6db4db5c..da2daeb7 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -22,6 +22,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; @@ -34,7 +35,8 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemData itemService, CollectionManager collectionManager, - ModManager modManager, ObjectIdentification identifier, MetaFileManager metaFileManager, Configuration config) + ModManager modManager, ModFileSystemSelector selector, ObjectIdentification identifier, MetaFileManager metaFileManager, + Configuration config) { _communicator = communicator; _collectionManager = collectionManager; @@ -46,15 +48,15 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, FullEquipType.Head), new ItemSelector(itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, FullEquipType.Body), new ItemSelector(itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, FullEquipType.Hands), new ItemSelector(itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, FullEquipType.Legs), new ItemSelector(itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, FullEquipType.Feet), new ItemSelector(itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, FullEquipType.Ears), new ItemSelector(itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, FullEquipType.Neck), new ItemSelector(itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, FullEquipType.Wrists), new ItemSelector(itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, FullEquipType.Finger), new ItemSelector(itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), // @formatter:on }; @@ -129,11 +131,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Weapon, } - private class ItemSelector(ItemData data, FullEquipType type) - : FilterComboCache(() => data.ByType[type], MouseWheelType.None, Penumbra.Log) + private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) + : FilterComboCache<(EquipItem Item, bool InMod)>(() => + { + var list = data.ByType[type]; + if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type)) + return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); + + return list.Select(i => (i, false)).ToList(); + }, MouseWheelType.None, Penumbra.Log) { - protected override string ToString(EquipItem obj) - => obj.Name; + protected override bool DrawSelectable(int globalIdx, bool selected) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value(), Items[globalIdx].InMod); + return base.DrawSelectable(globalIdx, selected); + } + + protected override string ToString((EquipItem Item, bool InMod) obj) + => obj.Item.Name; } private readonly Dictionary _selectors; @@ -186,17 +201,17 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Bracelet: case SwapType.Ring: var values = _selectors[_lastTab]; - if (values.Source.CurrentSelection.Type != FullEquipType.Unknown - && values.Target.CurrentSelection.Type != FullEquipType.Unknown) - _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, + if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown + && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); - if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection, _slotFrom, selectorFrom.CurrentSelection, + if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) + _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -455,7 +470,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -480,7 +495,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (_affectedItems is not { Length: > 1 }) return; @@ -489,7 +504,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) .Select(i => i.Name))); } @@ -521,7 +536,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) @@ -534,7 +549,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) { @@ -549,7 +564,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) .Select(i => i.Name))); } From 3ffe6151ffd05b732ac9df38cac17ba70b3a68ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:08:41 +0200 Subject: [PATCH 1860/2451] Add ToString for InternalEqpEntry. --- Penumbra/Meta/Manipulations/Eqp.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index ec4dd6e7..5d37aac8 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -72,4 +72,7 @@ public readonly record struct EqpEntryInternal(uint Value) var (offset, mask) = Eqp.OffsetAndMask(slot); return (uint)((ulong)(entry & mask) >> offset); } + + public override string ToString() + => Value.ToString("X8"); } From f143601aa00d8807fdf6fb016262afdc57cb00ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:08:58 +0200 Subject: [PATCH 1861/2451] Do not replace paths when mods are not enabled. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 10821287..3f055f64 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -17,15 +17,17 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; private readonly PapHandler _papHandler; + private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, Configuration config) { _resources = resources; _fileReadService = fileReadService; _texMdlService = texMdlService; + _config = config; ResetResolvePath(); _resources.ResourceRequested += ResourceHandler; @@ -39,7 +41,7 @@ public unsafe class ResourceLoader : IDisposable, IService private int PapResourceHandler(void* self, byte* path, int length) { - if (!Utf8GamePath.FromPointer(path, out var gamePath)) + if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, out var gamePath)) return length; var (resolvedPath, data) = _incMode.Value @@ -119,7 +121,7 @@ public unsafe class ResourceLoader : IDisposable, IService private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if (returnValue != null) + if (!_config.EnableMods || returnValue != null) return; CompareHash(ComputeHash(path.Path, parameters), hash, path); From 6f3d9eb272b7ad35fade8c951df8254285bb7fa5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:09:11 +0200 Subject: [PATCH 1862/2451] Fix bug in EquipmentSwap --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index b7827c47..2c292a14 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -186,7 +186,7 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, From cbf5baf65cef7450bbcb2be667d6fe06f69cb7f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:09:48 +0200 Subject: [PATCH 1863/2451] Remove Material Reassignment Tab from advanced editing due to being obsolete. --- .../AdvancedWindow/ModEditWindow.Materials.cs | 72 ++----------------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 - 2 files changed, 7 insertions(+), 66 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 0223ca6b..c3483c35 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -112,13 +111,13 @@ public partial class ModEditWindow } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) - { - ImGui.AlignTextToFramePadding(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); + using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); } if (unfolded) @@ -189,61 +188,4 @@ public partial class ModEditWindow if (t) Widget.DrawHexViewer(file.AdditionalData); } - - private void DrawMaterialReassignmentTab() - { - if (_editor.Files.Mdl.Count == 0) - return; - - using var tab = ImRaii.TabItem("Material Reassignment"); - if (!tab) - return; - - ImGui.NewLine(); - MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); - - ImGui.NewLine(); - using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); - if (!child) - return; - - using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); - if (!table) - return; - - var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) - { - using var id = ImRaii.PushId(idx); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) - info.Save(_editor.Compactor); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true)) - info.Restore(); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(400 * UiHelpers.Scale); - var tmp = info.CurrentMaterials[0]; - if (ImGui.InputText("##0", ref tmp, 64)) - info.SetMaterial(tmp, 0); - - for (var i = 1; i < info.Count; ++i) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(400 * UiHelpers.Scale); - tmp = info.CurrentMaterials[i]; - if (ImGui.InputText($"##{i}", ref tmp, 64)) - info.SetMaterial(tmp, i); - } - } - } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e915a879..79a8ae34 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,7 +179,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); - DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); From a1a7487897aeb8980e82db90b457873c95ae1f49 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:11:20 +0200 Subject: [PATCH 1864/2451] Remove Update Bibo Button. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f81b2831..f8874f89 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -111,27 +111,6 @@ public class ModPanelEditTab( MoveDirectory.Draw(modManager, _mod, buttonSize); UiHelpers.DefaultLineSpace(); - DrawUpdateBibo(buttonSize); - - UiHelpers.DefaultLineSpace(); - } - - private void DrawUpdateBibo(Vector2 buttonSize) - { - if (ImGui.Button("Update Bibo Material", buttonSize)) - { - editor.LoadMod(_mod); - editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); - editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); - editor.MdlMaterialEditor.SaveAllModels(editor.Compactor); - editWindow.UpdateModels(); - } - - ImGuiUtil.HoverTooltip( - "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" - + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" - + "Use this for outdated mods made for old Bibo bodies.\n" - + "Go to Advanced Editing for more fine-tuned control over material assignment."); } private void BackupButtons(Vector2 buttonSize) From 19166d8cf4951dafe62766832f7bba00ea25971c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:11:52 +0200 Subject: [PATCH 1865/2451] Add rudimentary start for knowledge window. --- Penumbra/CommandHandler.cs | 20 ++++++--- Penumbra/UI/Knowledge/IKnowledgeTab.cs | 8 ++++ Penumbra/UI/Knowledge/KnowledgeWindow.cs | 55 ++++++++++++++++++++++++ Penumbra/UI/Knowledge/RaceCodeTab.cs | 42 ++++++++++++++++++ Penumbra/UI/WindowSystem.cs | 17 +++++--- 5 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 Penumbra/UI/Knowledge/IKnowledgeTab.cs create mode 100644 Penumbra/UI/Knowledge/KnowledgeWindow.cs create mode 100644 Penumbra/UI/Knowledge/RaceCodeTab.cs diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 484dd954..db8d9aca 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -12,6 +12,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI; +using Penumbra.UI.Knowledge; namespace Penumbra; @@ -29,11 +30,12 @@ public class CommandHandler : IDisposable, IApiService private readonly CollectionManager _collectionManager; private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; + private readonly KnowledgeWindow _knowledgeWindow; public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, - Configuration config, - ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra, - CollectionEditor collectionEditor) + Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, + Penumbra penumbra, + CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow) { _commandManager = commandManager; _redrawService = redrawService; @@ -45,6 +47,7 @@ public class CommandHandler : IDisposable, IApiService _chat = chat; _penumbra = penumbra; _collectionEditor = collectionEditor; + _knowledgeWindow = knowledgeWindow; framework.RunOnFrameworkThread(() => { if (_commandManager.Commands.ContainsKey(CommandName)) @@ -69,7 +72,7 @@ public class CommandHandler : IDisposable, IApiService var argumentList = arguments.Split(' ', 2); arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty; - var _ = argumentList[0].ToLowerInvariant() switch + _ = argumentList[0].ToLowerInvariant() switch { "window" => ToggleWindow(arguments), "enable" => SetPenumbraState(arguments, true), @@ -83,6 +86,7 @@ public class CommandHandler : IDisposable, IApiService "collection" => SetCollection(arguments), "mod" => SetMod(arguments), "bulktag" => SetTag(arguments), + "knowledge" => HandleKnowledge(arguments), _ => PrintHelp(argumentList[0]), }; } @@ -304,7 +308,7 @@ public class CommandHandler : IDisposable, IApiService identifiers = _actors.FromUserString(split[2], false); } } - catch (ActorManager.IdentifierParseError e) + catch (ActorIdentifierFactory.IdentifierParseError e) { _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true) .AddText($" could not be converted to an identifier. {e.Message}") @@ -619,4 +623,10 @@ public class CommandHandler : IDisposable, IApiService if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text()); } + + private bool HandleKnowledge(string arguments) + { + _knowledgeWindow.Toggle(); + return true; + } } diff --git a/Penumbra/UI/Knowledge/IKnowledgeTab.cs b/Penumbra/UI/Knowledge/IKnowledgeTab.cs new file mode 100644 index 00000000..568d5fda --- /dev/null +++ b/Penumbra/UI/Knowledge/IKnowledgeTab.cs @@ -0,0 +1,8 @@ +namespace Penumbra.UI.Knowledge; + +public interface IKnowledgeTab +{ + public ReadOnlySpan Name { get; } + public ReadOnlySpan SearchTags { get; } + public void Draw(); +} diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs new file mode 100644 index 00000000..de1b36b8 --- /dev/null +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -0,0 +1,55 @@ +using System.Text.Unicode; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Memory; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; + +namespace Penumbra.UI.Knowledge; + +/// Draw the progress information for import. +public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUiService +{ + private readonly IReadOnlyList _tabs = + [ + new RaceCodeTab(), + ]; + + private IKnowledgeTab? _selected; + private readonly byte[] _filterStore = new byte[256]; + + private TerminatedByteString _filter = TerminatedByteString.Empty; + + public override void Draw() + { + DrawSelector(); + ImUtf8.SameLineInner(); + DrawMain(); + } + + private void DrawSelector() + { + using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(250 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); + if (!child) + return; + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImUtf8.InputText("##Filter"u8, _filterStore, out _filter, "Filter..."u8); + + foreach (var tab in _tabs) + { + if (ImUtf8.Selectable(tab.Name, _selected == tab)) + _selected = tab; + } + } + + private void DrawMain() + { + using var child = ImUtf8.Child("KnowledgeMain"u8, ImGui.GetContentRegionAvail(), true); + if (!child || _selected == null) + return; + + _selected.Draw(); + } +} diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs new file mode 100644 index 00000000..988506dd --- /dev/null +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -0,0 +1,42 @@ +using ImGuiNET; +using OtterGui.Text; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI.Knowledge; + +public sealed class RaceCodeTab : IKnowledgeTab +{ + public ReadOnlySpan Name + => "Race Codes"u8; + + public ReadOnlySpan SearchTags + => "deformersracecodesmodel"u8; + + public void Draw() + { + using var table = ImUtf8.Table("table"u8, 4, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableHeader("Race Code"u8); + ImUtf8.TableHeader("Race"u8); + ImUtf8.TableHeader("Gender"u8); + ImUtf8.TableHeader("NPC"u8); + + foreach (var genderRace in Enum.GetValues()) + { + ImGui.TableNextColumn(); + ImUtf8.Text(genderRace.ToRaceCode()); + + var (gender, race) = genderRace.Split(); + ImGui.TableNextColumn(); + ImUtf8.Text($"{race}"); + + ImGui.TableNextColumn(); + ImUtf8.Text($"{gender}"); + + ImGui.TableNextColumn(); + ImUtf8.Text(((ushort)genderRace & 0xF) != 1 ? "NPC"u8 : "Normal"u8); + } + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 72ac0d01..6d382ad4 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using OtterGui.Services; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; @@ -14,20 +15,24 @@ public class PenumbraWindowSystem : IDisposable, IUiService private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, - LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab) + LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, + KnowledgeWindow knowledgeWindow) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + KnowledgeWindow = knowledgeWindow; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); _windowSystem.AddWindow(importPopup); _windowSystem.AddWindow(debugTab); + _windowSystem.AddWindow(KnowledgeWindow); _uiBuilder.OpenMainUi += Window.Toggle; _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; From d0c4d6984cbd8e40db28e47398a309b71382f6ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 12:57:59 +0200 Subject: [PATCH 1866/2451] Dispose collection caches on plugin disposal. --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 80d4cf1d..a3b6bb83 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -84,6 +84,12 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + + foreach (var collection in _storage) + { + collection._cache?.Dispose(); + collection._cache = null; + } } public void AddChange(CollectionCache.ChangeData data) From bb4665c367a876d7bff704794651bd673d9c2978 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 12:58:17 +0200 Subject: [PATCH 1867/2451] Remove unused params. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f8874f89..90d8fb74 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -11,7 +11,6 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; using Penumbra.UI.ModsTab.Groups; @@ -22,8 +21,6 @@ public class ModPanelEditTab( ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, - ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, From 4806f8dc3eeea497ce95f52b30ce7b12bf12dc82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 13:43:01 +0200 Subject: [PATCH 1868/2451] Do not force loaded game paths to lowercase. --- Penumbra/Mods/SubMods/SubMod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a8c37369..f6b1be96 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } From af0bbeb8bfd0c093716f110e77442a60b69543f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 13:48:11 +0200 Subject: [PATCH 1869/2451] Force saving to be synchronous. --- Penumbra/Mods/TemporaryMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e1cf9f2b..e4049482 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -96,7 +96,7 @@ public class TemporaryMod : IMod var manips = new MetaDictionary(collection.MetaCache); defaultMod.Manipulations.UnionWith(manips); - saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); From 5d50523c72ca2ccdb41392b285711315e90ea998 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Jul 2024 12:12:56 +0000 Subject: [PATCH 1870/2451] [CI] Updating repo.json for testing_1.2.0.15 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f1108cfe..7bcc18b6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.14", + "TestingAssemblyVersion": "1.2.0.15", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.14/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.15/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d30d418afe571b8bde490f1566ee421955757c10 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 23:56:11 +0200 Subject: [PATCH 1871/2451] Remove a ToLower when resolving paths. --- Penumbra/Interop/PathResolving/PathResolver.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 49035dc8..67ec4fc3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,7 +52,6 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); - path = path.ToLower(); return category switch { // Only Interface collection. From e52b027545e01786dfce4754fe345fabcdae91b0 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Jul 2024 22:03:19 +0000 Subject: [PATCH 1872/2451] [CI] Updating repo.json for testing_1.2.0.16 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7bcc18b6..9c9e41db 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.15", + "TestingAssemblyVersion": "1.2.0.16", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.15/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.16/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 3754f5713224024aa778456d3ecc7d39b5b483a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 09:27:00 +0200 Subject: [PATCH 1873/2451] Reinstate spacebar heating --- .../AdvancedWindow/ModEditWindow.Materials.cs | 58 +++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + 2 files changed, 59 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index c3483c35..5a8fb13a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -188,4 +189,61 @@ public partial class ModEditWindow if (t) Widget.DrawHexViewer(file.AdditionalData); } + + private void DrawMaterialReassignmentTab() + { + if (_editor.Files.Mdl.Count == 0) + return; + + using var tab = ImRaii.TabItem("Material Reassignment"); + if (!tab) + return; + + ImGui.NewLine(); + MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); + + ImGui.NewLine(); + using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + if (!child) + return; + + using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) + return; + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) + { + using var id = ImRaii.PushId(idx); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) + info.Save(_editor.Compactor); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true)) + info.Restore(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + var tmp = info.CurrentMaterials[0]; + if (ImGui.InputText("##0", ref tmp, 64)) + info.SetMaterial(tmp, 0); + + for (var i = 1; i < info.Count; ++i) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + tmp = info.CurrentMaterials[i]; + if (ImGui.InputText($"##{i}", ref tmp, 64)) + info.SetMaterial(tmp, i); + } + } + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 79a8ae34..e915a879 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,6 +179,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); + DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); From 5512e0cad2b273caca8f1b10b82a72be2274be80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 09:43:07 +0200 Subject: [PATCH 1874/2451] Revert no-lowercasing for the moment. --- Penumbra/Interop/PathResolving/PathResolver.cs | 1 + Penumbra/Mods/SubMods/SubMod.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 67ec4fc3..49035dc8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,6 +52,7 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); + path = path.ToLower(); return category switch { // Only Interface collection. diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index f6b1be96..a8c37369 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p)) + if (Utf8GamePath.FromString(property.Name, out var p, true)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p)) + if (Utf8GamePath.FromString(property.Name, out var p, true)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } From ee801b637eb6a7b1c2092d0b5e54c532b20a462a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 29 Jul 2024 07:46:20 +0000 Subject: [PATCH 1875/2451] [CI] Updating repo.json for testing_1.2.0.17 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9c9e41db..adf152b0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.16", + "TestingAssemblyVersion": "1.2.0.17", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.16/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.17/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 7ceaeb826f88fc08f35f8e85cba7488a2644453a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 18:25:30 +0200 Subject: [PATCH 1876/2451] Move Material Reassignment to the back (and shoot it) --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e915a879..b0e9af7f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,7 +179,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); - DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); @@ -192,6 +191,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService } DrawMissingFilesTab(); + DrawMaterialReassignmentTab(); } /// A row of three buttonSizes and a help marker that can be used for material suffix changing. From 8518240bf919a7499953dad8b399114bfa10e1ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 18:25:41 +0200 Subject: [PATCH 1877/2451] Improve knowledge window somewhat. --- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 37 +++++++++++-- Penumbra/UI/Knowledge/RaceCodeTab.cs | 70 +++++++++++++++++++----- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index de1b36b8..b14949de 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -5,11 +5,12 @@ using Dalamud.Memory; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; +using Penumbra.String; namespace Penumbra.UI.Knowledge; /// Draw the progress information for import. -public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUiService +public sealed class KnowledgeWindow : Window, IUiService { private readonly IReadOnlyList _tabs = [ @@ -19,7 +20,16 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private IKnowledgeTab? _selected; private readonly byte[] _filterStore = new byte[256]; - private TerminatedByteString _filter = TerminatedByteString.Empty; + private ByteString _lower = ByteString.Empty; + + /// Draw the progress information for import. + public KnowledgeWindow() + : base("Penumbra Knowledge Window") + => SizeConstraints = new WindowSizeConstraints + { + MaximumSize = new Vector2(10000, 10000), + MinimumSize = new Vector2(400, 200), + }; public override void Draw() { @@ -30,15 +40,23 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private void DrawSelector() { - using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(250 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); + using var group = ImUtf8.Group(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##Filter"u8, _filterStore, out TerminatedByteString filter, "Filter..."u8)) + _lower = ByteString.FromSpanUnsafe(filter, true, null, null).AsciiToLowerClone(); + } + + using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); if (!child) return; - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - ImUtf8.InputText("##Filter"u8, _filterStore, out _filter, "Filter..."u8); - foreach (var tab in _tabs) { + if (!_lower.IsEmpty && tab.SearchTags.IndexOf(_lower.Span) < 0) + continue; + if (ImUtf8.Selectable(tab.Name, _selected == tab)) _selected = tab; } @@ -46,6 +64,13 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private void DrawMain() { + using var group = ImUtf8.Group(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImUtf8.TextFramed(_selected == null ? "No Selection"u8 : _selected.Name, ImGui.GetColorU32(ImGuiCol.FrameBg), + new Vector2(ImGui.GetContentRegionAvail().X, 0)); + } + using var child = ImUtf8.Child("KnowledgeMain"u8, ImGui.GetContentRegionAvail(), true); if (!child || _selected == null) return; diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs index 988506dd..36b048aa 100644 --- a/Penumbra/UI/Knowledge/RaceCodeTab.cs +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -4,7 +4,7 @@ using Penumbra.GameData.Enums; namespace Penumbra.UI.Knowledge; -public sealed class RaceCodeTab : IKnowledgeTab +public sealed class RaceCodeTab() : IKnowledgeTab { public ReadOnlySpan Name => "Race Codes"u8; @@ -14,29 +14,69 @@ public sealed class RaceCodeTab : IKnowledgeTab public void Draw() { - using var table = ImUtf8.Table("table"u8, 4, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; + var size = new Vector2((ImGui.GetContentRegionAvail().X - ImUtf8.ItemSpacing.X) / 2, 0); + using (var table = ImUtf8.Table("adults"u8, 4, ImGuiTableFlags.BordersOuter, size)) + { + if (!table) + return; - ImUtf8.TableHeader("Race Code"u8); - ImUtf8.TableHeader("Race"u8); - ImUtf8.TableHeader("Gender"u8); - ImUtf8.TableHeader("NPC"u8); + DrawHeaders(); + foreach (var gr in Enum.GetValues()) + { + var (gender, race) = gr.Split(); + if (gender is not Gender.Male and not Gender.Female || race is ModelRace.Unknown) + continue; - foreach (var genderRace in Enum.GetValues()) + DrawRow(gender, race, false); + } + } + + ImGui.SameLine(); + + using (var table = ImUtf8.Table("children"u8, 4, ImGuiTableFlags.BordersOuter, size)) + { + if (!table) + return; + + DrawHeaders(); + foreach (var race in (ReadOnlySpan) + [ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown]) + { + foreach (var gender in (ReadOnlySpan) [Gender.Male, Gender.Female]) + DrawRow(gender, race, true); + } + } + + return; + + static void DrawHeaders() { ImGui.TableNextColumn(); - ImUtf8.Text(genderRace.ToRaceCode()); - - var (gender, race) = genderRace.Split(); + ImUtf8.TableHeader("Race"u8); ImGui.TableNextColumn(); - ImUtf8.Text($"{race}"); + ImUtf8.TableHeader("Gender"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Age"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Race Code"u8); + } + + static void DrawRow(Gender gender, ModelRace race, bool child) + { + var gr = child + ? Names.CombinedRace(gender is Gender.Male ? Gender.MaleNpc : Gender.FemaleNpc, race) + : Names.CombinedRace(gender, race); + ImGui.TableNextColumn(); + ImUtf8.Text(race.ToName()); ImGui.TableNextColumn(); - ImUtf8.Text($"{gender}"); + ImUtf8.Text(gender.ToName()); ImGui.TableNextColumn(); - ImUtf8.Text(((ushort)genderRace & 0xF) != 1 ? "NPC"u8 : "Normal"u8); + ImUtf8.Text(child ? "Child"u8 : "Adult"u8); + + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(gr.ToRaceCode()); } } } From 5270ad4d0d8637c9c6ff5d3ed715e486236c9a1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 23:00:03 +0200 Subject: [PATCH 1878/2451] Update ImageSharp --- Penumbra/Penumbra.csproj | 2 +- Penumbra/packages.lock.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 70208737..24ffe469 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -79,7 +79,7 @@ - + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 42539e78..8e7106dd 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -44,9 +44,9 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.4, )", - "resolved": "3.1.4", - "contentHash": "lFIdxgGDA5iYkUMRFOze7BGLcdpoLFbR+a20kc1W7NepvzU7ejtxtWOg9RvgG7kb9tBoJ3ONYOK6kLil/dgF1w==" + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "JetBrains.Annotations": { "type": "Transitive", From 70281c576e7b16ac5b7551e53472e625b7f44a91 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:34:26 +0200 Subject: [PATCH 1879/2451] Update Submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 87a53262..33ffd7cb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 87a532620d622a00e60059b5dd42a04f0319b5b5 +Subproject commit 33ffd7cba3e487e98e55adca1677354078089943 diff --git a/Penumbra.GameData b/Penumbra.GameData index f5a74c70..d8ebd63c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f5a74c70ad3861c5c66e1df6ae9a29fc7a0d736a +Subproject commit d8ebd63cec1ac12ea547fd37b6c32bdf9b3f57d1 diff --git a/Penumbra.String b/Penumbra.String index f04abbab..91f0f211 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit f04abbabedf5e757c5cbb970f3e513fef23e53cf +Subproject commit 91f0f21137c61bd39281debf88a8ecc494043330 From 9d128a4d831849c791dbce8efa9dbcda4c75f75f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:38:25 +0200 Subject: [PATCH 1880/2451] Fix potential threading issue on launch. --- Penumbra/Interop/PathResolving/DrawObjectState.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 2a9ec7a9..5e413fe2 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -22,18 +23,19 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, - CharacterBaseDestructor characterBaseDestructor, GameState gameState) + CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework) { _objects = objects; _createCharacterBase = createCharacterBase; _weaponReload = weaponReload; _characterBaseDestructor = characterBaseDestructor; _gameState = gameState; + framework.RunOnFrameworkThread(InitializeDrawObjects); + _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); - InitializeDrawObjects(); } public bool ContainsKey(nint key) @@ -94,8 +96,8 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary private unsafe void InitializeDrawObjects() { - foreach(var actor in _objects) - { + foreach (var actor in _objects) + { if (actor is { IsCharacter: true, Model.Valid: true }) IterateDrawObjectTree((Object*)actor.Model.Address, actor, false, false); } From d247f83e1db8bf59ea647fdae3e9a22fb4996015 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:53:55 +0200 Subject: [PATCH 1881/2451] Use CiByteString for anything path-related. --- Penumbra/Api/Api/ResolveApi.cs | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 2 +- Penumbra/Api/DalamudSubstitutionProvider.cs | 3 +- Penumbra/Collections/Cache/ImcCache.cs | 6 ++-- Penumbra/Enums/ResourceTypeFlag.cs | 6 ++-- .../PostProcessing/PreBoneDeformerReplacer.cs | 3 +- .../Hooks/ResourceLoading/CreateFileWHook.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 21 +++++++------- .../Hooks/ResourceLoading/ResourceService.cs | 8 +++--- .../Interop/MaterialPreview/MaterialInfo.cs | 7 +++-- .../Interop/PathResolving/PathDataHandler.cs | 10 +++---- .../Interop/PathResolving/PathResolver.cs | 1 - Penumbra/Interop/PathResolving/PathState.cs | 2 +- .../Processing/AvfxPathPreProcessor.cs | 2 +- .../Processing/FilePostProcessService.cs | 4 +-- .../Processing/GamePathPreProcessService.cs | 4 +-- .../Processing/ImcFilePostProcessor.cs | 2 +- .../Interop/Processing/ImcPathPreProcessor.cs | 2 +- .../Processing/MaterialFilePostProcessor.cs | 2 +- .../Processing/MtrlPathPreProcessor.cs | 2 +- .../Interop/Processing/TmbPathPreProcessor.cs | 2 +- .../ResolveContext.PathResolution.cs | 9 +++--- .../Interop/ResourceTree/ResolveContext.cs | 20 ++++++------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 12 ++++---- Penumbra/Interop/Services/DecalReverter.cs | 5 ++-- Penumbra/Interop/Structs/ResourceHandle.cs | 4 +-- Penumbra/Interop/Structs/StructExtensions.cs | 24 ++++++++-------- Penumbra/Mods/ModCreator.cs | 4 +-- Penumbra/Mods/SubMods/SubMod.cs | 4 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 5 ++-- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 8 +++--- .../ModEditWindow.Materials.MtrlTab.cs | 4 +-- .../ModEditWindow.Models.MdlTab.cs | 2 +- .../ModEditWindow.ShaderPackages.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 28 ++++++++++++------- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 2 -- Penumbra/UI/ResourceWatcher/Record.cs | 18 ++++++------ .../UI/ResourceWatcher/ResourceWatcher.cs | 4 +-- .../ResourceWatcher/ResourceWatcherTable.cs | 4 +-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 28 +++++++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 5 ++-- 42 files changed, 163 insertions(+), 124 deletions(-) diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs index ec57eba7..481ea7ad 100644 --- a/Penumbra/Api/Api/ResolveApi.cs +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -94,7 +94,7 @@ public class ResolveApi( if (!config.EnableMods) return path; - var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; var ret = collection.ResolvePath(gamePath); return ret?.ToString() ?? path; } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 0894a8e5..f02b0d94 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -137,7 +137,7 @@ public class TemporaryApi( paths = new Dictionary(redirections.Count); foreach (var (gString, fString) in redirections) { - if (!Utf8GamePath.FromString(gString, out var path, false)) + if (!Utf8GamePath.FromString(gString, out var path)) { paths = null; return false; diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 6347447a..e10dc461 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -4,7 +4,6 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.String.Classes; @@ -130,7 +129,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService try { - if (!Utf8GamePath.FromString(path, out var utf8Path, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path)) return; var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path); diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 40c3d2c7..cac52f99 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -8,12 +8,12 @@ namespace Penumbra.Collections.Cache; public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary)> _imcFiles = []; + private readonly Dictionary)> _imcFiles = []; - public bool HasFile(ByteString path) + public bool HasFile(CiByteString path) => _imcFiles.ContainsKey(path); - public bool GetFile(ByteString path, [NotNullWhen(true)] out ImcFile? file) + public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file) { if (!_imcFiles.TryGetValue(path, out var p)) { diff --git a/Penumbra/Enums/ResourceTypeFlag.cs b/Penumbra/Enums/ResourceTypeFlag.cs index 461e7ac1..920e9780 100644 --- a/Penumbra/Enums/ResourceTypeFlag.cs +++ b/Penumbra/Enums/ResourceTypeFlag.cs @@ -216,10 +216,10 @@ public static class ResourceExtensions }; } - public static ResourceType Type(ByteString path) + public static ResourceType Type(CiByteString path) { var extIdx = path.LastIndexOf((byte)'.'); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? CiByteString.Empty : path.Substring(extIdx + 1); return ext.Length switch { @@ -231,7 +231,7 @@ public static class ResourceExtensions }; } - public static ResourceCategory Category(ByteString path) + public static ResourceCategory Category(CiByteString path) { if (path.Length < 3) return ResourceCategory.Debug; diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 903484ea..834a7d28 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -7,6 +7,7 @@ using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; +using Penumbra.String; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -15,7 +16,7 @@ namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { public static readonly Utf8GamePath PreBoneDeformerPath = - Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; // Approximate name guesses. private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index a8ac0608..8d0ac8cb 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -102,7 +102,7 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService { // Use static storage. var ptr = WriteFileName(name); - Penumbra.Log.Excessive($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}."); + Penumbra.Log.Excessive($"[ResourceHooks] Calling CreateFileWDetour with {CiByteString.FromSpanUnsafe(name, false)}."); return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 3f055f64..bcd09b37 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -41,7 +41,7 @@ public unsafe class ResourceLoader : IDisposable, IService private int PapResourceHandler(void* self, byte* path, int length) { - if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, out var gamePath)) + if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) return length; var (resolvedPath, data) = _incMode.Value @@ -64,7 +64,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Load a resource for a given path and a specific collection. - public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { _resolvedData = resolveData; var ret = _resources.GetResource(category, type, path); @@ -73,7 +73,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Load a resource for a given path and a specific collection. - public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { _resolvedData = resolveData; var ret = _resources.GetSafeResource(category, type, path); @@ -98,7 +98,7 @@ public unsafe class ResourceLoader : IDisposable, IService /// public event ResourceLoadedDelegate? ResourceLoaded; - public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + public delegate void FileLoadedDelegate(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, ReadOnlySpan additionalData); /// @@ -172,7 +172,8 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); + var path = CiByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, + gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); @@ -184,7 +185,7 @@ public unsafe class ResourceLoader : IDisposable, IService /// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, + private byte DefaultLoadResource(CiByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, bool isSync, ReadOnlySpan additionalData) { if (Utf8GamePath.IsRooted(gamePath)) @@ -265,7 +266,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Compute the CRC32 hash for a given path together with potential resource parameters. - private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) + private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams) { if (pGetResParams == null || !pGetResParams->IsPartialRead) return path.Crc32; @@ -273,11 +274,11 @@ public unsafe class ResourceLoader : IDisposable, IService // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - return ByteString.Join( + return CiByteString.Join( (byte)'.', path, - ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), - ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) + CiByteString.FromString(pGetResParams->SegmentOffset.ToString("x"), out var s1, MetaDataComputation.None) ? s1 : CiByteString.Empty, + CiByteString.FromString(pGetResParams->SegmentLength.ToString("x"), out var s2, MetaDataComputation.None) ? s2 : CiByteString.Empty ).Crc32; } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 0b00452b..8b99dc37 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -39,14 +39,14 @@ public unsafe class ResourceService : IDisposable, IRequiredService } } - public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path) + public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, CiByteString path) { var hash = path.Crc32; return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress, &category, &type, &hash, path.Path, null, false); } - public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, ByteString path) + public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, CiByteString path) => new((CSResourceHandle*)GetResource(category, type, path), false); public void Dispose() @@ -102,7 +102,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) { using var performance = _performance.Measure(PerformanceType.GetResourceHandler); - if (!Utf8GamePath.FromPointer(path, out var gamePath)) + if (!Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) { Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path."); return isSync @@ -120,7 +120,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService } /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path, + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, GetResourceParameters* resourceParameters = null, bool unk = false) => sync ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f7e6caf0..f2ea2d6c 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -49,7 +49,10 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy public static unsafe List FindMaterials(IEnumerable gameObjects, string materialPath) { - var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; + var needle = CiByteString.FromString(materialPath.Replace('\\', '/'), out var m, + MetaDataComputation.CiCrc32 | MetaDataComputation.Crc32) + ? m + : CiByteString.Empty; var result = new List(Enum.GetValues().Length); foreach (var objectPtr in gameObjects) @@ -83,7 +86,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); - var fileName = ByteString.FromSpanUnsafe(path, true); + var fileName = CiByteString.FromSpanUnsafe(path, true); if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); } diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index a8be97c8..9410ff98 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -31,27 +31,27 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateImc(ByteString path, ModCollection collection) + public static FullPath CreateImc(CiByteString path, ModCollection collection) => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateTmb(ByteString path, ModCollection collection) + public static FullPath CreateTmb(CiByteString path, ModCollection collection) => CreateBase(path, collection); /// Create the encoding path for an AVFX file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateAvfx(ByteString path, ModCollection collection) + public static FullPath CreateAvfx(CiByteString path, ModCollection collection) => CreateBase(path, collection); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateMtrl(ByteString path, ModCollection collection, Utf8GamePath originalPath) + public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static FullPath CreateBase(ByteString path, ModCollection collection) + private static FullPath CreateBase(CiByteString path, ModCollection collection) => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 49035dc8..67ec4fc3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,7 +52,6 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); - path = path.ToLower(); return category switch { // Only Interface collection. diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index bf9d1e25..60a61408 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -28,7 +28,7 @@ public sealed class PathState(CollectionResolver collectionResolver, MetaState m _internalResolve.Dispose(); } - public bool Consume(ByteString _, out ResolveData collection) + public bool Consume(CiByteString _, out ResolveData collection) { if (_resolveData.IsValueCreated) { diff --git a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs index 56f693e6..2194354a 100644 --- a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class AvfxPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Avfx; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateAvfx(path, resolveData.ModCollection) : resolved; } diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index bba53c94..ecf78c69 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Processing; public interface IFilePostProcessor : IService { public ResourceType Type { get; } - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData); } public unsafe class FilePostProcessService : IRequiredService, IDisposable @@ -30,7 +30,7 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable _resourceLoader.FileLoaded -= OnFileLoaded; } - private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, ReadOnlySpan additionalData) { if (_processors.TryGetValue(resource->FileType, out var processor)) diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs index 004b7168..65608ba0 100644 --- a/Penumbra/Interop/Processing/GamePathPreProcessService.cs +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -11,7 +11,7 @@ public interface IPathPreProcessor : IService { public ResourceType Type { get; } - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); } public class GamePathPreProcessService : IService @@ -24,7 +24,7 @@ public class GamePathPreProcessService : IService } - public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, + public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, FullPath? resolved, Utf8GamePath originalPath) { diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 4a0ebe22..33a3941a 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -11,7 +11,7 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP public ResourceType Type => ResourceType.Imc; - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) { if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) return; diff --git a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs index 907d7587..7030dd8d 100644 --- a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs @@ -11,7 +11,7 @@ public sealed class ImcPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Imc; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) => resolveData.ModCollection.MetaCache?.Imc.HasFile(originalGamePath.Path) ?? false ? PathDataHandler.CreateImc(path, resolveData.ModCollection) : resolved; diff --git a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs index 02b5d46c..26956845 100644 --- a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs @@ -10,7 +10,7 @@ public sealed class MaterialFilePostProcessor //: IFilePostProcessor public ResourceType Type => ResourceType.Mtrl; - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) { if (!PathDataHandler.ReadMtrl(additionalData, out var data)) return; diff --git a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs index 8fb2400b..603781ed 100644 --- a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class MtrlPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Mtrl; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalGamePath) : resolved; } diff --git a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs index dd887819..0a7aa16f 100644 --- a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class TmbPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Tmb; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateTmb(path, resolveData.ModCollection) : resolved; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 678dd8a9..85b3284a 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; @@ -99,7 +98,7 @@ internal partial record ResolveContext Span pathBuffer = stackalloc byte[260]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } [SkipLocalsInit] @@ -133,7 +132,7 @@ internal partial record ResolveContext if (weaponPosition >= 0) WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } } @@ -148,7 +147,7 @@ internal partial record ResolveContext Span pathBuffer = stackalloc byte[260]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant) @@ -196,7 +195,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialPathNative(byte* mtrlFileName) { - ByteString? path; + CiByteString? path; try { path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index acb320d4..3fc1ae3c 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -45,25 +45,25 @@ internal unsafe partial record ResolveContext( public CharacterBase* CharacterBase => CharacterBasePointer.Value; - private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private ModelType ModelType => CharacterBase->GetModelType(); - private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) + private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) { if (resourceHandle == null) return null; if (gamePath.IsEmpty) return null; - if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) + if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path)) return null; return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } [SkipLocalsInit] - private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) + private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11) { if (resourceHandle == null) return null; @@ -81,7 +81,7 @@ internal unsafe partial record ResolveContext( prefixed[lastDirectorySeparator + 2] = (byte)'-'; gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); - if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp)) + if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; path = tmp.Clone(); @@ -118,11 +118,11 @@ internal unsafe partial record ResolveContext( throw new ArgumentNullException(nameof(resourceHandle)); var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); - var additionalData = ByteString.Empty; + var additionalData = CiByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) - additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); + additionalData = CiByteString.FromSpanUnsafe(data, false).Clone(); - var fullPath = Utf8GamePath.FromSpan(fileName, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; + var fullPath = Utf8GamePath.FromSpan(fileName, MetaDataComputation.None, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this) { @@ -222,7 +222,7 @@ internal unsafe partial record ResolveContext( return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); if (shpkNode != null) { if (Global.WithUiData) @@ -236,7 +236,7 @@ internal unsafe partial record ResolveContext( var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i)), resource->Textures[i].IsDX11); if (texNode == null) continue; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 9c911791..de43a874 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -15,7 +15,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; - public ByteString AdditionalData; + public CiByteString AdditionalData; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -26,9 +26,9 @@ public class ResourceNode : ICloneable set { if (value.IsEmpty) - PossibleGamePaths = Array.Empty(); + PossibleGamePaths = []; else - PossibleGamePaths = new[] { value }; + PossibleGamePaths = [value]; } } @@ -40,8 +40,8 @@ public class ResourceNode : ICloneable Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; - PossibleGamePaths = Array.Empty(); - AdditionalData = ByteString.Empty; + PossibleGamePaths = []; + AdditionalData = CiByteString.Empty; Length = length; Children = new List(); ResolveContext = resolveContext; @@ -90,7 +90,7 @@ public class ResourceNode : ICloneable public readonly record struct UiData(string? Name, ChangedItemIcon Icon) { - public readonly UiData PrependName(string prefix) + public UiData PrependName(string prefix) => Name == null ? this : new UiData(prefix + Name, Icon); } } diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 21b51fd2..3d5d7845 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; @@ -9,10 +10,10 @@ namespace Penumbra.Interop.Services; public sealed unsafe class DecalReverter : IDisposable { public static readonly Utf8GamePath DecalPath = - Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; public static readonly Utf8GamePath TransparentPath = - Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; private readonly CharacterUtility _utility; private readonly Structs.TextureResourceHandle* _decal; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 6e428f25..65550563 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -47,11 +47,11 @@ public unsafe struct ResourceHandle public ulong DataLength; } - public readonly ByteString FileName() + public readonly CiByteString FileName() => CsHandle.FileName.AsByteString(); public readonly bool GamePath(out Utf8GamePath path) - => Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), out path); + => Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), MetaDataComputation.All, out path); [FieldOffset(0x00)] public CsHandle.ResourceHandle CsHandle; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index fc8b1c3d..9dd9a96d 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -6,48 +6,48 @@ namespace Penumbra.Interop.Structs; internal static class StructExtensions { - public static unsafe ByteString AsByteString(in this StdString str) - => ByteString.FromSpanUnsafe(str.AsSpan(), true); + public static CiByteString AsByteString(in this StdString str) + => CiByteString.FromSpanUnsafe(str.AsSpan(), true); - public static ByteString ResolveEidPathAsByteString(ref this CharacterBase character) + public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static ByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) + public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); } - public static ByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) + public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); } - public static unsafe ByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) + public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) { var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); } - public static ByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); } - public static ByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } - private static unsafe ByteString ToOwnedByteString(byte* str) - => str == null ? ByteString.Empty : new ByteString(str).Clone(); + private static unsafe CiByteString ToOwnedByteString(byte* str) + => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); - private static ByteString ToOwnedByteString(ReadOnlySpan str) - => str.Length == 0 ? ByteString.Empty : ByteString.FromSpanUnsafe(str, true).Clone(); + private static CiByteString ToOwnedByteString(ReadOnlySpan str) + => str.Length == 0 ? CiByteString.Empty : CiByteString.FromSpanUnsafe(str, true).Clone(); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 546f5f5c..0f4972e3 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -270,7 +270,7 @@ public partial class ModCreator( public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority) { var list = optionFolder.EnumerateNonHiddenFiles() - .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) + .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath), gamePath, new FullPath(f))) .Where(t => t.Item1); var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority); @@ -291,7 +291,7 @@ public partial class ModCreator( ReloadMod(mod, false, out _); foreach (var file in mod.FindUnusedFiles()) { - if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) + if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath)) mod.Default.Files.TryAdd(gamePath, file); } diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a8c37369..f6b1be96 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 2c6ac170..c783e17f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Compression; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.Mods.Editor; @@ -98,7 +99,7 @@ public class FileEditor( _inInput = ImGui.IsItemActive(); if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { - _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); + _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8); _quickImport = null; fileDialog.Reset(); try @@ -306,7 +307,7 @@ public class FileEditor( foreach (var (option, gamePath) in file.SubModUsage) { ImGui.TableNextColumn(); - UiHelpers.Text(gamePath.Path); + ImUtf8.Text(gamePath.Path.Span); ImGui.TableNextColumn(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); ImGui.TextUnformatted(option.GetFullName()); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 107c56e6..ffa7473d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -209,7 +209,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { - if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) + if (Utf8GamePath.FromString(_gamePathEdit, out var path)) _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; @@ -217,7 +217,7 @@ public partial class ModEditWindow } else if (_fileIdx == i && _pathIdx == j - && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + && (!Utf8GamePath.FromString(_gamePathEdit, out var path) || !path.IsEmpty && !path.Equals(gamePath) && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); @@ -241,7 +241,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { - if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) + if (Utf8GamePath.FromString(_gamePathEdit, out var path) && !path.IsEmpty) _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; @@ -249,7 +249,7 @@ public partial class ModEditWindow } else if (_fileIdx == i && _pathIdx == -1 - && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + && (!Utf8GamePath.FromString(_gamePathEdit, out var path) || !path.IsEmpty && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index cd7aca9d..a50599a1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -25,7 +25,7 @@ public partial class ModEditWindow { private const int ShpkPrefixLength = 16; - private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; @@ -77,7 +77,7 @@ public partial class ModEditWindow public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true)) + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; return _edit.FindBestMatch(defaultGamePath); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b05bcac2..b436448f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -271,7 +271,7 @@ public partial class ModEditWindow private byte[]? ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(path, out var utf8Path, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path)) throw new Exception($"Resolved path {path} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(utf8Path); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index a22c10ad..017478a7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); + private static readonly CiByteString DisassemblyLabel = CiByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); private readonly FileEditor _shaderPackageTab; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index b0e9af7f..0d3dce8c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -562,7 +562,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return new FullPath(path); } - private HashSet FindPathsStartingWith(ByteString prefix) + private HashSet FindPathsStartingWith(CiByteString prefix) { var ret = new HashSet(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 7315f136..c47414b9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -84,7 +84,8 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", + index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); @@ -149,7 +150,9 @@ public class ResourceTreeViewer var filterChanged = false; ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) + { filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + } var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; ImGui.SetNextItemWidth(fieldWidth); @@ -181,7 +184,8 @@ public class ResourceTreeViewer } }); - private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, + ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); @@ -196,9 +200,9 @@ public class ResourceTreeViewer return true; return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) @@ -226,10 +230,11 @@ public class ResourceTreeViewer visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); _filterCache.Add(nodePathHash, visibility); } + return visibility; } - string GetAdditionalDataSuffix(ByteString data) + string GetAdditionalDataSuffix(CiByteString data) => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) @@ -252,8 +257,9 @@ public class ResourceTreeViewer var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); - var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; + var hasVisibleChildren = resourceNode.Children.Any(child + => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); + var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); @@ -317,13 +323,15 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + ImGuiUtil.HoverTooltip( + $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); - ImGuiUtil.HoverTooltip($"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + ImGuiUtil.HoverTooltip( + $"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index b14949de..f831975b 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -1,7 +1,5 @@ -using System.Text.Unicode; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Memory; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 0fc51f26..b69d9944 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -18,8 +18,8 @@ public enum RecordType : byte internal unsafe struct Record { public DateTime Time; - public ByteString Path; - public ByteString OriginalPath; + public CiByteString Path; + public CiByteString OriginalPath; public string AssociatedGameObject; public ModCollection? Collection; public ResourceHandle* Handle; @@ -32,12 +32,12 @@ internal unsafe struct Record public OptionalBool CustomLoad; public LoadState LoadState; - public static Record CreateRequest(ByteString path, bool sync) + public static Record CreateRequest(CiByteString path, bool sync) => new() { Time = DateTime.UtcNow, Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = null, ResourceType = ResourceExtensions.Type(path).ToFlag(), @@ -51,7 +51,7 @@ internal unsafe struct Record LoadState = LoadState.None, }; - public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) + public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { path = path.IsOwned ? path : path.Clone(); return new Record @@ -73,7 +73,7 @@ internal unsafe struct Record }; } - public static Record CreateLoad(ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection, + public static Record CreateLoad(CiByteString path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, string associatedGameObject) => new() { @@ -100,7 +100,7 @@ internal unsafe struct Record { Time = DateTime.UtcNow, Path = path, - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -115,12 +115,12 @@ internal unsafe struct Record }; } - public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom) + public static Record CreateFileLoad(CiByteString path, ResourceHandle* handle, bool ret, bool custom) => new() { Time = DateTime.UtcNow, Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 14d69489..6f1ce9cf 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -163,7 +163,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService } } - private bool FilterMatch(ByteString path, out string match) + private bool FilterMatch(CiByteString path, out string match) { match = path.ToString(); return _logFilter.Length == 0 || (_logRegex?.IsMatch(match) ?? false) || match.Contains(_logFilter, StringComparison.OrdinalIgnoreCase); @@ -255,7 +255,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ReadOnlySpan _) + private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index b47574d0..33e301ae 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -50,7 +50,7 @@ internal sealed class ResourceWatcherTable : Table => DrawByteString(item.Path, 280 * UiHelpers.Scale); } - private static unsafe void DrawByteString(ByteString path, float length) + private static unsafe void DrawByteString(CiByteString path, float length) { Vector2 vec; ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); @@ -61,7 +61,7 @@ internal sealed class ResourceWatcherTable : Table else { var fileName = path.LastIndexOf((byte)'/'); - ByteString shortPath; + CiByteString shortPath; if (fileName != -1) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 4966dd64..a1e9da03 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -402,6 +403,33 @@ public class DebugTab : Window, ITab, IUiService } } } + + using (var tree = ImUtf8.TreeNode("String Memory"u8)) + { + if (tree) + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Currently Allocated Strings"u8); + ImUtf8.Text("Total Allocated Strings"u8); + ImUtf8.Text("Free'd Allocated Strings"u8); + ImUtf8.Text("Currently Allocated Bytes"u8); + ImUtf8.Text("Total Allocated Bytes"u8); + ImUtf8.Text("Free'd Allocated Bytes"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{PenumbraStringMemory.CurrentStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.AllocatedStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.FreedStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.CurrentBytes}"); + ImUtf8.Text($"{PenumbraStringMemory.AllocatedBytes}"); + ImUtf8.Text($"{PenumbraStringMemory.FreedBytes}"); + } + } + } } private void DrawPerformanceTab() diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index e0cab43f..ecf9a886 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -134,12 +135,12 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH { var (path, name) = pair; ImGui.TableNextColumn(); - UiHelpers.CopyOnClickSelectable(path.Path); + ImUtf8.CopyOnClickSelectable(path.Path.Span); ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - UiHelpers.CopyOnClickSelectable(name.Path.InternalName); + ImUtf8.CopyOnClickSelectable(name.Path.InternalName.Span); ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); } From 4b9870f09089d58e9171fa39511f93e7c8c9cbc0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 Jul 2024 23:09:37 +0200 Subject: [PATCH 1882/2451] Fix some OtterGui changes. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 14 -------------- Penumbra/UI/Tabs/ResourceTab.cs | 4 ++-- 6 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 Penumbra/UI/ModsTab/ImcManipulationDrawer.cs diff --git a/OtterGui b/OtterGui index 33ffd7cb..b0464b7f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 33ffd7cba3e487e98e55adca1677354078089943 +Subproject commit b0464b7f215a0db1393e600968c6666307a3ae05 diff --git a/Penumbra.GameData b/Penumbra.GameData index d8ebd63c..75582ece 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d8ebd63cec1ac12ea547fd37b6c32bdf9b3f57d1 +Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index a50599a1..29fd7531 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -405,7 +405,7 @@ public partial class ModEditWindow } var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(true)) + foreach (var (start, end) in handledElements.Ranges(complement:true)) { if ((shpkConstant.ByteOffset & 0x3) == 0) { diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 5d10febd..bbb5e54e 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -140,7 +140,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr var value = (mask & (1 << i)) != 0; using (ImRaii.Disabled(!cache.CanChange(i))) { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + if (ImUtf8.Checkbox(""u8, ref value)) { if (data is ImcModGroup g) editor.ChangeDefaultAttribute(g, cache, i, value); diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs deleted file mode 100644 index 1291f568..00000000 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Meta.Manipulations; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class ImcManipulationDrawer -{ - -} diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index a4dbba2f..c54e3433 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -81,7 +81,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa return; var address = $"0x{(ulong)r:X}"; - ImGuiUtil.TextNextColumn($"0x{hash:X8}"); + ImGuiUtil.DrawTableColumn($"0x{hash:X8}"); ImGui.TableNextColumn(); ImGuiUtil.CopyOnClickSelectable(address); @@ -101,7 +101,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any."); - ImGuiUtil.TextNextColumn(r->RefCount.ToString()); + ImGuiUtil.DrawTableColumn(r->RefCount.ToString()); }); } From 67a220f821afac69aeff24bc8e49ec7cc3dcb1b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 Jul 2024 23:24:59 +0200 Subject: [PATCH 1883/2451] Add context menu to change mod state from Collections tab. --- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 86 ++++++++++++++----- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 9f37f847..b7648428 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -11,9 +12,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) : ITab, IUiService +public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab, IUiService { - private readonly List<(ModCollection, ModCollection, uint, string)> _cache = []; + private enum ModState + { + Enabled, + Disabled, + Unconfigured, + } + + private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = []; public ReadOnlySpan Label => "Collections"u8; @@ -23,45 +31,83 @@ public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSele var (direct, inherited) = CountUsage(selector.Selected!); ImGui.NewLine(); if (direct == 1) - ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); + ImUtf8.Text("This Mod is directly configured in 1 collection."u8); else if (direct == 0) - ImGuiUtil.TextColored(Colors.RegexWarningBorder, "This mod is entirely unused."); + ImUtf8.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); else - ImGui.TextUnformatted($"This Mod is directly configured in {direct} collections."); + ImUtf8.Text($"This Mod is directly configured in {direct} collections."); if (inherited > 0) - ImGui.TextUnformatted( - $"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); + ImUtf8.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); ImGui.NewLine(); ImGui.Separator(); ImGui.NewLine(); - using var table = ImRaii.Table("##modCollections", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##modCollections"u8, 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; - var size = ImGui.CalcTextSize("Unconfigured").X + 20 * ImGuiHelpers.GlobalScale; + var size = ImUtf8.CalcTextSize(ToText(ModState.Unconfigured)).X + 20 * ImGuiHelpers.GlobalScale; var collectionSize = 200 * ImGuiHelpers.GlobalScale; ImGui.TableSetupColumn("Collection", ImGuiTableColumnFlags.WidthFixed, collectionSize); ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, size); ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, collectionSize); ImGui.TableHeadersRow(); - foreach (var (collection, parent, color, text) in _cache) + foreach (var ((collection, parent, color, state), idx) in _cache.WithIndex()) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(collection.Name); + using var id = ImUtf8.PushId(idx); + ImUtf8.DrawTableColumn(collection.Name); ImGui.TableNextColumn(); - using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + ImUtf8.Text(ToText(state), color); + + using (var context = ImUtf8.PopupContextItem("Context"u8, ImGuiPopupFlags.MouseButtonRight)) { - ImGui.TextUnformatted(text); + if (context) + { + ImUtf8.Text(collection.Name); + ImGui.Separator(); + using (ImRaii.Disabled(state is ModState.Enabled && parent == collection)) + { + if (ImUtf8.MenuItem("Enable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, true); + } + } + + using (ImRaii.Disabled(state is ModState.Disabled && parent == collection)) + { + if (ImUtf8.MenuItem("Disable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, false); + } + } + + using (ImRaii.Disabled(parent != collection)) + { + if (ImUtf8.MenuItem("Inherit"u8)) + manager.Editor.SetModInheritance(collection, selector.Selected!, true); + } + } } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(parent == collection ? string.Empty : parent.Name); + ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Name); } } + private static ReadOnlySpan ToText(ModState state) + => state switch + { + ModState.Unconfigured => "Unconfigured"u8, + ModState.Enabled => "Enabled"u8, + ModState.Disabled => "Disabled"u8, + _ => "Unknown"u8, + }; + private (int Direct, int Inherited) CountUsage(Mod mod) { _cache.Clear(); @@ -72,14 +118,14 @@ public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSele var disInherited = ColorId.InheritedDisabledMod.Value(); var directCount = 0; var inheritedCount = 0; - foreach (var collection in storage) + foreach (var collection in manager.Storage) { var (settings, parent) = collection[mod.Index]; var (color, text) = settings == null - ? (undefined, "Unconfigured") + ? (undefined, ModState.Unconfigured) : settings.Enabled - ? (parent == collection ? enabled : inherited, "Enabled") - : (parent == collection ? disabled : disInherited, "Disabled"); + ? (parent == collection ? enabled : inherited, ModState.Enabled) + : (parent == collection ? disabled : disInherited, ModState.Disabled); _cache.Add((collection, parent, color, text)); if (color == enabled) From 9e15865a99fd78240943f6d11bd784c2c122ca1a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:37:31 +0200 Subject: [PATCH 1884/2451] Fix some further issues with empty byte strings. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b0464b7f..2b79faac 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b0464b7f215a0db1393e600968c6666307a3ae05 +Subproject commit 2b79faacff30a31e9ad4b0a3c5d57ffd6e34cfa4 From a308fb9f779acf939191cf3c10c8468a440d7ba9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:40:37 +0200 Subject: [PATCH 1885/2451] Allow hook overrides. --- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Animation/CharacterBaseLoadAnimation.cs | 3 +- Penumbra/Interop/Hooks/Animation/Dismount.cs | 2 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 6 +- .../Hooks/Animation/LoadCharacterSound.cs | 2 +- .../Hooks/Animation/LoadCharacterVfx.cs | 2 +- .../Hooks/Animation/LoadTimelineResources.cs | 2 +- .../Interop/Hooks/Animation/PlayFootstep.cs | 2 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 2 +- .../Interop/Hooks/Animation/SomeActionLoad.cs | 2 +- .../Hooks/Animation/SomeMountAnimation.cs | 2 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Hooks/Animation/SomeParasolAnimation.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 144 ++++++++++++++++-- .../Interop/Hooks/Meta/CalculateHeight.cs | 2 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 6 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 5 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 6 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 5 +- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 6 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 10 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateRender.cs | 4 +- .../Hooks/Objects/CharacterBaseDestructor.cs | 2 +- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 2 +- .../Hooks/Objects/CreateCharacterBase.cs | 2 +- Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 4 +- .../PostProcessing/ShaderReplacementFixer.cs | 34 +++-- .../Hooks/ResourceLoading/CreateFileWHook.cs | 2 +- .../Hooks/ResourceLoading/FileReadService.cs | 2 +- .../Hooks/ResourceLoading/MappedCodeReader.cs | 11 +- .../Hooks/ResourceLoading/PapHandler.cs | 3 + .../Hooks/ResourceLoading/PeSigScanner.cs | 1 - .../Hooks/ResourceLoading/ResourceService.cs | 7 +- .../Hooks/ResourceLoading/TexMdlService.cs | 6 +- .../Hooks/Resources/ApricotResourceLoad.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlShpk.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Resources/ResourceHandleDestructor.cs | 3 +- Penumbra/Penumbra.cs | 9 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 21 +-- Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 63 ++++++++ 52 files changed, 326 insertions(+), 108 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 96a51027..44eb7ebb 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -24,7 +24,7 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook("Apricot Listener Sound Play Caller", Sigs.ApricotListenerSoundPlayCaller, Detour, - true); //HookSettings.VfxIdentificationHooks); + !HookOverrides.Instance.Animation.ApricotListenerSoundPlayCaller); } public delegate nint Delegate(nint a1, nint a2, float a3); diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs index f99d8ca4..22609afc 100644 --- a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -26,7 +26,8 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, + !HookOverrides.Instance.Animation.CharacterBaseLoadAnimation); } public delegate void Delegate(DrawObject* drawBase); diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 034011e7..17151083 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -16,7 +16,7 @@ public sealed unsafe class Dismount : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, !HookOverrides.Instance.Animation.Dismount); } public delegate void Delegate(MountContainer* a1, nint a2); diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 48dc0078..29afd4ea 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -17,10 +17,10 @@ public sealed unsafe class LoadAreaVfx : FastHook public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { - _state = state; + _state = state; _collectionResolver = collectionResolver; - _crashHandler = crashHandler; - Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, HookSettings.VfxIdentificationHooks); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, !HookOverrides.Instance.Animation.LoadAreaVfx); } public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index 8d1096d2..91b70ede 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -20,7 +20,7 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, - HookSettings.VfxIdentificationHooks); + !HookOverrides.Instance.Animation.LoadCharacterSound); } public delegate nint Delegate(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index af801345..9a57ca12 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -26,7 +26,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, !HookOverrides.Instance.Animation.LoadCharacterVfx); } public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 8bb14db6..cdd82b95 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -31,7 +31,7 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, !HookOverrides.Instance.Animation.LoadTimelineResources); } public delegate ulong Delegate(SchedulerTimeline* timeline); diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs index e4a8c83c..858357c8 100644 --- a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -14,7 +14,7 @@ public sealed unsafe class PlayFootstep : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep); } public delegate void Delegate(GameObject* gameObject, int id, int unk); diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index 645b3565..dfbc615a 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -23,7 +23,7 @@ public sealed unsafe class ScheduleClipUpdate : FastHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, !HookOverrides.Instance.Animation.ScheduleClipUpdate); } public delegate void Delegate(ClipScheduler* x); diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 1f3c0e3b..e1751261 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -20,7 +20,7 @@ public sealed unsafe class SomeActionLoad : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, !HookOverrides.Instance.Animation.SomeActionLoad); } public delegate void Delegate(TimelineContainer* timelineManager); diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs index f2b48afe..75f1240a 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeMountAnimation : FastHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, !HookOverrides.Instance.Animation.SomeMountAnimation); } public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 8f952df5..7339c397 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -22,7 +22,7 @@ public sealed unsafe class SomePapLoad : FastHook _collectionResolver = collectionResolver; _objects = objects; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, !HookOverrides.Instance.Animation.SomePapLoad); } public delegate void Delegate(nint a1, int a2, nint a3, int a4); diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs index 165bd5eb..9df8d4eb 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeParasolAnimation : FastHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, !HookOverrides.Instance.Animation.SomeParasolAnimation); } public delegate void Delegate(DrawObject* drawObject, int unk1); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 9c60096f..a4f4201f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -1,14 +1,140 @@ +using Dalamud.Plugin; +using Newtonsoft.Json; + namespace Penumbra.Interop.Hooks; -public static class HookSettings +public class HookOverrides { - public const bool AllHooks = true; + public static HookOverrides Instance = new(); - public const bool ObjectHooks = true && AllHooks; - public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = true && AllHooks; - public const bool MetaEntryHooks = true && AllHooks; - public const bool MetaParentHooks = true && AllHooks; - public const bool VfxIdentificationHooks = true && AllHooks; - public const bool PostProcessingHooks = true && AllHooks; + public AnimationHooks Animation; + public MetaHooks Meta; + public ObjectHooks Objects; + public PostProcessingHooks PostProcessing; + public ResourceLoadingHooks ResourceLoading; + public ResourceHooks Resources; + + public HookOverrides Clone() + => new() + { + Animation = Animation, + Meta = Meta, + Objects = Objects, + PostProcessing = PostProcessing, + ResourceLoading = ResourceLoading, + Resources = Resources, + }; + + public struct AnimationHooks + { + public bool ApricotListenerSoundPlayCaller; + public bool CharacterBaseLoadAnimation; + public bool Dismount; + public bool LoadAreaVfx; + public bool LoadCharacterSound; + public bool LoadCharacterVfx; + public bool LoadTimelineResources; + public bool PlayFootstep; + public bool ScheduleClipUpdate; + public bool SomeActionLoad; + public bool SomeMountAnimation; + public bool SomePapLoad; + public bool SomeParasolAnimation; + } + + public struct MetaHooks + { + public bool CalculateHeight; + public bool ChangeCustomize; + public bool EqdpAccessoryHook; + public bool EqdpEquipHook; + public bool EqpHook; + public bool EstHook; + public bool GmpHook; + public bool ModelLoadComplete; + public bool RspBustHook; + public bool RspHeightHook; + public bool RspSetupCharacter; + public bool RspTailHook; + public bool SetupVisor; + public bool UpdateModel; + public bool UpdateRender; + } + + public struct ObjectHooks + { + public bool CharacterBaseDestructor; + public bool CharacterDestructor; + public bool CopyCharacter; + public bool CreateCharacterBase; + public bool EnableDraw; + public bool WeaponReload; + } + + public struct PostProcessingHooks + { + public bool HumanSetupScaling; + public bool HumanCreateDeformer; + public bool HumanOnRenderMaterial; + public bool ModelRendererOnRenderMaterial; + } + + public struct ResourceLoadingHooks + { + public bool CreateFileWHook; + public bool PapHooks; + public bool ReadSqPack; + public bool IncRef; + public bool DecRef; + public bool GetResourceSync; + public bool GetResourceAsync; + public bool CheckFileState; + public bool TexResourceHandleOnLoad; + public bool LoadMdlFileExtern; + } + + public struct ResourceHooks + { + public bool ApricotResourceLoad; + public bool LoadMtrlShpk; + public bool LoadMtrlTex; + public bool ResolvePathHooks; + public bool ResourceHandleDestructor; + } + + public const string FileName = "HookOverrides.json"; + + public static HookOverrides LoadFile(IDalamudPluginInterface pi) + { + var path = Path.Combine(pi.GetPluginConfigDirectory(), FileName); + if (!File.Exists(path)) + return new HookOverrides(); + + try + { + var text = File.ReadAllText(path); + var ret = JsonConvert.DeserializeObject(text); + Penumbra.Log.Warning("A hook override file was loaded, some hooks may be disabled and Penumbra might not be working as expected."); + return ret; + } + catch (Exception ex) + { + Penumbra.Log.Error($"A hook override file was found at {path}, but could not be loaded:\n{ex}"); + return new HookOverrides(); + } + } + + public void Write(IDalamudPluginInterface pi) + { + var path = Path.Combine(pi.GetPluginConfigDirectory(), FileName); + try + { + var text = JsonConvert.SerializeObject(this, Formatting.Indented); + File.WriteAllText(path, text); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not write hook override file to {path}:\n{ex}"); + } + } } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index aab64871..e71d07dd 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,7 +14,7 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } public delegate ulong Delegate(Character* character); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index f69e98e7..368845b4 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 63cca53f..43328600 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -16,8 +16,10 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqdpAccessoryHook); + if (!HookOverrides.Instance.Meta.EqdpAccessoryHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 5d5d2f84..fa0d5a29 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -16,8 +16,9 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqdpEquipHook); + if (!HookOverrides.Instance.Meta.EqdpEquipHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index f47db795..f35b922b 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -15,8 +15,10 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqpHook); + if (!HookOverrides.Instance.Meta.EqpHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 825b1244..8284eb69 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -21,8 +21,9 @@ public unsafe class EstHook : FastHook, IDisposable _metaState = metaState; _characterUtility = characterUtility; Task = hooks.CreateHook("FindEstEntry", Sigs.FindEstEntry, Detour, - metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EstHook); + if (!HookOverrides.Instance.Meta.EstHook) + _metaState.Config.ModsEnabled += Toggle; } private EstEntry Detour(ResourceHandle* estResource, uint genderRace, uint id) diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 12b221d9..d656ebdb 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -17,8 +17,10 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.GmpHook); + if (!HookOverrides.Instance.Meta.GmpHook) + _metaState.Config.ModsEnabled += Toggle; } private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index c1803745..4b9b05b1 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, !HookOverrides.Instance.Meta.ModelLoadComplete); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index e08dc393..c49556bf 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -19,8 +19,10 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspBustHook); + if (!HookOverrides.Instance.Meta.RspBustHook) + _metaState.Config.ModsEnabled += Toggle; } private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) @@ -34,7 +36,9 @@ public unsafe class RspBustHook : FastHook, IDisposable } var ret = storage; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 20e3c939..49180d6e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -17,10 +17,12 @@ public class RspHeightHook : FastHook, IDisposable public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) { - _metaState = metaState; + _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspHeightHook); + if (!HookOverrides.Instance.Meta.RspHeightHook) + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) @@ -33,6 +35,7 @@ public class RspHeightHook : FastHook, IDisposable // Special cases. if (height == 0xFF) return 1.0f; + if (height > 100) height = 0; diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 8bcc7593..952a2e29 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class RspSetupCharacter : FastHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, !HookOverrides.Instance.Meta.RspSetupCharacter); } public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 86d21c6f..b434efa6 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -19,14 +19,18 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspTailHook); + if (!HookOverrides.Instance.Meta.RspTailHook) + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) { float scale; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 83c0e0c4..063a9462 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -19,7 +19,7 @@ public sealed unsafe class SetupVisor : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, !HookOverrides.Instance.Meta.SetupVisor); } public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index a088a0f2..72beea0e 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -15,7 +15,7 @@ public sealed unsafe class UpdateModel : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, !HookOverrides.Instance.Meta.UpdateModel); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateRender.cs b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs index 95cc0e15..ef0068b6 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateRender.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs @@ -13,8 +13,8 @@ public sealed unsafe class UpdateRender : FastHook public UpdateRender(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vTables) { _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, HookSettings.MetaParentHooks); + _metaState = metaState; + Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, !HookOverrides.Instance.Meta.UpdateRender); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index c67bb9f3..7636718e 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CharacterBaseDestructor); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index 618d0bd7..ffe2f72d 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, !HookOverrides.Instance.Objects.CharacterDestructor); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index d81043c8..bc18a7ad 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CopyCharacter); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index f00a9984..e29876ac 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -16,7 +16,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CreateCharacterBase); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 8b701fe5..68bb28af 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -17,7 +17,7 @@ public sealed unsafe class EnableDraw : IHookService public EnableDraw(HookManager hooks, GameState state) { _state = state; - _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, HookSettings.ObjectHooks); + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, !HookOverrides.Instance.Objects.EnableDraw); } private delegate void Delegate(GameObject* gameObject); diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index da31840f..b09103f6 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -16,7 +16,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.WeaponReload); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 834a7d28..1aa09d7f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -38,9 +38,9 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi _resourceLoader = resourceLoader; _framework = framework; _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, - HookSettings.PostProcessingHooks).Result; + !HookOverrides.Instance.PostProcessing.HumanSetupScaling).Result; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], - CreateDeformer, HookSettings.PostProcessingHooks).Result; + CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } public void Dispose() diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b87d33ef..53b69741 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -90,20 +90,26 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRenderer = modelRenderer; _communicator = communicator; - _skinState = new( + _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); - _irisState = new(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); - _characterGlassState = new(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new(() => _modelRenderer.CharacterTransparencyShaderPackage, () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new(() => _modelRenderer.CharacterTattooShaderPackage, () => _modelRenderer.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new(() => _modelRenderer.CharacterOcclusionShaderPackage, () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); - _hairMaskState = new(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, + () => _modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTransparencyShaderPackage, + () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTattooShaderPackage, + () => _modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => _modelRenderer.CharacterOcclusionShaderPackage, + () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + _hairMaskState = + new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], - OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; + OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", - Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; + Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, + !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } @@ -123,7 +129,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _skinState.ClearMaterials(); } - public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() + public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong + HairMask) GetAndResetSlowPathCallDeltas() => (_skinState.GetAndResetSlowPathCallDelta(), _irisState.GetAndResetSlowPathCallDelta(), _characterGlassState.GetAndResetSlowPathCallDelta(), @@ -208,7 +215,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForModelRenderer() - => _irisState.MaterialCount + _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + => _irisState.MaterialCount + + _characterGlassState.MaterialCount + + _characterTransparencyState.MaterialCount + + _characterTattooState.MaterialCount + + _characterOcclusionState.MaterialCount + + _hairMaskState.MaterialCount; private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index 8d0ac8cb..a9a5f41d 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -19,7 +19,7 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService public CreateFileWHook(IGameInteropProvider interop) { _createFileWHook = interop.HookFromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); - if (HookSettings.ReplacementHooks) + if (!HookOverrides.Instance.ResourceLoading.CreateFileWHook) _createFileWHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs index 199525fb..d8801b81 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs @@ -15,7 +15,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService _resourceManager = resourceManager; _performance = performance; interop.InitializeFromAttributes(this); - if (HookSettings.ReplacementHooks) + if (!HookOverrides.Instance.ResourceLoading.ReadSqPack) _readSqPackHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs index 81712cca..de0014d2 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs @@ -4,10 +4,11 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public class MappedCodeReader(UnmanagedMemoryAccessor data, long offset) : CodeReader { - public override int ReadByte() { - if (offset >= data.Capacity) - return -1; + public override int ReadByte() + { + if (offset >= data.Capacity) + return -1; - return data.ReadByte(offset++); - } + return data.ReadByte(offset++); + } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index ea12a480..5ba8c975 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -8,6 +8,9 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour public void Enable() { + if (HookOverrides.Instance.ResourceLoading.PapHooks) + return; + ReadOnlySpan<(string Sig, string Name)> signatures = [ (Sigs.LoadAlwaysResidentMotionPacks, nameof(Sigs.LoadAlwaysResidentMotionPacks)), diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index f5dd2d45..4be0da00 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -14,7 +14,6 @@ public unsafe class PeSigScanner : IDisposable private readonly nint _moduleBaseAddress; private readonly uint _textSectionVirtualAddress; - public PeSigScanner() { var mainModule = Process.GetCurrentProcess().MainModule!; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 8b99dc37..f75b0623 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -30,13 +30,14 @@ public unsafe class ResourceService : IDisposable, IRequiredService _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - if (HookSettings.ReplacementHooks) - { + if (HookOverrides.Instance.ResourceLoading.GetResourceSync) _getResourceSyncHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.DecRef) _decRefHook.Enable(); - } } public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, CiByteString path) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 80ba5cb9..fc3289bd 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -46,12 +46,12 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); - if (HookSettings.ReplacementHooks) - { + if (HookOverrides.Instance.ResourceLoading.CheckFileState) _checkFileStateHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) _loadMdlFileExternHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); - } } /// Add CRC64 if the given file is a model or texture file and has an associated path. diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index 511e842f..f6cccc19 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,7 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookOverrides.Instance.Resources.ApricotResourceLoad); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 8447762b..7aaa62d5 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -14,7 +14,7 @@ public sealed unsafe class LoadMtrlShpk : FastHook { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookOverrides.Instance.Resources.LoadMtrlShpk); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 7bc3c7b0..ed0e067b 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookOverrides.Instance.Resources.LoadMtrlTex); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index e1b6e46e..66945009 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -67,7 +67,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable // @formatter:on - if (HookSettings.ResourceHooks) + if (HookOverrides.Instance.Resources.ResolvePathHooks) Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index cd4a53c4..5c4b5c90 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -23,7 +23,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, HookSettings.ResourceHooks); + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, + HookOverrides.Instance.Resources.ResourceHandleDestructor); private readonly Task> _task; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5f8d6805..8ea74987 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; @@ -52,9 +53,10 @@ public class Penumbra : IDalamudPlugin { try { - _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); - Messager = _services.GetService(); - _validityChecker = _services.GetService(); + HookOverrides.Instance = HookOverrides.LoadFile(pluginInterface); + _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); + Messager = _services.GetService(); + _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); var startup = _services.GetService() @@ -215,6 +217,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); + sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); sb.Append( diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index a1e9da03..3a64e556 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -97,6 +97,7 @@ public class DebugTab : Window, ITab, IUiService private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly HookOverrideDrawer _hookOverrides; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -106,7 +107,8 @@ public class DebugTab : Window, ITab, IUiService DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, - Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer) + Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, + HookOverrideDrawer hookOverrides) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -143,6 +145,7 @@ public class DebugTab : Window, ITab, IUiService _ipcTester = ipcTester; _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; + _hookOverrides = hookOverrides; _objects = objects; _clientState = clientState; } @@ -166,34 +169,21 @@ public class DebugTab : Window, ITab, IUiService return; DrawDebugTabGeneral(); - ImGui.NewLine(); _crashHandlerPanel.Draw(); - ImGui.NewLine(); _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); - ImGui.NewLine(); DrawPathResolverDebug(); - ImGui.NewLine(); DrawActorsDebug(); - ImGui.NewLine(); DrawCollectionCaches(); - ImGui.NewLine(); _texHeaderDrawer.Draw(); - ImGui.NewLine(); DrawDebugCharacterUtility(); - ImGui.NewLine(); DrawShaderReplacementFixer(); - ImGui.NewLine(); DrawData(); - ImGui.NewLine(); DrawResourceProblems(); - ImGui.NewLine(); + _hookOverrides.Draw(); DrawPlayerModelInfo(); - ImGui.NewLine(); DrawGlobalVariableInfo(); - ImGui.NewLine(); DrawDebugTabIpc(); - ImGui.NewLine(); } @@ -434,7 +424,6 @@ public class DebugTab : Window, ITab, IUiService private void DrawPerformanceTab() { - ImGui.NewLine(); if (!ImGui.CollapsingHeader("Performance")) return; diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs new file mode 100644 index 00000000..7af1f884 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -0,0 +1,63 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Hooks; + +namespace Penumbra.UI.Tabs.Debug; + +public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : IUiService +{ + private HookOverrides? _overrides; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Generate Hook Override"u8); + if (!header) + return; + + _overrides ??= HookOverrides.Instance.Clone(); + + if (ImUtf8.Button("Save"u8)) + _overrides.Write(pluginInterface); + + ImGui.SameLine(); + var path = Path.Combine(pluginInterface.GetPluginConfigDirectory(), HookOverrides.FileName); + var exists = File.Exists(path); + if (ImUtf8.ButtonEx("Delete"u8, disabled: !exists, tooltip: exists ? ""u8 : "File does not exist."u8)) + try + { + File.Delete(path); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not delete hook override file at {path}:\n{ex}"); + } + + bool? all = null; + ImGui.SameLine(); + if (ImUtf8.Button("Disable All Hooks"u8)) + all = true; + ImGui.SameLine(); + if (ImUtf8.Button("Enable All Hooks"u8)) + all = false; + + foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true })) + { + using var tree = ImUtf8.TreeNode(propertyField.Name); + if (!tree) + continue; + + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + var value = valueField.GetValue(property) as bool? ?? false; + if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || all.HasValue) + { + valueField.SetValue(property, all ?? value); + propertyField.SetValue(_overrides, property); + } + } + } + } +} From 73b9d1fca0ede105413561ba0d61f17d7b176636 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:49:22 +0200 Subject: [PATCH 1886/2451] Meh. --- Penumbra/Interop/Hooks/HookSettings.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index a4f4201f..0c0a4020 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -5,6 +5,9 @@ namespace Penumbra.Interop.Hooks; public class HookOverrides { + [JsonIgnore] + public bool IsCustomLoaded { get; private set; } + public static HookOverrides Instance = new(); public AnimationHooks Animation; @@ -113,7 +116,8 @@ public class HookOverrides try { var text = File.ReadAllText(path); - var ret = JsonConvert.DeserializeObject(text); + var ret = JsonConvert.DeserializeObject(text)!; + ret.IsCustomLoaded = true; Penumbra.Log.Warning("A hook override file was loaded, some hooks may be disabled and Penumbra might not be working as expected."); return ret; } From 7579eaacbe0ffd3fcfcd03e33c6b526d5f2d2310 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 17:14:28 +0200 Subject: [PATCH 1887/2451] Meh. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs | 8 ++++---- Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs | 6 +++--- Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs | 3 ++- Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs | 4 ++-- Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Interop/Hooks/Resources/ResourceHandleDestructor.cs | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index f75b0623..e55c9bb0 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -30,13 +30,13 @@ public unsafe class ResourceService : IDisposable, IRequiredService _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - if (HookOverrides.Instance.ResourceLoading.GetResourceSync) + if (!HookOverrides.Instance.ResourceLoading.GetResourceSync) _getResourceSyncHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.GetResourceAsync) + if (!HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.IncRef) + if (!HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.DecRef) + if (!HookOverrides.Instance.ResourceLoading.DecRef) _decRefHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index fc3289bd..d4a2dfba 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -46,11 +46,11 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); - if (HookOverrides.Instance.ResourceLoading.CheckFileState) + if (!HookOverrides.Instance.ResourceLoading.CheckFileState) _checkFileStateHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) + if (!HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) _loadMdlFileExternHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) + if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index f6cccc19..40860b0b 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,8 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookOverrides.Instance.Resources.ApricotResourceLoad); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, + !HookOverrides.Instance.Resources.ApricotResourceLoad); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 7aaa62d5..8c410ad8 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -12,9 +12,9 @@ public sealed unsafe class LoadMtrlShpk : FastHook public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator) { - _gameState = gameState; + _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookOverrides.Instance.Resources.LoadMtrlShpk); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index ed0e067b..0759d9b1 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookOverrides.Instance.Resources.LoadMtrlTex); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, !HookOverrides.Instance.Resources.LoadMtrlTex); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 66945009..b1b23f27 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -67,7 +67,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable // @formatter:on - if (HookOverrides.Instance.Resources.ResolvePathHooks) + if (!HookOverrides.Instance.Resources.ResolvePathHooks) Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 5c4b5c90..bdb11752 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -24,7 +24,7 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, - HookOverrides.Instance.Resources.ResourceHandleDestructor); + !HookOverrides.Instance.Resources.ResourceHandleDestructor); private readonly Task> _task; From 1e1637f0e72dff18e6d1beee6c686d5ed509b9f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 17:14:46 +0200 Subject: [PATCH 1888/2451] Test IMC group toggling off. --- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- .../Manager/OptionEditor/ImcAttributeCache.cs | 34 +++++------------- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 36 +++++++++++++++---- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 03896134..5f99673e 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -242,7 +242,7 @@ public class ImcModGroup(Mod mod) : IModGroup continue; var option = OptionData[i]; - mask |= option.AttributeMask; + mask ^= option.AttributeMask; } return mask; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs index e1235c5b..a7b73ac9 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -21,23 +21,14 @@ public unsafe ref struct ImcAttributeCache _option[i] = byte.MaxValue; var flag = (ushort)(1 << i); - var set = (group.DefaultEntry.AttributeMask & flag) != 0; - if (set) - { - _canChange[i] = true; - _option[i] = byte.MaxValue - 1; - continue; - } - foreach (var (option, idx) in group.OptionData.WithIndex()) { - set = (option.AttributeMask & flag) != 0; - if (set) - { - _canChange[i] = option.AttributeMask != flag; - _option[i] = (byte)idx; - break; - } + if ((option.AttributeMask & flag) == 0) + continue; + + _canChange[i] = option.AttributeMask != flag; + _option[i] = (byte)idx; + break; } if (_option[i] == byte.MaxValue && LowestUnsetMask is 0) @@ -65,25 +56,16 @@ public unsafe ref struct ImcAttributeCache return true; } - if (!_canChange[idx]) - return false; - var mask = (ushort)(oldMask | flag); if (oldMask == mask) return false; group.DefaultEntry = group.DefaultEntry with { AttributeMask = mask }; - if (_option[idx] <= ImcEntry.NumAttributes) - { - var option = group.OptionData[_option[idx]]; - option.AttributeMask = (ushort)(option.AttributeMask & ~flag); - } - return true; } /// Set an attribute flag to a value if possible, remove it from its prior option or the default entry if necessary, and return if anything changed. - public readonly bool Set(ImcSubMod option, int idx, bool value) + public readonly bool Set(ImcSubMod option, int idx, bool value, bool turnOffDefault = false) { if (!_canChange[idx]) return false; @@ -110,7 +92,7 @@ public unsafe ref struct ImcAttributeCache var oldOption = option.Group.OptionData[_option[idx]]; oldOption.AttributeMask = (ushort)(oldOption.AttributeMask & ~flag); } - else if (_option[idx] is byte.MaxValue - 1) + else if (turnOffDefault && _option[idx] is byte.MaxValue - 1) { option.Group.DefaultEntry = option.Group.DefaultEntry with { diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index bbb5e54e..9d1ab78a 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -3,6 +3,9 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; +using OtterGui.Text.Widget; +using OtterGui.Widgets; +using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; @@ -86,7 +89,8 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr foreach (var (option, idx) in group.OptionData.WithIndex().Where(o => !o.Value.IsDisableSubMod)) { using var id = ImUtf8.PushId(idx); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option, + group.DefaultEntry.AttributeMask); } } } @@ -132,15 +136,18 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr } } - private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data, + ushort? defaultMask = null) { for (var i = 0; i < ImcEntry.NumAttributes; ++i) { - using var id = ImRaii.PushId(i); - var value = (mask & (1 << i)) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (mask & flag) != 0; + var inDefault = defaultMask.HasValue && (defaultMask & flag) != 0; + using (ImRaii.Disabled(defaultMask != null && !cache.CanChange(i))) { - if (ImUtf8.Checkbox(""u8, ref value)) + if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : ImUtf8.Checkbox(""u8, ref value)) { if (data is ImcModGroup g) editor.ChangeDefaultAttribute(g, cache, i, value); @@ -154,4 +161,21 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImUtf8.SameLineInner(); } } + + private sealed class NegativeCheckbox : MultiStateCheckbox + { + public static readonly NegativeCheckbox Instance = new(); + + protected override void RenderSymbol(bool value, Vector2 position, float size) + { + if (value) + SymbolHelpers.RenderCross(ImGui.GetWindowDrawList(), position, ImGui.GetColorU32(ImGuiCol.CheckMark), size); + } + + protected override bool NextValue(bool value) + => !value; + + protected override bool PreviousValue(bool value) + => !value; + } } From 5e9c7f7eac0c6334b3063918bd221a1525e80844 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 22:17:56 +0200 Subject: [PATCH 1889/2451] Fix IMC import sanity check. --- OtterGui | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 2b79faac..c53955cb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2b79faacff30a31e9ad4b0a3c5d57ffd6e34cfa4 +Subproject commit c53955cb6199dd418c5a9538d3251ac5942e7067 diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 5f99673e..7b0eb094 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -186,7 +186,7 @@ public class ImcModGroup(Mod mod) : IModGroup return null; } - var rollingMask = ret.DefaultEntry.AttributeMask; + var rollingMask = 0ul; if (options != null) foreach (var child in options.Children()) { From d903f1b8c3a61fdadc9387cad59cf7a3f337ab64 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 22:18:26 +0200 Subject: [PATCH 1890/2451] Update GetResourceSync and GetResourceAsync function signatures for testing, including unused stack parameters. --- .../Hooks/ResourceLoading/ResourceService.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index e55c9bb0..126505d1 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -44,7 +44,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService { var hash = path.Crc32; return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress, - &category, &type, &hash, path.Path, null, false); + &category, &type, &hash, path.Path, null, 0, 0, 0); } public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, CiByteString path) @@ -76,10 +76,10 @@ public unsafe class ResourceService : IDisposable, IRequiredService public event GetResourcePreDelegate? ResourceRequested; private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8); private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9); [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] private readonly Hook _getResourceSyncHook = null!; @@ -88,27 +88,28 @@ public unsafe class ResourceService : IDisposable, IRequiredService private readonly Hook _getResourceAsyncHook = null!; private ResourceHandle* GetResourceSyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams) - => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, nint unk8, uint unk9) + => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, 0, unk8, unk9); private ResourceHandle* GetResourceAsyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) - => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, byte isUnk, nint unk8, uint unk9) + => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk, unk8, unk9); /// /// Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. /// Both work basically the same, so we can reduce the main work to one function used by both hooks. /// private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, byte isUnk, nint unk8, uint unk9) { using var performance = _performance.Measure(PerformanceType.GetResourceHandler); if (!Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) { Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path."); return isSync - ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams) - : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, unk8, unk9) + : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk, unk8, + unk9); } ResourceHandle* returnValue = null; @@ -117,17 +118,17 @@ public unsafe class ResourceService : IDisposable, IRequiredService if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9); } /// Call the original GetResource function. public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, bool unk = false) + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) => sync ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters) + resourceParameters, unk8, unk9) : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk); + resourceParameters, unk, unk8, unk9); #endregion From f3e72711578f865e6c94fa68f69d50aa75248d90 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 2 Aug 2024 12:48:57 +0000 Subject: [PATCH 1891/2451] [CI] Updating repo.json for testing_1.2.0.18 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index adf152b0..e1653454 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.17", + "TestingAssemblyVersion": "1.2.0.18", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.17/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.18/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6241187431f313b6ac9d1959943677d04752546c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Aug 2024 15:15:19 +0200 Subject: [PATCH 1892/2451] Fix span constructors going over boundaries for ByteStrings. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 91f0f211..bd52d080 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 91f0f21137c61bd39281debf88a8ecc494043330 +Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779 From 4454ac48daadb2375806677f58fe9a7bb5710b8a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Aug 2024 13:18:14 +0000 Subject: [PATCH 1893/2451] [CI] Updating repo.json for testing_1.2.0.19 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e1653454..cbe2b121 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.18", + "TestingAssemblyVersion": "1.2.0.19", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.18/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.19/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 069b28272bcec59d03fc84e89850e7acf279e180 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:09:20 +0200 Subject: [PATCH 1894/2451] Add TextureArraySlicer --- .../Interop/Services/TextureArraySlicer.cs | 119 ++++++++++++++++++ Penumbra/Penumbra.csproj | 8 ++ Penumbra/UI/WindowSystem.cs | 31 +++-- 3 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 Penumbra/Interop/Services/TextureArraySlicer.cs diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs new file mode 100644 index 00000000..c934ac2b --- /dev/null +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -0,0 +1,119 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using OtterGui.Services; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; + +namespace Penumbra.Interop.Services; + +/// +/// Creates ImGui handles over slices of array textures, and manages their lifetime. +/// +public sealed unsafe class TextureArraySlicer : IUiService, IDisposable +{ + private const uint InitialTimeToLive = 2; + + private readonly Dictionary<(nint XivTexture, byte SliceIndex), SliceState> _activeSlices = []; + private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = []; + + /// Caching this across frames will cause a crash to desktop. + public nint GetImGuiHandle(Texture* texture, byte sliceIndex) + { + if (texture == null) + throw new ArgumentNullException(nameof(texture)); + if (sliceIndex >= texture->ArraySize) + throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) + { + state.Refresh(); + return (nint)state.ShaderResourceView; + } + var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; + var description = srv.Description; + switch (description.Dimension) + { + case ShaderResourceViewDimension.Texture1D: + case ShaderResourceViewDimension.Texture2D: + case ShaderResourceViewDimension.Texture2DMultisampled: + case ShaderResourceViewDimension.Texture3D: + case ShaderResourceViewDimension.TextureCube: + // This function treats these as single-slice arrays. + // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. + break; + case ShaderResourceViewDimension.Texture1DArray: + description.Texture1DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DArray: + description.Texture2DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DMultisampledArray: + description.Texture2DMSArray.FirstArraySlice = sliceIndex; + description.Texture2DMSArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.TextureCubeArray: + description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; + description.TextureCubeArray.CubeCount = 1; + break; + default: + throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); + } + state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); + _activeSlices.Add(((nint)texture, sliceIndex), state); + return (nint)state.ShaderResourceView; + } + + public void Tick() + { + try + { + foreach (var (key, slice) in _activeSlices) + { + if (!slice.Tick()) + _expiredKeys.Add(key); + } + foreach (var key in _expiredKeys) + { + _activeSlices.Remove(key); + } + } + finally + { + _expiredKeys.Clear(); + } + } + + public void Dispose() + { + foreach (var slice in _activeSlices.Values) + { + slice.Dispose(); + } + } + + private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable + { + public readonly ShaderResourceView ShaderResourceView = shaderResourceView; + + private uint _timeToLive = InitialTimeToLive; + + public void Refresh() + { + _timeToLive = InitialTimeToLive; + } + + public bool Tick() + { + if (unchecked(_timeToLive--) > 0) + return true; + + ShaderResourceView.Dispose(); + return false; + } + + public void Dispose() + { + ShaderResourceView.Dispose(); + } + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 24ffe469..8e143e3c 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -72,6 +72,14 @@ $(DalamudLibPath)Iced.dll False + + $(DalamudLibPath)SharpDX.dll + False + + + $(DalamudLibPath)SharpDX.Direct3D11.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 6d382ad4..575a381f 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using OtterGui.Services; +using Penumbra.Interop.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; @@ -10,23 +11,25 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly IUiBuilder _uiBuilder; - private readonly WindowSystem _windowSystem; - private readonly FileDialogService _fileDialog; - public readonly ConfigWindow Window; - public readonly PenumbraChangelog Changelog; - public readonly KnowledgeWindow KnowledgeWindow; + private readonly IUiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; + private readonly TextureArraySlicer _textureArraySlicer; + public readonly ConfigWindow Window; + public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, - KnowledgeWindow knowledgeWindow) + KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - KnowledgeWindow = knowledgeWindow; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + _textureArraySlicer = textureArraySlicer; + KnowledgeWindow = knowledgeWindow; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); @@ -37,6 +40,7 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; + _uiBuilder.Draw += _textureArraySlicer.Tick; _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; _uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes; _uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden; @@ -51,5 +55,6 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi -= Window.OpenSettings; _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _fileDialog.Draw; + _uiBuilder.Draw -= _textureArraySlicer.Tick; } } From 60986c78f8449b2e6aed128b5b2b42ce6aafe35a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:17:48 +0200 Subject: [PATCH 1895/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 75582ece..ee6c6faa 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c +Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a From 59b3859f117e083cb105e84838ad3d5bd8c186fc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:31:12 +0200 Subject: [PATCH 1896/2451] Minor upgrades to follow dependencies --- Penumbra/Import/Models/Export/MaterialExporter.cs | 11 ++++++----- Penumbra/Import/Textures/TextureDrawer.cs | 4 +--- Penumbra/Services/MigrationManager.cs | 4 ++-- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 6 +----- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 5df9e1c1..62892473 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table); + var table = new LegacyColorTable(material.Mtrl.Table!); var normal = material.Textures[TextureUsage.SamplerNormal]; @@ -103,6 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. + // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); @@ -139,17 +140,17 @@ public class MaterialExporter var nextRow = table[tableRow.Next]; // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); + var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorSpan[x].A = normalPixel.B; // Specular (table) - var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp(prevRow.SpecularStrength, nextRow.SpecularStrength, tableRow.Weight); + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); + var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); // Emissive (table) - var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index bd95d1ab..c83604e4 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -18,9 +18,7 @@ public static class TextureDrawer { if (texture.TextureWrap != null) { - size = size.X < texture.TextureWrap.Width - ? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width } - : new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height); + size = texture.TextureWrap.Size.Contain(size); ImGui.Image(texture.TextureWrap.ImGuiHandle, size); DrawData(texture); diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 84318da6..7726f6fd 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -279,7 +279,7 @@ public class MigrationManager(Configuration config) : IService Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); - if (file.IsDawnTrail) + if (file.IsDawntrail) { file.MigrateToDawntrail(); Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); @@ -329,7 +329,7 @@ public class MigrationManager(Configuration config) : IService try { var mtrl = new MtrlFile(data); - if (mtrl.IsDawnTrail) + if (mtrl.IsDawntrail) return data; mtrl.MigrateToDawntrail(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c47414b9..e2776b2f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -6,7 +6,6 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; using Penumbra.String; -using Penumbra.UI.Tabs; namespace Penumbra.UI.AdvancedWindow; @@ -245,10 +244,7 @@ public class ResourceTreeViewer if (visibility == NodeVisibility.Hidden) continue; - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity - - using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal); var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; From e8182f285e2fd83ee8c6ed532fa14c858f9557c1 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:22 +0200 Subject: [PATCH 1897/2451] Update StainService for DT --- .../Interop/Structs/CharacterUtilityData.cs | 28 ++++++- Penumbra/Services/StainService.cs | 84 ++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 51 +++++++---- 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 197de0bb..7595353f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,10 +5,15 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexSkinShpk = 83; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -97,8 +102,23 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexDecalTex * 8)] public TextureResourceHandle* DecalTexResource; + [FieldOffset(8 + IndexTileOrbArrayTex * 8)] + public TextureResourceHandle* TileOrbArrayTexResource; + + [FieldOffset(8 + IndexTileNormArrayTex * 8)] + public TextureResourceHandle* TileNormArrayTexResource; + [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] + public ResourceHandle* GudStmResource; + + [FieldOffset(8 + IndexLegacyStm * 8)] + public ResourceHandle* LegacyStmResource; + + [FieldOffset(8 + IndexSphereDArrayTex * 8)] + public TextureResourceHandle* SphereDArrayTexResource; + // not included resources have no known use case. } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 26b39229..50713968 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -6,19 +6,25 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; -using Penumbra.UI.AdvancedWindow; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.Services; public class StainService : IService { - public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { + // FIXME There might be a better way to handle that. + public int CurrentDyeChannel = 0; + protected override float GetFilterWidth() { var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (stainCombo.CurrentSelection.Key == 0) + if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) return baseSize; return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; @@ -47,33 +53,73 @@ public class StainService : IService protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = stainCombo.CurrentSelection.Key; + var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); var frame = new Vector2(ImGui.GetTextLineHeight()); - ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame); + ImGui.ColorButton("D", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame); + ImGui.ColorButton("S", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame); + ImGui.ColorButton("E", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); return ret; } } - public readonly DictStain StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; + public const int ChannelCount = 2; - public StainService(IDataManager dataManager, DictStain stainData) + public readonly DictStain StainData; + public readonly FilterComboColors StainCombo1; + public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this? + public readonly StmFile LegacyStmFile; + public readonly StmFile GudStmFile; + public readonly StainTemplateCombo LegacyTemplateCombo; + public readonly StainTemplateCombo GudTemplateCombo; + + public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo = new FilterComboColors(140, MouseWheelType.None, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), - Penumbra.Log); - StmFile = new StmFile(dataManager); - TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; + + LegacyTemplateCombo = new StainTemplateCombo(stainCombos, LegacyStmFile); + GudTemplateCombo = new StainTemplateCombo(stainCombos, GudStmFile); } + + /// Retrieves the instance for the given channel. Indexing is zero-based. + public FilterComboColors GetStainCombo(int channel) + => channel switch + { + 0 => StainCombo1, + 1 => StainCombo2, + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + }; + + /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + { + if (stmResourceHandle != null) + { + var stmData = stmResourceHandle->CsHandle.GetDataSpan(); + if (stmData.Length > 0) + { + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from ResourceHandle 0x{(nint)stmResourceHandle:X}"); + return new StmFile(stmData); + } + } + + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from Lumina"); + return new StmFile(dataManager); + } + + private FilterComboColors CreateStainCombo() + => new(140, MouseWheelType.None, + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + Penumbra.Log); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 3a64e556..ead02874 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,9 @@ using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -697,32 +700,48 @@ public class DebugTab : Window, ITab, IUiService if (!mainTree) return; - foreach (var (key, data) in _stains.StmFile.Entries) + using (var legacyTree = TreeNode("stainingtemplate.stm")) + { + if (legacyTree) + DrawStainTemplatesFile(_stains.LegacyStmFile); + } + + using (var gudTree = TreeNode("stainingtemplate_gud.stm")) + { + if (gudTree) + DrawStainTemplatesFile(_stains.GudStmFile); + } + } + + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + { + foreach (var (key, data) in stmFile.Entries) { using var tree = TreeNode($"Template {key}"); if (!tree) continue; - using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; - for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) { - var (r, g, b) = data.DiffuseEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + foreach (var list in data.Colors) + { + var color = list[i]; + ImGui.TableNextColumn(); + var frame = new Vector2(ImGui.GetTextLineHeight()); + ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame); + ImGui.SameLine(); + ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}"); + } - (r, g, b) = data.SpecularEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - (r, g, b) = data.EmissiveEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - var a = data.SpecularPowerEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); - - a = data.GlossEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); + foreach (var list in data.Scalars) + { + var scalar = list[i]; + ImGuiUtil.DrawTableColumn($"{scalar:F6}"); + } } } } From 450751e43fe4d9627da938715b3930e9d25ebe10 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:36 +0200 Subject: [PATCH 1898/2451] DT material editor, supporting components --- .../Materials/ConstantEditors.cs | 71 +++++++ .../Materials/MaterialTemplatePickers.cs | 177 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs new file mode 100644 index 00000000..690580df --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs @@ -0,0 +1,71 @@ +using System.Collections.Frozen; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public static class ConstantEditors +{ + public static readonly IEditor DefaultFloat = Editors.DefaultFloat.AsByteEditor(); + public static readonly IEditor DefaultInt = Editors.DefaultInt.AsByteEditor(); + public static readonly IEditor DefaultIntAsFloat = Editors.DefaultInt.IntAsFloatEditor().AsByteEditor(); + public static readonly IEditor DefaultColor = ColorEditor.HighDynamicRange.Reinterpreting(); + + /// + /// Material constants known to be encoded as native s. + /// + /// A editor is nonfunctional for them, as typical values for these constants would fall into the IEEE 754 denormalized number range. + /// + private static readonly FrozenSet KnownIntConstants; + + static ConstantEditors() + { + IReadOnlyList knownIntConstants = [ + "g_ToonIndex", + "g_ToonSpecIndex", + ]; + + KnownIntConstants = knownIntConstants.ToFrozenSet(); + } + + public static IEditor DefaultFor(Name name, MaterialTemplatePickers? materialTemplatePickers = null) + { + if (materialTemplatePickers != null) + { + if (name == Names.SphereMapIndexConstantName) + return materialTemplatePickers.SphereMapIndexPicker; + else if (name == Names.TileIndexConstantName) + return materialTemplatePickers.TileIndexPicker; + } + + if (name.Value != null && name.Value.EndsWith("Color")) + return DefaultColor; + + if (KnownIntConstants.Contains(name)) + return DefaultInt; + + return DefaultFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor AsByteEditor(this IEditor inner) where T : unmanaged + => inner.Reinterpreting(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor IntAsFloatEditor(this IEditor inner) + => inner.Converting(value => int.CreateSaturating(MathF.Round(value)), value => value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithExponent(this IEditor inner, T exponent) + where T : unmanaged, IPowerFunctions, IComparisonOperators + => exponent == T.MultiplicativeIdentity + ? inner + : inner.Converting(value => value < T.Zero ? -T.Pow(-value, T.MultiplicativeIdentity / exponent) : T.Pow(value, T.MultiplicativeIdentity / exponent), value => value < T.Zero ? -T.Pow(-value, exponent) : T.Pow(value, exponent)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithFactorAndBias(this IEditor inner, T factor, T bias) + where T : unmanaged, IMultiplicativeIdentity, IAdditiveIdentity, IMultiplyOperators, IAdditionOperators, ISubtractionOperators, IDivisionOperators, IEqualityOperators + => factor == T.MultiplicativeIdentity && bias == T.AdditiveIdentity + ? inner + : inner.Converting(value => (value - bias) / factor, value => value * factor + bias); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs new file mode 100644 index 00000000..6ffd1f88 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -0,0 +1,177 @@ +using Dalamud.Interface; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed unsafe class MaterialTemplatePickers : IUiService +{ + private const float MaximumTextureSize = 64.0f; + + private readonly TextureArraySlicer _textureArraySlicer; + private readonly CharacterUtility _characterUtility; + + public readonly IEditor TileIndexPicker; + public readonly IEditor SphereMapIndexPicker; + + public MaterialTemplatePickers(TextureArraySlicer textureArraySlicer, CharacterUtility characterUtility) + { + _textureArraySlicer = textureArraySlicer; + _characterUtility = characterUtility; + + TileIndexPicker = new Editor(DrawTileIndexPicker).AsByteEditor(); + SphereMapIndexPicker = new Editor(DrawSphereMapIndexPicker).AsByteEditor(); + } + + public bool DrawTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->TileOrbArrayTexResource, + _characterUtility.Address->TileNormArrayTexResource, + ]); + + public bool DrawSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->SphereDArrayTexResource, + ]); + + public bool DrawTextureArrayIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact, ReadOnlySpan> textureRHs) + { + TextureResourceHandle* firstNonNullTextureRH = null; + foreach (var texture in textureRHs) + { + if (texture.Value != null && texture.Value->CsHandle.Texture != null) + { + firstNonNullTextureRH = texture; + break; + } + } + var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; + + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; + + var ret = false; + + var framePadding = ImGui.GetStyle().FramePadding; + var itemSpacing = ImGui.GetStyle().ItemSpacing; + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + var spaceSize = ImUtf8.CalcTextSize(" "u8).X; + var spaces = (int)((ImGui.CalcItemWidth() - framePadding.X * 2.0f - (compact ? 0.0f : (textureSize.X + itemSpacing.X) * textureRHs.Length)) / spaceSize); + using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, framePadding + new Vector2(0.0f, Math.Max(textureSize.Y - ImGui.GetFrameHeight() + itemSpacing.Y, 0.0f) * 0.5f), !compact); + using var combo = ImUtf8.Combo(label, (value == ushort.MaxValue ? "-" : value.ToString()).PadLeft(spaces), ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge); + if (combo.Success && firstNonNullTextureRH != null) + { + var lineHeight = Math.Max(ImGui.GetTextLineHeightWithSpacing(), framePadding.Y * 2.0f + textureSize.Y); + var itemWidth = Math.Max(ImGui.GetContentRegionAvail().X, ImUtf8.CalcTextSize("MMM"u8).X + (itemSpacing.X + textureSize.X) * textureRHs.Length + framePadding.X * 2.0f); + using var center = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); + using var clipper = ImUtf8.ListClipper(count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd && i < count; i++) + { + if (ImUtf8.Selectable($"{i,3}", i == value, size: new(itemWidth, lineHeight))) + { + ret = value != i; + value = (ushort)i; + } + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var textureRegionStart = new Vector2( + rectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), + rectMin.Y + framePadding.Y); + var maxSize = new Vector2(textureSize.X, rectMax.Y - framePadding.Y - textureRegionStart.Y); + DrawTextureSlices(textureRegionStart, maxSize, itemSpacing.X, textureRHs, (byte)i); + } + } + } + } + if (!compact && value != ushort.MaxValue) + { + var cbRectMin = ImGui.GetItemRectMin(); + var cbRectMax = ImGui.GetItemRectMax(); + var cbTextureRegionStart = new Vector2(cbRectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), cbRectMin.Y + framePadding.Y); + var cbMaxSize = new Vector2(textureSize.X, cbRectMax.Y - framePadding.Y - cbTextureRegionStart.Y); + DrawTextureSlices(cbTextureRegionStart, cbMaxSize, itemSpacing.X, textureRHs, (byte)value); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && (description.Length > 0 || compact && value != ushort.MaxValue)) + { + using var disabled = ImRaii.Enabled(); + using var tt = ImUtf8.Tooltip(); + if (description.Length > 0) + ImUtf8.Text(description); + if (compact && value != ushort.MaxValue) + { + ImGui.Dummy(new Vector2(textureSize.X * textureRHs.Length + itemSpacing.X * (textureRHs.Length - 1), textureSize.Y)); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + DrawTextureSlices(rectMin, textureSize, itemSpacing.X, textureRHs, (byte)value); + } + } + + return ret; + } + + public void DrawTextureSlices(Vector2 regionStart, Vector2 itemSize, float itemSpacing, ReadOnlySpan> textureRHs, byte sliceIndex) + { + for (var j = 0; j < textureRHs.Length; ++j) + { + if (textureRHs[j].Value == null) + continue; + var texture = textureRHs[j].Value->CsHandle.Texture; + if (texture == null) + continue; + var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); + if (handle == 0) + continue; + + var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; + var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + position += (itemSize - size) * 0.5f; + ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, + new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + } + } + + private delegate bool DrawEditor(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact); + + private sealed class Editor(DrawEditor draw) : IEditor + { + public bool Draw(Span values, bool disabled) + { + var helper = Editors.PrepareMultiComponent(values.Length); + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + helper.SetupComponent(valueIdx); + + var value = ushort.CreateSaturating(MathF.Round(values[valueIdx])); + if (disabled) + { + using var _ = ImRaii.Disabled(); + draw(helper.Id, default, ref value, true); + } + else + { + if (draw(helper.Id, default, ref value, true)) + { + values[valueIdx] = value; + ret = true; + } + } + } + + return ret; + } + } +} From 36ab9573aec81e56655be92f62c7e3e473d737f5 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:41:38 +0200 Subject: [PATCH 1899/2451] DT material editor, main part --- Penumbra/Configuration.cs | 1 + .../LiveColorTablePreviewer.cs | 24 +- .../MaterialPreview/LiveMaterialPreviewer.cs | 20 +- .../Materials/MtrlTab.CommonColorTable.cs | 509 ++++++++++++ .../Materials/MtrlTab.Constants.cs | 277 +++++++ .../Materials/MtrlTab.Devkit.cs | 240 ++++++ .../Materials/MtrlTab.LegacyColorTable.cs | 368 ++++++++ .../Materials/MtrlTab.LivePreview.cs | 272 ++++++ .../Materials/MtrlTab.ShaderPackage.cs | 505 +++++++++++ .../Materials/MtrlTab.Textures.cs | 276 ++++++ .../UI/AdvancedWindow/Materials/MtrlTab.cs | 199 +++++ .../Materials/MtrlTabFactory.cs | 18 + .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.ConstantEditor.cs | 247 ------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ .../ModEditWindow.Materials.Shpk.cs | 481 ----------- .../AdvancedWindow/ModEditWindow.Materials.cs | 179 +--- .../ModEditWindow.QuickImport.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 76 +- Penumbra/UI/Classes/Colors.cs | 4 +- Penumbra/UI/Tabs/SettingsTab.cs | 12 + 21 files changed, 2744 insertions(+), 2286 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63325433..50426b38 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -107,6 +107,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AlwaysOpenDefaultImport { get; set; } = false; public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public bool EditRawTileTransforms { get; set; } = false; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 8e75a895..bbd3b16c 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { - public const int TextureWidth = 4; - public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows; - public const int TextureLength = TextureWidth * TextureHeight * 4; - private readonly IFramework _framework; private readonly Texture** _colorTableTexture; @@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private bool _updatePending; + public int Width { get; } + public int Height { get; } + public Half[] ColorTable { get; } public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo) @@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color table textures"); - _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); + _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + Width = (int)_originalColorTableTexture.Texture->Width; + Height = (int)_originalColorTableTexture.Texture->Height; + ColorTable = new Half[Width * Height * 4]; _updatePending = true; framework.Update += OnFrameworkUpdate; } + public Span GetColorRow(int i) + => ColorTable.AsSpan().Slice(Width * 4 * i, Width * 4); + protected override void Clear(bool disposing, bool reset) { _framework.Update -= OnFrameworkUpdate; @@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase return; var textureSize = stackalloc int[2]; - textureSize[0] = TextureWidth; - textureSize[1] = TextureHeight; + textureSize[0] = Width; + textureSize[1] = Height; using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); @@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) return false; - return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); } } diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 0556fdc4..60762ac7 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -7,9 +7,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase { private readonly ShaderPackage* _shaderPackage; - private readonly uint _originalShPkFlags; - private readonly float[] _originalMaterialParameter; - private readonly uint[] _originalSamplerFlags; + private readonly uint _originalShPkFlags; + private readonly byte[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo) : base(objects, materialInfo) @@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase _originalShPkFlags = Material->ShaderFlags; - _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); + _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); _originalSamplerFlags = new uint[Material->TextureCount]; for (var i = 0; i < _originalSamplerFlags.Length; ++i) @@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase return; Material->ShaderFlags = _originalShPkFlags; - var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); + var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); if (!materialParameter.IsEmpty) _originalMaterialParameter.AsSpan().CopyTo(materialParameter); @@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase Material->ShaderFlags = shPkFlags; } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + public void SetMaterialParameter(uint parameterCrc, Index offset, ReadOnlySpan value) { if (!CheckValidity()) return; @@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (constantBuffer == null) return; - var buffer = constantBuffer->TryGetBuffer(); + var buffer = constantBuffer->TryGetBuffer(); if (buffer.IsEmpty) return; @@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (parameter.CRC != parameterCrc) continue; - if ((parameter.Offset & 0x3) != 0 - || (parameter.Size & 0x3) != 0 - || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + if (parameter.Offset + parameter.Size > buffer.Length) return; - value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]); return; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs new file mode 100644 index 00000000..937614de --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -0,0 +1,509 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using OtterGui.Raii; +using OtterGui.Text.Widget; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + + private static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); + + private static (Vector2 Scale, float Rotation, float Shear)? _pinnedTileTransform; + + private bool DrawColorTableSection(bool disabled) + { + if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + ColorTableCopyAllClipboardButton(); + ImGui.SameLine(); + var ret = ColorTablePasteAllClipboardButton(disabled); + if (!disabled) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= ColorTableDyeableCheckbox(); + } + + if (Mtrl.DyeTable != null) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(disabled); + } + + ret |= Mtrl.Table switch + { + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, + }; + + return ret; + } + + private void ColorTableCopyAllClipboardButton() + { + if (Mtrl.Table == null) + return; + + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + return; + + try + { + var data1 = Mtrl.Table.AsBytes(); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private bool DrawPreviewDye(bool disabled) + { + var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; + var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; + var tt = dyeId1 == 0 && dyeId2 == 0 + ? "Select a preview dye first."u8 + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; + if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0)) + { + var ret = false; + if (Mtrl.DyeTable != null) + { + ret |= Mtrl.ApplyDye(_stainService.LegacyStmFile, [dyeId1, dyeId2]); + ret |= Mtrl.ApplyDye(_stainService.GudStmFile, [dyeId1, dyeId2]); + } + + UpdateColorTablePreview(); + + return ret; + } + + ImGui.SameLine(); + var label = dyeId1 == 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; + if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1)) + UpdateColorTablePreview(); + ImGui.SameLine(); + label = dyeId2 == 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; + if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2)) + UpdateColorTablePreview(); + return false; + } + + private bool ColorTablePasteAllClipboardButton(bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.ButtonEx("Import All Rows from Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0), disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var table = Mtrl.Table.AsBytes(); + var dyeTable = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + if (data.Length != table.Length && data.Length != table.Length + dyeTable.Length) + return false; + + data.AsSpan(0, table.Length).TryCopyTo(table); + data.AsSpan(table.Length).TryCopyTo(dyeTable); + + UpdateColorTablePreview(); + + return true; + } + catch + { + return false; + } + } + + [SkipLocalsInit] + private void ColorTableCopyClipboardButton(int rowIdx) + { + if (Mtrl.Table == null) + return; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, + ImGui.GetFrameHeight() * Vector2.One)) + return; + + try + { + var data1 = Mtrl.Table.RowAsBytes(rowIdx); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private bool ColorTableDyeableCheckbox() + { + var dyeable = Mtrl.DyeTable != null; + var ret = ImGui.Checkbox("Dyeable", ref dyeable); + + if (ret) + { + Mtrl.DyeTable = dyeable ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } : null; + UpdateColorTablePreview(); + } + + return ret; + } + + private bool ColorTablePasteFromClipboardButton(int rowIdx, bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var row = Mtrl.Table.RowAsBytes(rowIdx); + var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + return false; + + data.AsSpan(0, row.Length).TryCopyTo(row); + data.AsSpan(row.Length).TryCopyTo(dyeRow); + + UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + } + + private void ColorTableHighlightButton(int pairIdx, bool disabled) + { + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + + if (ImGui.IsItemHovered()) + HighlightColorTablePair(pairIdx); + else if (HighlightedColorTablePair == pairIdx) + CancelColorTableHighlight(); + } + + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) + { + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; + var frameThickness = style.FrameBorderSize; + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); + if (topColor == bottomColor) + drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + else + { + drawList.AddRectFilled( + rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight); + drawList.AddRectFilledMultiColor( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, + topColor, topColor, bottomColor, bottomColor); + drawList.AddRectFilled( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, + bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); + } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); + } + + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + { + var ret = false; + var inputSqrt = PseudoSqrtRgb((Vector3)current); + var tmp = inputSqrt; + if (ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) + { + setter((HalfColor)PseudoSquareRgb(tmp)); + ret = true; + } + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textColor = inputSqrt.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText(letter, center, textColor); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + + return ret; + } + + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + { + if (current.HasValue) + CtColorPicker(label, description, current.Value, Nop, letter); + else + { + var tmp = Vector4.Zero; + ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR + | ImGuiColorEditFlags.AlphaPreview); + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + } + } + + private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) + { + var tmp = current; + var result = ApplyStainCheckbox.Draw(label, ref tmp); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == current) + return false; + + setter(tmp); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + setter(newValue); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + value = newValue; + return true; + } + + private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; + CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + setter(tmp); + return true; + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + value = tmp; + return true; + } + + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; + CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); + } + + private bool CtTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtTileTransformMatrix(HalfMatrix2x2 value, float floatSize, bool twoRowLayout, Action setter) + { + var ret = false; + if (_config.EditRawTileTransforms) + { + var tmp = value; + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUU"u8, "Tile Repeat U"u8, ref tmp.UU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVV"u8, "Tile Repeat V"u8, ref tmp.VV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUV"u8, "Tile Skew U"u8, ref tmp.UV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!ret || tmp == value) + return false; + setter(tmp); + } + else + { + value.Decompose(out var scale, out var rotation, out var shear); + rotation *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; + ImGui.SetNextItemWidth(floatSize); + var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + var activated = ImGui.IsItemActivated(); + var deactivated = ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (deactivated) + _pinnedTileTransform = null; + else if (activated) + _pinnedTileTransform = (scale, rotation, shear); + ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; + if (!ret) + return false; + if (_pinnedTileTransform.HasValue) + { + var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; + if (!scaleXChanged) + scale.X = pinScale.X; + if (!scaleYChanged) + scale.Y = pinScale.Y; + if (!rotationChanged) + rotation = pinRotation; + if (!shearChanged) + shear = pinShear; + } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); + if (newValue == value) + return false; + setter(newValue); + } + return true; + } + + /// For use as setter of read-only fields. + private static void Nop(T _) + { } + + // Functions to deal with squared RGB values without making negatives useless. + + internal static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : x * x; + + internal static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + internal static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + internal static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + internal static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + internal static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs new file mode 100644 index 00000000..56496005 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -0,0 +1,277 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float MaterialConstantSize = 250.0f; + + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IEditor Editor)> + Constants)> Constants = new(16); + + private void UpdateConstants() + { + static List FindOrAddGroup(List<(string, List)> groups, string name) + { + foreach (var (groupName, group) in groups) + { + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; + } + + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; + } + + Constants.Clear(); + string mpPrefix; + if (AssociatedShpk == null) + { + mpPrefix = MaterialParamsConstantName.Value!; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) + { + var values = Mtrl.GetConstantValue(constant); + for (var i = 0; i < values.Length; i += 4) + { + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + ConstantEditors.DefaultFloat)); + } + } + } + else + { + mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + var name = Names.KnownNames.TryResolve(shpkConstant.Id); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var values = Mtrl.GetConstantValue(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.EffectiveByteOffset; + var length = values.Length - offset; + var constantSize = dkConstant.EffectiveByteSize; + if (constantSize.HasValue) + length = Math.Min(length, (int)constantSize.Value); + if (length <= 0) + continue; + + var editor = dkConstant.CreateEditor(_materialTemplatePickers); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } + + if (handledElements.IsFull) + continue; + + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(complement: true)) + { + if (start == 0 && end == values.Length && end - start <= 16) + { + if (name.Value != null) + { + fcGroup.Add(( + $"{name.Value.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); + continue; + } + } + + if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset; + for (int i = (start & ~0xF) - (offset & 0xF), j = offset >> 4; i < end; i += 16, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 16, end); + if (rangeEnd > rangeStart) + { + var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + fcGroup.Add(( + $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); + } + } + } + else + { + for (var i = start; i < end; i += 16) + { + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + DefaultConstantEditorFor(name))); + } + } + } + } + } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme, and cbuffer-location names appear after known variable names + foreach (var (_, group) in Constants) + { + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : y.Label)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IEditor DefaultConstantEditorFor(Name name) + => ConstantEditors.DefaultFor(name, _materialTemplatePickers); + + private bool DrawConstantsSection(bool disabled) + { + if (Constants.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) + return false; + + using var _ = ImRaii.PushId("MaterialConstants"); + + var ret = false; + foreach (var (header, group) in Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; + + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = Mtrl.GetConstantValue(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + ImGui.SetNextItemWidth(MaterialConstantSize * UiHelpers.Scale); + if (editor.Draw(buffer[slice], disabled)) + { + ret = true; + SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; + var canReset = AssociatedShpk?.MaterialParamsDefaults != null + ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) + : buffer[slice].ContainsAnyExcept((byte)0); + ImUtf8.SameLineInner(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + { + ret = true; + if (defaultValue.Length > 0) + defaultValue.CopyTo(buffer[slice]); + else + buffer[slice].Clear(); + + SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } + + return ret; + } + + private static bool IsValid(Range range, int length) + { + var start = range.Start.GetOffset(length); + var end = range.End.GetOffset(length); + return start >= 0 && start <= length && end >= start && end <= length; + } + + internal static string? MaterialParamName(bool componentOnly, int offset) + { + if (offset < 0) + return null; + + return (componentOnly, offset & 0x3) switch + { + (true, 0) => "x", + (true, 1) => "y", + (true, 2) => "z", + (true, 3) => "w", + (false, 0) => $"[{offset >> 2:D2}].x", + (false, 1) => $"[{offset >> 2:D2}].y", + (false, 2) => $"[{offset >> 2:D2}].z", + (false, 3) => $"[{offset >> 2:D2}].w", + _ => null, + }; + } + + /// Returned string is 4 chars long. + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; + + internal static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) + { + if (valueLength == 0 || valueOffset < 0) + return (null, false); + + var firstVector = valueOffset >> 2; + var lastVector = (valueOffset + valueLength - 1) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = (valueOffset + valueLength - 1) & 0x3; + if (firstVector == lastVector) + return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); + + var sb = new StringBuilder(128); + sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); + for (var i = firstVector + 1; i < lastVector; ++i) + sb.Append($", [{i}]"); + + sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); + return (sb.ToString(), false); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs new file mode 100644 index 00000000..cd62d58f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -0,0 +1,240 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Text.Widget.Editors; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) + { + try + { + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); + + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } + } + + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; + + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) + { + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; + } + + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = []; + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + /// Integer encoded as a float. + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. + Int32 = 4, + Int32Enum = 5, + Int8 = 6, + Int8Enum = 7, + Int16 = 8, + Int16Enum = 9, + Int64 = 10, + Int64Enum = 11, + Half = 12, + Double = 13, + TileIndex = 14, + SphereMapIndex = 15, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public double Value = 0; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public uint? ByteOffset = null; + public uint? ByteSize = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float Step = 0.0f; + public float StepFast = 0.0f; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Exponent = 1.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public bool Hex = false; + public bool Slider = true; + public bool Drag = true; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = []; + + public uint EffectiveByteOffset + => ByteOffset ?? Offset * ValueSize; + + public uint? EffectiveByteSize + => ByteSize ?? (Length * ValueSize); + + public unsafe uint ValueSize + => Type switch + { + DevkitConstantType.Hidden => sizeof(byte), + DevkitConstantType.Float => sizeof(float), + DevkitConstantType.Integer => sizeof(float), + DevkitConstantType.Color => sizeof(float), + DevkitConstantType.Enum => sizeof(float), + DevkitConstantType.Int32 => sizeof(int), + DevkitConstantType.Int32Enum => sizeof(int), + DevkitConstantType.Int8 => sizeof(byte), + DevkitConstantType.Int8Enum => sizeof(byte), + DevkitConstantType.Int16 => sizeof(short), + DevkitConstantType.Int16Enum => sizeof(short), + DevkitConstantType.Int64 => sizeof(long), + DevkitConstantType.Int64Enum => sizeof(long), + DevkitConstantType.Half => (uint)sizeof(Half), + DevkitConstantType.Double => sizeof(double), + DevkitConstantType.TileIndex => sizeof(float), + DevkitConstantType.SphereMapIndex => sizeof(float), + _ => sizeof(float), + }; + + public IEditor? CreateEditor(MaterialTemplatePickers? materialTemplatePickers) + => Type switch + { + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Integer => CreateIntegerEditor().IntAsFloatEditor().AsByteEditor(), + DevkitConstantType.Color => ColorEditor.Get(!Clamped).WithExponent(SquaredRgb ? 2.0f : 1.0f).AsByteEditor(), + DevkitConstantType.Enum => CreateEnumEditor(float.CreateSaturating).AsByteEditor(), + DevkitConstantType.Int32 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int32Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int8 => CreateIntegerEditor(), + DevkitConstantType.Int8Enum => CreateEnumEditor(ToInteger), + DevkitConstantType.Int16 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int16Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int64 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int64Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Half => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Double => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.TileIndex => materialTemplatePickers?.TileIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + DevkitConstantType.SphereMapIndex => materialTemplatePickers?.SphereMapIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + _ => ConstantEditors.DefaultFloat, + }; + + private IEditor CreateIntegerEditor() + where T : unmanaged, INumber + => ((Drag || Slider) && !Hex + ? (Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); + + private IEditor CreateFloatEditor() + where T : unmanaged, INumber, IPowerFunctions + => ((Drag || Slider) + ? (Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + .WithExponent(T.CreateSaturating(Exponent)) + .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); + + private EnumEditor CreateEnumEditor(Func convertValue) + where T : unmanaged, IUtf8SpanFormattable, IEqualityOperators + => new(Array.ConvertAll(Values, value => (ToUtf8(value.Label), convertValue(value.Value), ToUtf8(value.Description)))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(float value) where T : struct, INumberBase + => T.CreateSaturating(MathF.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(double value) where T : struct, INumberBase + => T.CreateSaturating(Math.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToInteger(float? value) where T : struct, INumberBase + => value.HasValue ? ToInteger(value.Value) : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToFloat(float? value) where T : struct, INumberBase + => value.HasValue ? T.CreateSaturating(value.Value) : null; + + private static ReadOnlyMemory ToUtf8(string value) + => Encoding.UTF8.GetBytes(value); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs new file mode 100644 index 00000000..f3ec5307 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -0,0 +1,368 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float LegacyColorTableFloatSize = 65.0f; + private const float LegacyColorTablePercentageSize = 50.0f; + private const float LegacyColorTableIntegerSize = 40.0f; + private const float LegacyColorTableByteSize = 25.0f; + + private bool DrawLegacyColorTable(LegacyColorTable table, LegacyColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < LegacyColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private bool DrawLegacyColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < ColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private static void DrawLegacyColorTableHeader(bool hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader(default(ReadOnlySpan)); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Row"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Diffuse"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Specular"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Emissive"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Gloss"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Tile"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Repeat / Skew"u8); + if (hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye Preview"u8); + } + } + + private bool DrawLegacyColorTableRow(LegacyColorTable table, LegacyColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.SpecularMask, + b => dyeTable[rowIdx].SpecularMask = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + v => table[rowIdx].Shininess = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Shininess, + b => dyeTable[rowIdx].Shininess = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyColorTableRow(ColorTable table, ColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + var byteSize = LegacyColorTableByteSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Scalar7 = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + v => table[rowIdx].Scalar3 = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##TileAlpha"u8, "Tile Opacity"u8, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(byteSize); + ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImUtf8.SameLineInner(); + _stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.StainCombo1.CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [stain], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private static void DrawLegacyDyePreview(LegacyDyePack values, float floatSize) + { + CtColorPicker("##diffusePreview"u8, default, values.DiffuseColor, "D"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##specularPreview"u8, default, values.SpecularColor, "S"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##emissivePreview"u8, default, values.EmissiveColor, "E"u8); + ImUtf8.SameLineInner(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth(floatSize); + var shininess = (float)values.Shininess; + ImGui.DragFloat("##shininessPreview", ref shininess, 0, shininess, shininess, "%.1f G"); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var specularMask = (float)values.SpecularMask * 100.0f; + ImGui.DragFloat("##specularMaskPreview", ref specularMask, 0, specularMask, specularMask, "%.0f%% S"); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs new file mode 100644 index 00000000..bb346534 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -0,0 +1,272 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List MaterialPreviewers = new(4); + public readonly List ColorTablePreviewers = new(4); + public int HighlightedColorTablePair = -1; + public readonly Stopwatch HighlightTime = new(); + + private void DrawMaterialLivePreviewRebind(bool disabled) + { + if (disabled) + return; + + if (ImGui.Button("Reload live preview")) + BindToMaterialInstances(); + + if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + return; + + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImUtf8.Text( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); + } + + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var instances = MaterialInfo.FindMaterials(_resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); + + var foundMaterials = new HashSet(); + foreach (var materialInfo in instances) + { + var material = materialInfo.GetDrawObjectMaterial(_objects); + if (foundMaterials.Contains((nint)material)) + continue; + + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateMaterialPreview(); + + if (Mtrl.Table == null) + return; + + foreach (var materialInfo in instances) + { + try + { + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateColorTablePreview(); + } + + private void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorTablePreviewers) + previewer.Dispose(); + ColorTablePreviewers.Clear(); + } + + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0;) + { + var previewer = MaterialPreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorTablePreviewers.Count; i-- > 0;) + { + var previewer = ColorTablePreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorTablePreviewers.RemoveAt(i); + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + private void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValue(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + public void HighlightColorTablePair(int pairIdx) + { + var oldPairIdx = HighlightedColorTablePair; + + if (HighlightedColorTablePair != pairIdx) + { + HighlightedColorTablePair = pairIdx; + HighlightTime.Restart(); + } + + if (oldPairIdx >= 0) + { + UpdateColorTableRowPreview(oldPairIdx << 1); + UpdateColorTableRowPreview((oldPairIdx << 1) | 1); + } + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void CancelColorTableHighlight() + { + var pairIdx = HighlightedColorTablePair; + + HighlightedColorTablePair = -1; + HighlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void UpdateColorTableRowPreview(int rowIdx) + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var row = Mtrl.Table switch + { + LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + ColorTable table => table[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), + }; + if (Mtrl.DyeTable != null) + { + var dyeRow = Mtrl.DyeTable switch + { + LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + }; + if (dyeRow.Channel < StainService.ChannelCount) + { + StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; + if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) + row.ApplyDye(dyeRow, legacyDyes); + if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) + row.ApplyDye(dyeRow, gudDyes); + } + } + + if (HighlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorTablePreviewers) + { + row[..].CopyTo(previewer.GetColorRow(rowIdx)); + previewer.ScheduleUpdate(); + } + } + + public void UpdateColorTablePreview() + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var rows = new ColorTable(Mtrl.Table); + var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; + if (dyeRows != null) + { + ReadOnlySpan stainIds = [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ]; + rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); + rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); + } + + if (HighlightedColorTablePair >= 0) + { + ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + } + + foreach (var previewer in ColorTablePreviewers) + { + rows.AsHalves().CopyTo(previewer.ColorTable); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + { + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); + var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); + var halfColor = (HalfColor)(color * color); + + row.DiffuseColor = halfColor; + row.SpecularColor = halfColor; + row.EmissiveColor = halfColor; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs new file mode 100644 index 00000000..21557939 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -0,0 +1,505 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause severe performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + internal static readonly IReadOnlyList StandardShaderPackages = new[] + { + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_fogModel.shpk", + // "apricot_gbuffer_decal_dummy.shpk", + // "apricot_gbuffer_decal_ring.shpk", + // "apricot_gbuffer_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bg_composite.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bgprop.shpk", + "bg.shpk", + "bguvscroll.shpk", + "characterglass.shpk", + "characterinc.shpk", + "characterlegacy.shpk", + "characterocclusion.shpk", + "characterreflection.shpk", + "characterscroll.shpk", + "charactershadowoffset.shpk", + "character.shpk", + "characterstockings.shpk", + "charactertattoo.shpk", + "charactertransparency.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "furblur.shpk", + "grassdynamicwave.shpk", + "grass.shpk", + "hairmask.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "subsurfaceblur.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + + private string[]? _shpkNames; + + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public bool ShpkLoading; + public JObject? AssociatedShpkDevkit; + + public readonly string LoadedBaseDevkitPathName; + public readonly JObject? AssociatedBaseDevkit; + + // Shader Key State + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); + + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public ReadOnlyMemory ShadersString = UnknownShadersString; + + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) + return FullPath.Empty; + + return _edit.FindBestMatch(defaultGamePath); + } + + public void LoadShpk(FullPath path) + => Task.Run(() => DoLoadShpk(path)); + + private async Task DoLoadShpk(FullPath path) + { + ShadersKnown = false; + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + ShpkLoading = true; + + try + { + var data = path.IsRooted + ? await File.ReadAllBytesAsync(path.FullName) + : _gameData.GetFile(path.InternalName.ToString())?.Data; + LoadedShpkPath = path; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + LoadedShpkPathName = path.ToPath(); + } + catch (Exception e) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + } + finally + { + ShpkLoading = false; + } + + if (LoadedShpkPath.InternalName.IsEmpty) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } + + UpdateShaderKeys(); + _updateOnNextFrame = true; + } + + private void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + foreach (var key in AssociatedShpk.MaterialKeys) + { + var keyName = Names.KnownNames.TryResolve(key.Id); + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var valueKnownNames = keyName.WithKnownSuffixes(); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + var valueName = valueKnownNames.TryResolve(Names.KnownNames, value); + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : valueName.ToString(), value, dkValue.Description); + + return (valueName.ToString(), value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); + } + else + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + { + var keyName = Names.KnownNames.TryResolve(key.Category); + var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); + ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + } + } + + private void UpdateShaders() + { + static void AddShader(HashSet globalSet, Dictionary> byPassSets, uint passId, int shaderIndex) + { + globalSet.Add(shaderIndex); + if (!byPassSets.TryGetValue(passId, out var passSet)) + { + passSet = []; + byPassSets.Add(passId, passSet); + } + passSet.Add(shaderIndex); + } + + VertexShaders.Clear(); + PixelShaders.Clear(); + + var vertexShadersByPass = new Dictionary>(); + var pixelShadersByPass = new Dictionary>(); + + if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + { + ShadersKnown = false; + } + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) + { + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + foreach (var pass in node.Value.Passes) + { + AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + } + else + ShadersKnown = false; + } + } + } + } + + if (ShadersKnown) + { + var builder = new StringBuilder(); + foreach (var (passId, passVS) in vertexShadersByPass) + { + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); + if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + { + shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + } + foreach (var (passId, passPS) in pixelShadersByPass) + { + if (vertexShadersByPass.ContainsKey(passId)) + continue; + + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + + ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + } + else + ShadersString = UnknownShadersString; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + } + + private bool DrawShaderSection(bool disabled) + { + var ret = false; + if (ImGui.CollapsingHeader(ShaderHeader)) + { + ret |= DrawPackageNameInput(disabled); + ret |= DrawShaderFlagsInput(disabled); + DrawCustomAssociations(); + ret |= DrawMaterialShaderKeys(disabled); + DrawMaterialShaders(); + } + + if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + if (AssociatedShpk == null) + { + ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } + else + { + ImUtf8.Text("No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, + ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow + } + } + + return ret; + } + + private bool DrawPackageNameInput(bool disabled) + { + if (disabled) + { + ImGui.TextUnformatted("Shader Package: " + Mtrl.ShaderPackage.Name); + return false; + } + + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in GetShpkNames()) + { + if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + { + Mtrl.ShaderPackage.Name = value; + ret = true; + AssociatedShpk = null; + LoadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); + } + } + + return ret; + } + + private bool DrawShaderFlagsInput(bool disabled) + { + var shpkFlags = (int)Mtrl.ShaderPackage.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + return false; + + Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + SetShaderPackageFlags((uint)shpkFlags); + return true; + } + + /// + /// Show the currently associated shpk file, if any, and the buttons to associate + /// a specific shpk from your drive, the modded shpk by path or the default shpk. + /// + private void DrawCustomAssociations() + { + const string tooltip = "Click to copy file path to clipboard."; + var text = AssociatedShpk == null + ? "Associated .shpk file: None" + : $"Associated .shpk file: {LoadedShpkPathName}"; + var devkitText = AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; + var baseDevkitText = AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + + if (ImGui.Button("Associate Custom .shpk File")) + _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => + { + if (success) + LoadShpk(new FullPath(name[0])); + }, 1, _edit.Mod!.ModPath.FullName, false); + + var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), + moddedPath.Equals(LoadedShpkPath))) + LoadShpk(moddedPath); + + if (!gamePath.Path.Equals(moddedPath.InternalName)) + { + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, + gamePath.Path.Equals(LoadedShpkPath.InternalName))) + LoadShpk(new FullPath(gamePath)); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private bool DrawMaterialShaderKeys(bool disabled) + { + if (ShaderKeys.Count == 0) + return false; + + var ret = false; + foreach (var (label, index, description, monoFont, values) in ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = + values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + { + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + Update(); + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + { + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); + } + else + { + ImGui.TextUnformatted($"{label}: {currentLabel}"); + } + } + + return ret; + } + + private void DrawMaterialShaders() + { + if (AssociatedShpk == null) + return; + + using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) + { + if (node) + ImUtf8.Text(ShadersString.Span); + } + + if (ShaderComment.Length > 0) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(ShaderComment); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs new file mode 100644 index 00000000..3181dafe --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -0,0 +1,276 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); + + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + + private void UpdateTextures() + { + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + } + + foreach (var samplerId in SamplerIds) + { + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (shpkSampler is not { Slot: 2 }) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); + } + + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.Table ??= new ColorTable(); + } + + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); + + TextureLabelWidth = 50f * UiHelpers.Scale; + + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } + + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + } + + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + } + + private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) + => addressMode switch + { + TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, + TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, + _ => ""u8, + }; + + private bool DrawTextureSection(bool disabled) + { + if (Textures.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table("##Textures", 3); + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in Textures) + { + using var _ = ImRaii.PushId(samplerI); + var tmp = Mtrl.Textures[textureI].Path; + var unfolded = UnfoldedTextures.Contains(samplerI); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), + new Vector2(frameHeight), + "Settings for this texture and the associated sampler", false, true)) + { + unfolded = !unfolded; + if (unfolded) + UnfoldedTextures.Add(samplerI); + else + UnfoldedTextures.Remove(samplerI); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != Mtrl.Textures[textureI].Path) + { + ret = true; + Mtrl.Textures[textureI].Path = tmp; + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + + if (unfolded) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler(disabled, textureI, samplerI); + ImGui.TableNextColumn(); + } + } + + return ret; + } + + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) + { + using var c = ImUtf8.Combo(label, value.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToString(), mode == value)) + { + value = mode; + ret = true; + } + + ImUtf8.SelectableHelpMarker(TextureAddressModeTooltip(mode)); + } + + return ret; + } + + private bool DrawMaterialSampler(bool disabled, int textureIdx, int samplerIdx) + { + var ret = false; + ref var texture = ref Mtrl.Textures[textureIdx]; + ref var sampler = ref Mtrl.ShaderPackage.Samplers[samplerIdx]; + + var dx11 = texture.DX11; + if (ImUtf8.Checkbox("Prepend -- to the file name on DirectX 11"u8, ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + var addressMode = samplerFlags.UAddressMode; + if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + { + samplerFlags.UAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + addressMode = samplerFlags.VAddressMode; + if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + { + samplerFlags.VAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = samplerFlags.LodBias; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) + { + samplerFlags.LodBias = lodBias; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = samplerFlags.MinLod; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) + { + samplerFlags.MinLod = minLod; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + + using var t = ImUtf8.TreeNode("Advanced Settings"u8); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Texture Flags"u8, ref texture.Flags, "%04X"u8, + flags: disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + ret = true; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Sampler Flags"u8, ref sampler.Flags, "%08X"u8, + flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + { + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs new file mode 100644 index 00000000..2d4e93f1 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -0,0 +1,199 @@ +using Dalamud.Plugin.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.GameData; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed partial class MtrlTab : IWritable, IDisposable +{ + private const int ShpkPrefixLength = 16; + + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly ObjectManager _objects; + private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly StainService _stainService; + private readonly ResourceTreeFactory _resourceTreeFactory; + private readonly FileDialogService _fileDialog; + private readonly MaterialTemplatePickers _materialTemplatePickers; + private readonly Configuration _config; + + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; + + private bool _updateOnNextFrame; + + public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) + { + _gameData = gameData; + _framework = framework; + _objects = objects; + _characterBaseDestructor = characterBaseDestructor; + _stainService = stainService; + _resourceTreeFactory = resourceTreeFactory; + _fileDialog = fileDialog; + _materialTemplatePickers = materialTemplatePickers; + _config = config; + + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + if (writable) + { + _characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); + BindToMaterialInstances(); + } + } + + public bool DrawVersionUpdate(bool disabled) + { + if (disabled || Mtrl.IsDawntrail) + return false; + + if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, + "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return false; + + Mtrl.MigrateToDawntrail(); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + return true; + } + + public bool DrawPanel(bool disabled) + { + if (_updateOnNextFrame) + { + _updateOnNextFrame = false; + Update(); + } + + DrawMaterialLivePreviewRebind(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderSection(disabled); + + ret |= DrawTextureSection(disabled); + ret |= DrawColorTableSection(disabled); + ret |= DrawConstantsSection(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(disabled); + + return !disabled && ret; + } + + private bool DrawBackFaceAndTransparency(bool disabled) + { + ref var shaderFlags = ref ShaderFlags.Wrap(ref Mtrl.ShaderPackage.Flags); + + var ret = false; + + using var dis = ImRaii.Disabled(disabled); + + var tmp = shaderFlags.EnableTransparency; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) + { + shaderFlags.EnableTransparency = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = shaderFlags.HideBackfaces; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + { + shaderFlags.HideBackfaces = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + if (ShpkLoading) + { + ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + + ImUtf8.Text("Loading shader (.shpk) file. Some functionality will only be available after this is done."u8, + ImGuiUtil.HalfBlendText(0x808000u)); // Half cyan + } + + return ret; + } + + private void DrawOtherMaterialDetails(bool _) + { + if (!ImGui.CollapsingHeader("Further Content")) + return; + + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.ColorSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + if (Mtrl.AdditionalData.Length <= 0) + return; + + using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + if (t) + Widget.DrawHexViewer(Mtrl.AdditionalData); + } + + public void Update() + { + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); + } + + public unsafe void Dispose() + { + UnbindFromMaterialInstances(); + if (Writable) + _characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); + } + + public bool Valid + => ShadersKnown && Mtrl.Valid; + + public byte[] Write() + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds); + + return output.Write(); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs new file mode 100644 index 00000000..af8b7db2 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +{ + public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, + materialTemplatePickers, config, edit, file, filePath, writable); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index 25c0e448..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; - - private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo(array); - data2.TryCopyTo(array.AsSpan(data1.Length)); - var text = Convert.ToBase64String(array); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private bool DrawPreviewDye(MtrlTab tab, bool disabled) - { - var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 - ? "Select a preview dye first." - : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) - { - var ret = false; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) - { - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(tab, rowIdx, disabled); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); - ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth(intSize); - if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) - return false; - - var values = entry[(int)stain]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - - var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Apply the selected dye to this row.", disabled, true); - - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); - - return ret; - } - - private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") - { - var ret = false; - var inputSqrt = PseudoSqrtRgb(input); - var tmp = inputSqrt; - if (ImGui.ColorEdit3(label, ref tmp, - ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB - | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) - && tmp != inputSqrt) - { - setter(PseudoSquareRgb(tmp)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - var textSize = ImGui.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText(center, textColor, letter); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return ret; - } - - // Functions to deal with squared RGB values without making negatives useless. - - private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : x * x; - - private static Vector3 PseudoSquareRgb(Vector3 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); - - private static Vector4 PseudoSquareRgb(Vector4 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); - - private static float PseudoSqrtRgb(float x) - => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - - internal static Vector3 PseudoSqrtRgb(Vector3 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); - - private static Vector4 PseudoSqrtRgb(Vector4 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs deleted file mode 100644 index 1f5db38e..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ /dev/null @@ -1,247 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui; -using Penumbra.GameData; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private interface IConstantEditor - { - bool Draw(Span values, bool disabled); - } - - private sealed class FloatConstantEditor : IConstantEditor - { - public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); - - private readonly float? _minimum; - private readonly float? _maximum; - private readonly float _speed; - private readonly float _relativeSpeed; - private readonly float _factor; - private readonly float _bias; - private readonly string _format; - - public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, - string unit) - { - _minimum = minimum; - _maximum = maximum; - _speed = speed; - _relativeSpeed = relativeSpeed; - _factor = factor; - _bias = bias; - _format = $"%.{Math.Min(precision, (byte)9)}f"; - if (unit.Length > 0) - _format = $"{_format} {unit.Replace("%", "%%")}"; - } - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var value = (values[valueIdx] - _bias) / _factor; - if (disabled) - { - ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); - } - else - { - if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, - _maximum ?? 0.0f, _format)) - { - values[valueIdx] = Clamp(value) * _factor + _bias; - ret = true; - } - } - } - - return ret; - } - - private float Clamp(float value) - => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); - } - - private sealed class IntConstantEditor : IConstantEditor - { - private readonly int? _minimum; - private readonly int? _maximum; - private readonly float _speed; - private readonly float _relativeSpeed; - private readonly float _factor; - private readonly float _bias; - private readonly string _format; - - public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) - { - _minimum = minimum; - _maximum = maximum; - _speed = speed; - _relativeSpeed = relativeSpeed; - _factor = factor; - _bias = bias; - _format = "%d"; - if (unit.Length > 0) - _format = $"{_format} {unit.Replace("%", "%%")}"; - } - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); - if (disabled) - { - ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); - } - else - { - if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, - _format)) - { - values[valueIdx] = Clamp(value) * _factor + _bias; - ret = true; - } - } - } - - return ret; - } - - private int Clamp(int value) - => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); - } - - private sealed class ColorConstantEditor : IConstantEditor - { - private readonly bool _squaredRgb; - private readonly bool _clamped; - - public ColorConstantEditor(bool squaredRgb, bool clamped) - { - _squaredRgb = squaredRgb; - _clamped = clamped; - } - - public bool Draw(Span values, bool disabled) - { - switch (values.Length) - { - case 3: - { - var value = new Vector3(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled) - return false; - - if (_squaredRgb) - value = PseudoSquareRgb(value); - if (_clamped) - value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); - value.CopyTo(values); - return true; - } - case 4: - { - var value = new Vector4(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (!ImGui.ColorEdit4("##0", ref value, - ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) - || disabled) - return false; - - if (_squaredRgb) - value = PseudoSquareRgb(value); - if (_clamped) - value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); - value.CopyTo(values); - return true; - } - default: return FloatConstantEditor.Default.Draw(values, disabled); - } - } - } - - private sealed class EnumConstantEditor : IConstantEditor - { - private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; - - public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) - => _values = values; - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - using var id = ImRaii.PushId(valueIdx); - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var currentValue = values[valueIdx]; - var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label - ?? currentValue.ToString(CultureInfo.CurrentCulture); - ret = disabled - ? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly) - : DrawCombo(currentLabel, ref values[valueIdx]); - } - - return ret; - } - - private bool DrawCombo(string label, ref float currentValue) - { - using var c = ImRaii.Combo(string.Empty, label); - if (!c) - return false; - - var ret = false; - foreach (var (valueLabel, value, valueDescription) in _values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - currentValue = value; - ret = true; - } - - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - - return ret; - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs deleted file mode 100644 index 29fd7531..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - private string[]? _shpkNames; - - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public JObject? AssociatedShpkDevkit; - - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; - - // Shader Key State - public readonly - List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); - - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; - - // Textures & Samplers - public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - - public readonly HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - public readonly - List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> - Constants)> Constants = new(16); - - // Live-Previewers - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - return _edit.FindBestMatch(defaultGamePath); - } - - public string[] GetShpkNames() - { - if (null != _shpkNames) - return _shpkNames; - - var names = new HashSet(StandardShaderPackages); - names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); - - _shpkNames = names.ToArray(); - Array.Sort(_shpkNames); - - return _shpkNames; - } - - public void LoadShpk(FullPath path) - { - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - - try - { - LoadedShpkPath = path; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); - } - catch (Exception e) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) - throw new Exception("Could not assemble ShPk dev-kit path."); - - var devkitFullPath = _edit.FindBestMatch(devkitPath); - if (!devkitFullPath.IsRooted) - throw new Exception("Could not resolve ShPk dev-kit path."); - - devkitPathName = devkitFullPath.FullName; - return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); - } - catch - { - devkitPathName = string.Empty; - return null; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - - private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class - { - if (devkit == null) - return null; - - try - { - var data = devkit[category]; - if (id.HasValue) - data = data?[id.Value.ToString()]; - - if (mayVary && (data as JObject)?["Vary"] != null) - { - var selector = BuildSelector(data!["Vary"]! - .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); - var index = (int)data["Selectors"]![selector.ToString()]!; - data = data["Items"]![index]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) - Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); - return null; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var valueSet = new HashSet(key.Values); - if (dkData != null) - valueSet.UnionWith(dkData.Values.Keys); - - var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); - var values = valueSet.Select(value => - { - if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) - return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - - return ($"0x{value:X8}", value, string.Empty); - }).ToArray(); - Array.Sort(values, (x, y) => - { - if (x.Value == key.DefaultValue) - return -1; - if (y.Value == key.DefaultValue) - return 1; - - return string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, - !hasDkLabel, values)); - } - else - foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) - ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } - - private void UpdateShaders() - { - VertexShaders.Clear(); - PixelShaders.Clear(); - if (AssociatedShpk == null) - { - ShadersKnown = false; - } - else - { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); - if (node.HasValue) - foreach (var pass in node.Value.Passes) - { - VertexShaders.Add((int)pass.VertexShader); - PixelShaders.Add((int)pass.PixelShader); - } - else - ShadersKnown = false; - } - } - } - } - - var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); - var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); - - VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; - - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, - dkData?.Description ?? string.Empty, !hasDkLabel)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - - TextureLabelWidth = 50f * UiHelpers.Scale; - - float helpWidth; - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; - } - - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (!monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, - ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - } - - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; - } - - private void UpdateConstants() - { - static List FindOrAddGroup(List<(string, List)> groups, string name) - { - foreach (var (groupName, group) in groups) - { - if (string.Equals(name, groupName, StringComparison.Ordinal)) - return group; - } - - var newGroup = new List(16); - groups.Add((name, newGroup)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - var values = Mtrl.GetConstantValues(constant); - for (var i = 0; i < values.Length; i += 4) - { - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - else - { - var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; - foreach (var shpkConstant in AssociatedShpk.MaterialParams) - { - if ((shpkConstant.ByteSize & 0x3) != 0) - continue; - - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); - var handledElements = new IndexSet(values.Length, false); - - var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); - if (dkData != null) - foreach (var dkConstant in dkData) - { - var offset = (int)dkConstant.Offset; - var length = values.Length - offset; - if (dkConstant.Length.HasValue) - length = Math.Min(length, (int)dkConstant.Length.Value); - if (length <= 0) - continue; - - var editor = dkConstant.CreateEditor(); - if (editor != null) - FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") - .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); - handledElements.AddRange(offset, length); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement:true)) - { - if ((shpkConstant.ByteOffset & 0x3) == 0) - { - var offset = shpkConstant.ByteOffset >> 2; - for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) - { - var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); - if (rangeEnd > rangeStart) - fcGroup.Add(( - $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", - constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); - } - } - else - { - for (var i = start; i < end; i += 4) - { - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - } - } - - Constants.RemoveAll(group => group.Constants.Count == 0); - Constants.Sort((x, y) => - { - if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) - return 1; - if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) - return -1; - - return string.Compare(x.Header, y.Header, StringComparison.Ordinal); - }); - // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme - foreach (var (_, group) in Constants) - { - group.Sort((x, y) => string.CompareOrdinal( - x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, - y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); - } - } - - public unsafe void BindToMaterialInstances() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - public void SetShaderPackageFlags(uint shPkFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetShaderPackageFlags(shPkFlags); - } - - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetMaterialParameter(parameterCrc, offset, value); - } - - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetSamplerFlags(samplerCrc, samplerFlags); - } - - private void UpdateMaterialPreview() - { - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); - foreach (var constant in Mtrl.ShaderPackage.Constants) - { - var values = Mtrl.GetConstantValues(constant); - if (values != null) - SetMaterialParameter(constant.Id, 0, values); - } - - foreach (var sampler in Mtrl.ShaderPackage.Samplers) - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - public void HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - return output.Write(); - } - - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } - - private sealed class DevkitShaderKey - { - public string Label = string.Empty; - public string Description = string.Empty; - public Dictionary Values = new(); - } - - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } - - private enum DevkitConstantType - { - Hidden = -1, - Float = 0, - Integer = 1, - Color = 2, - Enum = 3, - } - - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0; - } - - private sealed class DevkitConstant - { - public uint Offset = 0; - public uint? Length = null; - public string Group = string.Empty; - public string Label = string.Empty; - public string Description = string.Empty; - public DevkitConstantType Type = DevkitConstantType.Float; - - public float? Minimum = null; - public float? Maximum = null; - public float? Speed = null; - public float RelativeSpeed = 0.0f; - public float Factor = 1.0f; - public float Bias = 0.0f; - public byte Precision = 3; - public string Unit = string.Empty; - - public bool SquaredRgb = false; - public bool Clamped = false; - - public DevkitConstantValue[] Values = Array.Empty(); - - public IConstantEditor? CreateEditor() - => Type switch - { - DevkitConstantType.Hidden => null, - DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, - Unit), - DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, - Factor, Bias, Unit), - DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), - DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, - value => (value.Label, value.Value, value.Description))), - _ => FloatConstantEditor.Default, - }; - - private static int? ToInteger(float? value) - => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs deleted file mode 100644 index b9525b29..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ /dev/null @@ -1,481 +0,0 @@ -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData; -using Penumbra.String.Classes; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private readonly FileDialogService _fileDialog; - - // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' - // Apricot shader packages are unlisted because - // 1. they cause performance/memory issues when calculating the effective shader set - // 2. they probably aren't intended for use with materials anyway - private static readonly IReadOnlyList StandardShaderPackages = new[] - { - "3dui.shpk", - // "apricot_decal_dummy.shpk", - // "apricot_decal_ring.shpk", - // "apricot_decal.shpk", - // "apricot_lightmodel.shpk", - // "apricot_model_dummy.shpk", - // "apricot_model_morph.shpk", - // "apricot_model.shpk", - // "apricot_powder_dummy.shpk", - // "apricot_powder.shpk", - // "apricot_shape_dummy.shpk", - // "apricot_shape.shpk", - "bgcolorchange.shpk", - "bgcrestchange.shpk", - "bgdecal.shpk", - "bg.shpk", - "bguvscroll.shpk", - "channeling.shpk", - "characterglass.shpk", - "charactershadowoffset.shpk", - "character.shpk", - "cloud.shpk", - "createviewposition.shpk", - "crystal.shpk", - "directionallighting.shpk", - "directionalshadow.shpk", - "grass.shpk", - "hair.shpk", - "iris.shpk", - "lightshaft.shpk", - "linelighting.shpk", - "planelighting.shpk", - "pointlighting.shpk", - "river.shpk", - "shadowmask.shpk", - "skin.shpk", - "spotlighting.shpk", - "verticalfog.shpk", - "water.shpk", - "weather.shpk", - }; - - private enum TextureAddressMode : uint - { - Wrap = 0, - Mirror = 1, - Clamp = 2, - Border = 3, - } - - private static readonly IReadOnlyList TextureAddressModeTooltips = new[] - { - "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", - "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", - "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", - "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", - }; - - private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) - { - if (disabled) - { - ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); - return false; - } - - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); - if (c) - foreach (var value in tab.GetShpkNames()) - { - if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) - { - tab.Mtrl.ShaderPackage.Name = value; - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); - } - } - - return ret; - } - - private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) - { - var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - return false; - - tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; - tab.SetShaderPackageFlags((uint)shpkFlags); - return true; - } - - /// - /// Show the currently associated shpk file, if any, and the buttons to associate - /// a specific shpk from your drive, the modded shpk by path or the default shpk. - /// - private void DrawCustomAssociations(MtrlTab tab) - { - const string tooltip = "Click to copy file path to clipboard."; - var text = tab.AssociatedShpk == null - ? "Associated .shpk file: None" - : $"Associated .shpk file: {tab.LoadedShpkPathName}"; - var devkitText = tab.AssociatedShpkDevkit == null - ? "Associated dev-kit file: None" - : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; - var baseDevkitText = tab.AssociatedBaseDevkit == null - ? "Base dev-kit file: None" - : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - - ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip); - - if (ImGui.Button("Associate Custom .shpk File")) - _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => - { - if (success) - tab.LoadShpk(new FullPath(name[0])); - }, 1, Mod!.ModPath.FullName, false); - - var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(tab.LoadedShpkPath))) - tab.LoadShpk(moddedPath); - - if (!gamePath.Path.Equals(moddedPath.InternalName)) - { - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(tab.LoadedShpkPath.InternalName))) - tab.LoadShpk(new FullPath(gamePath)); - } - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - } - - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) - { - if (tab.ShaderKeys.Count == 0) - return false; - - var ret = false; - foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) - { - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - var currentValue = key.Value; - var (currentLabel, _, currentDescription) = - values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); - if (!disabled && shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) - { - if (c) - foreach (var (valueLabel, value, valueDescription) in values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - key.Value = value; - ret = true; - tab.Update(); - } - - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - } - - ImGui.SameLine(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - else if (description.Length > 0 || currentDescription.Length > 0) - { - ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", - description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); - } - else - { - ImGui.TextUnformatted($"{label}: {currentLabel}"); - } - } - - return ret; - } - - private static void DrawMaterialShaders(MtrlTab tab) - { - if (tab.AssociatedShpk == null) - return; - - ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - if (tab.ShaderComment.Length > 0) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(tab.ShaderComment); - } - } - - private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) - { - if (tab.Constants.Count == 0) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Material Constants")) - return false; - - using var _ = ImRaii.PushId("MaterialConstants"); - - var ret = false; - foreach (var (header, group) in tab.Constants) - { - using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); - if (!t) - continue; - - foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) - { - var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; - var buffer = tab.Mtrl.GetConstantValues(constant); - if (buffer.Length > 0) - { - using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); - ImGui.SetNextItemWidth(250.0f); - if (editor.Draw(buffer[slice], disabled)) - { - ret = true; - tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); - } - - ImGui.SameLine(); - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - } - } - - return ret; - } - - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) - { - var ret = false; - ref var texture = ref tab.Mtrl.Textures[textureIdx]; - ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) - { - fixed (ushort* v2 = &v) - { - return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", flags); - } - } - - static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) - { - var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); - using var c = ImRaii.Combo(label, current.ToString()); - if (!c) - return false; - - var ret = false; - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), value == current)) - { - samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); - ret = true; - } - - ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); - } - - return ret; - } - - var dx11 = texture.DX11; - if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) - { - texture.DX11 = dx11; - ret = true; - } - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) - { - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) - { - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); - - var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) - { - sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) - | ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10)); - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", - "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); - - var minLod = (int)((sampler.Flags >> 20) & 0xF); - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) - { - sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", - "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); - - using var t = ImRaii.TreeNode("Advanced Settings"); - if (!t) - return ret; - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (InputHexUInt16("Texture Flags", ref texture.Flags, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - ret = true; - - var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - sampler.Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); - } - - return ret; - } - - private bool DrawMaterialShader(MtrlTab tab, bool disabled) - { - var ret = false; - if (ImGui.CollapsingHeader(tab.ShaderHeader)) - { - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - } - - if (tab.AssociatedShpkDevkit == null) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - GC.KeepAlive(tab); - - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = - (textColor & 0xFF000000u) - | ((textColor & 0x00FEFEFE) >> 1) - | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - ImGui.TextUnformatted(tab.AssociatedShpk == null - ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." - : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); - } - - return ret; - } - - private static string? MaterialParamName(bool componentOnly, int offset) - { - if (offset < 0) - return null; - - return (componentOnly, offset & 0x3) switch - { - (true, 0) => "x", - (true, 1) => "y", - (true, 2) => "z", - (true, 3) => "w", - (false, 0) => $"[{offset >> 2:D2}].x", - (false, 1) => $"[{offset >> 2:D2}].y", - (false, 2) => $"[{offset >> 2:D2}].z", - (false, 3) => $"[{offset >> 2:D2}].w", - _ => null, - }; - } - - private static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - - private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) - { - if (valueLength == 0 || valueOffset < 0) - return (null, false); - - var firstVector = valueOffset >> 2; - var lastVector = (valueOffset + valueLength - 1) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = (valueOffset + valueLength - 1) & 0x3; - if (firstVector == lastVector) - return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); - - var sb = new StringBuilder(128); - sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); - for (var i = firstVector + 1; i < lastVector; ++i) - sb.Append($", [{i}]"); - - sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); - return (sb.ToString(), false); - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 5a8fb13a..ee883daf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,11 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Text; -using OtterGui.Widgets; -using Penumbra.GameData.Files; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -17,177 +13,10 @@ public partial class ModEditWindow private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - DrawVersionUpdate(tab, disabled); - DrawMaterialLivePreviewRebind(tab, disabled); + if (tab.DrawVersionUpdate(disabled)) + _materialTab.SaveFile(); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var ret = DrawBackFaceAndTransparency(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ret |= DrawMaterialShader(tab, disabled); - - ret |= DrawMaterialTextureChange(tab, disabled); - ret |= DrawMaterialColorTableChange(tab, disabled); - ret |= DrawMaterialConstants(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - DrawOtherMaterialDetails(tab.Mtrl, disabled); - - return !disabled && ret; - } - - private void DrawVersionUpdate(MtrlTab tab, bool disabled) - { - if (disabled || tab.Mtrl.IsDawnTrail) - return; - - if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, - "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, - new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) - return; - - tab.Mtrl.MigrateToDawntrail(); - _materialTab.SaveFile(); - } - - private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) - { - if (disabled) - return; - - if (ImGui.Button("Reload live preview")) - tab.BindToMaterialInstances(); - - if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0) - return; - - ImGui.SameLine(); - using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - ImGui.TextUnformatted( - "The current material has not been found on your character. Please check the Import from Screen tab for more information."); - } - - private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled) - { - if (tab.Textures.Count == 0) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - var frameHeight = ImGui.GetFrameHeight(); - var ret = false; - using var table = ImRaii.Table("##Textures", 3); - - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); - ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale); - foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) - { - using var _ = ImRaii.PushId(samplerI); - var tmp = tab.Mtrl.Textures[textureI].Path; - var unfolded = tab.UnfoldedTextures.Contains(samplerI); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), - new Vector2(frameHeight), - "Settings for this texture and the associated sampler", false, true)) - { - unfolded = !unfolded; - if (unfolded) - tab.UnfoldedTextures.Add(samplerI); - else - tab.UnfoldedTextures.Remove(samplerI); - } - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[textureI].Path) - { - ret = true; - tab.Mtrl.Textures[textureI].Path = tmp; - } - - ImGui.TableNextColumn(); - using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) - { - ImGui.AlignTextToFramePadding(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - - if (unfolded) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI); - ImGui.TableNextColumn(); - } - } - - return ret; - } - - private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled) - { - const uint transparencyBit = 0x10; - const uint backfaceBit = 0x01; - - var ret = false; - - using var dis = ImRaii.Disabled(disabled); - - var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0; - if (ImGui.Checkbox("Enable Transparency", ref tmp)) - { - tab.Mtrl.ShaderPackage.Flags = - tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; - ret = true; - tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); - } - - ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); - tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0; - if (ImGui.Checkbox("Hide Backfaces", ref tmp)) - { - tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; - ret = true; - tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); - } - - return ret; - } - - private static void DrawOtherMaterialDetails(MtrlFile file, bool _) - { - if (!ImGui.CollapsingHeader("Further Content")) - return; - - using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) - { - if (sets) - foreach (var set in file.UvSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) - { - if (sets) - foreach (var set in file.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (file.AdditionalData.Length <= 0) - return; - - using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); - if (t) - Widget.DrawHexViewer(file.AdditionalData); + return tab.DrawPanel(disabled); } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 55b7e748..6fb223df 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -15,6 +15,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; private readonly Dictionary _quickImportWritables = new(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0d3dce8c..f28cb632 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,10 +13,8 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; -using Penumbra.GameData.Interop; using Penumbra.Import.Models; using Penumbra.Import.Textures; -using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; @@ -26,6 +24,7 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; @@ -39,20 +38,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public readonly MigrationManager MigrationManager; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; - private readonly CommunicatorService _communicator; - private readonly IDragDropManager _dragDropManager; - private readonly IDataManager _gameData; - private readonly IFramework _framework; - private readonly ObjectManager _objects; - private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; + private readonly IDataManager _gameData; + private readonly IFramework _framework; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; @@ -541,7 +537,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. /// If no redirection is found in either of those options, returns the original path. /// - private FullPath FindBestMatch(Utf8GamePath path) + internal FullPath FindBestMatch(Utf8GamePath path) { var currentFile = _activeCollections.Current.ResolvePath(path); if (currentFile != null) @@ -562,7 +558,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return new FullPath(path); } - private HashSet FindPathsStartingWith(CiByteString prefix) + internal HashSet FindPathsStartingWith(CiByteString prefix) { var ret = new HashSet(); @@ -587,34 +583,32 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, + ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager) + ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework, + MetaDrawers metaDrawers, MigrationManager migrationManager, + MtrlTabFactory mtrlTabFactory) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _models = models; - _fileDialog = fileDialog; - _objects = objects; - _framework = framework; - _characterBaseDestructor = characterBaseDestructor; - MigrationManager = migrationManager; - _metaDrawers = metaDrawers; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _models = models; + _fileDialog = fileDialog; + _framework = framework; + MigrationManager = migrationManager; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); + (bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path)); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 4d0c62af..d135e10c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,6 +24,7 @@ public enum ColorId NoAssignment, SelectorPriority, InGameHighlight, + InGameHighlight2, ResTreeLocalPlayer, ResTreePlayer, ResTreeNetworked, @@ -70,7 +71,8 @@ public static class Colors ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), - ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ab47ce7c..41ca8d6e 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -327,6 +327,9 @@ public class SettingsTab : ITab, IUiService UiHelpers.DefaultLineSpace(); DrawModHandlingSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModEditorSettings(); ImGui.NewLine(); } @@ -723,6 +726,15 @@ public class SettingsTab : ITab, IUiService "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."); } + + /// Draw all settings pertaining to advanced editing of mods. + private void DrawModEditorSettings() + { + Checkbox("Advanced Editing: Edit Raw Tile UV Transforms", + "Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear.", + _config.EditRawTileTransforms, v => _config.EditRawTileTransforms = v); + } + #endregion /// Draw the entire Color subsection. From f4fe3605f003456a2ba7d1310c907b2d54d9547b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:42:32 +0200 Subject: [PATCH 1900/2451] DT material editor, new color tables --- .../Materials/MtrlTab.ColorTable.cs | 562 ++++++++++++++++++ .../Materials/MtrlTab.CommonColorTable.cs | 1 + 2 files changed, 563 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs new file mode 100644 index 00000000..dc87ec41 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -0,0 +1,562 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float ColorTableScalarSize = 65.0f; + + private int _colorTableSelectedPair = 0; + + private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + DrawColorTablePairSelector(table, disabled); + return DrawColorTablePairEditor(table, dyeTable, disabled); + } + + private void DrawColorTablePairSelector(ColorTable table, bool disabled) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) + { + for (var j = 0; j < 8; ++j) + { + var pairIndex = i + j; + using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + { + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + _colorTableSelectedPair = pairIndex; + } + + var rcMin = ImGui.GetItemRectMin() + framePadding; + var rcMax = ImGui.GetItemRectMax() - framePadding; + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 }, + rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing }, + rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight }, rcMax, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)) + ); + if (j < 7) + ImGui.SameLine(); + + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f }); + font.Pop(); + ColorTableHighlightButton(pairIndex, disabled); + font.Push(UiBuilder.MonoFont); + ImGui.SetCursorScreenPos(cursor); + } + } + } + + private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + var retA = false; + var retB = false; + ref var rowA = ref table[_colorTableSelectedPair << 1]; + ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; + var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; + var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; + var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; + var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); + var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); + ImUtf8.SameLineInner(); + retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); + columns.Next(); + ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); + ImUtf8.SameLineInner(); + retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); + } + + DrawHeader(" Colors"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("ColorsA"u8)) + retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("ColorsB"u8)) + retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Physical Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("PbrA"u8)) + retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("PbrB"u8)) + retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Sheen Layer Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("SheenA"u8)) + retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("SheenB"u8)) + retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Pair Blending"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("BlendingA"u8)) + retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("BlendingB"u8)) + retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Material Template"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("TemplateA"u8)) + retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("TemplateB"u8)) + retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (dyeTable != null) + { + DrawHeader(" Dye Properties"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("DyeA"u8)) + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("DyeB"u8)) + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + } + + DrawHeader(" Further Content"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("FurtherA"u8)) + retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("FurtherB"u8)) + retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (retA) + UpdateColorTableRowPreview(_colorTableSelectedPair << 1); + if (retB) + UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1); + + return retA | retB; + } + + /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. + private static void DrawHeader(ReadOnlySpan label) + { + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); + } + + private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeDiffuseColor"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewDiffuseColor"u8, "Dye Preview for Diffuse Color"u8, dyePack?.DiffuseColor); + } + + ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewSpecularColor"u8, "Dye Preview for Specular Color"u8, dyePack?.SpecularColor); + } + + ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewEmissiveColor"u8, "Dye Preview for Emissive Color"u8, dyePack?.EmissiveColor); + } + + return ret; + } + + private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var isRowB = (rowIdx & 1) != 0; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, + v => table[rowIdx].Anisotropy = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + b => dyeTable[rowIdx].Anisotropy = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + } + + return ret; + } + + private bool DrawTemplate(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; + var subcolWidth = CalculateSubcolumnWidth(2); + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, + v => table[rowIdx].ShaderId = v); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false, + v => table[rowIdx].SphereMapIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Sphere Map"u8); + if (dyeTable != null) + { + var textRectMin = ImGui.GetItemRectMin(); + var textRectMax = ImGui.GetItemRectMax(); + ImGui.SameLine(dyeOffset); + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - ImGui.GetFrameHeight() * 0.5f }); + ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex, + b => dyeTable[rowIdx].SphereMapIndex = b); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + using var dis = ImRaii.Disabled(); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + } + + ImGui.Dummy(new Vector2(64.0f, 0.0f)); + ImGui.SameLine(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask, + b => dyeTable[rowIdx].SphereMapMask = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); + ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); + ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, + v => table[rowIdx].TileIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Tile"u8); + + ImGui.SameLine(subcolWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); + using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + { + ImGui.Dummy(new Vector2(scalarSize, 0.0f)); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, + m => table[rowIdx].TileTransform = m); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImUtf8.Text("Tile Transform"u8); + } + + return ret; + } + + private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Roughness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness, + b => dyeTable[rowIdx].Roughness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Metalness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewMetalness"u8, "Dye Preview for Metalness"u8, (float?)dyePack?.Metalness * 100.0f, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate, + b => dyeTable[rowIdx].SheenRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, + b => dyeTable[rowIdx].SheenTintRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenTintRate"u8, "Dye Preview for Sheen Tint"u8, (float?)dyePack?.SheenTintRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture, + b => dyeTable[rowIdx].SheenAperture = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar11 = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar3 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar7 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar15 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar17 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar20 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar22 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #23"u8, default, row.Scalar23, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar23 = v); + + return ret; + } + + private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; + var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); + + var ret = false; + ref var dye = ref dyeTable[rowIdx]; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + ImUtf8.SameLineInner(); + ImUtf8.Text("Dye Template"u8); + ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); + using var dis = ImRaii.Disabled(!dyePack.HasValue); + if (ImUtf8.Button("Apply Preview Dye"u8)) + { + ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + } + + return ret; + } + + private static void CenteredTextInRest(string text) + => AlignedTextInRest(text, 0.5f); + + private static void AlignedTextInRest(string text, float alignment) + { + var width = ImGui.CalcTextSize(text).X; + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((ImGui.GetContentRegionAvail().X - width) * alignment, 0.0f)); + ImGui.TextUnformatted(text); + } + + private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + { + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 937614de..2b093e23 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -52,6 +52,7 @@ public partial class MtrlTab { LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), _ => false, }; From 5323add662874d3b0286f8ed35930b620fd99630 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:45:52 +0200 Subject: [PATCH 1901/2451] Improve ShPk tab --- .../ModEditWindow.ShaderPackages.cs | 287 ++++++++++++++---- .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 287 ++++++++++++++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 3 files changed, 494 insertions(+), 82 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 017478a7..8a1c729c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImGuiNET; -using Lumina.Misc; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; @@ -11,6 +10,9 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; +using Penumbra.GameData.Files.ShaderStructs; +using OtterGui.Text; +using Penumbra.GameData.Structs; namespace Penumbra.UI.AdvancedWindow; @@ -24,6 +26,9 @@ public partial class ModEditWindow { DrawShaderPackageSummary(file); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageFilterSection(file); + var ret = false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); @@ -50,15 +55,16 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { + if (tab.Shpk.IsLegacy) + { + ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } ImGui.TextUnformatted(tab.Header); if (!tab.Shpk.Disassembled) { - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing."); + ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red } } @@ -123,6 +129,7 @@ public partial class ModEditWindow { shaders[idx].UpdateResources(tab.Shpk); tab.Shpk.UpdateResources(); + tab.UpdateFilteredUsed(); } catch (Exception e) { @@ -149,6 +156,97 @@ public partial class ModEditWindow ImGuiInputTextFlags.ReadOnly, null, null); } + private static void DrawShaderUsage(ShpkTab tab, Shader shader) + { + using (var node = ImUtf8.TreeNode("Used with Shader Keys"u8)) + { + if (node) + { + foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } + } + + ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + private static void DrawShaderPackageFilterSection(ShpkTab tab) + { + if (!ImUtf8.CollapsingHeader(tab.FilterPopCount == tab.FilterMaximumPopCount ? "Filters###Filters"u8 : "Filters (ACTIVE)###Filters"u8)) + return; + + foreach (var (key, keyIdx) in tab.Shpk.SystemKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"System Key {tab.TryResolveName(key.Id)}", ref tab.FilterSystemValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.SceneKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Scene Key {tab.TryResolveName(key.Id)}", ref tab.FilterSceneValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.MaterialKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Material Key {tab.TryResolveName(key.Id)}", ref tab.FilterMaterialValues[keyIdx]); + + foreach (var (_, keyIdx) in tab.Shpk.SubViewKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Sub-View Key #{keyIdx}", ref tab.FilterSubViewValues[keyIdx]); + + DrawShaderPackageFilterSet(tab, "Passes", ref tab.FilterPasses); + } + + private static void DrawShaderPackageFilterSet(ShpkTab tab, string label, ref SharedSet values) + { + if (values.PossibleValues == null) + { + ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + return; + } + + using var node = ImRaii.TreeNode(label); + if (!node) + return; + + foreach (var value in values.PossibleValues) + { + var contains = values.Contains(value); + if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + continue; + if (contains) + { + if (values.AddExisting(value)) + { + ++tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + else + { + if (values.Remove(value)) + { + --tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + } + } + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) @@ -157,8 +255,11 @@ public partial class ModEditWindow var ret = false; for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[idx]; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + var shader = shaders[idx]; + if (!tab.IsFilterMatch(shader)) + continue; + + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -169,9 +270,11 @@ public partial class ModEditWindow DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true); - ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); @@ -187,12 +290,14 @@ public partial class ModEditWindow if (tab.Shpk.Disassembled) DrawRawDisassembly(shader); + + DrawShaderUsage(tab, shader); } return ret; } - private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled) + private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool hasFilter, bool disabled) { var ret = false; if (!disabled) @@ -205,16 +310,26 @@ public partial class ModEditWindow if (resource.Used == null) return ret; - var usedString = UsedComponentString(withSize, resource); + var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) - ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + if (hasFilter) + { + var filteredUsedString = UsedComponentString(withSize, true, resource); + if (filteredUsedString.Length > 0) + ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + else + ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } else - ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode(hasFilter ? "Globally Unused" : "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return ret; } - private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, bool disabled) { if (resources.Length == 0) return false; @@ -233,7 +348,7 @@ public partial class ModEditWindow using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); font.Dispose(); if (t2) - ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled); + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } return ret; @@ -268,7 +383,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { ImGui.TextUnformatted(tab.Shpk.Disassembled - ? "Parameter positions (continuations are grayed out, unused values are red):" + ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); using var table = ImRaii.Table("##MaterialParamLayout", 5, @@ -276,17 +391,17 @@ public partial class ModEditWindow if (!table) return false; - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale); - ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 40 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); ImGui.TableHeadersRow(); var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); - var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity - var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red - var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1); + var textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity + var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red + var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -296,14 +411,13 @@ public partial class ModEditWindow for (var j = 0; j < 4; ++j) { var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; - var color = colorType switch - { - ShpkTab.ColorType.Unused => textColorUnusedStart, - ShpkTab.ColorType.Used => textColorStart, - ShpkTab.ColorType.Continuation => textColorUnusedCont, - ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, - _ => textColorStart, - }; + var color = textColorStart; + if (!colorType.HasFlag(ShpkTab.ColorType.Used)) + color = ImGuiUtil.HalfBlend(color, 0x80u); // Half red + else if (!colorType.HasFlag(ShpkTab.ColorType.FilteredUsed)) + color = ImGuiUtil.HalfBlend(color, 0x8080u); // Half yellow + if (colorType.HasFlag(ShpkTab.ColorType.Continuation)) + color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) @@ -331,6 +445,35 @@ public partial class ModEditWindow return ret; } + private static void DrawShaderPackageMaterialDevkitExport(ShpkTab tab) + { + if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) + return; + + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + + void DoSave(bool success, string path) + { + if (!success) + return; + + try + { + File.WriteAllText(path, tab.ExportDevkit().ToString()); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, $"Could not export dev-kit for {Path.GetFileName(tab.FilePath)} to {path}.", + NotificationType.Error, false); + return; + } + + Penumbra.Messager.NotificationMessage( + $"Material dev-kit file for {Path.GetFileName(tab.FilePath)} exported successfully to {Path.GetFileName(path)}.", + NotificationType.Success, false); + } + } + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); @@ -396,23 +539,25 @@ public partial class ModEditWindow DrawShaderPackageEndCombo(tab); ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) - tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); + var newName = tab.NewMaterialParamName.Value!; + if (ImGui.InputText("Name", ref newName, 63)) + tab.NewMaterialParamName = newName; - var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) ? "The ID is already in use. Please choose a different name." : string.Empty; - if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), + if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip, tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { - Id = tab.NewMaterialParamId, + Id = tab.NewMaterialParamName.Crc32, ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), }); + tab.AddNameToCache(tab.NewMaterialParamName); tab.Update(); return true; } @@ -434,6 +579,9 @@ public partial class ModEditWindow else if (!disabled && sizeWellDefined) ret |= DrawShaderPackageNewParameter(tab); + if (tab.Shpk.Disassembled) + DrawShaderPackageMaterialDevkitExport(tab); + return ret; } @@ -444,14 +592,17 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Shader Resources")) return false; - ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled); - ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled); + var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } - private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) + private static void DrawKeyArray(ShpkTab tab, string arrayName, bool withId, IReadOnlyCollection keys) { if (keys.Count == 0) return; @@ -463,12 +614,11 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}"); + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); if (t2) { - ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } @@ -482,39 +632,46 @@ public partial class ModEditWindow if (!t) return; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex()) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + if (!tab.IsFilterMatch(node)) + continue; + + using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); if (!t2) continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) { - ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) { - ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) { - ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) - ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImRaii.TreeNode($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImRaii.TreeNode($"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -526,10 +683,10 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Shader Selection")) return; - DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys); - DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys); - DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys); - DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys); + DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray(tab, "Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray(tab, "Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); @@ -559,12 +716,14 @@ public partial class ModEditWindow } } - private static string UsedComponentString(bool withSize, in Resource resource) + private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { + var used = filtered ? resource.FilteredUsed : resource.Used; + var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; var sb = new StringBuilder(256); if (withSize) { - foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) + foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) { switch (components) { @@ -582,7 +741,7 @@ public partial class ModEditWindow } } - switch (resource.UsedDynamically ?? 0) + switch (usedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: @@ -590,7 +749,7 @@ public partial class ModEditWindow break; default: sb.Append("[*]."); - foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + foreach (var c in usedDynamically!.Value.ToString().Where(char.IsUpper)) sb.Append(char.ToLower(c)); sb.Append(", "); @@ -599,7 +758,7 @@ public partial class ModEditWindow } else { - var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + var components = (used is { Length: > 0 } ? used[0] : 0) | (usedDynamically ?? 0); if ((components & DisassembledShader.VectorComponents.X) != 0) sb.Append("Red, "); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 12b8d761..de20aa9f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,9 +1,12 @@ using Dalamud.Utility; -using Lumina.Misc; +using Newtonsoft.Json.Linq; using OtterGui; -using Penumbra.GameData.Data; +using OtterGui.Classes; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -12,18 +15,27 @@ public partial class ModEditWindow private class ShpkTab : IWritable { public readonly ShpkFile Shpk; + public readonly string FilePath; - public string NewMaterialParamName = string.Empty; - public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu); - public short NewMaterialParamStart; - public short NewMaterialParamEnd; + public Name NewMaterialParamName = string.Empty; + public short NewMaterialParamStart; + public short NewMaterialParamEnd; + + public SharedSet[] FilterSystemValues; + public SharedSet[] FilterSceneValues; + public SharedSet[] FilterMaterialValues; + public SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; + + public readonly int FilterMaximumPopCount; + public int FilterPopCount; public readonly FileDialogService FileDialog; public readonly string Header; public readonly string Extension; - public ShpkTab(FileDialogService fileDialog, byte[] bytes) + public ShpkTab(FileDialogService fileDialog, byte[] bytes, string filePath) { FileDialog = fileDialog; try @@ -34,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch @@ -42,15 +55,36 @@ public partial class ModEditWindow ShpkFile.DxVersion.DirectX11 => ".dxbc", _ => throw new NotImplementedException(), }; + + FilterSystemValues = Array.ConvertAll(Shpk.SystemKeys, key => key.Values.FullSet()); + FilterSceneValues = Array.ConvertAll(Shpk.SceneKeys, key => key.Values.FullSet()); + FilterMaterialValues = Array.ConvertAll(Shpk.MaterialKeys, key => key.Values.FullSet()); + FilterSubViewValues = Array.ConvertAll(Shpk.SubViewKeys, key => key.Values.FullSet()); + FilterPasses = Shpk.Passes.FullSet(); + + FilterMaximumPopCount = FilterPasses.Count; + foreach (var key in Shpk.SystemKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SceneKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.MaterialKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SubViewKeys) + FilterMaximumPopCount += key.Values.Count; + + FilterPopCount = FilterMaximumPopCount; + + UpdateNameCache(); + Shpk.UpdateFilteredUsed(IsFilterMatch); Update(); } [Flags] public enum ColorType : byte { - Unused = 0, Used = 1, - Continuation = 2, + FilteredUsed = 2, + Continuation = 4, } public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; @@ -58,10 +92,87 @@ public partial class ModEditWindow public readonly HashSet UsedIds = new(16); public readonly List<(string Name, short Index)> Orphans = new(16); + private readonly Dictionary _nameCache = []; + private readonly Dictionary, string> _nameSetCache = []; + private readonly Dictionary, string> _nameSetWithIdsCache = []; + + public void AddNameToCache(Name name) + { + if (name.Value != null) + _nameCache.TryAdd(name.Crc32, name); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + public void UpdateNameCache() + { + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } + + static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) + { + foreach (var key in keys) + { + var keyName = nameCache.TryResolve(Names.KnownNames, key.Id); + var valueNames = keyName.WithKnownSuffixes(); + foreach (var value in key.Values) + { + var valueName = valueNames.TryResolve(value); + if (valueName.Value != null) + nameCache.TryAdd(value, valueName); + } + } + } + + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Name TryResolveName(uint crc32) + => _nameCache.TryResolve(Names.KnownNames, crc32); + + public string NameSetToString(SharedSet nameSet, bool withIds = false) + { + var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; + if (cache.TryGetValue(nameSet, out var nameSetStr)) + return nameSetStr; + if (withIds) + nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); + else + nameSetStr = string.Join(", ", nameSet.Select(TryResolveName)); + cache.Add(nameSet, nameSetStr); + return nameSetStr; + } + + public void UpdateFilteredUsed() + { + Shpk.UpdateFilteredUsed(IsFilterMatch); + + var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + UpdateColors(materialParams); + } + public void Update() { var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4; + var defaults = Shpk.MaterialParamsDefaults != null ? (ReadOnlySpan)Shpk.MaterialParamsDefaults : []; + var defaultFloats = MemoryMarshal.Cast(defaults); Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; MalformedParameters.Clear(); @@ -75,14 +186,14 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } if (iEnd >= numParameters) { MalformedParameters.Add( - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})"); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"); continue; } @@ -91,9 +202,12 @@ public partial class ModEditWindow var end = i == iEnd ? jEnd : 3; for (var j = i == iStart ? jStart : 0; j <= end; ++j) { + var component = (i << 2) | j; var tt = - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})"; - Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; + if (component < defaultFloats.Length) + tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } } @@ -151,7 +265,7 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); } } @@ -168,11 +282,15 @@ public partial class ModEditWindow { var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All) | (materialParams?.UsedDynamically ?? 0); + var filteredUsedComponents = (materialParams?.FilteredUsed?[i] ?? DisassembledShader.VectorComponents.All) + | (materialParams?.FilteredUsedDynamically ?? 0); for (var j = 0; j < 4; ++j) { - var color = ((byte)usedComponents & (1 << j)) != 0 - ? ColorType.Used - : 0; + ColorType color = 0; + if (((byte)usedComponents & (1 << j)) != 0) + color |= ColorType.Used; + if (((byte)filteredUsedComponents & (1 << j)) != 0) + color |= ColorType.FilteredUsed; if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0) color |= ColorType.Continuation; @@ -182,6 +300,141 @@ public partial class ModEditWindow } } + public bool IsFilterMatch(ShpkFile.Shader shader) + { + if (!FilterPasses.Overlaps(shader.Passes)) + return false; + + for (var i = 0; i < shader.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(shader.SystemValues[i])) + return false; + } + + for (var i = 0; i < shader.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(shader.SceneValues[i])) + return false; + } + + for (var i = 0; i < shader.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(shader.MaterialValues[i])) + return false; + } + + for (var i = 0; i < shader.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(shader.SubViewValues[i])) + return false; + } + + return true; + } + + public bool IsFilterMatch(ShpkFile.Node node) + { + if (!node.Passes.Any(pass => FilterPasses.Contains(pass.Id))) + return false; + + for (var i = 0; i < node.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(node.SystemValues[i])) + return false; + } + + for (var i = 0; i < node.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(node.SceneValues[i])) + return false; + } + + for (var i = 0; i < node.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(node.MaterialValues[i])) + return false; + } + + for (var i = 0; i < node.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(node.SubViewValues[i])) + return false; + } + + return true; + } + + /// + /// Generates a minimal material dev-kit file for the given shader package. + /// + /// This file currently only hides globally unused material constants. + /// + public JObject ExportDevkit() + { + var devkit = new JObject(); + + var maybeMaterialParameter = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + if (maybeMaterialParameter.HasValue) + { + var materialParameter = maybeMaterialParameter.Value; + var materialParameterUsage = new IndexSet(materialParameter.Size << 2, true); + + var used = materialParameter.Used ?? []; + var usedDynamically = materialParameter.UsedDynamically ?? 0; + for (var i = 0; i < used.Length; ++i) + { + for (var j = 0; j < 4; ++j) + { + if (!(used[i] | usedDynamically).HasFlag((DisassembledShader.VectorComponents)(1 << j))) + materialParameterUsage[(i << 2) | j] = false; + } + } + + var dkConstants = new JObject(); + foreach (var param in Shpk.MaterialParams) + { + // Don't handle misaligned parameters. + if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) + continue; + + var start = param.ByteOffset >> 2; + var length = param.ByteSize >> 2; + + // If the parameter is fully used, don't include it. + if (!materialParameterUsage.Indices(start, length, true).Any()) + continue; + + var unusedSlices = new JArray(); + + if (materialParameterUsage.Indices(start, length).Any()) + { + foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + ["Offset"] = rgStart, + ["Length"] = rgEnd - rgStart, + }); + } + } + else + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + }); + } + + dkConstants[param.Id.ToString()] = unusedSlices; + } + + devkit["Constants"] = dkConstants; + } + + return devkit; + } + public bool Valid => Shpk.Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f28cb632..13458252 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -615,7 +615,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _shaderPackageTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); + (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; From c01aa000fb94afcbfc13fcfe87c4ab6bd5b5f86d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:46:29 +0200 Subject: [PATCH 1902/2451] Optimize I/O of ShPk for ResourceTree generation --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 11 ++++++++--- Penumbra/Interop/ResourceTree/TreeBuildCache.cs | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..41485d75 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -230,8 +230,8 @@ internal unsafe partial record ResolveContext( node.Children.Add(shpkNode); } - var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -255,7 +255,12 @@ internal unsafe partial record ResolveContext( alreadyProcessedSamplerIds.Add(samplerId.Value); var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); if (samplerCrc.HasValue) - name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + { + if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName)) + name = samplerName.Value; + else + name = $"Texture 0x{samplerCrc.Value:X8}"; + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index ca5ff736..49e00547 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,8 +1,11 @@ +using System.IO.MemoryMappedFiles; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.String.Classes; @@ -11,7 +14,7 @@ namespace Penumbra.Interop.ResourceTree; internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) { - private readonly Dictionary _shaderPackages = []; + private readonly Dictionary?> _shaderPackageNames = []; public unsafe bool IsLocalPlayerRelated(ICharacter character) { @@ -68,10 +71,10 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data } /// Try to read a shpk file from the given path and cache it on success. - public ShpkFile? ReadShaderPackage(FullPath path) - => ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); + public IReadOnlyDictionary? ReadShaderPackageNames(FullPath path) + => ReadFile(dataManager, path, _shaderPackageNames, bytes => ShpkFile.FastExtractNames(bytes.Span)); - private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func parseFile) + private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func, T> parseFile) where T : class { if (path.FullName.Length == 0) @@ -86,7 +89,8 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data { if (path.IsRooted) { - parsed = parseFile(File.ReadAllBytes(pathStr)); + using var mmFile = MmioMemoryManager.CreateFromFile(pathStr, access: MemoryMappedFileAccess.Read); + parsed = parseFile(mmFile.Memory); } else { From c849e310343b465d88619f05e9b30384d1caa709 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:48:42 +0200 Subject: [PATCH 1903/2451] RT: Use SpanTextWriter to assemble paths --- .../ResolveContext.PathResolution.cs | 28 +++++++++++++------ .../Interop/ResourceTree/ResolveContext.cs | 25 +++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 85b3284a..b99468f8 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -8,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.Interop.Structs.StructExtensions; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -95,7 +97,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -125,7 +127,7 @@ internal partial record ResolveContext fileName.CopyTo(mirroredFileName); WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); @@ -144,7 +146,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -175,13 +177,21 @@ internal partial record ResolveContext var baseDirectory = modelPath[..modelPosition]; - baseDirectory.CopyTo(materialPathBuffer); - "/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; - mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); + var writer = new SpanTextWriter(materialPathBuffer); + writer.Append(baseDirectory); + writer.Append("/material/v"u8); + WriteZeroPaddedNumber(ref writer, 4, variant); + writer.Append((byte)'/'); + writer.Append(mtrlFileName); + writer.EnsureNullTerminated(); - return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; + return materialPathBuffer[..writer.Position]; + } + + private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number) + { + WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number); + writer.Advance(width); } private static void WriteZeroPaddedNumber(Span destination, ushort number) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..29e15055 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using OtterGui; +using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Data; @@ -16,7 +16,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; using static Penumbra.Interop.Structs.StructExtensions; -using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; namespace Penumbra.Interop.ResourceTree; @@ -29,25 +29,25 @@ internal record GlobalResolveContext( { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, + public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } internal unsafe partial record ResolveContext( GlobalResolveContext Global, - Pointer CharacterBasePointer, + Pointer CharacterBasePointer, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { - public CharacterBase* CharacterBase + public CharaBase* CharacterBase => CharacterBasePointer.Value; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ModelType ModelType + private CharaBase.ModelType ModelType => CharacterBase->GetModelType(); private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) @@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext( if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) return null; - Span prefixed = stackalloc byte[260]; - gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); - prefixed[lastDirectorySeparator + 1] = (byte)'-'; - prefixed[lastDirectorySeparator + 2] = (byte)'-'; - gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + Span prefixed = stackalloc byte[CharaBase.PathBufferSize]; + + var writer = new SpanTextWriter(prefixed); + writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]); + writer.Append((byte)'-'); + writer.Append((byte)'-'); + writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]); + writer.EnsureNullTerminated(); if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; From 243593e30f74c43a14b7d0ccdd9a264830158a59 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:52:39 +0200 Subject: [PATCH 1904/2451] RT: Fix VPR offhand material paths --- .../ResolveContext.PathResolution.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b99468f8..c3894b05 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -111,31 +111,25 @@ internal partial record ResolveContext if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; - // MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand - if (setIdHigh is 3 or 16 or 18 or 26) + // Some offhands share materials with the corresponding mainhand + if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) { - var setIdLow = Equipment.Set.Id % 100; - if (setIdLow > 50) - { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var mirroredSetId = (ushort)(Equipment.Set.Id - 50); + Span mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id); - Span mirroredFileName = stackalloc byte[32]; - mirroredFileName = mirroredFileName[..fileName.Length]; - fileName.CopyTo(mirroredFileName); - WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); - Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); + if (weaponPosition >= 0) + WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id); - var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); - if (weaponPosition >= 0) - WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); From 75e3ef72f3dbaff37db0ba18d2770a5e7885f3ae Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 20:27:16 +0200 Subject: [PATCH 1905/2451] RT: Fix Facewear --- .../ResolveContext.PathResolution.cs | 16 ++++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 ++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index c3894b05..43324516 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -16,20 +16,26 @@ namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { + private static bool IsEquipmentOrAccessorySlot(uint slotIndex) + => slotIndex is < 10 or 16 or 17; + + private static bool IsEquipmentSlot(uint slotIndex) + => slotIndex is < 5 or 16 or 17; + private Utf8GamePath ResolveModelPath() { // Correctness: // Resolving a model path through the game's code can use EQDP metadata for human equipment models. return ModelType switch { - ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), - _ => ResolveModelPathNative(), + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(), + _ => ResolveModelPathNative(), }; } private Utf8GamePath ResolveEquipmentModelPath() { - var path = SlotIndex < 5 + var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; @@ -41,7 +47,7 @@ internal partial record ResolveContext private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { var slotIndex = slot.ToIndex(); - if (slotIndex >= 10 || ModelType != ModelType.Human) + if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; @@ -82,7 +88,7 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..f1507294 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -44,8 +45,8 @@ public class ResourceTree PlayerRelated = playerRelated; CollectionName = collectionName; AnonymizedCollectionName = anonymizedCollectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Nodes = []; + FlatNodes = []; } public void ProcessPostfix(Action action) @@ -59,13 +60,13 @@ public class ResourceTree var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; var modelType = model->GetModelType(); - var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; + var human = modelType == ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + ModelType.Human => new ReadOnlySpan(&human->Head, 12), + ModelType.DemiHuman => new ReadOnlySpan( Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + _ => [], }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -75,9 +76,18 @@ public class ResourceTree for (var i = 0u; i < model->SlotCount; ++i) { - var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) - : globalContext.CreateContext(model, i); + var slotContext = modelType switch + { + ModelType.Human => i switch + { + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), + 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), + _ => globalContext.CreateContext(model, i), + }, + _ => i < equipment.Length + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i), + }; var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); @@ -117,7 +127,7 @@ public class ResourceTree var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) + if (subObject->GetModelType() != ModelType.Weapon) continue; var weapon = (Weapon*)subObject; From da3f3b8df39c24a84b92d34ee730e7ffc74abe84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Aug 2024 22:45:38 +0200 Subject: [PATCH 1906/2451] Start rework of identified objects. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/CollectionApi.cs | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Api/Api/UiApi.cs | 10 +- .../Api/IpcTester/CollectionsIpcTester.cs | 16 +- Penumbra/Collections/Cache/CollectionCache.cs | 25 +- .../Collections/ModCollection.Cache.Access.cs | 6 +- Penumbra/Communication/ChangedItemClick.cs | 3 +- Penumbra/Communication/ChangedItemHover.cs | 3 +- Penumbra/EphemeralConfig.cs | 36 +- .../Interop/ResourceTree/ResolveContext.cs | 12 +- Penumbra/Interop/ResourceTree/ResourceNode.cs | 12 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 6 +- .../ResourceTree/ResourceTreeApiHelper.cs | 4 +- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 2 +- .../Manipulations/GlobalEqpManipulation.cs | 2 +- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- .../Meta/Manipulations/IMetaIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 4 +- Penumbra/Meta/Manipulations/Rsp.cs | 3 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 5 +- Penumbra/Mods/Mod.cs | 3 +- Penumbra/Penumbra.cs | 9 +- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 20 +- Penumbra/UI/ChangedItemDrawer.cs | 354 +++++------------- Penumbra/UI/ChangedItemIconFlag.cs | 122 ++++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 11 +- .../UI/ModsTab/ModSearchStringSplitter.cs | 12 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 9 +- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 10 +- 41 files changed, 342 insertions(+), 389 deletions(-) create mode 100644 Penumbra/UI/ChangedItemIconFlag.cs diff --git a/Penumbra.Api b/Penumbra.Api index 86249598..759a8e9d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1 +Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0 diff --git a/Penumbra.GameData b/Penumbra.GameData index 75582ece..44427ad0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c +Subproject commit 44427ad0149059ab5ccb4e4a2f42a1a43423e4c5 diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index ff393aaf..04299187 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -36,7 +36,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : collection = ModCollection.Empty; if (collection.HasCache) - return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject()); Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded."); return []; diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 60b00d37..790121d5 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -134,6 +134,6 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public Dictionary GetChangedItems(string modDirectory, string modName) => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject()) : []; } diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index cf3cd8f2..515874c0 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -1,7 +1,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, object? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) { if (ChangedItemClicked == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(object? data) + private void OnChangedItemHover(IIdentifiedObjectData? data) { if (ChangedItemTooltip == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 026fabbc..1d516eba 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -8,7 +8,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using ImGuiClip = OtterGui.ImGuiClip; namespace Penumbra.Api.IpcTester; @@ -17,10 +17,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService { private int _objectIdx; private string _collectionIdString = string.Empty; - private Guid? _collectionId = null; - private bool _allowCreation = true; - private bool _allowDeletion = true; - private ApiCollectionType _type = ApiCollectionType.Yourself; + private Guid? _collectionId; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Yourself; private Dictionary _collections = []; private (string, ChangedItemType, uint)[] _changedItems = []; @@ -116,7 +116,7 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty)); _changedItems = items.Select(kvp => { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value); + var (type, id) = kvp.Value.ToApiObject(); return (kvp.Key, type, id); }).ToArray(); ImGui.OpenPopup("Changed Item List"); @@ -130,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService if (!p) return; - using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) + using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) { - if (t) + if (table) ImGuiClip.ClippedDraw(_changedItems, t => { ImGuiUtil.DrawTableColumn(t.Item1); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4755840e..abc0dff8 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Util; +using Penumbra.GameData.Data; namespace Penumbra.Collections.Cache; @@ -18,14 +19,14 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// public sealed class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly CustomResourceCache CustomResources; - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly CustomResourceCache CustomResources; + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -41,7 +42,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems { get { @@ -412,7 +413,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { @@ -421,8 +422,8 @@ public sealed class CollectionCache : IDisposable if (!_changedItems.TryGetValue(name, out var data)) _changedItems.Add(name, (new SingleArray(mod), obj)); else if (!data.Item1.Contains(mod)) - _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj); - else if (obj is int x && data.Item2 is int y) + _changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); + else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y) _changedItems[name] = (data.Item1, x + y); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 983509a4..0b38dde8 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,8 +1,8 @@ using OtterGui.Classes; using Penumbra.Mods; -using Penumbra.Meta.Files; using Penumbra.String.Classes; using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; using Penumbra.Mods.Editor; namespace Penumbra.Collections; @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, object?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, object?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 554e2221..1aac4454 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Api.Api; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -11,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 2dcced35..4e72b558 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Api; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -9,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 7457c910..24ab466b 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -23,24 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable, IService [JsonIgnore] private readonly ModPathChanged _modPathChanged; - public int Version { get; set; } = Configuration.Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; - public bool DebugSeparateWindow { get; set; } = false; - public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; - public bool FixMainWindow { get; set; } = false; - public string LastModPath { get; set; } = string.Empty; - public bool AdvancedEditingOpen { get; set; } = false; - public bool ForceRedrawOnFileChange { get; set; } = false; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; + public bool FixMainWindow { get; set; } = false; + public string LastModPath { get; set; } = string.Empty; + public bool AdvancedEditingOpen { get; set; } = false; + public bool ForceRedrawOnFileChange { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..9d0f1e46 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -345,7 +345,7 @@ internal unsafe partial record ResolveContext( _ => string.Empty, } + item.Name; - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } var dataFromPath = GuessUiDataFromPath(gamePath); @@ -353,8 +353,8 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) - : new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath) @@ -362,13 +362,13 @@ internal unsafe partial record ResolveContext( foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; - if (name.StartsWith("Customization:")) + if (obj.Value is IdentifiedCustomization) name = name[14..].Trim(); if (name != "Unknown") - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } - return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index de43a874..6ab48325 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,7 +1,7 @@ using Penumbra.Api.Enums; using Penumbra.String; using Penumbra.String.Classes; -using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; @@ -9,7 +9,7 @@ public class ResourceNode : ICloneable { public string? Name; public string? FallbackName; - public ChangedItemIcon Icon; + public ChangedItemIconFlag IconFlag; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -51,7 +51,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - Icon = other.Icon; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -79,7 +79,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { Name = uiData.Name; - Icon = uiData.Icon; + IconFlag = uiData.IconFlag; } public void PrependName(string prefix) @@ -88,9 +88,9 @@ public class ResourceNode : ICloneable Name = prefix + Name; } - public readonly record struct UiData(string? Name, ChangedItemIcon Icon) + public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, Icon); + => Name == null ? this : new UiData(prefix + Name, IconFlag); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..dc83fa65 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -174,7 +174,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -192,7 +192,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -209,7 +209,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 22025dd6..48690e98 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -67,7 +67,7 @@ internal static class ResourceTreeApiHelper continue; var fullPath = node.FullPath.ToPath(); - resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon))); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)node.IconFlag.ToApiIcon())); } } @@ -106,7 +106,7 @@ internal static class ResourceTreeApiHelper var ret = new JObject { [nameof(ResourceNodeDto.Type)] = new JValue(node.Type), - [nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)), + [nameof(ResourceNodeDto.Icon)] = new JValue(node.IconFlag.ToApiIcon()), [nameof(ResourceNodeDto.Name)] = node.Name, [nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), [nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(), diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 3f856bd2..3a804d0c 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 5d37aac8..f758126c 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 2955dba4..cfe9b7d4 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,7 +24,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 2b88d962..ec59762b 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index a6fcf58b..1f41adfb 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 5707ffca..d1668a4d 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -18,7 +18,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index d4887fe2..1b2492ee 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 73d1d7e5..2d73ec7f 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,4 +1,3 @@ -using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -9,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 9327ced9..c5654019 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -45,7 +45,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 7b0eb094..d42804ba 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -121,7 +121,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index ee27d534..9cf7e6a3 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -126,7 +126,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index cc606f42..723cd5b1 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -111,7 +111,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 2c292a14..1a2f2798 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -186,7 +186,7 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, @@ -255,7 +255,8 @@ public static class EquipmentSwap { items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType() + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) + .Select(kvp => kvp.Value).OfType().Select(i => i.Item) .ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 16f06de2..488e3dc1 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -100,7 +101,7 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ea74987..dbe06803 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,8 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Dalamud.Plugin.Services; +using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; @@ -109,16 +111,17 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { _services.GetService(); + var itemSheet = _services.GetService().GetExcelSheet()!; _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is (Item, FullEquipType)) + if (it is IdentifiedItem) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); _communicatorService.ChangedItemClick.Subscribe((button, it) => { - if (button == MouseButton.Left && it is (Item item, FullEquipType type)) - Messager.LinkItem(item); + if (button == MouseButton.Left && it is IdentifiedItem item && itemSheet.GetRow(item.Item.ItemId.Id) is { } i) + Messager.LinkItem(i); }, ChangedItemClick.Priority.Link); } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 70b05a73..5ba57cf4 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -115,7 +115,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; - _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() + _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() ?? _config.Ephemeral.ChangedItemFilter; _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.Save(); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index da2daeb7..b75c5aef 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -135,7 +135,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService : FilterComboCache<(EquipItem Item, bool InMod)>(() => { var list = data.ByType[type]; - if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type)) + if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)) return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); return list.Select(i => (i, false)).ToList(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c47414b9..9834d9f0 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -27,7 +27,7 @@ public class ResourceTreeViewer private readonly Dictionary _filterCache; private TreeCategory _categoryFilter; - private ChangedItemDrawer.ChangedItemIcon _typeFilter; + private ChangedItemIconFlag _typeFilter; private string _nameFilter; private string _nodeFilter; @@ -48,7 +48,7 @@ public class ResourceTreeViewer _filterCache = []; _categoryFilter = AllCategories; - _typeFilter = ChangedItemDrawer.AllFlags; + _typeFilter = ChangedItemFlagExtensions.AllFlags; _nameFilter = string.Empty; _nodeFilter = string.Empty; } @@ -185,13 +185,13 @@ public class ResourceTreeViewer }); private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, - ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + ChangedItemIconFlag parentFilterIconFlag) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon) + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) { if (!_typeFilter.HasFlag(filterIcon)) return false; @@ -205,12 +205,12 @@ public class ResourceTreeViewer || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (node.Internal && !debugMode) return NodeVisibility.Hidden; - var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon; + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; @@ -223,7 +223,7 @@ public class ResourceTreeViewer return NodeVisibility.Hidden; } - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) { @@ -241,7 +241,7 @@ public class ResourceTreeViewer { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); - var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon); + var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIconFlag); if (visibility == NodeVisibility.Hidden) continue; @@ -250,7 +250,7 @@ public class ResourceTreeViewer using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); - var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; + var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag; using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); @@ -281,7 +281,7 @@ public class ResourceTreeViewer ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } - _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + _changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 72bfa266..af9782d5 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -3,72 +3,24 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Lumina.Data.Files; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; +using Penumbra.GameData.Data; using Penumbra.Services; using Penumbra.UI.Classes; -using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; public class ChangedItemDrawer : IDisposable, IUiService { - [Flags] - public enum ChangedItemIcon : uint - { - Head = 0x00_00_01, - Body = 0x00_00_02, - Hands = 0x00_00_04, - Legs = 0x00_00_08, - Feet = 0x00_00_10, - Ears = 0x00_00_20, - Neck = 0x00_00_40, - Wrists = 0x00_00_80, - Finger = 0x00_01_00, - Monster = 0x00_02_00, - Demihuman = 0x00_04_00, - Customization = 0x00_08_00, - Action = 0x00_10_00, - Mainhand = 0x00_20_00, - Offhand = 0x00_40_00, - Unknown = 0x00_80_00, - Emote = 0x01_00_00, - } + private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray(); - private static readonly ChangedItemIcon[] Order = - [ - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Emote, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - ChangedItemIcon.Unknown, - ]; - - private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray(); - - public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIcon slot) + public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIconFlag slot) { // Handle numeric cases before TryParse because numbers // are not logical otherwise. @@ -77,15 +29,15 @@ public class ChangedItemDrawer : IDisposable, IUiService // We assume users will use 1-based index, but if they enter 0, just use the first. if (idx == 0) { - slot = Order[0]; + slot = ChangedItemFlagExtensions.Order[0]; return true; } // Use 1-based index. --idx; - if (idx >= 0 && idx < Order.Length) + if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count) { - slot = Order[idx]; + slot = ChangedItemFlagExtensions.Order[idx]; return true; } } @@ -94,13 +46,13 @@ public class ChangedItemDrawer : IDisposable, IUiService return false; } - public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot) + public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot) { if (TryParseIndex(lowerInput, out slot)) return true; slot = 0; - foreach (var (item, flag) in LowerNames.Zip(Order)) + foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order)) { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; @@ -109,15 +61,11 @@ public class ChangedItemDrawer : IDisposable, IUiService return slot != 0; } - public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; - public static readonly int NumCategories = Order.Length; - public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; - private readonly Configuration _config; - private readonly ExcelSheet _items; - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - private float _smallestIconWidth; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth; public static Vector2 TypeFilterIconSize => new(2 * ImGui.GetTextLineHeight()); @@ -125,7 +73,6 @@ public class ChangedItemDrawer : IDisposable, IUiService public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { - _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); _communicator = communicator; _config = config; @@ -139,18 +86,19 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, object? data, LowerString filter) - => (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) - && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); + public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags + || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) + && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(string name, object? data) - => DrawCategoryIcon(GetCategoryIcon(name, data)); + public void DrawCategoryIcon(IIdentifiedObjectData? data) + => DrawCategoryIcon(data.GetIcon().ToFlag()); - public void DrawCategoryIcon(ChangedItemIcon iconType) + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) { var height = ImGui.GetFrameHeight(); - if (!_icons.TryGetValue(iconType, out var icon)) + if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); return; @@ -162,18 +110,18 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } } /// /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item Id in grey if requested. + /// Also draw the item ID in grey if requested. /// - public void DrawChangedItem(string name, object? data) + public void DrawChangedItem(string name, IIdentifiedObjectData? data) { - name = ChangedItemName(name, data); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) + name = data?.ToName(name) ?? name; + using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) { var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) @@ -182,31 +130,34 @@ public class ChangedItemDrawer : IDisposable, IUiService ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, Convert(data)); + _communicator.ChangedItemClick.Invoke(ret, data); } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) { // We can not be sure that any subscriber actually prints something in any case. // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - _communicator.ChangedItemHover.Invoke(Convert(data)); - group.Dispose(); + using var tt = ImRaii.Tooltip(); + using (ImRaii.Group()) + { + _communicator.ChangedItemHover.Invoke(data); + } + if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); } } /// Draw the model information, right-justified. - public void DrawModelData(object? data) + public static void DrawModelData(IIdentifiedObjectData? data) { - if (!GetChangedItemObject(data, out var text)) + var additionalData = data?.AdditionalData ?? string.Empty; + if (additionalData.Length == 0) return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); + ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); } /// Draw a header line with the different icon types to filter them. @@ -224,7 +175,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); @@ -232,16 +183,38 @@ public class ChangedItemDrawer : IDisposable, IUiService using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) + foreach (var iconType in ChangedItemFlagExtensions.Order) { - var ret = false; - var icon = _icons[type]; - var flag = typeFilter.HasFlag(type); + ret |= DrawIcon(iconType, ref typeFilter); + ImGui.SameLine(); + } + + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); + ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + typeFilter switch + { + 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), + ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f), + _ => new Vector4(0.5f, 0.5f, 1f, 1f), + }); + if (ImGui.IsItemClicked()) + { + typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags; + ret = true; + } + + return ret; + + bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter) + { + var localRet = false; + var icon = _icons[type]; + var flag = typeFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { typeFilter = flag ? typeFilter & ~type : typeFilter | type; - ret = true; + localRet = true; } using var popup = ImRaii.ContextPopupItem(type.ToString()); @@ -249,7 +222,7 @@ public class ChangedItemDrawer : IDisposable, IUiService if (ImGui.MenuItem("Enable Only This")) { typeFilter = type; - ret = true; + localRet = true; ImGui.CloseCurrentPopup(); } @@ -258,165 +231,13 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } - return ret; - } - - foreach (var iconType in Order) - { - ret |= DrawIcon(iconType, ref typeFilter); - ImGui.SameLine(); - } - - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); - if (ImGui.IsItemClicked()) - { - typeFilter = typeFilter == AllFlags ? 0 : AllFlags; - ret = true; - } - - return ret; - } - - /// Obtain the icon category corresponding to a changed item. - internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) - { - var iconType = ChangedItemIcon.Unknown; - switch (obj) - { - case EquipItem it: - iconType = GetCategoryIcon(it.Type.ToSlot()); - break; - case ModelChara m: - iconType = (CharacterBase.ModelType)m.Type switch - { - CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman, - CharacterBase.ModelType.Monster => ChangedItemIcon.Monster, - _ => ChangedItemIcon.Unknown, - }; - break; - default: - { - if (name.StartsWith("Action: ")) - iconType = ChangedItemIcon.Action; - else if (name.StartsWith("Emote: ")) - iconType = ChangedItemIcon.Emote; - else if (name.StartsWith("Customization: ")) - iconType = ChangedItemIcon.Customization; - break; - } - } - - return iconType; - } - - internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; - - /// Return more detailed object information in text, if it exists. - private static bool GetChangedItemObject(object? obj, out string text) - { - switch (obj) - { - case EquipItem it: - text = it.ModelString; - return true; - case ModelChara m: - text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; + return localRet; } } - /// We need to transform the internal EquipItem type to the Lumina Item type for API-events. - private object? Convert(object? data) - { - if (data is EquipItem it) - return (_items.GetRow(it.ItemId.Id), it.Type); - - return data; - } - - private static string ToDescription(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => EquipSlot.Head.ToName(), - ChangedItemIcon.Body => EquipSlot.Body.ToName(), - ChangedItemIcon.Hands => EquipSlot.Hands.ToName(), - ChangedItemIcon.Legs => EquipSlot.Legs.ToName(), - ChangedItemIcon.Feet => EquipSlot.Feet.ToName(), - ChangedItemIcon.Ears => EquipSlot.Ears.ToName(), - ChangedItemIcon.Neck => EquipSlot.Neck.ToName(), - ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(), - ChangedItemIcon.Finger => "Ring", - ChangedItemIcon.Monster => "Monster", - ChangedItemIcon.Demihuman => "Demi-Human", - ChangedItemIcon.Customization => "Customization", - ChangedItemIcon.Action => "Action", - ChangedItemIcon.Emote => "Emote", - ChangedItemIcon.Mainhand => "Weapon (Mainhand)", - ChangedItemIcon.Offhand => "Weapon (Offhand)", - _ => "Other", - }; - - internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => ApiChangedItemIcon.Head, - ChangedItemIcon.Body => ApiChangedItemIcon.Body, - ChangedItemIcon.Hands => ApiChangedItemIcon.Hands, - ChangedItemIcon.Legs => ApiChangedItemIcon.Legs, - ChangedItemIcon.Feet => ApiChangedItemIcon.Feet, - ChangedItemIcon.Ears => ApiChangedItemIcon.Ears, - ChangedItemIcon.Neck => ApiChangedItemIcon.Neck, - ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists, - ChangedItemIcon.Finger => ApiChangedItemIcon.Finger, - ChangedItemIcon.Monster => ApiChangedItemIcon.Monster, - ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman, - ChangedItemIcon.Customization => ApiChangedItemIcon.Customization, - ChangedItemIcon.Action => ApiChangedItemIcon.Action, - ChangedItemIcon.Emote => ApiChangedItemIcon.Emote, - ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand, - ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown, - _ => ApiChangedItemIcon.None, - }; - - /// Apply Changed Item Counters to the Name if necessary. - private static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - /// Add filterable information to the string. - private static string ChangedItemFilterName(string name, object? data) - => data switch - { - int counter => $"{counter} Files Manipulating {name}s", - EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", - ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", - _ => name, - }; - /// Initialize the icons. private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { @@ -425,30 +246,30 @@ public class ChangedItemDrawer : IDisposable, IUiService if (!equipTypeIcons.Valid) return false; - void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex) + void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex) { if (tex != null) _icons.Add(icon, tex); } - Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); - Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); - Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); - Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); - Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); - Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); - Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); - Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); - Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); - Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); - Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062042_hr1.tex")!)); - Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062041_hr1.tex")!)); - Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); - Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); - Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider)); - Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider)); - Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); + Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062044_hr1.tex")!)); + Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062045_hr1.tex")!)); + Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); _smallestIconWidth = _icons.Values.Min(i => i.Width); @@ -487,6 +308,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon"); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, + "Penumbra.EmoteItemIcon"); } } diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/ChangedItemIconFlag.cs new file mode 100644 index 00000000..fc7073f2 --- /dev/null +++ b/Penumbra/UI/ChangedItemIconFlag.cs @@ -0,0 +1,122 @@ +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI; + +[Flags] +public enum ChangedItemIconFlag : uint +{ + Head = 0x00_00_01, + Body = 0x00_00_02, + Hands = 0x00_00_04, + Legs = 0x00_00_08, + Feet = 0x00_00_10, + Ears = 0x00_00_20, + Neck = 0x00_00_40, + Wrists = 0x00_00_80, + Finger = 0x00_01_00, + Monster = 0x00_02_00, + Demihuman = 0x00_04_00, + Customization = 0x00_08_00, + Action = 0x00_10_00, + Mainhand = 0x00_20_00, + Offhand = 0x00_40_00, + Unknown = 0x00_80_00, + Emote = 0x01_00_00, +} + +public static class ChangedItemFlagExtensions +{ + public static readonly IReadOnlyList Order = + [ + ChangedItemIconFlag.Head, + ChangedItemIconFlag.Body, + ChangedItemIconFlag.Hands, + ChangedItemIconFlag.Legs, + ChangedItemIconFlag.Feet, + ChangedItemIconFlag.Ears, + ChangedItemIconFlag.Neck, + ChangedItemIconFlag.Wrists, + ChangedItemIconFlag.Finger, + ChangedItemIconFlag.Mainhand, + ChangedItemIconFlag.Offhand, + ChangedItemIconFlag.Customization, + ChangedItemIconFlag.Action, + ChangedItemIconFlag.Emote, + ChangedItemIconFlag.Monster, + ChangedItemIconFlag.Demihuman, + ChangedItemIconFlag.Unknown, + ]; + + public const ChangedItemIconFlag AllFlags = (ChangedItemIconFlag)0x01FFFF; + public static readonly int NumCategories = Order.Count; + public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand; + + public static string ToDescription(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => EquipSlot.Head.ToName(), + ChangedItemIconFlag.Body => EquipSlot.Body.ToName(), + ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(), + ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(), + ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(), + ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(), + ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(), + ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(), + ChangedItemIconFlag.Finger => "Ring", + ChangedItemIconFlag.Monster => "Monster", + ChangedItemIconFlag.Demihuman => "Demi-Human", + ChangedItemIconFlag.Customization => "Customization", + ChangedItemIconFlag.Action => "Action", + ChangedItemIconFlag.Emote => "Emote", + ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)", + ChangedItemIconFlag.Offhand => "Weapon (Offhand)", + _ => "Other", + }; + + public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => ChangedItemIcon.Head, + ChangedItemIconFlag.Body => ChangedItemIcon.Body, + ChangedItemIconFlag.Hands => ChangedItemIcon.Hands, + ChangedItemIconFlag.Legs => ChangedItemIcon.Legs, + ChangedItemIconFlag.Feet => ChangedItemIcon.Feet, + ChangedItemIconFlag.Ears => ChangedItemIcon.Ears, + ChangedItemIconFlag.Neck => ChangedItemIcon.Neck, + ChangedItemIconFlag.Wrists => ChangedItemIcon.Wrists, + ChangedItemIconFlag.Finger => ChangedItemIcon.Finger, + ChangedItemIconFlag.Monster => ChangedItemIcon.Monster, + ChangedItemIconFlag.Demihuman => ChangedItemIcon.Demihuman, + ChangedItemIconFlag.Customization => ChangedItemIcon.Customization, + ChangedItemIconFlag.Action => ChangedItemIcon.Action, + ChangedItemIconFlag.Emote => ChangedItemIcon.Emote, + ChangedItemIconFlag.Mainhand => ChangedItemIcon.Mainhand, + ChangedItemIconFlag.Offhand => ChangedItemIcon.Offhand, + ChangedItemIconFlag.Unknown => ChangedItemIcon.Unknown, + _ => ChangedItemIcon.None, + }; + + public static ChangedItemIconFlag ToFlag(this ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Unknown => ChangedItemIconFlag.Unknown, + ChangedItemIcon.Head => ChangedItemIconFlag.Head, + ChangedItemIcon.Body => ChangedItemIconFlag.Body, + ChangedItemIcon.Hands => ChangedItemIconFlag.Hands, + ChangedItemIcon.Legs => ChangedItemIconFlag.Legs, + ChangedItemIcon.Feet => ChangedItemIconFlag.Feet, + ChangedItemIcon.Ears => ChangedItemIconFlag.Ears, + ChangedItemIcon.Neck => ChangedItemIconFlag.Neck, + ChangedItemIcon.Wrists => ChangedItemIconFlag.Wrists, + ChangedItemIcon.Finger => ChangedItemIconFlag.Finger, + ChangedItemIcon.Mainhand => ChangedItemIconFlag.Mainhand, + ChangedItemIcon.Offhand => ChangedItemIconFlag.Offhand, + ChangedItemIcon.Customization => ChangedItemIconFlag.Customization, + ChangedItemIcon.Monster => ChangedItemIconFlag.Monster, + ChangedItemIcon.Demihuman => ChangedItemIconFlag.Demihuman, + ChangedItemIcon.Action => ChangedItemIconFlag.Action, + ChangedItemIcon.Emote => ChangedItemIconFlag.Emote, + _ => ChangedItemIconFlag.Unknown, + }; +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 55405313..42689efb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -550,7 +550,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector)selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -32,15 +33,15 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem ImGuiClip.DrawEndDummy(remainder, height); } - private bool CheckFilter((string Name, object? Data) kvp) + private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); - private void DrawChangedItem((string Name, object? Data) kvp) + private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Data); ImGui.SameLine(); drawer.DrawChangedItem(kvp.Name, kvp.Data); - drawer.DrawModelData(kvp.Data); + ChangedItemDrawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index e7550eea..1eff1919 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -19,16 +19,16 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter { - public string Needle { get; init; } - public ModSearchType Type { get; init; } - public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; } + public string Needle { get; init; } + public ModSearchType Type { get; init; } + public ChangedItemIconFlag IconFlagFilter { get; init; } public bool Contains(Entry other) { if (Type != other.Type) return false; if (Type is ModSearchType.Category) - return IconFilter == other.IconFilter; + return IconFlagFilter == other.IconFlagFilter; return Needle.Contains(other.Needle); } @@ -77,7 +77,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Category => leaf.Value.ChangedItems.Any(p - => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0), + => ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0), _ => true, }; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 2aeaaea0..256b0d79 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -6,6 +6,7 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; +using Penumbra.GameData.Data; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -66,22 +67,22 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, object?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, object?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2); ImGui.SameLine(); drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - drawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ab47ce7c..6c36e49a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -429,7 +429,7 @@ public class SettingsTab : ITab, IUiService _config.HideChangedItemFilters = v; if (v) { - _config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags; + _config.Ephemeral.ChangedItemFilter = ChangedItemFlagExtensions.AllFlags; _config.Ephemeral.Save(); } }); diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index cb43ac06..5bd3f77c 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,25 +19,25 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i]; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } } } - public static void RemoveMachinistOffhands(this SortedList, object?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i].Item2; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } } From ee086e3e7698a846cefa20c23dc09408b76caad8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 4 Aug 2024 00:57:39 +0200 Subject: [PATCH 1907/2451] Update GameData --- Penumbra.GameData | 2 +- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ee6c6faa..1ec903d5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a +Subproject commit 1ec903d53747fc16f62139e2ed3541f224ee3403 diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 50713968..ba5c3e63 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. public int CurrentDyeChannel = 0; @@ -102,7 +102,7 @@ public class StainService : IService }; /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. - private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack { if (stmResourceHandle != null) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ead02874..7dae19c8 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -713,7 +713,7 @@ public class DebugTab : Window, ITab, IUiService } } - private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack { foreach (var (key, data) in stmFile.Entries) { From d90c3dd1af6e256932140ce84ddba78d4f233616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 15:35:27 +0200 Subject: [PATCH 1908/2451] Update row type names. --- Penumbra.GameData | 2 +- .../ModEditWindow.Materials.ColorTable.cs | 16 ++++++++-------- .../ModEditWindow.Materials.MtrlTab.cs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 44427ad0..f2734d54 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 44427ad0149059ab5ccb4e4a2f42a1a43423e4c5 +Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 25c0e448..cb04dc0a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -171,7 +171,7 @@ public partial class ModEditWindow } [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) + private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -179,11 +179,11 @@ public partial class ModEditWindow try { - Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; + Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); } var text = Convert.ToBase64String(data); @@ -219,15 +219,15 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size + if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; + tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); } tab.UpdateColorTableRowPreview(rowIdx); @@ -453,7 +453,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 29fd7531..b95eca9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -597,11 +597,11 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); + var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); + var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -651,7 +651,7 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) + private static void ApplyHighlight(ref LegacyColorTableRow row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); From c8e859ae05ebb9d9b7f0fbce17d3223c19c88be3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 15:41:40 +0200 Subject: [PATCH 1909/2451] Fixups. --- .../Materials/MtrlTab.LegacyColorTable.cs | 38 +- .../Materials/MtrlTab.LivePreview.cs | 6 +- .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ 4 files changed, 28 insertions(+), 1337 deletions(-) delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index f3ec5307..a2165760 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -33,6 +33,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -56,6 +57,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -108,8 +110,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -131,9 +135,11 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); - ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -155,7 +161,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Shininess * 0.025f), v => table[rowIdx].Shininess = v); if (dyeTable != null) @@ -197,7 +204,7 @@ public partial class MtrlTab { using var id = ImRaii.PushId(rowIdx); ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; @@ -213,8 +220,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -236,6 +245,7 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, @@ -260,7 +270,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Scalar3 * 0.025f), v => table[rowIdx].Scalar3 = v); if (dyeTable != null) @@ -307,7 +318,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) { var stain = _stainService.StainCombo1.CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -326,7 +337,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -337,10 +348,11 @@ public partial class MtrlTab var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ - _stainService.StainCombo1.CurrentSelection.Key, - _stainService.StainCombo2.CurrentSelection.Key, - ], rowIdx); + ret = ret + && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); ImGui.SameLine(); DrawLegacyDyePreview(values, floatSize); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index bb346534..3482e581 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -191,7 +191,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { - LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; @@ -199,7 +199,7 @@ public partial class MtrlTab { var dyeRow = Mtrl.DyeTable switch { - LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), ColorDyeTable dyeTable => dyeTable[rowIdx], _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; @@ -258,7 +258,7 @@ public partial class MtrlTab } } - private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = colorId.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index cb04dc0a..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; - - private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo(array); - data2.TryCopyTo(array.AsSpan(data1.Length)); - var text = Convert.ToBase64String(array); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private bool DrawPreviewDye(MtrlTab tab, bool disabled) - { - var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 - ? "Select a preview dye first." - : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) - { - var ret = false; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) - { - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(tab, rowIdx, disabled); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); - ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth(intSize); - if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) - return false; - - var values = entry[(int)stain]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - - var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Apply the selected dye to this row.", disabled, true); - - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); - - return ret; - } - - private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") - { - var ret = false; - var inputSqrt = PseudoSqrtRgb(input); - var tmp = inputSqrt; - if (ImGui.ColorEdit3(label, ref tmp, - ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB - | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) - && tmp != inputSqrt) - { - setter(PseudoSquareRgb(tmp)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - var textSize = ImGui.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText(center, textColor, letter); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return ret; - } - - // Functions to deal with squared RGB values without making negatives useless. - - private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : x * x; - - private static Vector3 PseudoSquareRgb(Vector3 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); - - private static Vector4 PseudoSquareRgb(Vector4 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); - - private static float PseudoSqrtRgb(float x) - => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - - internal static Vector3 PseudoSqrtRgb(Vector3 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); - - private static Vector4 PseudoSqrtRgb(Vector4 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs deleted file mode 100644 index b95eca9d..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - private string[]? _shpkNames; - - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public JObject? AssociatedShpkDevkit; - - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; - - // Shader Key State - public readonly - List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); - - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; - - // Textures & Samplers - public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - - public readonly HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - public readonly - List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> - Constants)> Constants = new(16); - - // Live-Previewers - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - return _edit.FindBestMatch(defaultGamePath); - } - - public string[] GetShpkNames() - { - if (null != _shpkNames) - return _shpkNames; - - var names = new HashSet(StandardShaderPackages); - names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); - - _shpkNames = names.ToArray(); - Array.Sort(_shpkNames); - - return _shpkNames; - } - - public void LoadShpk(FullPath path) - { - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - - try - { - LoadedShpkPath = path; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); - } - catch (Exception e) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) - throw new Exception("Could not assemble ShPk dev-kit path."); - - var devkitFullPath = _edit.FindBestMatch(devkitPath); - if (!devkitFullPath.IsRooted) - throw new Exception("Could not resolve ShPk dev-kit path."); - - devkitPathName = devkitFullPath.FullName; - return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); - } - catch - { - devkitPathName = string.Empty; - return null; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - - private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class - { - if (devkit == null) - return null; - - try - { - var data = devkit[category]; - if (id.HasValue) - data = data?[id.Value.ToString()]; - - if (mayVary && (data as JObject)?["Vary"] != null) - { - var selector = BuildSelector(data!["Vary"]! - .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); - var index = (int)data["Selectors"]![selector.ToString()]!; - data = data["Items"]![index]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) - Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); - return null; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var valueSet = new HashSet(key.Values); - if (dkData != null) - valueSet.UnionWith(dkData.Values.Keys); - - var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); - var values = valueSet.Select(value => - { - if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) - return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - - return ($"0x{value:X8}", value, string.Empty); - }).ToArray(); - Array.Sort(values, (x, y) => - { - if (x.Value == key.DefaultValue) - return -1; - if (y.Value == key.DefaultValue) - return 1; - - return string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, - !hasDkLabel, values)); - } - else - foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) - ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } - - private void UpdateShaders() - { - VertexShaders.Clear(); - PixelShaders.Clear(); - if (AssociatedShpk == null) - { - ShadersKnown = false; - } - else - { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); - if (node.HasValue) - foreach (var pass in node.Value.Passes) - { - VertexShaders.Add((int)pass.VertexShader); - PixelShaders.Add((int)pass.PixelShader); - } - else - ShadersKnown = false; - } - } - } - } - - var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); - var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); - - VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; - - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, - dkData?.Description ?? string.Empty, !hasDkLabel)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - - TextureLabelWidth = 50f * UiHelpers.Scale; - - float helpWidth; - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; - } - - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (!monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, - ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - } - - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; - } - - private void UpdateConstants() - { - static List FindOrAddGroup(List<(string, List)> groups, string name) - { - foreach (var (groupName, group) in groups) - { - if (string.Equals(name, groupName, StringComparison.Ordinal)) - return group; - } - - var newGroup = new List(16); - groups.Add((name, newGroup)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - var values = Mtrl.GetConstantValues(constant); - for (var i = 0; i < values.Length; i += 4) - { - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - else - { - var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; - foreach (var shpkConstant in AssociatedShpk.MaterialParams) - { - if ((shpkConstant.ByteSize & 0x3) != 0) - continue; - - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); - var handledElements = new IndexSet(values.Length, false); - - var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); - if (dkData != null) - foreach (var dkConstant in dkData) - { - var offset = (int)dkConstant.Offset; - var length = values.Length - offset; - if (dkConstant.Length.HasValue) - length = Math.Min(length, (int)dkConstant.Length.Value); - if (length <= 0) - continue; - - var editor = dkConstant.CreateEditor(); - if (editor != null) - FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") - .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); - handledElements.AddRange(offset, length); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement:true)) - { - if ((shpkConstant.ByteOffset & 0x3) == 0) - { - var offset = shpkConstant.ByteOffset >> 2; - for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) - { - var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); - if (rangeEnd > rangeStart) - fcGroup.Add(( - $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", - constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); - } - } - else - { - for (var i = start; i < end; i += 4) - { - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - } - } - - Constants.RemoveAll(group => group.Constants.Count == 0); - Constants.Sort((x, y) => - { - if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) - return 1; - if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) - return -1; - - return string.Compare(x.Header, y.Header, StringComparison.Ordinal); - }); - // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme - foreach (var (_, group) in Constants) - { - group.Sort((x, y) => string.CompareOrdinal( - x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, - y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); - } - } - - public unsafe void BindToMaterialInstances() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - public void SetShaderPackageFlags(uint shPkFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetShaderPackageFlags(shPkFlags); - } - - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetMaterialParameter(parameterCrc, offset, value); - } - - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetSamplerFlags(samplerCrc, samplerFlags); - } - - private void UpdateMaterialPreview() - { - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); - foreach (var constant in Mtrl.ShaderPackage.Constants) - { - var values = Mtrl.GetConstantValues(constant); - if (values != null) - SetMaterialParameter(constant.Id, 0, values); - } - - foreach (var sampler in Mtrl.ShaderPackage.Samplers) - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - public void HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTableRow row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - return output.Write(); - } - - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } - - private sealed class DevkitShaderKey - { - public string Label = string.Empty; - public string Description = string.Empty; - public Dictionary Values = new(); - } - - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } - - private enum DevkitConstantType - { - Hidden = -1, - Float = 0, - Integer = 1, - Color = 2, - Enum = 3, - } - - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0; - } - - private sealed class DevkitConstant - { - public uint Offset = 0; - public uint? Length = null; - public string Group = string.Empty; - public string Label = string.Empty; - public string Description = string.Empty; - public DevkitConstantType Type = DevkitConstantType.Float; - - public float? Minimum = null; - public float? Maximum = null; - public float? Speed = null; - public float RelativeSpeed = 0.0f; - public float Factor = 1.0f; - public float Bias = 0.0f; - public byte Precision = 3; - public string Unit = string.Empty; - - public bool SquaredRgb = false; - public bool Clamped = false; - - public DevkitConstantValue[] Values = Array.Empty(); - - public IConstantEditor? CreateEditor() - => Type switch - { - DevkitConstantType.Hidden => null, - DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, - Unit), - DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, - Factor, Bias, Unit), - DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), - DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, - value => (value.Label, value.Value, value.Description))), - _ => FloatConstantEditor.Default, - }; - - private static int? ToInteger(float? value) - => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; - } - } -} From f3ab1ddbb48f8e1bab94c59d7628b83ed9d29ba8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 4 Aug 2024 22:27:34 +0200 Subject: [PATCH 1910/2451] Add game data file status to support info --- Penumbra/Penumbra.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index dbe06803..557e011c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -46,6 +46,7 @@ public class Penumbra : IDalamudPlugin private readonly CharacterUtility _characterUtility; private readonly RedrawService _redrawService; private readonly CommunicatorService _communicatorService; + private readonly IDataManager _gameData; private PenumbraWindowSystem? _windowSystem; private bool _disposed; @@ -78,6 +79,7 @@ public class Penumbra : IDalamudPlugin _tempCollections = _services.GetService(); _redrawService = _services.GetService(); _communicatorService = _services.GetService(); + _gameData = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); @@ -217,6 +219,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); + sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); From f8b034c42d14c038ed79183a37bee8262b9c8a7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 22:48:15 +0200 Subject: [PATCH 1911/2451] Auto-formatting, generous application of ImUtf8, minor cleanups. --- .../Materials/MtrlTab.ColorTable.cs | 246 +++++++++++------- .../Materials/MtrlTab.CommonColorTable.cs | 142 ++++++---- .../Materials/MtrlTab.Constants.cs | 29 ++- .../Materials/MtrlTab.Devkit.cs | 50 ++-- .../Materials/MtrlTab.LegacyColorTable.cs | 6 +- .../Materials/MtrlTab.LivePreview.cs | 129 ++++----- .../Materials/MtrlTab.ShaderPackage.cs | 218 ++++++++-------- .../Materials/MtrlTab.Textures.cs | 29 ++- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 40 +-- .../Materials/MtrlTabFactory.cs | 13 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 21 +- .../ModEditWindow.ShaderPackages.cs | 179 +++++++------ .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 62 ++--- 13 files changed, 647 insertions(+), 517 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index dc87ec41..352681bb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -13,7 +13,7 @@ public partial class MtrlTab { private const float ColorTableScalarSize = 65.0f; - private int _colorTableSelectedPair = 0; + private int _colorTableSelectedPair; private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { @@ -23,25 +23,27 @@ public partial class MtrlTab private void DrawColorTablePairSelector(ColorTable table, bool disabled) { + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var style = ImGui.GetStyle(); - var itemSpacing = style.ItemSpacing.X; - var itemInnerSpacing = style.ItemInnerSpacing.X; - var framePadding = style.FramePadding; - var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; - var frameHeight = ImGui.GetFrameHeight(); - var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; - var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; - var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) { for (var j = 0; j < 8; ++j) { var pairIndex = i + j; - using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) { - if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), + new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) _colorTableSelectedPair = pairIndex; } @@ -79,12 +81,10 @@ public partial class MtrlTab private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { - var retA = false; - var retB = false; - ref var rowA = ref table[_colorTableSelectedPair << 1]; - ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; - var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; - var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var retA = false; + var retB = false; + var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; + var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); @@ -108,68 +108,96 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("ColorsA"u8)) + using (ImUtf8.PushId("ColorsA"u8)) + { retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("ColorsB"u8)) + using (ImUtf8.PushId("ColorsB"u8)) + { retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Physical Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("PbrA"u8)) + using (ImUtf8.PushId("PbrA"u8)) + { retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("PbrB"u8)) + using (ImUtf8.PushId("PbrB"u8)) + { retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Sheen Layer Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("SheenA"u8)) + using (ImUtf8.PushId("SheenA"u8)) + { retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("SheenB"u8)) + using (ImUtf8.PushId("SheenB"u8)) + { retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Pair Blending"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("BlendingA"u8)) + using (ImUtf8.PushId("BlendingA"u8)) + { retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("BlendingB"u8)) + using (ImUtf8.PushId("BlendingB"u8)) + { retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Material Template"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("TemplateA"u8)) + using (ImUtf8.PushId("TemplateA"u8)) + { retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("TemplateB"u8)) + using (ImUtf8.PushId("TemplateB"u8)) + { retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (dyeTable != null) { DrawHeader(" Dye Properties"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using var columns = ImUtf8.Columns(2, "ColorTable"u8); + using var dis = ImRaii.Disabled(disabled); + using (ImUtf8.PushId("DyeA"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("DyeA"u8)) - retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); - columns.Next(); - using (var id = ImUtf8.PushId("DyeB"u8)) - retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + + columns.Next(); + using (ImUtf8.PushId("DyeB"u8)) + { + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); } } @@ -177,11 +205,16 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("FurtherA"u8)) + using (ImUtf8.PushId("FurtherA"u8)) + { retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("FurtherB"u8)) + using (ImUtf8.PushId("FurtherB"u8)) + { retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (retA) @@ -195,18 +228,21 @@ public partial class MtrlTab /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. private static void DrawHeader(ReadOnlySpan label) { - var headerColor = ImGui.GetColorU32(ImGuiCol.Header); - using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); } private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() * 2.0f; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, c => table[rowIdx].DiffuseColor = c); @@ -247,13 +283,17 @@ public partial class MtrlTab private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; var isRowB = (rowIdx & 1) != 0; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, @@ -261,11 +301,13 @@ public partial class MtrlTab if (dyeTable != null) { ImGui.SameLine(dyeOffset); - ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, + dye.Anisotropy, b => dyeTable[rowIdx].Anisotropy = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, + dyePack?.Anisotropy, "%.2f"u8); } return ret; @@ -276,11 +318,11 @@ public partial class MtrlTab var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var itemSpacing = ImGui.GetStyle().ItemSpacing.X; var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; - var subcolWidth = CalculateSubcolumnWidth(2); + var subColWidth = CalculateSubColumnWidth(2); - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, @@ -306,13 +348,15 @@ public partial class MtrlTab ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); using var dis = ImRaii.Disabled(); - CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, + Nop); } ImGui.Dummy(new Vector2(64.0f, 0.0f)); ImGui.SameLine(); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -326,10 +370,10 @@ public partial class MtrlTab ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; - var lineHeight = Math.Max(leftLineHeight, rightLineHeight); - var cursorPos = ImGui.GetCursorScreenPos(); + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, @@ -337,9 +381,10 @@ public partial class MtrlTab ImUtf8.SameLineInner(); ImUtf8.Text("Tile"u8); - ImGui.SameLine(subcolWidth); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); - using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + ImGui.SameLine(subColWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }); + using (ImUtf8.Child("###TileProperties"u8, + new Vector2(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)))) { ImGui.Dummy(new Vector2(scalarSize, 0.0f)); ImUtf8.SameLineInner(); @@ -350,7 +395,8 @@ public partial class MtrlTab ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, m => table[rowIdx].TileTransform = m); ImUtf8.SameLineInner(); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); ImUtf8.Text("Tile Transform"u8); } @@ -360,15 +406,20 @@ public partial class MtrlTab private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Roughness = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -380,13 +431,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Metalness = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, b => dyeTable[rowIdx].Metalness = b); ImUtf8.SameLineInner(); @@ -400,12 +452,16 @@ public partial class MtrlTab private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, @@ -420,13 +476,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, b => dyeTable[rowIdx].SheenTintRate = b); ImUtf8.SameLineInner(); @@ -435,7 +492,8 @@ public partial class MtrlTab } ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, + 100.0f / HalfEpsilon, 1.0f, v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); if (dyeTable != null) { @@ -444,7 +502,8 @@ public partial class MtrlTab b => dyeTable[rowIdx].SheenAperture = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, + "%.0f%%"u8); } return ret; @@ -453,12 +512,16 @@ public partial class MtrlTab private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, @@ -479,7 +542,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar3 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar7 = v); @@ -488,7 +551,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar15 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar17 = v); @@ -497,7 +560,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar20 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar22 = v); @@ -513,33 +576,32 @@ public partial class MtrlTab { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; - var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); - - var ret = false; + var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth); + + var ret = false; ref var dye = ref dyeTable[rowIdx]; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, - scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; ret = true; } + ImUtf8.SameLineInner(); ImUtf8.Text("Dye Template"u8); ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); using var dis = ImRaii.Disabled(!dyePack.HasValue); if (ImUtf8.Button("Apply Preview Dye"u8)) - { ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ], rowIdx); - } return ret; } @@ -554,9 +616,9 @@ public partial class MtrlTab ImGui.TextUnformatted(text); } - private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + private static float CalculateSubColumnWidth(int numSubColumns, float reservedSpace = 0.0f) { var itemSpacing = ImGui.GetStyle().ItemSpacing.X; - return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubColumns - 1)) / numSubColumns + itemSpacing; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 2b093e23..09c8ea61 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -12,9 +12,9 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; private static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); @@ -22,11 +22,11 @@ public partial class MtrlTab private bool DrawColorTableSection(bool disabled) { - if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + if (!_shpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen)) return false; ColorTableCopyAllClipboardButton(); @@ -35,7 +35,7 @@ public partial class MtrlTab if (!disabled) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= ColorTableDyeableCheckbox(); } @@ -43,17 +43,18 @@ public partial class MtrlTab if (Mtrl.DyeTable != null) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= DrawPreviewDye(disabled); } ret |= Mtrl.Table switch { - LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), - ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - _ => false, + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, + Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, }; return ret; @@ -64,7 +65,7 @@ public partial class MtrlTab if (Mtrl.Table == null) return; - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + if (!ImUtf8.Button("Export All Rows to Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0))) return; try @@ -178,16 +179,18 @@ public partial class MtrlTab private bool ColorTableDyeableCheckbox() { var dyeable = Mtrl.DyeTable != null; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); + var ret = ImUtf8.Checkbox("Dyeable"u8, ref dyeable); if (ret) { - Mtrl.DyeTable = dyeable ? Mtrl.Table switch - { - ColorTable => new ColorDyeTable(), - LegacyColorTable => new LegacyColorDyeTable(), - _ => null, - } : null; + Mtrl.DyeTable = dyeable + ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } + : null; UpdateColorTablePreview(); } @@ -227,24 +230,27 @@ public partial class MtrlTab private void ColorTableHighlightButton(int pairIdx, bool disabled) { - ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, - ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, + "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0); if (ImGui.IsItemHovered()) HighlightColorTablePair(pairIdx); - else if (HighlightedColorTablePair == pairIdx) + else if (_highlightedColorTablePair == pairIdx) CancelColorTableHighlight(); } private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) { - var style = ImGui.GetStyle(); - var frameRounding = style.FrameRounding; + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; var frameThickness = style.FrameBorderSize; - var borderColor = ImGui.GetColorU32(ImGuiCol.Border); - var drawList = ImGui.GetWindowDrawList(); + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); if (topColor == bottomColor) + { drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + } else { drawList.AddRectFilled( @@ -258,10 +264,12 @@ public partial class MtrlTab rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); } - private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, + ReadOnlySpan letter = default) { var ret = false; var inputSqrt = PseudoSqrtRgb((Vector3)current); @@ -291,10 +299,13 @@ public partial class MtrlTab return ret; } - private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, + ReadOnlySpan letter = default) { if (current.HasValue) + { CtColorPicker(label, description, current.Value, Nop, letter); + } else { var tmp = Vector4.Zero; @@ -308,8 +319,8 @@ public partial class MtrlTab if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImUtf8.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); } @@ -319,7 +330,7 @@ public partial class MtrlTab private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) { - var tmp = current; + var tmp = current; var result = ApplyStainCheckbox.Draw(label, ref tmp); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == current) @@ -329,68 +340,79 @@ public partial class MtrlTab return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, + float max, float speed, Action setter) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + setter(newValue); return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, + float min, float max, float speed) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + value = newValue; return true; } private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? Half.Zero; - var floatValue = (float)valueOrDefault; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, + T max, float speed, Action setter) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + setter(tmp); return true; } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, + T max, float speed) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + value = tmp; return true; } - private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) + where T : unmanaged, INumber { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? T.Zero; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); } @@ -398,14 +420,17 @@ public partial class MtrlTab { if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } - private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, + Action setter) { if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } @@ -430,33 +455,34 @@ public partial class MtrlTab ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); if (!ret || tmp == value) return false; + setter(tmp); } else { value.Decompose(out var scale, out var rotation, out var shear); rotation *= 180.0f / MathF.PI; - shear *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; ImGui.SetNextItemWidth(floatSize); var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); var activated = ImGui.IsItemActivated(); var deactivated = ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (!twoRowLayout) ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (deactivated) _pinnedTileTransform = null; else if (activated) @@ -464,6 +490,7 @@ public partial class MtrlTab ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; if (!ret) return false; + if (_pinnedTileTransform.HasValue) { var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; @@ -476,11 +503,14 @@ public partial class MtrlTab if (!shearChanged) shear = pinShear; } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); if (newValue == value) return false; + setter(newValue); } + return true; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index 56496005..176ec3f4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -35,7 +35,7 @@ public partial class MtrlTab Constants.Clear(); string mpPrefix; - if (AssociatedShpk == null) + if (_associatedShpk == null) { mpPrefix = MaterialParamsConstantName.Value!; var fcGroup = FindOrAddGroup(Constants, "Further Constants"); @@ -51,12 +51,12 @@ public partial class MtrlTab } else { - mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + mpPrefix = _associatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); - foreach (var shpkConstant in AssociatedShpk.MaterialParams) + foreach (var shpkConstant in _associatedShpk.MaterialParams) { var name = Names.KnownNames.TryResolve(shpkConstant.Id); - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, _associatedShpk, out var constantIndex); var values = Mtrl.GetConstantValue(constant); var handledElements = new IndexSet(values.Length, false); @@ -64,8 +64,8 @@ public partial class MtrlTab if (dkData != null) foreach (var dkConstant in dkData) { - var offset = (int)dkConstant.EffectiveByteOffset; - var length = values.Length - offset; + var offset = (int)dkConstant.EffectiveByteOffset; + var length = values.Length - offset; var constantSize = dkConstant.EffectiveByteSize; if (constantSize.HasValue) length = Math.Min(length, (int)constantSize.Value); @@ -86,7 +86,6 @@ public partial class MtrlTab foreach (var (start, end) in handledElements.Ranges(complement: true)) { if (start == 0 && end == values.Length && end - start <= 16) - { if (name.Value != null) { fcGroup.Add(( @@ -94,7 +93,6 @@ public partial class MtrlTab constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); continue; } - } if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) { @@ -105,7 +103,8 @@ public partial class MtrlTab var rangeEnd = Math.Min(i + 16, end); if (rangeEnd > rangeStart) { - var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + var autoName = + $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; fcGroup.Add(( $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); @@ -116,7 +115,8 @@ public partial class MtrlTab { for (var i = start; i < end; i += 16) { - fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, + i..Math.Min(i + 16, end), string.Empty, true, DefaultConstantEditorFor(name))); } } @@ -178,15 +178,16 @@ public partial class MtrlTab ret = true; SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); } - var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); - var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + + var shpkConstant = _associatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? _associatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; - var canReset = AssociatedShpk?.MaterialParamsDefaults != null + var canReset = _associatedShpk?.MaterialParamsDefaults != null ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) : buffer[slice].ContainsAnyExcept((byte)0); ImUtf8.SameLineInner(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) { ret = true; if (defaultValue.Length > 0) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs index cd62d58f..26fe3dcb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Newtonsoft.Json.Linq; using OtterGui.Text.Widget.Editors; using Penumbra.String.Classes; @@ -29,8 +30,8 @@ public partial class MtrlTab } private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + => TryGetShpkDevkitData(_associatedShpkDevkit, _loadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(_associatedBaseDevkit, _loadedBaseDevkitPathName, category, id, mayVary); private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class { @@ -47,7 +48,7 @@ public partial class MtrlTab { var selector = BuildSelector(data!["Vary"]! .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? _associatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; data = data["Items"]![index]; } @@ -62,12 +63,14 @@ public partial class MtrlTab } } + [UsedImplicitly] private sealed class DevkitShaderKeyValue { public string Label = string.Empty; public string Description = string.Empty; } + [UsedImplicitly] private sealed class DevkitShaderKey { public string Label = string.Empty; @@ -75,6 +78,7 @@ public partial class MtrlTab public Dictionary Values = []; } + [UsedImplicitly] private sealed class DevkitSampler { public string Label = string.Empty; @@ -84,14 +88,16 @@ public partial class MtrlTab private enum DevkitConstantType { - Hidden = -1, - Float = 0, + Hidden = -1, + Float = 0, + /// Integer encoded as a float. - Integer = 1, - Color = 2, - Enum = 3, + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. - Int32 = 4, + Int32 = 4, Int32Enum = 5, Int8 = 6, Int8Enum = 7, @@ -105,6 +111,7 @@ public partial class MtrlTab SphereMapIndex = 15, } + [UsedImplicitly] private sealed class DevkitConstantValue { public string Label = string.Empty; @@ -112,6 +119,7 @@ public partial class MtrlTab public double Value = 0; } + [UsedImplicitly] private sealed class DevkitConstant { public uint Offset = 0; @@ -147,7 +155,7 @@ public partial class MtrlTab => ByteOffset ?? Offset * ValueSize; public uint? EffectiveByteSize - => ByteSize ?? (Length * ValueSize); + => ByteSize ?? Length * ValueSize; public unsafe uint ValueSize => Type switch @@ -198,19 +206,23 @@ public partial class MtrlTab private IEditor CreateIntegerEditor() where T : unmanaged, INumber => ((Drag || Slider) && !Hex - ? (Drag - ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) - : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) - : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + ? Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), + Hex, Unit, 0)) .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); private IEditor CreateFloatEditor() where T : unmanaged, INumber, IPowerFunctions - => ((Drag || Slider) - ? (Drag - ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) - : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) - : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + => (Drag || Slider + ? Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, + Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), + T.CreateSaturating(StepFast), Precision, Unit, 0)) .WithExponent(T.CreateSaturating(Exponent)) .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index a2165760..0ff2b01f 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -110,9 +110,9 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); } ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 3482e581..6089f2d5 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -12,20 +12,20 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTablePair = -1; - public readonly Stopwatch HighlightTime = new(); + private readonly List _materialPreviewers = new(4); + private readonly List _colorTablePreviewers = new(4); + private int _highlightedColorTablePair = -1; + private readonly Stopwatch _highlightTime = new(); private void DrawMaterialLivePreviewRebind(bool disabled) { if (disabled) return; - if (ImGui.Button("Reload live preview")) + if (ImUtf8.Button("Reload live preview"u8)) BindToMaterialInstances(); - if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + if (_materialPreviewers.Count != 0 || _colorTablePreviewers.Count != 0) return; ImGui.SameLine(); @@ -34,7 +34,7 @@ public partial class MtrlTab "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); } - public unsafe void BindToMaterialInstances() + private unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); @@ -50,7 +50,7 @@ public partial class MtrlTab try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + _materialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -68,7 +68,7 @@ public partial class MtrlTab { try { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + _colorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); } catch (InvalidOperationException) { @@ -81,53 +81,53 @@ public partial class MtrlTab private void UnbindFromMaterialInstances() { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.Dispose(); - MaterialPreviewers.Clear(); + _materialPreviewers.Clear(); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) previewer.Dispose(); - ColorTablePreviewers.Clear(); + _colorTablePreviewers.Clear(); } private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) { - for (var i = MaterialPreviewers.Count; i-- > 0;) + for (var i = _materialPreviewers.Count; i-- > 0;) { - var previewer = MaterialPreviewers[i]; + var previewer = _materialPreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); + _materialPreviewers.RemoveAt(i); } - for (var i = ColorTablePreviewers.Count; i-- > 0;) + for (var i = _colorTablePreviewers.Count; i-- > 0;) { - var previewer = ColorTablePreviewers[i]; + var previewer = _colorTablePreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); + _colorTablePreviewers.RemoveAt(i); } } - public void SetShaderPackageFlags(uint shPkFlags) + private void SetShaderPackageFlags(uint shPkFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetShaderPackageFlags(shPkFlags); } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + private void SetMaterialParameter(uint parameterCrc, Index offset, Span value) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetMaterialParameter(parameterCrc, offset, value); } - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + private void SetSamplerFlags(uint samplerCrc, uint samplerFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetSamplerFlags(samplerCrc, samplerFlags); } @@ -145,14 +145,14 @@ public partial class MtrlTab SetSamplerFlags(sampler.SamplerId, sampler.Flags); } - public void HighlightColorTablePair(int pairIdx) + private void HighlightColorTablePair(int pairIdx) { - var oldPairIdx = HighlightedColorTablePair; + var oldPairIdx = _highlightedColorTablePair; - if (HighlightedColorTablePair != pairIdx) + if (_highlightedColorTablePair != pairIdx) { - HighlightedColorTablePair = pairIdx; - HighlightTime.Restart(); + _highlightedColorTablePair = pairIdx; + _highlightTime.Restart(); } if (oldPairIdx >= 0) @@ -160,19 +160,6 @@ public partial class MtrlTab UpdateColorTableRowPreview(oldPairIdx << 1); UpdateColorTableRowPreview((oldPairIdx << 1) | 1); } - if (pairIdx >= 0) - { - UpdateColorTableRowPreview(pairIdx << 1); - UpdateColorTableRowPreview((pairIdx << 1) | 1); - } - } - - public void CancelColorTableHighlight() - { - var pairIdx = HighlightedColorTablePair; - - HighlightedColorTablePair = -1; - HighlightTime.Reset(); if (pairIdx >= 0) { @@ -181,9 +168,23 @@ public partial class MtrlTab } } - public void UpdateColorTableRowPreview(int rowIdx) + private void CancelColorTableHighlight() { - if (ColorTablePreviewers.Count == 0) + var pairIdx = _highlightedColorTablePair; + + _highlightedColorTablePair = -1; + _highlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + private void UpdateColorTableRowPreview(int rowIdx) + { + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -192,7 +193,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), - ColorTable table => table[rowIdx], + ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; if (Mtrl.DyeTable != null) @@ -200,8 +201,8 @@ public partial class MtrlTab var dyeRow = Mtrl.DyeTable switch { LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), - ColorDyeTable dyeTable => dyeTable[rowIdx], - _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; if (dyeRow.Channel < StainService.ChannelCount) { @@ -213,21 +214,21 @@ public partial class MtrlTab } } - if (HighlightedColorTablePair << 1 == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + if (_highlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + else if (((_highlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)_highlightTime.Elapsed.TotalSeconds); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { row[..].CopyTo(previewer.GetColorRow(rowIdx)); previewer.ScheduleUpdate(); } } - public void UpdateColorTablePreview() + private void UpdateColorTablePreview() { - if (ColorTablePreviewers.Count == 0) + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -237,7 +238,8 @@ public partial class MtrlTab var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; if (dyeRows != null) { - ReadOnlySpan stainIds = [ + ReadOnlySpan stainIds = + [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ]; @@ -245,13 +247,14 @@ public partial class MtrlTab rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); } - if (HighlightedColorTablePair >= 0) + if (_highlightedColorTablePair >= 0) { - ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[_highlightedColorTablePair << 1], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(_highlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, + (float)_highlightTime.Elapsed.TotalSeconds); } - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); @@ -260,11 +263,11 @@ public partial class MtrlTab private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = colorId.Value(); + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); var halfColor = (HalfColor)(color * color); - + row.DiffuseColor = halfColor; row.SpecularColor = halfColor; row.EmissiveColor = halfColor; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index 21557939..ae57a122 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -21,8 +21,8 @@ public partial class MtrlTab // Apricot shader packages are unlisted because // 1. they cause severe performance/memory issues when calculating the effective shader set // 2. they probably aren't intended for use with materials anyway - internal static readonly IReadOnlyList StandardShaderPackages = new[] - { + private static readonly IReadOnlyList StandardShaderPackages = + [ "3dui.shpk", // "apricot_decal_dummy.shpk", // "apricot_decal_ring.shpk", @@ -80,35 +80,35 @@ public partial class MtrlTab "verticalfog.shpk", "water.shpk", "weather.shpk", - }; + ]; - private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + private static readonly byte[] UnknownShadersString = "Vertex Shaders: ???\nPixel Shaders: ???"u8.ToArray(); private string[]? _shpkNames; - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public bool ShpkLoading; - public JObject? AssociatedShpkDevkit; + private string _shaderHeader = "Shader###Shader"; + private FullPath _loadedShpkPath = FullPath.Empty; + private string _loadedShpkPathName = string.Empty; + private string _loadedShpkDevkitPathName = string.Empty; + private string _shaderComment = string.Empty; + private ShpkFile? _associatedShpk; + private bool _shpkLoading; + private JObject? _associatedShpkDevkit; - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; + private readonly string _loadedBaseDevkitPathName; + private readonly JObject? _associatedBaseDevkit; // Shader Key State - public readonly + private readonly List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); + Values)> _shaderKeys = new(16); - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public ReadOnlyMemory ShadersString = UnknownShadersString; + private readonly HashSet _vertexShaders = new(16); + private readonly HashSet _pixelShaders = new(16); + private bool _shadersKnown; + private ReadOnlyMemory _shadersString = UnknownShadersString; - public string[] GetShpkNames() + private string[] GetShpkNames() { if (null != _shpkNames) return _shpkNames; @@ -122,7 +122,7 @@ public partial class MtrlTab return _shpkNames; } - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + private FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) @@ -131,45 +131,45 @@ public partial class MtrlTab return _edit.FindBestMatch(defaultGamePath); } - public void LoadShpk(FullPath path) + private void LoadShpk(FullPath path) => Task.Run(() => DoLoadShpk(path)); private async Task DoLoadShpk(FullPath path) { - ShadersKnown = false; - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - ShpkLoading = true; + _shadersKnown = false; + _shaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + _shpkLoading = true; try { var data = path.IsRooted ? await File.ReadAllBytesAsync(path.FullName) : _gameData.GetFile(path.InternalName.ToString())?.Data; - LoadedShpkPath = path; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); + _loadedShpkPath = path; + _associatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + _loadedShpkPathName = path.ToPath(); } catch (Exception e) { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + _loadedShpkPath = FullPath.Empty; + _loadedShpkPathName = string.Empty; + _associatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {_loadedShpkPath.ToPath()}.", NotificationType.Error, false); } finally { - ShpkLoading = false; + _shpkLoading = false; } - if (LoadedShpkPath.InternalName.IsEmpty) + if (_loadedShpkPath.InternalName.IsEmpty) { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; + _associatedShpkDevkit = null; + _loadedShpkDevkitPathName = string.Empty; } else { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + _associatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out _loadedShpkDevkitPathName); } UpdateShaderKeys(); @@ -178,9 +178,9 @@ public partial class MtrlTab private void UpdateShaderKeys() { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) + _shaderKeys.Clear(); + if (_associatedShpk != null) + foreach (var key in _associatedShpk.MaterialKeys) { var keyName = Names.KnownNames.TryResolve(key.Id); var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); @@ -210,7 +210,7 @@ public partial class MtrlTab return string.Compare(x.Label, y.Label, StringComparison.Ordinal); }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + _shaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); } else @@ -218,7 +218,7 @@ public partial class MtrlTab { var keyName = Names.KnownNames.TryResolve(key.Category); var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); - ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + _shaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); } } @@ -232,27 +232,28 @@ public partial class MtrlTab passSet = []; byPassSets.Add(passId, passSet); } + passSet.Add(shaderIndex); } - VertexShaders.Clear(); - PixelShaders.Clear(); + _vertexShaders.Clear(); + _pixelShaders.Clear(); var vertexShadersByPass = new Dictionary>(); var pixelShadersByPass = new Dictionary>(); - if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + if (_associatedShpk == null || !_associatedShpk.IsExhaustiveNodeAnalysisFeasible()) { - ShadersKnown = false; + _shadersKnown = false; } else { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + _shadersKnown = true; + var systemKeySelectors = AllSelectors(_associatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(_associatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(_associatedShpk.SubViewKeys).ToArray(); var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + BuildSelector(_associatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); foreach (var systemKeySelector in systemKeySelectors) { @@ -261,38 +262,39 @@ public partial class MtrlTab foreach (var subViewKeySelector in subViewKeySelectors) { var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); + var node = _associatedShpk.GetNodeBySelector(selector); if (node.HasValue) foreach (var pass in node.Value.Passes) { - AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); - AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + AddShader(_vertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(_pixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); } else - ShadersKnown = false; + _shadersKnown = false; } } } } - if (ShadersKnown) + if (_shadersKnown) { var builder = new StringBuilder(); - foreach (var (passId, passVS) in vertexShadersByPass) + foreach (var (passId, passVertexShader) in vertexShadersByPass) { if (builder.Length > 0) builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passVertexShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); - if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + if (pixelShadersByPass.TryGetValue(passId, out var passPixelShader)) { - shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); } } - foreach (var (passId, passPS) in pixelShadersByPass) + + foreach (var (passId, passPixelShader) in pixelShadersByPass) { if (vertexShadersByPass.ContainsKey(passId)) continue; @@ -301,22 +303,24 @@ public partial class MtrlTab builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); } - ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + _shadersString = Encoding.UTF8.GetBytes(builder.ToString()); } else - ShadersString = UnknownShadersString; + { + _shadersString = UnknownShadersString; + } - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + _shaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } private bool DrawShaderSection(bool disabled) { var ret = false; - if (ImGui.CollapsingHeader(ShaderHeader)) + if (ImGui.CollapsingHeader(_shaderHeader)) { ret |= DrawPackageNameInput(disabled); ret |= DrawShaderFlagsInput(disabled); @@ -325,20 +329,17 @@ public partial class MtrlTab DrawMaterialShaders(); } - if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + if (!_shpkLoading && (_associatedShpk == null || _associatedShpkDevkit == null)) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (AssociatedShpk == null) - { + if (_associatedShpk == null) ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, ImGuiUtil.HalfBlendText(0x80u)); // Half red - } else - { - ImUtf8.Text("No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, + ImUtf8.Text( + "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow - } } return ret; @@ -358,14 +359,14 @@ public partial class MtrlTab if (c) foreach (var value in GetShpkNames()) { - if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) - { - Mtrl.ShaderPackage.Name = value; - ret = true; - AssociatedShpk = null; - LoadedShpkPath = FullPath.Empty; - LoadShpk(FindAssociatedShpk(out _, out _)); - } + if (!ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + continue; + + Mtrl.ShaderPackage.Name = value; + ret = true; + _associatedShpk = null; + _loadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); } return ret; @@ -391,23 +392,23 @@ public partial class MtrlTab private void DrawCustomAssociations() { const string tooltip = "Click to copy file path to clipboard."; - var text = AssociatedShpk == null + var text = _associatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {LoadedShpkPathName}"; - var devkitText = AssociatedShpkDevkit == null + : $"Associated .shpk file: {_loadedShpkPathName}"; + var devkitText = _associatedShpkDevkit == null ? "Associated dev-kit file: None" - : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; - var baseDevkitText = AssociatedBaseDevkit == null + : $"Associated dev-kit file: {_loadedShpkDevkitPathName}"; + var baseDevkitText = _associatedBaseDevkit == null ? "Base dev-kit file: None" - : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + : $"Base dev-kit file: {_loadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(text, _loadedShpkPathName, tooltip); + ImUtf8.CopyOnClickSelectable(devkitText, _loadedShpkDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(baseDevkitText, _loadedBaseDevkitPathName, tooltip); - if (ImGui.Button("Associate Custom .shpk File")) + if (ImUtf8.Button("Associate Custom .shpk File"u8)) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { if (success) @@ -416,15 +417,15 @@ public partial class MtrlTab var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(LoadedShpkPath))) + if (ImUtf8.ButtonEx("Associate Default .shpk File"u8, moddedPath.ToPath(), Vector2.Zero, + moddedPath.Equals(_loadedShpkPath))) LoadShpk(moddedPath); if (!gamePath.Path.Equals(moddedPath.InternalName)) { ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(LoadedShpkPath.InternalName))) + if (ImUtf8.ButtonEx("Associate Unmodded .shpk File", defaultPath, Vector2.Zero, + gamePath.Path.Equals(_loadedShpkPath.InternalName))) LoadShpk(new FullPath(gamePath)); } @@ -433,22 +434,23 @@ public partial class MtrlTab private bool DrawMaterialShaderKeys(bool disabled) { - if (ShaderKeys.Count == 0) + if (_shaderKeys.Count == 0) return false; var ret = false; - foreach (var (label, index, description, monoFont, values) in ShaderKeys) + foreach (var (label, index, description, monoFont, values) in _shaderKeys) { using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = AssociatedShpk?.GetMaterialKeyById(key.Category); + using var id = ImUtf8.PushId((int)key.Category); + var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Category); var currentValue = key.Value; var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); if (!disabled && shpkKey.HasValue) { ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + using (var c = ImUtf8.Combo(""u8, currentLabel)) { if (c) foreach (var (valueLabel, value, valueDescription) in values) @@ -469,16 +471,16 @@ public partial class MtrlTab if (description.Length > 0) ImGuiUtil.LabeledHelpMarker(label, description); else - ImGui.TextUnformatted(label); + ImUtf8.Text(label); } else if (description.Length > 0 || currentDescription.Length > 0) { - ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + ImUtf8.LabeledHelpMarker($"{label}: {currentLabel}", description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); } else { - ImGui.TextUnformatted($"{label}: {currentLabel}"); + ImUtf8.Text($"{label}: {currentLabel}"); } } @@ -487,19 +489,19 @@ public partial class MtrlTab private void DrawMaterialShaders() { - if (AssociatedShpk == null) + if (_associatedShpk == null) return; using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) { if (node) - ImUtf8.Text(ShadersString.Span); + ImUtf8.Text(_shadersString.Span); } - if (ShaderComment.Length > 0) + if (_shaderComment.Length > 0) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(ShaderComment); + ImUtf8.Text(_shaderComment); } } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 3181dafe..7ab2900d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -23,7 +23,7 @@ public partial class MtrlTab { Textures.Clear(); SamplerIds.Clear(); - if (AssociatedShpk == null) + if (_associatedShpk == null) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -34,11 +34,11 @@ public partial class MtrlTab } else { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) + foreach (var index in _vertexShaders) + SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in _pixelShaders) + SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!_shadersKnown) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -47,11 +47,11 @@ public partial class MtrlTab foreach (var samplerId in SamplerIds) { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + var shpkSampler = _associatedShpk.GetSamplerById(samplerId); if (shpkSampler is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); @@ -95,9 +95,12 @@ public partial class MtrlTab private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) => addressMode switch { - TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, - TextureAddressMode.Mirror => "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, - TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Wrap => + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, + TextureAddressMode.Clamp => + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, _ => ""u8, }; @@ -167,7 +170,7 @@ public partial class MtrlTab return ret; } - + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) { using var c = ImUtf8.Combo(label, value.ToString()); @@ -202,7 +205,7 @@ public partial class MtrlTab ret = true; } - ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + ref var samplerFlags = ref Wrap(ref sampler.Flags); ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); var addressMode = samplerFlags.UAddressMode; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 2d4e93f1..6e16de99 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; @@ -21,7 +20,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable private const int ShpkPrefixLength = 16; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - + private readonly IDataManager _gameData; private readonly IFramework _framework; private readonly ObjectManager _objects; @@ -40,7 +39,8 @@ public sealed partial class MtrlTab : IWritable, IDisposable private bool _updateOnNextFrame; public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, - StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) { _gameData = gameData; @@ -53,11 +53,11 @@ public sealed partial class MtrlTab : IWritable, IDisposable _materialTemplatePickers = materialTemplatePickers; _config = config; - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + _associatedBaseDevkit = TryLoadShpkDevkit("_base", out _loadedBaseDevkitPathName); Update(); LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) @@ -118,7 +118,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable using var dis = ImRaii.Disabled(disabled); var tmp = shaderFlags.EnableTransparency; - if (ImGui.Checkbox("Enable Transparency", ref tmp)) + if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) { shaderFlags.EnableTransparency = tmp; ret = true; @@ -127,14 +127,14 @@ public sealed partial class MtrlTab : IWritable, IDisposable ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); tmp = shaderFlags.HideBackfaces; - if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + if (ImUtf8.Checkbox("Hide Backfaces"u8, ref tmp)) { shaderFlags.HideBackfaces = tmp; ret = true; SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); } - if (ShpkLoading) + if (_shpkLoading) { ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); @@ -147,32 +147,32 @@ public sealed partial class MtrlTab : IWritable, IDisposable private void DrawOtherMaterialDetails(bool _) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("UV Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.UvSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("Color Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } if (Mtrl.AdditionalData.Length <= 0) return; - using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(Mtrl.AdditionalData); } - public void Update() + private void Update() { UpdateShaders(); UpdateTextures(); @@ -187,12 +187,12 @@ public sealed partial class MtrlTab : IWritable, IDisposable } public bool Valid - => ShadersKnown && Mtrl.Valid; + => _shadersKnown && Mtrl.Valid; public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); + output.GarbageCollect(_associatedShpk, SamplerIds); return output.Write(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs index af8b7db2..09db4277 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -8,9 +8,16 @@ using Penumbra.Services; namespace Penumbra.UI.AdvancedWindow.Materials; -public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, - CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, - FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +public sealed class MtrlTabFactory( + IDataManager gameData, + IFramework framework, + ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, + StainService stainService, + ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, + Configuration config) : IUiService { public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index ee883daf..59b38465 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -24,7 +25,7 @@ public partial class ModEditWindow if (_editor.Files.Mdl.Count == 0) return; - using var tab = ImRaii.TabItem("Material Reassignment"); + using var tab = ImUtf8.TabItem("Material Reassignment"u8); if (!tab) return; @@ -32,45 +33,43 @@ public partial class ModEditWindow MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + using var child = ImUtf8.Child("##mdlFiles"u8, -Vector2.One, true); if (!child) return; - using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + using var table = ImUtf8.Table("##files"u8, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); if (!table) return; - var iconSize = ImGui.GetFrameHeight() * Vector2.One; foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) { using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Save the changed mdl file.\nUse at own risk!"u8, disabled: !info.Changed)) info.Save(_editor.Compactor); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Recycle, "Restore current changes to default."u8, disabled: !info.Changed)) info.Restore(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); + ImUtf8.Text(info.Path.InternalName.Span[(Mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); var tmp = info.CurrentMaterials[0]; - if (ImGui.InputText("##0", ref tmp, 64)) + if (ImUtf8.InputText("##0"u8, ref tmp)) info.SetMaterial(tmp, 0); for (var i = 1; i < info.Count; ++i) { + using var id2 = ImUtf8.PushId(i); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); tmp = info.CurrentMaterials[i]; - if (ImGui.InputText($"##{i}", ref tmp, 64)) + if (ImUtf8.InputText(""u8, ref tmp)) info.SetMaterial(tmp, i); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 8a1c729c..41f1da26 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; -using Penumbra.GameData.Files.ShaderStructs; using OtterGui.Text; using Penumbra.GameData.Structs; @@ -56,21 +55,17 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { if (tab.Shpk.IsLegacy) - { ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } - ImGui.TextUnformatted(tab.Header); + ImUtf8.Text(tab.Header); if (!tab.Shpk.Disassembled) - { ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } } private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) { - if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) + if (!ImUtf8.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) return; var defaultName = objectName[0] switch @@ -106,7 +101,7 @@ public partial class ModEditWindow private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if (!ImGui.Button("Replace Shader Program Blob")) + if (!ImUtf8.Button("Replace Shader Program Blob"u8)) return; tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", @@ -145,8 +140,8 @@ public partial class ModEditWindow private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); - if (!t2) + using var tree = ImUtf8.TreeNode("Raw Program Disassembly"u8); + if (!tree) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); @@ -164,31 +159,34 @@ public partial class ModEditWindow { foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) { - ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageFilterSection(ShpkTab tab) @@ -215,19 +213,20 @@ public partial class ModEditWindow { if (values.PossibleValues == null) { - ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return; } - using var node = ImRaii.TreeNode(label); + using var node = ImUtf8.TreeNode(label); if (!node) return; foreach (var value in values.PossibleValues) { var contains = values.Contains(value); - if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + if (!ImUtf8.Checkbox($"{tab.TryResolveName(value)}", ref contains)) continue; + if (contains) { if (values.AddExisting(value)) @@ -249,7 +248,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) + if (shaders.Length == 0 || !ImUtf8.CollapsingHeader($"{objectName}s")) return false; var ret = false; @@ -259,7 +258,7 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(shader)) continue; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + using var t = ImUtf8.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -270,20 +269,20 @@ public partial class ModEditWindow DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); - ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) - ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.UsedInputs != 0) - ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.AdditionalHeader.Length > 8) { - using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + using var t2 = ImUtf8.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); if (t2) Widget.DrawHexViewer(shader.AdditionalHeader); } @@ -313,23 +312,28 @@ public partial class ModEditWindow var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) { - ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (hasFilter) { var filteredUsedString = UsedComponentString(withSize, true, resource); if (filteredUsedString.Length > 0) - ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); else - ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode("Unused within Filters"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } else - ImRaii.TreeNode(hasFilter ? "Globally Unused" : "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImUtf8.TreeNode(hasFilter ? "Globally Unused"u8 : "Unused"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } return ret; } - private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, bool disabled) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, + bool disabled) { if (resources.Length == 0) return false; @@ -345,8 +349,8 @@ public partial class ModEditWindow var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty); using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); - font.Dispose(); + using var t2 = ImUtf8.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + font.Pop(); if (t2) ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } @@ -361,7 +365,7 @@ public partial class ModEditWindow + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader(label); + var ret = ImUtf8.CollapsingHeader(label); ImGui.GetWindowDrawList() .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; @@ -374,7 +378,7 @@ public partial class ModEditWindow if (isSizeWellDefined) return true; - ImGui.TextUnformatted(materialParams.HasValue + ImUtf8.Text(materialParams.HasValue ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16"); return false; @@ -382,7 +386,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted(tab.Shpk.Disassembled + ImUtf8.Text(tab.Shpk.Disassembled ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); @@ -398,10 +402,7 @@ public partial class ModEditWindow ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); - var textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity - var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red - var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -420,12 +421,12 @@ public partial class ModEditWindow color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) + using (ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + using (ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable(name); + ImUtf8.Selectable(name); if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); @@ -434,11 +435,11 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip(tooltip); + ImUtf8.HoverTooltip(tooltip); } if (deletable) - ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); + ImUtf8.HoverTooltip("\nControl + Right-Click to remove."u8); } } @@ -450,7 +451,9 @@ public partial class ModEditWindow if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) return; - tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", + ".json", DoSave, null, false); + return; void DoSave(bool success, string path) { @@ -476,22 +479,22 @@ public partial class ModEditWindow private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + using var t = ImUtf8.TreeNode("Misaligned / Overflowing Parameters"u8); if (!t) return; using var _ = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var name in tab.MalformedParameters) - ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageStartCombo(ShpkTab tab) { using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + using var c = ImUtf8.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); if (c) foreach (var (start, idx) in tab.Orphans.WithIndex()) { @@ -501,7 +504,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("Start"); + ImUtf8.Text("Start"u8); } private static void DrawShaderPackageEndCombo(ShpkTab tab) @@ -510,7 +513,7 @@ public partial class ModEditWindow using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + using var c = ImUtf8.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); if (c) { var current = tab.Orphans[tab.NewMaterialParamStart].Index; @@ -527,7 +530,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("End"); + ImUtf8.Text("End"u8); } private static bool DrawShaderPackageNewParameter(ShpkTab tab) @@ -540,15 +543,14 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(UiHelpers.Scale * 400); var newName = tab.NewMaterialParamName.Value!; - if (ImGui.InputText("Name", ref newName, 63)) + if (ImUtf8.InputText("Name", ref newName)) tab.NewMaterialParamName = newName; var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) - ? "The ID is already in use. Please choose a different name." - : string.Empty; - if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), - tooltip, - tooltip.Length > 0)) + ? "The ID is already in use. Please choose a different name."u8 + : ""u8; + if (!ImUtf8.ButtonEx($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", tooltip, + new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam @@ -589,15 +591,15 @@ public partial class ModEditWindow { var ret = false; - if (!ImGui.CollapsingHeader("Shader Resources")) + if (!ImUtf8.CollapsingHeader("Shader Resources"u8)) return false; var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; - ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } @@ -607,18 +609,20 @@ public partial class ModEditWindow if (keys.Count == 0) return; - using var t = ImRaii.TreeNode(arrayName); + using var t = ImUtf8.TreeNode(arrayName); if (!t) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); + using var t2 = ImUtf8.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); if (t2) { - ImRaii.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } @@ -628,7 +632,7 @@ public partial class ModEditWindow if (tab.Shpk.Nodes.Length <= 0) return; - using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + using var t = ImUtf8.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); if (!t) return; @@ -639,39 +643,44 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(node)) continue; - using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + using var t2 = ImUtf8.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); if (!t2) continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) { - ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) { - ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) { - ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) { - ImRaii.TreeNode($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImUtf8.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode($"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImUtf8.TreeNode( + $"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -680,7 +689,7 @@ public partial class ModEditWindow private static void DrawShaderPackageSelection(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Shader Selection")) + if (!ImUtf8.CollapsingHeader("Shader Selection"u8)) return; DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys); @@ -689,13 +698,13 @@ public partial class ModEditWindow DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); - using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + using var t = ImUtf8.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); if (t) { using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var selector in tab.Shpk.NodeSelectors) { - ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + ImUtf8.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } } @@ -703,14 +712,14 @@ public partial class ModEditWindow private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (tab.Shpk.AdditionalData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(tab.Shpk.AdditionalData); } @@ -718,9 +727,9 @@ public partial class ModEditWindow private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { - var used = filtered ? resource.FilteredUsed : resource.Used; + var used = filtered ? resource.FilteredUsed : resource.Used; var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; - var sb = new StringBuilder(256); + var sb = new StringBuilder(256); if (withSize) { foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index de20aa9f..b5b39e90 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -21,11 +21,11 @@ public partial class ModEditWindow public short NewMaterialParamStart; public short NewMaterialParamEnd; - public SharedSet[] FilterSystemValues; - public SharedSet[] FilterSceneValues; - public SharedSet[] FilterMaterialValues; - public SharedSet[] FilterSubViewValues; - public SharedSet FilterPasses; + public readonly SharedSet[] FilterSystemValues; + public readonly SharedSet[] FilterSceneValues; + public readonly SharedSet[] FilterMaterialValues; + public readonly SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; public readonly int FilterMaximumPopCount; public int FilterPopCount; @@ -46,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; @@ -105,13 +106,21 @@ public partial class ModEditWindow _nameSetWithIdsCache.Clear(); } - public void UpdateNameCache() + private void UpdateNameCache() { - static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) - { - foreach (var resource in resources) - nameCache.TryAdd(resource.Id, resource.Name); - } + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + return; static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) { @@ -128,18 +137,11 @@ public partial class ModEditWindow } } - CollectResourceNames(_nameCache, Shpk.Constants); - CollectResourceNames(_nameCache, Shpk.Samplers); - CollectResourceNames(_nameCache, Shpk.Textures); - CollectResourceNames(_nameCache, Shpk.Uavs); - - CollectKeyNames(_nameCache, Shpk.SystemKeys); - CollectKeyNames(_nameCache, Shpk.SceneKeys); - CollectKeyNames(_nameCache, Shpk.MaterialKeys); - CollectKeyNames(_nameCache, Shpk.SubViewKeys); - - _nameSetCache.Clear(); - _nameSetWithIdsCache.Clear(); + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -151,6 +153,7 @@ public partial class ModEditWindow var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; if (cache.TryGetValue(nameSet, out var nameSetStr)) return nameSetStr; + if (withIds) nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); else @@ -186,7 +189,8 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add( + $"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } @@ -206,7 +210,8 @@ public partial class ModEditWindow var tt = $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; if (component < defaultFloats.Length) - tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + tt += + $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } @@ -265,7 +270,8 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", + linear)); } } @@ -407,7 +413,6 @@ public partial class ModEditWindow var unusedSlices = new JArray(); if (materialParameterUsage.Indices(start, length).Any()) - { foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) { unusedSlices.Add(new JObject @@ -417,14 +422,11 @@ public partial class ModEditWindow ["Length"] = rgEnd - rgStart, }); } - } else - { unusedSlices.Add(new JObject { ["Type"] = "Hidden", }); - } dkConstants[param.Id.ToString()] = unusedSlices; } From 6d42673aa4a3d9e289cc7d1ed1a8993ce1980a9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 22:52:53 +0200 Subject: [PATCH 1912/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f2734d54..8ee82929 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb +Subproject commit 8ee82929fa6c725b8f556904ba022fb418991b5c From e91e0b23f8ffd9e01e9e139786ff0f7167a9e78a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 23:18:18 +0200 Subject: [PATCH 1913/2451] Unused usings. --- Penumbra/Penumbra.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 557e011c..438cdc49 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -17,10 +17,8 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; -using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using System.Xml.Linq; using Dalamud.Plugin.Services; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; From 0064c4c96e04f29a90018e84e3a1fd3747eef0b7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Aug 2024 21:23:13 +0000 Subject: [PATCH 1914/2451] [CI] Updating repo.json for testing_1.2.0.20 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cbe2b121..ad1cbef0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.19", + "TestingAssemblyVersion": "1.2.0.20", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.19/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.20/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a5859761906db84e24ae1fcf8648530f9940c5aa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Aug 2024 00:41:39 +0200 Subject: [PATCH 1915/2451] Make ImcChecker threadsafe. --- Penumbra/Meta/ImcChecker.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 751113a0..4e3ff11b 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -12,12 +12,16 @@ public class ImcChecker public static int GetVariantCount(ImcIdentifier identifier) { - if (VariantCounts.TryGetValue(identifier, out var count)) - return count; + lock (VariantCounts) + { + if (VariantCounts.TryGetValue(identifier, out var count)) + return count; - count = GetFile(identifier)?.Count ?? 0; - VariantCounts[identifier] = count; - return count; + count = GetFile(identifier)?.Count ?? 0; + VariantCounts[identifier] = count; + + return count; + } } public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); From 2534f119e9c6a35c5280309f64cb3bdd9289d8c7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Aug 2024 00:41:55 +0200 Subject: [PATCH 1916/2451] Make StainService deal with early-loading. --- Penumbra/Services/StainService.cs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index ba5c3e63..0a437da0 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,8 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. public int CurrentDyeChannel = 0; @@ -80,11 +81,21 @@ public class StainService : IService public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo1 = CreateStainCombo(); - StainCombo2 = CreateStainCombo(); - LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); - GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + + if (characterUtility.Address == null) + { + LegacyStmFile = LoadStmFile(null, dataManager); + GudStmFile = LoadStmFile(null, dataManager); + } + else + { + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + } + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; @@ -98,11 +109,13 @@ public class StainService : IService { 0 => StainCombo1, 1 => StainCombo2, - _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, + $"Unsupported dye channel {channel} (supported values are 0 and 1)"), }; /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. - private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) + where TDyePack : unmanaged, IDyePack { if (stmResourceHandle != null) { From 1b5553284c102449bbd986be10e57259f2cb4bf4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Aug 2024 22:44:50 +0000 Subject: [PATCH 1917/2451] [CI] Updating repo.json for testing_1.2.0.21 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ad1cbef0..10a08fa1 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.20", + "TestingAssemblyVersion": "1.2.0.21", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.20/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.21/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 700fef4f04c283aada11ac2379bde2d5de7fb98a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:41:35 +0200 Subject: [PATCH 1918/2451] Move hook to MaterialResourceHandle.Load (inlining my beloathed) --- Penumbra.GameData | 2 +- .../{MtrlShpkLoaded.cs => MtrlLoaded.cs} | 4 ++-- Penumbra/Interop/Hooks/HookSettings.cs | 2 +- .../Hooks/PostProcessing/ShaderReplacementFixer.cs | 6 +++--- .../Resources/{LoadMtrlShpk.cs => LoadMtrl.cs} | 14 +++++++------- Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs | 1 + Penumbra/Services/CommunicatorService.cs | 6 +++--- 7 files changed, 18 insertions(+), 17 deletions(-) rename Penumbra/Communication/{MtrlShpkLoaded.cs => MtrlLoaded.cs} (73%) rename Penumbra/Interop/Hooks/Resources/{LoadMtrlShpk.cs => LoadMtrl.cs} (55%) diff --git a/Penumbra.GameData b/Penumbra.GameData index 8ee82929..ac9d9c78 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 8ee82929fa6c725b8f556904ba022fb418991b5c +Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947 diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlLoaded.cs similarity index 73% rename from Penumbra/Communication/MtrlShpkLoaded.cs rename to Penumbra/Communication/MtrlLoaded.cs index 9d3597a8..78498844 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlLoaded.cs @@ -6,11 +6,11 @@ namespace Penumbra.Communication; /// Parameter is the material resource handle for which the shader package has been loaded. /// Parameter is the associated game object. /// -public sealed class MtrlShpkLoaded() : EventWrapper(nameof(MtrlShpkLoaded)) +public sealed class MtrlLoaded() : EventWrapper(nameof(MtrlLoaded)) { public enum Priority { - /// + /// ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 0c0a4020..0bc55dc5 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -99,7 +99,7 @@ public class HookOverrides public struct ResourceHooks { public bool ApricotResourceLoad; - public bool LoadMtrlShpk; + public bool LoadMtrl; public bool LoadMtrlTex; public bool ResolvePathHooks; public bool ResourceHandleDestructor; diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 53b69741..d02e18bb 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -110,7 +110,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; - _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); + _communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } @@ -118,7 +118,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic { _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); - _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); _hairMaskState.ClearMaterials(); _characterOcclusionState.ClearMaterials(); @@ -147,7 +147,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); } - private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject) { var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; var shpk = mtrl->ShaderPackageResourceHandle; diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrl.cs similarity index 55% rename from Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs rename to Penumbra/Interop/Hooks/Resources/LoadMtrl.cs index 8c410ad8..f56177e4 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrl.cs @@ -5,28 +5,28 @@ using Penumbra.Services; namespace Penumbra.Interop.Hooks.Resources; -public sealed unsafe class LoadMtrlShpk : FastHook +public sealed unsafe class LoadMtrl : FastHook { private readonly GameState _gameState; private readonly CommunicatorService _communicator; - public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator) + public LoadMtrl(HookManager hooks, GameState gameState, CommunicatorService communicator) { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk); + Task = hooks.CreateHook("Load Material", Sigs.LoadMtrl, Detour, !HookOverrides.Instance.Resources.LoadMtrl); } - public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); + public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle, void* unk1, byte unk2); - private byte Detour(MaterialResourceHandle* handle) + private byte Detour(MaterialResourceHandle* handle, void* unk1, byte unk2) { var last = _gameState.MtrlData.Value; var mtrlData = _gameState.LoadSubFileHelper((nint)handle); _gameState.MtrlData.Value = mtrlData; - var ret = Task.Result.Original(handle); + var ret = Task.Result.Original(handle, unk1, unk2); _gameState.MtrlData.Value = last; - _communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject); + _communicator.MtrlLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject); return ret; } } diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 0759d9b1..1866e859 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -4,6 +4,7 @@ using Penumbra.GameData; namespace Penumbra.Interop.Hooks.Resources; +// TODO check if this is still needed, as our hooked function is called by LoadMtrl's hooked function public sealed unsafe class LoadMtrlTex : FastHook { private readonly GameState _gameState; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index cacbe689..5d745419 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -24,8 +24,8 @@ public class CommunicatorService : IDisposable, IService /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// - public readonly MtrlShpkLoaded MtrlShpkLoaded = new(); + /// + public readonly MtrlLoaded MtrlLoaded = new(); /// public readonly ModDataChanged ModDataChanged = new(); @@ -87,7 +87,7 @@ public class CommunicatorService : IDisposable, IService TemporaryGlobalModChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); - MtrlShpkLoaded.Dispose(); + MtrlLoaded.Dispose(); ModDataChanged.Dispose(); ModOptionChanged.Dispose(); ModDiscoveryStarted.Dispose(); From dba85f5da3774706f6005ddd56859bc78362afef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:43:18 +0200 Subject: [PATCH 1919/2451] Sanity check ShPk mods, ban incompatible ones --- .../Processing/ShpkPathPreProcessor.cs | 85 +++++++++++++++++++ Penumbra/Mods/Manager/ModManager.cs | 18 ++++ Penumbra/UI/ChatWarningService.cs | 56 ++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 Penumbra/Interop/Processing/ShpkPathPreProcessor.cs create mode 100644 Penumbra/UI/ChatWarningService.cs diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs new file mode 100644 index 00000000..2c6f6901 --- /dev/null +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -0,0 +1,85 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.Utility; +using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.UI; + +namespace Penumbra.Interop.Processing; + +/// +/// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game. +/// +public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, ChatWarningService chatWarningService) : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Shpk; + + public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + { + chatWarningService.CleanLastFileWarnings(false); + + if (!resolved.HasValue) + return null; + + // Skip the sanity check for game files. We are not considering the case where the user has modified game file: it's at their own risk. + var resolvedPath = resolved.Value; + if (!resolvedPath.IsRooted) + return resolvedPath; + + // If the ShPk is already loaded, it means that it already passed the sanity check. + var existingResource = resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); + if (existingResource != null) + return resolvedPath; + + var checkResult = SanityCheck(resolvedPath.FullName); + if (checkResult == SanityCheckResult.Success) + return resolvedPath; + + Penumbra.Log.Warning($"Refusing to honor file redirection because of failed sanity check (result: {checkResult}). Original path: {originalGamePath} Redirected path: {resolvedPath}"); + chatWarningService.PrintFileWarning(resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); + + return null; + } + + private static SanityCheckResult SanityCheck(string path) + { + try + { + using var file = MmioMemoryManager.CreateFromFile(path); + var bytes = file.GetSpan(); + + return ShpkFile.FastIsLegacy(bytes) + ? SanityCheckResult.Legacy + : SanityCheckResult.Success; + } + catch (FileNotFoundException) + { + return SanityCheckResult.NotFound; + } + catch (IOException) + { + return SanityCheckResult.IoError; + } + } + + private static string WarningMessageComplement(SanityCheckResult result) + => result switch + { + SanityCheckResult.IoError => "cannot read the modded file.", + SanityCheckResult.NotFound => "the modded file does not exist.", + SanityCheckResult.Legacy => "this mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + _ => string.Empty, + }; + + private enum SanityCheckResult + { + Success, + IoError, + NotFound, + Legacy, + } +} diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index f170a31b..59f8906e 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -350,4 +350,22 @@ public sealed class ModManager : ModStorage, IDisposable, IService Penumbra.Log.Error($"Could not scan for mods:\n{ex}"); } } + + public bool TryIdentifyPath(string path, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(true)] out string? relativePath) + { + var relPath = Path.GetRelativePath(BasePath.FullName, path); + if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + { + mod = null; + relativePath = null; + return false; + } + + var modDirectorySeparator = relPath.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]); + + var modDirectory = modDirectorySeparator < 0 ? relPath : relPath[..modDirectorySeparator]; + relativePath = modDirectorySeparator < 0 ? string.Empty : relPath[(modDirectorySeparator + 1)..]; + + return TryGetMod(modDirectory, "\0", out mod); + } } diff --git a/Penumbra/UI/ChatWarningService.cs b/Penumbra/UI/ChatWarningService.cs new file mode 100644 index 00000000..84ede2fb --- /dev/null +++ b/Penumbra/UI/ChatWarningService.cs @@ -0,0 +1,56 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; + +namespace Penumbra.UI; + +public sealed class ChatWarningService(IChatGui chatGui, IClientState clientState, ModManager modManager) : IUiService +{ + private readonly Dictionary _lastFileWarnings = []; + private int _lastFileWarningsCleanCounter; + + private const int LastFileWarningsCleanCycle = 100; + private static readonly TimeSpan LastFileWarningsMaxAge = new(1, 0, 0); + + public void CleanLastFileWarnings(bool force) + { + if (!force) + { + _lastFileWarningsCleanCounter = (_lastFileWarningsCleanCounter + 1) % LastFileWarningsCleanCycle; + if (_lastFileWarningsCleanCounter != 0) + return; + } + + var expiredDate = DateTime.Now - LastFileWarningsMaxAge; + var toRemove = new HashSet(); + foreach (var (key, value) in _lastFileWarnings) + { + if (value.Item1 <= expiredDate) + toRemove.Add(key); + } + foreach (var key in toRemove) + _lastFileWarnings.Remove(key); + } + + public void PrintFileWarning(string fullPath, Utf8GamePath originalGamePath, string messageComplement) + { + CleanLastFileWarnings(true); + + // Don't warn twice for the same file within a certain time interval unless the reason changed. + if (_lastFileWarnings.TryGetValue(fullPath, out var lastWarning) && lastWarning.Item2 == messageComplement) + return; + + // Don't warn for files managed by other plugins, or files we aren't sure about. + if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) + return; + + // Don't warn if there's no local player (as an approximation of no chat), so as not to trigger the cooldown. + if (clientState.LocalPlayer == null) + return; + + // The wording is an allusion to tar's "Cowardly refusing to create an empty archive" + chatGui.PrintError($"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ": " : ".")}{messageComplement}", "Penumbra"); + _lastFileWarnings[fullPath] = (DateTime.Now, messageComplement); + } +} From a36f9ccec7f4a1adf6e95c484da5a5a9ae9c2d3b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:45:02 +0200 Subject: [PATCH 1920/2451] Improve ResourceTree display with new function --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++++ .../ResourceTree/ResourceTreeFactory.cs | 19 ++++++++++++++++++- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 5 ++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 6ab48325..85d12ce7 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -15,6 +15,8 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public string? ModName; + public string? ModRelativePath; public CiByteString AdditionalData; public readonly ulong Length; public readonly List Children; @@ -57,6 +59,8 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + ModName = other.ModName; + ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; Length = other.Length; Children = other.Children; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 65fac68f..9738148f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Meta; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -21,7 +22,8 @@ public class ResourceTreeFactory( ObjectIdentification objectIdentifier, Configuration config, ActorManager actors, - PathState pathState) : IService + PathState pathState, + ModManager modManager) : IService { private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); @@ -93,7 +95,10 @@ public class ResourceTreeFactory( // This is currently unneeded as we can resolve all paths by querying the draw object: // ResolveGamePaths(tree, collectionResolveData.ModCollection); if (globalContext.WithUiData) + { ResolveUiData(tree); + ResolveModData(tree); + } FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null); Cleanup(tree); @@ -123,6 +128,18 @@ public class ResourceTreeFactory( }); } + private void ResolveModData(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + if (node.FullPath.IsRooted && modManager.TryIdentifyPath(node.FullPath.FullName, out var mod, out var relativePath)) + { + node.ModName = mod.Name; + node.ModRelativePath = relativePath; + } + } + } + private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) { foreach (var node in tree.FlatNodes) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index a991c948..361094c4 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -316,7 +316,10 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); if (resourceNode.FullPath.FullName.Length > 0) { - ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + var uiFullPathStr = resourceNode.ModName != null && resourceNode.ModRelativePath != null + ? $"[{resourceNode.ModName}] {resourceNode.ModRelativePath}" + : resourceNode.FullPath.ToPath(); + ImGui.Selectable(uiFullPathStr, false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); ImGuiUtil.HoverTooltip( From f68e919421f46ec24e9acf21bff5416f39d73a66 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Mon, 5 Aug 2024 04:08:24 +0200 Subject: [PATCH 1921/2451] Fix LiveCTPreviewer instantiation --- Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index bbd3b16c..61ccc95c 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -37,7 +37,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); - if (_originalColorTableTexture == null) + if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); Width = (int)_originalColorTableTexture.Texture->Width; From 0d1ed6a926ccb593bffa95d78a96b48bd222ecf7 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 09:18:57 +0200 Subject: [PATCH 1922/2451] No, ImGui, these buttons aren't the same. --- .../Materials/MtrlTab.ColorTable.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index 352681bb..df8485c9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -91,15 +91,21 @@ public partial class MtrlTab var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { - ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); - ImUtf8.SameLineInner(); - retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + using (ImUtf8.PushId("ClipboardA"u8)) + { + ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); + ImUtf8.SameLineInner(); + retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + } ImGui.SameLine(); CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); columns.Next(); - ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); - ImUtf8.SameLineInner(); - retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + using (ImUtf8.PushId("ClipboardB"u8)) + { + ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); + ImUtf8.SameLineInner(); + retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + } ImGui.SameLine(); CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); } From 1187efa243fe4e645770ca8dc1b80a6159ce0932 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 23:02:34 +0200 Subject: [PATCH 1923/2451] Reinstate single-row CT highlight --- .../Materials/MtrlTab.ColorTable.cs | 73 +++++++++++-------- .../Materials/MtrlTab.CommonColorTable.cs | 14 +++- .../Materials/MtrlTab.LegacyColorTable.cs | 14 +--- .../Materials/MtrlTab.LivePreview.cs | 28 ++++++- 4 files changed, 86 insertions(+), 43 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index df8485c9..0fa38a5d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -30,11 +30,14 @@ public partial class MtrlTab var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; var frameHeight = ImGui.GetFrameHeight(); var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; - var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; - var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + + // This depends on the font being pushed for "proper" alignment of the pair indices in the buttons. + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) { for (var j = 0; j < 8; ++j) @@ -72,7 +75,7 @@ public partial class MtrlTab var cursor = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f }); font.Pop(); - ColorTableHighlightButton(pairIndex, disabled); + ColorTablePairHighlightButton(pairIndex, disabled); font.Push(UiBuilder.MonoFont); ImGui.SetCursorScreenPos(cursor); } @@ -83,6 +86,8 @@ public partial class MtrlTab { var retA = false; var retB = false; + var rowAIdx = _colorTableSelectedPair << 1; + var rowBIdx = rowAIdx | 1; var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; @@ -91,23 +96,15 @@ public partial class MtrlTab var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { - using (ImUtf8.PushId("ClipboardA"u8)) + using (ImUtf8.PushId("RowHeaderA"u8)) { - ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); - ImUtf8.SameLineInner(); - retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + retA |= DrawRowHeader(rowAIdx, disabled); } - ImGui.SameLine(); - CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); columns.Next(); - using (ImUtf8.PushId("ClipboardB"u8)) + using (ImUtf8.PushId("RowHeaderB"u8)) { - ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); - ImUtf8.SameLineInner(); - retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + retB |= DrawRowHeader(rowBIdx, disabled); } - ImGui.SameLine(); - CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); } DrawHeader(" Colors"u8); @@ -116,13 +113,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("ColorsA"u8)) { - retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("ColorsB"u8)) { - retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx); } } @@ -132,13 +129,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("PbrA"u8)) { - retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("PbrB"u8)) { - retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx); } } @@ -148,13 +145,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("SheenA"u8)) { - retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("SheenB"u8)) { - retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx); } } @@ -164,13 +161,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("BlendingA"u8)) { - retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("BlendingB"u8)) { - retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx); } } @@ -180,13 +177,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("TemplateA"u8)) { - retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("TemplateB"u8)) { - retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx); } } @@ -197,13 +194,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("DyeA"u8)) { - retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawDye(dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("DyeB"u8)) { - retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawDye(dyeTable, dyePackB, rowBIdx); } } @@ -213,20 +210,20 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("FurtherA"u8)) { - retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("FurtherB"u8)) { - retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx); } } if (retA) - UpdateColorTableRowPreview(_colorTableSelectedPair << 1); + UpdateColorTableRowPreview(rowAIdx); if (retB) - UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1); + UpdateColorTableRowPreview(rowBIdx); return retA | retB; } @@ -239,6 +236,20 @@ public partial class MtrlTab ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); } + private bool DrawRowHeader(int rowIdx, bool disabled) + { + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); + + ImGui.SameLine(); + CenteredTextInRest($"Row {(rowIdx >> 1) + 1}{"AB"[rowIdx & 1]}"); + + return ret; + } + private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var dyeOffset = ImGui.GetContentRegionAvail().X diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 09c8ea61..38f02100 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -228,7 +228,7 @@ public partial class MtrlTab } } - private void ColorTableHighlightButton(int pairIdx, bool disabled) + private void ColorTablePairHighlightButton(int pairIdx, bool disabled) { ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, @@ -240,6 +240,18 @@ public partial class MtrlTab CancelColorTableHighlight(); } + private void ColorTableRowHighlightButton(int rowIdx, bool disabled) + { + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, + "Highlight this row on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0); + + if (ImGui.IsItemHovered()) + HighlightColorTableRow(rowIdx); + else if (_highlightedColorTableRow == rowIdx) + CancelColorTableHighlight(); + } + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) { var style = ImGui.GetStyle(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index 0ff2b01f..f21d86a9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -103,11 +103,8 @@ public partial class MtrlTab ColorTableCopyClipboardButton(rowIdx); ImUtf8.SameLineInner(); var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); - if ((rowIdx & 1) == 0) - { - ImUtf8.SameLineInner(); - ColorTableHighlightButton(rowIdx >> 1, disabled); - } + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); ImGui.TableNextColumn(); using (ImRaii.PushFont(UiBuilder.MonoFont)) @@ -213,11 +210,8 @@ public partial class MtrlTab ColorTableCopyClipboardButton(rowIdx); ImUtf8.SameLineInner(); var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); - if ((rowIdx & 1) == 0) - { - ImUtf8.SameLineInner(); - ColorTableHighlightButton(rowIdx >> 1, disabled); - } + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); ImGui.TableNextColumn(); using (ImRaii.PushFont(UiBuilder.MonoFont)) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 6089f2d5..01a40980 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -14,6 +14,7 @@ public partial class MtrlTab { private readonly List _materialPreviewers = new(4); private readonly List _colorTablePreviewers = new(4); + private int _highlightedColorTableRow = -1; private int _highlightedColorTablePair = -1; private readonly Stopwatch _highlightTime = new(); @@ -168,13 +169,35 @@ public partial class MtrlTab } } + private void HighlightColorTableRow(int rowIdx) + { + var oldRowIdx = _highlightedColorTableRow; + + if (_highlightedColorTableRow != rowIdx) + { + _highlightedColorTableRow = rowIdx; + _highlightTime.Restart(); + } + + if (oldRowIdx >= 0) + UpdateColorTableRowPreview(oldRowIdx); + + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + } + private void CancelColorTableHighlight() { + var rowIdx = _highlightedColorTableRow; var pairIdx = _highlightedColorTablePair; + _highlightedColorTableRow = -1; _highlightedColorTablePair = -1; _highlightTime.Reset(); + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + if (pairIdx >= 0) { UpdateColorTableRowPreview(pairIdx << 1); @@ -214,7 +237,7 @@ public partial class MtrlTab } } - if (_highlightedColorTablePair << 1 == rowIdx) + if (_highlightedColorTablePair << 1 == rowIdx || _highlightedColorTableRow == rowIdx) ApplyHighlight(ref row, ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); else if (((_highlightedColorTablePair << 1) | 1) == rowIdx) ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)_highlightTime.Elapsed.TotalSeconds); @@ -247,6 +270,9 @@ public partial class MtrlTab rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); } + if (_highlightedColorTableRow >= 0) + ApplyHighlight(ref rows[_highlightedColorTableRow], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + if (_highlightedColorTablePair >= 0) { ApplyHighlight(ref rows[_highlightedColorTablePair << 1], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); From 2bf08c8c899dcb2111098affc6902b60ffcca9c6 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Tue, 6 Aug 2024 13:05:21 +0200 Subject: [PATCH 1924/2451] Fix dye template combo (aka "git gud") --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index df8485c9..13a36c71 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -595,7 +595,7 @@ public partial class MtrlTab if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dye.Template = _stainService.GudTemplateCombo.CurrentSelection; ret = true; } From df58ac7e9248fdf3fcf465c1a6c2880577331d79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 7 Aug 2024 15:44:21 +0200 Subject: [PATCH 1925/2451] Fix ref. --- Penumbra/Communication/MtrlShpkLoaded.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 9d3597a8..2b286bb9 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper + /// ShaderReplacementFixer = 0, } } From fe4a046cc99b7ede7777c233df4089992931e914 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 7 Aug 2024 16:37:58 +0200 Subject: [PATCH 1926/2451] Make ChatWarningService part of the MessageService. --- OtterGui | 2 +- .../Processing/ShpkPathPreProcessor.cs | 27 +++++---- Penumbra/Services/MessageService.cs | 16 ++++++ Penumbra/UI/ChatWarningService.cs | 56 ------------------- 4 files changed, 32 insertions(+), 69 deletions(-) delete mode 100644 Penumbra/UI/ChatWarningService.cs diff --git a/OtterGui b/OtterGui index c53955cb..d9486ae5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c53955cb6199dd418c5a9538d3251ac5942e7067 +Subproject commit d9486ae54b5a4b61cf74f79ed27daa659eb1ce5b diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 2c6f6901..96d9daff 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -4,23 +4,26 @@ using Penumbra.Collections; using Penumbra.GameData.Files; using Penumbra.GameData.Files.Utility; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.UI; namespace Penumbra.Interop.Processing; /// /// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game. /// -public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, ChatWarningService chatWarningService) : IPathPreProcessor +public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, MessageService messager, ModManager modManager) + : IPathPreProcessor { public ResourceType Type => ResourceType.Shpk; - public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, + FullPath? resolved) { - chatWarningService.CleanLastFileWarnings(false); + messager.CleanTaggedMessages(false); if (!resolved.HasValue) return null; @@ -31,7 +34,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, return resolvedPath; // If the ShPk is already loaded, it means that it already passed the sanity check. - var existingResource = resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); + var existingResource = + resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); if (existingResource != null) return resolvedPath; @@ -39,8 +43,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, if (checkResult == SanityCheckResult.Success) return resolvedPath; - Penumbra.Log.Warning($"Refusing to honor file redirection because of failed sanity check (result: {checkResult}). Original path: {originalGamePath} Redirected path: {resolvedPath}"); - chatWarningService.PrintFileWarning(resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); + messager.PrintFileWarning(modManager, resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); return null; } @@ -49,8 +52,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { try { - using var file = MmioMemoryManager.CreateFromFile(path); - var bytes = file.GetSpan(); + using var file = MmioMemoryManager.CreateFromFile(path); + var bytes = file.GetSpan(); return ShpkFile.FastIsLegacy(bytes) ? SanityCheckResult.Legacy @@ -69,9 +72,9 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, private static string WarningMessageComplement(SanityCheckResult result) => result switch { - SanityCheckResult.IoError => "cannot read the modded file.", - SanityCheckResult.NotFound => "the modded file does not exist.", - SanityCheckResult.Legacy => "this mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + SanityCheckResult.IoError => "Cannot read the modded file.", + SanityCheckResult.NotFound => "The modded file does not exist.", + SanityCheckResult.Legacy => "This mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", _ => string.Empty, }; diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 08118483..a35a67f1 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -2,10 +2,14 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; using OtterGui.Services; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; +using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; @@ -38,4 +42,16 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti Message = payload, }); } + + public void PrintFileWarning(ModManager modManager, string fullPath, Utf8GamePath originalGamePath, string messageComplement) + { + // Don't warn for files managed by other plugins, or files we aren't sure about. + if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) + return; + + AddTaggedMessage($"{fullPath}.{messageComplement}", + new Notification( + $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", + NotificationType.Warning, 10000)); + } } diff --git a/Penumbra/UI/ChatWarningService.cs b/Penumbra/UI/ChatWarningService.cs deleted file mode 100644 index 84ede2fb..00000000 --- a/Penumbra/UI/ChatWarningService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Dalamud.Plugin.Services; -using OtterGui.Services; -using Penumbra.Mods.Manager; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public sealed class ChatWarningService(IChatGui chatGui, IClientState clientState, ModManager modManager) : IUiService -{ - private readonly Dictionary _lastFileWarnings = []; - private int _lastFileWarningsCleanCounter; - - private const int LastFileWarningsCleanCycle = 100; - private static readonly TimeSpan LastFileWarningsMaxAge = new(1, 0, 0); - - public void CleanLastFileWarnings(bool force) - { - if (!force) - { - _lastFileWarningsCleanCounter = (_lastFileWarningsCleanCounter + 1) % LastFileWarningsCleanCycle; - if (_lastFileWarningsCleanCounter != 0) - return; - } - - var expiredDate = DateTime.Now - LastFileWarningsMaxAge; - var toRemove = new HashSet(); - foreach (var (key, value) in _lastFileWarnings) - { - if (value.Item1 <= expiredDate) - toRemove.Add(key); - } - foreach (var key in toRemove) - _lastFileWarnings.Remove(key); - } - - public void PrintFileWarning(string fullPath, Utf8GamePath originalGamePath, string messageComplement) - { - CleanLastFileWarnings(true); - - // Don't warn twice for the same file within a certain time interval unless the reason changed. - if (_lastFileWarnings.TryGetValue(fullPath, out var lastWarning) && lastWarning.Item2 == messageComplement) - return; - - // Don't warn for files managed by other plugins, or files we aren't sure about. - if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) - return; - - // Don't warn if there's no local player (as an approximation of no chat), so as not to trigger the cooldown. - if (clientState.LocalPlayer == null) - return; - - // The wording is an allusion to tar's "Cowardly refusing to create an empty archive" - chatGui.PrintError($"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ": " : ".")}{messageComplement}", "Penumbra"); - _lastFileWarnings[fullPath] = (DateTime.Now, messageComplement); - } -} From d630a3dff42295b972a0c1d864468d4d384edb9b Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 7 Aug 2024 14:47:58 +0000 Subject: [PATCH 1927/2451] [CI] Updating repo.json for testing_1.2.0.22 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 10a08fa1..3de0c5c8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.21", + "TestingAssemblyVersion": "1.2.0.22", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.21/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.22/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From fb58a9c27194d2107cd926d3a31f5a8d4600a1d4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 8 Aug 2024 23:19:18 +0200 Subject: [PATCH 1928/2451] Add/improve ShaderReplacementFixer hooks --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../PostProcessing/HumanSetupScalingHook.cs | 55 ++++ .../PostProcessing/PreBoneDeformerReplacer.cs | 49 +-- .../PostProcessing/ShaderReplacementFixer.cs | 284 ++++++++++++++++-- Penumbra/Interop/Services/CharacterUtility.cs | 32 +- Penumbra/Interop/Services/ModelRenderer.cs | 44 +-- .../Interop/Structs/CharacterUtilityData.cs | 26 +- .../Interop/Structs/ModelRendererStructs.cs | 35 +++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 62 ++-- 10 files changed, 478 insertions(+), 113 deletions(-) create mode 100644 Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs create mode 100644 Penumbra/Interop/Structs/ModelRendererStructs.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ac9d9c78..2fd5aa44 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947 +Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 0bc55dc5..a1dd374f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -80,6 +80,8 @@ public class HookOverrides public bool HumanCreateDeformer; public bool HumanOnRenderMaterial; public bool ModelRendererOnRenderMaterial; + public bool ModelRendererUnkFunc; + public bool PrepareColorTable; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs new file mode 100644 index 00000000..5783c099 --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +// TODO: "SetupScaling" does not seem to only set up scaling -> find a better name? +public unsafe class HumanSetupScalingHook : FastHook +{ + private const int ReplacementCapacity = 2; + + public event EventDelegate? SetupReplacements; + + public HumanSetupScalingHook(HookManager hooks, CharacterBaseVTables vTables) + { + Task = hooks.CreateHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, + !HookOverrides.Instance.PostProcessing.HumanSetupScaling); + } + + private void Detour(CharacterBase* drawObject, uint slotIndex) + { + Span replacements = stackalloc Replacement[ReplacementCapacity]; + var numReplacements = 0; + IDisposable? pbdDisposable = null; + object? shpkLock = null; + var releaseLock = false; + + try + { + SetupReplacements?.Invoke(drawObject, slotIndex, replacements, ref numReplacements, ref pbdDisposable, ref shpkLock); + if (shpkLock != null) + { + Monitor.Enter(shpkLock); + releaseLock = true; + } + for (var i = 0; i < numReplacements; ++i) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToSet; + Task.Result.Original(drawObject, slotIndex); + } + finally + { + for (var i = numReplacements; i-- > 0;) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToRestore; + if (releaseLock) + Monitor.Exit(shpkLock!); + pbdDisposable?.Dispose(); + } + } + + public delegate void Delegate(CharacterBase* drawObject, uint slotIndex); + + public delegate void EventDelegate(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, + ref IDisposable? pbdDisposable, ref object? shpkLock); + + public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 1aa09d7f..9273a2cb 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -18,27 +18,26 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public static readonly Utf8GamePath PreBoneDeformerPath = Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; - // Approximate name guesses. - private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); - private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + // Approximate name guess. + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); - private readonly Hook _humanSetupScalingHook; private readonly Hook _humanCreateDeformerHook; - private readonly CharacterUtility _utility; - private readonly CollectionResolver _collectionResolver; - private readonly ResourceLoader _resourceLoader; - private readonly IFramework _framework; + private readonly CharacterUtility _utility; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceLoader _resourceLoader; + private readonly IFramework _framework; + private readonly HumanSetupScalingHook _humanSetupScalingHook; public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, - HookManager hooks, IFramework framework, CharacterBaseVTables vTables) + HookManager hooks, IFramework framework, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, - !HookOverrides.Instance.PostProcessing.HumanSetupScaling).Result; + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = humanSetupScalingHook; + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } @@ -46,7 +45,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public void Dispose() { _humanCreateDeformerHook.Dispose(); - _humanSetupScalingHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; } private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) @@ -58,22 +57,24 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } - private void SetupScaling(CharacterBase* drawObject, uint slotIndex) + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( - $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHSSReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { - if (!preBoneDeformer.IsInvalid) - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; - _humanSetupScalingHook.Original(drawObject, slotIndex); + pbdDisposable = preBoneDeformer; + replacements[numReplacements++] = new((nint)(&_utility.Address->HumanPbdResource), (nint)preBoneDeformer.ResourceHandle, + _utility.DefaultHumanPbdResource); } - finally + catch { - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + preBoneDeformer.Dispose(); + throw; } } diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index d02e18bb..20db7e25 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -7,6 +8,8 @@ using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; using Penumbra.Services; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; @@ -19,6 +22,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan CharacterStockingsShpkName + => "characterstockings.shpk"u8; + + public static ReadOnlySpan CharacterLegacyShpkName + => "characterlegacy.shpk"u8; + public static ReadOnlySpan IrisShpkName => "iris.shpk"u8; @@ -42,16 +51,26 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); + private delegate void ModelRendererUnkFuncDelegate(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, + uint unk3, uint unk4, uint unk5); + private readonly Hook _humanOnRenderMaterialHook; private readonly Hook _modelRendererOnRenderMaterialHook; + private readonly Hook _modelRendererUnkFuncHook; + + private readonly Hook _prepareColorTableHook; + private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; + private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _characterStockingsState; + private readonly ModdedShaderPackageState _characterLegacyState; private readonly ModdedShaderPackageState _irisState; private readonly ModdedShaderPackageState _characterGlassState; private readonly ModdedShaderPackageState _characterTransparencyState; @@ -64,6 +83,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public uint ModdedSkinShpkCount => _skinState.MaterialCount; + public uint ModdedCharacterStockingsShpkCount + => _characterStockingsState.MaterialCount; + + public uint ModdedCharacterLegacyShpkCount + => _characterLegacyState.MaterialCount; + public uint ModdedIrisShpkCount => _irisState.MaterialCount; @@ -83,16 +108,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) + CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _characterStockingsState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + _characterLegacyState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); @@ -105,33 +137,50 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState = new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; + _modelRendererUnkFuncHook = hooks.CreateHook("ModelRenderer.UnkFunc", + Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour, + !HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result; + _prepareColorTableHook = hooks.CreateHook("MaterialResourceHandle.PrepareColorTable", + Sigs.PrepareColorSet, PrepareColorTableDetour, + !HookOverrides.Instance.PostProcessing.PrepareColorTable).Result; + _communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } public void Dispose() { + _prepareColorTableHook.Dispose(); + _modelRendererUnkFuncHook.Dispose(); _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); + _hairMaskState.ClearMaterials(); _characterOcclusionState.ClearMaterials(); _characterTattooState.ClearMaterials(); _characterTransparencyState.ClearMaterials(); _characterGlassState.ClearMaterials(); _irisState.ClearMaterials(); + _characterLegacyState.ClearMaterials(); + _characterStockingsState.ClearMaterials(); _skinState.ClearMaterials(); } - public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong - HairMask) GetAndResetSlowPathCallDeltas() + public (ulong Skin, ulong CharacterStockings, ulong CharacterLegacy, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong + CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() => (_skinState.GetAndResetSlowPathCallDelta(), + _characterStockingsState.GetAndResetSlowPathCallDelta(), + _characterLegacyState.GetAndResetSlowPathCallDelta(), _irisState.GetAndResetSlowPathCallDelta(), _characterGlassState.GetAndResetSlowPathCallDelta(), _characterTransparencyState.GetAndResetSlowPathCallDelta(), @@ -155,7 +204,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return; var shpkName = mtrl->ShpkNameSpan; - var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); + var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) + ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); if (shpkState != null && shpk != shpkState.DefaultShaderPackage) shpkState.TryAddMaterial(mtrlResourceHandle); @@ -164,6 +214,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { _skinState.TryRemoveMaterial(handle); + _characterStockingsState.TryRemoveMaterial(handle); + _characterLegacyState.TryRemoveMaterial(handle); _irisState.TryRemoveMaterial(handle); _characterGlassState.TryRemoveMaterial(handle); _characterTransparencyState.TryRemoveMaterial(handle); @@ -172,10 +224,25 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState.TryRemoveMaterial(handle); } - private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) + { + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHumanSetup() + => _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) { if (SkinShpkName.SequenceEqual(shpkName)) return _skinState; @@ -184,17 +251,14 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForHuman() + private uint GetTotalMaterialCountForHumanRender() => _skinState.MaterialCount; - private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan shpkName) { - if (IrisShpkName.SequenceEqual(shpkName)) - return _irisState; - if (CharacterGlassShpkName.SequenceEqual(shpkName)) return _characterGlassState; @@ -204,9 +268,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (CharacterTattooShpkName.SequenceEqual(shpkName)) return _characterTattooState; - if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) - return _characterOcclusionState; - if (HairMaskShpkName.SequenceEqual(shpkName)) return _hairMaskState; @@ -214,24 +275,93 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForModelRenderer() - => _irisState.MaterialCount - + _characterGlassState.MaterialCount + private uint GetTotalMaterialCountForModelRendererRender() + => _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount - + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRendererUnk() + => _irisState.MaterialCount + + _characterOcclusionState.MaterialCount + + _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForColorTable(ReadOnlySpan shpkName) + { + if (CharacterLegacyShpkName.SequenceEqual(shpkName)) + return _characterLegacyState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForColorTable() + => _characterLegacyState.MaterialCount; + + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) + { + // If we don't have any on-screen instances of modded characterstockings.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForHumanSetup() == 0) + return; + + var model = drawObject->Models[slotIndex]; + if (model == null) + return; + MaterialResourceHandle* mtrlResource = null; + ModdedShaderPackageState? shpkState = null; + foreach (var material in model->MaterialsSpan) + { + if (material.Value == null) + continue; + + mtrlResource = material.Value->MaterialResourceHandle; + shpkState = GetStateForHumanSetup(mtrlResource); + // Despite this function being called with what designates a model (and therefore potentially many materials), + // we currently don't need to handle more than one modded ShPk. + if (shpkState != null) + break; + } + if (shpkState == null || shpkState.MaterialCount == 0) + return; + + shpkState.IncrementSlowPathCallDelta(); + + // This is less performance-critical than the others, as this is called by the game only on draw object creation and slot update. + // There are still thread safety concerns as it might be called in other threads by plugins. + shpkLock = shpkState; + replacements[numReplacements++] = new((nint)shpkState.ShaderPackageReference, (nint)mtrlResource->ShaderPackageResourceHandle, + (nint)shpkState.DefaultShaderPackage); + } + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || GetTotalMaterialCountForHuman() == 0) + if (!Enabled || GetTotalMaterialCountForHumanRender() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForHuman(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForHumanRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _humanOnRenderMaterialHook.Original(human, param); shpkState.IncrementSlowPathCallDelta(); @@ -259,18 +389,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { - // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. - if (!Enabled || GetTotalMaterialCountForModelRenderer() == 0) + // If we don't have any on-screen instances of modded characterglass.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererRender() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForModelRenderer(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForModelRendererRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); shpkState.IncrementSlowPathCallDelta(); - // Same performance considerations as above. + // Same performance considerations as OnRenderHumanMaterial. lock (shpkState) { var shpkReference = shpkState.ShaderPackageReference; @@ -286,6 +416,102 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } } + private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3, + uint unk4, uint unk5) + { + // If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + var mtrlResource = GetMaterialResourceHandle(unkPayload); + var shpkState = GetStateForModelRendererUnk(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as OnRenderHumanMaterial. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = mtrlResource->ShaderPackageResourceHandle; + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + + private MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) + { + // TODO ClientStructs-ify + var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var materialIndex = *(ushort*)(unkPointer + 8); + var material = unkPayload->Params->Model->Materials[materialIndex]; + if (material == null) + return null; + + var mtrlResource = material->MaterialResourceHandle; + if (mtrlResource == null) + return null; + + if (mtrlResource->ShaderPackageResourceHandle == null) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); + return null; + } + + if (mtrlResource->ShaderPackageResourceHandle->ShaderPackage != unkPayload->ShaderWrapper->ShaderPackage) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); + return null; + } + + return mtrlResource; + } + + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) + { + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForColorTable() == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var material = thisPtr->Material; + if (material == null) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan); + if (shpkState == null || shpkState.MaterialCount == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as HumanSetupScalingDetour. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = thisPtr->ShaderPackageResourceHandle; + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) { // MaterialResourceHandle set diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 532dc823..4ab156a9 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -28,10 +28,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService public bool Ready { get; private set; } public event Action LoadingFinished; - public nint DefaultHumanPbdResource { get; private set; } - public nint DefaultTransparentResource { get; private set; } - public nint DefaultDecalResource { get; private set; } - public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultHumanPbdResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultCharacterStockingsShpkResource { get; private set; } + public nint DefaultCharacterLegacyShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -108,6 +110,18 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService anyMissing |= DefaultSkinShpkResource == nint.Zero; } + if (DefaultCharacterStockingsShpkResource == nint.Zero) + { + DefaultCharacterStockingsShpkResource = (nint)Address->CharacterStockingsShpkResource; + anyMissing |= DefaultCharacterStockingsShpkResource == nint.Zero; + } + + if (DefaultCharacterLegacyShpkResource == nint.Zero) + { + DefaultCharacterLegacyShpkResource = (nint)Address->CharacterLegacyShpkResource; + anyMissing |= DefaultCharacterLegacyShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -122,10 +136,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService if (!Ready) return; - Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; - Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; - Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; - Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; + Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; + Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->CharacterStockingsShpkResource = (ResourceHandle*)DefaultCharacterStockingsShpkResource; + Address->CharacterLegacyShpkResource = (ResourceHandle*)DefaultCharacterLegacyShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 10f3977f..5e2cd1fb 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; +using ModelRendererData = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; namespace Penumbra.Interop.Services; @@ -9,46 +10,53 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } - public ShaderPackageResourceHandle** IrisShaderPackage + public ModelRendererData* Address => Manager.Instance() switch { null => null, - var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + var renderManager => &renderManager->ModelRenderer, + }; + + public ShaderPackageResourceHandle** IrisShaderPackage + => Address switch + { + null => null, + var data => &data->IrisShaderPackage, }; public ShaderPackageResourceHandle** CharacterGlassShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, + null => null, + var data => &data->CharacterGlassShaderPackage, }; public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + null => null, + var data => &data->CharacterTransparencyShaderPackage, }; public ShaderPackageResourceHandle** CharacterTattooShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + null => null, + var data => &data->CharacterTattooShaderPackage, }; public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + null => null, + var data => &data->CharacterOcclusionShaderPackage, }; public ShaderPackageResourceHandle** HairMaskShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + null => null, + var data => &data->HairMaskShaderPackage, }; public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } @@ -96,7 +104,7 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (DefaultCharacterTransparencyShaderPackage == null) { DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; - anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; } if (DefaultCharacterTattooShaderPackage == null) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 7595353f..8543466d 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,15 +5,17 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexTileOrbArrayTex = 81; - public const int IndexTileNormArrayTex = 82; - public const int IndexSkinShpk = 83; - public const int IndexGudStm = 94; - public const int IndexLegacyStm = 95; - public const int IndexSphereDArrayTex = 96; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexCharacterStockingsShpk = 84; + public const int IndexCharacterLegacyShpk = 85; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -111,6 +113,12 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexCharacterStockingsShpk * 8)] + public ResourceHandle* CharacterStockingsShpkResource; + + [FieldOffset(8 + IndexCharacterLegacyShpk * 8)] + public ResourceHandle* CharacterLegacyShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] public ResourceHandle* GudStmResource; diff --git a/Penumbra/Interop/Structs/ModelRendererStructs.cs b/Penumbra/Interop/Structs/ModelRendererStructs.cs new file mode 100644 index 00000000..551a32e3 --- /dev/null +++ b/Penumbra/Interop/Structs/ModelRendererStructs.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Structs; + +public static unsafe class ModelRendererStructs +{ + [StructLayout(LayoutKind.Explicit, Size = 0x28)] + public struct UnkShaderWrapper + { + [FieldOffset(0)] + public void* Vtbl; + + [FieldOffset(8)] + public ShaderPackage* ShaderPackage; + } + + // Unknown size, this is allocated on FUN_1404446c0's stack (E8 ?? ?? ?? ?? FF C3 41 3B DE 72 ?? 48 C7 85) + [StructLayout(LayoutKind.Explicit)] + public struct UnkPayload + { + [FieldOffset(0)] + public ModelRenderer.OnRenderModelParams* Params; + + [FieldOffset(8)] + public ModelResourceHandle* ModelResourceHandle; + + [FieldOffset(0x10)] + public UnkShaderWrapper* ShaderWrapper; + + [FieldOffset(0x1C)] + public ushort UnkIndex; + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 7dae19c8..5b82a523 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -833,20 +833,6 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableHeadersRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("skin.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("iris.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); - ImGui.TableNextColumn(); ImGui.TextUnformatted("characterglass.shpk"); ImGui.TableNextColumn(); @@ -855,18 +841,11 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TextUnformatted("characterlegacy.shpk"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterLegacyShpkCount}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertattoo.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterLegacy}"); ImGui.TableNextColumn(); ImGui.TextUnformatted("characterocclusion.shpk"); @@ -875,12 +854,47 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterstockings.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterStockingsShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterStockings}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertattoo.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); + ImGui.TableNextColumn(); ImGui.TextUnformatted("hairmask.shpk"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("iris.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("skin.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); } /// Draw information about the resident resource files. From 03e9dc55dfebc6de1fb3290fdd6b041f547a08e2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 8 Aug 2024 23:19:44 +0200 Subject: [PATCH 1929/2451] Use read-only MMIO for legacy ShPk ban --- Penumbra/Interop/Processing/ShpkPathPreProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 96d9daff..2fb35ae0 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -1,3 +1,4 @@ +using System.IO.MemoryMappedFiles; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -52,7 +53,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { try { - using var file = MmioMemoryManager.CreateFromFile(path); + using var file = MmioMemoryManager.CreateFromFile(path, access: MemoryMappedFileAccess.Read); var bytes = file.GetSpan(); return ShpkFile.FastIsLegacy(bytes) From c265b917b4e56ea359a1337a06cb9cf5da155cce Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 9 Aug 2024 00:02:36 +0200 Subject: [PATCH 1930/2451] "This is how you end up on a list." --- Penumbra/Penumbra.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 438cdc49..d99e3fcd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,6 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + "IllusioVitae", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From a52a43bd86cfb193204ac3bcf8d94dd8e2bf3fc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 01:15:05 +0200 Subject: [PATCH 1931/2451] Minor cleanup. --- .../PostProcessing/HumanSetupScalingHook.cs | 7 +-- .../PostProcessing/PreBoneDeformerReplacer.cs | 23 +++---- .../PostProcessing/ShaderReplacementFixer.cs | 61 +++++++------------ Penumbra/Interop/Services/CharacterUtility.cs | 2 +- 4 files changed, 38 insertions(+), 55 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs index 5783c099..870229d6 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs @@ -11,10 +11,8 @@ public unsafe class HumanSetupScalingHook : FastHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, + => Task = hooks.CreateHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, !HookOverrides.Instance.PostProcessing.HumanSetupScaling); - } private void Detour(CharacterBase* drawObject, uint slotIndex) { @@ -32,6 +30,7 @@ public unsafe class HumanSetupScalingHook : FastHook replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock); - + public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore); } diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 9273a2cb..30e643c7 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -19,7 +19,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; // Approximate name guess. - private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); private readonly Hook _humanCreateDeformerHook; @@ -32,12 +32,12 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, HookManager hooks, IFramework framework, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = humanSetupScalingHook; - _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = humanSetupScalingHook; + _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } @@ -45,7 +45,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public void Dispose() { _humanCreateDeformerHook.Dispose(); - _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements -= SetupHssReplacements; } private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) @@ -57,18 +57,19 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } - private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + private void SetupHssReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( - $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHSSReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { pbdDisposable = preBoneDeformer; - replacements[numReplacements++] = new((nint)(&_utility.Address->HumanPbdResource), (nint)preBoneDeformer.ResourceHandle, + replacements[numReplacements++] = new HumanSetupScalingHook.Replacement((nint)(&_utility.Address->HumanPbdResource), + (nint)preBoneDeformer.ResourceHandle, _utility.DefaultHumanPbdResource); } catch diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 20db7e25..80892b0f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -8,7 +8,6 @@ using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Services; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -137,7 +136,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState = new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); - _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", @@ -146,7 +145,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererUnkFuncHook = hooks.CreateHook("ModelRenderer.UnkFunc", Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour, !HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result; - _prepareColorTableHook = hooks.CreateHook("MaterialResourceHandle.PrepareColorTable", + _prepareColorTableHook = hooks.CreateHook( + "MaterialResourceHandle.PrepareColorTable", Sigs.PrepareColorSet, PrepareColorTableDetour, !HookOverrides.Instance.PostProcessing.PrepareColorTable).Result; @@ -160,7 +160,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererUnkFuncHook.Dispose(); _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); - _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements -= SetupHssReplacements; _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); @@ -188,14 +188,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _characterOcclusionState.GetAndResetSlowPathCallDelta(), _hairMaskState.GetAndResetSlowPathCallDelta()); - private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) - { - if (mtrlResource == null) - return false; - - return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); - } - private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject) { var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; @@ -203,9 +195,11 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; - var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) - ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); + var shpkName = mtrl->ShpkNameSpan; + var shpkState = GetStateForHumanSetup(shpkName) + ?? GetStateForHumanRender(shpkName) + ?? GetStateForModelRendererRender(shpkName) + ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); if (shpkState != null && shpk != shpkState.DefaultShaderPackage) shpkState.TryAddMaterial(mtrlResourceHandle); @@ -228,12 +222,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) - { - if (CharacterStockingsShpkName.SequenceEqual(shpkName)) - return _characterStockingsState; - - return null; - } + => CharacterStockingsShpkName.SequenceEqual(shpkName) ? _characterStockingsState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForHumanSetup() @@ -243,12 +232,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) - { - if (SkinShpkName.SequenceEqual(shpkName)) - return _skinState; - - return null; - } + => SkinShpkName.SequenceEqual(shpkName) ? _skinState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForHumanRender() @@ -305,18 +289,13 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic + _characterStockingsState.MaterialCount; private ModdedShaderPackageState? GetStateForColorTable(ReadOnlySpan shpkName) - { - if (CharacterLegacyShpkName.SequenceEqual(shpkName)) - return _characterLegacyState; - - return null; - } + => CharacterLegacyShpkName.SequenceEqual(shpkName) ? _characterLegacyState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForColorTable() => _characterLegacyState.MaterialCount; - private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + private void SetupHssReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { // If we don't have any on-screen instances of modded characterstockings.shpk, we don't need the slow path at all. @@ -326,6 +305,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic var model = drawObject->Models[slotIndex]; if (model == null) return; + MaterialResourceHandle* mtrlResource = null; ModdedShaderPackageState? shpkState = null; foreach (var material in model->MaterialsSpan) @@ -340,6 +320,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpkState != null) break; } + if (shpkState == null || shpkState.MaterialCount == 0) return; @@ -348,7 +329,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // This is less performance-critical than the others, as this is called by the game only on draw object creation and slot update. // There are still thread safety concerns as it might be called in other threads by plugins. shpkLock = shpkState; - replacements[numReplacements++] = new((nint)shpkState.ShaderPackageReference, (nint)mtrlResource->ShaderPackageResourceHandle, + replacements[numReplacements++] = new HumanSetupScalingHook.Replacement((nint)shpkState.ShaderPackageReference, + (nint)mtrlResource->ShaderPackageResourceHandle, (nint)shpkState.DefaultShaderPackage); } @@ -439,7 +421,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // Same performance considerations as OnRenderHumanMaterial. lock (shpkState) { - var shpkReference = shpkState.ShaderPackageReference; + var shpkReference = shpkState.ShaderPackageReference; try { *shpkReference = mtrlResource->ShaderPackageResourceHandle; @@ -452,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } } - private MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) + private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; @@ -467,13 +449,14 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (mtrlResource->ShaderPackageResourceHandle == null) { - Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); + Penumbra.Log.Warning("ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); return null; } if (mtrlResource->ShaderPackageResourceHandle->ShaderPackage != unkPayload->ShaderWrapper->ShaderPackage) { - Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); + Penumbra.Log.Warning( + $"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); return null; } diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 4ab156a9..1641e42d 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -67,7 +67,7 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService _framework.Update += LoadDefaultResources; } - /// We store the default data of the resources so we can always restore them. + /// We store the default data of the resources, so we can always restore them. private void LoadDefaultResources(object _) { if (Address == null) From 465e65e8fec7a3a3c1a51174ee99955390c15506 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 8 Aug 2024 23:17:52 +0000 Subject: [PATCH 1932/2451] [CI] Updating repo.json for testing_1.2.0.23 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3de0c5c8..efb71542 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.22", + "TestingAssemblyVersion": "1.2.0.23", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.22/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.23/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From e44b450548f42bac821d3c3746a868e976aec420 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 22:17:23 +0200 Subject: [PATCH 1933/2451] Disable model import/export for now. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index de088736..490fa147 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -97,7 +97,9 @@ public partial class ModEditWindow private void DrawImportExport(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Import / Export")) + // TODO: Enable when functional. + using var dawntrailDisabled = ImRaii.Disabled(); + if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true) return; var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); From b5f7f03e11cc9dc26555667f3f90448413d20d3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 22:46:34 +0200 Subject: [PATCH 1934/2451] Update BNPC Names. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2fd5aa44..bf020ebf 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff +Subproject commit bf020ebf5e4980f1814b336aabbaba5e2e00c362 From 1b671b95ab2c95b4af554430746f18d82d50e806 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:04:07 +0200 Subject: [PATCH 1935/2451] 1.2.1.0 --- Penumbra/UI/Changelog.cs | 63 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index f4cedf7d..55ce70e4 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -52,18 +52,68 @@ public class PenumbraChangelog : IUiService AddDummy(Changelog); Add1_1_0_0(Changelog); Add1_1_1_0(Changelog); - } - + Add1_2_1_0(Changelog); + } + #region Changelogs + private static void Add1_2_1_0(Changelog log) + => log.NextVersion("Version 1.2.1.0") + .RegisterHighlight("Penumbra is now released for Dawntrail.") + .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) + .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) + .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) + .RegisterImportant("I am sorry that it took this long, but there was an immense amount of work to be done from the start.") + .RegisterImportant( + "Since Penumbra has been in Testing for quite a while, multitudes of bugs and issues cropped up that needed to be dealt with.", + 1) + .RegisterEntry("There very well may still be a lot of issues, so please report any you find.", 1) + .RegisterImportant("BUT, please make sure that those issues are not caused by outdated mods before reporting them.", 1) + .RegisterEntry( + "This changelog may seem rather short for the timespan, but I omitted hundreds of smaller fixes and the details of getting Penumbra to work in Dawntrail.", + 1) + .RegisterHighlight("The Material Editing tab in the Advanced Editing Window has been heavily improved (by Ny).") + .RegisterEntry( + "Especially for Dawntrail materials using the new shaders, the window provides much more in-depth and user-friendly editing options.", + 1) + .RegisterHighlight("Many advancements regarding modded shaders, and modding bone deformers have been made.") + .RegisterHighlight("IMC groups now allow their options to toggle attributes off that are on in the default entry.") + .RegisterImportant( + "The 'Update Bibo' button was removed. The functionality is redundant since any mods that old need to be updated anyway.") + .RegisterEntry("Clicking the button on modern mods generally caused more harm than benefit.", 1) + .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") + .RegisterEntry( + "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", + 1) + .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") + .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") + .RegisterEntry("Path handling was improved in regards to case-sensitivity.") + .RegisterEntry("Fixed an issue with negative search matching on folders with no matches") + .RegisterEntry("Mod option groups on the same priority are now applied in reverse index order. (1.2.0.12)") + .RegisterEntry("Fixed the display of missing files in the Advanced Editing Window's header. (1.2.0.8)") + .RegisterEntry( + "Fixed some, but not all soft-locks that occur when your character gets redrawn while fishing. Just do not do that. (1.2.0.7)") + .RegisterEntry("Improved handling of invalid Offhand IMC files for certain jobs. (1.2.0.6)") + .RegisterEntry("Added automatic reduplication for files in the UI category, as they cause crashes when not unique. (1.2.0.5)") + .RegisterEntry("The mod import popup can now be closed by clicking outside of it, if it is finished. (1.2.0.5)") + .RegisterEntry("Fixed an issue with Mod Normalization skipping the default option. (1.2.0.5)") + .RegisterEntry("Improved the Support Info output. (1.1.1.5)") + .RegisterEntry("Reworked the handling of Meta Manipulations entirely. (1.1.1.3)") + .RegisterEntry("Added a configuration option to disable showing mods in the character lobby and at the aesthetician. (1.1.1.1)") + .RegisterEntry("Fixed an issue with the AddMods API and the root directory. (1.1.1.2)") + .RegisterEntry("Fixed an issue with the Mod Merger file lookup and casing. (1.1.1.2)") + .RegisterEntry("Fixed an issue with file saving not happening when merging mods or swapping items in some cases. (1.1.1.2)"); + private static void Add1_1_1_0(Changelog log) => log.NextVersion("Version 1.1.1.0") .RegisterHighlight("Filtering for mods is now tokenized and can filter for multiple things at once, or exclude specific things.") .RegisterEntry("Hover over the filter to see the new available options in the tooltip.", 1) - .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) - .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) + .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) + .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) .RegisterHighlight("Added initial identification of characters in the login-screen by name.") - .RegisterEntry("Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", 1) + .RegisterEntry( + "Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", + 1) .RegisterEntry("Added functionality for IMC groups to apply to all variants for a model instead of a specific one.") .RegisterEntry("Improved the resource tree view with filters and incognito mode. (by Ny)") .RegisterEntry("Added a tooltip to the global EQP condition.") @@ -131,7 +181,8 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") - .RegisterEntry("Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") + .RegisterEntry( + "Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") .RegisterEntry("Reworked the API and IPC structure heavily.") .RegisterImportant("This means some plugins interacting with Penumbra may not work correctly until they update.", 1) .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") From 741141f22769fe8af8991545867d44082a65a466 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:52:12 +0200 Subject: [PATCH 1936/2451] Woops. --- Penumbra/UI/Changelog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 55ce70e4..5bf4f59d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -59,7 +59,7 @@ public class PenumbraChangelog : IUiService private static void Add1_2_1_0(Changelog log) => log.NextVersion("Version 1.2.1.0") - .RegisterHighlight("Penumbra is now released for Dawntrail.") + .RegisterHighlight("Penumbra is now released for Dawntrail!") .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) @@ -81,10 +81,10 @@ public class PenumbraChangelog : IUiService .RegisterImportant( "The 'Update Bibo' button was removed. The functionality is redundant since any mods that old need to be updated anyway.") .RegisterEntry("Clicking the button on modern mods generally caused more harm than benefit.", 1) - .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterEntry( "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", 1) + .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") .RegisterEntry("Path handling was improved in regards to case-sensitivity.") From 421fde70b08db075c359bd699a079bf64e8297e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:57:42 +0200 Subject: [PATCH 1937/2451] Addendum --- Penumbra/UI/Changelog.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 5bf4f59d..41920d1c 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -63,6 +63,8 @@ public class PenumbraChangelog : IUiService .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) + .RegisterEntry( + "Some outdated mods can be identified by Penumbra and are prevented from loading entirely (specifically shaders, by Ny).", 1) .RegisterImportant("I am sorry that it took this long, but there was an immense amount of work to be done from the start.") .RegisterImportant( "Since Penumbra has been in Testing for quite a while, multitudes of bugs and issues cropped up that needed to be dealt with.", @@ -84,6 +86,7 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", 1) + .RegisterEntry("The On-Screen tab was updated and improved and can now display modded actual paths in more useful form.") .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") From 7ba7a6e31915ba84c05fd41ffc46b486b67e9caa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 11:55:30 +0200 Subject: [PATCH 1938/2451] API 5.3 --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 759a8e9d..552246e5 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0 +Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 0400c694..eaaf9f38 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 1); + => (5, 3); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; From ccce087b8706b95997f2c97e8df7c8ee3d9ff44c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 11:59:18 +0200 Subject: [PATCH 1939/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bf020ebf..3a65ed1c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bf020ebf5e4980f1814b336aabbaba5e2e00c362 +Subproject commit 3a65ed1c86a2d5fd5794ff5c0559b02fc25d7224 From 5c9e158da36bbae9fb1299da16ef918b1aef1417 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 10 Aug 2024 10:01:17 +0000 Subject: [PATCH 1940/2451] [CI] Updating repo.json for 1.2.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index efb71542..0e5c7799 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.23", + "AssemblyVersion": "1.2.1.0", + "TestingAssemblyVersion": "1.2.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.23/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a27ce6b0a78243708c1134fcc945a09f05796351 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 12:23:17 +0200 Subject: [PATCH 1941/2451] Update non-testing DalamudApiLevel. --- Penumbra.sln | 3 ++- repo.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 78fa1543..46609f85 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + repo.json = repo.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" @@ -18,7 +19,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/repo.json b/repo.json index 0e5c7799..73ae9eff 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.2.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "TestingDalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", From 7710d9249675e6550f9db2eaaf94e1c570929c23 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Sat, 10 Aug 2024 18:59:02 +0200 Subject: [PATCH 1942/2451] Add another plugin to Copy Support Info --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d99e3fcd..6f0b63ce 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", + "IllusioVitae", "Aetherment", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From 6b0b1629bd2e828d6f237fe64f84c84d6066534c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:22:05 +0200 Subject: [PATCH 1943/2451] Move GuidExtensions to OtterGui (unused atm) --- OtterGui | 2 +- Penumbra/GuidExtensions.cs | 254 ------------------------------------- 2 files changed, 1 insertion(+), 255 deletions(-) delete mode 100644 Penumbra/GuidExtensions.cs diff --git a/OtterGui b/OtterGui index d9486ae5..07a00913 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d9486ae54b5a4b61cf74f79ed27daa659eb1ce5b +Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a diff --git a/Penumbra/GuidExtensions.cs b/Penumbra/GuidExtensions.cs deleted file mode 100644 index fcbc8a3b..00000000 --- a/Penumbra/GuidExtensions.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System.Collections.Frozen; -using OtterGui; - -namespace Penumbra; - -public static class GuidExtensions -{ - private const string Chars = - "0123456789" - + "abcdefghij" - + "klmnopqrst" - + "uv"; - - private static ReadOnlySpan Bytes - => "0123456789abcdefghijklmnopqrstuv"u8; - - private static readonly FrozenDictionary - ReverseChars = Chars.WithIndex().ToFrozenDictionary(t => t.Value, t => (byte)t.Index); - - private static readonly FrozenDictionary ReverseBytes = - ReverseChars.ToFrozenDictionary(kvp => (byte)kvp.Key, kvp => kvp.Value); - - public static unsafe string OptimizedString(this Guid guid) - { - var bytes = stackalloc ulong[2]; - if (!guid.TryWriteBytes(new Span(bytes, 16))) - return guid.ToString("N"); - - var u1 = bytes[0]; - var u2 = bytes[1]; - Span text = - [ - Chars[(int)(u1 & 0x1F)], - Chars[(int)((u1 >> 5) & 0x1F)], - Chars[(int)((u1 >> 10) & 0x1F)], - Chars[(int)((u1 >> 15) & 0x1F)], - Chars[(int)((u1 >> 20) & 0x1F)], - Chars[(int)((u1 >> 25) & 0x1F)], - Chars[(int)((u1 >> 30) & 0x1F)], - Chars[(int)((u1 >> 35) & 0x1F)], - Chars[(int)((u1 >> 40) & 0x1F)], - Chars[(int)((u1 >> 45) & 0x1F)], - Chars[(int)((u1 >> 50) & 0x1F)], - Chars[(int)((u1 >> 55) & 0x1F)], - Chars[(int)((u1 >> 60) | ((u2 & 0x01) << 4))], - Chars[(int)((u2 >> 1) & 0x1F)], - Chars[(int)((u2 >> 6) & 0x1F)], - Chars[(int)((u2 >> 11) & 0x1F)], - Chars[(int)((u2 >> 16) & 0x1F)], - Chars[(int)((u2 >> 21) & 0x1F)], - Chars[(int)((u2 >> 26) & 0x1F)], - Chars[(int)((u2 >> 31) & 0x1F)], - Chars[(int)((u2 >> 36) & 0x1F)], - Chars[(int)((u2 >> 41) & 0x1F)], - Chars[(int)((u2 >> 46) & 0x1F)], - Chars[(int)((u2 >> 51) & 0x1F)], - Chars[(int)((u2 >> 56) & 0x1F)], - Chars[(int)((u2 >> 61) & 0x1F)], - ]; - return new string(text); - } - - public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) - { - if (text.Length != 26) - return Return(out guid); - - var bytes = stackalloc ulong[2]; - if (!ReverseChars.TryGetValue(text[0], out var b0)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[1], out var b1)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[2], out var b2)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[3], out var b3)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[4], out var b4)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[5], out var b5)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[6], out var b6)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[7], out var b7)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[8], out var b8)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[9], out var b9)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[10], out var b10)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[11], out var b11)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[12], out var b12)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[13], out var b13)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[14], out var b14)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[15], out var b15)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[16], out var b16)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[17], out var b17)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[18], out var b18)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[19], out var b19)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[20], out var b20)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[21], out var b21)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[22], out var b22)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[23], out var b23)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[24], out var b24)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[25], out var b25)) - return Return(out guid); - - bytes[0] = b0 - | ((ulong)b1 << 5) - | ((ulong)b2 << 10) - | ((ulong)b3 << 15) - | ((ulong)b4 << 20) - | ((ulong)b5 << 25) - | ((ulong)b6 << 30) - | ((ulong)b7 << 35) - | ((ulong)b8 << 40) - | ((ulong)b9 << 45) - | ((ulong)b10 << 50) - | ((ulong)b11 << 55) - | ((ulong)b12 << 60); - bytes[1] = ((ulong)b12 >> 4) - | ((ulong)b13 << 1) - | ((ulong)b14 << 6) - | ((ulong)b15 << 11) - | ((ulong)b16 << 16) - | ((ulong)b17 << 21) - | ((ulong)b18 << 26) - | ((ulong)b19 << 31) - | ((ulong)b20 << 36) - | ((ulong)b21 << 41) - | ((ulong)b22 << 46) - | ((ulong)b23 << 51) - | ((ulong)b24 << 56) - | ((ulong)b25 << 61); - guid = new Guid(new Span(bytes, 16)); - return true; - - static bool Return(out Guid guid) - { - guid = Guid.Empty; - return false; - } - } - - public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) - { - if (text.Length != 26) - return Return(out guid); - - var bytes = stackalloc ulong[2]; - if (!ReverseBytes.TryGetValue(text[0], out var b0)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[1], out var b1)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[2], out var b2)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[3], out var b3)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[4], out var b4)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[5], out var b5)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[6], out var b6)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[7], out var b7)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[8], out var b8)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[9], out var b9)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[10], out var b10)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[11], out var b11)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[12], out var b12)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[13], out var b13)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[14], out var b14)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[15], out var b15)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[16], out var b16)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[17], out var b17)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[18], out var b18)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[19], out var b19)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[20], out var b20)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[21], out var b21)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[22], out var b22)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[23], out var b23)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[24], out var b24)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[25], out var b25)) - return Return(out guid); - - bytes[0] = b0 - | ((ulong)b1 << 5) - | ((ulong)b2 << 10) - | ((ulong)b3 << 15) - | ((ulong)b4 << 20) - | ((ulong)b5 << 25) - | ((ulong)b6 << 30) - | ((ulong)b7 << 35) - | ((ulong)b8 << 40) - | ((ulong)b9 << 45) - | ((ulong)b10 << 50) - | ((ulong)b11 << 55) - | ((ulong)b12 << 60); - bytes[1] = ((ulong)b12 >> 4) - | ((ulong)b13 << 1) - | ((ulong)b14 << 6) - | ((ulong)b15 << 11) - | ((ulong)b16 << 16) - | ((ulong)b17 << 21) - | ((ulong)b18 << 26) - | ((ulong)b19 << 31) - | ((ulong)b20 << 36) - | ((ulong)b21 << 41) - | ((ulong)b22 << 46) - | ((ulong)b23 << 51) - | ((ulong)b24 << 56) - | ((ulong)b25 << 61); - guid = new Guid(new Span(bytes, 16)); - return true; - - static bool Return(out Guid guid) - { - guid = Guid.Empty; - return false; - } - } -} From 47268ab377db99a91cfc14e7560e459bbb095171 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:22:42 +0200 Subject: [PATCH 1944/2451] Prevent loading crashy shpks. --- Penumbra/Interop/PathResolving/PathResolver.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 67ec4fc3..63bbc8d8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -61,7 +61,7 @@ public class PathResolver : IDisposable, IService ResourceCategory.GameScript => (null, ResolveData.Invalid), // Use actual resolving. ResourceCategory.Chara => Resolve(path, resourceType), - ResourceCategory.Shader => Resolve(path, resourceType), + ResourceCategory.Shader => ResolveShader(path, resourceType), ResourceCategory.Vfx => Resolve(path, resourceType), ResourceCategory.Sound => Resolve(path, resourceType), // EXD Modding in general should probably be prohibited but is currently used for fan translations. @@ -83,6 +83,19 @@ public class PathResolver : IDisposable, IService }; } + /// Replacing the characterstockings.shpk or the characterocclusion.shpk files currently causes crashes, so we just entirely prevent that. + private (FullPath?, ResolveData) ResolveShader(Utf8GamePath gamePath, ResourceType type) + { + if (type is not ResourceType.Shpk) + return Resolve(gamePath, type); + + if (gamePath.Path.EndsWith("occlusion.shpk"u8) + || gamePath.Path.EndsWith("stockings.shpk"u8)) + return (null, ResolveData.Invalid); + + return Resolve(gamePath, type); + } + public (FullPath?, ResolveData) Resolve(Utf8GamePath gamePath, ResourceType type) { using var performance = _performance.Measure(PerformanceType.CharacterResolver); From 8ee326853d3e15c8b4e949dfdacfb0e5f5c1c817 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:24:18 +0200 Subject: [PATCH 1945/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a65ed1c..b7fdfe9d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a65ed1c86a2d5fd5794ff5c0559b02fc25d7224 +Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee From 6e351aa68b5903ff0b691eed43ed1c1d72ae65ff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:06 +0200 Subject: [PATCH 1946/2451] Make mods added via API migrate models if enabled. --- Penumbra/Api/Api/ModsApi.cs | 11 ++++++++++- Penumbra/Services/MigrationManager.cs | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 790121d5..2acdf031 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -15,15 +15,17 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable private readonly ModImportManager _modImportManager; private readonly Configuration _config; private readonly ModFileSystem _modFileSystem; + private readonly MigrationManager _migrationManager; public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, - CommunicatorService communicator) + CommunicatorService communicator, MigrationManager migrationManager) { _modManager = modManager; _modImportManager = modImportManager; _config = config; _modFileSystem = modFileSystem; _communicator = communicator; + _migrationManager = migrationManager; _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); } @@ -81,9 +83,16 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); + if (_config.MigrateImportedModelsToV6) + { + _migrationManager.MigrateMdlDirectory(dir.FullName, false); + _migrationManager.Await(); + } + if (_config.UseFileSystemCompression) new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + return ApiHelpers.Return(PenumbraApiEc.Success, args); } diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 7726f6fd..9041fbd0 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -63,6 +63,9 @@ public class MigrationManager(Configuration config) : IService public void CleanMtrlBackups(string path) => CleanBackups(path, "*.mtrl.bak", "material", MtrlCleanup, TaskType.MtrlCleanup); + public void Await() + => _currentTask?.Wait(); + private void CleanBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) From 3135d5e7e656cc37b6a60744f3cb63d50ca79b08 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:38 +0200 Subject: [PATCH 1947/2451] Make collection combo mousewheel-scrollable with ctrl. --- Penumbra/UI/CollectionTab/CollectionCombo.cs | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 9d195eed..1670be5e 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,14 +1,15 @@ using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; public sealed class CollectionCombo(CollectionManager manager, Func> items) - : FilterComboCache(items, MouseWheelType.None, Penumbra.Log) + : FilterComboCache(items, MouseWheelType.Control, Penumbra.Log) { private readonly ImRaii.Color _color = new(); @@ -20,14 +21,26 @@ public sealed class CollectionCombo(CollectionManager manager, Func obj.Name; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + ImUtf8.HoverTooltip("Control and mouse wheel to scroll."u8); + } } From bb9dd184a369513bd872cdb2e5c866f0d1b96b33 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:52 +0200 Subject: [PATCH 1948/2451] Fix order of gender and model in EQDP drawer. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 970b70cb..5206ece8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -86,11 +86,11 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil ImUtf8.HoverTooltip("Model Set ID"u8); ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); ImUtf8.HoverTooltip("Model Race"u8); ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); ImUtf8.HoverTooltip("Gender"u8); ImGui.TableNextColumn(); From bedf5dab794b1ddbe4a2c5edcd60074d239963f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:58:17 +0200 Subject: [PATCH 1949/2451] Make collection resolver not cache early resolved actors that aren't characters. --- Penumbra/Interop/PathResolving/CollectionResolver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 136da0f5..313c4f8b 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -216,10 +216,13 @@ public sealed unsafe class CollectionResolver( private ModCollection? CollectionByAttributes(Actor actor, ref bool notYetReady) { if (!actor.IsCharacter) + { + Penumbra.Log.Excessive($"Actor to be identified was not yet a Character."); + notYetReady = true; return null; + } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return null; From 1da095be9946e9fa2879ef8b61d201f25b3086d6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 12 Aug 2024 19:00:43 +0000 Subject: [PATCH 1950/2451] [CI] Updating repo.json for 1.2.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 73ae9eff..5a274d73 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.2.1.0", - "TestingAssemblyVersion": "1.2.1.0", + "AssemblyVersion": "1.2.1.1", + "TestingAssemblyVersion": "1.2.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 96f0479b53b62b41f9ccd136c87884d35ecc1234 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:42:29 +0200 Subject: [PATCH 1951/2451] Some cleanup. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs | 1 - Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 1 - Penumbra/UI/Tabs/Debug/DebugTab.cs | 5 ++--- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 07a00913..276327f8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a +Subproject commit 276327f812e2f7e6aac7aee9e5ef0a560b065765 diff --git a/Penumbra.GameData b/Penumbra.GameData index b7fdfe9d..c8708ec5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee +Subproject commit c8708ec5153cb60c9e43b2c53d02b81b2c8175f9 diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 4aae45a2..515f6ff4 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -2,7 +2,6 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Structs; -using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 9d1ab78a..4ab1c6aa 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget; -using OtterGui.Widgets; using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5b82a523..7c6cd01e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; @@ -43,7 +42,6 @@ using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; -using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -721,7 +719,8 @@ public class DebugTab : Window, ITab, IUiService if (!tree) continue; - using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; From 35492837690b93761a7e9c2c52f2c20cb28b5765 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:42:47 +0200 Subject: [PATCH 1952/2451] Order meta entries. --- .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 6 +++++- .../Meta/GlobalEqpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 4 +++- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 21 ++++++++++++------- .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 5 ++++- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 5206ece8..f586045c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -61,7 +61,10 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil } protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() - => Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.GenderRace) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 56c06bc9..b1031b44 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -59,7 +59,10 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() - => Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Eqp + .OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EqpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 5c3c5df5..628cee40 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -58,7 +58,11 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() - => Editor.Est.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Est + .OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.GenderRace) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EstIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 130831a0..bc2e1bde 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -47,7 +47,10 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me } protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() - => Editor.GlobalEqp.Select(identifier => (identifier, (byte)0)); + => Editor.GlobalEqp + .OrderBy(identifier => identifier.Type) + .ThenBy(identifier => identifier.Condition) + .Select(identifier => (identifier, (byte)0)); private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 87ed21dc..1e91731d 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -57,7 +57,9 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() - => Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Gmp + .OrderBy(kvp => kvp.Key.SetId) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref GmpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 58f626fc..e33eb1aa 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -140,7 +140,14 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() - => Editor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Imc + .OrderBy(kvp => kvp.Key.ObjectType) + .ThenBy(kvp => kvp.Key.PrimaryId) + .ThenBy(kvp => kvp.Key.EquipSlot) + .ThenBy(kvp => kvp.Key.BodySlot) + .ThenBy(kvp => kvp.Key.SecondaryId) + .ThenBy(kvp => kvp.Key.Variant) + .Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { @@ -149,18 +156,18 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile if (ret) { - var equipSlot = type switch + var (equipSlot, secondaryId) = type switch { - ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, + ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), + ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), + ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0), + _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), }; identifier = identifier with { ObjectType = type, EquipSlot = equipSlot, - SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + SecondaryId = secondaryId, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index be02e321..6d819b16 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -58,7 +58,10 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() - => Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Rsp + .OrderBy(kvp => kvp.Key.SubRace) + .ThenBy(kvp => kvp.Key.Attribute) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref RspIdentifier identifier) { From a2237773e315be9f3209ab8c6a462e5cdcf8837e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:43:11 +0200 Subject: [PATCH 1953/2451] Update packages. --- Penumbra/Import/Models/Export/MeshExporter.cs | 13 +- .../Import/Models/Export/VertexFragment.cs | 140 ++++++++++++------ .../Import/Models/Import/SubMeshImporter.cs | 2 +- Penumbra/Import/TexToolsImport.cs | 2 +- Penumbra/Import/TexToolsImporter.Archives.cs | 12 +- Penumbra/Penumbra.csproj | 8 +- Penumbra/Services/MigrationManager.cs | 2 +- Penumbra/packages.lock.json | 54 ++++--- 8 files changed, 139 insertions(+), 94 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 219a046e..3a57ab55 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Nodes; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; @@ -23,11 +25,11 @@ public class MeshExporter ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints]) : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); - var extras = new Dictionary(data.Attributes.Length); + var node = new JsonObject(); foreach (var attribute in data.Attributes) - extras.Add(attribute, true); + node[attribute] = true; - instance.WithExtras(JsonContent.CreateFrom(extras)); + instance.WithExtras(node); } } } @@ -233,10 +235,7 @@ public class MeshExporter // Named morph targets aren't part of the specification, however `MESH.extras.targetNames` // is a commonly-accepted means of providing the data. - meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() - { - { "targetNames", shapeNames }, - }); + meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) }; string[] attributes = []; var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 7a82e994..eff34d54 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -1,4 +1,6 @@ +using System; using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Memory; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Export; @@ -11,35 +13,40 @@ and there's reason to overhaul the export pipeline. public struct VertexColorFfxiv : IVertexCustom { - // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] + public IEnumerable> GetEncodingAttributes() + { + // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 0; + public int MaxTextCoords + => 0; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexColorFfxiv(Vector4 ffxivColor) - { - FfxivColor = ffxivColor; - } + => FfxivColor = ffxivColor; public void Add(in VertexMaterialDelta delta) - { - } + { } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - => new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + => new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); public Vector2 GetTexCoord(int index) => throw new ArgumentOutOfRangeException(nameof(index)); public void SetTexCoord(int setIndex, Vector2 coord) - { - } + { } public bool TryGetCustomAttribute(string attributeName, out object? value) { @@ -65,12 +72,17 @@ public struct VertexColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } @@ -78,22 +90,32 @@ public struct VertexColorFfxiv : IVertexCustom public struct VertexTexture1ColorFfxiv : IVertexCustom { - [VertexAttribute("TEXCOORD_0")] + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector2 TexCoord0; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 1; + public int MaxTextCoords + => 1; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) { - TexCoord0 = texCoord0; + TexCoord0 = texCoord0; FfxivColor = ffxivColor; } @@ -103,9 +125,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - { - return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); - } + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); public Vector2 GetTexCoord(int index) => index switch @@ -116,8 +136,10 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public void SetTexCoord(int setIndex, Vector2 coord) { - if (setIndex == 0) TexCoord0 = coord; - if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex)); + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex >= 1) + throw new ArgumentOutOfRangeException(nameof(setIndex)); } public bool TryGetCustomAttribute(string attributeName, out object? value) @@ -144,12 +166,17 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } @@ -157,26 +184,35 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public struct VertexTexture2ColorFfxiv : IVertexCustom { - [VertexAttribute("TEXCOORD_0")] + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector2 TexCoord0; - - [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 2; + public int MaxTextCoords + => 2; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; FfxivColor = ffxivColor; } @@ -187,9 +223,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - { - return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); - } + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); public Vector2 GetTexCoord(int index) => index switch @@ -201,9 +235,12 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom public void SetTexCoord(int setIndex, Vector2 coord) { - if (setIndex == 0) TexCoord0 = coord; - if (setIndex == 1) TexCoord1 = coord; - if (setIndex >= 2) throw new ArgumentOutOfRangeException(nameof(setIndex)); + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex >= 2) + throw new ArgumentOutOfRangeException(nameof(setIndex)); } public bool TryGetCustomAttribute(string attributeName, out object? value) @@ -230,12 +267,17 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index e81bb622..df08eea3 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -61,7 +61,7 @@ public class SubMeshImporter try { - _morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize>(); + _morphNames = node.Mesh.Extras["targetNames"].Deserialize>(); } catch { diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index ba089662..fed06573 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -148,7 +148,7 @@ public partial class TexToolsImporter : IDisposable // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName) - => file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName)); + => file.Entries.FirstOrDefault(e => e is { IsDirectory: false, Key: not null } && e.Key.Contains(fileName)); private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding) { diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index dea343c6..febbe179 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -82,7 +82,7 @@ public partial class TexToolsImporter if (name.Length == 0) throw new Exception("Invalid mod archive: mod meta has no name."); - using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key)); + using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key!)); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); } @@ -155,13 +155,9 @@ public partial class TexToolsImporter ret = directory; // Check that all other files are also contained in the top-level directory. - if (ret.IndexOfAny(new[] - { - '/', - '\\', - }) - >= 0 - || !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\'))) + if (ret.IndexOfAny(['/', '\\']) >= 0 + || !archive.Entries.All(e + => e.Key != null && e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\'))) throw new Exception( "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too."); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 8e143e3c..f42d16ad 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -86,11 +86,11 @@ - + - - - + + + diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 9041fbd0..aa2d445e 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -242,7 +242,7 @@ public class MigrationManager(Configuration config) : IService return; } - var path = Path.Combine(directory, reader.Entry.Key); + var path = Path.Combine(directory, reader.Entry.Key!); using var s = new MemoryStream(); using var e = reader.OpenEntryStream(); e.CopyTo(s); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 8e7106dd..fd3a0a9e 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -4,11 +4,11 @@ "net8.0-windows7.0": { "EmbedIO": { "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "requested": "[3.5.2, )", + "resolved": "3.5.2", + "contentHash": "YU4j+3XvuO8/VPkNf7KWOF1TpMhnyVhXnPsG1mvnDhTJ9D5BZOFXVDvCpE/SkQ1AJ0Aa+dXOVSW3ntgmLL7aJg==", "dependencies": { - "Unosquare.Swan.Lite": "3.0.0" + "Unosquare.Swan.Lite": "3.1.0" } }, "PeNet": { @@ -23,23 +23,26 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.33.0, )", - "resolved": "0.33.0", - "contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw==" + "requested": "[0.37.2, )", + "resolved": "0.37.2", + "contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==", + "dependencies": { + "ZstdSharp.Port": "0.8.0" + } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.0-alpha0030, )", - "resolved": "1.0.0-alpha0030", - "contentHash": "HVL6PcrM0H/uEk96nRZfhtPeYvSFGHnni3g1aIckot2IWVp0jLMH5KWgaWfsatEz4Yds3XcdSLUWmJZivDBUPA==" + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.0-alpha0030, )", - "resolved": "1.0.0-alpha0030", - "contentHash": "nsoJWAFhXgEky9bVCY0zLeZVDx+S88u7VjvuebvMb6dJiNyFOGF6FrrMHiJe+x5pcVBxxlc3VoXliBF7r/EqYA==", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==", "dependencies": { - "SharpGLTF.Runtime": "1.0.0-alpha0030" + "SharpGLTF.Runtime": "1.0.1" } }, "SixLabors.ImageSharp": { @@ -50,8 +53,8 @@ }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2023.3.0", - "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + "resolved": "2024.2.0", + "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", @@ -73,10 +76,10 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.0-alpha0030", - "contentHash": "Ysn+fyj9EVXj6mfG0BmzSTBGNi/QvcnTrMd54dBMOlI/TsMRvnOY3JjTn0MpeH2CgHXX4qogzlDt4m+rb3n4Og==", + "resolved": "1.0.1", + "contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==", "dependencies": { - "SharpGLTF.Core": "1.0.0-alpha0030" + "SharpGLTF.Core": "1.0.1" } }, "System.Formats.Asn1": { @@ -99,16 +102,21 @@ }, "Unosquare.Swan.Lite": { "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==", + "resolved": "3.1.0", + "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==", "dependencies": { "System.ValueTuple": "4.5.0" } }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.8.0", + "contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA==" + }, "ottergui": { "type": "Project", "dependencies": { - "JetBrains.Annotations": "[2023.3.0, )", + "JetBrains.Annotations": "[2024.2.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" } }, @@ -122,7 +130,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.2.0, )", + "Penumbra.Api": "[5.3.0, )", "Penumbra.String": "[1.0.4, )" } }, From 726340e4f8cc4a8ab22f9629e5362a800f3ae962 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:45:18 +0200 Subject: [PATCH 1954/2451] Meh. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c8708ec5..c43c5cac 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c8708ec5153cb60c9e43b2c53d02b81b2c8175f9 +Subproject commit c43c5cac4cee092bf0aed8d46bab112b037ef8f2 From f3346c5d7e52f1ba86332ee2e30f77f144bf9ee3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 18:20:29 +0200 Subject: [PATCH 1955/2451] Add Targa support. --- Penumbra/Import/Textures/Texture.cs | 15 ++++++++++++ Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Import/Textures/TextureManager.cs | 23 +++++++++++-------- .../AdvancedWindow/ModEditWindow.Textures.cs | 1 + 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c5207e94..ae0aabd9 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -10,6 +10,21 @@ public enum TextureType Tex, Png, Bitmap, + Targa, +} + +internal static class TextureTypeExtensions +{ + public static TextureType ReduceToBehaviour(this TextureType type) + => type switch + { + TextureType.Dds => TextureType.Dds, + TextureType.Tex => TextureType.Tex, + TextureType.Png => TextureType.Png, + TextureType.Bitmap => TextureType.Png, + TextureType.Targa => TextureType.Png, + _ => TextureType.Unknown, + }; } public sealed class Texture : IDisposable diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index c83604e4..b0a65ac0 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -66,7 +66,7 @@ public static class TextureDrawer current.Load(textures, paths[0]); } - fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); } ImGui.SameLine(); diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index cc785d02..4afc8a56 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -165,11 +165,13 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur return; } + var imageTypeBehaviour = image.Type.ReduceToBehaviour(); var dds = _type switch { - CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, + cancel, rgba, width, height), - CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), @@ -218,7 +220,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur => Path.GetExtension(path).ToLowerInvariant() switch { ".dds" => (LoadDds(path), TextureType.Dds), - ".png" => (LoadPng(path), TextureType.Png), + ".png" => (LoadImageSharp(path), TextureType.Png), + ".tga" => (LoadImageSharp(path), TextureType.Targa), + ".bmp" => (LoadImageSharp(path), TextureType.Bitmap), ".tex" => (LoadTex(path), TextureType.Tex), _ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."), }; @@ -234,17 +238,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public BaseImage LoadDds(string path) => ScratchImage.LoadDDS(path); - /// Load a .png file from drive using ImageSharp. - public BaseImage LoadPng(string path) + /// Load a supported file type from drive using ImageSharp. + public BaseImage LoadImageSharp(string path) { using var stream = File.OpenRead(path); return Image.Load(stream); } - /// Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. + /// Convert an existing image to ImageSharp. Does not create a deep copy of an existing ImageSharp file and just returns the existing one. public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: return input; case TextureType.Dds: @@ -261,7 +265,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: { @@ -291,7 +295,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: { @@ -470,6 +474,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Png => $"Custom {_width} x {_height} .png Image", + TextureType.Targa => $"Custom {_width} x {_height} .tga Image", TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", _ => "Unknown Image", }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 652ecb49..67a27a0b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -329,5 +329,6 @@ public partial class ModEditWindow ".png", ".dds", ".tex", + ".tga", }; } From c4853434c8842ee8fa390e5b716134eca407dbfd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 18:25:43 +0200 Subject: [PATCH 1956/2451] Whatever. --- Penumbra/Import/Textures/TextureManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 4afc8a56..996b5dbf 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -474,7 +474,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Png => $"Custom {_width} x {_height} .png Image", - TextureType.Targa => $"Custom {_width} x {_height} .tga Image", + TextureType.Targa => $"Custom {_width} x {_height} .tga Image", TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", _ => "Unknown Image", }; From ded910d8a128b7ffb9cc9483c201df847a2b9e4c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 21:21:38 +0200 Subject: [PATCH 1957/2451] Add Targa export. --- Penumbra.Api | 2 +- Penumbra/Api/Api/EditingApi.cs | 2 + Penumbra/Import/Textures/CombinedTexture.cs | 12 ++++ Penumbra/Import/Textures/TextureManager.cs | 56 ++++++++++++++----- .../AdvancedWindow/ModEditWindow.Textures.cs | 17 +++--- 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 552246e5..a38e9bcf 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a +Subproject commit a38e9bcfb80c456102945bbb4c59f5621cae0442 diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs index 93345053..e50b7a1b 100644 --- a/Penumbra/Api/Api/EditingApi.cs +++ b/Penumbra/Api/Api/EditingApi.cs @@ -10,6 +10,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA => textureType switch { TextureType.Png => textureManager.SavePng(inputFile, outputFile), + TextureType.Targa => textureManager.SaveTga(inputFile, outputFile), TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), @@ -26,6 +27,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA => textureType switch { TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 98b87ac3..c1a22088 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -55,6 +55,14 @@ public partial class CombinedTexture : IDisposable SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } + public void SaveAsTarga(TextureManager textures, string path) + { + if (!IsLoaded || _current == null) + return; + + SaveTask = textures.SaveTga(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); + } + private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) { if (!IsLoaded || _current == null) @@ -72,6 +80,7 @@ public partial class CombinedTexture : IDisposable ".tex" => TextureType.Tex, ".dds" => TextureType.Dds, ".png" => TextureType.Png, + ".tga" => TextureType.Targa, _ => TextureType.Unknown, }; @@ -85,6 +94,9 @@ public partial class CombinedTexture : IDisposable break; case TextureType.Png: SaveAsPng(textures, path); + break; + case TextureType.Targa: + SaveAsTarga(textures, path); break; default: throw new ArgumentException( diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 996b5dbf..7118f8af 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -8,6 +8,7 @@ using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -33,10 +34,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } public Task SavePng(string input, string output) - => Enqueue(new SavePngAction(this, input, output)); + => Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Png)); public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); + => Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Png, rgba, width, height)); + + public Task SaveTga(string input, string output) + => Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Targa)); + + public Task SaveTga(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + => Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Targa, rgba, width, height)); + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); @@ -66,44 +74,65 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur return t; } - private class SavePngAction : IAction + private class SaveImageSharpAction : IAction { private readonly TextureManager _textures; private readonly string _outputPath; private readonly ImageInputData _input; + private readonly TextureType _type; - public SavePngAction(TextureManager textures, string input, string output) + public SaveImageSharpAction(TextureManager textures, string input, string output, TextureType type) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; + _type = type; + if (_type.ReduceToBehaviour() is not TextureType.Png) + throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp."); } - public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveImageSharpAction(TextureManager textures, BaseImage image, string path, TextureType type, byte[]? rgba = null, int width = 0, + int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; + _type = type; + if (_type.ReduceToBehaviour() is not TextureType.Png) + throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp."); } public void Execute(CancellationToken cancel) { - _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as {_type} to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); cancel.ThrowIfCancellationRequested(); - Image? png = null; + Image? data = null; if (image.Type is TextureType.Unknown) { if (rgba != null && width > 0 && height > 0) - png = ConvertToPng(rgba, width, height).AsPng!; + data = ConvertToPng(rgba, width, height).AsPng!; } else { - png = ConvertToPng(image, cancel, rgba).AsPng!; + data = ConvertToPng(image, cancel, rgba).AsPng!; } cancel.ThrowIfCancellationRequested(); - png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel); + switch (_type) + { + case TextureType.Png: + data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) + .Wait(cancel); + return; + case TextureType.Targa: + data?.SaveAsync(_outputPath, new TgaEncoder() + { + Compression = TgaCompression.None, + BitsPerPixel = TgaBitsPerPixel.Pixel32, + }, cancel).Wait(cancel); + return; + } } public override string ToString() @@ -111,7 +140,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public bool Equals(IAction? other) { - if (other is not SavePngAction rhs) + if (other is not SaveImageSharpAction rhs) return false; return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); @@ -168,9 +197,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur var imageTypeBehaviour = image.Type.ReduceToBehaviour(); var dds = _type switch { - CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, - cancel, rgba, - width, height), + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, + rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 67a27a0b..c08e8a8e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -85,7 +85,7 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(newDesc); } - } + } private void RedrawOnSaveBox() { @@ -128,7 +128,8 @@ public partial class ModEditWindow ? "This saves the texture in place. This is not revertible." : $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save."; - var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { @@ -141,17 +142,18 @@ public partial class ModEditWindow if (ImGui.Button("Save as TEX", buttonSize2)) OpenSaveAsDialog(".tex"); - if (ImGui.Button("Export as PNG", buttonSize2)) + if (ImGui.Button("Export as TGA", buttonSize3)) + OpenSaveAsDialog(".tga"); + ImGui.SameLine(); + if (ImGui.Button("Export as PNG", buttonSize3)) OpenSaveAsDialog(".png"); ImGui.SameLine(); - if (ImGui.Button("Export as DDS", buttonSize2)) + if (ImGui.Button("Export as DDS", buttonSize3)) OpenSaveAsDialog(".dds"); - ImGui.NewLine(); var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy; - var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3, "This converts the texture to BC7 format in place. This is not revertible.", !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) @@ -226,7 +228,8 @@ public partial class ModEditWindow private void OpenSaveAsDialog(string defaultExtension) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS, PNG or TGA...", "Textures{.png,.dds,.tex,.tga},.tex,.dds,.png,.tga", fileName, + defaultExtension, (a, b) => { if (a) From 3e2c9177a71ecd9a64493b02359b7ba16188c651 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:48:02 +0200 Subject: [PATCH 1958/2451] Prepare API for new meta format. --- Penumbra/Api/Api/MetaApi.cs | 258 ++++++++++++++++++++++++++++++- Penumbra/Api/Api/TemporaryApi.cs | 26 +--- 2 files changed, 257 insertions(+), 27 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index ce1a9def..6f3ed51e 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -1,16 +1,21 @@ +using Dalamud.Plugin.Services; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; namespace Penumbra.Api.Api; -public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService +public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers) + : IPenumbraApiMeta, IApiService { - public const int CurrentVersion = 0; + public const int CurrentVersion = 1; public string GetPlayerMetaManipulations() { @@ -24,7 +29,32 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) return CompressMetaManipulations(collection); } + public Task GetPlayerMetaManipulationsAsync() + { + return Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false); + return CompressMetaManipulations(playerCollection); + }); + } + + public Task GetMetaManipulationsAsync(int gameObjectIdx) + { + return Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(() => + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + return collection; + }).ConfigureAwait(false); + return CompressMetaManipulations(playerCollection); + }); + } + internal static string CompressMetaManipulations(ModCollection collection) + => CompressMetaManipulationsV0(collection); + + private static string CompressMetaManipulationsV0(ModCollection collection) { var array = new JArray(); if (collection.MetaCache is { } cache) @@ -38,6 +68,228 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } - return Functions.ToCompressedBase64(array, CurrentVersion); + return Functions.ToCompressedBase64(array, 0); + } + + private static unsafe string CompressMetaManipulationsV1(ModCollection? collection) + { + using var ms = new MemoryStream(); + ms.Capacity = 1024; + using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true)) + { + zipStream.Write((byte)1); + zipStream.Write("META0001"u8); + if (collection?.MetaCache is not { } cache) + { + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + } + else + { + WriteCache(zipStream, cache.Imc); + WriteCache(zipStream, cache.Eqp); + WriteCache(zipStream, cache.Eqdp); + WriteCache(zipStream, cache.Est); + WriteCache(zipStream, cache.Rsp); + WriteCache(zipStream, cache.Gmp); + cache.GlobalEqp.EnterReadLock(); + + try + { + zipStream.Write(cache.GlobalEqp.Count); + foreach (var (globalEqp, _) in cache.GlobalEqp) + zipStream.Write(new ReadOnlySpan(&globalEqp, sizeof(GlobalEqpManipulation))); + } + finally + { + cache.GlobalEqp.ExitReadLock(); + } + } + } + + ms.Flush(); + ms.Position = 0; + var data = ms.GetBuffer().AsSpan(0, (int)ms.Length); + return Convert.ToBase64String(data); + + void WriteCache(Stream stream, MetaCacheBase metaCache) + where TKey : unmanaged, IMetaIdentifier + where TValue : unmanaged + { + metaCache.EnterReadLock(); + try + { + stream.Write(metaCache.Count); + foreach (var (identifier, (_, value)) in metaCache) + { + stream.Write(identifier); + stream.Write(value); + } + } + finally + { + metaCache.ExitReadLock(); + } + } + } + + /// + /// Convert manipulations from a transmitted base64 string to actual manipulations. + /// The empty string is treated as an empty set. + /// Only returns true if all conversions are successful and distinct. + /// + internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (manipString.Length == 0) + { + manips = new MetaDictionary(); + return true; + } + + try + { + var bytes = Convert.FromBase64String(manipString); + using var compressedStream = new MemoryStream(bytes); + using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var resultStream = new MemoryStream(); + zipStream.CopyTo(resultStream); + resultStream.Flush(); + resultStream.Position = 0; + var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); + var version = data[0]; + data = data[1..]; + switch (version) + { + case 0: return ConvertManipsV0(data, out manips); + case 1: return ConvertManipsV1(data, out manips); + default: + Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); + manips = null; + return false; + } + } + catch (Exception ex) + { + Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}"); + manips = null; + return false; + } + } + + private static bool ConvertManipsV1(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (!data.StartsWith("META0001"u8)) + { + Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix."); + manips = null; + return false; + } + + manips = new MetaDictionary(); + var r = new SpanBinaryReader(data[8..]); + var imcCount = r.ReadInt32(); + for (var i = 0; i < imcCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqpCount = r.ReadInt32(); + for (var i = 0; i < eqpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqdpCount = r.ReadInt32(); + for (var i = 0; i < eqdpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var estCount = r.ReadInt32(); + for (var i = 0; i < estCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var rspCount = r.ReadInt32(); + for (var i = 0; i < rspCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var gmpCount = r.ReadInt32(); + for (var i = 0; i < gmpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var globalEqpCount = r.ReadInt32(); + for (var i = 0; i < globalEqpCount; ++i) + { + var manip = r.Read(); + if (!manip.Validate() || !manips.TryAdd(manip)) + return false; + } + + return true; + } + + private static bool ConvertManipsV0(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + var json = Encoding.UTF8.GetString(data); + manips = JsonConvert.DeserializeObject(json); + return manips != null; + } + + internal void TestMetaManipulations() + { + var collection = collectionResolver.PlayerCollection(); + var dict = new MetaDictionary(collection.MetaCache); + var count = dict.Count; + + var watch = Stopwatch.StartNew(); + var v0 = CompressMetaManipulationsV0(collection); + var v0Time = watch.ElapsedMilliseconds; + + watch.Restart(); + var v1 = CompressMetaManipulationsV1(collection); + var v1Time = watch.ElapsedMilliseconds; + + watch.Restart(); + var v1Success = ConvertManips(v1, out var v1Roundtrip); + var v1RoundtripTime = watch.ElapsedMilliseconds; + + watch.Restart(); + var v0Success = ConvertManips(v0, out var v0Roundtrip); + var v0RoundtripTime = watch.ElapsedMilliseconds; + + Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal"); + Penumbra.Log.Information( + $"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}"); + Penumbra.Log.Information( + $"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}"); } } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index f02b0d94..516b4347 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -1,10 +1,8 @@ -using OtterGui; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.String.Classes; @@ -62,7 +60,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch @@ -88,7 +86,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch @@ -153,24 +151,4 @@ public class TemporaryApi( return true; } - - /// - /// Convert manipulations from a transmitted base64 string to actual manipulations. - /// The empty string is treated as an empty set. - /// Only returns true if all conversions are successful and distinct. - /// - private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) - { - if (manipString.Length == 0) - { - manips = new MetaDictionary(); - return true; - } - - if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion) - return true; - - manips = null; - return false; - } } From 233a9996507521c6a184743cc2b0314d73ac427b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:48:42 +0200 Subject: [PATCH 1959/2451] Add button to remove default-valued meta entries. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 64 ++++++++++++++++++- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 15 +++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index bacf4122..d9018ff6 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,12 +1,15 @@ using System.Collections.Frozen; using OtterGui.Services; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService +public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManager, ImcChecker imcChecker) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -62,6 +65,65 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService Changes = false; } + public void DeleteDefaultValues() + { + var clone = Clone(); + Clear(); + foreach (var (key, value) in clone.Imc) + { + var defaultEntry = imcChecker.GetDefaultEntry(key, false); + if (!defaultEntry.Entry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Eqp) + { + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Eqdp) + { + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Est) + { + var defaultEntry = EstFile.GetDefault(metaFileManager, key); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Gmp) + { + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Rsp) + { + var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + } + public void Apply(IModDataContainer container) { if (!Changes) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3ec6a4d5..49eac96e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -16,21 +16,21 @@ public partial class ModEditWindow private void DrawMetaTab() { - using var tab = ImRaii.TabItem("Meta Manipulations"); + using var tab = ImUtf8.TabItem("Meta Manipulations"u8); if (!tab) return; DrawOptionSelectHeader(); var setsEqual = !_editor.MetaEditor.Changes; - var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + var tt = setsEqual ? "No changes staged."u8 : "Apply the currently staged changes to the option."u8; ImGui.NewLine(); - if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) + if (ImUtf8.ButtonEx("Apply Changes"u8, tt, Vector2.Zero, setsEqual)) _editor.MetaEditor.Apply(_editor.Option!); ImGui.SameLine(); - tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) + tt = setsEqual ? "No changes staged."u8 : "Revert all currently staged changes."u8; + if (ImUtf8.ButtonEx("Revert Changes"u8, tt, Vector2.Zero, setsEqual)) _editor.MetaEditor.Load(_editor.Mod!, _editor.Option!); ImGui.SameLine(); @@ -40,8 +40,11 @@ public partial class ModEditWindow ImGui.SameLine(); CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor); ImGui.SameLine(); - if (ImGui.Button("Write as TexTools Files")) + if (ImUtf8.Button("Write as TexTools Files"u8)) _metaFileManager.WriteAllTexToolsMeta(Mod!); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8)) + _editor.MetaEditor.DeleteDefaultValues(); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) From a3c22f2826b4091010e6f76ad5a236e1c92b44fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:49:07 +0200 Subject: [PATCH 1960/2451] Fix ordering of meta entries. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index f586045c..aea2ef78 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -61,7 +61,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil } protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() - => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId) + => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.GenderRace) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index b1031b44..733517f3 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -60,7 +60,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() => Editor.Eqp - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 628cee40..a33f8b7b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -59,7 +59,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() => Editor.Est - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.GenderRace) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index bc2e1bde..5d67ddcf 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -49,7 +49,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() => Editor.GlobalEqp .OrderBy(identifier => identifier.Type) - .ThenBy(identifier => identifier.Condition) + .ThenBy(identifier => identifier.Condition.Id) .Select(identifier => (identifier, (byte)0)); private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 1e91731d..bd42b60a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -58,7 +58,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() => Editor.Gmp - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref GmpIdentifier identifier) diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index e33eb1aa..4e949b98 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -142,11 +142,11 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() => Editor.Imc .OrderBy(kvp => kvp.Key.ObjectType) - .ThenBy(kvp => kvp.Key.PrimaryId) + .ThenBy(kvp => kvp.Key.PrimaryId.Id) .ThenBy(kvp => kvp.Key.EquipSlot) .ThenBy(kvp => kvp.Key.BodySlot) - .ThenBy(kvp => kvp.Key.SecondaryId) - .ThenBy(kvp => kvp.Key.Variant) + .ThenBy(kvp => kvp.Key.SecondaryId.Id) + .ThenBy(kvp => kvp.Key.Variant.Id) .Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) From d713d5a112d138d43fc6d4470711cd8d1c711631 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:28:49 +0200 Subject: [PATCH 1961/2451] Improve handling of mod selection. --- Penumbra/Communication/CollectionChange.cs | 3 + .../CollectionInheritanceChanged.cs | 3 + Penumbra/Communication/ModSettingChanged.cs | 4 +- Penumbra/Mods/Editor/ModMerger.cs | 39 ++++--- Penumbra/Mods/ModSelection.cs | 104 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 7 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 18 +-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 82 ++++---------- Penumbra/UI/ModsTab/ModPanel.cs | 35 +++--- Penumbra/UI/ModsTab/ModPanelHeader.cs | 34 ++++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 43 ++++---- Penumbra/UI/Tabs/ModsTab.cs | 3 +- 12 files changed, 227 insertions(+), 148 deletions(-) create mode 100644 Penumbra/Mods/ModSelection.cs diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 95d4ac4d..2788177d 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -46,5 +46,8 @@ public sealed class CollectionChange() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index dbcf9e4a..30af2b20 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 7fda2f35..d4bf00be 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -35,5 +34,8 @@ public sealed class ModSettingChanged() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index b059813b..d75ac671 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -10,22 +10,21 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.UI.ModsTab; namespace Penumbra.Mods.Editor; public class ModMerger : IDisposable, IService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly ModGroupEditor _editor; - private readonly ModFileSystemSelector _selector; - private readonly DuplicateManager _duplicates; - private readonly ModManager _mods; - private readonly ModCreator _creator; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly ModGroupEditor _editor; + private readonly ModSelection _selection; + private readonly DuplicateManager _duplicates; + private readonly ModManager _mods; + private readonly ModCreator _creator; public Mod? MergeFromMod - => _selector.Selected; + => _selection.Mod; public Mod? MergeToMod; public string OptionGroupName = "Merges"; @@ -41,23 +40,23 @@ public class ModMerger : IDisposable, IService public readonly IReadOnlyList Warnings = new List(); public Exception? Error { get; private set; } - public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { - _editor = editor; - _selector = selector; - _duplicates = duplicates; - _communicator = communicator; - _creator = creator; - _config = config; - _mods = mods; - _selector.SelectionChanged += OnSelectionChange; + _editor = editor; + _selection = selection; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _config = config; + _mods = mods; + _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); } public void Dispose() { - _selector.SelectionChanged -= OnSelectionChange; + _selection.Unsubscribe(OnSelectionChange); _communicator.ModPathChanged.Unsubscribe(OnModPathChange); } @@ -390,7 +389,7 @@ public class ModMerger : IDisposable, IService } } - private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state) + private void OnSelectionChange(Mod? oldSelection, Mod? newSelection) { if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text) OptionName = newSelection?.Name.Text ?? string.Empty; diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs new file mode 100644 index 00000000..73d0272b --- /dev/null +++ b/Penumbra/Mods/ModSelection.cs @@ -0,0 +1,104 @@ +using OtterGui.Classes; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Mods; + +/// +/// Triggered whenever the selected mod changes +/// +/// Parameter is the old selected mod. +/// Parameter is the new selected mod +/// +/// +public class ModSelection : EventWrapper +{ + private readonly ActiveCollections _collections; + private readonly EphemeralConfig _config; + private readonly CommunicatorService _communicator; + + public ModSelection(CommunicatorService communicator, ModManager mods, ActiveCollections collections, EphemeralConfig config) + : base(nameof(ModSelection)) + { + _communicator = communicator; + _collections = collections; + _config = config; + if (_config.LastModPath.Length > 0) + SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase))); + + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection); + _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection); + _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); + } + + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + + + public void SelectMod(Mod? mod) + { + if (mod == Mod) + return; + + var oldMod = Mod; + Mod = mod; + OnCollectionChange(CollectionType.Current, null, _collections.Current, string.Empty); + Invoke(oldMod, Mod); + _config.LastModPath = mod?.ModPath.Name ?? string.Empty; + _config.Save(); + } + + protected override void Dispose(bool _) + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.CollectionInheritanceChanged.Unsubscribe(OnInheritanceChange); + _communicator.ModSettingChanged.Unsubscribe(OnSettingChange); + } + + private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _2) + { + if (type is CollectionType.Current && oldCollection != newCollection) + UpdateSettings(); + } + + private void OnSettingChange(ModCollection collection, ModSettingChange _1, Mod? mod, Setting _2, int _3, bool _4) + { + if (collection == _collections.Current && mod == Mod) + UpdateSettings(); + } + + private void OnInheritanceChange(ModCollection collection, bool arg2) + { + if (collection == _collections.Current) + UpdateSettings(); + } + + private void UpdateSettings() + { + if (Mod == null) + { + Settings = ModSettings.Empty; + Collection = ModCollection.Empty; + } + else + { + (var settings, Collection) = _collections.Current[Mod.Index]; + Settings = settings ?? ModSettings.Empty; + } + } + + public enum Priority + { + /// + ModPanel = 0, + + /// + ModMerger = 0, + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 13458252..7bb067d8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -36,8 +36,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; - public readonly MigrationManager MigrationManager; - private readonly PerformanceTracker _performance; private readonly ModEditor _editor; private readonly Configuration _config; @@ -587,7 +585,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework, MetaDrawers metaDrawers, MigrationManager migrationManager, - MtrlTabFactory mtrlTabFactory) + MtrlTabFactory mtrlTabFactory, ModSelection selection) : base(WindowBaseLabel) { _performance = performance; @@ -604,7 +602,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _models = models; _fileDialog = fileDialog; _framework = framework; - MigrationManager = migrationManager; _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, @@ -622,6 +619,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; + if (IsOpen && selection.Mod != null) + ChangeMod(selection.Mod); } public void Dispose() diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 0f9b2518..3972e350 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -5,24 +5,24 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; +using Penumbra.Mods; using Penumbra.UI.CollectionTab; -using Penumbra.UI.ModsTab; namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModFileSystemSelector _selector; - private readonly CollectionResolver _resolver; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; - public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector, + public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, CollectionResolver resolver) { _tutorial = tutorial; - _selector = selector; + _selection = selection; _resolver = resolver; _activeCollections = collectionManager.Active; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); @@ -115,7 +115,7 @@ public class CollectionSelectHeader : IUiService private (ModCollection?, string, string, bool) GetInheritedCollectionInfo() { - var collection = _selector.Selected == null ? null : _selector.SelectedSettingCollection; + var collection = _selection.Mod == null ? null : _selection.Collection; return CheckCollection(collection, true) switch { CollectionState.Unavailable => (null, "Not Inherited", diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 42689efb..2f76340b 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -25,7 +25,6 @@ namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector, IUiService { private readonly CommunicatorService _communicator; - private readonly MessageService _messager; private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; @@ -33,15 +32,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) - { - var mod = _modManager.FirstOrDefault(m - => string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase)); - if (mod != null) - SelectByValue(mod); - } - + if (_selection.Mod != null) + SelectByValue(_selection.Mod); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector); _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector); _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector); _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector); _communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector); _communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector); - OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, ""); - } - + SetFilterDirty(); + SelectionChanged += OnSelectionChanged; + } + public void SetRenameSearchPath(RenameField value) { switch (value) @@ -449,12 +439,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector _selection.SelectMod(newSelection); + #endregion #region Filters @@ -567,7 +529,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. protected override bool ChangeFilter(string filterValue) { - Filter.Parse(filterValue); + _filter.Parse(filterValue); return true; } @@ -597,7 +559,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Apply the string filters. private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) - => !Filter.IsVisible(leaf); + => !_filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index ee6fab1f..9d6ead62 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -10,22 +10,23 @@ namespace Penumbra.UI.ModsTab; public class ModPanel : IDisposable, IUiService { - private readonly MultiModPanel _multiModPanel; - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; - private bool _resetCursor; + private readonly MultiModPanel _multiModPanel; + private readonly ModSelection _selection; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; + private bool _resetCursor; - public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, + public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModEditWindow editWindow, ModPanelTabBar tabs, MultiModPanel multiModPanel, CommunicatorService communicator) { - _selector = selector; - _editWindow = editWindow; - _tabs = tabs; - _multiModPanel = multiModPanel; - _header = new ModPanelHeader(pi, communicator); - _selector.SelectionChanged += OnSelectionChange; + _selection = selection; + _editWindow = editWindow; + _tabs = tabs; + _multiModPanel = multiModPanel; + _header = new ModPanelHeader(pi, communicator); + _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel); + OnSelectionChange(null, _selection.Mod); } public void Draw() @@ -52,17 +53,17 @@ public class ModPanel : IDisposable, IUiService public void Dispose() { - _selector.SelectionChanged -= OnSelectionChange; + _selection.Unsubscribe(OnSelectionChange); _header.Dispose(); } private bool _valid; private Mod _mod = null!; - private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _) + private void OnSelectionChange(Mod? old, Mod? mod) { _resetCursor = true; - if (mod == null || _selector.Selected == null) + if (mod == null || _selection.Mod == null) { _editWindow.IsOpen = false; _valid = false; @@ -73,7 +74,7 @@ public class ModPanel : IDisposable, IUiService _editWindow.ChangeMod(mod); _valid = true; _mod = mod; - _header.UpdateModData(_mod); + _header.ChangeMod(_mod); _tabs.Settings.Reset(); _tabs.Edit.Reset(); } diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 6c974f9c..aafbffa6 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -18,7 +18,8 @@ public class ModPanelHeader : IDisposable private readonly IFontHandle _nameFont; private readonly CommunicatorService _communicator; - private float _lastPreSettingsHeight = 0; + private float _lastPreSettingsHeight; + private bool _dirty = true; public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) { @@ -33,6 +34,7 @@ public class ModPanelHeader : IDisposable /// public void Draw() { + UpdateModData(); var height = ImGui.GetContentRegionAvail().Y; var maxHeight = 3 * height / 4; using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers @@ -49,16 +51,25 @@ public class ModPanelHeader : IDisposable _lastPreSettingsHeight = ImGui.GetCursorPosY(); } + public void ChangeMod(Mod mod) + { + _mod = mod; + _dirty = true; + } + /// /// Update all mod header data. Should someone change frame padding or item spacing, /// or his default font, this will break, but he will just have to select a different mod to restore. /// - public void UpdateModData(Mod mod) + private void UpdateModData() { + if (!_dirty) + return; + + _dirty = false; _lastPreSettingsHeight = 0; - _mod = mod; // Name - var name = $" {mod.Name} "; + var name = $" {_mod.Name} "; if (name != _modName) { using var f = _nameFont.Push(); @@ -67,16 +78,16 @@ public class ModPanelHeader : IDisposable } // Author - if (mod.Author != _modAuthor) + if (_mod.Author != _modAuthor) { - var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}"; - _modAuthor = mod.Author.Text; + var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; + _modAuthor = _mod.Author.Text; _modAuthorWidth = ImGui.CalcTextSize(author).X; _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; } // Version - var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty; + var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; if (version != _modVersion) { _modVersion = version; @@ -84,9 +95,9 @@ public class ModPanelHeader : IDisposable } // Website - if (_modWebsite != mod.Website) + if (_modWebsite != _mod.Website) { - _modWebsite = mod.Website; + _modWebsite = _mod.Website; _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; @@ -253,7 +264,6 @@ public class ModPanelHeader : IDisposable { const ModDataChangeType relevantChanges = ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version; - if ((changeType & relevantChanges) != 0) - UpdateModData(mod); + _dirty = (changeType & relevantChanges) != 0; } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 7e3b8a95..d2fbd0cd 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -3,9 +3,9 @@ using OtterGui.Raii; using OtterGui; using OtterGui.Services; using OtterGui.Widgets; -using Penumbra.Collections; using Penumbra.UI.Classes; using Penumbra.Collections.Manager; +using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; @@ -16,16 +16,14 @@ namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab( CollectionManager collectionManager, ModManager modManager, - ModFileSystemSelector selector, + ModSelection selection, TutorialService tutorial, CommunicatorService communicator, ModGroupDrawer modGroupDrawer) : ITab, IUiService { - private bool _inherited; - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private int? _currentPriority; + private bool _inherited; + private int? _currentPriority; public ReadOnlySpan Label => "Settings"u8; @@ -42,12 +40,10 @@ public class ModPanelSettingsTab( if (!child) return; - _settings = selector.SelectedSettings; - _collection = selector.SelectedSettingCollection; - _inherited = _collection != collectionManager.Active.Current; + _inherited = selection.Collection != collectionManager.Active.Current; DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier); + communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); DrawEnabledInput(); tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); @@ -55,11 +51,11 @@ public class ModPanelSettingsTab( tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); - communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier); + communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); - modGroupDrawer.Draw(selector.Selected!, _settings); + modGroupDrawer.Draw(selection.Mod!, selection.Settings); UiHelpers.DefaultLineSpace(); - communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier); + communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier); } /// Draw a big red bar if the current setting is inherited. @@ -70,8 +66,8 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); + if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width)) + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -80,12 +76,12 @@ public class ModPanelSettingsTab( /// Draw a checkbox for the enabled status of the mod. private void DrawEnabledInput() { - var enabled = _settings.Enabled; + var enabled = selection.Settings.Enabled; if (!ImGui.Checkbox("Enabled", ref enabled)) return; - modManager.SetKnown(selector.Selected!); - collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled); + modManager.SetKnown(selection.Mod!); + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); } /// @@ -95,15 +91,16 @@ public class ModPanelSettingsTab( private void DrawPriorityInput() { using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority.Value; + var settings = selection.Settings; + var priority = _currentPriority ?? settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _settings.Priority.Value) - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + if (_currentPriority != settings.Priority.Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, new ModPriority(_currentPriority.Value)); _currentPriority = null; @@ -120,13 +117,13 @@ public class ModPanelSettingsTab( private void DrawRemoveSettings() { const string text = "Inherit Settings"; - if (_inherited || _settings == ModSettings.Empty) + if (_inherited || selection.Settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true); + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 50fdc1d3..87338bdb 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -82,8 +82,7 @@ public class ModsTab( + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" - + $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"); } } From 4970e571316fc6b6394b029e03bd26119fde98bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:29:12 +0200 Subject: [PATCH 1962/2451] Improve tooltip of file redirections tab. --- OtterGui | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/OtterGui b/OtterGui index 276327f8..9217ac56 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 276327f812e2f7e6aac7aee9e5ef0a560b065765 +Subproject commit 9217ac56697bc8285ced483b1fd4734fd36ba64d diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index ffa7473d..b07633b6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,8 +1,10 @@ +using System.Linq; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; @@ -144,22 +146,20 @@ public partial class ModEditWindow private static string DrawFileTooltip(FileRegistry registry, ColorId color) { - (string, int) GetMulti() - { - var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); - return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length); - } - var (text, groupCount) = color switch { - ColorId.ConflictingMod => (string.Empty, 0), - ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1), + ColorId.ConflictingMod => (null, 0), + ColorId.NewMod => ([registry.SubModUsage[0].Item1.GetName()], 1), ColorId.InheritedMod => GetMulti(), - _ => (string.Empty, 0), + _ => (null, 0), }; - if (text.Length > 0 && ImGui.IsItemHovered()) - ImGui.SetTooltip(text); + if (text != null && ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + using var c = ImRaii.DefaultColors(); + ImUtf8.Text(string.Join('\n', text)); + } return (groupCount, registry.SubModUsage.Count) switch @@ -169,6 +169,12 @@ public partial class ModEditWindow (1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)", _ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)", }; + + (IEnumerable, int) GetMulti() + { + var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); + return (groups.Select(g => g.Key.GetName()), groups.Length); + } } private void DrawSelectable(FileRegistry registry) From 6d408ba695666e67be5d77d5387b676be839a744 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:29:47 +0200 Subject: [PATCH 1963/2451] Clip meta changes. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 3 +++ .../UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 5 ++++- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 15 +++++++++------ Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 3 +++ 8 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index aea2ef78..f9baddbe 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -66,6 +66,9 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Eqdp.Count; + private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 733517f3..51b14459 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -64,6 +64,9 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Eqp.Count; + private static bool DrawIdentifierInput(ref EqpIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index a33f8b7b..09075319 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -64,6 +64,9 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Est.Count; + private static bool DrawIdentifierInput(ref EstIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 5d67ddcf..1aa9060e 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -52,6 +52,9 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me .ThenBy(identifier => identifier.Condition.Id) .Select(identifier => (identifier, (byte)0)); + protected override int Count + => Editor.GlobalEqp.Count; + private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index bd42b60a..9532d8e7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -59,7 +59,10 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() => Editor.Gmp .OrderBy(kvp => kvp.Key.SetId.Id) - .Select(kvp => (kvp.Key, kvp.Value)); + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Gmp.Count; private static bool DrawIdentifierInput(ref GmpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 4e949b98..53c61292 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -149,6 +149,9 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Variant.Id) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Imc.Count; + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 229526c4..75de20a7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -41,12 +41,14 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta using var id = ImUtf8.PushId((int)Identifier.Type); DrawNew(); - foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) - { - id.Push(idx); - DrawEntry(identifier, entry); - id.Pop(); - } + + var height = ImUtf8.FrameHeightSpacing; + var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); + var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); + ImGuiClip.DrawEndDummy(remainder, height); + + void DrawLine((TIdentifier Identifier, TEntry Value) pair) + => DrawEntry(pair.Identifier, pair.Value); } public abstract ReadOnlySpan Label { get; } @@ -57,6 +59,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + protected abstract int Count { get; } /// diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 6d819b16..87e8c5b8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -63,6 +63,9 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Attribute) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Rsp.Count; + private static bool DrawIdentifierInput(ref RspIdentifier identifier) { ImGui.TableNextColumn(); From 4117d45d152e9c6c3f29656c0f103f1884a5e38a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:32:22 +0200 Subject: [PATCH 1964/2451] Use ReadWriteDictionary as base for meta changes. --- OtterGui | 2 +- Penumbra/Collections/Cache/GlobalEqpCache.cs | 3 +- Penumbra/Collections/Cache/MetaCache.cs | 6 ++- .../Cache/{IMetaCache.cs => MetaCacheBase.cs} | 37 ++++++------------- 4 files changed, 20 insertions(+), 28 deletions(-) rename Penumbra/Collections/Cache/{IMetaCache.cs => MetaCacheBase.cs} (52%) diff --git a/OtterGui b/OtterGui index 9217ac56..bfbde4f8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9217ac56697bc8285ced483b1fd4734fd36ba64d +Subproject commit bfbde4f8aa6acc8eb3ed8bc419d5ae2afc77b5f1 diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index 1c80b47d..efcab109 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -5,7 +6,7 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public class GlobalEqpCache : Dictionary, IService +public class GlobalEqpCache : ReadWriteDictionary, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _doNotHideNecklace = []; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 1a6924a9..05a94ac5 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -3,7 +3,6 @@ using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; -using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; @@ -16,6 +15,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); + public bool IsDisposed { get; private set; } public int Count => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; @@ -42,6 +42,10 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public void Dispose() { + if (IsDisposed) + return; + + IsDisposed = true; Eqp.Dispose(); Eqdp.Dispose(); Est.Dispose(); diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/MetaCacheBase.cs similarity index 52% rename from Penumbra/Collections/Cache/IMetaCache.cs rename to Penumbra/Collections/Cache/MetaCacheBase.cs index fecc6f50..98a87e3f 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCacheBase.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -5,27 +6,19 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection) - : Dictionary + : ReadWriteDictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - protected readonly MetaFileManager Manager = manager; - protected readonly ModCollection Collection = collection; - - public void Dispose() - { - Dispose(true); - } + protected readonly MetaFileManager Manager = manager; + protected readonly ModCollection Collection = collection; public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { - lock (this) - { - if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) - return false; + if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) + return false; - this[identifier] = (source, entry); - } + this[identifier] = (source, entry); ApplyModInternal(identifier, entry); return true; @@ -33,17 +26,14 @@ public abstract class MetaCacheBase(MetaFileManager manager public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod) { - lock (this) + if (!Remove(identifier, out var pair)) { - if (!Remove(identifier, out var pair)) - { - mod = null; - return false; - } - - mod = pair.Source; + mod = null; + return false; } + mod = pair.Source; + RevertModInternal(identifier); return true; } @@ -54,7 +44,4 @@ public abstract class MetaCacheBase(MetaFileManager manager protected virtual void RevertModInternal(TIdentifier identifier) { } - - protected virtual void Dispose(bool _) - { } } From f04331188252806b983adac72aa99134d36bc5c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 23:10:22 +0200 Subject: [PATCH 1965/2451] Fix vulnerability warning. --- Penumbra/Penumbra.csproj | 2 ++ Penumbra/packages.lock.json | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f42d16ad..9b613729 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -86,6 +86,8 @@ + + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index fd3a0a9e..5b868212 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -51,6 +51,12 @@ "resolved": "3.1.5", "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, + "System.Formats.Asn1": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.2.0", @@ -82,11 +88,6 @@ "SharpGLTF.Core": "1.0.1" } }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==" - }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "8.0.0", From f8e3b6777fd347ff5c19899fcf1d0b695486c901 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 23:10:59 +0200 Subject: [PATCH 1966/2451] Add DeleteDefaultValues on general dicts. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 67 ++++++++++++++++++++------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index d9018ff6..6b5ec378 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,5 @@ using System.Collections.Frozen; using OtterGui.Services; -using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -65,65 +64,101 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage Changes = false; } - public void DeleteDefaultValues() + public bool DeleteDefaultValues(MetaDictionary dict) { - var clone = Clone(); - Clear(); + var clone = dict.Clone(); + dict.Clear(); + var ret = false; foreach (var (key, value) in clone.Imc) { var defaultEntry = imcChecker.GetDefaultEntry(key, false); if (!defaultEntry.Entry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Eqp) { var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Eqdp) { var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Est) { var defaultEntry = EstFile.GetDefault(metaFileManager, key); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Gmp) { var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Rsp) { var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } + + return ret; } + public void DeleteDefaultValues() + => Changes = DeleteDefaultValues(this); + public void Apply(IModDataContainer container) { if (!Changes) From f5e61324627be7e192ce124d2a59154673adb857 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 17:47:51 +0200 Subject: [PATCH 1967/2451] Delete default meta entries from archives and api added mods if not configured otherwise. --- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 33 +++++---- Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Manager/ModImportManager.cs | 2 +- Penumbra/Mods/Manager/ModManager.cs | 10 +-- Penumbra/Mods/Manager/ModMigration.cs | 6 +- .../Manager/OptionEditor/ModGroupEditor.cs | 30 ++++---- Penumbra/Mods/ModCreator.cs | 71 +++++++++++-------- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- 14 files changed, 95 insertions(+), 73 deletions(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 2acdf031..31f20c5e 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -82,7 +82,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable != Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName))) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); - _modManager.AddMod(dir); + _modManager.AddMod(dir, true); if (_config.MigrateImportedModelsToV6) { _migrationManager.MigrateMdlDirectory(dir.FullName, false); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index bcecf264..56a19766 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -225,7 +225,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod)) { mod = new Mod(modDirectory); - modManager.Creator.ReloadMod(mod, true, out _); + modManager.Creator.ReloadMod(mod, true, true, out _); } Clear(); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 55e0e94e..3b765215 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -151,7 +151,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu if (deletions <= 0) return; - modManager.Creator.ReloadMod(mod, false, out _); + modManager.Creator.ReloadMod(mod, false, false, out _); files.UpdateAll(mod, option); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index d75ac671..e3eb5f54 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -256,7 +256,7 @@ public class ModMerger : IDisposable, IService if (dir == null) throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); - _mods.AddMod(dir); + _mods.AddMod(dir, false); result = _mods[^1]; if (mods.Count == 1) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 6b5ec378..81a33db6 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -3,12 +3,15 @@ using OtterGui.Services; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManager, ImcChecker imcChecker) : MetaDictionary, IService +public class ModMetaEditor( + ModGroupEditor groupEditor, + MetaFileManager metaFileManager, + ImcChecker imcChecker) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -64,11 +67,11 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage Changes = false; } - public bool DeleteDefaultValues(MetaDictionary dict) + public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict) { var clone = dict.Clone(); dict.Clear(); - var ret = false; + var count = 0; foreach (var (key, value) in clone.Imc) { var defaultEntry = imcChecker.GetDefaultEntry(key, false); @@ -79,7 +82,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -93,7 +96,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -107,7 +110,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -121,7 +124,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -135,7 +138,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -149,22 +152,26 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } - return ret; + if (count <= 0) + return false; + + Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); + return true; } public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(this); + => Changes = DeleteDefaultValues(metaFileManager, imcChecker, this); public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, this); + groupEditor.SetManipulations(container, this); Changes = false; } } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 43cfc1ee..3e367a3b 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -46,7 +46,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ if (!config.AutoReduplicateUiOnImport) return; - if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod) + if (modManager.Creator.LoadMod(modDirectory, false, false) is not { } mod) return; Dictionary> paths = []; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index d984d374..22cc0c86 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -79,7 +79,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd return false; } - modManager.AddMod(directory); + modManager.AddMod(directory, true); mod = modManager.LastOrDefault(); return mod != null && mod.ModPath == directory; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 59f8906e..bf1b6637 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -81,13 +81,13 @@ public sealed class ModManager : ModStorage, IDisposable, IService } /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) + public void AddMod(DirectoryInfo modFolder, bool deleteDefaultMeta) { if (this.Any(m => m.ModPath.Name == modFolder.Name)) return; Creator.SplitMultiGroups(modFolder); - var mod = Creator.LoadMod(modFolder, true); + var mod = Creator.LoadMod(modFolder, true, deleteDefaultMeta); if (mod == null) return; @@ -141,7 +141,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService var oldName = mod.Name; _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!Creator.ReloadMod(mod, true, out var metaChange)) + if (!Creator.ReloadMod(mod, true, false, out var metaChange)) { Penumbra.Log.Warning(mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." @@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService dir.Refresh(); mod.ModPath = dir; - if (!Creator.ReloadMod(mod, false, out var metaChange)) + if (!Creator.ReloadMod(mod, false, false, out var metaChange)) { Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); return; @@ -332,7 +332,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService var queue = new ConcurrentQueue(); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => { - var mod = Creator.LoadMod(dir, false); + var mod = Creator.LoadMod(dir, false, false); if (mod != null) queue.Enqueue(mod); }); diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index c7eb7cc5..3e58c515 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -82,7 +82,7 @@ public static partial class ModMigration foreach (var (gamePath, swapPath) in swaps) mod.Default.FileSwaps.Add(gamePath, swapPath); - creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); + creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true); foreach (var group in mod.Groups) saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); @@ -182,7 +182,7 @@ public static partial class ModMigration Description = option.OptionDesc, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); return subMod; } @@ -196,7 +196,7 @@ public static partial class ModMigration Priority = priority, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); return subMod; } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 712630c6..7f18852d 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -39,7 +39,7 @@ public class ModGroupEditor( ImcModGroupEditor imcEditor, CommunicatorService communicator, SaveService saveService, - Configuration Config) : IService + Configuration config) : IService { public SingleModGroupEditor SingleEditor => singleEditor; @@ -57,7 +57,7 @@ public class ModGroupEditor( return; group.DefaultSettings = defaultOption; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); } @@ -68,9 +68,9 @@ public class ModGroupEditor( if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) return; - saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateDelete(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); group.Name = newName; - saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); } @@ -81,7 +81,7 @@ public class ModGroupEditor( var idx = group.GetIndex(); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); mod.Groups.RemoveAt(idx); - saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); } @@ -93,7 +93,7 @@ public class ModGroupEditor( if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; - saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); } @@ -104,7 +104,7 @@ public class ModGroupEditor( return; group.Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); } @@ -115,7 +115,7 @@ public class ModGroupEditor( return; group.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); } @@ -126,7 +126,7 @@ public class ModGroupEditor( return; option.Name = newName; - saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } @@ -137,7 +137,7 @@ public class ModGroupEditor( return; option.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } @@ -149,7 +149,7 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Manipulations.SetTo(manipulations); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } @@ -161,13 +161,13 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Files.SetTo(replacements); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Forces a file save of the given container's group. public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue) - => saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + => saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) @@ -176,7 +176,7 @@ public class ModGroupEditor( subMod.Files.AddFrom(additions); if (oldCount != subMod.Files.Count) { - saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } } @@ -189,7 +189,7 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.FileSwaps.SetTo(swaps); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0f4972e3..8cfdc9a7 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; @@ -20,11 +21,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; public partial class ModCreator( - SaveService _saveService, + SaveService saveService, Configuration config, - ModDataEditor _dataEditor, - MetaFileManager _metaFileManager, - GamePathParser _gamePathParser) : IService + ModDataEditor dataEditor, + MetaFileManager metaFileManager, + GamePathParser gamePathParser, + ImcChecker imcChecker) : IService { public readonly Configuration Config = config; @@ -34,7 +36,7 @@ public partial class ModCreator( try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - _dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; } @@ -46,7 +48,7 @@ public partial class ModCreator( } /// Load a mod by its directory. - public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges) + public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges, bool deleteDefaultMetaChanges) { modPath.Refresh(); if (!modPath.Exists) @@ -56,7 +58,7 @@ public partial class ModCreator( } var mod = new Mod(modPath); - if (ReloadMod(mod, incorporateMetaChanges, out _)) + if (ReloadMod(mod, incorporateMetaChanges, deleteDefaultMetaChanges, out _)) return mod; // Can not be base path not existing because that is checked before. @@ -65,21 +67,29 @@ public partial class ModCreator( } /// Reload a mod from its mod path. - public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange) + public bool ReloadMod(Mod mod, bool incorporateMetaChanges, bool deleteDefaultMetaChanges, out ModDataChangeType modDataChange) { modDataChange = ModDataChangeType.Deletion; if (!Directory.Exists(mod.ModPath.FullName)) return false; - modDataChange = _dataEditor.LoadMeta(this, mod); + modDataChange = dataEditor.LoadMeta(this, mod); if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - _dataEditor.LoadLocalData(mod); + dataEditor.LoadLocalData(mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true); + if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) + { + foreach (var container in mod.AllDataContainers) + { + if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations)) + saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); + } + } return true; } @@ -89,13 +99,13 @@ public partial class ModCreator( { mod.Groups.Clear(); var changes = false; - foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) + foreach (var file in saveService.FileNames.GetOptionGroupFiles(mod)) { var group = LoadModGroup(mod, file); if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes - || _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true) + || saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true) != Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true)); mod.Groups.Add(group); } @@ -106,13 +116,13 @@ public partial class ModCreator( } if (changes) - _saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); } /// Load the default option for a given mod. public void LoadDefaultOption(Mod mod) { - var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); + var defaultFile = saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); @@ -157,7 +167,7 @@ public partial class ModCreator( List deleteList = new(); foreach (var subMod in mod.AllDataContainers) { - var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); + var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true); changes |= localChanges; if (delete) deleteList.AddRange(localDeleteList); @@ -168,8 +178,8 @@ public partial class ModCreator( if (!changes) return; - _saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } @@ -177,7 +187,7 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -194,7 +204,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName), + var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName), Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); @@ -207,7 +217,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), + var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); @@ -223,7 +233,11 @@ public partial class ModCreator( } DeleteDeleteList(deleteList, delete); - return (oldSize < option.Manipulations.Count, deleteList); + var changes = oldSize < option.Manipulations.Count; + if (deleteDefault && !Config.KeepDefaultMetaChanges) + changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations); + + return (changes, deleteList); } /// @@ -250,7 +264,7 @@ public partial class ModCreator( group.Priority = priority; group.DefaultSettings = defaultSettings; group.OptionData.AddRange(subMods.Select(s => s.Clone(group))); - _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -260,7 +274,7 @@ public partial class ModCreator( group.Priority = priority; group.DefaultSettings = defaultSettings; group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group))); - _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -277,7 +291,8 @@ public partial class ModCreator( foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); - IncorporateMetaChanges(mod, baseFolder, true); + IncorporateMetaChanges(mod, baseFolder, true, true); + return mod; } @@ -288,15 +303,15 @@ public partial class ModCreator( internal void CreateDefaultFiles(DirectoryInfo directory) { var mod = new Mod(directory); - ReloadMod(mod, false, out _); + ReloadMod(mod, false, false, out _); foreach (var file in mod.FindUnusedFiles()) { if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath)) mod.Default.Files.TryAdd(gamePath, file); } - IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); + IncorporateMetaChanges(mod.Default, directory, true, true); + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -333,7 +348,7 @@ public partial class ModCreator( { var mod = new Mod(baseDir); - var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList(); + var files = saveService.FileNames.GetOptionGroupFiles(mod).ToList(); var idx = 0; var reorder = false; foreach (var groupFile in files) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e4049482..b5499624 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -97,7 +97,7 @@ public class TemporaryMod : IMod defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); - modManager.AddMod(dir); + modManager.AddMod(dir, false); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index b75c5aef..6ed1b55d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -281,7 +281,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (newDir == null) return; - _modManager.AddMod(newDir); + _modManager.AddMod(newDir, false); var mod = _modManager[^1]; if (!_swapData.WriteMod(_modManager, mod, mod.Default, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2f76340b..8bdd95ab 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -180,7 +180,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Thu, 29 Aug 2024 18:38:09 +0200 Subject: [PATCH 1968/2451] Stop raising errors when compressing the deleted files after updating Heliosphere mods. --- OtterGui | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index bfbde4f8..17bd4b75 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bfbde4f8aa6acc8eb3ed8bc419d5ae2afc77b5f1 +Subproject commit 17bd4b75b6d7750c92b65caf09715886d4df57cf diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 31f20c5e..64e201be 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -91,7 +91,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (_config.UseFileSystemCompression) new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), - CompressionAlgorithm.Xpress8K); + CompressionAlgorithm.Xpress8K, false); return ApiHelpers.Return(PenumbraApiEc.Success, args); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 27c7f2ed..9d8ea21c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -816,13 +816,13 @@ public class SettingsTab : ITab, IUiService if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero, "Try to compress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero, "Try to decompress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true); if (_compactor.MassCompactRunning) { From 5c5e45114f25f9429d8757b6edf852ecc37173c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:38:37 +0200 Subject: [PATCH 1969/2451] Make loading mods for advanced editing async. --- Penumbra/Mods/Editor/ModEditor.cs | 65 +++++++++---- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 102 +++++++++++++++----- 2 files changed, 124 insertions(+), 43 deletions(-) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index cacb7f88..19ca7022 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -25,6 +25,21 @@ public class ModEditor( public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly FileCompactor Compactor = compactor; + + public bool IsLoading + { + get + { + lock (_lock) + { + return _loadingMod is { IsCompleted: false }; + } + } + } + + private readonly object _lock = new(); + private Task? _loadingMod; + public Mod? Mod { get; private set; } public int GroupIdx { get; private set; } public int DataIdx { get; private set; } @@ -32,28 +47,42 @@ public class ModEditor( public IModGroup? Group { get; private set; } public IModDataContainer? Option { get; private set; } - public void LoadMod(Mod mod) - => LoadMod(mod, -1, 0); - - public void LoadMod(Mod mod, int groupIdx, int dataIdx) + public async Task LoadMod(Mod mod, int groupIdx, int dataIdx) { - Mod = mod; - LoadOption(groupIdx, dataIdx, true); - Files.UpdateAll(mod, Option!); - SwapEditor.Revert(Option!); - MetaEditor.Load(Mod!, Option!); - Duplicates.Clear(); - MdlMaterialEditor.ScanModels(Mod!); + await AppendTask(() => + { + Mod = mod; + LoadOption(groupIdx, dataIdx, true); + Files.UpdateAll(mod, Option!); + SwapEditor.Revert(Option!); + MetaEditor.Load(Mod!, Option!); + Duplicates.Clear(); + MdlMaterialEditor.ScanModels(Mod!); + }); } - public void LoadOption(int groupIdx, int dataIdx) + private Task AppendTask(Action run) { - LoadOption(groupIdx, dataIdx, true); - SwapEditor.Revert(Option!); - Files.UpdatePaths(Mod!, Option!); - MetaEditor.Load(Mod!, Option!); - FileEditor.Clear(); - Duplicates.Clear(); + lock (_lock) + { + if (_loadingMod == null || _loadingMod.IsCompleted) + return _loadingMod = Task.Run(run); + + return _loadingMod = _loadingMod.ContinueWith(_ => run()); + } + } + + public async Task LoadOption(int groupIdx, int dataIdx) + { + await AppendTask(() => + { + LoadOption(groupIdx, dataIdx, true); + SwapEditor.Revert(Option!); + Files.UpdatePaths(Mod!, Option!); + MetaEditor.Load(Mod!, Option!); + FileEditor.Clear(); + Duplicates.Clear(); + }); } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7bb067d8..f2fe8b9e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -8,6 +8,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -51,34 +52,68 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; - public Mod? Mod { get; private set; } + public Mod? Mod { get; private set; } + + + public bool IsLoading + { + get + { + lock (_lock) + { + return _editor.IsLoading || _loadingMod is { IsCompleted: false }; + } + } + } + + private readonly object _lock = new(); + private Task? _loadingMod; + + + private void AppendTask(Action run) + { + lock (_lock) + { + if (_loadingMod == null || _loadingMod.IsCompleted) + _loadingMod = Task.Run(run); + else + _loadingMod = _loadingMod.ContinueWith(_ => run()); + } + } public void ChangeMod(Mod mod) { if (mod == Mod) return; - _editor.LoadMod(mod, -1, 0); - Mod = mod; - - SizeConstraints = new WindowSizeConstraints + WindowName = $"{mod.Name} (LOADING){WindowBaseLabel}"; + AppendTask(() => { - MinimumSize = new Vector2(1240, 600), - MaximumSize = 4000 * Vector2.One, - }; - _selectedFiles.Clear(); - _modelTab.Reset(); - _materialTab.Reset(); - _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); - UpdateModels(); - _forceTextureStartPath = true; + _editor.LoadMod(mod, -1, 0).Wait(); + Mod = mod; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(1240, 600), + MaximumSize = 4000 * Vector2.One, + }; + _selectedFiles.Clear(); + _modelTab.Reset(); + _materialTab.Reset(); + _shaderPackageTab.Reset(); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + UpdateModels(); + _forceTextureStartPath = true; + }); } public void ChangeOption(IModDataContainer? subMod) { - var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); - _editor.LoadOption(groupIdx, dataIdx); + AppendTask(() => + { + var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, dataIdx).Wait(); + }); } public void UpdateModels() @@ -92,6 +127,9 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public override void PreDraw() { + if (IsLoading) + return; + using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow); var sb = new StringBuilder(256); @@ -144,13 +182,16 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public override void OnClose() { - _left.Dispose(); - _right.Dispose(); - _materialTab.Reset(); - _modelTab.Reset(); - _shaderPackageTab.Reset(); _config.Ephemeral.AdvancedEditingOpen = false; _config.Ephemeral.Save(); + AppendTask(() => + { + _left.Dispose(); + _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); + }); } public override void Draw() @@ -163,6 +204,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _config.Ephemeral.Save(); } + if (IsLoading) + { + var radius = 100 * ImUtf8.GlobalScale; + var thickness = (int) (20 * ImUtf8.GlobalScale); + var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius; + var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius; + ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY)); + ImUtf8.Spinner("##spinner"u8, radius, thickness, ImGui.GetColorU32(ImGuiCol.Text)); + return; + } + using var tabBar = ImRaii.TabBar("##tabs"); if (!tabBar) return; @@ -405,14 +457,14 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", _editor.Option is DefaultSubMod)) { - _editor.LoadOption(-1, 0); + _editor.LoadOption(-1, 0).Wait(); ret = true; } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) { - _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait(); ret = true; } @@ -430,7 +482,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) { var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.LoadOption(groupIdx, dataIdx); + _editor.LoadOption(groupIdx, dataIdx).Wait(); ret = true; } } From de3644e9e131baf5f4f953bc0d034a116c7da4d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:46:37 +0200 Subject: [PATCH 1970/2451] Make BC4 textures importable. --- Penumbra/Import/Textures/TexFileParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index ae4a39c0..1bf282e5 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -177,6 +177,7 @@ public static class TexFileParser DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, + DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, @@ -202,6 +203,7 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, From 2a7d2ef0d5cef009c60a701235a1786e56d191b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:58:30 +0200 Subject: [PATCH 1971/2451] Allow reading BC6. --- Penumbra/Import/Textures/TexFileParser.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 1bf282e5..0d817fa1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -177,8 +177,9 @@ public static class TexFileParser DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, - DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, + DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, + DXGIFormat.BC6HUF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, @@ -203,8 +204,9 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, - (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, + (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, From 176001195ba16d69c4540d2dbed9607932337ee6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 21:13:33 +0200 Subject: [PATCH 1972/2451] Improve mod filters. --- OtterGui | 2 +- Penumbra/Import/Textures/TexFileParser.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 23 +++++---- Penumbra/UI/ModsTab/ModFilter.cs | 49 ++++++++++---------- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/OtterGui b/OtterGui index 17bd4b75..3e6b0857 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 17bd4b75b6d7750c92b65caf09715886d4df57cf +Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97 diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 0d817fa1..979e4d3c 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -204,7 +204,7 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, - (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8bdd95ab..7a165feb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -9,6 +9,8 @@ using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -84,8 +86,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector filter switch - { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.Favorite => "Favorite", - ModFilter.NotFavorite => "No Favorite", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", - ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - ModFilter.Inherited => "Inherited Configuration", - ModFilter.Uninherited => "Own Configuration", - ModFilter.Undefined => "Not Configured", - _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), - }; + public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs = + [ + (ModFilter.Enabled, ModFilter.Disabled, "Enabled"), + (ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"), + (ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"), + (ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"), + (ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"), + (ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"), + (ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"), + ]; + + public static IReadOnlyList> Groups = + [ + [ + (ModFilter.NoConflict, "Has No Conflicts"), + (ModFilter.SolvedConflict, "Has Solved Conflicts"), + (ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"), + ], + [ + (ModFilter.Undefined, "Not Configured"), + (ModFilter.Inherited, "Inherited Configuration"), + (ModFilter.Uninherited, "Own Configuration"), + ], + ]; } From ff3e5410aac9e23606317e179f6278e710cb11ee Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 29 Aug 2024 19:18:17 +0000 Subject: [PATCH 1973/2451] [CI] Updating repo.json for testing_1.2.1.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5a274d73..6f9b8c69 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.1", + "TestingAssemblyVersion": "1.2.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From fb144d0b74ce1b263eb3e69625c37518e3725a1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:19 +0200 Subject: [PATCH 1974/2451] Cleanup. --- Penumbra/Import/Textures/TexFileParser.cs | 4 ++-- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 979e4d3c..220095c1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -179,7 +179,7 @@ public static class TexFileParser DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, - DXGIFormat.BC6HUF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina + DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, @@ -206,7 +206,7 @@ public static class TexFileParser TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, - (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina + (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 81a33db6..07a54391 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -156,7 +156,7 @@ public class ModMetaEditor( } } - if (count <= 0) + if (count == 0) return false; Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); From 04582ba00b8fedfb32a8ad7fbed0230ea89126f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:31 +0200 Subject: [PATCH 1975/2451] Add CustomArmor to UI events. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index a38e9bcf..97e9f427 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a38e9bcfb80c456102945bbb4c59f5621cae0442 +Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623 diff --git a/Penumbra.GameData b/Penumbra.GameData index c43c5cac..bb281fb0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c43c5cac4cee092bf0aed8d46bab112b037ef8f2 +Subproject commit bb281fb01d88d6fd815a286f87049978ef05de59 From 75858a61b5092b1567e332417610ef36a4f7e122 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:44 +0200 Subject: [PATCH 1976/2451] Fix MetaManipulations not resetting count when clearing. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 1093c6c5..70d4fd47 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -69,6 +69,7 @@ public class MetaDictionary public void Clear() { + Count = 0; _imc.Clear(); _eqp.Clear(); _eqdp.Clear(); From 6b858dc5ac9d9cd967601a3fdac91048c87bf7c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:23:34 +0200 Subject: [PATCH 1977/2451] Hmpf. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bb281fb0..66bc00dc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bb281fb01d88d6fd815a286f87049978ef05de59 +Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716 From 1b17404876d9248c77649b7831eda57332f84f96 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 20:52:01 +0200 Subject: [PATCH 1978/2451] Fix small issue with changed item tooltips. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6f0b63ce..b6b19ef2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -114,7 +114,7 @@ public class Penumbra : IDalamudPlugin var itemSheet = _services.GetService().GetExcelSheet()!; _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is IdentifiedItem) + if (it is IdentifiedItem { Item.Id.IsItem: true }) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); From 22cbecc6a459a3700b0d5f663847f098c69963aa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Sep 2024 22:48:15 +0200 Subject: [PATCH 1979/2451] Add Page to mod group data for TT interop. --- Penumbra/Mods/Groups/IModGroup.cs | 23 +++++++++++++++-------- Penumbra/Mods/Groups/ImcModGroup.cs | 1 + Penumbra/Mods/Groups/MultiModGroup.cs | 1 + Penumbra/Mods/Groups/SingleModGroup.cs | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index c5654019..a6f6e20d 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -24,14 +24,21 @@ public interface IModGroup { public const int MaxMultiOptions = 32; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public string Image { get; set; } - public GroupType Type { get; } - public GroupDrawBehaviour Behaviour { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public string Image { get; set; } + + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public int Page { get; set; } + + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index d42804ba..2b020184 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -29,6 +29,7 @@ public class ImcModGroup(Mod mod) : IModGroup => GroupDrawBehaviour.MultiSelection; public ModPriority Priority { get; set; } = ModPriority.Default; + public int Page { get; set; } public Setting DefaultSettings { get; set; } = Setting.Zero; public ImcIdentifier Identifier; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 9cf7e6a3..24dcc849 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -28,6 +28,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 723cd5b1..fddb96d6 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -26,6 +26,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; From bd59591ed8650c5ae544fa3546fbc5e4fa5e813b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Sep 2024 23:42:19 +0200 Subject: [PATCH 1980/2451] Add display of ImportDate and allow resetting it, add button to open local data json. --- Penumbra/Mods/Manager/ModDataEditor.cs | 11 ++++++ Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 19 +++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 37 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7a0467d0..933620d9 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -249,6 +249,17 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } + public void ResetModImportDate(Mod mod) + { + var newDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (mod.ImportDate == newDate) + return; + + mod.ImportDate = newDate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.ImportDate, mod, null); + } + public void ChangeModNote(Mod mod, string newNote) { if (mod.Note == newNote) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 8cfdc9a7..fe027ca4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -77,7 +77,7 @@ public partial class ModCreator( if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - dataEditor.LoadLocalData(mod); + modDataChange |= dataEditor.LoadLocalData(mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 7a165feb..0781312c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -447,16 +447,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Mon, 9 Sep 2024 14:10:54 +0200 Subject: [PATCH 1981/2451] Allow copying paths out of the resource logger. --- .../ResourceWatcher/ResourceWatcherTable.cs | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 33e301ae..2bb71b87 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; +using OtterGui.Text; using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; @@ -52,36 +53,41 @@ internal sealed class ResourceWatcherTable : Table private static unsafe void DrawByteString(CiByteString path, float length) { - Vector2 vec; - ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); - if (vec.X <= length) + if (path.IsEmpty) + return; + + var size = ImUtf8.CalcTextSize(path.Span); + var clicked = false; + if (size.X <= length) { - ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + clicked = ImUtf8.Selectable(path.Span); } else { - var fileName = path.LastIndexOf((byte)'/'); - CiByteString shortPath; - if (fileName != -1) + var fileName = path.LastIndexOf((byte)'/'); + using (ImRaii.Group()) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); - using var font = ImRaii.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString()); - ImGui.SameLine(); - shortPath = path.Substring(fileName, path.Length - fileName); - } - else - { - shortPath = path; + CiByteString shortPath; + if (fileName != -1) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString()); + ImUtf8.SameLineInner(); + shortPath = path.Substring(fileName, path.Length - fileName); + } + else + { + shortPath = path; + } + + clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap); } - ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length); - if (ImGui.IsItemClicked()) - ImGuiNative.igSetClipboardText(path.Path); - - if (ImGui.IsItemHovered()) - ImGuiNative.igSetTooltip(path.Path); + ImUtf8.HoverTooltip(path.Span); } + + if (clicked) + ImUtf8.SetClipboardText(path.Span); } private sealed class RecordTypeColumn : ColumnFlags From 10ce5da8c9bb5d2b226a940a23f1ef57026aec81 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 9 Sep 2024 13:42:42 +0000 Subject: [PATCH 1982/2451] [CI] Updating repo.json for testing_1.2.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6f9b8c69..cdd622c9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.2", + "TestingAssemblyVersion": "1.2.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 26371d42f75768a0bda28da0f2fc8976075fdb82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Sep 2024 16:51:23 +0200 Subject: [PATCH 1983/2451] Be less dumb. --- Penumbra/Mods/Groups/ImcModGroup.cs | 7 +------ Penumbra/Mods/Groups/ModSaveGroup.cs | 17 +++++++++++++++++ Penumbra/Mods/Groups/MultiModGroup.cs | 13 ++++--------- Penumbra/Mods/Groups/SingleModGroup.cs | 13 ++++--------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 2b020184..f8b4b2ef 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -170,14 +170,10 @@ public class ImcModGroup(Mod mod) : IModGroup var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); var ret = new ImcModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, }; - if (ret.Name.Length == 0) + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) @@ -216,7 +212,6 @@ public class ImcModGroup(Mod mod) : IModGroup } ret.Identifier = identifier.Value; - ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index c465822b..bda70b54 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,4 +1,7 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -90,6 +93,8 @@ public readonly struct ModSaveGroup : ISavable jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Image)); jWriter.WriteValue(group.Image); + jWriter.WritePropertyName(nameof(group.Page)); + jWriter.WriteValue(group.Page); jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WriteValue(group.Priority.Value); jWriter.WritePropertyName(nameof(group.Type)); @@ -97,4 +102,16 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } + + public static bool ReadJsonBase(JObject json, IModGroup group) + { + group.Name = json[nameof(IModGroup.Name)]?.ToObject() ?? string.Empty; + group.Description = json[nameof(IModGroup.Description)]?.ToObject() ?? string.Empty; + group.Image = json[nameof(IModGroup.Image)]?.ToObject() ?? string.Empty; + group.Page = json[nameof(IModGroup.Page)]?.ToObject() ?? 0; + group.Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + group.DefaultSettings = json[nameof(IModGroup.DefaultSettings)]?.ToObject() ?? Setting.Zero; + + return group.Name.Length > 0; + } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 24dcc849..0c9aa805 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -67,15 +67,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public static MultiModGroup? Load(Mod mod, JObject json) { - var ret = new MultiModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new MultiModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; var options = json["Options"]; @@ -106,6 +99,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index fddb96d6..ab0c2d4f 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -63,15 +63,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public static SingleModGroup? Load(Mod mod, JObject json) { var options = json["Options"]; - var ret = new SingleModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new SingleModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (options != null) @@ -92,6 +85,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); From ac1ea124d93e9a17e6fe0fe71b920d31d4c5fe99 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 9 Sep 2024 14:53:17 +0000 Subject: [PATCH 1984/2451] [CI] Updating repo.json for testing_1.2.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cdd622c9..6625cb24 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.3", + "TestingAssemblyVersion": "1.2.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 00fbb2686b864dcfe91bb9e67415b9059fc53a55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Sep 2024 22:54:06 +0200 Subject: [PATCH 1985/2451] Add option to apply only attributes from IMC group. --- Penumbra/Meta/ImcChecker.cs | 25 +++++++--------- Penumbra/Mods/Editor/ModMetaEditor.cs | 9 +++--- Penumbra/Mods/Groups/ImcModGroup.cs | 30 +++++++++++++------ .../Manager/OptionEditor/ImcModGroupEditor.cs | 10 +++++++ Penumbra/Mods/ModCreator.cs | 7 ++--- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 4 +-- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 6 ++-- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 12 ++++++-- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 4e3ff11b..a415c9b0 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -6,9 +6,9 @@ namespace Penumbra.Meta; public class ImcChecker { - private static readonly Dictionary VariantCounts = []; - private static MetaFileManager? _dataManager; - + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + private static readonly ConcurrentDictionary GlobalCachedDefaultEntries = []; public static int GetVariantCount(ImcIdentifier identifier) { @@ -26,23 +26,20 @@ public class ImcChecker public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); - private readonly Dictionary _cachedDefaultEntries = new(); - private readonly MetaFileManager _metaFileManager; - public ImcChecker(MetaFileManager metaFileManager) - { - _metaFileManager = metaFileManager; - _dataManager = metaFileManager; - } + => _dataManager = metaFileManager; - public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + public static CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { - if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + if (GlobalCachedDefaultEntries.TryGetValue(identifier, out var entry)) return entry; + if (_dataManager == null) + return new CachedEntry(default, false, false); + try { - var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_dataManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -51,7 +48,7 @@ public class ImcChecker } if (storeCache) - _cachedDefaultEntries.Add(identifier, entry); + GlobalCachedDefaultEntries.TryAdd(identifier, entry); return entry; } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 07a54391..64c585ea 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -10,8 +10,7 @@ namespace Penumbra.Mods.Editor; public class ModMetaEditor( ModGroupEditor groupEditor, - MetaFileManager metaFileManager, - ImcChecker imcChecker) : MetaDictionary, IService + MetaFileManager metaFileManager) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -67,14 +66,14 @@ public class ModMetaEditor( Changes = false; } - public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict) + public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { var clone = dict.Clone(); dict.Clear(); var count = 0; foreach (var (key, value) in clone.Imc) { - var defaultEntry = imcChecker.GetDefaultEntry(key, false); + var defaultEntry = ImcChecker.GetDefaultEntry(key, false); if (!defaultEntry.Entry.Equals(value)) { dict.TryAdd(key, value); @@ -164,7 +163,7 @@ public class ModMetaEditor( } public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(metaFileManager, imcChecker, this); + => Changes = DeleteDefaultValues(metaFileManager, this); public void Apply(IModDataContainer container) { diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index f8b4b2ef..2a1854ed 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -35,6 +35,7 @@ public class ImcModGroup(Mod mod) : IModGroup public ImcIdentifier Identifier; public ImcEntry DefaultEntry; public bool AllVariants; + public bool OnlyAttributes; public FullPath? FindBestMatch(Utf8GamePath gamePath) @@ -97,28 +98,36 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcEntry GetEntry(ushort mask) - => DefaultEntry with { AttributeMask = mask }; + private ImcEntry GetEntry(Variant variant, ushort mask) + { + if (!OnlyAttributes) + return DefaultEntry with { AttributeMask = mask }; + + var defaultEntry = ImcChecker.GetDefaultEntry(Identifier with { Variant = variant }, true); + if (defaultEntry.VariantExists) + return defaultEntry.Entry with { AttributeMask = mask }; + + return DefaultEntry with { AttributeMask = mask }; + } public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); - var entry = GetEntry(mask); + var mask = GetCurrentMask(setting); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); else for (var i = 0; i <= count; ++i) - manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, GetEntry((Variant)i, mask)); } else { - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); } } @@ -138,6 +147,8 @@ public class ImcModGroup(Mod mod) : IModGroup serializer.Serialize(jWriter, DefaultEntry); jWriter.WritePropertyName(nameof(AllVariants)); jWriter.WriteValue(AllVariants); + jWriter.WritePropertyName(nameof(OnlyAttributes)); + jWriter.WriteValue(OnlyAttributes); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -170,8 +181,9 @@ public class ImcModGroup(Mod mod) : IModGroup var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); var ret = new ImcModGroup(mod) { - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + OnlyAttributes = json[nameof(OnlyAttributes)]?.ToObject() ?? false, }; if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 515f6ff4..dc94c881 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -89,6 +89,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); } + public void ChangeOnlyAttributes(ImcModGroup group, bool onlyAttributes, SaveType saveType = SaveType.Queue) + { + if (group.OnlyAttributes == onlyAttributes) + return; + + group.OnlyAttributes = onlyAttributes; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index fe027ca4..1af9c1db 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -25,8 +25,7 @@ public partial class ModCreator( Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager, - GamePathParser gamePathParser, - ImcChecker imcChecker) : IService + GamePathParser gamePathParser) : IService { public readonly Configuration Config = config; @@ -86,7 +85,7 @@ public partial class ModCreator( { foreach (var container in mod.AllDataContainers) { - if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations)) + if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); } } @@ -235,7 +234,7 @@ public partial class ModCreator( DeleteDeleteList(deleteList, delete); var changes = oldSize < option.Manipulations.Count; if (deleteDefault && !Config.KeepDefaultMetaChanges) - changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations); + changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, option.Manipulations); return (changes, deleteList); } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 53c61292..c8310cf7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -30,7 +30,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } private void UpdateEntry() - => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + => (Entry, _fileExists, _) = ImcChecker.GetDefaultEntry(Identifier, true); protected override void DrawNew() { @@ -54,7 +54,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile DrawMetaButtons(identifier, entry); DrawIdentifier(identifier); - var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; if (DrawEntry(defaultEntry, ref entry, true)) Editor.Changes |= Editor.Update(identifier, entry); } diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 689571f3..c30239bc 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -24,13 +24,11 @@ public class AddGroupDrawer : IUiService private bool _imcFileExists; private bool _entryExists; private bool _entryInvalid; - private readonly ImcChecker _imcChecker; private readonly ModManager _modManager; - public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) + public AddGroupDrawer(ModManager modManager) { _modManager = modManager; - _imcChecker = imcChecker; UpdateEntry(); } @@ -142,7 +140,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + (_defaultEntry, _imcFileExists, _entryExists) = ImcChecker.GetDefaultEntry(_imcIdentifier, false); _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 4ab1c6aa..786bb8ff 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -6,6 +6,7 @@ using OtterGui.Text; using OtterGui.Text.Widget; using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; @@ -18,18 +19,25 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr public void Draw() { var identifier = group.Identifier; - var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; var entry = group.DefaultEntry; var changes = false; - var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - ImUtf8.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight; ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); var allVariants = group.AllVariants; if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); + ImGui.SameLine(); + var onlyAttributes = group.OnlyAttributes; + if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes)) + editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); + ImUtf8.HoverTooltip("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); + using (ImUtf8.Group()) { ImUtf8.TextFrameAligned("Material ID"u8); From 9b958a9d37856e060f66643ad306de3ea63b0bf2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Sep 2024 23:16:43 +0200 Subject: [PATCH 1986/2451] Update actions. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b40b2538..1783c9a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c9e2909..4799cbed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 91361646..0718ded2 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Debug/* From af2a14826cdee2472b9ac872e1808d505aae14e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Sep 2024 22:50:00 +0200 Subject: [PATCH 1987/2451] Add potential hidden priorities. --- Penumbra/Mods/Settings/ModPriority.cs | 6 ++++++ Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 5 +++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Settings/ModPriority.cs b/Penumbra/Mods/Settings/ModPriority.cs index 993bd577..cf234c00 100644 --- a/Penumbra/Mods/Settings/ModPriority.cs +++ b/Penumbra/Mods/Settings/ModPriority.cs @@ -66,4 +66,10 @@ public readonly record struct ModPriority(int Value) : public int CompareTo(ModPriority other) => Value.CompareTo(other.Value); + + public const int HiddenMin = -84037; + public const int HiddenMax = HiddenMin + 1000; + + public bool IsHidden + => Value is > HiddenMin and < HiddenMax; } diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index bee48068..bc18ac51 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -25,7 +25,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy => "Conflicts"u8; public bool IsVisible - => collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0; + => collectionManager.Active.Current.Conflicts(selector.Selected!).Any(c => !GetPriority(c).IsHidden); private readonly ConditionalWeakTable _expandedMods = []; @@ -58,7 +58,8 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy // Can not be null because otherwise the tab bar is never drawn. var mod = selector.Selected!; - foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).Where(c => !c.Mod2.Priority.IsHidden) + .OrderByDescending(GetPriority) .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { using var id = ImRaii.PushId(index); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index d2fbd0cd..8d889c3b 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.UI.Classes; using Penumbra.Collections.Manager; @@ -96,6 +97,9 @@ public class ModPanelSettingsTab( ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; + if (new ModPriority(priority).IsHidden) + ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { From 22aca49112b7f9d6feca07c999507a08ff41935c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 22 Sep 2024 19:47:34 +0000 Subject: [PATCH 1988/2451] [CI] Updating repo.json for testing_1.2.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6625cb24..36b27682 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.4", + "TestingAssemblyVersion": "1.2.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From caf4382e1f17b8fccd1bfc2f03293fd149596971 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 11:50:49 +0200 Subject: [PATCH 1989/2451] Update BNPCs --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 66bc00dc..fd50cb3d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716 +Subproject commit fd50cb3d33e8f59e8b60474c3def914a6952c485 From 776b4e9efbd1835cc0332d7f1ea9b68c4267bf72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 11:58:00 +0200 Subject: [PATCH 1990/2451] Update obsolete properties from CS. --- Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs | 4 ++-- .../UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 61ccc95c..c459a67a 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -40,8 +40,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); - Width = (int)_originalColorTableTexture.Texture->Width; - Height = (int)_originalColorTableTexture.Texture->Height; + Width = (int)_originalColorTableTexture.Texture->ActualWidth; + Height = (int)_originalColorTableTexture.Texture->ActualHeight; ColorTable = new Half[Width * Height * 4]; _updatePending = true; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 6ffd1f88..5c636b1d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -56,7 +56,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService } var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; - var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->ActualWidth, firstNonNullTexture->ActualHeight).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; var ret = false; @@ -135,10 +135,10 @@ public sealed unsafe class MaterialTemplatePickers : IUiService continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; - var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + var size = new Vector2(texture->ActualWidth, texture->ActualHeight).Contain(itemSize); position += (itemSize - size) * 0.5f; ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, - new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + new Vector2(texture->ActualWidth / (float)texture->AllocatedWidth, texture->ActualHeight / (float)texture->AllocatedHeight)); } } From 389c42e68f2d65eaa7ec8eff6a1fd38c955faf54 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 13:02:28 +0200 Subject: [PATCH 1991/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fd50cb3d..dd86dafb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fd50cb3d33e8f59e8b60474c3def914a6952c485 +Subproject commit dd86dafb88ca4c7b662938bbc1310729ba7f788d From 8084f481446dd601761c0c96bf6baf1d1366fb63 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:29:39 +1000 Subject: [PATCH 1992/2451] Init support for DT model i/o --- Penumbra/Import/Models/Export/MeshExporter.cs | 62 +++- Penumbra/Import/Models/Import/MeshImporter.cs | 32 ++- .../Import/Models/Import/VertexAttribute.cs | 265 ++++++++++++++---- 3 files changed, 281 insertions(+), 78 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 3a57ab55..20158776 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -311,15 +311,28 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, - reader.ReadByte() / 255f), + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), - (float)reader.ReadHalf()), - + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.UShort4 => ReadUShort4(reader), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } + + private byte[] ReadUShort4(BinaryReader reader) + { + var buffer = reader.ReadBytes(8); + var byteValues = new byte[8]; + byteValues[0] = buffer[0]; + byteValues[4] = buffer[1]; + byteValues[1] = buffer[2]; + byteValues[5] = buffer[3]; + byteValues[2] = buffer[4]; + byteValues[6] = buffer[5]; + byteValues[3] = buffer[6]; + byteValues[7] = buffer[7]; + return byteValues; + } /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlyDictionary usages) @@ -444,7 +457,16 @@ public class MeshExporter private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) - return typeof(VertexJoints4); + { + if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + { + return typeof(VertexJoints8); + } + else + { + return typeof(VertexJoints4); + } + } return typeof(VertexEmpty); } @@ -455,15 +477,17 @@ public class MeshExporter if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); - if (_skinningType == typeof(VertexJoints4)) + if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8)) { if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); - var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - - var bindings = Enumerable.Range(0, 4) + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indices = ToByteArray(indiciesData); + var weights = ToFloatArray(weightsData); + + var bindings = Enumerable.Range(0, indices.Length) .Select(bindingIndex => { // NOTE: I've not seen any files that throw this error that aren't completely broken. @@ -474,7 +498,13 @@ public class MeshExporter return (jointIndex, weights[bindingIndex]); }) .ToArray(); - return new VertexJoints4(bindings); + + return bindings.Length switch + { + 4 => new VertexJoints4(bindings), + 8 => new VertexJoints8(bindings), + _ => throw _notifier.Exception($"Invalid number of bone bindings {bindings.Length}.") + }; } throw _notifier.Exception($"Unknown skinning type {_skinningType}"); @@ -517,4 +547,12 @@ public class MeshExporter byte[] value => value, _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; + + private static float[] ToFloatArray(object data) + => data switch + { + byte[] value => value.Select(x => x / 255f).ToArray(), + _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), + }; } + diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1df97907..e3567780 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -194,17 +194,37 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); - var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - if (jointsAccessor == null || weightsAccessor == null) + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - for (var i = 0; i < jointsAccessor.Count; i++) + for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = jointsAccessor[i]; - var weights = weightsAccessor[i]; + var joints = joints0Accessor[i]; + var weights = weights0Accessor[i]; + for (var index = 0; index < 4; index++) + { + // If a joint has absolutely no weight, we omit the bone entirely. + if (weights[index] == 0) + continue; + + usedJoints.Add((ushort)joints[index]); + } + } + + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + + if (joints1Accessor == null || weights1Accessor == null) + continue; + + for (var i = 0; i < joints1Accessor.Count; i++) + { + var joints = joints1Accessor[i]; + var weights = weights1Accessor[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index af401ec1..b71ad429 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -40,6 +40,7 @@ public class VertexAttribute MdlFile.VertexType.NByte4 => 4, MdlFile.VertexType.Half2 => 4, MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.UShort4 => 8, _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; @@ -121,89 +122,219 @@ public class VertexAttribute public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); - var element = new MdlStructs.VertexElement() + if (accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; + if (!accessors.ContainsKey("JOINTS_1")) + throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; - var values = accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); - return new VertexAttribute( - element, - index => { - // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off - // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak - // the converted values to have the expected sum, preferencing values with minimal differences. - var originalValues = values[index]; - var byteValues = BuildNByte4(originalValues); - - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); - while (adjustment != 0) - { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); - var closestIndex = Enumerable.Range(0, 4) - .Where(index => { - var byteValue = byteValues[index]; - if (adjustment < 0) return byteValue > 0; - if (adjustment > 0) return byteValue < 255; - return true; - }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) - .MinBy(x => x.delta) - .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = weights1[index]; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); } - - return byteValues; - } - ); + ); + } + else + { + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = Vector4.Zero; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); + } + ); + + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.NByte4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var weight0 = weights0[index]; + var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + var newByteValues = AdjustByteArray(byteValues, originalData); + if (!newByteValues.SequenceEqual(byteValues)) + notifier.Warning("Adjusted blend weights to maintain precision."); + return newByteValues; + });*/ + } + } + + private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) + { + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + while (adjustment != 0) + { + var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); + var closestIndex = Enumerable.Range(0, byteValues.Length) + .Where(index => + { + var byteValue = byteValues[index]; + if (adjustment < 0) + return byteValue > 0; + if (adjustment > 0) + return byteValue < 255; + + return true; + }) + .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); + adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + } + + return byteValues; } public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) + if (!accessors.TryGetValue("JOINTS_0", out var joints0Accessor)) return null; - if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var element = new MdlStructs.VertexElement() + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + + if (accessors.TryGetValue("JOINTS_1", out var joints1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; + if (!accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) + throw notifier.Exception("Mesh contained JOINTS_1 attribute but no corresponding WEIGHTS_1 attribute."); - var joints = jointsAccessor.AsVector4Array(); - var weights = weightsAccessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => + var element = new MdlStructs.VertexElement { - var gltfIndices = joints[index]; - var gltfWeights = weights[index]; + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - ); + var joints1 = joints1Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1[index]; + var gltfWeights1 = weights1[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = new Vector4( + gltfWeights1.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.X], + gltfWeights1.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Y], + gltfWeights1.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Z], + gltfWeights1.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.W] + ); + + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + } + else + { + var element = new MdlStructs.VertexElement + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = Vector4.Zero; + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UByte4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices = joints0[index]; + var gltfWeights = weights0[index]; + return BuildUByte4(new Vector4( + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] + )); + } + );*/ + } } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) @@ -232,7 +363,7 @@ public class VertexAttribute var value = values[vertexIndex]; var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) + if (delta != null) value += delta.Value; return BuildSingle3(value); @@ -489,4 +620,18 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) + { + var buf = new float[8]; + buf[0] = v0.X; + buf[4] = v1.X; + buf[1] = v0.Y; + buf[5] = v1.Y; + buf[2] = v0.Z; + buf[6] = v1.Z; + buf[3] = v0.W; + buf[7] = v1.W; + return buf; + } } From fecdee05bda2de924b4b46aebf7c6f7d9ff3ccdd Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:35:35 +1000 Subject: [PATCH 1993/2451] Cleanup --- Penumbra/Import/Models/Export/MeshExporter.cs | 17 +- Penumbra/Import/Models/Import/MeshImporter.cs | 31 +- .../Import/Models/Import/VertexAttribute.cs | 272 +++++++----------- 3 files changed, 121 insertions(+), 199 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 20158776..3707ff79 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -314,25 +314,10 @@ public class MeshExporter MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.UShort4 => ReadUShort4(reader), + MdlFile.VertexType.UShort4 => reader.ReadBytes(8), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } - - private byte[] ReadUShort4(BinaryReader reader) - { - var buffer = reader.ReadBytes(8); - var byteValues = new byte[8]; - byteValues[0] = buffer[0]; - byteValues[4] = buffer[1]; - byteValues[1] = buffer[2]; - byteValues[5] = buffer[3]; - byteValues[2] = buffer[4]; - byteValues[6] = buffer[5]; - byteValues[3] = buffer[6]; - byteValues[7] = buffer[7]; - return byteValues; - } /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlyDictionary usages) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index e3567780..813ef422 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -196,7 +196,9 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); @@ -205,6 +207,8 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { var joints = joints0Accessor[i]; var weights = weights0Accessor[i]; + var joints1 = joints1Accessor?[i]; + var weights1 = weights1Accessor?[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. @@ -212,26 +216,11 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) continue; usedJoints.Add((ushort)joints[index]); - } - } - - var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); - var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); - - if (joints1Accessor == null || weights1Accessor == null) - continue; - - for (var i = 0; i < joints1Accessor.Count; i++) - { - var joints = joints1Accessor[i]; - var weights = weights1Accessor[i]; - for (var index = 0; index < 4; index++) - { - // If a joint has absolutely no weight, we omit the bone entirely. - if (weights[index] == 0) - continue; - - usedJoints.Add((ushort)joints[index]); + + if (joints1 != null && weights1 != null && weights1.Value[index] != 0) + { + usedJoints.Add((ushort)joints1.Value[index]); + } } } } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b71ad429..12ceba23 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -132,72 +132,50 @@ public class VertexAttribute { if (!accessors.ContainsKey("JOINTS_1")) throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - var weights1 = weights1Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => { - var weight0 = weights0[index]; - var weight1 = weights1[index]; - var originalData = BuildUshort4(weight0, weight1); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } - ); } - else + + var element = new MdlStructs.VertexElement() { - var element = new MdlStructs.VertexElement() + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); + + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = weights1?[index]; + var originalData = BuildUshort4(weight0, weight1 ?? Vector4.Zero); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); + } + ); + + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.NByte4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => { - var weight0 = weights0[index]; - var weight1 = Vector4.Zero; - var originalData = BuildUshort4(weight0, weight1); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } - ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => - { - var weight0 = weights0[index]; - var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - var newByteValues = AdjustByteArray(byteValues, originalData); - if (!newByteValues.SequenceEqual(byteValues)) - notifier.Warning("Adjusted blend weights to maintain precision."); - return newByteValues; - });*/ - } + var weight0 = weights0[index]; + var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + var newByteValues = AdjustByteArray(byteValues, originalData); + if (!newByteValues.SequenceEqual(byteValues)) + notifier.Warning("Adjusted blend weights to maintain precision."); + return newByteValues; + });*/ } private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) @@ -241,100 +219,77 @@ public class VertexAttribute if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var joints0 = joints0Accessor.AsVector4Array(); - var weights0 = weights0Accessor.AsVector4Array(); - - if (accessors.TryGetValue("JOINTS_1", out var joints1Accessor)) + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + accessors.TryGetValue("JOINTS_1", out var joints1Accessor); + accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor); + var element = new MdlStructs.VertexElement { - if (!accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) - throw notifier.Exception("Mesh contained JOINTS_1 attribute but no corresponding WEIGHTS_1 attribute."); + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - var element = new MdlStructs.VertexElement + var joints1 = joints1Accessor?.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); + + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - var joints1 = joints1Accessor.AsVector4Array(); - var weights1 = weights1Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1?[index]; + var gltfWeights1 = weights1?[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + + Vector4 v1; + if (gltfIndices1 != null && gltfWeights1 != null) { - var gltfIndices0 = joints0[index]; - var gltfWeights0 = weights0[index]; - var gltfIndices1 = joints1[index]; - var gltfWeights1 = weights1[index]; - var v0 = new Vector4( - gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], - gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], - gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], - gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + v1 = new Vector4( + gltfWeights1.Value.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.X], + gltfWeights1.Value.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Y], + gltfWeights1.Value.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Z], + gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W] ); - var v1 = new Vector4( - gltfWeights1.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.X], - gltfWeights1.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Y], - gltfWeights1.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Z], - gltfWeights1.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.W] - ); - - var byteValues = BuildUshort4(v0, v1); - - return byteValues.Select(x => (byte)x).ToArray(); } - ); - } - else + else + { + v1 = Vector4.Zero; + } + + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + + /*var element = new MdlStructs.VertexElement() { - var element = new MdlStructs.VertexElement - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices0 = joints0[index]; - var gltfWeights0 = weights0[index]; - var v0 = new Vector4( - gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], - gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], - gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], - gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] - ); - var v1 = Vector4.Zero; - var byteValues = BuildUshort4(v0, v1); + Stream = 0, + Type = (byte)MdlFile.VertexType.UByte4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - return byteValues.Select(x => (byte)x).ToArray(); - } - ); - /*var element = new MdlStructs.VertexElement() + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices = joints0[index]; - var gltfWeights = weights0[index]; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - );*/ - } + var gltfIndices = joints0[index]; + var gltfWeights = weights0[index]; + return BuildUByte4(new Vector4( + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] + )); + } + );*/ } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) @@ -620,18 +575,11 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; - - private static float[] BuildUshort4(Vector4 v0, Vector4 v1) - { - var buf = new float[8]; - buf[0] = v0.X; - buf[4] = v1.X; - buf[1] = v0.Y; - buf[5] = v1.Y; - buf[2] = v0.Z; - buf[6] = v1.Z; - buf[3] = v0.W; - buf[7] = v1.W; - return buf; - } + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) => + new[] + { + v0.X, v0.Y, v0.Z, v0.W, + v1.X, v1.Y, v1.Z, v1.W, + }; } From 9de6b3a9055bcd9e80a8c6d694bfbd22d5a37155 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:41:58 +1000 Subject: [PATCH 1994/2451] Vector4 to float array --- Penumbra/Import/Models/Export/MeshExporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 3707ff79..73160615 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -537,6 +537,7 @@ public class MeshExporter => data switch { byte[] value => value.Select(x => x / 255f).ToArray(), + Vector4 v4 => new[] { v4.X, v4.Y, v4.Z, v4.W }, _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), }; } From 9c6498e0282aa6626edc243abe4d364e1964e4c2 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:40:47 +1000 Subject: [PATCH 1995/2451] Conditionally still check for weights1 even if weights0 is 0 --- Penumbra/Import/Models/Import/MeshImporter.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 813ef422..6a46fb9f 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -205,17 +205,18 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Build a set of joints that are referenced by this mesh. for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = joints0Accessor[i]; - var weights = weights0Accessor[i]; + var joints0 = joints0Accessor[i]; + var weights0 = weights0Accessor[i]; var joints1 = joints1Accessor?[i]; var weights1 = weights1Accessor?[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. - if (weights[index] == 0) - continue; + if (weights0[index] != 0) + { + usedJoints.Add((ushort)joints0[index]); + } - usedJoints.Add((ushort)joints[index]); if (joints1 != null && weights1 != null && weights1.Value[index] != 0) { From 3e90524b0613fca177414b011ce00b59ac64039c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:40:59 +1000 Subject: [PATCH 1996/2451] Remove old impl comments --- .../Import/Models/Import/VertexAttribute.cs | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 12ceba23..743ee773 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -154,28 +154,6 @@ public class VertexAttribute return AdjustByteArray(byteValues, originalData); } ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => - { - var weight0 = weights0[index]; - var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - var newByteValues = AdjustByteArray(byteValues, originalData); - if (!newByteValues.SequenceEqual(byteValues)) - notifier.Warning("Adjusted blend weights to maintain precision."); - return newByteValues; - });*/ } private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) @@ -268,28 +246,6 @@ public class VertexAttribute return byteValues.Select(x => (byte)x).ToArray(); } ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices = joints0[index]; - var gltfWeights = weights0[index]; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - );*/ } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) From 5258c600b7f458aa37d60654378eb58b22372c5c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:35:11 +1000 Subject: [PATCH 1997/2451] Rework AdjustByteArray --- .../Import/Models/Import/VertexAttribute.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 743ee773..14a830d4 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -146,43 +146,39 @@ public class VertexAttribute return new VertexAttribute( element, - index => { - var weight0 = weights0[index]; - var weight1 = weights1?[index]; - var originalData = BuildUshort4(weight0, weight1 ?? Vector4.Zero); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } + index => BuildBlendWeights(weights0[index], weights1?[index] ?? Vector4.Zero) ); } - - private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) + + private static byte[] BuildBlendWeights(Vector4 v1, Vector4 v2) { + var originalData = BuildUshort4(v1, v2); + var byteValues = new byte[originalData.Length]; + for (var i = 0; i < originalData.Length; i++) + { + byteValues[i] = (byte)Math.Round(originalData[i] * 255f); + } + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak // the converted values to have the expected sum, preferencing values with minimal differences. - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + var adjustment = 255 - byteValues.Sum(value => value); while (adjustment != 0) { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); var closestIndex = Enumerable.Range(0, byteValues.Length) - .Where(index => + .Where(i => adjustment switch { - var byteValue = byteValues[index]; - if (adjustment < 0) - return byteValue > 0; - if (adjustment > 0) - return byteValue < 255; - - return true; + < 0 when byteValues[i] > 0 => true, + > 0 when byteValues[i] < 255 => true, + _ => true, }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f))))) .MinBy(x => x.delta) .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + byteValues[closestIndex] += (byte)Math.CopySign(1, adjustment); + adjustment = 255 - byteValues.Sum(value => value); } - + return byteValues; } @@ -226,7 +222,7 @@ public class VertexAttribute gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] ); - Vector4 v1; + var v1 = Vector4.Zero; if (gltfIndices1 != null && gltfWeights1 != null) { v1 = new Vector4( @@ -236,10 +232,6 @@ public class VertexAttribute gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W] ); } - else - { - v1 = Vector4.Zero; - } var byteValues = BuildUshort4(v0, v1); From 4719f413b6b7c101239490223f90c5e32a5f5de6 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:42:21 +1000 Subject: [PATCH 1998/2451] Fix adjustment switch --- Penumbra/Import/Models/Import/VertexAttribute.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 14a830d4..a1c3246b 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -168,9 +168,9 @@ public class VertexAttribute var closestIndex = Enumerable.Range(0, byteValues.Length) .Where(i => adjustment switch { - < 0 when byteValues[i] > 0 => true, - > 0 when byteValues[i] < 255 => true, - _ => true, + < 0 => byteValues[i] > 0, + > 0 => byteValues[i] < 255, + _ => true, }) .Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f))))) .MinBy(x => x.delta) From 8fa0875ec6bf37e2d48ddde571c77c724c528ccc Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 1 Sep 2024 21:32:31 +1000 Subject: [PATCH 1999/2451] Fix character*.shpk exports --- .../Import/Models/Export/MaterialExporter.cs | 188 ++++++++++-------- 1 file changed, 107 insertions(+), 81 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 62892473..0f98e5c4 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -36,12 +36,13 @@ public class MaterialExporter return material.Mtrl.ShaderPackage.Name switch { // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. - "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), - "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), - "hair.shpk" => BuildHair(material, name), - "iris.shpk" => BuildIris(material, name), - "skin.shpk" => BuildSkin(material, name), - _ => BuildFallback(material, name, notifier), + "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "hair.shpk" => BuildHair(material, name), + "iris.shpk" => BuildIris(material, name), + "skin.shpk" => BuildSkin(material, name), + _ => BuildFallback(material, name, notifier), }; } @@ -49,70 +50,65 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table!); + var table = new ColorTable(material.Mtrl.Table!); + var indexTexture = material.Textures[(TextureUsage)1449103320]; + var indexOperation = new ProcessCharacterIndexOperation(indexTexture, table); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, indexTexture.Bounds, in indexOperation); - var normal = material.Textures[TextureUsage.SamplerNormal]; + var normalTexture = material.Textures[TextureUsage.SamplerNormal]; + var normalOperation = new ProcessCharacterNormalOperation(normalTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normalTexture.Bounds, in normalOperation); - var operation = new ProcessCharacterNormalOperation(normal, table); - ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation); + // Merge in opacity from the normal. + var baseColor = indexOperation.BaseColor; + MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity); - // Check if full textures are provided, and merge in if available. - var baseColor = operation.BaseColor; + // Check if a full diffuse is provided, and merge in if available. if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { - MultiplyOperation.Execute(diffuse, operation.BaseColor); + MultiplyOperation.Execute(diffuse, indexOperation.BaseColor); baseColor = diffuse; } - Image specular = operation.Specular; + var specular = indexOperation.Specular; if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { - MultiplyOperation.Execute(specularTexture, operation.Specular); + MultiplyOperation.Execute(specularTexture, indexOperation.Specular); specular = specularTexture; } // Pull further information from the mask. if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { - // Extract the red channel for "ambient occlusion". - maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => - { - for (var y = 0; y < maskAccessor.Height; y++) - { - var maskSpan = maskAccessor.GetRowSpan(y); - var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskOperation = new ProcessCharacterMaskOperation(maskTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation); - for (var x = 0; x < maskSpan.Length; x++) - baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); - } - }); - // TODO: handle other textures stored in the mask? + // TODO: consider using the occusion gltf material property. + MultiplyOperation.Execute(baseColor, maskOperation.Occlusion); + + // Similar to base color's alpha, this is a pretty wasteful operation for a single channel. + MultiplyOperation.Execute(specular, maskOperation.SpecularFactor); } // Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture. var specularImage = BuildImage(specular, name, "specular"); return BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normalOperation.Normal, name, "normal")) + .WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1) .WithSpecularFactor(specularImage, 1) .WithSpecularColor(specularImage); } - // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. - // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. - private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation + private readonly struct ProcessCharacterIndexOperation(Image index, ColorTable table) : IRowOperation { - public Image Normal { get; } = normal.Clone(); - public Image BaseColor { get; } = new(normal.Width, normal.Height); - public Image Specular { get; } = new(normal.Width, normal.Height); - public Image Emissive { get; } = new(normal.Width, normal.Height); + public Image BaseColor { get; } = new(index.Width, index.Height); + public Image Specular { get; } = new(index.Width, index.Height); + public Image Emissive { get; } = new(index.Width, index.Height); - private Buffer2D NormalBuffer - => Normal.Frames.RootFrame.PixelBuffer; + private Buffer2D IndexBuffer + => index.Frames.RootFrame.PixelBuffer; private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; @@ -125,66 +121,96 @@ public class MaterialExporter public void Invoke(int y) { - var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var indexSpan = IndexBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < indexSpan.Length; x++) + { + ref var indexPixel = ref indexSpan[x]; + + // Calculate and fetch the color table rows being used for this pixel. + var tablePair = (int) Math.Round(indexPixel.R / 17f); + var rowBlend = 1.0f - indexPixel.G / 255f; + + var prevRow = table[tablePair * 2]; + var nextRow = table[Math.Min(tablePair * 2 + 1, ColorTable.NumRows)]; + + // Lerp between table row values to fetch final pixel values for each subtexture. + var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend); + baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); + emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); + } + } + } + + private readonly struct ProcessCharacterNormalOperation(Image normal) : IRowOperation + { + // TODO: Consider omitting the alpha channel here. + public Image Normal { get; } = normal.Clone(); + // TODO: We only really need the alpha here, however using A8 will result in the multiply later zeroing out the RGB channels. + public Image BaseColorOpacity { get; } = new(normal.Width, normal.Height); + + private Buffer2D NormalBuffer + => Normal.Frames.RootFrame.PixelBuffer; + + private Buffer2D BaseColorOpacityBuffer + => BaseColorOpacity.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) + { + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var baseColorOpacitySpan = BaseColorOpacityBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; - // Table row data (.a) - var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = table[tableRow.Previous]; - var nextRow = table[tableRow.Next]; + baseColorOpacitySpan[x].FromVector4(Vector4.One); + baseColorOpacitySpan[x].A = normalPixel.B; - // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); - baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); - baseColorSpan[x].A = normalPixel.B; - - // Specular (table) - var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); - - // Emissive (table) - var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); - emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); - - // Normal (.rg) - // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. normalPixel.B = byte.MaxValue; normalPixel.A = byte.MaxValue; } } } - private static TableRow GetTableRowIndices(float input) + private readonly struct ProcessCharacterMaskOperation(Image mask) : IRowOperation { - // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + public Image Occlusion { get; } = new(mask.Width, mask.Height); + public Image SpecularFactor { get; } = new(mask.Width, mask.Height); - var stepped = MathF.Floor(smoothed + 0.5f); + private Buffer2D MaskBuffer + => mask.Frames.RootFrame.PixelBuffer; - return new TableRow + private Buffer2D OcclusionBuffer + => Occlusion.Frames.RootFrame.PixelBuffer; + + private Buffer2D SpecularFactorBuffer + => SpecularFactor.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1, - }; - } + var maskSpan = MaskBuffer.DangerousGetRowSpan(y); + var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y); + var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y); - private ref struct TableRow - { - public int Stepped; - public int Previous; - public int Next; - public float Weight; + for (var x = 0; x < maskSpan.Length; x++) + { + ref var maskPixel = ref maskSpan[x]; + + occlusionSpan[x].FromL8(new L8(maskPixel.B)); + + specularFactorSpan[x].FromVector4(Vector4.One); + specularFactorSpan[x].A = maskPixel.R; + } + } } private readonly struct MultiplyOperation From 8468ed2c07f808c60c00e1ddd09fd7f9a5aadc06 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 00:01:30 +1000 Subject: [PATCH 2000/2451] Fix skin.shpk --- .../Import/Models/Export/MaterialExporter.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0f98e5c4..ee8484f0 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -340,21 +340,7 @@ public class MaterialExporter var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; var normal = material.Textures[TextureUsage.SamplerNormal]; - // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. - var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); - diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => - { - for (var y = 0; y < diffuseAccessor.Height; y++) - { - var diffuseSpan = diffuseAccessor.GetRowSpan(y); - var normalSpan = normalAccessor.GetRowSpan(y); - - for (var x = 0; x < diffuseSpan.Length; x++) - diffuseSpan[x].A = normalSpan[x].B; - } - }); - - // Clear the blue channel out of the normal now that we're done with it. + // The normal also stores the skin color influence (.b) and wetness mask (.a) - remove. normal.ProcessPixelRows(normalAccessor => { for (var y = 0; y < normalAccessor.Height; y++) @@ -362,7 +348,10 @@ public class MaterialExporter var normalSpan = normalAccessor.GetRowSpan(y); for (var x = 0; x < normalSpan.Length; x++) + { normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } } }); From efd08ae0534cd6fc3489e3e605a4e38c52bb040e Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 00:51:51 +1000 Subject: [PATCH 2001/2451] Add charactertattoo.shpk support --- .../Import/Models/Export/MaterialExporter.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index ee8484f0..31590400 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -39,6 +39,7 @@ public class MaterialExporter "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "charactertattoo.shpk" => BuildCharacterTattoo(material, name), "hair.shpk" => BuildHair(material, name), "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), @@ -244,6 +245,37 @@ public class MaterialExporter } } + private static readonly Vector4 DefaultTattooColor = new Vector4(38, 112, 102, 255) / new Vector4(255); + + private static MaterialBuilder BuildCharacterTattoo(Material material, string name) + { + var normal = material.Textures[TextureUsage.SamplerNormal]; + var baseColor = new Image(normal.Width, normal.Height); + + normal.ProcessPixelRows(baseColor, (normalAccessor, baseColorAccessor) => + { + for (var y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (var x = 0; x < normalSpan.Length; x++) + { + baseColorSpan[x].FromVector4(DefaultTattooColor); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(AlphaMode.BLEND); + } + // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here. private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); From 3b21de35cc0e0fac14d4d13600f4adffa2aa60cc Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 01:06:51 +1000 Subject: [PATCH 2002/2451] Fix iris.shpk --- .../Import/Models/Export/MaterialExporter.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 31590400..bcacf371 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -327,26 +327,23 @@ public class MaterialExporter // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now. private static MaterialBuilder BuildIris(Material material, string name) { - var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + var baseColor = material.Textures[TextureUsage.SamplerDiffuse]; - mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + mask.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - var baseColor = new Image(normal.Width, normal.Height); - normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + baseColor.ProcessPixelRows(mask, (baseColorAccessor, maskAccessor) => { - for (var y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < baseColor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); - for (var x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < baseColorSpan.Length; x++) { - baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f)); - baseColorSpan[x].A = normalSpan[x].A; - - normalSpan[x].A = byte.MaxValue; + var eyeColor = Vector4.Lerp(Vector4.One, DefaultEyeColor, maskSpan[x].B / 255f); + baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * eyeColor); } } }); From a1a880a0f4a85bdbe823e6e972f0bc59e2b8305f Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 01:30:21 +1000 Subject: [PATCH 2003/2451] Fix hair.shpk --- Penumbra/Import/Models/Export/MaterialExporter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index bcacf371..121e6eed 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -306,10 +306,11 @@ public class MaterialExporter for (var x = 0; x < normalSpan.Length; x++) { - var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f); - baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); + var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, normalSpan[x].B / 255f); + baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].A / 255f)); baseColorSpan[x].A = normalSpan[x].A; + normalSpan[x].B = byte.MaxValue; normalSpan[x].A = byte.MaxValue; } } From 76c0264cbee424429b7b6c611378015d227fd4c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:05:13 +0200 Subject: [PATCH 2004/2451] Reenable model IO for testing. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 490fa147..de088736 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -97,9 +97,7 @@ public partial class ModEditWindow private void DrawImportExport(MdlTab tab, bool disabled) { - // TODO: Enable when functional. - using var dawntrailDisabled = ImRaii.Disabled(); - if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true) + if (!ImGui.CollapsingHeader("Import / Export")) return; var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); From df0526e6e510c5129aa0deaf5038728318e5552f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:23:36 +0200 Subject: [PATCH 2005/2451] Fix readoing and displaying DemiHuman IMC Identifiers. --- Penumbra/Meta/Manipulations/Imc.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 1b2492ee..cba6c379 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -68,9 +68,13 @@ public readonly record struct ImcIdentifier( => (MetaIndex)(-1); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}"; + => ObjectType switch + { + ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", + ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", + _ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", + }; + public bool Validate() { @@ -102,6 +106,7 @@ public readonly record struct ImcIdentifier( return false; if (ItemData.AdaptOffhandImc(PrimaryId, out _)) return false; + break; } @@ -163,7 +168,7 @@ public readonly record struct ImcIdentifier( case ObjectType.DemiHuman: { var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); - var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var slot = jObj["EquipSlot"]?.ToObject() ?? EquipSlot.Unknown; ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown); break; } From 740816f3a670d9f53759d674795456a193ca202d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:51:52 +0200 Subject: [PATCH 2006/2451] Fix accessory VFX change not working. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index b1b23f27..a31dee4c 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -3,6 +3,8 @@ using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Resources; @@ -212,25 +214,39 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + [StructLayout(LayoutKind.Explicit)] + private struct ChangedEquipData + { + [FieldOffset(0)] + public PrimaryId Model; + + [FieldOffset(2)] + public Variant Variant; + + [FieldOffset(20)] + public ushort VfxId; + + [FieldOffset(22)] + public GenderRace GenderRace; + } + private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { if (slotIndex is <= 4 or >= 10) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var changedEquipData = ((Human*)drawObject)->ChangedEquipData; + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; // Enable vfxs for accessories if (changedEquipData == null) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex); - var model = slot[0]; - var variant = slot[1]; - var vfxId = slot[4]; + ref var slot = ref changedEquipData[slotIndex]; - if (model == 0 || variant == 0 || vfxId == 0) + if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0", + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", out _)) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); From c4b59295cb4db0ab1782fec14137d85b9e6de153 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 6 Oct 2024 12:54:51 +0000 Subject: [PATCH 2007/2451] [CI] Updating repo.json for testing_1.2.1.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 36b27682..38ea45c0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.5", + "TestingAssemblyVersion": "1.2.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2e424a693d67f16c02e154c28a8f63d9f7056fb2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 7 Oct 2024 16:18:51 +0200 Subject: [PATCH 2008/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index dd86dafb..34c96a55 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit dd86dafb88ca4c7b662938bbc1310729ba7f788d +Subproject commit 34c96a55efe1ce1296d9edcd8296f6396998cc6a From 4a0c996ff6d492d887a4712606f15e7cf69cb73a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Oct 2024 18:47:30 +0200 Subject: [PATCH 2009/2451] Fix some off-by-one errors with the import progress reports, add test implementation for pbd editing. --- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 17 +- Penumbra/Import/TexToolsImporter.Gui.cs | 19 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 + Penumbra/Mods/Editor/ModFileCollection.cs | 18 +- .../AdvancedWindow/ModEditWindow.Deformers.cs | 324 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 + 7 files changed, 368 insertions(+), 20 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 34c96a55..07b01ec9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 34c96a55efe1ce1296d9edcd8296f6396998cc6a +Subproject commit 07b01ec9b043e4b8f56d084f5d6cde1ed4ed9a58 diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 6d4f17b2..f6d1c9eb 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -9,7 +9,6 @@ using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,6 +27,8 @@ public class TemporaryIpcTester( { public Guid LastCreatedCollectionId = Guid.Empty; + private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9; + private Guid? _tempGuid; private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; @@ -48,9 +49,9 @@ public class TemporaryIpcTester( ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); - ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); - ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); @@ -102,7 +103,7 @@ public class TemporaryIpcTester( !collections.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { - var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -124,11 +125,11 @@ public class TemporaryIpcTester( public void DrawCollections() { - using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); + using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8); if (!collTree) return; - using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit); + using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -139,7 +140,7 @@ public class TemporaryIpcTester( var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) .FirstOrDefault() ?? "Unknown"; - if (ImGui.Button("Save##Collection")) + if (_debug && ImUtf8.Button("Save##Collection"u8)) TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character); using (ImRaii.PushFont(UiBuilder.MonoFont)) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index a069204c..f145f560 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -46,21 +46,28 @@ public partial class TexToolsImporter { ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; - ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); + if (_currentOptionIdx >= _currentNumOptions) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options"); + else + ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size, + $"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) ImGui.TextUnformatted( - $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); + $"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; - ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + if (_currentFileIdx >= _currentNumFiles) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files"); + else + ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + ImGui.TextUnformatted($"Extracting File {_currentFileName}..."); return false; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 3ae1eda9..7bbb762e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -151,6 +151,7 @@ public partial class TexToolsImporter _currentGroupName = string.Empty; _currentOptionName = "Default"; ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList); + ++_currentOptionIdx; } // Iterate through all pages @@ -208,6 +209,7 @@ public partial class TexToolsImporter options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); + ++_currentOptionIdx; } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 241f5b3b..20423493 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -12,6 +12,7 @@ public class ModFileCollection : IDisposable, IService private readonly List _mdl = []; private readonly List _tex = []; private readonly List _shpk = []; + private readonly List _pbd = []; private readonly SortedSet _missing = []; private readonly HashSet _usedPaths = []; @@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService => Ready ? _usedPaths : []; public IReadOnlyList Available - => Ready ? _available : Array.Empty(); + => Ready ? _available : []; public IReadOnlyList Mtrl - => Ready ? _mtrl : Array.Empty(); + => Ready ? _mtrl : []; public IReadOnlyList Mdl - => Ready ? _mdl : Array.Empty(); + => Ready ? _mdl : []; public IReadOnlyList Tex - => Ready ? _tex : Array.Empty(); + => Ready ? _tex : []; public IReadOnlyList Shpk - => Ready ? _shpk : Array.Empty(); + => Ready ? _shpk : []; + + public IReadOnlyList Pbd + => Ready ? _pbd : []; public bool Ready { get; private set; } = true; @@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService case ".shpk": _shpk.Add(registry); break; + case ".pbd": + _pbd.Add(registry); + break; } } } @@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService _mdl.Clear(); _tex.Clear(); _shpk.Clear(); + _pbd.Clear(); } private void ClearPaths(bool clearRegistries, CancellationToken tok) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs new file mode 100644 index 00000000..1b6535a7 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -0,0 +1,324 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private readonly FileEditor _pbdTab; + private readonly PbdData _pbdData = new(); + + private bool DrawDeformerPanel(PbdTab tab, bool disabled) + { + _pbdData.Update(tab.File); + DrawGenderRaceSelector(tab); + ImGui.SameLine(); + DrawBoneSelector(); + ImGui.SameLine(); + return DrawBoneData(tab, disabled); + } + + private void DrawGenderRaceSelector(PbdTab tab) + { + using var group = ImUtf8.Group(); + var width = ImUtf8.CalcTextSize("Hellsguard - Female (Child)____0000"u8).X + 2 * ImGui.GetStyle().WindowPadding.X; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("GenderRace"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + if (!child) + return; + + var metaColor = ColorId.ItemId.Value(); + foreach (var (deformer, index) in tab.File.Deformers.WithIndex()) + { + var name = deformer.GenderRace.ToName(); + var raceCode = deformer.GenderRace.ToRaceCode(); + // No clipping necessary since this are not that many objects anyway. + if (!name.Contains(_pbdData.RaceCodeFilter) && !raceCode.Contains(_pbdData.RaceCodeFilter)) + continue; + + using var id = ImUtf8.PushId(index); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), deformer.RacialDeformer.IsEmpty); + if (ImUtf8.Selectable(name, deformer.GenderRace == _pbdData.SelectedRaceCode)) + { + _pbdData.SelectedRaceCode = deformer.GenderRace; + _pbdData.SelectedDeformer = deformer.RacialDeformer; + } + + ImGui.SameLine(); + color.Push(ImGuiCol.Text, metaColor); + ImUtf8.TextRightAligned(raceCode); + } + } + + private void DrawBoneSelector() + { + using var group = ImUtf8.Group(); + var width = 200 * ImUtf8.GlobalScale; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("Bone"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + if (!child) + return; + + if (_pbdData.SelectedDeformer == null) + return; + + if (_pbdData.SelectedDeformer.IsEmpty) + { + ImUtf8.Text(""u8); + } + else + { + var height = ImGui.GetTextLineHeightWithSpacing(); + var skips = ImGuiClip.GetNecessarySkips(height); + var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, + b => b.Contains(_pbdData.BoneFilter), bone + => + { + if (ImUtf8.Selectable(bone, bone == _pbdData.SelectedBone)) + _pbdData.SelectedBone = bone; + }); + ImGuiClip.DrawEndDummy(remainder, height); + } + } + + private bool DrawBoneData(PbdTab tab, bool disabled) + { + using var child = ImUtf8.Child("Data"u8, ImGui.GetContentRegionMax() with { X = ImGui.GetContentRegionAvail().X}, true); + if (!child) + return false; + + if (_pbdData.SelectedBone == null) + return false; + + if (!_pbdData.SelectedDeformer!.DeformMatrices.TryGetValue(_pbdData.SelectedBone, out var matrix)) + return false; + + var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2; + var dummyHeight = ImGui.GetTextLineHeight() / 2; + var ret = DrawAddNewBone(tab, disabled, width); + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDeformerMatrix(disabled, matrix, width); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawCopyPasteButtons(disabled, matrix, width); + + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDecomposedData(disabled, matrix, width); + + return ret; + } + + private bool DrawAddNewBone(PbdTab tab, bool disabled, float width) + { + var ret = false; + ImUtf8.TextFrameAligned("Copy the values of the bone "u8); + ImGui.SameLine(0, 0); + using (ImRaii.PushColor(ImGuiCol.Text, ColorId.NewMod.Value())) + { + ImUtf8.TextFrameAligned(_pbdData.SelectedBone); + } + ImGui.SameLine(0, 0); + ImUtf8.TextFrameAligned(" to a new bone of name"u8); + + var fullWidth = width * 4 + ImGui.GetStyle().ItemSpacing.X * 3; + ImGui.SetNextItemWidth(fullWidth); + ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8); + ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8); + ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X); + if (!ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null)) + return ret; + + foreach (var deformer in tab.File.Deformers) + { + if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) + continue; + + if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) + && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) + && !newBoneMatrix.Equals(existingMatrix)) + Penumbra.Messager.AddMessage(new Notification( + $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", + NotificationType.Warning)); + else + ret = true; + } + + _pbdData.NewBoneName = string.Empty; + return ret; + } + + private bool DrawDeformerMatrix(bool disabled, in TransformMatrix matrix, float width) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + var ret = false; + for (var i = 0; i < 3; ++i) + { + for (var j = 0; j < 4; ++j) + { + using var id = ImUtf8.PushId(i * 4 + j); + ImGui.SetNextItemWidth(width); + var tmp = matrix[i, j]; + if (ImUtf8.InputScalar(""u8, ref tmp, "% 12.8f"u8)) + { + ret = true; + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = matrix.ChangeValue(i, j, tmp); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + return ret; + } + + private bool DrawCopyPasteButtons(bool disabled, in TransformMatrix matrix, float width) + { + var size = new Vector2(width, 0); + if (ImUtf8.Button("Copy Values"u8, size)) + _pbdData.CopiedMatrix = matrix; + + ImGui.SameLine(); + + if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue)) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value; + return true; + } + + return false; + } + + private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width) + { + var ret = false; + + + if (!matrix.TryDecompose(out var scale, out var rotation, out var translation)) + return false; + + using (ImUtf8.Group()) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleX"u8, ref scale.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleY"u8, ref scale.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleZ"u8, ref scale.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationX"u8, ref translation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationY"u8, ref translation.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationZ"u8, ref translation.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationR"u8, ref rotation.W, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationI"u8, ref rotation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationJ"u8, ref rotation.Y, "% 12.8f"u8); + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationK"u8, ref rotation.Z, "% 12.8f"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Scale"u8); + ImUtf8.TextFrameAligned("Translation"u8); + ImUtf8.TextFrameAligned("Rotation (Quaternion, rijk)"u8); + } + + if (ret) + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = TransformMatrix.Compose(scale, rotation, translation); + return ret; + } + + public class PbdTab(byte[] data, string filePath) : IWritable + { + public readonly string FilePath = filePath; + + public readonly PbdFile File = new(data); + + public bool Valid + => File.Valid; + + public byte[] Write() + => File.Write(); + } + + private class PbdData + { + public GenderRace SelectedRaceCode = GenderRace.Unknown; + public RacialDeformer? SelectedDeformer; + public string? SelectedBone; + public string NewBoneName = string.Empty; + public string BoneFilter = string.Empty; + public string RaceCodeFilter = string.Empty; + + public TransformMatrix? CopiedMatrix; + + public void Update(PbdFile file) + { + if (SelectedRaceCode is GenderRace.Unknown) + { + SelectedDeformer = null; + } + else + { + SelectedDeformer = file.Deformers.FirstOrDefault(p => p.GenderRace == SelectedRaceCode).RacialDeformer; + if (SelectedDeformer is null) + SelectedRaceCode = GenderRace.Unknown; + } + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f2fe8b9e..1a4065bb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -236,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _itemSwapTab.DrawContent(); } + _pbdTab.Draw(); + DrawMissingFilesTab(); DrawMaterialReassignmentTab(); } @@ -665,6 +667,10 @@ public partial class ModEditWindow : Window, IDisposable, IUiService () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); + _pbdTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Deformers", ".pbd", + () => _editor.Files.Pbd, DrawDeformerPanel, + () => Mod?.ModPath.FullName ?? string.Empty, + (bytes, path, _) => new PbdTab(bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; From 40c772a9da9f8b41dad7e023b7a47c48ea8b338e Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 9 Oct 2024 16:49:59 +0000 Subject: [PATCH 2010/2451] [CI] Updating repo.json for testing_1.2.1.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 38ea45c0..0d9e071f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.6", + "TestingAssemblyVersion": "1.2.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2c5ffc1bc583db158c932ab057a686d6275186a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Oct 2024 16:50:05 +0200 Subject: [PATCH 2011/2451] Add delete and single add button and fix child sizes. --- .../AdvancedWindow/ModEditWindow.Deformers.cs | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 1b6535a7..258e51ff 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -38,7 +38,8 @@ public partial class ModEditWindow ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8); } - using var child = ImUtf8.Child("GenderRace"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + using var child = ImUtf8.Child("GenderRace"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); if (!child) return; @@ -76,7 +77,8 @@ public partial class ModEditWindow ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); } - using var child = ImUtf8.Child("Bone"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + using var child = ImUtf8.Child("Bone"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); if (!child) return; @@ -104,7 +106,8 @@ public partial class ModEditWindow private bool DrawBoneData(PbdTab tab, bool disabled) { - using var child = ImUtf8.Child("Data"u8, ImGui.GetContentRegionMax() with { X = ImGui.GetContentRegionAvail().X}, true); + using var child = ImUtf8.Child("Data"u8, + ImGui.GetContentRegionAvail() with { Y = ImGui.GetContentRegionMax().Y - ImGui.GetStyle().WindowPadding.Y }, true); if (!child) return false; @@ -116,7 +119,7 @@ public partial class ModEditWindow var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2; var dummyHeight = ImGui.GetTextLineHeight() / 2; - var ret = DrawAddNewBone(tab, disabled, width); + var ret = DrawAddNewBone(tab, disabled, matrix, width); ImUtf8.Dummy(0, dummyHeight); ImGui.Separator(); @@ -134,7 +137,7 @@ public partial class ModEditWindow return ret; } - private bool DrawAddNewBone(PbdTab tab, bool disabled, float width) + private bool DrawAddNewBone(PbdTab tab, bool disabled, in TransformMatrix matrix, float width) { var ret = false; ImUtf8.TextFrameAligned("Copy the values of the bone "u8); @@ -143,6 +146,7 @@ public partial class ModEditWindow { ImUtf8.TextFrameAligned(_pbdData.SelectedBone); } + ImGui.SameLine(0, 0); ImUtf8.TextFrameAligned(" to a new bone of name"u8); @@ -151,26 +155,36 @@ public partial class ModEditWindow ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8); ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8); ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X); - if (!ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), + if (ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null)) - return ret; - - foreach (var deformer in tab.File.Deformers) { - if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) - continue; + foreach (var deformer in tab.File.Deformers) + { + if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) + continue; - if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) - && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) - && !newBoneMatrix.Equals(existingMatrix)) - Penumbra.Messager.AddMessage(new Notification( - $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", - NotificationType.Warning)); - else - ret = true; + if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) + && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) + && !newBoneMatrix.Equals(existingMatrix)) + Penumbra.Messager.AddMessage(new Notification( + $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", + NotificationType.Warning)); + else + ret = true; + } + + _pbdData.NewBoneName = string.Empty; } - _pbdData.NewBoneName = string.Empty; + if (ImUtf8.ButtonEx("Copy Values to Single New Bone Entry"u8, ""u8, new Vector2(fullWidth, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedDeformer!.DeformMatrices.ContainsKey(_pbdData.NewBoneName))) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.NewBoneName] = matrix; + ret = true; + _pbdData.NewBoneName = string.Empty; + } + + return ret; } @@ -209,13 +223,29 @@ public partial class ModEditWindow ImGui.SameLine(); + var ret = false; if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue)) { _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value; - return true; + ret = true; } - return false; + var modifier = _config.DeleteModModifier.IsActive(); + ImGui.SameLine(); + if (modifier) + { + if (ImUtf8.ButtonEx("Delete"u8, "Delete this bone entry."u8, size, disabled)) + { + ret |= _pbdData.SelectedDeformer!.DeformMatrices.Remove(_pbdData.SelectedBone!); + _pbdData.SelectedBone = null; + } + } + else + { + ImUtf8.ButtonEx("Delete"u8, $"Delete this bone entry. Hold {_config.DeleteModModifier} to delete.", size, true); + } + + return ret; } private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width) From 1d5a7a41ab9e056d7070d188d01f414debc1f81f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Oct 2024 16:35:47 +0200 Subject: [PATCH 2012/2451] Remove BonusItem from use and update ResourceTree a bit. --- Penumbra.GameData | 2 +- .../ResolveContext.PathResolution.cs | 8 +- .../Interop/ResourceTree/ResolveContext.cs | 19 ++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 11 ++- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 94 ++++++++++--------- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07b01ec9..61e06785 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07b01ec9b043e4b8f56d084f5d6cde1ed4ed9a58 +Subproject commit 61e067857c2cf62bf8426ff6b305e37990f7767a diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 43324516..c554d97a 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -36,13 +36,13 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } private GenderRace ResolveModelRaceCode() - => ResolveEqdpRaceCode(Slot, Equipment.Set); + => ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set); private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { @@ -161,7 +161,7 @@ internal partial record ResolveContext return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists); if (!exists) return variant.Id; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b99ee235..207551e7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -30,7 +30,7 @@ internal record GlobalResolveContext( public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, - EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) + FullEquipType slot = FullEquipType.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } @@ -38,7 +38,7 @@ internal unsafe partial record ResolveContext( GlobalResolveContext Global, Pointer CharacterBasePointer, uint SlotIndex, - EquipSlot Slot, + FullEquipType Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { @@ -346,13 +346,14 @@ internal unsafe partial record ResolveContext( if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { - var name = Slot switch + var name = item.Name; + if (Slot is FullEquipType.Finger) + name = SlotIndex switch { - EquipSlot.RFinger => "R: ", - EquipSlot.LFinger => "L: ", - _ => string.Empty, - } - + item.Name; + 8 => "R: " + name, + 9 => "L: " + name, + _ => name, + }; return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } @@ -361,7 +362,7 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + ? new ResourceNode.UiData(Slot.ToName(), Slot.GetCategoryIcon().ToFlag()) : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 38f6fe97..246a4508 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -80,12 +80,13 @@ public class ResourceTree { ModelType.Human => i switch { - < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), - 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), - _ => globalContext.CreateContext(model, i), + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]), + 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), + 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), + _ => globalContext.CreateContext(model, i), }, _ => i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + ? globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]) : globalContext.CreateContext(model, i), }; @@ -133,7 +134,7 @@ public class ResourceTree var weapon = (Weapon*)subObject; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; + var slot = weaponIndex > 0 ? FullEquipType.UnknownOffhand : FullEquipType.UnknownMainhand; var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1)); var weaponType = weapon->SecondaryId; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 361094c4..3aff2ac9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -190,52 +190,6 @@ public class ResourceTreeViewer var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) - { - if (!_typeFilter.HasFlag(filterIcon)) - return false; - - if (_nodeFilter.Length == 0) - return true; - - return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); - } - - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (node.Internal && !debugMode) - return NodeVisibility.Hidden; - - var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; - if (MatchesFilter(node, filterIcon)) - return NodeVisibility.Visible; - - foreach (var child in node.Children) - { - if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) - return NodeVisibility.DescendentsOnly; - } - - return NodeVisibility.Hidden; - } - - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) - { - visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); - _filterCache.Add(nodePathHash, visibility); - } - - return visibility; - } - - string GetAdditionalDataSuffix(CiByteString data) - => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; - foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); @@ -346,6 +300,54 @@ public class ResourceTreeViewer if (unfolded) DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); } + + return; + + string GetAdditionalDataSuffix(CiByteString data) + => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; + + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) + { + visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); + _filterCache.Add(nodePathHash, visibility); + } + + return visibility; + } + + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (node.Internal && !debugMode) + return NodeVisibility.Hidden; + + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; + if (MatchesFilter(node, filterIcon)) + return NodeVisibility.Visible; + + foreach (var child in node.Children) + { + if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) + return NodeVisibility.DescendentsOnly; + } + + return NodeVisibility.Hidden; + } + + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) + { + if (!_typeFilter.HasFlag(filterIcon)) + return false; + + if (_nodeFilter.Length == 0) + return true; + + return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + } } [Flags] From db2ce1328ff058548ed159b6ac5908f43a6f2045 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Oct 2024 18:19:12 +0200 Subject: [PATCH 2013/2451] Enable VFX for the glasses slot. --- Penumbra.GameData | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 61 ++++++++++++++----- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 61e06785..2f6acca6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 61e067857c2cf62bf8426ff6b305e37990f7767a +Subproject commit 2f6acca678b71203763ac4404c3f054747c14f75 diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index a31dee4c..d55caf34 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -6,6 +6,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -223,6 +224,12 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable [FieldOffset(2)] public Variant Variant; + [FieldOffset(8)] + public PrimaryId BonusModel; + + [FieldOffset(10)] + public Variant BonusVariant; + [FieldOffset(20)] public ushort VfxId; @@ -232,26 +239,50 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex is <= 4 or >= 10) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + switch (slotIndex) + { + case <= 4: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + case <= 10: + { + // Enable vfxs for accessories + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; - // Enable vfxs for accessories - if (changedEquipData == null) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + ref var slot = ref changedEquipData[slotIndex]; - ref var slot = ref changedEquipData[slotIndex]; + if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), - $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", - out _)) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + case 16: + { + // Enable vfxs for glasses + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - *(ulong*)unkOutParam = 4; - return ResolvePath(drawObject, pathBuffer); + ref var slot = ref changedEquipData[slotIndex - 6]; + + if (slot.BonusModel == 0 || slot.BonusVariant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/equipment/e{slot.BonusModel.Id:D4}/vfx/eff/ve{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + default: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + } } private nint VFunc81(nint drawObject, int estType, nint unk) From 97b310ca3ffa9397e722a429b8fc2665c4728e37 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 12 Oct 2024 15:13:06 +0200 Subject: [PATCH 2014/2451] Fix issue with meta file not being saved synchronously on creation. --- Penumbra/Mods/Manager/ModDataEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 933620d9..162f823d 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -37,7 +37,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - saveService.ImmediateSave(new ModMeta(mod)); + saveService.ImmediateSaveSync(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) From e646b48afaa58db4d1ac6c19fc7661cc8bbdbcc8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 12 Oct 2024 15:13:22 +0200 Subject: [PATCH 2015/2451] Add swaps to and from Glasses. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 77 ++++++------ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 135 ++++++++++++++++------ 3 files changed, 138 insertions(+), 76 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2f6acca6..63cbf824 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2f6acca678b71203763ac4404c3f054747c14f75 +Subproject commit 63cbf824178b5b1f91fd9edc22a6c2bbc2e1cd23 diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 1a2f2798..c7e43a26 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -32,26 +32,26 @@ public static class EquipmentSwap EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); var mtrlVariantTo = imcEntry.MaterialId; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -94,7 +94,7 @@ public static class EquipmentSwap { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); @@ -111,7 +111,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); @@ -122,18 +122,18 @@ public static class EquipmentSwap { EquipSlot.Head => EstType.Head, EquipSlot.Body => EstType.Body, - _ => (EstType)0, + _ => (EstType)0, }; var skipFemale = false; - var skipMale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -148,7 +148,7 @@ public static class EquipmentSwap swaps.Add(eqdp); var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; - var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); } @@ -176,7 +176,6 @@ public static class EquipmentSwap return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); @@ -186,9 +185,9 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); - var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); - var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, eqdpFromDefault, eqdpToIdentifier, eqdpToDefault); @@ -217,7 +216,7 @@ public static class EquipmentSwap ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -242,13 +241,13 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var imc = new ImcFile(manager, ident); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; - Variant[] variants; + Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); variants = [variantFrom]; } else @@ -271,9 +270,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new GmpIdentifier(idFrom); - var manipToIdentifier = new GmpIdentifier(idTo); - var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -288,9 +287,9 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); @@ -329,7 +328,7 @@ public static class EquipmentSwap var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { @@ -347,9 +346,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new EqpIdentifier(idFrom, slot); - var manipToIdentifier = new EqpIdentifier(idTo, slot); - var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); - var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -381,7 +380,7 @@ public static class EquipmentSwap if (newFileName != fileName) { - fileName = newFileName; + fileName = newFileName; dataWasChanged = true; } @@ -406,13 +405,13 @@ public static class EquipmentSwap EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } @@ -430,8 +429,8 @@ public static class EquipmentSwap PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); - filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6ed1b55d..3f7f2f6c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -48,15 +48,16 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), // @formatter:on }; @@ -129,6 +130,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Ears, Tail, Weapon, + Glasses, } private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) @@ -158,14 +160,14 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private ModSettings? _modSettings; private bool _dirty; - private SwapType _lastTab = SwapType.Hair; - private Gender _currentGender = Gender.Male; - private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId; - private int _sourceId; - private Exception? _loadException; - private EquipSlot _slotFrom = EquipSlot.Head; - private EquipSlot _slotTo = EquipSlot.Ears; + private SwapType _lastTab = SwapType.Hair; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId; + private int _sourceId; + private Exception? _loadException; + private BetweenSlotTypes _slotFrom = BetweenSlotTypes.Hat; + private BetweenSlotTypes _slotTo = BetweenSlotTypes.Earrings; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; @@ -200,18 +202,19 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Necklace: case SwapType.Bracelet: case SwapType.Ring: + case SwapType.Glasses: var values = _selectors[_lastTab]; if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); - break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item, + _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), + selectorFrom.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -264,7 +267,23 @@ public class ItemSwapTab : IDisposable, ITab, IUiService } private string CreateDescription() - => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + { + switch (_lastTab) + { + case SwapType.Ears: + case SwapType.Face: + case SwapType.Hair: + case SwapType.Tail: + return + $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + case SwapType.BetweenSlots: + return + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}."; + default: + return + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}."; + } + } private void UpdateOption() { @@ -416,6 +435,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService DrawEquipmentSwap(SwapType.Necklace); DrawEquipmentSwap(SwapType.Bracelet); DrawEquipmentSwap(SwapType.Ring); + DrawEquipmentSwap(SwapType.Glasses); DrawAccessorySwap(); DrawHairSwap(); //DrawFaceSwap(); @@ -454,23 +474,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName())) + using (var combo = ImRaii.Combo("##fromType", ToName(_slotFrom))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head)) + foreach (var slot in Enum.GetValues()) { - if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom) + if (!ImGui.Selectable(ToName(slot), slot == _slotFrom) || slot == _slotFrom) continue; _dirty = true; _slotFrom = slot; if (slot == _slotTo) - _slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s); + _slotTo = AvailableToTypes.First(s => slot != s); } } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, + InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -480,12 +501,12 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##toType", _slotTo.ToName())) + using (var combo = ImRaii.Combo("##toType", ToName(_slotTo))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom)) + foreach (var slot in AvailableToTypes.Where(t => t != _slotFrom)) { - if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo) + if (!ImGui.Selectable(ToName(slot), slot == _slotTo) || slot == _slotTo) continue; _dirty = true; @@ -508,17 +529,18 @@ public class ItemSwapTab : IDisposable, ITab, IUiService .Select(i => i.Name))); } - private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) + private (string, string, ItemSelector) GetAccessorySelector(BetweenSlotTypes slot, bool source) { var (type, article1, article2) = slot switch { - EquipSlot.Head => (SwapType.Hat, "this", "it"), - EquipSlot.Ears => (SwapType.Earrings, "these", "them"), - EquipSlot.Neck => (SwapType.Necklace, "this", "it"), - EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"), - EquipSlot.RFinger => (SwapType.Ring, "this", "it"), - EquipSlot.LFinger => (SwapType.Ring, "this", "it"), - _ => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Hat => (SwapType.Hat, "this", "it"), + BetweenSlotTypes.Earrings => (SwapType.Earrings, "these", "them"), + BetweenSlotTypes.Necklace => (SwapType.Necklace, "this", "it"), + BetweenSlotTypes.Bracelets => (SwapType.Bracelet, "these", "them"), + BetweenSlotTypes.RightRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.LeftRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Glasses => (SwapType.Glasses, "these", "them"), + _ => (SwapType.Ring, "this", "it"), }; var (itemSelector, target, _, _) = _selectors[type]; return (article1, article2, source ? itemSelector : target); @@ -689,6 +711,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService SwapType.Necklace => "One of the selected necklaces does not seem to exist.", SwapType.Bracelet => "One of the selected bracelets does not seem to exist.", SwapType.Ring => "One of the selected rings does not seem to exist.", + SwapType.Glasses => "One of the selected glasses does not seem to exist.", SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", @@ -746,4 +769,44 @@ public class ItemSwapTab : IDisposable, ITab, IUiService UpdateOption(); _dirty = true; } + + private enum BetweenSlotTypes + { + Hat, + Earrings, + Necklace, + Bracelets, + RightRing, + LeftRing, + Glasses, + } + + private static EquipSlot ToEquipSlot(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => EquipSlot.Head, + BetweenSlotTypes.Earrings => EquipSlot.Ears, + BetweenSlotTypes.Necklace => EquipSlot.Neck, + BetweenSlotTypes.Bracelets => EquipSlot.Wrists, + BetweenSlotTypes.RightRing => EquipSlot.RFinger, + BetweenSlotTypes.LeftRing => EquipSlot.LFinger, + BetweenSlotTypes.Glasses => BonusItemFlag.Glasses.ToEquipSlot(), + _ => EquipSlot.Unknown, + }; + + private static string ToName(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => "Hat", + BetweenSlotTypes.Earrings => "Earrings", + BetweenSlotTypes.Necklace => "Necklace", + BetweenSlotTypes.Bracelets => "Bracelets", + BetweenSlotTypes.RightRing => "Right Ring", + BetweenSlotTypes.LeftRing => "Left Ring", + BetweenSlotTypes.Glasses => "Glasses", + _ => "Unknown", + }; + + private static readonly IReadOnlyList AvailableToTypes = + Enum.GetValues().Where(s => s is not BetweenSlotTypes.Hat).ToArray(); } From a54e45f9c3b59e0532d422230a23c0a2748172b8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 12 Oct 2024 13:15:18 +0000 Subject: [PATCH 2016/2451] [CI] Updating repo.json for testing_1.2.1.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0d9e071f..4e8b1ed8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.7", + "TestingAssemblyVersion": "1.2.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 50db83146adab19207e9b867ca8768ae1d7cc06f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Oct 2024 13:55:01 +0200 Subject: [PATCH 2017/2451] Maybe fix left finger resource nodes. --- .../ResolveContext.PathResolution.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 4 +-- Penumbra/Interop/ResourceTree/ResourceNode.cs | 32 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 10 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index c554d97a..79f97881 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -37,7 +37,7 @@ internal partial record ResolveContext { var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()); + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 207551e7..54612070 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -340,9 +340,9 @@ internal unsafe partial record ResolveContext( internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { - var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); + var path = gamePath.Path.Split((byte)'/'); // Weapons intentionally left out. - var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment"; + var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 85d12ce7..6c3e1ebe 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -7,20 +7,20 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceNode : ICloneable { - public string? Name; - public string? FallbackName; - public ChangedItemIconFlag IconFlag; - public readonly ResourceType Type; - public readonly nint ObjectAddress; - public readonly nint ResourceHandle; - public Utf8GamePath[] PossibleGamePaths; - public FullPath FullPath; - public string? ModName; - public string? ModRelativePath; - public CiByteString AdditionalData; - public readonly ulong Length; - public readonly List Children; - internal ResolveContext? ResolveContext; + public string? Name; + public string? FallbackName; + public ChangedItemIconFlag IconFlag; + public readonly ResourceType Type; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; + public Utf8GamePath[] PossibleGamePaths; + public FullPath FullPath; + public string? ModName; + public string? ModRelativePath; + public CiByteString AdditionalData; + public readonly ulong Length; + public readonly List Children; + internal ResolveContext? ResolveContext; public Utf8GamePath GamePath { @@ -53,7 +53,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - IconFlag = other.IconFlag; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -82,7 +82,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { - Name = uiData.Name; + Name = uiData.Name; IconFlag = uiData.IconFlag; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 246a4508..89e0c62b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -81,8 +81,8 @@ public class ResourceTree ModelType.Human => i switch { < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]), - 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), - 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), + 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), + 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), _ => globalContext.CreateContext(model, i), }, _ => i < equipment.Length @@ -185,7 +185,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.IconFlag = ChangedItemIconFlag.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -203,7 +203,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.IconFlag = ChangedItemIconFlag.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -220,7 +220,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); From 9bd1f86a1d31efe180cc5755852bc6f129e4a187 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 13 Oct 2024 11:57:07 +0000 Subject: [PATCH 2018/2451] [CI] Updating repo.json for testing_1.2.1.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4e8b1ed8..2ead369a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.8", + "TestingAssemblyVersion": "1.2.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 339d1f8caf00e6afe3fde06ea20685523020887d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Oct 2024 14:41:21 +0200 Subject: [PATCH 2019/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 63cbf824..554e28a3 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 63cbf824178b5b1f91fd9edc22a6c2bbc2e1cd23 +Subproject commit 554e28a3d1fca9394a20fd9856f6387e2a5e4a57 From 9ddb011545c3b94b7c4958eb47f674627f93bc29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Oct 2024 16:03:08 +0200 Subject: [PATCH 2020/2451] Fix issue with long mod titles in the merge mods tab. --- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index b5f0255c..bd62089f 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -45,9 +46,30 @@ public class ModMergeTab(ModMerger modMerger) : IUiService private void DrawMergeInto(float size) { - using var bigGroup = ImRaii.Group(); + using var bigGroup = ImRaii.Group(); + var minComboSize = 300 * ImGuiHelpers.GlobalScale; + var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; + ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into "); + + using (ImRaii.Group()) + { + ImUtf8.Text("Merge "u8); + ImGui.SameLine(0, 0); + if (size - textSize < minComboSize) + { + ImUtf8.Text("selected mod"u8, ColorId.FolderLine.Value()); + ImUtf8.HoverTooltip(modMerger.MergeFromMod!.Name.Text); + } + else + { + ImUtf8.Text(modMerger.MergeFromMod!.Name.Text, ColorId.FolderLine.Value()); + } + + ImGui.SameLine(0, 0); + ImUtf8.Text(" into"u8); + } + ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); From 472d803141dab3baa9cf59bef4461986ad1a1f84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Oct 2024 16:24:30 +0200 Subject: [PATCH 2021/2451] 1.3.0.0 --- Penumbra/UI/Changelog.cs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 41920d1c..48ac90d8 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -53,10 +53,42 @@ public class PenumbraChangelog : IUiService Add1_1_0_0(Changelog); Add1_1_1_0(Changelog); Add1_2_1_0(Changelog); + Add1_3_0_0(Changelog); } #region Changelogs + private static void Add1_3_0_0(Changelog log) + => log.NextVersion("Version 1.3.0.0") + + .RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.") + .RegisterEntry("BC4 and BC6 textures can now also be imported.", 1) + .RegisterHighlight("Added item swapping from and to the Glasses slot.") + .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1) + .RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.") + .RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1) + .RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") + .RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1) + .RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).") + .RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.") + .RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1) + .RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.") + .RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.") + .RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") + .RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod") + .RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1) + .RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.") + .RegisterEntry("Paths from the resource logger can now be copied.") + .RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.") + .RegisterEntry("Added 'Page' to imported mod data for TexTools interop. The value is not used in Penumbra, just persisted.") + .RegisterEntry("Updated all external dependencies.") + .RegisterEntry("Fixed issue with Demihuman IMC entries.") + .RegisterEntry("Fixed some off-by-one errors on the mod import window.") + .RegisterEntry("Fixed a race-condition concerning the first-time creation of mod-meta files.") + .RegisterEntry("Fixed an issue with long mod titles in the merge mods tab.") + .RegisterEntry("A bunch of other miscellaneous fixes."); + + private static void Add1_2_1_0(Changelog log) => log.NextVersion("Version 1.2.1.0") .RegisterHighlight("Penumbra is now released for Dawntrail!") From 71101ef553dd19f50678fae379a8f617bb5cd9fb Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 18 Oct 2024 14:26:25 +0000 Subject: [PATCH 2022/2451] [CI] Updating repo.json for 1.3.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2ead369a..cba274c8 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.9", + "AssemblyVersion": "1.3.0.0", + "TestingAssemblyVersion": "1.3.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 69971c12afd084192dbf40b1f45068d1b6d93916 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Oct 2024 19:23:51 +0200 Subject: [PATCH 2023/2451] Fix EQP entries for earring hiding. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/GlobalEqpCache.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 554e28a3..e9fc5930 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 554e28a3d1fca9394a20fd9856f6387e2a5e4a57 +Subproject commit e9fc5930a9c035c1e1e3c87ee9bcc4f05eb3015b diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index efcab109..60e782b5 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -40,7 +40,7 @@ public class GlobalEqpCache : ReadWriteDictionary, original |= EqpEntry.HeadShowHrothgarHat; if (_doNotHideEarrings.Contains(armor[5].Set)) - original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman; + original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura; if (_doNotHideNecklace.Contains(armor[6].Set)) original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; From 7e6ea5008c3f2ee678a8f0505354fb63676d49fd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 17:30:45 +0100 Subject: [PATCH 2024/2451] Maybe fix other issue with left rings and resource trees. --- Penumbra.GameData | 2 +- .../ResourceTree/ResolveContext.PathResolution.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e9fc5930..e39a04c8 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e9fc5930a9c035c1e1e3c87ee9bcc4f05eb3015b +Subproject commit e39a04c83b67246580492677414888357b5ebed8 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 79f97881..b1cbb74d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -36,17 +36,16 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } private GenderRace ResolveModelRaceCode() - => ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set); + => ResolveEqdpRaceCode(SlotIndex, Equipment.Set); - private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) + private unsafe GenderRace ResolveEqdpRaceCode(uint slotIndex, PrimaryId primaryId) { - var slotIndex = slot.ToIndex(); if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; @@ -61,6 +60,7 @@ internal partial record ResolveContext var metaCache = Global.Collection.MetaCache; var entry = metaCache?.GetEqdpEntry(characterRaceCode, accessory, primaryId) ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, characterRaceCode, accessory, primaryId); + var slot = slotIndex.ToEquipSlot(); if (entry.ToBits(slot).Item2) return characterRaceCode; @@ -272,7 +272,7 @@ internal partial record ResolveContext { var human = (Human*)CharacterBase; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; - return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); + return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot.ToIndex(), equipment.Set), type, equipment.Set); } private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, From 2358eb378deebd960b16619f243e16fc9a86e845 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 17:31:38 +0100 Subject: [PATCH 2025/2451] Fix issue with characters in login screen, maybe. --- .../PathResolving/CollectionResolver.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 313c4f8b..1705f871 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using OtterGui; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -62,10 +63,11 @@ public sealed unsafe class CollectionResolver( try { - if (useCache && cache.TryGetValue(gameObject, out var data)) + // Login screen reuses the same actors and can not be cached. + if (LoginScreen(gameObject, out var data)) return data; - if (LoginScreen(gameObject, out data)) + if (useCache && cache.TryGetValue(gameObject, out data)) return data; if (Aesthetician(gameObject, out data)) @@ -116,16 +118,17 @@ public sealed unsafe class CollectionResolver( return true; } - var notYetReady = false; - var lobby = AgentLobby.Instance(); - if (lobby != null) + var notYetReady = false; + var lobby = AgentLobby.Instance(); + var characterList = CharaSelectCharacterList.Instance(); + if (lobby != null && characterList != null) { - var span = lobby->LobbyData.CharaSelectEntries.AsSpan(); // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; - if (idx >= 0 && idx < span.Length && span[idx].Value != null) + if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { - var item = span[idx].Value; + var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); Penumbra.Log.Verbose( $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); @@ -141,7 +144,7 @@ public sealed unsafe class CollectionResolver( var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ret = collection.ToResolveData(gameObject); return true; } From c4f6038d1ef629515e830e316ef11dc4871868da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 20:40:10 +0100 Subject: [PATCH 2026/2451] Make temporary collection always respect ownership. --- .../PathResolving/CollectionResolver.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1705f871..36c31af3 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -126,7 +126,7 @@ public sealed unsafe class CollectionResolver( // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) - && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); @@ -199,10 +199,24 @@ public sealed unsafe class CollectionResolver( /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => tempCollections.Collections.TryGetCollection(identifier, out var collection) - || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) - ? collection - : null; + { + if (tempCollections.Collections.TryGetCollection(identifier, out var collection)) + return collection; + + // Always inherit ownership for temporary collections. + if (identifier.Type is IdentifierType.Owned) + { + var playerIdentifier = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); + if (tempCollections.Collections.TryGetCollection(playerIdentifier, out collection)) + return collection; + } + + if (collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)) + return collection; + + return null; + } /// Check for the Yourself collection. private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor) From ed717c69f9676a3314e0235829539ab3a819a5be Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 20:40:10 +0100 Subject: [PATCH 2027/2451] Make temporary collection always respect ownership. --- .../Manager/IndividualCollections.Access.cs | 6 ++--- .../PathResolving/CollectionResolver.cs | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 6b90a333..d0a70630 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -48,8 +48,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa // Handle generic NPC var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, - ushort.MaxValue, - identifier.Kind, identifier.DataId); + ushort.MaxValue, identifier.Kind, identifier.DataId); if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) return true; @@ -58,8 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return false; identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, - identifier.HomeWorld.Id, - ObjectKind.None, uint.MaxValue); + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); } case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1705f871..36c31af3 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -126,7 +126,7 @@ public sealed unsafe class CollectionResolver( // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) - && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); @@ -199,10 +199,24 @@ public sealed unsafe class CollectionResolver( /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => tempCollections.Collections.TryGetCollection(identifier, out var collection) - || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) - ? collection - : null; + { + if (tempCollections.Collections.TryGetCollection(identifier, out var collection)) + return collection; + + // Always inherit ownership for temporary collections. + if (identifier.Type is IdentifierType.Owned) + { + var playerIdentifier = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); + if (tempCollections.Collections.TryGetCollection(playerIdentifier, out collection)) + return collection; + } + + if (collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)) + return collection; + + return null; + } /// Check for the Yourself collection. private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor) From 7dfc564a4cbafd26633aeff3cf8cc37d4a2e2e61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 4 Nov 2024 13:55:45 +0100 Subject: [PATCH 2028/2451] Add path resolving / est handling for kdb and bnmb files. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index d55caf34..54066782 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -36,7 +36,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveMdlPathHook; private readonly Hook _resolveMtrlPathHook; private readonly Hook _resolvePapPathHook; + private readonly Hook _resolveKdbPathHook; private readonly Hook _resolvePhybPathHook; + private readonly Hook _resolveBnmbPathHook; private readonly Hook _resolveSklbPathHook; private readonly Hook _resolveSkpPathHook; private readonly Hook _resolveTmbPathHook; @@ -54,11 +56,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); - + _resolveKdbPathHook = Create($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); - + _resolveBnmbPathHook = Create($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); - _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); @@ -83,7 +84,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Enable(); _resolveMtrlPathHook.Enable(); _resolvePapPathHook.Enable(); + _resolveKdbPathHook.Enable(); _resolvePhybPathHook.Enable(); + _resolveBnmbPathHook.Enable(); _resolveSklbPathHook.Enable(); _resolveSkpPathHook.Enable(); _resolveTmbPathHook.Enable(); @@ -101,7 +104,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Disable(); _resolveMtrlPathHook.Disable(); _resolvePapPathHook.Disable(); + _resolveKdbPathHook.Disable(); _resolvePhybPathHook.Disable(); + _resolveBnmbPathHook.Disable(); _resolveSklbPathHook.Disable(); _resolveSkpPathHook.Disable(); _resolveTmbPathHook.Disable(); @@ -119,7 +124,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Dispose(); _resolveMtrlPathHook.Dispose(); _resolvePapPathHook.Dispose(); + _resolveKdbPathHook.Dispose(); _resolvePhybPathHook.Dispose(); + _resolveBnmbPathHook.Dispose(); _resolveSklbPathHook.Dispose(); _resolveSkpPathHook.Dispose(); _resolveTmbPathHook.Dispose(); @@ -149,9 +156,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + private nint ResolveKdb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolvePhyb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolveBnmb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolveSklb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); @@ -188,6 +201,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + private nint ResolveKdbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); @@ -197,6 +219,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + private nint ResolveBnmbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); From c54141be5489753ce6fa4ad862478615ab25fbae Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 4 Nov 2024 12:59:01 +0000 Subject: [PATCH 2029/2451] [CI] Updating repo.json for testing_1.3.0.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cba274c8..686549c9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.0", + "TestingAssemblyVersion": "1.3.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From e3a1ae693813eb06d96d311958aabb5b3abfef55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 00:50:14 +0100 Subject: [PATCH 2030/2451] Current state. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Animation/ApricotListenerSoundPlay.cs | 14 ++++--- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Interop/Hooks/Meta/CalculateHeight.cs | 17 ++++---- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- .../PostProcessing/ShaderReplacementFixer.cs | 1 - .../PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Interop/Services/FontReloader.cs | 2 +- Penumbra/Interop/Services/RedrawService.cs | 4 +- Penumbra/Interop/VolatileOffsets.cs | 35 ++++++++++++++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 2 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- .../OptionEditor/MultiModGroupEditor.cs | 2 +- .../OptionEditor/SingleModGroupEditor.cs | 2 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Services/MessageService.cs | 6 +-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 ++ Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 41 ++++++++++++++----- 20 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 Penumbra/Interop/VolatileOffsets.cs diff --git a/OtterGui b/OtterGui index 3e6b0857..8ba88eff 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97 +Subproject commit 8ba88eff15326bb28ed5e6157f5252c114d40b5f diff --git a/Penumbra.GameData b/Penumbra.GameData index e39a04c8..fb81a0b5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e39a04c83b67246580492677414888357b5ebed8 +Subproject commit fb81a0b55d3c68f2b26357fac3049c79fb0c22fb diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 44eb7ebb..8838971c 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -33,25 +33,27 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook> 13) & 1) == 0) + var someIntermediate = *(nint*)(a1 + VolatileOffsets.ApricotListenerSoundPlayCaller.SomeIntermediate); + var flags = someIntermediate == nint.Zero + ? (ushort)0 + : *(ushort*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.Flags); + if (((flags >> VolatileOffsets.ApricotListenerSoundPlayCaller.BitShift) & 1) == 0) return Task.Result.Original(a1, unused, timeOffset); Penumbra.Log.Excessive( $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) - var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); + var apricotIInstanceListenner = *(nint*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.IInstanceListenner); if (apricotIInstanceListenner == nint.Zero) return Task.Result.Original(a1, unused, timeOffset); // In some cases we can obtain the associated caster via vfunc 1. var newData = ResolveData.Invalid; - var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[1](apricotIInstanceListenner); + var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[VolatileOffsets.ApricotListenerSoundPlayCaller.CasterVFunc](apricotIInstanceListenner); if (gameObject != null) { newData = _collectionResolver.IdentifyCollection(gameObject, true); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 7339c397..f19e4ce2 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -31,7 +31,7 @@ public sealed unsafe class SomePapLoad : FastHook private void Detour(nint a1, int a2, nint a3, int a4) { Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}."); - var timelinePtr = a1 + Offsets.TimeLinePtr; + var timelinePtr = a1 + VolatileOffsets.AnimationState.TimeLinePtr; if (timelinePtr != nint.Zero) { var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index e71d07dd..327e3d1e 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,7 +1,7 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Interop.PathResolving; -using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Hooks.Meta; @@ -13,19 +13,20 @@ public sealed unsafe class CalculateHeight : FastHook public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) { _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); + _metaState = metaState; + Task = hooks.CreateHook("Calculate Height", (nint)HeightContainer.MemberFunctionPointers.CalculateHeight, Detour, + !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(Character* character); + public delegate ulong Delegate(HeightContainer* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(Character* character) + private ulong Detour(HeightContainer* container) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(character); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(container); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index 72beea0e..9189ce3b 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -25,7 +25,7 @@ public sealed unsafe class UpdateModel : FastHook { // Shortcut because this is called all the time. // Same thing is checked at the beginning of the original function. - if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0) + if (*(int*)((nint)drawObject + VolatileOffsets.UpdateModel.ShortCircuit) == 0) return; Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 80892b0f..40958eb4 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -401,7 +401,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3, uint unk4, uint unk5) { - // If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0) { _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 36c31af3..50088008 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -240,7 +240,7 @@ public sealed unsafe class CollectionResolver( } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) + if (!IsModelHuman((uint)actor.AsCharacter->ModelCharaId)) return null; if (actor.Customize->Data[0] == 0) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 89e0c62b..62f4febe 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -68,7 +68,7 @@ public class ResourceTree Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => [], }; - ModelId = character->CharacterData.ModelCharaId; + ModelId = character->ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 4f48f08f..3a2c7022 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -43,7 +43,7 @@ public unsafe class FontReloader : IService return; _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[Offsets.ReloadFontsVfunc]; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[VolatileOffsets.FontReloader.ReloadFontsVFunc]; }); } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 2cdc1137..8f20ca5e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -38,10 +38,10 @@ public unsafe partial class RedrawService : IService // VFuncs that disable and enable draw, used only for GPose actors. private static void DisableDraw(IGameObject actor) - => ((delegate* unmanaged**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); + => ((delegate* unmanaged**)actor.Address)[0][VolatileOffsets.RedrawService.DisableDrawVFunc](actor.Address); private static void EnableDraw(IGameObject actor) - => ((delegate* unmanaged**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + => ((delegate* unmanaged**)actor.Address)[0][VolatileOffsets.RedrawService.EnableDrawVFunc](actor.Address); // Check whether we currently are in GPose. // Also clear the name list. diff --git a/Penumbra/Interop/VolatileOffsets.cs b/Penumbra/Interop/VolatileOffsets.cs new file mode 100644 index 00000000..2c6e3180 --- /dev/null +++ b/Penumbra/Interop/VolatileOffsets.cs @@ -0,0 +1,35 @@ +namespace Penumbra.Interop; + +public static class VolatileOffsets +{ + public static class ApricotListenerSoundPlayCaller + { + public const int PlayTimeOffset = 0x254; + public const int SomeIntermediate = 0x1F8; + public const int Flags = 0x4A4; + public const int IInstanceListenner = 0x270; + public const int BitShift = 13; + public const int CasterVFunc = 1; + } + + public static class AnimationState + { + public const int TimeLinePtr = 0x50; + } + + public static class UpdateModel + { + public const int ShortCircuit = 0xA2C; + } + + public static class FontReloader + { + public const int ReloadFontsVFunc = 43; + } + + public static class RedrawService + { + public const int EnableDrawVFunc = 12; + public const int DisableDrawVFunc = 13; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index dc94c881..f8760625 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -138,7 +138,7 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 7f18852d..d01297db 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -90,7 +90,7 @@ public class ModGroupEditor( { var mod = group.Mod; var idxFrom = group.GetIndex(); - if (!mod.Groups.Move(idxFrom, groupIdxTo)) + if (!mod.Groups.Move(ref idxFrom, ref groupIdxTo)) return; saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); diff --git a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index 74362325..2446ae80 100644 --- a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -75,7 +75,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 15a899a0..5fd785cf 100644 --- a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -48,7 +48,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b6b19ef2..41d8f668 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Log; using OtterGui.Services; @@ -20,6 +19,7 @@ using OtterGui.Tasks; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Dalamud.Plugin.Services; +using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; @@ -111,7 +111,7 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { _services.GetService(); - var itemSheet = _services.GetService().GetExcelSheet()!; + var itemSheet = _services.GetService().GetExcelSheet(); _communicatorService.ChangedItemHover.Subscribe(it => { if (it is IdentifiedItem { Item.Id.IsItem: true }) diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index a35a67f1..e610cb6a 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -4,7 +4,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; using Penumbra.Mods.Manager; @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { - public void LinkItem(Item item) + public void LinkItem(in Item item) { // @formatter:off var payloadList = new List @@ -29,7 +29,7 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti new TextPayload($"{(char)SeIconChar.LinkMarker}"), new UIForegroundPayload(0), new UIGlowPayload(0), - new TextPayload(item.Name), + new TextPayload(item.Name.ExtractText()), new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]), new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]), }; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 7c6cd01e..47c2c16c 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -54,6 +54,9 @@ public class Diagnostics(ServiceManager provider) : IUiService return; using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg); + if (!table) + return; + foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs index 7af1f884..e8ff9b9c 100644 --- a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -34,28 +34,49 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : IUiSe Penumbra.Log.Error($"Could not delete hook override file at {path}:\n{ex}"); } + bool? allVisible = null; + ImGui.SameLine(); + if (ImUtf8.Button("Disable All Visible Hooks"u8)) + allVisible = true; + ImGui.SameLine(); + if (ImUtf8.Button("Enable All VisibleHooks"u8)) + allVisible = false; + bool? all = null; ImGui.SameLine(); - if (ImUtf8.Button("Disable All Hooks"u8)) + if (ImUtf8.Button("Disable All Hooks")) all = true; ImGui.SameLine(); - if (ImUtf8.Button("Enable All Hooks"u8)) + if (ImUtf8.Button("Enable All Hooks")) all = false; foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true })) { using var tree = ImUtf8.TreeNode(propertyField.Name); if (!tree) - continue; - - var property = propertyField.GetValue(_overrides); - foreach (var valueField in propertyField.FieldType.GetFields()) { - var value = valueField.GetValue(property) as bool? ?? false; - if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || all.HasValue) + if (all.HasValue) { - valueField.SetValue(property, all ?? value); - propertyField.SetValue(_overrides, property); + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + valueField.SetValue(property, all.Value); + propertyField.SetValue(_overrides, property); + } + } + } + else + { + allVisible ??= all; + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + var value = valueField.GetValue(property) as bool? ?? false; + if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || allVisible.HasValue) + { + valueField.SetValue(property, allVisible ?? value); + propertyField.SetValue(_overrides, property); + } } } } From 5599f12753624981a79609bbc17a283e24bd0dc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 14:28:33 +0100 Subject: [PATCH 2031/2451] Further fixes. --- Penumbra/Interop/Hooks/Meta/CalculateHeight.cs | 12 ++++++------ Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 327e3d1e..0e85b3ae 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,19 +14,19 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)HeightContainer.MemberFunctionPointers.CalculateHeight, Detour, + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(HeightContainer* character); + public delegate ulong Delegate(Character* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(HeightContainer* container) + private ulong Detour(Character* character) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(container); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(character); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 50088008..576b61bb 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -240,7 +240,7 @@ public sealed unsafe class CollectionResolver( } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->ModelCharaId)) + if (!IsModelHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) return null; if (actor.Customize->Data[0] == 0) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 62f4febe..b50fc695 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -68,7 +68,7 @@ public class ResourceTree Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => [], }; - ModelId = character->ModelCharaId; + ModelId = character->ModelContainer.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; From 83e5feb7dbbe452d8499562733dc9ba0edcee1bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 14:30:47 +0100 Subject: [PATCH 2032/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 8ba88eff..95b8d177 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8ba88eff15326bb28ed5e6157f5252c114d40b5f +Subproject commit 95b8d177883b03f804d77434f45e9de97fdb9adf From a864ac196550776a4870b7fc646591a4f1d55632 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 22:00:07 +0100 Subject: [PATCH 2033/2451] 1.3.0.2 --- .github/workflows/test_release.yml | 2 +- Penumbra/Penumbra.json | 2 +- repo.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 0718ded2..549c967a 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 805f4d85..4790da18 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 10, + "DalamudApiLevel": 11, "LoadPriority": 69420, "LoadState": 2, "LoadSync": true, diff --git a/repo.json b/repo.json index 686549c9..71d8fad4 100644 --- a/repo.json +++ b/repo.json @@ -10,7 +10,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, - "TestingDalamudApiLevel": 10, + "TestingDalamudApiLevel": 11, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 597380355a4a769de852a0e4e03656163f34fa56 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Nov 2024 21:02:11 +0000 Subject: [PATCH 2034/2451] [CI] Updating repo.json for testing_1.3.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 71d8fad4..651802a6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.1", + "TestingAssemblyVersion": "1.3.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 7ab5299f7ae77c93e5f318529d1071ec3dfd4931 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 00:28:27 +0100 Subject: [PATCH 2035/2451] Add SCD handling and crc cache visualization. --- Penumbra.sln | 3 + .../Hooks/ResourceLoading/ResourceLoader.cs | 8 +-- .../Hooks/ResourceLoading/TexMdlService.cs | 62 ++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 29 ++++++++- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 46609f85..94a04ef3 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,7 +8,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\release.yml = .github\workflows\release.yml repo.json = repo.json + .github\workflows\test_release.yml = .github\workflows\test_release.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index bcd09b37..442bac15 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -15,18 +15,18 @@ public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlService _texMdlService; + private readonly TexMdlScdService _texMdlScdService; private readonly PapHandler _papHandler; private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlScdService texMdlScdService, Configuration config) { _resources = resources; _fileReadService = fileReadService; - _texMdlService = texMdlService; + _texMdlScdService = texMdlScdService; _config = config; ResetResolvePath(); @@ -140,7 +140,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - _texMdlService.AddCrc(type, resolvedPath); + _texMdlScdService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index d4a2dfba..9c17e0cf 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -10,7 +10,7 @@ using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.Re namespace Penumbra.Interop.Hooks.ResourceLoading; -public unsafe class TexMdlService : IDisposable, IRequiredService +public unsafe class TexMdlScdService : IDisposable, IRequiredService { /// /// We need to be able to obtain the requested LoD level. @@ -42,7 +42,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly LodService _lodService; - public TexMdlService(IGameInteropProvider interop) + public TexMdlScdService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); @@ -52,6 +52,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService _loadMdlFileExternHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); + _soundOnLoadHook.Enable(); } /// Add CRC64 if the given file is a model or texture file and has an associated path. @@ -59,8 +60,9 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { _ = type switch { - ResourceType.Mdl when path.HasValue => _customMdlCrc.Add(path.Value.Crc64), - ResourceType.Tex when path.HasValue => _customTexCrc.Add(path.Value.Crc64), + ResourceType.Mdl when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Mdl), + ResourceType.Tex when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Tex), + ResourceType.Scd when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Scd), _ => false, }; } @@ -70,15 +72,16 @@ public unsafe class TexMdlService : IDisposable, IRequiredService _checkFileStateHook.Dispose(); _loadMdlFileExternHook.Dispose(); _textureOnLoadHook.Dispose(); + _soundOnLoadHook.Dispose(); } /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// - private readonly HashSet _customMdlCrc = []; - - private readonly HashSet _customTexCrc = []; + private readonly Dictionary _customFileCrc = []; + public IReadOnlyDictionary CustomCache + => _customFileCrc; private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); @@ -86,12 +89,34 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly Hook _checkFileStateHook = null!; private readonly ThreadLocal _texReturnData = new(() => default); + private readonly ThreadLocal _scdReturnData = new(() => default); private delegate void UpdateCategoryDelegate(TextureResourceHandle* resourceHandle); [Signature(Sigs.TexHandleUpdateCategory)] private readonly UpdateCategoryDelegate _updateCategory = null!; + private delegate byte SoundOnLoadDelegate(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk); + + [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 8B 79 ?? 48 8B DA 8B D7")] + private readonly delegate* unmanaged _loadScdFileLocal = null!; + + [Signature("40 56 57 41 54 48 81 EC 90 00 00 00 80 3A 0B 45 0F B6 E0 48 8B F2", DetourName = nameof(OnScdLoadDetour))] + private readonly Hook _soundOnLoadHook = null!; + + private byte OnScdLoadDetour(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk) + { + var ret = _soundOnLoadHook.Original(handle, descriptor, unk); + if (!_scdReturnData.Value) + return ret; + + // Function failed on a replaced scd, call local. + _scdReturnData.Value = false; + ret = _loadScdFileLocal(handle, descriptor, unk); + _updateCategory((TextureResourceHandle*)handle); + return ret; + } + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. @@ -100,14 +125,17 @@ public unsafe class TexMdlService : IDisposable, IRequiredService /// private nint CheckFileStateDetour(nint ptr, ulong crc64) { - if (_customMdlCrc.Contains(crc64)) - return CustomFileFlag; - - if (_customTexCrc.Contains(crc64)) - { - _texReturnData.Value = true; - return nint.Zero; - } + if (_customFileCrc.TryGetValue(crc64, out var type)) + switch (type) + { + case ResourceType.Mdl: return CustomFileFlag; + case ResourceType.Tex: + _texReturnData.Value = true; + return nint.Zero; + case ResourceType.Scd: + _scdReturnData.Value = true; + return nint.Zero; + } var ret = _checkFileStateHook.Original(ptr, crc64); Penumbra.Log.Excessive($"[CheckFileState] Called on 0x{ptr:X} with CRC {crc64:X16}, returned 0x{ret:X}."); @@ -128,10 +156,10 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnLoadDetour))] + [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnTexLoadDetour))] private readonly Hook _textureOnLoadHook = null!; - private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + private byte OnTexLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) { var ret = _textureOnLoadHook.Original(handle, descriptor, unk2); if (!_texReturnData.Value) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 47c2c16c..9184ffe8 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -102,6 +102,7 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; + private readonly TexMdlScdService _texMdlScdService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -112,7 +113,7 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides) + HookOverrideDrawer hookOverrides, TexMdlScdService texMdlScdService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -150,6 +151,7 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; + _texMdlScdService = texMdlScdService; _objects = objects; _clientState = clientState; } @@ -183,6 +185,7 @@ public class DebugTab : Window, ITab, IUiService DrawDebugCharacterUtility(); DrawShaderReplacementFixer(); DrawData(); + DrawCrcCache(); DrawResourceProblems(); _hookOverrides.Draw(); DrawPlayerModelInfo(); @@ -1021,6 +1024,30 @@ public class DebugTab : Window, ITab, IUiService DrawDebugResidentResources(); } + private unsafe void DrawCrcCache() + { + var header = ImUtf8.CollapsingHeader("CRC Cache"u8); + if (!header) + return; + + using var table = ImUtf8.Table("table"u8, 2); + if (!table) + return; + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0')); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); + ImGui.TableHeadersRow(); + + foreach (var (hash, type) in _texMdlScdService.CustomCache) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"{hash:X16}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{type}"); + } + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From 41718d8f8fd9106136691f14588fab33b3073269 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 00:28:36 +0100 Subject: [PATCH 2036/2451] Fix screen actor indices. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fb81a0b5..79d8d782 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fb81a0b55d3c68f2b26357fac3049c79fb0c22fb +Subproject commit 79d8d782b3b454a41f7f87f398806ec4d08d485f From 0928d712c91e7ea893c8088d51af63221be486fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Nov 2024 23:30:41 +0000 Subject: [PATCH 2037/2451] [CI] Updating repo.json for testing_1.3.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 651802a6..77f5012e 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.2", + "TestingAssemblyVersion": "1.3.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8a53313c33d0a42edfbbdf09a141525f7db14794 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 20:14:06 +0100 Subject: [PATCH 2038/2451] Add .atch file debugging. --- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 56 ++++++++++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 29 +++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/AtchDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 79d8d782..1c82c086 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 79d8d782b3b454a41f7f87f398806ec4d08d485f +Subproject commit 1c82c086704e2f1b3608644a9b1d70628fbe0ca9 diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs new file mode 100644 index 00000000..f6f6c50e --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -0,0 +1,56 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Files; + +namespace Penumbra.UI.Tabs.Debug; + +public static class AtchDrawer +{ + public static void Draw(AtchFile file) + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Entries: "u8); + ImUtf8.Text("States: "u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{file.Entries.Count}"); + if (file.Entries.Count == 0) + { + ImUtf8.Text("0"u8); + return; + } + + ImUtf8.Text($"{file.Entries[0].States.Count}"); + } + + foreach (var (entry, index) in file.Entries.WithIndex()) + { + using var id = ImUtf8.PushId(index); + using var tree = ImUtf8.TreeNode(entry.Name.Span); + if (tree) + { + ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + foreach (var (state, i) in entry.States.WithIndex()) + { + id.Push(i); + using var t = ImUtf8.TreeNode(state.Bone.Span); + if (t) + { + ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"Offset: {state.Offset.X} | {state.Offset.Y} | {state.Offset.Z}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"Rotation: {state.Rotation.X} | {state.Rotation.Y} | {state.Rotation.Z}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + id.Pop(); + } + } + } + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9184ffe8..cdaaadaa 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -98,6 +98,7 @@ public class DebugTab : Window, ITab, IUiService private readonly Diagnostics _diagnostics; private readonly ObjectManager _objects; private readonly IClientState _clientState; + private readonly IDataManager _dataManager; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; @@ -105,7 +106,7 @@ public class DebugTab : Window, ITab, IUiService private readonly TexMdlScdService _texMdlScdService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, - IClientState clientState, + IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, CollectionResolver collectionResolver, @@ -154,6 +155,7 @@ public class DebugTab : Window, ITab, IUiService _texMdlScdService = texMdlScdService; _objects = objects; _clientState = clientState; + _dataManager = dataManager; } public ReadOnlySpan Label @@ -665,11 +667,36 @@ public class DebugTab : Window, ITab, IUiService DrawEmotes(); DrawStainTemplates(); + DrawAtch(); } private string _emoteSearchFile = string.Empty; private string _emoteSearchName = string.Empty; + + private AtchFile? _atchFile; + + private void DrawAtch() + { + try + { + _atchFile ??= new AtchFile(_dataManager.GetFile("chara/xls/attachOffset/c0101.atch")!.Data); + } + catch + { + // ignored + } + + if (_atchFile == null) + return; + + using var mainTree = ImUtf8.TreeNode("Atch File C0101"u8); + if (!mainTree) + return; + + AtchDrawer.Draw(_atchFile); + } + private void DrawEmotes() { using var mainTree = TreeNode("Emotes"); From 3beef61c6f04e6bc40ffb40be998d8cf14546cee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Nov 2024 10:44:09 +0100 Subject: [PATCH 2039/2451] Some debug vis improvements, disable .atch file modding for the moment until modular .atch file modding is implemented. --- .../Interop/PathResolving/PathResolver.cs | 4 ++ Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 51 +++++++++++++------ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 63bbc8d8..a7af42e3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,6 +52,10 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); + // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. + if (resourceType is ResourceType.Atch) + return (null, ResolveData.Invalid); + return category switch { // Only Interface collection. diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index f6f6c50e..3e407e99 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -31,7 +31,7 @@ public static class AtchDrawer foreach (var (entry, index) in file.Entries.WithIndex()) { using var id = ImUtf8.PushId(index); - using var tree = ImUtf8.TreeNode(entry.Name.Span); + using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Name.Span}"); if (tree) { ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index cdaaadaa..28911b05 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,7 @@ using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -196,7 +197,7 @@ public class DebugTab : Window, ITab, IUiService } - private void DrawCollectionCaches() + private unsafe void DrawCollectionCaches() { if (!ImGui.CollapsingHeader( $"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) @@ -207,25 +208,35 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})"); + using var node = TreeNode($"{collection.Name} (Change Counter {collection.ChangeCounter})###{collection.Name}"); if (!node) continue; color.Pop(); - foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) + using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8)) { - using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); - using var node2 = TreeNode(mod.Name.Text); - if (!node2) - continue; - - foreach (var path in paths) - - TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - - foreach (var manip in manips) - TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + if (resourceNode) + foreach (var (path, resource) in collection._cache!.CustomResources) + ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } + + using var modNode = ImUtf8.TreeNode("Enabled Mods"u8); + if (modNode) + foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) + { + using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); + using var node2 = TreeNode(mod.Name.Text); + if (!node2) + continue; + + foreach (var path in paths) + + TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + + foreach (var manip in manips) + TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } } else { @@ -1051,17 +1062,27 @@ public class DebugTab : Window, ITab, IUiService DrawDebugResidentResources(); } + private string _crcInput = string.Empty; + private FullPath _crcPath = FullPath.Empty; + private unsafe void DrawCrcCache() { var header = ImUtf8.CollapsingHeader("CRC Cache"u8); if (!header) return; + if (ImUtf8.InputText("##crcInput"u8, ref _crcInput, "Input path for CRC..."u8)) + _crcPath = new FullPath(_crcInput); + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.Text($" CRC32: {_crcPath.InternalName.CiCrc32:X8}"); + ImUtf8.Text($"CI CRC32: {_crcPath.InternalName.Crc32:X8}"); + ImUtf8.Text($" CRC64: {_crcPath.Crc64:X16}"); + using var table = ImUtf8.Table("table"u8, 2); if (!table) return; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0')); ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); ImGui.TableHeadersRow(); From 688b84141f3ea1964f7f589503d3af516b1531d4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 20 Nov 2024 09:46:01 +0000 Subject: [PATCH 2040/2451] [CI] Updating repo.json for testing_1.3.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 77f5012e..02437b14 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.3", + "TestingAssemblyVersion": "1.3.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ce75471e5129068ba765cbff8cdad5626866c31d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Nov 2024 18:08:24 +0100 Subject: [PATCH 2041/2451] Fix issue with resetting GEQP parameters on reload (again?) --- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 11 +++++++++++ Penumbra/Mods/Editor/ModMetaEditor.cs | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1c82c086..c855c17c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1c82c086704e2f1b3608644a9b1d70628fbe0ca9 +Subproject commit c855c17cffd7d270c3f013e01767cd052c24c462 diff --git a/Penumbra.String b/Penumbra.String index bd52d080..dd83f972 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779 +Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936 diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 70d4fd47..da061bec 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -79,6 +79,17 @@ public class MetaDictionary _globalEqp.Clear(); } + public void ClearForDefault() + { + Count = _globalEqp.Count; + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _est.Clear(); + _rsp.Clear(); + _gmp.Clear(); + } + public bool Equals(MetaDictionary other) => Count == other.Count && _imc.SetEquals(other._imc) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 64c585ea..217ba93d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -69,7 +69,8 @@ public class ModMetaEditor( public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { var clone = dict.Clone(); - dict.Clear(); + dict.ClearForDefault(); + var count = 0; foreach (var (key, value) in clone.Imc) { From f2bdaf1b490204de39f668707b3f6d319f5e0eb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 00:35:17 +0100 Subject: [PATCH 2042/2451] Circumvent rsf not existing. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../Hooks/ResourceLoading/TexMdlService.cs | 27 ++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c855c17c..07d18f7f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c855c17cffd7d270c3f013e01767cd052c24c462 +Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index a1dd374f..3deeb107 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -96,6 +96,7 @@ public class HookOverrides public bool CheckFileState; public bool TexResourceHandleOnLoad; public bool LoadMdlFileExtern; + public bool SoundOnLoad; } public struct ResourceHooks diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 9c17e0cf..b43f1ed5 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -7,6 +7,7 @@ using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Classes; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; +using TextureResourceHandle = Penumbra.Interop.Structs.TextureResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -52,7 +53,8 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService _loadMdlFileExternHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); - _soundOnLoadHook.Enable(); + if (!HookOverrides.Instance.ResourceLoading.SoundOnLoad) + _soundOnLoadHook.Enable(); } /// Add CRC64 if the given file is a model or texture file and has an associated path. @@ -80,6 +82,7 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// private readonly Dictionary _customFileCrc = []; + public IReadOnlyDictionary CustomCache => _customFileCrc; @@ -98,15 +101,31 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService private delegate byte SoundOnLoadDelegate(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk); - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 8B 79 ?? 48 8B DA 8B D7")] + [Signature(Sigs.LoadScdFileLocal)] private readonly delegate* unmanaged _loadScdFileLocal = null!; - [Signature("40 56 57 41 54 48 81 EC 90 00 00 00 80 3A 0B 45 0F B6 E0 48 8B F2", DetourName = nameof(OnScdLoadDetour))] + [Signature(Sigs.SoundOnLoad, DetourName = nameof(OnScdLoadDetour))] private readonly Hook _soundOnLoadHook = null!; + [Signature(Sigs.RsfServiceAddress, ScanType = ScanType.StaticAddress)] + private readonly nint* _rsfService = null; + private byte OnScdLoadDetour(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk) { - var ret = _soundOnLoadHook.Original(handle, descriptor, unk); + byte ret; + if (*_rsfService == nint.Zero) + { + Penumbra.Log.Debug( + $"Resource load of {handle->FileName} before FFXIV RSF-service was instantiated, workaround by setting pointer."); + *_rsfService = 1; + ret = _soundOnLoadHook.Original(handle, descriptor, unk); + *_rsfService = nint.Zero; + } + else + { + ret = _soundOnLoadHook.Original(handle, descriptor, unk); + } + if (!_scdReturnData.Value) return ret; From ee48ea0166171e5437a5d5731a069aeb6aabc99f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 00:43:36 +0100 Subject: [PATCH 2043/2451] Some stashed changes already applied. --- Penumbra/Collections/ModCollection.cs | 1 + .../Hooks/ResourceLoading/ResourceLoader.cs | 8 ++++---- .../{TexMdlService.cs => RsfService.cs} | 4 ++-- .../Interop/PathResolving/PathDataHandler.cs | 5 +++++ .../Interop/Processing/ImcFilePostProcessor.cs | 3 ++- Penumbra/UI/ResourceWatcher/Record.cs | 14 +++++++++++--- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcherTable.cs | 18 +++++++++++++++++- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 15 ++++++++------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 ++++---- 10 files changed, 55 insertions(+), 23 deletions(-) rename Penumbra/Interop/Hooks/ResourceLoading/{TexMdlService.cs => RsfService.cs} (96%) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index eb5ab46a..db9c19cb 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -57,6 +57,7 @@ public partial class ModCollection public int ChangeCounter { get; private set; } public uint ImcChangeCounter { get; set; } + public uint AtchChangeCounter { get; set; } /// Increment the number of changes in the effective file list. public int IncrementCounter() diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 442bac15..47f96d98 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -15,18 +15,18 @@ public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlScdService _texMdlScdService; + private readonly RsfService _rsfService; private readonly PapHandler _papHandler; private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlScdService texMdlScdService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config) { _resources = resources; _fileReadService = fileReadService; - _texMdlScdService = texMdlScdService; + _rsfService = rsfService; _config = config; ResetResolvePath(); @@ -140,7 +140,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - _texMdlScdService.AddCrc(type, resolvedPath); + _rsfService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs similarity index 96% rename from Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs index b43f1ed5..7ac1563f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs @@ -11,7 +11,7 @@ using TextureResourceHandle = Penumbra.Interop.Structs.TextureResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; -public unsafe class TexMdlScdService : IDisposable, IRequiredService +public unsafe class RsfService : IDisposable, IRequiredService { /// /// We need to be able to obtain the requested LoD level. @@ -43,7 +43,7 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService private readonly LodService _lodService; - public TexMdlScdService(IGameInteropProvider interop) + public RsfService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 9410ff98..5439151f 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -44,6 +44,11 @@ public static class PathDataHandler public static FullPath CreateAvfx(CiByteString path, ModCollection collection) => CreateBase(path, collection); + /// Create the encoding path for an ATCH file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateAtch(CiByteString path, ModCollection collection) + => new($"|{collection.LocalId.Id}_{collection.AtchChangeCounter}_{DiscriminatorString}|{path}"); + /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 33a3941a..a3233cfb 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.JobGauge.Types; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -24,7 +25,7 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP return; file.Replace(resource); - Penumbra.Log.Information( + Penumbra.Log.Verbose( $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); } } diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index b69d9944..7338e5a9 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -3,6 +3,7 @@ using Penumbra.Collections; using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.UI.ResourceWatcher; @@ -24,14 +25,16 @@ internal unsafe struct Record public ModCollection? Collection; public ResourceHandle* Handle; public ResourceTypeFlag ResourceType; - public ResourceCategoryFlag Category; + public ulong Crc64; public uint RefCount; + public ResourceCategoryFlag Category; public RecordType RecordType; public OptionalBool Synchronously; public OptionalBool ReturnValue; public OptionalBool CustomLoad; public LoadState LoadState; + public static Record CreateRequest(CiByteString path, bool sync) => new() { @@ -49,6 +52,7 @@ internal unsafe struct Record CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, LoadState = LoadState.None, + Crc64 = 0, }; public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) @@ -70,15 +74,16 @@ internal unsafe struct Record CustomLoad = false, AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, + Crc64 = 0, }; } - public static Record CreateLoad(CiByteString path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, + public static Record CreateLoad(FullPath path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, string associatedGameObject) => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), + Path = path.InternalName.IsOwned ? path.InternalName : path.InternalName.Clone(), OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), Collection = collection, Handle = handle, @@ -91,6 +96,7 @@ internal unsafe struct Record CustomLoad = true, AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, + Crc64 = path.Crc64, }; public static Record CreateDestruction(ResourceHandle* handle) @@ -112,6 +118,7 @@ internal unsafe struct Record CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, LoadState = handle->LoadState, + Crc64 = 0, }; } @@ -132,5 +139,6 @@ internal unsafe struct Record CustomLoad = custom, AssociatedGameObject = string.Empty, LoadState = handle->LoadState, + Crc64 = 0, }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 6f1ce9cf..d432e97e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -250,7 +250,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService var record = manipulatedPath == null ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) - : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data)); + : Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data)); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 2bb71b87..88b7120d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -29,7 +29,8 @@ internal sealed class ResourceWatcherTable : Table new HandleColumn { Label = "Resource" }, new LoadStateColumn { Label = "State" }, new RefCountColumn { Label = "#Ref" }, - new DateColumn { Label = "Time" } + new DateColumn { Label = "Time" }, + new Crc64Column { Label = "Crc64" } ) { } @@ -144,6 +145,21 @@ internal sealed class ResourceWatcherTable : Table => ImGui.TextUnformatted($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}"); } + private sealed class Crc64Column : ColumnString + { + public override float Width + => UiBuilder.MonoFont.GetCharAdvance('0') * 17; + + public override unsafe string ToName(Record item) + => item.Crc64 != 0 ? $"{item.Crc64:X16}" : string.Empty; + + public override unsafe void DrawColumn(Record item, int _) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null); + ImUtf8.Text(ToName(item)); + } + } + private sealed class CollectionColumn : ColumnString { diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index 3e407e99..d9058083 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Text; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; namespace Penumbra.UI.Tabs.Debug; @@ -18,27 +19,27 @@ public static class AtchDrawer ImGui.SameLine(); using (ImUtf8.Group()) { - ImUtf8.Text($"{file.Entries.Count}"); - if (file.Entries.Count == 0) + ImUtf8.Text($"{file.Points.Count}"); + if (file.Points.Count == 0) { ImUtf8.Text("0"u8); return; } - ImUtf8.Text($"{file.Entries[0].States.Count}"); + ImUtf8.Text($"{file.Points[0].Entries.Length}"); } - foreach (var (entry, index) in file.Entries.WithIndex()) + foreach (var (entry, index) in file.Points.WithIndex()) { using var id = ImUtf8.PushId(index); - using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Name.Span}"); + using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Type.ToName()}"); if (tree) { ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - foreach (var (state, i) in entry.States.WithIndex()) + foreach (var (state, i) in entry.Entries.WithIndex()) { id.Push(i); - using var t = ImUtf8.TreeNode(state.Bone.Span); + using var t = ImUtf8.TreeNode(state.Bone); if (t) { ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 28911b05..fc735d04 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -104,7 +104,7 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; - private readonly TexMdlScdService _texMdlScdService; + private readonly RsfService _rsfService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -115,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, TexMdlScdService texMdlScdService) + HookOverrideDrawer hookOverrides, RsfService rsfService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -153,7 +153,7 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; - _texMdlScdService = texMdlScdService; + _rsfService = rsfService; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -1087,7 +1087,7 @@ public class DebugTab : Window, ITab, IUiService ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); ImGui.TableHeadersRow(); - foreach (var (hash, type) in _texMdlScdService.CustomCache) + foreach (var (hash, type) in _rsfService.CustomCache) { ImGui.TableNextColumn(); ImUtf8.Text($"{hash:X16}"); From 37332c432b4128008538058e2088ed305117f26c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 12:34:45 +0100 Subject: [PATCH 2044/2451] 1.3.1.0 --- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 41d8f668..2c70816e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", + "IllusioVitae", "Aetherment", "LoporritSync", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 48ac90d8..0b0ca81a 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -54,10 +54,28 @@ public class PenumbraChangelog : IUiService Add1_1_1_0(Changelog); Add1_2_1_0(Changelog); Add1_3_0_0(Changelog); + Add1_3_1_0(Changelog); } #region Changelogs + private static void Add1_3_1_0(Changelog log) + => log.NextVersion("Version 1.3.1.0") + .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") + .RegisterImportant("There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") + .RegisterEntry("If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", 1) + .RegisterImportant("The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") + .RegisterEntry("A better way for modular modding of .atch files via meta changes will release to the testing branch soonish.", 1) + .RegisterHighlight("Temporary collections (as created by Mare) will now always respect ownership.") + .RegisterEntry("This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", 1) + .RegisterEntry("The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") + .RegisterEntry("Fixed issues with EQP entries being labeled wrongly and global EQP not changing all required values for earrings.") + .RegisterEntry("Fixed an issue with global EQP changes of a mod being reset upon reloading the mod.") + .RegisterEntry("Fixed another issue with left rings and mare synchronization / the on-screen tab.") + .RegisterEntry("Maybe fixed some issues with characters appearing in the login screen being misidentified.") + .RegisterEntry("Some improvements for debug visualization have been made."); + + private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") From 977cb2196a138ed6cb6f4a14d939ea1272ff72b8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 13:29:57 +0000 Subject: [PATCH 2045/2451] [CI] Updating repo.json for 1.3.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 02437b14..e7ad4df3 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.4", + "AssemblyVersion": "1.3.1.0", + "TestingAssemblyVersion": "1.3.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 234130cf862ee821e9dd6e0eb9470d68adb38a06 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:44:00 +0100 Subject: [PATCH 2046/2451] Update repo.json --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index e7ad4df3..659f4c24 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.3.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 11, "TestingDalamudApiLevel": 11, "IsHide": "False", "IsTestingExclusive": "False", From 06ba0ba956b0d4d121e5949e9b7c69c56fe3fa0c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 17:31:53 +0100 Subject: [PATCH 2047/2451] Fix glasses issue with resource trees. --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b1cbb74d..e67bf913 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -53,7 +53,7 @@ internal partial record ResolveContext if (characterRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - var accessory = slotIndex >= 5; + var accessory = IsEquipmentSlot(slotIndex); if ((ushort)characterRaceCode % 10 != 1 && accessory) return GenderRace.MidlanderMale; From 22be9f2d0726f78dd335b96fd84e4000bb8972c6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 16:34:25 +0000 Subject: [PATCH 2048/2451] [CI] Updating repo.json for 1.3.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 659f4c24..f1734ed7 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.1.0", - "TestingAssemblyVersion": "1.3.1.0", + "AssemblyVersion": "1.3.1.1", + "TestingAssemblyVersion": "1.3.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 17d8826ae920b1509cef8b9612fdaf5cef93f17a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 19:12:53 +0100 Subject: [PATCH 2049/2451] This time correctly, maybe? --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index e67bf913..0c36b745 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -53,7 +53,7 @@ internal partial record ResolveContext if (characterRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - var accessory = IsEquipmentSlot(slotIndex); + var accessory = !IsEquipmentSlot(slotIndex); if ((ushort)characterRaceCode % 10 != 1 && accessory) return GenderRace.MidlanderMale; From 5a46361d4f478eeecc45cc82fdde609e7aa92e98 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 18:16:51 +0000 Subject: [PATCH 2050/2451] [CI] Updating repo.json for 1.3.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f1734ed7..a714fbe0 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.1.1", - "TestingAssemblyVersion": "1.3.1.1", + "AssemblyVersion": "1.3.1.2", + "TestingAssemblyVersion": "1.3.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 25aac1a03e2b197f32f493835393c809654fbaf2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 23 Nov 2024 13:57:54 +0100 Subject: [PATCH 2051/2451] Fix CalculateHeight. --- Penumbra/Interop/Hooks/Meta/CalculateHeight.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 0e85b3ae..3dac17bd 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,19 +14,19 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, + Task = hooks.CreateHook("Calculate Height", (nint)ModelContainer.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(Character* character); + public delegate float Delegate(ModelContainer* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(Character* character) + private float Detour(ModelContainer* container) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(character); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(container); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } From 9822ab4128dedc12d9c5cd08af502dce3f06bb53 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Nov 2024 16:59:07 +0100 Subject: [PATCH 2052/2451] Add some debug helper output for SeFileDescriptor. --- Penumbra/Interop/Structs/SeFileDescriptor.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs index 67730799..02ab4dc8 100644 --- a/Penumbra/Interop/Structs/SeFileDescriptor.cs +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -1,3 +1,6 @@ +using Dalamud.Memory; +using Penumbra.String.Functions; + namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] @@ -14,4 +17,18 @@ public unsafe struct SeFileDescriptor [FieldOffset(0x70)] public char Utf16FileName; + + public FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle* CsResourceHandele + => (FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle*)ResourceHandle; + + public string FileName + { + get + { + fixed (char* ptr = &Utf16FileName) + { + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr).ToString(); + } + } + } } From d0e0ae46e67fc7e5c5a7af663811521cb1080c5a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Nov 2024 17:46:14 +0100 Subject: [PATCH 2053/2451] Push an ID in itemselector. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 95b8d177..215e0172 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 95b8d177883b03f804d77434f45e9de97fdb9adf +Subproject commit 215e01722a319c70b271dd23a40d99edc3fc197e From d2a015f32ad859b703449fc025546a30bed7156f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 01:58:44 +0100 Subject: [PATCH 2054/2451] Ughhhhhhhhhh --- Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs | 1 - Penumbra/Penumbra.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs index 7ac1563f..e7f06f91 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs @@ -132,7 +132,6 @@ public unsafe class RsfService : IDisposable, IRequiredService // Function failed on a replaced scd, call local. _scdReturnData.Value = false; ret = _loadScdFileLocal(handle, descriptor, unk); - _updateCategory((TextureResourceHandle*)handle); return ret; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2c70816e..1bf8844c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From cc49bdcb3669452debf57879d9ec4a2c78cfbd94 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 26 Nov 2024 01:02:41 +0000 Subject: [PATCH 2055/2451] [CI] Updating repo.json for 1.3.1.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a714fbe0..f5a8e2f1 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.1.2", - "TestingAssemblyVersion": "1.3.1.2", + "AssemblyVersion": "1.3.1.3", + "TestingAssemblyVersion": "1.3.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 65538868c314557be65cf0e2b2e2231de1b15f45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 15:49:33 +0100 Subject: [PATCH 2056/2451] Add Artemis --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 1bf8844c..917dba6c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From b1be868a6a9eaa94fec3f307cd0223e0c6b38e3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 12:19:14 +0100 Subject: [PATCH 2057/2451] Atch stuff. --- Penumbra.GameData | 2 +- Penumbra/Api/Api/MetaApi.cs | 12 + Penumbra/Collections/Cache/AtchCache.cs | 122 +++++++++ Penumbra/Collections/Cache/CollectionCache.cs | 33 ++- Penumbra/Collections/Cache/MetaCache.cs | 10 +- Penumbra/Interop/Hooks/DebugHook.cs | 10 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Interop/Hooks/Meta/AtchCallerHook1.cs | 39 +++ .../Interop/Hooks/Meta/AtchCallerHook2.cs | 38 +++ Penumbra/Interop/PathResolving/MetaState.cs | 1 + .../Interop/PathResolving/PathResolver.cs | 9 +- .../Processing/AtchFilePostProcessor.cs | 43 +++ .../Processing/AtchPathPreProcessor.cs | 44 ++++ .../Processing/GamePathPreProcessService.cs | 3 +- Penumbra/Meta/AtchManager.cs | 26 ++ Penumbra/Meta/Manipulations/AtchIdentifier.cs | 77 ++++++ .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 72 ++++- Penumbra/Meta/MetaFileManager.cs | 5 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 1 + Penumbra/Penumbra.cs | 1 + .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 245 ++++++++++++++++++ .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 3 +- .../Meta/GlobalEqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 9 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 14 +- .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 7 +- .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + 32 files changed, 802 insertions(+), 43 deletions(-) create mode 100644 Penumbra/Collections/Cache/AtchCache.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs create mode 100644 Penumbra/Interop/Processing/AtchFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/AtchPathPreProcessor.cs create mode 100644 Penumbra/Meta/AtchManager.cs create mode 100644 Penumbra/Meta/Manipulations/AtchIdentifier.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 07d18f7f..2b0c7f3b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 +Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 6f3ed51e..217cb1e3 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -66,6 +67,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } return Functions.ToCompressedBase64(array, 0); @@ -97,6 +99,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Gmp); + WriteCache(zipStream, cache.Atch); cache.GlobalEqp.EnterReadLock(); try @@ -246,6 +249,15 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } + var atchCount = r.ReadInt32(); + for (var i = 0; i < atchCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + var globalEqpCount = r.ReadInt32(); for (var i = 0; i < globalEqpCount; ++i) { diff --git a/Penumbra/Collections/Cache/AtchCache.cs b/Penumbra/Collections/Cache/AtchCache.cs new file mode 100644 index 00000000..9e0f6caf --- /dev/null +++ b/Penumbra/Collections/Cache/AtchCache.cs @@ -0,0 +1,122 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + private readonly Dictionary)> _atchFiles = []; + + public bool HasFile(GenderRace gr) + => _atchFiles.ContainsKey(gr); + + public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file) + { + if (!_atchFiles.TryGetValue(gr, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; + } + + public void Reset() + { + foreach (var (_, (_, set)) in _atchFiles) + set.Clear(); + + _atchFiles.Clear(); + Clear(); + } + + protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry) + { + ++Collection.AtchChangeCounter; + ApplyFile(identifier, entry); + } + + private void ApplyFile(AtchIdentifier identifier, AtchEntry entry) + { + try + { + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + { + if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested."); + + pair = (baseFile.Clone(), []); + } + + + if (!Apply(pair.Item1, identifier, entry)) + return; + + pair.Item2.Add(identifier); + _atchFiles[identifier.GenderRace] = pair; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}"); + } + } + + protected override void RevertModInternal(AtchIdentifier identifier) + { + ++Collection.AtchChangeCounter; + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + return; + + if (!pair.Item2.Remove(identifier)) + return; + + if (pair.Item2.Count == 0) + { + _atchFiles.Remove(identifier.GenderRace); + return; + } + + var def = GetDefault(Manager, identifier); + if (def == null) + throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to."); + + Apply(pair.Item1, identifier, def.Value); + } + + public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier) + { + if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + return null; + + if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return null; + + if (point.Entries.Length <= identifier.EntryIndex) + return null; + + return point.Entries[identifier.EntryIndex]; + } + + public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry) + { + if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return false; + + if (point.Entries.Length <= identifier.EntryIndex) + return false; + + point.Entries[identifier.EntryIndex] = entry; + return true; + } + + protected override void Dispose(bool _) + { + Clear(); + _atchFiles.Clear(); + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index abc0dff8..64cf54ea 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -228,20 +228,25 @@ public sealed class CollectionCache : IDisposable foreach (var (path, file) in files.FileRedirections) AddFile(path, file, mod); - foreach (var (identifier, entry) in files.Manipulations.Eqp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Eqdp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Est) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Gmp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Rsp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Imc) - AddManipulation(mod, identifier, entry); - foreach (var identifier in files.Manipulations.GlobalEqp) - AddManipulation(mod, identifier, null!); + if (files.Manipulations.Count > 0) + { + foreach (var (identifier, entry) in files.Manipulations.Eqp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Eqdp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Est) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Gmp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Rsp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Imc) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Atch) + AddManipulation(mod, identifier, entry); + foreach (var identifier in files.Manipulations.GlobalEqp) + AddManipulation(mod, identifier, null!); + } if (addMetaChanges) { diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 05a94ac5..7d8586c3 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; @@ -14,11 +15,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly GmpCache Gmp = new(manager, collection); public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); + public readonly AtchCache Atch = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -27,6 +29,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -37,6 +40,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Reset(); Rsp.Reset(); Imc.Reset(); + Atch.Reset(); GlobalEqp.Clear(); } @@ -52,6 +56,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Dispose(); Rsp.Dispose(); Imc.Dispose(); + Atch.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -65,6 +70,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod), ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), + AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -85,6 +91,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i => Gmp.RevertMod(i, out mod), ImcIdentifier i => Imc.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod), + AtchIdentifier i => Atch.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -100,6 +107,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e), ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), + AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs index db14805c..fe9754f9 100644 --- a/Penumbra/Interop/Hooks/DebugHook.cs +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using OtterGui.Services; +using Penumbra.Interop.Structs; namespace Penumbra.Interop.Hooks; @@ -31,12 +32,13 @@ public sealed unsafe class DebugHook : IHookService public bool Finished => _task?.IsCompletedSuccessfully ?? true; - private delegate void Delegate(nint a, int b, nint c, float* d); + private delegate nint Delegate(ResourceHandle* a, int b, int c); - private void Detour(nint a, int b, nint c, float* d) + private nint Detour(ResourceHandle* a, int b, int c) { - _task!.Result.Original(a, b, c, d); - Penumbra.Log.Information($"[Debug Hook] Results with 0x{a:X} {b} {c:X} {d[0]} {d[1]} {d[2]} {d[3]}."); + var ret = _task!.Result.Original(a, b, c); + Penumbra.Log.Information($"[Debug Hook] Results with 0x{(nint)a:X}, {b}, {c} -> 0x{ret:X}."); + return ret; } } #endif diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 3deeb107..63d93c9d 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -62,6 +62,8 @@ public class HookOverrides public bool SetupVisor; public bool UpdateModel; public bool UpdateRender; + public bool AtchCaller1; + public bool AtchCaller2; } public struct ObjectHooks diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs new file mode 100644 index 00000000..748ca93a --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -0,0 +1,39 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook1 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook1(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller1", Sigs.AtchCaller1, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller1); + if (!HookOverrides.Instance.Meta.AtchCaller1) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs new file mode 100644 index 00000000..9b3349f2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -0,0 +1,38 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook2 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook2(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller2", Sigs.AtchCaller2, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller2); + if (!HookOverrides.Instance.Meta.AtchCaller2) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel, unk2); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index e709c210..e7fc3176 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable, IService public readonly Stack EqdpCollection = []; public readonly Stack EstCollection = []; public readonly Stack RspCollection = []; + public readonly Stack AtchCollection = []; public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index a7af42e3..0b6c8340 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Linq; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -54,7 +55,7 @@ public class PathResolver : IDisposable, IService // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. if (resourceType is ResourceType.Atch) - return (null, ResolveData.Invalid); + return ResolveAtch(path); return category switch { @@ -142,4 +143,10 @@ public class PathResolver : IDisposable, IService private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), _collectionManager.Active.Interface.ToResolveData()); + + public (FullPath?, ResolveData) ResolveAtch(Utf8GamePath gamePath) + { + _metaState.AtchCollection.TryPeek(out var resolveData); + return _preprocessor.PreProcess(resolveData, gamePath.Path, false, ResourceType.Atch, null, gamePath); + } } diff --git a/Penumbra/Interop/Processing/AtchFilePostProcessor.cs b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs new file mode 100644 index 00000000..e4fab022 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs @@ -0,0 +1,43 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchFilePostProcessor(CollectionStorage collections, XivFileAllocator allocator) + : IFilePostProcessor +{ + private readonly IFileAllocator _allocator = allocator; + + public ResourceType Type + => ResourceType.Atch; + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!AtchPathPreProcessor.TryGetAtchGenderRace(originalGamePath, out var gr)) + return; + + if (!collection.MetaCache.Atch.GetFile(gr, out var file)) + return; + + using var bytes = file.Write(); + var length = (int)bytes.Position; + var alloc = _allocator.Allocate(length, 1); + bytes.GetBuffer().AsSpan(0, length).CopyTo(new Span(alloc, length)); + var (oldData, oldLength) = resource->GetData(); + _allocator.Release((void*)oldData, oldLength); + resource->SetData((nint)alloc, length); + Penumbra.Log.Information($"Post-Processed {originalGamePath} on resource 0x{(nint)resource:X} with {collection} for {gr.ToName()}."); + } +} diff --git a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs new file mode 100644 index 00000000..9a9096f3 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs @@ -0,0 +1,44 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Atch; + + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + { + if (!resolveData.Valid) + return resolved; + + if (!TryGetAtchGenderRace(path, out var gr)) + return resolved; + + Penumbra.Log.Information($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); + if (resolveData.ModCollection.MetaCache?.Atch.GetFile(gr, out var file) == true) + return PathDataHandler.CreateAtch(path, resolveData.ModCollection); + + return resolved; + } + + public static bool TryGetAtchGenderRace(CiByteString originalGamePath, out GenderRace genderRace) + { + if (originalGamePath[^6] != '1' + || originalGamePath[^7] != '0' + || !ushort.TryParse(originalGamePath.Span[^9..^7], out var grInt) + || grInt > 18) + { + genderRace = GenderRace.Unknown; + return false; + } + + genderRace = (GenderRace)(grInt * 100 + 1); + return true; + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs index 65608ba0..875eb254 100644 --- a/Penumbra/Interop/Processing/GamePathPreProcessService.cs +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -25,8 +25,7 @@ public class GamePathPreProcessService : IService public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, - FullPath? resolved, - Utf8GamePath originalPath) + FullPath? resolved, Utf8GamePath originalPath) { if (!_processors.TryGetValue(type, out var processor)) return (resolved, resolveData); diff --git a/Penumbra/Meta/AtchManager.cs b/Penumbra/Meta/AtchManager.cs new file mode 100644 index 00000000..68f2f815 --- /dev/null +++ b/Penumbra/Meta/AtchManager.cs @@ -0,0 +1,26 @@ +using System.Collections.Frozen; +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class AtchManager : IService +{ + private static readonly IReadOnlyList GenderRaces = + [ + GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, GenderRace.ElezenMale, + GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, GenderRace.RoegadynFemale, + GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, GenderRace.HrothgarMale, + GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public readonly IReadOnlyDictionary AtchFileBase; + + public AtchManager(IDataManager manager) + { + AtchFileBase = GenderRaces.ToFrozenDictionary(gr => gr, + gr => new AtchFile(manager.GetFile($"chara/xls/attachOffset/c{gr.ToRaceCode()}.atch")!.DataSpan)); + } +} diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs new file mode 100644 index 00000000..bce37620 --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRace, ushort EntryIndex) + : IComparable, IMetaIdentifier +{ + public Gender Gender + => GenderRace.Split().Item1; + + public ModelRace Race + => GenderRace.Split().Item2; + + public int CompareTo(AtchIdentifier other) + { + var typeComparison = Type.CompareTo(other.Type); + if (typeComparison != 0) + return typeComparison; + + var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); + if (genderRaceComparison != 0) + return genderRaceComparison; + + return EntryIndex.CompareTo(other.EntryIndex); + } + + public override string ToString() + => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing specific + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + var race = (int)GenderRace / 100; + var remainder = (int)GenderRace - 100 * race; + if (remainder != 1) + return false; + + return race is >= 0 and <= 18; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["Type"] = Type.ToAbbreviation(); + jObj["Index"] = EntryIndex; + return jObj; + } + + public static AtchIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var type = AtchExtensions.FromString(jObj["Type"]?.ToObject() ?? string.Empty); + var entryIndex = jObj["Index"]?.ToObject() ?? ushort.MaxValue; + if (entryIndex == ushort.MaxValue || type is AtchType.Unknown) + return null; + + var ret = new AtchIdentifier(type, Names.CombinedRace(gender, race), entryIndex); + return ret.Validate() ? ret : null; + } + + MetaManipulationType IMetaIdentifier.Type + => MetaManipulationType.Atch; +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index d1668a4d..999fd906 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -14,6 +14,7 @@ public enum MetaManipulationType : byte Gmp = 5, Rsp = 6, GlobalEqp = 7, + Atch = 8, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index da061bec..ca45c777 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -16,6 +17,7 @@ public class MetaDictionary private readonly Dictionary _est = []; private readonly Dictionary _rsp = []; private readonly Dictionary _gmp = []; + private readonly Dictionary _atch = []; private readonly HashSet _globalEqp = []; public IReadOnlyDictionary Imc @@ -36,6 +38,9 @@ public class MetaDictionary public IReadOnlyDictionary Rsp => _rsp; + public IReadOnlyDictionary Atch + => _atch; + public IReadOnlySet GlobalEqp => _globalEqp; @@ -50,6 +55,7 @@ public class MetaDictionary MetaManipulationType.Est => _est.Count, MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.Atch => _atch.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count, _ => 0, }; @@ -63,6 +69,7 @@ public class MetaDictionary GlobalEqpManipulation i => _globalEqp.Contains(i), GmpIdentifier i => _gmp.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i), + AtchIdentifier i => _atch.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i), _ => false, }; @@ -76,6 +83,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); _globalEqp.Clear(); } @@ -88,6 +96,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); } public bool Equals(MetaDictionary other) @@ -98,6 +107,7 @@ public class MetaDictionary && _est.SetEquals(other._est) && _rsp.SetEquals(other._rsp) && _gmp.SetEquals(other._gmp) + && _atch.SetEquals(other._atch) && _globalEqp.SetEquals(other._globalEqp); public IEnumerable Identifiers @@ -107,6 +117,7 @@ public class MetaDictionary .Concat(_est.Keys.Cast()) .Concat(_gmp.Keys.Cast()) .Concat(_rsp.Keys.Cast()) + .Concat(_atch.Keys.Cast()) .Concat(_globalEqp.Cast()); #region TryAdd @@ -171,6 +182,15 @@ public class MetaDictionary return true; } + public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { if (!_globalEqp.Add(identifier)) @@ -244,6 +264,15 @@ public class MetaDictionary return true; } + public bool Update(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.ContainsKey(identifier)) + return false; + + _atch[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -266,6 +295,9 @@ public class MetaDictionary public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) => _imc.TryGetValue(identifier, out value); + public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) + => _atch.TryGetValue(identifier, out value); + #endregion public bool Remove(IMetaIdentifier identifier) @@ -279,6 +311,7 @@ public class MetaDictionary GmpIdentifier i => _gmp.Remove(i), ImcIdentifier i => _imc.Remove(i), RspIdentifier i => _rsp.Remove(i), + AtchIdentifier i => _atch.Remove(i), _ => false, }; if (ret) @@ -308,6 +341,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._est) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._atch) + TryAdd(identifier, entry); + foreach (var identifier in manips._globalEqp) TryAdd(identifier); } @@ -351,6 +387,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -369,8 +411,9 @@ public class MetaDictionary _est.SetTo(other._est); _rsp.SetTo(other._rsp); _gmp.SetTo(other._gmp); + _atch.SetTo(other._atch); _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } public void UpdateTo(MetaDictionary other) @@ -381,8 +424,9 @@ public class MetaDictionary _est.UpdateTo(other._est); _rsp.UpdateTo(other._rsp); _gmp.UpdateTo(other._gmp); + _atch.UpdateTo(other._atch); _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } #endregion @@ -460,6 +504,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtchIdentifier identifier, AtchEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atch.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.ToJson(), + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -487,6 +541,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -531,6 +587,7 @@ public class MetaDictionary SerializeTo(array, value._est); SerializeTo(array, value._rsp); SerializeTo(array, value._gmp); + SerializeTo(array, value._atch); SerializeTo(array, value._globalEqp); array.WriteTo(writer); } @@ -618,6 +675,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; } + case MetaManipulationType.Atch: + { + var identifier = AtchIdentifier.FromJson(manip); + var entry = AtchEntry.FromJson(manip["Entry"] as JObject); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid ATCH Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); @@ -648,6 +715,7 @@ public class MetaDictionary _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); Count = cache.Count; } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 3755afa2..5250273b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -5,6 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Data; using Penumbra.Import; +using Penumbra.Interop.Hooks.Meta; using Penumbra.Interop.Services; using Penumbra.Meta.Files; using Penumbra.Mods; @@ -25,13 +26,14 @@ public class MetaFileManager : IService internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly AtchManager AtchManager; internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); internal readonly IFileAllocator XivAllocator; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, - FileCompactor compactor, IGameInteropProvider interop) + FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -41,6 +43,7 @@ public class MetaFileManager : IService ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + AtchManager = atchManager; ImcChecker = new ImcChecker(this); XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 217ba93d..7a5142dc 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -58,6 +58,7 @@ public class ModMetaEditor( OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); + OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 917dba6c..534911df 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; +using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs new file mode 100644 index 00000000..4cf01faa --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -0,0 +1,245 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class AtchMetaDrawer : MetaDrawer, IService +{ + public override ReadOnlySpan Label + => "Attachment Points (ATCH)###ATCH"u8; + + public override int NumColumns + => 10; + + public override float ColumnHeight + => 2 * ImUtf8.FrameHeightSpacing; + + private AtchFile? _currentBaseAtchFile; + private AtchPoint? _currentBaseAtchPoint; + private AtchPointCombo _combo; + + public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : base(editor, metaFiles) + { + _combo = new AtchPointCombo(() => _currentBaseAtchFile?.Points.Select(p => p.Type).ToList() ?? []); + } + + private sealed class AtchPointCombo(Func> generator) + : FilterComboCache(generator, MouseWheelType.Control, Penumbra.Log) + { + protected override string ToString(AtchType obj) + => obj.ToName(); + } + + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atch))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default; + DrawEntry(defaultEntry, ref defaultEntry, true); + } + + private void UpdateEntry() + => Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex]; + + protected override void Initialize() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale]; + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0); + Entry = _currentBaseAtchPoint.Entries[0]; + } + + protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default; + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate() + => Editor.Atch.Select(kvp => (kvp.Key, kvp.Value)) + .OrderBy(p => p.Key.GenderRace) + .ThenBy(p => p.Key.Type) + .ThenBy(p => p.Key.EntryIndex); + + protected override int Count + => Editor.Atch.Count; + + private bool DrawIdentifierInput(ref AtchIdentifier identifier) + { + var changes = false; + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier, false); + if (changes) + UpdateFile(); + ImGui.TableNextColumn(); + if (DrawPointInput(ref identifier, _combo)) + { + _currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type); + changes = true; + } + + ImGui.TableNextColumn(); + changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!); + + return changes; + } + + private void UpdateFile() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; + _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); + if (_currentBaseAtchPoint == null) + { + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; + } + + if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) + Identifier = Identifier with { EntryIndex = 0 }; + } + + private static void DrawIdentifier(AtchIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + DrawGender(ref identifier, true); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Attachment Point Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.EntryIndex.ToString(), FrameColor); + ImUtf8.HoverTooltip("State Entry Index"u8); + } + + private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + if (defaultEntry.Bone.Length == 0) + return false; + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##BoneName"u8, entry.FullSpan, out TerminatedByteString newBone)) + { + entry.SetBoneName(newBone); + changes = true; + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Bone Name"u8); + + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchScale"u8, ref entry.Scale); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Scale"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetX"u8, ref entry.OffsetX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationX"u8, ref entry.RotationX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetY"u8, ref entry.OffsetY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationY"u8, ref entry.RotationY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetZ"u8, ref entry.OffsetZ); + ImUtf8.HoverTooltip("Offset Z-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationZ"u8, ref entry.RotationZ); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8); + + return changes; + } + + private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + + return ret; + } + + private static bool DrawGender(ref AtchIdentifier identifier, bool disabled) + { + var isMale = identifier.Gender is Gender.Male; + + if (!ImUtf8.IconButton(isMale ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus, "Gender"u8, buttonColor: disabled ? 0x000F0000u : 0) + || disabled) + return false; + + identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) }; + return true; + } + + private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo) + { + if (!combo.Draw("##AtchPoint", identifier.Type.ToName(), "Attachment Point Type", 160 * ImUtf8.GlobalScale, + ImGui.GetTextLineHeightWithSpacing())) + return false; + + identifier = identifier with { Type = combo.CurrentSelection }; + return true; + } + + private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint) + { + var index = identifier.EntryIndex; + ImGui.SetNextItemWidth(40 * ImUtf8.GlobalScale); + var ret = ImUtf8.DragScalar("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f, + ImGuiSliderFlags.AlwaysClamp); + ImUtf8.HoverTooltip("State Entry Index"u8); + if (!ret) + return false; + + index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint!.Entries.Length - 1)); + identifier = identifier with { EntryIndex = index }; + return true; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index f9baddbe..348a0d4c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -34,7 +35,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqdp))); ImGui.TableNextColumn(); var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 51b14459..d6df95cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -34,7 +35,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 09075319..e5e28a3d 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Est))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 1aa9060e..929feadd 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.Meta; @@ -29,7 +30,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.GlobalEqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 9532d8e7..3691a4f7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -8,6 +8,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; namespace Penumbra.UI.AdvancedWindow.Meta; @@ -32,7 +33,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Gmp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index c8310cf7..34488a87 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -35,7 +36,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Imc))); ImGui.TableNextColumn(); var canAdd = _fileExists && !Editor.Contains(Identifier); var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; @@ -116,7 +117,6 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); ImUtf8.HoverTooltip("Equip Slot"u8); } - } private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) @@ -161,8 +161,9 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { var (equipSlot, secondaryId) = type switch { - ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), - ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), + ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId)0), + ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0), _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 75de20a7..4c9142d8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -14,8 +14,9 @@ namespace Penumbra.UI.AdvancedWindow.Meta; public interface IMetaDrawer { - public ReadOnlySpan Label { get; } - public int NumColumns { get; } + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public float ColumnHeight { get; } public void Draw(); } @@ -42,7 +43,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta using var id = ImUtf8.PushId((int)Identifier.Type); DrawNew(); - var height = ImUtf8.FrameHeightSpacing; + var height = ColumnHeight; var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); ImGuiClip.DrawEndDummy(remainder, height); @@ -54,6 +55,9 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta public abstract ReadOnlySpan Label { get; } public abstract int NumColumns { get; } + public virtual float ColumnHeight + => ImUtf8.FrameHeightSpacing; + protected abstract void DrawNew(); protected abstract void Initialize(); protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); @@ -138,14 +142,14 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new Lazy(() => new JArray { MetaDictionary.Serialize(identifier, entry)! })); ImGui.TableNextColumn(); if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) Editor.Changes |= Editor.Remove(identifier); } - protected void CopyToClipboardButton(ReadOnlySpan tooltip, JToken? manipulations) + protected void CopyToClipboardButton(ReadOnlySpan tooltip, Lazy manipulations) { if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index b3dd9299..d1c7cd52 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -10,7 +10,8 @@ public class MetaDrawers( GlobalEqpMetaDrawer globalEqp, GmpMetaDrawer gmp, ImcMetaDrawer imc, - RspMetaDrawer rsp) : IService + RspMetaDrawer rsp, + AtchMetaDrawer atch) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -19,6 +20,7 @@ public class MetaDrawers( public readonly RspMetaDrawer Rsp = rsp; public readonly ImcMetaDrawer Imc = imc; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + public readonly AtchMetaDrawer Atch = atch; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -29,7 +31,8 @@ public class MetaDrawers( MetaManipulationType.Est => Est, MetaManipulationType.Gmp => Gmp, MetaManipulationType.Rsp => Rsp, + MetaManipulationType.Atch => Atch, MetaManipulationType.GlobalEqp => GlobalEqp, - _ => null, + _ => null, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 87e8c5b8..d60f877b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Rsp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 49eac96e..22271d38 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -56,6 +56,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Est); DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.GlobalEqp); } From 10279fdc187d23c64ee158d89e3903137b085a00 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 17:04:38 +0100 Subject: [PATCH 2058/2451] fix inverted hook logic. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 5 ++--- Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index 748ca93a..07e34a66 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -1,4 +1,3 @@ -using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; using Penumbra.GameData; @@ -19,7 +18,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState = metaState; _collectionResolver = collectionResolver; Task = hooks.CreateHook("AtchCaller1", Sigs.AtchCaller1, Detour, - metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller1); + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.AtchCaller1); if (!HookOverrides.Instance.Meta.AtchCaller1) _metaState.Config.ModsEnabled += Toggle; } @@ -30,7 +29,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); - Penumbra.Log.Excessive( + Penumbra.Log.Information( $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); } diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs index 9b3349f2..aa2d3f31 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -18,7 +18,7 @@ public unsafe class AtchCallerHook2 : FastHook, IDispo _metaState = metaState; _collectionResolver = collectionResolver; Task = hooks.CreateHook("AtchCaller2", Sigs.AtchCaller2, Detour, - metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller2); + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.AtchCaller2); if (!HookOverrides.Instance.Meta.AtchCaller2) _metaState.Config.ModsEnabled += Toggle; } From 28250a9304f77be0dcfcc071f16a626e10785a0a Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 26 Nov 2024 16:11:29 +0000 Subject: [PATCH 2059/2451] [CI] Updating repo.json for testing_1.3.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f5a8e2f1..bfea1e12 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.3", + "TestingAssemblyVersion": "1.3.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8242cde15cfd5da17c3819157d3b61900bce92e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:00:42 +0100 Subject: [PATCH 2060/2451] Don't spam logs. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index 07e34a66..dcbaedc8 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -29,7 +29,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); } From 0aa8a44b8d878e6f383aeaf927d8237db32023ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:00:57 +0100 Subject: [PATCH 2061/2451] Fix meta manipulation copy/paste. --- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 4c9142d8..2b9285ef 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -154,7 +154,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations.Value, MetaApi.CurrentVersion); if (text.Length > 0) ImGui.SetClipboardText(text); } From ac2631384f09c5bb927588f0133620e0dfd0a503 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:01:10 +0100 Subject: [PATCH 2062/2451] Fix mod reload of atch manipulations. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 7a5142dc..876fe12f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using OtterGui.Services; +using Penumbra.Collections.Cache; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -157,6 +158,23 @@ public class ModMetaEditor( } } + foreach (var (key, value) in clone.Atch) + { + var defaultEntry = AtchCache.GetDefault(metaFileManager, key); + if (!defaultEntry.HasValue) + continue; + + if (!defaultEntry.Value.Equals(value)) + { + dict.TryAdd(key, value); + } + else + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ++count; + } + } + if (count == 0) return false; From c8ad4bc1062fc9ec7885fcdcabed2e4d15c3efc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:01:42 +0100 Subject: [PATCH 2063/2451] Use meta transfer v1. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 217cb1e3..871fe18b 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -53,7 +53,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV0(collection); + => CompressMetaManipulationsV1(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From 242c0ee38fd0265db808e04d33e2c6d44768cf01 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:41:16 +0100 Subject: [PATCH 2064/2451] Add testing to IPC Meta. --- Penumbra/Api/Api/MetaApi.cs | 20 ++++++++++--------- Penumbra/Api/Api/TemporaryApi.cs | 4 ++-- Penumbra/Api/IpcTester/MetaIpcTester.cs | 16 ++++++++++++++- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 871fe18b..3f876bbf 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -146,11 +146,12 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver /// The empty string is treated as an empty set. /// Only returns true if all conversions are successful and distinct. /// - internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) + internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips, out byte version) { if (manipString.Length == 0) { - manips = new MetaDictionary(); + manips = new MetaDictionary(); + version = byte.MaxValue; return true; } @@ -163,9 +164,9 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver zipStream.CopyTo(resultStream); resultStream.Flush(); resultStream.Position = 0; - var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); - var version = data[0]; - data = data[1..]; + var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); + version = data[0]; + data = data[1..]; switch (version) { case 0: return ConvertManipsV0(data, out manips); @@ -179,7 +180,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver catch (Exception ex) { Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}"); - manips = null; + manips = null; + version = byte.MaxValue; return false; } } @@ -274,7 +276,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver var json = Encoding.UTF8.GetString(data); manips = JsonConvert.DeserializeObject(json); return manips != null; - } + } internal void TestMetaManipulations() { @@ -291,11 +293,11 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver var v1Time = watch.ElapsedMilliseconds; watch.Restart(); - var v1Success = ConvertManips(v1, out var v1Roundtrip); + var v1Success = ConvertManips(v1, out var v1Roundtrip, out _); var v1RoundtripTime = watch.ElapsedMilliseconds; watch.Restart(); - var v0Success = ConvertManips(v0, out var v0Roundtrip); + var v0Success = ConvertManips(v0, out var v0Roundtrip, out _); var v0RoundtripTime = watch.ElapsedMilliseconds; Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal"); diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 516b4347..201839e7 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -60,7 +60,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!MetaApi.ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m, out _)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch @@ -86,7 +86,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!MetaApi.ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m, out _)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 8b393ade..010e3c5a 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -2,13 +2,19 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Api; using Penumbra.Api.IpcSubscribers; +using Penumbra.Meta.Manipulations; namespace Penumbra.Api.IpcTester; public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService { - private int _gameObjectIndex; + private int _gameObjectIndex; + private string _metaBase64 = string.Empty; + private MetaDictionary _metaDict = new(); + private byte _parsedVersion = byte.MaxValue; public void Draw() { @@ -17,6 +23,11 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService return; ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); + if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8)) + if (!MetaApi.ConvertManips(_metaBase64, out _metaDict, out _parsedVersion)) + _metaDict ??= new MetaDictionary(); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -34,5 +45,8 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex); ImGui.SetClipboardText(base64); } + + IpcTester.DrawIntro(string.Empty, "Parsed Data"); + ImUtf8.Text($"Version: {_parsedVersion}, Count: {_metaDict.Count}"); } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 2b9285ef..a6f042b7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -154,7 +154,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; - var text = Functions.ToCompressedBase64(manipulations.Value, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations.Value, 0); if (text.Length > 0) ImGui.SetClipboardText(text); } From 9787e5a85228d28b686a301296a528747ca0a910 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:49:04 +0100 Subject: [PATCH 2065/2451] Fix some meta issues. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 22271d38..7d688df9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -111,7 +111,7 @@ public partial class ModEditWindow if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations, 0); if (text.Length > 0) ImGui.SetClipboardText(text); } @@ -122,8 +122,7 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaApi.CurrentVersion && manips != null) + if (MetaApi.ConvertManips(clipboard, out var manips, out _)) { _editor.MetaEditor.UpdateTo(manips); _editor.MetaEditor.Changes = true; @@ -139,8 +138,7 @@ public partial class ModEditWindow if (ImGui.Button("Set from Clipboard")) { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaApi.CurrentVersion && manips != null) + if (MetaApi.ConvertManips(clipboard, out var manips, out _)) { _editor.MetaEditor.SetTo(manips); _editor.MetaEditor.Changes = true; From 97d7ea7759898f721df7b743cb86d154813e71b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 22:51:14 +0100 Subject: [PATCH 2066/2451] tmp --- Penumbra/Api/Api/MetaApi.cs | 27 ++++++++++--------- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- .../Processing/AtchPathPreProcessor.cs | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 3f876bbf..7c0cd5fc 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -16,8 +16,6 @@ namespace Penumbra.Api.Api; public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService { - public const int CurrentVersion = 1; - public string GetPlayerMetaManipulations() { var collection = collectionResolver.PlayerCollection(); @@ -99,7 +97,6 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Gmp); - WriteCache(zipStream, cache.Atch); cache.GlobalEqp.EnterReadLock(); try @@ -112,6 +109,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver { cache.GlobalEqp.ExitReadLock(); } + + WriteCache(zipStream, cache.Atch); } } @@ -251,15 +250,6 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } - var atchCount = r.ReadInt32(); - for (var i = 0; i < atchCount; ++i) - { - var identifier = r.Read(); - var value = r.Read(); - if (!identifier.Validate() || !manips.TryAdd(identifier, value)) - return false; - } - var globalEqpCount = r.ReadInt32(); for (var i = 0; i < globalEqpCount; ++i) { @@ -268,6 +258,19 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } + // Atch was added after there were already some V1 around, so check for size here. + if (r.Position < r.Count) + { + var atchCount = r.ReadInt32(); + for (var i = 0; i < atchCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + } + return true; } diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 010e3c5a..9cf20cd7 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -24,7 +24,7 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8)) - if (!MetaApi.ConvertManips(_metaBase64, out _metaDict, out _parsedVersion)) + if (!MetaApi.ConvertManips(_metaBase64, out _metaDict!, out _parsedVersion)) _metaDict ??= new MetaDictionary(); diff --git a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs index 9a9096f3..428826bc 100644 --- a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs @@ -20,7 +20,7 @@ public sealed class AtchPathPreProcessor : IPathPreProcessor if (!TryGetAtchGenderRace(path, out var gr)) return resolved; - Penumbra.Log.Information($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); + Penumbra.Log.Excessive($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); if (resolveData.ModCollection.MetaCache?.Atch.GetFile(gr, out var file) == true) return PathDataHandler.CreateAtch(path, resolveData.ModCollection); From 8b9f59426e3dba01e4f16267b05bf804ceca881d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 23:07:08 +0100 Subject: [PATCH 2067/2451] No V1 Meta yet... wait until next version ban or API increase. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 7c0cd5fc..ff88ae4e 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -51,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV1(collection); + => CompressMetaManipulationsV0(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From d7095af89b322af9bd81acb80a9bab842fbee90f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Nov 2024 17:33:05 +0100 Subject: [PATCH 2068/2451] Add jumping to mods in OnScreen tab. --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 3 + .../ResourceTree/ResourceTreeFactory.cs | 1 + .../UI/AdvancedWindow/ResourceTreeViewer.cs | 112 +++++++++--------- .../ResourceTreeViewerFactory.cs | 6 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 +- 5 files changed, 73 insertions(+), 57 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 6c3e1ebe..088527ca 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,5 @@ using Penumbra.Api.Enums; +using Penumbra.Mods; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -16,6 +17,7 @@ public class ResourceNode : ICloneable public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; public string? ModName; + public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; public readonly ulong Length; @@ -60,6 +62,7 @@ public class ResourceNode : ICloneable PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; ModName = other.ModName; + Mod = other.Mod; ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; Length = other.Length; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 9738148f..7e378f41 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -135,6 +135,7 @@ public class ResourceTreeFactory( if (node.FullPath.IsRooted && modManager.TryIdentifyPath(node.FullPath.FullName, out var mod, out var relativePath)) { node.ModName = mod.Name; + node.Mod.SetTarget(mod); node.ModRelativePath = relativePath; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3aff2ac9..7bad64f9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -3,55 +3,40 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Text; +using Penumbra.Api.Enums; using Penumbra.Interop.ResourceTree; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.String; namespace Penumbra.UI.AdvancedWindow; -public class ResourceTreeViewer +public class ResourceTreeViewer( + Configuration config, + ResourceTreeFactory treeFactory, + ChangedItemDrawer changedItemDrawer, + IncognitoService incognito, + int actionCapacity, + Action onRefresh, + Action drawActions, + CommunicatorService communicator) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; - private readonly ChangedItemDrawer _changedItemDrawer; - private readonly IncognitoService _incognito; - private readonly int _actionCapacity; - private readonly Action _onRefresh; - private readonly Action _drawActions; - private readonly HashSet _unfolded; + private readonly CommunicatorService _communicator = communicator; + private readonly HashSet _unfolded = []; - private readonly Dictionary _filterCache; + private readonly Dictionary _filterCache = []; - private TreeCategory _categoryFilter; - private ChangedItemIconFlag _typeFilter; - private string _nameFilter; - private string _nodeFilter; + private TreeCategory _categoryFilter = AllCategories; + private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; + private string _nameFilter = string.Empty; + private string _nodeFilter = string.Empty; private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito, int actionCapacity, Action onRefresh, Action drawActions) - { - _config = config; - _treeFactory = treeFactory; - _changedItemDrawer = changedItemDrawer; - _incognito = incognito; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = []; - - _filterCache = []; - - _categoryFilter = AllCategories; - _typeFilter = ChangedItemFlagExtensions.AllFlags; - _nameFilter = string.Empty; - _nodeFilter = string.Empty; - } - public void Draw() { DrawControls(); @@ -74,7 +59,7 @@ public class ResourceTreeViewer } else if (_task.IsCompletedSuccessfully) { - var debugMode = _config.DebugMode; + var debugMode = config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { var category = Classify(tree); @@ -83,7 +68,7 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", + var isOpen = ImGui.CollapsingHeader($"{(incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { @@ -98,9 +83,9 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {(_incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImGui.TextUnformatted($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); - using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, + using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; @@ -108,9 +93,9 @@ public class ResourceTreeViewer ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - if (_actionCapacity > 0) + if (actionCapacity > 0) ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, - (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); + (actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); @@ -150,7 +135,7 @@ public class ResourceTreeViewer ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) { - filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + filterChanged |= changedItemDrawer.DrawTypeFilter(ref _typeFilter); } var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; @@ -160,7 +145,7 @@ public class ResourceTreeViewer ImGui.SetNextItemWidth(fieldWidth); filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); - _incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); + incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); if (filterChanged) _filterCache.Clear(); @@ -171,7 +156,7 @@ public class ResourceTreeViewer { try { - return _treeFactory.FromObjectTable(ResourceTreeFactoryFlags) + return treeFactory.FromObjectTable(ResourceTreeFactoryFlags) .Select(entry => entry.ResourceTree) .ToArray(); } @@ -179,16 +164,16 @@ public class ResourceTreeViewer { _filterCache.Clear(); _unfolded.Clear(); - _onRefresh(); + onRefresh(); } }); private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemIconFlag parentFilterIconFlag) { - var debugMode = _config.DebugMode; + var debugMode = config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { @@ -231,7 +216,7 @@ public class ResourceTreeViewer ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } - _changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); + changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) @@ -270,14 +255,33 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); if (resourceNode.FullPath.FullName.Length > 0) { - var uiFullPathStr = resourceNode.ModName != null && resourceNode.ModRelativePath != null - ? $"[{resourceNode.ModName}] {resourceNode.ModRelativePath}" - : resourceNode.FullPath.ToPath(); - ImGui.Selectable(uiFullPathStr, false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + var hasMod = resourceNode.Mod.TryGetTarget(out var mod); + if (resourceNode is { ModName: not null, ModRelativePath: not null }) + { + var modName = $"[{(hasMod ? mod!.Name : resourceNode.ModName)}]"; + var textPos = ImGui.GetCursorPosX() + ImUtf8.CalcTextSize(modName).X + ImGui.GetStyle().ItemInnerSpacing.X; + using var group = ImUtf8.Group(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) + { + ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(textPos); + ImUtf8.Text(resourceNode.ModRelativePath); + } + else + { + ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + } + if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); + if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) + _communicator.SelectTab.Invoke(TabType.Mods, mod); + ImGuiUtil.HoverTooltip( - $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { @@ -289,12 +293,12 @@ public class ResourceTreeViewer mutedColor.Dispose(); - if (_actionCapacity > 0) + if (actionCapacity > 0) { ImGui.TableNextColumn(); using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); - _drawActions(resourceNode, new Vector2(frameHeight)); + drawActions(resourceNode, new Vector2(frameHeight)); } if (unfolded) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index ea64c0bf..10a4aea2 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,5 +1,6 @@ using OtterGui.Services; using Penumbra.Interop.ResourceTree; +using Penumbra.Services; namespace Penumbra.UI.AdvancedWindow; @@ -7,8 +8,9 @@ public class ResourceTreeViewerFactory( Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito) : IService + IncognitoService incognito, + CommunicatorService communicator) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index fc735d04..46e427ed 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -461,7 +461,7 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -485,6 +485,9 @@ public class DebugTab : Window, ITab, IUiService ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{(*(nint*)obj.Address):X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" : "NULL"); } return; @@ -499,6 +502,9 @@ public class DebugTab : Window, ITab, IUiService ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actors.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); } } From b377ca372ce2d349292b89c467948f659e312c24 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 29 Nov 2024 16:35:08 +0000 Subject: [PATCH 2069/2451] [CI] Updating repo.json for testing_1.3.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index bfea1e12..59ca83be 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.4", + "TestingAssemblyVersion": "1.3.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 22541b3fd894a244ee8160845ce52d9939c29c1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Dec 2024 20:23:15 +0100 Subject: [PATCH 2070/2451] Update variables drawer. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 160 +++--------------- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 126 ++++++++++++++ 2 files changed, 154 insertions(+), 132 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 46e427ed..30605101 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -6,7 +6,6 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; @@ -35,8 +34,6 @@ using Penumbra.UI.Classes; using Penumbra.Util; using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; @@ -80,8 +77,7 @@ public class DebugTab : Window, ITab, IUiService private readonly HttpApi _httpApi; private readonly ActorManager _actors; private readonly StainService _stains; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; + private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly ResourceManagerService _resourceManager; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; @@ -104,18 +100,17 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; - private readonly RsfService _rsfService; + private readonly RsfService _rsfService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, - CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, RsfService rsfService) + HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -132,8 +127,6 @@ public class DebugTab : Window, ITab, IUiService _httpApi = httpApi; _actors = actors; _stains = stains; - _characterUtility = characterUtility; - _residentResources = residentResources; _resourceManager = resourceManager; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; @@ -153,7 +146,8 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; - _rsfService = rsfService; + _rsfService = rsfService; + _globalVariablesDrawer = globalVariablesDrawer; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -185,14 +179,13 @@ public class DebugTab : Window, ITab, IUiService DrawActorsDebug(); DrawCollectionCaches(); _texHeaderDrawer.Draw(); - DrawDebugCharacterUtility(); DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); DrawResourceProblems(); _hookOverrides.Draw(); DrawPlayerModelInfo(); - DrawGlobalVariableInfo(); + _globalVariablesDrawer.Draw(); DrawDebugTabIpc(); } @@ -217,8 +210,10 @@ public class DebugTab : Window, ITab, IUiService { if (resourceNode) foreach (var (path, resource) in collection._cache!.CustomResources) + { ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } } using var modNode = ImUtf8.TreeNode("Enabled Mods"u8); @@ -485,9 +480,11 @@ public class DebugTab : Window, ITab, IUiService ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{(*(nint*)obj.Address):X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL"); ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero + ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" + : "NULL"); } return; @@ -795,68 +792,6 @@ public class DebugTab : Window, ITab, IUiService } } - /// - /// Draw information about the character utility class from SE, - /// displaying all files, their sizes, the default files and the default sizes. - /// - private unsafe void DrawDebugCharacterUtility() - { - if (!ImGui.CollapsingHeader("Character Utility")) - return; - - using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) - { - var intern = CharacterUtility.ReverseIndices[idx]; - var resource = _characterUtility.Address->Resource(idx); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"[{idx}]"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{(ulong)resource:X}"); - ImGui.TableNextColumn(); - if (resource == null) - { - ImGui.TableNextRow(); - continue; - } - - UiHelpers.Text(resource); - ImGui.TableNextColumn(); - var data = (nint)resource->CsHandle.GetData(); - var length = resource->CsHandle.GetLength(); - if (ImGui.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(length.ToString()); - - ImGui.TableNextColumn(); - if (intern.Value != -1) - { - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); - } - else - { - ImGui.TableNextColumn(); - } - } - } private void DrawShaderReplacementFixer() { @@ -946,45 +881,6 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); } - /// Draw information about the resident resource files. - private unsafe void DrawDebugResidentResources() - { - using var tree = TreeNode("Resident Resources"); - if (!tree) - return; - - if (_residentResources.Address == null || _residentResources.Address->NumResources == 0) - return; - - using var table = Table("##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - for (var i = 0; i < _residentResources.Address->NumResources; ++i) - { - var resource = _residentResources.Address->ResourceList[i]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{(ulong)resource:X}"); - ImGui.TableNextColumn(); - UiHelpers.Text(resource); - } - } - - private static void DrawCopyableAddress(string label, nint address) - { - using (var _ = PushFont(UiBuilder.MonoFont)) - { - if (ImGui.Selectable($"0x{address:X16} {label}")) - ImGui.SetClipboardText($"{address:X16}"); - } - - ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); - } - - private static unsafe void DrawCopyableAddress(string label, void* address) - => DrawCopyableAddress(label, (nint)address); - /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -993,13 +889,13 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; - DrawCopyableAddress("PlayerCharacter", player.Address); + DrawCopyableAddress("PlayerCharacter"u8, player.Address); var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; - DrawCopyableAddress("CharacterBase", model); + DrawCopyableAddress("CharacterBase"u8, model); using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { @@ -1054,20 +950,6 @@ public class DebugTab : Window, ITab, IUiService } } - /// Draw information about some game global variables. - private unsafe void DrawGlobalVariableInfo() - { - var header = ImGui.CollapsingHeader("Global Variables"); - ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); - if (!header) - return; - - DrawCopyableAddress("CharacterUtility", _characterUtility.Address); - DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); - DrawCopyableAddress("Device", Device.Instance()); - DrawDebugResidentResources(); - } - private string _crcInput = string.Empty; private FullPath _crcPath = FullPath.Empty; @@ -1169,4 +1051,18 @@ public class DebugTab : Window, ITab, IUiService _config.Ephemeral.DebugSeparateWindow = false; _config.Ephemeral.Save(); } + + public static unsafe void DrawCopyableAddress(ReadOnlySpan label, void* address) + { + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + if (ImUtf8.Selectable($"0x{(nint)address:X16} {label}")) + ImUtf8.SetClipboardText($"0x{(nint)address:X16}"); + } + + ImUtf8.HoverTooltip("Click to copy address to clipboard."u8); + } + + public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) + => DrawCopyableAddress(label, (void*)address); } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs new file mode 100644 index 00000000..601e9b4d --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -0,0 +1,126 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Services; + +namespace Penumbra.UI.Tabs.Debug; + +public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, ResidentResourceManager residentResources) : IUiService +{ + /// Draw information about some game global variables. + public void Draw() + { + var header = ImUtf8.CollapsingHeader("Global Variables"u8); + ImUtf8.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."u8); + if (!header) + return; + + DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); + DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); + DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + DrawCharacterUtility(); + DrawResidentResources(); + } + + /// + /// Draw information about the character utility class from SE, + /// displaying all files, their sizes, the default files and the default sizes. + /// + private void DrawCharacterUtility() + { + using var tree = ImUtf8.TreeNode("Character Utility"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("##CharacterUtility"u8, 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) + { + var intern = CharacterUtility.ReverseIndices[idx]; + var resource = characterUtility.Address->Resource(idx); + ImUtf8.DrawTableColumn($"[{idx}]"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } + + ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); + ImGui.TableNextColumn(); + var data = (nint)resource->CsHandle.GetData(); + var length = resource->CsHandle.GetLength(); + if (ImUtf8.Selectable($"0x{data:X}")) + if (data != nint.Zero && length > 0) + ImUtf8.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); + + ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + ImUtf8.DrawTableColumn(length.ToString()); + + ImGui.TableNextColumn(); + if (intern.Value != -1) + { + ImUtf8.Selectable($"0x{characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImUtf8.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)characterUtility.DefaultResource(intern).Address, + characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + + ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + + ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}"); + } + else + { + ImGui.TableNextColumn(); + } + } + } + + /// Draw information about the resident resource files. + private void DrawResidentResources() + { + using var tree = ImUtf8.TreeNode("Resident Resources"u8); + if (!tree) + return; + + if (residentResources.Address == null || residentResources.Address->NumResources == 0) + return; + + using var table = ImUtf8.Table("##ResidentResources"u8, 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var idx = 0; idx < residentResources.Address->NumResources; ++idx) + { + var resource = residentResources.Address->ResourceList[idx]; + ImUtf8.DrawTableColumn($"[{idx}]"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } + + ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); + ImGui.TableNextColumn(); + var data = (nint)resource->CsHandle.GetData(); + var length = resource->CsHandle.GetLength(); + if (ImUtf8.Selectable($"0x{data:X}")) + if (data != nint.Zero && length > 0) + ImUtf8.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); + + ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + ImUtf8.DrawTableColumn(length.ToString()); + } + } +} From 1434ad6190f0afb145eb502cad065131b50d600e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Dec 2024 20:35:53 +0100 Subject: [PATCH 2071/2451] Add context menu copying for paths in advanced editing. --- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index b07633b6..5cabd14b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,4 +1,3 @@ -using System.Linq; using Dalamud.Interface; using ImGuiNET; using OtterGui; @@ -15,6 +14,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { private readonly HashSet _selectedFiles = new(256); + private readonly HashSet _cutPaths = []; private LowerString _fileFilter = LowerString.Empty; private bool _showGamePaths = true; private string _gamePathEdit = string.Empty; @@ -125,7 +125,7 @@ public partial class ModEditWindow using var id = ImRaii.PushId(i); ImGui.TableNextColumn(); - DrawSelectable(registry); + DrawSelectable(registry, i); if (!_showGamePaths) continue; @@ -177,24 +177,63 @@ public partial class ModEditWindow } } - private void DrawSelectable(FileRegistry registry) + private void DrawSelectable(FileRegistry registry, int i) { var selected = _selectedFiles.Contains(registry); var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; - using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value()); - if (UiHelpers.Selectable(registry.RelPath.Path, selected)) + using (ImRaii.PushColor(ImGuiCol.Text, color.Value())) { - if (selected) - _selectedFiles.Remove(registry); - else - _selectedFiles.Add(registry); + if (UiHelpers.Selectable(registry.RelPath.Path, selected)) + { + if (selected) + _selectedFiles.Remove(registry); + else + _selectedFiles.Add(registry); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + + var rightText = DrawFileTooltip(registry, color); + + ImGui.SameLine(); + ImGuiUtil.RightAlign(rightText); } - var rightText = DrawFileTooltip(registry, color); + DrawContextMenu(registry, i); + } - ImGui.SameLine(); - ImGuiUtil.RightAlign(rightText); + private void DrawContextMenu(FileRegistry registry, int i) + { + using var context = ImUtf8.Popup("context"u8); + if (!context) + return; + + using (ImRaii.Disabled(registry.CurrentUsage == 0)) + { + if (ImUtf8.Selectable("Cut Game Paths"u8)) + { + _cutPaths.Clear(); + for (var j = 0; j < registry.SubModUsage.Count; ++j) + { + if (registry.SubModUsage[j].Item1 != _editor.Option) + continue; + + _cutPaths.Add(registry.SubModUsage[j].Item2); + _editor.FileEditor.SetGamePath(_editor.Option, i, j--, Utf8GamePath.Empty); + } + } + } + + using (ImRaii.Disabled(_cutPaths.Count == 0)) + { + if (ImUtf8.Selectable("Paste Game Paths"u8)) + { + foreach (var path in _cutPaths) + _editor.FileEditor.SetGamePath(_editor.Option!, i, -1, path); + } + } } private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath) From e9014fe4c3de3d3f24adb96cd9d4d84e6d045960 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 5 Dec 2024 19:38:51 +0000 Subject: [PATCH 2072/2451] [CI] Updating repo.json for testing_1.3.1.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 59ca83be..50838b67 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.5", + "TestingAssemblyVersion": "1.3.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 01db37cbd4724e173433e01d7ec381a7ae356569 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Dec 2024 21:22:21 +0100 Subject: [PATCH 2073/2451] Add Copy for paths, update npc names --- Penumbra.GameData | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2b0c7f3b..da74a4be 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d +Subproject commit da74a4be9c9728c6c52134c42603cd8a7040c568 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 5cabd14b..6792c359 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -210,6 +210,21 @@ public partial class ModEditWindow if (!context) return; + using (ImRaii.Disabled(registry.CurrentUsage == 0)) + { + if (ImUtf8.Selectable("Copy Game Paths"u8)) + { + _cutPaths.Clear(); + for (var j = 0; j < registry.SubModUsage.Count; ++j) + { + if (registry.SubModUsage[j].Item1 != _editor.Option) + continue; + + _cutPaths.Add(registry.SubModUsage[j].Item2); + } + } + } + using (ImRaii.Disabled(registry.CurrentUsage == 0)) { if (ImUtf8.Selectable("Cut Game Paths"u8)) From 4cc7d1930b04d951e0255e2c9b82395ed51ea2b9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Dec 2024 21:30:00 +0100 Subject: [PATCH 2074/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index da74a4be..fb692d13 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit da74a4be9c9728c6c52134c42603cd8a7040c568 +Subproject commit fb692d13205fed5e6c5f4c939477c28473198a3b From 22c3b3b629c3d997f0a6a7d689196977d0e5b6b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 15:43:09 +0100 Subject: [PATCH 2075/2451] Again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fb692d13..315258f4 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fb692d13205fed5e6c5f4c939477c28473198a3b +Subproject commit 315258f4f8a59d744aa4d2d1f8c31d410d041729 From 08ff9b679e86d3ab9d76f64c781cf83989617cb5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 17:48:54 +0100 Subject: [PATCH 2076/2451] Add changing mod settings to command / macro API. --- Penumbra/Api/Api/ModSettingsApi.cs | 70 +++++++++++++++++------------- Penumbra/CommandHandler.cs | 61 ++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index e046ce30..8c34c249 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -184,36 +184,9 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - - var setting = Setting.Zero; - switch (mod.Groups[groupIdx]) - { - case { Behaviour: GroupDrawBehaviour.SingleSelection } single: - { - var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting = Setting.Single(optionIdx); - break; - } - case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: - { - foreach (var name in optionNames) - { - var optionIdx = multi.Options.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting |= Setting.Multi(optionIdx); - } - - break; - } - } + var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting); + if (settingSuccess is not PenumbraApiEc.Success) + return ApiHelpers.Return(settingSuccess, args); var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success @@ -283,4 +256,41 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable TriggerSettingEdited(mod); } + + public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList optionNames, out int groupIndex, + out Setting setting) + { + groupIndex = mod.Groups.IndexOf(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase)); + setting = Setting.Zero; + if (groupIndex < 0) + return PenumbraApiEc.OptionGroupMissing; + + switch (mod.Groups[groupIndex]) + { + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return PenumbraApiEc.OptionMissing; + + setting = Setting.Single(optionIdx); + break; + } + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.Options.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return PenumbraApiEc.OptionMissing; + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + return PenumbraApiEc.Success; + } } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index db8d9aca..9c3eb988 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -379,16 +380,18 @@ public class CommandHandler : IDisposable, IApiService if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]") + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|setting]").AddText(" ") + .AddYellow("[Collection Name]") .AddText(" | ") - .AddPurple("[Mod Name or Mod Directory Name]"); + .AddPurple("[Mod Name or Mod Directory Name]") + .AddGreen(" <| [Option Group Name] | [Option1;Option2;...]>"); _chat.Print(seString.BuiltString); return true; } var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var nameSplit = split.Length != 2 - ? Array.Empty() + ? [] : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (nameSplit.Length != 2) { @@ -406,6 +409,23 @@ public class CommandHandler : IDisposable, IApiService if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; + var groupName = string.Empty; + var optionNames = Array.Empty(); + if (state is 4) + { + var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split2.Length < 2) + { + _chat.Print("Not enough arguments for changing settings provided."); + return false; + } + + nameSplit[1] = split2[0]; + groupName = split2[1]; + if (split2.Length == 3) + optionNames = split2[2].Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + } + if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod)) { _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.") @@ -413,12 +433,35 @@ public class CommandHandler : IDisposable, IApiService return false; } - if (HandleModState(state, collection!, mod)) - return true; + if (state < 4) + { + if (HandleModState(state, collection!, mod)) + return true; + + _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) + .AddText("already had the desired state in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); + return false; + } + + switch (ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIndex, out var setting)) + { + case PenumbraApiEc.OptionGroupMissing: + _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" has no group ") + .AddGreen(groupName, true).AddText(".").BuiltString); + return false; + case PenumbraApiEc.OptionMissing: + _chat.Print(new SeStringBuilder().AddText("Not all set options in the mod ").AddRed(nameSplit[1], true) + .AddText(" could be found in group ").AddGreen(groupName, true).AddText(".").BuiltString); + return false; + case PenumbraApiEc.Success: + _collectionEditor.SetModSetting(collection!, mod, groupIndex, setting); + Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ") + .AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); + return true; + } - _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) - .AddText("already had the desired state in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); return false; } @@ -556,6 +599,8 @@ public class CommandHandler : IDisposable, IApiService "toggle" => 2, "inherit" => 3, "inherited" => 3, + "setting" => 4, + "settings" => 4, _ => -1, }; From 5db3d53994d5a7beb0b617078f3becfe04ad5010 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 17:56:27 +0100 Subject: [PATCH 2077/2451] Small improvements. --- Penumbra/CommandHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 9c3eb988..aff7f16f 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -380,7 +380,7 @@ public class CommandHandler : IDisposable, IApiService if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|setting]").AddText(" ") + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|").AddGreen("setting").AddBlue("]").AddText(" ") .AddYellow("[Collection Name]") .AddText(" | ") .AddPurple("[Mod Name or Mod Directory Name]") @@ -416,7 +416,7 @@ public class CommandHandler : IDisposable, IApiService var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (split2.Length < 2) { - _chat.Print("Not enough arguments for changing settings provided."); + _chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); return false; } From 510b9a5f1f638151eab715f0f9eb7c09b990cff0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 18:11:17 +0100 Subject: [PATCH 2078/2451] 1.3.2.0 --- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 0b0ca81a..ec2a716c 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -55,10 +55,24 @@ public class PenumbraChangelog : IUiService Add1_2_1_0(Changelog); Add1_3_0_0(Changelog); Add1_3_1_0(Changelog); - } - + Add1_3_2_0(Changelog); + } + #region Changelogs + private static void Add1_3_2_0(Changelog log) + => log.NextVersion("Version 1.3.2.0") + .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") + .RegisterEntry("Those ATCH manipulations should be shared via Mare Synchronos.", 1) + .RegisterEntry("This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", 1) + .RegisterEntry("Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") + .RegisterEntry("Added some right-click context menu copy options in the File Redirections editor for paths.") + .RegisterHighlight("Added the option to change a specific mod's settings via chat commands by using '/penumbra mod settings'.") + .RegisterEntry("Fixed issues with the copy-pasting of meta manipulations.") + .RegisterEntry("Fixed some other issues related to meta manipulations.") + .RegisterEntry("Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); + + private static void Add1_3_1_0(Changelog log) => log.NextVersion("Version 1.3.1.0") .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") From b5a469c5245d43c2831fa692d5f54b7f79c4fb24 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 13 Dec 2024 17:13:38 +0000 Subject: [PATCH 2079/2451] [CI] Updating repo.json for 1.3.2.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 50838b67..c0e561da 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.6", + "AssemblyVersion": "1.3.2.0", + "TestingAssemblyVersion": "1.3.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From cc97ea0ce938a6c4f561c01991249cd768a613ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Dec 2024 17:52:57 +0100 Subject: [PATCH 2080/2451] Add an option to automatically select the collection assigned to the current character on login. --- .../Collections/CollectionAutoSelector.cs | 75 +++++++++++++++++++ Penumbra/Configuration.cs | 4 +- Penumbra/Services/MessageService.cs | 3 +- Penumbra/UI/Changelog.cs | 1 - Penumbra/UI/Tabs/SettingsTab.cs | 15 +++- 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Collections/CollectionAutoSelector.cs diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs new file mode 100644 index 00000000..e24fa6a9 --- /dev/null +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -0,0 +1,75 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Collections; + +public sealed class CollectionAutoSelector : IService, IDisposable +{ + private readonly Configuration _config; + private readonly ActiveCollections _collections; + private readonly IClientState _clientState; + private readonly CollectionResolver _resolver; + private readonly ObjectManager _objects; + + public CollectionAutoSelector(Configuration config, ActiveCollections collections, IClientState clientState, CollectionResolver resolver, + ObjectManager objects) + { + _config = config; + _collections = collections; + _clientState = clientState; + _resolver = resolver; + _objects = objects; + + if (_config.AutoSelectCollection) + Attach(); + } + + public bool Disposed { get; private set; } + + public void SetAutomaticSelection(bool value) + { + _config.AutoSelectCollection = value; + if (value) + Attach(); + else + Detach(); + } + + private void Attach() + { + if (Disposed) + return; + + _clientState.Login += OnLogin; + Select(); + } + + private void OnLogin() + => Select(); + + private void Detach() + => _clientState.Login -= OnLogin; + + private void Select() + { + if (!_objects[0].IsCharacter) + return; + + var collection = _resolver.PlayerCollection(); + Penumbra.Log.Debug($"Setting current collection to {collection.Identifier} through automatic collection selection."); + _collections.SetCollection(collection, CollectionType.Current); + } + + + public void Dispose() + { + if (Disposed) + return; + + Disposed = true; + Detach(); + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 50426b38..ec5784f8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -56,6 +56,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool AutoSelectCollection { get; set; } = false; + public bool ShowModsInLobby { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; @@ -100,7 +102,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; - public bool MigrateImportedModelsToV6 { get; set; } = true; + public bool MigrateImportedModelsToV6 { get; set; } = true; public bool MigrateImportedMaterialsToLegacy { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index e610cb6a..70ccf47b 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; +using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; using Notification = OtterGui.Classes.Notification; @@ -29,7 +30,7 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti new TextPayload($"{(char)SeIconChar.LinkMarker}"), new UIForegroundPayload(0), new UIGlowPayload(0), - new TextPayload(item.Name.ExtractText()), + new TextPayload(item.Name.ExtractTextExtended()), new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]), new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]), }; diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index ec2a716c..c78ca290 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -92,7 +92,6 @@ public class PenumbraChangelog : IUiService private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") - .RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.") .RegisterEntry("BC4 and BC6 textures can now also be imported.", 1) .RegisterHighlight("Added item swapping from and to the Glasses slot.") diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9d8ea21c..46e214cf 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api; +using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -46,6 +47,7 @@ public class SettingsTab : ITab, IUiService private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; private readonly MigrationSectionDrawer _migrationDrawer; + private readonly CollectionAutoSelector _autoSelector; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -57,7 +59,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector) { _pluginInterface = pluginInterface; _config = config; @@ -80,6 +82,7 @@ public class SettingsTab : ITab, IUiService _predefinedTagManager = predefinedTagConfig; _crashService = crashService; _migrationDrawer = migrationDrawer; + _autoSelector = autoSelector; } public void DrawHeader() @@ -421,6 +424,10 @@ public class SettingsTab : ITab, IUiService /// Draw all settings that do not fit into other categories. private void DrawMiscSettings() { + Checkbox("Automatically Select Character-Associated Collection", + "On every login, automatically select the collection associated with the current character as the current collection for editing.", + _config.AutoSelectCollection, _autoSelector.SetAutomaticSelection); + Checkbox("Print Chat Command Success Messages to Chat", "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", _config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v); @@ -816,13 +823,15 @@ public class SettingsTab : ITab, IUiService if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero, "Try to compress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, + true); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero, "Try to decompress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, + true); if (_compactor.MassCompactRunning) { From 18288815b294ce54549da904b40a6bb0c09dd854 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Dec 2024 18:04:17 +0100 Subject: [PATCH 2081/2451] Add partial copying of color and colordye tables. --- Penumbra.GameData | 2 +- .../Materials/MtrlTab.CommonColorTable.cs | 62 +++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 315258f4..6848397d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 315258f4f8a59d744aa4d2d1f8c31d410d041729 +Subproject commit 6848397dd77cfcdbff1accd860d5b7e95f8c9fe5 diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 38f02100..236a66c3 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -202,24 +202,74 @@ public partial class MtrlTab if (Mtrl.Table == null) return false; - if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, + "Import an exported row from your clipboard onto this row.\n\nRight-Click for more options."u8, ImGui.GetFrameHeight() * Vector2.One, disabled)) + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var row = Mtrl.Table.RowAsBytes(rowIdx); + var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + return false; + + data.AsSpan(0, row.Length).TryCopyTo(row); + data.AsSpan(row.Length).TryCopyTo(dyeRow); + + UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + + return ColorTablePasteFromClipboardContext(rowIdx, disabled); + } + + private unsafe bool ColorTablePasteFromClipboardContext(int rowIdx, bool disabled) + { + if (!disabled && ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + + using var context = ImUtf8.Popup("context"u8); + if (!context) + return false; + + using var _ = ImRaii.Disabled(disabled); + + IColorTable.ValueTypes copy = 0; + IColorDyeTable.ValueTypes dyeCopy = 0; + if (ImUtf8.Selectable("Import Colors Only"u8)) + { + copy = IColorTable.ValueTypes.Colors; + dyeCopy = IColorDyeTable.ValueTypes.Colors; + } + + if (ImUtf8.Selectable("Import Other Values Only"u8)) + { + copy = ~IColorTable.ValueTypes.Colors; + dyeCopy = ~IColorDyeTable.ValueTypes.Colors; + } + + if (copy == 0) return false; try { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - var row = Mtrl.Table.RowAsBytes(rowIdx); + var row = Mtrl.Table!.RowAsHalves(rowIdx); + var halves = new Span(Unsafe.AsPointer(ref data[0]), row.Length); var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; - if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + if (!Mtrl.Table.MergeSpecificValues(row, halves, copy)) return false; - data.AsSpan(0, row.Length).TryCopyTo(row); - data.AsSpan(row.Length).TryCopyTo(dyeRow); + Mtrl.DyeTable?.MergeSpecificValues(dyeRow, data.AsSpan(row.Length * 2), dyeCopy); UpdateColorTableRowPreview(rowIdx); - return true; } catch From f679e0cceeba9751f216c7c82734a25c71945da8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 Dec 2024 21:58:43 +0100 Subject: [PATCH 2082/2451] Fix some imgui assertions. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 215e0172..d9caded5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 215e01722a319c70b271dd23a40d99edc3fc197e +Subproject commit d9caded5efb7c9db0a273a43bb5f6d53cf4ace7f diff --git a/Penumbra.GameData b/Penumbra.GameData index 6848397d..ffc149cc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6848397dd77cfcdbff1accd860d5b7e95f8c9fe5 +Subproject commit ffc149cc8c169c2c6e838cbd138676f6fe4daeea From b3883c1306ed9b1b0a90699353efb2bf9f1bfdfa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 Dec 2024 00:06:51 +0100 Subject: [PATCH 2083/2451] Add handling for cached TMBs. --- Penumbra.GameData | 2 +- Penumbra/Communication/ResolvedFileChanged.cs | 4 +- Penumbra/Interop/GameState.cs | 3 + .../Animation/GetCachedScheduleResource.cs | 53 +++++++ .../Interop/Hooks/Animation/LoadActionTmb.cs | 55 +++++++ Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../SchedulerResourceManagementService.cs | 92 ++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 109 +++++++++----- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 135 +++++++++++++++++- 9 files changed, 415 insertions(+), 40 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs create mode 100644 Penumbra/Interop/Services/SchedulerResourceManagementService.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ffc149cc..19355cfa 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ffc149cc8c169c2c6e838cbd138676f6fe4daeea +Subproject commit 19355cfa0ec80e8d5a91de11ecffc49257b37b53 diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 75444340..0c91a18b 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -1,6 +1,5 @@ using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; @@ -33,5 +32,8 @@ public sealed class ResolvedFileChanged() { /// DalamudSubstitutionProvider = 0, + + /// + SchedulerResourceManagementService = 0, } } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 7e7abcd8..f80ef696 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -49,6 +49,9 @@ public class GameState : IService public void RestoreAnimationData(ResolveData old) => _animationLoadData.Value = old; + public readonly ThreadLocal InLoadActionTmb = new(() => false); + public readonly ThreadLocal SkipTmbCache = new(() => false); + #endregion #region Sound Data diff --git a/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs b/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs new file mode 100644 index 00000000..6ce1f899 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs @@ -0,0 +1,53 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using JetBrains.Annotations; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a cached TMB resource from SchedulerResourceManagement. +public sealed unsafe class GetCachedScheduleResource : FastHook +{ + private readonly GameState _state; + + public GetCachedScheduleResource(HookManager hooks, GameState state) + { + _state = state; + Task = hooks.CreateHook("Get Cached Schedule Resource", Sigs.GetCachedScheduleResource, Detour, + !HookOverrides.Instance.Animation.GetCachedScheduleResource); + } + + public delegate SchedulerResource* Delegate(SchedulerResourceManagement* a, ScheduleResourceLoadData* b, byte useMap); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private SchedulerResource* Detour(SchedulerResourceManagement* a, ScheduleResourceLoadData* b, byte c) + { + if (_state.SkipTmbCache.Value) + { + Penumbra.Log.Verbose( + $"[GetCachedScheduleResource] Called with 0x{(ulong)a:X}, {b->Id}, {new CiByteString(b->Path, MetaDataComputation.None)}, {c} from LoadActionTmb with forced skipping of cache, returning NULL."); + return null; + } + + var ret = Task.Result.Original(a, b, c); + Penumbra.Log.Excessive( + $"[GetCachedScheduleResource] Called with 0x{(ulong)a:X}, {b->Id}, {new CiByteString(b->Path, MetaDataComputation.None)}, {c}, returning 0x{(ulong)ret:X} ({(ret != null && Resource(ret) != null ? Resource(ret)->FileName().ToString() : "No Path")})."); + return ret; + } + + public struct ScheduleResourceLoadData + { + [UsedImplicitly] + public byte* Path; + + [UsedImplicitly] + public uint Id; + } + + + // #TODO: remove when fixed in CS. + public static ResourceHandle* Resource(SchedulerResource* r) + => ((ResourceHandle**)r)[3]; +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs b/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs new file mode 100644 index 00000000..457465d2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Services; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a Action TMB. +public sealed unsafe class LoadActionTmb : FastHook +{ + private readonly GameState _state; + private readonly SchedulerResourceManagementService _scheduler; + + public LoadActionTmb(HookManager hooks, GameState state, SchedulerResourceManagementService scheduler) + { + _state = state; + _scheduler = scheduler; + Task = hooks.CreateHook("Load Action TMB", Sigs.LoadActionTmb, Detour, !HookOverrides.Instance.Animation.LoadActionTmb); + } + + public delegate SchedulerResource* Delegate(SchedulerResourceManagement* scheduler, + GetCachedScheduleResource.ScheduleResourceLoadData* loadData, nint b, byte c, byte d, byte e); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private SchedulerResource* Detour(SchedulerResourceManagement* scheduler, GetCachedScheduleResource.ScheduleResourceLoadData* loadData, + nint b, byte c, byte d, byte e) + { + _state.InLoadActionTmb.Value = true; + SchedulerResource* ret; + if (ShouldSkipCache(loadData)) + { + _state.SkipTmbCache.Value = true; + ret = Task.Result.Original(scheduler, loadData, b, c, d, 1); + Penumbra.Log.Verbose( + $"[LoadActionTMB] Called with 0x{(ulong)scheduler:X}, {loadData->Id}, {new CiByteString(loadData->Path, MetaDataComputation.None)}, 0x{b:X}, {c}, {d}, {e}, forced no-cache use, returned 0x{(ulong)ret:X} ({(ret != null && GetCachedScheduleResource.Resource(ret) != null ? GetCachedScheduleResource.Resource(ret)->FileName().ToString() : "No Path")})."); + _state.SkipTmbCache.Value = false; + } + else + { + ret = Task.Result.Original(scheduler, loadData, b, c, d, e); + Penumbra.Log.Excessive( + $"[LoadActionTMB] Called with 0x{(ulong)scheduler:X}, {loadData->Id}, {new CiByteString(loadData->Path)}, 0x{b:X}, {c}, {d}, {e}, returned 0x{(ulong)ret:X} ({(ret != null && GetCachedScheduleResource.Resource(ret) != null ? GetCachedScheduleResource.Resource(ret)->FileName().ToString() : "No Path")})."); + } + + _state.InLoadActionTmb.Value = false; + + return ret; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool ShouldSkipCache(GetCachedScheduleResource.ScheduleResourceLoadData* loadData) + => _scheduler.Contains(loadData->Id); +} diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 63d93c9d..2aeeb14b 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -43,6 +43,8 @@ public class HookOverrides public bool SomeMountAnimation; public bool SomePapLoad; public bool SomeParasolAnimation; + public bool GetCachedScheduleResource; + public bool LoadActionTmb; } public struct MetaHooks diff --git a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs new file mode 100644 index 00000000..1d56fcdb --- /dev/null +++ b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs @@ -0,0 +1,92 @@ +using System.Collections.Frozen; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using Lumina.Excel.Sheets; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.Mods.Editor; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public unsafe class SchedulerResourceManagementService : IService, IDisposable +{ + private static readonly CiByteString TmbExtension = new(".tmb"u8, MetaDataComputation.All); + private static readonly CiByteString FolderPrefix = new("chara/action/"u8, MetaDataComputation.All); + + private readonly CommunicatorService _communicator; + private readonly FrozenDictionary _actionTmbs; + + private readonly ConcurrentDictionary _listedTmbIds = []; + + public bool Contains(uint tmbId) + => _listedTmbIds.ContainsKey(tmbId); + + public IReadOnlyDictionary ListedTmbs + => _listedTmbIds; + + public IReadOnlyDictionary ActionTmbs + => _actionTmbs; + + public SchedulerResourceManagementService(IGameInteropProvider interop, CommunicatorService communicator, IDataManager dataManager) + { + _communicator = communicator; + _actionTmbs = CreateActionTmbs(dataManager); + _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.SchedulerResourceManagementService); + interop.InitializeFromAttributes(this); + } + + private void OnResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath gamePath, FullPath oldPath, + FullPath newPath, IMod? mod) + { + switch (type) + { + case ResolvedFileChanged.Type.Added: + CheckFile(gamePath); + return; + case ResolvedFileChanged.Type.FullRecomputeFinished: + foreach (var path in collection.ResolvedFiles.Keys) + CheckFile(path); + return; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void CheckFile(Utf8GamePath gamePath) + { + if (!gamePath.Extension().Equals(TmbExtension)) + return; + + if (!gamePath.Path.StartsWith(FolderPrefix)) + return; + + var tmb = gamePath.Path.Substring(FolderPrefix.Length, gamePath.Length - FolderPrefix.Length - TmbExtension.Length).Clone(); + if (_actionTmbs.TryGetValue(tmb, out var rowId)) + _listedTmbIds[rowId] = tmb; + else + Penumbra.Log.Debug($"Action TMB {gamePath} encountered with no corresponding row ID."); + } + + [Signature(Sigs.SchedulerResourceManagementInstance, ScanType = ScanType.StaticAddress)] + public readonly SchedulerResourceManagement** Address = null; + + public SchedulerResourceManagement* Scheduler + => *Address; + + public void Dispose() + { + _listedTmbIds.Clear(); + _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange); + } + + private static FrozenDictionary CreateActionTmbs(IDataManager dataManager) + { + var sheet = dataManager.GetExcelSheet(); + return sheet.Where(row => !row.Key.IsEmpty).DistinctBy(row => row.Key).ToFrozenDictionary(row => new CiByteString(row.Key, MetaDataComputation.All).Clone(), row => row.RowId); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 30605101..125dbfa1 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -69,38 +69,39 @@ public class Diagnostics(ServiceManager provider) : IUiService public class DebugTab : Window, ITab, IUiService { - private readonly PerformanceTracker _performance; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; - private readonly ValidityChecker _validityChecker; - private readonly HttpApi _httpApi; - private readonly ActorManager _actors; - private readonly StainService _stains; - private readonly GlobalVariablesDrawer _globalVariablesDrawer; - private readonly ResourceManagerService _resourceManager; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; - private readonly PathState _pathState; - private readonly SubfileHelper _subfileHelper; - private readonly IdentifiedCollectionCache _identifiedCollectionCache; - private readonly CutsceneService _cutsceneService; - private readonly ModImportManager _modImporter; - private readonly ImportPopup _importPopup; - private readonly FrameworkManager _framework; - private readonly TextureManager _textureManager; - private readonly ShaderReplacementFixer _shaderReplacementFixer; - private readonly RedrawService _redraws; - private readonly DictEmote _emotes; - private readonly Diagnostics _diagnostics; - private readonly ObjectManager _objects; - private readonly IClientState _clientState; - private readonly IDataManager _dataManager; - private readonly IpcTester _ipcTester; - private readonly CrashHandlerPanel _crashHandlerPanel; - private readonly TexHeaderDrawer _texHeaderDrawer; - private readonly HookOverrideDrawer _hookOverrides; - private readonly RsfService _rsfService; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly ActorManager _actors; + private readonly StainService _stains; + private readonly GlobalVariablesDrawer _globalVariablesDrawer; + private readonly ResourceManagerService _resourceManager; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly PathState _pathState; + private readonly SubfileHelper _subfileHelper; + private readonly IdentifiedCollectionCache _identifiedCollectionCache; + private readonly CutsceneService _cutsceneService; + private readonly ModImportManager _modImporter; + private readonly ImportPopup _importPopup; + private readonly FrameworkManager _framework; + private readonly TextureManager _textureManager; + private readonly ShaderReplacementFixer _shaderReplacementFixer; + private readonly RedrawService _redraws; + private readonly DictEmote _emotes; + private readonly Diagnostics _diagnostics; + private readonly ObjectManager _objects; + private readonly IClientState _clientState; + private readonly IDataManager _dataManager; + private readonly IpcTester _ipcTester; + private readonly CrashHandlerPanel _crashHandlerPanel; + private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly HookOverrideDrawer _hookOverrides; + private readonly RsfService _rsfService; + private readonly SchedulerResourceManagementService _schedulerService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -110,7 +111,8 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer) + HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, + SchedulerResourceManagementService schedulerService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -148,6 +150,7 @@ public class DebugTab : Window, ITab, IUiService _hookOverrides = hookOverrides; _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; + _schedulerService = schedulerService; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -672,6 +675,22 @@ public class DebugTab : Window, ITab, IUiService } } } + + using (var tmbCache = TreeNode("TMB Cache")) + { + if (tmbCache) + { + using var table = Table("###TmbTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key)) + { + ImUtf8.DrawTableColumn($"{id:D6}"); + ImUtf8.DrawTableColumn(name.Span); + } + } + } + } } private void DrawData() @@ -680,6 +699,7 @@ public class DebugTab : Window, ITab, IUiService return; DrawEmotes(); + DrawActionTmbs(); DrawStainTemplates(); DrawAtch(); } @@ -739,6 +759,27 @@ public class DebugTab : Window, ITab, IUiService ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); } + private void DrawActionTmbs() + { + using var mainTree = TreeNode("Action TMBs"); + if (!mainTree) + return; + + using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); + if (!table) + return; + + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + var dummy = ImGuiClip.ClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + p => + { + ImUtf8.DrawTableColumn($"{p.Value}"); + ImUtf8.DrawTableColumn(p.Key.Span); + }); + ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); + } + private void DrawStainTemplates() { using var mainTree = TreeNode("Staining Templates"); @@ -1061,7 +1102,7 @@ public class DebugTab : Window, ITab, IUiService } ImUtf8.HoverTooltip("Click to copy address to clipboard."u8); - } + } public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) => DrawCopyableAddress(label, (void*)address); diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 601e9b4d..4e6cf62c 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -1,12 +1,23 @@ +using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using FFXIVClientStructs.Interop; +using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.String; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.UI.Tabs.Debug; -public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, ResidentResourceManager residentResources) : IUiService +public unsafe class GlobalVariablesDrawer( + CharacterUtility characterUtility, + ResidentResourceManager residentResources, + SchedulerResourceManagementService scheduler) : IUiService { /// Draw information about some game global variables. public void Draw() @@ -16,13 +27,22 @@ public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, Res if (!header) return; - DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); - DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); - DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + var actionManager = (ActionTimelineManager**)ActionTimelineManager.Instance(); + DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); + DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); + DebugTab.DrawCopyableAddress("ScheduleManagement"u8, ScheduleManagement.Instance()); + DebugTab.DrawCopyableAddress("ActionTimelineManager*"u8, actionManager); + DebugTab.DrawCopyableAddress("ActionTimelineManager"u8, actionManager != null ? *actionManager : null); + DebugTab.DrawCopyableAddress("SchedulerResourceManagement*"u8, scheduler.Address); + DebugTab.DrawCopyableAddress("SchedulerResourceManagement"u8, scheduler.Address != null ? *scheduler.Address : null); + DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); DrawCharacterUtility(); DrawResidentResources(); + DrawSchedulerResourcesMap(); + DrawSchedulerResourcesList(); } + /// /// Draw information about the character utility class from SE, /// displaying all files, their sizes, the default files and the default sizes. @@ -123,4 +143,111 @@ public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, Res ImUtf8.DrawTableColumn(length.ToString()); } } + + private string _schedulerFilterList = string.Empty; + private string _schedulerFilterMap = string.Empty; + private CiByteString _schedulerFilterListU8 = CiByteString.Empty; + private CiByteString _schedulerFilterMapU8 = CiByteString.Empty; + private int _shownResourcesList = 0; + private int _shownResourcesMap = 0; + + private void DrawSchedulerResourcesMap() + { + using var tree = ImUtf8.TreeNode("Scheduler Resources (Map)"u8); + if (!tree) + return; + + if (scheduler.Address == null || scheduler.Scheduler == null) + return; + + if (ImUtf8.InputText("##SchedulerMapFilter"u8, ref _schedulerFilterMap, "Filter..."u8)) + _schedulerFilterMapU8 = CiByteString.FromString(_schedulerFilterMap, out var t, MetaDataComputation.All, false) + ? t + : CiByteString.Empty; + ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->NumResources}"); + using var table = ImUtf8.Table("##SchedulerMapResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var map = (StdMap>*)&scheduler.Scheduler->Unknown; + var total = 0; + _shownResourcesMap = 0; + foreach (var (key, resourcePtr) in *map) + { + var resource = resourcePtr.Value; + if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMapU8.Span) >= 0) + { + ImUtf8.DrawTableColumn($"[{total:D4}]"); + ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); + ImUtf8.DrawTableColumn($"{resource->Consumers}"); + ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + var resourceHandle = *((ResourceHandle**)resource + 3); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); + ImGui.TableNextColumn(); + uint dataLength = 0; + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + ImUtf8.DrawTableColumn($"{dataLength}"); + ++_shownResourcesMap; + } + + ++total; + } + } + + private void DrawSchedulerResourcesList() + { + using var tree = ImUtf8.TreeNode("Scheduler Resources (List)"u8); + if (!tree) + return; + + if (scheduler.Address == null || scheduler.Scheduler == null) + return; + + if (ImUtf8.InputText("##SchedulerListFilter"u8, ref _schedulerFilterList, "Filter..."u8)) + _schedulerFilterListU8 = CiByteString.FromString(_schedulerFilterList, out var t, MetaDataComputation.All, false) + ? t + : CiByteString.Empty; + ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->NumResources}"); + using var table = ImUtf8.Table("##SchedulerListResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var resource = scheduler.Scheduler->Begin; + var total = 0; + _shownResourcesList = 0; + while (resource != null && total < (int)scheduler.Scheduler->NumResources) + { + if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) + { + ImUtf8.DrawTableColumn($"[{total:D4}]"); + ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); + ImUtf8.DrawTableColumn($"{resource->Consumers}"); + ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + var resourceHandle = *((ResourceHandle**)resource + 3); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); + ImGui.TableNextColumn(); + uint dataLength = 0; + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + ImUtf8.DrawTableColumn($"{dataLength}"); + ++_shownResourcesList; + } + + resource = resource->Previous; + ++total; + } + } } From f24056ea3101a4ead083ea26a27caab23d77fb8a Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 25 Dec 2024 23:08:52 +0000 Subject: [PATCH 2084/2451] [CI] Updating repo.json for testing_1.3.2.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c0e561da..97e55af0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.2.0", - "TestingAssemblyVersion": "1.3.2.0", + "TestingAssemblyVersion": "1.3.2.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 25d0a2c9a83323336bbff7865e3cd054b5488075 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 29 Dec 2024 23:04:32 +0100 Subject: [PATCH 2085/2451] Fix issue with ring IMCs in resource tree. --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 0c36b745..bdf66a16 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -161,7 +161,7 @@ internal partial record ResolveContext return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists); if (!exists) return variant.Id; From 0e2364497f2b16fbfd23aa0b6d9bfac9825a1da7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 Dec 2024 00:33:46 +0100 Subject: [PATCH 2086/2451] Maybe fix mtrl file issues. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19355cfa..33de79bc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19355cfa0ec80e8d5a91de11ecffc49257b37b53 +Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c From 2483f3dcdf776a1255b9400f1d5f26ea719cc5e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:08:10 +0100 Subject: [PATCH 2087/2451] Add Temporary Settings class --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Penumbra/Mods/Settings/TemporaryModSettings.cs diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs new file mode 100644 index 00000000..a0cdc2bb --- /dev/null +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Mods.Settings; + +public sealed class TemporaryModSettings : ModSettings +{ + public string Source = string.Empty; + public int Lock = 0; + public bool ForceInherit; +} From 50b5eeb700fab8d40880a8fa1839cfe598e0b5bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:08:35 +0100 Subject: [PATCH 2088/2451] Add FullModSettings struct. --- Penumbra/Mods/Settings/FullModSettings.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Penumbra/Mods/Settings/FullModSettings.cs diff --git a/Penumbra/Mods/Settings/FullModSettings.cs b/Penumbra/Mods/Settings/FullModSettings.cs new file mode 100644 index 00000000..904b56bd --- /dev/null +++ b/Penumbra/Mods/Settings/FullModSettings.cs @@ -0,0 +1,19 @@ +namespace Penumbra.Mods.Settings; + +public readonly record struct FullModSettings(ModSettings? Settings = null, TemporaryModSettings? TempSettings = null) +{ + public static readonly FullModSettings Empty = new(); + + public ModSettings? Resolve() + { + if (TempSettings == null) + return Settings; + if (TempSettings.ForceInherit) + return null; + + return TempSettings; + } + + public FullModSettings DeepCopy() + => new(Settings?.DeepCopy()); +} From 7a2691b9429cf801981389207ef563c5240b004a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:23:29 +0100 Subject: [PATCH 2089/2451] Add colors for temporary settings. --- Penumbra/UI/Classes/Colors.cs | 64 ++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index d135e10c..0389730d 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -31,6 +31,10 @@ public enum ColorId ResTreeNonNetworked, PredefinedTagAdd, PredefinedTagRemove, + TemporaryEnabledMod, + TemporaryDisabledMod, + TemporaryInheritedMod, + TemporaryInheritedDisabledMod, } public static class Colors @@ -52,34 +56,38 @@ public static class Colors => color switch { // @formatter:off - ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), - ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), - ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), - ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), - ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), - ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), - ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), - ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), - ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), - ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), - ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), - ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), - ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), - ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), - ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."), - ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), - ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), - ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), - ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), - ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), - ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), - ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), - ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), - ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), - ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), - ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), - ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), - _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), + ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), + ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."), + ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), + ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), + ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), + ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), + ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), + ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), + ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), + ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), + ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), + ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), + ColorId.TemporaryEnabledMod => ( 0xFFFFC0A0, "Mod Enabled By Temporary Settings", "A mod that is enabled by temporary settings in the currently selected collection." ), + ColorId.TemporaryDisabledMod => ( 0xFFB08070, "Mod Disabled By Temporary Settings", "A mod that is disabled by temporary settings in the currently selected collection." ), + ColorId.TemporaryInheritedMod => ( 0xFFE8FFB0, "Mod Enabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + ColorId.TemporaryInheritedDisabledMod => ( 0xFF90A080, "Mod Disabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; From fbbfe5e00d8b53b2e103c6bf29f7ea870f348b0a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:39:24 +0100 Subject: [PATCH 2090/2451] Extract collection counters. --- Penumbra/Collections/Cache/AtchCache.cs | 5 ++-- Penumbra/Collections/Cache/CollectionCache.cs | 8 +++--- .../Cache/CollectionCacheManager.cs | 4 +-- Penumbra/Collections/Cache/ImcCache.cs | 4 +-- Penumbra/Collections/CollectionCounters.cs | 28 +++++++++++++++++++ .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/TempCollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.cs | 15 ++-------- .../Interop/PathResolving/PathDataHandler.cs | 8 +++--- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 +-- 10 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 Penumbra/Collections/CollectionCounters.cs diff --git a/Penumbra/Collections/Cache/AtchCache.cs b/Penumbra/Collections/Cache/AtchCache.cs index 9e0f6caf..10990553 100644 --- a/Penumbra/Collections/Cache/AtchCache.cs +++ b/Penumbra/Collections/Cache/AtchCache.cs @@ -2,7 +2,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; @@ -37,7 +36,7 @@ public sealed class AtchCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry) { - ++Collection.AtchChangeCounter; + Collection.Counters.IncrementAtch(); ApplyFile(identifier, entry); } @@ -68,7 +67,7 @@ public sealed class AtchCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(AtchIdentifier identifier) { - ++Collection.AtchChangeCounter; + Collection.Counters.IncrementAtch(); if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) return; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 64cf54ea..bc431e88 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -177,7 +177,7 @@ public sealed class CollectionCache : IDisposable var (paths, manipulations) = ModData.RemoveMod(mod); if (addMetaChanges) - _collection.IncrementCounter(); + _collection.Counters.IncrementChange(); foreach (var path in paths) { @@ -250,7 +250,7 @@ public sealed class CollectionCache : IDisposable if (addMetaChanges) { - _collection.IncrementCounter(); + _collection.Counters.IncrementChange(); _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -408,12 +408,12 @@ public sealed class CollectionCache : IDisposable // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { - if (_changedItemsSaveCounter == _collection.ChangeCounter) + if (_changedItemsSaveCounter == _collection.Counters.Change) return; try { - _changedItemsSaveCounter = _collection.ChangeCounter; + _changedItemsSaveCounter = _collection.Counters.Change; _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index a3b6bb83..c3e00502 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -187,7 +187,7 @@ public class CollectionCacheManager : IDisposable, IService foreach (var mod in _modStorage) cache.AddModSync(mod, false); - collection.IncrementCounter(); + collection.Counters.IncrementChange(); MetaFileManager.ApplyDefaultFiles(collection); ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty, @@ -297,7 +297,7 @@ public class CollectionCacheManager : IDisposable, IService private void IncrementCounters() { foreach (var collection in _storage.Where(c => c.HasCache)) - collection.IncrementCounter(); + collection.Counters.IncrementChange(); MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index cac52f99..0f610d90 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -39,7 +39,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { - ++Collection.ImcChangeCounter; + Collection.Counters.IncrementImc(); ApplyFile(identifier, entry); } @@ -71,7 +71,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { - ++Collection.ImcChangeCounter; + Collection.Counters.IncrementImc(); var path = identifier.GamePath().Path; if (!_imcFiles.TryGetValue(path, out var pair)) return; diff --git a/Penumbra/Collections/CollectionCounters.cs b/Penumbra/Collections/CollectionCounters.cs new file mode 100644 index 00000000..91d240d6 --- /dev/null +++ b/Penumbra/Collections/CollectionCounters.cs @@ -0,0 +1,28 @@ +namespace Penumbra.Collections; + +public struct CollectionCounters(int changeCounter) +{ + /// Count the number of changes of the effective file list. + public int Change { get; private set; } = changeCounter; + + /// Count the number of IMC-relevant changes of the effective file list. + public int Imc { get; private set; } + + /// Count the number of ATCH-relevant changes of the effective file list. + public int Atch { get; private set; } + + /// Increment the number of changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementChange() + => ++Change; + + /// Increment the number of IMC-relevant changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementImc() + => ++Imc; + + /// Increment the number of ATCH-relevant changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementAtch() + => ++Imc; +} diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index a326fb92..cdbe11dc 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -372,7 +372,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { var (settings, _) = collection[mod.Index]; if (settings is { Enabled: true }) - collection.IncrementCounter(); + collection.Counters.IncrementChange(); } } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 5c893232..e5b844c8 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -75,7 +75,7 @@ public class TempCollectionManager : IDisposable, IService _storage.Delete(collection); Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); - GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); + GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { if (Collections[i].Collection != collection) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index db9c19cb..95e78da0 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -50,18 +50,7 @@ public partial class ModCollection /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } - /// - /// Count the number of changes of the effective file list. - /// This is used for material and imc changes. - /// - public int ChangeCounter { get; private set; } - - public uint ImcChangeCounter { get; set; } - public uint AtchChangeCounter { get; set; } - - /// Increment the number of changes in the effective file list. - public int IncrementCounter() - => ++ChangeCounter; + public CollectionCounters Counters; /// /// If a ModSetting is null, it can be inherited from other collections. @@ -213,7 +202,7 @@ public partial class ModCollection Id = id; LocalId = localId; Index = index; - ChangeCounter = changeCounter; + Counters = new CollectionCounters(changeCounter); Settings = appliedSettings; UnusedSettings = settings; DirectlyInheritsFrom = inheritsFrom; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 5439151f..25d4f7ea 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -47,17 +47,17 @@ public static class PathDataHandler /// Create the encoding path for an ATCH file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateAtch(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.AtchChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) - => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static FullPath CreateBase(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 125dbfa1..95afb10f 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -204,7 +204,7 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.Name} (Change Counter {collection.ChangeCounter})###{collection.Name}"); + using var node = TreeNode($"{collection.Name} (Change Counter {collection.Counters.Change})###{collection.Name}"); if (!node) continue; @@ -239,7 +239,7 @@ public class DebugTab : Window, ITab, IUiService else { using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", + TreeNode($"{collection.AnonymizedName} (Change Counter {collection.Counters.Change})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } From 67305d507a4e496202a66096050a9ffadedc5f52 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 11:36:30 +0100 Subject: [PATCH 2091/2451] Extract ModCollectionIdentity. --- OtterGui | 2 +- Penumbra/Api/Api/CollectionApi.cs | 32 +++++----- Penumbra/Api/Api/GameStateApi.cs | 4 +- Penumbra/Api/Api/ModSettingsApi.cs | 6 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 6 +- Penumbra/Api/TempModManager.cs | 4 +- Penumbra/Collections/Cache/CollectionCache.cs | 2 +- .../Cache/CollectionCacheManager.cs | 36 +++++------ .../Collections/CollectionAutoSelector.cs | 2 +- .../Manager/ActiveCollectionMigration.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 46 +++++++------- .../Collections/Manager/CollectionStorage.cs | 36 +++++------ .../Manager/IndividualCollections.Files.cs | 10 +-- .../Collections/Manager/InheritanceManager.cs | 14 ++--- .../Manager/TempCollectionManager.cs | 16 ++--- Penumbra/Collections/ModCollection.cs | 63 ++++++------------- Penumbra/Collections/ModCollectionIdentity.cs | 42 +++++++++++++ Penumbra/Collections/ModCollectionSave.cs | 16 ++--- Penumbra/Collections/ResolveData.cs | 2 +- Penumbra/CommandHandler.cs | 22 +++---- .../Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- .../Interop/Hooks/Meta/AtchCallerHook2.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathDataHandler.cs | 8 +-- .../Processing/ImcFilePostProcessor.cs | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 10 +-- Penumbra/Penumbra.cs | 12 ++-- Penumbra/Services/ConfigMigrationService.cs | 8 +-- Penumbra/Services/CrashHandlerService.cs | 4 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 25 ++++---- Penumbra/UI/CollectionTab/CollectionCombo.cs | 4 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 16 ++--- .../UI/CollectionTab/CollectionSelector.cs | 6 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 10 +-- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 6 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../ResourceWatcher/ResourceWatcherTable.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 22 +++---- Penumbra/UI/Tabs/ModsTab.cs | 4 +- Penumbra/UI/TutorialService.cs | 6 +- 43 files changed, 270 insertions(+), 252 deletions(-) create mode 100644 Penumbra/Collections/ModCollectionIdentity.cs diff --git a/OtterGui b/OtterGui index d9caded5..fcc96daa 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d9caded5efb7c9db0a273a43bb5f6d53cf4ace7f +Subproject commit fcc96daa02633f673325c14aeea6b6b568924f1e diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index 04299187..964da1a5 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -8,7 +8,7 @@ namespace Penumbra.Api.Api; public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService { public Dictionary GetCollections() - => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + => collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name); public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier) { @@ -17,14 +17,14 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : var list = new List<(Guid Id, string Name)>(4); if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty) - list.Add((collection.Id, collection.Name)); + list.Add((collection.Identity.Id, collection.Identity.Name)); else if (identifier.Length >= 8) - list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) - .Select(c => (c.Id, c.Name))); + list.AddRange(collections.Storage.Where(c => c.Identity.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) + .Select(c => (c.Identity.Id, c.Identity.Name))); list.AddRange(collections.Storage - .Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name))) - .Select(c => (c.Id, c.Name))); + .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Identity.Id, c.Identity.Name))) + .Select(c => (c.Identity.Id, c.Identity.Name))); return list; } @@ -54,7 +54,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : return null; var collection = collections.Active.ByType((CollectionType)type); - return collection == null ? null : (collection.Id, collection.Name); + return collection == null ? null : (collection.Identity.Id, collection.Identity.Name); } internal (Guid Id, string Name)? GetCollection(byte type) @@ -64,17 +64,17 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : { var id = helpers.AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name)); + return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name)); if (collections.Active.Individuals.TryGetValue(id, out var collection)) - return (true, true, (collection.Id, collection.Name)); + return (true, true, (collection.Identity.Id, collection.Identity.Name)); helpers.AssociatedCollection(gameObjectIdx, out collection); - return (true, false, (collection.Id, collection.Name)); + return (true, false, (collection.Identity.Id, collection.Identity.Name)); } public Guid[] GetCollectionByName(string name) - => collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray(); + => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id).ToArray(); public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, bool allowCreateNew, bool allowDelete) @@ -83,7 +83,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : return (PenumbraApiEc.InvalidArgument, null); var oldCollection = collections.Active.ByType((CollectionType)type); - var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?(); if (collectionId == null) { if (old == null) @@ -106,7 +106,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : collections.Active.CreateSpecialCollection((CollectionType)type); } - else if (old.Value.Item1 == collection.Id) + else if (old.Value.Item1 == collection.Identity.Id) { return (PenumbraApiEc.NothingChanged, old); } @@ -120,10 +120,10 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : { var id = helpers.AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name)); + return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name)); var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null; - var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?(); if (collectionId == null) { if (old == null) @@ -148,7 +148,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : var ids = collections.Active.Individuals.GetGroup(id); collections.Active.CreateIndividualCollection(ids); } - else if (old.Value.Item1 == collection.Id) + else if (old.Value.Item1 == collection.Identity.Id) { return (PenumbraApiEc.NothingChanged, old); } diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index c2cae32b..7f70c6bf 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -61,7 +61,7 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject) { var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name)); + return (data.AssociatedGameObject, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name)); } public int GetCutsceneParentIndex(int actorIdx) @@ -93,5 +93,5 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable } private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) - => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject); + => CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject); } diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 8c34c249..3dc900fc 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -77,7 +77,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Id == Guid.Empty + var settings = collection.Identity.Id == Guid.Empty ? null : ignoreInheritance ? collection.Settings[mod.Index] @@ -217,7 +217,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var collection = _collectionResolver.PlayerCollection(); var (settings, parent) = collection[mod.Index]; if (settings is { Enabled: true }) - ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection); + ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection); } private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) @@ -227,7 +227,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); + => ModSettingChanged?.Invoke(type, collection.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited); private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index f6d1c9eb..f3c23831 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -146,10 +146,10 @@ public class TemporaryIpcTester( using (ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(collection.Identifier); + ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier); } - ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.Identity.Name); ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); ImGuiUtil.DrawTableColumn(string.Join(", ", @@ -199,7 +199,7 @@ public class TemporaryIpcTester( { PrintList("All", tempMods.ModsForAllCollections); foreach (var (collection, list) in tempMods.Mods) - PrintList(collection.Name, list); + PrintList(collection.Identity.Name, list); } } } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 0b52e64a..b3c6066a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -85,13 +85,13 @@ public class TempModManager : IDisposable, IService { if (removed) { - Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.Identity.AnonymizedName}."); collection.Remove(mod); _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false); } else { - Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.Identity.AnonymizedName}."); collection.Apply(mod, created); _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index bc431e88..ad902aac 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -31,7 +31,7 @@ public sealed class CollectionCache : IDisposable public int Calculating = -1; public string AnonymizedName - => _collection.AnonymizedName; + => _collection.Identity.AnonymizedName; public IEnumerable> AllConflicts => ConflictDict.Values; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c3e00502..0a851154 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -114,16 +114,16 @@ public class CollectionCacheManager : IDisposable, IService /// Only creates a new cache, does not update an existing one. public bool CreateCache(ModCollection collection) { - if (collection.Index == ModCollection.Empty.Index) + if (collection.Identity.Index == ModCollection.Empty.Identity.Index) return false; if (collection._cache != null) return false; collection._cache = new CollectionCache(this, collection); - if (collection.Index > 0) + if (collection.Identity.Index > 0) Interlocked.Increment(ref _count); - Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}."); return true; } @@ -132,32 +132,32 @@ public class CollectionCacheManager : IDisposable, IService /// Does not create caches. /// public void CalculateEffectiveFileList(ModCollection collection) - => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier, + => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier, () => CalculateEffectiveFileListInternal(collection)); private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. - if (collection.Index == 0) + if (collection.Identity.Index == 0) return; - Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); + Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}"); if (!collection.HasCache) { Penumbra.Log.Error( - $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists."); } else if (collection._cache!.Calculating != -1) { Penumbra.Log.Error( - $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); } else { FullRecalculation(collection); Penumbra.Log.Debug( - $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); + $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished."); } } @@ -213,7 +213,7 @@ public class CollectionCacheManager : IDisposable, IService else { RemoveCache(old); - if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection)) + if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection)) CalculateEffectiveFileList(newCollection); if (type is CollectionType.Default) @@ -258,12 +258,12 @@ public class CollectionCacheManager : IDisposable, IService private void RemoveCache(ModCollection? collection) { if (collection != null - && collection.Index > ModCollection.Empty.Index - && collection.Index != _active.Default.Index - && collection.Index != _active.Interface.Index - && collection.Index != _active.Current.Index - && _active.SpecialAssignments.All(c => c.Value.Index != collection.Index) - && _active.Individuals.All(c => c.Collection.Index != collection.Index)) + && collection.Identity.Index > ModCollection.Empty.Identity.Index + && collection.Identity.Index != _active.Default.Identity.Index + && collection.Identity.Index != _active.Interface.Identity.Index + && collection.Identity.Index != _active.Current.Identity.Index + && _active.SpecialAssignments.All(c => c.Value.Identity.Index != collection.Identity.Index) + && _active.Individuals.All(c => c.Collection.Identity.Index != collection.Identity.Index)) ClearCache(collection); } @@ -359,9 +359,9 @@ public class CollectionCacheManager : IDisposable, IService collection._cache!.Dispose(); collection._cache = null; - if (collection.Index > 0) + if (collection.Identity.Index > 0) Interlocked.Decrement(ref _count); - Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}."); } /// diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs index e24fa6a9..68dac914 100644 --- a/Penumbra/Collections/CollectionAutoSelector.cs +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -59,7 +59,7 @@ public sealed class CollectionAutoSelector : IService, IDisposable return; var collection = _resolver.PlayerCollection(); - Penumbra.Log.Debug($"Setting current collection to {collection.Identifier} through automatic collection selection."); + Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); _collections.SetCollection(collection, CollectionType.Current); } diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 19f781fc..b4af0998 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -48,7 +48,7 @@ public static class ActiveCollectionMigration if (!storage.ByName(collectionName, out var collection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); dict.Add(player, ModCollection.Empty); } else diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 60f9a427..07fcb430 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -219,7 +219,7 @@ public class ActiveCollections : ISavable, IDisposable, IService _ => null, }; - if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count) + if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count) return; switch (collectionType) @@ -262,13 +262,13 @@ public class ActiveCollections : ISavable, IDisposable, IService var jObj = new JObject { { nameof(Version), Version }, - { nameof(Default), Default.Id }, - { nameof(Interface), Interface.Id }, - { nameof(Current), Current.Id }, + { nameof(Default), Default.Identity.Id }, + { nameof(Interface), Interface.Identity.Id }, + { nameof(Current), Current.Identity.Id }, }; foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null) .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Id); + jObj.Add(type.ToString(), collection.Identity.Id); jObj.Add(nameof(Individuals), Individuals.ToJObject()); using var j = new JsonTextWriter(writer); @@ -300,7 +300,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (oldCollection == Interface) SetCollection(ModCollection.Empty, CollectionType.Interface); if (oldCollection == Current) - SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); + SetCollection(Default.Identity.Index > ModCollection.Empty.Identity.Index ? Default : _storage.DefaultNamed, CollectionType.Current); for (var i = 0; i < SpecialCollections.Length; ++i) { @@ -325,11 +325,11 @@ public class ActiveCollections : ISavable, IDisposable, IService { var configChanged = false; // Load the default collection. If the name does not exist take the empty collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Name; + var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Identity.Name; if (!_storage.ByName(defaultName, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -340,11 +340,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the interface collection. If no string is set, use the name of whatever was set as Default. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Name; if (!_storage.ByName(interfaceName, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -355,11 +355,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; + var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Identity.Name; if (!_storage.ByName(currentName, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; @@ -404,7 +404,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ById(defaultId, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -415,11 +415,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the interface collection. If no string is set, use the name of whatever was set as Default. - var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Id; + var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Id; if (!_storage.ById(interfaceId, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -430,11 +430,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the current collection. - var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Id; + var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Identity.Id; if (!_storage.ById(currentId, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.", + $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; @@ -587,7 +587,7 @@ public class ActiveCollections : ISavable, IDisposable, IService case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: { var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index + return (global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." : string.Empty; } @@ -596,12 +596,12 @@ public class ActiveCollections : ISavable, IDisposable, IService { var global = ByType(CollectionType.Individual, _actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) + if ((global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index) return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; } var unowned = ByType(CollectionType.Individual, _actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index + return (unowned != null ? unowned.Identity.Index : null) == checkAssignment.Identity.Index ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." : string.Empty; } @@ -617,7 +617,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (maleNpc == null) { maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) + if (maleNpc.Identity.Index != checkAssignment.Identity.Index) return string.Empty; collection1 = CollectionType.Default; @@ -626,7 +626,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (femaleNpc == null) { femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) + if (femaleNpc.Identity.Index != checkAssignment.Identity.Index) return string.Empty; collection2 = CollectionType.Default; @@ -646,7 +646,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (assignment == null) continue; - if (assignment.Index == checkAssignment.Index) + if (assignment.Identity.Index == checkAssignment.Identity.Index) return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index cdbe11dc..2ed395ae 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -41,7 +41,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings, + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); _collectionsByLocal[CurrentCollectionId] = newCollection; CurrentCollectionId += 1; @@ -57,7 +57,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer } public void Delete(ModCollection collection) - => _collectionsByLocal.Remove(collection.LocalId); + => _collectionsByLocal.Remove(collection.Identity.LocalId); /// The empty collection is always available at Index 0. private readonly List _collections = @@ -92,7 +92,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) { if (name.Length != 0) - return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); collection = ModCollection.Empty; return true; @@ -102,7 +102,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) { if (id != Guid.Empty) - return _collections.FindFirst(c => c.Id == id, out collection); + return _collections.FindFirst(c => c.Identity.Id == id, out collection); collection = ModCollection.Empty; return true; @@ -158,7 +158,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer var newCollection = Create(name, _collections.Count, duplicate); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); - Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); + Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); return true; } @@ -168,13 +168,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// public bool RemoveCollection(ModCollection collection) { - if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) + if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) { Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); return false; } - if (collection.Index == DefaultNamed.Index) + if (collection.Identity.Index == DefaultNamed.Identity.Index) { Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); return false; @@ -182,13 +182,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer Delete(collection); _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); - _collections.RemoveAt(collection.Index); + _collections.RemoveAt(collection.Identity.Index); // Update indices. - for (var i = collection.Index; i < Count; ++i) - _collections[i].Index = i; - _collectionsByLocal.Remove(collection.LocalId); + for (var i = collection.Identity.Index; i < Count; ++i) + _collections[i].Identity.Index = i; + _collectionsByLocal.Remove(collection.Identity.LocalId); - Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false); + Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } @@ -246,13 +246,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { File.Move(file.FullName, correctName, false); Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.", NotificationType.Warning); } catch (Exception ex) { Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}", NotificationType.Warning); } } @@ -273,7 +273,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer catch (Exception e) { Penumbra.Messager.NotificationMessage(e, - $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.", NotificationType.Error); } @@ -291,14 +291,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// private ModCollection SetDefaultNamedCollection() { - if (ByName(ModCollection.DefaultCollectionName, out var collection)) + if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection)) return collection; - if (AddCollection(ModCollection.DefaultCollectionName, null)) + if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) return _collections[^1]; Penumbra.Messager.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", + $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index f7a26384..60e9fc5f 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -18,7 +18,7 @@ public partial class IndividualCollections foreach (var (name, identifiers, collection) in Assignments) { var tmp = identifiers[0].ToJson(); - tmp.Add("Collection", collection.Id); + tmp.Add("Collection", collection.Identity.Id); tmp.Add("Display", name); ret.Add(tmp); } @@ -182,7 +182,7 @@ public partial class IndividualCollections Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else Penumbra.Messager.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + $"Could not migrate {name} ({collection.Identity.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", NotificationType.Error); } // If it is not a valid NPC name, check if it can be a player name. @@ -192,16 +192,16 @@ public partial class IndividualCollections var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); // Try to migrate the player name without logging full names. if (Add($"{name} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", [identifier], collection)) - Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); + Penumbra.Log.Information($"Migrated {shortName} ({collection.Identity.AnonymizedName}) to Player Identifier."); else Penumbra.Messager.NotificationMessage( - $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + $"Could not migrate {shortName} ({collection.Identity.AnonymizedName}), please look through your individual collections.", NotificationType.Error); } else { Penumbra.Messager.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + $"Could not migrate {name} ({collection.Identity.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", NotificationType.Error); } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index bc1a362c..e003ad6b 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -89,7 +89,7 @@ public class InheritanceManager : IDisposable, IService _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); - Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); + Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances."); } /// Order in the inheritance list is relevant. @@ -101,7 +101,7 @@ public class InheritanceManager : IDisposable, IService _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); - Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); + Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}."); } /// @@ -119,7 +119,7 @@ public class InheritanceManager : IDisposable, IService RecurseInheritanceChanges(inheritor); } - Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances."); + Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances."); return true; } @@ -143,23 +143,23 @@ public class InheritanceManager : IDisposable, IService continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) { changes = true; - Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID."); + Penumbra.Log.Information($"Migrating inheritance for {collection.Identity.AnonymizedName} from name to GUID."); if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else { Penumbra.Messager.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", + $"Inherited collection {subCollectionName} for {collection.Identity.AnonymizedName} does not exist, it was removed.", NotificationType.Warning); changes = true; } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index e5b844c8..8aab5297 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -44,7 +44,7 @@ public class TempCollectionManager : IDisposable, IService => _customCollections.Values; public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection); + => _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection); public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) => _customCollections.TryGetValue(id, out collection); @@ -54,12 +54,12 @@ public class TempCollectionManager : IDisposable, IService if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++); - Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); - if (_customCollections.TryAdd(collection.Id, collection)) + Penumbra.Log.Debug($"Creating temporary collection {collection.Identity.Name} with {collection.Identity.Id}."); + if (_customCollections.TryAdd(collection.Identity.Id, collection)) { // Temporary collection created. _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty); - return collection.Id; + return collection.Identity.Id; } return Guid.Empty; @@ -74,7 +74,7 @@ public class TempCollectionManager : IDisposable, IService } _storage.Delete(collection); - Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); + Penumbra.Log.Debug($"Deleted temporary collection {collection.Identity.Id}."); GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { @@ -83,7 +83,7 @@ public class TempCollectionManager : IDisposable, IService // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); - Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}."); + Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}."); Collections.Delete(i--); } @@ -96,7 +96,7 @@ public class TempCollectionManager : IDisposable, IService return false; // Temporary collection assignment added. - Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}."); + Penumbra.Log.Verbose($"Assigned temporary collection {collection.Identity.AnonymizedName} to {Collections.Last().DisplayName}."); _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); return true; } @@ -127,6 +127,6 @@ public class TempCollectionManager : IDisposable, IService return false; var identifier = _actors.CreatePlayer(byteString, worldId); - return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id); + return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 95e78da0..9b33c1f4 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -13,42 +13,21 @@ namespace Penumbra.Collections; /// - Index is the collections index in the ModCollection.Manager /// - Settings has the same size as ModManager.Mods. /// - any change in settings or inheritance of the collection causes a Save. -/// - the name can not contain invalid path characters and has to be unique when lower-cased. /// public partial class ModCollection { - public const int CurrentVersion = 2; - public const string DefaultCollectionName = "Default"; - public const string EmptyCollectionName = "None"; + public const int CurrentVersion = 2; /// /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, [], [], []); - /// The name of a collection. - public string Name { get; set; } - - public Guid Id { get; } - - public LocalCollectionId LocalId { get; } - - public string Identifier - => Id.ToString(); - - public string ShortIdentifier - => Identifier[..8]; + public ModCollectionIdentity Identity; public override string ToString() - => Name.Length > 0 ? Name : ShortIdentifier; - - /// Get the first two letters of a collection name and its Index (or None if it is the empty collection). - public string AnonymizedName - => this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier; - - /// The index of the collection is set and kept up-to-date by the CollectionManager. - public int Index { get; internal set; } + => Identity.ToString(); public CollectionCounters Counters; @@ -90,7 +69,7 @@ public partial class ModCollection { get { - if (Index <= 0) + if (Identity.Index <= 0) return (ModSettings.Empty, this); foreach (var collection in GetFlattenedInheritance()) @@ -114,17 +93,17 @@ public partial class ModCollection public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), - [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, + Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], + UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version, - int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, ModCollectionIdentity identity, int version, Dictionary allSettings, IReadOnlyList inheritances) { - Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings) + Debug.Assert(identity.Index > 0, "Collection read with non-positive index."); + var ret = new ModCollection(identity, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -137,7 +116,7 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, [], [], []); return ret; } @@ -145,9 +124,8 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, - Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], - []); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, + Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. @@ -195,16 +173,13 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version, - List appliedSettings, List inheritsFrom, Dictionary settings) + private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, List appliedSettings, + List inheritsFrom, Dictionary settings) { - Name = name; - Id = id; - LocalId = localId; - Index = index; + Identity = identity; Counters = new CollectionCounters(changeCounter); - Settings = appliedSettings; - UnusedSettings = settings; + Settings = appliedSettings; + UnusedSettings = settings; DirectlyInheritsFrom = inheritsFrom; foreach (var c in DirectlyInheritsFrom) ((List)c.DirectParentOf).Add(this); diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs new file mode 100644 index 00000000..c7f60005 --- /dev/null +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -0,0 +1,42 @@ +using OtterGui; +using Penumbra.Collections.Manager; + +namespace Penumbra.Collections; + +public struct ModCollectionIdentity(Guid id, LocalCollectionId localId) +{ + public const string DefaultCollectionName = "Default"; + public const string EmptyCollectionName = "None"; + + public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0); + + public string Name { get; set; } + public Guid Id { get; } = id; + public LocalCollectionId LocalId { get; } = localId; + + /// The index of the collection is set and kept up-to-date by the CollectionManager. + public int Index { get; internal set; } + + public string Identifier + => Id.ToString(); + + public string ShortIdentifier + => Id.ShortGuid(); + + /// Get the short identifier of a collection unless it is a well-known collection name. + public string AnonymizedName + => Id == Guid.Empty ? EmptyCollectionName : Name == DefaultCollectionName ? Name : ShortIdentifier; + + public override string ToString() + => Name.Length > 0 ? Name : ShortIdentifier; + + public ModCollectionIdentity(Guid id, LocalCollectionId localId, string name, int index) + : this(id, localId) + { + Name = name; + Index = index; + } + + public static ModCollectionIdentity New(string name, LocalCollectionId id, int index) + => new(Guid.NewGuid(), id, name, index); +} diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index e6bb069b..6e1b51ac 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -15,7 +15,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection => fileNames.CollectionFile(modCollection); public string LogName(string _) - => modCollection.AnonymizedName; + => modCollection.Identity.AnonymizedName; public string TypeName => "Collection"; @@ -28,10 +28,10 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteStartObject(); j.WritePropertyName("Version"); j.WriteValue(ModCollection.CurrentVersion); - j.WritePropertyName(nameof(ModCollection.Id)); - j.WriteValue(modCollection.Identifier); - j.WritePropertyName(nameof(ModCollection.Name)); - j.WriteValue(modCollection.Name); + j.WritePropertyName(nameof(ModCollectionIdentity.Id)); + j.WriteValue(modCollection.Identity.Identifier); + j.WritePropertyName(nameof(ModCollectionIdentity.Name)); + j.WriteValue(modCollection.Identity.Name); j.WritePropertyName(nameof(ModCollection.Settings)); // Write all used and unused settings by mod directory name. @@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier)); + x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identity.Identifier)); j.WriteEndObject(); } @@ -79,8 +79,8 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection { var obj = JObject.Parse(File.ReadAllText(file.FullName)); version = obj["Version"]?.ToObject() ?? 0; - name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; - id = obj[nameof(ModCollection.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); + name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject() ?? string.Empty; + id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 8fe160b3..bda877ff 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -23,7 +23,7 @@ public readonly struct ResolveData(ModCollection collection, nint gameObject) { } public override string ToString() - => ModCollection.Name; + => ModCollection.Identity.Name; } public static class ResolveDataExtensions diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index aff7f16f..61946978 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -326,7 +326,7 @@ public class CommandHandler : IDisposable, IApiService { _chat.Print(collection == null ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" - : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); + : $"{collection.Identity.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); continue; } @@ -363,13 +363,13 @@ public class CommandHandler : IDisposable, IApiService } Print( - $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); + $"Removed {oldCollection.Identity.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); anySuccess = true; continue; } _collectionManager.Active.SetCollection(collection!, type, individualIndex); - Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); + Print($"Assigned {collection!.Identity.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); } return anySuccess; @@ -440,7 +440,7 @@ public class CommandHandler : IDisposable, IApiService _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) .AddText("already had the desired state in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); + .AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString); return false; } @@ -458,7 +458,7 @@ public class CommandHandler : IDisposable, IApiService _collectionEditor.SetModSetting(collection!, mod, groupIndex, setting); Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ") .AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); + .AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString); return true; } @@ -543,7 +543,7 @@ public class CommandHandler : IDisposable, IApiService changes |= HandleModState(state, collection!, mod); if (!changes) - Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true) + Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -558,7 +558,7 @@ public class CommandHandler : IDisposable, IApiService return true; } - collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) + collection = string.Equals(lowerName, ModCollection.Empty.Identity.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty : _collectionManager.Storage.ByIdentifier(lowerName, out var c) ? c @@ -614,7 +614,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -623,7 +623,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -634,7 +634,7 @@ public class CommandHandler : IDisposable, IApiService Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) .AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -643,7 +643,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(" to inherit.").BuiltString); return true; } diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index dcbaedc8..c350c157 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -30,7 +30,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); Penumbra.Log.Excessive( - $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); + $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.Identity.AnonymizedName}."); } public void Dispose() diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs index aa2d3f31..af38ce50 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -30,7 +30,7 @@ public unsafe class AtchCallerHook2 : FastHook, IDispo Task.Result.Original(data, slot, unk, playerModel, unk2); _metaState.AtchCollection.Pop(); Penumbra.Log.Excessive( - $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.AnonymizedName}."); + $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.Identity.AnonymizedName}."); } public void Dispose() diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index e7fc3176..eeae77cc 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -98,7 +98,7 @@ public sealed unsafe class MetaState : IDisposable, IService _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); + _lastCreatedCollection.ModCollection.Identity.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); var decal = new DecalReverter(Config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 25d4f7ea..e0c235a2 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -47,17 +47,17 @@ public static class PathDataHandler /// Create the encoding path for an ATCH file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateAtch(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) - => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static FullPath CreateBase(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index a3233cfb..513877d4 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -26,6 +26,6 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP file.Replace(resource); Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.Identity.AnonymizedName}."); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 7e378f41..f5659e7c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -81,7 +81,7 @@ public class ResourceTreeFactory( var (name, anonymizedName, related) = GetCharacterName(character); var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, - networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); + networked, collectionResolveData.ModCollection.Identity.Name, collectionResolveData.ModCollection.Identity.AnonymizedName); var globalContext = new GlobalResolveContext(metaFileManager, objectIdentifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index b5499624..8fdd09c5 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -63,10 +63,10 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); + dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Identity.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); - modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", + modManager.DataEditor.CreateMeta(dir, collection.Identity.Name, character ?? config.DefaultModAuthor, + $"Mod generated from temporary collection {collection.Identity.Id} for {character ?? "Unknown Character"} with name {collection.Identity.Name}.", null, null); var mod = new Mod(dir); var defaultMod = mod.Default; @@ -99,11 +99,11 @@ public class TemporaryMod : IMod saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir, false); Penumbra.Log.Information( - $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); + $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identity.Identifier}."); } catch (Exception e) { - Penumbra.Log.Error($"Could not save temporary collection {collection.Identifier} to permanent Mod:\n{e}"); + Penumbra.Log.Error($"Could not save temporary collection {collection.Identity.Identifier} to permanent Mod:\n{e}"); if (dir != null && Directory.Exists(dir.FullName)) { try diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 534911df..a2594145 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,24 +245,24 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c, CollectionCache _) => sb.Append( - $"> **`Collection {c.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); + $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); - sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); + sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.Identity.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.Identity.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.Identity.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { var collection = _collectionManager.Active.ByType(type); if (collection != null) - sb.Append($"> **`{name,-29}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{name,-29}`** {collection.Identity.AnonymizedName}\n"); } foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) - sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n"); foreach (var collection in _collectionManager.Caches.Active) PrintCollection(collection, collection._cache!); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 5ba57cf4..f58eb891 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -27,8 +27,8 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu private Configuration _config = null!; private JObject _data = null!; - public string CurrentCollection = ModCollection.DefaultCollectionName; - public string DefaultCollection = ModCollection.DefaultCollectionName; + public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName; + public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = []; public Dictionary ModSortOrder = []; @@ -346,7 +346,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu if (!collectionJson.Exists) return; - var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); + var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; @@ -380,7 +380,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu var emptyStorage = new ModStorage(); // Only used for saving and immediately discarded, so the local collection id here is irrelevant. - var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, LocalCollectionId.Zero, 0, 1, dict, []); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 9103b29c..4814795c 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -240,7 +240,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(character); lock (_eventWriter) { - _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type); + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Identity.Id, type); } } catch (Exception ex) @@ -293,7 +293,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(resolveData.AssociatedGameObject); lock (_eventWriter) { - _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id, + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Identity.Id, manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 817af0d2..ee096109 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -24,7 +24,7 @@ public class FilenameService(IDalamudPluginInterface pi) : IService /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => CollectionFile(collection.Identifier); + => CollectionFile(collection.Identity.Identifier); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 3972e350..0e1408c5 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -25,7 +25,7 @@ public class CollectionSelectHeader : IUiService _selection = selection; _resolver = resolver; _activeCollections = collectionManager.Active; - _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); + _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Identity.Name).ToList()); } /// Draw the header line that can quick switch between collections. @@ -77,10 +77,10 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The configured base collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the configured base collection {collection.Name} as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the configured base collection {collection.Identity.Name} as the current collection.", false), _ => throw new Exception("Can not happen."), }; } @@ -91,10 +91,11 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The loaded player character is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The collection configured to apply to the loaded player character is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the collection {collection.Name} that applies to the loaded player character as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the collection {collection.Identity.Name} that applies to the loaded player character as the current collection.", + false), _ => throw new Exception("Can not happen."), }; } @@ -105,10 +106,10 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The interface collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The configured interface collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the configured interface collection {collection.Name} as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the configured interface collection {collection.Identity.Name} as the current collection.", false), _ => throw new Exception("Can not happen."), }; } @@ -120,8 +121,8 @@ public class CollectionSelectHeader : IUiService { CollectionState.Unavailable => (null, "Not Inherited", "The settings of the selected mod are not inherited from another collection.", true), - CollectionState.Available => (collection, collection!.Name, - $"Select the collection {collection!.Name} from which the selected mod inherits its settings as the current collection.", + CollectionState.Available => (collection, collection!.Identity.Name, + $"Select the collection {collection!.Identity.Name} from which the selected mod inherits its settings as the current collection.", false), _ => throw new Exception("Can not happen."), }; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 1670be5e..0259713f 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -29,13 +29,13 @@ public sealed class CollectionCombo(CollectionManager manager, Func obj.Name; + => obj.Identity.Name; protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, ImGuiComboFlags flags) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 914f10d9..cab34b10 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -221,16 +221,16 @@ public sealed class CollectionPanel( ImGui.SameLine(); ImGui.BeginGroup(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = _newName ?? collection.Name; - var identifier = collection.Identifier; + var name = _newName ?? collection.Identity.Name; + var identifier = collection.Identity.Identifier; var width = ImGui.GetContentRegionAvail().X; var fileName = saveService.FileNames.CollectionFile(collection); ImGui.SetNextItemWidth(width); if (ImGui.InputText("##name", ref name, 128)) _newName = name; - if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Name) + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) { - collection.Name = _newName; + collection.Identity.Name = _newName; saveService.QueueSave(new ModCollectionSave(mods, collection)); selector.RestoreCollections(); _newName = null; @@ -242,7 +242,7 @@ public sealed class CollectionPanel( using (ImRaii.PushFont(UiBuilder.MonoFont)) { - if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) + if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0))) try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); @@ -289,9 +289,9 @@ public sealed class CollectionPanel( _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); } - foreach (var coll in _collections.OrderBy(c => c.Name)) + foreach (var coll in _collections.OrderBy(c => c.Identity.Name)) { - if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) + if (coll != collection && ImGui.MenuItem($"Use {coll.Identity.Name}.")) _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); } } @@ -418,7 +418,7 @@ public sealed class CollectionPanel( private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 024873bf..57429531 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -69,7 +69,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } protected override bool Filtered(int idx) - => !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); + => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); private const string PayloadString = "Collection"; @@ -111,12 +111,12 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => _incognito.IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; + => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name; public void RestoreCollections() { Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Name)) + foreach (var c in _storage.OrderBy(c => c.Identity.Name)) Items.Add(c); SetFilterDirty(); SetCurrent(_active.Current); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 418fe52c..a4d60b13 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -93,7 +93,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService /// private void DrawInheritedChildren(ModCollection collection) { - using var id = ImRaii.PushId(collection.Index); + using var id = ImRaii.PushId(collection.Identity.Index); using var indent = ImRaii.PushIndent(); // Get start point for the lines (top of the selector). @@ -114,7 +114,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Id}", + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Identity.Id}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); @@ -140,7 +140,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), _seenInheritedCollections.Contains(collection)); _seenInheritedCollections.Add(collection); - using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); + using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Identity.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); color.Pop(); DrawInheritanceTreeClicks(collection, true); DrawInheritanceDropSource(collection); @@ -252,7 +252,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService foreach (var collection in _collections .Where(c => InheritanceManager.CheckValidInheritance(_active.Current, c) == InheritanceManager.ValidInheritance.Valid) - .OrderBy(c => c.Name)) + .OrderBy(c => c.Identity.Name)) { if (ImGui.Selectable(Name(collection), _newInheritance == collection)) _newInheritance = collection; @@ -312,5 +312,5 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService } private string Name(ModCollection collection) - => incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + => incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index b7648428..89a7d765 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -56,7 +56,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele foreach (var ((collection, parent, color, state), idx) in _cache.WithIndex()) { using var id = ImUtf8.PushId(idx); - ImUtf8.DrawTableColumn(collection.Name); + ImUtf8.DrawTableColumn(collection.Identity.Name); ImGui.TableNextColumn(); ImUtf8.Text(ToText(state), color); @@ -65,7 +65,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele { if (context) { - ImUtf8.Text(collection.Name); + ImUtf8.Text(collection.Identity.Name); ImGui.Separator(); using (ImRaii.Disabled(state is ModState.Enabled && parent == collection)) { @@ -95,7 +95,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele } } - ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Name); + ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Identity.Name); } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 8d889c3b..261f6e92 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -67,7 +67,7 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width)) + if (ImGui.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width)) collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d432e97e..0f72efff 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -241,7 +241,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService { var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; Penumbra.Log.Information( - $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.Identity.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 88b7120d..7ac3cb99 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -167,7 +167,7 @@ internal sealed class ResourceWatcherTable : Table => 80 * UiHelpers.Scale; public override string ToName(Record item) - => item.Collection?.Name ?? string.Empty; + => (item.Collection != null ? item.Collection.Identity.Name : null) ?? string.Empty; } private sealed class ObjectColumn : ColumnString diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 95afb10f..d6a9f05a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -204,7 +204,7 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.Name} (Change Counter {collection.Counters.Change})###{collection.Name}"); + using var node = TreeNode($"{collection.Identity.Name} (Change Counter {collection.Counters.Change})###{collection.Identity.Name}"); if (!node) continue; @@ -239,7 +239,7 @@ public class DebugTab : Window, ITab, IUiService else { using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.Counters.Change})", + TreeNode($"{collection.Identity.AnonymizedName} (Change Counter {collection.Counters.Change})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } @@ -265,9 +265,9 @@ public class DebugTab : Window, ITab, IUiService { PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); @@ -518,7 +518,7 @@ public class DebugTab : Window, ITab, IUiService return; ImGui.TextUnformatted( - $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Name})"); + $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Identity.Name})"); using (var drawTree = TreeNode("Draw Object to Object")) { if (drawTree) @@ -545,7 +545,7 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted(name); ImGui.TableNextColumn(); var collection = _collectionResolver.IdentifyCollection(gameObject, true); - ImGui.TextUnformatted(collection.ModCollection.Name); + ImGui.TextUnformatted(collection.ModCollection.Identity.Name); } } } @@ -561,7 +561,7 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"{data.AssociatedGameObject:X}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(data.ModCollection.Name); + ImGui.TextUnformatted(data.ModCollection.Identity.Name); } } } @@ -574,12 +574,12 @@ public class DebugTab : Window, ITab, IUiService if (table) { ImGuiUtil.DrawTableColumn("Current Mtrl Data"); - ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Name); + ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.MtrlData.AssociatedGameObject:X}"); ImGui.TableNextColumn(); ImGuiUtil.DrawTableColumn("Current Avfx Data"); - ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Name); + ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.AvfxData.AssociatedGameObject:X}"); ImGui.TableNextColumn(); @@ -591,7 +591,7 @@ public class DebugTab : Window, ITab, IUiService foreach (var (resource, resolve) in _subfileHelper) { ImGuiUtil.DrawTableColumn($"0x{resource:X}"); - ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); + ImGuiUtil.DrawTableColumn(resolve.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); ImGuiUtil.DrawTableColumn($"{((ResourceHandle*)resource)->FileName()}"); } @@ -611,7 +611,7 @@ public class DebugTab : Window, ITab, IUiService ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{address:X}"); ImGuiUtil.DrawTableColumn(identifier.ToString()); - ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.Identity.Name); } } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 87338bdb..c226098d 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -77,12 +77,12 @@ public class ModsTab( { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"{modManager.Count} Mods\n" - + $"{_activeCollections.Current.AnonymizedName} Current Collection\n" + + $"{_activeCollections.Current.Identity.AnonymizedName} Current Collection\n" + $"{_activeCollections.Current.Settings.Count} Settings\n" + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"); + + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); } } diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 7d2a0d2a..69f2b616 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -83,14 +83,14 @@ public class TutorialService : IUiService + "Go here after setting up your root folder to continue the tutorial!") .Register("Initial Setup, Step 4: Managing Collections", "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" - + $"There will always be one collection called {ModCollection.DefaultCollectionName} that can not be deleted.") + + $"There will always be one collection called {ModCollectionIdentity.DefaultCollectionName} that can not be deleted.") .Register($"Initial Setup, Step 5: {SelectedCollection}", $"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n" - + $"We should already have the collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") + + $"We should already have the collection named {ModCollectionIdentity.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") .Register("Initial Setup, Step 6: Simple Assignments", "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n" - + $"If you are just starting, you can see that the {ModCollection.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + + $"If you are just starting, you can see that the {ModCollectionIdentity.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.") .Register("Individual Assignments", "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") From 98a89bb2b4b67a2767773ec0f18c9b346028dc07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 16:02:50 +0100 Subject: [PATCH 2092/2451] Current state. --- Penumbra/Api/Api/ModSettingsApi.cs | 154 ++++++++++++++- Penumbra/Collections/Cache/CollectionCache.cs | 6 +- .../Cache/CollectionCacheManager.cs | 16 +- .../Collections/Manager/ActiveCollections.cs | 2 +- .../Collections/Manager/CollectionEditor.cs | 40 ++-- .../Collections/Manager/CollectionStorage.cs | 28 +-- .../Collections/Manager/InheritanceManager.cs | 46 ++--- .../Manager/ModCollectionMigration.cs | 8 +- Penumbra/Collections/ModCollection.cs | 183 +++++++----------- .../Collections/ModCollectionInheritance.cs | 92 +++++++++ Penumbra/Collections/ModCollectionSave.cs | 12 +- Penumbra/Collections/ModSettingProvider.cs | 98 ++++++++++ Penumbra/CommandHandler.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 3 + Penumbra/Mods/ModSelection.cs | 21 +- Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 21 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 16 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 28 ++- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 37 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 40 +++- Penumbra/UI/Tabs/ModsTab.cs | 2 +- 28 files changed, 606 insertions(+), 265 deletions(-) create mode 100644 Penumbra/Collections/ModCollectionInheritance.cs create mode 100644 Penumbra/Collections/ModSettingProvider.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 3dc900fc..4027975b 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -24,18 +25,20 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; + private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator) + CommunicatorService communicator, ApiHelpers helpers) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; + _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -63,11 +66,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return new AvailableModSettings(dict); } - public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) - => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)) - : null; - public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance) { @@ -80,14 +78,14 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var settings = collection.Identity.Id == Guid.Empty ? null : ignoreInheritance - ? collection.Settings[mod.Index] - : collection[mod.Index].Settings; + ? collection.GetOwnSettings(mod.Index) + : collection.GetInheritedSettings(mod.Index).Settings; if (settings == null) return (PenumbraApiEc.Success, null); var (enabled, priority, dict) = settings.ConvertToShareable(mod); return (PenumbraApiEc.Success, - (enabled, priority.Value, dict, collection.Settings[mod.Index] == null)); + (enabled, priority.Value, dict, collection.GetOwnSettings(mod.Index) is null)); } public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) @@ -211,11 +209,147 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } + public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); + } + + public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + settings = new TemporaryModSettings + { + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + foreach (var (groupName, optionNames) in options) + { + var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var setting = Setting.Zero; + switch (mod.Groups[groupIdx]) + { + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting = Setting.Single(optionIdx); + break; + } + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.Options.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + settings.Settings[groupIdx] = setting; + } + + collection.Settings.SetTemporary(mod.Index, settings); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is not { } settings) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + collection.Settings.SetTemporary(mod.Index, null); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + return PenumbraApiEc.Success; + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) + { + collection.Settings.SetTemporary(i, null); + ++numRemoved; + } + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { var collection = _collectionResolver.PlayerCollection(); - var (settings, parent) = collection[mod.Index]; + var (settings, parent) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index ad902aac..8ca9aa36 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -260,7 +260,7 @@ public sealed class CollectionCache : IDisposable if (mod.Index < 0) return mod.GetData(); - var settings = _collection[mod.Index].Settings; + var settings = _collection.GetActualSettings(mod.Index).Settings; return settings is not { Enabled: true } ? AppliedModData.Empty : mod.GetData(settings); @@ -342,8 +342,8 @@ public sealed class CollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 0a851154..839c0376 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -231,11 +231,11 @@ public class CollectionCacheManager : IDisposable, IService { case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.RemoveMod(mod, true); break; case ModPathChangeType.Moved: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); break; } @@ -246,7 +246,7 @@ public class CollectionCacheManager : IDisposable, IService if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) return; - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.AddMod(mod, true); } @@ -273,7 +273,7 @@ public class CollectionCacheManager : IDisposable, IService { if (type is ModOptionChangeType.PrepareChange) { - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) collection._cache!.RemoveMod(mod, false); return; @@ -284,7 +284,7 @@ public class CollectionCacheManager : IDisposable, IService if (!recomputeList) return; - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) { if (justAdd) collection._cache!.AddMod(mod, true); @@ -317,7 +317,7 @@ public class CollectionCacheManager : IDisposable, IService cache.AddMod(mod!, true); else if (oldValue == Setting.True) cache.RemoveMod(mod!, true); - else if (collection[mod!.Index].Settings?.Enabled == true) + else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) cache.ReloadMod(mod!, true); else cache.RemoveMod(mod!, true); @@ -329,8 +329,8 @@ public class CollectionCacheManager : IDisposable, IService break; case ModSettingChange.Setting: - if (collection[mod!.Index].Settings?.Enabled == true) - cache.ReloadMod(mod!, true); + if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) + cache.ReloadMod(mod, true); break; case ModSettingChange.MultiInheritance: diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 07fcb430..2ced8ad6 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -282,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable, IService .Prepend(Interface) .Prepend(Default) .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + .SelectMany(c => c.Inheritance.FlatHierarchy).Contains(Current); /// Save if any of the active collections is changed and set new collections to Current. private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index caff2c86..66578a95 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -26,12 +26,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModState(ModCollection collection, Mod mod, bool newValue) { - var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True, 0); return true; @@ -55,13 +55,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu var changes = false; foreach (var mod in mods) { - var oldValue = collection.Settings[mod.Index]?.Enabled; + var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled; if (newValue == oldValue) continue; FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; - changes = true; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; + changes = true; } if (!changes) @@ -76,12 +76,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue) { - var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Priority = newValue; + collection.GetOwnSettings(mod.Index)!.Priority = newValue; InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0); return true; } @@ -92,15 +92,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { - var settings = collection.Settings[mod.Index] != null - ? collection.Settings[mod.Index]!.Settings - : collection[mod.Index].Settings?.Settings; + var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings; var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings; if (oldValue == newValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); + collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue); InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx); return true; } @@ -115,10 +113,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = sourceMod != null - ? collection.Settings[sourceMod.Index] != null - ? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod) + ? collection.GetOwnSettings(sourceMod.Index) is { } ownSettings + ? new ModSettings.SavedSettings(ownSettings, sourceMod) : null - : collection.UnusedSettings.TryGetValue(sourceName, out var s) + : collection.Settings.Unused.TryGetValue(sourceName, out var s) ? s : null; @@ -148,10 +146,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // or remove any unused settings for the target if they are inheriting. if (savedSettings != null) { - ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; + ((Dictionary)collection.Settings.Unused)[targetName] = savedSettings.Value; saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } - else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) + else if (((Dictionary)collection.Settings.Unused).Remove(targetName)) { saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } @@ -166,12 +164,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); if (inherit == (settings == null)) return false; - ((List)collection.Settings)[mod.Index] = - inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + ModSettings? settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + collection.Settings.Set(mod.Index, settings1); return true; } @@ -188,7 +186,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - foreach (var directInheritor in directParent.DirectParentOf) + foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy) { switch (type) { @@ -197,7 +195,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: - if (directInheritor.Settings[mod!.Index] == null) + if (directInheritor.GetOwnSettings(mod!.Index) == null) communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2ed395ae..e19acd35 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -41,8 +41,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, - inheritances); + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, + new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); _collectionsByLocal[CurrentCollectionId] = newCollection; CurrentCollectionId += 1; return newCollection; @@ -196,8 +196,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove all settings for not currently-installed mods from the given collection. public void CleanUnavailableSettings(ModCollection collection) { - var any = collection.UnusedSettings.Count > 0; - ((Dictionary)collection.UnusedSettings).Clear(); + var any = collection.Settings.Unused.Count > 0; + ((Dictionary)collection.Settings.Unused).Clear(); if (any) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -205,7 +205,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove a specific setting for not currently-installed mods from the given collection. public void CleanUnavailableSetting(ModCollection collection, string? setting) { - if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) + if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -307,7 +307,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer private void OnModDiscoveryStarted() { foreach (var collection in this) - collection.PrepareModDiscovery(_modStorage); + collection.Settings.PrepareModDiscovery(_modStorage); } /// Restore all settings in all collections to mods. @@ -315,7 +315,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { // Re-apply all mod settings. foreach (var collection in this) - collection.ApplyModSettings(_saveService, _modStorage); + collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); } /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. @@ -326,21 +326,22 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { case ModPathChangeType.Added: foreach (var collection in this) - collection.AddMod(mod); + collection.Settings.AddMod(mod); break; case ModPathChangeType.Deleted: foreach (var collection in this) - collection.RemoveMod(mod); + collection.Settings.RemoveMod(mod); break; case ModPathChangeType.Moved: - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; case ModPathChangeType.Reloaded: foreach (var collection in this) { - if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false) + if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } break; @@ -357,8 +358,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) + if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } } @@ -370,7 +372,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - var (settings, _) = collection[mod.Index]; + var (settings, _) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) collection.Counters.IncrementChange(); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index e003ad6b..5e361bde 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -63,10 +62,10 @@ public class InheritanceManager : IDisposable, IService if (ReferenceEquals(potentialParent, potentialInheritor)) return ValidInheritance.Self; - if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent)) + if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent)) return ValidInheritance.Contained; - if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor))) + if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor))) return ValidInheritance.Circle; return ValidInheritance.Valid; @@ -83,24 +82,22 @@ public class InheritanceManager : IDisposable, IService /// Remove an existing inheritance from a collection. public void RemoveInheritance(ModCollection inheritor, int idx) { - var parent = inheritor.DirectlyInheritsFrom[idx]; - ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); - ((List)parent.DirectParentOf).Remove(inheritor); + var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx); _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances."); } /// Order in the inheritance list is relevant. public void MoveInheritance(ModCollection inheritor, int from, int to) { - if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) + if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to)) return; _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}."); } @@ -110,15 +107,15 @@ public class InheritanceManager : IDisposable, IService if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid) return false; - ((List)inheritor.DirectlyInheritsFrom).Add(parent); - ((List)parent.DirectParentOf).Add(inheritor); + inheritor.Inheritance.AddInheritance(inheritor, parent); if (invokeEvent) { _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); } + RecurseInheritanceChanges(inheritor, invokeEvent); + Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances."); return true; } @@ -131,11 +128,11 @@ public class InheritanceManager : IDisposable, IService { foreach (var collection in _storage) { - if (collection.InheritanceByName == null) + if (collection.Inheritance.ConsumeNames() is not { } byName) continue; var changes = false; - foreach (var subCollectionName in collection.InheritanceByName) + foreach (var subCollectionName in byName) { if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection)) { @@ -143,7 +140,8 @@ public class InheritanceManager : IDisposable, IService continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) @@ -153,7 +151,8 @@ public class InheritanceManager : IDisposable, IService if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else @@ -165,7 +164,6 @@ public class InheritanceManager : IDisposable, IService } } - collection.InheritanceByName = null; if (changes) _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } @@ -178,20 +176,22 @@ public class InheritanceManager : IDisposable, IService foreach (var c in _storage) { - var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old); + var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old); if (inheritedIdx >= 0) RemoveInheritance(c, inheritedIdx); - ((List)c.DirectParentOf).Remove(old); + c.Inheritance.RemoveChild(old); } } - private void RecurseInheritanceChanges(ModCollection newInheritor) + private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent) { - foreach (var inheritor in newInheritor.DirectParentOf) + foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy) { - _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); - RecurseInheritanceChanges(inheritor); + ModCollectionInheritance.UpdateFlattenedInheritance(inheritor); + RecurseInheritanceChanges(inheritor, invokeEvent); + if (invokeEvent) + _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); } } } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index fe61285d..7db375f7 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -26,12 +26,12 @@ internal static class ModCollectionMigration // Remove all completely defaulted settings from active and inactive mods. for (var i = 0; i < collection.Settings.Count; ++i) { - if (SettingIsDefaultV0(collection.Settings[i])) - ((List)collection.Settings)[i] = null; + if (SettingIsDefaultV0(collection.GetOwnSettings(i))) + collection.Settings.SetAll(i, FullModSettings.Empty); } - foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) - ((Dictionary)collection.UnusedSettings).Remove(key); + foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) + collection.Settings.RemoveUnused(key); return true; } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9b33c1f4..69f82458 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,4 +1,3 @@ -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods.Settings; @@ -22,70 +21,74 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); public ModCollectionIdentity Identity; public override string ToString() => Identity.ToString(); - public CollectionCounters Counters; + public readonly ModSettingProvider Settings; + public ModCollectionInheritance Inheritance; + public CollectionCounters Counters; - /// - /// If a ModSetting is null, it can be inherited from other collections. - /// If no collection provides a setting for the mod, it is just disabled. - /// - public readonly IReadOnlyList Settings; - /// Settings for deleted mods will be kept via the mods identifier (directory name). - public readonly IReadOnlyDictionary UnusedSettings; - - /// Inheritances stored before they can be applied. - public IReadOnlyList? InheritanceByName; - - /// Contains all direct parent collections this collection inherits settings from. - public readonly IReadOnlyList DirectlyInheritsFrom; - - /// Contains all direct child collections that inherit from this collection. - public readonly IReadOnlyList DirectParentOf = new List(); - - /// All inherited collections in application order without filtering for duplicates. - public static IEnumerable InheritedCollections(ModCollection collection) - => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); - - /// - /// Iterate over all collections inherited from in depth-first order. - /// Skip already visited collections to avoid circular dependencies. - /// - public IEnumerable GetFlattenedInheritance() - => InheritedCollections(this).Distinct(); - - /// - /// Obtain the actual settings for a given mod via index. - /// Also returns the collection the settings are taken from. - /// If no collection provides settings for this mod, this collection is returned together with null. - /// - public (ModSettings? Settings, ModCollection Collection) this[Index idx] + public ModSettings? GetOwnSettings(Index idx) { - get + if (Identity.Index <= 0) + return ModSettings.Empty; + + return Settings.Settings[idx].Settings; + } + + public TemporaryModSettings? GetTempSettings(Index idx) + { + if (Identity.Index <= 0) + return null; + + return Settings.Settings[idx].TempSettings; + } + + public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + foreach (var collection in Inheritance.FlatHierarchy) { - if (Identity.Index <= 0) - return (ModSettings.Empty, this); - - foreach (var collection in GetFlattenedInheritance()) - { - var settings = collection.Settings[idx]; - if (settings != null) - return (settings, collection); - } - - return (null, this); + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); } + + return (null, this); + } + + public (ModSettings? Settings, ModCollection Collection) GetActualSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + // Check temp settings. + var ownTempSettings = Settings.Settings[idx].Resolve(); + if (ownTempSettings != null) + return (ownTempSettings, this); + + // Ignore temp settings for inherited collections. + foreach (var collection in Inheritance.FlatHierarchy.Skip(1)) + { + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); + } + + return (null, this); } /// Evaluates all settings along the whole inheritance tree. public IEnumerable ActualSettings - => Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings); + => Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings); /// /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. @@ -93,9 +96,7 @@ public partial class ModCollection public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], - UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone()); } /// Constructor for reading from files. @@ -103,11 +104,8 @@ public partial class ModCollection Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(identity.Index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(identity, 0, version, [], [], allSettings) - { - InheritanceByName = inheritances, - }; - ret.ApplyModSettings(saver, mods); + var ret = new ModCollection(identity, 0, version, new ModSettingProvider(allSettings), new ModCollectionInheritance(inheritances)); + ret.Settings.ApplyModSettings(ret, saver, mods); ModCollectionMigration.Migrate(saver, mods, version, ret); return ret; } @@ -116,7 +114,8 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); return ret; } @@ -124,64 +123,18 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount), + new ModCollectionInheritance()); } - /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - internal bool AddMod(Mod mod) + private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings, + ModCollectionInheritance inheritance) { - if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save)) - { - var ret = save.ToSettings(mod, out var settings); - ((List)Settings).Add(settings); - ((Dictionary)UnusedSettings).Remove(mod.ModPath.Name); - return ret; - } - - ((List)Settings).Add(null); - return false; - } - - /// Move settings from the current mod list to the unused mod settings. - internal void RemoveMod(Mod mod) - { - var settings = Settings[mod.Index]; - if (settings != null) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - - ((List)Settings).RemoveAt(mod.Index); - } - - /// Move all settings to unused settings for rediscovery. - internal void PrepareModDiscovery(ModStorage mods) - { - foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null)) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); - - ((List)Settings).Clear(); - } - - /// - /// Apply all mod settings from unused settings to the current set of mods. - /// Also fixes invalid settings. - /// - internal void ApplyModSettings(SaveService saver, ModStorage mods) - { - ((List)Settings).Capacity = Math.Max(((List)Settings).Capacity, mods.Count); - if (mods.Aggregate(false, (current, mod) => current | AddMod(mod))) - saver.ImmediateSave(new ModCollectionSave(mods, this)); - } - - private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, List appliedSettings, - List inheritsFrom, Dictionary settings) - { - Identity = identity; - Counters = new CollectionCounters(changeCounter); - Settings = appliedSettings; - UnusedSettings = settings; - DirectlyInheritsFrom = inheritsFrom; - foreach (var c in DirectlyInheritsFrom) - ((List)c.DirectParentOf).Add(this); + Identity = identity; + Counters = new CollectionCounters(changeCounter); + Settings = settings; + Inheritance = inheritance; + ModCollectionInheritance.UpdateChildren(this); + ModCollectionInheritance.UpdateFlattenedInheritance(this); } } diff --git a/Penumbra/Collections/ModCollectionInheritance.cs b/Penumbra/Collections/ModCollectionInheritance.cs new file mode 100644 index 00000000..151ed7db --- /dev/null +++ b/Penumbra/Collections/ModCollectionInheritance.cs @@ -0,0 +1,92 @@ +using OtterGui.Filesystem; + +namespace Penumbra.Collections; + +public struct ModCollectionInheritance +{ + public IReadOnlyList? InheritanceByName { get; private set; } + private readonly List _directlyInheritsFrom = []; + private readonly List _directlyInheritedBy = []; + private readonly List _flatHierarchy = []; + + public ModCollectionInheritance() + { } + + private ModCollectionInheritance(List inheritsFrom) + => _directlyInheritsFrom = [.. inheritsFrom]; + + public ModCollectionInheritance(IReadOnlyList byName) + => InheritanceByName = byName; + + public ModCollectionInheritance Clone() + => new(_directlyInheritsFrom); + + public IEnumerable Identifiers + => InheritanceByName ?? _directlyInheritsFrom.Select(c => c.Identity.Identifier); + + public IReadOnlyList? ConsumeNames() + { + var ret = InheritanceByName; + InheritanceByName = null; + return ret; + } + + public static void UpdateChildren(ModCollection parent) + { + foreach (var inheritance in parent.Inheritance.DirectlyInheritsFrom) + inheritance.Inheritance._directlyInheritedBy.Add(parent); + } + + public void AddInheritance(ModCollection inheritor, ModCollection newParent) + { + _directlyInheritsFrom.Add(newParent); + newParent.Inheritance._directlyInheritedBy.Add(inheritor); + UpdateFlattenedInheritance(inheritor); + } + + public ModCollection RemoveInheritanceAt(ModCollection inheritor, int idx) + { + var parent = DirectlyInheritsFrom[idx]; + _directlyInheritsFrom.RemoveAt(idx); + parent.Inheritance._directlyInheritedBy.Remove(parent); + UpdateFlattenedInheritance(inheritor); + return parent; + } + + public bool MoveInheritance(ModCollection inheritor, int from, int to) + { + if (!_directlyInheritsFrom.Move(from, to)) + return false; + + UpdateFlattenedInheritance(inheritor); + return true; + } + + public void RemoveChild(ModCollection child) + => _directlyInheritedBy.Remove(child); + + /// Contains all direct parent collections this collection inherits settings from. + public readonly IReadOnlyList DirectlyInheritsFrom + => _directlyInheritsFrom; + + /// Contains all direct child collections that inherit from this collection. + public readonly IReadOnlyList DirectlyInheritedBy + => _directlyInheritedBy; + + /// + /// Iterate over all collections inherited from in depth-first order. + /// Skip already visited collections to avoid circular dependencies. + /// + public readonly IReadOnlyList FlatHierarchy + => _flatHierarchy; + + public static void UpdateFlattenedInheritance(ModCollection parent) + { + parent.Inheritance._flatHierarchy.Clear(); + parent.Inheritance._flatHierarchy.AddRange(InheritedCollections(parent).Distinct()); + } + + /// All inherited collections in application order without filtering for duplicates. + private static IEnumerable InheritedCollections(ModCollection parent) + => parent.Inheritance.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(parent); +} diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 6e1b51ac..4c41a28c 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -32,19 +32,19 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteValue(modCollection.Identity.Identifier); j.WritePropertyName(nameof(ModCollectionIdentity.Name)); j.WriteValue(modCollection.Identity.Name); - j.WritePropertyName(nameof(ModCollection.Settings)); + j.WritePropertyName("Settings"); // Write all used and unused settings by mod directory name. j.WriteStartObject(); - var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count); + var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.Settings.Unused.Count); for (var i = 0; i < modCollection.Settings.Count; ++i) { - var settings = modCollection.Settings[i]; + var settings = modCollection.GetOwnSettings(i); if (settings != null) list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i]))); } - list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); + list.AddRange(modCollection.Settings.Unused.Select(kvp => (kvp.Key, kvp.Value))); list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); foreach (var (modDir, settings) in list) @@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identity.Identifier)); + x.Serialize(j, modCollection.Inheritance.Identifiers); j.WriteEndObject(); } @@ -82,7 +82,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject() ?? string.Empty; id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. - settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; + settings = obj["Settings"]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; return true; } diff --git a/Penumbra/Collections/ModSettingProvider.cs b/Penumbra/Collections/ModSettingProvider.cs new file mode 100644 index 00000000..3bf2f949 --- /dev/null +++ b/Penumbra/Collections/ModSettingProvider.cs @@ -0,0 +1,98 @@ +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Collections; + +public readonly struct ModSettingProvider +{ + private ModSettingProvider(IEnumerable settings, Dictionary unusedSettings) + { + _settings = settings.Select(s => s.DeepCopy()).ToList(); + _unused = unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); + } + + public ModSettingProvider() + { } + + public static ModSettingProvider Empty(int count) + => new(Enumerable.Repeat(FullModSettings.Empty, count), []); + + public ModSettingProvider(Dictionary allSettings) + => _unused = allSettings; + + private readonly List _settings = []; + + /// Settings for deleted mods will be kept via the mods identifier (directory name). + private readonly Dictionary _unused = []; + + public int Count + => _settings.Count; + + public bool RemoveUnused(string key) + => _unused.Remove(key); + + internal void Set(Index index, ModSettings? settings) + => _settings[index] = _settings[index] with { Settings = settings }; + + internal void SetTemporary(Index index, TemporaryModSettings? settings) + => _settings[index] = _settings[index] with { TempSettings = settings }; + + internal void SetAll(Index index, FullModSettings settings) + => _settings[index] = settings; + + public IReadOnlyList Settings + => _settings; + + public IReadOnlyDictionary Unused + => _unused; + + public ModSettingProvider Clone() + => new(_settings, _unused); + + /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + internal bool AddMod(Mod mod) + { + if (_unused.Remove(mod.ModPath.Name, out var save)) + { + var ret = save.ToSettings(mod, out var settings); + _settings.Add(new FullModSettings(settings)); + return ret; + } + + _settings.Add(FullModSettings.Empty); + return false; + } + + /// Move settings from the current mod list to the unused mod settings. + internal void RemoveMod(Mod mod) + { + var settings = _settings[mod.Index]; + if (settings.Settings != null) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(settings.Settings, mod); + + _settings.RemoveAt(mod.Index); + } + + /// Move all settings to unused settings for rediscovery. + internal void PrepareModDiscovery(ModStorage mods) + { + foreach (var (mod, setting) in mods.Zip(_settings).Where(s => s.Second.Settings != null)) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(setting.Settings!, mod); + + _settings.Clear(); + } + + /// + /// Apply all mod settings from unused settings to the current set of mods. + /// Also fixes invalid settings. + /// + internal void ApplyModSettings(ModCollection parent, SaveService saver, ModStorage mods) + { + _settings.Capacity = Math.Max(_settings.Capacity, mods.Count); + var settings = this; + if (mods.Aggregate(false, (current, mod) => current | settings.AddMod(mod))) + saver.ImmediateSave(new ModCollectionSave(mods, parent)); + } +} diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 61946978..dee46e32 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -606,7 +606,7 @@ public class CommandHandler : IDisposable, IApiService private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); switch (settingState) { case 0: diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 088527ca..4fa13e1f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -98,6 +98,6 @@ public class ResourceNode : ICloneable public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, IconFlag); + => Name == null ? this : this with { Name = prefix + Name }; } } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 876fe12f..c06af9c7 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -74,6 +74,9 @@ public class ModMetaEditor( dict.ClearForDefault(); var count = 0; + foreach (var value in clone.GlobalEqp) + dict.TryAdd(value); + foreach (var (key, value) in clone.Imc) { var defaultEntry = ImcChecker.GetDefaultEntry(key, false); diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 73d0272b..59cd5d71 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -36,9 +36,16 @@ public class ModSelection : EventWrapper _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); } - public ModSettings Settings { get; private set; } = ModSettings.Empty; - public ModCollection Collection { get; private set; } = ModCollection.Empty; - public Mod? Mod { get; private set; } + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + public ModSettings? OwnSettings { get; private set; } + + public bool IsTemporary + => OwnSettings != Settings; + + public TemporaryModSettings? AsTemporarySettings + => Settings as TemporaryModSettings; public void SelectMod(Mod? mod) @@ -83,12 +90,14 @@ public class ModSelection : EventWrapper { if (Mod == null) { - Settings = ModSettings.Empty; - Collection = ModCollection.Empty; + Settings = ModSettings.Empty; + Collection = ModCollection.Empty; + OwnSettings = null; } else { - (var settings, Collection) = _collections.Current[Mod.Index]; + (var settings, Collection) = _collections.Current.GetActualSettings(Mod.Index); + OwnSettings = _collections.Current.GetOwnSettings(Mod.Index); Settings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 25e4805d..671fba4d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -12,7 +12,7 @@ namespace Penumbra.Mods.Settings; public class ModSettings { public static readonly ModSettings Empty = new(); - public SettingList Settings { get; private init; } = []; + public SettingList Settings { get; internal init; } = []; public ModPriority Priority { get; set; } public bool Enabled { get; set; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a2594145..69dfe3e8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,7 +245,7 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c, CollectionCache _) => sb.Append( - $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); + $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.Inheritance.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index f58eb891..9fe8c420 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -240,7 +240,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu if (jObject["Name"]?.ToObject() == ForcedCollection) continue; - jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); + jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); File.WriteAllText(collection.FullName, jObject.ToString()); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3f7f2f6c..b0029f08 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -737,7 +737,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collectionType is not CollectionType.Current || _mod == null || newCollection == null) return; - UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.GetInheritedSettings(_mod.Index).Settings : null); } private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) @@ -754,7 +754,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collection != _collectionManager.Active.Current || _mod == null) return; - UpdateMod(_mod, collection[_mod.Index].Settings); + UpdateMod(_mod, collection.GetInheritedSettings(_mod.Index).Settings); _swapData.LoadMod(_mod, _modSettings); _dirty = true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 1a4065bb..02e945f3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -101,7 +101,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current.GetInheritedSettings(mod.Index).Settings); UpdateModels(); _forceTextureStartPath = true; }); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cab34b10..8b41b105 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -15,6 +15,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; @@ -497,7 +498,7 @@ public sealed class CollectionPanel( ImGui.Separator(); var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); - if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) + if (_inUseCache.Count == 0 && collection.Inheritance.DirectlyInheritedBy.Count == 0) { ImGui.Dummy(Vector2.One); using var f = _nameFont.Push(); @@ -559,7 +560,7 @@ public sealed class CollectionPanel( private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) { - if (collection.DirectParentOf.Count <= 0) + if (collection.Inheritance.DirectlyInheritedBy.Count <= 0) return; using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) @@ -570,11 +571,11 @@ public sealed class CollectionPanel( using var f = _nameFont.Push(); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); - ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); + ImGuiUtil.DrawTextButton(Name(collection.Inheritance.DirectlyInheritedBy[0]), Vector2.Zero, 0); var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X; - foreach (var parent in collection.DirectParentOf.Skip(1)) + foreach (var parent in collection.Inheritance.DirectlyInheritedBy.Skip(1)) { var name = Name(parent); var size = ImGui.CalcTextSize(name).X; @@ -602,7 +603,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection[m.Index])) + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection.GetInheritedSettings(m.Index))) .Where(t => t.Item2.Settings != null) .OrderBy(t => t.m.Name)) { @@ -625,12 +626,12 @@ public sealed class CollectionPanel( private void DrawInactiveSettingsList(ModCollection collection) { - if (collection.UnusedSettings.Count == 0) + if (collection.Settings.Unused.Count == 0) return; ImGui.Dummy(Vector2.One); - var text = collection.UnusedSettings.Count > 1 - ? $"Clear all {collection.UnusedSettings.Count} unused settings from deleted mods." + var text = collection.Settings.Unused.Count > 1 + ? $"Clear all {collection.Settings.Unused.Count} unused settings from deleted mods." : "Clear the currently unused setting from a deleted mods."; if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) _collections.CleanUnavailableSettings(collection); @@ -638,7 +639,7 @@ public sealed class CollectionPanel( ImGui.Dummy(Vector2.One); var size = new Vector2(ImGui.GetContentRegionAvail().X, - Math.Min(10, collection.UnusedSettings.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + Math.Min(10, collection.Settings.Unused.Count + 1) * ImGui.GetFrameHeightWithSpacing()); using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); if (!table) return; @@ -650,7 +651,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); string? delete = null; - foreach (var (name, settings) in collection.UnusedSettings.OrderBy(n => n.Key)) + foreach (var (name, settings) in collection.Settings.Unused.OrderBy(n => n.Key)) { using var id = ImRaii.PushId(name); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index a4d60b13..ce3cc3cb 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -107,7 +107,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService var lineEnd = lineStart; // Skip the collection itself. - foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) + foreach (var inheritance in collection.Inheritance.FlatHierarchy.Skip(1)) { // Draw the child, already seen collections are colored as conflicts. using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), @@ -150,7 +150,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService DrawInheritedChildren(collection); else // We still want to keep track of conflicts. - _seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance()); + _seenInheritedCollections.UnionWith(collection.Inheritance.FlatHierarchy); } /// Draw the list box containing the current inheritance information. @@ -163,7 +163,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService _seenInheritedCollections.Clear(); _seenInheritedCollections.Add(_active.Current); - foreach (var collection in _active.Current.DirectlyInheritsFrom.ToList()) + foreach (var collection in _active.Current.Inheritance.DirectlyInheritsFrom.ToList()) DrawInheritance(collection); } @@ -180,7 +180,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); } /// @@ -244,7 +244,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); _newInheritance ??= _collections.FirstOrDefault(c - => c != _active.Current && !_active.Current.DirectlyInheritsFrom.Contains(c)) + => c != _active.Current && !_active.Current.Inheritance.DirectlyInheritsFrom.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", Name(_newInheritance)); if (!combo) @@ -271,8 +271,8 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (_movedInheritance != null) { - var idx1 = _active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); - var idx2 = _active.Current.DirectlyInheritsFrom.IndexOf(collection); + var idx1 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance); + var idx2 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -302,7 +302,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0781312c..4607434c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -201,7 +201,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { @@ -650,14 +664,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector kvp.Key)) { ImUtf8.DrawTableColumn($"{id:D6}"); ImUtf8.DrawTableColumn(name.Span); } - } } } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index c226098d..8b4913c8 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -82,7 +82,7 @@ public class ModsTab( + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); + + $"{string.Join(", ", _activeCollections.Current.Inheritance.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); } } From 282189ef6dc47edd8135a2f4811721b5d5ca032f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 17:51:17 +0100 Subject: [PATCH 2093/2451] Current State. --- Penumbra.Api | 2 +- .../Cache/CollectionCacheManager.cs | 3 + .../Collections/Manager/CollectionEditor.cs | 19 ++++- Penumbra/Mods/ModSelection.cs | 17 ++-- Penumbra/Mods/Settings/ModSettings.cs | 1 + .../Mods/Settings/TemporaryModSettings.cs | 17 ++++ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 29 ++++--- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 81 ++++++++++++------- 9 files changed, 115 insertions(+), 56 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 97e9f427..fdda2054 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623 +Subproject commit fdda2054c26a30111ac55984ed6efde7f7214b68 diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 839c0376..27b969c2 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -333,6 +333,9 @@ public class CollectionCacheManager : IDisposable, IService cache.ReloadMod(mod, true); break; + case ModSettingChange.TemporarySetting: + cache.ReloadMod(mod!, true); + break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: FullRecalculation(collection); diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 66578a95..b456686e 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -88,7 +88,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// /// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. - /// /// If the mod is currently inherited, stop the inheritance. + /// If the mod is currently inherited, stop the inheritance. /// public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { @@ -103,6 +103,18 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0) + { + key = settings?.Lock ?? key; + var old = collection.GetTempSettings(mod.Index); + if (old != null && old.Lock != 0 && old.Lock != key) + return false; + + collection.Settings.SetTemporary(mod.Index, settings); + InvokeChange(collection, ModSettingChange.TemporarySetting, mod, Setting.Indefinite, 0); + return true; + } + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) { @@ -168,7 +180,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu if (inherit == (settings == null)) return false; - ModSettings? settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + var settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); collection.Settings.Set(mod.Index, settings1); return true; } @@ -179,7 +191,8 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu { saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); - RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); + if (type is not ModSettingChange.TemporarySetting) + RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } /// Trigger changes in all inherited collections. diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 59cd5d71..b728bd00 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -36,17 +36,11 @@ public class ModSelection : EventWrapper _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); } - public ModSettings Settings { get; private set; } = ModSettings.Empty; - public ModCollection Collection { get; private set; } = ModCollection.Empty; - public Mod? Mod { get; private set; } - public ModSettings? OwnSettings { get; private set; } - - public bool IsTemporary - => OwnSettings != Settings; - - public TemporaryModSettings? AsTemporarySettings - => Settings as TemporaryModSettings; - + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + public ModSettings? OwnSettings { get; private set; } + public TemporaryModSettings? TemporarySettings { get; private set; } public void SelectMod(Mod? mod) { @@ -98,6 +92,7 @@ public class ModSelection : EventWrapper { (var settings, Collection) = _collections.Current.GetActualSettings(Mod.Index); OwnSettings = _collections.Current.GetOwnSettings(Mod.Index); + TemporarySettings = _collections.Current.GetTempSettings(Mod.Index); Settings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 671fba4d..0420ee86 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -12,6 +12,7 @@ namespace Penumbra.Mods.Settings; public class ModSettings { public static readonly ModSettings Empty = new(); + public SettingList Settings { get; internal init; } = []; public ModPriority Priority { get; set; } public bool Enabled { get; set; } diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index a0cdc2bb..27987fa6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -5,4 +5,21 @@ public sealed class TemporaryModSettings : ModSettings public string Source = string.Empty; public int Lock = 0; public bool ForceInherit; + + // Create default settings for a given mod. + public static TemporaryModSettings DefaultSettings(Mod mod, string source, int key = 0) + => new() + { + Enabled = false, + Source = source, + Lock = key, + Priority = ModPriority.Default, + Settings = SettingList.Default(mod), + }; +} + +public static class ModSettingsExtensions +{ + public static bool IsTemporary(this ModSettings? settings) + => settings is TemporaryModSettings; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index b0029f08..8f1ed8d6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -742,7 +742,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) { - if (collection != _collectionManager.Active.Current || mod != _mod) + if (collection != _collectionManager.Active.Current || mod != _mod || type is ModSettingChange.TemporarySetting) return; _swapData.LoadMod(_mod, _modSettings); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 0389730d..fbead9c3 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,8 +1,9 @@ +using ImGuiNET; using OtterGui.Custom; namespace Penumbra.UI.Classes; -public enum ColorId +public enum ColorId : short { EnabledMod, DisabledMod, @@ -10,6 +11,7 @@ public enum ColorId InheritedMod, InheritedDisabledMod, NewMod, + NewModTint, ConflictingMod, HandledConflictMod, FolderExpanded, @@ -31,10 +33,8 @@ public enum ColorId ResTreeNonNetworked, PredefinedTagAdd, PredefinedTagRemove, - TemporaryEnabledMod, - TemporaryDisabledMod, - TemporaryInheritedMod, - TemporaryInheritedDisabledMod, + TemporaryModSettingsTint, + NoTint, } public static class Colors @@ -52,6 +52,18 @@ public static class Colors public const uint ReniColorHovered = CustomGui.ReniColorHovered; public const uint ReniColorActive = CustomGui.ReniColorActive; + public static uint Tinted(this ColorId color, ColorId tint) + { + var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); + var value = ImGui.ColorConvertU32ToFloat4(color.Value()); + var negAlpha = 1 - tintValue.W; + var newAlpha = negAlpha * value.W + tintValue.W; + var newR = (negAlpha * value.W * value.X + tintValue.W * tintValue.X) / newAlpha; + var newG = (negAlpha * value.W * value.Y + tintValue.W * tintValue.Y) / newAlpha; + var newB = (negAlpha * value.W * value.Z + tintValue.W * tintValue.Z) / newAlpha; + return ImGui.ColorConvertFloat4ToU32(new Vector4(newR, newG, newB, newAlpha)); + } + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { @@ -83,10 +95,9 @@ public static class Colors ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), - ColorId.TemporaryEnabledMod => ( 0xFFFFC0A0, "Mod Enabled By Temporary Settings", "A mod that is enabled by temporary settings in the currently selected collection." ), - ColorId.TemporaryDisabledMod => ( 0xFFB08070, "Mod Disabled By Temporary Settings", "A mod that is disabled by temporary settings in the currently selected collection." ), - ColorId.TemporaryInheritedMod => ( 0xFFE8FFB0, "Mod Enabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), - ColorId.TemporaryInheritedDisabledMod => ( 0xFF90A080, "Mod Disabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + ColorId.TemporaryModSettingsTint => ( 0x30FF0000, "Mod with Temporary Settings", "A mod that has temporary settings. This color is used as a tint for the regular state colors." ), + ColorId.NewModTint => ( 0x8000FF00, "New Mod Tint", "A mod that was newly imported or created during this session and has not been enabled yet. This color is used as a tint for the regular state colors."), + ColorId.NoTint => ( 0x00000000, "No Tint", "The default tint for all mods."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 4607434c..c3cb211c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -62,6 +62,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector SetQuickMove(f, 1, _config.QuickMoveFolder2, s => { _config.QuickMoveFolder2 = s; _config.Save(); }), 120); SubscribeRightClickFolder(f => SetQuickMove(f, 2, _config.QuickMoveFolder3, s => { _config.QuickMoveFolder3 = s; _config.Save(); }), 130); SubscribeRightClickLeaf(ToggleLeafFavorite); + SubscribeRightClickLeaf(RemoveTemporarySettings); + SubscribeRightClickLeaf(DisableTemporarily); SubscribeRightClickLeaf(l => QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); @@ -194,7 +196,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Tinted(state.Tint)) .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); using var id = ImRaii.PushId(leaf.Value.Index); ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); @@ -264,6 +266,23 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) + { + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings is { Lock: 0 }) + if (ImUtf8.MenuItem("Remove Temporary Settings")) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); + } + + private void DisableTemporarily(FileSystem.Leaf mod) + { + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings == null || tempSettings.Lock == 0) + if (ImUtf8.MenuItem("Disable Temporarily")) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); + } + private void SetDefaultImportFolder(ModFileSystem.Folder folder) { if (!ImGui.MenuItem("Set As Default Import Folder")) @@ -392,8 +411,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector !_filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. - private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) + private (ColorId Color, ColorId Tint) GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) { - if (_modManager.IsNew(mod)) - return ColorId.NewMod; + var tint = settings.IsTemporary() + ? ColorId.TemporaryModSettingsTint + : _modManager.IsNew(mod) + ? ColorId.NewModTint + : ColorId.NoTint; + if (settings.IsTemporary()) + tint = ColorId.TemporaryModSettingsTint; if (settings == null) - return ColorId.UndefinedMod; + return (ColorId.UndefinedMod, tint); if (!settings.Enabled) - return collection != _collectionManager.Active.Current - ? ColorId.InheritedDisabledMod - : settings is TemporaryModSettings - ? ColorId.TemporaryDisabledMod - : ColorId.DisabledMod; - - if (settings is TemporaryModSettings) - return ColorId.TemporaryEnabledMod; + return (collection != _collectionManager.Active.Current + ? ColorId.InheritedDisabledMod + : ColorId.DisabledMod, tint); var conflicts = _collectionManager.Active.Current.Conflicts(mod); if (conflicts.Count == 0) - return collection != _collectionManager.Active.Current ? ColorId.InheritedMod : ColorId.EnabledMod; + return (collection != _collectionManager.Active.Current ? ColorId.InheritedMod : ColorId.EnabledMod, tint); - return conflicts.Any(c => !c.Solved) + return (conflicts.Any(c => !c.Solved) ? ColorId.ConflictingMod - : ColorId.HandledConflictMod; + : ColorId.HandledConflictMod, tint); } private bool CheckStateFilters(Mod mod, ModSettings? settings, ModCollection collection, ref ModState state) @@ -627,6 +645,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { @@ -664,14 +686,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Sun, 29 Dec 2024 00:05:36 +0100 Subject: [PATCH 2094/2451] Current State. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 141 +----------------- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 140 ++++++++++++++++- Penumbra/Api/IpcProviders.cs | 8 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 40 +++++ .../Collections/Manager/CollectionEditor.cs | 9 +- .../SchedulerResourceManagementService.cs | 2 +- .../Mods/Settings/TemporaryModSettings.cs | 16 ++ Penumbra/UI/Classes/Colors.cs | 24 ++- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 89 +++++++---- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 111 ++++++++++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 +- 14 files changed, 381 insertions(+), 213 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index fdda2054..882b778e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit fdda2054c26a30111ac55984ed6efde7f7214b68 +Subproject commit 882b778e78bb0806dd7d38e8b3670ff138a84a31 diff --git a/Penumbra.String b/Penumbra.String index dd83f972..0647fbc5 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936 +Subproject commit 0647fbc5017ef9ced3f3ce1c2496eefd57c5b7a8 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 4027975b..b78523d3 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,5 +1,4 @@ using OtterGui; -using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -25,20 +24,18 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; - private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator, ApiHelpers helpers) + CommunicatorService communicator) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; - _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -209,142 +206,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } - public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, - "Priority", priority, "Options", options, "Source", source, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); - } - - public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, - bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - settings = new TemporaryModSettings - { - Enabled = enabled, - Priority = new ModPriority(priority), - Lock = key, - Source = source, - Settings = SettingList.Default(mod), - }; - - foreach (var (groupName, optionNames) in options) - { - var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); - if (groupIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - - var setting = Setting.Zero; - switch (mod.Groups[groupIdx]) - { - case { Behaviour: GroupDrawBehaviour.SingleSelection } single: - { - var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting = Setting.Single(optionIdx); - break; - } - case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: - { - foreach (var name in optionNames) - { - var optionIdx = multi.Options.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting |= Setting.Multi(optionIdx); - } - - break; - } - } - - settings.Settings[groupIdx] = setting; - } - - collection.Settings.SetTemporary(mod.Index, settings); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); - } - - private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is not { } settings) - return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); - - if (settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - collection.Settings.SetTemporary(mod.Index, null); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) - { - return PenumbraApiEc.Success; - } - - public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveAllTemporaryModSettings(args, collection, key); - } - - public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) - { - var numRemoved = 0; - for (var i = 0; i < collection.Settings.Count; ++i) - { - if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) - { - collection.Settings.SetTemporary(i, null); - ++numRemoved; - } - } - - return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index eaaf9f38..894b2674 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 3); + => (5, 4); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 201839e7..afddeae8 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -1,8 +1,11 @@ +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; +using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; using Penumbra.String.Classes; @@ -13,7 +16,9 @@ public class TemporaryApi( ObjectManager objects, ActorManager actors, CollectionManager collectionManager, - TempModManager tempMods) : IPenumbraApiTemporary, IApiService + TempModManager tempMods, + ApiHelpers apiHelpers, + ModManager modManager) : IPenumbraApiTemporary, IApiService { public Guid CreateTemporaryCollection(string name) => tempCollections.CreateTemporaryCollection(name); @@ -125,6 +130,139 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } + + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool inherit, bool enabled, int priority, IReadOnlyDictionary> options, string source, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key)) + if (collection.GetTempSettings(mod.Index) is { } oldSettings && oldSettings.Lock != 0 && oldSettings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + var newSettings = new TemporaryModSettings() + { + ForceInherit = inherit, + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + + foreach (var (groupName, optionNames) in options) + { + var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting); + if (ec != PenumbraApiEc.Success) + return ApiHelpers.Return(ec, args); + + newSettings.Settings[groupIdx] = setting; + } + + if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key)) + return ApiHelpers.Return(PenumbraApiEc.Success, args); + + // This should not happen since all error cases had been checked before. + return ApiHelpers.Return(PenumbraApiEc.UnknownError, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is null) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key)) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is not null + && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) + ++numRemoved; + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + /// /// Convert a dictionary of strings to a dictionary of game paths to full paths. /// Only returns true if all paths can successfully be converted and added. diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 861225fa..6f3b2c38 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -63,7 +63,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility - new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -97,6 +97,12 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index f3c23831..2364dddf 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -33,6 +33,7 @@ public class TemporaryIpcTester( private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; private string _tempModName = string.Empty; + private string _modDirectory = string.Empty; private string _tempGamePath = "test/game/path.mtrl"; private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; @@ -50,6 +51,7 @@ public class TemporaryIpcTester( ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); @@ -121,6 +123,44 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); if (ImGui.Button("Remove##ModAll")) _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue); + + IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); + if (ImUtf8.Button("Set##SetTemporary"u8)) + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); + if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338); } public void DrawCollections() diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index b456686e..124f8cf7 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -106,8 +106,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0) { key = settings?.Lock ?? key; - var old = collection.GetTempSettings(mod.Index); - if (old != null && old.Lock != 0 && old.Lock != key) + if (!CanSetTemporarySettings(collection, mod, key)) return false; collection.Settings.SetTemporary(mod.Index, settings); @@ -115,6 +114,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) + { + var old = collection.GetTempSettings(mod.Index); + return old == null || old.Lock == 0 || old.Lock == key; + } + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) { diff --git a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs index 1d56fcdb..b7f57a44 100644 --- a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs +++ b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs @@ -69,7 +69,7 @@ public unsafe class SchedulerResourceManagementService : IService, IDisposable if (_actionTmbs.TryGetValue(tmb, out var rowId)) _listedTmbIds[rowId] = tmb; else - Penumbra.Log.Debug($"Action TMB {gamePath} encountered with no corresponding row ID."); + Penumbra.Log.Verbose($"Action TMB {gamePath} encountered with no corresponding row ID."); } [Signature(Sigs.SchedulerResourceManagementInstance, ScanType = ScanType.StaticAddress)] diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index 27987fa6..de4570c5 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -16,6 +16,22 @@ public sealed class TemporaryModSettings : ModSettings Priority = ModPriority.Default, Settings = SettingList.Default(mod), }; + + public TemporaryModSettings() + { } + + public TemporaryModSettings(ModSettings? clone, string source, int key = 0) + { + Source = source; + Lock = key; + ForceInherit = clone == null; + if (clone != null) + { + Enabled = clone.Enabled; + Priority = clone.Priority; + Settings = clone.Settings.Clone(); + } + } } public static class ModSettingsExtensions diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index fbead9c3..4c0d1694 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -56,12 +56,24 @@ public static class Colors { var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); var value = ImGui.ColorConvertU32ToFloat4(color.Value()); - var negAlpha = 1 - tintValue.W; - var newAlpha = negAlpha * value.W + tintValue.W; - var newR = (negAlpha * value.W * value.X + tintValue.W * tintValue.X) / newAlpha; - var newG = (negAlpha * value.W * value.Y + tintValue.W * tintValue.Y) / newAlpha; - var newB = (negAlpha * value.W * value.Z + tintValue.W * tintValue.Z) / newAlpha; - return ImGui.ColorConvertFloat4ToU32(new Vector4(newR, newG, newB, newAlpha)); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + public static unsafe uint Tinted(this ImGuiCol color, ColorId tint) + { + var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); + ref var value = ref *ImGui.GetStyleColorVec4(color); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + private static unsafe Vector4 TintColor(in Vector4 color, in Vector4 tint) + { + var negAlpha = 1 - tint.W; + var newAlpha = negAlpha * color.W + tint.W; + var newR = (negAlpha * color.W * color.X + tint.W * tint.X) / newAlpha; + var newG = (negAlpha * color.W * color.Y + tint.W * tint.Y) / newAlpha; + var newB = (negAlpha * color.W * color.Z + tint.W * tint.Z) / newAlpha; + return new Vector4(newR, newG, newB, newAlpha); } public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index dec77430..527d8bce 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -16,13 +17,19 @@ namespace Penumbra.UI.ModsTab.Groups; public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; + private bool _temporary; + private bool _locked; + private TemporaryModSettings? _tempSettings; - public void Draw(Mod mod, ModSettings settings) + public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { if (mod.Groups.Count <= 0) return; _blockGroupCache.Clear(); + _tempSettings = tempSettings; + _temporary = tempSettings != null; + _locked = (tempSettings?.Lock ?? 0) != 0; var useDummy = true; foreach (var (group, idx) in mod.Groups.WithIndex()) { @@ -63,22 +70,23 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + using var disabled = ImRaii.Disabled(_locked); ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + using (var combo = ImUtf8.Combo(""u8, options[selectedOption].Name)) { if (combo) for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + if (ImUtf8.Selectable(option.Name, idx2 == selectedOption)) SetModSetting(group, groupIdx, Setting.Single(idx2)); if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); + ImUtf8.SelectableHelpMarker(option.Description); id.Pop(); } @@ -86,9 +94,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle ImGui.SameLine(); if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + ImUtf8.LabeledHelpMarker(group.Name, group.Description); else - ImGui.TextUnformatted(group.Name); + ImUtf8.Text(group.Name); } /// @@ -97,10 +105,10 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -108,11 +116,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < group.Options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + if (ImUtf8.RadioButton(option.Name, selectedOption == idx)) SetModSetting(group, groupIdx, Setting.Single(idx)); if (option.Description.Length <= 0) @@ -130,28 +139,29 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); + ImUtf8.OpenPopup($"##multi{groupIdx}"); DrawMultiPopup(group, groupIdx, label); return; void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var enabled = setting.HasFlag(idx); + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); - if (ImGui.Checkbox(option.Name, ref enabled)) + if (ImUtf8.Checkbox(option.Name, ref enabled)) SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); if (option.Description.Length > 0) @@ -171,11 +181,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle return; ImGui.TextUnformatted(group.Name); + using var disabled = ImRaii.Disabled(_locked); ImGui.Separator(); - if (ImGui.Selectable("Enable All")) + if (ImUtf8.Selectable("Enable All"u8)) SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); - if (ImGui.Selectable("Disable All")) + if (ImUtf8.Selectable("Disable All"u8)) SetModSetting(group, groupIdx, Setting.Zero); } @@ -187,11 +198,11 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImUtf8.GetId("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {options.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + var buttonWidth = Math.Max(ImUtf8.CalcTextSize(buttonTextShow).X, ImUtf8.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); if (shown) @@ -204,22 +215,22 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextHide, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); ImGui.SetCursorPos(endPos); } else { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImUtf8.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextShow, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); } } @@ -228,6 +239,18 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private ModCollection Current => collectionManager.Active.Current; + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) - => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + { + if (_temporary) + { + _tempSettings!.ForceInherit = false; + _tempSettings!.Settings[groupIdx] = setting; + collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + } + else + { + collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + } + } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 261f6e92..cf64c00a 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,6 +1,5 @@ using ImGuiNET; using OtterGui.Raii; -using OtterGui; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; @@ -24,6 +23,8 @@ public class ModPanelSettingsTab( : ITab, IUiService { private bool _inherited; + private bool _temporary; + private bool _locked; private int? _currentPriority; public ReadOnlySpan Label @@ -37,11 +38,14 @@ public class ModPanelSettingsTab( public void DrawContent() { - using var child = ImRaii.Child("##settings"); + using var child = ImUtf8.Child("##settings"u8, default); if (!child) return; - _inherited = selection.Collection != collectionManager.Active.Current; + _inherited = selection.Collection != collectionManager.Active.Current; + _temporary = selection.TemporarySettings != null; + _locked = (selection.TemporarySettings?.Lock ?? 0) != 0; + DrawTemporaryWarning(); DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); @@ -54,11 +58,27 @@ public class ModPanelSettingsTab( communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); - modGroupDrawer.Draw(selection.Mod!, selection.Settings); + modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); UiHelpers.DefaultLineSpace(); communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier); } + /// Draw a big tinted bar if the current setting is temporary. + private void DrawTemporaryWarning() + { + if (!_temporary) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint)); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + if (ImUtf8.ButtonEx($"These settings are temporary from {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", width, + _locked)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); + + ImUtf8.HoverTooltip("Changing settings in temporary settings will not save them across sessions.\n"u8 + + "You can click this button to remove the temporary settings and return to your normal settings."u8); + } + /// Draw a big red bar if the current setting is inherited. private void DrawInheritedWarning() { @@ -67,22 +87,42 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + if (ImUtf8.ButtonEx($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + } + } - ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); + ImUtf8.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"u8 + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); } /// Draw a checkbox for the enabled status of the mod. private void DrawEnabledInput() { - var enabled = selection.Settings.Enabled; - if (!ImGui.Checkbox("Enabled", ref enabled)) + var enabled = selection.Settings.Enabled; + using var disabled = ImRaii.Disabled(_locked); + if (!ImUtf8.Checkbox("Enabled"u8, ref enabled)) return; modManager.SetKnown(selection.Mod!); - collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Enabled = enabled; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + } } /// @@ -91,45 +131,66 @@ public class ModPanelSettingsTab( /// private void DrawPriorityInput() { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); var settings = selection.Settings; var priority = _currentPriority ?? settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); - if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + using var disabled = ImRaii.Disabled(_locked); + if (ImUtf8.InputScalar("##Priority"u8, ref priority)) _currentPriority = priority; if (new ModPriority(priority).IsHidden) - ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != settings.Priority.Value) - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, - new ModPriority(_currentPriority.Value)); + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Priority = new ModPriority(_currentPriority.Value); + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, + new ModPriority(_currentPriority.Value)); + } + } _currentPriority = null; } - ImGuiUtil.LabeledHelpMarker("Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."); + ImUtf8.LabeledHelpMarker("Priority"u8, "Mods with a higher number here take precedence before Mods with a lower number.\n"u8 + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); } /// /// Draw a button to remove the current settings and inherit them instead - /// on the top-right corner of the window/tab. + /// in the top-right corner of the window/tab. /// private void DrawRemoveSettings() { - const string text = "Inherit Settings"; if (_inherited || selection.Settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); - if (ImGui.Button(text)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + ImGui.SameLine(ImGui.GetWindowWidth() - ImUtf8.CalcTextSize("Inherit Settings"u8).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if (!ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + return; - ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled."); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5fd38d94..c5168109 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -791,19 +791,25 @@ public class DebugTab : Window, ITab, IUiService ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); } + private string _tmbKeyFilter = string.Empty; + private CiByteString _tmbKeyFilterU8 = CiByteString.Empty; + private void DrawActionTmbs() { using var mainTree = TreeNode("Action TMBs"); if (!mainTree) return; + if (ImGui.InputText("Key", ref _tmbKeyFilter, 256)) + _tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty; using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); if (!table) return; var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - var dummy = ImGuiClip.ClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + kvp => kvp.Key.Contains(_tmbKeyFilterU8), p => { ImUtf8.DrawTableColumn($"{p.Value}"); From cff482a2ed1c542935b6912dc14731e50cfb14ad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 16:36:46 +0100 Subject: [PATCH 2095/2451] Allow non-locking, negative identifier-locks --- Penumbra/Api/Api/TemporaryApi.cs | 4 ++-- Penumbra/Collections/Manager/CollectionEditor.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index afddeae8..b12ce707 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -163,7 +163,7 @@ public class TemporaryApi( return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key)) - if (collection.GetTempSettings(mod.Index) is { } oldSettings && oldSettings.Lock != 0 && oldSettings.Lock != key) + if (collection.GetTempSettings(mod.Index) is { Lock: > 0 } oldSettings && oldSettings.Lock != key) return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); var newSettings = new TemporaryModSettings() @@ -254,7 +254,7 @@ public class TemporaryApi( var numRemoved = 0; for (var i = 0; i < collection.Settings.Count; ++i) { - if (collection.GetTempSettings(i) is not null + if (collection.GetTempSettings(i) is {} tempSettings && tempSettings.Lock == key && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) ++numRemoved; } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 124f8cf7..437d4e0b 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -117,7 +117,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) { var old = collection.GetTempSettings(mod.Index); - return old == null || old.Lock == 0 || old.Lock == key; + return old is not { Lock: > 0 } || old.Lock == key; } /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 527d8bce..b723978b 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -29,7 +29,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle _blockGroupCache.Clear(); _tempSettings = tempSettings; _temporary = tempSettings != null; - _locked = (tempSettings?.Lock ?? 0) != 0; + _locked = (tempSettings?.Lock ?? 0) > 0; var useDummy = true; foreach (var (group, idx) in mod.Groups.WithIndex()) { diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c3cb211c..091a2937 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -269,7 +269,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is { Lock: 0 }) + if (tempSettings is { Lock: <= 0 }) if (ImUtf8.MenuItem("Remove Temporary Settings")) _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); } @@ -277,7 +277,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings == null || tempSettings.Lock == 0) + if (tempSettings is not { Lock: > 0 }) if (ImUtf8.MenuItem("Disable Temporarily")) _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index cf64c00a..60666810 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -44,7 +44,7 @@ public class ModPanelSettingsTab( _inherited = selection.Collection != collectionManager.Active.Current; _temporary = selection.TemporarySettings != null; - _locked = (selection.TemporarySettings?.Lock ?? 0) != 0; + _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; DrawTemporaryWarning(); DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); From 653f6269b7b190363da723739b359b4607e764d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 16:38:15 +0100 Subject: [PATCH 2096/2451] Update submodule. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 882b778e..de0f281f 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 882b778e78bb0806dd7d38e8b3670ff138a84a31 +Subproject commit de0f281fbf9d8d9d3aa8463a28025d54877cde8d From dbef1cccb2b0ff89eec53ec64c814126f0a4f839 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 17:10:09 +0100 Subject: [PATCH 2097/2451] Fix stuff after submodule update. --- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 12 ++++++------ Penumbra/Mods/Settings/TemporaryModSettings.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 2364dddf..832fea82 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -126,27 +126,27 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); if (ImUtf8.Button("Set##SetTemporary"u8)) - _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) - _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); if (ImUtf8.Button("Remove##RemoveTemporary"u8)) - _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1337); + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1337); ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8)) - _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1338); + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1338); IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection"); if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8)) - _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1337); + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1337); ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8)) - _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1338); + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1338); IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection"); if (ImUtf8.Button("Remove##RemoveAllTemporary"u8)) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index de4570c5..425e0348 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -7,10 +7,10 @@ public sealed class TemporaryModSettings : ModSettings public bool ForceInherit; // Create default settings for a given mod. - public static TemporaryModSettings DefaultSettings(Mod mod, string source, int key = 0) + public static TemporaryModSettings DefaultSettings(Mod mod, string source, bool enabled = false, int key = 0) => new() { - Enabled = false, + Enabled = enabled, Source = source, Lock = key, Priority = ModPriority.Default, From a2258e61606474b9fb4d45be226918f5d498ae08 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 17:10:18 +0100 Subject: [PATCH 2098/2451] Add some temporary context menu things. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 163 ++++++++++--------- 2 files changed, 87 insertions(+), 78 deletions(-) diff --git a/OtterGui b/OtterGui index fcc96daa..fd387218 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fcc96daa02633f673325c14aeea6b6b568924f1e +Subproject commit fd387218d2d2d237075cb35be6ca89eeb53e14e5 diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 091a2937..280956f4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -62,8 +62,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector SetQuickMove(f, 1, _config.QuickMoveFolder2, s => { _config.QuickMoveFolder2 = s; _config.Save(); }), 120); SubscribeRightClickFolder(f => SetQuickMove(f, 2, _config.QuickMoveFolder3, s => { _config.QuickMoveFolder3 = s; _config.Save(); }), 130); SubscribeRightClickLeaf(ToggleLeafFavorite); - SubscribeRightClickLeaf(RemoveTemporarySettings); - SubscribeRightClickLeaf(DisableTemporarily); + SubscribeRightClickLeaf(DrawTemporaryOptions); SubscribeRightClickLeaf(l => QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); @@ -135,7 +134,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector m.Extensions.Any(e => ValidModExtensions.Contains(e.ToLowerInvariant())), m => { - ImGui.TextUnformatted($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); + ImUtf8.Text($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); return true; }); base.Draw(width); @@ -198,8 +197,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { - if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + if (ImUtf8.MenuItem(mod.Value.Favorite ? "Remove Favorite"u8 : "Mark as Favorite"u8)) _modManager.DataEditor.ChangeModFavorite(mod.Value, !mod.Value.Favorite); } - private void RemoveTemporarySettings(FileSystem.Leaf mod) + private void DrawTemporaryOptions(FileSystem.Leaf mod) { - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is { Lock: <= 0 }) - if (ImUtf8.MenuItem("Remove Temporary Settings")) - _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); - } + const string source = "yourself"; + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings is { Lock: > 0 }) + return; - private void DisableTemporarily(FileSystem.Leaf mod) - { - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is not { Lock: > 0 }) - if (ImUtf8.MenuItem("Disable Temporarily")) - _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, - TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); + if (tempSettings is { Lock: <= 0 } && ImUtf8.MenuItem("Remove Temporary Settings"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); + var actual = _collectionManager.Active.Current.GetActualSettings(mod.Value.Index).Settings; + if (actual?.Enabled is true && ImUtf8.MenuItem("Disable Temporarily"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + new TemporaryModSettings(actual, source) { Enabled = false }); + + if (actual is not { Enabled: true } && ImUtf8.MenuItem("Enable Temporarily"u8)) + { + var newSettings = actual is null + ? TemporaryModSettings.DefaultSettings(mod.Value, source, true) + : new TemporaryModSettings(actual, source) { Enabled = true }; + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, newSettings); + } + + if (tempSettings is null && ImUtf8.MenuItem("Turn Temporary"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + new TemporaryModSettings(actual, source)); } private void SetDefaultImportFolder(ModFileSystem.Folder folder) { - if (!ImGui.MenuItem("Set As Default Import Folder")) + if (!ImUtf8.MenuItem("Set As Default Import Folder"u8)) return; var newName = folder.FullName(); @@ -298,7 +307,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Add an import mods button that opens a file selector. private void AddImportModButton(Vector2 size) { - var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !_modManager.Valid, true); + var button = ImUtf8.IconButton(FontAwesomeIcon.FileImport, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files."u8, size, !_modManager.Valid); _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); if (!button) return; @@ -351,14 +359,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector { ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Management"); - ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); + ImUtf8.Text("Mod Management"u8); + ImUtf8.BulletText("You can create empty mods or import mods with the buttons in this row."u8); using var indent = ImRaii.PushIndent(); - ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); - ImGui.BulletText( - "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + ImUtf8.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."u8); + ImUtf8.BulletText( + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."u8); indent.Pop(1); - ImGui.BulletText("You can also create empty mod folders and delete mods."); - ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImUtf8.BulletText("You can also create empty mod folders and delete mods."u8); + ImUtf8.BulletText( + "For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."u8); ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Selector"); - ImGui.BulletText("Select a mod to obtain more information or change settings."); - ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); + ImUtf8.Text("Mod Selector"u8); + ImUtf8.BulletText("Select a mod to obtain more information or change settings."u8); + ImUtf8.BulletText("Names are colored according to your config and their current state in the collection:"u8); indent.Push(); - ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); - ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); - ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority."); - ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); - ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); + ImUtf8.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."u8); + ImUtf8.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."u8); + ImUtf8.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."u8); + ImUtf8.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."u8); + ImUtf8.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."u8); + ImUtf8.BulletTextColored(ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."u8); + ImUtf8.BulletTextColored(ColorId.ConflictingMod.Value(), + "enabled and conflicting with another enabled Mod on the same priority."u8); + ImUtf8.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."u8); + ImUtf8.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"u8); indent.Pop(1); - ImGui.BulletText("Middle-click a mod to disable it if it is enabled or enable it if it is disabled."); + ImUtf8.BulletText("Middle-click a mod to disable it if it is enabled or enable it if it is disabled."u8); indent.Push(); - ImGui.BulletText( + ImUtf8.BulletText( $"Holding {_config.DeleteModModifier.ForcedModifier(new DoubleModifier(ModifierHotkey.Control, ModifierHotkey.Shift))} while middle-clicking lets it inherit, discarding settings."); indent.Pop(1); - ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + ImUtf8.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."u8); indent.Push(); - ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + ImUtf8.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."u8); + ImUtf8.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."u8); indent.Pop(1); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImUtf8.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."u8); indent.Push(); - ImGui.BulletText( - "You can select multiple mods and folders by holding Control while clicking them, and then drag all of them at once."); - ImGui.BulletText( - "Selected mods inside an also selected folder will be ignored when dragging and move inside their folder instead of directly into the target."); + ImUtf8.BulletText( + "You can select multiple mods and folders by holding Control while clicking them, and then drag all of them at once."u8); + ImUtf8.BulletText( + "Selected mods inside an also selected folder will be ignored when dragging and move inside their folder instead of directly into the target."u8); indent.Pop(1); - ImGui.BulletText("Right-clicking a folder opens a context menu."); - ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); - ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + ImUtf8.BulletText("Right-clicking a folder opens a context menu."u8); + ImUtf8.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."u8); + ImUtf8.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."u8); indent.Push(); - ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); - ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); - ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + ImUtf8.BulletText("You can enter n:[string] to filter only for names, without path."u8); + ImUtf8.BulletText("You can enter c:[string] to filter for Changed Items instead."u8); + ImUtf8.BulletText("You can enter a:[string] to filter for Mod Authors instead."u8); indent.Pop(1); - ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + ImUtf8.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."u8); }); } @@ -729,7 +738,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Tue, 31 Dec 2024 17:56:58 +0100 Subject: [PATCH 2099/2451] Keep enabled and priority at the top of settings, add button to turn temporary. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 61 ++++++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 60666810..260caf26 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -38,16 +38,19 @@ public class ModPanelSettingsTab( public void DrawContent() { - using var child = ImUtf8.Child("##settings"u8, default); - if (!child) + using var table = ImUtf8.Table("##settings"u8, 1, ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table) return; _inherited = selection.Collection != collectionManager.Active.Current; _temporary = selection.TemporarySettings != null; _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); DrawTemporaryWarning(); DrawInheritedWarning(); - UiHelpers.DefaultLineSpace(); + ImGui.Dummy(Vector2.Zero); communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); DrawEnabledInput(); tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); @@ -56,6 +59,7 @@ public class ModPanelSettingsTab( tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); + ImGui.TableNextColumn(); communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); @@ -71,7 +75,8 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint)); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImUtf8.ButtonEx($"These settings are temporary from {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", width, + if (ImUtf8.ButtonEx($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", + width, _locked)) collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); @@ -174,23 +179,45 @@ public class ModPanelSettingsTab( /// private void DrawRemoveSettings() { - if (_inherited || selection.Settings == ModSettings.Empty) + var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; + if (!drawInherited && _temporary) return; - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine(ImGui.GetWindowWidth() - ImUtf8.CalcTextSize("Inherit Settings"u8).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); - if (!ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 - + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) - return; + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X: 0; + var offset = (drawInherited, _temporary) switch + { + (true, true) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 2, + (false, false) => ImUtf8.CalcTextSize("Turn Temporary"u8).X + ImGui.GetStyle().FramePadding.X * 2, + (true, false) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + + ImUtf8.CalcTextSize("Turn Temporary"u8).X + + ImGui.GetStyle().FramePadding.X * 4 + + ImGui.GetStyle().ItemSpacing.X, + (false, true) => 0, // can not happen + }; - if (_temporary) + ImGui.SameLine(ImGui.GetWindowWidth() - offset - scroll); + if (!_temporary + && ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + new TemporaryModSettings(selection.Settings, "yourself")); + if (drawInherited) { - selection.TemporarySettings!.ForceInherit = true; - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); - } - else - { - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + if (!_temporary) + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); + if (ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } + } } } } From 6374362b2871a5fefe68a13a04a8da2de9171869 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 31 Dec 2024 17:05:32 +0000 Subject: [PATCH 2100/2451] [CI] Updating repo.json for testing_1.3.2.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 97e55af0..1b952d94 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.2.0", - "TestingAssemblyVersion": "1.3.2.1", + "TestingAssemblyVersion": "1.3.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0eed5f1707a025e376452d6b9b83f2b74d61db9a Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Sat, 21 Dec 2024 01:16:57 +0100 Subject: [PATCH 2101/2451] Add a watched plugin to Support Info --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 69dfe3e8..33ce9f40 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -187,7 +187,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From af7a8fbddd2b9285e176a03bc720733dddbb436b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:10:37 +0100 Subject: [PATCH 2102/2451] Fix bug with atch counter. --- Penumbra/Collections/CollectionCounters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/CollectionCounters.cs b/Penumbra/Collections/CollectionCounters.cs index 91d240d6..6ca0d0a0 100644 --- a/Penumbra/Collections/CollectionCounters.cs +++ b/Penumbra/Collections/CollectionCounters.cs @@ -24,5 +24,5 @@ public struct CollectionCounters(int changeCounter) /// Increment the number of ATCH-relevant changes in the effective file list. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IncrementAtch() - => ++Imc; + => ++Atch; } From 9a457a1a953b8bf6bdce428ce62eb0bd5d027582 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:11:05 +0100 Subject: [PATCH 2103/2451] Add debug panel to check changed item identification for paths. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index c5168109..8b2bcd77 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -36,6 +36,7 @@ using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; +using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; @@ -102,6 +103,7 @@ public class DebugTab : Window, ITab, IUiService private readonly HookOverrideDrawer _hookOverrides; private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; + private readonly ObjectIdentification _objectIdentification; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -112,7 +114,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -151,6 +153,7 @@ public class DebugTab : Window, ITab, IUiService _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; + _objectIdentification = objectIdentification; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -734,8 +737,37 @@ public class DebugTab : Window, ITab, IUiService DrawActionTmbs(); DrawStainTemplates(); DrawAtch(); + DrawChangedItemTest(); } + private string _changedItemPath = string.Empty; + private readonly Dictionary _changedItems = []; + + private void DrawChangedItemTest() + { + using var node = TreeNode("Changed Item Test"); + if (!node) + return; + + if (ImUtf8.InputText("##ChangedItemTest"u8, ref _changedItemPath, "Changed Item File Path..."u8)) + { + _changedItems.Clear(); + _objectIdentification.Identify(_changedItems, _changedItemPath); + } + + if (_changedItems.Count == 0) + return; + + using var list = ImUtf8.ListBox("##ChangedItemList"u8, + new Vector2(ImGui.GetContentRegionAvail().X, 8 * ImGui.GetTextLineHeightWithSpacing())); + if (!list) + return; + + foreach (var item in _changedItems) + ImUtf8.Selectable(item.Key); + } + + private string _emoteSearchFile = string.Empty; private string _emoteSearchName = string.Empty; From 756537c7760448176a254a8e679e7dbad0afebb1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:11:26 +0100 Subject: [PATCH 2104/2451] Add Turn Permanent button for temporary settings and improve buttons, make secure. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 82 +++++++++++++++------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 260caf26..417c7be2 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -19,7 +20,8 @@ public class ModPanelSettingsTab( ModSelection selection, TutorialService tutorial, CommunicatorService communicator, - ModGroupDrawer modGroupDrawer) + ModGroupDrawer modGroupDrawer, + Configuration config) : ITab, IUiService { private bool _inherited; @@ -180,32 +182,28 @@ public class ModPanelSettingsTab( private void DrawRemoveSettings() { var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; - if (!drawInherited && _temporary) - return; - - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X: 0; - var offset = (drawInherited, _temporary) switch - { - (true, true) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 2, - (false, false) => ImUtf8.CalcTextSize("Turn Temporary"u8).X + ImGui.GetStyle().FramePadding.X * 2, - (true, false) => ImUtf8.CalcTextSize("Inherit Settings"u8).X - + ImUtf8.CalcTextSize("Turn Temporary"u8).X - + ImGui.GetStyle().FramePadding.X * 4 - + ImGui.GetStyle().ItemSpacing.X, - (false, true) => 0, // can not happen - }; - + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X : 0; + var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; + var offset = drawInherited + ? buttonSize + ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 4 + ImGui.GetStyle().ItemSpacing.X + : buttonSize + ImGui.GetStyle().FramePadding.X * 2; ImGui.SameLine(ImGui.GetWindowWidth() - offset - scroll); - if (!_temporary - && ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - new TemporaryModSettings(selection.Settings, "yourself")); + var enabled = config.DeleteModModifier.IsActive(); if (drawInherited) { - if (!_temporary) - ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); - if (ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 - + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + var inherit = (enabled, _locked) switch + { + (true, false) => ImUtf8.ButtonEx("Inherit Settings"u8, + "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, false), + (false, false) => ImUtf8.ButtonEx("Inherit Settings"u8, + $"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.", + default, true), + (_, true) => ImUtf8.ButtonEx("Inherit Settings"u8, + "Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8, + default, true), + }; + if (inherit) { if (_temporary) { @@ -218,6 +216,42 @@ public class ModPanelSettingsTab( collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); } } + + ImGui.SameLine(); + } + + if (_temporary) + { + var overwrite = enabled + ? ImUtf8.ButtonEx("Turn Permanent"u8, + "Overwrite the actual settings for this mod in this collection with the current temporary settings."u8, + new Vector2(buttonSize, 0)) + : ImUtf8.ButtonEx("Turn Permanent"u8, + $"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.", + new Vector2(buttonSize, 0), true); + if (overwrite) + { + var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!; + if (settings.ForceInherit) + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority); + foreach (var (setting, index) in settings.Settings.WithIndex()) + collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting); + } + + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null); + } + } + else + { + if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + new TemporaryModSettings(selection.Settings, "yourself")); } } } From 349241d0ab9cf8bd9cfb19031dfe0b12202c42a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:49:19 +0100 Subject: [PATCH 2105/2451] Better attribution of authors in item swap. --- Penumbra/Mods/ModCreator.cs | 4 +-- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 31 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 1af9c1db..bdc16b72 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -30,12 +30,12 @@ public partial class ModCreator( public readonly Configuration Config = config; /// Creates directory and files necessary for a new mod without adding it to the manager. - public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "") + public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) { try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 8f1ed8d6..e590eb1e 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -12,6 +12,7 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; @@ -275,16 +276,38 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Hair: case SwapType.Tail: return - $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}"; case SwapType.BetweenSlots: return - $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}."; + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; default: return - $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}."; + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; } } + private string OriginalAuthor() + { + if (_mod!.Author.IsEmpty || _mod!.Author.Text is "TexTools User" or DefaultTexToolsData.Author) + return "."; + + return $" by {_mod!.Author}."; + } + + private string CreateAuthor() + { + if (_mod!.Author.IsEmpty) + return _config.DefaultModAuthor; + if (_mod!.Author.Text == _config.DefaultModAuthor) + return _config.DefaultModAuthor; + if (_mod!.Author.Text is "TexTools User" or DefaultTexToolsData.Author) + return _config.DefaultModAuthor; + if (_config.DefaultModAuthor is DefaultTexToolsData.Author) + return _mod!.Author; + + return $"{_mod!.Author} (Swap by {_config.DefaultModAuthor})"; + } + private void UpdateOption() { _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName); @@ -296,7 +319,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private void CreateMod() { - var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription()); + var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription(), CreateAuthor()); if (newDir == null) return; From f07780cf7babb92e7421bea0df59ad8d6dc00592 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 8 Jan 2025 20:02:14 +0100 Subject: [PATCH 2106/2451] Add RenderTargetHdrEnabler --- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../PostProcessing/RenderTargetHdrEnabler.cs | 136 ++++++++++++++++++ .../PostProcessing/ShaderReplacementFixer.cs | 9 ++ Penumbra/Penumbra.json | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 54 ++++++- Penumbra/UI/Tabs/SettingsTab.cs | 17 +++ 8 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 33de79bc..d5f92966 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c +Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ec5784f8..df44a51a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -110,6 +110,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; public bool EditRawTileTransforms { get; set; } = false; + public bool HdrRenderTargets { get; set; } = true; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 2aeeb14b..b95e5789 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -86,6 +86,7 @@ public class HookOverrides public bool ModelRendererOnRenderMaterial; public bool ModelRendererUnkFunc; public bool PrepareColorTable; + public bool RenderTargetManagerInitialize; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs new file mode 100644 index 00000000..d620935e --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -0,0 +1,136 @@ +using System.Collections.Immutable; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public unsafe class RenderTargetHdrEnabler : IService, IDisposable +{ + /// This array must be sorted by CreationOrder ascending. + private static readonly ImmutableArray ForcedTextureConfigs = + [ + new(9, TextureFormat.R16G16B16A16_FLOAT, "Main Diffuse GBuffer"), + new(10, TextureFormat.R16G16B16A16_FLOAT, "Hair Diffuse GBuffer"), + ]; + + private static readonly IComparer ForcedTextureConfigComparer + = Comparer.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder)); + + private readonly Configuration _config; + + private readonly ThreadLocal _textureIndices = new(() => new(-1, -1)); + private readonly ThreadLocal?> _textures = new(() => null); + + public TextureReportRecord[]? TextureReport { get; private set; } + + [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] + private Hook _renderTargetManagerInitialize = null!; + + [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] + private Hook _createTexture2D = null!; + + public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) + { + _config = config; + interop.InitializeFromAttributes(this); + if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) + _renderTargetManagerInitialize.Enable(); + } + + ~RenderTargetHdrEnabler() + => Dispose(false); + + public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) + { + var i = ForcedTextureConfigs.BinarySearch(new(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); + return i >= 0 ? ForcedTextureConfigs[i] : null; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool _) + { + _renderTargetManagerInitialize.Disable(); + if (_createTexture2D.IsEnabled) + _createTexture2D.Disable(); + + _createTexture2D.Dispose(); + _renderTargetManagerInitialize.Dispose(); + } + + private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) + { + _createTexture2D.Enable(); + _textureIndices.Value = new(0, 0); + _textures.Value = _config.DebugMode ? [] : null; + try + { + return _renderTargetManagerInitialize.Original(@this); + } + finally + { + if (_textures.Value != null) + { + TextureReport = CreateTextureReport(@this, _textures.Value); + _textures.Value = null; + } + _textureIndices.Value = new(-1, -1); + _createTexture2D.Disable(); + } + } + + private Texture* CreateTexture2DDetour( + Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + { + var originalTextureFormat = textureFormat; + var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new(-1, -1); + if (indices.ConfigIndex >= 0 && indices.ConfigIndex < ForcedTextureConfigs.Length && + ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) + { + var config = ForcedTextureConfigs[indices.ConfigIndex++]; + textureFormat = (uint)config.ForcedTextureFormat; + } + + if (indices.CreationOrder >= 0) + { + ++indices.CreationOrder; + _textureIndices.Value = indices; + } + + var texture = _createTexture2D.Original(@this, size, mipLevel, textureFormat, flags, unk); + if (_textures.IsValueCreated) + _textures.Value?.Add((nint)texture, (indices.CreationOrder - 1, originalTextureFormat)); + return texture; + } + + private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, Dictionary textures) + { + var rtmTextures = new Span(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint)); + var report = new List(); + for (var i = 0; i < rtmTextures.Length; ++i) + { + if (textures.TryGetValue(rtmTextures[i], out var texture)) + report.Add(new(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); + } + return report.ToArray(); + } + + private delegate nint RenderTargetManagerInitializeFunc(RenderTargetManager* @this); + + private delegate Texture* CreateTexture2DFunc(Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk); + + private record struct TextureIndices(int CreationOrder, int ConfigIndex); + + public readonly record struct ForcedTextureConfig(int CreationOrder, TextureFormat ForcedTextureFormat, string Comment); + + public readonly record struct TextureReportRecord(nint Offset, int CreationOrder, TextureFormat OriginalTextureFormat); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 40958eb4..3b41e752 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -7,6 +7,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.Services; @@ -462,8 +463,16 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return mtrlResource; } + private static int GetDataSetExpectedSize(uint dataFlags) + => (dataFlags & 4) != 0 + ? ColorTable.Size + ((dataFlags & 8) != 0 ? ColorDyeTable.Size : 0) + : 0; + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { + if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) + Penumbra.Log.Warning($"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForColorTable() == 0) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 4790da18..968bb750 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -10,7 +10,7 @@ "Tags": [ "modding" ], "DalamudApiLevel": 11, "LoadPriority": 69420, - "LoadState": 2, + "LoadRequiredState": 2, "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8b2bcd77..a759e11a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,7 @@ using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; +using CSGraphics = FFXIVClientStructs.FFXIV.Client.Graphics; namespace Penumbra.UI.Tabs.Debug; @@ -104,6 +105,7 @@ public class DebugTab : Window, ITab, IUiService private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; + private readonly RenderTargetHdrEnabler _renderTargetHdrEnabler; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -114,7 +116,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetHdrEnabler renderTargetHdrEnabler) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -154,6 +156,7 @@ public class DebugTab : Window, ITab, IUiService _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; _objectIdentification = objectIdentification; + _renderTargetHdrEnabler = renderTargetHdrEnabler; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -189,6 +192,7 @@ public class DebugTab : Window, ITab, IUiService DrawData(); DrawCrcCache(); DrawResourceProblems(); + DrawRenderTargets(); _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); @@ -1135,6 +1139,54 @@ public class DebugTab : Window, ITab, IUiService } + /// Draw information about render targets. + private unsafe void DrawRenderTargets() + { + if (!ImGui.CollapsingHeader("Render Targets")) + return; + + var report = _renderTargetHdrEnabler.TextureReport; + if (report == null) + { + ImGui.TextUnformatted("The RenderTargetManager report has not been gathered."); + ImGui.TextUnformatted("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."); + return; + } + + using var table = Table("##RenderTargetTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var record in report) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"0x{record.Offset:X}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{record.CreationOrder}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{record.OriginalTextureFormat}"); + ImGui.TableNextColumn(); + var texture = *(CSGraphics.Kernel.Texture**)((nint)CSGraphics.Render.RenderTargetManager.Instance() + record.Offset); + if (texture != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), texture->TextureFormat != record.OriginalTextureFormat); + ImUtf8.Text($"{texture->TextureFormat}"); + } + ImGui.TableNextColumn(); + var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); + if (forcedConfig.HasValue) + ImGui.TextUnformatted(forcedConfig.Value.Comment); + } + } + + /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 46e214cf..64fa57a5 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -773,6 +773,7 @@ public class SettingsTab : ITab, IUiService DrawCrashHandler(); DrawMinimumDimensionConfig(); + DrawHdrRenderTargets(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); @@ -902,6 +903,22 @@ public class SettingsTab : ITab, IUiService _config.Save(); } + private void DrawHdrRenderTargets() + { + var item = _config.HdrRenderTargets ? 1 : 0; + ImGui.SetNextItemWidth(ImGui.CalcTextSize("M").X * 5.0f + ImGui.GetFrameHeight()); + var edited = ImGui.Combo("##hdrRenderTarget", ref item, "SDR\0HDR\0"); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Diffuse Dynamic Range", + "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\nChanging this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."); + + if (!edited) + return; + + _config.HdrRenderTargets = item != 0; + _config.Save(); + } + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { From e8300fc5c83acc6f86dbaa5086869f721478317b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 9 Jan 2025 20:42:48 +0100 Subject: [PATCH 2107/2451] Improve RT-HDR texture comments --- .../Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index d620935e..80106fc9 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -14,8 +14,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable /// This array must be sorted by CreationOrder ascending. private static readonly ImmutableArray ForcedTextureConfigs = [ - new(9, TextureFormat.R16G16B16A16_FLOAT, "Main Diffuse GBuffer"), - new(10, TextureFormat.R16G16B16A16_FLOAT, "Hair Diffuse GBuffer"), + new(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), + new(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), ]; private static readonly IComparer ForcedTextureConfigComparer From b83564bce8424daaa7b0facfb91a19dac6062850 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jan 2025 19:55:33 +0100 Subject: [PATCH 2108/2451] 1.3.3.0 --- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c78ca290..f83c8989 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -56,10 +56,28 @@ public class PenumbraChangelog : IUiService Add1_3_0_0(Changelog); Add1_3_1_0(Changelog); Add1_3_2_0(Changelog); + Add1_3_3_0(Changelog); } #region Changelogs + private static void Add1_3_3_0(Changelog log) + => log.NextVersion("Version 1.3.3.0") + .RegisterHighlight("Added Temporary Settings to collections.") + .RegisterEntry("Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", 1) + .RegisterEntry("This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", 1) + .RegisterEntry("More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", 1) + .RegisterEntry("As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", 1) + .RegisterEntry("This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", 1) + .RegisterHighlight("Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") + .RegisterEntry("Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") + .RegisterHighlight("Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") + .RegisterEntry("The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") + .RegisterEntry("When creating new mods with Item Swap, the attributed author of the resulting mod was improved.") + .RegisterEntry("Fixed an issue with rings in the On-Screen tab and in the data sent over to other plugins via IPC.") + .RegisterEntry("Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") + .RegisterEntry("Fixed some ImGui assertions."); + private static void Add1_3_2_0(Changelog log) => log.NextVersion("Version 1.3.2.0") .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") From e6872cff64a8764d21bc7d37a2e11083825b3596 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 10 Jan 2025 19:00:27 +0000 Subject: [PATCH 2109/2451] [CI] Updating repo.json for 1.3.3.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1b952d94..25dd6da4 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.2.0", - "TestingAssemblyVersion": "1.3.2.2", + "AssemblyVersion": "1.3.3.0", + "TestingAssemblyVersion": "1.3.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From d4e6688369961f38b365ee4611dc7d1a807ef7b4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:26:43 +0100 Subject: [PATCH 2110/2451] Fix issue when empty settings are turned temporary. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 417c7be2..2420f06b 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -249,9 +249,10 @@ public class ModPanelSettingsTab( } else { + var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings; if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - new TemporaryModSettings(selection.Settings, "yourself")); + new TemporaryModSettings(actual, "yourself")); } } } From 0758739666917a98304d8cdb8288924a67152084 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:46:08 +0100 Subject: [PATCH 2111/2451] Cleanup UI code. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 57 ++----------------- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 59 ++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 34 +++++++---- 3 files changed, 86 insertions(+), 64 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index a759e11a..77eeb3d7 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,7 +42,6 @@ using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; -using CSGraphics = FFXIVClientStructs.FFXIV.Client.Graphics; namespace Penumbra.UI.Tabs.Debug; @@ -105,7 +104,7 @@ public class DebugTab : Window, ITab, IUiService private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; - private readonly RenderTargetHdrEnabler _renderTargetHdrEnabler; + private readonly RenderTargetDrawer _renderTargetDrawer; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -116,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetHdrEnabler renderTargetHdrEnabler) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -156,7 +155,7 @@ public class DebugTab : Window, ITab, IUiService _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; _objectIdentification = objectIdentification; - _renderTargetHdrEnabler = renderTargetHdrEnabler; + _renderTargetDrawer = renderTargetDrawer; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -192,7 +191,7 @@ public class DebugTab : Window, ITab, IUiService DrawData(); DrawCrcCache(); DrawResourceProblems(); - DrawRenderTargets(); + _renderTargetDrawer.Draw(); _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); @@ -1139,54 +1138,6 @@ public class DebugTab : Window, ITab, IUiService } - /// Draw information about render targets. - private unsafe void DrawRenderTargets() - { - if (!ImGui.CollapsingHeader("Render Targets")) - return; - - var report = _renderTargetHdrEnabler.TextureReport; - if (report == null) - { - ImGui.TextUnformatted("The RenderTargetManager report has not been gathered."); - ImGui.TextUnformatted("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."); - return; - } - - using var table = Table("##RenderTargetTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); - ImGui.TableHeadersRow(); - - foreach (var record in report) - { - ImGui.TableNextColumn(); - ImUtf8.Text($"0x{record.Offset:X}"); - ImGui.TableNextColumn(); - ImUtf8.Text($"{record.CreationOrder}"); - ImGui.TableNextColumn(); - ImUtf8.Text($"{record.OriginalTextureFormat}"); - ImGui.TableNextColumn(); - var texture = *(CSGraphics.Kernel.Texture**)((nint)CSGraphics.Render.RenderTargetManager.Instance() + record.Offset); - if (texture != null) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), texture->TextureFormat != record.OriginalTextureFormat); - ImUtf8.Text($"{texture->TextureFormat}"); - } - ImGui.TableNextColumn(); - var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); - if (forcedConfig.HasValue) - ImGui.TextUnformatted(forcedConfig.Value.Comment); - } - } - - /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs new file mode 100644 index 00000000..09c8b06c --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -0,0 +1,59 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using ImGuiNET; +using OtterGui; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Hooks.PostProcessing; + +namespace Penumbra.UI.Tabs.Debug; + +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler) : IUiService +{ + /// Draw information about render targets. + public unsafe void Draw() + { + if (!ImUtf8.CollapsingHeader("Render Targets"u8)) + return; + + var report = renderTargetHdrEnabler.TextureReport; + if (report == null) + { + ImUtf8.Text("The RenderTargetManager report has not been gathered."u8); + ImUtf8.Text("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."u8); + return; + } + + using var table = ImUtf8.Table("##RenderTargetTable"u8, 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var record in report) + { + ImUtf8.DrawTableColumn($"0x{record.Offset:X}"); + ImUtf8.DrawTableColumn($"{record.CreationOrder}"); + ImUtf8.DrawTableColumn($"{record.OriginalTextureFormat}"); + ImGui.TableNextColumn(); + var texture = *(Texture**)((nint)RenderTargetManager.Instance() + + record.Offset); + if (texture != null) + { + using var color = Dalamud.Interface.Utility.Raii.ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), + texture->TextureFormat != record.OriginalTextureFormat); + ImUtf8.Text($"{texture->TextureFormat}"); + } + + ImGui.TableNextColumn(); + var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); + if (forcedConfig.HasValue) + ImUtf8.Text(forcedConfig.Value.Comment); + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 64fa57a5..c7f66859 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -10,6 +10,7 @@ using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; @@ -905,18 +906,29 @@ public class SettingsTab : ITab, IUiService private void DrawHdrRenderTargets() { - var item = _config.HdrRenderTargets ? 1 : 0; - ImGui.SetNextItemWidth(ImGui.CalcTextSize("M").X * 5.0f + ImGui.GetFrameHeight()); - var edited = ImGui.Combo("##hdrRenderTarget", ref item, "SDR\0HDR\0"); + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("M"u8).X * 5.0f + ImGui.GetFrameHeight()); + using (var combo = ImUtf8.Combo("##hdrRenderTarget"u8, _config.HdrRenderTargets ? "HDR"u8 : "SDR"u8)) + { + if (combo) + { + if (ImUtf8.Selectable("HDR"u8, _config.HdrRenderTargets) && !_config.HdrRenderTargets) + { + _config.HdrRenderTargets = true; + _config.Save(); + } + + if (ImUtf8.Selectable("SDR"u8, !_config.HdrRenderTargets) && _config.HdrRenderTargets) + { + _config.HdrRenderTargets = false; + _config.Save(); + } + } + } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Diffuse Dynamic Range", - "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\nChanging this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."); - - if (!edited) - return; - - _config.HdrRenderTargets = item != 0; - _config.Save(); + ImUtf8.LabeledHelpMarker("Diffuse Dynamic Range"u8, + "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\n"u8 + + "Changing this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."u8); } /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. From e73b3e85bdd9e136761108299040433a3314e937 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:46:28 +0100 Subject: [PATCH 2112/2451] Autoformat and remove nagging. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 41 +++++++++-------- .../PostProcessing/ShaderReplacementFixer.cs | 46 +++++++++---------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index 80106fc9..b7ae771b 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -14,8 +14,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable /// This array must be sorted by CreationOrder ascending. private static readonly ImmutableArray ForcedTextureConfigs = [ - new(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), - new(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), + new ForcedTextureConfig(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), + new ForcedTextureConfig(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), ]; private static readonly IComparer ForcedTextureConfigComparer @@ -23,16 +23,17 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private readonly Configuration _config; - private readonly ThreadLocal _textureIndices = new(() => new(-1, -1)); + private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); + private readonly ThreadLocal?> _textures = new(() => null); public TextureReportRecord[]? TextureReport { get; private set; } [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] - private Hook _renderTargetManagerInitialize = null!; + private readonly Hook _renderTargetManagerInitialize = null!; [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] - private Hook _createTexture2D = null!; + private readonly Hook _createTexture2D = null!; public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) { @@ -47,7 +48,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) { - var i = ForcedTextureConfigs.BinarySearch(new(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); + var i = ForcedTextureConfigs.BinarySearch(new ForcedTextureConfig(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); return i >= 0 ? ForcedTextureConfigs[i] : null; } @@ -59,10 +60,6 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private void Dispose(bool _) { - _renderTargetManagerInitialize.Disable(); - if (_createTexture2D.IsEnabled) - _createTexture2D.Disable(); - _createTexture2D.Dispose(); _renderTargetManagerInitialize.Dispose(); } @@ -70,8 +67,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { _createTexture2D.Enable(); - _textureIndices.Value = new(0, 0); - _textures.Value = _config.DebugMode ? [] : null; + _textureIndices.Value = new TextureIndices(0, 0); + _textures.Value = _config.DebugMode ? [] : null; try { return _renderTargetManagerInitialize.Original(@this); @@ -80,10 +77,11 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable { if (_textures.Value != null) { - TextureReport = CreateTextureReport(@this, _textures.Value); + TextureReport = CreateTextureReport(@this, _textures.Value); _textures.Value = null; } - _textureIndices.Value = new(-1, -1); + + _textureIndices.Value = new TextureIndices(-1, -1); _createTexture2D.Disable(); } } @@ -92,9 +90,10 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) { var originalTextureFormat = textureFormat; - var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new(-1, -1); - if (indices.ConfigIndex >= 0 && indices.ConfigIndex < ForcedTextureConfigs.Length && - ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) + var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new TextureIndices(-1, -1); + if (indices.ConfigIndex >= 0 + && indices.ConfigIndex < ForcedTextureConfigs.Length + && ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) { var config = ForcedTextureConfigs[indices.ConfigIndex++]; textureFormat = (uint)config.ForcedTextureFormat; @@ -112,15 +111,17 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable return texture; } - private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, Dictionary textures) + private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, + Dictionary textures) { var rtmTextures = new Span(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint)); - var report = new List(); + var report = new List(); for (var i = 0; i < rtmTextures.Length; ++i) { if (textures.TryGetValue(rtmTextures[i], out var texture)) - report.Add(new(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); + report.Add(new TextureReportRecord(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); } + return report.ToArray(); } diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 3b41e752..f70ea06e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -64,8 +64,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - private readonly ModelRenderer _modelRenderer; private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; @@ -111,31 +109,31 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _modelRenderer = modelRenderer; - _communicator = communicator; - _humanSetupScalingHook = humanSetupScalingHook; + var utility1 = utility; + var modelRenderer1 = modelRenderer; + _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultSkinShpkResource); _characterStockingsState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterStockingsShpkResource); _characterLegacyState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); - _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); - _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, - () => _modelRenderer.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTransparencyShaderPackage, - () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTattooShaderPackage, - () => _modelRenderer.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new ModdedShaderPackageState(() => _modelRenderer.CharacterOcclusionShaderPackage, - () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterLegacyShpkResource); + _irisState = new ModdedShaderPackageState(() => modelRenderer1.IrisShaderPackage, () => modelRenderer1.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => modelRenderer1.CharacterGlassShaderPackage, + () => modelRenderer1.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTransparencyShaderPackage, + () => modelRenderer1.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTattooShaderPackage, + () => modelRenderer1.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer1.CharacterOcclusionShaderPackage, + () => modelRenderer1.DefaultCharacterOcclusionShaderPackage); _hairMaskState = - new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + new ModdedShaderPackageState(() => modelRenderer1.HairMaskShaderPackage, () => modelRenderer1.DefaultHairMaskShaderPackage); _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], @@ -463,6 +461,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return mtrlResource; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static int GetDataSetExpectedSize(uint dataFlags) => (dataFlags & 4) != 0 ? ColorTable.Size + ((dataFlags & 8) != 0 ? ColorDyeTable.Size : 0) @@ -471,7 +470,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) - Penumbra.Log.Warning($"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); + Penumbra.Log.Warning( + $"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForColorTable() == 0) From c99a7884bb38d8ce2fd1d21c4b705e51b946369e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 12:49:05 +0000 Subject: [PATCH 2113/2451] [CI] Updating repo.json for 1.3.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 25dd6da4..41956dc3 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.3.0", - "TestingAssemblyVersion": "1.3.3.0", + "AssemblyVersion": "1.3.3.1", + "TestingAssemblyVersion": "1.3.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 7b2e82b27f1f378d82e68055df48ae758af0ad71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 15:22:04 +0100 Subject: [PATCH 2114/2451] Add some HDR related debug data and support info. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 36 ++++++++++++++-- .../PostProcessing/ShaderReplacementFixer.cs | 34 ++++++++------- Penumbra/Penumbra.cs | 13 +++--- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 41 ++++++++++++++++++- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index b7ae771b..41c4dab1 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -1,11 +1,13 @@ using System.Collections.Immutable; using Dalamud.Hooking; +using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui.Services; using Penumbra.GameData; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.PostProcessing; @@ -21,9 +23,9 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private static readonly IComparer ForcedTextureConfigComparer = Comparer.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder)); - private readonly Configuration _config; - - private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); + private readonly Configuration _config; + private readonly Tuple _share; + private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); private readonly ThreadLocal?> _textures = new(() => null); @@ -35,17 +37,42 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] private readonly Hook _createTexture2D = null!; - public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) + public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config, IDalamudPluginInterface pi, + DalamudConfigService dalamudConfig) { _config = config; interop.InitializeFromAttributes(this); if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) _renderTargetManagerInitialize.Enable(); + + _share = pi.GetOrCreateData("Penumbra.RenderTargetHDR.V1", () => + { + bool? waitForPlugins = dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s) ? s : null; + return new Tuple(waitForPlugins, config.HdrRenderTargets, + !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize, [0], [false]); + }); + ++_share.Item4[0]; } + public bool? FirstLaunchWaitForPluginsState + => _share.Item1; + + public bool FirstLaunchHdrState + => _share.Item2; + + public bool FirstLaunchHdrHookOverrideState + => _share.Item3; + + public int PenumbraReloadCount + => _share.Item4[0]; + + public bool HdrEnabledSuccess + => _share.Item5[0]; + ~RenderTargetHdrEnabler() => Dispose(false); + public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) { var i = ForcedTextureConfigs.BinarySearch(new ForcedTextureConfig(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); @@ -67,6 +94,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { _createTexture2D.Enable(); + _share.Item5[0] = true; _textureIndices.Value = new TextureIndices(0, 0); _textures.Value = _config.DebugMode ? [] : null; try diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index f70ea06e..cae37776 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -109,31 +109,29 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - var utility1 = utility; - var modelRenderer1 = modelRenderer; _communicator = communicator; _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->SkinShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultSkinShpkResource); + () => (ShaderPackageResourceHandle**)&utility.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultSkinShpkResource); _characterStockingsState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterStockingsShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterStockingsShpkResource); + () => (ShaderPackageResourceHandle**)&utility.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultCharacterStockingsShpkResource); _characterLegacyState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterLegacyShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterLegacyShpkResource); - _irisState = new ModdedShaderPackageState(() => modelRenderer1.IrisShaderPackage, () => modelRenderer1.DefaultIrisShaderPackage); - _characterGlassState = new ModdedShaderPackageState(() => modelRenderer1.CharacterGlassShaderPackage, - () => modelRenderer1.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTransparencyShaderPackage, - () => modelRenderer1.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTattooShaderPackage, - () => modelRenderer1.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer1.CharacterOcclusionShaderPackage, - () => modelRenderer1.DefaultCharacterOcclusionShaderPackage); + () => (ShaderPackageResourceHandle**)&utility.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultCharacterLegacyShpkResource); + _irisState = new ModdedShaderPackageState(() => modelRenderer.IrisShaderPackage, () => modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => modelRenderer.CharacterGlassShaderPackage, + () => modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer.CharacterTransparencyShaderPackage, + () => modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => modelRenderer.CharacterTattooShaderPackage, + () => modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer.CharacterOcclusionShaderPackage, + () => modelRenderer.DefaultCharacterOcclusionShaderPackage); _hairMaskState = - new ModdedShaderPackageState(() => modelRenderer1.HairMaskShaderPackage, () => modelRenderer1.DefaultHairMaskShaderPackage); + new ModdedShaderPackageState(() => modelRenderer.HairMaskShaderPackage, () => modelRenderer.DefaultHairMaskShaderPackage); _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 33ce9f40..b6009627 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,6 +23,7 @@ using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; @@ -205,9 +206,10 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { - var sb = new StringBuilder(10240); - var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); - var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; + var sb = new StringBuilder(10240); + var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); + var hdrEnabler = _services.GetService(); + var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); @@ -223,9 +225,10 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); + sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n"); + sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); - sb.Append( - $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); + sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs index 09c8b06c..c8c90e09 100644 --- a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -4,18 +4,57 @@ using ImGuiNET; using OtterGui; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Services; namespace Penumbra.UI.Tabs.Debug; -public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler) : IUiService +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler, DalamudConfigService dalamudConfig, Configuration config) : IUiService { + private void DrawStatistics() + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Wait For Plugins (Now)"); + ImUtf8.Text("Wait For Plugins (First Launch)"); + + ImUtf8.Text("HDR Enabled (Now)"); + ImUtf8.Text("HDR Enabled (First Launch)"); + + ImUtf8.Text("HDR Hook Overriden (Now)"); + ImUtf8.Text("HDR Hook Overriden (First Launch)"); + + ImUtf8.Text("HDR Detour Called"); + ImUtf8.Text("Penumbra Reload Count"); + } + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{(dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool w) ? w.ToString() : "Unknown")}"); + ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"}"); + + ImUtf8.Text($"{config.HdrRenderTargets}"); + ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchHdrState}"); + + ImUtf8.Text($"{HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize}"); + ImUtf8.Text($"{!renderTargetHdrEnabler.FirstLaunchHdrHookOverrideState}"); + + ImUtf8.Text($"{renderTargetHdrEnabler.HdrEnabledSuccess}"); + ImUtf8.Text($"{renderTargetHdrEnabler.PenumbraReloadCount}"); + } + } + /// Draw information about render targets. public unsafe void Draw() { if (!ImUtf8.CollapsingHeader("Render Targets"u8)) return; + DrawStatistics(); + ImUtf8.Dummy(0); + ImGui.Separator(); + ImUtf8.Dummy(0); var report = renderTargetHdrEnabler.TextureReport; if (report == null) { From 2753c786fc938cb63a729945974d1e334e3a3934 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 15:55:14 +0100 Subject: [PATCH 2115/2451] Only put out warnings if the path is rooted. --- Penumbra.String | 2 +- .../Hooks/PostProcessing/ShaderReplacementFixer.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra.String b/Penumbra.String index 0647fbc5..b9003b97 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0647fbc5017ef9ced3f3ce1c2496eefd57c5b7a8 +Subproject commit b9003b97da2d1191fa203a4d66956bc54c21db2a diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index cae37776..8e12662e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Files.MaterialStructs; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.Services; +using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; using ModelRenderer = Penumbra.Interop.Services.ModelRenderer; @@ -109,8 +110,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - _communicator = communicator; - _humanSetupScalingHook = humanSetupScalingHook; + _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&utility.Address->SkinShpkResource, @@ -467,7 +468,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { - if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) + if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags) && Utf8GamePath.IsRooted(thisPtr->FileName.AsSpan())) Penumbra.Log.Warning( $"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); @@ -507,9 +508,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly ConcurrentSet _materials = new(); // ConcurrentDictionary.Count uses a lock in its current implementation. - private uint _materialCount = 0; - - private ulong _slowPathCallDelta = 0; + private uint _materialCount; + private ulong _slowPathCallDelta; public uint MaterialCount { From 7f52777fd48011606d6317934bcdd02f5183573e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 14:58:57 +0000 Subject: [PATCH 2116/2451] [CI] Updating repo.json for testing_1.3.3.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 41956dc3..ec1fc623 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.1", + "TestingAssemblyVersion": "1.3.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6ea38eac0a2b050940a030423012344cc26cbb21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 17:59:44 +0100 Subject: [PATCH 2117/2451] Share PeSigScanner and use in RenderTargetHdrEnabler because of ReShade. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 33 +++++++++++-------- .../Hooks/ResourceLoading/PapHandler.cs | 4 +-- .../Hooks/ResourceLoading/PapRewriter.cs | 9 ++--- .../Hooks/ResourceLoading/PeSigScanner.cs | 3 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 4 +-- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index 41c4dab1..653d9c1a 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -7,6 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui.Services; using Penumbra.GameData; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Services; namespace Penumbra.Interop.Hooks.PostProcessing; @@ -31,19 +32,23 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable public TextureReportRecord[]? TextureReport { get; private set; } - [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] - private readonly Hook _renderTargetManagerInitialize = null!; - - [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] - private readonly Hook _createTexture2D = null!; + private readonly Hook? _renderTargetManagerInitialize; + private readonly Hook? _createTexture2D; public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config, IDalamudPluginInterface pi, - DalamudConfigService dalamudConfig) + DalamudConfigService dalamudConfig, PeSigScanner peScanner) { _config = config; - interop.InitializeFromAttributes(this); - if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) - _renderTargetManagerInitialize.Enable(); + if (peScanner.TryScanText(Sigs.RenderTargetManagerInitialize, out var initializeAddress) + && peScanner.TryScanText(Sigs.DeviceCreateTexture2D, out var createAddress)) + { + _renderTargetManagerInitialize = + interop.HookFromAddress(initializeAddress, RenderTargetManagerInitializeDetour); + _createTexture2D = interop.HookFromAddress(createAddress, CreateTexture2DDetour); + + if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) + _renderTargetManagerInitialize.Enable(); + } _share = pi.GetOrCreateData("Penumbra.RenderTargetHDR.V1", () => { @@ -87,19 +92,19 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private void Dispose(bool _) { - _createTexture2D.Dispose(); - _renderTargetManagerInitialize.Dispose(); + _createTexture2D?.Dispose(); + _renderTargetManagerInitialize?.Dispose(); } private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { - _createTexture2D.Enable(); + _createTexture2D!.Enable(); _share.Item5[0] = true; _textureIndices.Value = new TextureIndices(0, 0); _textures.Value = _config.DebugMode ? [] : null; try { - return _renderTargetManagerInitialize.Original(@this); + return _renderTargetManagerInitialize!.Original(@this); } finally { @@ -133,7 +138,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable _textureIndices.Value = indices; } - var texture = _createTexture2D.Original(@this, size, mipLevel, textureFormat, flags, unk); + var texture = _createTexture2D!.Original(@this, size, mipLevel, textureFormat, flags, unk); if (_textures.IsValueCreated) _textures.Value?.Add((nint)texture, (indices.CreationOrder - 1, originalTextureFormat)); return texture; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 5ba8c975..35ee86dc 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -2,9 +2,9 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; -public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +public sealed class PapHandler(PeSigScanner sigScanner, PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { - private readonly PapRewriter _papRewriter = new(papResourceHandler); + private readonly PapRewriter _papRewriter = new(sigScanner, papResourceHandler); public void Enable() { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 2fb1623d..5fdec816 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -7,21 +7,20 @@ using Swan; namespace Penumbra.Interop.Hooks.ResourceLoading; -public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); private readonly Dictionary _hooks = []; private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { - if (!_scanner.TryScanText(sig, out var address)) + if (!sigScanner.TryScanText(sig, out var address)) throw new Exception($"Signature for {name} [{sig}] could not be found."); - var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); + var funcInstructions = sigScanner.GetFunctionInstructions(address).ToArray(); var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); foreach (var hookPoint in hookPoints) @@ -165,8 +164,6 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou public void Dispose() { - _scanner.Dispose(); - foreach (var hook in _hooks.Values) { hook.Disable(); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index 4be0da00..620f3160 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -1,12 +1,13 @@ using System.IO.MemoryMappedFiles; using Iced.Intel; +using OtterGui.Services; using PeNet; using Decoder = Iced.Intel.Decoder; namespace Penumbra.Interop.Hooks.ResourceLoading; // A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause Winter could not be faffed, Winter will definitely not rewrite it later -public unsafe class PeSigScanner : IDisposable +public unsafe class PeSigScanner : IDisposable, IService { private readonly MemoryMappedFile _file; private readonly MemoryMappedViewAccessor _textSection; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 47f96d98..ad9c41e6 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -22,7 +22,7 @@ public unsafe class ResourceLoader : IDisposable, IService private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner) { _resources = resources; _fileReadService = fileReadService; @@ -35,7 +35,7 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleDecRef += DecRefProtection; _fileReadService.ReadSqPack += ReadSqPackDetour; - _papHandler = new PapHandler(PapResourceHandler); + _papHandler = new PapHandler(sigScanner, PapResourceHandler); _papHandler.Enable(); } From 3687c99ee6d385af21e22371f6492f3bcb11c20c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 17:02:43 +0000 Subject: [PATCH 2118/2451] [CI] Updating repo.json for testing_1.3.3.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ec1fc623..14c5d8ac 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.2", + "TestingAssemblyVersion": "1.3.3.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 415e15f3b150e0ee662447cbffe7340b45f50845 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 21:12:14 +0100 Subject: [PATCH 2119/2451] Fix another issue with temporary mod settings. --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 10 ++++++++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 6 +++--- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index 425e0348..fa71e1b6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -20,17 +20,23 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } - public TemporaryModSettings(ModSettings? clone, string source, int key = 0) + public TemporaryModSettings(Mod mod, ModSettings? clone, string source, int key = 0) { Source = source; Lock = key; ForceInherit = clone == null; - if (clone != null) + if (clone != null && clone != Empty) { Enabled = clone.Enabled; Priority = clone.Priority; Settings = clone.Settings.Clone(); } + else + { + Enabled = false; + Priority = ModPriority.Default; + Settings = SettingList.Default(mod); + } } } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 280956f4..1a7d4e31 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -277,19 +277,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Sun, 12 Jan 2025 00:03:36 +0100 Subject: [PATCH 2120/2451] Add counts to multi mod selection. --- Penumbra/UI/ModsTab/MultiModPanel.cs | 90 +++++++++++++++++----------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 4079748e..0e9b5d39 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -4,65 +4,88 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) : IUiService +public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) : IUiService { public void Draw() { - if (_selector.SelectedPaths.Count == 0) + if (selector.SelectedPaths.Count == 0) return; ImGui.NewLine(); - DrawModList(); + var treeNodePos = ImGui.GetCursorPos(); + var numLeaves = DrawModList(); + DrawCounts(treeNodePos, numLeaves); DrawMultiTagger(); } - private void DrawModList() + private void DrawCounts(Vector2 treeNodePos, int numLeaves) { - using var tree = ImRaii.TreeNode("Currently Selected Objects", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); - ImGui.Separator(); - if (!tree) - return; + var startPos = ImGui.GetCursorPos(); + var numFolders = selector.SelectedPaths.Count - numLeaves; + var text = (numLeaves, numFolders) switch + { + (0, 0) => string.Empty, // should not happen + (> 0, 0) => $"{numLeaves} Mods", + (0, > 0) => $"{numFolders} Folders", + _ => $"{numLeaves} Mods, {numFolders} Folders", + }; + ImGui.SetCursorPos(treeNodePos); + ImUtf8.TextRightAligned(text); + ImGui.SetCursorPos(startPos); + } - var sizeType = ImGui.GetFrameHeight(); - var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; + private int DrawModList() + { + using var tree = ImUtf8.TreeNode("Currently Selected Objects###Selected"u8, + ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + ImGui.Separator(); + + + if (!tree) + return selector.SelectedPaths.Count(l => l is ModFileSystem.Leaf); + + var sizeType = new Vector2(ImGui.GetFrameHeight()); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType.X - 4 * ImGui.GetStyle().CellPadding.X) / 100; var sizeMods = availableSizePercent * 35; var sizeFolders = availableSizePercent * 65; - using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg)) + var leaves = 0; + using (var table = ImUtf8.Table("mods"u8, 3, ImGuiTableFlags.RowBg)) { if (!table) - return; + return selector.SelectedPaths.Count(l => l is ModFileSystem.Leaf); - ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); - ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); + ImUtf8.TableSetupColumn("type"u8, ImGuiTableColumnFlags.WidthFixed, sizeType.X); + ImUtf8.TableSetupColumn("mod"u8, ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImUtf8.TableSetupColumn("path"u8, ImGuiTableColumnFlags.WidthFixed, sizeFolders); var i = 0; - foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) + foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) { using var id = ImRaii.PushId(i++); + var (icon, text) = path is ModFileSystem.Leaf l + ? (FontAwesomeIcon.FileCircleMinus, l.Value.Name.Text) + : (FontAwesomeIcon.FolderMinus, string.Empty); ImGui.TableNextColumn(); - var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); - if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiSelection(path); + if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) + selector.RemovePathFromMultiSelection(path); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(fullName); + ImUtf8.DrawFrameColumn(text); + ImUtf8.DrawFrameColumn(fullName); + if (path is ModFileSystem.Leaf) + ++leaves; } } ImGui.Separator(); + return leaves; } private string _tag = string.Empty; @@ -72,11 +95,10 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito private void DrawMultiTagger() { var width = ImGuiHelpers.ScaledVector2(150, 0); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Multi Tagger:"); + ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImGui.SameLine(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); - ImGui.InputTextWithHint("##tag", "Local Tag Name...", ref _tag, 128); + ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); UpdateTagCache(); var label = _addMods.Count > 0 @@ -88,9 +110,9 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addMods.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) foreach (var mod in _addMods) - _editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); + editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); label = _removeMods.Count > 0 ? $"Remove from {_removeMods.Count} Mods" @@ -101,9 +123,9 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito : $"No selected mod contains the tag \"{_tag}\" locally." : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeMods.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) foreach (var (mod, index) in _removeMods) - _editor.ChangeLocalTag(mod, index, string.Empty); + editor.ChangeLocalTag(mod, index, string.Empty); ImGui.Separator(); } @@ -114,7 +136,7 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito if (_tag.Length == 0) return; - foreach (var leaf in _selector.SelectedPaths.OfType()) + foreach (var leaf in selector.SelectedPaths.OfType()) { var index = leaf.Value.LocalTags.IndexOf(_tag); if (index >= 0) From 30a4b90e843359230bbe7a127d49da16381aeb76 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:34:18 +0100 Subject: [PATCH 2121/2451] Add IPC for querying temporary settings. --- Penumbra.Api | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 56 ++++++++++++-- Penumbra/Api/IpcProviders.cs | 2 + Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 77 +++++++++++++++++++- 4 files changed, 128 insertions(+), 9 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index de0f281f..b4e716f8 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit de0f281fbf9d8d9d3aa8463a28025d54877cde8d +Subproject commit b4e716f86d94cd4d98d8f58e580ed5f619ea87ae diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index b12ce707..d951639c 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -130,11 +130,54 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } + public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, string modDirectory, + string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return (ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args), null, string.Empty); - public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + return QueryTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) + QueryTemporaryModSettingsPlayer(int objectIndex, + string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return (ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args), null, string.Empty); + + return QueryTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) QueryTemporaryModSettings( + in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return (ApiHelpers.Return(PenumbraApiEc.ModMissing, args), null, string.Empty); + + if (collection.Identity.Index <= 0) + return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty); + + var settings = collection.GetTempSettings(mod.Index); + if (settings == null) + return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty); + + if (settings.Lock > 0 && settings.Lock != key) + return (ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args), null, settings.Source); + + return (ApiHelpers.Return(PenumbraApiEc.Success, args), + (settings.ForceInherit, settings.Enabled, settings.Priority.Value, settings.ConvertToShareable(mod).Settings), settings.Source); + } + + + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, + int priority, IReadOnlyDictionary> options, string source, int key) { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, + "Enabled", enabled, "Priority", priority, "Options", options, "Source", source, "Key", key); if (!collectionManager.Storage.ById(collectionId, out var collection)) return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); @@ -142,10 +185,12 @@ public class TemporaryApi( return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); } - public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, + int priority, IReadOnlyDictionary> options, string source, int key) { - var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", + enabled, "Priority", priority, "Options", options, "Source", source, "Key", key); if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); @@ -254,7 +299,8 @@ public class TemporaryApi( var numRemoved = 0; for (var i = 0; i < collection.Settings.Count; ++i) { - if (collection.GetTempSettings(i) is {} tempSettings && tempSettings.Lock == key + if (collection.GetTempSettings(i) is { } tempSettings + && tempSettings.Lock == key && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) ++numRemoved; } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 6f3b2c38..f6948832 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -103,6 +103,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary), IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.QueryTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.QueryTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 832fea82..3dc8862e 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -51,7 +51,7 @@ public class TemporaryIpcTester( ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); + ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); @@ -126,12 +126,14 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); if (ImUtf8.Button("Set##SetTemporary"u8)) - _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, + new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) - _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, + new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); @@ -161,6 +163,75 @@ public class TemporaryIpcTester( ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8)) _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338); + + IpcTester.DrawIntro(QueryTemporaryModSettings.Label, "Query Temporary Mod Settings from specific Collection"); + ImUtf8.Button("Query##QueryTemporaryModSettings"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1337); + DrawTooltip(settings, source); + } + + ImGui.SameLine(); + ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporary"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1338); + DrawTooltip(settings, source); + } + + IpcTester.DrawIntro(QueryTemporaryModSettingsPlayer.Label, "Query Temporary Mod Settings from game object Collection"); + ImUtf8.Button("Query##QueryTemporaryModSettingsPlayer"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = + new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1337); + DrawTooltip(settings, source); + } + + ImGui.SameLine(); + ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporaryPlayer"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = + new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1338); + DrawTooltip(settings, source); + } + + void DrawTooltip((bool ForceInherit, bool Enabled, int Priority, Dictionary> Settings)? settings, string source) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text($"Query returned {_lastTempError}"); + if (settings != null) + ImUtf8.Text($"Settings created by {(source.Length == 0 ? "Unknown Source" : source)}:"); + else + ImUtf8.Text(source.Length > 0 ? $"Locked by {source}." : "No settings exist."); + ImGui.Separator(); + if (settings == null) + { + + return; + } + + using (ImUtf8.Group()) + { + ImUtf8.Text("Force Inherit"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); + foreach (var group in settings.Value.Settings.Keys) + ImUtf8.Text(group); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{settings.Value.ForceInherit}"); + ImUtf8.Text($"{settings.Value.Enabled}"); + ImUtf8.Text($"{settings.Value.Priority}"); + foreach (var group in settings.Value.Settings.Values) + ImUtf8.Text(string.Join("; ", group)); + } + } } public void DrawCollections() From cc981eba156e6af19c857d1d70f43b8f1afdff72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:34:34 +0100 Subject: [PATCH 2122/2451] Fix used dye channel in material editor previews. --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index ab93dc5f..f32a3dc9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -603,6 +603,7 @@ public partial class MtrlTab value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); + _stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { From 9c25fab1839473da1031382f1d54d7fe7105e5ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:41:32 +0100 Subject: [PATCH 2123/2451] Increase API minor version. --- Penumbra/Api/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 894b2674..05c47644 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 4); + => (5, 5); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; From 9559bd7358d1de390811fd331aef4bac97febcc9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 15:20:37 +0100 Subject: [PATCH 2124/2451] Improve RSP Identifier ToString. --- Penumbra/Meta/Manipulations/Rsp.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 2d73ec7f..9dc4fe90 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -40,6 +40,9 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib public MetaManipulationType Type => MetaManipulationType.Rsp; + + public override string ToString() + => $"RSP - {SubRace.ToName()} - {Attribute.ToFullString()}"; } [JsonConverter(typeof(Converter))] From e77fa18c61f5c98a6a3e9c0ce1ff0900ee6df5a3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jan 2025 15:42:23 +0100 Subject: [PATCH 2125/2451] Start for combining groups. --- Penumbra/Mods/Groups/CombiningModGroup.cs | 235 ++++++++++++++++++ Penumbra/Mods/Groups/IModGroup.cs | 3 +- .../OptionEditor/CombiningModGroupEditor.cs | 72 ++++++ .../Manager/OptionEditor/ModGroupEditor.cs | 55 ++-- .../Mods/SubMods/CombinedDataContainer.cs | 72 ++++++ Penumbra/Mods/SubMods/CombiningSubMod.cs | 25 ++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 8 +- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 13 + .../Groups/CombiningModGroupEditDrawer.cs | 11 + 9 files changed, 468 insertions(+), 26 deletions(-) create mode 100644 Penumbra/Mods/Groups/CombiningModGroup.cs create mode 100644 Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs create mode 100644 Penumbra/Mods/SubMods/CombinedDataContainer.cs create mode 100644 Penumbra/Mods/SubMods/CombiningSubMod.cs create mode 100644 Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs new file mode 100644 index 00000000..255f84aa --- /dev/null +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -0,0 +1,235 @@ +using Dalamud.Interface.ImGuiNotification; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Filesystem; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; +using Penumbra.Util; + +namespace Penumbra.Mods.Groups; + +/// Groups that allow all available options to be selected at once. +public sealed class CombiningModGroup : IModGroup +{ + + public GroupType Type + => GroupType.Combining; + + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + + public Mod Mod { get; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; + public ModPriority Priority { get; set; } + public int Page { get; set; } + public Setting DefaultSettings { get; set; } + public readonly List OptionData = []; + public List Data { get; private set; } + + /// Groups that allow all available options to be selected at once. + public CombiningModGroup(Mod mod) + { + Mod = mod; + Data = [new CombinedDataContainer(this)]; + } + + IReadOnlyList IModGroup.Options + => OptionData; + + public IReadOnlyList DataContainers + => Data; + + public bool IsOption + => OptionData.Count > 0; + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + { + foreach (var path in Data.SelectWhere(o + => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } + + public void RemoveOption(int index) + { + if(index < 0 || index >= OptionData.Count) + return; + + OptionData.RemoveAt(index); + var list = new List(Data.Count / 2); + var optionFlag = 1 << index; + list.AddRange(Data.Where((c, i) => (i & optionFlag) == 0)); + Data = list; + } + + public void MoveOption(int from, int to) + { + if (!OptionData.Move(ref from, ref to)) + return; + + var list = new List(Data.Count); + for (var i = 0ul; i < (ulong)Data.Count; ++i) + { + var actualIndex = (int) Functions.MoveBit(i, from, to); + list.Add(Data[actualIndex]); + } + + Data = list; + } + + public IModOption? AddOption(string name, string description = "") + { + var groupIdx = Mod.Groups.IndexOf(this); + if (groupIdx < 0) + return null; + + var subMod = new CombiningSubMod(this) + { + Name = name, + Description = description, + }; + // Double available containers. + FillContainers(2 * Data.Count); + OptionData.Add(subMod); + return subMod; + } + + public static CombiningModGroup? Load(Mod mod, JObject json) + { + var ret = new CombiningModGroup(mod, true); + if (!ModSaveGroup.ReadJsonBase(json, ret)) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) + { + if (ret.OptionData.Count == IModGroup.MaxCombiningOptions) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxCombiningOptions} options, ignoring excessive options.", + NotificationType.Warning); + break; + } + + var subMod = new CombiningSubMod(ret, child); + ret.OptionData.Add(subMod); + } + + var requiredContainers = 1 << ret.OptionData.Count; + var containers = json["Containers"]; + if (containers != null) + foreach (var child in containers.Children()) + { + if (requiredContainers <= ret.Data.Count) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has more data containers than it can support with {ret.OptionData.Count} options, ignoring excessive containers.", + NotificationType.Warning); + break; + } + + var container = new CombinedDataContainer(ret, child); + ret.Data.Add(container); + } + + if (requiredContainers > ret.Data.Count) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has not enough data containers for its {ret.OptionData.Count} options, filling with empty containers.", + NotificationType.Warning); + ret.FillContainers(requiredContainers); + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + + return ret; + } + + public int GetIndex() + => ModGroup.GetIndex(this); + + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new CombiningModGroupEditDrawer(editDrawer, this); + + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) + => Data[setting.AsIndex].AddDataTo(redirections, manipulations); + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + + jWriter.WritePropertyName("Containers"); + jWriter.WriteStartArray(); + foreach (var container in Data) + { + jWriter.WriteStartObject(); + if (container.Name.Length > 0) + { + jWriter.WritePropertyName("Name"); + jWriter.WriteValue(container.Name); + } + + SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => ModGroup.GetCountsBase(this); + + public Setting FixSetting(Setting setting) + => new(Math.Min(setting.Value, (ulong)(Data.Count - 1))); + + /// Create a group without a mod only for saving it in the creator. + internal static CombiningModGroup WithoutMod(string name) + => new(null!) + { + Name = name, + }; + + /// For loading when no empty container should be created. + private CombiningModGroup(Mod mod, bool _) + { + Mod = mod; + Data = []; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void FillContainers(int requiredCount) + { + if (requiredCount <= Data.Count) + return; + + Data.EnsureCapacity(requiredCount); + Data.AddRange(Enumerable.Repeat(0, requiredCount - Data.Count).Select(_ => new CombinedDataContainer(this))); + } +} diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index a6f6e20d..96422caf 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -22,7 +22,8 @@ public enum GroupDrawBehaviour public interface IModGroup { - public const int MaxMultiOptions = 32; + public const int MaxMultiOptions = 32; + public const int MaxCombiningOptions = 8; public Mod Mod { get; } public string Name { get; set; } diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs new file mode 100644 index 00000000..46c8e3db --- /dev/null +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -0,0 +1,72 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager.OptionEditor; + +public sealed class CombiningModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + protected override CombiningModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override CombiningSubMod? CloneOption(CombiningModGroup group, IModOption option) + { + if (group.OptionData.Count >= IModGroup.MaxCombiningOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " + + $"since only up to {IModGroup.MaxCombiningOptions} options are supported in one group."); + return null; + } + + var newOption = new CombiningSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + + if (option is IModDataContainer data) + { + SubMod.Clone(data, newOption); + if (option is MultiSubMod m) + newOption.Priority = m.Priority; + else + newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); + } + + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(CombiningModGroup group, int optionIndex) + { + var optionFlag = 1 << optionIndex; + for (var i = group.Data.Count - 1; i >= 0; --i) + { + group.Data.RemoveAll() + if ((i & optionFlag) == optionFlag) + group.Data.RemoveAt(i); + } + + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + } + + protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index d01297db..b66b4d8c 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -37,6 +37,7 @@ public class ModGroupEditor( SingleModGroupEditor singleEditor, MultiModGroupEditor multiEditor, ImcModGroupEditor imcEditor, + CombiningModGroupEditor combiningEditor, CommunicatorService communicator, SaveService saveService, Configuration config) : IService @@ -50,6 +51,9 @@ public class ModGroupEditor( public ImcModGroupEditor ImcEditor => imcEditor; + public CombiningModGroupEditor CombiningEditor + => combiningEditor; + /// Change the settings stored as default options in a mod. public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption) { @@ -223,52 +227,60 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.DeleteOption(i); return; + case CombiningModGroup c: + CombiningEditor.DeleteOption(c); + return; } } public IModOption? AddOption(IModGroup group, IModOption option) => group switch { - SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + SingleModGroup s => SingleEditor.AddOption(s, option), + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + CombiningModGroup c => CombiningEditor.AddOption(c, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { - SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + SingleModGroup s => SingleEditor.AddOption(s, newName), + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + CombiningModGroup c => CombiningEditor.AddOption(c, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { - GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), - _ => null, + GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), + GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, default, default, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { - GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Combining => CombiningEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { - SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + CombiningModGroup c => CombiningEditor.FindOrAddOption(c, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) @@ -284,6 +296,9 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.MoveOption(i, toIdx); return; + case CombiningSubMod c: + CombiningEditor.MoveOption(c, toIdx); + return; } } } diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs new file mode 100644 index 00000000..3e8ec95b --- /dev/null +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; +using Swan.Formatters; + +namespace Penumbra.Mods.SubMods; + +public class CombinedDataContainer(IModGroup group) : IModDataContainer +{ + public IMod Mod + => Group.Mod; + + public IModGroup Group { get; } = group; + + public string Name { get; } = string.Empty; + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); + + public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) + => SubMod.AddContainerTo(this, redirections, manipulations); + + public string GetName() + { + if (Name.Length > 0) + return Name; + + var index = GetDataIndex(); + if (index == 0) + return "None"; + + var sb = new StringBuilder(128); + for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i) + { + if ((index & 1) == 0) + continue; + + sb.Append(Group.Options[i].Name); + sb.Append(' ').Append('+').Append(' '); + index >>= 1; + if (index == 0) + break; + } + + return sb.ToString(0, sb.Length - 3); + } + + public string GetFullName() + => $"{Group.Name}: {GetName()}"; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } + + public CombinedDataContainer(CombiningModGroup group, JToken token) + : this(group) + { + SubMod.LoadDataContainer(token, this, group.Mod.ModPath); + Name = token["Name"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/CombiningSubMod.cs b/Penumbra/Mods/SubMods/CombiningSubMod.cs new file mode 100644 index 00000000..6eb5de9d --- /dev/null +++ b/Penumbra/Mods/SubMods/CombiningSubMod.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json.Linq; +using Penumbra.Mods.Groups; + +namespace Penumbra.Mods.SubMods; + +public class CombiningSubMod(IModGroup group) : IModOption +{ + public IModGroup Group { get; } = group; + + public Mod Mod + => Group.Mod; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; + + public string FullName + => $"{Group.Name}: {Name}"; + + public int GetIndex() + => SubMod.GetIndex(this); + + public CombiningSubMod(CombiningModGroup group, JToken json) + : this(group) + => SubMod.LoadOptionData(json, this); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 02e945f3..7f1a8ac5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -452,19 +452,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private bool DrawOptionSelectHeader() { - const string defaultOption = "Default Option"; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var ret = false; - if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor.Option is DefaultSubMod)) + if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0).Wait(); ret = true; } ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + if (ImUtf8.ButtonEx("Refresh Data"u8, "Refresh data for the current option.\nThis resets unsaved changes."u8, width)) { _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait(); ret = true; @@ -474,7 +472,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService ImGui.SetNextItemWidth(width.X); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName()); + using var combo = ImUtf8.Combo("##optionSelector"u8, _editor.Option!.GetFullName()); if (!combo) return ret; diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index c30239bc..a3e7ce14 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -48,6 +48,7 @@ public class AddGroupDrawer : IUiService DrawSingleGroupButton(mod, buttonWidth); ImUtf8.SameLineInner(); DrawMultiGroupButton(mod, buttonWidth); + DrawCombiningGroupButton(mod, buttonWidth); } private void DrawSingleGroupButton(Mod mod, Vector2 width) @@ -76,6 +77,18 @@ public class AddGroupDrawer : IUiService _groupNameValid = false; } + private void DrawCombiningGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid + ? "Add a new combining option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } private void DrawImcInput(float width) { var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width); diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs new file mode 100644 index 00000000..79d2fb43 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -0,0 +1,11 @@ +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + + } +} From 795fa7336e9654ff454ce79cae9387cef8858918 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 15 Jan 2025 17:44:22 +0100 Subject: [PATCH 2126/2451] Update with workable prototype. --- OtterGui | 2 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 49 +-------- .../OptionEditor/CombiningModGroupEditor.cs | 58 +++------- .../Manager/OptionEditor/ModGroupEditor.cs | 4 +- Penumbra/Mods/ModCreator.cs | 12 +-- .../Mods/SubMods/CombinedDataContainer.cs | 12 +-- Penumbra/Mods/SubMods/SubMod.cs | 41 ++++--- .../Groups/CombiningModGroupEditDrawer.cs | 102 +++++++++++++++++- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 4 + 9 files changed, 168 insertions(+), 116 deletions(-) diff --git a/OtterGui b/OtterGui index fd387218..055f1695 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fd387218d2d2d237075cb35be6ca89eeb53e14e5 +Subproject commit 055f169572223fd1b59389549c88b4c861c94608 diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 255f84aa..80f3c4c0 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; @@ -18,7 +17,6 @@ namespace Penumbra.Mods.Groups; /// Groups that allow all available options to be selected at once. public sealed class CombiningModGroup : IModGroup { - public GroupType Type => GroupType.Combining; @@ -60,33 +58,6 @@ public sealed class CombiningModGroup : IModGroup return null; } - public void RemoveOption(int index) - { - if(index < 0 || index >= OptionData.Count) - return; - - OptionData.RemoveAt(index); - var list = new List(Data.Count / 2); - var optionFlag = 1 << index; - list.AddRange(Data.Where((c, i) => (i & optionFlag) == 0)); - Data = list; - } - - public void MoveOption(int from, int to) - { - if (!OptionData.Move(ref from, ref to)) - return; - - var list = new List(Data.Count); - for (var i = 0ul; i < (ulong)Data.Count; ++i) - { - var actualIndex = (int) Functions.MoveBit(i, from, to); - list.Add(Data[actualIndex]); - } - - Data = list; - } - public IModOption? AddOption(string name, string description = "") { var groupIdx = Mod.Groups.IndexOf(this); @@ -98,10 +69,9 @@ public sealed class CombiningModGroup : IModGroup Name = name, Description = description, }; - // Double available containers. - FillContainers(2 * Data.Count); - OptionData.Add(subMod); - return subMod; + return OptionData.AddNewWithPowerSet(Data, subMod, () => new CombinedDataContainer(this), IModGroup.MaxCombiningOptions) + ? subMod + : null; } public static CombiningModGroup? Load(Mod mod, JObject json) @@ -148,7 +118,8 @@ public sealed class CombiningModGroup : IModGroup Penumbra.Messager.NotificationMessage( $"Combining Group {ret.Name} in {mod.Name} has not enough data containers for its {ret.OptionData.Count} options, filling with empty containers.", NotificationType.Warning); - ret.FillContainers(requiredContainers); + ret.Data.EnsureCapacity(requiredContainers); + ret.Data.AddRange(Enumerable.Repeat(0, requiredContainers - ret.Data.Count).Select(_ => new CombinedDataContainer(ret))); } ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); @@ -222,14 +193,4 @@ public sealed class CombiningModGroup : IModGroup Mod = mod; Data = []; } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void FillContainers(int requiredCount) - { - if (requiredCount <= Data.Count) - return; - - Data.EnsureCapacity(requiredCount); - Data.AddRange(Enumerable.Repeat(0, requiredCount - Data.Count).Select(_ => new CombinedDataContainer(this))); - } } diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs index 46c8e3db..ce5db454 100644 --- a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -1,5 +1,5 @@ +using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -19,54 +19,30 @@ public sealed class CombiningModGroupEditor(CommunicatorService communicator, Sa }; protected override CombiningSubMod? CloneOption(CombiningModGroup group, IModOption option) - { - if (group.OptionData.Count >= IModGroup.MaxCombiningOptions) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " - + $"since only up to {IModGroup.MaxCombiningOptions} options are supported in one group."); - return null; - } - - var newOption = new CombiningSubMod(group) - { - Name = option.Name, - Description = option.Description, - }; - - if (option is IModDataContainer data) - { - SubMod.Clone(data, newOption); - if (option is MultiSubMod m) - newOption.Priority = m.Priority; - else - newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); - } - - group.OptionData.Add(newOption); - return newOption; - } + => throw new NotImplementedException(); protected override void RemoveOption(CombiningModGroup group, int optionIndex) { - var optionFlag = 1 << optionIndex; - for (var i = group.Data.Count - 1; i >= 0; --i) - { - group.Data.RemoveAll() - if ((i & optionFlag) == optionFlag) - group.Data.RemoveAt(i); - } - - group.OptionData.RemoveAt(optionIndex); - group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + if (group.OptionData.RemoveWithPowerSet(group.Data, optionIndex)) + group.DefaultSettings.RemoveBit(optionIndex); } - protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + protected override bool MoveOption(CombiningModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) + if (!group.OptionData.MoveWithPowerSet(group.Data, ref optionIdxFrom, ref optionIdxTo)) return false; - group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); return true; } + + public void SetDisplayName(CombinedDataContainer container, string name, SaveType saveType = SaveType.Queue) + { + if (container.Name == name) + return; + + container.Name = name; + SaveService.Save(saveType, new ModSaveGroup(container.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, container.Group.Mod, container.Group, null, null, -1); + } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index b66b4d8c..1c077c58 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -227,7 +227,7 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.DeleteOption(i); return; - case CombiningModGroup c: + case CombiningSubMod c: CombiningEditor.DeleteOption(c); return; } @@ -259,7 +259,7 @@ public class ModGroupEditor( GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), - GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, default, default, saveType), + GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, saveType), _ => null, }; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index bdc16b72..18d2bc09 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -82,13 +82,11 @@ public partial class ModCreator( if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true); if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) - { foreach (var container in mod.AllDataContainers) { if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); } - } return true; } @@ -186,7 +184,8 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, + bool deleteDefault) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -447,9 +446,10 @@ public partial class ModCreator( var json = JObject.Parse(File.ReadAllText(file.FullName)); switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) { - case GroupType.Multi: return MultiModGroup.Load(mod, json); - case GroupType.Single: return SingleModGroup.Load(mod, json); - case GroupType.Imc: return ImcModGroup.Load(mod, json); + case GroupType.Multi: return MultiModGroup.Load(mod, json); + case GroupType.Single: return SingleModGroup.Load(mod, json); + case GroupType.Imc: return ImcModGroup.Load(mod, json); + case GroupType.Combining: return CombiningModGroup.Load(mod, json); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index 3e8ec95b..2c410c1c 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -4,7 +4,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.String.Classes; -using Swan.Formatters; namespace Penumbra.Mods.SubMods; @@ -15,7 +14,7 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer public IModGroup Group { get; } = group; - public string Name { get; } = string.Empty; + public string Name { get; set; } = string.Empty; public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public MetaDictionary Manipulations { get; set; } = new(); @@ -35,11 +34,12 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer var sb = new StringBuilder(128); for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i) { - if ((index & 1) == 0) - continue; + if ((index & 1) != 0) + { + sb.Append(Group.Options[i].Name); + sb.Append(' ').Append('+').Append(' '); + } - sb.Append(Group.Options[i].Name); - sb.Append(' ').Append('+').Append(' '); index >>= 1; if (index == 0) break; diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index f6b1be96..7f01884d 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -81,29 +81,40 @@ public static class SubMod [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { - j.WritePropertyName(nameof(data.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.Files) + if (data.Files.Count > 0) { - if (file.ToRelPath(basePath, out var relPath)) + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + } + + if (data.FileSwaps.Count > 0) + { + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) { j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); + j.WriteValue(file.ToString()); } + + j.WriteEndObject(); } - j.WriteEndObject(); - j.WritePropertyName(nameof(data.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.FileSwaps) + if (data.Manipulations.Count > 0) { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.Manipulations)); - serializer.Serialize(j, data.Manipulations); } /// Write the data for a selectable mod option on a JsonWriter. diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index 79d2fb43..f32e6da6 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,4 +1,10 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab.Groups; @@ -6,6 +12,100 @@ public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, Co { public void Draw() { - + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImUtf8.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + } + + DrawNewOption(); + DrawContainerNames(); + } + + private void DrawNewOption() + { + var count = group.OptionData.Count; + if (count >= IModGroup.MaxCombiningOptions) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, default, !validName)) + { + editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } + + private unsafe void DrawContainerNames() + { + if (ImUtf8.ButtonEx("Edit Container Names"u8, + "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8, + new Vector2(400 * ImUtf8.GlobalScale, 0))) + ImUtf8.OpenPopup("DataContainerNames"u8); + + var sizeX = group.OptionData.Count * (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) + 300 * ImUtf8.GlobalScale; + ImGui.SetNextWindowSize(new Vector2(sizeX, ImGui.GetFrameHeightWithSpacing() * Math.Min(16, group.Data.Count) + 200 * ImUtf8.GlobalScale)); + using var popup = ImUtf8.Popup("DataContainerNames"u8); + if (!popup) + return; + + foreach (var option in group.OptionData) + { + ImUtf8.RotatedText(option.Name, true); + ImUtf8.SameLineInner(); + } + + ImGui.NewLine(); + ImGui.Separator(); + using var child = ImUtf8.Child("##Child"u8, ImGui.GetContentRegionAvail()); + ImGuiClip.ClippedDraw(group.Data, DrawRow, ImGui.GetFrameHeightWithSpacing()); + } + + private void DrawRow(CombinedDataContainer container, int index) + { + using var id = ImUtf8.PushId(index); + using (ImRaii.Disabled()) + { + for (var i = 0; i < group.OptionData.Count; ++i) + { + id.Push(i); + var check = (index & (1 << i)) != 0; + ImUtf8.Checkbox(""u8, ref check); + ImUtf8.SameLineInner(); + id.Pop(); + } + } + + var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name; + if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8)) + { + editor.CombiningDisplayIndex = index; + editor.CombiningDisplayName = name; + } + + if (ImGui.IsItemDeactivatedAfterEdit()) + editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name); + + if (ImGui.IsItemDeactivated()) + { + editor.CombiningDisplayIndex = -1; + editor.CombiningDisplayName = null; + } } } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index ec5bb920..89812346 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -58,6 +58,9 @@ public sealed class ModGroupEditDrawer( private IModOption? _dragDropOption; private bool _draggingAcross; + internal string? CombiningDisplayName; + internal int CombiningDisplayIndex; + public void Draw(Mod mod) { PrepareStyle(); @@ -275,6 +278,7 @@ public sealed class ModGroupEditDrawer( [MethodImpl(MethodImplOptions.AggressiveInlining)] internal string DrawNewOptionBase(IModGroup group, int count) { + ImGui.AlignTextToFramePadding(); ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable); Target(group, count); From 1462891bd36fb0e467b5a0b9181a72d97281c211 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 15 Jan 2025 17:05:31 +0000 Subject: [PATCH 2127/2451] [CI] Updating repo.json for testing_1.3.3.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 14c5d8ac..d9d597ba 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.3", + "TestingAssemblyVersion": "1.3.3.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From bdc2da95c4ece4ce36b99fba9f1b9fea11b83251 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jan 2025 17:22:25 +0100 Subject: [PATCH 2128/2451] Make mods write empty containers again for now. --- Penumbra.GameData | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d5f92966..78ce195c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 +Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 7f01884d..fcb6cc0e 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -81,8 +81,9 @@ public static class SubMod [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { - if (data.Files.Count > 0) - { + // #TODO: remove comments when TexTools updated. + //if (data.Files.Count > 0) + //{ j.WritePropertyName(nameof(data.Files)); j.WriteStartObject(); foreach (var (gamePath, file) in data.Files) @@ -95,10 +96,10 @@ public static class SubMod } j.WriteEndObject(); - } + //} - if (data.FileSwaps.Count > 0) - { + //if (data.FileSwaps.Count > 0) + //{ j.WritePropertyName(nameof(data.FileSwaps)); j.WriteStartObject(); foreach (var (gamePath, file) in data.FileSwaps) @@ -108,13 +109,13 @@ public static class SubMod } j.WriteEndObject(); - } + //} - if (data.Manipulations.Count > 0) - { + //if (data.Manipulations.Count > 0) + //{ j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); - } + //} } /// Write the data for a selectable mod option on a JsonWriter. From df148b556a8a8b2f90b700a01a9ac0622ace3e31 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 16 Jan 2025 16:25:18 +0000 Subject: [PATCH 2129/2451] [CI] Updating repo.json for testing_1.3.3.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index d9d597ba..2e3dd8bc 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.4", + "TestingAssemblyVersion": "1.3.3.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a1931a93fb4bfad042b1235ec62270f4c74e3fbc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 01:45:37 +0100 Subject: [PATCH 2130/2451] Add drafts of JSON schemas --- schemas/container.json | 486 +++++++++++++++++++++++++++++++++++++++ schemas/default_mod.json | 25 ++ schemas/group.json | 206 +++++++++++++++++ schemas/meta-v3.json | 51 ++++ 4 files changed, 768 insertions(+) create mode 100644 schemas/container.json create mode 100644 schemas/default_mod.json create mode 100644 schemas/group.json create mode 100644 schemas/meta-v3.json diff --git a/schemas/container.json b/schemas/container.json new file mode 100644 index 00000000..5798f46c --- /dev/null +++ b/schemas/container.json @@ -0,0 +1,486 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "type": "object", + "properties": { + "Name": { + "description": "Name of the container/option/sub-mod.", + "type": ["string", "null"] + }, + "Files": { + "description": "File redirections in this container. Keys are game paths, values are relative file paths.", + "type": "object", + "patternProperties": { + "^[^/\\\\][^\\\\]*$": { + "type": "string", + "pattern": "^[^/\\\\][^/]*$" + } + }, + "additionalProperties": false + }, + "FileSwaps": { + "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", + "type": "object", + "patternProperties": { + "^[^/\\\\][^\\\\]*$": { + "type": "string", + "pattern": "^[^/\\\\][^/]*$" + } + }, + "additionalProperties": false + }, + "Manipulations": { + "type": "array", + "items": { + "$ref": "#/$defs/Manipulation" + } + } + }, + "$defs": { + "Manipulation": { + "type": "object", + "properties": { + "Type": { + "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] + }, + "Manipulation": { + "type": ["object", "null"] + } + }, + "required": ["Type", "Manipulation"], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Unknown" + }, + "Manipulation": { + "type": "null" + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "Manipulation": { + "$ref": "#/$defs/ImcManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqdp" + }, + "Manipulation": { + "$ref": "#/$defs/EqdpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqp" + }, + "Manipulation": { + "$ref": "#/$defs/EqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Est" + }, + "Manipulation": { + "$ref": "#/$defs/EstManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Gmp" + }, + "Manipulation": { + "$ref": "#/$defs/GmpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Rsp" + }, + "Manipulation": { + "$ref": "#/$defs/RspManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "GlobalEqp" + }, + "Manipulation": { + "$ref": "#/$defs/GlobalEqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Atch" + }, + "Manipulation": { + "$ref": "#/$defs/AtchManipulation" + } + } + } + ], + "additionalProperties": false + }, + "ImcManipulation": { + "type": "object", + "properties": { + "Entry": { + "$ref": "#/$defs/ImcEntry" + }, + "Valid": { + "type": "boolean" + } + }, + "required": [ + "Entry" + ], + "allOf": [ + { + "$ref": "#/$defs/ImcIdentifier" + } + ], + "unevaluatedProperties": false + }, + "ImcIdentifier": { + "type": "object", + "properties": { + "PrimaryId": { + "type": "integer" + }, + "SecondaryId": { + "type": "integer" + }, + "Variant": { + "type": "integer" + }, + "ObjectType": { + "$ref": "#/$defs/ObjectType" + }, + "EquipSlot": { + "$ref": "#/$defs/EquipSlot" + }, + "BodySlot": { + "$ref": "#/$defs/BodySlot" + } + }, + "required": [ + "PrimaryId", + "SecondaryId", + "Variant", + "ObjectType", + "EquipSlot", + "BodySlot" + ] + }, + "ImcEntry": { + "type": "object", + "properties": { + "AttributeAndSound": { + "type": "integer" + }, + "MaterialId": { + "type": "integer" + }, + "DecalId": { + "type": "integer" + }, + "VfxId": { + "type": "integer" + }, + "MaterialAnimationId": { + "type": "integer" + }, + "AttributeMask": { + "type": "integer" + }, + "SoundId": { + "type": "integer" + } + }, + "required": [ + "MaterialId", + "DecalId", + "VfxId", + "MaterialAnimationId", + "AttributeMask", + "SoundId" + ], + "additionalProperties": false + }, + "EqdpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + }, + "ShiftedEntry": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EqpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + } + }, + "required": [ + "Entry", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EstManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "enum": ["Hair", "Face", "Body", "Head"] + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "GmpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Animated": { + "type": "boolean" + }, + "RotationA": { + "type": "number" + }, + "RotationB": { + "type": "number" + }, + "RotationC": { + "type": "number" + }, + "UnknownA": { + "type": "number" + }, + "UnknownB": { + "type": "number" + }, + "UnknownTotal": { + "type": "number" + }, + "Value": { + "type": "number" + } + }, + "required": [ + "Enabled", + "Animated", + "RotationA", + "RotationB", + "RotationC", + "UnknownA", + "UnknownB", + "UnknownTotal", + "Value" + ], + "additionalProperties": false + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + } + }, + "required": [ + "Entry", + "SetId" + ], + "additionalProperties": false + }, + "RspManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "number" + }, + "SubRace": { + "$ref": "#/$defs/SubRace" + }, + "Attribute": { + "$ref": "#/$defs/RspAttribute" + } + }, + "additionalProperties": false + }, + "GlobalEqpManipulation": { + "type": "object", + "properties": { + "Condition": { + "type": "integer" + }, + "Type": { + "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] + } + }, + "additionalProperties": false + }, + "AtchManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Bone": { + "type": "string", + "maxLength": 34 + }, + "Scale": { + "type": "number" + }, + "OffsetX": { + "type": "number" + }, + "OffsetY": { + "type": "number" + }, + "OffsetZ": { + "type": "number" + }, + "RotationX": { + "type": "number" + }, + "RotationY": { + "type": "number" + }, + "RotationZ": { + "type": "number" + } + }, + "required": [ + "Bone", + "Scale", + "OffsetX", + "OffsetY", + "OffsetZ", + "RotationX", + "RotationY", + "RotationZ" + ], + "additionalProperties": false + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "Type": { + "type": "string", + "minLength": 3, + "maxLength": 3 + }, + "Index": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "Type", + "Index" + ], + "additionalProperties": false + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + }, + "EquipSlot": { + "enum": ["Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All"] + }, + "Gender": { + "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] + }, + "ModelRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] + }, + "ObjectType": { + "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] + }, + "BodySlot": { + "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] + }, + "SubRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] + }, + "RspAttribute": { + "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] + } + } +} diff --git a/schemas/default_mod.json b/schemas/default_mod.json new file mode 100644 index 00000000..eecd74d0 --- /dev/null +++ b/schemas/default_mod.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "allOf": [ + { + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Description": { + "description": "Description of the sub-mod.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] +} diff --git a/schemas/group.json b/schemas/group.json new file mode 100644 index 00000000..0078e9f3 --- /dev/null +++ b/schemas/group.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Name": { + "description": "Name of the group.", + "type": "string" + }, + "Description": { + "description": "Description of the group.", + "type": ["string", "null"] + }, + "Image": { + "description": "Relative path to a preview image for the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["string", "null"] + }, + "Page": { + "description": "TexTools page of the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["integer", "null"] + }, + "Priority": { + "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", + "type": ["integer", "null"] + }, + "Type": { + "description": "Group type. Single groups have one and only one of their options active at any point. Multi groups can have zero, one or many of their options active. Combining groups have n options, 2^n containers, and will have one and only one container active depending on the selected options.", + "enum": ["Single", "Multi", "Imc", "Combining"] + }, + "DefaultSettings": { + "description": "Default configuration for the group.", + "type": "integer" + } + }, + "required": [ + "Name", + "Type" + ], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Single" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Multi" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Priority": { + "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", + "type": ["integer", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "AllVariants": { + "type": "boolean" + }, + "OnlyAttributes": { + "type": "boolean" + }, + "Identifier": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" + }, + "DefaultEntry": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "oneOf": [ + { + "properties": { + "AttributeMask": { + "type": "integer" + } + }, + "required": [ + "AttributeMask" + ] + }, { + "properties": { + "IsDisableSubMod": { + "const": true + } + }, + "required": [ + "IsDisableSubMod" + ] + } + ], + "unevaluatedProperties": false + } + } + } + }, { + "properties": { + "Type": { + "const": "Combining" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "additionalProperties": false + } + }, + "Containers": { + "type": "array", + "items": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/meta-v3.json b/schemas/meta-v3.json new file mode 100644 index 00000000..1a132264 --- /dev/null +++ b/schemas/meta-v3.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", + "title": "Penumbra Mod Metadata", + "description": "Metadata of a Penumbra mod.", + "type": "object", + "properties": { + "FileVersion": { + "description": "Major version of the metadata schema used.", + "type": "integer", + "minimum": 3, + "maximum": 3 + }, + "Name": { + "description": "Name of the mod.", + "type": "string" + }, + "Author": { + "description": "Author of the mod.", + "type": ["string", "null"] + }, + "Description": { + "description": "Description of the mod. Can span multiple paragraphs.", + "type": ["string", "null"] + }, + "Image": { + "description": "Relative path to a preview image for the mod. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["string", "null"] + }, + "Version": { + "description": "Version of the mod. Can be an arbitrary string.", + "type": ["string", "null"] + }, + "Website": { + "description": "URL of the web page of the mod.", + "type": ["string", "null"] + }, + "ModTags": { + "description": "Author-defined tags for the mod.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "FileVersion", + "Name" + ] +} From 3b8aac8eca94e8253b357a8b885a56bb7919e7d2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 18:50:00 +0100 Subject: [PATCH 2131/2451] Add schema for Material Development Kit files --- schemas/shpk_devkit.json | 500 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 schemas/shpk_devkit.json diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json new file mode 100644 index 00000000..cd18ab81 --- /dev/null +++ b/schemas/shpk_devkit.json @@ -0,0 +1,500 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", + "type": "object", + "properties": { + "ShaderKeys": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/ShaderKey" + } + }, + "additionalProperties": false + }, + "Comment": { + "$ref": "#/$defs/MayVary" + }, + "Samplers": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "additionalProperties": false + }, + "Constants": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "ShaderKeyValue": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ShaderKey": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Values": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/ShaderKeyValue" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "Varying": { + "type": "object", + "properties": { + "Vary": { + "type": "array", + "items": { + "$ref": "#/$defs/LaxInteger" + } + }, + "Selectors": { + "description": "Keys are Σ 31^i shaderKey(Vary[i]).", + "type": "object", + "patternProperties": { + "^\\d+$": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Items": { + "type": "array", + "$comment": "Varying is defined by constraining this array's items to T" + } + }, + "required": [ + "Vary", + "Selectors", + "Items" + ], + "additionalProperties": false + }, + "MayVary": { + "oneOf": [ + { + "type": ["string", "null"] + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "type": ["string", "null"] + } + } + } + } + ] + } + ] + }, + "Sampler": { + "type": ["object", "null"], + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "DefaultTexture": { + "type": "string", + "pattern": "^[^/\\\\][^\\\\]*$" + } + }, + "additionalProperties": false + }, + "MayVary": { + "oneOf": [ + { + "$ref": "#/$defs/Sampler" + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/$defs/Sampler" + } + } + } + } + ] + } + ] + }, + "ConstantBase": { + "type": "object", + "properties": { + "Offset": { + "description": "Defaults to 0. Mutually exclusive with ByteOffset.", + "type": "integer", + "minimum": 0 + }, + "Length": { + "description": "Defaults to up to the end. Mutually exclusive with ByteLength.", + "type": "integer", + "minimum": 0 + }, + "ByteOffset": { + "description": "Defaults to 0. Mutually exclusive with Offset.", + "type": "integer", + "minimum": 0 + }, + "ByteLength": { + "description": "Defaults to up to the end. Mutually exclusive with Length.", + "type": "integer", + "minimum": 0 + }, + "Group": { + "description": "Defaults to \"Further Constants\".", + "type": "string" + }, + "Label": { + "type": "string" + }, + "Description": { + "description": "Defaults to empty.", + "type": "string" + }, + "Type": { + "description": "Defaults to Float.", + "enum": ["Hidden", "Float", "Integer", "Color", "Enum", "Int32", "Int32Enum", "Int8", "Int8Enum", "Int16", "Int16Enum", "Int64", "Int64Enum", "Half", "Double", "TileIndex", "SphereMapIndex"] + } + }, + "not": { + "anyOf": [ + { + "required": ["Offset", "ByteOffset"] + }, { + "required": ["Length", "ByteLenngth"] + } + ] + } + }, + "HiddenConstant": { + "type": "object", + "properties": { + "Type": { + "const": "Hidden" + } + }, + "required": [ + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "FloatConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Float", "Half", "Double"] + }, + "Minimum": { + "description": "Defaults to -∞.", + "type": "number" + }, + "Maximum": { + "description": "Defaults to ∞.", + "type": "number" + }, + "Speed": { + "description": "Defaults to 0.1.", + "type": "number", + "minimum": 0 + }, + "RelativeSpeed": { + "description": "Defaults to 0.", + "type": "number", + "minimum": 0 + }, + "Exponent": { + "description": "Defaults to 1. Uses an odd pseudo-power function, f(x) = sgn(x) |x|^n.", + "type": "number" + }, + "Factor": { + "description": "Defaults to 1.", + "type": "number" + }, + "Bias": { + "description": "Defaults to 0.", + "type": "number" + }, + "Precision": { + "description": "Defaults to 3.", + "type": "integer", + "minimum": 0, + "maximum": 9 + }, + "Slider": { + "description": "Defaults to true. Drag has priority over this.", + "type": "boolean" + }, + "Drag": { + "description": "Defaults to true. Has priority over Slider.", + "type": "boolean" + }, + "Unit": { + "description": "Defaults to no unit.", + "type": "string" + } + }, + "required": [ + "Label" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "IntConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Integer", "Int32", "Int8", "Int16", "Int64"] + }, + "Minimum": { + "description": "Defaults to -2^N, N being the explicit integer width specified in the type, or 32 for Int.", + "type": "number" + }, + "Maximum": { + "description": "Defaults to 2^N - 1, N being the explicit integer width specified in the type, or 32 for Int.", + "type": "number" + }, + "Speed": { + "description": "Defaults to 0.25.", + "type": "number", + "minimum": 0 + }, + "RelativeSpeed": { + "description": "Defaults to 0.", + "type": "number", + "minimum": 0 + }, + "Factor": { + "description": "Defaults to 1.", + "type": "number" + }, + "Bias": { + "description": "Defaults to 0.", + "type": "number" + }, + "Hex": { + "description": "Defaults to false. Has priority over Slider and Drag.", + "type": "boolean" + }, + "Slider": { + "description": "Defaults to true. Hex and Drag have priority over this.", + "type": "boolean" + }, + "Drag": { + "description": "Defaults to true. Has priority over Slider, but Hex has priority over this.", + "type": "boolean" + }, + "Unit": { + "description": "Defaults to no unit.", + "type": "string" + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "ColorConstant": { + "type": "object", + "properties": { + "Type": { + "const": "Color" + }, + "SquaredRgb": { + "description": "Defaults to false. Uses an odd pseudo-square function, f(x) = sgn(x) |x|².", + "type": "boolean" + }, + "Clamped": { + "description": "Defaults to false.", + "type": "boolean" + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "EnumValue": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Value": { + "type": "number" + } + }, + "required": [ + "Label", + "Value" + ], + "additionalProperties": false + }, + "EnumConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Enum", "Int32Enum", "Int8Enum", "Int16Enum", "Int64Enum"] + }, + "Values": { + "type": "array", + "items": { + "$ref": "#/$defs/EnumValue" + } + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "SpecialConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["TileIndex", "SphereMapIndex"] + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "Constant": { + "oneOf": [ + { + "$ref": "#/$defs/HiddenConstant" + }, { + "$ref": "#/$defs/FloatConstant" + }, { + "$ref": "#/$defs/IntConstant" + }, { + "$ref": "#/$defs/ColorConstant" + }, { + "$ref": "#/$defs/EnumConstant" + }, { + "$ref": "#/$defs/SpecialConstant" + } + ] + }, + "MayVary": { + "oneOf": [ + { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + } + } + } + } + ] + } + ] + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + } + } +} From 5f8377acaaf1bc20944af85e525b09313285272c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:54:40 +0100 Subject: [PATCH 2132/2451] Update mod loading structure. --- Penumbra.GameData | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 145 +------------------------ Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Mods/ModLocalData.cs | 57 ++++++++++ Penumbra/Mods/ModMeta.cs | 83 ++++++++++++++ 5 files changed, 146 insertions(+), 145 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 78ce195c..c5250722 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 +Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 162f823d..7c48205a 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -27,6 +27,9 @@ public enum ModDataChangeType : ushort public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { + public SaveService SaveService + => saveService; + /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, string? website) @@ -40,148 +43,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic saveService.ImmediateSaveSync(new ModMeta(mod)); } - public ModDataChangeType LoadLocalData(Mod mod) - { - var dataFile = saveService.FileNames.LocalDataFile(mod); - - var importDate = 0L; - var localTags = Enumerable.Empty(); - var favorite = false; - var note = string.Empty; - - var save = true; - if (File.Exists(dataFile)) - try - { - var text = File.ReadAllText(dataFile); - var json = JObject.Parse(text); - - importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; - save = false; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not load local mod data:\n{e}"); - } - - if (importDate == 0) - importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - ModDataChangeType changes = 0; - if (mod.ImportDate != importDate) - { - mod.ImportDate = importDate; - changes |= ModDataChangeType.ImportDate; - } - - changes |= ModLocalData.UpdateTags(mod, null, localTags); - - if (mod.Favorite != favorite) - { - mod.Favorite = favorite; - changes |= ModDataChangeType.Favorite; - } - - if (mod.Note != note) - { - mod.Note = note; - changes |= ModDataChangeType.Note; - } - - if (save) - saveService.QueueSave(new ModLocalData(mod)); - - return changes; - } - - public ModDataChangeType LoadMeta(ModCreator creator, Mod mod) - { - var metaFile = saveService.FileNames.ModMetaPath(mod); - if (!File.Exists(metaFile)) - { - Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); - return ModDataChangeType.Deletion; - } - - try - { - var text = File.ReadAllText(metaFile); - var json = JObject.Parse(text); - - var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; - var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; - var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; - var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; - var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; - var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; - var importDate = json[nameof(Mod.ImportDate)]?.Value(); - var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); - - ModDataChangeType changes = 0; - if (mod.Name != newName) - { - changes |= ModDataChangeType.Name; - mod.Name = newName; - } - - if (mod.Author != newAuthor) - { - changes |= ModDataChangeType.Author; - mod.Author = newAuthor; - } - - if (mod.Description != newDescription) - { - changes |= ModDataChangeType.Description; - mod.Description = newDescription; - } - - if (mod.Image != newImage) - { - changes |= ModDataChangeType.Image; - mod.Image = newImage; - } - - if (mod.Version != newVersion) - { - changes |= ModDataChangeType.Version; - mod.Version = newVersion; - } - - if (mod.Website != newWebsite) - { - changes |= ModDataChangeType.Website; - mod.Website = newWebsite; - } - - if (newFileVersion != ModMeta.FileVersion) - if (ModMigration.Migrate(creator, saveService, mod, json, ref newFileVersion)) - { - changes |= ModDataChangeType.Migration; - saveService.ImmediateSave(new ModMeta(mod)); - } - - if (importDate != null && mod.ImportDate != importDate.Value) - { - mod.ImportDate = importDate.Value; - changes |= ModDataChangeType.ImportDate; - } - - changes |= ModLocalData.UpdateTags(mod, modTags, null); - - return changes; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}"); - return ModDataChangeType.Deletion; - } - } - public void ChangeModName(Mod mod, string newName) { if (mod.Name.Text == newName) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 18d2bc09..0db83ef9 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -72,11 +72,11 @@ public partial class ModCreator( if (!Directory.Exists(mod.ModPath.FullName)) return false; - modDataChange = dataEditor.LoadMeta(this, mod); + modDataChange = ModMeta.Load(dataEditor, this, mod); if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - modDataChange |= dataEditor.LoadLocalData(mod); + modDataChange |= ModLocalData.Load(dataEditor, mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index beda0dc7..d3534391 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -27,6 +27,63 @@ public readonly struct ModLocalData(Mod mod) : ISavable jObject.WriteTo(jWriter); } + public static ModDataChangeType Load(ModDataEditor editor, Mod mod) + { + var dataFile = editor.SaveService.FileNames.LocalDataFile(mod); + + var importDate = 0L; + var localTags = Enumerable.Empty(); + var favorite = false; + var note = string.Empty; + + var save = true; + if (File.Exists(dataFile)) + try + { + var text = File.ReadAllText(dataFile); + var json = JObject.Parse(text); + + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; + save = false; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load local mod data:\n{e}"); + } + + if (importDate == 0) + importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + ModDataChangeType changes = 0; + if (mod.ImportDate != importDate) + { + mod.ImportDate = importDate; + changes |= ModDataChangeType.ImportDate; + } + + changes |= ModLocalData.UpdateTags(mod, null, localTags); + + if (mod.Favorite != favorite) + { + mod.Favorite = favorite; + changes |= ModDataChangeType.Favorite; + } + + if (mod.Note != note) + { + mod.Note = note; + changes |= ModDataChangeType.Note; + } + + if (save) + editor.SaveService.QueueSave(new ModLocalData(mod)); + + return changes; + } + internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable? newModTags, IEnumerable? newLocalTags) { if (newModTags == null && newLocalTags == null) diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 39dd20e4..0cebcf81 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,5 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; using Penumbra.Services; namespace Penumbra.Mods; @@ -28,4 +30,85 @@ public readonly struct ModMeta(Mod mod) : ISavable jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } + + public static ModDataChangeType Load(ModDataEditor editor, ModCreator creator, Mod mod) + { + var metaFile = editor.SaveService.FileNames.ModMetaPath(mod); + if (!File.Exists(metaFile)) + { + Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); + return ModDataChangeType.Deletion; + } + + try + { + var text = File.ReadAllText(metaFile); + var json = JObject.Parse(text); + + var newFileVersion = json[nameof(FileVersion)]?.Value() ?? 0; + + // Empty name gets checked after loading and is not allowed. + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + + var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; + var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; + var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; + var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; + var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + + ModDataChangeType changes = 0; + if (mod.Name != newName) + { + changes |= ModDataChangeType.Name; + mod.Name = newName; + } + + if (mod.Author != newAuthor) + { + changes |= ModDataChangeType.Author; + mod.Author = newAuthor; + } + + if (mod.Description != newDescription) + { + changes |= ModDataChangeType.Description; + mod.Description = newDescription; + } + + if (mod.Image != newImage) + { + changes |= ModDataChangeType.Image; + mod.Image = newImage; + } + + if (mod.Version != newVersion) + { + changes |= ModDataChangeType.Version; + mod.Version = newVersion; + } + + if (mod.Website != newWebsite) + { + changes |= ModDataChangeType.Website; + mod.Website = newWebsite; + } + + if (newFileVersion != FileVersion) + if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion)) + { + changes |= ModDataChangeType.Migration; + editor.SaveService.ImmediateSave(new ModMeta(mod)); + } + + changes |= ModLocalData.UpdateTags(mod, modTags, null); + + return changes; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}"); + return ModDataChangeType.Deletion; + } + } } From ec3ec7db4e686318c9d7c2ee7a3ded8cc6357d4f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:55:02 +0100 Subject: [PATCH 2133/2451] update schema organization anf change some things. --- Penumbra.sln | 32 ++ schemas/container.json | 486 --------------------- schemas/default_mod.json | 20 +- schemas/group.json | 189 +------- schemas/local_mod_data-v3.json | 32 ++ schemas/{meta-v3.json => mod_meta-v3.json} | 17 +- schemas/structs/container.json | 34 ++ schemas/structs/group_combining.json | 31 ++ schemas/structs/group_imc.json | 50 +++ schemas/structs/group_multi.json | 32 ++ schemas/structs/group_single.json | 22 + schemas/structs/manipulation.json | 95 ++++ schemas/structs/meta_atch.json | 67 +++ schemas/structs/meta_enums.json | 57 +++ schemas/structs/meta_eqdp.json | 30 ++ schemas/structs/meta_eqp.json | 20 + schemas/structs/meta_est.json | 28 ++ schemas/structs/meta_geqp.json | 40 ++ schemas/structs/meta_gmp.json | 59 +++ schemas/structs/meta_imc.json | 87 ++++ schemas/structs/meta_rsp.json | 20 + schemas/structs/option.json | 24 + 22 files changed, 796 insertions(+), 676 deletions(-) delete mode 100644 schemas/container.json create mode 100644 schemas/local_mod_data-v3.json rename schemas/{meta-v3.json => mod_meta-v3.json} (79%) create mode 100644 schemas/structs/container.json create mode 100644 schemas/structs/group_combining.json create mode 100644 schemas/structs/group_imc.json create mode 100644 schemas/structs/group_multi.json create mode 100644 schemas/structs/group_single.json create mode 100644 schemas/structs/manipulation.json create mode 100644 schemas/structs/meta_atch.json create mode 100644 schemas/structs/meta_enums.json create mode 100644 schemas/structs/meta_eqdp.json create mode 100644 schemas/structs/meta_eqp.json create mode 100644 schemas/structs/meta_est.json create mode 100644 schemas/structs/meta_geqp.json create mode 100644 schemas/structs/meta_gmp.json create mode 100644 schemas/structs/meta_imc.json create mode 100644 schemas/structs/meta_rsp.json create mode 100644 schemas/structs/option.json diff --git a/Penumbra.sln b/Penumbra.sln index 94a04ef3..c0b38118 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -24,6 +24,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}" + ProjectSection(SolutionItems) = preProject + schemas\files\default_mod.json = schemas\files\default_mod.json + schemas\files\group.json = schemas\files\group.json + schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json + schemas\files\mod_meta-v3.json = schemas\files\mod_meta-v3.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}" + ProjectSection(SolutionItems) = preProject + schemas\structs\container.json = schemas\structs\container.json + schemas\structs\group_combining.json = schemas\structs\group_combining.json + schemas\structs\group_imc.json = schemas\structs\group_imc.json + schemas\structs\group_multi.json = schemas\structs\group_multi.json + schemas\structs\group_single.json = schemas\structs\group_single.json + schemas\structs\manipulation.json = schemas\structs\manipulation.json + schemas\structs\meta_atch.json = schemas\structs\meta_atch.json + schemas\structs\meta_enums.json = schemas\structs\meta_enums.json + schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json + schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json + schemas\structs\meta_est.json = schemas\structs\meta_est.json + schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json + schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json + schemas\structs\meta_imc.json = schemas\structs\meta_imc.json + schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json + schemas\structs\option.json = schemas\structs\option.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +86,10 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502} + {B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} EndGlobalSection diff --git a/schemas/container.json b/schemas/container.json deleted file mode 100644 index 5798f46c..00000000 --- a/schemas/container.json +++ /dev/null @@ -1,486 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", - "type": "object", - "properties": { - "Name": { - "description": "Name of the container/option/sub-mod.", - "type": ["string", "null"] - }, - "Files": { - "description": "File redirections in this container. Keys are game paths, values are relative file paths.", - "type": "object", - "patternProperties": { - "^[^/\\\\][^\\\\]*$": { - "type": "string", - "pattern": "^[^/\\\\][^/]*$" - } - }, - "additionalProperties": false - }, - "FileSwaps": { - "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", - "type": "object", - "patternProperties": { - "^[^/\\\\][^\\\\]*$": { - "type": "string", - "pattern": "^[^/\\\\][^/]*$" - } - }, - "additionalProperties": false - }, - "Manipulations": { - "type": "array", - "items": { - "$ref": "#/$defs/Manipulation" - } - } - }, - "$defs": { - "Manipulation": { - "type": "object", - "properties": { - "Type": { - "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] - }, - "Manipulation": { - "type": ["object", "null"] - } - }, - "required": ["Type", "Manipulation"], - "oneOf": [ - { - "properties": { - "Type": { - "const": "Unknown" - }, - "Manipulation": { - "type": "null" - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "Manipulation": { - "$ref": "#/$defs/ImcManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqdp" - }, - "Manipulation": { - "$ref": "#/$defs/EqdpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqp" - }, - "Manipulation": { - "$ref": "#/$defs/EqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Est" - }, - "Manipulation": { - "$ref": "#/$defs/EstManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Gmp" - }, - "Manipulation": { - "$ref": "#/$defs/GmpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Rsp" - }, - "Manipulation": { - "$ref": "#/$defs/RspManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "GlobalEqp" - }, - "Manipulation": { - "$ref": "#/$defs/GlobalEqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Atch" - }, - "Manipulation": { - "$ref": "#/$defs/AtchManipulation" - } - } - } - ], - "additionalProperties": false - }, - "ImcManipulation": { - "type": "object", - "properties": { - "Entry": { - "$ref": "#/$defs/ImcEntry" - }, - "Valid": { - "type": "boolean" - } - }, - "required": [ - "Entry" - ], - "allOf": [ - { - "$ref": "#/$defs/ImcIdentifier" - } - ], - "unevaluatedProperties": false - }, - "ImcIdentifier": { - "type": "object", - "properties": { - "PrimaryId": { - "type": "integer" - }, - "SecondaryId": { - "type": "integer" - }, - "Variant": { - "type": "integer" - }, - "ObjectType": { - "$ref": "#/$defs/ObjectType" - }, - "EquipSlot": { - "$ref": "#/$defs/EquipSlot" - }, - "BodySlot": { - "$ref": "#/$defs/BodySlot" - } - }, - "required": [ - "PrimaryId", - "SecondaryId", - "Variant", - "ObjectType", - "EquipSlot", - "BodySlot" - ] - }, - "ImcEntry": { - "type": "object", - "properties": { - "AttributeAndSound": { - "type": "integer" - }, - "MaterialId": { - "type": "integer" - }, - "DecalId": { - "type": "integer" - }, - "VfxId": { - "type": "integer" - }, - "MaterialAnimationId": { - "type": "integer" - }, - "AttributeMask": { - "type": "integer" - }, - "SoundId": { - "type": "integer" - } - }, - "required": [ - "MaterialId", - "DecalId", - "VfxId", - "MaterialAnimationId", - "AttributeMask", - "SoundId" - ], - "additionalProperties": false - }, - "EqdpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - }, - "ShiftedEntry": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EqpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - } - }, - "required": [ - "Entry", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EstManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "enum": ["Hair", "Face", "Body", "Head"] - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "GmpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "object", - "properties": { - "Enabled": { - "type": "boolean" - }, - "Animated": { - "type": "boolean" - }, - "RotationA": { - "type": "number" - }, - "RotationB": { - "type": "number" - }, - "RotationC": { - "type": "number" - }, - "UnknownA": { - "type": "number" - }, - "UnknownB": { - "type": "number" - }, - "UnknownTotal": { - "type": "number" - }, - "Value": { - "type": "number" - } - }, - "required": [ - "Enabled", - "Animated", - "RotationA", - "RotationB", - "RotationC", - "UnknownA", - "UnknownB", - "UnknownTotal", - "Value" - ], - "additionalProperties": false - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - } - }, - "required": [ - "Entry", - "SetId" - ], - "additionalProperties": false - }, - "RspManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "number" - }, - "SubRace": { - "$ref": "#/$defs/SubRace" - }, - "Attribute": { - "$ref": "#/$defs/RspAttribute" - } - }, - "additionalProperties": false - }, - "GlobalEqpManipulation": { - "type": "object", - "properties": { - "Condition": { - "type": "integer" - }, - "Type": { - "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] - } - }, - "additionalProperties": false - }, - "AtchManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "object", - "properties": { - "Bone": { - "type": "string", - "maxLength": 34 - }, - "Scale": { - "type": "number" - }, - "OffsetX": { - "type": "number" - }, - "OffsetY": { - "type": "number" - }, - "OffsetZ": { - "type": "number" - }, - "RotationX": { - "type": "number" - }, - "RotationY": { - "type": "number" - }, - "RotationZ": { - "type": "number" - } - }, - "required": [ - "Bone", - "Scale", - "OffsetX", - "OffsetY", - "OffsetZ", - "RotationX", - "RotationY", - "RotationZ" - ], - "additionalProperties": false - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "Type": { - "type": "string", - "minLength": 3, - "maxLength": 3 - }, - "Index": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "Type", - "Index" - ], - "additionalProperties": false - }, - "LaxInteger": { - "oneOf": [ - { - "type": "integer" - }, { - "type": "string", - "pattern": "^\\d+$" - } - ] - }, - "EquipSlot": { - "enum": ["Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All"] - }, - "Gender": { - "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] - }, - "ModelRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] - }, - "ObjectType": { - "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] - }, - "BodySlot": { - "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] - }, - "SubRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] - }, - "RspAttribute": { - "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] - } - } -} diff --git a/schemas/default_mod.json b/schemas/default_mod.json index eecd74d0..8f50c5db 100644 --- a/schemas/default_mod.json +++ b/schemas/default_mod.json @@ -1,25 +1,19 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", "allOf": [ { "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] - }, - "Description": { - "description": "Description of the sub-mod.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer", + "minimum": 0, + "maximum": 0 } } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + }, + { + "$ref": "structs/container.json" } ] } diff --git a/schemas/group.json b/schemas/group.json index 0078e9f3..4c37b631 100644 --- a/schemas/group.json +++ b/schemas/group.json @@ -1,35 +1,35 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer" }, "Name": { "description": "Name of the group.", - "type": "string" + "type": "string", + "minLength": 1 }, "Description": { "description": "Description of the group.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Image": { "description": "Relative path to a preview image for the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["string", "null"] + "type": ["string", "null" ] }, "Page": { "description": "TexTools page of the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["integer", "null"] + "type": "integer" }, "Priority": { "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", - "type": ["integer", "null"] + "type": "integer" }, "Type": { "description": "Group type. Single groups have one and only one of their options active at any point. Multi groups can have zero, one or many of their options active. Combining groups have n options, 2^n containers, and will have one and only one container active depending on the selected options.", - "enum": ["Single", "Multi", "Imc", "Combining"] + "enum": [ "Single", "Multi", "Imc", "Combining" ] }, "DefaultSettings": { "description": "Default configuration for the group.", @@ -42,165 +42,16 @@ ], "oneOf": [ { - "properties": { - "Type": { - "const": "Single" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Multi" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Priority": { - "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", - "type": ["integer", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "AllVariants": { - "type": "boolean" - }, - "OnlyAttributes": { - "type": "boolean" - }, - "Identifier": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" - }, - "DefaultEntry": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "oneOf": [ - { - "properties": { - "AttributeMask": { - "type": "integer" - } - }, - "required": [ - "AttributeMask" - ] - }, { - "properties": { - "IsDisableSubMod": { - "const": true - } - }, - "required": [ - "IsDisableSubMod" - ] - } - ], - "unevaluatedProperties": false - } - } - } - }, { - "properties": { - "Type": { - "const": "Combining" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "additionalProperties": false - } - }, - "Containers": { - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - } - } + "$ref": "structs/group_combining.json" + }, + { + "$ref": "structs/group_imc.json" + }, + { + "$ref": "structs/group_multi.json" + }, + { + "$ref": "structs/group_single.json" } - ], - "unevaluatedProperties": false + ] } diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json new file mode 100644 index 00000000..bf5d1311 --- /dev/null +++ b/schemas/local_mod_data-v3.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Local Penumbra Mod Data", + "description": "The locally stored data for an installed mod in Penumbra", + "type": "object", + "properties": { + "FileVersion": { + "description": "Major version of the local data schema used.", + "type": "integer", + "minimum": 3, + "maximum": 3 + }, + "ImportDate": { + "description": "The date and time of the installation of the mod as a Unix Epoch millisecond timestamp.", + "type": "integer" + }, + "LocalTags": { + "description": "User-defined local tags for the mod.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "Favorite": { + "description": "Whether the mod is favourited by the user.", + "type": "boolean" + } + }, + "required": [ "FileVersion" ] +} diff --git a/schemas/meta-v3.json b/schemas/mod_meta-v3.json similarity index 79% rename from schemas/meta-v3.json rename to schemas/mod_meta-v3.json index 1a132264..a926b49e 100644 --- a/schemas/meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", "title": "Penumbra Mod Metadata", "description": "Metadata of a Penumbra mod.", "type": "object", @@ -13,33 +12,35 @@ }, "Name": { "description": "Name of the mod.", - "type": "string" + "type": "string", + "minLength": 1 }, "Author": { "description": "Author of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Description": { "description": "Description of the mod. Can span multiple paragraphs.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Image": { "description": "Relative path to a preview image for the mod. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Version": { "description": "Version of the mod. Can be an arbitrary string.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Website": { "description": "URL of the web page of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "ModTags": { "description": "Author-defined tags for the mod.", "type": "array", "items": { - "type": "string" + "type": "string", + "minLength": 1 }, "uniqueItems": true } diff --git a/schemas/structs/container.json b/schemas/structs/container.json new file mode 100644 index 00000000..74db4a23 --- /dev/null +++ b/schemas/structs/container.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Files": { + "description": "File redirections in this container. Keys are game paths, values are relative file paths.", + "type": [ "object", "null" ], + "patternProperties": { + "^[^/\\\\.:?][^\\\\?:]+$": { + "type": "string", + "pattern": "^[^/\\\\.:?][^?:]+$" + } + }, + "additionalProperties": false + }, + "FileSwaps": { + "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", + "type": [ "object", "null" ], + "patternProperties": { + "^[^/\\\\.?:][^\\\\?:]+$": { + "type": "string", + "pattern": "^[^/\\\\.:?][^?:]+$" + } + }, + "additionalProperties": false + }, + "Manipulations": { + "type": [ "array", "null" ], + "items": { + "$ref": "manipulation.json" + } + } + } +} diff --git a/schemas/structs/group_combining.json b/schemas/structs/group_combining.json new file mode 100644 index 00000000..e42edcb8 --- /dev/null +++ b/schemas/structs/group_combining.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "Type": { + "const": "Combining" + }, + "Options": { + "type": "array", + "items": { + "$ref": "option.json" + } + }, + "Containers": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "container.json" + }, + { + "properties": { + "Name": { + "type": [ "string", "null" ] + } + } + } + ] + } + } + } +} diff --git a/schemas/structs/group_imc.json b/schemas/structs/group_imc.json new file mode 100644 index 00000000..48a04bd9 --- /dev/null +++ b/schemas/structs/group_imc.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "Type": { + "const": "Imc" + }, + "AllVariants": { + "type": "boolean" + }, + "OnlyAttributes": { + "type": "boolean" + }, + "Identifier": { + "$ref": "meta_imc.json#ImcIdentifier" + }, + "DefaultEntry": { + "$ref": "meta_imc.json#ImcEntry" + }, + "Options": { + "type": "array", + "items": { + "$ref": "option.json", + "oneOf": [ + { + "properties": { + "AttributeMask": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + } + }, + "required": [ + "AttributeMask" + ] + }, + { + "properties": { + "IsDisableSubMod": { + "const": true + } + }, + "required": [ + "IsDisableSubMod" + ] + } + ] + } + } + } +} diff --git a/schemas/structs/group_multi.json b/schemas/structs/group_multi.json new file mode 100644 index 00000000..ca7d4dfa --- /dev/null +++ b/schemas/structs/group_multi.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "const": "Multi" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "option.json" + }, + { + "$ref": "container.json" + }, + { + "properties": { + "Priority": { + "type": "integer" + } + } + } + ] + } + } + } +} + + + diff --git a/schemas/structs/group_single.json b/schemas/structs/group_single.json new file mode 100644 index 00000000..24cda88d --- /dev/null +++ b/schemas/structs/group_single.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "const": "Single" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "option.json" + }, + { + "$ref": "container.json" + } + ] + } + } + } +} diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json new file mode 100644 index 00000000..4a41dbe2 --- /dev/null +++ b/schemas/structs/manipulation.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ] + }, + "Manipulation": { + "type": "object" + } + }, + "required": [ "Type", "Manipulation" ], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Imc" + }, + "Manipulation": { + "$ref": "meta_imc.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Eqdp" + }, + "Manipulation": { + "$ref": "meta_eqdp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Eqp" + }, + "Manipulation": { + "$ref": "meta_eqp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Est" + }, + "Manipulation": { + "$ref": "meta_est.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Gmp" + }, + "Manipulation": { + "$ref": "meta_gmp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Rsp" + }, + "Manipulation": { + "$ref": "meta_rsp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "GlobalEqp" + }, + "Manipulation": { + "$ref": "meta_geqp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Atch" + }, + "Manipulation": { + "$ref": "meta_atch.json" + } + } + } + ] +} diff --git a/schemas/structs/meta_atch.json b/schemas/structs/meta_atch.json new file mode 100644 index 00000000..3c9cbef5 --- /dev/null +++ b/schemas/structs/meta_atch.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Bone": { + "type": "string", + "maxLength": 34 + }, + "Scale": { + "type": "number" + }, + "OffsetX": { + "type": "number" + }, + "OffsetY": { + "type": "number" + }, + "OffsetZ": { + "type": "number" + }, + "RotationX": { + "type": "number" + }, + "RotationY": { + "type": "number" + }, + "RotationZ": { + "type": "number" + } + }, + "required": [ + "Bone", + "Scale", + "OffsetX", + "OffsetY", + "OffsetZ", + "RotationX", + "RotationY", + "RotationZ" + ] + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "Type": { + "type": "string", + "minLength": 1, + "maxLength": 4 + }, + "Index": { + "$ref": "meta_enums.json#U16" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "Type", + "Index" + ] +} diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json new file mode 100644 index 00000000..747da849 --- /dev/null +++ b/schemas/structs/meta_enums.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "EquipSlot": { + "$anchor": "EquipSlot", + "enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ] + }, + "Gender": { + "$anchor": "Gender", + "enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ] + }, + "ModelRace": { + "$anchor": "ModelRace", + "enum": [ "Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera" ] + }, + "ObjectType": { + "$anchor": "ObjectType", + "enum": [ "Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font" ] + }, + "BodySlot": { + "$anchor": "BodySlot", + "enum": [ "Unknown", "Hair", "Face", "Tail", "Body", "Zear" ] + }, + "SubRace": { + "$anchor": "SubRace", + "enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ] + }, + "U8": { + "$anchor": "U8", + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + { + "type": "string", + "pattern": "^0*(1[0-9][0-9]|[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$" + } + ] + }, + "U16": { + "$anchor": "U16", + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + { + "type": "string", + "pattern": "^0*([1-5][0-9]{4}|[0-9]{0,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" + } + ] + } + } +} diff --git a/schemas/structs/meta_eqdp.json b/schemas/structs/meta_eqdp.json new file mode 100644 index 00000000..f27606b9 --- /dev/null +++ b/schemas/structs/meta_eqdp.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "$ref": "meta_enums.json#EquipSlot" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_eqp.json b/schemas/structs/meta_eqp.json new file mode 100644 index 00000000..c829d7a7 --- /dev/null +++ b/schemas/structs/meta_eqp.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "$ref": "meta_enums.json#EquipSlot" + } + }, + "required": [ + "Entry", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_est.json b/schemas/structs/meta_est.json new file mode 100644 index 00000000..22bce12b --- /dev/null +++ b/schemas/structs/meta_est.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "$ref": "meta_enums.json#U16" + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "enum": [ "Hair", "Face", "Body", "Head" ] + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_geqp.json b/schemas/structs/meta_geqp.json new file mode 100644 index 00000000..3d4908f9 --- /dev/null +++ b/schemas/structs/meta_geqp.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Condition": { + "$ref": "meta_enums.json#U16" + }, + "Type": { + "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + } + }, + "required": [ "Type" ], + "oneOf": [ + { + "properties": { + "Type": { + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + }, + "Condition": { + "const": 0 + } + } + }, + { + "properties": { + "Type": { + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + } + } + }, + { + "properties": { + "Type": { + "const": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL" ] + }, + "Condition": {} + } + } + ] +} diff --git a/schemas/structs/meta_gmp.json b/schemas/structs/meta_gmp.json new file mode 100644 index 00000000..bf1fb1df --- /dev/null +++ b/schemas/structs/meta_gmp.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Animated": { + "type": "boolean" + }, + "RotationA": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "RotationB": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "RotationC": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "UnknownA": { + "type": "integer", + "minimum": 0, + "maximum": 15 + }, + "UnknownB": { + "type": "integer", + "minimum": 0, + "maximum": 15 + } + }, + "required": [ + "Enabled", + "Animated", + "RotationA", + "RotationB", + "RotationC", + "UnknownA", + "UnknownB" + ], + "additionalProperties": false + }, + "SetId": { + "$ref": "meta_enums.json#U16" + } + }, + "required": [ + "Entry", + "SetId" + ] +} diff --git a/schemas/structs/meta_imc.json b/schemas/structs/meta_imc.json new file mode 100644 index 00000000..aa9a4fca --- /dev/null +++ b/schemas/structs/meta_imc.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "$ref": "#ImcEntry" + } + }, + "required": [ + "Entry" + ], + "allOf": [ + { + "$ref": "#ImcIdentifier" + } + ], + "$defs": { + "ImcIdentifier": { + "type": "object", + "properties": { + "PrimaryId": { + "$ref": "meta_enums.json#U16" + }, + "SecondaryId": { + "$ref": "meta_enums.json#U16" + }, + "Variant": { + "$ref": "meta_enums.json#U8" + }, + "ObjectType": { + "$ref": "meta_enums.json#ObjectType" + }, + "EquipSlot": { + "$ref": "meta_enums.json#EquipSlot" + }, + "BodySlot": { + "$ref": "meta_enums.json#BodySlot" + } + }, + "$anchor": "ImcIdentifier", + "required": [ + "PrimaryId", + "SecondaryId", + "Variant", + "ObjectType", + "EquipSlot", + "BodySlot" + ] + }, + "ImcEntry": { + "type": "object", + "properties": { + "MaterialId": { + "$ref": "meta_enums.json#U8" + }, + "DecalId": { + "$ref": "meta_enums.json#U8" + }, + "VfxId": { + "$ref": "meta_enums.json#U8" + }, + "MaterialAnimationId": { + "$ref": "meta_enums.json#U8" + }, + "AttributeMask": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "SoundId": { + "type": "integer", + "minimum": 0, + "maximum": 63 + } + }, + "$anchor": "ImcEntry", + "required": [ + "MaterialId", + "DecalId", + "VfxId", + "MaterialAnimationId", + "AttributeMask", + "SoundId" + ] + } + } +} diff --git a/schemas/structs/meta_rsp.json b/schemas/structs/meta_rsp.json new file mode 100644 index 00000000..3354281b --- /dev/null +++ b/schemas/structs/meta_rsp.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "number" + }, + "SubRace": { + "$ref": "meta_enums.json#SubRace" + }, + "Attribute": { + "enum": [ "MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ" ] + } + }, + "required": [ + "Entry", + "SubRace", + "Attribute" + ] +} diff --git a/schemas/structs/option.json b/schemas/structs/option.json new file mode 100644 index 00000000..c45ccfdb --- /dev/null +++ b/schemas/structs/option.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string", + "minLength": 1 + }, + "Description": { + "description": "Description of the option.", + "type": [ "string", "null" ] + }, + "Priority": { + "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", + "type": "integer" + }, + "Image": { + "description": "Unused by Penumbra.", + "type": [ "string", "null" ] + } + }, + "required": [ "Name" ] +} From b62563d72131c2119370b3d25677457291c15b96 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 20:06:21 +0100 Subject: [PATCH 2134/2451] Remove $id from shpk_devkit schema --- schemas/shpk_devkit.json | 1 - 1 file changed, 1 deletion(-) diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json index cd18ab81..f03fbb05 100644 --- a/schemas/shpk_devkit.json +++ b/schemas/shpk_devkit.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", "type": "object", "properties": { "ShaderKeys": { From 4f0428832cadfe61c4c49dd7dcbfdeacde5332bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:13:09 +0100 Subject: [PATCH 2135/2451] Fix solution file for schemas. --- Penumbra.sln | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index c0b38118..e864fbee 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -26,10 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Pe EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}" ProjectSection(SolutionItems) = preProject - schemas\files\default_mod.json = schemas\files\default_mod.json - schemas\files\group.json = schemas\files\group.json - schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json - schemas\files\mod_meta-v3.json = schemas\files\mod_meta-v3.json + schemas\default_mod.json = schemas\default_mod.json + schemas\group.json = schemas\group.json + schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json + schemas\mod_meta-v3.json = schemas\mod_meta-v3.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}" From 7b517390b6c619a27de6698ada6627b80ae21c75 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:30:03 +0100 Subject: [PATCH 2136/2451] Fix temporary settings causing collection saves. --- Penumbra/Collections/Manager/CollectionEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 437d4e0b..f4902fda 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -194,7 +194,8 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); + if (type is not ModSettingChange.TemporarySetting) + saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); if (type is not ModSettingChange.TemporarySetting) RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); From 8779f4b6893b6d472c71fb63d0f31ef947140424 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:36:05 +0100 Subject: [PATCH 2137/2451] Add new cutscene ENPC tracking hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/GameState.cs | 3 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Objects/ConstructCutsceneCharacter.cs | 70 +++++++++++++++++++ Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/SetupPlayerNpc.cs | 55 +++++++++++++++ .../Interop/PathResolving/CutsceneService.cs | 29 +++++--- 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs create mode 100644 Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index c5250722..5bac66e5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 +Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2 diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index f80ef696..497be508 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -11,7 +11,8 @@ public class GameState : IService { #region Last Game Object - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + public readonly ThreadLocal CharacterAssociated = new(() => false); public nint LastGameObject => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index b95e5789..5a856764 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -76,6 +76,8 @@ public class HookOverrides public bool CreateCharacterBase; public bool EnableDraw; public bool WeaponReload; + public bool SetupPlayerNpc; + public bool ConstructCutsceneCharacter; } public struct PostProcessingHooks diff --git a/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs new file mode 100644 index 00000000..5fa3de32 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs @@ -0,0 +1,70 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.Interop.Hooks.Objects; + +public sealed unsafe class ConstructCutsceneCharacter : EventWrapperPtr, IHookService +{ + private readonly GameState _gameState; + private readonly ObjectManager _objects; + + public enum Priority + { + /// + CutsceneService = 0, + } + + public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects) + : base("ConstructCutsceneCharacter") + { + _gameState = gameState; + _objects = objects; + _task = hooks.CreateHook(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter); + } + + private readonly Task> _task; + + public delegate int Delegate(SetupPlayerNpc.SchedulerStruct* scheduler); + + public int Detour(SetupPlayerNpc.SchedulerStruct* scheduler) + { + // This is the function that actually creates the new game object + // and fills it into the object table at a free index etc. + var ret = _task.Result.Original(scheduler); + // Check for the copy state from SetupPlayerNpc. + if (_gameState.CharacterAssociated.Value) + { + // If the newly created character exists, invoke the event. + var character = _objects[ret + (int)ScreenActor.CutsceneStart].AsCharacter; + if (character != null) + { + Invoke(character); + Penumbra.Log.Verbose( + $"[{Name}] Created indirect copy of player character at 0x{(nint)character}, index {character->ObjectIndex}."); + } + _gameState.CharacterAssociated.Value = false; + } + + return ret; + } + + public IntPtr Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; +} diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 68bb28af..979cb87c 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService private void Detour(GameObject* gameObject) { _state.QueueGameObject(gameObject); - Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}."); + Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X} at {gameObject->ObjectIndex}."); _task.Result.Original.Invoke(gameObject); _state.DequeueGameObject(); } diff --git a/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs new file mode 100644 index 00000000..8f1226c3 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.Objects; + +public sealed unsafe class SetupPlayerNpc : FastHook +{ + private readonly GameState _gameState; + + public SetupPlayerNpc(GameState gameState, HookManager hooks) + { + _gameState = gameState; + Task = hooks.CreateHook("SetupPlayerNPC", Sigs.SetupPlayerNpc, Detour, + !HookOverrides.Instance.Objects.SetupPlayerNpc); + } + + public delegate SchedulerStruct* Delegate(byte* npcType, nint unk, NpcSetupData* setupData); + + public SchedulerStruct* Detour(byte* npcType, nint unk, NpcSetupData* setupData) + { + // This function actually seems to generate all NPC. + + // If an ENPC is being created, check the creation parameters. + // If CopyPlayerCustomize is true, the event NPC gets a timeline that copies its customize and glasses from the local player. + // Keep track of this, so we can associate the actor to be created for this with the player character, see ConstructCutsceneCharacter. + if (setupData->CopyPlayerCustomize && npcType != null && *npcType is 8) + _gameState.CharacterAssociated.Value = true; + + var ret = Task.Result.Original.Invoke(npcType, unk, setupData); + Penumbra.Log.Excessive( + $"[Setup Player NPC] Invoked for type {*npcType} with 0x{unk:X} and Copy Player Customize: {setupData->CopyPlayerCustomize}."); + return ret; + } + + [StructLayout(LayoutKind.Explicit)] + public struct NpcSetupData + { + [FieldOffset(0x0B)] + private byte _copyPlayerCustomize; + + public bool CopyPlayerCustomize + { + get => _copyPlayerCustomize != 0; + set => _copyPlayerCustomize = value ? (byte)1 : (byte)0; + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct SchedulerStruct + { + public static Character* GetCharacter(SchedulerStruct* s) + => ((delegate* unmanaged**)s)[0][19](s); + } +} diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 8e32dd76..6be19c46 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -15,10 +15,11 @@ public sealed class CutsceneService : IRequiredService, IDisposable public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; - private readonly ObjectManager _objects; - private readonly CopyCharacter _copyCharacter; - private readonly CharacterDestructor _characterDestructor; - private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); + private readonly ObjectManager _objects; + private readonly CopyCharacter _copyCharacter; + private readonly CharacterDestructor _characterDestructor; + private readonly ConstructCutsceneCharacter _constructCutsceneCharacter; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) @@ -26,13 +27,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, - IClientState clientState) + ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState) { - _objects = objects; - _copyCharacter = copyCharacter; - _characterDestructor = characterDestructor; + _objects = objects; + _copyCharacter = copyCharacter; + _characterDestructor = characterDestructor; + _constructCutsceneCharacter = constructCutsceneCharacter; _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); + _constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService); if (clientState.IsGPosing) RecoverGPoseActors(); } @@ -87,6 +90,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable { _copyCharacter.Unsubscribe(OnCharacterCopy); _characterDestructor.Unsubscribe(OnCharacterDestructor); + _constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc); } private unsafe void OnCharacterDestructor(Character* character) @@ -124,6 +128,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } + private unsafe void OnSetupPlayerNpc(Character* npc) + { + if (npc == null || npc->ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) + return; + + var idx = npc->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = 0; + } + /// Try to recover GPose actors on reloads into a running game. /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. private void RecoverGPoseActors() From 0c8571fba92058b8281efcea0cdb2583104bf7ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 17:14:13 +0100 Subject: [PATCH 2138/2451] Reduce and pad IMC allocations and log allocations. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 1 - Penumbra/Collections/ModCollectionIdentity.cs | 6 +++--- Penumbra/Interop/GameState.cs | 4 ++-- Penumbra/Meta/Files/ImcFile.cs | 15 ++++++++++----- Penumbra/Meta/Files/MetaBaseFile.cs | 15 +++++++++++++-- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 5bac66e5..ebeea67c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2 +Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262 diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 0f610d90..461ffccc 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -51,7 +51,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) if (!_imcFiles.TryGetValue(path, out var pair)) pair = (new ImcFile(Manager, identifier), []); - if (!Apply(pair.Item1, identifier, entry)) return; diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs index c7f60005..bd2d47c4 100644 --- a/Penumbra/Collections/ModCollectionIdentity.cs +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -10,9 +10,9 @@ public struct ModCollectionIdentity(Guid id, LocalCollectionId localId) public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0); - public string Name { get; set; } - public Guid Id { get; } = id; - public LocalCollectionId LocalId { get; } = localId; + public string Name { get; set; } = string.Empty; + public Guid Id { get; } = id; + public LocalCollectionId LocalId { get; } = localId; /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 497be508..95cef468 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -11,8 +11,8 @@ public class GameState : IService { #region Last Game Object - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); - public readonly ThreadLocal CharacterAssociated = new(() => false); + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + public readonly ThreadLocal CharacterAssociated = new(() => false); public nint LastGameObject => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 01ef3f16..de022f4c 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,3 +1,4 @@ +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -192,22 +193,26 @@ public unsafe class ImcFile : MetaBaseFile public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - if (length == ActualLength) + var actualLength = ActualLength; + if (length >= actualLength) { - MemoryUtility.MemCpyUnchecked((byte*)data, Data, ActualLength); + MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); + MemoryUtility.MemSet((byte*)data + actualLength, 0, length - actualLength); return; } - var newData = Manager.XivAllocator.Allocate(ActualLength, 8); + var paddedLength = actualLength.PadToMultiple(128); + var newData = Manager.XivAllocator.Allocate(paddedLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); + MemoryUtility.MemSet((byte*)data + actualLength, 0, paddedLength - actualLength); Manager.XivAllocator.Release((void*)data, length); - resource->SetData((nint)newData, ActualLength); + resource->SetData((nint)newData, paddedLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 5bc36068..0cb34ab3 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -28,11 +28,16 @@ public unsafe interface IFileAllocator public sealed class MarshalAllocator : IFileAllocator { public unsafe T* Allocate(int length, int alignment = 1) where T : unmanaged - => (T*)Marshal.AllocHGlobal(length * sizeof(T)); + { + var ret = (T*)Marshal.AllocHGlobal(length * sizeof(T)); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via Marshal Allocator to 0x{(nint)ret:X}."); + return ret; + } public unsafe void Release(ref T* pointer, int length) where T : unmanaged { Marshal.FreeHGlobal((nint)pointer); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via Marshal Allocator."); pointer = null; } } @@ -53,11 +58,17 @@ public sealed unsafe class XivFileAllocator : IFileAllocator, IService => ((delegate* unmanaged)_getFileSpaceAddress)(); public T* Allocate(int length, int alignment = 1) where T : unmanaged - => (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + { + var ret = (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via FFXIV File Allocator to 0x{(nint)ret:X}."); + return ret; + } public void Release(ref T* pointer, int length) where T : unmanaged { + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via FFXIV File Allocator."); pointer = null; } } From 9ca0145a7f59e002b805a1c6060e05df67d7a7ac Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 20 Jan 2025 16:48:19 +0000 Subject: [PATCH 2139/2451] [CI] Updating repo.json for testing_1.3.3.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2e3dd8bc..7e37f77c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.5", + "TestingAssemblyVersion": "1.3.3.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 39c73af2382228a0b5f867baf5be44979136deee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 20:33:28 +0100 Subject: [PATCH 2140/2451] Fix stupid. --- Penumbra/Meta/Files/ImcFile.cs | 14 ++++++++------ Penumbra/Meta/Files/MetaBaseFile.cs | 18 ++++++++++++++++++ Penumbra/Meta/MetaFileManager.cs | 26 ++++++++++++++------------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index de022f4c..c6e4ec94 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -197,22 +197,24 @@ public unsafe class ImcFile : MetaBaseFile if (length >= actualLength) { MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); - MemoryUtility.MemSet((byte*)data + actualLength, 0, length - actualLength); + if (length > actualLength) + MemoryUtility.MemSet((byte*)(data + actualLength), 0, length - actualLength); return; } var paddedLength = actualLength.PadToMultiple(128); - var newData = Manager.XivAllocator.Allocate(paddedLength, 8); + var newData = Manager.XivFileAllocator.Allocate(paddedLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); - MemoryUtility.MemSet((byte*)data + actualLength, 0, paddedLength - actualLength); - - Manager.XivAllocator.Release((void*)data, length); + if (paddedLength > actualLength) + MemoryUtility.MemSet(newData + actualLength, 0, paddedLength - actualLength); + + Manager.XivFileAllocator.Release((void*)data, length); resource->SetData((nint)newData, paddedLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 0cb34ab3..d04e1bdf 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -73,6 +73,24 @@ public sealed unsafe class XivFileAllocator : IFileAllocator, IService } } +public sealed unsafe class XivDefaultAllocator : IFileAllocator, IService +{ + public T* Allocate(int length, int alignment = 1) where T : unmanaged + { + var ret = (T*)IMemorySpace.GetDefaultSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via FFXIV Default Allocator to 0x{(nint)ret:X}."); + return ret; + } + + public void Release(ref T* pointer, int length) where T : unmanaged + { + + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via FFXIV Default Allocator."); + pointer = null; + } +} + public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, MetaIndex idx) : IDisposable { protected readonly MetaFileManager Manager = manager; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 5250273b..6130a48f 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -28,24 +28,26 @@ public class MetaFileManager : IService internal readonly ImcChecker ImcChecker; internal readonly AtchManager AtchManager; internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); - internal readonly IFileAllocator XivAllocator; + internal readonly IFileAllocator XivFileAllocator; + internal readonly IFileAllocator XivDefaultAllocator; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager) { - CharacterUtility = characterUtility; - ResidentResources = residentResources; - GameData = gameData; - ActiveCollections = activeCollections; - Config = config; - ValidityChecker = validityChecker; - Identifier = identifier; - Compactor = compactor; - AtchManager = atchManager; - ImcChecker = new ImcChecker(this); - XivAllocator = new XivFileAllocator(interop); + CharacterUtility = characterUtility; + ResidentResources = residentResources; + GameData = gameData; + ActiveCollections = activeCollections; + Config = config; + ValidityChecker = validityChecker; + Identifier = identifier; + Compactor = compactor; + AtchManager = atchManager; + ImcChecker = new ImcChecker(this); + XivFileAllocator = new XivFileAllocator(interop); + XivDefaultAllocator = new XivDefaultAllocator(); interop.InitializeFromAttributes(this); } From 737e74582bbc2a85ebbed6dfbc771adf881392fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 20 Jan 2025 19:36:05 +0000 Subject: [PATCH 2141/2451] [CI] Updating repo.json for testing_1.3.3.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7e37f77c..3e258788 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.6", + "TestingAssemblyVersion": "1.3.3.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2afd6b966e4c79c7c11bbcd9a2b2c8c074a91495 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 17:36:01 +0100 Subject: [PATCH 2142/2451] Add debug logging facilities. --- OtterGui | 2 +- Penumbra.String | 2 +- Penumbra/DebugConfiguration.cs | 6 ++++ Penumbra/Meta/Files/ImcFile.cs | 29 +++++++++++++++++-- .../UI/Tabs/Debug/DebugConfigurationDrawer.cs | 14 +++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 1 + 6 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 Penumbra/DebugConfiguration.cs create mode 100644 Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs diff --git a/OtterGui b/OtterGui index 055f1695..3c1260c9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 055f169572223fd1b59389549c88b4c861c94608 +Subproject commit 3c1260c9833303c2d33d12d6f77dc2b1afea3f34 diff --git a/Penumbra.String b/Penumbra.String index b9003b97..0bc2b0f6 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit b9003b97da2d1191fa203a4d66956bc54c21db2a +Subproject commit 0bc2b0f66eee1a02c9575b2bb30f27ce166f8632 diff --git a/Penumbra/DebugConfiguration.cs b/Penumbra/DebugConfiguration.cs new file mode 100644 index 00000000..76987df8 --- /dev/null +++ b/Penumbra/DebugConfiguration.cs @@ -0,0 +1,6 @@ +namespace Penumbra; + +public class DebugConfiguration +{ + public static bool WriteImcBytesToLog = false; +} diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index c6e4ec94..23339cfc 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,3 +1,4 @@ +using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -194,11 +195,28 @@ public unsafe class ImcFile : MetaBaseFile { var (data, length) = resource->GetData(); var actualLength = ActualLength; + + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}:"); + Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); + Penumbra.Log.Information(new Span(Data, actualLength).WriteHexBytes()); + Penumbra.Log.Information(new Span(Data, actualLength).WriteHexByteDiff(new Span((void*)data, length))); + } + if (length >= actualLength) { MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); if (length > actualLength) MemoryUtility.MemSet((byte*)(data + actualLength), 0, length - actualLength); + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information( + $"Copied {actualLength} bytes from local IMC file into {length} available bytes.{(length > actualLength ? $" Filled remaining {length - actualLength} bytes with 0." : string.Empty)}"); + Penumbra.Log.Information("Result IMC Resource Data:"); + Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); + } + return; } @@ -209,11 +227,18 @@ public unsafe class ImcFile : MetaBaseFile Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); if (paddedLength > actualLength) MemoryUtility.MemSet(newData + actualLength, 0, paddedLength - actualLength); - + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information( + $"Allocated {paddedLength} bytes for IMC file, copied {actualLength} bytes from local IMC file. {(length > actualLength ? $" Filled remaining {length - actualLength} bytes with 0." : string.Empty)}"); + Penumbra.Log.Information("Result IMC Resource Data:"); + Penumbra.Log.Information(new Span(newData, paddedLength).WriteHexBytes()); + } + Manager.XivFileAllocator.Release((void*)data, length); resource->SetData((nint)newData, paddedLength); } diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs new file mode 100644 index 00000000..34aafbea --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -0,0 +1,14 @@ +using OtterGui.Text; + +namespace Penumbra.UI.Tabs.Debug; + +public static class DebugConfigurationDrawer +{ + public static void Draw() + { + if (!ImUtf8.CollapsingHeaderId("Debug Logging Options")) + return; + + ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 77eeb3d7..ad4824c3 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -181,6 +181,7 @@ public class DebugTab : Window, ITab, IUiService DrawDebugTabGeneral(); _crashHandlerPanel.Draw(); + DebugConfigurationDrawer.Draw(); _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); DrawPathResolverDebug(); From dcc435477738d79a3c4301bf862e89ffef705bb7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 17:36:13 +0100 Subject: [PATCH 2143/2451] Fix clipping height in changed items tab. --- Penumbra/UI/Tabs/ChangedItemsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 256b0d79..5bac7d35 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -36,7 +36,7 @@ public class ChangedItemsTab( if (!child) return; - var height = ImGui.GetFrameHeight() + 2 * ImGui.GetStyle().CellPadding.Y; + var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips(height); using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) From dcab443b2fb221104b471c4358bdf8bc8da8bc54 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 22 Jan 2025 16:38:33 +0000 Subject: [PATCH 2144/2451] [CI] Updating repo.json for testing_1.3.3.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3e258788..e0cf2a70 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.7", + "TestingAssemblyVersion": "1.3.3.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 40168d7daf418c5743558bb2906ae52456e9d7e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 23:14:09 +0100 Subject: [PATCH 2145/2451] Fix issue with IPC adding mods before character utility is ready in rare cases. --- Penumbra/Api/IpcProviders.cs | 20 +++++++++++++--- .../Cache/CollectionCacheManager.cs | 6 ++--- .../Communication/CharacterUtilityFinished.cs | 23 +++++++++++++++++++ Penumbra/Interop/Services/CharacterUtility.cs | 21 +++++++++-------- Penumbra/Mods/Editor/ModMetaEditor.cs | 5 ++++ 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 Penumbra/Communication/CharacterUtilityFinished.cs diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index f6948832..fc97290f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -2,6 +2,8 @@ using Dalamud.Plugin; using OtterGui.Services; using Penumbra.Api.Api; using Penumbra.Api.Helpers; +using Penumbra.Communication; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Api; @@ -9,11 +11,13 @@ public sealed class IpcProviders : IDisposable, IApiService { private readonly List _providers; - private readonly EventProvider _disposedProvider; - private readonly EventProvider _initializedProvider; + private readonly EventProvider _disposedProvider; + private readonly EventProvider _initializedProvider; + private readonly CharacterUtility _characterUtility; - public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api) + public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility) { + _characterUtility = characterUtility; _disposedProvider = IpcSubscribers.Disposed.Provider(pi); _initializedProvider = IpcSubscribers.Initialized.Provider(pi); _providers = @@ -115,11 +119,21 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), ]; + if (_characterUtility.Ready) + _initializedProvider.Invoke(); + else + _characterUtility.LoadingFinished.Subscribe(OnCharacterUtilityReady, CharacterUtilityFinished.Priority.IpcProvider); + } + + private void OnCharacterUtilityReady() + { _initializedProvider.Invoke(); + _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady); } public void Dispose() { + _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady); foreach (var provider in _providers) provider.Dispose(); _providers.Clear(); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 27b969c2..c46759c7 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -71,7 +71,7 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager); if (!MetaFileManager.CharacterUtility.Ready) - MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager); } public void Dispose() @@ -83,7 +83,7 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); - MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters); foreach (var collection in _storage) { @@ -298,7 +298,7 @@ public class CollectionCacheManager : IDisposable, IService { foreach (var collection in _storage.Where(c => c.HasCache)) collection.Counters.IncrementChange(); - MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters); } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _) diff --git a/Penumbra/Communication/CharacterUtilityFinished.cs b/Penumbra/Communication/CharacterUtilityFinished.cs new file mode 100644 index 00000000..fbeeb8a7 --- /dev/null +++ b/Penumbra/Communication/CharacterUtilityFinished.cs @@ -0,0 +1,23 @@ +using OtterGui.Classes; +using Penumbra.Api; +using Penumbra.Interop.Services; + +namespace Penumbra.Communication; + +/// +/// Triggered when the Character Utility becomes ready. +/// +public sealed class CharacterUtilityFinished() : EventWrapper(nameof(CharacterUtilityFinished)) +{ + public enum Priority + { + /// + OnFinishedLoading = int.MaxValue, + + /// + IpcProvider = int.MinValue, + + /// + CollectionCacheManager = 0, + } +} diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 1641e42d..0add9d46 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using OtterGui.Services; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -26,14 +27,16 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService public CharacterUtilityData* Address => *_characterUtilityAddress; - public bool Ready { get; private set; } - public event Action LoadingFinished; - public nint DefaultHumanPbdResource { get; private set; } - public nint DefaultTransparentResource { get; private set; } - public nint DefaultDecalResource { get; private set; } - public nint DefaultSkinShpkResource { get; private set; } - public nint DefaultCharacterStockingsShpkResource { get; private set; } - public nint DefaultCharacterLegacyShpkResource { get; private set; } + public bool Ready { get; private set; } + + public readonly CharacterUtilityFinished LoadingFinished = new(); + + public nint DefaultHumanPbdResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultCharacterStockingsShpkResource { get; private set; } + public nint DefaultCharacterLegacyShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -61,7 +64,7 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService .Select(idx => new MetaList(new InternalIndex(idx))) .ToArray(); _framework = framework; - LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); + LoadingFinished.Subscribe(() => Penumbra.Log.Debug("Loading of CharacterUtility finished."), CharacterUtilityFinished.Priority.OnFinishedLoading); LoadDefaultResources(null!); if (!Ready) _framework.Update += LoadDefaultResources; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index c06af9c7..c5c8fb8b 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -70,6 +70,11 @@ public class ModMetaEditor( public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { + if (!metaFileManager.CharacterUtility.Ready) + { + Penumbra.Log.Warning("Trying to delete default meta values before CharacterUtility was ready, skipped."); + return false; + } var clone = dict.Clone(); dict.ClearForDefault(); From 55ce63383255b541d3644587a10db9c765b519b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Jan 2025 18:11:35 +0100 Subject: [PATCH 2146/2451] Try forcing IMC files to load synchronously for now. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs | 8 ++++---- Penumbra/Interop/Processing/ImcFilePostProcessor.cs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index ad9c41e6..a74a3712 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -176,7 +176,7 @@ public unsafe class ResourceLoader : IDisposable, IService gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; - MtrlForceSync(fileDescriptor, ref isSync); + ForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -215,14 +215,14 @@ public unsafe class ResourceLoader : IDisposable, IService } } - /// Special handling for materials. - private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) + /// Special handling for materials and IMCs. + private static void ForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) { // Force isSync = true for Materials. I don't really understand why, // or where the difference even comes from. // Was called with True on my client and with false on other peoples clients, // which caused problems. - isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl; + isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl or ResourceType.Imc; } /// diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 513877d4..949baaa3 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.JobGauge.Types; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; From 0159eb3d8348b4d01e21d542aeeb28cf06201bc6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 23 Jan 2025 17:13:52 +0000 Subject: [PATCH 2147/2451] [CI] Updating repo.json for testing_1.3.3.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e0cf2a70..0f61c85c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.8", + "TestingAssemblyVersion": "1.3.3.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a3ddce0ef5aed4a0282039e79d46417b108c5716 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 24 Jan 2025 02:50:02 +0100 Subject: [PATCH 2148/2451] Add mechanism to handle completion of async res loads --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../Hooks/ResourceLoading/ResourceLoader.cs | 120 +++++++++++++++--- .../Hooks/ResourceLoading/ResourceService.cs | 78 ++++++++++-- .../Resources/ResourceHandleDestructor.cs | 5 +- .../Processing/FilePostProcessService.cs | 18 ++- Penumbra/Interop/Structs/ResourceHandle.cs | 15 ++- Penumbra/UI/ResourceWatcher/Record.cs | 29 ++++- .../UI/ResourceWatcher/ResourceWatcher.cs | 31 ++++- .../ResourceWatcher/ResourceWatcherTable.cs | 25 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 37 +++++- 11 files changed, 303 insertions(+), 58 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ebeea67c..4a987167 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262 +Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 5a856764..bcff25d2 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -100,6 +100,7 @@ public class HookOverrides public bool DecRef; public bool GetResourceSync; public bool GetResourceAsync; + public bool UpdateResourceState; public bool CheckFileState; public bool TexResourceHandleOnLoad; public bool LoadMdlFileExtern; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index ad9c41e6..f9b8ff60 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,7 +1,9 @@ +using System.IO; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -13,27 +15,38 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceLoader : IDisposable, IService { - private readonly ResourceService _resources; - private readonly FileReadService _fileReadService; - private readonly RsfService _rsfService; - private readonly PapHandler _papHandler; - private readonly Configuration _config; + private readonly ResourceService _resources; + private readonly FileReadService _fileReadService; + private readonly RsfService _rsfService; + private readonly PapHandler _papHandler; + private readonly Configuration _config; + private readonly ResourceHandleDestructor _destructor; + + private readonly ConcurrentDictionary _ongoingLoads = []; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner) + public IReadOnlyDictionary OngoingLoads + => _ongoingLoads; + + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner, + ResourceHandleDestructor destructor) { _resources = resources; _fileReadService = fileReadService; - _rsfService = rsfService; + _rsfService = rsfService; _config = config; + _destructor = destructor; ResetResolvePath(); - _resources.ResourceRequested += ResourceHandler; - _resources.ResourceHandleIncRef += IncRefProtection; - _resources.ResourceHandleDecRef += DecRefProtection; - _fileReadService.ReadSqPack += ReadSqPackDetour; + _resources.ResourceRequested += ResourceHandler; + _resources.ResourceStateUpdating += ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated += ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef += IncRefProtection; + _resources.ResourceHandleDecRef += DecRefProtection; + _fileReadService.ReadSqPack += ReadSqPackDetour; + _destructor.Subscribe(ResourceDestructorHandler, ResourceHandleDestructor.Priority.ResourceLoader); _papHandler = new PapHandler(sigScanner, PapResourceHandler); _papHandler.Enable(); @@ -109,12 +122,32 @@ public unsafe class ResourceLoader : IDisposable, IService /// public event FileLoadedDelegate? FileLoaded; + public delegate void ResourceCompleteDelegate(ResourceHandle* resource, CiByteString path, Utf8GamePath originalPath, + ReadOnlySpan additionalData, bool isAsync); + + /// + /// Event fired just before a resource finishes loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? BeforeResourceComplete; + + /// + /// Event fired when a resource has finished loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? ResourceComplete; + public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; - _resources.ResourceHandleIncRef -= IncRefProtection; - _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceStateUpdating -= ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated -= ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef -= IncRefProtection; + _resources.ResourceHandleDecRef -= DecRefProtection; + _fileReadService.ReadSqPack -= ReadSqPackDetour; + _destructor.Unsubscribe(ResourceDestructorHandler); _papHandler.Dispose(); } @@ -135,7 +168,8 @@ public unsafe class ResourceLoader : IDisposable, IService if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; } @@ -145,10 +179,57 @@ public unsafe class ResourceLoader : IDisposable, IService hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } + private void TrackResourceLoad(ResourceHandle* handle, Utf8GamePath original) + { + if (handle->UnkState == 2 && handle->LoadState >= LoadState.Success) + return; + + _ongoingLoads.TryAdd((nint)handle, original.Clone()); + } + + private void ResourceStateUpdatedHandler(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte, LoadState) previousState, ref uint returnValue) + { + if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState.Item1 == 2 && previousState.Item2 >= LoadState.Success) + return; + + if (!_ongoingLoads.TryRemove((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + if (!syncOriginal.IsEmpty && !asyncOriginal.IsEmpty && !syncOriginal.Equals(asyncOriginal)) + Penumbra.Log.Warning($"[ResourceLoader] Resource original paths inconsistency: 0x{(nint)handle:X}, of path {path}, sync original {syncOriginal}, async original {asyncOriginal}."); + var original = !asyncOriginal.IsEmpty ? asyncOriginal : syncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + ResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + ResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + + private void ResourceStateUpdatingHandler(ResourceHandle* handle, Utf8GamePath syncOriginal) + { + if (handle->UnkState != 1 || handle->LoadState != LoadState.Success) + return; + + if (!_ongoingLoads.TryGetValue((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + var original = asyncOriginal.IsEmpty ? syncOriginal : asyncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + BeforeResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + BeforeResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) { if (fileDescriptor->ResourceHandle == null) @@ -265,6 +346,11 @@ public unsafe class ResourceLoader : IDisposable, IService returnValue = 1; } + private void ResourceDestructorHandler(ResourceHandle* handle) + { + _ongoingLoads.TryRemove((nint)handle, out _); + } + /// Compute the CRC32 hash for a given path together with potential resource parameters. private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams) { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 126505d1..238ed70f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -19,6 +19,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; + private readonly ThreadLocal _currentGetResourcePath = new(() => Utf8GamePath.Empty); + public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _performance = performance; @@ -34,6 +36,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService _getResourceSyncHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); + if (!HookOverrides.Instance.ResourceLoading.UpdateResourceState) + _updateResourceStateHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.DecRef) @@ -54,8 +58,10 @@ public unsafe class ResourceService : IDisposable, IRequiredService { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); + _updateResourceStateHook.Dispose(); _incRefHook.Dispose(); _decRefHook.Dispose(); + _currentGetResourcePath.Dispose(); } #region GetResource @@ -112,28 +118,84 @@ public unsafe class ResourceService : IDisposable, IRequiredService unk9); } + var original = gamePath; ResourceHandle* returnValue = null; - ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, gamePath, pGetResParams, ref isSync, + ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9, original); } /// Call the original GetResource function. public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) - => sync - ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk8, unk9) - : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk, unk8, unk9); + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0, Utf8GamePath original = default) + { + if (original.Path is null) // i. e. if original is default + Utf8GamePath.FromByteString(path, out original); + var previous = _currentGetResourcePath.Value; + try + { + _currentGetResourcePath.Value = original; + return sync + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk8, unk9) + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk, unk8, unk9); + } finally + { + _currentGetResourcePath.Value = previous; + } + } #endregion private delegate nint ResourceHandlePrototype(ResourceHandle* handle); + #region UpdateResourceState + + /// Invoked before a resource state is updated. + /// The resource handle. + /// The original game path of the resource, if loaded synchronously. + public delegate void ResourceStateUpdatingDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal); + + /// Invoked after a resource state is updated. + /// The resource handle. + /// The original game path of the resource, if loaded synchronously. + /// The previous state of the resource. + /// The return value to use. + public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatingDelegate? ResourceStateUpdating; + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatedDelegate? ResourceStateUpdated; + + private delegate uint UpdateResourceStatePrototype(ResourceHandle* handle, byte offFileThread); + + [Signature(Sigs.UpdateResourceState, DetourName = nameof(UpdateResourceStateDetour))] + private readonly Hook _updateResourceStateHook = null!; + + private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) + { + var previousState = (handle->UnkState, handle->LoadState); + var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value! : Utf8GamePath.Empty; + ResourceStateUpdating?.Invoke(handle, syncOriginal); + var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); + ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); + return ret; + } + + #endregion + #region IncRef /// Invoked before a resource handle reference count is incremented. diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index bdb11752..0e04029b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -14,9 +14,12 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr SubfileHelper, - /// + /// ShaderReplacementFixer, + /// + ResourceLoader, + /// ResourceWatcher, } diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index ecf78c69..a27f6d45 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Processing; @@ -20,20 +21,23 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) { - _resourceLoader = resourceLoader; - _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.FileLoaded += OnFileLoaded; + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.BeforeResourceComplete += OnResourceComplete; } public void Dispose() { - _resourceLoader.FileLoaded -= OnFileLoaded; + _resourceLoader.BeforeResourceComplete -= OnResourceComplete; } - private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) + private void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + ReadOnlySpan additionalData, bool isAsync) { + if (resource->LoadState != LoadState.Success) + return; + if (_processors.TryGetValue(resource->FileType, out var processor)) - processor.PostProcess(resource, path, additionalData); + processor.PostProcess(resource, original.Path, additionalData); } } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 65550563..1558c035 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -24,10 +24,20 @@ public unsafe struct TextureResourceHandle public enum LoadState : byte { + Constructing = 0x00, + Constructed = 0x01, + Async2 = 0x02, + AsyncRequested = 0x03, + Async4 = 0x04, + AsyncLoading = 0x05, + Async6 = 0x06, Success = 0x07, - Async = 0x03, + Unknown8 = 0x08, Failure = 0x09, FailedSubResource = 0x0A, + FailureB = 0x0B, + FailureC = 0x0C, + FailureD = 0x0D, None = 0xFF, } @@ -74,6 +84,9 @@ public unsafe struct ResourceHandle [FieldOffset(0x58)] public int FileNameLength; + [FieldOffset(0xA8)] + public byte UnkState; + [FieldOffset(0xA9)] public LoadState LoadState; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 7338e5a9..13a71656 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -10,10 +10,11 @@ namespace Penumbra.UI.ResourceWatcher; [Flags] public enum RecordType : byte { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, + ResourceComplete = 0x10, } internal unsafe struct Record @@ -141,4 +142,24 @@ internal unsafe struct Record LoadState = handle->LoadState, Crc64 = 0, }; + + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceComplete, + Synchronously = false, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, + LoadState = handle->LoadState, + Crc64 = 0, + }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 0f72efff..53d7e79d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -47,9 +47,10 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; - _loader.PapRequested += OnPapRequested; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.ResourceComplete += OnResourceComplete; + _loader.FileLoaded += OnFileLoaded; + _loader.PapRequested += OnPapRequested; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } @@ -73,9 +74,10 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _records.TrimExcess(); _resources.ResourceRequested -= OnResourceRequested; _destructor.Unsubscribe(OnResourceDestroyed); - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; - _loader.PapRequested -= OnPapRequested; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.ResourceComplete -= OnResourceComplete; + _loader.FileLoaded -= OnFileLoaded; + _loader.PapRequested -= OnPapRequested; } private void Clear() @@ -255,6 +257,23 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan _, bool isAsync) + { + if (!isAsync) + return; + + if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) + Penumbra.Log.Information( + $"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}."); + + if (!_ephemeral.EnableResourceWatcher) + return; + + var record = Record.CreateResourceComplete(path, resource, original); + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 7ac3cb99..a58d74d1 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -124,11 +124,12 @@ internal sealed class ResourceWatcherTable : Table { ImGui.TextUnformatted(item.RecordType switch { - RecordType.Request => "REQ", - RecordType.ResourceLoad => "LOAD", - RecordType.FileLoad => "FILE", - RecordType.Destruction => "DEST", - _ => string.Empty, + RecordType.Request => "REQ", + RecordType.ResourceLoad => "LOAD", + RecordType.FileLoad => "FILE", + RecordType.Destruction => "DEST", + RecordType.ResourceComplete => "DONE", + _ => string.Empty, }); } } @@ -317,10 +318,10 @@ internal sealed class ResourceWatcherTable : Table { LoadState.None => FilterValue.HasFlag(LoadStateFlag.None), LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success), - LoadState.Async => FilterValue.HasFlag(LoadStateFlag.Async), - LoadState.Failure => FilterValue.HasFlag(LoadStateFlag.Failed), LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub), - _ => FilterValue.HasFlag(LoadStateFlag.Unknown), + <= LoadState.Constructed => FilterValue.HasFlag(LoadStateFlag.Unknown), + < LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Async), + > LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Failed), }; public override void DrawColumn(Record item, int _) @@ -332,12 +333,12 @@ internal sealed class ResourceWatcherTable : Table { LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), $"Successfully loaded ({(byte)item.LoadState})."), - LoadState.Async => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), - LoadState.Failure => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), - $"Failed to load ({(byte)item.LoadState})."), LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), $"Dependencies failed to load ({(byte)item.LoadState})."), - _ => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Unknown state ({(byte)item.LoadState})."), + <= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Not yet loaded ({(byte)item.LoadState})."), + < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), + > LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), + $"Failed to load ({(byte)item.LoadState})."), }; using (var font = ImRaii.PushFont(UiBuilder.IconFont)) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ad4824c3..5dc203c2 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -80,6 +80,7 @@ public class DebugTab : Window, ITab, IUiService private readonly StainService _stains; private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly ResourceManagerService _resourceManager; + private readonly ResourceLoader _resourceLoader; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly PathState _pathState; @@ -109,7 +110,7 @@ public class DebugTab : Window, ITab, IUiService public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, - ResourceManagerService resourceManager, CollectionResolver collectionResolver, + ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, @@ -133,6 +134,7 @@ public class DebugTab : Window, ITab, IUiService _actors = actors; _stains = stains; _resourceManager = resourceManager; + _resourceLoader = resourceLoader; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _pathState = pathState; @@ -191,6 +193,7 @@ public class DebugTab : Window, ITab, IUiService DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); + DrawResourceLoader(); DrawResourceProblems(); _renderTargetDrawer.Draw(); _hookOverrides.Draw(); @@ -1099,6 +1102,38 @@ public class DebugTab : Window, ITab, IUiService } } + private unsafe void DrawResourceLoader() + { + if (!ImGui.CollapsingHeader("Resource Loader")) + return; + + var ongoingLoads = _resourceLoader.OngoingLoads; + var ongoingLoadCount = ongoingLoads.Count; + ImUtf8.Text($"Ongoing Loads: {ongoingLoadCount}"); + + if (ongoingLoadCount == 0) + return; + + using var table = ImUtf8.Table("ongoingLoadTable"u8, 3); + if (!table) + return; + + ImUtf8.TableSetupColumn("Resource Handle"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Actual Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImUtf8.TableSetupColumn("Original Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (handle, original) in ongoingLoads) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"0x{handle:X}"); + ImGui.TableNextColumn(); + ImUtf8.Text(((ResourceHandle*)handle)->CsHandle.FileName); + ImGui.TableNextColumn(); + ImUtf8.Text(original.Path.Span); + } + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From 9ab8985343591de70204d16a0abe1d97ffc3955a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 12:38:10 +0100 Subject: [PATCH 2149/2451] Debug logging. --- Penumbra/Meta/Files/ImcFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 23339cfc..0a0faf1e 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -198,7 +198,7 @@ public unsafe class ImcFile : MetaBaseFile if (DebugConfiguration.WriteImcBytesToLog) { - Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}:"); + Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}, current handle state {resource->LoadState}:"); Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); Penumbra.Log.Information(new Span(Data, actualLength).WriteHexBytes()); Penumbra.Log.Information(new Span(Data, actualLength).WriteHexByteDiff(new Span((void*)data, length))); From 30a957356af4e9b959a1280a015de1a3a34672f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 13:54:19 +0100 Subject: [PATCH 2150/2451] Minor changes. --- .../Hooks/ResourceLoading/ResourceLoader.cs | 11 +++++------ .../Hooks/ResourceLoading/ResourceService.cs | 10 ++++------ .../Processing/FilePostProcessService.cs | 9 +++------ Penumbra/UI/ResourceWatcher/Record.cs | 17 +++++++++++++++-- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 4 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 11 ++++------- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index f9b8ff60..d5e41b56 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,4 +1,3 @@ -using System.IO; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -168,7 +167,7 @@ public unsafe class ResourceLoader : IDisposable, IService if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, original, parameters); TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; @@ -179,7 +178,7 @@ public unsafe class ResourceLoader : IDisposable, IService hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, original, parameters); TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -194,7 +193,7 @@ public unsafe class ResourceLoader : IDisposable, IService private void ResourceStateUpdatedHandler(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte, LoadState) previousState, ref uint returnValue) { - if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState.Item1 == 2 && previousState.Item2 >= LoadState.Success) + if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState is { Item1: 2, Item2: >= LoadState.Success }) return; if (!_ongoingLoads.TryRemove((nint)handle, out var asyncOriginal)) @@ -205,7 +204,7 @@ public unsafe class ResourceLoader : IDisposable, IService Penumbra.Log.Warning($"[ResourceLoader] Resource original paths inconsistency: 0x{(nint)handle:X}, of path {path}, sync original {syncOriginal}, async original {asyncOriginal}."); var original = !asyncOriginal.IsEmpty ? asyncOriginal : syncOriginal; - // Penumbra.Log.Information($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); + Penumbra.Log.Excessive($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) ResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); else @@ -223,7 +222,7 @@ public unsafe class ResourceLoader : IDisposable, IService var path = handle->CsHandle.FileName; var original = asyncOriginal.IsEmpty ? syncOriginal : asyncOriginal; - // Penumbra.Log.Information($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); + Penumbra.Log.Excessive($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) BeforeResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); else diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 238ed70f..e90b4575 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -125,15 +125,13 @@ public unsafe class ResourceService : IDisposable, IRequiredService if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9, original); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9); } /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0, Utf8GamePath original = default) + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original, + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) { - if (original.Path is null) // i. e. if original is default - Utf8GamePath.FromByteString(path, out original); var previous = _currentGetResourcePath.Value; try { @@ -187,7 +185,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) { var previousState = (handle->UnkState, handle->LoadState); - var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value! : Utf8GamePath.Empty; + var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; ResourceStateUpdating?.Invoke(handle, syncOriginal); var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index a27f6d45..71340178 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -23,20 +23,17 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable { _resourceLoader = resourceLoader; _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.BeforeResourceComplete += OnResourceComplete; + _resourceLoader.BeforeResourceComplete += OnBeforeResourceComplete; } public void Dispose() { - _resourceLoader.BeforeResourceComplete -= OnResourceComplete; + _resourceLoader.BeforeResourceComplete -= OnBeforeResourceComplete; } - private void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + private void OnBeforeResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) { - if (resource->LoadState != LoadState.Success) - return; - if (_processors.TryGetValue(resource->FileType, out var processor)) processor.PostProcess(resource, original.Path, additionalData); } diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 13a71656..8ab96f4b 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -143,11 +143,11 @@ internal unsafe struct Record Crc64 = 0, }; - public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath) + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan additionalData) => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), + Path = CombinedPath(path, additionalData), OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), Collection = null, Handle = handle, @@ -162,4 +162,17 @@ internal unsafe struct Record LoadState = handle->LoadState, Crc64 = 0, }; + + private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan additionalData) + { + if (additionalData.Length is 0) + return path.IsOwned ? path : path.Clone(); + + fixed (byte* ptr = additionalData) + { + // If a path has additional data and is split, it is always in the form of |{additionalData}|{path}, + // so we can just read from the start of additional data - 1 and sum their length +2 for the pipes. + return new CiByteString(new ReadOnlySpan(ptr - 1, additionalData.Length + 2 + path.Length)).Clone(); + } + } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 53d7e79d..94bd4307 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -257,7 +257,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan _, bool isAsync) + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) { if (!isAsync) return; @@ -269,7 +269,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService if (!_ephemeral.EnableResourceWatcher) return; - var record = Record.CreateResourceComplete(path, resource, original); + var record = Record.CreateResourceComplete(path, resource, original, additionalData); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5dc203c2..8f76a54a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1104,7 +1104,7 @@ public class DebugTab : Window, ITab, IUiService private unsafe void DrawResourceLoader() { - if (!ImGui.CollapsingHeader("Resource Loader")) + if (!ImUtf8.CollapsingHeader("Resource Loader"u8)) return; var ongoingLoads = _resourceLoader.OngoingLoads; @@ -1125,12 +1125,9 @@ public class DebugTab : Window, ITab, IUiService foreach (var (handle, original) in ongoingLoads) { - ImGui.TableNextColumn(); - ImUtf8.Text($"0x{handle:X}"); - ImGui.TableNextColumn(); - ImUtf8.Text(((ResourceHandle*)handle)->CsHandle.FileName); - ImGui.TableNextColumn(); - ImUtf8.Text(original.Path.Span); + ImUtf8.DrawTableColumn($"0x{handle:X}"); + ImUtf8.DrawTableColumn(((ResourceHandle*)handle)->CsHandle.FileName); + ImUtf8.DrawTableColumn(original.Path.Span); } } From 4d26a63944f5841198ac889dda08187d2863adbc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 13:55:13 +0100 Subject: [PATCH 2151/2451] Test disabling MtrlForceSync. --- .../Interop/Hooks/ResourceLoading/ResourceLoader.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index d5e41b56..3f8cb23f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -256,7 +256,6 @@ public unsafe class ResourceLoader : IDisposable, IService gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; - MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -295,16 +294,6 @@ public unsafe class ResourceLoader : IDisposable, IService } } - /// Special handling for materials. - private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) - { - // Force isSync = true for Materials. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. - isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl; - } - /// /// A resource with ref count 0 that gets incremented goes through GetResourceAsync again. /// This means, that if the path determined from that is different than the resources path, From ac64b4db24bcba33646dceb43e9aa304403135c8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 25 Jan 2025 13:01:04 +0000 Subject: [PATCH 2152/2451] [CI] Updating repo.json for testing_1.3.3.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0f61c85c..85ba406a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.9", + "TestingAssemblyVersion": "1.3.3.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b0a8b1baa5110d0aed944f462c7c13c7bcd2aac3 Mon Sep 17 00:00:00 2001 From: Theo <58579310+Theo-Asterio@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:11:46 -0800 Subject: [PATCH 2153/2451] Bone and Material Limit updates. Fix UI in Models tab to allow for more than 4 Materials per DT spec. --- Penumbra/Import/Models/Import/ModelImporter.cs | 10 +++++----- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index a141d754..5367e892 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -208,10 +208,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) if (index >= 0) return (ushort)index; - // If there's already 4 materials, we can't add any more. + // If there's already 10 materials, we can't add any more. // TODO: permit, with a warning to reduce, and validation in MdlTab. var count = _materials.Count; - if (count >= 4) + if (count >= 10) return 0; _materials.Add(materialName); @@ -234,10 +234,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) boneIndices.Add((ushort)boneIndex); } - if (boneIndices.Count > 64) - throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones."); + if (boneIndices.Count > 128) + throw notifier.Exception("XIV does not support meshes weighted to a total of more than 128 bones."); - var boneIndicesArray = new ushort[64]; + var boneIndicesArray = new ushort[128]; Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); var boneTableIndex = _boneTables.Count; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index de088736..bbf3dd00 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 4; + private const int MdlMaterialMaximum = 10; private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; @@ -93,7 +93,7 @@ public partial class ModEditWindow tab.Mdl.ConvertV5ToV6(); _modelTab.SaveFile(); - } + } private void DrawImportExport(MdlTab tab, bool disabled) { @@ -427,7 +427,7 @@ public partial class ModEditWindow private static void DrawInvalidMaterialMarker() { - using (ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); ImGuiUtil.HoverTooltip( From 64748790cc877f613833a3de9f366e2e21168d9d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 Jan 2025 14:06:39 +0100 Subject: [PATCH 2154/2451] Make limits a bit cleaner. --- Penumbra/Import/Models/Import/ModelImporter.cs | 14 ++++++++------ Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 5367e892..502d060a 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -8,6 +8,9 @@ namespace Penumbra.Import.Models.Import; public partial class ModelImporter(ModelRoot model, IoNotifier notifier) { + public const int BoneLimit = 128; + public const int MaterialLimit = 10; + public static MdlFile Import(ModelRoot model, IoNotifier notifier) { var importer = new ModelImporter(model, notifier); @@ -208,10 +211,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) if (index >= 0) return (ushort)index; - // If there's already 10 materials, we can't add any more. // TODO: permit, with a warning to reduce, and validation in MdlTab. var count = _materials.Count; - if (count >= 10) + if (count >= MaterialLimit) return 0; _materials.Add(materialName); @@ -234,11 +236,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) boneIndices.Add((ushort)boneIndex); } - if (boneIndices.Count > 128) - throw notifier.Exception("XIV does not support meshes weighted to a total of more than 128 bones."); + if (boneIndices.Count > BoneLimit) + throw notifier.Exception($"XIV does not support meshes weighted to a total of more than {BoneLimit} bones."); - var boneIndicesArray = new ushort[128]; - Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + var boneIndicesArray = new ushort[BoneLimit]; + boneIndices.CopyTo(boneIndicesArray); var boneTableIndex = _boneTables.Count; _boneTables.Add(new BoneTableStruct() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bbf3dd00..8fbe5a68 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -9,6 +9,7 @@ using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; +using Penumbra.Import.Models.Import; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -16,7 +17,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 10; + private const int MdlMaterialMaximum = ModelImporter.MaterialLimit; private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; From 7022b37043c5f02d29d0078a8d086c374d30aa4a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Jan 2025 15:31:05 +0100 Subject: [PATCH 2155/2451] Add some improved Mod Setting API. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 68 +++++++++++++++--- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + .../Api/IpcTester/ModSettingsIpcTester.cs | 72 +++++++++++++++---- 5 files changed, 120 insertions(+), 26 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index b4e716f8..35b25bef 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b4e716f86d94cd4d98d8f58e580ed5f619ea87ae +Subproject commit 35b25bef92e9b0be96c44c150a3df89d848d2658 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index b78523d3..fe9bf366 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -65,6 +65,16 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance) + { + var ret = GetCurrentModSettingsWithTemp(collectionId, modDirectory, modName, ignoreInheritance, true, 0); + if (ret.Item2 is null) + return (ret.Item1, null); + + return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4)); + } + + public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId, + string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return (PenumbraApiEc.ModMissing, null); @@ -72,17 +82,32 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Identity.Id == Guid.Empty - ? null - : ignoreInheritance - ? collection.GetOwnSettings(mod.Index) - : collection.GetInheritedSettings(mod.Index).Settings; - if (settings == null) + if (collection.Identity.Id == Guid.Empty) return (PenumbraApiEc.Success, null); - var (enabled, priority, dict) = settings.ConvertToShareable(mod); - return (PenumbraApiEc.Success, - (enabled, priority.Value, dict, collection.GetOwnSettings(mod.Index) is null)); + if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings) + return (PenumbraApiEc.Success, settings); + + return (PenumbraApiEc.Success, null); + } + + public (PenumbraApiEc, Dictionary>, bool, bool)>?) GetAllModSettings(Guid collectionId, + bool ignoreInheritance, bool ignoreTemporary, int key) + { + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); + + if (collection.Identity.Id == Guid.Empty) + return (PenumbraApiEc.Success, []); + + var ret = new Dictionary>, bool, bool)>(_modManager.Count); + foreach (var mod in _modManager) + { + if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings) + ret[mod.Identifier] = settings; + } + + return (PenumbraApiEc.Success, ret); } public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) @@ -206,6 +231,31 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (bool, int, Dictionary>, bool, bool)? GetCurrentSettings(ModCollection collection, Mod mod, + bool ignoreInheritance, bool ignoreTemporary, int key) + { + var settings = collection.Settings.Settings[mod.Index]; + if (!ignoreTemporary && settings.TempSettings is { } tempSettings && (tempSettings.Lock <= 0 || tempSettings.Lock == key)) + { + if (!tempSettings.ForceInherit) + return (tempSettings.Enabled, tempSettings.Priority.Value, tempSettings.ConvertToShareable(mod).Settings, + false, true); + if (!ignoreInheritance && collection.GetActualSettings(mod.Index).Settings is { } actualSettingsTemp) + return (actualSettingsTemp.Enabled, actualSettingsTemp.Priority.Value, + actualSettingsTemp.ConvertToShareable(mod).Settings, true, true); + } + + if (settings.Settings is { } ownSettings) + return (ownSettings.Enabled, ownSettings.Priority.Value, ownSettings.ConvertToShareable(mod).Settings, false, + false); + if (!ignoreInheritance && collection.GetInheritedSettings(mod.Index).Settings is { } actualSettings) + return (actualSettings.Enabled, actualSettings.Priority.Value, + actualSettings.ConvertToShareable(mod).Settings, true, false); + + return null; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 05c47644..cfc9d470 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 5); + => (5, 6); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index fc97290f..9733f82e 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -57,6 +57,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings), + IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings), IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index 23078576..c8eb8496 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -22,16 +23,20 @@ public class ModSettingsIpcTester : IUiService, IDisposable private bool _lastSettingChangeInherited; private DateTimeOffset _lastSettingChange; - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private Guid? _settingsCollection; - private string _settingsCollectionName = string.Empty; - private bool _settingsIgnoreInheritance; - private bool _settingsInherit; - private bool _settingsEnabled; - private int _settingsPriority; - private IReadOnlyDictionary? _availableSettings; - private Dictionary>? _currentSettings; + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private Guid? _settingsCollection; + private string _settingsCollectionName = string.Empty; + private bool _settingsIgnoreInheritance; + private bool _settingsIgnoreTemporary; + private int _settingsKey; + private bool _settingsInherit; + private bool _settingsTemporary; + private bool _settingsEnabled; + private int _settingsPriority; + private IReadOnlyDictionary? _availableSettings; + private Dictionary>? _currentSettings; + private Dictionary>, bool, bool)>? _allSettings; public ModSettingsIpcTester(IDalamudPluginInterface pi) { @@ -54,7 +59,9 @@ public class ModSettingsIpcTester : IUiService, IDisposable ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName); - ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance); + ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance); + ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary); + ImUtf8.InputScalar("Key"u8, ref _settingsKey); var collection = _settingsCollection.GetValueOrDefault(Guid.Empty); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); @@ -83,10 +90,11 @@ public class ModSettingsIpcTester : IUiService, IDisposable _lastSettingsError = ret.Item1; if (ret.Item1 == PenumbraApiEc.Success) { - _settingsEnabled = ret.Item2?.Item1 ?? false; - _settingsInherit = ret.Item2?.Item4 ?? true; - _settingsPriority = ret.Item2?.Item2 ?? 0; - _currentSettings = ret.Item2?.Item3; + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsTemporary = false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; } else { @@ -94,6 +102,40 @@ public class ModSettingsIpcTester : IUiService, IDisposable } } + IpcTester.DrawIntro(GetCurrentModSettingsWithTemp.Label, "Get Current Settings With Temp"); + if (ImGui.Button("Get##CurrentTemp")) + { + var ret = new GetCurrentModSettingsWithTemp(_pi) + .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey); + _lastSettingsError = ret.Item1; + if (ret.Item1 == PenumbraApiEc.Success) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsTemporary = ret.Item2?.Item5 ?? false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + IpcTester.DrawIntro(GetAllModSettings.Label, "Get All Mod Settings"); + if (ImGui.Button("Get##All")) + { + var ret = new GetAllModSettings(_pi).Invoke(collection, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey); + _lastSettingsError = ret.Item1; + _allSettings = ret.Item2; + } + + if (_allSettings != null) + { + ImGui.SameLine(); + ImUtf8.Text($"{_allSettings.Count} Mods"); + } + IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod"); ImGui.Checkbox("##inherit", ref _settingsInherit); ImGui.SameLine(); From ec09a7eb0ee1aafc061306c5cd4e72b1316ae83b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Jan 2025 18:46:17 +0100 Subject: [PATCH 2156/2451] Add initial cleaning functions, to be improved. --- .../Collections/Manager/CollectionStorage.cs | 12 ++- Penumbra/Services/CleanupService.cs | 74 +++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 31 +++++++- 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 Penumbra/Services/CleanupService.cs diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index e19acd35..de723729 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -194,12 +194,16 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer } /// Remove all settings for not currently-installed mods from the given collection. - public void CleanUnavailableSettings(ModCollection collection) + public int CleanUnavailableSettings(ModCollection collection) { - var any = collection.Settings.Unused.Count > 0; - ((Dictionary)collection.Settings.Unused).Clear(); - if (any) + var count = collection.Settings.Unused.Count; + if (count > 0) + { + ((Dictionary)collection.Settings.Unused).Clear(); _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + + return count; } /// Remove a specific setting for not currently-installed mods from the given collection. diff --git a/Penumbra/Services/CleanupService.cs b/Penumbra/Services/CleanupService.cs new file mode 100644 index 00000000..490c2407 --- /dev/null +++ b/Penumbra/Services/CleanupService.cs @@ -0,0 +1,74 @@ +using OtterGui.Services; +using Penumbra.Collections.Manager; +using Penumbra.Mods.Manager; + +namespace Penumbra.Services; + +public class CleanupService(SaveService saveService, ModManager mods, CollectionManager collections) : IService +{ + public void CleanUnusedLocalData() + { + var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); + foreach (var file in saveService.FileNames.LocalDataFiles.ToList()) + { + try + { + if (!file.Exists || usedFiles.Contains(file.FullName)) + continue; + + file.Delete(); + Penumbra.Log.Information($"[CleanupService] Deleted unused local data file {file.Name}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + } + } + } + + public void CleanBackupFiles() + { + foreach (var file in mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories)) + { + try + { + if (!file.Exists) + continue; + + file.Delete(); + Penumbra.Log.Information($"[CleanupService] Deleted group backup file {file.FullName}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); + } + } + + foreach (var file in Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories)) + { + try + { + if (!File.Exists(file)) + continue; + + File.Delete(file); + Penumbra.Log.Information($"[CleanupService] Deleted config backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + } + } + } + + public void CleanupAllUnusedSettings() + { + foreach (var collection in collections.Storage) + { + var count = collections.Storage.CleanUnavailableSettings(collection); + if (count > 0) + Penumbra.Log.Information( + $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c7f66859..e847b291 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -49,6 +49,7 @@ public class SettingsTab : ITab, IUiService private readonly CrashHandlerService _crashService; private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; + private readonly CleanupService _cleanupService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -60,7 +61,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService) { _pluginInterface = pluginInterface; _config = config; @@ -84,6 +85,7 @@ public class SettingsTab : ITab, IUiService _crashService = crashService; _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; + _cleanupService = cleanupService; } public void DrawHeader() @@ -789,9 +791,13 @@ public class SettingsTab : ITab, IUiService DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); + ImGui.Separator(); DrawReloadResourceButton(); DrawReloadFontsButton(); + ImGui.Separator(); + DrawCleanupButtons(); ImGui.NewLine(); + } private void DrawCrashHandler() @@ -982,6 +988,29 @@ public class SettingsTab : ITab, IUiService _fontReloader.Reload(); } + private void DrawCleanupButtons() + { + var enabled = _config.DeleteModModifier.IsActive(); + if (ImUtf8.ButtonEx("Clear Unused Local Mod Data Files"u8, + "Delete all local mod data files that do not correspond to currently installed mods."u8, default, !enabled)) + _cleanupService.CleanUnusedLocalData(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + + if (ImUtf8.ButtonEx("Clear Backup Files"u8, + "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, + default, !enabled)) + _cleanupService.CleanBackupFiles(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + + if (ImUtf8.ButtonEx("Clear All Unused Settings"u8, + "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, !enabled)) + _cleanupService.CleanupAllUnusedSettings(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to remove settings."); + } + /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. private void DrawWaitForPluginsReflection() { From 981c2bace4d32c404447a14c95e7129ff4212163 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 3 Feb 2025 00:20:22 +0100 Subject: [PATCH 2157/2451] Fix out-of-root path detection logic --- Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index f5659e7c..95627566 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -25,6 +25,8 @@ public class ResourceTreeFactory( PathState pathState, ModManager modManager) : IService { + private static readonly string ParentDirectoryPrefix = $"..{Path.DirectorySeparatorChar}"; + private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); @@ -159,7 +161,7 @@ public class ResourceTreeFactory( if (onlyWithinPath != null) { var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); - if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + if (relPath == ".." || relPath.StartsWith(ParentDirectoryPrefix) || Path.IsPathRooted(relPath)) return false; } From f9b163e7c51a63bf01a4242f4d25cef7cef9c74c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 3 Feb 2025 00:51:13 +0100 Subject: [PATCH 2158/2451] Add explanations on why paths are redacted --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 9 +++++++++ .../ResourceTree/ResourceTreeFactory.cs | 13 +++++++----- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 20 +++++++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 4fa13e1f..60cc48de 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -16,6 +16,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public PathStatus FullPathStatus; public string? ModName; public readonly WeakReference Mod = new(null!); public string? ModRelativePath; @@ -61,6 +62,7 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + FullPathStatus = other.FullPathStatus; ModName = other.ModName; Mod = other.Mod; ModRelativePath = other.ModRelativePath; @@ -100,4 +102,11 @@ public class ResourceNode : ICloneable public UiData PrependName(string prefix) => Name == null ? this : this with { Name = prefix + Name }; } + + public enum PathStatus : byte + { + Valid, + NonExistent, + External, + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 95627566..cb8be184 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -147,25 +147,28 @@ public class ResourceTreeFactory( { foreach (var node in tree.FlatNodes) { - if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPathStatus = GetPathStatus(node.FullPath, onlyWithinPath); + if (node.FullPathStatus != ResourceNode.PathStatus.Valid) node.FullPath = FullPath.Empty; } return; - static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) + static ResourceNode.PathStatus GetPathStatus(FullPath fullPath, string? onlyWithinPath) { if (!fullPath.IsRooted) - return true; + return ResourceNode.PathStatus.Valid; if (onlyWithinPath != null) { var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); if (relPath == ".." || relPath.StartsWith(ParentDirectoryPrefix) || Path.IsPathRooted(relPath)) - return false; + return ResourceNode.PathStatus.External; } - return fullPath.Exists; + return fullPath.Exists + ? ResourceNode.PathStatus.Valid + : ResourceNode.PathStatus.NonExistent; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 7bad64f9..3482f620 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -285,10 +285,10 @@ public class ResourceTreeViewer( } else { - ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, + ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip( - $"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); @@ -354,6 +354,22 @@ public class ResourceTreeViewer( } } + private static ReadOnlySpan GetPathStatusLabel(ResourceNode.PathStatus status) + => status switch + { + ResourceNode.PathStatus.External => "(managed by external tools)"u8, + ResourceNode.PathStatus.NonExistent => "(not found)"u8, + _ => "(unavailable)"u8, + }; + + private static string GetPathStatusDescription(ResourceNode.PathStatus status) + => status switch + { + ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.", + ResourceNode.PathStatus.NonExistent => "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.", + _ => "The actual path to this file is unavailable.", + }; + [Flags] private enum TreeCategory : uint { From 4cc5041f0a04dc1066bc00917ce003dd3569b49e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Feb 2025 17:43:44 +0100 Subject: [PATCH 2159/2451] Improve cleanup. --- Penumbra/Services/CleanupService.cs | 175 +++++++++++++++++++++------- Penumbra/UI/Tabs/SettingsTab.cs | 28 +++-- 2 files changed, 154 insertions(+), 49 deletions(-) diff --git a/Penumbra/Services/CleanupService.cs b/Penumbra/Services/CleanupService.cs index 490c2407..bf76f5f0 100644 --- a/Penumbra/Services/CleanupService.cs +++ b/Penumbra/Services/CleanupService.cs @@ -6,69 +6,160 @@ namespace Penumbra.Services; public class CleanupService(SaveService saveService, ModManager mods, CollectionManager collections) : IService { + private CancellationTokenSource _cancel = new(); + private Task? _task; + + public double Progress { get; private set; } + + public bool IsRunning + => _task is { IsCompleted: false }; + + public void Cancel() + => _cancel.Cancel(); + public void CleanUnusedLocalData() { - var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); - foreach (var file in saveService.FileNames.LocalDataFiles.ToList()) - { - try - { - if (!file.Exists || usedFiles.Contains(file.FullName)) - continue; + if (IsRunning) + return; - file.Delete(); - Penumbra.Log.Information($"[CleanupService] Deleted unused local data file {file.Name}."); - } - catch (Exception ex) + var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); + Progress = 0; + var deleted = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => + { + var localFiles = saveService.FileNames.LocalDataFiles.ToList(); + var step = 0.9 / localFiles.Count; + Progress = 0.1; + foreach (var file in localFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + if (_cancel.IsCancellationRequested) + break; + + try + { + if (!file.Exists || usedFiles.Contains(file.FullName)) + continue; + + file.Delete(); + Penumbra.Log.Debug($"[CleanupService] Deleted unused local data file {file.Name}."); + ++deleted; + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + } + + Progress += step; } - } + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} unused local data files."); + Progress = 1; + }); } public void CleanBackupFiles() { - foreach (var file in mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories)) + if (IsRunning) + return; + + Progress = 0; + var deleted = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => { - try - { - if (!file.Exists) - continue; + var configFiles = Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories) + .ToList(); + Progress = 0.1; + if (_cancel.IsCancellationRequested) + return; - file.Delete(); - Penumbra.Log.Information($"[CleanupService] Deleted group backup file {file.FullName}."); - } - catch (Exception ex) + var groupFiles = mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories).ToList(); + Progress = 0.5; + var step = 0.4 / (groupFiles.Count + configFiles.Count); + foreach (var file in groupFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); - } - } + if (_cancel.IsCancellationRequested) + break; - foreach (var file in Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories)) - { - try - { - if (!File.Exists(file)) - continue; + try + { + if (!file.Exists) + continue; - File.Delete(file); - Penumbra.Log.Information($"[CleanupService] Deleted config backup file {file}."); + file.Delete(); + ++deleted; + Penumbra.Log.Debug($"[CleanupService] Deleted group backup file {file.FullName}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); + } + + Progress += step; } - catch (Exception ex) + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} group backup files."); + + deleted = 0; + foreach (var file in configFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + if (_cancel.IsCancellationRequested) + break; + + try + { + if (!File.Exists(file)) + continue; + + File.Delete(file); + ++deleted; + Penumbra.Log.Debug($"[CleanupService] Deleted config backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + } + + Progress += step; } - } + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} config backup files."); + Progress = 1; + }); } public void CleanupAllUnusedSettings() { - foreach (var collection in collections.Storage) + if (IsRunning) + return; + + Progress = 0; + var totalRemoved = 0; + var diffCollections = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => { - var count = collections.Storage.CleanUnavailableSettings(collection); - if (count > 0) - Penumbra.Log.Information( - $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); - } + var step = 1.0 / collections.Storage.Count; + foreach (var collection in collections.Storage) + { + if (_cancel.IsCancellationRequested) + break; + + var count = collections.Storage.CleanUnavailableSettings(collection); + if (count > 0) + { + Penumbra.Log.Debug( + $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); + totalRemoved += count; + ++diffCollections; + } + + Progress += step; + } + + Penumbra.Log.Information($"[CleanupService] Removed {totalRemoved} unused settings from {diffCollections} separate collections."); + Progress = 1; + }); } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index e847b291..9637adeb 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -797,7 +797,6 @@ public class SettingsTab : ITab, IUiService ImGui.Separator(); DrawCleanupButtons(); ImGui.NewLine(); - } private void DrawCrashHandler() @@ -991,24 +990,39 @@ public class SettingsTab : ITab, IUiService private void DrawCleanupButtons() { var enabled = _config.DeleteModModifier.IsActive(); + if (_cleanupService.Progress is not 0.0 and not 1.0) + { + ImUtf8.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetFrameHeight()), + $"{_cleanupService.Progress * 100}%"); + ImGui.SameLine(); + if (ImUtf8.Button("Cancel##FileCleanup"u8)) + _cleanupService.Cancel(); + } + else + { + ImGui.NewLine(); + } + if (ImUtf8.ButtonEx("Clear Unused Local Mod Data Files"u8, - "Delete all local mod data files that do not correspond to currently installed mods."u8, default, !enabled)) + "Delete all local mod data files that do not correspond to currently installed mods."u8, default, + !enabled || _cleanupService.IsRunning)) _cleanupService.CleanUnusedLocalData(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); if (ImUtf8.ButtonEx("Clear Backup Files"u8, "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, - default, !enabled)) + default, !enabled || _cleanupService.IsRunning)) _cleanupService.CleanBackupFiles(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); if (ImUtf8.ButtonEx("Clear All Unused Settings"u8, - "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, !enabled)) + "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, + !enabled || _cleanupService.IsRunning)) _cleanupService.CleanupAllUnusedSettings(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to remove settings."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to remove settings."); } /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. From f9952ada75e30a62835d4aa71c1f00e8002f11db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Feb 2025 16:19:57 +0100 Subject: [PATCH 2160/2451] 1.3.4.0 --- Penumbra/UI/Changelog.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index f83c8989..993ace62 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -57,10 +57,32 @@ public class PenumbraChangelog : IUiService Add1_3_1_0(Changelog); Add1_3_2_0(Changelog); Add1_3_3_0(Changelog); + Add1_3_4_0(Changelog); } #region Changelogs + private static void Add1_3_4_0(Changelog log) + => log.NextVersion("Version 1.3.4.0") + .RegisterHighlight("Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") + .RegisterEntry("This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", 1) + .RegisterHighlight("Added a new option group type: Combining Groups.") + .RegisterEntry("A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", 1) + .RegisterEntry("Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", 1) + .RegisterEntry("Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") + .RegisterEntry("Added a display of the number of selected files and folders to the multi mod selection.") + .RegisterEntry("Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") + .RegisterEntry("Updated the Bone and Material limits in the Model Importer.") + .RegisterEntry("Improved handling of IMC and Material files loaded asynchronously.") + .RegisterEntry("Added IPC functionality to query temporary settings.") + .RegisterEntry("Improved some mod setting IPC functions.") + .RegisterEntry("Fixed some path detection issues in the OnScreen tab.") + .RegisterEntry("Fixed some issues with temporary mod settings.") + .RegisterEntry("Fixed issues with IPC calls before the game has finished loading.") + .RegisterEntry("Fixed using the wrong dye channel in the material editor previews.") + .RegisterEntry("Added some log warnings if outdated materials are loaded by the game.") + .RegisterEntry("Added Schemas for some of the json files generated and read by Penumbra to the solution."); + private static void Add1_3_3_0(Changelog log) => log.NextVersion("Version 1.3.3.0") .RegisterHighlight("Added Temporary Settings to collections.") From 214be98662267915a5fa553b6ed2b159cdc0c597 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 6 Feb 2025 15:55:30 +0000 Subject: [PATCH 2161/2451] [CI] Updating repo.json for 1.3.4.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 85ba406a..9ee5b00d 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.10", + "AssemblyVersion": "1.3.4.0", + "TestingAssemblyVersion": "1.3.4.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9b18ffce66d26211e9af57d8afe802d6456b0aea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Feb 2025 16:58:41 +0100 Subject: [PATCH 2162/2451] Updated submodule Versions. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 35b25bef..c6780905 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 35b25bef92e9b0be96c44c150a3df89d848d2658 +Subproject commit c67809057fac73a0fd407e3ad567f0aa6bc0bc37 diff --git a/Penumbra.String b/Penumbra.String index 0bc2b0f6..4eb7c118 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0bc2b0f66eee1a02c9575b2bb30f27ce166f8632 +Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7 From 50c42078444659cc79157df572b40189d4847913 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Feb 2025 15:12:34 +0100 Subject: [PATCH 2163/2451] Give messages for unsupported file redirection types. --- Penumbra/Collections/Cache/CollectionCache.cs | 30 ++++++++- .../Cache/CollectionCacheManager.cs | 3 +- .../Interop/PathResolving/PathResolver.cs | 66 +++++++++---------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 8ca9aa36..3f0ed27b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; @@ -274,6 +275,24 @@ public sealed class CollectionCache : IDisposable _manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod); } + private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod) + { + var ext = path.Extension().AsciiToLower().ToString(); + switch (ext) + { + case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc": + Penumbra.Messager.NotificationMessage( + $"Redirection of {ext} files for {mod.Name} is unsupported. Please use the corresponding meta manipulations instead.", + NotificationType.Warning); + return false; + case ".lvb" or ".lgb" or ".sgb": + Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.", + NotificationType.Warning); + return false; + default: return true; + } + } + // Add a specific file redirection, handling potential conflicts. // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. @@ -283,6 +302,9 @@ public sealed class CollectionCache : IDisposable if (!CheckFullPath(path, file)) return; + if (!IsRedirectionSupported(path, mod)) + return; + try { if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) @@ -342,8 +364,9 @@ public sealed class CollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; + var existingPriority = + existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) { @@ -427,7 +450,8 @@ public sealed class CollectionCache : IDisposable if (!_changedItems.TryGetValue(name, out var data)) _changedItems.Add(name, (new SingleArray(mod), obj)); else if (!data.Item1.Contains(mod)) - _changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); + _changedItems[name] = (data.Item1.Append(mod), + obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y) _changedItems[name] = (data.Item1, x + y); } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c46759c7..ec48e608 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -171,8 +171,7 @@ public class CollectionCacheManager : IDisposable, IService try { ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty, - FullPath.Empty, - null); + FullPath.Empty, null); cache.ResolvedFiles.Clear(); cache.Meta.Reset(); cache.ConflictDict.Clear(); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 0b6c8340..8e5504d5 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -49,42 +49,38 @@ public class PathResolver : IDisposable, IService if (!_config.EnableMods) return (null, ResolveData.Invalid); - // Do not allow manipulating layers to prevent very obvious cheating and softlocks. - if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) - return (null, ResolveData.Invalid); - - // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. - if (resourceType is ResourceType.Atch) - return ResolveAtch(path); - - return category switch + return resourceType switch { - // Only Interface collection. - ResourceCategory.Ui => ResolveUi(path), - // Never allow changing scripts. - ResourceCategory.UiScript => (null, ResolveData.Invalid), - ResourceCategory.GameScript => (null, ResolveData.Invalid), - // Use actual resolving. - ResourceCategory.Chara => Resolve(path, resourceType), - ResourceCategory.Shader => ResolveShader(path, resourceType), - ResourceCategory.Vfx => Resolve(path, resourceType), - ResourceCategory.Sound => Resolve(path, resourceType), - // EXD Modding in general should probably be prohibited but is currently used for fan translations. - // We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code. - ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) - ? (null, ResolveData.Invalid) - : DefaultResolver(path), - // None of these files are ever associated with specific characters, - // always use the default resolver for now, - // except that common/font is conceptually more UI. - ResourceCategory.Common => path.Path.StartsWith("common/font"u8) - ? ResolveUi(path) - : DefaultResolver(path), - ResourceCategory.BgCommon => DefaultResolver(path), - ResourceCategory.Bg => DefaultResolver(path), - ResourceCategory.Cut => DefaultResolver(path), - ResourceCategory.Music => DefaultResolver(path), - _ => DefaultResolver(path), + // Do not allow manipulating layers to prevent very obvious cheating and softlocks. + ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb => (null, ResolveData.Invalid), + // Prevent .atch loading to prevent crashes on outdated .atch files. + ResourceType.Atch => ResolveAtch(path), + + _ => category switch + { + // Only Interface collection. + ResourceCategory.Ui => ResolveUi(path), + // Never allow changing scripts. + ResourceCategory.UiScript => (null, ResolveData.Invalid), + ResourceCategory.GameScript => (null, ResolveData.Invalid), + // Use actual resolving. + ResourceCategory.Chara => Resolve(path, resourceType), + ResourceCategory.Shader => ResolveShader(path, resourceType), + ResourceCategory.Vfx => Resolve(path, resourceType), + ResourceCategory.Sound => Resolve(path, resourceType), + // EXD Modding in general should probably be prohibited but is currently used for fan translations. + // We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code. + ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) ? (null, ResolveData.Invalid) : DefaultResolver(path), + // None of these files are ever associated with specific characters, + // always use the default resolver for now, + // except that common/font is conceptually more UI. + ResourceCategory.Common => path.Path.StartsWith("common/font"u8) ? ResolveUi(path) : DefaultResolver(path), + ResourceCategory.BgCommon => DefaultResolver(path), + ResourceCategory.Bg => DefaultResolver(path), + ResourceCategory.Cut => DefaultResolver(path), + ResourceCategory.Music => DefaultResolver(path), + _ => DefaultResolver(path), + } }; } From 60b9facea31c871d5951791ae8bffbc9ee9337fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Feb 2025 15:27:32 +0100 Subject: [PATCH 2164/2451] Cont. --- Penumbra/Interop/PathResolving/PathResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 8e5504d5..ec421304 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,3 @@ -using System.Linq; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -55,6 +54,8 @@ public class PathResolver : IDisposable, IService ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb => (null, ResolveData.Invalid), // Prevent .atch loading to prevent crashes on outdated .atch files. ResourceType.Atch => ResolveAtch(path), + // These are manipulated through Meta Edits instead. + ResourceType.Eqp or ResourceType.Eqdp or ResourceType.Est or ResourceType.Gmp or ResourceType.Cmp => (null, ResolveData.Invalid), _ => category switch { From 0af9667789d763a6c3a512a605d41b77925e2b1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Feb 2025 16:44:22 +0100 Subject: [PATCH 2165/2451] Add changed item adapters. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 6 ++ Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + Penumbra/Mods/Manager/ModCacheManager.cs | 2 +- .../Mods/Manager/ModChangedItemAdapter.cs | 102 ++++++++++++++++++ 6 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModChangedItemAdapter.cs diff --git a/Penumbra.Api b/Penumbra.Api index c6780905..7ae46f0d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c67809057fac73a0fd407e3ad567f0aa6bc0bc37 +Subproject commit 7ae46f0d09f40b36a5b2d10382db46fbfb729117 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 64e201be..ace98f83 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -145,4 +145,10 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable => _modManager.TryGetMod(modDirectory, modName, out var mod) ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject()) : []; + + public IReadOnlyDictionary> GetChangedItemAdapterDictionary() + => new ModChangedItemAdapter(new WeakReference(_modManager)); + + public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> GetChangedItemAdapterList() + => new ModChangedItemAdapter(new WeakReference(_modManager)); } diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index cfc9d470..36f799a0 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 6); + => (5, 7); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 9733f82e..085e57ca 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItemAdapterDictionary.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItemAdapterList.Provider(pi, api.Mods), IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 38d98d7c..4bf22272 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -15,7 +15,7 @@ public class ModCacheManager : IDisposable, IService private readonly CommunicatorService _communicator; private readonly ObjectIdentification _identifier; private readonly ModStorage _modManager; - private bool _updatingItems = false; + private bool _updatingItems; public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config) { diff --git a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs b/Penumbra/Mods/Manager/ModChangedItemAdapter.cs new file mode 100644 index 00000000..8b99cdf2 --- /dev/null +++ b/Penumbra/Mods/Manager/ModChangedItemAdapter.cs @@ -0,0 +1,102 @@ +using Penumbra.GameData.Data; + +namespace Penumbra.Mods.Manager; + +public sealed class ModChangedItemAdapter(WeakReference storage) + : IReadOnlyDictionary>, + IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> +{ + IEnumerator<(string ModDirectory, IReadOnlyDictionary ChangedItems)> + IEnumerable<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.GetEnumerator() + => Storage.Select(m => (m.Identifier, (IReadOnlyDictionary)new ChangedItemDictionaryAdapter(m.ChangedItems))) + .GetEnumerator(); + + public IEnumerator>> GetEnumerator() + => Storage.Select(m => new KeyValuePair>(m.Identifier, + new ChangedItemDictionaryAdapter(m.ChangedItems))) + .GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => Storage.Count; + + public bool ContainsKey(string key) + => Storage.TryGetMod(key, string.Empty, out _); + + public bool TryGetValue(string key, [NotNullWhen(true)] out IReadOnlyDictionary? value) + { + if (Storage.TryGetMod(key, string.Empty, out var mod)) + { + value = new ChangedItemDictionaryAdapter(mod.ChangedItems); + return true; + } + + value = null; + return false; + } + + public IReadOnlyDictionary this[string key] + => TryGetValue(key, out var v) ? v : throw new KeyNotFoundException(); + + (string ModDirectory, IReadOnlyDictionary ChangedItems) + IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.this[int index] + { + get + { + var m = Storage[index]; + return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems)); + } + } + + public IEnumerable Keys + => Storage.Select(m => m.Identifier); + + public IEnumerable> Values + => Storage.Select(m => new ChangedItemDictionaryAdapter(m.ChangedItems)); + + private ModStorage Storage + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => storage.TryGetTarget(out var t) + ? t + : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); + } + + private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary + { + public IEnumerator> GetEnumerator() + => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => data.Count; + + public bool ContainsKey(string key) + => data.ContainsKey(key); + + public bool TryGetValue(string key, out object? value) + { + if (data.TryGetValue(key, out var v)) + { + value = v?.ToInternalObject(); + return true; + } + + value = null; + return false; + } + + public object? this[string key] + => data[key]?.ToInternalObject(); + + public IEnumerable Keys + => data.Keys; + + public IEnumerable Values + => data.Values.Select(v => v?.ToInternalObject()); + } +} From a9a556eb55a2ed6c93899eb47926663301da2066 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Feb 2025 17:56:01 +0100 Subject: [PATCH 2166/2451] Add CheckCurrentChangedItemFunc, --- Penumbra.Api | 2 +- Penumbra/Api/Api/CollectionApi.cs | 22 +++++++++++++++++-- Penumbra/Api/IpcProviders.cs | 1 + .../Manager => Api}/ModChangedItemAdapter.cs | 3 ++- 4 files changed, 24 insertions(+), 4 deletions(-) rename Penumbra/{Mods/Manager => Api}/ModChangedItemAdapter.cs (95%) diff --git a/Penumbra.Api b/Penumbra.Api index 7ae46f0d..70f04683 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 7ae46f0d09f40b36a5b2d10382db46fbfb729117 +Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index 964da1a5..c40feb12 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -2,6 +2,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Mods; namespace Penumbra.Api.Api; @@ -23,11 +24,27 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : .Select(c => (c.Identity.Id, c.Identity.Name))); list.AddRange(collections.Storage - .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Identity.Id, c.Identity.Name))) + .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) + && !list.Contains((c.Identity.Id, c.Identity.Name))) .Select(c => (c.Identity.Id, c.Identity.Name))); return list; } + public Func CheckCurrentChangedItemFunc() + { + var weakRef = new WeakReference(collections); + return s => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying collection storage of this IPC container was disposed."); + + if (!c.Active.Current.ChangedItems.TryGetValue(s, out var d)) + return []; + + return d.Item1.Select(m => (m is Mod mod ? mod.Identifier : string.Empty, m.Name.Text)).ToArray(); + }; + } + public Dictionary GetChangedItemsForCollection(Guid collectionId) { try @@ -74,7 +91,8 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : } public Guid[] GetCollectionByName(string name) - => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id).ToArray(); + => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id) + .ToArray(); public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, bool allowCreateNew, bool allowDelete) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 085e57ca..d54faa6c 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -29,6 +29,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), IpcSubscribers.SetCollection.Provider(pi, api.Collection), IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection), + IpcSubscribers.CheckCurrentChangedItemFunc.Provider(pi, api.Collection), IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing), IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing), diff --git a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs similarity index 95% rename from Penumbra/Mods/Manager/ModChangedItemAdapter.cs rename to Penumbra/Api/ModChangedItemAdapter.cs index 8b99cdf2..8842f20a 100644 --- a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs +++ b/Penumbra/Api/ModChangedItemAdapter.cs @@ -1,6 +1,7 @@ using Penumbra.GameData.Data; +using Penumbra.Mods.Manager; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Api; public sealed class ModChangedItemAdapter(WeakReference storage) : IReadOnlyDictionary>, From f89eab8b2bd2b08c48b6608b2b62e042d610f3cc Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 13 Feb 2025 15:42:58 +0000 Subject: [PATCH 2167/2451] [CI] Updating repo.json for testing_1.3.4.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9ee5b00d..c5402b49 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.0", + "TestingAssemblyVersion": "1.3.4.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2be5bd06117caa208c51c27a68550a773075abf6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:45:46 +0100 Subject: [PATCH 2168/2451] Make EQP swaps also swap multi-slot items correctly. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 134 +++++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 10 +- 4 files changed, 93 insertions(+), 57 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 4a987167..f6dff467 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19 +Subproject commit f6dff467c7dad6b1213a7d7b65d40a56450f0672 diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index c7e43a26..8c80c91c 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -27,31 +27,31 @@ public static class EquipmentSwap : []; } - public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, + public static HashSet CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, MetaDictionary manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); var mtrlVariantTo = imcEntry.MaterialId; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -88,30 +88,40 @@ public static class EquipmentSwap return affectedItems; } - public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, + public static HashSet CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, MetaDictionary manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); - var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo); + HashSet affectedItems = []; + var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo); if (eqp != null) + { swaps.Add(eqp); + // Add items affected through multi-slot EQP edits. + foreach (var child in eqp.ChildSwaps.SelectMany(c => c.WithChildren()).OfType>()) + { + affectedItems.UnionWith(identifier + .Identify(GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) + .Select(kvp => kvp.Value).OfType().Select(i => i.Item)); + } + } var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo); if (gmp != null) swaps.Add(gmp); - var affectedItems = Array.Empty(); foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { - (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); + var (imcFileFrom, variants, affectedItemsLocal) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); + affectedItems.UnionWith(affectedItemsLocal); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); @@ -122,18 +132,18 @@ public static class EquipmentSwap { EquipSlot.Head => EstType.Head, EquipSlot.Body => EstType.Body, - _ => (EstType)0, + _ => (EstType)0, }; var skipFemale = false; - var skipMale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -148,7 +158,7 @@ public static class EquipmentSwap swaps.Add(eqdp); var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; - var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); } @@ -176,6 +186,7 @@ public static class EquipmentSwap return affectedItems; } + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); @@ -185,9 +196,9 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); - var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); - var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, eqdpFromDefault, eqdpToIdentifier, eqdpToDefault); @@ -216,7 +227,7 @@ public static class EquipmentSwap ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -238,16 +249,17 @@ public static class EquipmentSwap variant = i.Variant; } - private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, + private static (ImcFile, Variant[], HashSet) GetVariants(MetaFileManager manager, ObjectIdentification identifier, + EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var imc = new ImcFile(manager, ident); - EquipItem[] items; - Variant[] variants; + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); + HashSet items; + Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToHashSet(); variants = [variantFrom]; } else @@ -256,7 +268,7 @@ public static class EquipmentSwap ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item) - .ToArray(); + .ToHashSet(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } @@ -270,9 +282,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new GmpIdentifier(idFrom); - var manipToIdentifier = new GmpIdentifier(idTo); - var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -287,9 +299,9 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); @@ -328,7 +340,7 @@ public static class EquipmentSwap var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { @@ -339,18 +351,42 @@ public static class EquipmentSwap return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, - EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, EquipSlot slot, + PrimaryId idFrom, PrimaryId idTo) { if (slot.IsAccessory()) return null; var manipFromIdentifier = new EqpIdentifier(idFrom, slot); - var manipToIdentifier = new EqpIdentifier(idTo, slot); - var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); - var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); - return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + var swap = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var entry = swap.SwapToModdedEntry.ToEntry(slot); + // Add additional EQP entries if the swapped item is a multi-slot item, + // because those take the EQP entries of their other model-set slots when used. + switch (slot) + { + case EquipSlot.Body: + if (!entry.HasFlag(EqpEntry.BodyShowLeg) + && CreateEqp(manager, manips, EquipSlot.Legs, idFrom, idTo) is { } legChild) + swap.ChildSwaps.Add(legChild); + if (!entry.HasFlag(EqpEntry.BodyShowHead) + && CreateEqp(manager, manips, EquipSlot.Head, idFrom, idTo) is { } headChild) + swap.ChildSwaps.Add(headChild); + if (!entry.HasFlag(EqpEntry.BodyShowHand) + && CreateEqp(manager, manips, EquipSlot.Hands, idFrom, idTo) is { } handChild) + swap.ChildSwaps.Add(handChild); + break; + case EquipSlot.Legs: + if (!entry.HasFlag(EqpEntry.LegsShowFoot) + && CreateEqp(manager, manips, EquipSlot.Feet, idFrom, idTo) is { } footChild) + swap.ChildSwaps.Add(footChild); + break; + } + + return swap; } public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, @@ -380,7 +416,7 @@ public static class EquipmentSwap if (newFileName != fileName) { - fileName = newFileName; + fileName = newFileName; dataWasChanged = true; } @@ -405,13 +441,13 @@ public static class EquipmentSwap EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } @@ -429,8 +465,8 @@ public static class EquipmentSwap PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); - filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index d2deb9ef..a9d5e0d6 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -127,7 +127,7 @@ public class ItemSwapContainer ? new MetaDictionary(cache) : _appliedModData.Manipulations; - public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, + public HashSet LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true) { Swaps.Clear(); @@ -138,7 +138,7 @@ public class ItemSwapContainer return ret; } - public EquipItem[] LoadTypeSwap(EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null) + public HashSet LoadTypeSwap(EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null) { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index e590eb1e..cb56de08 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -180,7 +180,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private bool _useLeftRing = true; private bool _useRightRing = true; - private EquipItem[]? _affectedItems; + private HashSet? _affectedItems; private void UpdateState() { @@ -541,11 +541,11 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); - if (_affectedItems is not { Length: > 1 }) + if (_affectedItems is not { Count: > 1 }) return; ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) @@ -602,11 +602,11 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if (_affectedItems is not { Length: > 1 }) + if (_affectedItems is not { Count: > 1 }) return; ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) From 93e184c9a564302dde5434369028033dae037eb3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:46:03 +0100 Subject: [PATCH 2169/2451] Add import of .atch files into metadata. --- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 45 +++++++++++++++++-- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 25 ++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 4cf01faa..66db0932 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -1,11 +1,14 @@ using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; @@ -13,6 +16,7 @@ using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; namespace Penumbra.UI.AdvancedWindow.Meta; @@ -27,9 +31,9 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer public override float ColumnHeight => 2 * ImUtf8.FrameHeightSpacing; - private AtchFile? _currentBaseAtchFile; - private AtchPoint? _currentBaseAtchPoint; - private AtchPointCombo _combo; + private AtchFile? _currentBaseAtchFile; + private AtchPoint? _currentBaseAtchPoint; + private readonly AtchPointCombo _combo; public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : base(editor, metaFiles) @@ -44,6 +48,41 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer => obj.ToName(); } + public void ImportFile(string filePath) + { + try + { + if (filePath.Length == 0 || !File.Exists(filePath)) + throw new FileNotFoundException(); + + var gr = GamePaths.ParseRaceCode(filePath); + if (gr is GenderRace.Unknown) + throw new Exception($"Could not identify race code from path {filePath}."); + var text = File.ReadAllBytes(filePath); + var file = new AtchFile(text); + foreach (var point in file.Points) + { + foreach (var (entry, index) in point.Entries.WithIndex()) + { + var identifier = new AtchIdentifier(point.Type, gr, (ushort) index); + var defaultValue = AtchCache.GetDefault(MetaFiles, identifier); + if (defaultValue == null) + continue; + + if (defaultValue.Value.Equals(entry)) + Editor.Changes |= Editor.Remove(identifier); + else + Editor.Changes |= Editor.TryAdd(identifier, entry) || Editor.Update(identifier, entry); + } + } + } + catch (Exception ex) + { + Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", + NotificationType.Warning)); + } + } + protected override void DrawNew() { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 7d688df9..1356340c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -4,6 +4,8 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Api; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; @@ -43,8 +45,10 @@ public partial class ModEditWindow if (ImUtf8.Button("Write as TexTools Files"u8)) _metaFileManager.WriteAllTexToolsMeta(Mod!); ImGui.SameLine(); - if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8)) + if (ImUtf8.ButtonEx("Remove All Default-Values"u8, "Delete any entries from all lists that set the value to its default value."u8)) _editor.MetaEditor.DeleteDefaultValues(); + ImGui.SameLine(); + DrawAtchDragDrop(); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) @@ -60,6 +64,25 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.GlobalEqp); } + private void DrawAtchDragDrop() + { + _dragDropManager.CreateImGuiSource("atchDrag", f => f.Extensions.Contains(".atch"), f => + { + var gr = GamePaths.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); + if (gr is GenderRace.Unknown) + return false; + + ImUtf8.Text($"Dragging .atch for {gr.ToName()}..."); + return true; + }); + ImUtf8.ButtonEx("Import .atch"u8, + _dragDropManager.IsDragging ? ""u8 : "Drag a .atch file containinig its race code in the path here to import its values."u8, + default, + !_dragDropManager.IsDragging); + if (_dragDropManager.CreateImGuiTarget("atchDrag", out var files, out _) && files.FirstOrDefault() is { } file) + _metaDrawers.Atch.ImportFile(file); + } + private void DrawEditHeader(MetaManipulationType type) { var drawer = _metaDrawers.Get(type); From 40f24344af122eb4bb8bc3e0d5afdcd9c3395477 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:46:31 +0100 Subject: [PATCH 2170/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3c1260c9..0b6085ce 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3c1260c9833303c2d33d12d6f77dc2b1afea3f34 +Subproject commit 0b6085ce720ffb7c78cf42d4e51861f34db27744 From 79938b6dd01a914cb13dcd833c3f2d97de0372da Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 15 Feb 2025 15:50:18 +0000 Subject: [PATCH 2171/2451] [CI] Updating repo.json for testing_1.3.4.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c5402b49..fdb0b638 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.1", + "TestingAssemblyVersion": "1.3.4.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b7b9defaa6f8d861841cb134dfbb3ef161c24bda Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Feb 2025 17:38:42 +0100 Subject: [PATCH 2172/2451] Add context menu to clear temporary settings. --- Penumbra/Api/Api/TemporaryApi.cs | 14 +++----------- Penumbra/Collections/Manager/CollectionEditor.cs | 14 ++++++++++++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 9 ++++++++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index d951639c..a997ded8 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -130,8 +130,8 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } - public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, string modDirectory, - string modName, int key) + public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, + string modDirectory, string modName, int key) { var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName); if (!collectionManager.Storage.ById(collectionId, out var collection)) @@ -296,15 +296,7 @@ public class TemporaryApi( if (collection.Identity.Index <= 0) return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); - var numRemoved = 0; - for (var i = 0; i < collection.Settings.Count; ++i) - { - if (collection.GetTempSettings(i) is { } tempSettings - && tempSettings.Lock == key - && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) - ++numRemoved; - } - + var numRemoved = collectionManager.Editor.ClearTemporarySettings(collection, key); return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index f4902fda..5ccc38e2 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -114,6 +114,20 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public int ClearTemporarySettings(ModCollection collection, int key = 0) + { + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is { } tempSettings + && tempSettings.Lock == key + && SetTemporarySettings(collection, modStorage[i], null, key)) + ++numRemoved; + } + + return numRemoved; + } + public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) { var old = collection.GetTempSettings(mod.Index); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 1a7d4e31..0a68c077 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -64,6 +64,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); + SubscribeRightClickMain(ClearTemporarySettings, 105); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); SubscribeRightClickMain(() => ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); @@ -237,10 +238,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Mon, 17 Feb 2025 17:39:41 +0100 Subject: [PATCH 2173/2451] Add option to always work in temporary settings. --- Penumbra/Configuration.cs | 1 + .../Mods/Settings/TemporaryModSettings.cs | 9 ++-- Penumbra/UI/Classes/CollectionSelectHeader.cs | 43 ++++++++++++++++--- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 9 ++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 11 +++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 27 ++++++------ Penumbra/UI/Tabs/SettingsTab.cs | 4 +- 7 files changed, 72 insertions(+), 32 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index df44a51a..ce86dd4a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -70,6 +70,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public bool DefaultTemporaryMode { get; set; } = false; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index fa71e1b6..a16a9feb 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -2,9 +2,10 @@ namespace Penumbra.Mods.Settings; public sealed class TemporaryModSettings : ModSettings { - public string Source = string.Empty; - public int Lock = 0; - public bool ForceInherit; + public const string OwnSource = "yourself"; + public string Source = string.Empty; + public int Lock = 0; + public bool ForceInherit; // Create default settings for a given mod. public static TemporaryModSettings DefaultSettings(Mod mod, string source, bool enabled = false, int key = 0) @@ -20,7 +21,7 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } - public TemporaryModSettings(Mod mod, ModSettings? clone, string source, int key = 0) + public TemporaryModSettings(Mod mod, ModSettings? clone, string source = OwnSource, int key = 0) { Source = source; Lock = key; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 0e1408c5..54fcf279 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,7 +1,10 @@ +using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -12,18 +15,21 @@ namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModSelection _selection; - private readonly CollectionResolver _resolver; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; + private readonly FontAwesomeCheckbox _temporaryCheckbox = new(FontAwesomeIcon.Stopwatch); + private readonly Configuration _config; public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, - CollectionResolver resolver) + CollectionResolver resolver, Configuration config) { _tutorial = tutorial; _selection = selection; _resolver = resolver; + _config = config; _activeCollections = collectionManager.Active; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Identity.Name).ToList()); } @@ -33,6 +39,8 @@ public class CollectionSelectHeader : IUiService { using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); + DrawTemporaryCheckbox(); + ImGui.SameLine(); var comboWidth = ImGui.GetContentRegionAvail().X / 4f; var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) @@ -51,6 +59,29 @@ public class CollectionSelectHeader : IUiService ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } + private void DrawTemporaryCheckbox() + { + var hold = _config.DeleteModModifier.IsActive(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) + { + var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); + using var color = ImRaii.PushColor(ImGuiCol.FrameBgHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.FrameBgActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.CheckMark, tint) + .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); + if (_temporaryCheckbox.Draw("##tempCheck"u8, _config.DefaultTemporaryMode, out var newValue) && hold) + { + _config.DefaultTemporaryMode = newValue; + _config.Save(); + } + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired.\n"u8); + if (!hold) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to toggle."); + } + private enum CollectionState { Empty, diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index b723978b..8dee13bf 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -20,6 +20,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private bool _temporary; private bool _locked; private TemporaryModSettings? _tempSettings; + private ModSettings? _settings; public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { @@ -27,6 +28,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle return; _blockGroupCache.Clear(); + _settings = settings; _tempSettings = tempSettings; _temporary = tempSettings != null; _locked = (tempSettings?.Lock ?? 0) > 0; @@ -242,10 +244,11 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) { - if (_temporary) + if (_temporary || config.DefaultTemporaryMode) { - _tempSettings!.ForceInherit = false; - _tempSettings!.Settings[groupIdx] = setting; + _tempSettings ??= new TemporaryModSettings(group.Mod, _settings); + _tempSettings!.ForceInherit = false; + _tempSettings!.Settings[groupIdx] = setting; collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); } else diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0a68c077..a0383329 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -274,8 +274,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { - const string source = "yourself"; - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); if (tempSettings is { Lock: > 0 }) return; @@ -284,19 +283,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector _config.PrintSuccessfulCommandsToChat = v); @@ -618,6 +617,9 @@ public class SettingsTab : ITab, IUiService /// Draw all settings pertaining to import and export of mods. private void DrawModHandlingSettings() { + Checkbox("Use Temporary Settings Per Default", + "When you make any changes to your collection, apply them as temporary changes first and require a click to 'turn permanent' if you want to keep them.", + _config.DefaultTemporaryMode, v => _config.DefaultTemporaryMode = v); Checkbox("Replace Non-Standard Symbols On Import", "Replace all non-ASCII symbols in mod and option names with underscores when importing mods.", _config.ReplaceNonAsciiOnImport, v => _config.ReplaceNonAsciiOnImport = v); From 41672c31ce0e63f07c3c46e9c903ae05442dc0a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Feb 2025 15:10:16 +0100 Subject: [PATCH 2174/2451] Update message slightly. --- Penumbra/Collections/Cache/CollectionCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 3f0ed27b..a80928d0 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -282,11 +282,11 @@ public sealed class CollectionCache : IDisposable { case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc": Penumbra.Messager.NotificationMessage( - $"Redirection of {ext} files for {mod.Name} is unsupported. Please use the corresponding meta manipulations instead.", + $"Redirection of {ext} files for {mod.Name} is unsupported. This probably means that the mod is outdated and may not work correctly.\n\nPlease tell the mod creator to use the corresponding meta manipulations instead.", NotificationType.Warning); return false; case ".lvb" or ".lgb" or ".sgb": - Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.", + Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.\n\nThis mod will probably not work correctly.", NotificationType.Warning); return false; default: return true; From a73dee83b309104600f326d45e46f6fd9ab2180c Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 18 Feb 2025 14:12:54 +0000 Subject: [PATCH 2175/2451] [CI] Updating repo.json for testing_1.3.4.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index fdb0b638..c46f3d27 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.2", + "TestingAssemblyVersion": "1.3.4.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ef26049c53d57cb5625f9dab476d5c61ae1c904b Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 5 Feb 2025 11:25:09 -0600 Subject: [PATCH 2176/2451] Consider VertexElement's UsageIndex Allows VertexDeclarations to have multiple VertexElements of the same Type but different UsageIndex --- Penumbra/Import/Models/Export/MeshExporter.cs | 117 ++++++++++++------ .../Import/Models/Export/VertexFragment.cs | 109 ++++++++++++++++ 2 files changed, 187 insertions(+), 39 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 73160615..0dc8a9ac 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,13 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); + var usages = new Dictionary>(); - var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements - .ToImmutableDictionary( - element => (MdlFile.VertexUsage)element.Usage, - element => (MdlFile.VertexType)element.Type - ); - + foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) + { + if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) + { + usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); + } + usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; + } _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -104,14 +107,20 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { + Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) + { + Penumbra.Log.Debug("BoneTableIndex was 255"); return null; + } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; @@ -283,13 +292,20 @@ public class MeshExporter var vertices = new List(); - var attributes = new Dictionary(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + { + if (!attributes.TryGetValue(usage, out var value)) + { + value = new Dictionary(); + attributes[usage] = value; + } + + value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + } var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -320,7 +336,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -335,28 +351,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]), + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), bitangent ); } @@ -365,18 +381,23 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) - uvCount = type switch + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + { + foreach (var type in dict.Values) { - MdlFile.VertexType.Half2 => 1, - MdlFile.VertexType.Half4 => 2, - MdlFile.VertexType.Single2 => 1, - MdlFile.VertexType.Single4 => 2, - _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), - }; + uvCount += type switch + { + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single2 => 1, + MdlFile.VertexType.Single4 => 2, + _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), + }; + } + } var materialUsages = ( uvCount, @@ -385,6 +406,8 @@ public class MeshExporter return materialUsages switch { + (3, true) => typeof(VertexTexture3ColorFfxiv), + (3, false) => typeof(VertexTexture3), (2, true) => typeof(VertexTexture2ColorFfxiv), (2, false) => typeof(VertexTexture2), (1, true) => typeof(VertexTexture1ColorFfxiv), @@ -397,28 +420,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV]), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector2(attributes[MdlFile.VertexUsage.UV][0]), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -427,11 +450,27 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ); + } + if (_materialType == typeof(VertexTexture3)) + { + throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + } + + if (_materialType == typeof(VertexTexture3ColorFfxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3ColorFfxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); } @@ -439,11 +478,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -457,7 +496,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -467,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index eff34d54..c9b97997 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -282,3 +282,112 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3ColorFfxiv : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_2", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0; + public Vector2 TexCoord1; + public Vector2 TexCoord2; + public Vector4 FfxivColor; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + { + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; + TexCoord2 = texCoord2; + FfxivColor = ffxivColor; + } + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + TexCoord2 += delta.TexCoord2Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + 2 => TexCoord2, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex == 2) + TexCoord2 = coord; + if (setIndex >= 3) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR": + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} From 2f0bf19d00f9047817fb8bbfba38e5471f14bb20 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 09:48:22 -0600 Subject: [PATCH 2177/2451] Use First().Value --- Penumbra/Import/Models/Export/MeshExporter.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 0dc8a9ac..48f66177 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -355,24 +355,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), bitangent ); } @@ -426,22 +426,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV][0]), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +450,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +470,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } @@ -482,7 +482,7 @@ public class MeshExporter { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -506,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From 579969a9e107e9a7b4b7adfef5f419cbed648899 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 12:45:15 -0600 Subject: [PATCH 2178/2451] Using LINQ And also change types from using LINQ --- Penumbra/Import/Models/Export/MeshExporter.cs | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 48f66177..fb88dfc3 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,16 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); - var usages = new Dictionary>(); - foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) - { - if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) - { - usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); - } - usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; - } + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements + .GroupBy(ele => (MdlFile.VertexUsage)ele.Usage, ele => ele) + .ToImmutableDictionary( + g => g.Key, + g => g.OrderBy(ele => ele.UsageIndex) // OrderBy UsageIndex is probably unnecessary as they're probably already be in order + .Select(ele => (MdlFile.VertexType)ele.Type) + .ToList() + ); + _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -287,25 +287,22 @@ public class MeshExporter var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements .OrderBy(element => element.Offset) - .Select(element => ((MdlFile.VertexUsage)element.Usage, element)) .ToList(); - var vertices = new List(); - var attributes = new Dictionary>(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - { - if (!attributes.TryGetValue(usage, out var value)) - { - value = new Dictionary(); - attributes[usage] = value; - } - - value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); - } + attributes = sortedElements + .GroupBy(element => element.Usage) + .ToDictionary( + x => (MdlFile.VertexUsage)x.Key, + x => x.OrderBy(ele => ele.UsageIndex) // Once again, OrderBy UsageIndex is probably unnecessary + .Select(ele => ReadVertexAttribute((MdlFile.VertexType)ele.Type, streams[ele.Stream])) + .ToList() + ); + var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -336,7 +333,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary> usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -351,28 +348,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), bitangent ); } @@ -381,12 +378,12 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary> usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var list)) { - foreach (var type in dict.Values) + foreach (var type in list) { uvCount += type switch { @@ -420,28 +417,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector2(attributes[MdlFile.VertexUsage.UV].First()), + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +447,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +467,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } @@ -478,11 +475,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -496,7 +493,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -506,8 +503,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From b76626ac8dbe9e4595be5e87a8f221415314ed18 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:18:30 -0600 Subject: [PATCH 2179/2451] Added VertexTexture3 Not sure of accuracy but followed existing pattern --- Penumbra/Import/Models/Export/MeshExporter.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index fb88dfc3..a3cc2f04 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -456,7 +456,14 @@ public class MeshExporter } if (_materialType == typeof(VertexTexture3)) { - throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + // Not 100% sure about this + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y) + ); } if (_materialType == typeof(VertexTexture3ColorFfxiv)) From 6d2b72e0798ac6cad82d54d9d0996ae666ff4c1e Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:30:06 -0600 Subject: [PATCH 2180/2451] Removed irrelevant comments --- Penumbra/Import/Models/Export/MeshExporter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index a3cc2f04..6d65e152 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -107,19 +107,14 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { - Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) - { - Penumbra.Log.Debug("BoneTableIndex was 255"); return null; - } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. - Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { From 31f23024a45b0663b87c97a1a8c0fe56b4b891b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:08 +0100 Subject: [PATCH 2181/2451] Notify and fail when a list of vertex usages has more than one entry where this is not expected. --- Penumbra/Import/Models/Export/MeshExporter.cs | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 6d65e152..aa0811d7 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -7,7 +7,6 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.IO; using SharpGLTF.Materials; using SharpGLTF.Scenes; @@ -347,24 +346,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; + var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), bitangent ); } @@ -418,22 +417,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); + return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); + return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First()), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -442,11 +441,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } if (_materialType == typeof(VertexTexture3)) @@ -469,7 +468,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } @@ -477,18 +476,13 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) - { - return typeof(VertexJoints8); - } - else - { - return typeof(VertexJoints4); - } + return GetFirstSafe(usages, MdlFile.VertexUsage.BlendWeights) == MdlFile.VertexType.UShort4 + ? typeof(VertexJoints8) + : typeof(VertexJoints4); } return typeof(VertexEmpty); @@ -505,8 +499,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); + var indiciesData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendIndices); + var weightsData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendWeights); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); @@ -533,6 +527,17 @@ public class MeshExporter throw _notifier.Exception($"Unknown skinning type {_skinningType}"); } + /// Check that the list has length 1 for any case where this is expected and return the one entry. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private T GetFirstSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 1) + throw _notifier.Exception($"Multiple usage indices encountered for {usage}."); + + return list[0]; + } + /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) => data switch From f8d0616acd835eefc394b7303b6e4ee55f1e923d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:33 +0100 Subject: [PATCH 2182/2451] Notify when an unhandled UV count is reached. --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index aa0811d7..32b9b323 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -406,7 +406,7 @@ public class MeshExporter (0, true) => typeof(VertexColorFfxiv), (0, false) => typeof(VertexEmpty), - _ => throw new Exception("Unreachable."), + _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), }; } From d40c59eee98bcd2f20ac0ce701e181bf6eb7bf70 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:46 +0100 Subject: [PATCH 2183/2451] Slight cleanup. --- .../Import/Models/Export/VertexFragment.cs | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index c9b97997..56495f2f 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -1,4 +1,3 @@ -using System; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Memory; using SharpGLTF.Schema2; @@ -11,7 +10,7 @@ Realistically, it will need to stick around until transforms/mutations are built and there's reason to overhaul the export pipeline. */ -public struct VertexColorFfxiv : IVertexCustom +public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -20,7 +19,7 @@ public struct VertexColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -33,9 +32,6 @@ public struct VertexColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexColorFfxiv(Vector4 ffxivColor) - => FfxivColor = ffxivColor; - public void Add(in VertexMaterialDelta delta) { } @@ -88,7 +84,7 @@ public struct VertexColorFfxiv : IVertexCustom } } -public struct VertexTexture1ColorFfxiv : IVertexCustom +public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -98,9 +94,9 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; + public Vector2 TexCoord0 = texCoord0; - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -113,12 +109,6 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -182,7 +172,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom } } -public struct VertexTexture2ColorFfxiv : IVertexCustom +public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -194,9 +184,9 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -209,13 +199,6 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -283,7 +266,8 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom } } -public struct VertexTexture3ColorFfxiv : IVertexCustom +public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -297,10 +281,10 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector2 TexCoord2; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -313,14 +297,6 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - TexCoord2 = texCoord2; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -387,7 +363,7 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom FfxivColor.Z, FfxivColor.W, }; - if (components.Any(component => component < 0 || component > 1)) + if (components.Any(component => component is < 0f or > 1f)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } From 1f172b463206bc41b5769f0d6148d551aefdda50 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:37:15 +0100 Subject: [PATCH 2184/2451] Make default constructed models use V6 instead of V5. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f6dff467..b4a0806e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f6dff467c7dad6b1213a7d7b65d40a56450f0672 +Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7 From fdd75e2866a10aa380eddf46d615af6ad373f11a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 19:17:55 +0100 Subject: [PATCH 2185/2451] Use Meta Compression V1. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index ff88ae4e..7c0cd5fc 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -51,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV0(collection); + => CompressMetaManipulationsV1(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From 4a00d82921468397ed7262ff8aef470090d9d398 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 20 Feb 2025 18:20:23 +0000 Subject: [PATCH 2186/2451] [CI] Updating repo.json for testing_1.3.4.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c46f3d27..afb2c32d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.3", + "TestingAssemblyVersion": "1.3.4.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 514b0e7f30f4a792726a1bca9c811bdf0a373ee2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 27 Feb 2025 00:10:24 +0100 Subject: [PATCH 2187/2451] Add file types to Resource Tree and require Ctrl+Shift for some quick imports --- Penumbra.GameData | 2 +- .../ResolveContext.PathResolution.cs | 84 ++++++++++++----- .../Interop/ResourceTree/ResolveContext.cs | 93 ++++++++++++++++++- Penumbra/Interop/ResourceTree/ResourceNode.cs | 11 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 50 ++++++++-- Penumbra/Interop/Structs/StructExtensions.cs | 12 +++ .../ModEditWindow.QuickImport.cs | 5 +- 7 files changed, 224 insertions(+), 33 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b4a0806e..c59b1da6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7 +Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index bdf66a16..cd6b8568 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -22,6 +22,13 @@ internal partial record ResolveContext private static bool IsEquipmentSlot(uint slotIndex) => slotIndex is < 5 or 16 or 17; + private unsafe Variant Variant + => ModelType switch + { + ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant, + _ => Equipment.Variant, + }; + private Utf8GamePath ResolveModelPath() { // Correctness: @@ -92,7 +99,7 @@ internal partial record ResolveContext => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } @@ -100,7 +107,7 @@ internal partial record ResolveContext [SkipLocalsInit] private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var variant = ResolveImcData(imc).MaterialId; var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; @@ -118,9 +125,9 @@ internal partial record ResolveContext return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; // Some offhands share materials with the corresponding mainhand - if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) + if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId)) { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var variant = ResolveImcData(imc).MaterialId; var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span mirroredFileName = stackalloc byte[32]; @@ -141,31 +148,16 @@ internal partial record ResolveContext return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) - { - var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - - Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } - - private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant) + private unsafe ImcEntry ResolveImcData(ResourceHandle* imc) { var imcFileData = imc->GetDataSpan(); if (imcFileData.IsEmpty) { Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span"); - return variant.Id; + return default; } - var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists); - if (!exists) - return variant.Id; - - return entry.MaterialId; + return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _); } private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, @@ -317,4 +309,52 @@ internal partial record ResolveContext var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } + + private Utf8GamePath ResolvePhysicsModulePath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a physics module path through the game's code can use EST metadata for human skeletons. + // Additionally, it can dereference null pointers for human equipment skeletons. + return ModelType switch + { + ModelType.Human => ResolveHumanPhysicsModulePath(partialSkeletonIndex), + _ => ResolvePhysicsModulePathNative(partialSkeletonIndex), + }; + } + + private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolvePhysicsModulePathNative(uint partialSkeletonIndex) + { + var path = CharacterBase->ResolvePhybPathAsByteString(partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) + { + var animation = ResolveImcData(imc).MaterialAnimationId; + if (animation == 0) + return Utf8GamePath.Empty; + + var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc) + { + var decal = ResolveImcData(imc).DecalId; + if (decal == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Equipment.Decal.Path(decal); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 54612070..f33bf041 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -180,7 +180,15 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } - public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) + public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath) + { + if (tex == null) + return null; + + return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); + } + + public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) { if (mdl == null || mdl->ModelResourceHandle == null) return null; @@ -210,6 +218,14 @@ internal unsafe partial record ResolveContext( } } + var decalNode = CreateNodeFromDecal(decalHandle, imc); + if (null != decalNode) + node.Children.Add(decalNode); + + var mpapNode = CreateNodeFromMaterialPap(mpapHandle, imc); + if (null != mpapNode) + node.Children.Add(mpapNode); + Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); return node; @@ -301,7 +317,59 @@ internal unsafe partial record ResolveContext( } } - public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) + public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc) + { + if (decalHandle == null) + return null; + + var path = ResolveDecalPath(imc); + if (path.IsEmpty) + return null; + + var node = CreateNodeFromTex(decalHandle, path)!; + if (Global.WithUiData) + node.FallbackName = "Decal"; + + return node; + } + + public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc) + { + if (mpapHandle == null) + return null; + + var path = ResolveMaterialAnimationPath(imc); + if (path.IsEmpty) + return null; + + if (Global.Nodes.TryGetValue((path, (nint)mpapHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Pap, 0, mpapHandle, path); + if (Global.WithUiData) + node.FallbackName = "Material Animation"; + + return node; + } + + public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle) + { + if (sklbHandle == null) + return null; + + if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) + return null; + + if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path); + node.ForceInternal = true; + + return node; + } + + public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonResourceHandle == null) return null; @@ -315,6 +383,9 @@ internal unsafe partial record ResolveContext( var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); if (skpNode != null) node.Children.Add(skpNode); + var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex); + if (phybNode != null) + node.Children.Add(phybNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); return node; @@ -338,6 +409,24 @@ internal unsafe partial record ResolveContext( return node; } + private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex) + { + if (phybHandle == null) + return null; + + var path = ResolvePhysicsModulePath(partialSkeletonIndex); + + if (Global.Nodes.TryGetValue((path, (nint)phybHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Phyb, 0, phybHandle, path, false); + if (Global.WithUiData) + node.FallbackName = "Physics Module"; + Global.Nodes.Add((path, (nint)phybHandle), node); + + return node; + } + internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { var path = gamePath.Path.Split((byte)'/'); diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 60cc48de..24cb8f02 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -21,6 +21,8 @@ public class ResourceNode : ICloneable public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; + public bool ForceInternal; + public bool ForceProtected; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -37,8 +39,13 @@ public class ResourceNode : ICloneable } } + /// Whether to treat the file as internal (hide from user unless debug mode is on). public bool Internal - => Type is ResourceType.Eid or ResourceType.Imc; + => ForceInternal || Type is ResourceType.Eid or ResourceType.Imc; + + /// Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). + public bool Protected + => ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { @@ -67,6 +74,8 @@ public class ResourceNode : ICloneable Mod = other.Mod; ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; + ForceInternal = other.ForceInternal; + ForceProtected = other.ForceProtected; Length = other.Length; Children = other.Children; ResolveContext = other.ResolveContext; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b50fc695..ac1f889c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,7 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Physics; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -10,6 +12,7 @@ using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.ResourceTree; @@ -74,6 +77,18 @@ public class ResourceTree var genericContext = globalContext.CreateContext(model); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); + var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + var decalArray = modelType switch + { + ModelType.Human => human->SlotDecalsSpan, + ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals, + ModelType.Weapon => [((Weapon*)model)->Decal], + ModelType.Monster => [((Monster*)model)->Decal], + _ => [], + }; + for (var i = 0u; i < model->SlotCount; ++i) { var slotContext = modelType switch @@ -100,7 +115,8 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, + i < mpapArray.Length ? mpapArray[(int)i].Value : null); if (mdlNode != null) { if (globalContext.WithUiData) @@ -109,7 +125,9 @@ public class ResourceTree } } - AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton); + AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940)); AddWeapons(globalContext, model); @@ -140,6 +158,10 @@ public class ResourceTree var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948); + var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; + for (var i = 0; i < subObject->SlotCount; ++i) { var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); @@ -154,7 +176,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null); if (mdlNode != null) { if (globalContext.WithUiData) @@ -163,7 +185,9 @@ public class ResourceTree } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), $"Weapon #{weaponIndex}, "); ++weaponIndex; } @@ -216,6 +240,7 @@ public class ResourceTree var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); if (legacyDecalNode != null) { + legacyDecalNode.ForceProtected = !hasLegacyDecal; if (globalContext.WithUiData) { legacyDecalNode = legacyDecalNode.Clone(); @@ -227,7 +252,7 @@ public class ResourceTree } } - private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "") + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -242,7 +267,9 @@ public class ResourceTree for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i); if (sklbNode != null) { if (context.Global.WithUiData) @@ -251,4 +278,15 @@ public class ResourceTree } } } + + private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, string prefix = "") + { + var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle); + if (sklbNode == null) + return; + + if (context.Global.WithUiData) + sklbNode.FallbackName = $"{prefix}Material Animation Skeleton"; + nodes.Add(sklbNode); + } } diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 9dd9a96d..8b5974f0 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -33,6 +33,12 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); } + public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId)); + } + public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; @@ -45,6 +51,12 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } + public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); + } + private static unsafe CiByteString ToOwnedByteString(byte* str) => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 6fb223df..a49d2933 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -110,8 +110,11 @@ public partial class ModEditWindow _quickImportActions.Add((resourceNode.GamePath, writable), quickImport); } + var canQuickImport = quickImport.CanExecute; + var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive()); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, - $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) + $"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}", + !quickImportEnabled, true)) { quickImport.Execute(); _quickImportActions.Remove((resourceNode.GamePath, writable)); From 776a93dc73b03f87dbcea4165f4ab571f4f21b0c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:38:42 +0100 Subject: [PATCH 2188/2451] Some null-check cleanup. --- .../ResolveContext.PathResolution.cs | 10 +-- .../Interop/ResourceTree/ResolveContext.cs | 81 ++++++++----------- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index cd6b8568..b1ca24b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -248,7 +248,7 @@ internal partial record ResolveContext if (faceId < 201) faceId -= tribe switch { - 0xB when modelType == 4 => 100, + 0xB when modelType is 4 => 100, 0xE | 0xF => 100, _ => 0, }; @@ -297,7 +297,7 @@ internal partial record ResolveContext private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex) { var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); - if (set == 0) + if (set.Id is 0) return Utf8GamePath.Empty; var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set); @@ -325,7 +325,7 @@ internal partial record ResolveContext private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex) { var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); - if (set == 0) + if (set.Id is 0) return Utf8GamePath.Empty; var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); @@ -341,7 +341,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) { var animation = ResolveImcData(imc).MaterialAnimationId; - if (animation == 0) + if (animation is 0) return Utf8GamePath.Empty; var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation); @@ -351,7 +351,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc) { var decal = ResolveImcData(imc).DecalId; - if (decal == 0) + if (decal is 0) return Utf8GamePath.Empty; var path = GamePaths.Equipment.Decal.Path(decal); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index f33bf041..81904819 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -52,7 +52,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) { - if (resourceHandle == null) + if (resourceHandle is null) return null; if (gamePath.IsEmpty) return null; @@ -65,7 +65,7 @@ internal unsafe partial record ResolveContext( [SkipLocalsInit] private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11) { - if (resourceHandle == null) + if (resourceHandle is null) return null; Utf8GamePath path; @@ -105,7 +105,7 @@ internal unsafe partial record ResolveContext( private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath) { - if (resourceHandle == null) + if (resourceHandle is null) throw new ArgumentNullException(nameof(resourceHandle)); if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached)) @@ -117,7 +117,7 @@ internal unsafe partial record ResolveContext( private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool autoAdd = true) { - if (resourceHandle == null) + if (resourceHandle is null) throw new ArgumentNullException(nameof(resourceHandle)); var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); @@ -141,7 +141,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromEid(ResourceHandle* eid) { - if (eid == null) + if (eid is null) return null; if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path)) @@ -152,7 +152,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { - if (imc == null) + if (imc is null) return null; if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path)) @@ -163,7 +163,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd) { - if (pbd == null) + if (pbd is null) return null; return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath); @@ -171,7 +171,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { - if (tex == null) + if (tex is null) return null; if (!Utf8GamePath.FromString(gamePath, out var path)) @@ -182,7 +182,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath) { - if (tex == null) + if (tex is null) return null; return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); @@ -190,7 +190,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) { - if (mdl == null || mdl->ModelResourceHandle == null) + if (mdl is null || mdl->ModelResourceHandle is null) return null; var mdlResource = mdl->ModelResourceHandle; @@ -205,12 +205,12 @@ internal unsafe partial record ResolveContext( for (var i = 0; i < mdl->MaterialCount; i++) { var mtrl = mdl->Materials[i]; - if (mtrl == null) + if (mtrl is null) continue; var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i); var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName)); - if (mtrlNode != null) + if (mtrlNode is not null) { if (Global.WithUiData) mtrlNode.FallbackName = $"Material #{i}"; @@ -218,12 +218,10 @@ internal unsafe partial record ResolveContext( } } - var decalNode = CreateNodeFromDecal(decalHandle, imc); - if (null != decalNode) + if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode) node.Children.Add(decalNode); - var mpapNode = CreateNodeFromMaterialPap(mpapHandle, imc); - if (null != mpapNode) + if (CreateNodeFromMaterialPap(mpapHandle, imc) is { } mpapNode) node.Children.Add(mpapNode); Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); @@ -233,7 +231,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path) { - if (mtrl == null || mtrl->MaterialResourceHandle == null) + if (mtrl is null || mtrl->MaterialResourceHandle is null) return null; var resource = mtrl->MaterialResourceHandle; @@ -242,15 +240,15 @@ internal unsafe partial record ResolveContext( var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); - if (shpkNode != null) + if (shpkNode is not null) { if (Global.WithUiData) shpkNode.Name = "Shader Package"; node.Children.Add(shpkNode); } - var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; - var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkNames = Global.WithUiData && shpkNode is not null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode is not null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -263,7 +261,7 @@ internal unsafe partial record ResolveContext( if (Global.WithUiData) { string? name = null; - if (shpk != null) + if (shpk is not null) { var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); var samplerId = index != 0x001F @@ -319,7 +317,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc) { - if (decalHandle == null) + if (decalHandle is null) return null; var path = ResolveDecalPath(imc); @@ -335,7 +333,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc) { - if (mpapHandle == null) + if (mpapHandle is null) return null; var path = ResolveMaterialAnimationPath(imc); @@ -354,7 +352,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle) { - if (sklbHandle == null) + if (sklbHandle is null) return null; if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) @@ -371,7 +369,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) { - if (sklb == null || sklb->SkeletonResourceHandle == null) + if (sklb is null || sklb->SkeletonResourceHandle is null) return null; var path = ResolveSkeletonPath(partialSkeletonIndex); @@ -379,12 +377,10 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); - var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); - if (skpNode != null) + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); + if (CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex) is { } skpNode) node.Children.Add(skpNode); - var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex); - if (phybNode != null) + if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode) node.Children.Add(phybNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); @@ -393,7 +389,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { - if (sklb == null || sklb->SkeletonParameterResourceHandle == null) + if (sklb is null || sklb->SkeletonParameterResourceHandle is null) return null; var path = ResolveSkeletonParameterPath(partialSkeletonIndex); @@ -411,7 +407,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex) { - if (phybHandle == null) + if (phybHandle is null) return null; var path = ResolvePhysicsModulePath(partialSkeletonIndex); @@ -431,7 +427,9 @@ internal unsafe partial record ResolveContext( { var path = gamePath.Path.Split((byte)'/'); // Weapons intentionally left out. - var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); + var isEquipment = path.Count >= 2 + && path[0].Span.SequenceEqual("chara"u8) + && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { @@ -447,7 +445,7 @@ internal unsafe partial record ResolveContext( } var dataFromPath = GuessUiDataFromPath(gamePath); - if (dataFromPath.Name != null) + if (dataFromPath.Name is not null) return dataFromPath; return isEquipment @@ -462,24 +460,13 @@ internal unsafe partial record ResolveContext( var name = obj.Key; if (obj.Value is IdentifiedCustomization) name = name[14..].Trim(); - if (name != "Unknown") + if (name is not "Unknown") return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } - private static string? SafeGet(ReadOnlySpan array, Index index) - { - var i = index.GetOffset(array.Length); - return i >= 0 && i < array.Length ? array[i] : null; - } - private static ulong GetResourceHandleLength(ResourceHandle* handle) - { - if (handle == null) - return 0; - - return handle->GetLength(); - } + => handle is null ? 0ul : handle->GetLength(); } From e4cfd674ee1443f876574fa5f8e351413d739d7d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:39:19 +0100 Subject: [PATCH 2189/2451] Probably unnecessary size optimization. --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 24cb8f02..3699ae0b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -17,12 +17,12 @@ public class ResourceNode : ICloneable public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; public PathStatus FullPathStatus; + public bool ForceInternal; + public bool ForceProtected; public string? ModName; public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; - public bool ForceInternal; - public bool ForceProtected; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; From 70844610d8a913ffd915d0348882cb14bd50a89f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:45:06 +0100 Subject: [PATCH 2190/2451] Primary constructor and some null-check cleanup. --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 132 ++++++++---------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index ac1f889c..5e3f52d4 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -16,42 +16,35 @@ using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.Re namespace Penumbra.Interop.ResourceTree; -public class ResourceTree +public class ResourceTree( + string name, + string anonymizedName, + int gameObjectIndex, + nint gameObjectAddress, + nint drawObjectAddress, + bool localPlayerRelated, + bool playerRelated, + bool networked, + string collectionName, + string anonymizedCollectionName) { - public readonly string Name; - public readonly string AnonymizedName; - public readonly int GameObjectIndex; - public readonly nint GameObjectAddress; - public readonly nint DrawObjectAddress; - public readonly bool LocalPlayerRelated; - public readonly bool PlayerRelated; - public readonly bool Networked; - public readonly string CollectionName; - public readonly string AnonymizedCollectionName; - public readonly List Nodes; - public readonly HashSet FlatNodes; + public readonly string Name = name; + public readonly string AnonymizedName = anonymizedName; + public readonly int GameObjectIndex = gameObjectIndex; + public readonly nint GameObjectAddress = gameObjectAddress; + public readonly nint DrawObjectAddress = drawObjectAddress; + public readonly bool LocalPlayerRelated = localPlayerRelated; + public readonly bool PlayerRelated = playerRelated; + public readonly bool Networked = networked; + public readonly string CollectionName = collectionName; + public readonly string AnonymizedCollectionName = anonymizedCollectionName; + public readonly List Nodes = []; + public readonly HashSet FlatNodes = []; public int ModelId; public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, - bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) - { - Name = name; - AnonymizedName = anonymizedName; - GameObjectIndex = gameObjectIndex; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - LocalPlayerRelated = localPlayerRelated; - Networked = networked; - PlayerRelated = playerRelated; - CollectionName = collectionName; - AnonymizedCollectionName = anonymizedCollectionName; - Nodes = []; - FlatNodes = []; - } - public void ProcessPostfix(Action action) { foreach (var node in Nodes) @@ -73,13 +66,13 @@ public class ResourceTree }; ModelId = character->ModelContainer.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; - RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + RaceCode = human is not null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; var genericContext = globalContext.CreateContext(model); // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); - var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; var decalArray = modelType switch { ModelType.Human => human->SlotDecalsSpan, @@ -105,19 +98,17 @@ public class ResourceTree : globalContext.CreateContext(model, i), }; - var imc = (ResourceHandle*)model->IMCArray[i]; - var imcNode = slotContext.CreateNodeFromImc(imc); - if (imcNode != null) + var imc = (ResourceHandle*)model->IMCArray[i]; + if (slotContext.CreateNodeFromImc(imc) is { } imcNode) { if (globalContext.WithUiData) imcNode.FallbackName = $"IMC #{i}"; Nodes.Add(imcNode); } - var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, - i < mpapArray.Length ? mpapArray[(int)i].Value : null); - if (mdlNode != null) + var mdl = model->Models[i]; + if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, + i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Model #{i}"; @@ -131,7 +122,7 @@ public class ResourceTree AddWeapons(globalContext, model); - if (human != null) + if (human is not null) AddHumanResources(globalContext, human); } @@ -141,12 +132,12 @@ public class ResourceTree var weaponNodes = new List(); foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { - if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) + if (baseSubObject->GetObjectType() is not FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != ModelType.Weapon) + if (subObject->GetModelType() is not ModelType.Weapon) continue; var weapon = (Weapon*)subObject; @@ -160,24 +151,22 @@ public class ResourceTree // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948); - var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; + var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; for (var i = 0; i < subObject->SlotCount; ++i) { var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); - var imc = (ResourceHandle*)subObject->IMCArray[i]; - var imcNode = slotContext.CreateNodeFromImc(imc); - if (imcNode != null) + var imc = (ResourceHandle*)subObject->IMCArray[i]; + if (slotContext.CreateNodeFromImc(imc) is { } imcNode) { if (globalContext.WithUiData) imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}"; weaponNodes.Add(imcNode); } - var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null); - if (mdlNode != null) + var mdl = subObject->Models[i]; + if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; @@ -185,9 +174,11 @@ public class ResourceTree } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, + $"Weapon #{weaponIndex}, "); // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), $"Weapon #{weaponIndex}, "); + AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), + $"Weapon #{weaponIndex}, "); ++weaponIndex; } @@ -200,28 +191,25 @@ public class ResourceTree var genericContext = globalContext.CreateContext(&human->CharacterBase); var cache = globalContext.Collection._cache; - if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + if (cache is not null + && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle) + && genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode) { - var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); - if (pbdNode != null) + if (globalContext.WithUiData) { - if (globalContext.WithUiData) - { - pbdNode = pbdNode.Clone(); - pbdNode.FallbackName = "Racial Deformer"; - pbdNode.IconFlag = ChangedItemIconFlag.Customization; - } - - Nodes.Add(pbdNode); + pbdNode = pbdNode.Clone(); + pbdNode.FallbackName = "Racial Deformer"; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } + + Nodes.Add(pbdNode); } var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); - var decalPath = decalId != 0 + var decalPath = decalId is not 0 ? GamePaths.Human.Decal.FaceDecalPath(decalId) : GamePaths.Tex.TransparentPath; - var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath); - if (decalNode != null) + if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode) { if (globalContext.WithUiData) { @@ -237,8 +225,7 @@ public class ResourceTree var legacyDecalPath = hasLegacyDecal ? GamePaths.Human.Decal.LegacyDecalPath : GamePaths.Tex.TransparentPath; - var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); - if (legacyDecalNode != null) + if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode) { legacyDecalNode.ForceProtected = !hasLegacyDecal; if (globalContext.WithUiData) @@ -252,7 +239,8 @@ public class ResourceTree } } - private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, string prefix = "") + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, + string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -269,8 +257,7 @@ public class ResourceTree { // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i); - if (sklbNode != null) + if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; @@ -279,10 +266,11 @@ public class ResourceTree } } - private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, string prefix = "") + private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, + string prefix = "") { var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle); - if (sklbNode == null) + if (sklbNode is null) return; if (context.Global.WithUiData) From 9b25193d4e6feb39136b69a522b84b395b35fce2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:51:25 +0100 Subject: [PATCH 2191/2451] ImUtf8 and null-check cleanup. --- .../ModEditWindow.QuickImport.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index a49d2933..00caaabc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,8 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using Lumina.Data; -using OtterGui; -using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; @@ -43,7 +42,7 @@ public partial class ModEditWindow private void DrawQuickImportTab() { - using var tab = ImRaii.TabItem("Import from Screen"); + using var tab = ImUtf8.TabItem("Import from Screen"u8); if (!tab) { _quickImportActions.Clear(); @@ -73,14 +72,14 @@ public partial class ModEditWindow else { var file = _gameData.GetFile(path); - writable = file == null ? null : new RawGameFileWritable(file); + writable = file is null ? null : new RawGameFileWritable(file); } _quickImportWritables.Add(resourceNode.FullPath, writable); } - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", - resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize, + resourceNode.FullPath.FullName.Length is 0 || writable is null)) { var fullPathStr = resourceNode.FullPath.FullName; var ext = resourceNode.PossibleGamePaths.Length == 1 @@ -112,16 +111,17 @@ public partial class ModEditWindow var canQuickImport = quickImport.CanExecute; var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive()); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, + if (ImUtf8.IconButton(FontAwesomeIcon.FileImport, $"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}", - !quickImportEnabled, true)) + buttonSize, + !quickImportEnabled)) { quickImport.Execute(); _quickImportActions.Remove((resourceNode.GamePath, writable)); } } - private record class RawFileWritable(string Path) : IWritable + private record RawFileWritable(string Path) : IWritable { public bool Valid => true; @@ -130,7 +130,7 @@ public partial class ModEditWindow => File.ReadAllBytes(Path); } - private record class RawGameFileWritable(FileResource FileResource) : IWritable + private record RawGameFileWritable(FileResource FileResource) : IWritable { public bool Valid => true; @@ -188,19 +188,19 @@ public partial class ModEditWindow public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file) { var editor = owner._editor; - if (editor == null) + if (editor is null) return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); var subMod = editor.Option!; var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName; - if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) + if (gamePath.IsEmpty || file is null || editor.FileEditor.Changes) return new QuickImportAction(editor, optionName, gamePath); if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) return new QuickImportAction(editor, optionName, gamePath); var mod = owner.Mod; - if (mod == null) + if (mod is null) return new QuickImportAction(editor, optionName, gamePath); var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport); @@ -235,7 +235,7 @@ public partial class ModEditWindow { var path = mod.ModPath; var subDirs = 0; - if (subMod == null) + if (subMod is null) return (path, subDirs); var name = subMod.Name; From 8860d1e39afd872eabb140f4dd955896c61d9db7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 06:01:08 +0100 Subject: [PATCH 2192/2451] Fix an exception in incognito names in weird cutscene cases. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c59b1da6..f42c7fc9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7 +Subproject commit f42c7fc9de98e9fc72680dee7805251fd938af26 From c6de7ddebd7af3b52ecb5ebebe86efef07597b1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 13:08:41 +0100 Subject: [PATCH 2193/2451] Improve GamePaths and parsing, add support for identifying skeletons and phybs. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 12 ++++---- .../ResolveContext.PathResolution.cs | 14 ++++----- .../Interop/ResourceTree/ResolveContext.cs | 12 ++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 8 ++--- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 8 ++--- .../Manipulations/GlobalEqpManipulation.cs | 10 +++---- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 24 +++++---------- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 10 +++---- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 30 +++++++------------ Penumbra/Mods/ItemSwap/ItemSwap.cs | 4 +-- .../Materials/MtrlTab.ShaderPackage.cs | 2 +- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 4 +-- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 17 files changed, 65 insertions(+), 83 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f42c7fc9..bc339208 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f42c7fc9de98e9fc72680dee7805251fd938af26 +Subproject commit bc339208d1d453582eb146533c572823146a4592 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 0c19bc0a..19d06a52 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -63,7 +63,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM if (info.FileType is not FileType.Model) return []; - var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); + var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1); return info.ObjectType switch { @@ -79,9 +79,9 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), - ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], - ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], - ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], + ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)], + ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)], + ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)], _ => [], }; } @@ -105,7 +105,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)]; + return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. @@ -137,7 +137,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM var resolvedPath = info.ObjectType switch { - ObjectType.Character => GamePaths.Character.Mtrl.Path( + ObjectType.Character => GamePaths.Mtrl.Customization( info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), _ => absolutePath, }; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b1ca24b0..b6d04769 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -43,8 +43,8 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); + ? GamePaths.Mdl.Equipment(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) + : GamePaths.Mdl.Accessory(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -122,7 +122,7 @@ internal partial record ResolveContext var setIdHigh = Equipment.Set.Id / 100; // All MCH (20??) weapons' materials C are one and the same if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') - return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; + return Utf8GamePath.FromString(GamePaths.Mtrl.Weapon(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; // Some offhands share materials with the corresponding mainhand if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId)) @@ -230,7 +230,7 @@ internal partial record ResolveContext if (set == 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Sklb.Path(raceCode, slot, set); + var path = GamePaths.Sklb.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -300,7 +300,7 @@ internal partial record ResolveContext if (set.Id is 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set); + var path = GamePaths.Skp.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -328,7 +328,7 @@ internal partial record ResolveContext if (set.Id is 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); + var path = GamePaths.Phyb.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -354,7 +354,7 @@ internal partial record ResolveContext if (decal is 0) return Utf8GamePath.Empty; - var path = GamePaths.Equipment.Decal.Path(decal); + var path = GamePaths.Tex.EquipDecal(decal); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 81904819..ea4506c7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -355,13 +355,10 @@ internal unsafe partial record ResolveContext( if (sklbHandle is null) return null; - if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) - return null; - - if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached)) + if (Global.Nodes.TryGetValue((GamePaths.Sklb.MaterialAnimationSkeletonUtf8, (nint)sklbHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path); + var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, GamePaths.Sklb.MaterialAnimationSkeletonUtf8); node.ForceInternal = true; return node; @@ -455,11 +452,12 @@ internal unsafe partial record ResolveContext( internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath) { + const string customization = "Customization: "; foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; - if (obj.Value is IdentifiedCustomization) - name = name[14..].Trim(); + if (name.StartsWith(customization)) + name = name.AsSpan(14).Trim().ToString(); if (name is not "Unknown") return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 5e3f52d4..7be8694a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -207,8 +207,8 @@ public class ResourceTree( var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId is not 0 - ? GamePaths.Human.Decal.FaceDecalPath(decalId) - : GamePaths.Tex.TransparentPath; + ? GamePaths.Tex.FaceDecal(decalId) + : GamePaths.Tex.Transparent; if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode) { if (globalContext.WithUiData) @@ -223,8 +223,8 @@ public class ResourceTree( var hasLegacyDecal = (human->Customize[(int)CustomizeIndex.FaceFeatures] & 0x80) != 0; var legacyDecalPath = hasLegacyDecal - ? GamePaths.Human.Decal.LegacyDecalPath - : GamePaths.Tex.TransparentPath; + ? GamePaths.Tex.LegacyDecal + : GamePaths.Tex.Transparent; if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode) { legacyDecalNode.ForceProtected = !hasLegacyDecal; diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 3a804d0c..285f2309 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -16,7 +16,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge => GenderRace.Split().Item1; public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); public MetaIndex FileIndex() => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index f758126c..c71f2f4d 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -9,7 +9,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() => MetaIndex.Eqp; diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index cfe9b7d4..007cd02f 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -30,17 +30,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende { case EstType.Hair: changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair (Hair) {SetId}", null); + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); break; case EstType.Face: changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face (Face) {SetId}", null); + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); break; case EstType.Body: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Body)); + identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); break; case EstType.Head: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Head)); + identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); break; } } diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ec59762b..6a1ceaea 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -74,11 +74,11 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier { var path = Type switch { - GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), - GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), - GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), - GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), - GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), + GlobalEqpType.DoNotHideEarrings => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), GlobalEqpType.DoNotHideHrothgarHats => string.Empty, GlobalEqpType.DoNotHideVieraHats => string.Empty, _ => string.Empty, diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 1f41adfb..8cd07bfd 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -9,7 +9,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() => MetaIndex.Gmp; diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index cba6c379..6e893043 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -34,14 +34,14 @@ public readonly record struct ImcIdentifier( { var path = ObjectType switch { - ObjectType.Equipment when allVariants => GamePaths.Equipment.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), - ObjectType.Equipment => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), - ObjectType.Accessory when allVariants => GamePaths.Accessory.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), - ObjectType.Accessory => GamePaths.Accessory.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), - ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), - ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, + ObjectType.Equipment when allVariants => GamePaths.Mdl.Equipment(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Equipment => GamePaths.Mtrl.Equipment(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Accessory when allVariants => GamePaths.Mdl.Accessory(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Accessory => GamePaths.Mtrl.Accessory(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Weapon => GamePaths.Mtrl.Weapon(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.DemiHuman => GamePaths.Mtrl.DemiHuman(PrimaryId, SecondaryId.Id, EquipSlot, Variant, "a"), - ObjectType.Monster => GamePaths.Monster.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.Monster => GamePaths.Mtrl.Monster(PrimaryId, SecondaryId.Id, Variant, "a"), _ => string.Empty, }; if (path.Length == 0) @@ -51,15 +51,7 @@ public readonly record struct ImcIdentifier( } public string GamePathString() - => ObjectType switch - { - ObjectType.Accessory => GamePaths.Accessory.Imc.Path(PrimaryId), - ObjectType.Equipment => GamePaths.Equipment.Imc.Path(PrimaryId), - ObjectType.DemiHuman => GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), - ObjectType.Monster => GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), - ObjectType.Weapon => GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), - _ => string.Empty, - }; + => GamePaths.Imc.Path(ObjectType, PrimaryId, SecondaryId); public Utf8GamePath GamePath() => Utf8GamePath.FromString(GamePathString(), out var p) ? p : Utf8GamePath.Empty; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index cd36de93..c5406f66 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -17,8 +17,8 @@ public static class CustomizationSwap if (idFrom.Id > byte.MaxValue) throw new Exception($"The Customization ID {idFrom} is too large for {slot}."); - var mdlPathFrom = GamePaths.Character.Mdl.Path(race, slot, idFrom, slot.ToCustomizationType()); - var mdlPathTo = GamePaths.Character.Mdl.Path(race, slot, idTo, slot.ToCustomizationType()); + var mdlPathFrom = GamePaths.Mdl.Customization(race, slot, idFrom, slot.ToCustomizationType()); + var mdlPathTo = GamePaths.Mdl.Customization(race, slot, idTo, slot.ToCustomizationType()); var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); var range = slot == BodySlot.Tail @@ -47,8 +47,8 @@ public static class CustomizationSwap ref string fileName, ref bool dataWasChanged) { variant = slot is BodySlot.Face or BodySlot.Ear ? Variant.None.Id : variant; - var mtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); - var mtrlToPath = GamePaths.Character.Mtrl.Path(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); + var mtrlFromPath = GamePaths.Mtrl.Customization(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); + var mtrlToPath = GamePaths.Mtrl.Customization(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); var newFileName = fileName; newFileName = ItemSwap.ReplaceRace(newFileName, gameRaceTo, race, gameRaceTo != race); @@ -60,7 +60,7 @@ public static class CustomizationSwap var actualMtrlFromPath = mtrlFromPath; if (newFileName != fileName) { - actualMtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, newFileName, out _, out _, variant); + actualMtrlFromPath = GamePaths.Mtrl.Customization(race, slot, idFrom, newFileName, out _, out _, variant); fileName = newFileName; dataWasChanged = true; } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 8c80c91c..5c67df52 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -107,7 +107,7 @@ public static class EquipmentSwap foreach (var child in eqp.ChildSwaps.SelectMany(c => c.WithChildren()).OfType>()) { affectedItems.UnionWith(identifier - .Identify(GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) + .Identify(GamePaths.Mdl.Equipment(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item)); } } @@ -223,11 +223,9 @@ public static class EquipmentSwap public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { - var mdlPathFrom = slotFrom.IsAccessory() - ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) - : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); - var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdlPathFrom = GamePaths.Mdl.Gear(idFrom, gr, slotFrom); + var mdlPathTo = GamePaths.Mdl.Gear(idTo, gr, slotTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -264,9 +262,7 @@ public static class EquipmentSwap } else { - items = identifier.Identify(slotFrom.IsEquipment() - ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) + items = identifier.Identify(GamePaths.Mdl.Gear(idFrom, GenderRace.MidlanderMale, slotFrom)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item) .ToHashSet(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); @@ -324,7 +320,7 @@ public static class EquipmentSwap if (decalId == 0) return null; - var decalPath = GamePaths.Equipment.Decal.Path(decalId); + var decalPath = GamePaths.Tex.EquipDecal(decalId); return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath); } @@ -337,9 +333,9 @@ public static class EquipmentSwap if (vfxId == 0) return null; - var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); + var vfxPathFrom = GamePaths.Avfx.Path(slotFrom, idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); + var vfxPathTo = GamePaths.Avfx.Path(slotTo, idTo, vfxId); var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) @@ -402,14 +398,10 @@ public static class EquipmentSwap if (!fileName.Contains($"{prefix}{idTo.Id:D4}")) return null; - var folderTo = slotTo.IsAccessory() - ? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo) - : GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo); + var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); var pathTo = $"{folderTo}{fileName}"; - var folderFrom = slotFrom.IsAccessory() - ? GamePaths.Accessory.Mtrl.FolderPath(idFrom, variantTo) - : GamePaths.Equipment.Mtrl.FolderPath(idFrom, variantTo); + var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); newFileName = ItemSwap.ReplaceSlot(newFileName, slotTo, slotFrom, slotTo != slotFrom); var pathFrom = $"{folderFrom}{newFileName}"; @@ -457,7 +449,7 @@ public static class EquipmentSwap public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, ref bool dataWasChanged) { - var path = $"shader/sm5/shpk/{shaderName}"; + var path = GamePaths.Shader(shaderName); return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 03abfc45..0049fa12 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -132,14 +132,14 @@ public static class ItemSwap public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId); + var phybPath = GamePaths.Phyb.Customization(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId); + var sklbPath = GamePaths.Sklb.Customization(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index ae57a122..a13dd96b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -124,7 +124,7 @@ public partial class MtrlTab private FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + defaultPath = GamePaths.Shader(Mtrl.ShaderPackage.Name); if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 66db0932..80b10607 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -55,7 +55,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer if (filePath.Length == 0 || !File.Exists(filePath)) throw new FileNotFoundException(); - var gr = GamePaths.ParseRaceCode(filePath); + var gr = Parser.ParseRaceCode(filePath); if (gr is GenderRace.Unknown) throw new Exception($"Could not identify race code from path {filePath}."); var text = File.ReadAllBytes(filePath); @@ -277,7 +277,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer if (!ret) return false; - index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint!.Entries.Length - 1)); + index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint.Entries.Length - 1)); identifier = identifier with { EntryIndex = index }; return true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 1356340c..c9a1d059 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -68,7 +68,7 @@ public partial class ModEditWindow { _dragDropManager.CreateImGuiSource("atchDrag", f => f.Extensions.Contains(".atch"), f => { - var gr = GamePaths.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); + var gr = Parser.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); if (gr is GenderRace.Unknown) return false; From 1ebe4099d6782c60ab3769be2b1801d42bb07480 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 17:51:27 +0100 Subject: [PATCH 2194/2451] Add ImGuiCacheService. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 0b6085ce..3bf047bf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0b6085ce720ffb7c78cf42d4e51861f34db27744 +Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 From deba8ac9101572d3f7245e6a2fe4c8b33a34e0dc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 00:33:56 +0100 Subject: [PATCH 2195/2451] Heavily improve changed item display. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/UiApi.cs | 8 +- Penumbra/Api/ModChangedItemAdapter.cs | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 6 +- .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Communication/ChangedItemClick.cs | 2 +- Penumbra/Communication/ChangedItemHover.cs | 2 +- Penumbra/Meta/Manipulations/AtchIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 10 +- .../Manipulations/GlobalEqpManipulation.cs | 6 +- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- .../Meta/Manipulations/IMetaIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 4 +- Penumbra/Meta/Manipulations/Rsp.cs | 4 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 1 + Penumbra/Mods/Mod.cs | 9 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 3 +- Penumbra/UI/ChangedItemDrawer.cs | 83 +++--- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 253 ++++++++++++++++-- Penumbra/UI/Tabs/ChangedItemsTab.cs | 57 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 6 +- 30 files changed, 360 insertions(+), 126 deletions(-) diff --git a/OtterGui b/OtterGui index 3bf047bf..c347d29d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 +Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf diff --git a/Penumbra.GameData b/Penumbra.GameData index bc339208..955c4e6b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bc339208d1d453582eb146533c572823146a4592 +Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4 diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index 515874c0..b14f67ae 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(IIdentifiedObjectData? data) + private void OnChangedItemHover(IIdentifiedObjectData data) { if (ChangedItemTooltip == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs index 8842f20a..8d2d473c 100644 --- a/Penumbra/Api/ModChangedItemAdapter.cs +++ b/Penumbra/Api/ModChangedItemAdapter.cs @@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference storage) : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); } - private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary + private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary { public IEnumerator> GetEnumerator() => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index a80928d0..42c8b27d 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -23,7 +23,7 @@ public sealed class CollectionCache : IDisposable private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; public readonly CollectionModData ModData = new(); - private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + private readonly SortedList, IIdentifiedObjectData)> _changedItems = []; public readonly ConcurrentDictionary ResolvedFiles = new(); public readonly CustomResourceCache CustomResources; public readonly MetaCache Meta; @@ -43,7 +43,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems { get { @@ -441,7 +441,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 0b38dde8..716b153e 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 1aac4454..2d27f36a 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -12,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 4e72b558..92d770f7 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -10,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs index bce37620..c248c48b 100644 --- a/Penumbra/Meta/Manipulations/AtchIdentifier.cs +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -31,7 +31,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac public override string ToString() => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { // Nothing specific } diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 285f2309..c8423b92 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index c71f2f4d..154aca40 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 007cd02f..8a450eee 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,17 +24,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { case EstType.Hair: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName()); break; case EstType.Face: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName()); break; case EstType.Body: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 6a1ceaea..1365d9d3 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { @@ -86,9 +86,9 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier if (path.Length > 0) identifier.Identify(changedItems, path); else if (Type is GlobalEqpType.DoNotHideVieraHats) - changedItems["All Hats for Viera"] = null; + changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); else if (Type is GlobalEqpType.DoNotHideHrothgarHats) - changedItems["All Hats for Hrothgar"] = null; + changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); } public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 8cd07bfd..5bc81f26 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 999fd906..c897bb2a 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -19,7 +19,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 6e893043..fa726708 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 9dc4fe90..5f91a37c 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -8,8 +8,8 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => changedItems.UpdateCountOrSet($"{SubRace.ToName()} {Attribute.ToFullString()}", () => new IdentifiedName()); public MetaIndex FileIndex() => MetaIndex.HumanCmp; diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 80f3c4c0..90a962b7 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -136,7 +136,7 @@ public sealed class CombiningModGroup : IModGroup public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) => Data[setting.AsIndex].AddDataTo(redirections, manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 96422caf..cc961b0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -53,7 +53,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 2a1854ed..5ec32274 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -131,7 +131,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 0c9aa805..82555314 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -122,7 +122,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ab0c2d4f..c250182a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -107,7 +107,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 4bf22272..130c8fcb 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -139,6 +139,7 @@ public class ModCacheManager : IDisposable, IService mod.ChangedItems.RemoveMachinistOffhands(); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + ++mod.LastChangedItemsUpdate; } private static void UpdateCounts(Mod mod) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 488e3dc1..9829d5a0 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -101,13 +101,14 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; - public int TotalFileCount { get; internal set; } - public int TotalSwapCount { get; internal set; } - public int TotalManipulations { get; internal set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public ushort LastChangedItemsUpdate { get; internal set; } public bool HasOptions { get; internal set; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3482f620..eb9aa93d 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -25,7 +25,6 @@ public class ResourceTreeViewer( private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly CommunicatorService _communicator = communicator; private readonly HashSet _unfolded = []; private readonly Dictionary _filterCache = []; @@ -278,7 +277,7 @@ public class ResourceTreeViewer( if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) - _communicator.SelectTab.Invoke(TabType.Mods, mod); + communicator.SelectTab.Invoke(TabType.Mods, mod); ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index af9782d5..a9070360 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Services; @@ -86,18 +87,20 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + public bool FilterChangedItem(string name, IIdentifiedObjectData data, LowerString filter) => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(IIdentifiedObjectData? data) - => DrawCategoryIcon(data.GetIcon().ToFlag()); + public void DrawCategoryIcon(IIdentifiedObjectData data, float height) + => DrawCategoryIcon(data.GetIcon().ToFlag(), height); public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) + => DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight()); + + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height) { - var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -114,50 +117,50 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - /// - /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item ID in grey if requested. - /// - public void DrawChangedItem(string name, IIdentifiedObjectData? data) + public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked) { - name = data?.ToName(name) ?? name; - using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) - .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) - { - var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) - ? MouseButton.Left - : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); - } + var ret = leftClicked ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(ret, data); + if (!ImGui.IsItemHovered()) + return; - if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using (ImRaii.Group()) - { - _communicator.ChangedItemHover.Invoke(data); - } - - if (ImGui.GetItemRectSize() == Vector2.Zero) - ImGui.TextUnformatted("No actions available."); - } + using var tt = ImUtf8.Tooltip(); + if (data.Count == 1) + ImUtf8.Text("This item is changed through a single effective change.\n"); + else + ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n"); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + ImGui.Separator(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + _communicator.ChangedItemHover.Invoke(data); } /// Draw the model information, right-justified. - public static void DrawModelData(IIdentifiedObjectData? data) + public static void DrawModelData(IIdentifiedObjectData data, float height) { - var additionalData = data?.AdditionalData ?? string.Empty; + var additionalData = data.AdditionalData; if (additionalData.Length == 0) return; - ImGui.SameLine(ImGui.GetContentRegionAvail().X); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X); + } + + /// Draw the model information, right-justified. + public static void DrawModelData(ReadOnlySpan text, float height) + { + if (text.Length == 0) + return; + + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X); } /// Draw a header line with the different icon types to filter them. @@ -276,7 +279,7 @@ public class ChangedItemDrawer : IDisposable, IUiService return true; } - private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) + private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index a7bdadd3..ac4fd167 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,47 +1,268 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui; using OtterGui.Classes; -using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods; +using Penumbra.String; namespace Penumbra.UI.ModsTab; -public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) : ITab, IUiService +public class ModPanelChangedItemsTab( + ModFileSystemSelector selector, + ChangedItemDrawer drawer, + ImGuiCacheService cacheService, + EphemeralConfig config) + : ITab, IUiService { + private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); + + private class ChangedItemsCache + { + private Mod? _lastSelected; + private ushort _lastUpdate; + private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags; + private bool _reset; + public readonly List Data = []; + public bool AnyExpandable { get; private set; } + + public record struct Container + { + public IIdentifiedObjectData Data; + public ByteString Text; + public ByteString ModelData; + public uint Id; + public int Children; + public ChangedItemIconFlag Icon; + public bool Expandable; + public bool Expanded; + public bool Child; + + public static Container Single(string text, IIdentifiedObjectData data) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + + public static Container Parent(string text, IIdentifiedObjectData data, uint id, int children, bool expanded) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = true, + Expanded = expanded, + Data = data, + Id = id, + Children = children, + }; + + public static Container Indent(string text, IIdentifiedObjectData data) + => new() + { + Child = true, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + } + + public void Reset() + => _reset = true; + + public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter) + { + if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset) + return; + + _reset = false; + Data.Clear(); + AnyExpandable = false; + _lastSelected = mod; + _filter = filter; + if (_lastSelected == null) + return; + + _lastUpdate = _lastSelected.LastChangedItemsUpdate; + var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is not IdentifiedItem item) + continue; + + if (!drawer.FilterChangedItem(s, item, LowerString.Empty)) + continue; + + if (tmp.TryGetValue((item.Item.PrimaryId, item.Item.Type), out var p)) + p.Add(item); + else + tmp[(item.Item.PrimaryId, item.Item.Type)] = [item]; + } + + foreach (var list in tmp.Values) + { + list.Sort((i1, i2) => + { + // reversed + var count = i2.Count.CompareTo(i1.Count); + if (count != 0) + return count; + + return string.Compare(i1.Item.Name, i2.Item.Name, StringComparison.Ordinal); + }); + } + + var sortedTmp = tmp.Values.OrderBy(s => s[0].Item.Name).ToArray(); + + var sortedTmpIdx = 0; + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is IdentifiedItem) + continue; + + if (!drawer.FilterChangedItem(s, i, LowerString.Empty)) + continue; + + while (sortedTmpIdx < sortedTmp.Length + && string.Compare(sortedTmp[sortedTmpIdx][0].Item.Name, s, StringComparison.Ordinal) <= 0) + AddList(sortedTmp[sortedTmpIdx++]); + + Data.Add(Container.Single(s, i)); + } + + for (; sortedTmpIdx < sortedTmp.Length; ++sortedTmpIdx) + AddList(sortedTmp[sortedTmpIdx]); + return; + + void AddList(List list) + { + var mainItem = list[0]; + if (list.Count == 1) + { + Data.Add(Container.Single(mainItem.Item.Name, mainItem)); + } + else + { + var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}"); + var expanded = ImGui.GetStateStorage().GetBool(id, false); + Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded)); + AnyExpandable = true; + if (!expanded) + return; + + foreach (var item in list.Skip(1)) + Data.Add(Container.Indent(item.Item.Name, item)); + } + } + } + } + public ReadOnlySpan Label => "Changed Items"u8; public bool IsVisible => selector.Selected!.ChangedItems.Count > 0; + private ImGuiStoragePtr _stateStorage; + + private Vector2 _buttonSize; + public void DrawContent() { + if (cacheService.Cache(_cacheId, () => (new ChangedItemsCache(), "ModPanelChangedItemsCache")) is not { } cache) + return; + drawer.DrawTypeFilter(); + + _stateStorage = ImGui.GetStateStorage(); + cache.Update(selector.Selected, drawer, config.ChangedItemFilter); ImGui.Separator(); - using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); - var height = ImGui.GetFrameHeightWithSpacing(); - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); - ImGuiClip.DrawEndDummy(remainder, height); + if (cache.AnyExpandable) + { + ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y); + ImUtf8.TableSetupColumn("##text"u8, ImGuiTableColumnFlags.WidthStretch); + ImGuiClip.ClippedDraw(cache.Data, DrawContainerExpandable, _buttonSize.Y); + } + else + { + ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing()); + } } - private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) - => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + private void DrawContainerExpandable(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + ImGui.TableNextColumn(); + if (obj.Expandable) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); + if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, + obj.Expanded ? "Hide the other items using the same model." : + obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : + "Show one other item using the same model.", + _buttonSize)) + { + _stateStorage.SetBool(obj.Id, !obj.Expanded); + if (cacheService.TryGetCache(_cacheId, out var cache)) + cache.Reset(); + } + } + else + { + ImGui.Dummy(_buttonSize); + } - private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) + DrawBaseContainer(obj, idx); + } + + private void DrawContainer(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + DrawBaseContainer(obj, idx); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Data); - ImGui.SameLine(); - drawer.DrawChangedItem(kvp.Name, kvp.Data); - ChangedItemDrawer.DrawModelData(kvp.Data); + using var indent = ImRaii.PushIndent(1, obj.Child); + drawer.DrawCategoryIcon(obj.Icon, _buttonSize.Y); + ImGui.SameLine(0, 0); + var clicked = ImUtf8.Selectable(obj.Text.Span, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(obj.Data, clicked); + ChangedItemDrawer.DrawModelData(obj.ModelData.Span, _buttonSize.Y); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 5bac7d35..6cee22d6 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -26,30 +27,36 @@ public class ChangedItemsTab( private LowerString _changedItemFilter = LowerString.Empty; private LowerString _changedItemModFilter = LowerString.Empty; + private Vector2 _buttonSize; public void DrawContent() { collectionHeader.Draw(true); drawer.DrawTypeFilter(); var varWidth = DrawFilters(); - using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); + using var child = ImUtf8.Child("##changedItemsChild"u8, -Vector2.One); if (!child) return; - var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + var skips = ImGuiClip.GetNecessarySkips(_buttonSize.Y); + using var list = ImUtf8.Table("##changedItems"u8, 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) return; const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn("items", flags, 450 * UiHelpers.Scale); - ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); - ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("items"u8, flags, 450 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("mods"u8, flags, varWidth - 140 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("id"u8, flags, 140 * UiHelpers.Scale); var items = collectionManager.Active.Current.ChangedItems; var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); - ImGuiClip.DrawEndDummy(rest, height); + ImGuiClip.DrawEndDummy(rest, _buttonSize.Y); } /// Draw a pair of filters and return the variable width of the flexible column. @@ -67,22 +74,25 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Value.Item2); - ImGui.SameLine(); - drawer.DrawChangedItem(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2, _buttonSize.Y); + ImGui.SameLine(0, 0); + var name = item.Value.Item2.ToName(item.Key); + var clicked = ImUtf8.Selectable(name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(item.Value.Item2, clicked); + ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - ChangedItemDrawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2, _buttonSize.Y); } private void DrawModColumn(SingleArray mods) @@ -90,19 +100,18 @@ public class ChangedItemsTab( if (mods.Count <= 0) return; - var first = mods[0]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); - if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + var first = mods[0]; + if (ImUtf8.Selectable(first.Name.Text, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }) && ImGui.GetIO().KeyCtrl && first is Mod mod) communicator.SelectTab.Invoke(TabType.Mods, mod); - if (ImGui.IsItemHovered()) - { - using var _ = ImRaii.Tooltip(); - ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); - if (mods.Count > 1) - ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); - } + if (!ImGui.IsItemHovered()) + return; + + using var _ = ImRaii.Tooltip(); + ImUtf8.Text("Hold Control and click to jump to mod.\n"u8); + if (mods.Count > 1) + ImUtf8.Text("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8f76a54a..42502290 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -748,7 +748,7 @@ public class DebugTab : Window, ITab, IUiService } private string _changedItemPath = string.Empty; - private readonly Dictionary _changedItems = []; + private readonly Dictionary _changedItems = []; private void DrawChangedItemTest() { diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 5bd3f77c..f744e940 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,7 +19,7 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { @@ -31,7 +31,7 @@ public static class IdentifierExtensions } } - public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData)> changedItems) { for (var i = 0; i < changedItems.Count; i++) { From 26985e01a20c83c46c00a1107bda7fe9292132b3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Feb 2025 23:37:03 +0000 Subject: [PATCH 2196/2451] [CI] Updating repo.json for testing_1.3.4.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index afb2c32d..0b8d89cf 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.4", + "TestingAssemblyVersion": "1.3.4.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 13adbd54667e4eff64812d7e281c7d26dfb9689c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 16:55:50 +0100 Subject: [PATCH 2197/2451] Allow configuration of the changed item display. --- Penumbra/ChangedItemMode.cs | 57 ++++++++++++++ Penumbra/Configuration.cs | 78 ++++++++++++++----- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 26 +++++-- Penumbra/UI/Tabs/SettingsTab.cs | 9 +++ 4 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 Penumbra/ChangedItemMode.cs diff --git a/Penumbra/ChangedItemMode.cs b/Penumbra/ChangedItemMode.cs new file mode 100644 index 00000000..dccffded --- /dev/null +++ b/Penumbra/ChangedItemMode.cs @@ -0,0 +1,57 @@ +using ImGuiNET; +using OtterGui.Text; + +namespace Penumbra; + +public enum ChangedItemMode +{ + GroupedCollapsed, + GroupedExpanded, + Alphabetical, +} + +public static class ChangedItemModeExtensions +{ + public static ReadOnlySpan ToName(this ChangedItemMode mode) + => mode switch + { + ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8, + ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8, + ChangedItemMode.Alphabetical => "Alphabetical"u8, + _ => "Error"u8, + }; + + public static ReadOnlySpan ToTooltip(this ChangedItemMode mode) + => mode switch + { + ChangedItemMode.GroupedCollapsed => + "Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8, + ChangedItemMode.GroupedExpanded => + "Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8, + ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8, + _ => ""u8, + }; + + public static bool DrawCombo(ReadOnlySpan label, ChangedItemMode value, float width, Action setter) + { + ImGui.SetNextItemWidth(width); + using var combo = ImUtf8.Combo(label, value.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var newValue in Enum.GetValues()) + { + var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value); + if (selected) + { + ret = true; + setter(newValue); + } + + ImUtf8.HoverTooltip(newValue.ToTooltip()); + } + + return ret; + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ce86dd4a..939eb122 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -39,11 +39,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool EnableMods { get => _enableMods; - set - { - _enableMods = value; - ModsEnabled?.Invoke(value); - } + set => SetField(ref _enableMods, value, ModsEnabled); } public string ModDirectory { get; set; } = string.Empty; @@ -58,21 +54,22 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AutoSelectCollection { get; set; } = false; - public bool ShowModsInLobby { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public bool HideMachinistOffhandFromChangedItems { get; set; } = true; - public bool DefaultTemporaryMode { get; set; } = false; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool ShowModsInLobby { get; set; } = true; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public bool DefaultTemporaryMode { get; set; } = false; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); @@ -217,4 +214,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetField(ref T field, T value, Action? @event, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(value)) + return false; + + var oldValue = field; + field = value; + try + { + @event?.Invoke(oldValue, field); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} from {oldValue} to {field}:\n{ex}"); + throw; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetField(ref T field, T value, Action? @event, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(value)) + return false; + + field = value; + try + { + @event?.Invoke(field); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} to {field}:\n{ex}"); + throw; + } + + return true; + } } diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index ac4fd167..f97e4d51 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -18,7 +18,7 @@ public class ModPanelChangedItemsTab( ModFileSystemSelector selector, ChangedItemDrawer drawer, ImGuiCacheService cacheService, - EphemeralConfig config) + Configuration config) : ITab, IUiService { private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); @@ -28,6 +28,7 @@ public class ModPanelChangedItemsTab( private Mod? _lastSelected; private ushort _lastUpdate; private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags; + private ChangedItemMode _lastMode; private bool _reset; public readonly List Data = []; public bool AnyExpandable { get; private set; } @@ -90,9 +91,9 @@ public class ModPanelChangedItemsTab( public void Reset() => _reset = true; - public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter) + public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter, ChangedItemMode mode) { - if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset) + if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset && _lastMode == mode) return; _reset = false; @@ -100,12 +101,25 @@ public class ModPanelChangedItemsTab( AnyExpandable = false; _lastSelected = mod; _filter = filter; + _lastMode = mode; if (_lastSelected == null) return; _lastUpdate = _lastSelected.LastChangedItemsUpdate; - var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + if (mode is ChangedItemMode.Alphabetical) + { + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (drawer.FilterChangedItem(s, i, LowerString.Empty)) + Data.Add(Container.Single(s, i)); + } + + return; + } + + var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + var defaultExpansion = _lastMode is ChangedItemMode.GroupedExpanded; foreach (var (s, i) in _lastSelected.ChangedItems) { if (i is not IdentifiedItem item) @@ -165,7 +179,7 @@ public class ModPanelChangedItemsTab( else { var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}"); - var expanded = ImGui.GetStateStorage().GetBool(id, false); + var expanded = ImGui.GetStateStorage().GetBool(id, defaultExpansion); Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded)); AnyExpandable = true; if (!expanded) @@ -196,7 +210,7 @@ public class ModPanelChangedItemsTab( drawer.DrawTypeFilter(); _stateStorage = ImGui.GetStateStorage(); - cache.Update(selector.Selected, drawer, config.ChangedItemFilter); + cache.Update(selector.Selected, drawer, config.Ephemeral.ChangedItemFilter, config.ChangedItemDisplay); ImGui.Separator(); _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4ff0cd42..ba226aa8 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -445,6 +445,15 @@ public class SettingsTab : ITab, IUiService _config.Ephemeral.Save(); } }); + + ChangedItemModeExtensions.DrawCombo("##ChangedItemMode"u8, _config.ChangedItemDisplay, UiHelpers.InputTextWidth.X, v => + { + _config.ChangedItemDisplay = v; + _config.Save(); + }); + ImUtf8.LabeledHelpMarker("Mod Changed Item Display"u8, + "Configure how to display the changed items of a single mod in the mods info panel."u8); + Checkbox("Omit Machinist Offhands in Changed Items", "Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n" + "Changing this triggers a rediscovery of your mods so all changed items can be updated.", From 509f11561aee52acd23a56f7c35d6f4494572384 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 22:21:36 +0100 Subject: [PATCH 2198/2451] Add preferred changed items to mods. --- Penumbra/Mods/Manager/ModDataEditor.cs | 158 ++++++++++++++++-- Penumbra/Mods/Mod.cs | 28 ++-- Penumbra/Mods/ModLocalData.cs | 27 ++- Penumbra/Mods/ModMeta.cs | 12 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 71 +++++++- schemas/local_mod_data-v3.json | 9 + schemas/mod_meta-v3.json | 9 + 7 files changed, 273 insertions(+), 41 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7c48205a..1349b525 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,7 +1,8 @@ using Dalamud.Utility; -using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -9,23 +10,25 @@ namespace Penumbra.Mods.Manager; [Flags] public enum ModDataChangeType : ushort { - None = 0x0000, - Name = 0x0001, - Author = 0x0002, - Description = 0x0004, - Version = 0x0008, - Website = 0x0010, - Deletion = 0x0020, - Migration = 0x0040, - ModTags = 0x0080, - ImportDate = 0x0100, - Favorite = 0x0200, - LocalTags = 0x0400, - Note = 0x0800, - Image = 0x1000, + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, + Image = 0x1000, + DefaultChangedItems = 0x2000, + PreferredChangedItems = 0x4000, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService { public SaveService SaveService => saveService; @@ -35,7 +38,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic string? website) { var mod = new Mod(directory); - mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); mod.Author = author != null ? new LowerString(author) : mod.Author; mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; @@ -175,4 +178,125 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}"); } } + + public void AddPreferredItem(Mod mod, CustomItemId id, bool toDefault, bool cleanExisting) + { + if (CleanExisting(mod.PreferredChangedItems)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (toDefault && CleanExisting(mod.DefaultPreferredItems)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + bool CleanExisting(HashSet items) + { + if (!items.Add(id)) + return false; + + if (!cleanExisting) + return true; + + var it1Exists = itemData.Primary.TryGetValue(id, out var it1); + var it2Exists = itemData.Secondary.TryGetValue(id, out var it2); + var it3Exists = itemData.Tertiary.TryGetValue(id, out var it3); + + foreach (var item in items.ToArray()) + { + if (item == id) + continue; + + if (it1Exists + && itemData.Primary.TryGetValue(item, out var oldItem1) + && oldItem1.PrimaryId == it1.PrimaryId + && oldItem1.Type == it1.Type) + items.Remove(item); + + else if (it2Exists + && itemData.Primary.TryGetValue(item, out var oldItem2) + && oldItem2.PrimaryId == it2.PrimaryId + && oldItem2.Type == it2.Type) + items.Remove(item); + + else if (it3Exists + && itemData.Primary.TryGetValue(item, out var oldItem3) + && oldItem3.PrimaryId == it3.PrimaryId + && oldItem3.Type == it3.Type) + items.Remove(item); + } + + return true; + } + } + + public void RemovePreferredItem(Mod mod, CustomItemId id, bool fromDefault) + { + if (!fromDefault && mod.PreferredChangedItems.Remove(id)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (fromDefault && mod.DefaultPreferredItems.Remove(id)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + } + + public void ClearInvalidPreferredItems(Mod mod) + { + var currentChangedItems = mod.ChangedItems.Values.OfType().Select(i => i.Item.Id).Distinct().ToHashSet(); + var newSet = new HashSet(mod.PreferredChangedItems.Count); + + if (CheckItems(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = newSet; + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + newSet = new HashSet(mod.DefaultPreferredItems.Count); + if (CheckItems(mod.DefaultPreferredItems)) + { + mod.DefaultPreferredItems = newSet; + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + return; + + bool CheckItems(HashSet set) + { + var changes = false; + foreach (var item in set) + { + if (currentChangedItems.Contains(item)) + newSet.Add(item); + else + changes = true; + } + + return changes; + } + } + + public void ResetPreferredItems(Mod mod) + { + if (mod.PreferredChangedItems.SetEquals(mod.DefaultPreferredItems)) + return; + + mod.PreferredChangedItems.Clear(); + mod.PreferredChangedItems.UnionWith(mod.DefaultPreferredItems); + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 9829d5a0..efd92631 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,6 +1,7 @@ using OtterGui; using OtterGui.Classes; using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -47,21 +48,22 @@ public sealed class Mod : IMod => Name.Text; // Meta Data - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public string Image { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = []; + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public string Image { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = []; + public HashSet DefaultPreferredItems { get; internal set; } = []; // Local Data - public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public IReadOnlyList LocalTags { get; internal set; } = []; - public string Note { get; internal set; } = string.Empty; - public bool Favorite { get; internal set; } = false; - + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; internal set; } = []; + public string Note { get; internal set; } = string.Empty; + public HashSet PreferredChangedItems { get; internal set; } = []; + public bool Favorite { get; internal set; } = false; // Options public readonly DefaultSubMod Default; @@ -110,5 +112,5 @@ public sealed class Mod : IMod public int TotalSwapCount { get; internal set; } public int TotalManipulations { get; internal set; } public ushort LastChangedItemsUpdate { get; internal set; } - public bool HasOptions { get; internal set; } + public bool HasOptions { get; internal set; } } diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index d3534391..cc20fad6 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -21,6 +22,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable { nameof(Mod.LocalTags), JToken.FromObject(mod.LocalTags) }, { nameof(Mod.Note), JToken.FromObject(mod.Note) }, { nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) }, + { nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -36,6 +38,8 @@ public readonly struct ModLocalData(Mod mod) : ISavable var favorite = false; var note = string.Empty; + HashSet preferredChangedItems = []; + var save = true; if (File.Exists(dataFile)) try @@ -43,16 +47,21 @@ public readonly struct ModLocalData(Mod mod) : ISavable var text = File.ReadAllText(dataFile); var json = JObject.Parse(text); - importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; - save = false; + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; + preferredChangedItems = (json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values().Select(i => (CustomItemId) i).ToHashSet() ?? mod.DefaultPreferredItems; + save = false; } catch (Exception e) { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } + else + { + preferredChangedItems = mod.DefaultPreferredItems; + } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -64,7 +73,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.ImportDate; } - changes |= ModLocalData.UpdateTags(mod, null, localTags); + changes |= UpdateTags(mod, null, localTags); if (mod.Favorite != favorite) { @@ -78,6 +87,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.Note; } + if (!preferredChangedItems.SetEquals(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = preferredChangedItems; + changes |= ModDataChangeType.PreferredChangedItems; + } + if (save) editor.SaveService.QueueSave(new ModLocalData(mod)); diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 0cebcf81..1b104af4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -25,6 +26,7 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.Version), JToken.FromObject(mod.Version) }, { nameof(Mod.Website), JToken.FromObject(mod.Website) }, { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, + { nameof(Mod.DefaultPreferredItems), JToken.FromObject(mod.DefaultPreferredItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -48,7 +50,7 @@ public readonly struct ModMeta(Mod mod) : ISavable var newFileVersion = json[nameof(FileVersion)]?.Value() ?? 0; // Empty name gets checked after loading and is not allowed. - var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; @@ -56,6 +58,8 @@ public readonly struct ModMeta(Mod mod) : ISavable var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values().Select(i => (CustomItemId)i).ToHashSet() + ?? []; ModDataChangeType changes = 0; if (mod.Name != newName) @@ -94,6 +98,12 @@ public readonly struct ModMeta(Mod mod) : ISavable mod.Website = newWebsite; } + if (!mod.DefaultPreferredItems.SetEquals(defaultItems)) + { + changes |= ModDataChangeType.DefaultChangedItems; + mod.DefaultPreferredItems = defaultItems; + } + if (newFileVersion != FileVersion) if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion)) { diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index f97e4d51..700f1d66 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.String; namespace Penumbra.UI.ModsTab; @@ -18,7 +19,8 @@ public class ModPanelChangedItemsTab( ModFileSystemSelector selector, ChangedItemDrawer drawer, ImGuiCacheService cacheService, - Configuration config) + Configuration config, + ModDataEditor dataEditor) : ITab, IUiService { private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); @@ -77,7 +79,7 @@ public class ModPanelChangedItemsTab( => new() { Child = true, - Text = ByteString.FromStringUnsafe(data.ToName(text), false), + Text = ByteString.FromStringUnsafe(data.ToName(text), false), ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), Icon = data.GetIcon().ToFlag(), Expandable = false, @@ -93,7 +95,11 @@ public class ModPanelChangedItemsTab( public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter, ChangedItemMode mode) { - if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset && _lastMode == mode) + if (mod == _lastSelected + && _lastSelected!.LastChangedItemsUpdate == _lastUpdate + && _filter == filter + && !_reset + && _lastMode == mode) return; _reset = false; @@ -138,6 +144,12 @@ public class ModPanelChangedItemsTab( { list.Sort((i1, i2) => { + // reversed + var preferred = _lastSelected.PreferredChangedItems.Contains(i2.Item.Id) + .CompareTo(_lastSelected.PreferredChangedItems.Contains(i1.Item.Id)); + if (preferred != 0) + return preferred; + // reversed var count = i2.Count.CompareTo(i1.Count); if (count != 0) @@ -217,6 +229,7 @@ public class ModPanelChangedItemsTab( .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); @@ -241,7 +254,6 @@ public class ModPanelChangedItemsTab( ImGui.TableNextColumn(); if (obj.Expandable) { - using var color = ImRaii.PushColor(ImGuiCol.Button, 0); if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, obj.Expanded ? "Hide the other items using the same model." : obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : @@ -253,6 +265,10 @@ public class ModPanelChangedItemsTab( cache.Reset(); } } + else if (obj is { Child: true, Data: IdentifiedItem item }) + { + DrawPreferredButton(item, idx); + } else { ImGui.Dummy(_buttonSize); @@ -267,6 +283,53 @@ public class ModPanelChangedItemsTab( DrawBaseContainer(obj, idx); } + private void DrawPreferredButton(IdentifiedItem item, int idx) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Star, "Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8, _buttonSize, + false, ImGui.GetColorU32(ImGuiCol.TextDisabled, 0.1f))) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true); + using var context = ImUtf8.PopupContextItem("StarContext"u8, ImGuiPopupFlags.MouseButtonRight); + if (!context) + return; + + if (cacheService.TryGetCache(_cacheId, out var cache)) + for (--idx; idx >= 0; --idx) + { + if (!cache.Data[idx].Expanded) + continue; + + if (cache.Data[idx].Data is IdentifiedItem it) + { + if (selector.Selected!.PreferredChangedItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Local Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, false); + if (selector.Selected!.DefaultPreferredItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Default Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, true); + } + + break; + } + + var enabled = !selector.Selected!.DefaultPreferredItems.Contains(item.Item.Id); + if (enabled) + { + if (ImUtf8.MenuItem("Add to Local and Default Preferred Changed Items"u8)) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, true, true); + } + else + { + if (ImUtf8.MenuItem("Remove from Default Preferred Changed Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, item.Item.Id, true); + } + + if (ImUtf8.MenuItem("Reset Local Preferred Items to Default"u8)) + dataEditor.ResetPreferredItems(selector.Selected!); + + if (ImUtf8.MenuItem("Clear Local and Default Preferred Items not Changed by the Mod"u8)) + dataEditor.ClearInvalidPreferredItems(selector.Selected!); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json index bf5d1311..c50e130e 100644 --- a/schemas/local_mod_data-v3.json +++ b/schemas/local_mod_data-v3.json @@ -26,6 +26,15 @@ "Favorite": { "description": "Whether the mod is favourited by the user.", "type": "boolean" + }, + "PreferredChangedItems": { + "description": "Preferred items to list as the main item of a group.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [ "FileVersion" ] diff --git a/schemas/mod_meta-v3.json b/schemas/mod_meta-v3.json index a926b49e..ed63a228 100644 --- a/schemas/mod_meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -43,6 +43,15 @@ "minLength": 1 }, "uniqueItems": true + }, + "DefaultPreferredItems": { + "description": "Default preferred items to list as the main item of a group managed by the mod creator.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [ From cda9b1df655bb4aceed78f47a1a459b35cdc3692 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 22:34:50 +0100 Subject: [PATCH 2199/2451] Fix weapon identification bug. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 955c4e6b..a21c1467 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4 +Subproject commit a21c146790b370bd58b0f752385ae153f7e769c0 From 34d51b66aa53d3245e1c37960878e0492f9fdbcf Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Mar 2025 21:37:03 +0000 Subject: [PATCH 2200/2451] [CI] Updating repo.json for testing_1.3.4.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0b8d89cf..b098593a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.5", + "TestingAssemblyVersion": "1.3.4.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0b0c92eb098b50d4631fb0ba2d65db5f01273247 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Mar 2025 14:27:40 +0100 Subject: [PATCH 2201/2451] Some cleanup. --- Penumbra/Import/Structs/TexToolsStructs.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Structs/TexToolsStructs.cs b/Penumbra/Import/Structs/TexToolsStructs.cs index bf3bccd8..f5b5ef4a 100644 --- a/Penumbra/Import/Structs/TexToolsStructs.cs +++ b/Penumbra/Import/Structs/TexToolsStructs.cs @@ -26,7 +26,7 @@ public class SimpleMod public class ModPackPage { public int PageIndex = 0; - public ModGroup[] ModGroups = Array.Empty(); + public ModGroup[] ModGroups = []; } [Serializable] @@ -34,7 +34,7 @@ public class ModGroup { public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; - public OptionList[] OptionList = Array.Empty(); + public OptionList[] OptionList = []; public string Description = string.Empty; } @@ -44,7 +44,7 @@ public class OptionList public string Name = string.Empty; public string Description = string.Empty; public string ImagePath = string.Empty; - public SimpleMod[] ModsJsons = Array.Empty(); + public SimpleMod[] ModsJsons = []; public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; public bool IsChecked = false; @@ -59,8 +59,8 @@ public class ExtendedModPack public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; public string Url = string.Empty; - public ModPackPage[] ModPackPages = Array.Empty(); - public SimpleMod[] SimpleModsList = Array.Empty(); + public ModPackPage[] ModPackPages = []; + public SimpleMod[] SimpleModsList = []; } [Serializable] @@ -72,5 +72,5 @@ public class SimpleModPack public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; public string Url = string.Empty; - public SimpleMod[] SimpleModsList = Array.Empty(); + public SimpleMod[] SimpleModsList = []; } From 7cf0367361934ae063c8b5c6d826235a554a39e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:39:25 +0100 Subject: [PATCH 2202/2451] Try moving extracted folders 3 times for unknown issues. --- Penumbra/Import/TexToolsImporter.Archives.cs | 39 +++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index febbe179..8166dea7 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -96,17 +96,36 @@ public partial class TexToolsImporter _token.ThrowIfCancellationRequested(); var oldName = _currentModDirectory.FullName; - // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. - if (leadDir) + + // Try renaming the folder three times because sometimes we get AccessDenied here for some unknown reason. + const int numTries = 3; + for (var i = 1;; ++i) { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); - Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); - Directory.Delete(oldName); - } - else - { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); - Directory.Move(oldName, _currentModDirectory.FullName); + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. + try + { + if (leadDir) + { + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); + Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); + Directory.Delete(oldName); + } + else + { + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); + Directory.Move(oldName, _currentModDirectory.FullName); + } + } + catch (IOException io) + { + if (i == numTries) + throw; + + Penumbra.Log.Warning($"Error when renaming the extracted mod, try {i}/{numTries}: {io.Message}."); + continue; + } + + break; } _currentModDirectory.Refresh(); From 1afbbfef78f7ec295bc5ccb7cfd15b6a23b74eae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:39:41 +0100 Subject: [PATCH 2203/2451] Update NuGet packages. --- Penumbra/packages.lock.json | 87 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 5b868212..9aa1ebd5 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -13,67 +13,68 @@ }, "PeNet": { "type": "Direct", - "requested": "[4.0.5, )", - "resolved": "4.0.5", - "contentHash": "/OUfRnMG8STVuK8kTpdfm+WQGTDoUiJnI845kFw4QrDmv2gYourmSnH84pqVjHT1YHBSuRfCzfioIpHGjFJrGA==", + "requested": "[4.1.1, )", + "resolved": "4.1.1", + "contentHash": "TiRyOVcg1Bh2FyP6Dm2NEiYzemSlQderhaxuH3XWNyTYsnHrm1n/xvoTftgMwsWD4C/3kTqJw93oZOvHojJfKg==", "dependencies": { "PeNet.Asn1": "2.0.1", - "System.Security.Cryptography.Pkcs": "8.0.0" + "System.Security.Cryptography.Pkcs": "8.0.1" } }, "SharpCompress": { "type": "Direct", - "requested": "[0.37.2, )", - "resolved": "0.37.2", - "contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==", + "requested": "[0.39.0, )", + "resolved": "0.39.0", + "contentHash": "0esqIUDlg68Z7+Weuge4QzEvNtawUO4obTJFL7xuf4DBHMxVRr+wbNgiX9arMrj3kGXQSvLe0zbZG3oxpkwJOA==", "dependencies": { - "ZstdSharp.Port": "0.8.0" + "System.Buffers": "4.6.0", + "ZstdSharp.Port": "0.8.4" } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w==" + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "su+Flcg2g6GgOIgulRGBDMHA6zY5NBx6NYH1Ayd6iBbSbwspHsN2VQgZfANgJy92cBf7qtpjC0uMiShbO+TEEg==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vkEuf8ch76NNgZXU/3zoXTIXRO0o14H3aRoSFzcuUQb0PTxvV6jEfmWkUVO6JtLDuFCIimqZaf3hdxr32ltpfQ==", "dependencies": { - "SharpGLTF.Runtime": "1.0.1" + "SharpGLTF.Runtime": "1.0.3" } }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" + "requested": "[3.1.7, )", + "resolved": "3.1.7", + "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" }, "System.Formats.Asn1": { "type": "Direct", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "OKWHCPYQr/+cIoO8EVjFn7yFyiT8Mnf1wif/5bYGsqxQV6PrwlX2HQ9brZNx57ViOvRe4ing1xgHCKl/5Ko8xg==" }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2024.2.0", - "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "resolved": "9.0.2", + "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + "resolved": "9.0.2", + "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" }, "PeNet.Asn1": { "type": "Transitive", @@ -82,19 +83,21 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==", + "resolved": "1.0.3", + "contentHash": "W0bg2WyXlcSAJVu153hNUNm+BU4RP46yLwGD4099hSm8dsXG/H+J95PBoLJbIq8KGVkUWvfM0+XWHoEkCyd50A==", "dependencies": { - "SharpGLTF.Core": "1.0.1" + "SharpGLTF.Core": "1.0.3" } }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", - "dependencies": { - "System.Formats.Asn1": "8.0.0" - } + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" }, "System.ValueTuple": { "type": "Transitive", @@ -111,14 +114,14 @@ }, "ZstdSharp.Port": { "type": "Transitive", - "resolved": "0.8.0", - "contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA==" + "resolved": "0.8.4", + "contentHash": "eieSXq3kakCUXbgdxkKaRqWS6hF0KBJcqok9LlDCs60GOyrynLvPOcQ0pRw7shdPF7lh/VepJ9cP9n9HHc759g==" }, "ottergui": { "type": "Project", "dependencies": { - "JetBrains.Annotations": "[2024.2.0, )", - "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" + "JetBrains.Annotations": "[2024.3.0, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" } }, "penumbra.api": { @@ -131,8 +134,8 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.3.0, )", - "Penumbra.String": "[1.0.4, )" + "Penumbra.Api": "[5.6.0, )", + "Penumbra.String": "[1.0.5, )" } }, "penumbra.string": { From 6eacc82dcdb5bb26f54421de239ea0be4eec9750 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:48:41 +0100 Subject: [PATCH 2204/2451] Update references. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Penumbra.csproj | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index c347d29d..13f1a90b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf +Subproject commit 13f1a90b88d2b8572480748a209f957b70d6a46f diff --git a/Penumbra.Api b/Penumbra.Api index 70f04683..404c8aaa 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb +Subproject commit 404c8aaa5115925006963baa118bf710c7953380 diff --git a/Penumbra.GameData b/Penumbra.GameData index a21c1467..96163f79 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a21c146790b370bd58b0f752385ae153f7e769c0 +Subproject commit 96163f79e13c7d52cc36cdd82ab4e823763f4f31 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 9b613729..b4266aeb 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -87,13 +87,13 @@ - + - - - - - + + + + + From eab98ec0e4cefaeff12fccb55a6fdc5c36cbfde3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 14:41:45 +0100 Subject: [PATCH 2205/2451] 1.3.5.0 --- Penumbra/UI/Changelog.cs | 135 ++++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 993ace62..87dd101d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -58,20 +58,65 @@ public class PenumbraChangelog : IUiService Add1_3_2_0(Changelog); Add1_3_3_0(Changelog); Add1_3_4_0(Changelog); - } - + Add1_3_5_0(Changelog); + } + #region Changelogs + private static void Add1_3_5_0(Changelog log) + => log.NextVersion("Version 1.3.5.0") + .RegisterImportant( + "Redirections of unsupported file types like .atch will now produce warnings when they are enabled. Please update mods still containing them or request updates from their creators.") + .RegisterEntry("You can now import .atch in the Meta section of advanced editing to add their non-default changes to the mod.") + .RegisterHighlight("Added an option in settings and in the collection bar in the mod tab to always use temporary settings.") + .RegisterEntry( + "While this option is enabled, all changes you make in the current collection will be applied as temporary changes, and you have to use Turn Permanent to make them permanent.", + 1) + .RegisterEntry( + "This should be useful for trying out new mods without needing to reset their settings later, or for creating mod associations in Glamourer from them.", + 1) + .RegisterEntry( + "Added a context menu entry on the mod selector blank-space context menu to clear all temporary settings made manually.") + .RegisterHighlight( + "Resource Trees now consider some additional files like decals, and improved the quick-import behaviour for some files that should not generally be modded.") + .RegisterHighlight("The Changed Item display for single mods has been heavily improved.") + .RegisterEntry("Any changed item will now show how many individual edits are affecting it in the mod in its tooltip.", 1) + .RegisterEntry("Equipment pieces are now grouped by their model id, reducing clutter.", 1) + .RegisterEntry( + "The primary equipment piece displayed is the one with the most changes affecting it, but can be configured to a specific item by the mod creator and locally.", + 1) + .RegisterEntry( + "Preferred changed items stored in the mod will be shared when exporting the mod, and used as the default for local preferences, which will not be shared.", + 2) + .RegisterEntry( + "You can configure whether groups are automatically collapsed or expanded, or remove grouping entirely in the settings.", 1) + .RegisterHighlight("Fixed support for model import/export with more than one UV.") + .RegisterEntry("Added some IPC relating to changed items.") + .RegisterEntry("Skeleton and Physics changes should now be identified in Changed Items.") + .RegisterEntry("Item Swaps will now also correctly swap EQP entries of multi-slot pieces.") + .RegisterEntry("Meta edit transmission through IPC should be a lot more efficient than before.") + .RegisterEntry("Fixed an issue with incognito names in some cutscenes.") + .RegisterEntry("Newly extracted mod folders will now try to rename themselves three times before being considered a failure."); + private static void Add1_3_4_0(Changelog log) => log.NextVersion("Version 1.3.4.0") - .RegisterHighlight("Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") - .RegisterEntry("This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", 1) + .RegisterHighlight( + "Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") + .RegisterEntry( + "This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", + 1) .RegisterHighlight("Added a new option group type: Combining Groups.") - .RegisterEntry("A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", 1) - .RegisterEntry("Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", 1) - .RegisterEntry("Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") + .RegisterEntry( + "A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", + 1) + .RegisterEntry( + "Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", + 1) + .RegisterEntry( + "Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") .RegisterEntry("Added a display of the number of selected files and folders to the multi mod selection.") - .RegisterEntry("Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") + .RegisterEntry( + "Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") .RegisterEntry("Updated the Bone and Material limits in the Model Importer.") .RegisterEntry("Improved handling of IMC and Material files loaded asynchronously.") .RegisterEntry("Added IPC functionality to query temporary settings.") @@ -86,49 +131,75 @@ public class PenumbraChangelog : IUiService private static void Add1_3_3_0(Changelog log) => log.NextVersion("Version 1.3.3.0") .RegisterHighlight("Added Temporary Settings to collections.") - .RegisterEntry("Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", 1) - .RegisterEntry("This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", 1) - .RegisterEntry("More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", 1) - .RegisterEntry("As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", 1) - .RegisterEntry("This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", 1) - .RegisterHighlight("Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") - .RegisterEntry("Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") - .RegisterHighlight("Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") - .RegisterEntry("The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") + .RegisterEntry( + "Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", + 1) + .RegisterEntry( + "This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", + 1) + .RegisterEntry( + "More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", + 1) + .RegisterEntry( + "As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", + 1) + .RegisterEntry( + "This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", + 1) + .RegisterHighlight( + "Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") + .RegisterEntry( + "Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") + .RegisterHighlight( + "Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") + .RegisterEntry( + "The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") .RegisterEntry("When creating new mods with Item Swap, the attributed author of the resulting mod was improved.") .RegisterEntry("Fixed an issue with rings in the On-Screen tab and in the data sent over to other plugins via IPC.") - .RegisterEntry("Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") + .RegisterEntry( + "Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") .RegisterEntry("Fixed some ImGui assertions."); private static void Add1_3_2_0(Changelog log) => log.NextVersion("Version 1.3.2.0") .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") .RegisterEntry("Those ATCH manipulations should be shared via Mare Synchronos.", 1) - .RegisterEntry("This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", 1) - .RegisterEntry("Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") + .RegisterEntry( + "This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", + 1) + .RegisterEntry( + "Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") .RegisterEntry("Added some right-click context menu copy options in the File Redirections editor for paths.") .RegisterHighlight("Added the option to change a specific mod's settings via chat commands by using '/penumbra mod settings'.") .RegisterEntry("Fixed issues with the copy-pasting of meta manipulations.") .RegisterEntry("Fixed some other issues related to meta manipulations.") - .RegisterEntry("Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); + .RegisterEntry( + "Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); private static void Add1_3_1_0(Changelog log) => log.NextVersion("Version 1.3.1.0") .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") - .RegisterImportant("There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") - .RegisterEntry("If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", 1) - .RegisterImportant("The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") + .RegisterImportant( + "There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") + .RegisterEntry( + "If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", + 1) + .RegisterImportant( + "The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") .RegisterEntry("A better way for modular modding of .atch files via meta changes will release to the testing branch soonish.", 1) .RegisterHighlight("Temporary collections (as created by Mare) will now always respect ownership.") - .RegisterEntry("This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", 1) - .RegisterEntry("The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") + .RegisterEntry( + "This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", + 1) + .RegisterEntry( + "The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") .RegisterEntry("Fixed issues with EQP entries being labeled wrongly and global EQP not changing all required values for earrings.") .RegisterEntry("Fixed an issue with global EQP changes of a mod being reset upon reloading the mod.") .RegisterEntry("Fixed another issue with left rings and mare synchronization / the on-screen tab.") .RegisterEntry("Maybe fixed some issues with characters appearing in the login screen being misidentified.") .RegisterEntry("Some improvements for debug visualization have been made."); - + private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") @@ -138,16 +209,20 @@ public class PenumbraChangelog : IUiService .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1) .RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.") .RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1) - .RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") + .RegisterHighlight( + "IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") .RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1) .RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).") .RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.") .RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1) .RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.") .RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.") - .RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") + .RegisterEntry( + "Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") .RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod") - .RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1) + .RegisterEntry( + "Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", + 1) .RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.") .RegisterEntry("Paths from the resource logger can now be copied.") .RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.") From 1d70be8060f92e3c7ad9fdfd671a42b7437774bf Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 9 Mar 2025 22:16:58 +0000 Subject: [PATCH 2206/2451] [CI] Updating repo.json for 1.3.5.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index b098593a..8de11f0e 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.6", + "AssemblyVersion": "1.3.5.0", + "TestingAssemblyVersion": "1.3.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 93b0996794ea68104cc0f93bfb9ab826a91ec318 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Mar 2025 18:12:54 +0100 Subject: [PATCH 2207/2451] Add chat command to clear temporary settings. --- Penumbra/CommandHandler.cs | 47 ++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index dee46e32..9f681da2 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -75,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService _ = argumentList[0].ToLowerInvariant() switch { - "window" => ToggleWindow(arguments), - "enable" => SetPenumbraState(arguments, true), - "disable" => SetPenumbraState(arguments, false), - "toggle" => SetPenumbraState(arguments, null), - "reload" => Reload(arguments), - "redraw" => Redraw(arguments), - "lockui" => SetUiLockState(arguments), - "size" => SetUiMinimumSize(arguments), - "debug" => SetDebug(arguments), - "collection" => SetCollection(arguments), - "mod" => SetMod(arguments), - "bulktag" => SetTag(arguments), - "knowledge" => HandleKnowledge(arguments), - _ => PrintHelp(argumentList[0]), + "window" => ToggleWindow(arguments), + "enable" => SetPenumbraState(arguments, true), + "disable" => SetPenumbraState(arguments, false), + "toggle" => SetPenumbraState(arguments, null), + "reload" => Reload(arguments), + "redraw" => Redraw(arguments), + "lockui" => SetUiLockState(arguments), + "size" => SetUiMinimumSize(arguments), + "debug" => SetDebug(arguments), + "collection" => SetCollection(arguments), + "mod" => SetMod(arguments), + "bulktag" => SetTag(arguments), + "clearsettings" => ClearSettings(arguments), + "knowledge" => HandleKnowledge(arguments), + _ => PrintHelp(argumentList[0]), }; } @@ -126,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService _chat.Print(new SeStringBuilder() .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("clearsettings", + "Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.") + .BuiltString); + return true; + } + + private bool ClearSettings(string arguments) + { + if (arguments.Trim().ToLowerInvariant() is "all") + foreach (var collection in _collectionManager.Storage) + _collectionEditor.ClearTemporarySettings(collection); + else + _collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current); + return true; } @@ -416,7 +432,8 @@ public class CommandHandler : IDisposable, IApiService var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (split2.Length < 2) { - _chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); + _chat.Print( + "Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); return false; } From e5620e17e0f468ab6331e9dbf347e881c592cb5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 01:20:36 +0100 Subject: [PATCH 2208/2451] Improve texture saving --- Penumbra/Import/Textures/TextureManager.cs | 43 ++++++++++++------ Penumbra/Penumbra.csproj | 4 ++ .../Materials/MtrlTab.LivePreview.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 21 +++++++-- Penumbra/lib/OtterTex.dll | Bin 41984 -> 42496 bytes 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 7118f8af..6adf5861 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -6,15 +7,17 @@ using OtterGui.Log; using OtterGui.Services; using OtterGui.Tasks; using OtterTex; +using SharpDX.Direct3D11; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; +using DxgiDevice = SharpDX.DXGI.Device; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider) +public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder) : SingleTaskQueue, IDisposable, IService { private readonly Logger _logger = logger; @@ -47,11 +50,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, input, output)); public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, image, path, rgba, width, height)); private Task Enqueue(IAction action) { @@ -156,27 +159,30 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur private readonly string _outputPath; private readonly ImageInputData _input; private readonly CombinedTexture.TextureSaveType _type; + private readonly Device? _device; private readonly bool _mipMaps; private readonly bool _asTex; - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, - string output) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + string input, string output) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, - string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } @@ -201,8 +207,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, _device, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, _device, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -320,8 +326,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, - int width = 0, int height = 0) + public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel, + byte[]? rgba = null, int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) { @@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, cancel); + return CreateCompressed(dds, mipMaps, bc7, device, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, cancel); + return CreateCompressed(scratch, mipMaps, bc7, device, cancel); } default: return new BaseImage(); } @@ -384,7 +390,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel) { var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) @@ -398,6 +404,15 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur input = AddMipMaps(input, mipMaps); cancel.ThrowIfCancellationRequested(); + // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. + if (device is not null && format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + { + var dxgiDevice = device.QueryInterface(); + + using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); + return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); + } + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b4266aeb..870865da 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -80,6 +80,10 @@ $(DalamudLibPath)SharpDX.Direct3D11.dll False + + $(DalamudLibPath)SharpDX.DXGI.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 01a40980..5025bafd 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -138,7 +138,7 @@ public partial class MtrlTab foreach (var constant in Mtrl.ShaderPackage.Constants) { var values = Mtrl.GetConstantValue(constant); - if (values != null) + if (values != []) SetMaterialParameter(constant.Id, 0, values); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index c08e8a8e..d0764808 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -134,7 +134,7 @@ public partial class ModEditWindow tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -159,7 +159,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -169,7 +169,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -180,7 +180,7 @@ public partial class ModEditWindow || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } } @@ -235,7 +235,7 @@ public partial class ModEditWindow if (a) { _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, b); + AddChangeTask(b); if (b == _left.Path) AddReloadTask(_left.Path, false); else if (b == _right.Path) @@ -245,6 +245,17 @@ public partial class ModEditWindow _forceTextureStartPath = false; } + private void AddChangeTask(string path) + { + _center.SaveTask.ContinueWith(t => + { + if (!t.IsCompletedSuccessfully) + return; + + _framework.RunOnFrameworkThread(() => InvokeChange(Mod, path)); + }, TaskScheduler.Default); + } + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 29912e6215ff28a7959b73aa1e00cc9ebb919d76..c137aee184d789102648a05ecc24c0437df54ba1 100644 GIT binary patch delta 16738 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 delta 16281 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 Date: Wed, 12 Mar 2025 15:18:20 +0100 Subject: [PATCH 2209/2451] Simplify passing of the device (suggested by @rootdarkarchon) --- Penumbra/Import/Textures/TextureManager.cs | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 6adf5861..8fab097e 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -50,11 +50,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) - => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, input, output)); + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, image, path, rgba, width, height)); + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); private Task Enqueue(IAction action) { @@ -159,30 +159,27 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur private readonly string _outputPath; private readonly ImageInputData _input; private readonly CombinedTexture.TextureSaveType _type; - private readonly Device? _device; private readonly bool _mipMaps; private readonly bool _asTex; - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, - string input, string output) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, + string output) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; _type = type; - _device = device; _mipMaps = mipMaps; _asTex = asTex; } - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, - BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, + string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; _type = type; - _device = device; _mipMaps = mipMaps; _asTex = asTex; } @@ -207,8 +204,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, _device, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, _device, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -326,8 +323,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel, - byte[]? rgba = null, int width = 0, int height = 0) + public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) { @@ -337,12 +334,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, device, cancel); + return CreateCompressed(dds, mipMaps, bc7, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, device, cancel); + return CreateCompressed(scratch, mipMaps, bc7, cancel); } default: return new BaseImage(); } @@ -390,7 +387,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel) + public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) { var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) @@ -405,8 +402,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur input = AddMipMaps(input, mipMaps); cancel.ThrowIfCancellationRequested(); // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. - if (device is not null && format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { + var device = uiBuilder.Device; var dxgiDevice = device.QueryInterface(); using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); From 442ae960cf429ddf9703252615916acf3fa0d679 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 20:03:53 +0100 Subject: [PATCH 2210/2451] Add encoding support for BC1, BC4 and BC5 --- Penumbra/Import/Textures/CombinedTexture.cs | 3 +++ Penumbra/Import/Textures/TextureManager.cs | 16 +++++++++------- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 12 +++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c1a22088..f5f921be 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -6,7 +6,10 @@ public partial class CombinedTexture : IDisposable { AsIs, Bitmap, + BC1, BC3, + BC4, + BC5, BC7, } diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 8fab097e..0c85f5be 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -204,8 +204,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -323,7 +326,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) @@ -334,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, cancel); + return CreateCompressed(dds, mipMaps, format, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, cancel); + return CreateCompressed(scratch, mipMaps, format, cancel); } default: return new BaseImage(); } @@ -387,9 +390,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) return input; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index d0764808..274c216b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -26,9 +26,15 @@ public partial class ModEditWindow ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), ("RGBA (Uncompressed)", "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), - ("BC3 (Simple Compression)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality."), - ("BC7 (Complex Compression)", + ("BC1 (Simple Compression for Opaque RGB)", + "Save the current texture compressed via BC1/DXT1 compression. This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha."), + ("BC3 (Simple Compression for RGBA)", + "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA."), + ("BC4 (Simple Compression for Opaque Grayscale)", + "Save the current texture compressed via BC4 compression. This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha."), + ("BC5 (Simple Compression for Opaque RG)", + "Save the current texture compressed via BC5 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha."), + ("BC7 (Complex Compression for RGBA)", "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), }; From 4093228e6150c87cc3968ff0371f6a8f64cfcf36 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 23:04:57 +0100 Subject: [PATCH 2211/2451] Improve wording of block compressions (suggested by @Theo-Asterio) --- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 274c216b..4664372e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -25,17 +25,17 @@ public partial class ModEditWindow { ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), ("RGBA (Uncompressed)", - "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), + "Save the current texture as an uncompressed BGRA bitmap.\nThis requires the most space but technically offers the best quality."), ("BC1 (Simple Compression for Opaque RGB)", - "Save the current texture compressed via BC1/DXT1 compression. This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha."), + "Save the current texture compressed via BC1/DXT1 compression.\nThis offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha.\n\nCan be used for diffuse maps and equipment textures to save extra space."), ("BC3 (Simple Compression for RGBA)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA."), + "Save the current texture compressed via BC3/DXT5 compression.\nThis offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA.\n\nGeneric format that can be used for most textures."), ("BC4 (Simple Compression for Opaque Grayscale)", - "Save the current texture compressed via BC4 compression. This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha."), + "Save the current texture compressed via BC4 compression.\nThis offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha.\n\nCan be used for face paints and legacy marks."), ("BC5 (Simple Compression for Opaque RG)", - "Save the current texture compressed via BC5 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha."), + "Save the current texture compressed via BC5 compression.\nThis offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha.\n\nRecommended for index maps, unrecommended for normal maps."), ("BC7 (Complex Compression for RGBA)", - "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), + "Save the current texture compressed via BC7 compression.\nThis offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while.\n\nGeneric format that can be used for most textures."), }; private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize) From cda6a4c4202866deb8d26fff7726d4c2e99f450a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Mar 2025 00:13:01 +0100 Subject: [PATCH 2212/2451] Make preferred changed item star more noticeable, and make the color configurable. --- Penumbra/UI/Classes/Colors.cs | 2 ++ Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 4c0d1694..9c15ceb8 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -34,6 +34,7 @@ public enum ColorId : short PredefinedTagAdd, PredefinedTagRemove, TemporaryModSettingsTint, + ChangedItemPreferenceStar, NoTint, } @@ -110,6 +111,7 @@ public static class Colors ColorId.TemporaryModSettingsTint => ( 0x30FF0000, "Mod with Temporary Settings", "A mod that has temporary settings. This color is used as a tint for the regular state colors." ), ColorId.NewModTint => ( 0x8000FF00, "New Mod Tint", "A mod that was newly imported or created during this session and has not been enabled yet. This color is used as a tint for the regular state colors."), ColorId.NoTint => ( 0x00000000, "No Tint", "The default tint for all mods."), + ColorId.ChangedItemPreferenceStar => ( 0x30FFFFFF, "Preferred Changed Item Star", "The color of the star button in the mod panel's changed items tab to prioritize specific items."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 700f1d66..f70d63d2 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -12,6 +12,7 @@ using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.String; +using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; @@ -213,6 +214,7 @@ public class ModPanelChangedItemsTab( private ImGuiStoragePtr _stateStorage; private Vector2 _buttonSize; + private uint _starColor; public void DrawContent() { @@ -236,6 +238,7 @@ public class ModPanelChangedItemsTab( if (!table) return; + _starColor = ColorId.ChangedItemPreferenceStar.Value(); if (cache.AnyExpandable) { ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y); @@ -286,7 +289,7 @@ public class ModPanelChangedItemsTab( private void DrawPreferredButton(IdentifiedItem item, int idx) { if (ImUtf8.IconButton(FontAwesomeIcon.Star, "Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8, _buttonSize, - false, ImGui.GetColorU32(ImGuiCol.TextDisabled, 0.1f))) + false, _starColor)) dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true); using var context = ImUtf8.PopupContextItem("StarContext"u8, ImGuiPopupFlags.MouseButtonRight); if (!context) From 87f44d7a880203ee40a03c4a7e660e68ac6444e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Mar 2025 00:13:57 +0100 Subject: [PATCH 2213/2451] Some minor parser fixes thanks to Anna. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 96163f79..c59dd2e6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 96163f79e13c7d52cc36cdd82ab4e823763f4f31 +Subproject commit c59dd2e6724be71dfe6ade11dacf405f29634fde From dc47a08988d81844738051072a9e96607f5da9c7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 13 Mar 2025 23:20:54 +0000 Subject: [PATCH 2214/2451] [CI] Updating repo.json for testing_1.3.5.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8de11f0e..1617a879 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.5.0", - "TestingAssemblyVersion": "1.3.5.0", + "TestingAssemblyVersion": "1.3.5.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.5.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0213096c58e0f3a232f1b1602b88970c37c8a624 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:45:42 +0100 Subject: [PATCH 2215/2451] Add BodyHideGloveCuffs name to eqp entries. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c59dd2e6..1c1b3d1b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c59dd2e6724be71dfe6ade11dacf405f29634fde +Subproject commit 1c1b3d1b2f050ae0424cb299d30b2bbb65514aa6 diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index c681b230..da1a1d44 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -48,8 +48,8 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) ? entry.HasFlag(EqpEntry.BodyHideGlovesL) : entry.HasFlag(EqpEntry.BodyHideGlovesM); return testFlag - ? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS - : entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS); + ? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS + : entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] From 61d70f7b4e994de1674bdfdcec883c54524b7dcd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:45:56 +0100 Subject: [PATCH 2216/2451] Fix identification of EST changes. --- Penumbra/Meta/Manipulations/Est.cs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 8a450eee..05d4c014 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -29,19 +29,24 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende switch (Slot) { case EstType.Hair: + { + var (gender, race) = GenderRace.Split(); + var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName()); + $"Customization: {gender.ToName()} {race.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); break; + } case EstType.Face: + { + var (gender, race) = GenderRace.Split(); + var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName()); - break; - case EstType.Body: - identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); - break; - case EstType.Head: - identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); + $"Customization: {gender.ToName()} {race.ToName()} Face {SetId}", + () => IdentifiedCustomization.Face(race, gender, id)); break; + } + case EstType.Body: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); break; + case EstType.Head: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); break; } } From 26a6cc473502952c704465432e97f7a9684a6a06 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:46:16 +0100 Subject: [PATCH 2217/2451] Fix clipping in changed items panel without grouping. --- Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index f70d63d2..b12df97d 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -247,7 +247,7 @@ public class ModPanelChangedItemsTab( } else { - ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing()); + ImGuiClip.ClippedDraw(cache.Data, DrawContainer, _buttonSize.Y); } } From 82a1271281509e1158f4c5d3e421f603e0333fc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:46:44 +0100 Subject: [PATCH 2218/2451] Add option to import atch files from the mod itself via context. --- Penumbra/Mods/Editor/ModFileCollection.cs | 16 +++++++--- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 30 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 20423493..7667910f 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -13,6 +13,7 @@ public class ModFileCollection : IDisposable, IService private readonly List _tex = []; private readonly List _shpk = []; private readonly List _pbd = []; + private readonly List _atch = []; private readonly SortedSet _missing = []; private readonly HashSet _usedPaths = []; @@ -41,21 +42,24 @@ public class ModFileCollection : IDisposable, IService public IReadOnlyList Pbd => Ready ? _pbd : []; + public IReadOnlyList Atch + => Ready ? _atch : []; + public bool Ready { get; private set; } = true; public void UpdateAll(Mod mod, IModDataContainer option) { - UpdateFiles(mod, new CancellationToken()); - UpdatePaths(mod, option, false, new CancellationToken()); + UpdateFiles(mod, CancellationToken.None); + UpdatePaths(mod, option, false, CancellationToken.None); } public void UpdatePaths(Mod mod, IModDataContainer option) - => UpdatePaths(mod, option, true, new CancellationToken()); + => UpdatePaths(mod, option, true, CancellationToken.None); public void Clear() { ClearFiles(); - ClearPaths(false, new CancellationToken()); + ClearPaths(false, CancellationToken.None); } public void Dispose() @@ -135,6 +139,9 @@ public class ModFileCollection : IDisposable, IService case ".pbd": _pbd.Add(registry); break; + case ".atch": + _atch.Add(registry); + break; } } } @@ -147,6 +154,7 @@ public class ModFileCollection : IDisposable, IService _tex.Clear(); _shpk.Clear(); _pbd.Clear(); + _atch.Clear(); } private void ClearPaths(bool clearRegistries, CancellationToken tok) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index c9a1d059..68424ae9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -75,12 +75,34 @@ public partial class ModEditWindow ImUtf8.Text($"Dragging .atch for {gr.ToName()}..."); return true; }); - ImUtf8.ButtonEx("Import .atch"u8, - _dragDropManager.IsDragging ? ""u8 : "Drag a .atch file containinig its race code in the path here to import its values."u8, - default, - !_dragDropManager.IsDragging); + var hasAtch = _editor.Files.Atch.Count > 0; + if (ImUtf8.ButtonEx("Import .atch"u8, + _dragDropManager.IsDragging + ? ""u8 + : hasAtch + ? "Drag a .atch file containing its race code in the path here to import its values.\n\nClick to select an .atch file from the mod."u8 + : "Drag a .atch file containing its race code in the path here to import its values."u8, default, + !_dragDropManager.IsDragging && !hasAtch) + && hasAtch) + ImUtf8.OpenPopup("##atchPopup"u8); if (_dragDropManager.CreateImGuiTarget("atchDrag", out var files, out _) && files.FirstOrDefault() is { } file) _metaDrawers.Atch.ImportFile(file); + + using var popup = ImUtf8.Popup("##atchPopup"u8); + if (!popup) + return; + + if (!hasAtch) + { + ImGui.CloseCurrentPopup(); + return; + } + + foreach (var atchFile in _editor.Files.Atch) + { + if (ImUtf8.Selectable(atchFile.RelPath.Path.Span) && atchFile.File.Exists) + _metaDrawers.Atch.ImportFile(atchFile.File.FullName); + } } private void DrawEditHeader(MetaManipulationType type) From 279a861582c7168604e6e995d801ff5add94c2fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 22:17:37 +0100 Subject: [PATCH 2219/2451] Fix error in parser. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1c1b3d1b..757aaa39 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1c1b3d1b2f050ae0424cb299d30b2bbb65514aa6 +Subproject commit 757aaa39ac4aa988d0b8597ff088641a0f4f49fd From 03bb07a9c08f7fd867aa5344f62ce6070e97c1d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 12:04:38 +0100 Subject: [PATCH 2220/2451] Update for SDK. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- OtterGui | 2 +- Penumbra.Api | 2 +- .../Buffers/AnimationInvocationBuffer.cs | 4 +- .../Buffers/CharacterBaseBuffer.cs | 4 +- .../Buffers/MemoryMappedBuffer.cs | 3 + .../Buffers/ModdedFileBuffer.cs | 4 +- Penumbra.CrashHandler/CrashData.cs | 2 + Penumbra.CrashHandler/GameEventLogReader.cs | 5 +- Penumbra.CrashHandler/GameEventLogWriter.cs | 3 +- .../Penumbra.CrashHandler.csproj | 20 ++---- Penumbra.CrashHandler/Program.cs | 4 +- Penumbra.CrashHandler/packages.lock.json | 13 ++++ Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra.sln | 52 +++++++-------- .../PostProcessing/ShaderReplacementFixer.cs | 12 ++-- .../LiveColorTablePreviewer.cs | 4 +- .../Interop/ResourceTree/ResolveContext.cs | 4 +- Penumbra/Interop/Structs/StructExtensions.cs | 5 +- Penumbra/Penumbra.csproj | 64 +------------------ Penumbra/Penumbra.json | 2 +- Penumbra/Services/MigrationManager.cs | 64 +++++++++++++++++++ Penumbra/UI/LaunchButton.cs | 9 +-- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 9 +-- Penumbra/packages.lock.json | 24 ++++--- 28 files changed, 178 insertions(+), 147 deletions(-) create mode 100644 Penumbra.CrashHandler/packages.lock.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1783c9a4..7901a653 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4799cbed..c87c0244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 549c967a..2bece720 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/OtterGui b/OtterGui index 13f1a90b..3396ee17 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 13f1a90b88d2b8572480748a209f957b70d6a46f +Subproject commit 3396ee176fa72ad2dfb2de3294f7125ebce4dae5 diff --git a/Penumbra.Api b/Penumbra.Api index 404c8aaa..6d262cd3 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 404c8aaa5115925006963baa118bf710c7953380 +Subproject commit 6d262cd3181d44c29891c9473f7c423300320f15 diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index 11dc52db..292be2ff 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index a48fe846..89fea29d 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs index a1b3de52..e2ffcebe 100644 --- a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -1,5 +1,8 @@ +using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.IO.MemoryMappedFiles; +using System.Linq; using System.Numerics; using System.Text; diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index ac507e7f..e4ee66d0 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs index dd75f46e..55460548 100644 --- a/Penumbra.CrashHandler/CrashData.cs +++ b/Penumbra.CrashHandler/CrashData.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs index 1813a671..8a7f53f8 100644 --- a/Penumbra.CrashHandler/GameEventLogReader.cs +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs index e2c461f4..915c59a2 100644 --- a/Penumbra.CrashHandler/GameEventLogWriter.cs +++ b/Penumbra.CrashHandler/GameEventLogWriter.cs @@ -1,4 +1,5 @@ -using Penumbra.CrashHandler.Buffers; +using System; +using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index c9f97fde..4cb53c8b 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,20 +1,6 @@ - - + Exe - net8.0-windows - preview - enable - x64 - enable - true - false - - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(HOME)/.xlcore/dalamud/Hooks/dev/ - $(DALAMUD_HOME)/ @@ -25,4 +11,8 @@ embedded + + false + + diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index 3bc461f7..38c176a6 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; +using System.IO; using System.Text.Json; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/packages.lock.json b/Penumbra.CrashHandler/packages.lock.json new file mode 100644 index 00000000..1d395083 --- /dev/null +++ b/Penumbra.CrashHandler/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData b/Penumbra.GameData index 757aaa39..ab3ee0ee 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 757aaa39ac4aa988d0b8597ff088641a0f4f49fd +Subproject commit ab3ee0ee814e170b59e0c13b023bbb8bc9314c74 diff --git a/Penumbra.String b/Penumbra.String index 4eb7c118..2896c056 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7 +Subproject commit 2896c0561f60827f97408650d52a15c38f4d9d10 diff --git a/Penumbra.sln b/Penumbra.sln index e864fbee..0293df63 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -54,34 +54,34 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 8e12662e..b9c21556 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -193,7 +193,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; + var shpkName = mtrl->ShpkName.AsSpan(); var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) @@ -217,7 +217,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) => CharacterStockingsShpkName.SequenceEqual(shpkName) ? _characterStockingsState : null; @@ -227,7 +227,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _characterStockingsState.MaterialCount; private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) => SkinShpkName.SequenceEqual(shpkName) ? _skinState : null; @@ -237,7 +237,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _skinState.MaterialCount; private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan shpkName) { @@ -264,7 +264,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic + _hairMaskState.MaterialCount; private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan shpkName) { @@ -480,7 +480,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (material == null) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); - var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan); + var shpkState = GetStateForColorTable(thisPtr->ShpkName.AsSpan()); if (shpkState == null || shpkState.MaterialCount == 0) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index c459a67a..0415fc9d 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -84,7 +84,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase textureSize[1] = Height; using var texture = - new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); + new SafeTextureHandle( + Device.Instance()->CreateTexture2D(textureSize, 1, TextureFormat.R16G16B16A16_FLOAT, + TextureFlags.TextureNoSwizzle | TextureFlags.Immutable | TextureFlags.Managed, 7), false); if (texture.IsInvalid) return; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ea4506c7..41a27ed5 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -239,7 +239,7 @@ internal unsafe partial record ResolveContext( return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value)); if (shpkNode is not null) { if (Global.WithUiData) @@ -253,7 +253,7 @@ internal unsafe partial record ResolveContext( var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i)), + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i).Value), resource->Textures[i].IsDX11); if (texNode == null) continue; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 8b5974f0..03b4cf36 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.STD; +using InteropGenerator.Runtime; using Penumbra.String; namespace Penumbra.Interop.Structs; @@ -57,8 +58,8 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); } - private static unsafe CiByteString ToOwnedByteString(byte* str) - => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); + private static unsafe CiByteString ToOwnedByteString(CStringPointer str) + => str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty; private static CiByteString ToOwnedByteString(ReadOnlySpan str) => str.Length == 0 ? CiByteString.Empty : CiByteString.FromSpanUnsafe(str, true).Clone(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 870865da..a09abcaa 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,25 +1,15 @@ - + - net8.0-windows - preview - x64 Penumbra absolute gangstas Penumbra - Copyright © 2022 + Copyright © 2025 9.0.0.1 9.0.0.1 bin\$(Configuration)\ - true - enable - true - false - false - true - $(MSBuildWarningsAsMessages);MSB3277 PROFILING; @@ -35,63 +25,13 @@ - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)ImGui.NET.dll - False - - - $(DalamudLibPath)ImGuiScene.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - $(DalamudLibPath)Iced.dll - False - - - $(DalamudLibPath)SharpDX.dll - False - - - $(DalamudLibPath)SharpDX.Direct3D11.dll - False - - - $(DalamudLibPath)SharpDX.DXGI.dll - False - lib\OtterTex.dll - - diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 968bb750..924d7bd3 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 11, + "DalamudApiLevel": 12, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index aa2d445e..abc059e9 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,12 +1,76 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; using SharpCompress.Common; using SharpCompress.Readers; namespace Penumbra.Services; +public class ModMigrator +{ + private class FileData(string path) + { + public readonly string Path = path; + public readonly List<(string GamePath, int Option)> GamePaths = []; + } + + private sealed class FileDataDict : Dictionary + { + public void Add(string path, string gamePath, int option) + { + if (!TryGetValue(path, out var data)) + { + data = new FileData(path); + data.GamePaths.Add((gamePath, option)); + Add(path, data); + } + else + { + data.GamePaths.Add((gamePath, option)); + } + } + } + + private readonly FileDataDict Textures = []; + private readonly FileDataDict Models = []; + private readonly FileDataDict Materials = []; + + public void Update(IEnumerable mods) + { + CollectFiles(mods); + } + + private void CollectFiles(IEnumerable mods) + { + var option = 0; + foreach (var mod in mods) + { + AddDict(mod.Default.Files, option++); + foreach (var container in mod.Groups.SelectMany(group => group.DataContainers)) + AddDict(container.Files, option++); + } + + return; + + void AddDict(Dictionary dict, int currentOption) + { + foreach (var (gamePath, file) in dict) + { + switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) + { + case ResourceType.Tex: Textures.Add(file.FullName, gamePath.ToString(), currentOption); break; + case ResourceType.Mdl: Models.Add(file.FullName, gamePath.ToString(), currentOption); break; + case ResourceType.Mtrl: Materials.Add(file.FullName, gamePath.ToString(), currentOption); break; + } + } + } + } +} + public class MigrationManager(Configuration config) : IService { public enum TaskType : byte diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index cb533a00..49161c31 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin; using Dalamud.Plugin.Services; @@ -18,7 +19,6 @@ public class LaunchButton : IDisposable, IUiService private readonly string _fileName; private readonly ITextureProvider _textureProvider; - private IDalamudTextureWrap? _icon; private IReadOnlyTitleScreenMenuEntry? _entry; /// @@ -30,7 +30,6 @@ public class LaunchButton : IDisposable, IUiService _configWindow = ui; _textureProvider = textureProvider; _title = title; - _icon = null; _entry = null; _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); @@ -39,7 +38,6 @@ public class LaunchButton : IDisposable, IUiService public void Dispose() { - _icon?.Dispose(); if (_entry != null) _title.RemoveEntry(_entry); } @@ -52,9 +50,8 @@ public class LaunchButton : IDisposable, IUiService try { // TODO: update when API updated. - _icon = _textureProvider.GetFromFile(_fileName).RentAsync().Result; - if (_icon != null) - _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); + var icon = _textureProvider.GetFromFile(_fileName); + _entry = _title.AddEntry("Manage Penumbra", icon, OnTriggered); _uiBuilder.Draw -= CreateEntry; } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 4e6cf62c..bfe89768 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -164,13 +164,14 @@ public unsafe class GlobalVariablesDrawer( _schedulerFilterMapU8 = CiByteString.FromString(_schedulerFilterMap, out var t, MetaDataComputation.All, false) ? t : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->NumResources}"); + ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->Resources.LongCount}"); using var table = ImUtf8.Table("##SchedulerMapResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - var map = (StdMap>*)&scheduler.Scheduler->Unknown; + // TODO Remove cast when it'll have the right type in CS. + var map = (StdMap>*)&scheduler.Scheduler->Resources; var total = 0; _shownResourcesMap = 0; foreach (var (key, resourcePtr) in *map) @@ -214,7 +215,7 @@ public unsafe class GlobalVariablesDrawer( _schedulerFilterListU8 = CiByteString.FromString(_schedulerFilterList, out var t, MetaDataComputation.All, false) ? t : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->NumResources}"); + ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->Resources.LongCount}"); using var table = ImUtf8.Table("##SchedulerListResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -223,7 +224,7 @@ public unsafe class GlobalVariablesDrawer( var resource = scheduler.Scheduler->Begin; var total = 0; _shownResourcesList = 0; - while (resource != null && total < (int)scheduler.Scheduler->NumResources) + while (resource != null && total < scheduler.Scheduler->Resources.Count) { if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) { diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 9aa1ebd5..dda6b305 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,7 +1,19 @@ { "version": 1, "dependencies": { - "net8.0-windows7.0": { + "net9.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[12.0.0, )", + "resolved": "12.0.0", + "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, "EmbedIO": { "type": "Direct", "requested": "[3.5.2, )", @@ -52,12 +64,6 @@ "resolved": "3.1.7", "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" }, - "System.Formats.Asn1": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "OKWHCPYQr/+cIoO8EVjFn7yFyiT8Mnf1wif/5bYGsqxQV6PrwlX2HQ9brZNx57ViOvRe4ing1xgHCKl/5Ko8xg==" - }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.3.0", @@ -134,8 +140,8 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.6.0, )", - "Penumbra.String": "[1.0.5, )" + "Penumbra.Api": "[5.6.1, )", + "Penumbra.String": "[1.0.6, )" } }, "penumbra.string": { From 586bd9d0cc6e3ff308a69fa3ce6e65ff3fc41f2f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 12:07:45 +0100 Subject: [PATCH 2221/2451] Re-add wrong dependencies. --- Penumbra.sln | 8 ++++---- Penumbra/Penumbra.csproj | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 0293df63..ac1c9566 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -58,16 +58,16 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64 - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64 {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64 {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64 {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index a09abcaa..cc892fa8 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -26,6 +26,22 @@ + + $(DalamudLibPath)Iced.dll + False + + + $(DalamudLibPath)SharpDX.dll + False + + + $(DalamudLibPath)SharpDX.Direct3D11.dll + False + + + $(DalamudLibPath)SharpDX.DXGI.dll + False + lib\OtterTex.dll From b8b2127a5d63b3368ca047d206073a288b42166f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 15:53:59 +0100 Subject: [PATCH 2222/2451] Update STM and signatures. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- .../UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 6d262cd3..2cbf4bac 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 6d262cd3181d44c29891c9473f7c423300320f15 +Subproject commit 2cbf4bace53a5749d3eab1ff03025a6e6bd9fc37 diff --git a/Penumbra.GameData b/Penumbra.GameData index ab3ee0ee..9442f1d6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ab3ee0ee814e170b59e0c13b023bbb8bc9314c74 +Subproject commit 9442f1d60578dae7598cbb0a1ff545d24905bdfd diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 0a437da0..b16d4dcd 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + : FilterComboCache(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. @@ -31,7 +31,7 @@ public class StainService : IService return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; } - protected override string ToString(ushort obj) + protected override string ToString(StmKeyType obj) => $"{obj,4}"; protected override void DrawFilter(int currentSelected, float width) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index f32a3dc9..0c987972 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -607,7 +607,7 @@ public partial class MtrlTab if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dye.Template = _stainService.GudTemplateCombo.CurrentSelection; + dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort; ret = true; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index f21d86a9..0ffdd1cc 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -184,7 +184,7 @@ public partial class MtrlTab if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; ret = true; } @@ -299,7 +299,7 @@ public partial class MtrlTab if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; ret = true; } From 124b54ab046888f49df8d28120d1d3990dc30c69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 16:00:30 +0100 Subject: [PATCH 2223/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9442f1d6..64823f2e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9442f1d60578dae7598cbb0a1ff545d24905bdfd +Subproject commit 64823f2e29fdc65033d1891debe1ea18dadce1c8 From 525d1c6bf96ee2965e541d17a160a961f0ed85cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 18:18:04 +0100 Subject: [PATCH 2224/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 64823f2e..9ae4a971 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 64823f2e29fdc65033d1891debe1ea18dadce1c8 +Subproject commit 9ae4a97110fff005a54213815086ce950d4d8b2d From 49f077aca0eb850f3814af29d157911489e4967d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 27 Mar 2025 22:32:07 +0100 Subject: [PATCH 2225/2451] Fixes for 7.2 (ResourceTree + ShPk 13.1) --- Penumbra.GameData | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 54 +++++++++++-------- .../Interop/ResourceTree/TreeBuildCache.cs | 22 ++++---- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9ae4a971..1158cf40 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9ae4a97110fff005a54213815086ce950d4d8b2d +Subproject commit 1158cf404a16979d0b7e12f7bbcbbc651da16add diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index cb8be184..49194c3a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -23,24 +23,35 @@ public class ResourceTreeFactory( Configuration config, ActorManager actors, PathState pathState, + IFramework framework, ModManager modManager) : IService { private static readonly string ParentDirectoryPrefix = $"..{Path.DirectorySeparatorChar}"; private TreeBuildCache CreateTreeBuildCache() - => new(objects, gameData, actors); + => new(framework.IsInFrameworkUpdateThread ? objects : null, gameData, actors); + + private TreeBuildCache CreateTreeBuildCache(Flags flags) + => !framework.IsInFrameworkUpdateThread && flags.HasFlag(Flags.PopulateObjectTableData) + ? framework.RunOnFrameworkThread(CreateTreeBuildCache).Result + : CreateTreeBuildCache(); public IEnumerable GetLocalPlayerRelatedCharacters() - { - var cache = CreateTreeBuildCache(); - return cache.GetLocalPlayerRelatedCharacters(); - } + => framework.RunOnFrameworkThread(() => + { + var cache = CreateTreeBuildCache(); + return cache.GetLocalPlayerRelatedCharacters(); + }).Result; public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromObjectTable( Flags flags) { - var cache = CreateTreeBuildCache(); - var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters(); + var (cache, characters) = framework.RunOnFrameworkThread(() => + { + var cache = CreateTreeBuildCache(); + var characters = ((flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters()).ToArray(); + return (cache, characters); + }).Result; foreach (var character in characters) { @@ -53,7 +64,7 @@ public class ResourceTreeFactory( public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, Flags flags) { - var cache = CreateTreeBuildCache(); + var cache = CreateTreeBuildCache(flags); foreach (var character in characters) { var tree = FromCharacter(character, cache, flags); @@ -63,7 +74,7 @@ public class ResourceTreeFactory( } public ResourceTree? FromCharacter(ICharacter character, Flags flags) - => FromCharacter(character, CreateTreeBuildCache(), flags); + => FromCharacter(character, CreateTreeBuildCache(flags), flags); private unsafe ResourceTree? FromCharacter(ICharacter character, TreeBuildCache cache, Flags flags) { @@ -80,7 +91,7 @@ public class ResourceTreeFactory( return null; var localPlayerRelated = cache.IsLocalPlayerRelated(character); - var (name, anonymizedName, related) = GetCharacterName(character); + var (name, anonymizedName, related) = GetCharacterName((GameObject*)character.Address); var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Identity.Name, collectionResolveData.ModCollection.Identity.AnonymizedName); @@ -183,36 +194,37 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(ICharacter character) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(GameObject* character) { - var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = actors.FromObject(character, out var owner, true, false, false); var identifierStr = identifier.ToString(); return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } - private unsafe bool IsPlayerRelated(ICharacter? character) + private unsafe bool IsPlayerRelated(GameObject* character) { - if (character == null) + if (character is null) return false; - var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = actors.FromObject(character, out var owner, true, false, false); return IsPlayerRelated(identifier, owner); } - private bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) + private unsafe bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) => identifier.Type switch { IdentifierType.Player => true, - IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as ICharacter), + IdentifierType.Owned => IsPlayerRelated(owner.AsObject), _ => false, }; [Flags] public enum Flags { - RedactExternalPaths = 1, - WithUiData = 2, - LocalPlayerRelatedOnly = 4, - WithOwnership = 8, + RedactExternalPaths = 1, + WithUiData = 2, + LocalPlayerRelatedOnly = 4, + WithOwnership = 8, + PopulateObjectTableData = 16, } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 49e00547..c0114412 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -12,14 +12,15 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) +internal readonly struct TreeBuildCache(ObjectManager? objects, IDataManager dataManager, ActorManager actors) { private readonly Dictionary?> _shaderPackageNames = []; + private readonly IGameObject? _player = objects?.GetDalamudObject(0); + public unsafe bool IsLocalPlayerRelated(ICharacter character) { - var player = objects.GetDalamudObject(0); - if (player == null) + if (_player is null) return false; var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)character.Address; @@ -28,27 +29,26 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data return actualIndex switch { < 2 => true, - < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == player.EntityId, + < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == _player.EntityId, _ => false, }; } public IEnumerable GetCharacters() - => objects.Objects.OfType(); + => objects is not null ? objects.Objects.OfType() : []; public IEnumerable GetLocalPlayerRelatedCharacters() { - var player = objects.GetDalamudObject(0); - if (player == null) + if (_player is null) yield break; - yield return (ICharacter)player; + yield return (ICharacter)_player; - var minion = objects.GetDalamudObject(1); - if (minion != null) + var minion = objects!.GetDalamudObject(1); + if (minion is not null) yield return (ICharacter)minion; - var playerId = player.EntityId; + var playerId = _player.EntityId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { if (objects.GetDalamudObject(i) is ICharacter owned && owned.OwnerId == playerId) From b189ac027baa29fdeee79401985bd9968027d83b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 02:29:49 +0100 Subject: [PATCH 2226/2451] Fix imgui assert. --- Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs index 34aafbea..97761091 100644 --- a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -6,7 +6,8 @@ public static class DebugConfigurationDrawer { public static void Draw() { - if (!ImUtf8.CollapsingHeaderId("Debug Logging Options")) + using var id = ImUtf8.CollapsingHeaderId("Debug Logging Options"u8); + if (!id) return; ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); From 8e191ae07525e6f05398a0bbbfa6ac5435232ae7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 13:33:43 +0100 Subject: [PATCH 2227/2451] Fix offsets. --- Penumbra/Interop/VolatileOffsets.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/VolatileOffsets.cs b/Penumbra/Interop/VolatileOffsets.cs index 2c6e3180..85008aae 100644 --- a/Penumbra/Interop/VolatileOffsets.cs +++ b/Penumbra/Interop/VolatileOffsets.cs @@ -6,7 +6,7 @@ public static class VolatileOffsets { public const int PlayTimeOffset = 0x254; public const int SomeIntermediate = 0x1F8; - public const int Flags = 0x4A4; + public const int Flags = 0x4A8; public const int IInstanceListenner = 0x270; public const int BitShift = 13; public const int CasterVFunc = 1; @@ -19,7 +19,7 @@ public static class VolatileOffsets public static class UpdateModel { - public const int ShortCircuit = 0xA2C; + public const int ShortCircuit = 0xA3C; } public static class FontReloader From 974b21561002c46461b1b81eb26aa32333b47060 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 13:58:11 +0100 Subject: [PATCH 2228/2451] 1.3.6.0 --- Penumbra.sln | 1 + Penumbra/UI/Changelog.cs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Penumbra.sln b/Penumbra.sln index ac1c9566..e52045b0 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .github\workflows\build.yml = .github\workflows\build.yml + Penumbra\Penumbra.json = Penumbra\Penumbra.json .github\workflows\release.yml = .github\workflows\release.yml repo.json = repo.json .github\workflows\test_release.yml = .github\workflows\test_release.yml diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 87dd101d..1b0225ed 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -59,10 +59,36 @@ public class PenumbraChangelog : IUiService Add1_3_3_0(Changelog); Add1_3_4_0(Changelog); Add1_3_5_0(Changelog); + Add1_3_6_0(Changelog); } #region Changelogs + private static void Add1_3_6_0(Changelog log) + => log.NextVersion("Version 1.3.6.0") + .RegisterImportant("Updated Penumbra for update 7.20 and Dalamud API 12.") + .RegisterEntry( + "This is not thoroughly tested, but I decided to push to stable instead of testing because otherwise a lot of people would just go to testing just for early access again despite having no business doing so.", + 1) + .RegisterEntry( + "I also do not use most of the functionality of Penumbra myself, so I am unable to even encounter most issues myself.", 1) + .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) + .RegisterImportant("There is a known issue with the Material Editor due to the shader changes, please do not author materials for the moment, they will be broken!", 1) + .RegisterHighlight( + "The texture editor now has encoding support for Block Compression 1, 4 and 5 and tooltips explaining when to use which format.") + .RegisterEntry("It also is able to use GPU compression and thus has become much faster for BC7 in particular. (Thanks Ny!)", 1) + .RegisterEntry( + "Added the option to import .atch files found in the particular mod via right-click context menu on the import drag & drop button.") + .RegisterEntry("Added a chat command to clear temporary settings done manually in Penumbra.") + .RegisterEntry( + "The changed item star to select the preferred changed item is a bit more noticeable by default, and its color can be configured.") + .RegisterEntry("Some minor fixes for computing changed items. (Thanks Anna!)") + .RegisterEntry("The EQP entry previously named Unknown 4 was renamed to 'Hide Glove Cuffs'.") + .RegisterEntry("Fixed the changed item identification for EST changes.") + .RegisterEntry("Fixed clipping issues in the changed items panel when no grouping was active."); + + + private static void Add1_3_5_0(Changelog log) => log.NextVersion("Version 1.3.5.0") .RegisterImportant( From 60becf0a090fa2a999cda123adaadac369ee13ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 14:06:21 +0100 Subject: [PATCH 2229/2451] Use staging build for release for now. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87c0244..377919b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From b019da2a8c370595a9f9190c961cd18592a0cabb Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 13:09:26 +0000 Subject: [PATCH 2230/2451] [CI] Updating repo.json for 1.3.6.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1617a879..69db2f84 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.5.0", - "TestingAssemblyVersion": "1.3.5.1", + "AssemblyVersion": "1.3.6.0", + "TestingAssemblyVersion": "1.3.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.5.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 1a1d1c184026f363ddae1632d5c089b34c82cc0a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 14:10:52 +0100 Subject: [PATCH 2231/2451] Revert Dalamud staging on release, and update api level. --- .github/workflows/release.yml | 2 +- repo.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 377919b2..c87c0244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/repo.json b/repo.json index 69db2f84..e4fd5abc 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.3.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 11, - "TestingDalamudApiLevel": 11, + "DalamudApiLevel": 12, + "TestingDalamudApiLevel": 12, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 23ba77c107516a9f373d8f88cb71f421184f75f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 15:52:40 +0100 Subject: [PATCH 2232/2451] Update build step and check for pre 7.2 shps. --- Penumbra.GameData | 2 +- .../Interop/Processing/ShpkPathPreProcessor.cs | 8 ++++---- Penumbra/Penumbra.csproj | 17 +++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1158cf40..85921598 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1158cf404a16979d0b7e12f7bbcbbc651da16add +Subproject commit 859215989da41a4ccb59a5ce390223570a69c94e diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 2fb35ae0..826771dd 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -56,8 +56,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, using var file = MmioMemoryManager.CreateFromFile(path, access: MemoryMappedFileAccess.Read); var bytes = file.GetSpan(); - return ShpkFile.FastIsLegacy(bytes) - ? SanityCheckResult.Legacy + return ShpkFile.FastIsObsolete(bytes) + ? SanityCheckResult.Obsolete : SanityCheckResult.Success; } catch (FileNotFoundException) @@ -75,7 +75,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { SanityCheckResult.IoError => "Cannot read the modded file.", SanityCheckResult.NotFound => "The modded file does not exist.", - SanityCheckResult.Legacy => "This mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + SanityCheckResult.Obsolete => "This mod is not compatible with Dawntrail post patch 7.2. Get an updated version, if possible, or disable it.", _ => string.Empty, }; @@ -84,6 +84,6 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, Success, IoError, NotFound, - Legacy, + Obsolete, } } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index cc892fa8..f93b1815 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,6 +23,13 @@ PreserveNewest + + PreserveNewest + DirectXTexC.dll + + + PreserveNewest + @@ -64,16 +71,6 @@ - - - PreserveNewest - DirectXTexC.dll - - - PreserveNewest - - - From 7498bc469f413dbe4d4230cb7ae64b98df4037b3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 14:55:12 +0000 Subject: [PATCH 2233/2451] [CI] Updating repo.json for 1.3.6.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e4fd5abc..94200e61 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.0", - "TestingAssemblyVersion": "1.3.6.0", + "AssemblyVersion": "1.3.6.1", + "TestingAssemblyVersion": "1.3.6.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 01e6f5846335d3741395c741c3f244e71ef36446 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 16:53:43 +0100 Subject: [PATCH 2234/2451] Add Launching IPC Event. API 5.8 --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 5 ++++- Penumbra/Api/IpcLaunchingProvider.cs | 28 ++++++++++++++++++++++++++++ Penumbra/Penumbra.cs | 2 ++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Api/IpcLaunchingProvider.cs diff --git a/Penumbra.Api b/Penumbra.Api index 2cbf4bac..bd56d828 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2cbf4bace53a5749d3eab1ff03025a6e6bd9fc37 +Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 36f799a0..47d44cfc 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -16,13 +16,16 @@ public class PenumbraApi( TemporaryApi temporary, UiApi ui) : IDisposable, IApiService, IPenumbraApi { + public const int BreakingVersion = 5; + public const int FeatureVersion = 8; + public void Dispose() { Valid = false; } public (int Breaking, int Feature) ApiVersion - => (5, 7); + => (BreakingVersion, FeatureVersion); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcLaunchingProvider.cs b/Penumbra/Api/IpcLaunchingProvider.cs new file mode 100644 index 00000000..ff851003 --- /dev/null +++ b/Penumbra/Api/IpcLaunchingProvider.cs @@ -0,0 +1,28 @@ +using Dalamud.Plugin; +using OtterGui.Log; +using OtterGui.Services; +using Penumbra.Api.Api; +using Serilog.Events; + +namespace Penumbra.Api; + +public sealed class IpcLaunchingProvider : IApiService +{ + public IpcLaunchingProvider(IDalamudPluginInterface pi, Logger log) + { + try + { + using var subscriber = log.MainLogger.IsEnabled(LogEventLevel.Debug) + ? IpcSubscribers.Launching.Subscriber(pi, + (major, minor) => log.Debug($"[IPC] Invoked Penumbra.Launching IPC with API Version {major}.{minor}.")) + : null; + + using var provider = IpcSubscribers.Launching.Provider(pi); + provider.Invoke(PenumbraApi.BreakingVersion, PenumbraApi.FeatureVersion); + } + catch (Exception ex) + { + log.Error($"[IPC] Could not invoke Penumbra.Launching IPC:\n{ex}"); + } + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b6009627..79c7f2db 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -58,6 +58,8 @@ public class Penumbra : IDalamudPlugin { HookOverrides.Instance = HookOverrides.LoadFile(pluginInterface); _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); + // Invoke the IPC Penumbra.Launching method before any hooks or other services are created. + _services.GetService(); Messager = _services.GetService(); _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); From 8a68a1bff52bb80a79f2041a54efb5b03905d1cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 17:25:03 +0100 Subject: [PATCH 2235/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 85921598..e717a66f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 859215989da41a4ccb59a5ce390223570a69c94e +Subproject commit e717a66f33b0656a7c5c971ffa2f63fd96477d94 From 3bb7db10fba29088a83a0c0adf71b248de38552c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 16:29:20 +0000 Subject: [PATCH 2236/2451] [CI] Updating repo.json for 1.3.6.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 94200e61..4fac86da 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.1", - "TestingAssemblyVersion": "1.3.6.1", + "AssemblyVersion": "1.3.6.2", + "TestingAssemblyVersion": "1.3.6.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a1bf26e7e8a00deafea2b09e41df8437a69641ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 18:30:18 +0100 Subject: [PATCH 2237/2451] Run HTTP redraws on framework thread. --- Penumbra/Api/HttpApi.cs | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 859c46b4..b6e1d799 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; @@ -14,7 +15,7 @@ public class HttpApi : IDisposable, IApiService // @formatter:off [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); - [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); + [Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll(); [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); [Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); @@ -24,11 +25,13 @@ public class HttpApi : IDisposable, IApiService public const string Prefix = "http://localhost:42069/"; private readonly IPenumbraApi _api; + private readonly IFramework _framework; private WebServer? _server; - public HttpApi(Configuration config, IPenumbraApi api) + public HttpApi(Configuration config, IPenumbraApi api, IFramework framework) { - _api = api; + _api = api; + _framework = framework; if (config.EnableHttpApi) CreateWebServer(); } @@ -44,7 +47,7 @@ public class HttpApi : IDisposable, IApiService .WithUrlPrefix(Prefix) .WithMode(HttpListenerMode.EmbedIO)) .WithCors(Prefix) - .WithWebApi("/api", m => m.WithController(() => new Controller(_api))); + .WithWebApi("/api", m => m.WithController(() => new Controller(_api, _framework))); _server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}"); _server.RunAsync(); @@ -59,60 +62,58 @@ public class HttpApi : IDisposable, IApiService public void Dispose() => ShutdownWebServer(); - private partial class Controller + private partial class Controller(IPenumbraApi api, IFramework framework) { - private readonly IPenumbraApi _api; - - public Controller(IPenumbraApi api) - => _api = api; - public partial object? GetMods() { Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); - return _api.Mods.GetModList(); + return api.Mods.GetModList(); } public async partial Task Redraw() { - var data = await HttpContext.GetRequestDataAsync(); - Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); - if (data.ObjectTableIndex >= 0) - _api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); - else - _api.Redraw.RedrawAll(data.Type); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); + Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}."); + await framework.RunOnFrameworkThread(() => + { + if (data.ObjectTableIndex >= 0) + api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); + else + api.Redraw.RedrawAll(data.Type); + }).ConfigureAwait(false); } - public partial void RedrawAll() + public async partial Task RedrawAll() { Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered."); - _api.Redraw.RedrawAll(RedrawType.Redraw); + await framework.RunOnFrameworkThread(() => { api.Redraw.RedrawAll(RedrawType.Redraw); }).ConfigureAwait(false); } public async partial Task ReloadMod() { - var data = await HttpContext.GetRequestDataAsync(); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}."); // Add the mod if it is not already loaded and if the directory name is given. // AddMod returns Success if the mod is already loaded. if (data.Path.Length != 0) - _api.Mods.AddMod(data.Path); + api.Mods.AddMod(data.Path); // Reload the mod by path or name, which will also remove no-longer existing mods. - _api.Mods.ReloadMod(data.Path, data.Name); + api.Mods.ReloadMod(data.Path, data.Name); } public async partial Task InstallMod() { - var data = await HttpContext.GetRequestDataAsync(); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}."); if (data.Path.Length != 0) - _api.Mods.InstallMod(data.Path); + api.Mods.InstallMod(data.Path); } public partial void OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); - _api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); + api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } private record ModReloadData(string Path, string Name) From de408e4d58328686084a4f06e7e0924abb344011 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 17:33:26 +0000 Subject: [PATCH 2238/2451] [CI] Updating repo.json for 1.3.6.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4fac86da..7a09af2e 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.2", - "TestingAssemblyVersion": "1.3.6.2", + "AssemblyVersion": "1.3.6.3", + "TestingAssemblyVersion": "1.3.6.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5a5a1487a31ceed61ef98160d7eca6e2645a2f2c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 28 Mar 2025 20:24:22 +0100 Subject: [PATCH 2239/2451] Fix texture naming in Resource Trees --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 41a27ed5..99360077 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -270,13 +270,13 @@ internal unsafe partial record ResolveContext( if (samplerId.HasValue) { alreadyProcessedSamplerIds.Add(samplerId.Value); - var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); - if (samplerCrc.HasValue) + var textureCrc = GetTextureCrcById(shpk, samplerId.Value); + if (textureCrc.HasValue) { - if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName)) + if (shpkNames != null && shpkNames.TryGetValue(textureCrc.Value, out var samplerName)) name = samplerName.Value; else - name = $"Texture 0x{samplerCrc.Value:X8}"; + name = $"Texture 0x{textureCrc.Value:X8}"; } } } @@ -292,9 +292,9 @@ internal unsafe partial record ResolveContext( return node; - static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - => shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s) - ? s.CRC + static uint? GetTextureCrcById(ShaderPackage* shpk, uint id) + => shpk->TexturesSpan.FindFirst(t => t.Id == id, out var t) + ? t.CRC : null; static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) From cb0214ca2ff22c3d7ad8445aef685415e3d66088 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 29 Mar 2025 16:52:08 +0100 Subject: [PATCH 2240/2451] Fix material editor and improve pinning logic --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 4 +- .../Materials/MtrlTab.CommonColorTable.cs | 2 +- .../Materials/MtrlTab.ShaderPackage.cs | 8 +- .../Materials/MtrlTab.Textures.cs | 146 ++++++++++-------- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 19 ++- 6 files changed, 110 insertions(+), 71 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e717a66f..b6b91f84 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e717a66f33b0656a7c5c971ffa2f63fd96477d94 +Subproject commit b6b91f846096d15276b728ba2078f27b95317d15 diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 121e6eed..6be2ccbd 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -288,7 +288,7 @@ public class MaterialExporter const uint valueFace = 0x6E5B8F10; var isFace = material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key is { Category: categoryHairType, Value: valueFace }); + .Any(key => key is { Key: categoryHairType, Value: valueFace }); var normal = material.Textures[TextureUsage.SamplerNormal]; var mask = material.Textures[TextureUsage.SamplerMask]; @@ -363,7 +363,7 @@ public class MaterialExporter // Face is the default for the skin shader, so a lack of skin type category is also correct. var isFace = !material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key.Category == categorySkinType && key.Value != valueFace); + .Any(key => key.Key == categorySkinType && key.Value != valueFace); // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. // TODO: Specular? diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 236a66c3..d70a4b50 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -22,7 +22,7 @@ public partial class MtrlTab private bool DrawColorTableSection(bool disabled) { - if (!_shpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) + if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index a13dd96b..202047e4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -216,7 +216,7 @@ public partial class MtrlTab else foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) { - var keyName = Names.KnownNames.TryResolve(key.Category); + var keyName = Names.KnownNames.TryResolve(key.Key); var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); _shaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); } @@ -366,6 +366,7 @@ public partial class MtrlTab ret = true; _associatedShpk = null; _loadedShpkPath = FullPath.Empty; + UnpinResources(true); LoadShpk(FindAssociatedShpk(out _, out _)); } @@ -442,8 +443,8 @@ public partial class MtrlTab { using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; - using var id = ImUtf8.PushId((int)key.Category); - var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Category); + using var id = ImUtf8.PushId((int)key.Key); + var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Key); var currentValue = key.Value; var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); @@ -459,6 +460,7 @@ public partial class MtrlTab { key.Value = value; ret = true; + UnpinResources(false); Update(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 7ab2900d..dfa07d52 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -3,7 +3,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Classes; using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags; @@ -16,18 +15,22 @@ public partial class MtrlTab public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet TextureIds = new(16); public readonly HashSet SamplerIds = new(16); public float TextureLabelWidth; + private bool _samplersPinned; private void UpdateTextures() { Textures.Clear(); + TextureIds.Clear(); SamplerIds.Clear(); if (_associatedShpk == null) { + TextureIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) - SamplerIds.Add(TableSamplerId); + TextureIds.Add(TableSamplerId); foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); @@ -35,31 +38,39 @@ public partial class MtrlTab else { foreach (var index in _vertexShaders) - SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in _pixelShaders) - SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!_shadersKnown) { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.Table != null) - SamplerIds.Add(TableSamplerId); + TextureIds.UnionWith(_associatedShpk.VertexShaders[index].Textures.Select(texture => texture.Id)); + SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); } - foreach (var samplerId in SamplerIds) + foreach (var index in _pixelShaders) { - var shpkSampler = _associatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) + TextureIds.UnionWith(_associatedShpk.PixelShaders[index].Textures.Select(texture => texture.Id)); + SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + } + + if (_samplersPinned || !_shadersKnown) + { + TextureIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + TextureIds.Add(TableSamplerId); + } + + foreach (var textureId in TextureIds) + { + var shpkTexture = _associatedShpk.GetTextureById(textureId); + if (shpkTexture is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", textureId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + var sampler = Mtrl.GetOrAddSampler(textureId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkTexture.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); } - if (SamplerIds.Contains(TableSamplerId)) + if (TextureIds.Contains(TableSamplerId)) Mtrl.Table ??= new ColorTable(); } @@ -205,58 +216,67 @@ public partial class MtrlTab ret = true; } - ref var samplerFlags = ref Wrap(ref sampler.Flags); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - var addressMode = samplerFlags.UAddressMode; - if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + if (SamplerIds.Contains(sampler.SamplerId)) { - samplerFlags.UAddressMode = addressMode; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); + ref var samplerFlags = ref Wrap(ref sampler.Flags); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + var addressMode = samplerFlags.UAddressMode; + if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + { + samplerFlags.UAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("U Address Mode"u8, + "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + addressMode = samplerFlags.VAddressMode; + if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + { + samplerFlags.VAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("V Address Mode"u8, + "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = samplerFlags.LodBias; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) + { + samplerFlags.LodBias = lodBias; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = samplerFlags.MinLod; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) + { + samplerFlags.MinLod = minLod; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - addressMode = samplerFlags.VAddressMode; - if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + else { - samplerFlags.VAddressMode = addressMode; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); + ImUtf8.Text("This texture does not have a dedicated sampler."u8); } - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); - - var lodBias = samplerFlags.LodBias; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) - { - samplerFlags.LodBias = lodBias; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, - "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); - - var minLod = samplerFlags.MinLod; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) - { - samplerFlags.MinLod = minLod; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, - "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); - using var t = ImUtf8.TreeNode("Advanced Settings"u8); if (!t) return ret; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 6e16de99..97acf130 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -57,6 +57,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable Mtrl = file; FilePath = filePath; Writable = writable; + _samplersPinned = true; _associatedBaseDevkit = TryLoadShpkDevkit("_base", out _loadedBaseDevkitPathName); Update(); LoadShpk(FindAssociatedShpk(out _, out _)); @@ -172,6 +173,22 @@ public sealed partial class MtrlTab : IWritable, IDisposable Widget.DrawHexViewer(Mtrl.AdditionalData); } + private void UnpinResources(bool all) + { + _samplersPinned = false; + + if (!all) + return; + + var keys = Mtrl.ShaderPackage.ShaderKeys; + for (var i = 0; i < keys.Length; i++) + keys[i].Pinned = false; + + var constants = Mtrl.ShaderPackage.Constants; + for (var i = 0; i < constants.Length; i++) + constants[i].Pinned = false; + } + private void Update() { UpdateShaders(); @@ -192,7 +209,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(_associatedShpk, SamplerIds); + output.GarbageCollect(_associatedShpk, TextureIds); return output.Write(); } From 2dd6dd201c61f80a1ce3a01088120b3c5adc7aff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Mar 2025 18:03:50 +0100 Subject: [PATCH 2241/2451] Update PAP records. --- Penumbra/UI/ResourceWatcher/Record.cs | 20 +++++++++++++++++++ .../UI/ResourceWatcher/ResourceWatcher.cs | 12 +++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 8ab96f4b..b8730750 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -56,6 +56,26 @@ internal unsafe struct Record Crc64 = 0, }; + public static Record CreateRequest(CiByteString path, bool sync, FullPath fullPath, ResolveData resolve) + => new() + { + Time = DateTime.UtcNow, + Path = fullPath.InternalName.IsOwned ? fullPath.InternalName : fullPath.InternalName.Clone(), + OriginalPath = path.IsOwned ? path : path.Clone(), + Collection = resolve.Valid ? resolve.ModCollection : null, + Handle = null, + ResourceType = ResourceExtensions.Type(path).ToFlag(), + Category = ResourceExtensions.Category(path).ToFlag(), + RefCount = 0, + RecordType = RecordType.Request, + Synchronously = sync, + ReturnValue = OptionalBool.Null, + CustomLoad = fullPath.InternalName != path, + AssociatedGameObject = string.Empty, + LoadState = LoadState.None, + Crc64 = fullPath.Crc64, + }; + public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { path = path.IsOwned ? path : path.Clone(); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 94bd4307..d134cfe5 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -58,12 +58,19 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + { Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); + if (_1.HasValue) + Penumbra.Log.Information( + $"[ResourceLoader] [LOAD] Resolved {_1.Value.FullName} for {match} from collection {_2.ModCollection} for object 0x{_2.AssociatedGameObject:X}."); + } if (!_ephemeral.EnableResourceWatcher) return; - var record = Record.CreateRequest(original.Path, false); + var record = _1.HasValue + ? Record.CreateRequest(original.Path, false, _1.Value, _2) + : Record.CreateRequest(original.Path, false); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } @@ -257,7 +264,8 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + ReadOnlySpan additionalData, bool isAsync) { if (!isAsync) return; From f3bcc4d55492f422384099c1dadac50968ce3ba0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Mar 2025 18:05:47 +0100 Subject: [PATCH 2242/2451] Update changelog. --- Penumbra/UI/Changelog.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 1b0225ed..32abeb41 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -60,10 +60,15 @@ public class PenumbraChangelog : IUiService Add1_3_4_0(Changelog); Add1_3_5_0(Changelog); Add1_3_6_0(Changelog); + Add1_3_6_4(Changelog); } #region Changelogs + private static void Add1_3_6_4(Changelog log) + => log.NextVersion("Version 1.3.6.4") + .RegisterEntry("The material editor should be functional again."); + private static void Add1_3_6_0(Changelog log) => log.NextVersion("Version 1.3.6.0") .RegisterImportant("Updated Penumbra for update 7.20 and Dalamud API 12.") @@ -73,7 +78,6 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "I also do not use most of the functionality of Penumbra myself, so I am unable to even encounter most issues myself.", 1) .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) - .RegisterImportant("There is a known issue with the Material Editor due to the shader changes, please do not author materials for the moment, they will be broken!", 1) .RegisterHighlight( "The texture editor now has encoding support for Block Compression 1, 4 and 5 and tooltips explaining when to use which format.") .RegisterEntry("It also is able to use GPU compression and thus has become much faster for BC7 in particular. (Thanks Ny!)", 1) From cc76125b1c3f5c1e60e3fd3e0b344e01add3a8d9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Mar 2025 17:07:46 +0000 Subject: [PATCH 2243/2451] [CI] Updating repo.json for 1.3.6.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 7a09af2e..17fe95b3 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.3", - "TestingAssemblyVersion": "1.3.6.3", + "AssemblyVersion": "1.3.6.4", + "TestingAssemblyVersion": "1.3.6.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b589103b05d19122c6a9f01d9e8ceb9f33a7510f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 13:44:04 +0200 Subject: [PATCH 2244/2451] Make resolvedData thread-local --- .../Hooks/ResourceLoading/ResourceLoader.cs | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 3f8cb23f..6ddcbfda 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -23,7 +23,7 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ConcurrentDictionary _ongoingLoads = []; - private ResolveData _resolvedData = ResolveData.Invalid; + private readonly ThreadLocal _resolvedData = new(() => ResolveData.Invalid); public event Action? PapRequested; public IReadOnlyDictionary OngoingLoads @@ -56,10 +56,11 @@ public unsafe class ResourceLoader : IDisposable, IService if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) return length; + var resolvedData = _resolvedData.Value; var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) - : _resolvedData.Valid - ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) + : resolvedData.Valid + ? (resolvedData.ModCollection.ResolvePath(gamePath), resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); @@ -78,19 +79,31 @@ public unsafe class ResourceLoader : IDisposable, IService /// Load a resource for a given path and a specific collection. public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { - _resolvedData = resolveData; - var ret = _resources.GetResource(category, type, path); - _resolvedData = ResolveData.Invalid; - return ret; + var previous = _resolvedData.Value; + _resolvedData.Value = resolveData; + try + { + return _resources.GetResource(category, type, path); + } + finally + { + _resolvedData.Value = previous; + } } /// Load a resource for a given path and a specific collection. public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { - _resolvedData = resolveData; - var ret = _resources.GetSafeResource(category, type, path); - _resolvedData = ResolveData.Invalid; - return ret; + var previous = _resolvedData.Value; + _resolvedData.Value = resolveData; + try + { + return _resources.GetSafeResource(category, type, path); + } + finally + { + _resolvedData.Value = previous; + } } /// The function to use to resolve a given path. @@ -159,10 +172,11 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); // If no replacements are being made, we still want to be able to trigger the event. + var resolvedData = _resolvedData.Value; var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) - : _resolvedData.Valid - ? (_resolvedData.ModCollection.ResolvePath(path), _resolvedData) + : resolvedData.Valid + ? (resolvedData.ModCollection.ResolvePath(path), resolvedData) : ResolvePath(path, category, type); if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) From fe5d1bc36ee05017fde0bf9cdad71772fd1ad109 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 30 Mar 2025 16:08:59 +0000 Subject: [PATCH 2245/2451] [CI] Updating repo.json for 1.3.6.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 17fe95b3..2c2088c3 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.4", - "TestingAssemblyVersion": "1.3.6.4", + "AssemblyVersion": "1.3.6.5", + "TestingAssemblyVersion": "1.3.6.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 1d517103b3d52758afbd47648352d793446eb5c6 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 19:55:43 +0200 Subject: [PATCH 2246/2451] Mtrl editor: Fix texture pinning --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index dfa07d52..dd01ec2b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -59,7 +59,7 @@ public partial class MtrlTab foreach (var textureId in TextureIds) { var shpkTexture = _associatedShpk.GetTextureById(textureId); - if (shpkTexture is not { Slot: 2 }) + if (shpkTexture is not { Slot: 2 } && (shpkTexture is not null || textureId == TableSamplerId)) continue; var dkData = TryGetShpkDevkitData("Samplers", textureId, true); From abb47751c821b86eb8f7a3e88a9e12939a611d3a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 20:29:25 +0200 Subject: [PATCH 2247/2451] Mtrl editor: Disregard obsolete modded ShPks --- Penumbra/Interop/Processing/ShpkPathPreProcessor.cs | 4 ++-- .../UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 826771dd..ddd59121 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -49,7 +49,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, return null; } - private static SanityCheckResult SanityCheck(string path) + internal static SanityCheckResult SanityCheck(string path) { try { @@ -79,7 +79,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, _ => string.Empty, }; - private enum SanityCheckResult + internal enum SanityCheckResult { Success, IoError, diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index 202047e4..b76cffc2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -10,6 +10,7 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.Interop.Processing; using Penumbra.String.Classes; using static Penumbra.GameData.Files.ShpkFile; @@ -128,7 +129,11 @@ public partial class MtrlTab if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; - return _edit.FindBestMatch(defaultGamePath); + var path = _edit.FindBestMatch(defaultGamePath); + if (!path.IsRooted || ShpkPathPreProcessor.SanityCheck(path.FullName) == ShpkPathPreProcessor.SanityCheckResult.Success) + return path; + + return new FullPath(defaultPath); } private void LoadShpk(FullPath path) From c3be151d4023d6b4edc5985c259bfef8645cb4b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 2 Apr 2025 23:36:56 +0200 Subject: [PATCH 2248/2451] Maybe fix crash issue in AtchHook1 / issue with kept draw object links. --- .../Hooks/Objects/CharacterDestructor.cs | 3 ++ .../Interop/PathResolving/DrawObjectState.cs | 36 ++++++++++++++++++- Penumbra/UI/Changelog.cs | 1 - 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index ffe2f72d..55b392ba 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr IdentifiedCollectionCache = 0, + + /// + DrawObjectState = 0, } public CharacterDestructor(HookManager hooks) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 5e413fe2..28a0dd8d 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -15,6 +15,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; @@ -23,21 +24,24 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, - CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework) + CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework, CharacterDestructor characterDestructor) { _objects = objects; _createCharacterBase = createCharacterBase; _weaponReload = weaponReload; _characterBaseDestructor = characterBaseDestructor; _gameState = gameState; + _characterDestructor = characterDestructor; framework.RunOnFrameworkThread(InitializeDrawObjects); _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.DrawObjectState); } + public bool ContainsKey(nint key) => _drawObjectToGameObject.ContainsKey(key); @@ -68,6 +72,36 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary + /// Seems like sometimes the draw object of a game object is destroyed in frames after the original game object is already destroyed. + /// So protect against outdated game object pointers in the dictionary. + /// + private unsafe void OnCharacterDestructor(Character* a) + { + if (a is null) + return; + + var character = (nint)a; + var delete = stackalloc nint[5]; + var current = 0; + foreach (var (drawObject, (gameObject, _)) in _drawObjectToGameObject) + { + if (gameObject != character) + continue; + + delete[current++] = drawObject; + if (current is 4) + break; + } + + for (var ptr = delete; *ptr != nint.Zero; ++ptr) + { + _drawObjectToGameObject.Remove(*ptr, out var pair); + Penumbra.Log.Excessive($"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + } } private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 32abeb41..5e1612eb 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -90,7 +90,6 @@ public class PenumbraChangelog : IUiService .RegisterEntry("The EQP entry previously named Unknown 4 was renamed to 'Hide Glove Cuffs'.") .RegisterEntry("Fixed the changed item identification for EST changes.") .RegisterEntry("Fixed clipping issues in the changed items panel when no grouping was active."); - private static void Add1_3_5_0(Changelog log) From 09c2264de4bd93eec422fc4e69e9e40e0b8023f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 2 Apr 2025 23:41:08 +0200 Subject: [PATCH 2249/2451] Revert overeager BNPC Name update. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b6b91f84..ab63da80 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b6b91f846096d15276b728ba2078f27b95317d15 +Subproject commit ab63da8047f3d99240159bb1b17dbcb61d77326a From 2fdafc5c8581792acdd09c4a6e93bf8442b8edfa Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 2 Apr 2025 21:45:19 +0000 Subject: [PATCH 2250/2451] [CI] Updating repo.json for 1.3.6.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2c2088c3..f471e95e 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.5", - "TestingAssemblyVersion": "1.3.6.5", + "AssemblyVersion": "1.3.6.6", + "TestingAssemblyVersion": "1.3.6.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c3b2443ab52231658bfa73e36d0d42e671cc2c79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Apr 2025 22:35:23 +0200 Subject: [PATCH 2251/2451] Add Incognito modifier. --- Penumbra/Configuration.cs | 1 + Penumbra/EphemeralConfig.cs | 1 + .../Hooks/Objects/CharacterBaseDestructor.cs | 5 ++--- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Materials/MtrlTab.Textures.cs | 16 ++++++++-------- Penumbra/UI/Classes/CollectionSelectHeader.cs | 6 +++--- Penumbra/UI/IncognitoService.cs | 17 +++++++++++++---- Penumbra/UI/Tabs/SettingsTab.cs | 8 ++++++++ 8 files changed, 37 insertions(+), 19 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 939eb122..3a9bcdc4 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -94,6 +94,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control); public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true; public bool AutoReduplicateUiOnImport { get; set; } = true; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 24ab466b..678e53ad 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -41,6 +41,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService public string LastModPath { get; set; } = string.Empty; public bool AdvancedEditingOpen { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false; + public bool IncognitoMode { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index 7636718e..2d8e60b2 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -2,7 +2,6 @@ using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.Interop.Hooks.Objects; @@ -13,7 +12,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr DrawObjectState = 0, - /// + /// MtrlTab = -1000, } @@ -42,7 +41,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); + using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); } if (unfolded) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 54fcf279..d7a81876 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -61,7 +61,7 @@ public class CollectionSelectHeader : IUiService private void DrawTemporaryCheckbox() { - var hold = _config.DeleteModModifier.IsActive(); + var hold = _config.IncognitoModifier.IsActive(); using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) { var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); @@ -77,9 +77,9 @@ public class CollectionSelectHeader : IUiService } ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired.\n"u8); + "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired."u8); if (!hold) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to toggle."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {_config.IncognitoModifier} while clicking to toggle."); } private enum CollectionState diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index d58ea1ec..29358618 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using ImGuiNET; using Penumbra.UI.Classes; using OtterGui.Raii; using OtterGui.Services; @@ -6,19 +7,27 @@ using OtterGui.Text; namespace Penumbra.UI; -public class IncognitoService(TutorialService tutorial) : IService +public class IncognitoService(TutorialService tutorial, Configuration config) : IService { - public bool IncognitoMode; + public bool IncognitoMode + => config.Ephemeral.IncognitoMode; public void DrawToggle(float width) { + var hold = config.IncognitoModifier.IsActive(); var color = ColorId.FolderExpanded.Value(); using (ImRaii.PushFrameBorder(ImUtf8.GlobalScale, color)) { var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8; var icon = IncognitoMode ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; - if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color)) - IncognitoMode = !IncognitoMode; + if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color) && hold) + { + config.Ephemeral.IncognitoMode = !IncognitoMode; + config.Ephemeral.Save(); + } + + if (!hold) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); } tutorial.OpenTutorial(BasicTutorialSteps.Incognito); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ba226aa8..b1f82a91 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -621,6 +621,14 @@ public class SettingsTab : ITab, IUiService _config.DeleteModModifier = v; _config.Save(); }); + Widget.DoubleModifierSelector("Incognito Modifier", + "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", UiHelpers.InputTextWidth.X, + _config.IncognitoModifier, + v => + { + _config.IncognitoModifier = v; + _config.Save(); + }); } /// Draw all settings pertaining to import and export of mods. From 3b54485127dcbee68733d9eb5a5027c90e552619 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 14:42:25 +0200 Subject: [PATCH 2252/2451] Maybe fix AtchCaller crashes. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- Penumbra/Interop/PathResolving/CollectionResolver.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index c350c157..2a3d7468 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) { - var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + var collection = playerModel.Valid ? _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true) : _collectionResolver.DefaultCollection; _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 576b61bb..f14abbff 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -95,6 +95,10 @@ public sealed unsafe class CollectionResolver( return IdentifyCollection(obj, useCache); } + /// Get the default collection. + public ResolveData DefaultCollection + => collectionManager.Active.Default.ToResolveData(); + /// Return whether the given ModelChara id refers to a human-type model. public bool IsModelHuman(uint modelCharaId) => humanModels.IsHuman(modelCharaId); From 5437ab477f349682edb09781340f6db34fbc9be2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 5 Apr 2025 12:44:47 +0000 Subject: [PATCH 2253/2451] [CI] Updating repo.json for 1.3.6.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f471e95e..5163bb7d 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.6", - "TestingAssemblyVersion": "1.3.6.6", + "AssemblyVersion": "1.3.6.7", + "TestingAssemblyVersion": "1.3.6.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 93e60471de1b68cfc828fba0819b4d7ab06072cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 18:49:18 +0200 Subject: [PATCH 2254/2451] Update for new objectmanager. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 3396ee17..f53fd227 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3396ee176fa72ad2dfb2de3294f7125ebce4dae5 +Subproject commit f53fd227a242435ce44a9fe9c5e847d0ca788869 diff --git a/Penumbra.GameData b/Penumbra.GameData index ab63da80..4769bbcd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ab63da8047f3d99240159bb1b17dbcb61d77326a +Subproject commit 4769bbcdfce9e1d5a461c6b552b5b30ad6bc478e diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 42502290..6629c126 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -503,6 +503,8 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; + _objects.DrawDebug(); + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) From 0afcae45046c5e4b25b59757296500aa92d4aae2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 18:49:30 +0200 Subject: [PATCH 2255/2451] Run API redraws on framework. --- Penumbra/Api/Api/RedrawApi.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 82d14f7b..ec4de892 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -1,23 +1,32 @@ using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Interop.Services; namespace Penumbra.Api.Api; -public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService +public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService { public void RedrawObject(int gameObjectIndex, RedrawType setting) - => redrawService.RedrawObject(gameObjectIndex, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting)); + } public void RedrawObject(string name, RedrawType setting) - => redrawService.RedrawObject(name, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting)); + } public void RedrawObject(IGameObject? gameObject, RedrawType setting) - => redrawService.RedrawObject(gameObject, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting)); + } public void RedrawAll(RedrawType setting) - => redrawService.RedrawAll(setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting)); + } public event GameObjectRedrawnDelegate? GameObjectRedrawn { From 33ada1d9949b6b68315e0e6f96f6714973250128 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 8 Apr 2025 16:56:23 +0200 Subject: [PATCH 2256/2451] Remove meta-default-value checking from TT imports, move it entirely to mod loads, and keep default-valued entries if other options actually edit the same entry. --- .editorconfig | 12 ++ OtterGui | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 31 +--- Penumbra/Import/TexToolsMeta.Rgsp.cs | 7 +- Penumbra/Import/TexToolsMeta.cs | 9 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 162 ++++++++++++++++-- Penumbra/Mods/Manager/ModMigration.cs | 10 +- Penumbra/Mods/ModCreator.cs | 36 ++-- 8 files changed, 194 insertions(+), 75 deletions(-) diff --git a/.editorconfig b/.editorconfig index c645b573..f0328fd7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3576,6 +3576,18 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_ resharper_xaml_x_key_attribute_disallowed_highlighting=error resharper_xml_doc_comment_syntax_problem_highlighting=warning resharper_xunit_xunit_test_with_console_output_highlighting=warning +csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion +csharp_style_expression_bodied_methods = true:silent +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_expression_bodied_properties = true:silent [*.{cshtml,htm,html,proto,razor}] indent_style=tab diff --git a/OtterGui b/OtterGui index f53fd227..21ddfccb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f53fd227a242435ce44a9fe9c5e847d0ca788869 +Subproject commit 21ddfccb91ba3fa56e1c191e706ff91bffaa9515 diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 1f970dfe..7861a95b 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -2,7 +2,6 @@ using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Import.Structs; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -19,9 +18,7 @@ public partial class TexToolsMeta var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot); var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask; - var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask; - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Eqdp Entries and add them to the list if they are non-default. @@ -41,11 +38,9 @@ public partial class TexToolsMeta continue; var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr); - var mask = Eqdp.Mask(metaFileInfo.EquipSlot); - var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; - var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask; - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + var mask = Eqdp.Mask(metaFileInfo.EquipSlot); + var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; + MetaManipulations.TryAdd(identifier, value); } } @@ -55,10 +50,9 @@ public partial class TexToolsMeta if (data == null) return; - var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); - var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); - if (_keepDefault || value != def) - MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); + var identifier = new GmpIdentifier(metaFileInfo.PrimaryId); + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Est Entries and add them to the list if they are non-default. @@ -86,9 +80,7 @@ public partial class TexToolsMeta continue; var identifier = new EstIdentifier(id, type, gr); - var def = EstFile.GetDefault(_metaFileManager, type, gr, id); - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + MetaManipulations.TryAdd(identifier, value); } } @@ -108,15 +100,10 @@ public partial class TexToolsMeta { var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId, metaFileInfo.EquipSlot, metaFileInfo.SecondaryType); - var file = new ImcFile(_metaFileManager, identifier); - var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { identifier = identifier with { Variant = (Variant)i }; - var def = file.GetEntry(partIdx, (Variant)i); - if (_keepDefault || def != value && identifier.Validate()) - MetaManipulations.TryAdd(identifier, value); - + MetaManipulations.TryAdd(identifier, value); ++i; } } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 7b0bb5a8..77c70e6c 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -1,6 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -8,7 +7,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Parse a single rgsp file. - public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault) + public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data) { if (data.Length != 45 && data.Length != 42) { @@ -70,9 +69,7 @@ public partial class TexToolsMeta void Add(RspAttribute attribute, float value) { var identifier = new RspIdentifier(subRace, attribute); - var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def.Value) - ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); + ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); } } } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index c4a8e81f..f98eddbe 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -23,15 +23,11 @@ public partial class TexToolsMeta // The info class determines the files or table locations the changes need to apply to from the filename. public readonly uint Version; public readonly string FilePath; - public readonly MetaDictionary MetaManipulations = new(); - private readonly bool _keepDefault; + public readonly MetaDictionary MetaManipulations = new(); - private readonly MetaFileManager _metaFileManager; - public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault) + public TexToolsMeta(GamePathParser parser, byte[] data) { - _metaFileManager = metaFileManager; - _keepDefault = keepDefault; try { using var reader = new BinaryReader(new MemoryStream(data)); @@ -79,7 +75,6 @@ public partial class TexToolsMeta private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version) { - _metaFileManager = metaFileManager; FilePath = filePath; Version = version; } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index c5c8fb8b..050dab51 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,11 +1,15 @@ using System.Collections.Frozen; +using OtterGui.Classes; using OtterGui.Services; using Penumbra.Collections.Cache; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; +using Penumbra.Services; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Mods.Editor; @@ -68,13 +72,157 @@ public class ModMetaEditor( Changes = false; } - public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) + public static bool DeleteDefaultValues(Mod mod, MetaFileManager metaFileManager, SaveService? saveService, bool deleteAll = false) + { + if (deleteAll) + { + var changes = false; + foreach (var container in mod.AllDataContainers) + { + if (!DeleteDefaultValues(metaFileManager, container.Manipulations)) + continue; + + saveService?.ImmediateSaveSync(new ModSaveGroup(container, metaFileManager.Config.ReplaceNonAsciiOnImport)); + changes = true; + } + + return changes; + } + + var defaultEntries = new MultiDictionary(); + var actualEntries = new HashSet(); + if (!FilterDefaultValues(mod.AllDataContainers, metaFileManager, defaultEntries, actualEntries)) + return false; + + var groups = new HashSet(); + DefaultSubMod? defaultMod = null; + foreach (var (defaultIdentifier, containers) in defaultEntries.Grouped) + { + if (!deleteAll && actualEntries.Contains(defaultIdentifier)) + continue; + + foreach (var container in containers) + { + if (!container.Manipulations.Remove(defaultIdentifier)) + continue; + + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {defaultIdentifier}."); + if (container.Group is { } group) + groups.Add(group); + else if (container is DefaultSubMod d) + defaultMod = d; + } + } + + if (saveService is not null) + { + if (defaultMod is not null) + saveService.ImmediateSaveSync(new ModSaveGroup(defaultMod, metaFileManager.Config.ReplaceNonAsciiOnImport)); + foreach (var group in groups) + saveService.ImmediateSaveSync(new ModSaveGroup(group, metaFileManager.Config.ReplaceNonAsciiOnImport)); + } + + return defaultMod is not null || groups.Count > 0; + } + + public void DeleteDefaultValues() + => Changes = DeleteDefaultValues(metaFileManager, this); + + public void Apply(IModDataContainer container) + { + if (!Changes) + return; + + groupEditor.SetManipulations(container, this); + Changes = false; + } + + private static bool FilterDefaultValues(IEnumerable containers, MetaFileManager metaFileManager, + MultiDictionary defaultEntries, HashSet actualEntries) + { + if (!metaFileManager.CharacterUtility.Ready) + { + Penumbra.Log.Warning("Trying to filter default meta values before CharacterUtility was ready, skipped."); + return false; + } + + foreach (var container in containers) + { + foreach (var (key, value) in container.Manipulations.Imc) + { + var defaultEntry = ImcChecker.GetDefaultEntry(key, false); + if (defaultEntry.Entry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Eqp) + { + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Eqdp) + { + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Est) + { + var defaultEntry = EstFile.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Gmp) + { + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Rsp) + { + var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Atch) + { + var defaultEntry = AtchCache.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + } + + return true; + } + + private static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { if (!metaFileManager.CharacterUtility.Ready) { Penumbra.Log.Warning("Trying to delete default meta values before CharacterUtility was ready, skipped."); return false; } + var clone = dict.Clone(); dict.ClearForDefault(); @@ -189,16 +337,4 @@ public class ModMetaEditor( Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); return true; } - - public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(metaFileManager, this); - - public void Apply(IModDataContainer container) - { - if (!Changes) - return; - - groupEditor.SetManipulations(container, this); - Changes = false; - } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 3e58c515..8b5b80d0 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -82,9 +83,8 @@ public static partial class ModMigration foreach (var (gamePath, swapPath) in swaps) mod.Default.FileSwaps.Add(gamePath, swapPath); - creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true); - foreach (var group in mod.Groups) - saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); + creator.IncorporateAllMetaChanges(mod, true, true); + saveService.SaveAllOptionGroups(mod, false, creator.Config.ReplaceNonAsciiOnImport); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -182,7 +182,7 @@ public static partial class ModMigration Description = option.OptionDesc, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; } @@ -196,7 +196,7 @@ public static partial class ModMigration Priority = priority, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0db83ef9..f4f182eb 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -80,14 +80,10 @@ public partial class ModCreator( LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) - IncorporateAllMetaChanges(mod, true); - if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) - foreach (var container in mod.AllDataContainers) - { - if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) - saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); - } - + IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges); + else if (deleteDefaultMetaChanges) + ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); + return true; } @@ -158,19 +154,21 @@ public partial class ModCreator( /// Convert all .meta and .rgsp files to their respective meta changes and add them to their options. /// Deletes the source files if delete is true. /// - public void IncorporateAllMetaChanges(Mod mod, bool delete) + public void IncorporateAllMetaChanges(Mod mod, bool delete, bool removeDefaultValues) { var changes = false; - List deleteList = new(); + List deleteList = []; foreach (var subMod in mod.AllDataContainers) { - var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true); + var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); changes |= localChanges; if (delete) deleteList.AddRange(localDeleteList); } DeleteDeleteList(deleteList, delete); + if (removeDefaultValues && !Config.KeepDefaultMetaChanges) + changes |= ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, null, false); if (!changes) return; @@ -184,8 +182,7 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, - bool deleteDefault) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -202,8 +199,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName), - Config.KeepDefaultMetaChanges); + var meta = new TexToolsMeta(gamePathParser, File.ReadAllBytes(file.FullName)); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -215,8 +211,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), - Config.KeepDefaultMetaChanges); + var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName)); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -232,9 +227,6 @@ public partial class ModCreator( DeleteDeleteList(deleteList, delete); var changes = oldSize < option.Manipulations.Count; - if (deleteDefault && !Config.KeepDefaultMetaChanges) - changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, option.Manipulations); - return (changes, deleteList); } @@ -289,7 +281,7 @@ public partial class ModCreator( foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); - IncorporateMetaChanges(mod, baseFolder, true, true); + IncorporateMetaChanges(mod, baseFolder, true); return mod; } @@ -308,7 +300,7 @@ public partial class ModCreator( mod.Default.Files.TryAdd(gamePath, file); } - IncorporateMetaChanges(mod.Default, directory, true, true); + IncorporateMetaChanges(mod.Default, directory, true); saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } From 129156a1c195f0cbce0c5bc464b4d6c8402d0ea1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Apr 2025 15:04:47 +0200 Subject: [PATCH 2257/2451] Add some more safety and better IPC for draw object storage. --- Penumbra.Api | 2 +- Penumbra/Api/Api/GameStateApi.cs | 28 ++++++++++- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + .../PathResolving/CollectionResolver.cs | 9 ++-- .../Interop/PathResolving/DrawObjectState.cs | 50 ++++++++++++------- Penumbra/Penumbra.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 38 +++++++------- 8 files changed, 89 insertions(+), 45 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index bd56d828..47bd5424 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee +Subproject commit 47bd5424d04c667d0df1ac1dd1eeb3e50b476c2c diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index 7f70c6bf..74cde3a0 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable { private readonly CommunicatorService _communicator; private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; private readonly CutsceneService _cutsceneService; private readonly ResourceLoader _resourceLoader; public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, - ResourceLoader resourceLoader) + ResourceLoader resourceLoader, DrawObjectState drawObjectState) { _communicator = communicator; _collectionResolver = collectionResolver; _cutsceneService = cutsceneService; _resourceLoader = resourceLoader; + _drawObjectState = drawObjectState; _resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.PapRequested += OnPapRequested; _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); @@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public int GetCutsceneParentIndex(int actorIdx) => _cutsceneService.GetParentIndex(actorIdx); + public Func GetCutsceneParentIndexFunc() + { + var weakRef = new WeakReference(_cutsceneService); + return idx => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed."); + + return c.GetParentIndex(idx); + }; + } + + public Func GetGameObjectFromDrawObjectFunc() + { + var weakRef = new WeakReference(_drawObjectState); + return model => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed."); + + return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero; + }; + } + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) ? PenumbraApiEc.Success diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 47d44cfc..38125627 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 8; + public const int FeatureVersion = 9; public void Dispose() { diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index d54faa6c..f5a6c16d 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState), IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState), + IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState), + IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState), IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta), IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta), diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index f14abbff..02e1be54 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -89,10 +89,13 @@ public sealed unsafe class CollectionResolver( /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) { - var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + if (drawObject is null) + return DefaultCollection; + + Actor obj = drawObjectState.TryGetValue(drawObject, out var gameObject) ? gameObject.Item1 - : drawObjectState.LastGameObject); - return IdentifyCollection(obj, useCache); + : drawObjectState.LastGameObject; + return IdentifyCollection(obj.AsObject, useCache); } /// Get the default collection. diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 28a0dd8d..6f3e457c 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -9,7 +9,7 @@ using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; -public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService +public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { private readonly ObjectManager _objects; private readonly CreateCharacterBase _createCharacterBase; @@ -18,7 +18,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; + private readonly Dictionary _drawObjectToGameObject = []; public nint LastGameObject => _gameState.LastGameObject; @@ -41,11 +41,10 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.ContainsKey(key); - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() => _drawObjectToGameObject.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -54,16 +53,28 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.Count; - public bool TryGetValue(nint drawObject, out (nint, bool) gameObject) - => _drawObjectToGameObject.TryGetValue(drawObject, out gameObject); + public bool TryGetValue(Model drawObject, out (Actor, ObjectIndex, bool) gameObject) + { + if (!_drawObjectToGameObject.TryGetValue(drawObject, out gameObject)) + return false; - public (nint, bool) this[nint key] + var currentObject = _objects[gameObject.Item2]; + if (currentObject != gameObject.Item1) + { + Penumbra.Log.Warning($"[DrawObjectState] Stored association {drawObject} -> {gameObject.Item1} has index {gameObject.Item2}, which resolves to {currentObject}."); + return false; + } + + return true; + } + + public (Actor, ObjectIndex, bool) this[Model key] => _drawObjectToGameObject[key]; - public IEnumerable Keys + public IEnumerable Keys => _drawObjectToGameObject.Keys; - public IEnumerable<(nint, bool)> Values + public IEnumerable<(Actor, ObjectIndex, bool)> Values => _drawObjectToGameObject.Values; public unsafe void Dispose() @@ -87,20 +98,21 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + Penumbra.Log.Excessive( + $"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject.Address:X}, {pair.IsChild})."); } } @@ -119,9 +131,9 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary @@ -137,12 +149,12 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionaryChildObject, gameObject, true, true); if (!iterate) return; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 79c7f2db..7f4c1b23 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,7 +21,6 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; -using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -190,7 +189,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6629c126..9dd18ddd 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -569,29 +569,31 @@ public class DebugTab : Window, ITab, IUiService { if (drawTree) { - using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###DrawObjectResolverTable", 8, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState - .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) - .ThenBy(kvp => kvp.Value.Item2) - .ThenBy(kvp => kvp.Key)) + foreach (var (drawObject, (gameObjectPtr, idx, child)) in _drawObjectState + .OrderBy(kvp => kvp.Value.Item2.Index) + .ThenBy(kvp => kvp.Value.Item3) + .ThenBy(kvp => kvp.Key.Address)) { - var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"{drawObject}"); + ImUtf8.DrawTableColumn($"{gameObjectPtr.Index}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, gameObjectPtr.Index != idx)) + { + ImUtf8.DrawTableColumn($"{idx}"); + } - ImGuiUtil.CopyOnClickSelectable($"0x{drawObject:X}"); + ImUtf8.DrawTableColumn(child ? "Child"u8 : "Main"u8); ImGui.TableNextColumn(); - ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(child ? "Child" : "Main"); - ImGui.TableNextColumn(); - var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); - ImGuiUtil.CopyOnClickSelectable(address); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - ImGui.TableNextColumn(); - var collection = _collectionResolver.IdentifyCollection(gameObject, true); - ImGui.TextUnformatted(collection.ModCollection.Identity.Name); + ImUtf8.CopyOnClickSelectable($"{gameObjectPtr}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, _objects[idx] != gameObjectPtr)) + { + ImUtf8.DrawTableColumn($"{_objects[idx]}"); + } + ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span); + var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true); + ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name); } } } From 0ec6a17ac7c83c350f90fb6856c548f091b67f34 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:21 +0200 Subject: [PATCH 2258/2451] Add context to open backup directory. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1ccee2cb..1e6afa09 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -121,32 +121,42 @@ public class ModPanelEditTab( : backup.Exists ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." : $"Create exported archive of current mod at \"{backup.Name}\"."; - if (ImGuiUtil.DrawDisabledButton("Export Mod", buttonSize, tt, ModBackup.CreatingBackup)) + if (ImUtf8.ButtonEx("Export Mod"u8, tt, buttonSize, ModBackup.CreatingBackup)) backup.CreateAsync(); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + ImGui.SameLine(); tt = backup.Exists ? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; - if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) + if (ImUtf8.ButtonEx("Delete Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Delete(); tt = backup.Exists ? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) + if (ImUtf8.ButtonEx("Restore From Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Restore(modManager); if (backup.Exists) { ImGui.SameLine(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) { - ImGui.TextUnformatted(FontAwesomeIcon.CheckCircle.ToIconString()); + ImUtf8.Text(FontAwesomeIcon.CheckCircle.ToIconString()); } - ImGuiUtil.HoverTooltip($"Export exists in \"{backup.Name}\"."); + ImUtf8.HoverTooltip($"Export exists in \"{backup.Name}\"."); } + + using var context = ImUtf8.Popup("context"u8); + if (!context) + return; + + if (ImUtf8.Selectable("Open Backup Directory"u8)) + Process.Start(new ProcessStartInfo(modExportManager.ExportDirectory.FullName) { UseShellExecute = true }); } /// Anything about editing the regular meta information about the mod. From dc336569ff6d79a9ab08cbf40670c74aed3065a5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:36 +0200 Subject: [PATCH 2259/2451] Add context to copy the full file path from redirections. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 6792c359..071b0551 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -210,6 +210,9 @@ public partial class ModEditWindow if (!context) return; + if (ImUtf8.Selectable("Copy Full File Path")) + ImUtf8.SetClipboardText(registry.File.FullName); + using (ImRaii.Disabled(registry.CurrentUsage == 0)) { if (ImUtf8.Selectable("Copy Game Paths"u8)) @@ -244,10 +247,8 @@ public partial class ModEditWindow using (ImRaii.Disabled(_cutPaths.Count == 0)) { if (ImUtf8.Selectable("Paste Game Paths"u8)) - { foreach (var path in _cutPaths) _editor.FileEditor.SetGamePath(_editor.Option!, i, -1, path); - } } } From f9b5a626cfa2da3b245cc5d982662a3fec76795a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:49 +0200 Subject: [PATCH 2260/2451] Add some migration stuff. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/MigrationManager.cs | 67 +---- Penumbra/Services/ModMigrator.cs | 331 +++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 9 +- Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs | 55 ++++ 6 files changed, 397 insertions(+), 69 deletions(-) create mode 100644 Penumbra/Services/ModMigrator.cs create mode 100644 Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs diff --git a/Penumbra.Api b/Penumbra.Api index 47bd5424..14652039 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 47bd5424d04c667d0df1ac1dd1eeb3e50b476c2c +Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc diff --git a/Penumbra.GameData b/Penumbra.GameData index 4769bbcd..e10d8f33 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4769bbcdfce9e1d5a461c6b552b5b30ad6bc478e +Subproject commit e10d8f33a676ff4544d7ca05a93d555416f41222 diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index abc059e9..2438c0ad 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,76 +1,13 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; -using Penumbra.Api.Enums; -using Penumbra.GameData.Files; -using Penumbra.Mods; -using Penumbra.String.Classes; using SharpCompress.Common; using SharpCompress.Readers; +using MdlFile = Penumbra.GameData.Files.MdlFile; +using MtrlFile = Penumbra.GameData.Files.MtrlFile; namespace Penumbra.Services; -public class ModMigrator -{ - private class FileData(string path) - { - public readonly string Path = path; - public readonly List<(string GamePath, int Option)> GamePaths = []; - } - - private sealed class FileDataDict : Dictionary - { - public void Add(string path, string gamePath, int option) - { - if (!TryGetValue(path, out var data)) - { - data = new FileData(path); - data.GamePaths.Add((gamePath, option)); - Add(path, data); - } - else - { - data.GamePaths.Add((gamePath, option)); - } - } - } - - private readonly FileDataDict Textures = []; - private readonly FileDataDict Models = []; - private readonly FileDataDict Materials = []; - - public void Update(IEnumerable mods) - { - CollectFiles(mods); - } - - private void CollectFiles(IEnumerable mods) - { - var option = 0; - foreach (var mod in mods) - { - AddDict(mod.Default.Files, option++); - foreach (var container in mod.Groups.SelectMany(group => group.DataContainers)) - AddDict(container.Files, option++); - } - - return; - - void AddDict(Dictionary dict, int currentOption) - { - foreach (var (gamePath, file) in dict) - { - switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) - { - case ResourceType.Tex: Textures.Add(file.FullName, gamePath.ToString(), currentOption); break; - case ResourceType.Mdl: Models.Add(file.FullName, gamePath.ToString(), currentOption); break; - case ResourceType.Mtrl: Materials.Add(file.FullName, gamePath.ToString(), currentOption); break; - } - } - } - } -} - public class MigrationManager(Configuration config) : IService { public enum TaskType : byte diff --git a/Penumbra/Services/ModMigrator.cs b/Penumbra/Services/ModMigrator.cs new file mode 100644 index 00000000..20005c8f --- /dev/null +++ b/Penumbra/Services/ModMigrator.cs @@ -0,0 +1,331 @@ +using Dalamud.Plugin.Services; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Import.Textures; +using Penumbra.Mods; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Services; + +public class ModMigrator(IDataManager gameData, TextureManager textures) : IService +{ + private sealed class FileDataDict : MultiDictionary; + + private readonly Lazy _glassReferenceMaterial = new(() => + { + var bytes = gameData.GetFile("chara/equipment/e5001/material/v0001/mt_c0101e5001_met_b.mtrl"); + return new MtrlFile(bytes!.Data); + }); + + private readonly HashSet _changedMods = []; + private readonly HashSet _failedMods = []; + + private readonly FileDataDict Textures = []; + private readonly FileDataDict Models = []; + private readonly FileDataDict Materials = []; + private readonly FileDataDict FileSwaps = []; + + private readonly ConcurrentBag _messages = []; + + public void Update(IEnumerable mods) + { + CollectFiles(mods); + foreach (var (from, (to, container)) in FileSwaps) + MigrateFileSwaps(from, to, container); + foreach (var (model, list) in Models.Grouped) + MigrateModel(model, (Mod)list[0].Container.Mod); + } + + private void CollectFiles(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var container in mod.AllDataContainers) + { + foreach (var (gamePath, file) in container.Files) + { + switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) + { + case ResourceType.Tex: Textures.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + case ResourceType.Mdl: Models.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + case ResourceType.Mtrl: Materials.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + } + } + + foreach (var (swapFrom, swapTo) in container.FileSwaps) + FileSwaps.TryAdd(swapTo.FullName, (swapFrom.ToString(), container)); + } + } + } + + public Task CreateIndexFile(string normalPath, string targetPath) + { + const int rowBlend = 17; + + return Task.Run(async () => + { + var tex = textures.LoadTex(normalPath); + var data = tex.GetPixelData(); + var rgbaData = new RgbaPixelData(data.Width, data.Height, data.Rgba); + if (!BitOperations.IsPow2(rgbaData.Height) || !BitOperations.IsPow2(rgbaData.Width)) + { + var requiredHeight = (int)BitOperations.RoundUpToPowerOf2((uint)rgbaData.Height); + var requiredWidth = (int)BitOperations.RoundUpToPowerOf2((uint)rgbaData.Width); + rgbaData = rgbaData.Resize((requiredWidth, requiredHeight)); + } + + Parallel.ForEach(Enumerable.Range(0, rgbaData.PixelData.Length / 4), idx => + { + var pixelIdx = 4 * idx; + var normal = rgbaData.PixelData[pixelIdx + 3]; + + // Copied from TT + var blendRem = normal % (2 * rowBlend); + var originalRow = normal / rowBlend; + switch (blendRem) + { + // Goes to next row, clamped to the closer row. + case > 25: + blendRem = 0; + ++originalRow; + break; + // Stays in this row, clamped to the closer row. + case > 17: blendRem = 17; break; + } + + var newBlend = (byte)(255 - MathF.Round(blendRem / 17f * 255f)); + + // Slight add here to push the color deeper into the row to ensure BC5 compression doesn't + // cause any artifacting. + var newRow = (byte)(originalRow / 2 * 17 + 4); + + rgbaData.PixelData[pixelIdx] = newRow; + rgbaData.PixelData[pixelIdx] = newBlend; + rgbaData.PixelData[pixelIdx] = 0; + rgbaData.PixelData[pixelIdx] = 255; + }); + await textures.SaveAs(CombinedTexture.TextureSaveType.BC5, true, true, new BaseImage(), targetPath, rgbaData.PixelData, + rgbaData.Width, rgbaData.Height); + }); + } + + private void MigrateModel(string filePath, Mod mod) + { + if (MigrationManager.TryMigrateSingleModel(filePath, true)) + { + _messages.Add($"Migrated model {filePath} in {mod.Name}."); + } + else + { + _messages.Add($"Failed to migrate model {filePath} in {mod.Name}"); + _failedMods.Add(mod); + } + } + + private void SetGlassReferenceValues(MtrlFile mtrl) + { + var reference = _glassReferenceMaterial.Value; + mtrl.ShaderPackage.ShaderKeys = reference.ShaderPackage.ShaderKeys.ToArray(); + mtrl.ShaderPackage.Constants = reference.ShaderPackage.Constants.ToArray(); + mtrl.AdditionalData = reference.AdditionalData.ToArray(); + mtrl.ShaderPackage.Flags &= ~(0x04u | 0x08u); + // From TT. + if (mtrl.Table is ColorTable t) + foreach (ref var row in t.AsRows()) + row.SpecularColor = new HalfColor((Half)0.8100586, (Half)0.8100586, (Half)0.8100586); + } + + private ref struct MaterialPack + { + public readonly MtrlFile File; + public readonly bool UsesMaskAsSpecular; + + private readonly Dictionary Samplers = []; + + public MaterialPack(MtrlFile file) + { + File = file; + UsesMaskAsSpecular = File.ShaderPackage.ShaderKeys.Any(x => x.Key is 0xC8BD1DEF && x.Value is 0xA02F4828 or 0x198D11CD); + Add(Samplers, TextureUsage.Normal, ShpkFile.NormalSamplerId); + Add(Samplers, TextureUsage.Index, ShpkFile.IndexSamplerId); + Add(Samplers, TextureUsage.Mask, ShpkFile.MaskSamplerId); + Add(Samplers, TextureUsage.Diffuse, ShpkFile.DiffuseSamplerId); + Add(Samplers, TextureUsage.Specular, ShpkFile.SpecularSamplerId); + return; + + void Add(Dictionary dict, TextureUsage usage, uint samplerId) + { + var idx = new SamplerIndex(file, samplerId); + if (idx.Texture >= 0) + dict.Add(usage, idx); + } + } + + public readonly record struct SamplerIndex(int Sampler, int Texture) + { + public SamplerIndex(MtrlFile file, uint samplerId) + : this(file.FindSampler(samplerId), -1) + => Texture = Sampler < 0 ? -1 : file.ShaderPackage.Samplers[Sampler].TextureIndex; + } + + public enum TextureUsage + { + Normal, + Index, + Mask, + Diffuse, + Specular, + } + + public static bool AdaptPath(IDataManager data, string path, TextureUsage usage, out string newPath) + { + newPath = path; + if (Path.GetExtension(newPath) is not ".tex") + return false; + + if (data.FileExists(newPath)) + return true; + + switch (usage) + { + case TextureUsage.Normal: + newPath = path.Replace("_n.tex", "_norm.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_n_", "_norm_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Index: return false; + case TextureUsage.Mask: + newPath = path.Replace("_m.tex", "_mult.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m.tex", "_mask.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m_", "_mult_"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m_", "_mask_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Diffuse: + newPath = path.Replace("_d.tex", "_base.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_d_", "_base_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Specular: + return false; + default: throw new ArgumentOutOfRangeException(nameof(usage), usage, null); + } + } + } + + private void MigrateMaterial(string filePath, IReadOnlyList<(string GamePath, IModDataContainer Container)> redirections) + { + try + { + var bytes = File.ReadAllBytes(filePath); + var mtrl = new MtrlFile(bytes); + if (!CheckUpdateNeeded(mtrl)) + return; + + // Update colorsets, flags and character shader package. + var changes = mtrl.MigrateToDawntrail(); + + if (!changes) + switch (mtrl.ShaderPackage.Name) + { + case "hair.shpk": break; + case "characterglass.shpk": + SetGlassReferenceValues(mtrl); + changes = true; + break; + } + + // Remove DX11 flags and update paths if necessary. + foreach (ref var tex in mtrl.Textures.AsSpan()) + { + if (tex.DX11) + { + changes = true; + if (GamePaths.Tex.HandleDx11Path(tex, out var newPath)) + tex.Path = newPath; + tex.DX11 = false; + } + + if (gameData.FileExists(tex.Path)) + continue; + } + + // Dyeing, from TT. + if (mtrl.DyeTable is ColorDyeTable dye) + foreach (ref var row in dye.AsRows()) + row.Template += 1000; + } + catch + { + // ignored + } + + static bool CheckUpdateNeeded(MtrlFile mtrl) + { + if (!mtrl.IsDawntrail) + return true; + + if (mtrl.ShaderPackage.Name is not "hair.shpk") + return false; + + var foundOld = 0; + foreach (var c in mtrl.ShaderPackage.Constants) + { + switch (c.Id) + { + case 0x36080AD0: foundOld |= 1; break; // == 1, from TT + case 0x992869AB: foundOld |= 2; break; // == 3 (skin) or 4 (hair) from TT + } + + if (foundOld is 3) + return true; + } + + return false; + } + } + + private void MigrateFileSwaps(string swapFrom, string swapTo, IModDataContainer container) + { + var fromExists = gameData.FileExists(swapFrom); + var toExists = gameData.FileExists(swapTo); + if (fromExists && toExists) + return; + + if (ResourceTypeExtensions.FromExtension(Path.GetExtension(swapFrom.AsSpan())) is not ResourceType.Tex + || ResourceTypeExtensions.FromExtension(Path.GetExtension(swapTo.AsSpan())) is not ResourceType.Tex) + { + _messages.Add( + $"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}. Only textures may be migrated.{(fromExists ? "\n\tSource File does not exist." : "")}{(toExists ? "\n\tTarget File does not exist." : "")}"); + return; + } + + // try to migrate texture swaps + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9dd18ddd..6d6222ec 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -106,6 +106,7 @@ public class DebugTab : Window, ITab, IUiService private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; private readonly RenderTargetDrawer _renderTargetDrawer; + private readonly ModMigratorDebug _modMigratorDebug; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -116,7 +117,8 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, + ModMigratorDebug modMigratorDebug) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -158,6 +160,7 @@ public class DebugTab : Window, ITab, IUiService _schedulerService = schedulerService; _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; + _modMigratorDebug = modMigratorDebug; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -190,6 +193,7 @@ public class DebugTab : Window, ITab, IUiService DrawActorsDebug(); DrawCollectionCaches(); _texHeaderDrawer.Draw(); + _modMigratorDebug.Draw(); DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); @@ -591,6 +595,7 @@ public class DebugTab : Window, ITab, IUiService { ImUtf8.DrawTableColumn($"{_objects[idx]}"); } + ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span); var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true); ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name); @@ -751,7 +756,7 @@ public class DebugTab : Window, ITab, IUiService DrawChangedItemTest(); } - private string _changedItemPath = string.Empty; + private string _changedItemPath = string.Empty; private readonly Dictionary _changedItems = []; private void DrawChangedItemTest() diff --git a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs new file mode 100644 index 00000000..c8518315 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs @@ -0,0 +1,55 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Services; + +namespace Penumbra.UI.Tabs.Debug; + +public class ModMigratorDebug(ModMigrator migrator) : IUiService +{ + private string _inputPath = string.Empty; + private string _outputPath = string.Empty; + private Task? _indexTask; + private Task? _mdlTask; + + public void Draw() + { + if (!ImUtf8.CollapsingHeaderId("Mod Migrator"u8)) + return; + + ImUtf8.InputText("##input"u8, ref _inputPath, "Input Path..."u8); + ImUtf8.InputText("##output"u8, ref _outputPath, "Output Path..."u8); + + if (ImUtf8.ButtonEx("Create Index Texture"u8, "Requires input to be a path to a normal texture."u8, default, _inputPath.Length == 0 + || _outputPath.Length == 0 + || _indexTask is + { + IsCompleted: false, + })) + _indexTask = migrator.CreateIndexFile(_inputPath, _outputPath); + + if (_indexTask is not null) + { + ImGui.SameLine(); + ImUtf8.TextFrameAligned($"{_indexTask.Status}"); + } + + if (ImUtf8.ButtonEx("Update Model File"u8, "Requires input to be a path to a mdl."u8, default, _inputPath.Length == 0 + || _outputPath.Length == 0 + || _mdlTask is + { + IsCompleted: false, + })) + _mdlTask = Task.Run(() => + { + File.Copy(_inputPath, _outputPath, true); + MigrationManager.TryMigrateSingleModel(_outputPath, false); + }); + + if (_mdlTask is not null) + { + ImGui.SameLine(); + ImUtf8.TextFrameAligned($"{_mdlTask.Status}"); + } + } +} From f03a139e0e32acf0e35fff61cecd078f0ef72335 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:17:23 +0200 Subject: [PATCH 2261/2451] blech --- Penumbra/Services/ModMigrator.cs | 136 +++++++++++++++++-------------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/Penumbra/Services/ModMigrator.cs b/Penumbra/Services/ModMigrator.cs index 20005c8f..043d9631 100644 --- a/Penumbra/Services/ModMigrator.cs +++ b/Penumbra/Services/ModMigrator.cs @@ -1,17 +1,18 @@ -using Dalamud.Plugin.Services; -using OtterGui.Classes; -using OtterGui.Services; -using Penumbra.Api.Enums; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Import.Textures; -using Penumbra.Mods; -using Penumbra.Mods.SubMods; - -namespace Penumbra.Services; - +using Dalamud.Plugin.Services; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Import.Textures; +using Penumbra.Mods; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Services; + public class ModMigrator(IDataManager gameData, TextureManager textures) : IService { private sealed class FileDataDict : MultiDictionary; @@ -175,6 +176,7 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ public enum TextureUsage { + Unknown, Normal, Index, Mask, @@ -191,51 +193,44 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ if (data.FileExists(newPath)) return true; - switch (usage) + ReadOnlySpan<(string, string)> pairs = usage switch { - case TextureUsage.Normal: - newPath = path.Replace("_n.tex", "_norm.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_n_", "_norm_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Index: return false; - case TextureUsage.Mask: - newPath = path.Replace("_m.tex", "_mult.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m.tex", "_mask.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m_", "_mult_"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m_", "_mask_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Diffuse: - newPath = path.Replace("_d.tex", "_base.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_d_", "_base_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Specular: - return false; - default: throw new ArgumentOutOfRangeException(nameof(usage), usage, null); + TextureUsage.Unknown => + [ + ("_n.tex", "_norm.tex"), + ("_m.tex", "_mult.tex"), + ("_m.tex", "_mask.tex"), + ("_d.tex", "_base.tex"), + ], + TextureUsage.Normal => + [ + ("_n_", "_norm_"), + ("_n.tex", "_norm.tex"), + ], + TextureUsage.Mask => + [ + ("_m_", "_mult_"), + ("_m_", "_mask_"), + ("_m.tex", "_mult.tex"), + ("_m.tex", "_mask.tex"), + ], + TextureUsage.Diffuse => + [ + ("_d_", "_base_"), + ("_d.tex", "_base.tex"), + ], + TextureUsage.Index => [], + TextureUsage.Specular => [], + _ => [], + }; + foreach (var (from, to) in pairs) + { + newPath = path.Replace(from, to); + if (data.FileExists(newPath)) + return true; } + + return false; } } @@ -326,6 +321,29 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ return; } - // try to migrate texture swaps + var newSwapFrom = swapFrom; + if (!fromExists && !MaterialPack.AdaptPath(gameData, swapFrom, MaterialPack.TextureUsage.Unknown, out newSwapFrom)) + { + _messages.Add($"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}."); + return; + } + + var newSwapTo = swapTo; + if (!toExists && !MaterialPack.AdaptPath(gameData, swapTo, MaterialPack.TextureUsage.Unknown, out newSwapTo)) + { + _messages.Add($"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}."); + return; + } + + if (!Utf8GamePath.FromString(swapFrom, out var path) || !Utf8GamePath.FromString(newSwapFrom, out var newPath)) + { + _messages.Add( + $"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}. Unknown Error."); + return; + } + + container.FileSwaps.Remove(path); + container.FileSwaps.Add(newPath, new FullPath(newSwapTo)); + _changedMods.Add((Mod)container.Mod); } -} +} From 2bd0c895887a33c6161a743e40a0f063b5a9183f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 16:04:49 +0200 Subject: [PATCH 2262/2451] Better item sort for item swap selectors. --- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 52 ++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index cb56de08..f5d2a8c7 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -15,6 +16,7 @@ using Penumbra.GameData.Structs; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; @@ -46,19 +48,20 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _config = config; _swapData = new ItemSwapContainer(metaFileManager, identifier); + var a = collectionManager.Active; _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), - [SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), + [SwapType.Hat] = (new ItemSelector(a, itemService, selector, FullEquipType.Head), new ItemSelector(a, itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(a, itemService, selector, FullEquipType.Body), new ItemSelector(a, itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(a, itemService, selector, FullEquipType.Hands), new ItemSelector(a, itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(a, itemService, selector, FullEquipType.Legs), new ItemSelector(a, itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(a, itemService, selector, FullEquipType.Feet), new ItemSelector(a, itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(a, itemService, selector, FullEquipType.Ears), new ItemSelector(a, itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(a, itemService, selector, FullEquipType.Neck), new ItemSelector(a, itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(a, itemService, selector, FullEquipType.Wrists), new ItemSelector(a, itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(a, itemService, selector, FullEquipType.Finger), new ItemSelector(a, itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Glasses] = (new ItemSelector(a, itemService, selector, FullEquipType.Glasses), new ItemSelector(a, itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), // @formatter:on }; @@ -134,23 +137,34 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Glasses, } - private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) - : FilterComboCache<(EquipItem Item, bool InMod)>(() => + private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type) + : FilterComboCache<(EquipItem Item, bool InMod, SingleArray InCollection)>(() => { var list = data.ByType[type]; - if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)) - return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); - - return list.Select(i => (i, false)).ToList(); + var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type) + ? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name), collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())) + .OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count) + : selector is null + ? list.Select(i => (i, false, collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())).OrderBy(p => p.Item3.Count) + : list.Select(i => (i, false, collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())).OrderByDescending(p => p.Item3.Count); + return enumerable.ToList(); }, MouseWheelType.None, Penumbra.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value(), Items[globalIdx].InMod); - return base.DrawSelectable(globalIdx, selected); + var (_, inMod, inCollection) = Items[globalIdx]; + using var color = inMod + ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value()) + : inCollection.Count > 0 + ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeNonNetworked.Value()) + : null; + var ret = base.DrawSelectable(globalIdx, selected); + if (inCollection.Count > 0) + ImUtf8.HoverTooltip(string.Join('\n', inCollection.Select(m => m.Name.Text))); + return ret; } - protected override string ToString((EquipItem Item, bool InMod) obj) + protected override string ToString((EquipItem Item, bool InMod, SingleArray InCollection) obj) => obj.Item.Name; } From 5d5fc673b1ff703aec294cde7a557f7513de454c Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 10 Apr 2025 14:42:26 +0000 Subject: [PATCH 2263/2451] [CI] Updating repo.json for 1.3.6.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5163bb7d..8e88ad52 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.7", - "TestingAssemblyVersion": "1.3.6.7", + "AssemblyVersion": "1.3.6.8", + "TestingAssemblyVersion": "1.3.6.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 0954f509127736e266dfc26d19593b42bb51c05a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:05:56 +0200 Subject: [PATCH 2264/2451] Update OtterGui, GameData, Namespaces. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 2 +- Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs | 1 + Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 1 + Penumbra/Collections/Cache/CollectionCache.cs | 2 +- Penumbra/Collections/Manager/ActiveCollections.cs | 2 +- Penumbra/Collections/Manager/CollectionEditor.cs | 2 +- Penumbra/Collections/Manager/CollectionStorage.cs | 2 +- Penumbra/Collections/Manager/InheritanceManager.cs | 2 +- Penumbra/Collections/Manager/TempCollectionManager.cs | 2 +- Penumbra/Collections/ModCollectionIdentity.cs | 1 + Penumbra/Configuration.cs | 2 +- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- Penumbra/Import/Models/Import/MeshImporter.cs | 2 +- Penumbra/Import/Models/Import/ModelImporter.cs | 2 +- Penumbra/Import/Models/Import/PrimitiveImporter.cs | 2 +- Penumbra/Import/Models/Import/SubMeshImporter.cs | 2 +- Penumbra/Import/Models/ModelManager.cs | 2 +- Penumbra/Import/Models/SkeletonConverter.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs | 2 +- Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Meta/Files/ImcFile.cs | 2 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 2 +- Penumbra/Mods/Editor/ModFileCollection.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 1 + Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Editor/ModelMaterialInfo.cs | 2 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 1 + Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/Manager/ModMigration.cs | 2 +- Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs | 1 + Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs | 2 +- Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs | 2 +- Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/ModCreator.cs | 1 + Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Mods/SubMods/CombinedDataContainer.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs | 1 + Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs | 1 + Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs | 1 + Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 1 + Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 1 + Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 1 + Penumbra/UI/CollectionTab/InheritanceUi.cs | 1 + Penumbra/UI/FileDialogService.cs | 1 + Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 1 + Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- 75 files changed, 75 insertions(+), 47 deletions(-) diff --git a/OtterGui b/OtterGui index 21ddfccb..5704b215 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 21ddfccb91ba3fa56e1c191e706ff91bffaa9515 +Subproject commit 5704b2151bcdbf18b04dff1b199ca2f35765504f diff --git a/Penumbra.GameData b/Penumbra.GameData index e10d8f33..62bbce59 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e10d8f33a676ff4544d7ca05a93d555416f41222 +Subproject commit 62bbce5981e961a91322ca1a7d3bb5be25f67185 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index fe9bf366..3ba17cf4 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 088a77bd..48f3b4a8 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 3dc8862e..c106a867 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 42c8b27d..f6f038a1 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -1,5 +1,4 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -8,6 +7,7 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Util; using Penumbra.GameData.Data; +using OtterGui.Extensions; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 2ced8ad6..ffec7fd2 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 5ccc38e2..f62eea3f 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Mods; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index de723729..531b6333 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 5e361bde..34582677 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 8aab5297..9476e38c 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api; using Penumbra.Communication; diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs index bd2d47c4..7050450c 100644 --- a/Penumbra/Collections/ModCollectionIdentity.cs +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Extensions; using Penumbra.Collections.Manager; namespace Penumbra.Collections; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 3a9bcdc4..bd6ccfb1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,8 +1,8 @@ using Dalamud.Configuration; using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 32b9b323..0070a808 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; using Lumina.Extensions; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 6a46fb9f..16fe2ca0 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 502d060a..f4eefccc 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs index 5df7597e..57c7929f 100644 --- a/Penumbra/Import/Models/Import/PrimitiveImporter.cs +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index df08eea3..6aa46fb6 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 19d06a52..6818ad64 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 25e74332..e180662d 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -1,5 +1,5 @@ using System.Xml; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7bbb762e..1c28aef2 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5fdec816..caf43d08 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,7 +1,7 @@ using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; -using OtterGui; +using OtterGui.Extensions; using Penumbra.String.Classes; using Swan; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 02e1be54..10795e6d 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,7 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 99360077..013d7db7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 0a0faf1e..b8db66dd 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 2a23ffad..da580794 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 7667910f..15bd179e 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index e3eb5f54..88941edf 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 3e367a3b..527dbf7c 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs index 741c2388..fe46048f 100644 --- a/Penumbra/Mods/Editor/ModelMaterialInfo.cs +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 90a962b7..d3f14101 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 5ec32274..34174f7f 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 82555314..558ee6be 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index c250182a..f376c1c9 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8b5b80d0..f3b25f1a 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs index ce5db454..5acf5eb5 100644 --- a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs index a7b73ac9..12ed4c60 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index c067102e..5c5ed4f1 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index efd92631..99f86517 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index f4f182eb..df476a6f 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 0420ee86..07217d4d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index 2c410c1c..b467c360 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 8fac52d8..9044350d 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index fcb6cc0e..a7a2ee61 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index 176ec3f4..f413a6a2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget.Editors; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index b76cffc2..ee5341b2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -4,6 +4,7 @@ using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 7ecd97e0..ac88f77c 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 80b10607..5bc70fc3 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -2,7 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 258e51ff..36154105 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 071b0551..3f63967e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Editor; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 59b38465..4c946fe7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.UI.AdvancedWindow.Materials; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b436448f..fc197bc0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8fbe5a68..0c8c496f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -3,6 +3,7 @@ using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 41f1da26..a6a75e0d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -12,6 +12,7 @@ using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; using OtterGui.Text; using Penumbra.GameData.Structs; +using OtterGui.Extensions; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index b5b39e90..6c2953e0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,7 +1,7 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4664372e..ee4e1eda 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,5 +1,6 @@ using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterTex; using Penumbra.Import.Textures; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7f1a8ac5..ccbbf0db 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index bd62089f..3c110fab 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index eb9aa93d..4d33a3fc 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.String; +using OtterGui.Extensions; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 0259713f..98dc924f 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,5 +1,5 @@ using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8b41b105..dc0e71b5 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -9,6 +9,7 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index ce3cc3cb..cdc1e83e 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Collections; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index cc2a7f6a..6773bc88 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index f32e6da6..5bd5dfdf 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 786bb8ff..3d330093 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 8dee13bf..666fce61 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index 89812346..e9ab72ae 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs index f0275853..04ca6c82 100644 --- a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index be2dbd73..492a8fb7 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 2b4b665b..ec020c86 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index f7b6b42d..c750b8b0 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 38340a2d..3988de35 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -11,6 +10,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; using Penumbra.UI.ModsTab.Groups; +using OtterGui.Extensions; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 0e9b5d39..ff5f636d 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 8de613d4..12355672 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Mods.Manager; diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index d9058083..eb9f05d9 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,5 +1,5 @@ using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; From 53ef42adfa109cd073783acd328d096e01634453 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:09 +0200 Subject: [PATCH 2265/2451] Update EST Customization identification. --- Penumbra/Meta/Manipulations/Est.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 05d4c014..46a275a5 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -33,7 +33,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende var (gender, race) = GenderRace.Split(); var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {gender.ToName()} {race.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); + $"Customization: {race.ToName()} {gender.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); break; } case EstType.Face: @@ -41,7 +41,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende var (gender, race) = GenderRace.Split(); var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {gender.ToName()} {race.ToName()} Face {SetId}", + $"Customization: {race.ToName()} {gender.ToName()} Face {SetId}", () => IdentifiedCustomization.Face(race, gender, id)); break; } From 0c768979d4e34969aabbb4cf1dd4dc90cc1ed8e4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:22 +0200 Subject: [PATCH 2266/2451] Don't use DalamudPackager for no reason. --- Penumbra/Penumbra.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f93b1815..f668f775 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -11,6 +11,7 @@ PROFILING; + false From cbebfe5e99709b7babc3de2245eaadb2dd48c763 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:58 +0200 Subject: [PATCH 2267/2451] Fix sizing of mod panel. --- Penumbra/UI/Tabs/ModsTab.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 8b4913c8..b77240ec 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -55,12 +55,12 @@ public class ModsTab( { selector.Draw(GetModSelectorSize(config)); ImGui.SameLine(); + ImGui.SetCursorPosX(MathF.Round(ImGui.GetCursorPosX())); using var group = ImRaii.Group(); collectionHeader.Draw(false); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(ImGui.GetContentRegionAvail().X, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), true, ImGuiWindowFlags.HorizontalScrollbar)) { style.Pop(); @@ -94,9 +94,9 @@ public class ModsTab( var relativeSize = config.ScaleModSelector ? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) : 0; - return !config.ScaleModSelector - ? absoluteSize - : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + return MathF.Round(config.ScaleModSelector + ? Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100) + : absoluteSize); } private void DrawRedrawLine() From a5d221dc1359281e68614f5fb9a3db060a839278 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Apr 2025 00:14:11 +0200 Subject: [PATCH 2268/2451] Make temporary mode checkbox more visible. --- OtterGui | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 31 +++++++++---------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 1 + 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/OtterGui b/OtterGui index 5704b215..ac32553b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5704b2151bcdbf18b04dff1b199ca2f35765504f +Subproject commit ac32553b1e2e9feca7b9cd0c1b16eae81d5fcc31 diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index d7a81876..aa492362 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,10 +1,9 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui.Raii; using OtterGui; +using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; -using OtterGui.Text.Widget; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -15,13 +14,12 @@ namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModSelection _selection; - private readonly CollectionResolver _resolver; - private readonly FontAwesomeCheckbox _temporaryCheckbox = new(FontAwesomeIcon.Stopwatch); - private readonly Configuration _config; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; + private readonly Configuration _config; public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, CollectionResolver resolver, Configuration config) @@ -64,14 +62,15 @@ public class CollectionSelectHeader : IUiService var hold = _config.IncognitoModifier.IsActive(); using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) { - var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); - using var color = ImRaii.PushColor(ImGuiCol.FrameBgHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.FrameBgActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.CheckMark, tint) - .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); - if (_temporaryCheckbox.Draw("##tempCheck"u8, _config.DefaultTemporaryMode, out var newValue) && hold) + var tint = _config.DefaultTemporaryMode + ? ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint) + : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var color = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); + if (ImUtf8.IconButton(FontAwesomeIcon.Stopwatch, ""u8, default, false, tint, ImGui.GetColorU32(ImGuiCol.FrameBg)) && hold) { - _config.DefaultTemporaryMode = newValue; + _config.DefaultTemporaryMode = !_config.DefaultTemporaryMode; _config.Save(); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6d6222ec..b7bc8edf 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -12,6 +12,7 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; From 117724b0aebc0b063a5edb81342b28cca44a4fc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Apr 2025 23:11:45 +0200 Subject: [PATCH 2269/2451] Update npc names. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Services/StaticServiceManager.cs | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index ac32553b..089ed82a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ac32553b1e2e9feca7b9cd0c1b16eae81d5fcc31 +Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2 diff --git a/Penumbra.GameData b/Penumbra.GameData index 62bbce59..002260d9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 62bbce5981e961a91322ca1a7d3bb5be25f67185 +Subproject commit 002260d9815e571f1496c50374f5b712818e9880 diff --git a/Penumbra.String b/Penumbra.String index 2896c056..0e5dcd1a 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 2896c0561f60827f97408650d52a15c38f4d9d10 +Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5 diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index c0dc9314..27582395 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -17,6 +17,8 @@ using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; namespace Penumbra.Services; +#pragma warning disable SeStringEvaluator + public static class StaticServiceManager { public static ServiceManager CreateProvider(Penumbra penumbra, IDalamudPluginInterface pi, Logger log) @@ -60,5 +62,6 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) - .AddDalamudService(pi); + .AddDalamudService(pi) + .AddDalamudService(pi); } From 363d115be8980009db8bccc87204b12ce1d24e14 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sun, 20 Apr 2025 00:35:36 +0930 Subject: [PATCH 2270/2451] Add filter for temporary mods --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 14 ++++++++++++++ Penumbra/UI/ModsTab/ModFilter.cs | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index a0383329..6586747c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -647,6 +647,20 @@ public sealed class ModFileSystemSelector : FileSystemSelector TriStatePairs = [ @@ -38,6 +40,7 @@ public static class ModFilterExtensions (ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"), (ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"), (ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"), + (ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"), ]; public static IReadOnlyList> Groups = From 0fe4a3671ab6e577ca39a2d94bd714df1a2c3a06 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 May 2025 23:46:25 +0200 Subject: [PATCH 2271/2451] Improve small issue with redraw service. --- Penumbra/Interop/Services/RedrawService.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 8f20ca5e..08e9ddf5 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -10,7 +10,6 @@ using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.Structs; @@ -354,21 +353,14 @@ public sealed unsafe partial class RedrawService : IDisposable { switch (settings) { - case RedrawType.Redraw: - ReloadActor(actor); - break; - case RedrawType.AfterGPose: - ReloadActorAfterGPose(actor); - break; - default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); + case RedrawType.Redraw: ReloadActor(actor); break; + case RedrawType.AfterGPose: ReloadActorAfterGPose(actor); break; + default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); } } private IGameObject? GetLocalPlayer() - { - var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx); - return gPosePlayer ?? _objects.GetDalamudObject(0); - } + => InGPose ? _objects.GetDalamudObject(GPosePlayerIdx) ?? _objects.GetDalamudObject(0) : _objects.GetDalamudObject(0); public bool GetName(string lowerName, out IGameObject? actor) { From 0adec35848f8e02838d7d43e755cf6e0f8444980 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 00:26:59 +0200 Subject: [PATCH 2272/2451] Add initial support for custom shapes. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Communication/ModelAttributeComputed.cs | 24 ++++ Penumbra/Configuration.cs | 4 +- .../Hooks/PostProcessing/AttributeHooks.cs | 110 +++++++++++++++ .../Hooks/Resources/ResolvePathHooksBase.cs | 22 --- Penumbra/Meta/ShapeManager.cs | 127 ++++++++++++++++++ Penumbra/Meta/ShapeString.cs | 89 ++++++++++++ Penumbra/Penumbra.cs | 7 +- Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 12 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 89 +++++++----- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 71 ++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- Penumbra/packages.lock.json | 6 - 15 files changed, 502 insertions(+), 75 deletions(-) create mode 100644 Penumbra/Communication/ModelAttributeComputed.cs create mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs create mode 100644 Penumbra/Meta/ShapeManager.cs create mode 100644 Penumbra/Meta/ShapeString.cs create mode 100644 Penumbra/UI/Tabs/Debug/ShapeInspector.cs diff --git a/OtterGui b/OtterGui index 089ed82a..86b49242 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2 +Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c diff --git a/Penumbra.GameData b/Penumbra.GameData index 002260d9..0ca50105 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 002260d9815e571f1496c50374f5b712818e9880 +Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6 diff --git a/Penumbra/Communication/ModelAttributeComputed.cs b/Penumbra/Communication/ModelAttributeComputed.cs new file mode 100644 index 00000000..389f56b6 --- /dev/null +++ b/Penumbra/Communication/ModelAttributeComputed.cs @@ -0,0 +1,24 @@ +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a model recomputes its attribute masks. +/// +/// Parameter is the game object that recomputed its attributes. +/// Parameter is the draw object on which the recomputation was called. +/// Parameter is the collection associated with the game object. +/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. +/// +public sealed class ModelAttributeComputed() + : EventWrapper(nameof(ModelAttributeComputed)) +{ + public enum Priority + { + /// + ShapeManager = 0, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index bd6ccfb1..8c50dad7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -67,6 +67,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; + public bool EnableCustomShapes { get; set; } = true; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; @@ -84,9 +85,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs new file mode 100644 index 00000000..861962ee --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs @@ -0,0 +1,110 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public sealed unsafe class AttributeHooks : IRequiredService, IDisposable +{ + private delegate void SetupAttributes(Human* human, byte* data); + private delegate void AttributeUpdate(Human* human); + + private readonly Configuration _config; + private readonly ModelAttributeComputed _event; + private readonly CollectionResolver _resolver; + + private readonly AttributeHook[] _hooks; + private readonly Task> _updateHook; + private ModCollection _identifiedCollection = ModCollection.Empty; + private Actor _identifiedActor = Actor.Null; + private bool _inUpdate; + + public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks) + { + _config = config; + _event = communication.ModelAttributeComputed; + _resolver = resolver; + _hooks = + [ + new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body), + new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands), + new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs), + new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet), + ]; + _updateHook = hooks.CreateHook("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour, + _config.EnableCustomShapes); + } + + private class AttributeHook + { + private readonly AttributeHooks _parent; + public readonly string Name; + public readonly Task> Hook; + public readonly uint ModelIndex; + public readonly HumanSlot Slot; + + public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot) + { + _parent = parent; + Name = $"Setup{slot}Attributes"; + Slot = slot; + ModelIndex = slot.ToIndex(); + Hook = hooks.CreateHook(Name, signature, Detour, enabled); + } + + private void Detour(Human* human, byte* data) + { + Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X})."); + Hook.Result.Original(human, data); + if (_parent is { _inUpdate: true, _identifiedActor.Valid: true }) + _parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot); + } + } + + private void UpdateAttributesDetour(Human* human) + { + var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); + _identifiedActor = resolveData.AssociatedGameObject; + _identifiedCollection = resolveData.ModCollection; + _inUpdate = true; + Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X})."); + _event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown); + _updateHook.Result.Original(human); + _inUpdate = false; + } + + public void SetState(bool enabled) + { + if (_config.EnableCustomShapes == enabled) + return; + + _config.EnableCustomShapes = enabled; + _config.Save(); + if (enabled) + { + foreach (var hook in _hooks) + hook.Hook.Result.Enable(); + _updateHook.Result.Enable(); + } + else + { + foreach (var hook in _hooks) + hook.Hook.Result.Disable(); + _updateHook.Result.Disable(); + } + } + + public void Dispose() + { + foreach (var hook in _hooks) + hook.Hook.Result.Dispose(); + _updateHook.Result.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 54066782..8a45ec2c 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -246,28 +246,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } - [StructLayout(LayoutKind.Explicit)] - private struct ChangedEquipData - { - [FieldOffset(0)] - public PrimaryId Model; - - [FieldOffset(2)] - public Variant Variant; - - [FieldOffset(8)] - public PrimaryId BonusModel; - - [FieldOffset(10)] - public Variant BonusVariant; - - [FieldOffset(20)] - public ushort VfxId; - - [FieldOffset(22)] - public GenderRace GenderRace; - } - private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { switch (slotIndex) diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs new file mode 100644 index 00000000..4356086a --- /dev/null +++ b/Penumbra/Meta/ShapeManager.cs @@ -0,0 +1,127 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Services; + +namespace Penumbra.Meta; + +public class ShapeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 4; + private readonly CommunicatorService _communicator; + + private static ReadOnlySpan UsedModels + => [1, 2, 3, 4]; + + public ShapeManager(CommunicatorService communicator) + { + _communicator = communicator; + _communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager); + } + + private readonly Dictionary[] _temporaryIndices = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly uint[] _temporaryMasks = new uint[NumSlots]; + private readonly uint[] _temporaryValues = new uint[NumSlots]; + + private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot) + { + int index; + switch (slot) + { + case HumanSlot.Unknown: + ResetCache(model); + return; + case HumanSlot.Body: index = 0; break; + case HumanSlot.Hands: index = 1; break; + case HumanSlot.Legs: index = 2; break; + case HumanSlot.Feet: index = 3; break; + default: return; + } + + if (_temporaryMasks[index] is 0) + return; + + var modelIndex = UsedModels[index]; + var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask; + var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index]; + Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); + model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask; + } + + public void Dispose() + { + _communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed); + } + + private unsafe void ResetCache(Model human) + { + for (var i = 0; i < NumSlots; ++i) + { + _temporaryMasks[i] = 0; + _temporaryValues[i] = 0; + _temporaryIndices[i].Clear(); + + var modelIndex = UsedModels[i]; + var model = human.AsHuman->Models[modelIndex]; + if (model is null || model->ModelResourceHandle is null) + continue; + + ref var shapes = ref model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex))) + { + if (ShapeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryIndices[i].TryAdd(shapeString, index); + _temporaryMasks[i] |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + UpdateMasks(); + } + + private static bool CheckShapes(ReadOnlySpan shape, byte index) + => index switch + { + 1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8), + 2 => shape.StartsWith("shp_wr_"u8), + 3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8), + 4 => shape.StartsWith("shp_an"u8), + _ => false, + }; + + private void UpdateMasks() + { + foreach (var (shape, topIndex) in _temporaryIndices[0]) + { + if (_temporaryIndices[1].TryGetValue(shape, out var handIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[1] |= 1u << handIndex; + } + + if (_temporaryIndices[2].TryGetValue(shape, out var legIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[2] |= 1u << legIndex; + } + } + + foreach (var (shape, bottomIndex) in _temporaryIndices[2]) + { + if (_temporaryIndices[3].TryGetValue(shape, out var footIndex)) + { + _temporaryValues[2] |= 1u << bottomIndex; + _temporaryValues[3] |= 1u << footIndex; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs new file mode 100644 index 00000000..987ed474 --- /dev/null +++ b/Penumbra/Meta/ShapeString.cs @@ -0,0 +1,89 @@ +using Lumina.Misc; +using Newtonsoft.Json; +using Penumbra.GameData.Files.PhybStructs; + +namespace Penumbra.Meta; + +[JsonConverter(typeof(Converter))] +public struct ShapeString : IEquatable +{ + public const int MaxLength = 30; + + public static readonly ShapeString Empty = new(); + + private FixedString32 _buffer; + + public int Count + => _buffer[31]; + + public int Length + => _buffer[31]; + + public override string ToString() + => Encoding.UTF8.GetString(_buffer[..Length]); + + public bool Equals(ShapeString other) + => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); + + public override bool Equals(object? obj) + => obj is ShapeString other && Equals(other); + + public override int GetHashCode() + => (int)Crc32.Get(_buffer[..Length]); + + public static bool operator ==(ShapeString left, ShapeString right) + => left.Equals(right); + + public static bool operator !=(ShapeString left, ShapeString right) + => !left.Equals(right); + + public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + { + var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); + return TryRead(span, out ret); + } + + public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + { + if (utf8.Length is 0 or > MaxLength) + { + ret = Empty; + return false; + } + + ret = Empty; + utf8.CopyTo(ret._buffer); + ret._buffer[utf8.Length] = 0; + ret._buffer[31] = (byte)utf8.Length; + return true; + } + + public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + { + ret = Empty; + if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) + return false; + + ret._buffer[written] = 0; + ret._buffer[31] = (byte)written; + return true; + } + + private sealed class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + if (!TryRead(value, out existingValue)) + throw new JsonReaderException($"Could not parse {value} into ShapeString."); + + return existingValue; + } + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 7f4c1b23..70636bbf 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,6 +34,7 @@ public class Penumbra : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; + public static DynamisIpc Dynamis { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -59,8 +60,9 @@ public class Penumbra : IDalamudPlugin _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); // Invoke the IPC Penumbra.Launching method before any hooks or other services are created. _services.GetService(); - Messager = _services.GetService(); - _validityChecker = _services.GetService(); + Messager = _services.GetService(); + Dynamis = _services.GetService(); + _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); var startup = _services.GetService() @@ -228,6 +230,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n"); sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); + sb.Append($"> **`Custom Shapes Enabled: `** {_config.EnableCustomShapes}\n"); sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 5d745419..e008752f 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,6 +81,9 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); + /// + public readonly ModelAttributeComputed ModelAttributeComputed = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -105,5 +108,6 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); + ModelAttributeComputed.Dispose(); } } diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index eb9f05d9..3b25c1a9 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,11 +1,11 @@ using ImGuiNET; -using OtterGui.Extensions; -using OtterGui.Text; +using OtterGui.Extensions; +using OtterGui.Text; using Penumbra.GameData.Files; -using Penumbra.GameData.Files.AtchStructs; - -namespace Penumbra.UI.Tabs.Debug; - +using Penumbra.GameData.Files.AtchStructs; + +namespace Penumbra.UI.Tabs.Debug; + public static class AtchDrawer { public static void Draw(AtchFile file) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b7bc8edf..b4fa3b9f 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -108,6 +108,7 @@ public class DebugTab : Window, ITab, IUiService private readonly ObjectIdentification _objectIdentification; private readonly RenderTargetDrawer _renderTargetDrawer; private readonly ModMigratorDebug _modMigratorDebug; + private readonly ShapeInspector _shapeInspector; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -119,7 +120,7 @@ public class DebugTab : Window, ITab, IUiService Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, - ModMigratorDebug modMigratorDebug) + ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -162,6 +163,7 @@ public class DebugTab : Window, ITab, IUiService _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; _modMigratorDebug = modMigratorDebug; + _shapeInspector = shapeInspector; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -508,37 +510,50 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; - _objects.DrawDebug(); - - using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - DrawSpecial("Current Player", _actors.GetCurrentPlayer()); - DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); - DrawSpecial("Current Card", _actors.GetCardPlayer()); - DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - - foreach (var obj in _objects) + using (var objectTree = ImUtf8.TreeNode("Object Manager"u8)) { - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}"); - ImGui.TableNextColumn(); - if (obj.Address != nint.Zero) - ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); - var identifier = _actors.FromObject(obj, out _, false, true, false); - ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc - ? $"{identifier.DataId} | {obj.AsObject->BaseId}" - : identifier.DataId.ToString(); - ImGuiUtil.DrawTableColumn(id); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero - ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" - : "NULL"); + if (objectTree) + { + _objects.DrawDebug(); + + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + DrawSpecial("Current Player", _actors.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); + DrawSpecial("Current Card", _actors.GetCardPlayer()); + DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); + + foreach (var obj in _objects) + { + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address); + ImGui.TableNextColumn(); + if (obj.Address != nint.Zero) + Penumbra.Dynamis.DrawPointer((nint)((Character*)obj.Address)->GameObject.GetDrawObject()); + var identifier = _actors.FromObject(obj, out _, false, true, false); + ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); + var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->BaseId}" + : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn(id); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address != nint.Zero ? *(nint*)obj.Address : nint.Zero); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero + ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" + : "NULL"); + } + } + } + + using (var shapeTree = ImUtf8.TreeNode("Shape Inspector"u8)) + { + if (shapeTree) + _shapeInspector.Draw(); } return; @@ -1184,8 +1199,16 @@ public class DebugTab : Window, ITab, IUiService /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { - if (ImGui.CollapsingHeader("IPC")) - _ipcTester.Draw(); + if (!ImUtf8.CollapsingHeader("IPC"u8)) + return; + + using (var tree = ImUtf8.TreeNode("Dynamis"u8)) + { + if (tree) + Penumbra.Dynamis.DrawDebugInfo(); + } + + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs new file mode 100644 index 00000000..968bc484 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -0,0 +1,71 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.UI.Tabs.Debug; + +public class ShapeInspector(ObjectManager objects) : IUiService +{ + private int _objectIndex = 0; + + public unsafe void Draw() + { + ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); + var actor = objects[0]; + if (!actor.IsCharacter) + { + ImUtf8.Text("No valid character."u8); + return; + } + + var human = actor.Model; + if (!human.IsHuman) + { + ImUtf8.Text("No valid character."u8); + return; + } + + using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + foreach (var slot in Enum.GetValues()) + { + ImUtf8.DrawTableColumn($"{(uint)slot:D2}"); + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[(int)slot]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if ((idx % 8) < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index b1f82a91..7b3a3c8b 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -50,6 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly AttributeHooks _attributeHooks; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -61,7 +63,8 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + AttributeHooks attributeHooks) { _pluginInterface = pluginInterface; _config = config; @@ -86,6 +89,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _attributeHooks = attributeHooks; } public void DrawHeader() @@ -807,6 +811,8 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); + Checkbox("Enable Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + _config.EnableAttributeHooks, _attributeHooks.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index dda6b305..4a162f8f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -2,12 +2,6 @@ "version": 1, "dependencies": { "net9.0-windows7.0": { - "DalamudPackager": { - "type": "Direct", - "requested": "[12.0.0, )", - "resolved": "12.0.0", - "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" - }, "DotNet.ReproducibleBuilds": { "type": "Direct", "requested": "[1.2.25, )", From 6ad0b4299a29486af69a02d96bbf71cbbb77e939 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 17:46:53 +0200 Subject: [PATCH 2273/2451] Add shape meta manipulations and rework attribute hook. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.sln | 1 + Penumbra/Collections/Cache/CollectionCache.cs | 2 + Penumbra/Collections/Cache/MetaCache.cs | 9 +- Penumbra/Collections/Cache/ShpCache.cs | 109 ++++++++ .../Communication/ModelAttributeComputed.cs | 24 -- .../Hooks/PostProcessing/AttributeHook.cs | 85 ++++++ .../Hooks/PostProcessing/AttributeHooks.cs | 110 -------- .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 72 ++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 157 +++++++++++ Penumbra/Meta/ShapeManager.cs | 117 ++++----- Penumbra/Meta/ShapeString.cs | 33 ++- Penumbra/Services/CommunicatorService.cs | 4 - .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 5 +- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 247 ++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 114 +++++--- Penumbra/UI/Tabs/SettingsTab.cs | 64 +---- schemas/structs/manipulation.json | 12 +- schemas/structs/meta_enums.json | 4 + schemas/structs/meta_shp.json | 23 ++ 23 files changed, 900 insertions(+), 298 deletions(-) create mode 100644 Penumbra/Collections/Cache/ShpCache.cs delete mode 100644 Penumbra/Communication/ModelAttributeComputed.cs create mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs delete mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs create mode 100644 Penumbra/Meta/Manipulations/ShpIdentifier.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs create mode 100644 schemas/structs/meta_shp.json diff --git a/OtterGui b/OtterGui index 86b49242..f130c928 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c +Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98 diff --git a/Penumbra.GameData b/Penumbra.GameData index 0ca50105..8e57c2e1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6 +Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3 diff --git a/Penumbra.sln b/Penumbra.sln index e52045b0..642876ef 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -50,6 +50,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json schemas\structs\meta_imc.json = schemas\structs\meta_imc.json schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json + schemas\structs\meta_shp.json = schemas\structs\meta_shp.json schemas\structs\option.json = schemas\structs\option.json EndProjectSection EndProject diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index f6f038a1..c48a487c 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -245,6 +245,8 @@ public sealed class CollectionCache : IDisposable AddManipulation(mod, identifier, entry); foreach (var (identifier, entry) in files.Manipulations.Atch) AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Shp) + AddManipulation(mod, identifier, entry); foreach (var identifier in files.Manipulations.GlobalEqp) AddManipulation(mod, identifier, null!); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 7d8586c3..790dd3af 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -16,11 +16,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); public readonly AtchCache Atch = new(manager, collection); + public readonly ShpCache Shp = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -30,6 +31,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -41,6 +43,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Rsp.Reset(); Imc.Reset(); Atch.Reset(); + Shp.Reset(); GlobalEqp.Clear(); } @@ -57,6 +60,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Rsp.Dispose(); Imc.Dispose(); Atch.Dispose(); + Shp.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -71,6 +75,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), + ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -92,6 +97,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i => Imc.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod), + ShpIdentifier i => Shp.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -108,6 +114,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), + ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs new file mode 100644 index 00000000..2e90052d --- /dev/null +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -0,0 +1,109 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) + => _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + + internal IReadOnlyDictionary State + => _shpData; + + internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> + { + private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); + + public bool All + { + get => _allIds[^1]; + set => _allIds[^1] = value; + } + + public bool this[HumanSlot slot] + { + get + { + if (slot is HumanSlot.Unknown) + return All; + + return _allIds[(int)slot]; + } + set + { + if (slot is HumanSlot.Unknown) + _allIds[^1] = value; + else + _allIds[(int)slot] = value; + } + } + + public bool Contains(HumanSlot slot, PrimaryId id) + => All || this[slot] || Contains((slot, id)); + + public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value) + { + if (slot is HumanSlot.Unknown) + { + var old = All; + All = value.Value; + return old != value.Value; + } + + if (!id.HasValue) + { + var old = this[slot]; + this[slot] = value.Value; + return old != value.Value; + } + + if (value.Value) + return Add((slot, id.Value)); + + return Remove((slot, id.Value)); + } + + public new void Clear() + { + base.Clear(); + _allIds.SetAll(false); + } + + public bool IsEmpty + => !_allIds.HasAnySet() && Count is 0; + } + + private readonly Dictionary _shpData = []; + + public void Reset() + { + Clear(); + _shpData.Clear(); + } + + protected override void Dispose(bool _) + => Clear(); + + protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + { + value = []; + _shpData.Add(identifier.Shape, value); + } + + value.TrySet(identifier.Slot, identifier.Id, entry); + } + + protected override void RevertModInternal(ShpIdentifier identifier) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + _shpData.Remove(identifier.Shape); + } +} diff --git a/Penumbra/Communication/ModelAttributeComputed.cs b/Penumbra/Communication/ModelAttributeComputed.cs deleted file mode 100644 index 389f56b6..00000000 --- a/Penumbra/Communication/ModelAttributeComputed.cs +++ /dev/null @@ -1,24 +0,0 @@ -using OtterGui.Classes; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; - -namespace Penumbra.Communication; - -/// -/// Triggered whenever a model recomputes its attribute masks. -/// -/// Parameter is the game object that recomputed its attributes. -/// Parameter is the draw object on which the recomputation was called. -/// Parameter is the collection associated with the game object. -/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. -/// -public sealed class ModelAttributeComputed() - : EventWrapper(nameof(ModelAttributeComputed)) -{ - public enum Priority - { - /// - ShapeManager = 0, - } -} diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs new file mode 100644 index 00000000..cad049ad --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs @@ -0,0 +1,85 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +/// +/// Triggered whenever a model recomputes its attribute masks. +/// +/// Parameter is the game object that recomputed its attributes. +/// Parameter is the draw object on which the recomputation was called. +/// Parameter is the collection associated with the game object. +/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. +/// +public sealed unsafe class AttributeHook : EventWrapper, IHookService +{ + public enum Priority + { + /// + ShapeManager = 0, + } + + private readonly CollectionResolver _resolver; + private readonly Configuration _config; + + public AttributeHook(HookManager hooks, Configuration config, CollectionResolver resolver) + : base("Update Model Attributes") + { + _config = config; + _resolver = resolver; + _task = hooks.CreateHook(Name, Sigs.UpdateAttributes, Detour, config.EnableCustomShapes); + } + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => SetState(true); + + public void Disable() + => SetState(false); + + public void SetState(bool enabled) + { + if (_config.EnableCustomShapes == enabled) + return; + + _config.EnableCustomShapes = enabled; + _config.Save(); + if (enabled) + _task.Result.Enable(); + else + _task.Result.Disable(); + } + + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(Human* human); + + private void Detour(Human* human) + { + _task.Result.Original(human); + var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); + var identifiedActor = resolveData.AssociatedGameObject; + var identifiedCollection = resolveData.ModCollection; + Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{identifiedActor:X})."); + Invoke(identifiedActor, human, identifiedCollection); + } + + protected override void Dispose(bool disposing) + => _task.Result.Dispose(); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs deleted file mode 100644 index 861962ee..00000000 --- a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.Communication; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; -using Penumbra.Interop.PathResolving; -using Penumbra.Services; - -namespace Penumbra.Interop.Hooks.PostProcessing; - -public sealed unsafe class AttributeHooks : IRequiredService, IDisposable -{ - private delegate void SetupAttributes(Human* human, byte* data); - private delegate void AttributeUpdate(Human* human); - - private readonly Configuration _config; - private readonly ModelAttributeComputed _event; - private readonly CollectionResolver _resolver; - - private readonly AttributeHook[] _hooks; - private readonly Task> _updateHook; - private ModCollection _identifiedCollection = ModCollection.Empty; - private Actor _identifiedActor = Actor.Null; - private bool _inUpdate; - - public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks) - { - _config = config; - _event = communication.ModelAttributeComputed; - _resolver = resolver; - _hooks = - [ - new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body), - new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands), - new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs), - new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet), - ]; - _updateHook = hooks.CreateHook("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour, - _config.EnableCustomShapes); - } - - private class AttributeHook - { - private readonly AttributeHooks _parent; - public readonly string Name; - public readonly Task> Hook; - public readonly uint ModelIndex; - public readonly HumanSlot Slot; - - public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot) - { - _parent = parent; - Name = $"Setup{slot}Attributes"; - Slot = slot; - ModelIndex = slot.ToIndex(); - Hook = hooks.CreateHook(Name, signature, Detour, enabled); - } - - private void Detour(Human* human, byte* data) - { - Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X})."); - Hook.Result.Original(human, data); - if (_parent is { _inUpdate: true, _identifiedActor.Valid: true }) - _parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot); - } - } - - private void UpdateAttributesDetour(Human* human) - { - var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); - _identifiedActor = resolveData.AssociatedGameObject; - _identifiedCollection = resolveData.ModCollection; - _inUpdate = true; - Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X})."); - _event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown); - _updateHook.Result.Original(human); - _inUpdate = false; - } - - public void SetState(bool enabled) - { - if (_config.EnableCustomShapes == enabled) - return; - - _config.EnableCustomShapes = enabled; - _config.Save(); - if (enabled) - { - foreach (var hook in _hooks) - hook.Hook.Result.Enable(); - _updateHook.Result.Enable(); - } - else - { - foreach (var hook in _hooks) - hook.Hook.Result.Disable(); - _updateHook.Result.Disable(); - } - } - - public void Dispose() - { - foreach (var hook in _hooks) - hook.Hook.Result.Dispose(); - _updateHook.Result.Dispose(); - } -} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index c897bb2a..13feba51 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -15,6 +15,7 @@ public enum MetaManipulationType : byte Rsp = 6, GlobalEqp = 7, Atch = 8, + Shp = 9, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index ca45c777..a7225067 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -18,6 +18,7 @@ public class MetaDictionary private readonly Dictionary _rsp = []; private readonly Dictionary _gmp = []; private readonly Dictionary _atch = []; + private readonly Dictionary _shp = []; private readonly HashSet _globalEqp = []; public IReadOnlyDictionary Imc @@ -41,6 +42,9 @@ public class MetaDictionary public IReadOnlyDictionary Atch => _atch; + public IReadOnlyDictionary Shp + => _shp; + public IReadOnlySet GlobalEqp => _globalEqp; @@ -56,6 +60,7 @@ public class MetaDictionary MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Rsp => _rsp.Count, MetaManipulationType.Atch => _atch.Count, + MetaManipulationType.Shp => _shp.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count, _ => 0, }; @@ -70,6 +75,7 @@ public class MetaDictionary GmpIdentifier i => _gmp.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i), AtchIdentifier i => _atch.ContainsKey(i), + ShpIdentifier i => _shp.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i), _ => false, }; @@ -84,6 +90,7 @@ public class MetaDictionary _rsp.Clear(); _gmp.Clear(); _atch.Clear(); + _shp.Clear(); _globalEqp.Clear(); } @@ -108,6 +115,7 @@ public class MetaDictionary && _rsp.SetEquals(other._rsp) && _gmp.SetEquals(other._gmp) && _atch.SetEquals(other._atch) + && _shp.SetEquals(other._shp) && _globalEqp.SetEquals(other._globalEqp); public IEnumerable Identifiers @@ -118,6 +126,7 @@ public class MetaDictionary .Concat(_gmp.Keys.Cast()) .Concat(_rsp.Keys.Cast()) .Concat(_atch.Keys.Cast()) + .Concat(_shp.Keys.Cast()) .Concat(_globalEqp.Cast()); #region TryAdd @@ -191,6 +200,15 @@ public class MetaDictionary return true; } + public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry) + { + if (!_shp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { if (!_globalEqp.Add(identifier)) @@ -273,6 +291,15 @@ public class MetaDictionary return true; } + public bool Update(ShpIdentifier identifier, in ShpEntry entry) + { + if (!_shp.ContainsKey(identifier)) + return false; + + _shp[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -298,6 +325,9 @@ public class MetaDictionary public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) => _atch.TryGetValue(identifier, out value); + public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) + => _shp.TryGetValue(identifier, out value); + #endregion public bool Remove(IMetaIdentifier identifier) @@ -312,6 +342,7 @@ public class MetaDictionary ImcIdentifier i => _imc.Remove(i), RspIdentifier i => _rsp.Remove(i), AtchIdentifier i => _atch.Remove(i), + ShpIdentifier i => _shp.Remove(i), _ => false, }; if (ret) @@ -344,6 +375,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._atch) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._shp) + TryAdd(identifier, entry); + foreach (var identifier in manips._globalEqp) TryAdd(identifier); } @@ -393,13 +427,19 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; return false; } - failedIdentifier = default; + failedIdentifier = null; return true; } @@ -412,8 +452,9 @@ public class MetaDictionary _rsp.SetTo(other._rsp); _gmp.SetTo(other._gmp); _atch.SetTo(other._atch); + _shp.SetTo(other._shp); _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; } public void UpdateTo(MetaDictionary other) @@ -425,8 +466,9 @@ public class MetaDictionary _rsp.UpdateTo(other._rsp); _gmp.UpdateTo(other._gmp); _atch.UpdateTo(other._atch); + _shp.UpdateTo(other._shp); _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; } #endregion @@ -514,6 +556,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(ShpIdentifier identifier, ShpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Shp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -543,6 +595,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -588,6 +642,7 @@ public class MetaDictionary SerializeTo(array, value._rsp); SerializeTo(array, value._gmp); SerializeTo(array, value._atch); + SerializeTo(array, value._shp); SerializeTo(array, value._globalEqp); array.WriteTo(writer); } @@ -685,6 +740,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid ATCH Manipulation encountered."); break; } + case MetaManipulationType.Shp: + { + var identifier = ShpIdentifier.FromJson(manip); + var entry = new ShpEntry(manip["Entry"]?.Value() ?? true); + if (identifier.HasValue) + dict.TryAdd(identifier.Value, entry); + else + Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); @@ -716,6 +781,7 @@ public class MetaDictionary _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); Count = cache.Count; } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs new file mode 100644 index 00000000..fffa51ba --- /dev/null +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -0,0 +1,157 @@ +using Lumina.Models.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape) + : IComparable, IMetaIdentifier +{ + public int CompareTo(ShpIdentifier other) + { + var slotComparison = Slot.CompareTo(other.Slot); + if (slotComparison is not 0) + return slotComparison; + + if (Id.HasValue) + { + if (other.Id.HasValue) + { + var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id); + if (idComparison is not 0) + return idComparison; + } + else + { + return -1; + } + } + else if (other.Id.HasValue) + { + return 1; + } + + + return Shape.CompareTo(other.Shape); + } + + public override string ToString() + => $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}"; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing for now since it depends entirely on the shape key. + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) + return false; + + if (Slot is HumanSlot.Unknown && Id is not null) + return false; + + return ValidateCustomShapeString(Shape); + } + + public static bool ValidateCustomShapeString(ReadOnlySpan shape) + { + // "shp_xx_y" + if (shape.Length < 8) + return false; + + if (shape[0] is not (byte)'s' + || shape[1] is not (byte)'h' + || shape[2] is not (byte)'p' + || shape[3] is not (byte)'_' + || shape[6] is not (byte)'_') + return false; + + return true; + } + + public static unsafe bool ValidateCustomShapeString(byte* shape) + { + // "shp_xx_y" + if (shape is null) + return false; + + if (*shape++ is not (byte)'s' + || *shape++ is not (byte)'h' + || *shape++ is not (byte)'p' + || *shape++ is not (byte)'_' + || *shape++ is 0 + || *shape++ is 0 + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public static bool ValidateCustomShapeString(in ShapeString shape) + { + // "shp_xx_y" + if (shape.Length < 8) + return false; + + var span = shape.AsSpan; + if (span[0] is not (byte)'s' + || span[1] is not (byte)'h' + || span[2] is not (byte)'p' + || span[3] is not (byte)'_' + || span[6] is not (byte)'_') + return false; + + return true; + } + + public JObject AddToJson(JObject jObj) + { + if (Slot is not HumanSlot.Unknown) + jObj["Slot"] = Slot.ToString(); + if (Id.HasValue) + jObj["Id"] = Id.Value.Id.ToString(); + jObj["Shape"] = Shape.ToString(); + return jObj; + } + + public static ShpIdentifier? FromJson(JObject jObj) + { + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var shape = jObj["Shape"]?.ToObject(); + if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) + return null; + + var identifier = new ShpIdentifier(slot, id, shapeString); + return identifier.Validate() ? identifier : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Shp; +} + +[JsonConverter(typeof(Converter))] +public readonly record struct ShpEntry(bool Value) +{ + public static readonly ShpEntry True = new(true); + public static readonly ShpEntry False = new(false); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ShpEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override ShpEntry ReadJson(JsonReader reader, Type objectType, ShpEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 4356086a..ec8ddb50 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,24 +1,30 @@ +using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; -using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; -using Penumbra.Services; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; public class ShapeManager : IRequiredService, IDisposable { - public const int NumSlots = 4; - private readonly CommunicatorService _communicator; + public const int NumSlots = 14; + public const int ModelSlotSize = 18; + private readonly AttributeHook _attributeHook; - private static ReadOnlySpan UsedModels - => [1, 2, 3, 4]; + public static ReadOnlySpan UsedModels + => + [ + HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, + HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, + ]; - public ShapeManager(CommunicatorService communicator) + public ShapeManager(AttributeHook attributeHook) { - _communicator = communicator; - _communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager); + _attributeHook = attributeHook; + _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager); } private readonly Dictionary[] _temporaryIndices = @@ -27,38 +33,30 @@ public class ShapeManager : IRequiredService, IDisposable private readonly uint[] _temporaryMasks = new uint[NumSlots]; private readonly uint[] _temporaryValues = new uint[NumSlots]; - private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot) - { - int index; - switch (slot) - { - case HumanSlot.Unknown: - ResetCache(model); - return; - case HumanSlot.Body: index = 0; break; - case HumanSlot.Hands: index = 1; break; - case HumanSlot.Legs: index = 2; break; - case HumanSlot.Feet: index = 3; break; - default: return; - } + public void Dispose() + => _attributeHook.Unsubscribe(OnAttributeComputed); - if (_temporaryMasks[index] is 0) + private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) + { + ComputeCache(model, collection); + for (var i = 0; i < NumSlots; ++i) + { + if (_temporaryMasks[i] is 0) + continue; + + var modelIndex = UsedModels[i]; + var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask; + var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i]; + Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); + model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask; + } + } + + private unsafe void ComputeCache(Model human, ModCollection collection) + { + if (!collection.HasCache) return; - var modelIndex = UsedModels[index]; - var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask; - var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index]; - Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); - model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask; - } - - public void Dispose() - { - _communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed); - } - - private unsafe void ResetCache(Model human) - { for (var i = 0; i < NumSlots; ++i) { _temporaryMasks[i] = 0; @@ -66,17 +64,20 @@ public class ShapeManager : IRequiredService, IDisposable _temporaryIndices[i].Clear(); var modelIndex = UsedModels[i]; - var model = human.AsHuman->Models[modelIndex]; + var model = human.AsHuman->Models[modelIndex.ToIndex()]; if (model is null || model->ModelResourceHandle is null) continue; ref var shapes = ref model->ModelResourceHandle->Shapes; - foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex))) + foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) { if (ShapeString.TryRead(shape.Value, out var shapeString)) { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); + if (collection.MetaCache!.Shp.State.Count > 0 + && collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set)) + _temporaryValues[i] |= (ushort)(1 << index); } else { @@ -85,42 +86,32 @@ public class ShapeManager : IRequiredService, IDisposable } } - UpdateMasks(); + UpdateDefaultMasks(); } - private static bool CheckShapes(ReadOnlySpan shape, byte index) - => index switch - { - 1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8), - 2 => shape.StartsWith("shp_wr_"u8), - 3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8), - 4 => shape.StartsWith("shp_an"u8), - _ => false, - }; - - private void UpdateMasks() + private void UpdateDefaultMasks() { - foreach (var (shape, topIndex) in _temporaryIndices[0]) + foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (_temporaryIndices[1].TryGetValue(shape, out var handIndex)) + if (shape[4] is (byte)'w' && shape[5] is (byte)'r' && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { - _temporaryValues[0] |= 1u << topIndex; - _temporaryValues[1] |= 1u << handIndex; + _temporaryValues[1] |= 1u << topIndex; + _temporaryValues[2] |= 1u << handIndex; } - if (_temporaryIndices[2].TryGetValue(shape, out var legIndex)) + if (shape[4] is (byte)'w' && shape[5] is (byte)'a' && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { - _temporaryValues[0] |= 1u << topIndex; - _temporaryValues[2] |= 1u << legIndex; + _temporaryValues[1] |= 1u << topIndex; + _temporaryValues[3] |= 1u << legIndex; } } - foreach (var (shape, bottomIndex) in _temporaryIndices[2]) + foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (_temporaryIndices[3].TryGetValue(shape, out var footIndex)) + if (shape[4] is (byte)'a' && shape[5] is (byte)'n' && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { - _temporaryValues[2] |= 1u << bottomIndex; - _temporaryValues[3] |= 1u << footIndex; + _temporaryValues[3] |= 1u << bottomIndex; + _temporaryValues[4] |= 1u << footIndex; } } } diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs index 987ed474..5b6f9c52 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeString.cs @@ -1,11 +1,12 @@ using Lumina.Misc; using Newtonsoft.Json; using Penumbra.GameData.Files.PhybStructs; +using Penumbra.String.Functions; namespace Penumbra.Meta; [JsonConverter(typeof(Converter))] -public struct ShapeString : IEquatable +public struct ShapeString : IEquatable, IComparable { public const int MaxLength = 30; @@ -22,6 +23,20 @@ public struct ShapeString : IEquatable public override string ToString() => Encoding.UTF8.GetString(_buffer[..Length]); + public byte this[int index] + => _buffer[index]; + + public unsafe ReadOnlySpan AsSpan + { + get + { + fixed (void* ptr = &this) + { + return new ReadOnlySpan(ptr, Length); + } + } + } + public bool Equals(ShapeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); @@ -43,6 +58,14 @@ public struct ShapeString : IEquatable return TryRead(span, out ret); } + public unsafe int CompareTo(ShapeString other) + { + fixed (void* lhs = &this) + { + return ByteStringFunctions.Compare((byte*)lhs, Length, (byte*)&other, other.Length); + } + } + public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) { if (utf8.Length is 0 or > MaxLength) @@ -69,6 +92,14 @@ public struct ShapeString : IEquatable return true; } + public void ForceLength(byte length) + { + if (length > MaxLength) + length = MaxLength; + _buffer[length] = 0; + _buffer[31] = length; + } + private sealed class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index e008752f..5d745419 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,9 +81,6 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); - /// - public readonly ModelAttributeComputed ModelAttributeComputed = new(); - public void Dispose() { CollectionChange.Dispose(); @@ -108,6 +105,5 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); - ModelAttributeComputed.Dispose(); } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index d1c7cd52..70b5f83b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -11,7 +11,8 @@ public class MetaDrawers( GmpMetaDrawer gmp, ImcMetaDrawer imc, RspMetaDrawer rsp, - AtchMetaDrawer atch) : IService + AtchMetaDrawer atch, + ShpMetaDrawer shp) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -21,6 +22,7 @@ public class MetaDrawers( public readonly ImcMetaDrawer Imc = imc; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly AtchMetaDrawer Atch = atch; + public readonly ShpMetaDrawer Shp = shp; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -32,6 +34,7 @@ public class MetaDrawers( MetaManipulationType.Gmp => Gmp, MetaManipulationType.Rsp => Rsp, MetaManipulationType.Atch => Atch, + MetaManipulationType.Shp => Shp, MetaManipulationType.GlobalEqp => GlobalEqp, _ => null, }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs new file mode 100644 index 00000000..4be6e6aa --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -0,0 +1,247 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Shape Keys (SHP)###SHP"u8; + + private ShapeString _buffer = ShapeString.TryRead("shp_"u8, out var s) ? s : ShapeString.Empty; + private bool _identifierValid; + + public override int NumColumns + => 6; + + public override float ColumnHeight + => ImUtf8.FrameHeightSpacing; + + protected override void Initialize() + { + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty); + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current SHP manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var tt = canAdd + ? "Stage this edit."u8 + : _identifierValid + ? "This entry does not contain a valid shape key."u8 + : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, ShpEntry.True); + + DrawIdentifierInput(ref Identifier); + DrawEntry(ref Entry, true); + } + + protected override void DrawEntry(ShpIdentifier identifier, ShpEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + if (DrawEntry(ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(ShpIdentifier, ShpEntry)> Enumerate() + => Editor.Shp + .OrderBy(kvp => kvp.Key.Shape) + .ThenBy(kvp => kvp.Key.Slot) + .ThenBy(kvp => kvp.Key.Id) + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Shp.Count; + + private bool DrawIdentifierInput(ref ShpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawHumanSlot(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + return changes; + } + + private static void DrawIdentifier(ShpIdentifier identifier) + { + ImGui.TableNextColumn(); + + ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor); + ImUtf8.HoverTooltip("Model Slot"u8); + + ImGui.TableNextColumn(); + if (identifier.Id.HasValue) + ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); + else + ImUtf8.TextFramed("All IDs"u8, FrameColor); + ImUtf8.HoverTooltip("Primary ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); + } + + private static bool DrawEntry(ref ShpEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var value = entry.Value; + var changes = ImUtf8.Checkbox("##shpEntry"u8, ref value); + if (changes) + entry = new ShpEntry(value); + ImUtf8.HoverTooltip("Whether to enable or disable this shape key for the selected items."); + return changes; + } + + public static bool DrawPrimaryId(ref ShpIdentifier identifier, float unscaledWidth = 100) + { + var allSlots = identifier.Slot is HumanSlot.Unknown; + var all = !identifier.Id.HasValue; + var ret = false; + using (ImRaii.Disabled(allSlots)) + { + if (ImUtf8.Checkbox("##shpAll"u8, ref all)) + { + identifier = identifier with { Id = all ? null : 0 }; + ret = true; + } + } + + ImUtf8.HoverTooltip(allSlots ? "When using all slots, you also need to use all IDs."u8 : "Enable this shape key for all model IDs."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (all) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f), + new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + else + { + if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, + ExpandedEqpGmpBase.Count - 1, + false)) + { + identifier = identifier with { Id = setId }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8); + + return ret; + } + + public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##shpSlot"u8, SlotName(identifier.Slot))) + { + if (combo) + foreach (var slot in AvailableSlots) + { + if (!ImUtf8.Selectable(SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot) + continue; + + ret = true; + if (slot is HumanSlot.Unknown) + identifier = identifier with + { + Id = null, + Slot = slot, + }; + else + identifier = identifier with { Slot = slot }; + } + } + + ImUtf8.HoverTooltip("Model Slot"u8); + return ret; + } + + public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = ShpIdentifier.ValidateCustomShapeString(buffer); + if (valid) + identifier = identifier with { Shape = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Supported shape keys need to have the format `shp_xx_*` and a maximum length of 30 characters."u8); + return ret; + } + + private static ReadOnlySpan AvailableSlots + => + [ + HumanSlot.Unknown, + HumanSlot.Head, + HumanSlot.Body, + HumanSlot.Hands, + HumanSlot.Legs, + HumanSlot.Feet, + HumanSlot.Ears, + HumanSlot.Neck, + HumanSlot.Wrists, + HumanSlot.RFinger, + HumanSlot.LFinger, + HumanSlot.Glasses, + HumanSlot.Hair, + HumanSlot.Face, + HumanSlot.Ear, + ]; + + private static ReadOnlySpan SlotName(HumanSlot slot) + => slot switch + { + HumanSlot.Unknown => "All Slots"u8, + HumanSlot.Head => "Equipment: Head"u8, + HumanSlot.Body => "Equipment: Body"u8, + HumanSlot.Hands => "Equipment: Hands"u8, + HumanSlot.Legs => "Equipment: Legs"u8, + HumanSlot.Feet => "Equipment: Feet"u8, + HumanSlot.Ears => "Equipment: Ears"u8, + HumanSlot.Neck => "Equipment: Neck"u8, + HumanSlot.Wrists => "Equipment: Wrists"u8, + HumanSlot.RFinger => "Equipment: Right Finger"u8, + HumanSlot.LFinger => "Equipment: Left Finger"u8, + HumanSlot.Glasses => "Equipment: Glasses"u8, + HumanSlot.Hair => "Customization: Hair"u8, + HumanSlot.Face => "Customization: Face"u8, + HumanSlot.Ear => "Customization: Ears"u8, + _ => "Unknown"u8, + }; +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 68424ae9..70a15373 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -61,6 +61,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Atch); + DrawEditHeader(MetaManipulationType.Shp); DrawEditHeader(MetaManipulationType.GlobalEqp); } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 968bc484..5292bd17 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -5,10 +5,12 @@ using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; namespace Penumbra.UI.Tabs.Debug; -public class ShapeInspector(ObjectManager objects) : IUiService +public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService { private int _objectIndex = 0; @@ -29,42 +31,92 @@ public class ShapeInspector(ObjectManager objects) : IUiService return; } - using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg); - if (!table) - return; - - ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); - ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); - - var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - foreach (var slot in Enum.GetValues()) + var data = resolver.IdentifyCollection(actor.AsObject, true); + using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})")) { - ImUtf8.DrawTableColumn($"{(uint)slot:D2}"); - ImGui.TableNextColumn(); - var model = human.AsHuman->Models[(int)slot]; - Penumbra.Dynamis.DrawPointer((nint)model); - if (model is not null) + if (treeNode1.Success && data.ModCollection.HasCache) { - var mask = model->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch); + + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) { - var disabled = (mask & (1u << idx)) is 0; - using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); - ImGui.SameLine(0, 0); - ImUtf8.Text(", "u8); - if ((idx % 8) < 7) - ImGui.SameLine(0, 0); + ImUtf8.DrawTableColumn(shape.AsSpan); + if (set.All) + { + ImUtf8.DrawTableColumn("All"u8); + } + else + { + ImGui.TableNextColumn(); + foreach (var slot in ShapeManager.UsedModels) + { + if (!set[slot]) + continue; + + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var item in set.Where(i => !set[i.Slot])) + { + ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + } } } - else + } + + using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8)) + { + if (treeNode2) { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } } } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 7b3a3c8b..cb22b54a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -51,7 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; - private readonly AttributeHooks _attributeHooks; + private readonly AttributeHook _attributeHook; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -64,7 +64,7 @@ public class SettingsTab : ITab, IUiService DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, - AttributeHooks attributeHooks) + AttributeHook attributeHook) { _pluginInterface = pluginInterface; _config = config; @@ -89,7 +89,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; - _attributeHooks = attributeHooks; + _attributeHook = attributeHook; } public void DrawHeader() @@ -525,55 +525,6 @@ public class SettingsTab : ITab, IUiService ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab."); } - private float _absoluteSelectorSize = float.NaN; - - /// Draw a selector for the absolute size of the mod selector in pixels. - private void DrawAbsoluteSizeSelector() - { - if (float.IsNaN(_absoluteSelectorSize)) - _absoluteSelectorSize = _config.ModSelectorAbsoluteSize; - - if (ImGuiUtil.DragFloat("##absoluteSize", ref _absoluteSelectorSize, UiHelpers.InputTextWidth.X, 1, - Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f") - && _absoluteSelectorSize != _config.ModSelectorAbsoluteSize) - { - _config.ModSelectorAbsoluteSize = _absoluteSelectorSize; - _config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Mod Selector Absolute Size", - "The minimal absolute size of the mod selector in the mod tab in pixels."); - } - - private int _relativeSelectorSize = int.MaxValue; - - /// Draw a selector for the relative size of the mod selector as a percentage and a toggle to enable relative sizing. - private void DrawRelativeSizeSelector() - { - var scaleModSelector = _config.ScaleModSelector; - if (ImGui.Checkbox("Scale Mod Selector With Window Size", ref scaleModSelector)) - { - _config.ScaleModSelector = scaleModSelector; - _config.Save(); - } - - ImGui.SameLine(); - if (_relativeSelectorSize == int.MaxValue) - _relativeSelectorSize = _config.ModSelectorScaledSize; - if (ImGuiUtil.DragInt("##relativeSize", ref _relativeSelectorSize, UiHelpers.InputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, - Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%") - && _relativeSelectorSize != _config.ModSelectorScaledSize) - { - _config.ModSelectorScaledSize = _relativeSelectorSize; - _config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Mod Selector Relative Size", - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); - } - private void DrawRenameSettings() { ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); @@ -607,8 +558,6 @@ public class SettingsTab : ITab, IUiService private void DrawModSelectorSettings() { DrawFolderSortType(); - DrawAbsoluteSizeSelector(); - DrawRelativeSizeSelector(); DrawRenameSettings(); Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", _config.OpenFoldersByDefault, v => @@ -626,7 +575,8 @@ public class SettingsTab : ITab, IUiService _config.Save(); }); Widget.DoubleModifierSelector("Incognito Modifier", - "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", UiHelpers.InputTextWidth.X, + "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", + UiHelpers.InputTextWidth.X, _config.IncognitoModifier, v => { @@ -811,8 +761,8 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); - Checkbox("Enable Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", - _config.EnableAttributeHooks, _attributeHooks.SetState); + Checkbox("Enable Custom Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + _config.EnableCustomShapes, _attributeHook.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json index 4a41dbe2..55fc5cad 100644 --- a/schemas/structs/manipulation.json +++ b/schemas/structs/manipulation.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "Type": { - "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ] + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ] }, "Manipulation": { "type": "object" @@ -90,6 +90,16 @@ "$ref": "meta_atch.json" } } + }, + { + "properties": { + "Type": { + "const": "Shp" + }, + "Manipulation": { + "$ref": "meta_shp.json" + } + } } ] } diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json index 747da849..2fc65a0d 100644 --- a/schemas/structs/meta_enums.json +++ b/schemas/structs/meta_enums.json @@ -5,6 +5,10 @@ "$anchor": "EquipSlot", "enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ] }, + "HumanSlot": { + "$anchor": "HumanSlot", + "enum": [ "Head", "Body", "Hands", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "LFinger", "Hair", "Face", "Ear", "Glasses", "Unknown" ] + }, "Gender": { "$anchor": "Gender", "enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ] diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json new file mode 100644 index 00000000..e6b66420 --- /dev/null +++ b/schemas/structs/meta_shp.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "boolean" + }, + "Slot": { + "$ref": "meta_enums.json#HumanSlot" + }, + "Id": { + "$ref": "meta_enums.json#U16" + }, + "Shape": { + "type": "string", + "minLength": 8, + "maxLength": 30 + } + }, + "required": [ + "Shape" + ] +} From 480942339f4bff0d30e7afcb84852d3f318eb47a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 17:47:32 +0200 Subject: [PATCH 2274/2451] Add draggable mod selector width. --- Penumbra/EphemeralConfig.cs | 5 +++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 33 +++++++++++++++----- Penumbra/UI/Tabs/ModsTab.cs | 15 +-------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 678e53ad..ecb0218f 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui.Classes; +using OtterGui.FileSystem.Selector; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; @@ -23,6 +24,10 @@ public class EphemeralConfig : ISavable, IDisposable, IService [JsonIgnore] private readonly ModPathChanged _modPathChanged; + public float CurrentModSelectorWidth { get; set; } = 200f; + public float ModSelectorMinimumScale { get; set; } = 0.1f; + public float ModSelectorMaximumScale { get; set; } = 0.5f; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public bool DebugSeparateWindow { get; set; } = false; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 6586747c..2dff19ab 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -131,18 +131,41 @@ public sealed class ModFileSystemSelector : FileSystemSelector m.Extensions.Any(e => ValidModExtensions.Contains(e.ToLowerInvariant())), m => { ImUtf8.Text($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); return true; }); - base.Draw(width); + base.Draw(); if (_dragDrop.CreateImGuiTarget("ModDragDrop", out var files, out _)) _modImportManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f.ToLowerInvariant())))); } + protected override float CurrentWidth + => _config.Ephemeral.CurrentModSelectorWidth * ImUtf8.GlobalScale; + + protected override float MinimumAbsoluteRemainder + => 550 * ImUtf8.GlobalScale; + + protected override float MinimumScaling + => _config.Ephemeral.ModSelectorMinimumScale; + + protected override float MaximumScaling + => _config.Ephemeral.ModSelectorMaximumScale; + + protected override void SetSize(Vector2 size) + { + base.SetSize(size); + var adaptedSize = MathF.Round(size.X / ImUtf8.GlobalScale); + if (adaptedSize == _config.Ephemeral.CurrentModSelectorWidth) + return; + + _config.Ephemeral.CurrentModSelectorWidth = adaptedSize; + _config.Ephemeral.Save(); + } + public override void Dispose() { base.Dispose(); @@ -651,14 +674,10 @@ public sealed class ModFileSystemSelector : FileSystemSelector Get the correct size for the mod selector based on current config. - public static float GetModSelectorSize(Configuration config) - { - var absoluteSize = Math.Clamp(config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); - var relativeSize = config.ScaleModSelector - ? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) - : 0; - return MathF.Round(config.ScaleModSelector - ? Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100) - : absoluteSize); - } - private void DrawRedrawLine() { if (config.HideRedrawBar) From 70295b7a6bca0c5fb1358cf5f396dee4173a5e23 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 15 May 2025 15:50:16 +0000 Subject: [PATCH 2275/2451] [CI] Updating repo.json for testing_1.3.6.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8e88ad52..13d91e5c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.8", + "TestingAssemblyVersion": "1.3.6.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c0dcfdd83587a5c27bcb707f391ad9e53ed752fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 22:23:33 +0200 Subject: [PATCH 2276/2451] Update shape string format. --- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 31 ++++--------------- Penumbra/Meta/ShapeManager.cs | 10 ++++-- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 4 +-- schemas/structs/meta_shp.json | 5 +-- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index fffa51ba..c642167f 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,4 +1,3 @@ -using Lumina.Models.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -61,34 +60,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return ValidateCustomShapeString(Shape); } - public static bool ValidateCustomShapeString(ReadOnlySpan shape) - { - // "shp_xx_y" - if (shape.Length < 8) - return false; - - if (shape[0] is not (byte)'s' - || shape[1] is not (byte)'h' - || shape[2] is not (byte)'p' - || shape[3] is not (byte)'_' - || shape[6] is not (byte)'_') - return false; - - return true; - } - public static unsafe bool ValidateCustomShapeString(byte* shape) { - // "shp_xx_y" + // "shpx_*" if (shape is null) return false; if (*shape++ is not (byte)'s' || *shape++ is not (byte)'h' || *shape++ is not (byte)'p' - || *shape++ is not (byte)'_' - || *shape++ is 0 - || *shape++ is 0 + || *shape++ is not (byte)'x' || *shape++ is not (byte)'_' || *shape is 0) return false; @@ -98,16 +79,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape public static bool ValidateCustomShapeString(in ShapeString shape) { - // "shp_xx_y" - if (shape.Length < 8) + // "shpx_*" + if (shape.Length < 6) return false; var span = shape.AsSpan; if (span[0] is not (byte)'s' || span[1] is not (byte)'h' || span[2] is not (byte)'p' - || span[3] is not (byte)'_' - || span[6] is not (byte)'_') + || span[3] is not (byte)'x' + || span[4] is not (byte)'_') return false; return true; diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index ec8ddb50..dc3e1a1c 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -93,13 +93,13 @@ public class ShapeManager : IRequiredService, IDisposable { foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (shape[4] is (byte)'w' && shape[5] is (byte)'r' && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) + if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; } - if (shape[4] is (byte)'w' && shape[5] is (byte)'a' && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) + if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; @@ -108,11 +108,15 @@ public class ShapeManager : IRequiredService, IDisposable foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (shape[4] is (byte)'a' && shape[5] is (byte)'n' && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) + if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool CheckCenter(in ShapeString shape, char first, char second) + => shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_'; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 4be6e6aa..2c99af02 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,7 +19,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shp_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; private bool _identifierValid; public override int NumColumns @@ -200,7 +200,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } } - ImUtf8.HoverTooltip("Supported shape keys need to have the format `shp_xx_*` and a maximum length of 30 characters."u8); + ImUtf8.HoverTooltip("Supported shape keys need to have the format `shpx_*` and a maximum length of 30 characters."u8); return ret; } diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index e6b66420..197f3104 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -13,8 +13,9 @@ }, "Shape": { "type": "string", - "minLength": 8, - "maxLength": 30 + "minLength": 5, + "maxLength": 30, + "pattern": "^shpx_" } }, "required": [ From f1448ed947039cc9a9e235e6da4745cdcde483cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 May 2025 00:25:13 +0200 Subject: [PATCH 2277/2451] Add conditional connector shapes. --- Penumbra/Collections/Cache/ShpCache.cs | 79 +++++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 62 +++++- Penumbra/Meta/ShapeManager.cs | 58 ++++-- Penumbra/Meta/ShapeString.cs | 16 ++ .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 76 +++++++- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 183 ++++++++++-------- schemas/structs/meta_shp.json | 6 + 7 files changed, 360 insertions(+), 120 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2e90052d..eaf949d9 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -13,7 +13,23 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) internal IReadOnlyDictionary State => _shpData; - internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> + internal IEnumerable<(ShapeString, IReadOnlyDictionary)> ConditionState + => _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary)kvp.Value)); + + public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary? dict) + { + if (_conditionalSet.TryGetValue(condition, out var d)) + { + dict = d; + return true; + } + + dict = null; + return false; + } + + + public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -76,12 +92,14 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) => !_allIds.HasAnySet() && Count is 0; } - private readonly Dictionary _shpData = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary> _conditionalSet = []; public void Reset() { Clear(); _shpData.Clear(); + _conditionalSet.Clear(); } protected override void Dispose(bool _) @@ -89,21 +107,62 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) + if (identifier.ShapeCondition.Length > 0) { - value = []; - _shpData.Add(identifier.Shape, value); + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + { + if (!entry.Value) + return; + + shapes = new Dictionary(); + _conditionalSet.Add(identifier.ShapeCondition, shapes); + } + + Func(shapes); + } + else + { + Func(_shpData); } - value.TrySet(identifier.Slot, identifier.Id, entry); + void Func(Dictionary dict) + { + if (!dict.TryGetValue(identifier.Shape, out var value)) + { + if (!entry.Value) + return; + + value = []; + dict.Add(identifier.Shape, value); + } + + value.TrySet(identifier.Slot, identifier.Id, entry); + } } protected override void RevertModInternal(ShpIdentifier identifier) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) - return; + if (identifier.ShapeCondition.Length > 0) + { + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) - _shpData.Remove(identifier.Shape); + Func(shapes); + } + else + { + Func(_shpData); + } + + return; + + void Func(Dictionary dict) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + _shpData.Remove(identifier.Shape); + } } } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index c642167f..777be512 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -7,7 +7,7 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape) +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -34,12 +34,39 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return 1; } + var shapeComparison = Shape.CompareTo(other.Shape); + if (shapeComparison is not 0) + return shapeComparison; - return Shape.CompareTo(other.Shape); + return ShapeCondition.CompareTo(other.ShapeCondition); } + public override string ToString() - => $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}"; + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Shape); + if (Slot is HumanSlot.Unknown) + { + sb.Append(" - All Slots & IDs"); + } + else + { + sb.Append(" - ") + .Append(Slot.ToName()) + .Append(" - "); + if (Id.HasValue) + sb.Append(Id.Value.Id); + else + sb.Append("All IDs"); + } + + if (ShapeCondition.Length > 0) + sb.Append(" - ") + .Append(ShapeCondition); + return sb.ToString(); + } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { @@ -57,7 +84,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Slot is HumanSlot.Unknown && Id is not null) return false; - return ValidateCustomShapeString(Shape); + if (!ValidateCustomShapeString(Shape)) + return false; + + if (ShapeCondition.Length is 0) + return true; + + if (!ValidateCustomShapeString(ShapeCondition)) + return false; + + return Slot switch + { + HumanSlot.Hands when ShapeCondition.IsWrist() => true, + HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, + HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + HumanSlot.Feet when ShapeCondition.IsAnkle() => true, + HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + _ => false, + }; } public static unsafe bool ValidateCustomShapeString(byte* shape) @@ -101,18 +145,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id.HasValue) jObj["Id"] = Id.Value.Id.ToString(); jObj["Shape"] = Shape.ToString(); + if (ShapeCondition.Length > 0) + jObj["ShapeCondition"] = ShapeCondition.ToString(); return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); var shape = jObj["Shape"]?.ToObject(); if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) return null; - var identifier = new ShpIdentifier(slot, id, shapeString); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var shapeCondition = jObj["ShapeCondition"]?.ToObject(); + var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; + var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index dc3e1a1c..57f6f23f 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,8 +1,10 @@ using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Meta.Manipulations; @@ -30,15 +32,19 @@ public class ShapeManager : IRequiredService, IDisposable private readonly Dictionary[] _temporaryIndices = Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); - private readonly uint[] _temporaryMasks = new uint[NumSlots]; - private readonly uint[] _temporaryValues = new uint[NumSlots]; + private readonly uint[] _temporaryMasks = new uint[NumSlots]; + private readonly uint[] _temporaryValues = new uint[NumSlots]; + private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; public void Dispose() => _attributeHook.Unsubscribe(OnAttributeComputed); private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) { - ComputeCache(model, collection); + if (!collection.HasCache) + return; + + ComputeCache(model, collection.MetaCache!.Shp); for (var i = 0; i < NumSlots; ++i) { if (_temporaryMasks[i] is 0) @@ -52,11 +58,8 @@ public class ShapeManager : IRequiredService, IDisposable } } - private unsafe void ComputeCache(Model human, ModCollection collection) + private unsafe void ComputeCache(Model human, ShpCache cache) { - if (!collection.HasCache) - return; - for (var i = 0; i < NumSlots; ++i) { _temporaryMasks[i] = 0; @@ -68,6 +71,8 @@ public class ShapeManager : IRequiredService, IDisposable if (model is null || model->ModelResourceHandle is null) continue; + _ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set; + ref var shapes = ref model->ModelResourceHandle->Shapes; foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) { @@ -75,8 +80,8 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); - if (collection.MetaCache!.Shp.State.Count > 0 - && collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set)) + if (cache.State.Count > 0 + && cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) _temporaryValues[i] |= (ushort)(1 << index); } else @@ -86,37 +91,54 @@ public class ShapeManager : IRequiredService, IDisposable } } - UpdateDefaultMasks(); + UpdateDefaultMasks(cache); } - private void UpdateDefaultMasks() + private void UpdateDefaultMasks(ShpCache cache) { foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; + CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) + { + if (!cache.CheckConditionState(shape, out var dict)) + return; + + foreach (var (subShape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1])) + if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx1] |= 1u << subIndex; + if (set.Contains(slot2, _ids[idx2])) + if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx2] |= 1u << subIndex; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static bool CheckCenter(in ShapeString shape, char first, char second) - => shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_'; } diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs index 5b6f9c52..95ca0933 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeString.cs @@ -37,6 +37,22 @@ public struct ShapeString : IEquatable, IComparable } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsAnkle() + => CheckCenter('a', 'n'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWaist() + => CheckCenter('w', 'a'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWrist() + => CheckCenter('w', 'r'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool CheckCenter(char first, char second) + => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; + public bool Equals(ShapeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 2c99af02..fe7e743c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,18 +19,20 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _conditionBuffer = ShapeString.Empty; private bool _identifierValid; + private bool _conditionValid = true; public override int NumColumns - => 6; + => 7; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty); } protected override void DrawNew() @@ -40,7 +42,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); ImGui.TableNextColumn(); - var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid; var tt = canAdd ? "Stage this edit."u8 : _identifierValid @@ -67,6 +69,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .OrderBy(kvp => kvp.Key.Shape) .ThenBy(kvp => kvp.Key.Slot) .ThenBy(kvp => kvp.Key.Id) + .ThenBy(kvp => kvp.Key.ShapeCondition) .Select(kvp => (kvp.Key, kvp.Value)); protected override int Count @@ -82,6 +85,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + + ImGui.TableNextColumn(); + changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid); return changes; } @@ -101,6 +107,13 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); + + ImGui.TableNextColumn(); + if (identifier.ShapeCondition.Length > 0) + { + ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor); + ImUtf8.HoverTooltip("Connector condition for this shape to be activated."); + } } private static bool DrawEntry(ref ShpEntry entry, bool disabled) @@ -154,7 +167,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -168,13 +181,37 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ret = true; if (slot is HumanSlot.Unknown) + { identifier = identifier with { Id = null, Slot = slot, }; + } else - identifier = identifier with { Slot = slot }; + { + if (_conditionBuffer.Length > 0 + && (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs + || _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body + || _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs)) + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = ShapeString.Empty, + }; + _conditionValid = false; + } + else + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = _conditionBuffer, + }; + _conditionValid = true; + } + } } } @@ -204,6 +241,33 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } + public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = ShpIdentifier.ValidateCustomShapeString(buffer) + && (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs + || buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs + || buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands); + if (valid) + identifier = identifier with { ShapeCondition = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip( + "Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8); + return ret; + } + private static ReadOnlySpan AvailableSlots => [ diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 5292bd17..8439587c 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; @@ -12,9 +13,9 @@ namespace Penumbra.UI.Tabs.Debug; public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService { - private int _objectIndex = 0; + private int _objectIndex; - public unsafe void Draw() + public void Draw() { ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); var actor = objects[0]; @@ -31,93 +32,117 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; } - var data = resolver.IdentifyCollection(actor.AsObject, true); - using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})")) + DrawCollectionShapeCache(actor); + DrawCharacterShapes(human); + } + + private unsafe void DrawCollectionShapeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) { - if (treeNode1.Success && data.ModCollection.HasCache) - { - using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg); - if (!table) - return; - - ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch); - - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) - { - ImUtf8.DrawTableColumn(shape.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) - { - if (!set[slot]) - continue; - - ImUtf8.Text($"All {slot.ToName()}, "); - ImGui.SameLine(0, 0); - } - - foreach (var item in set.Where(i => !set[i.Slot])) - { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); - } - } - } - } + ImGui.TableNextColumn(); + DrawShape(shape, set); } - using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8)) + foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState) { - if (treeNode2) + foreach (var (shape, set) in dict) { - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); - if (!table) - return; + ImUtf8.DrawTableColumn(condition.AsSpan); + DrawShape(shape, set); + } + } + } - ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); - ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + { + ImUtf8.DrawTableColumn(shape.AsSpan); + if (set.All) + { + ImUtf8.DrawTableColumn("All"u8); + } + else + { + ImGui.TableNextColumn(); + foreach (var slot in ShapeManager.UsedModels) + { + if (!set[slot]) + continue; - var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - for (var i = 0; i < human.AsHuman->SlotCount; ++i) + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var item in set.Where(i => !set[i.Slot])) + { + ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + } + } + + private unsafe void DrawCharacterShapes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) { - ImUtf8.DrawTableColumn($"{(uint)i:D2}"); - ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); - - ImGui.TableNextColumn(); - var model = human.AsHuman->Models[i]; - Penumbra.Dynamis.DrawPointer((nint)model); - if (model is not null) - { - var mask = model->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) - { - var disabled = (mask & (1u << idx)) is 0; - using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); - ImGui.SameLine(0, 0); - ImUtf8.Text(", "u8); - if (idx % 8 < 7) - ImGui.SameLine(0, 0); - } - } - else - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - } + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); } } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } } } } diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 197f3104..4f868a0a 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -16,6 +16,12 @@ "minLength": 5, "maxLength": 30, "pattern": "^shpx_" + }, + "ShapeCondition": { + "type": "string", + "minLength": 8, + "maxLength": 30, + "pattern": "^shpx_(wa|an|wr)_" } }, "required": [ From 08e8b9d2a460ad7245ddcf6006e10d23cfc3f267 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 15 May 2025 22:28:36 +0000 Subject: [PATCH 2278/2451] [CI] Updating repo.json for testing_1.3.6.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 13d91e5c..137ba00c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.9", + "TestingAssemblyVersion": "1.3.6.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 52927ff06bbc7c159a237876e4cf50ea328dbddd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:31:13 +0200 Subject: [PATCH 2279/2451] Fix clipping in meta edits. --- OtterGui | 2 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index f130c928..9aeda9a8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98 +Subproject commit 9aeda9a892d9b971e32b10db21a8daf9c0b9ee53 diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index a6f042b7..7e788462 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -44,9 +44,13 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta DrawNew(); var height = ColumnHeight; - var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); - var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); - ImGuiClip.DrawEndDummy(remainder, height); + var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY(), Count); + if (skips < Count) + { + var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); + if (remainder > 0) + ImGuiClip.DrawEndDummy(remainder, height); + } void DrawLine((TIdentifier Identifier, TEntry Value) pair) => DrawEntry(pair.Identifier, pair.Value); From 3078c467d0666e28ab25dd31d515648300f946da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:31:34 +0200 Subject: [PATCH 2280/2451] Fix issue with empty and temporary settings. --- Penumbra/Mods/Settings/ModSettings.cs | 15 +++++++++++---- Penumbra/Mods/Settings/TemporaryModSettings.cs | 11 +++++++++-- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 07217d4d..bbdd6bfa 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -11,11 +11,18 @@ namespace Penumbra.Mods.Settings; /// Contains the settings for a given mod. public class ModSettings { - public static readonly ModSettings Empty = new(); + public static readonly ModSettings Empty = new(true); - public SettingList Settings { get; internal init; } = []; - public ModPriority Priority { get; set; } - public bool Enabled { get; set; } + public SettingList Settings { get; internal init; } = []; + public ModPriority Priority { get; set; } + public bool Enabled { get; set; } + public bool IsEmpty { get; protected init; } + + public ModSettings() + { } + + protected ModSettings(bool empty) + => IsEmpty = empty; // Create an independent copy of the current settings. public ModSettings DeepCopy() diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index a16a9feb..ce438aac 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -2,9 +2,11 @@ namespace Penumbra.Mods.Settings; public sealed class TemporaryModSettings : ModSettings { + public new static readonly TemporaryModSettings Empty = new(true); + public const string OwnSource = "yourself"; public string Source = string.Empty; - public int Lock = 0; + public int Lock; public bool ForceInherit; // Create default settings for a given mod. @@ -21,12 +23,16 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } + private TemporaryModSettings(bool empty) + : base(empty) + { } + public TemporaryModSettings(Mod mod, ModSettings? clone, string source = OwnSource, int key = 0) { Source = source; Lock = key; ForceInherit = clone == null; - if (clone != null && clone != Empty) + if (clone is { IsEmpty: false }) { Enabled = clone.Enabled; Priority = clone.Priority; @@ -34,6 +40,7 @@ public sealed class TemporaryModSettings : ModSettings } else { + IsEmpty = true; Enabled = false; Priority = ModPriority.Default; Settings = SettingList.Default(mod); diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 666fce61..3e165cb5 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -49,7 +49,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle case GroupDrawBehaviour.SingleSelection: ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; - DrawSingleGroupCombo(group, idx, settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]); + DrawSingleGroupCombo(group, idx, settings.IsEmpty ? group.DefaultSettings : settings.Settings[idx]); break; } } @@ -59,7 +59,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; - var option = settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]; + var option = settings.IsEmpty ? group.DefaultSettings : settings.Settings[idx]; if (group.Behaviour is GroupDrawBehaviour.MultiSelection) DrawMultiGroup(group, idx, option); else diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 3988de35..7c6ebf74 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -183,7 +183,7 @@ public class ModPanelSettingsTab( /// private void DrawRemoveSettings() { - var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; + var drawInherited = !_inherited && !selection.Settings.IsEmpty; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X : 0; var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; var offset = drawInherited From fbc4c2d054dd37575d495736d01fa6cd97b7ec07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:54:09 +0200 Subject: [PATCH 2281/2451] Improve option select combo. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 75 +++++++++++++------ .../UI/AdvancedWindow/OptionSelectCombo.cs | 43 +++++++++++ 2 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index ccbbf0db..0b9fcde9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,9 +7,11 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Extensions; +using OtterGui.Log; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -34,6 +36,41 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; +public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) + : FilterComboCache<(string FullName, (int Group, int Data) Index)>( + () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) +{ + private ImRaii.ColorStyle _border; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + _border.Dispose(); + } + + protected override void DrawFilter(int currentSelected, float width) + { + _border.Dispose(); + base.DrawFilter(currentSelected, width); + } + + public bool Draw(float width) + { + var flags = window.Mod!.AllDataContainers.Count() switch + { + 0 => ImGuiComboFlags.NoArrowButton, + > 8 => ImGuiComboFlags.HeightLargest, + _ => ImGuiComboFlags.None, + }; + return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + => ImUtf8.Selectable(Items[globalIdx].FullName, selected); +} + public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; @@ -49,11 +86,12 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private readonly IDragDropManager _dragDropManager; private readonly IDataManager _gameData; private readonly IFramework _framework; + private readonly OptionSelectCombo _optionSelect; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; - public Mod? Mod { get; private set; } + public Mod? Mod { get; private set; } public bool IsLoading @@ -208,7 +246,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (IsLoading) { var radius = 100 * ImUtf8.GlobalScale; - var thickness = (int) (20 * ImUtf8.GlobalScale); + var thickness = (int)(20 * ImUtf8.GlobalScale); var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius; var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius; ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY)); @@ -216,7 +254,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return; } - using var tabBar = ImRaii.TabBar("##tabs"); + using var tabBar = ImUtf8.TabBar("##tabs"u8); if (!tabBar) return; @@ -231,7 +269,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - using (var tab = ImRaii.TabItem("Item Swap")) + using (var tab = ImUtf8.TabItem("Item Swap"u8)) { if (tab) _itemSwapTab.DrawContent(); @@ -453,10 +491,11 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private bool DrawOptionSelectHeader() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); - var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); - var ret = false; - if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, _editor.Option is DefaultSubMod)) + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); + var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); + var ret = false; + if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, + _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0).Wait(); ret = true; @@ -470,22 +509,11 @@ public partial class ModEditWindow : Window, IDisposable, IUiService } ImGui.SameLine(); - ImGui.SetNextItemWidth(width.X); - style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImUtf8.Combo("##optionSelector"u8, _editor.Option!.GetFullName()); - if (!combo) - return ret; - - foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex()) + if (_optionSelect.Draw(width.X)) { - using var id = ImRaii.PushId(idx); - if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) - { - var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.LoadOption(groupIdx, dataIdx).Wait(); - ret = true; - } + var (groupIdx, dataIdx) = _optionSelect.CurrentSelection.Index; + _editor.LoadOption(groupIdx, dataIdx).Wait(); + ret = true; } return ret; @@ -656,6 +684,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _fileDialog = fileDialog; _framework = framework; _metaDrawers = metaDrawers; + _optionSelect = new OptionSelectCombo(editor, this); _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable)); diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs new file mode 100644 index 00000000..1fa12b6d --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs @@ -0,0 +1,43 @@ +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) + : FilterComboCache<(string FullName, (int Group, int Data) Index)>( + () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) +{ + private ImRaii.ColorStyle _border; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + _border.Dispose(); + } + + protected override void DrawFilter(int currentSelected, float width) + { + _border.Dispose(); + base.DrawFilter(currentSelected, width); + } + + public bool Draw(float width) + { + var flags = window.Mod!.AllDataContainers.Count() switch + { + 0 => ImGuiComboFlags.NoArrowButton, + > 8 => ImGuiComboFlags.HeightLargest, + _ => ImGuiComboFlags.None, + }; + return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + => ImUtf8.Selectable(Items[globalIdx].FullName, selected); +} From e326e3d809b0cfa7a71f9290e6e449f8e05e0f46 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 15:52:47 +0200 Subject: [PATCH 2282/2451] Update shp conditions. --- Penumbra/Collections/Cache/ShpCache.cs | 81 ++++++++--------- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 61 +++++++------ Penumbra/Meta/ShapeManager.cs | 26 +++--- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 89 +++++++++---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 35 -------- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 13 +-- schemas/structs/meta_enums.json | 4 + schemas/structs/meta_shp.json | 7 +- 8 files changed, 137 insertions(+), 179 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index eaf949d9..ee6a4e65 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -8,27 +8,24 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) - => _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); - internal IReadOnlyDictionary State - => _shpData; - - internal IEnumerable<(ShapeString, IReadOnlyDictionary)> ConditionState - => _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary)kvp.Value)); - - public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary? dict) - { - if (_conditionalSet.TryGetValue(condition, out var d)) + internal IReadOnlyDictionary State(ShapeConnectorCondition connector) + => connector switch { - dict = d; - return true; - } + ShapeConnectorCondition.None => _shpData, + ShapeConnectorCondition.Wrists => _wristConnectors, + ShapeConnectorCondition.Waist => _waistConnectors, + ShapeConnectorCondition.Ankles => _ankleConnectors, + _ => [], + }; - dict = null; - return false; - } + public int EnabledCount { get; private set; } + public bool ShouldBeEnabled(ShapeConnectorCondition connector, in ShapeString shape, HumanSlot slot, PrimaryId id) + => State(connector).TryGetValue(shape, out var value) && value.Contains(slot, id); + public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -92,14 +89,18 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) => !_allIds.HasAnySet() && Count is 0; } - private readonly Dictionary _shpData = []; - private readonly Dictionary> _conditionalSet = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary _wristConnectors = []; + private readonly Dictionary _waistConnectors = []; + private readonly Dictionary _ankleConnectors = []; public void Reset() { Clear(); _shpData.Clear(); - _conditionalSet.Clear(); + _wristConnectors.Clear(); + _waistConnectors.Clear(); + _ankleConnectors.Clear(); } protected override void Dispose(bool _) @@ -107,24 +108,16 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { - if (identifier.ShapeCondition.Length > 0) + switch (identifier.ConnectorCondition) { - if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) - { - if (!entry.Value) - return; - - shapes = new Dictionary(); - _conditionalSet.Add(identifier.ShapeCondition, shapes); - } - - Func(shapes); - } - else - { - Func(_shpData); + case ShapeConnectorCondition.None: Func(_shpData); break; + case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break; + case ShapeConnectorCondition.Waist: Func(_waistConnectors); break; + case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break; } + return; + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) @@ -136,22 +129,19 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) dict.Add(identifier.Shape, value); } - value.TrySet(identifier.Slot, identifier.Id, entry); + if (value.TrySet(identifier.Slot, identifier.Id, entry)) + ++EnabledCount; } } protected override void RevertModInternal(ShpIdentifier identifier) { - if (identifier.ShapeCondition.Length > 0) + switch (identifier.ConnectorCondition) { - if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) - return; - - Func(shapes); - } - else - { - Func(_shpData); + case ShapeConnectorCondition.None: Func(_shpData); break; + case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break; + case ShapeConnectorCondition.Waist: Func(_waistConnectors); break; + case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break; } return; @@ -162,7 +152,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + { + --EnabledCount; _shpData.Remove(identifier.Shape); + } } } } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 777be512..b3fdb0cb 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -7,7 +8,16 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) +[JsonConverter(typeof(StringEnumConverter))] +public enum ShapeConnectorCondition : byte +{ + None = 0, + Wrists = 1, + Waist = 2, + Ankles = 3, +} + +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -34,11 +44,11 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return 1; } - var shapeComparison = Shape.CompareTo(other.Shape); - if (shapeComparison is not 0) - return shapeComparison; + var conditionComparison = ConnectorCondition.CompareTo(other.ConnectorCondition); + if (conditionComparison is not 0) + return conditionComparison; - return ShapeCondition.CompareTo(other.ShapeCondition); + return Shape.CompareTo(other.Shape); } @@ -62,9 +72,13 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape sb.Append("All IDs"); } - if (ShapeCondition.Length > 0) - sb.Append(" - ") - .Append(ShapeCondition); + switch (ConnectorCondition) + { + case ShapeConnectorCondition.Wrists: sb.Append(" - Wrist Connector"); break; + case ShapeConnectorCondition.Waist: sb.Append(" - Waist Connector"); break; + case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break; + } + return sb.ToString(); } @@ -87,20 +101,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (!ValidateCustomShapeString(Shape)) return false; - if (ShapeCondition.Length is 0) - return true; - - if (!ValidateCustomShapeString(ShapeCondition)) + if (!Enum.IsDefined(ConnectorCondition)) return false; - return Slot switch + return ConnectorCondition switch { - HumanSlot.Hands when ShapeCondition.IsWrist() => true, - HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, - HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, - HumanSlot.Feet when ShapeCondition.IsAnkle() => true, - HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, - _ => false, + ShapeConnectorCondition.None => true, + ShapeConnectorCondition.Wrists => Slot is HumanSlot.Body or HumanSlot.Hands or HumanSlot.Unknown, + ShapeConnectorCondition.Waist => Slot is HumanSlot.Body or HumanSlot.Legs or HumanSlot.Unknown, + ShapeConnectorCondition.Ankles => Slot is HumanSlot.Legs or HumanSlot.Feet or HumanSlot.Unknown, + _ => false, }; } @@ -145,8 +155,8 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id.HasValue) jObj["Id"] = Id.Value.Id.ToString(); jObj["Shape"] = Shape.ToString(); - if (ShapeCondition.Length > 0) - jObj["ShapeCondition"] = ShapeCondition.ToString(); + if (ConnectorCondition is not ShapeConnectorCondition.None) + jObj["ConnectorCondition"] = ConnectorCondition.ToString(); return jObj; } @@ -156,11 +166,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) return null; - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); - var shapeCondition = jObj["ShapeCondition"]?.ToObject(); - var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; - var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; + var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 57f6f23f..7431b1c2 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,4 +1,3 @@ -using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -80,8 +79,7 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); - if (cache.State.Count > 0 - && cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) + if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) _temporaryValues[i] |= (ushort)(1 << index); } else @@ -102,14 +100,14 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; - CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2); + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); } if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; - CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3); + CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); } } @@ -119,25 +117,23 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; - CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4); + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); } } return; - void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) + void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) { - if (!cache.CheckConditionState(shape, out var dict)) + if (dict.Count is 0) return; - foreach (var (subShape, set) in dict) + foreach (var (shape, set) in dict) { - if (set.Contains(slot1, _ids[idx1])) - if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex)) - _temporaryValues[idx1] |= 1u << subIndex; - if (set.Contains(slot2, _ids[idx2])) - if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex)) - _temporaryValues[idx2] |= 1u << subIndex; + if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1)) + _temporaryValues[idx1] |= 1u << index1; + if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2)) + _temporaryValues[idx2] |= 1u << index2; } } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index fe7e743c..c40726f8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,10 +19,8 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; - private ShapeString _conditionBuffer = ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; private bool _identifierValid; - private bool _conditionValid = true; public override int NumColumns => 7; @@ -32,7 +30,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None); } protected override void DrawNew() @@ -42,7 +40,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); ImGui.TableNextColumn(); - var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid; + var canAdd = !Editor.Contains(Identifier) && _identifierValid; var tt = canAdd ? "Stage this edit."u8 : _identifierValid @@ -69,7 +67,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .OrderBy(kvp => kvp.Key.Shape) .ThenBy(kvp => kvp.Key.Slot) .ThenBy(kvp => kvp.Key.Id) - .ThenBy(kvp => kvp.Key.ShapeCondition) + .ThenBy(kvp => kvp.Key.ConnectorCondition) .Select(kvp => (kvp.Key, kvp.Value)); protected override int Count @@ -87,7 +85,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); ImGui.TableNextColumn(); - changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid); + changes |= DrawConnectorConditionInput(ref identifier); return changes; } @@ -109,9 +107,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); ImGui.TableNextColumn(); - if (identifier.ShapeCondition.Length > 0) + if (identifier.ConnectorCondition is not ShapeConnectorCondition.None) { - ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor); + ImUtf8.TextFramed($"{identifier.ConnectorCondition}", FrameColor); ImUtf8.HoverTooltip("Connector condition for this shape to be activated."); } } @@ -190,27 +188,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } else { - if (_conditionBuffer.Length > 0 - && (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs - || _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body - || _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs)) + identifier = identifier with { - identifier = identifier with + Slot = slot, + ConnectorCondition = Identifier.ConnectorCondition switch { - Slot = slot, - ShapeCondition = ShapeString.Empty, - }; - _conditionValid = false; - } - else - { - identifier = identifier with - { - Slot = slot, - ShapeCondition = _conditionBuffer, - }; - _conditionValid = true; - } + ShapeConnectorCondition.Wrists when slot is HumanSlot.Body or HumanSlot.Hands => ShapeConnectorCondition.Wrists, + ShapeConnectorCondition.Waist when slot is HumanSlot.Body or HumanSlot.Legs => ShapeConnectorCondition.Waist, + ShapeConnectorCondition.Ankles when slot is HumanSlot.Legs or HumanSlot.Feet => ShapeConnectorCondition.Ankles, + _ => ShapeConnectorCondition.None, + }, + }; + ret = true; } } } @@ -241,30 +230,40 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, - float unscaledWidth = 150) + public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150) { - var ret = false; - var ptr = Unsafe.AsPointer(ref buffer); - var span = new Span(ptr, ShapeString.MaxLength + 1); - using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + var (showWrists, showWaist, showAnkles, disable) = identifier.Slot switch { - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8)) + HumanSlot.Unknown => (true, true, true, false), + HumanSlot.Body => (true, true, false, false), + HumanSlot.Legs => (false, true, true, false), + HumanSlot.Hands => (true, false, false, false), + HumanSlot.Feet => (false, false, true, false), + _ => (false, false, false, true), + }; + using var disabled = ImRaii.Disabled(disable); + using (var combo = ImUtf8.Combo("##shpCondition"u8, $"{identifier.ConnectorCondition}")) + { + if (combo) { - buffer.ForceLength((byte)newLength); - valid = ShpIdentifier.ValidateCustomShapeString(buffer) - && (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs - || buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs - || buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands); - if (valid) - identifier = identifier with { ShapeCondition = buffer }; - ret = true; + if (ImUtf8.Selectable("None"u8, identifier.ConnectorCondition is ShapeConnectorCondition.None)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.None }; + + if (showWrists && ImUtf8.Selectable("Wrists"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Wrists)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Wrists }; + + if (showWaist && ImUtf8.Selectable("Waist"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Waist)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Waist }; + + if (showAnkles && ImUtf8.Selectable("Ankles"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Ankles)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Ankles }; } } ImUtf8.HoverTooltip( - "Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8); + "Only activate this shape key if any custom connector shape keys (shpx_[wr|wa|an]_*) are also enabled through matching attributes."u8); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0b9fcde9..e148167b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -36,41 +36,6 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; -public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) - : FilterComboCache<(string FullName, (int Group, int Data) Index)>( - () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) -{ - private ImRaii.ColorStyle _border; - - protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, - ImGuiComboFlags flags) - { - _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); - base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); - _border.Dispose(); - } - - protected override void DrawFilter(int currentSelected, float width) - { - _border.Dispose(); - base.DrawFilter(currentSelected, width); - } - - public bool Draw(float width) - { - var flags = window.Mod!.AllDataContainers.Count() switch - { - 0 => ImGuiComboFlags.NoArrowButton, - > 8 => ImGuiComboFlags.HeightLargest, - _ => ImGuiComboFlags.None, - }; - return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); - } - - protected override bool DrawSelectable(int globalIdx, bool selected) - => ImUtf8.Selectable(Items[globalIdx].FullName, selected); -} - public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 8439587c..109cb5c4 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -8,6 +8,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Meta; +using Penumbra.Meta.Manipulations; namespace Penumbra.UI.Tabs.Debug; @@ -52,17 +53,11 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) + foreach (var condition in Enum.GetValues()) { - ImGui.TableNextColumn(); - DrawShape(shape, set); - } - - foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState) - { - foreach (var (shape, set) in dict) + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) { - ImUtf8.DrawTableColumn(condition.AsSpan); + ImUtf8.DrawTableColumn(condition.ToString()); DrawShape(shape, set); } } diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json index 2fc65a0d..bad184e0 100644 --- a/schemas/structs/meta_enums.json +++ b/schemas/structs/meta_enums.json @@ -29,6 +29,10 @@ "$anchor": "SubRace", "enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ] }, + "ShapeConnectorCondition": { + "$anchor": "ShapeConnectorCondition", + "enum": [ "None", "Wrists", "Waist", "Ankles" ] + }, "U8": { "$anchor": "U8", "oneOf": [ diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 4f868a0a..851842a4 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -17,11 +17,8 @@ "maxLength": 30, "pattern": "^shpx_" }, - "ShapeCondition": { - "type": "string", - "minLength": 8, - "maxLength": 30, - "pattern": "^shpx_(wa|an|wr)_" + "ConnectorCondition": { + "$ref": "meta_enums.json#ShapeConnectorCondition" } }, "required": [ From 6e4e28fa00f73d5c937e8cd12553917af79bf798 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 16:00:09 +0200 Subject: [PATCH 2283/2451] Fix disabling conditional shapes. --- Penumbra/Collections/Cache/ShpCache.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index ee6a4e65..2fe7f933 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -22,10 +22,6 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) public int EnabledCount { get; private set; } - - public bool ShouldBeEnabled(ShapeConnectorCondition connector, in ShapeString shape, HumanSlot slot, PrimaryId id) - => State(connector).TryGetValue(shape, out var value) && value.Contains(slot, id); - public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -148,13 +144,14 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) void Func(Dictionary dict) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) + if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False)) { --EnabledCount; - _shpData.Remove(identifier.Shape); + if (value.IsEmpty) + dict.Remove(identifier.Shape); } } } From e18e4bb0e1fb0dcc450736c100cd3f2867d811aa Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 18 May 2025 14:02:16 +0000 Subject: [PATCH 2284/2451] [CI] Updating repo.json for testing_1.3.6.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 137ba00c..c077c355 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.10", + "TestingAssemblyVersion": "1.3.6.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 47b589540435c8f81826887de33c9827897b7f3d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 22:00:10 +0200 Subject: [PATCH 2285/2451] Fix issue with temp settings again. --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index ce438aac..d3e36ef6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -40,7 +40,6 @@ public sealed class TemporaryModSettings : ModSettings } else { - IsEmpty = true; Enabled = false; Priority = ModPriority.Default; Settings = SettingList.Default(mod); From 68b68d6ce7657d32466ad8a8622f203a43936887 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 May 2025 17:15:29 +0200 Subject: [PATCH 2286/2451] Fix some issues with customization IDs and supported counts. --- Penumbra.GameData | 2 +- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 7 +++++++ Penumbra/Meta/ShapeManager.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 10 +++++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 8e57c2e1..b15c0f07 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3 +Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index b3fdb0cb..3be46d32 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; @@ -98,6 +99,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Slot is HumanSlot.Unknown && Id is not null) return false; + if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue }) + return false; + + if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) + return false; + if (!ValidateCustomShapeString(Shape)) return false; diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 7431b1c2..abd4c3b8 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -70,7 +70,7 @@ public class ShapeManager : IRequiredService, IDisposable if (model is null || model->ModelResourceHandle is null) continue; - _ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set; + _ids[(int)modelIndex] = human.GetModelId(modelIndex); ref var shapes = ref model->ModelResourceHandle->Shapes; foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index c40726f8..6505ecc0 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -151,9 +152,8 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } else { - if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, - ExpandedEqpGmpBase.Count - 1, - false)) + var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1; + if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false)) { identifier = identifier with { Id = setId }; ret = true; @@ -190,6 +190,10 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { identifier = identifier with { + Id = identifier.Id.HasValue + ? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0, + slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1) + : null, Slot = slot, ConnectorCondition = Identifier.ConnectorCondition switch { From fefa3852f7755e42e63957d9e5a244af9dc8001a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 19 May 2025 15:17:54 +0000 Subject: [PATCH 2287/2451] [CI] Updating repo.json for testing_1.3.6.12 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c077c355..0413b876 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.11", + "TestingAssemblyVersion": "1.3.6.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.12/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 861cbc77590e196a3c1f9fb828aebb3432696ec1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 May 2025 17:05:49 +0200 Subject: [PATCH 2288/2451] Add global EQP edits to always hide horns or ears. --- Penumbra/Collections/Cache/GlobalEqpCache.cs | 27 ++++++++++++++++++- .../Manipulations/GlobalEqpManipulation.cs | 12 ++++++--- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 14 +++++++++- schemas/structs/meta_geqp.json | 6 ++--- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index 60e782b5..7d2fbf64 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary, private readonly HashSet _doNotHideRingR = []; private bool _doNotHideVieraHats; private bool _doNotHideHrothgarHats; + private bool _hideAuRaHorns; + private bool _hideVieraEars; + private bool _hideMiqoteEars; public new void Clear() { @@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary, _doNotHideRingR.Clear(); _doNotHideHrothgarHats = false; _doNotHideVieraHats = false; + _hideAuRaHorns = false; + _hideVieraEars = false; + _hideMiqoteEars = false; } public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) @@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary, if (_doNotHideHrothgarHats) original |= EqpEntry.HeadShowHrothgarHat; + if (_hideAuRaHorns) + original &= ~EqpEntry.HeadShowEarAuRa; + + if (_hideVieraEars) + original &= ~EqpEntry.HeadShowEarViera; + + if (_hideMiqoteEars) + original &= ~EqpEntry.HeadShowEarMiqote; + if (_doNotHideEarrings.Contains(armor[5].Set)) - original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura; + original |= EqpEntry.HeadShowEarringsHyurRoe + | EqpEntry.HeadShowEarringsLalaElezen + | EqpEntry.HeadShowEarringsMiqoHrothViera + | EqpEntry.HeadShowEarringsAura; if (_doNotHideNecklace.Contains(armor[6].Set)) original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; @@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary, if (_doNotHideRingL.Contains(armor[9].Set)) original |= EqpEntry.HandShowRingL; + return original; } @@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary, GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true), + GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true), + GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true), _ => false, }; return true; @@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary, GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false), + GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false), + GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false), _ => false, }; return true; diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 1365d9d3..33399a36 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -16,10 +16,10 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier if (!Enum.IsDefined(Type)) return false; - if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats) - return Condition == 0; + if (Type.HasCondition()) + return Condition.Id is not 0; - return Condition != 0; + return Condition.Id is 0; } public JObject AddToJson(JObject jObj) @@ -89,6 +89,12 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); else if (Type is GlobalEqpType.DoNotHideHrothgarHats) changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideHorns) + changedItems.UpdateCountOrSet("All Au Ra Horns", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideVieraEars) + changedItems.UpdateCountOrSet("All Viera Ears", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideMiqoteEars) + changedItems.UpdateCountOrSet("All Miqo'te Ears", () => new IdentifiedName()); } public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index 1a7396f9..29bfe825 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -13,6 +13,9 @@ public enum GlobalEqpType DoNotHideRingL, DoNotHideHrothgarHats, DoNotHideVieraHats, + HideHorns, + HideVieraEars, + HideMiqoteEars, } public static class GlobalEqpExtensions @@ -27,6 +30,9 @@ public static class GlobalEqpExtensions GlobalEqpType.DoNotHideRingL => true, GlobalEqpType.DoNotHideHrothgarHats => false, GlobalEqpType.DoNotHideVieraHats => false, + GlobalEqpType.HideHorns => false, + GlobalEqpType.HideVieraEars => false, + GlobalEqpType.HideMiqoteEars => false, _ => false, }; @@ -41,6 +47,9 @@ public static class GlobalEqpExtensions GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8, GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8, GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8, + GlobalEqpType.HideHorns => "Always Hide Horns (Au Ra)"u8, + GlobalEqpType.HideVieraEars => "Always Hide Ears (Viera)"u8, + GlobalEqpType.HideMiqoteEars => "Always Hide Ears (Miqo'te)"u8, _ => "\0"u8, }; @@ -60,6 +69,9 @@ public static class GlobalEqpExtensions "Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8, GlobalEqpType.DoNotHideVieraHats => "Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8, - _ => "\0"u8, + GlobalEqpType.HideHorns => "Forces the game to hide Au Ra horns regardless of headwear."u8, + GlobalEqpType.HideVieraEars => "Forces the game to hide Viera ears regardless of headwear."u8, + GlobalEqpType.HideMiqoteEars => "Forces the game to hide Miqo'te ears regardless of headwear."u8, + _ => "\0"u8, }; } diff --git a/schemas/structs/meta_geqp.json b/schemas/structs/meta_geqp.json index 3d4908f9..e38fbb86 100644 --- a/schemas/structs/meta_geqp.json +++ b/schemas/structs/meta_geqp.json @@ -6,7 +6,7 @@ "$ref": "meta_enums.json#U16" }, "Type": { - "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] } }, "required": [ "Type" ], @@ -14,7 +14,7 @@ { "properties": { "Type": { - "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] }, "Condition": { "const": 0 @@ -24,7 +24,7 @@ { "properties": { "Type": { - "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] } } }, From 3412786282f1aa52fab12c1328faa3bcfec51408 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 12:03:04 +0200 Subject: [PATCH 2289/2451] Optimize used memory by metadictionarys a bit. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 445 +++++++++++------- 1 file changed, 263 insertions(+), 182 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index a7225067..51ca09ab 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -11,129 +11,164 @@ namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] public class MetaDictionary { - private readonly Dictionary _imc = []; - private readonly Dictionary _eqp = []; - private readonly Dictionary _eqdp = []; - private readonly Dictionary _est = []; - private readonly Dictionary _rsp = []; - private readonly Dictionary _gmp = []; - private readonly Dictionary _atch = []; - private readonly Dictionary _shp = []; - private readonly HashSet _globalEqp = []; + private class Wrapper : HashSet + { + public readonly Dictionary Imc = []; + public readonly Dictionary Eqp = []; + public readonly Dictionary Eqdp = []; + public readonly Dictionary Est = []; + public readonly Dictionary Rsp = []; + public readonly Dictionary Gmp = []; + public readonly Dictionary Atch = []; + public readonly Dictionary Shp = []; + + public Wrapper() + { } + + public Wrapper(MetaCache cache) + { + Imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + Eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + Est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + foreach (var geqp in cache.GlobalEqp.Keys) + Add(geqp); + } + } + + private Wrapper? _data; public IReadOnlyDictionary Imc - => _imc; + => _data?.Imc ?? []; public IReadOnlyDictionary Eqp - => _eqp; + => _data?.Eqp ?? []; public IReadOnlyDictionary Eqdp - => _eqdp; + => _data?.Eqdp ?? []; public IReadOnlyDictionary Est - => _est; + => _data?.Est ?? []; public IReadOnlyDictionary Gmp - => _gmp; + => _data?.Gmp ?? []; public IReadOnlyDictionary Rsp - => _rsp; + => _data?.Rsp ?? []; public IReadOnlyDictionary Atch - => _atch; + => _data?.Atch ?? []; public IReadOnlyDictionary Shp - => _shp; + => _data?.Shp ?? []; public IReadOnlySet GlobalEqp - => _globalEqp; + => _data ?? []; public int Count { get; private set; } public int GetCount(MetaManipulationType type) - => type switch - { - MetaManipulationType.Imc => _imc.Count, - MetaManipulationType.Eqdp => _eqdp.Count, - MetaManipulationType.Eqp => _eqp.Count, - MetaManipulationType.Est => _est.Count, - MetaManipulationType.Gmp => _gmp.Count, - MetaManipulationType.Rsp => _rsp.Count, - MetaManipulationType.Atch => _atch.Count, - MetaManipulationType.Shp => _shp.Count, - MetaManipulationType.GlobalEqp => _globalEqp.Count, - _ => 0, - }; + => _data is null + ? 0 + : type switch + { + MetaManipulationType.Imc => _data.Imc.Count, + MetaManipulationType.Eqdp => _data.Eqdp.Count, + MetaManipulationType.Eqp => _data.Eqp.Count, + MetaManipulationType.Est => _data.Est.Count, + MetaManipulationType.Gmp => _data.Gmp.Count, + MetaManipulationType.Rsp => _data.Rsp.Count, + MetaManipulationType.Atch => _data.Atch.Count, + MetaManipulationType.Shp => _data.Shp.Count, + MetaManipulationType.GlobalEqp => _data.Count, + _ => 0, + }; public bool Contains(IMetaIdentifier identifier) - => identifier switch - { - EqdpIdentifier i => _eqdp.ContainsKey(i), - EqpIdentifier i => _eqp.ContainsKey(i), - EstIdentifier i => _est.ContainsKey(i), - GlobalEqpManipulation i => _globalEqp.Contains(i), - GmpIdentifier i => _gmp.ContainsKey(i), - ImcIdentifier i => _imc.ContainsKey(i), - AtchIdentifier i => _atch.ContainsKey(i), - ShpIdentifier i => _shp.ContainsKey(i), - RspIdentifier i => _rsp.ContainsKey(i), - _ => false, - }; + => _data is not null + && identifier switch + { + EqdpIdentifier i => _data.Eqdp.ContainsKey(i), + EqpIdentifier i => _data.Eqp.ContainsKey(i), + EstIdentifier i => _data.Est.ContainsKey(i), + GlobalEqpManipulation i => _data.Contains(i), + GmpIdentifier i => _data.Gmp.ContainsKey(i), + ImcIdentifier i => _data.Imc.ContainsKey(i), + AtchIdentifier i => _data.Atch.ContainsKey(i), + ShpIdentifier i => _data.Shp.ContainsKey(i), + RspIdentifier i => _data.Rsp.ContainsKey(i), + _ => false, + }; public void Clear() { + _data = null; Count = 0; - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _est.Clear(); - _rsp.Clear(); - _gmp.Clear(); - _atch.Clear(); - _shp.Clear(); - _globalEqp.Clear(); } public void ClearForDefault() { - Count = _globalEqp.Count; - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _est.Clear(); - _rsp.Clear(); - _gmp.Clear(); - _atch.Clear(); + if (_data is null) + return; + + if (_data.Count is 0 && Shp.Count is 0) + { + _data = null; + Count = 0; + } + + Count = GlobalEqp.Count + Shp.Count; + _data!.Imc.Clear(); + _data!.Eqp.Clear(); + _data!.Eqdp.Clear(); + _data!.Est.Clear(); + _data!.Rsp.Clear(); + _data!.Gmp.Clear(); + _data!.Atch.Clear(); } public bool Equals(MetaDictionary other) - => Count == other.Count - && _imc.SetEquals(other._imc) - && _eqp.SetEquals(other._eqp) - && _eqdp.SetEquals(other._eqdp) - && _est.SetEquals(other._est) - && _rsp.SetEquals(other._rsp) - && _gmp.SetEquals(other._gmp) - && _atch.SetEquals(other._atch) - && _shp.SetEquals(other._shp) - && _globalEqp.SetEquals(other._globalEqp); + { + if (Count != other.Count) + return false; + + if (_data is null) + return true; + + return _data.Imc.SetEquals(other._data!.Imc) + && _data.Eqp.SetEquals(other._data!.Eqp) + && _data.Eqdp.SetEquals(other._data!.Eqdp) + && _data.Est.SetEquals(other._data!.Est) + && _data.Rsp.SetEquals(other._data!.Rsp) + && _data.Gmp.SetEquals(other._data!.Gmp) + && _data.Atch.SetEquals(other._data!.Atch) + && _data.Shp.SetEquals(other._data!.Shp) + && _data.SetEquals(other._data!); + } public IEnumerable Identifiers - => _imc.Keys.Cast() - .Concat(_eqdp.Keys.Cast()) - .Concat(_eqp.Keys.Cast()) - .Concat(_est.Keys.Cast()) - .Concat(_gmp.Keys.Cast()) - .Concat(_rsp.Keys.Cast()) - .Concat(_atch.Keys.Cast()) - .Concat(_shp.Keys.Cast()) - .Concat(_globalEqp.Cast()); + => _data is null + ? [] + : _data.Imc.Keys.Cast() + .Concat(_data!.Eqdp.Keys.Cast()) + .Concat(_data!.Eqp.Keys.Cast()) + .Concat(_data!.Est.Keys.Cast()) + .Concat(_data!.Gmp.Keys.Cast()) + .Concat(_data!.Rsp.Keys.Cast()) + .Concat(_data!.Atch.Keys.Cast()) + .Concat(_data!.Shp.Keys.Cast()) + .Concat(_data!.Cast()); #region TryAdd public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { - if (!_imc.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Imc.TryAdd(identifier, entry)) return false; ++Count; @@ -142,7 +177,8 @@ public class MetaDictionary public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) { - if (!_eqp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Eqp.TryAdd(identifier, entry)) return false; ++Count; @@ -154,7 +190,8 @@ public class MetaDictionary public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) { - if (!_eqdp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Eqdp.TryAdd(identifier, entry)) return false; ++Count; @@ -166,7 +203,8 @@ public class MetaDictionary public bool TryAdd(EstIdentifier identifier, EstEntry entry) { - if (!_est.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Est.TryAdd(identifier, entry)) return false; ++Count; @@ -175,7 +213,8 @@ public class MetaDictionary public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) { - if (!_gmp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Gmp.TryAdd(identifier, entry)) return false; ++Count; @@ -184,7 +223,8 @@ public class MetaDictionary public bool TryAdd(RspIdentifier identifier, RspEntry entry) { - if (!_rsp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Rsp.TryAdd(identifier, entry)) return false; ++Count; @@ -193,7 +233,8 @@ public class MetaDictionary public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) { - if (!_atch.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Atch.TryAdd(identifier, entry)) return false; ++Count; @@ -202,7 +243,8 @@ public class MetaDictionary public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry) { - if (!_shp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Shp.TryAdd(identifier, entry)) return false; ++Count; @@ -211,7 +253,8 @@ public class MetaDictionary public bool TryAdd(GlobalEqpManipulation identifier) { - if (!_globalEqp.Add(identifier)) + _data ??= []; + if (!_data.Add(identifier)) return false; ++Count; @@ -224,19 +267,19 @@ public class MetaDictionary public bool Update(ImcIdentifier identifier, ImcEntry entry) { - if (!_imc.ContainsKey(identifier)) + if (_data is null || !_data.Imc.ContainsKey(identifier)) return false; - _imc[identifier] = entry; + _data.Imc[identifier] = entry; return true; } public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) { - if (!_eqp.ContainsKey(identifier)) + if (_data is null || !_data.Eqp.ContainsKey(identifier)) return false; - _eqp[identifier] = entry; + _data.Eqp[identifier] = entry; return true; } @@ -245,10 +288,10 @@ public class MetaDictionary public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) { - if (!_eqdp.ContainsKey(identifier)) + if (_data is null || !_data.Eqdp.ContainsKey(identifier)) return false; - _eqdp[identifier] = entry; + _data.Eqdp[identifier] = entry; return true; } @@ -257,46 +300,46 @@ public class MetaDictionary public bool Update(EstIdentifier identifier, EstEntry entry) { - if (!_est.ContainsKey(identifier)) + if (_data is null || !_data.Est.ContainsKey(identifier)) return false; - _est[identifier] = entry; + _data.Est[identifier] = entry; return true; } public bool Update(GmpIdentifier identifier, GmpEntry entry) { - if (!_gmp.ContainsKey(identifier)) + if (_data is null || !_data.Gmp.ContainsKey(identifier)) return false; - _gmp[identifier] = entry; + _data.Gmp[identifier] = entry; return true; } public bool Update(RspIdentifier identifier, RspEntry entry) { - if (!_rsp.ContainsKey(identifier)) + if (_data is null || !_data.Rsp.ContainsKey(identifier)) return false; - _rsp[identifier] = entry; + _data.Rsp[identifier] = entry; return true; } public bool Update(AtchIdentifier identifier, in AtchEntry entry) { - if (!_atch.ContainsKey(identifier)) + if (_data is null || !_data.Atch.ContainsKey(identifier)) return false; - _atch[identifier] = entry; + _data.Atch[identifier] = entry; return true; } public bool Update(ShpIdentifier identifier, in ShpEntry entry) { - if (!_shp.ContainsKey(identifier)) + if (_data is null || !_data.Shp.ContainsKey(identifier)) return false; - _shp[identifier] = entry; + _data.Shp[identifier] = entry; return true; } @@ -305,48 +348,59 @@ public class MetaDictionary #region TryGetValue public bool TryGetValue(EstIdentifier identifier, out EstEntry value) - => _est.TryGetValue(identifier, out value); + => _data?.Est.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) - => _eqp.TryGetValue(identifier, out value); + => _data?.Eqp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) - => _eqdp.TryGetValue(identifier, out value); + => _data?.Eqdp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) - => _gmp.TryGetValue(identifier, out value); + => _data?.Gmp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(RspIdentifier identifier, out RspEntry value) - => _rsp.TryGetValue(identifier, out value); + => _data?.Rsp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) - => _imc.TryGetValue(identifier, out value); + => _data?.Imc.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) - => _atch.TryGetValue(identifier, out value); + => _data?.Atch.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) - => _shp.TryGetValue(identifier, out value); + => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetDefault(out T? value) + { + value = default; + return false; + } #endregion public bool Remove(IMetaIdentifier identifier) { + if (_data is null) + return false; + var ret = identifier switch { - EqdpIdentifier i => _eqdp.Remove(i), - EqpIdentifier i => _eqp.Remove(i), - EstIdentifier i => _est.Remove(i), - GlobalEqpManipulation i => _globalEqp.Remove(i), - GmpIdentifier i => _gmp.Remove(i), - ImcIdentifier i => _imc.Remove(i), - RspIdentifier i => _rsp.Remove(i), - AtchIdentifier i => _atch.Remove(i), - ShpIdentifier i => _shp.Remove(i), + EqdpIdentifier i => _data.Eqdp.Remove(i), + EqpIdentifier i => _data.Eqp.Remove(i), + EstIdentifier i => _data.Est.Remove(i), + GlobalEqpManipulation i => _data.Remove(i), + GmpIdentifier i => _data.Gmp.Remove(i), + ImcIdentifier i => _data.Imc.Remove(i), + RspIdentifier i => _data.Rsp.Remove(i), + AtchIdentifier i => _data.Atch.Remove(i), + ShpIdentifier i => _data.Shp.Remove(i), _ => false, }; - if (ret) - --Count; + if (ret && --Count is 0) + _data = null; + return ret; } @@ -354,86 +408,97 @@ public class MetaDictionary public void UnionWith(MetaDictionary manips) { - foreach (var (identifier, entry) in manips._imc) + if (manips.Count is 0) + return; + + _data ??= []; + foreach (var (identifier, entry) in manips._data!.Imc) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._eqp) + foreach (var (identifier, entry) in manips._data!.Eqp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._eqdp) + foreach (var (identifier, entry) in manips._data!.Eqdp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._gmp) + foreach (var (identifier, entry) in manips._data!.Gmp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._rsp) + foreach (var (identifier, entry) in manips._data!.Rsp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._est) + foreach (var (identifier, entry) in manips._data!.Est) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._atch) + foreach (var (identifier, entry) in manips._data!.Atch) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._shp) + foreach (var (identifier, entry) in manips._data!.Shp) TryAdd(identifier, entry); - foreach (var identifier in manips._globalEqp) + foreach (var identifier in manips._data!) TryAdd(identifier); } /// Try to merge all manipulations from manips into this, and return the first failure, if any. public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) { - foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + if (manips.Count is 0) + { + failedIdentifier = null; + return true; + } + + _data ??= []; + foreach (var (identifier, _) in manips._data!.Imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) + foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; return false; @@ -445,30 +510,50 @@ public class MetaDictionary public void SetTo(MetaDictionary other) { - _imc.SetTo(other._imc); - _eqp.SetTo(other._eqp); - _eqdp.SetTo(other._eqdp); - _est.SetTo(other._est); - _rsp.SetTo(other._rsp); - _gmp.SetTo(other._gmp); - _atch.SetTo(other._atch); - _shp.SetTo(other._shp); - _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; + if (other.Count is 0) + { + _data = null; + Count = 0; + return; + } + + _data ??= []; + _data!.Imc.SetTo(other._data!.Imc); + _data!.Eqp.SetTo(other._data!.Eqp); + _data!.Eqdp.SetTo(other._data!.Eqdp); + _data!.Est.SetTo(other._data!.Est); + _data!.Rsp.SetTo(other._data!.Rsp); + _data!.Gmp.SetTo(other._data!.Gmp); + _data!.Atch.SetTo(other._data!.Atch); + _data!.Shp.SetTo(other._data!.Shp); + _data!.SetTo(other._data!); + Count = other.Count; } public void UpdateTo(MetaDictionary other) { - _imc.UpdateTo(other._imc); - _eqp.UpdateTo(other._eqp); - _eqdp.UpdateTo(other._eqdp); - _est.UpdateTo(other._est); - _rsp.UpdateTo(other._rsp); - _gmp.UpdateTo(other._gmp); - _atch.UpdateTo(other._atch); - _shp.UpdateTo(other._shp); - _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; + if (other.Count is 0) + return; + + _data ??= []; + _data!.Imc.UpdateTo(other._data!.Imc); + _data!.Eqp.UpdateTo(other._data!.Eqp); + _data!.Eqdp.UpdateTo(other._data!.Eqdp); + _data!.Est.UpdateTo(other._data!.Est); + _data!.Rsp.UpdateTo(other._data!.Rsp); + _data!.Gmp.UpdateTo(other._data!.Gmp); + _data!.Atch.UpdateTo(other._data!.Atch); + _data!.Shp.UpdateTo(other._data!.Shp); + _data!.UnionWith(other._data!); + Count = _data!.Imc.Count + + _data!.Eqp.Count + + _data!.Eqdp.Count + + _data!.Est.Count + + _data!.Rsp.Count + + _data!.Gmp.Count + + _data!.Atch.Count + + _data!.Shp.Count + + _data!.Count; } #endregion @@ -635,15 +720,19 @@ public class MetaDictionary } var array = new JArray(); - SerializeTo(array, value._imc); - SerializeTo(array, value._eqp); - SerializeTo(array, value._eqdp); - SerializeTo(array, value._est); - SerializeTo(array, value._rsp); - SerializeTo(array, value._gmp); - SerializeTo(array, value._atch); - SerializeTo(array, value._shp); - SerializeTo(array, value._globalEqp); + if (value._data is not null) + { + SerializeTo(array, value._data!.Imc); + SerializeTo(array, value._data!.Eqp); + SerializeTo(array, value._data!.Eqdp); + SerializeTo(array, value._data!.Est); + SerializeTo(array, value._data!.Rsp); + SerializeTo(array, value._data!.Gmp); + SerializeTo(array, value._data!.Atch); + SerializeTo(array, value._data!.Shp); + SerializeTo(array, value._data!); + } + array.WriteTo(writer); } @@ -771,18 +860,10 @@ public class MetaDictionary public MetaDictionary(MetaCache? cache) { - if (cache == null) + if (cache is null) return; - _imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); - _eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); - _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); - Count = cache.Count; + _data = new Wrapper(cache); + Count = cache.Count; } } From d7dee39fab37fcedf047b4fb2829f034526bf63d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 15:44:26 +0200 Subject: [PATCH 2290/2451] Add attribute handling, rework atr and shape caches. --- Penumbra.GameData | 2 +- Penumbra.sln | 1 + Penumbra/Collections/Cache/AtrCache.cs | 56 ++++ Penumbra/Collections/Cache/CollectionCache.cs | 2 + Penumbra/Collections/Cache/MetaCache.cs | 9 +- .../Cache/ShapeAttributeHashSet.cs | 123 ++++++++ Penumbra/Collections/Cache/ShpCache.cs | 85 +----- .../Hooks/PostProcessing/AttributeHook.cs | 4 +- Penumbra/Meta/Manipulations/AtrIdentifier.cs | 145 +++++++++ .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 70 ++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 73 ++--- Penumbra/Meta/ShapeAttributeManager.cs | 153 ++++++++++ ...ShapeString.cs => ShapeAttributeString.cs} | 95 +++++- Penumbra/Meta/ShapeManager.cs | 140 --------- .../UI/AdvancedWindow/Meta/AtrMetaDrawer.cs | 274 ++++++++++++++++++ .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 5 +- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 74 ++++- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 132 ++++++++- schemas/structs/manipulation.json | 12 +- schemas/structs/meta_atr.json | 27 ++ schemas/structs/meta_shp.json | 3 + 23 files changed, 1187 insertions(+), 300 deletions(-) create mode 100644 Penumbra/Collections/Cache/AtrCache.cs create mode 100644 Penumbra/Collections/Cache/ShapeAttributeHashSet.cs create mode 100644 Penumbra/Meta/Manipulations/AtrIdentifier.cs create mode 100644 Penumbra/Meta/ShapeAttributeManager.cs rename Penumbra/Meta/{ShapeString.cs => ShapeAttributeString.cs} (55%) delete mode 100644 Penumbra/Meta/ShapeManager.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs create mode 100644 schemas/structs/meta_atr.json diff --git a/Penumbra.GameData b/Penumbra.GameData index b15c0f07..bb3b462b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b +Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1 diff --git a/Penumbra.sln b/Penumbra.sln index 642876ef..fbcd6080 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F schemas\structs\group_single.json = schemas\structs\group_single.json schemas\structs\manipulation.json = schemas\structs\manipulation.json schemas\structs\meta_atch.json = schemas\structs\meta_atch.json + schemas\structs\meta_atr.json = schemas\structs\meta_atr.json schemas\structs\meta_enums.json = schemas\structs\meta_enums.json schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs new file mode 100644 index 00000000..757ddaa2 --- /dev/null +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -0,0 +1,56 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace); + + public int DisabledCount { get; private set; } + + internal IReadOnlyDictionary Data + => _atrData; + + private readonly Dictionary _atrData = []; + + public void Reset() + { + Clear(); + _atrData.Clear(); + } + + protected override void Dispose(bool _) + => Clear(); + + protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + { + if (entry.Value) + return; + + value = []; + _atrData.Add(identifier.Attribute, value); + } + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value)) + ++DisabledCount; + } + + protected override void RevertModInternal(AtrIdentifier identifier) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + { + --DisabledCount; + if (value.IsEmpty) + _atrData.Remove(identifier.Attribute); + } + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index c48a487c..8294624b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable AddManipulation(mod, identifier, entry); foreach (var (identifier, entry) in files.Manipulations.Shp) AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Atr) + AddManipulation(mod, identifier, entry); foreach (var identifier in files.Manipulations.GlobalEqp) AddManipulation(mod, identifier, null!); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 790dd3af..011cdd23 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly ImcCache Imc = new(manager, collection); public readonly AtchCache Atch = new(manager, collection); public readonly ShpCache Shp = new(manager, collection); + public readonly AtrCache Atr = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Reset(); Atch.Reset(); Shp.Reset(); + Atr.Reset(); GlobalEqp.Clear(); } @@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Dispose(); Atch.Dispose(); Shp.Dispose(); + Atr.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod), + AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod), ShpIdentifier i => Shp.RevertMod(i, out mod), + AtrIdentifier i => Atr.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e), + AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs new file mode 100644 index 00000000..f1fc7127 --- /dev/null +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -0,0 +1,123 @@ +using System.Collections.Frozen; +using OtterGui.Extensions; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; + +namespace Penumbra.Collections.Cache; + +public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong> +{ + public static readonly IReadOnlyList GenderRaceValues = + [ + GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, + GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, + GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, + GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public static readonly FrozenDictionary GenderRaceIndices = + GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); + + private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool this[HumanSlot slot] + => slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count]; + + public bool this[GenderRace genderRace] + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]; + + public bool this[HumanSlot slot, GenderRace genderRace] + { + get + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]) + return true; + + return _allIds[(int)slot * GenderRaceIndices.Count + index]; + } + set + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return; + + var genderRaceCount = GenderRaceValues.Count; + if (slot is HumanSlot.Unknown) + _allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value; + else + _allIds[(int)slot * genderRaceCount + index] = value; + } + } + + public bool All + => _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count]; + + public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && TryGetValue((slot, id), out var flags) + && (flags & (1ul << index)) is not 0; + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (!id.HasValue) + { + var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot; + var old = _allIds[slotIndex * GenderRaceIndices.Count + index]; + _allIds[slotIndex * GenderRaceIndices.Count + index] = value; + return old != value; + } + + if (value) + { + if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags | (1ul << index); + if (newFlags == flags) + return false; + + this[(slot, id.Value)] = newFlags; + return true; + } + + this[(slot, id.Value)] = 1ul << index; + return true; + } + else if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags & ~(1ul << index); + if (newFlags == flags) + return false; + + if (newFlags is 0) + { + Remove((slot, id.Value)); + return true; + } + + this[(slot, id.Value)] = newFlags; + return true; + } + + return false; + } + + public new void Clear() + { + base.Clear(); + _allIds.SetAll(false); + } + + public bool IsEmpty + => !_allIds.HasAnySet() && Count is 0; +} diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2fe7f933..22547d25 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -7,10 +7,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) - => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace); - internal IReadOnlyDictionary State(ShapeConnectorCondition connector) + internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch { ShapeConnectorCondition.None => _shpData, @@ -22,73 +22,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) public int EnabledCount { get; private set; } - public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> - { - private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); - - public bool All - { - get => _allIds[^1]; - set => _allIds[^1] = value; - } - - public bool this[HumanSlot slot] - { - get - { - if (slot is HumanSlot.Unknown) - return All; - - return _allIds[(int)slot]; - } - set - { - if (slot is HumanSlot.Unknown) - _allIds[^1] = value; - else - _allIds[(int)slot] = value; - } - } - - public bool Contains(HumanSlot slot, PrimaryId id) - => All || this[slot] || Contains((slot, id)); - - public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value) - { - if (slot is HumanSlot.Unknown) - { - var old = All; - All = value.Value; - return old != value.Value; - } - - if (!id.HasValue) - { - var old = this[slot]; - this[slot] = value.Value; - return old != value.Value; - } - - if (value.Value) - return Add((slot, id.Value)); - - return Remove((slot, id.Value)); - } - - public new void Clear() - { - base.Clear(); - _allIds.SetAll(false); - } - - public bool IsEmpty - => !_allIds.HasAnySet() && Count is 0; - } - - private readonly Dictionary _shpData = []; - private readonly Dictionary _wristConnectors = []; - private readonly Dictionary _waistConnectors = []; - private readonly Dictionary _ankleConnectors = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary _wristConnectors = []; + private readonly Dictionary _waistConnectors = []; + private readonly Dictionary _ankleConnectors = []; public void Reset() { @@ -114,7 +51,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) { @@ -125,7 +62,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, entry)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) ++EnabledCount; } } @@ -142,12 +79,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) { --EnabledCount; if (value.IsEmpty) diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs index cad049ad..00e5851f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs @@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper - ShapeManager = 0, + /// + ShapeAttributeManager = 0, } private readonly CollectionResolver _resolver; diff --git a/Penumbra/Meta/Manipulations/AtrIdentifier.cs b/Penumbra/Meta/Manipulations/AtrIdentifier.cs new file mode 100644 index 00000000..ca65f6aa --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtrIdentifier.cs @@ -0,0 +1,145 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition) + : IComparable, IMetaIdentifier +{ + public int CompareTo(AtrIdentifier other) + { + var slotComparison = Slot.CompareTo(other.Slot); + if (slotComparison is not 0) + return slotComparison; + + if (Id.HasValue) + { + if (other.Id.HasValue) + { + var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id); + if (idComparison is not 0) + return idComparison; + } + else + { + return -1; + } + } + else if (other.Id.HasValue) + { + return 1; + } + + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + + return Attribute.CompareTo(other.Attribute); + } + + + public override string ToString() + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Attribute); + if (Slot is HumanSlot.Unknown) + { + sb.Append(" - All Slots & IDs"); + } + else + { + sb.Append(" - ") + .Append(Slot.ToName()) + .Append(" - "); + if (Id.HasValue) + sb.Append(Id.Value.Id); + else + sb.Append("All IDs"); + } + + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + + return sb.ToString(); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing for now since it depends entirely on the shape key. + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) + return false; + + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (Slot is HumanSlot.Unknown && Id is not null) + return false; + + if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue }) + return false; + + if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) + return false; + + return Attribute.ValidateCustomAttributeString(); + } + + public JObject AddToJson(JObject jObj) + { + if (Slot is not HumanSlot.Unknown) + jObj["Slot"] = Slot.ToString(); + if (Id.HasValue) + jObj["Id"] = Id.Value.Id.ToString(); + jObj["Attribute"] = Attribute.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; + return jObj; + } + + public static AtrIdentifier? FromJson(JObject jObj) + { + var attribute = jObj["Attribute"]?.ToObject(); + if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString)) + return null; + + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition); + return identifier.Validate() ? identifier : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Atr; +} + +[JsonConverter(typeof(Converter))] +public readonly record struct AtrEntry(bool Value) +{ + public static readonly AtrEntry True = new(true); + public static readonly AtrEntry False = new(false); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 13feba51..922825c3 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -16,6 +16,7 @@ public enum MetaManipulationType : byte GlobalEqp = 7, Atch = 8, Shp = 9, + Atr = 10, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 51ca09ab..23eaec76 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -21,6 +21,7 @@ public class MetaDictionary public readonly Dictionary Gmp = []; public readonly Dictionary Atch = []; public readonly Dictionary Shp = []; + public readonly Dictionary Atr = []; public Wrapper() { } @@ -35,6 +36,7 @@ public class MetaDictionary Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); foreach (var geqp in cache.GlobalEqp.Keys) Add(geqp); } @@ -66,6 +68,9 @@ public class MetaDictionary public IReadOnlyDictionary Shp => _data?.Shp ?? []; + public IReadOnlyDictionary Atr + => _data?.Atr ?? []; + public IReadOnlySet GlobalEqp => _data ?? []; @@ -84,6 +89,7 @@ public class MetaDictionary MetaManipulationType.Rsp => _data.Rsp.Count, MetaManipulationType.Atch => _data.Atch.Count, MetaManipulationType.Shp => _data.Shp.Count, + MetaManipulationType.Atr => _data.Atr.Count, MetaManipulationType.GlobalEqp => _data.Count, _ => 0, }; @@ -100,6 +106,7 @@ public class MetaDictionary ImcIdentifier i => _data.Imc.ContainsKey(i), AtchIdentifier i => _data.Atch.ContainsKey(i), ShpIdentifier i => _data.Shp.ContainsKey(i), + AtrIdentifier i => _data.Atr.ContainsKey(i), RspIdentifier i => _data.Rsp.ContainsKey(i), _ => false, }; @@ -115,13 +122,13 @@ public class MetaDictionary if (_data is null) return; - if (_data.Count is 0 && Shp.Count is 0) + if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0) { _data = null; Count = 0; } - Count = GlobalEqp.Count + Shp.Count; + Count = GlobalEqp.Count + Shp.Count + Atr.Count; _data!.Imc.Clear(); _data!.Eqp.Clear(); _data!.Eqdp.Clear(); @@ -147,6 +154,7 @@ public class MetaDictionary && _data.Gmp.SetEquals(other._data!.Gmp) && _data.Atch.SetEquals(other._data!.Atch) && _data.Shp.SetEquals(other._data!.Shp) + && _data.Atr.SetEquals(other._data!.Atr) && _data.SetEquals(other._data!); } @@ -161,6 +169,7 @@ public class MetaDictionary .Concat(_data!.Rsp.Keys.Cast()) .Concat(_data!.Atch.Keys.Cast()) .Concat(_data!.Shp.Keys.Cast()) + .Concat(_data!.Atr.Keys.Cast()) .Concat(_data!.Cast()); #region TryAdd @@ -251,6 +260,16 @@ public class MetaDictionary return true; } + public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry) + { + _data ??= []; + if (!_data!.Atr.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { _data ??= []; @@ -343,6 +362,15 @@ public class MetaDictionary return true; } + public bool Update(AtrIdentifier identifier, in AtrEntry entry) + { + if (_data is null || !_data.Atr.ContainsKey(identifier)) + return false; + + _data.Atr[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -371,6 +399,9 @@ public class MetaDictionary public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value); + public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value) + => _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static bool SetDefault(out T? value) { @@ -396,6 +427,7 @@ public class MetaDictionary RspIdentifier i => _data.Rsp.Remove(i), AtchIdentifier i => _data.Atch.Remove(i), ShpIdentifier i => _data.Shp.Remove(i), + AtrIdentifier i => _data.Atr.Remove(i), _ => false, }; if (ret && --Count is 0) @@ -436,6 +468,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._data!.Shp) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._data!.Atr) + TryAdd(identifier, entry); + foreach (var identifier in manips._data!) TryAdd(identifier); } @@ -498,6 +533,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -526,6 +567,7 @@ public class MetaDictionary _data!.Gmp.SetTo(other._data!.Gmp); _data!.Atch.SetTo(other._data!.Atch); _data!.Shp.SetTo(other._data!.Shp); + _data!.Atr.SetTo(other._data!.Atr); _data!.SetTo(other._data!); Count = other.Count; } @@ -544,6 +586,7 @@ public class MetaDictionary _data!.Gmp.UpdateTo(other._data!.Gmp); _data!.Atch.UpdateTo(other._data!.Atch); _data!.Shp.UpdateTo(other._data!.Shp); + _data!.Atr.UpdateTo(other._data!.Atr); _data!.UnionWith(other._data!); Count = _data!.Imc.Count + _data!.Eqp.Count @@ -651,6 +694,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atr.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -682,6 +735,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -730,6 +785,7 @@ public class MetaDictionary SerializeTo(array, value._data!.Gmp); SerializeTo(array, value._data!.Atch); SerializeTo(array, value._data!.Shp); + SerializeTo(array, value._data!.Atr); SerializeTo(array, value._data!); } @@ -839,6 +895,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); break; } + case MetaManipulationType.Atr: + { + var identifier = AtrIdentifier.FromJson(manip); + var entry = new AtrEntry(manip["Entry"]?.Value() ?? true); + if (identifier.HasValue) + dict.TryAdd(identifier.Value, entry); + else + Penumbra.Log.Warning("Invalid ATR Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 3be46d32..0a5b71b7 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -18,7 +19,12 @@ public enum ShapeConnectorCondition : byte Ankles = 3, } -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition) +public readonly record struct ShpIdentifier( + HumanSlot Slot, + PrimaryId? Id, + ShapeAttributeString Shape, + ShapeConnectorCondition ConnectorCondition, + GenderRace GenderRaceCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -49,6 +55,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (conditionComparison is not 0) return conditionComparison; + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + return Shape.CompareTo(other.Shape); } @@ -80,6 +90,9 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break; } + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + return sb.ToString(); } @@ -96,6 +109,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) return false; + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (!Enum.IsDefined(ConnectorCondition)) + return false; + if (Slot is HumanSlot.Unknown && Id is not null) return false; @@ -105,10 +124,7 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) return false; - if (!ValidateCustomShapeString(Shape)) - return false; - - if (!Enum.IsDefined(ConnectorCondition)) + if (!Shape.ValidateCustomShapeString()) return false; return ConnectorCondition switch @@ -121,40 +137,6 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape }; } - public static unsafe bool ValidateCustomShapeString(byte* shape) - { - // "shpx_*" - if (shape is null) - return false; - - if (*shape++ is not (byte)'s' - || *shape++ is not (byte)'h' - || *shape++ is not (byte)'p' - || *shape++ is not (byte)'x' - || *shape++ is not (byte)'_' - || *shape is 0) - return false; - - return true; - } - - public static bool ValidateCustomShapeString(in ShapeString shape) - { - // "shpx_*" - if (shape.Length < 6) - return false; - - var span = shape.AsSpan; - if (span[0] is not (byte)'s' - || span[1] is not (byte)'h' - || span[2] is not (byte)'p' - || span[3] is not (byte)'x' - || span[4] is not (byte)'_') - return false; - - return true; - } - public JObject AddToJson(JObject jObj) { if (Slot is not HumanSlot.Unknown) @@ -164,19 +146,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape jObj["Shape"] = Shape.ToString(); if (ConnectorCondition is not ShapeConnectorCondition.None) jObj["ConnectorCondition"] = ConnectorCondition.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { var shape = jObj["Shape"]?.ToObject(); - if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) + if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString)) return null; - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); - var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; - var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs new file mode 100644 index 00000000..c6800141 --- /dev/null +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -0,0 +1,153 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public unsafe class ShapeAttributeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 14; + public const int ModelSlotSize = 18; + private readonly AttributeHook _attributeHook; + + public static ReadOnlySpan UsedModels + => + [ + HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, + HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, + ]; + + public ShapeAttributeManager(AttributeHook attributeHook) + { + _attributeHook = attributeHook; + _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager); + } + + private readonly Dictionary[] _temporaryShapes = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; + + private HumanSlot _modelIndex; + private int _slotIndex; + private GenderRace _genderRace; + + private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model; + + public void Dispose() + => _attributeHook.Unsubscribe(OnAttributeComputed); + + private void OnAttributeComputed(Actor actor, Model model, ModCollection collection) + { + if (!collection.HasCache) + return; + + _genderRace = (GenderRace)model.AsHuman->RaceSexId; + for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex) + { + _modelIndex = UsedModels[_slotIndex]; + _model = model.AsHuman->Models[_modelIndex.ToIndex()]; + if (_model is null || _model->ModelResourceHandle is null) + continue; + + _ids[(int)_modelIndex] = model.GetModelId(_modelIndex); + CheckShapes(collection.MetaCache!.Shp); + CheckAttributes(collection.MetaCache!.Atr); + } + + UpdateDefaultMasks(model, collection.MetaCache!.Shp); + } + + private void CheckAttributes(AtrCache attributeCache) + { + if (attributeCache.DisabledCount is 0) + return; + + ref var attributes = ref _model->ModelResourceHandle->Attributes; + foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString)) + { + // Mask out custom attributes if they are disabled. Attributes are enabled by default. + if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledAttributeIndexMask &= (ushort)~(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}."); + } + } + } + + private void CheckShapes(ShpCache shapeCache) + { + _temporaryShapes[_slotIndex].Clear(); + ref var shapes = ref _model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryShapes[_slotIndex].TryAdd(shapeString, index); + // Add custom shapes if they are enabled. Shapes are disabled by default. + if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledShapeKeyIndexMask |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + private void UpdateDefaultMasks(Model human, ShpCache cache) + { + foreach (var (shape, topIndex) in _temporaryShapes[1]) + { + if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); + } + + if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); + } + } + + foreach (var (shape, bottomIndex) in _temporaryShapes[3]) + { + if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + { + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex; + human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + HumanSlot slot2, int idx1, int idx2) + { + if (dict.Count is 0) + return; + + foreach (var (shape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) + human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1; + if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) + human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeAttributeString.cs similarity index 55% rename from Penumbra/Meta/ShapeString.cs rename to Penumbra/Meta/ShapeAttributeString.cs index 95ca0933..55e3f021 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeAttributeString.cs @@ -6,11 +6,11 @@ using Penumbra.String.Functions; namespace Penumbra.Meta; [JsonConverter(typeof(Converter))] -public struct ShapeString : IEquatable, IComparable +public struct ShapeAttributeString : IEquatable, IComparable { public const int MaxLength = 30; - public static readonly ShapeString Empty = new(); + public static readonly ShapeAttributeString Empty = new(); private FixedString32 _buffer; @@ -37,6 +37,72 @@ public struct ShapeString : IEquatable, IComparable } } + public static unsafe bool ValidateCustomShapeString(byte* shape) + { + // "shpx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'s' + || *shape++ is not (byte)'h' + || *shape++ is not (byte)'p' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomShapeString() + { + // "shpx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'s' + || _buffer[1] is not (byte)'h' + || _buffer[2] is not (byte)'p' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + + public static unsafe bool ValidateCustomAttributeString(byte* shape) + { + // "atrx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'a' + || *shape++ is not (byte)'t' + || *shape++ is not (byte)'r' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomAttributeString() + { + // "atrx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'a' + || _buffer[1] is not (byte)'t' + || _buffer[2] is not (byte)'r' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsAnkle() => CheckCenter('a', 'n'); @@ -53,28 +119,28 @@ public struct ShapeString : IEquatable, IComparable private bool CheckCenter(char first, char second) => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; - public bool Equals(ShapeString other) + public bool Equals(ShapeAttributeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); public override bool Equals(object? obj) - => obj is ShapeString other && Equals(other); + => obj is ShapeAttributeString other && Equals(other); public override int GetHashCode() => (int)Crc32.Get(_buffer[..Length]); - public static bool operator ==(ShapeString left, ShapeString right) + public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right) => left.Equals(right); - public static bool operator !=(ShapeString left, ShapeString right) + public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right) => !left.Equals(right); - public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret) { var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); return TryRead(span, out ret); } - public unsafe int CompareTo(ShapeString other) + public unsafe int CompareTo(ShapeAttributeString other) { fixed (void* lhs = &this) { @@ -82,7 +148,7 @@ public struct ShapeString : IEquatable, IComparable } } - public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf8, out ShapeAttributeString ret) { if (utf8.Length is 0 or > MaxLength) { @@ -97,7 +163,7 @@ public struct ShapeString : IEquatable, IComparable return true; } - public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf16, out ShapeAttributeString ret) { ret = Empty; if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) @@ -116,19 +182,20 @@ public struct ShapeString : IEquatable, IComparable _buffer[31] = length; } - private sealed class Converter : JsonConverter + private sealed class Converter : JsonConverter { - public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer) { writer.WriteValue(value.ToString()); } - public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString existingValue, + bool hasExistingValue, JsonSerializer serializer) { var value = serializer.Deserialize(reader); if (!TryRead(value, out existingValue)) - throw new JsonReaderException($"Could not parse {value} into ShapeString."); + throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString."); return existingValue; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs deleted file mode 100644 index abd4c3b8..00000000 --- a/Penumbra/Meta/ShapeManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.Collections.Cache; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.PostProcessing; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Meta; - -public class ShapeManager : IRequiredService, IDisposable -{ - public const int NumSlots = 14; - public const int ModelSlotSize = 18; - private readonly AttributeHook _attributeHook; - - public static ReadOnlySpan UsedModels - => - [ - HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, - HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, - ]; - - public ShapeManager(AttributeHook attributeHook) - { - _attributeHook = attributeHook; - _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager); - } - - private readonly Dictionary[] _temporaryIndices = - Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); - - private readonly uint[] _temporaryMasks = new uint[NumSlots]; - private readonly uint[] _temporaryValues = new uint[NumSlots]; - private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; - - public void Dispose() - => _attributeHook.Unsubscribe(OnAttributeComputed); - - private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) - { - if (!collection.HasCache) - return; - - ComputeCache(model, collection.MetaCache!.Shp); - for (var i = 0; i < NumSlots; ++i) - { - if (_temporaryMasks[i] is 0) - continue; - - var modelIndex = UsedModels[i]; - var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask; - var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i]; - Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); - model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask; - } - } - - private unsafe void ComputeCache(Model human, ShpCache cache) - { - for (var i = 0; i < NumSlots; ++i) - { - _temporaryMasks[i] = 0; - _temporaryValues[i] = 0; - _temporaryIndices[i].Clear(); - - var modelIndex = UsedModels[i]; - var model = human.AsHuman->Models[modelIndex.ToIndex()]; - if (model is null || model->ModelResourceHandle is null) - continue; - - _ids[(int)modelIndex] = human.GetModelId(modelIndex); - - ref var shapes = ref model->ModelResourceHandle->Shapes; - foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) - { - if (ShapeString.TryRead(shape.Value, out var shapeString)) - { - _temporaryIndices[i].TryAdd(shapeString, index); - _temporaryMasks[i] |= (ushort)(1 << index); - if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) - _temporaryValues[i] |= (ushort)(1 << index); - } - else - { - Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); - } - } - } - - UpdateDefaultMasks(cache); - } - - private void UpdateDefaultMasks(ShpCache cache) - { - foreach (var (shape, topIndex) in _temporaryIndices[1]) - { - if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[2] |= 1u << handIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); - } - - if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[3] |= 1u << legIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); - } - } - - foreach (var (shape, bottomIndex) in _temporaryIndices[3]) - { - if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) - { - _temporaryValues[3] |= 1u << bottomIndex; - _temporaryValues[4] |= 1u << footIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); - } - } - - return; - - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) - { - if (dict.Count is 0) - return; - - foreach (var (shape, set) in dict) - { - if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1)) - _temporaryValues[idx1] |= 1u << index1; - if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2)) - _temporaryValues[idx2] |= 1u << index2; - } - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs new file mode 100644 index 00000000..89fadfa8 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs @@ -0,0 +1,274 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class AtrMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Attributes(ATR)###ATR"u8; + + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("atrx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; + + public override int NumColumns + => 7; + + public override float ColumnHeight + => ImUtf8.FrameHeightSpacing; + + protected override void Initialize() + { + Identifier = new AtrIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, GenderRace.Unknown); + Entry = AtrEntry.True; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATR manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atr))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var tt = canAdd + ? "Stage this edit."u8 + : _identifierValid + ? "This entry does not contain a valid attribute."u8 + : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, AtrEntry.False); + + DrawIdentifierInput(ref Identifier); + DrawEntry(ref Entry, true); + } + + protected override void DrawEntry(AtrIdentifier identifier, AtrEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + if (DrawEntry(ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtrIdentifier, AtrEntry)> Enumerate() + => Editor.Atr + .OrderBy(kvp => kvp.Key.Attribute) + .ThenBy(kvp => kvp.Key.Slot) + .ThenBy(kvp => kvp.Key.Id) + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Atr.Count; + + private bool DrawIdentifierInput(ref AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawHumanSlot(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttributeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + return changes; + } + + private static void DrawIdentifier(AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + + ImUtf8.TextFramed(ShpMetaDrawer.SlotName(identifier.Slot), FrameColor); + ImUtf8.HoverTooltip("Model Slot"u8); + + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this attribute to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + + ImGui.TableNextColumn(); + if (identifier.Id.HasValue) + ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); + else + ImUtf8.TextFramed("All IDs"u8, FrameColor); + ImUtf8.HoverTooltip("Primary ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.AsSpan, FrameColor); + } + + private static bool DrawEntry(ref AtrEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var value = entry.Value; + var changes = ImUtf8.Checkbox("##atrEntry"u8, ref value); + if (changes) + entry = new AtrEntry(value); + ImUtf8.HoverTooltip("Whether to enable or disable this attribute for the selected items."); + return changes; + } + + public static bool DrawPrimaryId(ref AtrIdentifier identifier, float unscaledWidth = 100) + { + var allSlots = identifier.Slot is HumanSlot.Unknown; + var all = !identifier.Id.HasValue; + var ret = false; + using (ImRaii.Disabled(allSlots)) + { + if (ImUtf8.Checkbox("##atrAll"u8, ref all)) + { + identifier = identifier with { Id = all ? null : 0 }; + ret = true; + } + } + + ImUtf8.HoverTooltip(allSlots + ? "When using all slots, you also need to use all IDs."u8 + : "Enable this attribute for all model IDs."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (all) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f), + new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + else + { + var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1; + if (IdInput("##atrPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false)) + { + identifier = identifier with { Id = setId }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8); + + return ret; + } + + public bool DrawHumanSlot(ref AtrIdentifier identifier, float unscaledWidth = 150) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##atrSlot"u8, ShpMetaDrawer.SlotName(identifier.Slot))) + { + if (combo) + foreach (var slot in ShpMetaDrawer.AvailableSlots) + { + if (!ImUtf8.Selectable(ShpMetaDrawer.SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot) + continue; + + ret = true; + if (slot is HumanSlot.Unknown) + { + identifier = identifier with + { + Id = null, + Slot = slot, + }; + } + else + { + identifier = identifier with + { + Id = identifier.Id.HasValue + ? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0, + slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1) + : null, + Slot = slot, + }; + ret = true; + } + } + } + + ImUtf8.HoverTooltip("Model Slot"u8); + return ret; + } + + private static bool DrawGenderRaceConditionInput(ref AtrIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, + identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this attribute for this gender & race code."u8); + + return ret; + } + + public static unsafe bool DrawAttributeKeyInput(ref AtrIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##atrAttribute"u8, span, out int newLength, "Attribute..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = buffer.ValidateCustomAttributeString(); + if (valid) + identifier = identifier with { Attribute = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Supported attribute need to have the format `atrx_*` and a maximum length of 30 characters."u8); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index 70b5f83b..792611e2 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -12,7 +12,8 @@ public class MetaDrawers( ImcMetaDrawer imc, RspMetaDrawer rsp, AtchMetaDrawer atch, - ShpMetaDrawer shp) : IService + ShpMetaDrawer shp, + AtrMetaDrawer atr) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -23,6 +24,7 @@ public class MetaDrawers( public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly AtchMetaDrawer Atch = atch; public readonly ShpMetaDrawer Shp = shp; + public readonly AtrMetaDrawer Atr = atr; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -35,6 +37,7 @@ public class MetaDrawers( MetaManipulationType.Rsp => Rsp, MetaManipulationType.Atch => Atch, MetaManipulationType.Shp => Shp, + MetaManipulationType.Atr => Atr, MetaManipulationType.GlobalEqp => GlobalEqp, _ => null, }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 6505ecc0..35c8ccec 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -20,18 +21,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; - private bool _identifierValid; + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("shpx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; public override int NumColumns - => 7; + => 8; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, ShapeConnectorCondition.None, GenderRace.Unknown); } protected override void DrawNew() @@ -79,6 +80,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); var changes = DrawHumanSlot(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + ImGui.TableNextColumn(); changes |= DrawPrimaryId(ref identifier); @@ -97,6 +101,17 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor); ImUtf8.HoverTooltip("Model Slot"u8); + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this shape key to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + ImGui.TableNextColumn(); if (identifier.Id.HasValue) ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); @@ -165,7 +180,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 170) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -212,18 +227,19 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150) + public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 200) { var ret = false; var ptr = Unsafe.AsPointer(ref buffer); - var span = new Span(ptr, ShapeString.MaxLength + 1); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) { ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8)) { buffer.ForceLength((byte)newLength); - valid = ShpIdentifier.ValidateCustomShapeString(buffer); + valid = buffer.ValidateCustomShapeString(); if (valid) identifier = identifier with { Shape = buffer }; ret = true; @@ -234,7 +250,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150) + private static bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 80) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -271,7 +287,43 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - private static ReadOnlySpan AvailableSlots + private static bool DrawGenderRaceConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this shape key for this gender & race code."u8); + + return ret; + } + + public static ReadOnlySpan AvailableSlots => [ HumanSlot.Unknown, @@ -291,7 +343,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile HumanSlot.Ear, ]; - private static ReadOnlySpan SlotName(HumanSlot slot) + public static ReadOnlySpan SlotName(HumanSlot slot) => slot switch { HumanSlot.Unknown => "All Slots"u8, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 70a15373..3f19da5e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -62,6 +62,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.Shp); + DrawEditHeader(MetaManipulationType.Atr); DrawEditHeader(MetaManipulationType.GlobalEqp); } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 109cb5c4..9290e52d 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -35,6 +35,27 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) DrawCollectionShapeCache(actor); DrawCharacterShapes(human); + DrawCollectionAttributeCache(actor); + DrawCharacterAttributes(human); + } + + private unsafe void DrawCollectionAttributeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Attribute Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##aCache"u8, 2, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) + DrawShapeAttribute(attribute, set); } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -44,7 +65,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode1.Success || !data.ModCollection.HasCache) return; - using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##sCache"u8, 3, ImGuiTableFlags.RowBg); if (!table) return; @@ -58,14 +79,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShape(shape, set); + DrawShapeAttribute(shape, set); } } } - private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shape.AsSpan); + ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); if (set.All) { ImUtf8.DrawTableColumn("All"u8); @@ -73,7 +94,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) else { ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) + foreach (var slot in ShapeAttributeManager.UsedModels) { if (!set[slot]) continue; @@ -82,10 +103,52 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.SameLine(0, 0); } - foreach (var item in set.Where(i => !set[i.Slot])) + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); + if (set[gr]) + { + ImUtf8.Text($"All {gr.ToName()}, "); + ImGui.SameLine(0, 0); + } + else + { + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (!set[slot, gr]) + continue; + + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + } + } + + + foreach (var ((slot, id), flags) in set) + { + if ((flags & 1ul) is not 0) + { + if (set[slot]) + continue; + + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 1; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags); + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr]) + continue; + + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, "); + currentFlags >>= currentIndex; + currentIndex = BitOperations.TrailingZeroCount(currentFlags); + } + } } } } @@ -96,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg); if (!table) return; @@ -140,4 +203,55 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) } } } + + private unsafe void DrawCharacterAttributes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Attributes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledAttributeIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(attribute.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } } diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json index 55fc5cad..81f2cef3 100644 --- a/schemas/structs/manipulation.json +++ b/schemas/structs/manipulation.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "Type": { - "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ] + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp", "Atr" ] }, "Manipulation": { "type": "object" @@ -100,6 +100,16 @@ "$ref": "meta_shp.json" } } + }, + { + "properties": { + "Type": { + "const": "Atr" + }, + "Manipulation": { + "$ref": "meta_atr.json" + } + } } ] } diff --git a/schemas/structs/meta_atr.json b/schemas/structs/meta_atr.json new file mode 100644 index 00000000..479d4127 --- /dev/null +++ b/schemas/structs/meta_atr.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "boolean" + }, + "Slot": { + "$ref": "meta_enums.json#HumanSlot" + }, + "Id": { + "$ref": "meta_enums.json#U16" + }, + "Attribute": { + "type": "string", + "minLength": 5, + "maxLength": 30, + "pattern": "^atrx_" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] + } + }, + "required": [ + "Attribute" + ] +} diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 851842a4..cb7fd0ec 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -19,6 +19,9 @@ }, "ConnectorCondition": { "$ref": "meta_enums.json#ShapeConnectorCondition" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] } }, "required": [ From f5db888bbdb1e5193086027daf3f99da95636c33 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 21 May 2025 13:49:29 +0000 Subject: [PATCH 2291/2451] [CI] Updating repo.json for testing_1.3.6.13 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0413b876..3e2a9d2c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.12", + "TestingAssemblyVersion": "1.3.6.13", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.13/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 507b0a5aee46b0c7a62e2a3a8ba997a0dbc4944e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 18:07:12 +0200 Subject: [PATCH 2292/2451] Slight description update. --- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index cb22b54a..4bbdf2a9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -761,7 +761,7 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); - Checkbox("Enable Custom Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + Checkbox("Enable Custom Shape and Attribute Support", "Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.", _config.EnableCustomShapes, _attributeHook.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); From ac4c75d3c36f658c054d2ef508d909f94f975e39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:13:42 +0200 Subject: [PATCH 2293/2451] Fix not updating meta count correctly. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 23eaec76..ede062ae 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -596,6 +596,7 @@ public class MetaDictionary + _data!.Gmp.Count + _data!.Atch.Count + _data!.Shp.Count + + _data!.Atr.Count + _data!.Count; } From 400d7d0bea0a611be2071d56236ba3745828f4ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:13:58 +0200 Subject: [PATCH 2294/2451] Slight improvements. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 12 ++++++------ Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 10 ++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3f19da5e..aa3d9172 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -165,7 +165,7 @@ public partial class ModEditWindow private void AddFromClipboardButton() { - if (ImGui.Button("Add from Clipboard")) + if (ImUtf8.Button("Add from Clipboard"u8)) { var clipboard = ImGuiUtil.GetClipboardText(); @@ -176,13 +176,13 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip( - "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations."); + ImUtf8.HoverTooltip( + "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations."u8); } private void SetFromClipboardButton() { - if (ImGui.Button("Set from Clipboard")) + if (ImUtf8.Button("Set from Clipboard"u8)) { var clipboard = ImGuiUtil.GetClipboardText(); if (MetaApi.ConvertManips(clipboard, out var manips, out _)) @@ -192,7 +192,7 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip( - "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); + ImUtf8.HoverTooltip( + "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."u8); } } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 9290e52d..3180a212 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -159,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 6, ImGuiTableFlags.RowBg); if (!table) return; @@ -167,6 +167,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -184,6 +185,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledShapeKeyIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) { @@ -200,6 +202,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } @@ -210,7 +213,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##attributes"u8, 6, ImGuiTableFlags.RowBg); if (!table) return; @@ -218,6 +221,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -235,6 +239,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledAttributeIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) { @@ -251,6 +256,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } From bc4f88aee9e91b299a828e18b1196c6562df13e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:14:17 +0200 Subject: [PATCH 2295/2451] Fix shape/attribute mask stupidity. --- Penumbra/Meta/ShapeAttributeManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index c6800141..e9c9c169 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -75,7 +75,7 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable { // Mask out custom attributes if they are disabled. Attributes are enabled by default. if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) - _model->EnabledAttributeIndexMask &= (ushort)~(1 << index); + _model->EnabledAttributeIndexMask &= ~(1u << index); } else { @@ -95,7 +95,7 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable _temporaryShapes[_slotIndex].TryAdd(shapeString, index); // Add custom shapes if they are enabled. Shapes are disabled by default. if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) - _model->EnabledShapeKeyIndexMask |= (ushort)(1 << index); + _model->EnabledShapeKeyIndexMask |= 1u << index; } else { From 9e7c30455625e9296702f461a7bf644a10af934a Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 22 May 2025 09:16:22 +0000 Subject: [PATCH 2296/2451] [CI] Updating repo.json for testing_1.3.6.14 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3e2a9d2c..8b262136 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.13", + "TestingAssemblyVersion": "1.3.6.14", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.13/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.14/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1bdbfe22c1f7b576cdb25bd775fa437ded0db559 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 10:50:04 +0200 Subject: [PATCH 2297/2451] Update Libraries. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 9aeda9a8..421874a1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9aeda9a892d9b971e32b10db21a8daf9c0b9ee53 +Subproject commit 421874a12540b7f8c1279dcc6a92e895a94d2fbc diff --git a/Penumbra.GameData b/Penumbra.GameData index bb3b462b..14b3641f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1 +Subproject commit 14b3641f0fb520cb829ceb50fa7cb31255a1da4e From 08c91248583bdd07f98968f66de0a57bc1a58dea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 11:29:52 +0200 Subject: [PATCH 2298/2451] Fix issue with shapes/attributes not checking the groups correctly. --- .../Cache/ShapeAttributeHashSet.cs | 70 +++++++++++-------- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 11 +-- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index f1fc7127..74691e41 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -21,43 +21,39 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); - public bool this[HumanSlot slot] - => slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count]; - - public bool this[GenderRace genderRace] - => GenderRaceIndices.TryGetValue(genderRace, out var index) - && _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]; - - public bool this[HumanSlot slot, GenderRace genderRace] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool CheckGroups(HumanSlot slot, GenderRace genderRace) { - get - { - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return false; + if (All || this[slot]) + return true; - if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]) - return true; + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; - return _allIds[(int)slot * GenderRaceIndices.Count + index]; - } - set - { - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return; + if (_allIds[ToIndex(HumanSlot.Unknown, index)]) + return true; - var genderRaceCount = GenderRaceValues.Count; - if (slot is HumanSlot.Unknown) - _allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value; - else - _allIds[(int)slot * genderRaceCount + index] = value; - } + return _allIds[ToIndex(slot, index)]; } + public bool this[HumanSlot slot] + => _allIds[ToIndex(slot, 0)]; + + public bool this[GenderRace genderRace] + => ToIndex(HumanSlot.Unknown, genderRace, out var index) && _allIds[index]; + + public bool this[HumanSlot slot, GenderRace genderRace] + => ToIndex(slot, genderRace, out var index) && _allIds[index]; + public bool All - => _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count]; + => _allIds[AllIndex]; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int ToIndex(HumanSlot slot, int genderRaceIndex) + => slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count; public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace); + => CheckGroups(slot, genderRace) || ContainsEntry(slot, id, genderRace); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) @@ -72,9 +68,9 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (!id.HasValue) { - var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot; - var old = _allIds[slotIndex * GenderRaceIndices.Count + index]; - _allIds[slotIndex * GenderRaceIndices.Count + index] = value; + var slotIndex = ToIndex(slot, index); + var old = _allIds[slotIndex]; + _allIds[slotIndex] = value; return old != value; } @@ -120,4 +116,16 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI public bool IsEmpty => !_allIds.HasAnySet() && Count is 0; + + private static readonly int AllIndex = ShapeAttributeManager.ModelSlotSize * GenderRaceValues.Count; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool ToIndex(HumanSlot slot, GenderRace genderRace, out int index) + { + if (!GenderRaceIndices.TryGetValue(genderRace, out index)) + return false; + + index = ToIndex(slot, index); + return true; + } } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 3180a212..fd37bf35 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; using Penumbra.Collections.Cache; @@ -167,7 +168,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -187,9 +188,9 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.DrawTableColumn($"{mask:X8}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + foreach (var ((shape, flag), idx) in model->ModelResourceHandle->Shapes.WithIndex()) { - var disabled = (mask & (1u << idx)) is 0; + var disabled = (mask & (1u << flag)) is 0; using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); ImUtf8.Text(shape.AsSpan()); ImGui.SameLine(0, 0); @@ -241,9 +242,9 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.DrawTableColumn($"{mask:X8}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); - foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) + foreach (var ((attribute, flag), idx) in model->ModelResourceHandle->Attributes.WithIndex()) { - var disabled = (mask & (1u << idx)) is 0; + var disabled = (mask & (1u << flag)) is 0; using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); ImUtf8.Text(attribute.AsSpan()); ImGui.SameLine(0, 0); From ccc2c1fd4c9a226758dbb65d1330ae844fe21a80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 11:30:10 +0200 Subject: [PATCH 2299/2451] Fix missing other option notifications for shp/atr. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 050dab51..b4db457d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -64,6 +64,8 @@ public class ModMetaEditor( OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); + OtherData[MetaManipulationType.Shp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Shp)); + OtherData[MetaManipulationType.Atr].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atr)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } From cd56163b1b75132b8892e462e1d36225a4a7d79c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 23 May 2025 09:32:11 +0000 Subject: [PATCH 2300/2451] [CI] Updating repo.json for testing_1.3.6.15 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8b262136..9a41e09b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.14", + "TestingAssemblyVersion": "1.3.6.15", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.14/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.15/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 82fc334be7c45921102d621a0dfee6ac8b93bc39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 15:17:13 +0200 Subject: [PATCH 2301/2451] Use dynamis for some pointers. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b4fa3b9f..76df5acc 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1075,14 +1075,14 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"Slot {i}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(imc == null ? "NULL" : $"0x{(ulong)imc:X}"); + Penumbra.Dynamis.DrawPointer((nint)imc); ImGui.TableNextColumn(); if (imc != null) UiHelpers.Text(imc); var mdl = (RenderModel*)model->Models[i]; ImGui.TableNextColumn(); - ImGui.TextUnformatted(mdl == null ? "NULL" : $"0x{(ulong)mdl:X}"); + Penumbra.Dynamis.DrawPointer((nint)mdl); if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) continue; From ebe45c6a47eeea7cab7e7db38d2da90bb875304b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 May 2025 11:33:10 +0200 Subject: [PATCH 2302/2451] Update Lib. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 421874a1..cee50c3f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 421874a12540b7f8c1279dcc6a92e895a94d2fbc +Subproject commit cee50c3fe97a03ca7445c81de651b609620da526 From 2c115eda9426e53ffbcd06b494077b5863b17f3d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 May 2025 13:54:23 +0200 Subject: [PATCH 2303/2451] Slightly improve error message when importing wrongly named atch files. --- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 5bc70fc3..5b6d585a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -34,6 +34,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer private AtchFile? _currentBaseAtchFile; private AtchPoint? _currentBaseAtchPoint; private readonly AtchPointCombo _combo; + private string _fileImport = string.Empty; public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : base(editor, metaFiles) @@ -48,6 +49,8 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer => obj.ToName(); } + private sealed class RaceCodeException(string filePath) : Exception($"Could not identify race code from path {filePath}."); + public void ImportFile(string filePath) { try @@ -57,14 +60,15 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer var gr = Parser.ParseRaceCode(filePath); if (gr is GenderRace.Unknown) - throw new Exception($"Could not identify race code from path {filePath}."); - var text = File.ReadAllBytes(filePath); - var file = new AtchFile(text); + throw new RaceCodeException(filePath); + + var text = File.ReadAllBytes(filePath); + var file = new AtchFile(text); foreach (var point in file.Points) { foreach (var (entry, index) in point.Entries.WithIndex()) { - var identifier = new AtchIdentifier(point.Type, gr, (ushort) index); + var identifier = new AtchIdentifier(point.Type, gr, (ushort)index); var defaultValue = AtchCache.GetDefault(MetaFiles, identifier); if (defaultValue == null) continue; @@ -76,6 +80,12 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer } } } + catch (RaceCodeException ex) + { + Penumbra.Messager.AddMessage(new Notification(ex, "The imported .atch file does not contain a race code (cXXXX) in its name.", + "Could not import .atch file:", + NotificationType.Warning)); + } catch (Exception ex) { Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", @@ -157,12 +167,12 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer private void UpdateFile() { - _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); if (_currentBaseAtchPoint == null) { _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); - Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; + Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; } if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) From 5e985f4a84b45706373db655bcc4472917d4d10a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 May 2025 13:54:42 +0200 Subject: [PATCH 2304/2451] 1.4.0.0 --- Penumbra/UI/Changelog.cs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 5e1612eb..c1f7a1e6 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -61,10 +61,49 @@ public class PenumbraChangelog : IUiService Add1_3_5_0(Changelog); Add1_3_6_0(Changelog); Add1_3_6_4(Changelog); + Add1_4_0_0(Changelog); } #region Changelogs + private static void Add1_4_0_0(Changelog log) + => log.NextVersion("Version 1.4.0.0") + .RegisterHighlight("Added two types of new Meta Changes, SHP and ATR (Thanks Karou!).") + .RegisterEntry("Those allow mod creators to toggle custom shape keys and attributes for models on and off, respectively.", 1) + .RegisterEntry("Custom shape keys need to have the format 'shpx_*' and custom attributes need 'atrx_*'.", 1) + .RegisterHighlight( + "Shapes of the following formats will automatically be toggled on if both relevant slots contain the same shape key:", 1) + .RegisterEntry("'shpx_wa_*', for the waist seam between the body and leg slot,", 2) + .RegisterEntry("'shpx_wr_*', for the wrist seams between the body and hands slot,", 2) + .RegisterEntry("'shpx_an_*', for the ankle seams between the leg and feet slot.", 2) + .RegisterEntry( + "Custom shape key and attributes can be turned off in the advanced settings section for the moment, but this is not recommended.", + 1) + .RegisterHighlight("The mod selector width is now draggable within certain restrictions that depend on the total window width.") + .RegisterEntry("The current behavior may not be final, let me know if you have any comments.", 1) + .RegisterEntry("Improved the naming of NPCs for identifiers by using Haselnussbombers new naming functionality (Thanks Hasel!).") + .RegisterEntry("Added global EQP entries to always hide Au Ra horns, Viera ears, or Miqo'te ears, respectively.") + .RegisterEntry("This will leave holes in the heads of the respective race if not modded in some way.", 1) + .RegisterEntry("Added a filter for mods that have temporary settings in the mod selector panel (Thanks Caraxi).") + .RegisterEntry("Made the checkbox for toggling Temporary Settings Mode in the mod tab more visible.") + .RegisterEntry("Improved the option select combo in advanced editing.") + .RegisterEntry("Fixed some issues with item identification for EST changes.") + .RegisterEntry("Fixed the sizing of the mod panel being off by 1 pixel sometimes.") + .RegisterEntry("Fixed an issue with redrawing while in GPose when other plugins broke some assumptions about the game state.") + .RegisterEntry("Fixed a clipping issue within the Meta Manipulations tab in advanced editing.") + .RegisterEntry("Fixed an issue with empty and temporary settings.") + .RegisterHighlight( + "In the Item Swap tab, items changed by this mod are now sorted and highlighted before items changed in the current collection before other items for the source, and inversely for the target. (1.3.6.8)") + .RegisterHighlight( + "Default-valued meta edits should now be kept on import and only removed when the option to keep them is not set AND no other options in the mod edit the same entry. (1.3.6.8)") + .RegisterEntry("Added a right-click context menu on file redirections to copy the full file path. (1.3.6.8)") + .RegisterEntry( + "Added a right-click context menu on the mod export button to open the backup directory in your file explorer. (1.3.6.8)") + .RegisterEntry("Fixed some issues when redrawing characters from other plugins. (1.3.6.8)") + .RegisterEntry( + "Added a modifier key separate from the delete modifier key that is used for less important key-checks, specifically toggling incognito mode. (1.3.6.7)") + .RegisterEntry("Fixed some issues with the Material Editor (Thanks Ny). (1.3.6.6)"); + private static void Add1_3_6_4(Changelog log) => log.NextVersion("Version 1.3.6.4") .RegisterEntry("The material editor should be functional again."); From 1551d9b6f3fa35ffde863e5442eab23ea52c6e35 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 28 May 2025 11:56:49 +0000 Subject: [PATCH 2305/2451] [CI] Updating repo.json for 1.4.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9a41e09b..94020c96 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.15", + "AssemblyVersion": "1.4.0.0", + "TestingAssemblyVersion": "1.4.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.15/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f2927290f52ceebde1a60edd2f5b8b25cf56e186 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:35:13 +0200 Subject: [PATCH 2306/2451] Fix exceptions when unsubscribing during event invocation. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index cee50c3f..17a3ee57 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cee50c3fe97a03ca7445c81de651b609620da526 +Subproject commit 17a3ee5711ca30eb7f5b393dfb8136f0bce49b2b From ff2a9f95c46a7500528127aaca0c413653df706e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:36:07 +0200 Subject: [PATCH 2307/2451] Fix Atr and Shp not being transmitted via Mare, add improved compression but don't use it yet. --- Penumbra/Api/Api/MetaApi.cs | 232 ++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 7c0cd5fc..5cffc811 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -66,6 +66,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } return Functions.ToCompressedBase64(array, 0); @@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } WriteCache(zipStream, cache.Atch); + WriteCache(zipStream, cache.Shp); + WriteCache(zipStream, cache.Atr); } } @@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } } + public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8); + public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8); + public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P'; + public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8); + public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8); + public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8); + public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P'; + public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H'; + public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8); + public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8); + + private static unsafe string CompressMetaManipulationsV2(ModCollection? collection) + { + using var ms = new MemoryStream(); + ms.Capacity = 1024; + using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true)) + { + zipStream.Write((byte)2); + zipStream.Write("META0002"u8); + if (collection?.MetaCache is { } cache) + { + WriteCache(zipStream, cache.Imc, ImcKey); + WriteCache(zipStream, cache.Eqp, EqpKey); + WriteCache(zipStream, cache.Eqdp, EqdpKey); + WriteCache(zipStream, cache.Est, EstKey); + WriteCache(zipStream, cache.Rsp, RspKey); + WriteCache(zipStream, cache.Gmp, GmpKey); + cache.GlobalEqp.EnterReadLock(); + + try + { + if (cache.GlobalEqp.Count > 0) + { + zipStream.Write(GeqpKey); + zipStream.Write(cache.GlobalEqp.Count); + foreach (var (globalEqp, _) in cache.GlobalEqp) + zipStream.Write(new ReadOnlySpan(&globalEqp, sizeof(GlobalEqpManipulation))); + } + } + finally + { + cache.GlobalEqp.ExitReadLock(); + } + + WriteCache(zipStream, cache.Atch, AtchKey); + WriteCache(zipStream, cache.Shp, ShpKey); + WriteCache(zipStream, cache.Atr, AtrKey); + } + } + + ms.Flush(); + ms.Position = 0; + var data = ms.GetBuffer().AsSpan(0, (int)ms.Length); + return Convert.ToBase64String(data); + + void WriteCache(Stream stream, MetaCacheBase metaCache, uint label) + where TKey : unmanaged, IMetaIdentifier + where TValue : unmanaged + { + metaCache.EnterReadLock(); + try + { + if (metaCache.Count <= 0) + return; + + stream.Write(label); + stream.Write(metaCache.Count); + foreach (var (identifier, (_, value)) in metaCache) + { + stream.Write(identifier); + stream.Write(value); + } + } + finally + { + metaCache.ExitReadLock(); + } + } + } + /// /// Convert manipulations from a transmitted base64 string to actual manipulations. /// The empty string is treated as an empty set. @@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver { case 0: return ConvertManipsV0(data, out manips); case 1: return ConvertManipsV1(data, out manips); + case 2: return ConvertManipsV2(data, out manips); default: Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); manips = null; @@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } } + private static bool ConvertManipsV2(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (!data.StartsWith("META0002"u8)) + { + Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix."); + manips = null; + return false; + } + + manips = new MetaDictionary(); + var r = new SpanBinaryReader(data[8..]); + while (r.Remaining > 4) + { + var prefix = r.ReadUInt32(); + var count = r.Remaining > 4 ? r.ReadInt32() : 0; + if (count is 0) + continue; + + switch (prefix) + { + case ImcKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EqpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EqdpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EstKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case RspKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case GmpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case GeqpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier)) + return false; + } + + break; + case AtchKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case ShpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case AtrKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + } + } + + return true; + } + private static bool ConvertManipsV1(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) { if (!data.StartsWith("META0001"u8)) @@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver if (!identifier.Validate() || !manips.TryAdd(identifier, value)) return false; } + + // Shp and Atr was added later + if (r.Position < r.Count) + { + var shpCount = r.ReadInt32(); + for (var i = 0; i < shpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var atrCount = r.ReadInt32(); + for (var i = 0; i < atrCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + } } return true; From 74bd1cf911fdd906b16c3bce1acc38716bf14efe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:36:33 +0200 Subject: [PATCH 2308/2451] Fix checking the flags for all races and genders for specific IDs in shapes/attributes. --- Penumbra/Collections/Cache/ShapeAttributeHashSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index 74691e41..9670928f 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -59,7 +59,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) => GenderRaceIndices.TryGetValue(genderRace, out var index) && TryGetValue((slot, id), out var flags) - && (flags & (1ul << index)) is not 0; + && ((flags & 1ul) is not 0 || (flags & (1ul << index)) is not 0); public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) { From 75f4e66dbf5da8984e2cbe825fb8ba8d8a7e0652 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 30 May 2025 12:38:32 +0000 Subject: [PATCH 2309/2451] [CI] Updating repo.json for 1.4.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 94020c96..7cdc7bbb 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.4.0.0", - "TestingAssemblyVersion": "1.4.0.0", + "AssemblyVersion": "1.4.0.1", + "TestingAssemblyVersion": "1.4.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b48c4f440acdc02a32c9518023269e1524a8e191 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jun 2025 13:04:01 +0200 Subject: [PATCH 2310/2451] Make attributes and shapes completely toggleable. --- Penumbra/Collections/Cache/AtrCache.cs | 27 ++- .../Cache/ShapeAttributeHashSet.cs | 171 +++++++++++------- Penumbra/Collections/Cache/ShpCache.cs | 31 +++- Penumbra/Meta/ShapeAttributeManager.cs | 37 ++-- Penumbra/UI/ConfigWindow.cs | 12 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 123 +++++++------ 6 files changed, 245 insertions(+), 156 deletions(-) diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs index 757ddaa2..b017da32 100644 --- a/Penumbra/Collections/Cache/AtrCache.cs +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -8,10 +8,12 @@ namespace Penumbra.Collections.Cache; public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace) - => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace); + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false; + public int EnabledCount { get; private set; } public int DisabledCount { get; private set; } + internal IReadOnlyDictionary Data => _atrData; @@ -21,24 +23,28 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) { Clear(); _atrData.Clear(); + DisabledCount = 0; + EnabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry) { if (!_atrData.TryGetValue(identifier.Attribute, out var value)) { - if (entry.Value) - return; - value = []; _atrData.Add(identifier.Attribute, value); } - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value)) - ++DisabledCount; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } protected override void RevertModInternal(AtrIdentifier identifier) @@ -46,9 +52,12 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) if (!_atrData.TryGetValue(identifier.Attribute, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --DisabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) _atrData.Remove(identifier.Attribute); } diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index 9670928f..e50ceaa2 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -19,93 +19,126 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI public static readonly FrozenDictionary GenderRaceIndices = GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); - private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + private readonly BitArray _allIds = new(2 * (ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool? this[HumanSlot slot] + => AllCheck(ToIndex(slot, 0)); + + public bool? this[GenderRace genderRace] + => ToIndex(HumanSlot.Unknown, genderRace, out var index) ? AllCheck(index) : null; + + public bool? this[HumanSlot slot, GenderRace genderRace] + => ToIndex(slot, genderRace, out var index) ? AllCheck(index) : null; + + public bool? All + => Convert(_allIds[2 * AllIndex], _allIds[2 * AllIndex + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private bool CheckGroups(HumanSlot slot, GenderRace genderRace) - { - if (All || this[slot]) - return true; - - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return false; - - if (_allIds[ToIndex(HumanSlot.Unknown, index)]) - return true; - - return _allIds[ToIndex(slot, index)]; - } - - public bool this[HumanSlot slot] - => _allIds[ToIndex(slot, 0)]; - - public bool this[GenderRace genderRace] - => ToIndex(HumanSlot.Unknown, genderRace, out var index) && _allIds[index]; - - public bool this[HumanSlot slot, GenderRace genderRace] - => ToIndex(slot, genderRace, out var index) && _allIds[index]; - - public bool All - => _allIds[AllIndex]; + private bool? AllCheck(int idx) + => Convert(_allIds[idx], _allIds[idx + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static int ToIndex(HumanSlot slot, int genderRaceIndex) - => slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count; + => 2 * (slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count); - public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => CheckGroups(slot, genderRace) || ContainsEntry(slot, id, genderRace); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => GenderRaceIndices.TryGetValue(genderRace, out var index) - && TryGetValue((slot, id), out var flags) - && ((flags & 1ul) is not 0 || (flags & (1ul << index)) is not 0); - - public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public bool? CheckEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return null; + + // Check for specific ID. + if (TryGetValue((slot, id), out var flags)) + { + // Check completely specified entry. + if (Convert(flags, 2 * index) is { } specified) + return specified; + + // Check any gender / race. + if (Convert(flags, 0) is { } anyGr) + return anyGr; + } + + // Check for specified gender / race and slot, but no ID. + if (AllCheck(ToIndex(slot, index)) is { } noIdButGr) + return noIdButGr; + + // Check for specified gender / race but no slot or ID. + if (AllCheck(ToIndex(HumanSlot.Unknown, index)) is { } noSlotButGr) + return noSlotButGr; + + // Check for specified slot but no gender / race or ID. + if (AllCheck(ToIndex(slot, 0)) is { } noGrButSlot) + return noGrButSlot; + + return All; + } + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool? value, out bool which) + { + which = false; if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) return false; if (!id.HasValue) { var slotIndex = ToIndex(slot, index); - var old = _allIds[slotIndex]; - _allIds[slotIndex] = value; - return old != value; - } - - if (value) - { - if (TryGetValue((slot, id.Value), out var flags)) + var ret = false; + if (value is true) { - var newFlags = flags | (1ul << index); - if (newFlags == flags) - return false; + if (!_allIds[slotIndex]) + ret = true; + _allIds[slotIndex] = true; + _allIds[slotIndex + 1] = false; + } + else if (value is false) + { + if (!_allIds[slotIndex + 1]) + ret = true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = true; + } + else + { + if (_allIds[slotIndex]) + { + which = true; + ret = true; + } + else if (_allIds[slotIndex + 1]) + { + which = false; + ret = true; + } - this[(slot, id.Value)] = newFlags; - return true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = false; } - this[(slot, id.Value)] = 1ul << index; - return true; + return ret; } - else if (TryGetValue((slot, id.Value), out var flags)) + + if (TryGetValue((slot, id.Value), out var flags)) { - var newFlags = flags & ~(1ul << index); + var newFlags = value switch + { + true => (flags | (1ul << index)) & ~(1ul << (index + 1)), + false => (flags & ~(1ul << index)) | (1ul << (index + 1)), + _ => flags & ~(1ul << index) & ~(1ul << (index + 1)), + }; if (newFlags == flags) return false; - if (newFlags is 0) - { - Remove((slot, id.Value)); - return true; - } - this[(slot, id.Value)] = newFlags; + which = (flags & (1ul << index)) is not 0; return true; } - return false; + if (value is null) + return false; + + this[(slot, id.Value)] = 1ul << (index + (value.Value ? 0 : 1)); + return true; } public new void Clear() @@ -128,4 +161,20 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI index = ToIndex(slot, index); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(bool trueValue, bool falseValue) + => trueValue ? true : falseValue ? false : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(ulong mask, int idx) + { + mask >>= idx; + return (mask & 3) switch + { + 1 => true, + 2 => false, + _ => null, + }; + } } diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 22547d25..d8c3a036 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -8,7 +8,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) - => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace); + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true; + + public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false; internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch @@ -20,7 +23,8 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _ => [], }; - public int EnabledCount { get; private set; } + public int EnabledCount { get; private set; } + public int DisabledCount { get; private set; } private readonly Dictionary _shpData = []; private readonly Dictionary _wristConnectors = []; @@ -34,10 +38,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _wristConnectors.Clear(); _waistConnectors.Clear(); _ankleConnectors.Clear(); + EnabledCount = 0; + DisabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { @@ -55,15 +61,17 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) { if (!dict.TryGetValue(identifier.Shape, out var value)) { - if (!entry.Value) - return; - value = []; dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) - ++EnabledCount; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } } @@ -84,9 +92,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --EnabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) dict.Remove(identifier.Shape); } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index e9c9c169..a742806f 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -106,46 +106,59 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable private void UpdateDefaultMasks(Model human, ShpCache cache) { + var genderRace = (GenderRace)human.AsHuman->RaceSexId; foreach (var (shape, topIndex) in _temporaryShapes[1]) { - if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() + && _temporaryShapes[2].TryGetValue(shape, out var handIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[2] is not null) { human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() + && _temporaryShapes[3].TryGetValue(shape, out var legIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[3] is not null) { human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); + CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryShapes[3]) { - if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() + && _temporaryShapes[4].TryGetValue(shape, out var footIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace) + && human.AsHuman->Models[3] is not null + && human.AsHuman->Models[4] is not null) { human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex; human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4); } } return; - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + void CheckCondition(IReadOnlyDictionary dict, GenderRace genderRace, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) { - if (dict.Count is 0) - return; - foreach (var (shape, set) in dict) { - if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) + if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1; - if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) + if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 53fa0b33..64d370b5 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -17,12 +17,12 @@ namespace Penumbra.UI; public sealed class ConfigWindow : Window, IUiService { private readonly IDalamudPluginInterface _pluginInterface; - private readonly Configuration _config; - private readonly PerformanceTracker _tracker; - private readonly ValidityChecker _validityChecker; - private Penumbra? _penumbra; - private ConfigTabBar _configTabs = null!; - private string? _lastException; + private readonly Configuration _config; + private readonly PerformanceTracker _tracker; + private readonly ValidityChecker _validityChecker; + private Penumbra? _penumbra; + private ConfigTabBar _configTabs = null!; + private string? _lastException; public ConfigWindow(PerformanceTracker tracker, IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index fd37bf35..2de78c66 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -52,11 +52,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) - DrawShapeAttribute(attribute, set); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data.OrderBy(a => a.Key)) + { + ImUtf8.DrawTableColumn(attribute.AsSpan); + DrawValues(attribute, set); + } } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -72,83 +75,87 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); foreach (var condition in Enum.GetValues()) { - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition).OrderBy(shp => shp.Key)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShapeAttribute(shape, set); + ImUtf8.DrawTableColumn(shape.AsSpan); + DrawValues(shape, set); } } } - private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) + private static void DrawValues(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeAttributeManager.UsedModels) - { - if (!set[slot]) - continue; + ImGui.TableNextColumn(); - ImUtf8.Text($"All {slot.ToName()}, "); + if (set.All is { } value) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value); + ImUtf8.Text("All"u8); + ImGui.SameLine(0, 0); + } + + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (set[slot] is not { } value2) + continue; + + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value2); + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (set[gr] is { } value3) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value3); + ImUtf8.Text($"All {gr.ToName()}, "); ImGui.SameLine(0, 0); } - - foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + else { - if (set[gr]) + foreach (var slot in ShapeAttributeManager.UsedModels) { - ImUtf8.Text($"All {gr.ToName()}, "); - ImGui.SameLine(0, 0); - } - else - { - foreach (var slot in ShapeAttributeManager.UsedModels) - { - if (!set[slot, gr]) - continue; - - ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); - ImGui.SameLine(0, 0); - } - } - } - - - foreach (var ((slot, id), flags) in set) - { - if ((flags & 1ul) is not 0) - { - if (set[slot]) + if (set[slot, gr] is not { } value4) continue; - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value4); + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); ImGui.SameLine(0, 0); } - else - { - var currentFlags = flags >> 1; - var currentIndex = BitOperations.TrailingZeroCount(currentFlags); - while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) - { - var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; - if (set[slot, gr]) - continue; + } + } - ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, "); - currentFlags >>= currentIndex; - currentIndex = BitOperations.TrailingZeroCount(currentFlags); + foreach (var ((slot, id), flags) in set) + { + if ((flags & 3) is not 0) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), (flags & 2) is not 0); + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 2; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var value5 = (currentFlags & 1) is 1; + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr] != value5) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value5); + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); } + + currentFlags >>= currentIndex * 2; + currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; } } } From 6cba63ac9807b4797166c02b903b872b11890db8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jun 2025 13:04:20 +0200 Subject: [PATCH 2311/2451] Make shape names editable in models. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0c8c496f..cc592296 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -31,19 +31,18 @@ public partial class ModEditWindow private class LoadedData { - public MdlFile LastFile = null!; + public MdlFile LastFile = null!; public readonly List SubMeshAttributeTags = []; public long[] LodTriCount = []; } - private string _modelNewMaterial = string.Empty; + private string _modelNewMaterial = string.Empty; private readonly LoadedData _main = new(); private readonly LoadedData _preview = new(); - private string _customPath = string.Empty; - private Utf8GamePath _customGamePath = Utf8GamePath.Empty; - + private string _customPath = string.Empty; + private Utf8GamePath _customGamePath = Utf8GamePath.Empty; private LoadedData UpdateFile(MdlFile file, bool force, bool disabled) @@ -68,7 +67,7 @@ public partial class ModEditWindow private bool DrawModelPanel(MdlTab tab, bool disabled) { - var ret = tab.Dirty; + var ret = tab.Dirty; var data = UpdateFile(tab.Mdl, ret, disabled); DrawVersionUpdate(tab, disabled); DrawImportExport(tab, disabled); @@ -89,7 +88,8 @@ public partial class ModEditWindow if (disabled || tab.Mdl.Version is not MdlFile.V5) return; - if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, + if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, + "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) return; @@ -350,7 +350,7 @@ public partial class ModEditWindow if (!disabled) { ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); } var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; @@ -369,10 +369,11 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) { - ret |= true; - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; + ret |= true; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; } + ImGui.TableNextColumn(); if (!validName && _modelNewMaterial.Length > 0) DrawInvalidMaterialMarker(); @@ -423,14 +424,16 @@ public partial class ModEditWindow // Add markers to invalid materials. if (!tab.ValidateMaterial(temp)) DrawInvalidMaterialMarker(); - + return ret; } private static void DrawInvalidMaterialMarker() { using (ImRaii.PushFont(UiBuilder.IconFont)) + { ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); + } ImGuiUtil.HoverTooltip( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" @@ -498,11 +501,11 @@ public partial class ModEditWindow using var node = ImRaii.TreeNode($"Click to expand"); if (!node) return; - + var flags = ImGuiTableFlags.SizingFixedFit - | ImGuiTableFlags.RowBg - | ImGuiTableFlags.Borders - | ImGuiTableFlags.NoHostExtendX; + | ImGuiTableFlags.RowBg + | ImGuiTableFlags.Borders + | ImGuiTableFlags.NoHostExtendX; using var table = ImRaii.Table(string.Empty, 4, flags); if (!table) return; @@ -590,6 +593,7 @@ public partial class ModEditWindow if (!header) return false; + var ret = false; using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) { if (table) @@ -650,22 +654,49 @@ public partial class ModEditWindow using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) - foreach (var attribute in data.LastFile.Attributes) - ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Attributes.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var attribute = ref data.LastFile.Attributes[i]; + var name = attribute; + if (ImUtf8.InputText("##attribute"u8, ref name, "Attribute Name..."u8) && name.Length > 0 && name != attribute) + { + attribute = name; + ret = true; + } + } } using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { if (bones) - foreach (var bone in data.LastFile.Bones) - ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Bones.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var bone = ref data.LastFile.Bones[i]; + var name = bone; + if (ImUtf8.InputText("##bone"u8, ref name, "Bone Name..."u8) && name.Length > 0 && name != bone) + { + bone = name; + ret = true; + } + } } using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { if (shapes) - foreach (var shape in data.LastFile.Shapes) - ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Shapes.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var shape = ref data.LastFile.Shapes[i]; + var name = shape.ShapeName; + if (ImUtf8.InputText("##shape"u8, ref name, "Shape Name..."u8) && name.Length > 0 && name != shape.ShapeName) + { + shape.ShapeName = name; + ret = true; + } + } } if (data.LastFile.RemainingData.Length > 0) @@ -675,7 +706,7 @@ public partial class ModEditWindow Widget.DrawHexViewer(data.LastFile.RemainingData); } - return false; + return ret; } private static bool GetFirstModel(IEnumerable files, [NotNullWhen(true)] out string? file) From 98203e4e8a6a7bd660d0df4dabebc6c48776450d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 1 Jun 2025 11:06:37 +0000 Subject: [PATCH 2312/2451] [CI] Updating repo.json for testing_1.4.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7cdc7bbb..e36176d5 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.1", + "TestingAssemblyVersion": "1.4.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 318a41fe52ad00ce120d08b2c812e11a6a9b014a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jun 2025 18:39:46 +0200 Subject: [PATCH 2313/2451] Add checking for supported features with the currently new supported features 'Atch', 'Shp' and 'Atr'. --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 33 ++++--- Penumbra/Api/IpcProviders.cs | 2 + .../Api/IpcTester/PluginStateIpcTester.cs | 15 ++- Penumbra/Mods/FeatureChecker.cs | 95 +++++++++++++++++++ Penumbra/Mods/Manager/ModDataEditor.cs | 11 +++ Penumbra/Mods/Manager/ModManager.cs | 15 ++- Penumbra/Mods/Mod.cs | 29 +++++- Penumbra/Mods/ModCreator.cs | 11 ++- Penumbra/Mods/ModMeta.cs | 23 ++++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 5 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 2 +- schemas/mod_meta-v3.json | 8 ++ 14 files changed, 216 insertions(+), 37 deletions(-) create mode 100644 Penumbra/Mods/FeatureChecker.cs diff --git a/Penumbra.Api b/Penumbra.Api index 14652039..ff7b3b40 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc +Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 38125627..7ca41324 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 9; + public const int FeatureVersion = 10; public void Dispose() { diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index d69df448..f74553d1 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -1,39 +1,38 @@ +using System.Collections.Frozen; using Newtonsoft.Json; using OtterGui.Services; using Penumbra.Communication; +using Penumbra.Mods; using Penumbra.Services; namespace Penumbra.Api.Api; -public class PluginStateApi : IPenumbraApiPluginState, IApiService +public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - - public PluginStateApi(Configuration config, CommunicatorService communicator) - { - _config = config; - _communicator = communicator; - } - public string GetModDirectory() - => _config.ModDirectory; + => config.ModDirectory; public string GetConfiguration() - => JsonConvert.SerializeObject(_config, Formatting.Indented); + => JsonConvert.SerializeObject(config, Formatting.Indented); public event Action? ModDirectoryChanged { - add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - remove => _communicator.ModDirectoryChanged.Unsubscribe(value!); + add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value!); } public bool GetEnabledState() - => _config.EnableMods; + => config.EnableMods; public event Action? EnabledChange { - add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - remove => _communicator.EnabledChanged.Unsubscribe(value!); + add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => communicator.EnabledChanged.Unsubscribe(value!); } + + public FrozenSet SupportedFeatures + => FeatureChecker.SupportedFeatures.ToFrozenSet(); + + public string[] CheckSupportedFeatures(IEnumerable requiredFeatures) + => requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray(); } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index f5a6c16d..7dcee375 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -80,6 +80,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), IpcSubscribers.EnabledChange.Provider(pi, api.PluginState), + IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState), + IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState), IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index df82033d..a1bf4fc4 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -5,6 +5,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester; public class PluginStateIpcTester : IUiService, IDisposable { - private readonly IDalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber ModDirectoryChanged; public readonly EventSubscriber Initialized; public readonly EventSubscriber Disposed; @@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable private readonly List _initializedList = []; private readonly List _disposedList = []; + private string _requiredFeatureString = string.Empty; + private string[] _requiredFeatures = []; + private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; private bool? _lastEnabledValue; @@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable EnabledChange.Dispose(); } + public void Draw() { using var _ = ImRaii.TreeNode("Plugin State"); if (!_) return; + if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString)) + _requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change"); ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); + IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features"); + ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke())); + + IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features"); + ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures))); + DrawConfigPopup(); IpcTester.DrawIntro(GetConfiguration.Label, "Configuration"); if (ImGui.Button("Get")) diff --git a/Penumbra/Mods/FeatureChecker.cs b/Penumbra/Mods/FeatureChecker.cs new file mode 100644 index 00000000..5800ef07 --- /dev/null +++ b/Penumbra/Mods/FeatureChecker.cs @@ -0,0 +1,95 @@ +using System.Collections.Frozen; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Text; +using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; + +namespace Penumbra.Mods; + +public static class FeatureChecker +{ + /// Manually setup supported features to exclude None and Invalid and not make something supported too early. + private static readonly FrozenDictionary SupportedFlags = new[] + { + FeatureFlags.Atch, + FeatureFlags.Shp, + FeatureFlags.Atr, + }.ToFrozenDictionary(f => f.ToString(), f => f); + + public static IReadOnlyCollection SupportedFeatures + => SupportedFlags.Keys; + + public static FeatureFlags ParseFlags(string modDirectory, string modName, IEnumerable features) + { + var featureFlags = FeatureFlags.None; + HashSet missingFeatures = []; + foreach (var feature in features) + { + if (SupportedFlags.TryGetValue(feature, out var featureFlag)) + featureFlags |= featureFlag; + else + missingFeatures.Add(feature); + } + + if (missingFeatures.Count > 0) + { + Penumbra.Messager.AddMessage(new Notification( + $"Please update Penumbra to use the mod {modName}{(modDirectory != modName ? $" at {modDirectory}" : string.Empty)}!\n\nLoading failed because it requires the unsupported feature{(missingFeatures.Count > 1 ? $"s\n\n\t[{string.Join("], [", missingFeatures)}]." : $" [{missingFeatures.First()}].")}", + NotificationType.Warning)); + return FeatureFlags.Invalid; + } + + return featureFlags; + } + + public static bool Supported(string features) + => SupportedFlags.ContainsKey(features); + + public static void DrawFeatureFlagInput(ModDataEditor editor, Mod mod, float width) + { + const int numButtons = 5; + var innerSpacing = ImGui.GetStyle().ItemInnerSpacing; + var size = new Vector2((width - (numButtons - 1) * innerSpacing.X) / numButtons, 0); + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + var textColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, innerSpacing) + .Push(ImGuiStyleVar.FrameBorderSize, 0); + using (var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()) + .Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.Text, textColor)) + { + foreach (var flag in SupportedFlags.Values) + { + if (mod.RequiredFeatures.HasFlag(flag)) + { + style.Push(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale); + color.Pop(2); + if (ImUtf8.Button($"{flag}", size)) + editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures & ~flag); + color.Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.Text, textColor); + style.Pop(); + } + else if (ImUtf8.Button($"{flag}", size)) + { + editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures | flag); + } + + ImGui.SameLine(); + } + } + + if (ImUtf8.ButtonEx("Compute"u8, "Compute the required features automatically from the used features."u8, size)) + editor.ChangeRequiredFeatures(mod, mod.ComputeRequiredFeatures()); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Clear"u8, "Clear all required features."u8, size)) + editor.ChangeRequiredFeatures(mod, FeatureFlags.None); + + ImGui.SameLine(); + ImUtf8.Text("Required Features"u8); + } +} diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 1349b525..fc4fdadc 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -26,6 +26,7 @@ public enum ModDataChangeType : ushort Image = 0x1000, DefaultChangedItems = 0x2000, PreferredChangedItems = 0x4000, + RequiredFeatures = 0x8000, } public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService @@ -95,6 +96,16 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Website = newWebsite; saveService.QueueSave(new ModMeta(mod)); communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); + } + + public void ChangeRequiredFeatures(Mod mod, FeatureFlags flags) + { + if (mod.RequiredFeatures == flags) + return; + + mod.RequiredFeatures = flags; + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.RequiredFeatures, mod, null); } public void ChangeModTag(Mod mod, int tagIdx, string newTag) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index bf1b6637..32dac049 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -143,9 +143,10 @@ public sealed class ModManager : ModStorage, IDisposable, IService _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); if (!Creator.ReloadMod(mod, true, false, out var metaChange)) { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); + if (mod.RequiredFeatures is not FeatureFlags.Invalid) + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); RemoveMod(mod); return; } @@ -251,12 +252,8 @@ public sealed class ModManager : ModStorage, IDisposable, IService { switch (type) { - case ModPathChangeType.Added: - SetNew(mod); - break; - case ModPathChangeType.Deleted: - SetKnown(mod); - break; + case ModPathChangeType.Added: SetNew(mod); break; + case ModPathChangeType.Deleted: SetKnown(mod); break; case ModPathChangeType.Moved: if (oldDirectory != null && newDirectory != null) DataEditor.MoveDataFile(oldDirectory, newDirectory); diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 99f86517..e262e8f1 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,4 +1,3 @@ -using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; using Penumbra.GameData.Data; @@ -12,6 +11,16 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; +[Flags] +public enum FeatureFlags : ulong +{ + None = 0, + Atch = 1ul << 0, + Shp = 1ul << 1, + Atr = 1ul << 2, + Invalid = 1ul << 62, +} + public sealed class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() @@ -57,6 +66,7 @@ public sealed class Mod : IMod public string Image { get; internal set; } = string.Empty; public IReadOnlyList ModTags { get; internal set; } = []; public HashSet DefaultPreferredItems { get; internal set; } = []; + public FeatureFlags RequiredFeatures { get; internal set; } = 0; // Local Data @@ -70,6 +80,23 @@ public sealed class Mod : IMod public readonly DefaultSubMod Default; public readonly List Groups = []; + /// Compute the required feature flags for this mod. + public FeatureFlags ComputeRequiredFeatures() + { + var flags = FeatureFlags.None; + foreach (var option in AllDataContainers) + { + if (option.Manipulations.Atch.Count > 0) + flags |= FeatureFlags.Atch; + if (option.Manipulations.Atr.Count > 0) + flags |= FeatureFlags.Atr; + if (option.Manipulations.Shp.Count > 0) + flags |= FeatureFlags.Shp; + } + + return flags; + } + public AppliedModData GetData(ModSettings? settings = null) { if (settings is not { Enabled: true }) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index df476a6f..1bb2a073 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -28,7 +28,8 @@ public partial class ModCreator( MetaFileManager metaFileManager, GamePathParser gamePathParser) : IService { - public readonly Configuration Config = config; + public const FeatureFlags SupportedFeatures = FeatureFlags.Atch | FeatureFlags.Shp | FeatureFlags.Atr; + public readonly Configuration Config = config; /// Creates directory and files necessary for a new mod without adding it to the manager. public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) @@ -74,7 +75,7 @@ public partial class ModCreator( return false; modDataChange = ModMeta.Load(dataEditor, this, mod); - if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) + if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0 || mod.RequiredFeatures is FeatureFlags.Invalid) return false; modDataChange |= ModLocalData.Load(dataEditor, mod); @@ -82,9 +83,9 @@ public partial class ModCreator( LoadAllGroups(mod); if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges); - else if (deleteDefaultMetaChanges) - ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); - + else if (deleteDefaultMetaChanges) + ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); + return true; } diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 1b104af4..b52eecf4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Structs; -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,6 +27,19 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, { nameof(Mod.DefaultPreferredItems), JToken.FromObject(mod.DefaultPreferredItems) }, }; + if (mod.RequiredFeatures is not FeatureFlags.None) + { + var features = mod.RequiredFeatures; + var array = new JArray(); + foreach (var flag in Enum.GetValues()) + { + if ((features & flag) is not FeatureFlags.None) + array.Add(flag.ToString()); + } + + jObject[nameof(Mod.RequiredFeatures)] = array; + } + using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); @@ -60,6 +72,8 @@ public readonly struct ModMeta(Mod mod) : ISavable var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values().Select(i => (CustomItemId)i).ToHashSet() ?? []; + var requiredFeatureArray = (json[nameof(Mod.RequiredFeatures)] as JArray)?.Values() ?? []; + var requiredFeatures = FeatureChecker.ParseFlags(mod.ModPath.Name, newName.Length > 0 ? newName : mod.Name.Length > 0 ? mod.Name : "Unknown", requiredFeatureArray!); ModDataChangeType changes = 0; if (mod.Name != newName) @@ -111,6 +125,13 @@ public readonly struct ModMeta(Mod mod) : ISavable editor.SaveService.ImmediateSave(new ModMeta(mod)); } + // Required features get checked during parsing, in which case the new required features signal invalid. + if (requiredFeatures != mod.RequiredFeatures) + { + changes |= ModDataChangeType.RequiredFeatures; + mod.RequiredFeatures = requiredFeatures; + } + changes |= ModLocalData.UpdateTags(mod, modTags, null); return changes; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1e6afa09..478ab892 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -64,6 +64,10 @@ public class ModPanelEditTab( messager.NotificationMessage(e.Message, NotificationType.Warning, false); } + UiHelpers.DefaultLineSpace(); + + FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X); + UiHelpers.DefaultLineSpace(); var sharedTagsEnabled = predefinedTagManager.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; @@ -76,6 +80,7 @@ public class ModPanelEditTab( predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, selector.Selected!); + UiHelpers.DefaultLineSpace(); addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 2de78c66..a7bfd49c 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -96,7 +96,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (set.All is { } value) { using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value); - ImUtf8.Text("All"u8); + ImUtf8.Text("All, "u8); ImGui.SameLine(0, 0); } diff --git a/schemas/mod_meta-v3.json b/schemas/mod_meta-v3.json index ed63a228..6fc68714 100644 --- a/schemas/mod_meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -52,6 +52,14 @@ "type": "integer" }, "uniqueItems": true + }, + "RequiredFeatures": { + "description": "A list of required features by name.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } }, "required": [ From 535694e9c8fd5377abcfbf32b5149d1111d4f18a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 7 Jun 2025 22:10:17 +0200 Subject: [PATCH 2314/2451] Update some BNPC Names. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 14b3641f..94076bf6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 14b3641f0fb520cb829ceb50fa7cb31255a1da4e +Subproject commit 94076bf6bba27c02e0adbafa1c5cc9c279a0b5df From 4c0e6d2a67d5964717c9057cbcec7c73eb9651bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 7 Jun 2025 22:10:59 +0200 Subject: [PATCH 2315/2451] Update Mod Merger for other group types. --- Penumbra/Mods/Editor/ModMerger.cs | 139 +++++++++++++++--- .../Manager/OptionEditor/ModOptionEditor.cs | 48 +++--- 2 files changed, 139 insertions(+), 48 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 88941edf..bb84173a 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -6,6 +6,7 @@ using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; @@ -44,13 +45,13 @@ public class ModMerger : IDisposable, IService public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { - _editor = editor; - _selection = selection; - _duplicates = duplicates; - _communicator = communicator; - _creator = creator; - _config = config; - _mods = mods; + _editor = editor; + _selection = selection; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _config = config; + _mods = mods; _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); } @@ -99,26 +100,117 @@ public class ModMerger : IDisposable, IService foreach (var originalGroup in MergeFromMod!.Groups) { - var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); - if (groupCreated) - _createdGroups.Add(groupIdx); - if (group == null) - throw new Exception( - $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); - - foreach (var originalOption in originalGroup.DataContainers) + switch (originalGroup.Type) { - var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); - if (optionCreated) + case GroupType.Single: + case GroupType.Multi: { - _createdOptions.Add(option!); - // #TODO DataContainer <> Option. - MergeIntoOption([originalOption], (IModDataContainer)option!, false); + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); + + if (groupCreated) + { + _createdGroups.Add(groupIdx); + group.Description = originalGroup.Description; + group.Image = originalGroup.Image; + group.DefaultSettings = originalGroup.DefaultSettings; + group.Page = originalGroup.Page; + group.Priority = originalGroup.Priority; + } + + foreach (var originalOption in originalGroup.Options) + { + var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.Name); + if (optionCreated) + { + _createdOptions.Add(option!); + MergeIntoOption([(IModDataContainer)originalOption], (IModDataContainer)option!, false); + option!.Description = originalOption.Description; + if (option is MultiSubMod multiOption) + multiOption.Priority = ((MultiSubMod)originalOption).Priority; + } + else + { + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); + } + } + + break; } - else + + case GroupType.Imc when originalGroup is ImcModGroup imc: { - throw new Exception( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); + var group = _editor.ImcEditor.AddModGroup(MergeToMod!, imc.Name, imc.Identifier, imc.DefaultEntry); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged."); + + group.AllVariants = imc.AllVariants; + group.OnlyAttributes = imc.OnlyAttributes; + group.Description = imc.Description; + group.Image = imc.Image; + group.DefaultSettings = imc.DefaultSettings; + group.Page = imc.Page; + group.Priority = imc.Priority; + foreach (var originalOption in imc.OptionData) + { + if (originalOption.IsDisableSubMod) + { + _editor.ImcEditor.ChangeCanBeDisabled(group, true); + var disable = group.OptionData.First(s => s.IsDisableSubMod); + disable.Description = originalOption.Description; + disable.Name = originalOption.Name; + continue; + } + + var newOption = _editor.ImcEditor.AddOption(group, originalOption.Name); + if (newOption is null) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating IMC option {originalOption.FullName}."); + + newOption.Description = originalOption.Description; + newOption.AttributeMask = originalOption.AttributeMask; + } + + break; + } + case GroupType.Combining when originalGroup is CombiningModGroup combining: + { + var group = _editor.CombiningEditor.AddModGroup(MergeToMod!, combining.Name); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged."); + + group.Description = combining.Description; + group.Image = combining.Image; + group.DefaultSettings = combining.DefaultSettings; + group.Page = combining.Page; + group.Priority = combining.Priority; + foreach (var originalOption in combining.OptionData) + { + var option = _editor.CombiningEditor.AddOption(group, originalOption.Name); + if (option is null) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating combining option {originalOption.FullName}."); + + option.Description = originalOption.Description; + } + + if (group.Data.Count != combining.Data.Count) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error caused data container counts in combining group {originalGroup.Name} to differ."); + + foreach (var (originalContainer, container) in combining.Data.Zip(group.Data)) + { + container.Name = originalContainer.Name; + MergeIntoOption([originalContainer], container, false); + } + + + break; } } } @@ -151,7 +243,6 @@ public class ModMerger : IDisposable, IService if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - // #TODO DataContainer <> Option. MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index 5c5ed4f1..d9d672e3 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -15,8 +15,8 @@ public abstract class ModOptionEditor( where TOption : class, IModOption { protected readonly CommunicatorService Communicator = communicator; - protected readonly SaveService SaveService = saveService; - protected readonly Configuration Config = config; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) @@ -25,7 +25,7 @@ public abstract class ModOptionEditor( return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, maxPriority); + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -92,8 +92,8 @@ public abstract class ModOptionEditor( /// Delete the given option from the given group. public void DeleteOption(TOption option) { - var mod = option.Mod; - var group = option.Group; + var mod = option.Mod; + var group = option.Group; var optionIdx = option.GetIndex(); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); RemoveOption((TGroup)group, optionIdx); @@ -104,7 +104,7 @@ public abstract class ModOptionEditor( /// Move an option inside the given option group. public void MoveOption(TOption option, int optionIdxTo) { - var idx = option.GetIndex(); + var idx = option.GetIndex(); var group = (TGroup)option.Group; if (!MoveOption(group, idx, optionIdxTo)) return; @@ -113,10 +113,10 @@ public abstract class ModOptionEditor( Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); protected abstract TOption? CloneOption(TGroup group, IModOption option); - protected abstract void RemoveOption(TGroup group, int optionIndex); - protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension @@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension { (requiresSaving, requiresReloading, wasPrepared) = type switch { - ModOptionChangeType.GroupRenamed => (true, false, false), - ModOptionChangeType.GroupAdded => (true, false, false), - ModOptionChangeType.GroupDeleted => (true, true, false), - ModOptionChangeType.GroupMoved => (true, false, false), - ModOptionChangeType.GroupTypeChanged => (true, true, true), - ModOptionChangeType.PriorityChanged => (true, true, true), - ModOptionChangeType.OptionAdded => (true, true, true), - ModOptionChangeType.OptionDeleted => (true, true, false), - ModOptionChangeType.OptionMoved => (true, false, false), - ModOptionChangeType.OptionFilesChanged => (false, true, false), - ModOptionChangeType.OptionFilesAdded => (false, true, true), - ModOptionChangeType.OptionSwapsChanged => (false, true, false), - ModOptionChangeType.OptionMetaChanged => (false, true, false), - ModOptionChangeType.DisplayChange => (false, false, false), + ModOptionChangeType.GroupRenamed => (true, false, false), + ModOptionChangeType.GroupAdded => (true, false, false), + ModOptionChangeType.GroupDeleted => (true, true, false), + ModOptionChangeType.GroupMoved => (true, false, false), + ModOptionChangeType.GroupTypeChanged => (true, true, true), + ModOptionChangeType.PriorityChanged => (true, true, true), + ModOptionChangeType.OptionAdded => (true, true, true), + ModOptionChangeType.OptionDeleted => (true, true, false), + ModOptionChangeType.OptionMoved => (true, false, false), + ModOptionChangeType.OptionFilesChanged => (false, true, false), + ModOptionChangeType.OptionFilesAdded => (false, true, true), + ModOptionChangeType.OptionSwapsChanged => (false, true, false), + ModOptionChangeType.OptionMetaChanged => (false, true, false), + ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false), - _ => (false, false, false), + _ => (false, false, false), }; } } From a16fd85a7ea1fb6d6190b5c264f70c78a2b13c63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jun 2025 11:28:12 +0200 Subject: [PATCH 2316/2451] Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once). --- Penumbra/Import/TexToolsImporter.Archives.cs | 4 ++ Penumbra/Import/TexToolsImporter.ModPack.cs | 1 + Penumbra/Import/Textures/TexFileParser.cs | 69 ++++++++++++++++++++ Penumbra/Mods/Manager/ModImportManager.cs | 1 - Penumbra/Services/MigrationManager.cs | 43 ++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 8166dea7..a80730bf 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Services; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; @@ -146,6 +147,9 @@ public partial class TexToolsImporter case ".mtrl": _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); break; + case ".tex": + _migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions); + break; default: reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); break; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 1c28aef2..fd9e50c0 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -259,6 +259,7 @@ public partial class TexToolsImporter { ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), ".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data), + ".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data), _ => data.Data, }; diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 220095c1..6a12a0dd 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -61,6 +61,75 @@ public static class TexFileParser return 13; } + public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize) + { + var width = (uint)header.Width; + var height = (uint)header.Height; + var format = header.Format.ToDXGI(); + var bits = format.BitsPerPixel(); + var totalSize = 80u; + size -= totalSize; + var minSize = format.IsCompressed() ? 4u : 1u; + for (var i = 0; i < 13; ++i) + { + var requiredSize = (uint)((long)width * height * bits / 8); + if (requiredSize > size) + { + newSize = totalSize; + if (header.MipCount != i) + { + Penumbra.Log.Debug( + $"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed."); + FixLodOffsets(ref header, i); + } + + return; + } + + if (header.OffsetToSurface[i] != totalSize) + { + Penumbra.Log.Debug( + $"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed."); + header.OffsetToSurface[i] = totalSize; + } + + if (width == minSize && height == minSize) + { + newSize = totalSize; + if (header.MipCount != i) + { + Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints."); + FixLodOffsets(ref header, i); + } + + return; + } + + totalSize += requiredSize; + size -= requiredSize; + width = Math.Max(width / 2, minSize); + height = Math.Max(height / 2, minSize); + } + + newSize = totalSize; + if (header.MipCount != 13) + { + Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed."); + FixLodOffsets(ref header, 13); + } + + void FixLodOffsets(ref TexFile.TexHeader header, int index) + { + header.MipCount = index; + if (header.LodOffset[2] >= header.MipCount) + header.LodOffset[2] = (byte)(header.MipCount - 1); + if (header.LodOffset[1] >= header.MipCount) + header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1); + for (++index; index < 13; ++index) + header.OffsetToSurface[index] = 0; + } + } + private static unsafe void CopyData(ScratchImage image, BinaryReader r) { fixed (byte* ptr = image.Pixels) diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 22cc0c86..bb282262 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -70,7 +70,6 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd _import = null; } - public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod) { if (!_modsToAdd.TryDequeue(out var directory)) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 2438c0ad..8db62e48 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,6 +1,10 @@ using Dalamud.Interface.ImGuiNotification; +using Lumina.Data.Files; using OtterGui.Classes; using OtterGui.Services; +using Lumina.Extensions; +using Penumbra.GameData.Files.Utility; +using Penumbra.Import.Textures; using SharpCompress.Common; using SharpCompress.Readers; using MdlFile = Penumbra.GameData.Files.MdlFile; @@ -296,6 +300,26 @@ public class MigrationManager(Configuration config) : IService } } + public void FixMipMaps(IReader reader, string directory, ExtractionOptions options) + { + var path = Path.Combine(directory, reader.Entry.Key!); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + var length = s.Position; + s.Seek(0, SeekOrigin.Begin); + var br = new BinaryReader(s, Encoding.UTF8, true); + var header = br.ReadStructure(); + br.Dispose(); + TexFileParser.FixMipOffsets(length, ref header, out var actualSize); + + s.Seek(0, SeekOrigin.Begin); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + f.Write(header); + f.Write(s.GetBuffer().AsSpan(80, (int)actualSize - 80)); + } + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. public byte[] MigrateTtmpModel(string path, byte[] data) { @@ -348,6 +372,25 @@ public class MigrationManager(Configuration config) : IService } } + public byte[] FixTtmpMipMaps(string path, byte[] data) + { + using var m = new MemoryStream(data); + var br = new BinaryReader(m, Encoding.UTF8, true); + var header = br.ReadStructure(); + br.Dispose(); + TexFileParser.FixMipOffsets(data.Length, ref header, out var actualSize); + if (actualSize == data.Length) + return data; + + var ret = new byte[actualSize]; + using var m2 = new MemoryStream(ret); + using var bw = new BinaryWriter(m2); + bw.Write(header); + bw.Write(data.AsSpan(80, (int)actualSize - 80)); + + return ret; + } + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) { From 973814b31b88c8c6176f1b12dc6a75a5d9439696 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jun 2025 11:36:32 +0200 Subject: [PATCH 2317/2451] Some more BNPCs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 94076bf6..a1252cdc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 94076bf6bba27c02e0adbafa1c5cc9c279a0b5df +Subproject commit a1252cdcab09cbf4c9694971f29523f7485c90bc From 3d056623840592ffa8504577169270dac00bdad7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 8 Jun 2025 09:38:30 +0000 Subject: [PATCH 2318/2451] [CI] Updating repo.json for testing_1.4.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e36176d5..a6cec268 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.2", + "TestingAssemblyVersion": "1.4.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a8c05fc6eea82e5beae224a1467a7f8255eda37d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Jun 2025 17:19:33 +0200 Subject: [PATCH 2319/2451] Make middle-mouse button handle temporary settings. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2dff19ab..8a383791 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -229,14 +229,27 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 13 Jun 2025 17:26:18 +0200 Subject: [PATCH 2320/2451] BNPCs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a1252cdc..10fdb025 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a1252cdcab09cbf4c9694971f29523f7485c90bc +Subproject commit 10fdb025436f7ea9f1f5e97635c19eee0578de7b From 1f4ec984b348c66f962899721f3f97d34e6c098c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Jun 2025 17:27:49 +0200 Subject: [PATCH 2321/2451] Use improved filesystem. --- OtterGui | 2 +- Penumbra/Api/Api/ModsApi.cs | 4 ++-- Penumbra/Mods/Manager/ModFileSystem.cs | 14 ++------------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/OtterGui b/OtterGui index 17a3ee57..78528f93 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 17a3ee5711ca30eb7f5b393dfb8136f0bce49b2b +Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index ace98f83..78c62953 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -112,7 +112,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.TryGetValue(mod, out var leaf)) return (PenumbraApiEc.ModMissing, string.Empty, false, false); var fullPath = leaf.FullName(); @@ -127,7 +127,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return PenumbraApiEc.InvalidArgument; if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.TryGetValue(mod, out var leaf)) return PenumbraApiEc.ModMissing; try diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 693db944..a5c46972 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -80,7 +80,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer // Update sort order when defaulted mod names change. private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) { - if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf)) + if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !TryGetValue(mod, out var leaf)) return; var old = oldName.FixName(); @@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer CreateDuplicateLeaf(parent, mod.Name.Text, mod); break; case ModPathChangeType.Deleted: - if (FindLeaf(mod, out var leaf)) + if (TryGetValue(mod, out var leaf)) Delete(leaf); break; @@ -124,16 +124,6 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer } } - // Search the entire filesystem for the leaf corresponding to a mod. - public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf) - { - leaf = Root.GetAllDescendants(ISortMode.Lexicographical) - .OfType() - .FirstOrDefault(l => l.Value == mod); - return leaf != null; - } - - // Used for saving and loading. private static string ModToIdentifier(Mod mod) => mod.ModPath.Name; From 1961b03d37f651a54a9746f245d6e0e4614188d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 15 Jun 2025 23:18:46 +0200 Subject: [PATCH 2322/2451] Fix issues with shapes and attributes with ID. --- .../Cache/ShapeAttributeHashSet.cs | 3 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 39 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index e50ceaa2..4c61bdd2 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -120,6 +120,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (TryGetValue((slot, id.Value), out var flags)) { + index *= 2; var newFlags = value switch { true => (flags | (1ul << index)) & ~(1ul << (index + 1)), @@ -137,7 +138,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (value is null) return false; - this[(slot, id.Value)] = 1ul << (index + (value.Value ? 0 : 1)); + this[(slot, id.Value)] = 1ul << (2 * index + (value.Value ? 0 : 1)); return true; } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index a7bfd49c..7b940cd0 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -136,26 +136,33 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { if ((flags & 3) is not 0) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), (flags & 2) is not 0); - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); - ImGui.SameLine(0, 0); + var enabled = (flags & 1) is 1; + + if (set[slot, GenderRace.Unknown] != enabled) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !enabled); + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } } else { - var currentFlags = flags >> 2; - var currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + var currentIndex = BitOperations.TrailingZeroCount(flags) / 2; + var currentFlags = flags >> (2 * currentIndex); while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) { - var value5 = (currentFlags & 1) is 1; - var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; - if (set[slot, gr] != value5) + var enabled = (currentFlags & 1) is 1; + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr] != enabled) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value5); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !enabled); ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); + ImGui.SameLine(0, 0); } - currentFlags >>= currentIndex * 2; - currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + currentFlags &= ~0x3u; + currentIndex += BitOperations.TrailingZeroCount(currentFlags) / 2; + currentFlags = flags >> (2 * currentIndex); } } } @@ -167,7 +174,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##shapes"u8, 6, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 7, ImGuiTableFlags.RowBg); if (!table) return; @@ -175,6 +182,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("ID"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); @@ -193,6 +201,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledShapeKeyIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); foreach (var ((shape, flag), idx) in model->ModelResourceHandle->Shapes.WithIndex()) @@ -211,6 +220,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } @@ -221,7 +231,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##attributes"u8, 6, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##attributes"u8, 7, ImGuiTableFlags.RowBg); if (!table) return; @@ -229,6 +239,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("ID"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); @@ -247,6 +258,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledAttributeIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); foreach (var ((attribute, flag), idx) in model->ModelResourceHandle->Attributes.WithIndex()) @@ -265,6 +277,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } From 3c20b541ce5f5cd93ef5b1038407badb6ea4680c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 15 Jun 2025 23:20:13 +0200 Subject: [PATCH 2323/2451] Make mousewheel-scrolling work for setting combos, also filters. --- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 76 +++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 3e165cb5..566ec02c 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -15,13 +15,55 @@ using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab.Groups; -public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService +public sealed class ModGroupDrawer : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; private bool _temporary; private bool _locked; private TemporaryModSettings? _tempSettings; private ModSettings? _settings; + private readonly SingleGroupCombo _combo; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + + public ModGroupDrawer(Configuration config, CollectionManager collectionManager) + { + _config = config; + _collectionManager = collectionManager; + _combo = new SingleGroupCombo(this); + } + + private sealed class SingleGroupCombo(ModGroupDrawer parent) + : FilterComboCache(() => _group!.Options, MouseWheelType.Control, Penumbra.Log) + { + private static IModGroup? _group; + private static int _groupIdx; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var option = _group!.Options[globalIdx]; + var ret = ImUtf8.Selectable(option.Name, globalIdx == CurrentSelectionIdx); + + if (option.Description.Length > 0) + ImUtf8.SelectableHelpMarker(option.Description); + + return ret; + } + + protected override string ToString(IModOption obj) + => obj.Name; + + public void Draw(IModGroup group, int groupIndex, int currentOption) + { + _group = group; + _groupIdx = groupIndex; + CurrentSelectionIdx = currentOption; + CurrentSelection = _group.Options[CurrentSelectionIdx]; + if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4, + ImGui.GetTextLineHeightWithSpacing())) + parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx)); + } + } public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { @@ -41,7 +83,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle switch (group.Behaviour) { - case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax: case GroupDrawBehaviour.MultiSelection: _blockGroupCache.Add((group, idx)); break; @@ -76,25 +118,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle using var id = ImUtf8.PushId(groupIdx); var selectedOption = setting.AsIndex; using var disabled = ImRaii.Disabled(_locked); - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - var options = group.Options; - using (var combo = ImUtf8.Combo(""u8, options[selectedOption].Name)) - { - if (combo) - for (var idx2 = 0; idx2 < options.Count; ++idx2) - { - id.Push(idx2); - var option = options[idx2]; - if (ImUtf8.Selectable(option.Name, idx2 == selectedOption)) - SetModSetting(group, groupIdx, Setting.Single(idx2)); - - if (option.Description.Length > 0) - ImUtf8.SelectableHelpMarker(option.Description); - - id.Pop(); - } - } - + _combo.Draw(group, groupIdx, selectedOption); ImGui.SameLine(); if (group.Description.Length > 0) ImUtf8.LabeledHelpMarker(group.Name, group.Description); @@ -195,7 +219,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (options.Count <= config.OptionGroupCollapsibleMin) + if (options.Count <= _config.OptionGroupCollapsibleMin) { draw(); } @@ -240,21 +264,21 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } private ModCollection Current - => collectionManager.Active.Current; + => _collectionManager.Active.Current; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) { - if (_temporary || config.DefaultTemporaryMode) + if (_temporary || _config.DefaultTemporaryMode) { _tempSettings ??= new TemporaryModSettings(group.Mod, _settings); _tempSettings!.ForceInherit = false; _tempSettings!.Settings[groupIdx] = setting; - collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + _collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); } else { - collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + _collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); } } } From 9fc572ba0cb198a3acbf0bd98a696e243c2bc433 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 15 Jun 2025 21:47:41 +0000 Subject: [PATCH 2324/2451] [CI] Updating repo.json for testing_1.4.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a6cec268..e12c3c9d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.3", + "TestingAssemblyVersion": "1.4.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 62e9dc164d624c13ef13d023e4501ee1e4f755eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 Jun 2025 14:49:12 +0200 Subject: [PATCH 2325/2451] Add support button. --- OtterGui | 2 +- Penumbra/Penumbra.cs | 6 ++++-- Penumbra/UI/Tabs/SettingsTab.cs | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index 78528f93..2c3c32bf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873 +Subproject commit 2c3c32bfb7057d7be7678f413122c2b1453050d5 diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 70636bbf..cf96c7f6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -229,10 +229,12 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n"); - sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); + sb.Append( + $"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); sb.Append($"> **`Custom Shapes Enabled: `** {_config.EnableCustomShapes}\n"); sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); - sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); + sb.Append( + $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4bbdf2a9..c1aea97c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; +using OtterGuiInternal.Enums; using Penumbra.Api; using Penumbra.Collections; using Penumbra.Interop.Hooks.PostProcessing; @@ -20,6 +21,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; +using ImGuiId = OtterGuiInternal.Enums.ImGuiId; namespace Penumbra.UI.Tabs; @@ -113,6 +115,7 @@ public class SettingsTab : ITab, IUiService DrawRootFolder(); DrawDirectoryButtons(); ImGui.NewLine(); + ImGui.NewLine(); DrawGeneralSettings(); _migrationDrawer.Draw(); @@ -761,8 +764,9 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); - Checkbox("Enable Custom Shape and Attribute Support", "Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.", - _config.EnableCustomShapes, _attributeHook.SetState); + Checkbox("Enable Custom Shape and Attribute Support", + "Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.", + _config.EnableCustomShapes, _attributeHook.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); @@ -1050,6 +1054,9 @@ public class SettingsTab : ITab, IUiService ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); + + ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); + CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0)); } private void DrawPredefinedTagsSection() From 30e3cd1f383ce9c9c2a1e2927518993d9be9ad1c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 4 Jul 2025 19:41:31 +0200 Subject: [PATCH 2326/2451] Add OS thread ID info to the Resource Logger --- Penumbra/Interop/ProcessThreadApi.cs | 7 +++++++ Penumbra/UI/ResourceWatcher/Record.cs | 9 +++++++++ .../UI/ResourceWatcher/ResourceWatcherTable.cs | 18 +++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Interop/ProcessThreadApi.cs diff --git a/Penumbra/Interop/ProcessThreadApi.cs b/Penumbra/Interop/ProcessThreadApi.cs new file mode 100644 index 00000000..5ee213d9 --- /dev/null +++ b/Penumbra/Interop/ProcessThreadApi.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Interop; + +public static partial class ProcessThreadApi +{ + [LibraryImport("kernel32.dll")] + public static partial uint GetCurrentThreadId(); +} diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index b8730750..ba718bc9 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Enums; +using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; @@ -34,6 +35,7 @@ internal unsafe struct Record public OptionalBool ReturnValue; public OptionalBool CustomLoad; public LoadState LoadState; + public uint OsThreadId; public static Record CreateRequest(CiByteString path, bool sync) @@ -54,6 +56,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = LoadState.None, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateRequest(CiByteString path, bool sync, FullPath fullPath, ResolveData resolve) @@ -74,6 +77,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = LoadState.None, Crc64 = fullPath.Crc64, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) @@ -96,6 +100,7 @@ internal unsafe struct Record AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; } @@ -118,6 +123,7 @@ internal unsafe struct Record AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, Crc64 = path.Crc64, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateDestruction(ResourceHandle* handle) @@ -140,6 +146,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; } @@ -161,6 +168,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan additionalData) @@ -181,6 +189,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan additionalData) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index a58d74d1..009da842 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -30,7 +30,8 @@ internal sealed class ResourceWatcherTable : Table new LoadStateColumn { Label = "State" }, new RefCountColumn { Label = "#Ref" }, new DateColumn { Label = "Time" }, - new Crc64Column { Label = "Crc64" } + new Crc64Column { Label = "Crc64" }, + new OsThreadColumn { Label = "TID" } ) { } @@ -453,4 +454,19 @@ internal sealed class ResourceWatcherTable : Table public override int Compare(Record lhs, Record rhs) => lhs.RefCount.CompareTo(rhs.RefCount); } + + private sealed class OsThreadColumn : ColumnString + { + public override float Width + => 60 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.OsThreadId.ToString(); + + public override void DrawColumn(Record item, int _) + => ImGuiUtil.RightAlign(ToName(item)); + + public override int Compare(Record lhs, Record rhs) + => lhs.OsThreadId.CompareTo(rhs.OsThreadId); + } } From a97d9e49531ace985eeef1c305bdd123b11dfa38 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 5 Jul 2025 04:37:37 +0200 Subject: [PATCH 2327/2451] Add Human skin material handling --- .../Hooks/Resources/ResolvePathHooksBase.cs | 40 ++++++++----- .../Interop/ResourceTree/ResolveContext.cs | 9 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 12 +++- Penumbra/Interop/Structs/StructExtensions.cs | 60 +++++++++++-------- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8a45ec2c..85fb1098 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -35,6 +35,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveMPapPathHook; private readonly Hook _resolveMdlPathHook; private readonly Hook _resolveMtrlPathHook; + private readonly Hook _resolveSkinMtrlPathHook; private readonly Hook _resolvePapPathHook; private readonly Hook _resolveKdbPathHook; private readonly Hook _resolvePhybPathHook; @@ -52,22 +53,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable { _parent = parent; // @formatter:off - _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); - _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); - _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); - _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); - _resolveKdbPathHook = Create($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); - _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); - _resolveBnmbPathHook = Create($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); - _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); - _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); - _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); - _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); - _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); - _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); - _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); - _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); + _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); + _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); + _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); + _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); + _resolveKdbPathHook = Create($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); + _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); + _resolveBnmbPathHook = Create($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); + _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); + _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); + _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); + _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); + _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); + _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); + _resolveSkinMtrlPathHook = Create($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl); + _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); + _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); // @formatter:on @@ -83,6 +85,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Enable(); _resolveMdlPathHook.Enable(); _resolveMtrlPathHook.Enable(); + _resolveSkinMtrlPathHook.Enable(); _resolvePapPathHook.Enable(); _resolveKdbPathHook.Enable(); _resolvePhybPathHook.Enable(); @@ -103,6 +106,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Disable(); _resolveMdlPathHook.Disable(); _resolveMtrlPathHook.Disable(); + _resolveSkinMtrlPathHook.Disable(); _resolvePapPathHook.Disable(); _resolveKdbPathHook.Disable(); _resolvePhybPathHook.Disable(); @@ -123,6 +127,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Dispose(); _resolveMdlPathHook.Dispose(); _resolveMtrlPathHook.Dispose(); + _resolveSkinMtrlPathHook.Dispose(); _resolvePapPathHook.Dispose(); _resolveKdbPathHook.Dispose(); _resolvePhybPathHook.Dispose(); @@ -153,6 +158,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName) => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); + private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) + => ResolvePath(drawObject, _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 013d7db7..64a91302 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); } - public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) + public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, + MaterialResourceHandle* skinMtrlHandle, ResourceHandle* mpapHandle) { if (mdl is null || mdl->ModelResourceHandle is null) return null; @@ -218,6 +219,12 @@ internal unsafe partial record ResolveContext( } } + if (skinMtrlHandle is not null + && Utf8GamePath.FromByteString(CharacterBase->ResolveSkinMtrlPathAsByteString(SlotIndex), out var skinMtrlPath) + && CreateNodeFromMaterial(skinMtrlHandle->Material, skinMtrlPath) is + { } skinMaaterialNode) + node.Children.Add(skinMaaterialNode); + if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode) node.Children.Add(decalNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 7be8694a..97a926ad 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -73,6 +73,12 @@ public class ResourceTree( // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) + var skinMtrlArray = modelType switch + { + ModelType.Human => new ReadOnlySpan>((MaterialResourceHandle**)((nint)model + 0xB48), 5), + _ => [], + }; var decalArray = modelType switch { ModelType.Human => human->SlotDecalsSpan, @@ -108,7 +114,8 @@ public class ResourceTree( var mdl = model->Models[i]; if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, - i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode) + i < skinMtrlArray.Length ? skinMtrlArray[(int)i].Value : null, i < mpapArray.Length ? mpapArray[(int)i].Value : null) is + { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Model #{i}"; @@ -166,7 +173,8 @@ public class ResourceTree( } var mdl = subObject->Models[i]; - if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode) + if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, null, i < mpapArray.Length ? mpapArray[i].Value : null) is + { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 03b4cf36..62dca02e 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -10,28 +10,36 @@ internal static class StructExtensions public static CiByteString AsByteString(in this StdString str) => CiByteString.FromSpanUnsafe(str.AsSpan(), true); - public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); - } - - public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); + public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); + public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); } - public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) - { - var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); + public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); + } + + public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) + { + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); + } + + public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) + var vf91 = (delegate* unmanaged)((nint*)character.VirtualTable)[91]; + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(vf91((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, slotIndex)); } public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) @@ -40,16 +48,16 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId)); } - public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); + public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); } - public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); + public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) From 278bf43b29809ff4c0657921311f8581c820b9b8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 5 Jul 2025 05:20:24 +0200 Subject: [PATCH 2328/2451] ClientStructs-ify ResourceTree stuff --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 97a926ad..e7c4b11b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -70,8 +70,7 @@ public class ResourceTree( var genericContext = globalContext.CreateContext(model); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); + var mpapArrayPtr = model->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) var skinMtrlArray = modelType switch @@ -124,8 +123,7 @@ public class ResourceTree( } AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940)); + AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton); AddWeapons(globalContext, model); @@ -156,8 +154,7 @@ public class ResourceTree( var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948); + var mpapArrayPtr = subObject->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; for (var i = 0; i < subObject->SlotCount; ++i) @@ -184,8 +181,7 @@ public class ResourceTree( AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), + AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton, $"Weapon #{weaponIndex}, "); ++weaponIndex; @@ -263,7 +259,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1475) var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { From a953febfba522579b338d2c8015e3dcfd6315168 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Jul 2025 21:59:20 +0200 Subject: [PATCH 2329/2451] Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. --- Penumbra/Meta/ShapeAttributeManager.cs | 64 +++++++++++++++++++++++++ Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 49 ++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index a742806f..16901741 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -5,6 +6,8 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; @@ -58,11 +61,72 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable _ids[(int)_modelIndex] = model.GetModelId(_modelIndex); CheckShapes(collection.MetaCache!.Shp); CheckAttributes(collection.MetaCache!.Atr); + if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears) + AccessoryImcCheck(model); } UpdateDefaultMasks(model, collection.MetaCache!.Shp); } + private void AccessoryImcCheck(Model model) + { + var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex)); + + Span attr = + [ + (byte)'a', + (byte)'t', + (byte)'r', + (byte)'_', + AccessoryByte(_modelIndex), + (byte)'v', + (byte)'_', + (byte)'a', + 0, + ]; + for (var i = 1; i < 10; ++i) + { + var flag = (ushort)(1 << i); + if ((imcMask & flag) is not 0) + continue; + + attr[^2] = (byte)('a' + i); + + foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes) + { + if (!EqualAttribute(attr, attribute.Value)) + continue; + + _model->EnabledAttributeIndexMask &= ~(1u << index); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private static bool EqualAttribute(Span needle, byte* haystack) + { + foreach (var character in needle) + { + if (*haystack++ != character) + return false; + } + + return true; + } + + private static byte AccessoryByte(HumanSlot slot) + => slot switch + { + HumanSlot.Head => (byte)'m', + HumanSlot.Ears => (byte)'e', + HumanSlot.Neck => (byte)'n', + HumanSlot.Wrists => (byte)'w', + HumanSlot.RFinger => (byte)'r', + HumanSlot.LFinger => (byte)'r', + _ => 0, + }; + private void CheckAttributes(AtrCache attributeCache) { if (attributeCache.DisabledCount is 0) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5c67df52..216b5841 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -234,9 +234,56 @@ public static class EquipmentSwap mdl.ChildSwaps.Add(mtrl); } + FixAttributes(mdl, slotFrom, slotTo); + return mdl; } + private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo) + { + if (slotFrom == slotTo) + return; + + var needle = slotTo switch + { + EquipSlot.Head => "atr_mv_", + EquipSlot.Ears => "atr_ev_", + EquipSlot.Neck => "atr_nv_", + EquipSlot.Wrists => "atr_wv_", + EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_", + _ => string.Empty, + }; + + var replacement = slotFrom switch + { + EquipSlot.Head => 'm', + EquipSlot.Ears => 'e', + EquipSlot.Neck => 'n', + EquipSlot.Wrists => 'w', + EquipSlot.RFinger or EquipSlot.LFinger => 'r', + _ => 'm', + }; + + var attributes = swap.AsMdl()!.Attributes; + for (var i = 0; i < attributes.Length; ++i) + { + if (FixAttribute(ref attributes[i], needle, replacement)) + swap.DataWasChanged = true; + } + } + + private static unsafe bool FixAttribute(ref string attribute, string from, char to) + { + if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j') + return false; + + Span stack = stackalloc char[attribute.Length]; + attribute.CopyTo(stack); + stack[4] = to; + attribute = new string(stack); + return true; + } + private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant) { slot = i.Type.ToSlot(); @@ -399,7 +446,7 @@ public static class EquipmentSwap return null; var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); - var pathTo = $"{folderTo}{fileName}"; + var pathTo = $"{folderTo}{fileName}"; var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); From 49a6d935f3d0bd149442003f727f0f8a85b07019 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 5 Jul 2025 20:11:28 +0000 Subject: [PATCH 2330/2451] [CI] Updating repo.json for testing_1.4.0.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e12c3c9d..f0bf9a4a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.4", + "TestingAssemblyVersion": "1.4.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 140d150bb4b1c64051673811aa8cf349b2c56c80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Jul 2025 17:08:46 +0200 Subject: [PATCH 2331/2451] Fix character sound data. --- Penumbra/Interop/GameState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 95cef468..32b45b7e 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -60,7 +60,7 @@ public class GameState : IService private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); public ResolveData SoundData - => _animationLoadData.Value; + => _characterSoundData.Value; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public ResolveData SetSoundData(ResolveData data) From 00c02fd16e641eb40933010c48ea1e32602213b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Jul 2025 17:09:07 +0200 Subject: [PATCH 2332/2451] Fix tex file migration for small textures. --- Penumbra/Import/Textures/TexFileParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 6a12a0dd..04bbf5d8 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -95,7 +95,8 @@ public static class TexFileParser if (width == minSize && height == minSize) { - newSize = totalSize; + ++i; + newSize = totalSize + requiredSize; if (header.MipCount != i) { Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints."); From a4a6283e7b007a5d4f019ee12565887eabc235ad Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 14 Jul 2025 15:12:06 +0000 Subject: [PATCH 2333/2451] [CI] Updating repo.json for testing_1.4.0.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f0bf9a4a..af368d75 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.5", + "TestingAssemblyVersion": "1.4.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a9546e31eed28555f00bd724f9c00106a40e9da3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:05:27 +0200 Subject: [PATCH 2334/2451] Update packages. --- .../Import/Models/Import/VertexAttribute.cs | 2 +- Penumbra/Penumbra.csproj | 10 ++--- Penumbra/packages.lock.json | 44 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index a1c3246b..155fa833 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -319,7 +319,7 @@ public class VertexAttribute var normals = normalAccessor.AsVector3Array(); var tangents = accessors.TryGetValue("TANGENT", out var accessor) - ? accessor.AsVector4Array() + ? accessor.AsVector4Array().ToArray() : CalculateTangents(accessors, indices, normals, notifier); if (tangents == null) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f668f775..c61692f4 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -57,11 +57,11 @@ - - - - - + + + + + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 4a162f8f..778f776e 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -19,9 +19,9 @@ }, "PeNet": { "type": "Direct", - "requested": "[4.1.1, )", - "resolved": "4.1.1", - "contentHash": "TiRyOVcg1Bh2FyP6Dm2NEiYzemSlQderhaxuH3XWNyTYsnHrm1n/xvoTftgMwsWD4C/3kTqJw93oZOvHojJfKg==", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "XSd1PUwWo5uI8iqVHk7Mm02RT1bjndtAYsaRwLmdYZoHOAmb4ohkvRcZiqxJ7iLfBfdiwm+PHKQIMqDmOavBtw==", "dependencies": { "PeNet.Asn1": "2.0.1", "System.Security.Cryptography.Pkcs": "8.0.1" @@ -29,34 +29,34 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.39.0, )", - "resolved": "0.39.0", - "contentHash": "0esqIUDlg68Z7+Weuge4QzEvNtawUO4obTJFL7xuf4DBHMxVRr+wbNgiX9arMrj3kGXQSvLe0zbZG3oxpkwJOA==", + "requested": "[0.40.0, )", + "resolved": "0.40.0", + "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", "dependencies": { "System.Buffers": "4.6.0", - "ZstdSharp.Port": "0.8.4" + "ZstdSharp.Port": "0.8.5" } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "su+Flcg2g6GgOIgulRGBDMHA6zY5NBx6NYH1Ayd6iBbSbwspHsN2VQgZfANgJy92cBf7qtpjC0uMiShbO+TEEg==" + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "HNHKPqaHXm7R1nlXZ764K5UI02IeDOQ5DQKLjwYUVNTsSW27jJpw+wLGQx6ZFoiFYqUlyZjmsu+WfEak2GmJAg==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vkEuf8ch76NNgZXU/3zoXTIXRO0o14H3aRoSFzcuUQb0PTxvV6jEfmWkUVO6JtLDuFCIimqZaf3hdxr32ltpfQ==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "piQKk7PH2pSWQSQmCSd8cYPaDtAy/ppAD+Mrh2RUhhHI8awl81HqqLyAauwQhJwea3LNaiJ6f4ehZuOGk89TlA==", "dependencies": { - "SharpGLTF.Runtime": "1.0.3" + "SharpGLTF.Runtime": "1.0.5" } }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.7, )", - "resolved": "3.1.7", - "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" + "requested": "[3.1.11, )", + "resolved": "3.1.11", + "contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" }, "JetBrains.Annotations": { "type": "Transitive", @@ -83,10 +83,10 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "W0bg2WyXlcSAJVu153hNUNm+BU4RP46yLwGD4099hSm8dsXG/H+J95PBoLJbIq8KGVkUWvfM0+XWHoEkCyd50A==", + "resolved": "1.0.5", + "contentHash": "EVP32k4LqERxSVICiupT8xQvhHSHJCiXajBjNpqdfdajtREHayuVhH0Jmk6uSjTLX8/IIH9XfT34sw3TwvCziw==", "dependencies": { - "SharpGLTF.Core": "1.0.3" + "SharpGLTF.Core": "1.0.5" } }, "System.Buffers": { @@ -114,8 +114,8 @@ }, "ZstdSharp.Port": { "type": "Transitive", - "resolved": "0.8.4", - "contentHash": "eieSXq3kakCUXbgdxkKaRqWS6hF0KBJcqok9LlDCs60GOyrynLvPOcQ0pRw7shdPF7lh/VepJ9cP9n9HHc759g==" + "resolved": "0.8.5", + "contentHash": "TR4j17WeVSEb3ncgL2NqlXEqcy04I+Kk9CaebNDplUeL8XOgjkZ7fP4Wg4grBdPLIqsV86p2QaXTkZoRMVOsew==" }, "ottergui": { "type": "Project", From 012052daa0c5cf81e11c32bb27ee220533000577 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:06:03 +0200 Subject: [PATCH 2335/2451] Change behavior for directory names. --- Penumbra/Mods/Editor/ModNormalizer.cs | 4 ++-- .../Mods/SubMods/CombinedDataContainer.cs | 19 +++++++++++++++++++ Penumbra/Mods/SubMods/DefaultSubMod.cs | 3 +++ Penumbra/Mods/SubMods/IModDataContainer.cs | 1 + Penumbra/Mods/SubMods/OptionSubMod.cs | 3 +++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 527dbf7c..df1528f6 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -76,7 +76,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ else { var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport); - var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport); + var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetDirectoryName(), config.ReplaceNonAsciiOnImport); containers[container] = optionDir.FullName; } } @@ -286,7 +286,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { - var name = option.GetName(); + var name = option.GetDirectoryName(); var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true); newDict.Clear(); diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index b467c360..bfca2afd 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -48,6 +48,25 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer return sb.ToString(0, sb.Length - 3); } + public unsafe string GetDirectoryName() + { + if (Name.Length > 0) + return Name; + + var index = GetDataIndex(); + if (index == 0) + return "None"; + + var text = stackalloc char[IModGroup.MaxCombiningOptions].Slice(0, Group.Options.Count); + for (var i = 0; i < Group.Options.Count; ++i) + { + text[Group.Options.Count - 1 - i] = (index & 1) is 0 ? '0' : '1'; + index >>= 1; + } + + return new string(text); + } + public string GetFullName() => $"{Group.Name}: {GetName()}"; diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 3840468f..3282f518 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -27,6 +27,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public string GetName() => FullName; + public string GetDirectoryName() + => GetName(); + public string GetFullName() => FullName; diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 1a89ec17..92ccf7e1 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -15,6 +15,7 @@ public interface IModDataContainer public MetaDictionary Manipulations { get; set; } public string GetName(); + public string GetDirectoryName(); public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 9044350d..aa3fed8f 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -41,6 +41,9 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public string GetName() => Name; + public string GetDirectoryName() + => GetName(); + public string GetFullName() => FullName; From dc93eba34c322b20dcbbc0164ad2f2379f0909e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:06:25 +0200 Subject: [PATCH 2336/2451] Add initial complex group things. --- Penumbra/Mods/Groups/ComplexModGroup.cs | 180 ++++++++++++++++++ Penumbra/Mods/Groups/IModGroup.cs | 2 + Penumbra/Mods/SubMods/ComplexDataContainer.cs | 46 +++++ Penumbra/Mods/SubMods/ComplexSubMod.cs | 39 ++++ Penumbra/Mods/SubMods/MaskedSetting.cs | 27 +++ 5 files changed, 294 insertions(+) create mode 100644 Penumbra/Mods/Groups/ComplexModGroup.cs create mode 100644 Penumbra/Mods/SubMods/ComplexDataContainer.cs create mode 100644 Penumbra/Mods/SubMods/ComplexSubMod.cs create mode 100644 Penumbra/Mods/SubMods/MaskedSetting.cs diff --git a/Penumbra/Mods/Groups/ComplexModGroup.cs b/Penumbra/Mods/Groups/ComplexModGroup.cs new file mode 100644 index 00000000..435bc253 --- /dev/null +++ b/Penumbra/Mods/Groups/ComplexModGroup.cs @@ -0,0 +1,180 @@ +using Dalamud.Interface.ImGuiNotification; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using OtterGui.Extensions; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; +using Penumbra.Util; + +namespace Penumbra.Mods.Groups; + +public sealed class ComplexModGroup(Mod mod) : IModGroup +{ + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; + + public GroupType Type + => GroupType.Complex; + + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.Complex; + + public ModPriority Priority { get; set; } + public int Page { get; set; } + public Setting DefaultSettings { get; set; } + + public readonly List Options = []; + public readonly List Containers = []; + + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => throw new NotImplementedException(); + + public IModOption? AddOption(string name, string description = "") + => throw new NotImplementedException(); + + IReadOnlyList IModGroup.Options + => Options; + + IReadOnlyList IModGroup.DataContainers + => Containers; + + public bool IsOption + => Options.Count > 0; + + public int GetIndex() + => ModGroup.GetIndex(this); + + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => throw new NotImplementedException(); + + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) + { + foreach (var container in Containers.Where(c => c.Association.IsEnabled(setting))) + SubMod.AddContainerTo(container, redirections, manipulations); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in Containers) + identifier.AddChangedItems(container, changedItems); + } + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << Options.Count) - 1)); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in Options) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + if (!option.Conditions.IsZero) + { + jWriter.WritePropertyName("ConditionMask"); + jWriter.WriteValue(option.Conditions.Mask.Value); + jWriter.WritePropertyName("ConditionValue"); + jWriter.WriteValue(option.Conditions.Value.Value); + } + + if (option.Indentation > 0) + { + jWriter.WritePropertyName("Indentation"); + jWriter.WriteValue(option.Indentation); + } + + if (option.SubGroupLabel.Length > 0) + { + jWriter.WritePropertyName("SubGroup"); + jWriter.WriteValue(option.SubGroupLabel); + } + + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + + jWriter.WritePropertyName("Containers"); + jWriter.WriteStartArray(); + foreach (var container in Containers) + { + jWriter.WriteStartObject(); + if (container.Name.Length > 0) + { + jWriter.WritePropertyName("Name"); + jWriter.WriteValue(container.Name); + } + + if (!container.Association.IsZero) + { + jWriter.WritePropertyName("AssociationMask"); + jWriter.WriteValue(container.Association.Mask.Value); + + jWriter.WritePropertyName("AssociationValue"); + jWriter.WriteValue(container.Association.Value.Value); + } + + SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => ModGroup.GetCountsBase(this); + + public static ComplexModGroup? Load(Mod mod, JObject json) + { + var ret = new ComplexModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) + { + if (ret.Options.Count == IModGroup.MaxComplexOptions) + { + Penumbra.Messager.NotificationMessage( + $"Complex Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxComplexOptions} options, ignoring excessive options.", + NotificationType.Warning); + break; + } + + var subMod = new ComplexSubMod(ret, child); + ret.Options.Add(subMod); + } + + // Fix up conditions: No condition on itself. + foreach (var (option, index) in ret.Options.WithIndex()) + { + option.Conditions = option.Conditions.Limit(ret.Options.Count); + option.Conditions = new MaskedSetting(option.Conditions.Mask.SetBit(index, false), option.Conditions.Value); + } + + var containers = json["Containers"]; + if (containers != null) + foreach (var child in containers.Children()) + { + var container = new ComplexDataContainer(ret, child); + container.Association = container.Association.Limit(ret.Options.Count); + ret.Containers.Add(container); + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + + return ret; + } +} diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index cc961b0f..98f62862 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -18,11 +18,13 @@ public enum GroupDrawBehaviour { SingleSelection, MultiSelection, + Complex, } public interface IModGroup { public const int MaxMultiOptions = 32; + public const int MaxComplexOptions = MaxMultiOptions; public const int MaxCombiningOptions = 8; public Mod Mod { get; } diff --git a/Penumbra/Mods/SubMods/ComplexDataContainer.cs b/Penumbra/Mods/SubMods/ComplexDataContainer.cs new file mode 100644 index 00000000..0f0fdef8 --- /dev/null +++ b/Penumbra/Mods/SubMods/ComplexDataContainer.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Extensions; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public sealed class ComplexDataContainer(ComplexModGroup group) : IModDataContainer +{ + public IMod Mod + => Group.Mod; + + public IModGroup Group { get; } = group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); + + public MaskedSetting Association = MaskedSetting.Zero; + + public string Name { get; set; } = string.Empty; + + public string GetName() + => Name.Length > 0 ? Name : $"Container {Group.DataContainers.IndexOf(this)}"; + + public string GetDirectoryName() + => Name.Length > 0 ? Name : $"{Group.DataContainers.IndexOf(this)}"; + + public string GetFullName() + => $"{Group.Name}: {GetName()}"; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), Group.DataContainers.IndexOf(this)); + + public ComplexDataContainer(ComplexModGroup group, JToken json) + : this(group) + { + SubMod.LoadDataContainer(json, this, group.Mod.ModPath); + var mask = json["AssociationMask"]?.ToObject() ?? 0; + var value = json["AssociationMask"]?.ToObject() ?? 0; + Association = new MaskedSetting(mask, value); + Name = json["Name"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/ComplexSubMod.cs b/Penumbra/Mods/SubMods/ComplexSubMod.cs new file mode 100644 index 00000000..3eea6f15 --- /dev/null +++ b/Penumbra/Mods/SubMods/ComplexSubMod.cs @@ -0,0 +1,39 @@ +using ImSharp; +using Newtonsoft.Json.Linq; +using OtterGui.Extensions; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.SubMods; + +public sealed class ComplexSubMod(ComplexModGroup group) : IModOption +{ + public Mod Mod + => group.Mod; + + public IModGroup Group { get; } = group; + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public MaskedSetting Conditions = MaskedSetting.Zero; + public int Indentation = 0; + public string SubGroupLabel = string.Empty; + + public string Description { get; set; } = string.Empty; + + public int GetIndex() + => Group.Options.IndexOf(this); + + public ComplexSubMod(ComplexModGroup group, JToken json) + : this(group) + { + SubMod.LoadOptionData(json, this); + var mask = json["ConditionMask"]?.ToObject() ?? 0; + var value = json["ConditionMask"]?.ToObject() ?? 0; + Conditions = new MaskedSetting(mask, value); + Indentation = json["Indentation"]?.ToObject() ?? 0; + SubGroupLabel = json["SubGroup"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/MaskedSetting.cs b/Penumbra/Mods/SubMods/MaskedSetting.cs new file mode 100644 index 00000000..75bb46c2 --- /dev/null +++ b/Penumbra/Mods/SubMods/MaskedSetting.cs @@ -0,0 +1,27 @@ +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.SubMods; + +public readonly struct MaskedSetting(Setting mask, Setting value) +{ + public const int MaxSettings = IModGroup.MaxMultiOptions; + public static readonly MaskedSetting Zero = new(Setting.Zero, Setting.Zero); + public static readonly MaskedSetting FullMask = new(Setting.AllBits(IModGroup.MaxComplexOptions), Setting.Zero); + + public readonly Setting Mask = mask; + public readonly Setting Value = new(value.Value & mask.Value); + + public MaskedSetting(ulong mask, ulong value) + : this(new Setting(mask), new Setting(value)) + { } + + public MaskedSetting Limit(int numOptions) + => new(Mask.Value & Setting.AllBits(numOptions).Value, Value.Value); + + public bool IsZero + => Mask.Value is 0; + + public bool IsEnabled(Setting input) + => (input.Value & Mask.Value) == Value.Value; +} From baca3cdec21a705f231d7d45edfcea8c17735602 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:08:09 +0200 Subject: [PATCH 2337/2451] Update Libs. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 2c3c32bf..ad3bafa4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2c3c32bfb7057d7be7678f413122c2b1453050d5 +Subproject commit ad3bafa4f0af5b69833347f8c8ff2e178645e2f0 diff --git a/Penumbra.GameData b/Penumbra.GameData index 10fdb025..82b44672 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 10fdb025436f7ea9f1f5e97635c19eee0578de7b +Subproject commit 82b446721a9b9c99d2470c54ad49fe19ff4987e3 From 8527bfa29c6bc722d9dad579a1bf719700b9ccac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:13:35 +0200 Subject: [PATCH 2338/2451] Fix missing updates for OtterGui. --- Penumbra/Mods/Manager/ModFileSystem.cs | 16 ++++++++-------- Penumbra/Mods/SubMods/ComplexSubMod.cs | 2 -- Penumbra/UI/Tabs/SettingsTab.cs | 6 +++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index a5c46972..20a78995 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -37,11 +37,11 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer public struct ImportDate : ISortMode { - public string Name - => "Import Date (Older First)"; + public ReadOnlySpan Name + => "Import Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate)); @@ -49,11 +49,11 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer public struct InverseImportDate : ISortMode { - public string Name - => "Import Date (Newer First)"; + public ReadOnlySpan Name + => "Import Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate)); diff --git a/Penumbra/Mods/SubMods/ComplexSubMod.cs b/Penumbra/Mods/SubMods/ComplexSubMod.cs index 3eea6f15..7c189170 100644 --- a/Penumbra/Mods/SubMods/ComplexSubMod.cs +++ b/Penumbra/Mods/SubMods/ComplexSubMod.cs @@ -1,8 +1,6 @@ -using ImSharp; using Newtonsoft.Json.Linq; using OtterGui.Extensions; using Penumbra.Mods.Groups; -using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c1aea97c..2abf90ef 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -509,19 +509,19 @@ public class SettingsTab : ITab, IUiService { var sortMode = _config.SortMode; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + using (var combo = ImUtf8.Combo("##sortMode", sortMode.Name)) { if (combo) foreach (var val in Configuration.Constants.ValidSortModes) { - if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + if (ImUtf8.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) { _config.SortMode = val; _selector.SetFilterDirty(); _config.Save(); } - ImGuiUtil.HoverTooltip(val.Description); + ImUtf8.HoverTooltip(val.Description); } } From 898963fea530a04799eeb84675354c520b19ecf5 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Tue, 22 Jul 2025 14:08:30 -0500 Subject: [PATCH 2339/2451] Allow focusing a specified mod via HTTP API under the mods tab. --- Penumbra/Api/HttpApi.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index b6e1d799..8f8b44f4 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -19,6 +19,7 @@ public class HttpApi : IDisposable, IApiService [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); [Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); + [Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod(); // @formatter:on } @@ -115,6 +116,13 @@ public class HttpApi : IDisposable, IApiService Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } + public async partial Task FocusMod() + { + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); + Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered."); + if (data.Path.Length != 0) + api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name); + } private record ModReloadData(string Path, string Name) { @@ -123,6 +131,13 @@ public class HttpApi : IDisposable, IApiService { } } + private record ModFocusData(string Path, string Name) + { + public ModFocusData() + : this(string.Empty, string.Empty) + { } + } + private record ModInstallData(string Path) { public ModInstallData() From f5f4fe7259cb8aeec36cad45e2bc342eda3f109f Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:07:22 +1000 Subject: [PATCH 2340/2451] Invalid tangent fix example --- Penumbra/Import/Models/Export/MeshExporter.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 0070a808..11c84677 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -340,6 +340,39 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } + + private const float UnitLengthThresholdVec3 = 0.00674f; + internal static bool _IsFinite(float value) + { + return float.IsFinite(value); + } + + internal static bool _IsFinite(Vector2 v) + { + return _IsFinite(v.X) && _IsFinite(v.Y); + } + + internal static bool _IsFinite(Vector3 v) + { + return _IsFinite(v.X) && _IsFinite(v.Y) && _IsFinite(v.Z); + } + internal static Boolean IsNormalized(Vector3 normal) + { + if (!_IsFinite(normal)) return false; + + return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; + } + internal static Vector3 SanitizeNormal(Vector3 normal) + { + if (normal == Vector3.Zero) return Vector3.UnitX; + return IsNormalized(normal) ? normal : Vector3.Normalize(normal); + } + internal static Vector4 SanitizeTangent(Vector4 tangent) + { + var n = SanitizeNormal(new Vector3(tangent.X, tangent.Y, tangent.Z)); + var s = float.IsNaN(tangent.W) ? 1 : tangent.W; + return new Vector4(n, s > 0 ? 1 : -1); + } /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) @@ -352,19 +385,21 @@ public class MeshExporter if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) + SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; - + // var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; + var vec4 = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)); + var bitangent = vec4 with { W = vec4.W == 1 ? 1 : -1 }; + return new VertexPositionNormalTangent( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), - bitangent + SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))), + SanitizeTangent(bitangent) ); } From bdcab22a5528758d5f2d54505f3ecb5b866efb7d Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:03:27 +1000 Subject: [PATCH 2341/2451] Cleanup methods to extension class --- Penumbra/Import/Models/Export/MeshExporter.cs | 43 ++---------- Penumbra/Import/Models/ModelExtensions.cs | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 Penumbra/Import/Models/ModelExtensions.cs diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 11c84677..2e41f65a 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -340,39 +340,6 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } - - private const float UnitLengthThresholdVec3 = 0.00674f; - internal static bool _IsFinite(float value) - { - return float.IsFinite(value); - } - - internal static bool _IsFinite(Vector2 v) - { - return _IsFinite(v.X) && _IsFinite(v.Y); - } - - internal static bool _IsFinite(Vector3 v) - { - return _IsFinite(v.X) && _IsFinite(v.Y) && _IsFinite(v.Z); - } - internal static Boolean IsNormalized(Vector3 normal) - { - if (!_IsFinite(normal)) return false; - - return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; - } - internal static Vector3 SanitizeNormal(Vector3 normal) - { - if (normal == Vector3.Zero) return Vector3.UnitX; - return IsNormalized(normal) ? normal : Vector3.Normalize(normal); - } - internal static Vector4 SanitizeTangent(Vector4 tangent) - { - var n = SanitizeNormal(new Vector3(tangent.X, tangent.Y, tangent.Z)); - var s = float.IsNaN(tangent.W) ? 1 : tangent.W; - return new Vector4(n, s > 0 ? 1 : -1); - } /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) @@ -385,21 +352,19 @@ public class MeshExporter if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - // var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; - var vec4 = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)); - var bitangent = vec4 with { W = vec4.W == 1 ? 1 : -1 }; + var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; return new VertexPositionNormalTangent( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))), - SanitizeTangent(bitangent) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), + bitangent.SanitizeTangent() ); } diff --git a/Penumbra/Import/Models/ModelExtensions.cs b/Penumbra/Import/Models/ModelExtensions.cs new file mode 100644 index 00000000..2edb3ca4 --- /dev/null +++ b/Penumbra/Import/Models/ModelExtensions.cs @@ -0,0 +1,69 @@ +namespace Penumbra.Import.Models; + +public static class ModelExtensions +{ + // https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158 + private const float UnitLengthThresholdVec3 = 0.00674f; + private const float UnitLengthThresholdVec4 = 0.00769f; + + internal static bool _IsFinite(this float value) + { + return float.IsFinite(value); + } + + internal static bool _IsFinite(this Vector2 v) + { + return v.X._IsFinite() && v.Y._IsFinite(); + } + + internal static bool _IsFinite(this Vector3 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite(); + } + + internal static bool _IsFinite(this in Vector4 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite(); + } + + internal static Boolean IsNormalized(this Vector3 normal) + { + if (!normal._IsFinite()) return false; + + return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; + } + + internal static void ValidateNormal(this Vector3 normal, string msg) + { + if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid."); + + if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length."); + } + + internal static void ValidateTangent(this Vector4 tangent, string msg) + { + if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg); + + new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg); + } + + internal static Vector3 SanitizeNormal(this Vector3 normal) + { + if (normal == Vector3.Zero) return Vector3.UnitX; + return normal.IsNormalized() ? normal : Vector3.Normalize(normal); + } + + internal static bool IsValidTangent(this Vector4 tangent) + { + if (tangent.W != 1 && tangent.W != -1) return false; + + return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized(); + } + + internal static Vector4 SanitizeTangent(this Vector4 tangent) + { + var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal(); + var s = float.IsNaN(tangent.W) ? 1 : tangent.W; + return new Vector4(n, s > 0 ? 1 : -1); + } +} From 6689e326ee00d9dfddca4f813cb7232388cc0654 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Sat, 2 Aug 2025 00:27:21 +0200 Subject: [PATCH 2342/2451] Material tab: disallow "Enable Transparency" for stockings shader --- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 97acf130..77bfb795 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface.Components; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; @@ -119,11 +120,22 @@ public sealed partial class MtrlTab : IWritable, IDisposable using var dis = ImRaii.Disabled(disabled); var tmp = shaderFlags.EnableTransparency; - if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) + + // guardrail: the game crashes if transparency is enabled on characterstockings.shpk + var disallowTransparency = Mtrl.ShaderPackage.Name == "characterstockings.shpk"; + using (ImRaii.Disabled(disallowTransparency)) { - shaderFlags.EnableTransparency = tmp; - ret = true; - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) + { + shaderFlags.EnableTransparency = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + } + + if (disallowTransparency) + { + ImGuiComponents.HelpMarker("Enabling transparency for shader package characterstockings.shpk will crash the game."); } ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); From 3f18ad50de0d3f360771fd472ef98a05008e5bd3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 00:45:24 +0200 Subject: [PATCH 2343/2451] Initial API13 / 7.3 update. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/IpcTester/CollectionsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/EditingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 2 +- Penumbra/Api/IpcTester/IpcTester.cs | 2 +- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ModSettingsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ModsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/PluginStateIpcTester.cs | 2 +- Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ResolveIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 4 ++-- Penumbra/Api/IpcTester/UiIpcTester.cs | 2 +- Penumbra/ChangedItemMode.cs | 2 +- Penumbra/CommandHandler.cs | 2 +- Penumbra/Import/TexToolsImporter.Gui.cs | 2 +- .../Textures/CombinedTexture.Manipulation.cs | 2 +- Penumbra/Import/Textures/TextureDrawer.cs | 4 ++-- Penumbra/Import/Textures/TextureManager.cs | 2 +- Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- Penumbra/Interop/MaterialPreview/MaterialInfo.cs | 2 +- Penumbra/Interop/ResourceTree/ResolveContext.cs | 4 ++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 5 ++--- Penumbra/Interop/Services/TextureArraySlicer.cs | 7 ++++--- Penumbra/Meta/ShapeAttributeManager.cs | 3 --- Penumbra/Mods/FeatureChecker.cs | 2 +- Penumbra/Penumbra.cs | 4 +++- Penumbra/Penumbra.csproj | 2 +- Penumbra/Penumbra.json | 2 +- Penumbra/Services/StainService.cs | 2 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../Materials/MaterialTemplatePickers.cs | 2 +- .../AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- .../Materials/MtrlTab.CommonColorTable.cs | 14 +++++++------- .../AdvancedWindow/Materials/MtrlTab.Constants.cs | 2 +- .../Materials/MtrlTab.LegacyColorTable.cs | 2 +- .../Materials/MtrlTab.LivePreview.cs | 2 +- .../Materials/MtrlTab.ShaderPackage.cs | 4 ++-- .../AdvancedWindow/Materials/MtrlTab.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 2 +- .../UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Deformers.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Materials.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.QuickImport.cs | 2 +- .../AdvancedWindow/ModEditWindow.ShaderPackages.cs | 4 ++-- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs | 2 +- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 12 ++++++------ Penumbra/UI/Classes/CollectionSelectHeader.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 2 +- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 2 +- Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 4 ++-- Penumbra/UI/CollectionTab/CollectionSelector.cs | 4 ++-- .../UI/CollectionTab/IndividualAssignmentUi.cs | 2 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 ++-- Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/FileDialogService.cs | 2 +- Penumbra/UI/ImportPopup.cs | 2 +- Penumbra/UI/IncognitoService.cs | 2 +- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 2 +- Penumbra/UI/Knowledge/RaceCodeTab.cs | 2 +- Penumbra/UI/ModsTab/DescriptionEditPopup.cs | 2 +- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 2 +- .../ModsTab/Groups/CombiningModGroupEditDrawer.cs | 2 +- .../UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 2 +- .../UI/ModsTab/Groups/SingleModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanel.cs | 2 +- Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 10 +++++----- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelHeader.cs | 2 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 4 ++-- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcherTable.cs | 2 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 2 +- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 2 +- Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs | 3 +-- Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs | 2 +- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 2 +- Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs | 2 +- Penumbra/UI/Tabs/EffectiveTab.cs | 2 +- Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- Penumbra/UI/UiHelpers.cs | 12 ++++++------ 123 files changed, 158 insertions(+), 160 deletions(-) diff --git a/OtterGui b/OtterGui index ad3bafa4..9523b7ac 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ad3bafa4f0af5b69833347f8c8ff2e178645e2f0 +Subproject commit 9523b7ac725656b21fa98faef96962652e86e64f diff --git a/Penumbra.Api b/Penumbra.Api index ff7b3b40..c27a0600 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f +Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165 diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 4cb53c8b..abcb8e3d 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.GameData b/Penumbra.GameData index 82b44672..65c5bf3f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 82b446721a9b9c99d2470c54ad49fe19ff4987e3 +Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350 diff --git a/Penumbra.String b/Penumbra.String index 0e5dcd1a..878acce4 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5 +Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 1d516eba..c06bdeb4 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs index a1001630..d754cf90 100644 --- a/Penumbra/Api/IpcTester/EditingIpcTester.cs +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 04541a57..38a09714 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs index 201e7068..b03d7e03 100644 --- a/Penumbra/Api/IpcTester/IpcTester.cs +++ b/Penumbra/Api/IpcTester/IpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using Penumbra.Api.Api; diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 9cf20cd7..bee1981c 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index c8eb8496..152efa45 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index a24861a3..9ea53366 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index a1bf4fc4..073305d0 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index b862dde5..6b853ed2 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs index a79b099d..9fc5bfc7 100644 --- a/Penumbra/Api/IpcTester/ResolveIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -1,5 +1,5 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.IpcSubscribers; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 48f3b4a8..e6c8d52e 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -1,8 +1,8 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index c106a867..64adf256 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -282,7 +282,7 @@ public class TemporaryIpcTester( foreach (var mod in list) { ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Name); + ImGui.TextUnformatted(mod.Name.Text); ImGui.TableNextColumn(); ImGui.TextUnformatted(mod.Priority.ToString()); ImGui.TableNextColumn(); diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index 647a4dda..852339c9 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -1,5 +1,5 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/ChangedItemMode.cs b/Penumbra/ChangedItemMode.cs index dccffded..ddb79ee0 100644 --- a/Penumbra/ChangedItemMode.cs +++ b/Penumbra/ChangedItemMode.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; namespace Penumbra; diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 9f681da2..b5d307ef 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,7 +1,7 @@ using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Api.Api; diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index f145f560..5cb99d72 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.Import.Structs; diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 2d131d71..7a7e5888 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index b0a65ac0..14203dff 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; @@ -20,7 +20,7 @@ public static class TextureDrawer { size = texture.TextureWrap.Size.Contain(size); - ImGui.Image(texture.TextureWrap.ImGuiHandle, size); + ImGui.Image(texture.TextureWrap.Handle, size); DrawData(texture); } else if (texture.LoadError != null) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 0c85f5be..073fef2f 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -406,7 +406,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { - var device = uiBuilder.Device; + var device = new Device(uiBuilder.DeviceHandle); var dxgiDevice = device.QueryInterface(); using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 368845b4..523ae610 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); + Task = hooks.CreateHook("Change Customize", Sigs.UpdateDrawData, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f2ea2d6c..a9fb46ff 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -85,7 +85,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy if (mtrlHandle == null) continue; - PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); + PathDataHandler.Split(mtrlHandle->FileName.AsSpan(), out var path, out _); var fileName = CiByteString.FromSpanUnsafe(path, true); if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 64a91302..b2364e33 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext( if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path)) return null; - return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path); } [SkipLocalsInit] @@ -245,7 +245,7 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, (ResourceHandle*)resource, path, false); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value)); if (shpkNode is not null) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index e7c4b11b..49649e13 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -72,15 +72,14 @@ public class ResourceTree( var mpapArrayPtr = model->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) var skinMtrlArray = modelType switch { - ModelType.Human => new ReadOnlySpan>((MaterialResourceHandle**)((nint)model + 0xB48), 5), + ModelType.Human => ((Human*) model)->SlotSkinMaterials, _ => [], }; var decalArray = modelType switch { - ModelType.Human => human->SlotDecalsSpan, + ModelType.Human => human->SlotDecals, ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals, ModelType.Weapon => [((Weapon*)model)->Decal], ModelType.Monster => [((Monster*)model)->Decal], diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index c934ac2b..11498878 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -1,3 +1,4 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using OtterGui.Services; using SharpDX.Direct3D; @@ -16,7 +17,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = []; /// Caching this across frames will cause a crash to desktop. - public nint GetImGuiHandle(Texture* texture, byte sliceIndex) + public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex) { if (texture == null) throw new ArgumentNullException(nameof(texture)); @@ -25,7 +26,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) { state.Refresh(); - return (nint)state.ShaderResourceView; + return new ImTextureID((nint)state.ShaderResourceView); } var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; var description = srv.Description; @@ -60,7 +61,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable } state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); _activeSlices.Add(((nint)texture, sliceIndex), state); - return (nint)state.ShaderResourceView; + return new ImTextureID((nint)state.ShaderResourceView); } public void Tick() diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index 16901741..a7f71ac7 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -6,8 +5,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; diff --git a/Penumbra/Mods/FeatureChecker.cs b/Penumbra/Mods/FeatureChecker.cs index 5800ef07..10874fc9 100644 --- a/Penumbra/Mods/FeatureChecker.cs +++ b/Penumbra/Mods/FeatureChecker.cs @@ -1,7 +1,7 @@ using System.Collections.Frozen; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cf96c7f6..b22d049d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; +using Dalamud.Game; using OtterGui; using OtterGui.Log; using OtterGui.Services; @@ -20,6 +21,7 @@ using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index c61692f4..3159b736 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 924d7bd3..bd9a2479 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 12, + "DalamudApiLevel": 13, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index b16d4dcd..17294aa8 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; -using ImGuiNET; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c783e17f..a0305619 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Compression; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index f5d2a8c7..e9d76990 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 5c636b1d..241c3a91 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using FFXIVClientStructs.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index 0c987972..fad9adeb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index d70a4b50..39ff0a15 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files; using OtterGui.Text; @@ -338,10 +338,10 @@ public partial class MtrlTab var tmp = inputSqrt; if (ImUtf8.ColorEdit(label, ref tmp, ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.DisplayRgb + | ImGuiColorEditFlags.InputRgb | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) + | ImGuiColorEditFlags.Hdr) && tmp != inputSqrt) { setter((HalfColor)PseudoSquareRgb(tmp)); @@ -373,10 +373,10 @@ public partial class MtrlTab var tmp = Vector4.Zero; ImUtf8.ColorEdit(label, ref tmp, ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.DisplayRgb + | ImGuiColorEditFlags.InputRgb | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR + | ImGuiColorEditFlags.Hdr | ImGuiColorEditFlags.AlphaPreview); if (letter.Length > 0 && ImGui.IsItemVisible()) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index f413a6a2..4ad6968b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index 0ffdd1cc..bebacc94 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 5025bafd..dfa3a963 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index ee5341b2..43040ca3 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -384,7 +384,7 @@ public partial class MtrlTab var shpkFlags = (int)Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) return false; Mtrl.ShaderPackage.Flags = (uint)shpkFlags; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index ac88f77c..82ba7be4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 77bfb795..e15d1c90 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Components; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 5b6d585a..4a74cda5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs index 89fadfa8..4b375c26 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 348a0d4c..16af5217 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index d6df95cb..77c2915a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index e5e28a3d..84e09be5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 929feadd..b03f4aa5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 3691a4f7..4053560b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Structs; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 34488a87..bb87cd47 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 7e788462..f608a194 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index d60f877b..88abe0cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 35c8ccec..59692195 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 36154105..4f7ae8da 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 3f63967e..87d7487b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 4c946fe7..3caff226 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index aa3d9172..06cd0763 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index cc592296..a7db7f25 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 00caaabc..72350857 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Data; using OtterGui.Text; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index a6a75e0d..baaf4a82 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; @@ -147,7 +147,7 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.MonoFont); var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20); - ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, + ImGuiNative.InputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, (uint)shader.Disassembly!.RawDisassembly.Length + 1, size, ImGuiInputTextFlags.ReadOnly, null, null); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index ee4e1eda..34e1e0d4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e148167b..952d8489 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -4,7 +4,7 @@ using Dalamud.Interface.DragDrop; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Log; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 3c110fab..bf16fa37 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs index 1fa12b6d..c9996a1e 100644 --- a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs +++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 4d33a3fc..440baa2f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Text; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index a9070360..db54a8e5 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -3,7 +3,7 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; @@ -107,11 +107,11 @@ public class ChangedItemDrawer : IDisposable, IUiService return; } - ImGui.Image(icon.ImGuiHandle, new Vector2(height)); + ImGui.Image(icon.Handle, new Vector2(height)); if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } @@ -193,7 +193,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].Handle, size, Vector2.Zero, Vector2.One, typeFilter switch { 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), @@ -213,7 +213,7 @@ public class ChangedItemDrawer : IDisposable, IUiService var localRet = false; var icon = _icons[type]; var flag = typeFilter.HasFlag(type); - ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); + ImGui.Image(icon.Handle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { typeFilter = flag ? typeFilter & ~type : typeFilter | type; @@ -232,7 +232,7 @@ public class ChangedItemDrawer : IDisposable, IUiService if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index aa492362..355a6106 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 9c15ceb8..90ef0591 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Custom; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index a3dcd23a..98a59a5b 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Services; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 98dc924f..bf97f178 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index dc0e71b5..26fa2b14 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -6,7 +6,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -346,7 +346,7 @@ public sealed class CollectionPanel( if (!source) return; - ImGui.SetDragDropPayload("DragIndividual", nint.Zero, 0); + ImGui.SetDragDropPayload("DragIndividual", null, 0); ImGui.TextUnformatted($"Re-ordering {text}..."); _draggedIndividualAssignment = _active.Individuals.Index(id); } diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 57429531..e54f994e 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; @@ -85,7 +85,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl if (source) { _dragging = Items[idx]; - ImGui.SetDragDropPayload(PayloadString, nint.Zero, 0); + ImGui.SetDragDropPayload(PayloadString, null, 0); ImGui.TextUnformatted($"Assigning {Name(_dragging)} to..."); } diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index fd8f9b25..f472e346 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Custom; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index cdc1e83e..2053f269 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -288,7 +288,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (!source) return; - ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0); + ImGui.SetDragDropPayload(InheritanceDragDropLabel, null, 0); _movedInheritance = collection; ImGui.TextUnformatted($"Moving {(_movedInheritance != null ? Name(_movedInheritance) : "Unknown")}..."); } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 64d370b5..55d0bc19 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 6773bc88..3bbc4ba8 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Services; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 28767edc..59ed0308 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Import.Structs; diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index 29358618..678e072e 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.UI.Classes; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index f831975b..118ed479 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.String; diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs index 36b048aa..44b544eb 100644 --- a/Penumbra/UI/Knowledge/RaceCodeTab.cs +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs index c284afc3..7d7a6967 100644 --- a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs +++ b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Mods; diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index a3e7ce14..1430f17b 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index 5bd5dfdf..e9840e6c 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 3d330093..fa5b0ef6 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 566ec02c..3d8409ad 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Components; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e9ab72ae..9610f173 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index 492a8fb7..8fa6a377 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8a383791..16ff7b41 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -2,7 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.DragDrop; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 9d6ead62..b7546699 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Services; using Penumbra.Mods; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index b12df97d..332b64f0 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index ec020c86..70cad148 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index c750b8b0..1002d8ca 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -73,7 +73,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy ImGui.TableNextColumn(); using var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderLine.Value()); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(selector.Selected!.Name); + ImGui.TextUnformatted(selector.Selected!.Name.Text); ImGui.TableNextColumn(); var actualSettings = collectionManager.Active.Current.GetActualSettings(selector.Selected!.Index).Settings!; var priority = actualSettings.Priority.Value; @@ -81,7 +81,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy using (ImRaii.Disabled(actualSettings is TemporaryModSettings)) { ImGui.SetNextItemWidth(priorityWidth); - if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputInt("##priority", ref priority, 0, 0, flags: ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) @@ -104,7 +104,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy private void DrawConflictSelectable(ModConflicts conflict) { ImGui.AlignTextToFramePadding(); - if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) + if (ImGui.Selectable(conflict.Mod2.Name.Text) && conflict.Mod2 is Mod otherMod) selector.SelectByValue(otherMod); var hovered = ImGui.IsItemHovered(); var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); @@ -172,7 +172,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy var priority = _currentPriority ?? GetPriority(conflict).Value; ImGui.SetNextItemWidth(priorityWidth); - if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputInt("##priority", ref priority, 0, 0, flags: ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 6fe3e4c6..71c1a225 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 478ab892..5b831a66 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -325,7 +325,7 @@ public class ModPanelEditTab( var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; ImGui.SetNextItemWidth(width); - if (ImGui.InputText(label, ref tmp, maxLength)) + if (ImGui.InputText(label, ref tmp)) { _currentEdit = tmp; _optionIndex = option; diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index aafbffa6..b42ac680 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Communication; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 7c6ebf74..84f69bcb 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 639118f5..5981d979 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index ff5f636d..3eac972c 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 12355672..7e268e8c 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,6 +1,6 @@ -using Dalamud.Interface; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d134cfe5..ee3613fc 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 009da842..97df095e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 6cee22d6..4dc9474f 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 05a1f33b..f2a041eb 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 28827ad9..43ae2488 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index 3b25c1a9..f136bacd 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 94c6cbd6..471d770a 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.CrashHandler; diff --git a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs index c8e7f001..672b8c79 100644 --- a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs +++ b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs @@ -1,6 +1,6 @@ using System.Text.Json; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 76df5acc..eadee2d5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -8,7 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index bfe89768..7af33a36 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -1,10 +1,9 @@ -using Dalamud.Hooking; +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Scheduler; using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; -using ImGuiNET; using OtterGui.Services; using OtterGui.Text; using Penumbra.Interop.Services; diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs index e8ff9b9c..f1024950 100644 --- a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Services; using OtterGui.Text; using Penumbra.Interop.Hooks; diff --git a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs index c8518315..e6e01107 100644 --- a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs +++ b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Services; diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs index c8c90e09..d497f90a 100644 --- a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using ImGuiNET; using OtterGui; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 7b940cd0..4c3b43bf 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs index 08d51184..4244e455 100644 --- a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using Lumina.Data.Files; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index ecf9a886..5691f821 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 613a3532..79dcbb9e 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index c54e3433..593adde1 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,9 +1,9 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 2abf90ef..96d11baa 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,10 +1,10 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; -using ImGuiNET; using OtterGui; using OtterGui.Compression; using OtterGui.Custom; diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index deba7023..9fe90ee8 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -13,11 +13,11 @@ public static class UiHelpers { /// Draw text given by a ByteString. public static unsafe void Text(ByteString s) - => ImGuiNative.igTextUnformatted(s.Path, s.Path + s.Length); + => ImGuiNative.TextUnformatted(s.Path, s.Path + s.Length); /// Draw text given by a byte pointer and length. public static unsafe void Text(byte* s, int length) - => ImGuiNative.igTextUnformatted(s, s + length); + => ImGuiNative.TextUnformatted(s, s + length); /// Draw text given by a byte span. public static unsafe void Text(ReadOnlySpan s) @@ -36,7 +36,7 @@ public static class UiHelpers public static unsafe bool Selectable(ByteString s, bool selected) { var tmp = (byte)(selected ? 1 : 0); - return ImGuiNative.igSelectable_Bool(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; + return ImGuiNative.Selectable(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; } /// @@ -45,8 +45,8 @@ public static class UiHelpers /// public static unsafe void CopyOnClickSelectable(ByteString text) { - if (ImGuiNative.igSelectable_Bool(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) - ImGuiNative.igSetClipboardText(text.Path); + if (ImGuiNative.Selectable(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) + ImGuiNative.SetClipboardText(text.Path); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Click to copy to clipboard."); From a69811800d7203642f75b900bd56368199264283 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 15:56:25 +0200 Subject: [PATCH 2344/2451] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 65c5bf3f..ea49bc09 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350 +Subproject commit ea49bc099e783ecafdf78f0bd0bc41fb8c60ad19 From 2b36f3984860a4db41cf2832c93f3e7220cd23f0 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 4 Aug 2025 18:37:36 +0200 Subject: [PATCH 2345/2451] Fix basecolor texture in material export --- Penumbra/Import/Models/Export/MaterialExporter.cs | 7 ++++--- .../AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 6be2ccbd..0d91534e 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.UI.AdvancedWindow.Materials; using SharpGLTF.Materials; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; @@ -140,13 +141,13 @@ public class MaterialExporter // Lerp between table row values to fetch final pixel values for each subtexture. var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend); - baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1)); var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1)); var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); - emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); + emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1)); } } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 39ff0a15..9ea9c2e0 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -594,7 +594,7 @@ public partial class MtrlTab internal static float PseudoSqrtRgb(float x) => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - internal static Vector3 PseudoSqrtRgb(Vector3 vec) + public static Vector3 PseudoSqrtRgb(Vector3 vec) => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); internal static Vector4 PseudoSqrtRgb(Vector4 vec) From 8140d085575aab238d5d844561afd8926d413d2d Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 4 Aug 2025 20:19:19 +0200 Subject: [PATCH 2346/2451] Add vertex material types for usages of 2 colour attributes --- Penumbra/Import/Models/Export/MeshExporter.cs | 83 +++- .../Import/Models/Export/VertexFragment.cs | 450 ++++++++++++++++++ 2 files changed, 523 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 2e41f65a..6ea2b284 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -390,23 +390,30 @@ public class MeshExporter } } + usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours); + var nColors = colours?.Count ?? 0; + var materialUsages = ( uvCount, - usages.ContainsKey(MdlFile.VertexUsage.Color) + nColors ); return materialUsages switch { - (3, true) => typeof(VertexTexture3ColorFfxiv), - (3, false) => typeof(VertexTexture3), - (2, true) => typeof(VertexTexture2ColorFfxiv), - (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexTexture1ColorFfxiv), - (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColorFfxiv), - (0, false) => typeof(VertexEmpty), + (3, 2) => typeof(VertexTexture3Color2Ffxiv), + (3, 1) => typeof(VertexTexture3ColorFfxiv), + (3, 0) => typeof(VertexTexture3), + (2, 2) => typeof(VertexTexture2Color2Ffxiv), + (2, 1) => typeof(VertexTexture2ColorFfxiv), + (2, 0) => typeof(VertexTexture2), + (1, 2) => typeof(VertexTexture1Color2Ffxiv), + (1, 1) => typeof(VertexTexture1ColorFfxiv), + (1, 0) => typeof(VertexTexture1), + (0, 2) => typeof(VertexColor2Ffxiv), + (0, 1) => typeof(VertexColorFfxiv), + (0, 0) => typeof(VertexEmpty), - _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), + _ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."), }; } @@ -419,6 +426,12 @@ public class MeshExporter if (_materialType == typeof(VertexColorFfxiv)) return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); + if (_materialType == typeof(VertexColor2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1)); + } + if (_materialType == typeof(VertexTexture1)) return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); @@ -428,6 +441,16 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); + if (_materialType == typeof(VertexTexture1Color2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexTexture1Color2Ffxiv( + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(color0), + ToVector4(color1) + ); + } + // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) @@ -448,6 +471,20 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } + + if (_materialType == typeof(VertexTexture2Color2Ffxiv)) + { + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture2Color2Ffxiv( + new Vector2(uv.X, uv.Y), + new Vector2(uv.Z, uv.W), + ToVector4(color0), + ToVector4(color1) + ); + } + if (_materialType == typeof(VertexTexture3)) { // Not 100% sure about this @@ -472,6 +509,21 @@ public class MeshExporter ); } + if (_materialType == typeof(VertexTexture3Color2Ffxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture3Color2Ffxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(color0), + ToVector4(color1) + ); + } + throw _notifier.Exception($"Unknown material type {_skinningType}"); } @@ -537,6 +589,17 @@ public class MeshExporter return list[0]; } + + /// Check that the list has length 2 for any case where this is expected and return both entries. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (T First, T Second) GetBothSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 2) + throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2."); + + return (list[0], list[1]); + } /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 56495f2f..463c59fc 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom } } +public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 0; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetTexCoord(int setIndex, Vector2 coord) + { } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : } } +public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 1; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex >= 1) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec } } +public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 2; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex >= 2) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } + +} + public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) : IVertexCustom { @@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1) + : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + TexCoord2 += delta.TexCoord2Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + 2 => TexCoord2, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex == 2) + TexCoord2 = coord; + if (setIndex >= 3) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} From 93406e4d4e50e84b12292d74aeb5c1f0f697b8af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 16:17:59 +0200 Subject: [PATCH 2347/2451] 1.5.0.0 --- Penumbra/UI/Changelog.cs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c1f7a1e6..4b487104 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -62,10 +62,37 @@ public class PenumbraChangelog : IUiService Add1_3_6_0(Changelog); Add1_3_6_4(Changelog); Add1_4_0_0(Changelog); + Add1_5_0_0(Changelog); } #region Changelogs + private static void Add1_5_0_0(Changelog log) + => log.NextVersion("Version 1.5.0.0") + .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") + .RegisterEntry("Added support for exporting models using two vertex color schemes (thanks zeroeightysix!).") + .RegisterEntry("Possibly improved the color accuracy of the basecolor texture created when exporting models (thanks zeroeightysix!).") + .RegisterEntry("Disabled enabling transparency for materials that use the characterstockings shader due to crashes (thanks zeroeightysix!).") + .RegisterEntry("Fixed some issues with model i/o and invalid tangents (thanks PassiveModding!)") + .RegisterEntry("Changed the behavior for default directory names when using the mod normalizer with combining groups.") + .RegisterEntry("Added jumping to specific mods to the HTTP API.") + .RegisterEntry("Fixed an issue with character sound modding (1.4.0.6).") + .RegisterHighlight("Added support for IMC-toggle attributes to accessories beyond the first toggle (1.4.0.5).") + .RegisterEntry("Fixed up some slot-specific attributes and shapes in models when swapping items between slots (1.4.0.5).") + .RegisterEntry("Added handling for human skin materials to the OnScreen tab and similar functionality (thanks Ny!) (1.4.0.5).") + .RegisterEntry("The OS thread ID a resource was loaded from was added to the resource logger (1.4.0.5).") + .RegisterEntry("A button linking to my (Ottermandias') Ko-Fi and Patreon was added in the settings tab. Feel free, but not pressured, to use it! :D ") + .RegisterHighlight("Mod setting combos now support mouse-wheel scrolling with Control and have filters (1.4.0.4).") + .RegisterEntry("Using the middle mouse button to toggle designs now works correctly with temporary settings (1.4.0.4).") + .RegisterEntry("Updated some BNPC associations (1.4.0.3).") + .RegisterEntry("Fixed further issues with shapes and attributes (1.4.0.4).") + .RegisterEntry("Penumbra now handles textures with MipMap offsets broken by TexTools on import and removes unnecessary MipMaps (1.4.0.3).") + .RegisterEntry("Updated the Mod Merger for the new group types (1.4.0.3).") + .RegisterEntry("Added querying Penumbra for supported features via IPC (1.4.0.3).") + .RegisterEntry("Shape names can now be edited in Penumbras model editor (1.4.0.2).") + .RegisterEntry("Attributes and Shapes can be fully toggled (1.4.0.2).") + .RegisterEntry("Fixed several issues with attributes and shapes (1.4.0.1)."); + private static void Add1_4_0_0(Changelog log) => log.NextVersion("Version 1.4.0.0") .RegisterHighlight("Added two types of new Meta Changes, SHP and ATR (Thanks Karou!).") From 13df8b2248010554342a4081ac27ad4fd6d67471 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:02:22 +0200 Subject: [PATCH 2348/2451] Update gamedata. --- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ea49bc09..fd875c43 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ea49bc099e783ecafdf78f0bd0bc41fb8c60ad19 +Subproject commit fd875c43ee910350107b2609809335285bd4ac0f diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index eadee2d5..9356ff5e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -726,6 +726,9 @@ public class DebugTab : Window, ITab, IUiService if (agent->Data == null) agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + ImUtf8.Text("Agent: "); + ImGui.SameLine(0, 0); + Penumbra.Dynamis.DrawPointer((nint)agent); if (agent->Data != null) { using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); From bedfb22466801e3a9bf8503a6fe0745ce1766a30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:04:50 +0200 Subject: [PATCH 2349/2451] Use staging for release. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87c0244..377919b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From 13283c969015d8e1d89a37721e6f5bc54da32664 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:08:26 +0200 Subject: [PATCH 2350/2451] Fix dumb. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9356ff5e..b9e36d80 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1048,7 +1048,7 @@ public class DebugTab : Window, ITab, IUiService if (t1) { ImGuiUtil.DrawTableColumn("Flags"); - ImGuiUtil.DrawTableColumn($"{model->UnkFlags_01:X2}"); + ImGuiUtil.DrawTableColumn($"{model->StateFlags}"); ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded"); ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}"); ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded"); From 66543cc671f77d14d21dc3b0c5a1df644799e638 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 8 Aug 2025 21:12:00 +0000 Subject: [PATCH 2351/2451] [CI] Updating repo.json for 1.5.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index af368d75..16206811 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.6", + "AssemblyVersion": "1.5.0.0", + "TestingAssemblyVersion": "1.5.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 46cfbcb115f34d4e5d5e2203b41e0379223e679b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:13:23 +0200 Subject: [PATCH 2352/2451] Set Repo API level to 13 and remove stg from future releases. --- .github/workflows/release.yml | 2 +- repo.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 377919b2..c87c0244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/repo.json b/repo.json index 16206811..8cc42d45 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.5.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 12, - "TestingDalamudApiLevel": 12, + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 11cd08a9dec41d90b1e40f70e2c60d7508a4c7bf Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Aug 2025 03:31:17 +0200 Subject: [PATCH 2353/2451] ClientStructs-ify stuff --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 3 +-- Penumbra/Interop/Structs/StructExtensions.cs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 49649e13..ddef347d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -258,8 +258,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1475) - var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; + var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 62dca02e..031d24b1 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -36,10 +36,8 @@ internal static class StructExtensions public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) - var vf91 = (delegate* unmanaged)((nint*)character.VirtualTable)[91]; var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(vf91((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, slotIndex)); + return ToOwnedByteString(character.ResolveSkinMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex)); } public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) From 6242b30f93b1992b2639c3180a77aa333a365472 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 11:58:28 +0200 Subject: [PATCH 2354/2451] Fix resizable child. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9523b7ac..539ce9e5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9523b7ac725656b21fa98faef96962652e86e64f +Subproject commit 539ce9e504fdc8bb0c2ca229905f4d236c376f6a From ff2b2be95352191c50701d5634c28fb75b53c000 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 12:11:29 +0200 Subject: [PATCH 2355/2451] Fix popups not working early. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 539ce9e5..5224ac53 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 539ce9e504fdc8bb0c2ca229905f4d236c376f6a +Subproject commit 5224ac538b1a7c0e86e7d2ceaf652d8d807888ae From 391c9d727e2946e1a55bbda60cb238e2adde733a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 12:51:39 +0200 Subject: [PATCH 2356/2451] Fix shifted timeline vfunc offset. --- Penumbra/Interop/GameState.cs | 3 --- Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 32b45b7e..b5171244 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -59,9 +59,6 @@ public class GameState : IService private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); - public ResolveData SoundData - => _characterSoundData.Value; - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public ResolveData SetSoundData(ResolveData data) { diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index cdd82b95..e0eb7ec5 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -63,7 +63,8 @@ public sealed unsafe class LoadTimelineResources : FastHookGetOwningGameObjectIndex(); + // TODO: Clientstructify + var idx = ((delegate* unmanaged**)timeline)[0][29](timeline); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; From 02af52671f0f5ce8ff8293bbcfe1fc2f0419a277 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 13:00:40 +0200 Subject: [PATCH 2357/2451] Need staging again ... --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87c0244..377919b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From 3785a629ce42c500f604d85b78cd67ea03e7e6d6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 9 Aug 2025 11:03:24 +0000 Subject: [PATCH 2358/2451] [CI] Updating repo.json for 1.5.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8cc42d45..9a970ea7 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.0", - "TestingAssemblyVersion": "1.5.0.0", + "AssemblyVersion": "1.5.0.1", + "TestingAssemblyVersion": "1.5.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9aae2210a2f7194a61da4844f7c8df5812fbfa73 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Aug 2025 14:54:56 +0200 Subject: [PATCH 2359/2451] Fix nullptr crashes --- OtterGui | 2 +- .../UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 5224ac53..0eaf7655 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5224ac538b1a7c0e86e7d2ceaf652d8d807888ae +Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792 diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index bebacc94..e75cd633 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -67,7 +67,7 @@ public partial class MtrlTab private static void DrawLegacyColorTableHeader(bool hasDyeTable) { ImGui.TableNextColumn(); - ImUtf8.TableHeader(default(ReadOnlySpan)); + ImUtf8.TableHeader(""u8); ImGui.TableNextColumn(); ImUtf8.TableHeader("Row"u8); ImGui.TableNextColumn(); From 155d3d49aa640da456794d9756097e82de158417 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 9 Aug 2025 16:40:42 +0000 Subject: [PATCH 2360/2451] [CI] Updating repo.json for 1.5.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9a970ea7..cd2a8018 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.1", - "TestingAssemblyVersion": "1.5.0.1", + "AssemblyVersion": "1.5.0.2", + "TestingAssemblyVersion": "1.5.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f6bac93db78f1dc42ea48774a37c38c07b2460dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Aug 2025 19:58:24 +0200 Subject: [PATCH 2361/2451] Update ChangedEquipData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fd875c43..2f5e9013 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fd875c43ee910350107b2609809335285bd4ac0f +Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a From 12a218bb2b4cee341d31fa3d2887b76ec67b816c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 12:28:56 +0200 Subject: [PATCH 2362/2451] Protect against empty requested paths. --- .../Hooks/ResourceLoading/ResourceService.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index e90b4575..1a40accc 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -85,7 +86,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8); private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, + uint unk9); [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] private readonly Hook _getResourceSyncHook = null!; @@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService unk9); } - var original = gamePath; + if (gamePath.IsEmpty) + { + Penumbra.Log.Error($"[ResourceService] Empty resource path requested with category {*categoryId}, type {*resourceType}, hash {*resourceHash}."); + return null; + } + + var original = gamePath; ResourceHandle* returnValue = null; ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, + unk9); } /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original, + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, + Utf8GamePath original, GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) { var previous = _currentGetResourcePath.Value; @@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService resourceParameters, unk8, unk9) : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, resourceParameters, unk, unk8, unk9); - } finally + } + finally { _currentGetResourcePath.Value = previous; } @@ -163,7 +174,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService /// The original game path of the resource, if loaded synchronously. /// The previous state of the resource. /// The return value to use. - public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); + public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, + (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); /// /// @@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) { var previousState = (handle->UnkState, handle->LoadState); - var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; + var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; ResourceStateUpdating?.Invoke(handle, syncOriginal); var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); From 7af81a6c18f382cd2b2cf806134060fed421dbd9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 12:29:09 +0200 Subject: [PATCH 2363/2451] Fix issue with removing default metadata. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index ede062ae..c2c9e777 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -126,6 +126,7 @@ public class MetaDictionary { _data = null; Count = 0; + return; } Count = GlobalEqp.Count + Shp.Count + Atr.Count; From b112d75a27baac5ea02e60c353fcb32e6f46e609 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 12 Aug 2025 10:31:13 +0000 Subject: [PATCH 2364/2451] [CI] Updating repo.json for 1.5.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cd2a8018..305912c0 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.2", - "TestingAssemblyVersion": "1.5.0.2", + "AssemblyVersion": "1.5.0.3", + "TestingAssemblyVersion": "1.5.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9f8185f67baf18ee552bf68fa3556fa4509d3acb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 14:47:35 +0200 Subject: [PATCH 2365/2451] Add new parameter to LoadWeapon hook. --- Penumbra/Interop/Hooks/Objects/WeaponReload.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index b09103f6..4231b027 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task.IsCompletedSuccessfully; - private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g); + private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h); - private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g) + private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h) { var gameObject = drawData->OwnerObject; - Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); + Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}, {h}."); Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); - _task.Result.Original(drawData, slot, weapon, d, e, f, g); + _task.Result.Original(drawData, slot, weapon, d, e, f, g, h); _postEvent.Invoke(drawData, gameObject); } From 9aff388e21a6e0d688157123ec5b3d2d061b90dd Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 12 Aug 2025 12:53:33 +0000 Subject: [PATCH 2366/2451] [CI] Updating repo.json for 1.5.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 305912c0..6045e266 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.3", - "TestingAssemblyVersion": "1.5.0.3", + "AssemblyVersion": "1.5.0.4", + "TestingAssemblyVersion": "1.5.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a7246b9d98392db549ec6fd408d430843664e48b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Aug 2025 16:50:26 +0200 Subject: [PATCH 2367/2451] Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 2 +- .../Processing/PbdFilePostProcessor.cs | 119 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 16 +-- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 73 ++++++----- Penumbra/packages.lock.json | 22 +++- 8 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 Penumbra/Interop/Processing/PbdFilePostProcessor.cs diff --git a/OtterGui b/OtterGui index 0eaf7655..3ea61642 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792 +Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3 diff --git a/Penumbra.GameData b/Penumbra.GameData index 2f5e9013..2cf59c61 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a +Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 30e643c7..51af5813 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { diff --git a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs new file mode 100644 index 00000000..69f2ecd5 --- /dev/null +++ b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs @@ -0,0 +1,119 @@ +using Dalamud.Game; +using Dalamud.Plugin.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class PbdFilePostProcessor : IFilePostProcessor +{ + private readonly IFileAllocator _allocator; + private byte[] _epbdData; + private unsafe delegate* unmanaged _loadEpbdData; + + public ResourceType Type + => ResourceType.Pbd; + + public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner) + { + _allocator = allocator; + _epbdData = SetEpbdData(dataManager); + _loadEpbdData = (delegate* unmanaged)scanner.ScanText(Sigs.LoadEpbdData); + } + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) + { + if (_epbdData.Length is 0) + return; + + if (resource->LoadState is not LoadState.Success) + { + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} failed load ({resource->LoadState})."); + return; + } + + var (data, length) = resource->GetData(); + if (length is 0 || data == nint.Zero) + { + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} succeeded load but has no data."); + return; + } + + var span = new ReadOnlySpan((void*)data, (int)resource->FileSize); + var reader = new PackReader(span); + if (reader.HasData) + { + Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {originalGamePath} with EPBD data."); + return; + } + + var newData = AppendData(span); + fixed (byte* ptr = newData) + { + // Set the appended data and the actual file size, then re-load the EPBD data via game function call. + if (resource->SetData((nint)ptr, newData.Length)) + { + resource->FileSize = (uint)newData.Length; + resource->CsHandle.FileSize2 = (uint)newData.Length; + resource->CsHandle.FileSize3 = (uint)newData.Length; + _loadEpbdData(resource); + // Free original data. + _allocator.Release((void*)data, length); + Penumbra.Log.Verbose($"[ResourceLoader] Loaded {originalGamePath} from file and appended default EPBD data."); + } + else + { + Penumbra.Log.Warning( + $"[ResourceLoader] Failed to append EPBD data to custom PBD at {originalGamePath}."); + } + } + } + + /// Combine the given data with the default PBD data using the game's file allocator. + private unsafe ReadOnlySpan AppendData(ReadOnlySpan data) + { + // offset has to be set, otherwise not called. + var newLength = data.Length + _epbdData.Length; + var memory = _allocator.Allocate(newLength); + var span = new Span(memory, newLength); + data.CopyTo(span); + _epbdData.CopyTo(span[data.Length..]); + return span; + } + + /// Fetch the default EPBD data from the .pbd file of the game's installation. + private static byte[] SetEpbdData(IDataManager dataManager) + { + try + { + var file = dataManager.GetFile(GamePaths.Pbd.Path); + if (file is null || file.Data.Length is 0) + { + Penumbra.Log.Warning("Default PBD file has no data."); + return []; + } + + ReadOnlySpan span = file.Data; + var reader = new PackReader(span); + if (!reader.HasData) + { + Penumbra.Log.Warning("Default PBD file has no EPBD section."); + return []; + } + + var offset = span.Length - (int)reader.PackLength; + var ret = span[offset..]; + Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}."); + return ret.ToArray(); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}"); + return []; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index a0305619..424bc56f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -8,6 +8,7 @@ using OtterGui.Compression; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; +using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -80,7 +81,7 @@ public class FileEditor( private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; + private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty; private bool _inInput; private Utf8GamePath _defaultPathUtf8; private bool _isDefaultPathUtf8Valid; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b9e36d80..d41dd25a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1236,16 +1236,12 @@ public class DebugTab : Window, ITab, IUiService } public static unsafe void DrawCopyableAddress(ReadOnlySpan label, void* address) - { - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - if (ImUtf8.Selectable($"0x{(nint)address:X16} {label}")) - ImUtf8.SetClipboardText($"0x{(nint)address:X16}"); - } - - ImUtf8.HoverTooltip("Click to copy address to clipboard."u8); - } + => DrawCopyableAddress(label, (nint)address); public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) - => DrawCopyableAddress(label, (void*)address); + { + Penumbra.Dynamis.DrawPointer(address); + ImUtf8.SameLineInner(); + ImUtf8.Text(label); + } } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 7af33a36..f0ab1125 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -27,14 +27,31 @@ public unsafe class GlobalVariablesDrawer( return; var actionManager = (ActionTimelineManager**)ActionTimelineManager.Instance(); - DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); - DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); - DebugTab.DrawCopyableAddress("ScheduleManagement"u8, ScheduleManagement.Instance()); - DebugTab.DrawCopyableAddress("ActionTimelineManager*"u8, actionManager); - DebugTab.DrawCopyableAddress("ActionTimelineManager"u8, actionManager != null ? *actionManager : null); - DebugTab.DrawCopyableAddress("SchedulerResourceManagement*"u8, scheduler.Address); - DebugTab.DrawCopyableAddress("SchedulerResourceManagement"u8, scheduler.Address != null ? *scheduler.Address : null); - DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + using (ImUtf8.Group()) + { + Penumbra.Dynamis.DrawPointer(characterUtility.Address); + Penumbra.Dynamis.DrawPointer(residentResources.Address); + Penumbra.Dynamis.DrawPointer(ScheduleManagement.Instance()); + Penumbra.Dynamis.DrawPointer(actionManager); + Penumbra.Dynamis.DrawPointer(actionManager != null ? *actionManager : null); + Penumbra.Dynamis.DrawPointer(scheduler.Address); + Penumbra.Dynamis.DrawPointer(scheduler.Address != null ? *scheduler.Address : null); + Penumbra.Dynamis.DrawPointer(Device.Instance()); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text("CharacterUtility"u8); + ImUtf8.Text("ResidentResourceManager"u8); + ImUtf8.Text("ScheduleManagement"u8); + ImUtf8.Text("ActionTimelineManager*"u8); + ImUtf8.Text("ActionTimelineManager"u8); + ImUtf8.Text("SchedulerResourceManagement*"u8); + ImUtf8.Text("SchedulerResourceManagement"u8); + ImUtf8.Text("Device"u8); + } + DrawCharacterUtility(); DrawResidentResources(); DrawSchedulerResourcesMap(); @@ -63,7 +80,7 @@ public unsafe class GlobalVariablesDrawer( var resource = characterUtility.Address->Resource(idx); ImUtf8.DrawTableColumn($"[{idx}]"); ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); if (resource == null) { ImGui.TableNextRow(); @@ -74,25 +91,12 @@ public unsafe class GlobalVariablesDrawer( ImGui.TableNextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); - if (ImUtf8.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + Penumbra.Dynamis.DrawPointer(data); ImUtf8.DrawTableColumn(length.ToString()); - ImGui.TableNextColumn(); if (intern.Value != -1) { - ImUtf8.Selectable($"0x{characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)characterUtility.DefaultResource(intern).Address, - characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); - + Penumbra.Dynamis.DrawPointer(characterUtility.DefaultResource(intern).Address); ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}"); } else @@ -122,7 +126,7 @@ public unsafe class GlobalVariablesDrawer( var resource = residentResources.Address->ResourceList[idx]; ImUtf8.DrawTableColumn($"[{idx}]"); ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); if (resource == null) { ImGui.TableNextRow(); @@ -133,12 +137,7 @@ public unsafe class GlobalVariablesDrawer( ImGui.TableNextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); - if (ImUtf8.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + Penumbra.Dynamis.DrawPointer(data); ImUtf8.DrawTableColumn(length.ToString()); } } @@ -184,15 +183,15 @@ public unsafe class GlobalVariablesDrawer( ImUtf8.DrawTableColumn($"{resource->Consumers}"); ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + Penumbra.Dynamis.DrawPointer(resourceHandle); ImGui.TableNextColumn(); ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); ImGui.TableNextColumn(); uint dataLength = 0; - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); ImUtf8.DrawTableColumn($"{dataLength}"); ++_shownResourcesMap; } @@ -233,15 +232,15 @@ public unsafe class GlobalVariablesDrawer( ImUtf8.DrawTableColumn($"{resource->Consumers}"); ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + Penumbra.Dynamis.DrawPointer(resourceHandle); ImGui.TableNextColumn(); ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); ImGui.TableNextColumn(); uint dataLength = 0; - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); ImUtf8.DrawTableColumn($"{dataLength}"); ++_shownResourcesList; } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 778f776e..7499bffa 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -58,6 +58,19 @@ "resolved": "3.1.11", "contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" }, + "FlatSharp.Compiler": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A==" + }, + "FlatSharp.Runtime": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.3.0", @@ -94,6 +107,11 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "8.0.1", @@ -133,8 +151,10 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { + "FlatSharp.Compiler": "[7.9.0, )", + "FlatSharp.Runtime": "[7.9.0, )", "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.6.1, )", + "Penumbra.Api": "[5.10.0, )", "Penumbra.String": "[1.0.6, )" } }, From f69c2643176b2c2b08eb92e08facc945f96b3ce3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 13 Aug 2025 14:53:11 +0000 Subject: [PATCH 2368/2451] [CI] Updating repo.json for 1.5.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6045e266..f6d69c8b 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.4", - "TestingAssemblyVersion": "1.5.0.4", + "AssemblyVersion": "1.5.0.5", + "TestingAssemblyVersion": "1.5.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5917f5fad12125339d4cb52182dfc57bdbdbbf80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Aug 2025 17:42:40 +0200 Subject: [PATCH 2369/2451] Small fixes. --- Penumbra/Interop/Processing/PbdFilePostProcessor.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs index 69f2ecd5..674500cd 100644 --- a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs @@ -32,14 +32,14 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor if (resource->LoadState is not LoadState.Success) { - Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} failed load ({resource->LoadState})."); + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState})."); return; } var (data, length) = resource->GetData(); if (length is 0 || data == nint.Zero) { - Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} succeeded load but has no data."); + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data."); return; } @@ -47,7 +47,7 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor var reader = new PackReader(span); if (reader.HasData) { - Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {originalGamePath} with EPBD data."); + Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data."); return; } @@ -63,12 +63,12 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor _loadEpbdData(resource); // Free original data. _allocator.Release((void*)data, length); - Penumbra.Log.Verbose($"[ResourceLoader] Loaded {originalGamePath} from file and appended default EPBD data."); + Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data."); } else { Penumbra.Log.Warning( - $"[ResourceLoader] Failed to append EPBD data to custom PBD at {originalGamePath}."); + $"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}."); } } } From 87ace28bcfea13d8a50c8f8c1e3822a2f0302353 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Aug 2025 11:56:24 +0200 Subject: [PATCH 2370/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3ea61642..4a9b71a9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3 +Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89 From aa920b5e9b46f7139decfea27d248478212012fa Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 17 Aug 2025 01:41:49 +0200 Subject: [PATCH 2371/2451] Fix ImGui texture usage issue --- Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 241c3a91..24a5f9c2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -131,7 +131,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService if (texture == null) continue; var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); - if (handle == 0) + if (handle.IsNull) continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; From 41edc2382005e3ff2600872aefb276fabd741c30 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 17 Aug 2025 03:10:35 +0200 Subject: [PATCH 2372/2451] Allow changing the skin mtrl suffix --- .../Hooks/Resources/ResolvePathHooksBase.cs | 9 ++- .../Processing/SkinMtrlPathEarlyProcessing.cs | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 85fb1098..eecb98c5 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -6,6 +6,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Processing; using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -159,7 +160,13 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) - => ResolvePath(drawObject, _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + { + var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex); + if (finalPathBuffer != 0 && finalPathBuffer == pathBuffer) + SkinMtrlPathEarlyProcessing.Process(new Span((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex); + + return ResolvePath(drawObject, finalPathBuffer); + } private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs new file mode 100644 index 00000000..d35845e1 --- /dev/null +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -0,0 +1,61 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Processing; + +public static unsafe class SkinMtrlPathEarlyProcessing +{ + public static void Process(Span path, CharacterBase* character, uint slotIndex) + { + var end = path.IndexOf(".mtrl\0"u8); + if (end < 0) + return; + + var suffixPos = path[..end].LastIndexOf((byte)'_'); + if (suffixPos < 0) + return; + + var handle = GetModelResourceHandle(character, slotIndex); + if (handle == null) + return; + + var skinSuffix = GetSkinSuffix(handle); + if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7) + return; + + skinSuffix.CopyTo(path[(suffixPos + 1)..]); + ".mtrl\0"u8.CopyTo(path[(suffixPos + 1 + skinSuffix.Length)..]); + } + + private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex) + { + if (character == null) + return null; + + if (character->TempSlotData != null) + { + // TODO ClientStructs-ify + var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); + if (handle != null) + return handle; + } + + var model = character->Models[slotIndex]; + if (model == null) + return null; + + return model->ModelResourceHandle; + } + + private static ReadOnlySpan GetSkinSuffix(ModelResourceHandle* handle) + { + foreach (var (attribute, _) in handle->Attributes) + { + var attributeSpan = attribute.AsSpan(); + if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_') + return attributeSpan[12..]; + } + + return []; + } +} From 24cbc6c5e141aacddda897dc3e0a2eb637847f99 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Aug 2025 08:46:26 +0000 Subject: [PATCH 2373/2451] [CI] Updating repo.json for 1.5.0.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f6d69c8b..a452dc94 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.5", - "TestingAssemblyVersion": "1.5.0.5", + "AssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8304579d29788d4bb41b7af043516e3912561d86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Aug 2025 13:54:31 +0200 Subject: [PATCH 2374/2451] Add predefined tags to the multi mod selector. --- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 22 +++- Penumbra/UI/PredefinedTagManager.cs | 119 ++++++++++++++++-- 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 71c1a225..b8710707 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -30,7 +30,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 5b831a66..c3737b40 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.Count > 0; + var sharedTagsEnabled = predefinedTagManager.Enabled; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 3eac972c..947ede14 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using OtterGui.Extensions; +using OtterGui.Filesystem; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -10,7 +11,7 @@ using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) : IUiService +public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : IUiService { public void Draw() { @@ -97,7 +98,12 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) var width = ImGuiHelpers.ScaledVector2(150, 0); ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImGui.SameLine(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); + + var predefinedTagsEnabled = tagManager.Enabled; + var inputWidth = predefinedTagsEnabled + ? ImGui.GetContentRegionAvail().X - 2 * width.X - 3 * ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() + : ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.SetNextItemWidth(inputWidth); ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); UpdateTagCache(); @@ -109,7 +115,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) ? "No tag specified." : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name.Text))}"; - ImGui.SameLine(); + ImUtf8.SameLineInner(); if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) foreach (var mod in _addMods) editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); @@ -122,10 +128,18 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) ? "No tag specified." : $"No selected mod contains the tag \"{_tag}\" locally." : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name.Text))}"; - ImGui.SameLine(); + ImUtf8.SameLineInner(); if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) foreach (var (mod, index) in _removeMods) editor.ChangeLocalTag(mod, index, string.Empty); + + if (predefinedTagsEnabled) + { + ImUtf8.SameLineInner(); + tagManager.DrawToggleButton(); + tagManager.DrawListMulti(selector.SelectedPaths.OfType().Select(l => l.Value)); + } + ImGui.Separator(); } diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 7e268e8c..5a3a4b62 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -8,6 +8,8 @@ using OtterGui.Classes; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; @@ -52,6 +54,9 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer jObj.WriteTo(jWriter); } + public bool Enabled + => Count > 0; + public void Save() => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); @@ -98,9 +103,9 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer } public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, - Mods.Mod mod) + Mod mod) { - DrawToggleButton(); + DrawToggleButtonTopRight(); if (!DrawList(localTags, modTags, editLocal, out var changedTag, out var index)) return; @@ -110,17 +115,22 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer _modManager.DataEditor.ChangeModTag(mod, index, changedTag); } - private void DrawToggleButton() + public void DrawToggleButton() { - ImGui.SameLine(ImGui.GetContentRegionMax().X - - ImGui.GetFrameHeight() - - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), _isListOpen); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Predefined Tags...", false, true)) _isListOpen = !_isListOpen; } + private void DrawToggleButtonTopRight() + { + ImGui.SameLine(ImGui.GetContentRegionMax().X + - ImGui.GetFrameHeight() + - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); + DrawToggleButton(); + } + private bool DrawList(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, out string changedTag, out int changedIndex) { @@ -130,7 +140,7 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer if (!_isListOpen) return false; - ImGui.TextUnformatted("Predefined Tags"); + ImUtf8.Text("Predefined Tags"u8); ImGui.Separator(); var ret = false; @@ -155,6 +165,101 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer return ret; } + private readonly List _selectedMods = []; + private readonly List<(int Index, int DataIndex)> _countedMods = []; + + private void PrepareLists(IEnumerable selection) + { + _selectedMods.Clear(); + _selectedMods.AddRange(selection); + _countedMods.EnsureCapacity(_selectedMods.Count); + while (_countedMods.Count < _selectedMods.Count) + _countedMods.Add((-1, -1)); + } + + public void DrawListMulti(IEnumerable selection) + { + if (!_isListOpen) + return; + + ImUtf8.Text("Predefined Tags"u8); + PrepareLists(selection); + + _enabledColor = ColorId.PredefinedTagAdd.Value(); + _disabledColor = ColorId.PredefinedTagRemove.Value(); + using var color = new ImRaii.Color(); + foreach (var (tag, idx) in _predefinedTags.Keys.WithIndex()) + { + var alreadyContained = 0; + var inModData = 0; + var missing = 0; + + foreach (var (modIndex, mod) in _selectedMods.Index()) + { + var tagIdx = mod.LocalTags.IndexOf(tag); + if (tagIdx >= 0) + { + ++alreadyContained; + _countedMods[modIndex] = (tagIdx, -1); + } + else + { + var dataIdx = mod.ModTags.IndexOf(tag); + if (dataIdx >= 0) + { + ++inModData; + _countedMods[modIndex] = (-1, dataIdx); + } + else + { + ++missing; + _countedMods[modIndex] = (-1, -1); + } + } + } + + using var id = ImRaii.PushId(idx); + var buttonWidth = CalcTextButtonWidth(tag); + // Prevent adding a new tag past the right edge of the popup + if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + + var (usedColor, disabled, tt) = (missing, alreadyContained) switch + { + (> 0, _) => (_enabledColor, false, + $"Add this tag to {missing} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), + (_, > 0) => (_disabledColor, false, + $"Remove this tag from {alreadyContained} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), + _ => (_disabledColor, true, "This tag is already present in the mod tags of all selected mods."), + }; + color.Push(ImGuiCol.Button, usedColor); + if (ImUtf8.ButtonEx(tag, tt, new Vector2(buttonWidth, 0), disabled)) + { + if (missing > 0) + foreach (var (mod, (localIdx, _)) in _selectedMods.Zip(_countedMods)) + { + if (localIdx >= 0) + continue; + + _modManager.DataEditor.ChangeLocalTag(mod, mod.LocalTags.Count, tag); + } + else + foreach (var (mod, (localIdx, _)) in _selectedMods.Zip(_countedMods)) + { + if (localIdx < 0) + continue; + + _modManager.DataEditor.ChangeLocalTag(mod, localIdx, string.Empty); + } + } + ImGui.SameLine(); + + color.Pop(); + } + + ImGui.NewLine(); + } + private bool DrawColoredButton(string buttonLabel, int index, int tagIdx, bool inOther) { using var id = ImRaii.PushId(index); From 23257f94a4ad4db3b25bed01f9774a3e1512b3f1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Aug 2025 15:41:10 +0200 Subject: [PATCH 2375/2451] Some cleanup and add option to disable skin material attribute scanning. --- Penumbra/DebugConfiguration.cs | 3 ++- .../Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Processing/SkinMtrlPathEarlyProcessing.cs | 21 +++++++++++-------- .../UI/Tabs/Debug/DebugConfigurationDrawer.cs | 15 ++++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Penumbra/DebugConfiguration.cs b/Penumbra/DebugConfiguration.cs index 76987df8..3f9e8207 100644 --- a/Penumbra/DebugConfiguration.cs +++ b/Penumbra/DebugConfiguration.cs @@ -2,5 +2,6 @@ namespace Penumbra; public class DebugConfiguration { - public static bool WriteImcBytesToLog = false; + public static bool WriteImcBytesToLog = false; + public static bool UseSkinMaterialProcessing = true; } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index eecb98c5..db39889e 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -162,7 +162,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex); - if (finalPathBuffer != 0 && finalPathBuffer == pathBuffer) + if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer) SkinMtrlPathEarlyProcessing.Process(new Span((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex); return ResolvePath(drawObject, finalPathBuffer); diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index d35845e1..4487eb7f 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -7,7 +7,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing { public static void Process(Span path, CharacterBase* character, uint slotIndex) { - var end = path.IndexOf(".mtrl\0"u8); + var end = path.IndexOf(MaterialExtension()); if (end < 0) return; @@ -23,16 +23,22 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7) return; - skinSuffix.CopyTo(path[(suffixPos + 1)..]); - ".mtrl\0"u8.CopyTo(path[(suffixPos + 1 + skinSuffix.Length)..]); + ++suffixPos; + skinSuffix.CopyTo(path[suffixPos..]); + suffixPos += skinSuffix.Length; + MaterialExtension().CopyTo(path[suffixPos..]); + return; + + static ReadOnlySpan MaterialExtension() + => ".mtrl\0"u8; } private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex) { - if (character == null) + if (character is null) return null; - if (character->TempSlotData != null) + if (character->TempSlotData is not null) { // TODO ClientStructs-ify var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); @@ -41,10 +47,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing } var model = character->Models[slotIndex]; - if (model == null) - return null; - - return model->ModelResourceHandle; + return model is null ? null : model->ModelResourceHandle; } private static ReadOnlySpan GetSkinSuffix(ModelResourceHandle* handle) diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs index 97761091..087670c1 100644 --- a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -1,15 +1,16 @@ -using OtterGui.Text; - -namespace Penumbra.UI.Tabs.Debug; - +using OtterGui.Text; + +namespace Penumbra.UI.Tabs.Debug; + public static class DebugConfigurationDrawer { public static void Draw() { - using var id = ImUtf8.CollapsingHeaderId("Debug Logging Options"u8); + using var id = ImUtf8.CollapsingHeaderId("Debugging Options"u8); if (!id) return; - ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Scan for Skin Material Attributes"u8, ref DebugConfiguration.UseSkinMaterialProcessing); } -} +} From dad01e1af8c52ab3ddc46851b3f885bd5c3e2cf0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Aug 2025 15:24:00 +0200 Subject: [PATCH 2376/2451] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2cf59c61..15e7c8eb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac +Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7 From b7f326e29c87c31dc1a790a8feb7608a08bacfca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:43:55 +0200 Subject: [PATCH 2377/2451] Fix bug with collection setting and empty collection. --- Penumbra/Collections/CollectionAutoSelector.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs index 68dac914..f6e6bf72 100644 --- a/Penumbra/Collections/CollectionAutoSelector.cs +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable return; var collection = _resolver.PlayerCollection(); - Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); - _collections.SetCollection(collection, CollectionType.Current); + if (collection.Identity.Id == Guid.Empty) + { + Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned."); + } + else + { + Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); + _collections.SetCollection(collection, CollectionType.Current); + } } From e3b7f728932da3402f5e479319e459b23d018d74 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:44:33 +0200 Subject: [PATCH 2378/2451] Add initial PCP. --- Penumbra.Api | 2 +- Penumbra/Communication/ModPathChanged.cs | 8 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImport.cs | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 3 +- Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Services/PcpService.cs | 259 ++++++++++++++++++ .../UI/AdvancedWindow/ResourceTreeViewer.cs | 56 +++- .../ResourceTreeViewerFactory.cs | 5 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 5 +- Penumbra/UI/Tabs/SettingsTab.cs | 20 +- 11 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 Penumbra/Services/PcpService.cs diff --git a/Penumbra.Api b/Penumbra.Api index c27a0600..2e26d911 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165 +Subproject commit 2e26d9119249e67f03f415f8ebe1dcb7c28d5cf2 diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 1e4f8d36..efe59482 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.Communication; @@ -20,11 +21,14 @@ public sealed class ModPathChanged() { public enum Priority { + /// + PcpService = int.MinValue, + /// - ApiMods = int.MinValue, + ApiMods = int.MinValue + 1, /// - ApiModSettings = int.MinValue, + ApiModSettings = int.MinValue + 1, /// EphemeralConfig = -500, diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8c50dad7..e8f1d5ef 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -88,6 +88,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; + public string PcpFolderName { get; set; } = "PCP"; public string QuickMoveFolder1 { get; set; } = string.Empty; public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index fed06573..8e4fea41 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable // Puts out warnings if extension does not correspond to data. private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile) { - if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar") + if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar") return HandleRegularArchive(modPackFile); using var zfs = modPackFile.OpenRead(); diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index fc4fdadc..ffa73b76 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -36,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, - string? website) + string? website, params string[] tags) { var mod = new Mod(directory); mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); @@ -44,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; + mod.ModTags = tags; saveService.ImmediateSaveSync(new ModMeta(mod)); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 1bb2a073..3a7bd105 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -32,12 +32,12 @@ public partial class ModCreator( public readonly Configuration Config = config; /// Creates directory and files necessary for a new mod without adding it to the manager. - public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) + public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags) { try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags); CreateDefaultFiles(newDir); return newDir; } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs new file mode 100644 index 00000000..461045ba --- /dev/null +++ b/Penumbra/Services/PcpService.cs @@ -0,0 +1,259 @@ +using System.Buffers.Text; +using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceTree; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Services; + +public class PcpService : IApiService, IDisposable +{ + public const string Extension = ".pcp"; + + private readonly Configuration _config; + private readonly SaveService _files; + private readonly ResourceTreeFactory _treeFactory; + private readonly ObjectManager _objectManager; + private readonly ActorManager _actors; + private readonly FrameworkManager _framework; + private readonly CollectionResolver _collectionResolver; + private readonly CollectionManager _collections; + private readonly ModCreator _modCreator; + private readonly ModExportManager _modExport; + private readonly CommunicatorService _communicator; + private readonly SHA1 _sha1 = SHA1.Create(); + private readonly ModFileSystem _fileSystem; + + public PcpService(Configuration config, + SaveService files, + ResourceTreeFactory treeFactory, + ObjectManager objectManager, + ActorManager actors, + FrameworkManager framework, + CollectionManager collections, + CollectionResolver collectionResolver, + ModCreator modCreator, + ModExportManager modExport, + CommunicatorService communicator, + ModFileSystem fileSystem) + { + _config = config; + _files = files; + _treeFactory = treeFactory; + _objectManager = objectManager; + _actors = actors; + _framework = framework; + _collectionResolver = collectionResolver; + _collections = collections; + _modCreator = modCreator; + _modExport = modExport; + _communicator = communicator; + _fileSystem = fileSystem; + + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService); + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) + { + if (type is not ModPathChangeType.Added || newDirectory is null) + return; + + try + { + var file = Path.Combine(newDirectory.FullName, "collection.json"); + if (!File.Exists(file)) + return; + + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var identifier = _actors.FromJson(jObj["Actor"] as JObject); + if (!identifier.IsValid) + return; + + if (jObj["Collection"]?.ToObject() is not { } collectionName) + return; + + var name = $"PCP/{collectionName}"; + if (!_collections.Storage.AddCollection(name, null)) + return; + + var collection = _collections.Storage[^1]; + _collections.Editor.SetModState(collection, mod, true); + + var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); + _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + if (_fileSystem.TryGetValue(mod, out var leaf)) + { + try + { + var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpFolderName); + _fileSystem.Move(leaf, folder); + } + catch + { + // ignored. + } + } + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error reading the collection.json file from {mod.Identifier}:\n{ex}"); + } + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + + public async Task<(bool, string)> CreatePcp(ObjectIndex objectIndex, string note = "", CancellationToken cancel = default) + { + try + { + var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => + { + var (actor, identifier) = CheckActor(objectIndex); + cancel.ThrowIfCancellationRequested(); + unsafe + { + var collection = _collectionResolver.IdentifyCollection((GameObject*)actor.Address, true); + if (!collection.Valid || !collection.ModCollection.HasCache) + throw new Exception($"Actor {identifier} has no mods applying, nothing to do."); + + cancel.ThrowIfCancellationRequested(); + if (_treeFactory.FromCharacter(actor, 0) is not { } tree) + throw new Exception($"Unable to fetch modded resources for {identifier}."); + + return (identifier.CreatePermanent(), tree, collection); + } + }); + cancel.ThrowIfCancellationRequested(); + var time = DateTime.Now; + var modDirectory = CreateMod(identifier, note, time); + await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); + await CreateCollectionInfo(modDirectory, identifier, note, time, cancel); + var file = ZipUp(modDirectory); + return (true, file); + } + catch (Exception ex) + { + return (false, ex.Message); + } + } + + private static string ZipUp(DirectoryInfo directory) + { + var fileName = directory.FullName + Extension; + ZipFile.CreateFromDirectory(directory.FullName, fileName, CompressionLevel.Optimal, false); + directory.Delete(true); + return fileName; + } + + private static async Task CreateCollectionInfo(DirectoryInfo directory, ActorIdentifier actor, string note, DateTime time, + CancellationToken cancel = default) + { + var jObj = new JObject + { + ["Version"] = 1, + ["Actor"] = actor.ToJson(), + ["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(), + ["Time"] = time, + ["Note"] = note, + }; + if (note.Length > 0) + cancel.ThrowIfCancellationRequested(); + var filePath = Path.Combine(directory.FullName, "collection.json"); + await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); + await using var stream = new StreamWriter(file); + await using var json = new JsonTextWriter(stream); + json.Formatting = Formatting.Indented; + await jObj.WriteToAsync(json, cancel); + } + + private DirectoryInfo CreateMod(ActorIdentifier actor, string note, DateTime time) + { + var directory = _modExport.ExportDirectory; + directory.Create(); + var actorName = actor.ToName(); + var authorName = _actors.GetCurrentPlayer().ToName(); + var suffix = note.Length > 0 + ? note + : time.ToString("yyyy-MM-ddTHH\\:mm", CultureInfo.InvariantCulture); + var modName = $"{actorName} - {suffix}"; + var description = $"On-Screen Data for {actorName} as snapshotted on {time}."; + return _modCreator.CreateEmptyMod(directory, modName, description, authorName, "PCP") + ?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}."); + } + + private async Task CreateDefaultMod(DirectoryInfo modDirectory, ModCollection collection, ResourceTree tree, + CancellationToken cancel = default) + { + var subDirectory = modDirectory.CreateSubdirectory("files"); + var subMod = new DefaultSubMod(null!); + foreach (var node in tree.FlatNodes) + { + cancel.ThrowIfCancellationRequested(); + var gamePath = node.GamePath; + var fullPath = node.FullPath; + if (fullPath.IsRooted) + { + var hash = await _sha1.ComputeHashAsync(File.OpenRead(fullPath.FullName), cancel).ConfigureAwait(false); + cancel.ThrowIfCancellationRequested(); + var name = Convert.ToHexString(hash) + fullPath.Extension; + var newFile = Path.Combine(subDirectory.FullName, name); + if (!File.Exists(newFile)) + File.Copy(fullPath.FullName, newFile); + subMod.Files.TryAdd(gamePath, new FullPath(newFile)); + } + else if (gamePath.Path != fullPath.InternalName) + { + subMod.FileSwaps.TryAdd(gamePath, fullPath); + } + } + + cancel.ThrowIfCancellationRequested(); + subMod.Manipulations = new MetaDictionary(collection.MetaCache); + + var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport); + var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport); + cancel.ThrowIfCancellationRequested(); + await using var fileStream = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); + await using var writer = new StreamWriter(fileStream); + saveGroup.Save(writer); + } + + private (ICharacter Actor, ActorIdentifier Identifier) CheckActor(ObjectIndex objectIndex) + { + var actor = _objectManager[objectIndex]; + if (!actor.Valid) + throw new Exception($"No Actor at index {objectIndex} found."); + + if (!actor.Identifier(_actors, out var identifier)) + throw new Exception($"Could not create valid identifier for actor at index {objectIndex}."); + + if (!actor.IsCharacter) + throw new Exception($"Actor {identifier} at index {objectIndex} is not a valid character."); + + if (!actor.Model.Valid) + throw new Exception($"Actor {identifier} at index {objectIndex} has no model."); + + if (_objectManager.Objects.CreateObjectReference(actor.Address) is not ICharacter character) + throw new Exception($"Actor {identifier} at index {objectIndex} could not be converted to ICharacter"); + + return (character, identifier); + } +} diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 440baa2f..a2309343 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,15 +1,19 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Bindings.ImGui; -using OtterGui.Raii; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility; using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.Services; -using Penumbra.UI.Classes; using Penumbra.String; -using OtterGui.Extensions; +using Penumbra.UI.Classes; +using static System.Net.Mime.MediaTypeNames; namespace Penumbra.UI.AdvancedWindow; @@ -21,12 +25,13 @@ public class ResourceTreeViewer( int actionCapacity, Action onRefresh, Action drawActions, - CommunicatorService communicator) + CommunicatorService communicator, + PcpService pcpService) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly HashSet _unfolded = []; + private readonly HashSet _unfolded = []; private readonly Dictionary _filterCache = []; @@ -34,6 +39,7 @@ public class ResourceTreeViewer( private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; private string _nameFilter = string.Empty; private string _nodeFilter = string.Empty; + private string _note = string.Empty; private Task? _task; @@ -83,7 +89,28 @@ public class ResourceTreeViewer( using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImUtf8.TextFrameAligned($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Export Character Pack"u8, + "Note that this recomputes the current data of the actor if it still exists, and does not use the cached data."u8)) + { + pcpService.CreatePcp((ObjectIndex)tree.GameObjectIndex, _note).ContinueWith(t => + { + + var (success, text) = t.Result; + + if (success) + Penumbra.Messager.NotificationMessage($"Created {text}.", NotificationType.Success, false); + else + Penumbra.Messager.NotificationMessage(text, NotificationType.Error, false); + }); + _note = string.Empty; + } + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8); + using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -263,7 +290,8 @@ public class ResourceTreeViewer( using var group = ImUtf8.Group(); using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) { - ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); } ImGui.SameLine(); @@ -272,7 +300,8 @@ public class ResourceTreeViewer( } else { - ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); } if (ImGui.IsItemClicked()) @@ -365,9 +394,10 @@ public class ResourceTreeViewer( private static string GetPathStatusDescription(ResourceNode.PathStatus status) => status switch { - ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.", - ResourceNode.PathStatus.NonExistent => "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.", - _ => "The actual path to this file is unavailable.", + ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.", + ResourceNode.PathStatus.NonExistent => + "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.", + _ => "The actual path to this file is unavailable.", }; [Flags] diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 10a4aea2..ac06fe1a 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -9,8 +9,9 @@ public class ResourceTreeViewerFactory( ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, IncognitoService incognito, - CommunicatorService communicator) : IService + CommunicatorService communicator, + PcpService pcpService) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService); } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 16ff7b41..3f3c82aa 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -126,6 +126,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector + "Mod Packs{.ttmp,.ttmp2,.pmp,.pcp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp,.pcp},Archives{.zip,.7z,.rar},Penumbra Character Packs{.pcp}", (s, f) => { if (!s) return; @@ -445,7 +446,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Draw input for the default folder to sort put newly imported mods into. + private void DrawPcpFolder() + { + var tmp = _config.PcpFolderName; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) + _config.PcpFolderName = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder", + "The folder any penumbra character packs are moved to on import.\nLeave blank to import into Root."); + } + /// Draw all settings pertaining to advanced editing of mods. private void DrawModEditorSettings() @@ -1055,7 +1069,7 @@ public class SettingsTab : ITab, IUiService if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); - ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); + ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0)); } From 8043e6fb6be5751deb5a33657638a73542728c35 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:49:15 +0200 Subject: [PATCH 2379/2451] Add option to disable PCP. --- Penumbra/Configuration.cs | 1 + Penumbra/Services/PcpService.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e8f1d5ef..b9a0d9ce 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -68,6 +68,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; + public bool DisablePcpHandling { get; set; } = false; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 461045ba..5f4a844d 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -71,7 +71,7 @@ public class PcpService : IApiService, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - if (type is not ModPathChangeType.Added || newDirectory is null) + if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) return; try diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4bed1ef2..143709f4 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -598,6 +598,9 @@ public class SettingsTab : ITab, IUiService Checkbox("Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); + Checkbox("Handle PCP Files", + "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", + !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); From fb34238530f08918fdc18c63e553ca134f4f8fee Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Aug 2025 13:51:50 +0000 Subject: [PATCH 2380/2451] [CI] Updating repo.json for testing_1.5.0.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a452dc94..cf4fe6cb 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 10894d451a525e504422c4b16a3d3022601e8dfe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 18:08:22 +0200 Subject: [PATCH 2381/2451] Add Pcp Events. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 25 +++++++++++++++++------- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 ++ Penumbra/Communication/PcpCreation.cs | 20 +++++++++++++++++++ Penumbra/Communication/PcpParsing.cs | 21 ++++++++++++++++++++ Penumbra/Configuration.cs | 1 + Penumbra/Services/CommunicatorService.cs | 8 ++++++++ Penumbra/Services/PcpService.cs | 24 ++++++++++++++++------- 9 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 Penumbra/Communication/PcpCreation.cs create mode 100644 Penumbra/Communication/PcpParsing.cs diff --git a/Penumbra.Api b/Penumbra.Api index 2e26d911..0a970295 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2e26d9119249e67f03f415f8ebe1dcb7c28d5cf2 +Subproject commit 0a970295b2398683b1e49c46fd613541e2486210 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 78c62953..55f1e259 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using OtterGui.Compression; using OtterGui.Services; using Penumbra.Api.Enums; @@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable { switch (type) { - case ModPathChangeType.Deleted when oldDirectory != null: - ModDeleted?.Invoke(oldDirectory.Name); - break; - case ModPathChangeType.Added when newDirectory != null: - ModAdded?.Invoke(newDirectory.Name); - break; + case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break; + case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break; case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); break; @@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable } public void Dispose() - => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + { + _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + } public Dictionary GetModList() => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text); @@ -109,6 +108,18 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public event Action? ModAdded; public event Action? ModMoved; + public event Action? CreatingPcp + { + add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi); + remove => _communicator.PcpCreation.Unsubscribe(value!); + } + + public event Action? ParsingPcp + { + add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi); + remove => _communicator.PcpParsing.Unsubscribe(value!); + } + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod) diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 7ca41324..9e7eb964 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 10; + public const int FeatureVersion = 11; public void Dispose() { diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 7dcee375..0c80626f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModDeleted.Provider(pi, api.Mods), IpcSubscribers.ModAdded.Provider(pi, api.Mods), IpcSubscribers.ModMoved.Provider(pi, api.Mods), + IpcSubscribers.CreatingPcp.Provider(pi, api.Mods), + IpcSubscribers.ParsingPcp.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), diff --git a/Penumbra/Communication/PcpCreation.cs b/Penumbra/Communication/PcpCreation.cs new file mode 100644 index 00000000..cb11b3c3 --- /dev/null +++ b/Penumbra/Communication/PcpCreation.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered when the character.json file for a .pcp file is written. +/// +/// Parameter is the JObject that gets written to file. +/// Parameter is the object index of the game object this is written for. +/// +/// +public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) +{ + public enum Priority + { + /// + ModsApi = int.MinValue, + } +} diff --git a/Penumbra/Communication/PcpParsing.cs b/Penumbra/Communication/PcpParsing.cs new file mode 100644 index 00000000..95b78951 --- /dev/null +++ b/Penumbra/Communication/PcpParsing.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered when the character.json file for a .pcp file is parsed and applied. +/// +/// Parameter is parsed JObject that contains the data. +/// Parameter is the identifier of the created mod. +/// Parameter is the GUID of the created collection. +/// +/// +public sealed class PcpParsing() : EventWrapper(nameof(PcpParsing)) +{ + public enum Priority + { + /// + ModsApi = int.MinValue, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b9a0d9ce..d9a9f5fe 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -69,6 +69,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public bool DisablePcpHandling { get; set; } = false; + public bool AllowPcpIpc { get; set; } = true; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 5d745419..35f15e9e 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,6 +81,12 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); + /// + public readonly PcpCreation PcpCreation = new(); + + /// + public readonly PcpParsing PcpParsing = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -105,5 +111,7 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); + PcpCreation.Dispose(); + PcpParsing.Dispose(); } } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 5f4a844d..32eca652 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -1,4 +1,3 @@ -using System.Buffers.Text; using Dalamud.Game.ClientState.Objects.Types; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Newtonsoft.Json; @@ -76,9 +75,16 @@ public class PcpService : IApiService, IDisposable try { - var file = Path.Combine(newDirectory.FullName, "collection.json"); + var file = Path.Combine(newDirectory.FullName, "character.json"); if (!File.Exists(file)) - return; + { + // First version had collection.json, changed. + var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); + if (File.Exists(oldFile)) + File.Move(oldFile, file, true); + else + return; + } var text = File.ReadAllText(file); var jObj = JObject.Parse(text); @@ -110,10 +116,12 @@ public class PcpService : IApiService, IDisposable // ignored. } } + if (_config.AllowPcpIpc) + _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } catch (Exception ex) { - Penumbra.Log.Error($"Error reading the collection.json file from {mod.Identifier}:\n{ex}"); + Penumbra.Log.Error($"Error reading the character.json file from {mod.Identifier}:\n{ex}"); } } @@ -145,7 +153,7 @@ public class PcpService : IApiService, IDisposable var time = DateTime.Now; var modDirectory = CreateMod(identifier, note, time); await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); - await CreateCollectionInfo(modDirectory, identifier, note, time, cancel); + await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel); var file = ZipUp(modDirectory); return (true, file); } @@ -163,7 +171,7 @@ public class PcpService : IApiService, IDisposable return fileName; } - private static async Task CreateCollectionInfo(DirectoryInfo directory, ActorIdentifier actor, string note, DateTime time, + private async Task CreateCollectionInfo(DirectoryInfo directory, ObjectIndex index, ActorIdentifier actor, string note, DateTime time, CancellationToken cancel = default) { var jObj = new JObject @@ -176,7 +184,9 @@ public class PcpService : IApiService, IDisposable }; if (note.Length > 0) cancel.ThrowIfCancellationRequested(); - var filePath = Path.Combine(directory.FullName, "collection.json"); + if (_config.AllowPcpIpc) + await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index)); + var filePath = Path.Combine(directory.FullName, "character.json"); await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); await using var stream = new StreamWriter(file); await using var json = new JsonTextWriter(stream); From 0d643840592bac17b67a09dc66f971fed8dc35a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 20:31:40 +0200 Subject: [PATCH 2382/2451] Add cleanup buttons to PCP, add option to turn off PCP IPC. --- Penumbra/Services/PcpService.cs | 26 +++++++++++++++++++++++++- Penumbra/UI/Tabs/SettingsTab.cs | 21 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 32eca652..73c61cdb 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -38,6 +38,7 @@ public class PcpService : IApiService, IDisposable private readonly CommunicatorService _communicator; private readonly SHA1 _sha1 = SHA1.Create(); private readonly ModFileSystem _fileSystem; + private readonly ModManager _mods; public PcpService(Configuration config, SaveService files, @@ -50,7 +51,8 @@ public class PcpService : IApiService, IDisposable ModCreator modCreator, ModExportManager modExport, CommunicatorService communicator, - ModFileSystem fileSystem) + ModFileSystem fileSystem, + ModManager mods) { _config = config; _files = files; @@ -64,10 +66,27 @@ public class PcpService : IApiService, IDisposable _modExport = modExport; _communicator = communicator; _fileSystem = fileSystem; + _mods = mods; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService); } + public void CleanPcpMods() + { + var mods = _mods.Where(m => m.ModTags.Contains("PCP")).ToList(); + Penumbra.Log.Information($"[PCPService] Deleting {mods.Count} mods containing the tag PCP."); + foreach (var mod in mods) + _mods.DeleteMod(mod); + } + + public void CleanPcpCollections() + { + var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); + Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); + foreach (var collection in collections) + _collections.Storage.Delete(collection); + } + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) @@ -80,12 +99,14 @@ public class PcpService : IApiService, IDisposable { // First version had collection.json, changed. var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); + Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); if (File.Exists(oldFile)) File.Move(oldFile, file, true); else return; } + Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); var text = File.ReadAllText(file); var jObj = JObject.Parse(text); var identifier = _actors.FromJson(jObj["Actor"] as JObject); @@ -116,6 +137,7 @@ public class PcpService : IApiService, IDisposable // ignored. } } + if (_config.AllowPcpIpc) _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } @@ -132,6 +154,7 @@ public class PcpService : IApiService, IDisposable { try { + Penumbra.Log.Information($"[PCPService] Creating PCP file for game object {objectIndex.Index}."); var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => { var (actor, identifier) = CheckActor(objectIndex); @@ -178,6 +201,7 @@ public class PcpService : IApiService, IDisposable { ["Version"] = 1, ["Actor"] = actor.ToJson(), + ["Mod"] = directory.Name, ["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(), ["Time"] = time, ["Note"] = note, diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 143709f4..a6d03593 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -52,6 +52,7 @@ public class SettingsTab : ITab, IUiService private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; private readonly AttributeHook _attributeHook; + private readonly PcpService _pcpService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -64,7 +65,7 @@ public class SettingsTab : ITab, IUiService DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, - AttributeHook attributeHook) + AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; _config = config; @@ -90,6 +91,7 @@ public class SettingsTab : ITab, IUiService _autoSelector = autoSelector; _cleanupService = cleanupService; _attributeHook = attributeHook; + _pcpService = pcpService; } public void DrawHeader() @@ -601,6 +603,23 @@ public class SettingsTab : ITab, IUiService Checkbox("Handle PCP Files", "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); + + var active = _config.DeleteModModifier.IsActive(); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Mods"u8, "Deletes all mods tagged with 'PCP' from the mod list."u8, disabled: !active)) + _pcpService.CleanPcpMods(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, disabled: !active)) + _pcpService.CleanPcpCollections(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); + + Checkbox("Allow Other Plugins Access to PCP Handling", + "When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file.", + _config.AllowPcpIpc, v => _config.AllowPcpIpc = v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); From d302a17f1f2ca2f792acc8f28cf4722f3ded9be6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Aug 2025 18:33:43 +0000 Subject: [PATCH 2383/2451] [CI] Updating repo.json for testing_1.5.0.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cf4fe6cb..48d5b97f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.7", + "TestingAssemblyVersion": "1.5.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6079103505c3b0b99c26e041f59c26c41b13a543 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 23 Aug 2025 14:46:19 +0200 Subject: [PATCH 2384/2451] Add collection PCP settings. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Communication/PcpCreation.cs | 3 +- Penumbra/Configuration.cs | 19 ++++++---- Penumbra/Services/PcpService.cs | 51 ++++++++++++++++----------- Penumbra/UI/Tabs/SettingsTab.cs | 19 +++++++--- 6 files changed, 61 insertions(+), 35 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0a970295..297941bc 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0a970295b2398683b1e49c46fd613541e2486210 +Subproject commit 297941bc22300f4a8368f4d0177f62943eb69727 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 55f1e259..1f4f1cf4 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -108,7 +108,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public event Action? ModAdded; public event Action? ModMoved; - public event Action? CreatingPcp + public event Action? CreatingPcp { add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi); remove => _communicator.PcpCreation.Unsubscribe(value!); diff --git a/Penumbra/Communication/PcpCreation.cs b/Penumbra/Communication/PcpCreation.cs index cb11b3c3..ca0cfcf6 100644 --- a/Penumbra/Communication/PcpCreation.cs +++ b/Penumbra/Communication/PcpCreation.cs @@ -8,9 +8,10 @@ namespace Penumbra.Communication; /// /// Parameter is the JObject that gets written to file. /// Parameter is the object index of the game object this is written for. +/// Parameter is the full path to the directory being set up for the PCP creation. /// /// -public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) +public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) { public enum Priority { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index d9a9f5fe..f9cad217 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; +public record PcpSettings +{ + public bool CreateCollection { get; set; } = true; + public bool AssignCollection { get; set; } = true; + public bool AllowIpc { get; set; } = true; + public bool DisableHandling { get; set; } = false; + public string FolderName { get; set; } = "PCP"; +} + [Serializable] public class Configuration : IPluginConfiguration, ISavable, IService { @@ -68,11 +77,10 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; - public bool DisablePcpHandling { get; set; } = false; - public bool AllowPcpIpc { get; set; } = true; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public PcpSettings PcpSettings = new(); + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); @@ -90,7 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; - public string PcpFolderName { get; set; } = "PCP"; public string QuickMoveFolder1 { get; set; } = string.Empty; public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 73c61cdb..b9d472aa 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -89,7 +89,7 @@ public class PcpService : IApiService, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) + if (type is not ModPathChangeType.Added || _config.PcpSettings.DisableHandling || newDirectory is null) return; try @@ -107,29 +107,37 @@ public class PcpService : IApiService, IDisposable } Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); - var text = File.ReadAllText(file); - var jObj = JObject.Parse(text); - var identifier = _actors.FromJson(jObj["Actor"] as JObject); - if (!identifier.IsValid) - return; + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var collection = ModCollection.Empty; + // Create collection. + if (_config.PcpSettings.CreateCollection) + { + var identifier = _actors.FromJson(jObj["Actor"] as JObject); + if (identifier.IsValid && jObj["Collection"]?.ToObject() is { } collectionName) + { + var name = $"PCP/{collectionName}"; + if (_collections.Storage.AddCollection(name, null)) + { + collection = _collections.Storage[^1]; + _collections.Editor.SetModState(collection, mod, true); - if (jObj["Collection"]?.ToObject() is not { } collectionName) - return; + // Assign collection. + if (_config.PcpSettings.AssignCollection) + { + var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); + _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + } + } + } + } - var name = $"PCP/{collectionName}"; - if (!_collections.Storage.AddCollection(name, null)) - return; - - var collection = _collections.Storage[^1]; - _collections.Editor.SetModState(collection, mod, true); - - var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); - _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + // Move to folder. if (_fileSystem.TryGetValue(mod, out var leaf)) { try { - var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpFolderName); + var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpSettings.FolderName); _fileSystem.Move(leaf, folder); } catch @@ -138,7 +146,8 @@ public class PcpService : IApiService, IDisposable } } - if (_config.AllowPcpIpc) + // Invoke IPC. + if (_config.PcpSettings.AllowIpc) _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } catch (Exception ex) @@ -208,8 +217,8 @@ public class PcpService : IApiService, IDisposable }; if (note.Length > 0) cancel.ThrowIfCancellationRequested(); - if (_config.AllowPcpIpc) - await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index)); + if (_config.PcpSettings.AllowIpc) + await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index, directory.FullName)); var filePath = Path.Combine(directory.FullName, "character.json"); await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); await using var stream = new StreamWriter(file); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a6d03593..ded56bb1 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -602,7 +602,7 @@ public class SettingsTab : ITab, IUiService _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); Checkbox("Handle PCP Files", "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", - !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); + !_config.PcpSettings.DisableHandling, v => _config.PcpSettings.DisableHandling = !v); var active = _config.DeleteModModifier.IsActive(); ImGui.SameLine(); @@ -612,14 +612,23 @@ public class SettingsTab : ITab, IUiService ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); ImGui.SameLine(); - if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, disabled: !active)) + if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, + disabled: !active)) _pcpService.CleanPcpCollections(); if (!active) ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); Checkbox("Allow Other Plugins Access to PCP Handling", "When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file.", - _config.AllowPcpIpc, v => _config.AllowPcpIpc = v); + _config.PcpSettings.AllowIpc, v => _config.PcpSettings.AllowIpc = v); + + Checkbox("Create PCP Collections", + "When importing PCP files, create the associated collection.", + _config.PcpSettings.CreateCollection, v => _config.PcpSettings.CreateCollection = v); + + Checkbox("Assign PCP Collections", + "When importing PCP files and creating the associated collection, assign it to the associated character.", + _config.PcpSettings.AssignCollection, v => _config.PcpSettings.AssignCollection = v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); @@ -736,10 +745,10 @@ public class SettingsTab : ITab, IUiService /// Draw input for the default folder to sort put newly imported mods into. private void DrawPcpFolder() { - var tmp = _config.PcpFolderName; + var tmp = _config.PcpSettings.FolderName; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) - _config.PcpFolderName = tmp; + _config.PcpSettings.FolderName = tmp; if (ImGui.IsItemDeactivatedAfterEdit()) _config.Save(); From c8b6325a8733cfdbae82cb90b4eb2d903c6c1ca6 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 24 Aug 2025 08:34:54 +0200 Subject: [PATCH 2385/2451] Add game integrity message to On-Screen --- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 25 +++++++++++++++++-- .../ResourceTreeViewerFactory.cs | 6 +++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index a2309343..617ba30f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,7 +1,9 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; +using Dalamud.Plugin.Services; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -13,7 +15,6 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; -using static System.Net.Mime.MediaTypeNames; namespace Penumbra.UI.AdvancedWindow; @@ -26,7 +27,8 @@ public class ResourceTreeViewer( Action onRefresh, Action drawActions, CommunicatorService communicator, - PcpService pcpService) + PcpService pcpService, + IDataManager gameData) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; @@ -45,6 +47,7 @@ public class ResourceTreeViewer( public void Draw() { + DrawModifiedGameFilesWarning(); DrawControls(); _task ??= RefreshCharacterList(); @@ -130,6 +133,24 @@ public class ResourceTreeViewer( } } + private void DrawModifiedGameFilesWarning() + { + if (!gameData.HasModifiedGameDataFiles) + return; + + using var style = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + + ImUtf8.TextWrapped( + "Dalamud is reporting your FFXIV installation has modified game files. Any mods installed through TexTools will produce this message."u8); + ImUtf8.TextWrapped("Penumbra and some other plugins assume your FFXIV installation is unmodified in order to work."u8); + ImUtf8.TextWrapped( + "Data displayed here may be inaccurate because of this, which, in turn, can break functionality relying on it, such as Character Pack exports/imports, or mod synchronization functions provided by other plugins."u8); + ImUtf8.TextWrapped( + "Exit the game, open XIVLauncher, click the arrow next to Log In and select \"repair game files\" to resolve this issue. Afterwards, do not install any mods with TexTools. Your plugin configurations will remain, as will mods enabled in Penumbra."u8); + + ImGui.Separator(); + } + private void DrawControls() { var yOffset = (ChangedItemDrawer.TypeFilterIconSize.Y - ImGui.GetFrameHeight()) / 2f; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index ac06fe1a..43b60716 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Interop.ResourceTree; using Penumbra.Services; @@ -10,8 +11,9 @@ public class ResourceTreeViewerFactory( ChangedItemDrawer changedItemDrawer, IncognitoService incognito, CommunicatorService communicator, - PcpService pcpService) : IService + PcpService pcpService, + IDataManager gameData) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); } From 1fca78fa71239882dc7f88df72c31440a8660cb4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 24 Aug 2025 06:39:38 +0200 Subject: [PATCH 2386/2451] Add Kdb files to ResourceTree --- Penumbra.GameData | 2 +- .../Processing/SkinMtrlPathEarlyProcessing.cs | 2 +- .../ResolveContext.PathResolution.cs | 28 +++++++++++++++++++ .../Interop/ResourceTree/ResolveContext.cs | 23 ++++++++++++++- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 ++++++---- Penumbra/Interop/Structs/StructExtensions.cs | 9 ++++++ 7 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 15e7c8eb..73010350 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7 +Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4 diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index 4487eb7f..6be1b959 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -40,7 +40,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (character->TempSlotData is not null) { - // TODO ClientStructs-ify + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1564) var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); if (handle != null) return handle; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b6d04769..c204f141 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -338,6 +338,34 @@ internal partial record ResolveContext return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } + private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons. + // Additionally, it can dereference null pointers for human equipment skeletons. + return ModelType switch + { + ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex), + _ => ResolveKineDriverModulePathNative(partialSkeletonIndex), + }; + } + + private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set.Id is 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Kdb.Customization(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex) + { + var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) { var animation = ResolveImcData(imc).MaterialAnimationId; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b2364e33..bbe9b8ce 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -371,7 +371,8 @@ internal unsafe partial record ResolveContext( return node; } - public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) + public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle, + uint partialSkeletonIndex) { if (sklb is null || sklb->SkeletonResourceHandle is null) return null; @@ -386,6 +387,8 @@ internal unsafe partial record ResolveContext( node.Children.Add(skpNode); if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode) node.Children.Add(phybNode); + if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode) + node.Children.Add(kdbNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); return node; @@ -427,6 +430,24 @@ internal unsafe partial record ResolveContext( return node; } + private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex) + { + if (kdbHandle is null) + return null; + + var path = ResolveKineDriverModulePath(partialSkeletonIndex); + + if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Phyb, 0, kdbHandle, path, false); + if (Global.WithUiData) + node.FallbackName = "KineDriver Module"; + Global.Nodes.Add((path, (nint)kdbHandle), node); + + return node; + } + internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { var path = gamePath.Path.Split((byte)'/'); diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 3699ae0b..08dee818 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -45,7 +45,9 @@ public class ResourceNode : ICloneable /// Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). public bool Protected - => ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd; + => ForceProtected + || Internal + || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index ddef347d..23fe26b8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -121,7 +121,7 @@ public class ResourceTree( } } - AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); + AddSkeleton(Nodes, genericContext, model); AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton); AddWeapons(globalContext, model); @@ -178,8 +178,7 @@ public class ResourceTree( } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, - $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, "); AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton, $"Weapon #{weaponIndex}, "); @@ -242,8 +241,11 @@ public class ResourceTree( } } + private unsafe void AddSkeleton(List nodes, ResolveContext context, CharacterBase* model, string prefix = "") + => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix); + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, - string prefix = "") + void* kineDriver, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -259,7 +261,9 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; - if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1562) + var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null; + if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 031d24b1..5a29bb6f 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -64,6 +64,15 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); } + public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1561) + var vf80 = (delegate* unmanaged)((nint*)character.VirtualTable)[80]; + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, + partialSkeletonIndex)); + } + private static unsafe CiByteString ToOwnedByteString(CStringPointer str) => str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty; From f51f8a7bf80f5560c9a88251cad8766a71e17692 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 24 Aug 2025 15:24:50 +0200 Subject: [PATCH 2387/2451] Try to filter meta entries for relevance. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 182 ++++++++++++++++++ Penumbra/Services/PcpService.cs | 20 +- 2 files changed, 194 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index c2c9e777..8b448ec6 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,7 +1,10 @@ +using System.Collections.Frozen; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files.AtchStructs; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -40,6 +43,165 @@ public class MetaDictionary foreach (var geqp in cache.GlobalEqp.Keys) Add(geqp); } + + public static unsafe Wrapper Filtered(MetaCache cache, Actor actor) + { + if (!actor.IsCharacter) + return new Wrapper(cache); + + var model = actor.Model; + if (!model.IsHuman) + return new Wrapper(cache); + + var headId = model.GetModelId(HumanSlot.Head); + var bodyId = model.GetModelId(HumanSlot.Body); + var equipIdSet = ((IEnumerable) + [ + headId, + bodyId, + model.GetModelId(HumanSlot.Hands), + model.GetModelId(HumanSlot.Legs), + model.GetModelId(HumanSlot.Feet), + ]).ToFrozenSet(); + var earsId = model.GetModelId(HumanSlot.Ears); + var neckId = model.GetModelId(HumanSlot.Neck); + var wristId = model.GetModelId(HumanSlot.Wrists); + var rFingerId = model.GetModelId(HumanSlot.RFinger); + var lFingerId = model.GetModelId(HumanSlot.LFinger); + + var wrapper = new Wrapper(); + // Check for all relevant primary IDs due to slot overlap. + foreach (var (eqp, value) in cache.Eqp) + { + if (eqp.Slot.IsEquipment()) + { + if (equipIdSet.Contains(eqp.SetId)) + wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot)); + } + else + { + switch (eqp.Slot) + { + case EquipSlot.Ears when eqp.SetId == earsId: + case EquipSlot.Neck when eqp.SetId == neckId: + case EquipSlot.Wrists when eqp.SetId == wristId: + case EquipSlot.RFinger when eqp.SetId == rFingerId: + case EquipSlot.LFinger when eqp.SetId == lFingerId: + wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot)); + break; + } + } + } + + // Check also for body IDs due to body occupying head. + foreach (var (gmp, value) in cache.Gmp) + { + if (gmp.SetId == headId || gmp.SetId == bodyId) + wrapper.Gmp.Add(gmp, value.Entry); + } + + // Check for all races due to inheritance and all slots due to overlap. + foreach (var (eqdp, value) in cache.Eqdp) + { + if (eqdp.Slot.IsEquipment()) + { + if (equipIdSet.Contains(eqdp.SetId)) + wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot)); + } + else + { + switch (eqdp.Slot) + { + case EquipSlot.Ears when eqdp.SetId == earsId: + case EquipSlot.Neck when eqdp.SetId == neckId: + case EquipSlot.Wrists when eqdp.SetId == wristId: + case EquipSlot.RFinger when eqdp.SetId == rFingerId: + case EquipSlot.LFinger when eqdp.SetId == lFingerId: + wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot)); + break; + } + } + } + + var genderRace = (GenderRace)model.AsHuman->RaceSexId; + var hairId = model.GetModelId(HumanSlot.Hair); + var faceId = model.GetModelId(HumanSlot.Face); + // We do not need to care for racial inheritance for ESTs. + foreach (var (est, value) in cache.Est) + { + switch (est.Slot) + { + case EstType.Hair when est.SetId == hairId && est.GenderRace == genderRace: + case EstType.Face when est.SetId == faceId && est.GenderRace == genderRace: + case EstType.Body when est.SetId == bodyId && est.GenderRace == genderRace: + case EstType.Head when (est.SetId == headId || est.SetId == bodyId) && est.GenderRace == genderRace: + wrapper.Est.Add(est, value.Entry); + break; + } + } + + foreach (var (geqp, _) in cache.GlobalEqp) + { + switch (geqp.Type) + { + case GlobalEqpType.DoNotHideEarrings when geqp.Condition != earsId: + case GlobalEqpType.DoNotHideNecklace when geqp.Condition != neckId: + case GlobalEqpType.DoNotHideBracelets when geqp.Condition != wristId: + case GlobalEqpType.DoNotHideRingR when geqp.Condition != rFingerId: + case GlobalEqpType.DoNotHideRingL when geqp.Condition != lFingerId: + continue; + default: wrapper.Add(geqp); break; + } + } + + var (_, _, main, off) = model.GetWeapons(actor); + foreach (var (imc, value) in cache.Imc) + { + switch (imc.ObjectType) + { + case ObjectType.Equipment when equipIdSet.Contains(imc.PrimaryId): wrapper.Imc.Add(imc, value.Entry); break; + + case ObjectType.Weapon: + if (imc.PrimaryId == main.Skeleton && imc.SecondaryId == main.Weapon) + wrapper.Imc.Add(imc, value.Entry); + else if (imc.PrimaryId == off.Skeleton && imc.SecondaryId == off.Weapon) + wrapper.Imc.Add(imc, value.Entry); + break; + case ObjectType.Accessory: + switch (imc.EquipSlot) + { + case EquipSlot.Ears when imc.PrimaryId == earsId: + case EquipSlot.Neck when imc.PrimaryId == neckId: + case EquipSlot.Wrists when imc.PrimaryId == wristId: + case EquipSlot.RFinger when imc.PrimaryId == rFingerId: + case EquipSlot.LFinger when imc.PrimaryId == lFingerId: + wrapper.Imc.Add(imc, value.Entry); + break; + } + + break; + } + } + + var subRace = (SubRace)model.AsHuman->Customize[4]; + foreach (var (rsp, value) in cache.Rsp) + { + if (rsp.SubRace == subRace) + wrapper.Rsp.Add(rsp, value.Entry); + } + + // Keep all atch, atr and shp. + wrapper.Atch.EnsureCapacity(cache.Atch.Count); + wrapper.Shp.EnsureCapacity(cache.Shp.Count); + wrapper.Atr.EnsureCapacity(cache.Atr.Count); + foreach (var (atch, value) in cache.Atch) + wrapper.Atch.Add(atch, value.Entry); + foreach (var (shp, value) in cache.Shp) + wrapper.Shp.Add(shp, value.Entry); + foreach (var (atr, value) in cache.Atr) + wrapper.Atr.Add(atr, value.Entry); + return wrapper; + } } private Wrapper? _data; @@ -934,4 +1096,24 @@ public class MetaDictionary _data = new Wrapper(cache); Count = cache.Count; } + + public MetaDictionary(MetaCache? cache, Actor actor) + { + if (cache is null) + return; + + _data = Wrapper.Filtered(cache, actor); + Count = _data.Count + + _data.Eqp.Count + + _data.Eqdp.Count + + _data.Est.Count + + _data.Gmp.Count + + _data.Imc.Count + + _data.Rsp.Count + + _data.Atch.Count + + _data.Atr.Count + + _data.Shp.Count; + if (Count is 0) + _data = null; + } } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index b9d472aa..f75d3b5e 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -107,8 +107,8 @@ public class PcpService : IApiService, IDisposable } Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); - var text = File.ReadAllText(file); - var jObj = JObject.Parse(text); + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); var collection = ModCollection.Empty; // Create collection. if (_config.PcpSettings.CreateCollection) @@ -164,7 +164,7 @@ public class PcpService : IApiService, IDisposable try { Penumbra.Log.Information($"[PCPService] Creating PCP file for game object {objectIndex.Index}."); - var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => + var (identifier, tree, meta) = await _framework.Framework.RunOnFrameworkThread(() => { var (actor, identifier) = CheckActor(objectIndex); cancel.ThrowIfCancellationRequested(); @@ -178,13 +178,14 @@ public class PcpService : IApiService, IDisposable if (_treeFactory.FromCharacter(actor, 0) is not { } tree) throw new Exception($"Unable to fetch modded resources for {identifier}."); - return (identifier.CreatePermanent(), tree, collection); + var meta = new MetaDictionary(collection.ModCollection.MetaCache, actor.Address); + return (identifier.CreatePermanent(), tree, meta); } }); cancel.ThrowIfCancellationRequested(); var time = DateTime.Now; var modDirectory = CreateMod(identifier, note, time); - await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); + await CreateDefaultMod(modDirectory, meta, tree, cancel); await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel); var file = ZipUp(modDirectory); return (true, file); @@ -242,11 +243,15 @@ public class PcpService : IApiService, IDisposable ?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}."); } - private async Task CreateDefaultMod(DirectoryInfo modDirectory, ModCollection collection, ResourceTree tree, + private async Task CreateDefaultMod(DirectoryInfo modDirectory, MetaDictionary meta, ResourceTree tree, CancellationToken cancel = default) { var subDirectory = modDirectory.CreateSubdirectory("files"); - var subMod = new DefaultSubMod(null!); + var subMod = new DefaultSubMod(null!) + { + Manipulations = meta, + }; + foreach (var node in tree.FlatNodes) { cancel.ThrowIfCancellationRequested(); @@ -269,7 +274,6 @@ public class PcpService : IApiService, IDisposable } cancel.ThrowIfCancellationRequested(); - subMod.Manipulations = new MetaDictionary(collection.MetaCache); var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport); var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport); From 1e07e434985ce55cd47d783d1e6dc7f48e29c7b9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 24 Aug 2025 13:51:43 +0000 Subject: [PATCH 2388/2451] [CI] Updating repo.json for testing_1.5.0.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 48d5b97f..446932b5 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.8", + "TestingAssemblyVersion": "1.5.0.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a14347f73a39ae0579d721f9b77b05f3e989c8b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:31 +0200 Subject: [PATCH 2389/2451] Update temporary collection creation. --- Penumbra.Api | 2 +- Penumbra/Api/Api/IdentityChecker.cs | 7 +++++++ Penumbra/Api/Api/TemporaryApi.cs | 12 ++++++++++-- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 4 +++- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Api/Api/IdentityChecker.cs diff --git a/Penumbra.Api b/Penumbra.Api index 297941bc..af41b178 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 297941bc22300f4a8368f4d0177f62943eb69727 +Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5 diff --git a/Penumbra/Api/Api/IdentityChecker.cs b/Penumbra/Api/Api/IdentityChecker.cs new file mode 100644 index 00000000..e090053e --- /dev/null +++ b/Penumbra/Api/Api/IdentityChecker.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Api.Api; + +public static class IdentityChecker +{ + public static bool Check(string identity) + => true; +} diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index a997ded8..7567acd3 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -20,8 +20,16 @@ public class TemporaryApi( ApiHelpers apiHelpers, ModManager modManager) : IPenumbraApiTemporary, IApiService { - public Guid CreateTemporaryCollection(string name) - => tempCollections.CreateTemporaryCollection(name); + public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name) + { + if (!IdentityChecker.Check(identity)) + return (PenumbraApiEc.InvalidCredentials, Guid.Empty); + + var collection = tempCollections.CreateTemporaryCollection(name); + if (collection == Guid.Empty) + return (PenumbraApiEc.UnknownError, collection); + return (PenumbraApiEc.Success, collection); + } public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId) => tempCollections.RemoveTemporaryCollection(collectionId) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 64adf256..d46c5728 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -38,6 +38,7 @@ public class TemporaryIpcTester( private string _tempGamePath = "test/game/path.mtrl"; private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; + private string _identity = string.Empty; private PenumbraApiEc _lastTempError; private int _tempActorIndex; private bool _forceOverwrite; @@ -48,6 +49,7 @@ public class TemporaryIpcTester( if (!_) return; + ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128); ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); @@ -73,7 +75,7 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection"); if (ImGui.Button("Create##Collection")) { - LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName); + _lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId); if (_tempGuid == null) { _tempGuid = LastCreatedCollectionId; From bf90725dd2db6b300577fa0c64d309b5277eedee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:39 +0200 Subject: [PATCH 2390/2451] Fix resolvecontext issue. --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index bbe9b8ce..501bbc56 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -440,7 +440,7 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Phyb, 0, kdbHandle, path, false); + var node = CreateNode(ResourceType.Kdb, 0, kdbHandle, path, false); if (Global.WithUiData) node.FallbackName = "KineDriver Module"; Global.Nodes.Add((path, (nint)kdbHandle), node); From 79a4fc5904501fb30dd879ec37d8513c328ea120 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:48 +0200 Subject: [PATCH 2391/2451] Fix wrong logging. --- Penumbra/Services/PcpService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index f75d3b5e..63b8eab3 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -99,9 +99,11 @@ public class PcpService : IApiService, IDisposable { // First version had collection.json, changed. var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); - Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); if (File.Exists(oldFile)) + { + Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); File.Move(oldFile, file, true); + } else return; } From e16800f21649447cc316fa9ce8c7d88518ad19dd Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 25 Aug 2025 08:16:04 +0000 Subject: [PATCH 2392/2451] [CI] Updating repo.json for testing_1.5.0.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 446932b5..dea56357 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.9", + "TestingAssemblyVersion": "1.5.0.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From da47c19aeb30fcc293308652503b5cf1985a390d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:25:05 +0200 Subject: [PATCH 2393/2451] Woops, increment version. --- Penumbra/Api/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 9e7eb964..7304c9c7 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 11; + public const int FeatureVersion = 12; public void Dispose() { From c0120f81af3a713f861f275ad379a18ed14c0091 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:37:38 +0200 Subject: [PATCH 2394/2451] 1.5.1.0 --- Penumbra/Penumbra.json | 2 +- Penumbra/UI/Changelog.cs | 22 ++++++++++++++++-- repo.json | 48 ++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index bd9a2479..32032282 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -1,5 +1,5 @@ { - "Author": "Ottermandias, Adam, Wintermute", + "Author": "Ottermandias, Nylfae, Adam, Wintermute", "Name": "Penumbra", "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 4b487104..306dcc79 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -63,10 +63,28 @@ public class PenumbraChangelog : IUiService Add1_3_6_4(Changelog); Add1_4_0_0(Changelog); Add1_5_0_0(Changelog); - } - + Add1_5_1_0(Changelog); + } + #region Changelogs + private static void Add1_5_1_0(Changelog log) + => log.NextVersion("Version 1.5.1.0") + .RegisterHighlight("Added the option to export a characters current data as a .pcp modpack in the On-Screen tab.") + .RegisterEntry("Other plugins can attach to this functionality and package and interpret their own data.", 1) + .RegisterEntry("When a .pcp modpack is installed, it can create and assign collections for the corresponding character it was created for.", 1) + .RegisterEntry("This basically provides an easier way to manually synchronize other players, but does not contain any automation.", 1) + .RegisterEntry("The settings provide some fine control about what happens when a PCP is installed, as well as buttons to cleanup any PCP-created data.", 1) + .RegisterEntry("Added a warning message when the game's integrity is corrupted to the On-Screen tab.") + .RegisterEntry("Added .kdb files to the On-Screen tab and associated functionality (thanks Ny!).") + .RegisterEntry("Updated the creation of temporary collections to require a passed identity.") + .RegisterEntry("Added the option to change the skin material suffix in models using the stockings shader by adding specific attributes (thanks Ny!).") + .RegisterEntry("Added predefined tag utility to the multi-mod selection.") + .RegisterEntry("Fixed an issue with the automatic collection selection on character login when no mods are assigned.") + .RegisterImportant( + "Fixed issue with new deformer data that makes modded deformers not containing this data work implicitly. Updates are still recommended (1.5.0.5).") + .RegisterEntry("Fixed various issues after patch (1.5.0.1 - 1.5.0.4)."); + private static void Add1_5_0_0(Changelog log) => log.NextVersion("Version 1.5.0.0") .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") diff --git a/repo.json b/repo.json index dea56357..4675bccf 100644 --- a/repo.json +++ b/repo.json @@ -1,26 +1,26 @@ [ - { - "Author": "Ottermandias, Adam, Wintermute", - "Name": "Penumbra", - "Punchline": "Runtime mod loader and manager.", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.10", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" - } + { + "Author": "Ottermandias, Nylfae, Adam, Wintermute", + "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.10", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" + } ] From 71e24c13c7915e4741fe20fa86cc6dbebf1d2355 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 25 Aug 2025 08:39:42 +0000 Subject: [PATCH 2395/2451] [CI] Updating repo.json for 1.5.1.0 --- repo.json | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/repo.json b/repo.json index 4675bccf..e9a52799 100644 --- a/repo.json +++ b/repo.json @@ -1,26 +1,26 @@ [ - { - "Author": "Ottermandias, Nylfae, Adam, Wintermute", - "Name": "Penumbra", - "Punchline": "Runtime mod loader and manager.", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.10", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" - } + { + "Author": "Ottermandias, Nylfae, Adam, Wintermute", + "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "1.5.1.0", + "TestingAssemblyVersion": "1.5.1.0", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", + "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" + } ] From a04a5a071c99585f4d4bd749fc6b4f8b9d4dce99 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Aug 2025 18:51:57 +0200 Subject: [PATCH 2396/2451] Add warning in file redirections if extension doesn't match. --- Penumbra.Api | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index af41b178..953dd227 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5 +Subproject commit 953dd227afda6b3943b0b88cc965d8aee8a879b5 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 87d7487b..63c99b8a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -287,6 +287,17 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.IconFont); ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } + else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension) + { + ImGui.SameLine(); + ImGui.SetCursorPosX(pos); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString()); + } + + ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8); + } } private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod) @@ -319,6 +330,17 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.IconFont); ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } + else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension) + { + ImGui.SameLine(); + ImGui.SetCursorPosX(pos); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString()); + } + + ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8); + } } private void DrawButtonHeader() From f7cf5503bbd4c31b59c081f91b966afbc291b1f3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Aug 2025 18:52:06 +0200 Subject: [PATCH 2397/2451] Fix deleting PCP collections. --- Penumbra/Services/PcpService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 63b8eab3..bdf1adc5 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -84,7 +84,7 @@ public class PcpService : IApiService, IDisposable var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); foreach (var collection in collections) - _collections.Storage.Delete(collection); + _collections.Storage.RemoveCollection(collection); } private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) From 912020cc3f9a08324bb2515b0a35f22b720051cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Aug 2025 16:36:42 +0200 Subject: [PATCH 2398/2451] Update for staging and wrong tooltip. --- Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs | 5 ++--- Penumbra/Services/PcpService.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index 6be1b959..bd066d83 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -38,10 +38,9 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (character is null) return null; - if (character->TempSlotData is not null) + if (character->PerSlotStagingArea is not null) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1564) - var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); + var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle; if (handle != null) return handle; } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index bdf1adc5..17646564 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -82,7 +82,7 @@ public class PcpService : IApiService, IDisposable public void CleanPcpCollections() { var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); - Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); + Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} collections starting with PCP/."); foreach (var collection in collections) _collections.Storage.RemoveCollection(collection); } From 8c25ef4b47486df7b79c63d66c78fcf7710f2112 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:53:12 +0200 Subject: [PATCH 2399/2451] Make the save button ResourceTreeViewer baseline --- .../ModEditWindow.QuickImport.cs | 62 +---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 104 ++++++++++++++---- .../ResourceTreeViewerFactory.cs | 11 +- 4 files changed, 95 insertions(+), 84 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 72350857..f55ae576 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -17,7 +17,6 @@ public partial class ModEditWindow private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; - private readonly Dictionary _quickImportWritables = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); private HashSet GetPlayerResourcesOfType(ResourceType type) @@ -56,52 +55,11 @@ public partial class ModEditWindow private void OnQuickImportRefresh() { - _quickImportWritables.Clear(); _quickImportActions.Clear(); } - private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) + private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, Vector2 buttonSize) { - if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable)) - { - var path = resourceNode.FullPath.ToPath(); - if (resourceNode.FullPath.IsRooted) - { - writable = new RawFileWritable(path); - } - else - { - var file = _gameData.GetFile(path); - writable = file is null ? null : new RawGameFileWritable(file); - } - - _quickImportWritables.Add(resourceNode.FullPath, writable); - } - - if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize, - resourceNode.FullPath.FullName.Length is 0 || writable is null)) - { - var fullPathStr = resourceNode.FullPath.FullName; - var ext = resourceNode.PossibleGamePaths.Length == 1 - ? Path.GetExtension(resourceNode.GamePath.ToString()) - : Path.GetExtension(fullPathStr); - _fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, - (success, name) => - { - if (!success) - return; - - try - { - _editor.Compactor.WriteAllBytes(name, writable!.Write()); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); - } - }, null, false); - } - ImGui.SameLine(); if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) { @@ -121,24 +79,6 @@ public partial class ModEditWindow } } - private record RawFileWritable(string Path) : IWritable - { - public bool Valid - => true; - - public byte[] Write() - => File.ReadAllBytes(Path); - } - - private record RawGameFileWritable(FileResource FileResource) : IWritable - { - public bool Valid - => true; - - public byte[] Write() - => FileResource.Data; - } - public class QuickImportAction { public const string FallbackOptionName = "the current option"; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 952d8489..5a0fb849 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; if (IsOpen && selection.Mod != null) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 617ba30f..00003451 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -4,16 +4,20 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; +using Lumina.Data; using OtterGui; using OtterGui.Classes; +using OtterGui.Compression; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.String; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -25,17 +29,20 @@ public class ResourceTreeViewer( IncognitoService incognito, int actionCapacity, Action onRefresh, - Action drawActions, + Action drawActions, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; - private readonly Dictionary _filterCache = []; + private readonly Dictionary _filterCache = []; + private readonly Dictionary _writableCache = []; private TreeCategory _categoryFilter = AllCategories; private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; @@ -115,7 +122,7 @@ public class ResourceTreeViewer( ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8); - using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, + using var table = ImRaii.Table("##ResourceTree", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; @@ -123,9 +130,8 @@ public class ResourceTreeViewer( ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - if (actionCapacity > 0) - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, - (actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); @@ -211,6 +217,7 @@ public class ResourceTreeViewer( finally { _filterCache.Clear(); + _writableCache.Clear(); _unfolded.Clear(); onRefresh(); } @@ -221,7 +228,6 @@ public class ResourceTreeViewer( { var debugMode = config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { @@ -291,7 +297,7 @@ public class ResourceTreeViewer( 0 => "(none)", 1 => resourceNode.GamePath.ToString(), _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); if (hasGamePaths) { var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); @@ -312,7 +318,7 @@ public class ResourceTreeViewer( using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) { ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } ImGui.SameLine(); @@ -322,7 +328,7 @@ public class ResourceTreeViewer( else { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } if (ImGui.IsItemClicked()) @@ -336,20 +342,17 @@ public class ResourceTreeViewer( else { ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); ImGuiUtil.HoverTooltip( $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); - if (actionCapacity > 0) - { - ImGui.TableNextColumn(); - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); - drawActions(resourceNode, new Vector2(frameHeight)); - } + ImGui.TableNextColumn(); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); + DrawActions(resourceNode, new Vector2(frameHeight)); if (unfolded) DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); @@ -402,6 +405,51 @@ public class ResourceTreeViewer( || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } + + void DrawActions(ResourceNode resourceNode, Vector2 buttonSize) + { + if (!_writableCache!.TryGetValue(resourceNode.FullPath, out var writable)) + { + var path = resourceNode.FullPath.ToPath(); + if (resourceNode.FullPath.IsRooted) + { + writable = new RawFileWritable(path); + } + else + { + var file = gameData.GetFile(path); + writable = file is null ? null : new RawGameFileWritable(file); + } + + _writableCache.Add(resourceNode.FullPath, writable); + } + + if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize, + resourceNode.FullPath.FullName.Length is 0 || writable is null)) + { + var fullPathStr = resourceNode.FullPath.FullName; + var ext = resourceNode.PossibleGamePaths.Length == 1 + ? Path.GetExtension(resourceNode.GamePath.ToString()) + : Path.GetExtension(fullPathStr); + fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext, + (success, name) => + { + if (!success) + return; + + try + { + compactor.WriteAllBytes(name, writable!.Write()); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}"); + } + }, null, false); + } + + drawActions(resourceNode, writable, new Vector2(frameHeight)); + } } private static ReadOnlySpan GetPathStatusLabel(ResourceNode.PathStatus status) @@ -465,4 +513,22 @@ public class ResourceTreeViewer( Visible = 1, DescendentsOnly = 2, } + + private record RawFileWritable(string Path) : IWritable + { + public bool Valid + => true; + + public byte[] Write() + => File.ReadAllBytes(Path); + } + + private record RawGameFileWritable(FileResource FileResource) : IWritable + { + public bool Valid + => true; + + public byte[] Write() + => FileResource.Data; + } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 43b60716..6518ae67 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,5 +1,7 @@ using Dalamud.Plugin.Services; +using OtterGui.Compression; using OtterGui.Services; +using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Services; @@ -12,8 +14,11 @@ public class ResourceTreeViewerFactory( IncognitoService incognito, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) : IService + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) : IService { - public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); + public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData, + fileDialog, compactor); } From b3379a97105d37f685dd0686d89d0bf27c1c0807 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:55:20 +0200 Subject: [PATCH 2400/2451] Stop redacting external paths --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 00003451..cb765fcf 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -37,7 +37,7 @@ public class ResourceTreeViewer( FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = - ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; + ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; From f3ec4b2e081a4cb477f7c85189ac1525586f97c7 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 19:19:07 +0200 Subject: [PATCH 2401/2451] Only display the file name and last dir for externals --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index cb765fcf..ae450bec 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -325,6 +325,18 @@ public class ResourceTreeViewer( ImGui.SetCursorPosX(textPos); ImUtf8.Text(resourceNode.ModRelativePath); } + else if (resourceNode.FullPath.IsRooted) + { + var path = resourceNode.FullPath.FullName; + var lastDirectorySeparator = path.LastIndexOf('\\'); + var secondLastDirectorySeparator = lastDirectorySeparator > 0 + ? path.LastIndexOf('\\', lastDirectorySeparator - 1) + : -1; + if (secondLastDirectorySeparator >= 0) + path = $"…{path.AsSpan(secondLastDirectorySeparator)}"; + ImGui.Selectable(path.AsSpan(), false, ImGuiSelectableFlags.AllowItemOverlap, + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); + } else { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, From 5503bb32e059ed1438ebb139c5da6306e870f3b2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 04:13:56 +0200 Subject: [PATCH 2402/2451] CloudApi testing in Debug tab --- Penumbra/Interop/CloudApi.cs | 29 ++++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 39 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Penumbra/Interop/CloudApi.cs diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs new file mode 100644 index 00000000..9ec29fa5 --- /dev/null +++ b/Penumbra/Interop/CloudApi.cs @@ -0,0 +1,29 @@ +namespace Penumbra.Interop; + +public static unsafe partial class CloudApi +{ + private const int CfSyncRootInfoBasic = 0; + + public static bool IsCloudSynced(string path) + { + var buffer = stackalloc long[1]; + var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); + Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + if (hr < 0) + return false; + + if (length != sizeof(long)) + { + Penumbra.Log.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + return false; + } + + Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); + + return true; + } + + [LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)] + private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength, + out uint returnedLength); +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index d41dd25a..05f77e29 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; @@ -41,6 +42,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; @@ -206,6 +208,7 @@ public class DebugTab : Window, ITab, IUiService _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); + DrawCloudApi(); DrawDebugTabIpc(); } @@ -1199,6 +1202,42 @@ public class DebugTab : Window, ITab, IUiService } + private string _cloudTesterPath = string.Empty; + private bool? _cloudTesterReturn; + private Exception? _cloudTesterError; + + private void DrawCloudApi() + { + if (!ImUtf8.CollapsingHeader("Cloud API"u8)) + return; + + using var id = ImRaii.PushId("CloudApiTester"u8); + + if (ImUtf8.InputText("Path"u8, ref _cloudTesterPath, flags: ImGuiInputTextFlags.EnterReturnsTrue)) + { + try + { + _cloudTesterReturn = CloudApi.IsCloudSynced(_cloudTesterPath); + _cloudTesterError = null; + } + catch (Exception e) + { + _cloudTesterReturn = null; + _cloudTesterError = e; + } + } + + if (_cloudTesterReturn.HasValue) + ImUtf8.Text($"Is Cloud Synced? {_cloudTesterReturn}"); + + if (_cloudTesterError is not null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImUtf8.Text($"{_cloudTesterError}"); + } + } + + /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { From d59be1e660e26adce11664ffdbef5631e2511aeb Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 05:25:37 +0200 Subject: [PATCH 2403/2451] Refine IsCloudSynced --- Penumbra/Interop/CloudApi.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs index 9ec29fa5..603d4c9f 100644 --- a/Penumbra/Interop/CloudApi.cs +++ b/Penumbra/Interop/CloudApi.cs @@ -4,21 +4,39 @@ public static unsafe partial class CloudApi { private const int CfSyncRootInfoBasic = 0; + /// Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. + /// Can be expensive. Callers should cache the result when relevant. public static bool IsCloudSynced(string path) { - var buffer = stackalloc long[1]; - var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + var buffer = stackalloc long[1]; + int hr; + uint length; + try + { + hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length); + } + catch (DllNotFoundException) + { + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException"); + return false; + } + catch (EntryPointNotFoundException) + { + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException"); + return false; + } + + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}"); if (hr < 0) return false; if (length != sizeof(long)) { - Penumbra.Log.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); return false; } - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); return true; } From 2cf60b78cd73f01b6207325a2359663b39745079 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 06:42:45 +0200 Subject: [PATCH 2404/2451] Reject and warn about cloud-synced base directories --- Penumbra/Mods/Manager/ModManager.cs | 4 ++++ Penumbra/Penumbra.cs | 13 ++++++++----- Penumbra/UI/Tabs/SettingsTab.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 32dac049..77385bbd 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,5 +1,6 @@ using OtterGui.Services; using Penumbra.Communication; +using Penumbra.Interop; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Services; @@ -303,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService if (!firstTime && _config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(BasePath.FullName, Valid); } + + if (CloudApi.IsCloudSynced(BasePath.FullName)) + Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues."); } private void TriggerModDirectoryChange(string newPath, bool valid) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b22d049d..f036adc7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,6 +23,7 @@ using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Interop; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -211,10 +212,11 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { - var sb = new StringBuilder(10240); - var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); - var hdrEnabler = _services.GetService(); - var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; + var sb = new StringBuilder(10240); + var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); + var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory); + var hdrEnabler = _services.GetService(); + var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); @@ -223,7 +225,8 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n"); if (Dalamud.Utility.Util.IsWine()) sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n"); - sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); + sb.Append( + $"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ded56bb1..308cc471 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Interop; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; @@ -59,6 +60,9 @@ public class SettingsTab : ITab, IUiService private readonly TagButtons _sharedTags = new(); + private string _lastCloudSyncTestedPath = string.Empty; + private bool _lastCloudSyncTestResult = false; + public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, @@ -208,6 +212,15 @@ public class SettingsTab : ITab, IUiService if (IsSubPathOf(gameDir, newName)) return ("Path is not allowed to be inside your game folder.", false); + if (_lastCloudSyncTestedPath != newName) + { + _lastCloudSyncTestResult = CloudApi.IsCloudSynced(newName); + _lastCloudSyncTestedPath = newName; + } + + if (_lastCloudSyncTestResult) + return ("Path is not allowed to be cloud-synced.", false); + return selected ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) : ($"Click Here to Save (Current Directory: {old})", true); From ad1659caf637c6919f4cb3f03e918496cf5fc23b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 11:29:58 +0200 Subject: [PATCH 2405/2451] Update libraries. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 4a9b71a9..f3544447 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89 +Subproject commit f354444776591ae423e2d8374aae346308d81424 diff --git a/Penumbra.Api b/Penumbra.Api index 953dd227..dd141317 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 953dd227afda6b3943b0b88cc965d8aee8a879b5 +Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index abcb8e3d..1b1f0a28 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.GameData b/Penumbra.GameData index 73010350..3450df1f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4 +Subproject commit 3450df1f377543a226ded705e3db9e77ed2a0510 diff --git a/Penumbra.String b/Penumbra.String index 878acce4..c8611a0c 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd +Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 3159b736..fa45ffbf 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas From 4e788f7c2bfb5bf04f8e22d6ac56b489ff6ad942 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 11:51:59 +0200 Subject: [PATCH 2406/2451] Update sig. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3450df1f..27893a85 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3450df1f377543a226ded705e3db9e77ed2a0510 +Subproject commit 27893a85adb57a301dd93fd2c7d318bfd4c12a0f From f5f6dd3246202a186ca205afec4d4673219a673a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 16:12:01 +0200 Subject: [PATCH 2407/2451] Handle some TODOs. --- Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs | 3 +-- .../Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 ++-- Penumbra/Interop/Structs/StructExtensions.cs | 5 +---- Penumbra/Mods/Editor/ModMerger.cs | 1 - 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index e0eb7ec5..cdd82b95 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -63,8 +63,7 @@ public sealed unsafe class LoadTimelineResources : FastHook**)timeline)[0][29](timeline); + var idx = timeline->GetOwningGameObjectIndex(); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b9c21556..dd708e51 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -434,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify - var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var unkPointer = unkPayload->ModelResourceHandle.*(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; var materialIndex = *(ushort*)(unkPointer + 8); var material = unkPayload->Params->Model->Materials[materialIndex]; if (material == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 23fe26b8..345dd0fd 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -242,10 +242,10 @@ public class ResourceTree( } private unsafe void AddSkeleton(List nodes, ResolveContext context, CharacterBase* model, string prefix = "") - => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix); + => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix); private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, - void* kineDriver, string prefix = "") + BoneKineDriverModule* kineDriver, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 5a29bb6f..7349f6cc 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -66,11 +66,8 @@ internal static class StructExtensions public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1561) - var vf80 = (delegate* unmanaged)((nint*)character.VirtualTable)[80]; var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, - partialSkeletonIndex)); + return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex)); } private static unsafe CiByteString ToOwnedByteString(CStringPointer str) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index bb84173a..eb270e13 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -372,7 +372,6 @@ public class ModMerger : IDisposable, IService } else { - // TODO DataContainer <> Option. var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); From 5a6e06df3ba6a7ed056199b03f540ac567a52be9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 16:22:02 +0200 Subject: [PATCH 2408/2451] git is stupid --- .../Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index dd708e51..b9c21556 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -434,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify - var unkPointer = unkPayload->ModelResourceHandle.*(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; var materialIndex = *(ushort*)(unkPointer + 8); var material = unkPayload->Params->Model->Materials[materialIndex]; if (material == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 345dd0fd..1ebfe53d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -261,8 +261,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1562) - var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null; + var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) From 6348c4a639811786d2302ac021914dcd89a65a2b Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 2 Sep 2025 14:25:55 +0000 Subject: [PATCH 2409/2451] [CI] Updating repo.json for 1.5.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e9a52799..9ff227b6 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.0", - "TestingAssemblyVersion": "1.5.1.0", + "AssemblyVersion": "1.5.1.2", + "TestingAssemblyVersion": "1.5.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c3b00ff42613270e3a8452dcafebaa795b9c226b Mon Sep 17 00:00:00 2001 From: Stoia <23234609+StoiaCode@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:22:18 +0200 Subject: [PATCH 2410/2451] Integrate FileWatcher HEAVY WIP --- Penumbra/Configuration.cs | 2 + Penumbra/Penumbra.cs | 2 + Penumbra/Services/FileWatcher.cs | 136 +++++++++++++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 47 ++++++++++- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Services/FileWatcher.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f9cad217..500d5d57 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -53,6 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; + public string WatchDirectory { get; set; } = string.Empty; public bool? UseCrashHandler { get; set; } = null; public bool OpenWindowAtStart { get; set; } = false; @@ -76,6 +77,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; + public bool EnableDirectoryWatch { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public PcpSettings PcpSettings = new(); public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f036adc7..0f5703a3 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -44,6 +44,7 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; + private readonly FileWatcher _fileWatcher; private readonly CollectionManager _collectionManager; private readonly Configuration _config; private readonly CharacterUtility _characterUtility; @@ -81,6 +82,7 @@ public class Penumbra : IDalamudPlugin _residentResources = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _modManager = _services.GetService(); + _fileWatcher = _services.GetService(); _collectionManager = _services.GetService(); _tempCollections = _services.GetService(); _redrawService = _services.GetService(); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs new file mode 100644 index 00000000..8a2f9402 --- /dev/null +++ b/Penumbra/Services/FileWatcher.cs @@ -0,0 +1,136 @@ +using System.Threading.Channels; +using OtterGui.Services; +using Penumbra.Mods.Manager; + +namespace Penumbra.Services; +public class FileWatcher : IDisposable, IService +{ + private readonly FileSystemWatcher _fsw; + private readonly Channel _queue; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _consumer; + private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); + private readonly ModImportManager _modImportManager; + private readonly Configuration _config; + private readonly bool _enabled; + + public FileWatcher(ModImportManager modImportManager, Configuration config) + { + _config = config; + _modImportManager = modImportManager; + _enabled = config.EnableDirectoryWatch; + + if (!_enabled) return; + + _queue = Channel.CreateBounded(new BoundedChannelOptions(256) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.DropOldest + }); + + _fsw = new FileSystemWatcher(_config.WatchDirectory) + { + IncludeSubdirectories = false, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, + InternalBufferSize = 32 * 1024 + }; + + // Only wake us for the exact patterns we care about + _fsw.Filters.Add("*.pmp"); + _fsw.Filters.Add("*.pcp"); + _fsw.Filters.Add("*.ttmp"); + _fsw.Filters.Add("*.ttmp2"); + + _fsw.Created += OnPath; + _fsw.Renamed += OnPath; + + _consumer = Task.Factory.StartNew( + () => ConsumerLoopAsync(_cts.Token), + _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + + _fsw.EnableRaisingEvents = true; + } + + private void OnPath(object? sender, FileSystemEventArgs e) + { + // Cheap de-dupe: only queue once per filename until processed + if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return; + _ = _queue.Writer.TryWrite(e.FullPath); + } + + private async Task ConsumerLoopAsync(CancellationToken token) + { + if (!_enabled) return; + var reader = _queue.Reader; + while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) + { + while (reader.TryRead(out var path)) + { + try + { + await ProcessOneAsync(path, token).ConfigureAwait(false); + } + catch (OperationCanceledException) { Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); } + catch (Exception ex) + { + Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}"); + } + finally + { + _pending.TryRemove(path, out _); + } + } + } + } + + private async Task ProcessOneAsync(string path, CancellationToken token) + { + // Downloads often finish via rename; file may be locked briefly. + // Wait until it exists and is readable; also require two stable size checks. + const int maxTries = 40; + long lastLen = -1; + + for (int i = 0; i < maxTries && !token.IsCancellationRequested; i++) + { + if (!File.Exists(path)) { await Task.Delay(100, token); continue; } + + try + { + var fi = new FileInfo(path); + var len = fi.Length; + if (len > 0 && len == lastLen) + { + _modImportManager.AddUnpack(path); + return; + } + + lastLen = len; + } + catch (IOException) { Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); } + catch (UnauthorizedAccessException) { Penumbra.Log.Debug($"[FileWatcher] File is locked."); } + + await Task.Delay(150, token); + } + } + + public void UpdateDirectory(string newPath) + { + if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + + _fsw.EnableRaisingEvents = false; + _fsw.Path = newPath; + _fsw.EnableRaisingEvents = true; + } + + public void Dispose() + { + if (!_enabled) return; + _fsw.EnableRaisingEvents = false; + _cts.Cancel(); + _fsw.Dispose(); + _queue.Writer.TryComplete(); + try { _consumer.Wait(TimeSpan.FromSeconds(5)); } catch { /* swallow */ } + _cts.Dispose(); + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 308cc471..c84214f3 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -37,6 +37,7 @@ public class SettingsTab : ITab, IUiService private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; + private readonly FileWatcher _fileWatcher; private readonly ModExportManager _modExportManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; @@ -65,7 +66,7 @@ public class SettingsTab : ITab, IUiService public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, - CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, @@ -82,6 +83,7 @@ public class SettingsTab : ITab, IUiService _characterUtility = characterUtility; _residentResources = residentResources; _modExportManager = modExportManager; + _fileWatcher = fileWatcher; _httpApi = httpApi; _dalamudSubstitutionProvider = dalamudSubstitutionProvider; _compactor = compactor; @@ -647,6 +649,10 @@ public class SettingsTab : ITab, IUiService DrawDefaultModImportFolder(); DrawPcpFolder(); DrawDefaultModExportPath(); + Checkbox("Enable Automatic Import of Mods from Directory", + "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.", + _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + DrawFileWatcherPath(); } @@ -726,6 +732,45 @@ public class SettingsTab : ITab, IUiService + "Keep this empty to use the root directory."); } + private string _tempWatchDirectory = string.Empty; + /// Draw input for the Automatic Mod import path. + private void DrawFileWatcherPath() + { + var tmp = _config.WatchDirectory; + var spacing = new Vector2(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + if (ImGui.InputText("##fileWatchPath", ref tmp, 256)) + _tempWatchDirectory = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _fileWatcher.UpdateDirectory(_tempWatchDirectory); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + { + var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory) + ? _config.WatchDirectory + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : null; + _fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) => + { + if (b) + { + _fileWatcher.UpdateDirectory(s); + _config.WatchDirectory = s; + _config.Save(); + } + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Automatic Import Director", + "Choose the Directory the File Watcher listens to."); + } + /// Draw input for the default name to input as author into newly generated mods. private void DrawDefaultModAuthor() { From 97c8d82b338be04c513df4d15f1ef72a6fbbed4c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Sep 2025 10:45:28 +0200 Subject: [PATCH 2411/2451] Prevent default-named collection from being renamed and always put it at the top of the selector. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 44 ++++++++++--------- .../UI/CollectionTab/CollectionSelector.cs | 3 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 26fa2b14..e41ceade 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -11,6 +11,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -222,26 +223,31 @@ public sealed class CollectionPanel( ImGui.EndGroup(); ImGui.SameLine(); ImGui.BeginGroup(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = _newName ?? collection.Identity.Name; - var identifier = collection.Identity.Identifier; - var width = ImGui.GetContentRegionAvail().X; - var fileName = saveService.FileNames.CollectionFile(collection); - ImGui.SetNextItemWidth(width); - if (ImGui.InputText("##name", ref name, 128)) - _newName = name; - if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) + var width = ImGui.GetContentRegionAvail().X; + using (ImRaii.Disabled(_collections.DefaultNamed == collection)) { - collection.Identity.Name = _newName; - saveService.QueueSave(new ModCollectionSave(mods, collection)); - selector.RestoreCollections(); - _newName = null; - } - else if (ImGui.IsItemDeactivated()) - { - _newName = null; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var name = _newName ?? collection.Identity.Name; + ImGui.SetNextItemWidth(width); + if (ImGui.InputText("##name", ref name, 128)) + _newName = name; + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) + { + collection.Identity.Name = _newName; + saveService.QueueSave(new ModCollectionSave(mods, collection)); + selector.RestoreCollections(); + _newName = null; + } + else if (ImGui.IsItemDeactivated()) + { + _newName = null; + } } + if (_collections.DefaultNamed == collection) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "The Default collection can not be renamed."u8); + var identifier = collection.Identity.Identifier; + var fileName = saveService.FileNames.CollectionFile(collection); using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0))) @@ -375,9 +381,7 @@ public sealed class CollectionPanel( ImGuiUtil.TextWrapped(type.ToDescription()); switch (type) { - case CollectionType.Default: - ImGui.TextUnformatted("Overruled by any other Assignment."); - break; + case CollectionType.Default: ImGui.TextUnformatted("Overruled by any other Assignment."); break; case CollectionType.Yourself: ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); break; diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index e54f994e..79254090 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -116,7 +116,8 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl public void RestoreCollections() { Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Identity.Name)) + Items.Add(_storage.DefaultNamed); + foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed)) Items.Add(c); SetFilterDirty(); SetCurrent(_active.Current); From e9f67a009be51377226186d61b10340683f5d3f3 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 19 Sep 2025 03:50:28 +0200 Subject: [PATCH 2412/2451] Lift "shaders known" restriction for saving materials --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index e15d1c90..2c7c889e 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -216,7 +216,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable } public bool Valid - => _shadersKnown && Mtrl.Valid; + => Mtrl.Valid; // FIXME This should be _shadersKnown && Mtrl.Valid but the algorithm for _shadersKnown is flawed as of 7.2. public byte[] Write() { From a59689ebfe043b14d4c87f09bad3baddd10bea78 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Sep 2025 13:00:12 +0200 Subject: [PATCH 2413/2451] CS API update and add http API routes. --- Penumbra/Api/HttpApi.cs | 58 +++++++++++++++++++--- Penumbra/Interop/Services/RedrawService.cs | 4 +- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 8f8b44f4..dca9426a 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -5,6 +5,7 @@ using EmbedIO.WebApi; using OtterGui.Services; using Penumbra.Api.Api; using Penumbra.Api.Enums; +using Penumbra.Mods.Settings; namespace Penumbra.Api; @@ -13,13 +14,15 @@ public class HttpApi : IDisposable, IApiService private partial class Controller : WebApiController { // @formatter:off - [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); - [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); - [Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll(); - [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); - [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); - [Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); - [Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod(); + [Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory(); + [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); + [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); + [Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll(); + [Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod(); + [Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod(); + [Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow(); + [Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod(); + [Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings(); // @formatter:on } @@ -65,6 +68,12 @@ public class HttpApi : IDisposable, IApiService private partial class Controller(IPenumbraApi api, IFramework framework) { + public partial string GetModDirectory() + { + Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered."); + return api.PluginState.GetModDirectory(); + } + public partial object? GetMods() { Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); @@ -116,6 +125,7 @@ public class HttpApi : IDisposable, IApiService Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } + public async partial Task FocusMod() { var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); @@ -124,6 +134,30 @@ public class HttpApi : IDisposable, IApiService api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name); } + public async partial Task SetModSettings() + { + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); + Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered."); + await framework.RunOnFrameworkThread(() => + { + var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id; + if (data.Inherit.HasValue) + { + api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value); + if (data.Inherit.Value) + return; + } + + if (data.State.HasValue) + api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value); + if (data.Priority.HasValue) + api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value.Value); + foreach (var (group, settings) in data.Settings ?? []) + api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings); + } + ).ConfigureAwait(false); + } + private record ModReloadData(string Path, string Name) { public ModReloadData() @@ -151,5 +185,15 @@ public class HttpApi : IDisposable, IApiService : this(string.Empty, RedrawType.Redraw, -1) { } } + + private record SetModSettingsData( + Guid? CollectionId, + string ModPath, + string ModName, + bool? Inherit, + bool? State, + ModPriority? Priority, + Dictionary>? Settings) + { } } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 08e9ddf5..2d741277 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable return; - foreach (ref var f in currentTerritory->Furniture) + foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory) { - var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null; + var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null; if (gameObject == null) continue; From a0c3e820b0e9be6080f83d10447971bdaba5681d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 Sep 2025 11:02:39 +0000 Subject: [PATCH 2414/2451] [CI] Updating repo.json for testing_1.5.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9ff227b6..bac039b8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.2", + "TestingAssemblyVersion": "1.5.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c6b596169c0f970a7e4ee7bdf21f89347de8c0d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Sep 2025 14:01:21 +0200 Subject: [PATCH 2415/2451] Add default constructor. --- Penumbra/Api/HttpApi.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index dca9426a..995a6cd7 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -194,6 +194,10 @@ public class HttpApi : IDisposable, IApiService bool? State, ModPriority? Priority, Dictionary>? Settings) - { } + { + public SetModSettingsData() + : this(null, string.Empty, string.Empty, null, null, null, null) + {} + } } } From eb53f04c6b2b88806227981fdbc8c53f193e0ada Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 Sep 2025 12:03:35 +0000 Subject: [PATCH 2416/2451] [CI] Updating repo.json for testing_1.5.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index bac039b8..f404b8af 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.3", + "TestingAssemblyVersion": "1.5.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 699745413e224f2b55e9eb7bf014e13c821408c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Sep 2025 12:40:52 +0200 Subject: [PATCH 2417/2451] Make priority an int. --- Penumbra/Api/HttpApi.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 995a6cd7..79348a88 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -151,7 +151,7 @@ public class HttpApi : IDisposable, IApiService if (data.State.HasValue) api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value); if (data.Priority.HasValue) - api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value.Value); + api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value); foreach (var (group, settings) in data.Settings ?? []) api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings); } @@ -192,7 +192,7 @@ public class HttpApi : IDisposable, IApiService string ModName, bool? Inherit, bool? State, - ModPriority? Priority, + int? Priority, Dictionary>? Settings) { public SetModSettingsData() From 23c0506cb875f8613513f4169630eeb6549cc6ef Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Sep 2025 10:43:01 +0000 Subject: [PATCH 2418/2451] [CI] Updating repo.json for testing_1.5.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f404b8af..d6a7dd4c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.4", + "TestingAssemblyVersion": "1.5.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0881dfde8a26ebcea56bab0c9c5eadeca8884039 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Oct 2025 12:27:35 +0200 Subject: [PATCH 2419/2451] Update signatures. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 27893a85..7e7d510a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 27893a85adb57a301dd93fd2c7d318bfd4c12a0f +Subproject commit 7e7d510a2ce78e2af78312a8b2215c23bf43a56f From 049baa4fe49c0386532dd096663fc4368fd9dcf8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Oct 2025 12:42:54 +0200 Subject: [PATCH 2420/2451] Again. --- Penumbra.GameData | 2 +- Penumbra/Penumbra.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 7e7d510a..3baace73 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 7e7d510a2ce78e2af78312a8b2215c23bf43a56f +Subproject commit 3baace73c828271dcb71a8156e3e7b91e1dd12ae diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f036adc7..d433a0fb 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,7 +21,6 @@ using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop; using Penumbra.Interop.Hooks; From 300e0e6d8484f44c00a9320b48e068b10ea2ab1c Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 7 Oct 2025 10:45:04 +0000 Subject: [PATCH 2421/2451] [CI] Updating repo.json for 1.5.1.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index d6a7dd4c..2a31b75e 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.5", + "AssemblyVersion": "1.5.1.6", + "TestingAssemblyVersion": "1.5.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ebbe957c95d44d2b1569c4e22b3a7cd672246385 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Oct 2025 20:09:52 +0200 Subject: [PATCH 2422/2451] Remove login screen log spam. --- Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 10795e6d..136393d4 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver( { var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); - Penumbra.Log.Verbose( + Penumbra.Log.Excessive( $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll) { From 7ed81a982365fa99164a2ab5d8cdb6801987c0d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 17:53:02 +0200 Subject: [PATCH 2423/2451] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f3544447..9af1e5fc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f354444776591ae423e2d8374aae346308d81424 +Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 From f05cb52da2a77dc8b6bcd5cad3dd4b32d97febb3 Mon Sep 17 00:00:00 2001 From: Stoia <23234609+StoiaCode@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:20:44 +0200 Subject: [PATCH 2424/2451] Add Option to notify instead of auto install. And General Fixes --- Penumbra/Configuration.cs | 1 + Penumbra/Services/FileWatcher.cs | 42 +++++++++++++++++++++-------- Penumbra/Services/MessageService.cs | 32 ++++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 11 +++++--- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 500d5d57..e337997b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -78,6 +78,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableDirectoryWatch { get; set; } = false; + public bool EnableAutomaticModImport { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public PcpSettings PcpSettings = new(); public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 8a2f9402..e7172f58 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,5 +1,6 @@ using System.Threading.Channels; using OtterGui.Services; +using Penumbra.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; @@ -11,16 +12,16 @@ public class FileWatcher : IDisposable, IService private readonly Task _consumer; private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); private readonly ModImportManager _modImportManager; + private readonly MessageService _messageService; private readonly Configuration _config; - private readonly bool _enabled; - public FileWatcher(ModImportManager modImportManager, Configuration config) + public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { - _config = config; _modImportManager = modImportManager; - _enabled = config.EnableDirectoryWatch; + _messageService = messageService; + _config = config; - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _queue = Channel.CreateBounded(new BoundedChannelOptions(256) { @@ -55,13 +56,13 @@ public class FileWatcher : IDisposable, IService private void OnPath(object? sender, FileSystemEventArgs e) { // Cheap de-dupe: only queue once per filename until processed - if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return; + if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) return; _ = _queue.Writer.TryWrite(e.FullPath); } private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; var reader = _queue.Reader; while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { @@ -101,8 +102,27 @@ public class FileWatcher : IDisposable, IService var len = fi.Length; if (len > 0 && len == lastLen) { - _modImportManager.AddUnpack(path); - return; + if (_config.EnableAutomaticModImport) + { + _modImportManager.AddUnpack(path); + return; + } + else + { + var invoked = false; + Action installRequest = args => + { + if (invoked) return; + invoked = true; + _modImportManager.AddUnpack(path); + }; + + _messageService.PrintModFoundInfo( + Path.GetFileNameWithoutExtension(path), + installRequest); + + return; + } } lastLen = len; @@ -116,7 +136,7 @@ public class FileWatcher : IDisposable, IService public void UpdateDirectory(string newPath) { - if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; _fsw.EnableRaisingEvents = false; _fsw.Path = newPath; @@ -125,7 +145,7 @@ public class FileWatcher : IDisposable, IService public void Dispose() { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _fsw.EnableRaisingEvents = false; _cts.Cancel(); _fsw.Dispose(); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 70ccf47b..6c13fc38 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,19 +1,44 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; +using static OtterGui.Classes.MessageService; using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; +public class InstallNotification(string message, Action installRequest) : IMessage +{ + private readonly Action _installRequest = installRequest; + private bool _invoked = false; + + public string Message { get; } = message; + + public NotificationType NotificationType => NotificationType.Info; + + public uint NotificationDuration => 10000; + + public void OnNotificationActions(INotificationDrawArgs args) + { + if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) + { + _installRequest(true); + _invoked = true; + } + } +} + public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { @@ -55,4 +80,11 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", NotificationType.Warning, 10000)); } + + public void PrintModFoundInfo(string fileName, Action installRequest) + { + AddMessage( + new InstallNotification($"A new mod has been found: {fileName}", installRequest) + ); + } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c84214f3..217b6788 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -53,6 +53,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly MessageService _messageService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; @@ -69,7 +70,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MessageService messageService, AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; @@ -96,6 +97,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _messageService = messageService; _attributeHook = attributeHook; _pcpService = pcpService; } @@ -649,9 +651,12 @@ public class SettingsTab : ITab, IUiService DrawDefaultModImportFolder(); DrawPcpFolder(); DrawDefaultModExportPath(); - Checkbox("Enable Automatic Import of Mods from Directory", - "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.", + Checkbox("Enable Directory Watcher", + "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to open a Popup to import these mods.", _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + Checkbox("Enable Fully Automatic Import", + "Uses the File Watcher in order to not just open a Popup, but fully automatically import new mods.", + _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); DrawFileWatcherPath(); } From cbedc878b94ceda8cc91105d5b2456b76bda2fdb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 21:56:16 +0200 Subject: [PATCH 2425/2451] Slight cleanup and autoformat. --- Penumbra/Configuration.cs | 2 +- Penumbra/Penumbra.cs | 2 - Penumbra/Services/FileWatcher.cs | 91 +++++++++++++++++++---------- Penumbra/Services/MessageService.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 8 +-- 5 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e337997b..2991230e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -53,7 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public string WatchDirectory { get; set; } = string.Empty; + public string WatchDirectory { get; set; } = string.Empty; public bool? UseCrashHandler { get; set; } = null; public bool OpenWindowAtStart { get; set; } = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ed2c585..d433a0fb 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -43,7 +43,6 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; - private readonly FileWatcher _fileWatcher; private readonly CollectionManager _collectionManager; private readonly Configuration _config; private readonly CharacterUtility _characterUtility; @@ -81,7 +80,6 @@ public class Penumbra : IDalamudPlugin _residentResources = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _modManager = _services.GetService(); - _fileWatcher = _services.GetService(); _collectionManager = _services.GetService(); _tempCollections = _services.GetService(); _redrawService = _services.GetService(); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index e7172f58..141825f5 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,40 +1,41 @@ using System.Threading.Channels; using OtterGui.Services; -using Penumbra.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; + public class FileWatcher : IDisposable, IService { - private readonly FileSystemWatcher _fsw; - private readonly Channel _queue; - private readonly CancellationTokenSource _cts = new(); - private readonly Task _consumer; - private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); - private readonly ModImportManager _modImportManager; - private readonly MessageService _messageService; - private readonly Configuration _config; + private readonly FileSystemWatcher _fsw; + private readonly Channel _queue; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _consumer; + private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); + private readonly ModImportManager _modImportManager; + private readonly MessageService _messageService; + private readonly Configuration _config; public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { _modImportManager = modImportManager; - _messageService = messageService; - _config = config; + _messageService = messageService; + _config = config; - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; _queue = Channel.CreateBounded(new BoundedChannelOptions(256) { SingleReader = true, SingleWriter = false, - FullMode = BoundedChannelFullMode.DropOldest + FullMode = BoundedChannelFullMode.DropOldest, }); _fsw = new FileSystemWatcher(_config.WatchDirectory) { IncludeSubdirectories = false, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, - InternalBufferSize = 32 * 1024 + NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, + InternalBufferSize = 32 * 1024, }; // Only wake us for the exact patterns we care about @@ -56,13 +57,17 @@ public class FileWatcher : IDisposable, IService private void OnPath(object? sender, FileSystemEventArgs e) { // Cheap de-dupe: only queue once per filename until processed - if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) return; + if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) + return; + _ = _queue.Writer.TryWrite(e.FullPath); } private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; + var reader = _queue.Reader; while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { @@ -72,7 +77,10 @@ public class FileWatcher : IDisposable, IService { await ProcessOneAsync(path, token).ConfigureAwait(false); } - catch (OperationCanceledException) { Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); } + catch (OperationCanceledException) + { + Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); + } catch (Exception ex) { Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}"); @@ -90,15 +98,19 @@ public class FileWatcher : IDisposable, IService // Downloads often finish via rename; file may be locked briefly. // Wait until it exists and is readable; also require two stable size checks. const int maxTries = 40; - long lastLen = -1; + long lastLen = -1; - for (int i = 0; i < maxTries && !token.IsCancellationRequested; i++) + for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++) { - if (!File.Exists(path)) { await Task.Delay(100, token); continue; } + if (!File.Exists(path)) + { + await Task.Delay(100, token); + continue; + } try { - var fi = new FileInfo(path); + var fi = new FileInfo(path); var len = fi.Length; if (len > 0 && len == lastLen) { @@ -112,7 +124,9 @@ public class FileWatcher : IDisposable, IService var invoked = false; Action installRequest = args => { - if (invoked) return; + if (invoked) + return; + invoked = true; _modImportManager.AddUnpack(path); }; @@ -122,13 +136,19 @@ public class FileWatcher : IDisposable, IService installRequest); return; - } + } } lastLen = len; } - catch (IOException) { Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); } - catch (UnauthorizedAccessException) { Penumbra.Log.Debug($"[FileWatcher] File is locked."); } + catch (IOException) + { + Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); + } + catch (UnauthorizedAccessException) + { + Penumbra.Log.Debug($"[FileWatcher] File is locked."); + } await Task.Delay(150, token); } @@ -136,21 +156,32 @@ public class FileWatcher : IDisposable, IService public void UpdateDirectory(string newPath) { - if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) + return; _fsw.EnableRaisingEvents = false; - _fsw.Path = newPath; + _fsw.Path = newPath; _fsw.EnableRaisingEvents = true; } public void Dispose() { - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; + _fsw.EnableRaisingEvents = false; _cts.Cancel(); _fsw.Dispose(); _queue.Writer.TryComplete(); - try { _consumer.Wait(TimeSpan.FromSeconds(5)); } catch { /* swallow */ } + try + { + _consumer.Wait(TimeSpan.FromSeconds(5)); + } + catch + { + /* swallow */ + } + _cts.Dispose(); } } diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 6c13fc38..3dc6a90c 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -20,7 +20,6 @@ namespace Penumbra.Services; public class InstallNotification(string message, Action installRequest) : IMessage { - private readonly Action _installRequest = installRequest; private bool _invoked = false; public string Message { get; } = message; @@ -33,7 +32,7 @@ public class InstallNotification(string message, Action installRequest) : { if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) { - _installRequest(true); + installRequest(true); _invoked = true; } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 217b6788..46f4d38f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -53,7 +53,6 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; - private readonly MessageService _messageService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; @@ -70,7 +69,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MessageService messageService, + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; @@ -97,7 +96,6 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; - _messageService = messageService; _attributeHook = attributeHook; _pcpService = pcpService; } @@ -652,10 +650,10 @@ public class SettingsTab : ITab, IUiService DrawPcpFolder(); DrawDefaultModExportPath(); Checkbox("Enable Directory Watcher", - "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to open a Popup to import these mods.", + "Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods.", _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); Checkbox("Enable Fully Automatic Import", - "Uses the File Watcher in order to not just open a Popup, but fully automatically import new mods.", + "Uses the File Watcher in order to skip the query popup and automatically import any new mods.", _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); DrawFileWatcherPath(); } From 5bf901d0c45f7c0384480387cab03eb626d25899 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Oct 2025 17:30:29 +0200 Subject: [PATCH 2426/2451] Update actorobjectmanager when setting cutscene index. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Interop/PathResolving/CutsceneService.cs | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 9af1e5fc..a63f6735 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 +Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 diff --git a/Penumbra.Api b/Penumbra.Api index dd141317..c23ee05c 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa +Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 diff --git a/Penumbra.GameData b/Penumbra.GameData index 3baace73..283d51f6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3baace73c828271dcb71a8156e3e7b91e1dd12ae +Subproject commit 283d51f6f6c7721a810548d95ba83eef2484e17e diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 6be19c46..97e64f84 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable return false; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; + _objects.InvokeRequiredUpdates(); return true; } From 912c183fc6e05e58920552ff902078f4accbbde0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Oct 2025 23:45:20 +0200 Subject: [PATCH 2427/2451] Improve file watcher. --- Penumbra.GameData | 2 +- Penumbra/Services/FileWatcher.cs | 200 +++++++++++++---------- Penumbra/Services/InstallNotification.cs | 39 +++++ Penumbra/Services/MessageService.cs | 31 ---- Penumbra/UI/Tabs/SettingsTab.cs | 26 +-- 5 files changed, 165 insertions(+), 133 deletions(-) create mode 100644 Penumbra/Services/InstallNotification.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 283d51f6..d889f9ef 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 283d51f6f6c7721a810548d95ba83eef2484e17e +Subproject commit d889f9ef918514a46049725052d378b441915b00 diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 141825f5..1d572f05 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,37 +1,69 @@ -using System.Threading.Channels; -using OtterGui.Services; +using OtterGui.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; public class FileWatcher : IDisposable, IService { - private readonly FileSystemWatcher _fsw; - private readonly Channel _queue; - private readonly CancellationTokenSource _cts = new(); - private readonly Task _consumer; + // TODO: use ConcurrentSet when it supports comparers in Luna. private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); private readonly ModImportManager _modImportManager; private readonly MessageService _messageService; private readonly Configuration _config; + private bool _pausedConsumer; + private FileSystemWatcher? _fsw; + private CancellationTokenSource? _cts = new(); + private Task? _consumer; + public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { _modImportManager = modImportManager; _messageService = messageService; _config = config; - if (!_config.EnableDirectoryWatch) + if (_config.EnableDirectoryWatch) + { + SetupFileWatcher(_config.WatchDirectory); + SetupConsumerTask(); + } + } + + public void Toggle(bool value) + { + if (_config.EnableDirectoryWatch == value) return; - _queue = Channel.CreateBounded(new BoundedChannelOptions(256) + _config.EnableDirectoryWatch = value; + _config.Save(); + if (value) { - SingleReader = true, - SingleWriter = false, - FullMode = BoundedChannelFullMode.DropOldest, - }); + SetupFileWatcher(_config.WatchDirectory); + SetupConsumerTask(); + } + else + { + EndFileWatcher(); + EndConsumerTask(); + } + } - _fsw = new FileSystemWatcher(_config.WatchDirectory) + internal void PauseConsumer(bool pause) + => _pausedConsumer = pause; + + private void EndFileWatcher() + { + if (_fsw is null) + return; + + _fsw.Dispose(); + _fsw = null; + } + + private void SetupFileWatcher(string directory) + { + EndFileWatcher(); + _fsw = new FileSystemWatcher { IncludeSubdirectories = false, NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, @@ -46,49 +78,81 @@ public class FileWatcher : IDisposable, IService _fsw.Created += OnPath; _fsw.Renamed += OnPath; + UpdateDirectory(directory); + } + + private void EndConsumerTask() + { + if (_cts is not null) + { + _cts.Cancel(); + _cts = null; + } + _consumer = null; + } + + private void SetupConsumerTask() + { + EndConsumerTask(); + _cts = new CancellationTokenSource(); _consumer = Task.Factory.StartNew( () => ConsumerLoopAsync(_cts.Token), _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + } - _fsw.EnableRaisingEvents = true; + public void UpdateDirectory(string newPath) + { + if (_config.WatchDirectory != newPath) + { + _config.WatchDirectory = newPath; + _config.Save(); + } + + if (_fsw is null) + return; + + _fsw.EnableRaisingEvents = false; + if (!Directory.Exists(newPath) || newPath.Length is 0) + { + _fsw.Path = string.Empty; + } + else + { + _fsw.Path = newPath; + _fsw.EnableRaisingEvents = true; + } } private void OnPath(object? sender, FileSystemEventArgs e) - { - // Cheap de-dupe: only queue once per filename until processed - if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) - return; - - _ = _queue.Writer.TryWrite(e.FullPath); - } + => _pending.TryAdd(e.FullPath, 0); private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_config.EnableDirectoryWatch) - return; - - var reader = _queue.Reader; - while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) + while (true) { - while (reader.TryRead(out var path)) + var (path, _) = _pending.FirstOrDefault(); + if (path is null || _pausedConsumer) { - try - { - await ProcessOneAsync(path, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); - } - catch (Exception ex) - { - Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}"); - } - finally - { - _pending.TryRemove(path, out _); - } + await Task.Delay(500, token).ConfigureAwait(false); + continue; + } + + try + { + await ProcessOneAsync(path, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Penumbra.Log.Debug("[FileWatcher] Canceled via Token."); + } + catch (Exception ex) + { + Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}"); + } + finally + { + _pending.TryRemove(path, out _); } } } @@ -115,28 +179,10 @@ public class FileWatcher : IDisposable, IService if (len > 0 && len == lastLen) { if (_config.EnableAutomaticModImport) - { _modImportManager.AddUnpack(path); - return; - } else - { - var invoked = false; - Action installRequest = args => - { - if (invoked) - return; - - invoked = true; - _modImportManager.AddUnpack(path); - }; - - _messageService.PrintModFoundInfo( - Path.GetFileNameWithoutExtension(path), - installRequest); - - return; - } + _messageService.AddMessage(new InstallNotification(_modImportManager, path), false); + return; } lastLen = len; @@ -154,34 +200,10 @@ public class FileWatcher : IDisposable, IService } } - public void UpdateDirectory(string newPath) - { - if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) - return; - - _fsw.EnableRaisingEvents = false; - _fsw.Path = newPath; - _fsw.EnableRaisingEvents = true; - } public void Dispose() { - if (!_config.EnableDirectoryWatch) - return; - - _fsw.EnableRaisingEvents = false; - _cts.Cancel(); - _fsw.Dispose(); - _queue.Writer.TryComplete(); - try - { - _consumer.Wait(TimeSpan.FromSeconds(5)); - } - catch - { - /* swallow */ - } - - _cts.Dispose(); + EndConsumerTask(); + EndFileWatcher(); } } diff --git a/Penumbra/Services/InstallNotification.cs b/Penumbra/Services/InstallNotification.cs new file mode 100644 index 00000000..e3956076 --- /dev/null +++ b/Penumbra/Services/InstallNotification.cs @@ -0,0 +1,39 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; +using OtterGui.Text; +using Penumbra.Mods.Manager; + +namespace Penumbra.Services; + +public class InstallNotification(ModImportManager modImportManager, string filePath) : OtterGui.Classes.MessageService.IMessage +{ + public string Message + => "A new mod has been found!"; + + public NotificationType NotificationType + => NotificationType.Info; + + public uint NotificationDuration + => uint.MaxValue; + + public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath); + + public string LogMessage + => $"A new mod has been found: {Path.GetFileName(filePath)}"; + + public void OnNotificationActions(INotificationDrawArgs args) + { + var region = ImGui.GetContentRegionAvail(); + var buttonSize = new Vector2((region.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + if (ImUtf8.ButtonEx("Install"u8, ""u8, buttonSize)) + { + modImportManager.AddUnpack(filePath); + args.Notification.DismissNow(); + } + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Ignore"u8, ""u8, buttonSize)) + args.Notification.DismissNow(); + } +} diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 3dc6a90c..70ccf47b 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,43 +1,19 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; -using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; -using static OtterGui.Classes.MessageService; using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; -public class InstallNotification(string message, Action installRequest) : IMessage -{ - private bool _invoked = false; - - public string Message { get; } = message; - - public NotificationType NotificationType => NotificationType.Info; - - public uint NotificationDuration => 10000; - - public void OnNotificationActions(INotificationDrawArgs args) - { - if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) - { - installRequest(true); - _invoked = true; - } - } -} - public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { @@ -79,11 +55,4 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", NotificationType.Warning, 10000)); } - - public void PrintModFoundInfo(string fileName, Action installRequest) - { - AddMessage( - new InstallNotification($"A new mod has been found: {fileName}", installRequest) - ); - } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 46f4d38f..86c01cb2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -66,7 +66,8 @@ public class SettingsTab : ITab, IUiService public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, - CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, + FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, @@ -651,7 +652,7 @@ public class SettingsTab : ITab, IUiService DrawDefaultModExportPath(); Checkbox("Enable Directory Watcher", "Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods.", - _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + _config.EnableDirectoryWatch, _fileWatcher.Toggle); Checkbox("Enable Fully Automatic Import", "Uses the File Watcher in order to skip the query popup and automatically import any new mods.", _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); @@ -735,19 +736,24 @@ public class SettingsTab : ITab, IUiService + "Keep this empty to use the root directory."); } - private string _tempWatchDirectory = string.Empty; + private string? _tempWatchDirectory; + /// Draw input for the Automatic Mod import path. private void DrawFileWatcherPath() { - var tmp = _config.WatchDirectory; - var spacing = new Vector2(UiHelpers.ScaleX3); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + var tmp = _tempWatchDirectory ?? _config.WatchDirectory; + var spacing = new Vector2(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); if (ImGui.InputText("##fileWatchPath", ref tmp, 256)) _tempWatchDirectory = tmp; - if (ImGui.IsItemDeactivatedAfterEdit()) - _fileWatcher.UpdateDirectory(_tempWatchDirectory); + if (ImGui.IsItemDeactivated() && _tempWatchDirectory is not null) + { + if (ImGui.IsItemDeactivatedAfterEdit()) + _fileWatcher.UpdateDirectory(_tempWatchDirectory); + _tempWatchDirectory = null; + } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize, @@ -761,11 +767,7 @@ public class SettingsTab : ITab, IUiService _fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) => { if (b) - { _fileWatcher.UpdateDirectory(s); - _config.WatchDirectory = s; - _config.Save(); - } }, startDir, false); } From c4b6e4e00bd4a52b1b5be5059effccae58c8befb Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 23 Oct 2025 21:50:20 +0000 Subject: [PATCH 2428/2451] [CI] Updating repo.json for testing_1.5.1.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2a31b75e..34405eb6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.6", - "TestingAssemblyVersion": "1.5.1.6", + "TestingAssemblyVersion": "1.5.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ce54aa5d2559abc8552edfe0b270e61c450226c4 Mon Sep 17 00:00:00 2001 From: Karou Date: Sun, 2 Nov 2025 17:58:20 -0500 Subject: [PATCH 2429/2451] Added IPC call to allow for redrawing only members of specified collections --- Penumbra.Api | 2 +- Penumbra/Api/Api/RedrawApi.cs | 29 ++++++++++++++++--- Penumbra/Api/IpcProviders.cs | 1 + .../Api/IpcTester/CollectionsIpcTester.cs | 4 +++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index c23ee05c..874a3773 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 +Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index ec4de892..4cbb9f29 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -2,11 +2,14 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Interop; using Penumbra.Interop.Services; -namespace Penumbra.Api.Api; - -public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService +namespace Penumbra.Api.Api; + +public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService { public void RedrawObject(int gameObjectIndex, RedrawType setting) { @@ -28,9 +31,27 @@ public class RedrawApi(RedrawService redrawService, IFramework framework) : IPen framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting)); } + public void RedrawCollectionMembers(Guid collectionId, RedrawType setting) + { + + if (!collections.Storage.ById(collectionId, out var collection)) + collection = ModCollection.Empty; + framework.RunOnFrameworkThread(() => + { + foreach (var actor in objects.Objects) + { + helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection); + if (collection == modCollection) + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(actor.ObjectIndex, setting)); + } + } + }); + } + public event GameObjectRedrawnDelegate? GameObjectRedrawn { add => redrawService.GameObjectRedrawn += value; remove => redrawService.GameObjectRedrawn -= value; } -} +} diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 0c80626f..5f04540f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -88,6 +88,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), + IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw), IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index c06bdeb4..f033b7c3 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService }).ToArray(); ImGui.OpenPopup("Changed Item List"); } + IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members"); + if (ImGui.Button("Redraw##ObjectCollection")) + new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw); + } private void DrawChangedItemPopup() From 5be021b0eb248eede38e8d205bc75bd95b2305df Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 13 Nov 2025 19:53:50 +0100 Subject: [PATCH 2430/2451] Add integration settings sections --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/UiApi.cs | 26 +++-- Penumbra/Api/IpcProviders.cs | 2 + .../IntegrationSettingsRegistry.cs | 110 ++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- 6 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 Penumbra/UI/Integration/IntegrationSettingsRegistry.cs diff --git a/Penumbra.Api b/Penumbra.Api index 874a3773..b97784bd 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b +Subproject commit b97784bd7cd911bd0a323cd8e717714de1875469 diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 7304c9c7..c4026c72 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 12; + public const int FeatureVersion = 13; public void Dispose() { diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index b14f67ae..6fb116f3 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -5,20 +5,24 @@ using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; +using Penumbra.UI.Integration; +using Penumbra.UI.Tabs; namespace Penumbra.Api.Api; public class UiApi : IPenumbraApiUi, IApiService, IDisposable { - private readonly CommunicatorService _communicator; - private readonly ConfigWindow _configWindow; - private readonly ModManager _modManager; + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _configWindow; + private readonly ModManager _modManager; + private readonly IntegrationSettingsRegistry _integrationSettings; - public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) + public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings) { - _communicator = communicator; - _configWindow = configWindow; - _modManager = modManager; + _communicator = communicator; + _configWindow = configWindow; + _modManager = modManager; + _integrationSettings = integrationSettings; _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); } @@ -98,4 +102,12 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable var (type, id) = data.ToApiObject(); ChangedItemTooltip.Invoke(type, id); } + + public PenumbraApiEc RegisterSettingsSection(Action draw) + => _integrationSettings.RegisterSection(draw); + + public PenumbraApiEc UnregisterSettingsSection(Action draw) + => _integrationSettings.UnregisterSection(draw) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 5f04540f..197cf3d2 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -130,6 +130,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), + IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui), + IpcSubscribers.UnregisterSettingsSection.Provider(pi, api.Ui), ]; if (_characterUtility.Ready) _initializedProvider.Invoke(); diff --git a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs new file mode 100644 index 00000000..ab26a68f --- /dev/null +++ b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs @@ -0,0 +1,110 @@ +using Dalamud.Plugin; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Enums; + +namespace Penumbra.UI.Integration; + +public sealed class IntegrationSettingsRegistry : IService, IDisposable +{ + private readonly IDalamudPluginInterface _pluginInterface; + + private readonly List<(string InternalName, string Name, Action Draw)> _sections = []; + + private bool _disposed = false; + + public IntegrationSettingsRegistry(IDalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + + _pluginInterface.ActivePluginsChanged += OnActivePluginsChanged; + } + + public void Dispose() + { + _disposed = true; + + _pluginInterface.ActivePluginsChanged -= OnActivePluginsChanged; + + _sections.Clear(); + } + + public void Draw() + { + foreach (var (internalName, name, draw) in _sections) + { + if (!ImUtf8.CollapsingHeader($"Integration with {name}###IntegrationSettingsHeader.{internalName}")) + continue; + + using var id = ImUtf8.PushId($"IntegrationSettings.{internalName}"); + try + { + draw(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while drawing {internalName} integration settings: {e}"); + } + } + } + + public PenumbraApiEc RegisterSection(Action draw) + { + if (_disposed) + return PenumbraApiEc.SystemDisposed; + + var plugin = GetPlugin(draw); + if (plugin is null) + return PenumbraApiEc.InvalidArgument; + + var section = (plugin.InternalName, plugin.Name, draw); + + var index = FindSectionIndex(plugin.InternalName); + if (index >= 0) + { + if (_sections[index] == section) + return PenumbraApiEc.NothingChanged; + _sections[index] = section; + } + else + _sections.Add(section); + _sections.Sort((lhs, rhs) => string.Compare(lhs.Name, rhs.Name, StringComparison.CurrentCultureIgnoreCase)); + + return PenumbraApiEc.Success; + } + + public bool UnregisterSection(Action draw) + { + var index = FindSectionIndex(draw); + if (index < 0) + return false; + + _sections.RemoveAt(index); + return true; + } + + private void OnActivePluginsChanged(IActivePluginsChangedEventArgs args) + { + if (args.Kind is PluginListInvalidationKind.Loaded) + return; + + foreach (var internalName in args.AffectedInternalNames) + { + var index = FindSectionIndex(internalName); + if (index >= 0 && GetPlugin(_sections[index].Draw) is null) + { + _sections.RemoveAt(index); + Penumbra.Log.Warning($"Removed stale integration setting section of {internalName} (reason: {args.Kind})"); + } + } + } + + private IExposedPlugin? GetPlugin(Delegate @delegate) + => null; // TODO Use IDalamudPluginInterface.GetPlugin(Assembly) when it's in Dalamud stable. + + private int FindSectionIndex(string internalName) + => _sections.FindIndex(section => section.InternalName.Equals(internalName, StringComparison.Ordinal)); + + private int FindSectionIndex(Action draw) + => _sections.FindIndex(section => section.Draw == draw); +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 86c01cb2..09c7c58d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -20,6 +20,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.Integration; using Penumbra.UI.ModsTab; namespace Penumbra.UI.Tabs; @@ -55,6 +56,7 @@ public class SettingsTab : ITab, IUiService private readonly CleanupService _cleanupService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; + private readonly IntegrationSettingsRegistry _integrationSettings; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -71,7 +73,7 @@ public class SettingsTab : ITab, IUiService DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, - AttributeHook attributeHook, PcpService pcpService) + AttributeHook attributeHook, PcpService pcpService, IntegrationSettingsRegistry integrationSettings) { _pluginInterface = pluginInterface; _config = config; @@ -99,6 +101,7 @@ public class SettingsTab : ITab, IUiService _cleanupService = cleanupService; _attributeHook = attributeHook; _pcpService = pcpService; + _integrationSettings = integrationSettings; } public void DrawHeader() @@ -129,6 +132,7 @@ public class SettingsTab : ITab, IUiService DrawColorSettings(); DrawPredefinedTagsSection(); DrawAdvancedSettings(); + _integrationSettings.Draw(); DrawSupportButtons(); } @@ -1133,7 +1137,7 @@ public class SettingsTab : ITab, IUiService } #endregion - + /// Draw the support button group on the right-hand side of the window. private void DrawSupportButtons() { From e240a42a2ccd35d06be417033d034448c5c8be35 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 13 Nov 2025 19:55:32 +0100 Subject: [PATCH 2431/2451] Replace GetPlugin(Delegate) stub by actual implementation --- Penumbra/UI/Integration/IntegrationSettingsRegistry.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs index ab26a68f..2d3da488 100644 --- a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs +++ b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs @@ -100,7 +100,12 @@ public sealed class IntegrationSettingsRegistry : IService, IDisposable } private IExposedPlugin? GetPlugin(Delegate @delegate) - => null; // TODO Use IDalamudPluginInterface.GetPlugin(Assembly) when it's in Dalamud stable. + => @delegate.Method.DeclaringType + switch + { + null => null, + var type => _pluginInterface.GetPlugin(type.Assembly), + }; private int FindSectionIndex(string internalName) => _sections.FindIndex(section => section.InternalName.Equals(internalName, StringComparison.Ordinal)); From 338e3bc1a5107267aace6ae9b1a89e30a7e7a757 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 20 Nov 2025 18:41:32 +0100 Subject: [PATCH 2432/2451] Update Penumbra.Api --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index b97784bd..704d62f6 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b97784bd7cd911bd0a323cd8e717714de1875469 +Subproject commit 704d62f64f791b8cfd42363beaa464ad6f98ae48 From 5dd74297c623430ea63ad7a01531ba9b58e75eb7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Nov 2025 22:10:17 +0000 Subject: [PATCH 2433/2451] [CI] Updating repo.json for 1.5.1.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 34405eb6..7ddffd7c 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.6", - "TestingAssemblyVersion": "1.5.1.7", + "AssemblyVersion": "1.5.1.8", + "TestingAssemblyVersion": "1.5.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ccb5b01290c717b0581ce5c782e9c7554ff27357 Mon Sep 17 00:00:00 2001 From: Karou Date: Sat, 29 Nov 2025 12:14:10 -0500 Subject: [PATCH 2434/2451] Api version bump and remove redundant framework thread call --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/RedrawApi.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 874a3773..3d6cee1a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b +Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 7304c9c7..c4026c72 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 12; + public const int FeatureVersion = 13; public void Dispose() { diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 4cbb9f29..08f1f9df 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -43,7 +43,7 @@ public class RedrawApi(RedrawService redrawService, IFramework framework, Collec helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection); if (collection == modCollection) { - framework.RunOnFrameworkThread(() => redrawService.RedrawObject(actor.ObjectIndex, setting)); + redrawService.RedrawObject(actor.ObjectIndex, setting); } } }); From 3e7511cb348d936736621b9152ae54772e58ef21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Dec 2025 18:33:10 +0100 Subject: [PATCH 2435/2451] Update SDK. --- OtterGui | 2 +- Penumbra.Api | 2 +- .../Penumbra.CrashHandler.csproj | 2 +- Penumbra.CrashHandler/packages.lock.json | 8 ++--- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 3 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/packages.lock.json | 36 ++++--------------- 13 files changed, 23 insertions(+), 45 deletions(-) diff --git a/OtterGui b/OtterGui index a63f6735..6f323645 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 +Subproject commit 6f3236453b1edfaa25c8edcc8b39a9d9b2fc18ac diff --git a/Penumbra.Api b/Penumbra.Api index 3d6cee1a..e4934ccc 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf +Subproject commit e4934ccca0379f22dadf989ab2d34f30b3c5c7ea diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 1b1f0a28..4c864d39 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.CrashHandler/packages.lock.json b/Penumbra.CrashHandler/packages.lock.json index 1d395083..0a160ea5 100644 --- a/Penumbra.CrashHandler/packages.lock.json +++ b/Penumbra.CrashHandler/packages.lock.json @@ -1,12 +1,12 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { "DotNet.ReproducibleBuilds": { "type": "Direct", - "requested": "[1.2.25, )", - "resolved": "1.2.25", - "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" } } } diff --git a/Penumbra.GameData b/Penumbra.GameData index d889f9ef..2ff50e68 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d889f9ef918514a46049725052d378b441915b00 +Subproject commit 2ff50e68f7c951f0f8b25957a400a2e32ed9d6dc diff --git a/Penumbra.String b/Penumbra.String index c8611a0c..0315144a 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793 +Subproject commit 0315144ab5614c11911e2a4dddf436fb18c5d7e3 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index fa45ffbf..f04928a5 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index e41ceade..7a8ca032 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.Services; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -17,7 +18,6 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; -using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index f2a041eb..b458fc16 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,6 +1,6 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 05f77e29..c7f0635d 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Interface.Colors; using Microsoft.Extensions.DependencyInjection; using OtterGui; @@ -1033,7 +1034,7 @@ public class DebugTab : Window, ITab, IUiService /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { - var player = _clientState.LocalPlayer; + var player = _objects.Objects.LocalPlayer; var name = player?.Name.ToString() ?? "NULL"; if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 79dcbb9e..1d24c597 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -27,7 +27,6 @@ public class ModsTab( TutorialService tutorial, RedrawService redrawService, Configuration config, - IClientState clientState, CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) @@ -113,7 +112,7 @@ public class ModsTab( ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); using var id = ImRaii.PushId("Redraw"); - using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null); + using var disabled = ImRaii.Disabled(objects.Objects.LocalPlayer is null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; var tt = !objects[0].Valid diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 593adde1..2223075d 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,5 +1,5 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 7499bffa..c904870a 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,12 +1,12 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { "DotNet.ReproducibleBuilds": { "type": "Direct", - "requested": "[1.2.25, )", - "resolved": "1.2.25", - "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" }, "EmbedIO": { "type": "Direct", @@ -33,7 +33,6 @@ "resolved": "0.40.0", "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", "dependencies": { - "System.Buffers": "4.6.0", "ZstdSharp.Port": "0.8.5" } }, @@ -66,10 +65,7 @@ "FlatSharp.Runtime": { "type": "Transitive", "resolved": "7.9.0", - "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", - "dependencies": { - "System.Memory": "4.5.5" - } + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==" }, "JetBrains.Annotations": { "type": "Transitive", @@ -102,33 +98,15 @@ "SharpGLTF.Core": "1.0.5" } }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" - }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" - }, "Unosquare.Swan.Lite": { "type": "Transitive", "resolved": "3.1.0", - "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==", - "dependencies": { - "System.ValueTuple": "4.5.0" - } + "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==" }, "ZstdSharp.Port": { "type": "Transitive", @@ -154,7 +132,7 @@ "FlatSharp.Compiler": "[7.9.0, )", "FlatSharp.Runtime": "[7.9.0, )", "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.10.0, )", + "Penumbra.Api": "[5.13.0, )", "Penumbra.String": "[1.0.6, )" } }, From 7717251c6a1184075b2a685def2a44c33a9bfdff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 18 Dec 2025 20:45:15 +0100 Subject: [PATCH 2436/2451] Update to TerraFX. --- Penumbra.GameData | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 21 ++++ Penumbra/Api/Api/ResolveApi.cs | 34 +++++ Penumbra/Api/Api/UiApi.cs | 6 + Penumbra/Api/IpcProviders.cs | 3 + Penumbra/Import/Textures/TextureManager.cs | 85 ++++++++++--- .../Interop/Services/TextureArraySlicer.cs | 116 +++++++++++------- Penumbra/Penumbra.csproj | 12 +- Penumbra/Services/StaticServiceManager.cs | 1 + 9 files changed, 211 insertions(+), 69 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2ff50e68..3d4d8510 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2ff50e68f7c951f0f8b25957a400a2e32ed9d6dc +Subproject commit 3d4d8510f832dfd95d7069b86e6b3da4ec612558 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 3ba17cf4..d49c2904 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -73,6 +73,27 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4)); } + public PenumbraApiEc GetSettingsInAllCollections(string modDirectory, string modName, + out Dictionary>, bool, bool)> settings, + bool ignoreTemporaryCollections = false) + { + settings = []; + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return PenumbraApiEc.ModMissing; + + var collections = ignoreTemporaryCollections + ? _collectionManager.Storage.Where(c => c != ModCollection.Empty) + : _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values); + settings = []; + foreach (var collection in collections) + { + if (GetCurrentSettings(collection, mod, false, false, 0) is { } s) + settings.Add(collection.Identity.Id, s); + } + + return PenumbraApiEc.Success; + } + public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key) { diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs index 481ea7ad..00a0c86f 100644 --- a/Penumbra/Api/Api/ResolveApi.cs +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Services; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -41,6 +42,19 @@ public class ResolveApi( return ret.Select(r => r.ToString()).ToArray(); } + public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath) + { + resolvedPath = gamePath; + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return PenumbraApiEc.CollectionMissing; + + if (!collection.HasCache) + return PenumbraApiEc.CollectionInactive; + + resolvedPath = ResolvePath(gamePath, modManager, collection); + return PenumbraApiEc.Success; + } + public string[] ReverseResolvePlayerPath(string moddedPath) { if (!config.EnableMods) @@ -64,6 +78,26 @@ public class ResolveApi( return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } + public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward, + out string[][] resolvedReverse) + { + resolvedForward = forward; + resolvedReverse = []; + if (!config.EnableMods) + return PenumbraApiEc.Success; + + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return PenumbraApiEc.CollectionMissing; + + if (!collection.HasCache) + return PenumbraApiEc.CollectionInactive; + + resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray(); + var reverseResolved = collection.ReverseResolvePaths(reverse); + resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); + return PenumbraApiEc.Success; + } + public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) { if (!config.EnableMods) diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index b14f67ae..70f018bb 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -81,6 +81,12 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; + public PenumbraApiEc RegisterSettingsSection(Action draw) + => throw new NotImplementedException(); + + public PenumbraApiEc UnregisterSettingsSection(Action draw) + => throw new NotImplementedException(); + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 5f04540f..fdacc73b 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -66,6 +66,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings), IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetSettingsInAllCollections.Provider(pi, api.ModSettings), IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), @@ -98,6 +99,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve), IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve), IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve), IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree), IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree), diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 073fef2f..177722ec 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -7,12 +7,12 @@ using OtterGui.Log; using OtterGui.Services; using OtterGui.Tasks; using OtterTex; -using SharpDX.Direct3D11; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; -using DxgiDevice = SharpDX.DXGI.Device; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; @@ -125,11 +125,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur switch (_type) { case TextureType.Png: - data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) + data?.SaveAsync(_outputPath, new PngEncoder { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) .Wait(cancel); return; case TextureType.Targa: - data?.SaveAsync(_outputPath, new TgaEncoder() + data?.SaveAsync(_outputPath, new TgaEncoder { Compression = TgaCompression.None, BitsPerPixel = TgaBitsPerPixel.Pixel32, @@ -204,11 +204,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, + width, height), _ => throw new Exception("Wrong save type."), }; @@ -390,7 +395,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) + public unsafe ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) { if (input.Meta.Format == format) return input; @@ -406,11 +411,58 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { - var device = new Device(uiBuilder.DeviceHandle); - var dxgiDevice = device.QueryInterface(); + ref var device = ref *(ID3D11Device*)uiBuilder.DeviceHandle; + IDXGIDevice* dxgiDevice; + Marshal.ThrowExceptionForHR(device.QueryInterface(TerraFX.Interop.Windows.Windows.__uuidof(), (void**)&dxgiDevice)); - using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); - return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); + try + { + IDXGIAdapter* adapter = null; + Marshal.ThrowExceptionForHR(dxgiDevice->GetAdapter(&adapter)); + try + { + dxgiDevice->Release(); + dxgiDevice = null; + + ID3D11Device* deviceClone = null; + ID3D11DeviceContext* contextClone = null; + var featureLevel = device.GetFeatureLevel(); + Marshal.ThrowExceptionForHR(DirectX.D3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, + HMODULE.NULL, + device.GetCreationFlags(), + &featureLevel, + 1, + D3D11.D3D11_SDK_VERSION, + &deviceClone, + null, + &contextClone)); + try + { + adapter->Release(); + adapter = null; + return input.Compress((nint)deviceClone, format, CompressFlags.Parallel); + } + finally + { + if (contextClone is not null) + contextClone->Release(); + if (deviceClone is not null) + deviceClone->Release(); + } + } + finally + { + if (adapter is not null) + adapter->Release(); + } + } + finally + { + if (dxgiDevice is not null) + dxgiDevice->Release(); + } } return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); @@ -456,7 +508,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur GC.KeepAlive(input); } - private readonly struct ImageInputData + private readonly struct ImageInputData : IEquatable { private readonly string? _inputPath; @@ -524,5 +576,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public override int GetHashCode() => _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height); + + public override bool Equals(object? obj) + => obj is ImageInputData o && Equals(o); } } diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 11498878..7b873f26 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -1,8 +1,7 @@ using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using OtterGui.Services; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; +using TerraFX.Interop.DirectX; namespace Penumbra.Interop.Services; @@ -22,46 +21,78 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (texture == null) throw new ArgumentNullException(nameof(texture)); if (sliceIndex >= texture->ArraySize) - throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + throw new ArgumentOutOfRangeException(nameof(sliceIndex), + $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) { state.Refresh(); return new ImTextureID((nint)state.ShaderResourceView); } - var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; - var description = srv.Description; - switch (description.Dimension) + + ref var srv = ref *(ID3D11ShaderResourceView*)(nint)texture->D3D11ShaderResourceView; + srv.AddRef(); + try { - case ShaderResourceViewDimension.Texture1D: - case ShaderResourceViewDimension.Texture2D: - case ShaderResourceViewDimension.Texture2DMultisampled: - case ShaderResourceViewDimension.Texture3D: - case ShaderResourceViewDimension.TextureCube: - // This function treats these as single-slice arrays. - // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. - break; - case ShaderResourceViewDimension.Texture1DArray: - description.Texture1DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.Texture2DArray: - description.Texture2DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.Texture2DMultisampledArray: - description.Texture2DMSArray.FirstArraySlice = sliceIndex; - description.Texture2DMSArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.TextureCubeArray: - description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; - description.TextureCubeArray.CubeCount = 1; - break; - default: - throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); + D3D11_SHADER_RESOURCE_VIEW_DESC description; + srv.GetDesc(&description); + switch (description.ViewDimension) + { + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMS: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE3D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBE: + // This function treats these as single-slice arrays. + // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1DARRAY: + description.Texture1DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DARRAY: + description.Texture2DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMSARRAY: + description.Texture2DMSArray.FirstArraySlice = sliceIndex; + description.Texture2DMSArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBEARRAY: + description.TextureCubeArray.First2DArrayFace = sliceIndex * 6u; + description.TextureCubeArray.NumCubes = 1; + break; + default: + throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.ViewDimension}"); + } + + ID3D11Device* device = null; + srv.GetDevice(&device); + ID3D11Resource* resource = null; + srv.GetResource(&resource); + try + { + ID3D11ShaderResourceView* slicedSrv = null; + Marshal.ThrowExceptionForHR(device->CreateShaderResourceView(resource, &description, &slicedSrv)); + resource->Release(); + device->Release(); + + state = new SliceState(slicedSrv); + _activeSlices.Add(((nint)texture, sliceIndex), state); + return new ImTextureID((nint)state.ShaderResourceView); + } + finally + { + if (resource is not null) + resource->Release(); + if (device is not null) + device->Release(); + } + } + finally + { + srv.Release(); } - state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); - _activeSlices.Add(((nint)texture, sliceIndex), state); - return new ImTextureID((nint)state.ShaderResourceView); } public void Tick() @@ -73,10 +104,9 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (!slice.Tick()) _expiredKeys.Add(key); } + foreach (var key in _expiredKeys) - { _activeSlices.Remove(key); - } } finally { @@ -87,14 +117,12 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable public void Dispose() { foreach (var slice in _activeSlices.Values) - { slice.Dispose(); - } } - private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable + private sealed class SliceState(ID3D11ShaderResourceView* shaderResourceView) : IDisposable { - public readonly ShaderResourceView ShaderResourceView = shaderResourceView; + public readonly ID3D11ShaderResourceView* ShaderResourceView = shaderResourceView; private uint _timeToLive = InitialTimeToLive; @@ -108,13 +136,15 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (unchecked(_timeToLive--) > 0) return true; - ShaderResourceView.Dispose(); + if (ShaderResourceView is not null) + ShaderResourceView->Release(); return false; } public void Dispose() { - ShaderResourceView.Dispose(); + if (ShaderResourceView is not null) + ShaderResourceView->Release(); } } } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f04928a5..43f853f3 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -38,16 +38,8 @@ $(DalamudLibPath)Iced.dll False - - $(DalamudLibPath)SharpDX.dll - False - - - $(DalamudLibPath)SharpDX.Direct3D11.dll - False - - - $(DalamudLibPath)SharpDX.DXGI.dll + + $(DalamudLibPath)TerraFX.Interop.Windows.dll False diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 27582395..be482d1d 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -48,6 +48,7 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) + .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) From 4c8ff408211eda482cd3978c5ec502c19e6d48b6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 18 Dec 2025 20:47:49 +0100 Subject: [PATCH 2437/2451] Fix private Unks. --- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 9 +++++---- Penumbra/Util/PointerExtensions.cs | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Util/PointerExtensions.cs diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index f0ab1125..bc5f0765 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -9,6 +9,7 @@ using OtterGui.Text; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.UI.Tabs.Debug; @@ -178,10 +179,10 @@ public unsafe class GlobalVariablesDrawer( if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMapU8.Span) >= 0) { ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn($"{resource->Name.GetField(16)}"); // Unk1 ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImUtf8.DrawTableColumn($"{PointerExtensions.GetField(resource, 120)}"); // key, Unk1 ImGui.TableNextColumn(); Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); @@ -227,10 +228,10 @@ public unsafe class GlobalVariablesDrawer( if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) { ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn($"{resource->Name.GetField(16)}"); // Unk1 ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImUtf8.DrawTableColumn($"{PointerExtensions.GetField(resource, 120)}"); // key, Unk1 ImGui.TableNextColumn(); Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); diff --git a/Penumbra/Util/PointerExtensions.cs b/Penumbra/Util/PointerExtensions.cs new file mode 100644 index 00000000..c70e2177 --- /dev/null +++ b/Penumbra/Util/PointerExtensions.cs @@ -0,0 +1,20 @@ +namespace Penumbra.Util; + +public static class PointerExtensions +{ + public static unsafe ref TField GetField(this ref TPointer reference, int offset) + where TPointer : unmanaged + where TField : unmanaged + { + var pointer = (byte*)Unsafe.AsPointer(ref reference) + offset; + return ref *(TField*)pointer; + } + + public static unsafe ref TField GetField(TPointer* itemPointer, int offset) + where TPointer : unmanaged + where TField : unmanaged + { + var pointer = (byte*)itemPointer + offset; + return ref *(TField*)pointer; + } +} From febced07080a84e7526e56b1944128ecd6dc8d9a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:47:19 +0100 Subject: [PATCH 2438/2451] Fix bug in slicer. --- Penumbra/Interop/Services/TextureArraySlicer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 7b873f26..3cd57a33 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -48,7 +48,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable break; case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1DARRAY: description.Texture1DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; + description.Texture1DArray.ArraySize = 1; break; case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DARRAY: description.Texture2DArray.FirstArraySlice = sliceIndex; From ebcbc5d98a896c96756588c916aa52294a91773d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:51:39 +0100 Subject: [PATCH 2439/2451] Update SDK. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/Penumbra.json | 2 +- repo.json | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index 6f323645..ff1e6543 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6f3236453b1edfaa25c8edcc8b39a9d9b2fc18ac +Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf diff --git a/Penumbra.Api b/Penumbra.Api index e4934ccc..1750c41b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit e4934ccca0379f22dadf989ab2d34f30b3c5c7ea +Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41 diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 4c864d39..e07bb745 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.GameData b/Penumbra.GameData index 3d4d8510..0e973ed6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3d4d8510f832dfd95d7069b86e6b3da4ec612558 +Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60 diff --git a/Penumbra.String b/Penumbra.String index 0315144a..9bd016fb 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0315144ab5614c11911e2a4dddf436fb18c5d7e3 +Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 43f853f3..f9e33219 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 32032282..975c5bb3 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 13, + "DalamudApiLevel": 14, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/repo.json b/repo.json index 7ddffd7c..5b780560 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.5.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, + "DalamudApiLevel": 14, + "TestingDalamudApiLevel": 14, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From fb299d71f0921c6efad318d0b648bad1c0076e61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:54:09 +0100 Subject: [PATCH 2440/2451] Remove unimplemented ipc. --- Penumbra/Api/Api/UiApi.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index 6a293678..6fb116f3 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -85,12 +85,6 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - public PenumbraApiEc RegisterSettingsSection(Action draw) - => throw new NotImplementedException(); - - public PenumbraApiEc UnregisterSettingsSection(Action draw) - => throw new NotImplementedException(); - private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) From 37f30443767b9367970e16b1aba03d9608885592 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:56:50 +0100 Subject: [PATCH 2441/2451] Update dotnet. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7901a653..85ea0953 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 377919b2..e4a17130 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 2bece720..8af4a8c8 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 59fec5db822240213457b8e0b92566a9fad0ab89 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:05:50 +0100 Subject: [PATCH 2442/2451] Needs both versions for now due to flatsharp? --- .github/workflows/build.yml | 4 +++- .github/workflows/release.yml | 4 +++- .github/workflows/test_release.yml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85ea0953..26b1219d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a17130..a4442c14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 8af4a8c8..914eb136 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 953f243caf7f03a58e67ae75dae3962e89cd55f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:08:18 +0100 Subject: [PATCH 2443/2451] . --- .github/workflows/build.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- .github/workflows/test_release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26b1219d..1a61439e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,15 +10,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4442c14..c72b4800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,15 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 914eb136..90a8b176 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,15 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud From deb3686df5cf1f30365d70b3a2f382863136e953 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 19 Dec 2025 00:12:44 +0000 Subject: [PATCH 2444/2451] [CI] Updating repo.json for 1.5.1.9 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5b780560..611de678 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.8", - "TestingAssemblyVersion": "1.5.1.8", + "AssemblyVersion": "1.5.1.9", + "TestingAssemblyVersion": "1.5.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9cf7030f87142f6ae446b64601f56f9e849e67ca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:13:02 +0100 Subject: [PATCH 2445/2451] ... --- .github/workflows/test_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 90a8b176..c6b4e459 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,7 +9,7 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET From eff3784a85c6fb498ba7f9655d6b9d26b524953e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 15:34:08 +0100 Subject: [PATCH 2446/2451] Fix multi-release bug in texturearrayslicer. --- Penumbra/Interop/Services/TextureArraySlicer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 3cd57a33..a3db4d04 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -18,7 +18,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable /// Caching this across frames will cause a crash to desktop. public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex) { - if (texture == null) + if (texture is null) throw new ArgumentNullException(nameof(texture)); if (sliceIndex >= texture->ArraySize) throw new ArgumentOutOfRangeException(nameof(sliceIndex), @@ -74,9 +74,6 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable { ID3D11ShaderResourceView* slicedSrv = null; Marshal.ThrowExceptionForHR(device->CreateShaderResourceView(resource, &description, &slicedSrv)); - resource->Release(); - device->Release(); - state = new SliceState(slicedSrv); _activeSlices.Add(((nint)texture, sliceIndex), state); return new ImTextureID((nint)state.ShaderResourceView); From 9aa566f521d0375959ceeffee2e9fcbce660c999 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 15:34:50 +0100 Subject: [PATCH 2447/2451] Fix typo in new IPC providers. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 1750c41b..52a3216a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41 +Subproject commit 52a3216a525592205198303df2844435e382cf87 From 069323cfb8220b893df878b5067ee9d87656ab9a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Dec 2025 14:38:27 +0000 Subject: [PATCH 2448/2451] [CI] Updating repo.json for 1.5.1.11 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 611de678..f337e8ff 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.9", - "TestingAssemblyVersion": "1.5.1.9", + "AssemblyVersion": "1.5.1.11", + "TestingAssemblyVersion": "1.5.1.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 73f02851a64e2a0ea9447173a7981d098a38bac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 21:51:34 +0100 Subject: [PATCH 2449/2451] Cherry pick API support for other block compression types from Luna branch. --- Penumbra/Api/Api/EditingApi.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs index e50b7a1b..5a1fc347 100644 --- a/Penumbra/Api/Api/EditingApi.cs +++ b/Penumbra/Api/Api/EditingApi.cs @@ -19,6 +19,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, inputFile, outputFile), + TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, inputFile, outputFile), + TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, inputFile, outputFile), + TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, inputFile, outputFile), + TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, inputFile, outputFile), + TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, inputFile, outputFile), _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), }; @@ -36,6 +42,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), }; // @formatter:on From 6ba735eefba180538190842973604e8b0a592d0d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Dec 2025 20:53:36 +0000 Subject: [PATCH 2450/2451] [CI] Updating repo.json for 1.5.1.12 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f337e8ff..583e5e52 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.11", - "TestingAssemblyVersion": "1.5.1.11", + "AssemblyVersion": "1.5.1.12", + "TestingAssemblyVersion": "1.5.1.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 13500264b7f046cacbddae41df704089be7e7908 Mon Sep 17 00:00:00 2001 From: Marc-Aurel Zent Date: Mon, 22 Dec 2025 14:41:32 +0100 Subject: [PATCH 2451/2451] Use iced to create AsmHooks in PapRewriter. --- .../Hooks/ResourceLoading/PapRewriter.cs | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index caf43d08..ff794d81 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,6 +1,7 @@ using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; +using static Iced.Intel.AssemblerRegisters; using OtterGui.Extensions; using Penumbra.String.Classes; using Swan; @@ -46,36 +47,32 @@ public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResource stackAccesses.RemoveAll(instr => instr.IP == hp.IP); var detourPointer = Marshal.GetFunctionPointerForDelegate(papResourceHandler); - var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var targetRegister = GetRegister64(hookPoint.Op0Register); var hookAddress = new IntPtr((long)detourPoint.IP); var caveAllocation = NativeAllocCave(16); - var hook = new AsmHook( - hookAddress, - [ - "use64", - $"mov {targetRegister}, 0x{stringAllocation:x8}", // Move our char *path into the relevant register (rdx) + var assembler = new Assembler(64); + assembler.mov(targetRegister, stringAllocation); // Move our char *path into the relevant register (rdx) - // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves - // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call - // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh - $"mov r9, 0x{caveAllocation:x8}", - "mov [r9], rcx", - "mov [r9+0x8], rdx", + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + assembler.mov(r9, caveAllocation); + assembler.mov(__qword_ptr[r9], rcx); + assembler.mov(__qword_ptr[r9 + 8], rdx); - // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway - $"mov rax, 0x{detourPointer:x8}", // Get a pointer to our detour in place - "call rax", // Call detour + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + assembler.mov(rax, detourPointer); + assembler.call(rax); - // Do the reverse process and retrieve the stored stuff - $"mov r9, 0x{caveAllocation:x8}", - "mov rcx, [r9]", - "mov rdx, [r9+0x8]", + // Do the reverse process and retrieve the stored stuff + assembler.mov(r9, caveAllocation); + assembler.mov(rcx, __qword_ptr[r9]); + assembler.mov(rdx, __qword_ptr[r9 + 8]); - // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call - "mov r8, rax", - ], $"{name}.PapRedirection" - ); + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + assembler.mov(r8, rax); + var hook = new AsmHook(hookAddress, AssembleToBytes(assembler), $"{name}.PapRedirection"); _hooks.Add(hookAddress, hook); hook.Enable(); @@ -95,19 +92,45 @@ public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResource if (_hooks.ContainsKey(hookAddress)) continue; - var targetRegister = stackAccess.Op0Register.ToString().ToLower(); - var hook = new AsmHook( - hookAddress, - [ - "use64", - $"mov {targetRegister}, 0x{stringAllocation:x8}", - ], $"{name}.PapStackAccess[{index}]" - ); + var targetRegister = GetRegister64(stackAccess.Op0Register); + var assembler = new Assembler(64); + assembler.mov(targetRegister, stringAllocation); + var hook = new AsmHook(hookAddress, AssembleToBytes(assembler), $"{name}.PapStackAccess[{index}]"); _hooks.Add(hookAddress, hook); hook.Enable(); } } + + private static AssemblerRegister64 GetRegister64(Register reg) + => reg switch + { + Register.RAX => rax, + Register.RCX => rcx, + Register.RDX => rdx, + Register.RBX => rbx, + Register.RSP => rsp, + Register.RBP => rbp, + Register.RSI => rsi, + Register.RDI => rdi, + Register.R8 => r8, + Register.R9 => r9, + Register.R10 => r10, + Register.R11 => r11, + Register.R12 => r12, + Register.R13 => r13, + Register.R14 => r14, + Register.R15 => r15, + _ => throw new ArgumentOutOfRangeException(nameof(reg), reg, "Unsupported register."), + }; + + private static byte[] AssembleToBytes(Assembler assembler) + { + using var stream = new MemoryStream(); + var writer = new StreamCodeWriter(stream); + assembler.Assemble(writer, 0); + return stream.ToArray(); + } private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) {

ZnD(H&S#^ml9#4Qu`{%TDAJd1vWt~_T< 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 M!0-(GYi8iz0U`>_od5s; diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 86bddb838d6e0a11f5a3956f2a3bd4433685bb69..f0d0db79ff8176482907933d17517976a547ee5d 100644 GIT binary patch literal 31744 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 DPwzYa From 83d1ced8f5342a9032a4b998fe8476f92617eaff Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 31 Aug 2022 15:12:55 +0000 Subject: [PATCH 0444/2451] [CI] Updating repo.json for refs/tags/0.5.6.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 11558334..c32d148e 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.6.2", - "TestingAssemblyVersion": "0.5.6.2", + "AssemblyVersion": "0.5.6.3", + "TestingAssemblyVersion": "0.5.6.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 8aec63d0be4ed9cff41ac241cb99163c8b180fa3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Sep 2022 17:05:20 +0200 Subject: [PATCH 0445/2451] tmp --- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 2762179c..9b07b8a1 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -22,7 +22,7 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.UI.Classes; -public struct Texture : IDisposable +public class Texture : IDisposable { // Path to the file we tried to load. public string Path = string.Empty; @@ -44,6 +44,28 @@ public struct Texture : IDisposable public Texture() { } + public void Draw( Vector2 size ) + { + if( TextureWrap != null ) + { + ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); + size = size.X < TextureWrap.Width + ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } + : new Vector2( TextureWrap.Width, TextureWrap.Height ); + + ImGui.Image( TextureWrap.ImGuiHandle, size ); + } + else if( LoadError != null ) + { + ImGui.TextUnformatted( "Could not load file:" ); + ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); + } + else + { + ImGui.Dummy( size ); + } + } + private void Clean() { RGBAPixels = Array.Empty< byte >(); @@ -51,47 +73,88 @@ public struct Texture : IDisposable TextureWrap = null; ( BaseImage as IDisposable )?.Dispose(); BaseImage = null; + Loaded?.Invoke( false ); } public void Dispose() => Clean(); - public bool Load( string path ) + public event Action< bool >? Loaded; + + private void Load( string path ) { + _tmpPath = null; if( path == Path ) { - return false; + return; } Path = path; Clean(); - return System.IO.Path.GetExtension( Path ) switch + try { - ".dds" => LoadDds(), - ".png" => LoadPng(), - ".tex" => LoadTex(), - _ => true, - }; + var _ = System.IO.Path.GetExtension( Path ) switch + { + ".dds" => LoadDds(), + ".png" => LoadPng(), + ".tex" => LoadTex(), + _ => true, + }; + Loaded?.Invoke( true ); + } + catch( Exception e ) + { + LoadError = e; + Clean(); + } } private bool LoadDds() - => true; + { + var scratch = ScratchImage.LoadDDS( Path ); + BaseImage = scratch; + var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); + RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); + CreateTextureWrap( f.Meta.Width, f.Meta.Height ); + return true; + } private bool LoadPng() - => true; + { + BaseImage = null; + using var stream = File.OpenRead( Path ); + using var png = Image.Load< Rgba32 >( stream ); + RGBAPixels = new byte[png.Height * png.Width * 4]; + png.CopyPixelDataTo( RGBAPixels ); + CreateTextureWrap( png.Width, png.Height ); + return true; + } private bool LoadTex() - => true; + { + var tex = System.IO.Path.IsPathRooted( Path ) + ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) + : Dalamud.GameData.GetFile< TexFile >( Path ); + BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); + RGBAPixels = tex.GetRgbaImageData(); + CreateTextureWrap( tex.Header.Width, tex.Header.Height ); + return true; + } + + private void CreateTextureWrap( int width, int height ) + => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); + + private string? _tmpPath; public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) { - var tmp = Path; + _tmpPath ??= 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 ); + ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) { - Load( tmp ); + Load( _tmpPath ); } ImGuiUtil.HoverTooltip( tooltip ); @@ -119,7 +182,7 @@ public struct Texture : IDisposable } public static Texture Combined( Texture left, Texture right, InputManipulations leftManips, InputManipulations rightManips ) - => new Texture(); + => new(); } public struct InputManipulations @@ -352,13 +415,17 @@ public partial class ModEditWindow { try { - if (!ScratchImage.LoadDDS( path, out var f )) + if( !ScratchImage.LoadDDS( path, out var f ) ) + { return ( null, 0, 0 ); + } - if(!f.GetRGBA( out f )) + if( !f.GetRGBA( out f ) ) + { return ( null, 0, 0 ); + } - return ( f.Pixels[ ..(f.Meta.Width * f.Meta.Height * 4) ].ToArray(), f.Meta.Width, f.Meta.Height ); + return ( f.Pixels[ ..( f.Meta.Width * f.Meta.Height * 4 ) ].ToArray(), f.Meta.Width, f.Meta.Height ); } catch( Exception e ) { @@ -715,4 +782,4 @@ public partial class ModEditWindow } } } -} +} \ No newline at end of file From 2010e0203490750d70b5920b7c3cecd2740e947a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Sep 2022 18:36:34 +0200 Subject: [PATCH 0446/2451] Add some start for Evp Data. --- Penumbra.GameData/Files/MtrlFile.Write.cs | 1 - Penumbra.GameData/Structs/EqpEntry.cs | 117 +++++++++++----------- Penumbra/Meta/Files/EvpFile.cs | 68 +++++++++++++ Penumbra/Meta/Files/MetaBaseFile.cs | 2 +- 4 files changed, 128 insertions(+), 60 deletions(-) create mode 100644 Penumbra/Meta/Files/EvpFile.cs diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 8a3df6f5..4b54c30f 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using System.Text; diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 8ea20ecc..b2f3f22c 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -9,23 +9,23 @@ namespace Penumbra.GameData.Structs; [Flags] public enum EqpEntry : ulong { - BodyEnabled = 0x00_01ul, - BodyHideWaist = 0x00_02ul, - BodyHideThighs = 0x00_04ul, - BodyHideGlovesS = 0x00_08ul, - _4 = 0x00_10ul, - BodyHideGlovesM = 0x00_20ul, - BodyHideGlovesL = 0x00_40ul, - BodyHideGorget = 0x00_80ul, - BodyShowLeg = 0x01_00ul, - BodyShowHand = 0x02_00ul, - BodyShowHead = 0x04_00ul, - BodyShowNecklace = 0x08_00ul, - BodyShowBracelet = 0x10_00ul, - BodyShowTail = 0x20_00ul, - DisableBreastPhysics = 0x40_00ul, - _15 = 0x80_00ul, - BodyMask = 0xFF_FFul, + BodyEnabled = 0x00_01ul, + BodyHideWaist = 0x00_02ul, + BodyHideThighs = 0x00_04ul, + BodyHideGlovesS = 0x00_08ul, + _4 = 0x00_10ul, + BodyHideGlovesM = 0x00_20ul, + BodyHideGlovesL = 0x00_40ul, + BodyHideGorget = 0x00_80ul, + BodyShowLeg = 0x01_00ul, + BodyShowHand = 0x02_00ul, + BodyShowHead = 0x04_00ul, + BodyShowNecklace = 0x08_00ul, + BodyShowBracelet = 0x10_00ul, + BodyShowTail = 0x20_00ul, + BodyDisableBreastPhysics = 0x40_00ul, + BodyHasVfxObject = 0x80_00ul, + BodyMask = 0xFF_FFul, LegsEnabled = 0x01ul << 16, LegsHideKneePads = 0x02ul << 16, @@ -75,7 +75,7 @@ public enum EqpEntry : ulong _55 = 0x00_80_00ul << 40, HeadShowHrothgarHat = 0x01_00_00ul << 40, HeadShowVieraHat = 0x02_00_00ul << 40, - _58 = 0x04_00_00ul << 40, + HeadHasVfxObject = 0x04_00_00ul << 40, _59 = 0x08_00_00ul << 40, _60 = 0x10_00_00ul << 40, _61 = 0x20_00_00ul << 40, @@ -136,22 +136,22 @@ public static class Eqp { return entry switch { - EqpEntry.BodyEnabled => EquipSlot.Body, - EqpEntry.BodyHideWaist => EquipSlot.Body, - EqpEntry.BodyHideThighs => EquipSlot.Body, - EqpEntry.BodyHideGlovesS => EquipSlot.Body, - EqpEntry._4 => EquipSlot.Body, - EqpEntry.BodyHideGlovesM => EquipSlot.Body, - EqpEntry.BodyHideGlovesL => EquipSlot.Body, - EqpEntry.BodyHideGorget => EquipSlot.Body, - EqpEntry.BodyShowLeg => EquipSlot.Body, - EqpEntry.BodyShowHand => EquipSlot.Body, - EqpEntry.BodyShowHead => EquipSlot.Body, - EqpEntry.BodyShowNecklace => EquipSlot.Body, - EqpEntry.BodyShowBracelet => EquipSlot.Body, - EqpEntry.BodyShowTail => EquipSlot.Body, - EqpEntry.DisableBreastPhysics => EquipSlot.Body, - EqpEntry._15 => EquipSlot.Body, + EqpEntry.BodyEnabled => EquipSlot.Body, + EqpEntry.BodyHideWaist => EquipSlot.Body, + EqpEntry.BodyHideThighs => EquipSlot.Body, + EqpEntry.BodyHideGlovesS => EquipSlot.Body, + EqpEntry._4 => EquipSlot.Body, + EqpEntry.BodyHideGlovesM => EquipSlot.Body, + EqpEntry.BodyHideGlovesL => EquipSlot.Body, + EqpEntry.BodyHideGorget => EquipSlot.Body, + EqpEntry.BodyShowLeg => EquipSlot.Body, + EqpEntry.BodyShowHand => EquipSlot.Body, + EqpEntry.BodyShowHead => EquipSlot.Body, + EqpEntry.BodyShowNecklace => EquipSlot.Body, + EqpEntry.BodyShowBracelet => EquipSlot.Body, + EqpEntry.BodyShowTail => EquipSlot.Body, + EqpEntry.BodyDisableBreastPhysics => EquipSlot.Body, + EqpEntry.BodyHasVfxObject => EquipSlot.Body, EqpEntry.LegsEnabled => EquipSlot.Legs, EqpEntry.LegsHideKneePads => EquipSlot.Legs, @@ -198,9 +198,9 @@ public static class Eqp EqpEntry._55 => EquipSlot.Head, EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, EqpEntry.HeadShowVieraHat => EquipSlot.Head, + EqpEntry.HeadHasVfxObject => EquipSlot.Head, - // Currently unused. - EqpEntry._58 => EquipSlot.Unknown, + // currently unused EqpEntry._59 => EquipSlot.Unknown, EqpEntry._60 => EquipSlot.Unknown, EqpEntry._61 => EquipSlot.Unknown, @@ -215,22 +215,22 @@ public static class Eqp { return entry switch { - EqpEntry.BodyEnabled => "Enabled", - EqpEntry.BodyHideWaist => "Hide Waist", - EqpEntry.BodyHideThighs => "Hide Thigh Pads", - EqpEntry.BodyHideGlovesS => "Hide Small Gloves", - EqpEntry._4 => "Unknown 4", - EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", - EqpEntry.BodyHideGlovesL => "Hide Large Gloves", - EqpEntry.BodyHideGorget => "Hide Gorget", - EqpEntry.BodyShowLeg => "Show Legs", - EqpEntry.BodyShowHand => "Show Hands", - EqpEntry.BodyShowHead => "Show Head", - EqpEntry.BodyShowNecklace => "Show Necklace", - EqpEntry.BodyShowBracelet => "Show Bracelet", - EqpEntry.BodyShowTail => "Show Tail", - EqpEntry.DisableBreastPhysics => "Disable Breast Physics", - EqpEntry._15 => "Unknown 15", + EqpEntry.BodyEnabled => "Enabled", + EqpEntry.BodyHideWaist => "Hide Waist", + EqpEntry.BodyHideThighs => "Hide Thigh Pads", + EqpEntry.BodyHideGlovesS => "Hide Small Gloves", + EqpEntry._4 => "Unknown 4", + EqpEntry.BodyHideGlovesM => "Hide Medium Gloves", + EqpEntry.BodyHideGlovesL => "Hide Large Gloves", + EqpEntry.BodyHideGorget => "Hide Gorget", + EqpEntry.BodyShowLeg => "Show Legs", + EqpEntry.BodyShowHand => "Show Hands", + EqpEntry.BodyShowHead => "Show Head", + EqpEntry.BodyShowNecklace => "Show Necklace", + EqpEntry.BodyShowBracelet => "Show Bracelet", + EqpEntry.BodyShowTail => "Show Tail", + EqpEntry.BodyDisableBreastPhysics => "Disable Breast Physics", + EqpEntry.BodyHasVfxObject => "Has Special Effects", EqpEntry.LegsEnabled => "Enabled", EqpEntry.LegsHideKneePads => "Hide Knee Pads", @@ -277,12 +277,13 @@ public static class Eqp EqpEntry._55 => "Unknown 55", EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar", EqpEntry.HeadShowVieraHat => "Show on Viera", - EqpEntry._58 => "Unknown 58", - EqpEntry._59 => "Unknown 59", - EqpEntry._60 => "Unknown 60", - EqpEntry._61 => "Unknown 61", - EqpEntry._62 => "Unknown 62", - EqpEntry._63 => "Unknown 63", + EqpEntry.HeadHasVfxObject => "Has Special Effects", + + EqpEntry._59 => "Unknown 59", + EqpEntry._60 => "Unknown 60", + EqpEntry._61 => "Unknown 61", + EqpEntry._62 => "Unknown 62", + EqpEntry._63 => "Unknown 63", _ => throw new InvalidEnumArgumentException(), }; diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs new file mode 100644 index 00000000..b6dcbb30 --- /dev/null +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -0,0 +1,68 @@ +using System; + +namespace Penumbra.Meta.Files; + + +// EVP file structure: +// [Identifier:3 bytes, EVP] +// [NumModels:ushort] +// NumModels x [ModelId:ushort] +// Containing the relevant model IDs. Seems to be sorted. +// NumModels x [DataArray]:512 Byte] +// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. Unsure where the index into this array comes from. +public unsafe class EvpFile : MetaBaseFile +{ + public const int FlagArraySize = 512; + + [Flags] + public enum EvpFlag : byte + { + None = 0x00, + Body = 0x01, + Head = 0x02, + Both = Body | Head, + } + + public int NumModels + => Data[ 3 ]; + + public ReadOnlySpan< ushort > ModelSetIds + => new(Data + 4, NumModels); + + public ushort ModelSetId( int idx ) + => idx >= 0 && idx < NumModels ? ( ( ushort* )( Data + 4 ) )[ idx ] : ushort.MaxValue; + + public ReadOnlySpan< EvpFlag > Flags( int idx ) + => new(Data + 4 + idx * FlagArraySize, FlagArraySize); + + public EvpFlag Flag( ushort modelSet, int arrayIndex ) + { + if( arrayIndex is >= FlagArraySize or < 0 ) + { + return EvpFlag.None; + } + + var ids = ModelSetIds; + for( var i = 0; i < ids.Length; ++i ) + { + var model = ids[ i ]; + if( model < modelSet ) + { + continue; + } + + if( model > modelSet ) + { + break; + } + + return Flags( i )[ arrayIndex ]; + } + + return EvpFlag.None; + } + + public EvpFile() + : base( ( Interop.Structs.CharacterUtility.Index )1 ) // TODO: Name + { } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 8bc827f5..8a167216 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -12,7 +12,7 @@ public unsafe class MetaBaseFile : IDisposable public CharacterUtility.InternalIndex Index { get; } public MetaBaseFile( Interop.Structs.CharacterUtility.Index idx ) - => Index = CharacterUtility.ReverseIndices[(int) idx]; + => Index = CharacterUtility.ReverseIndices[ ( int )idx ]; protected (IntPtr Data, int Length) DefaultData => Penumbra.CharacterUtility.DefaultResource( Index ); From 37ad1f68b09160eb3da690b47eafa3518778ca0f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Sep 2022 18:37:35 +0200 Subject: [PATCH 0447/2451] Fix Adventurer Plates. --- .../Resolver/PathResolver.Identification.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index abd40704..9ffb6e94 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -5,6 +5,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; @@ -40,34 +41,27 @@ public unsafe partial class PathResolver } var ui = ( AtkUnitBase* )addon; - if( ui->UldManager.NodeListCount <= 60 ) - { - return null; - } + var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 2u : 6u; - var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 59 : 60; - - var text = ( AtkTextNode* )ui->UldManager.NodeList[ nodeId ]; + var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); return text != null ? text->NodeText.ToString() : null; } // Obtain the name displayed in the Character Card from the agent. private static string? GetCardName() { + // TODO: Update to ClientStructs when merged. if( !Penumbra.Config.UseCharacterCollectionsInCards ) { return null; } - var uiModule = ( UIModule* )Dalamud.GameGui.GetUIModule(); - var agentModule = uiModule->GetAgentModule(); - var agent = ( byte* )agentModule->GetAgentByInternalID( 393 ); + var agent = AgentCharaCard.Instance(); if( agent == null ) { return null; } - - var data = *( byte** )( agent + 0x28 ); + var data = *( byte** )( (byte*) agent + 0x28 ); if( data == null ) { return null; From ae842720eead07bbb6583ba3607af885cb58e418 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Sep 2022 15:23:44 +0200 Subject: [PATCH 0448/2451] More Evp stuff, update libs, fix archive extraction part 1. --- Penumbra.GameData/Structs/EqpEntry.cs | 12 ++++----- Penumbra/Import/TexToolsImporter.Archives.cs | 23 ++++++++++++++---- .../Resolver/PathResolver.AnimationState.cs | 21 +++++++++------- Penumbra/Meta/Files/EvpFile.cs | 3 ++- Penumbra/lib/DirectXTexC.dll | Bin 585728 -> 585728 bytes Penumbra/lib/OtterTex.dll | Bin 31744 -> 32256 bytes 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index b2f3f22c..b628aa63 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -24,7 +24,7 @@ public enum EqpEntry : ulong BodyShowBracelet = 0x10_00ul, BodyShowTail = 0x20_00ul, BodyDisableBreastPhysics = 0x40_00ul, - BodyHasVfxObject = 0x80_00ul, + BodyUsesEvpTable = 0x80_00ul, BodyMask = 0xFF_FFul, LegsEnabled = 0x01ul << 16, @@ -75,7 +75,7 @@ public enum EqpEntry : ulong _55 = 0x00_80_00ul << 40, HeadShowHrothgarHat = 0x01_00_00ul << 40, HeadShowVieraHat = 0x02_00_00ul << 40, - HeadHasVfxObject = 0x04_00_00ul << 40, + HeadUsesEvpTable = 0x04_00_00ul << 40, _59 = 0x08_00_00ul << 40, _60 = 0x10_00_00ul << 40, _61 = 0x20_00_00ul << 40, @@ -151,7 +151,7 @@ public static class Eqp EqpEntry.BodyShowBracelet => EquipSlot.Body, EqpEntry.BodyShowTail => EquipSlot.Body, EqpEntry.BodyDisableBreastPhysics => EquipSlot.Body, - EqpEntry.BodyHasVfxObject => EquipSlot.Body, + EqpEntry.BodyUsesEvpTable => EquipSlot.Body, EqpEntry.LegsEnabled => EquipSlot.Legs, EqpEntry.LegsHideKneePads => EquipSlot.Legs, @@ -198,7 +198,7 @@ public static class Eqp EqpEntry._55 => EquipSlot.Head, EqpEntry.HeadShowHrothgarHat => EquipSlot.Head, EqpEntry.HeadShowVieraHat => EquipSlot.Head, - EqpEntry.HeadHasVfxObject => EquipSlot.Head, + EqpEntry.HeadUsesEvpTable => EquipSlot.Head, // currently unused EqpEntry._59 => EquipSlot.Unknown, @@ -230,7 +230,7 @@ public static class Eqp EqpEntry.BodyShowBracelet => "Show Bracelet", EqpEntry.BodyShowTail => "Show Tail", EqpEntry.BodyDisableBreastPhysics => "Disable Breast Physics", - EqpEntry.BodyHasVfxObject => "Has Special Effects", + EqpEntry.BodyUsesEvpTable => "Uses EVP Table", EqpEntry.LegsEnabled => "Enabled", EqpEntry.LegsHideKneePads => "Hide Knee Pads", @@ -277,7 +277,7 @@ public static class Eqp EqpEntry._55 => "Unknown 55", EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar", EqpEntry.HeadShowVieraHat => "Show on Viera", - EqpEntry.HeadHasVfxObject => "Has Special Effects", + EqpEntry.HeadUsesEvpTable => "Uses EVP Table", EqpEntry._59 => "Unknown 59", EqpEntry._60 => "Unknown 60", diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 1b8a45fa..12f7428f 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -8,7 +8,11 @@ using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Mods; using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Archives.Zip; using SharpCompress.Common; +using SharpCompress.Readers; namespace Penumbra.Import; @@ -30,7 +34,14 @@ public partial class TexToolsImporter _currentModName = modPackFile.Name; _currentGroupName = string.Empty; _currentOptionName = DefaultTexToolsData.Name; - _currentNumFiles = archive.Entries.Count( e => !e.IsDirectory ); + _currentNumFiles = + archive switch + { + RarArchive r => r.Entries.Count, + ZipArchive z => z.Entries.Count, + SevenZipArchive s => s.Entries.Count, + _ => archive.Entries.Count(), + }; PluginLog.Log( $" -> Importing {archive.Type} Archive." ); _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); @@ -42,18 +53,20 @@ public partial class TexToolsImporter State = ImporterState.ExtractingModFiles; _currentFileIdx = 0; - foreach( var entry in archive.Entries ) + var reader = archive.ExtractAllEntries(); + + while(reader.MoveToNextEntry()) { _token.ThrowIfCancellationRequested(); - if( entry.IsDirectory ) + if( reader.Entry.IsDirectory ) { ++_currentFileIdx; continue; } - PluginLog.Log( " -> Extracting {0}", entry.Key ); - entry.WriteToDirectory( _currentModDirectory.FullName, options ); + PluginLog.Log( " -> Extracting {0}", reader.Entry.Key ); + reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); ++_currentFileIdx; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index ca282732..518e4a93 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -110,16 +110,19 @@ public unsafe partial class PathResolver var old = _animationLoadCollection; try { - var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; - var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < Dalamud.Objects.Length ) + if( timeline != IntPtr.Zero ) { - var obj = Dalamud.Objects[ idx ]; - _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; - } - else - { - _animationLoadCollection = null; + var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ]; + var idx = getGameObjectIdx( timeline ); + if( idx >= 0 && idx < Dalamud.Objects.Length ) + { + var obj = Dalamud.Objects[ idx ]; + _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; + } + else + { + _animationLoadCollection = null; + } } } finally diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index b6dcbb30..5b72c449 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -9,7 +9,8 @@ namespace Penumbra.Meta.Files; // NumModels x [ModelId:ushort] // Containing the relevant model IDs. Seems to be sorted. // NumModels x [DataArray]:512 Byte] -// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. Unsure where the index into this array comes from. +// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. +// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. public unsafe class EvpFile : MetaBaseFile { public const int FlagArraySize = 512; diff --git a/Penumbra/lib/DirectXTexC.dll b/Penumbra/lib/DirectXTexC.dll index 7a2d217a94bf5c1870c7bb81552beb7306102d9f..083c20e9efd442251c355fa32f721295f3f6ef4d 100644 GIT binary patch delta 50072 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 S?KPK$317Q^)wg=8@BaYiCn#$G delta 50049 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≫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 VDq2{qt_4g?xc}}|-|6YT{{?BIA+rDg diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index f0d0db79ff8176482907933d17517976a547ee5d..447939777545e5efbf41b084e85a3a1f804cb3c2 100644 GIT binary patch literal 32256 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 literal 31744 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 kh%>y#QuW3Ehcnl0t(<~|kf+ti Date: Sun, 4 Sep 2022 02:51:42 +0200 Subject: [PATCH 0449/2451] Stop replacing UI hashes to test if that fixes crashes. --- Penumbra/Interop/Loader/ResourceLoader.Debug.cs | 1 - .../Interop/Loader/ResourceLoader.Replacement.cs | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index bdcf1220..02af20ce 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection.Metadata; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index c14a6720..ddbcf10f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -97,9 +97,15 @@ public unsafe partial class ResourceLoader return retUnmodified; } - // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); - path = resolvedPath.Value.InternalName.Path; + // Replace the hash and path with the correct one for the replacement, + // but only for non-UI files. UI files can not reasonably be loaded multiple times at once, + // and seem to cause concurrency problems if multiple UI parts use the same resource for different use-cases. + if( *categoryId != ResourceCategory.Ui ) + { + *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); + } + + path = resolvedPath.Value.InternalName.Path; var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retModified, gamePath, resolvedPath.Value, data ); return retModified; From 07af64feeda1dd5390c9fcae9aea911115e65bb4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Sep 2022 12:39:41 +0200 Subject: [PATCH 0450/2451] Small fixes. --- Penumbra/Interop/Loader/ResourceLoader.Replacement.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Meta.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index ddbcf10f..bf1932dd 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -153,7 +153,7 @@ public unsafe partial class ResourceLoader // We hook ReadSqPack to redirect rooted files to ReadFile. public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = "ReadSqPackDetour" )] + [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = nameof(ReadSqPackDetour) )] public Hook< ReadSqPackPrototype > ReadSqPackHook = null!; private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs index 96fb8fa0..312ee9cf 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs @@ -12,7 +12,7 @@ public sealed partial class Mod public void ChangeModName( Index idx, string newName ) { var mod = this[ idx ]; - if( mod.Name != newName ) + if( mod.Name.Text != newName ) { var oldName = mod.Name; mod.Name = newName; From dcdc6d1be1f26427ea3482f516278ef18c40e83f Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 3 Sep 2022 16:09:31 +0200 Subject: [PATCH 0451/2451] add LinkedModCollection to be able to retrospectively verify which gamepath was resolved for which game object --- Penumbra/Api/IPenumbraApi.cs | 3 ++ Penumbra/Api/PenumbraApi.cs | 18 ++++++++--- Penumbra/Api/PenumbraIpc.cs | 17 ++++++++++ Penumbra/Collections/LinkedModCollection.cs | 32 +++++++++++++++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 5 +-- .../Loader/ResourceLoader.Replacement.cs | 7 ++-- Penumbra/Interop/Loader/ResourceLoader.cs | 5 +-- .../Resolver/PathResolver.AnimationState.cs | 10 +++--- .../Resolver/PathResolver.DrawObjectState.cs | 18 +++++------ .../Resolver/PathResolver.Identification.cs | 18 +++++------ .../Interop/Resolver/PathResolver.Material.cs | 18 +++++++---- .../Interop/Resolver/PathResolver.Meta.cs | 24 +++++++------- .../Resolver/PathResolver.PathState.cs | 18 +++++------ .../Resolver/PathResolver.ResolverHooks.cs | 13 ++++---- Penumbra/Interop/Resolver/PathResolver.cs | 20 ++++++------ Penumbra/UI/ConfigWindow.DebugTab.cs | 4 +-- 16 files changed, 151 insertions(+), 79 deletions(-) create mode 100644 Penumbra/Collections/LinkedModCollection.cs diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 69b7479e..57e13baa 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -27,6 +27,7 @@ public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollec IntPtr equipData ); public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr drawObject ); +public delegate void GameObjectResourceResolvedDelegate( IntPtr gameObject, string gamePath, string localPath ); public enum PenumbraApiEc { @@ -79,6 +80,8 @@ public interface IPenumbraApi : IPenumbraApiBase // so you can apply flag changes after finishing. public event CreatedCharacterBaseDelegate? CreatedCharacterBase; + public event GameObjectResourceResolvedDelegate GameObjectResourceResolved; + // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index fb708bbe..0546ac53 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -13,6 +13,7 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Resolver; +using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -21,7 +22,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 12 ); + => ( 4, 13 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -54,7 +55,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool Valid => _penumbra != null; - public PenumbraApi( Penumbra penumbra ) + public unsafe PenumbraApi( Penumbra penumbra ) { _penumbra = penumbra; _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() @@ -66,10 +67,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi } Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; + Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; } - public void Dispose() + public unsafe void Dispose() { + Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; _penumbra = null; _lumina = null; @@ -90,6 +93,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } + private unsafe void OnResourceLoaded( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, LinkedModCollection? resolveData ) + { + if( resolveData == null ) return; + GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), manipulatedPath?.ToString() ?? originalPath.ToString() ); + } + public event Action< string, bool >? ModDirectoryChanged { add => Penumbra.ModManager.ModDirectoryChanged += value; @@ -103,6 +112,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public event ChangedItemHover? ChangedItemTooltip; + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; public void RedrawObject( int tableIndex, RedrawType setting ) { @@ -232,7 +242,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject ); - return ( obj, collection.Name ); + return ( obj, collection.ModCollection.Name ); } public int GetCutsceneParentIndex( int actor ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 34172c92..a38aecce 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -283,6 +283,7 @@ public partial class PenumbraIpc public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase"; + public const string LabelProviderGameObjectResourcePathResolved = "Penumbra.GameObjectResourcePathResolved"; internal ICallGateProvider< string, string >? ProviderResolveDefault; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; @@ -293,6 +294,7 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; internal ICallGateProvider< IntPtr, string, IntPtr, object? >? ProviderCreatedCharacterBase; + internal ICallGateProvider? ProviderGameObjectResourcePathResolved; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -387,6 +389,21 @@ public partial class PenumbraIpc { PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" ); } + + try + { + ProviderGameObjectResourcePathResolved = pi.GetIpcProvider( LabelProviderGameObjectResourcePathResolved ); + Api.GameObjectResourceResolved += GameObjectResourceResolvdedEvent; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGameObjectResourcePathResolved}:\n{e}" ); + } + } + + private void GameObjectResourceResolvdedEvent( IntPtr gameObject, string gamePath, string localPath ) + { + ProviderGameObjectResourcePathResolved?.SendMessage( gameObject, gamePath, localPath ); } private void DisposeResolveProviders() diff --git a/Penumbra/Collections/LinkedModCollection.cs b/Penumbra/Collections/LinkedModCollection.cs new file mode 100644 index 00000000..80cd74c7 --- /dev/null +++ b/Penumbra/Collections/LinkedModCollection.cs @@ -0,0 +1,32 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Game.Object; + +namespace Penumbra.Collections; + +public class LinkedModCollection +{ + private IntPtr? _associatedGameObject; + public IntPtr AssociatedGameObject + { + get => _associatedGameObject ?? IntPtr.Zero; + set => _associatedGameObject = value; + } + public ModCollection ModCollection; + + public LinkedModCollection(ModCollection modCollection) + { + ModCollection = modCollection; + } + + public LinkedModCollection(IntPtr? gameObject, ModCollection collection) + { + AssociatedGameObject = gameObject ?? IntPtr.Zero; + ModCollection = collection; + } + + public unsafe LinkedModCollection(GameObject* gameObject, ModCollection collection) + { + AssociatedGameObject = ( IntPtr )gameObject; + ModCollection = collection; + } +} diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 02af20ce..4e162bca 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,6 +7,7 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -69,7 +70,7 @@ public unsafe partial class ResourceLoader } private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - object? resolverInfo ) + LinkedModCollection? resolverInfo ) { if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 ) { @@ -243,7 +244,7 @@ public unsafe partial class ResourceLoader private static void LogPath( Utf8GamePath path, bool synchronous ) => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) + private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, LinkedModCollection? _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index bf1932dd..a9a996f6 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -9,6 +9,7 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -113,14 +114,14 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. - public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) + public static (FullPath?, LinkedModCollection?) DefaultResolver( Utf8GamePath path ) { var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); - return ( resolved, null ); + return ( resolved, new LinkedModCollection( Penumbra.CollectionManager.Default ) ); } // Try all resolve path subscribers or use the default replacer. - private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + private (FullPath?, LinkedModCollection?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { if( !DoReplacements || _incMode.Value ) { diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 059c1b5d..8f23d71f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -2,6 +2,7 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -118,7 +119,7 @@ public unsafe partial class ResourceLoader : IDisposable // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. // resolveData is additional data returned by the current ResolvePath function and is user-defined. public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - object? resolveData ); + LinkedModCollection? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; @@ -133,7 +134,7 @@ public unsafe partial class ResourceLoader : IDisposable // Resolving goes through all subscribed functions in arbitrary order until one returns true, // or uses default resolving if none return true. public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash, - out (FullPath?, object?) ret ); + out (FullPath?, LinkedModCollection?) ret ); public event ResolvePathDelegate? ResolvePathCustomization; diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 518e4a93..d6106a87 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -15,8 +15,8 @@ public unsafe partial class PathResolver { private readonly DrawObjectState _drawObjectState; - private ModCollection? _animationLoadCollection; - private ModCollection? _lastAvfxCollection; + private LinkedModCollection? _animationLoadCollection; + private LinkedModCollection? _lastAvfxCollection; public AnimationState( DrawObjectState drawObjectState ) { @@ -24,7 +24,7 @@ public unsafe partial class PathResolver SignatureHelper.Initialise( this ); } - public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection ) + public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out LinkedModCollection? collection ) { switch( type ) { @@ -39,7 +39,7 @@ public unsafe partial class PathResolver break; case ResourceType.Avfx: - _lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default; + _lastAvfxCollection = _animationLoadCollection ?? new LinkedModCollection(Penumbra.CollectionManager.Default); if( _animationLoadCollection != null ) { collection = _animationLoadCollection; @@ -147,7 +147,7 @@ public unsafe partial class PathResolver { var last = _animationLoadCollection; _animationLoadCollection = _drawObjectState.LastCreatedCollection - ?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default ); + ?? ( FindParent( drawObject, out var collection ) != null ? collection : new LinkedModCollection(Penumbra.CollectionManager.Default) ); _characterBaseLoadAnimationHook.Original( drawObject ); _animationLoadCollection = last; } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index c3c40238..1e6ed477 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -20,13 +20,13 @@ public unsafe partial class PathResolver public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; - public IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjects + public IEnumerable< KeyValuePair< IntPtr, (LinkedModCollection, int) > > DrawObjects => _drawObjectToObject; public int Count => _drawObjectToObject.Count; - public bool TryGetValue( IntPtr drawObject, out (ModCollection, int) value, out GameObject* gameObject ) + public bool TryGetValue( IntPtr drawObject, out (LinkedModCollection, int) value, out GameObject* gameObject ) { gameObject = null; if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) @@ -40,7 +40,7 @@ public unsafe partial class PathResolver // Set and update a parent object if it exists and a last game object is set. - public ModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) + public LinkedModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) { if( parentObject == IntPtr.Zero && LastGameObject != null ) { @@ -53,7 +53,7 @@ public unsafe partial class PathResolver } - public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection ) + public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out LinkedModCollection? collection ) { if( type == ResourceType.Tex && LastCreatedCollection != null @@ -68,7 +68,7 @@ public unsafe partial class PathResolver } - public ModCollection? LastCreatedCollection + public LinkedModCollection? LastCreatedCollection => _lastCreatedCollection; public GameObject* LastGameObject { get; private set; } @@ -124,8 +124,8 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary< IntPtr, (ModCollection, int) > _drawObjectToObject = new(); - private ModCollection? _lastCreatedCollection; + private readonly Dictionary< IntPtr, (LinkedModCollection, int) > _drawObjectToObject = new(); + private LinkedModCollection? _lastCreatedCollection; // Keep track of created DrawObjects that are CharacterBase, // and use the last game object that called EnableDraw to link them. @@ -141,14 +141,14 @@ public unsafe partial class PathResolver if( LastGameObject != null ) { var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c ); + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); } var ret = _characterBaseCreateHook.Original( a, b, c, d ); if( LastGameObject != null ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ret ); + CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ret ); } return ret; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 9ffb6e94..9b0586dd 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -139,11 +139,11 @@ public unsafe partial class PathResolver } // Identify the correct collection for a GameObject by index and name. - private static ModCollection IdentifyCollection( GameObject* gameObject ) + private static LinkedModCollection IdentifyCollection( GameObject* gameObject ) { if( gameObject == null ) { - return Penumbra.CollectionManager.Default; + return new LinkedModCollection(Penumbra.CollectionManager.Default); } try @@ -153,8 +153,8 @@ public unsafe partial class PathResolver // Actors are also not named. So use Yourself > Players > Racial > Default. if( !Dalamud.ClientState.IsLoggedIn ) { - return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default ); + return new LinkedModCollection(gameObject, Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default )); } else { @@ -163,7 +163,7 @@ public unsafe partial class PathResolver && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer { - return Penumbra.CollectionManager.Default; + return new LinkedModCollection((IntPtr)gameObject, Penumbra.CollectionManager.Default); } string? actorName = null; @@ -174,7 +174,7 @@ public unsafe partial class PathResolver if( actorName.Length > 0 && CollectionByActorName( actorName, out var actorCollection ) ) { - return actorCollection; + return new LinkedModCollection(gameObject, actorCollection); } } @@ -193,17 +193,17 @@ public unsafe partial class PathResolver ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); // First check temporary character collections, then the own configuration, then special collections. - return CollectionByActorName( actualName, out var c ) + return new LinkedModCollection(gameObject, CollectionByActorName( actualName, out var c ) ? c : CollectionByActor( actualName, gameObject, out c ) ? c - : Penumbra.CollectionManager.Default; + : Penumbra.CollectionManager.Default); } } catch( Exception e ) { PluginLog.Error( $"Error identifying collection:\n{e}" ); - return Penumbra.CollectionManager.Default; + return new LinkedModCollection(gameObject, Penumbra.CollectionManager.Default); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 1d92f96d..485eb790 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -4,6 +4,7 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -20,7 +21,7 @@ public unsafe partial class PathResolver { private readonly PathState _paths; - private ModCollection? _mtrlCollection; + private LinkedModCollection? _mtrlCollection; public MaterialState( PathState paths ) { @@ -29,7 +30,7 @@ public unsafe partial class PathResolver } // Check specifically for shpk and tex files whether we are currently in a material load. - public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection ) + public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out LinkedModCollection? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { @@ -42,12 +43,12 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - public static void HandleCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, object?) data ) + public static void HandleCollection( LinkedModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, LinkedModCollection?) data ) { if( nonDefault && type == ResourceType.Mtrl ) { - var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" ); + var fullPath = new FullPath( $"|{collection.ModCollection.Name}_{collection.ModCollection.ChangeCounter}|{path}" ); data = ( fullPath, collection ); } else @@ -96,7 +97,12 @@ public unsafe partial class PathResolver #if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); #endif - _paths.SetCollection( path, collection ); + IntPtr gameObjAddr = IntPtr.Zero; + if ( Dalamud.Objects.FindFirst(f => f.Name.TextValue == name, out var gameObj ) ) + { + gameObjAddr = gameObj.Address; + } + _paths.SetCollection( gameObjAddr, path, collection ); } else { diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 44fe74f2..a2699544 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -82,8 +82,8 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - using var eqp = MetaChanger.ChangeEqp( collection ); - using var eqdp = MetaChanger.ChangeEqdp( collection ); + using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); + using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } else @@ -109,8 +109,8 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - using var eqp = MetaChanger.ChangeEqp( collection ); - using var eqdp = MetaChanger.ChangeEqdp( collection ); + using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); + using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); _updateModelsHook.Original.Invoke( drawObject ); } else @@ -217,7 +217,7 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - return ChangeEqp( collection ); + return ChangeEqp( collection.ModCollection ); } return new MetaChanger( MetaManipulation.Type.Unknown ); @@ -231,7 +231,7 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - return ChangeEqdp( collection ); + return ChangeEqdp( collection.ModCollection ); } } @@ -249,7 +249,7 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - collection.SetGmpFiles(); + collection.ModCollection.SetGmpFiles(); return new MetaChanger( MetaManipulation.Type.Gmp ); } @@ -261,21 +261,21 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - collection.SetEstFiles(); + collection.ModCollection.SetEstFiles(); return new MetaChanger( MetaManipulation.Type.Est ); } return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( GameObject* gameObject, out ModCollection? collection ) + public static MetaChanger ChangeCmp( GameObject* gameObject, out LinkedModCollection? collection ) { if( gameObject != null ) { collection = IdentifyCollection( gameObject ); - if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) + if( collection.ModCollection != Penumbra.CollectionManager.Default && collection.ModCollection.HasCache ) { - collection.SetCmpFiles(); + collection.ModCollection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } } @@ -292,7 +292,7 @@ public unsafe partial class PathResolver var collection = GetCollection( drawObject ); if( collection != null ) { - collection.SetCmpFiles(); + collection.ModCollection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index ab40fe1e..6ee4aefc 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -32,7 +32,7 @@ public unsafe partial class PathResolver private readonly ResolverHooks _monster; // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< Utf8String, ModCollection > _pathCollections = new(); + private readonly ConcurrentDictionary< Utf8String, LinkedModCollection > _pathCollections = new(); public PathState( PathResolver parent ) { @@ -70,18 +70,18 @@ public unsafe partial class PathResolver public int Count => _pathCollections.Count; - public IEnumerable< KeyValuePair< Utf8String, ModCollection > > Paths + public IEnumerable< KeyValuePair< Utf8String, LinkedModCollection > > Paths => _pathCollections; - public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out ModCollection? collection ) + public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out LinkedModCollection? collection ) => _pathCollections.TryGetValue( path, out collection ); - public bool Consume( Utf8String path, [NotNullWhen( true )] out ModCollection? collection ) + public bool Consume( Utf8String path, [NotNullWhen( true )] out LinkedModCollection? collection ) => _pathCollections.TryRemove( path, out collection ); // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - public IntPtr ResolvePath( ModCollection collection, IntPtr path ) + public IntPtr ResolvePath( IntPtr? gameObject, ModCollection collection, IntPtr path ) { if( path == IntPtr.Zero ) { @@ -89,20 +89,20 @@ public unsafe partial class PathResolver } var gamePath = new Utf8String( ( byte* )path ); - SetCollection( gamePath, collection ); + SetCollection( gameObject, gamePath, collection ); return path; } // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - public void SetCollection( Utf8String path, ModCollection collection ) + public void SetCollection( IntPtr? gameObject, Utf8String path, ModCollection collection ) { if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { - _pathCollections[ path ] = collection; + _pathCollections[ path ] = new LinkedModCollection(gameObject, collection); } else { - _pathCollections[ path.Clone() ] = collection; + _pathCollections[ path.Clone() ] = new LinkedModCollection(gameObject, collection); } } } diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 79c53d31..2b0bd8aa 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Collections; namespace Penumbra.Interop.Resolver; @@ -226,9 +227,9 @@ public partial class PathResolver // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePath( IntPtr drawObject, IntPtr path ) - => _parent._paths.ResolvePath( FindParent( drawObject, out var collection ) == null + => _parent._paths.ResolvePath( (IntPtr?)FindParent( drawObject, out _), FindParent( drawObject, out var collection ) == null ? Penumbra.CollectionManager.Default - : collection, path ); + : collection.ModCollection, path ); // Weapons have the characters DrawObject as a parent, // but that may not be set yet when creating a new object, so we have to do the same detour @@ -239,20 +240,20 @@ public partial class PathResolver var parent = FindParent( drawObject, out var collection ); if( parent != null ) { - return _parent._paths.ResolvePath( collection, path ); + return _parent._paths.ResolvePath( (IntPtr)parent, collection.ModCollection, path ); } var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); if( parentCollection != null ) { - return _parent._paths.ResolvePath( parentCollection, path ); + return _parent._paths.ResolvePath( (IntPtr)FindParent(parentObject, out _), parentCollection.ModCollection, path ); } parent = FindParent( parentObject, out collection ); - return _parent._paths.ResolvePath( parent == null + return _parent._paths.ResolvePath( (IntPtr?)parent, parent == null ? Penumbra.CollectionManager.Default - : collection, path ); + : collection.ModCollection, path ); } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 300cf1f3..76164318 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -40,7 +40,7 @@ public partial class PathResolver : IDisposable } // The modified resolver that handles game path resolving. - private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, object?) data ) + private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, LinkedModCollection?) data ) { // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, @@ -54,11 +54,11 @@ public partial class PathResolver : IDisposable || DrawObjects.HandleDecalFile( type, gamePath, out collection ); if( !nonDefault || collection == null ) { - collection = Penumbra.CollectionManager.Default; + collection = new LinkedModCollection(Penumbra.CollectionManager.Default); } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = collection.ResolvePath( gamePath ); + var resolved = collection.ModCollection.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. @@ -117,7 +117,7 @@ public partial class PathResolver : IDisposable _materials.Dispose(); } - public static unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject ) + public static unsafe (IntPtr, LinkedModCollection) IdentifyDrawObject( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); return ( ( IntPtr )parent, collection ); @@ -127,7 +127,7 @@ public partial class PathResolver : IDisposable => Cutscenes.GetParentIndex( idx ); // Use the stored information to find the GameObject and Collection linked to a DrawObject. - public static unsafe GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) + public static unsafe GameObject* FindParent( IntPtr drawObject, out LinkedModCollection collection ) { if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) { @@ -146,21 +146,21 @@ public partial class PathResolver : IDisposable return null; } - private static unsafe ModCollection? GetCollection( IntPtr drawObject ) + private static unsafe LinkedModCollection? GetCollection( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection == Penumbra.CollectionManager.Default ) + if( parent == null || collection.ModCollection == Penumbra.CollectionManager.Default ) { return null; } - return collection.HasCache ? collection : null; + return collection.ModCollection.HasCache ? collection : null; } - internal IEnumerable< KeyValuePair< Utf8String, ModCollection > > PathCollections + internal IEnumerable< KeyValuePair< Utf8String, LinkedModCollection > > PathCollections => _paths.Paths; - internal IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjectMap + internal IEnumerable< KeyValuePair< IntPtr, (LinkedModCollection, int) > > DrawObjectMap => DrawObjects.DrawObjects; internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 764dc452..59ea02d2 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -177,7 +177,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( name ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( c.Name ); + ImGui.TextUnformatted( c.ModCollection.Name ); } } } @@ -195,7 +195,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.Name ); + ImGui.TextUnformatted( collection.ModCollection.Name ); } } } From e0000c9ef90015d80ca76e67a3a329965e93b753 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 3 Sep 2022 16:17:01 +0200 Subject: [PATCH 0452/2451] remove ottergui from material --- Penumbra/Interop/Resolver/PathResolver.Material.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 485eb790..b4a6c494 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,10 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -97,11 +97,9 @@ public unsafe partial class PathResolver #if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); #endif - IntPtr gameObjAddr = IntPtr.Zero; - if ( Dalamud.Objects.FindFirst(f => f.Name.TextValue == name, out var gameObj ) ) - { - gameObjAddr = gameObj.Address; - } + + var objFromObjTable = Dalamud.Objects.FirstOrDefault( f => f.Name.TextValue == name ); + IntPtr gameObjAddr = objFromObjTable?.Address ?? IntPtr.Zero; _paths.SetCollection( gameObjAddr, path, collection ); } else From 75182d094b8fa76890884ba8dc6f3186aad23b6a Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 3 Sep 2022 19:36:41 +0200 Subject: [PATCH 0453/2451] changes to LinkedModCollection nullability --- Penumbra/Collections/LinkedModCollection.cs | 16 +++++----------- .../Interop/Resolver/PathResolver.PathState.cs | 4 ++-- .../Resolver/PathResolver.ResolverHooks.cs | 4 ++-- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Penumbra/Collections/LinkedModCollection.cs b/Penumbra/Collections/LinkedModCollection.cs index 80cd74c7..93575544 100644 --- a/Penumbra/Collections/LinkedModCollection.cs +++ b/Penumbra/Collections/LinkedModCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using FFXIVClientStructs.FFXIV.Client.Game.Object; namespace Penumbra.Collections; @@ -6,11 +6,7 @@ namespace Penumbra.Collections; public class LinkedModCollection { private IntPtr? _associatedGameObject; - public IntPtr AssociatedGameObject - { - get => _associatedGameObject ?? IntPtr.Zero; - set => _associatedGameObject = value; - } + public IntPtr AssociatedGameObject = IntPtr.Zero; public ModCollection ModCollection; public LinkedModCollection(ModCollection modCollection) @@ -18,15 +14,13 @@ public class LinkedModCollection ModCollection = modCollection; } - public LinkedModCollection(IntPtr? gameObject, ModCollection collection) + public LinkedModCollection(IntPtr gameObject, ModCollection collection) { - AssociatedGameObject = gameObject ?? IntPtr.Zero; + AssociatedGameObject = gameObject; ModCollection = collection; } - public unsafe LinkedModCollection(GameObject* gameObject, ModCollection collection) + public unsafe LinkedModCollection(GameObject* gameObject, ModCollection collection) : this((IntPtr)gameObject, collection) { - AssociatedGameObject = ( IntPtr )gameObject; - ModCollection = collection; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 6ee4aefc..6a1d745a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -81,7 +81,7 @@ public unsafe partial class PathResolver // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - public IntPtr ResolvePath( IntPtr? gameObject, ModCollection collection, IntPtr path ) + public IntPtr ResolvePath( IntPtr gameObject, ModCollection collection, IntPtr path ) { if( path == IntPtr.Zero ) { @@ -94,7 +94,7 @@ public unsafe partial class PathResolver } // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - public void SetCollection( IntPtr? gameObject, Utf8String path, ModCollection collection ) + public void SetCollection( IntPtr gameObject, Utf8String path, ModCollection collection ) { if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 2b0bd8aa..adc3dfeb 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -227,7 +227,7 @@ public partial class PathResolver // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePath( IntPtr drawObject, IntPtr path ) - => _parent._paths.ResolvePath( (IntPtr?)FindParent( drawObject, out _), FindParent( drawObject, out var collection ) == null + => _parent._paths.ResolvePath( (IntPtr?)FindParent( drawObject, out _) ?? IntPtr.Zero, FindParent( drawObject, out var collection ) == null ? Penumbra.CollectionManager.Default : collection.ModCollection, path ); @@ -251,7 +251,7 @@ public partial class PathResolver } parent = FindParent( parentObject, out collection ); - return _parent._paths.ResolvePath( (IntPtr?)parent, parent == null + return _parent._paths.ResolvePath( (IntPtr?)parent ?? IntPtr.Zero, parent == null ? Penumbra.CollectionManager.Default : collection.ModCollection, path ); } From d12a3dd152cfeb08ce79d73c53ba044022803103 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Sep 2022 13:30:07 +0200 Subject: [PATCH 0454/2451] Rework ResolveData. --- Penumbra/Api/PenumbraApi.cs | 11 +-- Penumbra/Collections/LinkedModCollection.cs | 26 ------- Penumbra/Collections/ResolveData.cs | 46 ++++++++++++ .../Interop/Loader/ResourceLoader.Debug.cs | 10 +-- .../Loader/ResourceLoader.Replacement.cs | 8 +-- Penumbra/Interop/Loader/ResourceLoader.cs | 6 +- .../Resolver/PathResolver.AnimationState.cs | 71 ++++++++++--------- .../Resolver/PathResolver.DrawObjectState.cs | 22 +++--- .../Resolver/PathResolver.Identification.cs | 25 ++++--- .../Interop/Resolver/PathResolver.Material.cs | 34 ++++----- .../Interop/Resolver/PathResolver.Meta.cs | 50 ++++++------- .../Resolver/PathResolver.PathState.cs | 12 ++-- .../Resolver/PathResolver.ResolverHooks.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.cs | 46 ++++++------ 14 files changed, 199 insertions(+), 170 deletions(-) delete mode 100644 Penumbra/Collections/LinkedModCollection.cs create mode 100644 Penumbra/Collections/ResolveData.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 0546ac53..f6292d82 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -67,12 +67,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi } Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; - Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; + Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; } public unsafe void Dispose() { - Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; + Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; _penumbra = null; _lumina = null; @@ -93,10 +93,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.Config.ModDirectory; } - private unsafe void OnResourceLoaded( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, LinkedModCollection? resolveData ) + private unsafe void OnResourceLoaded( ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, + ResolveData resolveData ) { - if( resolveData == null ) return; - GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), manipulatedPath?.ToString() ?? originalPath.ToString() ); + GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString() ); } public event Action< string, bool >? ModDirectoryChanged diff --git a/Penumbra/Collections/LinkedModCollection.cs b/Penumbra/Collections/LinkedModCollection.cs deleted file mode 100644 index 93575544..00000000 --- a/Penumbra/Collections/LinkedModCollection.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using FFXIVClientStructs.FFXIV.Client.Game.Object; - -namespace Penumbra.Collections; - -public class LinkedModCollection -{ - private IntPtr? _associatedGameObject; - public IntPtr AssociatedGameObject = IntPtr.Zero; - public ModCollection ModCollection; - - public LinkedModCollection(ModCollection modCollection) - { - ModCollection = modCollection; - } - - public LinkedModCollection(IntPtr gameObject, ModCollection collection) - { - AssociatedGameObject = gameObject; - ModCollection = collection; - } - - public unsafe LinkedModCollection(GameObject* gameObject, ModCollection collection) : this((IntPtr)gameObject, collection) - { - } -} diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs new file mode 100644 index 00000000..348b06a9 --- /dev/null +++ b/Penumbra/Collections/ResolveData.cs @@ -0,0 +1,46 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Game.Object; + +namespace Penumbra.Collections; + +public readonly struct ResolveData +{ + public static readonly ResolveData Invalid = new(ModCollection.Empty); + + public readonly ModCollection ModCollection; + public readonly IntPtr AssociatedGameObject; + + public bool Valid + => ModCollection != ModCollection.Empty; + + public ResolveData() + { + ModCollection = ModCollection.Empty; + AssociatedGameObject = IntPtr.Zero; + } + + public ResolveData( ModCollection collection, IntPtr gameObject ) + { + ModCollection = collection; + AssociatedGameObject = gameObject; + } + + public ResolveData( ModCollection collection ) + : this( collection, IntPtr.Zero ) + { } + + public override string ToString() + => ModCollection.Name; +} + +public static class ResolveDataExtensions +{ + public static ResolveData ToResolveData( this ModCollection collection ) + => new(collection); + + public static ResolveData ToResolveData( this ModCollection collection, IntPtr ptr ) + => new(collection, ptr); + + public static unsafe ResolveData ToResolveData( this ModCollection collection, void* ptr ) + => new(collection, ( IntPtr )ptr); +} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 4e162bca..ada91c0a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -48,7 +48,7 @@ public unsafe partial class ResourceLoader public Utf8GamePath OriginalPath; public FullPath ManipulatedPath; public ResourceCategory Category; - public object? ResolverInfo; + public ResolveData ResolverInfo; public ResourceType Extension; } @@ -59,18 +59,18 @@ public unsafe partial class ResourceLoader public void EnableDebug() { - _decRefHook?.Enable(); + _decRefHook.Enable(); ResourceLoaded += AddModifiedDebugInfo; } public void DisableDebug() { - _decRefHook?.Disable(); + _decRefHook.Disable(); ResourceLoaded -= AddModifiedDebugInfo; } private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - LinkedModCollection? resolverInfo ) + ResolveData resolverInfo ) { if( manipulatedPath == null || manipulatedPath.Value.Crc64 == 0 ) { @@ -244,7 +244,7 @@ public unsafe partial class ResourceLoader private static void LogPath( Utf8GamePath path, bool synchronous ) => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, LinkedModCollection? _ ) + private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index a9a996f6..f14f19b5 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -114,18 +114,18 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. - public static (FullPath?, LinkedModCollection?) DefaultResolver( Utf8GamePath path ) + public static (FullPath?, ResolveData) DefaultResolver( Utf8GamePath path ) { var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); - return ( resolved, new LinkedModCollection( Penumbra.CollectionManager.Default ) ); + return ( resolved, Penumbra.CollectionManager.Default.ToResolveData() ); } // Try all resolve path subscribers or use the default replacer. - private (FullPath?, LinkedModCollection?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + private (FullPath?, ResolveData) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) { if( !DoReplacements || _incMode.Value ) { - return ( null, null ); + return ( null, ResolveData.Invalid ); } path = path.ToLower(); diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 8f23d71f..44d55214 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -117,9 +117,9 @@ public unsafe partial class ResourceLoader : IDisposable // Event fired whenever a resource is returned. // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. - // resolveData is additional data returned by the current ResolvePath function and is user-defined. + // resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object. public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, - LinkedModCollection? resolveData ); + ResolveData resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; @@ -134,7 +134,7 @@ public unsafe partial class ResourceLoader : IDisposable // Resolving goes through all subscribed functions in arbitrary order until one returns true, // or uses default resolving if none return true. public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash, - out (FullPath?, LinkedModCollection?) ret ); + out (FullPath?, ResolveData) ret ); public event ResolvePathDelegate? ResolvePathCustomization; diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index d6106a87..7211d109 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -15,8 +15,8 @@ public unsafe partial class PathResolver { private readonly DrawObjectState _drawObjectState; - private LinkedModCollection? _animationLoadCollection; - private LinkedModCollection? _lastAvfxCollection; + private ResolveData _animationLoadData = ResolveData.Invalid; + private ResolveData _lastAvfxData = ResolveData.Invalid; public AnimationState( DrawObjectState drawObjectState ) { @@ -24,46 +24,48 @@ public unsafe partial class PathResolver SignatureHelper.Initialise( this ); } - public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out LinkedModCollection? collection ) + public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData ) { switch( type ) { case ResourceType.Tmb: case ResourceType.Pap: case ResourceType.Scd: - if( _animationLoadCollection != null ) + if( _animationLoadData.Valid ) { - collection = _animationLoadCollection; + resolveData = _animationLoadData; return true; } break; case ResourceType.Avfx: - _lastAvfxCollection = _animationLoadCollection ?? new LinkedModCollection(Penumbra.CollectionManager.Default); - if( _animationLoadCollection != null ) + _lastAvfxData = _animationLoadData.Valid + ? _animationLoadData + : Penumbra.CollectionManager.Default.ToResolveData(); + if( _animationLoadData.Valid ) { - collection = _animationLoadCollection; + resolveData = _animationLoadData; return true; } break; case ResourceType.Atex: - if( _lastAvfxCollection != null ) + if( _lastAvfxData.Valid ) { - collection = _lastAvfxCollection; + resolveData = _lastAvfxData; return true; } - if( _animationLoadCollection != null ) + if( _animationLoadData.Valid ) { - collection = _animationLoadCollection; + resolveData = _animationLoadData; return true; } break; } - collection = null; + resolveData = ResolveData.Invalid; return false; } @@ -107,7 +109,7 @@ public unsafe partial class PathResolver private ulong LoadTimelineResourcesDetour( IntPtr timeline ) { ulong ret; - var old = _animationLoadCollection; + var old = _animationLoadData; try { if( timeline != IntPtr.Zero ) @@ -117,11 +119,11 @@ public unsafe partial class PathResolver if( idx >= 0 && idx < Dalamud.Objects.Length ) { var obj = Dalamud.Objects[ idx ]; - _animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null; + _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : ResolveData.Invalid; } else { - _animationLoadCollection = null; + _animationLoadData = ResolveData.Invalid; } } } @@ -130,7 +132,7 @@ public unsafe partial class PathResolver ret = _loadTimelineResourcesHook.Original( timeline ); } - _animationLoadCollection = old; + _animationLoadData = old; return ret; } @@ -145,11 +147,14 @@ public unsafe partial class PathResolver private void CharacterBaseLoadAnimationDetour( IntPtr drawObject ) { - var last = _animationLoadCollection; - _animationLoadCollection = _drawObjectState.LastCreatedCollection - ?? ( FindParent( drawObject, out var collection ) != null ? collection : new LinkedModCollection(Penumbra.CollectionManager.Default) ); + var last = _animationLoadData; + _animationLoadData = _drawObjectState.LastCreatedCollection.Valid + ? _drawObjectState.LastCreatedCollection + : FindParent( drawObject, out var collection ) != null + ? collection + : Penumbra.CollectionManager.Default.ToResolveData(); _characterBaseLoadAnimationHook.Original( drawObject ); - _animationLoadCollection = last; + _animationLoadData = last; } @@ -160,10 +165,10 @@ public unsafe partial class PathResolver private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) { - var last = _animationLoadCollection; - _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + var last = _animationLoadData; + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); - _animationLoadCollection = last; + _animationLoadData = last; return ret; } @@ -177,18 +182,18 @@ public unsafe partial class PathResolver private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) { var timelinePtr = a1 + 0x50; - var last = _animationLoadCollection; + var last = _animationLoadData; if( timelinePtr != IntPtr.Zero ) { var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) { - _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); } } _loadSomePapHook.Original( a1, a2, a3, a4 ); - _animationLoadCollection = last; + _animationLoadData = last; } // Seems to load character actions when zoning or changing class, maybe. @@ -197,10 +202,10 @@ public unsafe partial class PathResolver private void SomeActionLoadDetour( IntPtr gameObject ) { - var last = _animationLoadCollection; - _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + var last = _animationLoadData; + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); _someActionLoadHook.Original( gameObject ); - _animationLoadCollection = last; + _animationLoadData = last; } [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )] @@ -208,11 +213,11 @@ public unsafe partial class PathResolver private void SomeOtherAvfxDetour( IntPtr unk ) { - var last = _animationLoadCollection; + var last = _animationLoadData; var gameObject = ( GameObject* )( unk - 0x8D0 ); - _animationLoadCollection = IdentifyCollection( gameObject ); + _animationLoadData = IdentifyCollection( gameObject ); _someOtherAvfxHook.Original( unk ); - _animationLoadCollection = last; + _animationLoadData = last; } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 1e6ed477..7c9690fc 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -20,13 +20,13 @@ public unsafe partial class PathResolver public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; - public IEnumerable< KeyValuePair< IntPtr, (LinkedModCollection, int) > > DrawObjects + public IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjects => _drawObjectToObject; public int Count => _drawObjectToObject.Count; - public bool TryGetValue( IntPtr drawObject, out (LinkedModCollection, int) value, out GameObject* gameObject ) + public bool TryGetValue( IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject ) { gameObject = null; if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) @@ -40,7 +40,7 @@ public unsafe partial class PathResolver // Set and update a parent object if it exists and a last game object is set. - public LinkedModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) + public ResolveData CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject ) { if( parentObject == IntPtr.Zero && LastGameObject != null ) { @@ -49,26 +49,26 @@ public unsafe partial class PathResolver return collection; } - return null; + return ResolveData.Invalid; } - public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out LinkedModCollection? collection ) + public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData ) { if( type == ResourceType.Tex - && LastCreatedCollection != null + && LastCreatedCollection.Valid && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) { - collection = LastCreatedCollection!; + resolveData = LastCreatedCollection; return true; } - collection = null; + resolveData = ResolveData.Invalid; return false; } - public LinkedModCollection? LastCreatedCollection + public ResolveData LastCreatedCollection => _lastCreatedCollection; public GameObject* LastGameObject { get; private set; } @@ -124,8 +124,8 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary< IntPtr, (LinkedModCollection, int) > _drawObjectToObject = new(); - private LinkedModCollection? _lastCreatedCollection; + private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); + private ResolveData _lastCreatedCollection = ResolveData.Invalid; // Keep track of created DrawObjects that are CharacterBase, // and use the last game object that called EnableDraw to link them. diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 9b0586dd..39efe25b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -40,7 +40,7 @@ public unsafe partial class PathResolver return null; } - var ui = ( AtkUnitBase* )addon; + var ui = ( AtkUnitBase* )addon; var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 2u : 6u; var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); @@ -61,7 +61,8 @@ public unsafe partial class PathResolver { return null; } - var data = *( byte** )( (byte*) agent + 0x28 ); + + var data = *( byte** )( ( byte* )agent + 0x28 ); if( data == null ) { return null; @@ -139,11 +140,11 @@ public unsafe partial class PathResolver } // Identify the correct collection for a GameObject by index and name. - private static LinkedModCollection IdentifyCollection( GameObject* gameObject ) + private static ResolveData IdentifyCollection( GameObject* gameObject ) { if( gameObject == null ) { - return new LinkedModCollection(Penumbra.CollectionManager.Default); + return new ResolveData( Penumbra.CollectionManager.Default ); } try @@ -153,8 +154,9 @@ public unsafe partial class PathResolver // Actors are also not named. So use Yourself > Players > Racial > Default. if( !Dalamud.ClientState.IsLoggedIn ) { - return new LinkedModCollection(gameObject, Penumbra.CollectionManager.ByType( CollectionType.Yourself ) - ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default )); + var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) + ?? ( CollectionByActor( string.Empty, gameObject, out var c ) ? c : Penumbra.CollectionManager.Default ); + return collection.ToResolveData( gameObject ); } else { @@ -163,7 +165,7 @@ public unsafe partial class PathResolver && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc && gameObject->DataID is 1011832 or 1011021 ) // cf. "E8 ?? ?? ?? ?? 0F B6 F8 88 45", male or female retainer { - return new LinkedModCollection((IntPtr)gameObject, Penumbra.CollectionManager.Default); + return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); } string? actorName = null; @@ -174,7 +176,7 @@ public unsafe partial class PathResolver if( actorName.Length > 0 && CollectionByActorName( actorName, out var actorCollection ) ) { - return new LinkedModCollection(gameObject, actorCollection); + return actorCollection.ToResolveData( gameObject ); } } @@ -193,17 +195,18 @@ public unsafe partial class PathResolver ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); // First check temporary character collections, then the own configuration, then special collections. - return new LinkedModCollection(gameObject, CollectionByActorName( actualName, out var c ) + var collection = CollectionByActorName( actualName, out var c ) ? c : CollectionByActor( actualName, gameObject, out c ) ? c - : Penumbra.CollectionManager.Default); + : Penumbra.CollectionManager.Default; + return collection.ToResolveData( gameObject ); } } catch( Exception e ) { PluginLog.Error( $"Error identifying collection:\n{e}" ); - return new LinkedModCollection(gameObject, Penumbra.CollectionManager.Default); + return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index b4a6c494..6e81dbac 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -21,7 +21,7 @@ public unsafe partial class PathResolver { private readonly PathState _paths; - private LinkedModCollection? _mtrlCollection; + private ResolveData _mtrlData = ResolveData.Invalid; public MaterialState( PathState paths ) { @@ -30,30 +30,30 @@ public unsafe partial class PathResolver } // Check specifically for shpk and tex files whether we are currently in a material load. - public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out LinkedModCollection? collection ) + public bool HandleSubFiles( ResourceType type, out ResolveData collection ) { - if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) + if( _mtrlData.Valid && type is ResourceType.Tex or ResourceType.Shpk ) { - collection = _mtrlCollection; + collection = _mtrlData; return true; } - collection = null; + collection = ResolveData.Invalid; return false; } // Materials need to be set per collection so they can load their textures independently from each other. - public static void HandleCollection( LinkedModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, LinkedModCollection?) data ) + public static void HandleCollection( ResolveData resolveData, string path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, ResolveData) data ) { if( nonDefault && type == ResourceType.Mtrl ) { - var fullPath = new FullPath( $"|{collection.ModCollection.Name}_{collection.ModCollection.ChangeCounter}|{path}" ); - data = ( fullPath, collection ); + var fullPath = new FullPath( $"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}" ); + data = ( fullPath, resolveData ); } else { - data = ( resolved, collection ); + data = ( resolved, resolveData ); } } @@ -74,8 +74,8 @@ public unsafe partial class PathResolver public void Dispose() { Disable(); - _loadMtrlShpkHook?.Dispose(); - _loadMtrlTexHook?.Dispose(); + _loadMtrlShpkHook.Dispose(); + _loadMtrlTexHook.Dispose(); } // We need to set the correct collection for the actual material path that is loaded @@ -97,9 +97,9 @@ public unsafe partial class PathResolver #if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); #endif - + var objFromObjTable = Dalamud.Objects.FirstOrDefault( f => f.Name.TextValue == name ); - IntPtr gameObjAddr = objFromObjTable?.Address ?? IntPtr.Zero; + var gameObjAddr = objFromObjTable?.Address ?? IntPtr.Zero; _paths.SetCollection( gameObjAddr, path, collection ); } else @@ -127,7 +127,7 @@ public unsafe partial class PathResolver { LoadMtrlHelper( mtrlResourceHandle ); var ret = _loadMtrlTexHook.Original( mtrlResourceHandle ); - _mtrlCollection = null; + _mtrlData = ResolveData.Invalid; return ret; } @@ -139,7 +139,7 @@ public unsafe partial class PathResolver { LoadMtrlHelper( mtrlResourceHandle ); var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle ); - _mtrlCollection = null; + _mtrlData = ResolveData.Invalid; return ret; } @@ -152,7 +152,7 @@ public unsafe partial class PathResolver var mtrl = ( MtrlResource* )mtrlResourceHandle; var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - _mtrlCollection = _paths.TryGetValue( mtrlPath, out var c ) ? c : null; + _mtrlData = _paths.TryGetValue( mtrlPath, out var c ) ? c : ResolveData.Invalid; } } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index a2699544..e305b6db 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -79,8 +79,8 @@ public unsafe partial class PathResolver private void OnModelLoadCompleteDetour( IntPtr drawObject ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var collection = GetResolveData( drawObject ); + if( collection.Valid ) { using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); @@ -106,8 +106,8 @@ public unsafe partial class PathResolver return; } - var collection = GetCollection( drawObject ); - if( collection != null ) + var collection = GetResolveData( drawObject ); + if( collection.Valid ) { using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); @@ -212,24 +212,24 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Eqp ); } - public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject ) + public static MetaChanger ChangeEqp( PathResolver _, IntPtr drawObject ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var resolveData = GetResolveData( drawObject ); + if( resolveData.Valid ) { - return ChangeEqp( collection.ModCollection ); + return ChangeEqp( resolveData.ModCollection ); } return new MetaChanger( MetaManipulation.Type.Unknown ); } // We only need to change anything if it is actually equipment here. - public static MetaChanger ChangeEqdp( PathResolver resolver, IntPtr drawObject, uint modelType ) + public static MetaChanger ChangeEqdp( PathResolver _, IntPtr drawObject, uint modelType ) { if( modelType < 10 ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var collection = GetResolveData( drawObject ); + if( collection.Valid ) { return ChangeEqdp( collection.ModCollection ); } @@ -246,10 +246,10 @@ public unsafe partial class PathResolver public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var resolveData = GetResolveData( drawObject ); + if( resolveData.Valid ) { - collection.ModCollection.SetGmpFiles(); + resolveData.ModCollection.SetGmpFiles(); return new MetaChanger( MetaManipulation.Type.Gmp ); } @@ -258,30 +258,30 @@ public unsafe partial class PathResolver public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var resolveData = GetResolveData( drawObject ); + if( resolveData.Valid ) { - collection.ModCollection.SetEstFiles(); + resolveData.ModCollection.SetEstFiles(); return new MetaChanger( MetaManipulation.Type.Est ); } return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( GameObject* gameObject, out LinkedModCollection? collection ) + public static MetaChanger ChangeCmp( GameObject* gameObject, out ResolveData resolveData ) { if( gameObject != null ) { - collection = IdentifyCollection( gameObject ); - if( collection.ModCollection != Penumbra.CollectionManager.Default && collection.ModCollection.HasCache ) + resolveData = IdentifyCollection( gameObject ); + if( resolveData.ModCollection != Penumbra.CollectionManager.Default && resolveData.ModCollection.HasCache ) { - collection.ModCollection.SetCmpFiles(); + resolveData.ModCollection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } } else { - collection = null; + resolveData = ResolveData.Invalid; } return new MetaChanger( MetaManipulation.Type.Unknown ); @@ -289,10 +289,10 @@ public unsafe partial class PathResolver public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) { - var collection = GetCollection( drawObject ); - if( collection != null ) + var resolveData = GetResolveData( drawObject ); + if( resolveData.Valid ) { - collection.ModCollection.SetCmpFiles(); + resolveData.ModCollection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index 6a1d745a..f5c11771 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -32,7 +32,7 @@ public unsafe partial class PathResolver private readonly ResolverHooks _monster; // This map links files to their corresponding collection, if it is non-default. - private readonly ConcurrentDictionary< Utf8String, LinkedModCollection > _pathCollections = new(); + private readonly ConcurrentDictionary< Utf8String, ResolveData > _pathCollections = new(); public PathState( PathResolver parent ) { @@ -70,13 +70,13 @@ public unsafe partial class PathResolver public int Count => _pathCollections.Count; - public IEnumerable< KeyValuePair< Utf8String, LinkedModCollection > > Paths + public IEnumerable< KeyValuePair< Utf8String, ResolveData > > Paths => _pathCollections; - public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out LinkedModCollection? collection ) + public bool TryGetValue( Utf8String path, out ResolveData collection ) => _pathCollections.TryGetValue( path, out collection ); - public bool Consume( Utf8String path, [NotNullWhen( true )] out LinkedModCollection? collection ) + public bool Consume( Utf8String path, out ResolveData collection ) => _pathCollections.TryRemove( path, out collection ); // Just add or remove the resolved path. @@ -98,11 +98,11 @@ public unsafe partial class PathResolver { if( _pathCollections.ContainsKey( path ) || path.IsOwned ) { - _pathCollections[ path ] = new LinkedModCollection(gameObject, collection); + _pathCollections[ path ] = collection.ToResolveData( gameObject ); } else { - _pathCollections[ path.Clone() ] = new LinkedModCollection(gameObject, collection); + _pathCollections[ path.Clone() ] = collection.ToResolveData( gameObject ); } } } diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index adc3dfeb..02729c07 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -245,7 +245,7 @@ public partial class PathResolver var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); - if( parentCollection != null ) + if( parentCollection.Valid ) { return _parent._paths.ResolvePath( (IntPtr)FindParent(parentObject, out _), parentCollection.ModCollection, path ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 76164318..06536b30 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -40,7 +40,7 @@ public partial class PathResolver : IDisposable } // The modified resolver that handles game path resolving. - private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, LinkedModCollection?) data ) + private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data ) { // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load, @@ -48,23 +48,23 @@ public partial class PathResolver : IDisposable // If not use the default collection. // We can remove paths after they have actually been loaded. // A potential next request will add the path anew. - var nonDefault = _materials.HandleSubFiles( type, out var collection ) - || _paths.Consume( gamePath.Path, out collection ) - || _animations.HandleFiles( type, gamePath, out collection ) - || DrawObjects.HandleDecalFile( type, gamePath, out collection ); - if( !nonDefault || collection == null ) + var nonDefault = _materials.HandleSubFiles( type, out var resolveData ) + || _paths.Consume( gamePath.Path, out resolveData ) + || _animations.HandleFiles( type, gamePath, out resolveData ) + || DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); + if( !nonDefault || !resolveData.Valid ) { - collection = new LinkedModCollection(Penumbra.CollectionManager.Default); + resolveData = Penumbra.CollectionManager.Default.ToResolveData(); } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = collection.ModCollection.ResolvePath( gamePath ); + var resolved = resolveData.ModCollection.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. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; - MaterialState.HandleCollection( collection, path, nonDefault, type, resolved, out data ); + MaterialState.HandleCollection( resolveData, path, nonDefault, type, resolved, out data ); return true; } @@ -117,50 +117,50 @@ public partial class PathResolver : IDisposable _materials.Dispose(); } - public static unsafe (IntPtr, LinkedModCollection) IdentifyDrawObject( IntPtr drawObject ) + public static unsafe (IntPtr, ResolveData) IdentifyDrawObject( IntPtr drawObject ) { - var parent = FindParent( drawObject, out var collection ); - return ( ( IntPtr )parent, collection ); + var parent = FindParent( drawObject, out var resolveData ); + return ( ( IntPtr )parent, resolveData ); } public int CutsceneActor( int idx ) => Cutscenes.GetParentIndex( idx ); // Use the stored information to find the GameObject and Collection linked to a DrawObject. - public static unsafe GameObject* FindParent( IntPtr drawObject, out LinkedModCollection collection ) + public static unsafe GameObject* FindParent( IntPtr drawObject, out ResolveData resolveData ) { if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) { - collection = data.Item1; + resolveData = data.Item1; return gameObject; } if( DrawObjects.LastGameObject != null && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) { - collection = IdentifyCollection( DrawObjects.LastGameObject ); + resolveData = IdentifyCollection( DrawObjects.LastGameObject ); return DrawObjects.LastGameObject; } - collection = IdentifyCollection( null ); + resolveData = IdentifyCollection( null ); return null; } - private static unsafe LinkedModCollection? GetCollection( IntPtr drawObject ) + private static unsafe ResolveData GetResolveData( IntPtr drawObject ) { - var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection.ModCollection == Penumbra.CollectionManager.Default ) + var parent = FindParent( drawObject, out var resolveData ); + if( parent == null || resolveData.ModCollection == Penumbra.CollectionManager.Default ) { - return null; + return ResolveData.Invalid; } - return collection.ModCollection.HasCache ? collection : null; + return resolveData.ModCollection.HasCache ? resolveData : ResolveData.Invalid; } - internal IEnumerable< KeyValuePair< Utf8String, LinkedModCollection > > PathCollections + internal IEnumerable< KeyValuePair< Utf8String, ResolveData > > PathCollections => _paths.Paths; - internal IEnumerable< KeyValuePair< IntPtr, (LinkedModCollection, int) > > DrawObjectMap + internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap => DrawObjects.DrawObjects; internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors From 0f35dd69f9e018ffe363dd2d6d4b4c06621de3d2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Sep 2022 14:01:12 +0200 Subject: [PATCH 0455/2451] Add IPC test, optimize tester a little, only call event when game object available. --- Penumbra/Api/IPenumbraApi.cs | 4 +- Penumbra/Api/IpcTester.cs | 88 +++++++++++++++++++++------- Penumbra/Api/PenumbraApi.cs | 7 ++- Penumbra/Api/PenumbraIpc.cs | 23 ++++---- Penumbra/UI/ConfigWindow.DebugTab.cs | 1 + 5 files changed, 87 insertions(+), 36 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 57e13baa..05b56c80 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -80,7 +80,9 @@ public interface IPenumbraApi : IPenumbraApiBase // so you can apply flag changes after finishing. public event CreatedCharacterBaseDelegate? CreatedCharacterBase; - public event GameObjectResourceResolvedDelegate GameObjectResourceResolved; + // Triggered whenever a resource is redirected by Penumbra for a specific, identified game object. + // Does not trigger if the resource is not requested for a known game object. + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index c251eb35..f5140db4 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -33,9 +33,12 @@ public class IpcTester : IDisposable private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreating; private readonly ICallGateSubscriber< IntPtr, string, IntPtr, object? > _characterBaseCreated; + private readonly ICallGateSubscriber< IntPtr, string, string, object? > _gameObjectResourcePathResolved; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); + private bool _subscribed = false; + public IpcTester( DalamudPluginInterface pi, PenumbraIpc ipc ) { @@ -51,31 +54,51 @@ public class IpcTester : IDisposable _characterBaseCreating = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); _characterBaseCreated = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, object? >( PenumbraIpc.LabelProviderCreatedCharacterBase ); - _initialized.Subscribe( AddInitialized ); - _disposed.Subscribe( AddDisposed ); - _redrawn.Subscribe( SetLastRedrawn ); - _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); - _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); - _settingChanged.Subscribe( UpdateLastModSetting ); - _characterBaseCreating.Subscribe( UpdateLastCreated ); - _characterBaseCreated.Subscribe( UpdateLastCreated2 ); - _modDirectoryChanged.Subscribe( UpdateModDirectoryChanged ); + _gameObjectResourcePathResolved = + _pi.GetIpcSubscriber< IntPtr, string, string, object? >( PenumbraIpc.LabelProviderGameObjectResourcePathResolved ); + } + + private void SubscribeEvents() + { + if( !_subscribed ) + { + + _initialized.Subscribe( AddInitialized ); + _disposed.Subscribe( AddDisposed ); + _redrawn.Subscribe( SetLastRedrawn ); + _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); + _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); + _settingChanged.Subscribe( UpdateLastModSetting ); + _characterBaseCreating.Subscribe( UpdateLastCreated ); + _characterBaseCreated.Subscribe( UpdateLastCreated2 ); + _modDirectoryChanged.Subscribe( UpdateModDirectoryChanged ); + _gameObjectResourcePathResolved.Subscribe( UpdateGameObjectResourcePath ); + _subscribed = true; + } + } + + public void UnsubscribeEvents() + { + if( _subscribed ) + { + _initialized.Unsubscribe( AddInitialized ); + _disposed.Unsubscribe( AddDisposed ); + _redrawn.Subscribe( SetLastRedrawn ); + _tooltip?.Unsubscribe( AddedTooltip ); + _click?.Unsubscribe( AddedClick ); + _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); + _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); + _settingChanged.Unsubscribe( UpdateLastModSetting ); + _characterBaseCreating.Unsubscribe( UpdateLastCreated ); + _characterBaseCreated.Unsubscribe( UpdateLastCreated2 ); + _modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged ); + _gameObjectResourcePathResolved.Unsubscribe( UpdateGameObjectResourcePath ); + _subscribed = false; + } } public void Dispose() - { - _initialized.Unsubscribe( AddInitialized ); - _disposed.Unsubscribe( AddDisposed ); - _redrawn.Subscribe( SetLastRedrawn ); - _tooltip?.Unsubscribe( AddedTooltip ); - _click?.Unsubscribe( AddedClick ); - _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); - _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); - _settingChanged.Unsubscribe( UpdateLastModSetting ); - _characterBaseCreating.Unsubscribe( UpdateLastCreated ); - _characterBaseCreated.Unsubscribe( UpdateLastCreated2 ); - _modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged ); - } + => UnsubscribeEvents(); private void AddInitialized() => _initializedList.Add( DateTimeOffset.UtcNow ); @@ -87,6 +110,7 @@ public class IpcTester : IDisposable { try { + SubscribeEvents(); DrawAvailable(); DrawGeneral(); DrawResolve(); @@ -224,6 +248,10 @@ public class IpcTester : IDisposable private string _lastCreatedGameObjectName = string.Empty; private IntPtr _lastCreatedDrawObject = IntPtr.Zero; private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; + private string _lastResolvedGamePath = string.Empty; + private string _lastResolvedFullPath = string.Empty; + private string _lastResolvedObject = string.Empty; + private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { @@ -241,6 +269,15 @@ public class IpcTester : IDisposable _lastCreatedDrawObject = drawObject; } + private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) + { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastResolvedObject = obj != null ? new Utf8String( obj->GetName() ).ToString() : "Unknown"; + _lastResolvedGamePath = gamePath; + _lastResolvedFullPath = fullPath; + _lastResolvedGamePathTime = DateTimeOffset.Now; + } + private void DrawResolve() { using var _ = ImRaii.TreeNode( "Resolve IPC" ); @@ -336,6 +373,13 @@ public class IpcTester : IDisposable ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); } + + DrawIntro( PenumbraIpc.LabelProviderGameObjectResourcePathResolved, "Last GamePath resolved" ); + if( _lastResolvedGamePathTime < DateTimeOffset.Now ) + { + ImGui.TextUnformatted( + $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}" ); + } } private string _redrawName = string.Empty; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index f6292d82..982d5c22 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -96,8 +96,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi private unsafe void OnResourceLoaded( ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData ) { - GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString() ); + if( resolveData.AssociatedGameObject != IntPtr.Zero ) + { + GameObjectResourceResolved?.Invoke( resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString() ); + } } public event Action< string, bool >? ModDirectoryChanged diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index a38aecce..f7c6fd36 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -274,15 +274,15 @@ public partial class PenumbraIpc public partial class PenumbraIpc { - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; - public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex"; - public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; - public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; - public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase"; + public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; + public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; + public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; + public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex"; + public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; + public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; + public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; + public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase"; public const string LabelProviderGameObjectResourcePathResolved = "Penumbra.GameObjectResourcePathResolved"; internal ICallGateProvider< string, string >? ProviderResolveDefault; @@ -294,7 +294,7 @@ public partial class PenumbraIpc internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; internal ICallGateProvider< IntPtr, string, IntPtr, object? >? ProviderCreatedCharacterBase; - internal ICallGateProvider? ProviderGameObjectResourcePathResolved; + internal ICallGateProvider< IntPtr, string, string, object? >? ProviderGameObjectResourcePathResolved; private void InitializeResolveProviders( DalamudPluginInterface pi ) { @@ -392,7 +392,8 @@ public partial class PenumbraIpc try { - ProviderGameObjectResourcePathResolved = pi.GetIpcProvider( LabelProviderGameObjectResourcePathResolved ); + ProviderGameObjectResourcePathResolved = + pi.GetIpcProvider< IntPtr, string, string, object? >( LabelProviderGameObjectResourcePathResolved ); Api.GameObjectResourceResolved += GameObjectResourceResolvdedEvent; } catch( Exception e ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 59ea02d2..68169211 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -422,6 +422,7 @@ public partial class ConfigWindow { if( !ImGui.CollapsingHeader( "IPC" ) ) { + _window._penumbra.Ipc.Tester.UnsubscribeEvents(); return; } From 55de29e0ac1d228e596d411b88f0eefdc1bfd97a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Sep 2022 14:09:50 +0200 Subject: [PATCH 0456/2451] Dispose fix and typo. --- Penumbra/Api/PenumbraIpc.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index f7c6fd36..fa9ada66 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -394,7 +394,7 @@ public partial class PenumbraIpc { ProviderGameObjectResourcePathResolved = pi.GetIpcProvider< IntPtr, string, string, object? >( LabelProviderGameObjectResourcePathResolved ); - Api.GameObjectResourceResolved += GameObjectResourceResolvdedEvent; + Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent; } catch( Exception e ) { @@ -402,7 +402,7 @@ public partial class PenumbraIpc } } - private void GameObjectResourceResolvdedEvent( IntPtr gameObject, string gamePath, string localPath ) + private void GameObjectResourceResolvedEvent( IntPtr gameObject, string gamePath, string localPath ) { ProviderGameObjectResourcePathResolved?.SendMessage( gameObject, gamePath, localPath ); } @@ -415,8 +415,9 @@ public partial class PenumbraIpc ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); ProviderReverseResolvePathPlayer?.UnregisterFunc(); - Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; - Api.CreatedCharacterBase -= CreatedCharacterBaseEvent; + Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; + Api.CreatedCharacterBase -= CreatedCharacterBaseEvent; + Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent; } private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData ) From 4beded8a7a5ce1a964f62c588b0b43b3d01756b6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Sep 2022 14:43:14 +0200 Subject: [PATCH 0457/2451] Make Invalid ResolveData more definitive. --- Penumbra/Collections/ResolveData.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 348b06a9..6fb60d55 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -5,23 +5,27 @@ namespace Penumbra.Collections; public readonly struct ResolveData { - public static readonly ResolveData Invalid = new(ModCollection.Empty); + public static readonly ResolveData Invalid = new(); - public readonly ModCollection ModCollection; - public readonly IntPtr AssociatedGameObject; + private readonly ModCollection? _modCollection; + + public ModCollection ModCollection + => _modCollection ?? ModCollection.Empty; + + public readonly IntPtr AssociatedGameObject; public bool Valid - => ModCollection != ModCollection.Empty; + => _modCollection != null; public ResolveData() { - ModCollection = ModCollection.Empty; + _modCollection = null!; AssociatedGameObject = IntPtr.Zero; } public ResolveData( ModCollection collection, IntPtr gameObject ) { - ModCollection = collection; + _modCollection = collection; AssociatedGameObject = gameObject; } From 1fe334e33a234145c53a8ba73ecf7c0fc9192192 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 6 Sep 2022 22:34:26 +0200 Subject: [PATCH 0458/2451] Add Changelog, prevent UI category files from deduplicating, revert ui hash change. --- OtterGui | 2 +- Penumbra/Collections/ResolveData.cs | 1 - Penumbra/Configuration.cs | 4 +++ .../Loader/ResourceLoader.Replacement.cs | 24 ++++++-------- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 7 ++++- Penumbra/Penumbra.cs | 1 + Penumbra/UI/ConfigWindow.Changelog.cs | 31 +++++++++++++++++++ Penumbra/UI/ConfigWindow.SettingsTab.cs | 6 ++++ 8 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 Penumbra/UI/ConfigWindow.Changelog.cs diff --git a/OtterGui b/OtterGui index 88bf2218..9ec5e2ad 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 88bf221852d4a1ac26f5ffbfb5e497220aef75c4 +Subproject commit 9ec5e2ad2f2d35d62c2ac7c300b914fffbda2191 diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 6fb60d55..722675fd 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,5 +1,4 @@ using System; -using FFXIVClientStructs.FFXIV.Client.Game.Object; namespace Penumbra.Collections; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 7f2e7848..0e95a7f6 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -8,8 +8,10 @@ using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Widgets; using Penumbra.Import; using Penumbra.Mods; +using Penumbra.UI; using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -20,6 +22,8 @@ public partial class Configuration : IPluginConfiguration { public int Version { get; set; } = Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; + public bool EnableMods { get; set; } = true; public string ModDirectory { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f14f19b5..afd18d2a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -98,13 +98,8 @@ public unsafe partial class ResourceLoader return retUnmodified; } - // Replace the hash and path with the correct one for the replacement, - // but only for non-UI files. UI files can not reasonably be loaded multiple times at once, - // and seem to cause concurrency problems if multiple UI parts use the same resource for different use-cases. - if( *categoryId != ResourceCategory.Ui ) - { - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); - } + // Replace the hash and path with the correct one for the replacement. + *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); path = resolvedPath.Value.InternalName.Path; var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); @@ -154,7 +149,7 @@ public unsafe partial class ResourceLoader // We hook ReadSqPack to redirect rooted files to ReadFile. public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = nameof(ReadSqPackDetour) )] + [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = nameof( ReadSqPackDetour ) )] public Hook< ReadSqPackPrototype > ReadSqPackHook = null!; private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) @@ -188,10 +183,10 @@ public unsafe partial class ResourceLoader var split = gamePath.Path.Split( ( byte )'|', 3, false ); fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; - - var funcFound = ResourceLoadCustomization.GetInvocationList() - .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui + && ResourceLoadCustomization.GetInvocationList() + .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); if( !funcFound ) { @@ -227,8 +222,9 @@ public unsafe partial class ResourceLoader var fdPtr = ( char* )( fd + 0x21 ); for( var i = 0; i < gamePath.Length; ++i ) { - ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; - fdPtr[ i ] = ( char )gamePath.Path[ i ]; + var c = ( char )gamePath.Path[ i ]; + ( &fileDescriptor->Utf16FileName )[ i ] = c; + fdPtr[ i ] = c; } ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index b5bd538c..486da507 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -124,6 +124,12 @@ public partial class Mod var lastSize = -1L; foreach( var file in files ) { + // Skip any UI Files because deduplication causes weird crashes for those. + if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( 'u', 'i', '/' ) ) ) + { + continue; + } + if( DuplicatesFinished ) { return; @@ -262,7 +268,6 @@ public partial class Mod } - // Deduplicate a mod simply by its directory without any confirmation or waiting time. internal static void DeduplicateMod( DirectoryInfo modDirectory ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0e2310f2..e4081ba0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -159,6 +159,7 @@ public class Penumbra : IDalamudPlugin system = new WindowSystem( Name ); system.AddWindow( _configWindow ); system.AddWindow( cfg.ModEditPopup ); + system.AddWindow( ConfigWindow.CreateChangelog() ); Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs new file mode 100644 index 00000000..c9dae1c2 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -0,0 +1,31 @@ +using Lumina.Excel.GeneratedSheets; +using OtterGui.Widgets; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public const int LastChangelogVersion = 0; + + public static Changelog CreateChangelog() + { + var ret = new Changelog( "Penumbra Changelog", () => Penumbra.Config.LastSeenVersion, version => + { + Penumbra.Config.LastSeenVersion = version; + Penumbra.Config.Save(); + } ); + + Add5_7_0( ret ); + + return ret; + } + + private static void Add5_7_0( Changelog log ) + => log.NextVersion( "Version 0.5.7.0" ) + .RegisterEntry( "Added a Changelog!" ) + .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) + .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) + .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) + .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) + .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index b256b04a..242e33b4 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -359,6 +359,12 @@ public partial class ConfigWindow Penumbra.Config.TutorialStep = 0; Penumbra.Config.Save(); } + + ImGui.SetCursorPos( new Vector2( xPos, 4 * ImGui.GetFrameHeightWithSpacing() ) ); + if( ImGui.Button( "Show Changelogs", new Vector2( width, 0 ) ) ) + { + Penumbra.Config.LastSeenVersion = 0; + } } } } \ No newline at end of file From 6e82242a72cae73b9967b07c0abeed25be2c71a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Sep 2022 16:10:04 +0200 Subject: [PATCH 0459/2451] Current Textures --- Penumbra/Import/TexToolsImport.cs | 4 +- Penumbra/Import/TexToolsImporter.Archives.cs | 67 +- .../Textures/CombinedTexture.Manipulation.cs | 228 +++++ Penumbra/Import/Textures/CombinedTexture.cs | 184 ++++ Penumbra/Import/Textures/Texture.cs | 183 ++++ .../{Dds => Textures}/TextureImporter.cs | 0 Penumbra/Mods/Mod.Creation.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 24 +- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 811 ++---------------- Penumbra/UI/Classes/ModEditWindow.cs | 11 + 10 files changed, 758 insertions(+), 762 deletions(-) create mode 100644 Penumbra/Import/Textures/CombinedTexture.Manipulation.cs create mode 100644 Penumbra/Import/Textures/CombinedTexture.cs create mode 100644 Penumbra/Import/Textures/Texture.cs rename Penumbra/Import/{Dds => Textures}/TextureImporter.cs (100%) diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 3dcf5d47..fd659278 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -81,8 +81,8 @@ public partial class TexToolsImporter : IDisposable private void ImportFiles() { - State = ImporterState.None; - _currentModPackIdx = 0; + State = ImporterState.None; + _currentModPackIdx = 0; foreach( var file in _modPackFiles ) { _currentModDirectory = null; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 12f7428f..5c0371dd 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -29,6 +29,7 @@ public partial class TexToolsImporter using var archive = ArchiveFactory.Open( zfs ); var baseName = FindArchiveModMeta( archive, out var leadDir ); + var name = string.Empty; _currentOptionIdx = 0; _currentNumOptions = 1; _currentModName = modPackFile.Name; @@ -44,7 +45,7 @@ public partial class TexToolsImporter }; PluginLog.Log( $" -> Importing {archive.Type} Archive." ); - _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -55,32 +56,61 @@ public partial class TexToolsImporter _currentFileIdx = 0; var reader = archive.ExtractAllEntries(); - while(reader.MoveToNextEntry()) + while( reader.MoveToNextEntry() ) { _token.ThrowIfCancellationRequested(); if( reader.Entry.IsDirectory ) { - ++_currentFileIdx; + --_currentNumFiles; continue; } PluginLog.Log( " -> Extracting {0}", reader.Entry.Key ); - reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); + // Check that the mod has a valid name in the meta.json file. + if( Path.GetFileName( reader.Entry.Key ) == "meta.json" ) + { + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo( s ); + s.Seek( 0, SeekOrigin.Begin ); + using var t = new StreamReader( s ); + using var j = new JsonTextReader( t ); + var obj = JObject.Load( j ); + name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; + if( name.Length == 0 ) + { + throw new Exception( "Invalid mod archive: mod meta has no name." ); + } + + using var f = File.OpenWrite( Path.Combine( _currentModDirectory.FullName, reader.Entry.Key ) ); + s.Seek( 0, SeekOrigin.Begin ); + s.WriteTo( f ); + } + else + { + reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); + } ++_currentFileIdx; } + _token.ThrowIfCancellationRequested(); + var oldName = _currentModDirectory.FullName; + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if( leadDir ) { - _token.ThrowIfCancellationRequested(); - var oldName = _currentModDirectory.FullName; - var tmpName = oldName + "__tmp"; - Directory.Move( oldName, tmpName ); - Directory.Move( Path.Combine( tmpName, baseName ), oldName ); - Directory.Delete( tmpName ); - _currentModDirectory = new DirectoryInfo( oldName ); + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName, false ); + Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); + Directory.Delete( oldName ); } + else + { + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, name, false ); + Directory.Move( oldName, _currentModDirectory.FullName ); + } + + _currentModDirectory.Refresh(); return _currentModDirectory; } @@ -88,7 +118,7 @@ public partial class TexToolsImporter // Search the archive for the meta.json file which needs to exist. private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) { - var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.EndsWith( "meta.json" ) ); + var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && Path.GetFileName( e.Key ) == "meta.json" ); // None found. if( entry == null ) { @@ -119,18 +149,7 @@ public partial class TexToolsImporter } } - // Check that the mod has a valid name in the meta.json file. - using var e = entry.OpenEntryStream(); - using var t = new StreamReader( e ); - using var j = new JsonTextReader( t ); - var obj = JObject.Load( j ); - var name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty; - if( name.Length == 0 ) - { - throw new Exception( "Invalid mod archive: mod meta has no name." ); - } - // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. - return ret.Length == 0 ? name : ret; + return ret; } } \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs new file mode 100644 index 00000000..d0c3e5b2 --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + 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 Vector4 DataLeft( int offset ) + => CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft ); + + private Vector4 DataRight( int offset ) + => CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + + private Vector4 DataRight( int x, int y ) + { + x += _offsetX; + y += _offsetY; + if( x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height ) + { + return Vector4.Zero; + } + + var offset = ( y * _right.TextureWrap!.Width + x ) * 4; + return CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + } + + private void AddPixelsMultiplied( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.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 ) + { + return; + } + + var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha; + var rgba = new Rgba32( sum with { W = alpha } ); + _centerStorage.RGBAPixels[ offset ] = rgba.R; + _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; + _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; + _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + } + } + + private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var rgba = new Rgba32( left ); + _centerStorage.RGBAPixels[ offset ] = rgba.R; + _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; + _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; + _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + } + } + + private void MultiplyPixelsRight( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _right.TextureWrap!.Width; ++x ) + { + var offset = ( _right.TextureWrap!.Width * y + x ) * 4; + var left = DataRight( offset ); + var rgba = new Rgba32( left ); + _centerStorage.RGBAPixels[ offset ] = rgba.R; + _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; + _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; + _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + } + } + + + private (int Width, int Height) CombineImage() + { + var (width, height) = _left.IsLoaded + ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) + : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); + _centerStorage.RGBAPixels = new byte[width * height * 4]; + + if( _left.IsLoaded ) + { + Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); + } + else + { + Parallel.For( 0, height, MultiplyPixelsRight ); + } + + return ( width, height ); + } + + private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) + { + if( bytes.Count == 0 ) + { + 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 ); + 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 ); + transformed.W = Math.Clamp( transformed.W, 0, 1 ); + return transformed; + } + + 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(); + } + + public void DrawMatrixInputLeft( float width ) + { + var ret = DrawMatrixInput( ref _multiplierLeft, width ); + ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft ); + if( ret ) + { + Update(); + } + } + + public void DrawMatrixInputRight( float width ) + { + var ret = DrawMatrixInput( ref _multiplierRight, width ); + ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( 75 ); + ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); + ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SameLine(); + ImGui.SetNextItemWidth( 75 ); + ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); + ret |= ImGui.IsItemDeactivatedAfterEdit(); + if( ret ) + { + Update(); + } + } + + private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width ) + { + 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 " ); + changes |= DragFloat( "##RR", inputWidth, ref multiplier.M11 ); + changes |= DragFloat( "##RG", inputWidth, ref multiplier.M12 ); + changes |= DragFloat( "##RB", inputWidth, ref multiplier.M13 ); + changes |= DragFloat( "##RA", inputWidth, ref multiplier.M14 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "G " ); + changes |= DragFloat( "##GR", inputWidth, ref multiplier.M21 ); + changes |= DragFloat( "##GG", inputWidth, ref multiplier.M22 ); + changes |= DragFloat( "##GB", inputWidth, ref multiplier.M23 ); + changes |= DragFloat( "##GA", inputWidth, ref multiplier.M24 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "B " ); + changes |= DragFloat( "##BR", inputWidth, ref multiplier.M31 ); + changes |= DragFloat( "##BG", inputWidth, ref multiplier.M32 ); + changes |= DragFloat( "##BB", inputWidth, ref multiplier.M33 ); + changes |= DragFloat( "##BA", inputWidth, ref multiplier.M34 ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "A " ); + changes |= DragFloat( "##AR", inputWidth, ref multiplier.M41 ); + changes |= DragFloat( "##AG", inputWidth, ref multiplier.M42 ); + changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); + changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + + return changes; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs new file mode 100644 index 00000000..8688ce2f --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -0,0 +1,184 @@ +using System; +using System.Numerics; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture : IDisposable +{ + private enum Mode + { + Empty, + LeftCopy, + RightCopy, + Custom, + } + + private readonly Texture _left; + private readonly Texture _right; + + private Texture? _current; + private Mode _mode = Mode.Empty; + + private readonly Texture _centerStorage = new(); + + public bool IsLoaded + => _mode != Mode.Empty; + + public void Draw( Vector2 size ) + { + if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) + { + var (width, height) = CombineImage(); + _centerStorage.TextureWrap = + Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + } + + _current?.Draw( size ); + } + + + public void SaveAsPng( string path ) + { + if( !IsLoaded || _current == null ) + { + return; + } + + var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height ); + image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + } + + public void SaveAsDDS( string path, DXGIFormat format, bool fast, float threshold = 0.5f ) + { + if( _current == null ) + return; + switch( _mode ) + { + case Mode.Empty: return; + case Mode.LeftCopy: + case Mode.RightCopy: + if( _centerStorage.BaseImage is ScratchImage s ) + { + if( format != s.Meta.Format ) + { + s = s.Convert( format, threshold ); + } + + s.SaveDDS( path ); + } + else + { + var image = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); + image.SaveDDS( path ).ThrowIfError(); + } + + break; + } + } + + //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: + // //ScratchImage.LoadDDS( _imageCenter, ) + // //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}" ); + // } + + public CombinedTexture( Texture left, Texture right ) + { + _left = left; + _right = right; + _left.Loaded += OnLoaded; + _right.Loaded += OnLoaded; + OnLoaded( false ); + } + + public void Dispose() + { + Clean(); + _left.Loaded -= OnLoaded; + _right.Loaded -= OnLoaded; + } + + private void OnLoaded( bool _ ) + => Update(); + + public void Update() + { + Clean(); + if( _left.IsLoaded ) + { + if( _right.IsLoaded ) + { + _current = _centerStorage; + _mode = Mode.Custom; + } + else if( !_invertLeft && _multiplierLeft.IsIdentity ) + { + _mode = Mode.LeftCopy; + _current = _left; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + else if( _right.IsLoaded ) + { + if( !_invertRight && _multiplierRight.IsIdentity ) + { + _current = _right; + _mode = Mode.RightCopy; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + } + + private void Clean() + { + _centerStorage.Dispose(); + _current = null; + _mode = Mode.Empty; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs new file mode 100644 index 00000000..cc3e656b --- /dev/null +++ b/Penumbra/Import/Textures/Texture.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using ImGuiNET; +using ImGuiScene; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Raii; +using OtterTex; +using Penumbra.GameData.ByteString; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public class Texture : IDisposable +{ + // Path to the file we tried to load. + public string Path = string.Empty; + + // If the load failed, an exception is stored. + public Exception? LoadError = null; + + // The pixels of the main image in RGBA order. + // Empty if LoadError != null or Path is empty. + public byte[] RGBAPixels = Array.Empty< byte >(); + + // The ImGui wrapper to load the image. + // null if LoadError != null or Path is empty. + public TextureWrap? TextureWrap = null; + + // The base image in whatever format it has. + public object? BaseImage = null; + + // Whether the file is successfully loaded and drawable. + public bool IsLoaded + => TextureWrap != null; + + public Texture() + { } + + public void Draw( Vector2 size ) + { + if( TextureWrap != null ) + { + ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); + size = size.X < TextureWrap.Width + ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } + : new Vector2( TextureWrap.Width, TextureWrap.Height ); + + ImGui.Image( TextureWrap.ImGuiHandle, size ); + } + else if( LoadError != null ) + { + ImGui.TextUnformatted( "Could not load file:" ); + ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); + } + else + { + ImGui.Dummy( size ); + } + } + + private void Clean() + { + RGBAPixels = Array.Empty< byte >(); + TextureWrap?.Dispose(); + TextureWrap = null; + ( BaseImage as IDisposable )?.Dispose(); + BaseImage = null; + Loaded?.Invoke( false ); + } + + public void Dispose() + => Clean(); + + public event Action< bool >? Loaded; + + private void Load( string path ) + { + _tmpPath = null; + if( path == Path ) + { + return; + } + + Path = path; + Clean(); + try + { + var _ = System.IO.Path.GetExtension( Path ) switch + { + ".dds" => LoadDds(), + ".png" => LoadPng(), + ".tex" => LoadTex(), + _ => true, + }; + Loaded?.Invoke( true ); + } + catch( Exception e ) + { + LoadError = e; + Clean(); + } + } + + private bool LoadDds() + { + var scratch = ScratchImage.LoadDDS( Path ); + BaseImage = scratch; + var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); + RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); + CreateTextureWrap( f.Meta.Width, f.Meta.Height ); + return true; + } + + private bool LoadPng() + { + BaseImage = null; + using var stream = File.OpenRead( Path ); + using var png = Image.Load< Rgba32 >( stream ); + RGBAPixels = new byte[png.Height * png.Width * 4]; + png.CopyPixelDataTo( RGBAPixels ); + CreateTextureWrap( png.Width, png.Height ); + return true; + } + + private bool LoadTex() + { + var tex = System.IO.Path.IsPathRooted( Path ) + ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) + : Dalamud.GameData.GetFile< TexFile >( Path ); + BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); + RGBAPixels = tex.GetRgbaImageData(); + CreateTextureWrap( tex.Header.Width, tex.Header.Height ); + return true; + } + + private void CreateTextureWrap( int width, int height ) + => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); + + private string? _tmpPath; + + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) + { + _tmpPath ??= 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 _tmpPath, Utf8GamePath.MaxGamePathLength ); + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Load( _tmpPath ); + } + + ImGuiUtil.HoverTooltip( tooltip ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, + true ) ) + { + if( Penumbra.Config.DefaultModImportPath.Length > 0 ) + { + startPath = Penumbra.Config.DefaultModImportPath; + } + + var texture = this; + + void UpdatePath( bool success, List< string > paths ) + { + if( success && paths.Count > 0 ) + { + texture.Load( paths[ 0 ] ); + } + } + + manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs similarity index 100% rename from Penumbra/Import/Dds/TextureImporter.cs rename to Penumbra/Import/Textures/TextureImporter.cs diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index d9c64cc1..f56f9572 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -16,7 +16,7 @@ public partial class Mod // - Not Empty // - Unique, by appending (digit) for duplicates. // - Containing no symbols invalid for FFXIV or windows paths. - internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) + internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { var name = modListName; if( name.Length == 0 ) @@ -31,7 +31,11 @@ public partial class Mod throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); } - Directory.CreateDirectory( newModFolder ); + if( create ) + { + Directory.CreateDirectory( newModFolder ); + } + return new DirectoryInfo( newModFolder ); } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 0f91ec56..cadae96e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -368,6 +368,16 @@ public partial class ModEditWindow private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) { + static bool FixFloat( ref float val, float current ) + { + if( val < 0 ) + { + val = 0; + } + + return val != current; + } + using var id = ImRaii.PushId( rowIdx ); var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var hasDye = file.ColorDyeSets.Length > colorSetIdx; @@ -383,7 +393,7 @@ public partial class ModEditWindow ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); + using var dis = ImRaii.Disabled( disabled ); ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); if( hasDye ) { @@ -397,7 +407,7 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.SpecularStrength ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.SpecularStrength) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -427,7 +437,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.GlossStrength ) + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; ret = true; @@ -455,7 +465,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.X ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.MaterialRepeat.X) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; @@ -465,7 +475,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.Y ) + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; @@ -476,7 +486,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.X ) + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; @@ -487,7 +497,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.Y ) + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 9b07b8a1..1cebaa0e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -1,702 +1,99 @@ using System; -using System.Collections.Generic; using System.IO; using System.Numerics; -using System.Threading.Tasks; -using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; -using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; -using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.GameData.ByteString; -using Penumbra.Import.Dds; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; +using Penumbra.Import.Textures; namespace Penumbra.UI.Classes; -public class Texture : IDisposable -{ - // Path to the file we tried to load. - public string Path = string.Empty; - - // If the load failed, an exception is stored. - public Exception? LoadError = null; - - // The pixels of the main image in RGBA order. - // Empty if LoadError != null or Path is empty. - public byte[] RGBAPixels = Array.Empty< byte >(); - - // The ImGui wrapper to load the image. - // null if LoadError != null or Path is empty. - public TextureWrap? TextureWrap = null; - - // The base image in whatever format it has. - public object? BaseImage = null; - - public Texture() - { } - - public void Draw( Vector2 size ) - { - if( TextureWrap != null ) - { - ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); - size = size.X < TextureWrap.Width - ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } - : new Vector2( TextureWrap.Width, TextureWrap.Height ); - - ImGui.Image( TextureWrap.ImGuiHandle, size ); - } - else if( LoadError != null ) - { - ImGui.TextUnformatted( "Could not load file:" ); - ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); - } - else - { - ImGui.Dummy( size ); - } - } - - private void Clean() - { - RGBAPixels = Array.Empty< byte >(); - TextureWrap?.Dispose(); - TextureWrap = null; - ( BaseImage as IDisposable )?.Dispose(); - BaseImage = null; - Loaded?.Invoke( false ); - } - - public void Dispose() - => Clean(); - - public event Action< bool >? Loaded; - - private void Load( string path ) - { - _tmpPath = null; - if( path == Path ) - { - return; - } - - Path = path; - Clean(); - try - { - var _ = System.IO.Path.GetExtension( Path ) switch - { - ".dds" => LoadDds(), - ".png" => LoadPng(), - ".tex" => LoadTex(), - _ => true, - }; - Loaded?.Invoke( true ); - } - catch( Exception e ) - { - LoadError = e; - Clean(); - } - } - - private bool LoadDds() - { - var scratch = ScratchImage.LoadDDS( Path ); - BaseImage = scratch; - var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); - RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); - CreateTextureWrap( f.Meta.Width, f.Meta.Height ); - return true; - } - - private bool LoadPng() - { - BaseImage = null; - using var stream = File.OpenRead( Path ); - using var png = Image.Load< Rgba32 >( stream ); - RGBAPixels = new byte[png.Height * png.Width * 4]; - png.CopyPixelDataTo( RGBAPixels ); - CreateTextureWrap( png.Width, png.Height ); - return true; - } - - private bool LoadTex() - { - var tex = System.IO.Path.IsPathRooted( Path ) - ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) - : Dalamud.GameData.GetFile< TexFile >( Path ); - BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); - RGBAPixels = tex.GetRgbaImageData(); - CreateTextureWrap( tex.Header.Width, tex.Header.Height ); - return true; - } - - private void CreateTextureWrap( int width, int height ) - => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); - - private string? _tmpPath; - - public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) - { - _tmpPath ??= 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 _tmpPath, Utf8GamePath.MaxGamePathLength ); - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Load( _tmpPath ); - } - - ImGuiUtil.HoverTooltip( tooltip ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, - true ) ) - { - if( Penumbra.Config.DefaultModImportPath.Length > 0 ) - { - startPath = Penumbra.Config.DefaultModImportPath; - } - - var texture = this; - - void UpdatePath( bool success, List< string > paths ) - { - if( success && paths.Count > 0 ) - { - texture.Load( paths[ 0 ] ); - } - } - - manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); - } - } - - public static Texture Combined( Texture left, Texture right, InputManipulations leftManips, InputManipulations rightManips ) - => new(); -} - -public struct InputManipulations -{ - public InputManipulations() - { } - - public Matrix4x4 _multiplier = Matrix4x4.Identity; - public bool _invert = false; - public int _offsetX = 0; - public int _offsetY = 0; - public int _outputWidth = 0; - public int _outputHeight = 0; - - - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) - { - 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 ); - 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 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); - return transformed; - } - - 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(); - } - - - public bool DrawMatrixInput( float width ) - { - 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 " ); - changes |= DragFloat( "##RR", inputWidth, ref _multiplier.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref _multiplier.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref _multiplier.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref _multiplier.M14 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref _multiplier.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref _multiplier.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref _multiplier.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref _multiplier.M24 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref _multiplier.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref _multiplier.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref _multiplier.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref _multiplier.M34 ); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref _multiplier.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref _multiplier.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref _multiplier.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref _multiplier.M44 ); - - return changes; - } -} - public partial class ModEditWindow { - private string _pathLeft = string.Empty; - private string _pathRight = string.Empty; + private readonly Texture _left = new(); + private readonly Texture _right = new(); + private readonly CombinedTexture _center; - private byte[]? _imageLeft; - private byte[]? _imageRight; - private byte[]? _imageCenter; + private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); + private bool _overlayCollapsed = true; + private DXGIFormat _currentFormat = DXGIFormat.R8G8B8A8UNorm; - private TextureWrap? _wrapLeft; - private TextureWrap? _wrapRight; - private TextureWrap? _wrapCenter; - - 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 = ConfigWindow.SetupFileManager(); - - private static bool DragFloat( string label, float width, ref float value ) + private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) { - 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 ) - { - 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 " ); - 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 " ); - 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 " ); - 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 " ); - 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 void PathInputBox( string label, string hint, string tooltip, int which ) - { - 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 ); - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - UpdateImage( tmp, which ); - } - - 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 ) - { - try - { - if( !ScratchImage.LoadDDS( path, out var f ) ) - { - return ( null, 0, 0 ); - } - - if( !f.GetRGBA( out f ) ) - { - return ( null, 0, 0 ); - } - - return ( f.Pixels[ ..( f.Meta.Width * f.Meta.Height * 4 ) ].ToArray(), f.Meta.Width, f.Meta.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 ); - if( tex == null ) - { - return ( null, 0, 0 ); - } - - var rgba = tex.GetRgbaImageData(); - return ( rgba, 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 ); - 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 ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse PNG {path} to RGBA:\n{e}" ); - return ( null, 0, 0 ); - } - } - - private void UpdateImage( string newPath, int which ) - { - if( which is < 0 or > 1 ) + using var child = ImRaii.Child( label, size, true ); + if( !child ) { return; } - ref var path = ref which == 0 ? ref _pathLeft : ref _pathRight; - if( path == newPath ) + using var id = ImRaii.PushId( label ); + ImGuiUtil.DrawTextButton( label, new Vector2( -1, 0 ), ImGui.GetColorU32( ImGuiCol.FrameBg ) ); + ImGui.NewLine(); + + tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, + _dialogManager ); + + if( tex == _left ) { - 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; - 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 ), - }; - } + _center.DrawMatrixInputLeft( size.X ); } else { - ( data, width, height ) = GetTexRgbaData( path, false ); + _center.DrawMatrixInputRight( size.X ); } - if( data != null ) + ImGui.NewLine(); + tex.Draw( imageSize ); + } + + private void DrawOutputChild( Vector2 size, Vector2 imageSize ) + { + using var child = ImRaii.Child( "Output", size, true ); + if( !child ) { - try + return; + } + + if( _center.IsLoaded ) + { + if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) { - wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 ); + var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); + _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => { }, _mod!.ModPath.FullName ); } - catch( Exception e ) + + if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) { - PluginLog.Error( $"Could not load raw image:\n{e}" ); + var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); + _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => { if( a ) _center.SaveAsDDS( b, _currentFormat, false ); }, _mod!.ModPath.FullName ); } - } - UpdateCenter(); - } - - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) - { - 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 ); - 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 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); - 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 = ( 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 ) - { - 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; - _imageCenter![ offset + 3 ] = rgba.A; - } - - private void UpdateCenter() - { - if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity && !_invertLeft ) - { - _imageCenter = _imageLeft; - _wrapCenter = _wrapLeft; - return; - } - - if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity && !_invertRight ) - { - _imageCenter = _imageRight; - _wrapCenter = _wrapRight; - return; - } - - if( !ReferenceEquals( _imageCenter, _imageLeft ) && !ReferenceEquals( _imageCenter, _imageRight ) ) - { - _wrapCenter?.Dispose(); - } - - if( _imageLeft != null || _imageRight != null ) - { - 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, _ ) => + if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) { - for( var x = 0; x < totalWidth; ++x ) - { - AddPixels( totalWidth, x, y ); - } - } ); - _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, totalWidth, totalHeight, 4 ); - return; - } - - _imageCenter = null; - _wrapCenter = null; - } - - private static void ScaledImage( string path, TextureWrap? wrap, Vector2 size ) - { - if( wrap != null ) - { - ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" ); - size = size.X < wrap.Width - ? size with { Y = wrap.Height * size.X / wrap.Width } - : new Vector2( wrap.Width, wrap.Height ); - - 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: - //ScratchImage.LoadDDS( _imageCenter, ) - //if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) - //{ - // File.WriteAllBytes( path, dds ); - //} - - break; + var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); + _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => { if (a) _center.SaveAsPng( b ); }, _mod!.ModPath.FullName ); } + + ImGui.NewLine(); } - catch( Exception e ) - { - PluginLog.Error( $"Could not save image to {path}:\n{e}" ); - } + + _center.Draw( imageSize ); } - private void SaveAsPng( bool success, string path ) - => SaveAs( success, path, 0 ); + private Vector2 GetChildWidth() + { + var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetTextLineHeight(); + if( _overlayCollapsed ) + { + var width = windowWidth - ImGui.GetStyle().FramePadding.X * 3; + return new Vector2( width / 2, -1 ); + } - private void SaveAsTex( bool success, string path ) - => SaveAs( success, path, 1 ); - - private void SaveAsDds( bool success, string path ) - => SaveAs( success, path, 2 ); + return new Vector2( ( windowWidth - ImGui.GetStyle().FramePadding.X * 5 ) / 3, -1 ); + } private void DrawTextureTab() { @@ -708,78 +105,38 @@ public partial class ModEditWindow return; } - var leftRightWidth = - new Vector2( - ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 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 ) ) + try { - if( child ) + var childWidth = GetChildWidth(); + var imageSize = new Vector2( childWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); + DrawInputChild( "Input Texture", _left, childWidth, imageSize ); + ImGui.SameLine(); + DrawOutputChild( childWidth, imageSize ); + if( !_overlayCollapsed ) { - PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 ); - - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) ) - { - UpdateCenter(); - } - - ImGui.NewLine(); - ScaledImage( _pathLeft, _wrapLeft, imageSize ); + ImGui.SameLine(); + DrawInputChild( "Overlay Texture", _right, childWidth, imageSize ); } + + ImGui.SameLine(); + DrawOverlayCollapseButton(); } - - ImGui.SameLine(); - using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) ) + catch( Exception e ) { - 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( child ) - { - PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 ); - - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) || ImGui.Checkbox( "Invert##Right", ref _invertRight ) ) - { - UpdateCenter(); - } - - ImGui.NewLine(); - ScaledImage( _pathRight, _wrapRight, imageSize ); - } + PluginLog.Error( $"Unknown Error while drawing textures:\n{e}" ); } } + + private void DrawOverlayCollapseButton() + { + var (label, tooltip) = _overlayCollapsed + ? ( ">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture." ) + : ( "<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any." ); + if( ImGui.Button( label, new Vector2( ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y ) ) ) + { + _overlayCollapsed = !_overlayCollapsed; + } + + ImGuiUtil.HoverTooltip( tooltip ); + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 4e3693a4..36aae29d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -11,6 +11,7 @@ using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.Import.Textures; using Penumbra.Mods; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -121,6 +122,12 @@ public partial class ModEditWindow : Window, IDisposable WindowName = sb.ToString(); } + public override void OnClose() + { + _left.Dispose(); + _right.Dispose(); + } + public override void Draw() { using var tabBar = ImRaii.TabBar( "##tabs" ); @@ -508,10 +515,14 @@ public partial class ModEditWindow : Window, IDisposable DrawMaterialPanel ); _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), DrawModelPanel ); + _center = new CombinedTexture( _left, _right ); } public void Dispose() { _editor?.Dispose(); + _left.Dispose(); + _right.Dispose(); + _center.Dispose(); } } \ No newline at end of file From 7b4654ce340411342376384c675ac3e7a0767c8d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Sep 2022 15:44:57 +0200 Subject: [PATCH 0460/2451] Bloop --- .../Textures/CombinedTexture.Manipulation.cs | 2 +- Penumbra/Import/Textures/Texture.cs | 121 +++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index d0c3e5b2..3e60601b 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -95,7 +95,7 @@ public partial class CombinedTexture ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); _centerStorage.RGBAPixels = new byte[width * height * 4]; - + _centerStorage.Type = Texture.FileType.Bitmap; if( _left.IsLoaded ) { Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index cc3e656b..a0cc6d33 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -5,6 +5,7 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using ImGuiScene; using Lumina.Data.Files; @@ -20,6 +21,15 @@ namespace Penumbra.Import.Textures; public class Texture : IDisposable { + public enum FileType + { + Unknown, + Dds, + Tex, + Png, + Bitmap, + } + // Path to the file we tried to load. public string Path = string.Empty; @@ -37,6 +47,9 @@ public class Texture : IDisposable // The base image in whatever format it has. public object? BaseImage = null; + // Original File Type. + public FileType Type = FileType.Unknown; + // Whether the file is successfully loaded and drawable. public bool IsLoaded => TextureWrap != null; @@ -48,21 +61,51 @@ public class Texture : IDisposable { if( TextureWrap != null ) { - ImGui.TextUnformatted( $"Image Dimensions: {TextureWrap.Width} x {TextureWrap.Height}" ); size = size.X < TextureWrap.Width ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } : new Vector2( TextureWrap.Width, TextureWrap.Height ); ImGui.Image( TextureWrap.ImGuiHandle, size ); + DrawData(); } else if( LoadError != null ) { ImGui.TextUnformatted( "Could not load file:" ); ImGuiUtil.TextColored( Colors.RegexWarningBorder, LoadError.ToString() ); } - else + } + + public void DrawData() + { + using var table = ImRaii.Table( "##data", 2, ImGuiTableFlags.SizingFixedFit ); + ImGuiUtil.DrawTableColumn( "Width" ); + ImGuiUtil.DrawTableColumn( TextureWrap!.Width.ToString() ); + ImGuiUtil.DrawTableColumn( "Height" ); + ImGuiUtil.DrawTableColumn( TextureWrap!.Height.ToString() ); + ImGuiUtil.DrawTableColumn( "File Type" ); + ImGuiUtil.DrawTableColumn( Type.ToString() ); + ImGuiUtil.DrawTableColumn( "Bitmap Size" ); + ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( RGBAPixels.Length )} ({RGBAPixels.Length} Bytes)" ); + switch( BaseImage ) { - ImGui.Dummy( size ); + case ScratchImage s: + ImGuiUtil.DrawTableColumn( "Format" ); + ImGuiUtil.DrawTableColumn( s.Meta.Format.ToString() ); + ImGuiUtil.DrawTableColumn( "Mip Levels" ); + ImGuiUtil.DrawTableColumn( s.Meta.MipLevels.ToString() ); + ImGuiUtil.DrawTableColumn( "Data Size" ); + ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( s.Pixels.Length )} ({s.Pixels.Length} Bytes)" ); + ImGuiUtil.DrawTableColumn( "Number of Images" ); + ImGuiUtil.DrawTableColumn( s.Images.Length.ToString() ); + break; + case TexFile t: + ImGuiUtil.DrawTableColumn( "Format" ); + ImGuiUtil.DrawTableColumn( t.Header.Format.ToString() ); + ImGuiUtil.DrawTableColumn( "Mip Levels" ); + ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString()) ; + ImGuiUtil.DrawTableColumn( "Data Size" ); + ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( t.ImageData.Length )} ({t.ImageData.Length} Bytes)" ); + break; } } @@ -73,6 +116,7 @@ public class Texture : IDisposable TextureWrap = null; ( BaseImage as IDisposable )?.Dispose(); BaseImage = null; + Type = FileType.Unknown; Loaded?.Invoke( false ); } @@ -111,6 +155,7 @@ public class Texture : IDisposable private bool LoadDds() { + Type = FileType.Dds; var scratch = ScratchImage.LoadDDS( Path ); BaseImage = scratch; var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); @@ -121,6 +166,7 @@ public class Texture : IDisposable private bool LoadPng() { + Type = FileType.Png; BaseImage = null; using var stream = File.OpenRead( Path ); using var png = Image.Load< Rgba32 >( stream ); @@ -132,6 +178,7 @@ public class Texture : IDisposable private bool LoadTex() { + Type = FileType.Tex; var tex = System.IO.Path.IsPathRooted( Path ) ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) : Dalamud.GameData.GetFile< TexFile >( Path ); @@ -180,4 +227,72 @@ public class Texture : IDisposable manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); } } +} + +public static class ScratchImageExtensions +{ + public static Exception? SaveAsTex( this ScratchImage image, string path ) + { + try + { + using var fileStream = File.OpenWrite( path ); + using var bw = new BinaryWriter( fileStream ); + + bw.Write( (uint) image.Meta.GetAttribute() ); + bw.Write( (uint) image.Meta.GetFormat() ); + bw.Write( (ushort) image.Meta.Width ); + bw.Write( (ushort) image.Meta.Height ); + bw.Write( (ushort) image.Meta.Depth ); + bw.Write( (ushort) image.Meta.MipLevels ); + } + catch( Exception e ) + { + return e; + } + + return null; + } + + public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) + { + var ret = new TexFile.TexHeader() + { + Type = image.Meta.GetAttribute(), + Format = image.Meta.GetFormat(), + Width = ( ushort )image.Meta.Width, + Height = ( ushort )image.Meta.Height, + Depth = ( ushort )image.Meta.Depth, + }; + ret.LodOffset[ 0 ] = 0; + ret.LodOffset[ 1 ] = 1; + ret.LodOffset[ 2 ] = 2; + //foreach(var surface in image.Images) + // ret.OffsetToSurface[ 0 ] = 80 + (image.P); + return ret; + } + + // Get all known flags for the TexFile.Attribute from the scratch image. + private static TexFile.Attribute GetAttribute( this TexMeta meta ) + { + var ret = meta.Dimension switch + { + TexDimension.Tex1D => TexFile.Attribute.TextureType1D, + TexDimension.Tex2D => TexFile.Attribute.TextureType2D, + TexDimension.Tex3D => TexFile.Attribute.TextureType3D, + _ => (TexFile.Attribute) 0, + }; + if( meta.IsCubeMap ) + ret |= TexFile.Attribute.TextureTypeCube; + if( meta.Format.IsDepthStencil() ) + ret |= TexFile.Attribute.TextureDepthStencil; + return ret; + } + + private static TexFile.TextureFormat GetFormat( this TexMeta meta ) + { + return meta.Format switch + { + _ => TexFile.TextureFormat.Unknown, + }; + } } \ No newline at end of file From 5eda2d3a23442201353eda70a3ee339a4ccf3e12 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Sep 2022 15:48:34 +0200 Subject: [PATCH 0461/2451] Fix wonky line --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9ec5e2ad..b830108e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9ec5e2ad2f2d35d62c2ac7c300b914fffbda2191 +Subproject commit b830108e60126f808280e72ae7d8f00cec34ba5c From 7b7f241923c5278be8424b92d14e5c483f70ae0f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Sep 2022 15:49:02 +0200 Subject: [PATCH 0462/2451] Add Penumbra Mod Pack file ending and migration. --- Penumbra/Configuration.Migration.cs | 9 ++++++ Penumbra/Configuration.cs | 3 +- Penumbra/Import/TexToolsImport.cs | 2 +- Penumbra/Mods/Editor/ModBackup.cs | 31 +++++++++++++++++++- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 5 ++++ Penumbra/Mods/Manager/Mod.Manager.cs | 3 ++ Penumbra/Penumbra.cs | 15 +++++++--- Penumbra/UI/Classes/ModFileSystemSelector.cs | 4 +-- 8 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index a62c091b..3eca3c07 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -47,6 +47,15 @@ public partial class Configuration m.Version1To2(); m.Version2To3(); m.Version3To4(); + m.Version4To5(); + } + + // Mod backup extension was changed from .zip to .pmp. + // Actual migration takes place in ModManager. + private void Version4To5() + { + Mod.Manager.MigrateModBackups = true; + _config.Version = 5; } // SortMode was changed from an enum to a type. diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 0e95a7f6..9b6381a7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -8,7 +8,6 @@ using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; -using OtterGui.Widgets; using Penumbra.Import; using Penumbra.Mods; using Penumbra.UI; @@ -142,7 +141,7 @@ public partial class Configuration : IPluginConfiguration // Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 4; + public const int CurrentVersion = 5; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 3dcf5d47..98c1df0b 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -121,7 +121,7 @@ public partial class TexToolsImporter : IDisposable // Puts out warnings if extension does not correspond to data. private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) { - if( modPackFile.Extension is ".zip" or ".7z" or ".rar" ) + if( modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar" ) { return HandleRegularArchive( modPackFile ); } diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index acc9e8af..fa353fd4 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -18,10 +18,39 @@ public class ModBackup public ModBackup( Mod mod ) { _mod = mod; - Name = mod.ModPath + ".zip"; + Name = _mod.ModPath + ".pmp"; Exists = File.Exists( Name ); } + // Migrate file extensions. + public static void MigrateZipToPmp(Mod.Manager manager) + { + foreach( var mod in manager ) + { + var pmpName = mod.ModPath + ".pmp"; + var zipName = mod.ModPath + ".zip"; + if( File.Exists( zipName ) ) + { + try + { + if( !File.Exists( pmpName ) ) + { + File.Move( zipName, pmpName ); + } + else + { + File.Delete( zipName ); + } + PluginLog.Information( $"Migrated mod backup from {zipName} to {pmpName}." ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not migrate mod backup of {mod.ModPath} from .pmp to .zip:\n{e}" ); + } + } + } + } + // Create a backup zip without blocking the main thread. public async void CreateAsync() { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 92a8c51e..cec734ee 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -99,6 +99,11 @@ public sealed partial class Mod ModDiscoveryFinished?.Invoke(); PluginLog.Information( "Rediscovered mods." ); + + if( MigrateModBackups ) + { + ModBackup.MigrateZipToPmp( this ); + } } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index ff800473..11f878ca 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -9,6 +9,9 @@ public sealed partial class Mod { public sealed partial class Manager : IReadOnlyList< Mod > { + // Set when reading Config and migrating from v4 to v5. + public static bool MigrateModBackups = false; + // An easily accessible set of new mods. // Mods are added when they are created or imported. // Mods are removed when they are deleted or when they are toggled in any collection. diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e4081ba0..fdb59a91 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -143,7 +143,7 @@ public class Penumbra : IDalamudPlugin Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); - PluginLog.Information( $"Loading native assembly from {OtterTex.NativeDll.Directory}." ); + PluginLog.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); } catch { @@ -165,10 +165,17 @@ public class Penumbra : IDalamudPlugin private void DisposeInterface() { - Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; + if( _windowSystem != null ) + { + Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + } + _launchButton?.Dispose(); - _configWindow?.Dispose(); + if( _configWindow != null ) + { + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; + _configWindow.Dispose(); + } } public bool Enable() diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index be0d838b..9b83fc67 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -200,7 +200,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void AddImportModButton( Vector2 size ) { var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ); + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true ); ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); if( !button ) { @@ -213,7 +213,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod _hasSetFolder = true; _fileManager.OpenFileDialog( "Import Mod Pack", - "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) => + "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", ( s, f ) => { if( s ) { From f15c20a9992e4049eff0264d7ddeeb64b5524f57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Sep 2022 16:20:33 +0200 Subject: [PATCH 0463/2451] Add PMP changelog entries. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.Changelog.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b830108e..98edb3c7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b830108e60126f808280e72ae7d8f00cec34ba5c +Subproject commit 98edb3c7901f1017393c09c6e8b258895ae1c01e diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index c9dae1c2..fecc41b7 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -26,6 +26,12 @@ public partial class ConfigWindow .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) + .RegisterEntry( "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) + .RegisterEntry( "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", 1 ) + .RegisterHighlight( "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR." , 1 ) + .RegisterEntry( "Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1 ) + .RegisterEntry( "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", 1 ) .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) + .RegisterEntry( "Fixed a wrongly displayed folder line in some circumstances." ) .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); } \ No newline at end of file From e9b12da97efb779906490ff70a679275187292c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 Sep 2022 16:39:55 +0200 Subject: [PATCH 0464/2451] Make identically named options selectable, fix crash after deleting options. --- Penumbra/Mods/Subclasses/ModSettings.cs | 2 +- Penumbra/UI/ConfigWindow.Changelog.cs | 17 +++++++++++++---- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 6 ++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 6d6f2f3d..b79f1c9e 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -70,7 +70,7 @@ public class ModSettings var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => config >= optionIdx ? Math.Max( 0, config - 1 ) : config, + SelectType.Single => config >= optionIdx ? (config > 1 ? config - 1 : 0) : config, SelectType.Multi => RemoveBit( config, optionIdx ), _ => config, }; diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index fecc41b7..db6cfec4 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -26,12 +26,21 @@ public partial class ConfigWindow .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) - .RegisterEntry( "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) - .RegisterEntry( "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", 1 ) - .RegisterHighlight( "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR." , 1 ) + .RegisterEntry( + "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) + .RegisterEntry( + "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", + 1 ) + .RegisterHighlight( + "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", + 1 ) .RegisterEntry( "Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1 ) - .RegisterEntry( "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", 1 ) + .RegisterEntry( + "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", + 1 ) .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) .RegisterEntry( "Fixed a wrongly displayed folder line in some circumstances." ) + .RegisterEntry( "Fixed crash after deleting mod options." ) + .RegisterEntry( "Made identically named options selectable in mod configuration. Do not name your options identically." ) .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 8fc57460..a1eda296 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -168,10 +168,13 @@ public partial class ConfigWindow { for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { + id.Push( idx2 ); if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) ) { Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); } + + id.Pop(); } } @@ -201,6 +204,7 @@ public partial class ConfigWindow Widget.BeginFramedGroup( group.Name, group.Description ); for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { + id.Push( idx2 ); var flag = 1u << idx2; var setting = ( flags & flag ) != 0; if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) ) @@ -208,6 +212,8 @@ public partial class ConfigWindow flags = setting ? flags | flag : flags & ~flag; Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); } + + id.Pop(); } Widget.EndFramedGroup(); From cceab7d98dc8ec6ddd6dd9c4fc7662b9309f2e38 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Sep 2022 14:58:43 +0200 Subject: [PATCH 0465/2451] Add temp collection stuff. --- Penumbra/Api/IpcTester.cs | 9 ++++- Penumbra/Mods/Mod.TemporaryMod.cs | 67 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index f5140db4..2bc4ebad 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Numerics; using System.Reflection; @@ -62,7 +63,6 @@ public class IpcTester : IDisposable { if( !_subscribed ) { - _initialized.Subscribe( AddInitialized ); _disposed.Subscribe( AddDisposed ); _redrawn.Subscribe( SetLastRedrawn ); @@ -911,7 +911,7 @@ public class IpcTester : IDisposable return; } - using var table = ImRaii.Table( "##collTree", 4 ); + using var table = ImRaii.Table( "##collTree", 5 ); if( !table ) { return; @@ -927,6 +927,11 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); ImGui.TableNextColumn(); ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); + ImGui.TableNextColumn(); + if( ImGui.Button( $"Save##{character}" ) ) + { + Mod.TemporaryMod.SaveTempCollection( collection, character ); + } } } diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 10d90979..7b55b994 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Classes; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -42,5 +47,67 @@ public sealed partial class Mod _default.FileData = dict; _default.ManipulationData = manips; } + + public static void SaveTempCollection( ModCollection collection, string? character = null ) + { + DirectoryInfo? dir = null; + try + { + dir = CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); + CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, + $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); + var mod = new Mod( dir ); + var defaultMod = mod._default; + foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) + { + if( gamePath.Path.EndsWith( '.', 'i', 'm', 'c' ) ) + { + continue; + } + + var targetPath = fullPath.Path.FullName; + if( fullPath.Path.Name.StartsWith( '|' ) ) + { + targetPath = targetPath.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries ).Last(); + } + + if( Path.IsPathRooted(targetPath) ) + { + var target = Path.Combine( fileDir.FullName, Path.GetFileName(targetPath) ); + File.Copy( targetPath, target, true ); + defaultMod.FileData[ gamePath ] = new FullPath( target ); + } + else + { + defaultMod.FileSwapData[ gamePath ] = new FullPath(targetPath); + } + } + + foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() ) + { + defaultMod.ManipulationData.Add( manip ); + } + + mod.SaveDefaultMod(); + Penumbra.ModManager.AddMod( dir ); + PluginLog.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); + if( dir != null && Directory.Exists( dir.FullName ) ) + { + try + { + Directory.Delete( dir.FullName, true ); + } + catch + { + // ignored + } + } + } + } } } \ No newline at end of file From aecb033537f6655ae146b315d0d52c7765fe9d29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Sep 2022 22:23:51 +0200 Subject: [PATCH 0466/2451] Fix an exception on inspect identification --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 39efe25b..1ebc0a8a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -44,7 +44,7 @@ public unsafe partial class PathResolver var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 2u : 6u; var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); - return text != null ? text->NodeText.ToString() : null; + return text != null && text->AtkResNode.Type == NodeType.Text ? text->NodeText.ToString() : null; } // Obtain the name displayed in the Character Card from the agent. From d65488632ab6a15300dd476d2909a2ba4de354f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Sep 2022 15:08:22 +0200 Subject: [PATCH 0467/2451] Fix Inspect Identification not working. --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 1ebc0a8a..7fe51b15 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -4,14 +4,12 @@ using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; @@ -20,7 +18,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { [Signature( "0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress )] - private static ushort* _inspectTitleId = null!; + private static readonly ushort* InspectTitleId = null!; // Obtain the name of the current player, if one exists. private static string? GetPlayerName() @@ -41,7 +39,7 @@ public unsafe partial class PathResolver } var ui = ( AtkUnitBase* )addon; - var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 2u : 6u; + var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *InspectTitleId )?.IsPrefix == true ? 7u : 6u; var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); return text != null && text->AtkResNode.Type == NodeType.Text ? text->NodeText.ToString() : null; From 521c86d81d065ab323e025c31937c4d5229b1859 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Sep 2022 15:42:23 +0200 Subject: [PATCH 0468/2451] Revert stupid from last commit, add changelog, push update. --- OtterGui | 2 +- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 4 ++-- Penumbra/UI/ConfigWindow.Changelog.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 98edb3c7..d8522bb1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 98edb3c7901f1017393c09c6e8b258895ae1c01e +Subproject commit d8522bb1d87cef54157ca2fb18d05c0c4fc4eeb2 diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 7fe51b15..b88c4e88 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -18,7 +18,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { [Signature( "0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress )] - private static readonly ushort* InspectTitleId = null!; + private static ushort* _inspectTitleId = null!; // Obtain the name of the current player, if one exists. private static string? GetPlayerName() @@ -39,7 +39,7 @@ public unsafe partial class PathResolver } var ui = ( AtkUnitBase* )addon; - var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *InspectTitleId )?.IsPrefix == true ? 7u : 6u; + var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 7u : 6u; var text = ( AtkTextNode* )ui->UldManager.SearchNodeById( nodeId ); return text != null && text->AtkResNode.Type == NodeType.Text ? text->NodeText.ToString() : null; diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index db6cfec4..654f020b 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -26,6 +26,7 @@ public partial class ConfigWindow .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) + .RegisterHighlight( "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", 1 ) .RegisterEntry( "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) .RegisterEntry( @@ -41,6 +42,7 @@ public partial class ConfigWindow .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) .RegisterEntry( "Fixed a wrongly displayed folder line in some circumstances." ) .RegisterEntry( "Fixed crash after deleting mod options." ) + .RegisterEntry( "Fixed Inspect Window collections not working correctly." ) .RegisterEntry( "Made identically named options selectable in mod configuration. Do not name your options identically." ) .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); } \ No newline at end of file From d820b886b3051ac720985530d1aa0d9fc545873e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 10 Sep 2022 13:44:40 +0000 Subject: [PATCH 0469/2451] [CI] Updating repo.json for refs/tags/0.5.7.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c32d148e..05087b4a 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.6.3", - "TestingAssemblyVersion": "0.5.6.3", + "AssemblyVersion": "0.5.7.0", + "TestingAssemblyVersion": "0.5.7.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.6.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From d89db756f37f7b676566b79885e1717c135b5f8c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Sep 2022 16:08:32 +0200 Subject: [PATCH 0470/2451] Fix Changelog for GlobalScales. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index d8522bb1..304f0d8a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d8522bb1d87cef54157ca2fb18d05c0c4fc4eeb2 +Subproject commit 304f0d8a86c07c3a634e84a92dae356e17c10c0c From e5ba2317acf58e0a8d952abc00aa2cb69ebce2d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Sep 2022 16:40:51 +0200 Subject: [PATCH 0471/2451] Reworked Changelog display slightly. --- OtterGui | 2 +- Penumbra/Penumbra.cs | 18 ++++++++++++------ Penumbra/UI/ConfigWindow.Changelog.cs | 10 +++++++++- Penumbra/UI/ConfigWindow.SettingsTab.cs | 4 ++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/OtterGui b/OtterGui index 304f0d8a..988c8291 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 304f0d8a86c07c3a634e84a92dae356e17c10c0c +Subproject commit 988c8291bb09649712bf89334869c1f4f5ce356d diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fdb59a91..fd7c2336 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -14,6 +14,7 @@ using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; +using OtterGui.Widgets; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; @@ -64,6 +65,7 @@ public class Penumbra : IDalamudPlugin private readonly ConfigWindow _configWindow; private readonly LaunchButton _launchButton; private readonly WindowSystem _windowSystem; + private readonly Changelog _changelog; internal WebServer? WebServer; @@ -99,7 +101,7 @@ public class Penumbra : IDalamudPlugin HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); + SetupInterface( out _configWindow, out _launchButton, out _windowSystem, out _changelog ); if( Config.EnableMods ) { @@ -152,14 +154,15 @@ public class Penumbra : IDalamudPlugin } } - private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) + private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system, out Changelog changelog ) { - cfg = new ConfigWindow( this ); - btn = new LaunchButton( _configWindow ); - system = new WindowSystem( Name ); + cfg = new ConfigWindow( this ); + btn = new LaunchButton( _configWindow ); + system = new WindowSystem( Name ); + changelog = ConfigWindow.CreateChangelog(); system.AddWindow( _configWindow ); system.AddWindow( cfg.ModEditPopup ); - system.AddWindow( ConfigWindow.CreateChangelog() ); + system.AddWindow( changelog ); Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; } @@ -223,6 +226,9 @@ public class Penumbra : IDalamudPlugin public bool SetEnabled( bool enabled ) => enabled ? Enable() : Disable(); + public void ForceChangelogOpen() + => _changelog.ForceOpen = true; + private void SubscribeItemLinks() { Api.ChangedItemTooltip += it => diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 654f020b..03d76923 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -16,17 +16,25 @@ public partial class ConfigWindow } ); Add5_7_0( ret ); + Add5_7_1( ret ); return ret; } + private static void Add5_7_1( Changelog log ) + => log.NextVersion( "Version 0.5.7.1" ) + .RegisterEntry( "Fixed the Changelog window not considering UI Scale correctly." ) + .RegisterEntry( "Reworked Changelog display slightly." ); + private static void Add5_7_0( Changelog log ) => log.NextVersion( "Version 0.5.7.0" ) .RegisterEntry( "Added a Changelog!" ) .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) - .RegisterHighlight( "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", 1 ) + .RegisterHighlight( + "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", + 1 ) .RegisterEntry( "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) .RegisterEntry( diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 242e33b4..2cbd2c24 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -335,7 +335,7 @@ public partial class ConfigWindow + "Not directly affiliated and potentially, but not usually out of date." ); } - private static void DrawSupportButtons() + private void DrawSupportButtons() { var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; var xPos = ImGui.GetWindowWidth() - width; @@ -363,7 +363,7 @@ public partial class ConfigWindow ImGui.SetCursorPos( new Vector2( xPos, 4 * ImGui.GetFrameHeightWithSpacing() ) ); if( ImGui.Button( "Show Changelogs", new Vector2( width, 0 ) ) ) { - Penumbra.Config.LastSeenVersion = 0; + _window._penumbra.ForceChangelogOpen(); } } } From 2404002041f97e88fbf25ca0c0f979ac8daeaa88 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 10 Sep 2022 14:43:57 +0000 Subject: [PATCH 0472/2451] [CI] Updating repo.json for refs/tags/0.5.7.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 05087b4a..f6a1fc63 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.7.0", - "TestingAssemblyVersion": "0.5.7.0", + "AssemblyVersion": "0.5.7.1", + "TestingAssemblyVersion": "0.5.7.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From fabbeeae1363d1d8181cae978cb3d0df9d9ce505 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Sep 2022 13:07:49 +0200 Subject: [PATCH 0473/2451] Fix Actor 201, maybe. --- Penumbra/Interop/Resolver/PathResolver.Identification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index b88c4e88..efa2d31d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -243,7 +243,7 @@ public unsafe partial class PathResolver collection = null; // Check for the Yourself collection. if( actor->ObjectIndex == 0 - || actor->ObjectIndex == ObjectReloader.GPosePlayerIdx && name.Length > 0 + || Cutscenes.GetParentIndex(actor->ObjectIndex) == 0 || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) { collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); From 72ef666d5110618fa92dbbd9231dc779d8d5984b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Sep 2022 15:02:23 +0200 Subject: [PATCH 0474/2451] Use custom logger everywhere. --- OtterGui | 2 +- .../ByteString/ByteStringFunctions.Case.cs | 1 - Penumbra.GameData/Enums/ObjectType.cs | 2 - Penumbra.GameData/GameData.cs | 1 - Penumbra/Api/IPenumbraApi.cs | 4 +- Penumbra/Api/IpcTester.cs | 16 ++- Penumbra/Api/ModsController.cs | 3 +- Penumbra/Api/PenumbraApi.cs | 19 ++-- Penumbra/Api/PenumbraIpc.cs | 107 +++++++++--------- Penumbra/Api/RedrawController.cs | 2 +- Penumbra/Api/TempModManager.cs | 6 +- .../Collections/CollectionManager.Active.cs | 23 ++-- Penumbra/Collections/CollectionManager.cs | 25 ++-- Penumbra/Collections/CollectionType.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 24 ++-- Penumbra/Collections/ModCollection.Cache.cs | 9 +- Penumbra/Collections/ModCollection.Changes.cs | 2 +- Penumbra/Collections/ModCollection.File.cs | 19 ++-- .../Collections/ModCollection.Inheritance.cs | 13 +-- .../Collections/ModCollection.Migration.cs | 2 +- Penumbra/Collections/ModCollection.cs | 4 +- Penumbra/Configuration.Migration.cs | 9 +- Penumbra/Configuration.cs | 5 +- Penumbra/Import/Dds/TextureImporter.cs | 4 +- Penumbra/Import/MetaFileInfo.cs | 2 +- Penumbra/Import/StreamDisposer.cs | 2 +- Penumbra/Import/TexToolsImport.cs | 5 +- Penumbra/Import/TexToolsImporter.Archives.cs | 11 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 13 +-- .../Import/TexToolsMeta.Deserialization.cs | 3 +- Penumbra/Import/TexToolsMeta.Rgsp.cs | 7 +- Penumbra/Import/TexToolsMeta.cs | 3 +- Penumbra/Interop/CharacterUtility.cs | 15 ++- Penumbra/Interop/FontReloader.cs | 3 +- .../Interop/Loader/ResourceLoader.Debug.cs | 16 ++- .../Loader/ResourceLoader.Replacement.cs | 9 +- Penumbra/Interop/Loader/ResourceLogger.cs | 3 +- Penumbra/Interop/ResidentResourceManager.cs | 3 +- .../Resolver/PathResolver.AnimationState.cs | 1 - .../Resolver/PathResolver.DrawObjectState.cs | 1 - .../Resolver/PathResolver.Identification.cs | 3 +- .../Interop/Resolver/PathResolver.Material.cs | 6 +- .../Interop/Resolver/PathResolver.Meta.cs | 1 - .../Resolver/PathResolver.PathState.cs | 1 - .../Resolver/PathResolver.ResolverHooks.cs | 1 - Penumbra/Interop/Resolver/PathResolver.cs | 5 +- Penumbra/Interop/Structs/ResourceHandle.cs | 1 - Penumbra/Meta/Files/EqpGmpFile.cs | 1 - Penumbra/Meta/Files/ImcFile.cs | 9 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 3 - Penumbra/Meta/Manager/MetaManager.Imc.cs | 10 +- Penumbra/Meta/Manager/MetaManager.cs | 3 +- .../Meta/Manipulations/GmpManipulation.cs | 1 - Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 9 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 7 +- Penumbra/Mods/Editor/Mod.Editor.Groups.cs | 3 - .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 5 +- Penumbra/Mods/Editor/ModBackup.cs | 19 ++-- Penumbra/Mods/Editor/ModCleanup.cs | 6 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 19 ++-- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 5 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 8 +- Penumbra/Mods/Mod.BasePath.cs | 5 +- Penumbra/Mods/Mod.Files.cs | 5 +- Penumbra/Mods/Mod.Meta.Migration.cs | 9 +- Penumbra/Mods/Mod.Meta.cs | 8 +- Penumbra/Mods/Mod.TemporaryMod.cs | 6 +- Penumbra/Mods/ModFileSystem.cs | 5 +- Penumbra/Mods/Subclasses/IModGroup.cs | 7 +- .../Subclasses/Mod.Files.MultiModGroup.cs | 4 +- .../Subclasses/Mod.Files.SingleModGroup.cs | 1 - Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 6 +- Penumbra/Penumbra.cs | 16 +-- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 3 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 3 +- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 11 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 10 +- Penumbra/UI/ConfigWindow.Changelog.cs | 1 - ...ConfigWindow.CollectionsTab.Inheritance.cs | 1 - Penumbra/UI/ConfigWindow.CollectionsTab.cs | 1 - Penumbra/UI/ConfigWindow.DebugTab.cs | 1 - Penumbra/UI/ConfigWindow.Misc.cs | 2 - Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 3 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 1 - Penumbra/UI/ConfigWindow.ModsTab.cs | 5 +- Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/Util/FrameworkManager.cs | 3 +- 87 files changed, 276 insertions(+), 371 deletions(-) diff --git a/OtterGui b/OtterGui index 988c8291..44dc1f51 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 988c8291bb09649712bf89334869c1f4f5ce356d +Subproject commit 44dc1f51fe7900a1fe0e3368c4c5c00b6f866325 diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs index e1e0970c..7d9a94f9 100644 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Runtime.InteropServices; using Penumbra.GameData.Util; namespace Penumbra.GameData.ByteString; diff --git a/Penumbra.GameData/Enums/ObjectType.cs b/Penumbra.GameData/Enums/ObjectType.cs index 414497b1..d081e6a6 100644 --- a/Penumbra.GameData/Enums/ObjectType.cs +++ b/Penumbra.GameData/Enums/ObjectType.cs @@ -1,5 +1,3 @@ -using System; - namespace Penumbra.GameData.Enums; public enum ObjectType : byte diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 421b0031..c8885c3c 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 05b56c80..cfc373df 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.Mods; +using System; +using System.Collections.Generic; namespace Penumbra.Api; diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 2bc4ebad..133f5f7e 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,13 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using ImGuiNET; @@ -17,6 +9,12 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Reflection; namespace Penumbra.Api; @@ -124,7 +122,7 @@ public class IpcTester : IDisposable } catch( Exception e ) { - PluginLog.Error( $"Error during IPC Tests:\n{e}" ); + Penumbra.Log.Error( $"Error during IPC Tests:\n{e}" ); } } diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 082bbb25..3432fb86 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using System.Linq; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; +using System.Linq; namespace Penumbra.Api; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 982d5c22..eb1a189b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; using Lumina.Data; using Newtonsoft.Json; using OtterGui; @@ -16,6 +9,12 @@ using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; namespace Penumbra.Api; @@ -206,12 +205,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 ); } - PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); + Penumbra.Log.Warning( $"Collection {collectionName} does not exist or is not loaded." ); return new Dictionary< string, object? >(); } catch( Exception e ) { - PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + Penumbra.Log.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); throw; } } @@ -619,7 +618,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } catch( Exception e ) { - PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + Penumbra.Log.Warning( $"Could not load file {resolvedPath}:\n{e}" ); return null; } } diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index fa9ada66..eb9035e1 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Penumbra.Collections; using Penumbra.GameData.Enums; +using System; +using System.Collections.Generic; namespace Penumbra.Api; @@ -73,7 +72,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); } try @@ -82,7 +81,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); } try @@ -90,13 +89,13 @@ public partial class PenumbraIpc ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); ProviderApiVersion.RegisterFunc( () => { - PluginLog.Warning( $"{LabelProviderApiVersion} is outdated. Please use {LabelProviderApiVersions} instead." ); + Penumbra.Log.Warning( $"{LabelProviderApiVersion} is outdated. Please use {LabelProviderApiVersions} instead." ); return Api.ApiVersion.Breaking; } ); } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); } try @@ -106,7 +105,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderApiVersions}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderApiVersions}:\n{e}" ); } try @@ -116,7 +115,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" ); } try @@ -126,7 +125,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderModDirectoryChanged}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderModDirectoryChanged}:\n{e}" ); } try @@ -136,7 +135,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetConfiguration}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetConfiguration}:\n{e}" ); } try @@ -146,7 +145,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderPreSettingsDraw}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderPreSettingsDraw}:\n{e}" ); } try @@ -156,7 +155,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderPostSettingsDraw}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderPostSettingsDraw}:\n{e}" ); } } @@ -215,7 +214,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); } try @@ -225,7 +224,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); } try @@ -235,7 +234,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); } try @@ -245,7 +244,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); } try @@ -255,7 +254,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGameObjectRedrawn}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGameObjectRedrawn}:\n{e}" ); } } @@ -305,7 +304,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); } try @@ -315,7 +314,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); } try @@ -325,7 +324,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); } try @@ -335,7 +334,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); } try @@ -345,7 +344,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" ); } try @@ -355,7 +354,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePath}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePath}:\n{e}" ); } try @@ -365,7 +364,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePlayerPath}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePlayerPath}:\n{e}" ); } try @@ -376,7 +375,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" ); } try @@ -387,7 +386,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" ); } try @@ -398,7 +397,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGameObjectResourcePathResolved}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGameObjectResourcePathResolved}:\n{e}" ); } } @@ -462,7 +461,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" ); } try @@ -472,7 +471,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } try @@ -482,7 +481,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } } @@ -521,7 +520,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetMods}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetMods}:\n{e}" ); } try @@ -531,7 +530,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCollections}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCollections}:\n{e}" ); } try @@ -541,7 +540,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderCurrentCollectionName}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCurrentCollectionName}:\n{e}" ); } try @@ -551,7 +550,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); } try @@ -561,7 +560,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderCharacterCollectionName}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCharacterCollectionName}:\n{e}" ); } try @@ -571,7 +570,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetPlayerMetaManipulations}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetPlayerMetaManipulations}:\n{e}" ); } try @@ -581,7 +580,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetMetaManipulations}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetMetaManipulations}:\n{e}" ); } } @@ -633,7 +632,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderModSettingChanged}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderModSettingChanged}:\n{e}" ); } try @@ -645,7 +644,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); } try @@ -655,7 +654,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderReloadMod}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReloadMod}:\n{e}" ); } try @@ -665,7 +664,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } try @@ -677,7 +676,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" ); } try @@ -687,7 +686,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" ); } try @@ -697,7 +696,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" ); } try @@ -707,7 +706,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" ); } try @@ -718,7 +717,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" ); } try @@ -729,7 +728,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" ); } } @@ -782,7 +781,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" ); } try @@ -793,7 +792,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" ); } try @@ -805,7 +804,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" ); } try @@ -817,7 +816,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" ); } try @@ -827,7 +826,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" ); } try @@ -837,7 +836,7 @@ public partial class PenumbraIpc } catch( Exception e ) { - PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" ); + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" ); } } diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs index df470a1a..9aef89b7 100644 --- a/Penumbra/Api/RedrawController.cs +++ b/Penumbra/Api/RedrawController.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using Penumbra.GameData.Enums; +using System.Threading.Tasks; namespace Penumbra.Api; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 9fb40ec6..263b892f 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Penumbra.Api; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 81b3d7db..6885cdee 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Mods; using Penumbra.UI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Penumbra.Collections; @@ -197,7 +196,7 @@ public partial class ModCollection var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { - PluginLog.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." ); + Penumbra.Log.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." ); Default = Empty; configChanged = true; } @@ -211,7 +210,7 @@ public partial class ModCollection var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - PluginLog.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); + Penumbra.Log.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); Current = DefaultName; configChanged = true; } @@ -229,7 +228,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName( typeName ); if( idx < 0 ) { - PluginLog.Error( $"Last choice of {type.ToName()} Collection {typeName} is not available, removed." ); + Penumbra.Log.Error( $"Last choice of {type.ToName()} Collection {typeName} is not available, removed." ); configChanged = true; } else @@ -246,7 +245,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName( collectionName ); if( idx < 0 ) { - PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); + Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); _characters.Add( player, Empty ); configChanged = true; } @@ -306,11 +305,11 @@ public partial class ModCollection j.WriteEndObject(); j.WriteEndObject(); - PluginLog.Verbose( "Active Collections saved." ); + Penumbra.Log.Verbose( "Active Collections saved." ); } catch( Exception e ) { - PluginLog.Error( $"Could not save active collections to file {file}:\n{e}" ); + Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" ); } } @@ -328,7 +327,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); + Penumbra.Log.Error( $"Could not read active collections from file {file}:\n{e}" ); } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 9bc2138c..961478b5 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -1,13 +1,12 @@ +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Mods; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Dalamud.Logging; -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Mods; namespace Penumbra.Collections; @@ -107,7 +106,7 @@ public partial class ModCollection { if( !CanAddCollection( name, out var fixedName ) ) { - PluginLog.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." ); + Penumbra.Log.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." ); return false; } @@ -115,7 +114,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); - PluginLog.Debug( "Added collection {Name:l}.", newCollection.AnonymizedName ); + Penumbra.Log.Debug( $"Added collection {newCollection.AnonymizedName}." ); CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); SetCollection( newCollection.Index, CollectionType.Current ); return true; @@ -128,13 +127,13 @@ public partial class ModCollection { if( idx <= Empty.Index || idx >= _collections.Count ) { - PluginLog.Error( "Can not remove the empty collection." ); + Penumbra.Log.Error( "Can not remove the empty collection." ); return false; } if( idx == DefaultName.Index ) { - PluginLog.Error( "Can not remove the default collection." ); + Penumbra.Log.Error( "Can not remove the default collection." ); return false; } @@ -179,7 +178,7 @@ public partial class ModCollection } } - PluginLog.Debug( "Removed collection {Name:l}.", collection.AnonymizedName ); + Penumbra.Log.Debug( $"Removed collection {collection.AnonymizedName}." ); CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); return true; } @@ -335,12 +334,12 @@ public partial class ModCollection if( !ByName( subCollectionName, out var subCollection ) ) { changes = true; - PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + Penumbra.Log.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); } else if( !collection.AddInheritance( subCollection ) ) { changes = true; - PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + Penumbra.Log.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); } } @@ -370,12 +369,12 @@ public partial class ModCollection if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + Penumbra.Log.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); } if( this[ collection.Name ] != null ) { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + Penumbra.Log.Warning( $"Duplicate collection found: {collection.Name} already exists." ); } else { diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 3aabea1f..2946498d 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -1,6 +1,6 @@ +using Penumbra.GameData.Enums; using System; using System.Linq; -using Penumbra.GameData.Enums; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index fdc65e4b..918d361c 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading; -using Dalamud.Logging; using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; namespace Penumbra.Collections; @@ -29,7 +27,7 @@ public partial class ModCollection if( _cache == null ) { CalculateEffectiveFileList(); - PluginLog.Verbose( "Created new cache for collection {Name:l}.", Name ); + Penumbra.Log.Verbose( $"Created new cache for collection {Name}." ); } } @@ -61,7 +59,7 @@ public partial class ModCollection { _cache?.Dispose(); _cache = null; - PluginLog.Verbose( "Cleared cache of collection {Name:l}.", Name ); + Penumbra.Log.Verbose( $"Cleared cache of collection {Name}." ); } public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath path ) @@ -87,7 +85,7 @@ public partial class ModCollection return true; } - PluginLog.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" ); + Penumbra.Log.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" ); return false; } @@ -125,13 +123,11 @@ public partial class ModCollection return; } - PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l}", - Thread.CurrentThread.ManagedThreadId, AnonymizedName ); + Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}" ); _cache ??= new Cache( this ); _cache.FullRecalculation(); - PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", - Thread.CurrentThread.ManagedThreadId, AnonymizedName ); + Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished." ); } // Set Metadata files. @@ -204,7 +200,7 @@ public partial class ModCollection else { _cache.MetaManipulations.SetFiles(); - PluginLog.Debug( "Set CharacterUtility resources for collection {Name:l}.", Name ); + Penumbra.Log.Debug( $"Set CharacterUtility resources for collection {Name}." ); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index dbb07b95..9d0be99a 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Logging; using OtterGui; using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.Linq; namespace Penumbra.Collections; @@ -496,7 +495,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Unknown Error:\n{e}" ); + Penumbra.Log.Error( $"Unknown Error:\n{e}" ); } } } diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index b2c9ffd5..32826dc8 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,7 +1,7 @@ +using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Mods; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 6f76199b..a9cef608 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -1,13 +1,12 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.Mods; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using Dalamud.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; -using Penumbra.Mods; namespace Penumbra.Collections; @@ -67,7 +66,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Could not save collection {AnonymizedName}:\n{e}" ); + Penumbra.Log.Error( $"Could not save collection {AnonymizedName}:\n{e}" ); } } @@ -90,11 +89,11 @@ public partial class ModCollection try { file.Delete(); - PluginLog.Information( "Deleted collection file for {Name:l}.", AnonymizedName ); + Penumbra.Log.Information( $"Deleted collection file for {AnonymizedName}." ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete collection file for {AnonymizedName}:\n{e}" ); + Penumbra.Log.Error( $"Could not delete collection file for {AnonymizedName}:\n{e}" ); } } @@ -105,7 +104,7 @@ public partial class ModCollection inheritance = Array.Empty< string >(); if( !file.Exists ) { - PluginLog.Error( $"Could not read collection because file does not exist." ); + Penumbra.Log.Error( "Could not read collection because file does not exist." ); return null; } @@ -123,7 +122,7 @@ public partial class ModCollection } catch( Exception e ) { - PluginLog.Error( $"Could not read collection information from file:\n{e}" ); + Penumbra.Log.Error( $"Could not read collection information from file:\n{e}" ); } return null; diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 12b5c054..16415a09 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,9 +1,8 @@ +using OtterGui.Filesystem; +using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; -using Dalamud.Logging; -using OtterGui.Filesystem; -using Penumbra.Mods; namespace Penumbra.Collections; @@ -81,7 +80,7 @@ public partial class ModCollection collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Added {InheritedName:l} to {Name:l} inheritances.", collection.AnonymizedName, AnonymizedName ); + Penumbra.Log.Debug( $"Added {collection.AnonymizedName} to {AnonymizedName} inheritances." ); return true; } @@ -91,7 +90,7 @@ public partial class ModCollection ClearSubscriptions( inheritance ); _inheritance.RemoveAt( idx ); InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Removed {InheritedName:l} from {Name:l} inheritances.", inheritance.AnonymizedName, AnonymizedName ); + Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." ); } private void ClearSubscriptions( ModCollection other ) @@ -106,7 +105,7 @@ public partial class ModCollection if( _inheritance.Move( from, to ) ) { InheritanceChanged.Invoke( false ); - PluginLog.Debug( "Moved {Name:l}s inheritance {From} to {To}.", AnonymizedName, from, to ); + Penumbra.Log.Debug( $"Moved {AnonymizedName}s inheritance {from} to {to}." ); } } @@ -122,7 +121,7 @@ public partial class ModCollection default: if( modIdx < 0 || modIdx >= _settings.Count ) { - PluginLog.Warning( + Penumbra.Log.Warning( $"Collection state broken, Mod {modIdx} in inheritance does not exist. ({_settings.Count} mods exist)." ); return; } diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 74215dd8..bc940651 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,6 +1,6 @@ +using Penumbra.Mods; using System.Collections.Generic; using System.Linq; -using Penumbra.Mods; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 0ffff1b5..b01a8fad 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,8 +1,8 @@ +using OtterGui.Filesystem; +using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; -using OtterGui.Filesystem; -using Penumbra.Mods; namespace Penumbra.Collections; diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 3eca3c07..cd11594b 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; @@ -126,7 +125,7 @@ public partial class Configuration } catch( Exception e ) { - PluginLog.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" ); + Penumbra.Log.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" ); } } } @@ -153,7 +152,7 @@ public partial class Configuration } catch( Exception e ) { - PluginLog.Error( + Penumbra.Log.Error( $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" ); } } @@ -258,7 +257,7 @@ public partial class Configuration } catch( Exception e ) { - PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); + Penumbra.Log.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); throw; } } @@ -274,7 +273,7 @@ public partial class Configuration } catch( Exception e ) { - PluginLog.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); + Penumbra.Log.Error( $"Could not create backup copy of config at {bakName}:\n{e}" ); } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9b6381a7..b60e42b3 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Configuration; -using Dalamud.Logging; using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; @@ -78,7 +77,7 @@ public partial class Configuration : IPluginConfiguration { void HandleDeserializationError( object? sender, ErrorEventArgs errorArgs ) { - PluginLog.Error( + Penumbra.Log.Error( $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}" ); errorArgs.ErrorContext.Handled = true; } @@ -116,7 +115,7 @@ public partial class Configuration : IPluginConfiguration } catch( Exception e ) { - PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); + Penumbra.Log.Error( $"Could not save plugin configuration:\n{e}" ); } } diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs index e029632a..7e62830f 100644 --- a/Penumbra/Import/Dds/TextureImporter.cs +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -1,8 +1,8 @@ -using System; -using System.IO; using Lumina.Data.Files; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; namespace Penumbra.Import.Dds; diff --git a/Penumbra/Import/MetaFileInfo.cs b/Penumbra/Import/MetaFileInfo.cs index 5393fa6c..53c23496 100644 --- a/Penumbra/Import/MetaFileInfo.cs +++ b/Penumbra/Import/MetaFileInfo.cs @@ -1,6 +1,6 @@ -using System.Text.RegularExpressions; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using System.Text.RegularExpressions; namespace Penumbra.Import; diff --git a/Penumbra/Import/StreamDisposer.cs b/Penumbra/Import/StreamDisposer.cs index 09300ed1..fb5ccef4 100644 --- a/Penumbra/Import/StreamDisposer.cs +++ b/Penumbra/Import/StreamDisposer.cs @@ -1,6 +1,6 @@ +using Penumbra.Util; using System; using System.IO; -using Penumbra.Util; namespace Penumbra.Import; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 98c1df0b..fce84f92 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.Mods; using FileMode = System.IO.FileMode; @@ -142,7 +141,7 @@ public partial class TexToolsImporter : IDisposable { if( modPackFile.Extension != ".ttmp2" ) { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); + Penumbra.Log.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." ); } return ImportV2ModPack( modPackFile, extractedModPack, modRaw ); @@ -150,7 +149,7 @@ public partial class TexToolsImporter : IDisposable if( modPackFile.Extension != ".ttmp" ) { - PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); + Penumbra.Log.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." ); } return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 12f7428f..23b63ee0 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; -using Dalamud.Logging; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -13,6 +9,9 @@ using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Readers; +using System; +using System.IO; +using System.Linq; namespace Penumbra.Import; @@ -42,7 +41,7 @@ public partial class TexToolsImporter SevenZipArchive s => s.Entries.Count, _ => archive.Entries.Count(), }; - PluginLog.Log( $" -> Importing {archive.Type} Archive." ); + Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." ); _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); var options = new ExtractionOptions() @@ -65,7 +64,7 @@ public partial class TexToolsImporter continue; } - PluginLog.Log( " -> Extracting {0}", reader.Entry.Key ); + Penumbra.Log.Information( $" -> Extracting {reader.Entry.Key}" ); reader.WriteEntryToDirectory( _currentModDirectory.FullName, options ); ++_currentFileIdx; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 574efb1f..5227b454 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.Mods; using Penumbra.Util; @@ -24,7 +23,7 @@ public partial class TexToolsImporter _currentGroupName = string.Empty; _currentOptionName = DefaultTexToolsData.DefaultOption; - PluginLog.Log( " -> Importing V1 ModPack" ); + Penumbra.Log.Information( " -> Importing V1 ModPack" ); var modListRaw = modRaw.Split( new[] { "\r\n", "\r", "\n" }, @@ -62,12 +61,12 @@ public partial class TexToolsImporter try { - PluginLog.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." ); + Penumbra.Log.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." ); return ImportSimpleV2ModPack( extractedModPack, modList ); } catch( Exception e1 ) { - PluginLog.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" ); + Penumbra.Log.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" ); try { return ImportExtendedV2ModPack( extractedModPack, modRaw ); @@ -87,7 +86,7 @@ public partial class TexToolsImporter _currentModName = modList.Name; _currentGroupName = string.Empty; _currentOptionName = DefaultTexToolsData.DefaultOption; - PluginLog.Log( " -> Importing Simple V2 ModPack" ); + Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); _currentModDirectory = Mod.CreateModFolder( _baseDirectory, _currentModName ); Mod.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) @@ -128,7 +127,7 @@ public partial class TexToolsImporter private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw ) { _currentOptionIdx = 0; - PluginLog.Log( " -> Importing Extended V2 ModPack" ); + Penumbra.Log.Information( " -> Importing Extended V2 ModPack" ); var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!; _currentNumOptions = GetOptionCount( modList ); @@ -243,7 +242,7 @@ public partial class TexToolsImporter return; } - PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) ); + Penumbra.Log.Information( $" -> Extracting {mod.FullPath} at {mod.ModOffset:X}" ); _token.ThrowIfCancellationRequested(); var data = stream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset ); diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 5e79659b..e2428cbb 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using Dalamud.Logging; using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -166,7 +165,7 @@ public partial class TexToolsMeta } catch( Exception e ) { - PluginLog.Warning( + Penumbra.Log.Warning( $"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 97ce6915..cc4055ad 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -14,7 +13,7 @@ public partial class TexToolsMeta { if( data.Length != 45 && data.Length != 42 ) { - PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); + Penumbra.Log.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); return Invalid; } @@ -32,7 +31,7 @@ public partial class TexToolsMeta var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); + Penumbra.Log.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); return Invalid; } @@ -40,7 +39,7 @@ public partial class TexToolsMeta var gender = br.ReadByte(); if( gender != 1 && gender != 0 ) { - PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); + Penumbra.Log.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); return Invalid; } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 1a0f05fd..0058764c 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using Dalamud.Logging; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -72,7 +71,7 @@ public partial class TexToolsMeta catch( Exception e ) { FilePath = ""; - PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); + Penumbra.Log.Error( $"Error while parsing .meta file:\n{e}" ); } } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 1175eb52..84dc750c 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Dalamud.Logging; using Dalamud.Utility.Signatures; namespace Penumbra.Interop; @@ -50,7 +49,7 @@ public unsafe class CharacterUtility : IDisposable public CharacterUtility() { SignatureHelper.Initialise( this ); - LoadingFinished += () => PluginLog.Debug( "Loading of CharacterUtility finished." ); + LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." ); LoadDefaultResources( null! ); if( !Ready ) { @@ -97,13 +96,13 @@ public unsafe class CharacterUtility : IDisposable { if( !Ready ) { - PluginLog.Error( $"Can not set resource {resourceIdx}: CharacterUtility not ready yet." ); + Penumbra.Log.Error( $"Can not set resource {resourceIdx}: CharacterUtility not ready yet." ); return false; } var resource = Address->Resource( resourceIdx ); var ret = resource->SetData( data, length ); - PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); + Penumbra.Log.Verbose( $"Set resource {resourceIdx} to 0x{( ulong )data:X} ({length} bytes)."); return ret; } @@ -112,13 +111,13 @@ public unsafe class CharacterUtility : IDisposable { if( !Ready ) { - PluginLog.Error( $"Can not reset {resourceIdx}: CharacterUtility not ready yet." ); + Penumbra.Log.Error( $"Can not reset {resourceIdx}: CharacterUtility not ready yet." ); return; } var (data, length) = DefaultResource( resourceIdx); var resource = Address->Resource( resourceIdx ); - PluginLog.Verbose( "Reset resource {Idx} to default at 0x{DefaultData:X} ({NewLength} bytes).", resourceIdx, ( ulong )data, length ); + Penumbra.Log.Verbose( $"Reset resource {resourceIdx} to default at 0x{(ulong)data:X} ({length} bytes)."); resource->SetData( data, length ); } @@ -127,7 +126,7 @@ public unsafe class CharacterUtility : IDisposable { if( !Ready ) { - PluginLog.Error( "Can not reset all resources: CharacterUtility not ready yet." ); + Penumbra.Log.Error( "Can not reset all resources: CharacterUtility not ready yet." ); return; } @@ -136,7 +135,7 @@ public unsafe class CharacterUtility : IDisposable ResetResource( idx ); } - PluginLog.Debug( "Reset all CharacterUtility resources to default." ); + Penumbra.Log.Debug( "Reset all CharacterUtility resources to default." ); } public void Dispose() diff --git a/Penumbra/Interop/FontReloader.cs b/Penumbra/Interop/FontReloader.cs index aad57199..e084f70c 100644 --- a/Penumbra/Interop/FontReloader.cs +++ b/Penumbra/Interop/FontReloader.cs @@ -1,4 +1,3 @@ -using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -22,7 +21,7 @@ public static unsafe class FontReloader } else { - PluginLog.Error( "Could not reload fonts, function could not be found." ); + Penumbra.Log.Error( "Could not reload fonts, function could not be found." ); } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index ada91c0a..f04f2930 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -20,7 +19,7 @@ public unsafe partial class ResourceLoader private readonly Hook< ResourceHandleDecRef > _decRefHook; public delegate IntPtr ResourceHandleDestructor( ResourceHandle* handle ); - + [Signature( "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8", DetourName = nameof( ResourceHandleDestructorDetour ) )] public static Hook< ResourceHandleDestructor >? ResourceHandleDestructorHook; @@ -29,8 +28,7 @@ public unsafe partial class ResourceLoader { if( handle != null ) { - PluginLog.Information( "[ResourceLoader] Destructing Resource Handle {Path:l} at 0x{Address:X} (Refcount {Refcount}).", - handle->FileName, ( ulong )handle, handle->RefCount ); + Penumbra.Log.Information( $"[ResourceLoader] Destructing Resource Handle {handle->FileName} at 0x{( ulong )handle:X} (Refcount {handle->RefCount})."); } return ResourceHandleDestructorHook!.Original( handle ); @@ -95,7 +93,7 @@ public unsafe partial class ResourceLoader } catch( Exception e ) { - PluginLog.Error( e.ToString() ); + Penumbra.Log.Error( e.ToString() ); } } @@ -236,22 +234,22 @@ public unsafe partial class ResourceLoader return _decRefHook.Original( handle ); } - PluginLog.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); + Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); return 1; } // Logging functions for EnableFullLogging. private static void LogPath( Utf8GamePath path, bool synchronous ) - => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); + => Penumbra.Log.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); - PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); + Penumbra.Log.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); } private static void LogLoadedFile( Utf8String path, bool success, bool custom ) - => PluginLog.Information( success + => Penumbra.Log.Information( success ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}" : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); } \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index afd18d2a..f0cd656b 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; @@ -68,7 +65,7 @@ public unsafe partial class ResourceLoader { if( local != game ) { - PluginLog.Warning( "Hash function appears to have changed. Computed {Hash1:X8} vs Game {Hash2:X8} for {Path}.", local, game, path ); + Penumbra.Log.Warning( $"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}." ); } } @@ -79,7 +76,7 @@ public unsafe partial class ResourceLoader { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) { - PluginLog.Error( "Could not create GamePath from resource path." ); + Penumbra.Log.Error( "Could not create GamePath from resource path." ); return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); } @@ -161,7 +158,7 @@ public unsafe partial class ResourceLoader if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) { - PluginLog.Error( "Failure to load file from SqPack: invalid File Descriptor." ); + Penumbra.Log.Error( "Failure to load file from SqPack: invalid File Descriptor." ); return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } diff --git a/Penumbra/Interop/Loader/ResourceLogger.cs b/Penumbra/Interop/Loader/ResourceLogger.cs index 278aea7d..25d8ea59 100644 --- a/Penumbra/Interop/Loader/ResourceLogger.cs +++ b/Penumbra/Interop/Loader/ResourceLogger.cs @@ -1,6 +1,5 @@ using System; using System.Text.RegularExpressions; -using Dalamud.Logging; using Penumbra.GameData.ByteString; namespace Penumbra.Interop.Loader; @@ -79,7 +78,7 @@ public class ResourceLogger : IDisposable var path = Match( data.Path ); if( path != null ) { - PluginLog.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); + Penumbra.Log.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" ); } } diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs index f533a7ba..37ed1bfe 100644 --- a/Penumbra/Interop/ResidentResourceManager.cs +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -1,4 +1,3 @@ -using Dalamud.Logging; using Dalamud.Utility.Signatures; namespace Penumbra.Interop; @@ -31,7 +30,7 @@ public unsafe class ResidentResourceManager { if( Address != null && Address->NumResources > 0 ) { - PluginLog.Debug( "Reload of resident resources triggered." ); + Penumbra.Log.Debug( "Reload of resident resources triggered." ); UnloadPlayerResources.Invoke( Address ); LoadPlayerResources.Invoke( Address ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 7211d109..9a38bf56 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using Penumbra.Collections; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 7c9690fc..e681a364 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -3,7 +3,6 @@ using Dalamud.Utility.Signatures; using Penumbra.Collections; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index efa2d31d..8b038a54 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -203,7 +202,7 @@ public unsafe partial class PathResolver } catch( Exception e ) { - PluginLog.Error( $"Error identifying collection:\n{e}" ); + Penumbra.Log.Error( $"Error identifying collection:\n{e}" ); return Penumbra.CollectionManager.Default.ToResolveData( gameObject ); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 6e81dbac..baf333f2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,8 +1,6 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; @@ -95,7 +93,7 @@ public unsafe partial class PathResolver || Penumbra.CollectionManager.ByName( name, out collection ) ) { #if DEBUG - PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); + Penumbra.Log.Verbose( $"Using MtrlLoadHandler with collection {name} for path {path}." ); #endif var objFromObjTable = Dalamud.Objects.FirstOrDefault( f => f.Name.TextValue == name ); @@ -105,7 +103,7 @@ public unsafe partial class PathResolver else { #if DEBUG - PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); + Penumbra.Log.Verbose( $"Using MtrlLoadHandler with no collection for path {path}." ); #endif } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index e305b6db..94d760e0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -2,7 +2,6 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Collections; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Interop/Resolver/PathResolver.PathState.cs b/Penumbra/Interop/Resolver/PathResolver.PathState.cs index f5c11771..a9bd1ee0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.PathState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.PathState.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Dalamud.Utility.Signatures; using Penumbra.Collections; diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 02729c07..d847480e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -2,7 +2,6 @@ using System; using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Collections; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 06536b30..0fda162c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -84,7 +83,7 @@ public partial class PathResolver : IDisposable _materials.Enable(); _loader.ResolvePathCustomization += CharacterResolver; - PluginLog.Debug( "Character Path Resolver enabled." ); + Penumbra.Log.Debug( "Character Path Resolver enabled." ); } public void Disable() @@ -103,7 +102,7 @@ public partial class PathResolver : IDisposable _materials.Disable(); _loader.ResolvePathCustomization -= CharacterResolver; - PluginLog.Debug( "Character Path Resolver disabled." ); + Penumbra.Log.Debug( "Character Path Resolver disabled." ); } public void Dispose() diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 88f38dca..6b3a8a72 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -2,7 +2,6 @@ using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.Enums; -using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index e6f48a3d..60649952 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Numerics; -using System.Runtime.CompilerServices; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index b6e631a3..dc9fe001 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,6 +1,5 @@ using System; using System.Numerics; -using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -125,7 +124,7 @@ public unsafe class ImcFile : MetaBaseFile if( ActualLength > Length ) { var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; - PluginLog.Verbose( "Resized IMC {Path} from {Length} to {NewLength}.", Path, Length, newLength ); + Penumbra.Log.Verbose( $"Resized IMC {Path} from {Length} to {newLength}." ); ResizeResources( newLength ); } @@ -135,7 +134,7 @@ public unsafe class ImcFile : MetaBaseFile Functions.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) ); } - PluginLog.Verbose( "Expanded IMC {Path} from {Count} to {NewCount} variants.", Path, oldCount, numVariants ); + Penumbra.Log.Verbose( $"Expanded IMC {Path} from {oldCount} to {numVariants} variants." ); return true; } @@ -151,7 +150,7 @@ public unsafe class ImcFile : MetaBaseFile var variantPtr = VariantPtr( Data, partIdx, variantIdx ); if( variantPtr == null ) { - PluginLog.Error( "Error during expansion of imc file." ); + Penumbra.Log.Error( "Error during expansion of imc file." ); return false; } @@ -222,7 +221,7 @@ public unsafe class ImcFile : MetaBaseFile var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( ActualLength, 8 ); if( newData == null ) { - PluginLog.Error("Could not replace loaded IMC data at 0x{Data:X}, allocation failed." ); + Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong) resource:X}, allocation failed." ); return; } Functions.MemCpyUnchecked( newData, Data, ActualLength ); diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index df35cace..91554e6a 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -1,12 +1,9 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using OtterGui.Filesystem; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 517681ff..bdbf9512 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Filesystem; -using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -81,7 +78,7 @@ public partial class MetaManager catch( Exception e ) { ++Penumbra.ImcExceptions; - PluginLog.Error( $"Could not apply IMC Manipulation:\n{e}" ); + Penumbra.Log.Error( $"Could not apply IMC Manipulation:\n{e}" ); return false; } } @@ -156,7 +153,7 @@ public partial class MetaManager return false; } - PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); + Penumbra.Log.Verbose( $"Using ImcLoadHandler for path {path}." ); ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); var lastUnderscore = split.LastIndexOf( ( byte )'_' ); @@ -166,8 +163,7 @@ public partial class MetaManager && collection.HasCache && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) { - PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, - collection.AnonymizedName ); + Penumbra.Log.Debug( $"Loaded {path} from file and replaced with IMC from collection {collection.AnonymizedName}." ); file.Replace( fileDescriptor->ResourceHandle ); } diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index aa46e7f7..fde0614c 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; -using Dalamud.Logging; using Penumbra.Collections; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; @@ -165,7 +164,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } Penumbra.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - PluginLog.Debug( "{Collection}: Loaded {Num} delayed meta manipulations.", _collection.Name, loaded ); + Penumbra.Log.Debug( $"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations." ); } [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index f686ec31..9890f113 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 486da507..0f9ef463 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; -using Dalamud.Logging; using Penumbra.GameData.ByteString; namespace Penumbra.Mods; @@ -87,7 +86,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" ); + Penumbra.Log.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" ); } } @@ -100,7 +99,7 @@ public partial class Mod } changes = true; - PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to ); + Penumbra.Log.Debug( $"[DeleteDuplicates] Changing {key} for {_mod.Name}\n : {from}\n -> {to}" ); return to; } @@ -263,7 +262,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" ); } } @@ -282,7 +281,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" ); + Penumbra.Log.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" ); } } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index ab610f5d..480261ee 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using Dalamud.Logging; using Penumbra.GameData.ByteString; namespace Penumbra.Mods; @@ -114,7 +113,7 @@ public partial class Mod return true; } - PluginLog.Debug( "[RemoveMissingPaths] Removing {GamePath} -> {File} from {Mod}.", key, file, _mod.Name ); + Penumbra.Log.Debug( $"[RemoveMissingPaths] Removing {key} -> {file} from {_mod.Name}." ); return false; } @@ -257,12 +256,12 @@ public partial class Mod try { File.Delete( file.File.FullName ); - PluginLog.Debug( "[DeleteFiles] Deleted {File} from {Mod}.", file.File.FullName, _mod.Name ); + Penumbra.Log.Debug( $"[DeleteFiles] Deleted {file.File.FullName} from {_mod.Name}." ); ++deletions; } catch( Exception e ) { - PluginLog.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" ); + Penumbra.Log.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" ); } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs index 9f2a35e9..2e1a4ab2 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Groups.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Groups.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using Penumbra.GameData.ByteString; - namespace Penumbra.Mods; public partial class Mod diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index 5c1f2c7e..3061d390 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Dalamud.Logging; using OtterGui; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -90,7 +89,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" ); + Penumbra.Log.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" ); } } } @@ -152,7 +151,7 @@ public partial class Mod catch( Exception e ) { Restore(); - PluginLog.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" ); } } diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index fa353fd4..48bbc4fa 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.IO.Compression; using System.Threading.Tasks; -using Dalamud.Logging; namespace Penumbra.Mods; @@ -41,11 +40,11 @@ public class ModBackup { File.Delete( zipName ); } - PluginLog.Information( $"Migrated mod backup from {zipName} to {pmpName}." ); + Penumbra.Log.Information( $"Migrated mod backup from {zipName} to {pmpName}." ); } catch( Exception e ) { - PluginLog.Warning( $"Could not migrate mod backup of {mod.ModPath} from .pmp to .zip:\n{e}" ); + Penumbra.Log.Warning( $"Could not migrate mod backup of {mod.ModPath} from .pmp to .zip:\n{e}" ); } } } @@ -72,11 +71,11 @@ public class ModBackup { Delete(); ZipFile.CreateFromDirectory( _mod.ModPath.FullName, Name, CompressionLevel.Optimal, false ); - PluginLog.Debug( "Created backup file {backupName} from {modDirectory}.", Name, _mod.ModPath.FullName ); + Penumbra.Log.Debug( $"Created backup file {Name} from {_mod.ModPath.FullName}."); } catch( Exception e ) { - PluginLog.Error( $"Could not backup mod {_mod.Name} to \"{Name}\":\n{e}" ); + Penumbra.Log.Error( $"Could not backup mod {_mod.Name} to \"{Name}\":\n{e}" ); } } @@ -91,11 +90,11 @@ public class ModBackup try { File.Delete( Name ); - PluginLog.Debug( "Deleted backup file {backupName}.", Name ); + Penumbra.Log.Debug( $"Deleted backup file {Name}." ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete file \"{Name}\":\n{e}" ); + Penumbra.Log.Error( $"Could not delete file \"{Name}\":\n{e}" ); } } @@ -108,16 +107,16 @@ public class ModBackup if( Directory.Exists( _mod.ModPath.FullName ) ) { Directory.Delete( _mod.ModPath.FullName, true ); - PluginLog.Debug( "Deleted mod folder {modFolder}.", _mod.ModPath.FullName ); + Penumbra.Log.Debug( $"Deleted mod folder {_mod.ModPath.FullName}." ); } ZipFile.ExtractToDirectory( Name, _mod.ModPath.FullName ); - PluginLog.Debug( "Extracted backup file {backupName} to {modName}.", Name, _mod.ModPath.FullName ); + Penumbra.Log.Debug( $"Extracted backup file {Name} to {_mod.ModPath.FullName}."); Penumbra.ModManager.ReloadMod( _mod.Index ); } catch( Exception e ) { - PluginLog.Error( $"Could not restore {_mod.Name} from backup \"{Name}\":\n{e}" ); + Penumbra.Log.Error( $"Could not restore {_mod.Name} from backup \"{Name}\":\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModCleanup.cs b/Penumbra/Mods/Editor/ModCleanup.cs index c19c1b1f..a2e7dec0 100644 --- a/Penumbra/Mods/Editor/ModCleanup.cs +++ b/Penumbra/Mods/Editor/ModCleanup.cs @@ -274,7 +274,7 @@ public partial class Mod // } // catch( Exception e ) // { -// PluginLog.Error( $"Could not split Mod:\n{e}" ); +// Penumbra.Log.Error( $"Could not split Mod:\n{e}" ); // } // } // @@ -394,7 +394,7 @@ public partial class Mod // } // } // -// PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); +// Penumbra.Log.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); // f2.Delete(); // } // @@ -498,7 +498,7 @@ public partial class Mod // } // catch( Exception e ) // { -// PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); +// Penumbra.Log.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); // return false; // } // diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index e9a2d104..0d908866 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using Dalamud.Logging; namespace Penumbra.Mods; @@ -34,7 +33,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" ); + Penumbra.Log.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" ); return; } @@ -55,7 +54,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" ); + Penumbra.Log.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" ); return; } @@ -63,7 +62,7 @@ public partial class Mod mod.ModPath = dir; if( !mod.Reload( out var metaChange ) ) { - PluginLog.Error( $"Error reloading moved mod {mod.Name}." ); + Penumbra.Log.Error( $"Error reloading moved mod {mod.Name}." ); return; } @@ -81,10 +80,10 @@ public partial class Mod var mod = this[ idx ]; var oldName = mod.Name; - ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); + ModPathChanged.Invoke( ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); if( !mod.Reload( out var metaChange ) ) { - PluginLog.Warning( mod.Name.Length == 0 + Penumbra.Log.Warning( mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead." ); @@ -110,11 +109,11 @@ public partial class Mod try { Directory.Delete( mod.ModPath.FullName, true ); - PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.ModPath.FullName, mod.Name ); + Penumbra.Log.Debug( $"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); } catch( Exception e ) { - PluginLog.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" ); + Penumbra.Log.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" ); } } @@ -125,7 +124,7 @@ public partial class Mod --remainingMod.Index; } - PluginLog.Debug( "Deleted mod {Name:l}.", mod.Name ); + Penumbra.Log.Debug( $"Deleted mod {mod.Name}." ); } // Load a new mod and add it to the manager if successful. @@ -145,7 +144,7 @@ public partial class Mod mod.Index = _mods.Count; _mods.Add( mod ); ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.ModPath ); - PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName ); + Penumbra.Log.Debug( $"Added new mod {mod.Name} from {modFolder.FullName}." ); } public enum NewDirectoryState diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index b5d3f36a..7c159820 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Dalamud.Logging; using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.ByteString; @@ -201,7 +200,7 @@ public sealed partial class Mod var group = mod._groups[ groupIdx ]; if( group.Count > 63 ) { - PluginLog.Error( + Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + "since only up to 64 options are supported in one group." ); return; @@ -307,7 +306,7 @@ public sealed partial class Mod { if( message ) { - PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); + Penumbra.Log.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); } return false; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index cec734ee..2bf658c2 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using Dalamud.Logging; namespace Penumbra.Mods; @@ -53,7 +52,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); } } @@ -68,8 +67,7 @@ public sealed partial class Mod private static void OnModDirectoryChange( string newPath, bool _ ) { - PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.", - Penumbra.Config.ModDirectory, newPath ); + Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." ); Penumbra.Config.ModDirectory = newPath; Penumbra.Config.Save(); } @@ -98,7 +96,7 @@ public sealed partial class Mod } ModDiscoveryFinished?.Invoke(); - PluginLog.Information( "Rediscovered mods." ); + Penumbra.Log.Information( "Rediscovered mods." ); if( MigrateModBackups ) { diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 2e01396f..2456dec2 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -1,5 +1,4 @@ using System.IO; -using Dalamud.Logging; namespace Penumbra.Mods; @@ -32,7 +31,7 @@ public partial class Mod modPath.Refresh(); if( !modPath.Exists ) { - PluginLog.Error( $"Supplied mod directory {modPath} does not exist." ); + Penumbra.Log.Error( $"Supplied mod directory {modPath} does not exist." ); return null; } @@ -40,7 +39,7 @@ public partial class Mod if( !mod.Reload( out _ ) ) { // Can not be base path not existing because that is checked before. - PluginLog.Error( $"Mod at {modPath} without name is not supported." ); + Penumbra.Log.Error( $"Mod at {modPath} without name is not supported." ); return null; } diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 87c69a82..c9c8683b 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.GameData.ByteString; @@ -88,7 +87,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not read mod group from {file.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not read mod group from {file.FullName}:\n{e}" ); } return null; @@ -133,7 +132,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not delete outdated group file {file}:\n{e}" ); + Penumbra.Log.Error( $"Could not delete outdated group file {file}:\n{e}" ); } } diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 1587864f..f6bba3a3 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; @@ -37,7 +36,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); + Penumbra.Log.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); } } @@ -69,7 +68,7 @@ public sealed partial class Mod if( unusedFile.ToGamePath( mod.ModPath, out var gamePath ) && !mod._default.FileData.TryAdd( gamePath, unusedFile ) ) { - PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); + Penumbra.Log.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." ); } } @@ -95,7 +94,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Warning( $"Could not delete meta file {file.FullName} during migration:\n{e}" ); + Penumbra.Log.Warning( $"Could not delete meta file {file.FullName} during migration:\n{e}" ); } } @@ -109,7 +108,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Warning( $"Could not delete old meta file {oldMetaFile} during migration:\n{e}" ); + Penumbra.Log.Warning( $"Could not delete old meta file {oldMetaFile} during migration:\n{e}" ); } } diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 615e0dc1..21db4857 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -1,9 +1,7 @@ using System; using System.IO; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; namespace Penumbra.Mods; @@ -48,7 +46,7 @@ public sealed partial class Mod var metaFile = MetaFile; if( !File.Exists( metaFile.FullName ) ) { - PluginLog.Debug( "No mod meta found for {ModLocation}.", ModPath.Name ); + Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." ); return MetaChangeType.Deletion; } @@ -115,7 +113,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not load mod meta:\n{e}" ); + Penumbra.Log.Error( $"Could not load mod meta:\n{e}" ); return MetaChangeType.Deletion; } } @@ -142,7 +140,7 @@ public sealed partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); } } diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 7b55b994..c41dcf57 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.ByteString; @@ -91,11 +89,11 @@ public sealed partial class Mod mod.SaveDefaultMod(); Penumbra.ModManager.AddMod( dir ); - PluginLog.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); + Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." ); } catch( Exception e ) { - PluginLog.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); + Penumbra.Log.Error( $"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}" ); if( dir != null && Directory.Exists( dir.FullName ) ) { try diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 0b186ea4..1060504c 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Logging; using OtterGui.Filesystem; namespace Penumbra.Mods; @@ -19,7 +18,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable private void SaveFilesystem() { SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); - PluginLog.Verbose( "Saved mod filesystem." ); + Penumbra.Log.Verbose( "Saved mod filesystem." ); } private void Save() @@ -79,7 +78,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable Save(); } - PluginLog.Debug( "Reloaded mod filesystem." ); + Penumbra.Log.Debug( "Reloaded mod filesystem." ); } // Save the filesystem on every filesystem change except full reloading. diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 190ca399..9507b0f3 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using Dalamud.Logging; using Newtonsoft.Json; using OtterGui.Filesystem; @@ -50,11 +49,11 @@ public interface IModGroup : IEnumerable< ISubMod > try { File.Delete( file ); - PluginLog.Debug( "Deleted group file {File:l} for group {GroupIdx}: {GroupName:l}.", file, groupIdx + 1, Name ); + Penumbra.Log.Debug( $"Deleted group file {file} for group {groupIdx + 1}: {Name}." ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete file {file}:\n{e}" ); + Penumbra.Log.Error( $"Could not delete file {file}:\n{e}" ); throw; } } @@ -92,7 +91,7 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteEndArray(); j.WriteEndObject(); - PluginLog.Debug( "Saved group file {File:l} for group {GroupIdx}: {GroupName:l}.", file, groupIdx + 1, group.Name ); + Penumbra.Log.Debug( $"Saved group file {file} for group {groupIdx + 1}: {group.Name}." ); } public IModGroup Convert( SelectType type ); diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index fc735981..3413cc6f 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -1,9 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; @@ -61,7 +59,7 @@ public partial class Mod { if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) { - PluginLog.Warning( + Penumbra.Log.Warning( $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options." ); break; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index 8cfb775c..e37b1988 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 9a13e642..07c752af 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; @@ -56,7 +55,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not parse default file for {Name}:\n{e}" ); + Penumbra.Log.Error( $"Could not parse default file for {Name}:\n{e}" ); } } @@ -196,8 +195,7 @@ public partial class Mod } catch( Exception e ) { - PluginLog.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); - continue; + Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fd7c2336..62e70d7a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; using EmbedIO; using EmbedIO.WebApi; @@ -14,6 +13,7 @@ using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; +using OtterGui.Log; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.GameData.Enums; @@ -44,6 +44,7 @@ public class Penumbra : IDalamudPlugin public static bool DevPenumbraExists; public static bool IsNotInstalledPenumbra; + public static Logger Log { get; private set; } = null!; public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; @@ -74,6 +75,7 @@ public class Penumbra : IDalamudPlugin try { Dalamud.Initialize( pluginInterface ); + Log = new Logger(); GameData.GameData.GetIdentifier( Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); @@ -135,17 +137,17 @@ public class Penumbra : IDalamudPlugin SubscribeItemLinks(); if( ImcExceptions > 0 ) { - PluginLog.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); + Log.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); } else { - PluginLog.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); + Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." ); } Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName ); - PluginLog.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); + Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); } catch { @@ -261,7 +263,7 @@ public class Penumbra : IDalamudPlugin .WithController( () => new ModsController( this ) ) .WithController( () => new RedrawController( this ) ) ); - WebServer.StateChanged += ( _, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); + WebServer.StateChanged += ( _, e ) => Log.Information( $"WebServer New State - {e.NewState}" ); WebServer.RunAsync(); } @@ -515,7 +517,7 @@ public class Penumbra : IDalamudPlugin } catch( Exception e ) { - PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + Log.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); return true; } #else @@ -530,7 +532,7 @@ public class Penumbra : IDalamudPlugin var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; if (!ret) - PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + Log.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); return !ret; #else return false; diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 0f91ec56..ff3b382c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using Dalamud.Interface; -using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -134,7 +133,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" ); _currentFile = null; } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 8390850b..74a5b6e9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; -using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -321,7 +320,7 @@ public partial class ModEditWindow var failedFiles = _editor!.ApplyFiles(); if( failedFiles > 0 ) { - PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." ); + Penumbra.Log.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." ); } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 9b07b8a1..054ce5b7 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -5,7 +5,6 @@ using System.Numerics; using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Logging; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -429,7 +428,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not parse DDS {path} to RGBA:\n{e}" ); + Penumbra.Log.Error( $"Could not parse DDS {path} to RGBA:\n{e}" ); return ( null, 0, 0 ); } } @@ -449,7 +448,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not parse TEX {path} to RGBA:\n{e}" ); + Penumbra.Log.Error( $"Could not parse TEX {path} to RGBA:\n{e}" ); return ( null, 0, 0 ); } } @@ -466,7 +465,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not parse PNG {path} to RGBA:\n{e}" ); + Penumbra.Log.Error( $"Could not parse PNG {path} to RGBA:\n{e}" ); return ( null, 0, 0 ); } } @@ -520,7 +519,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not load raw image:\n{e}" ); + Penumbra.Log.Error( $"Could not load raw image:\n{e}" ); } } @@ -685,7 +684,7 @@ public partial class ModEditWindow } catch( Exception e ) { - PluginLog.Error( $"Could not save image to {path}:\n{e}" ); + Penumbra.Log.Error( $"Could not save image to {path}:\n{e}" ); } } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 9b83fc67..77c7dea1 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,6 +1,5 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Filesystem; @@ -14,7 +13,6 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; -using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -100,7 +98,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } catch( Exception e ) { - PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); + Penumbra.Log.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); } } @@ -273,13 +271,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } catch( Exception e ) { - PluginLog.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); + Penumbra.Log.Error( $"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}" ); } } if( error is not OperationCanceledException ) { - PluginLog.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); + Penumbra.Log.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" ); } } else if( dir != null ) @@ -445,7 +443,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } catch( Exception e ) { - PluginLog.Warning( + Penumbra.Log.Warning( $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}" ); } } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 03d76923..4024f6d5 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -1,4 +1,3 @@ -using Lumina.Excel.GeneratedSheets; using OtterGui.Widgets; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs index 366a5b4f..e344a303 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs @@ -8,7 +8,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.UI.Classes; -using Penumbra.Util; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 1299f068..9c4a4547 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -6,7 +6,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; -using Penumbra.Util; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 68169211..fd36f308 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -11,7 +11,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using CharacterUtility = Penumbra.Interop.CharacterUtility; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index c6f1727f..c4141b73 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -13,7 +12,6 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.UI.Classes; -using Penumbra.Util; namespace Penumbra.UI; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index b24a3f4e..10bb3bd4 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -5,7 +5,6 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Logging; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -51,7 +50,7 @@ public partial class ConfigWindow } catch( Exception e ) { - PluginLog.Warning( e.Message ); + Penumbra.Log.Warning( e.Message ); } } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index a1eda296..bb012be3 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using Dalamud.Interface; using ImGuiNET; diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index fbafb806..c3288214 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -7,7 +7,6 @@ using Penumbra.UI.Classes; using System; using System.Linq; using System.Numerics; -using Dalamud.Logging; namespace Penumbra.UI; @@ -42,8 +41,8 @@ public partial class ConfigWindow } catch( Exception e ) { - PluginLog.Error( $"Exception thrown during ModPanel Render:\n{e}" ); - PluginLog.Error( $"{Penumbra.ModManager.Count} Mods\n" + Penumbra.Log.Error( $"Exception thrown during ModPanel Render:\n{e}" ); + Penumbra.Log.Error( $"{Penumbra.ModManager.Count} Mods\n" + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + $"{_selector.SortMode.Name} Sort Mode\n" diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 30465fb6..6a46ca1b 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -2,7 +2,6 @@ using System; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using ImGuiNET; using OtterGui.Raii; using Penumbra.UI.Classes; @@ -92,7 +91,7 @@ public sealed partial class ConfigWindow : Window, IDisposable } catch( Exception e ) { - PluginLog.Error( $"Exception thrown during UI Render:\n{e}" ); + Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); } } diff --git a/Penumbra/Util/FrameworkManager.cs b/Penumbra/Util/FrameworkManager.cs index d19179da..5a52d340 100644 --- a/Penumbra/Util/FrameworkManager.cs +++ b/Penumbra/Util/FrameworkManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dalamud.Game; -using Dalamud.Logging; namespace Penumbra.Util; @@ -58,7 +57,7 @@ public class FrameworkManager : IDisposable } catch( Exception e ) { - PluginLog.Error( $"Problem saving data:\n{e}" ); + Penumbra.Log.Error( $"Problem saving data:\n{e}" ); } } From 7c955cc2361a7db3dde485f83a845d011049a010 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Sep 2022 17:10:22 +0200 Subject: [PATCH 0475/2451] Improve some things and fix bugs in Option editing. --- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 23 ++++++++++++----- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 27 ++++++++++++-------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 7c159820..b2ecdc54 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -87,7 +87,7 @@ public sealed partial class Mod { foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( fromGroup ) ) { - foreach( var (o, optionIdx) in group.OfType().WithIndex() ) + foreach( var (o, optionIdx) in group.OfType< SubMod >().WithIndex() ) { o.SetPosition( groupIdx, optionIdx ); } @@ -175,18 +175,19 @@ public sealed partial class Mod public void AddOption( Mod mod, int groupIdx, string newName ) { - var group = mod._groups[groupIdx]; + var group = mod._groups[ groupIdx ]; + var subMod = new SubMod( mod ) { Name = newName }; + subMod.SetPosition( groupIdx, group.Count ); switch( group ) { case SingleModGroup s: - s.OptionData.Add( new SubMod(mod) { Name = newName } ); + s.OptionData.Add( subMod ); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( new SubMod(mod) { Name = newName }, 0 ) ); + m.PrioritizedOptions.Add( ( subMod, 0 ) ); break; } - group.UpdatePositions( group.Count - 1 ); ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); } @@ -206,6 +207,8 @@ public sealed partial class Mod return; } + o.SetPosition( groupIdx, group.Count ); + switch( group ) { case SingleModGroup s: @@ -215,13 +218,13 @@ public sealed partial class Mod m.PrioritizedOptions.Add( ( o, priority ) ); break; } - group.UpdatePositions( group.Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); } public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) { - var group = mod._groups[groupIdx]; + var group = mod._groups[ groupIdx ]; ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); switch( group ) { @@ -233,6 +236,7 @@ public sealed partial class Mod m.PrioritizedOptions.RemoveAt( optionIdx ); break; } + group.UpdatePositions( optionIdx ); ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 ); } @@ -332,6 +336,11 @@ public sealed partial class Mod private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) { + if( type == ModOptionChangeType.PrepareChange ) + { + return; + } + // File deletion is handled in the actual function. if( type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 10bb3bd4..4a3fec24 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -477,7 +477,7 @@ public partial class ConfigWindow EditOption( panel, group, groupIdx, optionIdx ); } - DrawNewOption( panel._mod, groupIdx, panel._window._iconButtonSize ); + DrawNewOption( panel, groupIdx, panel._window._iconButtonSize ); } // Draw a line for a single option. @@ -518,9 +518,14 @@ public partial class ConfigWindow } // Draw the line to add a new option. - private static void DrawNewOption( Mod mod, int groupIdx, Vector2 iconButtonSize ) + private static void DrawNewOption( ModPanel panel, int groupIdx, Vector2 iconButtonSize ) { + var mod = panel._mod; + var group = mod.Groups[ groupIdx ]; ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable( $"Option #{group.Count + 1}" ); + Target( panel, group, groupIdx, group.Count ); ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; @@ -564,14 +569,13 @@ public partial class ConfigWindow private static void Target( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) { - // TODO drag options to other groups without options. using var target = ImRaii.DragDropTarget(); if( !target.Success || !ImGuiUtil.IsDropping( DragDropLabel ) ) { return; } - if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) + if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) { if( _dragDropGroupIdx == groupIdx ) { @@ -580,15 +584,18 @@ public partial class ConfigWindow } else { - // Move from one group to another by deleting, then adding the option. - var sourceGroup = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var option = group[ _dragDropOptionIdx ]; - var priority = group.OptionPriority( _dragDropGroupIdx ); + // Move from one group to another by deleting, then adding, then moving the option. + var sourceGroupIdx = _dragDropGroupIdx; + var sourceOption = _dragDropOptionIdx; + var sourceGroup = panel._mod.Groups[ sourceGroupIdx ]; + var currentCount = group.Count; + var option = sourceGroup[sourceOption]; + var priority = sourceGroup.OptionPriority( _dragDropGroupIdx ); panel._delayedActions.Enqueue( () => { - Penumbra.ModManager.DeleteOption( panel._mod, sourceGroup, sourceOption ); + Penumbra.ModManager.DeleteOption( panel._mod, sourceGroupIdx, sourceOption ); Penumbra.ModManager.AddOption( panel._mod, groupIdx, option, priority ); + Penumbra.ModManager.MoveOption( panel._mod, groupIdx, currentCount, optionIdx ); } ); } } From ce73385333dec29c3ba439d032b0e50c3310259f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Sep 2022 17:11:02 +0200 Subject: [PATCH 0476/2451] Set changelog start. --- Penumbra/UI/ConfigWindow.Changelog.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 4024f6d5..41c25350 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -20,6 +20,12 @@ public partial class ConfigWindow return ret; } + private static void Add5_8_0( Changelog log ) + => log.NextVersion( "Version 0.5.8.0" ) + .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) + .RegisterEntry( "Fixed issues with and improved mod option editing." ) + .RegisterEntry( "Backend optimizations." ); + private static void Add5_7_1( Changelog log ) => log.NextVersion( "Version 0.5.7.1" ) .RegisterEntry( "Fixed the Changelog window not considering UI Scale correctly." ) From 1dae7fe0367695eaad1def68fb77f16f64f020c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Sep 2022 18:31:35 +0200 Subject: [PATCH 0477/2451] Add options for changelog display. --- OtterGui | 2 +- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/ConfigWindow.Changelog.cs | 12 +++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 44dc1f51..2b4b78b0 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 44dc1f51fe7900a1fe0e3368c4c5c00b6f866325 +Subproject commit 2b4b78b03d679144440d35b30830608b6d2bcb79 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b60e42b3..65eeb5db 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Widgets; using Penumbra.Import; using Penumbra.Mods; using Penumbra.UI; @@ -21,6 +22,7 @@ public partial class Configuration : IPluginConfiguration public int Version { get; set; } = Constants.CurrentVersion; public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; + public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; public bool EnableMods { get; set; } = true; public string ModDirectory { get; set; } = string.Empty; diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 41c25350..630b21ed 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -8,11 +8,13 @@ public partial class ConfigWindow public static Changelog CreateChangelog() { - var ret = new Changelog( "Penumbra Changelog", () => Penumbra.Config.LastSeenVersion, version => - { - Penumbra.Config.LastSeenVersion = version; - Penumbra.Config.Save(); - } ); + var ret = new Changelog( "Penumbra Changelog", () => ( Penumbra.Config.LastSeenVersion, Penumbra.Config.ChangeLogDisplayType ), + ( version, type ) => + { + Penumbra.Config.LastSeenVersion = version; + Penumbra.Config.ChangeLogDisplayType = type; + Penumbra.Config.Save(); + } ); Add5_7_0( ret ); Add5_7_1( ret ); From d11b7e11aa2c482b8675be4f2fd069d3b1ed1eda Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 16 Sep 2022 17:10:12 +0900 Subject: [PATCH 0478/2451] Fix ReadFileBlock (NotAdam/Lumina#42) --- Penumbra/Util/PenumbraSqPackStream.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 017d70c6..7bb0687d 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -334,14 +334,13 @@ public class PenumbraSqPackStream : IDisposable if( blockHeader.CompressedSize == 32000 ) { dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) ); - return blockHeader.UncompressedSize; } - - var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize ); - - using( var compressedStream = new MemoryStream( data ) ) + else { - using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress ); + var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize ); + + using var compressedStream = new MemoryStream( data ); + using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress ); zlibStream.CopyTo( dest ); } From 9753c14b322bc072fdb7296b3ef2f5272aa00c2a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 Sep 2022 17:18:09 +0200 Subject: [PATCH 0479/2451] Changelog Entry --- Penumbra/UI/ConfigWindow.Changelog.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 630b21ed..d4c6cb04 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -24,6 +24,7 @@ public partial class ConfigWindow private static void Add5_8_0( Changelog log ) => log.NextVersion( "Version 0.5.8.0" ) + .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) .RegisterEntry( "Fixed issues with and improved mod option editing." ) .RegisterEntry( "Backend optimizations." ); From af3a07c227f02d57f1aedaeea63341fa884ef368 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 Sep 2022 18:47:19 +0200 Subject: [PATCH 0480/2451] Switch CharacterUtility to use linked lists of changes. --- Penumbra/Interop/CharacterUtility.List.cs | 147 ++++++++++++++++++++++ Penumbra/Interop/CharacterUtility.cs | 79 ++++-------- 2 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 Penumbra/Interop/CharacterUtility.List.cs diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs new file mode 100644 index 00000000..27146af6 --- /dev/null +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; + +namespace Penumbra.Interop; + +public unsafe partial class CharacterUtility +{ + public class List : IDisposable + { + private readonly LinkedList< MetaReverter > _entries = new(); + public readonly InternalIndex Index; + public readonly Structs.CharacterUtility.Index GlobalIndex; + + private IntPtr _defaultResourceData = IntPtr.Zero; + private int _defaultResourceSize = 0; + public bool Ready { get; private set; } = false; + + public List( InternalIndex index ) + { + Index = index; + GlobalIndex = RelevantIndices[ index.Value ]; + } + + public void SetDefaultResource( IntPtr data, int size ) + { + if( !Ready ) + { + _defaultResourceData = data; + _defaultResourceSize = size; + Ready = _defaultResourceData != IntPtr.Zero && size != 0; + if( _entries.Count > 0 ) + { + var first = _entries.First!.Value; + SetResource( first.Data, first.Length ); + } + } + } + + public (IntPtr Address, int Size) DefaultResource + => ( _defaultResourceData, _defaultResourceSize ); + + public MetaReverter TemporarilySetResource( IntPtr data, int length ) + { + Penumbra.Log.Verbose( $"Temporarily set resource {GlobalIndex} to 0x{( ulong )data:X} ({length} bytes)." ); + var reverter = new MetaReverter( this, data, length ); + _entries.AddFirst( reverter ); + SetResourceInternal( data, length ); + return reverter; + } + + public MetaReverter TemporarilyResetResource() + { + Penumbra.Log.Verbose( $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); + var reverter = new MetaReverter( this ); + _entries.AddFirst( reverter ); + ResetResourceInternal(); + return reverter; + } + + public void SetResource( IntPtr data, int length ) + { + Penumbra.Log.Verbose( $"Set resource {GlobalIndex} to 0x{( ulong )data:X} ({length} bytes)." ); + SetResourceInternal( data, length ); + } + + public void ResetResource() + { + Penumbra.Log.Verbose( $"Reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); + ResetResourceInternal(); + } + + + // Set the currently stored data of this resource to new values. + private void SetResourceInternal( IntPtr data, int length ) + { + if( Ready ) + { + var resource = Penumbra.CharacterUtility.Address->Resource( GlobalIndex ); + resource->SetData( data, length ); + } + } + + // Reset the currently stored data of this resource to its default values. + private void ResetResourceInternal() + => SetResourceInternal( _defaultResourceData, _defaultResourceSize ); + + public void Dispose() + { + if( _entries.Count > 0 ) + { + _entries.Clear(); + ResetResourceInternal(); + } + } + + public sealed class MetaReverter : IDisposable + { + public readonly List List; + public readonly IntPtr Data; + public readonly int Length; + public readonly bool Resetter; + + public MetaReverter( List list, IntPtr data, int length ) + { + List = list; + Data = data; + Length = length; + } + + public MetaReverter( List list ) + { + List = list; + Data = IntPtr.Zero; + Length = 0; + Resetter = true; + } + + public void Dispose() + { + var list = List._entries; + var wasCurrent = ReferenceEquals( this, list.First?.Value ); + list.Remove( this ); + if( !wasCurrent ) + { + return; + } + + if( list.Count == 0 ) + { + List.ResetResourceInternal(); + } + else + { + var next = list.First!.Value; + if( next.Resetter ) + { + List.ResetResourceInternal(); + } + else + { + List.SetResourceInternal( next.Data, next.Length ); + } + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 84dc750c..c168f8c4 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -4,7 +4,7 @@ using Dalamud.Utility.Signatures; namespace Penumbra.Interop; -public unsafe class CharacterUtility : IDisposable +public unsafe partial class CharacterUtility : IDisposable { public record struct InternalIndex( int Value ); @@ -34,17 +34,15 @@ public unsafe class CharacterUtility : IDisposable public static readonly InternalIndex[] ReverseIndices = Enumerable.Range( 0, Structs.CharacterUtility.TotalNumResources ) - .Select( i => new InternalIndex( Array.IndexOf( RelevantIndices, (Structs.CharacterUtility.Index) i ) ) ) + .Select( i => new InternalIndex( Array.IndexOf( RelevantIndices, ( Structs.CharacterUtility.Index )i ) ) ) .ToArray(); - - private readonly (IntPtr Address, int Size)[] _defaultResources = new (IntPtr, int)[RelevantIndices.Length]; - - public (IntPtr Address, int Size) DefaultResource( Structs.CharacterUtility.Index idx ) - => _defaultResources[ ReverseIndices[ ( int )idx ].Value ]; + private readonly List[] _lists = Enumerable.Range( 0, RelevantIndices.Length ) + .Select( idx => new List( new InternalIndex( idx ) ) ) + .ToArray(); public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) - => _defaultResources[ idx.Value ]; + => _lists[ idx.Value ].DefaultResource; public CharacterUtility() { @@ -60,30 +58,25 @@ public unsafe class CharacterUtility : IDisposable // We store the default data of the resources so we can always restore them. private void LoadDefaultResources( object _ ) { - var missingCount = 0; if( Address == null ) { return; } + var anyMissing = false; for( var i = 0; i < RelevantIndices.Length; ++i ) { - if( _defaultResources[ i ].Size == 0 ) + var list = _lists[ i ]; + if( !list.Ready ) { - var resource = Address->Resource( RelevantIndices[i] ); - var data = resource->GetData(); - if( data.Data != IntPtr.Zero && data.Length != 0 ) - { - _defaultResources[ i ] = data; - } - else - { - ++missingCount; - } + var resource = Address->Resource( RelevantIndices[ i ] ); + var (data, length) = resource->GetData(); + list.SetDefaultResource( data, length ); + anyMissing |= !_lists[ i ].Ready; } } - if( missingCount == 0 ) + if( !anyMissing ) { Ready = true; LoadingFinished.Invoke(); @@ -91,51 +84,27 @@ public unsafe class CharacterUtility : IDisposable } } - // Set the data of one of the stored resources to a given pointer and length. - public bool SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) + public List.MetaReverter SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { - if( !Ready ) - { - Penumbra.Log.Error( $"Can not set resource {resourceIdx}: CharacterUtility not ready yet." ); - return false; - } - - var resource = Address->Resource( resourceIdx ); - var ret = resource->SetData( data, length ); - Penumbra.Log.Verbose( $"Set resource {resourceIdx} to 0x{( ulong )data:X} ({length} bytes)."); - return ret; + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; + return list.SetResource( data, length ); } - // Reset the data of one of the stored resources to its default values. - public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) + public List.MetaReverter ResetResource( Structs.CharacterUtility.Index resourceIdx ) { - if( !Ready ) - { - Penumbra.Log.Error( $"Can not reset {resourceIdx}: CharacterUtility not ready yet." ); - return; - } - - var (data, length) = DefaultResource( resourceIdx); - var resource = Address->Resource( resourceIdx ); - Penumbra.Log.Verbose( $"Reset resource {resourceIdx} to default at 0x{(ulong)data:X} ({length} bytes)."); - resource->SetData( data, length ); + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; + return list.ResetResource(); } // Return all relevant resources to the default resource. public void ResetAll() { - if( !Ready ) + foreach( var list in _lists ) { - Penumbra.Log.Error( "Can not reset all resources: CharacterUtility not ready yet." ); - return; + list.Dispose(); } - - foreach( var idx in RelevantIndices ) - { - ResetResource( idx ); - } - - Penumbra.Log.Debug( "Reset all CharacterUtility resources to default." ); } public void Dispose() From b34999a1a522ff50ee5a29cfafff8b240610e3e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 Sep 2022 21:14:04 +0200 Subject: [PATCH 0481/2451] Use MetaReverter for all cases, improve Eqdp handling through this. --- OtterGui | 2 +- .../Collections/ModCollection.Cache.Access.cs | 22 +- Penumbra/Interop/CharacterUtility.cs | 26 ++- .../Resolver/PathResolver.DrawObjectState.cs | 35 ++- .../Interop/Resolver/PathResolver.Meta.cs | 204 ++++-------------- .../Resolver/PathResolver.ResolverHooks.cs | 56 ++++- Penumbra/Interop/Resolver/PathResolver.cs | 2 +- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 3 + Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 18 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 3 + Penumbra/Meta/Manager/MetaManager.Est.cs | 14 ++ Penumbra/Meta/Manager/MetaManager.Gmp.cs | 3 + Penumbra/Meta/Manager/MetaManager.cs | 6 + 13 files changed, 199 insertions(+), 195 deletions(-) diff --git a/OtterGui b/OtterGui index 2b4b78b0..b92dbe60 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2b4b78b03d679144440d35b30830608b6d2bcb79 +Subproject commit b92dbe60887503a77a89aeae80729236fb2bfa10 diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 918d361c..351ae2fc 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,11 +1,14 @@ using OtterGui.Classes; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Meta.Manager; using Penumbra.Mods; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using Penumbra.Interop; +using Penumbra.Meta.Manipulations; namespace Penumbra.Collections; @@ -203,4 +206,21 @@ public partial class ModCollection Penumbra.Log.Debug( $"Set CharacterUtility resources for collection {Name}." ); } } -} \ No newline at end of file + + // Used for short periods of changed files. + public CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) + => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ); + + public CharacterUtility.List.MetaReverter? TemporarilySetEqpFile() + => _cache?.MetaManipulations.TemporarilySetEqpFile(); + + public CharacterUtility.List.MetaReverter? TemporarilySetGmpFile() + => _cache?.MetaManipulations.TemporarilySetGmpFile(); + + public CharacterUtility.List.MetaReverter? TemporarilySetCmpFile() + => _cache?.MetaManipulations.TemporarilySetCmpFile(); + + public CharacterUtility.List.MetaReverter? TemporarilySetEstFile( EstManipulation.EstType type ) + => _cache?.MetaManipulations.TemporarilySetEstFile( type ); + +} diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index c168f8c4..a5b155c1 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -84,18 +84,32 @@ public unsafe partial class CharacterUtility : IDisposable } } - public List.MetaReverter SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) + public void SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { - var idx = ReverseIndices[ ( int )resourceIdx ]; - var list = _lists[ idx.Value ]; - return list.SetResource( data, length ); + var idx = ReverseIndices[( int )resourceIdx]; + var list = _lists[idx.Value]; + list.SetResource( data, length ); } - public List.MetaReverter ResetResource( Structs.CharacterUtility.Index resourceIdx ) + public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) + { + var idx = ReverseIndices[( int )resourceIdx]; + var list = _lists[idx.Value]; + list.ResetResource(); + } + + public List.MetaReverter TemporarilySetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { var idx = ReverseIndices[ ( int )resourceIdx ]; var list = _lists[ idx.Value ]; - return list.ResetResource(); + return list.TemporarilySetResource( data, length ); + } + + public List.MetaReverter TemporarilyResetResource( Structs.CharacterUtility.Index resourceIdx ) + { + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; + return list.TemporarilyResetResource(); } // Return all relevant resources to the default resource. diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index e681a364..d6e6ec0e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -54,7 +54,7 @@ public unsafe partial class PathResolver public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData ) { - if( type == ResourceType.Tex + if( type == ResourceType.Tex && LastCreatedCollection.Valid && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) { @@ -123,7 +123,7 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); + private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); private ResolveData _lastCreatedCollection = ResolveData.Invalid; // Keep track of created DrawObjects that are CharacterBase, @@ -135,22 +135,37 @@ public unsafe partial class PathResolver private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { - using var cmp = MetaChanger.ChangeCmp( LastGameObject, out _lastCreatedCollection ); - + CharacterUtility.List.MetaReverter? cmp = null; if( LastGameObject != null ) { + _lastCreatedCollection = IdentifyCollection( LastGameObject ); var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); + if( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default ) + { + cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); + } + + try + { + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Unknown Error during CreatingCharacterBase:\n{e}" ); + } } var ret = _characterBaseCreateHook.Original( a, b, c, d ); - if( LastGameObject != null ) + using( cmp ) { - _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ret ); - } + if( LastGameObject != null ) + { + _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); + CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ret ); + } - return ret; + return ret; + } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 94d760e0..ba52182a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -1,9 +1,9 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.Collections; -using Penumbra.Meta.Manipulations; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Enums; +using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; namespace Penumbra.Interop.Resolver; @@ -29,17 +29,13 @@ namespace Penumbra.Interop.Resolver; // ChangeCustomize and RspSetupCharacter, which is hooked here. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. - public unsafe partial class PathResolver { - public unsafe class MetaState : IDisposable + public class MetaState : IDisposable { - private readonly PathResolver _parent; - - public MetaState( PathResolver parent, IntPtr* humanVTable ) + public MetaState( IntPtr* humanVTable ) { SignatureHelper.Initialise( this ); - _parent = parent; _onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour ); } @@ -81,8 +77,10 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); - using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); + var race = GetDrawObjectGenderRace( drawObject ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); + using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } else @@ -108,8 +106,10 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - using var eqp = MetaChanger.ChangeEqp( collection.ModCollection ); - using var eqdp = MetaChanger.ChangeEqdp( collection.ModCollection ); + var race = GetDrawObjectGenderRace( drawObject ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); + using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); _updateModelsHook.Original.Invoke( drawObject ); } else @@ -118,6 +118,24 @@ public unsafe partial class PathResolver } } + private static GenderRace GetDrawObjectGenderRace( IntPtr drawObject ) + { + var draw = ( DrawObject* )drawObject; + if( draw->Object.GetObjectType() == ObjectType.CharacterBase ) + { + var c = ( CharacterBase* )drawObject; + if( c->GetModelType() == CharacterBase.ModelType.Human ) + { + return GetHumanGenderRace( drawObject ); + } + } + + return GenderRace.Unknown; + } + + public static GenderRace GetHumanGenderRace( IntPtr human ) + => ( GenderRace )( ( Human* )human )->RaceSexId; + [Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", DetourName = nameof( GetEqpIndirectDetour ) )] private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!; @@ -131,7 +149,8 @@ public unsafe partial class PathResolver return; } - using var eqp = MetaChanger.ChangeEqp( _parent, drawObject ); + var resolveData = GetResolveData( drawObject ); + using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; _getEqpIndirectHook.Original( drawObject ); } @@ -145,7 +164,8 @@ public unsafe partial class PathResolver private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) { - using var gmp = MetaChanger.ChangeGmp( _parent, drawObject ); + var resolveData = GetResolveData( drawObject ); + using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetGmpFile() : null; return _setupVisorHook.Original( drawObject, modelId, visorState ); } @@ -163,13 +183,14 @@ public unsafe partial class PathResolver } else { - using var rsp = MetaChanger.ChangeCmp( _parent, drawObject ); + var resolveData = GetResolveData( drawObject ); + using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); } } // ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. - private bool _inChangeCustomize = false; + private bool _inChangeCustomize; private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment ); [Signature( "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86", DetourName = nameof( ChangeCustomizeDetour ) )] @@ -178,154 +199,9 @@ public unsafe partial class PathResolver private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment ) { _inChangeCustomize = true; - using var rsp = MetaChanger.ChangeCmp( _parent, human ); + var resolveData = GetResolveData( human ); + using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; return _changeCustomize.Original( human, data, skipEquipment ); } } - - // Small helper to handle setting metadata and reverting it at the end of the function. - // Since eqp and eqdp may be called multiple times in a row, we need to count them, - // so that we do not reset the files too early. - private readonly struct MetaChanger : IDisposable - { - private static int _eqpCounter; - private static int _eqdpCounter; - private readonly MetaManipulation.Type _type; - - private MetaChanger( MetaManipulation.Type type ) - { - _type = type; - if( type == MetaManipulation.Type.Eqp ) - { - ++_eqpCounter; - } - else if( type == MetaManipulation.Type.Eqdp ) - { - ++_eqdpCounter; - } - } - - public static MetaChanger ChangeEqp( ModCollection collection ) - { - collection.SetEqpFiles(); - return new MetaChanger( MetaManipulation.Type.Eqp ); - } - - public static MetaChanger ChangeEqp( PathResolver _, IntPtr drawObject ) - { - var resolveData = GetResolveData( drawObject ); - if( resolveData.Valid ) - { - return ChangeEqp( resolveData.ModCollection ); - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - // We only need to change anything if it is actually equipment here. - public static MetaChanger ChangeEqdp( PathResolver _, IntPtr drawObject, uint modelType ) - { - if( modelType < 10 ) - { - var collection = GetResolveData( drawObject ); - if( collection.Valid ) - { - return ChangeEqdp( collection.ModCollection ); - } - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - public static MetaChanger ChangeEqdp( ModCollection collection ) - { - collection.SetEqdpFiles(); - return new MetaChanger( MetaManipulation.Type.Eqdp ); - } - - public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) - { - var resolveData = GetResolveData( drawObject ); - if( resolveData.Valid ) - { - resolveData.ModCollection.SetGmpFiles(); - return new MetaChanger( MetaManipulation.Type.Gmp ); - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) - { - var resolveData = GetResolveData( drawObject ); - if( resolveData.Valid ) - { - resolveData.ModCollection.SetEstFiles(); - return new MetaChanger( MetaManipulation.Type.Est ); - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - public static MetaChanger ChangeCmp( GameObject* gameObject, out ResolveData resolveData ) - { - if( gameObject != null ) - { - resolveData = IdentifyCollection( gameObject ); - if( resolveData.ModCollection != Penumbra.CollectionManager.Default && resolveData.ModCollection.HasCache ) - { - resolveData.ModCollection.SetCmpFiles(); - return new MetaChanger( MetaManipulation.Type.Rsp ); - } - } - else - { - resolveData = ResolveData.Invalid; - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) - { - var resolveData = GetResolveData( drawObject ); - if( resolveData.Valid ) - { - resolveData.ModCollection.SetCmpFiles(); - return new MetaChanger( MetaManipulation.Type.Rsp ); - } - - return new MetaChanger( MetaManipulation.Type.Unknown ); - } - - public void Dispose() - { - switch( _type ) - { - case MetaManipulation.Type.Eqdp: - if( --_eqdpCounter == 0 ) - { - Penumbra.CollectionManager.Default.SetEqdpFiles(); - } - - break; - case MetaManipulation.Type.Eqp: - if( --_eqpCounter == 0 ) - { - Penumbra.CollectionManager.Default.SetEqpFiles(); - } - - break; - case MetaManipulation.Type.Est: - Penumbra.CollectionManager.Default.SetEstFiles(); - break; - case MetaManipulation.Type.Gmp: - Penumbra.CollectionManager.Default.SetGmpFiles(); - break; - case MetaManipulation.Type.Rsp: - Penumbra.CollectionManager.Default.SetCmpFiles(); - break; - } - } - } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index d847480e..b5335dc8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -2,6 +2,9 @@ using System; using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Resolver; @@ -140,34 +143,64 @@ public partial class PathResolver private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) { - using var eqdp = MetaChanger.ChangeEqdp( _parent, drawObject, modelType ); + CharacterUtility.List.MetaReverter? Get() + { + if( modelType > 9 ) + { + return null; + } + + var race = MetaState.GetHumanGenderRace( drawObject ); + if( race == GenderRace.Unknown ) + { + return null; + } + + var data = GetResolveData( drawObject ); + return !data.Valid ? null : data.ModCollection.TemporarilySetEqdpFile( race, modelType > 4 ); + } + + using var eqdp = Get(); return ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) ); } private IntPtr ResolvePapHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 ) { - using var est = MetaChanger.ChangeEst( _parent, drawObject ); + using var est = GetEstChanges( drawObject ); return ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) ); } private IntPtr ResolvePhybHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) { - using var est = MetaChanger.ChangeEst( _parent, drawObject ); + using var est = GetEstChanges( drawObject ); return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) ); } private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) { - using var est = MetaChanger.ChangeEst( _parent, drawObject ); + using var est = GetEstChanges( drawObject ); return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) ); } private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) { - using var est = MetaChanger.ChangeEst( _parent, drawObject ); + using var est = GetEstChanges( drawObject ); return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); } + private DisposableContainer GetEstChanges( IntPtr drawObject ) + { + var data = GetResolveData( drawObject ); + if( !data.Valid ) + { + return DisposableContainer.Empty; + } + + return new DisposableContainer( data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Face ), + data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Body ), + data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Hair ), + data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Head ) ); + } private IntPtr ResolveDecalWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) => ResolveWeaponPath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) ); @@ -226,9 +259,10 @@ public partial class PathResolver // Implementation [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePath( IntPtr drawObject, IntPtr path ) - => _parent._paths.ResolvePath( (IntPtr?)FindParent( drawObject, out _) ?? IntPtr.Zero, FindParent( drawObject, out var collection ) == null - ? Penumbra.CollectionManager.Default - : collection.ModCollection, path ); + => _parent._paths.ResolvePath( ( IntPtr? )FindParent( drawObject, out _ ) ?? IntPtr.Zero, + FindParent( drawObject, out var collection ) == null + ? Penumbra.CollectionManager.Default + : collection.ModCollection, path ); // Weapons have the characters DrawObject as a parent, // but that may not be set yet when creating a new object, so we have to do the same detour @@ -239,18 +273,18 @@ public partial class PathResolver var parent = FindParent( drawObject, out var collection ); if( parent != null ) { - return _parent._paths.ResolvePath( (IntPtr)parent, collection.ModCollection, path ); + return _parent._paths.ResolvePath( ( IntPtr )parent, collection.ModCollection, path ); } var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); if( parentCollection.Valid ) { - return _parent._paths.ResolvePath( (IntPtr)FindParent(parentObject, out _), parentCollection.ModCollection, path ); + return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path ); } parent = FindParent( parentObject, out collection ); - return _parent._paths.ResolvePath( (IntPtr?)parent ?? IntPtr.Zero, parent == null + return _parent._paths.ResolvePath( ( IntPtr? )parent ?? IntPtr.Zero, parent == null ? Penumbra.CollectionManager.Default : collection.ModCollection, path ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 0fda162c..6b1d76a4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -34,7 +34,7 @@ public partial class PathResolver : IDisposable _loader = loader; _animations = new AnimationState( DrawObjects ); _paths = new PathState( this ); - _meta = new MetaState( this, _paths.HumanVTable ); + _meta = new MetaState( _paths.HumanVTable ); _materials = new MaterialState( _paths ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index b624a360..8cd17a1e 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -18,6 +18,9 @@ public partial class MetaManager public static void ResetCmpFiles() => SetFile( null, CharacterUtility.Index.HumanCmp ); + public Interop.CharacterUtility.List.MetaReverter TemporarilySetCmpFile() + => TemporarilySetFile( _cmpFile, CharacterUtility.Index.HumanCmp ); + public void ResetCmp() { if( _cmpFile == null ) diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index dc9a31d6..35857be0 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OtterGui; using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -23,6 +24,21 @@ public partial class MetaManager } } + public Interop.CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) + { + var idx = CharacterUtility.EqdpIdx( genderRace, accessory ); + if( ( int )idx != -1 ) + { + var i = CharacterUtility.EqdpIndices.IndexOf( idx ); + if( i != -1 ) + { + return TemporarilySetFile( _eqdpFiles[ i ], idx ); + } + } + + return null; + } + public static void ResetEqdpFiles() { foreach( var idx in CharacterUtility.EqdpIndices ) @@ -33,7 +49,7 @@ public partial class MetaManager public void ResetEqdp() { - foreach( var file in _eqdpFiles.OfType() ) + foreach( var file in _eqdpFiles.OfType< ExpandedEqdpFile >() ) { var relevant = Interop.CharacterUtility.RelevantIndices[ file.Index.Value ]; file.Reset( _eqdpManipulations.Where( m => m.FileIndex() == relevant ).Select( m => ( int )m.SetId ) ); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index adae4378..7a5da091 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -18,6 +18,9 @@ public partial class MetaManager public static void ResetEqpFiles() => SetFile( null, CharacterUtility.Index.Eqp ); + public Interop.CharacterUtility.List.MetaReverter TemporarilySetEqpFile() + => TemporarilySetFile( _eqpFile, CharacterUtility.Index.Eqp ); + public void ResetEqp() { if( _eqpFile == null ) diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index 728a024d..193e749d 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -33,6 +33,20 @@ public partial class MetaManager SetFile( null, CharacterUtility.Index.HeadEst ); } + public Interop.CharacterUtility.List.MetaReverter? TemporarilySetEstFile(EstManipulation.EstType type) + { + var (file, idx) = type switch + { + EstManipulation.EstType.Face => ( _estFaceFile, CharacterUtility.Index.FaceEst ), + EstManipulation.EstType.Hair => ( _estHairFile, CharacterUtility.Index.HairEst ), + EstManipulation.EstType.Body => ( _estBodyFile, CharacterUtility.Index.BodyEst ), + EstManipulation.EstType.Head => ( _estHeadFile, CharacterUtility.Index.HeadEst ), + _ => ( null, 0 ), + }; + + return idx != 0 ? TemporarilySetFile( file, idx ) : null; + } + public void ResetEst() { _estFaceFile?.Reset(); diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 91554e6a..02e1dc78 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -18,6 +18,9 @@ public partial class MetaManager public static void ResetGmpFiles() => SetFile( null, CharacterUtility.Index.Gmp ); + public Interop.CharacterUtility.List.MetaReverter TemporarilySetGmpFile() + => TemporarilySetFile( _gmpFile, CharacterUtility.Index.Gmp ); + public void ResetGmp() { if( _gmpFile == null ) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index fde0614c..701b77c0 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -179,4 +179,10 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM Penumbra.CharacterUtility.SetResource( index, ( IntPtr )file.Data, file.Length ); } } + + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + private static unsafe Interop.CharacterUtility.List.MetaReverter TemporarilySetFile( MetaBaseFile? file, CharacterUtility.Index index ) + => file == null + ? Penumbra.CharacterUtility.TemporarilyResetResource( index ) + : Penumbra.CharacterUtility.TemporarilySetResource( index, ( IntPtr )file.Data, file.Length ); } \ No newline at end of file From 01c360416f811ba391e0d4c9434824d47cfc7948 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Sep 2022 00:48:38 +0200 Subject: [PATCH 0482/2451] Add file selection combo to textures. --- Penumbra/Import/Textures/Texture.cs | 24 ++++++++++++++++--- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 9 +++++-- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 4 ++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index a0cc6d33..c553ed9f 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -5,7 +5,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using ImGuiScene; using Lumina.Data.Files; @@ -19,7 +18,7 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public class Texture : IDisposable +public sealed class Texture : IDisposable { public enum FileType { @@ -193,10 +192,29 @@ public class Texture : IDisposable private string? _tmpPath; + public void PathSelectBox( string label, string tooltip, IEnumerable paths ) + { + ImGui.SetNextItemWidth( -0.0001f ); + var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; + using var combo = ImRaii.Combo( label, startPath ); + if( combo ) + { + foreach( var (path, idx) in paths.WithIndex() ) + { + using var id = ImRaii.PushId( idx ); + if( ImGui.Selectable( path, path == startPath ) && path != startPath ) + { + Load( path ); + } + } + } + ImGuiUtil.HoverTooltip( tooltip ); + } + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) { _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 480261ee..4263b83f 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -40,7 +40,7 @@ public partial class Mod public bool Equals( FileRegistry? other ) { - if( ReferenceEquals( null, other ) ) + if( other is null ) { return false; } @@ -50,7 +50,7 @@ public partial class Mod public override bool Equals( object? obj ) { - if( ReferenceEquals( null, obj ) ) + if( obj is null ) { return false; } @@ -75,6 +75,7 @@ public partial class Mod private List< FileRegistry > _availableFiles = null!; private List< FileRegistry > _mtrlFiles = null!; private List< FileRegistry > _mdlFiles = null!; + private List _texFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -89,6 +90,9 @@ public partial class Mod public IReadOnlyList< FileRegistry > MdlFiles => _mdlFiles; + public IReadOnlyList TexFiles + => _texFiles; + // Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths() { @@ -130,6 +134,7 @@ public partial class Mod _usedPaths.Clear(); _mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList(); _mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList(); + _texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList(); FileChanges = false; foreach( var subMod in _mod.AllSubMods ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index b74aef07..49e541e5 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; @@ -34,6 +35,9 @@ public partial class ModEditWindow tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _dialogManager ); + var files = _editor!.TexFiles.Select( f => f.File.FullName ) + .Concat( _editor.TexFiles.SelectMany( f => f.SubModUsage.Select( p => p.Item2.ToString() ) ) ); + tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files); if( tex == _left ) { From 85970700631da25c3445c31b1f42b03dfaa80254 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Sep 2022 21:45:24 +0200 Subject: [PATCH 0483/2451] Add redraw buttons and tutorial. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 31 +++++++++++++++++++++++++ Penumbra/UI/ConfigWindow.Tutorial.cs | 13 ++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index b92dbe60..41581a5d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b92dbe60887503a77a89aeae80729236fb2bfa10 +Subproject commit 41581a5d035d46a89819aaefaa8d01c815788eee diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 2cbd2c24..2f6f39c1 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,6 +11,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.GameData.Enums; using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -52,6 +53,8 @@ public partial class ConfigWindow : _window.Flags & ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); } ); + DrawRedrawButton(); + ImGui.NewLine(); DrawRootFolder(); DrawRediscoverButton(); @@ -335,6 +338,34 @@ public partial class ConfigWindow + "Not directly affiliated and potentially, but not usually out of date." ); } + private void DrawRedrawButton() + { + using( var group = ImRaii.Group() ) + { + using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); + + if( ImGui.Button( "Redraw Self" ) ) + { + _window._penumbra.ObjectReloader.RedrawObject( "self", RedrawType.Redraw ); + } + + ImGuiUtil.HoverTooltip( "Executes '/penumbra redraw self'." ); + + ImGui.SameLine(); + if( ImGui.Button( "Redraw All" ) ) + { + _window._penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + ImGuiUtil.HoverTooltip( "Executes '/penumbra redraw'." ); + + ImGui.SameLine(); + + ImGuiComponents.HelpMarker( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); + } + + OpenTutorial( BasicTutorialSteps.Redrawing ); + } + private void DrawSupportButtons() { var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index ef75566d..f4973eb8 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -18,6 +18,12 @@ public partial class ConfigWindow public const string ConditionalIndividual = "Character"; public const string IndividualAssignments = "Individual Assignments"; + public const string SupportedRedrawModifiers = " - 'self' or '': your own character\n" + + " - 'target' or '': your target\n" + + " - 'focus' or ': your focus target\n" + + " - 'mouseover' or '': the actor you are currently hovering\n" + + " - any specific actor name to redraw all actors of that exactly matching name."; + private static void UpdateTutorialStep() { var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); @@ -43,6 +49,7 @@ public partial class ConfigWindow EnableMods, AdvancedSettings, GeneralSettings, + Redrawing, Collections, EditingCollections, CurrentCollection, @@ -82,6 +89,10 @@ public partial class ConfigWindow + "If you need to do any editing of your mods, you will have to turn it on later." ) .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + "If you do not know what some of these do yet, return to this later!" ) + .Register( "Redrawing", + "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" + + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" + + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too." ) .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + "This is our next stop!\n\n" + "Go here after setting up your root folder to continue the tutorial!" ) @@ -104,7 +115,7 @@ public partial class ConfigWindow .Register( GroupAssignment + 's', "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" - + $"{IndividualAssignments} always take precedence before groups.") + + $"{IndividualAssignments} always take precedence before groups." ) .Register( IndividualAssignments, "Collections assigned here are used only for individual characters or NPCs that have the specified name.\n\n" + "They may also apply to objects 'owned' by those characters, e.g. minions or mounts - see the general settings for options on this.\n\n" ) From 273111775c03787a387c264d18e38b86744fc32d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Sep 2022 22:53:14 +0200 Subject: [PATCH 0484/2451] Add interface collection. --- .../Collections/CollectionManager.Active.cs | 42 ++++++++++++++++--- Penumbra/Collections/CollectionType.cs | 4 +- Penumbra/Configuration.Migration.cs | 25 ++++++++++- Penumbra/Configuration.cs | 2 +- .../Loader/ResourceLoader.Replacement.cs | 6 +++ Penumbra/Penumbra.cs | 4 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 13 +++++- Penumbra/UI/ConfigWindow.Tutorial.cs | 9 +++- 8 files changed, 91 insertions(+), 14 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 6885cdee..7a8bb658 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -26,6 +26,9 @@ public partial class ModCollection // The collection used for general file redirections and all characters not specifically named. public ModCollection Default { get; private set; } = Empty; + // The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } = Empty; + // A single collection that can not be deleted as a fallback for the current collection. private ModCollection DefaultName { get; set; } = Empty; @@ -53,6 +56,7 @@ public partial class ModCollection return type switch { CollectionType.Default => Default, + CollectionType.Interface => Interface, CollectionType.Current => Current, CollectionType.Character => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, @@ -65,8 +69,9 @@ public partial class ModCollection { var oldCollectionIdx = collectionType switch { - CollectionType.Default => Default.Index, - CollectionType.Current => Current.Index, + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, CollectionType.Character => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index @@ -97,6 +102,9 @@ public partial class ModCollection Default.SetFiles(); } + break; + case CollectionType.Interface: + Interface = newCollection; break; case CollectionType.Current: Current = newCollection; @@ -118,6 +126,7 @@ public partial class ModCollection private void UpdateCurrentCollectionInUse() => CurrentCollectionInUse = _specialCollections .OfType< ModCollection >() + .Prepend( Interface ) .Prepend( Default ) .Concat( Characters.Values ) .SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); @@ -192,7 +201,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? Empty.Name : DefaultCollection ); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { @@ -205,12 +214,28 @@ public partial class ModCollection Default = this[ defaultIdx ]; } + // Load the interface collection. + var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? (configChanged ? Empty.Name : Default.Name); + var interfaceIdx = GetIndexForCollectionName( interfaceName ); + if( interfaceIdx < 0 ) + { + Penumbra.Log.Error( + $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}." ); + Interface = Empty; + configChanged = true; + } + else + { + Interface = this[ interfaceIdx ]; + } + // Load the current collection. var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) { - Penumbra.Log.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); + Penumbra.Log.Error( + $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); Current = DefaultName; configChanged = true; } @@ -268,13 +293,14 @@ public partial class ModCollection public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), - () => SaveActiveCollections( Default.Name, Current.Name, Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ), + () => SaveActiveCollections( Default.Name, Interface.Name, Current.Name, + Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ), _specialCollections.WithIndex() .Where( c => c.Item1 != null ) .Select( c => ( ( CollectionType )c.Item2, c.Item1!.Name ) ) ) ); } - internal static void SaveActiveCollections( string def, string current, IEnumerable< (string, string) > characters, + internal static void SaveActiveCollections( string def, string ui, string current, IEnumerable< (string, string) > characters, IEnumerable< (CollectionType, string) > special ) { var file = ActiveCollectionFile; @@ -287,6 +313,8 @@ public partial class ModCollection j.WriteStartObject(); j.WritePropertyName( nameof( Default ) ); j.WriteValue( def ); + j.WritePropertyName( nameof( Interface ) ); + j.WriteValue( ui ); j.WritePropertyName( nameof( Current ) ); j.WriteValue( current ); foreach( var (type, collection) in special ) @@ -350,6 +378,7 @@ public partial class ModCollection private void CreateNecessaryCaches() { Default.CreateCache(); + Interface.CreateCache(); Current.CreateCache(); foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) @@ -362,6 +391,7 @@ public partial class ModCollection { if( idx != Empty.Index && idx != Default.Index + && idx != Interface.Index && idx != Current.Index && _specialCollections.All( c => c == null || c.Index != idx ) && _characters.Values.All( c => c.Index != idx ) ) diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 2946498d..e71c0433 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -45,8 +45,9 @@ public enum CollectionType : byte Inactive, // A collection was added or removed Default, // The default collection was changed + Interface, // The ui collection was changed Character, // A character collection was changed - Current, // The current collection was changed. + Current, // The current collection was changed } public static class CollectionTypeExtensions @@ -96,6 +97,7 @@ public static class CollectionTypeExtensions CollectionType.VeenaNpc => SubRace.Veena.ToName() + " (NPC)", CollectionType.Inactive => "Collection", CollectionType.Default => "Default", + CollectionType.Interface => "Interface", CollectionType.Character => "Character", CollectionType.Current => "Current", _ => string.Empty, diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index cd11594b..2a2335c3 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -47,12 +47,35 @@ public partial class Configuration m.Version2To3(); m.Version3To4(); m.Version4To5(); + m.Version5To6(); + } + + // A new tutorial step was inserted in the middle. + // The UI collection and a new tutorial for it was added. + // The migration for the UI collection itself happens in the ActiveCollections file. + private void Version5To6() + { + if( _config.Version != 5 ) + { + return; + } + if( _config.TutorialStep == 25 ) + { + _config.TutorialStep = 27; + } + + _config.Version = 6; } // Mod backup extension was changed from .zip to .pmp. // Actual migration takes place in ModManager. private void Version4To5() { + if( _config.Version != 4 ) + { + return; + } + Mod.Manager.MigrateModBackups = true; _config.Version = 5; } @@ -189,7 +212,7 @@ public partial class Configuration CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; - ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, + ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, DefaultCollection, CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 65eeb5db..bd4231b9 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -142,7 +142,7 @@ public partial class Configuration : IPluginConfiguration // Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 5; + public const int CurrentVersion = 6; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f0cd656b..3732dd9a 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -121,6 +121,12 @@ public unsafe partial class ResourceLoader } path = path.ToLower(); + if( category == ResourceCategory.Ui ) + { + var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); + return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); + } + if( ResolvePathCustomization != null ) { foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 62e70d7a..fa5b3fe7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -276,6 +276,7 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + ShutdownWebServer(); DisposeInterface(); Ipc?.Dispose(); Api?.Dispose(); @@ -289,8 +290,6 @@ public class Penumbra : IDalamudPlugin ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); - - ShutdownWebServer(); } public static bool SetCollection( string type, string collectionName ) @@ -481,6 +480,7 @@ public class Penumbra : IDalamudPlugin sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 ); sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) ); sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName ); + sb.AppendFormat( "> **`Interface Collection: `** {0}\n", CollectionManager.Interface.AnonymizedName ); sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName ); foreach( var type in CollectionTypeExtensions.Special ) { diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 9c4a4547..ffd7adf3 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -129,10 +129,19 @@ public partial class ConfigWindow DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker( DefaultCollection, - $"Mods in the {DefaultCollection} are loaded for anything that is not associated with a character in the game " + $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," + "as well as any character for whom no more specific conditions from below apply." ); } + private void DrawInterfaceCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true, null ); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker( InterfaceCollection, + $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); + } + // We do not check for valid character names. private void DrawNewSpecialCollection() { @@ -272,6 +281,8 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); DrawDefaultCollectionSelector(); OpenTutorial( BasicTutorialSteps.DefaultCollection ); + DrawInterfaceCollectionSelector(); + OpenTutorial( BasicTutorialSteps.InterfaceCollection ); ImGui.Dummy( _window._defaultSpace ); DrawSpecialAssignments(); diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index f4973eb8..97941f7b 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -10,6 +10,7 @@ public partial class ConfigWindow { public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; + public const string InterfaceCollection = "Interface Collection"; public const string ActiveCollections = "Active Collections"; public const string AssignedCollections = "Assigned Collections"; public const string GroupAssignment = "Group Assignment"; @@ -21,7 +22,7 @@ public partial class ConfigWindow public const string SupportedRedrawModifiers = " - 'self' or '': your own character\n" + " - 'target' or '': your target\n" + " - 'focus' or ': your focus target\n" - + " - 'mouseover' or '': the actor you are currently hovering\n" + + " - 'mouseover' or '': the actor you are currently hovering over\n" + " - any specific actor name to redraw all actors of that exactly matching name."; private static void UpdateTutorialStep() @@ -56,6 +57,7 @@ public partial class ConfigWindow Inheritance, ActiveCollections, DefaultCollection, + InterfaceCollection, SpecialCollections1, SpecialCollections2, Mods, @@ -111,7 +113,10 @@ public partial class ConfigWindow .Register( $"Initial Setup, Step 7: {DefaultCollection}", $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" - + "This is also the collection you need to use for all UI mods, music mods or any mods not associated with a character in the game at all." ) + + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods." ) + .Register( "Interface Collection", + $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" + + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") .Register( GroupAssignment + 's', "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" From 5538c5704dbf3bcfe5d094dfa5385aa7f26fc666 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 17 Sep 2022 23:27:38 +0200 Subject: [PATCH 0485/2451] Add IPC for Interface Collection. --- Penumbra/Api/IPenumbraApi.cs | 11 +++++++++-- Penumbra/Api/IpcTester.cs | 9 +++++++++ Penumbra/Api/PenumbraApi.cs | 18 ++++++++++++++--- Penumbra/Api/PenumbraIpc.cs | 28 ++++++++++++++++++++++++++- Penumbra/UI/ConfigWindow.Changelog.cs | 17 ++++++++++++++-- Penumbra/UI/ConfigWindow.Misc.cs | 5 ++++- 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index cfc373df..257cfbde 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -96,9 +96,13 @@ public interface IPenumbraApi : IPenumbraApiBase // Queue redrawing of all currently available actors with the given RedrawType. public void RedrawAll( RedrawType setting ); - // Resolve a given gamePath via Penumbra using the Default and Forced collections. + // Resolve a given gamePath via Penumbra using the Default collection. // Returns the given gamePath if penumbra would not manipulate it. - public string ResolvePath( string gamePath ); + public string ResolveDefaultPath( string gamePath ); + + // Resolve a given gamePath via Penumbra using the Interface collection. + // Returns the given gamePath if penumbra would not manipulate it. + public string ResolveInterfacePath( string gamePath ); // Resolve a given gamePath via Penumbra using the character collection for the given name (if it exists) and the Forced collections. // Returns the given gamePath if penumbra would not manipulate it. @@ -133,6 +137,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the name of the default collection. public string GetDefaultCollection(); + // Obtain the name of the interface collection. + public string GetInterfaceCollection(); + // Obtain the name of the collection associated with characterName and whether it is configured or inferred from default. public (string, bool) GetCharacterCollection( string characterName ); diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 133f5f7e..1fd29dec 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -311,6 +311,13 @@ public class IpcTester : IDisposable .InvokeFunc( _currentResolvePath ) ); } + DrawIntro( PenumbraIpc.LabelProviderResolveInterface, "Interface Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveInterface ) + .InvokeFunc( _currentResolvePath ) ); + } + DrawIntro( PenumbraIpc.LabelProviderResolveCharacter, "Character Collection Resolve" ); if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) { @@ -568,6 +575,8 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderCurrentCollectionName ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderDefaultCollectionName, "Default Collection" ); ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderDefaultCollectionName ).InvokeFunc() ); + DrawIntro( PenumbraIpc.LabelProviderInterfaceCollectionName, "Interface Collection" ); + ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderInterfaceCollectionName ).InvokeFunc() ); DrawIntro( PenumbraIpc.LabelProviderCharacterCollectionName, "Character" ); ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index eb1a189b..fad36b1c 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -21,7 +21,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 13 ); + => ( 4, 14 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; @@ -141,12 +141,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - public string ResolvePath( string path ) + public string ResolveDefaultPath( string path ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); } + public string ResolveInterfacePath( string path ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Interface ); + } + public string ResolvePlayerPath( string path ) { CheckInitialized(); @@ -185,7 +191,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); + => GetFileIntern< T >( ResolveDefaultPath( gamePath ) ); public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); @@ -233,6 +239,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.CollectionManager.Default.Name; } + public string GetInterfaceCollection() + { + CheckInitialized(); + return Penumbra.CollectionManager.Interface.Name; + } + public (string, bool) GetCharacterCollection( string characterName ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index eb9035e1..ab9496c5 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -274,6 +274,7 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; + public const string LabelProviderResolveInterface = "Penumbra.ResolveInterfacePath"; public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; @@ -285,6 +286,7 @@ public partial class PenumbraIpc public const string LabelProviderGameObjectResourcePathResolved = "Penumbra.GameObjectResourcePathResolved"; internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string >? ProviderResolveInterface; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; internal ICallGateProvider< string, string >? ProviderResolvePlayer; internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; @@ -300,13 +302,23 @@ public partial class PenumbraIpc try { ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); - ProviderResolveDefault.RegisterFunc( Api.ResolvePath ); + ProviderResolveDefault.RegisterFunc( Api.ResolveDefaultPath ); } catch( Exception e ) { Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); } + try + { + ProviderResolveInterface = pi.GetIpcProvider< string, string >( LabelProviderResolveInterface ); + ProviderResolveInterface.RegisterFunc( Api.ResolveInterfacePath ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveInterface}:\n{e}" ); + } + try { ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); @@ -411,6 +423,7 @@ public partial class PenumbraIpc ProviderGetDrawObjectInfo?.UnregisterFunc(); ProviderGetCutsceneParentIndex?.UnregisterFunc(); ProviderResolveDefault?.UnregisterFunc(); + ProviderResolveInterface?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc(); ProviderReverseResolvePathPlayer?.UnregisterFunc(); @@ -499,6 +512,7 @@ public partial class PenumbraIpc public const string LabelProviderGetCollections = "Penumbra.GetCollections"; public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; + public const string LabelProviderInterfaceCollectionName = "Penumbra.GetInterfaceCollectionName"; public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; public const string LabelProviderGetPlayerMetaManipulations = "Penumbra.GetPlayerMetaManipulations"; public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; @@ -507,6 +521,7 @@ public partial class PenumbraIpc internal ICallGateProvider< IList< string > >? ProviderGetCollections; internal ICallGateProvider< string >? ProviderCurrentCollectionName; internal ICallGateProvider< string >? ProviderDefaultCollectionName; + internal ICallGateProvider< string >? ProviderInterfaceCollectionName; internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; internal ICallGateProvider< string >? ProviderGetPlayerMetaManipulations; internal ICallGateProvider< string, string >? ProviderGetMetaManipulations; @@ -553,6 +568,16 @@ public partial class PenumbraIpc Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); } + try + { + ProviderInterfaceCollectionName = pi.GetIpcProvider( LabelProviderInterfaceCollectionName ); + ProviderInterfaceCollectionName.RegisterFunc( Api.GetInterfaceCollection ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderInterfaceCollectionName}:\n{e}" ); + } + try { ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); @@ -590,6 +615,7 @@ public partial class PenumbraIpc ProviderGetCollections?.UnregisterFunc(); ProviderCurrentCollectionName?.UnregisterFunc(); ProviderDefaultCollectionName?.UnregisterFunc(); + ProviderInterfaceCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc(); ProviderGetMetaManipulations?.UnregisterFunc(); } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index d4c6cb04..013e7357 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -18,16 +18,29 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); + Add5_8_0( ret ); return ret; } private static void Add5_8_0( Changelog log ) => log.NextVersion( "Version 0.5.8.0" ) - .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) + .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) + .RegisterEntry( "Added an Interface Collection assignment." ) + .RegisterEntry( "All your UI mods will have to be in the interface collection.", 1 ) + .RegisterEntry( "Files that are categorized as UI files by the game will only check for redirections in this collection.", 1 ) + .RegisterHighlight( + "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) + .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) + .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) + .RegisterEntry( + "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) + .RegisterEntry( "Collection Selectors now display None at the top if available." ) .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) .RegisterEntry( "Fixed issues with and improved mod option editing." ) - .RegisterEntry( "Backend optimizations." ); + .RegisterEntry( "Backend optimizations." ) + .RegisterEntry( "Changed metadata change system again.", 1 ) + .RegisterEntry( "Improved logging efficiency.", 1 ); private static void Add5_7_1( Changelog log ) => log.NextVersion( "Version 0.5.7.1" ) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index c4141b73..5b302ade 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -100,7 +100,10 @@ public partial class ConfigWindow using var combo = ImRaii.Combo( label, current?.Name ?? string.Empty ); if( combo ) { - foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) + var enumerator = Penumbra.CollectionManager.OrderBy( c => c.Name ).AsEnumerable(); + if( withEmpty ) + enumerator = enumerator.Prepend( ModCollection.Empty ); + foreach( var collection in enumerator ) { using var id = ImRaii.PushId( collection.Index ); if( ImGui.Selectable( collection.Name, collection == current ) ) From 358064cd5fc63100ab5fe5ec1fd6bb0a2ead7bfa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Sep 2022 02:39:16 +0200 Subject: [PATCH 0486/2451] Move redraw buttons to mod panel. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 74 +++++++++++++++++++++++-- Penumbra/UI/ConfigWindow.SettingsTab.cs | 30 ---------- Penumbra/UI/ConfigWindow.Tutorial.cs | 15 ++--- 4 files changed, 78 insertions(+), 43 deletions(-) diff --git a/OtterGui b/OtterGui index 41581a5d..28c4d856 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 41581a5d035d46a89819aaefaa8d01c815788eee +Subproject commit 28c4d8564296484b3d0fc0d2ea275cded8b1daa2 diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index c3288214..2a0e193e 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -7,6 +7,8 @@ using Penumbra.UI.Classes; using System; using System.Linq; using System.Numerics; +using Dalamud.Interface; +using Penumbra.GameData.Enums; namespace Penumbra.UI; @@ -33,11 +35,22 @@ public partial class ConfigWindow using var group = ImRaii.Group(); DrawHeaderLine(); - using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar ); - if( child ) + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); + + using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, -ImGui.GetFrameHeight() ), true, + ImGuiWindowFlags.HorizontalScrollbar ) ) { - _modPanel.Draw( _selector ); + style.Pop(); + if( child ) + { + _modPanel.Draw( _selector ); + } + + style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); } + + style.Push( ImGuiStyleVar.FrameRounding, 0 ); + DrawRedrawLine(); } catch( Exception e ) { @@ -48,18 +61,69 @@ public partial class ConfigWindow + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select(c => c.AnonymizedName) )} Inheritances\n" + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); } } + private void DrawRedrawLine() + { + var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); + var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); + using( var _ = ImRaii.Group() ) + { + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); + ImGui.SameLine(); + } + + ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); + } + + var hovered = ImGui.IsItemHovered(); + OpenTutorial( BasicTutorialSteps.Redrawing ); + if( hovered ) + { + ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); + } + + void DrawButton( Vector2 size, string label, string lower ) + { + if( ImGui.Button( label, size ) ) + { + if( lower.Length > 0 ) + { + _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + } + else + { + _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + + ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); + } + + using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); + ImGui.SameLine(); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; + DrawButton( buttonWidth, "All", string.Empty ); + ImGui.SameLine(); + DrawButton( buttonWidth, "Self", "self" ); + ImGui.SameLine(); + DrawButton( buttonWidth, "Target", "target" ); + ImGui.SameLine(); + DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); + } + // Draw the header line that can quick switch between collections. private void DrawHeaderLine() { using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - using( var group = ImRaii.Group() ) + using( var _ = ImRaii.Group() ) { DrawDefaultCollectionButton( 3 * buttonSize ); ImGui.SameLine(); diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 2f6f39c1..10f5e1b3 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -53,8 +53,6 @@ public partial class ConfigWindow : _window.Flags & ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); } ); - DrawRedrawButton(); - ImGui.NewLine(); DrawRootFolder(); DrawRediscoverButton(); @@ -338,34 +336,6 @@ public partial class ConfigWindow + "Not directly affiliated and potentially, but not usually out of date." ); } - private void DrawRedrawButton() - { - using( var group = ImRaii.Group() ) - { - using var disabled = ImRaii.Disabled( Dalamud.ClientState.LocalPlayer == null ); - - if( ImGui.Button( "Redraw Self" ) ) - { - _window._penumbra.ObjectReloader.RedrawObject( "self", RedrawType.Redraw ); - } - - ImGuiUtil.HoverTooltip( "Executes '/penumbra redraw self'." ); - - ImGui.SameLine(); - if( ImGui.Button( "Redraw All" ) ) - { - _window._penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - ImGuiUtil.HoverTooltip( "Executes '/penumbra redraw'." ); - - ImGui.SameLine(); - - ImGuiComponents.HelpMarker( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); - } - - OpenTutorial( BasicTutorialSteps.Redrawing ); - } - private void DrawSupportButtons() { var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 97941f7b..04fa3513 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -19,7 +19,8 @@ public partial class ConfigWindow public const string ConditionalIndividual = "Character"; public const string IndividualAssignments = "Individual Assignments"; - public const string SupportedRedrawModifiers = " - 'self' or '': your own character\n" + public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" + + " - 'self' or '': your own character\n" + " - 'target' or '': your target\n" + " - 'focus' or ': your focus target\n" + " - 'mouseover' or '': the actor you are currently hovering over\n" @@ -50,7 +51,6 @@ public partial class ConfigWindow EnableMods, AdvancedSettings, GeneralSettings, - Redrawing, Collections, EditingCollections, CurrentCollection, @@ -65,6 +65,7 @@ public partial class ConfigWindow AdvancedHelp, ModFilters, CollectionSelectors, + Redrawing, EnablingMods, Priority, ModOptions, @@ -91,10 +92,6 @@ public partial class ConfigWindow + "If you need to do any editing of your mods, you will have to turn it on later." ) .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + "If you do not know what some of these do yet, return to this later!" ) - .Register( "Redrawing", - "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" - + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" - + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too." ) .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" + "This is our next stop!\n\n" + "Go here after setting up your root folder to continue the tutorial!" ) @@ -116,7 +113,7 @@ public partial class ConfigWindow + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods." ) .Register( "Interface Collection", $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" - + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") + + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one." ) .Register( GroupAssignment + 's', "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" @@ -137,6 +134,10 @@ public partial class ConfigWindow + $"The first button sets it to your {DefaultCollection} (if any).\n\n" + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" + "The third is a regular collection selector to let you choose among all your collections." ) + .Register( "Redrawing", + "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" + + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" + + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too." ) .Register( "Initial Setup, Step 11: Enabling Mods", "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance." ) From 257c0d390b695480244f29310b11f0623156fdd8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Sep 2022 13:40:56 +0200 Subject: [PATCH 0487/2451] Let Eqdp change all files in the racial tree instead of just the own race code. --- OtterGui | 2 +- .../Interop/Resolver/PathResolver.Meta.cs | 80 +++++++++++++++++-- .../Resolver/PathResolver.ResolverHooks.cs | 12 +-- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/OtterGui b/OtterGui index 28c4d856..98064e79 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 28c4d8564296484b3d0fc0d2ea275cded8b1daa2 +Subproject commit 98064e790042c90c82a58fbfa79201bd69800758 diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index ba52182a..d8f1db0a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -1,9 +1,13 @@ using System; +using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.Collections; using Penumbra.GameData.Enums; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; +using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.Interop.Resolver; @@ -77,10 +81,8 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - var race = GetDrawObjectGenderRace( drawObject ); using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); - using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } else @@ -106,10 +108,8 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - var race = GetDrawObjectGenderRace( drawObject ); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp1 = collection.ModCollection.TemporarilySetEqdpFile( race, false ); - using var eqdp2 = collection.ModCollection.TemporarilySetEqdpFile( race, true ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _updateModelsHook.Original.Invoke( drawObject ); } else @@ -130,7 +130,7 @@ public unsafe partial class PathResolver } } - return GenderRace.Unknown; + return Unknown; } public static GenderRace GetHumanGenderRace( IntPtr human ) @@ -203,5 +203,69 @@ public unsafe partial class PathResolver using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; return _changeCustomize.Original( human, data, skipEquipment ); } + + public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory ) + { + DisposableContainer Convert( params GenderRace[] races ) + { + var equipmentEnumerable = + equipment + ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) + : Array.Empty().AsEnumerable(); + var accessoryEnumerable = + accessory + ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) + : Array.Empty().AsEnumerable(); + return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); + } + + return race switch + { + MidlanderMale => Convert( MidlanderMale ), + HighlanderMale => Convert( MidlanderMale, HighlanderMale ), + ElezenMale => Convert( MidlanderMale, ElezenMale ), + MiqoteMale => Convert( MidlanderMale, MiqoteMale ), + RoegadynMale => Convert( MidlanderMale, RoegadynMale ), + LalafellMale => Convert( MidlanderMale, LalafellMale ), + AuRaMale => Convert( MidlanderMale, AuRaMale ), + HrothgarMale => Convert( MidlanderMale, RoegadynMale, HrothgarMale ), + VieraMale => Convert( MidlanderMale, VieraMale ), + + MidlanderFemale => Convert( MidlanderMale, MidlanderFemale ), + HighlanderFemale => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale ), + ElezenFemale => Convert( MidlanderMale, MidlanderFemale, ElezenFemale ), + MiqoteFemale => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale ), + RoegadynFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale ), + LalafellFemale => Convert( MidlanderMale, LalafellMale, LalafellFemale ), + AuRaFemale => Convert( MidlanderMale, MidlanderFemale, AuRaFemale ), + HrothgarFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale ), + VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), + + MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), + HighlanderMaleNpc => Convert( MidlanderMale, HighlanderMale, HighlanderMaleNpc ), + ElezenMaleNpc => Convert( MidlanderMale, ElezenMale, ElezenMaleNpc ), + MiqoteMaleNpc => Convert( MidlanderMale, MiqoteMale, MiqoteMaleNpc ), + RoegadynMaleNpc => Convert( MidlanderMale, RoegadynMale, RoegadynMaleNpc ), + LalafellMaleNpc => Convert( MidlanderMale, LalafellMale, LalafellMaleNpc ), + AuRaMaleNpc => Convert( MidlanderMale, AuRaMale, AuRaMaleNpc ), + HrothgarMaleNpc => Convert( MidlanderMale, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), + VieraMaleNpc => Convert( MidlanderMale, VieraMale, VieraMaleNpc ), + + MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MidlanderFemaleNpc ), + HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale, HighlanderFemaleNpc ), + ElezenFemaleNpc => Convert( MidlanderMale, MidlanderFemale, ElezenFemale, ElezenFemaleNpc ), + MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale, MiqoteFemaleNpc ), + RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, RoegadynFemaleNpc ), + LalafellFemaleNpc => Convert( MidlanderMale, LalafellMale, LalafellFemale, LalafellFemaleNpc ), + AuRaFemaleNpc => Convert( MidlanderMale, MidlanderFemale, AuRaFemale, AuRaFemaleNpc ), + HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale, HrothgarFemaleNpc ), + VieraFemaleNpc => Convert( MidlanderMale, MidlanderFemale, VieraFemale, VieraFemaleNpc ), + + UnknownMaleNpc => Convert( MidlanderMale, UnknownMaleNpc ), + UnknownFemaleNpc => Convert( MidlanderMale, MidlanderFemale, UnknownFemaleNpc ), + _ => DisposableContainer.Empty, + }; + } + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index b5335dc8..7841027a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -143,21 +143,15 @@ public partial class PathResolver private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType ) { - CharacterUtility.List.MetaReverter? Get() + DisposableContainer Get() { if( modelType > 9 ) { - return null; - } - - var race = MetaState.GetHumanGenderRace( drawObject ); - if( race == GenderRace.Unknown ) - { - return null; + return DisposableContainer.Empty; } var data = GetResolveData( drawObject ); - return !data.Valid ? null : data.ModCollection.TemporarilySetEqdpFile( race, modelType > 4 ); + return !data.Valid ? DisposableContainer.Empty : MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4); } using var eqdp = Get(); From 57e66f9b66486fda20619393bfd8ece4a33eb532 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 Sep 2022 13:19:08 +0200 Subject: [PATCH 0488/2451] Fix some problems with super early files and meta files. --- .../Collections/CollectionManager.Active.cs | 28 +++---- .../Collections/ModCollection.Cache.Access.cs | 76 ++++-------------- Penumbra/Interop/CharacterUtility.List.cs | 10 ++- Penumbra/Interop/CharacterUtility.cs | 4 +- .../Resolver/PathResolver.DrawObjectState.cs | 2 +- .../Interop/Resolver/PathResolver.Meta.cs | 77 +++++++++---------- Penumbra/Meta/Manager/MetaManager.cs | 55 ++++++++++--- Penumbra/Penumbra.cs | 1 + 8 files changed, 120 insertions(+), 133 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 7a8bb658..12b42e91 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Penumbra.Collections; @@ -215,7 +216,7 @@ public partial class ModCollection } // Load the interface collection. - var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? (configChanged ? Empty.Name : Default.Name); + var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? ( configChanged ? Empty.Name : Default.Name ); var interfaceIdx = GetIndexForCollectionName( interfaceName ); if( interfaceIdx < 0 ) { @@ -285,8 +286,6 @@ public partial class ModCollection { SaveActiveCollections(); } - - CreateNecessaryCaches(); } @@ -363,7 +362,6 @@ public partial class ModCollection return false; } - // Save if any of the active collections is changed. private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) { @@ -373,18 +371,20 @@ public partial class ModCollection } } - - // Cache handling. - private void CreateNecessaryCaches() + // Cache handling. Usually recreate caches on the next framework tick, + // but at launch create all of them at once. + public void CreateNecessaryCaches() { - Default.CreateCache(); - Interface.CreateCache(); - Current.CreateCache(); + var tasks = _specialCollections.OfType< ModCollection >() + .Concat( _characters.Values ) + .Prepend( Current ) + .Prepend( Default ) + .Prepend( Interface ) + .Distinct() + .Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) ) + .ToArray(); - foreach( var collection in _specialCollections.OfType< ModCollection >().Concat( _characters.Values ) ) - { - collection.CreateCache(); - } + Task.WaitAll( tasks ); } private void RemoveCache( int idx ) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 351ae2fc..7f379dc5 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -133,67 +133,6 @@ public partial class ModCollection Penumbra.Log.Debug( $"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished." ); } - // Set Metadata files. - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.ResetEqpFiles(); - } - else - { - _cache.MetaManipulations.SetEqpFiles(); - } - } - - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.ResetEqdpFiles(); - } - else - { - _cache.MetaManipulations.SetEqdpFiles(); - } - } - - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.ResetGmpFiles(); - } - else - { - _cache.MetaManipulations.SetGmpFiles(); - } - } - - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.ResetEstFiles(); - } - else - { - _cache.MetaManipulations.SetEstFiles(); - } - } - - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.ResetCmpFiles(); - } - else - { - _cache.MetaManipulations.SetCmpFiles(); - } - } - public void SetFiles() { if( _cache == null ) @@ -207,6 +146,18 @@ public partial class ModCollection } } + public void SetMetaFile( Interop.Structs.CharacterUtility.Index idx ) + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetResource( idx ); + } + else + { + _cache.MetaManipulations.SetFile( idx ); + } + } + // Used for short periods of changed files. public CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ); @@ -222,5 +173,4 @@ public partial class ModCollection public CharacterUtility.List.MetaReverter? TemporarilySetEstFile( EstManipulation.EstType type ) => _cache?.MetaManipulations.TemporarilySetEstFile( type ); - -} +} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index 27146af6..e6f412ae 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -50,7 +50,8 @@ public unsafe partial class CharacterUtility public MetaReverter TemporarilyResetResource() { - Penumbra.Log.Verbose( $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); + Penumbra.Log.Verbose( + $"Temporarily reset resource {GlobalIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)." ); var reverter = new MetaReverter( this ); _entries.AddFirst( reverter ); ResetResourceInternal(); @@ -84,6 +85,9 @@ public unsafe partial class CharacterUtility private void ResetResourceInternal() => SetResourceInternal( _defaultResourceData, _defaultResourceSize ); + private void SetResourceToDefaultCollection() + => Penumbra.CollectionManager.Default.SetMetaFile( GlobalIndex ); + public void Dispose() { if( _entries.Count > 0 ) @@ -127,14 +131,14 @@ public unsafe partial class CharacterUtility if( list.Count == 0 ) { - List.ResetResourceInternal(); + List.SetResourceToDefaultCollection(); } else { var next = list.First!.Value; if( next.Resetter ) { - List.ResetResourceInternal(); + List.SetResourceToDefaultCollection(); } else { diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index a5b155c1..52e842c9 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -78,9 +78,9 @@ public unsafe partial class CharacterUtility : IDisposable if( !anyMissing ) { - Ready = true; - LoadingFinished.Invoke(); + Ready = true; Dalamud.Framework.Update -= LoadDefaultResources; + LoadingFinished.Invoke(); } } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index d6e6ec0e..9c71a121 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -56,7 +56,7 @@ public unsafe partial class PathResolver { if( type == ResourceType.Tex && LastCreatedCollection.Valid - && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) ) + && gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l' ) ) { resolveData = LastCreatedCollection; return true; diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index d8f1db0a..23d7e21c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -81,8 +81,8 @@ public unsafe partial class PathResolver var collection = GetResolveData( drawObject ); if( collection.Valid ) { - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); _onModelLoadCompleteHook.Original.Invoke( drawObject ); } else @@ -211,61 +211,60 @@ public unsafe partial class PathResolver var equipmentEnumerable = equipment ? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) ) - : Array.Empty().AsEnumerable(); + : Array.Empty< IDisposable? >().AsEnumerable(); var accessoryEnumerable = accessory ? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) ) - : Array.Empty().AsEnumerable(); + : Array.Empty< IDisposable? >().AsEnumerable(); return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) ); } return race switch { - MidlanderMale => Convert( MidlanderMale ), + MidlanderMale => Convert( MidlanderMale ), HighlanderMale => Convert( MidlanderMale, HighlanderMale ), - ElezenMale => Convert( MidlanderMale, ElezenMale ), - MiqoteMale => Convert( MidlanderMale, MiqoteMale ), - RoegadynMale => Convert( MidlanderMale, RoegadynMale ), - LalafellMale => Convert( MidlanderMale, LalafellMale ), - AuRaMale => Convert( MidlanderMale, AuRaMale ), - HrothgarMale => Convert( MidlanderMale, RoegadynMale, HrothgarMale ), - VieraMale => Convert( MidlanderMale, VieraMale ), + ElezenMale => Convert( MidlanderMale, ElezenMale ), + MiqoteMale => Convert( MidlanderMale, MiqoteMale ), + RoegadynMale => Convert( MidlanderMale, RoegadynMale ), + LalafellMale => Convert( MidlanderMale, LalafellMale ), + AuRaMale => Convert( MidlanderMale, AuRaMale ), + HrothgarMale => Convert( MidlanderMale, RoegadynMale, HrothgarMale ), + VieraMale => Convert( MidlanderMale, VieraMale ), - MidlanderFemale => Convert( MidlanderMale, MidlanderFemale ), + MidlanderFemale => Convert( MidlanderMale, MidlanderFemale ), HighlanderFemale => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale ), - ElezenFemale => Convert( MidlanderMale, MidlanderFemale, ElezenFemale ), - MiqoteFemale => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale ), - RoegadynFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale ), - LalafellFemale => Convert( MidlanderMale, LalafellMale, LalafellFemale ), - AuRaFemale => Convert( MidlanderMale, MidlanderFemale, AuRaFemale ), - HrothgarFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale ), - VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), + ElezenFemale => Convert( MidlanderMale, MidlanderFemale, ElezenFemale ), + MiqoteFemale => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale ), + RoegadynFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale ), + LalafellFemale => Convert( MidlanderMale, LalafellMale, LalafellFemale ), + AuRaFemale => Convert( MidlanderMale, MidlanderFemale, AuRaFemale ), + HrothgarFemale => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale ), + VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), - MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), + MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), HighlanderMaleNpc => Convert( MidlanderMale, HighlanderMale, HighlanderMaleNpc ), - ElezenMaleNpc => Convert( MidlanderMale, ElezenMale, ElezenMaleNpc ), - MiqoteMaleNpc => Convert( MidlanderMale, MiqoteMale, MiqoteMaleNpc ), - RoegadynMaleNpc => Convert( MidlanderMale, RoegadynMale, RoegadynMaleNpc ), - LalafellMaleNpc => Convert( MidlanderMale, LalafellMale, LalafellMaleNpc ), - AuRaMaleNpc => Convert( MidlanderMale, AuRaMale, AuRaMaleNpc ), - HrothgarMaleNpc => Convert( MidlanderMale, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), - VieraMaleNpc => Convert( MidlanderMale, VieraMale, VieraMaleNpc ), + ElezenMaleNpc => Convert( MidlanderMale, ElezenMale, ElezenMaleNpc ), + MiqoteMaleNpc => Convert( MidlanderMale, MiqoteMale, MiqoteMaleNpc ), + RoegadynMaleNpc => Convert( MidlanderMale, RoegadynMale, RoegadynMaleNpc ), + LalafellMaleNpc => Convert( MidlanderMale, LalafellMale, LalafellMaleNpc ), + AuRaMaleNpc => Convert( MidlanderMale, AuRaMale, AuRaMaleNpc ), + HrothgarMaleNpc => Convert( MidlanderMale, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), + VieraMaleNpc => Convert( MidlanderMale, VieraMale, VieraMaleNpc ), - MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MidlanderFemaleNpc ), + MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MidlanderFemaleNpc ), HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale, HighlanderFemaleNpc ), - ElezenFemaleNpc => Convert( MidlanderMale, MidlanderFemale, ElezenFemale, ElezenFemaleNpc ), - MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale, MiqoteFemaleNpc ), - RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, RoegadynFemaleNpc ), - LalafellFemaleNpc => Convert( MidlanderMale, LalafellMale, LalafellFemale, LalafellFemaleNpc ), - AuRaFemaleNpc => Convert( MidlanderMale, MidlanderFemale, AuRaFemale, AuRaFemaleNpc ), - HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale, HrothgarFemaleNpc ), - VieraFemaleNpc => Convert( MidlanderMale, MidlanderFemale, VieraFemale, VieraFemaleNpc ), + ElezenFemaleNpc => Convert( MidlanderMale, MidlanderFemale, ElezenFemale, ElezenFemaleNpc ), + MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale, MiqoteFemaleNpc ), + RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, RoegadynFemaleNpc ), + LalafellFemaleNpc => Convert( MidlanderMale, LalafellMale, LalafellFemale, LalafellFemaleNpc ), + AuRaFemaleNpc => Convert( MidlanderMale, MidlanderFemale, AuRaFemale, AuRaFemaleNpc ), + HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale, HrothgarFemaleNpc ), + VieraFemaleNpc => Convert( MidlanderMale, MidlanderFemale, VieraFemale, VieraFemaleNpc ), - UnknownMaleNpc => Convert( MidlanderMale, UnknownMaleNpc ), + UnknownMaleNpc => Convert( MidlanderMale, UnknownMaleNpc ), UnknownFemaleNpc => Convert( MidlanderMale, MidlanderFemale, UnknownFemaleNpc ), - _ => DisposableContainer.Empty, + _ => DisposableContainer.Empty, }; } - } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 701b77c0..d440e106 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using OtterGui; using Penumbra.Collections; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; @@ -83,17 +84,14 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } _manipulations[ manip ] = mod; - // Imc manipulations do not require character utility. - if( manip.ManipulationType == MetaManipulation.Type.Imc ) - { - return ApplyMod( manip.Imc ); - } if( !Penumbra.CharacterUtility.Ready ) { return true; } + // Imc manipulations do not require character utility, + // but they do require the file space to be ready. return manip.ManipulationType switch { MetaManipulation.Type.Eqp => ApplyMod( manip.Eqp ), @@ -101,6 +99,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -109,17 +108,13 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM public bool RevertMod( MetaManipulation manip ) { var ret = _manipulations.Remove( manip ); - // Imc manipulations do not require character utility. - if( manip.ManipulationType == MetaManipulation.Type.Imc ) - { - return RevertMod( manip.Imc ); - } - if( !Penumbra.CharacterUtility.Ready ) { return ret; } + // Imc manipulations do not require character utility, + // but they do require the file space to be ready. return manip.ManipulationType switch { MetaManipulation.Type.Eqp => RevertMod( manip.Eqp ), @@ -127,6 +122,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => RevertMod( manip.Eqdp ), MetaManipulation.Type.Est => RevertMod( manip.Est ), MetaManipulation.Type.Rsp => RevertMod( manip.Rsp ), + MetaManipulation.Type.Imc => RevertMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; @@ -150,6 +146,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM MetaManipulation.Type.Eqdp => ApplyMod( manip.Eqdp ), MetaManipulation.Type.Est => ApplyMod( manip.Est ), MetaManipulation.Type.Rsp => ApplyMod( manip.Rsp ), + MetaManipulation.Type.Imc => ApplyMod( manip.Imc ), MetaManipulation.Type.Unknown => false, _ => false, } @@ -167,6 +164,42 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM Penumbra.Log.Debug( $"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations." ); } + public void SetFile( CharacterUtility.Index index ) + { + switch( index ) + { + case CharacterUtility.Index.Eqp: + SetFile( _eqpFile, index ); + break; + case CharacterUtility.Index.Gmp: + SetFile( _gmpFile, index ); + break; + case CharacterUtility.Index.HumanCmp: + SetFile( _cmpFile, index ); + break; + case CharacterUtility.Index.FaceEst: + SetFile( _estFaceFile, index ); + break; + case CharacterUtility.Index.HairEst: + SetFile( _estHairFile, index ); + break; + case CharacterUtility.Index.HeadEst: + SetFile( _estHeadFile, index ); + break; + case CharacterUtility.Index.BodyEst: + SetFile( _estBodyFile, index ); + break; + default: + var i = CharacterUtility.EqdpIndices.IndexOf( index ); + if( i != -1 ) + { + SetFile( _eqdpFiles[ i ], index ); + } + + break; + } + } + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private static unsafe void SetFile( MetaBaseFile? file, CharacterUtility.Index index ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fa5b3fe7..b8ae544d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -94,6 +94,7 @@ public class Penumbra : IDalamudPlugin ModManager = new Mod.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); + CollectionManager.CreateNecessaryCaches(); ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); From ea023ebb5cd00eb06d74ab8e482437c65b56662d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 Sep 2022 18:52:49 +0200 Subject: [PATCH 0489/2451] Add handling for the 1.0 Decal texture. --- Penumbra.GameData/ByteString/FullPath.cs | 7 ++ .../Interop/CharacterUtility.DecalReverter.cs | 65 +++++++++++++++++++ Penumbra/Interop/CharacterUtility.cs | 25 +++++-- .../Loader/ResourceLoader.Replacement.cs | 10 ++- .../Interop/Loader/ResourceLoader.TexMdl.cs | 6 +- .../Resolver/PathResolver.DrawObjectState.cs | 23 ++++--- .../Interop/Resolver/PathResolver.Meta.cs | 3 + Penumbra/Interop/Structs/CharacterUtility.cs | 9 +++ Penumbra/Interop/Structs/ResourceHandle.cs | 16 +++++ 9 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 Penumbra/Interop/CharacterUtility.DecalReverter.cs diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs index 2284bf98..6d3cc0bd 100644 --- a/Penumbra.GameData/ByteString/FullPath.cs +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -31,6 +31,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath > Crc64 = Functions.ComputeCrc64( InternalName.Span ); } + public FullPath( Utf8GamePath path ) + { + FullName = path.ToString().Replace( '/', '\\' ); + InternalName = path.Path; + Crc64 = Functions.ComputeCrc64( InternalName.Span ); + } + public bool Exists => File.Exists( FullName ); diff --git a/Penumbra/Interop/CharacterUtility.DecalReverter.cs b/Penumbra/Interop/CharacterUtility.DecalReverter.cs new file mode 100644 index 00000000..a439ac48 --- /dev/null +++ b/Penumbra/Interop/CharacterUtility.DecalReverter.cs @@ -0,0 +1,65 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop; + +public unsafe partial class CharacterUtility +{ + public sealed class DecalReverter : IDisposable + { + public static readonly Utf8GamePath DecalPath = + Utf8GamePath.FromString( "chara/common/texture/decal_equip/_stigma.tex", out var p ) ? p : Utf8GamePath.Empty; + + public static readonly Utf8GamePath TransparentPath = + Utf8GamePath.FromString( "chara/common/texture/transparent.tex", out var p ) ? p : Utf8GamePath.Empty; + + private readonly Structs.TextureResourceHandle* _decal; + private readonly Structs.TextureResourceHandle* _transparent; + + public DecalReverter( ModCollection? collection, bool doDecal ) + { + var ptr = Penumbra.CharacterUtility.Address; + _decal = null; + _transparent = null; + if( doDecal ) + { + var decalPath = collection?.ResolvePath( DecalPath )?.InternalName ?? DecalPath.Path; + var decalHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, decalPath ); + _decal = ( Structs.TextureResourceHandle* )decalHandle; + if( _decal != null ) + { + ptr->DecalTexResource = _decal; + } + } + else + { + var transparentPath = collection?.ResolvePath( TransparentPath )?.InternalName ?? TransparentPath.Path; + var transparentHandle = Penumbra.ResourceLoader.ResolvePathSync( ResourceCategory.Chara, ResourceType.Tex, transparentPath ); + _transparent = ( Structs.TextureResourceHandle* )transparentHandle; + if( _transparent != null ) + { + ptr->TransparentTexResource = _transparent; + } + } + } + + public void Dispose() + { + var ptr = Penumbra.CharacterUtility.Address; + if( _decal != null ) + { + ptr->DecalTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultDecalResource; + --_decal->Handle.RefCount; + } + + if( _transparent != null ) + { + ptr->TransparentTexResource = ( Structs.TextureResourceHandle* )Penumbra.CharacterUtility._defaultTransparentResource; + --_transparent->Handle.RefCount; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 52e842c9..44b9204e 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -26,6 +26,8 @@ public unsafe partial class CharacterUtility : IDisposable public bool Ready { get; private set; } public event Action LoadingFinished; + private IntPtr _defaultTransparentResource; + private IntPtr _defaultDecalResource; // The relevant indices depend on which meta manipulations we allow for. // The defines are set in the project configuration. @@ -76,6 +78,18 @@ public unsafe partial class CharacterUtility : IDisposable } } + if( _defaultTransparentResource == IntPtr.Zero ) + { + _defaultTransparentResource = ( IntPtr )Address->TransparentTexResource; + anyMissing |= _defaultTransparentResource == IntPtr.Zero; + } + + if( _defaultDecalResource == IntPtr.Zero ) + { + _defaultDecalResource = ( IntPtr )Address->DecalTexResource; + anyMissing |= _defaultDecalResource == IntPtr.Zero; + } + if( !anyMissing ) { Ready = true; @@ -86,15 +100,15 @@ public unsafe partial class CharacterUtility : IDisposable public void SetResource( Structs.CharacterUtility.Index resourceIdx, IntPtr data, int length ) { - var idx = ReverseIndices[( int )resourceIdx]; - var list = _lists[idx.Value]; + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; list.SetResource( data, length ); } public void ResetResource( Structs.CharacterUtility.Index resourceIdx ) { - var idx = ReverseIndices[( int )resourceIdx]; - var list = _lists[idx.Value]; + var idx = ReverseIndices[ ( int )resourceIdx ]; + var list = _lists[ idx.Value ]; list.ResetResource(); } @@ -119,6 +133,9 @@ public unsafe partial class CharacterUtility : IDisposable { list.Dispose(); } + + Address->TransparentTexResource = ( Structs.TextureResourceHandle* )_defaultTransparentResource; + Address->DecalTexResource = ( Structs.TextureResourceHandle* )_defaultDecalResource; } public void Dispose() diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 3732dd9a..a0ca377f 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -71,7 +71,13 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, Utf8String path ) + { + var hash = path.Crc32; + return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); + } + + internal ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) @@ -86,7 +92,7 @@ public unsafe partial class ResourceLoader // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath, data ); + PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); if( resolvedPath == null ) { var retUnmodified = diff --git a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs index 2393a87d..67454300 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs @@ -26,7 +26,7 @@ public unsafe partial class ResourceLoader // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); - [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = "CheckFileStateDetour" )] + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = nameof(CheckFileStateDetour) )] public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) @@ -48,7 +48,7 @@ public unsafe partial class ResourceLoader // We hook the extern functions to just return the local one if given the custom flag as last argument. public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = "LoadTexFileExternDetour" )] + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = nameof(LoadTexFileExternDetour) )] public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) @@ -59,7 +59,7 @@ public unsafe partial class ResourceLoader public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); - [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = "LoadMdlFileExternDetour" )] + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = nameof(LoadMdlFileExternDetour) )] public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 9c71a121..d30248f9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -7,6 +7,7 @@ using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Api; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; @@ -135,18 +136,19 @@ public unsafe partial class PathResolver private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) { - CharacterUtility.List.MetaReverter? cmp = null; + var meta = DisposableContainer.Empty; if( LastGameObject != null ) { _lastCreatedCollection = IdentifyCollection( LastGameObject ); - var modelPtr = &a; - if( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default ) - { - cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(); - } - + // Change the transparent or 1.0 Decal if necessary. + var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); + // Change the rsp parameters if necessary. + meta = new DisposableContainer( _lastCreatedCollection.ModCollection != Penumbra.CollectionManager.Default + ? _lastCreatedCollection.ModCollection.TemporarilySetCmpFile() + : null, decal ); try { + var modelPtr = &a; CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); } catch( Exception e ) @@ -156,7 +158,7 @@ public unsafe partial class PathResolver } var ret = _characterBaseCreateHook.Original( a, b, c, d ); - using( cmp ) + using( meta ) { if( LastGameObject != null ) { @@ -168,6 +170,11 @@ public unsafe partial class PathResolver } } + // Check the customize array for the FaceCustomization byte and the last bit of that. + // Also check for humans. + public static bool UsesDecal( uint modelId, IntPtr customizeData ) + => modelId == 0 && ( ( byte* )customizeData )[ 12 ] > 0x7F; + // Remove DrawObjects from the list when they are destroyed. private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 23d7e21c..ba0c453b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -201,6 +201,9 @@ public unsafe partial class PathResolver _inChangeCustomize = true; var resolveData = GetResolveData( human ); using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; + using var decals = resolveData.Valid + ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal(0, data) ) + : null; return _changeCustomize.Original( human, data, skipEquipment ); } diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 40e346c1..e491de7b 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -80,6 +80,9 @@ public unsafe struct CharacterUtility BodyEst, } + public const int IndexTransparentTex = 72; + public const int IndexDecalTex = 73; + public static readonly Index[] EqdpIndices = Enum.GetNames< Index >() .Zip( Enum.GetValues< Index >() ) .Where( n => n.First.StartsWith( "Eqdp" ) ) @@ -157,5 +160,11 @@ public unsafe struct CharacterUtility [FieldOffset( 8 + ( int )Index.HeadEst * 8 )] public ResourceHandle* HeadEstResource; + [FieldOffset( 8 + IndexTransparentTex * 8 )] + public TextureResourceHandle* TransparentTexResource; + + [FieldOffset( 8 + IndexDecalTex * 8 )] + public TextureResourceHandle* DecalTexResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 6b3a8a72..c8b3522e 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -5,6 +5,22 @@ using Penumbra.GameData.Enums; namespace Penumbra.Interop.Structs; +[StructLayout( LayoutKind.Explicit )] +public unsafe struct TextureResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0x38 )] + public IntPtr Unk; + + [FieldOffset( 0x118 )] + public IntPtr KernelTexture; + + [FieldOffset( 0x20 )] + public IntPtr NewKernelTexture; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { From 1c97b5217988bf21cb65c2792fba6f46df4143dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Sep 2022 15:42:09 +0200 Subject: [PATCH 0490/2451] Automatically incorporate all .meta and .rgsp files when adding mods via API. --- .../Interop/Resolver/PathResolver.Meta.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 6 ++--- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 2 +- Penumbra/Mods/Mod.BasePath.cs | 22 ++++++++++++++++--- Penumbra/Mods/Mod.Creation.cs | 2 +- 7 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index ba0c453b..1121cb29 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -202,7 +202,7 @@ public unsafe partial class PathResolver var resolveData = GetResolveData( human ); using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; using var decals = resolveData.Valid - ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal(0, data) ) + ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ) : null; return _changeCustomize.Original( human, data, skipEquipment ); } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 0f9ef463..b0a136af 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -273,7 +273,7 @@ public partial class Mod try { var mod = new Mod( modDirectory ); - mod.Reload( out _ ); + mod.Reload( true, out _ ); var editor = new Editor( mod, mod.Default ); editor.DuplicatesFinished = false; editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() ); diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 480261ee..9f1d8fa5 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -267,7 +267,7 @@ public partial class Mod if( deletions > 0 ) { - _mod.Reload( out _ ); + _mod.Reload( false, out _ ); UpdateFiles(); } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 0d908866..a169cecc 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -60,7 +60,7 @@ public partial class Mod dir.Refresh(); mod.ModPath = dir; - if( !mod.Reload( out var metaChange ) ) + if( !mod.Reload( false, out var metaChange ) ) { Penumbra.Log.Error( $"Error reloading moved mod {mod.Name}." ); return; @@ -81,7 +81,7 @@ public partial class Mod var oldName = mod.Name; ModPathChanged.Invoke( ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); - if( !mod.Reload( out var metaChange ) ) + if( !mod.Reload( true, out var metaChange ) ) { Penumbra.Log.Warning( mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." @@ -135,7 +135,7 @@ public partial class Mod return; } - var mod = LoadMod( modFolder ); + var mod = LoadMod( modFolder, true ); if( mod == null ) { return; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 2bf658c2..7ad2140f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -84,7 +84,7 @@ public sealed partial class Mod { foreach( var modFolder in BasePath.EnumerateDirectories() ) { - var mod = LoadMod( modFolder ); + var mod = LoadMod( modFolder, false ); if( mod == null ) { continue; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 2456dec2..22760adf 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; namespace Penumbra.Mods; @@ -26,7 +27,7 @@ public partial class Mod _default = new SubMod( this ); } - private static Mod? LoadMod( DirectoryInfo modPath ) + private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges ) { modPath.Refresh(); if( !modPath.Exists ) @@ -36,7 +37,7 @@ public partial class Mod } var mod = new Mod( modPath ); - if( !mod.Reload( out _ ) ) + if( !mod.Reload( incorporateMetaChanges, out _ ) ) { // Can not be base path not existing because that is checked before. Penumbra.Log.Error( $"Mod at {modPath} without name is not supported." ); @@ -46,7 +47,7 @@ public partial class Mod return mod; } - private bool Reload( out MetaChangeType metaChange ) + private bool Reload( bool incorporateMetaChanges, out MetaChangeType metaChange ) { metaChange = MetaChangeType.Deletion; ModPath.Refresh(); @@ -63,8 +64,23 @@ public partial class Mod LoadDefaultOption(); LoadAllGroups(); + if( incorporateMetaChanges ) + { + IncorporateAllMetaChanges( true ); + } + ComputeChangedItems(); SetCounts(); return true; } + + // Convert all .meta and .rgsp files to their respective meta changes and add them to their options. + // Deletes the source files if delete is true. + private void IncorporateAllMetaChanges( bool delete ) + { + foreach( var subMod in AllSubMods.OfType< SubMod >() ) + { + subMod.IncorporateMetaChanges( ModPath, delete ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index d9c64cc1..35095002 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -122,7 +122,7 @@ public partial class Mod internal static void CreateDefaultFiles( DirectoryInfo directory ) { var mod = new Mod( directory ); - mod.Reload( out _ ); + mod.Reload( false, out _ ); foreach( var file in mod.FindUnusedFiles() ) { if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) From fe8f2e2fc5234397365c19b09fe1faadfb31085f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Sep 2022 14:22:59 +0200 Subject: [PATCH 0491/2451] Blep --- Penumbra/Import/Textures/TexFileParser.cs | 140 ++++++++++++++++++ Penumbra/Import/Textures/Texture.cs | 164 +++++++++++----------- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 4 +- 3 files changed, 226 insertions(+), 82 deletions(-) create mode 100644 Penumbra/Import/Textures/TexFileParser.cs diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs new file mode 100644 index 00000000..e3d06198 --- /dev/null +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using Lumina.Data.Files; +using Lumina.Extensions; +using OtterTex; + +namespace Penumbra.Import.Textures; + +public static class TexFileParser +{ + public static ScratchImage Parse( Stream data ) + { + using var r = new BinaryReader( data ); + var header = r.ReadStructure< TexFile.TexHeader >(); + + var meta = header.ToTexMeta(); + if( meta.Format == DXGIFormat.Unknown ) + { + throw new Exception( $"Could not convert format {header.Format} to DXGI Format." ); + } + + if( meta.Dimension == TexDimension.Unknown ) + { + throw new Exception( $"Could not obtain dimensionality from {header.Type}." ); + } + + var scratch = ScratchImage.Initialize( meta ); + CopyData( scratch, r ); + + return scratch; + } + + private static unsafe void CopyData( ScratchImage image, BinaryReader r ) + { + fixed( byte* ptr = image.Pixels ) + { + var span = new Span< byte >( ptr, image.Pixels.Length ); + var readBytes = r.Read( span ); + if( readBytes < image.Pixels.Length ) + { + throw new Exception( $"Invalid data length {readBytes} < {image.Pixels.Length}." ); + } + } + } + + public static TexFile.TexHeader ToTexHeader( this TexMeta meta ) + { + var ret = new TexFile.TexHeader() + { + Height = ( ushort )meta.Height, + Width = ( ushort )meta.Width, + Depth = ( ushort )Math.Max( meta.Depth, 1 ), + MipLevels = ( ushort )Math.Min(meta.MipLevels, 13), + Format = meta.Format.ToTexFormat(), + Type = meta.Dimension switch + { + _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, + TexDimension.Tex1D => TexFile.Attribute.TextureType1D, + TexDimension.Tex2D => TexFile.Attribute.TextureType2D, + TexDimension.Tex3D => TexFile.Attribute.TextureType3D, + _ => 0, + }, + }; + return ret; + } + + public static TexMeta ToTexMeta( this TexFile.TexHeader header ) + => new() + { + Height = header.Height, + Width = header.Width, + Depth = Math.Max( header.Depth, ( ushort )1 ), + MipLevels = header.MipLevels, + ArraySize = 1, + Format = header.Format.ToDXGI(), + Dimension = header.Type.ToDimension(), + MiscFlags = header.Type.HasFlag( TexFile.Attribute.TextureTypeCube ) ? D3DResourceMiscFlags.TextureCube : 0, + MiscFlags2 = 0, + }; + + private static TexDimension ToDimension( this TexFile.Attribute attribute ) + => ( attribute & TexFile.Attribute.TextureTypeMask ) switch + { + TexFile.Attribute.TextureType1D => TexDimension.Tex1D, + TexFile.Attribute.TextureType2D => TexDimension.Tex2D, + TexFile.Attribute.TextureType3D => TexDimension.Tex3D, + _ => TexDimension.Unknown, + }; + + public static TexFile.TextureFormat ToTexFormat( this DXGIFormat format ) + => format switch + { + DXGIFormat.R8UNorm => TexFile.TextureFormat.L8, + DXGIFormat.A8UNorm => TexFile.TextureFormat.A8, + DXGIFormat.B4G4R4A4UNorm => TexFile.TextureFormat.B4G4R4A4, + DXGIFormat.B5G5R5A1UNorm => TexFile.TextureFormat.B5G5R5A1, + DXGIFormat.B8G8R8A8UNorm => TexFile.TextureFormat.B8G8R8A8, + DXGIFormat.B8G8R8X8UNorm => TexFile.TextureFormat.B8G8R8X8, + DXGIFormat.R32Float => TexFile.TextureFormat.R32F, + DXGIFormat.R16G16Float => TexFile.TextureFormat.R16G16F, + DXGIFormat.R32G32Float => TexFile.TextureFormat.R32G32F, + DXGIFormat.R16G16B16A16Float => TexFile.TextureFormat.R16G16B16A16F, + DXGIFormat.R32G32B32A32Float => TexFile.TextureFormat.R32G32B32A32F, + DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, + DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, + DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, + DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, + DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, + DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, + DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, + DXGIFormat.R16Typeless => TexFile.TextureFormat.Shadow16, + _ => TexFile.TextureFormat.Unknown, + }; + + public static DXGIFormat ToDXGI( this TexFile.TextureFormat format ) + => format switch + { + TexFile.TextureFormat.L8 => DXGIFormat.R8UNorm, + TexFile.TextureFormat.A8 => DXGIFormat.A8UNorm, + TexFile.TextureFormat.B4G4R4A4 => DXGIFormat.B4G4R4A4UNorm, + TexFile.TextureFormat.B5G5R5A1 => DXGIFormat.B5G5R5A1UNorm, + TexFile.TextureFormat.B8G8R8A8 => DXGIFormat.B8G8R8A8UNorm, + TexFile.TextureFormat.B8G8R8X8 => DXGIFormat.B8G8R8X8UNorm, + TexFile.TextureFormat.R32F => DXGIFormat.R32Float, + TexFile.TextureFormat.R16G16F => DXGIFormat.R16G16Float, + TexFile.TextureFormat.R32G32F => DXGIFormat.R32G32Float, + TexFile.TextureFormat.R16G16B16A16F => DXGIFormat.R16G16B16A16Float, + TexFile.TextureFormat.R32G32B32A32F => DXGIFormat.R32G32B32A32Float, + TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, + TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, + TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, + TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, + TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, + TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, + TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, + TexFile.TextureFormat.Shadow16 => DXGIFormat.R16Typeless, + TexFile.TextureFormat.Shadow24 => DXGIFormat.R24G8Typeless, + _ => DXGIFormat.Unknown, + }; +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c553ed9f..5be06adb 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -4,7 +4,6 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Utility; using ImGuiNET; using ImGuiScene; using Lumina.Data.Files; @@ -18,6 +17,66 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; +//public static class ScratchImageExtensions +//{ +// public static Exception? SaveAsTex( this ScratchImage image, string path ) +// { +// try +// { +// using var fileStream = File.OpenWrite( path ); +// using var bw = new BinaryWriter( fileStream ); +// +// bw.Write( ( uint )image.Meta.GetAttribute() ); +// bw.Write( ( uint )image.Meta.GetFormat() ); +// bw.Write( ( ushort )image.Meta.Width ); +// bw.Write( ( ushort )image.Meta.Height ); +// bw.Write( ( ushort )image.Meta.Depth ); +// bw.Write( ( ushort )image.Meta.MipLevels ); +// } +// catch( Exception e ) +// { +// return e; +// } +// +// return null; +// } +// +// public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) +// { +// var ret = new TexFile.TexHeader() +// { +// Type = image.Meta.GetAttribute(), +// Format = image.Meta.GetFormat(), +// Width = ( ushort )image.Meta.Width, +// Height = ( ushort )image.Meta.Height, +// Depth = ( ushort )image.Meta.Depth, +// }; +// ret.LodOffset[0] = 0; +// ret.LodOffset[1] = 1; +// ret.LodOffset[2] = 2; +// //foreach(var surface in image.Images) +// // ret.OffsetToSurface[ 0 ] = 80 + (image.P); +// return ret; +// } +// +// // Get all known flags for the TexFile.Attribute from the scratch image. +// private static TexFile.Attribute GetAttribute( this TexMeta meta ) +// { +// var ret = meta.Dimension switch +// { +// TexDimension.Tex1D => TexFile.Attribute.TextureType1D, +// TexDimension.Tex2D => TexFile.Attribute.TextureType2D, +// TexDimension.Tex3D => TexFile.Attribute.TextureType3D, +// _ => ( TexFile.Attribute )0, +// }; +// if( meta.IsCubeMap ) +// ret |= TexFile.Attribute.TextureTypeCube; +// if( meta.Format.IsDepthStencil() ) +// ret |= TexFile.Attribute.TextureDepthStencil; +// return ret; +// } +//} + public sealed class Texture : IDisposable { public enum FileType @@ -101,7 +160,7 @@ public sealed class Texture : IDisposable ImGuiUtil.DrawTableColumn( "Format" ); ImGuiUtil.DrawTableColumn( t.Header.Format.ToString() ); ImGuiUtil.DrawTableColumn( "Mip Levels" ); - ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString()) ; + ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString() ); ImGuiUtil.DrawTableColumn( "Data Size" ); ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( t.ImageData.Length )} ({t.ImageData.Length} Bytes)" ); break; @@ -178,25 +237,36 @@ public sealed class Texture : IDisposable private bool LoadTex() { Type = FileType.Tex; - var tex = System.IO.Path.IsPathRooted( Path ) - ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) - : Dalamud.GameData.GetFile< TexFile >( Path ); - BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); - RGBAPixels = tex.GetRgbaImageData(); - CreateTextureWrap( tex.Header.Width, tex.Header.Height ); + using var stream = OpenTexStream(); + var scratch = TexFileParser.Parse( stream ); + BaseImage = scratch; + var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); + RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); + CreateTextureWrap( scratch.Meta.Width, scratch.Meta.Height ); return true; } + private Stream OpenTexStream() + { + if( System.IO.Path.IsPathRooted( Path ) ) + { + return File.OpenRead( Path ); + } + + var file = Dalamud.GameData.GetFile( Path ); + return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); + } + private void CreateTextureWrap( int width, int height ) => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); private string? _tmpPath; - public void PathSelectBox( string label, string tooltip, IEnumerable paths ) + public void PathSelectBox( string label, string tooltip, IEnumerable< string > paths ) { ImGui.SetNextItemWidth( -0.0001f ); - var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; - using var combo = ImRaii.Combo( label, startPath ); + var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; + using var combo = ImRaii.Combo( label, startPath ); if( combo ) { foreach( var (path, idx) in paths.WithIndex() ) @@ -208,13 +278,15 @@ public sealed class Texture : IDisposable } } } + ImGuiUtil.HoverTooltip( tooltip ); } public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) { _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) @@ -245,72 +317,4 @@ public sealed class Texture : IDisposable manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); } } -} - -public static class ScratchImageExtensions -{ - public static Exception? SaveAsTex( this ScratchImage image, string path ) - { - try - { - using var fileStream = File.OpenWrite( path ); - using var bw = new BinaryWriter( fileStream ); - - bw.Write( (uint) image.Meta.GetAttribute() ); - bw.Write( (uint) image.Meta.GetFormat() ); - bw.Write( (ushort) image.Meta.Width ); - bw.Write( (ushort) image.Meta.Height ); - bw.Write( (ushort) image.Meta.Depth ); - bw.Write( (ushort) image.Meta.MipLevels ); - } - catch( Exception e ) - { - return e; - } - - return null; - } - - public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) - { - var ret = new TexFile.TexHeader() - { - Type = image.Meta.GetAttribute(), - Format = image.Meta.GetFormat(), - Width = ( ushort )image.Meta.Width, - Height = ( ushort )image.Meta.Height, - Depth = ( ushort )image.Meta.Depth, - }; - ret.LodOffset[ 0 ] = 0; - ret.LodOffset[ 1 ] = 1; - ret.LodOffset[ 2 ] = 2; - //foreach(var surface in image.Images) - // ret.OffsetToSurface[ 0 ] = 80 + (image.P); - return ret; - } - - // Get all known flags for the TexFile.Attribute from the scratch image. - private static TexFile.Attribute GetAttribute( this TexMeta meta ) - { - var ret = meta.Dimension switch - { - TexDimension.Tex1D => TexFile.Attribute.TextureType1D, - TexDimension.Tex2D => TexFile.Attribute.TextureType2D, - TexDimension.Tex3D => TexFile.Attribute.TextureType3D, - _ => (TexFile.Attribute) 0, - }; - if( meta.IsCubeMap ) - ret |= TexFile.Attribute.TextureTypeCube; - if( meta.Format.IsDepthStencil() ) - ret |= TexFile.Attribute.TextureDepthStencil; - return ret; - } - - private static TexFile.TextureFormat GetFormat( this TexMeta meta ) - { - return meta.Format switch - { - _ => TexFile.TextureFormat.Unknown, - }; - } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 28cb2906..00f246a4 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -75,7 +75,7 @@ public partial class Mod private List< FileRegistry > _availableFiles = null!; private List< FileRegistry > _mtrlFiles = null!; private List< FileRegistry > _mdlFiles = null!; - private List _texFiles = null!; + private List< FileRegistry > _texFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -90,7 +90,7 @@ public partial class Mod public IReadOnlyList< FileRegistry > MdlFiles => _mdlFiles; - public IReadOnlyList TexFiles + public IReadOnlyList< FileRegistry > TexFiles => _texFiles; // Remove all path redirections where the pointed-to file does not exist. From 819264045b5c3fd409713248892b1b965385e7fe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Sep 2022 14:25:19 +0200 Subject: [PATCH 0492/2451] Add signal that a game path can not be added to file redirections, maybe fix some UsedGamePath bugs. --- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 9 +-------- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 9 +++++++-- Penumbra/UI/Classes/ModEditWindow.Files.cs | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 3b27f573..29f5389b 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -37,14 +37,7 @@ public partial class Mod } Penumbra.ModManager.OptionSetFiles( _mod, _subMod.GroupIdx, _subMod.OptionIdx, dict ); - if( num > 0 ) - { - RevertFiles(); - } - else - { - FileChanges = false; - } + UpdateFiles(); return num; } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 9f1d8fa5..f9505f9d 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -94,7 +94,7 @@ public partial class Mod { void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx ) { - var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key ) ) + var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key, mod == _subMod ) ) .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); if( newDict.Count != mod.Files.Count ) { @@ -106,13 +106,18 @@ public partial class Mod _missingFiles.Clear(); } - private bool CheckAgainstMissing( FullPath file, Utf8GamePath key ) + private bool CheckAgainstMissing( FullPath file, Utf8GamePath key, bool removeUsed ) { if( !_missingFiles.Contains( file ) ) { return true; } + if( removeUsed ) + { + _usedPaths.Remove( key ); + } + Penumbra.Log.Debug( $"[RemoveMissingPaths] Removing {key} -> {file} from {_mod.Name}." ); return false; } diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 74a5b6e9..dff8eaa6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -8,6 +8,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Util; using Penumbra.Mods; namespace Penumbra.UI.Classes; @@ -230,7 +231,7 @@ public partial class ModEditWindow using var id = ImRaii.PushId( j ); ImGui.TableNextColumn(); var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); - + var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); ImGui.SetNextItemWidth( -1 ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength ) ) { @@ -251,11 +252,20 @@ public partial class ModEditWindow _fileIdx = -1; _pathIdx = -1; } + else if( _fileIdx == i && _pathIdx == j && ( !Utf8GamePath.FromString( _gamePathEdit, out var path, false ) + || !path.IsEmpty && !path.Equals( gamePath ) && !_editor!.CanAddGamePath( path )) ) + { + ImGui.SameLine(); + ImGui.SetCursorPosX( pos ); + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + } } private void PrintNewGamePath( int i, Mod.Editor.FileRegistry registry, ISubMod subMod ) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; + var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); ImGui.SetNextItemWidth( -1 ); if( ImGui.InputTextWithHint( "##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength ) ) { @@ -274,6 +284,14 @@ public partial class ModEditWindow _fileIdx = -1; _pathIdx = -1; } + else if( _fileIdx == i && _pathIdx == -1 && (!Utf8GamePath.FromString( _gamePathEdit, out var path, false ) + || !path.IsEmpty && !_editor!.CanAddGamePath( path )) ) + { + ImGui.SameLine(); + ImGui.SetCursorPosX( pos ); + using var font = ImRaii.PushFont( UiBuilder.IconFont ); + ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + } } private void DrawButtonHeader() From 8d48fcff423a031b667cb600c2c0ade4b2d2e6e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Sep 2022 15:31:18 +0200 Subject: [PATCH 0493/2451] Add option for redraw bar. --- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/ConfigWindow.Changelog.cs | 4 ++++ Penumbra/UI/ConfigWindow.ModsTab.cs | 9 +++++++-- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index bd4231b9..0da4ca5e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -39,6 +39,8 @@ public partial class Configuration : IPluginConfiguration public bool PreferNamedCollectionsOverOwners { get; set; } = true; public bool UseDefaultCollectionForRetainers { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + #if DEBUG public bool DebugMode { get; set; } = true; #else diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 013e7357..5805cb30 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -33,11 +33,15 @@ public partial class ConfigWindow "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) + .RegisterEntry( "Files that the game loads super early should now be replaceable correctly via base or interface collection." ) + .RegisterEntry( "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) .RegisterEntry( "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) .RegisterEntry( "Collection Selectors now display None at the top if available." ) + .RegisterEntry( "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically." ) .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) .RegisterEntry( "Fixed issues with and improved mod option editing." ) + .RegisterEntry( "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use)." ) .RegisterEntry( "Backend optimizations." ) .RegisterEntry( "Changed metadata change system again.", 1 ) .RegisterEntry( "Improved logging efficiency.", 1 ); diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 2a0e193e..35b01d19 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -37,8 +37,8 @@ public partial class ConfigWindow using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, -ImGui.GetFrameHeight() ), true, - ImGuiWindowFlags.HorizontalScrollbar ) ) + using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight() ), + true, ImGuiWindowFlags.HorizontalScrollbar ) ) { style.Pop(); if( child ) @@ -68,6 +68,11 @@ public partial class ConfigWindow private void DrawRedrawLine() { + if( Penumbra.Config.HideRedrawBar ) + { + return; + } + var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); using( var _ = ImRaii.Group() ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index fe9983a6..1e0e0c50 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -59,6 +59,9 @@ public partial class ConfigWindow Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v; } ); ImGui.Dummy( _window._defaultSpace ); + Checkbox( "Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", + Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); + ImGui.Dummy( _window._defaultSpace ); Checkbox( $"Use {AssignedCollections} in Character Window", "Use the character collection for your characters name or the Your Character collection in your main character window, if it is set.", Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); From 3ad811a1d01132e116787655350f1116192ac22a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Sep 2022 18:52:16 +0200 Subject: [PATCH 0494/2451] Add default settings to mods. --- OtterGui | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 16 ++++++-- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 12 ++++++ Penumbra/Mods/Manager/ModOptionChangeType.cs | 32 ++++++++------- Penumbra/Mods/Mod.Creation.cs | 16 ++++---- Penumbra/Mods/Subclasses/IModGroup.cs | 8 +++- .../Subclasses/Mod.Files.MultiModGroup.cs | 19 ++++++--- .../Subclasses/Mod.Files.SingleModGroup.cs | 35 +++++++++++++--- Penumbra/Mods/Subclasses/ModSettings.cs | 31 +++----------- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 40 ++++++++++++++++--- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 4 +- 11 files changed, 141 insertions(+), 74 deletions(-) diff --git a/OtterGui b/OtterGui index 98064e79..c84ff1b1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 98064e790042c90c82a58fbfa79201bd69800758 +Subproject commit c84ff1b1c313c5f4ae28645a2e3fcb4d214f240a diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 5227b454..0edef44e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; +using OtterGui; using Penumbra.Mods; using Penumbra.Util; using SharpCompress.Archives.Zip; @@ -166,16 +167,17 @@ public partial class TexToolsImporter : ( 1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions ); _currentGroupName = GetGroupName( group.GroupName, groupNames ); - var optionIdx = 0; + var optionIdx = 0; for( var groupId = 0; groupId < numGroups; ++groupId ) { - var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; + var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); var description = new StringBuilder(); var groupFolder = Mod.NewSubFolderName( _currentModDirectory, name ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); + uint? defaultSettings = group.SelectionType == SelectType.Multi ? 0u : null; for( var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i ) { var option = allOptions[ i + optionIdx ]; @@ -191,6 +193,13 @@ public partial class TexToolsImporter description.Append( '\n' ); } + if( option.IsChecked ) + { + defaultSettings = group.SelectionType == SelectType.Multi + ? ( defaultSettings!.Value | ( 1u << i ) ) + : ( uint )i; + } + ++_currentOptionIdx; } @@ -205,11 +214,12 @@ public partial class TexToolsImporter { _currentOptionName = empty.Name; options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) ); + defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; } } Mod.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - description.ToString(), options ); + defaultSettings ?? 0, description.ToString(), options ); ++groupPriority; } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index b2ecdc54..3e248c0e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -28,6 +28,18 @@ public sealed partial class Mod ModOptionChanged.Invoke( ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1 ); } + public void ChangeModGroupDefaultOption( Mod mod, int groupIdx, uint defaultOption ) + { + var group = mod._groups[groupIdx]; + if( group.DefaultSettings == defaultOption ) + { + return; + } + + group.DefaultSettings = defaultOption; + ModOptionChanged.Invoke( ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1 ); + } + public void RenameModGroup( Mod mod, int groupIdx, string newName ) { var group = mod._groups[ groupIdx ]; diff --git a/Penumbra/Mods/Manager/ModOptionChangeType.cs b/Penumbra/Mods/Manager/ModOptionChangeType.cs index 080c2983..b4c4947e 100644 --- a/Penumbra/Mods/Manager/ModOptionChangeType.cs +++ b/Penumbra/Mods/Manager/ModOptionChangeType.cs @@ -17,6 +17,7 @@ public enum ModOptionChangeType OptionMetaChanged, DisplayChange, PrepareChange, + DefaultOptionChanged, } public static class ModOptionChangeTypeExtension @@ -30,21 +31,22 @@ public static class ModOptionChangeTypeExtension { ( requiresSaving, requiresReloading, wasPrepared ) = type switch { - ModOptionChangeType.GroupRenamed => ( true, false, false ), - ModOptionChangeType.GroupAdded => ( true, false, false ), - ModOptionChangeType.GroupDeleted => ( true, true, false ), - ModOptionChangeType.GroupMoved => ( true, false, false ), - ModOptionChangeType.GroupTypeChanged => ( true, true, true ), - ModOptionChangeType.PriorityChanged => ( true, true, true ), - ModOptionChangeType.OptionAdded => ( true, true, true ), - ModOptionChangeType.OptionDeleted => ( true, true, false ), - ModOptionChangeType.OptionMoved => ( true, false, false ), - ModOptionChangeType.OptionFilesChanged => ( false, true, false ), - ModOptionChangeType.OptionFilesAdded => ( false, true, true ), - ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), - ModOptionChangeType.OptionMetaChanged => ( false, true, false ), - ModOptionChangeType.DisplayChange => ( false, false, false ), - _ => ( false, false, false ), + ModOptionChangeType.GroupRenamed => ( true, false, false ), + ModOptionChangeType.GroupAdded => ( true, false, false ), + ModOptionChangeType.GroupDeleted => ( true, true, false ), + ModOptionChangeType.GroupMoved => ( true, false, false ), + ModOptionChangeType.GroupTypeChanged => ( true, true, true ), + ModOptionChangeType.PriorityChanged => ( true, true, true ), + ModOptionChangeType.OptionAdded => ( true, true, true ), + ModOptionChangeType.OptionDeleted => ( true, true, false ), + ModOptionChangeType.OptionMoved => ( true, false, false ), + ModOptionChangeType.OptionFilesChanged => ( false, true, false ), + ModOptionChangeType.OptionFilesAdded => ( false, true, true ), + ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), + ModOptionChangeType.OptionMetaChanged => ( false, true, false ), + ModOptionChangeType.DisplayChange => ( false, false, false ), + ModOptionChangeType.DefaultOptionChanged => ( true, false, false ), + _ => ( false, false, false ), }; } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 35095002..6501be57 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -59,7 +59,7 @@ public partial class Mod // Create a file for an option group from given data. internal static void CreateOptionGroup( DirectoryInfo baseFolder, SelectType type, string name, - int priority, int index, string desc, IEnumerable< ISubMod > subMods ) + int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) { switch( type ) { @@ -67,9 +67,10 @@ public partial class Mod { var group = new MultiModGroup() { - Name = name, - Description = desc, - Priority = priority, + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, }; group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); IModGroup.Save( group, baseFolder, index ); @@ -79,9 +80,10 @@ public partial class Mod { var group = new SingleModGroup() { - Name = name, - Description = desc, - Priority = priority, + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, }; group.OptionData.AddRange( subMods.OfType< SubMod >() ); IModGroup.Save( group, baseFolder, index ); diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 9507b0f3..fb2d305a 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -20,6 +20,7 @@ public interface IModGroup : IEnumerable< ISubMod > public string Description { get; } public SelectType Type { get; } public int Priority { get; } + public uint DefaultSettings { get; set; } public int OptionPriority( Index optionIdx ); @@ -60,7 +61,8 @@ public interface IModGroup : IEnumerable< ISubMod > public static void SaveDelayed( IModGroup group, DirectoryInfo basePath, int groupIdx ) { - Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", () => SaveModGroup( group, basePath, groupIdx ) ); + Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", + () => SaveModGroup( group, basePath, groupIdx ) ); } public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx ) @@ -82,6 +84,8 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteValue( group.Priority ); j.WritePropertyName( nameof( Type ) ); j.WriteValue( group.Type.ToString() ); + j.WritePropertyName( nameof( group.DefaultSettings ) ); + j.WriteValue( group.DefaultSettings ); j.WritePropertyName( "Options" ); j.WriteStartArray(); for( var idx = 0; idx < group.Count; ++idx ) @@ -96,5 +100,5 @@ public interface IModGroup : IEnumerable< ISubMod > public IModGroup Convert( SelectType type ); public bool MoveOption( int optionIdxFrom, int optionIdxTo ); - public void UpdatePositions(int from = 0); + public void UpdatePositions( int from = 0 ); } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 3413cc6f..91bc304b 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Numerics; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; @@ -20,6 +21,7 @@ public partial class Mod public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; public int Priority { get; set; } + public uint DefaultSettings { get; set; } public int OptionPriority( Index idx ) => PrioritizedOptions[ idx ].Priority; @@ -44,9 +46,10 @@ public partial class Mod var options = json[ "Options" ]; var ret = new MultiModGroup() { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0, }; if( ret.Name.Length == 0 ) { @@ -71,6 +74,8 @@ public partial class Mod } } + ret.DefaultSettings = (uint) (ret.DefaultSettings & ( ( 1ul << ret.Count ) - 1 )); + return ret; } @@ -82,9 +87,10 @@ public partial class Mod case SelectType.Single: var multi = new SingleModGroup() { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = ( uint )Math.Max( Math.Min( Count - 1, BitOperations.TrailingZeroCount( DefaultSettings) ), 0 ), }; multi.OptionData.AddRange( PrioritizedOptions.Select( p => p.Mod ) ); return multi; @@ -99,6 +105,7 @@ public partial class Mod return false; } + DefaultSettings = Functions.MoveBit( DefaultSettings, optionIdxFrom, optionIdxTo ); UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); return true; } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index e37b1988..3de02d5c 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -20,6 +20,7 @@ public partial class Mod public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public int Priority { get; set; } + public uint DefaultSettings { get; set; } public readonly List< SubMod > OptionData = new(); @@ -44,9 +45,10 @@ public partial class Mod var options = json[ "Options" ]; var ret = new SingleModGroup { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, }; if( ret.Name.Length == 0 ) { @@ -64,6 +66,9 @@ public partial class Mod } } + if( ( int )ret.DefaultSettings >= ret.Count ) + ret.DefaultSettings = 0; + return ret; } @@ -75,9 +80,10 @@ public partial class Mod case SelectType.Multi: var multi = new MultiModGroup() { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = 1u << ( int )DefaultSettings, }; multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); return multi; @@ -92,6 +98,23 @@ public partial class Mod return false; } + // Update default settings with the move. + if( DefaultSettings == optionIdxFrom ) + { + DefaultSettings = ( uint )optionIdxTo; + } + else if( optionIdxFrom < optionIdxTo ) + { + if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) + { + --DefaultSettings; + } + } + else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) + { + ++DefaultSettings; + } + UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); return true; } diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index b79f1c9e..6e8c7343 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -30,7 +30,7 @@ public class ModSettings { Enabled = false, Priority = 0, - Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(), + Settings = mod.Groups.Select( g => g.DefaultSettings ).ToList(), }; // Automatically react to changes in a mods available options. @@ -41,7 +41,7 @@ public class ModSettings case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert( groupIdx, 0 ); + Settings.Insert( groupIdx, mod.Groups[groupIdx].DefaultSettings ); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. @@ -70,8 +70,8 @@ public class ModSettings var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => config >= optionIdx ? (config > 1 ? config - 1 : 0) : config, - SelectType.Multi => RemoveBit( config, optionIdx ), + SelectType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, + SelectType.Multi => Functions.RemoveBit( config, optionIdx ), _ => config, }; return config != Settings[ groupIdx ]; @@ -88,7 +88,7 @@ public class ModSettings Settings[ groupIdx ] = group.Type switch { SelectType.Single => config == optionIdx ? ( uint )movedToIdx : config, - SelectType.Multi => MoveBit( config, optionIdx, movedToIdx ), + SelectType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), _ => config, }; return config != Settings[ groupIdx ]; @@ -114,27 +114,6 @@ public class ModSettings Settings[ groupIdx ] = FixSetting( group, newValue ); } - // Remove a single bit, moving all further bits one down. - private static uint RemoveBit( uint config, int bit ) - { - var lowMask = ( 1u << bit ) - 1u; - var highMask = ~( ( 1u << ( bit + 1 ) ) - 1u ); - var low = config & lowMask; - var high = ( config & highMask ) >> 1; - return low | high; - } - - // Move a bit in an uint from its position to another, shifting other bits accordingly. - private static uint MoveBit( uint config, int bit1, int bit2 ) - { - var enabled = ( config & ( 1 << bit1 ) ) != 0 ? 1u << bit2 : 0u; - config = RemoveBit( config, bit1 ); - var lowMask = ( 1u << bit2 ) - 1u; - var low = config & lowMask; - var high = ( config & ~lowMask ) << 1; - return low | enabled | high; - } - // Add defaulted settings up to the required count. private bool AddMissingSettings( int totalCount ) { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 4a3fec24..dcd7ef82 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -97,7 +97,7 @@ public partial class ConfigWindow ImGui.Dummy( _window._defaultSpace ); } - private void DrawUpdateBibo( Vector2 buttonSize) + private void DrawUpdateBibo( Vector2 buttonSize ) { if( ImGui.Button( "Update Bibo Material", buttonSize ) ) { @@ -108,7 +108,8 @@ public partial class ConfigWindow _window.ModEditPopup.UpdateModels(); } - ImGuiUtil.HoverTooltip( "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" + ImGuiUtil.HoverTooltip( + "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" + "Use this for outdated mods made for old Bibo bodies.\n" + "Go to Advanced Editing for more fine-tuned control over material assignment." ); @@ -459,15 +460,16 @@ public partial class ConfigWindow public static void Draw( ModPanel panel, int groupIdx ) { - using var table = ImRaii.Table( string.Empty, 4, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; } ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, - panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); + panel._window._inputTextWidth.X - 68 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); @@ -491,6 +493,31 @@ public partial class ConfigWindow Source( group, groupIdx, optionIdx ); Target( panel, group, groupIdx, optionIdx ); + ImGui.TableNextColumn(); + + + if( group.Type == SelectType.Single ) + { + if( ImGui.RadioButton( "##default", group.DefaultSettings == optionIdx ) ) + { + Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, ( uint )optionIdx ); + } + + ImGuiUtil.HoverTooltip( $"Set {option.Name} as the default choice for this group." ); + } + else + { + var isDefaultOption = ( ( group.DefaultSettings >> optionIdx ) & 1 ) != 0; + if( ImGui.Checkbox( "##default", ref isDefaultOption ) ) + { + Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, isDefaultOption + ? group.DefaultSettings | ( 1u << optionIdx ) + : group.DefaultSettings & ~( 1u << optionIdx ) ); + } + + ImGuiUtil.HoverTooltip( $"{( isDefaultOption ? "Disable" : "Enable" )} {option.Name} per default in this group." ); + } + ImGui.TableNextColumn(); if( Input.Text( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) { @@ -527,6 +554,7 @@ public partial class ConfigWindow ImGui.Selectable( $"Option #{group.Count + 1}" ); Target( panel, group, groupIdx, group.Count ); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); ImGui.SetNextItemWidth( -1 ); var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) ) @@ -575,7 +603,7 @@ public partial class ConfigWindow return; } - if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) + if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) { if( _dragDropGroupIdx == groupIdx ) { @@ -589,7 +617,7 @@ public partial class ConfigWindow var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[ sourceGroupIdx ]; var currentCount = group.Count; - var option = sourceGroup[sourceOption]; + var option = sourceGroup[ sourceOption ]; var priority = sourceGroup.OptionPriority( _dragDropGroupIdx ); panel._delayedActions.Enqueue( () => { diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index bb012be3..6c5582b8 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -160,7 +160,7 @@ public partial class ConfigWindow } using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ groupIdx ]; + var selectedOption = _emptySetting ? (int) group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ); if( combo ) @@ -199,7 +199,7 @@ public partial class ConfigWindow } using var id = ImRaii.PushId( groupIdx ); - var flags = _emptySetting ? 0u : _settings.Settings[ groupIdx ]; + var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; Widget.BeginFramedGroup( group.Name, group.Description ); for( var idx2 = 0; idx2 < group.Count; ++idx2 ) { From ac52515e0db7eb6b67885f32c43a7fcd3ebb3aaf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Sep 2022 23:12:18 +0200 Subject: [PATCH 0495/2451] Add Changelog --- Penumbra/UI/ConfigWindow.Changelog.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 5805cb30..214b284d 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -33,15 +33,22 @@ public partial class ConfigWindow "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) + .RegisterEntry( + "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured." ) + .RegisterEntry( "Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", + 1 ) .RegisterEntry( "Files that the game loads super early should now be replaceable correctly via base or interface collection." ) - .RegisterEntry( "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) + .RegisterEntry( + "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) .RegisterEntry( "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) - .RegisterEntry( "Collection Selectors now display None at the top if available." ) - .RegisterEntry( "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically." ) + .RegisterEntry( "Collection Selectors now display None at the top if available." ) + .RegisterEntry( + "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically." ) .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) .RegisterEntry( "Fixed issues with and improved mod option editing." ) - .RegisterEntry( "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use)." ) + .RegisterEntry( + "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use)." ) .RegisterEntry( "Backend optimizations." ) .RegisterEntry( "Changed metadata change system again.", 1 ) .RegisterEntry( "Improved logging efficiency.", 1 ); From 1f2d5246feb9d380a6d98ff72107fc1d3edfc982 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Sep 2022 15:21:30 +0200 Subject: [PATCH 0496/2451] Fix some stuff, add Saving. --- .../Textures/CombinedTexture.Manipulation.cs | 10 +- Penumbra/Import/Textures/CombinedTexture.cs | 178 ++++++++++++------ Penumbra/Import/Textures/TexFileParser.cs | 47 ++++- Penumbra/Import/Textures/Texture.cs | 68 +------ Penumbra/UI/Classes/ModEditWindow.Textures.cs | 84 ++++++++- 5 files changed, 248 insertions(+), 139 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 3e60601b..16bd6dfe 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -46,13 +46,9 @@ public partial class CombinedTexture var left = DataLeft( offset ); var right = DataRight( x, y ); 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 } ); + var rgba = alpha == 0 + ? new Rgba32() + : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); _centerStorage.RGBAPixels[ offset ] = rgba.R; _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 8688ce2f..61c3dbd5 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,5 +1,7 @@ using System; +using System.IO; using System.Numerics; +using Lumina.Data.Files; using OtterTex; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -10,6 +12,14 @@ namespace Penumbra.Import.Textures; public partial class CombinedTexture : IDisposable { + public enum TextureSaveType + { + AsIs, + Bitmap, + BC5, + BC7, + } + private enum Mode { Empty, @@ -29,6 +39,8 @@ public partial class CombinedTexture : IDisposable public bool IsLoaded => _mode != Mode.Empty; + public Exception? SaveException { get; private set; } = null; + public void Draw( Vector2 size ) { if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) @@ -44,81 +56,123 @@ public partial class CombinedTexture : IDisposable public void SaveAsPng( string path ) { - if( !IsLoaded || _current == null ) + if( !IsLoaded || _current == null ) { return; } - var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height ); - image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + try + { + var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height ); + image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + SaveException = null; + } + catch( Exception e ) + { + SaveException = e; + } } - public void SaveAsDDS( string path, DXGIFormat format, bool fast, float threshold = 0.5f ) + private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex ) { - if( _current == null ) - return; - switch( _mode ) + if( _current == null || _mode == Mode.Empty ) { - case Mode.Empty: return; - case Mode.LeftCopy: - case Mode.RightCopy: - if( _centerStorage.BaseImage is ScratchImage s ) - { - if( format != s.Meta.Format ) - { - s = s.Convert( format, threshold ); - } + return; + } - s.SaveDDS( path ); - } - else - { - var image = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); - image.SaveDDS( path ).ThrowIfError(); - } + try + { + if( _current.BaseImage is not ScratchImage s ) + { + s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); + } - break; + var tex = type switch + { + TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed(s, mipMaps ) : s, + TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), + TextureSaveType.BC5 => CreateCompressed( s, mipMaps, false ), + TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; + + if( !writeTex ) + { + tex.SaveDDS( path ); + } + else + { + SaveTex( path, tex ); + } + + SaveException = null; + } + catch( Exception e ) + { + SaveException = e; } } - //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: - // //ScratchImage.LoadDDS( _imageCenter, ) - // //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 static void SaveTex( string path, ScratchImage input ) + { + var header = input.Meta.ToTexHeader(); + if( header.Format == TexFile.TextureFormat.Unknown ) + { + throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." ); + } + + using var stream = File.OpenWrite( path ); + using var w = new BinaryWriter( stream ); + header.Write( w ); + w.Write( input.Pixels ); + } + + private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) + => mipMaps ? input.GenerateMipMaps() : input; + + private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) + { + if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm) + return AddMipMaps(input, mipMaps); + + if( input.Meta.Format.IsCompressed() ) + { + input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); + } + else + { + input = input.Convert( DXGIFormat.B8G8R8A8UNorm ); + } + + return AddMipMaps( input, mipMaps ); + } + + private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 ) + { + var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC5UNorm; + if( input.Meta.Format == format) + { + return input; + } + + if( input.Meta.Format.IsCompressed() ) + { + input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); + } + + input = AddMipMaps( input, mipMaps ); + + return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel ); + } + + public void SaveAsTex( string path, TextureSaveType type, bool mipMaps ) + => SaveAs( path, type, mipMaps, true ); + + public void SaveAsDds( string path, TextureSaveType type, bool mipMaps ) + => SaveAs( path, type, mipMaps, false ); + public CombinedTexture( Texture left, Texture right ) { diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index e3d06198..1d416f02 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -43,6 +43,26 @@ public static class TexFileParser } } + public static void Write( this TexFile.TexHeader header, BinaryWriter w ) + { + w.Write( ( uint )header.Type ); + w.Write( ( uint )header.Format ); + w.Write( header.Width ); + w.Write( header.Height ); + w.Write( header.Depth ); + w.Write( header.MipLevels ); + unsafe + { + w.Write( header.LodOffset[ 0 ] ); + w.Write( header.LodOffset[ 1 ] ); + w.Write( header.LodOffset[ 2 ] ); + for( var i = 0; i < 13; ++i ) + { + w.Write( header.OffsetToSurface[ i ] ); + } + } + } + public static TexFile.TexHeader ToTexHeader( this TexMeta meta ) { var ret = new TexFile.TexHeader() @@ -50,7 +70,7 @@ public static class TexFileParser Height = ( ushort )meta.Height, Width = ( ushort )meta.Width, Depth = ( ushort )Math.Max( meta.Depth, 1 ), - MipLevels = ( ushort )Math.Min(meta.MipLevels, 13), + MipLevels = ( ushort )Math.Min( meta.MipLevels, 12 ), Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { @@ -61,6 +81,31 @@ public static class TexFileParser _ => 0, }, }; + unsafe + { + ret.LodOffset[ 0 ] = 0; + ret.LodOffset[ 1 ] = 1; + ret.LodOffset[ 2 ] = 2; + + ret.OffsetToSurface[ 0 ] = 80; + var size = meta.Format.BitsPerPixel() * meta.Width * meta.Height / 8; + for( var i = 1; i < meta.MipLevels; ++i ) + { + ret.OffsetToSurface[ i ] = ( uint )( 80 + size ); + size >>= 2; + if( size == 0 ) + { + ret.MipLevels = ( ushort )i; + break; + } + } + + for( var i = ret.MipLevels; i < 13; ++i ) + { + ret.OffsetToSurface[ i ] = 0; + } + } + return ret; } diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 5be06adb..c55f4df1 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -17,66 +17,6 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -//public static class ScratchImageExtensions -//{ -// public static Exception? SaveAsTex( this ScratchImage image, string path ) -// { -// try -// { -// using var fileStream = File.OpenWrite( path ); -// using var bw = new BinaryWriter( fileStream ); -// -// bw.Write( ( uint )image.Meta.GetAttribute() ); -// bw.Write( ( uint )image.Meta.GetFormat() ); -// bw.Write( ( ushort )image.Meta.Width ); -// bw.Write( ( ushort )image.Meta.Height ); -// bw.Write( ( ushort )image.Meta.Depth ); -// bw.Write( ( ushort )image.Meta.MipLevels ); -// } -// catch( Exception e ) -// { -// return e; -// } -// -// return null; -// } -// -// public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) -// { -// var ret = new TexFile.TexHeader() -// { -// Type = image.Meta.GetAttribute(), -// Format = image.Meta.GetFormat(), -// Width = ( ushort )image.Meta.Width, -// Height = ( ushort )image.Meta.Height, -// Depth = ( ushort )image.Meta.Depth, -// }; -// ret.LodOffset[0] = 0; -// ret.LodOffset[1] = 1; -// ret.LodOffset[2] = 2; -// //foreach(var surface in image.Images) -// // ret.OffsetToSurface[ 0 ] = 80 + (image.P); -// return ret; -// } -// -// // Get all known flags for the TexFile.Attribute from the scratch image. -// private static TexFile.Attribute GetAttribute( this TexMeta meta ) -// { -// var ret = meta.Dimension switch -// { -// TexDimension.Tex1D => TexFile.Attribute.TextureType1D, -// TexDimension.Tex2D => TexFile.Attribute.TextureType2D, -// TexDimension.Tex3D => TexFile.Attribute.TextureType3D, -// _ => ( TexFile.Attribute )0, -// }; -// if( meta.IsCubeMap ) -// ret |= TexFile.Attribute.TextureTypeCube; -// if( meta.Format.IsDepthStencil() ) -// ret |= TexFile.Attribute.TextureDepthStencil; -// return ret; -// } -//} - public sealed class Texture : IDisposable { public enum FileType @@ -112,9 +52,6 @@ public sealed class Texture : IDisposable public bool IsLoaded => TextureWrap != null; - public Texture() - { } - public void Draw( Vector2 size ) { if( TextureWrap != null ) @@ -195,12 +132,15 @@ public sealed class Texture : IDisposable Clean(); try { + if( !File.Exists( path ) ) + throw new FileNotFoundException(); + var _ = System.IO.Path.GetExtension( Path ) switch { ".dds" => LoadDds(), ".png" => LoadPng(), ".tex" => LoadTex(), - _ => true, + _ => throw new Exception($"Extension {System.IO.Path.GetExtension( Path )} unknown."), }; Loaded?.Invoke( true ); } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 49e541e5..43307773 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Numerics; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using OtterGui; @@ -19,7 +20,20 @@ public partial class ModEditWindow private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); private bool _overlayCollapsed = true; - private DXGIFormat _currentFormat = DXGIFormat.R8G8B8A8UNorm; + + private bool _addMipMaps = true; + private int _currentSaveAs = 0; + + private static readonly (string, string)[] SaveAsStrings = + { + ( "As Is", "Save the current texture with its own format without additional conversion or compression, if possible." ), + ( "RGBA (Uncompressed)", + "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality." ), + ( "BC3 (Simple Compression)", + "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality." ), + ( "BC7 (Complex Compression)", + "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while." ), + }; private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) { @@ -37,7 +51,8 @@ public partial class ModEditWindow _dialogManager ); var files = _editor!.TexFiles.Select( f => f.File.FullName ) .Concat( _editor.TexFiles.SelectMany( f => f.SubModUsage.Select( p => p.Item2.ToString() ) ) ); - tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files); + tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", + files ); if( tex == _left ) { @@ -52,6 +67,35 @@ public partial class ModEditWindow tex.Draw( imageSize ); } + private void SaveAsCombo() + { + var (text, desc) = SaveAsStrings[ _currentSaveAs ]; + ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + using var combo = ImRaii.Combo( "##format", text ); + ImGuiUtil.HoverTooltip( desc ); + if( !combo ) + { + return; + } + + foreach( var ((newText, newDesc), idx) in SaveAsStrings.WithIndex() ) + { + if( ImGui.Selectable( newText, idx == _currentSaveAs ) ) + { + _currentSaveAs = idx; + } + + ImGuiUtil.HoverTooltip( newDesc ); + } + } + + private void MipMapInput() + { + ImGui.Checkbox( "##mipMaps", ref _addMipMaps ); + ImGuiUtil.HoverTooltip( + "Add the appropriate number of MipMaps to the file." ); + } + private void DrawOutputChild( Vector2 size, Vector2 imageSize ) { using var child = ImRaii.Child( "Output", size, true ); @@ -62,27 +106,57 @@ public partial class ModEditWindow if( _center.IsLoaded ) { + SaveAsCombo(); + ImGui.SameLine(); + MipMapInput(); if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) { var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); - _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => { }, _mod!.ModPath.FullName ); + _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => + { + if( a ) + { + _center.SaveAsTex( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); + } + }, _mod!.ModPath.FullName ); } if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) { var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => { if( a ) _center.SaveAsDDS( b, _currentFormat, false ); }, _mod!.ModPath.FullName ); + _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => + { + if( a ) + { + _center.SaveAsDds( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); + } + }, _mod!.ModPath.FullName ); } + ImGui.NewLine(); + if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) { var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => { if (a) _center.SaveAsPng( b ); }, _mod!.ModPath.FullName ); + _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => + { + if( a ) + { + _center.SaveAsPng( b ); + } + }, _mod!.ModPath.FullName ); } ImGui.NewLine(); } + if( _center.SaveException != null ) + { + ImGui.TextUnformatted( "Could not save file:" ); + using var color = ImRaii.PushColor( ImGuiCol.Text, 0xFF0000FF ); + ImGuiUtil.TextWrapped( _center.SaveException.ToString() ); + } + _center.Draw( imageSize ); } From daaee4feb68e23c28b63e406db14354c7710a235 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Sep 2022 15:31:19 +0200 Subject: [PATCH 0497/2451] Work on Changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 214b284d..0af2bdd2 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -33,6 +33,8 @@ public partial class ConfigWindow "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) + .RegisterHighlight( + "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being." ) .RegisterEntry( "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured." ) .RegisterEntry( "Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", @@ -40,6 +42,13 @@ public partial class ConfigWindow .RegisterEntry( "Files that the game loads super early should now be replaceable correctly via base or interface collection." ) .RegisterEntry( "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) + .RegisterEntry( "Continued Work on the Texture Import/Export Tab:" ) + .RegisterEntry( "Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1 ) + .RegisterEntry( "Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1 ) + .RegisterEntry( "Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1 ) + .RegisterHighlight( + "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", + 1 ) .RegisterEntry( "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) .RegisterEntry( "Collection Selectors now display None at the top if available." ) From 5c81970558c1cf65be05f7c389fd8b06341b5a67 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Sep 2022 17:59:07 +0200 Subject: [PATCH 0498/2451] Handle weird mip map/size inconsistencies, maybe? --- Penumbra/Import/Textures/TexFileParser.cs | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 1d416f02..7c0afbb5 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -24,12 +24,37 @@ public static class TexFileParser throw new Exception( $"Could not obtain dimensionality from {header.Type}." ); } + meta.MipLevels = CountMipLevels( data, in header ); + var scratch = ScratchImage.Initialize( meta ); + CopyData( scratch, r ); return scratch; } + private static unsafe int CountMipLevels( Stream data, in TexFile.TexHeader header ) + { + var lastOffset = 80u; + var levels = 1; + for( var i = 1; i < 13; ++i ) + { + var offset = header.OffsetToSurface[ i ]; + var diff = offset - lastOffset; + if( offset > 0 && offset < data.Length && diff > 0 ) + { + lastOffset = offset; + ++levels; + } + else + { + return levels; + } + } + + return 13; + } + private static unsafe void CopyData( ScratchImage image, BinaryReader r ) { fixed( byte* ptr = image.Pixels ) From cadaafb887a5125260a2e52debcd603876cc7daa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Sep 2022 18:01:49 +0200 Subject: [PATCH 0499/2451] Fix stupid file existence check, improve texture file selector. --- Penumbra/Import/Textures/Texture.cs | 43 ++++++++++++++----- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 8 ++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c55f4df1..a3ca306c 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -130,17 +131,19 @@ public sealed class Texture : IDisposable Path = path; Clean(); + if( path.Length == 0 ) + { + return; + } + try { - if( !File.Exists( path ) ) - throw new FileNotFoundException(); - var _ = System.IO.Path.GetExtension( Path ) switch { ".dds" => LoadDds(), ".png" => LoadPng(), ".tex" => LoadTex(), - _ => throw new Exception($"Extension {System.IO.Path.GetExtension( Path )} unknown."), + _ => throw new Exception( $"Extension {System.IO.Path.GetExtension( Path )} unknown." ), }; Loaded?.Invoke( true ); } @@ -202,20 +205,40 @@ public sealed class Texture : IDisposable private string? _tmpPath; - public void PathSelectBox( string label, string tooltip, IEnumerable< string > paths ) + public void PathSelectBox( string label, string tooltip, IEnumerable< (string, bool) > paths, int skipPrefix ) { ImGui.SetNextItemWidth( -0.0001f ); - var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; + var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here..."; using var combo = ImRaii.Combo( label, startPath ); if( combo ) { - foreach( var (path, idx) in paths.WithIndex() ) + foreach( var ((path, game), idx) in paths.WithIndex() ) { - using var id = ImRaii.PushId( idx ); - if( ImGui.Selectable( path, path == startPath ) && path != startPath ) + if( game ) { - Load( path ); + if( !Dalamud.GameData.FileExists( path ) ) + { + continue; + } } + else if( !File.Exists( path ) ) + { + continue; + } + + using var id = ImRaii.PushId( idx ); + using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(), game ) ) + { + var p = game ? $"--> {path}" : path[ skipPrefix.. ]; + if( ImGui.Selectable( p, path == startPath ) && path != startPath ) + { + Load( path ); + } + } + + ImGuiUtil.HoverTooltip( game + ? "This is a game path and refers to an unmanipulated file from your game data." + : "This is a path to a modded file on your file system." ); } } diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 43307773..7d244dd6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -21,7 +21,7 @@ public partial class ModEditWindow private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); private bool _overlayCollapsed = true; - private bool _addMipMaps = true; + private bool _addMipMaps = true; private int _currentSaveAs = 0; private static readonly (string, string)[] SaveAsStrings = @@ -49,10 +49,10 @@ public partial class ModEditWindow tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _dialogManager ); - var files = _editor!.TexFiles.Select( f => f.File.FullName ) - .Concat( _editor.TexFiles.SelectMany( f => f.SubModUsage.Select( p => p.Item2.ToString() ) ) ); + var files = _editor!.TexFiles.SelectMany( f => f.SubModUsage.Select( p => (p.Item2.ToString(), true) ) + .Prepend( (f.File.FullName, false ))); tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", - files ); + files, _mod.ModPath.FullName.Length + 1 ); if( tex == _left ) { From 3158d3da8c4458426808a2a7072e25172ac30999 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 Sep 2022 20:48:59 +0200 Subject: [PATCH 0500/2451] Change mipmap handling again. --- Penumbra/Import/Textures/TexFileParser.cs | 37 +++++++++++++------ Penumbra/UI/Classes/ModEditWindow.Textures.cs | 12 +++++- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 7c0afbb5..f38d066e 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -24,7 +24,7 @@ public static class TexFileParser throw new Exception( $"Could not obtain dimensionality from {header.Type}." ); } - meta.MipLevels = CountMipLevels( data, in header ); + meta.MipLevels = CountMipLevels( data, in meta, in header ); var scratch = ScratchImage.Initialize( meta ); @@ -33,23 +33,38 @@ public static class TexFileParser return scratch; } - private static unsafe int CountMipLevels( Stream data, in TexFile.TexHeader header ) + private static unsafe int CountMipLevels( Stream data, in TexMeta meta, in TexFile.TexHeader header ) { - var lastOffset = 80u; - var levels = 1; - for( var i = 1; i < 13; ++i ) + var width = meta.Width; + var height = meta.Height; + var bits = meta.Format.BitsPerPixel(); + + var lastOffset = 0L; + var lastSize = 80L; + for( var i = 0; i < 13; ++i ) { var offset = header.OffsetToSurface[ i ]; - var diff = offset - lastOffset; - if( offset > 0 && offset < data.Length && diff > 0 ) + if( offset == 0 ) { - lastOffset = offset; - ++levels; + return i; } - else + + var requiredSize = width * height * bits / 8; + if( offset + requiredSize > data.Length ) { - return levels; + return i; } + + var diff = offset - lastOffset; + if( diff != lastSize ) + { + return i; + } + + width = Math.Max( width / 2, 4 ); + height = Math.Max( height / 2, 4 ); + lastOffset = offset; + lastSize = requiredSize; } return 13; diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 7d244dd6..03f984c0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -64,7 +64,11 @@ public partial class ModEditWindow } ImGui.NewLine(); - tex.Draw( imageSize ); + using var child2 = ImRaii.Child( "image" ); + if( child2 ) + { + tex.Draw( imageSize ); + } } private void SaveAsCombo() @@ -157,7 +161,11 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped( _center.SaveException.ToString() ); } - _center.Draw( imageSize ); + using var child2 = ImRaii.Child( "image" ); + if( child2 ) + { + _center.Draw( imageSize ); + } } private Vector2 GetChildWidth() From b7b15532f86a5fd1a2884ae84c9f0862e6333a1c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 23 Sep 2022 18:58:51 +0000 Subject: [PATCH 0501/2451] [CI] Updating repo.json for refs/tags/0.5.8.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f6a1fc63..1b59c782 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.7.1", - "TestingAssemblyVersion": "0.5.7.1", + "AssemblyVersion": "0.5.8.0", + "TestingAssemblyVersion": "0.5.8.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.7.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b359c183603733f8fc44de1c146370aa1831341c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Sep 2022 15:16:03 +0200 Subject: [PATCH 0502/2451] Add debug tab for meta changes (pretty useless...) and maybe fix reset problem. --- Penumbra/Interop/CharacterUtility.List.cs | 5 ++- Penumbra/Interop/CharacterUtility.cs | 4 +++ .../Resolver/PathResolver.DrawObjectState.cs | 2 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 33 ++++++++++++++++--- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index e6f412ae..1e33592b 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -11,6 +11,9 @@ public unsafe partial class CharacterUtility public readonly InternalIndex Index; public readonly Structs.CharacterUtility.Index GlobalIndex; + public IReadOnlyCollection< MetaReverter > Entries + => _entries; + private IntPtr _defaultResourceData = IntPtr.Zero; private int _defaultResourceSize = 0; public bool Ready { get; private set; } = false; @@ -93,8 +96,8 @@ public unsafe partial class CharacterUtility if( _entries.Count > 0 ) { _entries.Clear(); - ResetResourceInternal(); } + ResetResourceInternal(); } public sealed class MetaReverter : IDisposable diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index 44b9204e..63d08bc0 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Dalamud.Utility.Signatures; @@ -43,6 +44,9 @@ public unsafe partial class CharacterUtility : IDisposable .Select( idx => new List( new InternalIndex( idx ) ) ) .ToArray(); + public IReadOnlyList< List > Lists + => _lists; + public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) => _lists[ idx.Value ].DefaultResource; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index d30248f9..40228fad 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -160,7 +160,7 @@ public unsafe partial class PathResolver var ret = _characterBaseCreateHook.Original( a, b, c, d ); using( meta ) { - if( LastGameObject != null ) + if( LastGameObject != null && ret != IntPtr.Zero ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ret ); diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index fd36f308..8ced65cb 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -58,6 +58,8 @@ public partial class ConfigWindow ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawDebugTabMetaLists(); + ImGui.NewLine(); DrawDebugResidentResources(); ImGui.NewLine(); DrawResourceProblems(); @@ -237,7 +239,7 @@ public partial class ConfigWindow { var idx = CharacterUtility.RelevantIndices[ i ]; var intern = new CharacterUtility.InternalIndex( i ); - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resource(idx); + var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resource( idx ); ImGui.TableNextColumn(); ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); ImGui.TableNextColumn(); @@ -259,18 +261,39 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( $"{resource->GetData().Length}" ); ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResource(intern).Address:X}" ); + ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResource( intern ).Address:X}" ); if( ImGui.IsItemClicked() ) { ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResource(intern).Address, - Penumbra.CharacterUtility.DefaultResource(intern).Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResource( intern ).Address, + Penumbra.CharacterUtility.DefaultResource( intern ).Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); } ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResource(intern).Size}" ); + ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResource( intern ).Size}" ); + } + } + + private static void DrawDebugTabMetaLists() + { + if( !ImGui.CollapsingHeader( "Metadata Changes" ) ) + { + return; + } + + using var table = ImRaii.Table( "##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + foreach( var list in Penumbra.CharacterUtility.Lists ) + { + ImGuiUtil.DrawTableColumn( list.GlobalIndex.ToString() ); + ImGuiUtil.DrawTableColumn( list.Entries.Count.ToString() ); + ImGuiUtil.DrawTableColumn( string.Join( ", ", list.Entries.Select( e => $"0x{e.Data:X}" ) ) ); } } From 020cdbb86872ee5cc8e107fe7a264f59040264e3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 24 Sep 2022 13:20:42 +0000 Subject: [PATCH 0503/2451] [CI] Updating repo.json for refs/tags/0.5.8.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1b59c782..2df31ef7 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.0", - "TestingAssemblyVersion": "0.5.8.0", + "AssemblyVersion": "0.5.8.1", + "TestingAssemblyVersion": "0.5.8.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 35c6e0ec8880e81dfa68bd042e998c4a7a3f8729 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Sep 2022 18:32:31 +0200 Subject: [PATCH 0504/2451] Fix some unnecessary crashes on mtrl. --- Penumbra.GameData/Files/MtrlFile.Write.cs | 19 ++++++++++++------- Penumbra.GameData/Files/MtrlFile.cs | 11 ++++++++++- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 4b54c30f..43225ea2 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Text; @@ -46,14 +47,19 @@ public partial class MtrlFile } w.Write( AdditionalData ); - foreach( var row in ColorSets.Select( c => c.Rows ) ) + var dataSetSize = 0; + foreach( var row in ColorSets.Where( c => c.HasRows ).Select( c => c.Rows ) ) { - w.Write( row.AsBytes() ); + var span = row.AsBytes(); + w.Write( span ); + dataSetSize += span.Length; } foreach( var row in ColorDyeSets.Select( c => c.Rows ) ) { - w.Write( row.AsBytes() ); + var span = row.AsBytes(); + w.Write( span ); + dataSetSize += span.Length; } w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); @@ -88,19 +94,18 @@ public partial class MtrlFile w.Write( value ); } - WriteHeader( w, ( ushort )w.BaseStream.Position, cumulativeStringOffset ); + WriteHeader( w, ( ushort )w.BaseStream.Position, dataSetSize, cumulativeStringOffset ); } return stream.ToArray(); } - private void WriteHeader( BinaryWriter w, ushort fileSize, ushort shaderPackageNameOffset ) + private void WriteHeader( BinaryWriter w, ushort fileSize, int dataSetSize, ushort shaderPackageNameOffset ) { w.BaseStream.Seek( 0, SeekOrigin.Begin ); w.Write( Version ); w.Write( fileSize ); - w.Write( ( ushort )( ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size - + ColorDyeSets.Length * ColorDyeSet.RowArray.NumRows * 2 ) ); + w.Write( ( ushort )dataSetSize ); w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); w.Write( shaderPackageNameOffset ); w.Write( ( byte )Textures.Length ); diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 76257c1f..aa78b7ab 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -143,6 +143,7 @@ public partial class MtrlFile : IWritable public RowArray Rows; public string Name; public ushort Index; + public bool HasRows; } public unsafe struct ColorDyeSet @@ -305,7 +306,15 @@ public partial class MtrlFile : IWritable AdditionalData = r.ReadBytes( additionalDataSize ); for( var i = 0; i < ColorSets.Length; ++i ) { - ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); + if( stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length ) + { + ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); + ColorSets[ i ].HasRows = true; + } + else + { + ColorSets[i].HasRows = false; + } } for( var i = 0; i < ColorDyeSets.Length; ++i ) diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 99c25d26..cc4ef1c9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -259,7 +260,7 @@ public partial class ModEditWindow private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) { - if( file.ColorSets.Length == 0 ) + if( !file.ColorSets.Any(c => c.HasRows ) ) { return false; } From 62d3053d345dff7c277586e79f879d9b57a27b04 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Sep 2022 18:34:12 +0200 Subject: [PATCH 0505/2451] Fix some meta bugs. --- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 6 +-- Penumbra/Interop/CharacterUtility.List.cs | 44 ++++++++++++------- .../Interop/Resolver/PathResolver.Meta.cs | 6 +-- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 12b42e91..dcadb979 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -97,7 +97,7 @@ public partial class ModCollection { case CollectionType.Default: Default = newCollection; - if( Penumbra.CharacterUtility.Ready ) + if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) { Penumbra.ResidentResources.Reload(); Default.SetFiles(); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 9d0be99a..d2bb2d92 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -177,7 +177,7 @@ public partial class ModCollection ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -239,7 +239,7 @@ public partial class ModCollection if( addMetaChanges ) { ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -297,7 +297,7 @@ public partial class ModCollection AddMetaFiles(); } - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index 1e33592b..a73d87aa 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -11,7 +11,7 @@ public unsafe partial class CharacterUtility public readonly InternalIndex Index; public readonly Structs.CharacterUtility.Index GlobalIndex; - public IReadOnlyCollection< MetaReverter > Entries + public IReadOnlyCollection< MetaReverter > Entries => _entries; private IntPtr _defaultResourceData = IntPtr.Zero; @@ -95,8 +95,14 @@ public unsafe partial class CharacterUtility { if( _entries.Count > 0 ) { + foreach( var entry in _entries ) + { + entry.Disposed = true; + } + _entries.Clear(); } + ResetResourceInternal(); } @@ -106,6 +112,7 @@ public unsafe partial class CharacterUtility public readonly IntPtr Data; public readonly int Length; public readonly bool Resetter; + public bool Disposed; public MetaReverter( List list, IntPtr data, int length ) { @@ -124,29 +131,34 @@ public unsafe partial class CharacterUtility public void Dispose() { - var list = List._entries; - var wasCurrent = ReferenceEquals( this, list.First?.Value ); - list.Remove( this ); - if( !wasCurrent ) + if( !Disposed ) { - return; - } + var list = List._entries; + var wasCurrent = ReferenceEquals( this, list.First?.Value ); + list.Remove( this ); + if( !wasCurrent ) + { + return; + } - if( list.Count == 0 ) - { - List.SetResourceToDefaultCollection(); - } - else - { - var next = list.First!.Value; - if( next.Resetter ) + if( list.Count == 0 ) { List.SetResourceToDefaultCollection(); } else { - List.SetResourceInternal( next.Data, next.Length ); + var next = list.First!.Value; + if( next.Resetter ) + { + List.SetResourceToDefaultCollection(); + } + else + { + List.SetResourceInternal( next.Data, next.Length ); + } } + + Disposed = true; } } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 1121cb29..0496d8fb 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -165,7 +165,7 @@ public unsafe partial class PathResolver private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) { var resolveData = GetResolveData( drawObject ); - using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetGmpFile() : null; + using var gmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetGmpFile() : null; return _setupVisorHook.Original( drawObject, modelId, visorState ); } @@ -184,7 +184,7 @@ public unsafe partial class PathResolver else { var resolveData = GetResolveData( drawObject ); - using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; + using var cmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); } } @@ -200,7 +200,7 @@ public unsafe partial class PathResolver { _inChangeCustomize = true; var resolveData = GetResolveData( human ); - using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; + using var cmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; using var decals = resolveData.Valid ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ) : null; From 0d150cf19bd772debc9e7ec1f267ea6084e95ff2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Sep 2022 18:34:22 +0200 Subject: [PATCH 0506/2451] Add reloading button to textures. --- Penumbra/Import/Textures/Texture.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index a3ca306c..1f2997ee 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -250,7 +250,7 @@ public sealed class Texture : IDisposable _tmpPath ??= Path; using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); - ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) { @@ -279,5 +279,15 @@ public sealed class Texture : IDisposable manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), + "Reload the currently selected path.", false, + true ) ) + { + var path = Path; + Path = string.Empty; + Load( path ); + } } } \ No newline at end of file From 2ee64137a75ce69476fa6f77d646d6e31cf77f7c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 25 Sep 2022 18:36:14 +0200 Subject: [PATCH 0507/2451] Add Changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 0af2bdd2..01e286f9 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -19,10 +19,18 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); Add5_8_0( ret ); + Add5_8_2( ret ); return ret; } + private static void Add5_8_2( Changelog log ) + => log.NextVersion( "Version 0.5.8.2" ) + .RegisterEntry( "Fixed some problems with metadata reloading and reverting. (5.8.1, too)." ) + .RegisterHighlight( + "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", + 1 ); + private static void Add5_8_0( Changelog log ) => log.NextVersion( "Version 0.5.8.0" ) .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) From 133a912941812fc75884b31cf3791f8b05a31632 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 25 Sep 2022 16:38:40 +0000 Subject: [PATCH 0508/2451] [CI] Updating repo.json for refs/tags/0.5.8.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2df31ef7..f60bf5bf 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.1", - "TestingAssemblyVersion": "0.5.8.1", + "AssemblyVersion": "0.5.8.2", + "TestingAssemblyVersion": "0.5.8.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 0ff851f7170f48a2995232cbf99391b69feaefd8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Sep 2022 13:12:42 +0200 Subject: [PATCH 0509/2451] Another try at fixing metadata, maybe. --- .../Collections/ModCollection.Cache.Access.cs | 26 ++++--- .../Interop/Resolver/PathResolver.Meta.cs | 78 ++++++++----------- Penumbra/Interop/Resolver/PathResolver.cs | 9 +-- 3 files changed, 50 insertions(+), 63 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 7f379dc5..5b636966 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Interop; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections; @@ -159,18 +160,23 @@ public partial class ModCollection } // Used for short periods of changed files. - public CharacterUtility.List.MetaReverter? TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) - => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ); + public CharacterUtility.List.MetaReverter TemporarilySetEqdpFile( GenderRace genderRace, bool accessory ) + => _cache?.MetaManipulations.TemporarilySetEqdpFile( genderRace, accessory ) + ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.EqdpIdx( genderRace, accessory ) ); - public CharacterUtility.List.MetaReverter? TemporarilySetEqpFile() - => _cache?.MetaManipulations.TemporarilySetEqpFile(); + public CharacterUtility.List.MetaReverter TemporarilySetEqpFile() + => _cache?.MetaManipulations.TemporarilySetEqpFile() + ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Eqp ); - public CharacterUtility.List.MetaReverter? TemporarilySetGmpFile() - => _cache?.MetaManipulations.TemporarilySetGmpFile(); + public CharacterUtility.List.MetaReverter TemporarilySetGmpFile() + => _cache?.MetaManipulations.TemporarilySetGmpFile() + ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.Gmp ); - public CharacterUtility.List.MetaReverter? TemporarilySetCmpFile() - => _cache?.MetaManipulations.TemporarilySetCmpFile(); + public CharacterUtility.List.MetaReverter TemporarilySetCmpFile() + => _cache?.MetaManipulations.TemporarilySetCmpFile() + ?? Penumbra.CharacterUtility.TemporarilyResetResource( Interop.Structs.CharacterUtility.Index.HumanCmp ); - public CharacterUtility.List.MetaReverter? TemporarilySetEstFile( EstManipulation.EstType type ) - => _cache?.MetaManipulations.TemporarilySetEstFile( type ); + public CharacterUtility.List.MetaReverter TemporarilySetEstFile( EstManipulation.EstType type ) + => _cache?.MetaManipulations.TemporarilySetEstFile( type ) + ?? Penumbra.CharacterUtility.TemporarilyResetResource( ( Interop.Structs.CharacterUtility.Index )type ); } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index 0496d8fb..dc1e46d2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -79,16 +79,9 @@ public unsafe partial class PathResolver private void OnModelLoadCompleteDetour( IntPtr drawObject ) { var collection = GetResolveData( drawObject ); - if( collection.Valid ) - { - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); - _onModelLoadCompleteHook.Original.Invoke( drawObject ); - } - else - { - _onModelLoadCompleteHook.Original.Invoke( drawObject ); - } + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); + _onModelLoadCompleteHook.Original.Invoke( drawObject ); } private delegate void UpdateModelDelegate( IntPtr drawObject ); @@ -106,16 +99,9 @@ public unsafe partial class PathResolver } var collection = GetResolveData( drawObject ); - if( collection.Valid ) - { - using var eqp = collection.ModCollection.TemporarilySetEqpFile(); - using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); - _updateModelsHook.Original.Invoke( drawObject ); - } - else - { - _updateModelsHook.Original.Invoke( drawObject ); - } + using var eqp = collection.ModCollection.TemporarilySetEqpFile(); + using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true ); + _updateModelsHook.Original.Invoke( drawObject ); } private static GenderRace GetDrawObjectGenderRace( IntPtr drawObject ) @@ -150,7 +136,7 @@ public unsafe partial class PathResolver } var resolveData = GetResolveData( drawObject ); - using var eqp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetEqpFile() : null; + using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(); _getEqpIndirectHook.Original( drawObject ); } @@ -165,7 +151,7 @@ public unsafe partial class PathResolver private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState ) { var resolveData = GetResolveData( drawObject ); - using var gmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetGmpFile() : null; + using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(); return _setupVisorHook.Original( drawObject, modelId, visorState ); } @@ -184,7 +170,7 @@ public unsafe partial class PathResolver else { var resolveData = GetResolveData( drawObject ); - using var cmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); _rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 ); } } @@ -200,10 +186,8 @@ public unsafe partial class PathResolver { _inChangeCustomize = true; var resolveData = GetResolveData( human ); - using var cmp = resolveData.Valid ? resolveData.ModCollection.TemporarilySetCmpFile() : null; - using var decals = resolveData.Valid - ? new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ) - : null; + using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(); + using var decals = new CharacterUtility.DecalReverter( resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) ); return _changeCustomize.Original( human, data, skipEquipment ); } @@ -224,6 +208,7 @@ public unsafe partial class PathResolver return race switch { + // @formatter:off MidlanderMale => Convert( MidlanderMale ), HighlanderMale => Convert( MidlanderMale, HighlanderMale ), ElezenMale => Convert( MidlanderMale, ElezenMale ), @@ -245,28 +230,29 @@ public unsafe partial class PathResolver VieraFemale => Convert( MidlanderMale, MidlanderFemale, VieraFemale ), MidlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc ), - HighlanderMaleNpc => Convert( MidlanderMale, HighlanderMale, HighlanderMaleNpc ), - ElezenMaleNpc => Convert( MidlanderMale, ElezenMale, ElezenMaleNpc ), - MiqoteMaleNpc => Convert( MidlanderMale, MiqoteMale, MiqoteMaleNpc ), - RoegadynMaleNpc => Convert( MidlanderMale, RoegadynMale, RoegadynMaleNpc ), - LalafellMaleNpc => Convert( MidlanderMale, LalafellMale, LalafellMaleNpc ), - AuRaMaleNpc => Convert( MidlanderMale, AuRaMale, AuRaMaleNpc ), - HrothgarMaleNpc => Convert( MidlanderMale, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), - VieraMaleNpc => Convert( MidlanderMale, VieraMale, VieraMaleNpc ), + HighlanderMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, HighlanderMale, HighlanderMaleNpc ), + ElezenMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, ElezenMale, ElezenMaleNpc ), + MiqoteMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MiqoteMale, MiqoteMaleNpc ), + RoegadynMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, RoegadynMale, RoegadynMaleNpc ), + LalafellMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, LalafellMale, LalafellMaleNpc ), + AuRaMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, AuRaMale, AuRaMaleNpc ), + HrothgarMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, RoegadynMaleNpc, RoegadynMale, HrothgarMale, HrothgarMaleNpc ), + VieraMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, VieraMale, VieraMaleNpc ), - MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MidlanderFemaleNpc ), - HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderFemale, HighlanderFemale, HighlanderFemaleNpc ), - ElezenFemaleNpc => Convert( MidlanderMale, MidlanderFemale, ElezenFemale, ElezenFemaleNpc ), - MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderFemale, MiqoteFemale, MiqoteFemaleNpc ), - RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, RoegadynFemaleNpc ), - LalafellFemaleNpc => Convert( MidlanderMale, LalafellMale, LalafellFemale, LalafellFemaleNpc ), - AuRaFemaleNpc => Convert( MidlanderMale, MidlanderFemale, AuRaFemale, AuRaFemaleNpc ), - HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderFemale, RoegadynFemale, HrothgarFemale, HrothgarFemaleNpc ), - VieraFemaleNpc => Convert( MidlanderMale, MidlanderFemale, VieraFemale, VieraFemaleNpc ), + MidlanderFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc ), + HighlanderFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, HighlanderFemale, HighlanderFemaleNpc ), + ElezenFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, ElezenFemale, ElezenFemaleNpc ), + MiqoteFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, MiqoteFemale, MiqoteFemaleNpc ), + RoegadynFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, RoegadynFemale, RoegadynFemaleNpc ), + LalafellFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, LalafellMale, LalafellMaleNpc, LalafellFemale, LalafellFemaleNpc ), + AuRaFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, AuRaFemale, AuRaFemaleNpc ), + HrothgarFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, RoegadynFemale, RoegadynFemaleNpc, HrothgarFemale, HrothgarFemaleNpc ), + VieraFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, VieraFemale, VieraFemaleNpc ), - UnknownMaleNpc => Convert( MidlanderMale, UnknownMaleNpc ), - UnknownFemaleNpc => Convert( MidlanderMale, MidlanderFemale, UnknownFemaleNpc ), + UnknownMaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, UnknownMaleNpc ), + UnknownFemaleNpc => Convert( MidlanderMale, MidlanderMaleNpc, MidlanderFemale, MidlanderFemaleNpc, UnknownFemaleNpc ), _ => DisposableContainer.Empty, + // @formatter:on }; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 6b1d76a4..17311ed3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -147,13 +147,8 @@ public partial class PathResolver : IDisposable private static unsafe ResolveData GetResolveData( IntPtr drawObject ) { - var parent = FindParent( drawObject, out var resolveData ); - if( parent == null || resolveData.ModCollection == Penumbra.CollectionManager.Default ) - { - return ResolveData.Invalid; - } - - return resolveData.ModCollection.HasCache ? resolveData : ResolveData.Invalid; + var _ = FindParent( drawObject, out var resolveData ); + return resolveData; } internal IEnumerable< KeyValuePair< Utf8String, ResolveData > > PathCollections From bb8d9f9ad9123e5a532d933a8a3cb01f9b1839c1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 26 Sep 2022 11:15:33 +0000 Subject: [PATCH 0510/2451] [CI] Updating repo.json for refs/tags/0.5.8.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f60bf5bf..68edf17f 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.2", - "TestingAssemblyVersion": "0.5.8.2", + "AssemblyVersion": "0.5.8.3", + "TestingAssemblyVersion": "0.5.8.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From e98deab60c8a045345db7d2c7f4dc6f64847edd9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Sep 2022 14:05:07 +0200 Subject: [PATCH 0511/2451] Set IMC files when character utility is ready. --- Penumbra/Meta/Manager/MetaManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index d440e106..7fe1aaa1 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -154,6 +154,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM : 0; } + SetImcFiles(); if( Penumbra.CollectionManager.Default == _collection ) { SetFiles(); From 9cfb85d1aacafe93a5c7efd043988d8dfaaf891b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Sep 2022 14:06:49 +0200 Subject: [PATCH 0512/2451] Changed changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 01e286f9..e126b85d 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -19,14 +19,14 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); Add5_8_0( ret ); - Add5_8_2( ret ); + Add5_8_5( ret ); return ret; } - private static void Add5_8_2( Changelog log ) - => log.NextVersion( "Version 0.5.8.2" ) - .RegisterEntry( "Fixed some problems with metadata reloading and reverting. (5.8.1, too)." ) + private static void Add5_8_5( Changelog log ) + => log.NextVersion( "Version 0.5.8.5" ) + .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.5)." ) .RegisterHighlight( "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", 1 ); From d7f8476e5b2d68b011d665a56b0baafb7dfc08d0 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 26 Sep 2022 12:13:30 +0000 Subject: [PATCH 0513/2451] [CI] Updating repo.json for refs/tags/0.5.8.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 68edf17f..e2dd1c40 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.3", - "TestingAssemblyVersion": "0.5.8.3", + "AssemblyVersion": "0.5.8.5", + "TestingAssemblyVersion": "0.5.8.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5cdb13328ce58e8ae319962a5b444be6ece6911d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Sep 2022 14:41:09 +0200 Subject: [PATCH 0514/2451] And another one. --- Penumbra/Meta/Manager/MetaManager.cs | 3 +-- Penumbra/UI/ConfigWindow.Changelog.cs | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 7fe1aaa1..0469dc09 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -137,7 +137,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM } var loaded = 0; - foreach( var manip in Manipulations.Where( m => m.ManipulationType != MetaManipulation.Type.Imc ) ) + foreach( var manip in Manipulations ) { loaded += manip.ManipulationType switch { @@ -154,7 +154,6 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM : 0; } - SetImcFiles(); if( Penumbra.CollectionManager.Default == _collection ) { SetFiles(); diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index e126b85d..cf158935 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -19,14 +19,14 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); Add5_8_0( ret ); - Add5_8_5( ret ); + Add5_8_6( ret ); return ret; } - private static void Add5_8_5( Changelog log ) - => log.NextVersion( "Version 0.5.8.5" ) - .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.5)." ) + private static void Add5_8_6( Changelog log ) + => log.NextVersion( "Version 0.5.8.6" ) + .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.6)." ) .RegisterHighlight( "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", 1 ); From 6b6e686ee6db1d5f5c77410f0e07650f3cafd316 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 26 Sep 2022 12:44:04 +0000 Subject: [PATCH 0515/2451] [CI] Updating repo.json for refs/tags/0.5.8.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e2dd1c40..1e624cc1 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.5", - "TestingAssemblyVersion": "0.5.8.5", + "AssemblyVersion": "0.5.8.6", + "TestingAssemblyVersion": "0.5.8.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 49b53b7a6a04f97b8a302bea2fa8d38efeed6dfe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Sep 2022 20:13:02 +0200 Subject: [PATCH 0516/2451] Maybe final meta fixes? --- Penumbra/Interop/CharacterUtility.List.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs | 9 ++------- Penumbra/UI/ConfigWindow.Changelog.cs | 8 ++++---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/CharacterUtility.List.cs b/Penumbra/Interop/CharacterUtility.List.cs index a73d87aa..3cf137e8 100644 --- a/Penumbra/Interop/CharacterUtility.List.cs +++ b/Penumbra/Interop/CharacterUtility.List.cs @@ -150,7 +150,7 @@ public unsafe partial class CharacterUtility var next = list.First!.Value; if( next.Resetter ) { - List.SetResourceToDefaultCollection(); + List.ResetResourceInternal(); } else { diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 7841027a..0e0716ea 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -151,7 +151,7 @@ public partial class PathResolver } var data = GetResolveData( drawObject ); - return !data.Valid ? DisposableContainer.Empty : MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4); + return MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4); } using var eqdp = Get(); @@ -182,14 +182,9 @@ public partial class PathResolver return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) ); } - private DisposableContainer GetEstChanges( IntPtr drawObject ) + private static DisposableContainer GetEstChanges( IntPtr drawObject ) { var data = GetResolveData( drawObject ); - if( !data.Valid ) - { - return DisposableContainer.Empty; - } - return new DisposableContainer( data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Face ), data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Body ), data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Hair ), diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index cf158935..134cad79 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -19,14 +19,14 @@ public partial class ConfigWindow Add5_7_0( ret ); Add5_7_1( ret ); Add5_8_0( ret ); - Add5_8_6( ret ); + Add5_8_7( ret ); return ret; } - private static void Add5_8_6( Changelog log ) - => log.NextVersion( "Version 0.5.8.6" ) - .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.6)." ) + private static void Add5_8_7( Changelog log ) + => log.NextVersion( "Version 0.5.8.7" ) + .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7)." ) .RegisterHighlight( "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", 1 ); From 55c17c7845a96db63a67f7ac478219e0903bb816 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 26 Sep 2022 18:15:36 +0000 Subject: [PATCH 0517/2451] [CI] Updating repo.json for refs/tags/0.5.8.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1e624cc1..9ac0efd0 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.6", - "TestingAssemblyVersion": "0.5.8.6", + "AssemblyVersion": "0.5.8.7", + "TestingAssemblyVersion": "0.5.8.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c681f1533db813aa701eab98925ba0139a7eb55c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 Sep 2022 10:13:36 +0200 Subject: [PATCH 0518/2451] Let meta incorporation look at both extensions. --- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 58 ++++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 07c752af..47c777cd 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -154,43 +154,41 @@ public partial class Mod { foreach( var (key, file) in Files.ToList() ) { + var ext1 = key.Extension().AsciiToLower().ToString(); + var ext2 = file.Extension.ToLowerInvariant(); try { - switch( file.Extension ) + if( ext1 == ".meta" || ext2 == ".meta" ) { - case ".meta": - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } - var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); - if( delete ) - { - File.Delete( file.FullName ); - } + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } - ManipulationData.UnionWith( meta.MetaManipulations ); + ManipulationData.UnionWith( meta.MetaManipulations ); + } + else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) + { + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } - break; - case ".rgsp": - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); - if( delete ) - { - File.Delete( file.FullName ); - } - - ManipulationData.UnionWith( rgsp.MetaManipulations ); - - break; - default: continue; + ManipulationData.UnionWith( rgsp.MetaManipulations ); } } catch( Exception e ) From ef418b6821efcbda39f6298654efd8b75650f14d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 Sep 2022 14:12:31 +0200 Subject: [PATCH 0519/2451] Fix default collection on fresh installs. --- Penumbra/Collections/CollectionManager.Active.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index dcadb979..dd2feb68 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -97,7 +97,7 @@ public partial class ModCollection { case CollectionType.Default: Default = newCollection; - if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) + if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); Default.SetFiles(); @@ -202,7 +202,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? Empty.Name : DefaultCollection ); + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { @@ -216,7 +216,7 @@ public partial class ModCollection } // Load the interface collection. - var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? ( configChanged ? Empty.Name : Default.Name ); + var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? Default.Name; var interfaceIdx = GetIndexForCollectionName( interfaceName ); if( interfaceIdx < 0 ) { From 1d6d696cb7d6061f5427204663aef174183aa0f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 Sep 2022 15:37:21 +0200 Subject: [PATCH 0520/2451] Split special collections. --- Penumbra.GameData/Enums/Race.cs | 2 +- .../Collections/CollectionManager.Active.cs | 30 +- Penumbra/Collections/CollectionType.cs | 545 +++++++++++++----- Penumbra/Configuration.Migration.cs | 12 + Penumbra/Configuration.cs | 2 +- .../Resolver/PathResolver.Identification.cs | 72 +-- Penumbra/Penumbra.cs | 4 +- Penumbra/UI/ConfigWindow.Changelog.cs | 7 + Penumbra/UI/ConfigWindow.CollectionsTab.cs | 28 +- 9 files changed, 478 insertions(+), 224 deletions(-) diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index ba8f6337..f7b5ce7b 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -202,7 +202,7 @@ public static class RaceEnumExtensions SubRace.Midlander => "Midlander", SubRace.Highlander => "Highlander", SubRace.Wildwood => "Wildwood", - SubRace.Duskwight => "Duskwright", + SubRace.Duskwight => "Duskwight", SubRace.Plainsfolk => "Plainsfolk", SubRace.Dunesfolk => "Dunesfolk", SubRace.SeekerOfTheSun => "Seeker Of The Sun", diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index dd2feb68..454b809a 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -195,7 +195,7 @@ public partial class ModCollection public static string ActiveCollectionFile => Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); - // Load default, current and character collections from config. + // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. private void LoadCollections() { @@ -246,7 +246,7 @@ public partial class ModCollection } // Load special collections. - foreach( var type in CollectionTypeExtensions.Special ) + foreach( var (type, name, _) in CollectionTypeExtensions.Special ) { var typeName = jObject[ type.ToString() ]?.ToObject< string >(); if( typeName != null ) @@ -254,7 +254,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName( typeName ); if( idx < 0 ) { - Penumbra.Log.Error( $"Last choice of {type.ToName()} Collection {typeName} is not available, removed." ); + Penumbra.Log.Error( $"Last choice of {name} Collection {typeName} is not available, removed." ); configChanged = true; } else @@ -288,6 +288,30 @@ public partial class ModCollection } } + // Migrate ungendered collections to Male and Female for 0.5.9.0. + public static void MigrateUngenderedCollections() + { + if( !ReadActiveCollections( out var jObject ) ) + return; + + foreach( var (type, _, _) in CollectionTypeExtensions.Special.Where( t => t.Item2.StartsWith( "Male " ) ) ) + { + var oldName = type.ToString()[ 5.. ]; + var value = jObject[oldName]; + if( value == null ) + continue; + + jObject.Remove( oldName ); + jObject.Add( "Male" + oldName, value ); + jObject.Add( "Female" + oldName, value ); + } + + using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); + using var writer = new StreamWriter( stream ); + using var j = new JsonTextWriter( writer ); + jObject.WriteTo( j ); + } + public void SaveActiveCollections() { diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index e71c0433..01ab5ab0 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -8,40 +8,91 @@ public enum CollectionType : byte { // Special Collections Yourself = 0, - PlayerCharacter, - NonPlayerCharacter, - Midlander, - Highlander, - Wildwood, - Duskwight, - Plainsfolk, - Dunesfolk, - SeekerOfTheSun, - KeeperOfTheMoon, - Seawolf, - Hellsguard, - Raen, - Xaela, - Helion, - Lost, - Rava, - Veena, - MidlanderNpc, - HighlanderNpc, - WildwoodNpc, - DuskwightNpc, - PlainsfolkNpc, - DunesfolkNpc, - SeekerOfTheSunNpc, - KeeperOfTheMoonNpc, - SeawolfNpc, - HellsguardNpc, - RaenNpc, - XaelaNpc, - HelionNpc, - LostNpc, - RavaNpc, - VeenaNpc, + + MalePlayerCharacter, + FemalePlayerCharacter, + MaleNonPlayerCharacter, + FemaleNonPlayerCharacter, + + MaleMidlander, + FemaleMidlander, + MaleHighlander, + FemaleHighlander, + + MaleWildwood, + FemaleWildwood, + MaleDuskwight, + FemaleDuskwight, + + MalePlainsfolk, + FemalePlainsfolk, + MaleDunesfolk, + FemaleDunesfolk, + + MaleSeekerOfTheSun, + FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon, + FemaleKeeperOfTheMoon, + + MaleSeawolf, + FemaleSeawolf, + MaleHellsguard, + FemaleHellsguard, + + MaleRaen, + FemaleRaen, + MaleXaela, + FemaleXaela, + + MaleHelion, + FemaleHelion, + MaleLost, + FemaleLost, + + MaleRava, + FemaleRava, + MaleVeena, + FemaleVeena, + + MaleMidlanderNpc, + FemaleMidlanderNpc, + MaleHighlanderNpc, + FemaleHighlanderNpc, + + MaleWildwoodNpc, + FemaleWildwoodNpc, + MaleDuskwightNpc, + FemaleDuskwightNpc, + + MalePlainsfolkNpc, + FemalePlainsfolkNpc, + MaleDunesfolkNpc, + FemaleDunesfolkNpc, + + MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc, + FemaleKeeperOfTheMoonNpc, + + MaleSeawolfNpc, + FemaleSeawolfNpc, + MaleHellsguardNpc, + FemaleHellsguardNpc, + + MaleRaenNpc, + FemaleRaenNpc, + MaleXaelaNpc, + FemaleXaelaNpc, + + MaleHelionNpc, + FemaleHelionNpc, + MaleLostNpc, + FemaleLostNpc, + + MaleRavaNpc, + FemaleRavaNpc, + MaleVeenaNpc, + FemaleVeenaNpc, Inactive, // A collection was added or removed Default, // The default collection was changed @@ -55,52 +106,190 @@ public static class CollectionTypeExtensions public static bool IsSpecial( this CollectionType collectionType ) => collectionType is >= CollectionType.Yourself and < CollectionType.Inactive; - public static readonly CollectionType[] Special = Enum.GetValues< CollectionType >().Where( IsSpecial ).ToArray(); + public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >() + .Where( IsSpecial ) + .Select( s => ( s, s.ToName(), s.ToDescription() ) ) + .ToArray(); + + public static CollectionType FromParts( Gender gender, bool npc ) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return ( gender, npc ) switch + { + (Gender.Male, false) => CollectionType.MalePlayerCharacter, + (Gender.Female, false) => CollectionType.FemalePlayerCharacter, + (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, + (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, + _ => CollectionType.Inactive, + }; + } + + public static CollectionType FromParts( SubRace race, Gender gender, bool npc ) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return ( race, gender, npc ) switch + { + (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, + (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, + (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, + (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, + (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, + (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, + (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, + (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, + (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, + (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, + (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, + (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, + (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, + + (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, + (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, + (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, + (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, + (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, + (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, + (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, + (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, + (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, + (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, + (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, + (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, + (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, + + (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, + (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, + (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, + (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, + (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, + (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, + (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, + (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, + (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, + (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, + + (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, + (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, + (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, + (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, + (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, + (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, + (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, + (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, + (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, + (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, + _ => CollectionType.Inactive, + }; + } public static string ToName( this CollectionType collectionType ) => collectionType switch { - CollectionType.Yourself => "Your Character", - CollectionType.PlayerCharacter => "Player Characters", - CollectionType.NonPlayerCharacter => "Non-Player Characters", - CollectionType.Midlander => SubRace.Midlander.ToName(), - CollectionType.Highlander => SubRace.Highlander.ToName(), - CollectionType.Wildwood => SubRace.Wildwood.ToName(), - CollectionType.Duskwight => SubRace.Duskwight.ToName(), - CollectionType.Plainsfolk => SubRace.Plainsfolk.ToName(), - CollectionType.Dunesfolk => SubRace.Dunesfolk.ToName(), - CollectionType.SeekerOfTheSun => SubRace.SeekerOfTheSun.ToName(), - CollectionType.KeeperOfTheMoon => SubRace.KeeperOfTheMoon.ToName(), - CollectionType.Seawolf => SubRace.Seawolf.ToName(), - CollectionType.Hellsguard => SubRace.Hellsguard.ToName(), - CollectionType.Raen => SubRace.Raen.ToName(), - CollectionType.Xaela => SubRace.Xaela.ToName(), - CollectionType.Helion => SubRace.Helion.ToName(), - CollectionType.Lost => SubRace.Lost.ToName(), - CollectionType.Rava => SubRace.Rava.ToName(), - CollectionType.Veena => SubRace.Veena.ToName(), - CollectionType.MidlanderNpc => SubRace.Midlander.ToName() + " (NPC)", - CollectionType.HighlanderNpc => SubRace.Highlander.ToName() + " (NPC)", - CollectionType.WildwoodNpc => SubRace.Wildwood.ToName() + " (NPC)", - CollectionType.DuskwightNpc => SubRace.Duskwight.ToName() + " (NPC)", - CollectionType.PlainsfolkNpc => SubRace.Plainsfolk.ToName() + " (NPC)", - CollectionType.DunesfolkNpc => SubRace.Dunesfolk.ToName() + " (NPC)", - CollectionType.SeekerOfTheSunNpc => SubRace.SeekerOfTheSun.ToName() + " (NPC)", - CollectionType.KeeperOfTheMoonNpc => SubRace.KeeperOfTheMoon.ToName() + " (NPC)", - CollectionType.SeawolfNpc => SubRace.Seawolf.ToName() + " (NPC)", - CollectionType.HellsguardNpc => SubRace.Hellsguard.ToName() + " (NPC)", - CollectionType.RaenNpc => SubRace.Raen.ToName() + " (NPC)", - CollectionType.XaelaNpc => SubRace.Xaela.ToName() + " (NPC)", - CollectionType.HelionNpc => SubRace.Helion.ToName() + " (NPC)", - CollectionType.LostNpc => SubRace.Lost.ToName() + " (NPC)", - CollectionType.RavaNpc => SubRace.Rava.ToName() + " (NPC)", - CollectionType.VeenaNpc => SubRace.Veena.ToName() + " (NPC)", - CollectionType.Inactive => "Collection", - CollectionType.Default => "Default", - CollectionType.Interface => "Interface", - CollectionType.Character => "Character", - CollectionType.Current => "Current", - _ => string.Empty, + CollectionType.Yourself => "Your Character", + CollectionType.MalePlayerCharacter => "Male Player Characters", + CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", + CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", + CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", + CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", + CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", + CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", + CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", + CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", + CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", + CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", + CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", + CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", + CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", + CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", + CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", + CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", + CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", + CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", + CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", + CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", + CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", + CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", + CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", + CollectionType.FemalePlayerCharacter => "Female Player Characters", + CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", + CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", + CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", + CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", + CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", + CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", + CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", + CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", + CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", + CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", + CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", + CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", + CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", + CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", + CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", + CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", + CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", + CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", + CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", + CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", + CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", + CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", + CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", + CollectionType.Inactive => "Collection", + CollectionType.Default => "Default", + CollectionType.Interface => "Interface", + CollectionType.Character => "Character", + CollectionType.Current => "Current", + _ => string.Empty, }; public static string ToDescription( this CollectionType collectionType ) @@ -108,74 +297,142 @@ public static class CollectionTypeExtensions { CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" + "It takes precedence before all other collections except for explicitly named character collections.", - CollectionType.PlayerCharacter => - "This collection applies to all player characters that do not have a more specific character or racial collections associated.", - CollectionType.NonPlayerCharacter => - "This collection applies to all human non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.Midlander => - "This collection applies to all player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.Highlander => - "This collection applies to all player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.Wildwood => - "This collection applies to all player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.Duskwight => - "This collection applies to all player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.Plainsfolk => - "This collection applies to all player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.Dunesfolk => - "This collection applies to all player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.SeekerOfTheSun => - "This collection applies to all player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.KeeperOfTheMoon => - "This collection applies to all player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.Seawolf => - "This collection applies to all player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.Hellsguard => - "This collection applies to all player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.Raen => - "This collection applies to all player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.Xaela => - "This collection applies to all player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.Helion => - "This collection applies to all player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.Lost => - "This collection applies to all player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.Rava => - "This collection applies to all player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.Veena => - "This collection applies to all player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.MidlanderNpc => - "This collection applies to all non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.HighlanderNpc => - "This collection applies to all non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.WildwoodNpc => - "This collection applies to all non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.DuskwightNpc => - "This collection applies to all non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.PlainsfolkNpc => - "This collection applies to all non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.DunesfolkNpc => - "This collection applies to all non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.SeekerOfTheSunNpc => - "This collection applies to all non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.KeeperOfTheMoonNpc => - "This collection applies to all non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.SeawolfNpc => - "This collection applies to all non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.HellsguardNpc => - "This collection applies to all non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.RaenNpc => - "This collection applies to all non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.XaelaNpc => - "This collection applies to all non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.HelionNpc => - "This collection applies to all non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.LostNpc => - "This collection applies to all non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.RavaNpc => - "This collection applies to all non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.VeenaNpc => - "This collection applies to all non-player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.MalePlayerCharacter => + "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", + CollectionType.MaleNonPlayerCharacter => + "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.MaleMidlander => + "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlander => + "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwood => + "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwight => + "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolk => + "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolk => + "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSun => + "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoon => + "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolf => + "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguard => + "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaen => + "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaela => + "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelion => + "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLost => + "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRava => + "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeena => + "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.MaleMidlanderNpc => + "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlanderNpc => + "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwoodNpc => + "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwightNpc => + "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolkNpc => + "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolkNpc => + "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSunNpc => + "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoonNpc => + "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolfNpc => + "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguardNpc => + "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaenNpc => + "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaelaNpc => + "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelionNpc => + "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLostNpc => + "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRavaNpc => + "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeenaNpc => + "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemalePlayerCharacter => + "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", + CollectionType.FemaleNonPlayerCharacter => + "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.FemaleMidlander => + "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlander => + "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwood => + "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwight => + "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolk => + "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolk => + "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSun => + "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoon => + "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolf => + "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguard => + "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaen => + "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaela => + "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelion => + "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLost => + "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRava => + "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeena => + "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemaleMidlanderNpc => + "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlanderNpc => + "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwoodNpc => + "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwightNpc => + "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolkNpc => + "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolkNpc => + "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSunNpc => + "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoonNpc => + "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolfNpc => + "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguardNpc => + "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaenNpc => + "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaelaNpc => + "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelionNpc => + "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLostNpc => + "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRavaNpc => + "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeenaNpc => + "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", _ => string.Empty, }; } \ No newline at end of file diff --git a/Penumbra/Configuration.Migration.cs b/Penumbra/Configuration.Migration.cs index 2a2335c3..20df5d04 100644 --- a/Penumbra/Configuration.Migration.cs +++ b/Penumbra/Configuration.Migration.cs @@ -48,8 +48,20 @@ public partial class Configuration m.Version3To4(); m.Version4To5(); m.Version5To6(); + m.Version6To7(); } + // Gendered special collections were added. + private void Version6To7() + { + if( _config.Version != 6 ) + return; + + ModCollection.Manager.MigrateUngenderedCollections(); + _config.Version = 7; + } + + // A new tutorial step was inserted in the middle. // The UI collection and a new tutorial for it was added. // The migration for the UI collection itself happens in the ActiveCollections file. diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 0da4ca5e..15b738e4 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -144,7 +144,7 @@ public partial class Configuration : IPluginConfiguration // Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 6; + public const int CurrentVersion = 7; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 8b038a54..ef09c616 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -241,9 +241,9 @@ public unsafe partial class PathResolver { collection = null; // Check for the Yourself collection. - if( actor->ObjectIndex == 0 - || Cutscenes.GetParentIndex(actor->ObjectIndex) == 0 - || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) + if( actor->ObjectIndex == 0 + || Cutscenes.GetParentIndex( actor->ObjectIndex ) == 0 + || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() ) { collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ); if( collection != null ) @@ -258,64 +258,16 @@ public unsafe partial class PathResolver // Only handle human models. if( character->ModelCharaId == 0 ) { - // Check if the object is a non-player human NPC. - if( actor->ObjectKind == ( byte )ObjectKind.Player ) + var race = ( SubRace )character->CustomizeData[ 4 ]; + var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 ); + var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player; + + var type = CollectionTypeExtensions.FromParts( race, gender, isNpc ); + collection = Penumbra.CollectionManager.ByType( type ); + collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) ); + if( collection != null ) { - // Check the subrace. If it does not fit any or no subrace collection is set, check the player character collection. - collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch - { - SubRace.Midlander => Penumbra.CollectionManager.ByType( CollectionType.Midlander ), - SubRace.Highlander => Penumbra.CollectionManager.ByType( CollectionType.Highlander ), - SubRace.Wildwood => Penumbra.CollectionManager.ByType( CollectionType.Wildwood ), - SubRace.Duskwight => Penumbra.CollectionManager.ByType( CollectionType.Duskwight ), - SubRace.Plainsfolk => Penumbra.CollectionManager.ByType( CollectionType.Plainsfolk ), - SubRace.Dunesfolk => Penumbra.CollectionManager.ByType( CollectionType.Dunesfolk ), - SubRace.SeekerOfTheSun => Penumbra.CollectionManager.ByType( CollectionType.SeekerOfTheSun ), - SubRace.KeeperOfTheMoon => Penumbra.CollectionManager.ByType( CollectionType.KeeperOfTheMoon ), - SubRace.Seawolf => Penumbra.CollectionManager.ByType( CollectionType.Seawolf ), - SubRace.Hellsguard => Penumbra.CollectionManager.ByType( CollectionType.Hellsguard ), - SubRace.Raen => Penumbra.CollectionManager.ByType( CollectionType.Raen ), - SubRace.Xaela => Penumbra.CollectionManager.ByType( CollectionType.Xaela ), - SubRace.Helion => Penumbra.CollectionManager.ByType( CollectionType.Helion ), - SubRace.Lost => Penumbra.CollectionManager.ByType( CollectionType.Lost ), - SubRace.Rava => Penumbra.CollectionManager.ByType( CollectionType.Rava ), - SubRace.Veena => Penumbra.CollectionManager.ByType( CollectionType.Veena ), - _ => null, - }; - collection ??= Penumbra.CollectionManager.ByType( CollectionType.PlayerCharacter ); - if( collection != null ) - { - return true; - } - } - else - { - // Check the subrace. If it does not fit any or no subrace collection is set, check the npn-player character collection. - collection = ( SubRace )( ( Character* )actor )->CustomizeData[ 4 ] switch - { - SubRace.Midlander => Penumbra.CollectionManager.ByType( CollectionType.MidlanderNpc ), - SubRace.Highlander => Penumbra.CollectionManager.ByType( CollectionType.HighlanderNpc ), - SubRace.Wildwood => Penumbra.CollectionManager.ByType( CollectionType.WildwoodNpc ), - SubRace.Duskwight => Penumbra.CollectionManager.ByType( CollectionType.DuskwightNpc ), - SubRace.Plainsfolk => Penumbra.CollectionManager.ByType( CollectionType.PlainsfolkNpc ), - SubRace.Dunesfolk => Penumbra.CollectionManager.ByType( CollectionType.DunesfolkNpc ), - SubRace.SeekerOfTheSun => Penumbra.CollectionManager.ByType( CollectionType.SeekerOfTheSunNpc ), - SubRace.KeeperOfTheMoon => Penumbra.CollectionManager.ByType( CollectionType.KeeperOfTheMoonNpc ), - SubRace.Seawolf => Penumbra.CollectionManager.ByType( CollectionType.SeawolfNpc ), - SubRace.Hellsguard => Penumbra.CollectionManager.ByType( CollectionType.HellsguardNpc ), - SubRace.Raen => Penumbra.CollectionManager.ByType( CollectionType.RaenNpc ), - SubRace.Xaela => Penumbra.CollectionManager.ByType( CollectionType.XaelaNpc ), - SubRace.Helion => Penumbra.CollectionManager.ByType( CollectionType.HelionNpc ), - SubRace.Lost => Penumbra.CollectionManager.ByType( CollectionType.LostNpc ), - SubRace.Rava => Penumbra.CollectionManager.ByType( CollectionType.RavaNpc ), - SubRace.Veena => Penumbra.CollectionManager.ByType( CollectionType.VeenaNpc ), - _ => null, - }; - collection ??= Penumbra.CollectionManager.ByType( CollectionType.NonPlayerCharacter ); - if( collection != null ) - { - return true; - } + return true; } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b8ae544d..3c784c66 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -483,12 +483,12 @@ public class Penumbra : IDalamudPlugin sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName ); sb.AppendFormat( "> **`Interface Collection: `** {0}\n", CollectionManager.Interface.AnonymizedName ); sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName ); - foreach( var type in CollectionTypeExtensions.Special ) + foreach( var (type, name, _) in CollectionTypeExtensions.Special ) { var collection = CollectionManager.ByType( type ); if( collection != null ) { - sb.AppendFormat( "> **`{0,-29}`** {1}\n", type.ToName(), collection.AnonymizedName ); + sb.AppendFormat( "> **`{0,-29}`** {1}\n", name, collection.AnonymizedName ); } } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 134cad79..8e889d70 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -20,10 +20,17 @@ public partial class ConfigWindow Add5_7_1( ret ); Add5_8_0( ret ); Add5_8_7( ret ); + Add5_9_0( ret ); return ret; } + private static void Add5_9_0( Changelog log ) + => log.NextVersion( "Version 0.5.9.0" ) + .RegisterEntry( "Special Collections are now split between male and female." ) + .RegisterEntry( "Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install." ) + .RegisterEntry( "TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions." ); + private static void Add5_8_7( Changelog log ) => log.NextVersion( "Version 0.5.8.7" ) .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7)." ) diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index ffd7adf3..3df8a241 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -38,10 +38,10 @@ public partial class ConfigWindow // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection = false; - private string _newCharacterName = string.Empty; - private CollectionType? _currentType = CollectionType.Yourself; + private string _newCollectionName = string.Empty; + private bool _canAddCollection = false; + private string _newCharacterName = string.Empty; + private (CollectionType, string, string)? _currentType = CollectionTypeExtensions.Special.First(); // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -150,9 +150,10 @@ public partial class ConfigWindow + $"but all {IndividualAssignments} take precedence before them."; ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value ) != null ) + if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value.Item1 ) != null ) { - _currentType = CollectionTypeExtensions.Special.FindFirst( t => Penumbra.CollectionManager.ByType( t ) == null, out var t2 ) + _currentType = CollectionTypeExtensions.Special.FindFirst( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null, + out var t2 ) ? t2 : null; } @@ -162,16 +163,17 @@ public partial class ConfigWindow return; } - using( var combo = ImRaii.Combo( "##NewSpecial", _currentType.Value.ToName() ) ) + using( var combo = ImRaii.Combo( "##NewSpecial", _currentType.Value.Item2 ) ) { if( combo ) { - foreach( var type in CollectionTypeExtensions.Special.Where( t => Penumbra.CollectionManager.ByType( t ) == null ) ) + foreach( var type in CollectionTypeExtensions.Special.Where( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null ) ) { - if( ImGui.Selectable( type.ToName(), type == _currentType.Value ) ) + if( ImGui.Selectable( type.Item2, type.Item1 == _currentType.Value.Item1 ) ) { _currentType = type; } + ImGuiUtil.HoverTooltip( type.Item3 ); } } } @@ -183,7 +185,7 @@ public partial class ConfigWindow : description; if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) { - Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value ); + Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value.Item1 ); _currentType = null; } } @@ -212,7 +214,7 @@ public partial class ConfigWindow private void DrawSpecialCollections() { - foreach( var type in CollectionTypeExtensions.Special ) + foreach( var (type, name, desc) in CollectionTypeExtensions.Special ) { var collection = Penumbra.CollectionManager.ByType( type ); if( collection != null ) @@ -228,7 +230,7 @@ public partial class ConfigWindow ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker( type.ToName(), type.ToDescription() ); + ImGuiUtil.LabeledHelpMarker( name, desc ); } } } @@ -246,7 +248,7 @@ public partial class ConfigWindow private void DrawIndividualAssignments() { using var _ = ImRaii.Group(); - ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); + ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); ImGui.Separator(); foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) { From bb06c27359db5bbc00c634d4a9aefe820524884f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Sep 2022 16:37:32 +0200 Subject: [PATCH 0521/2451] Use filterable combo in special collections selector. --- OtterGui | 2 +- .../Collections/CollectionManager.Active.cs | 13 ++-- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 72 +++++++++++-------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/OtterGui b/OtterGui index c84ff1b1..51d7cc62 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c84ff1b1c313c5f4ae28645a2e3fcb4d214f240a +Subproject commit 51d7cc6247240f3b76db00d48a8004eeb0ef32cb diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 454b809a..75bfcef5 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -292,23 +292,28 @@ public partial class ModCollection public static void MigrateUngenderedCollections() { if( !ReadActiveCollections( out var jObject ) ) + { return; + } foreach( var (type, _, _) in CollectionTypeExtensions.Special.Where( t => t.Item2.StartsWith( "Male " ) ) ) { - var oldName = type.ToString()[ 5.. ]; - var value = jObject[oldName]; + var oldName = type.ToString()[ 4.. ]; + var value = jObject[ oldName ]; if( value == null ) + { continue; + } jObject.Remove( oldName ); - jObject.Add( "Male" + oldName, value ); - jObject.Add( "Female" + oldName, value ); + jObject.Add( "Male" + oldName, value ); + jObject.Add( "Female" + oldName, value ); } using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); using var writer = new StreamWriter( stream ); using var j = new JsonTextWriter( writer ); + j.Formatting = Formatting.Indented; jObject.WriteTo( j ); } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 3df8a241..41b85245 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -5,6 +6,7 @@ using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.Collections; namespace Penumbra.UI; @@ -38,10 +40,9 @@ public partial class ConfigWindow // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection = false; - private string _newCharacterName = string.Empty; - private (CollectionType, string, string)? _currentType = CollectionTypeExtensions.Special.First(); + private string _newCollectionName = string.Empty; + private bool _canAddCollection = false; + private string _newCharacterName = string.Empty; // Create a new collection that is either empty or a duplicate of the current collection. // Resets the new collection name. @@ -142,6 +143,34 @@ public partial class ConfigWindow $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); } + private sealed class SpecialCombo : FilteredCombo< (CollectionType, string, string) > + { + public (CollectionType, string, string)? CurrentType + => CollectionTypeExtensions.Special[ CurrentIdx ]; + + public int CurrentIdx = 0; + + public SpecialCombo( string label, float unscaledWidth ) + : base( label, unscaledWidth, CollectionTypeExtensions.Special ) + { } + + public void Draw() + => Draw( CurrentIdx ); + + protected override void Select( int globalIdx ) + { + CurrentIdx = globalIdx; + } + + protected override string ToString( (CollectionType, string, string) obj ) + => obj.Item2; + + protected override bool IsVisible( (CollectionType, string, string) obj ) + => Filter.IsContained( obj.Item2 ) && Penumbra.CollectionManager.ByType( obj.Item1 ) == null; + } + + private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); + // We do not check for valid character names. private void DrawNewSpecialCollection() { @@ -150,43 +179,29 @@ public partial class ConfigWindow + $"but all {IndividualAssignments} take precedence before them."; ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _currentType == null || Penumbra.CollectionManager.ByType( _currentType.Value.Item1 ) != null ) + if( _specialCollectionCombo.CurrentIdx == -1 + || Penumbra.CollectionManager.ByType( _specialCollectionCombo.CurrentType!.Value.Item1 ) != null ) { - _currentType = CollectionTypeExtensions.Special.FindFirst( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null, - out var t2 ) - ? t2 - : null; + _specialCollectionCombo.ResetFilter(); + _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special + .IndexOf( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null ); } - if( _currentType == null ) + if( _specialCollectionCombo.CurrentType == null ) { return; } - using( var combo = ImRaii.Combo( "##NewSpecial", _currentType.Value.Item2 ) ) - { - if( combo ) - { - foreach( var type in CollectionTypeExtensions.Special.Where( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null ) ) - { - if( ImGui.Selectable( type.Item2, type.Item1 == _currentType.Value.Item1 ) ) - { - _currentType = type; - } - ImGuiUtil.HoverTooltip( type.Item3 ); - } - } - } - + _specialCollectionCombo.Draw(); ImGui.SameLine(); - var disabled = _currentType == null; + var disabled = _specialCollectionCombo.CurrentType == null; var tt = disabled ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + description : description; if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) { - Penumbra.CollectionManager.CreateSpecialCollection( _currentType!.Value.Item1 ); - _currentType = null; + Penumbra.CollectionManager.CreateSpecialCollection( _specialCollectionCombo.CurrentType!.Value.Item1 ); + _specialCollectionCombo.CurrentIdx = -1; } } @@ -226,6 +241,7 @@ public partial class ConfigWindow false, true ) ) { Penumbra.CollectionManager.RemoveSpecialCollection( type ); + _specialCollectionCombo.ResetFilter(); } ImGui.SameLine(); From 7baed8d4301c5af28dd13e9ed9adf1a6dbc0bf1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Sep 2022 23:02:42 +0200 Subject: [PATCH 0522/2451] Add resetting to cutscene actors. --- .../Interop/Resolver/CutsceneCharacters.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index e8c1f05e..b055320a 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -22,7 +23,10 @@ public class CutsceneCharacters : IDisposable .Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) ); public CutsceneCharacters() - => SignatureHelper.Initialise( this ); + { + SignatureHelper.Initialise( this ); + Dalamud.Conditions.ConditionChange += Reset; + } // Get the related actor to a cutscene actor. // Does not check for valid input index. @@ -48,6 +52,36 @@ public class CutsceneCharacters : IDisposable return -1; } + public void Reset( ConditionFlag flag, bool value ) + { + switch( flag ) + { + case ConditionFlag.BetweenAreas: + case ConditionFlag.BetweenAreas51: + if( !value ) + { + return; + } + + break; + case ConditionFlag.OccupiedInCutSceneEvent: + case ConditionFlag.WatchingCutscene: + case ConditionFlag.WatchingCutscene78: + if( value ) + { + return; + } + + break; + default: return; + } + + for( var i = 0; i < _copiedCharacters.Length; ++i ) + { + _copiedCharacters[ i ] = -1; + } + } + public void Enable() => _copyCharacterHook.Enable(); @@ -55,8 +89,10 @@ public class CutsceneCharacters : IDisposable => _copyCharacterHook.Disable(); public void Dispose() - => _copyCharacterHook.Dispose(); - + { + _copyCharacterHook.Dispose(); + Dalamud.Conditions.ConditionChange -= Reset; + } private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); @@ -73,6 +109,7 @@ public class CutsceneCharacters : IDisposable ? -1 : source->ObjectIndex; _copiedCharacters[ target->ObjectIndex - CutsceneStartIdx ] = ( short )parent; + Penumbra.Log.Debug( $"Set cutscene character {target->ObjectIndex} to {parent}." ); } } catch From 80efa1ccb889efa49f803ea40833a3f4e70d19b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Sep 2022 23:13:20 +0200 Subject: [PATCH 0523/2451] Add Changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 8e889d70..09dc4a08 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -29,6 +29,7 @@ public partial class ConfigWindow => log.NextVersion( "Version 0.5.9.0" ) .RegisterEntry( "Special Collections are now split between male and female." ) .RegisterEntry( "Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install." ) + .RegisterEntry( "Fix a bug where cutscene actors were not properly reset and could be misidentified across multiple cutscenes." ) .RegisterEntry( "TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions." ); private static void Add5_8_7( Changelog log ) From 1808d263dad1a567f7072093d64aa3ba63d9389e Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 28 Sep 2022 21:17:03 +0000 Subject: [PATCH 0524/2451] [CI] Updating repo.json for refs/tags/0.5.9.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9ac0efd0..c2babd66 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.8.7", - "TestingAssemblyVersion": "0.5.8.7", + "AssemblyVersion": "0.5.9.0", + "TestingAssemblyVersion": "0.5.9.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.8.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From fe4955f8fce6ca8668a6efb44b210f0060a9f327 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Sep 2022 12:59:26 +0200 Subject: [PATCH 0525/2451] Fix filtered combo width. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 51d7cc62..9df97a04 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 51d7cc6247240f3b76db00d48a8004eeb0ef32cb +Subproject commit 9df97a04115eb08804e5d15fb3b05063e537fed2 From fabe2a9a166077b3ade32edd93c35e7b67566f63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Sep 2022 13:00:31 +0200 Subject: [PATCH 0526/2451] Fix some texture handling. --- Penumbra/Import/Textures/CombinedTexture.cs | 26 +++++----- Penumbra/Import/Textures/TexFileParser.cs | 52 ++++++++++---------- Penumbra/lib/OtterTex.dll | Bin 32256 -> 32256 bytes 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 61c3dbd5..8e831806 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -91,11 +91,11 @@ public partial class CombinedTexture : IDisposable var tex = type switch { - TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed(s, mipMaps ) : s, + TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s, TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), - TextureSaveType.BC5 => CreateCompressed( s, mipMaps, false ), - TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + TextureSaveType.BC5 => CreateCompressed( s, mipMaps, false ), + TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; if( !writeTex ) @@ -117,25 +117,29 @@ public partial class CombinedTexture : IDisposable private static void SaveTex( string path, ScratchImage input ) { - var header = input.Meta.ToTexHeader(); + var header = input.ToTexHeader(); if( header.Format == TexFile.TextureFormat.Unknown ) { throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." ); } - using var stream = File.OpenWrite( path ); + using var stream = File.Open( path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); using var w = new BinaryWriter( stream ); header.Write( w ); w.Write( input.Pixels ); } private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) - => mipMaps ? input.GenerateMipMaps() : input; + => mipMaps + ? input.GenerateMipMaps( Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ) ) + : input; private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) { - if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm) - return AddMipMaps(input, mipMaps); + if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm ) + { + return AddMipMaps( input, mipMaps ); + } if( input.Meta.Format.IsCompressed() ) { @@ -151,8 +155,8 @@ public partial class CombinedTexture : IDisposable private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 ) { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC5UNorm; - if( input.Meta.Format == format) + var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; + if( input.Meta.Format == format ) { return input; } diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index f38d066e..e9de84a0 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -61,8 +61,8 @@ public static class TexFileParser return i; } - width = Math.Max( width / 2, 4 ); - height = Math.Max( height / 2, 4 ); + width = Math.Max( width / 2, 1 ); + height = Math.Max( height / 2, 1 ); lastOffset = offset; lastSize = requiredSize; } @@ -103,8 +103,9 @@ public static class TexFileParser } } - public static TexFile.TexHeader ToTexHeader( this TexMeta meta ) + public static TexFile.TexHeader ToTexHeader( this ScratchImage scratch ) { + var meta = scratch.Meta; var ret = new TexFile.TexHeader() { Height = ( ushort )meta.Height, @@ -121,34 +122,33 @@ public static class TexFileParser _ => 0, }, }; - unsafe - { - ret.LodOffset[ 0 ] = 0; - ret.LodOffset[ 1 ] = 1; - ret.LodOffset[ 2 ] = 2; - ret.OffsetToSurface[ 0 ] = 80; - var size = meta.Format.BitsPerPixel() * meta.Width * meta.Height / 8; - for( var i = 1; i < meta.MipLevels; ++i ) - { - ret.OffsetToSurface[ i ] = ( uint )( 80 + size ); - size >>= 2; - if( size == 0 ) - { - ret.MipLevels = ( ushort )i; - break; - } - } - - for( var i = ret.MipLevels; i < 13; ++i ) - { - ret.OffsetToSurface[ i ] = 0; - } - } + ret.FillSurfaceOffsets( scratch ); return ret; } + private static unsafe void FillSurfaceOffsets( this ref TexFile.TexHeader header, ScratchImage scratch ) + { + var idx = 0; + fixed( byte* ptr = scratch.Pixels ) + { + foreach( var image in scratch.Images ) + { + var offset = ( byte* )image.Pixels - ptr; + header.OffsetToSurface[ idx++ ] = ( uint )( 80 + offset ); + } + } + + for( ; idx < 13; ++idx ) + header.OffsetToSurface[ idx ] = 0; + + header.LodOffset[ 0 ] = 0; + header.LodOffset[ 1 ] = 1; + header.LodOffset[ 2 ] = 2; + } + + public static TexMeta ToTexMeta( this TexFile.TexHeader header ) => new() { diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 447939777545e5efbf41b084e85a3a1f804cb3c2..5e54e1f648ba8cd5d97b579a95c9157575a69754 100644 GIT binary patch literal 32256 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 From 6014d37bedd3f3c4623be556d41e36b3268a7f41 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Sep 2022 13:50:52 +0200 Subject: [PATCH 0527/2451] Further fixes. --- Penumbra/Import/Textures/CombinedTexture.cs | 2 +- Penumbra/Import/Textures/TexFileParser.cs | 5 +++-- Penumbra/lib/OtterTex.dll | Bin 32256 -> 32256 bytes 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 8e831806..ecbf9caa 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -131,7 +131,7 @@ public partial class CombinedTexture : IDisposable private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) => mipMaps - ? input.GenerateMipMaps( Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ) ) + ? input.GenerateMipMaps( Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ), FilterFlags.SeparateAlpha ) : input; private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index e9de84a0..700acc37 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -41,6 +41,7 @@ public static class TexFileParser var lastOffset = 0L; var lastSize = 80L; + var minSize = meta.Format.IsCompressed() ? 4 : 1; for( var i = 0; i < 13; ++i ) { var offset = header.OffsetToSurface[ i ]; @@ -61,8 +62,8 @@ public static class TexFileParser return i; } - width = Math.Max( width / 2, 1 ); - height = Math.Max( height / 2, 1 ); + width = Math.Max( width / 2, minSize ); + height = Math.Max( height / 2, minSize ); lastOffset = offset; lastSize = requiredSize; } diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 5e54e1f648ba8cd5d97b579a95c9157575a69754..32744d0dc101a9eac49f9111cdffddd8f23ffaf4 100644 GIT binary patch delta 10986 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 From 097923f5ff7bad4b13b1035a77b5fa85b8e1dd35 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Oct 2022 11:36:17 +0200 Subject: [PATCH 0528/2451] Several small fixes. --- OtterGui | 2 +- Penumbra.GameData/Enums/RspAttribute.cs | 159 +++++++------ Penumbra.GameData/Enums/WeaponCategory.cs | 209 ++++++++++++++++++ Penumbra/Import/Textures/TexFileParser.cs | 6 + Penumbra/Import/Textures/Texture.cs | 2 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 27 ++- 7 files changed, 314 insertions(+), 93 deletions(-) create mode 100644 Penumbra.GameData/Enums/WeaponCategory.cs diff --git a/OtterGui b/OtterGui index 9df97a04..750f7415 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9df97a04115eb08804e5d15fb3b05063e537fed2 +Subproject commit 750f7415d07334530ad12db10f3dd28fd7e1f130 diff --git a/Penumbra.GameData/Enums/RspAttribute.cs b/Penumbra.GameData/Enums/RspAttribute.cs index 07d03b67..c4016bb6 100644 --- a/Penumbra.GameData/Enums/RspAttribute.cs +++ b/Penumbra.GameData/Enums/RspAttribute.cs @@ -1,92 +1,91 @@ using System.ComponentModel; -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum RspAttribute : byte { - public enum RspAttribute : byte + MaleMinSize, + MaleMaxSize, + MaleMinTail, + MaleMaxTail, + FemaleMinSize, + FemaleMaxSize, + FemaleMinTail, + FemaleMaxTail, + BustMinX, + BustMinY, + BustMinZ, + BustMaxX, + BustMaxY, + BustMaxZ, + NumAttributes, +} + +public static class RspAttributeExtensions +{ + public static Gender ToGender( this RspAttribute attribute ) { - MaleMinSize, - MaleMaxSize, - MaleMinTail, - MaleMaxTail, - FemaleMinSize, - FemaleMaxSize, - FemaleMinTail, - FemaleMaxTail, - BustMinX, - BustMinY, - BustMinZ, - BustMaxX, - BustMaxY, - BustMaxZ, - NumAttributes, + return attribute switch + { + RspAttribute.MaleMinSize => Gender.Male, + RspAttribute.MaleMaxSize => Gender.Male, + RspAttribute.MaleMinTail => Gender.Male, + RspAttribute.MaleMaxTail => Gender.Male, + RspAttribute.FemaleMinSize => Gender.Female, + RspAttribute.FemaleMaxSize => Gender.Female, + RspAttribute.FemaleMinTail => Gender.Female, + RspAttribute.FemaleMaxTail => Gender.Female, + RspAttribute.BustMinX => Gender.Female, + RspAttribute.BustMinY => Gender.Female, + RspAttribute.BustMinZ => Gender.Female, + RspAttribute.BustMaxX => Gender.Female, + RspAttribute.BustMaxY => Gender.Female, + RspAttribute.BustMaxZ => Gender.Female, + _ => Gender.Unknown, + }; } - public static class RspAttributeExtensions + public static string ToUngenderedString( this RspAttribute attribute ) { - public static Gender ToGender( this RspAttribute attribute ) + return attribute switch { - return attribute switch - { - RspAttribute.MaleMinSize => Gender.Male, - RspAttribute.MaleMaxSize => Gender.Male, - RspAttribute.MaleMinTail => Gender.Male, - RspAttribute.MaleMaxTail => Gender.Male, - RspAttribute.FemaleMinSize => Gender.Female, - RspAttribute.FemaleMaxSize => Gender.Female, - RspAttribute.FemaleMinTail => Gender.Female, - RspAttribute.FemaleMaxTail => Gender.Female, - RspAttribute.BustMinX => Gender.Female, - RspAttribute.BustMinY => Gender.Female, - RspAttribute.BustMinZ => Gender.Female, - RspAttribute.BustMaxX => Gender.Female, - RspAttribute.BustMaxY => Gender.Female, - RspAttribute.BustMaxZ => Gender.Female, - _ => Gender.Unknown, - }; - } + RspAttribute.MaleMinSize => "MinSize", + RspAttribute.MaleMaxSize => "MaxSize", + RspAttribute.MaleMinTail => "MinTail", + RspAttribute.MaleMaxTail => "MaxTail", + RspAttribute.FemaleMinSize => "MinSize", + RspAttribute.FemaleMaxSize => "MaxSize", + RspAttribute.FemaleMinTail => "MinTail", + RspAttribute.FemaleMaxTail => "MaxTail", + RspAttribute.BustMinX => "BustMinX", + RspAttribute.BustMinY => "BustMinY", + RspAttribute.BustMinZ => "BustMinZ", + RspAttribute.BustMaxX => "BustMaxX", + RspAttribute.BustMaxY => "BustMaxY", + RspAttribute.BustMaxZ => "BustMaxZ", + _ => "", + }; + } - public static string ToUngenderedString( this RspAttribute attribute ) + public static string ToFullString( this RspAttribute attribute ) + { + return attribute switch { - return attribute switch - { - RspAttribute.MaleMinSize => "MinSize", - RspAttribute.MaleMaxSize => "MaxSize", - RspAttribute.MaleMinTail => "MinTail", - RspAttribute.MaleMaxTail => "MaxTail", - RspAttribute.FemaleMinSize => "MinSize", - RspAttribute.FemaleMaxSize => "MaxSize", - RspAttribute.FemaleMinTail => "MinTail", - RspAttribute.FemaleMaxTail => "MaxTail", - RspAttribute.BustMinX => "BustMinX", - RspAttribute.BustMinY => "BustMinY", - RspAttribute.BustMinZ => "BustMinZ", - RspAttribute.BustMaxX => "BustMaxX", - RspAttribute.BustMaxY => "BustMaxY", - RspAttribute.BustMaxZ => "BustMaxZ", - _ => "", - }; - } - - public static string ToFullString( this RspAttribute attribute ) - { - return attribute switch - { - RspAttribute.MaleMinSize => "Male Minimum Size", - RspAttribute.MaleMaxSize => "Male Maximum Size", - RspAttribute.FemaleMinSize => "Female Minimum Size", - RspAttribute.FemaleMaxSize => "Female Maximum Size", - RspAttribute.BustMinX => "Bust Minimum X-Axis", - RspAttribute.BustMaxX => "Bust Maximum X-Axis", - RspAttribute.BustMinY => "Bust Minimum Y-Axis", - RspAttribute.BustMaxY => "Bust Maximum Y-Axis", - RspAttribute.BustMinZ => "Bust Minimum Z-Axis", - RspAttribute.BustMaxZ => "Bust Maximum Z-Axis", - RspAttribute.MaleMinTail => "Male Minimum Tail Length", - RspAttribute.MaleMaxTail => "Male Maximum Tail Length", - RspAttribute.FemaleMinTail => "Female Minimum Tail Length", - RspAttribute.FemaleMaxTail => "Female Maximum Tail Length", - _ => throw new InvalidEnumArgumentException(), - }; - } + RspAttribute.MaleMinSize => "Male Minimum Size", + RspAttribute.MaleMaxSize => "Male Maximum Size", + RspAttribute.FemaleMinSize => "Female Minimum Size", + RspAttribute.FemaleMaxSize => "Female Maximum Size", + RspAttribute.BustMinX => "Bust Minimum X-Axis", + RspAttribute.BustMaxX => "Bust Maximum X-Axis", + RspAttribute.BustMinY => "Bust Minimum Y-Axis", + RspAttribute.BustMaxY => "Bust Maximum Y-Axis", + RspAttribute.BustMinZ => "Bust Minimum Z-Axis", + RspAttribute.BustMaxZ => "Bust Maximum Z-Axis", + RspAttribute.MaleMinTail => "Male Minimum Tail Length", + RspAttribute.MaleMaxTail => "Male Maximum Tail Length", + RspAttribute.FemaleMinTail => "Female Minimum Tail Length", + RspAttribute.FemaleMaxTail => "Female Maximum Tail Length", + _ => throw new InvalidEnumArgumentException(), + }; } } \ No newline at end of file diff --git a/Penumbra.GameData/Enums/WeaponCategory.cs b/Penumbra.GameData/Enums/WeaponCategory.cs new file mode 100644 index 00000000..f7b1fc91 --- /dev/null +++ b/Penumbra.GameData/Enums/WeaponCategory.cs @@ -0,0 +1,209 @@ +using System; + +namespace Penumbra.GameData.Enums; + +public enum WeaponCategory : byte +{ + Unknown = 0, + Pugilist, + Gladiator, + Marauder, + Archer, + Lancer, + Thaumaturge1, + Thaumaturge2, + Conjurer1, + Conjurer2, + Arcanist, + Shield, + CarpenterMain, + CarpenterOff, + BlacksmithMain, + BlacksmithOff, + ArmorerMain, + ArmorerOff, + GoldsmithMain, + GoldsmithOff, + LeatherworkerMain, + LeatherworkerOff, + WeaverMain, + WeaverOff, + AlchemistMain, + AlchemistOff, + CulinarianMain, + CulinarianOff, + MinerMain, + MinerOff, + BotanistMain, + BotanistOff, + FisherMain, + Rogue = 84, + DarkKnight = 87, + Machinist = 88, + Astrologian = 89, + Samurai = 96, + RedMage = 97, + Scholar = 98, + FisherOff = 99, + BlueMage = 105, + Gunbreaker = 106, + Dancer = 107, + Reaper = 108, + Sage = 109, +} + +public static class WeaponCategoryExtensions +{ + public static WeaponCategory AllowsOffHand( this WeaponCategory category ) + => category switch + { + WeaponCategory.Pugilist => WeaponCategory.Pugilist, + WeaponCategory.Gladiator => WeaponCategory.Shield, + WeaponCategory.Marauder => WeaponCategory.Unknown, + WeaponCategory.Archer => WeaponCategory.Unknown, + WeaponCategory.Lancer => WeaponCategory.Unknown, + WeaponCategory.Thaumaturge1 => WeaponCategory.Shield, + WeaponCategory.Thaumaturge2 => WeaponCategory.Unknown, + WeaponCategory.Conjurer1 => WeaponCategory.Shield, + WeaponCategory.Conjurer2 => WeaponCategory.Unknown, + WeaponCategory.Arcanist => WeaponCategory.Unknown, + WeaponCategory.Shield => WeaponCategory.Unknown, + WeaponCategory.CarpenterMain => WeaponCategory.CarpenterOff, + WeaponCategory.CarpenterOff => WeaponCategory.Unknown, + WeaponCategory.BlacksmithMain => WeaponCategory.BlacksmithOff, + WeaponCategory.BlacksmithOff => WeaponCategory.Unknown, + WeaponCategory.ArmorerMain => WeaponCategory.ArmorerOff, + WeaponCategory.ArmorerOff => WeaponCategory.Unknown, + WeaponCategory.GoldsmithMain => WeaponCategory.GoldsmithOff, + WeaponCategory.GoldsmithOff => WeaponCategory.Unknown, + WeaponCategory.LeatherworkerMain => WeaponCategory.LeatherworkerOff, + WeaponCategory.LeatherworkerOff => WeaponCategory.Unknown, + WeaponCategory.WeaverMain => WeaponCategory.WeaverOff, + WeaponCategory.WeaverOff => WeaponCategory.Unknown, + WeaponCategory.AlchemistMain => WeaponCategory.AlchemistOff, + WeaponCategory.AlchemistOff => WeaponCategory.Unknown, + WeaponCategory.CulinarianMain => WeaponCategory.CulinarianOff, + WeaponCategory.CulinarianOff => WeaponCategory.Unknown, + WeaponCategory.MinerMain => WeaponCategory.MinerOff, + WeaponCategory.MinerOff => WeaponCategory.Unknown, + WeaponCategory.BotanistMain => WeaponCategory.BotanistOff, + WeaponCategory.BotanistOff => WeaponCategory.Unknown, + WeaponCategory.FisherMain => WeaponCategory.FisherOff, + WeaponCategory.Rogue => WeaponCategory.Rogue, + WeaponCategory.DarkKnight => WeaponCategory.Unknown, + WeaponCategory.Machinist => WeaponCategory.Machinist, + WeaponCategory.Astrologian => WeaponCategory.Astrologian, + WeaponCategory.Samurai => WeaponCategory.Unknown, + WeaponCategory.RedMage => WeaponCategory.RedMage, + WeaponCategory.Scholar => WeaponCategory.Unknown, + WeaponCategory.FisherOff => WeaponCategory.Unknown, + WeaponCategory.BlueMage => WeaponCategory.Unknown, + WeaponCategory.Gunbreaker => WeaponCategory.Unknown, + WeaponCategory.Dancer => WeaponCategory.Dancer, + WeaponCategory.Reaper => WeaponCategory.Unknown, + WeaponCategory.Sage => WeaponCategory.Unknown, + _ => WeaponCategory.Unknown, + }; + + public static EquipSlot ToSlot( this WeaponCategory category ) + => category switch + { + WeaponCategory.Pugilist => EquipSlot.MainHand, + WeaponCategory.Gladiator => EquipSlot.MainHand, + WeaponCategory.Marauder => EquipSlot.MainHand, + WeaponCategory.Archer => EquipSlot.MainHand, + WeaponCategory.Lancer => EquipSlot.MainHand, + WeaponCategory.Thaumaturge1 => EquipSlot.MainHand, + WeaponCategory.Thaumaturge2 => EquipSlot.MainHand, + WeaponCategory.Conjurer1 => EquipSlot.MainHand, + WeaponCategory.Conjurer2 => EquipSlot.MainHand, + WeaponCategory.Arcanist => EquipSlot.MainHand, + WeaponCategory.Shield => EquipSlot.OffHand, + WeaponCategory.CarpenterMain => EquipSlot.MainHand, + WeaponCategory.CarpenterOff => EquipSlot.OffHand, + WeaponCategory.BlacksmithMain => EquipSlot.MainHand, + WeaponCategory.BlacksmithOff => EquipSlot.OffHand, + WeaponCategory.ArmorerMain => EquipSlot.MainHand, + WeaponCategory.ArmorerOff => EquipSlot.OffHand, + WeaponCategory.GoldsmithMain => EquipSlot.MainHand, + WeaponCategory.GoldsmithOff => EquipSlot.OffHand, + WeaponCategory.LeatherworkerMain => EquipSlot.MainHand, + WeaponCategory.LeatherworkerOff => EquipSlot.OffHand, + WeaponCategory.WeaverMain => EquipSlot.MainHand, + WeaponCategory.WeaverOff => EquipSlot.OffHand, + WeaponCategory.AlchemistMain => EquipSlot.MainHand, + WeaponCategory.AlchemistOff => EquipSlot.OffHand, + WeaponCategory.CulinarianMain => EquipSlot.MainHand, + WeaponCategory.CulinarianOff => EquipSlot.OffHand, + WeaponCategory.MinerMain => EquipSlot.MainHand, + WeaponCategory.MinerOff => EquipSlot.OffHand, + WeaponCategory.BotanistMain => EquipSlot.MainHand, + WeaponCategory.BotanistOff => EquipSlot.OffHand, + WeaponCategory.FisherMain => EquipSlot.MainHand, + WeaponCategory.Rogue => EquipSlot.MainHand, + WeaponCategory.DarkKnight => EquipSlot.MainHand, + WeaponCategory.Machinist => EquipSlot.MainHand, + WeaponCategory.Astrologian => EquipSlot.MainHand, + WeaponCategory.Samurai => EquipSlot.MainHand, + WeaponCategory.RedMage => EquipSlot.MainHand, + WeaponCategory.Scholar => EquipSlot.MainHand, + WeaponCategory.FisherOff => EquipSlot.OffHand, + WeaponCategory.BlueMage => EquipSlot.MainHand, + WeaponCategory.Gunbreaker => EquipSlot.MainHand, + WeaponCategory.Dancer => EquipSlot.MainHand, + WeaponCategory.Reaper => EquipSlot.MainHand, + WeaponCategory.Sage => EquipSlot.MainHand, + _ => EquipSlot.Unknown, + }; + + public static int ToIndex( this WeaponCategory category ) + => category switch + { + WeaponCategory.Pugilist => 0, + WeaponCategory.Gladiator => 1, + WeaponCategory.Marauder => 2, + WeaponCategory.Archer => 3, + WeaponCategory.Lancer => 4, + WeaponCategory.Thaumaturge1 => 5, + WeaponCategory.Thaumaturge2 => 6, + WeaponCategory.Conjurer1 => 7, + WeaponCategory.Conjurer2 => 8, + WeaponCategory.Arcanist => 9, + WeaponCategory.Shield => 10, + WeaponCategory.CarpenterMain => 11, + WeaponCategory.CarpenterOff => 12, + WeaponCategory.BlacksmithMain => 13, + WeaponCategory.BlacksmithOff => 14, + WeaponCategory.ArmorerMain => 15, + WeaponCategory.ArmorerOff => 16, + WeaponCategory.GoldsmithMain => 17, + WeaponCategory.GoldsmithOff => 18, + WeaponCategory.LeatherworkerMain => 19, + WeaponCategory.LeatherworkerOff => 20, + WeaponCategory.WeaverMain => 21, + WeaponCategory.WeaverOff => 22, + WeaponCategory.AlchemistMain => 23, + WeaponCategory.AlchemistOff => 24, + WeaponCategory.CulinarianMain => 25, + WeaponCategory.CulinarianOff => 26, + WeaponCategory.MinerMain => 27, + WeaponCategory.MinerOff => 28, + WeaponCategory.BotanistMain => 29, + WeaponCategory.BotanistOff => 30, + WeaponCategory.FisherMain => 31, + WeaponCategory.Rogue => 32, + WeaponCategory.DarkKnight => 33, + WeaponCategory.Machinist => 34, + WeaponCategory.Astrologian => 35, + WeaponCategory.Samurai => 36, + WeaponCategory.RedMage => 37, + WeaponCategory.Scholar => 38, + WeaponCategory.FisherOff => 39, + WeaponCategory.BlueMage => 40, + WeaponCategory.Gunbreaker => 41, + WeaponCategory.Dancer => 42, + WeaponCategory.Reaper => 43, + WeaponCategory.Sage => 44, + _ => -1, + }; +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 700acc37..6b77bd0e 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -25,6 +25,10 @@ public static class TexFileParser } meta.MipLevels = CountMipLevels( data, in meta, in header ); + if( meta.MipLevels == 0 ) + { + throw new Exception( "Could not load file. Image is corrupted and does not contain enough data for its size." ); + } var scratch = ScratchImage.Initialize( meta ); @@ -142,7 +146,9 @@ public static class TexFileParser } for( ; idx < 13; ++idx ) + { header.OffsetToSurface[ idx ] = 0; + } header.LodOffset[ 0 ] = 0; header.LodOffset[ 1 ] = 1; diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index 1f2997ee..b3df03f0 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -138,7 +138,7 @@ public sealed class Texture : IDisposable try { - var _ = System.IO.Path.GetExtension( Path ) switch + var _ = System.IO.Path.GetExtension( Path ).ToLowerInvariant() switch { ".dds" => LoadDds(), ".png" => LoadPng(), diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index cc4ef1c9..170b710e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -349,7 +349,7 @@ public partial class ModEditWindow fixed( byte* ptr = data ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( file.ColorDyeSets.Length <= colorSetIdx ) + if( colorSetIdx < file.ColorDyeSets.Length ) { file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 41b85245..bf8b0107 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -5,6 +5,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; @@ -143,30 +144,36 @@ public partial class ConfigWindow $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); } - private sealed class SpecialCombo : FilteredCombo< (CollectionType, string, string) > + private sealed class SpecialCombo : FilterComboBase< (CollectionType, string, string) > { public (CollectionType, string, string)? CurrentType => CollectionTypeExtensions.Special[ CurrentIdx ]; - public int CurrentIdx = 0; + public int CurrentIdx = 0; + private readonly float _unscaledWidth; + private readonly string _label; public SpecialCombo( string label, float unscaledWidth ) - : base( label, unscaledWidth, CollectionTypeExtensions.Special ) - { } + : base( CollectionTypeExtensions.Special, false ) + { + _label = label; + _unscaledWidth = unscaledWidth; + } public void Draw() - => Draw( CurrentIdx ); - - protected override void Select( int globalIdx ) { - CurrentIdx = globalIdx; + var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; + Draw(_label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing()); } protected override string ToString( (CollectionType, string, string) obj ) => obj.Item2; - protected override bool IsVisible( (CollectionType, string, string) obj ) - => Filter.IsContained( obj.Item2 ) && Penumbra.CollectionManager.ByType( obj.Item1 ) == null; + protected override bool IsVisible( int globalIdx, LowerString filter ) + { + var obj = Items[ globalIdx ]; + return filter.IsContained( obj.Item2 ) && Penumbra.CollectionManager.ByType( obj.Item1 ) == null; + } } private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); From 31ac6187bc9e320b135f37304951beece5759c24 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Oct 2022 12:31:08 +0200 Subject: [PATCH 0529/2451] Extended export capabilities --- Penumbra/Configuration.cs | 1 + Penumbra/Mods/Editor/ModBackup.cs | 41 +++++++++++---- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 4 +- Penumbra/Mods/Manager/Mod.Manager.Root.cs | 50 ++++++++++++++++++ Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 20 ++++---- .../UI/ConfigWindow.SettingsTab.General.cs | 51 +++++++++++++++++++ 6 files changed, 147 insertions(+), 20 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 15b738e4..711164dd 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -26,6 +26,7 @@ public partial class Configuration : IPluginConfiguration public bool EnableMods { get; set; } = true; public string ModDirectory { get; set; } = string.Empty; + public string ExportDirectory { get; set; } = string.Empty; public bool HideUiInGPose { get; set; } = false; public bool HideUiInCutscenes { get; set; } = true; diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 48bbc4fa..d554ee0e 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -17,12 +17,12 @@ public class ModBackup public ModBackup( Mod mod ) { _mod = mod; - Name = _mod.ModPath + ".pmp"; + Name = Path.Combine( Penumbra.ModManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp"; Exists = File.Exists( Name ); } // Migrate file extensions. - public static void MigrateZipToPmp(Mod.Manager manager) + public static void MigrateZipToPmp( Mod.Manager manager ) { foreach( var mod in manager ) { @@ -40,16 +40,39 @@ public class ModBackup { File.Delete( zipName ); } - Penumbra.Log.Information( $"Migrated mod backup from {zipName} to {pmpName}." ); + + Penumbra.Log.Information( $"Migrated mod export from {zipName} to {pmpName}." ); } catch( Exception e ) { - Penumbra.Log.Warning( $"Could not migrate mod backup of {mod.ModPath} from .pmp to .zip:\n{e}" ); + Penumbra.Log.Warning( $"Could not migrate mod export of {mod.ModPath} from .pmp to .zip:\n{e}" ); } } } } + // Move and/or rename an exported mod. + // This object is unusable afterwards. + public void Move( string? newBasePath = null, string? newName = null ) + { + if( CreatingBackup || !Exists ) + { + return; + } + + try + { + newBasePath ??= Path.GetDirectoryName( Name ) ?? string.Empty; + newName = newName == null ? Path.GetFileName( Name ) : newName + ".pmp"; + var newPath = Path.Combine( newBasePath, newName ); + File.Move( Name, newPath ); + } + catch( Exception e ) + { + Penumbra.Log.Warning( $"Could not move mod export file {Name}:\n{e}" ); + } + } + // Create a backup zip without blocking the main thread. public async void CreateAsync() { @@ -71,11 +94,11 @@ public class ModBackup { Delete(); ZipFile.CreateFromDirectory( _mod.ModPath.FullName, Name, CompressionLevel.Optimal, false ); - Penumbra.Log.Debug( $"Created backup file {Name} from {_mod.ModPath.FullName}."); + Penumbra.Log.Debug( $"Created export file {Name} from {_mod.ModPath.FullName}." ); } catch( Exception e ) { - Penumbra.Log.Error( $"Could not backup mod {_mod.Name} to \"{Name}\":\n{e}" ); + Penumbra.Log.Error( $"Could not export mod {_mod.Name} to \"{Name}\":\n{e}" ); } } @@ -90,7 +113,7 @@ public class ModBackup try { File.Delete( Name ); - Penumbra.Log.Debug( $"Deleted backup file {Name}." ); + Penumbra.Log.Debug( $"Deleted export file {Name}." ); } catch( Exception e ) { @@ -111,12 +134,12 @@ public class ModBackup } ZipFile.ExtractToDirectory( Name, _mod.ModPath.FullName ); - Penumbra.Log.Debug( $"Extracted backup file {Name} to {_mod.ModPath.FullName}."); + Penumbra.Log.Debug( $"Extracted exported file {Name} to {_mod.ModPath.FullName}." ); Penumbra.ModManager.ReloadMod( _mod.Index ); } catch( Exception e ) { - Penumbra.Log.Error( $"Could not restore {_mod.Name} from backup \"{Name}\":\n{e}" ); + Penumbra.Log.Error( $"Could not restore {_mod.Name} from export \"{Name}\":\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index a169cecc..99ab7aaa 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -58,6 +58,8 @@ public partial class Mod return; } + new ModBackup( mod ).Move( null, dir.Name ); + dir.Refresh(); mod.ModPath = dir; if( !mod.Reload( false, out var metaChange ) ) @@ -109,7 +111,7 @@ public partial class Mod try { Directory.Delete( mod.ModPath.FullName, true ); - Penumbra.Log.Debug( $"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); + Penumbra.Log.Debug( $"Deleted directory {mod.ModPath.FullName} for {mod.Name}." ); } catch( Exception e ) { diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 7ad2140f..38c2fae7 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -1,3 +1,4 @@ +using ImGuizmoNET; using System; using System.IO; @@ -8,6 +9,11 @@ public sealed partial class Mod public sealed partial class Manager { public DirectoryInfo BasePath { get; private set; } = null!; + private DirectoryInfo? _exportDirectory; + + public DirectoryInfo ExportDirectory + => _exportDirectory ?? BasePath; + public bool Valid { get; private set; } public event Action? ModDiscoveryStarted; @@ -103,5 +109,49 @@ public sealed partial class Mod ModBackup.MigrateZipToPmp( this ); } } + + public void UpdateExportDirectory( string newDirectory ) + { + if( newDirectory.Length == 0 ) + { + if( _exportDirectory == null ) + { + return; + } + + _exportDirectory = null; + Penumbra.Config.ExportDirectory = string.Empty; + Penumbra.Config.Save(); + return; + } + + var dir = new DirectoryInfo( newDirectory ); + if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) ) + { + return; + } + + if( !dir.Exists ) + { + try + { + Directory.CreateDirectory( dir.FullName ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" ); + return; + } + } + + foreach( var mod in _mods ) + { + new ModBackup( mod ).Move( dir.FullName ); + } + + _exportDirectory = dir; + Penumbra.Config.ExportDirectory = dir.FullName; + Penumbra.Config.Save(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index dcd7ef82..e3113bcb 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -119,29 +119,29 @@ public partial class ConfigWindow { var backup = new ModBackup( _mod ); var tt = ModBackup.CreatingBackup - ? "Already creating a backup." + ? "Already exporting a mod." : backup.Exists - ? $"Overwrite current backup \"{backup.Name}\" with current mod." - : $"Create backup archive of current mod at \"{backup.Name}\"."; - if( ImGuiUtil.DrawDisabledButton( "Create Backup", buttonSize, tt, ModBackup.CreatingBackup ) ) + ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." + : $"Create exported archive of current mod at \"{backup.Name}\"."; + if( ImGuiUtil.DrawDisabledButton( "Export Mod", buttonSize, tt, ModBackup.CreatingBackup ) ) { backup.CreateAsync(); } ImGui.SameLine(); tt = backup.Exists - ? $"Delete existing backup file \"{backup.Name}\"." - : $"Backup file \"{backup.Name}\" does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Delete Backup", buttonSize, tt, !backup.Exists ) ) + ? $"Delete existing mod export \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; + if( ImGuiUtil.DrawDisabledButton( "Delete Export", buttonSize, tt, !backup.Exists ) ) { backup.Delete(); } tt = backup.Exists - ? $"Restore mod from backup file \"{backup.Name}\"." - : $"Backup file \"{backup.Name}\" does not exist."; + ? $"Restore mod from exported file \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Restore From Backup", buttonSize, tt, !backup.Exists ) ) + if( ImGuiUtil.DrawDisabledButton( "Restore From Export", buttonSize, tt, !backup.Exists ) ) { backup.Restore(); } diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index 1e0e0c50..cc46d52a 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -111,6 +111,7 @@ public partial class ConfigWindow DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); + DrawDefaultModExportPath(); ImGui.NewLine(); } @@ -228,6 +229,56 @@ public partial class ConfigWindow "Set the directory that gets opened when using the file picker to import mods for the first time." ); } + private string _tempExportDirectory = string.Empty; + + private void DrawDefaultModExportPath() + { + var tmp = Penumbra.Config.ExportDirectory; + var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); + if( ImGui.InputText( "##defaultModExport", ref tmp, 256 ) ) + { + _tempExportDirectory = tmp; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + Penumbra.ModManager.UpdateExportDirectory( _tempExportDirectory ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##export", _window._iconButtonSize, + "Select a directory via dialog.", false, true ) ) + { + if( _dialogOpen ) + { + _dialogManager.Reset(); + _dialogOpen = false; + } + else + { + var startDir = Penumbra.Config.ExportDirectory.Length > 0 && Directory.Exists( Penumbra.Config.ExportDirectory ) + ? Penumbra.Config.ExportDirectory + : Directory.Exists( Penumbra.Config.ModDirectory ) + ? Penumbra.Config.ModDirectory + : "."; + + _dialogManager.OpenFolderDialog( "Choose Default Export Directory", ( b, s ) => + { + Penumbra.ModManager.UpdateExportDirectory( b ? s : Penumbra.Config.ExportDirectory ); + _dialogOpen = false; + }, startDir ); + _dialogOpen = true; + } + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker( "Default Mod Export Directory", + "Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n" + + "Keep this empty to use the root directory." ); + } + private void DrawDefaultModAuthor() { var tmp = Penumbra.Config.DefaultModAuthor; From 8d597f9da55a5a2d07984bacbfecc5e1dcf8ae9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Oct 2022 12:31:40 +0200 Subject: [PATCH 0530/2451] Add buttons to export and import all colorset rows at once, changelog. --- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 73 ++++++++++++++++++- Penumbra/UI/ConfigWindow.Changelog.cs | 13 ++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 170b710e..29ca6897 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using Dalamud.Interface; using ImGuiNET; using OtterGui; @@ -260,11 +261,14 @@ public partial class ModEditWindow private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) { - if( !file.ColorSets.Any(c => c.HasRows ) ) + if( !file.ColorSets.Any( c => c.HasRows ) ) { return false; } + ColorSetCopyAllClipboardButton( file, 0 ); + ImGui.SameLine(); + var ret = ColorSetPasteAllClipboardButton( file, 0 ); using var table = ImRaii.Table( "##ColorSets", 10, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); if( !table ) @@ -293,7 +297,6 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableHeader( "Dye" ); - var ret = false; for( var j = 0; j < file.ColorSets.Length; ++j ) { using var _ = ImRaii.PushId( j ); @@ -307,6 +310,68 @@ public partial class ModEditWindow return ret; } + private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Export All Rows to Clipboard" ) ) + { + return; + } + + try + { + var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo( array ); + data2.TryCopyTo( array.AsSpan( data1.Length ) ); + var text = Convert.ToBase64String( array ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + { + if( !ImGui.Button( "Import All Rows from Clipboard" ) || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) + { + return false; + } + + ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; + fixed( void* ptr = data, output = &rows ) + { + Functions.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); + if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() + && file.ColorDyeSets.Length > colorSetIdx) + { + ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; + fixed( void* output2 = &dyeRows ) + { + Functions.MemCpyUnchecked( output2, (byte*) ptr + Marshal.SizeOf(), Marshal.SizeOf() ); + } + } + } + + return true; + } + catch + { + return false; + } + } + private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -407,7 +472,7 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.SpecularStrength) ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -465,7 +530,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat(ref tmpFloat, row.MaterialRepeat.X) ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 09dc4a08..3ffb6f48 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -25,6 +25,19 @@ public partial class ConfigWindow return ret; } + private static void Add5_10_0( Changelog log ) + => log.NextVersion( "Version 0.5.10.0" ) + .RegisterEntry( "Renamed backup functionality to export functionality." ) + .RegisterEntry( "A default export directory can now optionally be specified." ) + .RegisterEntry( "If left blank, exports will still be stored in your mod directory.", 1 ) + .RegisterEntry( "Existing exports corresponding to existing mods will be moved automatically if the export directory is changed.", + 1 ) + .RegisterEntry( "Added buttons to export and import all color set rows at once during material editing." ) + .RegisterEntry( "Fixed texture import being case sensitive on the extension." ) + .RegisterEntry( "Fixed special collection selector increasing in size on non-default UI styling." ) + .RegisterEntry( "Fixed color set rows not importing the dye values during material editing." ) + .RegisterEntry( "Other miscallaneous small fixes." ); + private static void Add5_9_0( Changelog log ) => log.NextVersion( "Version 0.5.9.0" ) .RegisterEntry( "Special Collections are now split between male and female." ) From b7a09bd3bc7502be0e131357907c28413977c9de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Oct 2022 13:20:38 +0200 Subject: [PATCH 0531/2451] 5.10 --- Penumbra/UI/ConfigWindow.Changelog.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 3ffb6f48..514814a6 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -21,6 +21,7 @@ public partial class ConfigWindow Add5_8_0( ret ); Add5_8_7( ret ); Add5_9_0( ret ); + Add5_10_0( ret ); return ret; } @@ -36,7 +37,7 @@ public partial class ConfigWindow .RegisterEntry( "Fixed texture import being case sensitive on the extension." ) .RegisterEntry( "Fixed special collection selector increasing in size on non-default UI styling." ) .RegisterEntry( "Fixed color set rows not importing the dye values during material editing." ) - .RegisterEntry( "Other miscallaneous small fixes." ); + .RegisterEntry( "Other miscellaneous small fixes." ); private static void Add5_9_0( Changelog log ) => log.NextVersion( "Version 0.5.9.0" ) From d6d4a0db4c1e543def4ce36e3604c0929e852749 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 5 Oct 2022 11:22:59 +0000 Subject: [PATCH 0532/2451] [CI] Updating repo.json for refs/tags/0.5.10.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index c2babd66..1dd7d668 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.9.0", - "TestingAssemblyVersion": "0.5.9.0", + "AssemblyVersion": "0.5.10.0", + "TestingAssemblyVersion": "0.5.10.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.9.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b3f048bfe614421feb55f9a7a8575c8fabecdbed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Oct 2022 13:00:46 +0200 Subject: [PATCH 0533/2451] Add Mixed Case options for byte strings. --- .../ByteString/ByteStringFunctions.Case.cs | 12 ++++++++ .../ByteString/Utf8String.Manipulation.cs | 28 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs index 7d9a94f9..aeda4bfd 100644 --- a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs @@ -9,6 +9,10 @@ public static unsafe partial class ByteStringFunctions .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) .ToArray(); + private static readonly byte[] AsciiUpperCaseBytes = Enumerable.Range( 0, 256 ) + .Select( i => ( byte )char.ToUpperInvariant( ( char )i ) ) + .ToArray(); + // Convert a byte to its ASCII-lowercase version. public static byte AsciiToLower( byte b ) => AsciiLowerCaseBytes[ b ]; @@ -17,6 +21,14 @@ public static unsafe partial class ByteStringFunctions public static bool AsciiIsLower( byte b ) => AsciiToLower( b ) == b; + // Convert a byte to its ASCII-uppercase version. + public static byte AsciiToUpper( byte b ) + => AsciiUpperCaseBytes[ b ]; + + // Check if a byte is ASCII-uppercase. + public static bool AsciiIsUpper( byte b ) + => AsciiToUpper( b ) == b; + // Check if a byte array of given length is ASCII-lowercase. public static bool IsAsciiLowerCase( byte* path, int length ) { diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs index 2a941020..c4332a1d 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs @@ -25,6 +25,32 @@ public sealed unsafe partial class Utf8String ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) : this; + // Convert the ascii portion of the string to mixed case (i.e. capitalize every first letter in a word) + // Clones the string. + public Utf8String AsciiToMixed() + { + var length = Length; + if( length == 0 ) + { + return Empty; + } + + var ret = Clone(); + var previousWhitespace = true; + var end = ret.Path + length; + for( var ptr = ret.Path; ptr < end; ++ptr ) + { + if( previousWhitespace ) + { + *ptr = ByteStringFunctions.AsciiToUpper( *ptr ); + } + + previousWhitespace = char.IsWhiteSpace( ( char )*ptr ); + } + + return ret; + } + // Convert the ascii portion of the string to lowercase. // Guaranteed to create an owned copy. public Utf8String AsciiToLowerClone() @@ -37,7 +63,7 @@ public sealed unsafe partial class Utf8String { var ret = new Utf8String(); ret._length = _length | OwnedFlag | NullTerminatedFlag; - ret._path = ByteStringFunctions.CopyString(Path, Length); + ret._path = ByteStringFunctions.CopyString( Path, Length ); ret._crc32 = Crc32; return ret; } From 918d5db6a64a6e648c1d2932bb88079a392163c8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Oct 2022 02:02:33 +0200 Subject: [PATCH 0534/2451] Use external library for API interface and IPC. --- .editorconfig | 2 +- .gitmodules | 4 + Penumbra.Api | 1 + .../Enums/ChangedItemExtensions.cs | 32 + Penumbra.GameData/Enums/ChangedItemType.cs | 40 - Penumbra.GameData/Enums/MouseButton.cs | 10 - Penumbra.GameData/Penumbra.GameData.csproj | 4 + Penumbra.sln | 6 + Penumbra/Api/IpcTester.cs | 1729 ++++++++++------- Penumbra/Api/PenumbraApi.cs | 58 +- Penumbra/Api/PenumbraIpc.cs | 878 --------- Penumbra/Api/PenumbraIpcProviders.cs | 307 +++ Penumbra/Api/RedrawController.cs | 2 +- Penumbra/Collections/ModCollection.Cache.cs | 5 +- Penumbra/Collections/ModCollection.Changes.cs | 12 +- .../Collections/ModCollection.Inheritance.cs | 1 + Penumbra/Import/TexToolsImporter.ModPack.cs | 9 +- Penumbra/Import/TexToolsStructs.cs | 5 +- Penumbra/Interop/ObjectReloader.cs | 1 + .../Resolver/PathResolver.DrawObjectState.cs | 4 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 7 +- Penumbra/Mods/Mod.Creation.cs | 7 +- Penumbra/Mods/Mod.Files.cs | 7 +- Penumbra/Mods/Mod.Meta.Migration.cs | 7 +- Penumbra/Mods/ModFileSystem.cs | 31 +- Penumbra/Mods/Subclasses/IModGroup.cs | 17 +- .../Subclasses/Mod.Files.MultiModGroup.cs | 11 +- .../Subclasses/Mod.Files.SingleModGroup.cs | 11 +- Penumbra/Mods/Subclasses/ModSettings.cs | 19 +- Penumbra/Penumbra.cs | 33 +- Penumbra/Penumbra.csproj | 1 + Penumbra/UI/Classes/ModFileSystemSelector.cs | 1 + Penumbra/UI/ConfigWindow.DebugTab.cs | 4 +- Penumbra/UI/ConfigWindow.Misc.cs | 1 + Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 23 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 5 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 1 + Penumbra/packages.lock.json | 21 +- tmp/.editorconfig | 85 + tmp/.gitignore | 3 + tmp/Delegates.cs | 16 + tmp/Enums/ChangedItemType.cs | 9 + tmp/Enums/GroupType.cs | 7 + tmp/Enums/ModSettingChange.cs | 12 + tmp/Enums/MouseButton.cs | 9 + tmp/Enums/PenumbraApiEc.cs | 20 + .../Enums/RedrawType.cs | 2 +- tmp/Helpers/ActionProvider.cs | 66 + tmp/Helpers/ActionSubscriber.cs | 54 + tmp/Helpers/EventProvider.cs | 376 ++++ tmp/Helpers/EventSubscriber.cs | 607 ++++++ tmp/Helpers/FuncProvider.cs | 186 ++ tmp/Helpers/FuncSubscriber.cs | 163 ++ {Penumbra/Api => tmp}/IPenumbraApi.cs | 150 +- tmp/IPenumbraApiBase.cs | 11 + tmp/Ipc/Collection.cs | 76 + tmp/Ipc/Configuration.cs | 42 + tmp/Ipc/GameState.cs | 63 + tmp/Ipc/Meta.cs | 30 + tmp/Ipc/ModSettings.cs | 116 ++ tmp/Ipc/Mods.cs | 81 + tmp/Ipc/PluginState.cs | 68 + tmp/Ipc/Redraw.cs | 65 + tmp/Ipc/Resolve.cs | 74 + tmp/Ipc/Temporary.cs | 83 + tmp/Ipc/Ui.cs | 55 + tmp/Penumbra.Api.csproj | 47 + tmp/Penumbra.Api.csproj.DotSettings | 2 + tmp/README.md | 4 + 69 files changed, 4026 insertions(+), 1873 deletions(-) create mode 160000 Penumbra.Api create mode 100644 Penumbra.GameData/Enums/ChangedItemExtensions.cs delete mode 100644 Penumbra.GameData/Enums/ChangedItemType.cs delete mode 100644 Penumbra.GameData/Enums/MouseButton.cs delete mode 100644 Penumbra/Api/PenumbraIpc.cs create mode 100644 Penumbra/Api/PenumbraIpcProviders.cs create mode 100644 tmp/.editorconfig create mode 100644 tmp/.gitignore create mode 100644 tmp/Delegates.cs create mode 100644 tmp/Enums/ChangedItemType.cs create mode 100644 tmp/Enums/GroupType.cs create mode 100644 tmp/Enums/ModSettingChange.cs create mode 100644 tmp/Enums/MouseButton.cs create mode 100644 tmp/Enums/PenumbraApiEc.cs rename {Penumbra.GameData => tmp}/Enums/RedrawType.cs (61%) create mode 100644 tmp/Helpers/ActionProvider.cs create mode 100644 tmp/Helpers/ActionSubscriber.cs create mode 100644 tmp/Helpers/EventProvider.cs create mode 100644 tmp/Helpers/EventSubscriber.cs create mode 100644 tmp/Helpers/FuncProvider.cs create mode 100644 tmp/Helpers/FuncSubscriber.cs rename {Penumbra/Api => tmp}/IPenumbraApi.cs (86%) create mode 100644 tmp/IPenumbraApiBase.cs create mode 100644 tmp/Ipc/Collection.cs create mode 100644 tmp/Ipc/Configuration.cs create mode 100644 tmp/Ipc/GameState.cs create mode 100644 tmp/Ipc/Meta.cs create mode 100644 tmp/Ipc/ModSettings.cs create mode 100644 tmp/Ipc/Mods.cs create mode 100644 tmp/Ipc/PluginState.cs create mode 100644 tmp/Ipc/Redraw.cs create mode 100644 tmp/Ipc/Resolve.cs create mode 100644 tmp/Ipc/Temporary.cs create mode 100644 tmp/Ipc/Ui.cs create mode 100644 tmp/Penumbra.Api.csproj create mode 100644 tmp/Penumbra.Api.csproj.DotSettings create mode 100644 tmp/README.md diff --git a/.editorconfig b/.editorconfig index e283b2a2..238bb1dc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -37,7 +37,7 @@ resharper_autodetect_indent_settings=true resharper_braces_redundant=true resharper_constructor_or_destructor_body=expression_body resharper_csharp_empty_block_style=together -resharper_csharp_max_line_length=144 +resharper_csharp_max_line_length=180 resharper_csharp_space_within_array_access_brackets=true resharper_enforce_line_ending_style=true resharper_int_align_assignments=true diff --git a/.gitmodules b/.gitmodules index df7b5848..b5eb77bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = OtterGui url = git@github.com:Ottermandias/OtterGui.git branch = main +[submodule "Penumbra.Api"] + path = Penumbra.Api + url = git@github.com:Ottermandias/Penumbra.Api.git + branch = main diff --git a/Penumbra.Api b/Penumbra.Api new file mode 160000 index 00000000..0064bb82 --- /dev/null +++ b/Penumbra.Api @@ -0,0 +1 @@ +Subproject commit 0064bb82be9729676e7bf3202ff1407283e6f088 diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs new file mode 100644 index 00000000..68674268 --- /dev/null +++ b/Penumbra.GameData/Enums/ChangedItemExtensions.cs @@ -0,0 +1,32 @@ +using System; +using Lumina.Excel.GeneratedSheets; +using Penumbra.Api.Enums; +using Action = Lumina.Excel.GeneratedSheets.Action; + +namespace Penumbra.GameData.Enums; + +public static class ChangedItemExtensions +{ + public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item ) + { + return item switch + { + null => ( ChangedItemType.None, 0 ), + Item i => ( ChangedItemType.Item, i.RowId ), + Action a => ( ChangedItemType.Action, a.RowId ), + _ => ( ChangedItemType.Customization, 0 ), + }; + } + + public static object? GetObject( this ChangedItemType type, uint id ) + { + return type switch + { + ChangedItemType.None => null, + ChangedItemType.Item => ObjectIdentification.DataManager?.GetExcelSheet< Item >()?.GetRow( id ), + ChangedItemType.Action => ObjectIdentification.DataManager?.GetExcelSheet< Action >()?.GetRow( id ), + ChangedItemType.Customization => null, + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/ChangedItemType.cs b/Penumbra.GameData/Enums/ChangedItemType.cs deleted file mode 100644 index fa33382d..00000000 --- a/Penumbra.GameData/Enums/ChangedItemType.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Lumina.Excel.GeneratedSheets; -using Action = Lumina.Excel.GeneratedSheets.Action; - -namespace Penumbra.GameData.Enums -{ - public enum ChangedItemType - { - None, - Item, - Action, - Customization, - } - - public static class ChangedItemExtensions - { - public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item ) - { - return item switch - { - null => ( ChangedItemType.None, 0 ), - Item i => ( ChangedItemType.Item, i.RowId ), - Action a => ( ChangedItemType.Action, a.RowId ), - _ => ( ChangedItemType.Customization, 0 ), - }; - } - - public static object? GetObject( this ChangedItemType type, uint id ) - { - return type switch - { - ChangedItemType.None => null, - ChangedItemType.Item => ObjectIdentification.DataManager?.GetExcelSheet< Item >()?.GetRow( id ), - ChangedItemType.Action => ObjectIdentification.DataManager?.GetExcelSheet< Action >()?.GetRow( id ), - ChangedItemType.Customization => null, - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ) - }; - } - } -} diff --git a/Penumbra.GameData/Enums/MouseButton.cs b/Penumbra.GameData/Enums/MouseButton.cs deleted file mode 100644 index 99948d7c..00000000 --- a/Penumbra.GameData/Enums/MouseButton.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Penumbra.GameData.Enums -{ - public enum MouseButton - { - None, - Left, - Right, - Middle, - } -} \ No newline at end of file diff --git a/Penumbra.GameData/Penumbra.GameData.csproj b/Penumbra.GameData/Penumbra.GameData.csproj index ad466524..cb51d6dc 100644 --- a/Penumbra.GameData/Penumbra.GameData.csproj +++ b/Penumbra.GameData/Penumbra.GameData.csproj @@ -34,6 +34,10 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\ + + + + $(DalamudLibPath)Dalamud.dll diff --git a/Penumbra.sln b/Penumbra.sln index b43c7565..33e5a03d 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{87750518-1A20-40B4-9FC1-22F906EFB290}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 1fd29dec..486eb826 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -1,76 +1,94 @@ -using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; using Penumbra.Mods; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; -using System.Reflection; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; namespace Penumbra.Api; public class IpcTester : IDisposable { - private readonly PenumbraIpc _ipc; - private readonly DalamudPluginInterface _pi; + private readonly PenumbraIpcProviders _ipcProviders; + private bool _subscribed = true; - private readonly ICallGateSubscriber< object? > _initialized; - private readonly ICallGateSubscriber< object? > _disposed; - private readonly ICallGateSubscriber< string, object? > _preSettingsDraw; - private readonly ICallGateSubscriber< string, object? > _postSettingsDraw; - private readonly ICallGateSubscriber< string, bool, object? > _modDirectoryChanged; - private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn; - private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged; - private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreating; - private readonly ICallGateSubscriber< IntPtr, string, IntPtr, object? > _characterBaseCreated; - private readonly ICallGateSubscriber< IntPtr, string, string, object? > _gameObjectResourcePathResolved; + private readonly PluginState _pluginState; + private readonly Configuration _configuration; + private readonly Ui _ui; + private readonly Redrawing _redrawing; + private readonly GameState _gameState; + private readonly Resolve _resolve; + private readonly Collections _collections; + private readonly Meta _meta; + private readonly Mods _mods; + private readonly ModSettings _modSettings; + private readonly Temporary _temporary; - private readonly List< DateTimeOffset > _initializedList = new(); - private readonly List< DateTimeOffset > _disposedList = new(); - private bool _subscribed = false; - - - public IpcTester( DalamudPluginInterface pi, PenumbraIpc ipc ) + public IpcTester( DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders ) { - _ipc = ipc; - _pi = pi; - _initialized = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderInitialized ); - _disposed = _pi.GetIpcSubscriber< object? >( PenumbraIpc.LabelProviderDisposed ); - _redrawn = _pi.GetIpcSubscriber< IntPtr, int, object? >( PenumbraIpc.LabelProviderGameObjectRedrawn ); - _preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw ); - _postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw ); - _settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged ); - _modDirectoryChanged = _pi.GetIpcSubscriber< string, bool, object? >( PenumbraIpc.LabelProviderModDirectoryChanged ); - _characterBaseCreating = - _pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase ); - _characterBaseCreated = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, object? >( PenumbraIpc.LabelProviderCreatedCharacterBase ); - _gameObjectResourcePathResolved = - _pi.GetIpcSubscriber< IntPtr, string, string, object? >( PenumbraIpc.LabelProviderGameObjectResourcePathResolved ); + _ipcProviders = ipcProviders; + _pluginState = new PluginState( pi ); + _configuration = new Configuration( pi ); + _ui = new Ui( pi ); + _redrawing = new Redrawing( pi ); + _gameState = new GameState( pi ); + _resolve = new Resolve( pi ); + _collections = new Collections( pi ); + _meta = new Meta( pi ); + _mods = new Mods( pi ); + _modSettings = new ModSettings( pi ); + _temporary = new Temporary( pi ); + UnsubscribeEvents(); + } + + public void Draw() + { + try + { + SubscribeEvents(); + ImGui.TextUnformatted( $"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}" ); + _pluginState.Draw(); + _configuration.Draw(); + _ui.Draw(); + _redrawing.Draw(); + _gameState.Draw(); + _resolve.Draw(); + _collections.Draw(); + _meta.Draw(); + _mods.Draw(); + _modSettings.Draw(); + _temporary.Draw(); + _temporary.DrawCollections(); + _temporary.DrawMods(); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error during IPC Tests:\n{e}" ); + } } private void SubscribeEvents() { if( !_subscribed ) { - _initialized.Subscribe( AddInitialized ); - _disposed.Subscribe( AddDisposed ); - _redrawn.Subscribe( SetLastRedrawn ); - _preSettingsDraw.Subscribe( UpdateLastDrawnMod ); - _postSettingsDraw.Subscribe( UpdateLastDrawnMod ); - _settingChanged.Subscribe( UpdateLastModSetting ); - _characterBaseCreating.Subscribe( UpdateLastCreated ); - _characterBaseCreated.Subscribe( UpdateLastCreated2 ); - _modDirectoryChanged.Subscribe( UpdateModDirectoryChanged ); - _gameObjectResourcePathResolved.Subscribe( UpdateGameObjectResourcePath ); + _pluginState.Initialized.Enable(); + _pluginState.Disposed.Enable(); + _redrawing.Redrawn.Enable(); + _ui.PreSettingsDraw.Enable(); + _ui.PostSettingsDraw.Enable(); + _modSettings.SettingChanged.Enable(); + _gameState.CharacterBaseCreating.Enable(); + _gameState.CharacterBaseCreated.Enable(); + _configuration.ModDirectoryChanged.Enable(); + _gameState.GameObjectResourcePathResolved.Enable(); _subscribed = true; } } @@ -79,75 +97,37 @@ public class IpcTester : IDisposable { if( _subscribed ) { - _initialized.Unsubscribe( AddInitialized ); - _disposed.Unsubscribe( AddDisposed ); - _redrawn.Subscribe( SetLastRedrawn ); - _tooltip?.Unsubscribe( AddedTooltip ); - _click?.Unsubscribe( AddedClick ); - _preSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); - _postSettingsDraw.Unsubscribe( UpdateLastDrawnMod ); - _settingChanged.Unsubscribe( UpdateLastModSetting ); - _characterBaseCreating.Unsubscribe( UpdateLastCreated ); - _characterBaseCreated.Unsubscribe( UpdateLastCreated2 ); - _modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged ); - _gameObjectResourcePathResolved.Unsubscribe( UpdateGameObjectResourcePath ); + _pluginState.Initialized.Disable(); + _pluginState.Disposed.Disable(); + _redrawing.Redrawn.Disable(); + _ui.PreSettingsDraw.Disable(); + _ui.PostSettingsDraw.Disable(); + _ui.Tooltip.Disable(); + _ui.Click.Disable(); + _modSettings.SettingChanged.Disable(); + _gameState.CharacterBaseCreating.Disable(); + _gameState.CharacterBaseCreated.Disable(); + _configuration.ModDirectoryChanged.Disable(); + _gameState.GameObjectResourcePathResolved.Disable(); _subscribed = false; } } public void Dispose() - => UnsubscribeEvents(); - - private void AddInitialized() - => _initializedList.Add( DateTimeOffset.UtcNow ); - - private void AddDisposed() - => _disposedList.Add( DateTimeOffset.UtcNow ); - - public void Draw() { - try - { - SubscribeEvents(); - DrawAvailable(); - DrawGeneral(); - DrawResolve(); - DrawRedraw(); - DrawChangedItems(); - DrawData(); - DrawSetting(); - DrawTemp(); - DrawTempCollections(); - DrawTempMods(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error during IPC Tests:\n{e}" ); - } - } - - private void DrawAvailable() - { - using var _ = ImRaii.TreeNode( "Availability" ); - if( !_ ) - { - return; - } - - ImGui.TextUnformatted( $"API Version: {_ipc.Api.ApiVersion.Breaking}.{_ipc.Api.ApiVersion.Feature:D4}" ); - ImGui.TextUnformatted( "Available subscriptions:" ); - using var indent = ImRaii.PushIndent(); - - var dict = _ipc.GetType().GetFields( BindingFlags.Static | BindingFlags.Public ).Where( f => f.IsLiteral ) - .ToDictionary( f => f.Name, f => f.GetValue( _ipc ) as string ); - foreach( var provider in _ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) - { - var value = provider.GetValue( _ipc ); - if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) ) - { - ImGui.TextUnformatted( label ); - } - } + _pluginState.Initialized.Dispose(); + _pluginState.Disposed.Dispose(); + _redrawing.Redrawn.Dispose(); + _ui.PreSettingsDraw.Dispose(); + _ui.PostSettingsDraw.Dispose(); + _ui.Tooltip.Dispose(); + _ui.Click.Dispose(); + _modSettings.SettingChanged.Dispose(); + _gameState.CharacterBaseCreating.Dispose(); + _gameState.CharacterBaseCreated.Dispose(); + _configuration.ModDirectoryChanged.Dispose(); + _gameState.GameObjectResourcePathResolved.Dispose(); + _subscribed = false; } private static void DrawIntro( string label, string info ) @@ -159,605 +139,849 @@ public class IpcTester : IDisposable ImGui.TableNextColumn(); } - private string _currentConfiguration = string.Empty; - private string _lastDrawnMod = string.Empty; - private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; - private void UpdateLastDrawnMod( string name ) - => ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now ); - - private string _lastModDirectory = string.Empty; - private bool _lastModDirectoryValid = false; - private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; - - private void UpdateModDirectoryChanged( string path, bool valid ) - => ( _lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime ) = ( path, valid, DateTimeOffset.Now ); - - private void DrawGeneral() + private class PluginState { - using var _ = ImRaii.TreeNode( "General IPC" ); - if( !_ ) + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber Initialized; + public readonly EventSubscriber Disposed; + + private readonly List< DateTimeOffset > _initializedList = new(); + private readonly List< DateTimeOffset > _disposedList = new(); + + public PluginState( DalamudPluginInterface pi ) { - return; + _pi = pi; + Initialized = Ipc.Initialized.Subscriber( pi, AddInitialized ); + Disposed = Ipc.Disposed.Subscriber( pi, AddDisposed ); } - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - - void DrawList( string label, string text, List< DateTimeOffset > list ) + public void Draw() { - DrawIntro( label, text ); - if( list.Count == 0 ) + using var _ = ImRaii.TreeNode( "Plugin State" ); + if( !_ ) { - ImGui.TextUnformatted( "Never" ); + return; } - else + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) { - ImGui.TextUnformatted( list[ ^1 ].LocalDateTime.ToString( CultureInfo.CurrentCulture ) ); - if( list.Count > 1 && ImGui.IsItemHovered() ) + return; + } + + void DrawList( string label, string text, List< DateTimeOffset > list ) + { + DrawIntro( label, text ); + if( list.Count == 0 ) { - ImGui.SetTooltip( string.Join( "\n", - list.SkipLast( 1 ).Select( t => t.LocalDateTime.ToString( CultureInfo.CurrentCulture ) ) ) ); + ImGui.TextUnformatted( "Never" ); + } + else + { + ImGui.TextUnformatted( list[ ^1 ].LocalDateTime.ToString( CultureInfo.CurrentCulture ) ); + if( list.Count > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", + list.SkipLast( 1 ).Select( t => t.LocalDateTime.ToString( CultureInfo.CurrentCulture ) ) ) ); + } + } + } + + DrawList( Ipc.Initialized.Label, "Last Initialized", _initializedList ); + DrawList( Ipc.Disposed.Label, "Last Disposed", _disposedList ); + DrawIntro( Ipc.ApiVersions.Label, "Current Version" ); + var (breaking, features) = Ipc.ApiVersions.Subscriber( _pi ).Invoke(); + ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); + } + + private void AddInitialized() + => _initializedList.Add( DateTimeOffset.UtcNow ); + + private void AddDisposed() + => _disposedList.Add( DateTimeOffset.UtcNow ); + } + + private class Configuration + { + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber< string, bool > ModDirectoryChanged; + + private string _currentConfiguration = string.Empty; + private string _lastModDirectory = string.Empty; + private bool _lastModDirectoryValid; + private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; + + public Configuration( DalamudPluginInterface pi ) + { + _pi = pi; + ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber( pi, UpdateModDirectoryChanged ); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode( "Configuration" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.GetModDirectory.Label, "Current Mod Directory" ); + ImGui.TextUnformatted( Ipc.GetModDirectory.Subscriber( _pi ).Invoke() ); + DrawIntro( Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change" ); + ImGui.TextUnformatted( _lastModDirectoryTime > DateTimeOffset.MinValue + ? $"{_lastModDirectory} ({( _lastModDirectoryValid ? "Valid" : "Invalid" )}) at {_lastModDirectoryTime}" + : "None" ); + DrawIntro( Ipc.GetConfiguration.Label, "Configuration" ); + if( ImGui.Button( "Get" ) ) + { + _currentConfiguration = Ipc.GetConfiguration.Subscriber( _pi ).Invoke(); + ImGui.OpenPopup( "Config Popup" ); + } + + DrawConfigPopup(); + } + + private void DrawConfigPopup() + { + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var popup = ImRaii.Popup( "Config Popup" ); + if( popup ) + { + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGuiUtil.TextWrapped( _currentConfiguration ); + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) + { + ImGui.CloseCurrentPopup(); } } } - DrawList( PenumbraIpc.LabelProviderInitialized, "Last Initialized", _initializedList ); - DrawList( PenumbraIpc.LabelProviderDisposed, "Last Disposed", _disposedList ); - DrawIntro( PenumbraIpc.LabelProviderPostSettingsDraw, "Last Drawn Mod" ); - ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); - DrawIntro( PenumbraIpc.LabelProviderApiVersions, "Current Version" ); - var (breaking, features) = _pi.GetIpcSubscriber< (int, int) >( PenumbraIpc.LabelProviderApiVersions ).InvokeFunc(); - ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); - DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() ); - DrawIntro( PenumbraIpc.LabelProviderModDirectoryChanged, "Last Mod Directory Change" ); - ImGui.TextUnformatted( _lastModDirectoryTime > DateTimeOffset.MinValue - ? $"{_lastModDirectory} ({( _lastModDirectoryValid ? "Valid" : "Invalid" )}) at {_lastModDirectoryTime}" - : "None" ); - DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" ); - if( ImGui.Button( "Get" ) ) + private void UpdateModDirectoryChanged( string path, bool valid ) + => ( _lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime ) = ( path, valid, DateTimeOffset.Now ); + } + + private class Ui + { + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber< string > PreSettingsDraw; + public readonly EventSubscriber< string > PostSettingsDraw; + public readonly EventSubscriber< ChangedItemType, uint > Tooltip; + public readonly EventSubscriber< MouseButton, ChangedItemType, uint > Click; + + private string _lastDrawnMod = string.Empty; + private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; + private bool _subscribedToTooltip = false; + private bool _subscribedToClick = false; + private string _lastClicked = string.Empty; + private string _lastHovered = string.Empty; + + public Ui( DalamudPluginInterface pi ) { - _currentConfiguration = _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetConfiguration ).InvokeFunc(); - ImGui.OpenPopup( "Config Popup" ); + _pi = pi; + PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber( pi, UpdateLastDrawnMod ); + PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber( pi, UpdateLastDrawnMod ); + Tooltip = Ipc.ChangedItemTooltip.Subscriber( pi, AddedTooltip ); + Click = Ipc.ChangedItemClick.Subscriber( pi, AddedClick ); } - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var popup = ImRaii.Popup( "Config Popup" ); - if( popup ) + public void Draw() { - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var _ = ImRaii.TreeNode( "UI" ); + if( !_ ) { - ImGuiUtil.TextWrapped( _currentConfiguration ); + return; } - if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) { - ImGui.CloseCurrentPopup(); + return; } - } - } - private string _currentResolvePath = string.Empty; - private string _currentResolveCharacter = string.Empty; - private string _currentDrawObjectString = string.Empty; - private string _currentReversePath = string.Empty; - private IntPtr _currentDrawObject = IntPtr.Zero; - private int _currentCutsceneActor = 0; - private string _lastCreatedGameObjectName = string.Empty; - private IntPtr _lastCreatedDrawObject = IntPtr.Zero; - private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; - private string _lastResolvedGamePath = string.Empty; - private string _lastResolvedFullPath = string.Empty; - private string _lastResolvedObject = string.Empty; - private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; + DrawIntro( Ipc.PostSettingsDraw.Label, "Last Drawn Mod" ); + ImGui.TextUnformatted( _lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None" ); - private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) - { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = IntPtr.Zero; - } - - private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) - { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = drawObject; - } - - private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) - { - var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; - _lastResolvedObject = obj != null ? new Utf8String( obj->GetName() ).ToString() : "Unknown"; - _lastResolvedGamePath = gamePath; - _lastResolvedFullPath = fullPath; - _lastResolvedGamePathTime = DateTimeOffset.Now; - } - - private void DrawResolve() - { - using var _ = ImRaii.TreeNode( "Resolve IPC" ); - if( !_ ) - { - return; - } - - ImGui.InputTextWithHint( "##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength ); - ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 ); - ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, - Utf8GamePath.MaxGamePathLength ); - if( ImGui.InputTextWithHint( "##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, - ImGuiInputTextFlags.CharsHexadecimal ) ) - { - _currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp ) - ? tmp - : IntPtr.Zero; - } - - ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 ); - - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - DrawIntro( PenumbraIpc.LabelProviderResolveDefault, "Default Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveDefault ) - .InvokeFunc( _currentResolvePath ) ); - } - - DrawIntro( PenumbraIpc.LabelProviderResolveInterface, "Interface Collection Resolve" ); - if( _currentResolvePath.Length != 0 ) - { - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderResolveInterface ) - .InvokeFunc( _currentResolvePath ) ); - } - - DrawIntro( PenumbraIpc.LabelProviderResolveCharacter, "Character Collection Resolve" ); - if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) - { - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string, string, string >( PenumbraIpc.LabelProviderResolveCharacter ) - .InvokeFunc( _currentResolvePath, _currentResolveCharacter ) ); - } - - DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Draw Object Info" ); - if( _currentDrawObject == IntPtr.Zero ) - { - ImGui.TextUnformatted( "Invalid" ); - } - else - { - var (ptr, collection) = _pi.GetIpcSubscriber< IntPtr, (IntPtr, string) >( PenumbraIpc.LabelProviderGetDrawObjectInfo ) - .InvokeFunc( _currentDrawObject ); - ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); - } - - DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Cutscene Parent" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< int, int >( PenumbraIpc.LabelProviderGetCutsceneParentIndex ) - .InvokeFunc( _currentCutsceneActor ).ToString() ); - - DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" ); - if( _currentReversePath.Length > 0 ) - { - var list = _pi.GetIpcSubscriber< string, string, string[] >( PenumbraIpc.LabelProviderReverseResolvePath ) - .InvokeFunc( _currentReversePath, _currentResolveCharacter ); - if( list.Length > 0 ) + DrawIntro( Ipc.ChangedItemTooltip.Label, "Add Tooltip" ); + if( ImGui.Checkbox( "##tooltip", ref _subscribedToTooltip ) ) { - ImGui.TextUnformatted( list[ 0 ] ); - if( list.Length > 1 && ImGui.IsItemHovered() ) + if( _subscribedToTooltip ) { - ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + Tooltip.Enable(); + } + else + { + Tooltip.Disable(); } } - } - DrawIntro( PenumbraIpc.LabelProviderReverseResolvePlayerPath, "Reversed Game Paths (Player)" ); - if( _currentReversePath.Length > 0 ) - { - var list = _pi.GetIpcSubscriber< string, string[] >( PenumbraIpc.LabelProviderReverseResolvePlayerPath ) - .InvokeFunc( _currentReversePath ); - if( list.Length > 0 ) + ImGui.SameLine(); + ImGui.TextUnformatted( _lastHovered ); + + DrawIntro( Ipc.ChangedItemClick.Label, "Subscribe Click" ); + if( ImGui.Checkbox( "##click", ref _subscribedToClick ) ) { - ImGui.TextUnformatted( list[ 0 ] ); - if( list.Length > 1 && ImGui.IsItemHovered() ) + if( _subscribedToClick ) { - ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + Click.Enable(); + } + else + { + Click.Disable(); } } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastClicked ); } - DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" ); - if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) + private void UpdateLastDrawnMod( string name ) + => ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now ); + + private void AddedTooltip( ChangedItemType type, uint id ) { - ImGui.TextUnformatted( _lastCreatedDrawObject != IntPtr.Zero - ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" - : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); + _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; + ImGui.TextUnformatted( "IPC Test Successful" ); } - DrawIntro( PenumbraIpc.LabelProviderGameObjectResourcePathResolved, "Last GamePath resolved" ); - if( _lastResolvedGamePathTime < DateTimeOffset.Now ) + private void AddedClick( MouseButton button, ChangedItemType type, uint id ) { - ImGui.TextUnformatted( - $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}" ); + _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; } } - private string _redrawName = string.Empty; - private int _redrawIndex = 0; - private string _lastRedrawnString = "None"; - - private void SetLastRedrawn( IntPtr address, int index ) + private class Redrawing { - if( index < 0 || index > Dalamud.Objects.Length || address == IntPtr.Zero || Dalamud.Objects[ index ]?.Address != address ) + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber< IntPtr, int > Redrawn; + + private string _redrawName = string.Empty; + private int _redrawIndex = 0; + private string _lastRedrawnString = "None"; + + public Redrawing( DalamudPluginInterface pi ) { - _lastRedrawnString = "Invalid"; + _pi = pi; + Redrawn = Ipc.GameObjectRedrawn.Subscriber( pi, SetLastRedrawn ); } - _lastRedrawnString = $"{Dalamud.Objects[ index ]!.Name} (0x{address:X}, {index})"; - } - - private void DrawRedraw() - { - using var _ = ImRaii.TreeNode( "Redraw IPC" ); - if( !_ ) + public void Draw() { - return; - } - - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - DrawIntro( PenumbraIpc.LabelProviderRedrawName, "Redraw by Name" ); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); - ImGui.SameLine(); - if( ImGui.Button( "Redraw##Name" ) ) - { - _pi.GetIpcSubscriber< string, int, object? >( PenumbraIpc.LabelProviderRedrawName ) - .InvokeAction( _redrawName, ( int )RedrawType.Redraw ); - } - - DrawIntro( PenumbraIpc.LabelProviderRedrawObject, "Redraw Player Character" ); - if( ImGui.Button( "Redraw##pc" ) && Dalamud.ClientState.LocalPlayer != null ) - { - _pi.GetIpcSubscriber< GameObject, int, object? >( PenumbraIpc.LabelProviderRedrawObject ) - .InvokeAction( Dalamud.ClientState.LocalPlayer, ( int )RedrawType.Redraw ); - } - - DrawIntro( PenumbraIpc.LabelProviderRedrawIndex, "Redraw by Index" ); - var tmp = _redrawIndex; - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, Dalamud.Objects.Length ) ) - { - _redrawIndex = Math.Clamp( tmp, 0, Dalamud.Objects.Length ); - } - - ImGui.SameLine(); - if( ImGui.Button( "Redraw##Index" ) ) - { - _pi.GetIpcSubscriber< int, int, object? >( PenumbraIpc.LabelProviderRedrawIndex ) - .InvokeAction( _redrawIndex, ( int )RedrawType.Redraw ); - } - - DrawIntro( PenumbraIpc.LabelProviderRedrawAll, "Redraw All" ); - if( ImGui.Button( "Redraw##All" ) ) - { - _pi.GetIpcSubscriber< int, object? >( PenumbraIpc.LabelProviderRedrawAll ).InvokeAction( ( int )RedrawType.Redraw ); - } - - DrawIntro( PenumbraIpc.LabelProviderGameObjectRedrawn, "Last Redrawn Object:" ); - ImGui.TextUnformatted( _lastRedrawnString ); - } - - private bool _subscribedToTooltip = false; - private bool _subscribedToClick = false; - private string _changedItemCollection = string.Empty; - private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >(); - private string _lastClicked = string.Empty; - private string _lastHovered = string.Empty; - private ICallGateSubscriber< ChangedItemType, uint, object? >? _tooltip; - private ICallGateSubscriber< MouseButton, ChangedItemType, uint, object? >? _click; - - private void DrawChangedItems() - { - using var _ = ImRaii.TreeNode( "Changed Item IPC" ); - if( !_ ) - { - return; - } - - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - DrawIntro( PenumbraIpc.LabelProviderChangedItemTooltip, "Add Tooltip" ); - if( ImGui.Checkbox( "##tooltip", ref _subscribedToTooltip ) ) - { - _tooltip = _pi.GetIpcSubscriber< ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemTooltip ); - if( _subscribedToTooltip ) + using var _ = ImRaii.TreeNode( "Redrawing" ); + if( !_ ) { - _tooltip.Subscribe( AddedTooltip ); + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.RedrawObjectByName.Label, "Redraw by Name" ); + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); + ImGui.SameLine(); + if( ImGui.Button( "Redraw##Name" ) ) + { + Ipc.RedrawObjectByName.Subscriber( _pi ).Invoke( _redrawName, RedrawType.Redraw ); + } + + DrawIntro( Ipc.RedrawObject.Label, "Redraw Player Character" ); + if( ImGui.Button( "Redraw##pc" ) && Dalamud.ClientState.LocalPlayer != null ) + { + Ipc.RedrawObject.Subscriber( _pi ).Invoke( Dalamud.ClientState.LocalPlayer, RedrawType.Redraw ); + } + + DrawIntro( Ipc.RedrawObjectByIndex.Label, "Redraw by Index" ); + var tmp = _redrawIndex; + ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, Dalamud.Objects.Length ) ) + { + _redrawIndex = Math.Clamp( tmp, 0, Dalamud.Objects.Length ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Redraw##Index" ) ) + { + Ipc.RedrawObjectByIndex.Subscriber( _pi ).Invoke( _redrawIndex, RedrawType.Redraw ); + } + + DrawIntro( Ipc.RedrawAll.Label, "Redraw All" ); + if( ImGui.Button( "Redraw##All" ) ) + { + Ipc.RedrawAll.Subscriber( _pi ).Invoke( RedrawType.Redraw ); + } + + DrawIntro( Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:" ); + ImGui.TextUnformatted( _lastRedrawnString ); + } + + private void SetLastRedrawn( IntPtr address, int index ) + { + if( index < 0 || index > Dalamud.Objects.Length || address == IntPtr.Zero || Dalamud.Objects[ index ]?.Address != address ) + { + _lastRedrawnString = "Invalid"; + } + + _lastRedrawnString = $"{Dalamud.Objects[ index ]!.Name} (0x{address:X}, {index})"; + } + } + + private class GameState + { + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr > CharacterBaseCreating; + public readonly EventSubscriber< IntPtr, string, IntPtr > CharacterBaseCreated; + public readonly EventSubscriber< IntPtr, string, string > GameObjectResourcePathResolved; + + + private string _lastCreatedGameObjectName = string.Empty; + private IntPtr _lastCreatedDrawObject = IntPtr.Zero; + private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; + private string _lastResolvedGamePath = string.Empty; + private string _lastResolvedFullPath = string.Empty; + private string _lastResolvedObject = string.Empty; + private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; + private string _currentDrawObjectString = string.Empty; + private IntPtr _currentDrawObject = IntPtr.Zero; + private int _currentCutsceneActor = 0; + + public GameState( DalamudPluginInterface pi ) + { + _pi = pi; + CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber( pi, UpdateLastCreated ); + CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber( pi, UpdateLastCreated2 ); + GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber( pi, UpdateGameObjectResourcePath ); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode( "Game State" ); + if( !_ ) + { + return; + } + + if( ImGui.InputTextWithHint( "##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, + ImGuiInputTextFlags.CharsHexadecimal ) ) + { + _currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp ) + ? tmp + : IntPtr.Zero; + } + + ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.GetDrawObjectInfo.Label, "Draw Object Info" ); + if( _currentDrawObject == IntPtr.Zero ) + { + ImGui.TextUnformatted( "Invalid" ); } else { - _tooltip.Unsubscribe( AddedTooltip ); + var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber( _pi ).Invoke( _currentDrawObject ); + ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); } - } - ImGui.SameLine(); - ImGui.TextUnformatted( _lastHovered ); + DrawIntro( Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent" ); + ImGui.TextUnformatted( Ipc.GetCutsceneParentIndex.Subscriber( _pi ).Invoke( _currentCutsceneActor ).ToString() ); - DrawIntro( PenumbraIpc.LabelProviderChangedItemClick, "Subscribe Click" ); - if( ImGui.Checkbox( "##click", ref _subscribedToClick ) ) - { - _click = _pi.GetIpcSubscriber< MouseButton, ChangedItemType, uint, object? >( PenumbraIpc.LabelProviderChangedItemClick ); - if( _subscribedToClick ) + DrawIntro( Ipc.CreatingCharacterBase.Label, "Last Drawobject created" ); + if( _lastCreatedGameObjectTime < DateTimeOffset.Now ) { - _click.Subscribe( AddedClick ); + ImGui.TextUnformatted( _lastCreatedDrawObject != IntPtr.Zero + ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" + : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" ); } - else + + DrawIntro( Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved" ); + if( _lastResolvedGamePathTime < DateTimeOffset.Now ) { - _click.Unsubscribe( AddedClick ); + ImGui.TextUnformatted( + $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}" ); } } - ImGui.SameLine(); - ImGui.TextUnformatted( _lastClicked ); - - DrawIntro( PenumbraIpc.LabelProviderGetChangedItems, "Changed Item List" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); - ImGui.SameLine(); - if( ImGui.Button( "Get" ) ) + private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 ) { - _changedItems = _pi.GetIpcSubscriber< string, IReadOnlyDictionary< string, object? > >( PenumbraIpc.LabelProviderGetChangedItems ) - .InvokeFunc( _changedItemCollection ); - ImGui.OpenPopup( "Changed Item List" ); + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = IntPtr.Zero; } - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var p = ImRaii.Popup( "Changed Item List" ); - if( p ) + private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject ) { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString(); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = drawObject; + } + + private unsafe void UpdateGameObjectResourcePath( IntPtr gameObject, string gamePath, string fullPath ) + { + var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject; + _lastResolvedObject = obj != null ? new Utf8String( obj->GetName() ).ToString() : "Unknown"; + _lastResolvedGamePath = gamePath; + _lastResolvedFullPath = fullPath; + _lastResolvedGamePathTime = DateTimeOffset.Now; + } + } + + private class Resolve + { + private readonly DalamudPluginInterface _pi; + + private string _currentResolvePath = string.Empty; + private string _currentResolveCharacter = string.Empty; + private string _currentReversePath = string.Empty; + + public Resolve( DalamudPluginInterface pi ) + => _pi = pi; + + public void Draw() + { + using var _ = ImRaii.TreeNode( "Resolving" ); + if( !_ ) + { + return; + } + + ImGui.InputTextWithHint( "##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength ); + ImGui.InputTextWithHint( "##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32 ); + ImGui.InputTextWithHint( "##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, + Utf8GamePath.MaxGamePathLength ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.ResolveDefaultPath.Label, "Default Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( Ipc.ResolveDefaultPath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); + } + + DrawIntro( Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( Ipc.ResolveInterfacePath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); + } + + DrawIntro( Ipc.ResolvePlayerPath.Label, "Player Collection Resolve" ); + if( _currentResolvePath.Length != 0 ) + { + ImGui.TextUnformatted( Ipc.ResolvePlayerPath.Subscriber( _pi ).Invoke( _currentResolvePath ) ); + } + + DrawIntro( Ipc.ResolveCharacterPath.Label, "Character Collection Resolve" ); + if( _currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0 ) + { + ImGui.TextUnformatted( Ipc.ResolveCharacterPath.Subscriber( _pi ).Invoke( _currentResolvePath, _currentResolveCharacter ) ); + } + + DrawIntro( Ipc.ReverseResolvePath.Label, "Reversed Game Paths" ); + if( _currentReversePath.Length > 0 ) + { + var list = Ipc.ReverseResolvePath.Subscriber( _pi ).Invoke( _currentReversePath, _currentResolveCharacter ); + if( list.Length > 0 ) + { + ImGui.TextUnformatted( list[ 0 ] ); + if( list.Length > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + } + } + } + + DrawIntro( Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)" ); + if( _currentReversePath.Length > 0 ) + { + var list = Ipc.ReverseResolvePlayerPath.Subscriber( _pi ).Invoke( _currentReversePath ); + if( list.Length > 0 ) + { + ImGui.TextUnformatted( list[ 0 ] ); + if( list.Length > 1 && ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( "\n", list.Skip( 1 ) ) ); + } + } + } + } + } + + private class Collections + { + private readonly DalamudPluginInterface _pi; + + private string _characterCollectionName = string.Empty; + private IList< string > _collections = new List< string >(); + private string _changedItemCollection = string.Empty; + private IReadOnlyDictionary< string, object? > _changedItems = new Dictionary< string, object? >(); + + public Collections( DalamudPluginInterface pi ) + => _pi = pi; + + public void Draw() + { + using var _ = ImRaii.TreeNode( "Collections" ); + if( !_ ) + { + return; + } + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.GetCurrentCollectionName.Label, "Current Collection" ); + ImGui.TextUnformatted( Ipc.GetCurrentCollectionName.Subscriber( _pi ).Invoke() ); + DrawIntro( Ipc.GetDefaultCollectionName.Label, "Default Collection" ); + ImGui.TextUnformatted( Ipc.GetDefaultCollectionName.Subscriber( _pi ).Invoke() ); + DrawIntro( Ipc.GetInterfaceCollectionName.Label, "Interface Collection" ); + ImGui.TextUnformatted( Ipc.GetInterfaceCollectionName.Subscriber( _pi ).Invoke() ); + DrawIntro( Ipc.GetCharacterCollectionName.Label, "Character" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); + var (c, s) = Ipc.GetCharacterCollectionName.Subscriber( _pi ).Invoke( _characterCollectionName ); + ImGui.SameLine(); + ImGui.TextUnformatted( $"{c}, {( s ? "Custom" : "Default" )}" ); + + DrawIntro( Ipc.GetCollections.Label, "Collections" ); + if( ImGui.Button( "Get##Collections" ) ) + { + _collections = Ipc.GetCollections.Subscriber( _pi ).Invoke(); + ImGui.OpenPopup( "Collections" ); + } + + DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); + ImGui.SameLine(); + if( ImGui.Button( "Get" ) ) + { + _changedItems = Ipc.GetChangedItems.Subscriber( _pi ).Invoke( _changedItemCollection ); + ImGui.OpenPopup( "Changed Item List" ); + } + + DrawChangedItemPopup(); + DrawCollectionPopup(); + } + + private void DrawChangedItemPopup() + { + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var p = ImRaii.Popup( "Changed Item List" ); + if( !p ) + { + return; + } + foreach( var item in _changedItems ) { ImGui.TextUnformatted( item.Key ); } - if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) + { + ImGui.CloseCurrentPopup(); + } + } + + private void DrawCollectionPopup() + { + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var p = ImRaii.Popup( "Collections" ); + if( !p ) + { + return; + } + + foreach( var collection in _collections ) + { + ImGui.TextUnformatted( collection ); + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) { ImGui.CloseCurrentPopup(); } } } - private void AddedTooltip( ChangedItemType type, uint id ) + private class Meta { - _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; - ImGui.TextUnformatted( "IPC Test Successful" ); - } + private readonly DalamudPluginInterface _pi; - private void AddedClick( MouseButton button, ChangedItemType type, uint id ) - { - _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString( CultureInfo.CurrentCulture )}"; - } + private string _characterName = string.Empty; - private string _characterCollectionName = string.Empty; - private IList< (string, string) > _mods = new List< (string, string) >(); - private IList< string > _collections = new List< string >(); - private bool _collectionMode = false; + public Meta( DalamudPluginInterface pi ) + => _pi = pi; - private void DrawData() - { - using var _ = ImRaii.TreeNode( "Data IPC" ); - if( !_ ) + public void Draw() { - return; - } - - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - DrawIntro( PenumbraIpc.LabelProviderCurrentCollectionName, "Current Collection" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderCurrentCollectionName ).InvokeFunc() ); - DrawIntro( PenumbraIpc.LabelProviderDefaultCollectionName, "Default Collection" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderDefaultCollectionName ).InvokeFunc() ); - DrawIntro( PenumbraIpc.LabelProviderInterfaceCollectionName, "Interface Collection" ); - ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderInterfaceCollectionName ).InvokeFunc() ); - DrawIntro( PenumbraIpc.LabelProviderCharacterCollectionName, "Character" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); - var (c, s) = _pi.GetIpcSubscriber< string, (string, bool) >( PenumbraIpc.LabelProviderCharacterCollectionName ) - .InvokeFunc( _characterCollectionName ); - ImGui.SameLine(); - ImGui.TextUnformatted( $"{c}, {( s ? "Custom" : "Default" )}" ); - - DrawIntro( PenumbraIpc.LabelProviderGetCollections, "Collections" ); - if( ImGui.Button( "Get##Collections" ) ) - { - _collectionMode = true; - _collections = _pi.GetIpcSubscriber< IList< string > >( PenumbraIpc.LabelProviderGetCollections ).InvokeFunc(); - ImGui.OpenPopup( "Ipc Data" ); - } - - DrawIntro( PenumbraIpc.LabelProviderGetMods, "Mods" ); - if( ImGui.Button( "Get##Mods" ) ) - { - _collectionMode = false; - _mods = _pi.GetIpcSubscriber< IList< (string, string) > >( PenumbraIpc.LabelProviderGetMods ).InvokeFunc(); - ImGui.OpenPopup( "Ipc Data" ); - } - - DrawIntro( PenumbraIpc.LabelProviderGetMetaManipulations, "Meta Manipulations" ); - if( ImGui.Button( "Copy to Clipboard" ) ) - { - var base64 = _pi.GetIpcSubscriber< string, string >( PenumbraIpc.LabelProviderGetMetaManipulations ) - .InvokeFunc( _characterCollectionName ); - ImGui.SetClipboardText( base64 ); - } - - ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); - using var p = ImRaii.Popup( "Ipc Data" ); - if( p ) - { - if( _collectionMode ) + using var _ = ImRaii.TreeNode( "Meta" ); + if( !_ ) { - foreach( var collection in _collections ) - { - ImGui.TextUnformatted( collection ); - } - } - else - { - foreach( var (modDir, modName) in _mods ) - { - ImGui.TextUnformatted( $"{modDir}: {modName}" ); - } + return; } - if( ImGui.Button( "Close", -Vector2.UnitX ) || ImGui.IsWindowFocused() ) + ImGui.InputTextWithHint( "##characterName", "Character Name...", ref _characterName, 64 ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.GetMetaManipulations.Label, "Meta Manipulations" ); + if( ImGui.Button( "Copy to Clipboard" ) ) + { + var base64 = Ipc.GetMetaManipulations.Subscriber( _pi ).Invoke( _characterName ); + ImGui.SetClipboardText( base64 ); + } + + DrawIntro( Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations" ); + if( ImGui.Button( "Copy to Clipboard##Player" ) ) + { + var base64 = Ipc.GetPlayerMetaManipulations.Subscriber( _pi ).Invoke(); + ImGui.SetClipboardText( base64 ); + } + } + } + + private class Mods + { + private readonly DalamudPluginInterface _pi; + + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private IList< (string, string) > _mods = new List< (string, string) >(); + + public Mods( DalamudPluginInterface pi ) + => _pi = pi; + + public void Draw() + { + using var _ = ImRaii.TreeNode( "Mods" ); + if( !_ ) + { + return; + } + + ImGui.InputTextWithHint( "##modDir", "Mod Directory Name...", ref _modDirectory, 100 ); + ImGui.InputTextWithHint( "##modName", "Mod Name...", ref _modName, 100 ); + ImGui.InputTextWithHint( "##path", "New Path...", ref _pathInput, 100 ); + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( Ipc.GetMods.Label, "Mods" ); + if( ImGui.Button( "Get##Mods" ) ) + { + _mods = Ipc.GetMods.Subscriber( _pi ).Invoke(); + ImGui.OpenPopup( "Mods" ); + } + + DrawIntro( Ipc.ReloadMod.Label, "Reload Mod" ); + if( ImGui.Button( "Reload" ) ) + { + _lastReloadEc = Ipc.ReloadMod.Subscriber( _pi ).Invoke( _modDirectory, _modName ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastReloadEc.ToString() ); + + DrawIntro( Ipc.AddMod.Label, "Add Mod" ); + if( ImGui.Button( "Add" ) ) + { + _lastAddEc = Ipc.AddMod.Subscriber( _pi ).Invoke( _modDirectory ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastAddEc.ToString() ); + + DrawIntro( Ipc.DeleteMod.Label, "Delete Mod" ); + if( ImGui.Button( "Delete" ) ) + { + _lastDeleteEc = Ipc.DeleteMod.Subscriber( _pi ).Invoke( _modDirectory, _modName ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastDeleteEc.ToString() ); + + DrawIntro( Ipc.GetModPath.Label, "Current Path" ); + var (ec, path, def) = Ipc.GetModPath.Subscriber( _pi ).Invoke( _modDirectory, _modName ); + ImGui.TextUnformatted( $"{path} ({( def ? "Custom" : "Default" )}) [{ec}]" ); + + DrawIntro( Ipc.SetModPath.Label, "Set Path" ); + if( ImGui.Button( "Set" ) ) + { + _lastSetPathEc = Ipc.SetModPath.Subscriber( _pi ).Invoke( _modDirectory, _modName, _pathInput ); + } + + ImGui.SameLine(); + ImGui.TextUnformatted( _lastSetPathEc.ToString() ); + + + DrawModsPopup(); + } + + private void DrawModsPopup() + { + ImGui.SetNextWindowSize( ImGuiHelpers.ScaledVector2( 500, 500 ) ); + using var p = ImRaii.Popup( "Mods" ); + if( !p ) + { + return; + } + + foreach( var (modDir, modName) in _mods ) + { + ImGui.TextUnformatted( $"{modDir}: {modName}" ); + } + + if( ImGui.Button( "Close", -Vector2.UnitX ) || !ImGui.IsWindowFocused() ) { ImGui.CloseCurrentPopup(); } } } - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private string _settingsCollection = string.Empty; - private bool _settingsAllowInheritance = true; - private bool _settingsInherit = false; - private bool _settingsEnabled = false; - private int _settingsPriority = 0; - private IDictionary< string, (IList< string >, SelectType) >? _availableSettings; - private IDictionary< string, IList< string > >? _currentSettings = null; - private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; - private ModSettingChange _lastSettingChangeType; - private string _lastSettingChangeCollection = string.Empty; - private string _lastSettingChangeMod = string.Empty; - private bool _lastSettingChangeInherited; - private DateTimeOffset _lastSettingChange; - private PenumbraApiEc _lastReloadEc = PenumbraApiEc.Success; - - - private void UpdateLastModSetting( ModSettingChange type, string collection, string mod, bool inherited ) + private class ModSettings { - _lastSettingChangeType = type; - _lastSettingChangeCollection = collection; - _lastSettingChangeMod = mod; - _lastSettingChangeInherited = inherited; - _lastSettingChange = DateTimeOffset.Now; - } + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber< ModSettingChange, string, string, bool > SettingChanged; - private void DrawSetting() - { - using var _ = ImRaii.TreeNode( "Settings IPC" ); - if( !_ ) + private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; + private ModSettingChange _lastSettingChangeType; + private string _lastSettingChangeCollection = string.Empty; + private string _lastSettingChangeMod = string.Empty; + private bool _lastSettingChangeInherited; + private DateTimeOffset _lastSettingChange; + + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private string _settingsCollection = string.Empty; + private bool _settingsAllowInheritance = true; + private bool _settingsInherit = false; + private bool _settingsEnabled = false; + private int _settingsPriority = 0; + private IDictionary< string, (IList< string >, GroupType) >? _availableSettings; + private IDictionary< string, IList< string > >? _currentSettings = null; + + public ModSettings( DalamudPluginInterface pi ) { - return; + _pi = pi; + SettingChanged = Ipc.ModSettingChanged.Subscriber( pi, UpdateLastModSetting ); } - ImGui.InputTextWithHint( "##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100 ); - ImGui.InputTextWithHint( "##settingsName", "Mod Name...", ref _settingsModName, 100 ); - ImGui.InputTextWithHint( "##settingsCollection", "Collection...", ref _settingsCollection, 100 ); - ImGui.Checkbox( "Allow Inheritance", ref _settingsAllowInheritance ); - - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) + public void Draw() { - return; - } - - DrawIntro( "Last Error", _lastSettingsError.ToString() ); - DrawIntro( PenumbraIpc.LabelProviderModSettingChanged, "Last Mod Setting Changed" ); - ImGui.TextUnformatted( _lastSettingChangeMod.Length > 0 - ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{( _lastSettingChangeInherited ? " (Inherited)" : string.Empty )} at {_lastSettingChange}" - : "None" ); - DrawIntro( PenumbraIpc.LabelProviderGetAvailableModSettings, "Get Available Settings" ); - if( ImGui.Button( "Get##Available" ) ) - { - _availableSettings = _pi - .GetIpcSubscriber< string, string, IDictionary< string, (IList< string >, SelectType) >? >( - PenumbraIpc.LabelProviderGetAvailableModSettings ).InvokeFunc( _settingsModDirectory, _settingsModName ); - _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; - } - - DrawIntro( PenumbraIpc.LabelProviderReloadMod, "Reload Mod" ); - if( ImGui.Button( "Reload" ) ) - { - _lastReloadEc = _pi.GetIpcSubscriber< string, string, PenumbraApiEc >( PenumbraIpc.LabelProviderReloadMod ) - .InvokeFunc( _settingsModDirectory, _settingsModName ); - } - - ImGui.SameLine(); - ImGui.TextUnformatted( _lastReloadEc.ToString() ); - - DrawIntro( PenumbraIpc.LabelProviderGetCurrentModSettings, "Get Current Settings" ); - if( ImGui.Button( "Get##Current" ) ) - { - var ret = _pi - .GetIpcSubscriber< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( - PenumbraIpc.LabelProviderGetCurrentModSettings ).InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, - _settingsAllowInheritance ); - _lastSettingsError = ret.Item1; - if( ret.Item1 == PenumbraApiEc.Success ) + using var _ = ImRaii.TreeNode( "Mod Settings" ); + if( !_ ) { - _settingsEnabled = ret.Item2?.Item1 ?? false; - _settingsInherit = ret.Item2?.Item4 ?? false; - _settingsPriority = ret.Item2?.Item2 ?? 0; - _currentSettings = ret.Item2?.Item3; + return; } - else + + ImGui.InputTextWithHint( "##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100 ); + ImGui.InputTextWithHint( "##settingsName", "Mod Name...", ref _settingsModName, 100 ); + ImGui.InputTextWithHint( "##settingsCollection", "Collection...", ref _settingsCollection, 100 ); + ImGui.Checkbox( "Allow Inheritance", ref _settingsAllowInheritance ); + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) { - _currentSettings = null; + return; } - } - DrawIntro( PenumbraIpc.LabelProviderTryInheritMod, "Inherit Mod" ); - ImGui.Checkbox( "##inherit", ref _settingsInherit ); - ImGui.SameLine(); - if( ImGui.Button( "Set##Inherit" ) ) - { - _lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTryInheritMod ) - .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit ); - } + DrawIntro( "Last Error", _lastSettingsError.ToString() ); + DrawIntro( Ipc.ModSettingChanged.Label, "Last Mod Setting Changed" ); + ImGui.TextUnformatted( _lastSettingChangeMod.Length > 0 + ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{( _lastSettingChangeInherited ? " (Inherited)" : string.Empty )} at {_lastSettingChange}" + : "None" ); + DrawIntro( Ipc.GetAvailableModSettings.Label, "Get Available Settings" ); + if( ImGui.Button( "Get##Available" ) ) + { + _availableSettings = Ipc.GetAvailableModSettings.Subscriber( _pi ).Invoke( _settingsModDirectory, _settingsModName ); + _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; + } - DrawIntro( PenumbraIpc.LabelProviderTrySetMod, "Set Enabled" ); - ImGui.Checkbox( "##enabled", ref _settingsEnabled ); - ImGui.SameLine(); - if( ImGui.Button( "Set##Enabled" ) ) - { - _lastSettingsError = _pi.GetIpcSubscriber< string, string, string, bool, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetMod ) - .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled ); - } - DrawIntro( PenumbraIpc.LabelProviderTrySetModPriority, "Set Priority" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); - ImGui.DragInt( "##Priority", ref _settingsPriority ); - ImGui.SameLine(); - if( ImGui.Button( "Set##Priority" ) ) - { - _lastSettingsError = _pi - .GetIpcSubscriber< string, string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModPriority ) - .InvokeFunc( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); - } + DrawIntro( Ipc.GetCurrentModSettings.Label, "Get Current Settings" ); + if( ImGui.Button( "Get##Current" ) ) + { + var ret = Ipc.GetCurrentModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance ); + _lastSettingsError = ret.Item1; + if( ret.Item1 == PenumbraApiEc.Success ) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + DrawIntro( Ipc.TryInheritMod.Label, "Inherit Mod" ); + ImGui.Checkbox( "##inherit", ref _settingsInherit ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Inherit" ) ) + { + _lastSettingsError = Ipc.TryInheritMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit ); + } + + DrawIntro( Ipc.TrySetMod.Label, "Set Enabled" ); + ImGui.Checkbox( "##enabled", ref _settingsEnabled ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Enabled" ) ) + { + _lastSettingsError = Ipc.TrySetMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled ); + } + + DrawIntro( Ipc.TrySetModPriority.Label, "Set Priority" ); + ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.DragInt( "##Priority", ref _settingsPriority ); + ImGui.SameLine(); + if( ImGui.Button( "Set##Priority" ) ) + { + _lastSettingsError = Ipc.TrySetModPriority.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority ); + } + + DrawIntro( Ipc.TrySetModSetting.Label, "Set Setting(s)" ); + if( _availableSettings == null ) + { + return; + } - DrawIntro( PenumbraIpc.LabelProviderTrySetModSetting, "Set Setting(s)" ); - if( _availableSettings != null ) - { foreach( var (group, (list, type)) in _availableSettings ) { using var id = ImRaii.PushId( group ); @@ -802,19 +1026,15 @@ public class IpcTester : IDisposable ImGui.SameLine(); if( ImGui.Button( "Set##setting" ) ) { - if( type == SelectType.Single ) + if( type == GroupType.Single ) { - _lastSettingsError = _pi - .GetIpcSubscriber< string, string, string, string, string, - PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSetting ).InvokeFunc( _settingsCollection, - _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty ); + _lastSettingsError = Ipc.TrySetModSetting.Subscriber( _pi ).Invoke( _settingsCollection, + _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[ 0 ] : string.Empty ); } else { - _lastSettingsError = _pi - .GetIpcSubscriber< string, string, string, string, IReadOnlyList< string >, - PenumbraApiEc >( PenumbraIpc.LabelProviderTrySetModSettings ).InvokeFunc( _settingsCollection, - _settingsModDirectory, _settingsModName, group, current.ToArray() ); + _lastSettingsError = Ipc.TrySetModSettings.Subscriber( _pi ).Invoke( _settingsCollection, + _settingsModDirectory, _settingsModName, group, current.ToArray() ); } } @@ -822,176 +1042,183 @@ public class IpcTester : IDisposable ImGui.TextUnformatted( group ); } } + + private void UpdateLastModSetting( ModSettingChange type, string collection, string mod, bool inherited ) + { + _lastSettingChangeType = type; + _lastSettingChangeCollection = collection; + _lastSettingChangeMod = mod; + _lastSettingChangeInherited = inherited; + _lastSettingChange = DateTimeOffset.Now; + } } - private string _tempCollectionName = string.Empty; - private string _tempCharacterName = string.Empty; - private bool _forceOverwrite = true; - private string _tempModName = string.Empty; - private PenumbraApiEc _lastTempError = PenumbraApiEc.Success; - private string _lastCreatedCollectionName = string.Empty; - private string _tempGamePath = "test/game/path.mtrl"; - private string _tempFilePath = "test/success.mtrl"; - private string _tempManipulation = string.Empty; - - - private void DrawTemp() + private class Temporary { - using var _ = ImRaii.TreeNode( "Temp IPC" ); - if( !_ ) - { - return; - } + public readonly DalamudPluginInterface _pi; - ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 ); - ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 ); - ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 ); - ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 ); - ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 ); - ImGui.InputTextWithHint( "##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256 ); - ImGui.Checkbox( "Force Character Collection Overwrite", ref _forceOverwrite ); + public Temporary( DalamudPluginInterface pi ) + => _pi = pi; - using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } + public string LastCreatedCollectionName = string.Empty; - DrawIntro( "Last Error", _lastTempError.ToString() ); - DrawIntro( "Last Created Collection", _lastCreatedCollectionName ); - DrawIntro( PenumbraIpc.LabelProviderCreateTemporaryCollection, "Create Temporary Collection" ); - if( ImGui.Button( "Create##Collection" ) ) - { - ( _lastTempError, _lastCreatedCollectionName ) = - _pi.GetIpcSubscriber< string, string, bool, (PenumbraApiEc, string) >( PenumbraIpc.LabelProviderCreateTemporaryCollection ) - .InvokeFunc( _tempCollectionName, _tempCharacterName, _forceOverwrite ); - } + private string _tempCollectionName = string.Empty; + private string _tempCharacterName = string.Empty; + private string _tempModName = string.Empty; + private string _tempGamePath = "test/game/path.mtrl"; + private string _tempFilePath = "test/success.mtrl"; + private string _tempManipulation = string.Empty; + private PenumbraApiEc _lastTempError; + private bool _forceOverwrite; - DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryCollection, "Remove Temporary Collection from Character" ); - if( ImGui.Button( "Delete##Collection" ) ) + public void Draw() { - _lastTempError = _pi.GetIpcSubscriber< string, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryCollection ) - .InvokeFunc( _tempCharacterName ); - } + using var _ = ImRaii.TreeNode( "Temporary" ); + if( !_ ) + { + return; + } - DrawIntro( PenumbraIpc.LabelProviderAddTemporaryMod, "Add Temporary Mod to specific Collection" ); - if( ImGui.Button( "Add##Mod" ) ) - { - _lastTempError = _pi - .GetIpcSubscriber< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >( - PenumbraIpc.LabelProviderAddTemporaryMod ) - .InvokeFunc( _tempModName, _tempCollectionName, + ImGui.InputTextWithHint( "##tempCollection", "Collection Name...", ref _tempCollectionName, 128 ); + ImGui.InputTextWithHint( "##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32 ); + ImGui.InputTextWithHint( "##tempMod", "Temporary Mod Name...", ref _tempModName, 32 ); + ImGui.InputTextWithHint( "##tempGame", "Game Path...", ref _tempGamePath, 256 ); + ImGui.InputTextWithHint( "##tempFile", "File Path...", ref _tempFilePath, 256 ); + ImGui.InputTextWithHint( "##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256 ); + ImGui.Checkbox( "Force Character Collection Overwrite", ref _forceOverwrite ); + + + using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return; + } + + DrawIntro( "Last Error", _lastTempError.ToString() ); + DrawIntro( "Last Created Collection", LastCreatedCollectionName ); + DrawIntro( Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection" ); + if( ImGui.Button( "Create##Collection" ) ) + { + ( _lastTempError, LastCreatedCollectionName ) = Ipc.CreateTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempCharacterName, _forceOverwrite ); + } + + DrawIntro( Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character" ); + if( ImGui.Button( "Delete##Collection" ) ) + { + _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber( _pi ).Invoke( _tempCharacterName ); + } + + DrawIntro( Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection" ); + if( ImGui.Button( "Add##Mod" ) ) + { + _lastTempError = Ipc.AddTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); - } + } - DrawIntro( PenumbraIpc.LabelProviderAddTemporaryModAll, "Add Temporary Mod to all Collections" ); - if( ImGui.Button( "Add##All" ) ) - { - _lastTempError = _pi - .GetIpcSubscriber< string, Dictionary< string, string >, string, int, PenumbraApiEc >( - PenumbraIpc.LabelProviderAddTemporaryModAll ) - .InvokeFunc( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, + DrawIntro( Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections" ); + if( ImGui.Button( "Add##All" ) ) + { + _lastTempError = Ipc.AddTemporaryModAll.Subscriber( _pi ).Invoke( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } }, _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue ); - } + } - DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryMod, "Remove Temporary Mod from specific Collection" ); - if( ImGui.Button( "Remove##Mod" ) ) - { - _lastTempError = _pi.GetIpcSubscriber< string, string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryMod ) - .InvokeFunc( _tempModName, _tempCollectionName, int.MaxValue ); - } - - DrawIntro( PenumbraIpc.LabelProviderRemoveTemporaryModAll, "Remove Temporary Mod from all Collections" ); - if( ImGui.Button( "Remove##ModAll" ) ) - { - _lastTempError = _pi.GetIpcSubscriber< string, int, PenumbraApiEc >( PenumbraIpc.LabelProviderRemoveTemporaryModAll ) - .InvokeFunc( _tempModName, int.MaxValue ); - } - } - - private void DrawTempCollections() - { - using var collTree = ImRaii.TreeNode( "Collections" ); - if( !collTree ) - { - return; - } - - using var table = ImRaii.Table( "##collTree", 5 ); - if( !table ) - { - return; - } - - foreach( var (character, collection) in Penumbra.TempMods.Collections ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( character ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); - ImGui.TableNextColumn(); - if( ImGui.Button( $"Save##{character}" ) ) + DrawIntro( Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection" ); + if( ImGui.Button( "Remove##Mod" ) ) { - Mod.TemporaryMod.SaveTempCollection( collection, character ); + _lastTempError = Ipc.RemoveTemporaryMod.Subscriber( _pi ).Invoke( _tempModName, _tempCollectionName, int.MaxValue ); + } + + DrawIntro( Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections" ); + if( ImGui.Button( "Remove##ModAll" ) ) + { + _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber( _pi ).Invoke( _tempModName, int.MaxValue ); } } - } - private void DrawTempMods() - { - using var modTree = ImRaii.TreeNode( "Mods" ); - if( !modTree ) + public void DrawCollections() { - return; - } + using var collTree = ImRaii.TreeNode( "Collections" ); + if( !collTree ) + { + return; + } - using var table = ImRaii.Table( "##modTree", 5 ); + using var table = ImRaii.Table( "##collTree", 5 ); + if( !table ) + { + return; + } - void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) - { - foreach( var mod in list ) + foreach( var (character, collection) in Penumbra.TempMods.Collections ) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Name ); + ImGui.TextUnformatted( character ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TextUnformatted( collection.Name ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( collectionName ); + ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); - if( ImGui.IsItemHovered() ) + ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); + ImGui.TableNextColumn(); + if( ImGui.Button( $"Save##{character}" ) ) { - using var tt = ImRaii.Tooltip(); - foreach( var (path, file) in mod.Default.Files ) - { - ImGui.TextUnformatted( $"{path} -> {file}" ); - } - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); - if( ImGui.IsItemHovered() ) - { - using var tt = ImRaii.Tooltip(); - foreach( var manip in mod.Default.Manipulations ) - { - ImGui.TextUnformatted( manip.ToString() ); - } + Mod.TemporaryMod.SaveTempCollection( collection, character ); } } } - if( table ) + public void DrawMods() { - PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); - foreach( var (collection, list) in Penumbra.TempMods.Mods ) + using var modTree = ImRaii.TreeNode( "Mods" ); + if( !modTree ) { - PrintList( collection.Name, list ); + return; + } + + using var table = ImRaii.Table( "##modTree", 5 ); + + void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) + { + foreach( var mod in list ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collectionName ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var (path, file) in mod.Default.Files ) + { + ImGui.TextUnformatted( $"{path} -> {file}" ); + } + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var manip in mod.Default.Manipulations ) + { + ImGui.TextUnformatted( manip.ToString() ); + } + } + } + } + + if( table ) + { + PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); + foreach( var (collection, list) in Penumbra.TempMods.Mods ) + { + PrintList( collection.Name, list ); + } } } } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index fad36b1c..2da860f2 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -4,7 +4,6 @@ using Newtonsoft.Json; using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.GameData.Enums; using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -15,6 +14,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; +using Penumbra.Api.Enums; namespace Penumbra.Api; @@ -272,7 +272,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } - public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + public IDictionary< string, (IList< string >, GroupType) >? GetAvailableModSettings( string modDirectory, string modName ) { CheckInitialized(); return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) @@ -330,6 +330,56 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.Success; } + public PenumbraApiEc DeleteMod( string modDirectory, string modName ) + { + CheckInitialized(); + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + return PenumbraApiEc.NothingChanged; + + Penumbra.ModManager.DeleteMod( mod.Index ); + return PenumbraApiEc.Success; + } + + + public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ) + { + CheckInitialized(); + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) + || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) + { + return ( PenumbraApiEc.ModMissing, string.Empty, false ); + } + + var fullPath = leaf.FullName(); + + return ( PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath( mod, fullPath ) ); + } + + public PenumbraApiEc SetModPath( string modDirectory, string modName, string newPath ) + { + CheckInitialized(); + if( newPath.Length == 0 ) + { + return PenumbraApiEc.InvalidArgument; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) + || !_penumbra!.ModFileSystem.FindLeaf( mod, out var leaf ) ) + { + return PenumbraApiEc.ModMissing; + } + + try + { + _penumbra.ModFileSystem.RenameAndMove( leaf, newPath ); + return PenumbraApiEc.Success; + } + catch + { + return PenumbraApiEc.PathRenameFailed; + } + } + public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) { CheckInitialized(); @@ -405,7 +455,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.OptionMissing; } - var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx; + var setting = mod.Groups[ groupIdx ].Type == GroupType.Multi ? 1u << optionIdx : ( uint )optionIdx; return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -433,7 +483,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var group = mod.Groups[ groupIdx ]; uint setting = 0; - if( group.Type == SelectType.Single ) + if( group.Type == GroupType.Single ) { var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf( o => o.Name == optionNames[ ^1 ] ); if( optionIdx < 0 ) diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs deleted file mode 100644 index ab9496c5..00000000 --- a/Penumbra/Api/PenumbraIpc.cs +++ /dev/null @@ -1,878 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using System; -using System.Collections.Generic; - -namespace Penumbra.Api; - -public partial class PenumbraIpc : IDisposable -{ - internal readonly IPenumbraApi Api; - internal readonly IpcTester Tester; - - public PenumbraIpc( DalamudPluginInterface pi, IPenumbraApi api ) - { - Api = api; - Tester = new IpcTester( pi, this ); - InitializeGeneralProviders( pi ); - InitializeResolveProviders( pi ); - InitializeRedrawProviders( pi ); - InitializeChangedItemProviders( pi ); - InitializeDataProviders( pi ); - InitializeSettingProviders( pi ); - InitializeTempProviders( pi ); - ProviderInitialized?.SendMessage(); - InvokeModDirectoryChanged( Penumbra.ModManager.BasePath.FullName, Penumbra.ModManager.Valid ); - } - - public void Dispose() - { - DisposeDataProviders(); - DisposeChangedItemProviders(); - DisposeRedrawProviders(); - DisposeResolveProviders(); - DisposeGeneralProviders(); - DisposeSettingProviders(); - DisposeTempProviders(); - ProviderDisposed?.SendMessage(); - Tester.Dispose(); - } -} - -public partial class PenumbraIpc -{ - public const string LabelProviderInitialized = "Penumbra.Initialized"; - public const string LabelProviderDisposed = "Penumbra.Disposed"; - public const string LabelProviderApiVersion = "Penumbra.ApiVersion"; - public const string LabelProviderApiVersions = "Penumbra.ApiVersions"; - public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory"; - public const string LabelProviderModDirectoryChanged = "Penumbra.ModDirectoryChanged"; - public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration"; - public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw"; - public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw"; - - internal ICallGateProvider< object? >? ProviderInitialized; - internal ICallGateProvider< object? >? ProviderDisposed; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< (int Breaking, int Features) >? ProviderApiVersions; - internal ICallGateProvider< string >? ProviderGetModDirectory; - internal ICallGateProvider< string, bool, object? >? ProviderModDirectoryChanged; - internal ICallGateProvider< string >? ProviderGetConfiguration; - internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw; - internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw; - - private void InitializeGeneralProviders( DalamudPluginInterface pi ) - { - try - { - ProviderInitialized = pi.GetIpcProvider< object? >( LabelProviderInitialized ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderInitialized}:\n{e}" ); - } - - try - { - ProviderDisposed = pi.GetIpcProvider< object? >( LabelProviderDisposed ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDisposed}:\n{e}" ); - } - - try - { - ProviderApiVersion = pi.GetIpcProvider< int >( LabelProviderApiVersion ); - ProviderApiVersion.RegisterFunc( () => - { - Penumbra.Log.Warning( $"{LabelProviderApiVersion} is outdated. Please use {LabelProviderApiVersions} instead." ); - return Api.ApiVersion.Breaking; - } ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderApiVersion}:\n{e}" ); - } - - try - { - ProviderApiVersions = pi.GetIpcProvider< ( int, int ) >( LabelProviderApiVersions ); - ProviderApiVersions.RegisterFunc( () => Api.ApiVersion ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderApiVersions}:\n{e}" ); - } - - try - { - ProviderGetModDirectory = pi.GetIpcProvider< string >( LabelProviderGetModDirectory ); - ProviderGetModDirectory.RegisterFunc( Api.GetModDirectory ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" ); - } - - try - { - ProviderModDirectoryChanged = pi.GetIpcProvider< string, bool, object? >( LabelProviderModDirectoryChanged ); - Api.ModDirectoryChanged += InvokeModDirectoryChanged; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderModDirectoryChanged}:\n{e}" ); - } - - try - { - ProviderGetConfiguration = pi.GetIpcProvider< string >( LabelProviderGetConfiguration ); - ProviderGetConfiguration.RegisterFunc( Api.GetConfiguration ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetConfiguration}:\n{e}" ); - } - - try - { - ProviderPreSettingsDraw = pi.GetIpcProvider< string, object? >( LabelProviderPreSettingsDraw ); - Api.PreSettingsPanelDraw += InvokeSettingsPreDraw; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderPreSettingsDraw}:\n{e}" ); - } - - try - { - ProviderPostSettingsDraw = pi.GetIpcProvider< string, object? >( LabelProviderPostSettingsDraw ); - Api.PostSettingsPanelDraw += InvokeSettingsPostDraw; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderPostSettingsDraw}:\n{e}" ); - } - } - - private void DisposeGeneralProviders() - { - ProviderGetConfiguration?.UnregisterFunc(); - ProviderGetModDirectory?.UnregisterFunc(); - ProviderApiVersion?.UnregisterFunc(); - ProviderApiVersions?.UnregisterFunc(); - Api.PreSettingsPanelDraw -= InvokeSettingsPreDraw; - Api.PostSettingsPanelDraw -= InvokeSettingsPostDraw; - Api.ModDirectoryChanged -= InvokeModDirectoryChanged; - } - - private void InvokeSettingsPreDraw( string modDirectory ) - => ProviderPreSettingsDraw!.SendMessage( modDirectory ); - - private void InvokeSettingsPostDraw( string modDirectory ) - => ProviderPostSettingsDraw!.SendMessage( modDirectory ); - - private void InvokeModDirectoryChanged( string modDirectory, bool valid ) - => ProviderModDirectoryChanged?.SendMessage( modDirectory, valid ); -} - -public partial class PenumbraIpc -{ - public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; - public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; - public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; - public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; - public const string LabelProviderGameObjectRedrawn = "Penumbra.GameObjectRedrawn"; - - internal ICallGateProvider< string, int, object? >? ProviderRedrawName; - internal ICallGateProvider< GameObject, int, object? >? ProviderRedrawObject; - internal ICallGateProvider< int, int, object? >? ProviderRedrawIndex; - internal ICallGateProvider< int, object? >? ProviderRedrawAll; - internal ICallGateProvider< IntPtr, int, object? >? ProviderGameObjectRedrawn; - - private static RedrawType CheckRedrawType( int value ) - { - var type = ( RedrawType )value; - if( Enum.IsDefined( type ) ) - { - return type; - } - - throw new Exception( "The integer provided for a Redraw Function was not a valid RedrawType." ); - } - - private void InitializeRedrawProviders( DalamudPluginInterface pi ) - { - try - { - ProviderRedrawName = pi.GetIpcProvider< string, int, object? >( LabelProviderRedrawName ); - ProviderRedrawName.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); - } - - try - { - ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object? >( LabelProviderRedrawObject ); - ProviderRedrawObject.RegisterAction( ( s, i ) => Api.RedrawObject( s, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawObject}:\n{e}" ); - } - - try - { - ProviderRedrawIndex = pi.GetIpcProvider< int, int, object? >( LabelProviderRedrawIndex ); - ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); - } - - try - { - ProviderRedrawAll = pi.GetIpcProvider< int, object? >( LabelProviderRedrawAll ); - ProviderRedrawAll.RegisterAction( i => Api.RedrawAll( CheckRedrawType( i ) ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRedrawAll}:\n{e}" ); - } - - try - { - ProviderGameObjectRedrawn = pi.GetIpcProvider< IntPtr, int, object? >( LabelProviderGameObjectRedrawn ); - Api.GameObjectRedrawn += OnGameObjectRedrawn; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGameObjectRedrawn}:\n{e}" ); - } - } - - private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) - => ProviderGameObjectRedrawn?.SendMessage( objectAddress, objectTableIndex ); - - private void DisposeRedrawProviders() - { - ProviderRedrawName?.UnregisterAction(); - ProviderRedrawObject?.UnregisterAction(); - ProviderRedrawIndex?.UnregisterAction(); - ProviderRedrawAll?.UnregisterAction(); - Api.GameObjectRedrawn -= OnGameObjectRedrawn; - } -} - -public partial class PenumbraIpc -{ - public const string LabelProviderResolveDefault = "Penumbra.ResolveDefaultPath"; - public const string LabelProviderResolveInterface = "Penumbra.ResolveInterfacePath"; - public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; - public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; - public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; - public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex"; - public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; - public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; - public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; - public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase"; - public const string LabelProviderGameObjectResourcePathResolved = "Penumbra.GameObjectResourcePathResolved"; - - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string >? ProviderResolveInterface; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< string, string >? ProviderResolvePlayer; - internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; - internal ICallGateProvider< int, int >? ProviderGetCutsceneParentIndex; - internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; - internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; - internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; - internal ICallGateProvider< IntPtr, string, IntPtr, object? >? ProviderCreatedCharacterBase; - internal ICallGateProvider< IntPtr, string, string, object? >? ProviderGameObjectResourcePathResolved; - - private void InitializeResolveProviders( DalamudPluginInterface pi ) - { - try - { - ProviderResolveDefault = pi.GetIpcProvider< string, string >( LabelProviderResolveDefault ); - ProviderResolveDefault.RegisterFunc( Api.ResolveDefaultPath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveDefault}:\n{e}" ); - } - - try - { - ProviderResolveInterface = pi.GetIpcProvider< string, string >( LabelProviderResolveInterface ); - ProviderResolveInterface.RegisterFunc( Api.ResolveInterfacePath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveInterface}:\n{e}" ); - } - - try - { - ProviderResolveCharacter = pi.GetIpcProvider< string, string, string >( LabelProviderResolveCharacter ); - ProviderResolveCharacter.RegisterFunc( Api.ResolvePath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); - } - - try - { - ProviderResolvePlayer = pi.GetIpcProvider< string, string >( LabelProviderResolvePlayer ); - ProviderResolvePlayer.RegisterFunc( Api.ResolvePlayerPath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderResolveCharacter}:\n{e}" ); - } - - try - { - ProviderGetDrawObjectInfo = pi.GetIpcProvider< IntPtr, (IntPtr, string) >( LabelProviderGetDrawObjectInfo ); - ProviderGetDrawObjectInfo.RegisterFunc( Api.GetDrawObjectInfo ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); - } - - try - { - ProviderGetCutsceneParentIndex = pi.GetIpcProvider< int, int >( LabelProviderGetCutsceneParentIndex ); - ProviderGetCutsceneParentIndex.RegisterFunc( Api.GetCutsceneParentIndex ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" ); - } - - try - { - ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); - ProviderReverseResolvePath.RegisterFunc( Api.ReverseResolvePath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePath}:\n{e}" ); - } - - try - { - ProviderReverseResolvePathPlayer = pi.GetIpcProvider< string, string[] >( LabelProviderReverseResolvePlayerPath ); - ProviderReverseResolvePathPlayer.RegisterFunc( Api.ReverseResolvePlayerPath ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReverseResolvePlayerPath}:\n{e}" ); - } - - try - { - ProviderCreatingCharacterBase = - pi.GetIpcProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( LabelProviderCreatingCharacterBase ); - Api.CreatingCharacterBase += CreatingCharacterBaseEvent; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" ); - } - - try - { - ProviderCreatedCharacterBase = - pi.GetIpcProvider< IntPtr, string, IntPtr, object? >( LabelProviderCreatedCharacterBase ); - Api.CreatedCharacterBase += CreatedCharacterBaseEvent; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" ); - } - - try - { - ProviderGameObjectResourcePathResolved = - pi.GetIpcProvider< IntPtr, string, string, object? >( LabelProviderGameObjectResourcePathResolved ); - Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGameObjectResourcePathResolved}:\n{e}" ); - } - } - - private void GameObjectResourceResolvedEvent( IntPtr gameObject, string gamePath, string localPath ) - { - ProviderGameObjectResourcePathResolved?.SendMessage( gameObject, gamePath, localPath ); - } - - private void DisposeResolveProviders() - { - ProviderGetDrawObjectInfo?.UnregisterFunc(); - ProviderGetCutsceneParentIndex?.UnregisterFunc(); - ProviderResolveDefault?.UnregisterFunc(); - ProviderResolveInterface?.UnregisterFunc(); - ProviderResolveCharacter?.UnregisterFunc(); - ProviderReverseResolvePath?.UnregisterFunc(); - ProviderReverseResolvePathPlayer?.UnregisterFunc(); - Api.CreatingCharacterBase -= CreatingCharacterBaseEvent; - Api.CreatedCharacterBase -= CreatedCharacterBaseEvent; - Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent; - } - - private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData ) - { - ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, modelId, customize, equipData ); - } - - private void CreatedCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr drawObject ) - { - ProviderCreatedCharacterBase?.SendMessage( gameObject, collection.Name, drawObject ); - } -} - -public partial class PenumbraIpc -{ - public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; - public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; - public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - - internal ICallGateProvider< ChangedItemType, uint, object? >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object? >? ProviderChangedItemClick; - internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; - - private void OnClick( MouseButton click, object? item ) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ProviderChangedItemClick?.SendMessage( click, type, id ); - } - - private void OnTooltip( object? item ) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); - ProviderChangedItemTooltip?.SendMessage( type, id ); - } - - private void InitializeChangedItemProviders( DalamudPluginInterface pi ) - { - try - { - ProviderChangedItemTooltip = pi.GetIpcProvider< ChangedItemType, uint, object? >( LabelProviderChangedItemTooltip ); - Api.ChangedItemTooltip += OnTooltip; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemTooltip}:\n{e}" ); - } - - try - { - ProviderChangedItemClick = pi.GetIpcProvider< MouseButton, ChangedItemType, uint, object? >( LabelProviderChangedItemClick ); - Api.ChangedItemClicked += OnClick; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); - } - - try - { - ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); - ProviderGetChangedItems.RegisterFunc( Api.GetChangedItemsForCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); - } - } - - private void DisposeChangedItemProviders() - { - ProviderGetChangedItems?.UnregisterFunc(); - Api.ChangedItemClicked -= OnClick; - Api.ChangedItemTooltip -= OnTooltip; - } -} - -public partial class PenumbraIpc -{ - public const string LabelProviderGetMods = "Penumbra.GetMods"; - public const string LabelProviderGetCollections = "Penumbra.GetCollections"; - public const string LabelProviderCurrentCollectionName = "Penumbra.GetCurrentCollectionName"; - public const string LabelProviderDefaultCollectionName = "Penumbra.GetDefaultCollectionName"; - public const string LabelProviderInterfaceCollectionName = "Penumbra.GetInterfaceCollectionName"; - public const string LabelProviderCharacterCollectionName = "Penumbra.GetCharacterCollectionName"; - public const string LabelProviderGetPlayerMetaManipulations = "Penumbra.GetPlayerMetaManipulations"; - public const string LabelProviderGetMetaManipulations = "Penumbra.GetMetaManipulations"; - - internal ICallGateProvider< IList< (string, string) > >? ProviderGetMods; - internal ICallGateProvider< IList< string > >? ProviderGetCollections; - internal ICallGateProvider< string >? ProviderCurrentCollectionName; - internal ICallGateProvider< string >? ProviderDefaultCollectionName; - internal ICallGateProvider< string >? ProviderInterfaceCollectionName; - internal ICallGateProvider< string, (string, bool) >? ProviderCharacterCollectionName; - internal ICallGateProvider< string >? ProviderGetPlayerMetaManipulations; - internal ICallGateProvider< string, string >? ProviderGetMetaManipulations; - - private void InitializeDataProviders( DalamudPluginInterface pi ) - { - try - { - ProviderGetMods = pi.GetIpcProvider< IList< (string, string) > >( LabelProviderGetMods ); - ProviderGetMods.RegisterFunc( Api.GetModList ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetMods}:\n{e}" ); - } - - try - { - ProviderGetCollections = pi.GetIpcProvider< IList< string > >( LabelProviderGetCollections ); - ProviderGetCollections.RegisterFunc( Api.GetCollections ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCollections}:\n{e}" ); - } - - try - { - ProviderCurrentCollectionName = pi.GetIpcProvider< string >( LabelProviderCurrentCollectionName ); - ProviderCurrentCollectionName.RegisterFunc( Api.GetCurrentCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCurrentCollectionName}:\n{e}" ); - } - - try - { - ProviderDefaultCollectionName = pi.GetIpcProvider< string >( LabelProviderDefaultCollectionName ); - ProviderDefaultCollectionName.RegisterFunc( Api.GetDefaultCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderDefaultCollectionName}:\n{e}" ); - } - - try - { - ProviderInterfaceCollectionName = pi.GetIpcProvider( LabelProviderInterfaceCollectionName ); - ProviderInterfaceCollectionName.RegisterFunc( Api.GetInterfaceCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderInterfaceCollectionName}:\n{e}" ); - } - - try - { - ProviderCharacterCollectionName = pi.GetIpcProvider< string, (string, bool) >( LabelProviderCharacterCollectionName ); - ProviderCharacterCollectionName.RegisterFunc( Api.GetCharacterCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCharacterCollectionName}:\n{e}" ); - } - - try - { - ProviderGetPlayerMetaManipulations = pi.GetIpcProvider< string >( LabelProviderGetPlayerMetaManipulations ); - ProviderGetPlayerMetaManipulations.RegisterFunc( Api.GetPlayerMetaManipulations ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetPlayerMetaManipulations}:\n{e}" ); - } - - try - { - ProviderGetMetaManipulations = pi.GetIpcProvider< string, string >( LabelProviderGetMetaManipulations ); - ProviderGetMetaManipulations.RegisterFunc( Api.GetMetaManipulations ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetMetaManipulations}:\n{e}" ); - } - } - - private void DisposeDataProviders() - { - ProviderGetMods?.UnregisterFunc(); - ProviderGetCollections?.UnregisterFunc(); - ProviderCurrentCollectionName?.UnregisterFunc(); - ProviderDefaultCollectionName?.UnregisterFunc(); - ProviderInterfaceCollectionName?.UnregisterFunc(); - ProviderCharacterCollectionName?.UnregisterFunc(); - ProviderGetMetaManipulations?.UnregisterFunc(); - } -} - -public partial class PenumbraIpc -{ - public const string LabelProviderGetAvailableModSettings = "Penumbra.GetAvailableModSettings"; - public const string LabelProviderReloadMod = "Penumbra.ReloadMod"; - public const string LabelProviderAddMod = "Penumbra.AddMod"; - public const string LabelProviderGetCurrentModSettings = "Penumbra.GetCurrentModSettings"; - public const string LabelProviderTryInheritMod = "Penumbra.TryInheritMod"; - public const string LabelProviderTrySetMod = "Penumbra.TrySetMod"; - public const string LabelProviderTrySetModPriority = "Penumbra.TrySetModPriority"; - public const string LabelProviderTrySetModSetting = "Penumbra.TrySetModSetting"; - public const string LabelProviderTrySetModSettings = "Penumbra.TrySetModSettings"; - public const string LabelProviderModSettingChanged = "Penumbra.ModSettingChanged"; - - internal ICallGateProvider< ModSettingChange, string, string, bool, object? >? ProviderModSettingChanged; - - internal ICallGateProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >? ProviderGetAvailableModSettings; - internal ICallGateProvider< string, string, PenumbraApiEc >? ProviderReloadMod; - internal ICallGateProvider< string, PenumbraApiEc >? ProviderAddMod; - - internal ICallGateProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >? - ProviderGetCurrentModSettings; - - internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTryInheritMod; - internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTrySetMod; - internal ICallGateProvider< string, string, string, int, PenumbraApiEc >? ProviderTrySetModPriority; - internal ICallGateProvider< string, string, string, string, string, PenumbraApiEc >? ProviderTrySetModSetting; - internal ICallGateProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >? ProviderTrySetModSettings; - - private void InitializeSettingProviders( DalamudPluginInterface pi ) - { - try - { - ProviderModSettingChanged = pi.GetIpcProvider< ModSettingChange, string, string, bool, object? >( LabelProviderModSettingChanged ); - Api.ModSettingChanged += InvokeModSettingChanged; - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderModSettingChanged}:\n{e}" ); - } - - try - { - ProviderGetAvailableModSettings = - pi.GetIpcProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >( - LabelProviderGetAvailableModSettings ); - ProviderGetAvailableModSettings.RegisterFunc( Api.GetAvailableModSettings ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); - } - - try - { - ProviderReloadMod = pi.GetIpcProvider< string, string, PenumbraApiEc >( LabelProviderReloadMod ); - ProviderReloadMod.RegisterFunc( Api.ReloadMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderReloadMod}:\n{e}" ); - } - - try - { - ProviderAddMod = pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderAddMod ); - ProviderAddMod.RegisterFunc( Api.AddMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); - } - - try - { - ProviderGetCurrentModSettings = - pi.GetIpcProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( - LabelProviderGetCurrentModSettings ); - ProviderGetCurrentModSettings.RegisterFunc( Api.GetCurrentModSettings ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" ); - } - - try - { - ProviderTryInheritMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTryInheritMod ); - ProviderTryInheritMod.RegisterFunc( Api.TryInheritMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" ); - } - - try - { - ProviderTrySetMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTrySetMod ); - ProviderTrySetMod.RegisterFunc( Api.TrySetMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" ); - } - - try - { - ProviderTrySetModPriority = pi.GetIpcProvider< string, string, string, int, PenumbraApiEc >( LabelProviderTrySetModPriority ); - ProviderTrySetModPriority.RegisterFunc( Api.TrySetModPriority ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" ); - } - - try - { - ProviderTrySetModSetting = - pi.GetIpcProvider< string, string, string, string, string, PenumbraApiEc >( LabelProviderTrySetModSetting ); - ProviderTrySetModSetting.RegisterFunc( Api.TrySetModSetting ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" ); - } - - try - { - ProviderTrySetModSettings = - pi.GetIpcProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >( LabelProviderTrySetModSettings ); - ProviderTrySetModSettings.RegisterFunc( Api.TrySetModSettings ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" ); - } - } - - private void DisposeSettingProviders() - { - Api.ModSettingChanged -= InvokeModSettingChanged; - ProviderGetAvailableModSettings?.UnregisterFunc(); - ProviderReloadMod?.UnregisterFunc(); - ProviderAddMod?.UnregisterFunc(); - ProviderGetCurrentModSettings?.UnregisterFunc(); - ProviderTryInheritMod?.UnregisterFunc(); - ProviderTrySetMod?.UnregisterFunc(); - ProviderTrySetModPriority?.UnregisterFunc(); - ProviderTrySetModSetting?.UnregisterFunc(); - ProviderTrySetModSettings?.UnregisterFunc(); - } - - private void InvokeModSettingChanged( ModSettingChange type, string collection, string mod, bool inherited ) - => ProviderModSettingChanged?.SendMessage( type, collection, mod, inherited ); -} - -public partial class PenumbraIpc -{ - public const string LabelProviderCreateTemporaryCollection = "Penumbra.CreateTemporaryCollection"; - public const string LabelProviderRemoveTemporaryCollection = "Penumbra.RemoveTemporaryCollection"; - public const string LabelProviderAddTemporaryModAll = "Penumbra.AddTemporaryModAll"; - public const string LabelProviderAddTemporaryMod = "Penumbra.AddTemporaryMod"; - public const string LabelProviderRemoveTemporaryModAll = "Penumbra.RemoveTemporaryModAll"; - public const string LabelProviderRemoveTemporaryMod = "Penumbra.RemoveTemporaryMod"; - - internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection; - internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection; - - internal ICallGateProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc >? - ProviderAddTemporaryModAll; - - internal ICallGateProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >? - ProviderAddTemporaryMod; - - internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll; - internal ICallGateProvider< string, string, int, PenumbraApiEc >? ProviderRemoveTemporaryMod; - - private void InitializeTempProviders( DalamudPluginInterface pi ) - { - try - { - ProviderCreateTemporaryCollection = - pi.GetIpcProvider< string, string, bool, (PenumbraApiEc, string) >( LabelProviderCreateTemporaryCollection ); - ProviderCreateTemporaryCollection.RegisterFunc( Api.CreateTemporaryCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" ); - } - - try - { - ProviderRemoveTemporaryCollection = - pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderRemoveTemporaryCollection ); - ProviderRemoveTemporaryCollection.RegisterFunc( Api.RemoveTemporaryCollection ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" ); - } - - try - { - ProviderAddTemporaryModAll = - pi.GetIpcProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc >( - LabelProviderAddTemporaryModAll ); - ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" ); - } - - try - { - ProviderAddTemporaryMod = - pi.GetIpcProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc >( - LabelProviderAddTemporaryMod ); - ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" ); - } - - try - { - ProviderRemoveTemporaryModAll = pi.GetIpcProvider< string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryModAll ); - ProviderRemoveTemporaryModAll.RegisterFunc( Api.RemoveTemporaryModAll ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" ); - } - - try - { - ProviderRemoveTemporaryMod = pi.GetIpcProvider< string, string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryMod ); - ProviderRemoveTemporaryMod.RegisterFunc( Api.RemoveTemporaryMod ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" ); - } - } - - private void DisposeTempProviders() - { - ProviderCreateTemporaryCollection?.UnregisterFunc(); - ProviderRemoveTemporaryCollection?.UnregisterFunc(); - ProviderAddTemporaryModAll?.UnregisterFunc(); - ProviderAddTemporaryMod?.UnregisterFunc(); - ProviderRemoveTemporaryModAll?.UnregisterFunc(); - ProviderRemoveTemporaryMod?.UnregisterFunc(); - } -} \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs new file mode 100644 index 00000000..26a982ce --- /dev/null +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -0,0 +1,307 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using System; +using System.Collections.Generic; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +using CurrentSettings = ValueTuple< PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)? >; + +public class PenumbraIpcProviders : IDisposable +{ + internal readonly IPenumbraApi Api; + internal readonly IpcTester Tester; + + // Plugin State + internal readonly EventProvider Initialized; + internal readonly EventProvider Disposed; + internal readonly FuncProvider< int > ApiVersion; + internal readonly FuncProvider< (int Breaking, int Features) > ApiVersions; + + // Configuration + internal readonly FuncProvider< string > GetModDirectory; + internal readonly FuncProvider< string > GetConfiguration; + internal readonly EventProvider< string, bool > ModDirectoryChanged; + + // UI + internal readonly EventProvider< string > PreSettingsDraw; + internal readonly EventProvider< string > PostSettingsDraw; + internal readonly EventProvider< ChangedItemType, uint > ChangedItemTooltip; + internal readonly EventProvider< MouseButton, ChangedItemType, uint > ChangedItemClick; + + // Redrawing + internal readonly ActionProvider< RedrawType > RedrawAll; + internal readonly ActionProvider< GameObject, RedrawType > RedrawObject; + internal readonly ActionProvider< int, RedrawType > RedrawObjectByIndex; + internal readonly ActionProvider< string, RedrawType > RedrawObjectByName; + internal readonly EventProvider< nint, int > GameObjectRedrawn; + + // Game State + internal readonly FuncProvider< nint, (nint, string) > GetDrawObjectInfo; + internal readonly FuncProvider< int, int > GetCutsceneParentIndex; + internal readonly EventProvider< nint, string, nint, nint, nint > CreatingCharacterBase; + internal readonly EventProvider< nint, string, nint > CreatedCharacterBase; + internal readonly EventProvider< nint, string, string > GameObjectResourcePathResolved; + + // Resolve + internal readonly FuncProvider< string, string > ResolveDefaultPath; + internal readonly FuncProvider< string, string > ResolveInterfacePath; + internal readonly FuncProvider< string, string > ResolvePlayerPath; + internal readonly FuncProvider< string, string, string > ResolveCharacterPath; + internal readonly FuncProvider< string, string, string[] > ReverseResolvePath; + internal readonly FuncProvider< string, string[] > ReverseResolvePathPlayer; + + // Collections + internal readonly FuncProvider< IList< string > > GetCollections; + internal readonly FuncProvider< string > GetCurrentCollectionName; + internal readonly FuncProvider< string > GetDefaultCollectionName; + internal readonly FuncProvider< string > GetInterfaceCollectionName; + internal readonly FuncProvider< string, (string, bool) > GetCharacterCollectionName; + internal readonly FuncProvider< string, IReadOnlyDictionary< string, object? > > GetChangedItems; + + // Meta + internal readonly FuncProvider< string > GetPlayerMetaManipulations; + internal readonly FuncProvider< string, string > GetMetaManipulations; + + // Mods + internal readonly FuncProvider< IList< (string, string) > > GetMods; + internal readonly FuncProvider< string, string, PenumbraApiEc > ReloadMod; + internal readonly FuncProvider< string, PenumbraApiEc > AddMod; + internal readonly FuncProvider< string, string, PenumbraApiEc > DeleteMod; + internal readonly FuncProvider< string, string, (PenumbraApiEc, string, bool) > GetModPath; + internal readonly FuncProvider< string, string, string, PenumbraApiEc > SetModPath; + + // ModSettings + internal readonly FuncProvider< string, string, IDictionary< string, (IList< string >, GroupType) >? > GetAvailableModSettings; + internal readonly FuncProvider< string, string, string, bool, CurrentSettings > GetCurrentModSettings; + internal readonly FuncProvider< string, string, string, bool, PenumbraApiEc > TryInheritMod; + internal readonly FuncProvider< string, string, string, bool, PenumbraApiEc > TrySetMod; + internal readonly FuncProvider< string, string, string, int, PenumbraApiEc > TrySetModPriority; + internal readonly FuncProvider< string, string, string, string, string, PenumbraApiEc > TrySetModSetting; + internal readonly FuncProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > TrySetModSettings; + internal readonly EventProvider< ModSettingChange, string, string, bool > ModSettingChanged; + + // Temporary + internal readonly FuncProvider< string, string, bool, (PenumbraApiEc, string) > CreateTemporaryCollection; + internal readonly FuncProvider< string, PenumbraApiEc > RemoveTemporaryCollection; + internal readonly FuncProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryModAll; + internal readonly FuncProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > AddTemporaryMod; + internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; + internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; + + public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api ) + { + Api = api; + + // Plugin State + Initialized = Ipc.Initialized.Provider( pi ); + Disposed = Ipc.Disposed.Provider( pi ); + ApiVersion = Ipc.ApiVersion.Provider( pi, DeprecatedVersion ); + ApiVersions = Ipc.ApiVersions.Provider( pi, () => Api.ApiVersion ); + + // Configuration + GetModDirectory = Ipc.GetModDirectory.Provider( pi, Api.GetModDirectory ); + GetConfiguration = Ipc.GetConfiguration.Provider( pi, Api.GetConfiguration ); + ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider( pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a ); + + // UI + PreSettingsDraw = Ipc.PreSettingsDraw.Provider( pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a ); + PostSettingsDraw = Ipc.PostSettingsDraw.Provider( pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a ); + ChangedItemTooltip = Ipc.ChangedItemTooltip.Provider( pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip ); + ChangedItemClick = Ipc.ChangedItemClick.Provider( pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick ); + + // Redrawing + RedrawAll = Ipc.RedrawAll.Provider( pi, Api.RedrawAll ); + RedrawObject = Ipc.RedrawObject.Provider( pi, Api.RedrawObject ); + RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider( pi, Api.RedrawObject ); + RedrawObjectByName = Ipc.RedrawObjectByName.Provider( pi, Api.RedrawObject ); + GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider( pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn, () => Api.GameObjectRedrawn -= OnGameObjectRedrawn ); + + // Game State + GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider( pi, Api.GetDrawObjectInfo ); + GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider( pi, Api.GetCutsceneParentIndex ); + CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider( pi, + () => Api.CreatingCharacterBase += CreatingCharacterBaseEvent, + () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent ); + CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider( pi, + () => Api.CreatedCharacterBase += CreatedCharacterBaseEvent, + () => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent ); + GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider( pi, + () => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent, + () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent ); + + // Resolve + ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider( pi, Api.ResolveDefaultPath ); + ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider( pi, Api.ResolveInterfacePath ); + ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider( pi, Api.ResolvePlayerPath ); + ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider( pi, Api.ResolvePath ); + ReverseResolvePath = Ipc.ReverseResolvePath.Provider( pi, Api.ReverseResolvePath ); + ReverseResolvePathPlayer = Ipc.ReverseResolvePlayerPath.Provider( pi, Api.ReverseResolvePlayerPath ); + + // Collections + GetCollections = Ipc.GetCollections.Provider( pi, Api.GetCollections ); + GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider( pi, Api.GetCurrentCollection ); + GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider( pi, Api.GetDefaultCollection ); + GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider( pi, Api.GetInterfaceCollection ); + GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider( pi, Api.GetCharacterCollection ); + GetChangedItems = Ipc.GetChangedItems.Provider( pi, Api.GetChangedItemsForCollection ); + + // Meta + GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider( pi, Api.GetPlayerMetaManipulations ); + GetMetaManipulations = Ipc.GetMetaManipulations.Provider( pi, Api.GetMetaManipulations ); + + // Mods + GetMods = Ipc.GetMods.Provider( pi, Api.GetModList ); + ReloadMod = Ipc.ReloadMod.Provider( pi, Api.ReloadMod ); + AddMod = Ipc.AddMod.Provider( pi, Api.AddMod ); + DeleteMod = Ipc.DeleteMod.Provider( pi, Api.DeleteMod ); + GetModPath = Ipc.GetModPath.Provider( pi, Api.GetModPath ); + SetModPath = Ipc.SetModPath.Provider( pi, Api.SetModPath ); + + // ModSettings + + GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider( pi, Api.GetAvailableModSettings ); + GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider( pi, Api.GetCurrentModSettings ); + TryInheritMod = Ipc.TryInheritMod.Provider( pi, Api.TryInheritMod ); + TrySetMod = Ipc.TrySetMod.Provider( pi, Api.TrySetMod ); + TrySetModPriority = Ipc.TrySetModPriority.Provider( pi, Api.TrySetModPriority ); + TrySetModSetting = Ipc.TrySetModSetting.Provider( pi, Api.TrySetModSetting ); + TrySetModSettings = Ipc.TrySetModSettings.Provider( pi, Api.TrySetModSettings ); + ModSettingChanged = Ipc.ModSettingChanged.Provider( pi, + () => Api.ModSettingChanged += ModSettingChangedEvent, + () => Api.ModSettingChanged -= ModSettingChangedEvent ); + + // Temporary + CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider( pi, Api.CreateTemporaryCollection ); + RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider( pi, Api.RemoveTemporaryCollection ); + AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider( pi, Api.AddTemporaryModAll ); + AddTemporaryMod = Ipc.AddTemporaryMod.Provider( pi, Api.AddTemporaryMod ); + RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll ); + RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod ); + + Tester = new IpcTester( pi, this ); + + Initialized.Invoke(); + } + + public void Dispose() + { + // Plugin State + Initialized.Dispose(); + ApiVersion.Dispose(); + ApiVersions.Dispose(); + + // Configuration + GetModDirectory.Dispose(); + GetConfiguration.Dispose(); + ModDirectoryChanged.Dispose(); + + // UI + PreSettingsDraw.Dispose(); + PostSettingsDraw.Dispose(); + ChangedItemTooltip.Dispose(); + ChangedItemClick.Dispose(); + + // Redrawing + RedrawAll.Dispose(); + RedrawObject.Dispose(); + RedrawObjectByIndex.Dispose(); + RedrawObjectByName.Dispose(); + GameObjectRedrawn.Dispose(); + + // Game State + GetDrawObjectInfo.Dispose(); + GetCutsceneParentIndex.Dispose(); + CreatingCharacterBase.Dispose(); + CreatedCharacterBase.Dispose(); + GameObjectResourcePathResolved.Dispose(); + + // Resolve + ResolveDefaultPath.Dispose(); + ResolveInterfacePath.Dispose(); + ResolvePlayerPath.Dispose(); + ResolveCharacterPath.Dispose(); + ReverseResolvePath.Dispose(); + ReverseResolvePathPlayer.Dispose(); + + // Collections + GetCollections.Dispose(); + GetCurrentCollectionName.Dispose(); + GetDefaultCollectionName.Dispose(); + GetInterfaceCollectionName.Dispose(); + GetCharacterCollectionName.Dispose(); + GetChangedItems.Dispose(); + + // Meta + GetPlayerMetaManipulations.Dispose(); + GetMetaManipulations.Dispose(); + + // Mods + GetMods.Dispose(); + ReloadMod.Dispose(); + AddMod.Dispose(); + DeleteMod.Dispose(); + GetModPath.Dispose(); + SetModPath.Dispose(); + + // ModSettings + GetAvailableModSettings.Dispose(); + GetCurrentModSettings.Dispose(); + TryInheritMod.Dispose(); + TrySetMod.Dispose(); + TrySetModPriority.Dispose(); + TrySetModSetting.Dispose(); + TrySetModSettings.Dispose(); + ModSettingChanged.Dispose(); + + // Temporary + CreateTemporaryCollection.Dispose(); + RemoveTemporaryCollection.Dispose(); + AddTemporaryModAll.Dispose(); + AddTemporaryMod.Dispose(); + RemoveTemporaryModAll.Dispose(); + RemoveTemporaryMod.Dispose(); + + Disposed.Invoke(); + Disposed.Dispose(); + Tester.Dispose(); + } + + // Wrappers + private int DeprecatedVersion() + { + Penumbra.Log.Warning( $"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead." ); + return Api.ApiVersion.Breaking; + } + + private void OnClick( MouseButton click, object? item ) + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); + ChangedItemClick.Invoke( click, type, id ); + } + + private void OnTooltip( object? item ) + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId( item ); + ChangedItemTooltip.Invoke( type, id ); + } + + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) + => GameObjectRedrawn.Invoke( objectAddress, objectTableIndex ); + + private void CreatingCharacterBaseEvent( IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData ) + => CreatingCharacterBase.Invoke( gameObject, collectionName, modelId, customize, equipData ); + + private void CreatedCharacterBaseEvent( IntPtr gameObject, string collectionName, IntPtr drawObject ) + => CreatedCharacterBase.Invoke( gameObject, collectionName, drawObject ); + + private void GameObjectResourceResolvedEvent( IntPtr gameObject, string gamePath, string localPath ) + => GameObjectResourcePathResolved.Invoke( gameObject, gamePath, localPath ); + + private void ModSettingChangedEvent( ModSettingChange type, string collection, string mod, bool inherited ) + => ModSettingChanged.Invoke( type, collection, mod, inherited ); +} \ No newline at end of file diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs index 9aef89b7..ad37c587 100644 --- a/Penumbra/Api/RedrawController.cs +++ b/Penumbra/Api/RedrawController.cs @@ -1,8 +1,8 @@ using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; -using Penumbra.GameData.Enums; using System.Threading.Tasks; +using Penumbra.Api.Enums; namespace Penumbra.Api; diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index d2bb2d92..2d2262cc 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -7,6 +7,7 @@ using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; +using Penumbra.Api.Enums; namespace Penumbra.Collections; @@ -269,10 +270,10 @@ public partial class ModCollection var config = settings.Settings[ groupIndex ]; switch( group.Type ) { - case SelectType.Single: + case GroupType.Single: AddSubMod( group[ ( int )config ], mod ); break; - case SelectType.Multi: + case GroupType.Multi: { foreach( var (option, _) in group.WithIndex() .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 32826dc8..fead3281 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -2,20 +2,10 @@ using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; +using Penumbra.Api.Enums; namespace Penumbra.Collections; -// Different types a mod setting can change: -public enum ModSettingChange -{ - Inheritance, // it was set to inherit from other collections or not inherit anymore - EnableState, // it was enabled or disabled - Priority, // its priority was changed - Setting, // a specific setting was changed - MultiInheritance, // multiple mods were set to inherit from other collections or not inherit anymore. - MultiEnableState, // multiple mods were enabled or disabled at once. -} - public partial class ModCollection { // If the change type is a bool, oldValue will be 1 for true and 0 for false. diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 16415a09..44403758 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -3,6 +3,7 @@ using Penumbra.Mods; using System; using System.Collections.Generic; using System.Linq; +using Penumbra.Api.Enums; namespace Penumbra.Collections; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 0edef44e..ca8eb492 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Newtonsoft.Json; using OtterGui; +using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Util; using SharpCompress.Archives.Zip; @@ -162,7 +163,7 @@ public partial class TexToolsImporter foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) ) { var allOptions = group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ).ToList(); - var (numGroups, maxOptions) = group.SelectionType == SelectType.Single + var (numGroups, maxOptions) = group.SelectionType == GroupType.Single ? ( 1, allOptions.Count ) : ( 1 + allOptions.Count / IModGroup.MaxMultiOptions, IModGroup.MaxMultiOptions ); _currentGroupName = GetGroupName( group.GroupName, groupNames ); @@ -177,7 +178,7 @@ public partial class TexToolsImporter ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); - uint? defaultSettings = group.SelectionType == SelectType.Multi ? 0u : null; + uint? defaultSettings = group.SelectionType == GroupType.Multi ? 0u : null; for( var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i ) { var option = allOptions[ i + optionIdx ]; @@ -195,7 +196,7 @@ public partial class TexToolsImporter if( option.IsChecked ) { - defaultSettings = group.SelectionType == SelectType.Multi + defaultSettings = group.SelectionType == GroupType.Multi ? ( defaultSettings!.Value | ( 1u << i ) ) : ( uint )i; } @@ -207,7 +208,7 @@ public partial class TexToolsImporter // Handle empty options for single select groups without creating a folder for them. // We only want one of those at most, and it should usually be the first option. - if( group.SelectionType == SelectType.Single ) + if( group.SelectionType == GroupType.Single ) { var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ); if( empty != null ) diff --git a/Penumbra/Import/TexToolsStructs.cs b/Penumbra/Import/TexToolsStructs.cs index 51b73b90..bc85893c 100644 --- a/Penumbra/Import/TexToolsStructs.cs +++ b/Penumbra/Import/TexToolsStructs.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.Api.Enums; using Penumbra.Mods; namespace Penumbra.Import; @@ -34,7 +35,7 @@ internal class ModPackPage internal class ModGroup { public string GroupName = string.Empty; - public SelectType SelectionType = SelectType.Single; + public GroupType SelectionType = GroupType.Single; public OptionList[] OptionList = Array.Empty< OptionList >(); } @@ -46,7 +47,7 @@ internal class OptionList public string ImagePath = string.Empty; public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >(); public string GroupName = string.Empty; - public SelectType SelectionType = SelectType.Single; + public GroupType SelectionType = GroupType.Single; public bool IsChecked = false; } diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index a04801b9..4fecf454 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.Api; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index 40228fad..aa6e7ed4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -149,7 +149,7 @@ public unsafe partial class PathResolver try { var modelPtr = &a; - CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ( IntPtr )modelPtr, b, c ); + CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ( IntPtr )modelPtr, b, c ); } catch( Exception e ) { @@ -163,7 +163,7 @@ public unsafe partial class PathResolver if( LastGameObject != null && ret != IntPtr.Zero ) { _drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); - CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection, ret ); + CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret ); } return ret; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 3e248c0e..704d1285 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Api.Enums; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Util; @@ -16,7 +17,7 @@ public sealed partial class Mod public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ); public event ModOptionChangeDelegate ModOptionChanged; - public void ChangeModGroupType( Mod mod, int groupIdx, SelectType type ) + public void ChangeModGroupType( Mod mod, int groupIdx, GroupType type ) { var group = mod._groups[ groupIdx ]; if( group.Type == type ) @@ -61,7 +62,7 @@ public sealed partial class Mod ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1 ); } - public void AddModGroup( Mod mod, SelectType type, string newName ) + public void AddModGroup( Mod mod, GroupType type, string newName ) { if( !VerifyFileName( mod, null, newName, true ) ) { @@ -70,7 +71,7 @@ public sealed partial class Mod var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max( o => o.Priority ) + 1; - mod._groups.Add( type == SelectType.Multi + mod._groups.Add( type == GroupType.Multi ? new MultiModGroup { Name = newName, Priority = maxPriority } : new SingleModGroup { Name = newName, Priority = maxPriority } ); ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1 ); diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index 184dc8cb..699fceaa 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -5,6 +5,7 @@ using System.Text; using Dalamud.Utility; using OtterGui.Classes; using OtterGui.Filesystem; +using Penumbra.Api.Enums; using Penumbra.GameData.ByteString; using Penumbra.Import; @@ -62,12 +63,12 @@ public partial class Mod } // Create a file for an option group from given data. - internal static void CreateOptionGroup( DirectoryInfo baseFolder, SelectType type, string name, + internal static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) { switch( type ) { - case SelectType.Multi: + case GroupType.Multi: { var group = new MultiModGroup() { @@ -80,7 +81,7 @@ public partial class Mod IModGroup.Save( group, baseFolder, index ); break; } - case SelectType.Single: + case GroupType.Single: { var group = new SingleModGroup() { diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index c9c8683b..3f6a79b4 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using OtterGui; +using Penumbra.Api.Enums; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -79,10 +80,10 @@ public partial class Mod try { var json = JObject.Parse( File.ReadAllText( file.FullName ) ); - switch( json[ nameof( Type ) ]?.ToObject< SelectType >() ?? SelectType.Single ) + switch( json[ nameof( Type ) ]?.ToObject< GroupType >() ?? GroupType.Single ) { - case SelectType.Multi: return MultiModGroup.Load( mod, json, groupIdx ); - case SelectType.Single: return SingleModGroup.Load( mod, json, groupIdx ); + case GroupType.Multi: return MultiModGroup.Load( mod, json, groupIdx ); + case GroupType.Single: return SingleModGroup.Load( mod, json, groupIdx ); } } catch( Exception e ) diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index f6bba3a3..69e33628 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; +using Penumbra.Api.Enums; using Penumbra.GameData.ByteString; namespace Penumbra.Mods; @@ -128,7 +129,7 @@ public sealed partial class Mod switch( group.SelectionType ) { - case SelectType.Multi: + case GroupType.Multi: var optionPriority = 0; var newMultiGroup = new MultiModGroup() @@ -144,7 +145,7 @@ public sealed partial class Mod } break; - case SelectType.Single: + case GroupType.Single: if( group.Options.Count == 1 ) { AddFilesToSubMod( mod._default, mod.ModPath, group.Options[ 0 ], seenMetaFiles ); @@ -209,7 +210,7 @@ public sealed partial class Mod public string GroupName = string.Empty; [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType = SelectType.Single; + public GroupType SelectionType = GroupType.Single; public List< OptionV0 > Options = new(); diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 1060504c..c8cbdf22 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -121,8 +122,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable CreateLeaf( Root, name, mod ); break; case ModPathChangeType.Deleted: - var leaf = Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< Leaf >().FirstOrDefault( l => l.Value == mod ); - if( leaf != null ) + if( FindLeaf( mod, out var leaf ) ) { Delete( leaf ); } @@ -137,6 +137,16 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable } } + // Search the entire filesystem for the leaf corresponding to a mod. + public bool FindLeaf( Mod mod, [NotNullWhen( true )] out Leaf? leaf ) + { + leaf = Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) + .OfType< Leaf >() + .FirstOrDefault( l => l.Value == mod ); + return leaf != null; + } + + // Used for saving and loading. private static string ModToIdentifier( Mod mod ) => mod.ModPath.Name; @@ -144,15 +154,16 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable private static string ModToName( Mod mod ) => mod.Name.Text.FixName(); - private static (string, bool) SaveMod( Mod mod, string fullPath ) + // Return whether a mod has a custom path or is just a numbered default path. + public static bool ModHasDefaultPath( Mod mod, string fullPath ) { var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?$" ); - // Only save pairs with non-default paths. - if( regex.IsMatch( fullPath ) ) - { - return ( string.Empty, false ); - } - - return ( ModToIdentifier( mod ), true ); + return regex.IsMatch( fullPath ); } + + private static (string, bool) SaveMod( Mod mod, string fullPath ) + // Only save pairs with non-default paths. + => ModHasDefaultPath( mod, fullPath ) + ? ( string.Empty, false ) + : ( ModToIdentifier( mod ), true ); } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index fb2d305a..0c1ebf2f 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -3,22 +3,17 @@ using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using OtterGui.Filesystem; +using Penumbra.Api.Enums; namespace Penumbra.Mods; -public enum SelectType -{ - Single, - Multi, -} - public interface IModGroup : IEnumerable< ISubMod > { public const int MaxMultiOptions = 32; public string Name { get; } public string Description { get; } - public SelectType Type { get; } + public GroupType Type { get; } public int Priority { get; } public uint DefaultSettings { get; set; } @@ -31,8 +26,8 @@ public interface IModGroup : IEnumerable< ISubMod > public bool IsOption => Type switch { - SelectType.Single => Count > 1, - SelectType.Multi => Count > 0, + GroupType.Single => Count > 1, + GroupType.Multi => Count > 0, _ => false, }; @@ -90,7 +85,7 @@ public interface IModGroup : IEnumerable< ISubMod > j.WriteStartArray(); for( var idx = 0; idx < group.Count; ++idx ) { - ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == SelectType.Multi ? group.OptionPriority( idx ) : null ); + ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == GroupType.Multi ? group.OptionPriority( idx ) : null ); } j.WriteEndArray(); @@ -98,7 +93,7 @@ public interface IModGroup : IEnumerable< ISubMod > Penumbra.Log.Debug( $"Saved group file {file} for group {groupIdx + 1}: {group.Name}." ); } - public IModGroup Convert( SelectType type ); + public IModGroup Convert( GroupType type ); public bool MoveOption( int optionIdxFrom, int optionIdxTo ); public void UpdatePositions( int from = 0 ); } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 91bc304b..fe7b1173 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Api.Enums; namespace Penumbra.Mods; @@ -15,8 +16,8 @@ public partial class Mod // Groups that allow all available options to be selected at once. private sealed class MultiModGroup : IModGroup { - public SelectType Type - => SelectType.Multi; + public GroupType Type + => GroupType.Multi; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; @@ -79,12 +80,12 @@ public partial class Mod return ret; } - public IModGroup Convert( SelectType type ) + public IModGroup Convert( GroupType type ) { switch( type ) { - case SelectType.Multi: return this; - case SelectType.Single: + case GroupType.Multi: return this; + case GroupType.Single: var multi = new SingleModGroup() { Name = Name, diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index 3de02d5c..cfec230e 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Api.Enums; namespace Penumbra.Mods; @@ -14,8 +15,8 @@ public partial class Mod // Groups that allow only one of their available options to be selected. private sealed class SingleModGroup : IModGroup { - public SelectType Type - => SelectType.Single; + public GroupType Type + => GroupType.Single; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; @@ -72,12 +73,12 @@ public partial class Mod return ret; } - public IModGroup Convert( SelectType type ) + public IModGroup Convert( GroupType type ) { switch( type ) { - case SelectType.Single: return this; - case SelectType.Multi: + case GroupType.Single: return this; + case GroupType.Multi: var multi = new MultiModGroup() { Name = Name, diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 6e8c7343..7b2a23ab 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using OtterGui; using OtterGui.Filesystem; +using Penumbra.Api.Enums; namespace Penumbra.Mods; @@ -56,8 +57,8 @@ public class ModSettings var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), - SelectType.Multi => 1u << ( int )config, + GroupType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), + GroupType.Multi => 1u << ( int )config, _ => config, }; return config != Settings[ groupIdx ]; @@ -70,8 +71,8 @@ public class ModSettings var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, - SelectType.Multi => Functions.RemoveBit( config, optionIdx ), + GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, + GroupType.Multi => Functions.RemoveBit( config, optionIdx ), _ => config, }; return config != Settings[ groupIdx ]; @@ -87,8 +88,8 @@ public class ModSettings var config = Settings[ groupIdx ]; Settings[ groupIdx ] = group.Type switch { - SelectType.Single => config == optionIdx ? ( uint )movedToIdx : config, - SelectType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), + GroupType.Single => config == optionIdx ? ( uint )movedToIdx : config, + GroupType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), _ => config, }; return config != Settings[ groupIdx ]; @@ -101,8 +102,8 @@ public class ModSettings private static uint FixSetting( IModGroup group, uint value ) => group.Type switch { - SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), - SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), + GroupType.Single => ( uint )Math.Min( value, group.Count - 1 ), + GroupType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), _ => value, }; @@ -202,7 +203,7 @@ public class ModSettings } var group = mod.Groups[ idx ]; - if( group.Type == SelectType.Single && setting < group.Count ) + if( group.Type == GroupType.Single && setting < group.Count ) { dict.Add( group.Name, new[] { group[ ( int )setting ].Name } ); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3c784c66..cd08a20b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -16,6 +16,7 @@ using OtterGui.Classes; using OtterGui.Log; using OtterGui.Widgets; using Penumbra.Api; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.Interop; using Penumbra.UI; @@ -57,16 +58,16 @@ public class Penumbra : IDalamudPlugin public static FrameworkManager Framework { get; private set; } = null!; public static int ImcExceptions = 0; - public readonly ResourceLogger ResourceLogger; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly PenumbraIpc Ipc; - private readonly ConfigWindow _configWindow; - private readonly LaunchButton _launchButton; - private readonly WindowSystem _windowSystem; - private readonly Changelog _changelog; + public readonly ResourceLogger ResourceLogger; + public readonly PathResolver PathResolver; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystem ModFileSystem; + public readonly PenumbraApi Api; + public readonly PenumbraIpcProviders IpcProviders; + private readonly ConfigWindow _configWindow; + private readonly LaunchButton _launchButton; + private readonly WindowSystem _windowSystem; + private readonly Changelog _changelog; internal WebServer? WebServer; @@ -95,9 +96,9 @@ public class Penumbra : IDalamudPlugin ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); CollectionManager.CreateNecessaryCaches(); - ModFileSystem = ModFileSystem.Load(); - ObjectReloader = new ObjectReloader(); - PathResolver = new PathResolver( ResourceLoader ); + ModFileSystem = ModFileSystem.Load(); + ObjectReloader = new ObjectReloader(); + PathResolver = new PathResolver( ResourceLoader ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -133,8 +134,8 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); } - Api = new PenumbraApi( this ); - Ipc = new PenumbraIpc( Dalamud.PluginInterface, Api ); + Api = new PenumbraApi( this ); + IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); SubscribeItemLinks(); if( ImcExceptions > 0 ) { @@ -279,7 +280,7 @@ public class Penumbra : IDalamudPlugin { ShutdownWebServer(); DisposeInterface(); - Ipc?.Dispose(); + IpcProviders?.Dispose(); Api?.Dispose(); ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 624558c1..b7ab0231 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -75,6 +75,7 @@ + diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 77c7dea1..1ccf0e3c 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -13,6 +13,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; +using Penumbra.Api.Enums; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 8ced65cb..c9cf6284 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -444,11 +444,11 @@ public partial class ConfigWindow { if( !ImGui.CollapsingHeader( "IPC" ) ) { - _window._penumbra.Ipc.Tester.UnsubscribeEvents(); + _window._penumbra.IpcProviders.Tester.UnsubscribeEvents(); return; } - _window._penumbra.Ipc.Tester.Draw(); + _window._penumbra.IpcProviders.Tester.Draw(); } // Helper to print a property and its value in a 2-column table. diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 5b302ade..e767f755 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -7,6 +7,7 @@ using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index e3113bcb..e5948ee8 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.Mods; namespace Penumbra.UI; @@ -236,7 +237,7 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, tt, !nameValid, true ) ) { - Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName ); + Penumbra.ModManager.AddModGroup( mod, GroupType.Single, _newGroupName ); Reset(); } } @@ -496,7 +497,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); - if( group.Type == SelectType.Single ) + if( group.Type == GroupType.Single ) { if( ImGui.RadioButton( "##default", group.DefaultSettings == optionIdx ) ) { @@ -532,7 +533,7 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - if( group.Type == SelectType.Multi ) + if( group.Type == GroupType.Multi ) { if( Input.Priority( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, 50 * ImGuiHelpers.GlobalScale ) ) @@ -564,7 +565,7 @@ public partial class ConfigWindow } ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[ groupIdx ].Type != SelectType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; + var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." @@ -636,11 +637,11 @@ public partial class ConfigWindow // Draw a combo to select single or multi group and switch between them. private void DrawGroupCombo( IModGroup group, int groupIdx ) { - static string GroupTypeName( SelectType type ) + static string GroupTypeName( GroupType type ) => type switch { - SelectType.Single => "Single Group", - SelectType.Multi => "Multi Group", + GroupType.Single => "Single Group", + GroupType.Multi => "Multi Group", _ => "Unknown", }; @@ -651,16 +652,16 @@ public partial class ConfigWindow return; } - if( ImGui.Selectable( GroupTypeName( SelectType.Single ), group.Type == SelectType.Single ) ) + if( ImGui.Selectable( GroupTypeName( GroupType.Single ), group.Type == GroupType.Single ) ) { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, SelectType.Single ); + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Single ); } var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; using var style = ImRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti ); - if( ImGui.Selectable( GroupTypeName( SelectType.Multi ), group.Type == SelectType.Multi ) && canSwitchToMulti ) + if( ImGui.Selectable( GroupTypeName( GroupType.Multi ), group.Type == GroupType.Multi ) && canSwitchToMulti ) { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, SelectType.Multi ); + Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Multi ); } style.Pop(); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index 6c5582b8..feacb0bc 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -154,7 +155,7 @@ public partial class ConfigWindow // If a description is provided, add a help marker besides it. private void DrawSingleGroup( IModGroup group, int groupIdx ) { - if( group.Type != SelectType.Single || !group.IsOption ) + if( group.Type != GroupType.Single || !group.IsOption ) { return; } @@ -193,7 +194,7 @@ public partial class ConfigWindow // If a description is provided, add a help marker in the title. private void DrawMultiGroup( IModGroup group, int groupIdx ) { - if( group.Type != SelectType.Multi || !group.IsOption ) + if( group.Type != GroupType.Multi || !group.IsOption ) { return; } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 35b01d19..ba88c75d 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; namespace Penumbra.UI; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 3b072b89..b68a4adc 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -58,26 +58,17 @@ "System.ValueTuple": "4.5.0" } }, - "DirectXTex": { - "type": "Project" - }, - "directxtexc": { - "type": "Project", - "dependencies": { - "DirectXTex": "[1.0.0, )" - } - }, "ottergui": { "type": "Project" }, - "ottertex": { - "type": "Project", - "dependencies": { - "DirectXTexC": "[1.0.0, )" - } + "penumbra.api": { + "type": "Project" }, "penumbra.gamedata": { - "type": "Project" + "type": "Project", + "dependencies": { + "Penumbra.Api": "[1.0.0, )" + } } } } diff --git a/tmp/.editorconfig b/tmp/.editorconfig new file mode 100644 index 00000000..238bb1dc --- /dev/null +++ b/tmp/.editorconfig @@ -0,0 +1,85 @@ + +[*] +charset=utf-8 +end_of_line=lf +trim_trailing_whitespace=true +insert_final_newline=false +indent_style=space +indent_size=4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers=false +csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_prefer_braces=true:none +csharp_space_after_cast=false +csharp_space_after_keywords_in_control_flow_statements=false +csharp_space_between_method_call_parameter_list_parentheses=true +csharp_space_between_method_declaration_parameter_list_parentheses=true +csharp_space_between_parentheses=control_flow_statements,expressions,type_casts +csharp_style_var_elsewhere=true:suggestion +csharp_style_var_for_built_in_types=true:suggestion +csharp_style_var_when_type_is_apparent=true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion +dotnet_style_predefined_type_for_member_access=true:suggestion +dotnet_style_qualification_for_event=false:suggestion +dotnet_style_qualification_for_field=false:suggestion +dotnet_style_qualification_for_method=false:suggestion +dotnet_style_qualification_for_property=false:suggestion +dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion + +# ReSharper properties +resharper_align_multiline_binary_expressions_chain=false +resharper_align_multiline_calls_chain=false +resharper_autodetect_indent_settings=true +resharper_braces_redundant=true +resharper_constructor_or_destructor_body=expression_body +resharper_csharp_empty_block_style=together +resharper_csharp_max_line_length=180 +resharper_csharp_space_within_array_access_brackets=true +resharper_enforce_line_ending_style=true +resharper_int_align_assignments=true +resharper_int_align_comments=true +resharper_int_align_fields=true +resharper_int_align_invocations=false +resharper_int_align_nested_ternary=true +resharper_int_align_properties=false +resharper_int_align_switch_expressions=true +resharper_int_align_switch_sections=true +resharper_int_align_variables=true +resharper_local_function_body=expression_body +resharper_method_or_operator_body=expression_body +resharper_place_attribute_on_same_line=false +resharper_space_after_cast=false +resharper_space_within_checked_parentheses=true +resharper_space_within_default_parentheses=true +resharper_space_within_nameof_parentheses=true +resharper_space_within_single_line_array_initializer_braces=true +resharper_space_within_sizeof_parentheses=true +resharper_space_within_typeof_parentheses=true +resharper_space_within_type_argument_angles=true +resharper_space_within_type_parameter_angles=true +resharper_use_indent_from_vs=false +resharper_wrap_lines=true + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting=hint +resharper_arrange_this_qualifier_highlighting=hint +resharper_arrange_type_member_modifiers_highlighting=hint +resharper_arrange_type_modifiers_highlighting=hint +resharper_built_in_type_reference_style_for_member_access_highlighting=hint +resharper_built_in_type_reference_style_highlighting=hint +resharper_redundant_base_qualifier_highlighting=warning +resharper_suggest_var_or_type_built_in_types_highlighting=hint +resharper_suggest_var_or_type_elsewhere_highlighting=hint +resharper_suggest_var_or_type_simple_types_highlighting=hint +resharper_web_config_module_not_resolved_highlighting=warning +resharper_web_config_type_not_resolved_highlighting=warning +resharper_web_config_wrong_module_highlighting=warning + +[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style=space +indent_size=4 +tab_width=4 diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 00000000..3e168525 --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.vs/ \ No newline at end of file diff --git a/tmp/Delegates.cs b/tmp/Delegates.cs new file mode 100644 index 00000000..97726e81 --- /dev/null +++ b/tmp/Delegates.cs @@ -0,0 +1,16 @@ +using System; +using Penumbra.Api.Enums; + +namespace Penumbra.Api; + +// Delegates used by different events. +public delegate void ChangedItemHover( object? item ); +public delegate void ChangedItemClick( MouseButton button, object? item ); +public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); +public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); + +public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, + IntPtr equipData ); + +public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, string collectionName, IntPtr drawObject ); +public delegate void GameObjectResourceResolvedDelegate( IntPtr gameObject, string gamePath, string localPath ); \ No newline at end of file diff --git a/tmp/Enums/ChangedItemType.cs b/tmp/Enums/ChangedItemType.cs new file mode 100644 index 00000000..5cc1b5a7 --- /dev/null +++ b/tmp/Enums/ChangedItemType.cs @@ -0,0 +1,9 @@ +namespace Penumbra.Api.Enums; + +public enum ChangedItemType +{ + None, + Item, + Action, + Customization, +} \ No newline at end of file diff --git a/tmp/Enums/GroupType.cs b/tmp/Enums/GroupType.cs new file mode 100644 index 00000000..65a8ed39 --- /dev/null +++ b/tmp/Enums/GroupType.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Api.Enums; + +public enum GroupType +{ + Single, + Multi, +} \ No newline at end of file diff --git a/tmp/Enums/ModSettingChange.cs b/tmp/Enums/ModSettingChange.cs new file mode 100644 index 00000000..5e556d50 --- /dev/null +++ b/tmp/Enums/ModSettingChange.cs @@ -0,0 +1,12 @@ +namespace Penumbra.Api.Enums; + +// Different types a mod setting can change: +public enum ModSettingChange +{ + Inheritance, // it was set to inherit from other collections or not inherit anymore + EnableState, // it was enabled or disabled + Priority, // its priority was changed + Setting, // a specific setting was changed + MultiInheritance, // multiple mods were set to inherit from other collections or not inherit anymore. + MultiEnableState, // multiple mods were enabled or disabled at once. +} \ No newline at end of file diff --git a/tmp/Enums/MouseButton.cs b/tmp/Enums/MouseButton.cs new file mode 100644 index 00000000..2917c0f8 --- /dev/null +++ b/tmp/Enums/MouseButton.cs @@ -0,0 +1,9 @@ +namespace Penumbra.Api.Enums; + +public enum MouseButton +{ + None, + Left, + Right, + Middle, +} \ No newline at end of file diff --git a/tmp/Enums/PenumbraApiEc.cs b/tmp/Enums/PenumbraApiEc.cs new file mode 100644 index 00000000..a37aefa8 --- /dev/null +++ b/tmp/Enums/PenumbraApiEc.cs @@ -0,0 +1,20 @@ +namespace Penumbra.Api.Enums; + +public enum PenumbraApiEc +{ + Success = 0, + NothingChanged = 1, + CollectionMissing = 2, + ModMissing = 3, + OptionGroupMissing = 4, + OptionMissing = 5, + + CharacterCollectionExists = 6, + LowerPriority = 7, + InvalidGamePath = 8, + FileMissing = 9, + InvalidManipulation = 10, + InvalidArgument = 11, + PathRenameFailed = 12, + UnknownError = 255, +} \ No newline at end of file diff --git a/Penumbra.GameData/Enums/RedrawType.cs b/tmp/Enums/RedrawType.cs similarity index 61% rename from Penumbra.GameData/Enums/RedrawType.cs rename to tmp/Enums/RedrawType.cs index 4b698377..0295554f 100644 --- a/Penumbra.GameData/Enums/RedrawType.cs +++ b/tmp/Enums/RedrawType.cs @@ -1,4 +1,4 @@ -namespace Penumbra.GameData.Enums; +namespace Penumbra.Api.Enums; public enum RedrawType { diff --git a/tmp/Helpers/ActionProvider.cs b/tmp/Helpers/ActionProvider.cs new file mode 100644 index 00000000..c070dca8 --- /dev/null +++ b/tmp/Helpers/ActionProvider.cs @@ -0,0 +1,66 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +public sealed class ActionProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public ActionProvider( DalamudPluginInterface pi, string label, Action action ) + { + try + { + _provider = pi.GetIpcProvider( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterAction( action ); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~ActionProvider() + => Dispose(); +} + +public sealed class ActionProvider< T1, T2 > : IDisposable +{ + private ICallGateProvider< T1, T2, object? >? _provider; + + public ActionProvider( DalamudPluginInterface pi, string label, Action< T1, T2 > action ) + { + try + { + _provider = pi.GetIpcProvider< T1, T2, object? >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterAction( action ); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~ActionProvider() + => Dispose(); +} \ No newline at end of file diff --git a/tmp/Helpers/ActionSubscriber.cs b/tmp/Helpers/ActionSubscriber.cs new file mode 100644 index 00000000..e924e4eb --- /dev/null +++ b/tmp/Helpers/ActionSubscriber.cs @@ -0,0 +1,54 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +public readonly struct ActionSubscriber< T1 > +{ + private readonly ICallGateSubscriber< T1, object? >? _subscriber; + + public bool Valid + => _subscriber != null; + + public ActionSubscriber( DalamudPluginInterface pi, string label ) + { + try + { + _subscriber = pi.GetIpcSubscriber< T1, object? >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Invoke( T1 a ) + => _subscriber?.InvokeAction( a ); +} + +public readonly struct ActionSubscriber< T1, T2 > +{ + private readonly ICallGateSubscriber< T1, T2, object? >? _subscriber; + + public bool Valid + => _subscriber != null; + + public ActionSubscriber( DalamudPluginInterface pi, string label ) + { + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, object? >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Invoke( T1 a, T2 b ) + => _subscriber?.InvokeAction( a, b ); +} \ No newline at end of file diff --git a/tmp/Helpers/EventProvider.cs b/tmp/Helpers/EventProvider.cs new file mode 100644 index 00000000..b623d41e --- /dev/null +++ b/tmp/Helpers/EventProvider.cs @@ -0,0 +1,376 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +public sealed class EventProvider : IDisposable +{ + private ICallGateProvider< object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, (Action< Action > Add, Action< Action > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke() + => _provider?.SendMessage(); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} + +public sealed class EventProvider< T1 > : IDisposable +{ + private ICallGateProvider< T1, object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, (Action< Action< T1 > > Add, Action< Action< T1 > > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke( T1 a ) + => _provider?.SendMessage( a ); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action< T1 > > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} + +public sealed class EventProvider< T1, T2 > : IDisposable +{ + private ICallGateProvider< T1, T2, object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, + (Action< Action< T1, T2 > > Add, Action< Action< T1, T2 > > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke( T1 a, T2 b ) + => _provider?.SendMessage( a, b ); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action< T1, T2 > > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} + +public sealed class EventProvider< T1, T2, T3 > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, + (Action< Action< T1, T2, T3 > > Add, Action< Action< T1, T2, T3 > > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke( T1 a, T2 b, T3 c ) + => _provider?.SendMessage( a, b, c ); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action< T1, T2, T3 > > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} + +public sealed class EventProvider< T1, T2, T3, T4 > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, T4, object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, + (Action< Action< T1, T2, T3, T4 > > Add, Action< Action< T1, T2, T3, T4 > > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke( T1 a, T2 b, T3 c, T4 d ) + => _provider?.SendMessage( a, b, c, d ); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action< T1, T2, T3, T4 > > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} + +public sealed class EventProvider< T1, T2, T3, T4, T5 > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, T4, T5, object? >? _provider; + private Delegate? _unsubscriber; + + public EventProvider( DalamudPluginInterface pi, string label, + (Action< Action< T1, T2, T3, T4, T5 > > Add, Action< Action< T1, T2, T3, T4, T5 > > Del)? subscribe = null ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, object? >( label ); + subscribe?.Add( Invoke ); + _unsubscriber = subscribe?.Del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public EventProvider( DalamudPluginInterface pi, string label, Action add, Action del ) + { + _unsubscriber = null; + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, object? >( label ); + add(); + _unsubscriber = del; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + } + + public void Invoke( T1 a, T2 b, T3 c, T4 d, T5 e ) + => _provider?.SendMessage( a, b, c, d, e ); + + public void Dispose() + { + switch( _unsubscriber ) + { + case Action< Action< T1, T2, T3, T4, T5 > > a: + a( Invoke ); + break; + case Action b: + b(); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize( this ); + } + + ~EventProvider() + => Dispose(); +} \ No newline at end of file diff --git a/tmp/Helpers/EventSubscriber.cs b/tmp/Helpers/EventSubscriber.cs new file mode 100644 index 00000000..0df6bc11 --- /dev/null +++ b/tmp/Helpers/EventSubscriber.cs @@ -0,0 +1,607 @@ +using System; +using System.Collections.Generic; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action, Action > _delegates = new(); + private ICallGateSubscriber< object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action() + { + try + { + value(); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +public sealed class EventSubscriber< T1 > : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action< T1 >, Action< T1 > > _delegates = new(); + private ICallGateSubscriber< T1, object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1 >[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action< T1 > Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action( T1 a ) + { + try + { + value( a ); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +public sealed class EventSubscriber< T1, T2 > : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action< T1, T2 >, Action< T1, T2 > > _delegates = new(); + private ICallGateSubscriber< T1, T2, object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2 >[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action< T1, T2 > Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action( T1 a, T2 b ) + { + try + { + value( a, b ); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +public sealed class EventSubscriber< T1, T2, T3 > : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action< T1, T2, T3 >, Action< T1, T2, T3 > > _delegates = new(); + private ICallGateSubscriber< T1, T2, T3, object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3 >[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action< T1, T2, T3 > Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action( T1 a, T2 b, T3 c ) + { + try + { + value( a, b, c ); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +public sealed class EventSubscriber< T1, T2, T3, T4 > : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action< T1, T2, T3, T4 >, Action< T1, T2, T3, T4 > > _delegates = new(); + private ICallGateSubscriber< T1, T2, T3, T4, object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3, T4 >[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action< T1, T2, T3, T4 > Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action( T1 a, T2 b, T3 c, T4 d ) + { + try + { + value( a, b, c, d ); + } + catch( Exception e ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{e}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +public sealed class EventSubscriber< T1, T2, T3, T4, T5 > : IDisposable +{ + private readonly string _label; + private readonly Dictionary< Action< T1, T2, T3, T4, T5 >, Action< T1, T2, T3, T4, T5 > > _delegates = new(); + private ICallGateSubscriber< T1, T2, T3, T4, T5, object? >? _subscriber; + private bool _disabled; + + public EventSubscriber( DalamudPluginInterface pi, string label, params Action< T1, T2, T3, T4, T5 >[] actions ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, T5, object? >( label ); + foreach( var action in actions ) + { + Event += action; + } + + _disabled = false; + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public void Enable() + { + if( _disabled && _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Subscribe( action ); + } + + _disabled = false; + } + } + + public void Disable() + { + if( !_disabled ) + { + if( _subscriber != null ) + { + foreach( var action in _delegates.Keys ) + { + _subscriber.Unsubscribe( action ); + } + } + + _disabled = true; + } + } + + public event Action< T1, T2, T3, T4, T5 > Event + { + add + { + if( _subscriber != null && !_delegates.ContainsKey( value ) ) + { + void Action( T1 a, T2 b, T3 c, T4 d, T5 e ) + { + try + { + value( a, b, c, d, e ); + } + catch( Exception ex ) + { + PluginLog.Error( $"Exception invoking IPC event {_label}:\n{ex}" ); + } + } + + if( _delegates.TryAdd( value, Action ) && !_disabled ) + { + _subscriber.Subscribe( Action ); + } + } + } + remove + { + if( _subscriber != null && _delegates.Remove( value, out var action ) ) + { + _subscriber.Unsubscribe( action ); + } + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} \ No newline at end of file diff --git a/tmp/Helpers/FuncProvider.cs b/tmp/Helpers/FuncProvider.cs new file mode 100644 index 00000000..fac61ce3 --- /dev/null +++ b/tmp/Helpers/FuncProvider.cs @@ -0,0 +1,186 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +public sealed class FuncProvider< TRet > : IDisposable +{ + private ICallGateProvider< TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} + +public sealed class FuncProvider< T1, TRet > : IDisposable +{ + private ICallGateProvider< T1, TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< T1, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} + +public sealed class FuncProvider< T1, T2, TRet > : IDisposable +{ + private ICallGateProvider< T1, T2, TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< T1, T2, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} + +public sealed class FuncProvider< T1, T2, T3, TRet > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} + +public sealed class FuncProvider< T1, T2, T3, T4, TRet > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, T4, TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, T4, TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} + +public sealed class FuncProvider< T1, T2, T3, T4, T5, TRet > : IDisposable +{ + private ICallGateProvider< T1, T2, T3, T4, T5, TRet >? _provider; + + public FuncProvider( DalamudPluginInterface pi, string label, Func< T1, T2, T3, T4, T5, TRet > func ) + { + try + { + _provider = pi.GetIpcProvider< T1, T2, T3, T4, T5, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Provider for {label}\n{e}" ); + _provider = null; + } + + _provider?.RegisterFunc( func ); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize( this ); + } + + ~FuncProvider() + => Dispose(); +} \ No newline at end of file diff --git a/tmp/Helpers/FuncSubscriber.cs b/tmp/Helpers/FuncSubscriber.cs new file mode 100644 index 00000000..3f8cbdda --- /dev/null +++ b/tmp/Helpers/FuncSubscriber.cs @@ -0,0 +1,163 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace Penumbra.Api.Helpers; + +public readonly struct FuncSubscriber< TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke() + => _subscriber != null ? _subscriber.InvokeFunc() : throw new IpcNotReadyError( _label ); +} + +public readonly struct FuncSubscriber< T1, TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< T1, TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke( T1 a ) + => _subscriber != null ? _subscriber.InvokeFunc( a ) : throw new IpcNotReadyError( _label ); +} + +public readonly struct FuncSubscriber< T1, T2, TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< T1, T2, TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke( T1 a, T2 b ) + => _subscriber != null ? _subscriber.InvokeFunc( a, b ) : throw new IpcNotReadyError( _label ); +} + +public readonly struct FuncSubscriber< T1, T2, T3, TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< T1, T2, T3, TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke( T1 a, T2 b, T3 c ) + => _subscriber != null ? _subscriber.InvokeFunc( a, b, c ) : throw new IpcNotReadyError( _label ); +} + +public readonly struct FuncSubscriber< T1, T2, T3, T4, TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< T1, T2, T3, T4, TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke( T1 a, T2 b, T3 c, T4 d ) + => _subscriber != null ? _subscriber.InvokeFunc( a, b, c, d ) : throw new IpcNotReadyError( _label ); +} + +public readonly struct FuncSubscriber< T1, T2, T3, T4, T5, TRet > +{ + private readonly string _label; + private readonly ICallGateSubscriber< T1, T2, T3, T4, T5, TRet >? _subscriber; + + public bool Valid + => _subscriber != null; + + public FuncSubscriber( DalamudPluginInterface pi, string label ) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber< T1, T2, T3, T4, T5, TRet >( label ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC Subscriber for {label}\n{e}" ); + _subscriber = null; + } + } + + public TRet Invoke( T1 a, T2 b, T3 c, T4 d, T5 e ) + => _subscriber != null ? _subscriber.InvokeFunc( a, b, c, d, e ) : throw new IpcNotReadyError( _label ); +} \ No newline at end of file diff --git a/Penumbra/Api/IPenumbraApi.cs b/tmp/IPenumbraApi.cs similarity index 86% rename from Penumbra/Api/IPenumbraApi.cs rename to tmp/IPenumbraApi.cs index 257cfbde..51860312 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/tmp/IPenumbraApi.cs @@ -1,63 +1,28 @@ using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.Mods; using System; using System.Collections.Generic; +using Penumbra.Api.Enums; namespace Penumbra.Api; -public interface IPenumbraApiBase -{ - // The API version is staggered in two parts. - // The major/Breaking version only increments if there are changes breaking backwards compatibility. - // The minor/Feature version increments any time there is something added - // and resets when Breaking is incremented. - public (int Breaking, int Feature) ApiVersion { get; } - public bool Valid { get; } -} - -public delegate void ChangedItemHover( object? item ); -public delegate void ChangedItemClick( MouseButton button, object? item ); -public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ); -public delegate void ModSettingChanged( ModSettingChange type, string collectionName, string modDirectory, bool inherited ); - -public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, - IntPtr equipData ); - -public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr drawObject ); -public delegate void GameObjectResourceResolvedDelegate( IntPtr gameObject, string gamePath, string localPath ); - -public enum PenumbraApiEc -{ - Success = 0, - NothingChanged = 1, - CollectionMissing = 2, - ModMissing = 3, - OptionGroupMissing = 4, - OptionMissing = 5, - - CharacterCollectionExists = 6, - LowerPriority = 7, - InvalidGamePath = 8, - FileMissing = 9, - InvalidManipulation = 10, - InvalidArgument = 11, - UnknownError = 255, -} - public interface IPenumbraApi : IPenumbraApiBase { + #region Game State + // Obtain the currently set mod directory from the configuration. public string GetModDirectory(); + // Obtain the entire current penumbra configuration as a json encoded string. + public string GetConfiguration(); + // Fired whenever a mod directory change is finished. // Gives the full path of the mod directory and whether Penumbra treats it as valid. public event Action< string, bool >? ModDirectoryChanged; - // Obtain the entire current penumbra configuration as a json encoded string. - public string GetConfiguration(); + #endregion + + #region UI // Triggered when the user hovers over a listed changed object in a mod tab. // Can be used to append tooltips. @@ -70,8 +35,36 @@ public interface IPenumbraApi : IPenumbraApiBase // Triggered when the user clicks a listed changed object in a mod tab. public event ChangedItemClick? ChangedItemClicked; + + #endregion + + #region Redrawing + + // Queue redrawing of all actors of the given name with the given RedrawType. + public void RedrawObject( string name, 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 ); + + // 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 all currently available actors with the given RedrawType. + public void RedrawAll( RedrawType setting ); + + // Triggered whenever a game object is redrawn via Penumbra. public event GameObjectRedrawn? GameObjectRedrawn; + #endregion + + #region Game State + + // Obtain the game object associated with a given draw object and the name of the collection associated with this game object. + public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); + + // Obtain the parent game object index for an unnamed cutscene actor by its index. + public int GetCutsceneParentIndex( int actor ); + // Triggered when a character base is created and a corresponding gameObject could be found, // before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand. public event CreatingCharacterBaseDelegate? CreatingCharacterBase; @@ -84,17 +77,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Does not trigger if the resource is not requested for a known game object. public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - // Queue redrawing of all actors of the given name with the given RedrawType. - public void RedrawObject( string name, RedrawType setting ); + #endregion - // 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 ); - - // 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 all currently available actors with the given RedrawType. - public void RedrawAll( RedrawType setting ); + #region Resolving // Resolve a given gamePath via Penumbra using the Default collection. // Returns the given gamePath if penumbra would not manipulate it. @@ -125,8 +110,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource; - // Gets a dictionary of effected items from a collection - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ); + #endregion + + #region Collections // Obtain a list of the names of all currently installed collections. public IList< string > GetCollections(); @@ -143,11 +129,24 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the name of the collection associated with characterName and whether it is configured or inferred from default. public (string, bool) GetCharacterCollection( string characterName ); - // Obtain the game object associated with a given draw object and the name of the collection associated with this game object. - public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); + // Gets a dictionary of effected items from a collection + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ); - // Obtain the parent game object index for an unnamed cutscene actor by its index. - public int GetCutsceneParentIndex( int actor ); + #endregion + + #region Meta + + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + // for the collection currently associated with the player. + public string GetPlayerMetaManipulations(); + + // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + // for the given collection associated with the character name, or the default collection. + public string GetMetaManipulations( string characterName ); + + #endregion + + #region Mods // 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(); @@ -162,20 +161,29 @@ public interface IPenumbraApi : IPenumbraApiBase // Note that success does only imply a successful call, not a successful mod load. public PenumbraApiEc AddMod( string modDirectory ); - // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations - // for the collection currently associated with the player. - public string GetPlayerMetaManipulations(); + // Try to delete a mod given by its modDirectory or its name. + // Returns NothingDone if the mod can not be found or success otherwise. + // Note that success does only imply a successful call, not successful deletion. + public PenumbraApiEc DeleteMod( string modDirectory, string modName ); - // Obtain a base64 encoded, zipped json-string with a prepended version-byte of the current manipulations - // for the given collection associated with the character name, or the default collection. - public string GetMetaManipulations( string characterName ); + // Get the internal full filesystem path including search order for the specified mod. + // If success is returned, the second return value contains the full path + // and a bool indicating whether this is the default path (false) or a manually set one (true). + // Can return ModMissing or Success. + public (PenumbraApiEc, string, bool) GetModPath( string modDirectory, string modName ); + // Set the internal search order and filesystem path of the specified mod to the given path. + // Returns InvalidArgument if newPath is empty, ModMissing if the mod can not be found, + // PathRenameFailed if newPath could not be set and Success otherwise. + public PenumbraApiEc SetModPath( string modDirectory, string modName, string newPath ); - // ############## Mod Settings ################# + #endregion + + #region 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 IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); + public IDictionary< string, (IList< string >, GroupType) >? 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. @@ -207,6 +215,10 @@ public interface IPenumbraApi : IPenumbraApiBase // This event gets fired when any setting in any collection changes. public event ModSettingChanged? ModSettingChanged; + #endregion + + #region Temporary + // Create a temporary collection without actual settings but with a cache. // If no character collection for this character exists or forceOverwriteCharacter is true, // associate this collection to a specific character. @@ -233,4 +245,6 @@ public interface IPenumbraApi : IPenumbraApiBase // Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary. // Can return Okay or NothingDone. public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ); + + #endregion } \ No newline at end of file diff --git a/tmp/IPenumbraApiBase.cs b/tmp/IPenumbraApiBase.cs new file mode 100644 index 00000000..e4c452b4 --- /dev/null +++ b/tmp/IPenumbraApiBase.cs @@ -0,0 +1,11 @@ +namespace Penumbra.Api; + +public interface IPenumbraApiBase +{ + // The API version is staggered in two parts. + // The major/Breaking version only increments if there are changes breaking backwards compatibility. + // The minor/Feature version increments any time there is something added + // and resets when Breaking is incremented. + public (int Breaking, int Feature) ApiVersion { get; } + public bool Valid { get; } +} \ No newline at end of file diff --git a/tmp/Ipc/Collection.cs b/tmp/Ipc/Collection.cs new file mode 100644 index 00000000..3c40cd17 --- /dev/null +++ b/tmp/Ipc/Collection.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class GetCollections + { + public const string Label = $"Penumbra.{nameof( GetCollections )}"; + + public static FuncProvider< IList< string > > Provider( DalamudPluginInterface pi, Func< IList< string > > func ) + => new(pi, Label, func); + + public static FuncSubscriber< IList< string > > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetCurrentCollectionName + { + public const string Label = $"Penumbra.{nameof( GetCurrentCollectionName )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetDefaultCollectionName + { + public const string Label = $"Penumbra.{nameof( GetDefaultCollectionName )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetInterfaceCollectionName + { + public const string Label = $"Penumbra.{nameof( GetInterfaceCollectionName )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetCharacterCollectionName + { + public const string Label = $"Penumbra.{nameof( GetCharacterCollectionName )}"; + + public static FuncProvider< string, (string, bool) > Provider( DalamudPluginInterface pi, Func< string, (string, bool) > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, (string, bool) > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetChangedItems + { + public const string Label = $"Penumbra.{nameof( GetChangedItems )}"; + + public static FuncProvider< string, IReadOnlyDictionary< string, object? > > Provider( DalamudPluginInterface pi, + Func< string, IReadOnlyDictionary< string, object? > > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, IReadOnlyDictionary< string, object? > > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Configuration.cs b/tmp/Ipc/Configuration.cs new file mode 100644 index 00000000..67033458 --- /dev/null +++ b/tmp/Ipc/Configuration.cs @@ -0,0 +1,42 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class GetModDirectory + { + public const string Label = $"Penumbra.{nameof( GetModDirectory )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetConfiguration + { + public const string Label = $"Penumbra.{nameof( GetConfiguration )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ModDirectoryChanged + { + public const string Label = $"Penumbra.{nameof( ModDirectoryChanged )}"; + + public static EventProvider< string, bool > Provider( DalamudPluginInterface pi, + Action< Action< string, bool > > sub, Action< Action< string, bool > > unsub ) + => new(pi, Label, ( sub, unsub )); + + public static EventSubscriber< string, bool > Subscriber( DalamudPluginInterface pi, params Action< string, bool >[] actions ) + => new(pi, Label, actions); + } +} \ No newline at end of file diff --git a/tmp/Ipc/GameState.cs b/tmp/Ipc/GameState.cs new file mode 100644 index 00000000..89889fda --- /dev/null +++ b/tmp/Ipc/GameState.cs @@ -0,0 +1,63 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class GetDrawObjectInfo + { + public const string Label = $"Penumbra.{nameof( GetDrawObjectInfo )}"; + + public static FuncProvider< nint, (nint, string) > Provider( DalamudPluginInterface pi, Func< nint, (nint, string) > func ) + => new(pi, Label, func); + + public static FuncSubscriber< nint, (nint, string) > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetCutsceneParentIndex + { + public const string Label = $"Penumbra.{nameof( GetCutsceneParentIndex )}"; + + public static FuncProvider< int, int > Provider( DalamudPluginInterface pi, Func< int, int > func ) + => new(pi, Label, func); + + public static FuncSubscriber< int, int > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class CreatingCharacterBase + { + public const string Label = $"Penumbra.{nameof( CreatingCharacterBase )}"; + + public static EventProvider< nint, string, nint, nint, nint > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< nint, string, nint, nint, nint > Subscriber( DalamudPluginInterface pi, params Action< nint, string, nint, nint, nint >[] actions ) + => new(pi, Label, actions); + } + + public static class CreatedCharacterBase + { + public const string Label = $"Penumbra.{nameof( CreatedCharacterBase )}"; + + public static EventProvider< nint, string, nint > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< nint, string, nint > Subscriber( DalamudPluginInterface pi, params Action< nint, string, nint >[] actions ) + => new(pi, Label, actions); + } + + public static class GameObjectResourcePathResolved + { + public const string Label = $"Penumbra.{nameof( GameObjectResourcePathResolved )}"; + + public static EventProvider< nint, string, string > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< nint, string, string > Subscriber( DalamudPluginInterface pi, params Action< nint, string, string >[] actions ) + => new(pi, Label, actions); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Meta.cs b/tmp/Ipc/Meta.cs new file mode 100644 index 00000000..ef889f43 --- /dev/null +++ b/tmp/Ipc/Meta.cs @@ -0,0 +1,30 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class GetPlayerMetaManipulations + { + public const string Label = $"Penumbra.{nameof( GetPlayerMetaManipulations )}"; + + public static FuncProvider< string > Provider( DalamudPluginInterface pi, Func< string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetMetaManipulations + { + public const string Label = $"Penumbra.{nameof( GetMetaManipulations )}"; + + public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/ModSettings.cs b/tmp/Ipc/ModSettings.cs new file mode 100644 index 00000000..8a0e9cf8 --- /dev/null +++ b/tmp/Ipc/ModSettings.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +using CurrentSettings = ValueTuple< PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)? >; + +public static partial class Ipc +{ + public static class GetAvailableModSettings + { + public const string Label = $"Penumbra.{nameof( GetAvailableModSettings )}"; + + public static FuncProvider< string, string, IDictionary< string, (IList< string >, GroupType) >? > Provider( + DalamudPluginInterface pi, Func< string, string, IDictionary< string, (IList< string >, GroupType) >? > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, IDictionary< string, (IList< string >, GroupType) >? > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetCurrentModSettings + { + public const string Label = $"Penumbra.{nameof( GetCurrentModSettings )}"; + + public static FuncProvider< string, string, string, bool, CurrentSettings > Provider( DalamudPluginInterface pi, + Func< string, string, string, bool, CurrentSettings > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, bool, CurrentSettings > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class TryInheritMod + { + public const string Label = $"Penumbra.{nameof( TryInheritMod )}"; + + public static FuncProvider< string, string, string, bool, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, string, bool, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, bool, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class TrySetMod + { + public const string Label = $"Penumbra.{nameof( TrySetMod )}"; + + public static FuncProvider< string, string, string, bool, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, string, bool, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, bool, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class TrySetModPriority + { + public const string Label = $"Penumbra.{nameof( TrySetModPriority )}"; + + public static FuncProvider< string, string, string, int, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, string, int, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, int, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class TrySetModSetting + { + public const string Label = $"Penumbra.{nameof( TrySetModSetting )}"; + + public static FuncProvider< string, string, string, string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, string, string, string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, string, string, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class TrySetModSettings + { + public const string Label = $"Penumbra.{nameof( TrySetModSettings )}"; + + public static FuncProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > Provider( + DalamudPluginInterface pi, + Func< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ModSettingChanged + { + public const string Label = $"Penumbra.{nameof( ModSettingChanged )}"; + + public static EventProvider< ModSettingChange, string, string, bool > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< ModSettingChange, string, string, bool > Subscriber( DalamudPluginInterface pi, + params Action< ModSettingChange, string, string, bool >[] actions ) + => new(pi, Label, actions); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Mods.cs b/tmp/Ipc/Mods.cs new file mode 100644 index 00000000..d5d09036 --- /dev/null +++ b/tmp/Ipc/Mods.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class GetMods + { + public const string Label = $"Penumbra.{nameof( GetMods )}"; + + public static FuncProvider< IList< (string, string) > > Provider( DalamudPluginInterface pi, Func< IList< (string, string) > > func ) + => new(pi, Label, func); + + public static FuncSubscriber< IList< (string, string) > > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ReloadMod + { + public const string Label = $"Penumbra.{nameof( ReloadMod )}"; + + public static FuncProvider< string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class AddMod + { + public const string Label = $"Penumbra.{nameof( AddMod )}"; + + public static FuncProvider< string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class DeleteMod + { + public const string Label = $"Penumbra.{nameof( DeleteMod )}"; + + public static FuncProvider< string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GetModPath + { + public const string Label = $"Penumbra.{nameof( GetModPath )}"; + + public static FuncProvider< string, string, (PenumbraApiEc, string, bool) > Provider( DalamudPluginInterface pi, + Func< string, string, (PenumbraApiEc, string, bool) > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, (PenumbraApiEc, string, bool) > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class SetModPath + { + public const string Label = $"Penumbra.{nameof( SetModPath )}"; + + public static FuncProvider< string, string, string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, string, string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/PluginState.cs b/tmp/Ipc/PluginState.cs new file mode 100644 index 00000000..500bf1ee --- /dev/null +++ b/tmp/Ipc/PluginState.cs @@ -0,0 +1,68 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class Initialized + { + public const string Label = $"Penumbra.{nameof( Initialized )}"; + + public static EventProvider Provider( DalamudPluginInterface pi ) + => new(pi, Label); + + public static EventSubscriber Subscriber( DalamudPluginInterface pi, params Action[] actions ) + { + var ret = new EventSubscriber( pi, Label ); + foreach( var action in actions ) + { + ret.Event += action; + } + + return ret; + } + } + + public static class Disposed + { + public const string Label = $"Penumbra.{nameof( Disposed )}"; + + public static EventProvider Provider( DalamudPluginInterface pi ) + => new(pi, Label); + + public static EventSubscriber Subscriber( DalamudPluginInterface pi, params Action[] actions ) + { + var ret = new EventSubscriber( pi, Label ); + foreach( var action in actions ) + { + ret.Event += action; + } + + return ret; + } + } + + public static class ApiVersion + { + public const string Label = $"Penumbra.{nameof( ApiVersion )}"; + + public static FuncProvider< int > Provider( DalamudPluginInterface pi, Func< int > func ) + => new(pi, Label, func); + + public static FuncSubscriber< int > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ApiVersions + { + public const string Label = $"Penumbra.{nameof( ApiVersions )}"; + + public static FuncProvider< (int Breaking, int Features) > Provider( DalamudPluginInterface pi, Func< (int, int) > func ) + => new(pi, Label, func); + + public static FuncSubscriber< (int Breaking, int Features) > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Redraw.cs b/tmp/Ipc/Redraw.cs new file mode 100644 index 00000000..396dfe8a --- /dev/null +++ b/tmp/Ipc/Redraw.cs @@ -0,0 +1,65 @@ +using System; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class RedrawAll + { + public const string Label = $"Penumbra.{nameof( RedrawAll )}"; + + public static ActionProvider< RedrawType > Provider( DalamudPluginInterface pi, Action< RedrawType > action ) + => new(pi, Label, action); + + public static ActionSubscriber< RedrawType > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RedrawObject + { + public const string Label = $"Penumbra.{nameof( RedrawObject )}"; + + public static ActionProvider< GameObject, RedrawType > Provider( DalamudPluginInterface pi, Action< GameObject, RedrawType > action ) + => new(pi, Label, action); + + public static ActionSubscriber< GameObject, RedrawType > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RedrawObjectByIndex + { + public const string Label = $"Penumbra.{nameof( RedrawObjectByIndex )}"; + + public static ActionProvider< int, RedrawType > Provider( DalamudPluginInterface pi, Action< int, RedrawType > action ) + => new(pi, Label, action); + + public static ActionSubscriber< int, RedrawType > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RedrawObjectByName + { + public const string Label = $"Penumbra.{nameof( RedrawObjectByName )}"; + + public static ActionProvider< string, RedrawType > Provider( DalamudPluginInterface pi, Action< string, RedrawType > action ) + => new(pi, Label, action); + + public static ActionSubscriber< string, RedrawType > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class GameObjectRedrawn + { + public const string Label = $"Penumbra.{nameof( GameObjectRedrawn )}"; + + public static EventProvider< nint, int > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< nint, int > Subscriber( DalamudPluginInterface pi, params Action< nint, int >[] actions ) + => new(pi, Label, actions); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Resolve.cs b/tmp/Ipc/Resolve.cs new file mode 100644 index 00000000..8b9eb953 --- /dev/null +++ b/tmp/Ipc/Resolve.cs @@ -0,0 +1,74 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class ResolveDefaultPath + { + public const string Label = $"Penumbra.{nameof( ResolveDefaultPath )}"; + + public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ResolveInterfacePath + { + public const string Label = $"Penumbra.{nameof( ResolveInterfacePath )}"; + + public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ResolvePlayerPath + { + public const string Label = $"Penumbra.{nameof( ResolvePlayerPath )}"; + + public static FuncProvider< string, string > Provider( DalamudPluginInterface pi, Func< string, string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ResolveCharacterPath + { + public const string Label = $"Penumbra.{nameof( ResolveCharacterPath )}"; + + public static FuncProvider< string, string, string > Provider( DalamudPluginInterface pi, Func< string, string, string > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ReverseResolvePath + { + public const string Label = $"Penumbra.{nameof( ReverseResolvePath )}"; + + public static FuncProvider< string, string, string[] > Provider( DalamudPluginInterface pi, Func< string, string, string[] > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, string[] > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class ReverseResolvePlayerPath + { + public const string Label = $"Penumbra.{nameof( ReverseResolvePlayerPath )}"; + + public static FuncProvider< string, string[] > Provider( DalamudPluginInterface pi, Func< string, string[] > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string[] > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Temporary.cs b/tmp/Ipc/Temporary.cs new file mode 100644 index 00000000..55af6f22 --- /dev/null +++ b/tmp/Ipc/Temporary.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class CreateTemporaryCollection + { + public const string Label = $"Penumbra.{nameof( CreateTemporaryCollection )}"; + + public static FuncProvider< string, string, bool, (PenumbraApiEc, string) > Provider( DalamudPluginInterface pi, + Func< string, string, bool, (PenumbraApiEc, string) > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, bool, (PenumbraApiEc, string) > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RemoveTemporaryCollection + { + public const string Label = $"Penumbra.{nameof( RemoveTemporaryCollection )}"; + + public static FuncProvider< string, PenumbraApiEc > Provider( DalamudPluginInterface pi, + Func< string, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class AddTemporaryModAll + { + public const string Label = $"Penumbra.{nameof( AddTemporaryModAll )}"; + + public static FuncProvider< string, Dictionary< string, string >, string, int, PenumbraApiEc > Provider( + DalamudPluginInterface pi, Func< string, Dictionary< string, string >, string, int, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, Dictionary< string, string >, string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class AddTemporaryMod + { + public const string Label = $"Penumbra.{nameof( AddTemporaryMod )}"; + + public static FuncProvider< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > Provider( + DalamudPluginInterface pi, Func< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, Dictionary< string, string >, string, int, PenumbraApiEc > Subscriber( + DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RemoveTemporaryModAll + { + public const string Label = $"Penumbra.{nameof( RemoveTemporaryModAll )}"; + + public static FuncProvider< string, int, PenumbraApiEc > Provider( + DalamudPluginInterface pi, Func< string, int, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } + + public static class RemoveTemporaryMod + { + public const string Label = $"Penumbra.{nameof( RemoveTemporaryMod )}"; + + public static FuncProvider< string, string, int, PenumbraApiEc > Provider( + DalamudPluginInterface pi, Func< string, string, int, PenumbraApiEc > func ) + => new(pi, Label, func); + + public static FuncSubscriber< string, string, int, PenumbraApiEc > Subscriber( DalamudPluginInterface pi ) + => new(pi, Label); + } +} \ No newline at end of file diff --git a/tmp/Ipc/Ui.cs b/tmp/Ipc/Ui.cs new file mode 100644 index 00000000..d88d6718 --- /dev/null +++ b/tmp/Ipc/Ui.cs @@ -0,0 +1,55 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public static partial class Ipc +{ + public static class PreSettingsDraw + { + public const string Label = $"Penumbra.{nameof( PreSettingsDraw )}"; + + public static EventProvider< string > Provider( DalamudPluginInterface pi, Action< Action< string > > sub, + Action< Action< string > > unsub ) + => new(pi, Label, ( sub, unsub )); + + public static EventSubscriber< string > Subscriber( DalamudPluginInterface pi, params Action< string >[] actions ) + => new(pi, Label, actions); + } + + public static class PostSettingsDraw + { + public const string Label = $"Penumbra.{nameof( PostSettingsDraw )}"; + + public static EventProvider< string > Provider( DalamudPluginInterface pi, Action< Action< string > > sub, + Action< Action< string > > unsub ) + => new(pi, Label, ( sub, unsub )); + + public static EventSubscriber< string > Subscriber( DalamudPluginInterface pi, params Action< string >[] actions ) + => new(pi, Label, actions); + } + + public static class ChangedItemTooltip + { + public const string Label = $"Penumbra.{nameof( ChangedItemTooltip )}"; + + public static EventProvider< ChangedItemType, uint > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< ChangedItemType, uint > Subscriber( DalamudPluginInterface pi, params Action< ChangedItemType, uint >[] actions ) + => new(pi, Label, actions); + } + + public static class ChangedItemClick + { + public const string Label = $"Penumbra.{nameof( ChangedItemClick )}"; + + public static EventProvider< MouseButton, ChangedItemType, uint > Provider( DalamudPluginInterface pi, Action add, Action del ) + => new(pi, Label, add, del); + + public static EventSubscriber< MouseButton, ChangedItemType, uint > Subscriber( DalamudPluginInterface pi, params Action< MouseButton, ChangedItemType, uint >[] actions ) + => new(pi, Label, actions); + } +} \ No newline at end of file diff --git a/tmp/Penumbra.Api.csproj b/tmp/Penumbra.Api.csproj new file mode 100644 index 00000000..8962883b --- /dev/null +++ b/tmp/Penumbra.Api.csproj @@ -0,0 +1,47 @@ + + + net6.0-windows + preview + x64 + Penumbra.Api + absolute gangstas + Penumbra + Copyright © 2022 + 1.0.0.0 + 1.0.0.0 + bin\$(Configuration)\ + true + enable + true + false + false + + + + full + DEBUG;TRACE + + + + pdbonly + + + + $(MSBuildWarningsAsMessages);MSB3277 + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + + + + $(DalamudLibPath)Dalamud.dll + False + + + $(DalamudLibPath)Lumina.dll + False + + + diff --git a/tmp/Penumbra.Api.csproj.DotSettings b/tmp/Penumbra.Api.csproj.DotSettings new file mode 100644 index 00000000..7d7508cb --- /dev/null +++ b/tmp/Penumbra.Api.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/tmp/README.md b/tmp/README.md new file mode 100644 index 00000000..1e9bdf1a --- /dev/null +++ b/tmp/README.md @@ -0,0 +1,4 @@ +# Penumbra + +This is an auxiliary repository for Penumbras external API. +For more information, see the [main repo](https://github.com/xivdev/Penumbra). \ No newline at end of file From 8f93df533a46237af68256ca7acc94fcd12a16b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Oct 2022 02:04:00 +0200 Subject: [PATCH 0535/2451] Increment API version. --- Penumbra/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 2da860f2..eeb8bfb3 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -21,7 +21,7 @@ namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => ( 4, 14 ); + => ( 4, 15 ); private Penumbra? _penumbra; private Lumina.GameData? _lumina; From e226b20953c77af0746e52fba5998d27f56d239e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Oct 2022 19:06:09 +0200 Subject: [PATCH 0536/2451] Better. --- Penumbra.Api | 2 +- Penumbra/Api/PenumbraApi.cs | 8 ++++---- Penumbra/Api/PenumbraIpcProviders.cs | 4 ++-- Penumbra/Interop/ObjectReloader.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0064bb82..16298570 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0064bb82be9729676e7bf3202ff1407283e6f088 +Subproject commit 162985704fa4e020443bf18f88dd17dd702c2312 diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index eeb8bfb3..9b35332a 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -31,13 +31,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event Action< string >? PreSettingsPanelDraw; public event Action< string >? PostSettingsPanelDraw; - public event GameObjectRedrawn? GameObjectRedrawn + public event GameObjectRedrawnDelegate? GameObjectRedrawn { add => _penumbra!.ObjectReloader.GameObjectRedrawn += value; remove => _penumbra!.ObjectReloader.GameObjectRedrawn -= value; } - public event ModSettingChanged? ModSettingChanged; + public event ModSettingChangedDelegate? ModSettingChanged; public event CreatingCharacterBaseDelegate? CreatingCharacterBase { @@ -260,10 +260,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ( obj, collection.ModCollection.Name ); } - public int GetCutsceneParentIndex( int actor ) + public int GetCutsceneParentIndex( int actorIdx ) { CheckInitialized(); - return _penumbra!.PathResolver.CutsceneActor( actor ); + return _penumbra!.PathResolver.CutsceneActor( actorIdx ); } public IList< (string, string) > GetModList() diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 26a982ce..5156b081 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -163,7 +163,6 @@ public class PenumbraIpcProviders : IDisposable SetModPath = Ipc.SetModPath.Provider( pi, Api.SetModPath ); // ModSettings - GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider( pi, Api.GetAvailableModSettings ); GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider( pi, Api.GetCurrentModSettings ); TryInheritMod = Ipc.TryInheritMod.Provider( pi, Api.TryInheritMod ); @@ -190,6 +189,8 @@ public class PenumbraIpcProviders : IDisposable public void Dispose() { + Tester.Dispose(); + // Plugin State Initialized.Dispose(); ApiVersion.Dispose(); @@ -268,7 +269,6 @@ public class PenumbraIpcProviders : IDisposable Disposed.Invoke(); Disposed.Dispose(); - Tester.Dispose(); } // Wrappers diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 4fecf454..7a3e7915 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -109,7 +109,7 @@ public sealed unsafe partial class ObjectReloader : IDisposable private readonly List< int > _afterGPoseQueue = new(GPoseSlots); private int _target = -1; - public event GameObjectRedrawn? GameObjectRedrawn; + public event GameObjectRedrawnDelegate? GameObjectRedrawn; public ObjectReloader() => Dalamud.Framework.Update += OnUpdateEvent; From 707f308fac3aa0d630cb43715333a791ddb33562 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Oct 2022 22:56:09 +0200 Subject: [PATCH 0537/2451] Fix problems with manual meta edits not masking correctly. Add warning when not entering model Id. --- Penumbra.Api | 2 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 2 +- .../Meta/Manipulations/EqdpManipulation.cs | 16 +- .../Meta/Manipulations/EqpManipulation.cs | 11 +- .../Meta/Manipulations/EstManipulation.cs | 13 +- .../Meta/Manipulations/GmpManipulation.cs | 9 +- .../Meta/Manipulations/ImcManipulation.cs | 17 +- .../Meta/Manipulations/RspManipulation.cs | 10 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 206 ++++++++++-------- 9 files changed, 169 insertions(+), 117 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 16298570..860f4d62 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 162985704fa4e020443bf18f88dd17dd702c2312 +Subproject commit 860f4d6287d5cbb4b70fef1e74d52992e98ed837 diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index bdbf9512..cb586415 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -97,7 +97,7 @@ public partial class MetaManager } var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); - var manip = m with { Entry = def }; + var manip = m.Copy( def ); if( !manip.Apply( file ) ) { return false; diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index bd800a64..d746295e 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -12,28 +12,32 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > { - public EqdpEntry Entry { get; init; } + public EqdpEntry Entry { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public Gender Gender { get; init; } + public Gender Gender { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public ModelRace Race { get; init; } + public ModelRace Race { get; private init; } - public ushort SetId { get; init; } + public ushort SetId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public EquipSlot Slot { get; init; } + public EquipSlot Slot { get; private init; } + [JsonConstructor] public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) { - Entry = Eqdp.Mask( slot ) & entry; Gender = gender; Race = race; SetId = setId; Slot = slot; + Entry = Eqdp.Mask( Slot ) & entry; } + public EqdpManipulation Copy( EqdpEntry entry ) + => new(entry, Slot, Gender, Race, SetId); + public override string ToString() => $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}"; diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index dd2c8875..5734d0d2 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -7,6 +7,7 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Util; +using SharpCompress.Common; namespace Penumbra.Meta.Manipulations; @@ -14,13 +15,14 @@ namespace Penumbra.Meta.Manipulations; public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > { [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] - public EqpEntry Entry { get; init; } + public EqpEntry Entry { get; private init; } - public ushort SetId { get; init; } + public ushort SetId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public EquipSlot Slot { get; init; } + public EquipSlot Slot { get; private init; } + [JsonConstructor] public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) { Slot = slot; @@ -28,6 +30,9 @@ public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > Entry = Eqp.Mask( slot ) & entry; } + public EqpManipulation Copy( EqpEntry entry ) + => new(entry, Slot, SetId); + public override string ToString() => $"Eqp - {SetId} - {Slot}"; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index 1abe3220..b21148ef 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,18 +19,18 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.Index.HeadEst, } - public ushort Entry { get; init; } // SkeletonIdx. + public ushort Entry { get; private init; } // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] - public Gender Gender { get; init; } + public Gender Gender { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public ModelRace Race { get; init; } + public ModelRace Race { get; private init; } - public ushort SetId { get; init; } + public ushort SetId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public EstType Slot { get; init; } + public EstType Slot { get; private init; } [JsonConstructor] public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) @@ -42,6 +42,9 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Slot = slot; } + public EstManipulation Copy( ushort entry ) + => new(Gender, Race, Slot, SetId, entry); + public override string ToString() => $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}"; diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 9890f113..23a165c2 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Newtonsoft.Json; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; @@ -8,15 +9,19 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > { - public GmpEntry Entry { get; init; } - public ushort SetId { get; init; } + public GmpEntry Entry { get; private init; } + public ushort SetId { get; private init; } + [JsonConstructor] public GmpManipulation( GmpEntry entry, ushort setId ) { Entry = entry; SetId = setId; } + public GmpManipulation Copy( GmpEntry entry ) + => new(entry, SetId); + public override string ToString() => $"Gmp - {SetId}"; diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index e9c01c0b..5bd505df 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -12,19 +12,19 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { - public ImcEntry Entry { get; init; } - public ushort PrimaryId { get; init; } - public ushort Variant { get; init; } - public ushort SecondaryId { get; init; } + public ImcEntry Entry { get; private init; } + public ushort PrimaryId { get; private init; } + public ushort Variant { get; private init; } + public ushort SecondaryId { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public ObjectType ObjectType { get; init; } + public ObjectType ObjectType { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public EquipSlot EquipSlot { get; init; } + public EquipSlot EquipSlot { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public BodySlot BodySlot { get; init; } + public BodySlot BodySlot { get; private init; } public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) { @@ -71,6 +71,9 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > } } + public ImcManipulation Copy( ImcEntry entry ) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant, EquipSlot, entry); + public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 70908324..e69ec0a1 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -11,14 +11,15 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct RspManipulation : IMetaManipulation< RspManipulation > { - public float Entry { get; init; } + public float Entry { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public SubRace SubRace { get; init; } + public SubRace SubRace { get; private init; } [JsonConverter( typeof( StringEnumConverter ) )] - public RspAttribute Attribute { get; init; } + public RspAttribute Attribute { get; private init; } + [JsonConstructor] public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) { Entry = entry; @@ -26,6 +27,9 @@ public readonly struct RspManipulation : IMetaManipulation< RspManipulation > Attribute = attribute; } + public RspManipulation Copy( float entry ) + => new(SubRace, Attribute, entry); + public override string ToString() => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index eef21256..9a725685 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -17,6 +17,19 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { + private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + private const string PrimaryIdTooltip = "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; + private void DrawMetaTab() { using var tab = ImRaii.TabItem( "Meta Manipulations" ); @@ -108,25 +121,25 @@ public partial class ModEditWindow var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId ); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry } ); + editor.Meta.Add( _new.Copy( defaultEntry ) ); } // Identifier ImGui.TableNextColumn(); - if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) { - _new = _new with { SetId = setId }; + _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), _new.Slot, setId ); } - ImGuiUtil.HoverTooltip( "Model Set ID"); + ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) { - _new = _new with { Slot = slot }; + _new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId ); } - ImGuiUtil.HoverTooltip( "Equip Slot"); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -151,12 +164,13 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId ); + ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToName() ); - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); // Values ImGui.TableNextColumn(); @@ -170,7 +184,7 @@ public partial class ModEditWindow var currentValue = meta.Entry.HasFlag( flag ); if( Checkmark( "##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value ) ) { - editor.Meta.Change( meta with { Entry = value ? meta.Entry | flag : meta.Entry & ~flag } ); + editor.Meta.Change( meta.Copy( value ? meta.Entry | flag : meta.Entry & ~flag ) ); } ImGui.SameLine(); @@ -204,41 +218,45 @@ public partial class ModEditWindow : 0; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry } ); + editor.Meta.Add( _new.Copy( defaultEntry ) ); } // Identifier ImGui.TableNextColumn(); - if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) { - _new = _new with { SetId = setId }; + var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), setId ); + _new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId ); } - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); if( RaceCombo( "##eqdpRace", _new.Race, out var race ) ) { - _new = _new with { Race = race }; + var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId ); + _new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId ); } - ImGuiUtil.HoverTooltip( "Model Race" ); + ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) ) { - _new = _new with { Gender = gender }; + var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ); + _new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId ); } - ImGuiUtil.HoverTooltip( "Gender" ); + ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) ) { - _new = _new with { Slot = slot }; + var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId ); + _new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId ); } - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -257,19 +275,19 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Race.ToName() ); - ImGuiUtil.HoverTooltip( "Model Race" ); + ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Gender.ToName() ); - ImGuiUtil.HoverTooltip( "Gender" ); + ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToName() ); - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); // Values var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId ); @@ -278,13 +296,13 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if( Checkmark( "Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) { - editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) } ); + editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) ) ); } ImGui.SameLine(); if( Checkmark( "Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) { - editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) } ); + editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) ) ); } } } @@ -319,12 +337,12 @@ public partial class ModEditWindow editor.Meta.Imc.Select( m => ( MetaManipulation )m ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( _new ); - var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); - var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; + var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; defaultEntry ??= new ImcEntry(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry.Value } ); + editor.Meta.Add( _new.Copy( defaultEntry.Value ) ); } // Identifier @@ -332,18 +350,19 @@ public partial class ModEditWindow if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) ) { _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, - _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); + _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); } - ImGuiUtil.HoverTooltip( "Object Type" ); + ImGuiUtil.HoverTooltip( ObjectTypeTooltip ); ImGui.TableNextColumn(); - if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue ) ) + if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1 ) ) { - _new = _new with { PrimaryId = setId }; + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); } - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( PrimaryIdTooltip ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); @@ -354,28 +373,31 @@ public partial class ModEditWindow { if( EqdpEquipSlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) { - _new = _new with { EquipSlot = slot }; + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); } - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); } else { - if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue ) ) + if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) { - _new = _new with { SecondaryId = setId2 }; + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); } - ImGuiUtil.HoverTooltip( "Secondary ID" ); + ImGuiUtil.HoverTooltip( SecondaryIdTooltip ); } ImGui.TableNextColumn(); - if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue ) ) + if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false ) ) { - _new = _new with { Variant = variant }; + _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) + ?? new ImcEntry() ); } - ImGuiUtil.HoverTooltip( "Variant ID" ); + ImGuiUtil.HoverTooltip( VariantIdTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -415,29 +437,29 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.ObjectType.ToName() ); - ImGuiUtil.HoverTooltip( "Object Type" ); + ImGuiUtil.HoverTooltip( ObjectTypeTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.PrimaryId.ToString() ); - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( "Primary ID" ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) { ImGui.TextUnformatted( meta.EquipSlot.ToName() ); - ImGuiUtil.HoverTooltip( "Equip Slot" ); + ImGuiUtil.HoverTooltip( EquipSlotTooltip ); } else { ImGui.TextUnformatted( meta.SecondaryId.ToString() ); - ImGuiUtil.HoverTooltip( "Secondary ID" ); + ImGuiUtil.HoverTooltip( SecondaryIdTooltip ); } ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Variant.ToString() ); - ImGuiUtil.HoverTooltip( "Variant ID" ); + ImGuiUtil.HoverTooltip( VariantIdTooltip ); // Values using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, @@ -447,35 +469,35 @@ public partial class ModEditWindow if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialId = ( byte )materialId } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { MaterialId = ( byte )materialId } ) ); } ImGui.SameLine(); if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } ) ); } ImGui.TableNextColumn(); if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { DecalId = ( byte )decalId } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { DecalId = ( byte )decalId } ) ); } ImGui.SameLine(); if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, out var vfxId, 0, byte.MaxValue, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { VfxId = ( byte )vfxId } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { VfxId = ( byte )vfxId } ) ); } ImGui.SameLine(); if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { SoundId = ( byte )soundId } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { SoundId = ( byte )soundId } ) ); } ImGui.TableNextColumn(); @@ -487,7 +509,7 @@ public partial class ModEditWindow ( defaultEntry.AttributeMask & flag ) != 0, out var val ) ) { var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; - editor.Meta.Change( meta with { Entry = meta.Entry with { AttributeMask = ( ushort )attributes } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { AttributeMask = ( ushort )attributes } ) ); } ImGui.SameLine(); @@ -515,41 +537,45 @@ public partial class ModEditWindow var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry } ); + editor.Meta.Add( _new.Copy( defaultEntry ) ); } // Identifier ImGui.TableNextColumn(); - if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) { - _new = _new with { SetId = setId }; + var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), setId ); + _new = new EstManipulation( _new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry ); } - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); ImGui.TableNextColumn(); if( RaceCombo( "##estRace", _new.Race, out var race ) ) { - _new = _new with { Race = race }; + var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId ); + _new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry ); } - ImGuiUtil.HoverTooltip( "Model Race" ); + ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); if( GenderCombo( "##estGender", _new.Gender, out var gender ) ) { - _new = _new with { Gender = gender }; + var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId ); + _new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry ); } - ImGuiUtil.HoverTooltip( "Gender" ); + ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) ) { - _new = _new with { Slot = slot }; + var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); + _new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry ); } - ImGuiUtil.HoverTooltip( "EST Type" ); + ImGuiUtil.HoverTooltip( EstTypeTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -565,27 +591,27 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Race.ToName() ); - ImGuiUtil.HoverTooltip( "Model Race" ); + ImGuiUtil.HoverTooltip( ModelRaceTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Gender.ToName() ); - ImGuiUtil.HoverTooltip( "Gender" ); + ImGuiUtil.HoverTooltip( GenderTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Slot.ToString() ); - ImGuiUtil.HoverTooltip( "EST Type" ); + ImGuiUtil.HoverTooltip( EstTypeTooltip ); // Values - var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); + var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); ImGui.TableNextColumn(); if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, out var entry, 0, ushort.MaxValue, 0.05f ) ) { - editor.Meta.Change( meta with { Entry = ( ushort )entry } ); + editor.Meta.Change( meta.Copy( ( ushort )entry ) ); } } } @@ -614,17 +640,17 @@ public partial class ModEditWindow var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId ); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry } ); + editor.Meta.Add( _new.Copy( defaultEntry ) ); } // Identifier ImGui.TableNextColumn(); - if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) ) + if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) ) { - _new = _new with { SetId = setId }; + _new = new GmpManipulation( ExpandedGmpFile.GetDefault( setId ), setId ); } - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -655,55 +681,55 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SetId.ToString() ); - ImGuiUtil.HoverTooltip( "Model Set ID" ); + ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort ); // Values var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId ); ImGui.TableNextColumn(); if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { Enabled = enabled } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { Enabled = enabled } ) ); } ImGui.TableNextColumn(); if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { Animated = animated } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { Animated = animated } ) ); } ImGui.TableNextColumn(); if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { RotationA = ( ushort )rotationA } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { RotationA = ( ushort )rotationA } ) ); } ImGui.SameLine(); if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { RotationB = ( ushort )rotationB } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { RotationB = ( ushort )rotationB } ) ); } ImGui.SameLine(); if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { RotationC = ( ushort )rotationC } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { RotationC = ( ushort )rotationC } ) ); } ImGui.TableNextColumn(); if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkA } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkA } ) ); } ImGui.SameLine(); if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) ) { - editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkB } } ); + editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkB } ) ); } } } @@ -726,25 +752,25 @@ public partial class ModEditWindow var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute ); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) { - editor.Meta.Add( _new with { Entry = defaultEntry } ); + editor.Meta.Add( _new.Copy( defaultEntry ) ); } // Identifier ImGui.TableNextColumn(); if( SubRaceCombo( "##rspSubRace", _new.SubRace, out var subRace ) ) { - _new = _new with { SubRace = subRace }; + _new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) ); } - ImGuiUtil.HoverTooltip( "Racial Tribe" ); + ImGuiUtil.HoverTooltip( RacialTribeTooltip ); ImGui.TableNextColumn(); if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) ) { - _new = _new with { Attribute = attribute }; + _new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) ); } - ImGuiUtil.HoverTooltip( "Scaling Type" ); + ImGuiUtil.HoverTooltip( ScalingTypeTooltip ); // Values using var disabled = ImRaii.Disabled(); @@ -761,11 +787,11 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.SubRace.ToName() ); - ImGuiUtil.HoverTooltip( "Racial Tribe" ); + ImGuiUtil.HoverTooltip( RacialTribeTooltip ); ImGui.TableNextColumn(); ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); ImGui.TextUnformatted( meta.Attribute.ToFullString() ); - ImGuiUtil.HoverTooltip( "Scaling Type" ); + ImGuiUtil.HoverTooltip( ScalingTypeTooltip ); ImGui.TableNextColumn(); // Values @@ -777,7 +803,7 @@ public partial class ModEditWindow def != value ); if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) { - editor.Meta.Change( meta with { Entry = value } ); + editor.Meta.Change( meta.Copy( value ) ); } ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" ); @@ -815,10 +841,12 @@ public partial class ModEditWindow // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. - private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId ) + private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border ) { int tmp = currentId; ImGui.SetNextItemWidth( width ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, border ); + using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border ); if( ImGui.InputInt( label, ref tmp, 0 ) ) { tmp = Math.Clamp( tmp, minId, maxId ); From bbfdc7fad2eed034b62cc920a82fd070f8a6ea65 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Oct 2022 22:56:26 +0200 Subject: [PATCH 0538/2451] Clarify disallowed paths. --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 10f5e1b3..57d50a8b 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -97,7 +97,7 @@ public partial class ConfigWindow if( Path.GetDirectoryName( newName ) == null ) { - return ( "Path may not be a drive root. Please add a directory.", false ); + return ( "Path is not allowed to be a drive root. Please add a directory.", false ); } var symbol = '\0'; @@ -109,31 +109,31 @@ public partial class ConfigWindow var desktop = Environment.GetFolderPath( Environment.SpecialFolder.Desktop ); if( IsSubPathOf( desktop, newName ) ) { - return ( "Path may not be on your Desktop.", false ); + return ( "Path is not allowed to be on your Desktop.", false ); } var programFiles = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ); var programFilesX86 = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFilesX86 ); if( IsSubPathOf( programFiles, newName ) || IsSubPathOf( programFilesX86, newName ) ) { - return ( "Path may not be in ProgramFiles.", false ); + return ( "Path is not allowed to be in ProgramFiles.", false ); } var dalamud = Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!; if( IsSubPathOf( dalamud.FullName, newName ) ) { - return ( "Path may not be inside your Dalamud directories.", false ); + return ( "Path is not allowed to be inside your Dalamud directories.", false ); } if( Functions.GetDownloadsFolder( out var downloads ) && IsSubPathOf( downloads, newName ) ) { - return ( "Path may not be inside your Downloads folder.", false ); + return ( "Path is not allowed to be inside your Downloads folder.", false ); } var gameDir = Dalamud.GameData.GameData.DataPath.Parent!.Parent!.FullName; if( IsSubPathOf( gameDir, newName ) ) { - return ( "Path may not be inside your game folder.", false ); + return ( "Path is not allowed to be inside your game folder.", false ); } return ( $"Press Enter or Click Here to Save (Current Directory: {old})", true ); From 6eb2d9a9d29fa11137fb5b7ba6c439397cbfc7b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Oct 2022 22:56:53 +0200 Subject: [PATCH 0539/2451] Add Base64 to customizedata. --- Penumbra.GameData/Structs/CustomizeData.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Penumbra.GameData/Structs/CustomizeData.cs b/Penumbra.GameData/Structs/CustomizeData.cs index 4bce24cf..3f184bb5 100644 --- a/Penumbra.GameData/Structs/CustomizeData.cs +++ b/Penumbra.GameData/Structs/CustomizeData.cs @@ -55,4 +55,26 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData > return HashCode.Combine( *p, p[ 1 ], p[ 2 ], p[ 3 ], p[ 4 ], p[ 5 ], u ); } } + + public string WriteBase64() + { + fixed( byte* ptr = Data ) + { + var data = new ReadOnlySpan< byte >( ptr, Size ); + return Convert.ToBase64String( data ); + } + } + + public bool LoadBase64( string base64 ) + { + var buffer = stackalloc byte[Size]; + var span = new Span< byte >( buffer, Size ); + if( !Convert.TryFromBase64String( base64, span, out var written ) || written != Size ) + { + return false; + } + + Read( buffer ); + return true; + } } \ No newline at end of file From 1a1cbb540481543887fd7da553ba18eda8c116de Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 10 Oct 2022 13:52:49 +0200 Subject: [PATCH 0540/2451] Changelog. --- Penumbra/UI/Classes/ModFileSystemSelector.cs | 20 +++++++++++--------- Penumbra/UI/ConfigWindow.Changelog.cs | 7 +++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 1ccf0e3c..30d94f02 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -451,13 +451,22 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private static void DrawHelpPopup() { - ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 33.5f * ImGui.GetTextLineHeightWithSpacing() ), () => + ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing() ), () => { + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.TextUnformatted( "Mod Management" ); + ImGui.BulletText( "You can create empty mods or import mods with the buttons in this row." ); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText( "Supported formats for import are: .ttmp, .ttmp2, .pmp." ); + ImGui.BulletText( "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata." ); + indent.Pop( 1 ); + ImGui.BulletText( "You can also create empty mod folders and delete mods." ); + ImGui.BulletText( "For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup." ); ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); ImGui.TextUnformatted( "Mod Selector" ); ImGui.BulletText( "Select a mod to obtain more information or change settings." ); ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" ); - using var indent = ImRaii.PushIndent(); + indent.Push(); ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." ); ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." ); ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." ); @@ -489,13 +498,6 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); indent.Pop( 1 ); ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.TextUnformatted( "Mod Management" ); - ImGui.BulletText( "You can create empty mods or import TTMP-based mods with the buttons in this row." ); - ImGui.BulletText( - "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); - ImGui.BulletText( - "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); } ); } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 514814a6..29ae5192 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -26,6 +26,13 @@ public partial class ConfigWindow return ret; } + private static void Add5_11_0( Changelog log ) + => log.NextVersion( "Version 0.5.11.0" ) + .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) + .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) + .RegisterEntry( "Updated some tooltips and hints." ) + .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ); + private static void Add5_10_0( Changelog log ) => log.NextVersion( "Version 0.5.10.0" ) .RegisterEntry( "Renamed backup functionality to export functionality." ) From 8b156c7d587cba637b82f6950d9e98cbe3e145a6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 10 Oct 2022 15:09:43 +0200 Subject: [PATCH 0541/2451] Update some tutorials. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.ModsTab.cs | 1 + Penumbra/UI/ConfigWindow.Tutorial.cs | 11 +++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 750f7415..1e0d04b9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 750f7415d07334530ad12db10f3dd28fd7e1f130 +Subproject commit 1e0d04b90043faad979c3e7316a733870eb16108 diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index ba88c75d..57c48c9e 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -71,6 +71,7 @@ public partial class ConfigWindow { if( Penumbra.Config.HideRedrawBar ) { + SkipTutorial( BasicTutorialSteps.Redrawing ); return; } diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index 04fa3513..a5b34840 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -44,6 +44,14 @@ public partial class ConfigWindow Penumbra.Config.Save(); } ); + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static void SkipTutorial( BasicTutorialSteps step ) + => Tutorial.Skip( ( int )step, Penumbra.Config.TutorialStep, v => + { + Penumbra.Config.TutorialStep = v; + Penumbra.Config.Save(); + } ); + public enum BasicTutorialSteps { GeneralTooltips, @@ -88,8 +96,7 @@ public partial class ConfigWindow + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n" + "The folder should be an empty folder no other applications write to." ) .Register( "Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not." ) - .Register( "Advanced Settings", "When you are just starting, you should leave this off.\n\n" - + "If you need to do any editing of your mods, you will have to turn it on later." ) + .Deprecated() .Register( "General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" + "If you do not know what some of these do yet, return to this later!" ) .Register( "Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" From 6039be868582e5d4ac4f54f4ee61e81fb54349f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Oct 2022 15:10:59 +0200 Subject: [PATCH 0542/2451] Added Enabled State API. --- Penumbra.Api | 2 +- Penumbra/Api/IpcTester.cs | 27 +++++++--- Penumbra/Api/PenumbraApi.cs | 67 ++++++++++++++++++++++--- Penumbra/Api/PenumbraIpcProviders.cs | 18 +++++-- Penumbra/Penumbra.cs | 5 ++ Penumbra/UI/ConfigWindow.Changelog.cs | 4 +- Penumbra/UI/ConfigWindow.SettingsTab.cs | 1 - 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 860f4d62..f41af0fb 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 860f4d6287d5cbb4b70fef1e74d52992e98ed837 +Subproject commit f41af0fb88626f1579d3c4370b32b901f3c4d3c2 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 486eb826..7ccad1ce 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -81,6 +81,7 @@ public class IpcTester : IDisposable { _pluginState.Initialized.Enable(); _pluginState.Disposed.Enable(); + _pluginState.EnabledChange.Enable(); _redrawing.Redrawn.Enable(); _ui.PreSettingsDraw.Enable(); _ui.PostSettingsDraw.Enable(); @@ -99,6 +100,7 @@ public class IpcTester : IDisposable { _pluginState.Initialized.Disable(); _pluginState.Disposed.Disable(); + _pluginState.EnabledChange.Disable(); _redrawing.Redrawn.Disable(); _ui.PreSettingsDraw.Disable(); _ui.PostSettingsDraw.Disable(); @@ -117,6 +119,7 @@ public class IpcTester : IDisposable { _pluginState.Initialized.Dispose(); _pluginState.Disposed.Dispose(); + _pluginState.EnabledChange.Dispose(); _redrawing.Redrawn.Dispose(); _ui.PreSettingsDraw.Dispose(); _ui.PostSettingsDraw.Dispose(); @@ -142,18 +145,23 @@ public class IpcTester : IDisposable private class PluginState { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber Initialized; - public readonly EventSubscriber Disposed; + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber Initialized; + public readonly EventSubscriber Disposed; + public readonly EventSubscriber< bool > EnabledChange; private readonly List< DateTimeOffset > _initializedList = new(); private readonly List< DateTimeOffset > _disposedList = new(); + private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; + private bool? _lastEnabledValue; + public PluginState( DalamudPluginInterface pi ) { - _pi = pi; - Initialized = Ipc.Initialized.Subscriber( pi, AddInitialized ); - Disposed = Ipc.Disposed.Subscriber( pi, AddDisposed ); + _pi = pi; + Initialized = Ipc.Initialized.Subscriber( pi, AddInitialized ); + Disposed = Ipc.Disposed.Subscriber( pi, AddDisposed ); + EnabledChange = Ipc.EnabledChange.Subscriber( pi, SetLastEnabled ); } public void Draw() @@ -193,6 +201,10 @@ public class IpcTester : IDisposable DrawIntro( Ipc.ApiVersions.Label, "Current Version" ); var (breaking, features) = Ipc.ApiVersions.Subscriber( _pi ).Invoke(); ImGui.TextUnformatted( $"{breaking}.{features:D4}" ); + DrawIntro( Ipc.GetEnabledState.Label, "Current State" ); + ImGui.TextUnformatted( $"{Ipc.GetEnabledState.Subscriber( _pi ).Invoke()}" ); + DrawIntro( Ipc.EnabledChange.Label, "Last Change" ); + ImGui.TextUnformatted( _lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never" ); } private void AddInitialized() @@ -200,6 +212,9 @@ public class IpcTester : IDisposable private void AddDisposed() => _disposedList.Add( DateTimeOffset.UtcNow ); + + private void SetLastEnabled( bool val ) + => ( _lastEnabledChange, _lastEnabledValue ) = ( DateTimeOffset.Now, val ); } private class Configuration diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9b35332a..0484f986 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -33,22 +33,46 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event GameObjectRedrawnDelegate? GameObjectRedrawn { - add => _penumbra!.ObjectReloader.GameObjectRedrawn += value; - remove => _penumbra!.ObjectReloader.GameObjectRedrawn -= value; + add + { + CheckInitialized(); + _penumbra!.ObjectReloader.GameObjectRedrawn += value; + } + remove + { + CheckInitialized(); + _penumbra!.ObjectReloader.GameObjectRedrawn -= value; + } } public event ModSettingChangedDelegate? ModSettingChanged; public event CreatingCharacterBaseDelegate? CreatingCharacterBase { - add => PathResolver.DrawObjectState.CreatingCharacterBase += value; - remove => PathResolver.DrawObjectState.CreatingCharacterBase -= value; + add + { + CheckInitialized(); + PathResolver.DrawObjectState.CreatingCharacterBase += value; + } + remove + { + CheckInitialized(); + PathResolver.DrawObjectState.CreatingCharacterBase -= value; + } } public event CreatedCharacterBaseDelegate? CreatedCharacterBase { - add => PathResolver.DrawObjectState.CreatedCharacterBase += value; - remove => PathResolver.DrawObjectState.CreatedCharacterBase -= value; + add + { + CheckInitialized(); + PathResolver.DrawObjectState.CreatedCharacterBase += value; + } + remove + { + CheckInitialized(); + PathResolver.DrawObjectState.CreatedCharacterBase -= value; + } } public bool Valid @@ -104,8 +128,33 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event Action< string, bool >? ModDirectoryChanged { - add => Penumbra.ModManager.ModDirectoryChanged += value; - remove => Penumbra.ModManager.ModDirectoryChanged -= value; + add + { + CheckInitialized(); + Penumbra.ModManager.ModDirectoryChanged += value; + } + remove + { + CheckInitialized(); + Penumbra.ModManager.ModDirectoryChanged -= value; + } + } + + public bool GetEnabledState() + => Penumbra.Config.EnableMods; + + public event Action< bool >? EnabledChange + { + add + { + CheckInitialized(); + _penumbra!.EnabledChange += value; + } + remove + { + CheckInitialized(); + _penumbra!.EnabledChange -= value; + } } public string GetConfiguration() @@ -334,7 +383,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { return PenumbraApiEc.NothingChanged; + } Penumbra.ModManager.DeleteMod( mod.Index ); return PenumbraApiEc.Success; diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 5156b081..0db02dbd 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -1,6 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; -using Penumbra.Collections; using Penumbra.GameData.Enums; using System; using System.Collections.Generic; @@ -21,6 +20,8 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider Disposed; internal readonly FuncProvider< int > ApiVersion; internal readonly FuncProvider< (int Breaking, int Features) > ApiVersions; + internal readonly FuncProvider< bool > GetEnabledState; + internal readonly EventProvider< bool > EnabledChange; // Configuration internal readonly FuncProvider< string > GetModDirectory; @@ -98,10 +99,12 @@ public class PenumbraIpcProviders : IDisposable Api = api; // Plugin State - Initialized = Ipc.Initialized.Provider( pi ); - Disposed = Ipc.Disposed.Provider( pi ); - ApiVersion = Ipc.ApiVersion.Provider( pi, DeprecatedVersion ); - ApiVersions = Ipc.ApiVersions.Provider( pi, () => Api.ApiVersion ); + Initialized = Ipc.Initialized.Provider( pi ); + Disposed = Ipc.Disposed.Provider( pi ); + ApiVersion = Ipc.ApiVersion.Provider( pi, DeprecatedVersion ); + ApiVersions = Ipc.ApiVersions.Provider( pi, () => Api.ApiVersion ); + GetEnabledState = Ipc.GetEnabledState.Provider( pi, Api.GetEnabledState ); + EnabledChange = Ipc.EnabledChange.Provider( pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent ); // Configuration GetModDirectory = Ipc.GetModDirectory.Provider( pi, Api.GetModDirectory ); @@ -195,6 +198,8 @@ public class PenumbraIpcProviders : IDisposable Initialized.Dispose(); ApiVersion.Dispose(); ApiVersions.Dispose(); + GetEnabledState.Dispose(); + EnabledChange.Dispose(); // Configuration GetModDirectory.Dispose(); @@ -290,6 +295,9 @@ public class PenumbraIpcProviders : IDisposable ChangedItemTooltip.Invoke( type, id ); } + private void EnabledChangeEvent( bool value ) + => EnabledChange.Invoke( value ); + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) => GameObjectRedrawn.Invoke( objectAddress, objectTableIndex ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cd08a20b..e556c740 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -25,6 +25,7 @@ using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; +using Action = System.Action; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -185,6 +186,8 @@ public class Penumbra : IDalamudPlugin } } + public event Action< bool >? EnabledChange; + public bool Enable() { if( Config.EnableMods ) @@ -202,6 +205,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); ObjectReloader.RedrawAll( RedrawType.Redraw ); } + EnabledChange?.Invoke( true ); return true; } @@ -223,6 +227,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); ObjectReloader.RedrawAll( RedrawType.Redraw ); } + EnabledChange?.Invoke( false ); return true; } diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 29ae5192..7a3eed8c 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -31,7 +31,9 @@ public partial class ConfigWindow .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) .RegisterEntry( "Updated some tooltips and hints." ) - .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ); + .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ) + .RegisterEntry( "Added API to delete mods and read and set their pseudo-filesystem paths.", 1 ) + .RegisterEntry( "Added API to check Penumbras enabled state and updates to it.", 1 ); private static void Add5_10_0( Changelog log ) => log.NextVersion( "Version 0.5.10.0" ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index 57d50a8b..ab14c749 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -11,7 +11,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -using Penumbra.GameData.Enums; using Penumbra.UI.Classes; namespace Penumbra.UI; From febfa8836e2b7a209f1f95244442fa0f91dce0cf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Oct 2022 15:11:15 +0200 Subject: [PATCH 0543/2451] Misc. --- Penumbra.GameData/Enums/EquipSlot.cs | 50 +++++++++++++++------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index a68cacb9..c8004f94 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -39,33 +39,37 @@ public static class EquipSlotExtensions public static EquipSlot ToEquipSlot( this uint value ) => value switch { - 0 => EquipSlot.Head, - 1 => EquipSlot.Body, - 2 => EquipSlot.Hands, - 3 => EquipSlot.Legs, - 4 => EquipSlot.Feet, - 5 => EquipSlot.Ears, - 6 => EquipSlot.Neck, - 7 => EquipSlot.Wrists, - 8 => EquipSlot.RFinger, - 9 => EquipSlot.LFinger, - _ => EquipSlot.Unknown, + 0 => EquipSlot.Head, + 1 => EquipSlot.Body, + 2 => EquipSlot.Hands, + 3 => EquipSlot.Legs, + 4 => EquipSlot.Feet, + 5 => EquipSlot.Ears, + 6 => EquipSlot.Neck, + 7 => EquipSlot.Wrists, + 8 => EquipSlot.RFinger, + 9 => EquipSlot.LFinger, + 10 => EquipSlot.MainHand, + 11 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, }; public static uint ToIndex( this EquipSlot slot ) => slot switch { - EquipSlot.Head => 0, - EquipSlot.Body => 1, - EquipSlot.Hands => 2, - EquipSlot.Legs => 3, - EquipSlot.Feet => 4, - EquipSlot.Ears => 5, - EquipSlot.Neck => 6, - EquipSlot.Wrists => 7, - EquipSlot.RFinger => 8, - EquipSlot.LFinger => 9, - _ => uint.MaxValue, + EquipSlot.Head => 0, + EquipSlot.Body => 1, + EquipSlot.Hands => 2, + EquipSlot.Legs => 3, + EquipSlot.Feet => 4, + EquipSlot.Ears => 5, + EquipSlot.Neck => 6, + EquipSlot.Wrists => 7, + EquipSlot.RFinger => 8, + EquipSlot.LFinger => 9, + EquipSlot.MainHand => 10, + EquipSlot.OffHand => 11, + _ => uint.MaxValue, }; public static string ToSuffix( this EquipSlot value ) @@ -112,7 +116,7 @@ public static class EquipSlotExtensions EquipSlot.BodyHands => EquipSlot.Body, EquipSlot.BodyLegsFeet => EquipSlot.Body, EquipSlot.ChestHands => EquipSlot.Body, - _ => throw new InvalidEnumArgumentException($"{value} ({(int) value}) is not valid."), + _ => throw new InvalidEnumArgumentException( $"{value} ({( int )value}) is not valid." ), }; } From 847d8432ffeb2d96350f3236ef3ce8b3af3b1902 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Oct 2022 12:16:21 +0200 Subject: [PATCH 0544/2451] Add backface and transparency handling as well as more info to Mtrl handling. --- Penumbra.GameData/Files/MtrlFile.Write.cs | 2 +- Penumbra.GameData/Files/MtrlFile.cs | 4 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 123 ++++++++++++++++-- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 43225ea2..1c2b9e6b 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -66,7 +66,7 @@ public partial class MtrlFile w.Write( ( ushort )ShaderPackage.ShaderKeys.Length ); w.Write( ( ushort )ShaderPackage.Constants.Length ); w.Write( ( ushort )ShaderPackage.Samplers.Length ); - w.Write( ShaderPackage.Unk ); + w.Write( ShaderPackage.Flags ); foreach( var key in ShaderPackage.ShaderKeys ) { diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index aa78b7ab..cbefed8d 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -249,7 +249,7 @@ public partial class MtrlFile : IWritable public Constant[] Constants; public Sampler[] Samplers; public float[] ShaderValues; - public uint Unk; + public uint Flags; } @@ -326,7 +326,7 @@ public partial class MtrlFile : IWritable var shaderKeyCount = r.ReadUInt16(); var constantCount = r.ReadUInt16(); var samplerCount = r.ReadUInt16(); - ShaderPackage.Unk = r.ReadUInt32(); + ShaderPackage.Flags = r.ReadUInt32(); ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount ); ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount ); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 29ca6897..9515e585 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Numerics; @@ -32,7 +33,7 @@ public partial class ModEditWindow private bool _changed; private string _defaultPath = string.Empty; - private bool _inInput = false; + private bool _inInput; private T? _defaultFile; private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; @@ -230,10 +231,15 @@ public partial class ModEditWindow { var ret = DrawMaterialTextureChange( file, disabled ); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawBackFaceAndTransparency( file, disabled ); - ImGui.NewLine(); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawMaterialColorSetChange( file, disabled ); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawOtherMaterialDetails( file, disabled ); + return !disabled && ret; } @@ -266,7 +272,7 @@ public partial class ModEditWindow return false; } - ColorSetCopyAllClipboardButton( file, 0 ); + ColorSetCopyAllClipboardButton( file, 0 ); ImGui.SameLine(); var ret = ColorSetPasteAllClipboardButton( file, 0 ); using var table = ImRaii.Table( "##ColorSets", 10, @@ -310,9 +316,110 @@ public partial class ModEditWindow return ret; } + private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled ) + { + const uint transparencyBit = 0x10; + const uint backfaceBit = 0x01; + + var ret = false; + + using var dis = ImRaii.Disabled( disabled ); + + var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0; + if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) + { + file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit; + ret = true; + } + + ImGui.SameLine(200 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; + if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) + { + file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit; + ret = true; + } + + return ret; + } + + private static bool DrawOtherMaterialDetails( MtrlFile file, bool _ ) + { + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return false; + } + + using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( sets ) + { + foreach( var set in file.UvSets ) + { + ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + using( var shaders = ImRaii.TreeNode( "Shaders", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + if( shaders ) + { + ImRaii.TreeNode( $"Name: {file.ShaderPackage.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Flags: {file.ShaderPackage.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + { + using var t = ImRaii.TreeNode( $"Shader Key #{idx}" ); + if( t ) + { + ImRaii.TreeNode( $"Category: {key.Category}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Value: {key.Value}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + { + using var t = ImRaii.TreeNode( $"Constant #{idx}" ); + if( t ) + { + ImRaii.TreeNode( $"Category: {constant.Id}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Value: 0x{constant.Value:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + { + using var t = ImRaii.TreeNode( $"Sampler #{idx}" ); + if( t ) + { + ImRaii.TreeNode( $"ID: {sampler.SamplerId}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Texture Index: {sampler.TextureIndex}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Flags: 0x{sampler.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + foreach( var (value, idx) in file.ShaderPackage.ShaderValues.WithIndex() ) + { + ImRaii.TreeNode( $"Value #{idx}: {value.ToString( CultureInfo.InvariantCulture )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + + if( file.AdditionalData.Length > 0 ) + { + using var t = ImRaii.TreeNode( "Additional Data" ); + if( t ) + { + ImRaii.TreeNode( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ), ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + + return false; + } + private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) { - if( !ImGui.Button( "Export All Rows to Clipboard" ) ) + if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) { return; } @@ -335,7 +442,7 @@ public partial class ModEditWindow private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) { - if( !ImGui.Button( "Import All Rows from Clipboard" ) || file.ColorSets.Length <= colorSetIdx ) + if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) { return false; } @@ -353,13 +460,13 @@ public partial class ModEditWindow fixed( void* ptr = data, output = &rows ) { Functions.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); - if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx) + if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() + && file.ColorDyeSets.Length > colorSetIdx ) { ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; fixed( void* output2 = &dyeRows ) { - Functions.MemCpyUnchecked( output2, (byte*) ptr + Marshal.SizeOf(), Marshal.SizeOf() ); + Functions.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); } } } From b3814e61d1ef9c8b135d51971e2a7dd2a02303e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Oct 2022 12:51:36 +0200 Subject: [PATCH 0545/2451] Fix misidentification bug. --- Penumbra.GameData/ObjectIdentification.cs | 3 +++ Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 93e0a52c..fa137006 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -293,6 +293,9 @@ internal class ObjectIdentification : IObjectIdentifier case CustomizationType.Iris when race == ModelRace.Unknown: set[ $"Customization: All Eyes (Catchlight)" ] = null; break; + case CustomizationType.DecalEquip: + set[ $"Equipment Decal {info.PrimaryId}" ] = null; + break; default: { var customizationString = race == ModelRace.Unknown diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs index 9515e585..42727936 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -332,7 +332,7 @@ public partial class ModEditWindow ret = true; } - ImGui.SameLine(200 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + ImGui.SameLine( 200 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) { From 1be3b06292d2a234dc7b5c2870122ad102684ddd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Oct 2022 17:23:11 +0200 Subject: [PATCH 0546/2451] Improve IMC Exception Handling. --- .../Import/TexToolsMeta.Deserialization.cs | 4 +-- Penumbra/Meta/Files/ImcFile.cs | 33 +++++++++++++++---- Penumbra/Meta/Manager/MetaManager.Imc.cs | 13 +++++--- Penumbra/Penumbra.cs | 9 +++-- Penumbra/UI/ConfigWindow.cs | 17 ++++++++-- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index e2428cbb..0142e444 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -132,7 +132,7 @@ public partial class TexToolsMeta { if( metaFileInfo.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) { - var def = new ImcFile( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, new ImcEntry() ).GamePath() ); + var def = new ImcFile( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, new ImcEntry() ) ); var partIdx = ImcFile.PartIndex( metaFileInfo.EquipSlot ); foreach( var value in values ) { @@ -148,7 +148,7 @@ public partial class TexToolsMeta { var def = new ImcFile( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, - new ImcEntry() ).GamePath() ); + new ImcEntry() ) ); foreach( var value in values ) { if( !value.Equals( def.GetEntry( 0, i ) ) ) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index dc9fe001..f1309e64 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -54,6 +55,24 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > } } +public class ImcException : Exception +{ + public readonly ImcManipulation Manipulation; + public readonly string GamePath; + + public ImcException( ImcManipulation manip, Utf8GamePath path ) + { + Manipulation = manip; + GamePath = path.ToString(); + } + + public override string Message + => "Could not obtain default Imc File.\n" + + " Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted.\n" + + $" Game Path: {GamePath}\n" + + $" Manipulation: {Manipulation}"; +} + public unsafe class ImcFile : MetaBaseFile { private const int PreambleSize = 4; @@ -174,16 +193,14 @@ public unsafe class ImcFile : MetaBaseFile } } - public ImcFile( Utf8GamePath path ) + public ImcFile( ImcManipulation manip ) : base( 0 ) { - Path = path; - var file = Dalamud.GameData.GetFile( path.ToString() ); + Path = manip.GamePath(); + var file = Dalamud.GameData.GetFile( Path.ToString() ); if( file == null ) { - throw new Exception( - "Could not obtain default Imc File.\n" - + "Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted." ); + throw new ImcException( manip, Path ); } fixed( byte* ptr = file.Data ) @@ -211,6 +228,7 @@ public unsafe class ImcFile : MetaBaseFile exists = true; return *entry; } + return new ImcEntry(); } } @@ -221,9 +239,10 @@ public unsafe class ImcFile : MetaBaseFile var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( ActualLength, 8 ); if( newData == null ) { - Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong) resource:X}, allocation failed." ); + Penumbra.Log.Error( $"Could not replace loaded IMC data at 0x{( ulong )resource:X}, allocation failed." ); return; } + Functions.MemCpyUnchecked( newData, Data, ActualLength ); Penumbra.MetaFileManager.Free( data, length ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index cb586415..5887aba2 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -58,7 +58,7 @@ public partial class MetaManager { if( !_imcFiles.TryGetValue( path, out var file ) ) { - file = new ImcFile( path ); + file = new ImcFile( manip ); } if( !manip.Apply( file ) ) @@ -75,12 +75,17 @@ public partial class MetaManager return true; } + catch( ImcException e ) + { + Penumbra.ImcExceptions.Add( e ); + Penumbra.Log.Error( e.ToString() ); + } catch( Exception e ) { - ++Penumbra.ImcExceptions; - Penumbra.Log.Error( $"Could not apply IMC Manipulation:\n{e}" ); - return false; + Penumbra.Log.Error( $"Could not apply IMC Manipulation {manip}:\n{e}" ); } + + return false; } public bool RevertMod( ImcManipulation m ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e556c740..5e886c27 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -17,7 +17,6 @@ using OtterGui.Log; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.Interop; using Penumbra.UI; using Penumbra.Util; @@ -25,7 +24,6 @@ using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; -using Action = System.Action; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -57,7 +55,7 @@ public class Penumbra : IDalamudPlugin public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; - public static int ImcExceptions = 0; + public static readonly List< Exception > ImcExceptions = new(); public readonly ResourceLogger ResourceLogger; public readonly PathResolver PathResolver; @@ -134,11 +132,10 @@ public class Penumbra : IDalamudPlugin { ResidentResources.Reload(); } - Api = new PenumbraApi( this ); IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); SubscribeItemLinks(); - if( ImcExceptions > 0 ) + if( ImcExceptions.Count > 0 ) { Log.Error( $"{ImcExceptions} IMC Exceptions thrown. Please repair your game files." ); } @@ -205,6 +202,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); ObjectReloader.RedrawAll( RedrawType.Redraw ); } + EnabledChange?.Invoke( true ); return true; @@ -227,6 +225,7 @@ public class Penumbra : IDalamudPlugin ResidentResources.Reload(); ObjectReloader.RedrawAll( RedrawType.Redraw ); } + EnabledChange?.Invoke( false ); return true; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 6a46ca1b..e9ea7eff 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Windowing; using ImGuiNET; +using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; @@ -53,9 +54,9 @@ public sealed partial class ConfigWindow : Window, IDisposable { try { - if( Penumbra.ImcExceptions > 0 ) + if( Penumbra.ImcExceptions.Count > 0 ) { - DrawProblemWindow( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "Please use the Launcher's Repair Game Files function to repair your client installation." ); @@ -108,6 +109,18 @@ public sealed partial class ConfigWindow : Window, IDisposable SettingsTab.DrawDiscordButton( 0 ); ImGui.SameLine(); SettingsTab.DrawSupportButton(); + ImGui.NewLine(); + ImGui.NewLine(); + + ImGui.TextUnformatted( "Exceptions" ); + ImGui.Separator(); + using var box = ImRaii.ListBox( "##Exceptions", new Vector2(-1, -1) ); + foreach( var exception in Penumbra.ImcExceptions ) + { + ImGuiUtil.TextWrapped( exception.ToString() ); + ImGui.Separator(); + ImGui.NewLine(); + } } public void Dispose() From af3575a05368dae3f200e8f64de51a50f24a7f87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Oct 2022 17:25:36 +0200 Subject: [PATCH 0547/2451] Add Changelogs --- Penumbra/UI/ConfigWindow.Changelog.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 7a3eed8c..221cc29f 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -28,9 +28,12 @@ public partial class ConfigWindow private static void Add5_11_0( Changelog log ) => log.NextVersion( "Version 0.5.11.0" ) + .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) .RegisterEntry( "Updated some tooltips and hints." ) + .RegisterEntry( "Improved handling of IMC exception problems." ) + .RegisterEntry( "Fixed a bug with misidentification of equipment decals." ) .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ) .RegisterEntry( "Added API to delete mods and read and set their pseudo-filesystem paths.", 1 ) .RegisterEntry( "Added API to check Penumbras enabled state and updates to it.", 1 ); From e28184376037f1bd8e576c16702e27ced8355215 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Oct 2022 16:38:04 +0200 Subject: [PATCH 0548/2451] Add character collection setting via chat command. --- Penumbra/Penumbra.cs | 66 +++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5e886c27..0f081375 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -7,6 +7,7 @@ using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using Dalamud.Utility; using EmbedIO; using EmbedIO.WebApi; using ImGuiNET; @@ -132,6 +133,7 @@ public class Penumbra : IDalamudPlugin { ResidentResources.Reload(); } + Api = new PenumbraApi( this ); IpcProviders = new PenumbraIpcProviders( Dalamud.PluginInterface, Api ); SubscribeItemLinks(); @@ -298,11 +300,30 @@ public class Penumbra : IDalamudPlugin CharacterUtility?.Dispose(); } - public static bool SetCollection( string type, string collectionName ) + public static bool SetCollection( string typeName, string collectionName ) { - type = type.ToLowerInvariant(); - collectionName = collectionName.ToLowerInvariant(); + if( !Enum.TryParse< CollectionType >( typeName, true, out var type ) || type == CollectionType.Inactive ) + { + Dalamud.Chat.Print( + "Second command argument is not a valid collection type, the correct command format is: /penumbra collection [| characterName]" ); + return false; + } + string? characterName = null; + if( type is CollectionType.Character ) + { + var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); + if( split.Length < 2 || split[ 0 ].Length == 0 || split[ 1 ].Length == 0 ) + { + Dalamud.Chat.Print( "You need to provide a collection and a character name in the form of 'collection | character' to set a character collection." ); + return false; + } + + collectionName = split[ 0 ]; + characterName = split[ 1 ]; + } + + collectionName = collectionName.ToLowerInvariant(); var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) ? ModCollection.Empty : CollectionManager[ collectionName ]; @@ -312,34 +333,21 @@ public class Penumbra : IDalamudPlugin return false; } - foreach( var t in Enum.GetValues< CollectionType >() ) + var oldCollection = CollectionManager.ByType( type, characterName ); + if( collection == oldCollection ) { - if( t is CollectionType.Inactive or CollectionType.Character - || !string.Equals( t.ToString(), type, StringComparison.OrdinalIgnoreCase ) ) - { - continue; - } - - var oldCollection = CollectionManager.ByType( t ); - if( collection == oldCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the {t.ToName()} Collection." ); - return false; - } - - if( oldCollection == null && t.IsSpecial() ) - { - CollectionManager.CreateSpecialCollection( t ); - } - - CollectionManager.SetCollection( collection, t, null ); - Dalamud.Chat.Print( $"Set {collection.Name} as {t.ToName()} Collection." ); - return true; + Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); + return false; } - Dalamud.Chat.Print( - "Second command argument is not default, the correct command format is: /penumbra collection " ); - return false; + if( oldCollection == null && type.IsSpecial() ) + { + CollectionManager.CreateSpecialCollection( type ); + } + + CollectionManager.SetCollection( collection, type, characterName ); + Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); + return true; } private void OnCommand( string command, string rawArgs ) @@ -419,7 +427,7 @@ public class Penumbra : IDalamudPlugin else { Dalamud.Chat.Print( "Missing arguments, the correct command format is:" - + " /penumbra collection {default} " ); + + " /penumbra collection {default} [|characterName]" ); } break; From b9662e39a901730973c136d8d872936af856f8d2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Oct 2022 13:00:13 +0200 Subject: [PATCH 0549/2451] Fix assigning current default to new character. --- Penumbra/Penumbra.cs | 11 +++++++++-- Penumbra/UI/ConfigWindow.Changelog.cs | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0f081375..3bfb3568 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -340,9 +340,16 @@ public class Penumbra : IDalamudPlugin return false; } - if( oldCollection == null && type.IsSpecial() ) + if( oldCollection == null ) { - CollectionManager.CreateSpecialCollection( type ); + if( type.IsSpecial() ) + { + CollectionManager.CreateSpecialCollection( type ); + } + else if( type is CollectionType.Character ) + { + CollectionManager.CreateCharacterCollection( characterName! ); + } } CollectionManager.SetCollection( collection, type, characterName ); diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 221cc29f..75285eb3 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -34,6 +34,7 @@ public partial class ConfigWindow .RegisterEntry( "Updated some tooltips and hints." ) .RegisterEntry( "Improved handling of IMC exception problems." ) .RegisterEntry( "Fixed a bug with misidentification of equipment decals." ) + .RegisterEntry( "Character collections can now be set via chat command, too. (/penumbra collection character | )" ) .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ) .RegisterEntry( "Added API to delete mods and read and set their pseudo-filesystem paths.", 1 ) .RegisterEntry( "Added API to check Penumbras enabled state and updates to it.", 1 ); From ccfc05f2b28e89478283bfc23e185b480b96e066 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 19 Oct 2022 01:01:40 +0200 Subject: [PATCH 0550/2451] Add local data, favorites and tags. --- OtterGui | 2 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 16 +- Penumbra/Mods/Manager/Mod.Manager.Local.cs | 75 ++++++++ Penumbra/Mods/Manager/Mod.Manager.Meta.cs | 17 +- Penumbra/Mods/Mod.BasePath.cs | 10 +- Penumbra/Mods/Mod.LocalData.cs | 167 ++++++++++++++++++ Penumbra/Mods/Mod.Meta.Migration.cs | 21 ++- Penumbra/Mods/Mod.Meta.cs | 63 ++++--- Penumbra/Mods/ModFileSystem.cs | 8 +- Penumbra/Penumbra.cs | 1 + .../Classes/ModFileSystemSelector.Filters.cs | 15 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 15 +- Penumbra/UI/Classes/ModFilter.cs | 38 ++-- Penumbra/UI/ConfigWindow.Changelog.cs | 7 + Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 11 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 20 ++- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 76 +++++++- Penumbra/UI/ConfigWindow.ModsTab.cs | 9 +- Penumbra/UI/ConfigWindow.Tutorial.cs | 4 + 19 files changed, 481 insertions(+), 94 deletions(-) create mode 100644 Penumbra/Mods/Manager/Mod.Manager.Local.cs create mode 100644 Penumbra/Mods/Mod.LocalData.cs diff --git a/OtterGui b/OtterGui index 1e0d04b9..0d2284a8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1e0d04b90043faad979c3e7316a733870eb16108 +Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834 diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 99ab7aaa..9902ed57 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -58,6 +58,7 @@ public partial class Mod return; } + MoveDataFile( oldDirectory, BasePath ); new ModBackup( mod ).Move( null, dir.Name ); dir.Refresh(); @@ -69,9 +70,9 @@ public partial class Mod } ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath ); - if( metaChange != MetaChangeType.None ) + if( metaChange != ModDataChangeType.None ) { - ModMetaChanged?.Invoke( metaChange, mod, oldName ); + ModDataChanged?.Invoke( metaChange, mod, oldName ); } } @@ -94,9 +95,9 @@ public partial class Mod } ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); - if( metaChange != MetaChangeType.None ) + if( metaChange != ModDataChangeType.None ) { - ModMetaChanged?.Invoke( metaChange, mod, oldName ); + ModDataChanged?.Invoke( metaChange, mod, oldName ); } } @@ -211,6 +212,13 @@ public partial class Mod break; case ModPathChangeType.Deleted: NewMods.Remove( mod ); + break; + case ModPathChangeType.Moved: + if( oldDirectory != null && newDirectory != null ) + { + MoveDataFile( oldDirectory, newDirectory ); + } + break; } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs new file mode 100644 index 00000000..cecf73b4 --- /dev/null +++ b/Penumbra/Mods/Manager/Mod.Manager.Local.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ImGuiScene; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public partial class Manager + { + public void ChangeModFavorite( Index idx, bool state ) + { + var mod = this[ idx ]; + if( mod.Favorite != state ) + { + mod.Favorite = state; + mod.SaveLocalData(); + ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); + } + } + + public void ChangeModNote( Index idx, string newNote ) + { + var mod = this[ idx ]; + if( mod.Note != newNote ) + { + mod.Note = newNote; + mod.SaveLocalData(); + ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); + } + } + + + private void ChangeTag( Index idx, int tagIdx, string newTag, bool local ) + { + var mod = this[ idx ]; + var which = local ? mod.LocalTags : mod.ModTags; + if( tagIdx < 0 || tagIdx > which.Count ) + { + return; + } + + ModDataChangeType flags = 0; + if( tagIdx == which.Count ) + { + flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null ); + } + else + { + var tmp = which.ToArray(); + tmp[ tagIdx ] = newTag; + flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null ); + } + + if( flags.HasFlag( ModDataChangeType.ModTags ) ) + { + mod.SaveMeta(); + } + + if( flags.HasFlag( ModDataChangeType.LocalTags ) ) + { + mod.SaveLocalData(); + } + + if( flags != 0 ) + { + ModDataChanged?.Invoke( flags, mod, null ); + } + } + + public void ChangeLocalTag( Index idx, int tagIdx, string newTag ) + => ChangeTag( idx, tagIdx, newTag, true ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs index 312ee9cf..9c4d48ee 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs @@ -6,8 +6,8 @@ public sealed partial class Mod { public partial class Manager { - public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod mod, string? oldName ); - public event ModMetaChangeDelegate? ModMetaChanged; + public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName ); + public event ModDataChangeDelegate? ModDataChanged; public void ChangeModName( Index idx, string newName ) { @@ -17,7 +17,7 @@ public sealed partial class Mod var oldName = mod.Name; mod.Name = newName; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Name, mod, oldName.Text ); + ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text ); } } @@ -28,7 +28,7 @@ public sealed partial class Mod { mod.Author = newAuthor; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Author, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null ); } } @@ -39,7 +39,7 @@ public sealed partial class Mod { mod.Description = newDescription; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Description, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null ); } } @@ -50,7 +50,7 @@ public sealed partial class Mod { mod.Version = newVersion; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Version, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null ); } } @@ -61,8 +61,11 @@ public sealed partial class Mod { mod.Website = newWebsite; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null ); } } + + public void ChangeModTag( Index idx, int tagIdx, string newTag ) + => ChangeTag( idx, tagIdx, newTag, false ); } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 22760adf..42ebb23e 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -47,21 +47,23 @@ public partial class Mod return mod; } - private bool Reload( bool incorporateMetaChanges, out MetaChangeType metaChange ) + private bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { - metaChange = MetaChangeType.Deletion; + modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); if( !ModPath.Exists ) { return false; } - metaChange = LoadMeta(); - if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 ) + modDataChange = LoadMeta(); + if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 ) { return false; } + LoadLocalData(); + LoadDefaultOption(); LoadAllGroups(); if( incorporateMetaChanges ) diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs new file mode 100644 index 00000000..21841d3a --- /dev/null +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -0,0 +1,167 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public static DirectoryInfo LocalDataDirectory + => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data" )); + + public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + + public IReadOnlyList< string > LocalTags { get; private set; } = Array.Empty< string >(); + + public string AllTagsLower { get; private set; } = string.Empty; + public string Note { get; private set; } = string.Empty; + public bool Favorite { get; private set; } = false; + + private FileInfo LocalDataFile + => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" )); + + private ModDataChangeType LoadLocalData() + { + var dataFile = LocalDataFile; + + var importDate = 0L; + var localTags = Enumerable.Empty< string >(); + var favorite = false; + var note = string.Empty; + + var save = true; + if( File.Exists( dataFile.FullName ) ) + { + save = false; + try + { + var text = File.ReadAllText( dataFile.FullName ); + var json = JObject.Parse( text ); + + importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate; + favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite; + note = json[ nameof( Note ) ]?.Value< string >() ?? note; + localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags; + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load local mod data:\n{e}" ); + } + } + + if( importDate == 0 ) + { + importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + ModDataChangeType changes = 0; + if( ImportDate != importDate ) + { + ImportDate = importDate; + changes |= ModDataChangeType.ImportDate; + } + + changes |= UpdateTags( null, localTags ); + + if( Favorite != favorite ) + { + Favorite = favorite; + changes |= ModDataChangeType.Favorite; + } + + if( Note != note ) + { + Note = note; + changes |= ModDataChangeType.Note; + } + + if( save ) + { + SaveLocalDataFile(); + } + + return changes; + } + + private void SaveLocalData() + => Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile ); + + private void SaveLocalDataFile() + { + var dataFile = LocalDataFile; + try + { + var jObject = new JObject + { + { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, + { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, + { nameof( LocalTags ), JToken.FromObject( LocalTags ) }, + { nameof( Note ), JToken.FromObject( Note ) }, + { nameof( Favorite ), JToken.FromObject( Favorite ) }, + }; + dataFile.Directory!.Create(); + File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" ); + } + } + + private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod ) + { + var oldFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" ); + var newFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" ); + if( File.Exists( oldFile ) ) + { + try + { + File.Move( oldFile, newFile, true ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" ); + } + } + } + + private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags ) + { + if( newModTags == null && newLocalTags == null ) + { + return 0; + } + + ModDataChangeType type = 0; + if( newModTags != null ) + { + var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray(); + if( !modTags.SequenceEqual( ModTags ) ) + { + newLocalTags ??= LocalTags; + ModTags = modTags; + type |= ModDataChangeType.ModTags; + } + } + + if( newLocalTags != null ) + { + var localTags = newLocalTags!.Where( t => t.Length > 0 && !ModTags.Contains( t ) ).Distinct().ToArray(); + if( !localTags.SequenceEqual( LocalTags ) ) + { + LocalTags = localTags; + type |= ModDataChangeType.LocalTags; + } + } + + if( type != 0 ) + { + AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) ); + } + + return type; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 69e33628..3710896e 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -16,7 +16,20 @@ public sealed partial class Mod private static class Migration { public static bool Migrate( Mod mod, JObject json ) - => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ); + => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod ); + + private static bool MigrateV2ToV3( Mod mod ) + { + if( mod.FileVersion > 2 ) + { + return false; + } + + // Remove import time. + mod.FileVersion = 3; + mod.SaveMeta(); + return true; + } private static bool MigrateV1ToV2( Mod mod ) { @@ -56,8 +69,8 @@ public sealed partial class Mod var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - var priority = 1; + var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var priority = 1; var seenMetaFiles = new HashSet< FullPath >(); foreach( var group in groups.Values ) { @@ -187,7 +200,7 @@ public sealed partial class Mod private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) { - var subMod = new SubMod(mod) { Name = option.OptionName }; + var subMod = new SubMod( mod ) { Name = option.OptionName }; AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); subMod.IncorporateMetaChanges( mod.ModPath, false ); return subMod; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 21db4857..a03377ec 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; @@ -7,17 +9,21 @@ using OtterGui.Classes; namespace Penumbra.Mods; [Flags] -public enum MetaChangeType : ushort +public enum ModDataChangeType : ushort { - None = 0x00, - Name = 0x01, - Author = 0x02, - Description = 0x04, - Version = 0x08, - Website = 0x10, - Deletion = 0x20, - Migration = 0x40, - ImportDate = 0x80, + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, } public sealed partial class Mod @@ -29,25 +35,25 @@ public sealed partial class Mod Priority = int.MaxValue, }; - public const uint CurrentFileVersion = 1; + public const uint CurrentFileVersion = 3; public uint FileVersion { get; private set; } = CurrentFileVersion; public LowerString Name { get; private set; } = "New Mod"; public LowerString Author { get; private set; } = LowerString.Empty; public string Description { get; private set; } = string.Empty; public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; - public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >(); internal FileInfo MetaFile => new(Path.Combine( ModPath.FullName, "meta.json" )); - private MetaChangeType LoadMeta() + private ModDataChangeType LoadMeta() { var metaFile = MetaFile; if( !File.Exists( metaFile.FullName ) ) { Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." ); - return MetaChangeType.Deletion; + return ModDataChangeType.Deletion; } try @@ -61,36 +67,37 @@ public sealed partial class Mod var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; - var importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var importDate = json[ nameof( ImportDate ) ]?.Value< long >(); + var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >(); - MetaChangeType changes = 0; + ModDataChangeType changes = 0; if( Name != newName ) { - changes |= MetaChangeType.Name; + changes |= ModDataChangeType.Name; Name = newName; } if( Author != newAuthor ) { - changes |= MetaChangeType.Author; + changes |= ModDataChangeType.Author; Author = newAuthor; } if( Description != newDescription ) { - changes |= MetaChangeType.Description; + changes |= ModDataChangeType.Description; Description = newDescription; } if( Version != newVersion ) { - changes |= MetaChangeType.Version; + changes |= ModDataChangeType.Version; Version = newVersion; } if( Website != newWebsite ) { - changes |= MetaChangeType.Website; + changes |= ModDataChangeType.Website; Website = newWebsite; } @@ -99,22 +106,24 @@ public sealed partial class Mod FileVersion = newFileVersion; if( Migration.Migrate( this, json ) ) { - changes |= MetaChangeType.Migration; + changes |= ModDataChangeType.Migration; } } - if( ImportDate != importDate ) + if( importDate != null && ImportDate != importDate.Value ) { - ImportDate = importDate; - changes |= MetaChangeType.ImportDate; + ImportDate = importDate.Value; + changes |= ModDataChangeType.ImportDate; } + changes |= UpdateTags( modTags, null ); + return changes; } catch( Exception e ) { Penumbra.Log.Error( $"Could not load mod meta:\n{e}" ); - return MetaChangeType.Deletion; + return ModDataChangeType.Deletion; } } @@ -134,7 +143,7 @@ public sealed partial class Mod { nameof( Description ), JToken.FromObject( Description ) }, { nameof( Version ), JToken.FromObject( Version ) }, { nameof( Website ), JToken.FromObject( Website ) }, - { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, + { nameof( ModTags ), JToken.FromObject( ModTags ) }, }; File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); } diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index c8cbdf22..9b75dbc4 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -33,7 +33,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable ret.Changed += ret.OnChange; Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange; + Penumbra.ModManager.ModDataChanged += ret.OnDataChange; Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; return ret; @@ -43,7 +43,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { Penumbra.ModManager.ModPathChanged -= OnModPathChange; Penumbra.ModManager.ModDiscoveryFinished -= Reload; - Penumbra.ModManager.ModMetaChanged -= OnMetaChange; + Penumbra.ModManager.ModDataChanged -= OnDataChange; } public struct ImportDate : ISortMode< Mod > @@ -92,9 +92,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable } // Update sort order when defaulted mod names change. - private void OnMetaChange( MetaChangeType type, Mod mod, string? oldName ) + private void OnDataChange( ModDataChangeType type, Mod mod, string? oldName ) { - if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) + if( type.HasFlag( ModDataChangeType.Name ) && oldName != null ) { var old = oldName.FixName(); if( Find( old, out var child ) && child is not Folder ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3bfb3568..57648940 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -454,6 +454,7 @@ public class Penumbra : IDalamudPlugin var list = Directory.Exists( collectionDir ) ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() : new List< FileInfo >(); + list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); list.Add( Dalamud.PluginInterface.ConfigFile ); list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 79156cb5..93a3e9cd 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -29,8 +29,9 @@ public partial class ModFileSystemSelector private void SetFilterTooltip() { FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" - + "Enter n:[string] to filter only for mod names and no paths.\n" + "Enter c:[string] to filter for mods changing specific items.\n" + + "Enter t:[string] to filter for mods set to specific tags.\n" + + "Enter n:[string] to filter only for mod names and no paths.\n" + "Enter a:[string] to filter for mods by specific authors."; } @@ -49,6 +50,8 @@ public partial class ModFileSystemSelector 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), + 't' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), + 'T' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), _ => ( new LowerString( filterValue ), 0 ), }, _ => ( new LowerString( filterValue ), 0 ), @@ -96,7 +99,8 @@ public partial class ModFileSystemSelector 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), 1 => !mod.Name.Contains( _modFilter ), 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower, IgnoreCase ), + 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower ), + 4 => !mod.AllTagsLower.Contains( _modFilter.Lower ), _ => false, // Should never happen }; } @@ -143,6 +147,13 @@ public partial class ModFileSystemSelector return true; } + // Handle Favoritism + if( !_stateFilter.HasFlag( ModFilter.Favorite ) && mod.Favorite + || !_stateFilter.HasFlag( ModFilter.NotFavorite ) && !mod.Favorite ) + { + return true; + } + // Handle Inheritance if( collection == Penumbra.CollectionManager.Current ) { diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 30d94f02..f5ffdcf9 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -43,7 +43,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; - Penumbra.ModManager.ModMetaChanged += OnModMetaChange; + Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); @@ -54,7 +54,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod base.Dispose(); Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; - Penumbra.ModManager.ModMetaChanged -= OnModMetaChange; + Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; @@ -120,7 +120,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); using var id = ImRaii.PushId( leaf.Value.Index ); - using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); + ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); } @@ -347,12 +347,15 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } - private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName ) + private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName ) { switch( type ) { - case MetaChangeType.Name: - case MetaChangeType.Author: + case ModDataChangeType.Name: + case ModDataChangeType.Author: + case ModDataChangeType.ModTags: + case ModDataChangeType.LocalTags: + case ModDataChangeType.Favorite: SetFilterDirty(); break; } diff --git a/Penumbra/UI/Classes/ModFilter.cs b/Penumbra/UI/Classes/ModFilter.cs index 8812a203..3c68f15c 100644 --- a/Penumbra/UI/Classes/ModFilter.cs +++ b/Penumbra/UI/Classes/ModFilter.cs @@ -7,33 +7,37 @@ public enum ModFilter { Enabled = 1 << 0, Disabled = 1 << 1, - NoConflict = 1 << 2, - SolvedConflict = 1 << 3, - UnsolvedConflict = 1 << 4, - HasNoMetaManipulations = 1 << 5, - HasMetaManipulations = 1 << 6, - HasNoFileSwaps = 1 << 7, - HasFileSwaps = 1 << 8, - HasConfig = 1 << 9, - HasNoConfig = 1 << 10, - HasNoFiles = 1 << 11, - HasFiles = 1 << 12, - IsNew = 1 << 13, - NotNew = 1 << 14, - Inherited = 1 << 15, - Uninherited = 1 << 16, - Undefined = 1 << 17, + Favorite = 1 << 2, + NotFavorite = 1 << 3, + NoConflict = 1 << 4, + SolvedConflict = 1 << 5, + UnsolvedConflict = 1 << 6, + HasNoMetaManipulations = 1 << 7, + HasMetaManipulations = 1 << 8, + HasNoFileSwaps = 1 << 9, + HasFileSwaps = 1 << 10, + HasConfig = 1 << 11, + HasNoConfig = 1 << 12, + HasNoFiles = 1 << 13, + HasFiles = 1 << 14, + IsNew = 1 << 15, + NotNew = 1 << 16, + Inherited = 1 << 17, + Uninherited = 1 << 18, + Undefined = 1 << 19, }; public static class ModFilterExtensions { - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 18 ) - 1 ); + public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 20 ) - 1 ); public static string ToName( this ModFilter filter ) => filter switch { ModFilter.Enabled => "Enabled", ModFilter.Disabled => "Disabled", + ModFilter.Favorite => "Favorite", + ModFilter.NotFavorite => "No Favorite", ModFilter.NoConflict => "No Conflicts", ModFilter.SolvedConflict => "Solved Conflicts", ModFilter.UnsolvedConflict => "Unsolved Conflicts", diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 75285eb3..138c1d64 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -22,12 +22,19 @@ public partial class ConfigWindow Add5_8_7( ret ); Add5_9_0( ret ); Add5_10_0( ret ); + Add5_11_0( ret ); return ret; } private static void Add5_11_0( Changelog log ) => log.NextVersion( "Version 0.5.11.0" ) + .RegisterEntry( + "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections." ) + .RegisterEntry( "Moved the import date from mod metadata to local data.", 1 ) + .RegisterEntry( "Added Favorites. You can declare mods as favorites and filter for them.", 1 ) + .RegisterEntry( "Added Local Tags. You can apply custom Tags to mods and filter for them.", 1 ) + .RegisterEntry( "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported." ) .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index e5948ee8..6b09d7dc 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -55,6 +55,13 @@ public partial class ConfigWindow } } + ImGui.Dummy( _window._defaultSpace ); + var tagIdx = _modTags.Draw( "Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag ); + if( tagIdx >= 0 ) + { + Penumbra.ModManager.ChangeModTag( _mod.Index, tagIdx, editedTag ); + } + ImGui.Dummy( _window._defaultSpace ); AddOptionGroup.Draw( _window, _mod ); ImGui.Dummy( _window._defaultSpace ); @@ -566,7 +573,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; + var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; @@ -642,7 +649,7 @@ public partial class ConfigWindow { GroupType.Single => "Single Group", GroupType.Multi => "Multi Group", - _ => "Unknown", + _ => "Unknown", }; ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index feacb0bc..dfe692b5 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -58,16 +58,20 @@ public partial class ConfigWindow DrawPriorityInput(); OpenTutorial( BasicTutorialSteps.Priority ); DrawRemoveSettings(); - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) - { - DrawSingleGroup( _mod.Groups[ idx ], idx ); - } - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + if( _mod.Groups.Count > 0 ) { - DrawMultiGroup( _mod.Groups[ idx ], idx ); + ImGui.Dummy( _window._defaultSpace ); + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawSingleGroup( _mod.Groups[ idx ], idx ); + } + + ImGui.Dummy( _window._defaultSpace ); + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawMultiGroup( _mod.Groups[ idx ], idx ); + } } _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index c920b366..5fc8dcea 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -1,9 +1,11 @@ using System; using System.Numerics; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -36,10 +38,12 @@ public partial class ConfigWindow private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false ); private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false ); + private readonly TagButtons _modTags = new(); + private void DrawTabBar() { - ImGui.Dummy( _window._defaultSpace ); - using var tabBar = ImRaii.TabBar( "##ModTabs" ); + var tabBarHeight = ImGui.GetCursorPosY(); + using var tabBar = ImRaii.TabBar( "##ModTabs" ); if( !tabBar ) { return; @@ -47,8 +51,8 @@ public partial class ConfigWindow _availableTabs = Tabs.Settings | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) - | ( _mod.Description.Length > 0 ? Tabs.Description : 0 ) - | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) + | Tabs.Description + | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) | Tabs.Edit; DrawSettingsTab(); @@ -56,6 +60,12 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); + DrawAdvancedEditingButton(); + DrawFavoriteButton( tabBarHeight ); + } + + private void DrawAdvancedEditingButton() + { if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { _window.ModEditPopup.ChangeMod( _mod ); @@ -73,6 +83,44 @@ public partial class ConfigWindow + "\t\t- textures" ); } + private void DrawFavoriteButton( float height ) + { + var oldPos = ImGui.GetCursorPos(); + + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + var size = ImGui.CalcTextSize( FontAwesomeIcon.Star.ToIconString() ) + ImGui.GetStyle().FramePadding * 2; + var newPos = new Vector2( ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height ); + if( ImGui.GetScrollMaxX() > 0 ) + { + newPos.X += ImGui.GetScrollX(); + } + + var rectUpper = ImGui.GetWindowPos() + newPos; + var color = ImGui.IsMouseHoveringRect( rectUpper, rectUpper + size ) ? ImGui.GetColorU32( ImGuiCol.Text ) : + _mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32( ImGuiCol.TextDisabled ); + using var c = ImRaii.PushColor( ImGuiCol.Text, color ) + .Push( ImGuiCol.Button, 0 ) + .Push( ImGuiCol.ButtonHovered, 0 ) + .Push( ImGuiCol.ButtonActive, 0 ); + + ImGui.SetCursorPos( newPos ); + if( ImGui.Button( FontAwesomeIcon.Star.ToIconString() ) ) + { + Penumbra.ModManager.ChangeModFavorite( _mod.Index, !_mod.Favorite ); + } + } + + var hovered = ImGui.IsItemHovered(); + OpenTutorial( BasicTutorialSteps.Favorites ); + + if( hovered ) + { + ImGui.SetTooltip( "Favorite" ); + } + } + + // Just a simple text box with the wrapped description, if it exists. private void DrawDescriptionTab() { @@ -88,6 +136,26 @@ public partial class ConfigWindow return; } + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + var tagIdx = _localTags.Draw( "Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _mod.LocalTags, + out var editedTag ); + OpenTutorial( BasicTutorialSteps.Tags ); + if( tagIdx >= 0 ) + { + Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); + } + if( _mod.ModTags.Count > 0 ) + { + _modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false, + ImGui.CalcTextSize( "Local " ).X - ImGui.CalcTextSize( "Mod " ).X ); + } + + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + ImGui.Separator(); + ImGuiUtil.TextWrapped( _mod.Description ); } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 57c48c9e..eb3a3a8a 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -8,8 +8,8 @@ using System; using System.Linq; using System.Numerics; using Dalamud.Interface; +using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; namespace Penumbra.UI; @@ -198,9 +198,10 @@ public partial class ConfigWindow { private readonly ConfigWindow _window; - private bool _valid; - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; + private bool _valid; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + private readonly TagButtons _localTags = new(); public ModPanel( ConfigWindow window ) => _window = window; diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index a5b34840..5826d3f1 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -81,6 +81,8 @@ public partial class ConfigWindow Faq1, Faq2, Faq3, + Favorites, + Tags, } public static readonly Tutorial Tutorial = new Tutorial() @@ -159,5 +161,7 @@ public partial class ConfigWindow .Register( "FAQ 2", "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ) + .Register( "Favorites", "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections." ) + .Register( "Tags", "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'." ) .EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length ); } \ No newline at end of file From cb4f9f81314d2b20e4e48c80224fb9bbc055c54c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Oct 2022 14:25:28 +0200 Subject: [PATCH 0551/2451] Make migration and immediate file saving somewhat more stable, actually dispose Framework. --- Penumbra/Mods/Mod.Files.cs | 2 +- Penumbra/Mods/Mod.Meta.Migration.cs | 38 +++++++++++++++++++---------- Penumbra/Penumbra.cs | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 3f6a79b4..b09f680c 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -139,7 +139,7 @@ public partial class Mod foreach( var (group, index) in _groups.WithIndex() ) { - IModGroup.SaveDelayed( group, ModPath, index ); + IModGroup.Save( group, ModPath, index ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 3710896e..8d907be9 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -16,7 +16,16 @@ public sealed partial class Mod private static class Migration { public static bool Migrate( Mod mod, JObject json ) - => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod ); + { + var ret = MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod ); + if( ret ) + { + // Immediately save on migration. + mod.SaveMetaFile(); + } + + return ret; + } private static bool MigrateV2ToV3( Mod mod ) { @@ -27,10 +36,11 @@ public sealed partial class Mod // Remove import time. mod.FileVersion = 3; - mod.SaveMeta(); return true; } + + private static readonly Regex GroupRegex = new( @"group_\d{3}_", RegexOptions.Compiled ); private static bool MigrateV1ToV2( Mod mod ) { if( mod.FileVersion > 1 ) @@ -38,24 +48,26 @@ public sealed partial class Mod return false; } - foreach( var (group, index) in mod.GroupFiles.WithIndex().ToArray() ) + if (!mod.GroupFiles.All( g => GroupRegex.IsMatch( g.Name ))) { - var newName = Regex.Replace( group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled ); - try + foreach( var (group, index) in mod.GroupFiles.WithIndex().ToArray() ) { - if( newName != group.Name ) + var newName = Regex.Replace( group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled ); + try { - group.MoveTo( Path.Combine( group.DirectoryName ?? string.Empty, newName ), false ); + if( newName != group.Name ) + { + group.MoveTo( Path.Combine( group.DirectoryName ?? string.Empty, newName ), false ); + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not rename group file {group.Name} to {newName} during migration:\n{e}" ); } } mod.FileVersion = 2; - mod.SaveMeta(); return true; } @@ -128,7 +140,7 @@ public sealed partial class Mod mod.FileVersion = 1; mod.SaveDefaultMod(); - mod.SaveMeta(); + mod.SaveMetaFile(); return true; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 57648940..791bac47 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -7,7 +7,6 @@ using System.Text; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using Dalamud.Utility; using EmbedIO; using EmbedIO.WebApi; using ImGuiNET; @@ -291,6 +290,7 @@ public class Penumbra : IDalamudPlugin ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); + Framework?.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); From a49e3312d35a18961cd47cce66e6831d0639b13b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Oct 2022 14:39:06 +0200 Subject: [PATCH 0552/2451] Add Changelog. --- Penumbra/UI/ConfigWindow.Changelog.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 138c1d64..ea677f6c 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -23,10 +23,21 @@ public partial class ConfigWindow Add5_9_0( ret ); Add5_10_0( ret ); Add5_11_0( ret ); + Add5_11_1( ret ); return ret; } + private static void Add5_11_1( Changelog log ) + => log.NextVersion( "Version 0.5.11.1" ) + .RegisterEntry( + "The 0.5.11.0 Update exposed an issue in Penumbras file-saving scheme that rarely could cause some, most or even all of your mods to lose their group information." ) + .RegisterEntry( "If this has happened to you, you will need to reimport affected mods, or manually restore their groups. I am very sorry for that.", 1 ) + .RegisterEntry( + "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", + 1 ) + .RegisterHighlight( "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", 1 ); + private static void Add5_11_0( Changelog log ) => log.NextVersion( "Version 0.5.11.0" ) .RegisterEntry( From bc901f3ff699132d11e2ae1d524ce00e34a44cee Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 20 Oct 2022 12:41:49 +0000 Subject: [PATCH 0553/2451] [CI] Updating repo.json for refs/tags/0.5.11.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1dd7d668..2dd04e6d 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.10.0", - "TestingAssemblyVersion": "0.5.10.0", + "AssemblyVersion": "0.5.11.1", + "TestingAssemblyVersion": "0.5.11.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 7, @@ -16,9 +16,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.10.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.11.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 35baba18bf31af8120262e7eea4a88e9514147c2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Oct 2022 15:53:45 +0200 Subject: [PATCH 0554/2451] Extract Strings to separate submodule. --- .gitmodules | 4 + Penumbra.GameData/Actors/ActorIdentifier.cs | 152 ++++++++ Penumbra.GameData/Actors/ActorManager.cs | 352 ++++++++++++++++++ Penumbra.GameData/Actors/IdentifierType.cs | 10 + Penumbra.GameData/Actors/SpecialActor.cs | 12 + .../ByteString/ByteStringFunctions.Case.cs | 105 ------ .../ByteStringFunctions.Comparison.cs | 95 ----- .../ByteStringFunctions.Construction.cs | 68 ---- .../ByteStringFunctions.Manipulation.cs | 45 --- Penumbra.GameData/ByteString/FullPath.cs | 134 ------- Penumbra.GameData/ByteString/Utf8GamePath.cs | 168 --------- Penumbra.GameData/ByteString/Utf8RelPath.cs | 143 ------- .../ByteString/Utf8String.Access.cs | 75 ---- .../ByteString/Utf8String.Comparison.cs | 140 ------- .../ByteString/Utf8String.Construction.cs | 215 ----------- .../ByteString/Utf8String.Manipulation.cs | 168 --------- Penumbra.GameData/Enums/ResourceType.cs | 7 +- Penumbra.GameData/Penumbra.GameData.csproj | 5 + Penumbra.GameData/Structs/CharacterEquip.cs | 5 +- Penumbra.GameData/Structs/CustomizeData.cs | 10 +- Penumbra.GameData/Util/Functions.cs | 157 -------- Penumbra.String | 1 + Penumbra.sln | 6 + Penumbra/Api/IpcTester.cs | 9 +- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Api/TempModManager.cs | 2 +- .../Collections/CollectionManager.Active.cs | 8 +- .../Collections/ModCollection.Cache.Access.cs | 3 +- Penumbra/Collections/ModCollection.Cache.cs | 11 +- Penumbra/Import/Textures/Texture.cs | 3 +- .../Interop/CharacterUtility.DecalReverter.cs | 2 +- .../Interop/Loader/ResourceLoader.Debug.cs | 9 +- .../Loader/ResourceLoader.Replacement.cs | 19 +- .../Interop/Loader/ResourceLoader.TexMdl.cs | 8 +- Penumbra/Interop/Loader/ResourceLoader.cs | 7 +- Penumbra/Interop/Loader/ResourceLogger.cs | 5 +- .../Resolver/PathResolver.AnimationState.cs | 2 +- .../Resolver/PathResolver.DrawObjectState.cs | 4 +- .../Resolver/PathResolver.Identification.cs | 10 +- .../Interop/Resolver/PathResolver.Material.cs | 7 +- .../Resolver/PathResolver.PathState.cs | 14 +- Penumbra/Interop/Resolver/PathResolver.cs | 5 +- Penumbra/Meta/Files/CmpFile.cs | 3 +- Penumbra/Meta/Files/EqdpFile.cs | 9 +- Penumbra/Meta/Files/EqpGmpFile.cs | 5 +- Penumbra/Meta/Files/EstFile.cs | 5 +- Penumbra/Meta/Files/ImcFile.cs | 15 +- Penumbra/Meta/Files/MetaBaseFile.cs | 7 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 5 +- .../Meta/Manipulations/ImcManipulation.cs | 2 +- .../Meta/Manipulations/MetaManipulation.cs | 3 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 4 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 2 +- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 2 +- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 2 +- Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Mods/Mod.ChangedItems.cs | 3 +- Penumbra/Mods/Mod.Creation.cs | 2 +- Penumbra/Mods/Mod.Files.cs | 2 +- Penumbra/Mods/Mod.Meta.Migration.cs | 2 +- Penumbra/Mods/Mod.TemporaryMod.cs | 4 +- Penumbra/Mods/Subclasses/ISubMod.cs | 2 +- Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 2 +- Penumbra/Penumbra.cs | 11 +- Penumbra/UI/Classes/ModEditWindow.FileEdit.cs | 12 +- Penumbra/UI/Classes/ModEditWindow.Files.cs | 3 +- Penumbra/UI/Classes/ModEditWindow.cs | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 16 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 31 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 2 +- Penumbra/UI/ConfigWindow.Misc.cs | 15 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 16 +- Penumbra/UI/ConfigWindow.ResourceTab.cs | 2 +- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 2 +- Penumbra/packages.lock.json | 6 +- 75 files changed, 751 insertions(+), 1657 deletions(-) create mode 100644 Penumbra.GameData/Actors/ActorIdentifier.cs create mode 100644 Penumbra.GameData/Actors/ActorManager.cs create mode 100644 Penumbra.GameData/Actors/IdentifierType.cs create mode 100644 Penumbra.GameData/Actors/SpecialActor.cs delete mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs delete mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs delete mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs delete mode 100644 Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs delete mode 100644 Penumbra.GameData/ByteString/FullPath.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8GamePath.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8RelPath.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8String.Access.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8String.Comparison.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8String.Construction.cs delete mode 100644 Penumbra.GameData/ByteString/Utf8String.Manipulation.cs delete mode 100644 Penumbra.GameData/Util/Functions.cs create mode 160000 Penumbra.String diff --git a/.gitmodules b/.gitmodules index b5eb77bb..94049366 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ path = Penumbra.Api url = git@github.com:Ottermandias/Penumbra.Api.git branch = main +[submodule "Penumbra.String"] + path = Penumbra.String + url = git@github.com:Ottermandias/Penumbra.String.git + branch = main diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs new file mode 100644 index 00000000..5651382e --- /dev/null +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -0,0 +1,152 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Enums; +using Newtonsoft.Json.Linq; +using Penumbra.String; + +namespace Penumbra.GameData.Actors; + +[StructLayout( LayoutKind.Explicit )] +public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > +{ + public static ActorManager? Manager; + + public static readonly ActorIdentifier Invalid = new(IdentifierType.Invalid, 0, 0, 0, ByteString.Empty); + + // @formatter:off + [FieldOffset( 0 )] public readonly IdentifierType Type; // All + [FieldOffset( 1 )] public readonly ObjectKind Kind; // Npc, Owned + [FieldOffset( 2 )] public readonly ushort HomeWorld; // Player, Owned + [FieldOffset( 2 )] public readonly ushort Index; // NPC + [FieldOffset( 2 )] public readonly SpecialActor Special; // Special + [FieldOffset( 4 )] public readonly uint DataId; // Owned, NPC + [FieldOffset( 8 )] public readonly ByteString PlayerName; // Player, Owned + // @formatter:on + + public ActorIdentifier CreatePermanent() + => new(Type, Kind, Index, DataId, PlayerName.Clone()); + + public bool Equals( ActorIdentifier other ) + { + if( Type != other.Type ) + { + return false; + } + + return Type switch + { + IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ), + IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ) && Manager.DataIdEquals( this, other ), + IdentifierType.Special => Special == other.Special, + IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals( this, other ), + _ => false, + }; + } + + public override bool Equals( object? obj ) + => obj is ActorIdentifier other && Equals( other ); + + public bool IsValid + => Type != IdentifierType.Invalid; + + public override string ToString() + => Manager?.ToString( this ) + ?? Type switch + { + IdentifierType.Player => $"{PlayerName} ({HomeWorld})", + IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", + IdentifierType.Special => ActorManager.ToName( Special ), + IdentifierType.Npc => + Index == ushort.MaxValue + ? $"{Kind} #{DataId}" + : $"{Kind} #{DataId} at {Index}", + _ => "Invalid", + }; + + public override int GetHashCode() + => Type switch + { + IdentifierType.Player => HashCode.Combine( IdentifierType.Player, PlayerName, HomeWorld ), + IdentifierType.Owned => HashCode.Combine( IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId ), + IdentifierType.Special => HashCode.Combine( IdentifierType.Special, Special ), + IdentifierType.Npc => HashCode.Combine( IdentifierType.Npc, Kind, Index, DataId ), + _ => 0, + }; + + internal ActorIdentifier( IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName ) + { + Type = type; + Kind = kind; + Special = ( SpecialActor )index; + HomeWorld = Index = index; + DataId = data; + PlayerName = playerName; + } + + + public JObject ToJson() + { + var ret = new JObject { { nameof( Type ), Type.ToString() } }; + switch( Type ) + { + case IdentifierType.Player: + ret.Add( nameof( PlayerName ), PlayerName.ToString() ); + ret.Add( nameof( HomeWorld ), HomeWorld ); + return ret; + case IdentifierType.Owned: + ret.Add( nameof( PlayerName ), PlayerName.ToString() ); + ret.Add( nameof( HomeWorld ), HomeWorld ); + ret.Add( nameof( Kind ), Kind.ToString() ); + ret.Add( nameof( DataId ), DataId ); + return ret; + case IdentifierType.Special: + ret.Add( nameof( Special ), Special.ToString() ); + return ret; + case IdentifierType.Npc: + ret.Add( nameof( Kind ), Kind.ToString() ); + ret.Add( nameof( Index ), Index ); + ret.Add( nameof( DataId ), DataId ); + return ret; + } + + return ret; + } +} + +public static class ActorManagerExtensions +{ + public static bool DataIdEquals( this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs ) + { + if( lhs.Kind != rhs.Kind ) + { + return false; + } + + if( lhs.DataId == rhs.DataId ) + { + return true; + } + + if( manager == null ) + { + return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; + } + + return lhs.Kind switch + { + ObjectKind.MountType => manager.Mounts.TryGetValue( lhs.DataId, out var lhsName ) + && manager.Mounts.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.Companion => manager.Companions.TryGetValue( lhs.DataId, out var lhsName ) + && manager.Companions.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.BattleNpc => manager.BNpcs.TryGetValue( lhs.DataId, out var lhsName ) + && manager.BNpcs.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + ObjectKind.EventNpc => manager.ENpcs.TryGetValue( lhs.DataId, out var lhsName ) + && manager.ENpcs.TryGetValue( rhs.DataId, out var rhsName ) + && lhsName.Equals( rhsName, StringComparison.OrdinalIgnoreCase ), + _ => false, + }; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Actors/ActorManager.cs b/Penumbra.GameData/Actors/ActorManager.cs new file mode 100644 index 00000000..f3a1936e --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Dalamud.Data; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using Newtonsoft.Json.Linq; +using Penumbra.String; + +namespace Penumbra.GameData.Actors; + +public class ActorManager +{ + private readonly ObjectTable _objects; + private readonly ClientState _clientState; + + public readonly IReadOnlyDictionary< ushort, string > Worlds; + public readonly IReadOnlyDictionary< uint, string > Mounts; + public readonly IReadOnlyDictionary< uint, string > Companions; + public readonly IReadOnlyDictionary< uint, string > BNpcs; + public readonly IReadOnlyDictionary< uint, string > ENpcs; + + public IEnumerable< KeyValuePair< ushort, string > > AllWorlds + => Worlds.OrderBy( kvp => kvp.Key ).Prepend( new KeyValuePair< ushort, string >( ushort.MaxValue, "Any World" ) ); + + private readonly Func< ushort, short > _toParentIdx; + + public ActorManager( ObjectTable objects, ClientState state, DataManager gameData, Func< ushort, short > toParentIdx ) + { + _objects = objects; + _clientState = state; + Worlds = gameData.GetExcelSheet< World >()! + .Where( w => w.IsPublic && !w.Name.RawData.IsEmpty ) + .ToDictionary( w => ( ushort )w.RowId, w => w.Name.ToString() ); + + Mounts = gameData.GetExcelSheet< Mount >()! + .Where( m => m.Singular.RawData.Length > 0 && m.Order >= 0 ) + .ToDictionary( m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( m.Singular.ToDalamudString().ToString() ) ); + Companions = gameData.GetExcelSheet< Companion >()! + .Where( c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue ) + .ToDictionary( c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( c.Singular.ToDalamudString().ToString() ) ); + + BNpcs = gameData.GetExcelSheet< BNpcName >()! + .Where( n => n.Singular.RawData.Length > 0 ) + .ToDictionary( n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( n.Singular.ToDalamudString().ToString() ) ); + + ENpcs = gameData.GetExcelSheet< ENpcResident >()! + .Where( e => e.Singular.RawData.Length > 0 ) + .ToDictionary( e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( e.Singular.ToDalamudString().ToString() ) ); + + _toParentIdx = toParentIdx; + + ActorIdentifier.Manager = this; + } + + public ActorIdentifier FromJson( JObject data ) + { + var type = data[ nameof( ActorIdentifier.Type ) ]?.Value< IdentifierType >() ?? IdentifierType.Invalid; + switch( type ) + { + case IdentifierType.Player: + { + var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); + var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; + return CreatePlayer( name, homeWorld ); + } + case IdentifierType.Owned: + { + var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); + var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; + var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; + var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; + return CreateOwned( name, homeWorld, kind, dataId ); + } + case IdentifierType.Special: + { + var special = data[ nameof( ActorIdentifier.Special ) ]?.Value< SpecialActor >() ?? 0; + return CreateSpecial( special ); + } + case IdentifierType.Npc: + { + var index = data[ nameof( ActorIdentifier.Index ) ]?.Value< ushort >() ?? 0; + var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; + var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; + return CreateNpc( kind, index, dataId ); + } + case IdentifierType.Invalid: + default: + return ActorIdentifier.Invalid; + } + } + + public string ToString( ActorIdentifier id ) + { + return id.Type switch + { + IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id + ? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})" + : id.PlayerName.ToString(), + IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id + ? $"{id.PlayerName} ({Worlds[ id.HomeWorld ]})'s {ToName( id.Kind, id.DataId )}" + : $"{id.PlayerName}s {ToName( id.Kind, id.DataId )}", + IdentifierType.Special => ToName( id.Special ), + IdentifierType.Npc => + id.Index == ushort.MaxValue + ? ToName( id.Kind, id.DataId ) + : $"{ToName( id.Kind, id.DataId )} at {id.Index}", + _ => "Invalid", + }; + } + + public static string ToName( SpecialActor actor ) + => actor switch + { + SpecialActor.CharacterScreen => "Character Screen Actor", + SpecialActor.ExamineScreen => "Examine Screen Actor", + SpecialActor.FittingRoom => "Fitting Room Actor", + SpecialActor.DyePreview => "Dye Preview Actor", + SpecialActor.Portrait => "Portrait Actor", + _ => "Invalid", + }; + + public string ToName( ObjectKind kind, uint dataId ) + => TryGetName( kind, dataId, out var ret ) ? ret : "Invalid"; + + public bool TryGetName( ObjectKind kind, uint dataId, [NotNullWhen( true )] out string? name ) + { + name = null; + return kind switch + { + ObjectKind.MountType => Mounts.TryGetValue( dataId, out name ), + ObjectKind.Companion => Companions.TryGetValue( dataId, out name ), + ObjectKind.BattleNpc => BNpcs.TryGetValue( dataId, out name ), + ObjectKind.EventNpc => ENpcs.TryGetValue( dataId, out name ), + _ => false, + }; + } + + public unsafe ActorIdentifier FromObject( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor ) + { + if( actor == null ) + { + return ActorIdentifier.Invalid; + } + + var idx = actor->ObjectIndex; + if( idx is >= ( ushort )SpecialActor.CutsceneStart and < ( ushort )SpecialActor.CutsceneEnd ) + { + var parentIdx = _toParentIdx( idx ); + if( parentIdx >= 0 ) + { + return FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )_objects.GetObjectAddress( parentIdx ) ); + } + } + else if( idx is >= ( ushort )SpecialActor.CharacterScreen and <= ( ushort )SpecialActor.Portrait ) + { + return CreateSpecial( ( SpecialActor )idx ); + } + + switch( ( ObjectKind )actor->ObjectKind ) + { + case ObjectKind.Player: + { + var name = new ByteString( actor->Name ); + var homeWorld = ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->HomeWorld; + return CreatePlayer( name, homeWorld ); + } + case ObjectKind.BattleNpc: + { + var ownerId = actor->OwnerID; + if( ownerId != 0xE0000000 ) + { + var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )( _objects.SearchById( ownerId )?.Address ?? IntPtr.Zero ); + if( owner == null ) + { + return ActorIdentifier.Invalid; + } + + var name = new ByteString( owner->GameObject.Name ); + var homeWorld = owner->HomeWorld; + return CreateOwned( name, homeWorld, ObjectKind.BattleNpc, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); + } + + return CreateNpc( ObjectKind.BattleNpc, actor->ObjectIndex, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); + } + case ObjectKind.EventNpc: return CreateNpc( ObjectKind.EventNpc, actor->ObjectIndex, actor->DataID ); + case ObjectKind.MountType: + case ObjectKind.Companion: + { + if( actor->ObjectIndex % 2 == 0 ) + { + return ActorIdentifier.Invalid; + } + + var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )_objects.GetObjectAddress( actor->ObjectIndex - 1 ); + if( owner == null ) + { + return ActorIdentifier.Invalid; + } + + var dataId = GetCompanionId( actor, &owner->GameObject ); + return CreateOwned( new ByteString( owner->GameObject.Name ), owner->HomeWorld, ( ObjectKind )actor->ObjectKind, dataId ); + } + default: return ActorIdentifier.Invalid; + } + } + + private unsafe uint GetCompanionId( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner ) + { + return ( ObjectKind )actor->ObjectKind switch + { + ObjectKind.MountType => *( ushort* )( ( byte* )owner + 0x668 ), + ObjectKind.Companion => *( ushort* )( ( byte* )actor + 0x1AAC ), + _ => actor->DataID, + }; + } + + public unsafe ActorIdentifier FromObject( GameObject? actor ) + => FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )( actor?.Address ?? IntPtr.Zero ) ); + + + public ActorIdentifier CreatePlayer( ByteString name, ushort homeWorld ) + { + if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( name ) ) + { + return ActorIdentifier.Invalid; + } + + return new ActorIdentifier( IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name ); + } + + public ActorIdentifier CreateSpecial( SpecialActor actor ) + { + if( !VerifySpecial( actor ) ) + { + return ActorIdentifier.Invalid; + } + + return new ActorIdentifier( IdentifierType.Special, ObjectKind.Player, ( ushort )actor, 0, ByteString.Empty ); + } + + public ActorIdentifier CreateNpc( ObjectKind kind, ushort index = ushort.MaxValue, uint data = uint.MaxValue ) + { + if( !VerifyIndex( index ) || !VerifyNpcData( kind, data ) ) + { + return ActorIdentifier.Invalid; + } + + return new ActorIdentifier( IdentifierType.Npc, kind, index, data, ByteString.Empty ); + } + + public ActorIdentifier CreateOwned( ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId ) + { + if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( ownerName ) || !VerifyOwnedData( kind, dataId ) ) + { + return ActorIdentifier.Invalid; + } + + return new ActorIdentifier( IdentifierType.Owned, kind, homeWorld, dataId, ownerName ); + } + + + ///

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